shakers-cli-beta 0.10.4 → 0.12.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.
package/bin/certify.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- // `certify` — dimension certification interview over LiveKit (src/certify-dimension-flow.js).
4
+ // `certify` — discover + pick a dimension, then hand over the web link (src/certify-dimension-flow.js).
5
5
 
6
6
  const { detectReportLang, getCatalog } = require('../src/i18n');
7
7
  const { loadAuthSession, sessionStatus } = require('../src/auth-session-store');
package/bin/login.js CHANGED
@@ -4,8 +4,7 @@
4
4
  // `login` / `logout` — a registered Talent identifies themselves to the CLI (talents-ai-score, issue 122 / ADR-044).
5
5
 
6
6
  const { detectReportLang, getCatalog } = require('../src/i18n');
7
- const { getLoginEndpoint, getAuthTokenEndpoint, getDeviceAuthorizeEndpoint, getDeviceTokenEndpoint, getCompleteRegistrationEndpoint } = require('../src/config');
8
- const { requestLogin } = require('../src/auth-client');
7
+ const { getAuthTokenEndpoint, getDeviceAuthorizeEndpoint, getDeviceTokenEndpoint, getCompleteRegistrationEndpoint } = require('../src/config');
9
8
  const { runDeviceLogin, appendProviderParam } = require('../src/device-login');
10
9
  const { buildRegistrationContext } = require('../src/signup-client');
11
10
  const { openPath } = require('../src/open-file');
@@ -15,7 +14,7 @@ const {
15
14
  loadAuthSession,
16
15
  sessionStatus,
17
16
  } = require('../src/auth-session-store');
18
- const { isValidEmail, normalizeEmail, loadConsentState, clearConsentState } = require('../src/share');
17
+ const { normalizeEmail, loadConsentState, clearConsentState } = require('../src/share');
19
18
  const { createStdinAsk } = require('../src/stdin-ask');
20
19
  const { chooseAuthMethod } = require('../src/auth-method-picker');
21
20
 
@@ -50,23 +49,6 @@ async function chooseLoginMethod({ ask, stdinIsTTY, c, input, output }) {
50
49
  return chooseAuthMethod({ flow: 'login', ask, stdinIsTTY, catalog: c, input, output });
51
50
  }
52
51
 
53
- // The SECRET prompt (issue 102) — falls back to the ordinary line read on a pipe, which `ask()` still services.
54
- async function promptSecret(ask, label) {
55
- const read = typeof ask.secret === 'function' ? ask.secret.bind(ask) : ask;
56
- return String(await read(label)).trim();
57
- }
58
-
59
- async function resolveEmail(opts, c, ask) {
60
- if (opts.email) return String(opts.email).trim();
61
- for (let attempt = 0; attempt < 5; attempt++) {
62
- const raw = String(await ask(c.emailPrompt)).trim();
63
- if (raw === '') return null; // EOF / empty line / cancel
64
- if (isValidEmail(raw)) return raw;
65
- process.stdout.write(` ${c.emailInvalid}\n`);
66
- }
67
- return null;
68
- }
69
-
70
52
  async function run(
71
53
  argv = process.argv.slice(2),
72
54
  { ask: injectedAsk = null, openBrowser = undefined, stdinIsTTY: ttyOverride = undefined, input = undefined, output = undefined, sleep = undefined } = {},
@@ -75,75 +57,42 @@ async function run(
75
57
  const lang = opts.lang || detectReportLang();
76
58
  const c = getCatalog(lang).login;
77
59
 
78
- if (opts.google) return runSocial('google', c, { openBrowser, sleep, lang });
79
- if (opts.linkedin) return runSocial('linkedin', c, { openBrowser, sleep, lang });
60
+ // Every method signs in through the real web device flow (RFC 8628): the
61
+ // Talent reuses an existing web session or signs in on the `/cli-login` page,
62
+ // so no password is ever read in the terminal. Email now travels the same
63
+ // path Google/LinkedIn already use; only the `provider` query differs.
64
+ let method = null;
65
+ if (opts.google) method = 'google';
66
+ else if (opts.linkedin) method = 'linkedin';
67
+ else if (opts.provider === 'email' || !!opts.email) method = 'email';
80
68
 
81
- // No method flag: offer the picker (ADR-041/046 — Email, Google and LinkedIn; LinkedIn pending Hub OAuth provisioning).
82
- if (!hasMethodFlag(opts)) {
69
+ if (!method) {
83
70
  const stdinIsTTY = ttyOverride !== undefined ? ttyOverride : !!process.stdin.isTTY;
71
+ // ADR-044: an ACTIVE session is not silently overwritten, and the picker never shows.
72
+ if (sessionStatus(loadAuthSession()) === 'active') {
73
+ (output || process.stdout).write(`\n ${c.alreadyLoggedIn}\n\n`);
74
+ return;
75
+ }
84
76
  if (stdinIsTTY) {
85
- if (sessionStatus(loadAuthSession()) === 'active') {
86
- process.stdout.write(`\n ${c.alreadyLoggedIn}\n\n`);
87
- return;
88
- }
77
+ // ADR-041/046 — the picker offers Email, Google and LinkedIn.
89
78
  const pickerAsk = injectedAsk || createStdinAsk();
90
- let method;
91
79
  try {
92
80
  method = await chooseLoginMethod({ ask: pickerAsk, stdinIsTTY, c, input, output });
93
81
  } finally {
94
82
  if (!injectedAsk) pickerAsk.close();
95
83
  }
96
84
  if (method === null) return; // esc/ctrl-c: cancelled, nothing sent
97
- if (method === 'google' || method === 'linkedin') return runSocial(method, c, { openBrowser, sleep, lang });
98
- // method === 'email' falls through to the unchanged flow below.
85
+ } else {
86
+ method = 'email'; // non-interactive default (matches the picker's own non-TTY default)
99
87
  }
100
88
  }
101
89
 
102
- const endpoint = getLoginEndpoint();
103
- if (!endpoint) {
104
- process.stderr.write(`\n ${c.errorNoEndpoint}\n\n`);
105
- process.exitCode = 1;
106
- return;
107
- }
108
-
109
- // ADR-044: an ACTIVE session is not silently overwritten — the Talent is told, and pointed at `logout` to switch accounts.
110
- const status = sessionStatus(loadAuthSession());
111
- if (status === 'active') {
112
- process.stdout.write(`\n ${c.alreadyLoggedIn}\n\n`);
113
- return;
114
- }
115
- process.stdout.write(`\n ${status === 'expired' ? c.sessionExpiredIntro : c.intro}\n`);
116
-
117
- const ask = injectedAsk || createStdinAsk();
118
- try {
119
- const email = await resolveEmail(opts, c, ask);
120
- if (!email || !isValidEmail(email)) {
121
- process.stderr.write(`\n ${c.needInput}\n\n`);
122
- process.exitCode = 1;
123
- return;
124
- }
125
- const password = await promptSecret(ask, c.passwordPrompt);
126
- if (!password) {
127
- process.stderr.write(`\n ${c.needInput}\n\n`);
128
- process.exitCode = 1;
129
- return;
130
- }
131
-
132
- const normalizedEmail = normalizeEmail(email);
133
- // better-auth email login: sign-in captures the session cookie, then a hub
134
- // JWT is minted from it (auth-client.js). `endpoint` is the sign-in URL.
135
- const result = await requestLogin(
136
- { email: normalizedEmail, password },
137
- { signInEndpoint: endpoint, tokenEndpoint: getAuthTokenEndpoint() },
138
- );
139
- handleResult(result, c, normalizedEmail);
140
- } finally {
141
- if (!injectedAsk) ask.close();
142
- }
90
+ return runDeviceFlow(method, c, { openBrowser, sleep, lang, output });
143
91
  }
144
92
 
145
93
  // Shared success path: persist the session (atomic 0600 store) and report.
146
- function persistAndReport(result, c, email = null) {
94
+ // `output` lets a caller (and tests) redirect the human-facing lines; errors stay on stderr.
95
+ function persistAndReport(result, c, email = null, output = process.stdout) {
147
96
  if (email) {
148
97
  const prior = loadConsentState();
149
98
  if (prior && prior.email && normalizeEmail(prior.email) !== normalizeEmail(email)) {
@@ -167,37 +116,16 @@ function persistAndReport(result, c, email = null) {
167
116
  process.exitCode = 1;
168
117
  return false;
169
118
  }
170
- process.stdout.write(`\n ${c.success}\n`);
171
- if (result.expiresAt) process.stdout.write(` ${c.expiresAt(result.expiresAt)}\n`);
172
- process.stdout.write('\n');
119
+ output.write(`\n ${c.success}\n`);
120
+ if (result.expiresAt) output.write(` ${c.expiresAt(result.expiresAt)}\n`);
121
+ output.write('\n');
173
122
  return true;
174
123
  }
175
124
 
176
- // `typedEmail` is what the CALLER typed/passed as `--email` for THIS attempt.
177
- function handleResult(result, c, typedEmail = null) {
178
- if (result.ok) return void persistAndReport(result, c, result.email || typedEmail);
179
-
180
- // One message per contract reason (ADR-044) — each names the cause (issue 109).
181
- const messageFor = {
182
- 'invalid-credentials': c.errorInvalidCredentials,
183
- 'no-email-password': c.errorNoEmailPassword,
184
- upstream: c.errorUpstream,
185
- // Creds were good but the hub JWT could not be minted from the cookie.
186
- 'token-fetch-failed': c.errorUpstream,
187
- timeout: c.errorUnreachable,
188
- 'network-error': c.errorUnreachable,
189
- 'invalid-url': c.errorConfig,
190
- 'no-endpoint': c.errorNoEndpoint,
191
- 'bad-response': c.errorGeneric,
192
- 'http-error': c.errorGeneric,
193
- 'local-error': c.errorGeneric,
194
- };
195
- process.stderr.write(`\n ${messageFor[result.reason] || c.errorGeneric}\n\n`);
196
- process.exitCode = 1;
197
- }
198
-
199
- // `login --google` / `--linkedin` — one device flow (RFC 8628); only the `provider` query on the URL differs.
200
- async function runSocial(provider, c, { openBrowser = openPath, sleep = undefined, lang = 'en' } = {}) {
125
+ // `login` for one method — one device flow (RFC 8628); only the `provider` query
126
+ // on the `/cli-login` URL differs (google | linkedin | email). Email reuses an
127
+ // existing web session or signs in on the page, with no password in the terminal.
128
+ async function runDeviceFlow(provider, c, { openBrowser = openPath, sleep = undefined, lang = 'en', output = process.stdout } = {}) {
201
129
  const authorizeEndpoint = getDeviceAuthorizeEndpoint();
202
130
  const tokenEndpoint = getDeviceTokenEndpoint();
203
131
  const authTokenEndpoint = getAuthTokenEndpoint();
@@ -206,18 +134,21 @@ async function runSocial(provider, c, { openBrowser = openPath, sleep = undefine
206
134
  process.exitCode = 1;
207
135
  return;
208
136
  }
209
- // A new social account also needs complete-registration before the JWT (freelanceType is a transient default).
137
+ // A new account also needs complete-registration before the JWT (freelanceType is a transient default).
210
138
  const completeRegistrationEndpoint = getCompleteRegistrationEndpoint();
211
139
  const registrationContext = buildRegistrationContext({ preferredLanguage: lang, newsletterConsent: false, freelanceType: 'POTENTIAL_FREELANCE' });
212
- const providerName = provider === 'linkedin' ? c.methodLinkedin : c.methodGoogle;
140
+ const providerName = provider === 'linkedin' ? c.methodLinkedin : provider === 'email' ? c.methodEmail : c.methodGoogle;
213
141
 
214
- // Same as email: an ACTIVE session is not silently overwritten.
142
+ // An ACTIVE session is not silently overwritten.
215
143
  const status = sessionStatus(loadAuthSession());
216
144
  if (status === 'active') {
217
- process.stdout.write(`\n ${c.alreadyLoggedIn}\n\n`);
145
+ output.write(`\n ${c.alreadyLoggedIn}\n\n`);
218
146
  return;
219
147
  }
220
- process.stdout.write(`\n ${status === 'expired' ? c.sessionExpiredIntro : c.deviceIntro(providerName)}\n`);
148
+ const intro = status === 'expired'
149
+ ? c.sessionExpiredIntro
150
+ : (provider === 'email' ? c.emailDeviceIntro : c.deviceIntro(providerName));
151
+ output.write(`\n ${intro}\n`);
221
152
 
222
153
  const result = await runDeviceLogin(
223
154
  { authorizeEndpoint, tokenEndpoint, authTokenEndpoint, completeRegistrationEndpoint, registrationContext },
@@ -225,21 +156,23 @@ async function runSocial(provider, c, { openBrowser = openPath, sleep = undefine
225
156
  ...(sleep ? { sleep } : {}),
226
157
  onPrompt: ({ verificationUriComplete, userCode }) => {
227
158
  const url = appendProviderParam(verificationUriComplete, provider);
228
- process.stdout.write(`\n ${c.deviceVisit}\n ${url}\n\n ${c.deviceCodeLabel(userCode)}\n\n ${c.deviceWaiting}\n`);
159
+ output.write(`\n ${c.deviceVisit}\n ${url}\n\n ${c.deviceCodeLabel(userCode)}\n\n ${c.deviceWaiting}\n`);
160
+ if (provider === 'email') output.write(` ${c.deviceApproveHint}\n`);
229
161
  try { openBrowser(url); } catch { /* the printed URL covers it */ }
230
162
  },
231
163
  },
232
164
  );
233
- handleSocialResult(result, c);
165
+ handleDeviceResult(result, c, output);
234
166
  }
235
167
 
236
- function handleSocialResult(result, c) {
168
+ function handleDeviceResult(result, c, output = process.stdout) {
237
169
  if (result.ok) {
238
170
  // Map the device-flow JWT to the shared persist shape (no cookie).
239
171
  return void persistAndReport(
240
172
  { accessToken: result.token, hubAccessToken: result.token, email: result.email || null },
241
173
  c,
242
174
  result.email || null,
175
+ output,
243
176
  );
244
177
  }
245
178
 
@@ -264,14 +197,14 @@ function handleSocialResult(result, c) {
264
197
  }
265
198
 
266
199
  // `logout` — local only (no endpoint, no network): forget the stored session.
267
- async function runLogout(argv = process.argv.slice(2)) {
200
+ async function runLogout(argv = process.argv.slice(2), { output = process.stdout } = {}) {
268
201
  const opts = parseArgs(argv);
269
202
  const lang = opts.lang || detectReportLang();
270
203
  const c = getCatalog(lang).login;
271
204
  const had = loadAuthSession() !== null;
272
205
  clearAuthSession();
273
206
  clearConsentState();
274
- process.stdout.write(`\n ${had ? c.loggedOut : c.notLoggedIn}\n\n`);
207
+ output.write(`\n ${had ? c.loggedOut : c.notLoggedIn}\n\n`);
275
208
  }
276
209
 
277
210
  module.exports = { run, runLogout, chooseLoginMethod, parseArgs, hasMethodFlag };
package/bin/shakers.js CHANGED
@@ -46,7 +46,11 @@ const { run: runRegister } = require('./register');
46
46
  const { run: runOnboarding } = require('./onboarding');
47
47
  const { run: runConfig } = require('./config');
48
48
  const { run: runMcp } = require('./mcp');
49
+ const { run: runUpdate } = require('./update');
49
50
  const { checkOnboardingCompleted } = require('../src/onboarding-availability');
51
+ const { emitCommandAudit } = require('../src/command-audit');
52
+ const { readPackageInfo } = require('../src/update-flow');
53
+ const { isSuppressed, maybeNotify, triggerBackgroundCheck } = require('../src/update-notifier');
50
54
 
51
55
  let VERSION = '';
52
56
  try {
@@ -95,13 +99,14 @@ const HANDLERS = {
95
99
  register: runRegister,
96
100
  onboarding: runOnboarding,
97
101
  config: runConfig,
102
+ update: runUpdate,
98
103
  login: runLogin,
99
104
  logout: runLogout,
100
105
  superadmin: runSuperadmin,
101
106
  mcp: runMcp,
102
107
  };
103
108
 
104
- const SESSION_EXEMPT = new Set(['ai-usage', 'register', 'config', 'login', 'logout', 'superadmin', 'mcp', 'share', 'alma']);
109
+ const SESSION_EXEMPT = new Set(['ai-usage', 'register', 'config', 'login', 'logout', 'superadmin', 'mcp', 'share', 'alma', 'update']);
105
110
 
106
111
  // Command FAMILIES — a navigation layer over the flat commands (which all stay
107
112
  // valid as aliases). `shakers <family> <sub>` resolves to the flat handler; every
@@ -119,7 +124,7 @@ const FAMILIES = {
119
124
  invitations: 'invitations', applications: 'applications',
120
125
  },
121
126
  usage: { run: 'ai-usage', report: 'report', share: 'share' },
122
- account: { login: 'login', logout: 'logout', register: 'register', config: 'config' },
127
+ account: { login: 'login', logout: 'logout', register: 'register', config: 'config', update: 'update' },
123
128
  certs: { list: 'certifications', certify: 'certify' },
124
129
  };
125
130
 
@@ -212,12 +217,13 @@ function usageText(lang, { hideOnboarding = false } = {}) {
212
217
  + ` Sesión:\n`
213
218
  + ` login Inicia sesión en tu cuenta de Shakers\n`
214
219
  + ` logout Cierra la sesión\n`
215
- + ` config Lee/escribe la configuración (bases de hub/certs)\n\n`
220
+ + ` config Lee/escribe la configuración (bases de hub/certs)\n`
221
+ + ` update Actualiza el CLI a la última versión publicada (--check solo informa)\n\n`
216
222
  + ` Familias (agrupan los comandos de arriba; los nombres planos siguen valiendo como alias):\n`
217
223
  + ` add <skill|agent|project|role> · role <add|change>\n`
218
224
  + ` profile <show|rate|languages|socials|experiences|portfolios|availability|certifications>\n`
219
225
  + ` projects <find|show|save|unsave|invitations|applications>\n`
220
- + ` usage <run|report|share> · certs <list|certify> · account <login|logout|register|config>\n`
226
+ + ` usage <run|report|share> · certs <list|certify> · account <login|logout|register|config|update>\n`
221
227
  + ` Ejecuta \`shakers <familia>\` (sin subcomando) para ver sus subcomandos.\n\n`
222
228
  + ` Ejecuta shakers <comando> --help para las opciones de cada comando.\n\n`;
223
229
  }
@@ -255,12 +261,13 @@ function usageText(lang, { hideOnboarding = false } = {}) {
255
261
  + ` Session:\n`
256
262
  + ` login Sign in to your Shakers account\n`
257
263
  + ` logout Sign out\n`
258
- + ` config Read/write configuration (hub/certs bases)\n\n`
264
+ + ` config Read/write configuration (hub/certs bases)\n`
265
+ + ` update Update the CLI to the latest published version (--check only reports)\n\n`
259
266
  + ` Families (group the commands above; the flat names still work as aliases):\n`
260
267
  + ` add <skill|agent|project|role> · role <add|change>\n`
261
268
  + ` profile <show|rate|languages|socials|experiences|portfolios|availability|certifications>\n`
262
269
  + ` projects <find|show|save|unsave|invitations|applications>\n`
263
- + ` usage <run|report|share> · certs <list|certify> · account <login|logout|register|config>\n`
270
+ + ` usage <run|report|share> · certs <list|certify> · account <login|logout|register|config|update>\n`
264
271
  + ` Run \`shakers <family>\` (no subcommand) to list its subcommands.\n\n`
265
272
  + ` Run \`shakers <command> --help\` for a command's options.\n\n`;
266
273
  }
@@ -295,6 +302,18 @@ async function main() {
295
302
  try { await ensureFreshSession(process.env); } catch { /* never blocks a command */ }
296
303
 
297
304
  const token = argv.find((a) => !a.startsWith('-'));
305
+
306
+ // Single dispatch choke-point (ADR-060): audit EXACTLY once before branching, fire-and-forget (never awaited). MCP tool calls run in-process and never reach here.
307
+ const auditCommand =
308
+ !token || token.toLowerCase() === 'help' ? 'help' : token.toLowerCase();
309
+ emitCommandAudit({ command: auditCommand, argv, env: process.env });
310
+
311
+ // Non-blocking update notice: reads a cached version synchronously, registry check runs in a detached worker; stderr-only, suppressed for --json/non-TTY/CI/update/opt-out.
312
+ if (!isSuppressed({ argv, command: auditCommand, env: process.env })) {
313
+ maybeNotify({ currentVersion: VERSION, lang, env: process.env });
314
+ triggerBackgroundCheck({ name: readPackageInfo().name, env: process.env });
315
+ }
316
+
298
317
  if (!token || token.toLowerCase() === 'help') {
299
318
  let hideOnboarding = false;
300
319
  try {
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Detached worker: fetches the latest version from the PUBLIC, unauthenticated npm registry, writes the `{lastCheck, latest}` cache, fails silent.
5
+
6
+ const https = require('https');
7
+ const { writeCache } = require('../src/update-notifier');
8
+
9
+ const FETCH_TIMEOUT_MS = 10000;
10
+ const MAX_BODY_BYTES = 3 * 1024 * 1024;
11
+
12
+ // Scoped names (`@scope/pkg`) address the registry as `@scope%2fpkg`, `@` literal; install-v1 metadata still carries `dist-tags`.
13
+ function registryUrl(name) {
14
+ const p = name.startsWith('@') ? `@${name.slice(1).replace('/', '%2f')}` : name;
15
+ return `https://registry.npmjs.org/${p}`;
16
+ }
17
+
18
+ function fetchLatest(name, timeoutMs) {
19
+ return new Promise((resolve) => {
20
+ let settled = false;
21
+ const done = (v) => {
22
+ if (!settled) {
23
+ settled = true;
24
+ resolve(v);
25
+ }
26
+ };
27
+ let req;
28
+ try {
29
+ req = https.get(
30
+ registryUrl(name),
31
+ { headers: { Accept: 'application/vnd.npm.install-v1+json' } },
32
+ (res) => {
33
+ if (res.statusCode !== 200) {
34
+ res.resume();
35
+ return done(null);
36
+ }
37
+ let body = '';
38
+ res.setEncoding('utf8');
39
+ res.on('data', (c) => {
40
+ body += c;
41
+ if (body.length > MAX_BODY_BYTES) req.destroy();
42
+ });
43
+ res.on('end', () => {
44
+ try {
45
+ const json = JSON.parse(body);
46
+ const latest = json && json['dist-tags'] && json['dist-tags'].latest;
47
+ done(typeof latest === 'string' && latest ? latest : null);
48
+ } catch {
49
+ done(null);
50
+ }
51
+ });
52
+ },
53
+ );
54
+ } catch {
55
+ return done(null);
56
+ }
57
+ req.setTimeout(timeoutMs, () => req.destroy());
58
+ req.on('error', () => done(null));
59
+ });
60
+ }
61
+
62
+ async function main() {
63
+ const name = process.argv[2];
64
+ if (!name) return;
65
+ const latest = await fetchLatest(name, FETCH_TIMEOUT_MS);
66
+ if (!latest) return;
67
+ writeCache({ lastCheck: Date.now(), latest });
68
+ }
69
+
70
+ main();
package/bin/update.js ADDED
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // `update` — update the CLI to the latest published version (CLI-only, never an MCP tool); `--check` reports without installing. Logic in src/update-flow.js.
5
+
6
+ const { detectReportLang, getCatalog } = require('../src/i18n');
7
+ const updateFlow = require('../src/update-flow');
8
+
9
+ function parseUpdateArgs(argv = []) {
10
+ const o = { help: false, check: false, json: false, lang: null };
11
+ for (let i = 0; i < argv.length; i++) {
12
+ const a = argv[i];
13
+ if (a === '--help' || a === '-h') o.help = true;
14
+ else if (a === '--check' || a === '--dry-run') o.check = true;
15
+ else if (a === '--json') o.json = true;
16
+ else if (a === '--lang') {
17
+ const v = argv[++i];
18
+ o.lang = v === 'es' || v === 'en' ? v : null;
19
+ } else if (a.startsWith('--lang=')) {
20
+ const v = a.slice(7);
21
+ o.lang = v === 'es' || v === 'en' ? v : null;
22
+ }
23
+ }
24
+ return o;
25
+ }
26
+
27
+ async function run(
28
+ argv = process.argv.slice(2),
29
+ { out = null, env = process.env, deps = null } = {},
30
+ ) {
31
+ const opts = parseUpdateArgs(argv);
32
+ const lang = opts.lang || detectReportLang();
33
+ const t = getCatalog(lang).cli.update;
34
+ const write = out || ((s) => process.stdout.write(s));
35
+ const flow = deps || updateFlow;
36
+
37
+ if (opts.help) {
38
+ write(t.help + '\n');
39
+ return;
40
+ }
41
+
42
+ const info = flow.readPackageInfo();
43
+ const install = flow.detectInstall(env);
44
+ const current = info.version;
45
+
46
+ if (!opts.json) write(` ${t.checking}\n`);
47
+ const latest = flow.fetchLatestVersion(info);
48
+
49
+ if (opts.json) {
50
+ const updateAvailable =
51
+ latest.ok && current ? flow.compareVersions(current, latest.version) < 0 : null;
52
+ write(
53
+ `${JSON.stringify({
54
+ package: info.name,
55
+ current: current || null,
56
+ latest: latest.ok ? latest.version : null,
57
+ install: install.kind,
58
+ updateAvailable,
59
+ reason: latest.ok ? null : latest.reason,
60
+ })}\n`,
61
+ );
62
+ if (!latest.ok) process.exitCode = 1;
63
+ return;
64
+ }
65
+
66
+ if (current) write(` ${t.current(current)}\n`);
67
+
68
+ if (!latest.ok) {
69
+ write(` ${t.latestUnknown(latest.reason)}\n`);
70
+ process.exitCode = 1;
71
+ return;
72
+ }
73
+
74
+ if (!current || flow.compareVersions(current, latest.version) >= 0) {
75
+ write(` ${t.upToDate(latest.version)}\n`);
76
+ return;
77
+ }
78
+
79
+ write(` ${t.available(current, latest.version)}\n`);
80
+
81
+ // `--check`: report only, never install.
82
+ if (opts.check) {
83
+ write(` ${t.checkHint}\n`);
84
+ return;
85
+ }
86
+
87
+ const plan = flow.buildUpdatePlan(install, info, env);
88
+ if (!plan) {
89
+ if (install.kind === 'source') write(` ${t.sourceCheckout}\n`);
90
+ else write(` ${t.unknownInstall(flow.manualCommand(info))}\n`);
91
+ process.exitCode = 1;
92
+ return;
93
+ }
94
+
95
+ write(` ${t.detected(plan.label)}\n`);
96
+ write(` ${t.updating(info.name, current, latest.version)}\n\n`);
97
+
98
+ const result = flow.runInstall(plan, env);
99
+ if (result.ok) {
100
+ write(`\n ${t.success(latest.version)}\n ${t.reRun}\n`);
101
+ return;
102
+ }
103
+ write(`\n ${t.failInstall(result.reason)}\n ${t.failHint(flow.manualCommand(info))}\n`);
104
+ process.exitCode = 1;
105
+ }
106
+
107
+ module.exports = { run, parseUpdateArgs };
108
+
109
+ if (require.main === module) {
110
+ run();
111
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shakers-cli-beta",
3
- "version": "0.10.4",
3
+ "version": "0.12.0",
4
4
  "description": "Shakers AI usage & skills evaluator: a local-first CLI (`shakers` binary) plus a local MCP server. Commands: ai-usage (scan + classify your AI-tool setup, T0-T7), certify (certify Skills from your code), share (branded LinkedIn card).",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -95,7 +95,7 @@ async function requestDeclareAgent(
95
95
  return { ok: true, agentId };
96
96
  }
97
97
 
98
- // `PATCH {hub}/works/me/agents/:id/portfolios` — DIRECT to Hub (ADR-041), replace-set.
98
+ // DIRECT to Hub (ADR-041), replace-set.
99
99
  async function requestRelateAgentPortfolios(
100
100
  { agentId, items, hubAccessToken } = {},
101
101
  { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // GET hub `works/me/ai-profile` — the same read model the "My work with AI" front
3
+ // The same read model the "My work with AI" front
4
4
  // paints: { setup, usage, cell, isAiNative, traction, vision, howIWork }.
5
5
 
6
6
  const { postJsonWithTimeout } = require('./backend-request');
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // My-applications client (hub `works/candidatures/me`, talent Bearer). Lists the
3
+ // My-applications client. Lists the
4
4
  // positions the talent applied to, with the candidature status.
5
5
 
6
6
  const { postJsonWithTimeout } = require('./backend-request');
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  // Availability client. VIEW = GET `works/me/availability`; SET = PUT
4
- // `works/talents/me/work-details/availability` (the same endpoint register uses).
4
+ // (the same endpoint register uses).
5
5
 
6
6
  const { postJsonWithTimeout } = require('./backend-request');
7
7
  const { reasonForError } = require('./onboarding-client');
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // My-certifications client (hub `works/certifications/me`, talent Bearer). The
3
+ // My-certifications client. The
4
4
  // "By role" page: main-role cluster + its dimension rows (state/band) + ratios.
5
5
 
6
6
  const { postJsonWithTimeout } = require('./backend-request');