@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.
- package/README.md +12 -7
- package/dist/__build_info__-0wqQ-rtf.mjs +180 -0
- package/dist/apply-Mb0fNpCX.mjs +17 -0
- package/dist/argument-error-CihH46Le.mjs +27 -0
- package/dist/attach-C0-YyNoY.mjs +92 -0
- package/dist/attach-CAKNqnUK.mjs +1259 -0
- package/dist/bin.mjs +1885 -1609
- package/dist/brief-C2hew8cS.mjs +116 -0
- package/dist/{decide-CGyx9rv7.mjs → builtin-files-YYAnbZGD.mjs} +3 -51
- package/dist/chips-CP_pgKlj.mjs +48 -0
- package/dist/command-tree-CEkzWNy6.mjs +63 -0
- package/dist/{completion-CSU5vkEt.mjs → completion-K4rAVsrf.mjs} +200 -30
- package/dist/context-B-fjPJky.mjs +106 -0
- package/dist/context-DMK0U1w0.mjs +160 -0
- package/dist/detached-q0AYG0Bs.mjs +39 -0
- package/dist/extra-arguments-BxQMXARO.mjs +13 -0
- package/dist/{find-D6Gj1sxg.mjs → find-Ct5CNl1J.mjs} +2 -2
- package/dist/flags-C90N4J1i.mjs +145 -0
- package/dist/follow-ups-D3HPyNYM.mjs +568 -0
- package/dist/format-CiPhQqbX.mjs +260 -0
- package/dist/{help-groups-BwYoAWsh.mjs → help-groups-BQvcKQ9H.mjs} +49 -78
- package/dist/in-a-run-D_wg4iJi.mjs +27 -0
- package/dist/install-Hji6LIei.mjs +259 -0
- package/dist/{limits-B1-DYc3d.mjs → limits-BD6RZRCS.mjs} +2 -2
- package/dist/{mcp-DE_EmyVm.mjs → mcp-CpbDmBLG.mjs} +6 -6
- package/dist/once-J8Qn5fTG.mjs +112 -0
- package/dist/orphan-guard-BFKg8VSd.mjs +58 -0
- package/dist/output-DXZbnFt_.mjs +458 -0
- package/dist/outputs-text-CNTJ7Qq_.mjs +19 -0
- package/dist/{pause-CL6N_Xrj.mjs → pause-Cl4ZYA35.mjs} +7 -4
- package/dist/print-DzugGZQB.mjs +19 -0
- package/dist/project-records-CtkmlsHN.mjs +233 -0
- package/dist/refusals-DQQX3AIb.mjs +116 -0
- package/dist/{relay-0SD0erwf.mjs → relay-CiP77vga.mjs} +2 -2
- package/dist/run-CtuNY3mE.mjs +234 -0
- package/dist/send-DRYGks8K.mjs +104 -0
- package/dist/short-flags-DkJLWVqh.mjs +1 -0
- package/dist/short-flags-Qf21as0E.mjs +83 -0
- package/dist/src-BMFQKHz4.mjs +3122 -0
- package/dist/{src-BbyEoMn4.mjs → src-BrFfDKux.mjs} +800 -235
- package/dist/{src-Dujmvv4s.mjs → src-BuF9Lbow.mjs} +12580 -5505
- package/dist/{src-DOcK8Jfp.mjs → src-k0A_QpST.mjs} +1024 -590
- package/dist/store-rTgRu8xy.mjs +25 -0
- package/dist/ui/THIRD_PARTY_LICENSES.txt +612 -0
- package/dist/ui/assets/app-BYxpKJRh.css +1 -0
- package/dist/ui/assets/app-SEFEJLkF.js +112 -0
- package/dist/ui/assets/{index-9dFha9Qn.js → index-Cb6Voq_W.js} +3 -3
- package/dist/ui/assets/theme-CdXvqTQ6.js +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/{ui-CTmO2Xzu.mjs → ui-C2f37TSb.mjs} +57 -25
- package/dist/usage-BuvGcQSk.mjs +256 -0
- package/dist/validate-ObP5lt29.mjs +1 -0
- package/dist/validate-_HjACtk9.mjs +632 -0
- package/dist/verdict-B592j1W3.mjs +84 -0
- package/dist/{watch-CtbgDAcz.mjs → watch-DV3ixCYj.mjs} +45 -17
- package/dist/{workflows-D1_WVukk.mjs → workflows-QLjBhGNS.mjs} +30 -5
- package/dist/worktrees-D3ubG_Qw.mjs +302 -0
- package/dist/yolo-BSH1E_kE.mjs +33 -0
- package/package.json +10 -8
- package/dist/__build_info__-fy6L1Mrd.mjs +0 -615
- package/dist/clean-DaHjs__2.mjs +0 -88
- package/dist/context-DT8MfEIR.mjs +0 -274
- package/dist/format-Dnfv8zMT.mjs +0 -427
- package/dist/in-a-run-pLAZeNys.mjs +0 -22
- package/dist/list-BrH8KRSJ.mjs +0 -110
- package/dist/output-BLQWD6MJ.mjs +0 -950
- package/dist/remove-BeGdqroy.mjs +0 -194
- package/dist/report-DK4LGqwF.mjs +0 -129
- package/dist/send-C8tunWhv.mjs +0 -163
- package/dist/src-CCUKRX51.mjs +0 -1204
- package/dist/ui/assets/app-BNT0SuIz.css +0 -1
- package/dist/ui/assets/app-T7wVAhgx.js +0 -67
- package/dist/ui/assets/theme-3xH5jxKs.js +0 -1
- package/dist/usage-DYUmP0i4.mjs +0 -419
- 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 {
|
|
2
|
-
import {
|
|
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";
|