@krapht/ork 1.0.0-alpha.2 → 1.0.0-alpha.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -7
- package/dist/__build_info__-D6mXC4cD.mjs +228 -0
- package/dist/apply-Mb0fNpCX.mjs +17 -0
- package/dist/argument-error-CihH46Le.mjs +27 -0
- package/dist/attach-C1OnxyMh.mjs +92 -0
- package/dist/attach-CEGbRg1D.mjs +1230 -0
- package/dist/bin.mjs +1312 -2390
- package/dist/brief-DH66xmj8.mjs +115 -0
- package/dist/{decide-CGyx9rv7.mjs → builtin-files-Cf-NZDqM.mjs} +3 -51
- package/dist/chips-BqcT58qr.mjs +43 -0
- package/dist/command-tree-CEkzWNy6.mjs +63 -0
- package/dist/{completion-CSU5vkEt.mjs → completion-DS0gCZRm.mjs} +200 -30
- package/dist/context-CGPh12HV.mjs +106 -0
- package/dist/detached-Dakw5RZj.mjs +38 -0
- package/dist/extra-arguments-BxQMXARO.mjs +13 -0
- package/dist/{find-D6Gj1sxg.mjs → find-BTikxc54.mjs} +2 -2
- package/dist/flags-C8yCq0j5.mjs +145 -0
- package/dist/follow-ups-CtlhHbwe.mjs +563 -0
- package/dist/format-BB4E-T6x.mjs +249 -0
- package/dist/{help-groups-BwYoAWsh.mjs → help-groups-BpRgBSTh.mjs} +42 -76
- package/dist/in-a-run-CbnKjO7t.mjs +27 -0
- package/dist/install-CWcpX5g4.mjs +211 -0
- package/dist/{limits-B1-DYc3d.mjs → limits-VQqsBoQk.mjs} +2 -2
- package/dist/{mcp-DE_EmyVm.mjs → mcp-wGfkMCVF.mjs} +6 -6
- package/dist/once-BHxSdZz_.mjs +112 -0
- package/dist/orphan-guard-BFKg8VSd.mjs +58 -0
- package/dist/output-B1kwo0YU.mjs +439 -0
- package/dist/outputs-text-CNTJ7Qq_.mjs +19 -0
- package/dist/{pause-CL6N_Xrj.mjs → pause-C_Wxh-KM.mjs} +7 -4
- package/dist/print-DNYzq4Rr.mjs +19 -0
- package/dist/project-records-XY3ku95x.mjs +233 -0
- package/dist/refusals-DQQX3AIb.mjs +116 -0
- package/dist/{relay-0SD0erwf.mjs → relay-BA6tsnpc.mjs} +2 -2
- package/dist/run-Bvvg-fiF.mjs +234 -0
- package/dist/run-DBUAq-Uu.mjs +1452 -0
- package/dist/send-nrKtak5F.mjs +105 -0
- package/dist/{src-Dujmvv4s.mjs → src-8cOPOgrT.mjs} +12523 -5507
- package/dist/{src-BbyEoMn4.mjs → src-CmEvHdju.mjs} +797 -233
- package/dist/src-D4JSymhK.mjs +3104 -0
- package/dist/{src-DOcK8Jfp.mjs → src-I_RyKu6_.mjs} +1022 -590
- package/dist/store-CDpk8Wtz.mjs +16 -0
- package/dist/ui/THIRD_PARTY_LICENSES.txt +612 -0
- package/dist/ui/assets/app--FijEXiJ.css +1 -0
- package/dist/ui/assets/app-BowhjEAe.js +112 -0
- package/dist/ui/assets/{index-9dFha9Qn.js → index-CrMa2oHB.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-DY3Dnpho.mjs} +54 -25
- package/dist/usage-1VwSg_Qt.mjs +257 -0
- package/dist/validate-BxayjjkV.mjs +632 -0
- package/dist/verdict-CMvPCpWC.mjs +84 -0
- package/dist/{watch-CtbgDAcz.mjs → watch-CT0EtLPZ.mjs} +45 -17
- package/dist/{workflows-D1_WVukk.mjs → workflows-DHHi-APg.mjs} +30 -5
- package/dist/worktrees-CZvbHzbN.mjs +302 -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,84 @@
|
|
|
1
|
+
import { C as termOf, p as apply } from "./output-B1kwo0YU.mjs";
|
|
2
|
+
import { n as chip, t as CHIP_LABELS } from "./chips-BqcT58qr.mjs";
|
|
3
|
+
// src/run/view.ts
|
|
4
|
+
/**
|
|
5
|
+
* @file How a run's lines are drawn: the terminal kit's {@link Term} for the
|
|
6
|
+
* stream they go to, and what a transcript has to remember between events (a
|
|
7
|
+
* node's worktree, to show its paths relative to it). Plain, a view draws
|
|
8
|
+
* today's line-based transcript; rich, it draws chips and cuts to the width.
|
|
9
|
+
*/
|
|
10
|
+
/** A view for `stream` (stdout when not given), as `env` sets it. */
|
|
11
|
+
const createView = (stream = process.stdout, env = process.env) => ({
|
|
12
|
+
term: termOf(stream, env),
|
|
13
|
+
roots: /* @__PURE__ */ new Map()
|
|
14
|
+
});
|
|
15
|
+
/** Styles text for `term`: no escape codes when it is plain. */
|
|
16
|
+
const styler = (term) => (format, text) => apply(term, format, text);
|
|
17
|
+
// src/run/format-bus.ts
|
|
18
|
+
/** The first line of a message body, cut to fit one terminal line. */
|
|
19
|
+
const firstLine = (body) => {
|
|
20
|
+
const [line = ""] = body.trim().split("\n");
|
|
21
|
+
return line.length > 100 ? `${line.slice(0, 99)}…` : line;
|
|
22
|
+
};
|
|
23
|
+
/** Messages, owner questions and claims, without the node prefix. */
|
|
24
|
+
const formatBusEvent = (event, term = createView().term) => {
|
|
25
|
+
const style = styler(term);
|
|
26
|
+
if (event.type === "claim.taken") return [style("dim", `⚑ claimed ${event.resource}`)];
|
|
27
|
+
if (event.type === "claim.refused") {
|
|
28
|
+
const held = event.held === event.resource ? "" : ` (it holds ${event.held})`;
|
|
29
|
+
return [style("yellow", `⚑ ${event.resource} is ${event.holder}'s${held}`)];
|
|
30
|
+
}
|
|
31
|
+
if (event.type === "claim.released") return [style("dim", `⚑ released ${event.resource}`)];
|
|
32
|
+
if (event.type === "message.sent") {
|
|
33
|
+
const how = event.via === void 0 ? "" : ` (${event.via})`;
|
|
34
|
+
return [style("dim", `✉ to ${event.to}${how}: ${firstLine(event.body)}`)];
|
|
35
|
+
}
|
|
36
|
+
if (event.type === "message.received") {
|
|
37
|
+
const how = event.via === void 0 ? "" : ` (${event.via})`;
|
|
38
|
+
return [style("dim", `✉ from ${event.from}${how}: ${firstLine(event.body)}`)];
|
|
39
|
+
}
|
|
40
|
+
if (event.type === "call.started") return [style("dim", `↳ started ${event.workflow} as run ${event.run}`)];
|
|
41
|
+
if (event.type === "link.notice") return [style("dim", `↔ ${event.text}`)];
|
|
42
|
+
if (event.type === "message.read") return [];
|
|
43
|
+
if (event.type === "owner.asked") return [`${term.rich ? chip(term, CHIP_LABELS.ask, "accent") : style(["yellow", "bold"], "?")} asked as the owner of ${event.about}: ${event.text} ${style("dim", `(${event.question})`)}`];
|
|
44
|
+
if (event.type === "owner.answered") return [style("dim", `→ ${event.answer} (owner)`)];
|
|
45
|
+
return [style("dim", "→ its session ended first; asking you")];
|
|
46
|
+
};
|
|
47
|
+
// src/run/items-ran.ts
|
|
48
|
+
/**
|
|
49
|
+
* @file How a fan-out's finished line counts its items: only the ones that
|
|
50
|
+
* started.
|
|
51
|
+
*/
|
|
52
|
+
/** How many of a fan-out's items started; a skipped one never did. */
|
|
53
|
+
const itemsRan = (count, statuses) => {
|
|
54
|
+
const ran = Array.isArray(statuses) ? statuses.filter((status) => status !== "skipped").length : count;
|
|
55
|
+
return ran === count ? `${String(count)} ran` : `${String(ran)} of ${String(count)} ran`;
|
|
56
|
+
};
|
|
57
|
+
// src/run/verdict.ts
|
|
58
|
+
/**
|
|
59
|
+
* @file The one-line verdict of a finished node: its report's status, unless
|
|
60
|
+
* the node failed or its work did not merge.
|
|
61
|
+
*/
|
|
62
|
+
/** Whether a finished node's work did not merge into its owner. */
|
|
63
|
+
const hasConflict = (output) => {
|
|
64
|
+
const { merge } = output;
|
|
65
|
+
return typeof merge === "object" && merge !== null && "ok" in merge && merge.ok === false;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* A one-line verdict for a finished node, by what its type outputs. A node that
|
|
69
|
+
* failed reads `fail` even when its report said `pass` (a hook refused its
|
|
70
|
+
* commit); a child that did not merge says so.
|
|
71
|
+
*/
|
|
72
|
+
const nodeVerdict = (output, ok = true, error) => {
|
|
73
|
+
const { status, summary, exitCode, choice, answeredBy, count } = output;
|
|
74
|
+
if (typeof status === "string") {
|
|
75
|
+
if (!ok && (status === "pass" || status === "done")) return error === void 0 ? "fail" : `fail: ${firstLine(error)}`;
|
|
76
|
+
const label = hasConflict(output) ? `${status}, merge conflict` : status;
|
|
77
|
+
return typeof summary === "string" && summary !== "" ? `${label}: ${summary}` : label;
|
|
78
|
+
}
|
|
79
|
+
if (typeof exitCode === "number") return `exit ${String(exitCode)}`;
|
|
80
|
+
if (typeof choice === "string") return `${choice} (${String(answeredBy)})`;
|
|
81
|
+
if (typeof count === "number") return itemsRan(count, output["statuses"]);
|
|
82
|
+
return "done";
|
|
83
|
+
};
|
|
84
|
+
export { createView as a, formatBusEvent as i, nodeVerdict as n, styler as o, firstLine as r, hasConflict as t };
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
import { A as triggerStanding, Ei as timeoutMs, F as watchingPid, M as readProjectTriggers, O as triggersReport, P as takeWatchLock, Pn as liveStatus, R as openStore, Si as isToolNode, Vn as stillTheSameProcess, Wr as placeFiles, _i as CheckReportSchema, j as declaredTriggers, k as createWatcher, mr as runGh, pt as detachRun, xn as startFollowUpChecks } from "./src-8cOPOgrT.mjs";
|
|
2
|
+
import { c as toolExecutor, o as spawnTool, s as mcpCallExecutor } from "./src-I_RyKu6_.mjs";
|
|
3
|
+
import { n as newRunId } from "./find-BTikxc54.mjs";
|
|
4
|
+
import { C as termOf, E as widthOf, a as plain, c as write, l as writeErr, s as style, t as confirm, w as cut } from "./output-B1kwo0YU.mjs";
|
|
5
|
+
import { n as chip, t as CHIP_LABELS } from "./chips-BqcT58qr.mjs";
|
|
6
|
+
import { i as runLimits, r as limitsRefusal } from "./limits-VQqsBoQk.mjs";
|
|
7
|
+
import { t as acceptRefusal } from "./in-a-run-CbnKjO7t.mjs";
|
|
7
8
|
import path from "node:path";
|
|
8
9
|
import { mkdirSync } from "node:fs";
|
|
9
10
|
import { homedir } from "node:os";
|
|
@@ -63,7 +64,7 @@ const lookAt = async (options) => {
|
|
|
63
64
|
});
|
|
64
65
|
return result.ok ? resultOf(result.output["json"]) : {
|
|
65
66
|
ok: false,
|
|
66
|
-
error: result.error ?? "the check failed"
|
|
67
|
+
error: [result.error ?? "the check failed", result.end?.detail].filter((part) => part !== void 0).join(": ")
|
|
67
68
|
};
|
|
68
69
|
};
|
|
69
70
|
// src/watch/report.ts
|
|
@@ -73,10 +74,21 @@ const STANDING = {
|
|
|
73
74
|
changed: "off: changed since it was accepted",
|
|
74
75
|
invalid: "off: has an error"
|
|
75
76
|
};
|
|
76
|
-
|
|
77
|
-
const
|
|
77
|
+
/** A failure's mark: a `FAIL` chip when rich, else the red `✗`. */
|
|
78
|
+
const failMark = (term) => term.rich ? chip(term, CHIP_LABELS.fail, "danger") : style("red", "✗");
|
|
79
|
+
/** A warning's mark: a `WARN` chip when rich, else the yellow `!`. */
|
|
80
|
+
const warnMark = (term) => term.rich ? chip(term, CHIP_LABELS.warn, "warn") : style("yellow", "!");
|
|
81
|
+
/** A trigger's standing, rich: a chip and what it adds. */
|
|
82
|
+
const standingChip = (term, standing) => {
|
|
83
|
+
const live = standing === "live";
|
|
84
|
+
const label = live ? "LIVE" : "OFF";
|
|
85
|
+
const text = STANDING[standing] ?? standing;
|
|
86
|
+
return `${chip(term, label, live ? "ok" : standing === "invalid" ? "danger" : "warn")}${live ? "" : ` ${text.replace(/^off: /u, "")}`}`;
|
|
87
|
+
};
|
|
88
|
+
const brokenLines = (broken, term = termOf(process.stdout)) => broken.map(({ file, message }) => `${failMark(term)} ${plain(file)}: ${plain(message)}`);
|
|
89
|
+
const statusLines = (report, term = termOf(process.stdout)) => {
|
|
78
90
|
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);
|
|
91
|
+
const broken = brokenLines(report.broken, term);
|
|
80
92
|
if (report.triggers.length === 0) return [
|
|
81
93
|
head,
|
|
82
94
|
...broken,
|
|
@@ -86,11 +98,11 @@ const statusLines = (report) => {
|
|
|
86
98
|
head,
|
|
87
99
|
...broken,
|
|
88
100
|
...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}`}`,
|
|
101
|
+
term.rich ? `${style("bold", plain(status.trigger))} ${style("dim", cut(plain(`${status.project} → ${status.run}, every ${status.every}`), Math.max(8, term.width - widthOf(status.trigger) - 1)))}` : `${style("bold", plain(status.trigger))} ${style("dim", plain(`${status.project} → ${status.run}, every ${status.every}`))}`,
|
|
102
|
+
` ${term.rich ? standingChip(term, status.standing) : STANDING[status.standing] ?? status.standing}${status.lookedAt === void 0 ? "" : `, last looked ${status.lookedAt}`}`,
|
|
91
103
|
...status.error === void 0 ? [] : [` last look: ${plain(status.error)}`],
|
|
92
104
|
...status.held === void 0 ? [] : [` waiting: ${plain(status.held)}`],
|
|
93
|
-
...status.problems.map((problem) => ` ${
|
|
105
|
+
...status.problems.map((problem) => ` ${failMark(term)} ${plain(problem)}`)
|
|
94
106
|
])
|
|
95
107
|
];
|
|
96
108
|
};
|
|
@@ -120,22 +132,26 @@ const checkLines = (item) => {
|
|
|
120
132
|
const workflowLines = ({ workflow, workflowFile }) => {
|
|
121
133
|
if (workflow === null) return [];
|
|
122
134
|
const budget = workflow.limits?.budgetUsd;
|
|
123
|
-
|
|
135
|
+
const tokens = workflow.limits?.budgetTokens;
|
|
136
|
+
const caps = [...budget === void 0 ? [] : [`$${String(budget)}`], ...tokens === void 0 ? [] : [`${String(tokens)} tokens`]];
|
|
137
|
+
return [` workflow: ${plain(workflowFile)}${caps.length === 0 ? "" : `, up to ${caps.join(" and ")} a run`}`, ...Object.entries(workflow.nodes).map(([id, node]) => ` ${plain(id)}: ${plain(node.type)}`)];
|
|
124
138
|
};
|
|
125
139
|
const STANDS = {
|
|
126
140
|
new: "new",
|
|
127
141
|
changed: "changed since you accepted it, here or in the workflow it starts"
|
|
128
142
|
};
|
|
129
|
-
const acceptLines = (item, standing) => [
|
|
143
|
+
const acceptLines = (item, standing, term = termOf(process.stdout)) => [
|
|
130
144
|
`${style("bold", plain(item.id))} ${style("dim", plain(item.place.root))}`,
|
|
131
145
|
` ${STANDS[standing] ?? standing}`,
|
|
132
146
|
...checkLines(item),
|
|
133
147
|
` starts: ${plain(item.trigger.run)}, looking every ${item.trigger.every}, with up to ${String(item.trigger.maxRunsPerDay ?? 10)} runs a day`,
|
|
134
148
|
...workflowLines(item),
|
|
135
|
-
...item.diagnostics.filter(({ severity }) => severity === "warning").map(({ message }) => ` ${
|
|
149
|
+
...item.diagnostics.filter(({ severity }) => severity === "warning").map(({ message }) => ` ${warnMark(term)} ${plain(message)}`)
|
|
136
150
|
];
|
|
137
151
|
/** 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 }) => ` ${
|
|
152
|
+
const unacceptableLines = (item, term = termOf(process.stdout)) => [`${style("bold", plain(item.id))} ${style("dim", plain(item.place.root))}`, ...item.diagnostics.filter(({ severity }) => severity === "error").map(({ message }) => ` ${failMark(term)} ${plain(message)}`)];
|
|
153
|
+
/** What `ork watch` says when follow-ups come due. */
|
|
154
|
+
const dueLines = (due) => due.map((f) => `▸ follow-up due (${plain(f.for)}): ${plain(f.title)}${f.note === void 0 ? "" : ` (${plain(f.note)})`}, \`ork follow-ups show ${f.id}\``);
|
|
139
155
|
// src/watch/start.ts
|
|
140
156
|
/**
|
|
141
157
|
* @file Starting a trigger's run (RFC-011): an ordinary run in the background,
|
|
@@ -259,7 +275,19 @@ const watchLoop = async (store) => {
|
|
|
259
275
|
const timer = setInterval(() => {
|
|
260
276
|
watcher.tick();
|
|
261
277
|
}, TICK_MS);
|
|
278
|
+
const stopChecks = startFollowUpChecks({
|
|
279
|
+
rows: store,
|
|
280
|
+
gh: runGh,
|
|
281
|
+
projects: () => store.projects().map((project) => project.root),
|
|
282
|
+
onDue: (due) => {
|
|
283
|
+
for (const line of dueLines(due)) write(line);
|
|
284
|
+
},
|
|
285
|
+
onError: (error) => {
|
|
286
|
+
writeErr(`follow-ups: ${error}`);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
262
289
|
await stopped;
|
|
290
|
+
stopChecks();
|
|
263
291
|
clearInterval(timer);
|
|
264
292
|
stopping.abort();
|
|
265
293
|
await watcher.settled();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { Gr as placeFor, R as openStore, Wr as placeFiles, Xr as inside, qr as runDirOf, z as storePath } from "./src-8cOPOgrT.mjs";
|
|
2
|
+
import { a as isBuiltinArg, n as builtinWorkflowFile, t as builtinProblem } from "./builtin-files-Cf-NZDqM.mjs";
|
|
3
|
+
import { l as writeErr } from "./output-B1kwo0YU.mjs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
5
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
6
6
|
// src/project/place.ts
|
|
7
7
|
/**
|
|
8
8
|
* @file Which project a folder belongs to, from the projects the store has
|
|
@@ -40,6 +40,7 @@ const WORKFLOW_SUFFIX = ".ork.yaml";
|
|
|
40
40
|
* that folder.
|
|
41
41
|
*/
|
|
42
42
|
const findWorkflowFiles = (paths, cwd, workflows = path.join(cwd, WORKFLOW_DIR)) => {
|
|
43
|
+
if (paths.length === 0 && !existsSync(workflows)) return [];
|
|
43
44
|
return (paths.length > 0 ? paths : [fromCwd(workflows, cwd)]).flatMap((target) => {
|
|
44
45
|
const absolute = path.resolve(cwd, target);
|
|
45
46
|
if (existsSync(absolute) && statSync(absolute).isDirectory()) return readdirSync(absolute).filter((name) => name.endsWith(WORKFLOW_SUFFIX)).toSorted().map((name) => path.join(target, name));
|
|
@@ -85,4 +86,28 @@ const workflowArg = (arg, cwd) => {
|
|
|
85
86
|
runDir: (runId) => runDirOf(place, runId)
|
|
86
87
|
};
|
|
87
88
|
};
|
|
88
|
-
|
|
89
|
+
/**
|
|
90
|
+
* The `name:` line of a workflow file, read without parsing it (it may be
|
|
91
|
+
* broken).
|
|
92
|
+
*/
|
|
93
|
+
const nameOf = (file) => {
|
|
94
|
+
try {
|
|
95
|
+
return /^name:\s*["']?([^\s"'#]+)/mu.exec(readFileSync(file, "utf8"))?.[1];
|
|
96
|
+
} catch {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* The text for a workflow argument that is no file: the workflows the project
|
|
102
|
+
* has, by file name (and `name:` when it differs), and that a file name or path
|
|
103
|
+
* is what the command takes. Empty-handed when the folder is missing.
|
|
104
|
+
*/
|
|
105
|
+
const missingWorkflowText = (arg, workflows) => {
|
|
106
|
+
const listed = (existsSync(workflows) ? readdirSync(workflows).filter((name) => name.endsWith(WORKFLOW_SUFFIX)).toSorted() : []).map((file) => {
|
|
107
|
+
const name = nameOf(path.join(workflows, file));
|
|
108
|
+
const stem = file.slice(0, -9);
|
|
109
|
+
return name === void 0 || name === stem ? file : `${file} (${name})`;
|
|
110
|
+
});
|
|
111
|
+
return `no workflow file ${arg}; a file name or path is meant, not a workflow name (${listed.length === 0 ? "this project has no workflows" : `this project's workflows: ${listed.join(", ")}`})`;
|
|
112
|
+
};
|
|
113
|
+
export { refusedBuiltin as a, projectAt as c, missingWorkflowText as i, registeredProjects as l, WORKFLOW_SUFFIX as n, resolveWorkflowArg as o, findWorkflowFiles as r, workflowArg as s, WORKFLOW_DIR as t };
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { $t as planClean, Qt as keyOf, R as openStore, Xt as applyClean, Yt as worktreeListTable, a as planRemove, c as repoRoot, d as mergeStateOf, en as stepActs, f as planLine, i as lostWork, l as isDetached, m as stateParts, nn as inspectProject, r as applyRemove, s as readWorktrees, u as labelOf } from "./src-8cOPOgrT.mjs";
|
|
2
|
+
import { t as applyFilters } from "./apply-Mb0fNpCX.mjs";
|
|
3
|
+
import { C as termOf, c as write, h as paint, l as writeErr, s as style, t as confirm, w as cut } from "./output-B1kwo0YU.mjs";
|
|
4
|
+
import { n as chip } from "./chips-BqcT58qr.mjs";
|
|
5
|
+
import { c as refuseUnknownFlags, i as readListArgs, n as filterArgs, o as resolveRun, r as projectScope } from "./flags-C8yCq0j5.mjs";
|
|
6
|
+
import { t as printList } from "./print-DNYzq4Rr.mjs";
|
|
7
|
+
import { r as withStore } from "./project-records-XY3ku95x.mjs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
10
|
+
import { defineCommand } from "citty";
|
|
11
|
+
// src/project/go-ahead.ts
|
|
12
|
+
/**
|
|
13
|
+
* @file The question `ork worktrees clean` and `remove` ask before they act:
|
|
14
|
+
* skipped with `--yes`, impossible without a terminal (so `--yes` is needed),
|
|
15
|
+
* and a no says so rather than leaving the command silent.
|
|
16
|
+
*/
|
|
17
|
+
/** Whether to carry on; says why not when it won't. */
|
|
18
|
+
const goAhead = async (yes, verb, terminal = process.stdin.isTTY, ask = confirm) => {
|
|
19
|
+
if (yes) return true;
|
|
20
|
+
if (!terminal) {
|
|
21
|
+
writeErr(`Pass --yes to ${verb} without a terminal.`);
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
if (await ask("Go ahead?")) return true;
|
|
26
|
+
write("Cancelled.");
|
|
27
|
+
return false;
|
|
28
|
+
};
|
|
29
|
+
// src/project/worktrees.ts
|
|
30
|
+
/**
|
|
31
|
+
* @file What `ork worktrees` reads across projects, and prints: each worktree
|
|
32
|
+
* or branch a run left, with its state read from git, the plan `ork worktrees
|
|
33
|
+
* clean` or `remove` would carry out, and what a forced remove would lose.
|
|
34
|
+
* The words are core's (worktree/words.ts), which the Worktrees panel says
|
|
35
|
+
* too; this only colors them.
|
|
36
|
+
*/
|
|
37
|
+
/** One line of state: its merge state, then what clean would care about. */
|
|
38
|
+
const worktreeState = (item, term = termOf(process.stdout)) => {
|
|
39
|
+
const merge = mergeStateOf(item);
|
|
40
|
+
const head = merge === "merged" ? [term.rich ? chip(term, "MERGED", "ok") : style("green", "merged")] : merge === "detached" ? ["detached"] : [];
|
|
41
|
+
const parts = stateParts(item).map((part) => part.risk ? paint(term, "warn", part.text) : part.text);
|
|
42
|
+
return [...head, ...parts].join(" · ");
|
|
43
|
+
};
|
|
44
|
+
/** A name as the list shows it, saying when it's detached. */
|
|
45
|
+
const nameOf = (item) => isDetached(item) ? `${labelOf(item)} (detached)` : labelOf(item);
|
|
46
|
+
const formatWorktrees = (items, term = termOf(process.stdout)) => {
|
|
47
|
+
if (items.length === 0) return ["No Ork worktrees or branches."];
|
|
48
|
+
const width = Math.max(...items.map((i) => nameOf(i).length));
|
|
49
|
+
return items.flatMap((item) => [`${style("bold", nameOf(item).padEnd(width))} ${worktreeState(item, term)}`, ...item.path === null ? [] : [` ${style("dim", term.rich ? cut(item.path, term.width - 2) : item.path)}`]]);
|
|
50
|
+
};
|
|
51
|
+
const formatPlan = (steps) => steps.map((step) => {
|
|
52
|
+
const { text, reason } = planLine(step);
|
|
53
|
+
return ` ${text}${reason === void 0 ? "" : style("dim", ` (${reason})`)}`;
|
|
54
|
+
});
|
|
55
|
+
/** What a forced remove loses, as git prints it, under a heading each. */
|
|
56
|
+
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}`)]];
|
|
57
|
+
/** What a forced clean loses, item by item: the same lines `remove` prints. */
|
|
58
|
+
const formatLosses = (losses) => losses.flatMap(({ item, lost }) => {
|
|
59
|
+
const lines = formatLost(lost);
|
|
60
|
+
return lines.length === 0 ? [] : [`Loses in ${labelOf(item)}:`, ...lines];
|
|
61
|
+
});
|
|
62
|
+
/** What Ork created in each registered project's repository still on disk. */
|
|
63
|
+
const inProjects = async (roots) => {
|
|
64
|
+
const store = openStore();
|
|
65
|
+
try {
|
|
66
|
+
return (await Promise.all([...new Set(roots)].filter((root) => existsSync(root)).map(async (root) => {
|
|
67
|
+
try {
|
|
68
|
+
return (await readWorktrees(store, root)).items;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
writeErr(`Skipped ${root}: ${error instanceof Error ? error.message : String(error)}`);
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
}))).flat();
|
|
74
|
+
} finally {
|
|
75
|
+
store.close();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
// src/commands/worktrees.ts
|
|
79
|
+
/**
|
|
80
|
+
* @file `ork worktrees` (or `ork wt`): lists the worktrees and branches runs
|
|
81
|
+
* left behind (those the runs recorded creating, whatever the project's git
|
|
82
|
+
* rules named them, and any `ork/` branch), with their state read from git;
|
|
83
|
+
* cleans them up safely after showing the plan and asking; and removes one,
|
|
84
|
+
* the twin of the Worktrees panel's Remove…. The plans are core's, so the
|
|
85
|
+
* panel keeps and removes exactly what these do.
|
|
86
|
+
*/
|
|
87
|
+
/**
|
|
88
|
+
* The repo this folder is in, and what Ork created there; null outside one. A
|
|
89
|
+
* repo with no commit has nothing of Ork's, which only a read says as an empty
|
|
90
|
+
* list.
|
|
91
|
+
*/
|
|
92
|
+
const here = async (emptyWithoutCommit = false) => {
|
|
93
|
+
const cwd = process.cwd();
|
|
94
|
+
const root = (await inspectProject(cwd))?.root ?? null;
|
|
95
|
+
if (root === null) {
|
|
96
|
+
const bare = emptyWithoutCommit ? await repoRoot(cwd) : null;
|
|
97
|
+
if (bare !== null) return {
|
|
98
|
+
root: bare,
|
|
99
|
+
items: [],
|
|
100
|
+
live: /* @__PURE__ */ new Set(),
|
|
101
|
+
runs: []
|
|
102
|
+
};
|
|
103
|
+
writeErr("Not in a git repository with a commit.");
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const store = openStore();
|
|
108
|
+
try {
|
|
109
|
+
return {
|
|
110
|
+
root,
|
|
111
|
+
runs: store.runs({ project: root }).map((run) => run.id),
|
|
112
|
+
...await readWorktrees(store, root)
|
|
113
|
+
};
|
|
114
|
+
} finally {
|
|
115
|
+
store.close();
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const realOr = (target) => {
|
|
119
|
+
try {
|
|
120
|
+
return realpathSync(target);
|
|
121
|
+
} catch {
|
|
122
|
+
return target;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const done = (lines) => {
|
|
126
|
+
for (const line of lines) write(`${style("green", "✓")} ${line}`);
|
|
127
|
+
};
|
|
128
|
+
const show = async (args) => {
|
|
129
|
+
const request = readListArgs(worktreeListTable, args);
|
|
130
|
+
if (request === void 0) return;
|
|
131
|
+
const projects = withStore((store) => store.projects());
|
|
132
|
+
const scope = projectScope(request.filter, projects);
|
|
133
|
+
if (scope === void 0) return;
|
|
134
|
+
const filter = withStore((store) => resolveRun(store, request.filter));
|
|
135
|
+
if (filter === void 0) return;
|
|
136
|
+
let items;
|
|
137
|
+
if (scope === "here") {
|
|
138
|
+
const found = await here(true);
|
|
139
|
+
if (found === null) return;
|
|
140
|
+
items = found.items;
|
|
141
|
+
} else items = await inProjects(scope === "all" ? projects.map((p) => p.root) : [scope.root]);
|
|
142
|
+
const shown = applyFilters(worktreeListTable, items, filter).slice(0, request.limit);
|
|
143
|
+
printList(shown, worktreeListTable, args, (rows) => {
|
|
144
|
+
for (const line of formatWorktrees(rows)) write(line);
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
const listArgs = filterArgs(worktreeListTable);
|
|
148
|
+
const list = defineCommand({
|
|
149
|
+
meta: {
|
|
150
|
+
name: "list",
|
|
151
|
+
description: "List Ork's worktrees and branches (the default)"
|
|
152
|
+
},
|
|
153
|
+
plugins: [refuseUnknownFlags],
|
|
154
|
+
args: listArgs,
|
|
155
|
+
run: ({ args }) => show(args)
|
|
156
|
+
});
|
|
157
|
+
const yes = {
|
|
158
|
+
type: "boolean",
|
|
159
|
+
description: "Do not ask",
|
|
160
|
+
default: false
|
|
161
|
+
};
|
|
162
|
+
const clean = defineCommand({
|
|
163
|
+
meta: {
|
|
164
|
+
name: "clean",
|
|
165
|
+
description: "Remove Ork's worktrees (merged ones by default); never one with uncommitted changes unless --force, nor one whose run is still going"
|
|
166
|
+
},
|
|
167
|
+
args: {
|
|
168
|
+
run: {
|
|
169
|
+
type: "string",
|
|
170
|
+
description: "Only this run's worktrees"
|
|
171
|
+
},
|
|
172
|
+
all: {
|
|
173
|
+
type: "boolean",
|
|
174
|
+
description: "Include worktrees whose branch is not merged",
|
|
175
|
+
default: false
|
|
176
|
+
},
|
|
177
|
+
branches: {
|
|
178
|
+
type: "boolean",
|
|
179
|
+
description: "Also delete the branches",
|
|
180
|
+
default: false
|
|
181
|
+
},
|
|
182
|
+
force: {
|
|
183
|
+
type: "boolean",
|
|
184
|
+
description: "Remove uncommitted changes and unmerged branches too",
|
|
185
|
+
default: false
|
|
186
|
+
},
|
|
187
|
+
yes
|
|
188
|
+
},
|
|
189
|
+
run: async ({ args }) => {
|
|
190
|
+
const [name] = args._;
|
|
191
|
+
if (name !== void 0) {
|
|
192
|
+
writeErr(`\`clean\` takes no name (got ${name}); to remove one worktree: ork worktrees remove ${name}`);
|
|
193
|
+
process.exitCode = 1;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const found = await here();
|
|
197
|
+
if (found === null) return;
|
|
198
|
+
const filter = { merged: !args.all };
|
|
199
|
+
if (args.run !== void 0) {
|
|
200
|
+
const runId = found.runs.find((id) => id === args.run);
|
|
201
|
+
if (runId === void 0) {
|
|
202
|
+
writeErr(`No run ${args.run} in this project.`);
|
|
203
|
+
process.exitCode = 1;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
filter.runId = runId;
|
|
207
|
+
}
|
|
208
|
+
const steps = planClean(found.items, filter, {
|
|
209
|
+
branches: args.branches,
|
|
210
|
+
force: args.force,
|
|
211
|
+
live: found.live
|
|
212
|
+
});
|
|
213
|
+
const acting = steps.filter((s) => stepActs(s));
|
|
214
|
+
if (acting.length === 0) {
|
|
215
|
+
for (const line of formatPlan(steps)) write(line);
|
|
216
|
+
if (steps.length === 0) write("Nothing to clean.");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
write("Plan:");
|
|
220
|
+
for (const line of formatPlan(steps)) write(line);
|
|
221
|
+
if (args.force) {
|
|
222
|
+
const losses = await Promise.all(acting.map(async (step) => ({
|
|
223
|
+
item: step.item,
|
|
224
|
+
lost: await lostWork(found.root, {
|
|
225
|
+
step,
|
|
226
|
+
force: true
|
|
227
|
+
})
|
|
228
|
+
})));
|
|
229
|
+
for (const line of formatLosses(losses)) write(line);
|
|
230
|
+
}
|
|
231
|
+
if (!await goAhead(args.yes, "clean")) return;
|
|
232
|
+
done(await applyClean(found.root, acting, args.force));
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
const remove = defineCommand({
|
|
236
|
+
meta: {
|
|
237
|
+
name: "remove",
|
|
238
|
+
description: "Remove one of Ork's worktrees, by its branch or its path; --force when that loses work"
|
|
239
|
+
},
|
|
240
|
+
args: {
|
|
241
|
+
target: {
|
|
242
|
+
type: "positional",
|
|
243
|
+
description: "The branch, or the worktree's path",
|
|
244
|
+
required: true
|
|
245
|
+
},
|
|
246
|
+
branch: {
|
|
247
|
+
type: "boolean",
|
|
248
|
+
description: "Also delete the branch",
|
|
249
|
+
default: false
|
|
250
|
+
},
|
|
251
|
+
force: {
|
|
252
|
+
type: "boolean",
|
|
253
|
+
description: "Remove even when that loses uncommitted changes or commits",
|
|
254
|
+
default: false
|
|
255
|
+
},
|
|
256
|
+
yes
|
|
257
|
+
},
|
|
258
|
+
run: async ({ args }) => {
|
|
259
|
+
const found = await here();
|
|
260
|
+
if (found === null) return;
|
|
261
|
+
const byPath = path.resolve(args.target);
|
|
262
|
+
const paths = /* @__PURE__ */ new Set([byPath, realOr(byPath)]);
|
|
263
|
+
const item = found.items.find((i) => keyOf(i) === args.target || labelOf(i) === args.target || i.path !== null && paths.has(i.path));
|
|
264
|
+
const planned = planRemove(found, item === void 0 ? args.target : keyOf(item), args.branch);
|
|
265
|
+
if (!planned.ok) {
|
|
266
|
+
writeErr(planned.error);
|
|
267
|
+
process.exitCode = 1;
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const { plan } = planned;
|
|
271
|
+
write("Plan:");
|
|
272
|
+
for (const line of formatPlan([plan.step])) write(line);
|
|
273
|
+
if (plan.force) {
|
|
274
|
+
for (const line of formatLost(await lostWork(found.root, plan))) write(line);
|
|
275
|
+
if (!args.force) {
|
|
276
|
+
writeErr("That loses work; pass --force to remove it anyway.");
|
|
277
|
+
process.exitCode = 1;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (!await goAhead(args.yes, "remove")) return;
|
|
282
|
+
done(await applyRemove(found.root, plan));
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
const worktrees = defineCommand({
|
|
286
|
+
meta: {
|
|
287
|
+
name: "worktrees",
|
|
288
|
+
description: "Track and clean up the git worktrees Ork creates for runs"
|
|
289
|
+
},
|
|
290
|
+
args: listArgs,
|
|
291
|
+
subCommands: {
|
|
292
|
+
list,
|
|
293
|
+
clean,
|
|
294
|
+
remove
|
|
295
|
+
},
|
|
296
|
+
run: async ({ args, rawArgs }) => {
|
|
297
|
+
const [first] = rawArgs;
|
|
298
|
+
if (first === "list" || first === "clean" || first === "remove") return;
|
|
299
|
+
await show(args);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
export { worktrees };
|
package/package.json
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krapht/ork",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
4
|
-
"description": "Orchestrate
|
|
3
|
+
"version": "1.0.0-alpha.20",
|
|
4
|
+
"description": "Orchestrate coding agents (Claude Code, OpenCode, Command Code) on your machine: hierarchical, serializable workflows with a CLI and a localhost UI.",
|
|
5
5
|
"keywords": [
|
|
6
|
-
"claude",
|
|
7
6
|
"agents",
|
|
7
|
+
"coding-agents",
|
|
8
8
|
"orchestration",
|
|
9
9
|
"workflow",
|
|
10
10
|
"cli",
|
|
11
|
-
"ai-engineering"
|
|
11
|
+
"ai-engineering",
|
|
12
|
+
"claude",
|
|
13
|
+
"claude-code",
|
|
14
|
+
"opencode",
|
|
15
|
+
"command-code"
|
|
12
16
|
],
|
|
13
17
|
"license": "MIT",
|
|
14
18
|
"repository": {
|
|
@@ -38,8 +42,6 @@
|
|
|
38
42
|
"provenance": true
|
|
39
43
|
},
|
|
40
44
|
"dependencies": {
|
|
41
|
-
"@anthropic-ai/claude-agent-sdk": "0.3.316",
|
|
42
|
-
"@anthropic-ai/sdk": "1.3.0",
|
|
43
45
|
"@modelcontextprotocol/sdk": "1.32.2",
|
|
44
46
|
"citty": "0.2.2",
|
|
45
47
|
"yaml": "2.9.1",
|
|
@@ -49,13 +51,13 @@
|
|
|
49
51
|
"@krapht/ork-core": "0.0.0",
|
|
50
52
|
"@krapht/ork-integrations": "0.0.0",
|
|
51
53
|
"@krapht/ork-nodes": "0.0.0",
|
|
52
|
-
"@krapht/ork-runtime
|
|
54
|
+
"@krapht/ork-runtime": "0.0.0",
|
|
53
55
|
"@krapht/ork-schema": "0.0.0",
|
|
54
56
|
"@krapht/ork-server": "0.0.0",
|
|
55
57
|
"@krapht/ork-ui": "0.0.0",
|
|
56
58
|
"tsdown": "0.23.4"
|
|
57
59
|
},
|
|
58
60
|
"scripts": {
|
|
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"
|
|
61
|
+
"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 && node ../../tools/release/issues-workflow.ts && tsdown && cp ../../LICENSE ../../THIRD_PARTY_NOTICES.md . && cp -R ../ui/dist dist/ui"
|
|
60
62
|
}
|
|
61
63
|
}
|