@krapht/ork 1.0.0-alpha.14 → 1.0.0-alpha.16
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 +10 -4
- package/dist/__build_info__-DcRdK0Du.mjs +359 -0
- package/dist/apply-Mb0fNpCX.mjs +17 -0
- package/dist/{attach-CdtGX9se.mjs → attach-7lB5_jpr.mjs} +5 -4
- package/dist/{store-CleF7KeS.mjs → attach-B-FeE1dJ.mjs} +60 -25
- package/dist/bin.mjs +328 -1734
- package/dist/brief-BvMtIFio.mjs +58 -0
- package/dist/{builtin-files-Bdq23IIi.mjs → builtin-files-BRSOObAv.mjs} +1 -1
- package/dist/command-tree-BkSYWmYJ.mjs +63 -0
- package/dist/{completion--ywde-56.mjs → completion-Cgb2qPPh.mjs} +45 -6
- package/dist/context-BN9A2Nnu.mjs +106 -0
- package/dist/{find-BVX-bL4u.mjs → find-mLYsR0uC.mjs} +2 -2
- package/dist/flags-CdgIH1L6.mjs +230 -0
- package/dist/follow-ups-DxCsOby1.mjs +496 -0
- package/dist/{format-CvTE5CZQ.mjs → format-hiDvtNGM.mjs} +35 -8
- package/dist/{help-groups-CpR2UzSn.mjs → help-groups-hB6kY34d.mjs} +5 -63
- package/dist/{in-a-run-cL5N9gpH.mjs → in-a-run-a447hxq8.mjs} +1 -1
- package/dist/{__build_info__-B82-dGJY.mjs → install-23g_1ngs.mjs} +3 -227
- package/dist/{limits-Dhe--wmX.mjs → limits-C_Xz_t3C.mjs} +1 -1
- package/dist/{mcp-CsmboyXv.mjs → mcp-CWng9I9n.mjs} +6 -6
- package/dist/{once-CrwI-ZdN.mjs → once-lvwcWfVf.mjs} +3 -3
- package/dist/orphan-guard-BFKg8VSd.mjs +58 -0
- package/dist/{pause-DssrYcda.mjs → pause-Cy11KrXK.mjs} +3 -3
- package/dist/post-C9soCx7i.mjs +46 -0
- package/dist/print-BaWiWf4v.mjs +19 -0
- package/dist/project-records-BRiyQzUo.mjs +230 -0
- package/dist/{relay-DASRgsTQ.mjs → relay-C0M1TWMB.mjs} +1 -1
- package/dist/run-B-j3Qxqc.mjs +1464 -0
- package/dist/{run-D8divGCs.mjs → run-D_u9V_Ve.mjs} +1 -1
- package/dist/{send-CXv8bx0i.mjs → send-qBxj0JbJ.mjs} +2 -2
- package/dist/{src-njdtejgZ.mjs → src-CXBmaGwS.mjs} +213 -75
- package/dist/{src-C7z_6fhn.mjs → src-DHDQ0itC.mjs} +106 -50
- package/dist/{src-tsuZRu0H.mjs → src-DN-RPsil.mjs} +5423 -4467
- package/dist/{src-8Mul0695.mjs → src-nXLIwDrt.mjs} +325 -81
- package/dist/store-Dyr1mP8K.mjs +16 -0
- package/dist/ui/assets/app-BPSCxb_l.css +1 -0
- package/dist/ui/assets/app-CnNRowFn.js +111 -0
- package/dist/ui/assets/{index-D-_uPI2v.js → index-CVzwYlz3.js} +2 -2
- package/dist/ui/index.html +1 -1
- package/dist/{ui-CNO1M0qT.mjs → ui-DLrSt5ny.mjs} +3 -3
- package/dist/{usage-BVXi0iFY.mjs → usage-CxmpYYO4.mjs} +61 -107
- package/dist/{context-Y_I4Mofq.mjs → validate-BVrT43GO.mjs} +35 -108
- package/dist/{watch-DVNIciHp.mjs → watch-mkLm4t9D.mjs} +19 -5
- package/dist/{workflows-B30QkwTk.mjs → workflows-Bvl2Fquu.mjs} +2 -2
- package/dist/{worktrees-DD4_TpkH.mjs → worktrees-DOOCoS2w.mjs} +49 -20
- package/package.json +8 -4
- package/dist/ui/assets/app-D_nAPnHM.css +0 -1
- package/dist/ui/assets/app-DqBAtXSZ.js +0 -111
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# @krapht/ork
|
|
2
2
|
|
|
3
|
-
The `ork` command: orchestrate
|
|
4
|
-
serializable workflows.
|
|
3
|
+
The `ork` command: orchestrate coding agents (Claude Code, OpenCode, Command
|
|
4
|
+
Code) on your machine with hierarchical, serializable workflows.
|
|
5
5
|
|
|
6
|
-
> **Alpha.** Workflows of
|
|
6
|
+
> **Alpha.** Workflows of coding agents and tools, run from the terminal or the
|
|
7
7
|
> browser (`ork ui`). Expect changes until 1.0.0. See the
|
|
8
8
|
> [project README](https://github.com/mimshins/ork#readme), the
|
|
9
9
|
> [guides](https://github.com/mimshins/ork/tree/main/docs#readme) and
|
|
@@ -28,7 +28,13 @@ ork upgrade # a newer version? upgrade the way you installed Ork
|
|
|
28
28
|
|
|
29
29
|
A workflow that names no agent driver runs on the one that's ready. Today that
|
|
30
30
|
is `claude-code`: your own `claude`, logged in or with `ANTHROPIC_API_KEY` or a
|
|
31
|
-
cloud provider set.
|
|
31
|
+
cloud provider set. Your own `opencode` (`open-code`) and Command Code's `cmd`
|
|
32
|
+
(`command-code`) work too: `--driver <id>`, or a driver per node
|
|
33
|
+
([Drivers](https://github.com/mimshins/ork/blob/main/docs/guides/drivers.md)).
|
|
34
|
+
|
|
35
|
+
Ork is built with Ork: its own workflows plan, build, review and document its
|
|
36
|
+
features, and each run is measured against a manual session in
|
|
37
|
+
[Benchmarks: Ork building Ork](https://github.com/mimshins/ork/blob/main/benchmarks/README.md).
|
|
32
38
|
|
|
33
39
|
Ork never stores or asks for credentials: it uses the tools you already have,
|
|
34
40
|
signed in with your own accounts. Once a day it asks the npm registry whether a
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { C as compareVersions, T as parseVersion, w as isNewer } from "./src-DN-RPsil.mjs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
// ../core/src/follow-ups/change.ts
|
|
6
|
+
/** The project's follow-up whose id is, or starts with, `prefix`. */
|
|
7
|
+
const findFollowUp = (rows, project, prefix) => {
|
|
8
|
+
const exact = rows.followUp(project, prefix);
|
|
9
|
+
if (exact !== void 0) return {
|
|
10
|
+
ok: true,
|
|
11
|
+
followUp: exact
|
|
12
|
+
};
|
|
13
|
+
const matches = prefix === "" ? [] : rows.followUps(project).filter((f) => f.id.startsWith(prefix));
|
|
14
|
+
const [only, other] = matches;
|
|
15
|
+
if (only === void 0) return {
|
|
16
|
+
ok: false,
|
|
17
|
+
error: `No follow-up \`${prefix}\` in this project.`
|
|
18
|
+
};
|
|
19
|
+
if (other !== void 0) return {
|
|
20
|
+
ok: false,
|
|
21
|
+
error: `\`${prefix}\` matches ${matches.map((f) => f.id).join(", ")}; give more of the id.`
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
ok: true,
|
|
25
|
+
followUp: only
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
/** Which statuses each action starts from, and where it goes. */
|
|
29
|
+
const ACTIONS = {
|
|
30
|
+
done: {
|
|
31
|
+
from: ["waiting", "due"],
|
|
32
|
+
to: () => "done",
|
|
33
|
+
note: "done"
|
|
34
|
+
},
|
|
35
|
+
drop: {
|
|
36
|
+
from: ["waiting", "due"],
|
|
37
|
+
to: () => "dropped",
|
|
38
|
+
note: "dropped"
|
|
39
|
+
},
|
|
40
|
+
reopen: {
|
|
41
|
+
from: ["done", "dropped"],
|
|
42
|
+
to: (f) => f.due === "now" ? "due" : "waiting",
|
|
43
|
+
note: "reopened"
|
|
44
|
+
},
|
|
45
|
+
due: {
|
|
46
|
+
from: ["waiting"],
|
|
47
|
+
to: () => "due",
|
|
48
|
+
note: "marked due"
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
/** Why `action` can't be done to `f` now, or `undefined`. */
|
|
52
|
+
const refusal = (f, action) => {
|
|
53
|
+
const rule = ACTIONS[action];
|
|
54
|
+
return rule.from.includes(f.status) ? void 0 : `${f.id} is ${f.status}, so it can't be ${rule.note}.`;
|
|
55
|
+
};
|
|
56
|
+
const check = (f, action) => {
|
|
57
|
+
const error = refusal(f, action);
|
|
58
|
+
return error === void 0 ? { ok: true } : {
|
|
59
|
+
ok: false,
|
|
60
|
+
kind: "status",
|
|
61
|
+
why: `it is ${f.status}`,
|
|
62
|
+
error
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
/** Whether `to` names one GitHub issue or pull request by its URL. */
|
|
66
|
+
const isGithubIssueUrl = (to) => /^https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/(?:issues|pull)\/\d+$/u.test(to);
|
|
67
|
+
/**
|
|
68
|
+
* Why `f`'s draft can't be posted to GitHub whatever its status, or
|
|
69
|
+
* `undefined`.
|
|
70
|
+
*/
|
|
71
|
+
const githubDraftRefusal = (f) => {
|
|
72
|
+
if (f.kind !== "message" || f.draft?.channel !== "github") return {
|
|
73
|
+
why: "not drafted for the github channel",
|
|
74
|
+
error: "only a message drafted for the github channel is posted; send the others yourself"
|
|
75
|
+
};
|
|
76
|
+
return isGithubIssueUrl(f.draft.to) ? void 0 : {
|
|
77
|
+
why: "its `to` isn't a GitHub issue or pull request URL",
|
|
78
|
+
error: `${f.id}'s \`to\` (${f.draft.to}) isn't a GitHub issue or pull request URL, so it is not posted; copy the text and send it yourself.`
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
const postCheck = (f) => {
|
|
82
|
+
const open = f.status === "waiting" || f.status === "due";
|
|
83
|
+
const refused = githubDraftRefusal(f);
|
|
84
|
+
if (refused !== void 0) return {
|
|
85
|
+
ok: false,
|
|
86
|
+
kind: "draft",
|
|
87
|
+
...refused
|
|
88
|
+
};
|
|
89
|
+
return open ? { ok: true } : {
|
|
90
|
+
ok: false,
|
|
91
|
+
kind: "status",
|
|
92
|
+
why: `it is ${f.status}`,
|
|
93
|
+
error: `${f.id} is ${f.status}, so it is not posted.`
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
const followUpActions = (f) => ({
|
|
97
|
+
done: check(f, "done"),
|
|
98
|
+
drop: check(f, "drop"),
|
|
99
|
+
reopen: check(f, "reopen"),
|
|
100
|
+
due: check(f, "due"),
|
|
101
|
+
post: postCheck(f)
|
|
102
|
+
});
|
|
103
|
+
/** Applies `action` to `followUp`, and keeps it. */
|
|
104
|
+
const changeFollowUp = (rows, followUp, action, now = () => (/* @__PURE__ */ new Date()).toISOString()) => {
|
|
105
|
+
const rule = ACTIONS[action];
|
|
106
|
+
const refused = refusal(followUp, action);
|
|
107
|
+
if (refused !== void 0) return {
|
|
108
|
+
ok: false,
|
|
109
|
+
error: refused
|
|
110
|
+
};
|
|
111
|
+
const { note: _old, ...rest } = followUp;
|
|
112
|
+
const changed = {
|
|
113
|
+
...rest,
|
|
114
|
+
status: rule.to(followUp),
|
|
115
|
+
note: rule.note,
|
|
116
|
+
updatedAt: now()
|
|
117
|
+
};
|
|
118
|
+
rows.putFollowUp(changed);
|
|
119
|
+
return {
|
|
120
|
+
ok: true,
|
|
121
|
+
followUp: changed
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Why a due follow-up is due, read from the note `due.ts` left: its ref merged,
|
|
126
|
+
* its date came, its ref closed unmerged, or a person marked it. `undefined`
|
|
127
|
+
* for one that is not due, or was due from the start.
|
|
128
|
+
*/
|
|
129
|
+
const dueCause = (f) => {
|
|
130
|
+
if (f.status !== "due" || f.note === void 0) return void 0;
|
|
131
|
+
if (f.note.includes("closed without merging")) return "closed-unmerged";
|
|
132
|
+
if (f.note.endsWith(" merged")) return "merged";
|
|
133
|
+
if (f.note.startsWith("due on ")) return "overdue";
|
|
134
|
+
return f.note === "marked due" ? "marked" : void 0;
|
|
135
|
+
};
|
|
136
|
+
// ../core/src/remote/session.ts
|
|
137
|
+
const chattable = (type) => type === "agent.leader" || type === "agent.worker";
|
|
138
|
+
/** Reads `node`'s session from a run's events, and every node's last output. */
|
|
139
|
+
const nodeSession = (events, node) => {
|
|
140
|
+
const session = {
|
|
141
|
+
known: false,
|
|
142
|
+
chattable: false,
|
|
143
|
+
running: false
|
|
144
|
+
};
|
|
145
|
+
const outputs = {};
|
|
146
|
+
for (const { event } of events) {
|
|
147
|
+
if (event.type === "run.started") {
|
|
148
|
+
const declared = event.nodes.find((n) => n.id === node);
|
|
149
|
+
if (declared !== void 0) {
|
|
150
|
+
session.known = true;
|
|
151
|
+
session.chattable = chattable(declared.type);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (event.type === "node.finished") outputs[event.id] = event.output;
|
|
155
|
+
if (!("id" in event) || event.id !== node) continue;
|
|
156
|
+
if (event.type === "node.started") {
|
|
157
|
+
session.known = true;
|
|
158
|
+
session.chattable = chattable(event.nodeType);
|
|
159
|
+
session.running = true;
|
|
160
|
+
}
|
|
161
|
+
if (event.type === "node.finished") session.running = false;
|
|
162
|
+
if (event.type === "worktree" && event.action === "created") session.checkout = {
|
|
163
|
+
path: event.path,
|
|
164
|
+
...event.branch === void 0 ? {} : { branch: event.branch },
|
|
165
|
+
own: true,
|
|
166
|
+
owner: node
|
|
167
|
+
};
|
|
168
|
+
if (event.type !== "node.event") continue;
|
|
169
|
+
const inner = event.event;
|
|
170
|
+
if (inner.fork === true) continue;
|
|
171
|
+
if (inner.type === "session.started") {
|
|
172
|
+
session.sessionId = inner.sessionId;
|
|
173
|
+
if (inner.cwd !== void 0) session.cwd = inner.cwd;
|
|
174
|
+
if (inner.driver !== void 0) session.driver = inner.driver;
|
|
175
|
+
if (inner.model !== "") session.model = inner.model;
|
|
176
|
+
}
|
|
177
|
+
if (inner.type === "session.finished" && inner.outcome.sessionId !== "") session.sessionId = inner.outcome.sessionId;
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
session,
|
|
181
|
+
outputs
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
/** The dist-tags every install follows, toward and after 1.0.0. */
|
|
185
|
+
const CHANNEL_TAGS = [
|
|
186
|
+
"latest",
|
|
187
|
+
"alpha",
|
|
188
|
+
"beta",
|
|
189
|
+
"rc"
|
|
190
|
+
];
|
|
191
|
+
/** The dist-tag of snapshots from `main`, followed only by a canary. */
|
|
192
|
+
const CANARY_TAG = "canary";
|
|
193
|
+
const updateCachePath = (home = homedir()) => path.join(home, ".ork", "update-check.json");
|
|
194
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
195
|
+
const text = (record, key) => {
|
|
196
|
+
const value = record[key];
|
|
197
|
+
return typeof value === "string" ? value : void 0;
|
|
198
|
+
};
|
|
199
|
+
const notifiedOf = (value) => {
|
|
200
|
+
if (!isRecord(value)) return void 0;
|
|
201
|
+
const version = text(value, "version");
|
|
202
|
+
const at = text(value, "at");
|
|
203
|
+
return version === void 0 || at === void 0 ? void 0 : {
|
|
204
|
+
version,
|
|
205
|
+
at
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
/** The cache, or `null` when there is none or it cannot be read. */
|
|
209
|
+
const readUpdateCache = (file) => {
|
|
210
|
+
let parsed;
|
|
211
|
+
try {
|
|
212
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
if (!isRecord(parsed)) return null;
|
|
217
|
+
const checkedAt = text(parsed, "checkedAt");
|
|
218
|
+
if (checkedAt === void 0) return null;
|
|
219
|
+
const latest = text(parsed, "latest");
|
|
220
|
+
const notified = notifiedOf(parsed["notified"]);
|
|
221
|
+
return {
|
|
222
|
+
checkedAt,
|
|
223
|
+
...latest === void 0 ? {} : { latest },
|
|
224
|
+
...notified === void 0 ? {} : { notified }
|
|
225
|
+
};
|
|
226
|
+
};
|
|
227
|
+
/** Writes the cache whole (a temporary file, then a rename). */
|
|
228
|
+
const writeUpdateCache = (file, cache) => {
|
|
229
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
230
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
231
|
+
writeFileSync(temporary, `${JSON.stringify(cache, null, 2)}\n`);
|
|
232
|
+
renameSync(temporary, file);
|
|
233
|
+
};
|
|
234
|
+
const age = (iso, now) => {
|
|
235
|
+
const time = Date.parse(iso);
|
|
236
|
+
return Number.isNaN(time) ? Infinity : now.getTime() - time;
|
|
237
|
+
};
|
|
238
|
+
/** Whether it is time to ask the registry again. */
|
|
239
|
+
const checkDue = (cache, now) => cache === null || age(cache.checkedAt, now) >= 864e5;
|
|
240
|
+
/** Whether `version` is a canary snapshot: its pre-release starts `canary`. */
|
|
241
|
+
const isCanary = (version) => parseVersion(version)?.pre[0]?.startsWith(CANARY_TAG) === true;
|
|
242
|
+
/**
|
|
243
|
+
* The newest version, by semver precedence, among the dist-tags `current`
|
|
244
|
+
* follows: the channel tags, and `canary` too when `current` is a canary. A tag
|
|
245
|
+
* whose value doesn't parse is skipped; `null` when none does.
|
|
246
|
+
*/
|
|
247
|
+
const newestTagged = (tags, current) => {
|
|
248
|
+
return (current !== void 0 && isCanary(current) ? [...CHANNEL_TAGS, "canary"] : CHANNEL_TAGS).flatMap((tag) => {
|
|
249
|
+
const value = tags[tag];
|
|
250
|
+
const version = typeof value === "string" ? parseVersion(value) : null;
|
|
251
|
+
return typeof value === "string" && version !== null ? [{
|
|
252
|
+
value,
|
|
253
|
+
version
|
|
254
|
+
}] : [];
|
|
255
|
+
}).reduce((best, candidate) => best === null || compareVersions(candidate.version, best.version) > 0 ? candidate : best, null)?.value ?? null;
|
|
256
|
+
};
|
|
257
|
+
/**
|
|
258
|
+
* The newest version on the registry that the running version follows ({@link
|
|
259
|
+
* newestTagged}), or `null` on any failure.
|
|
260
|
+
*/
|
|
261
|
+
const fetchLatest = async (name, options = {}) => {
|
|
262
|
+
const registry = (options.registry ?? "https://registry.npmjs.org").replace(/\/+$/u, "");
|
|
263
|
+
const get = options.fetch ?? globalThis.fetch;
|
|
264
|
+
try {
|
|
265
|
+
const response = await get(`${registry}/-/package/${name}/dist-tags`, {
|
|
266
|
+
headers: { accept: "application/json" },
|
|
267
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 5e3)
|
|
268
|
+
});
|
|
269
|
+
if (!response.ok) return null;
|
|
270
|
+
const tags = await response.json();
|
|
271
|
+
return isRecord(tags) ? newestTagged(tags, options.current) : null;
|
|
272
|
+
} catch {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
/**
|
|
277
|
+
* Asks the registry and records the answer. A failed check is recorded too, so
|
|
278
|
+
* an offline machine is not asked again until the interval passes; the last
|
|
279
|
+
* known version is kept. `options.current` is the running version.
|
|
280
|
+
*/
|
|
281
|
+
const refreshUpdateCache = async (file, name, now, options = {}) => {
|
|
282
|
+
const before = readUpdateCache(file);
|
|
283
|
+
const latest = await fetchLatest(name, options) ?? before?.latest;
|
|
284
|
+
const cache = {
|
|
285
|
+
...before,
|
|
286
|
+
checkedAt: now.toISOString(),
|
|
287
|
+
...latest === void 0 ? {} : { latest }
|
|
288
|
+
};
|
|
289
|
+
writeUpdateCache(file, cache);
|
|
290
|
+
return cache;
|
|
291
|
+
};
|
|
292
|
+
/** The newer version the cache knows of, if any. */
|
|
293
|
+
const newerVersion = (cache, current) => cache?.latest !== void 0 && isNewer(cache.latest, current) ? cache.latest : null;
|
|
294
|
+
/**
|
|
295
|
+
* The version to tell the user about now: newer than theirs, and not already
|
|
296
|
+
* told today. `null` when there is nothing to say.
|
|
297
|
+
*/
|
|
298
|
+
const pendingNotice = (cache, current, now) => {
|
|
299
|
+
const newer = newerVersion(cache, current);
|
|
300
|
+
if (newer === null) return null;
|
|
301
|
+
const told = cache?.notified;
|
|
302
|
+
return told?.version === newer && age(told.at, now) < 864e5 ? null : newer;
|
|
303
|
+
};
|
|
304
|
+
// ../core/src/update/switches.ts
|
|
305
|
+
/**
|
|
306
|
+
* @file The environment's say over the update check (ADR-038): the variables
|
|
307
|
+
* that turn it off (`ORK_NO_UPDATE_CHECK`, the common `NO_UPDATE_NOTIFIER`,
|
|
308
|
+
* and `CI`), and the registry to ask instead of npm's. The CLI adds its own
|
|
309
|
+
* rules about where a notice may go; the server behind `ork ui` uses only
|
|
310
|
+
* these.
|
|
311
|
+
*/
|
|
312
|
+
/** Turns the automatic check and its notice off. */
|
|
313
|
+
const NO_UPDATE_ENV = "ORK_NO_UPDATE_CHECK";
|
|
314
|
+
/** A registry to ask instead of npm's, e.g. a company mirror. */
|
|
315
|
+
const UPDATE_REGISTRY_ENV = "ORK_UPDATE_REGISTRY";
|
|
316
|
+
/** The variables that turn the check off, in the order they are looked at. */
|
|
317
|
+
const UPDATE_SWITCHES = [
|
|
318
|
+
NO_UPDATE_ENV,
|
|
319
|
+
"NO_UPDATE_NOTIFIER",
|
|
320
|
+
"CI"
|
|
321
|
+
];
|
|
322
|
+
/** Set to anything but empty, `0` or `false`. */
|
|
323
|
+
const isSwitchedOn = (value) => value !== void 0 && value !== "" && value !== "0" && value.toLowerCase() !== "false";
|
|
324
|
+
/** The variable that turns the check off in `env`, or `null` when none does. */
|
|
325
|
+
const updateSwitchOff = (env) => UPDATE_SWITCHES.find((name) => isSwitchedOn(env[name])) ?? null;
|
|
326
|
+
/** The registry `ORK_UPDATE_REGISTRY` names, if it names one. */
|
|
327
|
+
const registryOf = (env) => {
|
|
328
|
+
const registry = env[UPDATE_REGISTRY_ENV];
|
|
329
|
+
return registry === void 0 || registry === "" ? void 0 : registry;
|
|
330
|
+
};
|
|
331
|
+
/** The host of a registry that isn't an http(s) URL, past any `user:pass@`. */
|
|
332
|
+
const hostOf = (registry) => registry.replace(/^[a-z][\w+.-]*:\/\//iu, "").replace(/^[^/?#]*@/u, "").split(/[/?#]/u)[0] ?? "";
|
|
333
|
+
/**
|
|
334
|
+
* The registry as it may be shown (on a page, in a message): an http(s) URL
|
|
335
|
+
* without its user name and password, which a mirror's URL may carry; anything
|
|
336
|
+
* else only as its host.
|
|
337
|
+
*/
|
|
338
|
+
const registryShown = (registry) => {
|
|
339
|
+
try {
|
|
340
|
+
const url = new URL(registry);
|
|
341
|
+
if (url.protocol === "http:" || url.protocol === "https:") {
|
|
342
|
+
if (url.username === "" && url.password === "") return registry;
|
|
343
|
+
url.username = "";
|
|
344
|
+
url.password = "";
|
|
345
|
+
return url.href;
|
|
346
|
+
}
|
|
347
|
+
} catch {}
|
|
348
|
+
return hostOf(registry);
|
|
349
|
+
};
|
|
350
|
+
// src/__build_info__.ts
|
|
351
|
+
/**
|
|
352
|
+
* @file The published package's name and version, bound to
|
|
353
|
+
* `packages/cli/package.json`. Do not edit by hand: `pnpm changesets:version`
|
|
354
|
+
* and `pnpm build` rewrite it (`tools/release/build-info.ts`), and a test
|
|
355
|
+
* fails when it is stale.
|
|
356
|
+
*/
|
|
357
|
+
const ORK_NAME = "@krapht/ork";
|
|
358
|
+
const ORK_VERSION = "1.0.0-alpha.16";
|
|
359
|
+
export { dueCause as _, registryShown as a, githubDraftRefusal as b, fetchLatest as c, readUpdateCache as d, refreshUpdateCache as f, changeFollowUp as g, nodeSession as h, registryOf as i, newerVersion as l, writeUpdateCache as m, ORK_VERSION as n, updateSwitchOff as o, updateCachePath as p, isSwitchedOn as r, checkDue as s, ORK_NAME as t, pendingNotice as u, findFollowUp as v, isGithubIssueUrl as x, followUpActions as y };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// ../core/src/list-filters/apply.ts
|
|
2
|
+
/**
|
|
3
|
+
* @file Keeps the rows a list's filter matches (RFC-017). The list's own order
|
|
4
|
+
* and `--limit` stay the caller's: filter, then order, then limit.
|
|
5
|
+
*/
|
|
6
|
+
const fieldMatches = (field, row, wanted) => {
|
|
7
|
+
if (wanted === void 0 || wanted === "all" || field.get === void 0) return true;
|
|
8
|
+
const got = field.get(row);
|
|
9
|
+
if (got === void 0) return false;
|
|
10
|
+
const values = typeof got === "string" ? [got] : got;
|
|
11
|
+
if (field.type === "since") return values.some((value) => Date.parse(value) >= Date.parse(wanted));
|
|
12
|
+
if (field.type === "until") return values.some((value) => Date.parse(value) < Date.parse(wanted));
|
|
13
|
+
return values.includes(wanted);
|
|
14
|
+
};
|
|
15
|
+
/** The rows of `rows` every field of `filter` matches, in their order. */
|
|
16
|
+
const applyFilters = (table, rows, filter) => rows.filter((row) => table.fields.every((field) => fieldMatches(field, row, filter[field.name])));
|
|
17
|
+
export { applyFilters as t };
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import "./src-
|
|
2
|
-
import { l as createTerminalHuman,
|
|
3
|
-
import { t as findRun } from "./find-
|
|
1
|
+
import "./src-DN-RPsil.mjs";
|
|
2
|
+
import { l as createTerminalHuman, t as attachRun } from "./attach-B-FeE1dJ.mjs";
|
|
3
|
+
import { t as findRun } from "./find-mLYsR0uC.mjs";
|
|
4
4
|
import { c as write, l as writeErr, s as style } from "./output-B1kwo0YU.mjs";
|
|
5
|
-
import { n as attachRefusal } from "./in-a-run-
|
|
5
|
+
import { n as attachRefusal } from "./in-a-run-a447hxq8.mjs";
|
|
6
|
+
import { t as withStore } from "./store-Dyr1mP8K.mjs";
|
|
6
7
|
import { defineCommand } from "citty";
|
|
7
8
|
// src/commands/attach.ts
|
|
8
9
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Ft as slotWaitLine, Gn as endOf, I as isNodeEvent, Jn as END_KINDS, Kn as outcomeEnd, kt as lockWaitLine, nt as answerDecision, rt as pendingDecisions, tt as followRunTree, ut as followEvents, vr as maskSecrets } from "./src-DN-RPsil.mjs";
|
|
2
|
+
import { t as applyFilters } from "./apply-Mb0fNpCX.mjs";
|
|
2
3
|
import { C as termOf, D as wrap, E as widthOf, O as readKeys, _ as interrupt, a as plain, b as canKey, g as createRegion, h as paint, m as bold, n as cost, p as apply, r as costOf, s as style, u as confirmOrStop, v as readLine, w as cut, x as plainEnv, y as withdrawn } from "./output-B1kwo0YU.mjs";
|
|
3
4
|
import { n as chip, r as nodeChip, t as CHIP_LABELS } from "./chips-BqcT58qr.mjs";
|
|
4
5
|
import { a as createView, i as formatBusEvent, n as nodeVerdict, o as styler, r as firstLine, t as hasConflict } from "./verdict-CMvPCpWC.mjs";
|
|
@@ -769,8 +770,9 @@ const createTerminalHuman = (io) => {
|
|
|
769
770
|
/**
|
|
770
771
|
* @file How a node's end reads on the terminal (specs/runtime/session-ends.md):
|
|
771
772
|
* its kind's label and Ork's own message, a note for a kind that may pass,
|
|
772
|
-
* the setup hint for an account or provider end,
|
|
773
|
-
* only when asked for. Never
|
|
773
|
+
* the setup hint for an account or provider end, the last line of a failed
|
|
774
|
+
* tool's own stderr, and the scrubbed raw detail only when asked for. Never
|
|
775
|
+
* the provider's own words first.
|
|
774
776
|
*/
|
|
775
777
|
/** What a transient kind says in place of its hint on the log line. */
|
|
776
778
|
const MAY_PASS = "(may pass; ork resume retries)";
|
|
@@ -779,11 +781,17 @@ const endHeadline = (end) => {
|
|
|
779
781
|
const { label, transient } = END_KINDS[end.kind];
|
|
780
782
|
return `${label}: ${end.message}${transient ? ` ${MAY_PASS}` : ""}`;
|
|
781
783
|
};
|
|
784
|
+
/** The longest a script's last stderr line is shown on the log line. */
|
|
785
|
+
const LAST_LINE_LIMIT = 200;
|
|
786
|
+
/** The last non-empty line of a scrubbed detail, cut; "" when there is none. */
|
|
787
|
+
const lastLine = (detail) => (detail ?? "").split("\n").map((line) => line.trim()).findLast((line) => line !== "")?.slice(0, LAST_LINE_LIMIT) ?? "";
|
|
782
788
|
/**
|
|
783
789
|
* The lines under a headline: the hint for an account or provider end (what to
|
|
784
790
|
* set up) or whenever `full` (`--verbose`, one node's log), and the detail only
|
|
785
791
|
* when `full`. A transient kind's hint is its retry wording, which the headline
|
|
786
|
-
* already says, so it is not printed twice.
|
|
792
|
+
* already says, so it is not printed twice. A failed tool (`command_failed`)
|
|
793
|
+
* also shows the last line of its detail without `full`: a script's stderr is
|
|
794
|
+
* the workflow's own message, not the provider's.
|
|
787
795
|
*/
|
|
788
796
|
const endLines = (end, full, term = createView().term) => {
|
|
789
797
|
const style = styler(term);
|
|
@@ -792,6 +800,10 @@ const endLines = (end, full, term = createView().term) => {
|
|
|
792
800
|
const lines = [];
|
|
793
801
|
if (hint !== "" && !transient && (full || setup)) lines.push(` ${style("dim", hint)}`);
|
|
794
802
|
if (full && end.detail !== void 0) lines.push(style("dim", " details:"), ...end.detail.split("\n").map((line) => style("dim", ` ${line}`)));
|
|
803
|
+
else if (end.kind === "command_failed") {
|
|
804
|
+
const last = lastLine(end.detail);
|
|
805
|
+
if (last !== "") lines.push(` ${style("dim", last)}`);
|
|
806
|
+
}
|
|
795
807
|
return lines;
|
|
796
808
|
};
|
|
797
809
|
/**
|
|
@@ -1027,7 +1039,8 @@ const formatRunEvent = (event, width, tools, terminal = false, verbose = false,
|
|
|
1027
1039
|
if (event.type === "node.started") {
|
|
1028
1040
|
const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
|
|
1029
1041
|
const owner = event.owner === void 0 ? "" : `, for ${event.owner}`;
|
|
1030
|
-
|
|
1042
|
+
const named = event.description === void 0 ? "" : ` · ${event.description}`;
|
|
1043
|
+
return [`${tag(event.id)} ${style("dim", `${event.nodeType}${round}${owner}${named}`)}`];
|
|
1031
1044
|
}
|
|
1032
1045
|
if (event.type === "node.event") {
|
|
1033
1046
|
const inner = event.event;
|
|
@@ -1110,7 +1123,7 @@ const formatRunEvent = (event, width, tools, terminal = false, verbose = false,
|
|
|
1110
1123
|
/** Turns stored events into lines, keeping the state a transcript needs. */
|
|
1111
1124
|
const createLogPrinter = (options) => {
|
|
1112
1125
|
const tools = /* @__PURE__ */ new Map();
|
|
1113
|
-
const view = createView();
|
|
1126
|
+
const view = options.view ?? createView();
|
|
1114
1127
|
let width = 8;
|
|
1115
1128
|
return (stored) => {
|
|
1116
1129
|
const { event } = stored;
|
|
@@ -1130,6 +1143,19 @@ const unknownNode = (stored, node, live) => {
|
|
|
1130
1143
|
const declared = stored.some(({ event }) => event.type === "run.started" && event.nodes.some((n) => n.id === node));
|
|
1131
1144
|
return named || declared || live ? null : `Run has no node ${node}.`;
|
|
1132
1145
|
};
|
|
1146
|
+
/** `ork logs`'s filter flags (RFC-017): an event's time; no ids. */
|
|
1147
|
+
const logListTable = { fields: [{
|
|
1148
|
+
name: "since",
|
|
1149
|
+
description: "Only events since an age (24h, 30d, 12w), a day (2026-11-01), or all",
|
|
1150
|
+
type: "since",
|
|
1151
|
+
all: true,
|
|
1152
|
+
get: (stored) => stored.at
|
|
1153
|
+
}, {
|
|
1154
|
+
name: "until",
|
|
1155
|
+
description: "Only events on or before this day (2026-11-01)",
|
|
1156
|
+
type: "until",
|
|
1157
|
+
get: (stored) => stored.at
|
|
1158
|
+
}] };
|
|
1133
1159
|
/**
|
|
1134
1160
|
* Prints a run's events; with `follow`, then each new one as it is stored:
|
|
1135
1161
|
* pushed over the run's socket, or polled from the store when there is none.
|
|
@@ -1138,8 +1164,13 @@ const unknownNode = (stored, node, live) => {
|
|
|
1138
1164
|
*/
|
|
1139
1165
|
const followRun = async (store, run, out, options) => {
|
|
1140
1166
|
const print = createLogPrinter(options);
|
|
1167
|
+
const matches = (stored) => applyFilters(logListTable, [stored], options.filter ?? {}).length > 0 && (options.node === void 0 || isNodeEvent(stored.event, options.node));
|
|
1168
|
+
const replayed = options.limit === void 0 ? [] : store.events(run.id, options.after ?? 0).filter((stored) => matches(stored)).slice(0, -options.limit);
|
|
1169
|
+
const dropped = new Set(replayed.map((stored) => stored.seq));
|
|
1141
1170
|
if (await followEvents(store, run.id, (stored) => {
|
|
1142
|
-
|
|
1171
|
+
const lines = print(stored);
|
|
1172
|
+
if (!matches(stored) || dropped.has(stored.seq)) return;
|
|
1173
|
+
for (const line of lines) out(line);
|
|
1143
1174
|
}, {
|
|
1144
1175
|
follow: options.follow,
|
|
1145
1176
|
...options.after === void 0 ? {} : { after: options.after },
|
|
@@ -1197,13 +1228,15 @@ const askStored = async (store, human, decision, { place, by, signal, home }) =>
|
|
|
1197
1228
|
* asked through one `TerminalHuman`, withdrawn when it closes elsewhere.
|
|
1198
1229
|
*/
|
|
1199
1230
|
const where = (path) => path.join(" › ");
|
|
1231
|
+
/** The gutter before a called run's lines: its call path, then ` › `. */
|
|
1232
|
+
const gutterOf = (path) => path.length === 0 ? "" : `${where(path)} › `;
|
|
1200
1233
|
/**
|
|
1201
1234
|
* Prints the events of `root` and of the runs it called until the root ends or
|
|
1202
1235
|
* `signal` aborts, and asks each open decision with `options.human`.
|
|
1203
1236
|
*/
|
|
1204
1237
|
const attachRun = async (store, root, out, options = {}) => {
|
|
1205
1238
|
const { human, basePath = [] } = options;
|
|
1206
|
-
const { term } = createView();
|
|
1239
|
+
const { term } = options.view ?? createView();
|
|
1207
1240
|
const say = styler(term);
|
|
1208
1241
|
const printers = /* @__PURE__ */ new Map();
|
|
1209
1242
|
const prompts = /* @__PURE__ */ new Map();
|
|
@@ -1215,9 +1248,25 @@ const attachRun = async (store, root, out, options = {}) => {
|
|
|
1215
1248
|
succeeded: false
|
|
1216
1249
|
};
|
|
1217
1250
|
const show = (path, lines) => {
|
|
1218
|
-
const
|
|
1251
|
+
const gutter = gutterOf(path);
|
|
1252
|
+
const prefix = gutter === "" ? "" : paint(term, "muted", gutter);
|
|
1219
1253
|
for (const line of lines) out(line === "" ? line : `${prefix}${line}`);
|
|
1220
1254
|
};
|
|
1255
|
+
/**
|
|
1256
|
+
* A run's printer, laid out in the columns its gutter leaves, so a line fits
|
|
1257
|
+
* the terminal with the gutter before it and a wrapped line goes on under its
|
|
1258
|
+
* own text, not at the terminal's first column.
|
|
1259
|
+
*/
|
|
1260
|
+
const printerFor = (path) => createLogPrinter({
|
|
1261
|
+
json: false,
|
|
1262
|
+
view: {
|
|
1263
|
+
term: {
|
|
1264
|
+
...term,
|
|
1265
|
+
width: Math.max(20, term.width - widthOf(gutterOf(path)))
|
|
1266
|
+
},
|
|
1267
|
+
roots: /* @__PURE__ */ new Map()
|
|
1268
|
+
}
|
|
1269
|
+
});
|
|
1221
1270
|
const ask = (run, path, id) => {
|
|
1222
1271
|
const decision = store.decision(id);
|
|
1223
1272
|
if (human === void 0 || decision === void 0) return;
|
|
@@ -1277,7 +1326,7 @@ const attachRun = async (store, root, out, options = {}) => {
|
|
|
1277
1326
|
}
|
|
1278
1327
|
let print = printers.get(run);
|
|
1279
1328
|
if (print === void 0) {
|
|
1280
|
-
print =
|
|
1329
|
+
print = printerFor(path);
|
|
1281
1330
|
printers.set(run, print);
|
|
1282
1331
|
}
|
|
1283
1332
|
show(path, print(stored));
|
|
@@ -1296,18 +1345,4 @@ const attachRun = async (store, root, out, options = {}) => {
|
|
|
1296
1345
|
exitCode: end === "aborted" || seen.succeeded ? 0 : 1
|
|
1297
1346
|
};
|
|
1298
1347
|
};
|
|
1299
|
-
|
|
1300
|
-
/**
|
|
1301
|
-
* @file Opening the user's store (`~/.ork/state.db`) for the length of one
|
|
1302
|
-
* command, and closing it after, whatever happens.
|
|
1303
|
-
*/
|
|
1304
|
-
/** Runs `body` with the user's store open, and closes it after. */
|
|
1305
|
-
const withStore = async (body) => {
|
|
1306
|
-
const store = openStore();
|
|
1307
|
-
try {
|
|
1308
|
-
await body(store);
|
|
1309
|
-
} finally {
|
|
1310
|
-
store.close();
|
|
1311
|
-
}
|
|
1312
|
-
};
|
|
1313
|
-
export { unknownNode as a, callsShown as c, createDecisionQueue as d, followRun as i, createTerminalHuman as l, attachRun as n, formatEvent as o, askStored as r, formatRunEvent as s, withStore as t, readText as u };
|
|
1348
|
+
export { unknownNode as a, callsShown as c, createDecisionQueue as d, logListTable as i, createTerminalHuman as l, askStored as n, formatEvent as o, followRun as r, formatRunEvent as s, attachRun as t, readText as u };
|