@krapht/ork 0.3.0 → 1.0.0-alpha.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/README.md +16 -12
- package/dist/__build_info__-DdTVd2p5.mjs +615 -0
- package/dist/bin.mjs +1572 -909
- package/dist/clean-NnboOuz6.mjs +88 -0
- package/dist/completion-snnYGjgK.mjs +405 -0
- package/dist/{context-BFGC_C02.mjs → context-DO3sBy9v.mjs} +30 -191
- package/dist/decide-B81wCZQg.mjs +113 -0
- package/dist/{find-BcvhT6UN.mjs → find-BKW-uGey.mjs} +1 -1
- package/dist/{format-DqpObyMl.mjs → format-CmTlTOjT.mjs} +72 -198
- package/dist/help-groups-BwYoAWsh.mjs +354 -0
- package/dist/{in-a-run-BbLhZOFW.mjs → in-a-run-BPXX09Bs.mjs} +1 -1
- package/dist/{limits-BKYAmqCR.mjs → limits-JCve2QpI.mjs} +1 -1
- package/dist/list-C7UEgmtd.mjs +110 -0
- package/dist/{mcp-BWsdQyri.mjs → mcp-CKPyNaKD.mjs} +7 -7
- package/dist/{output-Q2EdeVUt.mjs → output-B1V6ZJSx.mjs} +130 -36
- package/dist/{pause-DKACDfV1.mjs → pause-B_SL_yDz.mjs} +4 -4
- package/dist/{relay-CbK-Wme9.mjs → relay-o-Luee1c.mjs} +2 -2
- package/dist/remove-DAZ7ZCzt.mjs +194 -0
- package/dist/{send-C6yr04Fe.mjs → send-3B64fBNB.mjs} +3 -3
- package/dist/{src-D5ruZFgD.mjs → src-BJTegEzY.mjs} +1212 -57
- package/dist/{src-DtN1k-B3.mjs → src-C38BPzit.mjs} +251 -18
- package/dist/{src-Cz1yOUTg.mjs → src-DHvWU4tX.mjs} +2742 -1659
- package/dist/{src-CZOVF2yU.mjs → src-Dyz_Gy1q.mjs} +35 -8
- package/dist/ui/THIRD_PARTY_LICENSES.txt +2 -2
- package/dist/ui/assets/app-C8nJ6NpT.js +67 -0
- package/dist/ui/assets/app-CH7YC3in.css +1 -0
- package/dist/ui/assets/{index-BiO54L90.css → index-BPIbqRRb.css} +1 -1
- package/dist/ui/assets/{index-BoG6O-Lp.js → index-D83F2OKZ.js} +2 -2
- package/dist/ui/index.html +2 -3
- package/dist/ui-CY4xxJUu.mjs +472 -0
- package/dist/{usage-BIjf9Xdt.mjs → usage-CJnytoe4.mjs} +6 -5
- package/dist/{watch-B8o4Szd_.mjs → watch-BJB82ZJ7.mjs} +7 -7
- package/dist/workflows-CiM7S3l0.mjs +88 -0
- package/dist/worktrees-86ySXxR6.mjs +212 -0
- package/package.json +6 -6
- package/dist/__build_info__-CXXMLvjt.mjs +0 -104
- package/dist/ui/assets/app-BlDdP396.js +0 -61
- package/dist/ui/assets/app-C722xNZ3.css +0 -1
- package/dist/ui-CZS1XKla.mjs +0 -160
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createInterface } from "node:readline/promises";
|
|
1
|
+
import { $ as text, Bt as FEEDBACK_TARGETS, Q as inTransaction, Rt as FEEDBACK_KINDS, X as usageQueries, Xt as maskReplacer, Y as backfillUsage, Z as eventOf, Zt as maskSecrets, gn as PROJECT_MODES, yt as ownProcessStart, zt as FEEDBACK_SEVERITIES } from "./src-DHvWU4tX.mjs";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { mkdirSync } from "node:fs";
|
|
5
4
|
import { homedir } from "node:os";
|
|
6
5
|
import { DatabaseSync } from "node:sqlite";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
7
7
|
import { stripVTControlCharacters, styleText } from "node:util";
|
|
8
8
|
// ../core/src/store/drafts.ts
|
|
9
9
|
const rowOf = (row) => {
|
|
@@ -49,6 +49,11 @@ const choicesOf = (json) => {
|
|
|
49
49
|
return [choice];
|
|
50
50
|
});
|
|
51
51
|
};
|
|
52
|
+
/** A note as stored: masked, or null when blank. */
|
|
53
|
+
const noteOf = (note) => {
|
|
54
|
+
const trimmed = note?.trim() ?? "";
|
|
55
|
+
return trimmed === "" ? null : maskSecrets(trimmed);
|
|
56
|
+
};
|
|
52
57
|
const kindOf = (value) => value === "permission" ? "permission" : "question";
|
|
53
58
|
const statusOf$1 = (value) => value === "answered" || value === "withdrawn" ? value : "open";
|
|
54
59
|
const decisionOf = (row) => {
|
|
@@ -66,17 +71,19 @@ const decisionOf = (row) => {
|
|
|
66
71
|
recommended: text(row, "recommended"),
|
|
67
72
|
context: text(row, "context"),
|
|
68
73
|
answer: text(row, "answer"),
|
|
69
|
-
answeredAt: text(row, "answered_at")
|
|
74
|
+
answeredAt: text(row, "answered_at"),
|
|
75
|
+
note: text(row, "note"),
|
|
76
|
+
owner: text(row, "owner")
|
|
70
77
|
};
|
|
71
78
|
for (const [key, value] of Object.entries(optional)) if (value !== "") Object.assign(record, { [key]: value });
|
|
72
79
|
const by = text(row, "answered_by");
|
|
73
|
-
if (by === "terminal" || by === "decide" || by === "ui") record.answeredBy = by;
|
|
80
|
+
if (by === "terminal" || by === "decide" || by === "ui" || by === "owner") record.answeredBy = by;
|
|
74
81
|
return record;
|
|
75
82
|
};
|
|
76
83
|
const decisionQueries = (db) => {
|
|
77
|
-
const insert = db.prepare(`INSERT INTO decisions (id, run_id, node, kind, question, options, recommended, context, opened_at, status)
|
|
78
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'open')`);
|
|
79
|
-
const answer = db.prepare(`UPDATE decisions SET status = 'answered', answer = ?, answered_by = ?, answered_at = ?
|
|
84
|
+
const insert = db.prepare(`INSERT INTO decisions (id, run_id, node, kind, question, options, recommended, context, owner, opened_at, status)
|
|
85
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'open')`);
|
|
86
|
+
const answer = db.prepare(`UPDATE decisions SET status = 'answered', answer = ?, answered_by = ?, answered_at = ?, note = ?
|
|
80
87
|
WHERE id = ? AND status = 'open'`);
|
|
81
88
|
const withdraw = db.prepare("UPDATE decisions SET status = 'withdrawn', answered_at = ? WHERE id = ? AND status = 'open'");
|
|
82
89
|
const byId = db.prepare("SELECT * FROM decisions WHERE id = ?");
|
|
@@ -97,14 +104,14 @@ const decisionQueries = (db) => {
|
|
|
97
104
|
question: maskSecrets(decision.question),
|
|
98
105
|
...decision.context === void 0 ? {} : { context: maskSecrets(decision.context) }
|
|
99
106
|
};
|
|
100
|
-
insert.run(masked.id, masked.runId, masked.node, masked.kind, masked.question, JSON.stringify(masked.options), masked.recommended ?? null, masked.context ?? null, openedAt);
|
|
107
|
+
insert.run(masked.id, masked.runId, masked.node, masked.kind, masked.question, JSON.stringify(masked.options), masked.recommended ?? null, masked.context ?? null, masked.owner ?? null, openedAt);
|
|
101
108
|
return {
|
|
102
109
|
...masked,
|
|
103
110
|
openedAt,
|
|
104
111
|
status: "open"
|
|
105
112
|
};
|
|
106
113
|
},
|
|
107
|
-
answerDecision: (id, choice, by) => Number(answer.run(choice, by, (/* @__PURE__ */ new Date()).toISOString(), id).changes) === 1,
|
|
114
|
+
answerDecision: (id, choice, by, note) => Number(answer.run(choice, by, (/* @__PURE__ */ new Date()).toISOString(), noteOf(note), id).changes) === 1,
|
|
108
115
|
withdrawDecision: (id) => {
|
|
109
116
|
withdraw.run((/* @__PURE__ */ new Date()).toISOString(), id);
|
|
110
117
|
},
|
|
@@ -213,8 +220,10 @@ const recordOf$3 = (row) => {
|
|
|
213
220
|
};
|
|
214
221
|
const itemOf = (row) => {
|
|
215
222
|
const severities = strings(row, "severities");
|
|
223
|
+
const id = text(row, "fingerprint");
|
|
216
224
|
return {
|
|
217
|
-
id
|
|
225
|
+
id,
|
|
226
|
+
merged: strings(row, "fingerprints").filter((f) => f !== id),
|
|
218
227
|
status: oneOf(STATUSES$1, text(row, "status"), "new"),
|
|
219
228
|
title: text(row, "title"),
|
|
220
229
|
target: oneOf(FEEDBACK_TARGETS, text(row, "target"), "workflow"),
|
|
@@ -229,16 +238,20 @@ const itemOf = (row) => {
|
|
|
229
238
|
models: strings(row, "models")
|
|
230
239
|
};
|
|
231
240
|
};
|
|
232
|
-
|
|
233
|
-
|
|
241
|
+
/** Each record with the item it lists under: its own, or the one it merged into. */
|
|
242
|
+
const ITEMS = `SELECT r.fingerprint, r.status,
|
|
243
|
+
(SELECT title FROM feedback WHERE fingerprint = r.fingerprint ORDER BY id LIMIT 1) AS title,
|
|
234
244
|
MIN(f.target) AS target, MIN(f.kind) AS kind,
|
|
235
245
|
json_group_array(DISTINCT f.severity) AS severities,
|
|
236
246
|
json_group_array(DISTINCT f.source) AS sources,
|
|
237
247
|
COUNT(*) AS count, MIN(f.at) AS first_seen, MAX(f.at) AS last_seen,
|
|
238
248
|
json_group_array(DISTINCT f.run_id) AS runs,
|
|
239
249
|
json_group_array(DISTINCT f.workflow) AS workflows,
|
|
240
|
-
json_group_array(DISTINCT f.model) AS models
|
|
241
|
-
|
|
250
|
+
json_group_array(DISTINCT f.model) AS models,
|
|
251
|
+
json_group_array(DISTINCT f.fingerprint) AS fingerprints
|
|
252
|
+
FROM feedback f
|
|
253
|
+
JOIN feedback_items i ON i.fingerprint = f.fingerprint
|
|
254
|
+
JOIN feedback_items r ON r.fingerprint = COALESCE(i.merged_into, i.fingerprint)`;
|
|
242
255
|
const feedbackQueries = (db) => {
|
|
243
256
|
const insert = db.prepare(`INSERT INTO feedback (run_id, seq, at, node, source, kind, target, severity,
|
|
244
257
|
title, body, evidence, code, fingerprint, workflow, workflow_hash,
|
|
@@ -246,21 +259,51 @@ const feedbackQueries = (db) => {
|
|
|
246
259
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
247
260
|
const insertItem = db.prepare(`INSERT INTO feedback_items (fingerprint, status, updated_at)
|
|
248
261
|
VALUES (?, 'new', ?) ON CONFLICT (fingerprint) DO NOTHING`);
|
|
249
|
-
const shown = db.prepare(`${ITEMS} WHERE
|
|
250
|
-
const every = db.prepare(`${ITEMS} GROUP BY
|
|
251
|
-
const byPrefix = db.prepare(`${ITEMS} WHERE
|
|
252
|
-
|
|
262
|
+
const shown = db.prepare(`${ITEMS} WHERE r.status != 'dismissed' GROUP BY r.fingerprint ORDER BY last_seen DESC`);
|
|
263
|
+
const every = db.prepare(`${ITEMS} GROUP BY r.fingerprint ORDER BY last_seen DESC`);
|
|
264
|
+
const byPrefix = db.prepare(`${ITEMS} WHERE r.fingerprint IN (
|
|
265
|
+
SELECT COALESCE(merged_into, fingerprint) FROM feedback_items
|
|
266
|
+
WHERE substr(fingerprint, 1, ?) = ?
|
|
267
|
+
) GROUP BY r.fingerprint ORDER BY last_seen DESC`);
|
|
268
|
+
const records = db.prepare(`SELECT * FROM feedback
|
|
269
|
+
WHERE fingerprint = ? OR fingerprint IN (
|
|
270
|
+
SELECT fingerprint FROM feedback_items WHERE merged_into = ?
|
|
271
|
+
) ORDER BY id`);
|
|
272
|
+
const rootOf = db.prepare("SELECT COALESCE(merged_into, fingerprint) AS root FROM feedback_items WHERE fingerprint = ?");
|
|
273
|
+
const mergeInto = db.prepare(`UPDATE feedback_items SET merged_into = ?, updated_at = ?
|
|
274
|
+
WHERE (fingerprint = ? OR merged_into = ?) AND fingerprint != ?`);
|
|
275
|
+
const exported = db.prepare(`SELECT f.* FROM feedback f
|
|
276
|
+
LEFT JOIN feedback_items i ON i.fingerprint = f.fingerprint
|
|
277
|
+
LEFT JOIN feedback_items r ON r.fingerprint = COALESCE(i.merged_into, f.fingerprint)
|
|
278
|
+
WHERE ? = 1 OR COALESCE(r.status, 'new') != 'dismissed' ORDER BY f.id`);
|
|
279
|
+
const known = db.prepare("SELECT 1 FROM feedback WHERE run_id = ? AND seq = ?");
|
|
280
|
+
const split = db.prepare("UPDATE feedback_items SET merged_into = NULL, updated_at = ? WHERE merged_into = ?");
|
|
253
281
|
const setStatus = db.prepare("UPDATE feedback_items SET status = ?, updated_at = ? WHERE fingerprint = ?");
|
|
282
|
+
/** Writes a record, its secrets masked, and its item when it's new. */
|
|
283
|
+
const insertRecord = (runId, seq, at, node, feedback) => {
|
|
284
|
+
const { origin } = feedback;
|
|
285
|
+
insert.run(runId, seq, at, node ?? null, feedback.source, feedback.kind, feedback.target, feedback.severity, maskSecrets(feedback.title), maskSecrets(feedback.body), JSON.stringify(feedback.evidence.map((e) => maskSecrets(e))), feedback.code ?? null, feedback.fingerprint, origin.workflow ?? null, origin.workflowHash ?? null, origin.nodeType ?? null, origin.model ?? null, origin.driver ?? null, origin.version ?? null, 1);
|
|
286
|
+
insertItem.run(feedback.fingerprint, at);
|
|
287
|
+
};
|
|
254
288
|
return {
|
|
255
|
-
insertFeedback:
|
|
256
|
-
const { origin } = feedback;
|
|
257
|
-
insert.run(runId, seq, at, node ?? null, feedback.source, feedback.kind, feedback.target, feedback.severity, maskSecrets(feedback.title), maskSecrets(feedback.body), JSON.stringify(feedback.evidence.map((e) => maskSecrets(e))), feedback.code ?? null, feedback.fingerprint, origin.workflow ?? null, origin.workflowHash ?? null, origin.nodeType ?? null, origin.model ?? null, origin.driver ?? null, origin.version ?? null, 1);
|
|
258
|
-
insertItem.run(feedback.fingerprint, at);
|
|
259
|
-
},
|
|
289
|
+
insertFeedback: insertRecord,
|
|
260
290
|
feedbackItems: (filter = {}) => (filter.all === true ? every : shown).all().map((r) => itemOf(r)),
|
|
261
291
|
findFeedback: (prefix) => byPrefix.all(prefix.length, prefix).map((r) => itemOf(r)),
|
|
262
|
-
feedbackRecords: (fingerprint) => records.all(fingerprint).map((r) => recordOf$3(r)),
|
|
263
|
-
setFeedbackStatus: (fingerprint, status) => Number(setStatus.run(status, (/* @__PURE__ */ new Date()).toISOString(), fingerprint).changes) > 0
|
|
292
|
+
feedbackRecords: (fingerprint) => records.all(fingerprint, fingerprint).map((r) => recordOf$3(r)),
|
|
293
|
+
setFeedbackStatus: (fingerprint, status) => Number(setStatus.run(status, (/* @__PURE__ */ new Date()).toISOString(), fingerprint).changes) > 0,
|
|
294
|
+
mergeFeedback: (into, from) => {
|
|
295
|
+
const root = text(rootOf.get(into), "root");
|
|
296
|
+
if (root === "") return 0;
|
|
297
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
298
|
+
return inTransaction(db, () => from.filter((fingerprint) => fingerprint !== root).reduce((moved, fingerprint) => moved + Number(mergeInto.run(root, at, fingerprint, fingerprint, root).changes), 0));
|
|
299
|
+
},
|
|
300
|
+
splitFeedback: (fingerprint) => Number(split.run((/* @__PURE__ */ new Date()).toISOString(), fingerprint).changes),
|
|
301
|
+
exportFeedback: (filter = {}) => exported.all(filter.all === true ? 1 : 0).map((r) => recordOf$3(r)),
|
|
302
|
+
importFeedback: (record) => {
|
|
303
|
+
if (known.get(record.runId, record.seq) !== void 0) return false;
|
|
304
|
+
insertRecord(record.runId, record.seq, record.at, record.node, record.feedback);
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
264
307
|
};
|
|
265
308
|
};
|
|
266
309
|
// ../core/src/store/links.ts
|
|
@@ -681,9 +724,24 @@ const MIGRATIONS = [
|
|
|
681
724
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
682
725
|
pid INTEGER NOT NULL,
|
|
683
726
|
at TEXT NOT NULL
|
|
684
|
-
)
|
|
727
|
+
);`,
|
|
728
|
+
`ALTER TABLE feedback_items ADD COLUMN merged_into TEXT;
|
|
729
|
+
CREATE INDEX feedback_items_merged ON feedback_items (merged_into);`,
|
|
730
|
+
`ALTER TABLE runs ADD COLUMN process_start TEXT;`,
|
|
731
|
+
`ALTER TABLE runs ADD COLUMN skipped INTEGER;
|
|
732
|
+
ALTER TABLE runs ADD COLUMN failures_handled INTEGER;`,
|
|
733
|
+
`ALTER TABLE decisions ADD COLUMN note TEXT;`,
|
|
734
|
+
`ALTER TABLE decisions ADD COLUMN owner TEXT;`
|
|
685
735
|
];
|
|
686
736
|
const versionOf = (db) => Number(db.prepare("PRAGMA user_version").get()?.["user_version"] ?? 0);
|
|
737
|
+
/**
|
|
738
|
+
* Refuses a database that is not at this Ork's schema version, newer or older,
|
|
739
|
+
* for a reader that may not migrate it (a read-only open).
|
|
740
|
+
*/
|
|
741
|
+
const requireCurrent = (db, migrations = MIGRATIONS) => {
|
|
742
|
+
const version = versionOf(db);
|
|
743
|
+
if (version !== migrations.length) throw new Error(`The store is at schema version ${String(version)}; this Ork reads ${String(migrations.length)}.`);
|
|
744
|
+
};
|
|
687
745
|
/** Applies the migrations the database has not had yet. */
|
|
688
746
|
const migrate = (db, migrations = MIGRATIONS) => {
|
|
689
747
|
for (const [index, step] of migrations.entries()) {
|
|
@@ -702,7 +760,9 @@ const migrate = (db, migrations = MIGRATIONS) => {
|
|
|
702
760
|
* project, `~/.ork/state.db`, in WAL mode so other `ork` processes read while
|
|
703
761
|
* a run writes. Events are stored with secrets masked, feedback records
|
|
704
762
|
* beside them, and the registered projects; the schema is versioned with
|
|
705
|
-
* `user_version`.
|
|
763
|
+
* `user_version`. Tab completion opens it read-only: no store created, no
|
|
764
|
+
* migration, a store at another version refused, and the database never
|
|
765
|
+
* changed (SQLite may still add the `-wal` and `-shm` files beside it).
|
|
706
766
|
*/
|
|
707
767
|
/** `~/.ork/state.db`; `home` is `~` (tests pass a folder). */
|
|
708
768
|
const storePath = (home = homedir()) => path.join(home, ".ork", "state.db");
|
|
@@ -728,10 +788,16 @@ const recordOf = (row) => {
|
|
|
728
788
|
costUsd: Number(row?.["cost_usd"]),
|
|
729
789
|
depth: Number(row?.["depth"] ?? 0)
|
|
730
790
|
};
|
|
791
|
+
const processStart = text(row, "process_start");
|
|
792
|
+
if (processStart !== "") record.processStart = processStart;
|
|
731
793
|
const parent = text(row, "parent");
|
|
732
794
|
if (parent !== "") record.parent = parent;
|
|
733
795
|
const finished = text(row, "finished_at");
|
|
734
796
|
if (finished !== "") record.finishedAt = finished;
|
|
797
|
+
const skipped = row?.["skipped"];
|
|
798
|
+
if (typeof skipped === "number") record.skipped = skipped;
|
|
799
|
+
const handled = row?.["failures_handled"];
|
|
800
|
+
if (typeof handled === "number") record.failuresHandled = handled;
|
|
735
801
|
return record;
|
|
736
802
|
};
|
|
737
803
|
/** The node an event is about, for filtering. */
|
|
@@ -740,8 +806,20 @@ const nodeOf = (event) => {
|
|
|
740
806
|
if ("node" in event && typeof event.node === "string") return event.node;
|
|
741
807
|
return null;
|
|
742
808
|
};
|
|
743
|
-
/**
|
|
744
|
-
const
|
|
809
|
+
/** A database opened for reading only, refused unless it is current. */
|
|
810
|
+
const openReadOnly = (file) => {
|
|
811
|
+
const db = new DatabaseSync(file, { readOnly: true });
|
|
812
|
+
try {
|
|
813
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
814
|
+
requireCurrent(db);
|
|
815
|
+
return db;
|
|
816
|
+
} catch (error) {
|
|
817
|
+
db.close();
|
|
818
|
+
throw error;
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
/** A database opened for writing: created, in WAL mode, migrated. */
|
|
822
|
+
const openWritable = (file) => {
|
|
745
823
|
if (file !== ":memory:") mkdirSync(path.dirname(file), { recursive: true });
|
|
746
824
|
const db = new DatabaseSync(file);
|
|
747
825
|
db.exec("PRAGMA busy_timeout = 5000");
|
|
@@ -749,36 +827,48 @@ const openStore = (file = storePath()) => {
|
|
|
749
827
|
db.exec("PRAGMA synchronous = NORMAL");
|
|
750
828
|
db.exec("PRAGMA foreign_keys = ON");
|
|
751
829
|
migrate(db);
|
|
752
|
-
|
|
753
|
-
|
|
830
|
+
return db;
|
|
831
|
+
};
|
|
832
|
+
/**
|
|
833
|
+
* Opens (and creates) the store at `file`; `:memory:` for tests. With
|
|
834
|
+
* `readOnly`, it neither creates nor migrates, and throws when it can't read.
|
|
835
|
+
*/
|
|
836
|
+
const openStore = (file = storePath(), options = {}) => {
|
|
837
|
+
const ownStart = options.processStart ?? ownProcessStart;
|
|
838
|
+
const db = options.readOnly === true ? openReadOnly(file) : openWritable(file);
|
|
839
|
+
const insertRun = db.prepare(`INSERT INTO runs (id, workflow, file, project, status, pid, process_start, started_at, parent, depth)
|
|
840
|
+
VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?, ?)`);
|
|
754
841
|
const lastSeq = db.prepare("SELECT COALESCE(MAX(seq), 0) AS seq FROM events WHERE run_id = ?");
|
|
755
842
|
const insertEvent = db.prepare("INSERT INTO events (run_id, seq, at, type, node, data) VALUES (?, ?, ?, ?, ?, ?)");
|
|
756
|
-
const finishRun = db.prepare("UPDATE runs SET status = ?, finished_at = ?, cost_usd = ? WHERE id = ?");
|
|
757
|
-
const reopenRun = db.prepare("UPDATE runs SET status = 'running', pid = ?, finished_at = NULL WHERE id = ?");
|
|
843
|
+
const finishRun = db.prepare("UPDATE runs SET status = ?, finished_at = ?, cost_usd = ?, skipped = ?, failures_handled = ? WHERE id = ?");
|
|
844
|
+
const reopenRun = db.prepare("UPDATE runs SET status = 'running', pid = ?, process_start = ?, finished_at = NULL, skipped = NULL, failures_handled = NULL WHERE id = ?");
|
|
758
845
|
const addCost = db.prepare("UPDATE runs SET cost_usd = cost_usd + ? WHERE id = ?");
|
|
759
846
|
const byId = db.prepare("SELECT * FROM runs WHERE id = ?");
|
|
760
847
|
const byPrefix = db.prepare("SELECT * FROM runs WHERE substr(id, 1, ?) = ? ORDER BY started_at DESC, rowid DESC");
|
|
761
848
|
const allRuns = db.prepare("SELECT * FROM runs ORDER BY started_at DESC, rowid DESC LIMIT ?");
|
|
762
849
|
const projectRuns = db.prepare("SELECT * FROM runs WHERE project = ? ORDER BY started_at DESC, rowid DESC LIMIT ?");
|
|
763
850
|
const eventsAfter = db.prepare("SELECT seq, at, data FROM events WHERE run_id = ? AND seq > ? ORDER BY seq");
|
|
851
|
+
const runNodes = db.prepare("SELECT node FROM events WHERE run_id = ? AND node IS NOT NULL GROUP BY node ORDER BY MIN(seq)");
|
|
764
852
|
const { insertFeedback, recordUsage, recordPlanLimit, ...queries } = storeQueries(db);
|
|
765
853
|
return {
|
|
766
854
|
...queries,
|
|
767
855
|
begin: (run) => {
|
|
768
856
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
769
857
|
const depth = run.parent === void 0 ? 0 : Number(byId.get(run.parent)?.["depth"] ?? 0) + 1;
|
|
770
|
-
|
|
858
|
+
const processStart = ownStart();
|
|
859
|
+
insertRun.run(run.id, run.workflow, run.file, run.project, process.pid, processStart ?? null, startedAt, run.parent ?? null, depth);
|
|
771
860
|
return {
|
|
772
861
|
...run,
|
|
773
862
|
depth,
|
|
774
863
|
status: "running",
|
|
775
864
|
pid: process.pid,
|
|
865
|
+
...processStart === void 0 ? {} : { processStart },
|
|
776
866
|
startedAt,
|
|
777
867
|
costUsd: 0
|
|
778
868
|
};
|
|
779
869
|
},
|
|
780
870
|
reopen: (runId) => {
|
|
781
|
-
reopenRun.run(process.pid, runId);
|
|
871
|
+
reopenRun.run(process.pid, ownStart() ?? null, runId);
|
|
782
872
|
},
|
|
783
873
|
append: (runId, event) => inTransaction(db, () => {
|
|
784
874
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -788,7 +878,10 @@ const openStore = (file = storePath()) => {
|
|
|
788
878
|
if (event.type === "node.event" && event.event.type === "session.finished") recordUsage(runId);
|
|
789
879
|
if (event.type === "node.event" && event.event.type === "plan.limit") recordPlanLimit(event.event, at);
|
|
790
880
|
if (event.type === "feedback.recorded") insertFeedback(runId, seq, at, event.node, event.feedback);
|
|
791
|
-
if (event.type === "run.finished")
|
|
881
|
+
if (event.type === "run.finished") {
|
|
882
|
+
const { status, costUsd, skipped, failuresHandled } = event.result;
|
|
883
|
+
finishRun.run(status, at, costUsd, skipped ?? null, failuresHandled ?? null, runId);
|
|
884
|
+
}
|
|
792
885
|
return {
|
|
793
886
|
runId,
|
|
794
887
|
seq,
|
|
@@ -811,6 +904,7 @@ const openStore = (file = storePath()) => {
|
|
|
811
904
|
at: text(r, "at"),
|
|
812
905
|
event: eventOf(text(r, "data"))
|
|
813
906
|
})),
|
|
907
|
+
nodes: (runId) => runNodes.all(runId).map((r) => text(r, "node")),
|
|
814
908
|
close: () => {
|
|
815
909
|
db.close();
|
|
816
910
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as findRun } from "./find-
|
|
3
|
-
import { a as write, c as openStore, i as style, o as writeErr } from "./output-
|
|
4
|
-
import { r as continueRefusal } from "./in-a-run-
|
|
1
|
+
import { G as requestContinue, K as requestPause } from "./src-DHvWU4tX.mjs";
|
|
2
|
+
import { t as findRun } from "./find-BKW-uGey.mjs";
|
|
3
|
+
import { a as write, c as openStore, i as style, o as writeErr } from "./output-B1V6ZJSx.mjs";
|
|
4
|
+
import { r as continueRefusal } from "./in-a-run-BPXX09Bs.mjs";
|
|
5
5
|
import { defineCommand } from "citty";
|
|
6
6
|
// src/commands/pause.ts
|
|
7
7
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as write, c as openStore, i as style, o as writeErr } from "./output-
|
|
1
|
+
import { B as linkNodes, L as addressOf, U as unlinkNodes } from "./src-DHvWU4tX.mjs";
|
|
2
|
+
import { a as write, c as openStore, i as style, o as writeErr } from "./output-B1V6ZJSx.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
// src/commands/relay.ts
|
|
5
5
|
/**
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { ht as liveStatus, j as lines, k as git } from "./src-DHvWU4tX.mjs";
|
|
2
|
+
import { n as listOrkWorktrees, t as createdWorktrees } from "./list-C7UEgmtd.mjs";
|
|
3
|
+
import { i as keyOf, n as applyStep, r as cleanStep, s as stepPrunes } from "./clean-NnboOuz6.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.inMainCheckout) part("checked out in the main checkout");
|
|
49
|
+
if (item.heldBy !== void 0) part(`checked out in ${item.heldBy}`);
|
|
50
|
+
return parts;
|
|
51
|
+
};
|
|
52
|
+
/** What a step does, as a plan says it. */
|
|
53
|
+
const planWhat = (step) => {
|
|
54
|
+
const actions = [
|
|
55
|
+
...step.removeWorktree ? ["remove worktree"] : [],
|
|
56
|
+
...stepPrunes(step) && !step.removeWorktree ? ["prune worktree"] : [],
|
|
57
|
+
...step.deleteBranch ? ["delete branch"] : []
|
|
58
|
+
];
|
|
59
|
+
return actions.length > 0 ? actions.join(" + ") : "keep";
|
|
60
|
+
};
|
|
61
|
+
/** `ork/r1/lead: remove worktree`, `r1/check (detached): keep (…)`. */
|
|
62
|
+
const planLine = (step) => {
|
|
63
|
+
const { item } = step;
|
|
64
|
+
return {
|
|
65
|
+
text: `${item.branch === void 0 && item.checkedOut === void 0 ? `${labelOf(item)} (detached)` : labelOf(item)}: ${planWhat(step)}`,
|
|
66
|
+
...step.skip === void 0 ? {} : { reason: step.skip }
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Why an item can't be removed at all, even forced, if it can't: the main
|
|
71
|
+
* checkout or a worktree Ork didn't create holds it, or its run is still
|
|
72
|
+
* going.
|
|
73
|
+
*/
|
|
74
|
+
const removeRefusal = (item, live) => {
|
|
75
|
+
if (item.inMainCheckout) return "checked out in the main checkout; switch it to another branch first";
|
|
76
|
+
if (item.heldBy !== void 0) return `checked out in ${item.heldBy}, a worktree Ork didn't create; switch it to another branch first`;
|
|
77
|
+
return live ? `run ${item.runId} is still going; stop it first` : void 0;
|
|
78
|
+
};
|
|
79
|
+
// ../core/src/worktree/remove.ts
|
|
80
|
+
/** The ids of the project's runs whose process is still going. */
|
|
81
|
+
const liveRunIds = (store, root) => new Set(store.runs({ project: root }).filter((run) => liveStatus(run) === "running").map((run) => run.id));
|
|
82
|
+
/**
|
|
83
|
+
* Whether `root` is a repository with a commit, and the branch checked out
|
|
84
|
+
* there, which "merged" means merged into (null when its HEAD is detached).
|
|
85
|
+
*/
|
|
86
|
+
const projectHead = async (root) => {
|
|
87
|
+
if ((await git(root, [
|
|
88
|
+
"rev-parse",
|
|
89
|
+
"--verify",
|
|
90
|
+
"--quiet",
|
|
91
|
+
"HEAD"
|
|
92
|
+
])).code !== 0) return { ok: false };
|
|
93
|
+
const answer = await git(root, [
|
|
94
|
+
"symbolic-ref",
|
|
95
|
+
"--short",
|
|
96
|
+
"-q",
|
|
97
|
+
"HEAD"
|
|
98
|
+
]);
|
|
99
|
+
return {
|
|
100
|
+
ok: true,
|
|
101
|
+
branch: answer.code === 0 ? answer.stdout.trim() : null
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
/** What Ork created in the repo at `root`, from its runs' records and git. */
|
|
105
|
+
const readWorktrees = async (store, root) => ({
|
|
106
|
+
items: await listOrkWorktrees(root, createdWorktrees(store, root)),
|
|
107
|
+
live: liveRunIds(store, root)
|
|
108
|
+
});
|
|
109
|
+
/**
|
|
110
|
+
* The plan to remove one item, named by `keyOf`. A branch with no worktree is
|
|
111
|
+
* removed by deleting it, so `deleteBranch` is implied there.
|
|
112
|
+
*/
|
|
113
|
+
const planRemove = ({ items, live }, key, deleteBranch) => {
|
|
114
|
+
const item = items.find((i) => keyOf(i) === key);
|
|
115
|
+
if (item === void 0) return {
|
|
116
|
+
ok: false,
|
|
117
|
+
error: `No Ork worktree or branch ${key}.`,
|
|
118
|
+
gone: true
|
|
119
|
+
};
|
|
120
|
+
const refusal = removeRefusal(item, live.has(item.runId));
|
|
121
|
+
if (refusal !== void 0) return {
|
|
122
|
+
ok: false,
|
|
123
|
+
error: `Can't remove ${labelOf(item)}: ${refusal}.`,
|
|
124
|
+
gone: false
|
|
125
|
+
};
|
|
126
|
+
const branches = item.branch !== void 0 && (deleteBranch || item.path === null);
|
|
127
|
+
const careful = cleanStep(item, {
|
|
128
|
+
branches,
|
|
129
|
+
force: false,
|
|
130
|
+
live
|
|
131
|
+
});
|
|
132
|
+
if (careful.skip === void 0) return {
|
|
133
|
+
ok: true,
|
|
134
|
+
plan: {
|
|
135
|
+
step: careful,
|
|
136
|
+
force: false
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
return {
|
|
140
|
+
ok: true,
|
|
141
|
+
plan: {
|
|
142
|
+
step: cleanStep(item, {
|
|
143
|
+
branches,
|
|
144
|
+
force: true,
|
|
145
|
+
live
|
|
146
|
+
}),
|
|
147
|
+
force: true
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
const out = async (cwd, args) => lines((await git(cwd, args)).stdout);
|
|
152
|
+
/** What carrying out `plan` would lose, read from git. */
|
|
153
|
+
const lostWork = async (root, { step }) => {
|
|
154
|
+
const { item } = step;
|
|
155
|
+
const inside = item.path !== null && !item.missing ? item.path : null;
|
|
156
|
+
const changes = inside !== null && item.dirty && step.removeWorktree ? await out(inside, ["status", "--porcelain"]) : [];
|
|
157
|
+
if (step.deleteBranch && item.branch !== void 0 && !item.merged) return {
|
|
158
|
+
changes,
|
|
159
|
+
commits: await out(root, [
|
|
160
|
+
"log",
|
|
161
|
+
"--oneline",
|
|
162
|
+
`HEAD..${item.branch}`
|
|
163
|
+
]),
|
|
164
|
+
commitsOn: "unmerged"
|
|
165
|
+
};
|
|
166
|
+
if (item.branch === void 0 && item.checkedOut === void 0 && inside !== null && item.ahead > 0 && step.removeWorktree) return {
|
|
167
|
+
changes,
|
|
168
|
+
commits: await out(inside, [
|
|
169
|
+
"log",
|
|
170
|
+
"--oneline",
|
|
171
|
+
"HEAD",
|
|
172
|
+
"--not",
|
|
173
|
+
"--branches",
|
|
174
|
+
"--tags",
|
|
175
|
+
"--remotes"
|
|
176
|
+
]),
|
|
177
|
+
commitsOn: "no branch"
|
|
178
|
+
};
|
|
179
|
+
return {
|
|
180
|
+
changes,
|
|
181
|
+
commits: [],
|
|
182
|
+
commitsOn: null
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* Carries out a remove plan; what it did goes into `done` as it goes. The
|
|
187
|
+
* caller has checked that a plan needing `force` was asked to force. It touches
|
|
188
|
+
* only its own item, as each of clean's steps does.
|
|
189
|
+
*/
|
|
190
|
+
const applyRemove = async (root, { step, force }, done = []) => {
|
|
191
|
+
await applyStep(root, step, force, done);
|
|
192
|
+
return done;
|
|
193
|
+
};
|
|
194
|
+
export { readWorktrees as a, planLine as c, projectHead as i, removeRefusal as l, lostWork as n, labelOf as o, planRemove as r, mergeStateOf as s, applyRemove as t, stateParts as u };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { c as openStore, o as writeErr } from "./output-
|
|
3
|
-
import { u as inheritedScrubber } from "./src-
|
|
1
|
+
import { an as wakeRun, ht as liveStatus, kt as scriptContactsOf, rn as socketPath } from "./src-DHvWU4tX.mjs";
|
|
2
|
+
import { c as openStore, o as writeErr } from "./output-B1V6ZJSx.mjs";
|
|
3
|
+
import { u as inheritedScrubber } from "./src-C38BPzit.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
// ../core/src/remote/send.ts
|
|
6
6
|
/**
|