@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.
Files changed (64) hide show
  1. package/dist/agent-env.d.ts +2 -1
  2. package/dist/agent-env.js +9 -3
  3. package/dist/browser.d.ts +2 -0
  4. package/dist/browser.js +3 -0
  5. package/dist/budget.js +5 -1
  6. package/dist/cli/check.js +12 -12
  7. package/dist/cli/comment-output.d.ts +4 -2
  8. package/dist/cli/comment-output.js +16 -8
  9. package/dist/cli/config.js +24 -24
  10. package/dist/cli/e2e.js +17 -17
  11. package/dist/cli/main.js +42 -26
  12. package/dist/cli/marketplace.js +3 -3
  13. package/dist/cli/new.js +11 -8
  14. package/dist/cli/output.d.ts +3 -2
  15. package/dist/cli/output.js +39 -37
  16. package/dist/cli/progress.d.ts +33 -0
  17. package/dist/cli/progress.js +69 -0
  18. package/dist/cli/publish.js +2 -2
  19. package/dist/cli/record.d.ts +66 -0
  20. package/dist/cli/record.js +344 -0
  21. package/dist/cli/renderer.d.ts +4 -0
  22. package/dist/cli/renderer.js +20 -13
  23. package/dist/cli/run-output.d.ts +13 -6
  24. package/dist/cli/run-output.js +36 -26
  25. package/dist/cli/runs.js +17 -11
  26. package/dist/cli/setup.js +7 -7
  27. package/dist/cli/style.d.ts +26 -0
  28. package/dist/cli/style.js +48 -0
  29. package/dist/cli/terminal.d.ts +47 -0
  30. package/dist/cli/terminal.js +123 -0
  31. package/dist/cli/ticket.d.ts +89 -0
  32. package/dist/cli/ticket.js +218 -0
  33. package/dist/cli/try.d.ts +19 -8
  34. package/dist/cli/try.js +97 -65
  35. package/dist/cli/upgrade.js +1 -1
  36. package/dist/executor.d.ts +8 -1
  37. package/dist/executor.js +20 -2
  38. package/dist/git-credentials.d.ts +106 -0
  39. package/dist/git-credentials.js +322 -0
  40. package/dist/harness/acp.js +3 -0
  41. package/dist/harness/claude-code.d.ts +7 -0
  42. package/dist/harness/claude-code.js +44 -3
  43. package/dist/harness/codex.js +3 -0
  44. package/dist/harness/pi.js +3 -0
  45. package/dist/harness/policy.d.ts +5 -0
  46. package/dist/harness/policy.js +14 -0
  47. package/dist/harness/sandbox-wrapper.d.ts +9 -2
  48. package/dist/harness/sandbox-wrapper.js +14 -4
  49. package/dist/harness/types.d.ts +10 -0
  50. package/dist/index.d.ts +2 -0
  51. package/dist/index.js +2 -0
  52. package/dist/mermaid.d.ts +10 -5
  53. package/dist/mermaid.js +11 -13
  54. package/dist/safe-outputs.js +5 -0
  55. package/dist/skills/git-push.d.ts +62 -0
  56. package/dist/skills/git-push.js +132 -0
  57. package/dist/skills/github.d.ts +17 -0
  58. package/dist/skills/github.js +23 -0
  59. package/dist/theme.d.ts +185 -0
  60. package/dist/theme.js +183 -0
  61. package/dist/workflows/browser.js +2 -2
  62. package/dist/workflows/implement.yml +2 -1
  63. package/dist/workflows/triage.yml +2 -1
  64. package/package.json +6 -1
@@ -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
- * `policy: env scoped, sandbox on, 1 output staged`. Only what the run knew:
94
- * no segment for a fact no node reported. Undefined when there is nothing to say.
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 formatPolicySegment(p) {
97
+ export function policyParts(p) {
97
98
  if (!p)
98
- return undefined;
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
- /** `budget exceeded: tokens 52k of 50k (run, at node gather)`. */
141
- export function formatBudgetOverrun(b) {
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 `budget exceeded: ${unit} ${amount(b.spent)} of ${amount(b.limit)} (${b.scope}, at node ${b.node})`;
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: colored only when `color` is true. */
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
- return s.ok ? chalk.green(line) : chalk.red(line);
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 + status-colored Mermaid DAG. */
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, classDefs: STEP_SUMMARY_CLASS_DEFS }),
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(` ⚠ could not write GITHUB_STEP_SUMMARY: ${err instanceof Error ? err.message : err}\n`);
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(` ${chalk.yellow("⚠")} ${msg.trim()}\n`),
249
- error: (msg) => emit(` ${chalk.red("✗")} ${msg.trim()}\n`),
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 ICON = { success: "✓", failed: "✗", crashed: "✗", skipped: "−" };
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 chalk.green(text);
31
+ return p.success(text);
27
32
  if (status === "skipped")
28
- return chalk.dim(text);
29
- return chalk.red(text);
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 ? chalk.dim(headLine) : headLine, ...rows.map((r) => line(r.cells, r.status))].join("\n") + "\n";
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 = (s) => (color ? chalk.dim(s) : s);
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(chalk.red(" --limit must be a positive integer"));
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.isTTY ?? false) + "\n");
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(chalk.red(` ${picked.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.isTTY ?? false) + "\n");
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 — needs human clarification before it can proceed",
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 — needs human investigation",
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 — automation must not touch this issue or PR",
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 — exploratory work without a direct code change",
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 — agent converted non-technical input into a structured spec",
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 — marks all autonomous agent work",
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 — no UUIDs needed.")}
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>;