@krapht/ork 1.0.0-alpha.10 → 1.0.0-alpha.12
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/{__build_info__-DFF18K8y.mjs → __build_info__-BJbE3Zvp.mjs} +4 -72
- package/dist/attach-BmV5ckb9.mjs +89 -0
- package/dist/bin.mjs +467 -864
- package/dist/{decide-3l9q8UtX.mjs → builtin-files-w0YULJ4c.mjs} +2 -50
- package/dist/chips-CyeB_rhR.mjs +43 -0
- package/dist/{clean-DVaEH9e7.mjs → clean-D8GI_2K-.mjs} +1 -1
- package/dist/{completion-Dv5kf598.mjs → completion-CzElB8nS.mjs} +24 -16
- package/dist/{context-CEaKOVmG.mjs → context-BAwgVSX5.mjs} +43 -31
- package/dist/decide-DlbyctOE.mjs +50 -0
- package/dist/{find-DEjjOlTG.mjs → find-p05wlbmu.mjs} +1 -1
- package/dist/format-Dni98cbh.mjs +206 -0
- package/dist/{help-groups-BwYoAWsh.mjs → help-groups-DYP_zMRP.mjs} +4 -0
- package/dist/{in-a-run-BS9Nc-JW.mjs → in-a-run-BI-hLIl_.mjs} +4 -2
- package/dist/{limits-BSwEjQST.mjs → limits-CElmhowV.mjs} +1 -1
- package/dist/{list-Qxr6rwsY.mjs → list-zW2xKuGr.mjs} +1 -1
- package/dist/{mcp-CslnG5wf.mjs → mcp-CM3b65AZ.mjs} +6 -6
- package/dist/output-CnRzrbUM.mjs +423 -0
- package/dist/{pause-D7OVB5Ac.mjs → pause-BuS83WAI.mjs} +4 -4
- package/dist/{relay-EceRIebO.mjs → relay-DAsJA8wF.mjs} +2 -2
- package/dist/{remove-0IQZVWmI.mjs → remove-Dh7D_hVn.mjs} +3 -3
- package/dist/{report-DK4LGqwF.mjs → report-CEN8VoUb.mjs} +31 -20
- package/dist/{send-C0uwfUYi.mjs → send-MFEQZH31.mjs} +3 -3
- package/dist/{src-cmmXKHn1.mjs → src-BfxkJOYm.mjs} +234 -150
- package/dist/{src-Df6KOIDC.mjs → src-CdDTQLdQ.mjs} +2081 -1035
- package/dist/{src-oaJYIddp.mjs → src-D1QedD5y.mjs} +523 -81
- package/dist/{src-i5k2Y-kf.mjs → src-S9yK2OHU.mjs} +79 -11
- package/dist/store-B2OaE4ID.mjs +1356 -0
- package/dist/ui/assets/{app-qs8Lofv6.js → app-CAoZtku5.js} +16 -16
- package/dist/ui/assets/app-QoHVi-mb.css +1 -0
- package/dist/ui/assets/{index-DV1ZaOk-.js → index-BL7Ad0OA.js} +2 -2
- package/dist/ui/index.html +1 -1
- package/dist/{ui-CBkHJdqk.mjs → ui-C4Ys26oO.mjs} +4 -4
- package/dist/{usage-Cd8pEYyK.mjs → usage-C2zx-X6q.mjs} +43 -32
- package/dist/verdict-CIy8tAyX.mjs +84 -0
- package/dist/{watch-DW5R2OPO.mjs → watch-DzCeqM05.mjs} +28 -16
- package/dist/{workflows-BT1gDRIV.mjs → workflows-Rr5w90A0.mjs} +3 -3
- package/dist/{worktrees-g1MQMGie.mjs → worktrees-f6SAbum5.mjs} +11 -10
- package/package.json +1 -1
- package/dist/format-Cb5_m-Qd.mjs +0 -191
- package/dist/output-Bqljtmp_.mjs +0 -40
- package/dist/ui/assets/app-C8uhHVYp.css +0 -1
package/dist/ui/index.html
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
href="/favicon.svg"
|
|
20
20
|
type="image/svg+xml"
|
|
21
21
|
/>
|
|
22
|
-
<script type="module" crossorigin src="/assets/index-
|
|
22
|
+
<script type="module" crossorigin src="/assets/index-BL7Ad0OA.js"></script>
|
|
23
23
|
<link rel="modulepreload" crossorigin href="/assets/theme-CdXvqTQ6.js">
|
|
24
24
|
<link rel="stylesheet" crossorigin href="/assets/index-BPIbqRRb.css">
|
|
25
25
|
</head>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { n as ORK_VERSION, t as ORK_NAME } from "./__build_info__-
|
|
2
|
-
import { a as
|
|
3
|
-
import {
|
|
4
|
-
import { i as uiFilePath, l as newToken, n as startServer, r as runningServer, t as DEFAULT_PORT } from "./src-
|
|
1
|
+
import { n as ORK_VERSION, t as ORK_NAME } from "./__build_info__-BJbE3Zvp.mjs";
|
|
2
|
+
import { a as style, o as write, s as writeErr } from "./output-CnRzrbUM.mjs";
|
|
3
|
+
import { o as mayShowSignIn } from "./in-a-run-BI-hLIl_.mjs";
|
|
4
|
+
import { i as uiFilePath, l as newToken, n as startServer, r as runningServer, t as DEFAULT_PORT } from "./src-S9yK2OHU.mjs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { constants } from "node:os";
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as listOrkWorktrees, t as createdWorktrees } from "./list-
|
|
3
|
-
import { n as usageReport, t as USAGE_DIMENSIONS } from "./report-
|
|
4
|
-
import { a as
|
|
5
|
-
import "./
|
|
6
|
-
import { n as limitsHere } from "./limits-
|
|
1
|
+
import { C as openStore, Un as runDirOf, Vn as placeFor, ot as usd, xt as liveStatus } from "./src-CdDTQLdQ.mjs";
|
|
2
|
+
import { n as listOrkWorktrees, t as createdWorktrees } from "./list-zW2xKuGr.mjs";
|
|
3
|
+
import { n as usageReport, t as USAGE_DIMENSIONS } from "./report-CEN8VoUb.mjs";
|
|
4
|
+
import { a as style, n as cost, o as write, s as writeErr, t as confirm, x as termOf } from "./output-CnRzrbUM.mjs";
|
|
5
|
+
import { n as chip, t as CHIP_LABELS } from "./chips-CyeB_rhR.mjs";
|
|
6
|
+
import { n as limitsHere } from "./limits-CElmhowV.mjs";
|
|
7
|
+
import { a as header, o as table } from "./format-Dni98cbh.mjs";
|
|
7
8
|
import { existsSync, rmSync } from "node:fs";
|
|
8
9
|
import { defineCommand } from "citty";
|
|
9
10
|
// ../core/src/store/prune.ts
|
|
@@ -197,24 +198,31 @@ const TITLES = {
|
|
|
197
198
|
driver: "By driver",
|
|
198
199
|
project: "By project"
|
|
199
200
|
};
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
201
|
+
/** Seconds of CPU, `12.3 s`; empty with none. */
|
|
202
|
+
const cpuText = (ms) => ms === 0 ? "" : `${(ms / 1e3).toFixed(1)} s`;
|
|
203
|
+
/** `cpu`: a column for the CPU Ork's tool commands used. */
|
|
204
|
+
const usageTable = (term, groups, cpu = false) => table(term, [
|
|
205
|
+
{ flex: true },
|
|
206
|
+
{ align: "right" },
|
|
207
|
+
{ align: "right" },
|
|
208
|
+
{},
|
|
209
|
+
{},
|
|
210
|
+
...cpu ? [{ align: "right" }] : []
|
|
211
|
+
], [[
|
|
212
|
+
header(""),
|
|
213
|
+
header("COST"),
|
|
214
|
+
header("SESSIONS"),
|
|
215
|
+
header("TOKENS"),
|
|
216
|
+
header("CACHE"),
|
|
217
|
+
...cpu ? [header("TOOL CPU")] : []
|
|
218
|
+
], ...groups.map((group) => [
|
|
219
|
+
{ text: group.key },
|
|
220
|
+
{ text: cost(group.costUsd) },
|
|
221
|
+
{ text: String(group.sessions) },
|
|
222
|
+
{ text: `${tokens(group.inputTokens + group.cacheReadTokens + group.cacheWriteTokens)} in / ${tokens(group.outputTokens)} out` },
|
|
223
|
+
{ text: percent(group.cacheHitRate) },
|
|
224
|
+
...cpu ? [{ text: cpuText(group.cpuMs) }] : []
|
|
225
|
+
])], " ");
|
|
218
226
|
/** What `ork usage` prints without `--by`. */
|
|
219
227
|
const OVERVIEW = [
|
|
220
228
|
"day",
|
|
@@ -228,29 +236,31 @@ const shortTime = (iso) => new Date(iso).toLocaleString(void 0, {
|
|
|
228
236
|
dateStyle: "medium",
|
|
229
237
|
timeStyle: "short"
|
|
230
238
|
});
|
|
239
|
+
/** A warning line: a `WARN` chip when rich, else the line in yellow. */
|
|
240
|
+
const warned = (term, text) => term.rich ? ` ${chip(term, CHIP_LABELS.warn, "warn")} ${text}` : style("yellow", ` ${text}`);
|
|
231
241
|
/** The limits across runs, above a report; nothing with none set. */
|
|
232
|
-
const formatLimits = ({ problems, caps, plan }) => {
|
|
242
|
+
const formatLimits = ({ problems, caps, plan }, term = termOf(process.stdout)) => {
|
|
233
243
|
if (problems.length === 0 && caps.length === 0 && plan.length === 0) return [];
|
|
234
244
|
return [
|
|
235
245
|
style("bold", "Limits"),
|
|
236
|
-
...problems.map((problem) =>
|
|
246
|
+
...problems.map((problem) => warned(term, problem)),
|
|
237
247
|
...caps.map((cap) => {
|
|
238
248
|
const line = ` ${cap.project === void 0 ? "all projects" : "this project"}, this ${cap.window}: ${usd(cap.spentUsd)} of ${usd(cap.capUsd)}, resets ${shortTime(cap.resetsAt)}`;
|
|
239
|
-
return cap.leftUsd <= 0 ?
|
|
249
|
+
return cap.leftUsd <= 0 ? warned(term, line.trimStart()) : line;
|
|
240
250
|
}),
|
|
241
|
-
...plan.map((limit) =>
|
|
251
|
+
...plan.map((limit) => warned(term, `Claude plan's ${limit.window.replaceAll("_", " ")} limit ${limit.status === "rejected" ? "was reached" : "is close"}${limit.resetsAt === void 0 ? "" : `, resets ${shortTime(limit.resetsAt)}`}`)),
|
|
242
252
|
""
|
|
243
253
|
];
|
|
244
254
|
};
|
|
245
255
|
/** The report, as the terminal shows it. */
|
|
246
|
-
const formatUsage = (report, scope, by) => {
|
|
256
|
+
const formatUsage = (report, scope, by, term = termOf(process.stdout)) => {
|
|
247
257
|
const { totals } = report;
|
|
248
258
|
if (totals.sessions === 0) return [`No Claude sessions in ${scope}.`];
|
|
249
259
|
const lines = [`${style("bold", cost(totals.costUsd))} in ${scope}`, `${counted(totals.runs, "run")} · ${counted(totals.sessions, "session")} · ${counted(totals.turns, "turn")} · ${tokens(totals.inputTokens + totals.cacheReadTokens + totals.cacheWriteTokens)} in / ${tokens(totals.outputTokens)} out · ${percent(totals.cacheHitRate)} from cache`];
|
|
250
260
|
for (const dimension of by === void 0 ? OVERVIEW : [by]) {
|
|
251
261
|
const groups = report.groups[dimension];
|
|
252
262
|
const shown = by === void 0 ? dimension === "day" ? groups.slice(-10) : groups.slice(0, OVERVIEW_ROWS) : groups;
|
|
253
|
-
lines.push("", style("bold", TITLES[dimension]), ...
|
|
263
|
+
lines.push("", style("bold", TITLES[dimension]), ...usageTable(term, shown, dimension === "node"));
|
|
254
264
|
if (shown.length < groups.length) lines.push(style("dim", ` and ${String(groups.length - shown.length)} more (--by ${dimension})`));
|
|
255
265
|
}
|
|
256
266
|
const hints = report.hints.length === 0 ? [] : [
|
|
@@ -333,12 +343,13 @@ const usage = defineCommand({
|
|
|
333
343
|
const store = openStore();
|
|
334
344
|
try {
|
|
335
345
|
if (args["clear-plan-limits"]) store.clearPlanLimits();
|
|
336
|
-
const
|
|
346
|
+
const filter = {
|
|
337
347
|
...args.all ? {} : { project: placeFor(process.cwd(), store.projects()).root },
|
|
338
348
|
...args.workflow === void 0 ? {} : { workflow: args.workflow },
|
|
339
349
|
...since === void 0 ? {} : { since },
|
|
340
350
|
...until === void 0 ? {} : { until }
|
|
341
|
-
}
|
|
351
|
+
};
|
|
352
|
+
const report = usageReport(store.usage(filter), store.toolCpu(filter));
|
|
342
353
|
const limits = limitsHere(store, args.all ? void 0 : placeFor(process.cwd(), store.projects()));
|
|
343
354
|
if (args.json) {
|
|
344
355
|
write(JSON.stringify({
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { d as apply, x as termOf } from "./output-CnRzrbUM.mjs";
|
|
2
|
+
import { n as chip, t as CHIP_LABELS } from "./chips-CyeB_rhR.mjs";
|
|
3
|
+
// src/run/view.ts
|
|
4
|
+
/**
|
|
5
|
+
* @file How a run's lines are drawn: the terminal kit's {@link Term} for the
|
|
6
|
+
* stream they go to, and what a transcript has to remember between events (a
|
|
7
|
+
* node's worktree, to show its paths relative to it). Plain, a view draws
|
|
8
|
+
* today's line-based transcript; rich, it draws chips and cuts to the width.
|
|
9
|
+
*/
|
|
10
|
+
/** A view for `stream` (stdout when not given), as `env` sets it. */
|
|
11
|
+
const createView = (stream = process.stdout, env = process.env) => ({
|
|
12
|
+
term: termOf(stream, env),
|
|
13
|
+
roots: /* @__PURE__ */ new Map()
|
|
14
|
+
});
|
|
15
|
+
/** Styles text for `term`: no escape codes when it is plain. */
|
|
16
|
+
const styler = (term) => (format, text) => apply(term, format, text);
|
|
17
|
+
// src/run/format-bus.ts
|
|
18
|
+
/** The first line of a message body, cut to fit one terminal line. */
|
|
19
|
+
const firstLine = (body) => {
|
|
20
|
+
const [line = ""] = body.trim().split("\n");
|
|
21
|
+
return line.length > 100 ? `${line.slice(0, 99)}…` : line;
|
|
22
|
+
};
|
|
23
|
+
/** Messages, owner questions and claims, without the node prefix. */
|
|
24
|
+
const formatBusEvent = (event, term = createView().term) => {
|
|
25
|
+
const style = styler(term);
|
|
26
|
+
if (event.type === "claim.taken") return [style("dim", `⚑ claimed ${event.resource}`)];
|
|
27
|
+
if (event.type === "claim.refused") {
|
|
28
|
+
const held = event.held === event.resource ? "" : ` (it holds ${event.held})`;
|
|
29
|
+
return [style("yellow", `⚑ ${event.resource} is ${event.holder}'s${held}`)];
|
|
30
|
+
}
|
|
31
|
+
if (event.type === "claim.released") return [style("dim", `⚑ released ${event.resource}`)];
|
|
32
|
+
if (event.type === "message.sent") {
|
|
33
|
+
const how = event.via === void 0 ? "" : ` (${event.via})`;
|
|
34
|
+
return [style("dim", `✉ to ${event.to}${how}: ${firstLine(event.body)}`)];
|
|
35
|
+
}
|
|
36
|
+
if (event.type === "message.received") {
|
|
37
|
+
const how = event.via === void 0 ? "" : ` (${event.via})`;
|
|
38
|
+
return [style("dim", `✉ from ${event.from}${how}: ${firstLine(event.body)}`)];
|
|
39
|
+
}
|
|
40
|
+
if (event.type === "call.started") return [style("dim", `↳ started ${event.workflow} as run ${event.run}`)];
|
|
41
|
+
if (event.type === "link.notice") return [style("dim", `↔ ${event.text}`)];
|
|
42
|
+
if (event.type === "message.read") return [];
|
|
43
|
+
if (event.type === "owner.asked") return [`${term.rich ? chip(term, CHIP_LABELS.ask, "accent") : style(["yellow", "bold"], "?")} asked as the owner of ${event.about}: ${event.text} ${style("dim", `(${event.question})`)}`];
|
|
44
|
+
if (event.type === "owner.answered") return [style("dim", `→ ${event.answer} (owner)`)];
|
|
45
|
+
return [style("dim", "→ its session ended first; asking you")];
|
|
46
|
+
};
|
|
47
|
+
// src/run/items-ran.ts
|
|
48
|
+
/**
|
|
49
|
+
* @file How a fan-out's finished line counts its items: only the ones that
|
|
50
|
+
* started.
|
|
51
|
+
*/
|
|
52
|
+
/** How many of a fan-out's items started; a skipped one never did. */
|
|
53
|
+
const itemsRan = (count, statuses) => {
|
|
54
|
+
const ran = Array.isArray(statuses) ? statuses.filter((status) => status !== "skipped").length : count;
|
|
55
|
+
return ran === count ? `${String(count)} ran` : `${String(ran)} of ${String(count)} ran`;
|
|
56
|
+
};
|
|
57
|
+
// src/run/verdict.ts
|
|
58
|
+
/**
|
|
59
|
+
* @file The one-line verdict of a finished node: its report's status, unless
|
|
60
|
+
* the node failed or its work did not merge.
|
|
61
|
+
*/
|
|
62
|
+
/** Whether a finished node's work did not merge into its owner. */
|
|
63
|
+
const hasConflict = (output) => {
|
|
64
|
+
const { merge } = output;
|
|
65
|
+
return typeof merge === "object" && merge !== null && "ok" in merge && merge.ok === false;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* A one-line verdict for a finished node, by what its type outputs. A node that
|
|
69
|
+
* failed reads `fail` even when its report said `pass` (a hook refused its
|
|
70
|
+
* commit); a child that did not merge says so.
|
|
71
|
+
*/
|
|
72
|
+
const nodeVerdict = (output, ok = true, error) => {
|
|
73
|
+
const { status, summary, exitCode, choice, answeredBy, count } = output;
|
|
74
|
+
if (typeof status === "string") {
|
|
75
|
+
if (!ok && (status === "pass" || status === "done")) return error === void 0 ? "fail" : `fail: ${firstLine(error)}`;
|
|
76
|
+
const label = hasConflict(output) ? `${status}, merge conflict` : status;
|
|
77
|
+
return typeof summary === "string" && summary !== "" ? `${label}: ${summary}` : label;
|
|
78
|
+
}
|
|
79
|
+
if (typeof exitCode === "number") return `exit ${String(exitCode)}`;
|
|
80
|
+
if (typeof choice === "string") return `${choice} (${String(answeredBy)})`;
|
|
81
|
+
if (typeof count === "number") return itemsRan(count, output["statuses"]);
|
|
82
|
+
return "done";
|
|
83
|
+
};
|
|
84
|
+
export { createView as a, formatBusEvent as i, nodeVerdict as n, styler as o, firstLine as r, hasConflict as t };
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as newRunId } from "./find-
|
|
3
|
-
import { a as
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
import { Bn as placeFiles, C as openStore, Ot as stillTheSameProcess, _ as readProjectTriggers, _r as isToolNode, b as watchingPid, et as detachRun, fr as CheckReportSchema, g as declaredTriggers, h as triggerStanding, m as createWatcher, p as triggersReport, xr as timeoutMs, xt as liveStatus, y as takeWatchLock } from "./src-CdDTQLdQ.mjs";
|
|
2
|
+
import { n as newRunId } from "./find-p05wlbmu.mjs";
|
|
3
|
+
import { S as cut, a as style, i as plain, o as write, s as writeErr, t as confirm, w as widthOf, x as termOf } from "./output-CnRzrbUM.mjs";
|
|
4
|
+
import { n as chip, t as CHIP_LABELS } from "./chips-CyeB_rhR.mjs";
|
|
5
|
+
import { c as toolExecutor, o as spawnTool, s as mcpCallExecutor } from "./src-BfxkJOYm.mjs";
|
|
6
|
+
import { i as runLimits, r as limitsRefusal } from "./limits-CElmhowV.mjs";
|
|
7
|
+
import { t as acceptRefusal } from "./in-a-run-BI-hLIl_.mjs";
|
|
7
8
|
import path from "node:path";
|
|
8
9
|
import { mkdirSync } from "node:fs";
|
|
9
10
|
import { homedir } from "node:os";
|
|
@@ -63,7 +64,7 @@ const lookAt = async (options) => {
|
|
|
63
64
|
});
|
|
64
65
|
return result.ok ? resultOf(result.output["json"]) : {
|
|
65
66
|
ok: false,
|
|
66
|
-
error: result.error ?? "the check failed"
|
|
67
|
+
error: [result.error ?? "the check failed", result.end?.detail].filter((part) => part !== void 0).join(": ")
|
|
67
68
|
};
|
|
68
69
|
};
|
|
69
70
|
// src/watch/report.ts
|
|
@@ -73,10 +74,21 @@ const STANDING = {
|
|
|
73
74
|
changed: "off: changed since it was accepted",
|
|
74
75
|
invalid: "off: has an error"
|
|
75
76
|
};
|
|
76
|
-
|
|
77
|
-
const
|
|
77
|
+
/** A failure's mark: a `FAIL` chip when rich, else the red `✗`. */
|
|
78
|
+
const failMark = (term) => term.rich ? chip(term, CHIP_LABELS.fail, "danger") : style("red", "✗");
|
|
79
|
+
/** A warning's mark: a `WARN` chip when rich, else the yellow `!`. */
|
|
80
|
+
const warnMark = (term) => term.rich ? chip(term, CHIP_LABELS.warn, "warn") : style("yellow", "!");
|
|
81
|
+
/** A trigger's standing, rich: a chip and what it adds. */
|
|
82
|
+
const standingChip = (term, standing) => {
|
|
83
|
+
const live = standing === "live";
|
|
84
|
+
const label = live ? "LIVE" : "OFF";
|
|
85
|
+
const text = STANDING[standing] ?? standing;
|
|
86
|
+
return `${chip(term, label, live ? "ok" : standing === "invalid" ? "danger" : "warn")}${live ? "" : ` ${text.replace(/^off: /u, "")}`}`;
|
|
87
|
+
};
|
|
88
|
+
const brokenLines = (broken, term = termOf(process.stdout)) => broken.map(({ file, message }) => `${failMark(term)} ${plain(file)}: ${plain(message)}`);
|
|
89
|
+
const statusLines = (report, term = termOf(process.stdout)) => {
|
|
78
90
|
const head = report.pid === void 0 ? "ork watch isn't running. Start it with `ork watch`, or `ork watch --detach`." : `ork watch is running (process ${String(report.pid)}).`;
|
|
79
|
-
const broken = brokenLines(report.broken);
|
|
91
|
+
const broken = brokenLines(report.broken, term);
|
|
80
92
|
if (report.triggers.length === 0) return [
|
|
81
93
|
head,
|
|
82
94
|
...broken,
|
|
@@ -86,11 +98,11 @@ const statusLines = (report) => {
|
|
|
86
98
|
head,
|
|
87
99
|
...broken,
|
|
88
100
|
...report.triggers.flatMap((status) => [
|
|
89
|
-
`${style("bold", plain(status.trigger))} ${style("dim", plain(`${status.project} → ${status.run}, every ${status.every}`))}`,
|
|
90
|
-
` ${STANDING[status.standing] ?? status.standing}${status.lookedAt === void 0 ? "" : `, last looked ${status.lookedAt}`}`,
|
|
101
|
+
term.rich ? `${style("bold", plain(status.trigger))} ${style("dim", cut(plain(`${status.project} → ${status.run}, every ${status.every}`), Math.max(8, term.width - widthOf(status.trigger) - 1)))}` : `${style("bold", plain(status.trigger))} ${style("dim", plain(`${status.project} → ${status.run}, every ${status.every}`))}`,
|
|
102
|
+
` ${term.rich ? standingChip(term, status.standing) : STANDING[status.standing] ?? status.standing}${status.lookedAt === void 0 ? "" : `, last looked ${status.lookedAt}`}`,
|
|
91
103
|
...status.error === void 0 ? [] : [` last look: ${plain(status.error)}`],
|
|
92
104
|
...status.held === void 0 ? [] : [` waiting: ${plain(status.held)}`],
|
|
93
|
-
...status.problems.map((problem) => ` ${
|
|
105
|
+
...status.problems.map((problem) => ` ${failMark(term)} ${plain(problem)}`)
|
|
94
106
|
])
|
|
95
107
|
];
|
|
96
108
|
};
|
|
@@ -126,16 +138,16 @@ const STANDS = {
|
|
|
126
138
|
new: "new",
|
|
127
139
|
changed: "changed since you accepted it, here or in the workflow it starts"
|
|
128
140
|
};
|
|
129
|
-
const acceptLines = (item, standing) => [
|
|
141
|
+
const acceptLines = (item, standing, term = termOf(process.stdout)) => [
|
|
130
142
|
`${style("bold", plain(item.id))} ${style("dim", plain(item.place.root))}`,
|
|
131
143
|
` ${STANDS[standing] ?? standing}`,
|
|
132
144
|
...checkLines(item),
|
|
133
145
|
` starts: ${plain(item.trigger.run)}, looking every ${item.trigger.every}, with up to ${String(item.trigger.maxRunsPerDay ?? 10)} runs a day`,
|
|
134
146
|
...workflowLines(item),
|
|
135
|
-
...item.diagnostics.filter(({ severity }) => severity === "warning").map(({ message }) => ` ${
|
|
147
|
+
...item.diagnostics.filter(({ severity }) => severity === "warning").map(({ message }) => ` ${warnMark(term)} ${plain(message)}`)
|
|
136
148
|
];
|
|
137
149
|
/** The triggers `--accept` can't offer, so they aren't silently left out. */
|
|
138
|
-
const unacceptableLines = (item) => [`${style("bold", plain(item.id))} ${style("dim", plain(item.place.root))}`, ...item.diagnostics.filter(({ severity }) => severity === "error").map(({ message }) => ` ${
|
|
150
|
+
const unacceptableLines = (item, term = termOf(process.stdout)) => [`${style("bold", plain(item.id))} ${style("dim", plain(item.place.root))}`, ...item.diagnostics.filter(({ severity }) => severity === "error").map(({ message }) => ` ${failMark(term)} ${plain(message)}`)];
|
|
139
151
|
// src/watch/start.ts
|
|
140
152
|
/**
|
|
141
153
|
* @file Starting a trigger's run (RFC-011): an ordinary run in the background,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { Bn as placeFiles, C as openStore, Kn as inside, Un as runDirOf, Vn as placeFor, w as storePath } from "./src-CdDTQLdQ.mjs";
|
|
2
|
+
import { a as isBuiltinArg, n as builtinWorkflowFile, t as builtinProblem } from "./builtin-files-w0YULJ4c.mjs";
|
|
3
|
+
import { s as writeErr } from "./output-CnRzrbUM.mjs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
6
6
|
// src/project/place.ts
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as planClean, i as keyOf, o as stepActs, t as applyClean } from "./clean-
|
|
3
|
-
import { a as readWorktrees, c as mergeStateOf, d as stateParts, l as planLine, n as lostWork, o as repoRoot, r as planRemove, s as labelOf, t as applyRemove } from "./remove-
|
|
4
|
-
import {
|
|
1
|
+
import { C as openStore, E as inspectProject } from "./src-CdDTQLdQ.mjs";
|
|
2
|
+
import { a as planClean, i as keyOf, o as stepActs, t as applyClean } from "./clean-D8GI_2K-.mjs";
|
|
3
|
+
import { a as readWorktrees, c as mergeStateOf, d as stateParts, l as planLine, n as lostWork, o as repoRoot, r as planRemove, s as labelOf, t as applyRemove } from "./remove-Dh7D_hVn.mjs";
|
|
4
|
+
import { S as cut, a as style, o as write, p as paint, s as writeErr, t as confirm, x as termOf } from "./output-CnRzrbUM.mjs";
|
|
5
|
+
import { n as chip } from "./chips-CyeB_rhR.mjs";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import { realpathSync } from "node:fs";
|
|
7
8
|
import { defineCommand } from "citty";
|
|
@@ -19,7 +20,7 @@ const goAhead = async (yes, verb, terminal = process.stdin.isTTY, ask = confirm)
|
|
|
19
20
|
process.exitCode = 1;
|
|
20
21
|
return false;
|
|
21
22
|
}
|
|
22
|
-
if (await ask("Go ahead?
|
|
23
|
+
if (await ask("Go ahead?")) return true;
|
|
23
24
|
write("Cancelled.");
|
|
24
25
|
return false;
|
|
25
26
|
};
|
|
@@ -32,18 +33,18 @@ const goAhead = async (yes, verb, terminal = process.stdin.isTTY, ask = confirm)
|
|
|
32
33
|
* them.
|
|
33
34
|
*/
|
|
34
35
|
/** One line of state: its merge state, then what clean would care about. */
|
|
35
|
-
const worktreeState = (item) => {
|
|
36
|
+
const worktreeState = (item, term = termOf(process.stdout)) => {
|
|
36
37
|
const merge = mergeStateOf(item);
|
|
37
|
-
const head = merge === "merged" ? [style("green", "merged")] : merge === "detached" ? ["detached"] : [];
|
|
38
|
-
const parts = stateParts(item).map((part) => part.risk ?
|
|
38
|
+
const head = merge === "merged" ? [term.rich ? chip(term, "MERGED", "ok") : style("green", "merged")] : merge === "detached" ? ["detached"] : [];
|
|
39
|
+
const parts = stateParts(item).map((part) => part.risk ? paint(term, "warn", part.text) : part.text);
|
|
39
40
|
return [...head, ...parts].join(" · ");
|
|
40
41
|
};
|
|
41
42
|
/** A name as the list shows it, saying when it's detached. */
|
|
42
43
|
const nameOf = (item) => mergeStateOf(item) === "detached" ? `${labelOf(item)} (detached)` : labelOf(item);
|
|
43
|
-
const formatWorktrees = (items) => {
|
|
44
|
+
const formatWorktrees = (items, term = termOf(process.stdout)) => {
|
|
44
45
|
if (items.length === 0) return ["No Ork worktrees or branches."];
|
|
45
46
|
const width = Math.max(...items.map((i) => nameOf(i).length));
|
|
46
|
-
return items.flatMap((item) => [`${style("bold", nameOf(item).padEnd(width))} ${worktreeState(item)}`, ...item.path === null ? [] : [` ${style("dim", item.path)}`]]);
|
|
47
|
+
return items.flatMap((item) => [`${style("bold", nameOf(item).padEnd(width))} ${worktreeState(item, term)}`, ...item.path === null ? [] : [` ${style("dim", term.rich ? cut(item.path, term.width - 2) : item.path)}`]]);
|
|
47
48
|
};
|
|
48
49
|
const formatPlan = (steps) => steps.map((step) => {
|
|
49
50
|
const { text, reason } = planLine(step);
|
package/package.json
CHANGED
package/dist/format-Cb5_m-Qd.mjs
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { Dt as isItem, bt as liveStatus } from "./src-Df6KOIDC.mjs";
|
|
2
|
-
import { i as style, n as cost } from "./output-Bqljtmp_.mjs";
|
|
3
|
-
// src/run/format-bus.ts
|
|
4
|
-
/** The first line of a message body, cut to fit one terminal line. */
|
|
5
|
-
const firstLine = (body) => {
|
|
6
|
-
const [line = ""] = body.trim().split("\n");
|
|
7
|
-
return line.length > 100 ? `${line.slice(0, 99)}…` : line;
|
|
8
|
-
};
|
|
9
|
-
/** Messages, owner questions and claims, without the node prefix. */
|
|
10
|
-
const formatBusEvent = (event) => {
|
|
11
|
-
if (event.type === "claim.taken") return [style("dim", `⚑ claimed ${event.resource}`)];
|
|
12
|
-
if (event.type === "claim.refused") {
|
|
13
|
-
const held = event.held === event.resource ? "" : ` (it holds ${event.held})`;
|
|
14
|
-
return [style("yellow", `⚑ ${event.resource} is ${event.holder}'s${held}`)];
|
|
15
|
-
}
|
|
16
|
-
if (event.type === "claim.released") return [style("dim", `⚑ released ${event.resource}`)];
|
|
17
|
-
if (event.type === "message.sent") {
|
|
18
|
-
const how = event.via === void 0 ? "" : ` (${event.via})`;
|
|
19
|
-
return [style("dim", `✉ to ${event.to}${how}: ${firstLine(event.body)}`)];
|
|
20
|
-
}
|
|
21
|
-
if (event.type === "message.received") {
|
|
22
|
-
const how = event.via === void 0 ? "" : ` (${event.via})`;
|
|
23
|
-
return [style("dim", `✉ from ${event.from}${how}: ${firstLine(event.body)}`)];
|
|
24
|
-
}
|
|
25
|
-
if (event.type === "call.started") return [style("dim", `↳ started ${event.workflow} as run ${event.run}`)];
|
|
26
|
-
if (event.type === "link.notice") return [style("dim", `↔ ${event.text}`)];
|
|
27
|
-
if (event.type === "message.read") return [];
|
|
28
|
-
if (event.type === "owner.asked") return [`${style(["yellow", "bold"], "?")} asked as the owner of ${event.about}: ${event.text} ${style("dim", `(${event.question})`)}`];
|
|
29
|
-
if (event.type === "owner.answered") return [style("dim", `→ ${event.answer} (owner)`)];
|
|
30
|
-
return [style("dim", "→ its session ended first; asking you")];
|
|
31
|
-
};
|
|
32
|
-
// src/run/items-ran.ts
|
|
33
|
-
/**
|
|
34
|
-
* @file How a fan-out's finished line counts its items: only the ones that
|
|
35
|
-
* started.
|
|
36
|
-
*/
|
|
37
|
-
/** How many of a fan-out's items started; a skipped one never did. */
|
|
38
|
-
const itemsRan = (count, statuses) => {
|
|
39
|
-
const ran = Array.isArray(statuses) ? statuses.filter((status) => status !== "skipped").length : count;
|
|
40
|
-
return ran === count ? `${String(count)} ran` : `${String(ran)} of ${String(count)} ran`;
|
|
41
|
-
};
|
|
42
|
-
// src/run/verdict.ts
|
|
43
|
-
/**
|
|
44
|
-
* @file The one-line verdict of a finished node: its report's status, unless
|
|
45
|
-
* the node failed or its work did not merge.
|
|
46
|
-
*/
|
|
47
|
-
/** Whether a finished node's work did not merge into its owner. */
|
|
48
|
-
const hasConflict = (output) => {
|
|
49
|
-
const { merge } = output;
|
|
50
|
-
return typeof merge === "object" && merge !== null && "ok" in merge && merge.ok === false;
|
|
51
|
-
};
|
|
52
|
-
/**
|
|
53
|
-
* A one-line verdict for a finished node, by what its type outputs. A node that
|
|
54
|
-
* failed reads `fail` even when its report said `pass` (a hook refused its
|
|
55
|
-
* commit); a child that did not merge says so.
|
|
56
|
-
*/
|
|
57
|
-
const nodeVerdict = (output, ok = true, error) => {
|
|
58
|
-
const { status, summary, exitCode, choice, answeredBy, count } = output;
|
|
59
|
-
if (typeof status === "string") {
|
|
60
|
-
if (!ok && (status === "pass" || status === "done")) return error === void 0 ? "fail" : `fail: ${firstLine(error)}`;
|
|
61
|
-
const label = hasConflict(output) ? `${status}, merge conflict` : status;
|
|
62
|
-
return typeof summary === "string" && summary !== "" ? `${label}: ${summary}` : label;
|
|
63
|
-
}
|
|
64
|
-
if (typeof exitCode === "number") return `exit ${String(exitCode)}`;
|
|
65
|
-
if (typeof choice === "string") return `${choice} (${String(answeredBy)})`;
|
|
66
|
-
if (typeof count === "number") return itemsRan(count, output["statuses"]);
|
|
67
|
-
return "done";
|
|
68
|
-
};
|
|
69
|
-
// src/runs/format.ts
|
|
70
|
-
/**
|
|
71
|
-
* @file How stored runs read on the terminal: the `ork ps` table and the `ork
|
|
72
|
-
* tree` ownership tree with each node's state, tokens and cost, and what is
|
|
73
|
-
* paused (ADR-041).
|
|
74
|
-
*/
|
|
75
|
-
const STATUS_STYLE = {
|
|
76
|
-
running: "cyan",
|
|
77
|
-
paused: "yellow",
|
|
78
|
-
succeeded: "green",
|
|
79
|
-
failed: "red",
|
|
80
|
-
crashed: "red",
|
|
81
|
-
budget_exhausted: "yellow",
|
|
82
|
-
quota_exhausted: "yellow",
|
|
83
|
-
timed_out: "yellow",
|
|
84
|
-
interrupted: "yellow"
|
|
85
|
-
};
|
|
86
|
-
/** `just now`, `5m ago`, `3h ago`, `2d ago`, then the date. */
|
|
87
|
-
const ago = (iso, now = Date.now()) => {
|
|
88
|
-
const minutes = Math.floor((now - Date.parse(iso)) / 6e4);
|
|
89
|
-
if (minutes < 1) return "just now";
|
|
90
|
-
if (minutes < 60) return `${String(minutes)}m ago`;
|
|
91
|
-
if (minutes < 1440) return `${String(Math.floor(minutes / 60))}h ago`;
|
|
92
|
-
if (minutes < 10080) return `${String(Math.floor(minutes / 1440))}d ago`;
|
|
93
|
-
return iso.slice(0, 10);
|
|
94
|
-
};
|
|
95
|
-
/** `42s`, `3m12s`, `1h05m`. */
|
|
96
|
-
const duration = (ms) => {
|
|
97
|
-
const s = Math.max(0, Math.round(ms / 1e3));
|
|
98
|
-
if (s < 60) return `${String(s)}s`;
|
|
99
|
-
if (s < 3600) return `${String(Math.floor(s / 60))}m${String(s % 60).padStart(2, "0")}s`;
|
|
100
|
-
return `${String(Math.floor(s / 3600))}h${String(Math.floor(s % 3600 / 60)).padStart(2, "0")}m`;
|
|
101
|
-
};
|
|
102
|
-
/**
|
|
103
|
-
* The `ork ps` table; `withProject` adds each run's project. A called run's
|
|
104
|
-
* cost is dimmed and says its caller's, whose cost already counts it.
|
|
105
|
-
*/
|
|
106
|
-
const formatRuns = (runs, withProject, now = Date.now(), statusOf = liveStatus) => {
|
|
107
|
-
if (runs.length === 0) return ["No runs."];
|
|
108
|
-
const rows = runs.map((run) => {
|
|
109
|
-
const status = statusOf(run);
|
|
110
|
-
const end = run.finishedAt === void 0 ? now : Date.parse(run.finishedAt);
|
|
111
|
-
return {
|
|
112
|
-
id: run.id,
|
|
113
|
-
workflow: run.workflow,
|
|
114
|
-
status,
|
|
115
|
-
when: `${ago(run.startedAt, now)} · ${duration(end - Date.parse(run.startedAt))}`,
|
|
116
|
-
cost: run.parent === void 0 ? cost(run.costUsd) : `${cost(run.costUsd)} in ${run.parent}`,
|
|
117
|
-
called: run.parent !== void 0,
|
|
118
|
-
project: run.project
|
|
119
|
-
};
|
|
120
|
-
});
|
|
121
|
-
const width = (pick, title) => Math.max(title.length, ...rows.map((r) => pick(r).length));
|
|
122
|
-
const w = {
|
|
123
|
-
id: width((r) => r.id, "RUN"),
|
|
124
|
-
workflow: width((r) => r.workflow, "WORKFLOW"),
|
|
125
|
-
status: width((r) => r.status, "STATUS"),
|
|
126
|
-
when: width((r) => r.when, "STARTED"),
|
|
127
|
-
cost: width((r) => r.cost, "COST")
|
|
128
|
-
};
|
|
129
|
-
const header = [
|
|
130
|
-
"RUN".padEnd(w.id),
|
|
131
|
-
"WORKFLOW".padEnd(w.workflow),
|
|
132
|
-
"STATUS".padEnd(w.status),
|
|
133
|
-
"STARTED".padEnd(w.when),
|
|
134
|
-
"COST".padStart(w.cost),
|
|
135
|
-
...withProject ? ["PROJECT"] : []
|
|
136
|
-
].join(" ");
|
|
137
|
-
return [style("dim", header), ...rows.map((r) => [
|
|
138
|
-
style("bold", r.id.padEnd(w.id)),
|
|
139
|
-
r.workflow.padEnd(w.workflow),
|
|
140
|
-
style(STATUS_STYLE[r.status] ?? "reset", r.status.padEnd(w.status)),
|
|
141
|
-
style("dim", r.when.padEnd(w.when)),
|
|
142
|
-
r.called ? style("dim", r.cost.padStart(w.cost)) : r.cost.padStart(w.cost),
|
|
143
|
-
...withProject ? [style("dim", r.project)] : []
|
|
144
|
-
].join(" "))];
|
|
145
|
-
};
|
|
146
|
-
const STATE_MARK = {
|
|
147
|
-
waiting: style("dim", "·"),
|
|
148
|
-
running: style("cyan", "●"),
|
|
149
|
-
ok: style("green", "✓"),
|
|
150
|
-
failed: style("red", "✗"),
|
|
151
|
-
skipped: style("dim", "-")
|
|
152
|
-
};
|
|
153
|
-
/** `950`, `12.3k`, `1.2M`. */
|
|
154
|
-
const count = (n) => {
|
|
155
|
-
if (n < 1e3) return String(n);
|
|
156
|
-
if (n < 1e6) return `${(n / 1e3).toFixed(1)}k`;
|
|
157
|
-
return `${(n / 1e6).toFixed(1)}M`;
|
|
158
|
-
};
|
|
159
|
-
const stateText = (node) => {
|
|
160
|
-
if (node.state === "running") return node.runs > 1 ? `running (round ${String(node.runs)})` : "running";
|
|
161
|
-
if (node.state === "ok" || node.state === "failed") {
|
|
162
|
-
const verdict = nodeVerdict(node.output ?? {}, node.state === "ok");
|
|
163
|
-
const line = verdict.length > 60 ? `${verdict.slice(0, 59)}…` : verdict;
|
|
164
|
-
return node.runs > 1 ? `${line} (${String(node.runs)} runs)` : line;
|
|
165
|
-
}
|
|
166
|
-
return node.state;
|
|
167
|
-
};
|
|
168
|
-
/** The ownership tree, one line per node, children under their owner. */
|
|
169
|
-
const formatTree = (roots, now = Date.now()) => {
|
|
170
|
-
const lines = [];
|
|
171
|
-
const walk = (node, lead, branch) => {
|
|
172
|
-
lines.push({
|
|
173
|
-
label: `${lead}${branch}${node.id}`,
|
|
174
|
-
node
|
|
175
|
-
});
|
|
176
|
-
const next = lead + (branch === "├─ " ? "│ " : branch === "" ? "" : " ");
|
|
177
|
-
node.children.forEach((child, index) => {
|
|
178
|
-
walk(child, next, index === node.children.length - 1 ? "└─ " : "├─ ");
|
|
179
|
-
});
|
|
180
|
-
};
|
|
181
|
-
for (const root of roots) walk(root, "", "");
|
|
182
|
-
const label = Math.max(...lines.map((l) => l.label.length));
|
|
183
|
-
const type = Math.max(...lines.map((l) => l.node.type.length));
|
|
184
|
-
return lines.map(({ label: text, node }) => {
|
|
185
|
-
const usage = node.inputTokens + node.outputTokens === 0 ? "" : style("dim", ` ${count(node.inputTokens)} in / ${count(node.outputTokens)} out · ${cost(node.costUsd)}`);
|
|
186
|
-
const state = node.held === true ? `${style("yellow", "‖")} paused` : `${STATE_MARK[node.state]} ${stateText(node)}`;
|
|
187
|
-
const spent = isItem(node) && node.startedAt !== void 0 ? style("dim", ` ${duration((node.finishedAt === void 0 ? now : Date.parse(node.finishedAt)) - Date.parse(node.startedAt))}`) : "";
|
|
188
|
-
return `${text.padEnd(label)} ${style("dim", node.type.padEnd(type))} ${state}${spent}${usage}`;
|
|
189
|
-
});
|
|
190
|
-
};
|
|
191
|
-
export { nodeVerdict as a, hasConflict as i, formatRuns as n, firstLine as o, formatTree as r, formatBusEvent as s, ago as t };
|
package/dist/output-Bqljtmp_.mjs
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { createInterface } from "node:readline/promises";
|
|
2
|
-
import { stripVTControlCharacters, styleText } from "node:util";
|
|
3
|
-
// src/output.ts
|
|
4
|
-
/**
|
|
5
|
-
* @file Terminal output helpers: styled text that turns plain when stdout is
|
|
6
|
-
* not a TTY or `NO_COLOR` is set, and a yes-or-no question on the terminal.
|
|
7
|
-
*/
|
|
8
|
-
/** Styles text for stdout; plain when stdout is not a TTY or NO_COLOR is set. */
|
|
9
|
-
const style = (format, text) => styleText(format, text, { stream: process.stdout });
|
|
10
|
-
/**
|
|
11
|
-
* Text from outside, fit for a terminal: escape sequences could hide a line on
|
|
12
|
-
* the very screen a person judges from, and a line break could forge one.
|
|
13
|
-
*/
|
|
14
|
-
const plain = (text) => stripVTControlCharacters(text).replaceAll(/[\p{Cc}\p{Bidi_Control}]/gu, " ");
|
|
15
|
-
const write = (text) => {
|
|
16
|
-
process.stdout.write(`${text}\n`);
|
|
17
|
-
};
|
|
18
|
-
const writeErr = (text) => {
|
|
19
|
-
process.stderr.write(`${text}\n`);
|
|
20
|
-
};
|
|
21
|
-
/** What a run spent: `$0.0312`, or `$0.00` for nothing. */
|
|
22
|
-
const cost = (usd) => usd === 0 ? "$0.00" : `$${usd.toFixed(4)}`;
|
|
23
|
-
/** Asks a yes-or-no question on the terminal; anything but yes is no. */
|
|
24
|
-
const confirm = async (question) => {
|
|
25
|
-
const rl = createInterface({
|
|
26
|
-
input: process.stdin,
|
|
27
|
-
output: process.stderr
|
|
28
|
-
});
|
|
29
|
-
const { promise: ended, resolve: end } = Promise.withResolvers();
|
|
30
|
-
rl.once("close", () => {
|
|
31
|
-
end("");
|
|
32
|
-
});
|
|
33
|
-
try {
|
|
34
|
-
const answer = await Promise.race([rl.question(question), ended]);
|
|
35
|
-
return ["y", "yes"].includes(answer.trim().toLowerCase());
|
|
36
|
-
} finally {
|
|
37
|
-
rl.close();
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
export { write as a, style as i, cost as n, writeErr as o, plain as r, confirm as t };
|