@sweny-ai/core 0.1.170 → 0.1.172
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/dist/agent-env.d.ts +2 -1
- package/dist/agent-env.js +9 -3
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +3 -0
- package/dist/budget.js +5 -1
- package/dist/cli/check.js +12 -12
- package/dist/cli/comment-output.d.ts +4 -2
- package/dist/cli/comment-output.js +16 -8
- package/dist/cli/config.js +24 -24
- package/dist/cli/e2e.js +17 -17
- package/dist/cli/main.js +42 -26
- package/dist/cli/marketplace.js +3 -3
- package/dist/cli/new.js +11 -8
- package/dist/cli/output.d.ts +3 -2
- package/dist/cli/output.js +39 -37
- package/dist/cli/progress.d.ts +33 -0
- package/dist/cli/progress.js +69 -0
- package/dist/cli/publish.js +2 -2
- package/dist/cli/record.d.ts +66 -0
- package/dist/cli/record.js +344 -0
- package/dist/cli/renderer.d.ts +4 -0
- package/dist/cli/renderer.js +20 -13
- package/dist/cli/run-output.d.ts +13 -6
- package/dist/cli/run-output.js +36 -26
- package/dist/cli/runs.js +17 -11
- package/dist/cli/setup.js +7 -7
- package/dist/cli/style.d.ts +26 -0
- package/dist/cli/style.js +48 -0
- package/dist/cli/terminal.d.ts +47 -0
- package/dist/cli/terminal.js +123 -0
- package/dist/cli/ticket.d.ts +89 -0
- package/dist/cli/ticket.js +218 -0
- package/dist/cli/try.d.ts +19 -8
- package/dist/cli/try.js +97 -65
- package/dist/cli/upgrade.js +1 -1
- package/dist/executor.d.ts +8 -1
- package/dist/executor.js +20 -2
- package/dist/git-credentials.d.ts +106 -0
- package/dist/git-credentials.js +322 -0
- package/dist/harness/acp.js +3 -0
- package/dist/harness/claude-code.d.ts +7 -0
- package/dist/harness/claude-code.js +44 -3
- package/dist/harness/codex.js +3 -0
- package/dist/harness/pi.js +3 -0
- package/dist/harness/policy.d.ts +5 -0
- package/dist/harness/policy.js +14 -0
- package/dist/harness/sandbox-wrapper.d.ts +9 -2
- package/dist/harness/sandbox-wrapper.js +14 -4
- package/dist/harness/types.d.ts +10 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mermaid.d.ts +10 -5
- package/dist/mermaid.js +11 -13
- package/dist/safe-outputs.js +5 -0
- package/dist/skills/git-push.d.ts +62 -0
- package/dist/skills/git-push.js +132 -0
- package/dist/skills/github.d.ts +17 -0
- package/dist/skills/github.js +23 -0
- package/dist/theme.d.ts +185 -0
- package/dist/theme.js +183 -0
- package/dist/workflows/browser.js +2 -2
- package/dist/workflows/implement.yml +2 -1
- package/dist/workflows/triage.yml +2 -1
- package/package.json +6 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sweny try --record <file.svg>`: the demo as an animated terminal SVG.
|
|
3
|
+
*
|
|
4
|
+
* Runs the real `sweny try` replay (instant, no TTY) with color forced on,
|
|
5
|
+
* captures the ANSI text, and lays it out as a slate terminal window whose
|
|
6
|
+
* lines appear on a CSS keyframe timeline: the command, a spinner per node
|
|
7
|
+
* until it settles, the answer, the receipt ticket with its stamp
|
|
8
|
+
* landing last. Then it loops.
|
|
9
|
+
*
|
|
10
|
+
* Self-contained by construction: inline CSS only, a monospace font stack, no
|
|
11
|
+
* external fonts, images, links or scripts. Deterministic: no clock, no ids,
|
|
12
|
+
* no randomness; the same fixture and theme always yield the same bytes, so
|
|
13
|
+
* CI can regenerate the file and fail when the committed copy is stale.
|
|
14
|
+
* `prefers-reduced-motion` shows the finished frame with no animation.
|
|
15
|
+
*/
|
|
16
|
+
import { type TryFixture } from "./try.js";
|
|
17
|
+
/** Terminal width the demo is rendered at (the ticket and DAG are designed for 80). */
|
|
18
|
+
export declare const RECORD_COLUMNS = 80;
|
|
19
|
+
/** Lines longer than this wrap in the window, like a terminal would. */
|
|
20
|
+
export declare const RECORD_WRAP = 96;
|
|
21
|
+
/** Width of one terminal column in the window, in px. Every segment is placed at column * CELL_W. */
|
|
22
|
+
export declare const CELL_W = 7.85;
|
|
23
|
+
/** Left edge of column 0. */
|
|
24
|
+
export declare const TEXT_X = 24;
|
|
25
|
+
export interface Style {
|
|
26
|
+
fg?: string;
|
|
27
|
+
bold?: boolean;
|
|
28
|
+
dim?: boolean;
|
|
29
|
+
underline?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface Cell {
|
|
32
|
+
ch: string;
|
|
33
|
+
style: Style;
|
|
34
|
+
}
|
|
35
|
+
/** One line of ANSI text as styled cells. Cursor and erase sequences are dropped. */
|
|
36
|
+
export declare function parseAnsiLine(line: string): Cell[];
|
|
37
|
+
/** Soft-wrap a row of cells at `max` columns, on spaces, continuation indented two past the first. */
|
|
38
|
+
export declare function wrapCells(cells: Cell[], max: number): Cell[][];
|
|
39
|
+
export interface Row {
|
|
40
|
+
cells: Cell[];
|
|
41
|
+
/** Vertical slot. A spinner shares the slot of the line that replaces it. */
|
|
42
|
+
slot: number;
|
|
43
|
+
/** Seconds at which the row appears. */
|
|
44
|
+
from: number;
|
|
45
|
+
/** Seconds at which it disappears again (spinners). Absent: stays until the loop restarts. */
|
|
46
|
+
to?: number;
|
|
47
|
+
spinner?: boolean;
|
|
48
|
+
}
|
|
49
|
+
/** Lay captured output lines on the timeline. Pure. */
|
|
50
|
+
export declare function buildTimeline(lines: string[], fixture: TryFixture): {
|
|
51
|
+
rows: Row[];
|
|
52
|
+
slots: number;
|
|
53
|
+
total: number;
|
|
54
|
+
};
|
|
55
|
+
/** Render the timeline as a self-contained animated SVG. Pure and deterministic. */
|
|
56
|
+
export declare function renderRecordingSvg(lines: string[], fixture: TryFixture): string;
|
|
57
|
+
/**
|
|
58
|
+
* The PR-comment preview (heading plus DAG) sits between the answer and the
|
|
59
|
+
* ticket. The recording drops it to stay a README-sized hero: tagline, nodes,
|
|
60
|
+
* answer, ticket.
|
|
61
|
+
*/
|
|
62
|
+
export declare function dropCommentPreview(lines: string[]): string[];
|
|
63
|
+
/** Run the demo and return the SVG text. */
|
|
64
|
+
export declare function recordTrySvg(fixture?: TryFixture): Promise<string>;
|
|
65
|
+
/** Record to `file`, creating its directory. Returns the byte size written. */
|
|
66
|
+
export declare function recordTrySvgToFile(file: string, fixture?: TryFixture): Promise<number>;
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sweny try --record <file.svg>`: the demo as an animated terminal SVG.
|
|
3
|
+
*
|
|
4
|
+
* Runs the real `sweny try` replay (instant, no TTY) with color forced on,
|
|
5
|
+
* captures the ANSI text, and lays it out as a slate terminal window whose
|
|
6
|
+
* lines appear on a CSS keyframe timeline: the command, a spinner per node
|
|
7
|
+
* until it settles, the answer, the receipt ticket with its stamp
|
|
8
|
+
* landing last. Then it loops.
|
|
9
|
+
*
|
|
10
|
+
* Self-contained by construction: inline CSS only, a monospace font stack, no
|
|
11
|
+
* external fonts, images, links or scripts. Deterministic: no clock, no ids,
|
|
12
|
+
* no randomness; the same fixture and theme always yield the same bytes, so
|
|
13
|
+
* CI can regenerate the file and fail when the committed copy is stale.
|
|
14
|
+
* `prefers-reduced-motion` shows the finished frame with no animation.
|
|
15
|
+
*/
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import chalk from "chalk";
|
|
19
|
+
import { GLYPHS, PALETTE, ROLE_COLORS, SPINNER_FRAMES, SURFACE_COLORS } from "../theme.js";
|
|
20
|
+
import { loadTryFixture, runTry } from "./try.js";
|
|
21
|
+
/** Terminal width the demo is rendered at (the ticket and DAG are designed for 80). */
|
|
22
|
+
export const RECORD_COLUMNS = 80;
|
|
23
|
+
/** Lines longer than this wrap in the window, like a terminal would. */
|
|
24
|
+
export const RECORD_WRAP = 96;
|
|
25
|
+
const FONT_SIZE = 13;
|
|
26
|
+
/** Width of one terminal column in the window, in px. Every segment is placed at column * CELL_W. */
|
|
27
|
+
export const CELL_W = 7.85;
|
|
28
|
+
/** Left edge of column 0. */
|
|
29
|
+
export const TEXT_X = 24;
|
|
30
|
+
const NBSP = "\u00A0";
|
|
31
|
+
const LINE_H = 19;
|
|
32
|
+
const PAD_X = TEXT_X;
|
|
33
|
+
const BAR_H = 38;
|
|
34
|
+
const PAD_TOP = 18;
|
|
35
|
+
const PAD_BOTTOM = 22;
|
|
36
|
+
const WINDOW_W = Math.ceil(RECORD_WRAP * CELL_W + PAD_X * 2);
|
|
37
|
+
const FONT_STACK = "ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace";
|
|
38
|
+
const SECONDS_PER_LINE = 0.06;
|
|
39
|
+
const SECONDS_PER_BLANK = 0.25;
|
|
40
|
+
const SECONDS_STAMP_DELAY = 0.5;
|
|
41
|
+
const SECONDS_HOLD = 5;
|
|
42
|
+
const BASIC_COLORS = {
|
|
43
|
+
30: PALETTE.slate900,
|
|
44
|
+
31: ROLE_COLORS.error,
|
|
45
|
+
32: ROLE_COLORS.success,
|
|
46
|
+
33: ROLE_COLORS.warning,
|
|
47
|
+
34: ROLE_COLORS.brand,
|
|
48
|
+
35: PALETTE.blue300,
|
|
49
|
+
36: ROLE_COLORS.info,
|
|
50
|
+
37: SURFACE_COLORS.dark.text,
|
|
51
|
+
90: ROLE_COLORS.muted,
|
|
52
|
+
91: ROLE_COLORS.error,
|
|
53
|
+
92: ROLE_COLORS.success,
|
|
54
|
+
93: ROLE_COLORS.warning,
|
|
55
|
+
94: ROLE_COLORS.info,
|
|
56
|
+
95: PALETTE.blue300,
|
|
57
|
+
96: ROLE_COLORS.info,
|
|
58
|
+
97: PALETTE.white,
|
|
59
|
+
};
|
|
60
|
+
const hex = (n) => n.toString(16).padStart(2, "0");
|
|
61
|
+
/** Apply one SGR parameter list to a style. */
|
|
62
|
+
function applySgr(style, params) {
|
|
63
|
+
const next = { ...style };
|
|
64
|
+
for (let i = 0; i < params.length; i++) {
|
|
65
|
+
const p = params[i];
|
|
66
|
+
if (p === 0)
|
|
67
|
+
return {};
|
|
68
|
+
else if (p === 1)
|
|
69
|
+
next.bold = true;
|
|
70
|
+
else if (p === 2)
|
|
71
|
+
next.dim = true;
|
|
72
|
+
else if (p === 22) {
|
|
73
|
+
delete next.bold;
|
|
74
|
+
delete next.dim;
|
|
75
|
+
}
|
|
76
|
+
else if (p === 4)
|
|
77
|
+
next.underline = true;
|
|
78
|
+
else if (p === 24)
|
|
79
|
+
delete next.underline;
|
|
80
|
+
else if (p === 39)
|
|
81
|
+
delete next.fg;
|
|
82
|
+
else if (p === 38 && params[i + 1] === 2) {
|
|
83
|
+
next.fg = `#${hex(params[i + 2] ?? 0)}${hex(params[i + 3] ?? 0)}${hex(params[i + 4] ?? 0)}`;
|
|
84
|
+
i += 4;
|
|
85
|
+
}
|
|
86
|
+
else if (p === 38 && params[i + 1] === 5) {
|
|
87
|
+
next.fg = SURFACE_COLORS.dark.text;
|
|
88
|
+
i += 2;
|
|
89
|
+
}
|
|
90
|
+
else if (BASIC_COLORS[p])
|
|
91
|
+
next.fg = BASIC_COLORS[p];
|
|
92
|
+
}
|
|
93
|
+
return next;
|
|
94
|
+
}
|
|
95
|
+
/** One line of ANSI text as styled cells. Cursor and erase sequences are dropped. */
|
|
96
|
+
export function parseAnsiLine(line) {
|
|
97
|
+
const cells = [];
|
|
98
|
+
let style = {};
|
|
99
|
+
const re = /\x1B\[([0-9;]*)([A-Za-z])/g;
|
|
100
|
+
let last = 0;
|
|
101
|
+
const push = (text) => {
|
|
102
|
+
for (const ch of text.replace(/\r/g, ""))
|
|
103
|
+
cells.push({ ch, style });
|
|
104
|
+
};
|
|
105
|
+
for (let m = re.exec(line); m; m = re.exec(line)) {
|
|
106
|
+
push(line.slice(last, m.index));
|
|
107
|
+
if (m[2] === "m")
|
|
108
|
+
style = applySgr(style, m[1] === "" ? [0] : m[1].split(";").map(Number));
|
|
109
|
+
last = m.index + m[0].length;
|
|
110
|
+
}
|
|
111
|
+
push(line.slice(last));
|
|
112
|
+
return cells;
|
|
113
|
+
}
|
|
114
|
+
const sameStyle = (a, b) => a.fg === b.fg && !!a.bold === !!b.bold && !!a.dim === !!b.dim && !!a.underline === !!b.underline;
|
|
115
|
+
/** Soft-wrap a row of cells at `max` columns, on spaces, continuation indented two past the first. */
|
|
116
|
+
export function wrapCells(cells, max) {
|
|
117
|
+
const rows = [];
|
|
118
|
+
const indent = cells.findIndex((c) => c.ch !== " ");
|
|
119
|
+
const pad = " ".repeat(Math.max(0, indent) + 2);
|
|
120
|
+
let rest = cells;
|
|
121
|
+
while (rest.length > max) {
|
|
122
|
+
let cut = -1;
|
|
123
|
+
for (let i = max; i > pad.length; i--) {
|
|
124
|
+
if (rest[i].ch === " ") {
|
|
125
|
+
cut = i;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (cut < 0)
|
|
130
|
+
cut = max;
|
|
131
|
+
rows.push(rest.slice(0, cut));
|
|
132
|
+
let tail = rest.slice(cut);
|
|
133
|
+
while (tail.length > 0 && tail[0].ch === " ")
|
|
134
|
+
tail = tail.slice(1);
|
|
135
|
+
rest = [...[...pad].map((ch) => ({ ch, style: {} })), ...tail];
|
|
136
|
+
}
|
|
137
|
+
rows.push(rest);
|
|
138
|
+
return rows;
|
|
139
|
+
}
|
|
140
|
+
const plainText = (cells) => cells.map((c) => c.ch).join("");
|
|
141
|
+
function textCells(text, style = {}) {
|
|
142
|
+
return [...text].map((ch) => ({ ch, style }));
|
|
143
|
+
}
|
|
144
|
+
/** Lay captured output lines on the timeline. Pure. */
|
|
145
|
+
export function buildTimeline(lines, fixture) {
|
|
146
|
+
const rows = [];
|
|
147
|
+
const nodeIds = Object.keys(fixture.nodes);
|
|
148
|
+
const settledPrefix = (id) => ` ${GLYPHS.unicode.success} ${id}`;
|
|
149
|
+
const spinnerGlyph = SPINNER_FRAMES.unicode[3];
|
|
150
|
+
let t = 0.5;
|
|
151
|
+
let slot = 0;
|
|
152
|
+
// The command being typed.
|
|
153
|
+
rows.push({
|
|
154
|
+
cells: [...textCells("$ ", { fg: ROLE_COLORS.muted }), ...textCells("sweny try", { bold: true })],
|
|
155
|
+
slot: slot++,
|
|
156
|
+
from: t,
|
|
157
|
+
});
|
|
158
|
+
t += 0.9;
|
|
159
|
+
for (const line of lines) {
|
|
160
|
+
const cells = parseAnsiLine(line);
|
|
161
|
+
const text = plainText(cells);
|
|
162
|
+
const id = nodeIds.find((n) => text.startsWith(settledPrefix(n)));
|
|
163
|
+
if (id) {
|
|
164
|
+
const seconds = 0.8 + Math.min(1.6, fixture.nodes[id].duration_ms / 12000);
|
|
165
|
+
rows.push({
|
|
166
|
+
cells: [...textCells(" "), ...textCells(spinnerGlyph, { fg: ROLE_COLORS.brand }), ...textCells(` ${id}`)],
|
|
167
|
+
slot,
|
|
168
|
+
from: t,
|
|
169
|
+
to: t + seconds,
|
|
170
|
+
spinner: true,
|
|
171
|
+
});
|
|
172
|
+
t += seconds;
|
|
173
|
+
}
|
|
174
|
+
else if (/\[ (ENV|SANDBOXED|UNSANDBOXED)\b/.test(text)) {
|
|
175
|
+
// The policy stamp lands a beat after the card.
|
|
176
|
+
t += SECONDS_STAMP_DELAY;
|
|
177
|
+
}
|
|
178
|
+
for (const part of wrapCells(cells, RECORD_WRAP)) {
|
|
179
|
+
rows.push({ cells: part, slot, from: t });
|
|
180
|
+
slot++;
|
|
181
|
+
}
|
|
182
|
+
t += text.trim() === "" ? SECONDS_PER_BLANK : SECONDS_PER_LINE;
|
|
183
|
+
}
|
|
184
|
+
return { rows, slots: slot, total: t + SECONDS_HOLD };
|
|
185
|
+
}
|
|
186
|
+
// ── SVG ─────────────────────────────────────────────────────────
|
|
187
|
+
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
188
|
+
const pct = (seconds, total) => ((seconds / total) * 100).toFixed(2);
|
|
189
|
+
/** Render the timeline as a self-contained animated SVG. Pure and deterministic. */
|
|
190
|
+
export function renderRecordingSvg(lines, fixture) {
|
|
191
|
+
const { rows, slots, total } = buildTimeline(lines, fixture);
|
|
192
|
+
const height = BAR_H + PAD_TOP + slots * LINE_H + PAD_BOTTOM;
|
|
193
|
+
const dark = SURFACE_COLORS.dark;
|
|
194
|
+
// Color classes by first use, so the CSS is small and the order is stable.
|
|
195
|
+
const colorClass = new Map();
|
|
196
|
+
const classFor = (style) => {
|
|
197
|
+
const names = [];
|
|
198
|
+
if (style.fg) {
|
|
199
|
+
let c = colorClass.get(style.fg);
|
|
200
|
+
if (!c) {
|
|
201
|
+
c = `c${colorClass.size}`;
|
|
202
|
+
colorClass.set(style.fg, c);
|
|
203
|
+
}
|
|
204
|
+
names.push(c);
|
|
205
|
+
}
|
|
206
|
+
if (style.bold)
|
|
207
|
+
names.push("b");
|
|
208
|
+
if (style.dim)
|
|
209
|
+
names.push("d");
|
|
210
|
+
if (style.underline)
|
|
211
|
+
names.push("u");
|
|
212
|
+
return names.join(" ");
|
|
213
|
+
};
|
|
214
|
+
// Keyframes by (from, to), numbered by first use.
|
|
215
|
+
const frames = new Map();
|
|
216
|
+
const frameFor = (from, to) => {
|
|
217
|
+
const key = `${pct(from, total)}:${to === undefined ? "" : pct(to, total)}`;
|
|
218
|
+
let name = frames.get(key);
|
|
219
|
+
if (!name) {
|
|
220
|
+
name = `k${frames.size}`;
|
|
221
|
+
frames.set(key, name);
|
|
222
|
+
}
|
|
223
|
+
return name;
|
|
224
|
+
};
|
|
225
|
+
const body = [];
|
|
226
|
+
for (const row of rows) {
|
|
227
|
+
const y = BAR_H + PAD_TOP + row.slot * LINE_H + FONT_SIZE;
|
|
228
|
+
const spans = [];
|
|
229
|
+
let i = 0;
|
|
230
|
+
while (i < row.cells.length) {
|
|
231
|
+
let j = i + 1;
|
|
232
|
+
while (j < row.cells.length && sameStyle(row.cells[i].style, row.cells[j].style))
|
|
233
|
+
j++;
|
|
234
|
+
const raw = row.cells
|
|
235
|
+
.slice(i, j)
|
|
236
|
+
.map((c) => c.ch)
|
|
237
|
+
.join("");
|
|
238
|
+
// Layout must not depend on whitespace handling (rsvg, proxies and some viewers collapse
|
|
239
|
+
// spaces): each segment sits at an explicit x for its column, edge spaces become offset
|
|
240
|
+
// or nothing, and inner spaces are non-breaking.
|
|
241
|
+
const lead = raw.length - raw.trimStart().length;
|
|
242
|
+
const body_ = raw.trim();
|
|
243
|
+
if (body_ !== "") {
|
|
244
|
+
const x = (TEXT_X + (i + lead) * CELL_W).toFixed(2);
|
|
245
|
+
const text = esc(body_).replace(/ /g, NBSP);
|
|
246
|
+
const cls = classFor(row.cells[i].style);
|
|
247
|
+
spans.push(`<tspan x="${x}"${cls ? ` class="${cls}"` : ""}>${text}</tspan>`);
|
|
248
|
+
}
|
|
249
|
+
i = j;
|
|
250
|
+
}
|
|
251
|
+
const cls = `r ${frameFor(row.from, row.to)}${row.spinner ? " sp" : ""}`;
|
|
252
|
+
body.push(`<text class="${cls}" x="${TEXT_X}" y="${y}" xml:space="preserve">${spans.join("")}</text>`);
|
|
253
|
+
}
|
|
254
|
+
const css = [
|
|
255
|
+
`text{font-family:${FONT_STACK};font-size:${FONT_SIZE}px;fill:${dark.text};white-space:pre}`,
|
|
256
|
+
// No-animation default is the FINAL frame: lines visible, spinner frames hidden. Keyframes drive the replay.
|
|
257
|
+
`.r{opacity:1}`,
|
|
258
|
+
`.sp{opacity:0}`,
|
|
259
|
+
`.b{font-weight:700}`,
|
|
260
|
+
`.d{fill-opacity:.65}`,
|
|
261
|
+
`.u{text-decoration:underline}`,
|
|
262
|
+
`.t{fill:${dark.textSecondary};font-size:12px}`,
|
|
263
|
+
];
|
|
264
|
+
for (const [fg, name] of colorClass)
|
|
265
|
+
css.push(`.${name}{fill:${fg}}`);
|
|
266
|
+
for (const [key, name] of frames) {
|
|
267
|
+
const [from, to] = key.split(":");
|
|
268
|
+
const a = Number(from);
|
|
269
|
+
const keyframe = to === ""
|
|
270
|
+
? `0%,${from}%{opacity:0}${(a + 0.01).toFixed(2)}%,98%{opacity:1}100%{opacity:0}`
|
|
271
|
+
: `0%,${from}%{opacity:0}${(a + 0.01).toFixed(2)}%,${to}%{opacity:1}${(Number(to) + 0.01).toFixed(2)}%,100%{opacity:0}`;
|
|
272
|
+
css.push(`@keyframes ${name}{${keyframe}}`);
|
|
273
|
+
css.push(`.${name}{animation:${name} ${total.toFixed(2)}s linear infinite}`);
|
|
274
|
+
}
|
|
275
|
+
css.push(`@media (prefers-reduced-motion:reduce){.r{animation:none!important}.sp{display:none}}`);
|
|
276
|
+
const label = "Recorded demo of sweny try: two nodes run, then the answer and the receipt ticket.";
|
|
277
|
+
return [
|
|
278
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${WINDOW_W} ${height}" width="${WINDOW_W}" height="${height}" xml:space="preserve" role="img" aria-label="${esc(label)}">`,
|
|
279
|
+
`<title>sweny try</title>`,
|
|
280
|
+
`<style>${css.join("")}</style>`,
|
|
281
|
+
`<rect x="0.5" y="0.5" width="${WINDOW_W - 1}" height="${height - 1}" rx="10" fill="${dark.background}" stroke="${dark.border}"/>`,
|
|
282
|
+
`<path d="M0.5 ${BAR_H}V10.5A10 10 0 0 1 10.5 0.5H${WINDOW_W - 10.5}A10 10 0 0 1 ${WINDOW_W - 0.5} 10.5V${BAR_H}Z" fill="${PALETTE.slate900}" stroke="${dark.border}"/>`,
|
|
283
|
+
`<circle cx="20" cy="19" r="5.5" fill="${ROLE_COLORS.error}"/>`,
|
|
284
|
+
`<circle cx="40" cy="19" r="5.5" fill="${ROLE_COLORS.warning}"/>`,
|
|
285
|
+
`<circle cx="60" cy="19" r="5.5" fill="${ROLE_COLORS.success}"/>`,
|
|
286
|
+
`<text class="t" x="${WINDOW_W / 2}" y="23" text-anchor="middle">sweny try</text>`,
|
|
287
|
+
...body,
|
|
288
|
+
`</svg>`,
|
|
289
|
+
``,
|
|
290
|
+
].join("\n");
|
|
291
|
+
}
|
|
292
|
+
// ── Entry points ────────────────────────────────────────────────
|
|
293
|
+
const stripAnsi = (s) => s.replace(/\x1B\[[0-9;]*[A-Za-z]/g, "");
|
|
294
|
+
/**
|
|
295
|
+
* The PR-comment preview (heading plus DAG) sits between the answer and the
|
|
296
|
+
* ticket. The recording drops it to stay a README-sized hero: tagline, nodes,
|
|
297
|
+
* answer, ticket.
|
|
298
|
+
*/
|
|
299
|
+
export function dropCommentPreview(lines) {
|
|
300
|
+
const from = lines.findIndex((l) => stripAnsi(l).includes("On a pull request, CI posts"));
|
|
301
|
+
const to = lines.findIndex((l, i) => i > from && stripAnsi(l).startsWith(" \u256D"));
|
|
302
|
+
if (from < 0 || to < 0)
|
|
303
|
+
return lines;
|
|
304
|
+
return [...lines.slice(0, from), ...lines.slice(to)];
|
|
305
|
+
}
|
|
306
|
+
/** Run the demo and return the SVG text. */
|
|
307
|
+
export async function recordTrySvg(fixture = loadTryFixture()) {
|
|
308
|
+
let out = "";
|
|
309
|
+
// The DAG renderer paints through the default chalk; pin it so the capture carries color on any host.
|
|
310
|
+
const prevLevel = chalk.level;
|
|
311
|
+
chalk.level = 3;
|
|
312
|
+
try {
|
|
313
|
+
const code = await runTry({
|
|
314
|
+
fast: true,
|
|
315
|
+
tty: false,
|
|
316
|
+
color: true,
|
|
317
|
+
rich: true,
|
|
318
|
+
unicode: true,
|
|
319
|
+
columns: RECORD_COLUMNS,
|
|
320
|
+
fixture,
|
|
321
|
+
write: (s) => {
|
|
322
|
+
out += s;
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
if (code !== 0)
|
|
326
|
+
throw new Error(`the demo run exited ${code}`);
|
|
327
|
+
}
|
|
328
|
+
finally {
|
|
329
|
+
chalk.level = prevLevel;
|
|
330
|
+
}
|
|
331
|
+
const lines = out.split("\n");
|
|
332
|
+
while (lines.length > 0 && lines[0].trim() === "")
|
|
333
|
+
lines.shift();
|
|
334
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "")
|
|
335
|
+
lines.pop();
|
|
336
|
+
return renderRecordingSvg(dropCommentPreview(lines), fixture);
|
|
337
|
+
}
|
|
338
|
+
/** Record to `file`, creating its directory. Returns the byte size written. */
|
|
339
|
+
export async function recordTrySvgToFile(file, fixture) {
|
|
340
|
+
const svg = await recordTrySvg(fixture);
|
|
341
|
+
fs.mkdirSync(path.dirname(path.resolve(file)), { recursive: true });
|
|
342
|
+
fs.writeFileSync(file, svg);
|
|
343
|
+
return Buffer.byteLength(svg);
|
|
344
|
+
}
|
package/dist/cli/renderer.d.ts
CHANGED
|
@@ -7,6 +7,10 @@ export interface DagRendererOptions {
|
|
|
7
7
|
animate?: boolean;
|
|
8
8
|
/** Stream to write to when animate=true or render() is called. Default: process.stderr. */
|
|
9
9
|
stream?: NodeJS.WriteStream;
|
|
10
|
+
/** Show the status legend under the graph. Default: true. */
|
|
11
|
+
legend?: boolean;
|
|
12
|
+
/** Draw with Unicode box characters, else ASCII. Default: what the terminal supports. */
|
|
13
|
+
unicode?: boolean;
|
|
10
14
|
}
|
|
11
15
|
export declare class DagRenderer {
|
|
12
16
|
private readonly workflow;
|
package/dist/cli/renderer.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
+
import { GLYPHS, ROLE_COLORS } from "../theme.js";
|
|
3
|
+
import { supportsUnicode, toAsciiDrawing } from "./terminal.js";
|
|
2
4
|
// ── Helpers ──────────────────────────────────────────────────────
|
|
3
5
|
/** Strip ANSI escape codes for accurate visible-width calculations. */
|
|
4
6
|
export function stripAnsi(str) {
|
|
@@ -48,17 +50,18 @@ function topologicalOrder(workflow) {
|
|
|
48
50
|
}
|
|
49
51
|
return order;
|
|
50
52
|
}
|
|
51
|
-
// ── Status icons + colors
|
|
53
|
+
// ── Status icons + colors (the theme glyph set, one role color each) ─
|
|
54
|
+
const G = GLYPHS.unicode;
|
|
52
55
|
function statusIcon(state) {
|
|
53
56
|
switch (state) {
|
|
54
57
|
case "completed":
|
|
55
|
-
return chalk.
|
|
58
|
+
return chalk.hex(ROLE_COLORS.success)(G.success);
|
|
56
59
|
case "running":
|
|
57
|
-
return chalk.
|
|
60
|
+
return chalk.hex(ROLE_COLORS.brand)(G.running);
|
|
58
61
|
case "pending":
|
|
59
|
-
return chalk.
|
|
62
|
+
return chalk.hex(ROLE_COLORS.muted)(G.pending);
|
|
60
63
|
case "failed":
|
|
61
|
-
return chalk.
|
|
64
|
+
return chalk.hex(ROLE_COLORS.error)(G.failure);
|
|
62
65
|
}
|
|
63
66
|
}
|
|
64
67
|
// ── Graph helpers ────────────────────────────────────────────────
|
|
@@ -143,7 +146,7 @@ function renderNodeBox(nodeName, nodeRenderState, innerWidth, indent, topConnect
|
|
|
143
146
|
else {
|
|
144
147
|
bottom = `${pad}└${"─".repeat(innerWidth)}┘`;
|
|
145
148
|
}
|
|
146
|
-
// Content lines
|
|
149
|
+
// Content lines: icon on first line, wrapped name
|
|
147
150
|
// Available space for text: innerWidth - 4 (1 space + icon + 1 space + text + padding + 1 space)
|
|
148
151
|
// "│ ○ Name...padding │" → icon(1) + spaces(2) + name + pad
|
|
149
152
|
const iconVis = stripAnsi(icon); // 1 char
|
|
@@ -184,10 +187,10 @@ function renderNodeBox(nodeName, nodeRenderState, innerWidth, indent, topConnect
|
|
|
184
187
|
// ── Legend ───────────────────────────────────────────────────────
|
|
185
188
|
function renderLegend() {
|
|
186
189
|
const items = [
|
|
187
|
-
`${
|
|
188
|
-
`${
|
|
189
|
-
`${
|
|
190
|
-
`${
|
|
190
|
+
`${statusIcon("completed")} completed`,
|
|
191
|
+
`${statusIcon("running")} running`,
|
|
192
|
+
`${statusIcon("pending")} pending`,
|
|
193
|
+
`${statusIcon("failed")} failed`,
|
|
191
194
|
];
|
|
192
195
|
return `\n ${chalk.dim(items.join(" "))}`;
|
|
193
196
|
}
|
|
@@ -202,6 +205,8 @@ export class DagRenderer {
|
|
|
202
205
|
this.workflow = workflow;
|
|
203
206
|
this.options = {
|
|
204
207
|
animate: options.animate ?? false,
|
|
208
|
+
legend: options.legend ?? true,
|
|
209
|
+
unicode: options.unicode ?? supportsUnicode(),
|
|
205
210
|
stream: options.stream ??
|
|
206
211
|
(typeof process !== "undefined" ? process.stderr : undefined),
|
|
207
212
|
};
|
|
@@ -298,7 +303,7 @@ export class DagRenderer {
|
|
|
298
303
|
continue;
|
|
299
304
|
const leftState = this.nodeStates.get(leftId) ?? { state: "pending", toolCallCount: 0 };
|
|
300
305
|
const rightState = this.nodeStates.get(rightId) ?? { state: "pending", toolCallCount: 0 };
|
|
301
|
-
// Determine child box widths
|
|
306
|
+
// Determine child box widths: each child gets its own width based on its name
|
|
302
307
|
const leftKids = children.get(leftId) ?? [];
|
|
303
308
|
const rightKids = children.get(rightId) ?? [];
|
|
304
309
|
// For side-by-side, compute widths that fit
|
|
@@ -391,8 +396,10 @@ export class DagRenderer {
|
|
|
391
396
|
}
|
|
392
397
|
}
|
|
393
398
|
}
|
|
394
|
-
|
|
395
|
-
|
|
399
|
+
if (this.options.legend)
|
|
400
|
+
lines.push(renderLegend());
|
|
401
|
+
const out = lines.join("\n");
|
|
402
|
+
return this.options.unicode ? out : toAsciiDrawing(out);
|
|
396
403
|
}
|
|
397
404
|
/**
|
|
398
405
|
* Writes the current DAG render to the stream.
|
package/dist/cli/run-output.d.ts
CHANGED
|
@@ -68,6 +68,11 @@ export interface RunPolicy {
|
|
|
68
68
|
export declare function degradedKey(entry: string): string;
|
|
69
69
|
/** Sum the per-node results into one run summary. */
|
|
70
70
|
export declare function summarizeRun(results: Map<string, NodeResult>, durationMs: number, crashed?: boolean, trace?: ExecutionTrace): RunSummary;
|
|
71
|
+
/**
|
|
72
|
+
* The policy facts the run knew, as short phrases: `env scoped`, `sandbox on`,
|
|
73
|
+
* `1 output staged`. No phrase for a fact no node reported.
|
|
74
|
+
*/
|
|
75
|
+
export declare function policyParts(p: RunPolicy | undefined): string[];
|
|
71
76
|
/**
|
|
72
77
|
* `policy: env scoped, sandbox on, 1 output staged`. Only what the run knew:
|
|
73
78
|
* no segment for a fact no node reported. Undefined when there is nothing to say.
|
|
@@ -76,8 +81,12 @@ export declare function formatPolicySegment(p: RunPolicy | undefined): string |
|
|
|
76
81
|
export declare function formatReceiptDuration(ms: number): string;
|
|
77
82
|
export declare function formatTokenCount(n: number): string;
|
|
78
83
|
export declare function formatCost(usd: number): string;
|
|
84
|
+
/** `tokens 52k of 50k`: the overrun, in the budget's unit. */
|
|
85
|
+
export declare function formatBudgetAmount(b: NonNullable<RunSummary["budget"]>): string;
|
|
79
86
|
/** `budget exceeded: tokens 52k of 50k (run, at node gather)`. */
|
|
80
87
|
export declare function formatBudgetOverrun(b: NonNullable<RunSummary["budget"]>): string;
|
|
88
|
+
/** `agreed 4/5`, or `fell through 2/2` when no decision passed the gates. */
|
|
89
|
+
export declare function formatDeciderCounts(d: NonNullable<RunSummary["decider"]>): string;
|
|
81
90
|
/**
|
|
82
91
|
* One plain line: `✓ 3/3 nodes · 41 tool calls · 2m10s · 12k tokens · $0.18`.
|
|
83
92
|
* Token and cost segments are omitted when the SDK reported none. A harness
|
|
@@ -86,15 +95,11 @@ export declare function formatBudgetOverrun(b: NonNullable<RunSummary["budget"]>
|
|
|
86
95
|
* when writing to a TTY.
|
|
87
96
|
*/
|
|
88
97
|
export declare function formatReceipt(s: RunSummary): string;
|
|
89
|
-
/** Receipt for a terminal:
|
|
98
|
+
/** Receipt for a terminal: in the success or error color only when `color` is true. */
|
|
90
99
|
export declare function renderReceiptLine(s: RunSummary, color: boolean): string;
|
|
91
|
-
/** Brand blue (blue-600 / blue-700), not indigo. Applied to successful nodes. */
|
|
92
|
-
export declare const STEP_SUMMARY_CLASS_DEFS: {
|
|
93
|
-
readonly success: "fill:#2563eb,stroke:#1d4ed8,color:#fff,stroke-width:2px";
|
|
94
|
-
};
|
|
95
100
|
/** Per-node Mermaid status from run results. */
|
|
96
101
|
export declare function nodeStates(results: Map<string, NodeResult>): Record<string, NodeStatus>;
|
|
97
|
-
/** Markdown for `$GITHUB_STEP_SUMMARY`: receipt +
|
|
102
|
+
/** Markdown for `$GITHUB_STEP_SUMMARY`: receipt + Mermaid DAG in the brand classDefs. */
|
|
98
103
|
export declare function formatStepSummary(workflow: Workflow, results: Map<string, NodeResult>, summary: RunSummary, trace?: ExecutionTrace): string;
|
|
99
104
|
/**
|
|
100
105
|
* Append the step summary to the file named by GITHUB_STEP_SUMMARY.
|
|
@@ -116,6 +121,8 @@ export interface RunLogger extends Logger {
|
|
|
116
121
|
export declare function createRunLogger(opts: {
|
|
117
122
|
verbose: boolean;
|
|
118
123
|
tty: boolean;
|
|
124
|
+
/** Color the markers. Default: `tty` and color is enabled for stderr. */
|
|
125
|
+
color?: boolean;
|
|
119
126
|
write?: (s: string) => void;
|
|
120
127
|
}): RunLogger;
|
|
121
128
|
export declare const WORKFLOW_RUN_DESCRIPTION = "Run a workflow from a YAML or JSON file. With no file, batch-runs every workflow in .sweny/e2e/ (lists and confirms first; use --yes to skip the prompt).";
|