shakers-cli-beta 0.8.0-beta.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 (218) hide show
  1. package/LICENSE +59 -0
  2. package/README.md +1175 -0
  3. package/bin/add-agent.js +88 -0
  4. package/bin/add-project.js +88 -0
  5. package/bin/add-role.js +61 -0
  6. package/bin/add-skill.js +88 -0
  7. package/bin/ai-usage.js +81 -0
  8. package/bin/alma.js +99 -0
  9. package/bin/applications.js +69 -0
  10. package/bin/availability.js +70 -0
  11. package/bin/certifications.js +69 -0
  12. package/bin/certify.js +73 -0
  13. package/bin/change-role.js +61 -0
  14. package/bin/config.js +120 -0
  15. package/bin/experiences.js +69 -0
  16. package/bin/find-projects.js +89 -0
  17. package/bin/invitations.js +69 -0
  18. package/bin/lang.js +70 -0
  19. package/bin/login.js +283 -0
  20. package/bin/mcp.js +98 -0
  21. package/bin/me.js +69 -0
  22. package/bin/onboarding.js +134 -0
  23. package/bin/portfolios.js +69 -0
  24. package/bin/rate.js +70 -0
  25. package/bin/register.js +325 -0
  26. package/bin/report-html.js +134 -0
  27. package/bin/save.js +77 -0
  28. package/bin/shakers.js +344 -0
  29. package/bin/share.js +72 -0
  30. package/bin/show-project.js +72 -0
  31. package/bin/socials.js +70 -0
  32. package/bin/superadmin.js +351 -0
  33. package/package.json +40 -0
  34. package/src/agent-category.js +13 -0
  35. package/src/agent-evaluation.js +177 -0
  36. package/src/agent-org-chart.js +355 -0
  37. package/src/agent-synthesis.js +132 -0
  38. package/src/agent-usage.js +118 -0
  39. package/src/agents-client.js +165 -0
  40. package/src/ai-fluency-cell.js +72 -0
  41. package/src/ai-profile-client.js +45 -0
  42. package/src/ai-profile-preview.js +154 -0
  43. package/src/ai.js +66 -0
  44. package/src/alma-client.js +123 -0
  45. package/src/alma-flow.js +32 -0
  46. package/src/alma-repo-context.js +47 -0
  47. package/src/alma-shell.js +161 -0
  48. package/src/ansi.js +62 -0
  49. package/src/applications-client.js +79 -0
  50. package/src/applications-flow.js +58 -0
  51. package/src/auth-client.js +144 -0
  52. package/src/auth-device-client.js +118 -0
  53. package/src/auth-method-picker.js +44 -0
  54. package/src/auth-methods.js +35 -0
  55. package/src/auth-session-store.js +214 -0
  56. package/src/authorship.js +203 -0
  57. package/src/automations-detector.js +170 -0
  58. package/src/availability-client.js +92 -0
  59. package/src/availability-flow.js +105 -0
  60. package/src/backend-request.js +170 -0
  61. package/src/blind-approval-extract.js +48 -0
  62. package/src/brand-ansi.js +14 -0
  63. package/src/brand-svg.js +9 -0
  64. package/src/browser-tools-detector.js +21 -0
  65. package/src/build-next-level.js +56 -0
  66. package/src/certifications-client.js +89 -0
  67. package/src/certifications-flow.js +75 -0
  68. package/src/certify-dimension-client.js +217 -0
  69. package/src/certify-dimension-flow.js +193 -0
  70. package/src/certify-disclaimer.js +51 -0
  71. package/src/certify-remediation-prompt.js +23 -0
  72. package/src/cli-args.js +83 -0
  73. package/src/config-dir.js +120 -0
  74. package/src/config.js +778 -0
  75. package/src/consent-flow.js +115 -0
  76. package/src/consent-skip.js +27 -0
  77. package/src/decisions-extract.js +43 -0
  78. package/src/detect-repos.js +145 -0
  79. package/src/detected-tools.js +15 -0
  80. package/src/detection-evidence.js +105 -0
  81. package/src/detection-noise.js +132 -0
  82. package/src/detectors.js +221 -0
  83. package/src/device-auth-store.js +100 -0
  84. package/src/device-login.js +118 -0
  85. package/src/email-verification.js +152 -0
  86. package/src/env-paths.js +16 -0
  87. package/src/find-projects-cache.js +89 -0
  88. package/src/find-projects-client.js +138 -0
  89. package/src/find-projects-flow.js +164 -0
  90. package/src/git-activity.js +123 -0
  91. package/src/graph-certs.js +58 -0
  92. package/src/graph-generator.js +230 -0
  93. package/src/graph-scan.js +239 -0
  94. package/src/i18n.js +3264 -0
  95. package/src/inter-fonts.js +6 -0
  96. package/src/interactive-select.js +183 -0
  97. package/src/interview-client.js +226 -0
  98. package/src/interview-transcript.js +56 -0
  99. package/src/inventory-client.js +90 -0
  100. package/src/invitations-client.js +66 -0
  101. package/src/invitations-flow.js +50 -0
  102. package/src/lang-client.js +147 -0
  103. package/src/lang-codes.js +14 -0
  104. package/src/lang-flow.js +85 -0
  105. package/src/legal-notice.js +53 -0
  106. package/src/locale.js +51 -0
  107. package/src/markdown-ansi.js +50 -0
  108. package/src/maturity.js +111 -0
  109. package/src/mcp-ai-usage-result-tool.js +109 -0
  110. package/src/mcp-ai-usage-tool.js +333 -0
  111. package/src/mcp-alma-tools.js +91 -0
  112. package/src/mcp-applications-tools.js +42 -0
  113. package/src/mcp-ask-tools.js +50 -0
  114. package/src/mcp-auth-form.js +282 -0
  115. package/src/mcp-availability-tools.js +43 -0
  116. package/src/mcp-certifications-tools.js +42 -0
  117. package/src/mcp-certify-dimension-tools.js +58 -0
  118. package/src/mcp-detector.js +223 -0
  119. package/src/mcp-device-login-tools.js +173 -0
  120. package/src/mcp-find-projects-tools.js +67 -0
  121. package/src/mcp-install.js +245 -0
  122. package/src/mcp-invitations-tools.js +42 -0
  123. package/src/mcp-lang-tools.js +39 -0
  124. package/src/mcp-onboarding-tools.js +388 -0
  125. package/src/mcp-portfolios-tools.js +46 -0
  126. package/src/mcp-profile-tools.js +46 -0
  127. package/src/mcp-rate-tools.js +42 -0
  128. package/src/mcp-register-tools.js +517 -0
  129. package/src/mcp-report-tools.js +101 -0
  130. package/src/mcp-role-tools.js +112 -0
  131. package/src/mcp-saved-positions-tools.js +55 -0
  132. package/src/mcp-server.js +170 -0
  133. package/src/mcp-show-project-tools.js +50 -0
  134. package/src/mcp-socials-tools.js +39 -0
  135. package/src/memory-structure-detector.js +108 -0
  136. package/src/model-call-record.js +198 -0
  137. package/src/onboarding-availability.js +26 -0
  138. package/src/onboarding-client.js +364 -0
  139. package/src/onboarding-flow.js +1078 -0
  140. package/src/onboarding-livekit-client.js +102 -0
  141. package/src/onboarding-status-client.js +42 -0
  142. package/src/open-file.js +32 -0
  143. package/src/osc-link.js +16 -0
  144. package/src/portfolio-client.js +164 -0
  145. package/src/portfolio-git-info.js +61 -0
  146. package/src/portfolios-client.js +85 -0
  147. package/src/portfolios-flow.js +79 -0
  148. package/src/profile-client.js +125 -0
  149. package/src/profile-flow.js +115 -0
  150. package/src/profile-switch.js +46 -0
  151. package/src/prompt-select.js +29 -0
  152. package/src/rate-client.js +82 -0
  153. package/src/rate-flow.js +137 -0
  154. package/src/render-ai-fluency.js +266 -0
  155. package/src/render-certification.js +177 -0
  156. package/src/render-html.js +140 -0
  157. package/src/render-report-text.js +27 -0
  158. package/src/render-sheet.js +261 -0
  159. package/src/render-terminal.js +109 -0
  160. package/src/render-usage-report-text.js +155 -0
  161. package/src/repo-context.js +215 -0
  162. package/src/report-gating.js +49 -0
  163. package/src/report-store.js +226 -0
  164. package/src/report-theme.js +177 -0
  165. package/src/report-view-client.js +50 -0
  166. package/src/reveal-output.js +29 -0
  167. package/src/roadmap-content.js +453 -0
  168. package/src/roadmap-personalization.js +132 -0
  169. package/src/roadmap-prompt.js +154 -0
  170. package/src/roles-client.js +114 -0
  171. package/src/roles-flow.js +192 -0
  172. package/src/sanitize-network-text.js +36 -0
  173. package/src/saved-positions-client.js +71 -0
  174. package/src/saved-positions-flow.js +50 -0
  175. package/src/scan-exclusions.js +50 -0
  176. package/src/scanner.js +612 -0
  177. package/src/scope-signals.js +148 -0
  178. package/src/secret-prompt.js +72 -0
  179. package/src/session-gate.js +30 -0
  180. package/src/session-refresh.js +51 -0
  181. package/src/session-scan.js +336 -0
  182. package/src/session-signals.js +36 -0
  183. package/src/shakers-wordmark.js +17 -0
  184. package/src/share-card.js +362 -0
  185. package/src/share-certification.js +111 -0
  186. package/src/share-consent-state.js +157 -0
  187. package/src/share-payload.js +300 -0
  188. package/src/share.js +184 -0
  189. package/src/show-project-client.js +58 -0
  190. package/src/show-project-flow.js +119 -0
  191. package/src/signup-client.js +91 -0
  192. package/src/skills-client.js +102 -0
  193. package/src/socials-client.js +91 -0
  194. package/src/socials-flow.js +83 -0
  195. package/src/specificity-extract.js +41 -0
  196. package/src/start-add-agents.js +306 -0
  197. package/src/start-add-portfolio.js +388 -0
  198. package/src/start-add-skills.js +260 -0
  199. package/src/stdin-ask.js +129 -0
  200. package/src/steering-extract.js +43 -0
  201. package/src/strip-internal-comments.js +45 -0
  202. package/src/tech-detector.js +295 -0
  203. package/src/templates/report-sheet.html +572 -0
  204. package/src/terminal-agents.js +199 -0
  205. package/src/terminal-format.js +85 -0
  206. package/src/terminal-progress.js +106 -0
  207. package/src/terminal-sections.js +219 -0
  208. package/src/tier-analysis.js +126 -0
  209. package/src/tier-engine.js +114 -0
  210. package/src/tier-projection.js +117 -0
  211. package/src/transcript-turns.js +49 -0
  212. package/src/translations-client.js +84 -0
  213. package/src/typewriter.js +154 -0
  214. package/src/usage-model-calls.js +119 -0
  215. package/src/usage-oneshot-commands.js +180 -0
  216. package/src/usage-repo-scope.js +162 -0
  217. package/src/usage-run-steps.js +338 -0
  218. package/src/work-streams.js +74 -0
@@ -0,0 +1,119 @@
1
+ 'use strict';
2
+
3
+ // The three ephemeral model-call orchestrators of the `ai-usage` flow, extracted from bin/ai-usage.js (structure refactor, issue 020).
4
+
5
+ const { parseAgentDescriptions, parseAgentDefinitionsForRoots } = require('./agent-org-chart');
6
+ const {
7
+ buildSynthesisRequest,
8
+ requestAgentSynthesis,
9
+ buildAgentEvaluationRequest,
10
+ requestAgentEvaluation,
11
+ buildRoadmapPersonalizationRequest,
12
+ requestRoadmapPersonalization,
13
+ } = require('./ai');
14
+ const { MODEL_CALL, REASON, skipped, failedLocally, attachModelCall } = require('./model-call-record');
15
+ const {
16
+ getSynthesisEndpoint,
17
+ getRoadmapEndpoint,
18
+ getAgentEvaluationEndpoint,
19
+ } = require('./config');
20
+ const { hasTraceContentConsent } = require('./share');
21
+ const { getRoadmapEntry } = require('./roadmap-content');
22
+ const { computeTierResult } = require('./tier-engine');
23
+
24
+ // Agent-synthesis call (talents-ai-score, ADR-010/ADR-011, "ephemeral" — not persisted; ADR-051 amends WHEN it runs, not that).
25
+ async function maybeSynthesizeAgents(report, root) {
26
+ const KEY = MODEL_CALL.SYNTHESIS;
27
+ if (!Array.isArray(report.agents) || report.agents.length === 0) {
28
+ attachModelCall(report, KEY, skipped(REASON.NO_AGENTS));
29
+ return null;
30
+ }
31
+ const endpoint = getSynthesisEndpoint();
32
+ if (!endpoint) {
33
+ attachModelCall(report, KEY, skipped(REASON.NO_ENDPOINT));
34
+ return null;
35
+ }
36
+
37
+ try {
38
+ const descriptions = Array.isArray(report.agentDescriptions)
39
+ ? report.agentDescriptions
40
+ : parseAgentDescriptions(root);
41
+ const requestBody = buildSynthesisRequest(report.agents, descriptions, hasTraceContentConsent());
42
+ return await requestAgentSynthesis(requestBody, {
43
+ endpoint,
44
+ onOutcome: (rec) => attachModelCall(report, KEY, rec),
45
+ });
46
+ } catch {
47
+ // A failure BEFORE the wire (reading/parsing the agent files threw), which
48
+ // is a local defect and not the endpoint's fault — recorded as such.
49
+ attachModelCall(report, KEY, failedLocally());
50
+ return null; // never breaks the local report — falls back to the org chart
51
+ }
52
+ }
53
+
54
+ // Ephemeral agent-evaluation call (ADR-016): scores each agent's DEFINITION quality server-side (gemini-2.5-flash — see src/agent-evaluation.js).
55
+ async function maybeEvaluateAgents(report, roots, lang) {
56
+ const KEY = MODEL_CALL.EVALUATION;
57
+ if (!Array.isArray(report.agents) || report.agents.length === 0) {
58
+ attachModelCall(report, KEY, skipped(REASON.NO_AGENTS));
59
+ return null;
60
+ }
61
+ const endpoint = getAgentEvaluationEndpoint();
62
+ if (!endpoint) {
63
+ attachModelCall(report, KEY, skipped(REASON.NO_ENDPOINT));
64
+ return null;
65
+ }
66
+
67
+ try {
68
+ const definitions = parseAgentDefinitionsForRoots(roots);
69
+ // ADR-026: pass the report language so the rationale + the one-line description come back translated (a Spanish-authored definition still yields report-language prose).
70
+ const requestBody = buildAgentEvaluationRequest(report.agents, definitions, lang, hasTraceContentConsent());
71
+ return await requestAgentEvaluation(requestBody, {
72
+ endpoint,
73
+ onOutcome: (rec) => attachModelCall(report, KEY, rec),
74
+ });
75
+ } catch {
76
+ attachModelCall(report, KEY, failedLocally());
77
+ return null; // never breaks the local report
78
+ }
79
+ }
80
+
81
+ async function maybePersonalizeRoadmap(report, maturity, lang) {
82
+ const KEY = MODEL_CALL.ROADMAP;
83
+ const tierKey = maturity && maturity.tierKey;
84
+ if (!tierKey) {
85
+ attachModelCall(report, KEY, skipped(REASON.NOT_APPLICABLE));
86
+ return null;
87
+ }
88
+ const entry = getRoadmapEntry(tierKey, lang);
89
+ if (!entry) {
90
+ attachModelCall(report, KEY, skipped(REASON.NOT_APPLICABLE));
91
+ return null;
92
+ }
93
+ if (entry.maxTier) {
94
+ attachModelCall(report, KEY, skipped(REASON.MAX_TIER));
95
+ return null;
96
+ }
97
+
98
+ const endpoint = getRoadmapEndpoint();
99
+ if (!endpoint) {
100
+ attachModelCall(report, KEY, skipped(REASON.NO_ENDPOINT));
101
+ return null;
102
+ }
103
+
104
+ try {
105
+ const tierResult = computeTierResult(report);
106
+ // ADR-026: pass the report language so the personalized prose is localized.
107
+ // ADR-028: fresh consent read, see maybeSynthesizeAgents above.
108
+ const requestBody = buildRoadmapPersonalizationRequest(entry, tierResult, report, lang, hasTraceContentConsent());
109
+ return await requestRoadmapPersonalization(requestBody, {
110
+ endpoint,
111
+ onOutcome: (rec) => attachModelCall(report, KEY, rec),
112
+ });
113
+ } catch {
114
+ attachModelCall(report, KEY, failedLocally());
115
+ return null; // never breaks the local report — falls back to the curated content
116
+ }
117
+ }
118
+
119
+ module.exports = { maybeSynthesizeAgents, maybeEvaluateAgents, maybePersonalizeRoadmap };
@@ -0,0 +1,180 @@
1
+ 'use strict';
2
+
3
+ // One-shot command handlers extracted from bin/ai-usage.js (structure refactor, issue 020).
4
+
5
+ const {
6
+ getConsentStatus,
7
+ revokeConsent,
8
+ resetConsent,
9
+ setEmail,
10
+ } = require('./share');
11
+ const { createStdinAsk } = require('./stdin-ask');
12
+ const {
13
+ setIngestEndpoint,
14
+ resolveIngestEndpoint,
15
+ hasRetiredFallbackConfig,
16
+ isTalentProfile,
17
+ } = require('./config');
18
+ const { buildNextLevelStarter } = require('./build-next-level');
19
+
20
+ // One-shot consent management commands (issue 007). Mirror the retired
21
+ // `--enroll` pattern: they act immediately and do NOT scan.
22
+
23
+ function doConsentStatus(catalog) {
24
+ const status = getConsentStatus();
25
+ const s = catalog.consent.status;
26
+ process.stdout.write(`\n ${s.heading}\n\n`);
27
+ const decisionLine =
28
+ status.consent === 'granted' ? s.decisionGranted
29
+ : status.consent === 'denied' ? s.decisionDenied
30
+ : s.decisionNone;
31
+ process.stdout.write(` ${decisionLine}\n`);
32
+ process.stdout.write(` ${s.email(status.email)}\n`);
33
+ if (status.consent === 'granted' && status.email && !status.emailVerified) {
34
+ process.stdout.write(` ${isTalentProfile() ? s.verificationPending : s.emailUnverifiedExternal}\n`);
35
+ }
36
+ process.stdout.write(` ${s.lastSentAt(status.lastSentAt)}\n\n`);
37
+ }
38
+
39
+ function doConsentRevoke(catalog) {
40
+ revokeConsent();
41
+ process.stdout.write(`\n ${catalog.consent.revoked}\n\n`);
42
+ }
43
+
44
+ // skill-code-certification / ADR-003: clears the decision back to "no decision yet" so the consent question is asked again next run — distinct from revoke (which persists `denied`).
45
+ function doConsentReset(catalog) {
46
+ resetConsent();
47
+ process.stdout.write(`\n ${catalog.consent.reset}\n\n`);
48
+ }
49
+
50
+ function doConsentEmail(newEmail, catalog) {
51
+ const r = setEmail(newEmail);
52
+ if (r.ok) {
53
+ process.stdout.write(`\n ${catalog.consent.emailChanged(r.state.email)}\n\n`);
54
+ } else {
55
+ process.stdout.write(`\n ${catalog.consent.emailInvalidCli}\n\n`);
56
+ process.exitCode = 1;
57
+ }
58
+ }
59
+
60
+ async function confirmEndpointHost(host, ask, catalog) {
61
+ const e = catalog.endpoint;
62
+ const answer = await ask(e.confirmHostPrompt(host));
63
+ return String(answer || '').trim() === host;
64
+ }
65
+
66
+ // Endpoint config (endpoint-config task): one-shot, don't scan.
67
+ async function doSetEndpoint(url, catalog, ask) {
68
+ const e = catalog.endpoint;
69
+ let r = setIngestEndpoint(url);
70
+ if (!r.ok && r.reason === 'needs-confirmation') {
71
+ const confirmed = await confirmEndpointHost(r.host, ask, catalog);
72
+ if (!confirmed) {
73
+ process.stdout.write(`\n ${e.confirmHostMismatch}\n\n`);
74
+ process.exitCode = 1;
75
+ return;
76
+ }
77
+ r = setIngestEndpoint(url, process.env, { confirmed: true });
78
+ }
79
+ if (r.ok) {
80
+ process.stdout.write(`\n ${e.setOk(r.value, r.path)}\n\n`);
81
+ } else {
82
+ const msg =
83
+ r.reason === 'insecure-remote' ? e.errInsecureRemote
84
+ : r.reason === 'invalid-url' || r.reason === 'bad-protocol' ? e.errInvalidUrl
85
+ : e.errEmpty;
86
+ process.stdout.write(`\n ${msg}\n\n`);
87
+ process.exitCode = 1;
88
+ }
89
+ }
90
+
91
+ // Print the effective ingest endpoint(s) and where each resolved from (env var > config file > none), so a Talent can see what's configured without editing or exporting anything.
92
+ function doShowEndpoint(catalog) {
93
+ const e = catalog.endpoint;
94
+ const r = resolveIngestEndpoint();
95
+ if (r.source === 'env') {
96
+ process.stdout.write(`\n ${e.showEnv(r.endpoint)}\n\n`);
97
+ } else if (r.source === 'config-file') {
98
+ process.stdout.write(`\n ${e.showConfigFile(r.endpoint, r.path)}\n\n`);
99
+ } else if (r.source === 'config-file-invalid') {
100
+ process.stdout.write(`\n ${e.showConfigInvalid(r.path)}\n\n`);
101
+ } else {
102
+ process.stdout.write(`\n ${e.showNone}\n\n`);
103
+ }
104
+
105
+ // ADR-042: there is no second hop any more.
106
+ if (hasRetiredFallbackConfig()) {
107
+ process.stdout.write(` ${e.showFallbackRetired}\n\n`);
108
+ }
109
+
110
+ }
111
+
112
+ // "Construir el siguiente nivel ahora" (issue 021): optional, explicit phase — never runs unless the talent asks for it via --build-next-level.
113
+ function doBuildNextLevel(root, maturity, force, catalog) {
114
+ const b = catalog.buildNextLevel;
115
+ const result = buildNextLevelStarter(root || process.cwd(), maturity.tierKey, { force });
116
+
117
+ if (!result.ok) {
118
+ const message =
119
+ result.reason === 'max-tier' ? b.maxTier
120
+ : result.reason === 'no-file-target' ? b.noFileTarget
121
+ : b.unrecognizedTier;
122
+ process.stdout.write(`\n ${message}\n\n`);
123
+ return;
124
+ }
125
+
126
+ process.stdout.write(`\n ${b.heading(result.targetTierKey)}\n`);
127
+ for (const f of result.files) {
128
+ const line =
129
+ f.status === 'created' ? b.created(f.filename)
130
+ : f.status === 'overwritten' ? b.overwritten(f.filename)
131
+ : b.skippedExists(f.filename);
132
+ process.stdout.write(` ${line}\n`);
133
+ }
134
+ process.stdout.write('\n');
135
+ }
136
+
137
+ // The one-shot dispatch that used to sit at the top of `run()`: each command acts immediately and returns, without scanning.
138
+ async function handleOneShotCommand({ opts, catalog, injectedAsk }) {
139
+ if (opts.consentStatus) {
140
+ doConsentStatus(catalog);
141
+ return true;
142
+ }
143
+ if (opts.consentRevoke) {
144
+ doConsentRevoke(catalog);
145
+ return true;
146
+ }
147
+ if (opts.consentReset) {
148
+ doConsentReset(catalog);
149
+ return true;
150
+ }
151
+ if (opts.consentEmail) {
152
+ doConsentEmail(opts.consentEmail, catalog);
153
+ return true;
154
+ }
155
+ if (opts.setEndpoint !== null) {
156
+ const ask = injectedAsk || createStdinAsk();
157
+ try {
158
+ await doSetEndpoint(opts.setEndpoint, catalog, ask);
159
+ } finally {
160
+ if (!injectedAsk) ask.close();
161
+ }
162
+ return true;
163
+ }
164
+ if (opts.showEndpoint) {
165
+ doShowEndpoint(catalog);
166
+ return true;
167
+ }
168
+ return false;
169
+ }
170
+
171
+ module.exports = {
172
+ handleOneShotCommand,
173
+ doConsentStatus,
174
+ doConsentRevoke,
175
+ doConsentReset,
176
+ doConsentEmail,
177
+ doSetEndpoint,
178
+ doShowEndpoint,
179
+ doBuildNextLevel,
180
+ };
@@ -0,0 +1,162 @@
1
+ 'use strict';
2
+
3
+ // Repo-scoping for the `ai-usage` command, extracted from bin/ai-usage.js (structure refactor, issue 020).
4
+
5
+ const { isRefusedWalkRoot } = require('./scan-exclusions');
6
+ const {
7
+ detectRepos,
8
+ repoIdentityForCwd,
9
+ matchReposByFlag,
10
+ scopeFromSelection,
11
+ indexOfCurrentRepo,
12
+ } = require('./detect-repos');
13
+ const { promptSelect } = require('./prompt-select');
14
+ const { createStdinAsk } = require('./stdin-ask');
15
+
16
+ const MAX_MACHINE_WIDE_REPOS = 5;
17
+ const SCOPE_MACHINE = 'machine';
18
+ const SCOPE_REPO = 'repo';
19
+
20
+ function currentRepoToplevel(cwd, repoIdentityFn = repoIdentityForCwd) {
21
+ try {
22
+ return repoIdentityFn(cwd).toplevel || cwd;
23
+ } catch {
24
+ return cwd;
25
+ }
26
+ }
27
+
28
+ // The real git toplevel of `cwd`, or null when `cwd` is not a repo (or is a refused system root like `/`, the MCP `cwd=/` case).
29
+ function cwdRepoToplevel(cwd, repoIdentityFn = repoIdentityForCwd) {
30
+ if (isRefusedWalkRoot(cwd)) return null;
31
+ try {
32
+ return repoIdentityFn(cwd).toplevel || null;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ // Pure: builds the machine-wide root set.
39
+ function machineWideToplevels(cwdRepoTop, detectedRepos, cap = MAX_MACHINE_WIDE_REPOS) {
40
+ const ordered = [];
41
+ const seen = new Set();
42
+ const push = (t) => {
43
+ if (typeof t !== 'string' || !t) return;
44
+ if (isRefusedWalkRoot(t)) return;
45
+ if (seen.has(t)) return;
46
+ seen.add(t);
47
+ ordered.push(t);
48
+ };
49
+ push(cwdRepoTop);
50
+ for (const r of detectedRepos || []) push(r && r.toplevel);
51
+ const capped = ordered.slice(0, cap);
52
+ return { toplevels: capped, total: ordered.length, capped: ordered.length > capped.length };
53
+ }
54
+
55
+ function cwdOnlyScope(repos, cwdToplevel) {
56
+ const idx = indexOfCurrentRepo(repos, cwdToplevel);
57
+ if (idx === -1) {
58
+ return { selectedCwds: new Set(), toplevels: [cwdToplevel], mode: 'cwd' };
59
+ }
60
+ const { selectedCwds, selectedToplevels } = scopeFromSelection(repos, [idx]);
61
+ return {
62
+ selectedCwds,
63
+ toplevels: selectedToplevels.length ? selectedToplevels : [cwdToplevel],
64
+ mode: 'cwd',
65
+ };
66
+ }
67
+
68
+ async function promptScope({ catalog, stdinIsTTY = true, mkAsk = createStdinAsk } = {}) {
69
+ const c = (catalog && catalog.cli) || {};
70
+ const items = [
71
+ { key: SCOPE_MACHINE, label: c.reposScopeOptionMachine || 'Whole machine' },
72
+ { key: SCOPE_REPO, label: c.reposScopeOptionRepo || 'Current repo' },
73
+ ];
74
+ const ask = mkAsk();
75
+ try {
76
+ const picked = await promptSelect({
77
+ ask,
78
+ stdinIsTTY,
79
+ out: (s) => process.stdout.write(`${s}\n`),
80
+ items,
81
+ labelFor: (it) => it.label,
82
+ header: c.reposScopePromptHeader || '',
83
+ hint: c.reposScopePromptHint || '',
84
+ });
85
+ return picked ? picked.key : null;
86
+ } finally {
87
+ if (ask && typeof ask.close === 'function') ask.close();
88
+ }
89
+ }
90
+
91
+ // Resolve which repos feed the AI-usage evaluation.
92
+ async function resolveRepoScope({
93
+ opts,
94
+ cwd,
95
+ injectedAsk,
96
+ catalog,
97
+ detectReposFn = detectRepos,
98
+ repoIdentityFn = repoIdentityForCwd,
99
+ chooseScope = promptScope,
100
+ isTTY = !!process.stdin.isTTY && !!process.stdout.isTTY,
101
+ }) {
102
+ const cwdToplevel = currentRepoToplevel(cwd, repoIdentityFn);
103
+ const cwdRepoTop = cwdRepoToplevel(cwd, repoIdentityFn);
104
+ const note = (s) => {
105
+ if (!opts.json) process.stdout.write(`\n ${s}\n`);
106
+ };
107
+
108
+ const detect = () => {
109
+ try {
110
+ return detectReposFn(process.env);
111
+ } catch {
112
+ return { repos: [], unassignedSessionCount: 0 };
113
+ }
114
+ };
115
+
116
+ // 1. Explicit repo list always wins.
117
+ if (opts.repos) {
118
+ const { repos, unassignedSessionCount } = detect();
119
+ note(catalog.cli.reposDetectedNote(repos.length, unassignedSessionCount));
120
+ const { indices, unmatched } = matchReposByFlag(repos, opts.repos);
121
+ if (unmatched.length > 0) {
122
+ process.stderr.write(` ${catalog.cli.reposFlagUnmatched(unmatched.join(', '))}\n`);
123
+ }
124
+ if (indices.length > 0) {
125
+ const { selectedCwds, selectedToplevels } = scopeFromSelection(repos, indices);
126
+ return { selectedCwds, toplevels: [...new Set(selectedToplevels)], mode: 'flag' };
127
+ }
128
+ return cwdOnlyScope(repos, cwdToplevel);
129
+ }
130
+
131
+ const wantsMachine = opts.allRepos || opts.machine || opts.scope === SCOPE_MACHINE;
132
+ const wantsRepo = opts.repo || opts.scope === SCOPE_REPO;
133
+
134
+ let choice = null;
135
+ if (wantsMachine) choice = SCOPE_MACHINE;
136
+ else if (wantsRepo) choice = SCOPE_REPO;
137
+ else if (isTTY && !injectedAsk && !opts.json) {
138
+ choice = await chooseScope({ catalog, stdinIsTTY: true });
139
+ }
140
+ if (choice === null) choice = SCOPE_REPO;
141
+
142
+ if (choice === SCOPE_MACHINE) {
143
+ const { repos, unassignedSessionCount } = detect();
144
+ note(catalog.cli.reposDetectedNote(repos.length, unassignedSessionCount));
145
+ const mw = machineWideToplevels(cwdRepoTop, repos);
146
+ if (mw.capped) {
147
+ process.stderr.write(`\n ${catalog.cli.reposScopeCapped(mw.toplevels.length, mw.total)}\n`);
148
+ }
149
+ note(catalog.cli.reposScopeAll);
150
+ return {
151
+ selectedCwds: null,
152
+ toplevels: mw.toplevels.length ? mw.toplevels : [cwdToplevel],
153
+ mode: 'all',
154
+ };
155
+ }
156
+
157
+ // Current repo.
158
+ const { repos } = detect();
159
+ return cwdOnlyScope(repos, cwdToplevel);
160
+ }
161
+
162
+ module.exports = { resolveRepoScope, machineWideToplevels, promptScope };