@krapht/ork 0.2.0 → 0.4.0
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/THIRD_PARTY_NOTICES.md +7 -0
- package/dist/__build_info__-CtSVVuZE.mjs +691 -0
- package/dist/bin.mjs +1654 -3532
- package/dist/clean-Dcr807be.mjs +88 -0
- package/dist/context-C_bksb0C.mjs +351 -0
- package/dist/find-DptmbhGd.mjs +47 -0
- package/dist/format-J-a21Yvn.mjs +393 -0
- package/dist/in-a-run-D1Qc5Mit.mjs +22 -0
- package/dist/limits-BDZCmGEu.mjs +44 -0
- package/dist/list-ZvY2OcLq.mjs +110 -0
- package/dist/mcp-CHVKKg3B.mjs +91 -0
- package/dist/output-CXSJO00X.mjs +900 -0
- package/dist/pause-BPSmg4yz.mjs +65 -0
- package/dist/relay-Do-aNbSD.mjs +122 -0
- package/dist/remove-AwaMMaHf.mjs +194 -0
- package/dist/report-DK4LGqwF.mjs +129 -0
- package/dist/send-DhGBkowy.mjs +163 -0
- package/dist/{src-DAsOYtoR.mjs → src-BHnxpJyH.mjs} +496 -193
- package/dist/src-CGqMati3.mjs +12505 -0
- package/dist/src-CpDKIulI.mjs +3118 -0
- package/dist/src-UWkk1sl9.mjs +1899 -0
- package/dist/ui/THIRD_PARTY_LICENSES.txt +631 -0
- package/dist/ui/assets/app-BgVDC_5l.js +67 -0
- package/dist/ui/assets/app-DVf68fj5.css +1 -0
- package/dist/ui/assets/geist-mono-cyrillic-400-normal-C51Di1Mf.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-400-normal-CPJFfJgk.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-500-normal-D2cXvDHF.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-500-normal-DfuvdVgn.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-400-normal-CkxHOdCb.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-400-normal-XB2BPmDK.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-500-normal-BY1KTvO5.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-500-normal-C7aYOB1D.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-400-normal-B2yvC1Cq.woff +0 -0
- package/dist/ui/assets/geist-mono-latin-400-normal-DTRLJnHl.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-500-normal-D-GG86Jb.woff +0 -0
- package/dist/ui/assets/geist-mono-latin-500-normal-YINYabwD.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-400-normal-CfzLURNc.woff +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-400-normal-DI-rJ0UV.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-500-normal-Buglb9-a.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-500-normal-CrWEgvU_.woff +0 -0
- package/dist/ui/assets/geist-mono-symbols2-400-normal-BS9BGVxz.woff2 +0 -0
- package/dist/ui/assets/geist-mono-symbols2-400-normal-DjbiTEEA.woff +0 -0
- package/dist/ui/assets/geist-mono-symbols2-500-normal-CmnCPGa0.woff +0 -0
- package/dist/ui/assets/geist-mono-symbols2-500-normal-Q_ksOahU.woff2 +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-400-normal-CKEaXIlx.woff2 +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-400-normal-CZEPLOgu.woff +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-500-normal-Bs29RrUZ.woff2 +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-500-normal-Dx9-epDo.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-400-normal-BOaIZNA2.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-400-normal-gapTbOY8.woff2 +0 -0
- package/dist/ui/assets/geist-sans-latin-500-normal-CN2lyvyL.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-500-normal-uokXdC-Q.woff2 +0 -0
- package/dist/ui/assets/geist-sans-latin-600-normal-CA1yjETN.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-600-normal-DFOURf8L.woff2 +0 -0
- package/dist/ui/assets/index-BiO54L90.css +1 -0
- package/dist/ui/assets/index-MQNNgsyT.js +10 -0
- package/dist/ui/assets/theme-3xH5jxKs.js +1 -0
- package/dist/ui/favicon.ico +0 -0
- package/dist/ui/favicon.svg +4 -0
- package/dist/ui/index.html +30 -0
- package/dist/ui/ork-wordmark-small-on-light.svg +4 -0
- package/dist/ui/ork-wordmark-small.svg +4 -0
- package/dist/ui-DhutUMgQ.mjs +472 -0
- package/dist/usage-CT7Z-yjr.mjs +419 -0
- package/dist/watch-0Qr2UfCQ.mjs +394 -0
- package/dist/worktrees-DHl80k96.mjs +212 -0
- package/package.json +6 -4
- package/dist/src-CJ_zF0Zj.mjs +0 -5433
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { C as watchingPid, Mn as CheckReportSchema, S as takeWatchLock, Un as timeoutMs, Vn as isToolNode, _ as createWatcher, b as readProjectTriggers, dn as placeFiles, g as triggersReport, ht as stillTheSameProcess, lt as liveStatus, rt as detachRun, v as triggerStanding, y as declaredTriggers } from "./src-CGqMati3.mjs";
|
|
2
|
+
import { n as newRunId } from "./find-DptmbhGd.mjs";
|
|
3
|
+
import { a as write, c as openStore, i as style, o as writeErr, r as plain, t as confirm } from "./output-CXSJO00X.mjs";
|
|
4
|
+
import { i as runLimits, r as limitsRefusal } from "./limits-BDZCmGEu.mjs";
|
|
5
|
+
import { c as toolExecutor, o as spawnTool, s as mcpCallExecutor } from "./src-UWkk1sl9.mjs";
|
|
6
|
+
import { t as acceptRefusal } from "./in-a-run-D1Qc5Mit.mjs";
|
|
7
|
+
import { defineCommand } from "citty";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { mkdirSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
const NO_HUMAN = {
|
|
12
|
+
permit: () => Promise.resolve({
|
|
13
|
+
allow: false,
|
|
14
|
+
message: "no one is watching a check"
|
|
15
|
+
}),
|
|
16
|
+
ask: () => Promise.resolve("no one is watching a check")
|
|
17
|
+
};
|
|
18
|
+
/** A tool's result has to be read as JSON; an MCP call's already is. */
|
|
19
|
+
const asNode = (check) => isToolNode(check) ? {
|
|
20
|
+
...check,
|
|
21
|
+
output: { format: "json" }
|
|
22
|
+
} : check;
|
|
23
|
+
const resultOf = (json) => {
|
|
24
|
+
const report = CheckReportSchema.safeParse(json);
|
|
25
|
+
return report.success ? {
|
|
26
|
+
ok: true,
|
|
27
|
+
events: report.data.events
|
|
28
|
+
} : {
|
|
29
|
+
ok: false,
|
|
30
|
+
error: "the check must report { \"events\": [{ \"key\": \"...\", \"inputs\": { ... } }] }"
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
const lookAt = async (options) => {
|
|
34
|
+
const { place, id, signal } = options;
|
|
35
|
+
const node = asNode(options.check);
|
|
36
|
+
const workflow = {
|
|
37
|
+
schemaVersion: 1,
|
|
38
|
+
name: `trigger ${id}`,
|
|
39
|
+
nodes: { check: node }
|
|
40
|
+
};
|
|
41
|
+
const runDir = path.join(placeFiles(place).runs, ".triggers", id);
|
|
42
|
+
mkdirSync(runDir, { recursive: true });
|
|
43
|
+
const executor = node.type === "mcp.call" ? mcpCallExecutor() : toolExecutor(spawnTool);
|
|
44
|
+
const limit = AbortSignal.timeout(timeoutMs(node.timeout ?? "5m") ?? 0);
|
|
45
|
+
const result = await executor({
|
|
46
|
+
runId: `trigger-${id}`,
|
|
47
|
+
id: "check",
|
|
48
|
+
node,
|
|
49
|
+
workflow,
|
|
50
|
+
inputs: {},
|
|
51
|
+
outputs: {},
|
|
52
|
+
input: void 0,
|
|
53
|
+
round: 1,
|
|
54
|
+
cwd: place.root,
|
|
55
|
+
runDir,
|
|
56
|
+
workflowDir: place.dir,
|
|
57
|
+
projectRoot: place.root,
|
|
58
|
+
orkDir: place.dir,
|
|
59
|
+
budgetLeftUsd: void 0,
|
|
60
|
+
human: NO_HUMAN,
|
|
61
|
+
emit: () => {},
|
|
62
|
+
signal: AbortSignal.any([signal, limit])
|
|
63
|
+
});
|
|
64
|
+
return result.ok ? resultOf(result.output["json"]) : {
|
|
65
|
+
ok: false,
|
|
66
|
+
error: result.error ?? "the check failed"
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
// src/watch/report.ts
|
|
70
|
+
const STANDING = {
|
|
71
|
+
live: "live",
|
|
72
|
+
new: "off: new, not accepted",
|
|
73
|
+
changed: "off: changed since it was accepted",
|
|
74
|
+
invalid: "off: has an error"
|
|
75
|
+
};
|
|
76
|
+
const brokenLines = (broken) => broken.map(({ file, message }) => `${style("red", "✗")} ${plain(file)}: ${plain(message)}`);
|
|
77
|
+
const statusLines = (report) => {
|
|
78
|
+
const head = report.pid === void 0 ? "ork watch isn't running. Start it with `ork watch`, or `ork watch --detach`." : `ork watch is running (process ${String(report.pid)}).`;
|
|
79
|
+
const broken = brokenLines(report.broken);
|
|
80
|
+
if (report.triggers.length === 0) return [
|
|
81
|
+
head,
|
|
82
|
+
...broken,
|
|
83
|
+
"No triggers in any registered project."
|
|
84
|
+
];
|
|
85
|
+
return [
|
|
86
|
+
head,
|
|
87
|
+
...broken,
|
|
88
|
+
...report.triggers.flatMap((status) => [
|
|
89
|
+
`${style("bold", plain(status.trigger))} ${style("dim", plain(`${status.project} → ${status.run}, every ${status.every}`))}`,
|
|
90
|
+
` ${STANDING[status.standing] ?? status.standing}${status.lookedAt === void 0 ? "" : `, last looked ${status.lookedAt}`}`,
|
|
91
|
+
...status.error === void 0 ? [] : [` last look: ${plain(status.error)}`],
|
|
92
|
+
...status.held === void 0 ? [] : [` waiting: ${plain(status.held)}`],
|
|
93
|
+
...status.problems.map((problem) => ` ${style("red", "✗")} ${plain(problem)}`)
|
|
94
|
+
])
|
|
95
|
+
];
|
|
96
|
+
};
|
|
97
|
+
const asJson = (value) => plain(JSON.stringify(value));
|
|
98
|
+
/** What a check runs with, past its command. */
|
|
99
|
+
const checkSettings = ({ trigger }) => {
|
|
100
|
+
const { check } = trigger;
|
|
101
|
+
if (check === void 0) return [];
|
|
102
|
+
return [
|
|
103
|
+
..."cwd" in check && check.cwd !== void 0 ? [` in: ${plain(check.cwd)}`] : [],
|
|
104
|
+
...check.env === void 0 ? [] : [` env: ${asJson(check.env)}`],
|
|
105
|
+
..."arguments" in check && check.arguments !== void 0 ? [` arguments: ${asJson(check.arguments)}`] : []
|
|
106
|
+
];
|
|
107
|
+
};
|
|
108
|
+
/** What a trigger runs, in as many words as it takes to judge it. */
|
|
109
|
+
const checkLines = (item) => {
|
|
110
|
+
const { check } = item.trigger;
|
|
111
|
+
if (check === void 0) return [" no check: starts a run every time"];
|
|
112
|
+
if (check.type === "mcp.call") return [` looks with: ${plain(check.server)}.${plain(check.tool)}, from the project's .mcp.json`, ...checkSettings(item)];
|
|
113
|
+
return [
|
|
114
|
+
` looks with: ${plain(check.type)} ${plain(check.command ?? "")}`.trimEnd(),
|
|
115
|
+
...check.script === void 0 ? [] : check.script.trimEnd().split("\n").map((line) => ` ${plain(line)}`),
|
|
116
|
+
...checkSettings(item)
|
|
117
|
+
];
|
|
118
|
+
};
|
|
119
|
+
/** What the run does, since a changed workflow looks the same from outside. */
|
|
120
|
+
const workflowLines = ({ workflow, workflowFile }) => {
|
|
121
|
+
if (workflow === null) return [];
|
|
122
|
+
const budget = workflow.limits?.budgetUsd;
|
|
123
|
+
return [` workflow: ${plain(workflowFile)}${budget === void 0 ? "" : `, up to $${String(budget)} a run`}`, ...Object.entries(workflow.nodes).map(([id, node]) => ` ${plain(id)}: ${plain(node.type)}`)];
|
|
124
|
+
};
|
|
125
|
+
const STANDS = {
|
|
126
|
+
new: "new",
|
|
127
|
+
changed: "changed since you accepted it, here or in the workflow it starts"
|
|
128
|
+
};
|
|
129
|
+
const acceptLines = (item, standing) => [
|
|
130
|
+
`${style("bold", plain(item.id))} ${style("dim", plain(item.place.root))}`,
|
|
131
|
+
` ${STANDS[standing] ?? standing}`,
|
|
132
|
+
...checkLines(item),
|
|
133
|
+
` starts: ${plain(item.trigger.run)}, looking every ${item.trigger.every}, with up to ${String(item.trigger.maxRunsPerDay ?? 10)} runs a day`,
|
|
134
|
+
...workflowLines(item),
|
|
135
|
+
...item.diagnostics.filter(({ severity }) => severity === "warning").map(({ message }) => ` ${style("yellow", "!")} ${plain(message)}`)
|
|
136
|
+
];
|
|
137
|
+
/** The triggers `--accept` can't offer, so they aren't silently left out. */
|
|
138
|
+
const unacceptableLines = (item) => [`${style("bold", plain(item.id))} ${style("dim", plain(item.place.root))}`, ...item.diagnostics.filter(({ severity }) => severity === "error").map(({ message }) => ` ${style("red", "✗")} ${plain(message)}`)];
|
|
139
|
+
// src/watch/start.ts
|
|
140
|
+
/**
|
|
141
|
+
* @file Starting a trigger's run (RFC-011): an ordinary run in the background,
|
|
142
|
+
* so everything else works on it. It runs the file that was accepted, by its
|
|
143
|
+
* whole path and only if its text is still what was accepted, so nothing a
|
|
144
|
+
* branch adds or a pull changes runs in its place. What a check reported
|
|
145
|
+
* can't become extra arguments: each value is joined to its flag, and its
|
|
146
|
+
* name must be an input the workflow declares.
|
|
147
|
+
*/
|
|
148
|
+
/** What a run gets, and which of it came from what the check reported. */
|
|
149
|
+
const inputsFor = (trigger, event) => {
|
|
150
|
+
const reported = Object.fromEntries(Object.entries(event.inputs ?? {}).map(([name, value]) => [name, String(value)]));
|
|
151
|
+
const own = trigger.inputs ?? {};
|
|
152
|
+
return {
|
|
153
|
+
inputs: {
|
|
154
|
+
...reported,
|
|
155
|
+
...own
|
|
156
|
+
},
|
|
157
|
+
untrusted: Object.keys(reported).filter((name) => !Object.hasOwn(own, name))
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
/** How the run is started, with each value joined to its flag. */
|
|
161
|
+
const triggerRunArgs = (options) => [
|
|
162
|
+
"run",
|
|
163
|
+
options.workflowFile,
|
|
164
|
+
`--expect-workflow=${options.workflowHash}`,
|
|
165
|
+
...Object.entries(options.inputs).map(([name, value]) => `--input=${name}=${value}`),
|
|
166
|
+
`--trigger=${options.id}`,
|
|
167
|
+
...options.untrusted.map((name) => `--untrusted=${name}`),
|
|
168
|
+
`--run-id=${options.runId}`
|
|
169
|
+
];
|
|
170
|
+
const startTriggerRun = async (options) => {
|
|
171
|
+
const { store, place, id, trigger, workflow, event, runId } = options;
|
|
172
|
+
const { workflowFile, workflowHash } = options;
|
|
173
|
+
const { inputs, untrusted } = inputsFor(trigger, event);
|
|
174
|
+
const unknown = Object.keys(inputs).find((name) => !Object.hasOwn(workflow.inputs ?? {}, name));
|
|
175
|
+
if (unknown !== void 0) return {
|
|
176
|
+
ok: false,
|
|
177
|
+
error: `the check reported input \`${unknown}\`, which ${trigger.run} doesn't declare`
|
|
178
|
+
};
|
|
179
|
+
const detached = await detachRun({
|
|
180
|
+
store,
|
|
181
|
+
runId,
|
|
182
|
+
cwd: place.root,
|
|
183
|
+
runDir: path.join(placeFiles(place).runs, runId),
|
|
184
|
+
args: triggerRunArgs({
|
|
185
|
+
id,
|
|
186
|
+
workflowFile,
|
|
187
|
+
workflowHash,
|
|
188
|
+
inputs,
|
|
189
|
+
untrusted,
|
|
190
|
+
runId
|
|
191
|
+
})
|
|
192
|
+
});
|
|
193
|
+
return detached.ok ? { ok: true } : {
|
|
194
|
+
ok: false,
|
|
195
|
+
error: detached.error
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
// src/commands/watch.ts
|
|
199
|
+
/**
|
|
200
|
+
* @file `ork watch` (RFC-011): looks at what each registered project's triggers
|
|
201
|
+
* declare, and starts a run for each new thing they report. One per machine,
|
|
202
|
+
* in the foreground or with `--detach`; Ork installs nothing to keep it
|
|
203
|
+
* alive. It runs only what was accepted here, and accepting takes a terminal,
|
|
204
|
+
* so a browser tab can't approve code to run with nobody there, nor a program
|
|
205
|
+
* by accident.
|
|
206
|
+
*/
|
|
207
|
+
/** How often it sees what's due. A trigger looks once a minute at most. */
|
|
208
|
+
const TICK_MS = 1e4;
|
|
209
|
+
const watchLoop = async (store) => {
|
|
210
|
+
const lock = takeWatchLock(store);
|
|
211
|
+
if (!lock.ok) {
|
|
212
|
+
writeErr(`ork watch is already running (process ${String(lock.pid)}).`);
|
|
213
|
+
return 1;
|
|
214
|
+
}
|
|
215
|
+
const stopping = new AbortController();
|
|
216
|
+
const watcher = createWatcher({
|
|
217
|
+
store,
|
|
218
|
+
declared: () => declaredTriggers(store.projects()),
|
|
219
|
+
look: ({ place, id, trigger }) => trigger.check === void 0 ? Promise.resolve({
|
|
220
|
+
ok: true,
|
|
221
|
+
events: []
|
|
222
|
+
}) : lookAt({
|
|
223
|
+
place,
|
|
224
|
+
id,
|
|
225
|
+
check: trigger.check,
|
|
226
|
+
signal: stopping.signal
|
|
227
|
+
}),
|
|
228
|
+
start: ({ place, id, trigger, workflow, workflowFile, workflowHash }, event, runId) => startTriggerRun({
|
|
229
|
+
store,
|
|
230
|
+
place,
|
|
231
|
+
id,
|
|
232
|
+
trigger,
|
|
233
|
+
workflow,
|
|
234
|
+
workflowFile,
|
|
235
|
+
workflowHash,
|
|
236
|
+
event,
|
|
237
|
+
runId
|
|
238
|
+
}),
|
|
239
|
+
newRunId: () => newRunId(store),
|
|
240
|
+
isRunning: (runId) => {
|
|
241
|
+
const [run] = store.lookup(runId);
|
|
242
|
+
if (run === void 0 || liveStatus(run) !== "running") return false;
|
|
243
|
+
const last = store.events(run.id).at(-1)?.at ?? run.startedAt;
|
|
244
|
+
return stillTheSameProcess(run.pid, last);
|
|
245
|
+
},
|
|
246
|
+
blocked: ({ place, workflow }) => limitsRefusal(runLimits(store, place), workflow)[0],
|
|
247
|
+
note: (line) => {
|
|
248
|
+
write(`${style("dim", (/* @__PURE__ */ new Date()).toISOString())} ${plain(line)}`);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
const { promise: stopped, resolve: stop } = Promise.withResolvers();
|
|
252
|
+
const onSignal = () => {
|
|
253
|
+
stop();
|
|
254
|
+
};
|
|
255
|
+
process.once("SIGINT", onSignal);
|
|
256
|
+
process.once("SIGTERM", onSignal);
|
|
257
|
+
write("▸ watching. `ork watch --status` says what's live.");
|
|
258
|
+
watcher.tick();
|
|
259
|
+
const timer = setInterval(() => {
|
|
260
|
+
watcher.tick();
|
|
261
|
+
}, TICK_MS);
|
|
262
|
+
await stopped;
|
|
263
|
+
clearInterval(timer);
|
|
264
|
+
stopping.abort();
|
|
265
|
+
await watcher.settled();
|
|
266
|
+
lock.release();
|
|
267
|
+
return 0;
|
|
268
|
+
};
|
|
269
|
+
const detach = async (store) => {
|
|
270
|
+
const running = watchingPid(store);
|
|
271
|
+
if (running !== void 0) {
|
|
272
|
+
writeErr(`ork watch is already running (process ${String(running)}).`);
|
|
273
|
+
return 1;
|
|
274
|
+
}
|
|
275
|
+
const started = await detachRun({
|
|
276
|
+
store,
|
|
277
|
+
runId: "watch",
|
|
278
|
+
cwd: homedir(),
|
|
279
|
+
runDir: path.join(homedir(), ".ork", "watch"),
|
|
280
|
+
args: ["watch"],
|
|
281
|
+
started: (pid) => pid !== void 0 && watchingPid(store) === pid
|
|
282
|
+
});
|
|
283
|
+
if (!started.ok) {
|
|
284
|
+
writeErr(started.error);
|
|
285
|
+
return 1;
|
|
286
|
+
}
|
|
287
|
+
write(`▸ watching in the background; its output is in ${started.log}`);
|
|
288
|
+
return 0;
|
|
289
|
+
};
|
|
290
|
+
const stopWatching = (store) => {
|
|
291
|
+
const pid = watchingPid(store);
|
|
292
|
+
if (pid === void 0) {
|
|
293
|
+
write("ork watch isn't running.");
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
process.kill(pid, "SIGTERM");
|
|
297
|
+
write(`Told ork watch (process ${String(pid)}) to stop.`);
|
|
298
|
+
return 0;
|
|
299
|
+
};
|
|
300
|
+
const hasError = ({ diagnostics }) => diagnostics.some(({ severity }) => severity === "error");
|
|
301
|
+
/** Asks about each trigger that's new or changed, in this terminal. */
|
|
302
|
+
const accept = async (store) => {
|
|
303
|
+
const refused = acceptRefusal();
|
|
304
|
+
if (refused !== null) {
|
|
305
|
+
writeErr(refused);
|
|
306
|
+
return 1;
|
|
307
|
+
}
|
|
308
|
+
if (!process.stdin.isTTY) {
|
|
309
|
+
writeErr("`ork watch --accept` asks in a terminal: it approves code to run with nobody there.");
|
|
310
|
+
return 1;
|
|
311
|
+
}
|
|
312
|
+
const projects = store.projects();
|
|
313
|
+
const read = projects.map((place) => ({
|
|
314
|
+
place,
|
|
315
|
+
found: readProjectTriggers(place, void 0, projects)
|
|
316
|
+
}));
|
|
317
|
+
for (const { place, found } of read) {
|
|
318
|
+
const broken = found.diagnostics.map(({ message }) => ({
|
|
319
|
+
project: place.id ?? "",
|
|
320
|
+
file: found.file,
|
|
321
|
+
message
|
|
322
|
+
}));
|
|
323
|
+
for (const line of brokenLines(broken)) write(line);
|
|
324
|
+
for (const item of found.triggers.filter(hasError)) for (const line of unacceptableLines(item)) write(line);
|
|
325
|
+
}
|
|
326
|
+
const waiting = read.flatMap(({ place, found }) => {
|
|
327
|
+
const records = store.triggerRecords(place.id);
|
|
328
|
+
return found.triggers.flatMap((item) => {
|
|
329
|
+
const standing = triggerStanding(item, records.find(({ trigger }) => trigger === item.id));
|
|
330
|
+
return hasError(item) || standing === "live" ? [] : [{
|
|
331
|
+
item,
|
|
332
|
+
standing
|
|
333
|
+
}];
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
if (waiting.length === 0) {
|
|
337
|
+
write("No new or changed triggers to accept.");
|
|
338
|
+
return 0;
|
|
339
|
+
}
|
|
340
|
+
for (const { item, standing } of waiting) {
|
|
341
|
+
for (const line of acceptLines(item, standing)) write(line);
|
|
342
|
+
if (await confirm("Accept this trigger, to run with nobody there? [y/N] ")) {
|
|
343
|
+
const project = item.place.id;
|
|
344
|
+
if (project !== void 0) {
|
|
345
|
+
store.acceptTrigger(project, item.id, item.hash);
|
|
346
|
+
write(`Accepted ${plain(item.id)}; its first look starts nothing.`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return 0;
|
|
351
|
+
};
|
|
352
|
+
const watch = defineCommand({
|
|
353
|
+
meta: {
|
|
354
|
+
name: "watch",
|
|
355
|
+
description: "Look at each project's triggers, and start a run for each new thing they report"
|
|
356
|
+
},
|
|
357
|
+
args: {
|
|
358
|
+
detach: {
|
|
359
|
+
type: "boolean",
|
|
360
|
+
description: "Watch in the background",
|
|
361
|
+
default: false
|
|
362
|
+
},
|
|
363
|
+
status: {
|
|
364
|
+
type: "boolean",
|
|
365
|
+
description: "Say what's live, and when each trigger last looked",
|
|
366
|
+
default: false
|
|
367
|
+
},
|
|
368
|
+
accept: {
|
|
369
|
+
type: "boolean",
|
|
370
|
+
description: "Accept new or changed triggers, in a terminal",
|
|
371
|
+
default: false
|
|
372
|
+
},
|
|
373
|
+
stop: {
|
|
374
|
+
type: "boolean",
|
|
375
|
+
description: "Stop the ork watch that's running",
|
|
376
|
+
default: false
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
run: async ({ args }) => {
|
|
380
|
+
const store = openStore();
|
|
381
|
+
try {
|
|
382
|
+
if (args.status) {
|
|
383
|
+
const report = triggersReport(store, store.projects());
|
|
384
|
+
for (const line of statusLines(report)) write(line);
|
|
385
|
+
} else if (args.stop) process.exitCode = stopWatching(store);
|
|
386
|
+
else if (args.accept) process.exitCode = await accept(store);
|
|
387
|
+
else if (args.detach) process.exitCode = await detach(store);
|
|
388
|
+
else process.exitCode = await watchLoop(store);
|
|
389
|
+
} finally {
|
|
390
|
+
store.close();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
export { watch };
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { T as inspectProject } from "./src-CGqMati3.mjs";
|
|
2
|
+
import { a as write, c as openStore, i as style, o as writeErr, t as confirm } from "./output-CXSJO00X.mjs";
|
|
3
|
+
import { a as planClean, i as keyOf, o as stepActs, t as applyClean } from "./clean-Dcr807be.mjs";
|
|
4
|
+
import { a as readWorktrees, c as planLine, n as lostWork, o as labelOf, r as planRemove, s as mergeStateOf, t as applyRemove, u as stateParts } from "./remove-AwaMMaHf.mjs";
|
|
5
|
+
import { defineCommand } from "citty";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
// src/project/worktrees.ts
|
|
8
|
+
/**
|
|
9
|
+
* @file What `ork worktrees` prints: each worktree or branch a run left, with
|
|
10
|
+
* its state read from git, the plan `ork worktrees clean` or `remove` would
|
|
11
|
+
* carry out, and what a forced remove would lose. The words are core's
|
|
12
|
+
* (worktree/words.ts), which the Worktrees panel says too; this only colors
|
|
13
|
+
* them.
|
|
14
|
+
*/
|
|
15
|
+
/** One line of state: its merge state, then what clean would care about. */
|
|
16
|
+
const worktreeState = (item) => {
|
|
17
|
+
const merge = mergeStateOf(item);
|
|
18
|
+
const head = merge === "merged" ? [style("green", "merged")] : merge === "detached" ? ["detached"] : [];
|
|
19
|
+
const parts = stateParts(item).map((part) => part.risk ? style("yellow", part.text) : part.text);
|
|
20
|
+
return [...head, ...parts].join(" · ");
|
|
21
|
+
};
|
|
22
|
+
/** A name as the list shows it, saying when it's detached. */
|
|
23
|
+
const nameOf = (item) => mergeStateOf(item) === "detached" ? `${labelOf(item)} (detached)` : labelOf(item);
|
|
24
|
+
const formatWorktrees = (items) => {
|
|
25
|
+
if (items.length === 0) return ["No Ork worktrees or branches."];
|
|
26
|
+
const width = Math.max(...items.map((i) => nameOf(i).length));
|
|
27
|
+
return items.flatMap((item) => [`${style("bold", nameOf(item).padEnd(width))} ${worktreeState(item)}`, ...item.path === null ? [] : [` ${style("dim", item.path)}`]]);
|
|
28
|
+
};
|
|
29
|
+
const formatPlan = (steps) => steps.map((step) => {
|
|
30
|
+
const { text, reason } = planLine(step);
|
|
31
|
+
return ` ${text}${reason === void 0 ? "" : style("dim", ` (${reason})`)}`;
|
|
32
|
+
});
|
|
33
|
+
/** What a forced remove loses, as git prints it, under a heading each. */
|
|
34
|
+
const formatLost = (lost) => [...lost.changes.length === 0 ? [] : ["Uncommitted changes:", ...lost.changes.map((line) => ` ${line}`)], ...lost.commits.length === 0 ? [] : [lost.commitsOn === "no branch" ? "Commits on no branch:" : "Commits not merged:", ...lost.commits.map((line) => ` ${line}`)]];
|
|
35
|
+
// src/commands/worktrees.ts
|
|
36
|
+
/**
|
|
37
|
+
* @file `ork worktrees` (or `ork wt`): lists the worktrees and branches runs
|
|
38
|
+
* left behind (those the runs recorded creating, whatever the project's git
|
|
39
|
+
* rules named them, and any `ork/` branch), with their state read from git;
|
|
40
|
+
* cleans them up safely after showing the plan and asking; and removes one,
|
|
41
|
+
* the twin of the Worktrees panel's Remove…. The plans are core's, so the
|
|
42
|
+
* panel keeps and removes exactly what these do.
|
|
43
|
+
*/
|
|
44
|
+
/** The repo this folder is in, and what Ork created there; null outside one. */
|
|
45
|
+
const here = async () => {
|
|
46
|
+
const root = (await inspectProject(process.cwd()))?.root ?? null;
|
|
47
|
+
if (root === null) {
|
|
48
|
+
writeErr("Not in a git repository with a commit.");
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const store = openStore();
|
|
53
|
+
try {
|
|
54
|
+
return {
|
|
55
|
+
root,
|
|
56
|
+
...await readWorktrees(store, root)
|
|
57
|
+
};
|
|
58
|
+
} finally {
|
|
59
|
+
store.close();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
/** Asks before acting, unless `--yes`; without a terminal `--yes` is needed. */
|
|
63
|
+
const goAhead = (yes, verb) => {
|
|
64
|
+
if (yes) return Promise.resolve(true);
|
|
65
|
+
if (!process.stdin.isTTY) {
|
|
66
|
+
writeErr(`Pass --yes to ${verb} without a terminal.`);
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
return Promise.resolve(false);
|
|
69
|
+
}
|
|
70
|
+
return confirm("Go ahead? [y/N] ");
|
|
71
|
+
};
|
|
72
|
+
const done = (lines) => {
|
|
73
|
+
for (const line of lines) write(`${style("green", "✓")} ${line}`);
|
|
74
|
+
};
|
|
75
|
+
const show = async (json) => {
|
|
76
|
+
const found = await here();
|
|
77
|
+
if (found === null) return;
|
|
78
|
+
if (json) write(JSON.stringify(found.items, void 0, 2));
|
|
79
|
+
else for (const line of formatWorktrees(found.items)) write(line);
|
|
80
|
+
};
|
|
81
|
+
const list = defineCommand({
|
|
82
|
+
meta: {
|
|
83
|
+
name: "list",
|
|
84
|
+
description: "List Ork's worktrees and branches (the default)"
|
|
85
|
+
},
|
|
86
|
+
args: { json: {
|
|
87
|
+
type: "boolean",
|
|
88
|
+
description: "Print JSON",
|
|
89
|
+
default: false
|
|
90
|
+
} },
|
|
91
|
+
run: ({ args }) => show(args.json)
|
|
92
|
+
});
|
|
93
|
+
const yes = {
|
|
94
|
+
type: "boolean",
|
|
95
|
+
description: "Do not ask",
|
|
96
|
+
default: false
|
|
97
|
+
};
|
|
98
|
+
const clean = defineCommand({
|
|
99
|
+
meta: {
|
|
100
|
+
name: "clean",
|
|
101
|
+
description: "Remove Ork's worktrees (merged ones by default); never one with uncommitted changes unless --force, nor one whose run is still going"
|
|
102
|
+
},
|
|
103
|
+
args: {
|
|
104
|
+
run: {
|
|
105
|
+
type: "string",
|
|
106
|
+
description: "Only this run's worktrees"
|
|
107
|
+
},
|
|
108
|
+
all: {
|
|
109
|
+
type: "boolean",
|
|
110
|
+
description: "Include worktrees whose branch is not merged",
|
|
111
|
+
default: false
|
|
112
|
+
},
|
|
113
|
+
branches: {
|
|
114
|
+
type: "boolean",
|
|
115
|
+
description: "Also delete the branches",
|
|
116
|
+
default: false
|
|
117
|
+
},
|
|
118
|
+
force: {
|
|
119
|
+
type: "boolean",
|
|
120
|
+
description: "Remove uncommitted changes and unmerged branches too",
|
|
121
|
+
default: false
|
|
122
|
+
},
|
|
123
|
+
yes
|
|
124
|
+
},
|
|
125
|
+
run: async ({ args }) => {
|
|
126
|
+
const found = await here();
|
|
127
|
+
if (found === null) return;
|
|
128
|
+
const filter = { merged: !args.all };
|
|
129
|
+
if (args.run !== void 0) filter.runId = args.run;
|
|
130
|
+
const steps = planClean(found.items, filter, {
|
|
131
|
+
branches: args.branches,
|
|
132
|
+
force: args.force,
|
|
133
|
+
live: found.live
|
|
134
|
+
});
|
|
135
|
+
const acting = steps.filter((s) => stepActs(s));
|
|
136
|
+
if (acting.length === 0) {
|
|
137
|
+
for (const line of formatPlan(steps)) write(line);
|
|
138
|
+
write("Nothing to clean.");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
write("Plan:");
|
|
142
|
+
for (const line of formatPlan(steps)) write(line);
|
|
143
|
+
if (!await goAhead(args.yes, "clean")) return;
|
|
144
|
+
done(await applyClean(found.root, acting, args.force));
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
const remove = defineCommand({
|
|
148
|
+
meta: {
|
|
149
|
+
name: "remove",
|
|
150
|
+
description: "Remove one of Ork's worktrees, by its branch or its path; --force when that loses work"
|
|
151
|
+
},
|
|
152
|
+
args: {
|
|
153
|
+
target: {
|
|
154
|
+
type: "positional",
|
|
155
|
+
description: "The branch, or the worktree's path",
|
|
156
|
+
required: true
|
|
157
|
+
},
|
|
158
|
+
branch: {
|
|
159
|
+
type: "boolean",
|
|
160
|
+
description: "Also delete the branch",
|
|
161
|
+
default: false
|
|
162
|
+
},
|
|
163
|
+
force: {
|
|
164
|
+
type: "boolean",
|
|
165
|
+
description: "Remove even when that loses uncommitted changes or commits",
|
|
166
|
+
default: false
|
|
167
|
+
},
|
|
168
|
+
yes
|
|
169
|
+
},
|
|
170
|
+
run: async ({ args }) => {
|
|
171
|
+
const found = await here();
|
|
172
|
+
if (found === null) return;
|
|
173
|
+
const byPath = path.resolve(args.target);
|
|
174
|
+
const item = found.items.find((i) => keyOf(i) === args.target || i.path === byPath);
|
|
175
|
+
const planned = planRemove(found, item === void 0 ? args.target : keyOf(item), args.branch);
|
|
176
|
+
if (!planned.ok) {
|
|
177
|
+
writeErr(planned.error);
|
|
178
|
+
process.exitCode = 1;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const { plan } = planned;
|
|
182
|
+
write("Plan:");
|
|
183
|
+
for (const line of formatPlan([plan.step])) write(line);
|
|
184
|
+
if (plan.force) {
|
|
185
|
+
for (const line of formatLost(await lostWork(found.root, plan))) write(line);
|
|
186
|
+
if (!args.force) {
|
|
187
|
+
writeErr("That loses work; pass --force to remove it anyway.");
|
|
188
|
+
process.exitCode = 1;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (!await goAhead(args.yes, "remove")) return;
|
|
193
|
+
done(await applyRemove(found.root, plan));
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
const worktrees = defineCommand({
|
|
197
|
+
meta: {
|
|
198
|
+
name: "worktrees",
|
|
199
|
+
description: "Track and clean up the git worktrees Ork creates for runs"
|
|
200
|
+
},
|
|
201
|
+
subCommands: {
|
|
202
|
+
list,
|
|
203
|
+
clean,
|
|
204
|
+
remove
|
|
205
|
+
},
|
|
206
|
+
run: async ({ rawArgs }) => {
|
|
207
|
+
const [first] = rawArgs;
|
|
208
|
+
if (first === "list" || first === "clean" || first === "remove") return;
|
|
209
|
+
await show(rawArgs.includes("--json"));
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
export { worktrees };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krapht/ork",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Orchestrate Claude agents on your machine: hierarchical, serializable workflows with a CLI and a localhost UI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@anthropic-ai/claude-agent-sdk": "0.3.309",
|
|
42
|
-
"@anthropic-ai/sdk": "1.
|
|
42
|
+
"@anthropic-ai/sdk": "1.3.0",
|
|
43
43
|
"@modelcontextprotocol/sdk": "1.32.1",
|
|
44
44
|
"citty": "0.2.2",
|
|
45
45
|
"yaml": "2.9.1",
|
|
@@ -51,9 +51,11 @@
|
|
|
51
51
|
"@krapht/ork-nodes": "0.0.0",
|
|
52
52
|
"@krapht/ork-runtime-claude": "0.0.0",
|
|
53
53
|
"@krapht/ork-schema": "0.0.0",
|
|
54
|
-
"
|
|
54
|
+
"@krapht/ork-server": "0.0.0",
|
|
55
|
+
"@krapht/ork-ui": "0.0.0",
|
|
56
|
+
"tsdown": "0.23.4"
|
|
55
57
|
},
|
|
56
58
|
"scripts": {
|
|
57
|
-
"build": "node ../../tools/release/build-info.ts && tsdown && cp ../../LICENSE ../../THIRD_PARTY_NOTICES.md ."
|
|
59
|
+
"build": "pnpm --filter @krapht/ork-ui build && node ../../tools/release/build-info.ts && node ../../tools/release/plugin-skills.ts && node ../../tools/release/builtin-workflows.ts && tsdown && cp ../../LICENSE ../../THIRD_PARTY_NOTICES.md . && cp -R ../ui/dist dist/ui"
|
|
58
60
|
}
|
|
59
61
|
}
|