@nforma.ai/nforma 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +1024 -0
- package/agents/qgsd-codebase-mapper.md +764 -0
- package/agents/qgsd-debugger.md +1201 -0
- package/agents/qgsd-executor.md +472 -0
- package/agents/qgsd-integration-checker.md +443 -0
- package/agents/qgsd-phase-researcher.md +502 -0
- package/agents/qgsd-plan-checker.md +643 -0
- package/agents/qgsd-planner.md +1182 -0
- package/agents/qgsd-project-researcher.md +621 -0
- package/agents/qgsd-quorum-orchestrator.md +628 -0
- package/agents/qgsd-quorum-slot-worker.md +41 -0
- package/agents/qgsd-quorum-synthesizer.md +133 -0
- package/agents/qgsd-quorum-test-worker.md +37 -0
- package/agents/qgsd-quorum-worker.md +161 -0
- package/agents/qgsd-research-synthesizer.md +239 -0
- package/agents/qgsd-roadmapper.md +660 -0
- package/agents/qgsd-verifier.md +628 -0
- package/bin/accept-debug-invariant.cjs +165 -0
- package/bin/account-manager.cjs +719 -0
- package/bin/aggregate-requirements.cjs +466 -0
- package/bin/analyze-assumptions.cjs +757 -0
- package/bin/analyze-state-space.cjs +921 -0
- package/bin/attribute-trace-divergence.cjs +150 -0
- package/bin/auth-drivers/gh-cli.cjs +93 -0
- package/bin/auth-drivers/index.cjs +46 -0
- package/bin/auth-drivers/pool.cjs +67 -0
- package/bin/auth-drivers/simple.cjs +95 -0
- package/bin/autoClosePtoF.cjs +110 -0
- package/bin/blessed-terminal.cjs +350 -0
- package/bin/build-phase-index.cjs +472 -0
- package/bin/call-quorum-slot.cjs +541 -0
- package/bin/ccr-secure-config.cjs +99 -0
- package/bin/ccr-secure-start.cjs +83 -0
- package/bin/check-bundled-sdks.cjs +177 -0
- package/bin/check-coverage-guard.cjs +112 -0
- package/bin/check-liveness-fairness.cjs +95 -0
- package/bin/check-mcp-health.cjs +123 -0
- package/bin/check-provider-health.cjs +395 -0
- package/bin/check-results-exit.cjs +24 -0
- package/bin/check-spec-sync.cjs +360 -0
- package/bin/check-trace-redaction.cjs +271 -0
- package/bin/check-trace-schema-drift.cjs +99 -0
- package/bin/compareDrift.cjs +21 -0
- package/bin/conformance-schema.cjs +12 -0
- package/bin/count-scenarios.cjs +420 -0
- package/bin/debt-dedup.cjs +144 -0
- package/bin/debt-ledger.cjs +61 -0
- package/bin/debt-retention.cjs +76 -0
- package/bin/debt-state-machine.cjs +80 -0
- package/bin/detect-coverage-gaps.cjs +204 -0
- package/bin/detect-project-intent.cjs +362 -0
- package/bin/export-prism-constants.cjs +164 -0
- package/bin/extract-annotations.cjs +633 -0
- package/bin/extractFormalExpected.cjs +104 -0
- package/bin/fingerprint-drift.cjs +24 -0
- package/bin/fingerprint-issue.cjs +46 -0
- package/bin/formal-core.cjs +519 -0
- package/bin/formal-ref-linker.cjs +141 -0
- package/bin/formal-test-sync.cjs +788 -0
- package/bin/generate-formal-specs.cjs +588 -0
- package/bin/generate-petri-net.cjs +397 -0
- package/bin/generate-phase-spec.cjs +249 -0
- package/bin/generate-proposed-changes.cjs +194 -0
- package/bin/generate-tla-cfg.cjs +122 -0
- package/bin/generate-traceability-matrix.cjs +701 -0
- package/bin/generate-triage-bundle.cjs +300 -0
- package/bin/gh-account-rotate.cjs +34 -0
- package/bin/initialize-model-registry.cjs +105 -0
- package/bin/install-formal-tools.cjs +382 -0
- package/bin/install.js +2424 -0
- package/bin/isNumericThreshold.cjs +34 -0
- package/bin/issue-classifier.cjs +151 -0
- package/bin/levenshtein.cjs +74 -0
- package/bin/lint-formal-models.cjs +580 -0
- package/bin/load-baseline-requirements.cjs +275 -0
- package/bin/manage-agents-core.cjs +815 -0
- package/bin/migrate-formal-dir.cjs +172 -0
- package/bin/migrate-planning.cjs +206 -0
- package/bin/migrate-to-slots.cjs +255 -0
- package/bin/nForma.cjs +2726 -0
- package/bin/observe-config.cjs +353 -0
- package/bin/observe-debt-writer.cjs +140 -0
- package/bin/observe-handler-grafana.cjs +128 -0
- package/bin/observe-handler-internal.cjs +301 -0
- package/bin/observe-handler-logstash.cjs +153 -0
- package/bin/observe-handler-prometheus.cjs +185 -0
- package/bin/observe-handlers.cjs +436 -0
- package/bin/observe-registry.cjs +131 -0
- package/bin/observe-render.cjs +168 -0
- package/bin/planning-paths.cjs +167 -0
- package/bin/polyrepo.cjs +560 -0
- package/bin/prism-priority.cjs +153 -0
- package/bin/probe-quorum-slots.cjs +167 -0
- package/bin/promote-model.cjs +225 -0
- package/bin/propose-debug-invariants.cjs +165 -0
- package/bin/providers.json +392 -0
- package/bin/pty-proxy.py +129 -0
- package/bin/qgsd-solve.cjs +2477 -0
- package/bin/quorum-consensus-gate.cjs +238 -0
- package/bin/quorum-formal-context.cjs +183 -0
- package/bin/quorum-slot-dispatch.cjs +934 -0
- package/bin/read-policy.cjs +60 -0
- package/bin/requirement-map.cjs +63 -0
- package/bin/requirements-core.cjs +247 -0
- package/bin/resolve-cli.cjs +101 -0
- package/bin/review-mcp-logs.cjs +294 -0
- package/bin/run-account-manager-tlc.cjs +188 -0
- package/bin/run-account-pool-alloy.cjs +158 -0
- package/bin/run-alloy.cjs +153 -0
- package/bin/run-audit-alloy.cjs +187 -0
- package/bin/run-breaker-tlc.cjs +181 -0
- package/bin/run-formal-check.cjs +395 -0
- package/bin/run-formal-verify.cjs +701 -0
- package/bin/run-installer-alloy.cjs +188 -0
- package/bin/run-oauth-rotation-prism.cjs +132 -0
- package/bin/run-oscillation-tlc.cjs +202 -0
- package/bin/run-phase-tlc.cjs +228 -0
- package/bin/run-prism.cjs +446 -0
- package/bin/run-protocol-tlc.cjs +201 -0
- package/bin/run-quorum-composition-alloy.cjs +155 -0
- package/bin/run-sensitivity-sweep.cjs +231 -0
- package/bin/run-stop-hook-tlc.cjs +188 -0
- package/bin/run-tlc.cjs +467 -0
- package/bin/run-transcript-alloy.cjs +173 -0
- package/bin/run-uppaal.cjs +264 -0
- package/bin/secrets.cjs +134 -0
- package/bin/sensitivity-report.cjs +219 -0
- package/bin/sensitivity-sweep-feedback.cjs +194 -0
- package/bin/set-secret.cjs +29 -0
- package/bin/setup-telemetry-cron.sh +36 -0
- package/bin/sweepPtoF.cjs +63 -0
- package/bin/sync-baseline-requirements.cjs +290 -0
- package/bin/task-envelope.cjs +360 -0
- package/bin/telemetry-collector.cjs +229 -0
- package/bin/unified-mcp-server.mjs +735 -0
- package/bin/update-agents.cjs +369 -0
- package/bin/update-scoreboard.cjs +1134 -0
- package/bin/validate-debt-entry.cjs +207 -0
- package/bin/validate-invariant.cjs +419 -0
- package/bin/validate-memory.cjs +389 -0
- package/bin/validate-requirements-haiku.cjs +435 -0
- package/bin/validate-traces.cjs +438 -0
- package/bin/verify-formal-results.cjs +124 -0
- package/bin/verify-quorum-health.cjs +273 -0
- package/bin/write-check-result.cjs +106 -0
- package/bin/xstate-to-tla.cjs +483 -0
- package/bin/xstate-trace-walker.cjs +205 -0
- package/commands/qgsd/add-phase.md +43 -0
- package/commands/qgsd/add-requirement.md +24 -0
- package/commands/qgsd/add-todo.md +47 -0
- package/commands/qgsd/audit-milestone.md +37 -0
- package/commands/qgsd/check-todos.md +45 -0
- package/commands/qgsd/cleanup.md +18 -0
- package/commands/qgsd/close-formal-gaps.md +33 -0
- package/commands/qgsd/complete-milestone.md +136 -0
- package/commands/qgsd/debug.md +166 -0
- package/commands/qgsd/discuss-phase.md +83 -0
- package/commands/qgsd/execute-phase.md +117 -0
- package/commands/qgsd/fix-tests.md +27 -0
- package/commands/qgsd/formal-test-sync.md +32 -0
- package/commands/qgsd/health.md +22 -0
- package/commands/qgsd/help.md +22 -0
- package/commands/qgsd/insert-phase.md +32 -0
- package/commands/qgsd/join-discord.md +18 -0
- package/commands/qgsd/list-phase-assumptions.md +46 -0
- package/commands/qgsd/map-codebase.md +71 -0
- package/commands/qgsd/map-requirements.md +20 -0
- package/commands/qgsd/mcp-restart.md +176 -0
- package/commands/qgsd/mcp-set-model.md +134 -0
- package/commands/qgsd/mcp-setup.md +1371 -0
- package/commands/qgsd/mcp-status.md +274 -0
- package/commands/qgsd/mcp-update.md +238 -0
- package/commands/qgsd/new-milestone.md +44 -0
- package/commands/qgsd/new-project.md +42 -0
- package/commands/qgsd/observe.md +260 -0
- package/commands/qgsd/pause-work.md +38 -0
- package/commands/qgsd/plan-milestone-gaps.md +34 -0
- package/commands/qgsd/plan-phase.md +44 -0
- package/commands/qgsd/polyrepo.md +50 -0
- package/commands/qgsd/progress.md +24 -0
- package/commands/qgsd/queue.md +54 -0
- package/commands/qgsd/quick.md +133 -0
- package/commands/qgsd/quorum-test.md +275 -0
- package/commands/qgsd/quorum.md +707 -0
- package/commands/qgsd/reapply-patches.md +110 -0
- package/commands/qgsd/remove-phase.md +31 -0
- package/commands/qgsd/research-phase.md +189 -0
- package/commands/qgsd/resume-work.md +40 -0
- package/commands/qgsd/set-profile.md +34 -0
- package/commands/qgsd/settings.md +39 -0
- package/commands/qgsd/solve.md +565 -0
- package/commands/qgsd/sync-baselines.md +119 -0
- package/commands/qgsd/triage.md +233 -0
- package/commands/qgsd/update.md +37 -0
- package/commands/qgsd/verify-work.md +38 -0
- package/hooks/dist/config-loader.js +297 -0
- package/hooks/dist/conformance-schema.cjs +12 -0
- package/hooks/dist/gsd-context-monitor.js +64 -0
- package/hooks/dist/qgsd-check-update.js +62 -0
- package/hooks/dist/qgsd-circuit-breaker.js +682 -0
- package/hooks/dist/qgsd-precompact.js +156 -0
- package/hooks/dist/qgsd-prompt.js +653 -0
- package/hooks/dist/qgsd-session-start.js +122 -0
- package/hooks/dist/qgsd-slot-correlator.js +58 -0
- package/hooks/dist/qgsd-spec-regen.js +86 -0
- package/hooks/dist/qgsd-statusline.js +91 -0
- package/hooks/dist/qgsd-stop.js +553 -0
- package/hooks/dist/qgsd-token-collector.js +133 -0
- package/hooks/dist/unified-mcp-server.mjs +669 -0
- package/package.json +95 -0
- package/scripts/build-hooks.js +46 -0
- package/scripts/postinstall.js +48 -0
- package/scripts/secret-audit.sh +45 -0
- package/templates/qgsd.json +49 -0
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* account-manager.cjs — OAuth account pool manager for QGSD providers
|
|
6
|
+
*
|
|
7
|
+
* Manages multiple OAuth credentials for providers with oauth_rotation config.
|
|
8
|
+
* The implementation is a state machine that directly mirrors
|
|
9
|
+
* .planning/formal/tla/QGSDAccountManager.tla — each TLA+ action is one FSM event.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* node bin/account-manager.cjs add --login [--name alias] [--provider gemini-1]
|
|
13
|
+
* node bin/account-manager.cjs add --name user@gmail.com [--provider gemini-1]
|
|
14
|
+
* node bin/account-manager.cjs list [--provider gemini-1]
|
|
15
|
+
* node bin/account-manager.cjs switch next|prev|<name>|<N> [--provider gemini-1]
|
|
16
|
+
* node bin/account-manager.cjs remove <name> [--provider gemini-1]
|
|
17
|
+
* node bin/account-manager.cjs status [--provider gemini-1]
|
|
18
|
+
*
|
|
19
|
+
* Credential layout (configurable via oauth_rotation in providers.json):
|
|
20
|
+
* active_file — ~/.gemini/oauth_creds.json (the live credential Gemini CLI reads)
|
|
21
|
+
* creds_dir — ~/.gemini/accounts/ (pool: one .json per account)
|
|
22
|
+
* active_ptr — ~/.gemini/accounts/.qgsd-active (sidecar: name of active account)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
const os = require('os');
|
|
28
|
+
const { spawn } = require('child_process');
|
|
29
|
+
|
|
30
|
+
// ─── FSM states and events (mirrors QGSDAccountManager.tla) ──────────────────
|
|
31
|
+
|
|
32
|
+
const S = Object.freeze({
|
|
33
|
+
IDLE: 'IDLE',
|
|
34
|
+
ADDING: 'ADDING',
|
|
35
|
+
SAVING: 'SAVING',
|
|
36
|
+
SWITCHING: 'SWITCHING',
|
|
37
|
+
REMOVING: 'REMOVING',
|
|
38
|
+
ERROR: 'ERROR',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const E = Object.freeze({
|
|
42
|
+
ADD: 'ADD',
|
|
43
|
+
OAUTH_SUCCESS: 'OAUTH_SUCCESS',
|
|
44
|
+
OAUTH_FAIL: 'OAUTH_FAIL',
|
|
45
|
+
WRITE_OK: 'WRITE_OK',
|
|
46
|
+
WRITE_FAIL: 'WRITE_FAIL',
|
|
47
|
+
SWITCH: 'SWITCH',
|
|
48
|
+
SWAP_OK: 'SWAP_OK',
|
|
49
|
+
SWAP_FAIL: 'SWAP_FAIL',
|
|
50
|
+
REMOVE: 'REMOVE',
|
|
51
|
+
RM_OK: 'RM_OK',
|
|
52
|
+
RM_FAIL: 'RM_FAIL',
|
|
53
|
+
RESET: 'RESET',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Transition table derived from QGSDAccountManager.tla Next relation.
|
|
57
|
+
// TRANSITIONS[currentState][event] = nextState
|
|
58
|
+
const TRANSITIONS = {
|
|
59
|
+
[S.IDLE]: {
|
|
60
|
+
[E.ADD]: S.ADDING,
|
|
61
|
+
[E.SWITCH]: S.SWITCHING,
|
|
62
|
+
[E.REMOVE]: S.REMOVING,
|
|
63
|
+
},
|
|
64
|
+
[S.ADDING]: {
|
|
65
|
+
[E.OAUTH_SUCCESS]: S.SAVING,
|
|
66
|
+
[E.OAUTH_FAIL]: S.ERROR,
|
|
67
|
+
},
|
|
68
|
+
[S.SAVING]: {
|
|
69
|
+
[E.WRITE_OK]: S.IDLE,
|
|
70
|
+
[E.WRITE_FAIL]: S.ERROR,
|
|
71
|
+
},
|
|
72
|
+
[S.SWITCHING]: {
|
|
73
|
+
[E.SWAP_OK]: S.IDLE,
|
|
74
|
+
[E.SWAP_FAIL]: S.ERROR,
|
|
75
|
+
},
|
|
76
|
+
[S.REMOVING]: {
|
|
77
|
+
[E.RM_OK]: S.IDLE,
|
|
78
|
+
[E.RM_FAIL]: S.ERROR,
|
|
79
|
+
},
|
|
80
|
+
[S.ERROR]: {
|
|
81
|
+
[E.RESET]: S.IDLE,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
class AccountManagerFSM {
|
|
86
|
+
constructor() {
|
|
87
|
+
this.state = S.IDLE;
|
|
88
|
+
this.pendingOp = null; // { type: string, target: string } — mirrors TLA+ pending_op
|
|
89
|
+
this.errorMsg = null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// send(event, payload?) — drives the FSM one step.
|
|
93
|
+
// Mutates this.state and this.pendingOp per the transition table.
|
|
94
|
+
// Throws on invalid transitions (maps to TLA+ TypeOK violation).
|
|
95
|
+
send(event, payload = {}) {
|
|
96
|
+
const next = TRANSITIONS[this.state]?.[event];
|
|
97
|
+
if (!next) throw new Error(`[FSM] Invalid: ${this.state} + ${event}`);
|
|
98
|
+
|
|
99
|
+
if (payload.target !== undefined) {
|
|
100
|
+
this.pendingOp = { type: event, target: payload.target };
|
|
101
|
+
}
|
|
102
|
+
if (next === S.IDLE) { this.pendingOp = null; this.errorMsg = null; }
|
|
103
|
+
if (next === S.ERROR) { this.errorMsg = payload.error ?? 'unknown error'; }
|
|
104
|
+
this.state = next;
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── Provider resolution ──────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
function expandHome(p) {
|
|
112
|
+
return typeof p === 'string' && p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : p;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function findProviders() {
|
|
116
|
+
const search = [
|
|
117
|
+
path.join(__dirname, 'providers.json'),
|
|
118
|
+
path.join(os.homedir(), '.claude', 'qgsd-bin', 'providers.json'),
|
|
119
|
+
];
|
|
120
|
+
try {
|
|
121
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8'));
|
|
122
|
+
const u1 = cfg?.mcpServers?.['unified-1']?.args ?? [];
|
|
123
|
+
const srv = u1.find(a => typeof a === 'string' && a.endsWith('unified-mcp-server.mjs'));
|
|
124
|
+
if (srv) search.unshift(path.join(path.dirname(srv), 'providers.json'));
|
|
125
|
+
} catch (_) {}
|
|
126
|
+
for (const p of search) {
|
|
127
|
+
try { if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf8')).providers; } catch (_) {}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function resolveProvider(providerArg) {
|
|
133
|
+
const providers = findProviders();
|
|
134
|
+
if (!providers) die('Could not find providers.json');
|
|
135
|
+
if (providerArg) {
|
|
136
|
+
const p = providers.find(p => p.name === providerArg);
|
|
137
|
+
if (!p) die(`Unknown provider: ${providerArg}`);
|
|
138
|
+
if (!p.oauth_rotation) die(`Provider "${providerArg}" has no oauth_rotation config`);
|
|
139
|
+
return p;
|
|
140
|
+
}
|
|
141
|
+
const p = providers.find(p => p.oauth_rotation?.enabled);
|
|
142
|
+
if (!p) die('No OAuth-enabled provider found in providers.json');
|
|
143
|
+
return p;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ─── Login + identity helpers ─────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
// Spawns `<provider.cli> auth login` with fully inherited stdio so the browser
|
|
149
|
+
// redirect URL and confirmation prompts appear inline in the user's terminal.
|
|
150
|
+
function spawnInteractiveLogin(provider) {
|
|
151
|
+
return new Promise((resolve, reject) => {
|
|
152
|
+
console.log(`\n Launching ${provider.cli} auth login …\n`);
|
|
153
|
+
const child = spawn(provider.cli, ['auth', 'login'], { stdio: 'inherit' });
|
|
154
|
+
child.on('close', (code) => {
|
|
155
|
+
if (code !== 0) reject(new Error(`auth login exited ${code}`));
|
|
156
|
+
else resolve();
|
|
157
|
+
});
|
|
158
|
+
child.on('error', reject);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Decodes the id_token JWT (no network call) to extract the Google account email.
|
|
163
|
+
// id_token = <header>.<payload>.<sig> — payload is base64url-encoded JSON.
|
|
164
|
+
function extractEmailFromCreds(activeFile) {
|
|
165
|
+
try {
|
|
166
|
+
const creds = JSON.parse(fs.readFileSync(activeFile, 'utf8'));
|
|
167
|
+
const jwt = creds.id_token;
|
|
168
|
+
if (!jwt) return null;
|
|
169
|
+
const payload = Buffer.from(jwt.split('.')[1], 'base64url').toString('utf8');
|
|
170
|
+
return JSON.parse(payload).email ?? null;
|
|
171
|
+
} catch (_) { return null; }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ─── Credential pool helpers ──────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
function getCredsDir(provider) {
|
|
177
|
+
return expandHome(provider.oauth_rotation?.creds_dir ?? '~/.gemini/accounts');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function getActiveFile(provider) {
|
|
181
|
+
return expandHome(provider.oauth_rotation?.active_file ?? '~/.gemini/oauth_creds.json');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Sidecar pointer file — stores just the account name of the current active.
|
|
185
|
+
// Avoids content-diffing which breaks when tokens are silently refreshed.
|
|
186
|
+
function getActivePtr(credsDir) {
|
|
187
|
+
return path.join(credsDir, '.qgsd-active');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function listPool(credsDir) {
|
|
191
|
+
if (!fs.existsSync(credsDir)) return [];
|
|
192
|
+
return fs.readdirSync(credsDir)
|
|
193
|
+
.filter(f => f.endsWith('.json'))
|
|
194
|
+
.map(f => f.replace(/\.json$/, ''))
|
|
195
|
+
.sort();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function readActivePtr(credsDir) {
|
|
199
|
+
const ptr = getActivePtr(credsDir);
|
|
200
|
+
if (!fs.existsSync(ptr)) return null;
|
|
201
|
+
return fs.readFileSync(ptr, 'utf8').trim() || null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function writeActivePtr(credsDir, name) {
|
|
205
|
+
fs.mkdirSync(credsDir, { recursive: true });
|
|
206
|
+
fs.writeFileSync(getActivePtr(credsDir), name, 'utf8');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function clearActivePtr(credsDir) {
|
|
210
|
+
const ptr = getActivePtr(credsDir);
|
|
211
|
+
if (fs.existsSync(ptr)) fs.rmSync(ptr);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ─── Commands (each drives the FSM through the appropriate TLA+ action sequence)
|
|
215
|
+
|
|
216
|
+
// add: StartAdd → OAuthSuccess → SaveOk (or OAuthFail / SaveFail → ERROR)
|
|
217
|
+
//
|
|
218
|
+
// Two modes:
|
|
219
|
+
// --login Spawns `<provider.cli> auth login` inline (inherited stdio),
|
|
220
|
+
// then auto-extracts email from the id_token JWT. No --name needed.
|
|
221
|
+
// (no flag) Captures the current active credential. --name is required.
|
|
222
|
+
async function cmdAdd(fsm, provider, nameArg, doLogin) {
|
|
223
|
+
const credsDir = getCredsDir(provider);
|
|
224
|
+
const activeFile = getActiveFile(provider);
|
|
225
|
+
|
|
226
|
+
// Placeholder target for the FSM until we know the real name
|
|
227
|
+
fsm.send(E.ADD, { target: nameArg ?? '__pending__' }); // IDLE → ADDING
|
|
228
|
+
|
|
229
|
+
if (doLogin) {
|
|
230
|
+
try {
|
|
231
|
+
await spawnInteractiveLogin(provider);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
fsm.send(E.OAUTH_FAIL, { error: err.message });
|
|
234
|
+
die(`Login failed: ${fsm.errorMsg}`);
|
|
235
|
+
}
|
|
236
|
+
} else if (!fs.existsSync(activeFile)) {
|
|
237
|
+
fsm.send(E.OAUTH_FAIL, { error: `Active credential not found: ${activeFile}` });
|
|
238
|
+
die(`${fsm.errorMsg}\n Run with --login to authenticate, or run \`${provider.cli} auth login\` first.`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Resolve name: explicit --name overrides auto-detection
|
|
242
|
+
const name = nameArg ?? extractEmailFromCreds(activeFile);
|
|
243
|
+
if (!name) {
|
|
244
|
+
fsm.send(E.OAUTH_FAIL, { error: 'Could not detect email from id_token' });
|
|
245
|
+
die(`${fsm.errorMsg}\n Re-run with --name <email> to set the account name manually.`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (doLogin) console.log(` Detected account: ${name}`);
|
|
249
|
+
fsm.send(E.OAUTH_SUCCESS); // ADDING → SAVING
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
fs.mkdirSync(credsDir, { recursive: true });
|
|
253
|
+
const dest = path.join(credsDir, `${name}.json`);
|
|
254
|
+
if (fs.existsSync(dest)) {
|
|
255
|
+
process.stderr.write(` [warn] Pool already contains "${name}" — overwriting.\n`);
|
|
256
|
+
}
|
|
257
|
+
fs.copyFileSync(activeFile, dest);
|
|
258
|
+
|
|
259
|
+
const pool = listPool(credsDir);
|
|
260
|
+
const isFirstAccount = pool.length === 1; // pool now contains this file
|
|
261
|
+
if (isFirstAccount || readActivePtr(credsDir) === null) {
|
|
262
|
+
writeActivePtr(credsDir, name);
|
|
263
|
+
} else if (doLogin) {
|
|
264
|
+
// --login flow: newly added account becomes active (user just authenticated it)
|
|
265
|
+
writeActivePtr(credsDir, name);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
fsm.send(E.WRITE_OK); // SAVING → IDLE
|
|
269
|
+
console.log(` ✓ "${name}" added to pool`);
|
|
270
|
+
if (readActivePtr(credsDir) === name) {
|
|
271
|
+
console.log(` ✓ Set as active account`);
|
|
272
|
+
}
|
|
273
|
+
} catch (err) {
|
|
274
|
+
fsm.send(E.WRITE_FAIL, { error: err.message });
|
|
275
|
+
die(`Save failed: ${fsm.errorMsg}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// list: read-only, no FSM transitions needed
|
|
280
|
+
function cmdList(provider) {
|
|
281
|
+
const credsDir = getCredsDir(provider);
|
|
282
|
+
const pool = listPool(credsDir);
|
|
283
|
+
const active = readActivePtr(credsDir);
|
|
284
|
+
|
|
285
|
+
if (pool.length === 0) {
|
|
286
|
+
console.log(` (pool empty — run \`add --login\` to add your first account)`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
console.log(` OAuth pool for ${provider.name} (${provider.display_provider ?? provider.name}):`);
|
|
290
|
+
pool.forEach((name, i) => {
|
|
291
|
+
const marker = name === active ? '●' : ' ';
|
|
292
|
+
console.log(` ${marker} ${i + 1}. ${name}`);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// switch: StartSwitch → SwapOk (or SwapFail → ERROR)
|
|
297
|
+
function cmdSwitch(fsm, provider, target) {
|
|
298
|
+
const credsDir = getCredsDir(provider);
|
|
299
|
+
const activeFile = getActiveFile(provider);
|
|
300
|
+
const pool = listPool(credsDir);
|
|
301
|
+
|
|
302
|
+
if (pool.length === 0) die('Pool is empty — nothing to switch to');
|
|
303
|
+
|
|
304
|
+
let targetName;
|
|
305
|
+
if (target === 'next' || target === 'prev') {
|
|
306
|
+
const active = readActivePtr(credsDir);
|
|
307
|
+
const idx = active ? pool.indexOf(active) : -1;
|
|
308
|
+
if (pool.length === 1) {
|
|
309
|
+
console.log(` (only one account in pool — already on "${pool[0]}")`);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
targetName = target === 'next'
|
|
313
|
+
? pool[(idx + 1) % pool.length]
|
|
314
|
+
: pool[(idx - 1 + pool.length) % pool.length];
|
|
315
|
+
} else if (/^\d+$/.test(target)) {
|
|
316
|
+
const idx = parseInt(target, 10) - 1;
|
|
317
|
+
if (idx < 0 || idx >= pool.length) die(`Index ${target} out of range (1–${pool.length})`);
|
|
318
|
+
targetName = pool[idx];
|
|
319
|
+
} else {
|
|
320
|
+
if (!pool.includes(target)) die(`Account "${target}" not in pool`);
|
|
321
|
+
targetName = target;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
fsm.send(E.SWITCH, { target: targetName }); // IDLE → SWITCHING
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
fs.copyFileSync(path.join(credsDir, `${targetName}.json`), activeFile);
|
|
328
|
+
writeActivePtr(credsDir, targetName);
|
|
329
|
+
fsm.send(E.SWAP_OK); // SWITCHING → IDLE
|
|
330
|
+
console.log(` ✓ Switched to "${targetName}"`);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
fsm.send(E.SWAP_FAIL, { error: err.message });
|
|
333
|
+
die(`Switch failed: ${fsm.errorMsg}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// remove: StartRemove → RemoveOk (or RemoveFail → ERROR)
|
|
338
|
+
function cmdRemove(fsm, provider, name) {
|
|
339
|
+
const credsDir = getCredsDir(provider);
|
|
340
|
+
const activeFile = getActiveFile(provider);
|
|
341
|
+
const pool = listPool(credsDir);
|
|
342
|
+
|
|
343
|
+
if (!pool.includes(name)) die(`Account "${name}" not in pool`);
|
|
344
|
+
|
|
345
|
+
fsm.send(E.REMOVE, { target: name }); // IDLE → REMOVING
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
const active = readActivePtr(credsDir);
|
|
349
|
+
const remaining = pool.filter(a => a !== name);
|
|
350
|
+
|
|
351
|
+
// If removing active account, rotate to next remaining (mirrors TLA+ RemoveOk CHOOSE)
|
|
352
|
+
if (active === name) {
|
|
353
|
+
if (remaining.length > 0) {
|
|
354
|
+
const next = remaining[0];
|
|
355
|
+
fs.copyFileSync(path.join(credsDir, `${next}.json`), activeFile);
|
|
356
|
+
writeActivePtr(credsDir, next);
|
|
357
|
+
console.log(` → Rotated active to "${next}"`);
|
|
358
|
+
} else {
|
|
359
|
+
clearActivePtr(credsDir);
|
|
360
|
+
console.log(` ⚠ Removed last account — active credential unchanged on disk`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
fs.rmSync(path.join(credsDir, `${name}.json`));
|
|
365
|
+
fsm.send(E.RM_OK); // REMOVING → IDLE
|
|
366
|
+
console.log(` ✓ Removed "${name}" from pool`);
|
|
367
|
+
} catch (err) {
|
|
368
|
+
fsm.send(E.RM_FAIL, { error: err.message });
|
|
369
|
+
die(`Remove failed: ${fsm.errorMsg}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// status: read-only
|
|
374
|
+
function cmdStatus(provider) {
|
|
375
|
+
const credsDir = getCredsDir(provider);
|
|
376
|
+
const activeFile = getActiveFile(provider);
|
|
377
|
+
const pool = listPool(credsDir);
|
|
378
|
+
const active = readActivePtr(credsDir);
|
|
379
|
+
const rot = provider.oauth_rotation;
|
|
380
|
+
|
|
381
|
+
console.log(` Provider : ${provider.name} (${provider.display_provider ?? provider.name})`);
|
|
382
|
+
console.log(` Pool dir : ${credsDir} (${pool.length} account${pool.length !== 1 ? 's' : ''})`);
|
|
383
|
+
console.log(` Active : ${active ?? '(none)'}`);
|
|
384
|
+
console.log(` Active file : ${activeFile} ${fs.existsSync(activeFile) ? '✓' : '✗ missing'}`);
|
|
385
|
+
console.log(` Max retries : ${rot?.max_retries ?? 3}`);
|
|
386
|
+
console.log(` Rotate cmd : ${(rot?.rotate_cmd ?? []).join(' ')}`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ─── Utilities ────────────────────────────────────────────────────────────────
|
|
390
|
+
|
|
391
|
+
// ─── TUI ──────────────────────────────────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
const C = {
|
|
394
|
+
reset: '\x1b[0m',
|
|
395
|
+
bold: '\x1b[1m',
|
|
396
|
+
dim: '\x1b[2m',
|
|
397
|
+
green: '\x1b[32m',
|
|
398
|
+
yellow: '\x1b[33m',
|
|
399
|
+
cyan: '\x1b[36m',
|
|
400
|
+
red: '\x1b[31m',
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
function printAccountsHeader(provider, pool, active) {
|
|
405
|
+
const tag = `QGSD · Accounts · ${provider.name} · ${provider.display_provider ?? ''}`;
|
|
406
|
+
const border = '─'.repeat(tag.length + 4);
|
|
407
|
+
console.log('');
|
|
408
|
+
console.log(` ${C.cyan}╭${border}╮${C.reset}`);
|
|
409
|
+
console.log(` ${C.cyan}│${C.reset} ${C.bold}${tag}${C.reset} ${C.cyan}│${C.reset}`);
|
|
410
|
+
console.log(` ${C.cyan}╰${border}╯${C.reset}`);
|
|
411
|
+
console.log('');
|
|
412
|
+
if (pool.length === 0) {
|
|
413
|
+
console.log(` ${C.dim} (pool is empty)${C.reset}`);
|
|
414
|
+
} else {
|
|
415
|
+
pool.forEach((name, i) => {
|
|
416
|
+
const isActive = name === active;
|
|
417
|
+
const dot = isActive ? `${C.green}●${C.reset}` : `${C.dim}○${C.reset}`;
|
|
418
|
+
const label = isActive
|
|
419
|
+
? `${C.bold}${name}${C.reset} ${C.dim}(active)${C.reset}`
|
|
420
|
+
: `${C.dim}${name}${C.reset}`;
|
|
421
|
+
console.log(` ${dot} ${i + 1}. ${label}`);
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
console.log('');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function tuiFlowAdd(provider, inquirer) {
|
|
428
|
+
const { doLogin } = await inquirer.prompt([{
|
|
429
|
+
type: 'confirm',
|
|
430
|
+
name: 'doLogin',
|
|
431
|
+
message: 'Launch Google OAuth login? (opens a browser)',
|
|
432
|
+
default: true,
|
|
433
|
+
prefix: ' ',
|
|
434
|
+
}]);
|
|
435
|
+
if (!doLogin) return;
|
|
436
|
+
|
|
437
|
+
try {
|
|
438
|
+
await spawnInteractiveLogin(provider);
|
|
439
|
+
} catch (err) {
|
|
440
|
+
console.log(`\n ${C.red}✗${C.reset} Login failed: ${err.message}`);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const detectedEmail = extractEmailFromCreds(getActiveFile(provider));
|
|
445
|
+
let name;
|
|
446
|
+
|
|
447
|
+
if (detectedEmail) {
|
|
448
|
+
console.log(`\n ${C.green}✓${C.reset} Detected: ${C.bold}${detectedEmail}${C.reset}`);
|
|
449
|
+
const { useDetected } = await inquirer.prompt([{
|
|
450
|
+
type: 'confirm',
|
|
451
|
+
name: 'useDetected',
|
|
452
|
+
message: `Save as "${detectedEmail}"?`,
|
|
453
|
+
default: true,
|
|
454
|
+
prefix: ' ',
|
|
455
|
+
}]);
|
|
456
|
+
if (useDetected) {
|
|
457
|
+
name = detectedEmail;
|
|
458
|
+
} else {
|
|
459
|
+
const { alias } = await inquirer.prompt([{
|
|
460
|
+
type: 'input',
|
|
461
|
+
name: 'alias',
|
|
462
|
+
message: 'Enter an alias for this account:',
|
|
463
|
+
prefix: ' ',
|
|
464
|
+
}]);
|
|
465
|
+
name = alias.trim();
|
|
466
|
+
}
|
|
467
|
+
} else {
|
|
468
|
+
const { alias } = await inquirer.prompt([{
|
|
469
|
+
type: 'input',
|
|
470
|
+
name: 'alias',
|
|
471
|
+
message: 'Could not detect email — enter a name for this account:',
|
|
472
|
+
prefix: ' ',
|
|
473
|
+
}]);
|
|
474
|
+
name = alias.trim();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (!name) { console.log(` ${C.yellow}⚠${C.reset} Cancelled`); return; }
|
|
478
|
+
|
|
479
|
+
const credsDir = getCredsDir(provider);
|
|
480
|
+
if (listPool(credsDir).includes(name)) {
|
|
481
|
+
const { overwrite } = await inquirer.prompt([{
|
|
482
|
+
type: 'confirm',
|
|
483
|
+
name: 'overwrite',
|
|
484
|
+
message: `"${name}" already in pool — overwrite?`,
|
|
485
|
+
default: false,
|
|
486
|
+
prefix: ' ',
|
|
487
|
+
}]);
|
|
488
|
+
if (!overwrite) return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Drive FSM through the same transitions as cmdAdd (IDLE→ADDING→SAVING→IDLE)
|
|
492
|
+
// so all TLA+ invariants (ActiveIsPoolMember, NoActiveWhenEmpty) are enforced.
|
|
493
|
+
const fsm = new AccountManagerFSM();
|
|
494
|
+
fsm.send(E.ADD, { target: name }); // IDLE → ADDING (name now resolved)
|
|
495
|
+
fsm.send(E.OAUTH_SUCCESS); // ADDING → SAVING (login already done)
|
|
496
|
+
|
|
497
|
+
try {
|
|
498
|
+
const credsDir = getCredsDir(provider);
|
|
499
|
+
const activeFile = getActiveFile(provider);
|
|
500
|
+
fs.mkdirSync(credsDir, { recursive: true });
|
|
501
|
+
fs.copyFileSync(activeFile, path.join(credsDir, `${name}.json`));
|
|
502
|
+
const pool = listPool(credsDir);
|
|
503
|
+
const isFirst = pool.length === 1;
|
|
504
|
+
if (isFirst || readActivePtr(credsDir) === null) writeActivePtr(credsDir, name);
|
|
505
|
+
else writeActivePtr(credsDir, name); // TUI add-with-login → new account is active
|
|
506
|
+
fsm.send(E.WRITE_OK); // SAVING → IDLE
|
|
507
|
+
console.log(`\n ${C.green}✓${C.reset} "${name}" added to pool`);
|
|
508
|
+
if (readActivePtr(credsDir) === name) console.log(` ${C.green}✓${C.reset} Set as active account`);
|
|
509
|
+
} catch (err) {
|
|
510
|
+
fsm.send(E.WRITE_FAIL, { error: err.message }); // SAVING → ERROR
|
|
511
|
+
console.log(`\n ${C.red}✗${C.reset} Save failed: ${err.message}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async function tuiFlowSwitch(provider, pool, active, inquirer) {
|
|
516
|
+
const choices = pool.map(name => ({
|
|
517
|
+
name: name === active
|
|
518
|
+
? `${name} ${C.dim}(active)${C.reset}`
|
|
519
|
+
: `${C.dim}${name}${C.reset}`,
|
|
520
|
+
value: name,
|
|
521
|
+
}));
|
|
522
|
+
choices.push(new inquirer.Separator(' ─────────────────────────────'));
|
|
523
|
+
choices.push({ name: ` ${C.dim}Cancel${C.reset}`, value: null });
|
|
524
|
+
|
|
525
|
+
const { target } = await inquirer.prompt([{
|
|
526
|
+
type: 'list',
|
|
527
|
+
name: 'target',
|
|
528
|
+
message: 'Switch to',
|
|
529
|
+
choices,
|
|
530
|
+
prefix: ' ',
|
|
531
|
+
pageSize: 15,
|
|
532
|
+
}]);
|
|
533
|
+
|
|
534
|
+
if (!target || target === active) return;
|
|
535
|
+
|
|
536
|
+
// Drive FSM through IDLE→SWITCHING→IDLE, same as cmdSwitch,
|
|
537
|
+
// so ActiveIsPoolMember is preserved on partial failures.
|
|
538
|
+
const fsm = new AccountManagerFSM();
|
|
539
|
+
const credsDir = getCredsDir(provider);
|
|
540
|
+
const activeFile = getActiveFile(provider);
|
|
541
|
+
fsm.send(E.SWITCH, { target }); // IDLE → SWITCHING
|
|
542
|
+
|
|
543
|
+
try {
|
|
544
|
+
fs.copyFileSync(path.join(credsDir, `${target}.json`), activeFile);
|
|
545
|
+
writeActivePtr(credsDir, target);
|
|
546
|
+
fsm.send(E.SWAP_OK); // SWITCHING → IDLE
|
|
547
|
+
console.log(`\n ${C.green}✓${C.reset} Switched to ${C.bold}"${target}"${C.reset}`);
|
|
548
|
+
} catch (err) {
|
|
549
|
+
fsm.send(E.SWAP_FAIL, { error: err.message }); // SWITCHING → ERROR
|
|
550
|
+
console.log(`\n ${C.red}✗${C.reset} Switch failed: ${err.message}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async function tuiFlowRemove(provider, pool, active, inquirer) {
|
|
555
|
+
const choices = pool.map(name => ({
|
|
556
|
+
name: name === active
|
|
557
|
+
? `${name} ${C.dim}(active)${C.reset}`
|
|
558
|
+
: `${C.dim}${name}${C.reset}`,
|
|
559
|
+
value: name,
|
|
560
|
+
}));
|
|
561
|
+
choices.push(new inquirer.Separator(' ─────────────────────────────'));
|
|
562
|
+
choices.push({ name: ` ${C.dim}Cancel${C.reset}`, value: null });
|
|
563
|
+
|
|
564
|
+
const { target } = await inquirer.prompt([{
|
|
565
|
+
type: 'list',
|
|
566
|
+
name: 'target',
|
|
567
|
+
message: 'Remove which account?',
|
|
568
|
+
choices,
|
|
569
|
+
prefix: ' ',
|
|
570
|
+
pageSize: 15,
|
|
571
|
+
}]);
|
|
572
|
+
if (!target) return;
|
|
573
|
+
|
|
574
|
+
const { confirm } = await inquirer.prompt([{
|
|
575
|
+
type: 'confirm',
|
|
576
|
+
name: 'confirm',
|
|
577
|
+
message: `Remove "${target}" from pool permanently?`,
|
|
578
|
+
default: false,
|
|
579
|
+
prefix: ' ',
|
|
580
|
+
}]);
|
|
581
|
+
if (!confirm) return;
|
|
582
|
+
|
|
583
|
+
const fsm = new AccountManagerFSM();
|
|
584
|
+
cmdRemove(fsm, provider, target);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function runTUI(provider) {
|
|
588
|
+
const inquirer = require('inquirer');
|
|
589
|
+
|
|
590
|
+
while (true) {
|
|
591
|
+
const credsDir = getCredsDir(provider);
|
|
592
|
+
const pool = listPool(credsDir);
|
|
593
|
+
const active = readActivePtr(credsDir);
|
|
594
|
+
|
|
595
|
+
process.stdout.write('\x1Bc');
|
|
596
|
+
printAccountsHeader(provider, pool, active);
|
|
597
|
+
|
|
598
|
+
const choices = [
|
|
599
|
+
{ name: ` ${C.green}+${C.reset} Add account`, value: 'add' },
|
|
600
|
+
];
|
|
601
|
+
if (pool.length > 1) choices.push({ name: ` ${C.cyan}↕${C.reset} Switch account`, value: 'switch' });
|
|
602
|
+
if (pool.length > 0) choices.push({ name: ` ${C.red}×${C.reset} Remove account`, value: 'remove' });
|
|
603
|
+
choices.push(new inquirer.Separator(' ─────────────────────────────'));
|
|
604
|
+
choices.push({ name: ` ${C.dim}Exit${C.reset}`, value: 'exit' });
|
|
605
|
+
|
|
606
|
+
const { action } = await inquirer.prompt([{
|
|
607
|
+
type: 'list',
|
|
608
|
+
name: 'action',
|
|
609
|
+
message: 'Action',
|
|
610
|
+
choices,
|
|
611
|
+
prefix: ' ',
|
|
612
|
+
pageSize: 10,
|
|
613
|
+
}]);
|
|
614
|
+
|
|
615
|
+
if (action === 'exit') { process.stdout.write('\x1Bc'); break; }
|
|
616
|
+
|
|
617
|
+
console.log('');
|
|
618
|
+
try {
|
|
619
|
+
if (action === 'add') await tuiFlowAdd(provider, inquirer);
|
|
620
|
+
if (action === 'switch') await tuiFlowSwitch(provider, pool, active, inquirer);
|
|
621
|
+
if (action === 'remove') await tuiFlowRemove(provider, pool, active, inquirer);
|
|
622
|
+
} catch (err) {
|
|
623
|
+
console.log(`\n ${C.red}✗${C.reset} ${err.message}`);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
console.log('');
|
|
627
|
+
await inquirer.prompt([{
|
|
628
|
+
type: 'input',
|
|
629
|
+
name: '_',
|
|
630
|
+
message: `${C.dim}Press Enter to continue${C.reset}`,
|
|
631
|
+
prefix: ' ',
|
|
632
|
+
}]);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// ─── Utilities ────────────────────────────────────────────────────────────────
|
|
637
|
+
|
|
638
|
+
function die(msg) {
|
|
639
|
+
process.stderr.write(`[account-manager] ${msg}\n`);
|
|
640
|
+
process.exit(1);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function usage(prefix = 'node bin/account-manager.cjs') {
|
|
644
|
+
console.log([
|
|
645
|
+
`Usage: ${prefix} <command> [options]`,
|
|
646
|
+
'',
|
|
647
|
+
'Commands:',
|
|
648
|
+
' add --login [--name alias] Authenticate inline and add to pool (recommended)',
|
|
649
|
+
' add --name <email> Capture current active credential into pool',
|
|
650
|
+
' list Show pool accounts',
|
|
651
|
+
' switch <name|next|prev|N> Switch active account',
|
|
652
|
+
' remove <name> Remove account from pool',
|
|
653
|
+
' status Show provider and pool state',
|
|
654
|
+
'',
|
|
655
|
+
'Options:',
|
|
656
|
+
' --provider <slot> Provider slot (default: first oauth_rotation-enabled provider)',
|
|
657
|
+
' --login Spawn auth login inline; auto-detect email from id_token',
|
|
658
|
+
' --name <email> Account name/email (overrides auto-detection)',
|
|
659
|
+
'',
|
|
660
|
+
'Formal spec: .planning/formal/tla/QGSDAccountManager.tla',
|
|
661
|
+
].join('\n'));
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// ─── Exported entry point (called by qgsd.cjs) ─────────────────────────
|
|
665
|
+
|
|
666
|
+
async function run(argv, usagePrefix) {
|
|
667
|
+
const getArg = (f) => { const i = argv.indexOf(f); return i !== -1 && argv[i + 1] ? argv[i + 1] : null; };
|
|
668
|
+
const command = argv[0];
|
|
669
|
+
const providerArg = getArg('--provider');
|
|
670
|
+
const nameArg = getArg('--name');
|
|
671
|
+
const doLogin = argv.includes('--login');
|
|
672
|
+
|
|
673
|
+
if (!command || argv.includes('--help') || argv.includes('-h')) {
|
|
674
|
+
usage(usagePrefix);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const provider = resolveProvider(providerArg);
|
|
679
|
+
const fsm = new AccountManagerFSM();
|
|
680
|
+
|
|
681
|
+
switch (command) {
|
|
682
|
+
case 'add':
|
|
683
|
+
if (!doLogin && !nameArg) die('add requires --login or --name <email>');
|
|
684
|
+
await cmdAdd(fsm, provider, nameArg, doLogin);
|
|
685
|
+
break;
|
|
686
|
+
|
|
687
|
+
case 'list':
|
|
688
|
+
cmdList(provider);
|
|
689
|
+
break;
|
|
690
|
+
|
|
691
|
+
case 'switch': {
|
|
692
|
+
const target = argv[1];
|
|
693
|
+
if (!target) die('switch requires <name|next|prev|N>');
|
|
694
|
+
cmdSwitch(fsm, provider, target);
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
case 'remove': {
|
|
699
|
+
const target = argv[1];
|
|
700
|
+
if (!target) die('remove requires <name>');
|
|
701
|
+
cmdRemove(fsm, provider, target);
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
case 'status':
|
|
706
|
+
cmdStatus(provider);
|
|
707
|
+
break;
|
|
708
|
+
|
|
709
|
+
default:
|
|
710
|
+
die(`Unknown accounts command: "${command}". Run with --help for usage.`);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
module.exports = { run, runTUI, resolveProvider };
|
|
715
|
+
|
|
716
|
+
// Allow direct invocation: node bin/account-manager.cjs <args>
|
|
717
|
+
if (require.main === module) {
|
|
718
|
+
run(process.argv.slice(2)).catch(err => die(err.message));
|
|
719
|
+
}
|