@basou/cli 0.8.0 → 0.9.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 +199 -15
- package/dist/index.js.map +1 -1
- package/dist/program.js +199 -15
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -124,6 +124,7 @@ import {
|
|
|
124
124
|
linkYamlFile,
|
|
125
125
|
loadApproval,
|
|
126
126
|
prefixedUlid,
|
|
127
|
+
readSessionYaml,
|
|
127
128
|
readYamlFile,
|
|
128
129
|
replayEvents,
|
|
129
130
|
resolveRepositoryRoot
|
|
@@ -331,6 +332,15 @@ async function doRunApprovalResolve(idInput, options, ctx, decision) {
|
|
|
331
332
|
if (isLazyExpired(approval, now)) {
|
|
332
333
|
throw new Error(`Approval already expired: ${idInput}`);
|
|
333
334
|
}
|
|
335
|
+
let sessionStatus = null;
|
|
336
|
+
try {
|
|
337
|
+
sessionStatus = (await readSessionYaml(paths, approval.session_id)).session.status;
|
|
338
|
+
} catch {
|
|
339
|
+
sessionStatus = null;
|
|
340
|
+
}
|
|
341
|
+
if (sessionStatus === "imported") {
|
|
342
|
+
throw new Error(`Cannot resolve an approval for an imported session: ${idInput}`);
|
|
343
|
+
}
|
|
334
344
|
const occurredAt = now.toISOString();
|
|
335
345
|
const eventId = prefixedUlid("evt");
|
|
336
346
|
if (decision === "approve") {
|
|
@@ -1342,7 +1352,7 @@ import {
|
|
|
1342
1352
|
findErrorCode as findErrorCode5,
|
|
1343
1353
|
importSessionFromJson,
|
|
1344
1354
|
readManifest as readManifest3,
|
|
1345
|
-
readSessionYaml,
|
|
1355
|
+
readSessionYaml as readSessionYaml2,
|
|
1346
1356
|
reimportPreservingId,
|
|
1347
1357
|
resolveRepositoryRoot as resolveRepositoryRoot6,
|
|
1348
1358
|
SessionImportPayloadSchema
|
|
@@ -1524,7 +1534,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
|
|
|
1524
1534
|
dryRun: options.dryRun === true
|
|
1525
1535
|
});
|
|
1526
1536
|
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)";
|
|
1537
|
+
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
1538
|
console.error(`Import: ${externalId} ${detail}; re-import skipped`);
|
|
1529
1539
|
counts.skippedNoAction++;
|
|
1530
1540
|
continue;
|
|
@@ -1612,7 +1622,7 @@ async function loadExistingByExternalId(paths, sourceKind) {
|
|
|
1612
1622
|
for (const sessionId of sessionIds) {
|
|
1613
1623
|
let session;
|
|
1614
1624
|
try {
|
|
1615
|
-
session = await
|
|
1625
|
+
session = await readSessionYaml2(paths, sessionId);
|
|
1616
1626
|
} catch {
|
|
1617
1627
|
continue;
|
|
1618
1628
|
}
|
|
@@ -2785,12 +2795,14 @@ import {
|
|
|
2785
2795
|
appendEventToExistingSession as appendEventToExistingSession2,
|
|
2786
2796
|
assertBasouRootSafe as assertBasouRootSafe9,
|
|
2787
2797
|
basouPaths as basouPaths9,
|
|
2798
|
+
enumerateSessionDirs as enumerateSessionDirs2,
|
|
2788
2799
|
findErrorCode as findErrorCode8,
|
|
2789
2800
|
importSessionFromJson as importSessionFromJson2,
|
|
2790
2801
|
loadSessionEntries,
|
|
2791
2802
|
readAllEvents,
|
|
2792
2803
|
readManifest as readManifest5,
|
|
2793
2804
|
readYamlFile as readYamlFile4,
|
|
2805
|
+
rechainSessionInPlace,
|
|
2794
2806
|
resolveRepositoryRoot as resolveRepositoryRoot10,
|
|
2795
2807
|
resolveSessionId as resolveSessionId2,
|
|
2796
2808
|
resolveTaskId,
|
|
@@ -2839,6 +2851,11 @@ function registerSessionCommand(program2) {
|
|
|
2839
2851
|
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
2852
|
await runSessionNote(sessionIdInput, options);
|
|
2841
2853
|
});
|
|
2854
|
+
session.command("rechain").description(
|
|
2855
|
+
"Add the tamper-evidence hash chain, in place, to imported sessions created before chaining existed"
|
|
2856
|
+
).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) => {
|
|
2857
|
+
await runSessionRechain(options);
|
|
2858
|
+
});
|
|
2842
2859
|
}
|
|
2843
2860
|
async function runSessionList(options, ctx = {}) {
|
|
2844
2861
|
try {
|
|
@@ -3380,6 +3397,74 @@ function printSessionNoteResult(options, sessionId, eventId, sessionStatus, body
|
|
|
3380
3397
|
const preview = body.length > NOTE_BODY_PREVIEW_LIMIT ? `${body.slice(0, NOTE_BODY_PREVIEW_HEAD)}...` : body;
|
|
3381
3398
|
console.log(`Added note to session ${sid} (${sessionStatus}): ${preview}`);
|
|
3382
3399
|
}
|
|
3400
|
+
async function runSessionRechain(options, ctx = {}) {
|
|
3401
|
+
try {
|
|
3402
|
+
await doRunSessionRechain(options, ctx);
|
|
3403
|
+
} catch (error) {
|
|
3404
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
3405
|
+
process.exitCode = 1;
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
async function doRunSessionRechain(options, ctx) {
|
|
3409
|
+
if (options.session !== void 0 && options.all === true) {
|
|
3410
|
+
throw new Error("Specify either --session <id> or --all, not both");
|
|
3411
|
+
}
|
|
3412
|
+
if (options.session === void 0 && options.all !== true) {
|
|
3413
|
+
throw new Error("Specify --session <id> or --all");
|
|
3414
|
+
}
|
|
3415
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
3416
|
+
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "rechain");
|
|
3417
|
+
const paths = basouPaths9(repositoryRoot);
|
|
3418
|
+
await assertWorkspaceInitialized7(paths.root);
|
|
3419
|
+
const sessionIds = options.session !== void 0 ? [await resolveSessionId2(paths, options.session)] : await enumerateSessionDirs2(paths);
|
|
3420
|
+
const dryRun = options.dryRun === true;
|
|
3421
|
+
const rows = [];
|
|
3422
|
+
for (const sessionId of sessionIds) {
|
|
3423
|
+
let outcome;
|
|
3424
|
+
try {
|
|
3425
|
+
outcome = await rechainSessionInPlace(paths, sessionId, { dryRun });
|
|
3426
|
+
} catch (error) {
|
|
3427
|
+
rows.push({
|
|
3428
|
+
session_id: sessionId,
|
|
3429
|
+
status: "error",
|
|
3430
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
3431
|
+
});
|
|
3432
|
+
continue;
|
|
3433
|
+
}
|
|
3434
|
+
if (outcome.status === "rechained") {
|
|
3435
|
+
rows.push({ session_id: sessionId, status: "rechained", event_count: outcome.eventCount });
|
|
3436
|
+
} else {
|
|
3437
|
+
rows.push({ session_id: sessionId, status: "skipped", reason: outcome.reason });
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
const tamperedCount = rows.filter((r) => r.reason === "tampered").length;
|
|
3441
|
+
const errorCount = rows.filter((r) => r.status === "error").length;
|
|
3442
|
+
if (options.json === true) {
|
|
3443
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
3444
|
+
} else {
|
|
3445
|
+
for (const row of rows) {
|
|
3446
|
+
console.log(`${row.session_id} ${renderRechainRow(row, dryRun)}`);
|
|
3447
|
+
}
|
|
3448
|
+
const rechained = rows.filter((r) => r.status === "rechained").length;
|
|
3449
|
+
const skipped = rows.filter((r) => r.status === "skipped").length;
|
|
3450
|
+
console.log(
|
|
3451
|
+
`Sessions: ${rows.length} total \u2014 ${rechained} ${dryRun ? "would be rechained" : "rechained"}, ${skipped} skipped, ${errorCount} errors`
|
|
3452
|
+
);
|
|
3453
|
+
}
|
|
3454
|
+
if (tamperedCount > 0 || errorCount > 0) {
|
|
3455
|
+
process.exitCode = 1;
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
function renderRechainRow(row, dryRun) {
|
|
3459
|
+
switch (row.status) {
|
|
3460
|
+
case "rechained":
|
|
3461
|
+
return `${dryRun ? "would rechain" : "rechained"} (${row.event_count} events)`;
|
|
3462
|
+
case "skipped":
|
|
3463
|
+
return row.reason === "tampered" ? "skipped (TAMPERED \u2014 inspect with 'basou verify')" : `skipped (${row.reason})`;
|
|
3464
|
+
case "error":
|
|
3465
|
+
return `error (${row.message})`;
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3383
3468
|
|
|
3384
3469
|
// src/commands/stats.ts
|
|
3385
3470
|
import {
|
|
@@ -4716,9 +4801,107 @@ function maxLen3(values, floor) {
|
|
|
4716
4801
|
return max;
|
|
4717
4802
|
}
|
|
4718
4803
|
|
|
4804
|
+
// src/commands/verify.ts
|
|
4805
|
+
import {
|
|
4806
|
+
assertBasouRootSafe as assertBasouRootSafe13,
|
|
4807
|
+
basouPaths as basouPaths13,
|
|
4808
|
+
enumerateSessionDirs as enumerateSessionDirs3,
|
|
4809
|
+
findErrorCode as findErrorCode12,
|
|
4810
|
+
resolveRepositoryRoot as resolveRepositoryRoot14,
|
|
4811
|
+
resolveSessionId as resolveSessionId4,
|
|
4812
|
+
verifyEventsChain
|
|
4813
|
+
} from "@basou/core";
|
|
4814
|
+
function registerVerifyCommand(program2) {
|
|
4815
|
+
program2.command("verify").description(
|
|
4816
|
+
"Verify the tamper-evidence hash chain of imported sessions' event logs (read-only)"
|
|
4817
|
+
).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) => {
|
|
4818
|
+
await runVerify(opts);
|
|
4819
|
+
});
|
|
4820
|
+
}
|
|
4821
|
+
async function runVerify(options, ctx = {}) {
|
|
4822
|
+
try {
|
|
4823
|
+
await doRunVerify(options, ctx);
|
|
4824
|
+
} catch (error) {
|
|
4825
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
4826
|
+
process.exitCode = 1;
|
|
4827
|
+
}
|
|
4828
|
+
}
|
|
4829
|
+
async function doRunVerify(options, ctx) {
|
|
4830
|
+
if (options.session !== void 0 && options.all === true) {
|
|
4831
|
+
throw new Error("Specify either --session <id> or --all, not both");
|
|
4832
|
+
}
|
|
4833
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
4834
|
+
const repositoryRoot = await resolveRepositoryRootForVerify(cwd);
|
|
4835
|
+
const paths = basouPaths13(repositoryRoot);
|
|
4836
|
+
await assertWorkspaceInitialized10(paths.root);
|
|
4837
|
+
const sessionIds = options.session !== void 0 ? [await resolveSessionId4(paths, options.session)] : await enumerateSessionDirs3(paths);
|
|
4838
|
+
const rows = [];
|
|
4839
|
+
for (const sessionId of sessionIds) {
|
|
4840
|
+
const verdict = await verifyEventsChain(paths, sessionId);
|
|
4841
|
+
rows.push({
|
|
4842
|
+
session_id: sessionId,
|
|
4843
|
+
status: verdict.status,
|
|
4844
|
+
event_count: verdict.eventCount,
|
|
4845
|
+
...verdict.reason !== void 0 ? { reason: verdict.reason } : {},
|
|
4846
|
+
...verdict.line !== void 0 ? { line: verdict.line } : {}
|
|
4847
|
+
});
|
|
4848
|
+
}
|
|
4849
|
+
const tamperedCount = rows.filter((r) => r.status === "tampered").length;
|
|
4850
|
+
if (options.json === true) {
|
|
4851
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
4852
|
+
} else {
|
|
4853
|
+
for (const row of rows) {
|
|
4854
|
+
console.log(`${row.session_id} ${renderVerdict(row)}`);
|
|
4855
|
+
}
|
|
4856
|
+
const tally = (status) => rows.filter((r) => r.status === status).length;
|
|
4857
|
+
console.log(
|
|
4858
|
+
`Sessions: ${rows.length} total \u2014 ${tally("verified")} verified, ${tally("unchained")} unchained, ${tally("empty")} empty, ${tally("incomplete")} incomplete, ${tamperedCount} tampered`
|
|
4859
|
+
);
|
|
4860
|
+
}
|
|
4861
|
+
if (tamperedCount > 0) {
|
|
4862
|
+
process.exitCode = 1;
|
|
4863
|
+
}
|
|
4864
|
+
}
|
|
4865
|
+
function renderVerdict(row) {
|
|
4866
|
+
switch (row.status) {
|
|
4867
|
+
case "verified":
|
|
4868
|
+
return `verified (${row.event_count} events)`;
|
|
4869
|
+
case "tampered":
|
|
4870
|
+
return row.line !== void 0 ? `TAMPERED (${row.reason} at line ${row.line})` : `TAMPERED (${row.reason})`;
|
|
4871
|
+
case "incomplete":
|
|
4872
|
+
return "incomplete (session.yaml missing; re-import to repair)";
|
|
4873
|
+
case "unchained":
|
|
4874
|
+
return "unchained (not an imported session, or imported before chaining)";
|
|
4875
|
+
case "empty":
|
|
4876
|
+
return "empty";
|
|
4877
|
+
}
|
|
4878
|
+
}
|
|
4879
|
+
async function resolveRepositoryRootForVerify(cwd) {
|
|
4880
|
+
try {
|
|
4881
|
+
return await resolveRepositoryRoot14(cwd);
|
|
4882
|
+
} catch (error) {
|
|
4883
|
+
if (error instanceof Error && error.message === "Not a git repository") {
|
|
4884
|
+
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou verify'.", {
|
|
4885
|
+
cause: error
|
|
4886
|
+
});
|
|
4887
|
+
}
|
|
4888
|
+
throw error;
|
|
4889
|
+
}
|
|
4890
|
+
}
|
|
4891
|
+
async function assertWorkspaceInitialized10(basouRoot) {
|
|
4892
|
+
try {
|
|
4893
|
+
await assertBasouRootSafe13(basouRoot);
|
|
4894
|
+
} catch (error) {
|
|
4895
|
+
if (findErrorCode12(error, "ENOENT")) {
|
|
4896
|
+
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
4897
|
+
}
|
|
4898
|
+
throw error;
|
|
4899
|
+
}
|
|
4900
|
+
}
|
|
4901
|
+
|
|
4719
4902
|
// src/commands/view.ts
|
|
4720
4903
|
import { spawn } from "child_process";
|
|
4721
|
-
import { assertBasouRootSafe as
|
|
4904
|
+
import { assertBasouRootSafe as assertBasouRootSafe14, basouPaths as basouPaths14, findErrorCode as findErrorCode14, resolveRepositoryRoot as resolveRepositoryRoot15 } from "@basou/core";
|
|
4722
4905
|
import { InvalidArgumentError as InvalidArgumentError5 } from "commander";
|
|
4723
4906
|
|
|
4724
4907
|
// src/lib/view-server.ts
|
|
@@ -4727,7 +4910,7 @@ import { join as join8 } from "path";
|
|
|
4727
4910
|
import {
|
|
4728
4911
|
computeWorkStats as computeWorkStats2,
|
|
4729
4912
|
enumerateApprovals as enumerateApprovals2,
|
|
4730
|
-
findErrorCode as
|
|
4913
|
+
findErrorCode as findErrorCode13,
|
|
4731
4914
|
isLazyExpired as isLazyExpired2,
|
|
4732
4915
|
loadApproval as loadApproval2,
|
|
4733
4916
|
loadSessionEntries as loadSessionEntries3,
|
|
@@ -4735,7 +4918,7 @@ import {
|
|
|
4735
4918
|
readAllEvents as readAllEvents2,
|
|
4736
4919
|
readManifest as readManifest8,
|
|
4737
4920
|
readMarkdownFile as readMarkdownFile4,
|
|
4738
|
-
readSessionYaml as
|
|
4921
|
+
readSessionYaml as readSessionYaml3,
|
|
4739
4922
|
readTaskFile as readTaskFile2,
|
|
4740
4923
|
renderDecisions as renderDecisions3,
|
|
4741
4924
|
renderHandoff as renderHandoff3
|
|
@@ -5329,7 +5512,7 @@ async function overview(deps) {
|
|
|
5329
5512
|
try {
|
|
5330
5513
|
manifest = await readManifest8(deps.paths);
|
|
5331
5514
|
} catch (error) {
|
|
5332
|
-
if (
|
|
5515
|
+
if (findErrorCode13(error, "ENOENT")) {
|
|
5333
5516
|
return { initialized: false, repoRoot: deps.repoRoot };
|
|
5334
5517
|
}
|
|
5335
5518
|
throw error;
|
|
@@ -5376,7 +5559,7 @@ async function sessionsList(deps) {
|
|
|
5376
5559
|
async function sessionDetail(deps, sessionId) {
|
|
5377
5560
|
let session;
|
|
5378
5561
|
try {
|
|
5379
|
-
session = await
|
|
5562
|
+
session = await readSessionYaml3(deps.paths, sessionId);
|
|
5380
5563
|
} catch (error) {
|
|
5381
5564
|
if (error instanceof Error && error.message === "YAML file not found") {
|
|
5382
5565
|
throw new HttpError(404, "Session not found");
|
|
@@ -5544,8 +5727,8 @@ async function runView(options, ctx = {}) {
|
|
|
5544
5727
|
async function doRunView(options, ctx) {
|
|
5545
5728
|
const cwd = ctx.cwd ?? process.cwd();
|
|
5546
5729
|
const repositoryRoot = await resolveRepositoryRootForView(cwd);
|
|
5547
|
-
const paths =
|
|
5548
|
-
await
|
|
5730
|
+
const paths = basouPaths14(repositoryRoot);
|
|
5731
|
+
await assertWorkspaceInitialized11(paths.root);
|
|
5549
5732
|
const deps = {
|
|
5550
5733
|
paths,
|
|
5551
5734
|
repoRoot: repositoryRoot,
|
|
@@ -5576,7 +5759,7 @@ async function startListening(port, deps) {
|
|
|
5576
5759
|
try {
|
|
5577
5760
|
return await startViewServer({ port, deps });
|
|
5578
5761
|
} catch (error) {
|
|
5579
|
-
if (
|
|
5762
|
+
if (findErrorCode14(error, "EADDRINUSE")) {
|
|
5580
5763
|
throw new Error(`Port ${port} is already in use. Pass --port <n> to choose another.`, {
|
|
5581
5764
|
cause: error
|
|
5582
5765
|
});
|
|
@@ -5627,7 +5810,7 @@ function waitForShutdown(signal) {
|
|
|
5627
5810
|
}
|
|
5628
5811
|
async function resolveRepositoryRootForView(cwd) {
|
|
5629
5812
|
try {
|
|
5630
|
-
return await
|
|
5813
|
+
return await resolveRepositoryRoot15(cwd);
|
|
5631
5814
|
} catch (error) {
|
|
5632
5815
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
5633
5816
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou view'.", {
|
|
@@ -5637,11 +5820,11 @@ async function resolveRepositoryRootForView(cwd) {
|
|
|
5637
5820
|
throw error;
|
|
5638
5821
|
}
|
|
5639
5822
|
}
|
|
5640
|
-
async function
|
|
5823
|
+
async function assertWorkspaceInitialized11(basouRoot) {
|
|
5641
5824
|
try {
|
|
5642
|
-
await
|
|
5825
|
+
await assertBasouRootSafe14(basouRoot);
|
|
5643
5826
|
} catch (error) {
|
|
5644
|
-
if (
|
|
5827
|
+
if (findErrorCode14(error, "ENOENT")) {
|
|
5645
5828
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
5646
5829
|
}
|
|
5647
5830
|
throw error;
|
|
@@ -5663,6 +5846,7 @@ function buildProgram() {
|
|
|
5663
5846
|
registerSessionCommand(program2);
|
|
5664
5847
|
registerImportCommand(program2);
|
|
5665
5848
|
registerRefreshCommand(program2);
|
|
5849
|
+
registerVerifyCommand(program2);
|
|
5666
5850
|
registerViewCommand(program2);
|
|
5667
5851
|
registerApprovalCommand(program2);
|
|
5668
5852
|
registerDecisionCommand(program2);
|