@sweny-ai/core 0.1.169 → 0.1.171
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/comment-output.d.ts +4 -2
- package/dist/cli/comment-output.js +16 -8
- package/dist/cli/main.js +42 -26
- package/dist/cli/new.js +3 -0
- 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/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/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 +15 -8
- package/dist/cli/try.js +84 -65
- package/dist/executor.d.ts +8 -1
- package/dist/executor.js +21 -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/testing.d.ts +16 -12
- package/dist/testing.js +45 -20
- package/dist/theme.d.ts +185 -0
- package/dist/theme.js +183 -0
- package/dist/types.d.ts +2 -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,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node progress lines for `sweny workflow run` and `sweny try`: a spinner on
|
|
3
|
+
* the running node when the output is live, then one settled line per node.
|
|
4
|
+
*
|
|
5
|
+
* ⠙ survey 7 tool calls (live, redrawn in place)
|
|
6
|
+
* ✓ survey 18s · 12 tool calls (settled)
|
|
7
|
+
*
|
|
8
|
+
* Not live (pipes, CI, --verbose): no redraws, no spinner, one line per event.
|
|
9
|
+
*/
|
|
10
|
+
import { SPINNER_INTERVAL_MS } from "../theme.js";
|
|
11
|
+
import { glyphsFor, spinnerFramesFor } from "./terminal.js";
|
|
12
|
+
export function toolCallsText(n) {
|
|
13
|
+
return `${n} tool ${n === 1 ? "call" : "calls"}`;
|
|
14
|
+
}
|
|
15
|
+
export function createNodeProgress(o) {
|
|
16
|
+
const g = glyphsFor(o.unicode);
|
|
17
|
+
const frames = spinnerFramesFor(o.unicode);
|
|
18
|
+
const { paint } = o;
|
|
19
|
+
const pad = (id) => id.padEnd(o.idWidth ?? 0);
|
|
20
|
+
let timer;
|
|
21
|
+
let current;
|
|
22
|
+
let frame = 0;
|
|
23
|
+
const draw = () => {
|
|
24
|
+
if (!current)
|
|
25
|
+
return;
|
|
26
|
+
const calls = current.calls > 0 ? ` ${paint.muted(toolCallsText(current.calls))}` : "";
|
|
27
|
+
o.write(`\r\x1B[2K ${paint.brand(frames[frame++ % frames.length])} ${current.node}${calls}`);
|
|
28
|
+
};
|
|
29
|
+
const clear = () => {
|
|
30
|
+
if (timer)
|
|
31
|
+
clearInterval(timer);
|
|
32
|
+
timer = undefined;
|
|
33
|
+
if (current && o.live)
|
|
34
|
+
o.write("\r\x1B[2K");
|
|
35
|
+
current = undefined;
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
enter(node) {
|
|
39
|
+
clear();
|
|
40
|
+
if (o.live) {
|
|
41
|
+
current = { node, calls: 0 };
|
|
42
|
+
frame = 0;
|
|
43
|
+
draw();
|
|
44
|
+
timer = setInterval(draw, SPINNER_INTERVAL_MS);
|
|
45
|
+
timer.unref?.();
|
|
46
|
+
}
|
|
47
|
+
else if (o.announce) {
|
|
48
|
+
o.write(` ${paint.muted(g.pending)} ${paint.muted(node)}${g.ellipsis}\n`);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
tick(node) {
|
|
52
|
+
if (current && current.node === node)
|
|
53
|
+
current.calls++;
|
|
54
|
+
},
|
|
55
|
+
exit(node, status, detail) {
|
|
56
|
+
clear();
|
|
57
|
+
const glyph = status === "success"
|
|
58
|
+
? paint.success(g.success)
|
|
59
|
+
: status === "failed"
|
|
60
|
+
? paint.error(g.failure)
|
|
61
|
+
: paint.muted(g.skipped);
|
|
62
|
+
const tail = detail ? ` ${paint.muted(detail)}` : "";
|
|
63
|
+
o.write(` ${glyph} ${detail ? pad(node) : node}${tail}\n`);
|
|
64
|
+
},
|
|
65
|
+
stop() {
|
|
66
|
+
clear();
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
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).";
|
package/dist/cli/run-output.js
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* model prose: only counts, durations, token totals, and cost.
|
|
7
7
|
*/
|
|
8
8
|
import fs from "node:fs";
|
|
9
|
-
import chalk from "chalk";
|
|
10
9
|
import { consoleLogger } from "../types.js";
|
|
11
10
|
import { summarizeDecisions } from "../decider.js";
|
|
12
11
|
import { toMermaidBlock } from "../mermaid.js";
|
|
12
|
+
import { createPaint } from "./style.js";
|
|
13
|
+
import { colorEnabled, glyphs } from "./terminal.js";
|
|
13
14
|
/** The short key of a `degraded` entry: the text before its first colon. */
|
|
14
15
|
export function degradedKey(entry) {
|
|
15
16
|
const i = entry.indexOf(":");
|
|
@@ -90,12 +91,12 @@ function decisionCounts(trace) {
|
|
|
90
91
|
return { compared: d.compared, agreed: d.agreed, fellThrough: d.fell_through };
|
|
91
92
|
}
|
|
92
93
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
94
|
+
* The policy facts the run knew, as short phrases: `env scoped`, `sandbox on`,
|
|
95
|
+
* `1 output staged`. No phrase for a fact no node reported.
|
|
95
96
|
*/
|
|
96
|
-
export function
|
|
97
|
+
export function policyParts(p) {
|
|
97
98
|
if (!p)
|
|
98
|
-
return
|
|
99
|
+
return [];
|
|
99
100
|
const parts = [];
|
|
100
101
|
if (p.envScope !== undefined)
|
|
101
102
|
parts.push(p.envScope ? "env scoped" : "env unscoped");
|
|
@@ -113,6 +114,14 @@ export function formatPolicySegment(p) {
|
|
|
113
114
|
if (p.outputs.applied > 0)
|
|
114
115
|
parts.push(`${n(p.outputs.applied)} applied`);
|
|
115
116
|
}
|
|
117
|
+
return parts;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* `policy: env scoped, sandbox on, 1 output staged`. Only what the run knew:
|
|
121
|
+
* no segment for a fact no node reported. Undefined when there is nothing to say.
|
|
122
|
+
*/
|
|
123
|
+
export function formatPolicySegment(p) {
|
|
124
|
+
const parts = policyParts(p);
|
|
116
125
|
return parts.length > 0 ? `policy: ${parts.join(", ")}` : undefined;
|
|
117
126
|
}
|
|
118
127
|
export function formatReceiptDuration(ms) {
|
|
@@ -137,11 +146,19 @@ export function formatCost(usd) {
|
|
|
137
146
|
return "<$0.01";
|
|
138
147
|
return `$${usd.toFixed(2)}`;
|
|
139
148
|
}
|
|
140
|
-
/** `
|
|
141
|
-
export function
|
|
149
|
+
/** `tokens 52k of 50k`: the overrun, in the budget's unit. */
|
|
150
|
+
export function formatBudgetAmount(b) {
|
|
142
151
|
const amount = (n) => b.unit === "cost_usd" ? (n < 0.01 ? `$${n.toFixed(4)}` : formatCost(n)) : formatTokenCount(n);
|
|
143
152
|
const unit = b.unit === "cost_usd" ? "cost" : "tokens";
|
|
144
|
-
return
|
|
153
|
+
return `${unit} ${amount(b.spent)} of ${amount(b.limit)}`;
|
|
154
|
+
}
|
|
155
|
+
/** `budget exceeded: tokens 52k of 50k (run, at node gather)`. */
|
|
156
|
+
export function formatBudgetOverrun(b) {
|
|
157
|
+
return `budget exceeded: ${formatBudgetAmount(b)} (${b.scope}, at node ${b.node})`;
|
|
158
|
+
}
|
|
159
|
+
/** `agreed 4/5`, or `fell through 2/2` when no decision passed the gates. */
|
|
160
|
+
export function formatDeciderCounts(d) {
|
|
161
|
+
return d.compared > 0 ? `agreed ${d.agreed}/${d.compared}` : `fell through ${d.fellThrough}/${d.fellThrough}`;
|
|
145
162
|
}
|
|
146
163
|
/**
|
|
147
164
|
* One plain line: `✓ 3/3 nodes · 41 tool calls · 2m10s · 12k tokens · $0.18`.
|
|
@@ -162,28 +179,19 @@ export function formatReceipt(s) {
|
|
|
162
179
|
...(s.degraded && s.degraded.length > 0 ? [`degraded: ${s.degraded.join(", ")}`] : []),
|
|
163
180
|
...(formatPolicySegment(s.policy) ? [formatPolicySegment(s.policy)] : []),
|
|
164
181
|
...(s.budget ? [formatBudgetOverrun(s.budget)] : []),
|
|
165
|
-
...(s.decider
|
|
166
|
-
? [
|
|
167
|
-
s.decider.compared > 0
|
|
168
|
-
? `decider agreed ${s.decider.agreed}/${s.decider.compared}`
|
|
169
|
-
: `decider fell through ${s.decider.fellThrough}/${s.decider.fellThrough}`,
|
|
170
|
-
]
|
|
171
|
-
: []),
|
|
182
|
+
...(s.decider ? [`decider ${formatDeciderCounts(s.decider)}`] : []),
|
|
172
183
|
];
|
|
173
184
|
return parts.join(" · ");
|
|
174
185
|
}
|
|
175
|
-
/** Receipt for a terminal:
|
|
186
|
+
/** Receipt for a terminal: in the success or error color only when `color` is true. */
|
|
176
187
|
export function renderReceiptLine(s, color) {
|
|
177
188
|
const line = formatReceipt(s);
|
|
178
189
|
if (!color)
|
|
179
190
|
return line;
|
|
180
|
-
|
|
191
|
+
const paint = createPaint(true);
|
|
192
|
+
return s.ok ? paint.success(line) : paint.error(line);
|
|
181
193
|
}
|
|
182
194
|
// ── Step summary ────────────────────────────────────────────────
|
|
183
|
-
/** Brand blue (blue-600 / blue-700), not indigo. Applied to successful nodes. */
|
|
184
|
-
export const STEP_SUMMARY_CLASS_DEFS = {
|
|
185
|
-
success: "fill:#2563eb,stroke:#1d4ed8,color:#fff,stroke-width:2px",
|
|
186
|
-
};
|
|
187
195
|
/** Per-node Mermaid status from run results. */
|
|
188
196
|
export function nodeStates(results) {
|
|
189
197
|
const state = {};
|
|
@@ -192,7 +200,7 @@ export function nodeStates(results) {
|
|
|
192
200
|
}
|
|
193
201
|
return state;
|
|
194
202
|
}
|
|
195
|
-
/** Markdown for `$GITHUB_STEP_SUMMARY`: receipt +
|
|
203
|
+
/** Markdown for `$GITHUB_STEP_SUMMARY`: receipt + Mermaid DAG in the brand classDefs. */
|
|
196
204
|
export function formatStepSummary(workflow, results, summary, trace) {
|
|
197
205
|
const state = nodeStates(results);
|
|
198
206
|
return [
|
|
@@ -200,7 +208,7 @@ export function formatStepSummary(workflow, results, summary, trace) {
|
|
|
200
208
|
"",
|
|
201
209
|
`\`${formatReceipt(summary)}\``,
|
|
202
210
|
"",
|
|
203
|
-
toMermaidBlock(workflow, { state, trace
|
|
211
|
+
toMermaidBlock(workflow, { state, trace }),
|
|
204
212
|
"",
|
|
205
213
|
"",
|
|
206
214
|
].join("\n");
|
|
@@ -219,7 +227,7 @@ export function writeStepSummary(workflow, results, summary, trace, env = proces
|
|
|
219
227
|
return true;
|
|
220
228
|
}
|
|
221
229
|
catch (err) {
|
|
222
|
-
process.stderr.write(`
|
|
230
|
+
process.stderr.write(` ${glyphs().warning} could not write GITHUB_STEP_SUMMARY: ${err instanceof Error ? err.message : err}\n`);
|
|
223
231
|
return false;
|
|
224
232
|
}
|
|
225
233
|
}
|
|
@@ -234,6 +242,8 @@ export function createRunLogger(opts) {
|
|
|
234
242
|
const write = opts.write ?? ((s) => void process.stderr.write(s));
|
|
235
243
|
if (opts.verbose)
|
|
236
244
|
return { ...consoleLogger, flush() { } };
|
|
245
|
+
const paint = createPaint(opts.color ?? (opts.tty && colorEnabled(process.stderr)));
|
|
246
|
+
const g = glyphs();
|
|
237
247
|
const held = [];
|
|
238
248
|
const emit = (line) => {
|
|
239
249
|
if (opts.tty)
|
|
@@ -245,8 +255,8 @@ export function createRunLogger(opts) {
|
|
|
245
255
|
return {
|
|
246
256
|
info: noop,
|
|
247
257
|
debug: noop,
|
|
248
|
-
warn: (msg) => emit(` ${
|
|
249
|
-
error: (msg) => emit(` ${
|
|
258
|
+
warn: (msg) => emit(` ${paint.warning(g.warning)} ${msg.trim()}\n`),
|
|
259
|
+
error: (msg) => emit(` ${paint.error(g.failure)} ${msg.trim()}\n`),
|
|
250
260
|
flush() {
|
|
251
261
|
while (held.length)
|
|
252
262
|
write(held.shift());
|
package/dist/cli/runs.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* `sweny runs` and `sweny runs diff`: read-only views over `.sweny/runs/`.
|
|
3
3
|
* Metadata only. Color only when `color` is true (TTY).
|
|
4
4
|
*/
|
|
5
|
-
import chalk from "chalk";
|
|
6
5
|
import { findRun, listRuns } from "./run-history.js";
|
|
7
6
|
import { formatCost, formatReceiptDuration, formatTokenCount } from "./run-output.js";
|
|
7
|
+
import { createPaint } from "./style.js";
|
|
8
|
+
import { colorEnabled } from "./terminal.js";
|
|
9
|
+
import { GLYPHS } from "../theme.js";
|
|
8
10
|
// ── Small formatters ────────────────────────────────────────────
|
|
9
11
|
export function formatAge(startedAt, nowMs = Date.now()) {
|
|
10
12
|
const s = Math.max(0, Math.round((nowMs - Date.parse(startedAt)) / 1000));
|
|
@@ -18,15 +20,18 @@ export function formatAge(startedAt, nowMs = Date.now()) {
|
|
|
18
20
|
return `${h}h ago`;
|
|
19
21
|
return `${Math.floor(h / 24)}d ago`;
|
|
20
22
|
}
|
|
21
|
-
const
|
|
23
|
+
const G = GLYPHS.unicode;
|
|
24
|
+
const ICON = { success: G.success, failed: G.failure, crashed: G.failure, skipped: G.skipped };
|
|
25
|
+
/** Status text in its role color: success, error, or muted for skipped. */
|
|
22
26
|
function paint(text, status, color) {
|
|
23
27
|
if (!color)
|
|
24
28
|
return text;
|
|
29
|
+
const p = createPaint(true);
|
|
25
30
|
if (status === "success")
|
|
26
|
-
return
|
|
31
|
+
return p.success(text);
|
|
27
32
|
if (status === "skipped")
|
|
28
|
-
return
|
|
29
|
-
return
|
|
33
|
+
return p.muted(text);
|
|
34
|
+
return p.error(text);
|
|
30
35
|
}
|
|
31
36
|
const tokensText = (n) => (n === null ? "-" : formatTokenCount(n));
|
|
32
37
|
const costText = (n) => (n === null ? "-" : formatCost(n));
|
|
@@ -63,7 +68,8 @@ export function formatRunsTable(runs, color, nowMs = Date.now()) {
|
|
|
63
68
|
.join(" ")
|
|
64
69
|
.trimEnd();
|
|
65
70
|
const headLine = line(head);
|
|
66
|
-
return [color ?
|
|
71
|
+
return ([color ? createPaint(true).muted(headLine) : headLine, ...rows.map((r) => line(r.cells, r.status))].join("\n") +
|
|
72
|
+
"\n");
|
|
67
73
|
}
|
|
68
74
|
// ── sweny runs diff ─────────────────────────────────────────────
|
|
69
75
|
const sign = (n) => (n > 0 ? "+" : n < 0 ? "-" : "");
|
|
@@ -94,7 +100,7 @@ function nodeChanges(a, b) {
|
|
|
94
100
|
}
|
|
95
101
|
const routeKey = (r) => `${r.from} → ${r.to}`;
|
|
96
102
|
export function formatRunDiff(a, b, color) {
|
|
97
|
-
const dim = (
|
|
103
|
+
const dim = createPaint(color).muted;
|
|
98
104
|
const out = [];
|
|
99
105
|
const hdr = (tag, r) => ` ${tag} ${r.run_id} ${r.workflow_id} ${paint(`${ICON[r.status] ?? "?"} ${r.status}`, r.status, color)} ${formatReceiptDuration(r.duration_ms)}`;
|
|
100
106
|
out.push(hdr("a", a), hdr("b", b), "");
|
|
@@ -183,7 +189,7 @@ export function registerRunsCommand(program) {
|
|
|
183
189
|
.action((opts) => {
|
|
184
190
|
const limit = Number(opts.limit ?? "20");
|
|
185
191
|
if (!Number.isInteger(limit) || limit < 1) {
|
|
186
|
-
console.error(
|
|
192
|
+
console.error(createPaint(colorEnabled(process.stderr)).error(" --limit must be a positive integer"));
|
|
187
193
|
process.exitCode = 1;
|
|
188
194
|
return;
|
|
189
195
|
}
|
|
@@ -195,7 +201,7 @@ export function registerRunsCommand(program) {
|
|
|
195
201
|
process.stdout.write(JSON.stringify(runs, null, 2) + "\n");
|
|
196
202
|
return;
|
|
197
203
|
}
|
|
198
|
-
process.stdout.write("\n" + formatRunsTable(runs, process.stdout
|
|
204
|
+
process.stdout.write("\n" + formatRunsTable(runs, colorEnabled(process.stdout)) + "\n");
|
|
199
205
|
});
|
|
200
206
|
runsCmd
|
|
201
207
|
.command("diff [a] [b]")
|
|
@@ -206,11 +212,11 @@ export function registerRunsCommand(program) {
|
|
|
206
212
|
const runs = listRuns();
|
|
207
213
|
const picked = pickRunsForDiff(runs, refs, opts.workflow);
|
|
208
214
|
if ("error" in picked) {
|
|
209
|
-
console.error(
|
|
215
|
+
console.error(createPaint(colorEnabled(process.stderr)).error(` ${picked.error}`));
|
|
210
216
|
process.exitCode = 1;
|
|
211
217
|
return;
|
|
212
218
|
}
|
|
213
|
-
process.stdout.write("\n" + formatRunDiff(picked.a, picked.b, process.stdout
|
|
219
|
+
process.stdout.write("\n" + formatRunDiff(picked.a, picked.b, colorEnabled(process.stdout)) + "\n");
|
|
214
220
|
});
|
|
215
221
|
return runsCmd;
|
|
216
222
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal painters built from the theme tokens. The only place the CLI
|
|
3
|
+
* turns a color role into ANSI.
|
|
4
|
+
*
|
|
5
|
+
* `createPaint(false)` returns identity painters, so a formatter takes one
|
|
6
|
+
* `Paint` and renders the same text with or without color.
|
|
7
|
+
*/
|
|
8
|
+
export type Painter = (text: string) => string;
|
|
9
|
+
export interface Paint {
|
|
10
|
+
brand: Painter;
|
|
11
|
+
success: Painter;
|
|
12
|
+
warning: Painter;
|
|
13
|
+
error: Painter;
|
|
14
|
+
info: Painter;
|
|
15
|
+
muted: Painter;
|
|
16
|
+
/** Emphasis without a color: bold. */
|
|
17
|
+
strong: Painter;
|
|
18
|
+
link: Painter;
|
|
19
|
+
}
|
|
20
|
+
/** Painters for one output. `enabled: false` paints nothing. */
|
|
21
|
+
export declare function createPaint(enabled: boolean): Paint;
|
|
22
|
+
/**
|
|
23
|
+
* Honor `NO_COLOR` for every chalk call in the process, including the ones
|
|
24
|
+
* that do not go through `createPaint`. Call once at CLI startup.
|
|
25
|
+
*/
|
|
26
|
+
export declare function applyNoColor(env?: Record<string, string | undefined>): void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal painters built from the theme tokens. The only place the CLI
|
|
3
|
+
* turns a color role into ANSI.
|
|
4
|
+
*
|
|
5
|
+
* `createPaint(false)` returns identity painters, so a formatter takes one
|
|
6
|
+
* `Paint` and renders the same text with or without color.
|
|
7
|
+
*/
|
|
8
|
+
import chalk, { Chalk, chalkStderr } from "chalk";
|
|
9
|
+
import { ROLE_COLORS } from "../theme.js";
|
|
10
|
+
const identity = (s) => s;
|
|
11
|
+
/** Painters for one output. `enabled: false` paints nothing. */
|
|
12
|
+
export function createPaint(enabled) {
|
|
13
|
+
if (!enabled) {
|
|
14
|
+
return {
|
|
15
|
+
brand: identity,
|
|
16
|
+
success: identity,
|
|
17
|
+
warning: identity,
|
|
18
|
+
error: identity,
|
|
19
|
+
info: identity,
|
|
20
|
+
muted: identity,
|
|
21
|
+
strong: identity,
|
|
22
|
+
link: identity,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
// Force a level so a caller that decided "color on" gets color even when
|
|
26
|
+
// chalk's own detection looked at a different stream.
|
|
27
|
+
const k = new Chalk({ level: Math.max(chalk.level, chalkStderr.level, 1) });
|
|
28
|
+
return {
|
|
29
|
+
brand: (s) => k.hex(ROLE_COLORS.brand)(s),
|
|
30
|
+
success: (s) => k.hex(ROLE_COLORS.success)(s),
|
|
31
|
+
warning: (s) => k.hex(ROLE_COLORS.warning)(s),
|
|
32
|
+
error: (s) => k.hex(ROLE_COLORS.error)(s),
|
|
33
|
+
info: (s) => k.hex(ROLE_COLORS.info)(s),
|
|
34
|
+
muted: (s) => k.hex(ROLE_COLORS.muted)(s),
|
|
35
|
+
strong: (s) => k.bold(s),
|
|
36
|
+
link: (s) => k.hex(ROLE_COLORS.info).underline(s),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Honor `NO_COLOR` for every chalk call in the process, including the ones
|
|
41
|
+
* that do not go through `createPaint`. Call once at CLI startup.
|
|
42
|
+
*/
|
|
43
|
+
export function applyNoColor(env = process.env) {
|
|
44
|
+
if (env.NO_COLOR) {
|
|
45
|
+
chalk.level = 0;
|
|
46
|
+
chalkStderr.level = 0;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the terminal can show: color, Unicode, width, live redraws.
|
|
3
|
+
*
|
|
4
|
+
* Every check reads the env and stream passed in, so specs can pin each
|
|
5
|
+
* case. Defaults read the real process.
|
|
6
|
+
*/
|
|
7
|
+
import { type GlyphSet, type TicketBox } from "../theme.js";
|
|
8
|
+
type Env = Record<string, string | undefined>;
|
|
9
|
+
/** The narrowest layout SWEny designs for. Narrower terminals still work, they just wrap. */
|
|
10
|
+
export declare const MIN_COLUMNS = 40;
|
|
11
|
+
/**
|
|
12
|
+
* True when the terminal can draw box-drawing characters and the glyph set.
|
|
13
|
+
*
|
|
14
|
+
* `SWENY_ASCII=1` forces ASCII; `SWENY_UNICODE=1` forces Unicode. Otherwise a
|
|
15
|
+
* locale that names a charset decides (`LC_ALL`, then `LC_CTYPE`, then
|
|
16
|
+
* `LANG`): UTF-8 is Unicode, `C`/`POSIX`/anything else is ASCII. The Linux
|
|
17
|
+
* console (`TERM=linux`) is ASCII. With no locale set, POSIX terminals are
|
|
18
|
+
* assumed Unicode; Windows is Unicode only in terminals known to support it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function supportsUnicode(env?: Env, platform?: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* True when output to `stream` should carry ANSI color. `NO_COLOR` (any
|
|
23
|
+
* non-empty value) always wins; then `FORCE_COLOR` (anything but `0`); then a
|
|
24
|
+
* TTY that is not `TERM=dumb`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function colorEnabled(stream: {
|
|
27
|
+
isTTY?: boolean;
|
|
28
|
+
} | undefined, env?: Env): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* True when `stream` gets the rich view (the ticket, redraws, a spinner):
|
|
31
|
+
* a TTY, not CI, not dumb. Everything else gets plain lines.
|
|
32
|
+
*/
|
|
33
|
+
export declare function richOutput(stream: {
|
|
34
|
+
isTTY?: boolean;
|
|
35
|
+
} | undefined, env?: Env): boolean;
|
|
36
|
+
/** Usable width of `stream`: its column count, else `COLUMNS`, else 80. */
|
|
37
|
+
export declare function terminalColumns(stream: {
|
|
38
|
+
columns?: number;
|
|
39
|
+
} | undefined, env?: Env): number;
|
|
40
|
+
export declare function glyphsFor(unicode: boolean): GlyphSet;
|
|
41
|
+
export declare function ticketBoxFor(unicode: boolean): TicketBox;
|
|
42
|
+
export declare function spinnerFramesFor(unicode: boolean): readonly string[];
|
|
43
|
+
/** The glyphs for the current process. */
|
|
44
|
+
export declare function glyphs(env?: Env): GlyphSet;
|
|
45
|
+
/** Replace single-column Unicode drawing characters with their ASCII stand-ins. */
|
|
46
|
+
export declare function toAsciiDrawing(text: string): string;
|
|
47
|
+
export {};
|