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,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 };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// AI-profile client. The "My work
|
|
4
|
+
// with AI" read model: setup/usage tier+level, 3x3 cell, vision, howIWork.
|
|
5
|
+
|
|
6
|
+
const { postJsonWithTimeout } = require('./backend-request');
|
|
7
|
+
const { reasonForError } = require('./onboarding-client');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 20000;
|
|
10
|
+
|
|
11
|
+
function parseJson(raw) {
|
|
12
|
+
try {
|
|
13
|
+
return raw ? JSON.parse(raw) : null;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function bearerHeaders(accessToken) {
|
|
20
|
+
const headers = {};
|
|
21
|
+
if (typeof accessToken === 'string' && accessToken) headers.Authorization = `Bearer ${accessToken}`;
|
|
22
|
+
return headers;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function str(v) {
|
|
26
|
+
return typeof v === 'string' && v ? v : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function requestAiProfile({ hubAccessToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
30
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
31
|
+
if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
|
|
32
|
+
let res;
|
|
33
|
+
try {
|
|
34
|
+
res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, bearerHeaders(hubAccessToken));
|
|
35
|
+
} catch (e) {
|
|
36
|
+
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
37
|
+
}
|
|
38
|
+
if (res.status === 404) return { ok: false, reason: 'not-found' };
|
|
39
|
+
if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
|
|
40
|
+
|
|
41
|
+
const env = parseJson(res.raw);
|
|
42
|
+
const data = env && env.status === 'OK' ? env.data : env;
|
|
43
|
+
if (!data || typeof data !== 'object') return { ok: false, reason: 'bad-response' };
|
|
44
|
+
const setup = data.setup && typeof data.setup === 'object' ? data.setup : {};
|
|
45
|
+
const usage = data.usage && typeof data.usage === 'object' ? data.usage : {};
|
|
46
|
+
const traction = data.traction && typeof data.traction === 'object' ? data.traction : {};
|
|
47
|
+
return {
|
|
48
|
+
ok: true,
|
|
49
|
+
aiProfile: {
|
|
50
|
+
setupTier: str(setup.tier),
|
|
51
|
+
setupLevel: str(setup.level),
|
|
52
|
+
usageLevel: str(usage.level),
|
|
53
|
+
cell: str(data.cell),
|
|
54
|
+
isAiNative: data.isAiNative === true,
|
|
55
|
+
agentsOnProfile: typeof traction.agentsOnProfile === 'number' ? traction.agentsOnProfile : null,
|
|
56
|
+
vision: data.vision && typeof data.vision === 'object' ? str(data.vision.text) : null,
|
|
57
|
+
howIWork: data.howIWork && typeof data.howIWork === 'object' ? str(data.howIWork.body) : null,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function fetchAiProfile(deps = {}, { hubAccessToken, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
63
|
+
const {
|
|
64
|
+
getAiProfileEndpoint = require('./config').getAiProfileEndpoint,
|
|
65
|
+
requestAiProfile: request = requestAiProfile,
|
|
66
|
+
} = deps;
|
|
67
|
+
const endpoint = getAiProfileEndpoint();
|
|
68
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
69
|
+
return request({ hubAccessToken }, { endpoint, timeoutMs });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// GET on a hub endpoint that returns a `{status,data}` envelope, mapped by `map`.
|
|
73
|
+
async function getMapped(endpoint, hubAccessToken, timeoutMs, map) {
|
|
74
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
75
|
+
if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
|
|
76
|
+
let res;
|
|
77
|
+
try {
|
|
78
|
+
res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, bearerHeaders(hubAccessToken));
|
|
79
|
+
} catch (e) {
|
|
80
|
+
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
81
|
+
}
|
|
82
|
+
if (res.status === 404) return { ok: false, reason: 'not-found' };
|
|
83
|
+
if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
|
|
84
|
+
const env = parseJson(res.raw);
|
|
85
|
+
const data = env && env.status === 'OK' ? env.data : env;
|
|
86
|
+
if (data === undefined || data === null) return { ok: false, reason: 'bad-response' };
|
|
87
|
+
return { ok: true, ...map(data) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function fetchMeProfile(deps = {}, { hubAccessToken, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
91
|
+
const getEndpoint = deps.getMeProfileEndpoint || require('./config').getMeProfileEndpoint;
|
|
92
|
+
const req = deps.getMapped || getMapped;
|
|
93
|
+
return req(getEndpoint(), hubAccessToken, timeoutMs, (d) => ({ headline: str(d && d.headline) }));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function fetchTalentMeProfile(deps = {}, { hubAccessToken, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
97
|
+
const getEndpoint = deps.getTalentMeProfileEndpoint || require('./config').getTalentMeProfileEndpoint;
|
|
98
|
+
const req = deps.getMapped || getMapped;
|
|
99
|
+
return req(getEndpoint(), hubAccessToken, timeoutMs, (d) => ({
|
|
100
|
+
completedProfilePercentage: typeof (d && d.completedProfilePercentage) === 'number' ? d.completedProfilePercentage : null,
|
|
101
|
+
freelanceType: str(d && d.freelanceType),
|
|
102
|
+
onboardingStatus: str(d && d.onboardingStatus),
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function fetchLanguages(deps = {}, { hubAccessToken, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
107
|
+
const getEndpoint = deps.getLanguagesEndpoint || require('./config').getLanguagesEndpoint;
|
|
108
|
+
const req = deps.getMapped || getMapped;
|
|
109
|
+
return req(getEndpoint(), hubAccessToken, timeoutMs, (d) => {
|
|
110
|
+
const rows = Array.isArray(d) ? d : (Array.isArray(d && d.items) ? d.items : []);
|
|
111
|
+
const codes = rows.map((r) => (r && r.language && str(r.language.code)) || str(r && r.code)).filter(Boolean);
|
|
112
|
+
return { languageCodes: codes, languageCount: rows.length };
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// `works/users/me` -> the stored phone, null when hub has none.
|
|
117
|
+
async function fetchTelephone(deps = {}, { hubAccessToken, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
118
|
+
const getEndpoint = deps.getUserMeEndpoint || require('./config').getUserMeEndpoint;
|
|
119
|
+
const req = deps.getMapped || getMapped;
|
|
120
|
+
return req(getEndpoint(), hubAccessToken, timeoutMs, (d) => {
|
|
121
|
+
const t = d && d.telephone;
|
|
122
|
+
return { telephone: t && str(t.code) && str(t.number) ? { code: str(t.code), number: str(t.number) } : null };
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = {
|
|
127
|
+
requestAiProfile,
|
|
128
|
+
fetchAiProfile,
|
|
129
|
+
fetchMeProfile,
|
|
130
|
+
fetchTalentMeProfile,
|
|
131
|
+
fetchLanguages,
|
|
132
|
+
fetchTelephone,
|
|
133
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// `me` / `profile` flow: a summary combining the "My work with AI" read model,
|
|
4
|
+
// the main role (from certifications) and the per-project rate. Read-only.
|
|
5
|
+
|
|
6
|
+
const { styleLabelPrefix } = require('./ansi');
|
|
7
|
+
|
|
8
|
+
function makeProfileDeps(overrides = {}) {
|
|
9
|
+
const profile = require('./profile-client');
|
|
10
|
+
const rate = require('./rate-client');
|
|
11
|
+
const certs = require('./certifications-client');
|
|
12
|
+
const availability = require('./availability-client');
|
|
13
|
+
return {
|
|
14
|
+
fetchAiProfile: (opts) => profile.fetchAiProfile({}, opts),
|
|
15
|
+
fetchPricingRate: (opts) => rate.fetchPricingRate({}, opts),
|
|
16
|
+
fetchMeCertifications: (opts) => certs.fetchMeCertifications({}, opts),
|
|
17
|
+
fetchMeProfile: (opts) => profile.fetchMeProfile({}, opts),
|
|
18
|
+
fetchTalentMeProfile: (opts) => profile.fetchTalentMeProfile({}, opts),
|
|
19
|
+
fetchLanguages: (opts) => profile.fetchLanguages({}, opts),
|
|
20
|
+
fetchAvailability: (opts) => availability.fetchAvailability({}, opts),
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function trim(text, max) {
|
|
26
|
+
if (!text) return null;
|
|
27
|
+
const t = String(text).trim();
|
|
28
|
+
return t.length > max ? `${t.slice(0, max - 1)}…` : t;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function runProfile({ io, rawOut, session, catalog, opts = {}, deps = makeProfileDeps() }) {
|
|
32
|
+
const fp = catalog.profile;
|
|
33
|
+
const hubAccessToken = session ? session.hubAccessToken : null;
|
|
34
|
+
|
|
35
|
+
// Independent reads; each degrades to null so one outage never blanks the
|
|
36
|
+
// whole summary.
|
|
37
|
+
const doFetch = () => Promise.all([
|
|
38
|
+
deps.fetchAiProfile({ hubAccessToken }),
|
|
39
|
+
deps.fetchPricingRate({ hubAccessToken }),
|
|
40
|
+
deps.fetchMeCertifications({ hubAccessToken }),
|
|
41
|
+
deps.fetchMeProfile({ hubAccessToken }),
|
|
42
|
+
deps.fetchTalentMeProfile({ hubAccessToken }),
|
|
43
|
+
deps.fetchLanguages({ hubAccessToken }),
|
|
44
|
+
deps.fetchAvailability({ hubAccessToken }),
|
|
45
|
+
]);
|
|
46
|
+
const [ai, rate, certs, meProfile, talentProfile, languages, availability] = opts.json ? await doFetch() : await io.withProgress(fp.loading, doFetch);
|
|
47
|
+
|
|
48
|
+
if (!ai.ok && !rate.ok && !certs.ok && !meProfile.ok && !talentProfile.ok) {
|
|
49
|
+
io.error(fp.fetchFailed(ai.reason || rate.reason || certs.reason));
|
|
50
|
+
return { ok: false, reason: ai.reason || rate.reason || certs.reason };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const aiProfile = ai.ok ? ai.aiProfile : null;
|
|
54
|
+
const pricing = rate.ok ? rate.pricing : null;
|
|
55
|
+
const mainRole = certs.ok ? certs.mainRole : null;
|
|
56
|
+
const headline = meProfile.ok ? meProfile.headline : null;
|
|
57
|
+
const completion = talentProfile.ok ? talentProfile.completedProfilePercentage : null;
|
|
58
|
+
const freelanceType = talentProfile.ok ? talentProfile.freelanceType : null;
|
|
59
|
+
const languageCodes = languages.ok ? languages.languageCodes : [];
|
|
60
|
+
const availabilitySummary = availability.ok ? availability.availability : null;
|
|
61
|
+
const skillsCount = certs.ok && certs.skillsRatio && typeof certs.skillsRatio.total === 'number' ? certs.skillsRatio.total : null;
|
|
62
|
+
const agentsCount = aiProfile ? aiProfile.agentsOnProfile : null;
|
|
63
|
+
|
|
64
|
+
if (opts.json) {
|
|
65
|
+
(rawOut || ((s) => process.stdout.write(s)))(`${JSON.stringify({
|
|
66
|
+
ok: true, mainRole, headline, completedProfilePercentage: completion, freelanceType,
|
|
67
|
+
languageCodes, skillsCount, agentsCount, availability: availabilitySummary, aiProfile, pricing,
|
|
68
|
+
}, null, 2)}\n`);
|
|
69
|
+
return { ok: true, mainRole, aiProfile, pricing };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
io.section(fp.title);
|
|
73
|
+
if (headline) io.notify(styleLabelPrefix(fp.headline(headline)));
|
|
74
|
+
if (mainRole && mainRole.name) io.notify(styleLabelPrefix(fp.role(mainRole.name)));
|
|
75
|
+
if (freelanceType) io.notify(styleLabelPrefix(fp.freelanceType(freelanceType)));
|
|
76
|
+
if (completion != null) io.notify(styleLabelPrefix(fp.completion(completion)));
|
|
77
|
+
if (pricing) {
|
|
78
|
+
const m = pricing.fullTimeSelected ? pricing.fullTimePrice : null;
|
|
79
|
+
io.notify(styleLabelPrefix(fp.rate(m && m.amount != null ? `${m.amount}${m.currency ? ` ${m.currency}` : ''}` : fp.rateNotSet)));
|
|
80
|
+
}
|
|
81
|
+
if (availabilitySummary) {
|
|
82
|
+
io.notify(styleLabelPrefix(fp.availability(
|
|
83
|
+
availabilitySummary.available === true ? fp.availYes : availabilitySummary.available === false ? fp.availNo : fp.availUnknown,
|
|
84
|
+
availabilitySummary.monthlyHours || fp.availUnknown,
|
|
85
|
+
)));
|
|
86
|
+
}
|
|
87
|
+
if (languageCodes.length) io.notify(styleLabelPrefix(fp.languages(languageCodes.join(', '))));
|
|
88
|
+
const counts = [
|
|
89
|
+
skillsCount != null ? fp.skillsCount(skillsCount) : null,
|
|
90
|
+
agentsCount != null ? fp.agentsCount(agentsCount) : null,
|
|
91
|
+
].filter(Boolean);
|
|
92
|
+
if (counts.length) io.notify(counts.join(' · '));
|
|
93
|
+
if (aiProfile) {
|
|
94
|
+
if (aiProfile.cell) io.notify(styleLabelPrefix(fp.cell(aiProfile.cell)));
|
|
95
|
+
const axes = [
|
|
96
|
+
aiProfile.setupTier ? styleLabelPrefix(fp.setup(aiProfile.setupTier, aiProfile.setupLevel || '—')) : null,
|
|
97
|
+
aiProfile.usageLevel ? styleLabelPrefix(fp.usage(aiProfile.usageLevel)) : null,
|
|
98
|
+
].filter(Boolean);
|
|
99
|
+
if (axes.length) io.notify(axes.join(' · '));
|
|
100
|
+
if (aiProfile.isAiNative) io.notify(fp.aiNative);
|
|
101
|
+
const vision = trim(aiProfile.vision, 240);
|
|
102
|
+
if (vision) { io.section(fp.visionHeading); io.notify(vision); }
|
|
103
|
+
const howIWork = trim(aiProfile.howIWork, 240);
|
|
104
|
+
if (howIWork) { io.section(fp.howIWorkHeading); io.notify(howIWork); }
|
|
105
|
+
} else {
|
|
106
|
+
io.notify(fp.noAiProfile);
|
|
107
|
+
}
|
|
108
|
+
io.success(fp.done);
|
|
109
|
+
return { ok: true, mainRole, aiProfile, pricing };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = {
|
|
113
|
+
makeProfileDeps,
|
|
114
|
+
runProfile,
|
|
115
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const { getConfigDir } = require('./config-dir');
|
|
7
|
+
const { loadConfigFile, saveConfigFile, VALID_PROFILES } = require('./config');
|
|
8
|
+
const { clearAuthSession } = require('./auth-session-store');
|
|
9
|
+
|
|
10
|
+
function reportStatePath(env) {
|
|
11
|
+
return path.join(getConfigDir(env), 'report-state.json');
|
|
12
|
+
}
|
|
13
|
+
function consentStatePath(env) {
|
|
14
|
+
return path.join(getConfigDir(env), 'consent.json');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Best-effort delete: a file that doesn't exist (nothing to purge yet, e.g.
|
|
18
|
+
// a superadmin who never ran `usage` in this profile) is not a failure.
|
|
19
|
+
function unlinkIfExists(p) {
|
|
20
|
+
try {
|
|
21
|
+
fs.unlinkSync(p);
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// `switchProfile(target, env)`.
|
|
29
|
+
function switchProfile(target, env = process.env) {
|
|
30
|
+
if (!VALID_PROFILES.has(target)) return { ok: false, reason: 'invalid-profile' };
|
|
31
|
+
|
|
32
|
+
const config = loadConfigFile(env);
|
|
33
|
+
const from = VALID_PROFILES.has(config.profile) ? config.profile : 'talent';
|
|
34
|
+
config.profile = target;
|
|
35
|
+
saveConfigFile(config, env);
|
|
36
|
+
|
|
37
|
+
const purged = {
|
|
38
|
+
reportState: unlinkIfExists(reportStatePath(env)),
|
|
39
|
+
consent: unlinkIfExists(consentStatePath(env)),
|
|
40
|
+
authSession: clearAuthSession(env),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return { ok: true, from, to: target, purged };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { switchProfile, reportStatePath, consentStatePath };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { runInteractiveMultiSelect } = require('./interactive-select');
|
|
4
|
+
|
|
5
|
+
async function promptSelect({ ask, stdinIsTTY, out, items, labelFor = (x) => String(x), header = '', hint = '', input, output } = {}) {
|
|
6
|
+
if (stdinIsTTY && typeof ask.suspend === 'function') {
|
|
7
|
+
ask.suspend();
|
|
8
|
+
const picked = await runInteractiveMultiSelect({
|
|
9
|
+
items,
|
|
10
|
+
labelFor,
|
|
11
|
+
header,
|
|
12
|
+
hint,
|
|
13
|
+
single: true,
|
|
14
|
+
...(input ? { input } : {}),
|
|
15
|
+
...(output ? { output } : {}),
|
|
16
|
+
});
|
|
17
|
+
ask.resume();
|
|
18
|
+
return picked && picked.length ? picked[0] : null;
|
|
19
|
+
}
|
|
20
|
+
if (header) out(header);
|
|
21
|
+
items.forEach((it, i) => out(` ${i + 1}) ${labelFor(it)}`));
|
|
22
|
+
const raw = (await ask(' > ')).trim();
|
|
23
|
+
const n = parseInt(raw, 10);
|
|
24
|
+
if (Number.isInteger(n) && n >= 1 && n <= items.length) return items[n - 1];
|
|
25
|
+
const lowered = raw.toLowerCase();
|
|
26
|
+
return items.find((it) => labelFor(it).toLowerCase() === lowered || String(it).toLowerCase() === lowered) || null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Multi-pick variant: SPACE toggles, ENTER confirms; returns the chosen items as
|
|
30
|
+
// an array (possibly empty). Non-TTY fallback accepts space/comma-separated
|
|
31
|
+
// indices or labels.
|
|
32
|
+
async function promptMultiSelect({ ask, stdinIsTTY, out, items, labelFor = (x) => String(x), header = '', hint = '', initialMarked = [], input, output } = {}) {
|
|
33
|
+
if (stdinIsTTY && typeof ask.suspend === 'function') {
|
|
34
|
+
ask.suspend();
|
|
35
|
+
const picked = await runInteractiveMultiSelect({
|
|
36
|
+
items,
|
|
37
|
+
labelFor,
|
|
38
|
+
header,
|
|
39
|
+
hint,
|
|
40
|
+
single: false,
|
|
41
|
+
initialMarked,
|
|
42
|
+
...(input ? { input } : {}),
|
|
43
|
+
...(output ? { output } : {}),
|
|
44
|
+
});
|
|
45
|
+
ask.resume();
|
|
46
|
+
return Array.isArray(picked) ? picked : [];
|
|
47
|
+
}
|
|
48
|
+
if (header) out(header);
|
|
49
|
+
items.forEach((it, i) => out(` ${i + 1}) ${labelFor(it)}`));
|
|
50
|
+
const raw = (await ask(' > ')).trim();
|
|
51
|
+
if (!raw) return [];
|
|
52
|
+
const chosen = [];
|
|
53
|
+
for (const tok of raw.split(/[\s,]+/).filter(Boolean)) {
|
|
54
|
+
const n = parseInt(tok, 10);
|
|
55
|
+
let item = null;
|
|
56
|
+
if (Number.isInteger(n) && n >= 1 && n <= items.length) item = items[n - 1];
|
|
57
|
+
else {
|
|
58
|
+
const lowered = tok.toLowerCase();
|
|
59
|
+
item = items.find((it) => labelFor(it).toLowerCase() === lowered || String(it).toLowerCase() === lowered) || null;
|
|
60
|
+
}
|
|
61
|
+
if (item != null && !chosen.includes(item)) chosen.push(item);
|
|
62
|
+
}
|
|
63
|
+
return chosen;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { promptSelect, promptMultiSelect };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Pricing-rate client. READ-ONLY here: setting the rate is a financial PUT, left to register.
|
|
4
|
+
|
|
5
|
+
const { postJsonWithTimeout } = require('./backend-request');
|
|
6
|
+
const { reasonForError } = require('./onboarding-client');
|
|
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 bearerHeaders(accessToken) {
|
|
19
|
+
const headers = {};
|
|
20
|
+
if (typeof accessToken === 'string' && accessToken) headers.Authorization = `Bearer ${accessToken}`;
|
|
21
|
+
return headers;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function money(m) {
|
|
25
|
+
if (!m || typeof m !== 'object') return null;
|
|
26
|
+
return { amount: typeof m.amount === 'number' ? m.amount : null, currency: typeof m.currency === 'string' ? m.currency : null };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function requestPricingRate({ hubAccessToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
30
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
31
|
+
if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
|
|
32
|
+
let res;
|
|
33
|
+
try {
|
|
34
|
+
res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, bearerHeaders(hubAccessToken));
|
|
35
|
+
} catch (e) {
|
|
36
|
+
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
37
|
+
}
|
|
38
|
+
if (res.status === 404) return { ok: false, reason: 'not-found' };
|
|
39
|
+
if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
|
|
40
|
+
|
|
41
|
+
const env = parseJson(res.raw);
|
|
42
|
+
const data = env && env.status === 'OK' ? env.data : env;
|
|
43
|
+
if (!data || typeof data !== 'object') return { ok: false, reason: 'bad-response' };
|
|
44
|
+
return {
|
|
45
|
+
ok: true,
|
|
46
|
+
pricing: {
|
|
47
|
+
fullTimeSelected: data.fullTimeProjectSelected === true,
|
|
48
|
+
fullTimePrice: money(data.fullTimeProjectPrice),
|
|
49
|
+
partTimeSelected: data.partTimeProjectSelected === true,
|
|
50
|
+
partTimePrice: money(data.partTimeProjectPrice),
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function fetchPricingRate(deps = {}, { hubAccessToken, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
56
|
+
const {
|
|
57
|
+
getPricingRateEndpoint = require('./config').getPricingRateEndpoint,
|
|
58
|
+
requestPricingRate: request = requestPricingRate,
|
|
59
|
+
} = deps;
|
|
60
|
+
const endpoint = getPricingRateEndpoint();
|
|
61
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
62
|
+
return request({ hubAccessToken }, { endpoint, timeoutMs });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// PUT the pricing (upsert). Reuses the SAME endpoint + request the register
|
|
66
|
+
// pricing step uses, so `rate --set` and onboarding never diverge on the wire.
|
|
67
|
+
async function savePricingRate(deps = {}, { hubAccessToken, pricing, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
68
|
+
const {
|
|
69
|
+
getPricingRateEndpoint = require('./config').getPricingRateEndpoint,
|
|
70
|
+
requestSetPricingRate = require('./onboarding-client').requestSetPricingRate,
|
|
71
|
+
} = deps;
|
|
72
|
+
const endpoint = getPricingRateEndpoint();
|
|
73
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
74
|
+
return requestSetPricingRate({ pricing, hubAccessToken }, { endpoint, timeoutMs });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
requestPricingRate,
|
|
79
|
+
fetchPricingRate,
|
|
80
|
+
savePricingRate,
|
|
81
|
+
};
|