shakers-cli 0.15.0 → 0.17.0

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.
Files changed (132) hide show
  1. package/README.md +56 -1203
  2. package/bin/ai-usage.js +32 -58
  3. package/bin/login.js +10 -10
  4. package/bin/mcp.js +17 -12
  5. package/bin/mcpb.js +1 -1
  6. package/bin/onboarding.js +3 -2
  7. package/bin/register.js +19 -5
  8. package/bin/report-html.js +2 -2
  9. package/bin/shakers-cli.js +352 -0
  10. package/bin/shakers.js +20 -344
  11. package/bin/share.js +1 -1
  12. package/bin/superadmin.js +5 -5
  13. package/bin/version.js +3 -0
  14. package/package.json +12 -3
  15. package/src/account-marker.js +40 -0
  16. package/src/account-trace.js +23 -0
  17. package/src/agent-category.js +1 -1
  18. package/src/agent-evaluation.js +3 -3
  19. package/src/agent-org-chart.js +5 -5
  20. package/src/agent-synthesis.js +3 -3
  21. package/src/agent-usage.js +1 -1
  22. package/src/agents-client.js +4 -4
  23. package/src/ai-usage-review.js +101 -0
  24. package/src/ai.js +1 -1
  25. package/src/ansi.js +1 -1
  26. package/src/auth-client.js +18 -4
  27. package/src/auth-method-picker.js +1 -1
  28. package/src/auth-methods.js +1 -1
  29. package/src/auth-session-store.js +18 -8
  30. package/src/authorship-mismatch.js +1 -1
  31. package/src/authorship.js +2 -2
  32. package/src/automations-detector.js +1 -1
  33. package/src/backend-request.js +5 -3
  34. package/src/brand-ansi.js +1 -1
  35. package/src/browser-tools-detector.js +1 -1
  36. package/src/certify-disclaimer.js +1 -1
  37. package/src/certify-remediation-prompt.js +1 -1
  38. package/src/cli-args.js +2 -2
  39. package/src/command-audit.js +8 -5
  40. package/src/config-dir.js +1 -1
  41. package/src/config.js +114 -48
  42. package/src/consent-flow.js +7 -6
  43. package/src/consent-skip.js +1 -1
  44. package/src/detection-evidence.js +2 -2
  45. package/src/detection-noise.js +1 -1
  46. package/src/detectors.js +1 -1
  47. package/src/device-login.js +12 -1
  48. package/src/email-verification.js +3 -3
  49. package/src/env-paths.js +1 -1
  50. package/src/find-projects-client.js +12 -0
  51. package/src/find-projects-flow.js +1 -13
  52. package/src/git-activity.js +1 -1
  53. package/src/graph-certs.js +3 -3
  54. package/src/graph-scan.js +9 -9
  55. package/src/i18n.js +148 -138
  56. package/src/interactive-select.js +1 -1
  57. package/src/interview-client.js +6 -1
  58. package/src/interview-transcript.js +1 -1
  59. package/src/legal-notice.js +1 -1
  60. package/src/maturity.js +6 -6
  61. package/src/mcp-ai-usage-result-tool.js +34 -12
  62. package/src/mcp-ai-usage-tool.js +282 -163
  63. package/src/mcp-auth-form.js +7 -3
  64. package/src/mcp-detector.js +3 -3
  65. package/src/mcp-device-login-tools.js +2 -1
  66. package/src/mcp-find-projects-tools.js +1 -2
  67. package/src/mcp-install.js +279 -44
  68. package/src/mcp-instructions.js +1 -1
  69. package/src/mcp-onboarding-tools.js +4 -4
  70. package/src/mcp-prompts.js +4 -4
  71. package/src/mcp-register-tools.js +1 -0
  72. package/src/mcp-role-tools.js +24 -1
  73. package/src/mcp-server.js +5 -1
  74. package/src/mcp-signup-client.js +2 -2
  75. package/src/mcp-signup-tools.js +39 -9
  76. package/src/memory-structure-detector.js +1 -1
  77. package/src/model-call-record.js +2 -2
  78. package/src/onboarding-flow.js +63 -42
  79. package/src/onboarding-funnel.js +153 -20
  80. package/src/open-file.js +1 -1
  81. package/src/osc-link.js +1 -1
  82. package/src/portfolio-client.js +11 -1
  83. package/src/portfolio-git-info.js +1 -1
  84. package/src/profile-flow.js +1 -1
  85. package/src/profile-switch.js +5 -12
  86. package/src/render-certification.js +10 -10
  87. package/src/render-html.js +6 -6
  88. package/src/render-report-text.js +1 -1
  89. package/src/render-sheet.js +10 -10
  90. package/src/render-terminal.js +6 -6
  91. package/src/repo-scope.js +132 -0
  92. package/src/report-gating.js +5 -3
  93. package/src/report-store.js +13 -8
  94. package/src/roadmap-content.js +5 -5
  95. package/src/roadmap-personalization.js +3 -3
  96. package/src/roadmap-prompt.js +2 -2
  97. package/src/roles-flow.js +8 -5
  98. package/src/scan-exclusions.js +1 -1
  99. package/src/scanner.js +14 -14
  100. package/src/secret-prompt.js +1 -1
  101. package/src/session-gate.js +1 -1
  102. package/src/session-refresh.js +33 -3
  103. package/src/session-scan.js +1 -1
  104. package/src/shakers-wordmark.js +1 -1
  105. package/src/share-card.js +2 -2
  106. package/src/share-certification.js +4 -4
  107. package/src/share-consent-state.js +9 -9
  108. package/src/share-payload.js +7 -7
  109. package/src/share.js +7 -7
  110. package/src/signup-client.js +1 -1
  111. package/src/signup-copy.js +177 -2
  112. package/src/skills-client.js +1 -1
  113. package/src/start-add-agents.js +2 -2
  114. package/src/start-add-portfolio.js +3 -13
  115. package/src/start-add-skills.js +2 -2
  116. package/src/stdin-ask.js +16 -4
  117. package/src/strip-internal-comments.js +1 -1
  118. package/src/tech-detector.js +2 -2
  119. package/src/templates/report-sheet.html +11 -13
  120. package/src/terminal-agents.js +8 -8
  121. package/src/terminal-format.js +3 -3
  122. package/src/terminal-sections.js +11 -11
  123. package/src/tier-engine.js +3 -3
  124. package/src/tier-projection.js +2 -2
  125. package/src/transcript-turns.js +1 -1
  126. package/src/typewriter.js +1 -1
  127. package/src/usage-model-calls.js +6 -6
  128. package/src/usage-oneshot-commands.js +5 -5
  129. package/src/usage-repo-scope.js +11 -129
  130. package/src/usage-run-steps.js +82 -181
  131. package/src/usage-scan.js +235 -0
  132. package/src/work-streams.js +1 -1
package/bin/ai-usage.js CHANGED
@@ -1,74 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- const { classify } = require('../src/maturity');
5
4
  const { detectReportLang, getCatalog } = require('../src/i18n');
6
5
  const { parseArgs } = require('../src/cli-args');
7
- const { loadConsentState } = require('../src/share');
8
6
  const { handleOneShotCommand } = require('../src/usage-oneshot-commands');
9
- const { prefetchGitLogs } = require('../src/git-log-prefetch');
10
- const { GIT_ACTIVITY_LOG_ARGS } = require('../src/git-activity');
11
- const { WORK_STREAMS_LOG_ARGS } = require('../src/work-streams');
12
- const { AUTHORSHIP_LOG_ARGS } = require('../src/authorship');
7
+ const { createLazyStdinAsk } = require('../src/stdin-ask');
8
+ const { computeUsage } = require('../src/usage-scan');
9
+ const { hasAccountTrace } = require('../src/account-trace');
13
10
  const {
14
11
  resolveRepoScope,
15
12
  machineWideToplevels,
16
13
  promptScope,
17
14
  } = require('../src/usage-repo-scope');
18
15
  const {
19
- performScan,
20
16
  prepareIdentityAndUsage,
21
17
  resolveAiConsent,
22
- runAggregation,
23
- runModelCalls,
24
18
  renderAndFinalize,
25
19
  } = require('../src/usage-run-steps');
26
20
 
27
- // Steps 1-6 of a run: scan, scope, aggregate, classify; nothing is rendered or sent.
28
- async function computeUsage({ opts, catalog, lang, injectedAsk = null, inRegistration = false }) {
29
- // 1. Local scan + classify (always computed and shown, ADR-011/013).
30
- const { report, root, rootDir } = performScan({ opts, catalog });
31
-
32
- // 2. Identity gate + local agent-usage signal. Roadmap only for anonymous runs
33
- // (gate.showRoadmap = !loggedIn) AND never during register/onboarding.
34
- const { gate: identityGate } = prepareIdentityAndUsage({ report, catalog, quiet: opts.json === true });
35
- const gate = (inRegistration && identityGate.showRoadmap)
36
- ? { ...identityGate, showRoadmap: false }
37
- : identityGate;
38
-
39
- // 3. Consent gate before any AI/egress call (ADR-051); a decline still renders.
40
- const aiConsentGranted = await resolveAiConsent({ opts, catalog, gate, injectedAsk });
41
-
42
- // 4. Repo scoping (exclude client work).
43
- const scope = await resolveRepoScope({ opts, cwd: rootDir, injectedAsk, catalog });
44
- const consentEmail = (loadConsentState() || {}).email || null;
45
- const agentRoots = scope.toplevels;
46
-
47
- // 5. Aggregate scoped signals + rebuild scan/org-chart over the selection, re-classify.
48
- await prefetchGitLogs(agentRoots, [GIT_ACTIVITY_LOG_ARGS, WORK_STREAMS_LOG_ARGS, AUTHORSHIP_LOG_ARGS]);
49
- runAggregation({ report, scope, consentEmail, agentRoots, root, catalog });
50
- const maturity = classify(report);
51
- const hasAgents = Array.isArray(report.agents) && report.agents.length > 0;
52
- report.analyzedRepos =
53
- scope.mode === 'all'
54
- ? null
55
- : scope.toplevels.map((tl) => tl.split(/[/\\]/).pop() || tl);
56
-
57
- // 6. The three ephemeral model calls, serialized and gated by consent (ADR-051).
58
- await runModelCalls({
59
- report, maturity, aiConsentGranted, opts, gate, hasAgents, agentRoots, rootDir, lang, catalog,
60
- });
61
- return { report, maturity, root, gate };
62
- }
63
-
64
- // The MCP's scan: same steps, returned instead of printed, since stdout is its JSON-RPC channel.
65
- async function scanUsage(argv) {
66
- const opts = parseArgs(argv);
67
- const lang = opts.lang || detectReportLang();
68
- const { report, maturity } = await computeUsage({ opts, catalog: getCatalog(lang), lang });
69
- return { report, maturity };
70
- }
71
-
72
21
  // Shared by the dispatcher and register flow: an injected `ask` is caller-owned, `bypassThrottle` is register-only.
73
22
  async function run(argv = process.argv.slice(2), { ask: injectedAsk = null, bypassThrottle = false, inRegistration = false } = {}) {
74
23
  const opts = parseArgs(argv);
@@ -85,13 +34,38 @@ async function run(argv = process.argv.slice(2), { ask: injectedAsk = null, bypa
85
34
  // One-shot commands (consent/endpoint config): act immediately, do NOT scan.
86
35
  if (await handleOneShotCommand({ opts, catalog, injectedAsk })) return;
87
36
 
88
- const { report, maturity, root, gate } = await computeUsage({ opts, catalog, lang, injectedAsk, inRegistration });
37
+ if (opts.roadmap && hasAccountTrace()) {
38
+ process.stdout.write(`\n ${catalog.cli.roadmapNotForAccounts}\n\n`);
39
+ return;
40
+ }
89
41
 
90
- // 7. Render (or emit --json), persist, optional --build-next-level, auto-share.
91
- await renderAndFinalize({ report, maturity, lang, opts, root, gate, catalog, bypassThrottle });
42
+ const ask = injectedAsk || createLazyStdinAsk();
43
+ try {
44
+ const { report, maturity, root, gate, repos } = await computeUsage({
45
+ opts,
46
+ catalog,
47
+ lang,
48
+ inRegistration,
49
+ prepareIdentity: prepareIdentityAndUsage,
50
+ resolveConsent: (args) => resolveAiConsent({ ...args, injectedAsk: ask }),
51
+ resolveScope: async (args) => {
52
+ if (injectedAsk) return resolveRepoScope({ ...args, injectedAsk });
53
+ ask.suspend();
54
+ try {
55
+ return await resolveRepoScope(args);
56
+ } finally {
57
+ ask.resume();
58
+ }
59
+ },
60
+ });
61
+
62
+ await renderAndFinalize({ report, maturity, lang, opts, root, gate, catalog, bypassThrottle, repos, injectedAsk: ask });
63
+ } finally {
64
+ if (!injectedAsk) ask.close();
65
+ }
92
66
  }
93
67
 
94
- module.exports = { run, scanUsage, resolveRepoScope, machineWideToplevels, promptScope };
68
+ module.exports = { run, resolveRepoScope, machineWideToplevels, promptScope };
95
69
 
96
70
  // Auto-run only when executed directly, not when require()'d by the dispatcher.
97
71
  if (require.main === module) {
package/bin/login.js CHANGED
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- // `login` / `logout` — a registered Talent identifies themselves to the CLI (talents-ai-score, issue 122 / ADR-044).
4
+ // `login` / `logout` — a registered Talent identifies themselves to the CLI.
5
5
 
6
6
  const { detectReportLang, getCatalog } = require('../src/i18n');
7
7
  const { getAuthTokenEndpoint, getDeviceAuthorizeEndpoint, getDeviceTokenEndpoint, getCompleteRegistrationEndpoint } = require('../src/config');
8
- const { runDeviceLogin, appendProviderParam } = require('../src/device-login');
8
+ const { runDeviceLogin, appendProviderParam, deviceSessionRecord } = require('../src/device-login');
9
9
  const { buildRegistrationContext } = require('../src/signup-client');
10
10
  const { openPath } = require('../src/open-file');
11
+ const { endSession } = require('../src/session-refresh');
11
12
  const {
12
13
  saveAuthSession,
13
- clearAuthSession,
14
14
  loadAuthSession,
15
15
  sessionStatus,
16
16
  } = require('../src/auth-session-store');
@@ -68,13 +68,13 @@ async function run(
68
68
 
69
69
  if (!method) {
70
70
  const stdinIsTTY = ttyOverride !== undefined ? ttyOverride : !!process.stdin.isTTY;
71
- // ADR-044: an ACTIVE session is not silently overwritten, and the picker never shows.
71
+ // An ACTIVE session is not silently overwritten, and the picker never shows.
72
72
  if (sessionStatus(loadAuthSession()) === 'active') {
73
73
  (output || process.stdout).write(`\n ${c.alreadyLoggedIn}\n\n`);
74
74
  return;
75
75
  }
76
76
  if (stdinIsTTY) {
77
- // ADR-041/046 — the picker offers Email, Google and LinkedIn.
77
+ // The picker offers Email, Google and LinkedIn.
78
78
  const pickerAsk = injectedAsk || createStdinAsk();
79
79
  try {
80
80
  method = await chooseLoginMethod({ ask: pickerAsk, stdinIsTTY, c, input, output });
@@ -108,6 +108,8 @@ function persistAndReport(result, c, email = null, output = process.stdout) {
108
108
  accessTokenExpiresAt: result.expiresAt,
109
109
  cookie: result.cookie || null,
110
110
  cookieExpiresAt: result.cookieExpiresAt || null,
111
+ sessionToken: result.sessionToken || null,
112
+ sessionExpiresAt: result.sessionExpiresAt || null,
111
113
  email,
112
114
  hubAccessToken: result.hubAccessToken || null,
113
115
  });
@@ -167,9 +169,8 @@ async function runDeviceFlow(provider, c, { openBrowser = openPath, sleep = unde
167
169
 
168
170
  function handleDeviceResult(result, c, output = process.stdout) {
169
171
  if (result.ok) {
170
- // Map the device-flow JWT to the shared persist shape (no cookie).
171
172
  return void persistAndReport(
172
- { accessToken: result.token, hubAccessToken: result.token, email: result.email || null },
173
+ deviceSessionRecord(result),
173
174
  c,
174
175
  result.email || null,
175
176
  output,
@@ -196,13 +197,12 @@ function handleDeviceResult(result, c, output = process.stdout) {
196
197
  process.exitCode = 1;
197
198
  }
198
199
 
199
- // `logout` — local only (no endpoint, no network): forget the stored session.
200
+ // `logout`: revoke the hub session (best-effort) and forget the stored one.
200
201
  async function runLogout(argv = process.argv.slice(2), { output = process.stdout } = {}) {
201
202
  const opts = parseArgs(argv);
202
203
  const lang = opts.lang || detectReportLang();
203
204
  const c = getCatalog(lang).login;
204
- const had = loadAuthSession() !== null;
205
- clearAuthSession();
205
+ const had = await endSession(process.env);
206
206
  clearConsentState();
207
207
  output.write(`\n ${had ? c.loggedOut : c.notLoggedIn}\n\n`);
208
208
  }
package/bin/mcp.js CHANGED
@@ -2,7 +2,7 @@
2
2
  'use strict';
3
3
 
4
4
  const { createMcpServer } = require('../src/mcp-server');
5
- const { makeAiUsageTool } = require('../src/mcp-ai-usage-tool');
5
+ const { makeAiUsageTools } = require('../src/mcp-ai-usage-tool');
6
6
  const { makeAiUsageResultTool } = require('../src/mcp-ai-usage-result-tool');
7
7
  const { makeCertifyDimensionTools } = require('../src/mcp-certify-dimension-tools');
8
8
  const { makeFindProjectsTools } = require('../src/mcp-find-projects-tools');
@@ -43,7 +43,7 @@ const HELP = `\n shakers mcp — local MCP server (stdio)\n\n`
43
43
  + ` Usage: shakers mcp\n\n`
44
44
  + ` This command speaks JSON-RPC on stdio; it is meant to be launched by an\n`
45
45
  + ` MCP client, not run by hand.\n`
46
- + ` Tools: ai_usage, ai_usage_result, logout, social_signin_start, social_signin_poll,\n`
46
+ + ` Tools: ai_usage, ai_usage_upload, ai_usage_result, logout, social_signin_start, social_signin_poll,\n`
47
47
  + ` list_addable, add_skill, add_agent,\n`
48
48
  + ` add_project, signup_start, signup_email, signup_draft, signup_create_account,\n`
49
49
  + ` signup_status, update_existing_profile, import_profile, save_profile_details, open_web,\n`
@@ -61,13 +61,14 @@ const HELP = `\n shakers mcp — local MCP server (stdio)\n\n`
61
61
 
62
62
  // The LOCAL (filesystem/machine) tools — the only ones the .mcpb bundle exposes; the
63
63
  // server-backed tools belong to the remote connector.
64
- const LOCAL_ONLY_TOOL_NAMES = new Set(['ai_usage', 'ai_usage_result', 'read_cv', 'suggest_register_context']);
64
+ const LOCAL_ONLY_TOOL_NAMES = new Set(['ai_usage', 'ai_usage_upload', 'ai_usage_result', 'read_cv', 'suggest_register_context']);
65
65
 
66
66
  // `localOnly` builds the scan-only server for the Claude .mcpb bundle, reusing the SAME
67
67
  // tool registry (just filtered) — no duplicate registration.
68
- function buildServer({ localOnly = false, funnel = createFunnel({ surface: 'mcp' }) } = {}) {
68
+ function buildServer({ localOnly = false, funnel = createFunnel({ surface: 'mcp' }), makeInterviewClient = null } = {}) {
69
69
  const lang = detectFlowLang();
70
- const allTools = [makeAiUsageTool(), makeAiUsageResultTool(), ...makeOnboardingTools(), ...makeRoleTools({ lang, funnel }), ...makeDeviceLoginTools(), ...makeSignupTools({ lang, funnel }), ...makeRegisterTools({ lang, funnel }), ...makeReportTools(), ...makeCertifyDimensionTools(), ...makeFindProjectsTools(), ...makeShowProjectTools(), ...makeSavedPositionsTools(), ...makeInvitationsTools(), ...makeApplicationsTools(), ...makeAvailabilityTools(), ...makeCertificationsTools(), ...makeProfileTools(), ...makeRateTools(), ...makeLangTools(), ...makeSocialsTools(), ...makePortfolioTools(), ...makeAlmaTools()];
70
+ const aiUsage = makeAiUsageTools();
71
+ const allTools = [aiUsage.usage, aiUsage.upload, makeAiUsageResultTool(), ...makeOnboardingTools(), ...makeRoleTools({ lang, funnel }), ...makeDeviceLoginTools(), ...makeSignupTools({ lang, funnel }), ...makeRegisterTools({ lang, funnel, flowDeps: { makeInterviewClient } }), ...makeReportTools(), ...makeCertifyDimensionTools(), ...makeFindProjectsTools(), ...makeShowProjectTools(), ...makeSavedPositionsTools(), ...makeInvitationsTools(), ...makeApplicationsTools(), ...makeAvailabilityTools(), ...makeCertificationsTools(), ...makeProfileTools(), ...makeRateTools(), ...makeLangTools(), ...makeSocialsTools(), ...makePortfolioTools(), ...makeAlmaTools()];
71
72
  const tools = localOnly ? allTools.filter((t) => LOCAL_ONLY_TOOL_NAMES.has(t.name)) : allTools;
72
73
  // Slash-command prompts, kept to the tools actually registered in this mode.
73
74
  const prompts = makePrompts(new Set(tools.map((t) => t.name)), lang);
@@ -79,6 +80,7 @@ function buildServer({ localOnly = false, funnel = createFunnel({ surface: 'mcp'
79
80
  // Long-lived server: refresh the email session's hub JWT from the stored
80
81
  // cookie before each tool call so a stale (~15-min) JWT never reaches a tool.
81
82
  beforeCall: () => ensureFreshSession(process.env),
83
+ afterCall: () => { funnel.flush(); },
82
84
  onInitialize: ({ clientInfo }) => funnel.setMcpClient(clientInfo.name),
83
85
  });
84
86
  }
@@ -88,21 +90,23 @@ function prepareProcess(env = process.env) {
88
90
  require('../src/backend-request').setClientIdentity('mcp');
89
91
  }
90
92
 
91
- async function run(argv = process.argv.slice(2)) {
92
- // The installer's last step; not a command the Talent needs to know.
93
- if (argv[0] === 'install') {
94
- await require('../src/mcp-install').runMcpInstall();
95
- return;
96
- }
93
+ async function run(argv = process.argv.slice(2), { makeInterviewClient = null } = {}) {
97
94
  if (argv.includes('--help') || argv.includes('-h')) {
98
95
  process.stdout.write(HELP);
99
96
  return;
100
97
  }
101
98
 
99
+ const problem = require('../src/config').envProfileProblem(process.env);
100
+ if (problem) {
101
+ process.stderr.write(` [mcp] ${problem}\n`);
102
+ process.exitCode = 1;
103
+ return;
104
+ }
102
105
  prepareProcess();
103
106
 
104
107
  // `--local` = the scan-only server shipped in the Claude .mcpb bundle.
105
- const server = buildServer({ localOnly: argv.includes('--local') });
108
+ const funnel = createFunnel({ surface: 'mcp' });
109
+ const server = buildServer({ localOnly: argv.includes('--local'), funnel, makeInterviewClient });
106
110
  const rl = server.start({
107
111
  input: process.stdin,
108
112
  output: process.stdout,
@@ -115,6 +119,7 @@ async function run(argv = process.argv.slice(2)) {
115
119
  rl.on('close', resolve);
116
120
  process.stdin.on('end', () => rl.close());
117
121
  });
122
+ await funnel.settle();
118
123
  }
119
124
 
120
125
  module.exports = { run, buildServer, buildServerInstructions, prepareProcess };
package/bin/mcpb.js CHANGED
@@ -2,4 +2,4 @@
2
2
  'use strict';
3
3
 
4
4
  // Claude Desktop's built-in Node imports this entry from its own host, so it starts without a require.main check.
5
- require('./mcp').run();
5
+ require('./mcp').run(process.argv.slice(2), { makeInterviewClient: require('../src/interview-client').createInterviewClient });
package/bin/onboarding.js CHANGED
@@ -5,6 +5,7 @@ const { detectFlowLang } = require('../src/i18n');
5
5
  const { loadAuthSession, sessionStatus } = require('../src/auth-session-store');
6
6
  const { createStdinAsk } = require('../src/stdin-ask');
7
7
  const { runOnboardingInterview, runRepeatOnboardingInterview, makeDeps } = require('../src/onboarding-flow');
8
+ const { createInterviewClient } = require('../src/interview-client');
8
9
  const { checkOnboardingCompleted } = require('../src/onboarding-availability');
9
10
  const { makeIo } = require('./register');
10
11
  const { c: fmt } = require('../src/terminal-format');
@@ -87,7 +88,7 @@ async function run(
87
88
  // Explicit overwrite-repeat: its own warning + confirmation, bypassing the legacy already-completed gate.
88
89
  if (o.repeat) {
89
90
  const io = makeIo({ ask, lang, out: write, acceptDisclaimer: o.acceptDisclaimer, stdinIsTTY });
90
- const result = await runRepeatOnboardingInterview(io, deps || makeDeps(), { confirmed: o.yes });
91
+ const result = await runRepeatOnboardingInterview(io, deps || makeDeps({ makeInterviewClient: createInterviewClient }), { confirmed: o.yes });
91
92
  if (result && result.ok === false) {
92
93
  process.stderr.write(`\n ${red(`onboarding: ${result.reason}`)}\n\n`);
93
94
  process.exitCode = 1;
@@ -116,7 +117,7 @@ async function run(
116
117
  }
117
118
 
118
119
  const io = makeIo({ ask, lang, out: write, acceptDisclaimer: o.acceptDisclaimer, stdinIsTTY });
119
- const result = await runOnboardingInterview(io, deps || makeDeps());
120
+ const result = await runOnboardingInterview(io, deps || makeDeps({ makeInterviewClient: createInterviewClient }));
120
121
  if (!result.ok) {
121
122
  process.stderr.write(`\n ${red(`onboarding: ${result.reason}${result.step ? ` (${result.step})` : ''}`)}\n\n`);
122
123
  process.exitCode = 1;
package/bin/register.js CHANGED
@@ -11,6 +11,7 @@ const {
11
11
  WORK_SITUATIONS, EMPLOYMENT_PARTICIPATIONS, FREELANCE_OPINIONS, CHANGE_MOTIVATORS, WORK_MODES,
12
12
  LANGUAGE_CODES, LANGUAGE_LEVELS, showsFreelanceOpinion, showsChangeMotivators,
13
13
  } = require('../src/onboarding-flow');
14
+ const { createInterviewClient } = require('../src/interview-client');
14
15
  const { promptSelect, promptMultiSelect } = require('../src/prompt-select');
15
16
  const { chooseAuthMethod } = require('../src/auth-method-picker');
16
17
  const { methodIdsForFlow } = require('../src/auth-methods');
@@ -121,9 +122,10 @@ function makeIo({ ask, lang, out, acceptDisclaimer, stdinIsTTY = false }) {
121
122
  }
122
123
  return { fullTimeSelected: true, fullTimeAmount, fullTimeCurrency, partTimeSelected, partTimeAmount, partTimeCurrency };
123
124
  },
124
- askAvailability: async (c) => {
125
+ askAvailability: async (c, { onLocation = () => {} } = {}) => {
125
126
  const available = isYes(await ask(` ${c.availableAsk} `));
126
127
  const monthlyHours = await promptSelect({ ask, stdinIsTTY, out: selectOut, items: MONTHLY_HOURS, header: ` ${c.monthlyHoursAsk}` });
128
+ onLocation();
127
129
  const workModes = await promptMultiSelect({
128
130
  ask,
129
131
  stdinIsTTY,
@@ -305,7 +307,20 @@ async function run(
305
307
  return;
306
308
  }
307
309
 
308
- const ask = injectedAsk || createStdinAsk();
310
+ // Injected deps (tests) bring their own funnel or none.
311
+ const registrationFunnel = funnel || (deps ? undefined : createFunnel({ surface: 'cli' }));
312
+ let interrupted = false;
313
+ const onInterrupt = () => {
314
+ if (interrupted) process.exit(130);
315
+ interrupted = true;
316
+ process.stdout.write('\n');
317
+ const settled = registrationFunnel ? registrationFunnel.settle() : Promise.resolve();
318
+ settled.then(() => {
319
+ ask.close();
320
+ process.exit(130);
321
+ });
322
+ };
323
+ const ask = injectedAsk || createStdinAsk({ onInterrupt });
309
324
  try {
310
325
  // Method choice (Email + Google) from the shared auth-methods registry; a
311
326
  // valid method flag skips the picker.
@@ -317,9 +332,7 @@ async function run(
317
332
  const io = makeIo({ ask, lang, out: write, acceptDisclaimer: o.acceptDisclaimer, stdinIsTTY });
318
333
  // At a terminal the profile opens in the browser at the end; piped runs, tests and --no-open only print it.
319
334
  const openProfile = stdinIsTTY && !o.noOpen ? (openBrowser || openPath) : undefined;
320
- // Injected deps (tests) bring their own funnel or none.
321
- const registrationFunnel = funnel || (deps ? undefined : createFunnel({ surface: 'cli' }));
322
- const result = await runOnboarding(io, deps || makeDeps(), { method, openBrowser, onAuthUrl: io.onGoogleAuthUrl, openProfile, funnel: registrationFunnel });
335
+ const result = await runOnboarding(io, deps || makeDeps({ makeInterviewClient: createInterviewClient }), { method, openBrowser, onAuthUrl: io.onGoogleAuthUrl, openProfile, funnel: registrationFunnel });
323
336
  if (!result.ok) {
324
337
  process.stderr.write(`\n ${red(`register: ${result.reason}${result.step ? ` (${result.step})` : ''}`)}\n\n`);
325
338
  process.exitCode = 1;
@@ -327,6 +340,7 @@ async function run(
327
340
  return result;
328
341
  } finally {
329
342
  if (!injectedAsk) ask.close();
343
+ if (registrationFunnel) await registrationFunnel.settle();
330
344
  }
331
345
  }
332
346
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- // `report` — materializes and OPENS the shareable, cumulative HTML report for the current project (ADR-016).
4
+ // `report` — materializes and OPENS the shareable, cumulative HTML report for the current project.
5
5
 
6
6
  const { detectReportLang, getCatalog } = require('../src/i18n');
7
7
  const { materializeProjectReport } = require('../src/report-store');
@@ -119,7 +119,7 @@ async function run(argv = process.argv.slice(2), { ask } = {}) { // eslint-disab
119
119
  const opened = opts.open ? openPath(result.htmlPath) : false;
120
120
 
121
121
  // Clear "the report was UPDATED" notice (fires once per run — this is the sole
122
- // report write point, ADR-016). OSC 8: clickable file:// link where supported.
122
+ // report write point). OSC 8: clickable file:// link where supported.
123
123
  const updatedLabel = lang === 'es' ? 'Report actualizado' : 'Report updated';
124
124
  process.stdout.write(`\n ✓ ${updatedLabel} · ${oscLink(result.fileUrl)}\n`);
125
125
  if (opened) process.stdout.write(` ${r.opening}\n`);