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,306 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
getUsageDiscoveredInventoryEndpoint,
|
|
5
|
+
getAgentsListEndpoint,
|
|
6
|
+
getAgentsDeclareEndpoint,
|
|
7
|
+
getAgentsDraftFieldsEndpoint,
|
|
8
|
+
getPortfoliosListEndpoint,
|
|
9
|
+
getAgentPortfoliosEndpoint,
|
|
10
|
+
} = require('./config');
|
|
11
|
+
const {
|
|
12
|
+
requestListAgents, requestDeclareAgent, requestDraftAgentFields, requestRelateAgentPortfolios,
|
|
13
|
+
} = require('./agents-client');
|
|
14
|
+
const { requestListPortfolios } = require('./portfolio-client');
|
|
15
|
+
const { requestDiscoveredInventory } = require('./inventory-client');
|
|
16
|
+
const { ensureFreshSession } = require('./session-refresh');
|
|
17
|
+
const { confirmDisclaimerAcceptance } = require('./certify-disclaimer');
|
|
18
|
+
const { runInteractiveMultiSelect } = require('./interactive-select');
|
|
19
|
+
const { loadAuthSession, sessionStatus } = require('./auth-session-store');
|
|
20
|
+
const { withSpinner } = require('./terminal-progress');
|
|
21
|
+
const { reasonCopy, pickPortfolios } = require('./start-add-skills');
|
|
22
|
+
// `askWithDefault`: pre-filled line prompt (empty enter accepts the default),
|
|
23
|
+
// reused from the portfolio route.
|
|
24
|
+
const { askWithDefault } = require('./start-add-portfolio');
|
|
25
|
+
const { styleQuestion, styleSuccess } = require('./ansi');
|
|
26
|
+
|
|
27
|
+
// `start`'s "Add agent to profile" route (talents-ai-score Phase 2), mirror of src/start-add-portfolio.js: the IA pre-drafts, the Talent edits.
|
|
28
|
+
|
|
29
|
+
const MAX_DEFINITION_CHARS = 4000;
|
|
30
|
+
|
|
31
|
+
async function askFreeText(ask, promptText) {
|
|
32
|
+
return String(await ask(styleQuestion(promptText))).trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function askNumberedChoice(ask, promptText, count) {
|
|
36
|
+
const raw = (await ask(styleQuestion(promptText))).trim();
|
|
37
|
+
if (!raw) return null;
|
|
38
|
+
const n = Number.parseInt(raw, 10);
|
|
39
|
+
if (!Number.isInteger(n) || n < 1 || n > count) return null;
|
|
40
|
+
return n - 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Step 1 — pick ONE detected agent (single-select, numbered-prompt fallback).
|
|
44
|
+
// Returns the agent, or null on cancel.
|
|
45
|
+
async function chooseAgent({ ask, stdinIsTTY, agents, c, input, output }) {
|
|
46
|
+
const labelFor = (a) => a.pickerLabel || a.name;
|
|
47
|
+
if (stdinIsTTY && typeof ask.suspend === 'function') {
|
|
48
|
+
ask.suspend();
|
|
49
|
+
const picked = await runInteractiveMultiSelect({
|
|
50
|
+
items: agents,
|
|
51
|
+
labelFor,
|
|
52
|
+
header: c.agentPickHeading,
|
|
53
|
+
hint: c.agentPickHint,
|
|
54
|
+
single: true,
|
|
55
|
+
...(input ? { input } : {}),
|
|
56
|
+
...(output ? { output } : {}),
|
|
57
|
+
});
|
|
58
|
+
ask.resume();
|
|
59
|
+
// Drain the picker-residual-Enter (the suspend/resume mode switch can leave a
|
|
60
|
+
// stray Enter that pre-answers the next prompt) — same fix as bin/start.js/certify.js.
|
|
61
|
+
if (typeof ask.drain === 'function') ask.drain();
|
|
62
|
+
return picked && picked.length ? picked[0] : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
process.stdout.write(`\n ${styleQuestion(c.agentPickHeading)}\n`);
|
|
66
|
+
agents.forEach((a, i) => process.stdout.write(` ${i + 1}) ${labelFor(a)}\n`));
|
|
67
|
+
const idx = await askNumberedChoice(ask, ` ${c.agentPickPrompt(agents.length)}`, agents.length);
|
|
68
|
+
return idx === null ? null : agents[idx];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Step 4 — the two AI-drafted fields, consent-gated (ADR-052) before any egress.
|
|
72
|
+
async function resolveAgentFields({ ask, catalog, c, stdinIsTTY, agent, accessToken, preAccepted = false }) {
|
|
73
|
+
const freeText = async () => ({
|
|
74
|
+
whatItDoes: await askFreeText(ask, c.agentWhatItDoesFreeTextPrompt),
|
|
75
|
+
humanDecides: await askFreeText(ask, c.agentHumanDecidesFreeTextPrompt),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const draftEndpoint = getAgentsDraftFieldsEndpoint();
|
|
79
|
+
if (!draftEndpoint) return freeText();
|
|
80
|
+
|
|
81
|
+
const acceptance = await confirmDisclaimerAcceptance({
|
|
82
|
+
ask, catalog, stdinIsTTY, preAccepted, text: c.agentFieldsConsentDisclaimer,
|
|
83
|
+
});
|
|
84
|
+
if (!acceptance.accepted) return freeText();
|
|
85
|
+
|
|
86
|
+
const definition = typeof agent.definition === 'string' && agent.definition
|
|
87
|
+
? agent.definition.slice(0, MAX_DEFINITION_CHARS)
|
|
88
|
+
: null;
|
|
89
|
+
const draft = await withSpinner(c.agentDraftingLabel, () => requestDraftAgentFields(
|
|
90
|
+
{ name: agent.name, tools: agent.tools, model: agent.model, parent: agent.parent, definition, accessToken },
|
|
91
|
+
{ endpoint: draftEndpoint },
|
|
92
|
+
));
|
|
93
|
+
if (!draft.ok) return freeText();
|
|
94
|
+
|
|
95
|
+
process.stdout.write(`\n ${styleQuestion(c.agentWhatItDoesDraftLabel)}\n ${draft.whatItDoes}\n`);
|
|
96
|
+
const whatItDoes = await askWithDefault(ask, c.agentWhatItDoesEditPrompt, draft.whatItDoes);
|
|
97
|
+
process.stdout.write(`\n ${styleQuestion(c.agentHumanDecidesDraftLabel)}\n ${draft.humanDecides}\n`);
|
|
98
|
+
const humanDecides = await askWithDefault(ask, c.agentHumanDecidesEditPrompt, draft.humanDecides);
|
|
99
|
+
return { whatItDoes, humanDecides };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Asks to re-login and returns the fresh session, or null. Reuses the portfolio
|
|
103
|
+
// route's generic relogin copys.
|
|
104
|
+
async function offerRelogin({ ask, c, runLogin }) {
|
|
105
|
+
if (typeof runLogin !== 'function') return null;
|
|
106
|
+
const raw = String(await ask(`\n ${styleQuestion(c.portfolioOfferRelogin)} `)).trim();
|
|
107
|
+
if (!/^(y|yes|s|si|sí)$/i.test(raw)) {
|
|
108
|
+
process.stdout.write(`\n ${c.portfolioRelaunchDeclined}\n\n`);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
await runLogin([], { ask });
|
|
112
|
+
const fresh = loadAuthSession();
|
|
113
|
+
if (sessionStatus(fresh) !== 'active' || !fresh.hubAccessToken) {
|
|
114
|
+
process.stdout.write(`\n ${c.portfolioReloginFailed}\n\n`);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
return fresh;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function offerRelateAgentToPortfolios({
|
|
121
|
+
ask, stdinIsTTY, agentId, agentName, session, c, input, output,
|
|
122
|
+
}) {
|
|
123
|
+
if (!agentId) return;
|
|
124
|
+
const listEndpoint = getPortfoliosListEndpoint();
|
|
125
|
+
const relateEndpoint = getAgentPortfoliosEndpoint(agentId);
|
|
126
|
+
if (!listEndpoint || !relateEndpoint) return;
|
|
127
|
+
|
|
128
|
+
const listed = await requestListPortfolios(
|
|
129
|
+
{ accessToken: session.accessToken, hubAccessToken: session.hubAccessToken },
|
|
130
|
+
{ endpoint: listEndpoint },
|
|
131
|
+
);
|
|
132
|
+
if (!listed.ok || listed.portfolios.length === 0) {
|
|
133
|
+
process.stdout.write(`\n ${c.agentRelateNoPortfolios}\n`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const wantsRelate = /^(y|yes|s|si|sí)$/i.test(String(await ask(`\n ${styleQuestion(c.agentRelateAsk(agentName))} `)).trim());
|
|
138
|
+
if (!wantsRelate) {
|
|
139
|
+
process.stdout.write(` ${c.agentRelateSkipped(agentName)}\n`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const heading = c.agentRelateSelectHeading(agentName);
|
|
144
|
+
const chosen = await pickPortfolios({ ask, stdinIsTTY, portfolios: listed.portfolios, heading, c, input, output });
|
|
145
|
+
if (!chosen || chosen.length === 0) {
|
|
146
|
+
process.stdout.write(` ${c.agentRelateSkipped(agentName)}\n`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const items = chosen.map((portfolio) => ({ portfolioId: portfolio.id }));
|
|
151
|
+
|
|
152
|
+
const r = await requestRelateAgentPortfolios(
|
|
153
|
+
{ agentId, items, hubAccessToken: session.hubAccessToken },
|
|
154
|
+
{ endpoint: relateEndpoint },
|
|
155
|
+
);
|
|
156
|
+
if (r.ok) {
|
|
157
|
+
for (const portfolio of chosen) {
|
|
158
|
+
process.stdout.write(` ${styleSuccess(c.agentRelated(agentName, portfolio.name))}\n`);
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
process.stdout.write(` ${c.agentRelateFailed(agentName, reasonCopy(r.reason, c))}\n`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function runAddAgents({
|
|
166
|
+
ask, catalog, root, session, stdinIsTTY, input, output, preAccepted = false, deps = {},
|
|
167
|
+
} = {}) {
|
|
168
|
+
const c = catalog.start;
|
|
169
|
+
const runLogin = deps.runLogin;
|
|
170
|
+
const refreshSession = deps.refreshSession || (() => ensureFreshSession(process.env));
|
|
171
|
+
|
|
172
|
+
const inventoryEndpoint = getUsageDiscoveredInventoryEndpoint();
|
|
173
|
+
const listEndpoint = getAgentsListEndpoint();
|
|
174
|
+
const declareEndpoint = getAgentsDeclareEndpoint();
|
|
175
|
+
if (!inventoryEndpoint || !listEndpoint || !declareEndpoint) {
|
|
176
|
+
process.stderr.write(`\n ${c.agentErrorNoEndpoint}\n\n`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let hubAccessToken = session.hubAccessToken;
|
|
181
|
+
if (!hubAccessToken) {
|
|
182
|
+
process.stderr.write(`\n ${c.agentNoHubToken}\n\n`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let inventory = await requestDiscoveredInventory(
|
|
187
|
+
{ accessToken: session.accessToken },
|
|
188
|
+
{ endpoint: inventoryEndpoint },
|
|
189
|
+
);
|
|
190
|
+
// An expired hub JWT (the same 401 the usage poll hits) must not break add-agent:
|
|
191
|
+
// refresh the session once and retry the read (and adopt the fresh tokens downstream).
|
|
192
|
+
if (inventory.reason === 'http-401') {
|
|
193
|
+
let fresh = null;
|
|
194
|
+
try { fresh = await refreshSession(); } catch { fresh = null; }
|
|
195
|
+
if (fresh && (fresh.accessToken || fresh.hubAccessToken)) {
|
|
196
|
+
session = { ...session, accessToken: fresh.accessToken || session.accessToken, hubAccessToken: fresh.hubAccessToken || session.hubAccessToken };
|
|
197
|
+
hubAccessToken = session.hubAccessToken;
|
|
198
|
+
inventory = await requestDiscoveredInventory(
|
|
199
|
+
{ accessToken: session.accessToken },
|
|
200
|
+
{ endpoint: inventoryEndpoint },
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (!inventory.ok) {
|
|
205
|
+
process.stderr.write(`\n ${c.agentInventoryError(reasonCopy(inventory.reason, c))}\n\n`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const agents = inventory.agents;
|
|
209
|
+
if (agents.length === 0) {
|
|
210
|
+
process.stdout.write(`\n ${c.agentNoAgentsDetected}\n\n`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
for (const a of agents) a.pickerLabel = a.role ? `${a.name} — ${a.role}` : a.name;
|
|
214
|
+
|
|
215
|
+
// DEDUP by name (before asking anything). A failed list is reported as "could
|
|
216
|
+
// not verify" (never "no duplicates") and the flow continues.
|
|
217
|
+
const listed = await withSpinner(
|
|
218
|
+
c.agentDedupCheckingLabel,
|
|
219
|
+
() => requestListAgents({ accessToken: session.accessToken, hubAccessToken }, { endpoint: listEndpoint }),
|
|
220
|
+
);
|
|
221
|
+
const existingNames = listed.ok ? listed.agents.map((a) => a.name.toLowerCase()) : null;
|
|
222
|
+
if (!listed.ok) process.stdout.write(`\n ${c.agentDedupCheckFailed(reasonCopy(listed.reason, c))}\n`);
|
|
223
|
+
|
|
224
|
+
process.stdout.write(`\n ${c.agentIntro}\n`);
|
|
225
|
+
|
|
226
|
+
// (1) Pick a detected agent.
|
|
227
|
+
const agent = await chooseAgent({ ask, stdinIsTTY, agents, c, input, output });
|
|
228
|
+
if (!agent) {
|
|
229
|
+
process.stdout.write(`\n ${c.agentCancelled}\n\n`);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// (3) Name ← detected, editable. (2) dedup applies to the FINAL name.
|
|
234
|
+
const name = await askWithDefault(ask, c.agentNamePrompt(agent.name), agent.name);
|
|
235
|
+
if (existingNames && existingNames.includes(name.toLowerCase())) {
|
|
236
|
+
const existing = listed.ok ? listed.agents.find((a) => a.name.toLowerCase() === name.toLowerCase()) : null;
|
|
237
|
+
const existingId = existing && existing.id ? existing.id : null;
|
|
238
|
+
if (existingId) {
|
|
239
|
+
process.stdout.write(`\n ${c.agentAlreadyAdded(name)}\n`);
|
|
240
|
+
await offerRelateAgentToPortfolios({
|
|
241
|
+
ask, stdinIsTTY, agentId: existingId, agentName: name, session, c, input, output,
|
|
242
|
+
});
|
|
243
|
+
process.stdout.write('\n');
|
|
244
|
+
} else {
|
|
245
|
+
process.stdout.write(`\n ${c.agentDuplicateName(name)}\n\n`);
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// (4) whatItDoes + humanDecides — AI draft (consent-gated) or free-text.
|
|
251
|
+
const { whatItDoes, humanDecides } = await resolveAgentFields({
|
|
252
|
+
ask, catalog, c, stdinIsTTY, agent, accessToken: session.accessToken, preAccepted,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const matchedRole = agent.role || null;
|
|
256
|
+
if (matchedRole) process.stdout.write(`\n ${c.agentCatalogMatched(matchedRole)}\n`);
|
|
257
|
+
|
|
258
|
+
const payload = { name, whatItDoes: whatItDoes || agent.whatItDoes, humanDecides };
|
|
259
|
+
|
|
260
|
+
// Declare.
|
|
261
|
+
const declared = await withSpinner(
|
|
262
|
+
c.agentDeclaring,
|
|
263
|
+
() => requestDeclareAgent({ ...payload, accessToken: session.accessToken, hubAccessToken }, { endpoint: declareEndpoint }),
|
|
264
|
+
);
|
|
265
|
+
if (declared.ok) {
|
|
266
|
+
process.stdout.write(`\n ${styleSuccess(c.agentDeclaredSuccess(name))}\n`);
|
|
267
|
+
await offerRelateAgentToPortfolios({
|
|
268
|
+
ask, stdinIsTTY, agentId: declared.agentId, agentName: name, session, c, input, output,
|
|
269
|
+
});
|
|
270
|
+
process.stdout.write('\n');
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (declared.reason !== 'hub-session-expired') {
|
|
274
|
+
process.stdout.write(`\n ${c.agentDeclareFailed(reasonCopy(declared.reason, c))}\n\n`);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Hub session expired mid-flight — offer one relogin + retry.
|
|
279
|
+
process.stdout.write(`\n ${c.agentHubSessionExpired}\n`);
|
|
280
|
+
const relogged = await offerRelogin({ ask, c, runLogin });
|
|
281
|
+
if (!relogged) return;
|
|
282
|
+
process.stdout.write(` ${c.portfolioRelaunchAfterRelogin}\n`);
|
|
283
|
+
const retried = await withSpinner(
|
|
284
|
+
c.agentDeclaring,
|
|
285
|
+
() => requestDeclareAgent(
|
|
286
|
+
{ ...payload, accessToken: relogged.accessToken, hubAccessToken: relogged.hubAccessToken },
|
|
287
|
+
{ endpoint: declareEndpoint },
|
|
288
|
+
),
|
|
289
|
+
);
|
|
290
|
+
if (retried.ok) {
|
|
291
|
+
process.stdout.write(`\n ${styleSuccess(c.agentDeclaredSuccess(name))}\n`);
|
|
292
|
+
await offerRelateAgentToPortfolios({
|
|
293
|
+
ask, stdinIsTTY, agentId: retried.agentId, agentName: name, session: relogged, c, input, output,
|
|
294
|
+
});
|
|
295
|
+
process.stdout.write('\n');
|
|
296
|
+
} else {
|
|
297
|
+
process.stdout.write(`\n ${c.agentDeclareFailed(reasonCopy(retried.reason, c))}\n\n`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
module.exports = {
|
|
302
|
+
runAddAgents,
|
|
303
|
+
chooseAgent,
|
|
304
|
+
resolveAgentFields,
|
|
305
|
+
offerRelateAgentToPortfolios,
|
|
306
|
+
};
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const { detectTechnologies } = require('./tech-detector');
|
|
7
|
+
const {
|
|
8
|
+
getPortfoliosListEndpoint, getPortfoliosDeclareEndpoint, getPortfolioDraftDescriptionEndpoint,
|
|
9
|
+
getSkillsResolveMatchedEndpoint, getSkillsResolveAddableEndpoint, getSkillsDeclareEndpoint,
|
|
10
|
+
} = require('./config');
|
|
11
|
+
const { requestListPortfolios, requestDeclarePortfolio, requestDraftDescription } = require('./portfolio-client');
|
|
12
|
+
const { requestResolveAddableSkills: requestResolveMatchedSkills, requestResolveAddableSkills } = require('./skills-client');
|
|
13
|
+
const { getRemoteUrl, getCommitDateRange } = require('./portfolio-git-info');
|
|
14
|
+
const { confirmDisclaimerAcceptance } = require('./certify-disclaimer');
|
|
15
|
+
const { runInteractiveMultiSelect, wrapDesc } = require('./interactive-select');
|
|
16
|
+
const { loadAuthSession, sessionStatus } = require('./auth-session-store');
|
|
17
|
+
const { withSpinner } = require('./terminal-progress');
|
|
18
|
+
const { parseMultiIndices, reasonCopy, declareAll, printProfileBoost } = require('./start-add-skills');
|
|
19
|
+
const { palette, styleQuestion, styleSuccess } = require('./ansi');
|
|
20
|
+
|
|
21
|
+
// Only `dim`/`reset` for the numbered fallback's descriptions — the exact treatment bin/start.js's own menu and bin/certify.js's picker give theirs.
|
|
22
|
+
const ANSI = palette({ reset: '\x1b[0m', dim: '\x1b[2m' });
|
|
23
|
+
|
|
24
|
+
// `start`'s "Add project to portfolio" route (talents-ai-score, ADR-059).
|
|
25
|
+
|
|
26
|
+
const MAX_README_CHARS = 4000;
|
|
27
|
+
const MAX_PACKAGE_DESCRIPTION_CHARS = 500;
|
|
28
|
+
|
|
29
|
+
function readPackageDescription(root) {
|
|
30
|
+
try {
|
|
31
|
+
const raw = fs.readFileSync(path.join(root, 'package.json'), 'utf8');
|
|
32
|
+
const pkg = JSON.parse(raw);
|
|
33
|
+
if (typeof pkg.description === 'string' && pkg.description.trim()) {
|
|
34
|
+
return pkg.description.trim().slice(0, MAX_PACKAGE_DESCRIPTION_CHARS);
|
|
35
|
+
}
|
|
36
|
+
} catch { /* no package.json, unreadable, or malformed JSON */ }
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readReadme(root) {
|
|
41
|
+
for (const name of ['README.md', 'README.MD', 'Readme.md', 'readme.md']) {
|
|
42
|
+
try {
|
|
43
|
+
const raw = fs.readFileSync(path.join(root, name), 'utf8');
|
|
44
|
+
if (raw && raw.trim()) return raw.trim().slice(0, MAX_README_CHARS);
|
|
45
|
+
} catch { /* try the next candidate */ }
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function askWithDefault(ask, promptText, defaultValue) {
|
|
51
|
+
const raw = String(await ask(styleQuestion(promptText))).trim();
|
|
52
|
+
return raw || String(defaultValue || '');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function askFreeText(ask, promptText) {
|
|
56
|
+
return String(await ask(styleQuestion(promptText))).trim();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeDomain(raw) {
|
|
60
|
+
let s = String(raw || '').trim().toLowerCase();
|
|
61
|
+
if (!s) return '';
|
|
62
|
+
s = s.replace(/^https?:\/\//, '');
|
|
63
|
+
s = s.split('/')[0].split('?')[0].split('#')[0];
|
|
64
|
+
s = s.replace(/^www\./, '');
|
|
65
|
+
return s;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Step 2 — Type.
|
|
69
|
+
function typeOptions(c) {
|
|
70
|
+
return [
|
|
71
|
+
{ key: 'PORTFOLIO', label: c.portfolioTypePortfolio, desc: c.portfolioTypePortfolioDesc },
|
|
72
|
+
{ key: 'EXPERIENCE', label: c.portfolioTypeExperience, desc: c.portfolioTypeExperienceDesc },
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function askNumberedChoice(ask, promptText, count) {
|
|
77
|
+
const raw = (await ask(styleQuestion(promptText))).trim();
|
|
78
|
+
if (!raw) return null;
|
|
79
|
+
const n = Number.parseInt(raw, 10);
|
|
80
|
+
if (!Number.isInteger(n) || n < 1 || n > count) return null;
|
|
81
|
+
return n - 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function chooseType({ ask, c, input, output }) {
|
|
85
|
+
const options = typeOptions(c);
|
|
86
|
+
if (typeof ask.suspend === 'function') {
|
|
87
|
+
ask.suspend();
|
|
88
|
+
const picked = await runInteractiveMultiSelect({
|
|
89
|
+
items: options,
|
|
90
|
+
labelFor: (o) => o.label,
|
|
91
|
+
header: c.portfolioTypeHeading,
|
|
92
|
+
hint: c.portfolioTypeHint,
|
|
93
|
+
single: true,
|
|
94
|
+
descriptionFor: (o) => o.desc || null,
|
|
95
|
+
...(input ? { input } : {}),
|
|
96
|
+
...(output ? { output } : {}),
|
|
97
|
+
});
|
|
98
|
+
ask.resume();
|
|
99
|
+
return picked && picked.length ? picked[0].key : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
process.stdout.write(`\n ${styleQuestion(c.portfolioTypeHeading)}\n`);
|
|
103
|
+
options.forEach((o, i) => {
|
|
104
|
+
process.stdout.write(` ${i + 1}) ${o.label}\n`);
|
|
105
|
+
if (o.desc) for (const line of wrapDesc(o.desc)) process.stdout.write(` ${ANSI.dim}${line}${ANSI.reset}\n`);
|
|
106
|
+
});
|
|
107
|
+
const idx = await askNumberedChoice(ask, ` ${c.portfolioTypePrompt(options.length)}`, options.length);
|
|
108
|
+
return idx === null ? null : options[idx].key;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Step 5 — Skills.
|
|
112
|
+
async function pickPortfolioSkills({ ask, stdinIsTTY, candidates, c, input, output }) {
|
|
113
|
+
if (stdinIsTTY && typeof ask.suspend === 'function') {
|
|
114
|
+
ask.suspend();
|
|
115
|
+
const picked = await runInteractiveMultiSelect({
|
|
116
|
+
items: candidates,
|
|
117
|
+
labelFor: (s) => `${s.skillName}${s.technology ? ` (${s.technology})` : ''}`,
|
|
118
|
+
header: c.portfolioSkillsSelectHeading,
|
|
119
|
+
hint: c.portfolioSkillsSelectHint,
|
|
120
|
+
initialMarked: candidates.map((_, i) => i),
|
|
121
|
+
...(input ? { input } : {}),
|
|
122
|
+
...(output ? { output } : {}),
|
|
123
|
+
});
|
|
124
|
+
ask.resume();
|
|
125
|
+
return picked; // null on cancel, [] is a legal ("unchecked everything") choice
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
process.stdout.write(`\n ${styleQuestion(c.portfolioSkillsSelectHeading)}\n`);
|
|
129
|
+
candidates.forEach((s, i) => {
|
|
130
|
+
process.stdout.write(` [x] ${i + 1}) ${s.skillName}${s.technology ? ` (${s.technology})` : ''}\n`);
|
|
131
|
+
});
|
|
132
|
+
const raw = (await ask(` ${styleQuestion(c.portfolioSkillsSelectPrompt(candidates.length))}`)).trim();
|
|
133
|
+
if (!raw) return candidates; // empty -> keep the pre-marked default (ALL), not "none"
|
|
134
|
+
const idxs = parseMultiIndices(raw, candidates.length);
|
|
135
|
+
if (!idxs) {
|
|
136
|
+
process.stdout.write(` ${c.portfolioSkillsSelectInvalid}\n`);
|
|
137
|
+
return candidates; // unparseable -> the forgiving default, never a silent []
|
|
138
|
+
}
|
|
139
|
+
return idxs.map((i) => candidates[i]);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Resolves the pre-marked Skill candidates for step 5.
|
|
143
|
+
async function resolvePortfolioSkills({ ask, stdinIsTTY, technologies, c, input, output, accessToken }) {
|
|
144
|
+
if (technologies.length === 0) return [];
|
|
145
|
+
const endpoint = getSkillsResolveMatchedEndpoint();
|
|
146
|
+
if (!endpoint) return [];
|
|
147
|
+
|
|
148
|
+
const resolved = await withSpinner(c.portfolioSkillsResolvingLabel, () => requestResolveMatchedSkills({ technologies, accessToken }, { endpoint }));
|
|
149
|
+
if (!resolved.ok || resolved.addable.length === 0) return [];
|
|
150
|
+
|
|
151
|
+
const chosen = await pickPortfolioSkills({ ask, stdinIsTTY, candidates: resolved.addable, c, input, output });
|
|
152
|
+
return chosen || []; // esc/cancel -> treat like "none chosen" for this OPTIONAL step, never abort the whole route
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function pickAddableSkillsForProfile({ ask, stdinIsTTY, candidates, c, input, output }) {
|
|
156
|
+
if (stdinIsTTY && typeof ask.suspend === 'function') {
|
|
157
|
+
ask.suspend();
|
|
158
|
+
const picked = await runInteractiveMultiSelect({
|
|
159
|
+
items: candidates,
|
|
160
|
+
labelFor: (s) => `${s.skillName}${s.technology ? ` (${s.technology})` : ''}`,
|
|
161
|
+
header: c.portfolioAddSkillsHeading,
|
|
162
|
+
hint: c.portfolioAddSkillsHint,
|
|
163
|
+
initialMarked: candidates.map((_, i) => i),
|
|
164
|
+
...(input ? { input } : {}),
|
|
165
|
+
...(output ? { output } : {}),
|
|
166
|
+
});
|
|
167
|
+
ask.resume();
|
|
168
|
+
return picked; // null on cancel, [] is a legal ("unchecked everything") choice
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
process.stdout.write(`\n ${styleQuestion(c.portfolioAddSkillsHeading)}\n`);
|
|
172
|
+
candidates.forEach((s, i) => {
|
|
173
|
+
process.stdout.write(` [x] ${i + 1}) ${s.skillName}${s.technology ? ` (${s.technology})` : ''}\n`);
|
|
174
|
+
});
|
|
175
|
+
const raw = (await ask(` ${styleQuestion(c.portfolioAddSkillsPrompt(candidates.length))}`)).trim();
|
|
176
|
+
if (!raw) return candidates; // empty -> keep the pre-marked default (ALL), not "none"
|
|
177
|
+
const idxs = parseMultiIndices(raw, candidates.length);
|
|
178
|
+
if (!idxs) {
|
|
179
|
+
process.stdout.write(` ${c.portfolioAddSkillsInvalid}\n`);
|
|
180
|
+
return candidates; // unparseable -> the forgiving default, never a silent []
|
|
181
|
+
}
|
|
182
|
+
return idxs.map((i) => candidates[i]);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// The whole opt-in step.
|
|
186
|
+
async function offerAddableSkillsForProfile({ ask, catalog, c, technologies, session, stdinIsTTY, input, output, runLogin }) {
|
|
187
|
+
if (technologies.length === 0) return;
|
|
188
|
+
const resolveEndpoint = getSkillsResolveAddableEndpoint();
|
|
189
|
+
const declareEndpoint = getSkillsDeclareEndpoint();
|
|
190
|
+
if (!resolveEndpoint || !declareEndpoint) return;
|
|
191
|
+
|
|
192
|
+
const resolved = await withSpinner(
|
|
193
|
+
c.portfolioAddSkillsResolvingLabel,
|
|
194
|
+
() => requestResolveAddableSkills({ technologies, accessToken: session.accessToken }, { endpoint: resolveEndpoint }),
|
|
195
|
+
);
|
|
196
|
+
if (!resolved.ok) return; // an actual failure (network/etc) — unrelated to dedup, stays silent as before
|
|
197
|
+
if (resolved.addable.length === 0) {
|
|
198
|
+
process.stdout.write(`\n ${c.portfolioAddSkillsAllAlreadyDeclared}\n`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const hubAccessToken = session.hubAccessToken;
|
|
203
|
+
if (!hubAccessToken) return; // shouldn't happen this late (the portfolio declare above already needed one), never crash
|
|
204
|
+
|
|
205
|
+
process.stdout.write(`\n ${c.portfolioAddSkillsIntro}\n`);
|
|
206
|
+
const chosen = await pickAddableSkillsForProfile({ ask, stdinIsTTY, candidates: resolved.addable, c, input, output });
|
|
207
|
+
if (!chosen || chosen.length === 0) return; // esc/cancel or "none chosen" -> silent, this whole step is optional
|
|
208
|
+
|
|
209
|
+
process.stdout.write('\n');
|
|
210
|
+
const { pending, hubSessionExpired, succeeded } = await declareAll(chosen, { accessToken: session.accessToken, hubAccessToken }, declareEndpoint, c);
|
|
211
|
+
if (!hubSessionExpired) {
|
|
212
|
+
printProfileBoost(succeeded, c);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
process.stdout.write(`\n ${c.addSkillsHubSessionExpired}\n`);
|
|
217
|
+
const relogged = await offerRelogin({ ask, c, runLogin });
|
|
218
|
+
if (!relogged) return;
|
|
219
|
+
process.stdout.write(` ${c.addSkillsRelaunchAfterRelogin}\n\n`);
|
|
220
|
+
// Same fix as above — no spinner around a task that prints its own lines.
|
|
221
|
+
const retried = await declareAll(pending, { accessToken: relogged.accessToken, hubAccessToken: relogged.hubAccessToken }, declareEndpoint, c);
|
|
222
|
+
printProfileBoost(succeeded + retried.succeeded, c);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Step 3 — Description.
|
|
226
|
+
async function resolveDescription({ ask, catalog, c, stdinIsTTY, root, title, technologies, accessToken, preAccepted = false }) {
|
|
227
|
+
const draftEndpoint = getPortfolioDraftDescriptionEndpoint();
|
|
228
|
+
if (!draftEndpoint) return askFreeText(ask, c.portfolioDescriptionFreeTextPrompt);
|
|
229
|
+
|
|
230
|
+
const acceptance = await confirmDisclaimerAcceptance({
|
|
231
|
+
ask, catalog, stdinIsTTY, preAccepted, text: c.portfolioDescriptionConsentDisclaimer,
|
|
232
|
+
});
|
|
233
|
+
if (!acceptance.accepted) return askFreeText(ask, c.portfolioDescriptionFreeTextPrompt);
|
|
234
|
+
|
|
235
|
+
const draft = await withSpinner(c.portfolioDescriptionDraftingLabel, () => requestDraftDescription(
|
|
236
|
+
{ name: title, technologies, readme: readReadme(root), packageDescription: readPackageDescription(root), accessToken },
|
|
237
|
+
{ endpoint: draftEndpoint },
|
|
238
|
+
));
|
|
239
|
+
if (!draft.ok || !draft.description) return askFreeText(ask, c.portfolioDescriptionFreeTextPrompt);
|
|
240
|
+
|
|
241
|
+
process.stdout.write(`\n ${styleQuestion(c.portfolioDescriptionDraftLabel)}\n ${draft.description}\n`);
|
|
242
|
+
return askWithDefault(ask, c.portfolioDescriptionEditPrompt, draft.description);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function offerRelogin({ ask, c, runLogin }) {
|
|
246
|
+
if (typeof runLogin !== 'function') return null;
|
|
247
|
+
const raw = String(await ask(`\n ${styleQuestion(c.portfolioOfferRelogin)} `)).trim();
|
|
248
|
+
if (!/^(y|yes|s|si|sí)$/i.test(raw)) {
|
|
249
|
+
process.stdout.write(`\n ${c.portfolioRelaunchDeclined}\n\n`);
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
await runLogin([], { ask });
|
|
253
|
+
const fresh = loadAuthSession();
|
|
254
|
+
if (sessionStatus(fresh) !== 'active' || !fresh.hubAccessToken) {
|
|
255
|
+
process.stdout.write(`\n ${c.portfolioReloginFailed}\n\n`);
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
return fresh;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// The whole route.
|
|
262
|
+
async function runAddPortfolio({
|
|
263
|
+
ask, catalog, root, session, stdinIsTTY, input, output, preAccepted = false, deps = {},
|
|
264
|
+
} = {}) {
|
|
265
|
+
const c = catalog.start;
|
|
266
|
+
const runLogin = deps.runLogin;
|
|
267
|
+
|
|
268
|
+
const listEndpoint = getPortfoliosListEndpoint();
|
|
269
|
+
const declareEndpoint = getPortfoliosDeclareEndpoint();
|
|
270
|
+
if (!listEndpoint || !declareEndpoint) {
|
|
271
|
+
process.stderr.write(`\n ${c.portfolioErrorNoEndpoint}\n\n`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const hubAccessToken = session.hubAccessToken;
|
|
276
|
+
if (!hubAccessToken) {
|
|
277
|
+
process.stderr.write(`\n ${c.portfolioNoHubToken}\n\n`);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const listed = await withSpinner(
|
|
282
|
+
c.portfolioDedupCheckingLabel,
|
|
283
|
+
() => requestListPortfolios({ accessToken: session.accessToken, hubAccessToken }, { endpoint: listEndpoint }),
|
|
284
|
+
);
|
|
285
|
+
const existingNames = listed.ok ? listed.portfolios.map((p) => p.name.toLowerCase()) : null;
|
|
286
|
+
if (!listed.ok) process.stdout.write(`\n ${c.portfolioDedupCheckFailed(reasonCopy(listed.reason, c))}\n`);
|
|
287
|
+
|
|
288
|
+
const absRoot = path.resolve(root);
|
|
289
|
+
const defaultTitle = path.basename(absRoot) || 'project';
|
|
290
|
+
|
|
291
|
+
process.stdout.write(`\n ${c.portfolioIntro}\n`);
|
|
292
|
+
|
|
293
|
+
// (1) Title.
|
|
294
|
+
const title = await askWithDefault(ask, c.portfolioTitlePrompt(defaultTitle), defaultTitle);
|
|
295
|
+
if (existingNames && existingNames.includes(title.toLowerCase())) {
|
|
296
|
+
process.stdout.write(`\n ${c.portfolioDuplicateName(title)}\n\n`);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// (2) Type — obligatorio.
|
|
301
|
+
const type = await chooseType({ ask, c, input, output });
|
|
302
|
+
if (!type) {
|
|
303
|
+
process.stdout.write(`\n ${c.portfolioCancelled}\n\n`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// (3) Description.
|
|
308
|
+
const technologies = detectTechnologies(absRoot);
|
|
309
|
+
const description = await resolveDescription({
|
|
310
|
+
ask, catalog, c, stdinIsTTY, root: absRoot, title, technologies, accessToken: session.accessToken, preAccepted,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// (4) URL.
|
|
314
|
+
const detectedUrl = getRemoteUrl(absRoot);
|
|
315
|
+
const url = await askWithDefault(ask, c.portfolioUrlPrompt(detectedUrl), detectedUrl || '');
|
|
316
|
+
|
|
317
|
+
// (5) Skills.
|
|
318
|
+
const chosenSkills = await resolvePortfolioSkills({
|
|
319
|
+
ask, stdinIsTTY, technologies, c, input, output, accessToken: session.accessToken,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// (6) Client — optional.
|
|
323
|
+
const clientName = await askFreeText(ask, c.portfolioClientPrompt);
|
|
324
|
+
const clientDomain = normalizeDomain(await askFreeText(ask, c.portfolioClientWebsitePrompt));
|
|
325
|
+
|
|
326
|
+
// Auto: dates from git history.
|
|
327
|
+
const { startDate, endDate } = getCommitDateRange(absRoot);
|
|
328
|
+
|
|
329
|
+
const payload = {
|
|
330
|
+
name: title,
|
|
331
|
+
type,
|
|
332
|
+
skillIds: chosenSkills.map((s) => s.skillId),
|
|
333
|
+
description,
|
|
334
|
+
url,
|
|
335
|
+
clientName,
|
|
336
|
+
clientDomain,
|
|
337
|
+
startDate,
|
|
338
|
+
endDate,
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const declared = await withSpinner(
|
|
342
|
+
c.portfolioDeclaring,
|
|
343
|
+
() => requestDeclarePortfolio({ ...payload, accessToken: session.accessToken, hubAccessToken }, { endpoint: declareEndpoint }),
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
if (declared.ok) {
|
|
347
|
+
process.stdout.write(`\n ${styleSuccess(c.portfolioDeclaredSuccess(title))}\n\n`);
|
|
348
|
+
// OPT-IN (dueño, 2026-08-12): offer the REST of the detected skills for the Talent's PROFILE, now that the portfolio itself (and whatever skillIds it carried) is safely declared.
|
|
349
|
+
await offerAddableSkillsForProfile({ ask, catalog, c, technologies, session, stdinIsTTY, input, output, runLogin });
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (declared.reason !== 'hub-session-expired') {
|
|
354
|
+
process.stdout.write(`\n ${c.portfolioDeclareFailed(reasonCopy(declared.reason, c))}\n\n`);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
process.stdout.write(`\n ${c.portfolioHubSessionExpired}\n`);
|
|
359
|
+
const relogged = await offerRelogin({ ask, c, runLogin });
|
|
360
|
+
if (!relogged) return;
|
|
361
|
+
process.stdout.write(` ${c.portfolioRelaunchAfterRelogin}\n`);
|
|
362
|
+
const retried = await withSpinner(
|
|
363
|
+
c.portfolioDeclaring,
|
|
364
|
+
() => requestDeclarePortfolio(
|
|
365
|
+
{ ...payload, accessToken: relogged.accessToken, hubAccessToken: relogged.hubAccessToken },
|
|
366
|
+
{ endpoint: declareEndpoint },
|
|
367
|
+
),
|
|
368
|
+
);
|
|
369
|
+
if (retried.ok) {
|
|
370
|
+
process.stdout.write(`\n ${styleSuccess(c.portfolioDeclaredSuccess(title))}\n\n`);
|
|
371
|
+
await offerAddableSkillsForProfile({
|
|
372
|
+
ask, catalog, c, technologies, session: relogged, stdinIsTTY, input, output, runLogin,
|
|
373
|
+
});
|
|
374
|
+
} else {
|
|
375
|
+
process.stdout.write(`\n ${c.portfolioDeclareFailed(reasonCopy(retried.reason, c))}\n\n`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
module.exports = {
|
|
380
|
+
runAddPortfolio,
|
|
381
|
+
readPackageDescription,
|
|
382
|
+
readReadme,
|
|
383
|
+
askWithDefault,
|
|
384
|
+
chooseType,
|
|
385
|
+
typeOptions,
|
|
386
|
+
normalizeDomain,
|
|
387
|
+
offerAddableSkillsForProfile,
|
|
388
|
+
};
|