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,126 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { computeTierResult, setupLevelForTier, SETUP_LEVELS, TIERS } = require('./tier-engine');
|
|
4
|
+
|
|
5
|
+
// One entry per tier BOUNDARY (T(n-1) -> Tn), in ladder order.
|
|
6
|
+
const CRITERIA = [
|
|
7
|
+
{
|
|
8
|
+
toTier: 1,
|
|
9
|
+
met: (s) => s.totalDetected >= 1,
|
|
10
|
+
metText: (s, tt) => tt.criterion.t1Met(s.totalDetected),
|
|
11
|
+
blockingText: (s, tt) => tt.criterion.t1Blocking(s.totalDetected),
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
toTier: 2,
|
|
15
|
+
met: (s) => s.context >= 1,
|
|
16
|
+
metText: (s, tt) => tt.criterion.t2Met(s.context),
|
|
17
|
+
blockingText: (s, tt) => tt.criterion.t2Blocking(s.context),
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
toTier: 3,
|
|
21
|
+
met: (s) => s.mcp >= 1,
|
|
22
|
+
metText: (s, tt) => tt.criterion.t3Met(s.mcp),
|
|
23
|
+
blockingText: (s, tt) => tt.criterion.t3Blocking(s.mcp),
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
toTier: 4,
|
|
27
|
+
met: (s) => s.custom >= 1,
|
|
28
|
+
metText: (s, tt) => tt.criterion.t4Met(s.custom),
|
|
29
|
+
blockingText: (s, tt) => tt.criterion.t4Blocking(s.custom),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
toTier: 5,
|
|
33
|
+
met: (s) => s.hasAgentic && s.mcp >= 1 && s.custom >= 1,
|
|
34
|
+
metText: (s, tt) => tt.criterion.t5Met(s.hasAgentic, s.mcp, s.custom),
|
|
35
|
+
blockingText: (s, tt) => tt.criterion.t5Blocking(s.hasAgentic, s.mcp, s.custom),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
toTier: 6,
|
|
39
|
+
met: (s) => s.agentCount >= 2,
|
|
40
|
+
metText: (s, tt) => tt.criterion.t6Met(s.agentCount),
|
|
41
|
+
blockingText: (s, tt) => tt.criterion.t6Blocking(s.agentCount),
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
toTier: 7,
|
|
45
|
+
met: (s) => s.hooks >= 1,
|
|
46
|
+
metText: (s, tt) => tt.criterion.t7Met(s.hooks),
|
|
47
|
+
blockingText: (s, tt) => tt.criterion.t7Blocking(s.hooks),
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
// Builds the full analysis for a report: `{ tier, tierKey, tierName, band, signals, metCriteria, blockingCriterion }`.
|
|
52
|
+
function analyzeTier(report, t) {
|
|
53
|
+
const tt = t.tierAnalysis;
|
|
54
|
+
const result = computeTierResult(report || {});
|
|
55
|
+
const { signals, tier } = result;
|
|
56
|
+
|
|
57
|
+
const tierName = (t.tierNames && t.tierNames[result.tierKey]) || result.tierName;
|
|
58
|
+
|
|
59
|
+
const metCriteria = CRITERIA.filter((c) => c.toTier <= tier).map((c) => ({
|
|
60
|
+
toTier: c.toTier,
|
|
61
|
+
text: c.metText(signals, tt),
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
const blockingEntry = tier < 7 ? CRITERIA.find((c) => c.toTier === tier + 1) : null;
|
|
65
|
+
const blockingCriterion = blockingEntry ? blockingEntry.blockingText(signals, tt) : null;
|
|
66
|
+
|
|
67
|
+
return { ...result, tierName, metCriteria, blockingCriterion };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildLadder(report, t) {
|
|
71
|
+
const tt = t.tierAnalysis;
|
|
72
|
+
const ld = t.ladder;
|
|
73
|
+
const result = computeTierResult(report || {});
|
|
74
|
+
const { signals, tier } = result;
|
|
75
|
+
const currentSetupRank = setupLevelForTier(tier).rank;
|
|
76
|
+
|
|
77
|
+
const statusFor = (index, current) =>
|
|
78
|
+
index < current ? 'done' : index === current ? 'current' : 'pending';
|
|
79
|
+
|
|
80
|
+
const tierNode = (meta) => {
|
|
81
|
+
const status = statusFor(meta.tier, tier);
|
|
82
|
+
// Pending tiers carry the exact criterion that unlocks them (reused from the
|
|
83
|
+
// tier-analysis CRITERIA, already localized). done/current show none.
|
|
84
|
+
let unlock = null;
|
|
85
|
+
if (status === 'pending') {
|
|
86
|
+
const crit = CRITERIA.find((c) => c.toTier === meta.tier);
|
|
87
|
+
unlock = crit ? crit.blockingText(signals, tt) : null;
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
tier: meta.tier,
|
|
91
|
+
tierKey: meta.key,
|
|
92
|
+
name: (t.tierNames && t.tierNames[meta.key]) || meta.name,
|
|
93
|
+
description: (ld && ld.tierDesc && ld.tierDesc[meta.key]) || '',
|
|
94
|
+
status,
|
|
95
|
+
unlock,
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// NESTED ladder: each SETUP LEVEL groups the TIERS that compose it.
|
|
100
|
+
const tiersBySetup = new Map();
|
|
101
|
+
for (const meta of TIERS) {
|
|
102
|
+
const key = setupLevelForTier(meta.tier).key;
|
|
103
|
+
if (!tiersBySetup.has(key)) tiersBySetup.set(key, []);
|
|
104
|
+
tiersBySetup.get(key).push(tierNode(meta));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const setupLevels = SETUP_LEVELS.map((meta) => {
|
|
108
|
+
const tiers = (tiersBySetup.get(meta.key) || []).sort((a, b) => a.tier - b.tier);
|
|
109
|
+
const copy = (t.setupLevels && t.setupLevels[meta.key]) || {};
|
|
110
|
+
return {
|
|
111
|
+
key: meta.key,
|
|
112
|
+
code: meta.code,
|
|
113
|
+
rank: meta.rank,
|
|
114
|
+
emoji: meta.emoji,
|
|
115
|
+
label: copy.label || meta.key,
|
|
116
|
+
description: copy.desc || '',
|
|
117
|
+
status: statusFor(meta.rank, currentSetupRank),
|
|
118
|
+
tierKeys: tiers.map((x) => x.tierKey),
|
|
119
|
+
tiers,
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return { currentTier: tier, currentSetupRank, setupLevels };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { analyzeTier, buildLadder, CRITERIA };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const TIERS = [
|
|
4
|
+
{ tier: 0, key: 'T0', name: 'Banco vacío', means: 'No AI tools detected yet' },
|
|
5
|
+
{ tier: 1, key: 'T1', name: 'Primera herramienta', means: 'At least one AI tool in use' },
|
|
6
|
+
{ tier: 2, key: 'T2', name: 'Banco con notas', means: 'AI tools plus project context (instructions, config, rules)' },
|
|
7
|
+
{ tier: 3, key: 'T3', name: 'Banco conectado', means: 'Adds an MCP server integration' },
|
|
8
|
+
{ tier: 4, key: 'T4', name: 'Herramienta propia', means: 'Adds your own customization (skills, commands, rules)' },
|
|
9
|
+
{ tier: 5, key: 'T5', name: 'Operador agéntico', means: 'An agentic tool wired with MCP and customization' },
|
|
10
|
+
{ tier: 6, key: 'T6', name: 'Multi-agente', means: 'Two or more custom agents' },
|
|
11
|
+
{ tier: 7, key: 'T7', name: 'Taller orquestado', means: 'Custom agents plus automation hooks (orchestrated)' },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
function tierLadder() {
|
|
15
|
+
return TIERS.map((t) => ({ key: t.key, name: t.name, means: t.means }));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const AGENTIC_IDS = ['claude-code', 'aider', 'gemini-cli', 'codex-cli', 'amazon-q-developer'];
|
|
19
|
+
|
|
20
|
+
// Band 0-4 derived from tier (level-model.md, single source of truth): index = tier -> band.
|
|
21
|
+
const BAND_BY_TIER = [0, 1, 2, 3, 3, 4, 4, 4];
|
|
22
|
+
|
|
23
|
+
// Setup Level (Talent Certification Framework, ADR-016): the 3-value rollup that REPLACES the retired 0-4 band on every display surface.
|
|
24
|
+
const SETUP_LEVELS = [
|
|
25
|
+
{ key: 'none', code: null, rank: 0, emoji: '○' },
|
|
26
|
+
{ key: 'S1', code: 'S1', rank: 1, emoji: '◔' },
|
|
27
|
+
{ key: 'S2', code: 'S2', rank: 2, emoji: '◑' },
|
|
28
|
+
{ key: 'S3', code: 'S3', rank: 3, emoji: '●' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// tier -> setup-level key.
|
|
32
|
+
const SETUP_LEVEL_BY_TIER = ['none', 'S1', 'S1', 'S2', 'S2', 'S3', 'S3', 'S3'];
|
|
33
|
+
|
|
34
|
+
function setupLevelForTier(tier) {
|
|
35
|
+
const key = SETUP_LEVEL_BY_TIER[tier] ?? 'none';
|
|
36
|
+
return SETUP_LEVELS.find((s) => s.key === key) || SETUP_LEVELS[0];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Aggregates the raw signals the ladder needs, straight from the report's existing fields — not a new detector, only a sum over what scanner.js already produces.
|
|
40
|
+
function aggregateTierSignals(report) {
|
|
41
|
+
const tools = report && Array.isArray(report.tools) ? report.tools : [];
|
|
42
|
+
const detected = tools.filter((t) => t && t.detected);
|
|
43
|
+
|
|
44
|
+
let context = 0; // instructions + config + rules (T2)
|
|
45
|
+
let mcp = 0; // configured MCP servers (T3)
|
|
46
|
+
let custom = 0; // skills + commands + rules, own assets (T4)
|
|
47
|
+
let hooks = 0; // hook-based automation (T7)
|
|
48
|
+
|
|
49
|
+
for (const t of detected) {
|
|
50
|
+
const d = t.depth || {};
|
|
51
|
+
context += (d.instructions || 0) + (d.config || 0) + (d.rules || 0);
|
|
52
|
+
mcp += d.mcpServers || 0;
|
|
53
|
+
custom += (d.skills || 0) + (d.commands || 0) + (d.rules || 0);
|
|
54
|
+
hooks += d.hooks || 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const hasAgentic = detected.some((t) => AGENTIC_IDS.includes(t.id));
|
|
58
|
+
const agentCount =
|
|
59
|
+
report && report.agentCounts && typeof report.agentCounts.agents === 'number'
|
|
60
|
+
? report.agentCounts.agents
|
|
61
|
+
: 0;
|
|
62
|
+
|
|
63
|
+
// THE MCP SIGNAL IS THE DEDUPED SET THE REPORT SHOWS (issue 112).
|
|
64
|
+
const mcpTotal =
|
|
65
|
+
report && report.mcp && typeof report.mcp.total === 'number' ? report.mcp.total : mcp;
|
|
66
|
+
|
|
67
|
+
return { totalDetected: detected.length, context, mcp: mcpTotal, custom, hooks, hasAgentic, agentCount };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function computeTier(signals) {
|
|
71
|
+
let tier = 0;
|
|
72
|
+
if (signals.totalDetected >= 1) tier = 1;
|
|
73
|
+
if (tier === 1 && signals.context >= 1) tier = 2;
|
|
74
|
+
if (tier === 2 && signals.mcp >= 1) tier = 3;
|
|
75
|
+
if (tier === 3 && signals.custom >= 1) tier = 4;
|
|
76
|
+
if (tier === 4 && signals.hasAgentic && signals.mcp >= 1 && signals.custom >= 1) tier = 5;
|
|
77
|
+
if (tier === 5 && signals.agentCount >= 2) tier = 6;
|
|
78
|
+
if (tier === 6 && signals.hooks >= 1) tier = 7;
|
|
79
|
+
return tier;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function bandForTier(tier) {
|
|
83
|
+
return BAND_BY_TIER[tier] ?? 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Computes the full tier result for a report: {tier, tierKey, tierName, band, signals}.
|
|
87
|
+
function computeTierResult(report) {
|
|
88
|
+
const signals = aggregateTierSignals(report || {});
|
|
89
|
+
const tier = computeTier(signals);
|
|
90
|
+
const meta = TIERS[tier];
|
|
91
|
+
return {
|
|
92
|
+
tier: meta.tier,
|
|
93
|
+
tierKey: meta.key,
|
|
94
|
+
tierName: meta.name,
|
|
95
|
+
band: bandForTier(tier),
|
|
96
|
+
// Setup Level (ADR-016) — the shown rollup. `{key, code, rank, emoji}`;
|
|
97
|
+
// the label is resolved from i18n by `key` at the render layer.
|
|
98
|
+
setupLevel: setupLevelForTier(tier),
|
|
99
|
+
signals,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
computeTierResult,
|
|
105
|
+
tierLadder,
|
|
106
|
+
computeTier,
|
|
107
|
+
aggregateTierSignals,
|
|
108
|
+
bandForTier,
|
|
109
|
+
setupLevelForTier,
|
|
110
|
+
SETUP_LEVELS,
|
|
111
|
+
SETUP_LEVEL_BY_TIER,
|
|
112
|
+
AGENTIC_IDS,
|
|
113
|
+
TIERS,
|
|
114
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { classify } = require('./maturity');
|
|
4
|
+
const { computeTierResult, aggregateTierSignals, computeTier } = require('./tier-engine');
|
|
5
|
+
|
|
6
|
+
// Where this roadmap leaves you, computed with the REAL engine (issue 084).
|
|
7
|
+
|
|
8
|
+
// One entry per TARGET tier: the smallest report-shape changes that satisfy that tier's criterion.
|
|
9
|
+
const AGENTIC_TOOL_ID = 'claude-code';
|
|
10
|
+
|
|
11
|
+
function cloneReport(report) {
|
|
12
|
+
return JSON.parse(JSON.stringify(report));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function detectedTools(r) {
|
|
16
|
+
return (Array.isArray(r.tools) ? r.tools : []).filter((t) => t && t.detected);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Every depth bump needs SOME detected tool to hang off, because the engine only aggregates depth over detected tools.
|
|
20
|
+
function ensureDetectedTool(r, { agentic = false } = {}) {
|
|
21
|
+
if (!Array.isArray(r.tools)) r.tools = [];
|
|
22
|
+
const existing = detectedTools(r);
|
|
23
|
+
const wanted = agentic ? existing.find((t) => t.id === AGENTIC_TOOL_ID) : existing[0];
|
|
24
|
+
if (wanted) {
|
|
25
|
+
if (!wanted.depth || typeof wanted.depth !== 'object') wanted.depth = {};
|
|
26
|
+
return wanted;
|
|
27
|
+
}
|
|
28
|
+
const tool = {
|
|
29
|
+
id: agentic ? AGENTIC_TOOL_ID : 'projected-tool',
|
|
30
|
+
name: agentic ? 'Claude Code' : 'AI tool',
|
|
31
|
+
detected: true,
|
|
32
|
+
depth: {},
|
|
33
|
+
footprint: null,
|
|
34
|
+
recency: { lastModified: null, daysSinceModified: null, bucket: null },
|
|
35
|
+
};
|
|
36
|
+
r.tools.push(tool);
|
|
37
|
+
return tool;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const bumpDepth = (field) => (r) => {
|
|
41
|
+
const tool = ensureDetectedTool(r);
|
|
42
|
+
tool.depth[field] = (tool.depth[field] || 0) + 1;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// "Configure one MCP server" needs its own delta, and the reason is a collision worth recording (issue 112 landing after issue 084).
|
|
46
|
+
const addMcpServer = (r) => {
|
|
47
|
+
const tool = ensureDetectedTool(r);
|
|
48
|
+
tool.depth.mcpServers = (tool.depth.mcpServers || 0) + 1;
|
|
49
|
+
if (!r.mcp || typeof r.mcp !== 'object') r.mcp = { servers: [], countsByCategory: {}, total: 0, byTool: {} };
|
|
50
|
+
r.mcp.total = (typeof r.mcp.total === 'number' ? r.mcp.total : 0) + 1;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const DELTAS = {
|
|
54
|
+
1: [(r) => { ensureDetectedTool(r); }],
|
|
55
|
+
2: [bumpDepth('instructions')],
|
|
56
|
+
3: [addMcpServer],
|
|
57
|
+
4: [bumpDepth('skills')],
|
|
58
|
+
5: [
|
|
59
|
+
(r) => { ensureDetectedTool(r, { agentic: true }); },
|
|
60
|
+
addMcpServer,
|
|
61
|
+
bumpDepth('skills'),
|
|
62
|
+
],
|
|
63
|
+
6: [(r) => {
|
|
64
|
+
if (!r.agentCounts || typeof r.agentCounts !== 'object') r.agentCounts = {};
|
|
65
|
+
r.agentCounts.agents = Math.max(2, (r.agentCounts.agents || 0) + 1);
|
|
66
|
+
}],
|
|
67
|
+
7: [bumpDepth('hooks')],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const MAX_TIER = 7;
|
|
71
|
+
|
|
72
|
+
function projectNextTier(report) {
|
|
73
|
+
if (!report || typeof report !== 'object') return null;
|
|
74
|
+
|
|
75
|
+
const currentTierResult = computeTierResult(report);
|
|
76
|
+
const currentTier = currentTierResult.tier;
|
|
77
|
+
if (currentTier >= MAX_TIER) return null;
|
|
78
|
+
|
|
79
|
+
const deltas = DELTAS[currentTier + 1];
|
|
80
|
+
if (!deltas) return null;
|
|
81
|
+
|
|
82
|
+
let projectedReport;
|
|
83
|
+
try {
|
|
84
|
+
projectedReport = cloneReport(report);
|
|
85
|
+
for (const apply of deltas) apply(projectedReport);
|
|
86
|
+
} catch {
|
|
87
|
+
// A report shape this cannot clone or mutate is a report this must not
|
|
88
|
+
// guess about: no projection is better than a made-up one.
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const projectedTierResult = computeTierResult(projectedReport);
|
|
93
|
+
// Guard against a delta that no longer satisfies its criterion (a ladder
|
|
94
|
+
// change): under-projecting silently would be worse than saying nothing.
|
|
95
|
+
if (projectedTierResult.tier <= currentTier) return null;
|
|
96
|
+
|
|
97
|
+
const currentMaturity = classify(report);
|
|
98
|
+
const projectedMaturity = classify(projectedReport);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
current: {
|
|
102
|
+
tier: currentTier,
|
|
103
|
+
tierKey: currentTierResult.tierKey,
|
|
104
|
+
setupLevelKey: currentTierResult.setupLevel.key,
|
|
105
|
+
score: currentMaturity.score,
|
|
106
|
+
},
|
|
107
|
+
projected: {
|
|
108
|
+
tier: projectedTierResult.tier,
|
|
109
|
+
tierKey: projectedTierResult.tierKey,
|
|
110
|
+
setupLevelKey: projectedTierResult.setupLevel.key,
|
|
111
|
+
score: projectedMaturity.score,
|
|
112
|
+
},
|
|
113
|
+
cascaded: projectedTierResult.tier > currentTier + 1,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { projectNextTier, DELTAS, aggregateTierSignals, computeTier };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { iterateSessionObjects } = require('./session-scan');
|
|
4
|
+
|
|
5
|
+
// Human-turn reader for the steering/decisions extractors (hybrid evaluation, Slice 2).
|
|
6
|
+
|
|
7
|
+
function isHumanTurn(obj) {
|
|
8
|
+
if (!obj || typeof obj !== 'object') return false;
|
|
9
|
+
const role = obj.message && obj.message.role;
|
|
10
|
+
return obj.type === 'user' || role === 'user';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function messageOf(obj) {
|
|
14
|
+
return obj.message && typeof obj.message === 'object' ? obj.message : obj;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function* textsFromContent(content) {
|
|
18
|
+
if (typeof content === 'string') {
|
|
19
|
+
yield content;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (!Array.isArray(content)) return;
|
|
23
|
+
for (const part of content) {
|
|
24
|
+
if (part && part.type === 'text' && typeof part.text === 'string') {
|
|
25
|
+
yield part.text;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Yields each human-authored text turn from a SINGLE already-parsed session object.
|
|
31
|
+
function* humanTextsFromObject(obj, { minLength = 4 } = {}) {
|
|
32
|
+
if (!isHumanTurn(obj)) return;
|
|
33
|
+
for (const text of textsFromContent(messageOf(obj).content)) {
|
|
34
|
+
const trimmed = text.trim();
|
|
35
|
+
if (trimmed.length < minLength) continue;
|
|
36
|
+
if (trimmed.startsWith('<')) continue;
|
|
37
|
+
yield trimmed;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Yields each human-authored text turn across every AI-tool session on this machine, walking the shared bounded jsonl inventory.
|
|
42
|
+
function* eachHumanText(env = process.env, { minLength = 4, selectedCwds = null } = {}) {
|
|
43
|
+
for (const { obj, cwd } of iterateSessionObjects(env)) {
|
|
44
|
+
if (selectedCwds !== null && (cwd === null || !selectedCwds.has(cwd))) continue;
|
|
45
|
+
yield* humanTextsFromObject(obj, { minLength });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { eachHumanText, humanTextsFromObject };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Static-data i18n resolver. The hub stores skill (and other static-data) names
|
|
4
|
+
// as KEYS (staticDataSkillsSkill_N); `GET translations?namespace=static-data`
|
|
5
|
+
// returns every key with its per-language text. We fetch it ONCE (cached per
|
|
6
|
+
// language for the process — MCP is long-lived) and resolve keys to names.
|
|
7
|
+
|
|
8
|
+
const { postJsonWithTimeout } = require('./backend-request');
|
|
9
|
+
const { reasonForError } = require('./onboarding-client');
|
|
10
|
+
|
|
11
|
+
const DEFAULT_TIMEOUT_MS = 20000;
|
|
12
|
+
const cache = new Map(); // langKey -> Map(key -> text)
|
|
13
|
+
|
|
14
|
+
function parseJson(raw) {
|
|
15
|
+
try {
|
|
16
|
+
return raw ? JSON.parse(raw) : null;
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function bearerHeaders(accessToken) {
|
|
23
|
+
const headers = {};
|
|
24
|
+
if (typeof accessToken === 'string' && accessToken) headers.Authorization = `Bearer ${accessToken}`;
|
|
25
|
+
return headers;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function requestTranslations({ hubAccessToken } = {}, { endpoint, namespace = 'static-data', timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
29
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
30
|
+
if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
|
|
31
|
+
const url = `${endpoint}${endpoint.includes('?') ? '&' : '?'}namespace=${encodeURIComponent(namespace)}`;
|
|
32
|
+
let res;
|
|
33
|
+
try {
|
|
34
|
+
res = await postJsonWithTimeout(url, null, timeoutMs, 'GET', null, bearerHeaders(hubAccessToken));
|
|
35
|
+
} catch (e) {
|
|
36
|
+
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
37
|
+
}
|
|
38
|
+
if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
|
|
39
|
+
const env = parseJson(res.raw);
|
|
40
|
+
const data = env && env.status === 'OK' ? env.data : env;
|
|
41
|
+
const rows = Array.isArray(data) ? data : (Array.isArray(data && data.items) ? data.items : null);
|
|
42
|
+
if (!rows) return { ok: false, reason: 'bad-response' };
|
|
43
|
+
return { ok: true, rows };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Returns `{ ok, map }`. On failure `map` is null (callers degrade to the raw
|
|
47
|
+
// value rather than blocking the whole listing on a translations outage).
|
|
48
|
+
async function fetchTranslationMap(deps = {}, { hubAccessToken, lang = 'en', timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
49
|
+
const key = lang === 'es' ? 'es' : 'en';
|
|
50
|
+
if (cache.has(key)) return { ok: true, map: cache.get(key) };
|
|
51
|
+
const getEndpoint = deps.getTranslationsEndpoint || require('./config').getTranslationsEndpoint;
|
|
52
|
+
const request = deps.requestTranslations || requestTranslations;
|
|
53
|
+
const endpoint = getEndpoint();
|
|
54
|
+
if (!endpoint) return { ok: false, map: null, reason: 'no-endpoint' };
|
|
55
|
+
const res = await request({ hubAccessToken }, { endpoint, timeoutMs });
|
|
56
|
+
if (!res.ok) return { ok: false, map: null, reason: res.reason };
|
|
57
|
+
const map = new Map();
|
|
58
|
+
for (const r of res.rows) {
|
|
59
|
+
if (r && typeof r.key === 'string') {
|
|
60
|
+
const text = r[key] || r.en || r.es || null;
|
|
61
|
+
if (text) map.set(r.key, text);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
cache.set(key, map);
|
|
65
|
+
return { ok: true, map };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Resolve a name: if the map holds it (a static-data key) -> readable text;
|
|
69
|
+
// otherwise return it unchanged (already-plain names, or unknown keys).
|
|
70
|
+
function resolveName(map, name) {
|
|
71
|
+
if (!name || !map) return name;
|
|
72
|
+
return map.has(name) ? map.get(name) : name;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function clearCache() {
|
|
76
|
+
cache.clear();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
requestTranslations,
|
|
81
|
+
fetchTranslationMap,
|
|
82
|
+
resolveName,
|
|
83
|
+
clearCache,
|
|
84
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isInteractive } = require('./terminal-progress');
|
|
4
|
+
|
|
5
|
+
// Progressive reveal of text that is ALREADY IN HAND (talents-ai-score, camino A).
|
|
6
|
+
|
|
7
|
+
// 60fps. Fine enough that the reveal reads as continuous, coarse enough that a
|
|
8
|
+
// 700ms budget is ~43 writes rather than one per character.
|
|
9
|
+
const TICK_MS = 16;
|
|
10
|
+
|
|
11
|
+
// The hard ceiling on what this can add, for ANY length.
|
|
12
|
+
const MAX_TOTAL_MS = 700;
|
|
13
|
+
|
|
14
|
+
// How many code points to reveal per tick so that `length` finishes within `maxTicks`.
|
|
15
|
+
function charsPerTick(length, maxTicks) {
|
|
16
|
+
if (!(length > 0)) return 0;
|
|
17
|
+
if (!(maxTicks > 0)) return length;
|
|
18
|
+
return Math.max(1, Math.ceil(length / maxTicks));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
|
+
|
|
23
|
+
// Writes `text` to `stream`, progressively in a TTY and in one write otherwise.
|
|
24
|
+
async function typeOut(text, { stream, tickMs = TICK_MS, maxTotalMs = MAX_TOTAL_MS, sleep = defaultSleep } = {}) {
|
|
25
|
+
const s = typeof text === 'string' ? text : '';
|
|
26
|
+
if (!s || !stream || typeof stream.write !== 'function') return;
|
|
27
|
+
if (!isInteractive(stream)) {
|
|
28
|
+
stream.write(s);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const chars = Array.from(s);
|
|
32
|
+
const maxTicks = Math.max(1, Math.floor((maxTotalMs > 0 ? maxTotalMs : 0) / (tickMs > 0 ? tickMs : TICK_MS)));
|
|
33
|
+
const step = charsPerTick(chars.length, maxTicks);
|
|
34
|
+
for (let i = 0; i < chars.length; i += step) {
|
|
35
|
+
stream.write(chars.slice(i, i + step).join(''));
|
|
36
|
+
// AFTER the write, and skipped on the last slice: the reveal ends when the
|
|
37
|
+
// text is on screen, never one idle tick later.
|
|
38
|
+
if (i + step < chars.length) await sleep(tickMs);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Total for the WHOLE output, same order of magnitude as the typewriter's.
|
|
43
|
+
const REVEAL_TOTAL_MS = 600;
|
|
44
|
+
// A floor, so a two-section report still reads as a reveal rather than a blink, and
|
|
45
|
+
// a ceiling, so a very long one never feels like waiting for the tool.
|
|
46
|
+
const REVEAL_MIN_STEP_MS = 24;
|
|
47
|
+
const REVEAL_MAX_STEP_MS = 120;
|
|
48
|
+
const RULE_RE = /^[\s]*[\u2500\u2501\u2508\u2509-]{20,}[\s]*$/;
|
|
49
|
+
|
|
50
|
+
// Splits rendered output into reveal chunks on BLANK LINES, holding everything between two horizontal rules together (the copyable block).
|
|
51
|
+
function splitSections(text) {
|
|
52
|
+
const s = typeof text === 'string' ? text : '';
|
|
53
|
+
if (!s) return [];
|
|
54
|
+
const parts = s.split('\n');
|
|
55
|
+
const last = parts.length - 1;
|
|
56
|
+
const out = [];
|
|
57
|
+
let current = '';
|
|
58
|
+
let held = false;
|
|
59
|
+
for (let i = 0; i <= last; i++) {
|
|
60
|
+
const line = parts[i];
|
|
61
|
+
// The newline that FOLLOWED this line in the input belongs to this chunk, or the concatenation would lose it.
|
|
62
|
+
current += line + (i < last ? '\n' : '');
|
|
63
|
+
if (RULE_RE.test(line)) { held = !held; continue; }
|
|
64
|
+
if (line.trim() === '' && !held && current.length) {
|
|
65
|
+
out.push(current);
|
|
66
|
+
current = '';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (current.length) out.push(current);
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function revealEnabled({ stream = process.stdout, env = process.env, animate = null } = {}) {
|
|
74
|
+
if (animate === false) return false;
|
|
75
|
+
const flag = env ? env.NO_ANIMATION : undefined;
|
|
76
|
+
if (typeof flag === 'string' && flag.length > 0) return false;
|
|
77
|
+
return isInteractive(stream);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Writes `text` to `stream`, one section at a time when the reveal is on and in a SINGLE write otherwise.
|
|
81
|
+
async function revealText(text, { stream = process.stdout, env = process.env, animate = null, sleep = defaultSleep, totalMs = REVEAL_TOTAL_MS } = {}) {
|
|
82
|
+
const s = typeof text === 'string' ? text : '';
|
|
83
|
+
if (!s || !stream || typeof stream.write !== 'function') return;
|
|
84
|
+
if (!revealEnabled({ stream, env, animate })) {
|
|
85
|
+
stream.write(s);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const sections = splitSections(s);
|
|
89
|
+
if (sections.length <= 1) {
|
|
90
|
+
stream.write(s);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// THE FLOOR MUST NOT BREAK THE CEILING, and getting this wrong is the whole failure mode the issue warns about.
|
|
94
|
+
const maxPauses = Math.max(1, Math.floor(totalMs / REVEAL_MIN_STEP_MS));
|
|
95
|
+
const groups = [];
|
|
96
|
+
const perGroup = Math.ceil(sections.length / (maxPauses + 1));
|
|
97
|
+
for (let i = 0; i < sections.length; i += perGroup) groups.push(sections.slice(i, i + perGroup).join(''));
|
|
98
|
+
const step = Math.min(REVEAL_MAX_STEP_MS, Math.max(REVEAL_MIN_STEP_MS, Math.floor(totalMs / groups.length)));
|
|
99
|
+
for (let i = 0; i < groups.length; i++) {
|
|
100
|
+
stream.write(groups[i]);
|
|
101
|
+
// After the write and never after the last one: the reveal ends when the report
|
|
102
|
+
// is on screen, not one step later.
|
|
103
|
+
if (i < groups.length - 1) await sleep(step);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Backlog we tolerate before speeding up the reveal: at TICK_MS this bounds how
|
|
108
|
+
// far behind the stream the animation can lag (~STREAM_CATCHUP_TICKS * tickMs).
|
|
109
|
+
const STREAM_CATCHUP_TICKS = 50;
|
|
110
|
+
|
|
111
|
+
// A STREAMING typewriter for text that arrives in chunks over time (Alma's NDJSON
|
|
112
|
+
// deltas). Same pacing + gate as the report's reveal: char-by-char in a TTY,
|
|
113
|
+
// plain pass-through otherwise (non-TTY / NO_ANIMATION). `push(text)` feeds the
|
|
114
|
+
// queue; a single loop drains it at `tickMs`, revealing more per tick when the
|
|
115
|
+
// queue backs up so a big chunk animates continuously instead of dumping at once.
|
|
116
|
+
// `end()` flushes whatever is left and resolves.
|
|
117
|
+
function makeStreamReveal({ stream = process.stdout, env = process.env, animate = null, tickMs = TICK_MS, sleep = defaultSleep, catchupTicks = STREAM_CATCHUP_TICKS } = {}) {
|
|
118
|
+
const canWrite = stream && typeof stream.write === 'function';
|
|
119
|
+
if (!canWrite || !revealEnabled({ stream, env, animate })) {
|
|
120
|
+
// Plain: write each chunk as it arrives, no pacing (matches the report's
|
|
121
|
+
// non-TTY degradation).
|
|
122
|
+
return { push: (t) => { if (t && canWrite) stream.write(String(t)); }, end: async () => {} };
|
|
123
|
+
}
|
|
124
|
+
const queue = [];
|
|
125
|
+
let ended = false;
|
|
126
|
+
let draining = null;
|
|
127
|
+
const drain = async () => {
|
|
128
|
+
for (;;) {
|
|
129
|
+
if (queue.length === 0) {
|
|
130
|
+
if (ended) return;
|
|
131
|
+
await sleep(tickMs); // wait for more input without busy-spinning
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const step = Math.max(1, Math.ceil(queue.length / catchupTicks));
|
|
135
|
+
stream.write(queue.splice(0, step).join(''));
|
|
136
|
+
if (queue.length > 0 || !ended) await sleep(tickMs);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
return {
|
|
140
|
+
push: (t) => {
|
|
141
|
+
if (!t) return;
|
|
142
|
+
for (const ch of String(t)) queue.push(ch);
|
|
143
|
+
if (!draining) draining = drain();
|
|
144
|
+
},
|
|
145
|
+
end: async () => { ended = true; if (draining) await draining; },
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
typeOut, charsPerTick, TICK_MS, MAX_TOTAL_MS,
|
|
151
|
+
revealText, splitSections, revealEnabled,
|
|
152
|
+
makeStreamReveal, STREAM_CATCHUP_TICKS,
|
|
153
|
+
REVEAL_TOTAL_MS, REVEAL_MIN_STEP_MS, REVEAL_MAX_STEP_MS,
|
|
154
|
+
};
|