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,221 @@
1
+ 'use strict';
2
+
3
+ // Catalog of AI tools for developers.
4
+
5
+ const CATEGORIES = {
6
+ AGENTIC_CLI: 'CLI agéntica',
7
+ AI_EDITOR: 'Editor con IA',
8
+ IDE_ASSISTANT: 'Asistente en IDE',
9
+ COMPLETION: 'Autocompletado',
10
+ AI_TERMINAL: 'Terminal con IA',
11
+ };
12
+
13
+ const detectors = [
14
+ {
15
+ id: 'claude-code',
16
+ name: 'Claude Code',
17
+ vendor: 'Anthropic',
18
+ category: CATEGORIES.AGENTIC_CLI,
19
+ signals: [
20
+ { type: 'projectPath', path: '.claude' },
21
+ { type: 'projectPath', path: 'CLAUDE.md' },
22
+ { type: 'projectPath', path: '.mcp.json' },
23
+ { type: 'homePath', path: '.claude' },
24
+ { type: 'bin', name: 'claude' },
25
+ ],
26
+ },
27
+ {
28
+ id: 'cursor',
29
+ name: 'Cursor',
30
+ vendor: 'Anysphere',
31
+ category: CATEGORIES.AI_EDITOR,
32
+ signals: [
33
+ { type: 'projectPath', path: '.cursor' },
34
+ { type: 'projectPath', path: '.cursorrules' },
35
+ { type: 'homePath', path: '.cursor' },
36
+ { type: 'bin', name: 'cursor' },
37
+ ],
38
+ },
39
+ {
40
+ id: 'github-copilot',
41
+ name: 'GitHub Copilot',
42
+ vendor: 'GitHub',
43
+ category: CATEGORIES.IDE_ASSISTANT,
44
+ signals: [
45
+ { type: 'projectPath', path: '.github/copilot-instructions.md' },
46
+ { type: 'vscodeExt', prefix: 'github.copilot' },
47
+ { type: 'bin', name: 'copilot' },
48
+ ],
49
+ },
50
+ {
51
+ id: 'windsurf',
52
+ name: 'Windsurf',
53
+ vendor: 'Codeium',
54
+ category: CATEGORIES.AI_EDITOR,
55
+ signals: [
56
+ { type: 'projectPath', path: '.windsurf' },
57
+ { type: 'projectPath', path: '.windsurfrules' },
58
+ { type: 'homePath', path: '.codeium/windsurf' },
59
+ { type: 'bin', name: 'windsurf' },
60
+ ],
61
+ },
62
+ {
63
+ id: 'aider',
64
+ name: 'Aider',
65
+ vendor: 'Aider AI',
66
+ category: CATEGORIES.AGENTIC_CLI,
67
+ signals: [
68
+ { type: 'projectPath', path: '.aider.conf.yml' },
69
+ { type: 'projectPath', path: '.aider.chat.history.md' },
70
+ { type: 'homePath', path: '.aider.conf.yml' },
71
+ { type: 'bin', name: 'aider' },
72
+ ],
73
+ },
74
+ {
75
+ id: 'continue',
76
+ name: 'Continue',
77
+ vendor: 'Continue',
78
+ category: CATEGORIES.IDE_ASSISTANT,
79
+ signals: [
80
+ { type: 'projectPath', path: '.continue' },
81
+ { type: 'homePath', path: '.continue' },
82
+ { type: 'vscodeExt', prefix: 'continue.continue' },
83
+ ],
84
+ },
85
+ {
86
+ id: 'cline',
87
+ name: 'Cline',
88
+ vendor: 'Cline',
89
+ category: CATEGORIES.IDE_ASSISTANT,
90
+ signals: [
91
+ { type: 'projectPath', path: '.clinerules' },
92
+ { type: 'vscodeExt', prefix: 'saoudrizwan.claude-dev' },
93
+ ],
94
+ },
95
+ {
96
+ id: 'gemini-cli',
97
+ name: 'Gemini CLI',
98
+ vendor: 'Google',
99
+ category: CATEGORIES.AGENTIC_CLI,
100
+ signals: [
101
+ { type: 'projectPath', path: '.gemini' },
102
+ { type: 'projectPath', path: 'GEMINI.md' },
103
+ { type: 'homePath', path: '.gemini' },
104
+ { type: 'bin', name: 'gemini' },
105
+ ],
106
+ },
107
+ {
108
+ id: 'codex-cli',
109
+ name: 'Codex CLI',
110
+ vendor: 'OpenAI',
111
+ category: CATEGORIES.AGENTIC_CLI,
112
+ signals: [
113
+ { type: 'projectPath', path: 'AGENTS.md' },
114
+ { type: 'homePath', path: '.codex' },
115
+ { type: 'bin', name: 'codex' },
116
+ ],
117
+ },
118
+ {
119
+ id: 'cody',
120
+ name: 'Cody',
121
+ vendor: 'Sourcegraph',
122
+ category: CATEGORIES.IDE_ASSISTANT,
123
+ signals: [
124
+ { type: 'projectPath', path: '.sourcegraph' },
125
+ { type: 'vscodeExt', prefix: 'sourcegraph.cody-ai' },
126
+ { type: 'bin', name: 'cody' },
127
+ ],
128
+ },
129
+ {
130
+ id: 'zed',
131
+ name: 'Zed',
132
+ vendor: 'Zed Industries',
133
+ category: CATEGORIES.AI_EDITOR,
134
+ signals: [
135
+ { type: 'homePath', path: '.config/zed' },
136
+ { type: 'bin', name: 'zed' },
137
+ ],
138
+ },
139
+ {
140
+ id: 'tabnine',
141
+ name: 'Tabnine',
142
+ vendor: 'Tabnine',
143
+ category: CATEGORIES.COMPLETION,
144
+ signals: [
145
+ { type: 'homePath', path: '.config/TabNine' },
146
+ { type: 'vscodeExt', prefix: 'tabnine.tabnine-vscode' },
147
+ ],
148
+ },
149
+
150
+ // ---- Expansion (talents-ai-score, more signals) — confidence per entry below ----
151
+ {
152
+ id: 'amazon-q-developer',
153
+ name: 'Amazon Q Developer',
154
+ vendor: 'AWS',
155
+ category: CATEGORIES.AGENTIC_CLI,
156
+ signals: [
157
+ // High confidence: binary and extension id are stable for the official CLI/extension.
158
+ { type: 'bin', name: 'q' },
159
+ { type: 'vscodeExt', prefix: 'amazonwebservices.amazon-q-vscode' },
160
+ // Medium confidence: CLI credentials/cache directory under .aws.
161
+ { type: 'homePath', path: '.aws/amazonq' },
162
+ ],
163
+ },
164
+ {
165
+ id: 'codeium',
166
+ name: 'Codeium',
167
+ vendor: 'Codeium (Exafunction)',
168
+ category: CATEGORIES.IDE_ASSISTANT,
169
+ signals: [
170
+ // Codeium's base extension (autocomplete in any editor), distinct from Windsurf (its own editor, already cataloged above).
171
+ { type: 'vscodeExt', prefix: 'codeium.codeium' },
172
+ { type: 'homePath', path: '.codeium/config.json' },
173
+ ],
174
+ },
175
+ {
176
+ id: 'supermaven',
177
+ name: 'Supermaven',
178
+ vendor: 'Supermaven',
179
+ category: CATEGORIES.COMPLETION,
180
+ signals: [
181
+ // High confidence in the extension id; medium in the local state directory.
182
+ { type: 'vscodeExt', prefix: 'supermaven.supermaven' },
183
+ { type: 'homePath', path: '.supermaven' },
184
+ ],
185
+ },
186
+ {
187
+ id: 'pieces',
188
+ name: 'Pieces for Developers',
189
+ vendor: 'Mesh Intelligent Technologies',
190
+ category: CATEGORIES.IDE_ASSISTANT,
191
+ signals: [
192
+ // Medium confidence: extension id recalled from memory, could not be verified against the marketplace in this environment.
193
+ { type: 'vscodeExt', prefix: 'meshintelligenttechnologiesinc.pieces-vscode' },
194
+ ],
195
+ },
196
+ {
197
+ id: 'warp-ai',
198
+ name: 'Warp (AI terminal)',
199
+ vendor: 'Warp',
200
+ category: CATEGORIES.AI_TERMINAL,
201
+ signals: [
202
+ // Medium confidence: config directory of the Warp terminal app.
203
+ { type: 'homePath', path: '.warp' },
204
+ // On Linux, Warp's config lives under ~/.config/warp-terminal.
205
+ { type: 'homePath', path: '.config/warp-terminal' },
206
+ ],
207
+ },
208
+ {
209
+ id: 'trae',
210
+ name: 'Trae',
211
+ vendor: 'ByteDance',
212
+ category: CATEGORIES.AI_EDITOR,
213
+ signals: [
214
+ { type: 'projectPath', path: '.trae' },
215
+ { type: 'homePath', path: '.trae' },
216
+ { type: 'bin', name: 'trae' },
217
+ ],
218
+ },
219
+ ];
220
+
221
+ module.exports = { detectors, CATEGORIES };
@@ -0,0 +1,100 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { getConfigDir } = require('./config-dir');
7
+
8
+ // `~/.config/shakers/device-auth.json` — bridges the MCP start/poll tools; the secret device_code lives here (0600), never in a tool result.
9
+
10
+ const DEVICE_FILE_NAME = 'device-auth.json';
11
+ const DEVICE_FILE_VERSION = 1;
12
+
13
+ function devicePath(env = process.env) {
14
+ return path.join(getConfigDir(env), DEVICE_FILE_NAME);
15
+ }
16
+
17
+ // Atomic 0600 write (temp + fsync + rename), same recipe as auth-session-store.
18
+ function savePendingDevice(pending, env = process.env, { fs: fsImpl = fs } = {}) {
19
+ const payload = `${JSON.stringify(
20
+ {
21
+ deviceCode: typeof pending.deviceCode === 'string' && pending.deviceCode ? pending.deviceCode : null,
22
+ verificationUrl: typeof pending.verificationUrl === 'string' && pending.verificationUrl ? pending.verificationUrl : null,
23
+ userCode: typeof pending.userCode === 'string' && pending.userCode ? pending.userCode : null,
24
+ tokenEndpoint: typeof pending.tokenEndpoint === 'string' && pending.tokenEndpoint ? pending.tokenEndpoint : null,
25
+ authTokenEndpoint: typeof pending.authTokenEndpoint === 'string' && pending.authTokenEndpoint ? pending.authTokenEndpoint : null,
26
+ interval: typeof pending.interval === 'number' && pending.interval > 0 ? Math.floor(pending.interval) : 5,
27
+ expiresAt: typeof pending.expiresAt === 'string' && pending.expiresAt ? pending.expiresAt : null,
28
+ mode: pending.mode === 'register' ? 'register' : 'login',
29
+ provider: pending.provider === 'linkedin' ? 'linkedin' : 'google',
30
+ registerContext: pending.registerContext && typeof pending.registerContext === 'object' ? pending.registerContext : null,
31
+ email: typeof pending.email === 'string' && pending.email ? pending.email : null,
32
+ version: DEVICE_FILE_VERSION,
33
+ },
34
+ null,
35
+ 2,
36
+ )}\n`;
37
+
38
+ const target = devicePath(env);
39
+ const dir = path.dirname(target);
40
+ fsImpl.mkdirSync(dir, { recursive: true, mode: 0o700 });
41
+
42
+ const tmp = path.join(dir, `.${DEVICE_FILE_NAME}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`);
43
+ let fd = null;
44
+ try {
45
+ fd = fsImpl.openSync(tmp, 'wx', 0o600);
46
+ fsImpl.writeFileSync(fd, payload);
47
+ try { fsImpl.fsyncSync(fd); } catch { /* e.g. EINVAL on some filesystems */ }
48
+ fsImpl.closeSync(fd);
49
+ fd = null;
50
+ fsImpl.renameSync(tmp, target);
51
+ } catch (e) {
52
+ if (fd !== null) { try { fsImpl.closeSync(fd); } catch { /* already closed */ } }
53
+ try { fsImpl.unlinkSync(tmp); } catch { /* never existed */ }
54
+ throw e;
55
+ }
56
+ return target;
57
+ }
58
+
59
+ function loadPendingDevice(env = process.env, { fs: fsImpl = fs } = {}) {
60
+ let parsed;
61
+ try {
62
+ parsed = JSON.parse(fsImpl.readFileSync(devicePath(env), 'utf8'));
63
+ } catch {
64
+ return null;
65
+ }
66
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
67
+ if (parsed.version !== DEVICE_FILE_VERSION) return null;
68
+ if (typeof parsed.deviceCode !== 'string' || !parsed.deviceCode) return null;
69
+ return {
70
+ deviceCode: parsed.deviceCode,
71
+ verificationUrl: typeof parsed.verificationUrl === 'string' && parsed.verificationUrl ? parsed.verificationUrl : null,
72
+ userCode: typeof parsed.userCode === 'string' && parsed.userCode ? parsed.userCode : null,
73
+ tokenEndpoint: typeof parsed.tokenEndpoint === 'string' && parsed.tokenEndpoint ? parsed.tokenEndpoint : null,
74
+ authTokenEndpoint: typeof parsed.authTokenEndpoint === 'string' && parsed.authTokenEndpoint ? parsed.authTokenEndpoint : null,
75
+ interval: typeof parsed.interval === 'number' && parsed.interval > 0 ? Math.floor(parsed.interval) : 5,
76
+ expiresAt: typeof parsed.expiresAt === 'string' && parsed.expiresAt ? parsed.expiresAt : null,
77
+ mode: parsed.mode === 'register' ? 'register' : 'login',
78
+ provider: parsed.provider === 'linkedin' ? 'linkedin' : 'google',
79
+ registerContext: parsed.registerContext && typeof parsed.registerContext === 'object' ? parsed.registerContext : null,
80
+ email: typeof parsed.email === 'string' && parsed.email ? parsed.email : null,
81
+ };
82
+ }
83
+
84
+ function clearPendingDevice(env = process.env, { fs: fsImpl = fs } = {}) {
85
+ try {
86
+ fsImpl.unlinkSync(devicePath(env));
87
+ return true;
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
92
+
93
+ module.exports = {
94
+ DEVICE_FILE_NAME,
95
+ DEVICE_FILE_VERSION,
96
+ devicePath,
97
+ savePendingDevice,
98
+ loadPendingDevice,
99
+ clearPendingDevice,
100
+ };
@@ -0,0 +1,118 @@
1
+ 'use strict';
2
+
3
+ const { requestDeviceAuthorization, requestDeviceToken, requestApiToken } = require('./auth-device-client');
4
+
5
+ const SLOW_DOWN_STEP_SECONDS = 5;
6
+ const DEFAULT_MAX_WAIT_MS = 15 * 60 * 1000;
7
+
8
+ function defaultSleep(ms) {
9
+ // NOT unref'd: the loop's await sleep(...) is the only handle keeping the process alive while it waits.
10
+ return new Promise((resolve) => { setTimeout(resolve, ms); });
11
+ }
12
+
13
+ // The /cli-login page reads `provider` from the query to pick the social button.
14
+ function appendProviderParam(url, provider) {
15
+ if (!url || !provider) return url;
16
+ try {
17
+ const u = new URL(url);
18
+ u.searchParams.set('provider', provider);
19
+ return u.href;
20
+ } catch {
21
+ return `${url}${url.includes('?') ? '&' : '?'}provider=${encodeURIComponent(provider)}`;
22
+ }
23
+ }
24
+
25
+ async function startDeviceLogin({ authorizeEndpoint, clientId, scope, timeoutMs } = {}) {
26
+ return requestDeviceAuthorization({ endpoint: authorizeEndpoint, clientId, scope, timeoutMs });
27
+ }
28
+
29
+ async function pollDeviceToken({ deviceCode, tokenEndpoint, clientId, timeoutMs } = {}) {
30
+ return requestDeviceToken({ deviceCode, clientId }, { endpoint: tokenEndpoint, timeoutMs });
31
+ }
32
+
33
+ // ORDER: a NEW user needs complete-registration BEFORE GET /auth/token (else the Hub 500s: no domain user).
34
+ async function finalizeDeviceSession(
35
+ { sessionToken, isNewUser, authTokenEndpoint, completeRegistrationEndpoint = null, registrationContext = null, timeoutMs } = {},
36
+ {
37
+ requestCompleteRegistration = require('./signup-client').requestCompleteRegistration,
38
+ requestApiToken: apiToken = requestApiToken,
39
+ } = {},
40
+ ) {
41
+ if (!authTokenEndpoint) return { ok: false, reason: 'no-endpoint' };
42
+
43
+ if (isNewUser && completeRegistrationEndpoint) {
44
+ const done = await requestCompleteRegistration(registrationContext || {}, { endpoint: completeRegistrationEndpoint, bearerToken: sessionToken, timeoutMs });
45
+ if (!done.ok) return { ok: false, reason: done.reason || 'complete-registration-failed' };
46
+ }
47
+
48
+ const jwt = await apiToken({ accessToken: sessionToken }, { endpoint: authTokenEndpoint, timeoutMs });
49
+ if (!jwt.ok) return { ok: false, reason: 'token-exchange-failed' };
50
+ return { ok: true, token: jwt.token };
51
+ }
52
+
53
+ async function runDeviceLogin(
54
+ { authorizeEndpoint, tokenEndpoint, authTokenEndpoint, completeRegistrationEndpoint = null, registrationContext = null, clientId, scope, timeoutMs } = {},
55
+ {
56
+ onPrompt = null,
57
+ onWaiting = null,
58
+ sleep = defaultSleep,
59
+ now = () => Date.now(),
60
+ maxWaitMs = DEFAULT_MAX_WAIT_MS,
61
+ } = {},
62
+ ) {
63
+ if (!authorizeEndpoint || !tokenEndpoint || !authTokenEndpoint) return { ok: false, reason: 'no-endpoint' };
64
+
65
+ const authz = await startDeviceLogin({ authorizeEndpoint, clientId, scope, timeoutMs });
66
+ if (!authz.ok) return { ok: false, reason: authz.reason || 'authorize-failed' };
67
+
68
+ if (typeof onPrompt === 'function') {
69
+ onPrompt({
70
+ verificationUriComplete: authz.verificationUriComplete,
71
+ verificationUri: authz.verificationUri,
72
+ userCode: authz.userCode,
73
+ expiresIn: authz.expiresIn,
74
+ });
75
+ }
76
+
77
+ let intervalSeconds = authz.interval;
78
+ const deadline = now() + Math.min(authz.expiresIn * 1000, maxWaitMs);
79
+
80
+ for (;;) {
81
+ if (now() >= deadline) return { ok: false, reason: 'expired' };
82
+ if (typeof onWaiting === 'function') onWaiting();
83
+ await sleep(intervalSeconds * 1000);
84
+
85
+ const res = await pollDeviceToken({ deviceCode: authz.deviceCode, tokenEndpoint, clientId, timeoutMs });
86
+ if (res.status === 'authorized') {
87
+ const fin = await finalizeDeviceSession({
88
+ sessionToken: res.accessToken,
89
+ isNewUser: res.isNewUser,
90
+ authTokenEndpoint,
91
+ completeRegistrationEndpoint,
92
+ registrationContext,
93
+ timeoutMs,
94
+ });
95
+ if (!fin.ok) return { ok: false, reason: fin.reason };
96
+ return { ok: true, token: fin.token, sessionToken: res.accessToken, isNewUser: res.isNewUser, email: res.email || null };
97
+ }
98
+ if (res.status === 'pending') continue;
99
+ if (res.status === 'slow_down') {
100
+ intervalSeconds += SLOW_DOWN_STEP_SECONDS;
101
+ continue;
102
+ }
103
+ if (res.status === 'denied') return { ok: false, reason: 'denied' };
104
+ if (res.status === 'expired') return { ok: false, reason: 'expired' };
105
+ return { ok: false, reason: res.reason || 'poll-failed' };
106
+ }
107
+ }
108
+
109
+ module.exports = {
110
+ startDeviceLogin,
111
+ pollDeviceToken,
112
+ finalizeDeviceSession,
113
+ runDeviceLogin,
114
+ appendProviderParam,
115
+ defaultSleep,
116
+ SLOW_DOWN_STEP_SECONDS,
117
+ DEFAULT_MAX_WAIT_MS,
118
+ };
@@ -0,0 +1,152 @@
1
+ 'use strict';
2
+
3
+ const { requestBackend } = require('./backend-request');
4
+
5
+ // Email-verification client + wait-mode loop (skill-code-certification, ADR-006).
6
+
7
+ const DEFAULT_TIMEOUT_MS = 15000;
8
+ // Bounded so a Talent who can never produce a matching code can't loop forever — after this many CODE-ENTRY attempts we give up (nothing persisted, asked again next run).
9
+ const MAX_CODE_ATTEMPTS = 5;
10
+
11
+ // POST {email} to the request endpoint.
12
+ async function requestCode({ email }, { url, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
13
+ if (!url) return { ok: false, reason: 'no-endpoint' };
14
+
15
+ let res;
16
+ try {
17
+ res = await requestBackend({ endpoint: url, body: { email }, timeoutMs });
18
+ } catch (e) {
19
+ if (e && e.kind === 'timeout') return { ok: false, reason: 'timeout' };
20
+ return { ok: false, reason: 'network-error', detail: e && e.message };
21
+ }
22
+
23
+ if (res.status < 200 || res.status >= 300) {
24
+ return { ok: false, reason: `http-${res.status}` };
25
+ }
26
+ return { ok: true, backend: res.backend };
27
+ }
28
+
29
+ // Maps a server "not verified" outcome to a soft reason the loop can retry on.
30
+ function mapNotVerifiedReason(bodyReason) {
31
+ return bodyReason === 'expired' ? 'expired' : 'invalid-code';
32
+ }
33
+
34
+ // POST {email, code} to the verify endpoint.
35
+ async function verifyCode({ email, code }, { url, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
36
+ if (!url) return { ok: false, reason: 'no-endpoint' };
37
+
38
+ let res;
39
+ try {
40
+ res = await requestBackend({ endpoint: url, body: { email, code }, timeoutMs });
41
+ } catch (e) {
42
+ if (e && e.kind === 'timeout') return { ok: false, reason: 'timeout' };
43
+ return { ok: false, reason: 'network-error', detail: e && e.message };
44
+ }
45
+
46
+ let parsed = null;
47
+ try {
48
+ parsed = res.raw ? JSON.parse(res.raw) : null;
49
+ } catch {
50
+ // fall through: unparseable body handled per status below
51
+ }
52
+
53
+ if (res.status >= 200 && res.status < 300) {
54
+ if (parsed && parsed.verified === true) return { ok: true, verified: true, backend: res.backend };
55
+ if (parsed && typeof parsed === 'object') return { ok: false, reason: mapNotVerifiedReason(parsed.reason) };
56
+ return { ok: false, reason: 'invalid-json' };
57
+ }
58
+
59
+ // Non-2xx: a bad/expired code the server rejects at the HTTP layer is still
60
+ // a SOFT failure (retryable). 5xx and anything else is technical.
61
+ if (res.status === 400 || res.status === 401 || res.status === 410 || res.status === 422) {
62
+ return { ok: false, reason: mapNotVerifiedReason(parsed && parsed.reason) };
63
+ }
64
+ return { ok: false, reason: `http-${res.status}` };
65
+ }
66
+
67
+ // Categorizes a verifyCode reason for the wait-mode loop: 'soft' -> invalid/expired code: show a message, keep waiting (retry).
68
+ function classifyVerifyReason(reason) {
69
+ if (reason === 'invalid-code' || reason === 'expired') return 'soft';
70
+ return 'technical';
71
+ }
72
+
73
+ // The interactive "modo espera": send a code, then loop reading the pasted code until it verifies, the Talent cancels, or attempts run out.
74
+ // talents-ai-score, ADR-042: `requestUrl`/`verifyUrl` address the ONE backend (the certifications service).
75
+ async function runEmailVerification({
76
+ email,
77
+ ask,
78
+ catalog,
79
+ requestUrl,
80
+ verifyUrl,
81
+ deps = {},
82
+ }) {
83
+ const v = catalog.verify;
84
+ const doRequest = deps.requestCode || requestCode;
85
+ const doVerify = deps.verifyCode || verifyCode;
86
+ const write = deps.write || ((s) => process.stdout.write(s));
87
+
88
+ // No endpoint could be derived (ingest unset): nothing to verify against,
89
+ // and nowhere to persist either. Quiet, non-error outcome.
90
+ if (!requestUrl || !verifyUrl) {
91
+ write(` ${v.unavailable}\n`);
92
+ return { verified: false, reason: 'unavailable' };
93
+ }
94
+
95
+ const sent = await doRequest({ email }, { url: requestUrl });
96
+ if (!sent.ok) {
97
+ write(` ${v.requestFailed}\n`);
98
+ return { verified: false, reason: 'request-failed' };
99
+ }
100
+
101
+ // Entered wait mode.
102
+ write(`\n ${v.sent(email)}\n`);
103
+ write(` ${v.waitHint}\n`);
104
+
105
+ for (let attempt = 0; attempt < MAX_CODE_ATTEMPTS; ) {
106
+ // The OTP is a secret too (issue 102): same reader, same echo, and the fix is the same call.
107
+ const readCode = typeof ask.secret === 'function' ? ask.secret.bind(ask) : ask;
108
+ const raw = String(await readCode(v.codePrompt)).trim();
109
+
110
+ // Empty line / EOF (piped, closed stdin) / Ctrl-C-then-Enter -> cancel.
111
+ if (raw === '') {
112
+ write(` ${v.cancelled}\n`);
113
+ return { verified: false, reason: 'cancelled' };
114
+ }
115
+
116
+ // Resend, without consuming an attempt.
117
+ if (/^r$/i.test(raw)) {
118
+ const resent = await doRequest({ email }, { url: requestUrl });
119
+ write(resent.ok ? ` ${v.resent(email)}\n` : ` ${v.resendFailed}\n`);
120
+ continue;
121
+ }
122
+
123
+ attempt++;
124
+ const result = await doVerify({ email, code: raw }, { url: verifyUrl });
125
+ if (result.ok && result.verified) {
126
+ write(` ${v.verified}\n`);
127
+ return { verified: true, backend: result.backend };
128
+ }
129
+
130
+ if (classifyVerifyReason(result.reason) === 'soft') {
131
+ write(result.reason === 'expired' ? ` ${v.expired}\n` : ` ${v.invalidCode}\n`);
132
+ continue;
133
+ }
134
+
135
+ // Technical: the Hub couldn't be reached or answered unexpectedly. Legible
136
+ // error, bail out (nothing persisted); the report was already shown.
137
+ write(` ${v.technicalError}\n`);
138
+ return { verified: false, reason: 'technical' };
139
+ }
140
+
141
+ write(` ${v.tooManyAttempts}\n`);
142
+ return { verified: false, reason: 'exhausted' };
143
+ }
144
+
145
+ module.exports = {
146
+ requestCode,
147
+ verifyCode,
148
+ classifyVerifyReason,
149
+ runEmailVerification,
150
+ DEFAULT_TIMEOUT_MS,
151
+ MAX_CODE_ATTEMPTS,
152
+ };
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+
5
+ // Shared home-directory + env resolution (talents-ai-score, ADR-014 — "Alcance del tier: Talento = proyecto ∪ home", applied uniformly to every category).
6
+ function readEnv(env, suffix) {
7
+ const next = env[`SHAKERS_CLI_${suffix}`];
8
+ if (next !== undefined && next !== '') return next;
9
+ return env[`AI_FOOTPRINT_${suffix}`];
10
+ }
11
+
12
+ function getHomeDir(env = process.env) {
13
+ return readEnv(env, 'HOME_DIR') || os.homedir();
14
+ }
15
+
16
+ module.exports = { getHomeDir, readEnv };