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,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Thin certs adapter: exchange a created onboarding interview for LiveKit room credentials so the CLI can join over text (lk.chat).
|
|
4
|
+
|
|
5
|
+
const { postJsonWithTimeout } = require('./backend-request');
|
|
6
|
+
const { reasonForError } = require('./onboarding-client');
|
|
7
|
+
const { toUpperLang } = require('./lang-codes');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 20000;
|
|
10
|
+
// Sub-path appended to `{base}/{interviewId}/` — the real post-merge certs
|
|
11
|
+
// route (sibling of `text-session`). Change this ONE constant if it moves.
|
|
12
|
+
const LIVEKIT_SESSION_PATH = 'livekit-text-session';
|
|
13
|
+
// Completion route: `PATCH {base}/{interviewId}/complete`.
|
|
14
|
+
const COMPLETE_SESSION_PATH = 'complete';
|
|
15
|
+
|
|
16
|
+
function parseJson(raw) {
|
|
17
|
+
try {
|
|
18
|
+
return raw ? JSON.parse(raw) : null;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function envelopeData(raw) {
|
|
25
|
+
const json = parseJson(raw);
|
|
26
|
+
if (!json || typeof json !== 'object' || json.status !== 'OK') return undefined;
|
|
27
|
+
return json.data;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function bearerHeaders(accessToken) {
|
|
31
|
+
const headers = {};
|
|
32
|
+
if (typeof accessToken === 'string' && accessToken) headers.Authorization = `Bearer ${accessToken}`;
|
|
33
|
+
return headers;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function pickString(obj, keys) {
|
|
37
|
+
for (const k of keys) {
|
|
38
|
+
const v = obj && obj[k];
|
|
39
|
+
if (typeof v === 'string' && v) return v;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function requestStartOnboardingLivekitSession(
|
|
45
|
+
{ interviewId, language, accessToken } = {},
|
|
46
|
+
{ base, path = LIVEKIT_SESSION_PATH, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
|
|
47
|
+
) {
|
|
48
|
+
if (!base) return { ok: false, reason: 'no-endpoint' };
|
|
49
|
+
if (!interviewId) return { ok: false, reason: 'no-interview' };
|
|
50
|
+
|
|
51
|
+
const url = `${base.replace(/\/+$/, '')}/${encodeURIComponent(interviewId)}/${path}`;
|
|
52
|
+
const body = { language: toUpperLang(language) };
|
|
53
|
+
let res;
|
|
54
|
+
try {
|
|
55
|
+
res = await postJsonWithTimeout(url, body, timeoutMs, 'POST', null, bearerHeaders(accessToken));
|
|
56
|
+
} catch (e) {
|
|
57
|
+
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
58
|
+
}
|
|
59
|
+
if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
|
|
60
|
+
|
|
61
|
+
const data = envelopeData(res.raw);
|
|
62
|
+
if (data === undefined || !data || typeof data !== 'object') return { ok: false, reason: 'bad-response' };
|
|
63
|
+
|
|
64
|
+
const livekitUrl = pickString(data, ['livekitUrl', 'url', 'wsUrl', 'serverUrl']);
|
|
65
|
+
const token = pickString(data, ['token', 'accessToken', 'participantToken']);
|
|
66
|
+
const roomName = pickString(data, ['roomName', 'room', 'room_name']);
|
|
67
|
+
const closingMessage = pickString(data, ['closingMessage', 'closing']);
|
|
68
|
+
const resolvedId = pickString(data, ['interviewId', 'id']) || interviewId;
|
|
69
|
+
|
|
70
|
+
if (!livekitUrl || !token) return { ok: false, reason: 'bad-response' };
|
|
71
|
+
return { ok: true, livekitUrl, token, roomName: roomName || null, interviewId: resolvedId, closingMessage: closingMessage || null };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Persist the onboarding interview's transcript on session end (the LiveKit text flow is the one path that was not sending it; certs already stores + serves it).
|
|
75
|
+
async function requestCompleteOnboardingLivekitSession(
|
|
76
|
+
{ interviewId, durationSeconds, transcripts, accessToken } = {},
|
|
77
|
+
{ base, path = COMPLETE_SESSION_PATH, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
|
|
78
|
+
) {
|
|
79
|
+
if (!base) return { ok: false, reason: 'no-endpoint' };
|
|
80
|
+
if (!interviewId) return { ok: false, reason: 'no-interview' };
|
|
81
|
+
if (!Array.isArray(transcripts) || transcripts.length === 0) return { ok: false, reason: 'no-transcript' };
|
|
82
|
+
|
|
83
|
+
const url = `${base.replace(/\/+$/, '')}/${encodeURIComponent(interviewId)}/${path}`;
|
|
84
|
+
const body = { transcripts };
|
|
85
|
+
if (Number.isInteger(durationSeconds) && durationSeconds >= 0) body.durationSeconds = durationSeconds;
|
|
86
|
+
|
|
87
|
+
let res;
|
|
88
|
+
try {
|
|
89
|
+
res = await postJsonWithTimeout(url, body, timeoutMs, 'PATCH', null, bearerHeaders(accessToken));
|
|
90
|
+
} catch (e) {
|
|
91
|
+
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
92
|
+
}
|
|
93
|
+
if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
|
|
94
|
+
return { ok: true };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = {
|
|
98
|
+
requestStartOnboardingLivekitSession,
|
|
99
|
+
requestCompleteOnboardingLivekitSession,
|
|
100
|
+
LIVEKIT_SESSION_PATH,
|
|
101
|
+
COMPLETE_SESSION_PATH,
|
|
102
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { postJsonWithTimeout } = require('./backend-request');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 12000;
|
|
6
|
+
|
|
7
|
+
function parseJson(raw) {
|
|
8
|
+
try {
|
|
9
|
+
return raw ? JSON.parse(raw) : null;
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function authHeaders({ accessToken } = {}) {
|
|
16
|
+
const headers = {};
|
|
17
|
+
if (typeof accessToken === 'string' && accessToken) headers.Authorization = `Bearer ${accessToken}`;
|
|
18
|
+
return headers;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function requestOnboardingStatus({ accessToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
22
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
23
|
+
|
|
24
|
+
let res;
|
|
25
|
+
try {
|
|
26
|
+
res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, authHeaders({ accessToken }));
|
|
27
|
+
} catch (e) {
|
|
28
|
+
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (res.status < 200 || res.status >= 300) {
|
|
32
|
+
return { ok: false, reason: `http-${res.status}` };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const json = parseJson(res.raw);
|
|
36
|
+
const data = json && typeof json === 'object' ? json.data : null;
|
|
37
|
+
if (!data || typeof data !== 'object') return { ok: false, reason: 'bad-response' };
|
|
38
|
+
|
|
39
|
+
return { ok: true, completed: data.completed === true };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { requestOnboardingStatus };
|
package/src/open-file.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
|
|
5
|
+
// Zero-dependency, best-effort "open this file in the OS default app" (ADR-016, used by the `report` command to open the generated HTML in the browser).
|
|
6
|
+
function openPath(target) {
|
|
7
|
+
let cmd;
|
|
8
|
+
let args;
|
|
9
|
+
if (process.platform === 'darwin') {
|
|
10
|
+
cmd = 'open';
|
|
11
|
+
args = [target];
|
|
12
|
+
} else if (process.platform === 'win32') {
|
|
13
|
+
// `start` needs an (empty) title arg first when the target may be quoted.
|
|
14
|
+
cmd = 'cmd';
|
|
15
|
+
args = ['/c', 'start', '', target];
|
|
16
|
+
} else {
|
|
17
|
+
cmd = 'xdg-open';
|
|
18
|
+
args = [target];
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
|
|
22
|
+
// An absent opener (ENOENT) surfaces as an async 'error' — swallow it so it
|
|
23
|
+
// never becomes an unhandled exception that breaks the REPL.
|
|
24
|
+
child.on('error', () => {});
|
|
25
|
+
child.unref();
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { openPath };
|
package/src/osc-link.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// OSC 8 terminal hyperlink helper (talents-ai-score, user request: links were printed as plain text and could NOT be clicked in iTerm2).
|
|
4
|
+
|
|
5
|
+
const OSC = '\x1b]8;;';
|
|
6
|
+
const ST = '\x1b\\';
|
|
7
|
+
|
|
8
|
+
// Wrap `label` as an OSC 8 hyperlink to `url`.
|
|
9
|
+
function oscLink(url, label) {
|
|
10
|
+
const target = url == null ? '' : String(url);
|
|
11
|
+
const text = label == null || label === '' ? target : String(label);
|
|
12
|
+
if (!target) return text;
|
|
13
|
+
return `${OSC}${target}${ST}${text}${OSC}${ST}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = { oscLink };
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { postJsonWithTimeout, postMultipartWithTimeout } = require('./backend-request');
|
|
4
|
+
const { reasonForError, WORKS_CODE_TO_REASON } = require('./skills-client');
|
|
5
|
+
|
|
6
|
+
// Talent-portfolio client (talents-ai-score, ADR-059 — `start`'s "Add project to portfolio" route, src/start-add-portfolio.js).
|
|
7
|
+
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = 20000;
|
|
9
|
+
|
|
10
|
+
function parseJson(raw) {
|
|
11
|
+
try {
|
|
12
|
+
return raw ? JSON.parse(raw) : null;
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function authHeaders({ accessToken, hubAccessToken } = {}) {
|
|
19
|
+
const headers = {};
|
|
20
|
+
if (typeof accessToken === 'string' && accessToken) headers.Authorization = `Bearer ${accessToken}`;
|
|
21
|
+
if (typeof hubAccessToken === 'string' && hubAccessToken) headers['X-Hub-Token'] = hubAccessToken;
|
|
22
|
+
return headers;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function hubHeaders({ hubAccessToken } = {}) {
|
|
26
|
+
const headers = {};
|
|
27
|
+
if (typeof hubAccessToken === 'string' && hubAccessToken) headers.Authorization = `Bearer ${hubAccessToken}`;
|
|
28
|
+
return headers;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// `GET .../portfolios`.
|
|
32
|
+
async function requestListPortfolios({ accessToken, hubAccessToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
33
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
34
|
+
if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
|
|
35
|
+
|
|
36
|
+
let res;
|
|
37
|
+
try {
|
|
38
|
+
res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, authHeaders({ accessToken, hubAccessToken }));
|
|
39
|
+
} catch (e) {
|
|
40
|
+
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (res.status < 200 || res.status >= 300) {
|
|
44
|
+
return { ok: false, reason: reasonForError(res.status, res.raw) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const json = parseJson(res.raw);
|
|
48
|
+
const data = json && typeof json === 'object' && Array.isArray(json.data) ? json.data : null;
|
|
49
|
+
if (!data) return { ok: false, reason: 'bad-response' };
|
|
50
|
+
|
|
51
|
+
const portfolios = data
|
|
52
|
+
.map((entry) => ({
|
|
53
|
+
id: entry && typeof entry.id === 'string' ? entry.id : null,
|
|
54
|
+
name: entry && typeof entry.name === 'string' ? entry.name : null,
|
|
55
|
+
type: entry && typeof entry.type === 'string' ? entry.type : null,
|
|
56
|
+
skillIds: entry && Array.isArray(entry.skillIds) ? entry.skillIds : [],
|
|
57
|
+
startDate: entry && typeof entry.startDate === 'string' ? entry.startDate : null,
|
|
58
|
+
endDate: entry && typeof entry.endDate === 'string' ? entry.endDate : null,
|
|
59
|
+
isCurrent: entry ? entry.isCurrent === true : false,
|
|
60
|
+
}))
|
|
61
|
+
.filter((p) => p.name !== null);
|
|
62
|
+
return { ok: true, portfolios };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// `POST .../portfolios/declare`.
|
|
66
|
+
async function requestDeclarePortfolio(
|
|
67
|
+
{ name, type, skillIds, description, url, clientName, clientDomain, startDate, endDate, accessToken, hubAccessToken } = {},
|
|
68
|
+
{ endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
|
|
69
|
+
) {
|
|
70
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
71
|
+
if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
|
|
72
|
+
|
|
73
|
+
const body = { name, type };
|
|
74
|
+
if (Array.isArray(skillIds) && skillIds.length > 0) body.skillIds = skillIds;
|
|
75
|
+
if (typeof description === 'string' && description) body.description = description;
|
|
76
|
+
if (typeof url === 'string' && url) body.url = url;
|
|
77
|
+
if (typeof clientName === 'string' && clientName) body.clientName = clientName;
|
|
78
|
+
if (typeof clientDomain === 'string' && clientDomain) body.clientDomain = clientDomain;
|
|
79
|
+
if (typeof startDate === 'string' && startDate) body.startDate = startDate;
|
|
80
|
+
if (typeof endDate === 'string' && endDate) body.endDate = endDate;
|
|
81
|
+
body.generatedWith = 'AI';
|
|
82
|
+
|
|
83
|
+
let res;
|
|
84
|
+
try {
|
|
85
|
+
res = await postJsonWithTimeout(endpoint, body, timeoutMs, 'POST', null, authHeaders({ accessToken, hubAccessToken }));
|
|
86
|
+
} catch (e) {
|
|
87
|
+
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (res.status < 200 || res.status >= 300) {
|
|
91
|
+
return { ok: false, reason: reasonForError(res.status, res.raw) };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const json = parseJson(res.raw);
|
|
95
|
+
if (!json || json.status !== 'OK') return { ok: false, reason: 'bad-response' };
|
|
96
|
+
return { ok: true };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// `POST .../portfolios/draft-description`.
|
|
100
|
+
async function requestDraftDescription(
|
|
101
|
+
{ name, technologies, readme, packageDescription, accessToken } = {},
|
|
102
|
+
{ endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
|
|
103
|
+
) {
|
|
104
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
105
|
+
|
|
106
|
+
const body = {
|
|
107
|
+
consent: true,
|
|
108
|
+
name,
|
|
109
|
+
technologies: Array.isArray(technologies) ? technologies.filter((t) => typeof t === 'string' && t) : [],
|
|
110
|
+
};
|
|
111
|
+
if (typeof readme === 'string' && readme) body.readme = readme;
|
|
112
|
+
if (typeof packageDescription === 'string' && packageDescription) body.packageDescription = packageDescription;
|
|
113
|
+
|
|
114
|
+
let res;
|
|
115
|
+
try {
|
|
116
|
+
res = await postJsonWithTimeout(endpoint, body, timeoutMs, 'POST', null, authHeaders({ accessToken }));
|
|
117
|
+
} catch (e) {
|
|
118
|
+
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (res.status < 200 || res.status >= 300) {
|
|
122
|
+
return { ok: false, reason: reasonForError(res.status, res.raw) };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const json = parseJson(res.raw);
|
|
126
|
+
const description = json && json.status === 'OK' && json.data && typeof json.data.description === 'string'
|
|
127
|
+
? json.data.description
|
|
128
|
+
: null;
|
|
129
|
+
if (!description) return { ok: false, reason: 'bad-response' };
|
|
130
|
+
return { ok: true, description };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function requestUpdatePortfolioSkills(
|
|
134
|
+
{ portfolioId, skillIds, hubAccessToken } = {},
|
|
135
|
+
{ endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
|
|
136
|
+
) {
|
|
137
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
138
|
+
if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
|
|
139
|
+
if (!portfolioId) return { ok: false, reason: 'no-portfolio' };
|
|
140
|
+
|
|
141
|
+
const url = `${endpoint.replace(/\/+$/, '')}/${encodeURIComponent(portfolioId)}`;
|
|
142
|
+
const fields = { skillIds: (Array.isArray(skillIds) ? skillIds : []).join(',') };
|
|
143
|
+
let res;
|
|
144
|
+
try {
|
|
145
|
+
res = await postMultipartWithTimeout(url, fields, timeoutMs, 'PATCH', hubHeaders({ hubAccessToken }));
|
|
146
|
+
} catch (e) {
|
|
147
|
+
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (res.status < 200 || res.status >= 300) {
|
|
151
|
+
return { ok: false, reason: reasonForError(res.status, res.raw) };
|
|
152
|
+
}
|
|
153
|
+
const json = parseJson(res.raw);
|
|
154
|
+
if (!json || json.status !== 'OK') return { ok: false, reason: 'bad-response' };
|
|
155
|
+
return { ok: true };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = {
|
|
159
|
+
requestListPortfolios,
|
|
160
|
+
requestDeclarePortfolio,
|
|
161
|
+
requestDraftDescription,
|
|
162
|
+
requestUpdatePortfolioSkills,
|
|
163
|
+
WORKS_CODE_TO_REASON,
|
|
164
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFileSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
// Git-derived signals for the "Add project to portfolio" route (talents-ai- score, ADR-059): the remote URL (step 4) and the first/last-commit date range (auto `startDate`/`endDate`).
|
|
6
|
+
|
|
7
|
+
const GIT_TIMEOUT_MS = 15000;
|
|
8
|
+
|
|
9
|
+
function runGit(root, args) {
|
|
10
|
+
try {
|
|
11
|
+
return execFileSync('git', ['-C', root, ...args], {
|
|
12
|
+
encoding: 'utf8',
|
|
13
|
+
timeout: GIT_TIMEOUT_MS,
|
|
14
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
15
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
16
|
+
});
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getRemoteUrl(root) {
|
|
23
|
+
const raw = runGit(root, ['remote', 'get-url', 'origin']);
|
|
24
|
+
if (!raw || !raw.trim()) return null;
|
|
25
|
+
return normalizeRemoteUrl(raw.trim());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeRemoteUrl(url) {
|
|
29
|
+
const stripped = url.replace(/\.git$/, '');
|
|
30
|
+
const scp = stripped.match(/^[^@/]+@([^:]+):(.+)$/); // git@host:owner/repo
|
|
31
|
+
if (scp) return `https://${scp[1]}/${scp[2]}`;
|
|
32
|
+
try {
|
|
33
|
+
const u = new URL(stripped);
|
|
34
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:') return null; // e.g. an ssh:// scheme
|
|
35
|
+
return `${u.protocol}//${u.host}${u.pathname}`.replace(/\/+$/, '');
|
|
36
|
+
} catch {
|
|
37
|
+
return null; // not a URL this function recognizes — never guess
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getCommitDateRange(root) {
|
|
42
|
+
const endRaw = runGit(root, ['log', '-1', '--format=%aI']);
|
|
43
|
+
const endDate = isoDateOnly(endRaw);
|
|
44
|
+
|
|
45
|
+
const rootShas = runGit(root, ['rev-list', '--max-parents=0', 'HEAD']);
|
|
46
|
+
const firstSha = rootShas && rootShas.trim() ? rootShas.trim().split('\n')[0] : null;
|
|
47
|
+
const startRaw = firstSha ? runGit(root, ['show', '-s', '--format=%aI', firstSha]) : null;
|
|
48
|
+
const startDate = isoDateOnly(startRaw);
|
|
49
|
+
|
|
50
|
+
return { startDate, endDate };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isoDateOnly(raw) {
|
|
54
|
+
const s = raw && raw.trim();
|
|
55
|
+
if (!s) return null;
|
|
56
|
+
const d = new Date(s);
|
|
57
|
+
if (Number.isNaN(d.getTime())) return null;
|
|
58
|
+
return s.slice(0, 10); // %aI is already ISO 8601 (e.g. 2026-08-01T12:00:00+02:00); take the date part
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { getRemoteUrl, normalizeRemoteUrl, getCommitDateRange };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Portfolio entries client (hub `works/me/portfolios`, talent Bearer). ONE
|
|
4
|
+
// entity with a `type` discriminator: EXPERIENCE (work history) vs PORTFOLIO
|
|
5
|
+
// (showcase pieces). `experiences` and `portfolios` are the two `?type=` subsets.
|
|
6
|
+
|
|
7
|
+
const { postJsonWithTimeout } = require('./backend-request');
|
|
8
|
+
const { reasonForError } = require('./onboarding-client');
|
|
9
|
+
|
|
10
|
+
const DEFAULT_TIMEOUT_MS = 20000;
|
|
11
|
+
const TYPES = { experiences: 'EXPERIENCE', portfolios: 'PORTFOLIO' };
|
|
12
|
+
|
|
13
|
+
function parseJson(raw) {
|
|
14
|
+
try {
|
|
15
|
+
return raw ? JSON.parse(raw) : null;
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function bearerHeaders(accessToken) {
|
|
22
|
+
const headers = {};
|
|
23
|
+
if (typeof accessToken === 'string' && accessToken) headers.Authorization = `Bearer ${accessToken}`;
|
|
24
|
+
return headers;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function str(v) {
|
|
28
|
+
return typeof v === 'string' && v ? v : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Keep only the human-useful fields (+ id/projectId for --json/tools).
|
|
32
|
+
function normalizePortfolio(raw) {
|
|
33
|
+
const it = raw && typeof raw === 'object' ? raw : {};
|
|
34
|
+
const skills = Array.isArray(it.skillIds)
|
|
35
|
+
? it.skillIds.map((s) => (s && typeof s === 'object' ? str(s.name) : str(s))).filter(Boolean)
|
|
36
|
+
: [];
|
|
37
|
+
return {
|
|
38
|
+
id: str(it.id),
|
|
39
|
+
name: str(it.name),
|
|
40
|
+
company: str(it.clientName),
|
|
41
|
+
startDate: it.startDate ? String(it.startDate) : null,
|
|
42
|
+
endDate: it.endDate ? String(it.endDate) : null,
|
|
43
|
+
isCurrent: it.isCurrent === true,
|
|
44
|
+
location: str(it.location),
|
|
45
|
+
description: str(it.aboutDescription),
|
|
46
|
+
url: str(it.aboutUrl),
|
|
47
|
+
skills,
|
|
48
|
+
projectId: str(it.projectId),
|
|
49
|
+
generatedWith: str(it.generatedWith),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function requestPortfolios({ hubAccessToken } = {}, { endpoint, type, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
54
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
55
|
+
if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
|
|
56
|
+
const url = type ? `${endpoint}${endpoint.includes('?') ? '&' : '?'}type=${encodeURIComponent(type)}` : endpoint;
|
|
57
|
+
let res;
|
|
58
|
+
try {
|
|
59
|
+
res = await postJsonWithTimeout(url, null, timeoutMs, 'GET', null, bearerHeaders(hubAccessToken));
|
|
60
|
+
} catch (e) {
|
|
61
|
+
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
62
|
+
}
|
|
63
|
+
if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
|
|
64
|
+
const env = parseJson(res.raw);
|
|
65
|
+
const data = env && env.status === 'OK' ? env.data : env;
|
|
66
|
+
const rows = Array.isArray(data) ? data : (Array.isArray(data && data.items) ? data.items : null);
|
|
67
|
+
if (!rows) return { ok: false, reason: 'bad-response' };
|
|
68
|
+
return { ok: true, items: rows.map(normalizePortfolio) };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// `kind` = 'experiences' | 'portfolios' → the matching `type` filter.
|
|
72
|
+
async function fetchPortfolios(deps = {}, { hubAccessToken, kind, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
73
|
+
const getEndpoint = deps.getMePortfoliosEndpoint || require('./config').getMePortfoliosEndpoint;
|
|
74
|
+
const request = deps.requestPortfolios || requestPortfolios;
|
|
75
|
+
const endpoint = getEndpoint();
|
|
76
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
77
|
+
return request({ hubAccessToken }, { endpoint, type: TYPES[kind] || null, timeoutMs });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
TYPES,
|
|
82
|
+
normalizePortfolio,
|
|
83
|
+
requestPortfolios,
|
|
84
|
+
fetchPortfolios,
|
|
85
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Shared flow for `experiences` (type=EXPERIENCE) and `portfolios`
|
|
4
|
+
// (type=PORTFOLIO): both list the talent's portfolio entries, read-only, with
|
|
5
|
+
// the same shape. `kind` selects the `?type=` subset and the i18n namespace.
|
|
6
|
+
|
|
7
|
+
const { styleLabelPrefix } = require('./ansi');
|
|
8
|
+
const { resolveName } = require('./translations-client');
|
|
9
|
+
|
|
10
|
+
function makePortfoliosDeps(overrides = {}) {
|
|
11
|
+
const client = require('./portfolios-client');
|
|
12
|
+
return {
|
|
13
|
+
fetchPortfolios: (opts) => client.fetchPortfolios({}, opts),
|
|
14
|
+
fetchTranslationMap: (opts) => require('./translations-client').fetchTranslationMap({}, opts),
|
|
15
|
+
...overrides,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Skill names arrive as static-data i18n KEYS; resolve them to readable names
|
|
20
|
+
// (only fetch the translations map when there is at least one key to resolve).
|
|
21
|
+
async function resolveSkillNames(items, { deps, hubAccessToken, lang }) {
|
|
22
|
+
const needs = items.some((it) => (it.skills || []).some((s) => /^staticData/.test(s)));
|
|
23
|
+
if (!needs) return items;
|
|
24
|
+
const tm = await deps.fetchTranslationMap({ hubAccessToken, lang });
|
|
25
|
+
const map = tm && tm.ok ? tm.map : null;
|
|
26
|
+
if (!map) return items;
|
|
27
|
+
return items.map((it) => ({ ...it, skills: (it.skills || []).map((s) => resolveName(map, s)) }));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function dateRange(item, fp) {
|
|
31
|
+
const start = item.startDate ? item.startDate.slice(0, 10) : null;
|
|
32
|
+
const end = item.isCurrent ? fp.current : (item.endDate ? item.endDate.slice(0, 10) : null);
|
|
33
|
+
if (!start && !end) return null;
|
|
34
|
+
return `${start || '—'} → ${end || '—'}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Human render never prints ids (they ride --json / the tools).
|
|
38
|
+
function formatItem(item, index, fp) {
|
|
39
|
+
const headline = item.name || fp.untitled;
|
|
40
|
+
const lines = [`${index}) ${headline}${item.company ? ` — ${item.company}` : ''}`];
|
|
41
|
+
const range = dateRange(item, fp);
|
|
42
|
+
if (range) lines.push(` ${styleLabelPrefix(fp.datesLabel(range))}`);
|
|
43
|
+
if (item.location) lines.push(` ${styleLabelPrefix(fp.locationLabel(item.location))}`);
|
|
44
|
+
if (item.skills.length) lines.push(` ${styleLabelPrefix(fp.skillsLabel(item.skills.join(', ')))}`);
|
|
45
|
+
return lines.join('\n ');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function runPortfolioList({ io, rawOut, lang = 'en', session, catalog, kind, opts = {}, deps = makePortfoliosDeps() }) {
|
|
49
|
+
const fp = catalog[kind];
|
|
50
|
+
const hubAccessToken = session ? session.hubAccessToken : null;
|
|
51
|
+
|
|
52
|
+
const doFetch = () => deps.fetchPortfolios({ hubAccessToken, kind });
|
|
53
|
+
const res = opts.json ? await doFetch() : await io.withProgress(fp.loading, doFetch);
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
io.error(fp.fetchFailed(res.reason));
|
|
56
|
+
return { ok: false, reason: res.reason };
|
|
57
|
+
}
|
|
58
|
+
const items = await resolveSkillNames(Array.isArray(res.items) ? res.items : [], { deps, hubAccessToken, lang });
|
|
59
|
+
|
|
60
|
+
if (opts.json) {
|
|
61
|
+
(rawOut || ((s) => process.stdout.write(s)))(`${JSON.stringify({ ok: true, items }, null, 2)}\n`);
|
|
62
|
+
return { ok: true, items };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
io.section(fp.title);
|
|
66
|
+
if (items.length === 0) {
|
|
67
|
+
io.notify(fp.none);
|
|
68
|
+
return { ok: true, items };
|
|
69
|
+
}
|
|
70
|
+
items.forEach((item, i) => {
|
|
71
|
+
if (i > 0) io.notify('');
|
|
72
|
+
io.notify(formatItem(item, i + 1, fp));
|
|
73
|
+
});
|
|
74
|
+
io.notify(styleLabelPrefix(fp.countNote(items.length)));
|
|
75
|
+
io.success(fp.done);
|
|
76
|
+
return { ok: true, items };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { makePortfoliosDeps, runPortfolioList, formatItem };
|