@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
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
|
}
|
package/dist/cli/setup.js
CHANGED
|
@@ -12,17 +12,17 @@ const SIGNAL_LABELS = [
|
|
|
12
12
|
{
|
|
13
13
|
name: "agent-needs-input",
|
|
14
14
|
color: "CA8A04",
|
|
15
|
-
description: "Agent hit a decision point
|
|
15
|
+
description: "Agent hit a decision point - needs human clarification before it can proceed",
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
name: "agent-error",
|
|
19
19
|
color: "B91C1C",
|
|
20
|
-
description: "Unexpected technical failure during agent execution
|
|
20
|
+
description: "Unexpected technical failure during agent execution - needs human investigation",
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
name: "human-only",
|
|
24
24
|
color: "6B7280",
|
|
25
|
-
description: "Guard rail
|
|
25
|
+
description: "Guard rail - automation must not touch this issue or PR",
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
name: "needs-review",
|
|
@@ -52,7 +52,7 @@ const WORK_TYPE_LABELS = [
|
|
|
52
52
|
{
|
|
53
53
|
name: "research",
|
|
54
54
|
color: "D97706",
|
|
55
|
-
description: "Spike, investigation, or report by the agent
|
|
55
|
+
description: "Spike, investigation, or report by the agent - exploratory work without a direct code change",
|
|
56
56
|
isWorkType: true,
|
|
57
57
|
},
|
|
58
58
|
{
|
|
@@ -64,7 +64,7 @@ const WORK_TYPE_LABELS = [
|
|
|
64
64
|
{
|
|
65
65
|
name: "spec",
|
|
66
66
|
color: "BE185D",
|
|
67
|
-
description: "Spec generation
|
|
67
|
+
description: "Spec generation - agent converted non-technical input into a structured spec",
|
|
68
68
|
isWorkType: true,
|
|
69
69
|
},
|
|
70
70
|
{
|
|
@@ -77,7 +77,7 @@ const WORK_TYPE_LABELS = [
|
|
|
77
77
|
const AGENT_PARENT = {
|
|
78
78
|
name: "agent",
|
|
79
79
|
color: "7C3AED",
|
|
80
|
-
description: "Parent group
|
|
80
|
+
description: "Parent group - marks all autonomous agent work",
|
|
81
81
|
};
|
|
82
82
|
const BUG_LABEL = {
|
|
83
83
|
name: "bug",
|
|
@@ -254,7 +254,7 @@ ${chalk.cyan(` source-control-provider: github
|
|
|
254
254
|
pr-labels: agent,triage,needs-review
|
|
255
255
|
issue-labels: agent`)}
|
|
256
256
|
|
|
257
|
-
${chalk.dim(" GitHub labels are referenced by name
|
|
257
|
+
${chalk.dim(" GitHub labels are referenced by name - no UUIDs needed.")}
|
|
258
258
|
`);
|
|
259
259
|
}
|
|
260
260
|
// ── Command registration ───────────────────────────────────────────────────
|
|
@@ -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 {};
|
|
@@ -0,0 +1,123 @@
|
|
|
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 { GLYPHS, SPINNER_FRAMES, TICKET_BOX } from "../theme.js";
|
|
8
|
+
/** The narrowest layout SWEny designs for. Narrower terminals still work, they just wrap. */
|
|
9
|
+
export const MIN_COLUMNS = 40;
|
|
10
|
+
/**
|
|
11
|
+
* True when the terminal can draw box-drawing characters and the glyph set.
|
|
12
|
+
*
|
|
13
|
+
* `SWENY_ASCII=1` forces ASCII; `SWENY_UNICODE=1` forces Unicode. Otherwise a
|
|
14
|
+
* locale that names a charset decides (`LC_ALL`, then `LC_CTYPE`, then
|
|
15
|
+
* `LANG`): UTF-8 is Unicode, `C`/`POSIX`/anything else is ASCII. The Linux
|
|
16
|
+
* console (`TERM=linux`) is ASCII. With no locale set, POSIX terminals are
|
|
17
|
+
* assumed Unicode; Windows is Unicode only in terminals known to support it.
|
|
18
|
+
*/
|
|
19
|
+
export function supportsUnicode(env = process.env, platform = process.platform) {
|
|
20
|
+
if (truthy(env.SWENY_ASCII))
|
|
21
|
+
return false;
|
|
22
|
+
if (truthy(env.SWENY_UNICODE))
|
|
23
|
+
return true;
|
|
24
|
+
if (env.TERM === "linux")
|
|
25
|
+
return false;
|
|
26
|
+
const locale = env.LC_ALL || env.LC_CTYPE || env.LANG;
|
|
27
|
+
if (locale)
|
|
28
|
+
return /utf-?8/i.test(locale);
|
|
29
|
+
if (platform !== "win32")
|
|
30
|
+
return true;
|
|
31
|
+
return Boolean(env.WT_SESSION ||
|
|
32
|
+
env.TERMINUS_SUBLIME ||
|
|
33
|
+
env.ConEmuTask === "{cmd::Cmder}" ||
|
|
34
|
+
env.TERM_PROGRAM === "vscode" ||
|
|
35
|
+
env.TERM === "xterm-256color" ||
|
|
36
|
+
env.TERM === "alacritty" ||
|
|
37
|
+
env.TERMINAL_EMULATOR === "JetBrains-JediTerm");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* True when output to `stream` should carry ANSI color. `NO_COLOR` (any
|
|
41
|
+
* non-empty value) always wins; then `FORCE_COLOR` (anything but `0`); then a
|
|
42
|
+
* TTY that is not `TERM=dumb`.
|
|
43
|
+
*/
|
|
44
|
+
export function colorEnabled(stream, env = process.env) {
|
|
45
|
+
if (env.NO_COLOR)
|
|
46
|
+
return false;
|
|
47
|
+
if (env.FORCE_COLOR !== undefined && env.FORCE_COLOR !== "")
|
|
48
|
+
return env.FORCE_COLOR !== "0";
|
|
49
|
+
if (env.TERM === "dumb")
|
|
50
|
+
return false;
|
|
51
|
+
return Boolean(stream?.isTTY);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* True when `stream` gets the rich view (the ticket, redraws, a spinner):
|
|
55
|
+
* a TTY, not CI, not dumb. Everything else gets plain lines.
|
|
56
|
+
*/
|
|
57
|
+
export function richOutput(stream, env = process.env) {
|
|
58
|
+
return Boolean(stream?.isTTY) && !truthy(env.CI) && env.TERM !== "dumb";
|
|
59
|
+
}
|
|
60
|
+
/** Usable width of `stream`: its column count, else `COLUMNS`, else 80. */
|
|
61
|
+
export function terminalColumns(stream, env = process.env) {
|
|
62
|
+
const fromStream = stream?.columns;
|
|
63
|
+
if (typeof fromStream === "number" && fromStream > 0)
|
|
64
|
+
return fromStream;
|
|
65
|
+
const fromEnv = Number(env.COLUMNS);
|
|
66
|
+
if (Number.isInteger(fromEnv) && fromEnv > 0)
|
|
67
|
+
return fromEnv;
|
|
68
|
+
return 80;
|
|
69
|
+
}
|
|
70
|
+
export function glyphsFor(unicode) {
|
|
71
|
+
return unicode ? GLYPHS.unicode : GLYPHS.ascii;
|
|
72
|
+
}
|
|
73
|
+
export function ticketBoxFor(unicode) {
|
|
74
|
+
return unicode ? TICKET_BOX.unicode : TICKET_BOX.ascii;
|
|
75
|
+
}
|
|
76
|
+
export function spinnerFramesFor(unicode) {
|
|
77
|
+
return unicode ? SPINNER_FRAMES.unicode : SPINNER_FRAMES.ascii;
|
|
78
|
+
}
|
|
79
|
+
/** The glyphs for the current process. */
|
|
80
|
+
export function glyphs(env = process.env) {
|
|
81
|
+
return glyphsFor(supportsUnicode(env));
|
|
82
|
+
}
|
|
83
|
+
function truthy(v) {
|
|
84
|
+
return v !== undefined && v !== "" && v !== "0" && v.toLowerCase() !== "false";
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Box-drawing and glyph fallbacks for text drawn with Unicode (the DAG
|
|
88
|
+
* renderer). Each maps to a single ASCII column so alignment holds.
|
|
89
|
+
*/
|
|
90
|
+
const ASCII_FALLBACK = {
|
|
91
|
+
"┌": "+",
|
|
92
|
+
"┐": "+",
|
|
93
|
+
"└": "+",
|
|
94
|
+
"┘": "+",
|
|
95
|
+
"├": "+",
|
|
96
|
+
"┤": "+",
|
|
97
|
+
"┬": "+",
|
|
98
|
+
"┴": "+",
|
|
99
|
+
"┼": "+",
|
|
100
|
+
"╭": "+",
|
|
101
|
+
"╮": "+",
|
|
102
|
+
"╰": "+",
|
|
103
|
+
"╯": "+",
|
|
104
|
+
"─": "-",
|
|
105
|
+
"┄": "-",
|
|
106
|
+
"╌": "-",
|
|
107
|
+
"│": "|",
|
|
108
|
+
"┆": ":",
|
|
109
|
+
"▼": "v",
|
|
110
|
+
"▶": ">",
|
|
111
|
+
[GLYPHS.unicode.success]: GLYPHS.ascii.success,
|
|
112
|
+
[GLYPHS.unicode.failure]: GLYPHS.ascii.failure,
|
|
113
|
+
[GLYPHS.unicode.skipped]: GLYPHS.ascii.skipped,
|
|
114
|
+
[GLYPHS.unicode.pending]: GLYPHS.ascii.pending,
|
|
115
|
+
[GLYPHS.unicode.running]: GLYPHS.ascii.running,
|
|
116
|
+
[GLYPHS.unicode.brand]: GLYPHS.ascii.brand,
|
|
117
|
+
[GLYPHS.unicode.sep]: GLYPHS.ascii.sep,
|
|
118
|
+
};
|
|
119
|
+
const ASCII_FALLBACK_RE = new RegExp(`[${Object.keys(ASCII_FALLBACK).join("")}]`, "g");
|
|
120
|
+
/** Replace single-column Unicode drawing characters with their ASCII stand-ins. */
|
|
121
|
+
export function toAsciiDrawing(text) {
|
|
122
|
+
return text.replace(ASCII_FALLBACK_RE, (ch) => ASCII_FALLBACK[ch] ?? ch);
|
|
123
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The receipt ticket: the run receipt as a small perforated card with a tear
|
|
3
|
+
* line and a policy stamp. Shown at the end of a run on a TTY, and at the top
|
|
4
|
+
* of the PR comment. CI logs, pipes and `--json` keep the plain receipt line.
|
|
5
|
+
*
|
|
6
|
+
* METADATA ONLY, like the receipt line: counts, durations, tokens, cost,
|
|
7
|
+
* policy facts. Never prompts, node outputs, tool inputs, or model prose.
|
|
8
|
+
*/
|
|
9
|
+
import type { RunSummary } from "./run-output.js";
|
|
10
|
+
import { type Paint } from "./style.js";
|
|
11
|
+
/** Widest ticket, in columns, frame included. */
|
|
12
|
+
export declare const TICKET_MAX_WIDTH = 48;
|
|
13
|
+
/** Narrowest ticket. Below this the fields stop being readable. */
|
|
14
|
+
export declare const TICKET_MIN_WIDTH = 32;
|
|
15
|
+
/** Left indent the CLI prints the ticket at. */
|
|
16
|
+
export declare const TICKET_INDENT = 2;
|
|
17
|
+
/** How long the stub sits empty before the stamp lands (the one animation frame). */
|
|
18
|
+
export declare const STAMP_DELAY_MS = 140;
|
|
19
|
+
export interface TicketData {
|
|
20
|
+
summary: RunSummary;
|
|
21
|
+
/** Workflow id, shown under the status. */
|
|
22
|
+
workflow: string;
|
|
23
|
+
/** Full run id; the ticket shows its short hash. */
|
|
24
|
+
runId?: string;
|
|
25
|
+
/** The run crashed before finishing (not a node failure). */
|
|
26
|
+
crashed?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface TicketRenderOptions {
|
|
29
|
+
/** Columns available, indent included. Default 80. The ticket fits in `columns - 2`. */
|
|
30
|
+
columns?: number;
|
|
31
|
+
/** Box drawing and glyphs in Unicode (default) or ASCII. */
|
|
32
|
+
unicode?: boolean;
|
|
33
|
+
/** Painters. Default: no color. */
|
|
34
|
+
paint?: Paint;
|
|
35
|
+
/** Draw the policy stamp. False leaves the stub empty: the frame before the stamp lands. Default true. */
|
|
36
|
+
stamp?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface RenderedTicket {
|
|
39
|
+
/** The ticket, one string per row, no indent. */
|
|
40
|
+
lines: string[];
|
|
41
|
+
/** Index of the stamp row in `lines`, or -1 when the run reported no policy facts. */
|
|
42
|
+
stampRow: number;
|
|
43
|
+
}
|
|
44
|
+
/** `20260930-110000-3f9a2c` shows as `3f9a2c`; any other id as its first 8 characters. */
|
|
45
|
+
export declare function shortRunId(runId: string): string;
|
|
46
|
+
/** The ticket's status word. */
|
|
47
|
+
export declare function ticketStatus(data: TicketData): "passed" | "failed" | "budget stop" | "crashed";
|
|
48
|
+
/**
|
|
49
|
+
* The policy stamp words, from what the run reported: `ENV SCOPED`,
|
|
50
|
+
* `SANDBOXED`. Empty when no node reported env scope or sandbox facts.
|
|
51
|
+
* `strong` is false when any fact is weaker than the default (an unscoped
|
|
52
|
+
* env, no sandbox), which the stamp shows in the warning color.
|
|
53
|
+
*/
|
|
54
|
+
export declare function stampWords(summary: RunSummary): {
|
|
55
|
+
words: string[];
|
|
56
|
+
strong: boolean;
|
|
57
|
+
};
|
|
58
|
+
/** Ticket width for a terminal of `columns`: fits beside the indent, within the min and max. */
|
|
59
|
+
export declare function ticketWidth(columns: number): number;
|
|
60
|
+
/**
|
|
61
|
+
* Render the ticket. Pure: same input, same rows. Every row is exactly the
|
|
62
|
+
* ticket width in visible columns.
|
|
63
|
+
*/
|
|
64
|
+
export declare function renderTicket(data: TicketData, opts?: TicketRenderOptions): RenderedTicket;
|
|
65
|
+
/** The ticket as plain text (no color, Unicode): what the PR comment carries. */
|
|
66
|
+
export declare function formatTicketText(data: TicketData, columns?: number): string;
|
|
67
|
+
export interface ReceiptStream {
|
|
68
|
+
write(s: string): unknown;
|
|
69
|
+
isTTY?: boolean;
|
|
70
|
+
columns?: number;
|
|
71
|
+
}
|
|
72
|
+
export interface WriteReceiptOptions extends Omit<TicketData, "summary"> {
|
|
73
|
+
stream: ReceiptStream;
|
|
74
|
+
env?: Record<string, string | undefined>;
|
|
75
|
+
/** Land the stamp one frame after the ticket. Default true on a rich stream. */
|
|
76
|
+
animate?: boolean;
|
|
77
|
+
/** Override the rich (ticket) or plain (one line) choice. Default: `richOutput(stream, env)`. */
|
|
78
|
+
rich?: boolean;
|
|
79
|
+
/** Override color. Default: `colorEnabled(stream, env)`. */
|
|
80
|
+
color?: boolean;
|
|
81
|
+
/** Override Unicode. Default: `supportsUnicode(env)`. */
|
|
82
|
+
unicode?: boolean;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Print the receipt. A TTY (not CI) gets the ticket, its stamp landing one
|
|
86
|
+
* frame after the card; anything else gets the plain line. Both end with a
|
|
87
|
+
* blank line.
|
|
88
|
+
*/
|
|
89
|
+
export declare function writeReceipt(summary: RunSummary, opts: WriteReceiptOptions): Promise<void>;
|