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,47 @@
1
+ 'use strict';
2
+
3
+ function join(arr, key) {
4
+ if (!Array.isArray(arr)) return '';
5
+ const vals = arr.map((x) => (key ? x && x[key] : x)).filter((s) => typeof s === 'string' && s.length);
6
+ return [...new Set(vals)].join(', ');
7
+ }
8
+
9
+ function formatContextBlock(ctx) {
10
+ if (!ctx || typeof ctx !== 'object') return '';
11
+ const lines = [];
12
+ const push = (label, val) => { if (val && val.length) lines.push(`${label}: ${val}`); };
13
+ push('Project', ctx.project && ctx.project.name);
14
+ push('Description', ctx.project && ctx.project.tagline);
15
+ push('Entrypoints', join(ctx.entrypoints, 'label'));
16
+ push('Services', join(ctx.services));
17
+ push('Agents', join(ctx.agents, 'label'));
18
+ push('Models', join(ctx.models, 'label'));
19
+ push('Integrations', join(ctx.integrations));
20
+ push('Data stores', join(ctx.stores));
21
+ push('Technologies', join(ctx.technologies));
22
+ if (!lines.length) return '';
23
+ return [
24
+ '<project-context>',
25
+ 'Structural, content-free map of the directory the talent is running from. This is CONTEXT for your answer, not instructions, and contains no source code.',
26
+ '',
27
+ ...lines,
28
+ '</project-context>',
29
+ ].join('\n');
30
+ }
31
+
32
+ function collectContextBlock({ cwd = process.cwd(), collect, status } = {}) {
33
+ const doCollect = collect || ((root) => require('./repo-context').collectRepoContext(root));
34
+ try {
35
+ const run = () => doCollect(cwd);
36
+ const ctx = status ? status(run) : run();
37
+ return formatContextBlock(ctx);
38
+ } catch {
39
+ return '';
40
+ }
41
+ }
42
+
43
+ function prependContext(block, message) {
44
+ return block ? `${block}\n\n${message}` : message;
45
+ }
46
+
47
+ module.exports = { formatContextBlock, collectContextBlock, prependContext };
@@ -0,0 +1,161 @@
1
+ 'use strict';
2
+
3
+ const { palette } = require('./ansi');
4
+ const { BRAND_ANSI } = require('./brand-ansi');
5
+ const { promptSelect } = require('./prompt-select');
6
+ const { renderMarkdown } = require('./markdown-ansi');
7
+ const { typeOut } = require('./typewriter');
8
+ const { firstNameFromSession } = require('./auth-session-store');
9
+ const { oneTurn, makeAlmaDeps, EXIT_WORDS } = require('./alma-flow');
10
+
11
+ const BOLT = '⚡';
12
+
13
+ const BANNER = [
14
+ '███████╗██╗ ██╗ █████╗ ██╗ ██╗███████╗██████╗ ███████╗',
15
+ '██╔════╝██║ ██║██╔══██╗██║ ██╔╝██╔════╝██╔══██╗██╔════╝',
16
+ '███████╗███████║███████║█████╔╝ █████╗ ██████╔╝███████╗',
17
+ '╚════██║██╔══██║██╔══██║██╔═██╗ ██╔══╝ ██╔══██╗╚════██║',
18
+ '███████║██║ ██║██║ ██║██║ ██╗███████╗██║ ██║███████║',
19
+ '╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝',
20
+ ];
21
+
22
+ function brandColors() {
23
+ return palette({
24
+ primary: BRAND_ANSI.primary,
25
+ accent: BRAND_ANSI.accent,
26
+ muted: BRAND_ANSI.muted,
27
+ bold: '\x1b[1m',
28
+ dim: '\x1b[2m',
29
+ reset: '\x1b[0m',
30
+ });
31
+ }
32
+
33
+ function brandHeader(catalog, out) {
34
+ const C = brandColors();
35
+ const a = catalog.alma;
36
+ out('\n');
37
+ BANNER.forEach((line, idx) => {
38
+ if (idx === 0) out(` ${C.accent}${C.bold}${BOLT}${C.reset} ${C.primary}${C.bold}${line}${C.reset}\n`);
39
+ else out(` ${C.primary}${C.bold}${line}${C.reset}\n`);
40
+ });
41
+ out(` ${C.accent}${C.bold}${a.title}${C.reset} ${C.dim}· ${a.tagline}${C.reset}\n`);
42
+ out(` ${C.dim}${a.replHint}${C.reset}\n`);
43
+ }
44
+
45
+ function userPrompt(catalog, session) {
46
+ const C = palette({ primary: BRAND_ANSI.primary, reset: '\x1b[0m' });
47
+ const first = firstNameFromSession(session);
48
+ const label = first ? `${first}:` : catalog.alma.prompt;
49
+ return `${C.primary}${BOLT}${C.reset} ${label}`;
50
+ }
51
+
52
+ function displayWidth(s) {
53
+ let w = 0;
54
+ for (const ch of s) w += ch === BOLT ? 2 : 1;
55
+ return w;
56
+ }
57
+
58
+ async function printAlmaReply(res, catalog, write, { streamIsTTY = false, env = null } = {}) {
59
+ if (!res || !res.ok) return;
60
+ const body = String(res.text || '').trim();
61
+ if (!body) return;
62
+ const C = brandColors();
63
+ const label = `${C.accent}${C.bold}${BOLT} ${catalog.alma.title}:${C.reset}`;
64
+ const pad = ' '.repeat(displayWidth(` ${BOLT} ${catalog.alma.title}: `));
65
+ const lines = renderMarkdown(body).split('\n');
66
+ let block = `\n ${label} ${lines[0]}\n`;
67
+ for (let i = 1; i < lines.length; i++) block += lines[i] ? `${pad}${lines[i]}\n` : '\n';
68
+ const animate = !!streamIsTTY && !(env && env.NO_ANIMATION);
69
+ const outStream = { write: (s) => { write(s); return true; }, isTTY: animate };
70
+ await typeOut(block, { stream: outStream });
71
+ }
72
+
73
+ async function almaTurn({ io, ask, stdinIsTTY, streamIsTTY, env, write, hubAccessToken, catalog, deps, message }) {
74
+ const a = catalog.alma;
75
+ const C = brandColors();
76
+ const dimNote = (text) => write(` ${C.dim}${text}${C.reset}\n`);
77
+
78
+ let pending = null;
79
+ const links = [];
80
+ const capture = (evt) => {
81
+ if (!evt) return;
82
+ if (evt.type === 'confirmation_required') pending = { text: evt.text || '', detail: evt.detail || '', data: evt.data || {} };
83
+ else if (evt.type === 'link' && evt.data && evt.data.to) links.push(String(evt.data.to));
84
+ };
85
+
86
+ const res = await oneTurn({ io, hubAccessToken, message, catalog: { ask: a }, deps, onStreamEvent: capture });
87
+ await printAlmaReply(res, catalog, write, { streamIsTTY, env });
88
+ for (const to of links) dimNote(a.webLink(to));
89
+ if (!res.ok || !pending) return res;
90
+
91
+ if (pending.text) write(`\n ${renderMarkdown(pending.text)}\n`);
92
+ if (pending.detail) io.warn(a.irreversible);
93
+ if (pending.data && pending.data.to) dimNote(a.webLink(pending.data.to));
94
+
95
+ const choice = await promptSelect({
96
+ ask,
97
+ stdinIsTTY,
98
+ out: (line) => write(` ${line}\n`),
99
+ header: a.confirmPrompt,
100
+ items: [{ id: 'yes', label: a.confirmYes }, { id: 'no', label: a.confirmNo }],
101
+ labelFor: (it) => it.label,
102
+ });
103
+ const approved = !!(choice && choice.id === 'yes');
104
+
105
+ const dres = await oneTurn({
106
+ io,
107
+ hubAccessToken,
108
+ message: null,
109
+ catalog: { ask: a },
110
+ deps,
111
+ sendTurn: ({ onEvent }) => deps.decideAlma({ hubAccessToken, approved, onEvent }),
112
+ });
113
+ await printAlmaReply(dres, catalog, write, { streamIsTTY, env });
114
+ return res;
115
+ }
116
+
117
+ async function runAlmaShell({ io, ask, stdinIsTTY, streamIsTTY, env = process.env, rawOut, session, catalog, deps = makeAlmaDeps() }) {
118
+ const a = catalog.alma;
119
+ const write = rawOut || ((s) => process.stdout.write(s));
120
+ const hubAccessToken = session ? session.hubAccessToken : null;
121
+
122
+ brandHeader(catalog, write);
123
+
124
+ // Collect the (code-free) project map ONCE, then ask consent before ever sending
125
+ // it — privacy gate. Declining (or no TTY to ask) sends every turn without context.
126
+ const ctxBlock = deps.collectContextBlock
127
+ ? deps.collectContextBlock({ status: (fn) => (deps.withStatus ? deps.withStatus(a.contextLabel, fn) : fn()) })
128
+ : '';
129
+ const readLine = typeof ask === 'function' ? ask : (p) => io.ask(p);
130
+
131
+ let ctxConsent = false;
132
+ if (ctxBlock && stdinIsTTY) {
133
+ const choice = await promptSelect({
134
+ ask: readLine,
135
+ stdinIsTTY,
136
+ out: (line) => write(` ${line}\n`),
137
+ header: a.contextConsentHeader,
138
+ hint: a.contextConsentHint,
139
+ items: [{ id: 'yes', label: a.contextConsentYes }, { id: 'no', label: a.contextConsentNo }],
140
+ labelFor: (it) => it.label,
141
+ });
142
+ ctxConsent = !!(choice && choice.id === 'yes');
143
+ if (!ctxConsent) io.notify(a.contextConsentDeclined);
144
+ }
145
+ const withCtx = (msg) => (ctxConsent && ctxBlock ? `${ctxBlock}\n\n${msg}` : msg);
146
+ const prompt = userPrompt(catalog, session);
147
+ let first = true;
148
+ for (;;) {
149
+ const line = (await readLine(prompt)).trim();
150
+ if (!line || EXIT_WORDS.has(line.toLowerCase())) {
151
+ io.notify(a.bye);
152
+ break;
153
+ }
154
+ const message = first ? withCtx(line) : line;
155
+ first = false;
156
+ await almaTurn({ io, ask, stdinIsTTY, streamIsTTY, env, write, hubAccessToken, catalog, deps, message });
157
+ }
158
+ return { ok: true };
159
+ }
160
+
161
+ module.exports = { runAlmaShell, almaTurn, printAlmaReply, brandHeader, userPrompt };
package/src/ansi.js ADDED
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ const { isInteractive } = require('./terminal-progress');
4
+ const { BRAND_ANSI } = require('./brand-ansi');
5
+
6
+ // THE ONE GATE for terminal colour (talents-ai-score).
7
+
8
+ // `NO_COLOR` per the de-facto standard: present and non-empty disables colour, whatever the value.
9
+ function noColorRequested(env = process.env) {
10
+ const value = env ? env.NO_COLOR : undefined;
11
+ return typeof value === 'string' && value.length > 0;
12
+ }
13
+
14
+ // The gate. `stream` defaults to stdout because every palette in this repo
15
+ // decorates text that ends up there.
16
+ function colorEnabled({ stream = process.stdout, env = process.env } = {}) {
17
+ if (noColorRequested(env)) return false;
18
+ return isInteractive(stream);
19
+ }
20
+
21
+ // Wraps a map of escape codes so each key yields its code when colour is on and `''` when it is off.
22
+ function palette(codes, opts = {}) {
23
+ const out = {};
24
+ for (const key of Object.keys(codes)) {
25
+ const code = codes[key];
26
+ Object.defineProperty(out, key, {
27
+ enumerable: true,
28
+ get: () => (colorEnabled(opts) ? code : ''),
29
+ });
30
+ }
31
+ return out;
32
+ }
33
+
34
+ function styleQuestion(text, opts = {}) {
35
+ const ANSI = palette({ bold: '\x1b[1m', primary: BRAND_ANSI.primary, reset: '\x1b[0m' }, opts);
36
+ return `${ANSI.bold}${ANSI.primary}${text}${ANSI.reset}`;
37
+ }
38
+
39
+ const SUCCESS_GREEN = '\x1b[38;5;114m';
40
+
41
+ function styleSuccess(text, opts = {}) {
42
+ const ANSI = palette({ success: SUCCESS_GREEN, reset: '\x1b[0m' }, opts);
43
+ return `${ANSI.success}${text}${ANSI.reset}`;
44
+ }
45
+
46
+ // Bold a field LABEL (no colour). Gate-aware via `palette`: empty codes when
47
+ // colour is off (non-TTY / NO_COLOR), so piped/redirected output stays clean.
48
+ function styleLabel(text, opts = {}) {
49
+ const ANSI = palette({ bold: '\x1b[1m', reset: '\x1b[0m' }, opts);
50
+ return `${ANSI.bold}${text}${ANSI.reset}`;
51
+ }
52
+
53
+ // Bold only the "Label:" prefix of a "Label: value" line, leaving the value
54
+ // plain. No `: ` -> returned unchanged (value-only lines never get bolded).
55
+ function styleLabelPrefix(line, opts = {}) {
56
+ const s = typeof line === 'string' ? line : '';
57
+ const idx = s.indexOf(': ');
58
+ if (idx === -1) return s;
59
+ return `${styleLabel(s.slice(0, idx + 1), opts)}${s.slice(idx + 1)}`;
60
+ }
61
+
62
+ module.exports = { palette, colorEnabled, noColorRequested, styleQuestion, styleSuccess, styleLabel, styleLabelPrefix, SUCCESS_GREEN };
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ // My-applications client (hub `works/candidatures/me`, talent Bearer). Lists the
4
+ // positions the talent applied to, with the candidature status.
5
+
6
+ const { postJsonWithTimeout } = require('./backend-request');
7
+ const { reasonForError } = require('./onboarding-client');
8
+
9
+ const DEFAULT_TIMEOUT_MS = 20000;
10
+ const DEFAULT_LIMIT = 30;
11
+
12
+ function parseJson(raw) {
13
+ try {
14
+ return raw ? JSON.parse(raw) : null;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ function bearerHeaders(accessToken) {
21
+ const headers = {};
22
+ if (typeof accessToken === 'string' && accessToken) headers.Authorization = `Bearer ${accessToken}`;
23
+ return headers;
24
+ }
25
+
26
+ function str(v) {
27
+ return typeof v === 'string' && v ? v : null;
28
+ }
29
+
30
+ function normalizeApplication(raw) {
31
+ const it = raw && typeof raw === 'object' ? raw : {};
32
+ const project = it.project && typeof it.project === 'object' ? it.project : {};
33
+ const position = it.position && typeof it.position === 'object' ? it.position : {};
34
+ const org = it.organization && typeof it.organization === 'object' ? it.organization : {};
35
+ return {
36
+ positionId: str(position.id),
37
+ title: str(position.name) || str(project.name),
38
+ projectName: str(project.name),
39
+ company: str(org.name),
40
+ status: str(it.status),
41
+ createdAt: str(it.createdAt),
42
+ };
43
+ }
44
+
45
+ async function requestMyCandidatures({ hubAccessToken } = {}, { endpoint, limit = DEFAULT_LIMIT, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
46
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
47
+ if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
48
+ const criteria = { limit, offset: 0 };
49
+ const url = `${endpoint}${endpoint.includes('?') ? '&' : '?'}criteria=${encodeURIComponent(JSON.stringify(criteria))}`;
50
+ let res;
51
+ try {
52
+ res = await postJsonWithTimeout(url, null, timeoutMs, 'GET', null, bearerHeaders(hubAccessToken));
53
+ } catch (e) {
54
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
55
+ }
56
+ if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
57
+
58
+ const env = parseJson(res.raw);
59
+ const data = env && env.status === 'OK' ? env.data : env;
60
+ const rows = Array.isArray(data) ? data : (Array.isArray(data && data.items) ? data.items : null);
61
+ if (!rows) return { ok: false, reason: 'bad-response' };
62
+ return { ok: true, items: rows.map(normalizeApplication), meta: (env && typeof env.meta === 'object' ? env.meta : null) };
63
+ }
64
+
65
+ async function fetchApplications(deps = {}, { hubAccessToken, limit = DEFAULT_LIMIT, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
66
+ const {
67
+ getMyCandidaturesEndpoint = require('./config').getMyCandidaturesEndpoint,
68
+ requestMyCandidatures: request = requestMyCandidatures,
69
+ } = deps;
70
+ const endpoint = getMyCandidaturesEndpoint();
71
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
72
+ return request({ hubAccessToken }, { endpoint, limit, timeoutMs });
73
+ }
74
+
75
+ module.exports = {
76
+ normalizeApplication,
77
+ requestMyCandidatures,
78
+ fetchApplications,
79
+ };
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ // `applications` flow: list the positions the talent applied to + status.
4
+ // Read-only. Writes the SAME ordinal->positionId cache as find-projects, so
5
+ // `show-project N` / `save-project N` work after `applications` too.
6
+
7
+ const { styleLabelPrefix } = require('./ansi');
8
+
9
+ function makeApplicationsDeps(overrides = {}) {
10
+ const client = require('./applications-client');
11
+ return {
12
+ fetchApplications: (opts) => client.fetchApplications({}, opts),
13
+ writePositionCache: (ids) => require('./find-projects-cache').writePositionCache(ids),
14
+ ...overrides,
15
+ };
16
+ }
17
+
18
+ async function runApplications({ io, rawOut, session, catalog, opts = {}, deps = makeApplicationsDeps() }) {
19
+ const fp = catalog.applications;
20
+ const hubAccessToken = session ? session.hubAccessToken : null;
21
+
22
+ const doFetch = () => deps.fetchApplications({ hubAccessToken });
23
+ const res = opts.json ? await doFetch() : await io.withProgress(fp.loading, doFetch);
24
+ if (!res.ok) {
25
+ io.error(fp.fetchFailed(res.reason));
26
+ return { ok: false, reason: res.reason };
27
+ }
28
+ const items = Array.isArray(res.items) ? res.items : [];
29
+
30
+ if (opts.json) {
31
+ (rawOut || ((s) => process.stdout.write(s)))(`${JSON.stringify({ ok: true, items }, null, 2)}\n`);
32
+ return { ok: true, items };
33
+ }
34
+
35
+ // Human path: refresh the number->positionId cache (last listing wins).
36
+ try { deps.writePositionCache(items.map((it) => it.positionId)); } catch { /* best-effort */ }
37
+
38
+ io.section(fp.title);
39
+ if (items.length === 0) {
40
+ io.notify(fp.none);
41
+ return { ok: true, items };
42
+ }
43
+ // No ids in the human render (they ride the cache + --json).
44
+ items.forEach((a, i) => {
45
+ if (i > 0) io.notify('');
46
+ const headline = a.title || a.projectName || fp.untitled;
47
+ io.notify(`${i + 1}) ${headline}${a.company ? ` — ${a.company}` : ''}`);
48
+ io.notify(styleLabelPrefix(fp.statusLine(a.status || fp.statusUnknown)));
49
+ });
50
+ io.notify(styleLabelPrefix(fp.countNote(items.length)));
51
+ io.success(fp.done);
52
+ return { ok: true, items };
53
+ }
54
+
55
+ module.exports = {
56
+ makeApplicationsDeps,
57
+ runApplications,
58
+ };
@@ -0,0 +1,144 @@
1
+ 'use strict';
2
+
3
+ const { postJsonWithTimeout, extractSetCookie } = require('./backend-request');
4
+
5
+ // Login / token acquisition against the Hub's better-auth email engine (owner decision, supersedes ADR-031 for the email path).
6
+
7
+ const DEFAULT_TIMEOUT_MS = 20000;
8
+ const SESSION_COOKIE_NAME = 'better-auth.session_token';
9
+ // Refresh the cookie session's own validity from the Set-Cookie Max-Age; fall
10
+ // back to 7 days (better-auth's default) when the attribute is absent.
11
+ const DEFAULT_COOKIE_TTL_SECONDS = 7 * 24 * 60 * 60;
12
+
13
+ function parseJson(raw) {
14
+ try { return raw ? JSON.parse(raw) : null; } catch { return null; }
15
+ }
16
+
17
+ // `exp` (JWT seconds) -> ISO, or null. Never throws.
18
+ function expiresAtFromJwt(token) {
19
+ try {
20
+ const parts = String(token).split('.');
21
+ if (parts.length < 2) return null;
22
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
23
+ const exp = typeof payload.exp === 'number' && Number.isFinite(payload.exp) ? payload.exp : null;
24
+ return exp === null ? null : new Date(exp * 1000).toISOString();
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ // ISO expiry for the session cookie, read from the Set-Cookie `Max-Age` (or
31
+ // `Expires`) of the named cookie; defaults to +7 days when neither is present.
32
+ function cookieExpiresAtFromHeaders(headers, name = SESSION_COOKIE_NAME) {
33
+ const raw = headers && headers['set-cookie'];
34
+ const list = Array.isArray(raw) ? raw : (typeof raw === 'string' && raw ? [raw] : []);
35
+ // Match the base name with or without better-auth's HTTPS `__Secure-`/`__Host-` prefixes.
36
+ const names = [name, `__Secure-${name}`, `__Host-${name}`];
37
+ const entry = list.find((e) => typeof e === 'string' && names.some((n) => e.startsWith(`${n}=`)));
38
+ if (entry) {
39
+ const maxAge = /(?:^|;\s*)max-age=(\d+)/i.exec(entry);
40
+ if (maxAge) return new Date(Date.now() + Number(maxAge[1]) * 1000).toISOString();
41
+ const expires = /(?:^|;\s*)expires=([^;]+)/i.exec(entry);
42
+ if (expires) {
43
+ const t = Date.parse(expires[1]);
44
+ if (Number.isFinite(t)) return new Date(t).toISOString();
45
+ }
46
+ }
47
+ return new Date(Date.now() + DEFAULT_COOKIE_TTL_SECONDS * 1000).toISOString();
48
+ }
49
+
50
+ function normalizeResponse(status, raw) {
51
+ if (status >= 200 && status < 300) {
52
+ let json = null;
53
+ try { json = raw ? JSON.parse(raw) : null; } catch { json = null; }
54
+ const payload = json && typeof json.data === 'object' && json.data !== null ? json.data : json;
55
+ const accessToken = payload && typeof payload.accessToken === 'string' && payload.accessToken ? payload.accessToken : null;
56
+ if (!accessToken) return { ok: false, reason: 'bad-response', status };
57
+ const expiresAt = payload && typeof payload.expiresAt === 'string' && payload.expiresAt ? payload.expiresAt : null;
58
+ const emailRaw = payload && (payload.userEmail || payload.email);
59
+ const email = typeof emailRaw === 'string' && emailRaw ? emailRaw : null;
60
+ const hubAccessToken = payload && typeof payload.hubAccessToken === 'string' && payload.hubAccessToken ? payload.hubAccessToken : null;
61
+ return { ok: true, accessToken, expiresAt, email, hubAccessToken };
62
+ }
63
+ if (status === 401) return { ok: false, reason: 'invalid-credentials', status };
64
+ if (status === 409 || status === 422) return { ok: false, reason: 'no-email-password', status };
65
+ if (status === 502 || status === 503) return { ok: false, reason: 'upstream', status };
66
+ return { ok: false, reason: 'http-error', status };
67
+ }
68
+
69
+ // Maps the sign-in POST outcome to a captured cookie + email, or a named reason.
70
+ function normalizeSignIn(status, raw, headers) {
71
+ if (status >= 200 && status < 300) {
72
+ const cookie = extractSetCookie(headers, SESSION_COOKIE_NAME);
73
+ if (!cookie) return { ok: false, reason: 'bad-response', status };
74
+ const body = parseJson(raw);
75
+ const user = body && typeof body === 'object' ? body.user : null;
76
+ const emailRaw = user && typeof user.email === 'string' ? user.email : null;
77
+ return {
78
+ ok: true,
79
+ cookie,
80
+ cookieExpiresAt: cookieExpiresAtFromHeaders(headers),
81
+ email: emailRaw || null,
82
+ };
83
+ }
84
+ if (status === 401) return { ok: false, reason: 'invalid-credentials', status };
85
+ if (status === 409 || status === 422) return { ok: false, reason: 'no-email-password', status };
86
+ if (status === 502 || status === 503) return { ok: false, reason: 'upstream', status };
87
+ return { ok: false, reason: 'http-error', status };
88
+ }
89
+
90
+ // Mints a hub JWT from a better-auth session cookie: `GET {tokenEndpoint}` with the `Cookie` header.
91
+ async function requestAuthToken({ cookie } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
92
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
93
+ if (!cookie) return { ok: false, reason: 'no-cookie' };
94
+ let res;
95
+ try {
96
+ res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, { Cookie: cookie });
97
+ } catch (e) {
98
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
99
+ }
100
+ if (res.status < 200 || res.status >= 300) return { ok: false, reason: 'http-error', status: res.status };
101
+ const body = parseJson(res.raw);
102
+ const token = body && typeof body.token === 'string' && body.token ? body.token : null;
103
+ if (!token) return { ok: false, reason: 'bad-response', status: res.status };
104
+ return { ok: true, accessToken: token, expiresAt: expiresAtFromJwt(token) };
105
+ }
106
+
107
+ // The full email login: sign-in (capture cookie) -> mint JWT.
108
+ async function requestLogin(
109
+ { email, password },
110
+ { signInEndpoint, tokenEndpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
111
+ ) {
112
+ if (!signInEndpoint) return { ok: false, reason: 'no-endpoint' };
113
+ let signIn;
114
+ try {
115
+ const { status, raw, headers } = await postJsonWithTimeout(signInEndpoint, { email, password }, timeoutMs);
116
+ signIn = normalizeSignIn(status, raw, headers);
117
+ } catch (e) {
118
+ return { ok: false, reason: (e && e.kind) || 'local-error' };
119
+ }
120
+ if (!signIn.ok) return signIn;
121
+
122
+ const tok = await requestAuthToken({ cookie: signIn.cookie }, { endpoint: tokenEndpoint, timeoutMs });
123
+ if (!tok.ok) return { ok: false, reason: 'token-fetch-failed', status: tok.status || null };
124
+
125
+ return {
126
+ ok: true,
127
+ accessToken: tok.accessToken,
128
+ hubAccessToken: tok.accessToken,
129
+ expiresAt: tok.expiresAt,
130
+ email: signIn.email || (typeof email === 'string' ? email : null),
131
+ cookie: signIn.cookie,
132
+ cookieExpiresAt: signIn.cookieExpiresAt,
133
+ };
134
+ }
135
+
136
+ module.exports = {
137
+ requestLogin,
138
+ requestAuthToken,
139
+ normalizeSignIn,
140
+ normalizeResponse,
141
+ expiresAtFromJwt,
142
+ SESSION_COOKIE_NAME,
143
+ DEFAULT_TIMEOUT_MS,
144
+ };
@@ -0,0 +1,118 @@
1
+ 'use strict';
2
+
3
+ const { postJsonWithTimeout } = require('./backend-request');
4
+
5
+ // OAuth 2.0 Device Authorization Grant (RFC 8628) client against the Hub broker.
6
+
7
+ const DEFAULT_TIMEOUT_MS = 20000;
8
+
9
+ const CLIENT_ID = 'shakers-cli';
10
+ const DEVICE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
11
+
12
+ function parseJson(raw) {
13
+ try {
14
+ return raw ? JSON.parse(raw) : null;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ function unwrap(json) {
21
+ if (json && typeof json === 'object' && json.data && typeof json.data === 'object') return json.data;
22
+ return json && typeof json === 'object' ? json : null;
23
+ }
24
+
25
+ function str(value) {
26
+ return typeof value === 'string' && value ? value : null;
27
+ }
28
+
29
+ function posInt(value) {
30
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.floor(value) : null;
31
+ }
32
+
33
+ async function requestDeviceAuthorization({ endpoint, clientId = CLIENT_ID, scope = null, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
34
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
35
+ const body = { client_id: clientId };
36
+ if (scope) body.scope = scope;
37
+ let res;
38
+ try {
39
+ res = await postJsonWithTimeout(endpoint, body, timeoutMs);
40
+ } catch (e) {
41
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
42
+ }
43
+ const { status, raw } = res;
44
+ if (status === 502 || status === 503) return { ok: false, reason: 'upstream', status };
45
+ if (status < 200 || status >= 300) return { ok: false, reason: 'http-error', status };
46
+
47
+ const data = unwrap(parseJson(raw));
48
+ const deviceCode = data && str(data.device_code);
49
+ const userCode = data && str(data.user_code);
50
+ const verificationUri = data && str(data.verification_uri);
51
+ if (!deviceCode || !userCode || !verificationUri) return { ok: false, reason: 'bad-response', status };
52
+
53
+ return {
54
+ ok: true,
55
+ deviceCode,
56
+ userCode,
57
+ verificationUri,
58
+ verificationUriComplete: (data && str(data.verification_uri_complete)) || verificationUri,
59
+ expiresIn: (data && posInt(data.expires_in)) || 600,
60
+ interval: (data && posInt(data.interval)) || 5,
61
+ };
62
+ }
63
+
64
+ // One poll. Success carries the better-auth SESSION token (access_token), NOT the API JWT.
65
+ async function requestDeviceToken({ deviceCode, clientId = CLIENT_ID }, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
66
+ if (!endpoint) return { status: 'error', reason: 'no-endpoint' };
67
+ if (!deviceCode) return { status: 'error', reason: 'no-device-code' };
68
+ const body = { grant_type: DEVICE_GRANT_TYPE, device_code: deviceCode, client_id: clientId };
69
+ let res;
70
+ try {
71
+ res = await postJsonWithTimeout(endpoint, body, timeoutMs);
72
+ } catch (e) {
73
+ return { status: 'error', reason: (e && e.kind) || 'network-error' };
74
+ }
75
+ const { status, raw } = res;
76
+ const data = unwrap(parseJson(raw));
77
+
78
+ if (status >= 200 && status < 300) {
79
+ const accessToken = data && (str(data.access_token) || str(data.accessToken) || str(data.token));
80
+ if (!accessToken) return { status: 'error', reason: 'bad-response', http: status };
81
+ return {
82
+ status: 'authorized',
83
+ accessToken,
84
+ tokenType: (data && str(data.token_type)) || 'Bearer',
85
+ expiresIn: data && posInt(data.expires_in),
86
+ isNewUser: !!(data && (data.isNewUser === true || data.is_new_user === true)),
87
+ email: data && (str(data.email) || str(data.userEmail)),
88
+ };
89
+ }
90
+
91
+ const code = data && (str(data.error) || str(data.code));
92
+ if (code === 'authorization_pending') return { status: 'pending' };
93
+ if (code === 'slow_down') return { status: 'slow_down' };
94
+ if (code === 'access_denied') return { status: 'denied' };
95
+ if (code === 'expired_token') return { status: 'expired' };
96
+ if (status === 502 || status === 503) return { status: 'error', reason: 'upstream', http: status };
97
+ return { status: 'error', reason: code || 'http-error', http: status };
98
+ }
99
+
100
+ async function requestApiToken({ accessToken }, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
101
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
102
+ if (!accessToken) return { ok: false, reason: 'no-access-token' };
103
+ let res;
104
+ try {
105
+ res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, { Authorization: `Bearer ${accessToken}` });
106
+ } catch (e) {
107
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
108
+ }
109
+ const { status, raw } = res;
110
+ if (status === 502 || status === 503) return { ok: false, reason: 'upstream', status };
111
+ if (status < 200 || status >= 300) return { ok: false, reason: 'http-error', status };
112
+ const data = unwrap(parseJson(raw));
113
+ const token = data && str(data.token);
114
+ if (!token) return { ok: false, reason: 'bad-response', status };
115
+ return { ok: true, token };
116
+ }
117
+
118
+ module.exports = { requestDeviceAuthorization, requestDeviceToken, requestApiToken, CLIENT_ID, DEVICE_GRANT_TYPE, DEFAULT_TIMEOUT_MS };