shakers-cli 0.0.0-stage → 0.14.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.
Files changed (237) hide show
  1. package/LICENSE +59 -0
  2. package/README.md +1233 -2
  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 +99 -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 +124 -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 +216 -0
  20. package/bin/mcp.js +124 -0
  21. package/bin/mcpb.js +5 -0
  22. package/bin/me.js +69 -0
  23. package/bin/onboarding.js +134 -0
  24. package/bin/portfolios.js +69 -0
  25. package/bin/rate.js +70 -0
  26. package/bin/register.js +337 -0
  27. package/bin/report-html.js +134 -0
  28. package/bin/save.js +77 -0
  29. package/bin/shakers.js +363 -0
  30. package/bin/share.js +72 -0
  31. package/bin/show-project.js +72 -0
  32. package/bin/socials.js +70 -0
  33. package/bin/superadmin.js +351 -0
  34. package/bin/update-check-worker.js +70 -0
  35. package/bin/update.js +111 -0
  36. package/package.json +40 -4
  37. package/src/agent-category.js +13 -0
  38. package/src/agent-evaluation.js +177 -0
  39. package/src/agent-org-chart.js +355 -0
  40. package/src/agent-synthesis.js +132 -0
  41. package/src/agent-usage.js +118 -0
  42. package/src/agents-client.js +165 -0
  43. package/src/ai-fluency-cell.js +72 -0
  44. package/src/ai-profile-client.js +45 -0
  45. package/src/ai-profile-preview.js +154 -0
  46. package/src/ai.js +66 -0
  47. package/src/alma-client.js +123 -0
  48. package/src/alma-flow.js +32 -0
  49. package/src/alma-repo-context.js +47 -0
  50. package/src/alma-shell.js +161 -0
  51. package/src/ansi.js +62 -0
  52. package/src/applications-client.js +79 -0
  53. package/src/applications-flow.js +58 -0
  54. package/src/auth-client.js +144 -0
  55. package/src/auth-device-client.js +118 -0
  56. package/src/auth-method-picker.js +44 -0
  57. package/src/auth-methods.js +35 -0
  58. package/src/auth-session-store.js +215 -0
  59. package/src/authorship-mismatch.js +62 -0
  60. package/src/authorship.js +204 -0
  61. package/src/automations-detector.js +170 -0
  62. package/src/availability-client.js +93 -0
  63. package/src/availability-flow.js +124 -0
  64. package/src/backend-request.js +191 -0
  65. package/src/blind-approval-extract.js +48 -0
  66. package/src/brand-ansi.js +14 -0
  67. package/src/brand-svg.js +9 -0
  68. package/src/browser-tools-detector.js +21 -0
  69. package/src/build-next-level.js +56 -0
  70. package/src/certifications-client.js +89 -0
  71. package/src/certifications-flow.js +75 -0
  72. package/src/certify-dimension-client.js +163 -0
  73. package/src/certify-dimension-flow.js +135 -0
  74. package/src/certify-disclaimer.js +51 -0
  75. package/src/certify-remediation-prompt.js +23 -0
  76. package/src/cli-args.js +83 -0
  77. package/src/command-audit.js +105 -0
  78. package/src/config-dir.js +120 -0
  79. package/src/config.js +877 -0
  80. package/src/consent-flow.js +115 -0
  81. package/src/consent-skip.js +27 -0
  82. package/src/cv-file.js +62 -0
  83. package/src/cv-text.js +92 -0
  84. package/src/decisions-extract.js +43 -0
  85. package/src/detect-repos.js +145 -0
  86. package/src/detected-tools.js +15 -0
  87. package/src/detection-evidence.js +105 -0
  88. package/src/detection-noise.js +132 -0
  89. package/src/detectors.js +221 -0
  90. package/src/device-auth-store.js +100 -0
  91. package/src/device-login.js +120 -0
  92. package/src/email-verification.js +152 -0
  93. package/src/env-paths.js +30 -0
  94. package/src/find-projects-cache.js +89 -0
  95. package/src/find-projects-client.js +148 -0
  96. package/src/find-projects-flow.js +164 -0
  97. package/src/git-activity.js +124 -0
  98. package/src/git-log-prefetch.js +53 -0
  99. package/src/graph-certs.js +58 -0
  100. package/src/graph-generator.js +230 -0
  101. package/src/graph-scan.js +239 -0
  102. package/src/i18n.js +3341 -0
  103. package/src/inter-fonts.js +6 -0
  104. package/src/interactive-select.js +183 -0
  105. package/src/interview-client.js +226 -0
  106. package/src/interview-transcript.js +56 -0
  107. package/src/inventory-client.js +90 -0
  108. package/src/invitations-client.js +66 -0
  109. package/src/invitations-flow.js +50 -0
  110. package/src/lang-client.js +147 -0
  111. package/src/lang-codes.js +14 -0
  112. package/src/lang-flow.js +85 -0
  113. package/src/legal-notice.js +53 -0
  114. package/src/locale.js +72 -0
  115. package/src/markdown-ansi.js +50 -0
  116. package/src/maturity.js +111 -0
  117. package/src/mcp-ai-usage-result-tool.js +111 -0
  118. package/src/mcp-ai-usage-tool.js +401 -0
  119. package/src/mcp-alma-tools.js +95 -0
  120. package/src/mcp-applications-tools.js +44 -0
  121. package/src/mcp-auth-form.js +480 -0
  122. package/src/mcp-availability-tools.js +45 -0
  123. package/src/mcp-certifications-tools.js +44 -0
  124. package/src/mcp-certify-dimension-tools.js +66 -0
  125. package/src/mcp-choice.js +95 -0
  126. package/src/mcp-detector.js +223 -0
  127. package/src/mcp-device-login-tools.js +177 -0
  128. package/src/mcp-find-projects-tools.js +69 -0
  129. package/src/mcp-install.js +275 -0
  130. package/src/mcp-instructions.js +27 -0
  131. package/src/mcp-invitations-tools.js +44 -0
  132. package/src/mcp-lang-tools.js +41 -0
  133. package/src/mcp-onboarding-tools.js +379 -0
  134. package/src/mcp-portfolios-tools.js +50 -0
  135. package/src/mcp-profile-tools.js +48 -0
  136. package/src/mcp-prompts.js +73 -0
  137. package/src/mcp-rate-tools.js +44 -0
  138. package/src/mcp-register-tools.js +441 -0
  139. package/src/mcp-report-tools.js +105 -0
  140. package/src/mcp-role-tools.js +372 -0
  141. package/src/mcp-saved-positions-tools.js +59 -0
  142. package/src/mcp-server.js +290 -0
  143. package/src/mcp-show-project-tools.js +52 -0
  144. package/src/mcp-signup-client.js +118 -0
  145. package/src/mcp-signup-tools.js +1171 -0
  146. package/src/mcp-socials-tools.js +41 -0
  147. package/src/memory-structure-detector.js +108 -0
  148. package/src/model-call-record.js +198 -0
  149. package/src/onboarding-availability.js +26 -0
  150. package/src/onboarding-client.js +374 -0
  151. package/src/onboarding-flow.js +1162 -0
  152. package/src/onboarding-funnel.js +272 -0
  153. package/src/onboarding-livekit-client.js +102 -0
  154. package/src/onboarding-status-client.js +57 -0
  155. package/src/open-file.js +32 -0
  156. package/src/osc-link.js +16 -0
  157. package/src/portfolio-client.js +164 -0
  158. package/src/portfolio-git-info.js +61 -0
  159. package/src/portfolios-client.js +85 -0
  160. package/src/portfolios-flow.js +79 -0
  161. package/src/profile-client.js +133 -0
  162. package/src/profile-flow.js +115 -0
  163. package/src/profile-switch.js +46 -0
  164. package/src/prompt-select.js +66 -0
  165. package/src/rate-client.js +81 -0
  166. package/src/rate-flow.js +137 -0
  167. package/src/register-context.js +145 -0
  168. package/src/render-ai-fluency.js +266 -0
  169. package/src/render-certification.js +177 -0
  170. package/src/render-html.js +140 -0
  171. package/src/render-report-text.js +27 -0
  172. package/src/render-sheet.js +261 -0
  173. package/src/render-terminal.js +109 -0
  174. package/src/render-usage-report-text.js +155 -0
  175. package/src/repo-context.js +215 -0
  176. package/src/report-gating.js +49 -0
  177. package/src/report-store.js +226 -0
  178. package/src/report-theme.js +177 -0
  179. package/src/report-view-client.js +50 -0
  180. package/src/reveal-output.js +29 -0
  181. package/src/roadmap-content.js +453 -0
  182. package/src/roadmap-personalization.js +132 -0
  183. package/src/roadmap-prompt.js +154 -0
  184. package/src/roles-client.js +112 -0
  185. package/src/roles-flow.js +192 -0
  186. package/src/sanitize-network-text.js +36 -0
  187. package/src/saved-positions-client.js +71 -0
  188. package/src/saved-positions-flow.js +50 -0
  189. package/src/scan-exclusions.js +50 -0
  190. package/src/scanner.js +624 -0
  191. package/src/scope-signals.js +148 -0
  192. package/src/secret-prompt.js +72 -0
  193. package/src/session-gate.js +30 -0
  194. package/src/session-normalizers.js +174 -0
  195. package/src/session-refresh.js +51 -0
  196. package/src/session-scan.js +475 -0
  197. package/src/session-signals.js +36 -0
  198. package/src/shakers-wordmark.js +17 -0
  199. package/src/share-card.js +362 -0
  200. package/src/share-certification.js +111 -0
  201. package/src/share-consent-state.js +157 -0
  202. package/src/share-payload.js +300 -0
  203. package/src/share.js +185 -0
  204. package/src/show-project-client.js +58 -0
  205. package/src/show-project-flow.js +119 -0
  206. package/src/signup-client.js +103 -0
  207. package/src/signup-copy.js +450 -0
  208. package/src/signup-language.js +21 -0
  209. package/src/skills-client.js +102 -0
  210. package/src/socials-client.js +91 -0
  211. package/src/socials-flow.js +83 -0
  212. package/src/specificity-extract.js +41 -0
  213. package/src/start-add-agents.js +306 -0
  214. package/src/start-add-portfolio.js +388 -0
  215. package/src/start-add-skills.js +260 -0
  216. package/src/stdin-ask.js +129 -0
  217. package/src/steering-extract.js +43 -0
  218. package/src/strip-internal-comments.js +45 -0
  219. package/src/tech-detector.js +295 -0
  220. package/src/templates/report-sheet.html +572 -0
  221. package/src/terminal-agents.js +207 -0
  222. package/src/terminal-format.js +85 -0
  223. package/src/terminal-progress.js +106 -0
  224. package/src/terminal-sections.js +219 -0
  225. package/src/tier-analysis.js +126 -0
  226. package/src/tier-engine.js +114 -0
  227. package/src/tier-projection.js +117 -0
  228. package/src/transcript-turns.js +49 -0
  229. package/src/translations-client.js +84 -0
  230. package/src/typewriter.js +154 -0
  231. package/src/update-flow.js +223 -0
  232. package/src/update-notifier.js +113 -0
  233. package/src/usage-model-calls.js +119 -0
  234. package/src/usage-oneshot-commands.js +180 -0
  235. package/src/usage-repo-scope.js +162 -0
  236. package/src/usage-run-steps.js +354 -0
  237. package/src/work-streams.js +79 -0
@@ -0,0 +1,154 @@
1
+ 'use strict';
2
+
3
+ const { tierName: getTierName } = require('./i18n');
4
+
5
+ // Deterministic, ready-to-paste "implementation prompt" (talents-ai-score: "next steps -> prompt").
6
+
7
+ const TEMPLATES = {
8
+ es: {
9
+ intro: (frameworksText) =>
10
+ `Ayúdame a implementar el siguiente paso en mi proyecto${frameworksText ? ` (uso ${frameworksText})` : ''}.`,
11
+ context: (tierKey, tierName) =>
12
+ `Contexto: mi proyecto está en el tier ${tierKey} (${tierName}) según AI Footprint, una herramienta de auto-diagnóstico de uso de IA.`,
13
+ unlocksLabel: 'Lo que quiero conseguir:',
14
+ toolsLabel: 'Herramientas de IA que ya uso:',
15
+ stepsLabel: 'Pasos a seguir:',
16
+ snippetLabel: 'Punto de partida (referencia — adáptalo a mi proyecto real):',
17
+ closing:
18
+ 'Hazlo directamente en mi proyecto: crea o edita lo que haga falta, sigue las convenciones que ya uso, '
19
+ + 'y al final explícame brevemente qué has cambiado y por qué.',
20
+ introMax: (frameworksText) =>
21
+ `Ayúdame a consolidar y afinar el setup de IA de mi proyecto${frameworksText ? ` (uso ${frameworksText})` : ''}.`,
22
+ contextMax: (tierKey, tierName) =>
23
+ `Contexto: mi proyecto ya está en el tier máximo ${tierKey} (${tierName}) según AI Footprint. No busco añadir más herramientas, sino sacarle más partido a lo que ya tengo.`,
24
+ remainsLabel: 'Dónde está el margen ahora:',
25
+ stepsLabelMax: 'Refinamientos a aplicar:',
26
+ closingMax:
27
+ 'Revisa mi proyecto y aplícalo directamente: recorta lo que estorbe, afina lo existente y '
28
+ + 'sigue las convenciones que ya uso; al final explícame brevemente qué has cambiado y por qué.',
29
+ },
30
+ en: {
31
+ intro: (frameworksText) =>
32
+ `Help me implement the next step in my project${frameworksText ? ` (I use ${frameworksText})` : ''}.`,
33
+ context: (tierKey, tierName) =>
34
+ `Context: my project is at tier ${tierKey} (${tierName}) according to AI Footprint, a self-diagnostic tool for AI usage.`,
35
+ unlocksLabel: 'What I want to achieve:',
36
+ toolsLabel: 'AI tools I already use:',
37
+ stepsLabel: 'Steps to follow:',
38
+ snippetLabel: 'Starting point (reference — adapt it to my actual project):',
39
+ closing:
40
+ "Do it directly in my project: create or edit whatever's needed, follow the conventions I already use, "
41
+ + 'and briefly explain at the end what you changed and why.',
42
+ introMax: (frameworksText) =>
43
+ `Help me consolidate and sharpen my project's AI setup${frameworksText ? ` (I use ${frameworksText})` : ''}.`,
44
+ contextMax: (tierKey, tierName) =>
45
+ `Context: my project is already at the top tier ${tierKey} (${tierName}) according to AI Footprint. I'm not looking to add more tools, but to get more out of what I already have.`,
46
+ remainsLabel: 'Where the margin is now:',
47
+ stepsLabelMax: 'Refinements to apply:',
48
+ closingMax:
49
+ 'Review my project and apply it directly: trim what gets in the way, sharpen what exists and '
50
+ + 'follow the conventions I already use; briefly explain at the end what you changed and why.',
51
+ },
52
+ };
53
+
54
+ function resolveTemplate(lang) {
55
+ return TEMPLATES[lang] || TEMPLATES.es;
56
+ }
57
+
58
+ // `entry` is the roadmap entry ALREADY resolved by the caller (curated, or curated-with-personalization-merged — src/roadmap-personalization.js's mergeRoadmapPersonalization).
59
+ function buildImplementationPrompt(entry, report, maturity, lang) {
60
+ if (!entry) return null;
61
+ if (entry.maxTier) return buildConsolidationPrompt(entry, report, maturity, lang);
62
+
63
+ const T = resolveTemplate(lang);
64
+ const r = report || {};
65
+ const frameworks = Array.isArray(r.technologies) ? r.technologies : [];
66
+ const toolNames = Array.isArray(r.tools) ? r.tools.filter((x) => x && x.detected).map((x) => x.name) : [];
67
+ const tierKey = (maturity && maturity.tierKey) || entry.tierKey || '';
68
+ // talents-ai-score, i18n audit: NEVER maturity.tierName directly — that field comes straight from tier-engine.js and is Spanish-only by design (domain logic, not i18n).
69
+ const tierName = getTierName(tierKey, lang);
70
+
71
+ const lines = [];
72
+ lines.push(T.intro(frameworks.join(', ')));
73
+ lines.push('');
74
+ lines.push(T.context(tierKey, tierName));
75
+ lines.push('');
76
+ lines.push(T.unlocksLabel);
77
+ lines.push(entry.unlocks);
78
+
79
+ if (toolNames.length) {
80
+ lines.push('');
81
+ lines.push(T.toolsLabel);
82
+ lines.push(toolNames.join(', '));
83
+ }
84
+
85
+ lines.push('');
86
+ lines.push(T.stepsLabel);
87
+ const steps = Array.isArray(entry.steps) ? entry.steps : [];
88
+ steps.forEach((s, i) => {
89
+ lines.push(`${i + 1}. ${s.text}${s.estimate ? ` (${s.estimate})` : ''}`);
90
+ });
91
+
92
+ if (entry.snippet && entry.snippet.code) {
93
+ lines.push('');
94
+ lines.push(T.snippetLabel);
95
+ if (entry.snippet.filename) lines.push(`${entry.snippet.filename}:`);
96
+ lines.push('```' + (entry.snippet.language || ''));
97
+ lines.push(entry.snippet.code);
98
+ lines.push('```');
99
+ if (entry.snippet.secondFile) {
100
+ lines.push('');
101
+ lines.push(`${entry.snippet.secondFile.filename}:`);
102
+ lines.push('```' + (entry.snippet.language || ''));
103
+ lines.push(entry.snippet.secondFile.code);
104
+ lines.push('```');
105
+ }
106
+ }
107
+
108
+ lines.push('');
109
+ lines.push(T.closing);
110
+
111
+ return lines.join('\n');
112
+ }
113
+
114
+ function buildConsolidationPrompt(entry, report, maturity, lang) {
115
+ const steps = Array.isArray(entry.consolidationSteps) ? entry.consolidationSteps : [];
116
+ if (steps.length === 0 && !entry.whatRemains) return null;
117
+
118
+ const T = resolveTemplate(lang);
119
+ const r = report || {};
120
+ const frameworks = Array.isArray(r.technologies) ? r.technologies : [];
121
+ const toolNames = Array.isArray(r.tools) ? r.tools.filter((x) => x && x.detected).map((x) => x.name) : [];
122
+ const tierKey = (maturity && maturity.tierKey) || entry.tierKey || 'T7';
123
+ const tierName = getTierName(tierKey, lang);
124
+
125
+ const lines = [];
126
+ lines.push(T.introMax(frameworks.join(', ')));
127
+ lines.push('');
128
+ lines.push(T.contextMax(tierKey, tierName));
129
+
130
+ if (entry.whatRemains) {
131
+ lines.push('');
132
+ lines.push(T.remainsLabel);
133
+ lines.push(entry.whatRemains);
134
+ }
135
+
136
+ if (toolNames.length) {
137
+ lines.push('');
138
+ lines.push(T.toolsLabel);
139
+ lines.push(toolNames.join(', '));
140
+ }
141
+
142
+ if (steps.length) {
143
+ lines.push('');
144
+ lines.push(T.stepsLabelMax);
145
+ steps.forEach((s, i) => lines.push(`${i + 1}. ${s}`));
146
+ }
147
+
148
+ lines.push('');
149
+ lines.push(T.closingMax);
150
+
151
+ return lines.join('\n');
152
+ }
153
+
154
+ module.exports = { buildImplementationPrompt };
@@ -0,0 +1,112 @@
1
+ 'use strict';
2
+
3
+ // Role/cluster hub clients (talent Bearer): list the addable catalog, list the
4
+ // talent's own assigned roles, add a GROWTH role, and set the MAIN role.
5
+
6
+ const { postJsonWithTimeout } = require('./backend-request');
7
+ const { reasonForError } = require('./onboarding-client');
8
+
9
+ const DEFAULT_TIMEOUT_MS = 20000;
10
+
11
+ function parseJson(raw) {
12
+ try {
13
+ return raw ? JSON.parse(raw) : null;
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ function envelopeData(raw) {
20
+ const json = parseJson(raw);
21
+ if (!json || typeof json !== 'object') return undefined;
22
+ return json.status === 'OK' ? json.data : json;
23
+ }
24
+
25
+ function bearerHeaders(hubAccessToken) {
26
+ const headers = {};
27
+ if (typeof hubAccessToken === 'string' && hubAccessToken) headers.Authorization = `Bearer ${hubAccessToken}`;
28
+ return headers;
29
+ }
30
+
31
+ function normalizeRole(r) {
32
+ if (!r || typeof r !== 'object') return null;
33
+ const clusterId = typeof r.clusterId === 'string' ? r.clusterId : null;
34
+ if (!clusterId) return null;
35
+ return {
36
+ clusterId,
37
+ // Hub's assigned-clusters rows name the role in clusterName.
38
+ name: typeof r.name === 'string' ? r.name : (typeof r.clusterName === 'string' ? r.clusterName : null),
39
+ category: typeof r.category === 'string' ? r.category : null,
40
+ proposalKind: typeof r.proposalKind === 'string' ? r.proposalKind : null,
41
+ type: typeof r.type === 'string' ? r.type : null,
42
+ source: typeof r.source === 'string' ? r.source : null,
43
+ };
44
+ }
45
+
46
+ async function requestAvailableRoles({ hubAccessToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
47
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
48
+ if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
49
+ let res;
50
+ try {
51
+ res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, bearerHeaders(hubAccessToken));
52
+ } catch (e) {
53
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
54
+ }
55
+ if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
56
+ const data = envelopeData(res.raw);
57
+ const rows = data && Array.isArray(data.roles) ? data.roles : (Array.isArray(data) ? data : null);
58
+ if (!rows) return { ok: false, reason: 'bad-response' };
59
+ return { ok: true, roles: rows.map(normalizeRole).filter(Boolean) };
60
+ }
61
+
62
+ async function requestAssignedClusters({ hubAccessToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
63
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
64
+ if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
65
+ let res;
66
+ try {
67
+ res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, bearerHeaders(hubAccessToken));
68
+ } catch (e) {
69
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
70
+ }
71
+ if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
72
+ const data = envelopeData(res.raw);
73
+ const rows = Array.isArray(data) ? data : (data && Array.isArray(data.clusters) ? data.clusters : null);
74
+ if (!rows) return { ok: false, reason: 'bad-response' };
75
+ return { ok: true, clusters: rows.map(normalizeRole).filter(Boolean) };
76
+ }
77
+
78
+ async function requestAddGrowthRole({ clusterId, hubAccessToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
79
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
80
+ if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
81
+ if (typeof clusterId !== 'string' || !clusterId) return { ok: false, reason: 'no-cluster' };
82
+ let res;
83
+ try {
84
+ res = await postJsonWithTimeout(endpoint, { clusterId }, timeoutMs, 'POST', null, bearerHeaders(hubAccessToken));
85
+ } catch (e) {
86
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
87
+ }
88
+ if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
89
+ return { ok: true, clusterId };
90
+ }
91
+
92
+ async function requestSetMainRole({ clusterId, hubAccessToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
93
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
94
+ if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
95
+ if (typeof clusterId !== 'string' || !clusterId) return { ok: false, reason: 'no-cluster' };
96
+ let res;
97
+ try {
98
+ res = await postJsonWithTimeout(endpoint, { clusterId }, timeoutMs, 'PATCH', null, bearerHeaders(hubAccessToken));
99
+ } catch (e) {
100
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
101
+ }
102
+ if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
103
+ return { ok: true, clusterId };
104
+ }
105
+
106
+ module.exports = {
107
+ normalizeRole,
108
+ requestAvailableRoles,
109
+ requestAssignedClusters,
110
+ requestAddGrowthRole,
111
+ requestSetMainRole,
112
+ };
@@ -0,0 +1,192 @@
1
+ 'use strict';
2
+
3
+ // Shared role flows: add a GROWTH role, change the MAIN role, and the onboarding
4
+ // final main-role step. Every choice uses the dynamic arrow selector (io.select).
5
+
6
+ function makeRolesDeps(overrides = {}) {
7
+ const config = require('./config');
8
+ const client = require('./roles-client');
9
+ const certs = require('./certifications-client');
10
+ return {
11
+ getAvailableRolesEndpoint: config.getAvailableRolesEndpoint,
12
+ getSetMainRoleEndpoint: config.getSetMainRoleEndpoint,
13
+ getAssignedClustersEndpoint: config.getAssignedClustersEndpoint,
14
+ getMyCertificationsEndpoint: config.getMyCertificationsEndpoint,
15
+ requestAvailableRoles: client.requestAvailableRoles,
16
+ requestAssignedClusters: client.requestAssignedClusters,
17
+ requestAddGrowthRole: client.requestAddGrowthRole,
18
+ requestSetMainRole: client.requestSetMainRole,
19
+ requestMeCertifications: (args, opts) => certs.requestMeCertifications(args, opts),
20
+ ...overrides,
21
+ };
22
+ }
23
+
24
+ function roleLabel(role) {
25
+ if (!role) return '';
26
+ const name = role.name || role.clusterId;
27
+ return role.category ? `${name} — ${role.category}` : name;
28
+ }
29
+
30
+ function dedupeByCluster(roles) {
31
+ const seen = new Set();
32
+ const out = [];
33
+ for (const r of roles || []) {
34
+ if (!r || !r.clusterId || seen.has(r.clusterId)) continue;
35
+ seen.add(r.clusterId);
36
+ out.push(r);
37
+ }
38
+ return out;
39
+ }
40
+
41
+ async function chooseRole({ io, roles, header, hint, initialClusterId = null }) {
42
+ const initialMarked = [];
43
+ if (initialClusterId) {
44
+ const idx = roles.findIndex((r) => r.clusterId === initialClusterId);
45
+ if (idx >= 0) initialMarked.push(idx);
46
+ }
47
+ return io.select({ header, hint, items: roles, labelFor: roleLabel, initialMarked });
48
+ }
49
+
50
+ // `add-role`: list the addable catalog and add the chosen one as a GROWTH role.
51
+ async function runAddRole({ io, session, catalog, deps = makeRolesDeps() }) {
52
+ const r = catalog.roles;
53
+ const hubAccessToken = session ? session.hubAccessToken : null;
54
+
55
+ const listEndpoint = deps.getAvailableRolesEndpoint();
56
+ const res = await io.withProgress(r.loadingAvailable, () =>
57
+ deps.requestAvailableRoles({ hubAccessToken }, { endpoint: listEndpoint }));
58
+ if (!res.ok) {
59
+ io.error(r.fetchFailed(res.reason));
60
+ return { ok: false, reason: res.reason };
61
+ }
62
+ const roles = dedupeByCluster(res.roles);
63
+ if (roles.length === 0) {
64
+ io.notify(r.noneAvailable);
65
+ return { ok: true, added: null };
66
+ }
67
+
68
+ io.section(r.addTitle);
69
+ const choice = await chooseRole({ io, roles, header: r.addPrompt, hint: r.selectHint });
70
+ if (!choice) {
71
+ io.notify(r.cancelled);
72
+ return { ok: true, added: null };
73
+ }
74
+
75
+ const addEndpoint = deps.getAssignedClustersEndpoint();
76
+ const added = await io.withProgress(r.savingAdd, () =>
77
+ deps.requestAddGrowthRole({ clusterId: choice.clusterId, hubAccessToken }, { endpoint: addEndpoint }));
78
+ if (!added.ok) {
79
+ if (added.reason === 'already-assigned') { io.warn(r.alreadyAssigned(roleLabel(choice))); return { ok: true, added: choice.clusterId }; }
80
+ io.error(r.addFailed(added.reason));
81
+ return { ok: false, reason: added.reason };
82
+ }
83
+ io.success(r.addedOk(roleLabel(choice)));
84
+ return { ok: true, added: choice.clusterId };
85
+ }
86
+
87
+ // Candidate roles the talent already holds (their own GROWTH picks + growing-into),
88
+ // deduped, for setting/switching the MAIN role.
89
+ async function fetchHeldRoles({ deps, hubAccessToken }) {
90
+ const held = [];
91
+ let mainClusterId = null;
92
+ const certEndpoint = deps.getMyCertificationsEndpoint();
93
+ if (certEndpoint) {
94
+ const cert = await deps.requestMeCertifications({ hubAccessToken }, { endpoint: certEndpoint });
95
+ if (cert.ok) {
96
+ if (cert.mainRole && cert.mainRole.clusterId) { held.push(cert.mainRole); mainClusterId = cert.mainRole.clusterId; }
97
+ for (const g of cert.growingInto || []) held.push(g);
98
+ }
99
+ }
100
+ const assignedEndpoint = deps.getAssignedClustersEndpoint();
101
+ if (assignedEndpoint) {
102
+ const assigned = await deps.requestAssignedClusters({ hubAccessToken }, { endpoint: assignedEndpoint });
103
+ if (assigned.ok) for (const c of assigned.clusters || []) held.push(c);
104
+ }
105
+ return { held: dedupeByCluster(held), mainClusterId };
106
+ }
107
+
108
+ // `change-role`: switch the MAIN role to one of the roles the talent already holds.
109
+ async function runChangeMainRole({ io, session, catalog, deps = makeRolesDeps() }) {
110
+ const r = catalog.roles;
111
+ const hubAccessToken = session ? session.hubAccessToken : null;
112
+
113
+ const { held, mainClusterId } = await io.withProgress(r.loadingHeld, () => fetchHeldRoles({ deps, hubAccessToken }));
114
+ const candidates = held.filter((c) => c.clusterId !== mainClusterId);
115
+ if (candidates.length === 0) {
116
+ io.notify(r.noOtherRoles);
117
+ return { ok: true, changed: null };
118
+ }
119
+
120
+ io.section(r.changeTitle);
121
+ const current = held.find((c) => c.clusterId === mainClusterId);
122
+ if (current) io.notify(r.currentMain(roleLabel(current)));
123
+ const choice = await chooseRole({ io, roles: candidates, header: r.changePrompt, hint: r.selectHint });
124
+ if (!choice) {
125
+ io.notify(r.cancelled);
126
+ return { ok: true, changed: null };
127
+ }
128
+ return applySetMain({ io, deps, hubAccessToken, choice, catalog });
129
+ }
130
+
131
+ // The onboarding final step: pick a MAIN role from the roles resolved for the
132
+ // talent (interview recommendations + any held). Degrades gracefully to a skip.
133
+ async function runOnboardingMainRoleStep({ io, session, catalog, deps = makeRolesDeps() }) {
134
+ const r = catalog.roles;
135
+ const hubAccessToken = session ? session.hubAccessToken : null;
136
+
137
+ io.section(r.mainRoleTitle);
138
+ const { held, mainClusterId } = await io.withProgress(r.loadingHeld, () => fetchHeldRoles({ deps, hubAccessToken }));
139
+
140
+ // Prefer the talent's own/recommended roles; fall back to the catalog for a
141
+ // fresh talent so a main role can always be set (picking one adds it first).
142
+ let roles = held;
143
+ let fromCatalog = false;
144
+ if (held.length === 0) {
145
+ const av = await deps.requestAvailableRoles({ hubAccessToken }, { endpoint: deps.getAvailableRolesEndpoint() });
146
+ roles = av.ok ? dedupeByCluster(av.roles) : [];
147
+ fromCatalog = true;
148
+ }
149
+ if (roles.length === 0) {
150
+ io.notify(r.mainRolePending);
151
+ return { ok: true, chosen: null };
152
+ }
153
+ const choice = await chooseRole({
154
+ io, roles, header: r.mainRolePrompt, hint: r.selectHint, initialClusterId: mainClusterId,
155
+ });
156
+ if (!choice) {
157
+ io.notify(r.mainRoleSkipped);
158
+ return { ok: true, chosen: null };
159
+ }
160
+ if (choice.clusterId === mainClusterId) {
161
+ io.notify(r.mainRoleUnchanged(roleLabel(choice)));
162
+ return { ok: true, chosen: choice.clusterId };
163
+ }
164
+ // main-role requires the cluster to be a held assignment: add it first if it came from the catalog.
165
+ if (fromCatalog) {
166
+ await deps.requestAddGrowthRole({ clusterId: choice.clusterId, hubAccessToken }, { endpoint: deps.getAssignedClustersEndpoint() });
167
+ }
168
+ return applySetMain({ io, deps, hubAccessToken, choice, catalog });
169
+ }
170
+
171
+ async function applySetMain({ io, deps, hubAccessToken, choice, catalog }) {
172
+ const r = catalog.roles;
173
+ const endpoint = deps.getSetMainRoleEndpoint();
174
+ const set = await io.withProgress(r.savingMain, () =>
175
+ deps.requestSetMainRole({ clusterId: choice.clusterId, hubAccessToken }, { endpoint }));
176
+ if (!set.ok) {
177
+ io.error(r.setMainFailed(set.reason));
178
+ return { ok: false, reason: set.reason };
179
+ }
180
+ io.success(r.mainSetOk(roleLabel(choice)));
181
+ return { ok: true, chosen: choice.clusterId, changed: choice.clusterId };
182
+ }
183
+
184
+ module.exports = {
185
+ makeRolesDeps,
186
+ roleLabel,
187
+ dedupeByCluster,
188
+ fetchHeldRoles,
189
+ runAddRole,
190
+ runChangeMainRole,
191
+ runOnboardingMainRoleStep,
192
+ };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ // Making text that came off the wire SAFE TO DISPLAY.
4
+
5
+ // eslint-disable-next-line no-control-regex
6
+ const CONTROL_CHARS_RE = /[\x00-\x08\x0B-\x1F\x7F]/g;
7
+
8
+ const INVISIBLE_CHARS_RE =
9
+ /[\u00AD\u061C\u115F\u1160\u17B4\u17B5\u180E\u200B-\u200F\u2028\u2029\u202A-\u202E\u2060-\u2064\u2066-\u2069\u3164\uFEFF\uFFA0]|[\u{E0000}-\u{E007F}]/gu;
10
+
11
+ function stripControlChars(s) {
12
+ if (typeof s !== 'string') return '';
13
+ return s.replace(CONTROL_CHARS_RE, '');
14
+ }
15
+
16
+ // Replaces every bidi/invisible character with a visible `[U+XXXX]` token.
17
+ function markInvisibleChars(s) {
18
+ if (typeof s !== 'string') return '';
19
+ return s.replace(INVISIBLE_CHARS_RE, (ch) => {
20
+ const hex = ch.codePointAt(0).toString(16).toUpperCase();
21
+ return `[U+${hex.length < 4 ? hex.padStart(4, '0') : hex}]`;
22
+ });
23
+ }
24
+
25
+ // What every renderer should call on text that came off the wire before painting it.
26
+ function sanitizeRenderText(s) {
27
+ return markInvisibleChars(stripControlChars(s));
28
+ }
29
+
30
+ // `jsonEscapeInvisibleChars` WAS HERE, and what is missing is the SURFACE, not the helper.
31
+
32
+ module.exports = {
33
+ stripControlChars,
34
+ markInvisibleChars,
35
+ sanitizeRenderText,
36
+ };
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ // Saved-positions client. Both
4
+ // operations are idempotent server-side, so no read-before-write is needed.
5
+
6
+ const { postJsonWithTimeout } = require('./backend-request');
7
+ const { reasonForError } = require('./onboarding-client');
8
+
9
+ const DEFAULT_TIMEOUT_MS = 20000;
10
+
11
+ function bearerHeaders(accessToken) {
12
+ const headers = {};
13
+ if (typeof accessToken === 'string' && accessToken) headers.Authorization = `Bearer ${accessToken}`;
14
+ return headers;
15
+ }
16
+
17
+ async function requestSavePosition({ hubAccessToken } = {}, { positionId, endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
18
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
19
+ if (!positionId) return { ok: false, reason: 'no-id' };
20
+ if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
21
+ let res;
22
+ try {
23
+ res = await postJsonWithTimeout(endpoint, { positionId }, timeoutMs, 'POST', null, bearerHeaders(hubAccessToken));
24
+ } catch (e) {
25
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
26
+ }
27
+ if (res.status === 404) return { ok: false, reason: 'not-found' };
28
+ if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
29
+ return { ok: true };
30
+ }
31
+
32
+ async function requestUnsavePosition({ hubAccessToken } = {}, { positionId, endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
33
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
34
+ if (!positionId) return { ok: false, reason: 'no-id' };
35
+ if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
36
+ let res;
37
+ try {
38
+ res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'DELETE', null, bearerHeaders(hubAccessToken));
39
+ } catch (e) {
40
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
41
+ }
42
+ if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
43
+ return { ok: true };
44
+ }
45
+
46
+ async function savePosition(deps = {}, { hubAccessToken, positionId, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
47
+ const {
48
+ getSavePositionEndpoint = require('./config').getSavePositionEndpoint,
49
+ requestSavePosition: request = requestSavePosition,
50
+ } = deps;
51
+ const endpoint = getSavePositionEndpoint();
52
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
53
+ return request({ hubAccessToken }, { positionId, endpoint, timeoutMs });
54
+ }
55
+
56
+ async function unsavePosition(deps = {}, { hubAccessToken, positionId, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
57
+ const {
58
+ getUnsavePositionEndpoint = require('./config').getUnsavePositionEndpoint,
59
+ requestUnsavePosition: request = requestUnsavePosition,
60
+ } = deps;
61
+ const endpoint = getUnsavePositionEndpoint(positionId);
62
+ if (!endpoint) return { ok: false, reason: positionId ? 'no-endpoint' : 'no-id' };
63
+ return request({ hubAccessToken }, { positionId, endpoint, timeoutMs });
64
+ }
65
+
66
+ module.exports = {
67
+ requestSavePosition,
68
+ requestUnsavePosition,
69
+ savePosition,
70
+ unsavePosition,
71
+ };
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ // `save` / `unsave` flows: bookmark a Position. Reversible + idempotent, so no
4
+ // confirmation (owner: low risk). Talent Bearer.
5
+
6
+ function makeSavedPositionsDeps(overrides = {}) {
7
+ const client = require('./saved-positions-client');
8
+ return {
9
+ savePosition: (opts) => client.savePosition({}, opts),
10
+ unsavePosition: (opts) => client.unsavePosition({}, opts),
11
+ resolveRef: (raw) => require('./find-projects-cache').resolvePositionRef(raw),
12
+ ...overrides,
13
+ };
14
+ }
15
+
16
+ async function runToggleSave({ io, rawOut, session, catalog, mode, opts = {}, deps = makeSavedPositionsDeps() }) {
17
+ const fp = catalog.savedPositions;
18
+ const hubAccessToken = session ? session.hubAccessToken : null;
19
+ // A pure integer resolves against the last `find-projects` listing; a UUID/Mongo id passes through.
20
+ const resolved = deps.resolveRef(opts.id);
21
+ if (!resolved.ok) {
22
+ const idRequired = mode === 'save' ? fp.saveIdRequired : fp.unsaveIdRequired;
23
+ io.error(resolved.reason === 'no-cache' ? fp.refNoCache
24
+ : resolved.reason === 'out-of-range' ? fp.refOutOfRange(resolved.count)
25
+ : idRequired);
26
+ return { ok: false, reason: resolved.reason };
27
+ }
28
+ const positionId = resolved.id;
29
+
30
+ const label = mode === 'save' ? fp.saving : fp.unsaving;
31
+ const res = await io.withProgress(label, () => (mode === 'save'
32
+ ? deps.savePosition({ hubAccessToken, positionId })
33
+ : deps.unsavePosition({ hubAccessToken, positionId })));
34
+ if (!res.ok) {
35
+ io.error(res.reason === 'not-found' ? fp.notFound : fp.failed(res.reason));
36
+ return { ok: false, reason: res.reason };
37
+ }
38
+
39
+ if (opts.json) {
40
+ (rawOut || ((s) => process.stdout.write(s)))(`${JSON.stringify({ ok: true, mode, positionId }, null, 2)}\n`);
41
+ return { ok: true };
42
+ }
43
+ io.success(mode === 'save' ? fp.saved : fp.unsaved);
44
+ return { ok: true };
45
+ }
46
+
47
+ module.exports = {
48
+ makeSavedPositionsDeps,
49
+ runToggleSave,
50
+ };
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ // Directory names never descended into when walking a project tree (skill-code-certification, issue 008).
6
+ const EXCLUDED_DIRS = new Set([
7
+ 'node_modules', 'dist', 'build', '.git', 'vendor', 'coverage', '.next', 'out',
8
+ // Python
9
+ '__pycache__', '.venv', 'venv', '.tox', '.mypy_cache', '.pytest_cache', '.eggs',
10
+ // Ruby
11
+ '.bundle',
12
+ // Java / JVM (Gradle/Maven build output)
13
+ 'target', '.gradle',
14
+ ]);
15
+
16
+ const MAX_WALK_DEPTH = 8;
17
+
18
+ const SYSTEM_ROOT_PATHS = new Set([
19
+ '/System', '/Library', '/Volumes', '/private', '/dev', '/proc',
20
+ '/opt', '/Applications', '/usr', '/bin', '/sbin', '/etc', '/var',
21
+ '/cores', '/Network',
22
+ ]);
23
+
24
+ function normalizeWalkRoot(root) {
25
+ const resolved = path.resolve(root);
26
+ return resolved.length > 1 ? resolved.replace(/\/+$/, '') : resolved;
27
+ }
28
+
29
+ function isRefusedWalkRoot(root) {
30
+ if (typeof root !== 'string' || !root) return true;
31
+ let norm;
32
+ try {
33
+ norm = normalizeWalkRoot(root);
34
+ } catch {
35
+ return true;
36
+ }
37
+ return norm === '/' || SYSTEM_ROOT_PATHS.has(norm);
38
+ }
39
+
40
+ function isExcludedWalkPath(absDir) {
41
+ return SYSTEM_ROOT_PATHS.has(absDir);
42
+ }
43
+
44
+ module.exports = {
45
+ EXCLUDED_DIRS,
46
+ MAX_WALK_DEPTH,
47
+ SYSTEM_ROOT_PATHS,
48
+ isRefusedWalkRoot,
49
+ isExcludedWalkPath,
50
+ };