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,75 @@
1
+ 'use strict';
2
+
3
+ // `certifications` flow: render the talent's dimension certifications by state
4
+ // (certified / uncertified / expired) + band + main role. Read-only.
5
+
6
+ const { styleLabelPrefix } = require('./ansi');
7
+
8
+ function makeCertificationsDeps(overrides = {}) {
9
+ const client = require('./certifications-client');
10
+ return {
11
+ fetchMeCertifications: (opts) => client.fetchMeCertifications({}, opts),
12
+ ...overrides,
13
+ };
14
+ }
15
+
16
+ function groupByState(dimensions) {
17
+ const g = { certified: [], uncertified: [], expired: [] };
18
+ for (const d of dimensions || []) {
19
+ if (d.state === 'CERTIFIED') g.certified.push(d);
20
+ else if (d.state === 'EXPIRED') g.expired.push(d);
21
+ else g.uncertified.push(d);
22
+ }
23
+ return g;
24
+ }
25
+
26
+ function renderGroup(io, fp, heading, items, withBand) {
27
+ if (!items.length) return;
28
+ io.section(heading);
29
+ items.forEach((d) => {
30
+ const label = d.name || d.slug || fp.untitled;
31
+ io.notify(withBand && d.band ? `${label} — ${d.band}` : label);
32
+ });
33
+ }
34
+
35
+ async function runCertifications({ io, rawOut, session, catalog, opts = {}, deps = makeCertificationsDeps() }) {
36
+ const fp = catalog.certifications;
37
+ const hubAccessToken = session ? session.hubAccessToken : null;
38
+
39
+ const doFetch = () => deps.fetchMeCertifications({ hubAccessToken });
40
+ const res = opts.json ? await doFetch() : await io.withProgress(fp.loading, doFetch);
41
+ if (!res.ok) {
42
+ io.error(fp.fetchFailed(res.reason));
43
+ return { ok: false, reason: res.reason };
44
+ }
45
+
46
+ if (opts.json) {
47
+ (rawOut || ((s) => process.stdout.write(s)))(`${JSON.stringify({ ok: true, mainRole: res.mainRole, dimensions: res.dimensions, growingInto: res.growingInto }, null, 2)}\n`);
48
+ return { ok: true, mainRole: res.mainRole, dimensions: res.dimensions };
49
+ }
50
+
51
+ io.section(fp.title);
52
+ if (res.mainRole && res.mainRole.name) {
53
+ const ratio = res.mainRole.total != null ? ` (${res.mainRole.certified ?? 0}/${res.mainRole.total})` : '';
54
+ io.notify(styleLabelPrefix(fp.mainRole(res.mainRole.name) + ratio));
55
+ } else {
56
+ io.notify(fp.noMainRole);
57
+ }
58
+
59
+ const g = groupByState(res.dimensions);
60
+ if (g.certified.length === 0 && g.uncertified.length === 0 && g.expired.length === 0) {
61
+ io.notify(fp.noDimensions);
62
+ return { ok: true, mainRole: res.mainRole, dimensions: res.dimensions };
63
+ }
64
+ renderGroup(io, fp, fp.headingCertified, g.certified, true);
65
+ renderGroup(io, fp, fp.headingExpired, g.expired, true);
66
+ renderGroup(io, fp, fp.headingUncertified, g.uncertified, false);
67
+ io.success(fp.done);
68
+ return { ok: true, mainRole: res.mainRole, dimensions: res.dimensions };
69
+ }
70
+
71
+ module.exports = {
72
+ makeCertificationsDeps,
73
+ groupByState,
74
+ runCertifications,
75
+ };
@@ -0,0 +1,217 @@
1
+ 'use strict';
2
+
3
+ // Dimension-certification discovery + lifecycle client (hub + certs endpoints).
4
+
5
+ const { postJsonWithTimeout } = require('./backend-request');
6
+ const { reasonForError } = require('./onboarding-client');
7
+ const { toUpperLang } = require('./lang-codes');
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 payloadOf(raw) {
20
+ const json = parseJson(raw);
21
+ if (!json || typeof json !== 'object') return undefined;
22
+ if (json.status === 'OK') return json.data;
23
+ return json;
24
+ }
25
+
26
+ function bearerHeaders(accessToken) {
27
+ const headers = {};
28
+ if (typeof accessToken === 'string' && accessToken) headers.Authorization = `Bearer ${accessToken}`;
29
+ return headers;
30
+ }
31
+
32
+ function pick(obj, keys) {
33
+ for (const k of keys) {
34
+ const v = obj && obj[k];
35
+ if (v !== undefined && v !== null && v !== '') return v;
36
+ }
37
+ return null;
38
+ }
39
+
40
+ // FLAG: hub gives `clusterId`, certs keys templates by `clusterRef`; assumed equal here.
41
+ function composeDimensionKey(clusterRef, slug) {
42
+ if (!clusterRef || !slug) return null;
43
+ return `${clusterRef}/${slug}`;
44
+ }
45
+
46
+ async function requestMyCertifications({ 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
+
57
+ const data = payloadOf(res.raw);
58
+ if (data === undefined || !data || typeof data !== 'object') return { ok: false, reason: 'bad-response' };
59
+ const rawDims = Array.isArray(data) ? data
60
+ : (Array.isArray(data.dimensions) ? data.dimensions
61
+ : (Array.isArray(data.items) ? data.items : []));
62
+ const dimensions = rawDims
63
+ .map((d) => ({
64
+ slug: pick(d, ['slug', 'dimensionSlug', 'key']),
65
+ clusterId: pick(d, ['clusterRef', 'clusterId', 'clusterKey', 'cluster']),
66
+ state: pick(d, ['state', 'status']),
67
+ band: pick(d, ['band', 'level']) || null,
68
+ }))
69
+ .filter((d) => d.slug && d.clusterId);
70
+ return { ok: true, mainRole: pick(data, ['mainRole', 'role']) || null, dimensions };
71
+ }
72
+
73
+ // @Public: no auth header. Returns a Map dimensionKey -> deliveryFormat.
74
+ async function requestTemplatesByDimension({ dimensionKeys } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
75
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
76
+ const keys = (Array.isArray(dimensionKeys) ? dimensionKeys : []).filter(Boolean);
77
+ if (keys.length === 0) return { ok: true, formats: new Map() };
78
+
79
+ const url = `${endpoint}${endpoint.includes('?') ? '&' : '?'}dimensionKeys=${keys.map(encodeURIComponent).join(',')}`;
80
+ let res;
81
+ try {
82
+ res = await postJsonWithTimeout(url, null, timeoutMs, 'GET', null, {});
83
+ } catch (e) {
84
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
85
+ }
86
+ if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
87
+
88
+ const data = payloadOf(res.raw);
89
+ if (data === undefined || data === null) return { ok: false, reason: 'bad-response' };
90
+
91
+ // The real certs route returns rows under `data.items`; templates/bare-array are tolerant fallbacks.
92
+ const formats = new Map();
93
+ const rows = Array.isArray(data)
94
+ ? data
95
+ : (Array.isArray(data.items) ? data.items
96
+ : (Array.isArray(data.templates) ? data.templates : null));
97
+ if (rows) {
98
+ for (const t of rows) {
99
+ const key = pick(t, ['dimensionKey', 'key', 'dimension']);
100
+ const fmt = pick(t, ['deliveryFormat', 'delivery_format', 'format']);
101
+ if (key && fmt) formats.set(String(key), String(fmt));
102
+ }
103
+ } else if (typeof data === 'object') {
104
+ for (const [key, value] of Object.entries(data)) {
105
+ const fmt = value && typeof value === 'object' ? pick(value, ['deliveryFormat', 'delivery_format', 'format']) : value;
106
+ if (fmt) formats.set(String(key), String(fmt));
107
+ }
108
+ }
109
+ return { ok: true, formats };
110
+ }
111
+
112
+ // What the terminal can run: the templates marked for it, and the spoken ones, which are
113
+ // conversation only. Whiteboard and screen share need the web.
114
+ const TEXT_DELIVERY_FORMATS = new Set(['terminal', 'spoken_case']);
115
+
116
+ function composeOfferableDimensions({ dimensions, formats } = {}) {
117
+ const fmt = formats instanceof Map ? formats : new Map(Object.entries(formats || {}));
118
+ const candidates = (Array.isArray(dimensions) ? dimensions : [])
119
+ .filter((d) => d && d.state !== 'CERTIFIED')
120
+ .map((d) => ({ ...d, dimensionKey: composeDimensionKey(d.clusterId, d.slug) }))
121
+ .filter((d) => d.dimensionKey);
122
+ const offerable = candidates.filter((d) => TEXT_DELIVERY_FORMATS.has(fmt.get(d.dimensionKey)));
123
+ const unmatched = candidates
124
+ .filter((d) => !fmt.has(d.dimensionKey))
125
+ .map((d) => d.dimensionKey);
126
+ return { offerable, unmatched };
127
+ }
128
+
129
+ async function discoverOfferableDimensions(deps = {}, { accessToken, hubAccessToken, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
130
+ const {
131
+ getMyCertificationsEndpoint = require('./config').getMyCertificationsEndpoint,
132
+ getTemplatesByDimensionEndpoint = require('./config').getTemplatesByDimensionEndpoint,
133
+ requestMyCertifications: myCerts = requestMyCertifications,
134
+ requestTemplatesByDimension: templates = requestTemplatesByDimension,
135
+ } = deps;
136
+
137
+ const mineEndpoint = getMyCertificationsEndpoint();
138
+ if (!mineEndpoint) return { ok: false, reason: 'no-endpoint' };
139
+ const mine = await myCerts({ hubAccessToken }, { endpoint: mineEndpoint, timeoutMs });
140
+ if (!mine.ok) return { ok: false, reason: mine.reason };
141
+
142
+ const candidateDims = mine.dimensions.filter((d) => d.state !== 'CERTIFIED');
143
+ if (candidateDims.length === 0) return { ok: true, mainRole: mine.mainRole, offerable: [], unmatched: [] };
144
+
145
+ const templatesEndpoint = getTemplatesByDimensionEndpoint();
146
+ if (!templatesEndpoint) return { ok: false, reason: 'no-endpoint' };
147
+ const keys = candidateDims
148
+ .map((d) => composeDimensionKey(d.clusterId, d.slug))
149
+ .filter(Boolean);
150
+ const tmpl = await templates({ dimensionKeys: keys }, { endpoint: templatesEndpoint, timeoutMs });
151
+ if (!tmpl.ok) return { ok: false, reason: tmpl.reason };
152
+
153
+ const { offerable, unmatched } = composeOfferableDimensions({ dimensions: candidateDims, formats: tmpl.formats });
154
+ return { ok: true, mainRole: mine.mainRole, offerable, unmatched };
155
+ }
156
+
157
+ async function requestCreateCertificationInterview(
158
+ { dimensionKey, languageCode, accessToken } = {},
159
+ { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
160
+ ) {
161
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
162
+ if (!dimensionKey) return { ok: false, reason: 'no-dimension' };
163
+ const body = { dimensionKey };
164
+ if (languageCode) body.languageCode = toUpperLang(languageCode);
165
+ let res;
166
+ try {
167
+ res = await postJsonWithTimeout(endpoint, body, timeoutMs, 'POST', null, bearerHeaders(accessToken));
168
+ } catch (e) {
169
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
170
+ }
171
+ if (res.status < 200 || res.status >= 300) {
172
+ const reason = reasonForError(res.status, res.raw);
173
+ if (reason !== 'dimension-on-cooldown') return { ok: false, reason };
174
+ // certs puts the date in the message only: "... available on 2026-10-28T14:50:38.265Z".
175
+ const available = String(res.raw || '').match(/available on (\d{4}-\d{2}-\d{2}T[\d:.]+Z)/);
176
+ return { ok: false, reason, availableOn: available ? available[1] : null };
177
+ }
178
+ const data = payloadOf(res.raw);
179
+ const interviewId = pick(data || {}, ['interviewId', 'id']);
180
+ if (!interviewId) return { ok: false, reason: 'bad-response' };
181
+ return { ok: true, interviewId, language: pick(data || {}, ['language', 'languageCode']) || null };
182
+ }
183
+
184
+ // Poll the verdict; a 2xx/404/425/202 with no band yet resolves `ready:false` (retriable).
185
+ async function requestCertificationReport(
186
+ { interviewId, accessToken } = {},
187
+ { base, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
188
+ ) {
189
+ if (!base) return { ok: false, reason: 'no-endpoint' };
190
+ if (!interviewId) return { ok: false, reason: 'no-interview' };
191
+ const url = `${base.replace(/\/+$/, '')}/${encodeURIComponent(interviewId)}/report`;
192
+ let res;
193
+ try {
194
+ res = await postJsonWithTimeout(url, null, timeoutMs, 'GET', null, bearerHeaders(accessToken));
195
+ } catch (e) {
196
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
197
+ }
198
+ if (res.status === 404 || res.status === 425 || res.status === 202) return { ok: true, ready: false, report: null };
199
+ if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
200
+ const data = payloadOf(res.raw);
201
+ if (data === undefined || data === null || typeof data !== 'object') return { ok: true, ready: false, report: null };
202
+ const band = pick(data, ['band', 'level', 'combinedLevel']);
203
+ // A report with too little evidence for a level has band null; it is still finished.
204
+ if (!band && !pick(data, ['evaluationId'])) return { ok: true, ready: false, report: null };
205
+ return { ok: true, ready: true, report: data };
206
+ }
207
+
208
+ module.exports = {
209
+ requestMyCertifications,
210
+ requestTemplatesByDimension,
211
+ requestCreateCertificationInterview,
212
+ requestCertificationReport,
213
+ composeDimensionKey,
214
+ composeOfferableDimensions,
215
+ discoverOfferableDimensions,
216
+ TEXT_DELIVERY_FORMATS,
217
+ };
@@ -0,0 +1,193 @@
1
+ 'use strict';
2
+
3
+ // `certify` dimension flow: discover -> pick -> create -> LiveKit interview -> poll verdict.
4
+
5
+ const { runInteractiveMultiSelect } = require('./interactive-select');
6
+
7
+ function makeCertifyDimensionDeps(overrides = {}) {
8
+ const config = require('./config');
9
+ const client = require('./certify-dimension-client');
10
+ const onboardingFlow = require('./onboarding-flow');
11
+ return {
12
+ getCertificationInterviewsEndpoint: config.getCertificationInterviewsEndpoint,
13
+ // `{certsBase}/interviews` — shared base for LiveKit start/complete + the report poll.
14
+ getInterviewsBase: config.getOnboardingInterviewsEndpoint,
15
+ discoverOfferableDimensions: (opts) => client.discoverOfferableDimensions({}, opts),
16
+ requestCreateCertificationInterview: client.requestCreateCertificationInterview,
17
+ requestCertificationReport: client.requestCertificationReport,
18
+ conductLivekitInterview: onboardingFlow.conductLivekitInterview,
19
+ onboardingDeps: onboardingFlow.makeDeps(),
20
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
21
+ ...overrides,
22
+ };
23
+ }
24
+
25
+ // Numbered fallback when `ask` can't release stdin for raw mode.
26
+ async function askNumberedChoice(ask, promptText, count) {
27
+ const raw = (await ask(promptText)).trim();
28
+ if (!raw) return null;
29
+ const n = Number.parseInt(raw, 10);
30
+ if (!Number.isInteger(n) || n < 1 || n > count) return null;
31
+ return n - 1;
32
+ }
33
+
34
+ async function chooseDimension(ask, stdinIsTTY, items, cd, out, { input, output } = {}) {
35
+ const labelFor = (d) => `${d.slug}${d.state === 'EXPIRED' ? ` (${cd.stateExpired})` : ''}`;
36
+ if (stdinIsTTY && typeof ask.suspend === 'function') {
37
+ ask.suspend();
38
+ const picked = await runInteractiveMultiSelect({
39
+ items,
40
+ labelFor,
41
+ header: cd.selectHeading,
42
+ hint: cd.selectHint,
43
+ single: true,
44
+ ...(input ? { input } : {}),
45
+ ...(output ? { output } : {}),
46
+ });
47
+ ask.resume();
48
+ return picked && picked.length ? picked[0] : null;
49
+ }
50
+ out(`\n ${cd.selectHeading}\n`);
51
+ items.forEach((d, i) => out(` ${i + 1}) ${labelFor(d)}\n`));
52
+ const idx = await askNumberedChoice(ask, ` ${cd.selectPrompt(items.length)}`, items.length);
53
+ return idx === null ? null : items[idx];
54
+ }
55
+
56
+ function resolveDimensionArg(arg, items) {
57
+ if (arg == null) return null;
58
+ const raw = String(arg).trim();
59
+ const n = Number.parseInt(raw, 10);
60
+ if (Number.isInteger(n) && String(n) === raw && n >= 1 && n <= items.length) return items[n - 1];
61
+ return items.find((d) => d.dimensionKey === raw || d.slug === raw) || undefined;
62
+ }
63
+
64
+ // The evaluation lands one to two minutes after the interview ends, four when the queue is busy.
65
+ async function pollReport(deps, { interviewId, accessToken }, { base, attempts = 60, intervalMs = 5000 } = {}) {
66
+ for (let i = 0; i < attempts; i++) {
67
+ const res = await deps.requestCertificationReport({ interviewId, accessToken }, { base });
68
+ if (!res.ok) return { ok: false, reason: res.reason };
69
+ if (res.ready) return { ok: true, report: res.report };
70
+ if (i < attempts - 1) await deps.sleep(intervalMs);
71
+ }
72
+ return { ok: false, reason: 'timeout' };
73
+ }
74
+
75
+ function formatDay(iso, lang) {
76
+ const d = iso ? new Date(iso) : null;
77
+ if (!d || Number.isNaN(d.getTime())) return null;
78
+ return d.toLocaleDateString(lang === 'en' ? 'en-GB' : 'es-ES', { day: 'numeric', month: 'long', year: 'numeric' });
79
+ }
80
+
81
+ function renderVerdict(report, cd) {
82
+ const lines = [];
83
+ const band = report.band || report.level || report.combinedLevel;
84
+ const dimension = report.dimensionName || report.dimension || report.dimensionKey;
85
+ if (dimension) lines.push(cd.verdictDimension(dimension));
86
+ if (band) lines.push(cd.verdictBand(band));
87
+ if (typeof report.certified === 'boolean') lines.push(report.certified ? cd.verdictCertified : cd.verdictNotCertified);
88
+ const summary = report.summary || report.rationale || report.feedback;
89
+ if (typeof summary === 'string' && summary.trim()) lines.push(summary.trim());
90
+ const areas = Array.isArray(report.areas) ? report.areas : null;
91
+ if (areas && areas.length) {
92
+ for (const a of areas) {
93
+ const name = a && (a.name || a.area || a.tag);
94
+ const note = a && (a.note || a.comment);
95
+ if (name) lines.push(`- ${name}${note ? `: ${note}` : ''}`);
96
+ }
97
+ }
98
+ return lines.join('\n ');
99
+ }
100
+
101
+ async function runCertifyDimension({ io, ask, stdinIsTTY, session, lang, catalog, opts = {}, deps = makeCertifyDimensionDeps() }) {
102
+ const cd = catalog.certifyDimension;
103
+ const accessToken = session ? session.accessToken : null;
104
+ const hubAccessToken = session ? session.hubAccessToken : null;
105
+
106
+ io.section(cd.title);
107
+
108
+ const discovered = await io.withProgress(cd.discovering, () =>
109
+ deps.discoverOfferableDimensions({ accessToken, hubAccessToken }));
110
+ if (!discovered.ok) {
111
+ io.error(cd.discoverFailed(discovered.reason));
112
+ return { ok: false, reason: discovered.reason, step: 'discover' };
113
+ }
114
+
115
+ if (Array.isArray(discovered.unmatched) && discovered.unmatched.length > 0) {
116
+ io.warn(cd.unmatchedNote(discovered.unmatched.join(', ')));
117
+ }
118
+
119
+ const offerable = Array.isArray(discovered.offerable) ? discovered.offerable : [];
120
+ if (offerable.length === 0) {
121
+ io.notify(cd.noneOfferable);
122
+ return { ok: true, none: true };
123
+ }
124
+ const mainRole = discovered.mainRole;
125
+ // The hub sends the main role as { clusterId, name, ... }.
126
+ const mainRoleName = mainRole && typeof mainRole === 'object' ? mainRole.name || mainRole.clusterId : mainRole;
127
+ if (mainRoleName) io.notify(cd.mainRole(mainRoleName));
128
+
129
+ let chosen;
130
+ if (opts.dimension != null) {
131
+ const resolved = resolveDimensionArg(opts.dimension, offerable);
132
+ if (!resolved) {
133
+ io.error(cd.dimensionInvalid);
134
+ return { ok: false, reason: 'bad-dimension', step: 'select' };
135
+ }
136
+ chosen = resolved;
137
+ io.notify(cd.dimensionUsing(chosen.slug));
138
+ } else if (!stdinIsTTY) {
139
+ io.error(cd.selectNonInteractive);
140
+ return { ok: false, reason: 'non-interactive', step: 'select' };
141
+ } else {
142
+ chosen = await chooseDimension(ask, stdinIsTTY, offerable, cd, (s) => io.notify(s.replace(/\n$/, '')), opts);
143
+ if (!chosen) {
144
+ io.notify(cd.selectNoneChosen);
145
+ return { ok: true, cancelled: true };
146
+ }
147
+ }
148
+
149
+ const createEndpoint = deps.getCertificationInterviewsEndpoint();
150
+ const created = await io.withProgress(cd.creating, () =>
151
+ deps.requestCreateCertificationInterview(
152
+ { dimensionKey: chosen.dimensionKey, languageCode: lang, accessToken },
153
+ { endpoint: createEndpoint },
154
+ ));
155
+ if (!created.ok) {
156
+ io.error(created.reason === 'dimension-on-cooldown'
157
+ ? cd.onCooldown(formatDay(created.availableOn, lang))
158
+ : cd.createFailed(created.reason));
159
+ return { ok: false, reason: created.reason, step: 'create' };
160
+ }
161
+
162
+ const interview = await deps.conductLivekitInterview(io, deps.onboardingDeps, {
163
+ interviewId: created.interviewId,
164
+ language: lang,
165
+ plain: true,
166
+ });
167
+ if (!interview.ok) {
168
+ io.warn(interview.reason === 'needs-web' ? cd.needsWeb : cd.interviewUnavailable(interview.reason));
169
+ return { ok: false, reason: interview.reason, step: 'interview' };
170
+ }
171
+
172
+ const reportBase = deps.getInterviewsBase();
173
+ const polled = await io.withProgress(cd.reportPolling, () =>
174
+ pollReport(deps, { interviewId: created.interviewId, accessToken }, { base: reportBase }));
175
+ if (!polled.ok) {
176
+ io.warn(polled.reason === 'timeout' ? cd.reportTimeout : cd.reportFailed(polled.reason));
177
+ return { ok: true, completed: 'interview', verdict: null };
178
+ }
179
+
180
+ io.section(cd.verdictTitle);
181
+ io.notify(renderVerdict(polled.report, cd));
182
+ io.success(cd.done);
183
+ return { ok: true, completed: 'interview', verdict: polled.report };
184
+ }
185
+
186
+ module.exports = {
187
+ makeCertifyDimensionDeps,
188
+ runCertifyDimension,
189
+ chooseDimension,
190
+ resolveDimensionArg,
191
+ pollReport,
192
+ renderVerdict,
193
+ };
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ // Legal disclaimer + EXPLICIT acceptance gate (ADR-001), shown before egress. Skill-code certify was retired (certify is now a dimension LiveKit interview); consumers today are the add-skill/add-agent/add-project consent gates.
4
+
5
+ const { renderLegalNotice } = require('./legal-notice');
6
+ const { styleQuestion } = require('./ansi');
7
+
8
+ const MAX_ATTEMPTS = 5;
9
+ const YES_RE = /^(y|yes|s|si|sí)$/i;
10
+ const NO_RE = /^(n|no)$/i;
11
+
12
+ function isAffirmative(raw) {
13
+ return YES_RE.test(String(raw).trim());
14
+ }
15
+ function isNegative(raw) {
16
+ return NO_RE.test(String(raw).trim());
17
+ }
18
+
19
+ async function confirmDisclaimerAcceptance({ ask, catalog, preAccepted = false, stdinIsTTY = true, text = null, drainBeforeAsk = false }) {
20
+ const d = catalog.certify;
21
+ const shown = text || d.disclaimer;
22
+
23
+ // The disclaimer TEXT is always shown, even when pre-accepting via flag — the talent (or the script author who chose the flag) still sees what they're accepting.
24
+ process.stdout.write(`\n ${renderLegalNotice(shown, catalog)}\n`);
25
+
26
+ if (preAccepted) {
27
+ process.stdout.write(` ${d.disclaimerAcceptedFlag}\n\n`);
28
+ return { accepted: true, reason: 'flag' };
29
+ }
30
+
31
+ if (!stdinIsTTY) {
32
+ process.stdout.write(` ${d.disclaimerNonInteractive}\n\n`);
33
+ return { accepted: false, reason: 'non-interactive' };
34
+ }
35
+
36
+ if (drainBeforeAsk && typeof ask.drain === 'function') ask.drain();
37
+
38
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
39
+ const raw = String(await ask(styleQuestion(d.disclaimerQuestion))).trim();
40
+ if (isAffirmative(raw)) return { accepted: true, reason: 'interactive' };
41
+ if (isNegative(raw)) {
42
+ process.stdout.write(` ${d.disclaimerDeclined}\n\n`);
43
+ return { accepted: false, reason: 'declined' };
44
+ }
45
+ process.stdout.write(` ${d.disclaimerInvalidAnswer}\n`);
46
+ }
47
+ process.stdout.write(` ${d.disclaimerNoAnswer}\n\n`);
48
+ return { accepted: false, reason: 'no-answer' };
49
+ }
50
+
51
+ module.exports = { confirmDisclaimerAcceptance, isAffirmative, isNegative };
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ const { getCatalog } = require('./i18n');
4
+
5
+ // Client-side "remediation prompt" for the certify report (skill-code- certification, issue 011).
6
+ function buildRemediationPrompt(item, lang) {
7
+ const r = getCatalog(lang).certify.report;
8
+ const improvements =
9
+ item && item.result && Array.isArray(item.result.improvements)
10
+ ? item.result.improvements.filter((x) => typeof x === 'string' && x)
11
+ : [];
12
+ if (improvements.length === 0) return null;
13
+
14
+ const lines = [];
15
+ lines.push(r.remediationIntro(item.skillName, item.technology));
16
+ lines.push('');
17
+ improvements.forEach((imp, i) => lines.push(`${i + 1}. ${imp}`));
18
+ lines.push('');
19
+ lines.push(r.remediationClosing);
20
+ return lines.join('\n');
21
+ }
22
+
23
+ module.exports = { buildRemediationPrompt };
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ const VALID_LANGS = new Set(['es', 'en']);
4
+ const VALID_SCOPES = new Set(['machine', 'repo']);
5
+
6
+ function parseScope(value) {
7
+ return VALID_SCOPES.has(value) ? value : null;
8
+ }
9
+
10
+ function parseRepoList(value) {
11
+ if (typeof value !== 'string') return null;
12
+ const items = value
13
+ .split(',')
14
+ .map((s) => s.trim())
15
+ .filter(Boolean);
16
+ return items.length > 0 ? items : null;
17
+ }
18
+
19
+ function parseArgs(argv) {
20
+ const opts = {
21
+ json: false,
22
+ save: true,
23
+ root: null,
24
+ help: false,
25
+ consentStatus: false,
26
+ consentRevoke: false,
27
+ consentReset: false,
28
+ consentEmail: null,
29
+ setEndpoint: null,
30
+ showEndpoint: false,
31
+ buildNextLevel: false,
32
+ force: false,
33
+ lang: null,
34
+ roadmap: false,
35
+ repos: null,
36
+ allRepos: false,
37
+ // Dynamic scope selector for the DEDICATED `ai-usage` command (a symptom fix: machine-wide runs were silently collapsing to the current repo's agents).
38
+ machine: false,
39
+ repo: false,
40
+ scope: null,
41
+ noAi: false,
42
+ // Issue 103: turns the section reveal off. Same name in both parsers, and the
43
+ // env var `NO_ANIMATION` does the same thing (the `NO_COLOR` pattern of 079).
44
+ noAnimation: false,
45
+ };
46
+ for (let i = 0; i < argv.length; i++) {
47
+ const a = argv[i];
48
+ if (a === '--no-animation') opts.noAnimation = true;
49
+ else if (a === '--json') opts.json = true;
50
+ else if (a === '--no-save') opts.save = false;
51
+ else if (a === '--root') opts.root = argv[++i];
52
+ else if (a === '--consent-status') opts.consentStatus = true;
53
+ else if (a === '--consent-revoke') opts.consentRevoke = true;
54
+ else if (a === '--consent-reset') opts.consentReset = true;
55
+ else if (a === '--consent-email') opts.consentEmail = argv[++i];
56
+ else if (a.startsWith('--consent-email=')) opts.consentEmail = a.slice('--consent-email='.length);
57
+ else if (a === '--set-endpoint') opts.setEndpoint = argv[++i];
58
+ else if (a.startsWith('--set-endpoint=')) opts.setEndpoint = a.slice('--set-endpoint='.length);
59
+ else if (a === '--show-endpoint') opts.showEndpoint = true;
60
+ else if (a === '--build-next-level') opts.buildNextLevel = true;
61
+ else if (a === '--force') opts.force = true;
62
+ // ADR-016: the tier roadmap / "next steps" block is removed from the
63
+ // default footprint output; `--roadmap` (alias `--next`) shows it again.
64
+ else if (a === '--roadmap' || a === '--next') opts.roadmap = true;
65
+ else if (a === '--lang') opts.lang = VALID_LANGS.has(argv[++i]) ? argv[i] : null;
66
+ else if (a.startsWith('--lang=')) {
67
+ const value = a.slice('--lang='.length);
68
+ opts.lang = VALID_LANGS.has(value) ? value : null;
69
+ }
70
+ else if (a === '--no-ai') opts.noAi = true;
71
+ else if (a === '--all-repos') opts.allRepos = true;
72
+ else if (a === '--machine') opts.machine = true;
73
+ else if (a === '--repo') opts.repo = true;
74
+ else if (a === '--scope') opts.scope = parseScope(argv[++i]);
75
+ else if (a.startsWith('--scope=')) opts.scope = parseScope(a.slice('--scope='.length));
76
+ else if (a === '--repos') opts.repos = parseRepoList(argv[++i]);
77
+ else if (a.startsWith('--repos=')) opts.repos = parseRepoList(a.slice('--repos='.length));
78
+ else if (a === '--help' || a === '-h') opts.help = true;
79
+ }
80
+ return opts;
81
+ }
82
+
83
+ module.exports = { parseArgs };