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
package/bin/rate.js ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // `rate` — view the talent's per-project pricing (src/rate-flow.js). View-only.
5
+
6
+ const { detectReportLang, getCatalog } = require('../src/i18n');
7
+ const { loadAuthSession, sessionStatus } = require('../src/auth-session-store');
8
+ const { createStdinAsk } = require('../src/stdin-ask');
9
+ const { makeIo } = require('./register');
10
+ const { makeRevealSink } = require('../src/reveal-output');
11
+ const { runRate, makeRateDeps } = require('../src/rate-flow');
12
+
13
+ function parseRateArgs(argv = []) {
14
+ const o = { lang: null, help: false, json: false, set: false };
15
+ for (let i = 0; i < argv.length; i++) {
16
+ const a = argv[i];
17
+ if (a === '--help' || a === '-h') o.help = true;
18
+ else if (a === '--lang') { const v = argv[++i]; o.lang = v === 'es' || v === 'en' ? v : null; }
19
+ else if (a.startsWith('--lang=')) { const v = a.slice(7); o.lang = v === 'es' || v === 'en' ? v : null; }
20
+ else if (a === '--json') o.json = true;
21
+ else if (a === '--set') o.set = true;
22
+ }
23
+ return o;
24
+ }
25
+
26
+ async function run(
27
+ argv = process.argv.slice(2),
28
+ { ask: injectedAsk = null, stdinIsTTY: ttyOverride = undefined, deps = null, session: injectedSession = null, out = null } = {},
29
+ ) {
30
+ const opts = parseRateArgs(argv);
31
+ const lang = opts.lang || detectReportLang();
32
+ const catalog = getCatalog(lang);
33
+ const fp = catalog.rate;
34
+ const write = out || ((s) => process.stdout.write(s));
35
+
36
+ if (opts.help) {
37
+ write(fp.help + '\n');
38
+ return;
39
+ }
40
+
41
+ const session = injectedSession || loadAuthSession();
42
+ if (sessionStatus(session) !== 'active') {
43
+ process.stderr.write(`\n ${fp.loginRequired}\n\n`);
44
+ process.exitCode = 1;
45
+ return;
46
+ }
47
+
48
+ const stdinIsTTY = ttyOverride !== undefined ? ttyOverride : !!process.stdin.isTTY;
49
+ const ask = injectedAsk || createStdinAsk();
50
+ const sink = makeRevealSink({ json: opts.json, interactive: !!opts.set, out, env: process.env });
51
+ try {
52
+ const io = makeIo({ ask, lang, out: sink.write, stdinIsTTY });
53
+ const result = await runRate({
54
+ io, rawOut: sink.write, ask, session, catalog,
55
+ opts: { json: opts.json, set: opts.set, stdinIsTTY },
56
+ deps: deps || makeRateDeps(),
57
+ });
58
+ await sink.finish();
59
+ if (result && result.ok === false) process.exitCode = 1;
60
+ return result;
61
+ } finally {
62
+ if (!injectedAsk) ask.close();
63
+ }
64
+ }
65
+
66
+ module.exports = { run, parseRateArgs };
67
+
68
+ if (require.main === module) {
69
+ run();
70
+ }
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { detectFlowLang, getCatalog } = require('../src/i18n');
5
+ const { loadAuthSession, sessionStatus } = require('../src/auth-session-store');
6
+ const { createStdinAsk } = require('../src/stdin-ask');
7
+ const { run: runLogin } = require('./login');
8
+ const { run: runAiUsage } = require('./ai-usage');
9
+ const {
10
+ runOnboarding, makeDeps, CURRENCIES, MONTHLY_HOURS, normalizeAmount,
11
+ WORK_SITUATIONS, EMPLOYMENT_PARTICIPATIONS, FREELANCE_OPINIONS, CHANGE_MOTIVATORS,
12
+ LANGUAGE_CODES, LANGUAGE_LEVELS, showsFreelanceOpinion, showsChangeMotivators,
13
+ } = require('../src/onboarding-flow');
14
+ const { promptSelect } = require('../src/prompt-select');
15
+ const { chooseAuthMethod } = require('../src/auth-method-picker');
16
+ const { methodIdsForFlow } = require('../src/auth-methods');
17
+ const { palette } = require('../src/ansi');
18
+ const { BRAND_ANSI } = require('../src/brand-ansi');
19
+ const { typeOut } = require('../src/typewriter');
20
+ const { withSpinner } = require('../src/terminal-progress');
21
+ const { openPath } = require('../src/open-file');
22
+ const { c: fmt } = require('../src/terminal-format');
23
+ const red = (s) => `${fmt.danger}${s}${fmt.reset}`;
24
+
25
+ const VALID_LANGS = new Set(['es', 'en']);
26
+
27
+ const COPY = {
28
+ es: {
29
+ help: 'register — registro completo en Shakers (perfil, particular/empresa + precio, uso de IA y entrevista). Uso: shakers register [--lang es|en] [--accept-disclaimer] [--no-open]',
30
+ missingAccept: 'Modo no interactivo: falta --accept-disclaimer.\n register es un flujo interactivo con pasos que envian datos (import, evaluacion, entrevista).\n Reejecuta con --accept-disclaimer, aportando las respuestas por stdin.',
31
+ loginNeeded: 'Necesitas iniciar sesion para el registro.',
32
+ },
33
+ en: {
34
+ help: 'register — full Shakers registration (profile, individual/company + pricing, AI usage and interview). Usage: shakers register [--lang es|en] [--accept-disclaimer] [--no-open]',
35
+ missingAccept: 'Non-interactive mode: --accept-disclaimer is missing.\n register is an interactive flow with steps that send data (import, evaluation, interview).\n Re-run with --accept-disclaimer, feeding the answers over stdin.',
36
+ loginNeeded: 'You need to sign in for registration.',
37
+ },
38
+ };
39
+
40
+ // `--email`/`--google`/`--provider <m>` select the auth method and skip the
41
+ // picker; an unavailable method falls through to the picker rather than failing.
42
+ function parseArgs(argv) {
43
+ const o = { lang: null, help: false, acceptDisclaimer: false, methodFlag: null, noOpen: false };
44
+ for (let i = 0; i < argv.length; i++) {
45
+ const a = argv[i];
46
+ if (a === '--lang') o.lang = VALID_LANGS.has(argv[++i]) ? argv[i] : null;
47
+ else if (a.startsWith('--lang=')) { const v = a.slice(7); o.lang = VALID_LANGS.has(v) ? v : null; }
48
+ else if (a === '--accept-disclaimer') o.acceptDisclaimer = true;
49
+ else if (a === '--no-open') o.noOpen = true;
50
+ else if (a === '--email') o.methodFlag = 'email';
51
+ else if (a === '--google') o.methodFlag = 'google';
52
+ else if (a === '--linkedin') o.methodFlag = 'linkedin';
53
+ else if (a === '--provider' && (argv[i + 1] === 'email' || argv[i + 1] === 'google' || argv[i + 1] === 'linkedin')) o.methodFlag = argv[++i];
54
+ else if (a === '--provider=email') o.methodFlag = 'email';
55
+ else if (a === '--provider=google') o.methodFlag = 'google';
56
+ else if (a === '--provider=linkedin') o.methodFlag = 'linkedin';
57
+ else if (a === '--help' || a === '-h') o.help = true;
58
+ }
59
+ return o;
60
+ }
61
+
62
+ function isYes(answer) {
63
+ const s = (answer || '').trim().toLowerCase();
64
+ return s === 's' || s === 'si' || s === 'sí' || s === 'y' || s === 'yes';
65
+ }
66
+
67
+ function detectTimezone() {
68
+ try { return Intl.DateTimeFormat().resolvedOptions().timeZone || ''; } catch { return ''; }
69
+ }
70
+
71
+ function makeIo({ ask, lang, out, acceptDisclaimer, stdinIsTTY = false }) {
72
+ const C = palette({ primary: BRAND_ANSI.primary, accent: BRAND_ANSI.accent, muted: BRAND_ANSI.muted, danger: BRAND_ANSI.danger, warning: BRAND_ANSI.warning, success: '\x1b[38;5;114m', bold: '\x1b[1m', dim: '\x1b[2m', reset: '\x1b[0m' });
73
+ const write = (line) => out(` ${line}\n`);
74
+ const writeErr = (line) => out(` ${C.danger}${line}${C.reset}\n`);
75
+ const selectOut = (line) => out(` ${line}\n`);
76
+ return {
77
+ lang,
78
+ section: (text) => out(`\n ${C.primary}${C.bold}${text}${C.reset}\n`),
79
+ notify: (text) => write(text),
80
+ // Success/confirmation: a distinct soft green. Error: red. Warning: yellow.
81
+ // All degrade to plain when color is off.
82
+ success: (text) => out(` ${C.success}${text}${C.reset}\n`),
83
+ error: (text) => out(` ${C.danger}${text}${C.reset}\n`),
84
+ warn: (text) => out(` ${C.warning}${text}${C.reset}\n`),
85
+ // Loader for an async wait: animated spinner on a real TTY, else a persistent
86
+ // label line — the label ALWAYS shows (the old animate-gate ran silently off-TTY).
87
+ withProgress: (label, task) => {
88
+ const animate = stdinIsTTY && process.stderr.isTTY && !process.env.NO_ANIMATION;
89
+ if (animate) return withSpinner(label, task);
90
+ write(label);
91
+ return task();
92
+ },
93
+ ask: async (text) => (await ask(` ${text} `)),
94
+ // Dynamic arrow selector (raw TTY, numbered fallback off-TTY) for any single choice.
95
+ select: ({ header = '', hint = '', items = [], labelFor = (x) => String(x), initialMarked = [] } = {}) =>
96
+ promptSelect({ ask, stdinIsTTY, out: selectOut, items, labelFor, header, hint, initialMarked }),
97
+ confirm: async (text) => (acceptDisclaimer ? true : isYes(await ask(` ${text} `))),
98
+ disclaimer: (infoText, goalText) => {
99
+ out(` ${C.dim}${infoText}${C.reset}\n`);
100
+ out(` ${C.dim}${goalText}${C.reset}\n`);
101
+ },
102
+ askPricing: async (c) => {
103
+ const askAmount = async (label, cur) => {
104
+ for (;;) {
105
+ const raw = (await ask(` ${label(cur)} `)).trim();
106
+ if (!stdinIsTTY || normalizeAmount(raw) !== null) return raw;
107
+ writeErr(c.pricingAmountInvalid);
108
+ }
109
+ };
110
+ // Standard per-project price: always asked (the main modality).
111
+ const fullTimeCurrency = await promptSelect({ ask, stdinIsTTY, out: selectOut, items: CURRENCIES, header: ` ${c.askPricingFullCurrency}` });
112
+ const fullTimeAmount = await askAmount(c.askPricingFullAmount, fullTimeCurrency);
113
+ // Part-time price: optional second modality, gated on the y/n.
114
+ const partTimeSelected = isYes(await ask(` ${c.askPricingPartSelect} `));
115
+ let partTimeAmount;
116
+ let partTimeCurrency;
117
+ if (partTimeSelected) {
118
+ partTimeCurrency = await promptSelect({ ask, stdinIsTTY, out: selectOut, items: CURRENCIES, header: ` ${c.askPricingPartCurrency}` });
119
+ partTimeAmount = await askAmount(c.askPricingPartAmount, partTimeCurrency);
120
+ }
121
+ return { fullTimeSelected: true, fullTimeAmount, fullTimeCurrency, partTimeSelected, partTimeAmount, partTimeCurrency };
122
+ },
123
+ askAvailability: async (c) => {
124
+ const available = isYes(await ask(` ${c.availableAsk} `));
125
+ const monthlyHours = await promptSelect({ ask, stdinIsTTY, out: selectOut, items: MONTHLY_HOURS, header: ` ${c.monthlyHoursAsk}` });
126
+ const onlyRemote = isYes(await ask(` ${c.onlyRemoteAsk} `));
127
+ let country = '';
128
+ for (;;) {
129
+ country = (await ask(` ${c.countryAsk} `)).trim().toUpperCase();
130
+ if (!stdinIsTTY || country === '' || /^[A-Z]{2}$/.test(country)) break;
131
+ writeErr(c.countryInvalid);
132
+ }
133
+ const subdivision = (await ask(` ${c.subdivisionAsk} `)).trim();
134
+ const detectedTz = detectTimezone();
135
+ const tzAnswer = (await ask(` ${c.timezoneAsk(detectedTz)} `)).trim();
136
+ const timezone = tzAnswer || detectedTz;
137
+ const longFullTimeProjects = isYes(await ask(` ${c.longFullTimeAsk} `));
138
+ const telephoneCode = (await ask(` ${c.phonePrefixAsk} `)).trim();
139
+ const telephoneNumber = (await ask(` ${c.phoneNumberAsk} `)).trim();
140
+ return { available, monthlyHours, onlyRemote, country, subdivision, timezone, longFullTimeProjects, telephoneCode, telephoneNumber };
141
+ },
142
+ askSignUp: async (c) => {
143
+ const name = (await ask(` ${c.askName} `)).trim();
144
+ const lastName = (await ask(` ${c.askLastName} `)).trim();
145
+ const email = (await ask(` ${c.askEmail} `)).trim();
146
+ const askSecret = (prompt) => (typeof ask.secret === 'function' ? ask.secret(` ${prompt} `) : ask(` ${prompt} `).then((v) => v.trim()));
147
+ let password;
148
+ for (;;) {
149
+ password = await askSecret(c.askPassword);
150
+ const again = await askSecret(c.askPasswordConfirm);
151
+ if (password === again) break;
152
+ writeErr(c.passwordMismatch);
153
+ }
154
+ const newsletterConsent = isYes(await ask(` ${c.askNewsletter} `));
155
+ // freelanceType is NOT asked here anymore: it is derived from the work
156
+ // situation (asked once, after the sources) and sent via professional-details.
157
+ return { name, lastName, email, password, newsletterConsent };
158
+ },
159
+ askGoogleSignUp: async () => {
160
+ // Nothing to ask: Google supplies name/email; freelanceType is derived later.
161
+ return {};
162
+ },
163
+ askWorkSituation: async (c) => {
164
+ const situation = await promptSelect({
165
+ ask,
166
+ stdinIsTTY,
167
+ out: (line) => out(` ${line}\n`),
168
+ items: WORK_SITUATIONS,
169
+ labelFor: (v) => c.workSituationLabels[v] || v,
170
+ header: ` ${c.askWorkSituation}`,
171
+ });
172
+ let participation;
173
+ if (situation === 'EMPLOYED') {
174
+ participation = await promptSelect({
175
+ ask,
176
+ stdinIsTTY,
177
+ out: (line) => out(` ${line}\n`),
178
+ items: EMPLOYMENT_PARTICIPATIONS,
179
+ labelFor: (v) => c.employmentParticipationLabels[v] || v,
180
+ header: ` ${c.askEmploymentParticipation}`,
181
+ });
182
+ }
183
+ let opinion;
184
+ if (showsFreelanceOpinion(situation)) {
185
+ opinion = await promptSelect({
186
+ ask,
187
+ stdinIsTTY,
188
+ out: (line) => out(` ${line}\n`),
189
+ items: FREELANCE_OPINIONS,
190
+ labelFor: (v) => c.freelanceOpinionLabels[v] || v,
191
+ header: ` ${c.askFreelanceOpinion}`,
192
+ });
193
+ }
194
+ const freelanceIntent = situation === 'FREELANCE' ? 'ALREADY_FREELANCE' : opinion;
195
+ let changeMotivators;
196
+ if (showsChangeMotivators(freelanceIntent)) {
197
+ changeMotivators = await promptSelect({
198
+ ask,
199
+ stdinIsTTY,
200
+ out: (line) => out(` ${line}\n`),
201
+ items: CHANGE_MOTIVATORS,
202
+ labelFor: (v) => c.changeMotivatorLabels[v] || v,
203
+ header: ` ${c.askChangeMotivators}`,
204
+ });
205
+ }
206
+ return { situation, participation, opinion, changeMotivators };
207
+ },
208
+ askLanguages: async (c) => {
209
+ const rows = [];
210
+ for (;;) {
211
+ const remaining = LANGUAGE_CODES.filter((code) => !rows.some((r) => r.language === code));
212
+ const language = await promptSelect({
213
+ ask,
214
+ stdinIsTTY,
215
+ out: (line) => out(` ${line}\n`),
216
+ items: remaining,
217
+ labelFor: (v) => `${c.languageNames[v] || v} (${v})`,
218
+ header: ` ${c.askLanguageCode}`,
219
+ });
220
+ const level = await promptSelect({
221
+ ask,
222
+ stdinIsTTY,
223
+ out: (line) => out(` ${line}\n`),
224
+ items: LANGUAGE_LEVELS,
225
+ labelFor: (v) => c.languageLevelLabels[v] || v,
226
+ header: ` ${c.askLanguageLevel}`,
227
+ });
228
+ rows.push({ language, level });
229
+ if (!isYes(await ask(` ${c.askAnotherLanguage} `))) break;
230
+ }
231
+ return rows;
232
+ },
233
+ onGoogleAuthUrl: (url, userCode) => {
234
+ const g = getCatalog(lang).login;
235
+ out(`\n ${g.deviceVisit}\n ${url}\n\n ${g.deviceCodeLabel(userCode)}\n\n ${g.deviceWaiting}\n`);
236
+ },
237
+ login: async () => {
238
+ await runLogin(['--lang', lang], { ask });
239
+ return sessionStatus(loadAuthSession()) === 'active';
240
+ },
241
+ // Registration always measures the whole machine (--all-repos).
242
+ runUsage: async () => { await runAiUsage(['--lang', lang, '--all-repos'], { ask, bypassThrottle: true, inRegistration: true }); },
243
+ // `plain`: a certification shows Alma's messages only. "Pregunta N" and "Gracias. Sigamos:"
244
+ // read wrong once she is playing a character in a role play or a case.
245
+ interviewLoop: async ({ open, turn, plain = false }) => {
246
+ const c = getCatalog(lang).onboarding;
247
+ const animate = stdinIsTTY && !process.env.NO_ANIMATION;
248
+ const outStream = { write: (s) => { out(s); return true; }, isTTY: animate };
249
+ const think = (task) => (animate ? withSpinner(c.thinking, task) : task());
250
+ let turnNum = 1;
251
+ const showQuestion = async (text, { withHeader = true } = {}) => {
252
+ if (withHeader && !plain) out(`\n ${C.dim}${c.questionHeading(turnNum)}${C.reset}\n`);
253
+ out(`\n ${C.primary}${C.bold}`);
254
+ await typeOut(String(text == null ? '' : text), { stream: outStream });
255
+ out(`${C.reset}\n`);
256
+ };
257
+ const started = await think(open);
258
+ if (!started.ok) { write(started.reason || c.interviewTurnError); return { ok: false, reason: started.reason }; }
259
+ out(`\n ${C.dim}${c.interviewIntro}${C.reset}\n`);
260
+ await showQuestion(started.greeting);
261
+ let ended = started.ended === true;
262
+ while (!ended) {
263
+ const message = (await ask(` ${C.primary}>${C.reset} `)).trim();
264
+ const res = await think(() => turn(message));
265
+ if (!res.ok) { write(res.reason || c.interviewTurnError); return { ok: false, reason: res.reason }; }
266
+ ended = res.ended === true;
267
+ if (res.response) {
268
+ if (ended) { out(`\n ${C.primary}`); await typeOut(String(res.response), { stream: outStream }); out(`${C.reset}\n`); }
269
+ else { turnNum += 1; if (!plain) out(`\n ${C.dim}${c.interviewTransition}${C.reset}\n`); await showQuestion(res.response); }
270
+ }
271
+ }
272
+ return { ok: true };
273
+ },
274
+ };
275
+ }
276
+
277
+ async function run(
278
+ argv = process.argv.slice(2),
279
+ { ask: injectedAsk = null, stdinIsTTY: ttyOverride = undefined, deps = null, out = null, openBrowser = undefined, input = undefined, output = undefined } = {},
280
+ ) {
281
+ const o = parseArgs(argv);
282
+ const lang = o.lang || detectFlowLang();
283
+ const c = COPY[lang] || COPY.en;
284
+ const write = out || ((s) => process.stdout.write(s));
285
+
286
+ if (o.help) {
287
+ write(`\n ${c.help}\n\n`);
288
+ return;
289
+ }
290
+
291
+ const stdinIsTTY = ttyOverride !== undefined ? ttyOverride : !!process.stdin.isTTY;
292
+ if (!stdinIsTTY && !o.acceptDisclaimer && !injectedAsk) {
293
+ process.stderr.write(`\n ${red(c.missingAccept)}\n\n`);
294
+ process.exitCode = 1;
295
+ return;
296
+ }
297
+
298
+ const ask = injectedAsk || createStdinAsk();
299
+ try {
300
+ // Method choice (Email + Google) from the shared auth-methods registry; a
301
+ // valid method flag skips the picker.
302
+ const pickerCatalog = { ...getCatalog(lang).login, chooseMethodHeading: getCatalog(lang).onboarding.chooseMethodHeading };
303
+ const wanted = o.methodFlag && methodIdsForFlow('register').includes(o.methodFlag) ? o.methodFlag : null;
304
+ const method = wanted || (await chooseAuthMethod({ flow: 'register', ask, stdinIsTTY, catalog: pickerCatalog, input, output }));
305
+ if (method === null) return { ok: false, reason: 'cancelled', step: 'method' };
306
+
307
+ const io = makeIo({ ask, lang, out: write, acceptDisclaimer: o.acceptDisclaimer, stdinIsTTY });
308
+ // At a terminal the profile opens in the browser at the end; piped runs, tests and --no-open only print it.
309
+ const openProfile = stdinIsTTY && !o.noOpen ? (openBrowser || openPath) : undefined;
310
+ const result = await runOnboarding(io, deps || makeDeps(), { method, openBrowser, onAuthUrl: io.onGoogleAuthUrl, openProfile });
311
+ if (!result.ok) {
312
+ process.stderr.write(`\n ${red(`register: ${result.reason}${result.step ? ` (${result.step})` : ''}`)}\n\n`);
313
+ process.exitCode = 1;
314
+ }
315
+ return result;
316
+ } finally {
317
+ if (!injectedAsk) ask.close();
318
+ }
319
+ }
320
+
321
+ module.exports = { run, parseArgs, makeIo, isYes };
322
+
323
+ if (require.main === module) {
324
+ run();
325
+ }
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // `report` — materializes and OPENS the shareable, cumulative HTML report for the current project (ADR-016).
5
+
6
+ const { detectReportLang, getCatalog } = require('../src/i18n');
7
+ const { materializeProjectReport } = require('../src/report-store');
8
+ const { openPath } = require('../src/open-file');
9
+ const { oscLink } = require('../src/osc-link');
10
+ const { loadAuthSession, sessionStatus } = require('../src/auth-session-store');
11
+ const { getUsageReportEndpoint } = require('../src/config');
12
+ const { requestUsageReport } = require('../src/report-view-client');
13
+ const { renderUsageReportText } = require('../src/render-usage-report-text');
14
+
15
+ const VALID_LANGS = new Set(['es', 'en']);
16
+
17
+ const STORED_COPY = {
18
+ es: {
19
+ noSession: '"report --stored" requiere una sesion activa. Ejecuta primero: shakers login',
20
+ noEndpoint: 'No hay endpoint de certs configurado para leer el informe almacenado.',
21
+ noReport: 'Aún no hay ningún informe almacenado: sale de tu evaluación de AI-usage. Ejecuta `shakers ai-usage` (y comparte el informe cuando te lo pida) y vuelve a ejecutar esto.',
22
+ error: 'No se pudo leer el informe almacenado (motivo: %r).',
23
+ },
24
+ en: {
25
+ noSession: '"report --stored" requires an active session. Run `shakers login` first.',
26
+ noEndpoint: 'No certs endpoint configured to read the stored report.',
27
+ noReport: "There's no stored report yet — it comes from your AI-usage evaluation. Run `shakers ai-usage` (and share the report when prompted), then run this again.",
28
+ error: 'Could not read the stored report (reason: %r).',
29
+ },
30
+ };
31
+
32
+ // Minimal, report-specific arg parsing: --root <dir>, --lang es|en, --no-open, --help.
33
+ function parseReportArgs(argv) {
34
+ const opts = { root: null, lang: null, open: true, help: false, stored: false };
35
+ for (let i = 0; i < argv.length; i++) {
36
+ const a = argv[i];
37
+ if (a === '--root') opts.root = argv[++i];
38
+ else if (a.startsWith('--root=')) opts.root = a.slice('--root='.length);
39
+ else if (a === '--lang') opts.lang = VALID_LANGS.has(argv[++i]) ? argv[i] : null;
40
+ else if (a.startsWith('--lang=')) {
41
+ const value = a.slice('--lang='.length);
42
+ opts.lang = VALID_LANGS.has(value) ? value : null;
43
+ } else if (a === '--no-open') opts.open = false;
44
+ else if (a === '--stored' || a === '--text') opts.stored = true;
45
+ else if (a === '--help' || a === '-h') opts.help = true;
46
+ }
47
+ return opts;
48
+ }
49
+
50
+ async function runStored(lang, { session = null, loadSession = loadAuthSession, endpoint, fetchReport = requestUsageReport, out } = {}) {
51
+ const c = STORED_COPY[lang] || STORED_COPY.en;
52
+ const write = out || ((s) => process.stdout.write(s));
53
+
54
+ const active = session || loadSession();
55
+ if (sessionStatus(active) !== 'active') {
56
+ process.stderr.write(`\n ${c.noSession}\n\n`);
57
+ process.exitCode = 1;
58
+ return { ok: false, reason: 'no-session' };
59
+ }
60
+
61
+ const url = endpoint || getUsageReportEndpoint();
62
+ if (!url) {
63
+ process.stderr.write(`\n ${c.noEndpoint}\n\n`);
64
+ process.exitCode = 1;
65
+ return { ok: false, reason: 'no-endpoint' };
66
+ }
67
+
68
+ const res = await fetchReport({ accessToken: active.accessToken }, { endpoint: url });
69
+ if (!res.ok) {
70
+ process.stderr.write(`\n ${c.error.replace('%r', res.reason || 'unknown')}\n\n`);
71
+ process.exitCode = 1;
72
+ return res;
73
+ }
74
+ if (!res.report) {
75
+ write(`\n ${c.noReport}\n\n`);
76
+ return { ok: true, hasReport: false };
77
+ }
78
+ write(renderUsageReportText(res, { lang }));
79
+ return { ok: true, hasReport: true };
80
+ }
81
+
82
+ // `ask` is accepted for signature parity with the other REPL commands; `report`
83
+ // is non-interactive so it isn't used.
84
+ async function run(argv = process.argv.slice(2), { ask } = {}) { // eslint-disable-line no-unused-vars
85
+ const opts = parseReportArgs(argv);
86
+ const lang = opts.lang || detectReportLang();
87
+ const catalog = getCatalog(lang);
88
+ const r = catalog.cli.report;
89
+
90
+ if (opts.help) {
91
+ process.stdout.write(`\n ${r.help}\n`);
92
+ process.stdout.write(
93
+ lang === 'es'
94
+ ? ` --stored Imprime en texto plano tu informe completo almacenado (uso + fluidez + interaccion), sin re-escanear.\n\n`
95
+ : ` --stored Print your full stored report as plain text (usage + fluency + interaction), without re-scanning.\n\n`,
96
+ );
97
+ return;
98
+ }
99
+
100
+ if (opts.stored) {
101
+ await runStored(lang);
102
+ return;
103
+ }
104
+
105
+ let result;
106
+ try {
107
+ result = materializeProjectReport({ root: opts.root, lang });
108
+ } catch {
109
+ // Never crash the shell over a failed report write.
110
+ process.stdout.write(`\n ${r.error}\n\n`);
111
+ return;
112
+ }
113
+
114
+ if (!result.hasData) {
115
+ process.stdout.write(`\n ${r.noData}\n\n`);
116
+ return;
117
+ }
118
+
119
+ const opened = opts.open ? openPath(result.htmlPath) : false;
120
+
121
+ // Clear "the report was UPDATED" notice (fires once per run — this is the sole
122
+ // report write point, ADR-016). OSC 8: clickable file:// link where supported.
123
+ const updatedLabel = lang === 'es' ? 'Report actualizado' : 'Report updated';
124
+ process.stdout.write(`\n ✓ ${updatedLabel} · ${oscLink(result.fileUrl)}\n`);
125
+ if (opened) process.stdout.write(` ${r.opening}\n`);
126
+ process.stdout.write('\n');
127
+ }
128
+
129
+ module.exports = { run, parseReportArgs, runStored };
130
+
131
+ // Only auto-run when executed directly (guarded so the REPL can require() it).
132
+ if (require.main === module) {
133
+ run();
134
+ }
package/bin/save.js ADDED
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // `save` / `unsave` — bookmark a Position (src/saved-positions-flow.js).
5
+
6
+ const { detectReportLang, getCatalog } = require('../src/i18n');
7
+ const { loadAuthSession, sessionStatus } = require('../src/auth-session-store');
8
+ const { createStdinAsk } = require('../src/stdin-ask');
9
+ const { makeIo } = require('./register');
10
+ const { runToggleSave, makeSavedPositionsDeps } = require('../src/saved-positions-flow');
11
+ const { makeRevealSink } = require('../src/reveal-output');
12
+
13
+ function parseSaveArgs(argv = []) {
14
+ const o = { lang: null, help: false, json: false, id: null };
15
+ for (let i = 0; i < argv.length; i++) {
16
+ const a = argv[i];
17
+ if (a === '--help' || a === '-h') o.help = true;
18
+ else if (a === '--lang') { const v = argv[++i]; o.lang = v === 'es' || v === 'en' ? v : null; }
19
+ else if (a.startsWith('--lang=')) { const v = a.slice(7); o.lang = v === 'es' || v === 'en' ? v : null; }
20
+ else if (a === '--json') o.json = true;
21
+ else if (a === '--id') o.id = argv[++i] != null ? argv[i] : null;
22
+ else if (a.startsWith('--id=')) o.id = a.slice('--id='.length);
23
+ else if (!a.startsWith('-') && o.id == null) o.id = a; // positional <id>
24
+ }
25
+ return o;
26
+ }
27
+
28
+ function makeRun(mode) {
29
+ return async function run(
30
+ argv = process.argv.slice(2),
31
+ { ask: injectedAsk = null, stdinIsTTY: ttyOverride = undefined, deps = null, session: injectedSession = null, out = null } = {},
32
+ ) {
33
+ const opts = parseSaveArgs(argv);
34
+ const lang = opts.lang || detectReportLang();
35
+ const catalog = getCatalog(lang);
36
+ const fp = catalog.savedPositions;
37
+ const write = out || ((s) => process.stdout.write(s));
38
+
39
+ if (opts.help) {
40
+ write((mode === 'save' ? fp.helpSave : fp.helpUnsave) + '\n');
41
+ return;
42
+ }
43
+
44
+ const session = injectedSession || loadAuthSession();
45
+ if (sessionStatus(session) !== 'active') {
46
+ process.stderr.write(`\n ${fp.loginRequired}\n\n`);
47
+ process.exitCode = 1;
48
+ return;
49
+ }
50
+
51
+ const stdinIsTTY = ttyOverride !== undefined ? ttyOverride : !!process.stdin.isTTY;
52
+ const ask = injectedAsk || createStdinAsk();
53
+ const sink = makeRevealSink({ json: opts.json, out, env: process.env });
54
+ try {
55
+ const io = makeIo({ ask, lang, out: sink.write, stdinIsTTY });
56
+ const result = await runToggleSave({
57
+ io, rawOut: sink.write, session, catalog, mode,
58
+ opts: { id: opts.id, json: opts.json },
59
+ deps: deps || makeSavedPositionsDeps(),
60
+ });
61
+ await sink.finish();
62
+ if (result && result.ok === false) process.exitCode = 1;
63
+ return result;
64
+ } finally {
65
+ if (!injectedAsk) ask.close();
66
+ }
67
+ };
68
+ }
69
+
70
+ const run = makeRun('save');
71
+ const runUnsave = makeRun('unsave');
72
+
73
+ module.exports = { run, runUnsave, parseSaveArgs };
74
+
75
+ if (require.main === module) {
76
+ run();
77
+ }