shakers-cli 0.0.0-stage → 0.14.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/LICENSE +59 -0
- package/README.md +1233 -2
- package/bin/add-agent.js +88 -0
- package/bin/add-project.js +88 -0
- package/bin/add-role.js +61 -0
- package/bin/add-skill.js +88 -0
- package/bin/ai-usage.js +99 -0
- package/bin/alma.js +99 -0
- package/bin/applications.js +69 -0
- package/bin/availability.js +70 -0
- package/bin/certifications.js +69 -0
- package/bin/certify.js +73 -0
- package/bin/change-role.js +61 -0
- package/bin/config.js +124 -0
- package/bin/experiences.js +69 -0
- package/bin/find-projects.js +89 -0
- package/bin/invitations.js +69 -0
- package/bin/lang.js +70 -0
- package/bin/login.js +216 -0
- package/bin/mcp.js +124 -0
- package/bin/mcpb.js +5 -0
- package/bin/me.js +69 -0
- package/bin/onboarding.js +134 -0
- package/bin/portfolios.js +69 -0
- package/bin/rate.js +70 -0
- package/bin/register.js +337 -0
- package/bin/report-html.js +134 -0
- package/bin/save.js +77 -0
- package/bin/shakers.js +363 -0
- package/bin/share.js +72 -0
- package/bin/show-project.js +72 -0
- package/bin/socials.js +70 -0
- package/bin/superadmin.js +351 -0
- package/bin/update-check-worker.js +70 -0
- package/bin/update.js +111 -0
- package/package.json +40 -4
- package/src/agent-category.js +13 -0
- package/src/agent-evaluation.js +177 -0
- package/src/agent-org-chart.js +355 -0
- package/src/agent-synthesis.js +132 -0
- package/src/agent-usage.js +118 -0
- package/src/agents-client.js +165 -0
- package/src/ai-fluency-cell.js +72 -0
- package/src/ai-profile-client.js +45 -0
- package/src/ai-profile-preview.js +154 -0
- package/src/ai.js +66 -0
- package/src/alma-client.js +123 -0
- package/src/alma-flow.js +32 -0
- package/src/alma-repo-context.js +47 -0
- package/src/alma-shell.js +161 -0
- package/src/ansi.js +62 -0
- package/src/applications-client.js +79 -0
- package/src/applications-flow.js +58 -0
- package/src/auth-client.js +144 -0
- package/src/auth-device-client.js +118 -0
- package/src/auth-method-picker.js +44 -0
- package/src/auth-methods.js +35 -0
- package/src/auth-session-store.js +215 -0
- package/src/authorship-mismatch.js +62 -0
- package/src/authorship.js +204 -0
- package/src/automations-detector.js +170 -0
- package/src/availability-client.js +93 -0
- package/src/availability-flow.js +124 -0
- package/src/backend-request.js +191 -0
- package/src/blind-approval-extract.js +48 -0
- package/src/brand-ansi.js +14 -0
- package/src/brand-svg.js +9 -0
- package/src/browser-tools-detector.js +21 -0
- package/src/build-next-level.js +56 -0
- package/src/certifications-client.js +89 -0
- package/src/certifications-flow.js +75 -0
- package/src/certify-dimension-client.js +163 -0
- package/src/certify-dimension-flow.js +135 -0
- package/src/certify-disclaimer.js +51 -0
- package/src/certify-remediation-prompt.js +23 -0
- package/src/cli-args.js +83 -0
- package/src/command-audit.js +105 -0
- package/src/config-dir.js +120 -0
- package/src/config.js +877 -0
- package/src/consent-flow.js +115 -0
- package/src/consent-skip.js +27 -0
- package/src/cv-file.js +62 -0
- package/src/cv-text.js +92 -0
- package/src/decisions-extract.js +43 -0
- package/src/detect-repos.js +145 -0
- package/src/detected-tools.js +15 -0
- package/src/detection-evidence.js +105 -0
- package/src/detection-noise.js +132 -0
- package/src/detectors.js +221 -0
- package/src/device-auth-store.js +100 -0
- package/src/device-login.js +120 -0
- package/src/email-verification.js +152 -0
- package/src/env-paths.js +30 -0
- package/src/find-projects-cache.js +89 -0
- package/src/find-projects-client.js +148 -0
- package/src/find-projects-flow.js +164 -0
- package/src/git-activity.js +124 -0
- package/src/git-log-prefetch.js +53 -0
- package/src/graph-certs.js +58 -0
- package/src/graph-generator.js +230 -0
- package/src/graph-scan.js +239 -0
- package/src/i18n.js +3341 -0
- package/src/inter-fonts.js +6 -0
- package/src/interactive-select.js +183 -0
- package/src/interview-client.js +226 -0
- package/src/interview-transcript.js +56 -0
- package/src/inventory-client.js +90 -0
- package/src/invitations-client.js +66 -0
- package/src/invitations-flow.js +50 -0
- package/src/lang-client.js +147 -0
- package/src/lang-codes.js +14 -0
- package/src/lang-flow.js +85 -0
- package/src/legal-notice.js +53 -0
- package/src/locale.js +72 -0
- package/src/markdown-ansi.js +50 -0
- package/src/maturity.js +111 -0
- package/src/mcp-ai-usage-result-tool.js +111 -0
- package/src/mcp-ai-usage-tool.js +401 -0
- package/src/mcp-alma-tools.js +95 -0
- package/src/mcp-applications-tools.js +44 -0
- package/src/mcp-auth-form.js +480 -0
- package/src/mcp-availability-tools.js +45 -0
- package/src/mcp-certifications-tools.js +44 -0
- package/src/mcp-certify-dimension-tools.js +66 -0
- package/src/mcp-choice.js +95 -0
- package/src/mcp-detector.js +223 -0
- package/src/mcp-device-login-tools.js +177 -0
- package/src/mcp-find-projects-tools.js +69 -0
- package/src/mcp-install.js +275 -0
- package/src/mcp-instructions.js +27 -0
- package/src/mcp-invitations-tools.js +44 -0
- package/src/mcp-lang-tools.js +41 -0
- package/src/mcp-onboarding-tools.js +379 -0
- package/src/mcp-portfolios-tools.js +50 -0
- package/src/mcp-profile-tools.js +48 -0
- package/src/mcp-prompts.js +73 -0
- package/src/mcp-rate-tools.js +44 -0
- package/src/mcp-register-tools.js +441 -0
- package/src/mcp-report-tools.js +105 -0
- package/src/mcp-role-tools.js +372 -0
- package/src/mcp-saved-positions-tools.js +59 -0
- package/src/mcp-server.js +290 -0
- package/src/mcp-show-project-tools.js +52 -0
- package/src/mcp-signup-client.js +118 -0
- package/src/mcp-signup-tools.js +1171 -0
- package/src/mcp-socials-tools.js +41 -0
- package/src/memory-structure-detector.js +108 -0
- package/src/model-call-record.js +198 -0
- package/src/onboarding-availability.js +26 -0
- package/src/onboarding-client.js +374 -0
- package/src/onboarding-flow.js +1162 -0
- package/src/onboarding-funnel.js +272 -0
- package/src/onboarding-livekit-client.js +102 -0
- package/src/onboarding-status-client.js +57 -0
- package/src/open-file.js +32 -0
- package/src/osc-link.js +16 -0
- package/src/portfolio-client.js +164 -0
- package/src/portfolio-git-info.js +61 -0
- package/src/portfolios-client.js +85 -0
- package/src/portfolios-flow.js +79 -0
- package/src/profile-client.js +133 -0
- package/src/profile-flow.js +115 -0
- package/src/profile-switch.js +46 -0
- package/src/prompt-select.js +66 -0
- package/src/rate-client.js +81 -0
- package/src/rate-flow.js +137 -0
- package/src/register-context.js +145 -0
- package/src/render-ai-fluency.js +266 -0
- package/src/render-certification.js +177 -0
- package/src/render-html.js +140 -0
- package/src/render-report-text.js +27 -0
- package/src/render-sheet.js +261 -0
- package/src/render-terminal.js +109 -0
- package/src/render-usage-report-text.js +155 -0
- package/src/repo-context.js +215 -0
- package/src/report-gating.js +49 -0
- package/src/report-store.js +226 -0
- package/src/report-theme.js +177 -0
- package/src/report-view-client.js +50 -0
- package/src/reveal-output.js +29 -0
- package/src/roadmap-content.js +453 -0
- package/src/roadmap-personalization.js +132 -0
- package/src/roadmap-prompt.js +154 -0
- package/src/roles-client.js +112 -0
- package/src/roles-flow.js +192 -0
- package/src/sanitize-network-text.js +36 -0
- package/src/saved-positions-client.js +71 -0
- package/src/saved-positions-flow.js +50 -0
- package/src/scan-exclusions.js +50 -0
- package/src/scanner.js +624 -0
- package/src/scope-signals.js +148 -0
- package/src/secret-prompt.js +72 -0
- package/src/session-gate.js +30 -0
- package/src/session-normalizers.js +174 -0
- package/src/session-refresh.js +51 -0
- package/src/session-scan.js +475 -0
- package/src/session-signals.js +36 -0
- package/src/shakers-wordmark.js +17 -0
- package/src/share-card.js +362 -0
- package/src/share-certification.js +111 -0
- package/src/share-consent-state.js +157 -0
- package/src/share-payload.js +300 -0
- package/src/share.js +185 -0
- package/src/show-project-client.js +58 -0
- package/src/show-project-flow.js +119 -0
- package/src/signup-client.js +103 -0
- package/src/signup-copy.js +450 -0
- package/src/signup-language.js +21 -0
- package/src/skills-client.js +102 -0
- package/src/socials-client.js +91 -0
- package/src/socials-flow.js +83 -0
- package/src/specificity-extract.js +41 -0
- package/src/start-add-agents.js +306 -0
- package/src/start-add-portfolio.js +388 -0
- package/src/start-add-skills.js +260 -0
- package/src/stdin-ask.js +129 -0
- package/src/steering-extract.js +43 -0
- package/src/strip-internal-comments.js +45 -0
- package/src/tech-detector.js +295 -0
- package/src/templates/report-sheet.html +572 -0
- package/src/terminal-agents.js +207 -0
- package/src/terminal-format.js +85 -0
- package/src/terminal-progress.js +106 -0
- package/src/terminal-sections.js +219 -0
- package/src/tier-analysis.js +126 -0
- package/src/tier-engine.js +114 -0
- package/src/tier-projection.js +117 -0
- package/src/transcript-turns.js +49 -0
- package/src/translations-client.js +84 -0
- package/src/typewriter.js +154 -0
- package/src/update-flow.js +223 -0
- package/src/update-notifier.js +113 -0
- package/src/usage-model-calls.js +119 -0
- package/src/usage-oneshot-commands.js +180 -0
- package/src/usage-repo-scope.js +162 -0
- package/src/usage-run-steps.js +354 -0
- package/src/work-streams.js +79 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFileSync } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
// `update` is explicit and user-invoked (never a hot path), so these timeouts can be generous.
|
|
9
|
+
const VIEW_TIMEOUT_MS = 20000;
|
|
10
|
+
const INSTALL_TIMEOUT_MS = 180000;
|
|
11
|
+
|
|
12
|
+
// Documented installer one-liner (README `curl | bash`, `release` branch); overridable for a non-default deployment.
|
|
13
|
+
const DEFAULT_INSTALL_URL =
|
|
14
|
+
'https://raw.githubusercontent.com/devShakers/ai-usage-evaluator/release/install.sh';
|
|
15
|
+
|
|
16
|
+
// Name/version/registry come from THIS package.json, so a renamed flavor adapts with no code change here.
|
|
17
|
+
function readPackageInfo() {
|
|
18
|
+
try {
|
|
19
|
+
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
|
20
|
+
const registry =
|
|
21
|
+
pkg.publishConfig && typeof pkg.publishConfig.registry === 'string'
|
|
22
|
+
? pkg.publishConfig.registry
|
|
23
|
+
: null;
|
|
24
|
+
return {
|
|
25
|
+
name: typeof pkg.name === 'string' ? pkg.name : null,
|
|
26
|
+
version: typeof pkg.version === 'string' ? pkg.version : null,
|
|
27
|
+
registry,
|
|
28
|
+
};
|
|
29
|
+
} catch {
|
|
30
|
+
return { name: null, version: null, registry: null };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function realPathOrSelf(p) {
|
|
35
|
+
try {
|
|
36
|
+
return fs.realpathSync(p);
|
|
37
|
+
} catch {
|
|
38
|
+
return p;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Root of the installed package = the parent of this src/ directory.
|
|
43
|
+
function packageRoot() {
|
|
44
|
+
return realPathOrSelf(path.resolve(__dirname, '..'));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function npmGlobalRoot() {
|
|
48
|
+
try {
|
|
49
|
+
const out = execFileSync('npm', ['root', '-g'], {
|
|
50
|
+
encoding: 'utf8',
|
|
51
|
+
timeout: VIEW_TIMEOUT_MS,
|
|
52
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
53
|
+
});
|
|
54
|
+
return out ? realPathOrSelf(out.trim()) : null;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Detects how this CLI was installed (prefix | npm-global | npm-local | source | unknown) so the update lands where the CLI lives.
|
|
61
|
+
function detectInstall(env = process.env) {
|
|
62
|
+
const root = packageRoot();
|
|
63
|
+
const nodeModulesSeg = `${path.sep}node_modules${path.sep}`;
|
|
64
|
+
|
|
65
|
+
if (!root.includes(nodeModulesSeg)) {
|
|
66
|
+
const home = realPathOrSelf(
|
|
67
|
+
env.SHAKERS_CLI_HOME ||
|
|
68
|
+
env.AI_FOOTPRINT_HOME ||
|
|
69
|
+
path.join(os.homedir(), '.shakers'),
|
|
70
|
+
);
|
|
71
|
+
if (root === home) return { kind: 'prefix', root };
|
|
72
|
+
if (fs.existsSync(path.join(root, '.git'))) return { kind: 'source', root };
|
|
73
|
+
return { kind: 'unknown', root };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const info = readPackageInfo();
|
|
77
|
+
const gRoot = npmGlobalRoot();
|
|
78
|
+
const globalPkgPath =
|
|
79
|
+
gRoot && info.name
|
|
80
|
+
? realPathOrSelf(path.join(gRoot, ...info.name.split('/')))
|
|
81
|
+
: null;
|
|
82
|
+
if (globalPkgPath && root === globalPkgPath) {
|
|
83
|
+
return { kind: 'npm-global', root, global: true, projectDir: null };
|
|
84
|
+
}
|
|
85
|
+
// `.../project/node_modules/<@scope/name>` → the project dir owning the local node_modules is `up` segments up.
|
|
86
|
+
const up = info.name && info.name.includes('/') ? 3 : 2;
|
|
87
|
+
const projectDir = path.resolve(root, ...Array(up).fill('..'));
|
|
88
|
+
return { kind: 'npm-local', root, global: false, projectDir };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function classifyError(error) {
|
|
92
|
+
const code = error && error.code;
|
|
93
|
+
const stderr =
|
|
94
|
+
(error && error.stderr && error.stderr.toString()) ||
|
|
95
|
+
(error && error.message) ||
|
|
96
|
+
'';
|
|
97
|
+
if (code === 'ENOENT') return 'npm-not-found';
|
|
98
|
+
if (code === 'ETIMEDOUT' || /ETIMEDOUT|timed out/i.test(stderr))
|
|
99
|
+
return 'timeout';
|
|
100
|
+
if (/EACCES|permission denied|EPERM/i.test(stderr)) return 'permission';
|
|
101
|
+
if (/ENOTFOUND|ECONNREFUSED|network|ECONNRESET|getaddrinfo/i.test(stderr))
|
|
102
|
+
return 'network';
|
|
103
|
+
if (/\b401\b|\b403\b|unauthorized|forbidden|authentication/i.test(stderr))
|
|
104
|
+
return 'auth';
|
|
105
|
+
if (/E404|not found/i.test(stderr)) return 'not-found';
|
|
106
|
+
return 'unknown';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// `npm view <name>@latest version` prints only the version string (last line).
|
|
110
|
+
function fetchLatestVersion({ name, registry } = {}) {
|
|
111
|
+
if (!name) return { ok: false, reason: 'no-package-name' };
|
|
112
|
+
const args = ['view', `${name}@latest`, 'version'];
|
|
113
|
+
if (registry) args.push('--registry', registry);
|
|
114
|
+
try {
|
|
115
|
+
const out = execFileSync('npm', args, {
|
|
116
|
+
encoding: 'utf8',
|
|
117
|
+
timeout: VIEW_TIMEOUT_MS,
|
|
118
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
119
|
+
});
|
|
120
|
+
const version = (out || '')
|
|
121
|
+
.trim()
|
|
122
|
+
.split('\n')
|
|
123
|
+
.map((l) => l.trim())
|
|
124
|
+
.filter(Boolean)
|
|
125
|
+
.pop();
|
|
126
|
+
return version ? { ok: true, version } : { ok: false, reason: 'empty-response' };
|
|
127
|
+
} catch (error) {
|
|
128
|
+
return { ok: false, reason: classifyError(error) };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseSemver(v) {
|
|
133
|
+
const core = String(v).trim().replace(/^v/, '').split('-')[0].split('+')[0];
|
|
134
|
+
return core.split('.').map((n) => parseInt(n, 10) || 0);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// -1/0/1 on the semver core; prerelease suffixes ignored (fine for an "is a newer release available" check).
|
|
138
|
+
function compareVersions(a, b) {
|
|
139
|
+
const pa = parseSemver(a);
|
|
140
|
+
const pb = parseSemver(b);
|
|
141
|
+
for (let i = 0; i < 3; i++) {
|
|
142
|
+
const x = pa[i] || 0;
|
|
143
|
+
const y = pb[i] || 0;
|
|
144
|
+
if (x !== y) return x < y ? -1 : 1;
|
|
145
|
+
}
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function manualCommand({ name, registry } = {}) {
|
|
150
|
+
if (!name) return 'npm install -g <package>@latest';
|
|
151
|
+
const reg = registry ? ` --registry ${registry}` : '';
|
|
152
|
+
return `npm install -g ${name}@latest${reg}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Resolves the exact command this install model needs to pull `@latest`.
|
|
156
|
+
function buildUpdatePlan(install, info, env = process.env) {
|
|
157
|
+
const { name, registry } = info;
|
|
158
|
+
if (!name) return null;
|
|
159
|
+
if (install.kind === 'npm-global') {
|
|
160
|
+
const args = ['install', '-g', `${name}@latest`];
|
|
161
|
+
if (registry) args.push('--registry', registry);
|
|
162
|
+
return { type: 'npm', cmd: 'npm', args, cwd: undefined, label: 'npm (global)' };
|
|
163
|
+
}
|
|
164
|
+
if (install.kind === 'npm-local') {
|
|
165
|
+
const args = ['install', `${name}@latest`];
|
|
166
|
+
if (registry) args.push('--registry', registry);
|
|
167
|
+
return {
|
|
168
|
+
type: 'npm',
|
|
169
|
+
cmd: 'npm',
|
|
170
|
+
args,
|
|
171
|
+
cwd: install.projectDir || undefined,
|
|
172
|
+
label: 'npm (local project)',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (install.kind === 'prefix') {
|
|
176
|
+
const url = env.SHAKERS_CLI_INSTALL_URL || DEFAULT_INSTALL_URL;
|
|
177
|
+
return {
|
|
178
|
+
type: 'installer',
|
|
179
|
+
url,
|
|
180
|
+
label: 'installer (~/.shakers)',
|
|
181
|
+
command: `curl -fsSL "${url}" | bash`,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Inherited stdio so the user sees npm's progress; the installer path keeps SHAKERS_CLI_HOME/BIN so a re-run lands in the same prefix.
|
|
188
|
+
function runInstall(plan, env = process.env) {
|
|
189
|
+
if (!plan) return { ok: false, reason: 'no-plan' };
|
|
190
|
+
try {
|
|
191
|
+
if (plan.type === 'npm') {
|
|
192
|
+
execFileSync(plan.cmd, plan.args, {
|
|
193
|
+
stdio: 'inherit',
|
|
194
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
195
|
+
cwd: plan.cwd,
|
|
196
|
+
env,
|
|
197
|
+
});
|
|
198
|
+
return { ok: true };
|
|
199
|
+
}
|
|
200
|
+
if (plan.type === 'installer') {
|
|
201
|
+
execFileSync('bash', ['-c', plan.command], {
|
|
202
|
+
stdio: 'inherit',
|
|
203
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
204
|
+
env,
|
|
205
|
+
});
|
|
206
|
+
return { ok: true };
|
|
207
|
+
}
|
|
208
|
+
return { ok: false, reason: 'no-plan' };
|
|
209
|
+
} catch (error) {
|
|
210
|
+
return { ok: false, reason: classifyError(error) };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = {
|
|
215
|
+
readPackageInfo,
|
|
216
|
+
detectInstall,
|
|
217
|
+
fetchLatestVersion,
|
|
218
|
+
compareVersions,
|
|
219
|
+
buildUpdatePlan,
|
|
220
|
+
runInstall,
|
|
221
|
+
manualCommand,
|
|
222
|
+
DEFAULT_INSTALL_URL,
|
|
223
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const { spawn } = require('child_process');
|
|
7
|
+
const { getConfigDir } = require('./config-dir');
|
|
8
|
+
const { getCatalog } = require('./i18n');
|
|
9
|
+
const { compareVersions } = require('./update-flow');
|
|
10
|
+
|
|
11
|
+
// Non-blocking update notifier: the hot path only reads the cache synchronously; a detached unref'd worker does the network check.
|
|
12
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
function cacheFilePath(env = process.env) {
|
|
15
|
+
return path.join(getConfigDir(env), 'update-check.json');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// `{ lastCheck, latest }` or null. Never throws — a missing/corrupt cache is "no data".
|
|
19
|
+
function readCache(env = process.env) {
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(fs.readFileSync(cacheFilePath(env), 'utf8'));
|
|
22
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
23
|
+
return parsed;
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Written by the background worker (and nowhere else on the hot path). Best-effort, atomic + 0600 like every other home-dir write (temp-in-same-dir + fsync + rename).
|
|
30
|
+
function writeCache(data, env = process.env) {
|
|
31
|
+
const target = cacheFilePath(env);
|
|
32
|
+
const dir = path.dirname(target);
|
|
33
|
+
const tmp = path.join(dir, `.update-check.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`);
|
|
34
|
+
let fd = null;
|
|
35
|
+
try {
|
|
36
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
37
|
+
const payload = JSON.stringify({ lastCheck: data.lastCheck, latest: data.latest });
|
|
38
|
+
fd = fs.openSync(tmp, 'wx', 0o600);
|
|
39
|
+
fs.writeFileSync(fd, payload);
|
|
40
|
+
try { fs.fsyncSync(fd); } catch { /* e.g. EINVAL on some filesystems */ }
|
|
41
|
+
fs.closeSync(fd);
|
|
42
|
+
fd = null;
|
|
43
|
+
fs.renameSync(tmp, target);
|
|
44
|
+
return true;
|
|
45
|
+
} catch {
|
|
46
|
+
if (fd !== null) { try { fs.closeSync(fd); } catch { /* already closed */ } }
|
|
47
|
+
try { fs.unlinkSync(tmp); } catch { /* never existed */ }
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// `stdoutIsTTY` is injectable for tests; defaults to the real tty bit so piped/redirected/mcp-stdio stdout stays quiet.
|
|
53
|
+
function isSuppressed({
|
|
54
|
+
argv = [],
|
|
55
|
+
command = null,
|
|
56
|
+
env = process.env,
|
|
57
|
+
stdoutIsTTY = process.stdout.isTTY,
|
|
58
|
+
} = {}) {
|
|
59
|
+
if (env.SHAKERS_CLI_NO_UPDATE_NOTIFIER === '1') return true;
|
|
60
|
+
if (env.CI && env.CI !== 'false' && env.CI !== '0') return true;
|
|
61
|
+
if (!stdoutIsTTY) return true;
|
|
62
|
+
if (Array.isArray(argv) && argv.includes('--json')) return true;
|
|
63
|
+
if (command === 'update') return true;
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Synchronous: prints to stderr when the cached latest beats the installed version, never polluting stdout. Returns the notice (or null).
|
|
68
|
+
function maybeNotify({ currentVersion, lang, env = process.env, out = null } = {}) {
|
|
69
|
+
if (!currentVersion) return null;
|
|
70
|
+
const cache = readCache(env);
|
|
71
|
+
if (!cache || typeof cache.latest !== 'string' || !cache.latest) return null;
|
|
72
|
+
if (compareVersions(currentVersion, cache.latest) >= 0) return null;
|
|
73
|
+
const t = getCatalog(lang).cli.updateNotifier;
|
|
74
|
+
const write = out || ((s) => process.stderr.write(s));
|
|
75
|
+
write(`\n ${t.available(currentVersion, cache.latest)}\n ${t.howTo}\n\n`);
|
|
76
|
+
return t.available(currentVersion, cache.latest);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Spawns the detached, unref'd worker to refresh the cache, but only when it is missing or older than CHECK_INTERVAL_MS. Failures swallowed.
|
|
80
|
+
function triggerBackgroundCheck({ name, env = process.env } = {}) {
|
|
81
|
+
if (!name) return false;
|
|
82
|
+
const cache = readCache(env);
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
if (
|
|
85
|
+
cache &&
|
|
86
|
+
typeof cache.lastCheck === 'number' &&
|
|
87
|
+
now - cache.lastCheck < CHECK_INTERVAL_MS
|
|
88
|
+
) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const worker = path.join(__dirname, '..', 'bin', 'update-check-worker.js');
|
|
93
|
+
const child = spawn(process.execPath, [worker, name], {
|
|
94
|
+
detached: true,
|
|
95
|
+
stdio: 'ignore',
|
|
96
|
+
});
|
|
97
|
+
child.on('error', () => {});
|
|
98
|
+
child.unref();
|
|
99
|
+
return true;
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = {
|
|
106
|
+
cacheFilePath,
|
|
107
|
+
readCache,
|
|
108
|
+
writeCache,
|
|
109
|
+
isSuppressed,
|
|
110
|
+
maybeNotify,
|
|
111
|
+
triggerBackgroundCheck,
|
|
112
|
+
CHECK_INTERVAL_MS,
|
|
113
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The three ephemeral model-call orchestrators of the `ai-usage` flow, extracted from bin/ai-usage.js (structure refactor, issue 020).
|
|
4
|
+
|
|
5
|
+
const { parseAgentDescriptions, parseAgentDefinitionsForRoots } = require('./agent-org-chart');
|
|
6
|
+
const {
|
|
7
|
+
buildSynthesisRequest,
|
|
8
|
+
requestAgentSynthesis,
|
|
9
|
+
buildAgentEvaluationRequest,
|
|
10
|
+
requestAgentEvaluation,
|
|
11
|
+
buildRoadmapPersonalizationRequest,
|
|
12
|
+
requestRoadmapPersonalization,
|
|
13
|
+
} = require('./ai');
|
|
14
|
+
const { MODEL_CALL, REASON, skipped, failedLocally, attachModelCall } = require('./model-call-record');
|
|
15
|
+
const {
|
|
16
|
+
getSynthesisEndpoint,
|
|
17
|
+
getRoadmapEndpoint,
|
|
18
|
+
getAgentEvaluationEndpoint,
|
|
19
|
+
} = require('./config');
|
|
20
|
+
const { hasTraceContentConsent } = require('./share');
|
|
21
|
+
const { getRoadmapEntry } = require('./roadmap-content');
|
|
22
|
+
const { computeTierResult } = require('./tier-engine');
|
|
23
|
+
|
|
24
|
+
// Agent-synthesis call (talents-ai-score, ADR-010/ADR-011, "ephemeral" — not persisted; ADR-051 amends WHEN it runs, not that).
|
|
25
|
+
async function maybeSynthesizeAgents(report, root) {
|
|
26
|
+
const KEY = MODEL_CALL.SYNTHESIS;
|
|
27
|
+
if (!Array.isArray(report.agents) || report.agents.length === 0) {
|
|
28
|
+
attachModelCall(report, KEY, skipped(REASON.NO_AGENTS));
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const endpoint = getSynthesisEndpoint();
|
|
32
|
+
if (!endpoint) {
|
|
33
|
+
attachModelCall(report, KEY, skipped(REASON.NO_ENDPOINT));
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const descriptions = Array.isArray(report.agentDescriptions)
|
|
39
|
+
? report.agentDescriptions
|
|
40
|
+
: parseAgentDescriptions(root);
|
|
41
|
+
const requestBody = buildSynthesisRequest(report.agents, descriptions, hasTraceContentConsent());
|
|
42
|
+
return await requestAgentSynthesis(requestBody, {
|
|
43
|
+
endpoint,
|
|
44
|
+
onOutcome: (rec) => attachModelCall(report, KEY, rec),
|
|
45
|
+
});
|
|
46
|
+
} catch {
|
|
47
|
+
// A failure BEFORE the wire (reading/parsing the agent files threw), which
|
|
48
|
+
// is a local defect and not the endpoint's fault — recorded as such.
|
|
49
|
+
attachModelCall(report, KEY, failedLocally());
|
|
50
|
+
return null; // never breaks the local report — falls back to the org chart
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Ephemeral agent-evaluation call (ADR-016): scores each agent's DEFINITION quality server-side (gemini-2.5-flash — see src/agent-evaluation.js).
|
|
55
|
+
async function maybeEvaluateAgents(report, roots, lang) {
|
|
56
|
+
const KEY = MODEL_CALL.EVALUATION;
|
|
57
|
+
if (!Array.isArray(report.agents) || report.agents.length === 0) {
|
|
58
|
+
attachModelCall(report, KEY, skipped(REASON.NO_AGENTS));
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const endpoint = getAgentEvaluationEndpoint();
|
|
62
|
+
if (!endpoint) {
|
|
63
|
+
attachModelCall(report, KEY, skipped(REASON.NO_ENDPOINT));
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const definitions = parseAgentDefinitionsForRoots(roots);
|
|
69
|
+
// ADR-026: pass the report language so the rationale + the one-line description come back translated (a Spanish-authored definition still yields report-language prose).
|
|
70
|
+
const requestBody = buildAgentEvaluationRequest(report.agents, definitions, lang, hasTraceContentConsent());
|
|
71
|
+
return await requestAgentEvaluation(requestBody, {
|
|
72
|
+
endpoint,
|
|
73
|
+
onOutcome: (rec) => attachModelCall(report, KEY, rec),
|
|
74
|
+
});
|
|
75
|
+
} catch {
|
|
76
|
+
attachModelCall(report, KEY, failedLocally());
|
|
77
|
+
return null; // never breaks the local report
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function maybePersonalizeRoadmap(report, maturity, lang) {
|
|
82
|
+
const KEY = MODEL_CALL.ROADMAP;
|
|
83
|
+
const tierKey = maturity && maturity.tierKey;
|
|
84
|
+
if (!tierKey) {
|
|
85
|
+
attachModelCall(report, KEY, skipped(REASON.NOT_APPLICABLE));
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const entry = getRoadmapEntry(tierKey, lang);
|
|
89
|
+
if (!entry) {
|
|
90
|
+
attachModelCall(report, KEY, skipped(REASON.NOT_APPLICABLE));
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
if (entry.maxTier) {
|
|
94
|
+
attachModelCall(report, KEY, skipped(REASON.MAX_TIER));
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const endpoint = getRoadmapEndpoint();
|
|
99
|
+
if (!endpoint) {
|
|
100
|
+
attachModelCall(report, KEY, skipped(REASON.NO_ENDPOINT));
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const tierResult = computeTierResult(report);
|
|
106
|
+
// ADR-026: pass the report language so the personalized prose is localized.
|
|
107
|
+
// ADR-028: fresh consent read, see maybeSynthesizeAgents above.
|
|
108
|
+
const requestBody = buildRoadmapPersonalizationRequest(entry, tierResult, report, lang, hasTraceContentConsent());
|
|
109
|
+
return await requestRoadmapPersonalization(requestBody, {
|
|
110
|
+
endpoint,
|
|
111
|
+
onOutcome: (rec) => attachModelCall(report, KEY, rec),
|
|
112
|
+
});
|
|
113
|
+
} catch {
|
|
114
|
+
attachModelCall(report, KEY, failedLocally());
|
|
115
|
+
return null; // never breaks the local report — falls back to the curated content
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { maybeSynthesizeAgents, maybeEvaluateAgents, maybePersonalizeRoadmap };
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// One-shot command handlers extracted from bin/ai-usage.js (structure refactor, issue 020).
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
getConsentStatus,
|
|
7
|
+
revokeConsent,
|
|
8
|
+
resetConsent,
|
|
9
|
+
setEmail,
|
|
10
|
+
} = require('./share');
|
|
11
|
+
const { createStdinAsk } = require('./stdin-ask');
|
|
12
|
+
const {
|
|
13
|
+
setIngestEndpoint,
|
|
14
|
+
resolveIngestEndpoint,
|
|
15
|
+
hasRetiredFallbackConfig,
|
|
16
|
+
isTalentProfile,
|
|
17
|
+
} = require('./config');
|
|
18
|
+
const { buildNextLevelStarter } = require('./build-next-level');
|
|
19
|
+
|
|
20
|
+
// One-shot consent management commands (issue 007). Mirror the retired
|
|
21
|
+
// `--enroll` pattern: they act immediately and do NOT scan.
|
|
22
|
+
|
|
23
|
+
function doConsentStatus(catalog) {
|
|
24
|
+
const status = getConsentStatus();
|
|
25
|
+
const s = catalog.consent.status;
|
|
26
|
+
process.stdout.write(`\n ${s.heading}\n\n`);
|
|
27
|
+
const decisionLine =
|
|
28
|
+
status.consent === 'granted' ? s.decisionGranted
|
|
29
|
+
: status.consent === 'denied' ? s.decisionDenied
|
|
30
|
+
: s.decisionNone;
|
|
31
|
+
process.stdout.write(` ${decisionLine}\n`);
|
|
32
|
+
process.stdout.write(` ${s.email(status.email)}\n`);
|
|
33
|
+
if (status.consent === 'granted' && status.email && !status.emailVerified) {
|
|
34
|
+
process.stdout.write(` ${isTalentProfile() ? s.verificationPending : s.emailUnverifiedExternal}\n`);
|
|
35
|
+
}
|
|
36
|
+
process.stdout.write(` ${s.lastSentAt(status.lastSentAt)}\n\n`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function doConsentRevoke(catalog) {
|
|
40
|
+
revokeConsent();
|
|
41
|
+
process.stdout.write(`\n ${catalog.consent.revoked}\n\n`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// skill-code-certification / ADR-003: clears the decision back to "no decision yet" so the consent question is asked again next run — distinct from revoke (which persists `denied`).
|
|
45
|
+
function doConsentReset(catalog) {
|
|
46
|
+
resetConsent();
|
|
47
|
+
process.stdout.write(`\n ${catalog.consent.reset}\n\n`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function doConsentEmail(newEmail, catalog) {
|
|
51
|
+
const r = setEmail(newEmail);
|
|
52
|
+
if (r.ok) {
|
|
53
|
+
process.stdout.write(`\n ${catalog.consent.emailChanged(r.state.email)}\n\n`);
|
|
54
|
+
} else {
|
|
55
|
+
process.stdout.write(`\n ${catalog.consent.emailInvalidCli}\n\n`);
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function confirmEndpointHost(host, ask, catalog) {
|
|
61
|
+
const e = catalog.endpoint;
|
|
62
|
+
const answer = await ask(e.confirmHostPrompt(host));
|
|
63
|
+
return String(answer || '').trim() === host;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Endpoint config (endpoint-config task): one-shot, don't scan.
|
|
67
|
+
async function doSetEndpoint(url, catalog, ask) {
|
|
68
|
+
const e = catalog.endpoint;
|
|
69
|
+
let r = setIngestEndpoint(url);
|
|
70
|
+
if (!r.ok && r.reason === 'needs-confirmation') {
|
|
71
|
+
const confirmed = await confirmEndpointHost(r.host, ask, catalog);
|
|
72
|
+
if (!confirmed) {
|
|
73
|
+
process.stdout.write(`\n ${e.confirmHostMismatch}\n\n`);
|
|
74
|
+
process.exitCode = 1;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
r = setIngestEndpoint(url, process.env, { confirmed: true });
|
|
78
|
+
}
|
|
79
|
+
if (r.ok) {
|
|
80
|
+
process.stdout.write(`\n ${e.setOk(r.value, r.path)}\n\n`);
|
|
81
|
+
} else {
|
|
82
|
+
const msg =
|
|
83
|
+
r.reason === 'insecure-remote' ? e.errInsecureRemote
|
|
84
|
+
: r.reason === 'invalid-url' || r.reason === 'bad-protocol' ? e.errInvalidUrl
|
|
85
|
+
: e.errEmpty;
|
|
86
|
+
process.stdout.write(`\n ${msg}\n\n`);
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Print the effective ingest endpoint(s) and where each resolved from (env var > config file > none), so a Talent can see what's configured without editing or exporting anything.
|
|
92
|
+
function doShowEndpoint(catalog) {
|
|
93
|
+
const e = catalog.endpoint;
|
|
94
|
+
const r = resolveIngestEndpoint();
|
|
95
|
+
if (r.source === 'env') {
|
|
96
|
+
process.stdout.write(`\n ${e.showEnv(r.endpoint)}\n\n`);
|
|
97
|
+
} else if (r.source === 'config-file') {
|
|
98
|
+
process.stdout.write(`\n ${e.showConfigFile(r.endpoint, r.path)}\n\n`);
|
|
99
|
+
} else if (r.source === 'config-file-invalid') {
|
|
100
|
+
process.stdout.write(`\n ${e.showConfigInvalid(r.path)}\n\n`);
|
|
101
|
+
} else {
|
|
102
|
+
process.stdout.write(`\n ${e.showNone}\n\n`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ADR-042: there is no second hop any more.
|
|
106
|
+
if (hasRetiredFallbackConfig()) {
|
|
107
|
+
process.stdout.write(` ${e.showFallbackRetired}\n\n`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// "Construir el siguiente nivel ahora" (issue 021): optional, explicit phase — never runs unless the talent asks for it via --build-next-level.
|
|
113
|
+
function doBuildNextLevel(root, maturity, force, catalog) {
|
|
114
|
+
const b = catalog.buildNextLevel;
|
|
115
|
+
const result = buildNextLevelStarter(root || process.cwd(), maturity.tierKey, { force });
|
|
116
|
+
|
|
117
|
+
if (!result.ok) {
|
|
118
|
+
const message =
|
|
119
|
+
result.reason === 'max-tier' ? b.maxTier
|
|
120
|
+
: result.reason === 'no-file-target' ? b.noFileTarget
|
|
121
|
+
: b.unrecognizedTier;
|
|
122
|
+
process.stdout.write(`\n ${message}\n\n`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
process.stdout.write(`\n ${b.heading(result.targetTierKey)}\n`);
|
|
127
|
+
for (const f of result.files) {
|
|
128
|
+
const line =
|
|
129
|
+
f.status === 'created' ? b.created(f.filename)
|
|
130
|
+
: f.status === 'overwritten' ? b.overwritten(f.filename)
|
|
131
|
+
: b.skippedExists(f.filename);
|
|
132
|
+
process.stdout.write(` ${line}\n`);
|
|
133
|
+
}
|
|
134
|
+
process.stdout.write('\n');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// The one-shot dispatch that used to sit at the top of `run()`: each command acts immediately and returns, without scanning.
|
|
138
|
+
async function handleOneShotCommand({ opts, catalog, injectedAsk }) {
|
|
139
|
+
if (opts.consentStatus) {
|
|
140
|
+
doConsentStatus(catalog);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
if (opts.consentRevoke) {
|
|
144
|
+
doConsentRevoke(catalog);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
if (opts.consentReset) {
|
|
148
|
+
doConsentReset(catalog);
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
if (opts.consentEmail) {
|
|
152
|
+
doConsentEmail(opts.consentEmail, catalog);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
if (opts.setEndpoint !== null) {
|
|
156
|
+
const ask = injectedAsk || createStdinAsk();
|
|
157
|
+
try {
|
|
158
|
+
await doSetEndpoint(opts.setEndpoint, catalog, ask);
|
|
159
|
+
} finally {
|
|
160
|
+
if (!injectedAsk) ask.close();
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
if (opts.showEndpoint) {
|
|
165
|
+
doShowEndpoint(catalog);
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = {
|
|
172
|
+
handleOneShotCommand,
|
|
173
|
+
doConsentStatus,
|
|
174
|
+
doConsentRevoke,
|
|
175
|
+
doConsentReset,
|
|
176
|
+
doConsentEmail,
|
|
177
|
+
doSetEndpoint,
|
|
178
|
+
doShowEndpoint,
|
|
179
|
+
doBuildNextLevel,
|
|
180
|
+
};
|