@krapht/ork 0.2.0 → 0.4.0
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/THIRD_PARTY_NOTICES.md +7 -0
- package/dist/__build_info__-CtSVVuZE.mjs +691 -0
- package/dist/bin.mjs +1654 -3532
- package/dist/clean-Dcr807be.mjs +88 -0
- package/dist/context-C_bksb0C.mjs +351 -0
- package/dist/find-DptmbhGd.mjs +47 -0
- package/dist/format-J-a21Yvn.mjs +393 -0
- package/dist/in-a-run-D1Qc5Mit.mjs +22 -0
- package/dist/limits-BDZCmGEu.mjs +44 -0
- package/dist/list-ZvY2OcLq.mjs +110 -0
- package/dist/mcp-CHVKKg3B.mjs +91 -0
- package/dist/output-CXSJO00X.mjs +900 -0
- package/dist/pause-BPSmg4yz.mjs +65 -0
- package/dist/relay-Do-aNbSD.mjs +122 -0
- package/dist/remove-AwaMMaHf.mjs +194 -0
- package/dist/report-DK4LGqwF.mjs +129 -0
- package/dist/send-DhGBkowy.mjs +163 -0
- package/dist/{src-DAsOYtoR.mjs → src-BHnxpJyH.mjs} +496 -193
- package/dist/src-CGqMati3.mjs +12505 -0
- package/dist/src-CpDKIulI.mjs +3118 -0
- package/dist/src-UWkk1sl9.mjs +1899 -0
- package/dist/ui/THIRD_PARTY_LICENSES.txt +631 -0
- package/dist/ui/assets/app-BgVDC_5l.js +67 -0
- package/dist/ui/assets/app-DVf68fj5.css +1 -0
- package/dist/ui/assets/geist-mono-cyrillic-400-normal-C51Di1Mf.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-400-normal-CPJFfJgk.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-500-normal-D2cXvDHF.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-500-normal-DfuvdVgn.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-400-normal-CkxHOdCb.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-400-normal-XB2BPmDK.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-500-normal-BY1KTvO5.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-500-normal-C7aYOB1D.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-400-normal-B2yvC1Cq.woff +0 -0
- package/dist/ui/assets/geist-mono-latin-400-normal-DTRLJnHl.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-500-normal-D-GG86Jb.woff +0 -0
- package/dist/ui/assets/geist-mono-latin-500-normal-YINYabwD.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-400-normal-CfzLURNc.woff +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-400-normal-DI-rJ0UV.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-500-normal-Buglb9-a.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-500-normal-CrWEgvU_.woff +0 -0
- package/dist/ui/assets/geist-mono-symbols2-400-normal-BS9BGVxz.woff2 +0 -0
- package/dist/ui/assets/geist-mono-symbols2-400-normal-DjbiTEEA.woff +0 -0
- package/dist/ui/assets/geist-mono-symbols2-500-normal-CmnCPGa0.woff +0 -0
- package/dist/ui/assets/geist-mono-symbols2-500-normal-Q_ksOahU.woff2 +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-400-normal-CKEaXIlx.woff2 +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-400-normal-CZEPLOgu.woff +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-500-normal-Bs29RrUZ.woff2 +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-500-normal-Dx9-epDo.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-400-normal-BOaIZNA2.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-400-normal-gapTbOY8.woff2 +0 -0
- package/dist/ui/assets/geist-sans-latin-500-normal-CN2lyvyL.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-500-normal-uokXdC-Q.woff2 +0 -0
- package/dist/ui/assets/geist-sans-latin-600-normal-CA1yjETN.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-600-normal-DFOURf8L.woff2 +0 -0
- package/dist/ui/assets/index-BiO54L90.css +1 -0
- package/dist/ui/assets/index-MQNNgsyT.js +10 -0
- package/dist/ui/assets/theme-3xH5jxKs.js +1 -0
- package/dist/ui/favicon.ico +0 -0
- package/dist/ui/favicon.svg +4 -0
- package/dist/ui/index.html +30 -0
- package/dist/ui/ork-wordmark-small-on-light.svg +4 -0
- package/dist/ui/ork-wordmark-small.svg +4 -0
- package/dist/ui-DhutUMgQ.mjs +472 -0
- package/dist/usage-CT7Z-yjr.mjs +419 -0
- package/dist/watch-0Qr2UfCQ.mjs +394 -0
- package/dist/worktrees-DHl80k96.mjs +212 -0
- package/package.json +6 -4
- package/dist/src-CJ_zF0Zj.mjs +0 -5433
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { c as usd, fn as placeFor, lt as liveStatus, mn as runDirOf } from "./src-CGqMati3.mjs";
|
|
2
|
+
import { a as write, c as openStore, i as style, n as cost, o as writeErr, t as confirm } from "./output-CXSJO00X.mjs";
|
|
3
|
+
import { n as listOrkWorktrees, t as createdWorktrees } from "./list-ZvY2OcLq.mjs";
|
|
4
|
+
import { n as usageReport, t as USAGE_DIMENSIONS } from "./report-DK4LGqwF.mjs";
|
|
5
|
+
import "./format-J-a21Yvn.mjs";
|
|
6
|
+
import { n as limitsHere } from "./limits-BDZCmGEu.mjs";
|
|
7
|
+
import { defineCommand } from "citty";
|
|
8
|
+
import { existsSync, rmSync } from "node:fs";
|
|
9
|
+
// ../core/src/store/prune.ts
|
|
10
|
+
/**
|
|
11
|
+
* @file `ork prune` (ADR-046): which runs may lose their transcript, and doing
|
|
12
|
+
* it. A run goes with the runs it called or was called by, or not at all, and
|
|
13
|
+
* not while it is live, is linked, or has a worktree or branch left: those
|
|
14
|
+
* read the run's events. The usage ledger and feedback records stay.
|
|
15
|
+
*/
|
|
16
|
+
const messageOf = (error) => error instanceof Error ? error.message : String(error);
|
|
17
|
+
/**
|
|
18
|
+
* The runs in a project that still have a worktree or branch. With its folder
|
|
19
|
+
* gone, or no longer a repository, there is nothing left to clean up; any other
|
|
20
|
+
* failure says nothing about them, so it rejects.
|
|
21
|
+
*/
|
|
22
|
+
const runsWithWorktrees = (store) => async (root) => {
|
|
23
|
+
if (!existsSync(root)) return /* @__PURE__ */ new Set();
|
|
24
|
+
try {
|
|
25
|
+
const found = await listOrkWorktrees(root, createdWorktrees(store, root));
|
|
26
|
+
return new Set(found.map((w) => w.runId));
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (/not a git repository/iu.test(messageOf(error))) return /* @__PURE__ */ new Set();
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
/** The run at the top of a run's chain of calls. */
|
|
33
|
+
const callRootOf = (run, byId) => {
|
|
34
|
+
let top = run;
|
|
35
|
+
const seen = /* @__PURE__ */ new Set([run.id]);
|
|
36
|
+
while (top.parent !== void 0) {
|
|
37
|
+
const parent = byId.get(top.parent);
|
|
38
|
+
if (parent === void 0 || seen.has(parent.id)) break;
|
|
39
|
+
seen.add(parent.id);
|
|
40
|
+
top = parent;
|
|
41
|
+
}
|
|
42
|
+
return top.id;
|
|
43
|
+
};
|
|
44
|
+
/** Sorts the runs before the cutoff into those to remove and those to keep. */
|
|
45
|
+
const planPrune = async (store, filter, worktreesOf = runsWithWorktrees(store)) => {
|
|
46
|
+
const all = store.runs();
|
|
47
|
+
const byId = new Map(all.map((run) => [run.id, run]));
|
|
48
|
+
const inScope = (run) => filter.project === void 0 || run.project === filter.project;
|
|
49
|
+
const old = (run) => (store.lastEventAt(run.id) ?? run.finishedAt ?? run.startedAt) < filter.before;
|
|
50
|
+
const trees = /* @__PURE__ */ new Map();
|
|
51
|
+
for (const run of all) {
|
|
52
|
+
const root = callRootOf(run, byId);
|
|
53
|
+
trees.set(root, [...trees.get(root) ?? [], run]);
|
|
54
|
+
}
|
|
55
|
+
const projects = [...new Set(all.filter((r) => old(r) && inScope(r)).map((r) => r.project))];
|
|
56
|
+
const withWorktrees = new Map(await Promise.all(projects.map(async (project) => {
|
|
57
|
+
try {
|
|
58
|
+
return [project, await worktreesOf(project)];
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return [project, messageOf(error)];
|
|
61
|
+
}
|
|
62
|
+
})));
|
|
63
|
+
/** Why this run itself cannot go, or `null` when it can. */
|
|
64
|
+
const blocker = (run) => {
|
|
65
|
+
if (liveStatus(run) === "running") return "it is still running";
|
|
66
|
+
const worktrees = withWorktrees.get(run.project);
|
|
67
|
+
if (typeof worktrees === "string") return `cannot tell whether it has a worktree left: ${worktrees}`;
|
|
68
|
+
if (worktrees?.has(run.id) === true) return "it has a worktree or branch left (ork worktrees clean)";
|
|
69
|
+
if (store.links({
|
|
70
|
+
run: run.id,
|
|
71
|
+
live: true
|
|
72
|
+
}).length > 0) return "it has a live link";
|
|
73
|
+
return null;
|
|
74
|
+
};
|
|
75
|
+
const plan = {
|
|
76
|
+
remove: [],
|
|
77
|
+
keep: []
|
|
78
|
+
};
|
|
79
|
+
for (const tree of trees.values()) {
|
|
80
|
+
const held = tree.map((run) => {
|
|
81
|
+
if (!inScope(run)) return `run ${run.id} of its call is in another project (--all)`;
|
|
82
|
+
if (!old(run)) return `run ${run.id} of its call is newer`;
|
|
83
|
+
const why = blocker(run);
|
|
84
|
+
return why === null ? null : `run ${run.id} of its call stays: ${why}`;
|
|
85
|
+
}).find((why) => why !== null);
|
|
86
|
+
for (const run of tree) {
|
|
87
|
+
if (!inScope(run) || !old(run)) continue;
|
|
88
|
+
if (held === void 0) plan.remove.push(run);
|
|
89
|
+
else plan.keep.push({
|
|
90
|
+
run,
|
|
91
|
+
reason: blocker(run) ?? held
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return plan;
|
|
96
|
+
};
|
|
97
|
+
const SAFE_ID = /^[\w-]+$/u;
|
|
98
|
+
/** Forgets the plan's runs and removes their folders. */
|
|
99
|
+
const applyPrune = (store, plan) => {
|
|
100
|
+
const outcome = {
|
|
101
|
+
removed: 0,
|
|
102
|
+
folders: 0,
|
|
103
|
+
failed: []
|
|
104
|
+
};
|
|
105
|
+
const places = store.projects();
|
|
106
|
+
for (const run of plan.remove) {
|
|
107
|
+
if (!store.deleteRun(run.id)) {
|
|
108
|
+
outcome.failed.push({
|
|
109
|
+
run: run.id,
|
|
110
|
+
reason: "it is running again"
|
|
111
|
+
});
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
outcome.removed += 1;
|
|
115
|
+
if (!SAFE_ID.test(run.id)) continue;
|
|
116
|
+
const place = places.find((p) => p.root === run.project) ?? placeFor(run.project, places);
|
|
117
|
+
const folder = runDirOf(place, run.id);
|
|
118
|
+
if (!existsSync(folder)) continue;
|
|
119
|
+
try {
|
|
120
|
+
rmSync(folder, {
|
|
121
|
+
recursive: true,
|
|
122
|
+
force: true
|
|
123
|
+
});
|
|
124
|
+
outcome.folders += 1;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
outcome.failed.push({
|
|
127
|
+
run: run.id,
|
|
128
|
+
reason: `its folder stays, at ${folder}: ${messageOf(error)}`
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return outcome;
|
|
133
|
+
};
|
|
134
|
+
// src/runs/usage.ts
|
|
135
|
+
/**
|
|
136
|
+
* @file How usage reads on the terminal (`ork usage`, specs/usage.md), and the
|
|
137
|
+
* times its `--since`, `--until` and `ork prune --older-than` take.
|
|
138
|
+
*/
|
|
139
|
+
const UNITS = {
|
|
140
|
+
h: 36e5,
|
|
141
|
+
d: 864e5,
|
|
142
|
+
w: 6048e5
|
|
143
|
+
};
|
|
144
|
+
/** `30d`, `12w`, `24h` before `now`, as an ISO time; `null` when it is not one. */
|
|
145
|
+
const agoTime = (text, now) => {
|
|
146
|
+
const match = /^(\d+)([hdw])$/u.exec(text);
|
|
147
|
+
if (match === null) return null;
|
|
148
|
+
const [, amount = "0", unit = "d"] = match;
|
|
149
|
+
if (Number(amount) === 0) return null;
|
|
150
|
+
const date = new Date(now - Number(amount) * (UNITS[unit] ?? 0));
|
|
151
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
152
|
+
};
|
|
153
|
+
/** A `YYYY-MM-DD` day's start in the machine's time zone, `after` days on. */
|
|
154
|
+
const dayTime = (text, after) => {
|
|
155
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/u.exec(text);
|
|
156
|
+
if (match === null) return null;
|
|
157
|
+
const [, year = "", month = "", day = ""] = match;
|
|
158
|
+
const start = new Date(Number(year), Number(month) - 1, Number(day));
|
|
159
|
+
if (start.getMonth() !== Number(month) - 1) return null;
|
|
160
|
+
if (start.getDate() !== Number(day)) return null;
|
|
161
|
+
start.setDate(start.getDate() + after);
|
|
162
|
+
return start.toISOString();
|
|
163
|
+
};
|
|
164
|
+
/** The start of the day `days` whole days ago, counting today as one. */
|
|
165
|
+
const wholeDays = (days, now) => {
|
|
166
|
+
const today = new Date(now);
|
|
167
|
+
return new Date(today.getFullYear(), today.getMonth(), today.getDate() - (days - 1)).toISOString();
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* `--since`: an age or the day it starts from. Days and weeks are whole days,
|
|
171
|
+
* today among them, as the Usage panel counts them; hours are exact.
|
|
172
|
+
*/
|
|
173
|
+
const sinceTime = (text, now) => {
|
|
174
|
+
const ago = agoTime(text, now);
|
|
175
|
+
if (ago === null) return dayTime(text, 0);
|
|
176
|
+
if (text.endsWith("h")) return ago;
|
|
177
|
+
const amount = Number(text.slice(0, -1));
|
|
178
|
+
return wholeDays(text.endsWith("w") ? amount * 7 : amount, now);
|
|
179
|
+
};
|
|
180
|
+
/** `--until`: the last day it includes. */
|
|
181
|
+
const untilTime = (text) => dayTime(text, 1);
|
|
182
|
+
/** `950`, `12.3k`, `4.5M`. */
|
|
183
|
+
const tokens = (count) => {
|
|
184
|
+
if (count < 1e3) return String(count);
|
|
185
|
+
if (count < 1e6) return `${(count / 1e3).toFixed(1)}k`;
|
|
186
|
+
return `${(count / 1e6).toFixed(1)}M`;
|
|
187
|
+
};
|
|
188
|
+
const percent = (share) => `${String(Math.round(share * 100))}%`;
|
|
189
|
+
const counted = (count, noun) => `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
|
|
190
|
+
const TITLES = {
|
|
191
|
+
day: "By day",
|
|
192
|
+
week: "By week",
|
|
193
|
+
month: "By month",
|
|
194
|
+
workflow: "By workflow",
|
|
195
|
+
node: "Costliest nodes",
|
|
196
|
+
model: "By model",
|
|
197
|
+
driver: "By driver",
|
|
198
|
+
project: "By project"
|
|
199
|
+
};
|
|
200
|
+
const table = (groups) => {
|
|
201
|
+
const rows = groups.map((group) => ({
|
|
202
|
+
key: group.key,
|
|
203
|
+
cost: cost(group.costUsd),
|
|
204
|
+
sessions: String(group.sessions),
|
|
205
|
+
tokens: `${tokens(group.inputTokens + group.cacheReadTokens + group.cacheWriteTokens)} in / ${tokens(group.outputTokens)} out`,
|
|
206
|
+
cache: percent(group.cacheHitRate)
|
|
207
|
+
}));
|
|
208
|
+
const width = (pick, title) => Math.max(title.length, ...rows.map((row) => pick(row).length));
|
|
209
|
+
const w = {
|
|
210
|
+
key: width((r) => r.key, ""),
|
|
211
|
+
cost: width((r) => r.cost, "COST"),
|
|
212
|
+
sessions: width((r) => r.sessions, "SESSIONS"),
|
|
213
|
+
tokens: width((r) => r.tokens, "TOKENS")
|
|
214
|
+
};
|
|
215
|
+
const line = (key, c, s, t, h) => ` ${key.padEnd(w.key)} ${c.padStart(w.cost)} ${s.padStart(w.sessions)} ${t.padEnd(w.tokens)} ${h}`;
|
|
216
|
+
return [style("dim", line("", "COST", "SESSIONS", "TOKENS", "CACHE")), ...rows.map((r) => line(r.key, r.cost, r.sessions, r.tokens, r.cache))];
|
|
217
|
+
};
|
|
218
|
+
/** What `ork usage` prints without `--by`. */
|
|
219
|
+
const OVERVIEW = [
|
|
220
|
+
"day",
|
|
221
|
+
"workflow",
|
|
222
|
+
"model",
|
|
223
|
+
"node"
|
|
224
|
+
];
|
|
225
|
+
/** How many rows the overview shows of each; `--by` shows them all. */
|
|
226
|
+
const OVERVIEW_ROWS = 10;
|
|
227
|
+
const shortTime = (iso) => new Date(iso).toLocaleString(void 0, {
|
|
228
|
+
dateStyle: "medium",
|
|
229
|
+
timeStyle: "short"
|
|
230
|
+
});
|
|
231
|
+
/** The limits across runs, above a report; nothing with none set. */
|
|
232
|
+
const formatLimits = ({ problems, caps, plan }) => {
|
|
233
|
+
if (problems.length === 0 && caps.length === 0 && plan.length === 0) return [];
|
|
234
|
+
return [
|
|
235
|
+
style("bold", "Limits"),
|
|
236
|
+
...problems.map((problem) => style("yellow", ` ${problem}`)),
|
|
237
|
+
...caps.map((cap) => {
|
|
238
|
+
const line = ` ${cap.project === void 0 ? "all projects" : "this project"}, this ${cap.window}: ${usd(cap.spentUsd)} of ${usd(cap.capUsd)}, resets ${shortTime(cap.resetsAt)}`;
|
|
239
|
+
return cap.leftUsd <= 0 ? style("yellow", line) : line;
|
|
240
|
+
}),
|
|
241
|
+
...plan.map((limit) => style("yellow", ` Claude plan's ${limit.window.replaceAll("_", " ")} limit ${limit.status === "rejected" ? "was reached" : "is close"}${limit.resetsAt === void 0 ? "" : `, resets ${shortTime(limit.resetsAt)}`}`)),
|
|
242
|
+
""
|
|
243
|
+
];
|
|
244
|
+
};
|
|
245
|
+
/** The report, as the terminal shows it. */
|
|
246
|
+
const formatUsage = (report, scope, by) => {
|
|
247
|
+
const { totals } = report;
|
|
248
|
+
if (totals.sessions === 0) return [`No Claude sessions in ${scope}.`];
|
|
249
|
+
const lines = [`${style("bold", cost(totals.costUsd))} in ${scope}`, `${counted(totals.runs, "run")} · ${counted(totals.sessions, "session")} · ${counted(totals.turns, "turn")} · ${tokens(totals.inputTokens + totals.cacheReadTokens + totals.cacheWriteTokens)} in / ${tokens(totals.outputTokens)} out · ${percent(totals.cacheHitRate)} from cache`];
|
|
250
|
+
for (const dimension of by === void 0 ? OVERVIEW : [by]) {
|
|
251
|
+
const groups = report.groups[dimension];
|
|
252
|
+
const shown = by === void 0 ? dimension === "day" ? groups.slice(-10) : groups.slice(0, OVERVIEW_ROWS) : groups;
|
|
253
|
+
lines.push("", style("bold", TITLES[dimension]), ...table(shown));
|
|
254
|
+
if (shown.length < groups.length) lines.push(style("dim", ` and ${String(groups.length - shown.length)} more (--by ${dimension})`));
|
|
255
|
+
}
|
|
256
|
+
const hints = report.hints.length === 0 ? [] : [
|
|
257
|
+
"",
|
|
258
|
+
style("bold", "Worth a look"),
|
|
259
|
+
...report.hints.map((h) => ` ${h.message}`)
|
|
260
|
+
];
|
|
261
|
+
return [
|
|
262
|
+
...lines,
|
|
263
|
+
...hints,
|
|
264
|
+
"",
|
|
265
|
+
style("dim", "Costs are Claude Code's own estimates. On a subscription they are not what you are billed.")
|
|
266
|
+
];
|
|
267
|
+
};
|
|
268
|
+
// src/commands/usage.ts
|
|
269
|
+
/**
|
|
270
|
+
* @file `ork usage` and `ork prune` (specs/usage.md): what runs spent over
|
|
271
|
+
* time, from the store's ledger, and forgetting old runs' transcripts while
|
|
272
|
+
* keeping that ledger.
|
|
273
|
+
*/
|
|
274
|
+
const isDimension = (value) => USAGE_DIMENSIONS.some((dimension) => dimension === value);
|
|
275
|
+
const refuse = (message) => {
|
|
276
|
+
writeErr(message);
|
|
277
|
+
process.exitCode = 1;
|
|
278
|
+
};
|
|
279
|
+
const usage = defineCommand({
|
|
280
|
+
meta: {
|
|
281
|
+
name: "usage",
|
|
282
|
+
description: "Show what this project's runs spent over time"
|
|
283
|
+
},
|
|
284
|
+
args: {
|
|
285
|
+
all: {
|
|
286
|
+
type: "boolean",
|
|
287
|
+
description: "Every project's runs",
|
|
288
|
+
default: false
|
|
289
|
+
},
|
|
290
|
+
"clear-plan-limits": {
|
|
291
|
+
type: "boolean",
|
|
292
|
+
description: "Forget what Claude Code last said of your plan's limits, for one that has lifted",
|
|
293
|
+
default: false
|
|
294
|
+
},
|
|
295
|
+
since: {
|
|
296
|
+
type: "string",
|
|
297
|
+
description: "Whole days counting today (30d, 12w), exact hours (24h), a day (2026-11-01), or all",
|
|
298
|
+
default: "30d"
|
|
299
|
+
},
|
|
300
|
+
until: {
|
|
301
|
+
type: "string",
|
|
302
|
+
description: "The last day to include"
|
|
303
|
+
},
|
|
304
|
+
by: {
|
|
305
|
+
type: "string",
|
|
306
|
+
description: USAGE_DIMENSIONS.join(", ")
|
|
307
|
+
},
|
|
308
|
+
workflow: {
|
|
309
|
+
type: "string",
|
|
310
|
+
description: "Only this workflow"
|
|
311
|
+
},
|
|
312
|
+
json: {
|
|
313
|
+
type: "boolean",
|
|
314
|
+
description: "Print JSON",
|
|
315
|
+
default: false
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
run: ({ args }) => {
|
|
319
|
+
const since = args.since === "all" ? void 0 : sinceTime(args.since, Date.now());
|
|
320
|
+
const until = args.until === void 0 ? void 0 : untilTime(args.until);
|
|
321
|
+
if (since === null) {
|
|
322
|
+
refuse(`--since takes an age (30d), a day (2026-11-01) or all, not \`${args.since}\`.`);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (until === null) {
|
|
326
|
+
refuse(`--until takes a day (2026-11-01), not \`${String(args.until)}\`.`);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (args.by !== void 0 && !isDimension(args.by)) {
|
|
330
|
+
refuse(`--by takes one of ${USAGE_DIMENSIONS.join(", ")}, not \`${args.by}\`.`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const store = openStore();
|
|
334
|
+
try {
|
|
335
|
+
if (args["clear-plan-limits"]) store.clearPlanLimits();
|
|
336
|
+
const report = usageReport(store.usage({
|
|
337
|
+
...args.all ? {} : { project: placeFor(process.cwd(), store.projects()).root },
|
|
338
|
+
...args.workflow === void 0 ? {} : { workflow: args.workflow },
|
|
339
|
+
...since === void 0 ? {} : { since },
|
|
340
|
+
...until === void 0 ? {} : { until }
|
|
341
|
+
}));
|
|
342
|
+
const limits = limitsHere(store, args.all ? void 0 : placeFor(process.cwd(), store.projects()));
|
|
343
|
+
if (args.json) {
|
|
344
|
+
write(JSON.stringify({
|
|
345
|
+
...report,
|
|
346
|
+
limits
|
|
347
|
+
}, void 0, 2));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
for (const line of formatLimits(limits)) write(line);
|
|
351
|
+
const place = args.all ? "every project" : "this project";
|
|
352
|
+
const period = args.since === "all" ? "" : `, since ${args.since}`;
|
|
353
|
+
for (const line of formatUsage(report, `${place}${period}`, args.by)) write(line);
|
|
354
|
+
} finally {
|
|
355
|
+
store.close();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
const prune = defineCommand({
|
|
360
|
+
meta: {
|
|
361
|
+
name: "prune",
|
|
362
|
+
description: "Forget old runs' transcripts; what they spent stays"
|
|
363
|
+
},
|
|
364
|
+
args: {
|
|
365
|
+
"older-than": {
|
|
366
|
+
type: "string",
|
|
367
|
+
description: "Runs that ended this long ago: 90d, 12w",
|
|
368
|
+
required: true
|
|
369
|
+
},
|
|
370
|
+
all: {
|
|
371
|
+
type: "boolean",
|
|
372
|
+
description: "Every project's runs",
|
|
373
|
+
default: false
|
|
374
|
+
},
|
|
375
|
+
"dry-run": {
|
|
376
|
+
type: "boolean",
|
|
377
|
+
description: "Show what would go, and change nothing",
|
|
378
|
+
default: false
|
|
379
|
+
},
|
|
380
|
+
yes: {
|
|
381
|
+
type: "boolean",
|
|
382
|
+
alias: "y",
|
|
383
|
+
description: "Do not ask",
|
|
384
|
+
default: false
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
run: async ({ args }) => {
|
|
388
|
+
const before = agoTime(args["older-than"], Date.now());
|
|
389
|
+
if (before === null) {
|
|
390
|
+
refuse(`--older-than takes an age (90d, 12w), not \`${args["older-than"]}\`.`);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const store = openStore();
|
|
394
|
+
try {
|
|
395
|
+
const filter = {
|
|
396
|
+
before,
|
|
397
|
+
...args.all ? {} : { project: placeFor(process.cwd(), store.projects()).root }
|
|
398
|
+
};
|
|
399
|
+
const plan = await planPrune(store, filter);
|
|
400
|
+
for (const { run, reason } of plan.keep) write(style("dim", `keep ${run.id} (${run.workflow}): ${reason}`));
|
|
401
|
+
for (const run of plan.remove) write(`${args["dry-run"] ? "would remove" : "remove"} ${run.id} (${run.workflow}, ${run.startedAt.slice(0, 10)})`);
|
|
402
|
+
if (plan.remove.length === 0) {
|
|
403
|
+
write("Nothing to prune.");
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (args["dry-run"]) return;
|
|
407
|
+
const question = `Forget ${String(plan.remove.length)} run(s) and their logs for good? They can no longer be resumed or read. [y/N] `;
|
|
408
|
+
if (!args.yes && !await confirm(question)) return;
|
|
409
|
+
const agreed = new Set(plan.remove.map((run) => run.id));
|
|
410
|
+
const outcome = applyPrune(store, { remove: (args.yes ? plan : await planPrune(store, filter)).remove.filter((run) => agreed.has(run.id)) });
|
|
411
|
+
const notShrunk = store.compact();
|
|
412
|
+
write(`Removed ${String(outcome.removed)} run(s) and ${String(outcome.folders)} folder(s). Their usage stays in \`ork usage\`.${notShrunk === null ? "" : ` The store's file did not shrink: ${notShrunk}. The next prune tries again, and its free space is used first.`}`);
|
|
413
|
+
for (const { run, reason } of outcome.failed) refuse(`Run ${run}: ${reason}.`);
|
|
414
|
+
} finally {
|
|
415
|
+
store.close();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
export { prune, usage };
|