@krapht/ork 1.0.0-alpha.13 → 1.0.0-alpha.15
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/dist/__build_info__-Bsb1kjgF.mjs +340 -0
- package/dist/{store-CM3cERGl.mjs → attach-BHO3X3cf.mjs} +36 -91
- package/dist/{attach-E6NNyA3T.mjs → attach-CPespxlQ.mjs} +6 -5
- package/dist/bin.mjs +148 -1444
- package/dist/brief-za0ozN_E.mjs +58 -0
- package/dist/{builtin-files-n6o6oWpB.mjs → builtin-files-2bHf76x-.mjs} +1 -1
- package/dist/{chips-CyeB_rhR.mjs → chips-BqcT58qr.mjs} +1 -1
- package/dist/{completion-SNNNxnv9.mjs → completion-CQ2UYKuJ.mjs} +52 -11
- package/dist/context-BNqDw9z1.mjs +106 -0
- package/dist/{find-CjsNYAAf.mjs → find-BYLlszqj.mjs} +1 -1
- package/dist/follow-ups-B4GJ3Vzw.mjs +461 -0
- package/dist/{format-C60TFpRe.mjs → format-C8MsFj8_.mjs} +30 -16
- package/dist/{help-groups-CyfPWlST.mjs → help-groups-CQ98mLjF.mjs} +20 -6
- package/dist/{in-a-run-BYjTXaST.mjs → in-a-run-Bp0k0J09.mjs} +1 -1
- package/dist/{__build_info__-Da73_nBs.mjs → install-DWtXTq4y.mjs} +3 -227
- package/dist/{limits-C5QKZNLT.mjs → limits-B1qzS4cX.mjs} +1 -1
- package/dist/{mcp-bcctR-wc.mjs → mcp-BOrVkyhN.mjs} +6 -6
- package/dist/{once-DPQu5Q3H.mjs → once-C1fL3GVo.mjs} +4 -4
- package/dist/orphan-guard-BFKg8VSd.mjs +58 -0
- package/dist/{output-CnRzrbUM.mjs → output-B1kwo0YU.mjs} +17 -1
- package/dist/{pause-BPvlDIvz.mjs → pause-DOMK-R8V.mjs} +4 -4
- package/dist/post-BzA6Qobq.mjs +44 -0
- package/dist/{relay-C8W0t_zc.mjs → relay-OWuu4fib.mjs} +2 -2
- package/dist/run--4-1xAF2.mjs +1451 -0
- package/dist/{run-CQIfq918.mjs → run-CiPsW9Kf.mjs} +1 -1
- package/dist/send-CnNNW-ot.mjs +105 -0
- package/dist/{src-CqRqsb2t.mjs → src-B9zWIVqo.mjs} +226 -44
- package/dist/{src-CGJGL1sZ.mjs → src-DQKz2eOH.mjs} +326 -81
- package/dist/{src-D93tHZ5B.mjs → src-DcrhgipG.mjs} +107 -47
- package/dist/{src-BeR6f70A.mjs → src-Dr_fTSu-.mjs} +5858 -3746
- package/dist/store-oKJ-cCf3.mjs +16 -0
- package/dist/ui/assets/app-Bdc6OBeZ.css +1 -0
- package/dist/ui/assets/app-Drp8Dm9y.js +111 -0
- package/dist/ui/assets/{index-CWwm8HTG.js → index-BtwkftKe.js} +2 -2
- package/dist/ui/index.html +1 -1
- package/dist/{ui-D2Ydofq0.mjs → ui-CzKlsCnm.mjs} +4 -4
- package/dist/{usage-6WIowblz.mjs → usage-DHGNk2JL.mjs} +13 -141
- package/dist/{context-DHudLA5e.mjs → validate-Bs_gg22g.mjs} +17 -107
- package/dist/{verdict-CIy8tAyX.mjs → verdict-CMvPCpWC.mjs} +2 -2
- package/dist/{watch-uOU_WeBI.mjs → watch-CCCfw3n7.mjs} +24 -8
- package/dist/{workflows-7tQdMnrA.mjs → workflows-CW28ZU-z.mjs} +3 -3
- package/dist/{worktrees-ZCII298F.mjs → worktrees-CyE8aF6j.mjs} +3 -5
- package/package.json +2 -2
- package/dist/clean-Bh4KZgzc.mjs +0 -91
- package/dist/decide-DuIHACvH.mjs +0 -50
- package/dist/list-BUb5Uc3H.mjs +0 -117
- package/dist/remove-BkFWGVYo.mjs +0 -201
- package/dist/report-CEN8VoUb.mjs +0 -140
- package/dist/send-CaZlGG5a.mjs +0 -163
- package/dist/ui/assets/app-DYRC0V4p.js +0 -111
- package/dist/ui/assets/app-QoHVi-mb.css +0 -1
package/dist/list-BUb5Uc3H.mjs
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { Bt as lines, Rt as git, zt as gitOk } from "./src-BeR6f70A.mjs";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
// ../core/src/worktree/list.ts
|
|
4
|
-
/**
|
|
5
|
-
* @file What `ork worktrees` shows: every worktree and branch Ork's runs
|
|
6
|
-
* recorded creating, whatever the project's git rules named them, and any
|
|
7
|
-
* `ork/*` branch besides, with their state read from git itself. What clean
|
|
8
|
-
* and remove do with them is clean.ts.
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* The worktrees the project's runs recorded creating, oldest run first; one
|
|
12
|
-
* entry per branch or folder, from the latest run that created it.
|
|
13
|
-
*/
|
|
14
|
-
const createdWorktrees = (store, project) => {
|
|
15
|
-
const byName = /* @__PURE__ */ new Map();
|
|
16
|
-
for (const run of store.runs({ project }).toReversed()) for (const { event } of store.events(run.id)) {
|
|
17
|
-
if (event.type !== "worktree" || event.action !== "created") continue;
|
|
18
|
-
byName.set(event.branch ?? event.path, {
|
|
19
|
-
runId: run.id,
|
|
20
|
-
nodeId: event.id,
|
|
21
|
-
path: event.path,
|
|
22
|
-
...event.branch === void 0 ? {} : { branch: event.branch }
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
return [...byName.values()];
|
|
26
|
-
};
|
|
27
|
-
const parsePorcelain = (text) => text.split("\n\n").map((block) => lines(block)).map((fields) => {
|
|
28
|
-
const value = (key) => fields.find((f) => f.startsWith(`${key} `))?.slice(key.length + 1);
|
|
29
|
-
return {
|
|
30
|
-
path: value("worktree") ?? "",
|
|
31
|
-
branch: (value("branch") ?? "").replace(/^refs\/heads\//u, ""),
|
|
32
|
-
prunable: fields.some((f) => f.startsWith("prunable"))
|
|
33
|
-
};
|
|
34
|
-
}).filter((entry) => entry.path !== "");
|
|
35
|
-
/**
|
|
36
|
-
* The `ork/<run>/<node>` branches of runs that recorded nothing; with no path
|
|
37
|
-
* recorded, the worktree is whichever has the branch checked out.
|
|
38
|
-
*/
|
|
39
|
-
const prefixed = (branch) => {
|
|
40
|
-
const [, runId = "", ...rest] = branch.split("/");
|
|
41
|
-
return {
|
|
42
|
-
runId,
|
|
43
|
-
nodeId: rest.join("/"),
|
|
44
|
-
path: "",
|
|
45
|
-
branch
|
|
46
|
-
};
|
|
47
|
-
};
|
|
48
|
-
/** How many commits a detached worktree holds that no branch, tag or remote has. */
|
|
49
|
-
const onNoBranch = async (entry) => entry === void 0 ? "0" : (await git(entry.path, [
|
|
50
|
-
"rev-list",
|
|
51
|
-
"--count",
|
|
52
|
-
"HEAD",
|
|
53
|
-
"--not",
|
|
54
|
-
"--branches",
|
|
55
|
-
"--tags",
|
|
56
|
-
"--remotes"
|
|
57
|
-
])).stdout;
|
|
58
|
-
/** One created worktree or branch, with its state read from git. */
|
|
59
|
-
const stateOf = async (root, entries, item, recorded) => {
|
|
60
|
-
const { branch } = item;
|
|
61
|
-
const entry = branch === void 0 || item.path !== "" ? entries.find((e) => e.path === item.path) : entries.find((e) => e.branch === branch);
|
|
62
|
-
const holder = branch === void 0 ? void 0 : entries.find((e) => e.branch === branch && e.path !== entry?.path);
|
|
63
|
-
const missing = entry !== void 0 && (entry.prunable || !existsSync(entry.path));
|
|
64
|
-
const status = entry === void 0 || missing ? "" : (await git(entry.path, ["status", "--porcelain"])).stdout;
|
|
65
|
-
const checkedOut = branch === void 0 && entry !== void 0 && entry.branch !== "" ? entry.branch : void 0;
|
|
66
|
-
const on = branch ?? checkedOut;
|
|
67
|
-
const counted = on === void 0 ? await onNoBranch(entry === void 0 || missing ? void 0 : entry) : (await git(root, [
|
|
68
|
-
"rev-list",
|
|
69
|
-
"--count",
|
|
70
|
-
`HEAD..${on}`
|
|
71
|
-
])).stdout;
|
|
72
|
-
const ahead = Number(counted);
|
|
73
|
-
const changes = lines(status).length;
|
|
74
|
-
return {
|
|
75
|
-
...branch === void 0 ? {} : { branch },
|
|
76
|
-
...checkedOut === void 0 ? {} : { checkedOut },
|
|
77
|
-
runId: item.runId,
|
|
78
|
-
nodeId: item.nodeId,
|
|
79
|
-
recorded,
|
|
80
|
-
path: entry?.path ?? null,
|
|
81
|
-
missing,
|
|
82
|
-
dirty: changes > 0,
|
|
83
|
-
changes,
|
|
84
|
-
ahead: Number.isNaN(ahead) ? 0 : ahead,
|
|
85
|
-
merged: on !== void 0 && ahead === 0,
|
|
86
|
-
inMainCheckout: entry?.path === root || holder?.path === root,
|
|
87
|
-
...holder === void 0 || holder.path === root ? {} : { heldBy: holder.path }
|
|
88
|
-
};
|
|
89
|
-
};
|
|
90
|
-
/**
|
|
91
|
-
* Every worktree and branch Ork's runs created in the repository at `root`,
|
|
92
|
-
* with its state: those the runs recorded (`created`, whatever the project's
|
|
93
|
-
* git rules named them), and any `ork/` branch besides. A recorded one that git
|
|
94
|
-
* no longer has is left out.
|
|
95
|
-
*/
|
|
96
|
-
const listOrkWorktrees = async (root, created = []) => {
|
|
97
|
-
const entries = parsePorcelain(await gitOk(root, [
|
|
98
|
-
"worktree",
|
|
99
|
-
"list",
|
|
100
|
-
"--porcelain"
|
|
101
|
-
]));
|
|
102
|
-
const branches = lines(await gitOk(root, [
|
|
103
|
-
"for-each-ref",
|
|
104
|
-
"--format=%(refname:short)",
|
|
105
|
-
"refs/heads/"
|
|
106
|
-
]));
|
|
107
|
-
const recordedBranches = new Set(created.flatMap((c) => c.branch ?? []));
|
|
108
|
-
const items = [...created.filter((c) => c.branch === void 0 ? entries.some((e) => e.path === c.path) : branches.includes(c.branch)).map((item) => ({
|
|
109
|
-
item,
|
|
110
|
-
recorded: true
|
|
111
|
-
})), ...branches.filter((b) => b.startsWith("ork/") && !recordedBranches.has(b)).map((b) => ({
|
|
112
|
-
item: prefixed(b),
|
|
113
|
-
recorded: false
|
|
114
|
-
}))];
|
|
115
|
-
return Promise.all(items.map(({ item, recorded }) => stateOf(root, entries, item, recorded)));
|
|
116
|
-
};
|
|
117
|
-
export { listOrkWorktrees as n, createdWorktrees as t };
|
package/dist/remove-BkFWGVYo.mjs
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
import { Bt as lines, Rt as git, _t as liveStatus } from "./src-BeR6f70A.mjs";
|
|
2
|
-
import { n as listOrkWorktrees, t as createdWorktrees } from "./list-BUb5Uc3H.mjs";
|
|
3
|
-
import { i as keyOf, n as applyStep, r as cleanStep, s as stepPrunes } from "./clean-Bh4KZgzc.mjs";
|
|
4
|
-
// ../core/src/worktree/words.ts
|
|
5
|
-
/**
|
|
6
|
-
* @file The plain words for what `ork worktrees` shows, in one place, so the
|
|
7
|
-
* CLI and the Worktrees panel say the same thing: an item's name, its merge
|
|
8
|
-
* state, its state as parts (each marked when it's work that removing would
|
|
9
|
-
* lose), a plan's line, and why an item can't be removed. The CLI colors
|
|
10
|
-
* them; the panel lays them out.
|
|
11
|
-
*/
|
|
12
|
-
/** An item's name: its branch, or its run and node when created detached. */
|
|
13
|
-
const labelOf = (item) => item.branch ?? `${item.runId}/${item.nodeId}`;
|
|
14
|
-
/** Merged into the current branch, not, or detached on no branch at all. */
|
|
15
|
-
const mergeStateOf = (item) => {
|
|
16
|
-
if (item.branch === void 0 && item.checkedOut === void 0) return "detached";
|
|
17
|
-
return item.merged ? "merged" : "unmerged";
|
|
18
|
-
};
|
|
19
|
-
/** `3 commits`, `1 commit`. */
|
|
20
|
-
const counted = (count, noun) => `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
|
|
21
|
-
/**
|
|
22
|
-
* An item's state, in order: where its worktree is when it isn't plain, the
|
|
23
|
-
* workflow's own branch, uncommitted changes, unmerged or unreachable commits,
|
|
24
|
-
* and the main checkout or another worktree holding the branch. `into` names
|
|
25
|
-
* the branch "not merged" is measured against, when the words should say it.
|
|
26
|
-
*/
|
|
27
|
-
const stateParts = (item, into) => {
|
|
28
|
-
const parts = [];
|
|
29
|
-
const part = (text, risk = false) => {
|
|
30
|
-
parts.push({
|
|
31
|
-
text,
|
|
32
|
-
risk
|
|
33
|
-
});
|
|
34
|
-
};
|
|
35
|
-
if (item.path === null) part("no worktree");
|
|
36
|
-
else if (item.missing) part("missing (prunable)");
|
|
37
|
-
if (item.checkedOut !== void 0) part(`on the workflow's branch ${item.checkedOut}`);
|
|
38
|
-
if (item.dirty) part("uncommitted changes", true);
|
|
39
|
-
const state = mergeStateOf(item);
|
|
40
|
-
if (state === "unmerged") {
|
|
41
|
-
const where = into === void 0 ? "" : ` into ${into}`;
|
|
42
|
-
part(`${counted(item.ahead, "commit")} not merged${where}`);
|
|
43
|
-
}
|
|
44
|
-
if (state === "detached") {
|
|
45
|
-
if (item.ahead > 0) part(`${counted(item.ahead, "commit")} on no branch`, true);
|
|
46
|
-
else if (item.path !== null && !item.missing) part("nothing committed");
|
|
47
|
-
}
|
|
48
|
-
if (!item.recorded) part("not created by Ork");
|
|
49
|
-
if (item.inMainCheckout) part("checked out in the main checkout");
|
|
50
|
-
if (item.heldBy !== void 0) part(`checked out in ${item.heldBy}`);
|
|
51
|
-
return parts;
|
|
52
|
-
};
|
|
53
|
-
/** What a step does, as a plan says it. */
|
|
54
|
-
const planWhat = (step) => {
|
|
55
|
-
const actions = [
|
|
56
|
-
...step.removeWorktree ? ["remove worktree"] : [],
|
|
57
|
-
...stepPrunes(step) && !step.removeWorktree ? ["prune worktree"] : [],
|
|
58
|
-
...step.deleteBranch ? ["delete branch"] : []
|
|
59
|
-
];
|
|
60
|
-
return actions.length > 0 ? actions.join(" + ") : "keep";
|
|
61
|
-
};
|
|
62
|
-
/** `ork/r1/lead: remove worktree`, `r1/check (detached): keep (…)`. */
|
|
63
|
-
const planLine = (step) => {
|
|
64
|
-
const { item } = step;
|
|
65
|
-
return {
|
|
66
|
-
text: `${item.branch === void 0 && item.checkedOut === void 0 ? `${labelOf(item)} (detached)` : labelOf(item)}: ${planWhat(step)}`,
|
|
67
|
-
...step.skip === void 0 ? {} : { reason: step.skip }
|
|
68
|
-
};
|
|
69
|
-
};
|
|
70
|
-
/**
|
|
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.
|
|
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";
|
|
77
|
-
if (item.inMainCheckout) return "checked out in the main checkout; switch it to another branch first";
|
|
78
|
-
if (item.heldBy !== void 0) return `checked out in ${item.heldBy}, a worktree Ork didn't create; switch it to another branch first`;
|
|
79
|
-
return live ? `run ${item.runId} is still going; stop it first` : void 0;
|
|
80
|
-
};
|
|
81
|
-
// ../core/src/worktree/remove.ts
|
|
82
|
-
/** The ids of the project's runs whose process is still going. */
|
|
83
|
-
const liveRunIds = (store, root) => new Set(store.runs({ project: root }).filter((run) => liveStatus(run) === "running").map((run) => run.id));
|
|
84
|
-
/**
|
|
85
|
-
* Whether `root` is a repository with a commit, and the branch checked out
|
|
86
|
-
* there, which "merged" means merged into (null when its HEAD is detached).
|
|
87
|
-
*/
|
|
88
|
-
const projectHead = async (root) => {
|
|
89
|
-
if ((await git(root, [
|
|
90
|
-
"rev-parse",
|
|
91
|
-
"--verify",
|
|
92
|
-
"--quiet",
|
|
93
|
-
"HEAD"
|
|
94
|
-
])).code !== 0) return { ok: false };
|
|
95
|
-
const answer = await git(root, [
|
|
96
|
-
"symbolic-ref",
|
|
97
|
-
"--short",
|
|
98
|
-
"-q",
|
|
99
|
-
"HEAD"
|
|
100
|
-
]);
|
|
101
|
-
return {
|
|
102
|
-
ok: true,
|
|
103
|
-
branch: answer.code === 0 ? answer.stdout.trim() : null
|
|
104
|
-
};
|
|
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
|
-
};
|
|
111
|
-
/** What Ork created in the repo at `root`, from its runs' records and git. */
|
|
112
|
-
const readWorktrees = async (store, root) => ({
|
|
113
|
-
items: await listOrkWorktrees(root, createdWorktrees(store, root)),
|
|
114
|
-
live: liveRunIds(store, root)
|
|
115
|
-
});
|
|
116
|
-
/**
|
|
117
|
-
* The plan to remove one item, named by `keyOf`. A branch with no worktree is
|
|
118
|
-
* removed by deleting it, so `deleteBranch` is implied there.
|
|
119
|
-
*/
|
|
120
|
-
const planRemove = ({ items, live }, key, deleteBranch) => {
|
|
121
|
-
const item = items.find((i) => keyOf(i) === key);
|
|
122
|
-
if (item === void 0) return {
|
|
123
|
-
ok: false,
|
|
124
|
-
error: `No Ork worktree or branch ${key}.`,
|
|
125
|
-
gone: true
|
|
126
|
-
};
|
|
127
|
-
const refusal = removeRefusal(item, live.has(item.runId));
|
|
128
|
-
if (refusal !== void 0) return {
|
|
129
|
-
ok: false,
|
|
130
|
-
error: `Can't remove ${labelOf(item)}: ${refusal}.`,
|
|
131
|
-
gone: false
|
|
132
|
-
};
|
|
133
|
-
const branches = item.branch !== void 0 && (deleteBranch || item.path === null);
|
|
134
|
-
const careful = cleanStep(item, {
|
|
135
|
-
branches,
|
|
136
|
-
force: false,
|
|
137
|
-
live
|
|
138
|
-
});
|
|
139
|
-
if (careful.skip === void 0) return {
|
|
140
|
-
ok: true,
|
|
141
|
-
plan: {
|
|
142
|
-
step: careful,
|
|
143
|
-
force: false
|
|
144
|
-
}
|
|
145
|
-
};
|
|
146
|
-
return {
|
|
147
|
-
ok: true,
|
|
148
|
-
plan: {
|
|
149
|
-
step: cleanStep(item, {
|
|
150
|
-
branches,
|
|
151
|
-
force: true,
|
|
152
|
-
live
|
|
153
|
-
}),
|
|
154
|
-
force: true
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
};
|
|
158
|
-
const out = async (cwd, args) => lines((await git(cwd, args)).stdout);
|
|
159
|
-
/** What carrying out `plan` would lose, read from git. */
|
|
160
|
-
const lostWork = async (root, { step }) => {
|
|
161
|
-
const { item } = step;
|
|
162
|
-
const inside = item.path !== null && !item.missing ? item.path : null;
|
|
163
|
-
const changes = inside !== null && item.dirty && step.removeWorktree ? await out(inside, ["status", "--porcelain"]) : [];
|
|
164
|
-
if (step.deleteBranch && item.branch !== void 0 && !item.merged) return {
|
|
165
|
-
changes,
|
|
166
|
-
commits: await out(root, [
|
|
167
|
-
"log",
|
|
168
|
-
"--oneline",
|
|
169
|
-
`HEAD..${item.branch}`
|
|
170
|
-
]),
|
|
171
|
-
commitsOn: "unmerged"
|
|
172
|
-
};
|
|
173
|
-
if (item.branch === void 0 && item.checkedOut === void 0 && inside !== null && item.ahead > 0 && step.removeWorktree) return {
|
|
174
|
-
changes,
|
|
175
|
-
commits: await out(inside, [
|
|
176
|
-
"log",
|
|
177
|
-
"--oneline",
|
|
178
|
-
"HEAD",
|
|
179
|
-
"--not",
|
|
180
|
-
"--branches",
|
|
181
|
-
"--tags",
|
|
182
|
-
"--remotes"
|
|
183
|
-
]),
|
|
184
|
-
commitsOn: "no branch"
|
|
185
|
-
};
|
|
186
|
-
return {
|
|
187
|
-
changes,
|
|
188
|
-
commits: [],
|
|
189
|
-
commitsOn: null
|
|
190
|
-
};
|
|
191
|
-
};
|
|
192
|
-
/**
|
|
193
|
-
* Carries out a remove plan; what it did goes into `done` as it goes. The
|
|
194
|
-
* caller has checked that a plan needing `force` was asked to force. It touches
|
|
195
|
-
* only its own item, as each of clean's steps does.
|
|
196
|
-
*/
|
|
197
|
-
const applyRemove = async (root, { step, force }, done = []) => {
|
|
198
|
-
await applyStep(root, step, force, done);
|
|
199
|
-
return done;
|
|
200
|
-
};
|
|
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 };
|
package/dist/report-CEN8VoUb.mjs
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
// ../core/src/usage/report.ts
|
|
2
|
-
const USAGE_DIMENSIONS = [
|
|
3
|
-
"day",
|
|
4
|
-
"week",
|
|
5
|
-
"month",
|
|
6
|
-
"workflow",
|
|
7
|
-
"node",
|
|
8
|
-
"model",
|
|
9
|
-
"driver",
|
|
10
|
-
"project"
|
|
11
|
-
];
|
|
12
|
-
const pad = (value) => String(value).padStart(2, "0");
|
|
13
|
-
/** `2026-11-03`, in the machine's own time zone. */
|
|
14
|
-
const dayOf = (date) => `${String(date.getFullYear())}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
15
|
-
/** The Monday of a time's week. */
|
|
16
|
-
const weekOf = (date) => {
|
|
17
|
-
const monday = new Date(date);
|
|
18
|
-
monday.setDate(date.getDate() - (date.getDay() + 6) % 7);
|
|
19
|
-
return dayOf(monday);
|
|
20
|
-
};
|
|
21
|
-
const KEYS = {
|
|
22
|
-
day: (row) => dayOf(new Date(row.at)),
|
|
23
|
-
week: (row) => weekOf(new Date(row.at)),
|
|
24
|
-
month: (row) => dayOf(new Date(row.at)).slice(0, 7),
|
|
25
|
-
workflow: (row) => row.workflow,
|
|
26
|
-
node: (row) => `${row.workflow}/${row.template ?? row.node}`,
|
|
27
|
-
model: (row) => row.model ?? "unknown",
|
|
28
|
-
driver: (row) => row.driver ?? "unknown",
|
|
29
|
-
project: (row) => row.project
|
|
30
|
-
};
|
|
31
|
-
const TIMES = /* @__PURE__ */ new Set([
|
|
32
|
-
"day",
|
|
33
|
-
"week",
|
|
34
|
-
"month"
|
|
35
|
-
]);
|
|
36
|
-
/** Dimensions a tool command has no value in. */
|
|
37
|
-
const SESSION_ONLY = /* @__PURE__ */ new Set(["model", "driver"]);
|
|
38
|
-
const totalsOf = (rows, cpu) => {
|
|
39
|
-
const totals = {
|
|
40
|
-
costUsd: 0,
|
|
41
|
-
inputTokens: 0,
|
|
42
|
-
outputTokens: 0,
|
|
43
|
-
cacheReadTokens: 0,
|
|
44
|
-
cacheWriteTokens: 0,
|
|
45
|
-
turns: 0,
|
|
46
|
-
durationMs: 0,
|
|
47
|
-
sessions: rows.length,
|
|
48
|
-
runs: new Set(rows.map((row) => row.runId)).size,
|
|
49
|
-
failedUsd: 0,
|
|
50
|
-
reworkUsd: 0,
|
|
51
|
-
chatUsd: 0,
|
|
52
|
-
cacheHitRate: 0,
|
|
53
|
-
cpuMs: cpu.reduce((sum, row) => sum + row.userMs + row.systemMs, 0)
|
|
54
|
-
};
|
|
55
|
-
for (const row of rows) {
|
|
56
|
-
totals.costUsd += row.costUsd;
|
|
57
|
-
totals.inputTokens += row.inputTokens;
|
|
58
|
-
totals.outputTokens += row.outputTokens;
|
|
59
|
-
totals.cacheReadTokens += row.cacheReadTokens;
|
|
60
|
-
totals.cacheWriteTokens += row.cacheWriteTokens;
|
|
61
|
-
totals.turns += row.turns;
|
|
62
|
-
totals.durationMs += row.durationMs;
|
|
63
|
-
if (row.failed) totals.failedUsd += row.costUsd;
|
|
64
|
-
if (row.kind === "chat") totals.chatUsd += row.costUsd;
|
|
65
|
-
else if (row.round > 1) totals.reworkUsd += row.costUsd;
|
|
66
|
-
}
|
|
67
|
-
const read = totals.inputTokens + totals.cacheReadTokens + totals.cacheWriteTokens;
|
|
68
|
-
totals.cacheHitRate = read === 0 ? 0 : totals.cacheReadTokens / read;
|
|
69
|
-
return totals;
|
|
70
|
-
};
|
|
71
|
-
const groupBy = (rows, cpu, dimension) => {
|
|
72
|
-
const buckets = /* @__PURE__ */ new Map();
|
|
73
|
-
const bucketOf = (row) => {
|
|
74
|
-
const key = KEYS[dimension](row);
|
|
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 }));
|
|
87
|
-
return TIMES.has(dimension) ? groups.toSorted((a, b) => a.key.localeCompare(b.key)) : groups.toSorted((a, b) => b.costUsd - a.costUsd);
|
|
88
|
-
};
|
|
89
|
-
/** Below this, a percentage says nothing worth acting on. */
|
|
90
|
-
const MIN_SPEND_USD = .5;
|
|
91
|
-
const REWORK_SHARE = .2;
|
|
92
|
-
const FAILED_SHARE = .1;
|
|
93
|
-
const CONCENTRATED_SHARE = .5;
|
|
94
|
-
const LOW_CACHE_RATE = .5;
|
|
95
|
-
const MIN_CACHE_TOKENS = 1e5;
|
|
96
|
-
const percent = (share) => `${String(Math.round(share * 100))}%`;
|
|
97
|
-
const hintsOf = (totals, nodes) => {
|
|
98
|
-
const spend = totals.costUsd;
|
|
99
|
-
if (spend < MIN_SPEND_USD) return [];
|
|
100
|
-
const hints = [];
|
|
101
|
-
if (totals.reworkUsd / spend >= REWORK_SHARE) hints.push({
|
|
102
|
-
code: "rework",
|
|
103
|
-
message: `${percent(totals.reworkUsd / spend)} of the spend went to loop rounds after the first. Sharper briefs or an earlier check can stop work coming back.`
|
|
104
|
-
});
|
|
105
|
-
if (totals.failedUsd / spend >= FAILED_SHARE) hints.push({
|
|
106
|
-
code: "failed",
|
|
107
|
-
message: `${percent(totals.failedUsd / spend)} of the spend went to sessions that failed. A cheap check before them, or a lower budget per session, can stop them sooner.`
|
|
108
|
-
});
|
|
109
|
-
const top = nodes[0];
|
|
110
|
-
if (top !== void 0 && nodes.length > 1 && top.costUsd / spend >= CONCENTRATED_SHARE) hints.push({
|
|
111
|
-
code: "concentrated",
|
|
112
|
-
message: `${top.key} took ${percent(top.costUsd / spend)} of the spend. It is the first place to try a smaller model, a lower effort or a tighter brief.`
|
|
113
|
-
});
|
|
114
|
-
if (totals.inputTokens + totals.cacheReadTokens + totals.cacheWriteTokens >= MIN_CACHE_TOKENS && totals.cacheHitRate < LOW_CACHE_RATE) hints.push({
|
|
115
|
-
code: "cache",
|
|
116
|
-
message: `Only ${percent(totals.cacheHitRate)} of input tokens came from the prompt cache. Long runs of fresh sessions cost more than resumed ones.`
|
|
117
|
-
});
|
|
118
|
-
return hints;
|
|
119
|
-
};
|
|
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);
|
|
124
|
-
const groups = {
|
|
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")
|
|
133
|
-
};
|
|
134
|
-
return {
|
|
135
|
-
totals,
|
|
136
|
-
groups,
|
|
137
|
-
hints: hintsOf(totals, groups.node)
|
|
138
|
-
};
|
|
139
|
-
};
|
|
140
|
-
export { usageReport as n, USAGE_DIMENSIONS as t };
|
package/dist/send-CaZlGG5a.mjs
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { Cn as socketPath, Dt as scriptContactsOf, Tn as wakeRun, _t as liveStatus, w as openStore } from "./src-BeR6f70A.mjs";
|
|
2
|
-
import { s as writeErr } from "./output-CnRzrbUM.mjs";
|
|
3
|
-
import { u as inheritedScrubber } from "./src-D93tHZ5B.mjs";
|
|
4
|
-
import { defineCommand } from "citty";
|
|
5
|
-
// ../core/src/remote/send.ts
|
|
6
|
-
/**
|
|
7
|
-
* @file `ork send` (RFC-005): a program in a tool node writes to a session on
|
|
8
|
-
* its run's bus. The check is on the sender's side, as a refusal must be an
|
|
9
|
-
* exit code a script can act on; then a row in the store and a wake, as for a
|
|
10
|
-
* message between linked runs. `ORK_RUN` and `ORK_NODE` say who it is, and
|
|
11
|
-
* they are attribution, not proof: the check stops mistakes, not a program
|
|
12
|
-
* running as the same user.
|
|
13
|
-
*/
|
|
14
|
-
/** A first value, as for links; real use will show whether it is the right one. */
|
|
15
|
-
const DEFAULT_SCRIPT_MESSAGES = 100;
|
|
16
|
-
/**
|
|
17
|
-
* Why this sender may not write to this node; `null` when it may. The body is
|
|
18
|
-
* not needed, so a program is told before it pipes a whole message in.
|
|
19
|
-
*/
|
|
20
|
-
const scriptRefusal = (store, send) => {
|
|
21
|
-
const { run: runId, node, to } = send;
|
|
22
|
-
if (runId === void 0 || node === void 0) return "`ork send` runs inside a tool node of a run: `ORK_RUN` and `ORK_NODE` are not set.";
|
|
23
|
-
if (runId.startsWith("node-")) return `Run ${runId} runs one node, which has no message bus; run the whole workflow.`;
|
|
24
|
-
const run = store.lookup(runId).find((found) => found.id === runId);
|
|
25
|
-
if (run === void 0) return `There is no run ${runId}.`;
|
|
26
|
-
const status = liveStatus(run);
|
|
27
|
-
if (status !== "running") return `Run ${runId} is not running (${status}).`;
|
|
28
|
-
const started = store.events(runId).map((stored) => stored.event).findLast((event) => event.type === "run.started");
|
|
29
|
-
if (started === void 0) return `Run ${runId} has not started yet.`;
|
|
30
|
-
const own = started.nodes.find((found) => found.id === node);
|
|
31
|
-
if (own === void 0 || !own.type.startsWith("tool.")) return `${node} is not a tool node of run ${runId}.`;
|
|
32
|
-
const contacts = scriptContactsOf(node, started.nodes, started.channels ?? []);
|
|
33
|
-
if (contacts.includes(to)) return null;
|
|
34
|
-
return contacts.length === 0 ? `\`${node}\` has no one to write to: give it an owner, or a channel under \`messages:\`.` : `\`${to}\` is not a contact of \`${node}\`, which can write to \`${contacts.join("`, `")}\` (its owner and its channels).`;
|
|
35
|
-
};
|
|
36
|
-
/** Reading all of it would cost as much as sending it. */
|
|
37
|
-
const TOO_LONG = "The message is over 64 KiB. Write it to a file and send the file's path.";
|
|
38
|
-
/** Checks the message, writes it for the run, and wakes it. */
|
|
39
|
-
const sendFromScript = async (store, send, home) => {
|
|
40
|
-
const refused = scriptRefusal(store, send);
|
|
41
|
-
if (refused !== null) return {
|
|
42
|
-
ok: false,
|
|
43
|
-
error: refused
|
|
44
|
-
};
|
|
45
|
-
if (Buffer.byteLength(send.body) > 65536) return {
|
|
46
|
-
ok: false,
|
|
47
|
-
error: TOO_LONG
|
|
48
|
-
};
|
|
49
|
-
const run = String(send.run);
|
|
50
|
-
const node = String(send.node);
|
|
51
|
-
if (!store.scriptCommand(run, {
|
|
52
|
-
kind: "script",
|
|
53
|
-
node: send.to,
|
|
54
|
-
from: node,
|
|
55
|
-
body: send.body
|
|
56
|
-
}, DEFAULT_SCRIPT_MESSAGES)) return {
|
|
57
|
-
ok: false,
|
|
58
|
-
error: `\`${node}\` has sent ${String(DEFAULT_SCRIPT_MESSAGES)} messages in this run, which is the most it may.`
|
|
59
|
-
};
|
|
60
|
-
await wakeRun(socketPath(run, home));
|
|
61
|
-
return { ok: true };
|
|
62
|
-
};
|
|
63
|
-
// src/commands/send.ts
|
|
64
|
-
/**
|
|
65
|
-
* @file `ork send` (RFC-005): a program in a tool node writes to a session on
|
|
66
|
-
* its run's bus. It prints nothing when it works, as what a program prints is
|
|
67
|
-
* its node's output, and says why on stderr, with exit 1, when it is refused:
|
|
68
|
-
* before reading a message it could not send, and for anything wrong with the
|
|
69
|
-
* command itself, which would otherwise go to stdout as usage.
|
|
70
|
-
*/
|
|
71
|
-
const refuse = (message) => {
|
|
72
|
-
writeErr(message);
|
|
73
|
-
process.exitCode = 1;
|
|
74
|
-
};
|
|
75
|
-
/** Stdin, up to the limit: past it, the rest is not worth holding. */
|
|
76
|
-
const readBounded = async () => {
|
|
77
|
-
const chunks = [];
|
|
78
|
-
let size = 0;
|
|
79
|
-
for await (const chunk of process.stdin) {
|
|
80
|
-
const part = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
81
|
-
size += part.length;
|
|
82
|
-
if (size > 65536) return null;
|
|
83
|
-
chunks.push(part);
|
|
84
|
-
}
|
|
85
|
-
return Buffer.concat(chunks);
|
|
86
|
-
};
|
|
87
|
-
/** The message, or why it can't go. */
|
|
88
|
-
const bodyOf = async (given) => {
|
|
89
|
-
if (given !== void 0) return { body: given };
|
|
90
|
-
const read = await readBounded();
|
|
91
|
-
if (read === null) return { refused: TOO_LONG };
|
|
92
|
-
try {
|
|
93
|
-
return { body: new TextDecoder("utf-8", { fatal: true }).decode(read) };
|
|
94
|
-
} catch {
|
|
95
|
-
return { refused: "The message is not UTF-8 text." };
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
const deliver = async (store, to, given) => {
|
|
99
|
-
const sender = {
|
|
100
|
-
run: process.env["ORK_RUN"],
|
|
101
|
-
node: process.env["ORK_NODE"],
|
|
102
|
-
to
|
|
103
|
-
};
|
|
104
|
-
const refused = scriptRefusal(store, sender);
|
|
105
|
-
if (refused !== null) {
|
|
106
|
-
refuse(refused);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
const message = await bodyOf(given);
|
|
110
|
-
if ("refused" in message) {
|
|
111
|
-
refuse(message.refused);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
if (message.body.replaceAll(/[\p{Cc}\s]/gu, "") === "") {
|
|
115
|
-
refuse("The message is empty.");
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const sent = await sendFromScript(store, {
|
|
119
|
-
...sender,
|
|
120
|
-
body: inheritedScrubber(process.env)(message.body)
|
|
121
|
-
});
|
|
122
|
-
if (!sent.ok) refuse(sent.error);
|
|
123
|
-
};
|
|
124
|
-
const send = defineCommand({
|
|
125
|
-
meta: {
|
|
126
|
-
name: "send",
|
|
127
|
-
description: "From a tool node's program: write to a session in its run"
|
|
128
|
-
},
|
|
129
|
-
args: {
|
|
130
|
-
to: {
|
|
131
|
-
type: "positional",
|
|
132
|
-
description: "The node to write to: the owner, or a channel",
|
|
133
|
-
required: false
|
|
134
|
-
},
|
|
135
|
-
message: {
|
|
136
|
-
type: "string",
|
|
137
|
-
alias: "m",
|
|
138
|
-
description: "The message"
|
|
139
|
-
},
|
|
140
|
-
stdin: {
|
|
141
|
-
type: "boolean",
|
|
142
|
-
description: "Read the message from stdin",
|
|
143
|
-
default: false
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
run: async ({ args }) => {
|
|
147
|
-
if (args.to === void 0 || args._.length !== 1) {
|
|
148
|
-
refuse("Give one node to write to: ork send <to> -m <body>.");
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
if (args.message === void 0 === !args.stdin) {
|
|
152
|
-
refuse("Give the message with -m or --stdin, one of them.");
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
const store = openStore();
|
|
156
|
-
try {
|
|
157
|
-
await deliver(store, args.to, args.message);
|
|
158
|
-
} finally {
|
|
159
|
-
store.close();
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
export { send };
|