shakers-cli-beta 0.8.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +59 -0
- package/README.md +1175 -0
- 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 +81 -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 +120 -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 +283 -0
- package/bin/mcp.js +98 -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 +325 -0
- package/bin/report-html.js +134 -0
- package/bin/save.js +77 -0
- package/bin/shakers.js +344 -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/package.json +40 -0
- 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 +214 -0
- package/src/authorship.js +203 -0
- package/src/automations-detector.js +170 -0
- package/src/availability-client.js +92 -0
- package/src/availability-flow.js +105 -0
- package/src/backend-request.js +170 -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 +217 -0
- package/src/certify-dimension-flow.js +193 -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/config-dir.js +120 -0
- package/src/config.js +778 -0
- package/src/consent-flow.js +115 -0
- package/src/consent-skip.js +27 -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 +118 -0
- package/src/email-verification.js +152 -0
- package/src/env-paths.js +16 -0
- package/src/find-projects-cache.js +89 -0
- package/src/find-projects-client.js +138 -0
- package/src/find-projects-flow.js +164 -0
- package/src/git-activity.js +123 -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 +3264 -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 +51 -0
- package/src/markdown-ansi.js +50 -0
- package/src/maturity.js +111 -0
- package/src/mcp-ai-usage-result-tool.js +109 -0
- package/src/mcp-ai-usage-tool.js +333 -0
- package/src/mcp-alma-tools.js +91 -0
- package/src/mcp-applications-tools.js +42 -0
- package/src/mcp-ask-tools.js +50 -0
- package/src/mcp-auth-form.js +282 -0
- package/src/mcp-availability-tools.js +43 -0
- package/src/mcp-certifications-tools.js +42 -0
- package/src/mcp-certify-dimension-tools.js +58 -0
- package/src/mcp-detector.js +223 -0
- package/src/mcp-device-login-tools.js +173 -0
- package/src/mcp-find-projects-tools.js +67 -0
- package/src/mcp-install.js +245 -0
- package/src/mcp-invitations-tools.js +42 -0
- package/src/mcp-lang-tools.js +39 -0
- package/src/mcp-onboarding-tools.js +388 -0
- package/src/mcp-portfolios-tools.js +46 -0
- package/src/mcp-profile-tools.js +46 -0
- package/src/mcp-rate-tools.js +42 -0
- package/src/mcp-register-tools.js +517 -0
- package/src/mcp-report-tools.js +101 -0
- package/src/mcp-role-tools.js +112 -0
- package/src/mcp-saved-positions-tools.js +55 -0
- package/src/mcp-server.js +170 -0
- package/src/mcp-show-project-tools.js +50 -0
- package/src/mcp-socials-tools.js +39 -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 +364 -0
- package/src/onboarding-flow.js +1078 -0
- package/src/onboarding-livekit-client.js +102 -0
- package/src/onboarding-status-client.js +42 -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 +125 -0
- package/src/profile-flow.js +115 -0
- package/src/profile-switch.js +46 -0
- package/src/prompt-select.js +29 -0
- package/src/rate-client.js +82 -0
- package/src/rate-flow.js +137 -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 +114 -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 +612 -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-refresh.js +51 -0
- package/src/session-scan.js +336 -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 +184 -0
- package/src/show-project-client.js +58 -0
- package/src/show-project-flow.js +119 -0
- package/src/signup-client.js +91 -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 +199 -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/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 +338 -0
- package/src/work-streams.js +74 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The derived-payload whitelist for the `usage/reports` ingest, extracted from src/share.js (structure refactor, issue 020).
|
|
4
|
+
|
|
5
|
+
const { scrubSecrets } = require('./agent-synthesis');
|
|
6
|
+
const { MAX_DEFINITION_CHARS } = require('./agent-evaluation');
|
|
7
|
+
|
|
8
|
+
// Only these fields leave the machine.
|
|
9
|
+
function whitelistSchedulerProbe(probe) {
|
|
10
|
+
return {
|
|
11
|
+
inspected: !!(probe && probe.inspected),
|
|
12
|
+
matches: probe && typeof probe.matches === 'number' ? probe.matches : 0,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// WHY ONE MORE FIELD IS ADMISSIBLE ON THE AGENT WHITELIST (issue 115).
|
|
17
|
+
function agentCatalogIdsByName(report) {
|
|
18
|
+
const evaluations =
|
|
19
|
+
report.agentEvaluation && Array.isArray(report.agentEvaluation.evaluations)
|
|
20
|
+
? report.agentEvaluation.evaluations
|
|
21
|
+
: [];
|
|
22
|
+
|
|
23
|
+
const byName = new Map();
|
|
24
|
+
for (const e of evaluations) {
|
|
25
|
+
if (!e || typeof e.name !== 'string' || !e.name) continue;
|
|
26
|
+
if (byName.has(e.name)) continue; // first occurrence wins — see docblock
|
|
27
|
+
const id =
|
|
28
|
+
e.classification && typeof e.classification.catalogId === 'string' && e.classification.catalogId
|
|
29
|
+
? e.classification.catalogId
|
|
30
|
+
: null;
|
|
31
|
+
byName.set(e.name, id);
|
|
32
|
+
}
|
|
33
|
+
return byName;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// WHY THE AGENT `definition` NOW RIDES THE INGEST PAYLOAD (agent-classifier fix).
|
|
37
|
+
function agentDefinitionsByName(report) {
|
|
38
|
+
const definitions = Array.isArray(report.agentDefinitions)
|
|
39
|
+
? report.agentDefinitions
|
|
40
|
+
: [];
|
|
41
|
+
const byName = new Map();
|
|
42
|
+
for (const entry of definitions) {
|
|
43
|
+
if (!entry || typeof entry.name !== 'string' || !entry.name) continue;
|
|
44
|
+
if (byName.has(entry.name)) continue; // first occurrence wins — mirrors agentCatalogIdsByName
|
|
45
|
+
const text = typeof entry.definition === 'string' ? entry.definition : '';
|
|
46
|
+
byName.set(entry.name, scrubSecrets(text).slice(0, MAX_DEFINITION_CHARS));
|
|
47
|
+
}
|
|
48
|
+
return byName;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function derivePayload(report, maturity) {
|
|
52
|
+
const agents = Array.isArray(report.agents) ? report.agents : [];
|
|
53
|
+
const catalogIdsByName = agentCatalogIdsByName(report);
|
|
54
|
+
const definitionsByName = agentDefinitionsByName(report);
|
|
55
|
+
const technologies = Array.isArray(report.technologies) ? report.technologies : [];
|
|
56
|
+
const synthesisAgents =
|
|
57
|
+
report.agentSynthesis && Array.isArray(report.agentSynthesis.agents)
|
|
58
|
+
? report.agentSynthesis.agents
|
|
59
|
+
: [];
|
|
60
|
+
|
|
61
|
+
const mcp = report.mcp || {};
|
|
62
|
+
const memory = report.memory || {};
|
|
63
|
+
const automations = report.automations || {};
|
|
64
|
+
const automationsScripts = automations.scripts || {};
|
|
65
|
+
const automationsSchedulers = automations.schedulers || {};
|
|
66
|
+
const browserTools = report.browserTools || {};
|
|
67
|
+
const browserToolsVia = browserTools.via || {};
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
schemaVersion: report.schemaVersion,
|
|
71
|
+
generatedAt: report.generatedAt,
|
|
72
|
+
anonId: report.anonId,
|
|
73
|
+
platform: report.platform,
|
|
74
|
+
level: maturity.level,
|
|
75
|
+
levelName: maturity.name,
|
|
76
|
+
score: maturity.score,
|
|
77
|
+
totalDetected: report.summary.totalDetected,
|
|
78
|
+
categories: report.summary.categories,
|
|
79
|
+
tools: report.tools.map((t) => ({
|
|
80
|
+
id: t.id,
|
|
81
|
+
detected: t.detected,
|
|
82
|
+
depth: t.depth || {},
|
|
83
|
+
})),
|
|
84
|
+
agents: agents.map((a) => ({
|
|
85
|
+
name: a.name,
|
|
86
|
+
tools: Array.isArray(a.tools) ? a.tools : [],
|
|
87
|
+
model: a.model || null,
|
|
88
|
+
parent: a.parent || null,
|
|
89
|
+
// issue 115 — the ONLY classification field that travels.
|
|
90
|
+
catalogId: catalogIdsByName.get(a.name) || null,
|
|
91
|
+
// agent-classifier fix — the agent's own definition (frontmatter description + body), scrubbed + capped.
|
|
92
|
+
...(definitionsByName.get(a.name)
|
|
93
|
+
? { definition: definitionsByName.get(a.name) }
|
|
94
|
+
: {}),
|
|
95
|
+
})),
|
|
96
|
+
agentCounts: report.agentCounts || { agents: 0, skills: 0, commands: 0, mcpServers: 0, hooks: 0 },
|
|
97
|
+
technologies,
|
|
98
|
+
agentSynthesis: synthesisAgents.map((a) => ({
|
|
99
|
+
name: a.name,
|
|
100
|
+
symbolicName: scrubSecrets(a.symbolicName || '') || null,
|
|
101
|
+
whatItDoes: scrubSecrets(a.whatItDoes || '') || null,
|
|
102
|
+
})),
|
|
103
|
+
tier: typeof maturity.tier === 'number' ? maturity.tier : null,
|
|
104
|
+
tierKey: maturity.tierKey || null,
|
|
105
|
+
mcp: {
|
|
106
|
+
countsByCategory: { ...(mcp.countsByCategory || { data: 0, comms: 0, dev: 0, browser: 0, other: 0 }) },
|
|
107
|
+
total: typeof mcp.total === 'number' ? mcp.total : 0,
|
|
108
|
+
},
|
|
109
|
+
memory: {
|
|
110
|
+
totalImports: typeof memory.totalImports === 'number' ? memory.totalImports : 0,
|
|
111
|
+
maxDepth: typeof memory.maxDepth === 'number' ? memory.maxDepth : 0,
|
|
112
|
+
layered: !!memory.layered,
|
|
113
|
+
},
|
|
114
|
+
automations: {
|
|
115
|
+
scripts: {
|
|
116
|
+
npm: typeof automationsScripts.npm === 'number' ? automationsScripts.npm : 0,
|
|
117
|
+
shell: typeof automationsScripts.shell === 'number' ? automationsScripts.shell : 0,
|
|
118
|
+
},
|
|
119
|
+
jsonPiping: typeof automations.jsonPiping === 'number' ? automations.jsonPiping : 0,
|
|
120
|
+
schedulers: {
|
|
121
|
+
cron: whitelistSchedulerProbe(automationsSchedulers.cron),
|
|
122
|
+
launchd: whitelistSchedulerProbe(automationsSchedulers.launchd),
|
|
123
|
+
pm2: whitelistSchedulerProbe(automationsSchedulers.pm2),
|
|
124
|
+
systemd: whitelistSchedulerProbe(automationsSchedulers.systemd),
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
browserTools: {
|
|
128
|
+
detected: !!browserTools.detected,
|
|
129
|
+
count: typeof browserTools.count === 'number' ? browserTools.count : 0,
|
|
130
|
+
via: {
|
|
131
|
+
dependency: Array.isArray(browserToolsVia.dependencies) && browserToolsVia.dependencies.length > 0,
|
|
132
|
+
mcp: Array.isArray(browserToolsVia.mcp) && browserToolsVia.mcp.length > 0,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
// Hybrid evaluation, Slice 1: two DETERMINISTIC behavioural blocks (no LLM).
|
|
136
|
+
gitActivity: whitelistGitActivity(report.gitActivity),
|
|
137
|
+
sessions: whitelistSessions(report.sessions),
|
|
138
|
+
// Hybrid evaluation, Slice 2: one DETERMINISTIC block (workStreams) and two REDACTED-excerpt blocks (steering, decisions).
|
|
139
|
+
workStreams: whitelistWorkStreams(report.workStreams),
|
|
140
|
+
steering: whitelistSteering(report.steering),
|
|
141
|
+
decisions: whitelistDecisions(report.decisions),
|
|
142
|
+
// Hybrid evaluation, Slice 3c: PER-CANDIDATE authorship + depth (per detected skill, per detected agent).
|
|
143
|
+
detectionEvidence: whitelistDetectionEvidence(report.detectionEvidence),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Re-derives the `detectionEvidence` block field-by-field (never spreads the source).
|
|
148
|
+
function whitelistDetectionEvidence(evidence) {
|
|
149
|
+
if (!evidence || typeof evidence !== 'object') return null;
|
|
150
|
+
const skills = Array.isArray(evidence.skills)
|
|
151
|
+
? evidence.skills.map((s) => ({
|
|
152
|
+
tech: typeof s.tech === 'string' ? s.tech : '',
|
|
153
|
+
authoredFileCount: numOr0(s.authoredFileCount),
|
|
154
|
+
totalFileCount: numOr0(s.totalFileCount),
|
|
155
|
+
authorshipKnown: !!s.authorshipKnown,
|
|
156
|
+
depth: numOrNull(s.depth),
|
|
157
|
+
vendored: !!s.vendored,
|
|
158
|
+
recencyDays: numOrNull(s.recencyDays),
|
|
159
|
+
}))
|
|
160
|
+
: [];
|
|
161
|
+
const agents = Array.isArray(evidence.agents)
|
|
162
|
+
? evidence.agents.map((a) => ({
|
|
163
|
+
name: typeof a.name === 'string' ? a.name : '',
|
|
164
|
+
tools: Array.isArray(a.tools)
|
|
165
|
+
? a.tools.filter((t) => typeof t === 'string')
|
|
166
|
+
: [],
|
|
167
|
+
model: typeof a.model === 'string' && a.model ? a.model : null,
|
|
168
|
+
authoredFileCount: numOr0(a.authoredFileCount),
|
|
169
|
+
totalFileCount: numOr0(a.totalFileCount),
|
|
170
|
+
authorshipKnown: !!a.authorshipKnown,
|
|
171
|
+
depth: numOrNull(a.depth),
|
|
172
|
+
vendored: !!a.vendored,
|
|
173
|
+
recencyDays: numOrNull(a.recencyDays),
|
|
174
|
+
}))
|
|
175
|
+
: [];
|
|
176
|
+
return { skills, agents, provenance: whitelistProvenance(evidence.provenance) };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Re-derives the repo-level anti-noise `provenance` (Slice 3): the (weak) fork signal and the (conservative) boilerplate/tutorial signal.
|
|
180
|
+
function whitelistProvenance(provenance) {
|
|
181
|
+
if (!provenance || typeof provenance !== 'object') return null;
|
|
182
|
+
const marker =
|
|
183
|
+
typeof provenance.boilerplateMarker === 'string' && provenance.boilerplateMarker
|
|
184
|
+
? provenance.boilerplateMarker.slice(0, 60)
|
|
185
|
+
: null;
|
|
186
|
+
const forkSignal =
|
|
187
|
+
typeof provenance.forkSignal === 'string' && provenance.forkSignal
|
|
188
|
+
? provenance.forkSignal.slice(0, 60)
|
|
189
|
+
: null;
|
|
190
|
+
return {
|
|
191
|
+
fork: !!provenance.fork,
|
|
192
|
+
forkSignal,
|
|
193
|
+
boilerplate: !!provenance.boilerplate,
|
|
194
|
+
boilerplateMarker: marker,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function numOrNull(n) {
|
|
199
|
+
return typeof n === 'number' && Number.isFinite(n) ? n : null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Re-derives the `workStreams` block field-by-field (never spreads the source).
|
|
203
|
+
// `null` when absent — the server treats it as "not reported".
|
|
204
|
+
function whitelistWorkStreams(ws) {
|
|
205
|
+
if (!ws || typeof ws !== 'object') return null;
|
|
206
|
+
return {
|
|
207
|
+
streamCount: numOr0(ws.streamCount),
|
|
208
|
+
multiDayStreams: numOr0(ws.multiDayStreams),
|
|
209
|
+
avgCommitsPerSession: numOr0(ws.avgCommitsPerSession),
|
|
210
|
+
maxStreamSpanDays: numOr0(ws.maxStreamSpanDays),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Re-derives the `steering` block.
|
|
215
|
+
function whitelistSteering(s) {
|
|
216
|
+
if (!s || typeof s !== 'object') return null;
|
|
217
|
+
return {
|
|
218
|
+
steeringTraceCount: numOr0(s.steeringTraceCount),
|
|
219
|
+
redactedSteeringExcerpts: whitelistExcerpts(s.redactedSteeringExcerpts),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Re-derives the `decisions` block, same discipline as `steering`. `null` when absent.
|
|
224
|
+
function whitelistDecisions(d) {
|
|
225
|
+
if (!d || typeof d !== 'object') return null;
|
|
226
|
+
return {
|
|
227
|
+
decisionExchangeCount: numOr0(d.decisionExchangeCount),
|
|
228
|
+
redactedDecisionExcerpts: whitelistExcerpts(d.redactedDecisionExcerpts),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const MAX_WHITELISTED_EXCERPTS = 20;
|
|
233
|
+
const MAX_WHITELISTED_EXCERPT_LEN = 200;
|
|
234
|
+
function whitelistExcerpts(arr) {
|
|
235
|
+
return Array.isArray(arr)
|
|
236
|
+
? arr
|
|
237
|
+
.filter((x) => typeof x === 'string' && x)
|
|
238
|
+
.slice(0, MAX_WHITELISTED_EXCERPTS)
|
|
239
|
+
.map((x) => scrubSecrets(x).slice(0, MAX_WHITELISTED_EXCERPT_LEN))
|
|
240
|
+
.filter((x) => x)
|
|
241
|
+
: [];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Re-derives the `gitActivity` block field-by-field (never spreads the source).
|
|
245
|
+
// `null` when absent — the server treats it as "not reported".
|
|
246
|
+
function whitelistGitActivity(git) {
|
|
247
|
+
if (!git || typeof git !== 'object') return null;
|
|
248
|
+
const filesByType = {};
|
|
249
|
+
if (git.filesByType && typeof git.filesByType === 'object') {
|
|
250
|
+
for (const [ext, count] of Object.entries(git.filesByType)) {
|
|
251
|
+
if (typeof count === 'number') filesByType[ext] = count;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
commitCount: numOr0(git.commitCount),
|
|
256
|
+
authoredCommitCount: numOr0(git.authoredCommitCount),
|
|
257
|
+
linesAdded: numOr0(git.linesAdded),
|
|
258
|
+
linesDeleted: numOr0(git.linesDeleted),
|
|
259
|
+
filesByType,
|
|
260
|
+
velocity: numOr0(git.velocity),
|
|
261
|
+
spanDays: numOr0(git.spanDays),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Re-derives the `sessions` block field-by-field (never spreads the source).
|
|
266
|
+
function whitelistSessions(sessions) {
|
|
267
|
+
if (!sessions || typeof sessions !== 'object') return null;
|
|
268
|
+
return {
|
|
269
|
+
toolsSeen: Array.isArray(sessions.toolsSeen)
|
|
270
|
+
? sessions.toolsSeen.filter((t) => typeof t === 'string')
|
|
271
|
+
: [],
|
|
272
|
+
sessionCount: numOr0(sessions.sessionCount),
|
|
273
|
+
subagentRunCount: numOr0(sessions.subagentRunCount),
|
|
274
|
+
activeHours: numOrNull(sessions.activeHours),
|
|
275
|
+
crossToolLinks: numOr0(sessions.crossToolLinks),
|
|
276
|
+
filesTouchedCount: numOr0(sessions.filesTouchedCount),
|
|
277
|
+
bashCommandsCount: numOr0(sessions.bashCommandsCount),
|
|
278
|
+
planningSignalCount: numOr0(sessions.planningSignalCount),
|
|
279
|
+
// Recency-windowed traction signals for the hub profile.
|
|
280
|
+
sessions90d: numOrNull(sessions.sessions90d),
|
|
281
|
+
activeDaysPerWeek: numOrNull(sessions.activeDaysPerWeek),
|
|
282
|
+
blindApprovalCount: numOrNull(sessions.blindApprovalCount),
|
|
283
|
+
humanTurnCount: numOrNull(sessions.humanTurnCount),
|
|
284
|
+
specificInstructionCount: numOrNull(sessions.specificInstructionCount),
|
|
285
|
+
instructionTurnCount: numOrNull(sessions.instructionTurnCount),
|
|
286
|
+
avgInstructionLength: numOrNull(sessions.avgInstructionLength),
|
|
287
|
+
redactedCommandSample: Array.isArray(sessions.redactedCommandSample)
|
|
288
|
+
? sessions.redactedCommandSample
|
|
289
|
+
.filter((c) => typeof c === 'string')
|
|
290
|
+
.map((c) => scrubSecrets(c).slice(0, MAX_WHITELISTED_EXCERPT_LEN))
|
|
291
|
+
.filter((c) => c)
|
|
292
|
+
: [],
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function numOr0(n) {
|
|
297
|
+
return typeof n === 'number' && Number.isFinite(n) ? n : 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
module.exports = { derivePayload };
|
package/src/share.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
const { getIngestEndpoint, isTalentProfile } = require('./config');
|
|
6
|
+
const { requestBackend } = require('./backend-request');
|
|
7
|
+
const { recordBackendAcceptance } = require('./report-store');
|
|
8
|
+
const {
|
|
9
|
+
consentPath,
|
|
10
|
+
SEND_THROTTLE_MS,
|
|
11
|
+
loadConsentState,
|
|
12
|
+
saveConsentState,
|
|
13
|
+
getConsentDecision,
|
|
14
|
+
hasTraceContentConsent,
|
|
15
|
+
isValidEmail,
|
|
16
|
+
normalizeEmail,
|
|
17
|
+
recordConsent,
|
|
18
|
+
getConsentStatus,
|
|
19
|
+
revokeConsent,
|
|
20
|
+
resetConsent,
|
|
21
|
+
clearConsentState,
|
|
22
|
+
setEmail,
|
|
23
|
+
isThrottled,
|
|
24
|
+
} = require('./share-consent-state');
|
|
25
|
+
const { derivePayload } = require('./share-payload');
|
|
26
|
+
const {
|
|
27
|
+
deriveCertificationPayload,
|
|
28
|
+
isCertifyThrottled,
|
|
29
|
+
CERTIFY_SEND_THROTTLE_MS,
|
|
30
|
+
} = require('./share-certification');
|
|
31
|
+
|
|
32
|
+
// Sharing (= PERSISTENCE) layer — the thin transport + orchestration.
|
|
33
|
+
|
|
34
|
+
/* ---------- minimal HTTP utility (no dependencies) ---------- */
|
|
35
|
+
|
|
36
|
+
// POSTs `body` to the ONE backend (talents-ai-score, ADR-042 — the PRIMARY -> FALLBACK chain is retired), via src/backend-request.js.
|
|
37
|
+
async function requestJsonBackend(endpoint, body, { timeoutMs = 15000, idempotencyKey = null } = {}) {
|
|
38
|
+
const { status, raw, backend } = await requestBackend({ endpoint, body, timeoutMs, idempotencyKey });
|
|
39
|
+
let json = null;
|
|
40
|
+
try { json = raw ? JSON.parse(raw) : null; } catch { /* non-JSON response */ }
|
|
41
|
+
return { status, json, raw, backend };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* ---------- automatic sending ---------- */
|
|
45
|
+
|
|
46
|
+
async function autoShare(report, maturity, { root, bypassThrottle = false } = {}) {
|
|
47
|
+
const state = loadConsentState();
|
|
48
|
+
const decision = getConsentDecision(state);
|
|
49
|
+
|
|
50
|
+
if (decision !== 'granted') {
|
|
51
|
+
return { ok: false, skipped: true, reason: decision === 'denied' ? 'consent-denied' : 'no-decision' };
|
|
52
|
+
}
|
|
53
|
+
if (!state.email) {
|
|
54
|
+
return { ok: false, skipped: true, reason: 'no-email' };
|
|
55
|
+
}
|
|
56
|
+
// ADR-006: never SEND under an email whose ownership wasn't proven.
|
|
57
|
+
if (state.emailVerified === false && isTalentProfile()) {
|
|
58
|
+
return { ok: false, skipped: true, reason: 'email-unverified' };
|
|
59
|
+
}
|
|
60
|
+
if (!bypassThrottle && isThrottled(state)) {
|
|
61
|
+
return { ok: false, skipped: true, reason: 'throttled' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const endpoint = getIngestEndpoint();
|
|
65
|
+
if (!endpoint) {
|
|
66
|
+
return { ok: false, skipped: true, reason: 'no-endpoint-configured' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const payload = derivePayload(report, maturity);
|
|
70
|
+
|
|
71
|
+
const idempotencyKey = crypto.randomUUID();
|
|
72
|
+
let res;
|
|
73
|
+
try {
|
|
74
|
+
res = await requestJsonBackend(endpoint, { email: state.email, payload }, { idempotencyKey });
|
|
75
|
+
} catch (e) {
|
|
76
|
+
// Network failure (on every hop): doesn't break the local report.
|
|
77
|
+
return { ok: false, skipped: false, reason: 'network-error', error: e.message };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (res.status >= 200 && res.status < 300) {
|
|
81
|
+
state.lastSentAt = new Date().toISOString();
|
|
82
|
+
saveConsentState(state);
|
|
83
|
+
try {
|
|
84
|
+
recordBackendAcceptance({ root, kind: 'footprint', backend: res.backend });
|
|
85
|
+
} catch {
|
|
86
|
+
// Never break the send over a failed local state write.
|
|
87
|
+
}
|
|
88
|
+
return { ok: true, response: res.json, backend: res.backend };
|
|
89
|
+
}
|
|
90
|
+
if (res.status === 429) {
|
|
91
|
+
return { ok: false, skipped: false, reason: 'rate-limited' };
|
|
92
|
+
}
|
|
93
|
+
if (res.status === 503) {
|
|
94
|
+
// Generic "service unavailable" from the server (ADR-011 retires the
|
|
95
|
+
// server-side kill switch that used to be the main 503 cause).
|
|
96
|
+
return { ok: false, skipped: false, reason: 'service-unavailable' };
|
|
97
|
+
}
|
|
98
|
+
return { ok: false, skipped: false, reason: `http-${res.status}` };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* ---------- certification persistence (skill-code-certification, issue 005) ---------- */
|
|
102
|
+
|
|
103
|
+
// Persists the analyzed certification result if (and only if) consent is granted.
|
|
104
|
+
async function shareCertification(items, { repository = null, commitRange = null, toolVersion = null, superadminToken = null, root = null } = {}) {
|
|
105
|
+
const state = loadConsentState();
|
|
106
|
+
const decision = getConsentDecision(state);
|
|
107
|
+
|
|
108
|
+
if (decision !== 'granted') {
|
|
109
|
+
return { ok: false, skipped: true, reason: decision === 'denied' ? 'consent-denied' : 'no-decision' };
|
|
110
|
+
}
|
|
111
|
+
if (!state.email) {
|
|
112
|
+
return { ok: false, skipped: true, reason: 'no-email' };
|
|
113
|
+
}
|
|
114
|
+
// ADR-006 (mirror of autoShare): don't persist under an unverified email.
|
|
115
|
+
if (state.emailVerified === false) {
|
|
116
|
+
return { ok: false, skipped: true, reason: 'email-unverified' };
|
|
117
|
+
}
|
|
118
|
+
if (isCertifyThrottled(state)) {
|
|
119
|
+
return { ok: false, skipped: true, reason: 'throttled' };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const payload = deriveCertificationPayload(items, { repository, commitRange, toolVersion });
|
|
123
|
+
if (payload.skillCodeAssessments.length === 0) {
|
|
124
|
+
return { ok: false, skipped: true, reason: 'nothing-to-persist' };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const endpoint = getIngestEndpoint();
|
|
128
|
+
if (!endpoint) {
|
|
129
|
+
return { ok: false, skipped: true, reason: 'no-endpoint-configured' };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let res;
|
|
133
|
+
try {
|
|
134
|
+
// ADR-027: forward the superadmin session token so the backend stamps test_origin=true (server-authoritative, non-prod only).
|
|
135
|
+
const body = { email: state.email, payload };
|
|
136
|
+
if (superadminToken) body.superadminToken = superadminToken;
|
|
137
|
+
// Idempotency-Key (three-way security review, cross-repo): ONE key per
|
|
138
|
+
// logical certification-persist submission — same reasoning as autoShare.
|
|
139
|
+
const idempotencyKey = crypto.randomUUID();
|
|
140
|
+
res = await requestJsonBackend(endpoint, body, { idempotencyKey });
|
|
141
|
+
} catch (e) {
|
|
142
|
+
return { ok: false, skipped: false, reason: 'network-error', error: e.message };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (res.status >= 200 && res.status < 300) {
|
|
146
|
+
state.lastCertifySentAt = new Date().toISOString();
|
|
147
|
+
saveConsentState(state);
|
|
148
|
+
try {
|
|
149
|
+
recordBackendAcceptance({ root, kind: 'certification', backend: res.backend });
|
|
150
|
+
} catch {
|
|
151
|
+
// Never break the send over a failed local state write.
|
|
152
|
+
}
|
|
153
|
+
return { ok: true, response: res.json, backend: res.backend };
|
|
154
|
+
}
|
|
155
|
+
if (res.status === 429) {
|
|
156
|
+
return { ok: false, skipped: false, reason: 'rate-limited' };
|
|
157
|
+
}
|
|
158
|
+
return { ok: false, skipped: false, reason: `http-${res.status}` };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = {
|
|
162
|
+
autoShare,
|
|
163
|
+
deriveCertificationPayload,
|
|
164
|
+
shareCertification,
|
|
165
|
+
isCertifyThrottled,
|
|
166
|
+
CERTIFY_SEND_THROTTLE_MS,
|
|
167
|
+
loadConsentState,
|
|
168
|
+
saveConsentState,
|
|
169
|
+
getConsentDecision,
|
|
170
|
+
hasTraceContentConsent,
|
|
171
|
+
recordConsent,
|
|
172
|
+
getConsentStatus,
|
|
173
|
+
revokeConsent,
|
|
174
|
+
resetConsent,
|
|
175
|
+
clearConsentState,
|
|
176
|
+
setEmail,
|
|
177
|
+
isValidEmail,
|
|
178
|
+
normalizeEmail,
|
|
179
|
+
isThrottled,
|
|
180
|
+
derivePayload,
|
|
181
|
+
requestJsonBackend,
|
|
182
|
+
consentPath,
|
|
183
|
+
SEND_THROTTLE_MS,
|
|
184
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Position detail client (hub `works/positions/:id/detail`, talent Bearer).
|
|
4
|
+
// A "project" here is the Position + its parent project's fields.
|
|
5
|
+
|
|
6
|
+
const { postJsonWithTimeout } = require('./backend-request');
|
|
7
|
+
const { reasonForError } = require('./onboarding-client');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 20000;
|
|
10
|
+
|
|
11
|
+
function parseJson(raw) {
|
|
12
|
+
try {
|
|
13
|
+
return raw ? JSON.parse(raw) : null;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function bearerHeaders(accessToken) {
|
|
20
|
+
const headers = {};
|
|
21
|
+
if (typeof accessToken === 'string' && accessToken) headers.Authorization = `Bearer ${accessToken}`;
|
|
22
|
+
return headers;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function requestPositionDetail({ hubAccessToken } = {}, { positionId, endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
26
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
27
|
+
if (!positionId) return { ok: false, reason: 'no-id' };
|
|
28
|
+
if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
|
|
29
|
+
let res;
|
|
30
|
+
try {
|
|
31
|
+
res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, bearerHeaders(hubAccessToken));
|
|
32
|
+
} catch (e) {
|
|
33
|
+
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
34
|
+
}
|
|
35
|
+
if (res.status === 404) return { ok: false, reason: 'not-found' };
|
|
36
|
+
if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
|
|
37
|
+
|
|
38
|
+
const env = parseJson(res.raw);
|
|
39
|
+
const data = env && env.status === 'OK' ? env.data : env;
|
|
40
|
+
if (!data || typeof data !== 'object') return { ok: false, reason: 'bad-response' };
|
|
41
|
+
return { ok: true, position: data.position || null, redirectUrl: data.redirectUrl || null };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// CLI+MCP entry: resolve the endpoint from config, then fetch one detail.
|
|
45
|
+
async function fetchPositionDetail(deps = {}, { hubAccessToken, positionId, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
46
|
+
const {
|
|
47
|
+
getPositionDetailEndpoint = require('./config').getPositionDetailEndpoint,
|
|
48
|
+
requestPositionDetail: request = requestPositionDetail,
|
|
49
|
+
} = deps;
|
|
50
|
+
const endpoint = getPositionDetailEndpoint(positionId);
|
|
51
|
+
if (!endpoint) return { ok: false, reason: positionId ? 'no-endpoint' : 'no-id' };
|
|
52
|
+
return request({ hubAccessToken }, { positionId, endpoint, timeoutMs });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = {
|
|
56
|
+
requestPositionDetail,
|
|
57
|
+
fetchPositionDetail,
|
|
58
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// `show-project` flow: render the detail of one Position/Project. Read-only.
|
|
4
|
+
|
|
5
|
+
const { styleLabel, styleLabelPrefix } = require('./ansi');
|
|
6
|
+
const { resolveName } = require('./translations-client');
|
|
7
|
+
|
|
8
|
+
function makeShowProjectDeps(overrides = {}) {
|
|
9
|
+
const client = require('./show-project-client');
|
|
10
|
+
return {
|
|
11
|
+
fetchPositionDetail: (opts) => client.fetchPositionDetail({}, opts),
|
|
12
|
+
resolveRef: (raw) => require('./find-projects-cache').resolvePositionRef(raw),
|
|
13
|
+
fetchTranslationMap: (opts) => require('./translations-client').fetchTranslationMap({}, opts),
|
|
14
|
+
...overrides,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Skill names come as static-data i18n KEYS; resolve them to readable names
|
|
19
|
+
// (only fetch the translations map when there is a key to resolve).
|
|
20
|
+
async function resolvePositionSkills(position, { deps, hubAccessToken, lang }) {
|
|
21
|
+
const skills = position && Array.isArray(position.skills) ? position.skills : null;
|
|
22
|
+
if (!skills || !skills.some((s) => s && /^staticData/.test(s.name))) return position;
|
|
23
|
+
const tm = await deps.fetchTranslationMap({ hubAccessToken, lang });
|
|
24
|
+
const map = tm && tm.ok ? tm.map : null;
|
|
25
|
+
if (!map) return position;
|
|
26
|
+
return { ...position, skills: skills.map((s) => ({ ...s, name: resolveName(map, s && s.name) })) };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function budgetLine(budget, fp) {
|
|
30
|
+
if (!budget) return fp.budgetUnknown;
|
|
31
|
+
if (budget.display) return budget.display;
|
|
32
|
+
if (budget.from != null || budget.to != null) {
|
|
33
|
+
return `${budget.from ?? '·'}–${budget.to ?? '·'}${budget.unit ? ` ${budget.unit}` : ''}`;
|
|
34
|
+
}
|
|
35
|
+
return fp.budgetUnknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The human render never prints the position/project ids (they stay in --json).
|
|
39
|
+
function renderDetail(io, fp, p) {
|
|
40
|
+
const company = p.company && p.company.name ? p.company.name : (p.company && p.company.restricted ? fp.companyRestricted : null);
|
|
41
|
+
io.section(p.title || fp.untitled);
|
|
42
|
+
if (company) io.notify(styleLabelPrefix(fp.companyLine(company)));
|
|
43
|
+
if (p.subtitle) io.notify(styleLabelPrefix(fp.projectLine(p.subtitle)));
|
|
44
|
+
|
|
45
|
+
const meta = [
|
|
46
|
+
p.attendance || null,
|
|
47
|
+
p.country || null,
|
|
48
|
+
p.requiredMonthlyHours != null ? fp.hoursPerMonth(p.requiredMonthlyHours) : null,
|
|
49
|
+
budgetLine(p.budget, fp),
|
|
50
|
+
p.match != null ? fp.matchScore(p.match) : null,
|
|
51
|
+
].filter(Boolean);
|
|
52
|
+
io.notify(meta.join(' · '));
|
|
53
|
+
|
|
54
|
+
const skills = Array.isArray(p.skills) ? p.skills.map((s) => s && s.name).filter(Boolean) : [];
|
|
55
|
+
if (skills.length) io.notify(styleLabelPrefix(fp.skillsLine(skills.join(', '))));
|
|
56
|
+
const langs = Array.isArray(p.languages) ? p.languages.filter(Boolean) : [];
|
|
57
|
+
if (langs.length) io.notify(styleLabelPrefix(fp.languagesLine(langs.join(', '))));
|
|
58
|
+
|
|
59
|
+
const flags = [
|
|
60
|
+
p.isSaved ? fp.savedBadge : null,
|
|
61
|
+
p.hasApplied ? fp.appliedBadge : (p.canApply ? fp.canApplyBadge : null),
|
|
62
|
+
].filter(Boolean);
|
|
63
|
+
if (flags.length) io.notify(flags.join(' · '));
|
|
64
|
+
|
|
65
|
+
if (p.description) { io.section(fp.descriptionHeading); io.notify(p.description.trim()); }
|
|
66
|
+
if (p.goals) { io.section(fp.goalsHeading); io.notify(p.goals.trim()); }
|
|
67
|
+
const faqs = Array.isArray(p.faqs) ? p.faqs : [];
|
|
68
|
+
if (faqs.length) {
|
|
69
|
+
io.section(fp.faqsHeading);
|
|
70
|
+
faqs.forEach((f) => { if (f && f.question) io.notify(`${styleLabel(f.question)}${f.answer ? `\n ${f.answer}` : ''}`); });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function runShowProject({ io, rawOut, lang = 'en', session, catalog, opts = {}, deps = makeShowProjectDeps() }) {
|
|
75
|
+
const fp = catalog.showProject;
|
|
76
|
+
const hubAccessToken = session ? session.hubAccessToken : null;
|
|
77
|
+
// A pure integer resolves against the last `find-projects` listing; a UUID/Mongo id passes through.
|
|
78
|
+
const resolved = deps.resolveRef(opts.id);
|
|
79
|
+
if (!resolved.ok) {
|
|
80
|
+
io.error(resolved.reason === 'no-cache' ? fp.refNoCache
|
|
81
|
+
: resolved.reason === 'out-of-range' ? fp.refOutOfRange(resolved.count)
|
|
82
|
+
: fp.idRequired);
|
|
83
|
+
return { ok: false, reason: resolved.reason };
|
|
84
|
+
}
|
|
85
|
+
const positionId = resolved.id;
|
|
86
|
+
|
|
87
|
+
const doFetch = () => deps.fetchPositionDetail({ hubAccessToken, positionId });
|
|
88
|
+
const res = opts.json ? await doFetch() : await io.withProgress(fp.loading, doFetch);
|
|
89
|
+
if (!res.ok) {
|
|
90
|
+
io.error(res.reason === 'not-found' ? fp.notFound : fp.fetchFailed(res.reason));
|
|
91
|
+
return { ok: false, reason: res.reason };
|
|
92
|
+
}
|
|
93
|
+
if (!res.position) {
|
|
94
|
+
// --json must stay machine-readable even when the position is hidden.
|
|
95
|
+
if (opts.json) {
|
|
96
|
+
(rawOut || ((s) => process.stdout.write(s)))(`${JSON.stringify({ ok: true, position: null, redirectUrl: res.redirectUrl }, null, 2)}\n`);
|
|
97
|
+
return { ok: true, position: null, redirectUrl: res.redirectUrl };
|
|
98
|
+
}
|
|
99
|
+
io.notify(fp.notVisible);
|
|
100
|
+
return { ok: true, position: null, redirectUrl: res.redirectUrl };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const position = await resolvePositionSkills(res.position, { deps, hubAccessToken, lang });
|
|
104
|
+
|
|
105
|
+
if (opts.json) {
|
|
106
|
+
(rawOut || ((s) => process.stdout.write(s)))(`${JSON.stringify({ ok: true, position, redirectUrl: res.redirectUrl }, null, 2)}\n`);
|
|
107
|
+
return { ok: true, position };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
renderDetail(io, fp, position);
|
|
111
|
+
io.success(fp.done);
|
|
112
|
+
return { ok: true, position };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
makeShowProjectDeps,
|
|
117
|
+
runShowProject,
|
|
118
|
+
renderDetail,
|
|
119
|
+
};
|