@pleri/olam-cli 0.1.12 → 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 (85) hide show
  1. package/dist/__tests__/image-presence.test.d.ts +2 -0
  2. package/dist/__tests__/image-presence.test.d.ts.map +1 -0
  3. package/dist/__tests__/image-presence.test.js +44 -0
  4. package/dist/__tests__/image-presence.test.js.map +1 -0
  5. package/dist/__tests__/protocol-version.test.d.ts +2 -0
  6. package/dist/__tests__/protocol-version.test.d.ts.map +1 -0
  7. package/dist/__tests__/protocol-version.test.js +170 -0
  8. package/dist/__tests__/protocol-version.test.js.map +1 -0
  9. package/dist/__tests__/registry-allowlist.test.d.ts +2 -0
  10. package/dist/__tests__/registry-allowlist.test.d.ts.map +1 -0
  11. package/dist/__tests__/registry-allowlist.test.js +129 -0
  12. package/dist/__tests__/registry-allowlist.test.js.map +1 -0
  13. package/dist/commands/__tests__/upgrade.all-three.test.d.ts +19 -0
  14. package/dist/commands/__tests__/upgrade.all-three.test.d.ts.map +1 -0
  15. package/dist/commands/__tests__/upgrade.all-three.test.js +92 -0
  16. package/dist/commands/__tests__/upgrade.all-three.test.js.map +1 -0
  17. package/dist/commands/__tests__/upgrade.history.test.d.ts +15 -0
  18. package/dist/commands/__tests__/upgrade.history.test.d.ts.map +1 -0
  19. package/dist/commands/__tests__/upgrade.history.test.js +199 -0
  20. package/dist/commands/__tests__/upgrade.history.test.js.map +1 -0
  21. package/dist/commands/__tests__/upgrade.lock.test.d.ts +15 -0
  22. package/dist/commands/__tests__/upgrade.lock.test.d.ts.map +1 -0
  23. package/dist/commands/__tests__/upgrade.lock.test.js +253 -0
  24. package/dist/commands/__tests__/upgrade.lock.test.js.map +1 -0
  25. package/dist/commands/__tests__/upgrade.olam-tag.test.d.ts +21 -0
  26. package/dist/commands/__tests__/upgrade.olam-tag.test.d.ts.map +1 -0
  27. package/dist/commands/__tests__/upgrade.olam-tag.test.js +127 -0
  28. package/dist/commands/__tests__/upgrade.olam-tag.test.js.map +1 -0
  29. package/dist/commands/__tests__/upgrade.poll.test.d.ts +14 -0
  30. package/dist/commands/__tests__/upgrade.poll.test.d.ts.map +1 -0
  31. package/dist/commands/__tests__/upgrade.poll.test.js +136 -0
  32. package/dist/commands/__tests__/upgrade.poll.test.js.map +1 -0
  33. package/dist/commands/__tests__/upgrade.recreate.test.d.ts +17 -0
  34. package/dist/commands/__tests__/upgrade.recreate.test.d.ts.map +1 -0
  35. package/dist/commands/__tests__/upgrade.recreate.test.js +95 -0
  36. package/dist/commands/__tests__/upgrade.recreate.test.js.map +1 -0
  37. package/dist/commands/__tests__/upgrade.rollback.test.d.ts +12 -0
  38. package/dist/commands/__tests__/upgrade.rollback.test.d.ts.map +1 -0
  39. package/dist/commands/__tests__/upgrade.rollback.test.js +275 -0
  40. package/dist/commands/__tests__/upgrade.rollback.test.js.map +1 -0
  41. package/dist/commands/__tests__/upgrade.sha-capture.test.d.ts +12 -0
  42. package/dist/commands/__tests__/upgrade.sha-capture.test.d.ts.map +1 -0
  43. package/dist/commands/__tests__/upgrade.sha-capture.test.js +63 -0
  44. package/dist/commands/__tests__/upgrade.sha-capture.test.js.map +1 -0
  45. package/dist/commands/__tests__/upgrade.smoke.test.d.ts +19 -0
  46. package/dist/commands/__tests__/upgrade.smoke.test.d.ts.map +1 -0
  47. package/dist/commands/__tests__/upgrade.smoke.test.js +101 -0
  48. package/dist/commands/__tests__/upgrade.smoke.test.js.map +1 -0
  49. package/dist/commands/__tests__/upgrade.swap.test.d.ts +19 -0
  50. package/dist/commands/__tests__/upgrade.swap.test.d.ts.map +1 -0
  51. package/dist/commands/__tests__/upgrade.swap.test.js +333 -0
  52. package/dist/commands/__tests__/upgrade.swap.test.js.map +1 -0
  53. package/dist/commands/create.d.ts.map +1 -1
  54. package/dist/commands/create.js +31 -0
  55. package/dist/commands/create.js.map +1 -1
  56. package/dist/commands/upgrade-history.d.ts +17 -0
  57. package/dist/commands/upgrade-history.d.ts.map +1 -0
  58. package/dist/commands/upgrade-history.js +40 -0
  59. package/dist/commands/upgrade-history.js.map +1 -0
  60. package/dist/commands/upgrade-lock.d.ts +102 -0
  61. package/dist/commands/upgrade-lock.d.ts.map +1 -0
  62. package/dist/commands/upgrade-lock.js +225 -0
  63. package/dist/commands/upgrade-lock.js.map +1 -0
  64. package/dist/commands/upgrade-log.d.ts +86 -0
  65. package/dist/commands/upgrade-log.d.ts.map +1 -0
  66. package/dist/commands/upgrade-log.js +146 -0
  67. package/dist/commands/upgrade-log.js.map +1 -0
  68. package/dist/commands/upgrade.d.ts +265 -0
  69. package/dist/commands/upgrade.d.ts.map +1 -1
  70. package/dist/commands/upgrade.js +840 -10
  71. package/dist/commands/upgrade.js.map +1 -1
  72. package/dist/image-presence.d.ts +40 -0
  73. package/dist/image-presence.d.ts.map +1 -0
  74. package/dist/image-presence.js +39 -0
  75. package/dist/image-presence.js.map +1 -0
  76. package/dist/index.js +1015 -163
  77. package/dist/protocol-version.d.ts +79 -0
  78. package/dist/protocol-version.d.ts.map +1 -0
  79. package/dist/protocol-version.js +133 -0
  80. package/dist/protocol-version.js.map +1 -0
  81. package/dist/registry-allowlist.d.ts +47 -0
  82. package/dist/registry-allowlist.d.ts.map +1 -0
  83. package/dist/registry-allowlist.js +67 -0
  84. package/dist/registry-allowlist.js.map +1 -0
  85. package/package.json +1 -1
@@ -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"}
@@ -0,0 +1,146 @@
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
+ import * as fs from 'node:fs';
30
+ import * as os from 'node:os';
31
+ import * as path from 'node:path';
32
+ /**
33
+ * Resolve the upgrade-log path lazily so tests can override HOME via
34
+ * process.env. Lazy resolution is also forward-compatible with operators
35
+ * who set XDG_DATA_HOME or similar overrides at session start.
36
+ */
37
+ export function getUpgradeLogPath() {
38
+ const home = process.env['HOME'] ?? os.homedir();
39
+ return path.join(home, '.olam', 'upgrade.log');
40
+ }
41
+ /** Convenience constant for callers who want the production path string. */
42
+ export const UPGRADE_LOG_PATH = getUpgradeLogPath();
43
+ /**
44
+ * Append a single row to ~/.olam/upgrade.log.
45
+ *
46
+ * Open-per-write to survive log rotation. Errors swallowed to stderr —
47
+ * audit log failure must never block an upgrade.
48
+ *
49
+ * `logPath` parameter is a test seam; production callers omit it.
50
+ */
51
+ export function appendUpgradeLog(row, logPath = getUpgradeLogPath()) {
52
+ try {
53
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
54
+ const line = JSON.stringify(row) + '\n';
55
+ fs.appendFileSync(logPath, line, { mode: 0o644 });
56
+ }
57
+ catch (err) {
58
+ process.stderr.write(`[upgrade-log] failed to append: ${err instanceof Error ? err.message : String(err)}\n`);
59
+ }
60
+ }
61
+ /**
62
+ * Read up to N most-recent rows from ~/.olam/upgrade.log.
63
+ *
64
+ * Returns an empty array on missing file (first-run UX).
65
+ *
66
+ * Per audit invariant: corrupt JSON lines (partial mid-write, manual
67
+ * tampering) are SKIPPED with a stderr warning rather than crashing
68
+ * `--history`.
69
+ */
70
+ export function readUpgradeLog(limit = 10, logPath = getUpgradeLogPath()) {
71
+ if (!fs.existsSync(logPath))
72
+ return [];
73
+ let raw;
74
+ try {
75
+ raw = fs.readFileSync(logPath, 'utf-8');
76
+ }
77
+ catch (err) {
78
+ process.stderr.write(`[upgrade-log] failed to read: ${err instanceof Error ? err.message : String(err)}\n`);
79
+ return [];
80
+ }
81
+ const lines = raw.split('\n').filter((l) => l.length > 0);
82
+ const rows = [];
83
+ for (let i = 0; i < lines.length; i++) {
84
+ const line = lines[i];
85
+ try {
86
+ const parsed = JSON.parse(line);
87
+ // Defensive shape check — silently skip rows that don't look right.
88
+ if (typeof parsed.ts === 'string' &&
89
+ typeof parsed.started_at === 'number' &&
90
+ typeof parsed.status === 'string') {
91
+ rows.push(parsed);
92
+ }
93
+ else {
94
+ process.stderr.write(`[upgrade-log] skipped malformed row at line ${i + 1}\n`);
95
+ }
96
+ }
97
+ catch {
98
+ process.stderr.write(`[upgrade-log] skipped corrupt JSON at line ${i + 1}\n`);
99
+ }
100
+ }
101
+ // Most recent last (file is chronological); return last N.
102
+ return rows.slice(-Math.max(0, limit));
103
+ }
104
+ /** Format duration ms → "1.4s" / "12m04s" / "1h23m" for table display. */
105
+ export function formatDuration(ms) {
106
+ if (ms < 1000)
107
+ return `${ms}ms`;
108
+ const totalSec = Math.round(ms / 1000);
109
+ if (totalSec < 60)
110
+ return `${totalSec}s`;
111
+ const min = Math.floor(totalSec / 60);
112
+ const sec = totalSec % 60;
113
+ if (min < 60)
114
+ return `${min}m${String(sec).padStart(2, '0')}s`;
115
+ const hr = Math.floor(min / 60);
116
+ const remMin = min % 60;
117
+ return `${hr}h${String(remMin).padStart(2, '0')}m`;
118
+ }
119
+ /**
120
+ * Format an array of upgrade-log rows as a 5-column ASCII table:
121
+ * timestamp | sha (8-char) | status | duration | failed_step
122
+ *
123
+ * Status column uses ✓/✗/↩ icons + plain text for grep-friendliness.
124
+ */
125
+ export function formatHistoryTable(rows) {
126
+ if (rows.length === 0) {
127
+ return 'No upgrade history yet. Run `olam upgrade` to create your first record.';
128
+ }
129
+ const lines = [];
130
+ lines.push('TIMESTAMP SHA STATUS DURATION FAILED-STEP');
131
+ lines.push('────────────────────────────────────────────────────────────────────────────────');
132
+ for (const r of rows) {
133
+ const ts = r.ts.slice(0, 19).replace('T', ' ');
134
+ const sha = r.sha_target.slice(0, 8);
135
+ const statusIcon = r.status === 'success' ? '✓ success' : r.status === 'rolled_back' ? '↩ rolled_back' : '✗ failed';
136
+ const dur = formatDuration(r.ended_at - r.started_at);
137
+ const failed = r.failed_step ?? '';
138
+ lines.push(`${ts.padEnd(28)}${sha.padEnd(10)}${statusIcon.padEnd(15)}${dur.padEnd(11)}${failed}`);
139
+ }
140
+ return lines.join('\n');
141
+ }
142
+ /** Format rows as one JSON object per line (JSONL passthrough — same as on-disk format). */
143
+ export function formatHistoryJson(rows) {
144
+ return rows.map((r) => JSON.stringify(r)).join('\n');
145
+ }
146
+ //# sourceMappingURL=upgrade-log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upgrade-log.js","sourceRoot":"","sources":["../../src/commands/upgrade-log.ts"],"names":[],"mappings":"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"}