@basou/cli 0.8.0 → 0.10.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/dist/index.js +313 -102
- package/dist/index.js.map +1 -1
- package/dist/program.js +313 -102
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/program.js
CHANGED
|
@@ -8,7 +8,8 @@ import { join } from "path";
|
|
|
8
8
|
import {
|
|
9
9
|
ApprovalSchema,
|
|
10
10
|
ApprovalStatusSchema,
|
|
11
|
-
|
|
11
|
+
acquireLock,
|
|
12
|
+
appendChainedEventLocked,
|
|
12
13
|
assertBasouRootSafe,
|
|
13
14
|
basouPaths,
|
|
14
15
|
enumerateApprovals,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
linkYamlFile,
|
|
18
19
|
loadApproval,
|
|
19
20
|
prefixedUlid,
|
|
21
|
+
readSessionYaml,
|
|
20
22
|
readYamlFile,
|
|
21
23
|
replayEvents,
|
|
22
24
|
resolveRepositoryRoot
|
|
@@ -319,46 +321,63 @@ async function doRunApprovalResolve(idInput, options, ctx, decision) {
|
|
|
319
321
|
if (approval.status !== "pending") {
|
|
320
322
|
throw new Error(`Approval status mismatch: pending YAML has status=${approval.status}`);
|
|
321
323
|
}
|
|
322
|
-
const sessionDir = join(paths.sessions, approval.session_id);
|
|
323
|
-
for await (const ev of replayEvents(sessionDir, {
|
|
324
|
-
onWarning: (w) => printReplayWarning(w, approval.session_id)
|
|
325
|
-
})) {
|
|
326
|
-
if (isApprovalEvent(ev) && ev.approval_id === approval.id && (ev.type === "approval_approved" || ev.type === "approval_rejected" || ev.type === "approval_expired")) {
|
|
327
|
-
throw new Error(`Approval already resolved (per events.jsonl): ${idInput}`);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
324
|
const now = /* @__PURE__ */ new Date();
|
|
331
|
-
if (isLazyExpired(approval, now)) {
|
|
332
|
-
throw new Error(`Approval already expired: ${idInput}`);
|
|
333
|
-
}
|
|
334
325
|
const occurredAt = now.toISOString();
|
|
335
326
|
const eventId = prefixedUlid("evt");
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
327
|
+
const sessionLock = await acquireLock(paths, "session", approval.session_id);
|
|
328
|
+
try {
|
|
329
|
+
const sessionDir = join(paths.sessions, approval.session_id);
|
|
330
|
+
for await (const ev of replayEvents(sessionDir, {
|
|
331
|
+
onWarning: (w) => printReplayWarning(w, approval.session_id)
|
|
332
|
+
})) {
|
|
333
|
+
if (isApprovalEvent(ev) && ev.approval_id === approval.id && (ev.type === "approval_approved" || ev.type === "approval_rejected" || ev.type === "approval_expired")) {
|
|
334
|
+
throw new Error(`Approval already resolved (per events.jsonl): ${idInput}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (isLazyExpired(approval, now)) {
|
|
338
|
+
throw new Error(`Approval already expired: ${idInput}`);
|
|
339
|
+
}
|
|
340
|
+
let sessionStatus = null;
|
|
341
|
+
try {
|
|
342
|
+
sessionStatus = (await readSessionYaml(paths, approval.session_id)).session.status;
|
|
343
|
+
} catch {
|
|
344
|
+
sessionStatus = null;
|
|
345
|
+
}
|
|
346
|
+
const attachable = sessionStatus === "initialized" || sessionStatus === "running" || sessionStatus === "waiting_approval";
|
|
347
|
+
if (sessionStatus !== null && !attachable) {
|
|
348
|
+
throw new Error(
|
|
349
|
+
`Cannot resolve an approval for a session that is not active (status=${sessionStatus}): ${idInput}`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
if (decision === "approve") {
|
|
353
|
+
const note = options.note ?? null;
|
|
354
|
+
await appendChainedEventLocked(paths, approval.session_id, {
|
|
355
|
+
schema_version: "0.1.0",
|
|
356
|
+
id: eventId,
|
|
357
|
+
session_id: approval.session_id,
|
|
358
|
+
occurred_at: occurredAt,
|
|
359
|
+
source: "local-cli",
|
|
360
|
+
type: "approval_approved",
|
|
361
|
+
approval_id: approval.id,
|
|
362
|
+
resolver: "local-cli",
|
|
363
|
+
note
|
|
364
|
+
});
|
|
365
|
+
} else {
|
|
366
|
+
const reason = options.reason;
|
|
367
|
+
await appendChainedEventLocked(paths, approval.session_id, {
|
|
368
|
+
schema_version: "0.1.0",
|
|
369
|
+
id: eventId,
|
|
370
|
+
session_id: approval.session_id,
|
|
371
|
+
occurred_at: occurredAt,
|
|
372
|
+
source: "local-cli",
|
|
373
|
+
type: "approval_rejected",
|
|
374
|
+
approval_id: approval.id,
|
|
375
|
+
resolver: "local-cli",
|
|
376
|
+
reason
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
} finally {
|
|
380
|
+
await sessionLock.release();
|
|
362
381
|
}
|
|
363
382
|
const resolvedApproval = decision === "approve" ? {
|
|
364
383
|
...approval,
|
|
@@ -615,7 +634,7 @@ function printNoApprovals(options) {
|
|
|
615
634
|
|
|
616
635
|
// src/commands/decision.ts
|
|
617
636
|
import {
|
|
618
|
-
acquireLock,
|
|
637
|
+
acquireLock as acquireLock2,
|
|
619
638
|
appendEventToExistingSession,
|
|
620
639
|
assertBasouRootSafe as assertBasouRootSafe2,
|
|
621
640
|
basouPaths as basouPaths2,
|
|
@@ -680,7 +699,7 @@ async function doRunDecisionRecord(options, ctx) {
|
|
|
680
699
|
if (options.session !== void 0) {
|
|
681
700
|
const sessionId = await resolveSessionId(paths, options.session);
|
|
682
701
|
const sesId = sessionId;
|
|
683
|
-
const sessionLock = await
|
|
702
|
+
const sessionLock = await acquireLock2(paths, "session", sesId);
|
|
684
703
|
let result;
|
|
685
704
|
try {
|
|
686
705
|
result = await appendEventToExistingSession({
|
|
@@ -944,10 +963,12 @@ import { mkdir } from "fs/promises";
|
|
|
944
963
|
import { homedir } from "os";
|
|
945
964
|
import { join as join2 } from "path";
|
|
946
965
|
import {
|
|
966
|
+
acquireLock as acquireLock3,
|
|
947
967
|
assertBasouRootSafe as assertBasouRootSafe4,
|
|
948
968
|
basouPaths as basouPaths4,
|
|
949
969
|
ChildProcessRunner,
|
|
950
|
-
|
|
970
|
+
appendChainedEvent as coreAppendChainedEvent,
|
|
971
|
+
finalizeSessionYaml,
|
|
951
972
|
getSnapshot,
|
|
952
973
|
overwriteYamlFile,
|
|
953
974
|
parseDuration,
|
|
@@ -973,7 +994,6 @@ function registerExecCommand(program) {
|
|
|
973
994
|
async function runExec(command, args, options, ctx = {}) {
|
|
974
995
|
const runner = ctx.runner ?? new ChildProcessRunner();
|
|
975
996
|
const now = ctx.now ?? (() => /* @__PURE__ */ new Date());
|
|
976
|
-
const appendEvent2 = ctx.appendEvent ?? coreAppendEvent;
|
|
977
997
|
const cwd = options.cwd ?? process.cwd();
|
|
978
998
|
const timeout_ms = options.timeout !== void 0 ? parseDuration(options.timeout) : void 0;
|
|
979
999
|
const repoRoot = await resolveRepositoryRootForExec(cwd);
|
|
@@ -983,6 +1003,9 @@ async function runExec(command, args, options, ctx = {}) {
|
|
|
983
1003
|
const sessionId = prefixedUlid3("ses");
|
|
984
1004
|
const sessionDir = join2(paths.sessions, sessionId);
|
|
985
1005
|
await mkdir(sessionDir, { recursive: true });
|
|
1006
|
+
const appendEvent = ctx.appendEvent ?? (async (_sessionDir, event) => {
|
|
1007
|
+
await coreAppendChainedEvent(paths, sessionId, event);
|
|
1008
|
+
});
|
|
986
1009
|
const startedAt = now().toISOString();
|
|
987
1010
|
const sessionYamlPath = join2(sessionDir, "session.yaml");
|
|
988
1011
|
const session = buildInitialSession({
|
|
@@ -994,7 +1017,7 @@ async function runExec(command, args, options, ctx = {}) {
|
|
|
994
1017
|
startedAt
|
|
995
1018
|
});
|
|
996
1019
|
await writeYamlFile(sessionYamlPath, session);
|
|
997
|
-
await
|
|
1020
|
+
await appendEvent(sessionDir, {
|
|
998
1021
|
schema_version: "0.1.0",
|
|
999
1022
|
type: "session_started",
|
|
1000
1023
|
id: prefixedUlid3("evt"),
|
|
@@ -1003,10 +1026,10 @@ async function runExec(command, args, options, ctx = {}) {
|
|
|
1003
1026
|
source: "terminal-recording"
|
|
1004
1027
|
});
|
|
1005
1028
|
if (options.snapshot !== false) {
|
|
1006
|
-
await tryAppendGitSnapshot(sessionDir, sessionId, repoRoot, now,
|
|
1029
|
+
await tryAppendGitSnapshot(sessionDir, sessionId, repoRoot, now, appendEvent);
|
|
1007
1030
|
}
|
|
1008
1031
|
const runningAt = now().toISOString();
|
|
1009
|
-
await
|
|
1032
|
+
await appendEvent(sessionDir, {
|
|
1010
1033
|
schema_version: "0.1.0",
|
|
1011
1034
|
type: "session_status_changed",
|
|
1012
1035
|
id: prefixedUlid3("evt"),
|
|
@@ -1016,9 +1039,14 @@ async function runExec(command, args, options, ctx = {}) {
|
|
|
1016
1039
|
from: "initialized",
|
|
1017
1040
|
to: "running"
|
|
1018
1041
|
});
|
|
1019
|
-
await
|
|
1020
|
-
|
|
1021
|
-
|
|
1042
|
+
const runningLock = await acquireLock3(paths, "session", sessionId);
|
|
1043
|
+
try {
|
|
1044
|
+
await mutateSessionYaml(sessionYamlPath, (s) => {
|
|
1045
|
+
s.session.status = "running";
|
|
1046
|
+
});
|
|
1047
|
+
} finally {
|
|
1048
|
+
await runningLock.release();
|
|
1049
|
+
}
|
|
1022
1050
|
const controller = new AbortController();
|
|
1023
1051
|
let signalReceived = null;
|
|
1024
1052
|
let activeChild = null;
|
|
@@ -1054,7 +1082,7 @@ async function runExec(command, args, options, ctx = {}) {
|
|
|
1054
1082
|
}
|
|
1055
1083
|
});
|
|
1056
1084
|
} catch (spawnError) {
|
|
1057
|
-
await finalizeSessionAsFailed(
|
|
1085
|
+
await finalizeSessionAsFailed(paths, sessionDir, sessionId, appendEvent, {
|
|
1058
1086
|
command,
|
|
1059
1087
|
args,
|
|
1060
1088
|
cwd,
|
|
@@ -1070,7 +1098,7 @@ async function runExec(command, args, options, ctx = {}) {
|
|
|
1070
1098
|
activeChild = null;
|
|
1071
1099
|
}
|
|
1072
1100
|
const endedAt = now().toISOString();
|
|
1073
|
-
await
|
|
1101
|
+
await appendEvent(sessionDir, {
|
|
1074
1102
|
schema_version: "0.1.0",
|
|
1075
1103
|
type: "command_executed",
|
|
1076
1104
|
id: prefixedUlid3("evt"),
|
|
@@ -1086,10 +1114,10 @@ async function runExec(command, args, options, ctx = {}) {
|
|
|
1086
1114
|
duration_ms: result.duration_ms
|
|
1087
1115
|
});
|
|
1088
1116
|
if (options.snapshot !== false) {
|
|
1089
|
-
await tryAppendGitSnapshot(sessionDir, sessionId, repoRoot, now,
|
|
1117
|
+
await tryAppendGitSnapshot(sessionDir, sessionId, repoRoot, now, appendEvent);
|
|
1090
1118
|
}
|
|
1091
1119
|
const finalStatus = decideFinalStatus(result, signalReceived);
|
|
1092
|
-
await
|
|
1120
|
+
await appendEvent(sessionDir, {
|
|
1093
1121
|
schema_version: "0.1.0",
|
|
1094
1122
|
type: "session_status_changed",
|
|
1095
1123
|
id: prefixedUlid3("evt"),
|
|
@@ -1099,7 +1127,7 @@ async function runExec(command, args, options, ctx = {}) {
|
|
|
1099
1127
|
from: "running",
|
|
1100
1128
|
to: finalStatus
|
|
1101
1129
|
});
|
|
1102
|
-
await
|
|
1130
|
+
await appendEvent(sessionDir, {
|
|
1103
1131
|
schema_version: "0.1.0",
|
|
1104
1132
|
type: "session_ended",
|
|
1105
1133
|
id: prefixedUlid3("evt"),
|
|
@@ -1108,7 +1136,7 @@ async function runExec(command, args, options, ctx = {}) {
|
|
|
1108
1136
|
source: "terminal-recording",
|
|
1109
1137
|
...result.exit_code !== null ? { exit_code: result.exit_code } : {}
|
|
1110
1138
|
});
|
|
1111
|
-
await
|
|
1139
|
+
await finalizeSessionYaml(paths, sessionId, (s) => {
|
|
1112
1140
|
s.session.status = finalStatus;
|
|
1113
1141
|
s.session.ended_at = endedAt;
|
|
1114
1142
|
s.session.invocation.exit_code = result.exit_code;
|
|
@@ -1138,7 +1166,7 @@ function signalToExitCode(sig) {
|
|
|
1138
1166
|
const num = SIGNUM_MAP[sig] ?? 1;
|
|
1139
1167
|
return 128 + num;
|
|
1140
1168
|
}
|
|
1141
|
-
async function tryAppendGitSnapshot(sessionDir, sessionId, repoRoot, now,
|
|
1169
|
+
async function tryAppendGitSnapshot(sessionDir, sessionId, repoRoot, now, appendEvent) {
|
|
1142
1170
|
let snapshot;
|
|
1143
1171
|
try {
|
|
1144
1172
|
snapshot = await getSnapshot(repoRoot);
|
|
@@ -1146,7 +1174,7 @@ async function tryAppendGitSnapshot(sessionDir, sessionId, repoRoot, now, append
|
|
|
1146
1174
|
console.warn(normalizeGitSnapshotSkipMessage(error));
|
|
1147
1175
|
return;
|
|
1148
1176
|
}
|
|
1149
|
-
await
|
|
1177
|
+
await appendEvent(sessionDir, {
|
|
1150
1178
|
schema_version: "0.1.0",
|
|
1151
1179
|
type: "git_snapshot",
|
|
1152
1180
|
id: prefixedUlid3("evt"),
|
|
@@ -1202,8 +1230,8 @@ async function mutateSessionYaml(filePath, mutator) {
|
|
|
1202
1230
|
const validated = SessionSchema.parse(parsed);
|
|
1203
1231
|
await overwriteYamlFile(filePath, validated);
|
|
1204
1232
|
}
|
|
1205
|
-
async function finalizeSessionAsFailed(
|
|
1206
|
-
await
|
|
1233
|
+
async function finalizeSessionAsFailed(paths, sessionDir, sessionId, appendEvent, ctx) {
|
|
1234
|
+
await appendEvent(sessionDir, {
|
|
1207
1235
|
schema_version: "0.1.0",
|
|
1208
1236
|
type: "command_executed",
|
|
1209
1237
|
id: prefixedUlid3("evt"),
|
|
@@ -1218,7 +1246,7 @@ async function finalizeSessionAsFailed(sessionDir, sessionYamlPath, sessionId, a
|
|
|
1218
1246
|
...ctx.signalReceived !== null ? { received_signal: ctx.signalReceived } : {},
|
|
1219
1247
|
duration_ms: 0
|
|
1220
1248
|
});
|
|
1221
|
-
await
|
|
1249
|
+
await appendEvent(sessionDir, {
|
|
1222
1250
|
schema_version: "0.1.0",
|
|
1223
1251
|
type: "session_status_changed",
|
|
1224
1252
|
id: prefixedUlid3("evt"),
|
|
@@ -1228,7 +1256,7 @@ async function finalizeSessionAsFailed(sessionDir, sessionYamlPath, sessionId, a
|
|
|
1228
1256
|
from: "running",
|
|
1229
1257
|
to: "failed"
|
|
1230
1258
|
});
|
|
1231
|
-
await
|
|
1259
|
+
await appendEvent(sessionDir, {
|
|
1232
1260
|
schema_version: "0.1.0",
|
|
1233
1261
|
type: "session_ended",
|
|
1234
1262
|
id: prefixedUlid3("evt"),
|
|
@@ -1236,7 +1264,7 @@ async function finalizeSessionAsFailed(sessionDir, sessionYamlPath, sessionId, a
|
|
|
1236
1264
|
occurred_at: ctx.occurredAt,
|
|
1237
1265
|
source: "terminal-recording"
|
|
1238
1266
|
});
|
|
1239
|
-
await
|
|
1267
|
+
await finalizeSessionYaml(paths, sessionId, (s) => {
|
|
1240
1268
|
s.session.status = "failed";
|
|
1241
1269
|
s.session.ended_at = ctx.occurredAt;
|
|
1242
1270
|
s.session.invocation.exit_code = null;
|
|
@@ -1342,7 +1370,7 @@ import {
|
|
|
1342
1370
|
findErrorCode as findErrorCode5,
|
|
1343
1371
|
importSessionFromJson,
|
|
1344
1372
|
readManifest as readManifest3,
|
|
1345
|
-
readSessionYaml,
|
|
1373
|
+
readSessionYaml as readSessionYaml2,
|
|
1346
1374
|
reimportPreservingId,
|
|
1347
1375
|
resolveRepositoryRoot as resolveRepositoryRoot6,
|
|
1348
1376
|
SessionImportPayloadSchema
|
|
@@ -1524,7 +1552,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
|
|
|
1524
1552
|
dryRun: options.dryRun === true
|
|
1525
1553
|
});
|
|
1526
1554
|
if (outcome.status === "skipped") {
|
|
1527
|
-
const detail = outcome.reason === "prior_events_unreadable" ? "prior events.jsonl has unreadable lines" : "source changed in a non-append way (derived events would be dropped)";
|
|
1555
|
+
const detail = outcome.reason === "prior_events_unreadable" ? "prior events.jsonl has unreadable lines" : outcome.reason === "prior_chain_broken" ? "prior events.jsonl failed hash-chain verification (run 'basou verify')" : "source changed in a non-append way (derived events would be dropped)";
|
|
1528
1556
|
console.error(`Import: ${externalId} ${detail}; re-import skipped`);
|
|
1529
1557
|
counts.skippedNoAction++;
|
|
1530
1558
|
continue;
|
|
@@ -1612,7 +1640,7 @@ async function loadExistingByExternalId(paths, sourceKind) {
|
|
|
1612
1640
|
for (const sessionId of sessionIds) {
|
|
1613
1641
|
let session;
|
|
1614
1642
|
try {
|
|
1615
|
-
session = await
|
|
1643
|
+
session = await readSessionYaml2(paths, sessionId);
|
|
1616
1644
|
} catch {
|
|
1617
1645
|
continue;
|
|
1618
1646
|
}
|
|
@@ -2388,11 +2416,13 @@ import { mkdir as mkdir2 } from "fs/promises";
|
|
|
2388
2416
|
import { homedir as homedir4 } from "os";
|
|
2389
2417
|
import { join as join5 } from "path";
|
|
2390
2418
|
import {
|
|
2419
|
+
acquireLock as acquireLock4,
|
|
2391
2420
|
assertBasouRootSafe as assertBasouRootSafe8,
|
|
2392
2421
|
basouPaths as basouPaths8,
|
|
2393
2422
|
ChildProcessRunner as ChildProcessRunner2,
|
|
2394
2423
|
claudeCodeAdapterMetadata,
|
|
2395
|
-
|
|
2424
|
+
appendChainedEvent as coreAppendChainedEvent2,
|
|
2425
|
+
finalizeSessionYaml as finalizeSessionYaml2,
|
|
2396
2426
|
getDiff,
|
|
2397
2427
|
getSnapshot as getSnapshot2,
|
|
2398
2428
|
overwriteYamlFile as overwriteYamlFile2,
|
|
@@ -2428,7 +2458,6 @@ function registerRunCommand(program, ctx = {}) {
|
|
|
2428
2458
|
async function runClaudeCode(args, options, ctx = {}) {
|
|
2429
2459
|
const runner = ctx.runner ?? new ChildProcessRunner2();
|
|
2430
2460
|
const now = ctx.now ?? (() => /* @__PURE__ */ new Date());
|
|
2431
|
-
const appendEvent2 = ctx.appendEvent ?? coreAppendEvent2;
|
|
2432
2461
|
const resolveCommand = ctx.resolveCommand ?? resolveClaudeCodeCommand;
|
|
2433
2462
|
const getDiffFn = ctx.getDiff ?? getDiff;
|
|
2434
2463
|
const { command } = await resolveCommand();
|
|
@@ -2440,6 +2469,9 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2440
2469
|
const sessionId = prefixedUlid4("ses");
|
|
2441
2470
|
const sessionDir = join5(paths.sessions, sessionId);
|
|
2442
2471
|
await mkdir2(sessionDir, { recursive: true });
|
|
2472
|
+
const appendEvent = ctx.appendEvent ?? (async (_sessionDir, event) => {
|
|
2473
|
+
await coreAppendChainedEvent2(paths, sessionId, event);
|
|
2474
|
+
});
|
|
2443
2475
|
const startedAt = now().toISOString();
|
|
2444
2476
|
const sessionYamlPath = join5(sessionDir, "session.yaml");
|
|
2445
2477
|
const session = buildInitialSession2({
|
|
@@ -2451,7 +2483,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2451
2483
|
startedAt
|
|
2452
2484
|
});
|
|
2453
2485
|
await writeYamlFile2(sessionYamlPath, session);
|
|
2454
|
-
await
|
|
2486
|
+
await appendEvent(sessionDir, {
|
|
2455
2487
|
schema_version: "0.1.0",
|
|
2456
2488
|
type: "session_started",
|
|
2457
2489
|
id: prefixedUlid4("evt"),
|
|
@@ -2461,10 +2493,10 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2461
2493
|
});
|
|
2462
2494
|
let preSnapshot = null;
|
|
2463
2495
|
if (options.snapshot !== false) {
|
|
2464
|
-
preSnapshot = await tryAppendGitSnapshot2(sessionDir, sessionId, repoRoot, now,
|
|
2496
|
+
preSnapshot = await tryAppendGitSnapshot2(sessionDir, sessionId, repoRoot, now, appendEvent);
|
|
2465
2497
|
}
|
|
2466
2498
|
const runningAt = now().toISOString();
|
|
2467
|
-
await
|
|
2499
|
+
await appendEvent(sessionDir, {
|
|
2468
2500
|
schema_version: "0.1.0",
|
|
2469
2501
|
type: "session_status_changed",
|
|
2470
2502
|
id: prefixedUlid4("evt"),
|
|
@@ -2474,9 +2506,14 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2474
2506
|
from: "initialized",
|
|
2475
2507
|
to: "running"
|
|
2476
2508
|
});
|
|
2477
|
-
await
|
|
2478
|
-
|
|
2479
|
-
|
|
2509
|
+
const runningLock = await acquireLock4(paths, "session", sessionId);
|
|
2510
|
+
try {
|
|
2511
|
+
await mutateSessionYaml2(sessionYamlPath, (s) => {
|
|
2512
|
+
s.session.status = "running";
|
|
2513
|
+
});
|
|
2514
|
+
} finally {
|
|
2515
|
+
await runningLock.release();
|
|
2516
|
+
}
|
|
2480
2517
|
const controller = new AbortController();
|
|
2481
2518
|
let signalReceived = null;
|
|
2482
2519
|
let activeChild = null;
|
|
@@ -2511,7 +2548,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2511
2548
|
}
|
|
2512
2549
|
});
|
|
2513
2550
|
} catch (spawnError) {
|
|
2514
|
-
await finalizeSessionAsFailed2(
|
|
2551
|
+
await finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEvent, {
|
|
2515
2552
|
command,
|
|
2516
2553
|
args,
|
|
2517
2554
|
cwd: repoRoot,
|
|
@@ -2527,7 +2564,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2527
2564
|
activeChild = null;
|
|
2528
2565
|
}
|
|
2529
2566
|
const endedAt = now().toISOString();
|
|
2530
|
-
await
|
|
2567
|
+
await appendEvent(sessionDir, {
|
|
2531
2568
|
schema_version: "0.1.0",
|
|
2532
2569
|
type: "command_executed",
|
|
2533
2570
|
id: prefixedUlid4("evt"),
|
|
@@ -2544,7 +2581,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2544
2581
|
});
|
|
2545
2582
|
let postSnapshot = null;
|
|
2546
2583
|
if (options.snapshot !== false) {
|
|
2547
|
-
postSnapshot = await tryAppendGitSnapshot2(sessionDir, sessionId, repoRoot, now,
|
|
2584
|
+
postSnapshot = await tryAppendGitSnapshot2(sessionDir, sessionId, repoRoot, now, appendEvent);
|
|
2548
2585
|
}
|
|
2549
2586
|
let diff = null;
|
|
2550
2587
|
if (preSnapshot !== null && postSnapshot !== null) {
|
|
@@ -2555,7 +2592,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2555
2592
|
preSnapshot.head,
|
|
2556
2593
|
postSnapshot.head,
|
|
2557
2594
|
now().toISOString(),
|
|
2558
|
-
|
|
2595
|
+
appendEvent,
|
|
2559
2596
|
getDiffFn
|
|
2560
2597
|
);
|
|
2561
2598
|
}
|
|
@@ -2565,7 +2602,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2565
2602
|
homedir: homedir4()
|
|
2566
2603
|
}).sanitized;
|
|
2567
2604
|
const finalStatus = decideFinalStatus2(result, signalReceived);
|
|
2568
|
-
await
|
|
2605
|
+
await appendEvent(sessionDir, {
|
|
2569
2606
|
schema_version: "0.1.0",
|
|
2570
2607
|
type: "session_status_changed",
|
|
2571
2608
|
id: prefixedUlid4("evt"),
|
|
@@ -2575,7 +2612,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2575
2612
|
from: "running",
|
|
2576
2613
|
to: finalStatus
|
|
2577
2614
|
});
|
|
2578
|
-
await
|
|
2615
|
+
await appendEvent(sessionDir, {
|
|
2579
2616
|
schema_version: "0.1.0",
|
|
2580
2617
|
type: "session_ended",
|
|
2581
2618
|
id: prefixedUlid4("evt"),
|
|
@@ -2584,7 +2621,7 @@ async function runClaudeCode(args, options, ctx = {}) {
|
|
|
2584
2621
|
source: claudeCodeAdapterMetadata.kind,
|
|
2585
2622
|
...result.exit_code !== null ? { exit_code: result.exit_code } : {}
|
|
2586
2623
|
});
|
|
2587
|
-
await
|
|
2624
|
+
await finalizeSessionYaml2(paths, sessionId, (s) => {
|
|
2588
2625
|
s.session.status = finalStatus;
|
|
2589
2626
|
s.session.ended_at = endedAt;
|
|
2590
2627
|
s.session.invocation.exit_code = result.exit_code;
|
|
@@ -2613,7 +2650,7 @@ function signalToExitCode2(sig) {
|
|
|
2613
2650
|
const num = SIGNUM_MAP2[sig] ?? 1;
|
|
2614
2651
|
return 128 + num;
|
|
2615
2652
|
}
|
|
2616
|
-
async function tryAppendGitSnapshot2(sessionDir, sessionId, repoRoot, now,
|
|
2653
|
+
async function tryAppendGitSnapshot2(sessionDir, sessionId, repoRoot, now, appendEvent) {
|
|
2617
2654
|
let snapshot;
|
|
2618
2655
|
try {
|
|
2619
2656
|
snapshot = await getSnapshot2(repoRoot);
|
|
@@ -2621,7 +2658,7 @@ async function tryAppendGitSnapshot2(sessionDir, sessionId, repoRoot, now, appen
|
|
|
2621
2658
|
console.warn(normalizeGitSnapshotSkipMessage2(error));
|
|
2622
2659
|
return null;
|
|
2623
2660
|
}
|
|
2624
|
-
await
|
|
2661
|
+
await appendEvent(sessionDir, {
|
|
2625
2662
|
schema_version: "0.1.0",
|
|
2626
2663
|
type: "git_snapshot",
|
|
2627
2664
|
id: prefixedUlid4("evt"),
|
|
@@ -2632,7 +2669,7 @@ async function tryAppendGitSnapshot2(sessionDir, sessionId, repoRoot, now, appen
|
|
|
2632
2669
|
});
|
|
2633
2670
|
return snapshot;
|
|
2634
2671
|
}
|
|
2635
|
-
async function tryAppendFileChangedEvents(sessionDir, sessionId, repoRoot, baseRef, headRef, occurredAt,
|
|
2672
|
+
async function tryAppendFileChangedEvents(sessionDir, sessionId, repoRoot, baseRef, headRef, occurredAt, appendEvent, getDiffFn) {
|
|
2636
2673
|
let diff;
|
|
2637
2674
|
try {
|
|
2638
2675
|
diff = await getDiffFn(repoRoot, baseRef, headRef);
|
|
@@ -2641,7 +2678,7 @@ async function tryAppendFileChangedEvents(sessionDir, sessionId, repoRoot, baseR
|
|
|
2641
2678
|
return null;
|
|
2642
2679
|
}
|
|
2643
2680
|
for (const change of diff.changed_files) {
|
|
2644
|
-
await
|
|
2681
|
+
await appendEvent(sessionDir, {
|
|
2645
2682
|
schema_version: "0.1.0",
|
|
2646
2683
|
type: "file_changed",
|
|
2647
2684
|
id: prefixedUlid4("evt"),
|
|
@@ -2724,8 +2761,8 @@ async function mutateSessionYaml2(filePath, mutator) {
|
|
|
2724
2761
|
const validated = SessionSchema2.parse(parsed);
|
|
2725
2762
|
await overwriteYamlFile2(filePath, validated);
|
|
2726
2763
|
}
|
|
2727
|
-
async function finalizeSessionAsFailed2(
|
|
2728
|
-
await
|
|
2764
|
+
async function finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEvent, ctx) {
|
|
2765
|
+
await appendEvent(sessionDir, {
|
|
2729
2766
|
schema_version: "0.1.0",
|
|
2730
2767
|
type: "command_executed",
|
|
2731
2768
|
id: prefixedUlid4("evt"),
|
|
@@ -2740,7 +2777,7 @@ async function finalizeSessionAsFailed2(sessionDir, sessionYamlPath, sessionId,
|
|
|
2740
2777
|
...ctx.signalReceived !== null ? { received_signal: ctx.signalReceived } : {},
|
|
2741
2778
|
duration_ms: 0
|
|
2742
2779
|
});
|
|
2743
|
-
await
|
|
2780
|
+
await appendEvent(sessionDir, {
|
|
2744
2781
|
schema_version: "0.1.0",
|
|
2745
2782
|
type: "session_status_changed",
|
|
2746
2783
|
id: prefixedUlid4("evt"),
|
|
@@ -2750,7 +2787,7 @@ async function finalizeSessionAsFailed2(sessionDir, sessionYamlPath, sessionId,
|
|
|
2750
2787
|
from: "running",
|
|
2751
2788
|
to: "failed"
|
|
2752
2789
|
});
|
|
2753
|
-
await
|
|
2790
|
+
await appendEvent(sessionDir, {
|
|
2754
2791
|
schema_version: "0.1.0",
|
|
2755
2792
|
type: "session_ended",
|
|
2756
2793
|
id: prefixedUlid4("evt"),
|
|
@@ -2758,7 +2795,7 @@ async function finalizeSessionAsFailed2(sessionDir, sessionYamlPath, sessionId,
|
|
|
2758
2795
|
occurred_at: ctx.occurredAt,
|
|
2759
2796
|
source: claudeCodeAdapterMetadata.kind
|
|
2760
2797
|
});
|
|
2761
|
-
await
|
|
2798
|
+
await finalizeSessionYaml2(paths, sessionId, (s) => {
|
|
2762
2799
|
s.session.status = "failed";
|
|
2763
2800
|
s.session.ended_at = ctx.occurredAt;
|
|
2764
2801
|
s.session.invocation.exit_code = null;
|
|
@@ -2781,16 +2818,18 @@ async function resolveRepositoryRootForRun(cwd) {
|
|
|
2781
2818
|
import { readFile as readFile2 } from "fs/promises";
|
|
2782
2819
|
import { basename as basename3, isAbsolute, join as join6, relative as relative2 } from "path";
|
|
2783
2820
|
import {
|
|
2784
|
-
acquireLock as
|
|
2821
|
+
acquireLock as acquireLock5,
|
|
2785
2822
|
appendEventToExistingSession as appendEventToExistingSession2,
|
|
2786
2823
|
assertBasouRootSafe as assertBasouRootSafe9,
|
|
2787
2824
|
basouPaths as basouPaths9,
|
|
2825
|
+
enumerateSessionDirs as enumerateSessionDirs2,
|
|
2788
2826
|
findErrorCode as findErrorCode8,
|
|
2789
2827
|
importSessionFromJson as importSessionFromJson2,
|
|
2790
2828
|
loadSessionEntries,
|
|
2791
2829
|
readAllEvents,
|
|
2792
2830
|
readManifest as readManifest5,
|
|
2793
2831
|
readYamlFile as readYamlFile4,
|
|
2832
|
+
rechainSessionInPlace,
|
|
2794
2833
|
resolveRepositoryRoot as resolveRepositoryRoot10,
|
|
2795
2834
|
resolveSessionId as resolveSessionId2,
|
|
2796
2835
|
resolveTaskId,
|
|
@@ -2839,6 +2878,11 @@ function registerSessionCommand(program) {
|
|
|
2839
2878
|
session.command("note <session_id>").description("Append a note_added event to an existing session").option("--body <text>", "Note body (inline)", parseNoteBodyOption).option("--from-file <path>", "Read note body from a file").option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (sessionIdInput, options) => {
|
|
2840
2879
|
await runSessionNote(sessionIdInput, options);
|
|
2841
2880
|
});
|
|
2881
|
+
session.command("rechain").description(
|
|
2882
|
+
"Add the tamper-evidence hash chain, in place, to imported sessions created before chaining existed"
|
|
2883
|
+
).option("--session <id>", "Rechain a single session (unique id prefix accepted)").option("--all", "Rechain every session in the workspace").option("--dry-run", "Compute the outcomes only; do not write").option("--json", "Output the outcomes as JSON").option("-v, --verbose", "Show error causes").action(async (options) => {
|
|
2884
|
+
await runSessionRechain(options);
|
|
2885
|
+
});
|
|
2842
2886
|
}
|
|
2843
2887
|
async function runSessionList(options, ctx = {}) {
|
|
2844
2888
|
try {
|
|
@@ -3324,7 +3368,7 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
|
|
|
3324
3368
|
}
|
|
3325
3369
|
const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3326
3370
|
const sesId = sessionId;
|
|
3327
|
-
const sessionLock = await
|
|
3371
|
+
const sessionLock = await acquireLock5(paths, "session", sesId);
|
|
3328
3372
|
let result;
|
|
3329
3373
|
try {
|
|
3330
3374
|
result = await appendEventToExistingSession2({
|
|
@@ -3380,6 +3424,74 @@ function printSessionNoteResult(options, sessionId, eventId, sessionStatus, body
|
|
|
3380
3424
|
const preview = body.length > NOTE_BODY_PREVIEW_LIMIT ? `${body.slice(0, NOTE_BODY_PREVIEW_HEAD)}...` : body;
|
|
3381
3425
|
console.log(`Added note to session ${sid} (${sessionStatus}): ${preview}`);
|
|
3382
3426
|
}
|
|
3427
|
+
async function runSessionRechain(options, ctx = {}) {
|
|
3428
|
+
try {
|
|
3429
|
+
await doRunSessionRechain(options, ctx);
|
|
3430
|
+
} catch (error) {
|
|
3431
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
3432
|
+
process.exitCode = 1;
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
async function doRunSessionRechain(options, ctx) {
|
|
3436
|
+
if (options.session !== void 0 && options.all === true) {
|
|
3437
|
+
throw new Error("Specify either --session <id> or --all, not both");
|
|
3438
|
+
}
|
|
3439
|
+
if (options.session === void 0 && options.all !== true) {
|
|
3440
|
+
throw new Error("Specify --session <id> or --all");
|
|
3441
|
+
}
|
|
3442
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
3443
|
+
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "rechain");
|
|
3444
|
+
const paths = basouPaths9(repositoryRoot);
|
|
3445
|
+
await assertWorkspaceInitialized7(paths.root);
|
|
3446
|
+
const sessionIds = options.session !== void 0 ? [await resolveSessionId2(paths, options.session)] : await enumerateSessionDirs2(paths);
|
|
3447
|
+
const dryRun = options.dryRun === true;
|
|
3448
|
+
const rows = [];
|
|
3449
|
+
for (const sessionId of sessionIds) {
|
|
3450
|
+
let outcome;
|
|
3451
|
+
try {
|
|
3452
|
+
outcome = await rechainSessionInPlace(paths, sessionId, { dryRun });
|
|
3453
|
+
} catch (error) {
|
|
3454
|
+
rows.push({
|
|
3455
|
+
session_id: sessionId,
|
|
3456
|
+
status: "error",
|
|
3457
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
3458
|
+
});
|
|
3459
|
+
continue;
|
|
3460
|
+
}
|
|
3461
|
+
if (outcome.status === "rechained") {
|
|
3462
|
+
rows.push({ session_id: sessionId, status: "rechained", event_count: outcome.eventCount });
|
|
3463
|
+
} else {
|
|
3464
|
+
rows.push({ session_id: sessionId, status: "skipped", reason: outcome.reason });
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
const tamperedCount = rows.filter((r) => r.reason === "tampered").length;
|
|
3468
|
+
const errorCount = rows.filter((r) => r.status === "error").length;
|
|
3469
|
+
if (options.json === true) {
|
|
3470
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
3471
|
+
} else {
|
|
3472
|
+
for (const row of rows) {
|
|
3473
|
+
console.log(`${row.session_id} ${renderRechainRow(row, dryRun)}`);
|
|
3474
|
+
}
|
|
3475
|
+
const rechained = rows.filter((r) => r.status === "rechained").length;
|
|
3476
|
+
const skipped = rows.filter((r) => r.status === "skipped").length;
|
|
3477
|
+
console.log(
|
|
3478
|
+
`Sessions: ${rows.length} total \u2014 ${rechained} ${dryRun ? "would be rechained" : "rechained"}, ${skipped} skipped, ${errorCount} errors`
|
|
3479
|
+
);
|
|
3480
|
+
}
|
|
3481
|
+
if (tamperedCount > 0 || errorCount > 0) {
|
|
3482
|
+
process.exitCode = 1;
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
function renderRechainRow(row, dryRun) {
|
|
3486
|
+
switch (row.status) {
|
|
3487
|
+
case "rechained":
|
|
3488
|
+
return `${dryRun ? "would rechain" : "rechained"} (${row.event_count} events)`;
|
|
3489
|
+
case "skipped":
|
|
3490
|
+
return row.reason === "tampered" ? "skipped (TAMPERED \u2014 inspect with 'basou verify')" : `skipped (${row.reason})`;
|
|
3491
|
+
case "error":
|
|
3492
|
+
return `error (${row.message})`;
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3383
3495
|
|
|
3384
3496
|
// src/commands/stats.ts
|
|
3385
3497
|
import {
|
|
@@ -4716,9 +4828,107 @@ function maxLen3(values, floor) {
|
|
|
4716
4828
|
return max;
|
|
4717
4829
|
}
|
|
4718
4830
|
|
|
4831
|
+
// src/commands/verify.ts
|
|
4832
|
+
import {
|
|
4833
|
+
assertBasouRootSafe as assertBasouRootSafe13,
|
|
4834
|
+
basouPaths as basouPaths13,
|
|
4835
|
+
enumerateSessionDirs as enumerateSessionDirs3,
|
|
4836
|
+
findErrorCode as findErrorCode12,
|
|
4837
|
+
resolveRepositoryRoot as resolveRepositoryRoot14,
|
|
4838
|
+
resolveSessionId as resolveSessionId4,
|
|
4839
|
+
verifyEventsChain
|
|
4840
|
+
} from "@basou/core";
|
|
4841
|
+
function registerVerifyCommand(program) {
|
|
4842
|
+
program.command("verify").description("Verify the tamper-evidence hash chain of sessions' event logs (read-only)").option("--session <id>", "Verify a single session (unique id prefix accepted)").option("--all", "Verify every session (the default when --session is omitted)").option("--json", "Output the verdicts as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
|
|
4843
|
+
await runVerify(opts);
|
|
4844
|
+
});
|
|
4845
|
+
}
|
|
4846
|
+
async function runVerify(options, ctx = {}) {
|
|
4847
|
+
try {
|
|
4848
|
+
await doRunVerify(options, ctx);
|
|
4849
|
+
} catch (error) {
|
|
4850
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
4851
|
+
process.exitCode = 1;
|
|
4852
|
+
}
|
|
4853
|
+
}
|
|
4854
|
+
async function doRunVerify(options, ctx) {
|
|
4855
|
+
if (options.session !== void 0 && options.all === true) {
|
|
4856
|
+
throw new Error("Specify either --session <id> or --all, not both");
|
|
4857
|
+
}
|
|
4858
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
4859
|
+
const repositoryRoot = await resolveRepositoryRootForVerify(cwd);
|
|
4860
|
+
const paths = basouPaths13(repositoryRoot);
|
|
4861
|
+
await assertWorkspaceInitialized10(paths.root);
|
|
4862
|
+
const sessionIds = options.session !== void 0 ? [await resolveSessionId4(paths, options.session)] : await enumerateSessionDirs3(paths);
|
|
4863
|
+
const rows = [];
|
|
4864
|
+
for (const sessionId of sessionIds) {
|
|
4865
|
+
const verdict = await verifyEventsChain(paths, sessionId);
|
|
4866
|
+
rows.push({
|
|
4867
|
+
session_id: sessionId,
|
|
4868
|
+
status: verdict.status,
|
|
4869
|
+
event_count: verdict.eventCount,
|
|
4870
|
+
...verdict.reason !== void 0 ? { reason: verdict.reason } : {},
|
|
4871
|
+
...verdict.line !== void 0 ? { line: verdict.line } : {}
|
|
4872
|
+
});
|
|
4873
|
+
}
|
|
4874
|
+
const tamperedCount = rows.filter((r) => r.status === "tampered").length;
|
|
4875
|
+
if (options.json === true) {
|
|
4876
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
4877
|
+
} else {
|
|
4878
|
+
for (const row of rows) {
|
|
4879
|
+
console.log(`${row.session_id} ${renderVerdict(row)}`);
|
|
4880
|
+
}
|
|
4881
|
+
const tally = (status) => rows.filter((r) => r.status === status).length;
|
|
4882
|
+
console.log(
|
|
4883
|
+
`Sessions: ${rows.length} total \u2014 ${tally("verified")} verified, ${tally("unchained")} unchained, ${tally("empty")} empty, ${tally("incomplete")} incomplete, ${tally("in_progress")} in_progress, ${tamperedCount} tampered`
|
|
4884
|
+
);
|
|
4885
|
+
}
|
|
4886
|
+
if (tamperedCount > 0) {
|
|
4887
|
+
process.exitCode = 1;
|
|
4888
|
+
}
|
|
4889
|
+
}
|
|
4890
|
+
function renderVerdict(row) {
|
|
4891
|
+
switch (row.status) {
|
|
4892
|
+
case "verified":
|
|
4893
|
+
return `verified (${row.event_count} events)`;
|
|
4894
|
+
case "tampered":
|
|
4895
|
+
return row.line !== void 0 ? `TAMPERED (${row.reason} at line ${row.line})` : `TAMPERED (${row.reason})`;
|
|
4896
|
+
case "incomplete":
|
|
4897
|
+
return "incomplete (session.yaml missing; re-import to repair)";
|
|
4898
|
+
case "in_progress":
|
|
4899
|
+
return `in_progress (${row.event_count} events; live session, anchor written at finalize)`;
|
|
4900
|
+
case "unchained":
|
|
4901
|
+
return "unchained (session created before event-log chaining)";
|
|
4902
|
+
case "empty":
|
|
4903
|
+
return "empty";
|
|
4904
|
+
}
|
|
4905
|
+
}
|
|
4906
|
+
async function resolveRepositoryRootForVerify(cwd) {
|
|
4907
|
+
try {
|
|
4908
|
+
return await resolveRepositoryRoot14(cwd);
|
|
4909
|
+
} catch (error) {
|
|
4910
|
+
if (error instanceof Error && error.message === "Not a git repository") {
|
|
4911
|
+
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou verify'.", {
|
|
4912
|
+
cause: error
|
|
4913
|
+
});
|
|
4914
|
+
}
|
|
4915
|
+
throw error;
|
|
4916
|
+
}
|
|
4917
|
+
}
|
|
4918
|
+
async function assertWorkspaceInitialized10(basouRoot) {
|
|
4919
|
+
try {
|
|
4920
|
+
await assertBasouRootSafe13(basouRoot);
|
|
4921
|
+
} catch (error) {
|
|
4922
|
+
if (findErrorCode12(error, "ENOENT")) {
|
|
4923
|
+
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
4924
|
+
}
|
|
4925
|
+
throw error;
|
|
4926
|
+
}
|
|
4927
|
+
}
|
|
4928
|
+
|
|
4719
4929
|
// src/commands/view.ts
|
|
4720
4930
|
import { spawn } from "child_process";
|
|
4721
|
-
import { assertBasouRootSafe as
|
|
4931
|
+
import { assertBasouRootSafe as assertBasouRootSafe14, basouPaths as basouPaths14, findErrorCode as findErrorCode14, resolveRepositoryRoot as resolveRepositoryRoot15 } from "@basou/core";
|
|
4722
4932
|
import { InvalidArgumentError as InvalidArgumentError5 } from "commander";
|
|
4723
4933
|
|
|
4724
4934
|
// src/lib/view-server.ts
|
|
@@ -4727,7 +4937,7 @@ import { join as join8 } from "path";
|
|
|
4727
4937
|
import {
|
|
4728
4938
|
computeWorkStats as computeWorkStats2,
|
|
4729
4939
|
enumerateApprovals as enumerateApprovals2,
|
|
4730
|
-
findErrorCode as
|
|
4940
|
+
findErrorCode as findErrorCode13,
|
|
4731
4941
|
isLazyExpired as isLazyExpired2,
|
|
4732
4942
|
loadApproval as loadApproval2,
|
|
4733
4943
|
loadSessionEntries as loadSessionEntries3,
|
|
@@ -4735,7 +4945,7 @@ import {
|
|
|
4735
4945
|
readAllEvents as readAllEvents2,
|
|
4736
4946
|
readManifest as readManifest8,
|
|
4737
4947
|
readMarkdownFile as readMarkdownFile4,
|
|
4738
|
-
readSessionYaml as
|
|
4948
|
+
readSessionYaml as readSessionYaml3,
|
|
4739
4949
|
readTaskFile as readTaskFile2,
|
|
4740
4950
|
renderDecisions as renderDecisions3,
|
|
4741
4951
|
renderHandoff as renderHandoff3
|
|
@@ -5329,7 +5539,7 @@ async function overview(deps) {
|
|
|
5329
5539
|
try {
|
|
5330
5540
|
manifest = await readManifest8(deps.paths);
|
|
5331
5541
|
} catch (error) {
|
|
5332
|
-
if (
|
|
5542
|
+
if (findErrorCode13(error, "ENOENT")) {
|
|
5333
5543
|
return { initialized: false, repoRoot: deps.repoRoot };
|
|
5334
5544
|
}
|
|
5335
5545
|
throw error;
|
|
@@ -5376,7 +5586,7 @@ async function sessionsList(deps) {
|
|
|
5376
5586
|
async function sessionDetail(deps, sessionId) {
|
|
5377
5587
|
let session;
|
|
5378
5588
|
try {
|
|
5379
|
-
session = await
|
|
5589
|
+
session = await readSessionYaml3(deps.paths, sessionId);
|
|
5380
5590
|
} catch (error) {
|
|
5381
5591
|
if (error instanceof Error && error.message === "YAML file not found") {
|
|
5382
5592
|
throw new HttpError(404, "Session not found");
|
|
@@ -5544,8 +5754,8 @@ async function runView(options, ctx = {}) {
|
|
|
5544
5754
|
async function doRunView(options, ctx) {
|
|
5545
5755
|
const cwd = ctx.cwd ?? process.cwd();
|
|
5546
5756
|
const repositoryRoot = await resolveRepositoryRootForView(cwd);
|
|
5547
|
-
const paths =
|
|
5548
|
-
await
|
|
5757
|
+
const paths = basouPaths14(repositoryRoot);
|
|
5758
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
5549
5759
|
const deps = {
|
|
5550
5760
|
paths,
|
|
5551
5761
|
repoRoot: repositoryRoot,
|
|
@@ -5576,7 +5786,7 @@ async function startListening(port, deps) {
|
|
|
5576
5786
|
try {
|
|
5577
5787
|
return await startViewServer({ port, deps });
|
|
5578
5788
|
} catch (error) {
|
|
5579
|
-
if (
|
|
5789
|
+
if (findErrorCode14(error, "EADDRINUSE")) {
|
|
5580
5790
|
throw new Error(`Port ${port} is already in use. Pass --port <n> to choose another.`, {
|
|
5581
5791
|
cause: error
|
|
5582
5792
|
});
|
|
@@ -5627,7 +5837,7 @@ function waitForShutdown(signal) {
|
|
|
5627
5837
|
}
|
|
5628
5838
|
async function resolveRepositoryRootForView(cwd) {
|
|
5629
5839
|
try {
|
|
5630
|
-
return await
|
|
5840
|
+
return await resolveRepositoryRoot15(cwd);
|
|
5631
5841
|
} catch (error) {
|
|
5632
5842
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
5633
5843
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou view'.", {
|
|
@@ -5637,11 +5847,11 @@ async function resolveRepositoryRootForView(cwd) {
|
|
|
5637
5847
|
throw error;
|
|
5638
5848
|
}
|
|
5639
5849
|
}
|
|
5640
|
-
async function
|
|
5850
|
+
async function assertWorkspaceInitialized11(basouRoot) {
|
|
5641
5851
|
try {
|
|
5642
|
-
await
|
|
5852
|
+
await assertBasouRootSafe14(basouRoot);
|
|
5643
5853
|
} catch (error) {
|
|
5644
|
-
if (
|
|
5854
|
+
if (findErrorCode14(error, "ENOENT")) {
|
|
5645
5855
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
5646
5856
|
}
|
|
5647
5857
|
throw error;
|
|
@@ -5663,6 +5873,7 @@ function buildProgram() {
|
|
|
5663
5873
|
registerSessionCommand(program);
|
|
5664
5874
|
registerImportCommand(program);
|
|
5665
5875
|
registerRefreshCommand(program);
|
|
5876
|
+
registerVerifyCommand(program);
|
|
5666
5877
|
registerViewCommand(program);
|
|
5667
5878
|
registerApprovalCommand(program);
|
|
5668
5879
|
registerDecisionCommand(program);
|