@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.
Files changed (71) hide show
  1. package/README.md +12 -7
  2. package/dist/__build_info__-D6mXC4cD.mjs +228 -0
  3. package/dist/apply-Mb0fNpCX.mjs +17 -0
  4. package/dist/argument-error-CihH46Le.mjs +27 -0
  5. package/dist/attach-C1OnxyMh.mjs +92 -0
  6. package/dist/attach-CEGbRg1D.mjs +1230 -0
  7. package/dist/bin.mjs +1312 -2390
  8. package/dist/brief-DH66xmj8.mjs +115 -0
  9. package/dist/{decide-CGyx9rv7.mjs → builtin-files-Cf-NZDqM.mjs} +3 -51
  10. package/dist/chips-BqcT58qr.mjs +43 -0
  11. package/dist/command-tree-CEkzWNy6.mjs +63 -0
  12. package/dist/{completion-CSU5vkEt.mjs → completion-DS0gCZRm.mjs} +200 -30
  13. package/dist/context-CGPh12HV.mjs +106 -0
  14. package/dist/detached-Dakw5RZj.mjs +38 -0
  15. package/dist/extra-arguments-BxQMXARO.mjs +13 -0
  16. package/dist/{find-D6Gj1sxg.mjs → find-BTikxc54.mjs} +2 -2
  17. package/dist/flags-C8yCq0j5.mjs +145 -0
  18. package/dist/follow-ups-CtlhHbwe.mjs +563 -0
  19. package/dist/format-BB4E-T6x.mjs +249 -0
  20. package/dist/{help-groups-BwYoAWsh.mjs → help-groups-BpRgBSTh.mjs} +42 -76
  21. package/dist/in-a-run-CbnKjO7t.mjs +27 -0
  22. package/dist/install-CWcpX5g4.mjs +211 -0
  23. package/dist/{limits-B1-DYc3d.mjs → limits-VQqsBoQk.mjs} +2 -2
  24. package/dist/{mcp-DE_EmyVm.mjs → mcp-wGfkMCVF.mjs} +6 -6
  25. package/dist/once-BHxSdZz_.mjs +112 -0
  26. package/dist/orphan-guard-BFKg8VSd.mjs +58 -0
  27. package/dist/output-B1kwo0YU.mjs +439 -0
  28. package/dist/outputs-text-CNTJ7Qq_.mjs +19 -0
  29. package/dist/{pause-CL6N_Xrj.mjs → pause-C_Wxh-KM.mjs} +7 -4
  30. package/dist/print-DNYzq4Rr.mjs +19 -0
  31. package/dist/project-records-XY3ku95x.mjs +233 -0
  32. package/dist/refusals-DQQX3AIb.mjs +116 -0
  33. package/dist/{relay-0SD0erwf.mjs → relay-BA6tsnpc.mjs} +2 -2
  34. package/dist/run-Bvvg-fiF.mjs +234 -0
  35. package/dist/run-DBUAq-Uu.mjs +1452 -0
  36. package/dist/send-nrKtak5F.mjs +105 -0
  37. package/dist/{src-Dujmvv4s.mjs → src-8cOPOgrT.mjs} +12523 -5507
  38. package/dist/{src-BbyEoMn4.mjs → src-CmEvHdju.mjs} +797 -233
  39. package/dist/src-D4JSymhK.mjs +3104 -0
  40. package/dist/{src-DOcK8Jfp.mjs → src-I_RyKu6_.mjs} +1022 -590
  41. package/dist/store-CDpk8Wtz.mjs +16 -0
  42. package/dist/ui/THIRD_PARTY_LICENSES.txt +612 -0
  43. package/dist/ui/assets/app--FijEXiJ.css +1 -0
  44. package/dist/ui/assets/app-BowhjEAe.js +112 -0
  45. package/dist/ui/assets/{index-9dFha9Qn.js → index-CrMa2oHB.js} +3 -3
  46. package/dist/ui/assets/theme-CdXvqTQ6.js +1 -0
  47. package/dist/ui/index.html +2 -2
  48. package/dist/{ui-CTmO2Xzu.mjs → ui-DY3Dnpho.mjs} +54 -25
  49. package/dist/usage-1VwSg_Qt.mjs +257 -0
  50. package/dist/validate-BxayjjkV.mjs +632 -0
  51. package/dist/verdict-CMvPCpWC.mjs +84 -0
  52. package/dist/{watch-CtbgDAcz.mjs → watch-CT0EtLPZ.mjs} +45 -17
  53. package/dist/{workflows-D1_WVukk.mjs → workflows-DHHi-APg.mjs} +30 -5
  54. package/dist/worktrees-CZvbHzbN.mjs +302 -0
  55. package/package.json +10 -8
  56. package/dist/__build_info__-fy6L1Mrd.mjs +0 -615
  57. package/dist/clean-DaHjs__2.mjs +0 -88
  58. package/dist/context-DT8MfEIR.mjs +0 -274
  59. package/dist/format-Dnfv8zMT.mjs +0 -427
  60. package/dist/in-a-run-pLAZeNys.mjs +0 -22
  61. package/dist/list-BrH8KRSJ.mjs +0 -110
  62. package/dist/output-BLQWD6MJ.mjs +0 -950
  63. package/dist/remove-BeGdqroy.mjs +0 -194
  64. package/dist/report-DK4LGqwF.mjs +0 -129
  65. package/dist/send-C8tunWhv.mjs +0 -163
  66. package/dist/src-CCUKRX51.mjs +0 -1204
  67. package/dist/ui/assets/app-BNT0SuIz.css +0 -1
  68. package/dist/ui/assets/app-T7wVAhgx.js +0 -67
  69. package/dist/ui/assets/theme-3xH5jxKs.js +0 -1
  70. package/dist/usage-DYUmP0i4.mjs +0 -419
  71. 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 new shell is one more entry.
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
- cands=()
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 can't source <(…); saving it to a file and sourcing that
52
- # works too.) Every expansion is quoted and lines are read with read -r, so a
53
- # candidate is never split or glob-expanded.
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 what follows it.
63
- if [[ "$cur" == *:* && "$COMP_WORDBREAKS" == *:* ]]; then
64
- strip="\${cur%"\${cur##*:}"}"
65
- fi
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 { findEntry as a, subcommandsOf as c, SHELL_SCRIPTS as d, argsOf as i, COMPLETE_COMMAND as l, groupedEntries as n, publicArgs as o, argInfoOf as r, resolved as s, groupOf as t, SHELLS as u };
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 };