@pleri/olam-cli 0.1.11 → 0.1.13

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.
Files changed (112) hide show
  1. package/dist/__tests__/auth-status.test.js +8 -7
  2. package/dist/__tests__/auth-status.test.js.map +1 -1
  3. package/dist/__tests__/help-output.test.d.ts +2 -0
  4. package/dist/__tests__/help-output.test.d.ts.map +1 -0
  5. package/dist/__tests__/help-output.test.js +74 -0
  6. package/dist/__tests__/help-output.test.js.map +1 -0
  7. package/dist/__tests__/image-presence.test.d.ts +2 -0
  8. package/dist/__tests__/image-presence.test.d.ts.map +1 -0
  9. package/dist/__tests__/image-presence.test.js +44 -0
  10. package/dist/__tests__/image-presence.test.js.map +1 -0
  11. package/dist/__tests__/protocol-version.test.d.ts +2 -0
  12. package/dist/__tests__/protocol-version.test.d.ts.map +1 -0
  13. package/dist/__tests__/protocol-version.test.js +170 -0
  14. package/dist/__tests__/protocol-version.test.js.map +1 -0
  15. package/dist/__tests__/registry-allowlist.test.d.ts +2 -0
  16. package/dist/__tests__/registry-allowlist.test.d.ts.map +1 -0
  17. package/dist/__tests__/registry-allowlist.test.js +129 -0
  18. package/dist/__tests__/registry-allowlist.test.js.map +1 -0
  19. package/dist/commands/__tests__/crystallize.test.d.ts +2 -0
  20. package/dist/commands/__tests__/crystallize.test.d.ts.map +1 -0
  21. package/dist/commands/__tests__/crystallize.test.js +133 -0
  22. package/dist/commands/__tests__/crystallize.test.js.map +1 -0
  23. package/dist/commands/__tests__/upgrade.all-three.test.d.ts +19 -0
  24. package/dist/commands/__tests__/upgrade.all-three.test.d.ts.map +1 -0
  25. package/dist/commands/__tests__/upgrade.all-three.test.js +92 -0
  26. package/dist/commands/__tests__/upgrade.all-three.test.js.map +1 -0
  27. package/dist/commands/__tests__/upgrade.history.test.d.ts +15 -0
  28. package/dist/commands/__tests__/upgrade.history.test.d.ts.map +1 -0
  29. package/dist/commands/__tests__/upgrade.history.test.js +199 -0
  30. package/dist/commands/__tests__/upgrade.history.test.js.map +1 -0
  31. package/dist/commands/__tests__/upgrade.lock.test.d.ts +15 -0
  32. package/dist/commands/__tests__/upgrade.lock.test.d.ts.map +1 -0
  33. package/dist/commands/__tests__/upgrade.lock.test.js +253 -0
  34. package/dist/commands/__tests__/upgrade.lock.test.js.map +1 -0
  35. package/dist/commands/__tests__/upgrade.olam-tag.test.d.ts +21 -0
  36. package/dist/commands/__tests__/upgrade.olam-tag.test.d.ts.map +1 -0
  37. package/dist/commands/__tests__/upgrade.olam-tag.test.js +127 -0
  38. package/dist/commands/__tests__/upgrade.olam-tag.test.js.map +1 -0
  39. package/dist/commands/__tests__/upgrade.poll.test.d.ts +14 -0
  40. package/dist/commands/__tests__/upgrade.poll.test.d.ts.map +1 -0
  41. package/dist/commands/__tests__/upgrade.poll.test.js +136 -0
  42. package/dist/commands/__tests__/upgrade.poll.test.js.map +1 -0
  43. package/dist/commands/__tests__/upgrade.recreate.test.d.ts +17 -0
  44. package/dist/commands/__tests__/upgrade.recreate.test.d.ts.map +1 -0
  45. package/dist/commands/__tests__/upgrade.recreate.test.js +95 -0
  46. package/dist/commands/__tests__/upgrade.recreate.test.js.map +1 -0
  47. package/dist/commands/__tests__/upgrade.rollback.test.d.ts +12 -0
  48. package/dist/commands/__tests__/upgrade.rollback.test.d.ts.map +1 -0
  49. package/dist/commands/__tests__/upgrade.rollback.test.js +275 -0
  50. package/dist/commands/__tests__/upgrade.rollback.test.js.map +1 -0
  51. package/dist/commands/__tests__/upgrade.sha-capture.test.d.ts +12 -0
  52. package/dist/commands/__tests__/upgrade.sha-capture.test.d.ts.map +1 -0
  53. package/dist/commands/__tests__/upgrade.sha-capture.test.js +63 -0
  54. package/dist/commands/__tests__/upgrade.sha-capture.test.js.map +1 -0
  55. package/dist/commands/__tests__/upgrade.smoke.test.d.ts +19 -0
  56. package/dist/commands/__tests__/upgrade.smoke.test.d.ts.map +1 -0
  57. package/dist/commands/__tests__/upgrade.smoke.test.js +101 -0
  58. package/dist/commands/__tests__/upgrade.smoke.test.js.map +1 -0
  59. package/dist/commands/__tests__/upgrade.swap.test.d.ts +19 -0
  60. package/dist/commands/__tests__/upgrade.swap.test.d.ts.map +1 -0
  61. package/dist/commands/__tests__/upgrade.swap.test.js +333 -0
  62. package/dist/commands/__tests__/upgrade.swap.test.js.map +1 -0
  63. package/dist/commands/auth-status.d.ts +8 -1
  64. package/dist/commands/auth-status.d.ts.map +1 -1
  65. package/dist/commands/auth-status.js +2 -1
  66. package/dist/commands/auth-status.js.map +1 -1
  67. package/dist/commands/create.d.ts.map +1 -1
  68. package/dist/commands/create.js +31 -0
  69. package/dist/commands/create.js.map +1 -1
  70. package/dist/commands/crystallize.d.ts +11 -1
  71. package/dist/commands/crystallize.d.ts.map +1 -1
  72. package/dist/commands/crystallize.js +32 -8
  73. package/dist/commands/crystallize.js.map +1 -1
  74. package/dist/commands/upgrade-history.d.ts +17 -0
  75. package/dist/commands/upgrade-history.d.ts.map +1 -0
  76. package/dist/commands/upgrade-history.js +40 -0
  77. package/dist/commands/upgrade-history.js.map +1 -0
  78. package/dist/commands/upgrade-lock.d.ts +102 -0
  79. package/dist/commands/upgrade-lock.d.ts.map +1 -0
  80. package/dist/commands/upgrade-lock.js +225 -0
  81. package/dist/commands/upgrade-lock.js.map +1 -0
  82. package/dist/commands/upgrade-log.d.ts +86 -0
  83. package/dist/commands/upgrade-log.d.ts.map +1 -0
  84. package/dist/commands/upgrade-log.js +146 -0
  85. package/dist/commands/upgrade-log.js.map +1 -0
  86. package/dist/commands/upgrade.d.ts +265 -0
  87. package/dist/commands/upgrade.d.ts.map +1 -1
  88. package/dist/commands/upgrade.js +840 -10
  89. package/dist/commands/upgrade.js.map +1 -1
  90. package/dist/exit-codes.d.ts +35 -0
  91. package/dist/exit-codes.d.ts.map +1 -0
  92. package/dist/exit-codes.js +35 -0
  93. package/dist/exit-codes.js.map +1 -0
  94. package/dist/image-presence.d.ts +40 -0
  95. package/dist/image-presence.d.ts.map +1 -0
  96. package/dist/image-presence.js +39 -0
  97. package/dist/image-presence.js.map +1 -0
  98. package/dist/index.js +1058 -168
  99. package/dist/index.js.map +1 -1
  100. package/dist/pleri-config.d.ts +22 -0
  101. package/dist/pleri-config.d.ts.map +1 -0
  102. package/dist/pleri-config.js +42 -0
  103. package/dist/pleri-config.js.map +1 -0
  104. package/dist/protocol-version.d.ts +79 -0
  105. package/dist/protocol-version.d.ts.map +1 -0
  106. package/dist/protocol-version.js +133 -0
  107. package/dist/protocol-version.js.map +1 -0
  108. package/dist/registry-allowlist.d.ts +47 -0
  109. package/dist/registry-allowlist.d.ts.map +1 -0
  110. package/dist/registry-allowlist.js +67 -0
  111. package/dist/registry-allowlist.js.map +1 -0
  112. package/package.json +1 -1
@@ -8,34 +8,50 @@ import * as path from 'node:path';
8
8
  import ora from 'ora';
9
9
  import { loadContext } from '../context.js';
10
10
  import { printError, printInfo, printHeader } from '../output.js';
11
+ import { EXIT_GENERIC_ERROR, EXIT_PLERI_NOT_CONFIGURED } from '../exit-codes.js';
11
12
  import { getWorldDbPath } from '@olam/core/src/world-paths.js';
12
- export function registerCrystallize(program) {
13
- program
13
+ export function registerCrystallize(program, options = {}) {
14
+ const cmd = program
14
15
  .command('crystallize')
15
16
  .description('Crystallize thoughts from a world to Pleri Plane')
16
17
  .argument('<world>', 'World ID')
17
18
  .action(async (worldId) => {
18
19
  const { ctx, error } = await loadContext();
20
+ // PLERI is by definition not configured if Olam itself isn't initialised
21
+ // (no `.olam/` dir = no `pleri:` block = nothing to crystallize against).
22
+ // The fresh-install scenario this epic targets routes through here, so
23
+ // emit the same skip semantics as the no-pleri-block case below.
24
+ if (!ctx && error?.name === 'OlamConfigNotFoundError') {
25
+ process.stderr.write('warn: crystallize requires PLERI_BASE_URL — skipping (Olam is not initialised; run `olam init` to set up a PLERI block, then re-run).\n');
26
+ process.exitCode = EXIT_PLERI_NOT_CONFIGURED;
27
+ return;
28
+ }
19
29
  if (!ctx) {
20
30
  printError(error?.message ?? 'Olam is not configured. Run `olam init` first.');
21
- process.exitCode = 1;
31
+ process.exitCode = EXIT_GENERIC_ERROR;
22
32
  return;
23
33
  }
24
34
  if (!ctx.pleriClient) {
25
- printError('Pleri Plane is not configured. Add pleri section to .olam/config.yaml.');
26
- process.exitCode = 1;
35
+ // Optional feature: PLERI not configured. Warn to stderr (so piped
36
+ // scripts can ignore via 2>/dev/null), keep stdout empty (so exit-2
37
+ // is distinguishable from exit-0 success without parsing output),
38
+ // and exit with the named constant so pipelines can decide whether
39
+ // to treat the skip as a failure or a no-op.
40
+ process.stderr.write('warn: crystallize requires PLERI_BASE_URL — skipping (no work performed). ' +
41
+ 'Configure pleri in .olam/config.yaml or set PLERI_BASE_URL to enable.\n');
42
+ process.exitCode = EXIT_PLERI_NOT_CONFIGURED;
27
43
  return;
28
44
  }
29
45
  const world = ctx.worldManager.getWorld(worldId);
30
46
  if (!world) {
31
47
  printError(`World "${worldId}" not found.`);
32
- process.exitCode = 1;
48
+ process.exitCode = EXIT_GENERIC_ERROR;
33
49
  return;
34
50
  }
35
51
  const thoughtDbPath = getWorldDbPath(world.workspacePath);
36
52
  if (!fs.existsSync(thoughtDbPath)) {
37
53
  printError(`No thoughts captured yet for "${worldId}". Run a dispatch first.`);
38
- process.exitCode = 1;
54
+ process.exitCode = EXIT_GENERIC_ERROR;
39
55
  return;
40
56
  }
41
57
  const spinner = ora('Crystallizing thoughts...').start();
@@ -94,8 +110,16 @@ export function registerCrystallize(program) {
94
110
  catch (err) {
95
111
  spinner.fail('Crystallization failed');
96
112
  printError(err instanceof Error ? err.message : String(err));
97
- process.exitCode = 1;
113
+ process.exitCode = EXIT_GENERIC_ERROR;
98
114
  }
99
115
  });
116
+ if (options.hidden) {
117
+ // commander v13 omits a Command from parent's help listing when its
118
+ // internal `_hidden` flag is true. The flag is normally set via the
119
+ // `hidden: true` option at `.command(name, desc, opts)` registration
120
+ // time, but we register via the chained shape — set the flag after.
121
+ // Confirmed via node_modules/commander/lib/command.js:165 (`cmd._hidden`).
122
+ cmd._hidden = true;
123
+ }
100
124
  }
101
125
  //# sourceMappingURL=crystallize.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"crystallize.js","sourceRoot":"","sources":["../../src/commands/crystallize.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"crystallize.js","sourceRoot":"","sources":["../../src/commands/crystallize.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `olam upgrade --history` — read + format the JSONL audit log.
3
+ *
4
+ * Phase 2c — C2.
5
+ */
6
+ import type { Command } from 'commander';
7
+ export interface HistoryOpts {
8
+ readonly limit: number;
9
+ readonly json: boolean;
10
+ }
11
+ export declare function parseHistoryOpts(raw: {
12
+ n?: string | number;
13
+ json?: boolean;
14
+ }): HistoryOpts;
15
+ export declare function handleHistory(opts: HistoryOpts): void;
16
+ export declare function registerHistoryFlag(_program: Command): void;
17
+ //# sourceMappingURL=upgrade-history.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upgrade-history.d.ts","sourceRoot":"","sources":["../../src/commands/upgrade-history.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIzC,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE;IAAE,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,WAAW,CAY1F;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAarD;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAK3D"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `olam upgrade --history` — read + format the JSONL audit log.
3
+ *
4
+ * Phase 2c — C2.
5
+ */
6
+ import { readUpgradeLog, formatHistoryTable, formatHistoryJson, UPGRADE_LOG_PATH } from './upgrade-log.js';
7
+ import { printInfo } from '../output.js';
8
+ export function parseHistoryOpts(raw) {
9
+ const rawLimit = raw.n;
10
+ const limit = typeof rawLimit === 'number'
11
+ ? rawLimit
12
+ : typeof rawLimit === 'string'
13
+ ? Number.parseInt(rawLimit, 10)
14
+ : 10;
15
+ return {
16
+ limit: Number.isFinite(limit) && limit > 0 ? limit : 10,
17
+ json: raw.json === true,
18
+ };
19
+ }
20
+ export function handleHistory(opts) {
21
+ const rows = readUpgradeLog(opts.limit);
22
+ if (opts.json) {
23
+ process.stdout.write(formatHistoryJson(rows) + '\n');
24
+ return;
25
+ }
26
+ if (rows.length === 0) {
27
+ printInfo('Log file', UPGRADE_LOG_PATH);
28
+ process.stdout.write(formatHistoryTable(rows) + '\n');
29
+ return;
30
+ }
31
+ printInfo('Log file', UPGRADE_LOG_PATH);
32
+ process.stdout.write(formatHistoryTable(rows) + '\n');
33
+ }
34
+ export function registerHistoryFlag(_program) {
35
+ // History is exposed as `olam upgrade --history` rather than a
36
+ // separate sub-command. The flag is registered alongside the upgrade
37
+ // command's options in registerUpgrade(); this function exists to
38
+ // keep the symbol-export surface consistent with other CLI modules.
39
+ }
40
+ //# sourceMappingURL=upgrade-history.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upgrade-history.js","sourceRoot":"","sources":["../../src/commands/upgrade-history.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAC3G,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAOzC,MAAM,UAAU,gBAAgB,CAAC,GAA4C;IAC3E,MAAM,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC;IACvB,MAAM,KAAK,GACT,OAAO,QAAQ,KAAK,QAAQ;QAC1B,CAAC,CAAC,QAAQ;QACV,CAAC,CAAC,OAAO,QAAQ,KAAK,QAAQ;YAC5B,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,EAAE,EAAE,CAAC;YAC/B,CAAC,CAAC,EAAE,CAAC;IACX,OAAO;QACL,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;QACvD,IAAI,EAAE,GAAG,CAAC,IAAI,KAAK,IAAI;KACxB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,IAAiB;IAC7C,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACxC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QACd,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;QACrD,OAAO;IACT,CAAC;IACD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,SAAS,CAAC,UAAU,EAAE,gBAAgB,CAAC,CAAC;QACxC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,kBAAkB,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;QACtD,OAAO;IACT,CAAC;IACD,SAAS,CAAC,UAAU,EAAE,gBAAgB,CAAC,CAAC;IACxC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,kBAAkB,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;AACxD,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,QAAiB;IACnD,+DAA+D;IAC/D,qEAAqE;IACrE,kEAAkE;IAClE,oEAAoE;AACtE,CAAC"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * `olam upgrade` CLI lock — atomic create-or-fail with stale-lock recovery.
3
+ *
4
+ * Lock file: ~/.olam/.upgrade.lock
5
+ * Contents: {"pid": <int>, "startTs": <epoch-ms>}
6
+ *
7
+ * Stale-lock semantics:
8
+ * - File unreadable / parse error / empty → stale
9
+ * - PID not alive (process.kill(pid, 0) throws) → stale
10
+ * - PID alive but command name is not node/olam → stale (avoids macOS PID-recycling false-positives)
11
+ * - start_ts > 30 min ago → stale
12
+ * - Otherwise → live; second invocation refuses with exit 1.
13
+ *
14
+ * Atomicity: fs.openSync(path, 'wx') is atomic create-or-fail per POSIX. No TOCTOU window.
15
+ */
16
+ export declare const LOCK_FILE_PATH: string;
17
+ export declare const STALE_LOCK_TIMEOUT_MS: number;
18
+ export interface LockContent {
19
+ readonly pid: number;
20
+ readonly startTs: number;
21
+ }
22
+ export type AcquireResult = {
23
+ acquired: true;
24
+ lockPath: string;
25
+ } | {
26
+ acquired: false;
27
+ reason: 'live' | 'race';
28
+ existingPid?: number;
29
+ existingStartTs?: number;
30
+ };
31
+ /**
32
+ * Read lock file contents. Returns null on:
33
+ * - file missing
34
+ * - empty file
35
+ * - JSON parse error
36
+ * - shape mismatch (missing pid or startTs as numbers)
37
+ *
38
+ * Caller treats null as "stale lock; can recover."
39
+ */
40
+ export declare function readLockFile(lockPath: string): LockContent | null;
41
+ /** Returns true if process exists, false on ESRCH or any other error. */
42
+ export declare function isPidAlive(pid: number): boolean;
43
+ /**
44
+ * Sentinel returned when `ps` itself is unavailable (binary missing, fork
45
+ * pressure, container without procfs). Distinct from "ps ran but the PID
46
+ * doesn't exist" which returns the empty string equivalent (null).
47
+ *
48
+ * Callers MUST treat this sentinel as "command unknown — assume live, refuse
49
+ * to recover" so a missing `ps` binary cannot stealth-break the lock.
50
+ *
51
+ * Per audit A1-003: previously returned null on both branches and isStaleLock
52
+ * treated null as stale, which let `ps`-unavailable invocations silently
53
+ * unlink a live olam upgrade's lock and corrupt the docker layer cache.
54
+ */
55
+ export declare const PS_UNAVAILABLE = "__ps_unavailable__";
56
+ /**
57
+ * Read the command name of a PID via `ps -p <pid> -o comm=`.
58
+ *
59
+ * Returns:
60
+ * - The command name (e.g. 'node', 'olam') when `ps` ran AND the PID exists.
61
+ * - null when `ps` ran but the PID does not exist (ps exit status 1; output empty).
62
+ * - PS_UNAVAILABLE sentinel when `ps` failed to spawn (ENOENT, EAGAIN, signalled).
63
+ */
64
+ export declare function getPidCommand(pid: number): string | null | typeof PS_UNAVAILABLE;
65
+ /**
66
+ * Match command names that legitimately hold the upgrade lock.
67
+ *
68
+ * Accepts:
69
+ * - `node`, `olam`, `olam-cli` (canonical)
70
+ * - path-prefixed forms (`/usr/local/bin/node` — strip via basename)
71
+ * - worker-pool suffixed forms (`node (vitest 1)` — strip parenthesized suffix)
72
+ *
73
+ * Rejects unrelated commands (bash, zsh, python3, etc.) so PID recycling to a
74
+ * non-node process correctly classifies the lock as stale.
75
+ */
76
+ export declare function isOlamUpgradeCommand(comm: string | null | typeof PS_UNAVAILABLE): boolean;
77
+ /**
78
+ * Stale-lock test. Returns true if the lock should be recovered (deleted + retried).
79
+ *
80
+ * Inputs:
81
+ * - content: parsed lock contents (null → empty/parse-error/missing → stale)
82
+ * - nowMs: current epoch ms (defaults to Date.now(); injected for test determinism)
83
+ */
84
+ export declare function isStaleLock(content: LockContent | null, nowMs?: number): boolean;
85
+ /**
86
+ * Atomically acquire the upgrade lock.
87
+ *
88
+ * Returns { acquired: true } on success, or { acquired: false, reason } when a live lock exists.
89
+ * Stale locks are auto-recovered (delete + retry once).
90
+ *
91
+ * Side effects:
92
+ * - mkdir -p path.dirname(lockPath) (ensures ~/.olam exists)
93
+ * - writes JSON {pid, startTs} to lockPath
94
+ */
95
+ export declare function acquireLock(lockPath?: string, nowMs?: number): AcquireResult;
96
+ /** Release the lock. Idempotent — no error if file already removed. */
97
+ export declare function releaseLock(lockPath?: string): void;
98
+ /** Format a human-readable refusal message for the operator. */
99
+ export declare function formatRefusalMessage(result: Extract<AcquireResult, {
100
+ acquired: false;
101
+ }>, lockPath?: string): string;
102
+ //# sourceMappingURL=upgrade-lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upgrade-lock.d.ts","sourceRoot":"","sources":["../../src/commands/upgrade-lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAOH,eAAO,MAAM,cAAc,QAAoD,CAAC;AAKhF,eAAO,MAAM,qBAAqB,QAAgB,CAAC;AAEnD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACpC;IACE,QAAQ,EAAE,KAAK,CAAC;IAChB,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEN;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAWjE;AAED,yEAAyE;AACzE,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAO/C;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,cAAc,uBAAuB,CAAC;AAEnD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,OAAO,cAAc,CAWhF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,cAAc,GAAG,OAAO,CAMzF;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,EAAE,KAAK,GAAE,MAAmB,GAAG,OAAO,CAY5F;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CACzB,QAAQ,GAAE,MAAuB,EACjC,KAAK,GAAE,MAAmB,GACzB,aAAa,CAkDf;AAED,uEAAuE;AACvE,wBAAgB,WAAW,CAAC,QAAQ,GAAE,MAAuB,GAAG,IAAI,CAOnE;AAED,gEAAgE;AAChE,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,OAAO,CAAC,aAAa,EAAE;IAAE,QAAQ,EAAE,KAAK,CAAA;CAAE,CAAC,EACnD,QAAQ,GAAE,MAAuB,GAChC,MAAM,CASR"}
@@ -0,0 +1,225 @@
1
+ /**
2
+ * `olam upgrade` CLI lock — atomic create-or-fail with stale-lock recovery.
3
+ *
4
+ * Lock file: ~/.olam/.upgrade.lock
5
+ * Contents: {"pid": <int>, "startTs": <epoch-ms>}
6
+ *
7
+ * Stale-lock semantics:
8
+ * - File unreadable / parse error / empty → stale
9
+ * - PID not alive (process.kill(pid, 0) throws) → stale
10
+ * - PID alive but command name is not node/olam → stale (avoids macOS PID-recycling false-positives)
11
+ * - start_ts > 30 min ago → stale
12
+ * - Otherwise → live; second invocation refuses with exit 1.
13
+ *
14
+ * Atomicity: fs.openSync(path, 'wx') is atomic create-or-fail per POSIX. No TOCTOU window.
15
+ */
16
+ import * as fs from 'node:fs';
17
+ import * as os from 'node:os';
18
+ import * as path from 'node:path';
19
+ import { spawnSync } from 'node:child_process';
20
+ export const LOCK_FILE_PATH = path.join(os.homedir(), '.olam', '.upgrade.lock');
21
+ // 5 min — bounds the false-refusal blast-radius after a crash. Real upgrades
22
+ // should always finish well within this window (3-22 min spec; if a build
23
+ // genuinely runs longer the operator gets a 'wait or rm' message, which is
24
+ // fine UX). Per audit A1-004: was 30 min; shortened to 5 min.
25
+ export const STALE_LOCK_TIMEOUT_MS = 5 * 60 * 1000;
26
+ /**
27
+ * Read lock file contents. Returns null on:
28
+ * - file missing
29
+ * - empty file
30
+ * - JSON parse error
31
+ * - shape mismatch (missing pid or startTs as numbers)
32
+ *
33
+ * Caller treats null as "stale lock; can recover."
34
+ */
35
+ export function readLockFile(lockPath) {
36
+ try {
37
+ if (!fs.existsSync(lockPath))
38
+ return null;
39
+ const raw = fs.readFileSync(lockPath, 'utf-8').trim();
40
+ if (raw.length === 0)
41
+ return null;
42
+ const parsed = JSON.parse(raw);
43
+ if (typeof parsed.pid !== 'number' || typeof parsed.startTs !== 'number')
44
+ return null;
45
+ return { pid: parsed.pid, startTs: parsed.startTs };
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ /** Returns true if process exists, false on ESRCH or any other error. */
52
+ export function isPidAlive(pid) {
53
+ try {
54
+ process.kill(pid, 0);
55
+ return true;
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ }
61
+ /**
62
+ * Sentinel returned when `ps` itself is unavailable (binary missing, fork
63
+ * pressure, container without procfs). Distinct from "ps ran but the PID
64
+ * doesn't exist" which returns the empty string equivalent (null).
65
+ *
66
+ * Callers MUST treat this sentinel as "command unknown — assume live, refuse
67
+ * to recover" so a missing `ps` binary cannot stealth-break the lock.
68
+ *
69
+ * Per audit A1-003: previously returned null on both branches and isStaleLock
70
+ * treated null as stale, which let `ps`-unavailable invocations silently
71
+ * unlink a live olam upgrade's lock and corrupt the docker layer cache.
72
+ */
73
+ export const PS_UNAVAILABLE = '__ps_unavailable__';
74
+ /**
75
+ * Read the command name of a PID via `ps -p <pid> -o comm=`.
76
+ *
77
+ * Returns:
78
+ * - The command name (e.g. 'node', 'olam') when `ps` ran AND the PID exists.
79
+ * - null when `ps` ran but the PID does not exist (ps exit status 1; output empty).
80
+ * - PS_UNAVAILABLE sentinel when `ps` failed to spawn (ENOENT, EAGAIN, signalled).
81
+ */
82
+ export function getPidCommand(pid) {
83
+ const result = spawnSync('ps', ['-p', String(pid), '-o', 'comm='], {
84
+ encoding: 'utf-8',
85
+ stdio: ['ignore', 'pipe', 'ignore'],
86
+ });
87
+ // Spawn-level failure (status === null + error populated; binary missing,
88
+ // fork pressure, killed by signal). Distinct from "ps reported pid absent."
89
+ if (result.status === null || result.error !== undefined)
90
+ return PS_UNAVAILABLE;
91
+ if (result.status !== 0)
92
+ return null;
93
+ const out = result.stdout.trim();
94
+ return out.length === 0 ? null : out;
95
+ }
96
+ /**
97
+ * Match command names that legitimately hold the upgrade lock.
98
+ *
99
+ * Accepts:
100
+ * - `node`, `olam`, `olam-cli` (canonical)
101
+ * - path-prefixed forms (`/usr/local/bin/node` — strip via basename)
102
+ * - worker-pool suffixed forms (`node (vitest 1)` — strip parenthesized suffix)
103
+ *
104
+ * Rejects unrelated commands (bash, zsh, python3, etc.) so PID recycling to a
105
+ * non-node process correctly classifies the lock as stale.
106
+ */
107
+ export function isOlamUpgradeCommand(comm) {
108
+ if (!comm)
109
+ return false;
110
+ if (comm === PS_UNAVAILABLE)
111
+ return false;
112
+ const base = comm.split('/').pop() ?? comm;
113
+ const stripped = base.replace(/\s*\(.*\)\s*$/, '').trim();
114
+ return stripped === 'node' || stripped === 'olam' || stripped === 'olam-cli';
115
+ }
116
+ /**
117
+ * Stale-lock test. Returns true if the lock should be recovered (deleted + retried).
118
+ *
119
+ * Inputs:
120
+ * - content: parsed lock contents (null → empty/parse-error/missing → stale)
121
+ * - nowMs: current epoch ms (defaults to Date.now(); injected for test determinism)
122
+ */
123
+ export function isStaleLock(content, nowMs = Date.now()) {
124
+ if (!content)
125
+ return true;
126
+ if (nowMs - content.startTs > STALE_LOCK_TIMEOUT_MS)
127
+ return true;
128
+ if (!isPidAlive(content.pid))
129
+ return true;
130
+ const comm = getPidCommand(content.pid);
131
+ // Audit A1-003 fail-live invariant: if `ps` is unavailable we cannot prove
132
+ // the PID isn't an olam process. Treat as live (not stale) — operator gets
133
+ // a refusal message and can `rm ~/.olam/.upgrade.lock` if they're confident
134
+ // the lock is stale. Better than silently deleting a live lock.
135
+ if (comm === PS_UNAVAILABLE)
136
+ return false;
137
+ if (!isOlamUpgradeCommand(comm))
138
+ return true;
139
+ return false;
140
+ }
141
+ /**
142
+ * Atomically acquire the upgrade lock.
143
+ *
144
+ * Returns { acquired: true } on success, or { acquired: false, reason } when a live lock exists.
145
+ * Stale locks are auto-recovered (delete + retry once).
146
+ *
147
+ * Side effects:
148
+ * - mkdir -p path.dirname(lockPath) (ensures ~/.olam exists)
149
+ * - writes JSON {pid, startTs} to lockPath
150
+ */
151
+ export function acquireLock(lockPath = LOCK_FILE_PATH, nowMs = Date.now()) {
152
+ const dir = path.dirname(lockPath);
153
+ fs.mkdirSync(dir, { recursive: true });
154
+ for (let attempt = 0; attempt < 2; attempt++) {
155
+ try {
156
+ const fd = fs.openSync(lockPath, 'wx', 0o644);
157
+ try {
158
+ const content = { pid: process.pid, startTs: nowMs };
159
+ fs.writeSync(fd, JSON.stringify(content));
160
+ }
161
+ finally {
162
+ fs.closeSync(fd);
163
+ }
164
+ return { acquired: true, lockPath };
165
+ }
166
+ catch (err) {
167
+ const code = err.code;
168
+ if (code !== 'EEXIST')
169
+ throw err;
170
+ const existing = readLockFile(lockPath);
171
+ if (isStaleLock(existing, nowMs)) {
172
+ try {
173
+ fs.unlinkSync(lockPath);
174
+ }
175
+ catch (unlinkErr) {
176
+ const ucode = unlinkErr.code;
177
+ if (ucode !== 'ENOENT')
178
+ throw unlinkErr;
179
+ }
180
+ continue;
181
+ }
182
+ return {
183
+ acquired: false,
184
+ reason: 'live',
185
+ ...(existing?.pid !== undefined && { existingPid: existing.pid }),
186
+ ...(existing?.startTs !== undefined && { existingStartTs: existing.startTs }),
187
+ };
188
+ }
189
+ }
190
+ // Both retry attempts hit a non-stale lock. Per audit A1-001: surface this
191
+ // as 'live' (the user's correct response is "wait or rm if stale"), not
192
+ // 'race' (which sounded like a transient that warrants retry). Operationally
193
+ // identical — both branches return acquired:false — but the message text
194
+ // matches the operator's mental model.
195
+ const existing = readLockFile(lockPath);
196
+ return {
197
+ acquired: false,
198
+ reason: 'live',
199
+ ...(existing?.pid !== undefined && { existingPid: existing.pid }),
200
+ ...(existing?.startTs !== undefined && { existingStartTs: existing.startTs }),
201
+ };
202
+ }
203
+ /** Release the lock. Idempotent — no error if file already removed. */
204
+ export function releaseLock(lockPath = LOCK_FILE_PATH) {
205
+ try {
206
+ fs.unlinkSync(lockPath);
207
+ }
208
+ catch (err) {
209
+ const code = err.code;
210
+ if (code !== 'ENOENT')
211
+ throw err;
212
+ }
213
+ }
214
+ /** Format a human-readable refusal message for the operator. */
215
+ export function formatRefusalMessage(result, lockPath = LOCK_FILE_PATH) {
216
+ const pidStr = result.existingPid !== undefined ? ` (pid ${result.existingPid})` : '';
217
+ const lines = [
218
+ `Upgrade in progress${pidStr}.`,
219
+ 'Wait for the running upgrade to finish, or:',
220
+ ' - Check progress: olam upgrade --history',
221
+ ` - If stale (crashed CLI): rm ${lockPath}`,
222
+ ];
223
+ return lines.join('\n');
224
+ }
225
+ //# sourceMappingURL=upgrade-lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upgrade-lock.js","sourceRoot":"","sources":["../../src/commands/upgrade-lock.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * `~/.olam/upgrade.log` — JSONL append-only audit log for `olam upgrade`.
3
+ *
4
+ * Phase 2c — C1.
5
+ *
6
+ * Schema:
7
+ * {
8
+ * ts: ISO 8601,
9
+ * started_at: epoch ms,
10
+ * ended_at: epoch ms,
11
+ * sha_target: string (40-char SHA, captured-after-pull),
12
+ * sha_before: { hostCp, authService, devbox },
13
+ * sha_after: { hostCp, authService, devbox },
14
+ * status: "success" | "failed" | "rolled_back",
15
+ * failed_step: string | null,
16
+ * durations_ms: { [step_label]: number }
17
+ * }
18
+ *
19
+ * Open-per-write semantics — open() + write() + close() per row so
20
+ * external log-rotators can rename the file mid-run without us writing
21
+ * to a now-unlinked fd. Best-effort; errors logged to stderr and
22
+ * swallowed so the audit log never blocks an upgrade from completing.
23
+ *
24
+ * `failed_step` MUST hold a step LABEL only (e.g. "bash build-auth.sh"),
25
+ * NEVER raw stdout/stderr — paths and credentials may leak. (Per audit
26
+ * security finding A6-007.) The CLI's terminal already shows the full
27
+ * stderr; the log row records the step name for queryability.
28
+ */
29
+ /**
30
+ * Resolve the upgrade-log path lazily so tests can override HOME via
31
+ * process.env. Lazy resolution is also forward-compatible with operators
32
+ * who set XDG_DATA_HOME or similar overrides at session start.
33
+ */
34
+ export declare function getUpgradeLogPath(): string;
35
+ /** Convenience constant for callers who want the production path string. */
36
+ export declare const UPGRADE_LOG_PATH: string;
37
+ export interface UpgradeLogRow {
38
+ readonly ts: string;
39
+ readonly started_at: number;
40
+ readonly ended_at: number;
41
+ readonly sha_target: string;
42
+ readonly sha_before?: {
43
+ readonly hostCp?: string;
44
+ readonly authService?: string;
45
+ readonly devbox?: string;
46
+ };
47
+ readonly sha_after?: {
48
+ readonly hostCp?: string;
49
+ readonly authService?: string;
50
+ readonly devbox?: string;
51
+ };
52
+ readonly status: 'success' | 'failed' | 'rolled_back';
53
+ readonly failed_step: string | null;
54
+ readonly durations_ms: Record<string, number>;
55
+ }
56
+ /**
57
+ * Append a single row to ~/.olam/upgrade.log.
58
+ *
59
+ * Open-per-write to survive log rotation. Errors swallowed to stderr —
60
+ * audit log failure must never block an upgrade.
61
+ *
62
+ * `logPath` parameter is a test seam; production callers omit it.
63
+ */
64
+ export declare function appendUpgradeLog(row: UpgradeLogRow, logPath?: string): void;
65
+ /**
66
+ * Read up to N most-recent rows from ~/.olam/upgrade.log.
67
+ *
68
+ * Returns an empty array on missing file (first-run UX).
69
+ *
70
+ * Per audit invariant: corrupt JSON lines (partial mid-write, manual
71
+ * tampering) are SKIPPED with a stderr warning rather than crashing
72
+ * `--history`.
73
+ */
74
+ export declare function readUpgradeLog(limit?: number, logPath?: string): UpgradeLogRow[];
75
+ /** Format duration ms → "1.4s" / "12m04s" / "1h23m" for table display. */
76
+ export declare function formatDuration(ms: number): string;
77
+ /**
78
+ * Format an array of upgrade-log rows as a 5-column ASCII table:
79
+ * timestamp | sha (8-char) | status | duration | failed_step
80
+ *
81
+ * Status column uses ✓/✗/↩ icons + plain text for grep-friendliness.
82
+ */
83
+ export declare function formatHistoryTable(rows: ReadonlyArray<UpgradeLogRow>): string;
84
+ /** Format rows as one JSON object per line (JSONL passthrough — same as on-disk format). */
85
+ export declare function formatHistoryJson(rows: ReadonlyArray<UpgradeLogRow>): string;
86
+ //# sourceMappingURL=upgrade-log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upgrade-log.d.ts","sourceRoot":"","sources":["../../src/commands/upgrade-log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAMH;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAG1C;AAED,4EAA4E;AAC5E,eAAO,MAAM,gBAAgB,QAAsB,CAAC;AAEpD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,CAAC,EAAE;QACpB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAC9B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;KAC1B,CAAC;IACF,QAAQ,CAAC,SAAS,CAAC,EAAE;QACnB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAC9B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;KAC1B,CAAC;IACF,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,aAAa,CAAC;IACtD,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,EAAE,OAAO,GAAE,MAA4B,GAAG,IAAI,CAUhG;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,KAAK,GAAE,MAAW,EAAE,OAAO,GAAE,MAA4B,GAAG,aAAa,EAAE,CAmCzG;AAED,0EAA0E;AAC1E,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAUjD;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,aAAa,CAAC,aAAa,CAAC,GAAG,MAAM,CAmB7E;AAED,4FAA4F;AAC5F,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,aAAa,CAAC,aAAa,CAAC,GAAG,MAAM,CAE5E"}