|
| 1 | +/*--------------------------------------------------------------------------------------------- |
| 2 | + * Atom++ — Sync |
| 3 | + * |
| 4 | + * Contributes the `atompp` authentication provider that the editor's BUILT-IN Settings Sync |
| 5 | + * uses (matched via product.json → configurationSync.store.authenticationProviders). Once |
| 6 | + * signed in + turned on, the editor syncs settings/keybindings/snippets/theme/MCP/UI-state to |
| 7 | + * the configured sync server using its own battle-tested engine — we add no sync logic here. |
| 8 | + * |
| 9 | + * Sign-in is a DEV flow for now (mints a local token); the real loopback+PKCE flow against |
| 10 | + * Atom++ Cloud (PRD §6.2.3) drops into createSession() later. See docs/atompp-sync-design.md. |
| 11 | + *--------------------------------------------------------------------------------------------*/ |
| 12 | +// @ts-check |
| 13 | +'use strict'; |
| 14 | + |
| 15 | +const vscode = require('vscode'); |
| 16 | +const S = require('./session'); |
| 17 | + |
| 18 | +const PROVIDER_ID = 'atompp'; |
| 19 | +const PROVIDER_LABEL = 'Atom++ Account'; |
| 20 | +const SECRET_PREFIX = 'atompp.sync.token.'; // + session.id → token in SecretStorage (keychain) |
| 21 | +const META_KEY = 'atompp.sync.sessions'; // globalState → [{id, account, scopes}] (no tokens) |
| 22 | + |
| 23 | +/** @type {vscode.ExtensionContext} */ |
| 24 | +let ctx; |
| 25 | +/** @type {vscode.StatusBarItem} */ |
| 26 | +let statusItem; |
| 27 | +const sessionsChanged = new vscode.EventEmitter(); |
| 28 | + |
| 29 | +/** Load full sessions: metadata from globalState + token from SecretStorage. */ |
| 30 | +async function loadSessions() { |
| 31 | + const meta = S.parseMeta(ctx.globalState.get(META_KEY, '[]')); |
| 32 | + const out = []; |
| 33 | + for (const m of meta) { |
| 34 | + const token = await ctx.secrets.get(SECRET_PREFIX + m.id); |
| 35 | + if (token) { out.push({ id: m.id, accessToken: token, account: m.account, scopes: m.scopes }); } |
| 36 | + } |
| 37 | + return out; |
| 38 | +} |
| 39 | + |
| 40 | +async function addSession(session) { |
| 41 | + const sessions = await loadSessions(); |
| 42 | + const next = sessions.filter(s => s.id !== session.id).concat([session]); |
| 43 | + await ctx.secrets.store(SECRET_PREFIX + session.id, session.accessToken); |
| 44 | + await ctx.globalState.update(META_KEY, S.serializeMeta(next)); |
| 45 | +} |
| 46 | + |
| 47 | +async function deleteSession(id) { |
| 48 | + const sessions = await loadSessions(); |
| 49 | + const removed = sessions.find(s => s.id === id) || null; |
| 50 | + const next = sessions.filter(s => s.id !== id); |
| 51 | + await ctx.secrets.delete(SECRET_PREFIX + id); |
| 52 | + await ctx.globalState.update(META_KEY, S.serializeMeta(next)); |
| 53 | + return removed; |
| 54 | +} |
| 55 | + |
| 56 | +async function currentSession() { |
| 57 | + const sessions = await loadSessions(); |
| 58 | + return sessions[0] || null; |
| 59 | +} |
| 60 | + |
| 61 | +/** @type {vscode.AuthenticationProvider} */ |
| 62 | +const provider = { |
| 63 | + onDidChangeSessions: sessionsChanged.event, |
| 64 | + async getSessions(scopes) { |
| 65 | + const sessions = await loadSessions(); |
| 66 | + return sessions.filter(s => S.scopesMatch(s, scopes ? Array.from(scopes) : undefined)); |
| 67 | + }, |
| 68 | + async createSession(scopes) { |
| 69 | + const email = await vscode.window.showInputBox({ |
| 70 | + title: 'Sign in to Atom++ Sync', |
| 71 | + prompt: 'Your Atom++ account email — dev sign-in for now (real OAuth/PKCE lands later)', |
| 72 | + placeHolder: 'you@example.com', |
| 73 | + ignoreFocusOut: true, |
| 74 | + validateInput: (v) => (S.validateEmail(v) ? undefined : 'Enter a valid email address') |
| 75 | + }); |
| 76 | + if (!email) { throw new Error('Atom++ Sync sign-in cancelled'); } |
| 77 | + const token = S.mintDevToken(email.trim()); |
| 78 | + const session = S.makeSession(email.trim(), token, scopes ? Array.from(scopes) : S.SCOPES); |
| 79 | + await addSession(session); |
| 80 | + sessionsChanged.fire({ added: [session], removed: [], changed: [] }); |
| 81 | + updateStatus(session); |
| 82 | + return session; |
| 83 | + }, |
| 84 | + async removeSession(sessionId) { |
| 85 | + const removed = await deleteSession(sessionId); |
| 86 | + if (removed) { sessionsChanged.fire({ added: [], removed: [removed], changed: [] }); } |
| 87 | + updateStatus(null); |
| 88 | + } |
| 89 | +}; |
| 90 | + |
| 91 | +function updateStatus(sessionOrNull) { |
| 92 | + if (!statusItem) { return; } |
| 93 | + if (!vscode.workspace.getConfiguration('atompp.sync').get('showStatusBar', true)) { |
| 94 | + statusItem.hide(); |
| 95 | + return; |
| 96 | + } |
| 97 | + if (sessionOrNull) { |
| 98 | + statusItem.text = '$(sync) ' + sessionOrNull.account.label; |
| 99 | + statusItem.tooltip = 'Atom++ Sync — signed in. Click for status.'; |
| 100 | + statusItem.command = 'atompp.sync.status'; |
| 101 | + } else { |
| 102 | + statusItem.text = '$(sync-ignored) Atom++ Sync'; |
| 103 | + statusItem.tooltip = 'Atom++ Sync — not signed in. Click to set up.'; |
| 104 | + statusItem.command = 'atompp.sync.setUp'; |
| 105 | + } |
| 106 | + statusItem.show(); |
| 107 | +} |
| 108 | + |
| 109 | +async function activate(context) { |
| 110 | + ctx = context; |
| 111 | + |
| 112 | + // Registered here (onStartupFinished) so the status bar reflects state at launch — but also |
| 113 | + // lazily on demand: when Settings Sync requests a session, the implicit |
| 114 | + // `onAuthenticationRequest:atompp` activation fires, so timing relative to sync is never a concern. |
| 115 | + context.subscriptions.push( |
| 116 | + vscode.authentication.registerAuthenticationProvider(PROVIDER_ID, PROVIDER_LABEL, provider, { supportsMultipleAccounts: false }) |
| 117 | + ); |
| 118 | + |
| 119 | + statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 50); |
| 120 | + context.subscriptions.push(statusItem); |
| 121 | + updateStatus(await currentSession()); |
| 122 | + |
| 123 | + context.subscriptions.push(vscode.commands.registerCommand('atompp.sync.signIn', async () => { |
| 124 | + const session = await vscode.authentication.getSession(PROVIDER_ID, S.SCOPES, { createIfNone: true }); |
| 125 | + if (session) { vscode.window.showInformationMessage('Atom++ Sync: signed in as ' + session.account.label + '.'); } |
| 126 | + })); |
| 127 | + |
| 128 | + context.subscriptions.push(vscode.commands.registerCommand('atompp.sync.setUp', async () => { |
| 129 | + const session = await vscode.authentication.getSession(PROVIDER_ID, S.SCOPES, { createIfNone: true }); |
| 130 | + if (!session) { return; } |
| 131 | + // Hand off to the editor's built-in Settings Sync (uses our provider + product.json store). |
| 132 | + try { |
| 133 | + await vscode.commands.executeCommand('workbench.userDataSync.actions.turnOn'); |
| 134 | + } catch (e) { |
| 135 | + vscode.window.showWarningMessage( |
| 136 | + 'Signed in. Now run "Settings Sync: Turn On" to start syncing. (' + (e && e.message ? e.message : String(e)) + ')' |
| 137 | + ); |
| 138 | + } |
| 139 | + })); |
| 140 | + |
| 141 | + context.subscriptions.push(vscode.commands.registerCommand('atompp.sync.signOut', async () => { |
| 142 | + const session = await currentSession(); |
| 143 | + if (!session) { vscode.window.showInformationMessage('Atom++ Sync: not signed in.'); return; } |
| 144 | + await provider.removeSession(session.id); |
| 145 | + vscode.window.showInformationMessage('Atom++ Sync: signed out.'); |
| 146 | + })); |
| 147 | + |
| 148 | + context.subscriptions.push(vscode.commands.registerCommand('atompp.sync.status', async () => { |
| 149 | + const session = await currentSession(); |
| 150 | + vscode.window.showInformationMessage( |
| 151 | + session |
| 152 | + ? 'Atom++ Sync: signed in as ' + session.account.label + '. Sync server is configured in product.json → configurationSync.store.' |
| 153 | + : 'Atom++ Sync: not signed in. Run "Atom++ Sync: Set Up Sync".' |
| 154 | + ); |
| 155 | + })); |
| 156 | + |
| 157 | + context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(async (e) => { |
| 158 | + if (e.affectsConfiguration('atompp.sync.showStatusBar')) { updateStatus(await currentSession()); } |
| 159 | + })); |
| 160 | +} |
| 161 | + |
| 162 | +function deactivate() { } |
| 163 | + |
| 164 | +module.exports = { activate, deactivate }; |
0 commit comments