@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,249 @@
|
|
|
1
|
+
import { Fn as lockWaitsOf, Hn as isItem, Ln as runStatus, Nt as lockWaitLine, Pn as liveStatus, Rn as slotWaitsOf, kr as maskSecrets } from "./src-8cOPOgrT.mjs";
|
|
2
|
+
import { C as termOf, E as widthOf, n as cost, o as shortCount, r as costOf, s as style, w as cut } from "./output-B1kwo0YU.mjs";
|
|
3
|
+
import { i as statusChip, n as chip } from "./chips-BqcT58qr.mjs";
|
|
4
|
+
import { n as nodeVerdict } from "./verdict-CMvPCpWC.mjs";
|
|
5
|
+
// src/term/table.ts
|
|
6
|
+
/**
|
|
7
|
+
* @file Aligned columns for the listing commands: each column as wide as its
|
|
8
|
+
* widest cell, a two-space gap, the last column left unpadded. Rich, a cell
|
|
9
|
+
* may be a chip and a table wider than the screen cuts its flexible columns
|
|
10
|
+
* with `…`; plain, nothing is cut or styled, as the listings always printed.
|
|
11
|
+
*/
|
|
12
|
+
/** A dimmed header cell. */
|
|
13
|
+
const header = (text) => ({
|
|
14
|
+
text,
|
|
15
|
+
look: (s) => style("dim", s)
|
|
16
|
+
});
|
|
17
|
+
const GAP = " ";
|
|
18
|
+
/** A flexible column is never cut under this many columns. */
|
|
19
|
+
const MIN_FLEX = 8;
|
|
20
|
+
/** The text a cell shows, and its columns. */
|
|
21
|
+
const shown = (term, cell) => term.rich && cell.chip !== void 0 ? cell.chip.label : cell.text;
|
|
22
|
+
const widthOfCell = (term, cell) => widthOf(shown(term, cell)) + (term.rich && cell.chip !== void 0 ? 2 : 0);
|
|
23
|
+
/** The lines of a table, one per row (a header is a row the caller passes). */
|
|
24
|
+
const table = (term, columns, rows, indent = "") => {
|
|
25
|
+
const widths = columns.map((_, c) => Math.max(0, ...rows.map((row) => widthOfCell(term, row[c] ?? { text: "" }))));
|
|
26
|
+
if (term.rich) {
|
|
27
|
+
const gaps = 2 * (columns.length - 1);
|
|
28
|
+
let over = indent.length + widths.reduce((a, b) => a + b, 0) + gaps - term.width;
|
|
29
|
+
columns.forEach((column, c) => {
|
|
30
|
+
const width = widths[c] ?? 0;
|
|
31
|
+
if (over <= 0 || column.flex !== true || width <= MIN_FLEX) return;
|
|
32
|
+
const next = Math.max(MIN_FLEX, width - over);
|
|
33
|
+
over -= width - next;
|
|
34
|
+
widths[c] = next;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
const last = columns.length - 1;
|
|
38
|
+
return rows.map((row) => indent + columns.map((column, c) => {
|
|
39
|
+
const cell = row[c] ?? { text: "" };
|
|
40
|
+
const width = widths[c] ?? 0;
|
|
41
|
+
const look = cell.look ?? ((s) => s);
|
|
42
|
+
if (term.rich && cell.chip !== void 0) {
|
|
43
|
+
const used = widthOfCell(term, cell);
|
|
44
|
+
const pad = " ".repeat(Math.max(0, width - used));
|
|
45
|
+
return column.align === "right" ? pad + chip(term, cell.chip.label, cell.chip.role) : chip(term, cell.chip.label, cell.chip.role) + (c === last ? "" : pad);
|
|
46
|
+
}
|
|
47
|
+
const text = cut(shown(term, cell), width);
|
|
48
|
+
const pad = " ".repeat(Math.max(0, width - widthOf(text)));
|
|
49
|
+
if (column.align === "right") return look(pad + text);
|
|
50
|
+
return c === last ? look(text) : look(text + pad);
|
|
51
|
+
}).join(GAP));
|
|
52
|
+
};
|
|
53
|
+
// src/runs/format.ts
|
|
54
|
+
/**
|
|
55
|
+
* @file How stored runs read on the terminal: the `ork ps` table and the `ork
|
|
56
|
+
* tree` ownership tree with each node's state, tokens and cost, and what is
|
|
57
|
+
* paused (ADR-041).
|
|
58
|
+
*/
|
|
59
|
+
const STATUS_STYLE = {
|
|
60
|
+
running: "cyan",
|
|
61
|
+
paused: "yellow",
|
|
62
|
+
succeeded: "green",
|
|
63
|
+
failed: "red",
|
|
64
|
+
crashed: "red",
|
|
65
|
+
budget_exhausted: "yellow",
|
|
66
|
+
quota_exhausted: "yellow",
|
|
67
|
+
timed_out: "yellow",
|
|
68
|
+
interrupted: "yellow"
|
|
69
|
+
};
|
|
70
|
+
/** `just now`, `5m ago`, `3h ago`, `2d ago`, then the date. */
|
|
71
|
+
const ago = (iso, now = Date.now()) => {
|
|
72
|
+
const minutes = Math.floor((now - Date.parse(iso)) / 6e4);
|
|
73
|
+
if (minutes < 1) return "just now";
|
|
74
|
+
if (minutes < 60) return `${String(minutes)}m ago`;
|
|
75
|
+
if (minutes < 1440) return `${String(Math.floor(minutes / 60))}h ago`;
|
|
76
|
+
if (minutes < 10080) return `${String(Math.floor(minutes / 1440))}d ago`;
|
|
77
|
+
return iso.slice(0, 10);
|
|
78
|
+
};
|
|
79
|
+
/** `42s`, `3m12s`, `1h05m`. */
|
|
80
|
+
const duration = (ms) => {
|
|
81
|
+
const s = Math.max(0, Math.round(ms / 1e3));
|
|
82
|
+
if (s < 60) return `${String(s)}s`;
|
|
83
|
+
if (s < 3600) return `${String(Math.floor(s / 60))}m${String(s % 60).padStart(2, "0")}s`;
|
|
84
|
+
return `${String(Math.floor(s / 3600))}h${String(Math.floor(s % 3600 / 60)).padStart(2, "0")}m`;
|
|
85
|
+
};
|
|
86
|
+
const dim = (s) => style("dim", s);
|
|
87
|
+
/**
|
|
88
|
+
* A run's status as `ork ps` reads it: for a running run, `waiting for <lock>,
|
|
89
|
+
* held by <run>/<node>` with a node waiting for a lock, or `waiting for slot`
|
|
90
|
+
* with a command waiting for a machine-wide slot; `events` are a running
|
|
91
|
+
* run's.
|
|
92
|
+
*/
|
|
93
|
+
const listedStatus = (run, events) => {
|
|
94
|
+
const status = runStatus(run, events);
|
|
95
|
+
if (status !== "running") return status;
|
|
96
|
+
const [lock] = lockWaitsOf(events);
|
|
97
|
+
if (lock !== void 0) return lockWaitLine(lock.lock, lock.holders);
|
|
98
|
+
return slotWaitsOf(events).length > 0 ? "waiting for slot" : status;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* The `ork ps` table; `withProject` adds each run's project. A called run's
|
|
102
|
+
* cost is dimmed and says its caller's, whose cost already counts it.
|
|
103
|
+
*/
|
|
104
|
+
const formatRuns = (runs, withProject, now = Date.now(), statusOf = liveStatus, term = termOf(process.stdout)) => {
|
|
105
|
+
if (runs.length === 0) return ["No runs."];
|
|
106
|
+
const rows = runs.map((run) => {
|
|
107
|
+
const status = statusOf(run);
|
|
108
|
+
const end = run.finishedAt === void 0 ? now : Date.parse(run.finishedAt);
|
|
109
|
+
return {
|
|
110
|
+
id: run.id,
|
|
111
|
+
workflow: run.workflow,
|
|
112
|
+
status,
|
|
113
|
+
when: `${ago(run.startedAt, now)} · ${duration(end - Date.parse(run.startedAt))}`,
|
|
114
|
+
cost: run.parent === void 0 ? costOf(run.costUsd, run.partial, run.tokens) : `${costOf(run.costUsd, run.partial, run.tokens)} in ${run.parent}`,
|
|
115
|
+
called: run.parent !== void 0,
|
|
116
|
+
project: run.project
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
const rowOf = (r) => {
|
|
120
|
+
const cells = [
|
|
121
|
+
{
|
|
122
|
+
text: r.id,
|
|
123
|
+
look: (s) => style("bold", s)
|
|
124
|
+
},
|
|
125
|
+
{ text: r.workflow },
|
|
126
|
+
{
|
|
127
|
+
text: r.status,
|
|
128
|
+
chip: statusChip(r.status),
|
|
129
|
+
look: (s) => style(STATUS_STYLE[r.status] ?? "reset", s)
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
text: r.when,
|
|
133
|
+
look: dim
|
|
134
|
+
},
|
|
135
|
+
r.called ? {
|
|
136
|
+
text: r.cost,
|
|
137
|
+
look: dim
|
|
138
|
+
} : { text: r.cost }
|
|
139
|
+
];
|
|
140
|
+
if (withProject) cells.push({
|
|
141
|
+
text: r.project,
|
|
142
|
+
look: dim
|
|
143
|
+
});
|
|
144
|
+
return cells;
|
|
145
|
+
};
|
|
146
|
+
const heads = [
|
|
147
|
+
"RUN",
|
|
148
|
+
"WORKFLOW",
|
|
149
|
+
"STATUS",
|
|
150
|
+
"STARTED",
|
|
151
|
+
"COST"
|
|
152
|
+
].map((text) => header(text));
|
|
153
|
+
if (withProject) heads.push(header("PROJECT"));
|
|
154
|
+
const columns = [
|
|
155
|
+
{},
|
|
156
|
+
{ flex: true },
|
|
157
|
+
{},
|
|
158
|
+
{},
|
|
159
|
+
{ align: "right" }
|
|
160
|
+
];
|
|
161
|
+
if (withProject) columns.push({ flex: true });
|
|
162
|
+
return table(term, columns, [heads, ...rows.map((row) => rowOf(row))]);
|
|
163
|
+
};
|
|
164
|
+
/** Marks are made when used, so `--plain` (set after load) is read. */
|
|
165
|
+
const STATE_MARK = {
|
|
166
|
+
waiting: () => style("dim", "·"),
|
|
167
|
+
running: () => style("cyan", "●"),
|
|
168
|
+
ok: () => style("green", "✓"),
|
|
169
|
+
failed: () => style("red", "✗"),
|
|
170
|
+
skipped: () => style("dim", "-")
|
|
171
|
+
};
|
|
172
|
+
/** `950`, `12.3k`, `1.2M`. */
|
|
173
|
+
const count = shortCount;
|
|
174
|
+
const stateText = (node) => {
|
|
175
|
+
if (node.state === "running") return node.runs > 1 ? `running (round ${String(node.runs)})` : "running";
|
|
176
|
+
if (node.state === "ok" || node.state === "failed") {
|
|
177
|
+
const verdict = nodeVerdict(node.output ?? {}, node.state === "ok");
|
|
178
|
+
const line = verdict.length > 60 ? `${verdict.slice(0, 59)}…` : verdict;
|
|
179
|
+
return node.runs > 1 ? `${line} (${String(node.runs)} runs)` : line;
|
|
180
|
+
}
|
|
181
|
+
return node.state;
|
|
182
|
+
};
|
|
183
|
+
/** A node's counted tokens: input, output and cache write. */
|
|
184
|
+
const countedOf = (node) => node.inputTokens + node.outputTokens + (node.cacheWriteTokens ?? 0);
|
|
185
|
+
/**
|
|
186
|
+
* What a node spent: its counted tokens, then dollars unless its driver reports
|
|
187
|
+
* none, where `$0.00` would be false. Blank with no tokens.
|
|
188
|
+
*/
|
|
189
|
+
const worth = (node) => {
|
|
190
|
+
const counted = countedOf(node);
|
|
191
|
+
if (counted === 0) return "";
|
|
192
|
+
const tok = `${count(counted)} tok`;
|
|
193
|
+
return style("dim", ` ${node.unpriced === true ? tok : `${tok} · ${cost(node.costUsd)}`}`);
|
|
194
|
+
};
|
|
195
|
+
/**
|
|
196
|
+
* A crashed run's node that never ended: cut off when it had started, not run
|
|
197
|
+
* when it had not; `null` for one that ended.
|
|
198
|
+
*/
|
|
199
|
+
const crashedState = (node) => {
|
|
200
|
+
if (node.state === "running") return `${STATE_MARK.failed()} cut off`;
|
|
201
|
+
if (node.state === "waiting") return `${STATE_MARK.skipped()} not run`;
|
|
202
|
+
return null;
|
|
203
|
+
};
|
|
204
|
+
/**
|
|
205
|
+
* The ownership tree, one line per node, children under their owner. A node in
|
|
206
|
+
* `waits` (a live run's `lockWaitsOf`) reads `waiting for <lock>`, an item too.
|
|
207
|
+
* In a `crashed` run, a node started but not ended reads `cut off` and one not
|
|
208
|
+
* started `not run`: nothing will end them.
|
|
209
|
+
*/
|
|
210
|
+
const formatTree = (roots, now = Date.now(), waits = [], crashed = false) => {
|
|
211
|
+
const lines = [];
|
|
212
|
+
const walk = (raw, lead, branch) => {
|
|
213
|
+
const node = raw.description === void 0 ? raw : {
|
|
214
|
+
...raw,
|
|
215
|
+
description: maskSecrets(raw.description)
|
|
216
|
+
};
|
|
217
|
+
const named = node.description === void 0 ? "" : ` ${node.description}`;
|
|
218
|
+
lines.push({
|
|
219
|
+
label: `${lead}${branch}${node.id}${named}`,
|
|
220
|
+
node
|
|
221
|
+
});
|
|
222
|
+
const next = lead + (branch === "├─ " ? "│ " : branch === "" ? "" : " ");
|
|
223
|
+
node.children.forEach((child, index) => {
|
|
224
|
+
walk(child, next, index === node.children.length - 1 ? "└─ " : "├─ ");
|
|
225
|
+
});
|
|
226
|
+
};
|
|
227
|
+
for (const root of roots) walk(root, "", "");
|
|
228
|
+
const label = Math.max(...lines.map((l) => widthOf(l.label)));
|
|
229
|
+
const type = Math.max(...lines.map((l) => l.node.type.length));
|
|
230
|
+
return lines.map(({ label: text, node }) => {
|
|
231
|
+
const usage = worth(node);
|
|
232
|
+
const lock = waits.find((wait) => wait.id === node.id)?.lock;
|
|
233
|
+
const cut = crashed ? crashedState(node) : null;
|
|
234
|
+
const state = cut ?? (node.held === true ? `${style("yellow", "‖")} paused` : lock === void 0 ? `${STATE_MARK[node.state]()} ${stateText(node)}` : `${STATE_MARK.waiting()} waiting for ${lock}`);
|
|
235
|
+
const spent = isItem(node) && node.startedAt !== void 0 && cut === null ? style("dim", ` ${duration((node.finishedAt === void 0 ? now : Date.parse(node.finishedAt)) - Date.parse(node.startedAt))}`) : "";
|
|
236
|
+
return `${node.description === void 0 ? text : `${text.slice(0, text.length - node.description.length)}${dim(node.description)}`}${" ".repeat(label - widthOf(text))} ${style("dim", node.type.padEnd(type))} ${state}${spent}${usage}`;
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
/**
|
|
240
|
+
* What `ork tree` prints: the run's header (its status as `ork ps` reads it)
|
|
241
|
+
* and the tree. A finished run's lock waits are stale, so only a live run's
|
|
242
|
+
* show; `events` are the run's, `roots` its nested tree.
|
|
243
|
+
*/
|
|
244
|
+
const formatTreeView = (run, events, roots, now = Date.now()) => {
|
|
245
|
+
const status = runStatus(run, events);
|
|
246
|
+
const waits = status === "running" ? lockWaitsOf(events) : [];
|
|
247
|
+
return [`${style("bold", `run ${run.id}`)} ${dim(`of ${run.workflow} · ${listedStatus(run, events)} · ${cost(run.costUsd)}`)}`, ...formatTree(roots, now, waits, status === "crashed")];
|
|
248
|
+
};
|
|
249
|
+
export { header as a, listedStatus as i, formatRuns as n, table as o, formatTreeView as r, ago as t };
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* shell. Each is thin: it runs `ork __complete -- <words…>` with the word
|
|
5
5
|
* under the cursor last, and offers the lines it prints, `value<TAB>
|
|
6
6
|
* description`. A line `:group <title>` starts a group (zsh labels the menu
|
|
7
|
-
* with it); `:files` asks for file names too. A
|
|
7
|
+
* with it); `:files` asks for file names too. A value ending in `=` (an
|
|
8
|
+
* input's `name=`) takes no space after it, so its value is typed next. A new
|
|
9
|
+
* shell is one more entry.
|
|
8
10
|
*/
|
|
9
11
|
/** The hidden command the scripts run. */
|
|
10
12
|
const COMPLETE_COMMAND = "__complete";
|
|
@@ -15,9 +17,11 @@ const SHELL_SCRIPTS = {
|
|
|
15
17
|
# (ork completion zsh > ~/.zsh/completions/_ork), or source it.
|
|
16
18
|
|
|
17
19
|
_ork() {
|
|
18
|
-
local -a lines cands
|
|
20
|
+
local -a lines cands opens
|
|
19
21
|
local line group=commands files=0 n=0 ret=1
|
|
20
22
|
lines=("\${(@f)$("\${words[1]}" ${COMPLETE_COMMAND} -- "\${(@)words[2,CURRENT]}" 2>/dev/null)}")
|
|
23
|
+
# A last group line offers what the last group gathered.
|
|
24
|
+
lines+=(":group end")
|
|
21
25
|
for line in "\${lines[@]}"; do
|
|
22
26
|
case $line in
|
|
23
27
|
(:files) files=1 ;;
|
|
@@ -25,15 +29,17 @@ _ork() {
|
|
|
25
29
|
if (( \${#cands} )); then
|
|
26
30
|
_describe -V -t "ork-$(( ++n ))" "$group" cands && ret=0
|
|
27
31
|
fi
|
|
28
|
-
|
|
32
|
+
# name= is followed by its value, not a space.
|
|
33
|
+
if (( \${#opens} )); then
|
|
34
|
+
_describe -V -t "ork-$(( ++n ))" "$group" opens -S '' && ret=0
|
|
35
|
+
fi
|
|
36
|
+
cands=() opens=()
|
|
29
37
|
group=\${line#:group }
|
|
30
38
|
;;
|
|
39
|
+
(*=$'\\t'*) opens+=("\${\${line%%$'\\t'*}//:/\\\\:}:\${line#*$'\\t'}") ;;
|
|
31
40
|
(?*) cands+=("\${\${line%%$'\\t'*}//:/\\\\:}:\${line#*$'\\t'}") ;;
|
|
32
41
|
esac
|
|
33
42
|
done
|
|
34
|
-
if (( \${#cands} )); then
|
|
35
|
-
_describe -V -t "ork-$(( ++n ))" "$group" cands && ret=0
|
|
36
|
-
fi
|
|
37
43
|
if (( files )); then
|
|
38
44
|
_files && ret=0
|
|
39
45
|
fi
|
|
@@ -48,9 +54,10 @@ fi
|
|
|
48
54
|
`,
|
|
49
55
|
bash: `# Tab completion for ork, in bash. Add to your ~/.bashrc:
|
|
50
56
|
# eval "$(ork completion bash)"
|
|
51
|
-
# (macOS's bash 3.2
|
|
52
|
-
#
|
|
53
|
-
#
|
|
57
|
+
# (macOS's bash 3.2 loads nothing from source <(…), and prints no error;
|
|
58
|
+
# saving it to a file and sourcing that works in every version.) Every
|
|
59
|
+
# expansion is quoted and lines are read with read -r, so a candidate is never
|
|
60
|
+
# split or glob-expanded.
|
|
54
61
|
|
|
55
62
|
_ork() {
|
|
56
63
|
local line="\${COMP_LINE:0:COMP_POINT}" cur files=0 strip="" out
|
|
@@ -59,10 +66,15 @@ _ork() {
|
|
|
59
66
|
if [[ "$line" == *[[:space:]] ]]; then words+=(""); fi
|
|
60
67
|
cur="\${words[\${#words[@]}-1]}"
|
|
61
68
|
COMPREPLY=()
|
|
62
|
-
# bash splits a word at ":" (ork:feature); offer only
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
# bash splits a word at ":" and "=" (ork:feature, --input=plan=); offer only
|
|
70
|
+
# what follows the last of them that is a word break.
|
|
71
|
+
local brk pre
|
|
72
|
+
for brk in : =; do
|
|
73
|
+
if [[ "$cur" == *"$brk"* && "$COMP_WORDBREAKS" == *"$brk"* ]]; then
|
|
74
|
+
pre="\${cur%"\${cur##*"$brk"}"}"
|
|
75
|
+
if (( \${#pre} > \${#strip} )); then strip="$pre"; fi
|
|
76
|
+
fi
|
|
77
|
+
done
|
|
66
78
|
out="$("\${words[0]}" ${COMPLETE_COMMAND} -- "\${words[@]:1}" 2>/dev/null)"
|
|
67
79
|
while IFS= read -r line; do
|
|
68
80
|
case "$line" in
|
|
@@ -71,6 +83,10 @@ _ork() {
|
|
|
71
83
|
*)
|
|
72
84
|
line="\${line%%$'\\t'*}"
|
|
73
85
|
COMPREPLY+=("\${line#"$strip"}")
|
|
86
|
+
# name= is followed by its value, not a space (bash 4 and later).
|
|
87
|
+
if [[ "$line" == *= ]] && type compopt &>/dev/null; then
|
|
88
|
+
compopt -o nospace
|
|
89
|
+
fi
|
|
74
90
|
;;
|
|
75
91
|
esac
|
|
76
92
|
done <<< "$out"
|
|
@@ -103,68 +119,6 @@ complete -c ork -f -a '(__ork_complete)'
|
|
|
103
119
|
};
|
|
104
120
|
/** The shells there is a script for. */
|
|
105
121
|
const SHELLS = Object.keys(SHELL_SCRIPTS);
|
|
106
|
-
// src/command-tree.ts
|
|
107
|
-
/** How a flag that only Ork passes to itself starts its description. */
|
|
108
|
-
const INTERNAL = "Internal:";
|
|
109
|
-
/** Whether citty was given a function that gives the value. */
|
|
110
|
-
const isThunk = (value) => typeof value === "function";
|
|
111
|
-
/** A value citty lets be given as is, as a promise, or as a function. */
|
|
112
|
-
const resolved = (value) => Promise.resolve(isThunk(value) ? value() : value);
|
|
113
|
-
/** `args` without the flags only Ork passes to itself. */
|
|
114
|
-
const publicArgs = (args) => Object.fromEntries(Object.entries(args).filter(([, def]) => !(def.description ?? "").startsWith(INTERNAL)));
|
|
115
|
-
/** A command's flags and positionals, which citty lets be given lazily. */
|
|
116
|
-
const argsOf = (cmd) => cmd.args === void 0 ? Promise.resolve({}) : resolved(cmd.args);
|
|
117
|
-
const listOf = (value) => value === void 0 ? [] : [value].flat();
|
|
118
|
-
const flagOf = (name, def) => ({
|
|
119
|
-
name,
|
|
120
|
-
description: def.description ?? "",
|
|
121
|
-
takesValue: def.type === "string" || def.type === "enum",
|
|
122
|
-
aliases: "alias" in def ? listOf(def.alias) : [],
|
|
123
|
-
options: def.type === "enum" ? def.options ?? [] : [],
|
|
124
|
-
negatable: def.type === "boolean" && def.default === true
|
|
125
|
-
});
|
|
126
|
-
/** A command's public flags and positionals, in the order it defines them. */
|
|
127
|
-
const argInfoOf = async (cmd) => {
|
|
128
|
-
const entries = Object.entries(publicArgs(await argsOf(cmd)));
|
|
129
|
-
return {
|
|
130
|
-
flags: entries.filter(([, def]) => def.type !== "positional").map(([name, def]) => flagOf(name, def)),
|
|
131
|
-
positionals: entries.filter(([, def]) => def.type === "positional").map(([name, def]) => ({
|
|
132
|
-
name,
|
|
133
|
-
description: def.description ?? ""
|
|
134
|
-
}))
|
|
135
|
-
};
|
|
136
|
-
};
|
|
137
|
-
/**
|
|
138
|
-
* A command's subcommands, in order, loaded ones resolved. A name whose
|
|
139
|
-
* definition is one listed before it (`wt`, `worktrees`'s) is an alias of that
|
|
140
|
-
* one, not an entry of its own.
|
|
141
|
-
*/
|
|
142
|
-
const subcommandsOf = async (cmd) => {
|
|
143
|
-
const subs = cmd.subCommands === void 0 ? {} : await resolved(cmd.subCommands);
|
|
144
|
-
const loaded = await Promise.all(Object.entries(subs).map(async ([name, sub]) => {
|
|
145
|
-
const def = await resolved(sub);
|
|
146
|
-
return {
|
|
147
|
-
name,
|
|
148
|
-
def,
|
|
149
|
-
meta: def.meta === void 0 ? {} : await resolved(def.meta)
|
|
150
|
-
};
|
|
151
|
-
}));
|
|
152
|
-
const entries = [];
|
|
153
|
-
for (const { name, def, meta } of loaded) {
|
|
154
|
-
const same = entries.find((entry) => entry.def === def);
|
|
155
|
-
if (same === void 0) entries.push({
|
|
156
|
-
name,
|
|
157
|
-
aliases: listOf(meta.alias),
|
|
158
|
-
description: meta.description ?? "",
|
|
159
|
-
hidden: meta.hidden === true,
|
|
160
|
-
def
|
|
161
|
-
});
|
|
162
|
-
else same.aliases.push(name);
|
|
163
|
-
}
|
|
164
|
-
return entries;
|
|
165
|
-
};
|
|
166
|
-
/** The subcommand `name` (or one of its aliases) of `entries`. */
|
|
167
|
-
const findEntry = (entries, name) => entries.find((entry) => entry.name === name || entry.aliases.includes(name));
|
|
168
122
|
// src/help-groups.ts
|
|
169
123
|
const HELP_GROUPS = [
|
|
170
124
|
{
|
|
@@ -220,6 +174,10 @@ const HELP_GROUPS = [
|
|
|
220
174
|
name: "logs",
|
|
221
175
|
short: "Print a run's transcript"
|
|
222
176
|
},
|
|
177
|
+
{
|
|
178
|
+
name: "attach",
|
|
179
|
+
short: "Follow a run here and answer its questions"
|
|
180
|
+
},
|
|
223
181
|
{
|
|
224
182
|
name: "tree",
|
|
225
183
|
short: "Show a run's tree, tokens and cost"
|
|
@@ -270,6 +228,10 @@ const HELP_GROUPS = [
|
|
|
270
228
|
name: "feedback",
|
|
271
229
|
short: "What got in the way in your runs"
|
|
272
230
|
},
|
|
231
|
+
{
|
|
232
|
+
name: "follow-ups",
|
|
233
|
+
short: "What shipped changes left for later"
|
|
234
|
+
},
|
|
273
235
|
{
|
|
274
236
|
name: "worktrees",
|
|
275
237
|
short: "Track and clean up Ork's worktrees"
|
|
@@ -316,6 +278,10 @@ const HELP_GROUPS = [
|
|
|
316
278
|
name: "send",
|
|
317
279
|
short: "Write to a session from a tool node's program"
|
|
318
280
|
},
|
|
281
|
+
{
|
|
282
|
+
name: "once",
|
|
283
|
+
short: "Run a check unless it passed on these files"
|
|
284
|
+
},
|
|
319
285
|
{
|
|
320
286
|
name: "completion",
|
|
321
287
|
short: "Print a tab-completion script"
|
|
@@ -351,4 +317,4 @@ const groupedEntries = (entries) => {
|
|
|
351
317
|
entries: other
|
|
352
318
|
}].filter((group) => group.entries.length > 0);
|
|
353
319
|
};
|
|
354
|
-
export {
|
|
320
|
+
export { SHELL_SCRIPTS as a, SHELLS as i, groupedEntries as n, COMPLETE_COMMAND as r, groupOf as t };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z as storePath } from "./src-8cOPOgrT.mjs";
|
|
2
|
+
import { g as storeGuarded, h as runMarkerIn } from "./src-I_RyKu6_.mjs";
|
|
3
|
+
// src/commands/in-a-run.ts
|
|
4
|
+
/**
|
|
5
|
+
* @file The commands that act as the user refuse a program or an agent in a run
|
|
6
|
+
* (ADR-047), in the stores that run guards: the store of a nested run under a
|
|
7
|
+
* temp HOME is not guarded unless an enclosing run lists it. The refusal
|
|
8
|
+
* names the variable it saw, for someone at their own terminal who inherited
|
|
9
|
+
* it. It stops accidents; it is not a boundary.
|
|
10
|
+
*/
|
|
11
|
+
const refusal = (why, env, store) => {
|
|
12
|
+
const marker = runMarkerIn(env);
|
|
13
|
+
return marker === null || !storeGuarded(env, store) ? null : `${why} ${marker} is set here. If this is your own terminal, it inherited that from a program in a run (a tmux server started by one, say); open a shell without it.`;
|
|
14
|
+
};
|
|
15
|
+
/** Why `ork watch --accept` refuses here, or `null` when it may run. */
|
|
16
|
+
const acceptRefusal = (env = process.env, store = storePath()) => refusal("`ork watch --accept` approves code to run with nobody there, so a program or an agent in a run cannot use it.", env, store);
|
|
17
|
+
/** Why `ork chat` refuses here, or `null` when it may run. */
|
|
18
|
+
const chatRefusal = (env = process.env, store = storePath()) => refusal("`ork chat` speaks as you, so a program or an agent in a run cannot use it. A program writes to a session with `ork send`.", env, store);
|
|
19
|
+
/** Why `ork decide` will not answer here, or `null` when it may. */
|
|
20
|
+
const decideRefusal = (env = process.env, store = storePath()) => refusal("`ork decide` answers as you, so a program or an agent in a run cannot answer with it; `ork decide --list` still shows what is open.", env, store);
|
|
21
|
+
/** Why `ork continue` refuses here, or `null` when it may run. */
|
|
22
|
+
const continueRefusal = (env = process.env, store = storePath()) => refusal("`ork continue` undoes a pause you set, so a program or an agent in a run cannot use it.", env, store);
|
|
23
|
+
/** Whether `ork ui` may print its sign-in link, which acts as the user. */
|
|
24
|
+
const mayShowSignIn = (env = process.env, store = storePath()) => !storeGuarded(env, store);
|
|
25
|
+
/** Why `ork attach` will not answer here, or `null` when it may. */
|
|
26
|
+
const attachRefusal = (env = process.env, store = storePath()) => refusal("`ork attach` answers as you, so a program or an agent in a run cannot use it; `ork logs` still shows the run.", env, store);
|
|
27
|
+
export { decideRefusal as a, continueRefusal as i, attachRefusal as n, mayShowSignIn as o, chatRefusal as r, acceptRefusal as t };
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { $r as estimateTokens, Gr as placeFor, Qr as LESSON_CAP_TOKENS, Wr as placeFiles, Xr as inside, ei as guardedBase, ni as writePlainFile, ti as plainPathInside, vr as feedbackActions } from "./src-8cOPOgrT.mjs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
// ../core/src/feedback/lessons.ts
|
|
6
|
+
/**
|
|
7
|
+
* @file Accepting a feedback item as a lesson (specs/feedback.md, ADR-059): one
|
|
8
|
+
* line in the workflow's `.ork/lessons/<workflow>.md`, marked with the item
|
|
9
|
+
* it came from so it isn't added twice, and refused when it would take the
|
|
10
|
+
* file past the cap a session loads, or when the file, or a folder on the way
|
|
11
|
+
* to it, is a link a cloned repository could aim at the person's own files.
|
|
12
|
+
* Only a person's acceptance writes one: feedback is model output that
|
|
13
|
+
* repository content can steer.
|
|
14
|
+
*/
|
|
15
|
+
/** A workflow's name, as the format allows it: never a path. */
|
|
16
|
+
const SLUG = /^[a-z0-9][a-z0-9._-]*$/u;
|
|
17
|
+
/** A workflow's lessons file in a project. */
|
|
18
|
+
const lessonsFileOf = (place, workflow) => path.join(placeFiles(place).lessons, `${workflow}.md`);
|
|
19
|
+
/** What marks a lesson with its item, in the file. */
|
|
20
|
+
const lessonMark = (item) => `<!-- feedback ${item.slice(0, 12)} -->`;
|
|
21
|
+
/** The lesson as one list entry, its later lines indented under the first. */
|
|
22
|
+
const entryOf = (text, item) => {
|
|
23
|
+
const [first = "", ...rest] = text.trim().split("\n").map((line) => line.trimEnd());
|
|
24
|
+
return [`- ${first} ${lessonMark(item)}`, ...rest.map((l) => ` ${l}`)].join("\n").concat("\n");
|
|
25
|
+
};
|
|
26
|
+
/** Adds a lesson to its workflow's file, or says why it can't. */
|
|
27
|
+
const addLesson = (ask) => {
|
|
28
|
+
if (ask.text.trim() === "") return {
|
|
29
|
+
ok: false,
|
|
30
|
+
error: "The lesson is empty."
|
|
31
|
+
};
|
|
32
|
+
if (!SLUG.test(ask.workflow) || !plainPathInside(ask.base, ask.file)) return {
|
|
33
|
+
ok: false,
|
|
34
|
+
error: `${ask.shownAs} is a link, or behind one, or outside the project: Ork writes lessons only to a plain file there, so it wrote nothing.`
|
|
35
|
+
};
|
|
36
|
+
const current = existsSync(ask.file) ? readFileSync(ask.file, "utf8") : `# Lessons for ${ask.workflow}\n\n`;
|
|
37
|
+
if (current.includes(lessonMark(ask.item))) return {
|
|
38
|
+
ok: false,
|
|
39
|
+
error: `${ask.item.slice(0, 12)} is already a lesson in ${ask.shownAs}.`
|
|
40
|
+
};
|
|
41
|
+
const next = `${current}${current === "" || current.endsWith("\n") ? "" : "\n"}${entryOf(ask.text, ask.item)}`;
|
|
42
|
+
const tokens = estimateTokens(next);
|
|
43
|
+
if (tokens > 1500) return {
|
|
44
|
+
ok: false,
|
|
45
|
+
error: `${ask.shownAs} would be about ${tokens.toLocaleString("en")} tokens, over the ${LESSON_CAP_TOKENS.toLocaleString("en")} a session loads. Edit it to make room, then accept again.`
|
|
46
|
+
};
|
|
47
|
+
const written = writePlainFile(ask.base, ask.file, next, ask.shownAs);
|
|
48
|
+
return written.ok ? {
|
|
49
|
+
ok: true,
|
|
50
|
+
tokens
|
|
51
|
+
} : written;
|
|
52
|
+
};
|
|
53
|
+
// ../core/src/feedback/accept.ts
|
|
54
|
+
/**
|
|
55
|
+
* @file Accepting a feedback item (`ork feedback accept`, the Feedback panel):
|
|
56
|
+
* marks an item accepted and, with `--lesson`, writes it as a lesson into the
|
|
57
|
+
* lessons file of the workflow it was seen in, in the project of its newest
|
|
58
|
+
* record there (ADR-059). An `ork` item is about Ork itself, so it's never a
|
|
59
|
+
* lesson: it can be published instead.
|
|
60
|
+
*/
|
|
61
|
+
const listed = (names) => names.map((n) => `\`${n}\``).join(", ");
|
|
62
|
+
/** The workflow a lesson is for: the one named, or the only one it was seen in. */
|
|
63
|
+
const workflowFor = (item, named) => {
|
|
64
|
+
if (named !== void 0) return item.workflows.includes(named) ? {
|
|
65
|
+
ok: true,
|
|
66
|
+
workflow: named
|
|
67
|
+
} : {
|
|
68
|
+
ok: false,
|
|
69
|
+
error: `${item.id.slice(0, 12)} wasn't seen in \`${named}\`, only in ${listed(item.workflows)}.`
|
|
70
|
+
};
|
|
71
|
+
const [only, other] = item.workflows;
|
|
72
|
+
if (only === void 0) return {
|
|
73
|
+
ok: false,
|
|
74
|
+
error: `No workflow is recorded for ${item.id.slice(0, 12)}, so it has no lessons file.`
|
|
75
|
+
};
|
|
76
|
+
return other === void 0 ? {
|
|
77
|
+
ok: true,
|
|
78
|
+
workflow: only
|
|
79
|
+
} : {
|
|
80
|
+
ok: false,
|
|
81
|
+
error: `${item.id.slice(0, 12)} was seen in ${listed(item.workflows)}: name one with --workflow.`
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
const real = (dir) => {
|
|
85
|
+
try {
|
|
86
|
+
return realpathSync(dir);
|
|
87
|
+
} catch {
|
|
88
|
+
return path.resolve(dir);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* The project of the item's newest record (in `workflow`, when given): the
|
|
93
|
+
* folder its run started in, when that's still the same project. A clone that
|
|
94
|
+
* was deleted is refused, never taken for a repository above it.
|
|
95
|
+
*/
|
|
96
|
+
const placeOf = (store, item, workflow, registered) => {
|
|
97
|
+
const record = store.feedbackRecords(item.id).findLast((r) => workflow === void 0 || r.feedback.origin.workflow === workflow);
|
|
98
|
+
const [run] = record === void 0 ? [] : store.lookup(record.runId);
|
|
99
|
+
if (run === void 0) return {
|
|
100
|
+
ok: false,
|
|
101
|
+
error: `The runs ${item.id.slice(0, 12)} was seen in are gone, so its project isn't known.`
|
|
102
|
+
};
|
|
103
|
+
if (!existsSync(run.project)) return {
|
|
104
|
+
ok: false,
|
|
105
|
+
error: `Its project at ${run.project} is gone.`
|
|
106
|
+
};
|
|
107
|
+
const folder = path.dirname(run.file);
|
|
108
|
+
const place = placeFor(existsSync(folder) ? folder : run.project, registered);
|
|
109
|
+
return real(place.root) === real(run.project) ? {
|
|
110
|
+
ok: true,
|
|
111
|
+
place,
|
|
112
|
+
project: run.project
|
|
113
|
+
} : {
|
|
114
|
+
ok: false,
|
|
115
|
+
error: `Its project at ${run.project} isn't a project any more: Ork would take it for ${place.root}.`
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
/** Accepts `item`, as a lesson with `--lesson`. */
|
|
119
|
+
const acceptItem = (store, item, args, registered) => {
|
|
120
|
+
const name = `${item.id.slice(0, 12)}: ${item.title}`;
|
|
121
|
+
const actions = feedbackActions(item);
|
|
122
|
+
if (!args.lesson) {
|
|
123
|
+
if (!actions.accept.ok) return {
|
|
124
|
+
ok: false,
|
|
125
|
+
error: actions.accept.error
|
|
126
|
+
};
|
|
127
|
+
store.setFeedbackStatus(item.id, "accepted");
|
|
128
|
+
return {
|
|
129
|
+
ok: true,
|
|
130
|
+
lines: [`accepted ${name}`]
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (!actions.lesson.ok) return {
|
|
134
|
+
ok: false,
|
|
135
|
+
error: actions.lesson.error
|
|
136
|
+
};
|
|
137
|
+
const chosen = workflowFor(item, args.workflow);
|
|
138
|
+
if (!chosen.ok) return chosen;
|
|
139
|
+
const found = placeOf(store, item, chosen.workflow, registered);
|
|
140
|
+
if (!found.ok) return found;
|
|
141
|
+
const { place } = found;
|
|
142
|
+
const file = lessonsFileOf(place, chosen.workflow);
|
|
143
|
+
const shownAs = inside(place.root, file) ? path.relative(place.root, file) : file;
|
|
144
|
+
const added = addLesson({
|
|
145
|
+
file,
|
|
146
|
+
base: guardedBase(place),
|
|
147
|
+
shownAs,
|
|
148
|
+
workflow: chosen.workflow,
|
|
149
|
+
item: item.id,
|
|
150
|
+
text: args.text ?? item.title
|
|
151
|
+
});
|
|
152
|
+
if (!added.ok) return added;
|
|
153
|
+
if (actions.accept.ok) store.setFeedbackStatus(item.id, "accepted");
|
|
154
|
+
return {
|
|
155
|
+
ok: true,
|
|
156
|
+
lesson: {
|
|
157
|
+
file: shownAs,
|
|
158
|
+
root: place.root,
|
|
159
|
+
workflow: chosen.workflow,
|
|
160
|
+
tokens: added.tokens
|
|
161
|
+
},
|
|
162
|
+
lines: [`accepted ${name}`, `lesson added to ${shownAs} in ${place.root} (about ${added.tokens.toLocaleString("en")} of ${LESSON_CAP_TOKENS.toLocaleString("en")} tokens); every session of \`${chosen.workflow}\` reads it from now on`]
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
const lessonPlaceOf = (store, item, workflow, registered) => {
|
|
166
|
+
const found = placeOf(store, item, workflow, registered);
|
|
167
|
+
if (!found.ok) return {
|
|
168
|
+
ok: false,
|
|
169
|
+
workflow,
|
|
170
|
+
error: found.error
|
|
171
|
+
};
|
|
172
|
+
const { place } = found;
|
|
173
|
+
const file = lessonsFileOf(place, workflow);
|
|
174
|
+
const exists = existsSync(file);
|
|
175
|
+
return {
|
|
176
|
+
ok: true,
|
|
177
|
+
workflow,
|
|
178
|
+
file: inside(place.root, file) ? path.relative(place.root, file) : file,
|
|
179
|
+
root: place.root,
|
|
180
|
+
tokens: exists ? estimateTokens(readFileSync(file, "utf8")) : 0,
|
|
181
|
+
exists
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* Runs the command; resolves its exit code. Without `onOutput` it writes on the
|
|
186
|
+
* terminal.
|
|
187
|
+
*/
|
|
188
|
+
const runInstall = (argv, onOutput) => new Promise((resolve) => {
|
|
189
|
+
const [program = "", ...args] = argv;
|
|
190
|
+
const child = spawn(program, args, { stdio: onOutput === void 0 ? "inherit" : [
|
|
191
|
+
"ignore",
|
|
192
|
+
"pipe",
|
|
193
|
+
"pipe"
|
|
194
|
+
] });
|
|
195
|
+
for (const stream of ["stdout", "stderr"]) {
|
|
196
|
+
child[stream]?.setEncoding("utf8");
|
|
197
|
+
child[stream]?.on("data", (text) => {
|
|
198
|
+
onOutput?.(stream, text);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
child.on("error", (error) => {
|
|
202
|
+
const line = `Could not run ${program}: ${error.message}\n`;
|
|
203
|
+
if (onOutput === void 0) process.stderr.write(line);
|
|
204
|
+
else onOutput("stderr", line);
|
|
205
|
+
resolve(127);
|
|
206
|
+
});
|
|
207
|
+
child.on("close", (code) => {
|
|
208
|
+
resolve(code ?? 1);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
export { placeOf as i, acceptItem as n, lessonPlaceOf as r, runInstall as t };
|