fullstackgtm 0.44.0 → 0.46.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/CHANGELOG.md +293 -1
- package/INSTALL_FOR_AGENTS.md +13 -11
- package/README.md +45 -24
- package/dist/audit.d.ts +3 -1
- package/dist/audit.js +29 -2
- package/dist/backfill.d.ts +86 -0
- package/dist/backfill.js +251 -0
- package/dist/bin.js +4 -1
- package/dist/cli/audit.d.ts +15 -0
- package/dist/cli/audit.js +392 -0
- package/dist/cli/auth.d.ts +82 -0
- package/dist/cli/auth.js +607 -0
- package/dist/cli/backfill.d.ts +1 -0
- package/dist/cli/backfill.js +125 -0
- package/dist/cli/backfillRuns.d.ts +1 -0
- package/dist/cli/backfillRuns.js +187 -0
- package/dist/cli/call.d.ts +1 -0
- package/dist/cli/call.js +387 -0
- package/dist/cli/capabilities.d.ts +30 -0
- package/dist/cli/capabilities.js +197 -0
- package/dist/cli/draft.d.ts +7 -0
- package/dist/cli/draft.js +87 -0
- package/dist/cli/enrich.d.ts +8 -0
- package/dist/cli/enrich.js +806 -0
- package/dist/cli/fix.d.ts +33 -0
- package/dist/cli/fix.js +346 -0
- package/dist/cli/help.d.ts +25 -0
- package/dist/cli/help.js +646 -0
- package/dist/cli/icp.d.ts +7 -0
- package/dist/cli/icp.js +229 -0
- package/dist/cli/init.d.ts +7 -0
- package/dist/cli/init.js +58 -0
- package/dist/cli/market.d.ts +1 -0
- package/dist/cli/market.js +391 -0
- package/dist/cli/plans.d.ts +5 -0
- package/dist/cli/plans.js +456 -0
- package/dist/cli/schedule.d.ts +8 -0
- package/dist/cli/schedule.js +479 -0
- package/dist/cli/shared.d.ts +71 -0
- package/dist/cli/shared.js +342 -0
- package/dist/cli/signals.d.ts +7 -0
- package/dist/cli/signals.js +412 -0
- package/dist/cli/suggest.d.ts +49 -0
- package/dist/cli/suggest.js +135 -0
- package/dist/cli/tam.d.ts +9 -0
- package/dist/cli/tam.js +387 -0
- package/dist/cli/ui.d.ts +142 -0
- package/dist/cli/ui.js +427 -0
- package/dist/cli.d.ts +1 -57
- package/dist/cli.js +123 -5157
- package/dist/connector.d.ts +23 -0
- package/dist/connector.js +47 -0
- package/dist/connectors/hubspot.d.ts +10 -1
- package/dist/connectors/hubspot.js +244 -10
- package/dist/connectors/hubspotAuth.js +5 -1
- package/dist/connectors/linkedin.js +14 -14
- package/dist/connectors/salesforce.d.ts +10 -1
- package/dist/connectors/salesforce.js +39 -6
- package/dist/connectors/signalSources.js +7 -59
- package/dist/connectors/stripe.d.ts +37 -0
- package/dist/connectors/stripe.js +103 -31
- package/dist/enrich.d.ts +7 -0
- package/dist/enrich.js +7 -0
- package/dist/health.d.ts +11 -69
- package/dist/health.js +4 -134
- package/dist/healthScore.d.ts +71 -0
- package/dist/healthScore.js +143 -0
- package/dist/icp.d.ts +15 -11
- package/dist/icp.js +19 -36
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/judge.d.ts +2 -0
- package/dist/judge.js +6 -0
- package/dist/llm.d.ts +29 -0
- package/dist/llm.js +206 -0
- package/dist/market.d.ts +39 -1
- package/dist/market.js +116 -44
- package/dist/marketClassify.d.ts +11 -1
- package/dist/marketClassify.js +17 -2
- package/dist/marketTaxonomy.d.ts +6 -1
- package/dist/marketTaxonomy.js +4 -2
- package/dist/mcp-bin.js +31 -2
- package/dist/mcp.js +372 -166
- package/dist/progress.d.ts +96 -0
- package/dist/progress.js +142 -0
- package/dist/runReport.d.ts +24 -0
- package/dist/runReport.js +139 -4
- package/dist/schedule.d.ts +80 -2
- package/dist/schedule.js +254 -1
- package/dist/spoolFiles.d.ts +44 -0
- package/dist/spoolFiles.js +114 -0
- package/dist/types.d.ts +29 -1
- package/docs/api.md +75 -8
- package/docs/architecture.md +2 -0
- package/docs/linkedin-connector-spec.md +1 -1
- package/docs/signal-spool-format.md +13 -0
- package/llms.txt +18 -9
- package/package.json +10 -3
- package/skills/fullstackgtm/SKILL.md +2 -1
- package/src/audit.ts +27 -1
- package/src/backfill.ts +340 -0
- package/src/bin.ts +5 -1
- package/src/cli/audit.ts +447 -0
- package/src/cli/auth.ts +669 -0
- package/src/cli/backfill.ts +156 -0
- package/src/cli/backfillRuns.ts +198 -0
- package/src/cli/call.ts +414 -0
- package/src/cli/capabilities.ts +215 -0
- package/src/cli/draft.ts +101 -0
- package/src/cli/enrich.ts +908 -0
- package/src/cli/fix.ts +373 -0
- package/src/cli/help.ts +702 -0
- package/src/cli/icp.ts +265 -0
- package/src/cli/init.ts +65 -0
- package/src/cli/market.ts +423 -0
- package/src/cli/plans.ts +524 -0
- package/src/cli/schedule.ts +526 -0
- package/src/cli/shared.ts +392 -0
- package/src/cli/signals.ts +434 -0
- package/src/cli/suggest.ts +151 -0
- package/src/cli/tam.ts +435 -0
- package/src/cli/ui.ts +497 -0
- package/src/cli.ts +122 -5855
- package/src/connector.ts +66 -0
- package/src/connectors/hubspot.ts +273 -9
- package/src/connectors/hubspotAuth.ts +5 -1
- package/src/connectors/linkedin.ts +14 -14
- package/src/connectors/salesforce.ts +51 -3
- package/src/connectors/signalSources.ts +7 -56
- package/src/connectors/stripe.ts +154 -34
- package/src/enrich.ts +13 -0
- package/src/health.ts +14 -213
- package/src/healthScore.ts +223 -0
- package/src/icp.ts +19 -35
- package/src/index.ts +28 -1
- package/src/judge.ts +7 -0
- package/src/llm.ts +239 -0
- package/src/market.ts +157 -44
- package/src/marketClassify.ts +26 -2
- package/src/marketTaxonomy.ts +10 -2
- package/src/mcp-bin.ts +34 -2
- package/src/mcp.ts +270 -63
- package/src/progress.ts +197 -0
- package/src/runReport.ts +159 -4
- package/src/schedule.ts +310 -3
- package/src/spoolFiles.ts +116 -0
- package/src/types.ts +32 -2
- package/docs/dx-punch-list.md +0 -87
- package/docs/roadmap-to-1.0.md +0 -211
package/dist/cli/ui.js
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
// Terminal presentation layer — zero-dependency by design (this package ships
|
|
2
|
+
// with no runtime deps and holds CRM credentials; every avoided dependency is
|
|
3
|
+
// supply-chain surface we don't carry).
|
|
4
|
+
//
|
|
5
|
+
// The contract that keeps the agent surface safe:
|
|
6
|
+
// - stdout NEVER carries ANSI bytes unless stdout itself is an interactive
|
|
7
|
+
// TTY. Piped/redirected/--json output is byte-identical to the unstyled CLI.
|
|
8
|
+
// - Animation (spinners, progress, checklists) goes to stderr ONLY, and only
|
|
9
|
+
// when stderr is an interactive TTY. Non-TTY runs render nothing — no
|
|
10
|
+
// frame spam in CI logs or agent transcripts.
|
|
11
|
+
// - NO_COLOR (https://no-color.org), TERM=dumb, and CI disable styling;
|
|
12
|
+
// FORCE_COLOR (non-"0") re-enables it, matching the wider ecosystem.
|
|
13
|
+
/** Should `stream` receive color/ANSI styling at all? */
|
|
14
|
+
export function colorEnabled(stream = process.stdout, env = process.env) {
|
|
15
|
+
if (env.FORCE_COLOR === "0")
|
|
16
|
+
return false;
|
|
17
|
+
if (env.FORCE_COLOR !== undefined)
|
|
18
|
+
return true;
|
|
19
|
+
if (env.NO_COLOR !== undefined)
|
|
20
|
+
return false;
|
|
21
|
+
if (env.TERM === "dumb")
|
|
22
|
+
return false;
|
|
23
|
+
if (env.CI !== undefined)
|
|
24
|
+
return false;
|
|
25
|
+
return Boolean(stream?.isTTY);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Should `stream` receive live single-line repaints (spinners, bars)?
|
|
29
|
+
* Stricter than color: FORCE_COLOR opts into color codes, not into cursor
|
|
30
|
+
* animation — repaint sequences in a captured log are never what anyone wants.
|
|
31
|
+
*/
|
|
32
|
+
export function animationEnabled(stream = process.stderr, env = process.env) {
|
|
33
|
+
if (env.NO_COLOR !== undefined)
|
|
34
|
+
return false;
|
|
35
|
+
if (env.TERM === "dumb")
|
|
36
|
+
return false;
|
|
37
|
+
if (env.CI !== undefined)
|
|
38
|
+
return false;
|
|
39
|
+
return Boolean(stream?.isTTY);
|
|
40
|
+
}
|
|
41
|
+
const CODES = {
|
|
42
|
+
reset: "\u001b[0m",
|
|
43
|
+
bold: "\u001b[1m",
|
|
44
|
+
dim: "\u001b[2m",
|
|
45
|
+
red: "\u001b[31m",
|
|
46
|
+
green: "\u001b[32m",
|
|
47
|
+
yellow: "\u001b[33m",
|
|
48
|
+
blue: "\u001b[34m",
|
|
49
|
+
magenta: "\u001b[35m",
|
|
50
|
+
cyan: "\u001b[36m",
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Style functions for one stream: identity functions when styling is off, so
|
|
54
|
+
* call sites never branch. `paint(colorEnabled(process.stdout))`.
|
|
55
|
+
*/
|
|
56
|
+
export function paint(enabled) {
|
|
57
|
+
const entry = (code) => enabled ? (text) => `${code}${text}${CODES.reset}` : (text) => text;
|
|
58
|
+
return {
|
|
59
|
+
enabled,
|
|
60
|
+
bold: entry(CODES.bold),
|
|
61
|
+
dim: entry(CODES.dim),
|
|
62
|
+
red: entry(CODES.red),
|
|
63
|
+
green: entry(CODES.green),
|
|
64
|
+
yellow: entry(CODES.yellow),
|
|
65
|
+
blue: entry(CODES.blue),
|
|
66
|
+
magenta: entry(CODES.magenta),
|
|
67
|
+
cyan: entry(CODES.cyan),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export const GLYPH = {
|
|
71
|
+
ok: "✓",
|
|
72
|
+
warn: "!",
|
|
73
|
+
fail: "✗",
|
|
74
|
+
pending: "○",
|
|
75
|
+
bullet: "●",
|
|
76
|
+
};
|
|
77
|
+
/** `✓` in green / `!` in yellow / `✗` in red — or the bare glyph unstyled. */
|
|
78
|
+
export function statusGlyph(state, p) {
|
|
79
|
+
if (state === "ok")
|
|
80
|
+
return p.green(GLYPH.ok);
|
|
81
|
+
if (state === "warn")
|
|
82
|
+
return p.yellow(GLYPH.warn);
|
|
83
|
+
if (state === "fail")
|
|
84
|
+
return p.red(GLYPH.fail);
|
|
85
|
+
return p.dim(GLYPH.pending);
|
|
86
|
+
}
|
|
87
|
+
/** Color banding for 0–100 scores: ≥80 green, ≥60 yellow, else red. */
|
|
88
|
+
export function scoreColor(score, p) {
|
|
89
|
+
const text = String(score);
|
|
90
|
+
if (score >= 80)
|
|
91
|
+
return p.green(text);
|
|
92
|
+
if (score >= 60)
|
|
93
|
+
return p.yellow(text);
|
|
94
|
+
return p.red(text);
|
|
95
|
+
}
|
|
96
|
+
const SPARKS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
|
|
97
|
+
/** Min–max normalized sparkline: [42, 51, 68, 81] → "▁▃▅█". */
|
|
98
|
+
export function sparkline(values) {
|
|
99
|
+
if (values.length === 0)
|
|
100
|
+
return "";
|
|
101
|
+
const min = Math.min(...values);
|
|
102
|
+
const max = Math.max(...values);
|
|
103
|
+
const span = max - min;
|
|
104
|
+
return values
|
|
105
|
+
.map((value) => {
|
|
106
|
+
const fraction = span === 0 ? 0.5 : (value - min) / span;
|
|
107
|
+
return SPARKS[Math.min(SPARKS.length - 1, Math.floor(fraction * SPARKS.length))];
|
|
108
|
+
})
|
|
109
|
+
.join("");
|
|
110
|
+
}
|
|
111
|
+
/** `formatBar(0.67)` → "▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱" (clamped to [0,1]). */
|
|
112
|
+
export function formatBar(fraction, width = 20) {
|
|
113
|
+
const clamped = Math.max(0, Math.min(1, fraction));
|
|
114
|
+
const filled = Math.round(clamped * width);
|
|
115
|
+
return "▰".repeat(filled) + "▱".repeat(width - filled);
|
|
116
|
+
}
|
|
117
|
+
/** 950 → "950ms"; 12_400 → "12s"; 84_000 → "1m 24s". */
|
|
118
|
+
export function formatDuration(ms) {
|
|
119
|
+
if (ms < 1000)
|
|
120
|
+
return `${Math.max(0, Math.round(ms))}ms`;
|
|
121
|
+
const seconds = Math.round(ms / 1000);
|
|
122
|
+
if (seconds < 60)
|
|
123
|
+
return `${seconds}s`;
|
|
124
|
+
const minutes = Math.floor(seconds / 60);
|
|
125
|
+
const rest = seconds % 60;
|
|
126
|
+
return `${minutes}m ${String(rest).padStart(2, "0")}s`;
|
|
127
|
+
}
|
|
128
|
+
/** 12345 → "12,345" (fixed en-US grouping — output must not vary by host locale). */
|
|
129
|
+
export function formatCount(value) {
|
|
130
|
+
return value.toLocaleString("en-US");
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Rounded-corner frame around pre-rendered lines. `width` is measured on the
|
|
134
|
+
* raw text, so style the CONTENT only after framing (or pass unstyled lines) —
|
|
135
|
+
* ANSI codes inside `lines` would inflate the measured width.
|
|
136
|
+
*/
|
|
137
|
+
export function box(lines, p, title) {
|
|
138
|
+
const inner = Math.max(...lines.map((line) => line.length), title ? title.length + 2 : 0);
|
|
139
|
+
const top = title
|
|
140
|
+
? `╭─ ${title} ${"─".repeat(Math.max(0, inner - title.length - 1))}╮`
|
|
141
|
+
: `╭${"─".repeat(inner + 2)}╮`;
|
|
142
|
+
const bottom = `╰${"─".repeat(inner + 2)}╯`;
|
|
143
|
+
return [
|
|
144
|
+
p.dim(top),
|
|
145
|
+
...lines.map((line) => `${p.dim("│")} ${line.padEnd(inner)} ${p.dim("│")}`),
|
|
146
|
+
p.dim(bottom),
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Left-aligned columns with two-space gutters. Rows may be ragged; widths are
|
|
151
|
+
* measured on raw cell text, so pass unstyled cells and style whole lines, or
|
|
152
|
+
* style via the optional per-column painters (applied AFTER padding).
|
|
153
|
+
*/
|
|
154
|
+
export function table(rows, painters) {
|
|
155
|
+
const widths = [];
|
|
156
|
+
for (const row of rows) {
|
|
157
|
+
row.forEach((cell, index) => {
|
|
158
|
+
widths[index] = Math.max(widths[index] ?? 0, cell.length);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return rows.map((row) => row
|
|
162
|
+
.map((cell, index) => {
|
|
163
|
+
const padded = index === row.length - 1 ? cell : cell.padEnd(widths[index]);
|
|
164
|
+
const painterFn = painters?.[index];
|
|
165
|
+
return painterFn ? painterFn(padded) : padded;
|
|
166
|
+
})
|
|
167
|
+
.join(" ")
|
|
168
|
+
.trimEnd());
|
|
169
|
+
}
|
|
170
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
171
|
+
const SPINNER_INTERVAL_MS = 80;
|
|
172
|
+
const CLEAR_LINE = "\r\u001b[2K";
|
|
173
|
+
const NOOP_STATUS = { set() { }, done() { }, active: false };
|
|
174
|
+
/**
|
|
175
|
+
* One live spinner line on stderr, repainted in place. Returns an inert no-op
|
|
176
|
+
* outside an interactive TTY, so call sites never branch. `done()` always
|
|
177
|
+
* clears the animation; pass `finalText` to leave a plain summary line behind.
|
|
178
|
+
*/
|
|
179
|
+
export function createStatusLine(stream = process.stderr, env = process.env) {
|
|
180
|
+
if (!animationEnabled(stream, env))
|
|
181
|
+
return NOOP_STATUS;
|
|
182
|
+
let text = "";
|
|
183
|
+
let frame = 0;
|
|
184
|
+
let timer = null;
|
|
185
|
+
const render = () => {
|
|
186
|
+
const columns = stream.columns ?? 80;
|
|
187
|
+
const line = `${SPINNER_FRAMES[frame % SPINNER_FRAMES.length]} ${text}`;
|
|
188
|
+
stream.write(CLEAR_LINE + (line.length >= columns ? `${line.slice(0, columns - 2)}…` : line));
|
|
189
|
+
};
|
|
190
|
+
const status = {
|
|
191
|
+
set(next) {
|
|
192
|
+
text = next;
|
|
193
|
+
if (!timer) {
|
|
194
|
+
// The timer only advances the spinner frame; text changes repaint
|
|
195
|
+
// immediately so progress is never a tick behind the work.
|
|
196
|
+
timer = setInterval(() => {
|
|
197
|
+
frame += 1;
|
|
198
|
+
render();
|
|
199
|
+
}, SPINNER_INTERVAL_MS);
|
|
200
|
+
// Never hold the event loop open: a forgotten spinner must not stop
|
|
201
|
+
// the CLI from exiting.
|
|
202
|
+
timer.unref?.();
|
|
203
|
+
}
|
|
204
|
+
render();
|
|
205
|
+
},
|
|
206
|
+
done(finalText) {
|
|
207
|
+
if (timer)
|
|
208
|
+
clearInterval(timer);
|
|
209
|
+
timer = null;
|
|
210
|
+
stream.write(CLEAR_LINE);
|
|
211
|
+
if (finalText !== undefined)
|
|
212
|
+
stream.write(`${finalText}\n`);
|
|
213
|
+
},
|
|
214
|
+
active: true,
|
|
215
|
+
};
|
|
216
|
+
return status;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* A status line that re-renders itself every second with the elapsed time —
|
|
220
|
+
* for single long waits (an LLM call) where nothing else ticks. `label`
|
|
221
|
+
* receives the formatted elapsed time. Inert outside an interactive TTY.
|
|
222
|
+
*/
|
|
223
|
+
export function startElapsedStatus(label, stream = process.stderr, env = process.env) {
|
|
224
|
+
const status = createStatusLine(stream, env);
|
|
225
|
+
if (!status.active)
|
|
226
|
+
return { done() { }, active: false };
|
|
227
|
+
const started = Date.now();
|
|
228
|
+
const refresh = () => status.set(label(formatDuration(Date.now() - started)));
|
|
229
|
+
refresh();
|
|
230
|
+
const timer = setInterval(refresh, 1000);
|
|
231
|
+
timer.unref?.();
|
|
232
|
+
return {
|
|
233
|
+
done(finalText) {
|
|
234
|
+
clearInterval(timer);
|
|
235
|
+
status.done(finalText);
|
|
236
|
+
},
|
|
237
|
+
active: true,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const NOOP_CHECKLIST = { update() { }, done() { }, active: false };
|
|
241
|
+
/**
|
|
242
|
+
* A multi-line live board — one line per item, each flipping ○ → ⠹ → ✓ as work
|
|
243
|
+
* progresses (the audit rule registry renders through this). Repaints with
|
|
244
|
+
* cursor-up; erases itself on done() so the verb's real output owns stdout.
|
|
245
|
+
*/
|
|
246
|
+
export function createChecklist(items, stream = process.stderr, env = process.env) {
|
|
247
|
+
if (!animationEnabled(stream, env) || items.length === 0)
|
|
248
|
+
return NOOP_CHECKLIST;
|
|
249
|
+
const p = paint(true);
|
|
250
|
+
const states = new Map(items.map((item) => [item.id, { state: "pending" }]));
|
|
251
|
+
let frame = 0;
|
|
252
|
+
let painted = 0;
|
|
253
|
+
let timer = null;
|
|
254
|
+
const labelWidth = Math.max(...items.map((item) => item.label.length));
|
|
255
|
+
const render = () => {
|
|
256
|
+
const spin = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
|
|
257
|
+
const lines = items.map((item) => {
|
|
258
|
+
const entry = states.get(item.id);
|
|
259
|
+
const glyph = entry.state === "running" ? p.cyan(spin) : statusGlyph(entry.state, p);
|
|
260
|
+
const note = entry.note ? ` ${p.dim(entry.note)}` : "";
|
|
261
|
+
const label = entry.state === "pending" ? p.dim(item.label.padEnd(labelWidth)) : item.label.padEnd(labelWidth);
|
|
262
|
+
return ` ${glyph} ${label}${note}`;
|
|
263
|
+
});
|
|
264
|
+
const up = painted > 0 ? `\u001b[${painted}A` : "";
|
|
265
|
+
stream.write(`${up}${lines.map((line) => `\u001b[2K${line}`).join("\n")}\n`);
|
|
266
|
+
painted = lines.length;
|
|
267
|
+
};
|
|
268
|
+
const start = () => {
|
|
269
|
+
if (!timer) {
|
|
270
|
+
timer = setInterval(() => {
|
|
271
|
+
frame += 1;
|
|
272
|
+
render();
|
|
273
|
+
}, SPINNER_INTERVAL_MS);
|
|
274
|
+
timer.unref?.();
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
return {
|
|
278
|
+
update(id, state, note) {
|
|
279
|
+
const entry = states.get(id);
|
|
280
|
+
if (!entry)
|
|
281
|
+
return;
|
|
282
|
+
entry.state = state;
|
|
283
|
+
entry.note = note;
|
|
284
|
+
start();
|
|
285
|
+
render();
|
|
286
|
+
},
|
|
287
|
+
done() {
|
|
288
|
+
if (timer)
|
|
289
|
+
clearInterval(timer);
|
|
290
|
+
timer = null;
|
|
291
|
+
if (painted > 0) {
|
|
292
|
+
// Erase the board: cursor up over every painted line, clear each.
|
|
293
|
+
stream.write(`\u001b[${painted}A${"\u001b[2K\n".repeat(painted)}\u001b[${painted}A`);
|
|
294
|
+
painted = 0;
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
active: true,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const NOOP_RENDERER = { listener: () => { }, done() { }, active: false };
|
|
301
|
+
/**
|
|
302
|
+
* Adapter from the shared progress vocabulary (src/progress.ts) to the
|
|
303
|
+
* existing TTY primitives: `stage` events advance a live checklist (○ → ⠹ →
|
|
304
|
+
* ✓), while `items` / `note` / `opResult` / `meter` events annotate the
|
|
305
|
+
* running stage ("contacts 1,200/4,800", the apply ticker tallies, a fuel
|
|
306
|
+
* gauge). Outside an interactive TTY this is an inert no-op — piped/CI/agent
|
|
307
|
+
* output stays byte-identical (streaming listeners composed alongside still
|
|
308
|
+
* run; rendering is presentation only).
|
|
309
|
+
*/
|
|
310
|
+
export function createProgressRenderer(stages, stream = process.stderr, env = process.env) {
|
|
311
|
+
const board = createChecklist(stages.map((stage) => ({ id: stage, label: stage })), stream, env);
|
|
312
|
+
if (!board.active)
|
|
313
|
+
return NOOP_RENDERER;
|
|
314
|
+
let current = null;
|
|
315
|
+
const noteFor = (event, snapshot) => {
|
|
316
|
+
if (event.kind === "items") {
|
|
317
|
+
return event.total !== undefined
|
|
318
|
+
? `${formatCount(event.done)}/${formatCount(event.total)}`
|
|
319
|
+
: formatCount(event.done);
|
|
320
|
+
}
|
|
321
|
+
if (event.kind === "note")
|
|
322
|
+
return event.note;
|
|
323
|
+
if (event.kind === "opResult") {
|
|
324
|
+
return `${GLYPH.ok} ${formatCount(snapshot.opsApplied)} applied · ${formatCount(snapshot.opsFailed)} failed · ${formatCount(snapshot.opsSkipped)} skipped`;
|
|
325
|
+
}
|
|
326
|
+
if (event.kind === "meter") {
|
|
327
|
+
const fraction = event.budget > 0 ? event.spent / event.budget : 0;
|
|
328
|
+
return `${formatBar(fraction, 10)} ${formatCount(event.spent)}/${formatCount(event.budget)} ${event.unit}`;
|
|
329
|
+
}
|
|
330
|
+
return undefined;
|
|
331
|
+
};
|
|
332
|
+
return {
|
|
333
|
+
listener(event, snapshot) {
|
|
334
|
+
if (event.kind === "stage") {
|
|
335
|
+
// Advance the board: close out the previous stage, spin the new one.
|
|
336
|
+
if (current && current !== event.stage)
|
|
337
|
+
board.update(current, "ok");
|
|
338
|
+
current = event.stage;
|
|
339
|
+
board.update(current, "running");
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
// Events for a stage the board doesn't know (e.g. a nested pull inside a
|
|
343
|
+
// larger flow) are ignored by createChecklist's unknown-id no-op.
|
|
344
|
+
if (current)
|
|
345
|
+
board.update(current, "running", noteFor(event, snapshot));
|
|
346
|
+
},
|
|
347
|
+
done() {
|
|
348
|
+
board.done();
|
|
349
|
+
},
|
|
350
|
+
active: true,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/** Truncate to `max` display characters, ending in "…" when cut. */
|
|
354
|
+
export function truncateToWidth(text, max) {
|
|
355
|
+
if (max <= 0)
|
|
356
|
+
return "";
|
|
357
|
+
if (text.length <= max)
|
|
358
|
+
return text;
|
|
359
|
+
if (max === 1)
|
|
360
|
+
return "…";
|
|
361
|
+
return `${text.slice(0, max - 1).trimEnd()}…`;
|
|
362
|
+
}
|
|
363
|
+
/** critical → red, warning → yellow, info → dim. */
|
|
364
|
+
export function severityWord(word, p) {
|
|
365
|
+
if (word === "critical")
|
|
366
|
+
return p.red(word);
|
|
367
|
+
if (word === "warning")
|
|
368
|
+
return p.yellow(word);
|
|
369
|
+
return p.dim(word);
|
|
370
|
+
}
|
|
371
|
+
/** needs_approval → yellow, approved/applied → green, rejected → red. */
|
|
372
|
+
export function planStatusWord(status, p) {
|
|
373
|
+
if (status === "needs_approval")
|
|
374
|
+
return p.yellow(status);
|
|
375
|
+
if (status === "approved" || status === "applied")
|
|
376
|
+
return p.green(status);
|
|
377
|
+
if (status === "rejected")
|
|
378
|
+
return p.red(status);
|
|
379
|
+
return status;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Interactive-terminal styling for the patch-plan markdown that `audit` and
|
|
383
|
+
* `plans show` print: headings bold, severities banded, and the core diff —
|
|
384
|
+
* current value red, proposed value green (placeholders yellow: they need a
|
|
385
|
+
* human before they can be written). Line-level and additive — with paint
|
|
386
|
+
* disabled the text passes through untouched, so piped output is unchanged.
|
|
387
|
+
* One deliberate text change in rich mode: finding headlines render
|
|
388
|
+
* terminal-bold instead of markdown-bold (the ** markers drop).
|
|
389
|
+
*/
|
|
390
|
+
export function stylizePlanMarkdown(text, p) {
|
|
391
|
+
if (!p.enabled)
|
|
392
|
+
return text;
|
|
393
|
+
return text
|
|
394
|
+
.split("\n")
|
|
395
|
+
.map((line) => {
|
|
396
|
+
if (line.startsWith("# ") || line.startsWith("## "))
|
|
397
|
+
return p.bold(line);
|
|
398
|
+
// Rule-summary table severity cell: "| stale-deal | 50 | warning |"
|
|
399
|
+
const cell = line.match(/^(\| .+ \| \d+ \| )(critical|warning|info)( \|)$/);
|
|
400
|
+
if (cell)
|
|
401
|
+
return `${cell[1]}${severityWord(cell[2], p)}${cell[3]}`;
|
|
402
|
+
// Finding headline: "- **Deal has no valid owner** (warning, planned)"
|
|
403
|
+
const headline = line.match(/^- \*\*(.+)\*\* \((critical|warning|info)(, .+)$/);
|
|
404
|
+
if (headline)
|
|
405
|
+
return `- ${p.bold(headline[1])} (${severityWord(headline[2], p)}${headline[3]}`;
|
|
406
|
+
// The diff at the heart of the plan: what is → what would be written.
|
|
407
|
+
const current = line.match(/^(\s*- Current CRM value: )(.*)$/);
|
|
408
|
+
if (current)
|
|
409
|
+
return `${current[1]}${p.red(current[2])}`;
|
|
410
|
+
const proposed = line.match(/^(\s*- Proposed value: )(.*)$/);
|
|
411
|
+
if (proposed) {
|
|
412
|
+
const value = proposed[2];
|
|
413
|
+
return `${proposed[1]}${value.startsWith("requires_human") ? p.yellow(value) : p.green(value)}`;
|
|
414
|
+
}
|
|
415
|
+
if (line.startsWith("Status: ")) {
|
|
416
|
+
return `Status: ${planStatusWord(line.slice("Status: ".length), p)}`;
|
|
417
|
+
}
|
|
418
|
+
if (line.startsWith("Dry run: "))
|
|
419
|
+
return p.dim(line);
|
|
420
|
+
return line;
|
|
421
|
+
})
|
|
422
|
+
.join("\n");
|
|
423
|
+
}
|
|
424
|
+
/** Strip ANSI escape sequences (test helper + safety net for width math). */
|
|
425
|
+
export function stripAnsi(text) {
|
|
426
|
+
return text.replace(/\u001b\[[0-9;]*[A-Za-z]/g, "");
|
|
427
|
+
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,58 +1,2 @@
|
|
|
1
|
-
import { type LlmProvider } from "./llm.ts";
|
|
2
|
-
/**
|
|
3
|
-
* The broker channel carries a long-lived pairing bearer and receives freshly
|
|
4
|
-
* minted live-CRM tokens, so it must be TLS unless it's an explicit localhost
|
|
5
|
-
* dev target. Refuse http:// (and non-http schemes) otherwise — single-quote
|
|
6
|
-
* shell escaping does nothing for a token sent in cleartext.
|
|
7
|
-
*/
|
|
8
|
-
export declare function assertSecureBrokerUrl(raw: string): URL;
|
|
9
|
-
type ProviderDoctorStatus = {
|
|
10
|
-
source: "env" | "stored" | "broker" | "none";
|
|
11
|
-
detail: string;
|
|
12
|
-
};
|
|
13
|
-
export declare function doctorReport(env?: Record<string, string | undefined>): {
|
|
14
|
-
package: {
|
|
15
|
-
name: string;
|
|
16
|
-
version: string;
|
|
17
|
-
};
|
|
18
|
-
node: {
|
|
19
|
-
version: string;
|
|
20
|
-
ok: boolean;
|
|
21
|
-
required: string;
|
|
22
|
-
};
|
|
23
|
-
profile: string;
|
|
24
|
-
credentialStore: {
|
|
25
|
-
path: string;
|
|
26
|
-
exists: boolean;
|
|
27
|
-
};
|
|
28
|
-
config: {
|
|
29
|
-
path: string;
|
|
30
|
-
exists: boolean;
|
|
31
|
-
};
|
|
32
|
-
providers: Record<string, ProviderDoctorStatus>;
|
|
33
|
-
broker: {
|
|
34
|
-
paired: boolean;
|
|
35
|
-
baseUrl: string;
|
|
36
|
-
} | {
|
|
37
|
-
paired: boolean;
|
|
38
|
-
baseUrl?: undefined;
|
|
39
|
-
};
|
|
40
|
-
llm: {
|
|
41
|
-
configured: boolean;
|
|
42
|
-
provider: LlmProvider;
|
|
43
|
-
source: "stored" | "env";
|
|
44
|
-
detail?: undefined;
|
|
45
|
-
} | {
|
|
46
|
-
configured: boolean;
|
|
47
|
-
detail: string;
|
|
48
|
-
provider?: undefined;
|
|
49
|
-
source?: undefined;
|
|
50
|
-
};
|
|
51
|
-
mcp: {
|
|
52
|
-
peersInstalled: boolean;
|
|
53
|
-
missing: string[];
|
|
54
|
-
};
|
|
55
|
-
nextSteps: string[];
|
|
56
|
-
};
|
|
57
1
|
export declare function runCli(argv: string[]): Promise<void>;
|
|
58
|
-
export {};
|
|
2
|
+
export { assertSecureBrokerUrl, doctorReport } from "./cli/auth.ts";
|