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,125 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// AI-profile client (hub `works/me/ai-profile`, talent Bearer). 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
|
+
// `works/me/profile` -> headline (basic profile text).
|
|
91
|
+
async function fetchMeProfile(deps = {}, { hubAccessToken, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
92
|
+
const getEndpoint = deps.getMeProfileEndpoint || require('./config').getMeProfileEndpoint;
|
|
93
|
+
const req = deps.getMapped || getMapped;
|
|
94
|
+
return req(getEndpoint(), hubAccessToken, timeoutMs, (d) => ({ headline: str(d && d.headline) }));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// `works/talents/me/profile` -> completion %, freelance type, onboarding status.
|
|
98
|
+
async function fetchTalentMeProfile(deps = {}, { hubAccessToken, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
99
|
+
const getEndpoint = deps.getTalentMeProfileEndpoint || require('./config').getTalentMeProfileEndpoint;
|
|
100
|
+
const req = deps.getMapped || getMapped;
|
|
101
|
+
return req(getEndpoint(), hubAccessToken, timeoutMs, (d) => ({
|
|
102
|
+
completedProfilePercentage: typeof (d && d.completedProfilePercentage) === 'number' ? d.completedProfilePercentage : null,
|
|
103
|
+
freelanceType: str(d && d.freelanceType),
|
|
104
|
+
onboardingStatus: str(d && d.onboardingStatus),
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// `works/talents/me/work-details/languages` -> count + codes.
|
|
109
|
+
async function fetchLanguages(deps = {}, { hubAccessToken, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
110
|
+
const getEndpoint = deps.getLanguagesEndpoint || require('./config').getLanguagesEndpoint;
|
|
111
|
+
const req = deps.getMapped || getMapped;
|
|
112
|
+
return req(getEndpoint(), hubAccessToken, timeoutMs, (d) => {
|
|
113
|
+
const rows = Array.isArray(d) ? d : (Array.isArray(d && d.items) ? d.items : []);
|
|
114
|
+
const codes = rows.map((r) => (r && r.language && str(r.language.code)) || str(r && r.code)).filter(Boolean);
|
|
115
|
+
return { languageCodes: codes, languageCount: rows.length };
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
requestAiProfile,
|
|
121
|
+
fetchAiProfile,
|
|
122
|
+
fetchMeProfile,
|
|
123
|
+
fetchTalentMeProfile,
|
|
124
|
+
fetchLanguages,
|
|
125
|
+
};
|
|
@@ -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,29 @@
|
|
|
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
|
+
module.exports = { promptSelect };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Pricing-rate client (hub `works/talents/me/work-details/pricing-rate`, talent
|
|
4
|
+
// Bearer). READ-ONLY here: setting the rate is a financial PUT, left to register.
|
|
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 money(m) {
|
|
26
|
+
if (!m || typeof m !== 'object') return null;
|
|
27
|
+
return { amount: typeof m.amount === 'number' ? m.amount : null, currency: typeof m.currency === 'string' ? m.currency : null };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function requestPricingRate({ hubAccessToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
31
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
32
|
+
if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
|
|
33
|
+
let res;
|
|
34
|
+
try {
|
|
35
|
+
res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, bearerHeaders(hubAccessToken));
|
|
36
|
+
} catch (e) {
|
|
37
|
+
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
38
|
+
}
|
|
39
|
+
if (res.status === 404) return { ok: false, reason: 'not-found' };
|
|
40
|
+
if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
|
|
41
|
+
|
|
42
|
+
const env = parseJson(res.raw);
|
|
43
|
+
const data = env && env.status === 'OK' ? env.data : env;
|
|
44
|
+
if (!data || typeof data !== 'object') return { ok: false, reason: 'bad-response' };
|
|
45
|
+
return {
|
|
46
|
+
ok: true,
|
|
47
|
+
pricing: {
|
|
48
|
+
fullTimeSelected: data.fullTimeProjectSelected === true,
|
|
49
|
+
fullTimePrice: money(data.fullTimeProjectPrice),
|
|
50
|
+
partTimeSelected: data.partTimeProjectSelected === true,
|
|
51
|
+
partTimePrice: money(data.partTimeProjectPrice),
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function fetchPricingRate(deps = {}, { hubAccessToken, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
57
|
+
const {
|
|
58
|
+
getPricingRateEndpoint = require('./config').getPricingRateEndpoint,
|
|
59
|
+
requestPricingRate: request = requestPricingRate,
|
|
60
|
+
} = deps;
|
|
61
|
+
const endpoint = getPricingRateEndpoint();
|
|
62
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
63
|
+
return request({ hubAccessToken }, { endpoint, timeoutMs });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// PUT the pricing (upsert). Reuses the SAME endpoint + request the register
|
|
67
|
+
// pricing step uses, so `rate --set` and onboarding never diverge on the wire.
|
|
68
|
+
async function savePricingRate(deps = {}, { hubAccessToken, pricing, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
69
|
+
const {
|
|
70
|
+
getPricingRateEndpoint = require('./config').getPricingRateEndpoint,
|
|
71
|
+
requestSetPricingRate = require('./onboarding-client').requestSetPricingRate,
|
|
72
|
+
} = deps;
|
|
73
|
+
const endpoint = getPricingRateEndpoint();
|
|
74
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
75
|
+
return requestSetPricingRate({ pricing, hubAccessToken }, { endpoint, timeoutMs });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = {
|
|
79
|
+
requestPricingRate,
|
|
80
|
+
fetchPricingRate,
|
|
81
|
+
savePricingRate,
|
|
82
|
+
};
|
package/src/rate-flow.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// `rate` flow: view the talent's per-project pricing, and `--set` to change it
|
|
4
|
+
// (interactive, explicit confirmation — it changes what clients pay).
|
|
5
|
+
|
|
6
|
+
const { styleLabelPrefix } = require('./ansi');
|
|
7
|
+
|
|
8
|
+
const CURRENCIES = ['EUR', 'USD', 'GBP'];
|
|
9
|
+
const MAX_AMOUNT = 999999.99;
|
|
10
|
+
|
|
11
|
+
function makeRateDeps(overrides = {}) {
|
|
12
|
+
const client = require('./rate-client');
|
|
13
|
+
return {
|
|
14
|
+
fetchPricingRate: (opts) => client.fetchPricingRate({}, opts),
|
|
15
|
+
savePricingRate: (opts) => client.savePricingRate({}, opts),
|
|
16
|
+
promptSelect: (opts) => require('./prompt-select').promptSelect(opts),
|
|
17
|
+
...overrides,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function priceLine(fp, money) {
|
|
22
|
+
if (!money || money.amount == null) return fp.notSet;
|
|
23
|
+
return `${money.amount}${money.currency ? ` ${money.currency}` : ''}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Empty pricing when the talent has none yet (upsert still works).
|
|
27
|
+
const EMPTY_PRICING = { fullTimeSelected: false, fullTimePrice: null, partTimeSelected: false, partTimePrice: null };
|
|
28
|
+
|
|
29
|
+
function parseAmount(raw) {
|
|
30
|
+
const s = String(raw == null ? '' : raw).trim().replace(',', '.');
|
|
31
|
+
if (!/^\d+(\.\d+)?$/.test(s)) return null;
|
|
32
|
+
const n = Number.parseFloat(s);
|
|
33
|
+
if (!Number.isFinite(n) || n < 0 || n > MAX_AMOUNT) return null;
|
|
34
|
+
return n;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Merge the chosen modality's new price into the current pricing → the PUT body
|
|
38
|
+
// (full object, so the untouched modality is preserved, never cleared).
|
|
39
|
+
function buildBody(current, modality, amount, currency) {
|
|
40
|
+
const body = {
|
|
41
|
+
fullTimeProjectSelected: current.fullTimeSelected === true,
|
|
42
|
+
fullTimeProjectPrice: current.fullTimePrice ? { amount: current.fullTimePrice.amount, currency: current.fullTimePrice.currency } : null,
|
|
43
|
+
partTimeProjectSelected: current.partTimeSelected === true,
|
|
44
|
+
partTimeProjectPrice: current.partTimePrice ? { amount: current.partTimePrice.amount, currency: current.partTimePrice.currency } : null,
|
|
45
|
+
};
|
|
46
|
+
if (modality === 'full') { body.fullTimeProjectSelected = true; body.fullTimeProjectPrice = { amount, currency }; }
|
|
47
|
+
else { body.partTimeProjectSelected = true; body.partTimeProjectPrice = { amount, currency }; }
|
|
48
|
+
return body;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function renderView(io, fp, p) {
|
|
52
|
+
io.section(fp.title);
|
|
53
|
+
io.notify(styleLabelPrefix(fp.fullTime(priceLine(fp, p.fullTimeSelected ? p.fullTimePrice : null))));
|
|
54
|
+
if (p.partTimeSelected) io.notify(styleLabelPrefix(fp.partTime(priceLine(fp, p.partTimePrice))));
|
|
55
|
+
io.success(fp.done);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function runSet({ io, ask, session, catalog, opts, deps }) {
|
|
59
|
+
const fp = catalog.rate;
|
|
60
|
+
const hubAccessToken = session ? session.hubAccessToken : null;
|
|
61
|
+
if (!opts.stdinIsTTY) {
|
|
62
|
+
io.error(fp.setNeedsInteractive);
|
|
63
|
+
return { ok: false, reason: 'non-interactive' };
|
|
64
|
+
}
|
|
65
|
+
const selOut = (s) => io.notify(s);
|
|
66
|
+
|
|
67
|
+
// Read current so we can show it, preserve the other modality, and diff.
|
|
68
|
+
const read = await io.withProgress(fp.loading, () => deps.fetchPricingRate({ hubAccessToken }));
|
|
69
|
+
if (!read.ok && read.reason !== 'not-found') {
|
|
70
|
+
io.error(fp.fetchFailed(read.reason));
|
|
71
|
+
return { ok: false, reason: read.reason };
|
|
72
|
+
}
|
|
73
|
+
const current = read.ok ? read.pricing : EMPTY_PRICING;
|
|
74
|
+
|
|
75
|
+
io.section(fp.setTitle);
|
|
76
|
+
io.notify(styleLabelPrefix(fp.fullTime(priceLine(fp, current.fullTimeSelected ? current.fullTimePrice : null))));
|
|
77
|
+
io.notify(styleLabelPrefix(fp.partTime(priceLine(fp, current.partTimeSelected ? current.partTimePrice : null))));
|
|
78
|
+
|
|
79
|
+
// Arrow-key pickers for every discrete choice (modality, currency).
|
|
80
|
+
const modItem = await deps.promptSelect({
|
|
81
|
+
ask, stdinIsTTY: opts.stdinIsTTY, out: selOut, header: fp.setWhich,
|
|
82
|
+
items: [{ label: fp.modalityFull, value: 'full' }, { label: fp.modalityPart, value: 'part' }],
|
|
83
|
+
labelFor: (x) => x.label, input: opts.input, output: opts.output,
|
|
84
|
+
});
|
|
85
|
+
if (!modItem) { io.notify(fp.setCancelled); return { ok: true, cancelled: true }; }
|
|
86
|
+
const modality = modItem.value;
|
|
87
|
+
const modalityLabel = modality === 'full' ? fp.modalityFull : fp.modalityPart;
|
|
88
|
+
|
|
89
|
+
const amount = parseAmount(await io.ask(fp.setAmount(modalityLabel)));
|
|
90
|
+
if (amount === null) { io.error(fp.setAmountInvalid); return { ok: false, reason: 'bad-amount' }; }
|
|
91
|
+
|
|
92
|
+
const currency = await deps.promptSelect({
|
|
93
|
+
ask, stdinIsTTY: opts.stdinIsTTY, out: selOut, header: fp.setCurrency,
|
|
94
|
+
items: CURRENCIES, labelFor: (x) => x, input: opts.input, output: opts.output,
|
|
95
|
+
});
|
|
96
|
+
if (!currency) { io.notify(fp.setCancelled); return { ok: true, cancelled: true }; }
|
|
97
|
+
|
|
98
|
+
const oldMoney = modality === 'full' ? (current.fullTimeSelected ? current.fullTimePrice : null) : (current.partTimeSelected ? current.partTimePrice : null);
|
|
99
|
+
const oldStr = priceLine(fp, oldMoney);
|
|
100
|
+
const newStr = `${amount} ${currency}`;
|
|
101
|
+
io.notify(fp.setDiff(modalityLabel, oldStr, newStr));
|
|
102
|
+
const confirmed = await io.confirm(fp.setConfirm);
|
|
103
|
+
if (!confirmed) { io.notify(fp.setCancelled); return { ok: true, cancelled: true }; }
|
|
104
|
+
|
|
105
|
+
const body = buildBody(current, modality, amount, currency);
|
|
106
|
+
const res = await io.withProgress(fp.setSaving, () => deps.savePricingRate({ hubAccessToken, pricing: body }));
|
|
107
|
+
if (!res.ok) { io.error(fp.setFailed(res.reason)); return { ok: false, reason: res.reason }; }
|
|
108
|
+
io.success(fp.setDone(modalityLabel, newStr));
|
|
109
|
+
return { ok: true, set: { modality, amount, currency } };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function runRate({ io, rawOut, ask, session, catalog, opts = {}, deps = makeRateDeps() }) {
|
|
113
|
+
const fp = catalog.rate;
|
|
114
|
+
if (opts.set) return runSet({ io, ask, session, catalog, opts, deps });
|
|
115
|
+
|
|
116
|
+
const hubAccessToken = session ? session.hubAccessToken : null;
|
|
117
|
+
const doFetch = () => deps.fetchPricingRate({ hubAccessToken });
|
|
118
|
+
const res = opts.json ? await doFetch() : await io.withProgress(fp.loading, doFetch);
|
|
119
|
+
if (!res.ok) {
|
|
120
|
+
io.error(res.reason === 'not-found' ? fp.notFound : fp.fetchFailed(res.reason));
|
|
121
|
+
return { ok: false, reason: res.reason };
|
|
122
|
+
}
|
|
123
|
+
if (opts.json) {
|
|
124
|
+
(rawOut || ((s) => process.stdout.write(s)))(`${JSON.stringify({ ok: true, pricing: res.pricing }, null, 2)}\n`);
|
|
125
|
+
return { ok: true, pricing: res.pricing };
|
|
126
|
+
}
|
|
127
|
+
renderView(io, fp, res.pricing);
|
|
128
|
+
return { ok: true, pricing: res.pricing };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = {
|
|
132
|
+
makeRateDeps,
|
|
133
|
+
runRate,
|
|
134
|
+
parseAmount,
|
|
135
|
+
buildBody,
|
|
136
|
+
CURRENCIES,
|
|
137
|
+
};
|