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 +1 -1
- package/bin/login.js +44 -111
- package/bin/shakers.js +25 -6
- package/bin/update-check-worker.js +70 -0
- package/bin/update.js +111 -0
- package/package.json +1 -1
- package/src/agents-client.js +1 -1
- package/src/ai-profile-client.js +1 -1
- package/src/applications-client.js +1 -1
- package/src/availability-client.js +1 -1
- package/src/certifications-client.js +1 -1
- package/src/certify-dimension-client.js +0 -79
- package/src/certify-dimension-flow.js +24 -105
- package/src/command-audit.js +105 -0
- package/src/config.js +31 -22
- package/src/find-projects-client.js +1 -1
- package/src/i18n.js +92 -78
- package/src/lang-client.js +1 -1
- package/src/mcp-device-login-tools.js +3 -3
- package/src/mcp-install.js +2 -1
- package/src/mcp-instructions.js +2 -2
- package/src/mcp-onboarding-tools.js +10 -16
- package/src/mcp-role-tools.js +13 -5
- package/src/mcp-signup-client.js +1 -1
- package/src/mcp-signup-tools.js +76 -18
- package/src/profile-client.js +1 -4
- package/src/rate-client.js +1 -2
- package/src/roles-client.js +0 -4
- package/src/saved-positions-client.js +1 -1
- package/src/show-project-client.js +1 -1
- package/src/signup-copy.js +34 -12
- package/src/update-flow.js +223 -0
- package/src/update-notifier.js +113 -0
package/bin/certify.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
// `certify` — dimension
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
85
|
+
} else {
|
|
86
|
+
method = 'email'; // non-interactive default (matches the picker's own non-TTY default)
|
|
99
87
|
}
|
|
100
88
|
}
|
|
101
89
|
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
171
|
-
if (result.expiresAt)
|
|
172
|
-
|
|
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
|
-
// `
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
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
|
-
//
|
|
142
|
+
// An ACTIVE session is not silently overwritten.
|
|
215
143
|
const status = sessionStatus(loadAuthSession());
|
|
216
144
|
if (status === 'active') {
|
|
217
|
-
|
|
145
|
+
output.write(`\n ${c.alreadyLoggedIn}\n\n`);
|
|
218
146
|
return;
|
|
219
147
|
}
|
|
220
|
-
|
|
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
|
-
|
|
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
|
-
|
|
165
|
+
handleDeviceResult(result, c, output);
|
|
234
166
|
}
|
|
235
167
|
|
|
236
|
-
function
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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": {
|
package/src/agents-client.js
CHANGED
|
@@ -95,7 +95,7 @@ async function requestDeclareAgent(
|
|
|
95
95
|
return { ok: true, agentId };
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
//
|
|
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 } = {},
|
package/src/ai-profile-client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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');
|