@krapht/ork 1.0.0-alpha.1 → 1.0.0-alpha.11
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 +3 -4
- package/dist/{__build_info__-D9lSlMFy.mjs → __build_info__-CIchF6nr.mjs} +2 -2
- package/dist/bin.mjs +1479 -600
- package/dist/chips-BA7SrrH5.mjs +43 -0
- package/dist/{clean-CA_Oi2Wl.mjs → clean-Di-KT7Mf.mjs} +5 -2
- package/dist/{completion-DBGfL-gN.mjs → completion-C5D0sEs5.mjs} +23 -16
- package/dist/context-BaNshWr9.mjs +695 -0
- package/dist/{decide-CoIJh34e.mjs → decide-B6PnKeqo.mjs} +3 -3
- package/dist/{find-rBBk7Jui.mjs → find-BicBbGMg.mjs} +1 -1
- package/dist/format-lWhN-jYR.mjs +286 -0
- package/dist/{in-a-run-DdAakpRR.mjs → in-a-run-CgkVouyG.mjs} +1 -1
- package/dist/{limits-BPM9wUlV.mjs → limits-jZQp9ZJC.mjs} +1 -1
- package/dist/{list-Bv5Lxu7z.mjs → list-CEaJ07jd.mjs} +11 -4
- package/dist/{mcp-DM30MFBw.mjs → mcp-DYgVA9f5.mjs} +6 -6
- package/dist/output-CM95njYt.mjs +421 -0
- package/dist/{pause-C7lJkSma.mjs → pause-D6PuSlNl.mjs} +4 -4
- package/dist/{relay-BipJYszF.mjs → relay-DNx7be9k.mjs} +2 -2
- package/dist/{remove-BDs9isB6.mjs → remove-CpMb4NDl.mjs} +14 -7
- package/dist/{report-DK4LGqwF.mjs → report-CEN8VoUb.mjs} +31 -20
- package/dist/{send-nbkjf3sc.mjs → send-CLs_fyg7.mjs} +3 -3
- package/dist/{src-BxUJPGa5.mjs → src-9iGkjnsy.mjs} +487 -341
- package/dist/{src-B0TT7DBi.mjs → src-B6KvTm7B.mjs} +106 -90
- package/dist/src-BnZgZGrf.mjs +2697 -0
- package/dist/{src-hrj2h0IZ.mjs → src-D6_xjm-x.mjs} +6719 -3389
- package/dist/ui/THIRD_PARTY_LICENSES.txt +594 -0
- package/dist/ui/assets/app-CxURph7a.js +75 -0
- package/dist/ui/assets/app-QoHVi-mb.css +1 -0
- package/dist/ui/assets/{index-CPx74lFf.js → index-By_8KKG6.js} +3 -3
- package/dist/ui/assets/theme-CdXvqTQ6.js +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/{ui-B98FSboa.mjs → ui-CIci4UBW.mjs} +4 -4
- package/dist/{usage-EW7DI6Z7.mjs → usage-GceBGMjU.mjs} +43 -32
- package/dist/{watch-BAkSGaOp.mjs → watch-BmoybMQg.mjs} +28 -16
- package/dist/{workflows-vsEohqa7.mjs → workflows-DZvW3i1e.mjs} +4 -3
- package/dist/{worktrees-Ahj9rdWK.mjs → worktrees-Be51E2rB.mjs} +84 -24
- package/package.json +2 -4
- package/dist/context-6x3kXREO.mjs +0 -274
- package/dist/format-zMmhqyLE.mjs +0 -427
- package/dist/output-TV_j7Ni0.mjs +0 -950
- package/dist/src-B9v0lDc6.mjs +0 -1204
- package/dist/ui/assets/app-BNT0SuIz.css +0 -1
- package/dist/ui/assets/app-CmkynZyW.js +0 -67
- package/dist/ui/assets/theme-3xH5jxKs.js +0 -1
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { stripVTControlCharacters, styleText } from "node:util";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
// src/term/keys.ts
|
|
4
|
+
/** What a `ESC [ …` or `ESC O …` sequence names. */
|
|
5
|
+
const SEQUENCES = {
|
|
6
|
+
"[A": "up",
|
|
7
|
+
"[B": "down",
|
|
8
|
+
"[C": "right",
|
|
9
|
+
"[D": "left",
|
|
10
|
+
"[H": "home",
|
|
11
|
+
"[F": "end",
|
|
12
|
+
"[1~": "home",
|
|
13
|
+
"[4~": "end",
|
|
14
|
+
"[7~": "home",
|
|
15
|
+
"[8~": "end",
|
|
16
|
+
"[3~": "delete",
|
|
17
|
+
OA: "up",
|
|
18
|
+
OB: "down",
|
|
19
|
+
OC: "right",
|
|
20
|
+
OD: "left",
|
|
21
|
+
OH: "home",
|
|
22
|
+
OF: "end"
|
|
23
|
+
};
|
|
24
|
+
const CONTROLS = {
|
|
25
|
+
"\r": "enter",
|
|
26
|
+
"\n": "enter",
|
|
27
|
+
"": "backspace",
|
|
28
|
+
"\b": "backspace",
|
|
29
|
+
"": "ctrl-c",
|
|
30
|
+
"": "ctrl-d",
|
|
31
|
+
"": "ctrl-a",
|
|
32
|
+
"": "ctrl-e",
|
|
33
|
+
"": "ctrl-u"
|
|
34
|
+
};
|
|
35
|
+
/** The keys in one chunk of input; what it does not know it skips. */
|
|
36
|
+
const parseKeys = (chunk) => {
|
|
37
|
+
const keys = [];
|
|
38
|
+
const chars = Array.from(chunk);
|
|
39
|
+
for (let i = 0; i < chars.length; i += 1) {
|
|
40
|
+
const char = chars[i] ?? "";
|
|
41
|
+
if (char === "\x1B") {
|
|
42
|
+
if (i === chars.length - 1) {
|
|
43
|
+
keys.push({ name: "escape" });
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
let end = i + (chars[i + 1] === "O" ? 3 : 2);
|
|
47
|
+
while (chars[i + 1] === "[" && end < chars.length + 1 && !/[A-Za-z~]/u.test(chars[end - 1] ?? "")) end += 1;
|
|
48
|
+
const name = SEQUENCES[chars.slice(i + 1, end).join("")];
|
|
49
|
+
if (name !== void 0) keys.push({ name });
|
|
50
|
+
i = end - 1;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const name = CONTROLS[char];
|
|
54
|
+
if (name !== void 0) keys.push({ name });
|
|
55
|
+
else if (char >= " ") keys.push({
|
|
56
|
+
name: "char",
|
|
57
|
+
text: char
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return keys;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Reads keys from `input` in raw mode, calling `onKey` for each, until the
|
|
64
|
+
* returned function is called, which restores the mode and pauses the input.
|
|
65
|
+
*/
|
|
66
|
+
const readKeys = (input, onKey) => {
|
|
67
|
+
const wasRaw = input.isRaw === true;
|
|
68
|
+
input.setRawMode?.(true);
|
|
69
|
+
input.resume();
|
|
70
|
+
const onData = (chunk) => {
|
|
71
|
+
for (const key of parseKeys(String(chunk))) onKey(key);
|
|
72
|
+
};
|
|
73
|
+
input.on("data", onData);
|
|
74
|
+
return () => {
|
|
75
|
+
input.off("data", onData);
|
|
76
|
+
input.setRawMode?.(wasRaw);
|
|
77
|
+
input.pause();
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
// src/term/layout.ts
|
|
81
|
+
/**
|
|
82
|
+
* @file Layout for plain text: its width, wrapped to a width, cut to one line
|
|
83
|
+
* with `…`, and hung under a lead. `wrap`, `cut` and `hang` take text before
|
|
84
|
+
* it is styled (they would cut inside an escape code), so style each piece
|
|
85
|
+
* after laying it out; `widthOf` and `rowsOf` ignore escape codes. Width
|
|
86
|
+
* counts one column per code point: wide characters (CJK, emoji) may overrun
|
|
87
|
+
* a line (specs/gaps.md).
|
|
88
|
+
*/
|
|
89
|
+
/** The columns of `text`, escape codes not counted. */
|
|
90
|
+
const widthOf = (text) => Array.from(stripVTControlCharacters(text)).length;
|
|
91
|
+
/** `text` of one line within `width`, with `…` where it was cut. */
|
|
92
|
+
const cut = (text, width) => {
|
|
93
|
+
const chars = Array.from(text);
|
|
94
|
+
return chars.length <= width ? text : `${chars.slice(0, Math.max(0, width - 1)).join("")}…`;
|
|
95
|
+
};
|
|
96
|
+
/** A word that is over `width` is split into pieces of `width`. */
|
|
97
|
+
const pieces = (word, width) => {
|
|
98
|
+
const chars = Array.from(word);
|
|
99
|
+
const out = [];
|
|
100
|
+
for (let i = 0; i < chars.length; i += width) out.push(chars.slice(i, i + width).join(""));
|
|
101
|
+
return out;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* `text` wrapped to lines of at most `width` columns, at spaces; each line
|
|
105
|
+
* break in `text` stays, and a blank line stays blank. A word longer than the
|
|
106
|
+
* width is split.
|
|
107
|
+
*/
|
|
108
|
+
const wrap = (text, width) => text.split("\n").flatMap((paragraph) => {
|
|
109
|
+
const words = paragraph.split(/ +/u).filter((word) => word !== "");
|
|
110
|
+
const lines = [];
|
|
111
|
+
let line = "";
|
|
112
|
+
for (const word of words.flatMap((w) => pieces(w, width))) if (line === "") line = word;
|
|
113
|
+
else if (widthOf(line) + 1 + widthOf(word) <= width) line += ` ${word}`;
|
|
114
|
+
else {
|
|
115
|
+
lines.push(line);
|
|
116
|
+
line = word;
|
|
117
|
+
}
|
|
118
|
+
lines.push(line);
|
|
119
|
+
return lines;
|
|
120
|
+
});
|
|
121
|
+
/**
|
|
122
|
+
* `text` wrapped under `lead`: the first line starts with `lead`, the others
|
|
123
|
+
* with spaces as wide as `lead` (a hanging indent), all within `width`.
|
|
124
|
+
*/
|
|
125
|
+
const hang = (lead, text, width) => {
|
|
126
|
+
const indent = " ".repeat(widthOf(lead));
|
|
127
|
+
return wrap(text, Math.max(1, width - indent.length)).map((line, index) => (index === 0 ? lead : indent).concat(line));
|
|
128
|
+
};
|
|
129
|
+
/** How many terminal rows `line` takes at `width` (at least one). */
|
|
130
|
+
const rowsOf = (line, width) => Math.max(1, Math.ceil(widthOf(line) / width));
|
|
131
|
+
const DEFAULT_WIDTH = 80;
|
|
132
|
+
const DEFAULT_ROWS = 24;
|
|
133
|
+
/** Whether `ORK_PLAIN` is on: set, and not `0` or `false`. */
|
|
134
|
+
const orkPlain = (value) => value !== void 0 && ![
|
|
135
|
+
"",
|
|
136
|
+
"0",
|
|
137
|
+
"false"
|
|
138
|
+
].includes(value.toLowerCase());
|
|
139
|
+
/** Whether the environment asks for plain output (not the stream). */
|
|
140
|
+
const plainEnv = (env = process.env) => (env["NO_COLOR"] ?? "") !== "" || orkPlain(env["ORK_PLAIN"]) || env["TERM"] === "dumb";
|
|
141
|
+
/** How to write to `stream`, given `env`. */
|
|
142
|
+
const termOf = (stream, env = process.env) => ({
|
|
143
|
+
rich: stream.isTTY === true && !plainEnv(env),
|
|
144
|
+
width: Math.max(20, stream.columns ?? DEFAULT_WIDTH),
|
|
145
|
+
rows: stream.rows ?? DEFAULT_ROWS
|
|
146
|
+
});
|
|
147
|
+
/** Whether a prompt can take keys and redraw: rich output, a raw-mode input. */
|
|
148
|
+
const canKey = (input, output, env = process.env) => termOf(output, env).rich && input.isTTY === true && typeof input.setRawMode === "function";
|
|
149
|
+
/**
|
|
150
|
+
* Takes the global `--plain` flag out of `argv` (up to a `--`, after which
|
|
151
|
+
* words belong to a program). It sets `ORK_PLAIN=1` in `env`, which every child
|
|
152
|
+
* Ork starts then inherits.
|
|
153
|
+
*/
|
|
154
|
+
const takePlainFlag = (argv, env = process.env) => {
|
|
155
|
+
const end = argv.indexOf("--");
|
|
156
|
+
const head = end === -1 ? argv : argv.slice(0, end);
|
|
157
|
+
if (!head.includes("--plain")) return [...argv];
|
|
158
|
+
env["ORK_PLAIN"] = "1";
|
|
159
|
+
return [...head.filter((word) => word !== "--plain"), ...argv.slice(head.length)];
|
|
160
|
+
};
|
|
161
|
+
// src/term/line.ts
|
|
162
|
+
/**
|
|
163
|
+
* @file The plain way to ask: one prompt, one line read. It is what every kit
|
|
164
|
+
* prompt falls back to (not a terminal, `NO_COLOR`, `--plain`), so scripts
|
|
165
|
+
* and tests that feed lines keep working, and what a screen reader sees.
|
|
166
|
+
*/
|
|
167
|
+
/**
|
|
168
|
+
* Prints `prompt` and reads one line; `undefined` when the input ends first
|
|
169
|
+
* (Ctrl-D, a closed pipe). An aborted `signal` rejects.
|
|
170
|
+
*/
|
|
171
|
+
const readLine = async (io, prompt, signal) => {
|
|
172
|
+
if (io.readLine !== void 0) return io.readLine(prompt, signal);
|
|
173
|
+
const rl = createInterface({
|
|
174
|
+
input: io.input,
|
|
175
|
+
output: io.output,
|
|
176
|
+
...plainEnv(io.env) ? { terminal: false } : {}
|
|
177
|
+
});
|
|
178
|
+
const { promise: ended, resolve: end } = Promise.withResolvers();
|
|
179
|
+
rl.once("close", () => {
|
|
180
|
+
end();
|
|
181
|
+
});
|
|
182
|
+
try {
|
|
183
|
+
const answer = await Promise.race([signal === void 0 ? rl.question(prompt) : rl.question(prompt, { signal }), ended]);
|
|
184
|
+
return typeof answer === "string" ? answer : void 0;
|
|
185
|
+
} finally {
|
|
186
|
+
rl.close();
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
/** Ends a rich prompt on Ctrl-C: the caller's hook, else `SIGINT`. */
|
|
190
|
+
const interrupt = (io) => {
|
|
191
|
+
if (io.interrupt === void 0) process.kill(process.pid, "SIGINT");
|
|
192
|
+
else io.interrupt();
|
|
193
|
+
};
|
|
194
|
+
/** The error a withdrawn prompt rejects with: the signal's own, if it is one. */
|
|
195
|
+
const withdrawn = (signal) => signal?.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("the prompt was withdrawn");
|
|
196
|
+
// src/term/screen.ts
|
|
197
|
+
/**
|
|
198
|
+
* @file A redrawn region of the terminal: a prompt draws its lines, and draws
|
|
199
|
+
* them again, moving the cursor up over its own lines and clearing down,
|
|
200
|
+
* never the screen. When it is done it leaves one line (or none). The cursor
|
|
201
|
+
* is hidden while a region is open unless a prompt places it, and is always
|
|
202
|
+
* restored by {@link Region.finish}. While a region is open, a line another
|
|
203
|
+
* part of the program prints (a run's transcript, from parallel nodes) goes
|
|
204
|
+
* through {@link printAbove}, which erases the region, prints the line and
|
|
205
|
+
* draws the region again, so the rows the region counts stay true.
|
|
206
|
+
*/
|
|
207
|
+
const UP = (n) => n > 0 ? `\u001B[${String(n)}A` : "";
|
|
208
|
+
const CLEAR_DOWN = "\r\x1B[J";
|
|
209
|
+
const HIDE = "\x1B[?25l";
|
|
210
|
+
const SHOW = "\x1B[?25h";
|
|
211
|
+
/** The regions drawn now, the newest last. */
|
|
212
|
+
const open = /* @__PURE__ */ new Set();
|
|
213
|
+
/**
|
|
214
|
+
* Prints `text` above the open region, if one is drawn: `true`, and the caller
|
|
215
|
+
* writes nothing. `false` when no region is open, and the caller writes as
|
|
216
|
+
* usual.
|
|
217
|
+
*/
|
|
218
|
+
const printAbove = (text) => {
|
|
219
|
+
const region = [...open].at(-1);
|
|
220
|
+
if (region === void 0) return false;
|
|
221
|
+
region.above(text);
|
|
222
|
+
return true;
|
|
223
|
+
};
|
|
224
|
+
/** A region at the cursor of `out`; `term` gives the width, read live. */
|
|
225
|
+
const createRegion = (out, term) => {
|
|
226
|
+
/** Rows between the cursor and the region's first row. */
|
|
227
|
+
let up = 0;
|
|
228
|
+
let last = {
|
|
229
|
+
lines: [],
|
|
230
|
+
column: void 0
|
|
231
|
+
};
|
|
232
|
+
const erase = () => `${UP(up)}${CLEAR_DOWN}`;
|
|
233
|
+
const draw = (lines, column) => {
|
|
234
|
+
open.add(region);
|
|
235
|
+
const width = term().width;
|
|
236
|
+
last = {
|
|
237
|
+
lines,
|
|
238
|
+
column
|
|
239
|
+
};
|
|
240
|
+
const rows = lines.reduce((sum, line) => sum + rowsOf(line, width), 0);
|
|
241
|
+
const placed = column === void 0 ? HIDE : `${SHOW}\r${column > 0 ? `\u001B[${String(column)}C` : ""}`;
|
|
242
|
+
out.write(`${erase()}${lines.join("\n")}${placed}`);
|
|
243
|
+
up = Math.max(0, rows - 1);
|
|
244
|
+
};
|
|
245
|
+
const region = {
|
|
246
|
+
draw,
|
|
247
|
+
redraw: () => {
|
|
248
|
+
draw(last.lines, last.column);
|
|
249
|
+
},
|
|
250
|
+
above: (text) => {
|
|
251
|
+
out.write(`${erase()}${text}\n`);
|
|
252
|
+
up = 0;
|
|
253
|
+
draw(last.lines, last.column);
|
|
254
|
+
},
|
|
255
|
+
finish: (line) => {
|
|
256
|
+
out.write(`${erase()}${line === void 0 ? "" : `${line}\n`}${SHOW}`);
|
|
257
|
+
up = 0;
|
|
258
|
+
last = {
|
|
259
|
+
lines: [],
|
|
260
|
+
column: void 0
|
|
261
|
+
};
|
|
262
|
+
open.delete(region);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
return region;
|
|
266
|
+
};
|
|
267
|
+
// src/term/theme.ts
|
|
268
|
+
/**
|
|
269
|
+
* @file The terminal kit's one theme: a role says what a piece of text means
|
|
270
|
+
* (accent, ok, warn, danger, muted), and the theme gives it a look that reads
|
|
271
|
+
* on dark and light terminals: the 16 ANSI colors only, and a fixed
|
|
272
|
+
* foreground on every chip background. Nothing is said by color alone: a chip
|
|
273
|
+
* carries words and a mark a symbol. In plain mode every call returns the
|
|
274
|
+
* text unstyled.
|
|
275
|
+
*/
|
|
276
|
+
/** The text look of each role. */
|
|
277
|
+
const TEXT = {
|
|
278
|
+
accent: "cyan",
|
|
279
|
+
ok: "green",
|
|
280
|
+
warn: "yellow",
|
|
281
|
+
danger: "red",
|
|
282
|
+
muted: "dim"
|
|
283
|
+
};
|
|
284
|
+
/** The chip look of each role: a background and the foreground fixed on it. */
|
|
285
|
+
const CHIP = {
|
|
286
|
+
accent: ["bgCyan", "black"],
|
|
287
|
+
ok: ["bgGreen", "black"],
|
|
288
|
+
warn: ["bgYellow", "black"],
|
|
289
|
+
danger: ["bgRed", "whiteBright"],
|
|
290
|
+
muted: ["bgWhite", "black"]
|
|
291
|
+
};
|
|
292
|
+
/** The backgrounds a node chip takes, by a hash of its id. */
|
|
293
|
+
const NODE_PALETTE = [
|
|
294
|
+
["bgCyan", "black"],
|
|
295
|
+
["bgGreen", "black"],
|
|
296
|
+
["bgYellow", "black"],
|
|
297
|
+
["bgMagenta", "whiteBright"],
|
|
298
|
+
["bgBlue", "whiteBright"],
|
|
299
|
+
["bgWhite", "black"]
|
|
300
|
+
];
|
|
301
|
+
/** `text` in `format` when the terminal is rich, else as is. */
|
|
302
|
+
const apply = (term, format, text) => term.rich ? styleText(format, text, { validateStream: false }) : text;
|
|
303
|
+
/** `text` in a role's color. */
|
|
304
|
+
const paint = (term, role, text) => apply(term, TEXT[role], text);
|
|
305
|
+
/** `text` in bold. */
|
|
306
|
+
const bold = (term, text) => apply(term, "bold", text);
|
|
307
|
+
// src/term/confirm.ts
|
|
308
|
+
/**
|
|
309
|
+
* @file The yes-or-no prompt: on a terminal, `Yes` and `No` that ←/→ (or `h`
|
|
310
|
+
* `l`, Space) move between, `y` and `n` answer at once, Enter takes the one
|
|
311
|
+
* shown, and Esc is no; it collapses to one line. Plain mode asks `question
|
|
312
|
+
* [y/N] ` and reads a line: anything but yes is no (the default, for an empty
|
|
313
|
+
* line), and a closed input is no.
|
|
314
|
+
*/
|
|
315
|
+
/**
|
|
316
|
+
* Asks a yes-or-no question; `null` on Ctrl-C, after the terminal was restored
|
|
317
|
+
* and the interrupt sent, for a caller that must not carry on as if it were
|
|
318
|
+
* no.
|
|
319
|
+
*/
|
|
320
|
+
const confirmOrStop = async (io, options) => {
|
|
321
|
+
const { question, signal } = options;
|
|
322
|
+
const fallback = options.default === true;
|
|
323
|
+
if (signal?.aborted === true) throw withdrawn(signal);
|
|
324
|
+
if (!canKey(io.input, io.output, io.env)) {
|
|
325
|
+
const answer = (await readLine(io, ` ${question} ${fallback ? "[Y/n]" : "[y/N]"} `, signal) ?? "").trim().toLowerCase();
|
|
326
|
+
return answer === "" ? fallback : ["y", "yes"].includes(answer);
|
|
327
|
+
}
|
|
328
|
+
const term = () => termOf(io.output, io.env);
|
|
329
|
+
const region = createRegion(io.output, term);
|
|
330
|
+
return new Promise((resolve, reject) => {
|
|
331
|
+
let yes = fallback;
|
|
332
|
+
const render = () => {
|
|
333
|
+
const t = term();
|
|
334
|
+
const asked = hang(" ? ", question, t.width).map((line, i) => i === 0 ? ` ${paint(t, "accent", "?")} ${bold(t, line.slice(3))}` : line);
|
|
335
|
+
const pick = (label, on) => on ? apply(t, "inverse", ` ${label} `) : ` ${label} `;
|
|
336
|
+
const keys = paint(t, "muted", "y/n · ←/→ · Enter");
|
|
337
|
+
region.draw([...asked, ` ${pick("Yes", yes)} ${pick("No", !yes)} ${keys}`]);
|
|
338
|
+
};
|
|
339
|
+
const finish = (answer) => {
|
|
340
|
+
stop();
|
|
341
|
+
io.output.off("resize", render);
|
|
342
|
+
signal?.removeEventListener("abort", onAbort);
|
|
343
|
+
const t = term();
|
|
344
|
+
const mark = answer ? paint(t, "ok", "✓") : paint(t, "danger", "✗");
|
|
345
|
+
region.finish(cut(` ${mark} ${question} · ${answer ? "Yes" : "No"}`, t.width));
|
|
346
|
+
resolve(answer);
|
|
347
|
+
};
|
|
348
|
+
const onAbort = () => {
|
|
349
|
+
stop();
|
|
350
|
+
io.output.off("resize", render);
|
|
351
|
+
region.finish();
|
|
352
|
+
reject(withdrawn(signal));
|
|
353
|
+
};
|
|
354
|
+
const stop = readKeys(io.input, (key) => {
|
|
355
|
+
if (key.name === "enter") finish(yes);
|
|
356
|
+
else if (key.name === "escape") finish(false);
|
|
357
|
+
else if (key.name === "ctrl-c") {
|
|
358
|
+
stop();
|
|
359
|
+
io.output.off("resize", render);
|
|
360
|
+
signal?.removeEventListener("abort", onAbort);
|
|
361
|
+
region.finish();
|
|
362
|
+
resolve(null);
|
|
363
|
+
interrupt(io);
|
|
364
|
+
} else if (key.name === "left" || key.name === "right") {
|
|
365
|
+
yes = !yes;
|
|
366
|
+
render();
|
|
367
|
+
} else if (key.name === "char") {
|
|
368
|
+
const c = key.text.toLowerCase();
|
|
369
|
+
if (c === "y") finish(true);
|
|
370
|
+
else if (c === "n") finish(false);
|
|
371
|
+
else if (c === "h" || c === "l" || c === " ") {
|
|
372
|
+
yes = !yes;
|
|
373
|
+
render();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
io.output.on("resize", render);
|
|
378
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
379
|
+
render();
|
|
380
|
+
});
|
|
381
|
+
};
|
|
382
|
+
/** Asks a yes-or-no question; Ctrl-C (after restoring the terminal) is no. */
|
|
383
|
+
const confirm$1 = async (io, options) => await confirmOrStop(io, options) === true;
|
|
384
|
+
// src/output.ts
|
|
385
|
+
/**
|
|
386
|
+
* @file Terminal output helpers: styled text that turns plain when stdout is
|
|
387
|
+
* not a TTY or `NO_COLOR` is set, and a yes-or-no question on the terminal
|
|
388
|
+
* (through the terminal kit).
|
|
389
|
+
*/
|
|
390
|
+
/**
|
|
391
|
+
* Styles text for stdout; plain when stdout is not a TTY, `NO_COLOR` is set, or
|
|
392
|
+
* `--plain` (`ORK_PLAIN`) was given. The environment is read on each call:
|
|
393
|
+
* `--plain` is set after the modules load.
|
|
394
|
+
*/
|
|
395
|
+
const style = (format, text) => plainEnv() ? text : styleText(format, text, { stream: process.stdout });
|
|
396
|
+
/**
|
|
397
|
+
* Text from outside, fit for a terminal: escape sequences could hide a line on
|
|
398
|
+
* the very screen a person judges from, and a line break could forge one.
|
|
399
|
+
*/
|
|
400
|
+
const plain = (text) => stripVTControlCharacters(text).replaceAll(/[\p{Cc}\p{Bidi_Control}]/gu, " ");
|
|
401
|
+
/** A line to stdout; above a prompt that is open on the same terminal. */
|
|
402
|
+
const write = (text) => {
|
|
403
|
+
if (process.stdout.isTTY && printAbove(text)) return;
|
|
404
|
+
process.stdout.write(`${text}\n`);
|
|
405
|
+
};
|
|
406
|
+
const writeErr = (text) => {
|
|
407
|
+
if (process.stderr.isTTY && printAbove(text)) return;
|
|
408
|
+
process.stderr.write(`${text}\n`);
|
|
409
|
+
};
|
|
410
|
+
/** What a run spent: `$0.0312`, or `$0.00` for nothing. */
|
|
411
|
+
const cost = (usd) => usd === 0 ? "$0.00" : `$${usd.toFixed(4)}`;
|
|
412
|
+
/**
|
|
413
|
+
* Asks a yes-or-no question on the terminal (stderr), through the kit's confirm
|
|
414
|
+
* prompt; anything but yes is no. A trailing `[y/N]` in `question` is dropped:
|
|
415
|
+
* the prompt adds its own.
|
|
416
|
+
*/
|
|
417
|
+
const confirm = (question) => confirm$1({
|
|
418
|
+
input: process.stdin,
|
|
419
|
+
output: process.stderr
|
|
420
|
+
}, { question: question.replace(/\s*\[y\/N\]\s*$/u, "") });
|
|
421
|
+
export { widthOf as C, hang as S, readKeys as T, canKey as _, write as a, termOf as b, CHIP as c, bold as d, paint as f, withdrawn as g, readLine as h, style as i, NODE_PALETTE as l, interrupt as m, cost as n, writeErr as o, createRegion as p, plain as r, confirmOrStop as s, confirm as t, apply as u, plainEnv as v, wrap as w, cut as x, takePlainFlag as y };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { G as requestContinue, K as requestPause } from "./src-
|
|
2
|
-
import { t as findRun } from "./find-
|
|
3
|
-
import { a as write,
|
|
4
|
-
import { r as continueRefusal } from "./in-a-run-
|
|
1
|
+
import { C as openStore, G as requestContinue, K as requestPause } from "./src-D6_xjm-x.mjs";
|
|
2
|
+
import { t as findRun } from "./find-BicBbGMg.mjs";
|
|
3
|
+
import { a as write, i as style, o as writeErr } from "./output-CM95njYt.mjs";
|
|
4
|
+
import { r as continueRefusal } from "./in-a-run-CgkVouyG.mjs";
|
|
5
5
|
import { defineCommand } from "citty";
|
|
6
6
|
// src/commands/pause.ts
|
|
7
7
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { B as linkNodes, L as addressOf, U as unlinkNodes } from "./src-
|
|
2
|
-
import { a as write,
|
|
1
|
+
import { B as linkNodes, C as openStore, L as addressOf, U as unlinkNodes } from "./src-D6_xjm-x.mjs";
|
|
2
|
+
import { a as write, i as style, o as writeErr } from "./output-CM95njYt.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
// src/commands/relay.ts
|
|
5
5
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as listOrkWorktrees, t as createdWorktrees } from "./list-
|
|
3
|
-
import { i as keyOf, n as applyStep, r as cleanStep, s as stepPrunes } from "./clean-
|
|
1
|
+
import { A as lines, O as git, xt as liveStatus } from "./src-D6_xjm-x.mjs";
|
|
2
|
+
import { n as listOrkWorktrees, t as createdWorktrees } from "./list-CEaJ07jd.mjs";
|
|
3
|
+
import { i as keyOf, n as applyStep, r as cleanStep, s as stepPrunes } from "./clean-Di-KT7Mf.mjs";
|
|
4
4
|
// ../core/src/worktree/words.ts
|
|
5
5
|
/**
|
|
6
6
|
* @file The plain words for what `ork worktrees` shows, in one place, so the
|
|
@@ -45,6 +45,7 @@ const stateParts = (item, into) => {
|
|
|
45
45
|
if (item.ahead > 0) part(`${counted(item.ahead, "commit")} on no branch`, true);
|
|
46
46
|
else if (item.path !== null && !item.missing) part("nothing committed");
|
|
47
47
|
}
|
|
48
|
+
if (!item.recorded) part("not created by Ork");
|
|
48
49
|
if (item.inMainCheckout) part("checked out in the main checkout");
|
|
49
50
|
if (item.heldBy !== void 0) part(`checked out in ${item.heldBy}`);
|
|
50
51
|
return parts;
|
|
@@ -67,11 +68,12 @@ const planLine = (step) => {
|
|
|
67
68
|
};
|
|
68
69
|
};
|
|
69
70
|
/**
|
|
70
|
-
* Why an item can't be removed at all, even forced, if it can't:
|
|
71
|
-
* checkout or a worktree Ork didn't create holds
|
|
72
|
-
* going.
|
|
71
|
+
* Why an item can't be removed at all, even forced, if it can't: no run
|
|
72
|
+
* recorded creating it, the main checkout or a worktree Ork didn't create holds
|
|
73
|
+
* it, or its run is still going.
|
|
73
74
|
*/
|
|
74
75
|
const removeRefusal = (item, live) => {
|
|
76
|
+
if (!item.recorded) return "not created by Ork (no run recorded it); delete it with git if it's yours to delete";
|
|
75
77
|
if (item.inMainCheckout) return "checked out in the main checkout; switch it to another branch first";
|
|
76
78
|
if (item.heldBy !== void 0) return `checked out in ${item.heldBy}, a worktree Ork didn't create; switch it to another branch first`;
|
|
77
79
|
return live ? `run ${item.runId} is still going; stop it first` : void 0;
|
|
@@ -101,6 +103,11 @@ const projectHead = async (root) => {
|
|
|
101
103
|
branch: answer.code === 0 ? answer.stdout.trim() : null
|
|
102
104
|
};
|
|
103
105
|
};
|
|
106
|
+
/** The top of the repository `cwd` is in, even one with no commit yet. */
|
|
107
|
+
const repoRoot = async (cwd) => {
|
|
108
|
+
const top = await git(cwd, ["rev-parse", "--show-toplevel"]);
|
|
109
|
+
return top.code === 0 ? top.stdout.trim() : null;
|
|
110
|
+
};
|
|
104
111
|
/** What Ork created in the repo at `root`, from its runs' records and git. */
|
|
105
112
|
const readWorktrees = async (store, root) => ({
|
|
106
113
|
items: await listOrkWorktrees(root, createdWorktrees(store, root)),
|
|
@@ -191,4 +198,4 @@ const applyRemove = async (root, { step, force }, done = []) => {
|
|
|
191
198
|
await applyStep(root, step, force, done);
|
|
192
199
|
return done;
|
|
193
200
|
};
|
|
194
|
-
export { readWorktrees as a,
|
|
201
|
+
export { readWorktrees as a, mergeStateOf as c, stateParts as d, projectHead as i, planLine as l, lostWork as n, repoRoot as o, planRemove as r, labelOf as s, applyRemove as t, removeRefusal as u };
|
|
@@ -33,7 +33,9 @@ const TIMES = /* @__PURE__ */ new Set([
|
|
|
33
33
|
"week",
|
|
34
34
|
"month"
|
|
35
35
|
]);
|
|
36
|
-
|
|
36
|
+
/** Dimensions a tool command has no value in. */
|
|
37
|
+
const SESSION_ONLY = /* @__PURE__ */ new Set(["model", "driver"]);
|
|
38
|
+
const totalsOf = (rows, cpu) => {
|
|
37
39
|
const totals = {
|
|
38
40
|
costUsd: 0,
|
|
39
41
|
inputTokens: 0,
|
|
@@ -47,7 +49,8 @@ const totalsOf = (rows) => {
|
|
|
47
49
|
failedUsd: 0,
|
|
48
50
|
reworkUsd: 0,
|
|
49
51
|
chatUsd: 0,
|
|
50
|
-
cacheHitRate: 0
|
|
52
|
+
cacheHitRate: 0,
|
|
53
|
+
cpuMs: cpu.reduce((sum, row) => sum + row.userMs + row.systemMs, 0)
|
|
51
54
|
};
|
|
52
55
|
for (const row of rows) {
|
|
53
56
|
totals.costUsd += row.costUsd;
|
|
@@ -65,15 +68,22 @@ const totalsOf = (rows) => {
|
|
|
65
68
|
totals.cacheHitRate = read === 0 ? 0 : totals.cacheReadTokens / read;
|
|
66
69
|
return totals;
|
|
67
70
|
};
|
|
68
|
-
const groupBy = (rows, dimension) => {
|
|
71
|
+
const groupBy = (rows, cpu, dimension) => {
|
|
69
72
|
const buckets = /* @__PURE__ */ new Map();
|
|
70
|
-
|
|
73
|
+
const bucketOf = (row) => {
|
|
71
74
|
const key = KEYS[dimension](row);
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
const found = buckets.get(key);
|
|
76
|
+
if (found !== void 0) return found;
|
|
77
|
+
const fresh = {
|
|
78
|
+
rows: [],
|
|
79
|
+
cpu: []
|
|
80
|
+
};
|
|
81
|
+
buckets.set(key, fresh);
|
|
82
|
+
return fresh;
|
|
83
|
+
};
|
|
84
|
+
for (const row of rows) bucketOf(row).rows.push(row);
|
|
85
|
+
if (!SESSION_ONLY.has(dimension)) for (const row of cpu) bucketOf(row).cpu.push(row);
|
|
86
|
+
const groups = [...buckets].map(([key, bucket]) => Object.assign(totalsOf(bucket.rows, bucket.cpu), { key }));
|
|
77
87
|
return TIMES.has(dimension) ? groups.toSorted((a, b) => a.key.localeCompare(b.key)) : groups.toSorted((a, b) => b.costUsd - a.costUsd);
|
|
78
88
|
};
|
|
79
89
|
/** Below this, a percentage says nothing worth acting on. */
|
|
@@ -107,18 +117,19 @@ const hintsOf = (totals, nodes) => {
|
|
|
107
117
|
});
|
|
108
118
|
return hints;
|
|
109
119
|
};
|
|
110
|
-
/** Everything a report shows, from the ledger's rows. */
|
|
111
|
-
const usageReport = (rows) => {
|
|
112
|
-
const totals = totalsOf(rows);
|
|
120
|
+
/** Everything a report shows, from the ledger's rows and tool commands' CPU. */
|
|
121
|
+
const usageReport = (rows, cpu = []) => {
|
|
122
|
+
const totals = totalsOf(rows, cpu);
|
|
123
|
+
const by = (dimension) => groupBy(rows, cpu, dimension);
|
|
113
124
|
const groups = {
|
|
114
|
-
day:
|
|
115
|
-
week:
|
|
116
|
-
month:
|
|
117
|
-
workflow:
|
|
118
|
-
node:
|
|
119
|
-
model:
|
|
120
|
-
driver:
|
|
121
|
-
project:
|
|
125
|
+
day: by("day"),
|
|
126
|
+
week: by("week"),
|
|
127
|
+
month: by("month"),
|
|
128
|
+
workflow: by("workflow"),
|
|
129
|
+
node: by("node"),
|
|
130
|
+
model: by("model"),
|
|
131
|
+
driver: by("driver"),
|
|
132
|
+
project: by("project")
|
|
122
133
|
};
|
|
123
134
|
return {
|
|
124
135
|
totals,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { u as inheritedScrubber } from "./src-
|
|
1
|
+
import { C as openStore, Cn as wakeRun, jt as scriptContactsOf, xn as socketPath, xt as liveStatus } from "./src-D6_xjm-x.mjs";
|
|
2
|
+
import { o as writeErr } from "./output-CM95njYt.mjs";
|
|
3
|
+
import { u as inheritedScrubber } from "./src-9iGkjnsy.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
// ../core/src/remote/send.ts
|
|
6
6
|
/**
|