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,199 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The agents + activity blocks of the terminal report, extracted from src/render-terminal.js (structure refactor, issue 020).
|
|
4
|
+
|
|
5
|
+
const { label } = require('./i18n');
|
|
6
|
+
const { buildAgentCardTree } = require('./render-html');
|
|
7
|
+
const { sanitizeRenderText } = require('./sanitize-network-text');
|
|
8
|
+
const { MODEL_CALL, REASON, callFailed, callPartial, callTimedOut, modelCall, omittedByCall } = require('./model-call-record');
|
|
9
|
+
const { isFloorCategory } = require('./agent-category');
|
|
10
|
+
const { DEFAULT_REPORT_GATE } = require('./report-gating');
|
|
11
|
+
const { c, wrap, summarize } = require('./terminal-format');
|
|
12
|
+
const { detectedToolNames } = require('./detected-tools');
|
|
13
|
+
|
|
14
|
+
// agents (ADR-016: one line per agent) As SIMPLE as possible: ONE line per agent.
|
|
15
|
+
|
|
16
|
+
// AI product (from the source, e.g. Claude Code) shown in place of the LLM model.
|
|
17
|
+
function agentProductLabel(card, t) {
|
|
18
|
+
if (!card.aiProduct) return '';
|
|
19
|
+
const map = (t.html && t.html.aiProducts) || {};
|
|
20
|
+
return map[card.aiProduct] || card.aiProduct;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function agentLine(card, depth, t) {
|
|
24
|
+
const indent = ' '.repeat(depth);
|
|
25
|
+
// Root = a filled bullet; every deeper level = that many stacked down-arrows.
|
|
26
|
+
const marker = depth === 0 ? `${c.success}●${c.reset}` : `${c.primary}${'↓'.repeat(depth)}${c.reset}`;
|
|
27
|
+
// card.symbolicName is agent-synthesis (LLM, ADR-010) content; card.name is
|
|
28
|
+
// the local, deterministic agent id — only the former needs stripping.
|
|
29
|
+
const title = card.symbolicName
|
|
30
|
+
? `${sanitizeRenderText(card.symbolicName)} ${c.muted}(${card.name})${c.reset}`
|
|
31
|
+
: card.name;
|
|
32
|
+
// No model, no usage (user decision) — show the AI PRODUCT instead of the model.
|
|
33
|
+
const product = agentProductLabel(card, t);
|
|
34
|
+
const productBit = product ? ` ${c.dim}[${product}]${c.reset}` : '';
|
|
35
|
+
return ` ${indent}${marker} ${c.bold}${c.white}${title}${c.reset}${productBit}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The agent's wired tools/integrations = the per-card "technologies" (user wants
|
|
39
|
+
// MORE tech visible). One dim line under the agent; deduped; omitted when empty.
|
|
40
|
+
function agentToolsLine(card, depth) {
|
|
41
|
+
const tools = Array.from(new Set((card.tools || []).filter((x) => typeof x === 'string' && x.trim())));
|
|
42
|
+
if (tools.length === 0) return null;
|
|
43
|
+
const indent = ' '.repeat(depth);
|
|
44
|
+
return ` ${indent} ${c.muted}${tools.join(' · ')}${c.reset}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// A compact, dim, summarized description under each agent (frontmatter `description` via buildAgentCardTree's whatItDoes), truncated to ~90 chars on a clean boundary.
|
|
48
|
+
function agentDescLine(card, depth) {
|
|
49
|
+
if (!card.whatItDoes) return null;
|
|
50
|
+
const indent = ' '.repeat(depth);
|
|
51
|
+
return ` ${indent} ${c.dim}${summarize(sanitizeRenderText(card.whatItDoes), 90)}${c.reset}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function agentClassLine(card, depth, t) {
|
|
55
|
+
const indent = ' '.repeat(depth);
|
|
56
|
+
const cc = t.classification;
|
|
57
|
+
// FOURTH STATE (2026-08-03): the evaluation SUCCEEDED and left this agent out.
|
|
58
|
+
if (card.evaluationState === 'omitted') {
|
|
59
|
+
return ` ${indent} ${c.warning}${cc.agentEvalOmitted}${c.reset}`;
|
|
60
|
+
}
|
|
61
|
+
// No evaluation ran for this agent → no classification line.
|
|
62
|
+
if (!card.classification) return '';
|
|
63
|
+
if (card.classification.method === 'unclassified' || !card.classification.catalogId) {
|
|
64
|
+
return ` ${indent} ${c.muted}${cc.noCategory}${c.reset}`;
|
|
65
|
+
}
|
|
66
|
+
const { category, role, level } = card.classification;
|
|
67
|
+
const catLabel = (category && cc.categories[category]) || sanitizeRenderText(category);
|
|
68
|
+
const levelLabel = (level && cc.levels[level]) || sanitizeRenderText(level);
|
|
69
|
+
const bits = [];
|
|
70
|
+
// The FLOOR category is dimmed, not accented (issue 120).
|
|
71
|
+
const floor = isFloorCategory(category);
|
|
72
|
+
if (catLabel) {
|
|
73
|
+
bits.push(floor ? `${c.dim}${catLabel}${c.reset}` : `${c.primary}${catLabel}${c.reset}`);
|
|
74
|
+
}
|
|
75
|
+
// The floor's ROLE is dropped, and it is not a style preference.
|
|
76
|
+
if (role && !floor) bits.push(`${c.white}${sanitizeRenderText(role)}${c.reset}`);
|
|
77
|
+
if (levelLabel) bits.push(`${c.dim}${levelLabel}${c.reset}`);
|
|
78
|
+
return ` ${indent} ${bits.join(`${c.muted} · ${c.reset}`)}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// v4 (report req 3): the "how to improve" tips, one per line under the agent.
|
|
82
|
+
function agentImprovementLines(card, depth, t) {
|
|
83
|
+
const tips = Array.isArray(card.improvements) ? card.improvements : [];
|
|
84
|
+
if (tips.length === 0) return [];
|
|
85
|
+
const indent = ' '.repeat(depth);
|
|
86
|
+
const out = [` ${indent} ${c.dim}${t.classification.improvementsHeading}${c.reset}`];
|
|
87
|
+
for (const tip of tips) out.push(` ${indent} ${c.muted}- ${sanitizeRenderText(tip)}${c.reset}`);
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function printActivity(report, t, p) {
|
|
92
|
+
const s = report && report.sessions ? report.sessions : null;
|
|
93
|
+
const g = report && report.gitActivity ? report.gitActivity : null;
|
|
94
|
+
const w = report && report.workStreams ? report.workStreams : null;
|
|
95
|
+
if (!s && !g && !w) return false;
|
|
96
|
+
|
|
97
|
+
const a = t.terminal.activity;
|
|
98
|
+
p(` ${c.bold}${a.heading}${c.reset}`);
|
|
99
|
+
if (Array.isArray(report.analyzedRepos) && report.analyzedRepos.length) {
|
|
100
|
+
p(` ${c.dim}${a.repos(report.analyzedRepos.map(sanitizeRenderText).join(', '))}${c.reset}`);
|
|
101
|
+
}
|
|
102
|
+
if (s) {
|
|
103
|
+
const sub =
|
|
104
|
+
s.subagentRunCount > 0
|
|
105
|
+
? ` ${c.muted}·${c.reset} ${a.subagents(s.subagentRunCount)}`
|
|
106
|
+
: '';
|
|
107
|
+
p(` ${c.success}●${c.reset} ${a.sessions(s.sessionCount)}${sub}`);
|
|
108
|
+
if (typeof s.activeHours === 'number' && s.activeHours > 0) {
|
|
109
|
+
p(` ${c.success}●${c.reset} ${a.hours(s.activeHours)}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (g) {
|
|
113
|
+
p(
|
|
114
|
+
` ${c.success}●${c.reset} ${a.commits(g.authoredCommitCount, g.commitCount)} ${c.muted}·${c.reset} ${a.lines(g.linesAdded, g.linesDeleted)} ${c.muted}·${c.reset} ${a.velocity(g.velocity)}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (w) {
|
|
118
|
+
p(
|
|
119
|
+
` ${c.success}●${c.reset} ${a.workStreams(w.streamCount, w.multiDayStreams, w.maxStreamSpanDays)}`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
// Traction (ADR-039): recency-windowed signals from the local scan; agentsOnProfile
|
|
123
|
+
// is hub-only, shown only when the ai-profile preview has already been fetched.
|
|
124
|
+
const tr = tractionParts(report, s, a);
|
|
125
|
+
if (tr.length) p(` ${c.success}●${c.reset} ${a.tractionLabel}: ${tr.join(' · ')}`);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function tractionParts(report, s, a) {
|
|
130
|
+
const parts = [];
|
|
131
|
+
if (s && typeof s.sessions90d === 'number') parts.push(a.tSessions(s.sessions90d));
|
|
132
|
+
if (s && typeof s.activeDaysPerWeek === 'number') parts.push(a.tDays(s.activeDaysPerWeek));
|
|
133
|
+
const detected = report && report.agentCounts ? report.agentCounts.agents : null;
|
|
134
|
+
const onProfile = report && report.aiWorkPreview && report.aiWorkPreview.traction
|
|
135
|
+
? report.aiWorkPreview.traction.agentsOnProfile : null;
|
|
136
|
+
if (typeof detected === 'number') parts.push(a.tAgents(detected, typeof onProfile === 'number' ? onProfile : null));
|
|
137
|
+
const tools = detectedToolNames(report && report.tools);
|
|
138
|
+
if (tools.length) parts.push(a.tTools(tools.join(', ')));
|
|
139
|
+
return parts;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function printAgents(report, t, p, gate = DEFAULT_REPORT_GATE) {
|
|
143
|
+
const { childrenByParent, roots } = buildAgentCardTree(report, t);
|
|
144
|
+
// The names the evaluation left out.
|
|
145
|
+
const omittedNames = new Set(omittedByCall(report, MODEL_CALL.EVALUATION));
|
|
146
|
+
p(` ${c.bold}${label(t.html.diagramHeading, 'Agents')}${c.reset}`);
|
|
147
|
+
if (!roots.length) {
|
|
148
|
+
p(` ${c.muted} ${label(t.html.agentsEmpty, 'No configured AI agents detected.')}${c.reset}`);
|
|
149
|
+
p();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// THE ROOT THE AGENTS HANG FROM (issue 086 — restored, not invented).
|
|
153
|
+
p(` ${c.muted}${label(t.html.orchestratorLabel, 'Orchestrator')}${c.reset}`);
|
|
154
|
+
const visited = new Set();
|
|
155
|
+
const walk = (card, depth) => {
|
|
156
|
+
if (visited.has(card.name)) return;
|
|
157
|
+
visited.add(card.name);
|
|
158
|
+
p(agentLine(card, depth, t));
|
|
159
|
+
const desc = agentDescLine(card, depth);
|
|
160
|
+
if (desc) p(desc);
|
|
161
|
+
const toolsLine = agentToolsLine(card, depth);
|
|
162
|
+
if (toolsLine) p(toolsLine);
|
|
163
|
+
const classLine = agentClassLine(card, depth, t);
|
|
164
|
+
if (classLine) p(classLine);
|
|
165
|
+
if (gate.showAgentSuggestions) {
|
|
166
|
+
for (const line of agentImprovementLines(card, depth, t)) p(line);
|
|
167
|
+
}
|
|
168
|
+
const children = childrenByParent.get(card.name) || [];
|
|
169
|
+
for (const child of children) walk(child, depth + 1);
|
|
170
|
+
};
|
|
171
|
+
for (const root of roots) walk(root, 0);
|
|
172
|
+
// A single note when the local usage history is unavailable, so a blank usage
|
|
173
|
+
// column reads as "no local history" rather than "never used".
|
|
174
|
+
if (report.agentUsage && report.agentUsage.available === false) {
|
|
175
|
+
p(` ${c.dim}${t.terminal.agentUsageUnavailable}${c.reset}`);
|
|
176
|
+
}
|
|
177
|
+
// A PARTIAL RUN SAYS SO, AND BY NAME (2026-08-03).
|
|
178
|
+
if (callPartial(report, MODEL_CALL.EVALUATION)) {
|
|
179
|
+
const names = [...omittedNames].map(sanitizeRenderText).join(', ');
|
|
180
|
+
for (const line of wrap(t.classification.agentsEvalPartial(names, omittedNames.size))) p(` ${c.dim}${line}${c.reset}`);
|
|
181
|
+
}
|
|
182
|
+
// THE CASE THAT SAID NOTHING AT ALL (issue 116).
|
|
183
|
+
if (callFailed(report, MODEL_CALL.EVALUATION)) {
|
|
184
|
+
const detected = Array.isArray(report.agents) ? report.agents.length : 0;
|
|
185
|
+
const cls = t.classification;
|
|
186
|
+
// FOUR outcomes, because they ask the talent to do different things — and ADR-042 is what made three of them distinguishable at all.
|
|
187
|
+
const record = modelCall(report, MODEL_CALL.EVALUATION);
|
|
188
|
+
const reason = record && record.reason;
|
|
189
|
+
let notice;
|
|
190
|
+
if (callTimedOut(report, MODEL_CALL.EVALUATION)) notice = cls.agentsEvalTimedOut(detected);
|
|
191
|
+
else if (reason === REASON.NETWORK) notice = cls.agentsEvalUnreachable;
|
|
192
|
+
else if (reason === REASON.HTTP) notice = cls.agentsEvalErrored;
|
|
193
|
+
else notice = cls.agentsEvalMissing;
|
|
194
|
+
for (const line of wrap(notice)) p(` ${c.dim}${line}${c.reset}`);
|
|
195
|
+
}
|
|
196
|
+
p();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports = { printAgents, printActivity };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { palette } = require('./ansi');
|
|
4
|
+
const { BRAND_ANSI } = require('./brand-ansi');
|
|
5
|
+
|
|
6
|
+
const c = palette({
|
|
7
|
+
reset: '\x1b[0m',
|
|
8
|
+
bold: '\x1b[1m',
|
|
9
|
+
dim: '\x1b[2m',
|
|
10
|
+
primary: BRAND_ANSI.primary,
|
|
11
|
+
accent: BRAND_ANSI.accent,
|
|
12
|
+
success: BRAND_ANSI.success,
|
|
13
|
+
warning: BRAND_ANSI.warning,
|
|
14
|
+
danger: BRAND_ANSI.danger,
|
|
15
|
+
muted: BRAND_ANSI.muted,
|
|
16
|
+
white: '\x1b[97m',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function bar(score, width = 24) {
|
|
20
|
+
const filled = Math.round((score / 100) * width);
|
|
21
|
+
return '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ADR-016: a light visual break between the reordered sections — a dim rule
|
|
25
|
+
// with air around it, keeping the branded ANSI look and zero deps.
|
|
26
|
+
function sep(p) {
|
|
27
|
+
p();
|
|
28
|
+
p(` ${c.muted}${'─'.repeat(46)}${c.reset}`);
|
|
29
|
+
p();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// WRAPPING, added for issue 087.
|
|
33
|
+
const WRAP_COLS = 76;
|
|
34
|
+
|
|
35
|
+
function wrap(text, width = WRAP_COLS) {
|
|
36
|
+
const s = String(text || '').trim().replace(/\s+/g, ' ');
|
|
37
|
+
if (!s) return [];
|
|
38
|
+
const out = [];
|
|
39
|
+
let line = '';
|
|
40
|
+
for (const word of s.split(' ')) {
|
|
41
|
+
if (!line) line = word;
|
|
42
|
+
else if (`${line} ${word}`.length <= width) line += ` ${word}`;
|
|
43
|
+
else { out.push(line); line = word; }
|
|
44
|
+
}
|
|
45
|
+
if (line) out.push(line);
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// One step, wrapped, with its estimate.
|
|
50
|
+
function printStepLines(p, marker, text, estimate) {
|
|
51
|
+
const indent = ' '.repeat(4 + marker.length + 1);
|
|
52
|
+
const width = WRAP_COLS - indent.length;
|
|
53
|
+
const lines = wrap(text, width);
|
|
54
|
+
if (lines.length === 0) return;
|
|
55
|
+
// The estimate is measured, not just appended.
|
|
56
|
+
const plain = estimate ? ` (${estimate})` : '';
|
|
57
|
+
const coloured = estimate ? ` ${c.dim}(${estimate})${c.reset}` : '';
|
|
58
|
+
const last = lines.length - 1;
|
|
59
|
+
const fits = !estimate || lines[last].length + plain.length <= width;
|
|
60
|
+
lines.forEach((line, i) => {
|
|
61
|
+
const head = i === 0 ? ` ${marker} ` : indent;
|
|
62
|
+
p(`${head}${line}${i === last && fits ? coloured : ''}`);
|
|
63
|
+
});
|
|
64
|
+
if (estimate && !fits) p(`${indent}${c.dim}(${estimate})${c.reset}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// A LABELLED BLOCK: the label on its own dim line, the value wrapped and indented under it.
|
|
68
|
+
function labelledBlock(p, label, value, indent = ' ') {
|
|
69
|
+
if (!value) return;
|
|
70
|
+
p(` ${c.dim}${label}${c.reset}`);
|
|
71
|
+
for (const line of wrap(value, WRAP_COLS - indent.length)) p(`${indent}${c.muted}${line}${c.reset}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Terminal-SUMMARIZE (user feedback, 2026-07-16): the earlier condense (commit 465badb) over-trimmed — it left the terminal as headings + copyable prompts with the prose stripped out.
|
|
75
|
+
function summarize(text, max = 140) {
|
|
76
|
+
const s = String(text || '').trim().replace(/\s+/g, ' ');
|
|
77
|
+
if (s.length <= max) return s;
|
|
78
|
+
const slice = s.slice(0, max);
|
|
79
|
+
const lastStop = slice.lastIndexOf('. ');
|
|
80
|
+
if (lastStop >= max * 0.5) return slice.slice(0, lastStop + 1);
|
|
81
|
+
const lastSpace = slice.lastIndexOf(' ');
|
|
82
|
+
return (lastSpace > 0 ? slice.slice(0, lastSpace) : slice).replace(/[\s,;:.]+$/, '') + '…';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { c, bar, sep, wrap, printStepLines, labelledBlock, summarize, WRAP_COLS };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
4
|
+
const TICK_MS = 80;
|
|
5
|
+
|
|
6
|
+
function isInteractive(stream) {
|
|
7
|
+
return !!(stream && stream.isTTY);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ERASE_LINE = '\r\x1b[2K\r';
|
|
11
|
+
|
|
12
|
+
// Runs a SYNCHRONOUS `fn` while showing `label` as a static status line.
|
|
13
|
+
// Never animates (see header note: a blocking call can't yield ticks).
|
|
14
|
+
function withStaticStatus(label, fn, stream = process.stderr) {
|
|
15
|
+
if (!isInteractive(stream)) {
|
|
16
|
+
stream.write(` ${label}\n`);
|
|
17
|
+
return fn();
|
|
18
|
+
}
|
|
19
|
+
stream.write(` ${label}`);
|
|
20
|
+
try {
|
|
21
|
+
return fn();
|
|
22
|
+
} finally {
|
|
23
|
+
stream.write(ERASE_LINE);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Runs an ASYNC `task` (a zero-arg function returning a Promise) while showing an animated spinner with `label`.
|
|
28
|
+
async function withSpinner(label, task, stream = process.stderr) {
|
|
29
|
+
if (!isInteractive(stream)) {
|
|
30
|
+
stream.write(` ${label}\n`);
|
|
31
|
+
return task();
|
|
32
|
+
}
|
|
33
|
+
let frame = 0;
|
|
34
|
+
stream.write(`\r ${FRAMES[0]} ${label}`);
|
|
35
|
+
const timer = setInterval(() => {
|
|
36
|
+
frame = (frame + 1) % FRAMES.length;
|
|
37
|
+
stream.write(`\r ${FRAMES[frame]} ${label}`);
|
|
38
|
+
}, TICK_MS);
|
|
39
|
+
try {
|
|
40
|
+
return await task();
|
|
41
|
+
} finally {
|
|
42
|
+
clearInterval(timer);
|
|
43
|
+
stream.write(ERASE_LINE);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Manual spinner for a wait whose END isn't a single awaitable (e.g. streaming:
|
|
48
|
+
// spin until the FIRST token arrives). Returns an idempotent `stop()` that
|
|
49
|
+
// clears the line. On a non-TTY it prints the label once and `stop()` is a no-op.
|
|
50
|
+
function startSpinner(label, stream = process.stderr) {
|
|
51
|
+
if (!isInteractive(stream)) {
|
|
52
|
+
stream.write(` ${label}\n`);
|
|
53
|
+
return () => {};
|
|
54
|
+
}
|
|
55
|
+
let frame = 0;
|
|
56
|
+
stream.write(`\r ${FRAMES[0]} ${label}`);
|
|
57
|
+
const timer = setInterval(() => {
|
|
58
|
+
frame = (frame + 1) % FRAMES.length;
|
|
59
|
+
stream.write(`\r ${FRAMES[frame]} ${label}`);
|
|
60
|
+
}, TICK_MS);
|
|
61
|
+
let stopped = false;
|
|
62
|
+
return () => {
|
|
63
|
+
if (stopped) return;
|
|
64
|
+
stopped = true;
|
|
65
|
+
clearInterval(timer);
|
|
66
|
+
stream.write(ERASE_LINE);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const PHASE_MS = 8000;
|
|
71
|
+
|
|
72
|
+
// Pure: which phase index is active after `elapsedMs`.
|
|
73
|
+
function phaseAt(phaseCount, elapsedMs, phaseMs = PHASE_MS) {
|
|
74
|
+
if (!(phaseCount > 0)) return 0;
|
|
75
|
+
if (!(elapsedMs > 0) || !(phaseMs > 0)) return 0;
|
|
76
|
+
const i = Math.floor(elapsedMs / phaseMs);
|
|
77
|
+
return i >= phaseCount ? phaseCount - 1 : i;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function withPhasedSpinner(phases, task, { stream = process.stderr, phaseMs = PHASE_MS, tickMs = TICK_MS } = {}) {
|
|
81
|
+
const list = (Array.isArray(phases) ? phases : [phases]).filter((s) => typeof s === 'string' && s.length > 0);
|
|
82
|
+
const first = list[0] || '';
|
|
83
|
+
if (!isInteractive(stream)) {
|
|
84
|
+
if (first) stream.write(` ${first}\n`);
|
|
85
|
+
return task();
|
|
86
|
+
}
|
|
87
|
+
const start = Date.now();
|
|
88
|
+
let frame = 0;
|
|
89
|
+
let maxLen = 0;
|
|
90
|
+
const render = () => {
|
|
91
|
+
const copy = list[phaseAt(list.length, Date.now() - start, phaseMs)] || first;
|
|
92
|
+
const line = ` ${FRAMES[frame]} ${copy}`;
|
|
93
|
+
maxLen = Math.max(maxLen, line.length);
|
|
94
|
+
stream.write(`\r${line.padEnd(maxLen)}`); // padEnd clears a longer previous phase's tail
|
|
95
|
+
};
|
|
96
|
+
render();
|
|
97
|
+
const timer = setInterval(() => { frame = (frame + 1) % FRAMES.length; render(); }, tickMs);
|
|
98
|
+
try {
|
|
99
|
+
return await task();
|
|
100
|
+
} finally {
|
|
101
|
+
clearInterval(timer);
|
|
102
|
+
stream.write(ERASE_LINE);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { withStaticStatus, withSpinner, startSpinner, withPhasedSpinner, phaseAt, isInteractive };
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The MCP-services, tier-analysis, progression-ladder and tier-roadmap sections of the terminal report, extracted from src/render-terminal.js (structure refactor, issue 020).
|
|
4
|
+
|
|
5
|
+
const { getRoadmapEntry } = require('./roadmap-content');
|
|
6
|
+
const { analyzeTier, buildLadder } = require('./tier-analysis');
|
|
7
|
+
const { mergeRoadmapPersonalization } = require('./roadmap-personalization');
|
|
8
|
+
const { buildImplementationPrompt } = require('./roadmap-prompt');
|
|
9
|
+
const { sanitizeRenderText } = require('./sanitize-network-text');
|
|
10
|
+
const { callFailed, MODEL_CALL } = require('./model-call-record');
|
|
11
|
+
const { projectNextTier } = require('./tier-projection');
|
|
12
|
+
const { c, sep, wrap, summarize, labelledBlock, printStepLines } = require('./terminal-format');
|
|
13
|
+
|
|
14
|
+
function printMcpServices(report, t, p) {
|
|
15
|
+
const mcp = report && report.mcp ? report.mcp : null;
|
|
16
|
+
const services = mcp && Array.isArray(mcp.services) ? mcp.services : [];
|
|
17
|
+
const unidentified = mcp && typeof mcp.unidentified === 'number' ? mcp.unidentified : 0;
|
|
18
|
+
|
|
19
|
+
p(` ${c.bold}${t.html.mcpHeading}${c.reset}`);
|
|
20
|
+
if (services.length === 0 && unidentified === 0) {
|
|
21
|
+
for (const line of wrap(t.html.mcpEmpty)) p(` ${c.muted}${line}${c.reset}`);
|
|
22
|
+
p();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (services.length) {
|
|
26
|
+
const shown = services.map((s) => `${sanitizeRenderText(s.label)}${s.count > 1 ? ` ×${s.count}` : ''}`);
|
|
27
|
+
p(` ${c.primary}${shown.join(`${c.reset}${c.muted} · ${c.reset}${c.primary}`)}${c.reset}`);
|
|
28
|
+
}
|
|
29
|
+
// Stated, never omitted: the list has to account for every server the tier
|
|
30
|
+
// counted, or the screen shows two different numbers for one set (issue 096).
|
|
31
|
+
if (unidentified > 0) {
|
|
32
|
+
for (const line of wrap(t.html.mcpUnidentified(unidentified))) p(` ${c.dim}${line}${c.reset}`);
|
|
33
|
+
}
|
|
34
|
+
p();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// tier analysis: why this tier (summarized) Same deterministic source (src/tier-analysis.js) as the HTML report.
|
|
38
|
+
|
|
39
|
+
function printTierAnalysis(report, t, p) {
|
|
40
|
+
const analysis = analyzeTier(report, t);
|
|
41
|
+
const tt = t.tierAnalysis;
|
|
42
|
+
|
|
43
|
+
p(` ${c.bold}${tt.heading}${c.reset}`);
|
|
44
|
+
// Keep the first sentence of the intro ("Your current tier is X (Name).") —
|
|
45
|
+
// the rest explains the engine mechanics at length, which stays HTML-only.
|
|
46
|
+
const introFull = tt.intro(analysis.tierKey, analysis.tierName);
|
|
47
|
+
const firstStop = introFull.indexOf('. ');
|
|
48
|
+
const intro = firstStop >= 0 ? introFull.slice(0, firstStop + 1) : introFull;
|
|
49
|
+
p(` ${c.muted}${intro}${c.reset}`);
|
|
50
|
+
|
|
51
|
+
// Summarized met-criteria checklist (reintroduced 2026-07-16): the criteria
|
|
52
|
+
// already satisfied, one concise line each. Full-length wording stays in HTML.
|
|
53
|
+
if (Array.isArray(analysis.metCriteria) && analysis.metCriteria.length) {
|
|
54
|
+
p(` ${c.dim}${tt.metHeading}${c.reset}`);
|
|
55
|
+
analysis.metCriteria.forEach((m) => p(` ${c.success}✓${c.reset} ${c.muted}${summarize(m.text, 110)}${c.reset}`));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (analysis.blockingCriterion) {
|
|
59
|
+
p(` ${c.bold}${c.warning}${tt.blockingLabel}${c.reset}`);
|
|
60
|
+
p(` ${c.white}${analysis.blockingCriterion}${c.reset}`);
|
|
61
|
+
} else {
|
|
62
|
+
p(` ${c.white}${tt.maxTierNote}${c.reset}`);
|
|
63
|
+
}
|
|
64
|
+
p();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function ladderMark(status) {
|
|
68
|
+
if (status === 'done') return `${c.success}✓${c.reset}`;
|
|
69
|
+
if (status === 'current') return `${c.primary}${c.bold}●${c.reset}`;
|
|
70
|
+
return `${c.muted}○${c.reset}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function printLadder(report, t, p) {
|
|
74
|
+
const ld = t.ladder;
|
|
75
|
+
const { setupLevels } = buildLadder(report, t);
|
|
76
|
+
const allTiers = setupLevels.flatMap((lvl) => lvl.tiers);
|
|
77
|
+
const done = allTiers.filter((x) => x.status === 'done').length;
|
|
78
|
+
const current = allTiers.filter((x) => x.status === 'current').length;
|
|
79
|
+
const pending = allTiers.filter((x) => x.status === 'pending').length;
|
|
80
|
+
|
|
81
|
+
p(` ${c.bold}${ld.setupHeading}${c.reset}`);
|
|
82
|
+
p(` ${c.muted}${ld.setupIntro}${c.reset}`);
|
|
83
|
+
p(` ${c.dim}${ld.legend(done, current, pending)}${c.reset}`);
|
|
84
|
+
p();
|
|
85
|
+
// NESTED (ADR-016): each SETUP LEVEL, then its tiers indented beneath.
|
|
86
|
+
for (const lvl of setupLevels) {
|
|
87
|
+
const nameStyle = lvl.status === 'pending' ? c.muted : `${c.bold}${c.white}`;
|
|
88
|
+
const keys = lvl.tierKeys.length ? ` ${c.muted}— [${lvl.tierKeys.join(', ')}]${c.reset}` : '';
|
|
89
|
+
// "You are here" lives ONLY on the current TIER, not the level header (user request).
|
|
90
|
+
p(` ${ladderMark(lvl.status)} ${nameStyle}${lvl.emoji} ${lvl.label}${c.reset}${keys}`);
|
|
91
|
+
p(` ${c.dim}${lvl.description}${c.reset}`);
|
|
92
|
+
for (const tier of lvl.tiers) {
|
|
93
|
+
const tierNameStyle = tier.status === 'pending' ? c.muted : c.white;
|
|
94
|
+
const tierBadge = tier.status === 'current' ? ` ${c.primary}${ld.currentLabel}${c.reset}` : '';
|
|
95
|
+
p(` ${ladderMark(tier.status)} ${c.bold}${tier.tierKey}${c.reset} ${tierNameStyle}${tier.name}${c.reset}${tierBadge}`);
|
|
96
|
+
p(` ${c.dim}${tier.description}${c.reset}`);
|
|
97
|
+
if (tier.unlock) {
|
|
98
|
+
p(` ${c.muted}${ld.unlockLabel}: ${tier.unlock}${c.reset}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
p();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// One side of the before/after, in the SAME format for both — the comparison is the feature, so a second formatter here would defeat it.
|
|
106
|
+
function projectionLine(side, t) {
|
|
107
|
+
const name = (t.tierNames && t.tierNames[side.tierKey]) || side.tierKey;
|
|
108
|
+
const setup = (t.setupLevels && t.setupLevels[side.setupLevelKey] && t.setupLevels[side.setupLevelKey].label)
|
|
109
|
+
|| side.setupLevelKey;
|
|
110
|
+
return `${side.tierKey} · ${name} · ${setup} · ${side.score}/100`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function printProjection(report, t, p) {
|
|
114
|
+
const projection = projectNextTier(report);
|
|
115
|
+
if (!projection) return;
|
|
116
|
+
|
|
117
|
+
p();
|
|
118
|
+
p(` ${c.dim}${t.html.roadmapNowLabel}${c.reset}`);
|
|
119
|
+
p(` ${c.white}${projectionLine(projection.current, t)}${c.reset}`);
|
|
120
|
+
p();
|
|
121
|
+
// The tag rides ON the heading, not in a footnote: a reader who skims the two
|
|
122
|
+
// numbers must still see that the second one is a projection.
|
|
123
|
+
p(` ${c.dim}${t.html.roadmapProjectionLabel}${c.reset} ${c.warning}[${t.html.roadmapProjectionTag}]${c.reset}`);
|
|
124
|
+
p(` ${c.white}${projectionLine(projection.projected, t)}${c.reset}`);
|
|
125
|
+
for (const line of wrap(t.html.roadmapProjectionNote)) p(` ${c.dim}${line}${c.reset}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function printRoadmap(report, maturity, t, lang, p) {
|
|
129
|
+
const tierKey = maturity && maturity.tierKey;
|
|
130
|
+
const curatedEntry = tierKey ? getRoadmapEntry(tierKey, lang) : null;
|
|
131
|
+
|
|
132
|
+
if (!curatedEntry) {
|
|
133
|
+
const nextStep = t.nextSteps[maturity.level] || maturity.next;
|
|
134
|
+
p(` ${c.bold}${c.warning}${t.terminal.nextStep}${c.reset}`);
|
|
135
|
+
p(` ${c.white}${nextStep}${c.reset}`);
|
|
136
|
+
p();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
p(` ${c.bold}${c.warning}${t.html.roadmapHeading}${c.reset}`);
|
|
141
|
+
// Issue 083: the register, stated before the content.
|
|
142
|
+
// Wrapped like every other prose line in this block (issue 087): it was the one line still running past the 80th column, which is the shape the squad read as a dump.
|
|
143
|
+
for (const line of wrap(t.html.roadmapSubheading)) p(` ${c.dim}${line}${c.reset}`);
|
|
144
|
+
|
|
145
|
+
if (curatedEntry.contentUnavailable) {
|
|
146
|
+
p(` ${c.white}${t.html.roadmapContentUnavailable}${c.reset}`);
|
|
147
|
+
if (!curatedEntry.maxTier) p(` ${c.dim}${t.cli.buildNextLevelHint}${c.reset}`);
|
|
148
|
+
p();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const personalization = report && report.roadmapPersonalization;
|
|
153
|
+
const entry = mergeRoadmapPersonalization(curatedEntry, personalization);
|
|
154
|
+
|
|
155
|
+
// Issue 087: the jump title gets air and its own line, and everything below it hangs off it.
|
|
156
|
+
// THE BEFORE AND THE AFTER (issue 084), above the steps and not below them.
|
|
157
|
+
printProjection(report, t, p);
|
|
158
|
+
|
|
159
|
+
p();
|
|
160
|
+
p(` ${c.white}${c.bold}${sanitizeRenderText(entry.title)}${c.reset}`);
|
|
161
|
+
p();
|
|
162
|
+
|
|
163
|
+
// Issue 109: whether this roadmap is YOURS or the template, said out loud.
|
|
164
|
+
if (personalization) {
|
|
165
|
+
p(` ${c.dim}${t.html.roadmapPersonalizedNotice}${c.reset}`);
|
|
166
|
+
p();
|
|
167
|
+
} else if (callFailed(report, MODEL_CALL.ROADMAP)) {
|
|
168
|
+
for (const line of wrap(t.html.roadmapNotPersonalizedNotice)) p(` ${c.dim}${line}${c.reset}`);
|
|
169
|
+
p();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (entry.maxTier) {
|
|
173
|
+
// ADR-008 (skill-code-certification): T7 is NOT a dead end.
|
|
174
|
+
if (entry.whatRemains) {
|
|
175
|
+
for (const line of wrap(sanitizeRenderText(entry.whatRemains))) p(` ${c.muted}${line}${c.reset}`);
|
|
176
|
+
p();
|
|
177
|
+
}
|
|
178
|
+
if (Array.isArray(entry.consolidationSteps) && entry.consolidationSteps.length) {
|
|
179
|
+
p(` ${c.dim}${t.html.roadmapConsolidationLabel}:${c.reset}`);
|
|
180
|
+
entry.consolidationSteps.forEach((step) => printStepLines(p, `${c.success}•${c.reset}`, sanitizeRenderText(step), null));
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
labelledBlock(p, t.html.roadmapUpgradeWhenLabel, sanitizeRenderText(entry.upgradeWhen));
|
|
184
|
+
if (entry.upgradeWhen) p();
|
|
185
|
+
labelledBlock(p, `${t.html.roadmapUnlocksLabel}:`, sanitizeRenderText(entry.unlocks));
|
|
186
|
+
if (entry.unlocks) p();
|
|
187
|
+
if (Array.isArray(entry.steps) && entry.steps.length) {
|
|
188
|
+
p(` ${c.dim}${t.html.roadmapStepsLabel}:${c.reset}`);
|
|
189
|
+
// Numbered, wrapped, and with the estimate on the same line as the step it belongs to.
|
|
190
|
+
const width = String(entry.steps.length).length;
|
|
191
|
+
entry.steps.forEach((step, i) => {
|
|
192
|
+
const marker = `${String(i + 1).padStart(width)}.`;
|
|
193
|
+
printStepLines(p, marker, sanitizeRenderText(step.text), sanitizeRenderText(step.estimate));
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
{
|
|
199
|
+
const promptText = buildImplementationPrompt(entry, report, maturity, lang);
|
|
200
|
+
if (promptText) {
|
|
201
|
+
// Issue 087: THE wall of text.
|
|
202
|
+
sep(p);
|
|
203
|
+
p(` ${c.bold}${t.html.implementationPromptHeading}${c.reset}`);
|
|
204
|
+
for (const line of wrap(t.html.implementationPromptHint)) p(` ${c.dim}${line}${c.reset}`);
|
|
205
|
+
p();
|
|
206
|
+
const rule = `${'─'.repeat(48)}`;
|
|
207
|
+
p(` ${c.muted}${rule}${c.reset}`);
|
|
208
|
+
for (const line of promptText.split('\n')) p(` ${sanitizeRenderText(line)}`);
|
|
209
|
+
p(` ${c.muted}${rule}${c.reset}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// --build-next-level (issue 021) is now a SECONDARY, opt-in alternative to the prompt above — never when already at the terminal T7 entry (there is nothing left to build).
|
|
214
|
+
if (!entry.maxTier) p(` ${c.dim}${t.cli.buildNextLevelHint}${c.reset}`);
|
|
215
|
+
|
|
216
|
+
p();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = { printMcpServices, printTierAnalysis, printLadder, printRoadmap };
|