@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,233 @@
1
+ import { $r as projectRootOf, Gr as newProjectId, Jr as projectsHome, Kr as placeFiles, R as openStore, Ur as hasInRepoFiles, Wr as inRepoPlace, Xr as shownPath } from "./src-BuF9Lbow.mjs";
2
+ import { c as write, l as writeErr, s as style, v as termOf, y as cut } from "./output-DXZbnFt_.mjs";
3
+ import { r as chip } from "./chips-CP_pgKlj.mjs";
4
+ import { t as refuseExtraArguments } from "./extra-arguments-BxQMXARO.mjs";
5
+ import path from "node:path";
6
+ import { existsSync, mkdirSync, realpathSync, statSync } from "node:fs";
7
+ import { defineCommand } from "citty";
8
+ // src/project/register.ts
9
+ /**
10
+ * @file Registering projects (ADR-035): `ork register` attaches a repository
11
+ * from outside it, with its workflows, rules and runs in
12
+ * `~/.ork/projects/<id>/` and nothing written into the repository; `ork init`
13
+ * records an in-repo project, whose files are its `.ork/`.
14
+ */
15
+ const ID = /^[a-z0-9][a-z0-9._-]*$/u;
16
+ /** The git root of `target`'s main clone, or why there is none. */
17
+ const cloneOf = (target) => {
18
+ if (!existsSync(target)) return {
19
+ ok: false,
20
+ error: `${target} does not exist`
21
+ };
22
+ const root = projectRootOf(realpathSync(target));
23
+ const dotGit = path.join(root, ".git");
24
+ if (!existsSync(dotGit)) return {
25
+ ok: false,
26
+ error: `${target} is not in a git repository`
27
+ };
28
+ if (statSync(dotGit).isFile()) return {
29
+ ok: false,
30
+ error: `${root} is a git worktree; register the main clone instead`
31
+ };
32
+ return {
33
+ ok: true,
34
+ root
35
+ };
36
+ };
37
+ /**
38
+ * Attaches the repository at `target`: creates `~/.ork/projects/<id>/workflows`
39
+ * and records the project. Registering it again returns the same project.
40
+ */
41
+ const registerProject = (store, target, options = {}) => {
42
+ const clone = cloneOf(target);
43
+ if (!clone.ok) return clone;
44
+ const { root } = clone;
45
+ const projects = store.projects();
46
+ const existing = projects.find((p) => p.root === root);
47
+ if (existing?.mode === "attached") return {
48
+ ok: true,
49
+ project: existing,
50
+ created: false
51
+ };
52
+ if (existing !== void 0 || hasInRepoFiles(root)) return {
53
+ ok: false,
54
+ error: `${root} keeps Ork's files in its own .ork/ (in-repo), so it is not attached`
55
+ };
56
+ const taken = projects.map((p) => p.id);
57
+ if (options.id !== void 0) {
58
+ if (!ID.test(options.id)) return {
59
+ ok: false,
60
+ error: `--id \`${options.id}\` must be lowercase letters, digits, ".", "_" or "-"`
61
+ };
62
+ if (taken.includes(options.id)) return {
63
+ ok: false,
64
+ error: `a project is already registered as ${options.id}`
65
+ };
66
+ }
67
+ const id = options.id ?? newProjectId(root, taken);
68
+ const project = {
69
+ id,
70
+ root,
71
+ mode: "attached",
72
+ dir: path.join(projectsHome(options.home), id)
73
+ };
74
+ mkdirSync(placeFiles(project).workflows, { recursive: true });
75
+ return {
76
+ ok: true,
77
+ project: store.addProject(project),
78
+ created: true
79
+ };
80
+ };
81
+ /**
82
+ * Records the in-repo project at `cwd`'s git root, if it is not registered yet;
83
+ * an attached one stays attached (`ork init` must not take it over).
84
+ */
85
+ const recordInRepo = (store, cwd) => {
86
+ const root = projectRootOf(realpathSync(cwd));
87
+ const projects = store.projects();
88
+ const existing = projects.find((p) => p.root === root);
89
+ if (existing?.mode === "attached") return {
90
+ ok: false,
91
+ error: `${root} is attached: its workflows are in ${shownPath(existing.dir, root)}, and Ork writes nothing into the repository`
92
+ };
93
+ if (existing !== void 0) return {
94
+ ok: true,
95
+ project: existing,
96
+ created: false
97
+ };
98
+ const project = {
99
+ ...inRepoPlace(root),
100
+ id: newProjectId(root, projects.map((p) => p.id))
101
+ };
102
+ return {
103
+ ok: true,
104
+ project: store.addProject(project),
105
+ created: true
106
+ };
107
+ };
108
+ /**
109
+ * Points a registered project at its clone's new place, after the clone was
110
+ * moved or renamed; its runs follow, and an in-repo project's `.ork/` with it.
111
+ */
112
+ const relocateProject = (store, id, target) => {
113
+ const projects = store.projects();
114
+ const project = projects.find((p) => p.id === id);
115
+ if (project === void 0) return {
116
+ ok: false,
117
+ error: `No project is registered as ${id}.`
118
+ };
119
+ const clone = cloneOf(target);
120
+ if (!clone.ok) return clone;
121
+ const { root } = clone;
122
+ const other = projects.find((p) => p.root === root && p.id !== id);
123
+ if (other !== void 0) return {
124
+ ok: false,
125
+ error: `${root} is already registered as ${other.id}`
126
+ };
127
+ const dir = project.mode === "in-repo" ? inRepoPlace(root).dir : project.dir;
128
+ store.moveProject(id, {
129
+ root,
130
+ mode: project.mode,
131
+ dir
132
+ });
133
+ return {
134
+ ok: true,
135
+ project: {
136
+ ...project,
137
+ root,
138
+ dir
139
+ },
140
+ created: false
141
+ };
142
+ };
143
+ /**
144
+ * One project's lines for `ork projects`; rich, its mode and a missing clone
145
+ * are chips, its id is padded to `idWidth` and its root is cut to the screen.
146
+ */
147
+ const formatProject = (project, term = termOf(process.stdout), idWidth = 0) => {
148
+ const missing = !existsSync(project.root);
149
+ if (!term.rich) {
150
+ const gone = missing ? style("yellow", " (missing)") : "";
151
+ return [`${style("bold", project.id)} ${style("dim", project.mode)} ${project.root}${gone}`, style("dim", ` files: ${shownPath(project.dir, project.root)}`)];
152
+ }
153
+ const mode = chip(term, project.mode.toUpperCase(), "muted");
154
+ const gone = missing ? ` ${chip(term, "MISSING", "warn")}` : "";
155
+ const room = term.width - (idWidth + 2 + project.mode.length + 2 + 2) - (missing ? 10 : 0);
156
+ return [`${style("bold", project.id.padEnd(idWidth))} ${mode} ${cut(project.root, Math.max(8, room))}${gone}`, style("dim", cut(` files: ${shownPath(project.dir, project.root)}`, term.width))];
157
+ };
158
+ /** Every project's lines, their ids aligned when rich. */
159
+ const formatProjects = (projects, term = termOf(process.stdout)) => {
160
+ const idWidth = term.rich ? Math.max(0, ...projects.map((p) => p.id.length)) : 0;
161
+ return projects.flatMap((p) => formatProject(p, term, idWidth));
162
+ };
163
+ /** What `ork register` prints: the project, and where its files go. */
164
+ const formatRegistered = (project, created) => {
165
+ const files = placeFiles(project);
166
+ const shown = (file) => shownPath(file, project.root);
167
+ return [
168
+ `${style("green", created ? "registered" : "already registered")} ${project.id} → ${project.root}`,
169
+ `\nWorkflows: ${shown(files.workflows)}/ · rules: ${shown(files.rules)} · runs: ${shown(files.runs)}/`,
170
+ `Nothing is written into ${project.root}. In it, ${style("bold", "ork run <name>")} finds the workflows above.`
171
+ ];
172
+ };
173
+ // src/commands/project-records.ts
174
+ /**
175
+ * @file `ork projects`' subcommands on one project's record (ADR-035): `forget`
176
+ * drops it and keeps its files and runs; `move` points it at its clone's new
177
+ * place after the clone was moved or renamed, and its runs follow.
178
+ */
179
+ /** Runs `use` with the store open, and closes it. */
180
+ const withStore = (use) => {
181
+ const store = openStore();
182
+ try {
183
+ return use(store);
184
+ } finally {
185
+ store.close();
186
+ }
187
+ };
188
+ const id = {
189
+ type: "positional",
190
+ description: "The project's id",
191
+ required: true
192
+ };
193
+ const forget = defineCommand({
194
+ meta: {
195
+ name: "forget",
196
+ description: "Forget a registered project; its files and runs are kept"
197
+ },
198
+ plugins: [refuseExtraArguments],
199
+ args: { id },
200
+ run: ({ args }) => {
201
+ if (withStore((store) => store.forgetProject(args.id))) {
202
+ write(`${style("green", "forgot")} ${args.id} (its files are kept)`);
203
+ return;
204
+ }
205
+ writeErr(`No project is registered as ${args.id}.`);
206
+ process.exitCode = 1;
207
+ }
208
+ });
209
+ const move = defineCommand({
210
+ meta: {
211
+ name: "move",
212
+ description: "Point a registered project at its clone's new place, after moving or renaming it"
213
+ },
214
+ plugins: [refuseExtraArguments],
215
+ args: {
216
+ id,
217
+ path: {
218
+ type: "positional",
219
+ description: "Where the clone is now",
220
+ required: true
221
+ }
222
+ },
223
+ run: ({ args }) => {
224
+ const result = withStore((store) => relocateProject(store, args.id, args.path));
225
+ if (result.ok) {
226
+ write(`${style("green", "moved")} ${args.id} → ${result.project.root}`);
227
+ return;
228
+ }
229
+ writeErr(result.error);
230
+ process.exitCode = 1;
231
+ }
232
+ });
233
+ export { formatRegistered as a, formatProjects as i, move as n, recordInRepo as o, withStore as r, registerProject as s, forget as t };
@@ -0,0 +1,116 @@
1
+ // ../core/src/list-filters/time.ts
2
+ /**
3
+ * @file The times every list's `--since` and `--until` take (specs/usage.md,
4
+ * RFC-017), and `ork prune --older-than`'s age: an age (`24h`, `30d`, `12w`)
5
+ * or a day (`2026-11-01`, in the machine's time zone).
6
+ */
7
+ const UNITS = {
8
+ h: 36e5,
9
+ d: 864e5,
10
+ w: 6048e5
11
+ };
12
+ /** `30d`, `12w`, `24h` before `now`, as an ISO time; `null` when it is not one. */
13
+ const agoTime = (text, now) => {
14
+ const match = /^(\d+)([hdw])$/u.exec(text);
15
+ if (match === null) return null;
16
+ const [, amount = "0", unit = "d"] = match;
17
+ if (Number(amount) === 0) return null;
18
+ const date = new Date(now - Number(amount) * (UNITS[unit] ?? 0));
19
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
20
+ };
21
+ /** A `YYYY-MM-DD` day's start in the machine's time zone, `after` days on. */
22
+ const dayTime = (text, after) => {
23
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/u.exec(text);
24
+ if (match === null) return null;
25
+ const [, year = "", month = "", day = ""] = match;
26
+ const start = new Date(Number(year), Number(month) - 1, Number(day));
27
+ if (start.getMonth() !== Number(month) - 1) return null;
28
+ if (start.getDate() !== Number(day)) return null;
29
+ start.setDate(start.getDate() + after);
30
+ return start.toISOString();
31
+ };
32
+ /** The start of the day `days` whole days ago, counting today as one. */
33
+ const wholeDays = (days, now) => {
34
+ const today = new Date(now);
35
+ return new Date(today.getFullYear(), today.getMonth(), today.getDate() - (days - 1)).toISOString();
36
+ };
37
+ /**
38
+ * `--since`: an age or the day it starts from. Days and weeks are whole days,
39
+ * today among them, as the Usage panel counts them; hours are exact.
40
+ */
41
+ const sinceTime = (text, now) => {
42
+ const ago = agoTime(text, now);
43
+ if (ago === null) return dayTime(text, 0);
44
+ if (text.endsWith("h")) return ago;
45
+ const amount = Number(text.slice(0, -1));
46
+ return wholeDays(text.endsWith("w") ? amount * 7 : amount, now);
47
+ };
48
+ /** `--until`: the last day it includes. */
49
+ const untilTime = (text) => dayTime(text, 1);
50
+ // ../core/src/list-filters/parse.ts
51
+ /**
52
+ * @file Reads a list's filter flags against its field table (RFC-017): each
53
+ * value checked, times turned into ISO times, and `--limit`.
54
+ */
55
+ const FOR = /^(agent|me|(project|person):\S+)$/u;
56
+ /** The field's value, checked; `null` when the field does not take it. */
57
+ const valueOf = (field, text, now) => {
58
+ if (text === "all" && field.all === true) return "all";
59
+ return {
60
+ enum: () => field.values?.includes(text) === true ? text : null,
61
+ text: () => text === "" ? null : text,
62
+ for: () => FOR.test(text) ? text : null,
63
+ since: () => sinceTime(text, now),
64
+ until: () => untilTime(text)
65
+ }[field.type]();
66
+ };
67
+ /** What the field takes, for the error that refuses a value. */
68
+ const takes = (field) => {
69
+ const all = field.all === true ? ["all"] : [];
70
+ const or = all.length === 0 ? "" : " or all";
71
+ return {
72
+ enum: `one of ${[...field.values ?? [], ...all].join(", ")}`,
73
+ text: `a value${or}`,
74
+ for: "agent, me, project:<name> or person:<name>",
75
+ since: `an age (30d), a day (2026-11-01)${or}`,
76
+ until: "a day (2026-11-01)"
77
+ }[field.type];
78
+ };
79
+ /**
80
+ * The filter `given` (a command's parsed flags, by field name) asks for, each
81
+ * value checked against `table`; `now` dates an age. The first value a field
82
+ * does not take is refused, naming the flag and what it takes.
83
+ */
84
+ const parseFilters = (table, given, now) => {
85
+ const filter = {};
86
+ for (const field of table.fields) {
87
+ const text = given[field.name];
88
+ if (typeof text !== "string") continue;
89
+ const value = valueOf(field, text, now);
90
+ if (value === null) return {
91
+ ok: false,
92
+ error: `--${field.name} takes ${takes(field)}, not \`${text}\`.`
93
+ };
94
+ filter[field.name] = value;
95
+ }
96
+ return {
97
+ ok: true,
98
+ filter
99
+ };
100
+ };
101
+ /** `--limit`: a positive whole number, else `null` (`0`, `-1`, `abc`, `2.5`). */
102
+ const parseLimit = (text) => {
103
+ if (!/^[1-9]\d*$/u.test(text)) return null;
104
+ const limit = Number(text);
105
+ return Number.isSafeInteger(limit) ? limit : null;
106
+ };
107
+ // ../core/src/list-filters/refusals.ts
108
+ /**
109
+ * @file The sentences a list refuses with, shared so `ork` and `ork mcp` say
110
+ * the same words (the server cannot import the CLI).
111
+ */
112
+ /** The refusal for a `--limit` that is not a positive whole number in digits. */
113
+ const limitRefusal = (text) => `--limit takes a positive whole number written in digits, not \`${text}\`.`;
114
+ /** The refusal for a `--project` that names no registered project. */
115
+ const unknownProjectRefusal = (key, ids) => `No registered project \`${key}\`; registered: ${ids.length === 0 ? "none" : ids.join(", ")}.`;
116
+ export { agoTime as a, parseLimit as i, unknownProjectRefusal as n, parseFilters as r, limitRefusal as t };
@@ -1,5 +1,5 @@
1
- import { B as linkNodes, L as addressOf, U as unlinkNodes } from "./src-Dujmvv4s.mjs";
2
- import { a as write, c as openStore, i as style, o as writeErr } from "./output-BLQWD6MJ.mjs";
1
+ import { En as addressOf, On as linkNodes, R as openStore, kn as unlinkNodes } from "./src-BuF9Lbow.mjs";
2
+ import { c as write, l as writeErr, s as style } from "./output-DXZbnFt_.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  // src/commands/relay.ts
5
5
  /**
@@ -0,0 +1,234 @@
1
+ import { in as gitOk, rn as git } from "./src-BuF9Lbow.mjs";
2
+ import path from "node:path";
3
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import { tmpdir } from "node:os";
6
+ // ../core/src/once/key.ts
7
+ /**
8
+ * @file The key of a check run once (`ork once`, `verify: { once: true }`): the
9
+ * working tree's files as a git tree (tracked and untracked, not ignored),
10
+ * written through a temporary index so the real one is untouched, with `HEAD`
11
+ * and the repository's common git folder, where passes are kept for every
12
+ * worktree. The key is the files, not the commit: a commit or a no-op rebase
13
+ * keeps it. With pathspecs (`--on`), only the files they match count.
14
+ */
15
+ /**
16
+ * Git rejected the pathspecs, or they match no file: a usage error, never a
17
+ * silent hit.
18
+ */
19
+ var OncePathspecError = class extends Error {
20
+ constructor(message) {
21
+ super(message);
22
+ this.name = "OncePathspecError";
23
+ }
24
+ };
25
+ /** The part of the key a pass is matched on: the files, else the tree. */
26
+ const filesOf = (key) => key.files ?? key.tree;
27
+ /**
28
+ * The key of the files in the worktree at `cwd`. Rejects outside a git
29
+ * repository, and with `OncePathspecError` when git rejects `on` or it matches
30
+ * no file. Starts from a copy of the real index, so git hashes only the files
31
+ * whose stat changed.
32
+ */
33
+ const treeKeyOf = async (cwd, { on = [] } = {}) => {
34
+ const [commonDir = "", index = ""] = (await gitOk(cwd, [
35
+ "rev-parse",
36
+ "--git-common-dir",
37
+ "--git-path",
38
+ "index"
39
+ ])).split("\n").map((line) => path.resolve(cwd, line.trim()));
40
+ const scratch = mkdtempSync(path.join(tmpdir(), "ork-once-"));
41
+ const env = { GIT_INDEX_FILE: path.join(scratch, "index") };
42
+ try {
43
+ if (existsSync(index)) copyFileSync(index, env.GIT_INDEX_FILE);
44
+ await gitOk(cwd, ["add", "-A"], env);
45
+ const tree = (await gitOk(cwd, ["write-tree"], env)).trim();
46
+ const head = await git(cwd, [
47
+ "rev-parse",
48
+ "--verify",
49
+ "-q",
50
+ "HEAD"
51
+ ]);
52
+ const key = {
53
+ tree,
54
+ head: head.code === 0 ? head.stdout.trim() : null,
55
+ commonDir
56
+ };
57
+ if (on.length === 0) return key;
58
+ const listed = await git(cwd, [
59
+ "ls-files",
60
+ "-s",
61
+ "-z",
62
+ "--full-name",
63
+ "--",
64
+ ...on
65
+ ], env);
66
+ if (listed.code !== 0) throw new OncePathspecError(`git rejected the pathspecs ${on.join(" ")}: ${listed.stderr.trim()}`);
67
+ if (listed.stdout === "") throw new OncePathspecError(`the pathspecs ${on.join(" ")} match no file: check them, or drop --on to key every file`);
68
+ const files = createHash("sha256").update(listed.stdout).digest("hex");
69
+ return {
70
+ ...key,
71
+ files,
72
+ on: [...on]
73
+ };
74
+ } finally {
75
+ rmSync(scratch, {
76
+ recursive: true,
77
+ force: true
78
+ });
79
+ }
80
+ };
81
+ // ../core/src/once/store.ts
82
+ /**
83
+ * @file The passes of checks run once, kept in `<git-common-dir>/ork-once/`,
84
+ * outside every worktree and shared by all of them. A pass is one JSON file
85
+ * per check name, files and Node version; it holds the tree, `HEAD`, the time
86
+ * and who ran it, nothing else. Only a passing run writes one (Ork's process
87
+ * or the check's own, never an agent's edit). Pruned past 7 days or 500
88
+ * entries.
89
+ */
90
+ const ONCE_MAX_AGE_MS = 6048e5;
91
+ /** The folder of passes for a repository's common git folder. */
92
+ const onceDirOf = (commonDir) => path.join(commonDir, "ork-once");
93
+ /** Without pathspecs the name key is unchanged, so earlier passes still hit. */
94
+ const fileOf = (dir, name, key, node) => path.join(dir, `${createHash("sha256").update(JSON.stringify(key.on === void 0 || key.on.length === 0 ? [
95
+ name,
96
+ key.tree,
97
+ node
98
+ ] : [
99
+ name,
100
+ filesOf(key),
101
+ node,
102
+ key.on
103
+ ])).digest("hex")}.json`);
104
+ const read = (file) => {
105
+ try {
106
+ const value = JSON.parse(readFileSync(file, "utf8"));
107
+ if (typeof value !== "object" || value === null || !("name" in value && "tree" in value && "head" in value && "node" in value && "at" in value && "who" in value)) return null;
108
+ const { name, tree, head, node, at, who } = value;
109
+ return typeof name === "string" && typeof tree === "string" && (typeof head === "string" || head === null) && typeof node === "string" && typeof at === "string" && typeof who === "string" ? {
110
+ name,
111
+ tree,
112
+ head,
113
+ node,
114
+ at,
115
+ who
116
+ } : null;
117
+ } catch {
118
+ return null;
119
+ }
120
+ };
121
+ /** The pass of `name` on these files, or `null`. */
122
+ const findPass = ({ key, name, withHead = false, node = process.version }) => {
123
+ const pass = read(fileOf(onceDirOf(key.commonDir), name, key, node));
124
+ if (pass === null || pass.tree !== filesOf(key) || pass.node !== node) return null;
125
+ return withHead && pass.head !== key.head ? null : pass;
126
+ };
127
+ /** Records a pass under `name` and each covered name, then prunes. */
128
+ const recordPass = ({ key, name, covers = [], who, node = process.version, at = /* @__PURE__ */ new Date() }) => {
129
+ const dir = onceDirOf(key.commonDir);
130
+ mkdirSync(dir, { recursive: true });
131
+ const pass = {
132
+ name,
133
+ tree: filesOf(key),
134
+ head: key.head,
135
+ node,
136
+ at: at.toISOString(),
137
+ who
138
+ };
139
+ for (const each of [name, ...covers]) {
140
+ const temp = path.join(dir, `.${randomUUID()}.tmp`);
141
+ writeFileSync(temp, `${JSON.stringify({
142
+ ...pass,
143
+ name: each
144
+ })}\n`);
145
+ renameSync(temp, fileOf(dir, each, key, node));
146
+ }
147
+ prunePasses(key.commonDir, { now: at });
148
+ return pass;
149
+ };
150
+ /**
151
+ * Removes passes older than `maxAgeMs` or past the newest `maxEntries`, and
152
+ * files that don't read as a pass. Returns how many it removed.
153
+ */
154
+ const prunePasses = (commonDir, { now = /* @__PURE__ */ new Date(), maxAgeMs = ONCE_MAX_AGE_MS, maxEntries = 500 } = {}) => {
155
+ const dir = onceDirOf(commonDir);
156
+ let names;
157
+ try {
158
+ names = readdirSync(dir).filter((name) => name.endsWith(".json"));
159
+ } catch {
160
+ return 0;
161
+ }
162
+ const kept = [];
163
+ const stale = [];
164
+ for (const name of names) {
165
+ const file = path.join(dir, name);
166
+ const at = Date.parse(read(file)?.at ?? "");
167
+ if (Number.isNaN(at) || now.getTime() - at > maxAgeMs) stale.push(file);
168
+ else kept.push({
169
+ file,
170
+ at
171
+ });
172
+ }
173
+ kept.sort((a, b) => b.at - a.at);
174
+ stale.push(...kept.slice(maxEntries).map((entry) => entry.file));
175
+ for (const file of stale) rmSync(file, { force: true });
176
+ return stale.length;
177
+ };
178
+ // ../core/src/once/run.ts
179
+ /**
180
+ * @file Runs a check once per set of files, for `ork once` and for
181
+ * `verify:`/tool commands with `once: true`: skip it when the same check
182
+ * already passed on identical files, else run it and record a pass when it
183
+ * exits 0 and the files didn't change while it ran. `force` and
184
+ * `ORK_ONCE=off` run it anyway; outside a git repository it always runs.
185
+ */
186
+ /**
187
+ * `null` outside a git repository: nothing to key the files by. Rejected
188
+ * pathspecs reject, so a bad `--on` is an error, never a run without a key.
189
+ */
190
+ const keyOf = (cwd, on) => treeKeyOf(cwd, { on }).catch((error) => {
191
+ if (error instanceof OncePathspecError) throw error;
192
+ return null;
193
+ });
194
+ /** Runs `run` unless the check already passed on these files. */
195
+ const runOnce = async ({ cwd, name, covers = [], withHead = false, on = [], force = false, who = "you", run, env = process.env, node = process.version }) => {
196
+ if (env["ORK_ONCE"] === "off") return {
197
+ skipped: false,
198
+ code: await run()
199
+ };
200
+ const before = await keyOf(cwd, on);
201
+ const earlier = before === null || force ? null : findPass({
202
+ key: before,
203
+ name,
204
+ withHead,
205
+ node
206
+ });
207
+ if (earlier !== null) return {
208
+ skipped: true,
209
+ pass: earlier,
210
+ code: 0
211
+ };
212
+ const code = await run();
213
+ if (code !== 0 || before === null) return {
214
+ skipped: false,
215
+ code
216
+ };
217
+ const after = await keyOf(cwd, on).catch(() => null);
218
+ if (after === null || filesOf(after) !== filesOf(before) || after.head !== before.head) return {
219
+ skipped: false,
220
+ code
221
+ };
222
+ return {
223
+ skipped: false,
224
+ pass: recordPass({
225
+ key: before,
226
+ name,
227
+ covers,
228
+ who,
229
+ node
230
+ }),
231
+ code
232
+ };
233
+ };
234
+ export { OncePathspecError as n, runOnce as t };
@@ -0,0 +1,104 @@
1
+ import { R as openStore, et as TOO_LONG, nt as sendFromScript, tt as scriptRefusal } from "./src-BuF9Lbow.mjs";
2
+ import { u as inheritedScrubber } from "./src-k0A_QpST.mjs";
3
+ import { l as writeErr } from "./output-DXZbnFt_.mjs";
4
+ import { defineCommand } from "citty";
5
+ // src/commands/send.ts
6
+ /**
7
+ * @file `ork send` (RFC-005): a program in a tool node writes to a session on
8
+ * its run's bus. It prints nothing when it works, as what a program prints is
9
+ * its node's output, and says why on stderr, with exit 1, when it is refused:
10
+ * before reading a message it could not send, and for anything wrong with the
11
+ * command itself, which would otherwise go to stdout as usage.
12
+ */
13
+ const refuse = (message) => {
14
+ writeErr(message);
15
+ process.exitCode = 1;
16
+ };
17
+ /** Stdin, up to the limit: past it, the rest is not worth holding. */
18
+ const readBounded = async () => {
19
+ const chunks = [];
20
+ let size = 0;
21
+ for await (const chunk of process.stdin) {
22
+ const part = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
23
+ size += part.length;
24
+ if (size > 65536) return null;
25
+ chunks.push(part);
26
+ }
27
+ return Buffer.concat(chunks);
28
+ };
29
+ /** The message, or why it can't go. */
30
+ const bodyOf = async (given) => {
31
+ if (given !== void 0) return { body: given };
32
+ const read = await readBounded();
33
+ if (read === null) return { refused: TOO_LONG };
34
+ try {
35
+ return { body: new TextDecoder("utf-8", { fatal: true }).decode(read) };
36
+ } catch {
37
+ return { refused: "The message is not UTF-8 text." };
38
+ }
39
+ };
40
+ const deliver = async (store, to, given) => {
41
+ const sender = {
42
+ run: process.env["ORK_RUN"],
43
+ node: process.env["ORK_NODE"],
44
+ to
45
+ };
46
+ const refused = scriptRefusal(store, sender);
47
+ if (refused !== null) {
48
+ refuse(refused);
49
+ return;
50
+ }
51
+ const message = await bodyOf(given);
52
+ if ("refused" in message) {
53
+ refuse(message.refused);
54
+ return;
55
+ }
56
+ if (message.body.replaceAll(/[\p{Cc}\s]/gu, "") === "") {
57
+ refuse("The message is empty.");
58
+ return;
59
+ }
60
+ const sent = await sendFromScript(store, {
61
+ ...sender,
62
+ body: inheritedScrubber(process.env)(message.body)
63
+ });
64
+ if (!sent.ok) refuse(sent.error);
65
+ };
66
+ const send = defineCommand({
67
+ meta: {
68
+ name: "send",
69
+ description: "From a tool node's program: write to a session in its run"
70
+ },
71
+ args: {
72
+ to: {
73
+ type: "positional",
74
+ description: "The node to write to: the owner, or a channel",
75
+ required: false
76
+ },
77
+ message: {
78
+ type: "string",
79
+ description: "The message"
80
+ },
81
+ stdin: {
82
+ type: "boolean",
83
+ description: "Read the message from stdin",
84
+ default: false
85
+ }
86
+ },
87
+ run: async ({ args }) => {
88
+ if (args.to === void 0 || args._.length !== 1) {
89
+ refuse("Give one node to write to: ork send <to> -m <body>.");
90
+ return;
91
+ }
92
+ if (args.message === void 0 === !args.stdin) {
93
+ refuse("Give the message with -m or --stdin, one of them.");
94
+ return;
95
+ }
96
+ const store = openStore();
97
+ try {
98
+ await deliver(store, args.to, args.message);
99
+ } finally {
100
+ store.close();
101
+ }
102
+ }
103
+ });
104
+ export { send };
@@ -0,0 +1 @@
1
+ export { a as withShortFlags } from "./short-flags-Qf21as0E.mjs";