@krapht/ork 1.0.0-alpha.2 → 1.0.0-alpha.20

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 (71) hide show
  1. package/README.md +12 -7
  2. package/dist/__build_info__-D6mXC4cD.mjs +228 -0
  3. package/dist/apply-Mb0fNpCX.mjs +17 -0
  4. package/dist/argument-error-CihH46Le.mjs +27 -0
  5. package/dist/attach-C1OnxyMh.mjs +92 -0
  6. package/dist/attach-CEGbRg1D.mjs +1230 -0
  7. package/dist/bin.mjs +1312 -2390
  8. package/dist/brief-DH66xmj8.mjs +115 -0
  9. package/dist/{decide-CGyx9rv7.mjs → builtin-files-Cf-NZDqM.mjs} +3 -51
  10. package/dist/chips-BqcT58qr.mjs +43 -0
  11. package/dist/command-tree-CEkzWNy6.mjs +63 -0
  12. package/dist/{completion-CSU5vkEt.mjs → completion-DS0gCZRm.mjs} +200 -30
  13. package/dist/context-CGPh12HV.mjs +106 -0
  14. package/dist/detached-Dakw5RZj.mjs +38 -0
  15. package/dist/extra-arguments-BxQMXARO.mjs +13 -0
  16. package/dist/{find-D6Gj1sxg.mjs → find-BTikxc54.mjs} +2 -2
  17. package/dist/flags-C8yCq0j5.mjs +145 -0
  18. package/dist/follow-ups-CtlhHbwe.mjs +563 -0
  19. package/dist/format-BB4E-T6x.mjs +249 -0
  20. package/dist/{help-groups-BwYoAWsh.mjs → help-groups-BpRgBSTh.mjs} +42 -76
  21. package/dist/in-a-run-CbnKjO7t.mjs +27 -0
  22. package/dist/install-CWcpX5g4.mjs +211 -0
  23. package/dist/{limits-B1-DYc3d.mjs → limits-VQqsBoQk.mjs} +2 -2
  24. package/dist/{mcp-DE_EmyVm.mjs → mcp-wGfkMCVF.mjs} +6 -6
  25. package/dist/once-BHxSdZz_.mjs +112 -0
  26. package/dist/orphan-guard-BFKg8VSd.mjs +58 -0
  27. package/dist/output-B1kwo0YU.mjs +439 -0
  28. package/dist/outputs-text-CNTJ7Qq_.mjs +19 -0
  29. package/dist/{pause-CL6N_Xrj.mjs → pause-C_Wxh-KM.mjs} +7 -4
  30. package/dist/print-DNYzq4Rr.mjs +19 -0
  31. package/dist/project-records-XY3ku95x.mjs +233 -0
  32. package/dist/refusals-DQQX3AIb.mjs +116 -0
  33. package/dist/{relay-0SD0erwf.mjs → relay-BA6tsnpc.mjs} +2 -2
  34. package/dist/run-Bvvg-fiF.mjs +234 -0
  35. package/dist/run-DBUAq-Uu.mjs +1452 -0
  36. package/dist/send-nrKtak5F.mjs +105 -0
  37. package/dist/{src-Dujmvv4s.mjs → src-8cOPOgrT.mjs} +12523 -5507
  38. package/dist/{src-BbyEoMn4.mjs → src-CmEvHdju.mjs} +797 -233
  39. package/dist/src-D4JSymhK.mjs +3104 -0
  40. package/dist/{src-DOcK8Jfp.mjs → src-I_RyKu6_.mjs} +1022 -590
  41. package/dist/store-CDpk8Wtz.mjs +16 -0
  42. package/dist/ui/THIRD_PARTY_LICENSES.txt +612 -0
  43. package/dist/ui/assets/app--FijEXiJ.css +1 -0
  44. package/dist/ui/assets/app-BowhjEAe.js +112 -0
  45. package/dist/ui/assets/{index-9dFha9Qn.js → index-CrMa2oHB.js} +3 -3
  46. package/dist/ui/assets/theme-CdXvqTQ6.js +1 -0
  47. package/dist/ui/index.html +2 -2
  48. package/dist/{ui-CTmO2Xzu.mjs → ui-DY3Dnpho.mjs} +54 -25
  49. package/dist/usage-1VwSg_Qt.mjs +257 -0
  50. package/dist/validate-BxayjjkV.mjs +632 -0
  51. package/dist/verdict-CMvPCpWC.mjs +84 -0
  52. package/dist/{watch-CtbgDAcz.mjs → watch-CT0EtLPZ.mjs} +45 -17
  53. package/dist/{workflows-D1_WVukk.mjs → workflows-DHHi-APg.mjs} +30 -5
  54. package/dist/worktrees-CZvbHzbN.mjs +302 -0
  55. package/package.json +10 -8
  56. package/dist/__build_info__-fy6L1Mrd.mjs +0 -615
  57. package/dist/clean-DaHjs__2.mjs +0 -88
  58. package/dist/context-DT8MfEIR.mjs +0 -274
  59. package/dist/format-Dnfv8zMT.mjs +0 -427
  60. package/dist/in-a-run-pLAZeNys.mjs +0 -22
  61. package/dist/list-BrH8KRSJ.mjs +0 -110
  62. package/dist/output-BLQWD6MJ.mjs +0 -950
  63. package/dist/remove-BeGdqroy.mjs +0 -194
  64. package/dist/report-DK4LGqwF.mjs +0 -129
  65. package/dist/send-C8tunWhv.mjs +0 -163
  66. package/dist/src-CCUKRX51.mjs +0 -1204
  67. package/dist/ui/assets/app-BNT0SuIz.css +0 -1
  68. package/dist/ui/assets/app-T7wVAhgx.js +0 -67
  69. package/dist/ui/assets/theme-3xH5jxKs.js +0 -1
  70. package/dist/usage-DYUmP0i4.mjs +0 -419
  71. package/dist/worktrees-DdMYoNOC.mjs +0 -212
@@ -1,4 +1,4 @@
1
- import { a as limitsNow, i as NOT_STARTED, o as limitsReport, r as capsHere } from "./src-Dujmvv4s.mjs";
1
+ import { _t as limitsReport, gt as limitsNow, ht as NOT_STARTED, v as capsHere } from "./src-8cOPOgrT.mjs";
2
2
  // src/run/limits.ts
3
3
  /**
4
4
  * @file The limits across runs that apply here (RFC-007): the user's settings
@@ -26,7 +26,7 @@ const runLimits = (store, place, home) => {
26
26
  * Whether what is left of a workflow can spend: a tool-only one, or a resume
27
27
  * with only tools to go, is not held back by a quota.
28
28
  */
29
- const spendsAnything = (workflow, history) => Object.entries(workflow.nodes).some(([id, node]) => (node.type === "claude.leader" || node.type === "claude.worker" || node.type === "workflow.call") && history?.get(id)?.at(-1)?.ok !== true);
29
+ const spendsAnything = (workflow, history) => Object.entries(workflow.nodes).some(([id, node]) => (node.type === "agent.leader" || node.type === "agent.worker" || node.type === "workflow.call") && history?.get(id)?.at(-1)?.ok !== true);
30
30
  /** Why a run won't start: a settings file it can't read, or a spent limit. */
31
31
  const limitsRefusal = (limits, workflow, history) => {
32
32
  if (!spendsAnything(workflow, history)) return [];
@@ -1,8 +1,8 @@
1
- import { h as isNewer } from "./src-Dujmvv4s.mjs";
2
- import { n as ORK_VERSION } from "./__build_info__-fy6L1Mrd.mjs";
3
- import { a as openMcp, c as refused, o as tool, s as answer } from "./src-BbyEoMn4.mjs";
4
- import { n as explainNode } from "./context-DT8MfEIR.mjs";
5
- import { p as runMarkerIn } from "./src-DOcK8Jfp.mjs";
1
+ import { T as isNewer } from "./src-8cOPOgrT.mjs";
2
+ import { h as runMarkerIn } from "./src-I_RyKu6_.mjs";
3
+ import { n as ORK_VERSION } from "./__build_info__-D6mXC4cD.mjs";
4
+ import { n as explainNode } from "./context-CGPh12HV.mjs";
5
+ import { a as openMcp, c as refused, o as tool, s as answer } from "./src-CmEvHdju.mjs";
6
6
  import { z } from "zod";
7
7
  import { defineCommand } from "citty";
8
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -43,7 +43,7 @@ const orkMcpServer = (options = {}) => {
43
43
  ...options.script === void 0 ? {} : { script: options.script }
44
44
  });
45
45
  const all = [...opened.tools, explainTool(opened, options.home)];
46
- const tools = options.expects !== void 0 && isNewer(options.expects, "1.0.0-alpha.2") ? tooOld(all, options.expects) : all;
46
+ const tools = options.expects !== void 0 && isNewer(options.expects, "1.0.0-alpha.20") ? tooOld(all, options.expects) : all;
47
47
  const server = new McpServer({
48
48
  name: "ork",
49
49
  version: ORK_VERSION
@@ -0,0 +1,112 @@
1
+ import "./src-8cOPOgrT.mjs";
2
+ import { n as OncePathspecError, t as runOnce } from "./run-Bvvg-fiF.mjs";
3
+ import { c as write, l as writeErr, s as style } from "./output-B1kwo0YU.mjs";
4
+ import { t as ago } from "./format-BB4E-T6x.mjs";
5
+ import { spawn } from "node:child_process";
6
+ import { defineCommand, parseArgs } from "citty";
7
+ // src/commands/once.ts
8
+ /**
9
+ * @file `ork once -- <command>`: runs a command unless the same check already
10
+ * passed on identical files, and records a pass (specs/check-once.md). The
11
+ * command's own output and exit code are `ork once`'s; a hit prints one line
12
+ * and exits 0. Everything after `--` is the command, so its flags are never
13
+ * Ork's.
14
+ */
15
+ const args = {
16
+ as: {
17
+ type: "string",
18
+ description: "The check's name (default: the command's text)"
19
+ },
20
+ covers: {
21
+ type: "string",
22
+ description: "Also record the pass for this name (repeat for more)"
23
+ },
24
+ on: {
25
+ type: "string",
26
+ description: "A git pathspec the check depends on; only matching files key it (repeat for more)"
27
+ },
28
+ "with-head": {
29
+ type: "boolean",
30
+ description: "Key the check by HEAD too (for checks relative to it)"
31
+ },
32
+ force: {
33
+ type: "boolean",
34
+ description: "Run even after a pass"
35
+ }
36
+ };
37
+ /** The command's exit code: a signal or a program that can't start is 1. */
38
+ const exitCodeOf = (command) => new Promise((resolve) => {
39
+ const [program = "", ...rest] = command;
40
+ const child = spawn(program, rest, { stdio: "inherit" });
41
+ child.on("error", (error) => {
42
+ writeErr(`Cannot run ${program}: ${error.message}`);
43
+ resolve(1);
44
+ });
45
+ child.on("close", (code) => {
46
+ resolve(code ?? 1);
47
+ });
48
+ });
49
+ /**
50
+ * Every use of a repeatable flag, `--flag v` or `--flag=v`: citty keeps only
51
+ * the last. Not `run.ts`'s `flagValues`, which would load the run command's
52
+ * imports on every `ork once`.
53
+ */
54
+ const valuesOf = (before, flag) => before.flatMap((arg, index) => {
55
+ if (arg.startsWith(`${flag}=`)) return [arg.slice(flag.length + 1)];
56
+ if (arg !== flag) return [];
57
+ const next = before[index + 1];
58
+ return [next === void 0 || next.startsWith("--") ? void 0 : next];
59
+ });
60
+ /** The values of `flag`, or `undefined` when one of its uses has none. */
61
+ const listOf = (before, flag) => {
62
+ const values = valuesOf(before, flag);
63
+ return values.every((value) => value !== void 0 && value !== "") ? values.filter((value) => value !== void 0) : void 0;
64
+ };
65
+ const once = defineCommand({
66
+ meta: {
67
+ name: "once",
68
+ description: "Run a command unless the same check already passed on these files: ork once [flags] -- <command…>"
69
+ },
70
+ args,
71
+ run: async ({ rawArgs }) => {
72
+ const split = rawArgs.indexOf("--");
73
+ const command = split === -1 ? [] : rawArgs.slice(split + 1);
74
+ if (command.length === 0) {
75
+ writeErr("Say what to run after --: ork once [--as <name>] [--covers <name>]… [--on <pathspec>]… [--with-head] [--force] -- <command…>");
76
+ process.exitCode = 1;
77
+ return;
78
+ }
79
+ const before = rawArgs.slice(0, split);
80
+ const given = parseArgs(before, args);
81
+ const covers = listOf(before, "--covers");
82
+ const on = listOf(before, "--on");
83
+ if (covers === void 0 || on === void 0) {
84
+ writeErr(`${covers === void 0 ? "--covers needs a name" : "--on needs a pathspec"}: ork once [--covers <name>]… [--on <pathspec>]… -- <command…>`);
85
+ process.exitCode = 1;
86
+ return;
87
+ }
88
+ const run = process.env["ORK_RUN"];
89
+ const node = process.env["ORK_NODE"];
90
+ const result = await runOnce({
91
+ cwd: process.cwd(),
92
+ name: given["as"] ?? command.join(" "),
93
+ covers,
94
+ on,
95
+ withHead: given["with-head"] === true,
96
+ force: given["force"] === true,
97
+ ...run === void 0 || node === void 0 ? {} : { who: `${run}/${node}` },
98
+ run: () => exitCodeOf(command)
99
+ }).catch((error) => {
100
+ if (!(error instanceof OncePathspecError)) throw error;
101
+ writeErr(error.message);
102
+ return null;
103
+ });
104
+ if (result === null) {
105
+ process.exitCode = 1;
106
+ return;
107
+ }
108
+ if (result.skipped) write(`${style("green", "✓")} check already passed on these files · ${ago(result.pass.at)} · ${result.pass.who}`);
109
+ process.exitCode = result.code;
110
+ }
111
+ });
112
+ export { once };
@@ -0,0 +1,58 @@
1
+ import { Readable, Writable } from "node:stream";
2
+ // ../core/src/machine/orphan-guard.ts
3
+ /**
4
+ * @file The orphan guard: a node's processes end with `ork`, whatever ends
5
+ * `ork` (SIGKILL, the OOM killer, a crash exit). A child started in its own
6
+ * process group (`detached: true`) under `sh` also starts a small watcher in
7
+ * that group, which reads a pipe that only `ork` holds open. `ork` never
8
+ * writes to it, so when `ork` dies the read sees EOF, and the watcher sends
9
+ * its own group (`kill 0`, never another's) SIGTERM, then SIGKILL after
10
+ * {@link KILL_AFTER_MS}. A line on the pipe instead lets the watcher exit
11
+ * quietly, and so does killing it, which is how a node that ends normally
12
+ * leaves its leftover background processes alone. POSIX only. Two uses:
13
+ *
14
+ * - A shell that outlives the program (the tool runner's timing wrapper):
15
+ * `${orphanGuard(4)} w=$!; "$0" "$@" 4<&-; s=$?; kill -s KILL $w`, then
16
+ * spawn with a `"pipe"` at `stdio[4]` and call
17
+ * `holdGuard(child.stdio[4])`.
18
+ * - A shell that `exec`s the program (a session, so its pid stays the
19
+ * driver's): `${orphanGuard(3)} exec 3<&-; exec "$0" "$@"` (a bare `exec`
20
+ * closes the pipe: macOS's `sh` keeps a copy of one an `exec` command
21
+ * closes, which the driver would inherit), with a `"pipe"` at `stdio[3]`;
22
+ * nothing is left to kill the watcher, so call the function `holdGuard`
23
+ * returns once the driver has exited (on `exit`: the pipe holds `close`).
24
+ */
25
+ /** How long the group gets to end after SIGTERM before SIGKILL. */
26
+ const KILL_AFTER_MS = 2e3;
27
+ /** The file descriptors a redirect can name in every POSIX shell. */
28
+ const SHELL_FDS = [
29
+ 3,
30
+ 4,
31
+ 5,
32
+ 6,
33
+ 7,
34
+ 8,
35
+ 9
36
+ ];
37
+ /**
38
+ * Shell text that starts the watcher in the background on pipe `fd` (3 to 9);
39
+ * it ends with `&`, so `$!` after it is the watcher's pid. The watcher holds no
40
+ * other descriptor, so it never keeps the child's output pipes open.
41
+ */
42
+ const orphanGuard = (fd) => {
43
+ const others = SHELL_FDS.filter((other) => other !== fd).map((other) => `${String(other)}>&-`).join(" ");
44
+ return `{ read -r _ork <&${String(fd)} || { trap '' TERM; kill -s TERM 0; sleep ${String(KILL_AFTER_MS / 1e3)}; kill -s KILL 0; }; } </dev/null >/dev/null 2>&1 ${others} & `;
45
+ };
46
+ /**
47
+ * Keeps `ork`'s end of the watch pipe (`child.stdio[fd]`) open and drained, so
48
+ * it never holds back the child's `close`; returns the release, which writes
49
+ * the line that lets the watcher exit without touching its group.
50
+ */
51
+ const holdGuard = (pipe) => {
52
+ pipe?.on("error", () => {});
53
+ if (pipe instanceof Readable) pipe.resume();
54
+ return () => {
55
+ if (pipe instanceof Writable && pipe.writable) pipe.write("\n");
56
+ };
57
+ };
58
+ export { holdGuard as n, orphanGuard as r, KILL_AFTER_MS as t };
@@ -0,0 +1,439 @@
1
+ import { stripVTControlCharacters, styleText } from "node:util";
2
+ import { createInterface } from "node:readline/promises";
3
+ // src/term/keys.ts
4
+ /** What a `ESC [ …` or `ESC O …` sequence names. */
5
+ const SEQUENCES = {
6
+ "[A": "up",
7
+ "[B": "down",
8
+ "[C": "right",
9
+ "[D": "left",
10
+ "[H": "home",
11
+ "[F": "end",
12
+ "[1~": "home",
13
+ "[4~": "end",
14
+ "[7~": "home",
15
+ "[8~": "end",
16
+ "[3~": "delete",
17
+ OA: "up",
18
+ OB: "down",
19
+ OC: "right",
20
+ OD: "left",
21
+ OH: "home",
22
+ OF: "end"
23
+ };
24
+ const CONTROLS = {
25
+ "\r": "enter",
26
+ "\n": "enter",
27
+ "": "backspace",
28
+ "\b": "backspace",
29
+ "": "ctrl-c",
30
+ "": "ctrl-d",
31
+ "": "ctrl-a",
32
+ "": "ctrl-e",
33
+ "": "ctrl-u"
34
+ };
35
+ /** The keys in one chunk of input; what it does not know it skips. */
36
+ const parseKeys = (chunk) => {
37
+ const keys = [];
38
+ const chars = Array.from(chunk);
39
+ for (let i = 0; i < chars.length; i += 1) {
40
+ const char = chars[i] ?? "";
41
+ if (char === "\x1B") {
42
+ if (i === chars.length - 1) {
43
+ keys.push({ name: "escape" });
44
+ continue;
45
+ }
46
+ let end = i + (chars[i + 1] === "O" ? 3 : 2);
47
+ while (chars[i + 1] === "[" && end < chars.length + 1 && !/[A-Za-z~]/u.test(chars[end - 1] ?? "")) end += 1;
48
+ const name = SEQUENCES[chars.slice(i + 1, end).join("")];
49
+ if (name !== void 0) keys.push({ name });
50
+ i = end - 1;
51
+ continue;
52
+ }
53
+ const name = CONTROLS[char];
54
+ if (name !== void 0) keys.push({ name });
55
+ else if (char >= " ") keys.push({
56
+ name: "char",
57
+ text: char
58
+ });
59
+ }
60
+ return keys;
61
+ };
62
+ /**
63
+ * Reads keys from `input` in raw mode, calling `onKey` for each, until the
64
+ * returned function is called, which restores the mode and pauses the input.
65
+ */
66
+ const readKeys = (input, onKey) => {
67
+ const wasRaw = input.isRaw === true;
68
+ input.setRawMode?.(true);
69
+ input.resume();
70
+ const onData = (chunk) => {
71
+ for (const key of parseKeys(String(chunk))) onKey(key);
72
+ };
73
+ input.on("data", onData);
74
+ return () => {
75
+ input.off("data", onData);
76
+ input.setRawMode?.(wasRaw);
77
+ input.pause();
78
+ };
79
+ };
80
+ // src/term/layout.ts
81
+ /**
82
+ * @file Layout for plain text: its width, wrapped to a width, cut to one line
83
+ * with `…`, and hung under a lead. `wrap`, `cut` and `hang` take text before
84
+ * it is styled (they would cut inside an escape code), so style each piece
85
+ * after laying it out; `widthOf` and `rowsOf` ignore escape codes. Width
86
+ * counts one column per code point: wide characters (CJK, emoji) may overrun
87
+ * a line (specs/gaps.md).
88
+ */
89
+ /** The columns of `text`, escape codes not counted. */
90
+ const widthOf = (text) => Array.from(stripVTControlCharacters(text)).length;
91
+ /** `text` of one line within `width`, with `…` where it was cut. */
92
+ const cut = (text, width) => {
93
+ const chars = Array.from(text);
94
+ return chars.length <= width ? text : `${chars.slice(0, Math.max(0, width - 1)).join("")}…`;
95
+ };
96
+ /** A word that is over `width` is split into pieces of `width`. */
97
+ const pieces = (word, width) => {
98
+ const chars = Array.from(word);
99
+ const out = [];
100
+ for (let i = 0; i < chars.length; i += width) out.push(chars.slice(i, i + width).join(""));
101
+ return out;
102
+ };
103
+ /**
104
+ * `text` wrapped to lines of at most `width` columns, at spaces; each line
105
+ * break in `text` stays, and a blank line stays blank. A word longer than the
106
+ * width is split.
107
+ */
108
+ const wrap = (text, width) => text.split("\n").flatMap((paragraph) => {
109
+ const words = paragraph.split(/ +/u).filter((word) => word !== "");
110
+ const lines = [];
111
+ let line = "";
112
+ for (const word of words.flatMap((w) => pieces(w, width))) if (line === "") line = word;
113
+ else if (widthOf(line) + 1 + widthOf(word) <= width) line += ` ${word}`;
114
+ else {
115
+ lines.push(line);
116
+ line = word;
117
+ }
118
+ lines.push(line);
119
+ return lines;
120
+ });
121
+ /**
122
+ * `text` wrapped under `lead`: the first line starts with `lead`, the others
123
+ * with spaces as wide as `lead` (a hanging indent), all within `width`.
124
+ */
125
+ const hang = (lead, text, width) => {
126
+ const indent = " ".repeat(widthOf(lead));
127
+ return wrap(text, Math.max(1, width - indent.length)).map((line, index) => (index === 0 ? lead : indent).concat(line));
128
+ };
129
+ /** How many terminal rows `line` takes at `width` (at least one). */
130
+ const rowsOf = (line, width) => Math.max(1, Math.ceil(widthOf(line) / width));
131
+ const DEFAULT_WIDTH = 80;
132
+ const DEFAULT_ROWS = 24;
133
+ /** Whether `ORK_PLAIN` is on: set, and not `0` or `false`. */
134
+ const orkPlain = (value) => value !== void 0 && ![
135
+ "",
136
+ "0",
137
+ "false"
138
+ ].includes(value.toLowerCase());
139
+ /** Whether the environment asks for plain output (not the stream). */
140
+ const plainEnv = (env = process.env) => (env["NO_COLOR"] ?? "") !== "" || orkPlain(env["ORK_PLAIN"]) || env["TERM"] === "dumb";
141
+ /** How to write to `stream`, given `env`. */
142
+ const termOf = (stream, env = process.env) => ({
143
+ rich: stream.isTTY === true && !plainEnv(env),
144
+ width: Math.max(20, stream.columns ?? DEFAULT_WIDTH),
145
+ rows: stream.rows ?? DEFAULT_ROWS
146
+ });
147
+ /** Whether a prompt can take keys and redraw: rich output, a raw-mode input. */
148
+ const canKey = (input, output, env = process.env) => termOf(output, env).rich && input.isTTY === true && typeof input.setRawMode === "function";
149
+ /**
150
+ * Takes the global `--plain` flag out of `argv` (up to a `--`, after which
151
+ * words belong to a program). It sets `ORK_PLAIN=1` in `env`, which every child
152
+ * Ork starts then inherits.
153
+ */
154
+ const takePlainFlag = (argv, env = process.env) => {
155
+ const end = argv.indexOf("--");
156
+ const head = end === -1 ? argv : argv.slice(0, end);
157
+ if (!head.includes("--plain")) return [...argv];
158
+ env["ORK_PLAIN"] = "1";
159
+ return [...head.filter((word) => word !== "--plain"), ...argv.slice(head.length)];
160
+ };
161
+ // src/term/line.ts
162
+ /**
163
+ * @file The plain way to ask: one prompt, one line read. It is what every kit
164
+ * prompt falls back to (not a terminal, `NO_COLOR`, `--plain`), so scripts
165
+ * and tests that feed lines keep working, and what a screen reader sees.
166
+ */
167
+ /**
168
+ * Prints `prompt` and reads one line; `undefined` when the input ends first
169
+ * (Ctrl-D, a closed pipe). An aborted `signal` rejects.
170
+ */
171
+ const readLine = async (io, prompt, signal) => {
172
+ if (io.readLine !== void 0) return io.readLine(prompt, signal);
173
+ const rl = createInterface({
174
+ input: io.input,
175
+ output: io.output,
176
+ ...plainEnv(io.env) ? { terminal: false } : {}
177
+ });
178
+ const { promise: ended, resolve: end } = Promise.withResolvers();
179
+ rl.once("close", () => {
180
+ end();
181
+ });
182
+ try {
183
+ const answer = await Promise.race([signal === void 0 ? rl.question(prompt) : rl.question(prompt, { signal }), ended]);
184
+ return typeof answer === "string" ? answer : void 0;
185
+ } finally {
186
+ rl.close();
187
+ }
188
+ };
189
+ /** Ends a rich prompt on Ctrl-C: the caller's hook, else `SIGINT`. */
190
+ const interrupt = (io) => {
191
+ if (io.interrupt === void 0) process.kill(process.pid, "SIGINT");
192
+ else io.interrupt();
193
+ };
194
+ /** The error a withdrawn prompt rejects with: the signal's own, if it is one. */
195
+ const withdrawn = (signal) => signal?.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("the prompt was withdrawn");
196
+ // src/term/screen.ts
197
+ /**
198
+ * @file A redrawn region of the terminal: a prompt draws its lines, and draws
199
+ * them again, moving the cursor up over its own lines and clearing down,
200
+ * never the screen. When it is done it leaves one line (or none). The cursor
201
+ * is hidden while a region is open unless a prompt places it, and is always
202
+ * restored by {@link Region.finish}. While a region is open, a line another
203
+ * part of the program prints (a run's transcript, from parallel nodes) goes
204
+ * through {@link printAbove}, which erases the region, prints the line and
205
+ * draws the region again, so the rows the region counts stay true.
206
+ */
207
+ const UP = (n) => n > 0 ? `\u001B[${String(n)}A` : "";
208
+ const CLEAR_DOWN = "\r\x1B[J";
209
+ const HIDE = "\x1B[?25l";
210
+ const SHOW = "\x1B[?25h";
211
+ /** The regions drawn now, the newest last. */
212
+ const open = /* @__PURE__ */ new Set();
213
+ /**
214
+ * Prints `text` above the open region, if one is drawn: `true`, and the caller
215
+ * writes nothing. `false` when no region is open, and the caller writes as
216
+ * usual.
217
+ */
218
+ const printAbove = (text) => {
219
+ const region = [...open].at(-1);
220
+ if (region === void 0) return false;
221
+ region.above(text);
222
+ return true;
223
+ };
224
+ /** A region at the cursor of `out`; `term` gives the width, read live. */
225
+ const createRegion = (out, term) => {
226
+ /** Rows between the cursor and the region's first row. */
227
+ let up = 0;
228
+ let last = {
229
+ lines: [],
230
+ column: void 0
231
+ };
232
+ const erase = () => `${UP(up)}${CLEAR_DOWN}`;
233
+ const draw = (lines, column) => {
234
+ open.add(region);
235
+ const width = term().width;
236
+ last = {
237
+ lines,
238
+ column
239
+ };
240
+ const rows = lines.reduce((sum, line) => sum + rowsOf(line, width), 0);
241
+ const placed = column === void 0 ? HIDE : `${SHOW}\r${column > 0 ? `\u001B[${String(column)}C` : ""}`;
242
+ out.write(`${erase()}${lines.join("\n")}${placed}`);
243
+ up = Math.max(0, rows - 1);
244
+ };
245
+ const region = {
246
+ draw,
247
+ redraw: () => {
248
+ draw(last.lines, last.column);
249
+ },
250
+ above: (text) => {
251
+ out.write(`${erase()}${text}\n`);
252
+ up = 0;
253
+ draw(last.lines, last.column);
254
+ },
255
+ finish: (line) => {
256
+ out.write(`${erase()}${line === void 0 ? "" : `${line}\n`}${SHOW}`);
257
+ up = 0;
258
+ last = {
259
+ lines: [],
260
+ column: void 0
261
+ };
262
+ open.delete(region);
263
+ }
264
+ };
265
+ return region;
266
+ };
267
+ // src/term/theme.ts
268
+ /**
269
+ * @file The terminal kit's one theme: a role says what a piece of text means
270
+ * (accent, ok, warn, danger, muted), and the theme gives it a look that reads
271
+ * on dark and light terminals: the 16 ANSI colors only, and a fixed
272
+ * foreground on every chip background. Nothing is said by color alone: a chip
273
+ * carries words and a mark a symbol. In plain mode every call returns the
274
+ * text unstyled.
275
+ */
276
+ /** The text look of each role. */
277
+ const TEXT = {
278
+ accent: "cyan",
279
+ ok: "green",
280
+ warn: "yellow",
281
+ danger: "red",
282
+ muted: "dim"
283
+ };
284
+ /** The chip look of each role: a background and the foreground fixed on it. */
285
+ const CHIP = {
286
+ accent: ["bgCyan", "black"],
287
+ ok: ["bgGreen", "black"],
288
+ warn: ["bgYellow", "black"],
289
+ danger: ["bgRed", "whiteBright"],
290
+ muted: ["bgWhite", "black"]
291
+ };
292
+ /** The backgrounds a node chip takes, by a hash of its id. */
293
+ const NODE_PALETTE = [
294
+ ["bgCyan", "black"],
295
+ ["bgGreen", "black"],
296
+ ["bgYellow", "black"],
297
+ ["bgMagenta", "whiteBright"],
298
+ ["bgBlue", "whiteBright"],
299
+ ["bgWhite", "black"]
300
+ ];
301
+ /** `text` in `format` when the terminal is rich, else as is. */
302
+ const apply = (term, format, text) => term.rich ? styleText(format, text, { validateStream: false }) : text;
303
+ /** `text` in a role's color. */
304
+ const paint = (term, role, text) => apply(term, TEXT[role], text);
305
+ /** `text` in bold. */
306
+ const bold = (term, text) => apply(term, "bold", text);
307
+ // src/term/confirm.ts
308
+ /**
309
+ * @file The yes-or-no prompt: on a terminal, `Yes` and `No` that ←/→ (or `h`
310
+ * `l`, Space) move between, `y` and `n` answer at once, Enter takes the one
311
+ * shown, and Esc is no; it collapses to one line. Plain mode asks `question
312
+ * [y/N] ` and reads a line: anything but yes is no (the default, for an empty
313
+ * line), and a closed input is no.
314
+ */
315
+ /**
316
+ * Asks a yes-or-no question; `null` on Ctrl-C, after the terminal was restored
317
+ * and the interrupt sent, for a caller that must not carry on as if it were
318
+ * no.
319
+ */
320
+ const confirmOrStop = async (io, options) => {
321
+ const { question, signal } = options;
322
+ const fallback = options.default === true;
323
+ if (signal?.aborted === true) throw withdrawn(signal);
324
+ if (!canKey(io.input, io.output, io.env)) {
325
+ const answer = (await readLine(io, ` ${question} ${fallback ? "[Y/n]" : "[y/N]"} `, signal) ?? "").trim().toLowerCase();
326
+ return answer === "" ? fallback : ["y", "yes"].includes(answer);
327
+ }
328
+ const term = () => termOf(io.output, io.env);
329
+ const region = createRegion(io.output, term);
330
+ return new Promise((resolve, reject) => {
331
+ let yes = fallback;
332
+ const render = () => {
333
+ const t = term();
334
+ const asked = hang(" ? ", question, t.width).map((line, i) => i === 0 ? ` ${paint(t, "accent", "?")} ${bold(t, line.slice(3))}` : line);
335
+ const pick = (label, on) => on ? apply(t, "inverse", ` ${label} `) : ` ${label} `;
336
+ const keys = paint(t, "muted", "y/n · ←/→ · Enter");
337
+ region.draw([...asked, ` ${pick("Yes", yes)} ${pick("No", !yes)} ${keys}`]);
338
+ };
339
+ const finish = (answer) => {
340
+ stop();
341
+ io.output.off("resize", render);
342
+ signal?.removeEventListener("abort", onAbort);
343
+ const t = term();
344
+ const mark = answer ? paint(t, "ok", "✓") : paint(t, "danger", "✗");
345
+ region.finish(cut(` ${mark} ${question} · ${answer ? "Yes" : "No"}`, t.width));
346
+ resolve(answer);
347
+ };
348
+ const onAbort = () => {
349
+ stop();
350
+ io.output.off("resize", render);
351
+ region.finish();
352
+ reject(withdrawn(signal));
353
+ };
354
+ const stop = readKeys(io.input, (key) => {
355
+ if (key.name === "enter") finish(yes);
356
+ else if (key.name === "escape") finish(false);
357
+ else if (key.name === "ctrl-c") {
358
+ stop();
359
+ io.output.off("resize", render);
360
+ signal?.removeEventListener("abort", onAbort);
361
+ region.finish();
362
+ resolve(null);
363
+ interrupt(io);
364
+ } else if (key.name === "left" || key.name === "right") {
365
+ yes = !yes;
366
+ render();
367
+ } else if (key.name === "char") {
368
+ const c = key.text.toLowerCase();
369
+ if (c === "y") finish(true);
370
+ else if (c === "n") finish(false);
371
+ else if (c === "h" || c === "l" || c === " ") {
372
+ yes = !yes;
373
+ render();
374
+ }
375
+ }
376
+ });
377
+ io.output.on("resize", render);
378
+ signal?.addEventListener("abort", onAbort, { once: true });
379
+ render();
380
+ });
381
+ };
382
+ /** Asks a yes-or-no question; Ctrl-C (after restoring the terminal) is no. */
383
+ const confirm$1 = async (io, options) => await confirmOrStop(io, options) === true;
384
+ // src/output.ts
385
+ /**
386
+ * @file Terminal output helpers: styled text that turns plain when stdout is
387
+ * not a TTY or `NO_COLOR` is set, and a yes-or-no question on the terminal
388
+ * (through the terminal kit).
389
+ */
390
+ /**
391
+ * Styles text for stdout; plain when stdout is not a TTY, `NO_COLOR` is set, or
392
+ * `--plain` (`ORK_PLAIN`) was given. The environment is read on each call:
393
+ * `--plain` is set after the modules load.
394
+ */
395
+ const style = (format, text) => plainEnv() ? text : styleText(format, text, { stream: process.stdout });
396
+ /**
397
+ * Text from outside, fit for a terminal: escape sequences could hide a line on
398
+ * the very screen a person judges from, and a line break could forge one.
399
+ */
400
+ const plain = (text) => stripVTControlCharacters(text).replaceAll(/[\p{Cc}\p{Bidi_Control}]/gu, " ");
401
+ /** A line to stdout; above a prompt that is open on the same terminal. */
402
+ const write = (text) => {
403
+ if (process.stdout.isTTY && printAbove(text)) return;
404
+ process.stdout.write(`${text}\n`);
405
+ };
406
+ const writeErr = (text) => {
407
+ if (process.stderr.isTTY && printAbove(text)) return;
408
+ process.stderr.write(`${text}\n`);
409
+ };
410
+ /** What to run to follow, steer or stop the detached run `id` (ADR-081). */
411
+ const detachHint = (id) => `ork attach ${id} · ork logs ${id} -f · ork tree ${id} · ork decide · ork stop ${id}`;
412
+ /** What a run spent: `$0.0312`, or `$0.00` for nothing. */
413
+ const cost = (usd) => usd === 0 ? "$0.00" : `$${usd.toFixed(4)}`;
414
+ /**
415
+ * A dollar total that misses sessions on a driver with no dollars (RFC-015),
416
+ * said so, with the counted tokens beside it: `$1.2000 (partial) · 3.0k
417
+ * tokens`. A complete total reads as `cost` does.
418
+ */
419
+ const costOf = (usd, partial, tokens) => {
420
+ if (partial !== true) return cost(usd);
421
+ const units = tokens === void 0 ? "" : ` · ${shortCount(tokens)} tokens`;
422
+ return `${cost(usd)} (partial)${units}`;
423
+ };
424
+ /** `950`, `12.3k`, `1.2M`. */
425
+ const shortCount = (n) => {
426
+ if (n < 1e3) return String(n);
427
+ if (n < 1e6) return `${(n / 1e3).toFixed(1)}k`;
428
+ return `${(n / 1e6).toFixed(1)}M`;
429
+ };
430
+ /**
431
+ * Asks a yes-or-no question on the terminal (stderr), through the kit's confirm
432
+ * prompt; anything but yes is no. A trailing `[y/N]` in `question` is dropped:
433
+ * the prompt adds its own.
434
+ */
435
+ const confirm = (question) => confirm$1({
436
+ input: process.stdin,
437
+ output: process.stderr
438
+ }, { question: question.replace(/\s*\[y\/N\]\s*$/u, "") });
439
+ export { termOf as C, wrap as D, widthOf as E, readKeys as O, takePlainFlag as S, hang as T, interrupt as _, plain as a, canKey as b, write as c, CHIP as d, NODE_PALETTE as f, createRegion as g, paint as h, detachHint as i, writeErr as l, bold as m, cost as n, shortCount as o, apply as p, costOf as r, style as s, confirm as t, confirmOrStop as u, readLine as v, cut as w, plainEnv as x, withdrawn as y };
@@ -0,0 +1,19 @@
1
+ import { stripVTControlCharacters } from "node:util";
2
+ const OUTPUT_MAX_CHARS = 2e3;
3
+ /** Escapes stripped, other control characters (not line breaks, tabs) blanked. */
4
+ const scrub = (value) => stripVTControlCharacters(value).replaceAll(/[^\P{Cc}\n\t]|\p{Bidi_Control}/gu, " ");
5
+ /**
6
+ * The declared outputs in key order, each scrubbed and cut to
7
+ * {@link OUTPUT_MAX_LINES} lines and {@link OUTPUT_MAX_CHARS} characters; empty
8
+ * when there are none.
9
+ */
10
+ const outputsText = (outputs) => Object.entries(outputs ?? {}).map(([name, value]) => {
11
+ const clean = scrub(value.replaceAll("\r\n", "\n")).trimEnd();
12
+ const text = clean.split("\n").slice(0, 20).join("\n").slice(0, OUTPUT_MAX_CHARS);
13
+ return {
14
+ name,
15
+ text,
16
+ cut: text.length < clean.length
17
+ };
18
+ });
19
+ export { outputsText as t };