@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.
- package/dist/__tests__/image-presence.test.d.ts +2 -0
- package/dist/__tests__/image-presence.test.d.ts.map +1 -0
- package/dist/__tests__/image-presence.test.js +44 -0
- package/dist/__tests__/image-presence.test.js.map +1 -0
- package/dist/__tests__/protocol-version.test.d.ts +2 -0
- package/dist/__tests__/protocol-version.test.d.ts.map +1 -0
- package/dist/__tests__/protocol-version.test.js +170 -0
- package/dist/__tests__/protocol-version.test.js.map +1 -0
- package/dist/__tests__/registry-allowlist.test.d.ts +2 -0
- package/dist/__tests__/registry-allowlist.test.d.ts.map +1 -0
- package/dist/__tests__/registry-allowlist.test.js +129 -0
- package/dist/__tests__/registry-allowlist.test.js.map +1 -0
- package/dist/commands/__tests__/upgrade.all-three.test.d.ts +19 -0
- package/dist/commands/__tests__/upgrade.all-three.test.d.ts.map +1 -0
- package/dist/commands/__tests__/upgrade.all-three.test.js +92 -0
- package/dist/commands/__tests__/upgrade.all-three.test.js.map +1 -0
- package/dist/commands/__tests__/upgrade.history.test.d.ts +15 -0
- package/dist/commands/__tests__/upgrade.history.test.d.ts.map +1 -0
- package/dist/commands/__tests__/upgrade.history.test.js +199 -0
- package/dist/commands/__tests__/upgrade.history.test.js.map +1 -0
- package/dist/commands/__tests__/upgrade.lock.test.d.ts +15 -0
- package/dist/commands/__tests__/upgrade.lock.test.d.ts.map +1 -0
- package/dist/commands/__tests__/upgrade.lock.test.js +253 -0
- package/dist/commands/__tests__/upgrade.lock.test.js.map +1 -0
- package/dist/commands/__tests__/upgrade.olam-tag.test.d.ts +21 -0
- package/dist/commands/__tests__/upgrade.olam-tag.test.d.ts.map +1 -0
- package/dist/commands/__tests__/upgrade.olam-tag.test.js +127 -0
- package/dist/commands/__tests__/upgrade.olam-tag.test.js.map +1 -0
- package/dist/commands/__tests__/upgrade.poll.test.d.ts +14 -0
- package/dist/commands/__tests__/upgrade.poll.test.d.ts.map +1 -0
- package/dist/commands/__tests__/upgrade.poll.test.js +136 -0
- package/dist/commands/__tests__/upgrade.poll.test.js.map +1 -0
- package/dist/commands/__tests__/upgrade.recreate.test.d.ts +17 -0
- package/dist/commands/__tests__/upgrade.recreate.test.d.ts.map +1 -0
- package/dist/commands/__tests__/upgrade.recreate.test.js +95 -0
- package/dist/commands/__tests__/upgrade.recreate.test.js.map +1 -0
- package/dist/commands/__tests__/upgrade.rollback.test.d.ts +12 -0
- package/dist/commands/__tests__/upgrade.rollback.test.d.ts.map +1 -0
- package/dist/commands/__tests__/upgrade.rollback.test.js +275 -0
- package/dist/commands/__tests__/upgrade.rollback.test.js.map +1 -0
- package/dist/commands/__tests__/upgrade.sha-capture.test.d.ts +12 -0
- package/dist/commands/__tests__/upgrade.sha-capture.test.d.ts.map +1 -0
- package/dist/commands/__tests__/upgrade.sha-capture.test.js +63 -0
- package/dist/commands/__tests__/upgrade.sha-capture.test.js.map +1 -0
- package/dist/commands/__tests__/upgrade.smoke.test.d.ts +19 -0
- package/dist/commands/__tests__/upgrade.smoke.test.d.ts.map +1 -0
- package/dist/commands/__tests__/upgrade.smoke.test.js +101 -0
- package/dist/commands/__tests__/upgrade.smoke.test.js.map +1 -0
- package/dist/commands/__tests__/upgrade.swap.test.d.ts +19 -0
- package/dist/commands/__tests__/upgrade.swap.test.d.ts.map +1 -0
- package/dist/commands/__tests__/upgrade.swap.test.js +333 -0
- package/dist/commands/__tests__/upgrade.swap.test.js.map +1 -0
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +31 -0
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/upgrade-history.d.ts +17 -0
- package/dist/commands/upgrade-history.d.ts.map +1 -0
- package/dist/commands/upgrade-history.js +40 -0
- package/dist/commands/upgrade-history.js.map +1 -0
- package/dist/commands/upgrade-lock.d.ts +102 -0
- package/dist/commands/upgrade-lock.d.ts.map +1 -0
- package/dist/commands/upgrade-lock.js +225 -0
- package/dist/commands/upgrade-lock.js.map +1 -0
- package/dist/commands/upgrade-log.d.ts +86 -0
- package/dist/commands/upgrade-log.d.ts.map +1 -0
- package/dist/commands/upgrade-log.js +146 -0
- package/dist/commands/upgrade-log.js.map +1 -0
- package/dist/commands/upgrade.d.ts +265 -0
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +840 -10
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/image-presence.d.ts +40 -0
- package/dist/image-presence.d.ts.map +1 -0
- package/dist/image-presence.js +39 -0
- package/dist/image-presence.js.map +1 -0
- package/dist/index.js +1015 -163
- package/dist/protocol-version.d.ts +79 -0
- package/dist/protocol-version.d.ts.map +1 -0
- package/dist/protocol-version.js +133 -0
- package/dist/protocol-version.js.map +1 -0
- package/dist/registry-allowlist.d.ts +47 -0
- package/dist/registry-allowlist.d.ts.map +1 -0
- package/dist/registry-allowlist.js +67 -0
- package/dist/registry-allowlist.js.map +1 -0
- 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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"}
|