shakers-cli 0.0.0-stage → 0.14.1
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,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
|
+
};
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getUsageDiscoveredInventoryEndpoint, getSkillsDeclareEndpoint, getPortfoliosListEndpoint, getPortfolioUpdateEndpoint } = require('./config');
|
|
4
|
+
const { requestDeclareSkill } = require('./skills-client');
|
|
5
|
+
const { requestDiscoveredInventory } = require('./inventory-client');
|
|
6
|
+
const { requestListPortfolios, requestUpdatePortfolioSkills } = require('./portfolio-client');
|
|
7
|
+
const { confirmDisclaimerAcceptance } = require('./certify-disclaimer');
|
|
8
|
+
const { runInteractiveMultiSelect } = require('./interactive-select');
|
|
9
|
+
const { loadAuthSession, sessionStatus } = require('./auth-session-store');
|
|
10
|
+
const { styleSuccess } = require('./ansi');
|
|
11
|
+
|
|
12
|
+
// `start`'s "Añadir skills" route (talents-ai-score, ADR-054/055).
|
|
13
|
+
|
|
14
|
+
// Maps a technical failure reason to the localized `start.errorReason` copy.
|
|
15
|
+
function reasonCopy(reason, c) {
|
|
16
|
+
if (typeof reason === 'string' && reason.startsWith('http-')) {
|
|
17
|
+
return c.errorReasonHttp(reason.slice('http-'.length));
|
|
18
|
+
}
|
|
19
|
+
return c.errorReason[reason] || c.errorReason.generic;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseMultiIndices(raw, count) {
|
|
23
|
+
const trimmed = String(raw == null ? '' : raw).trim();
|
|
24
|
+
if (!trimmed) return null;
|
|
25
|
+
const parts = trimmed.split(/[\s,]+/).filter(Boolean);
|
|
26
|
+
const idx = new Set();
|
|
27
|
+
for (const part of parts) {
|
|
28
|
+
if (!/^\d+$/.test(part)) return null;
|
|
29
|
+
const n = Number(part);
|
|
30
|
+
if (n < 1 || n > count) return null;
|
|
31
|
+
idx.add(n - 1);
|
|
32
|
+
}
|
|
33
|
+
return idx.size > 0 ? [...idx].sort((a, b) => a - b) : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Numbered fallback prompt for the multi-select, mirroring
|
|
37
|
+
// bin/certify.js#askNumberedChoice's shape but for a LIST of indices.
|
|
38
|
+
async function askMultiIndices(ask, promptText, count) {
|
|
39
|
+
const raw = await ask(promptText);
|
|
40
|
+
return parseMultiIndices(raw, count);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function pickSkills({ ask, stdinIsTTY, candidates, c, input, output }) {
|
|
44
|
+
if (stdinIsTTY && typeof ask.suspend === 'function') {
|
|
45
|
+
ask.suspend();
|
|
46
|
+
const picked = await runInteractiveMultiSelect({
|
|
47
|
+
items: candidates,
|
|
48
|
+
labelFor: (s) => `${s.skillName}${s.technology ? ` (${s.technology})` : ''}`,
|
|
49
|
+
header: c.addSkillsSelectHeading,
|
|
50
|
+
hint: c.addSkillsSelectHint,
|
|
51
|
+
...(input ? { input } : {}),
|
|
52
|
+
...(output ? { output } : {}),
|
|
53
|
+
});
|
|
54
|
+
ask.resume();
|
|
55
|
+
return picked || [];
|
|
56
|
+
}
|
|
57
|
+
process.stdout.write(`\n ${c.addSkillsSelectHeading}\n`);
|
|
58
|
+
candidates.forEach((s, i) => {
|
|
59
|
+
process.stdout.write(` ${i + 1}) ${s.skillName}${s.technology ? ` (${s.technology})` : ''}\n`);
|
|
60
|
+
});
|
|
61
|
+
const idxs = await askMultiIndices(ask, ` ${c.addSkillsSelectPrompt(candidates.length)}`, candidates.length);
|
|
62
|
+
return idxs ? idxs.map((i) => candidates[i]) : [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Declares every chosen skill against the CURRENT `hubAccessToken`.
|
|
66
|
+
async function declareAll(chosen, { accessToken, hubAccessToken }, endpoint, c) {
|
|
67
|
+
const pending = [];
|
|
68
|
+
const declared = [];
|
|
69
|
+
let hubSessionExpired = false;
|
|
70
|
+
let succeeded = 0;
|
|
71
|
+
for (const skill of chosen) {
|
|
72
|
+
if (hubSessionExpired) {
|
|
73
|
+
pending.push(skill);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const r = await requestDeclareSkill({ skillId: skill.skillId, accessToken, hubAccessToken }, { endpoint });
|
|
77
|
+
if (r.ok) {
|
|
78
|
+
process.stdout.write(` ${styleSuccess(c.addSkillsDeclaredOne(skill.skillName))}\n`);
|
|
79
|
+
succeeded += 1;
|
|
80
|
+
declared.push(skill);
|
|
81
|
+
} else if (r.reason === 'hub-session-expired') {
|
|
82
|
+
hubSessionExpired = true;
|
|
83
|
+
pending.push(skill);
|
|
84
|
+
} else {
|
|
85
|
+
process.stdout.write(` ${c.addSkillsDeclareFailed(skill.skillName, reasonCopy(r.reason, c))}\n`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { pending, hubSessionExpired, succeeded, declared };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function pickPortfolios({ ask, stdinIsTTY, portfolios, heading, c, input, output }) {
|
|
92
|
+
if (stdinIsTTY && typeof ask.suspend === 'function') {
|
|
93
|
+
ask.suspend();
|
|
94
|
+
const picked = await runInteractiveMultiSelect({
|
|
95
|
+
items: portfolios,
|
|
96
|
+
labelFor: (p) => p.name,
|
|
97
|
+
header: heading,
|
|
98
|
+
hint: c.addSkillsRelateSelectHint,
|
|
99
|
+
...(input ? { input } : {}),
|
|
100
|
+
...(output ? { output } : {}),
|
|
101
|
+
});
|
|
102
|
+
ask.resume();
|
|
103
|
+
return picked || [];
|
|
104
|
+
}
|
|
105
|
+
process.stdout.write(`\n ${heading}\n`);
|
|
106
|
+
portfolios.forEach((p, i) => {
|
|
107
|
+
process.stdout.write(` ${i + 1}) ${p.name}\n`);
|
|
108
|
+
});
|
|
109
|
+
const idxs = await askMultiIndices(ask, ` ${c.addSkillsRelateSelectPrompt(portfolios.length)}`, portfolios.length);
|
|
110
|
+
return idxs ? idxs.map((i) => portfolios[i]) : [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function relateSkillToPortfolios({ ask, stdinIsTTY, skill, portfolios, hubAccessToken, updateEndpoint, c, input, output }) {
|
|
114
|
+
const wantsRelate = /^(y|yes|s|si|sí)$/i.test(String(await ask(`\n ${c.addSkillsRelateAsk(skill.skillName)} `)).trim());
|
|
115
|
+
if (!wantsRelate) {
|
|
116
|
+
process.stdout.write(` ${c.addSkillsRelateSkipped(skill.skillName)}\n`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const relateHeading = c.addSkillsRelateSelectHeading(skill.skillName);
|
|
120
|
+
const chosenPortfolios = await pickPortfolios({ ask, stdinIsTTY, portfolios, heading: relateHeading, c, input, output });
|
|
121
|
+
if (!chosenPortfolios || chosenPortfolios.length === 0) {
|
|
122
|
+
process.stdout.write(` ${c.addSkillsRelateSkipped(skill.skillName)}\n`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
for (const portfolio of chosenPortfolios) {
|
|
126
|
+
const skillIds = Array.from(new Set([...(portfolio.skillIds || []), skill.skillId]));
|
|
127
|
+
const r = await requestUpdatePortfolioSkills({ portfolioId: portfolio.id, skillIds, hubAccessToken }, { endpoint: updateEndpoint });
|
|
128
|
+
if (r.ok) {
|
|
129
|
+
portfolio.skillIds = skillIds;
|
|
130
|
+
process.stdout.write(`${styleSuccess(c.addSkillsRelated(skill.skillName, portfolio.name))}\n`);
|
|
131
|
+
} else {
|
|
132
|
+
process.stdout.write(`${c.addSkillsRelateFailed(skill.skillName, portfolio.name, reasonCopy(r.reason, c))}\n`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function offerRelateToPortfolios({ ask, stdinIsTTY, declared, session, c, input, output }) {
|
|
138
|
+
if (!declared || declared.length === 0) return;
|
|
139
|
+
const listEndpoint = getPortfoliosListEndpoint();
|
|
140
|
+
const updateEndpoint = getPortfolioUpdateEndpoint();
|
|
141
|
+
if (!listEndpoint || !updateEndpoint) return;
|
|
142
|
+
|
|
143
|
+
const listed = await requestListPortfolios(
|
|
144
|
+
{ accessToken: session.accessToken, hubAccessToken: session.hubAccessToken },
|
|
145
|
+
{ endpoint: listEndpoint },
|
|
146
|
+
);
|
|
147
|
+
if (!listed.ok || listed.portfolios.length === 0) {
|
|
148
|
+
process.stdout.write(`\n ${c.addSkillsRelateNoPortfolios}\n`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const skill of declared) {
|
|
153
|
+
await relateSkillToPortfolios({
|
|
154
|
+
ask, stdinIsTTY, skill, portfolios: listed.portfolios, hubAccessToken: session.hubAccessToken, updateEndpoint, c, input, output,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// The whole route.
|
|
160
|
+
async function runAddSkills({
|
|
161
|
+
ask, catalog, root, session, stdinIsTTY, input, output, preAccepted = false, deps = {},
|
|
162
|
+
} = {}) {
|
|
163
|
+
const c = catalog.start;
|
|
164
|
+
const runLogin = deps.runLogin;
|
|
165
|
+
|
|
166
|
+
const inventoryEndpoint = getUsageDiscoveredInventoryEndpoint();
|
|
167
|
+
const declareEndpoint = getSkillsDeclareEndpoint();
|
|
168
|
+
if (!inventoryEndpoint || !declareEndpoint) {
|
|
169
|
+
process.stderr.write(`\n ${c.addSkillsErrorNoEndpoint}\n\n`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const acceptance = await confirmDisclaimerAcceptance({ ask, catalog, stdinIsTTY, preAccepted });
|
|
174
|
+
if (!acceptance.accepted) return; // confirmDisclaimerAcceptance already printed why
|
|
175
|
+
|
|
176
|
+
const inventory = await requestDiscoveredInventory(
|
|
177
|
+
{ accessToken: session.accessToken },
|
|
178
|
+
{ endpoint: inventoryEndpoint },
|
|
179
|
+
);
|
|
180
|
+
if (!inventory.ok) {
|
|
181
|
+
process.stderr.write(`\n ${c.addSkillsResolveErrorIntro} ${reasonCopy(inventory.reason, c)}\n\n`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const candidates = inventory.skills.map((skill) => ({
|
|
186
|
+
skillId: skill.skillId,
|
|
187
|
+
skillName: skill.skillName,
|
|
188
|
+
technology: skill.technologies.join(', '),
|
|
189
|
+
}));
|
|
190
|
+
if (candidates.length === 0) {
|
|
191
|
+
process.stdout.write(`\n ${c.addSkillsNoneMatched}\n\n`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const hubAccessToken = session.hubAccessToken;
|
|
196
|
+
if (!hubAccessToken) {
|
|
197
|
+
process.stderr.write(`\n ${c.addSkillsNoHubToken}\n\n`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const chosen = await pickSkills({ ask, stdinIsTTY, candidates, c, input, output });
|
|
202
|
+
if (!chosen || chosen.length === 0) {
|
|
203
|
+
process.stdout.write(`\n ${c.addSkillsNoneChosen}\n\n`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
process.stdout.write('\n');
|
|
208
|
+
const { pending, hubSessionExpired, succeeded, declared } = await declareAll(
|
|
209
|
+
chosen, { accessToken: session.accessToken, hubAccessToken }, declareEndpoint, c,
|
|
210
|
+
);
|
|
211
|
+
if (!hubSessionExpired) {
|
|
212
|
+
printProfileBoost(succeeded, c);
|
|
213
|
+
await offerRelateToPortfolios({ ask, stdinIsTTY, declared, session, c, input, output });
|
|
214
|
+
process.stdout.write('\n');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
process.stdout.write(`\n ${c.addSkillsHubSessionExpired}\n`);
|
|
219
|
+
const relogged = await offerRelogin({ ask, c, runLogin });
|
|
220
|
+
if (!relogged) return;
|
|
221
|
+
process.stdout.write(` ${c.addSkillsRelaunchAfterRelogin}\n\n`);
|
|
222
|
+
const retried = await declareAll(pending, { accessToken: relogged.accessToken, hubAccessToken: relogged.hubAccessToken }, declareEndpoint, c);
|
|
223
|
+
printProfileBoost(succeeded + retried.succeeded, c);
|
|
224
|
+
await offerRelateToPortfolios({
|
|
225
|
+
ask, stdinIsTTY, declared: [...declared, ...retried.declared], session: relogged, c, input, output,
|
|
226
|
+
});
|
|
227
|
+
process.stdout.write('\n');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function printProfileBoost(succeeded, c) {
|
|
231
|
+
if (succeeded > 0) process.stdout.write(` ${c.addSkillsProfileBoost}\n`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Asks the Talent whether to re-login, right there, and runs it if they say yes.
|
|
235
|
+
async function offerRelogin({ ask, c, runLogin }) {
|
|
236
|
+
if (typeof runLogin !== 'function') return null;
|
|
237
|
+
const raw = String(await ask(`\n ${c.addSkillsOfferRelogin} `)).trim();
|
|
238
|
+
if (!/^(y|yes|s|si|sí)$/i.test(raw)) {
|
|
239
|
+
process.stdout.write(`\n ${c.addSkillsRelaunchDeclined}\n\n`);
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
await runLogin([], { ask });
|
|
243
|
+
const fresh = loadAuthSession();
|
|
244
|
+
if (sessionStatus(fresh) !== 'active' || !fresh.hubAccessToken) {
|
|
245
|
+
process.stdout.write(`\n ${c.addSkillsReloginFailed}\n\n`);
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
return fresh;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
module.exports = {
|
|
252
|
+
runAddSkills,
|
|
253
|
+
parseMultiIndices,
|
|
254
|
+
reasonCopy,
|
|
255
|
+
declareAll,
|
|
256
|
+
printProfileBoost,
|
|
257
|
+
offerRelateToPortfolios,
|
|
258
|
+
// Exported for src/start-add-agents.js's own relate step (ADR-041): the multi-select over the Talent's portfolios, reused verbatim.
|
|
259
|
+
pickPortfolios,
|
|
260
|
+
};
|