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

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 (75) hide show
  1. package/README.md +12 -7
  2. package/dist/__build_info__-0wqQ-rtf.mjs +180 -0
  3. package/dist/apply-Mb0fNpCX.mjs +17 -0
  4. package/dist/argument-error-CihH46Le.mjs +27 -0
  5. package/dist/attach-C0-YyNoY.mjs +92 -0
  6. package/dist/attach-CAKNqnUK.mjs +1259 -0
  7. package/dist/bin.mjs +1885 -1609
  8. package/dist/brief-C2hew8cS.mjs +116 -0
  9. package/dist/{decide-CGyx9rv7.mjs → builtin-files-YYAnbZGD.mjs} +3 -51
  10. package/dist/chips-CP_pgKlj.mjs +48 -0
  11. package/dist/command-tree-CEkzWNy6.mjs +63 -0
  12. package/dist/{completion-CSU5vkEt.mjs → completion-K4rAVsrf.mjs} +200 -30
  13. package/dist/context-B-fjPJky.mjs +106 -0
  14. package/dist/context-DMK0U1w0.mjs +160 -0
  15. package/dist/detached-q0AYG0Bs.mjs +39 -0
  16. package/dist/extra-arguments-BxQMXARO.mjs +13 -0
  17. package/dist/{find-D6Gj1sxg.mjs → find-Ct5CNl1J.mjs} +2 -2
  18. package/dist/flags-C90N4J1i.mjs +145 -0
  19. package/dist/follow-ups-D3HPyNYM.mjs +568 -0
  20. package/dist/format-CiPhQqbX.mjs +260 -0
  21. package/dist/{help-groups-BwYoAWsh.mjs → help-groups-BQvcKQ9H.mjs} +49 -78
  22. package/dist/in-a-run-D_wg4iJi.mjs +27 -0
  23. package/dist/install-Hji6LIei.mjs +259 -0
  24. package/dist/{limits-B1-DYc3d.mjs → limits-BD6RZRCS.mjs} +2 -2
  25. package/dist/{mcp-DE_EmyVm.mjs → mcp-CpbDmBLG.mjs} +6 -6
  26. package/dist/once-J8Qn5fTG.mjs +112 -0
  27. package/dist/orphan-guard-BFKg8VSd.mjs +58 -0
  28. package/dist/output-DXZbnFt_.mjs +458 -0
  29. package/dist/outputs-text-CNTJ7Qq_.mjs +19 -0
  30. package/dist/{pause-CL6N_Xrj.mjs → pause-Cl4ZYA35.mjs} +7 -4
  31. package/dist/print-DzugGZQB.mjs +19 -0
  32. package/dist/project-records-CtkmlsHN.mjs +233 -0
  33. package/dist/refusals-DQQX3AIb.mjs +116 -0
  34. package/dist/{relay-0SD0erwf.mjs → relay-CiP77vga.mjs} +2 -2
  35. package/dist/run-CtuNY3mE.mjs +234 -0
  36. package/dist/send-DRYGks8K.mjs +104 -0
  37. package/dist/short-flags-DkJLWVqh.mjs +1 -0
  38. package/dist/short-flags-Qf21as0E.mjs +83 -0
  39. package/dist/src-BMFQKHz4.mjs +3122 -0
  40. package/dist/{src-BbyEoMn4.mjs → src-BrFfDKux.mjs} +800 -235
  41. package/dist/{src-Dujmvv4s.mjs → src-BuF9Lbow.mjs} +12580 -5505
  42. package/dist/{src-DOcK8Jfp.mjs → src-k0A_QpST.mjs} +1024 -590
  43. package/dist/store-rTgRu8xy.mjs +25 -0
  44. package/dist/ui/THIRD_PARTY_LICENSES.txt +612 -0
  45. package/dist/ui/assets/app-BYxpKJRh.css +1 -0
  46. package/dist/ui/assets/app-SEFEJLkF.js +112 -0
  47. package/dist/ui/assets/{index-9dFha9Qn.js → index-Cb6Voq_W.js} +3 -3
  48. package/dist/ui/assets/theme-CdXvqTQ6.js +1 -0
  49. package/dist/ui/index.html +2 -2
  50. package/dist/{ui-CTmO2Xzu.mjs → ui-C2f37TSb.mjs} +57 -25
  51. package/dist/usage-BuvGcQSk.mjs +256 -0
  52. package/dist/validate-ObP5lt29.mjs +1 -0
  53. package/dist/validate-_HjACtk9.mjs +632 -0
  54. package/dist/verdict-B592j1W3.mjs +84 -0
  55. package/dist/{watch-CtbgDAcz.mjs → watch-DV3ixCYj.mjs} +45 -17
  56. package/dist/{workflows-D1_WVukk.mjs → workflows-QLjBhGNS.mjs} +30 -5
  57. package/dist/worktrees-D3ubG_Qw.mjs +302 -0
  58. package/dist/yolo-BSH1E_kE.mjs +33 -0
  59. package/package.json +10 -8
  60. package/dist/__build_info__-fy6L1Mrd.mjs +0 -615
  61. package/dist/clean-DaHjs__2.mjs +0 -88
  62. package/dist/context-DT8MfEIR.mjs +0 -274
  63. package/dist/format-Dnfv8zMT.mjs +0 -427
  64. package/dist/in-a-run-pLAZeNys.mjs +0 -22
  65. package/dist/list-BrH8KRSJ.mjs +0 -110
  66. package/dist/output-BLQWD6MJ.mjs +0 -950
  67. package/dist/remove-BeGdqroy.mjs +0 -194
  68. package/dist/report-DK4LGqwF.mjs +0 -129
  69. package/dist/send-C8tunWhv.mjs +0 -163
  70. package/dist/src-CCUKRX51.mjs +0 -1204
  71. package/dist/ui/assets/app-BNT0SuIz.css +0 -1
  72. package/dist/ui/assets/app-T7wVAhgx.js +0 -67
  73. package/dist/ui/assets/theme-3xH5jxKs.js +0 -1
  74. package/dist/usage-DYUmP0i4.mjs +0 -419
  75. package/dist/worktrees-DdMYoNOC.mjs +0 -212
@@ -0,0 +1,259 @@
1
+ import { Kr as placeFiles, Qr as inside, br as feedbackActions, ei as LESSON_CAP_TOKENS, ii as writePlainFile, ni as guardedBase, qr as placeFor, ri as plainPathInside, ti as estimateTokens } from "./src-BuF9Lbow.mjs";
2
+ import path from "node:path";
3
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
4
+ import { spawn } from "node:child_process";
5
+ // ../core/src/feedback/lessons.ts
6
+ /**
7
+ * @file Accepting a feedback item as a lesson (specs/feedback.md, ADR-059): one
8
+ * line in the workflow's `.ork/lessons/<workflow>.md`, marked with the item
9
+ * it came from so it isn't added twice, and refused when it would take the
10
+ * file past the cap a session loads, or when the file, or a folder on the way
11
+ * to it, is a link a cloned repository could aim at the person's own files.
12
+ * Only a person's acceptance writes one: feedback is model output that
13
+ * repository content can steer.
14
+ */
15
+ /** A workflow's name, as the format allows it: never a path. */
16
+ const SLUG = /^[a-z0-9][a-z0-9._-]*$/u;
17
+ /** A workflow's lessons file in a project. */
18
+ const lessonsFileOf = (place, workflow) => path.join(placeFiles(place).lessons, `${workflow}.md`);
19
+ /** What marks a lesson with its item, in the file. */
20
+ const lessonMark = (item) => `<!-- feedback ${item.slice(0, 12)} -->`;
21
+ /** The lesson as one list entry, its later lines indented under the first. */
22
+ const entryOf = (text, item) => {
23
+ const [first = "", ...rest] = text.trim().split("\n").map((line) => line.trimEnd());
24
+ return [`- ${first} ${lessonMark(item)}`, ...rest.map((l) => ` ${l}`)].join("\n").concat("\n");
25
+ };
26
+ /** Adds a lesson to its workflow's file, or says why it can't. */
27
+ const addLesson = (ask) => {
28
+ if (ask.text.trim() === "") return {
29
+ ok: false,
30
+ error: "The lesson is empty."
31
+ };
32
+ if (!SLUG.test(ask.workflow) || !plainPathInside(ask.base, ask.file)) return {
33
+ ok: false,
34
+ error: `${ask.shownAs} is a link, or behind one, or outside the project: Ork writes lessons only to a plain file there, so it wrote nothing.`
35
+ };
36
+ const current = existsSync(ask.file) ? readFileSync(ask.file, "utf8") : `# Lessons for ${ask.workflow}\n\n`;
37
+ if (current.includes(lessonMark(ask.item))) return {
38
+ ok: false,
39
+ error: `${ask.item.slice(0, 12)} is already a lesson in ${ask.shownAs}.`
40
+ };
41
+ const next = `${current}${current === "" || current.endsWith("\n") ? "" : "\n"}${entryOf(ask.text, ask.item)}`;
42
+ const tokens = estimateTokens(next);
43
+ if (tokens > 1500) return {
44
+ ok: false,
45
+ error: `${ask.shownAs} would be about ${tokens.toLocaleString("en")} tokens, over the ${LESSON_CAP_TOKENS.toLocaleString("en")} a session loads. Edit it to make room, then accept again.`
46
+ };
47
+ const written = writePlainFile(ask.base, ask.file, next, ask.shownAs);
48
+ return written.ok ? {
49
+ ok: true,
50
+ tokens
51
+ } : written;
52
+ };
53
+ // ../core/src/feedback/accept.ts
54
+ /**
55
+ * @file Accepting a feedback item (`ork feedback accept`, the Feedback panel):
56
+ * marks an item accepted and, with `--lesson`, writes it as a lesson into the
57
+ * lessons file of the workflow it was seen in, in the project of its newest
58
+ * record there (ADR-059). An `ork` item is about Ork itself, so it's never a
59
+ * lesson: it can be published instead.
60
+ */
61
+ const listed = (names) => names.map((n) => `\`${n}\``).join(", ");
62
+ /** The workflow a lesson is for: the one named, or the only one it was seen in. */
63
+ const workflowFor = (item, named) => {
64
+ if (named !== void 0) return item.workflows.includes(named) ? {
65
+ ok: true,
66
+ workflow: named
67
+ } : {
68
+ ok: false,
69
+ error: `${item.id.slice(0, 12)} wasn't seen in \`${named}\`, only in ${listed(item.workflows)}.`
70
+ };
71
+ const [only, other] = item.workflows;
72
+ if (only === void 0) return {
73
+ ok: false,
74
+ error: `No workflow is recorded for ${item.id.slice(0, 12)}, so it has no lessons file.`
75
+ };
76
+ return other === void 0 ? {
77
+ ok: true,
78
+ workflow: only
79
+ } : {
80
+ ok: false,
81
+ error: `${item.id.slice(0, 12)} was seen in ${listed(item.workflows)}: name one with --workflow.`
82
+ };
83
+ };
84
+ const real = (dir) => {
85
+ try {
86
+ return realpathSync(dir);
87
+ } catch {
88
+ return path.resolve(dir);
89
+ }
90
+ };
91
+ /**
92
+ * The project of the item's newest record (in `workflow`, when given): the
93
+ * folder its run started in, when that's still the same project. A clone that
94
+ * was deleted is refused, never taken for a repository above it.
95
+ */
96
+ const placeOf = (store, item, workflow, registered) => {
97
+ const record = store.feedbackRecords(item.id).findLast((r) => workflow === void 0 || r.feedback.origin.workflow === workflow);
98
+ const [run] = record === void 0 ? [] : store.lookup(record.runId);
99
+ if (run === void 0) return {
100
+ ok: false,
101
+ error: `The runs ${item.id.slice(0, 12)} was seen in are gone, so its project isn't known.`
102
+ };
103
+ if (!existsSync(run.project)) return {
104
+ ok: false,
105
+ error: `Its project at ${run.project} is gone.`
106
+ };
107
+ const folder = path.dirname(run.file);
108
+ const place = placeFor(existsSync(folder) ? folder : run.project, registered);
109
+ return real(place.root) === real(run.project) ? {
110
+ ok: true,
111
+ place,
112
+ project: run.project
113
+ } : {
114
+ ok: false,
115
+ error: `Its project at ${run.project} isn't a project any more: Ork would take it for ${place.root}.`
116
+ };
117
+ };
118
+ /** Accepts `item`, as a lesson with `--lesson`. */
119
+ const acceptItem = (store, item, args, registered) => {
120
+ const name = `${item.id.slice(0, 12)}: ${item.title}`;
121
+ const actions = feedbackActions(item);
122
+ if (!args.lesson) {
123
+ if (!actions.accept.ok) return {
124
+ ok: false,
125
+ error: actions.accept.error
126
+ };
127
+ store.setFeedbackStatus(item.id, "accepted");
128
+ return {
129
+ ok: true,
130
+ lines: [`accepted ${name}`]
131
+ };
132
+ }
133
+ if (!actions.lesson.ok) return {
134
+ ok: false,
135
+ error: actions.lesson.error
136
+ };
137
+ const chosen = workflowFor(item, args.workflow);
138
+ if (!chosen.ok) return chosen;
139
+ const found = placeOf(store, item, chosen.workflow, registered);
140
+ if (!found.ok) return found;
141
+ const { place } = found;
142
+ const file = lessonsFileOf(place, chosen.workflow);
143
+ const shownAs = inside(place.root, file) ? path.relative(place.root, file) : file;
144
+ const added = addLesson({
145
+ file,
146
+ base: guardedBase(place),
147
+ shownAs,
148
+ workflow: chosen.workflow,
149
+ item: item.id,
150
+ text: args.text ?? item.title
151
+ });
152
+ if (!added.ok) return added;
153
+ if (actions.accept.ok) store.setFeedbackStatus(item.id, "accepted");
154
+ return {
155
+ ok: true,
156
+ lesson: {
157
+ file: shownAs,
158
+ root: place.root,
159
+ workflow: chosen.workflow,
160
+ tokens: added.tokens
161
+ },
162
+ lines: [`accepted ${name}`, `lesson added to ${shownAs} in ${place.root} (about ${added.tokens.toLocaleString("en")} of ${LESSON_CAP_TOKENS.toLocaleString("en")} tokens); every session of \`${chosen.workflow}\` reads it from now on`]
163
+ };
164
+ };
165
+ const lessonPlaceOf = (store, item, workflow, registered) => {
166
+ const found = placeOf(store, item, workflow, registered);
167
+ if (!found.ok) return {
168
+ ok: false,
169
+ workflow,
170
+ error: found.error
171
+ };
172
+ const { place } = found;
173
+ const file = lessonsFileOf(place, workflow);
174
+ const exists = existsSync(file);
175
+ return {
176
+ ok: true,
177
+ workflow,
178
+ file: inside(place.root, file) ? path.relative(place.root, file) : file,
179
+ root: place.root,
180
+ tokens: exists ? estimateTokens(readFileSync(file, "utf8")) : 0,
181
+ exists
182
+ };
183
+ };
184
+ // ../core/src/remote/session.ts
185
+ const chattable = (type) => type === "agent.leader" || type === "agent.worker";
186
+ /** Reads `node`'s session from a run's events, and every node's last output. */
187
+ const nodeSession = (events, node) => {
188
+ const session = {
189
+ known: false,
190
+ chattable: false,
191
+ running: false
192
+ };
193
+ const outputs = {};
194
+ for (const { event } of events) {
195
+ if (event.type === "run.started") {
196
+ const declared = event.nodes.find((n) => n.id === node);
197
+ if (declared !== void 0) {
198
+ session.known = true;
199
+ session.chattable = chattable(declared.type);
200
+ }
201
+ }
202
+ if (event.type === "node.finished") outputs[event.id] = event.output;
203
+ if (!("id" in event) || event.id !== node) continue;
204
+ if (event.type === "node.started") {
205
+ session.known = true;
206
+ session.chattable = chattable(event.nodeType);
207
+ session.running = true;
208
+ }
209
+ if (event.type === "node.finished") session.running = false;
210
+ if (event.type === "worktree" && event.action === "created") session.checkout = {
211
+ path: event.path,
212
+ ...event.branch === void 0 ? {} : { branch: event.branch },
213
+ own: true,
214
+ owner: node
215
+ };
216
+ if (event.type !== "node.event") continue;
217
+ const inner = event.event;
218
+ if (inner.fork === true) continue;
219
+ if (inner.type === "session.started") {
220
+ session.sessionId = inner.sessionId;
221
+ if (inner.cwd !== void 0) session.cwd = inner.cwd;
222
+ if (inner.driver !== void 0) session.driver = inner.driver;
223
+ if (inner.model !== "") session.model = inner.model;
224
+ }
225
+ if (inner.type === "session.finished" && inner.outcome.sessionId !== "") session.sessionId = inner.outcome.sessionId;
226
+ }
227
+ return {
228
+ session,
229
+ outputs
230
+ };
231
+ };
232
+ /**
233
+ * Runs the command; resolves its exit code. Without `onOutput` it writes on the
234
+ * terminal.
235
+ */
236
+ const runInstall = (argv, onOutput) => new Promise((resolve) => {
237
+ const [program = "", ...args] = argv;
238
+ const child = spawn(program, args, { stdio: onOutput === void 0 ? "inherit" : [
239
+ "ignore",
240
+ "pipe",
241
+ "pipe"
242
+ ] });
243
+ for (const stream of ["stdout", "stderr"]) {
244
+ child[stream]?.setEncoding("utf8");
245
+ child[stream]?.on("data", (text) => {
246
+ onOutput?.(stream, text);
247
+ });
248
+ }
249
+ child.on("error", (error) => {
250
+ const line = `Could not run ${program}: ${error.message}\n`;
251
+ if (onOutput === void 0) process.stderr.write(line);
252
+ else onOutput("stderr", line);
253
+ resolve(127);
254
+ });
255
+ child.on("close", (code) => {
256
+ resolve(code ?? 1);
257
+ });
258
+ });
259
+ export { placeOf as a, lessonPlaceOf as i, nodeSession as n, acceptItem as r, runInstall as t };
@@ -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-BuF9Lbow.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-BuF9Lbow.mjs";
2
+ import { h as runMarkerIn } from "./src-k0A_QpST.mjs";
3
+ import { n as ORK_VERSION } from "./__build_info__-0wqQ-rtf.mjs";
4
+ import { n as explainNode } from "./context-B-fjPJky.mjs";
5
+ import { a as openMcp, c as refused, o as tool, s as answer } from "./src-BrFfDKux.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.21") ? 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-BuF9Lbow.mjs";
2
+ import { n as OncePathspecError, t as runOnce } from "./run-CtuNY3mE.mjs";
3
+ import { c as write, l as writeErr, s as style } from "./output-DXZbnFt_.mjs";
4
+ import { t as ago } from "./format-CiPhQqbX.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 };