@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 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 readSessionYaml(paths, sessionId);
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 assertBasouRootSafe13, basouPaths as basouPaths13, findErrorCode as findErrorCode13, resolveRepositoryRoot as resolveRepositoryRoot14 } from "@basou/core";
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 findErrorCode12,
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 readSessionYaml2,
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 (findErrorCode12(error, "ENOENT")) {
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 readSessionYaml2(deps.paths, sessionId);
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 = basouPaths13(repositoryRoot);
5548
- await assertWorkspaceInitialized10(paths.root);
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 (findErrorCode13(error, "EADDRINUSE")) {
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 resolveRepositoryRoot14(cwd);
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 assertWorkspaceInitialized10(basouRoot) {
5823
+ async function assertWorkspaceInitialized11(basouRoot) {
5641
5824
  try {
5642
- await assertBasouRootSafe13(basouRoot);
5825
+ await assertBasouRootSafe14(basouRoot);
5643
5826
  } catch (error) {
5644
- if (findErrorCode13(error, "ENOENT")) {
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);