@pleri/olam-cli 0.1.12 → 0.1.14
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 +849 -13
- 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 +1043 -167
- package/dist/index.js.map +1 -1
- 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
package/dist/index.js
CHANGED
|
@@ -421,8 +421,8 @@ var init_parseUtil = __esm({
|
|
|
421
421
|
init_errors();
|
|
422
422
|
init_en();
|
|
423
423
|
makeIssue = (params) => {
|
|
424
|
-
const { data, path:
|
|
425
|
-
const fullPath = [...
|
|
424
|
+
const { data, path: path36, errorMaps, issueData } = params;
|
|
425
|
+
const fullPath = [...path36, ...issueData.path || []];
|
|
426
426
|
const fullIssue = {
|
|
427
427
|
...issueData,
|
|
428
428
|
path: fullPath
|
|
@@ -730,11 +730,11 @@ var init_types = __esm({
|
|
|
730
730
|
init_parseUtil();
|
|
731
731
|
init_util();
|
|
732
732
|
ParseInputLazyPath = class {
|
|
733
|
-
constructor(parent, value,
|
|
733
|
+
constructor(parent, value, path36, key) {
|
|
734
734
|
this._cachedPath = [];
|
|
735
735
|
this.parent = parent;
|
|
736
736
|
this.data = value;
|
|
737
|
-
this._path =
|
|
737
|
+
this._path = path36;
|
|
738
738
|
this._key = key;
|
|
739
739
|
}
|
|
740
740
|
get path() {
|
|
@@ -4221,7 +4221,7 @@ import YAML from "yaml";
|
|
|
4221
4221
|
function bootstrapStepCmd(entry) {
|
|
4222
4222
|
return typeof entry === "string" ? entry : entry.cmd;
|
|
4223
4223
|
}
|
|
4224
|
-
function refineForbiddenKeys(value,
|
|
4224
|
+
function refineForbiddenKeys(value, path36, ctx, rejectSource) {
|
|
4225
4225
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
4226
4226
|
return;
|
|
4227
4227
|
}
|
|
@@ -4229,12 +4229,12 @@ function refineForbiddenKeys(value, path33, ctx, rejectSource) {
|
|
|
4229
4229
|
if (FORBIDDEN_KEYS.has(key)) {
|
|
4230
4230
|
ctx.addIssue({
|
|
4231
4231
|
code: external_exports.ZodIssueCode.custom,
|
|
4232
|
-
path: [...
|
|
4232
|
+
path: [...path36, key],
|
|
4233
4233
|
message: `forbidden key "${key}" (prototype-pollution surface)`
|
|
4234
4234
|
});
|
|
4235
4235
|
continue;
|
|
4236
4236
|
}
|
|
4237
|
-
if (rejectSource &&
|
|
4237
|
+
if (rejectSource && path36.length === 0 && key === "source") {
|
|
4238
4238
|
ctx.addIssue({
|
|
4239
4239
|
code: external_exports.ZodIssueCode.custom,
|
|
4240
4240
|
path: ["source"],
|
|
@@ -4244,30 +4244,30 @@ function refineForbiddenKeys(value, path33, ctx, rejectSource) {
|
|
|
4244
4244
|
}
|
|
4245
4245
|
refineForbiddenKeys(
|
|
4246
4246
|
value[key],
|
|
4247
|
-
[...
|
|
4247
|
+
[...path36, key],
|
|
4248
4248
|
ctx,
|
|
4249
4249
|
false
|
|
4250
4250
|
);
|
|
4251
4251
|
}
|
|
4252
4252
|
}
|
|
4253
|
-
function rejectForbiddenKeys(value,
|
|
4253
|
+
function rejectForbiddenKeys(value, path36, rejectSource) {
|
|
4254
4254
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
4255
4255
|
return;
|
|
4256
4256
|
}
|
|
4257
4257
|
for (const key of Object.keys(value)) {
|
|
4258
4258
|
if (FORBIDDEN_KEYS.has(key)) {
|
|
4259
4259
|
throw new Error(
|
|
4260
|
-
`[manifest] ${
|
|
4260
|
+
`[manifest] ${path36}: forbidden key "${key}" (prototype-pollution surface)`
|
|
4261
4261
|
);
|
|
4262
4262
|
}
|
|
4263
4263
|
if (rejectSource && key === "source") {
|
|
4264
4264
|
throw new Error(
|
|
4265
|
-
`[manifest] ${
|
|
4265
|
+
`[manifest] ${path36}: top-level "source" is loader-stamped \u2014 manifests must not author it`
|
|
4266
4266
|
);
|
|
4267
4267
|
}
|
|
4268
4268
|
rejectForbiddenKeys(
|
|
4269
4269
|
value[key],
|
|
4270
|
-
`${
|
|
4270
|
+
`${path36}.${key}`,
|
|
4271
4271
|
false
|
|
4272
4272
|
);
|
|
4273
4273
|
}
|
|
@@ -5208,8 +5208,8 @@ var init_client = __esm({
|
|
|
5208
5208
|
throw new Error(`failed to report rate-limit for ${accountId} (HTTP ${res.status})`);
|
|
5209
5209
|
}
|
|
5210
5210
|
}
|
|
5211
|
-
async request(method,
|
|
5212
|
-
const url = `${this.baseUrl}${
|
|
5211
|
+
async request(method, path36, body, attempt = 0) {
|
|
5212
|
+
const url = `${this.baseUrl}${path36}`;
|
|
5213
5213
|
const controller = new AbortController();
|
|
5214
5214
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
5215
5215
|
const headers = {};
|
|
@@ -5225,7 +5225,7 @@ var init_client = __esm({
|
|
|
5225
5225
|
} catch (err) {
|
|
5226
5226
|
if (attempt < RETRY_COUNT && isTransient(err)) {
|
|
5227
5227
|
await sleep(RETRY_BACKOFF_MS * (attempt + 1));
|
|
5228
|
-
return this.request(method,
|
|
5228
|
+
return this.request(method, path36, body, attempt + 1);
|
|
5229
5229
|
}
|
|
5230
5230
|
throw err;
|
|
5231
5231
|
} finally {
|
|
@@ -6676,8 +6676,8 @@ var init_provider3 = __esm({
|
|
|
6676
6676
|
// -----------------------------------------------------------------------
|
|
6677
6677
|
// Internal fetch helper
|
|
6678
6678
|
// -----------------------------------------------------------------------
|
|
6679
|
-
async request(
|
|
6680
|
-
const url = `${this.config.workerUrl}${
|
|
6679
|
+
async request(path36, method, body) {
|
|
6680
|
+
const url = `${this.config.workerUrl}${path36}`;
|
|
6681
6681
|
const bearer = await this.config.mintToken();
|
|
6682
6682
|
const headers = {
|
|
6683
6683
|
Authorization: `Bearer ${bearer}`
|
|
@@ -7790,8 +7790,8 @@ import { execFileSync as execFileSync3 } from "node:child_process";
|
|
|
7790
7790
|
import * as fs13 from "node:fs";
|
|
7791
7791
|
import * as os9 from "node:os";
|
|
7792
7792
|
import * as path14 from "node:path";
|
|
7793
|
-
function expandHome(p,
|
|
7794
|
-
return p.replace(/^~(?=$|\/|\\)/,
|
|
7793
|
+
function expandHome(p, homedir17) {
|
|
7794
|
+
return p.replace(/^~(?=$|\/|\\)/, homedir17());
|
|
7795
7795
|
}
|
|
7796
7796
|
function sanitizeRepoFilename(name) {
|
|
7797
7797
|
const sanitized = name.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
@@ -7812,7 +7812,7 @@ ${stderr}`;
|
|
|
7812
7812
|
}
|
|
7813
7813
|
function snapshotBaselineDiff(repos, workspacePath, deps = {}) {
|
|
7814
7814
|
const exec = deps.exec ?? ((cmd, args, opts) => execFileSync3(cmd, args, opts));
|
|
7815
|
-
const
|
|
7815
|
+
const homedir17 = deps.homedir ?? (() => os9.homedir());
|
|
7816
7816
|
const baselineDir = path14.join(workspacePath, ".olam", "baseline");
|
|
7817
7817
|
try {
|
|
7818
7818
|
fs13.mkdirSync(baselineDir, { recursive: true });
|
|
@@ -7827,7 +7827,7 @@ function snapshotBaselineDiff(repos, workspacePath, deps = {}) {
|
|
|
7827
7827
|
if (!repo.path) continue;
|
|
7828
7828
|
const filename = `${sanitizeRepoFilename(repo.name)}.diff`;
|
|
7829
7829
|
const outPath = path14.join(baselineDir, filename);
|
|
7830
|
-
const repoPath = expandHome(repo.path,
|
|
7830
|
+
const repoPath = expandHome(repo.path, homedir17);
|
|
7831
7831
|
if (!fs13.existsSync(repoPath)) {
|
|
7832
7832
|
writeBaselineFile(outPath, `# repo: ${repo.name}
|
|
7833
7833
|
# (skipped: path ${repoPath} does not exist)
|
|
@@ -11780,6 +11780,61 @@ var init_context = __esm({
|
|
|
11780
11780
|
}
|
|
11781
11781
|
});
|
|
11782
11782
|
|
|
11783
|
+
// src/registry-allowlist.ts
|
|
11784
|
+
var registry_allowlist_exports = {};
|
|
11785
|
+
__export(registry_allowlist_exports, {
|
|
11786
|
+
decideAllowlist: () => decideAllowlist,
|
|
11787
|
+
resolveDevboxImageOverride: () => resolveDevboxImageOverride
|
|
11788
|
+
});
|
|
11789
|
+
function decideAllowlist(input) {
|
|
11790
|
+
const { imageRef, allowCustomRegistry } = input;
|
|
11791
|
+
const allowedByDefault = DEFAULT_ALLOWLIST_PATTERNS.some((re) => re.test(imageRef));
|
|
11792
|
+
if (allowedByDefault) {
|
|
11793
|
+
return {
|
|
11794
|
+
imageRef,
|
|
11795
|
+
allowedByDefault: true,
|
|
11796
|
+
accepted: true,
|
|
11797
|
+
stderrLine: ""
|
|
11798
|
+
};
|
|
11799
|
+
}
|
|
11800
|
+
if (allowCustomRegistry) {
|
|
11801
|
+
return {
|
|
11802
|
+
imageRef,
|
|
11803
|
+
allowedByDefault: false,
|
|
11804
|
+
accepted: true,
|
|
11805
|
+
stderrLine: `Warning: using custom devbox image '${imageRef}'. (--allow-custom-registry was specified.) Verify the source and digest before proceeding.`
|
|
11806
|
+
};
|
|
11807
|
+
}
|
|
11808
|
+
return {
|
|
11809
|
+
imageRef,
|
|
11810
|
+
allowedByDefault: false,
|
|
11811
|
+
accepted: false,
|
|
11812
|
+
stderrLine: `Error: image '${imageRef}' is outside allowed registries (ghcr.io/pleri/*).
|
|
11813
|
+
To override: re-run with --allow-custom-registry
|
|
11814
|
+
Verify the source and digest before doing so.`
|
|
11815
|
+
};
|
|
11816
|
+
}
|
|
11817
|
+
function resolveDevboxImageOverride(flagValue, env = process.env) {
|
|
11818
|
+
if (flagValue && flagValue.trim().length > 0) {
|
|
11819
|
+
return flagValue.trim();
|
|
11820
|
+
}
|
|
11821
|
+
const envValue = env.OLAM_DEVBOX_IMAGE;
|
|
11822
|
+
if (envValue && envValue.trim().length > 0) {
|
|
11823
|
+
return envValue.trim();
|
|
11824
|
+
}
|
|
11825
|
+
return void 0;
|
|
11826
|
+
}
|
|
11827
|
+
var DEFAULT_ALLOWLIST_PATTERNS;
|
|
11828
|
+
var init_registry_allowlist = __esm({
|
|
11829
|
+
"src/registry-allowlist.ts"() {
|
|
11830
|
+
"use strict";
|
|
11831
|
+
DEFAULT_ALLOWLIST_PATTERNS = [
|
|
11832
|
+
// ghcr.io/pleri/<anything>:<tag> or ghcr.io/pleri/<anything>@sha256:<digest>
|
|
11833
|
+
/^ghcr\.io\/pleri\/[^/\s]+(?::[^\s]+|@sha256:[a-f0-9]+)?$/
|
|
11834
|
+
];
|
|
11835
|
+
}
|
|
11836
|
+
});
|
|
11837
|
+
|
|
11783
11838
|
// ../core/src/orchestrator/enter.ts
|
|
11784
11839
|
var enter_exports = {};
|
|
11785
11840
|
__export(enter_exports, {
|
|
@@ -11843,6 +11898,9 @@ var init_checksum = __esm({
|
|
|
11843
11898
|
|
|
11844
11899
|
// src/index.ts
|
|
11845
11900
|
import { Command } from "commander";
|
|
11901
|
+
import * as fs31 from "node:fs";
|
|
11902
|
+
import * as path35 from "node:path";
|
|
11903
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
11846
11904
|
|
|
11847
11905
|
// src/commands/init.ts
|
|
11848
11906
|
import * as fs5 from "node:fs";
|
|
@@ -12249,11 +12307,11 @@ var UnknownArchetypeError = class extends Error {
|
|
|
12249
12307
|
known;
|
|
12250
12308
|
};
|
|
12251
12309
|
var ArchetypeCycleError = class extends Error {
|
|
12252
|
-
constructor(
|
|
12310
|
+
constructor(path36) {
|
|
12253
12311
|
super(
|
|
12254
|
-
`Archetype inheritance cycle detected: ${
|
|
12312
|
+
`Archetype inheritance cycle detected: ${path36.join(" \u2192 ")} \u2192 ${path36[0] ?? "?"}`
|
|
12255
12313
|
);
|
|
12256
|
-
this.path =
|
|
12314
|
+
this.path = path36;
|
|
12257
12315
|
this.name = "ArchetypeCycleError";
|
|
12258
12316
|
}
|
|
12259
12317
|
path;
|
|
@@ -13072,10 +13130,10 @@ async function readHostCpToken2() {
|
|
|
13072
13130
|
if (!fs19.existsSync(tp)) return null;
|
|
13073
13131
|
return fs19.readFileSync(tp, "utf-8").trim();
|
|
13074
13132
|
}
|
|
13075
|
-
async function callHostCpProxy(method, worldId,
|
|
13133
|
+
async function callHostCpProxy(method, worldId, path36, body) {
|
|
13076
13134
|
const token = await readHostCpToken2();
|
|
13077
13135
|
if (!token) return { ok: false, status: 0, error: "no token (host CP not started)" };
|
|
13078
|
-
const url = `http://127.0.0.1:${HOST_CP_PORT}/api/world/${encodeURIComponent(worldId)}${
|
|
13136
|
+
const url = `http://127.0.0.1:${HOST_CP_PORT}/api/world/${encodeURIComponent(worldId)}${path36}`;
|
|
13079
13137
|
try {
|
|
13080
13138
|
const headers = {
|
|
13081
13139
|
Authorization: `Bearer ${token}`
|
|
@@ -13686,9 +13744,9 @@ function formatFreshnessWarning(result, image = DEFAULT_DEVBOX_IMAGE) {
|
|
|
13686
13744
|
"These source files have changed since the image was built; the",
|
|
13687
13745
|
"changes will NOT take effect in fresh worlds until you rebuild:"
|
|
13688
13746
|
];
|
|
13689
|
-
for (const { path:
|
|
13747
|
+
for (const { path: path36, mtimeMs } of result.newerSources) {
|
|
13690
13748
|
const when = new Date(mtimeMs).toISOString();
|
|
13691
|
-
lines.push(` \u2022 ${
|
|
13749
|
+
lines.push(` \u2022 ${path36} (modified ${when})`);
|
|
13692
13750
|
}
|
|
13693
13751
|
lines.push("");
|
|
13694
13752
|
lines.push("Rebuild with:");
|
|
@@ -13848,15 +13906,15 @@ init_context();
|
|
|
13848
13906
|
var HOST_CP_URL = "http://127.0.0.1:19000";
|
|
13849
13907
|
async function readHostCpTokenForCreate() {
|
|
13850
13908
|
try {
|
|
13851
|
-
const { default:
|
|
13852
|
-
const { default:
|
|
13853
|
-
const { default:
|
|
13854
|
-
const tp =
|
|
13855
|
-
process.env.OLAM_HOME ??
|
|
13909
|
+
const { default: fs32 } = await import("node:fs");
|
|
13910
|
+
const { default: os18 } = await import("node:os");
|
|
13911
|
+
const { default: path36 } = await import("node:path");
|
|
13912
|
+
const tp = path36.join(
|
|
13913
|
+
process.env.OLAM_HOME ?? path36.join(os18.homedir(), ".olam"),
|
|
13856
13914
|
"host-cp.token"
|
|
13857
13915
|
);
|
|
13858
|
-
if (!
|
|
13859
|
-
return
|
|
13916
|
+
if (!fs32.existsSync(tp)) return null;
|
|
13917
|
+
return fs32.readFileSync(tp, "utf-8").trim();
|
|
13860
13918
|
} catch {
|
|
13861
13919
|
return null;
|
|
13862
13920
|
}
|
|
@@ -13865,7 +13923,23 @@ function registerCreate(program2) {
|
|
|
13865
13923
|
program2.command("create").description("Create a new development world").option("--name <name>", "World name (required unless --from-prompt is set; auto-derived in that case)").option("--repos <repos...>", "Repos to include (names from .olam/config.yaml; wins over --workspace)").option("--workspace <name>", "Named workspace from the host catalog (~/.olam/workspaces/<name>.yaml)").option("--task <task>", "Initial task to dispatch").option("--branch <branch>", "Override default branch name").option("--plan <file>", "Path to a plan file to inject").option("--no-auth", "Skip auto-injecting host credentials").option("--no-host-cp", 'Suppress the host CP "you might want to start it" hint').option("--auto-codex-review", "Spawn a parallel codex-review lane that critiques main as it works").option("--no-open", "Suppress auto-opening the Host CP UI in the browser on success").option("--rebuild-base", "Rebuild olam-devbox:latest before creating (slow)").option("--no-freshness-check", "Skip the devbox image freshness check").option("--from-prompt <prompt>", "NL prompt \u2192 infer workspace + dispatch (CLI parity for olam_create_from_prompt MCP tool)").option("--keep-after-merge", "Disable auto-destroy when the world's PR merges (useful for inspection/debugging)").option("--carry-uncommitted", "Preserve operator's uncommitted edits in the world's worktree").option(
|
|
13866
13924
|
"--allow-bootstrap-failure",
|
|
13867
13925
|
"Treat bootstrap step failures as warnings instead of destroying the world (dogfood escape hatch for cross-repo seed coupling)"
|
|
13868
|
-
).action(async (opts) => {
|
|
13926
|
+
).option("--devbox-image <ref>", "Override the default devbox image (full registry/name:tag or @sha256: ref)").option("--allow-custom-registry", "Allow --devbox-image refs outside ghcr.io/pleri/* (logs a warning)").action(async (opts) => {
|
|
13927
|
+
const { resolveDevboxImageOverride: resolveDevboxImageOverride2, decideAllowlist: decideAllowlist2 } = await Promise.resolve().then(() => (init_registry_allowlist(), registry_allowlist_exports));
|
|
13928
|
+
const overrideRef = resolveDevboxImageOverride2(opts.devboxImage);
|
|
13929
|
+
if (overrideRef) {
|
|
13930
|
+
const decision = decideAllowlist2({
|
|
13931
|
+
imageRef: overrideRef,
|
|
13932
|
+
allowCustomRegistry: opts.allowCustomRegistry === true
|
|
13933
|
+
});
|
|
13934
|
+
if (!decision.accepted) {
|
|
13935
|
+
process.stderr.write(decision.stderrLine + "\n");
|
|
13936
|
+
process.exitCode = 1;
|
|
13937
|
+
return;
|
|
13938
|
+
}
|
|
13939
|
+
if (decision.stderrLine) {
|
|
13940
|
+
process.stderr.write(decision.stderrLine + "\n");
|
|
13941
|
+
}
|
|
13942
|
+
}
|
|
13869
13943
|
let resolvedName = opts.name;
|
|
13870
13944
|
let resolvedWorkspace = opts.workspace;
|
|
13871
13945
|
let resolvedRepos = opts.repos;
|
|
@@ -14170,12 +14244,12 @@ function defaultNameFromPrompt(prompt) {
|
|
|
14170
14244
|
}
|
|
14171
14245
|
async function readHostCpToken3() {
|
|
14172
14246
|
try {
|
|
14173
|
-
const { default:
|
|
14174
|
-
const { default:
|
|
14175
|
-
const { default:
|
|
14176
|
-
const tp =
|
|
14177
|
-
if (!
|
|
14178
|
-
const raw =
|
|
14247
|
+
const { default: fs32 } = await import("node:fs");
|
|
14248
|
+
const { default: os18 } = await import("node:os");
|
|
14249
|
+
const { default: path36 } = await import("node:path");
|
|
14250
|
+
const tp = path36.join(os18.homedir(), ".olam", "host-cp.token");
|
|
14251
|
+
if (!fs32.existsSync(tp)) return null;
|
|
14252
|
+
const raw = fs32.readFileSync(tp, "utf-8").trim();
|
|
14179
14253
|
return raw.length > 0 ? raw : null;
|
|
14180
14254
|
} catch {
|
|
14181
14255
|
return null;
|
|
@@ -16840,17 +16914,246 @@ function registerPolicyCheck(program2) {
|
|
|
16840
16914
|
}
|
|
16841
16915
|
|
|
16842
16916
|
// src/commands/upgrade.ts
|
|
16917
|
+
import * as fs24 from "node:fs";
|
|
16918
|
+
import * as path28 from "node:path";
|
|
16919
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
16920
|
+
import pc15 from "picocolors";
|
|
16921
|
+
|
|
16922
|
+
// src/commands/upgrade-lock.ts
|
|
16843
16923
|
import * as fs22 from "node:fs";
|
|
16924
|
+
import * as os13 from "node:os";
|
|
16844
16925
|
import * as path26 from "node:path";
|
|
16845
16926
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
16846
|
-
|
|
16927
|
+
var LOCK_FILE_PATH = path26.join(os13.homedir(), ".olam", ".upgrade.lock");
|
|
16928
|
+
var STALE_LOCK_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
16929
|
+
function readLockFile(lockPath) {
|
|
16930
|
+
try {
|
|
16931
|
+
if (!fs22.existsSync(lockPath)) return null;
|
|
16932
|
+
const raw = fs22.readFileSync(lockPath, "utf-8").trim();
|
|
16933
|
+
if (raw.length === 0) return null;
|
|
16934
|
+
const parsed = JSON.parse(raw);
|
|
16935
|
+
if (typeof parsed.pid !== "number" || typeof parsed.startTs !== "number") return null;
|
|
16936
|
+
return { pid: parsed.pid, startTs: parsed.startTs };
|
|
16937
|
+
} catch {
|
|
16938
|
+
return null;
|
|
16939
|
+
}
|
|
16940
|
+
}
|
|
16941
|
+
function isPidAlive(pid) {
|
|
16942
|
+
try {
|
|
16943
|
+
process.kill(pid, 0);
|
|
16944
|
+
return true;
|
|
16945
|
+
} catch {
|
|
16946
|
+
return false;
|
|
16947
|
+
}
|
|
16948
|
+
}
|
|
16949
|
+
var PS_UNAVAILABLE = "__ps_unavailable__";
|
|
16950
|
+
function getPidCommand(pid) {
|
|
16951
|
+
const result = spawnSync6("ps", ["-p", String(pid), "-o", "comm="], {
|
|
16952
|
+
encoding: "utf-8",
|
|
16953
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
16954
|
+
});
|
|
16955
|
+
if (result.status === null || result.error !== void 0) return PS_UNAVAILABLE;
|
|
16956
|
+
if (result.status !== 0) return null;
|
|
16957
|
+
const out = result.stdout.trim();
|
|
16958
|
+
return out.length === 0 ? null : out;
|
|
16959
|
+
}
|
|
16960
|
+
function isOlamUpgradeCommand(comm) {
|
|
16961
|
+
if (!comm) return false;
|
|
16962
|
+
if (comm === PS_UNAVAILABLE) return false;
|
|
16963
|
+
const base = comm.split("/").pop() ?? comm;
|
|
16964
|
+
const stripped = base.replace(/\s*\(.*\)\s*$/, "").trim();
|
|
16965
|
+
return stripped === "node" || stripped === "olam" || stripped === "olam-cli";
|
|
16966
|
+
}
|
|
16967
|
+
function isStaleLock(content, nowMs = Date.now()) {
|
|
16968
|
+
if (!content) return true;
|
|
16969
|
+
if (nowMs - content.startTs > STALE_LOCK_TIMEOUT_MS) return true;
|
|
16970
|
+
if (!isPidAlive(content.pid)) return true;
|
|
16971
|
+
const comm = getPidCommand(content.pid);
|
|
16972
|
+
if (comm === PS_UNAVAILABLE) return false;
|
|
16973
|
+
if (!isOlamUpgradeCommand(comm)) return true;
|
|
16974
|
+
return false;
|
|
16975
|
+
}
|
|
16976
|
+
function acquireLock(lockPath = LOCK_FILE_PATH, nowMs = Date.now()) {
|
|
16977
|
+
const dir = path26.dirname(lockPath);
|
|
16978
|
+
fs22.mkdirSync(dir, { recursive: true });
|
|
16979
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
16980
|
+
try {
|
|
16981
|
+
const fd = fs22.openSync(lockPath, "wx", 420);
|
|
16982
|
+
try {
|
|
16983
|
+
const content = { pid: process.pid, startTs: nowMs };
|
|
16984
|
+
fs22.writeSync(fd, JSON.stringify(content));
|
|
16985
|
+
} finally {
|
|
16986
|
+
fs22.closeSync(fd);
|
|
16987
|
+
}
|
|
16988
|
+
return { acquired: true, lockPath };
|
|
16989
|
+
} catch (err) {
|
|
16990
|
+
const code = err.code;
|
|
16991
|
+
if (code !== "EEXIST") throw err;
|
|
16992
|
+
const existing2 = readLockFile(lockPath);
|
|
16993
|
+
if (isStaleLock(existing2, nowMs)) {
|
|
16994
|
+
try {
|
|
16995
|
+
fs22.unlinkSync(lockPath);
|
|
16996
|
+
} catch (unlinkErr) {
|
|
16997
|
+
const ucode = unlinkErr.code;
|
|
16998
|
+
if (ucode !== "ENOENT") throw unlinkErr;
|
|
16999
|
+
}
|
|
17000
|
+
continue;
|
|
17001
|
+
}
|
|
17002
|
+
return {
|
|
17003
|
+
acquired: false,
|
|
17004
|
+
reason: "live",
|
|
17005
|
+
...existing2?.pid !== void 0 && { existingPid: existing2.pid },
|
|
17006
|
+
...existing2?.startTs !== void 0 && { existingStartTs: existing2.startTs }
|
|
17007
|
+
};
|
|
17008
|
+
}
|
|
17009
|
+
}
|
|
17010
|
+
const existing = readLockFile(lockPath);
|
|
17011
|
+
return {
|
|
17012
|
+
acquired: false,
|
|
17013
|
+
reason: "live",
|
|
17014
|
+
...existing?.pid !== void 0 && { existingPid: existing.pid },
|
|
17015
|
+
...existing?.startTs !== void 0 && { existingStartTs: existing.startTs }
|
|
17016
|
+
};
|
|
17017
|
+
}
|
|
17018
|
+
function releaseLock(lockPath = LOCK_FILE_PATH) {
|
|
17019
|
+
try {
|
|
17020
|
+
fs22.unlinkSync(lockPath);
|
|
17021
|
+
} catch (err) {
|
|
17022
|
+
const code = err.code;
|
|
17023
|
+
if (code !== "ENOENT") throw err;
|
|
17024
|
+
}
|
|
17025
|
+
}
|
|
17026
|
+
function formatRefusalMessage(result, lockPath = LOCK_FILE_PATH) {
|
|
17027
|
+
const pidStr = result.existingPid !== void 0 ? ` (pid ${result.existingPid})` : "";
|
|
17028
|
+
const lines = [
|
|
17029
|
+
`Upgrade in progress${pidStr}.`,
|
|
17030
|
+
"Wait for the running upgrade to finish, or:",
|
|
17031
|
+
" - Check progress: olam upgrade --history",
|
|
17032
|
+
` - If stale (crashed CLI): rm ${lockPath}`
|
|
17033
|
+
];
|
|
17034
|
+
return lines.join("\n");
|
|
17035
|
+
}
|
|
17036
|
+
|
|
17037
|
+
// src/commands/upgrade-log.ts
|
|
17038
|
+
import * as fs23 from "node:fs";
|
|
17039
|
+
import * as os14 from "node:os";
|
|
17040
|
+
import * as path27 from "node:path";
|
|
17041
|
+
function getUpgradeLogPath() {
|
|
17042
|
+
const home = process.env["HOME"] ?? os14.homedir();
|
|
17043
|
+
return path27.join(home, ".olam", "upgrade.log");
|
|
17044
|
+
}
|
|
17045
|
+
var UPGRADE_LOG_PATH = getUpgradeLogPath();
|
|
17046
|
+
function appendUpgradeLog(row, logPath = getUpgradeLogPath()) {
|
|
17047
|
+
try {
|
|
17048
|
+
fs23.mkdirSync(path27.dirname(logPath), { recursive: true });
|
|
17049
|
+
const line = JSON.stringify(row) + "\n";
|
|
17050
|
+
fs23.appendFileSync(logPath, line, { mode: 420 });
|
|
17051
|
+
} catch (err) {
|
|
17052
|
+
process.stderr.write(
|
|
17053
|
+
`[upgrade-log] failed to append: ${err instanceof Error ? err.message : String(err)}
|
|
17054
|
+
`
|
|
17055
|
+
);
|
|
17056
|
+
}
|
|
17057
|
+
}
|
|
17058
|
+
function readUpgradeLog(limit = 10, logPath = getUpgradeLogPath()) {
|
|
17059
|
+
if (!fs23.existsSync(logPath)) return [];
|
|
17060
|
+
let raw;
|
|
17061
|
+
try {
|
|
17062
|
+
raw = fs23.readFileSync(logPath, "utf-8");
|
|
17063
|
+
} catch (err) {
|
|
17064
|
+
process.stderr.write(
|
|
17065
|
+
`[upgrade-log] failed to read: ${err instanceof Error ? err.message : String(err)}
|
|
17066
|
+
`
|
|
17067
|
+
);
|
|
17068
|
+
return [];
|
|
17069
|
+
}
|
|
17070
|
+
const lines = raw.split("\n").filter((l) => l.length > 0);
|
|
17071
|
+
const rows = [];
|
|
17072
|
+
for (let i = 0; i < lines.length; i++) {
|
|
17073
|
+
const line = lines[i];
|
|
17074
|
+
try {
|
|
17075
|
+
const parsed = JSON.parse(line);
|
|
17076
|
+
if (typeof parsed.ts === "string" && typeof parsed.started_at === "number" && typeof parsed.status === "string") {
|
|
17077
|
+
rows.push(parsed);
|
|
17078
|
+
} else {
|
|
17079
|
+
process.stderr.write(`[upgrade-log] skipped malformed row at line ${i + 1}
|
|
17080
|
+
`);
|
|
17081
|
+
}
|
|
17082
|
+
} catch {
|
|
17083
|
+
process.stderr.write(`[upgrade-log] skipped corrupt JSON at line ${i + 1}
|
|
17084
|
+
`);
|
|
17085
|
+
}
|
|
17086
|
+
}
|
|
17087
|
+
return rows.slice(-Math.max(0, limit));
|
|
17088
|
+
}
|
|
17089
|
+
function formatDuration(ms) {
|
|
17090
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
17091
|
+
const totalSec = Math.round(ms / 1e3);
|
|
17092
|
+
if (totalSec < 60) return `${totalSec}s`;
|
|
17093
|
+
const min = Math.floor(totalSec / 60);
|
|
17094
|
+
const sec = totalSec % 60;
|
|
17095
|
+
if (min < 60) return `${min}m${String(sec).padStart(2, "0")}s`;
|
|
17096
|
+
const hr = Math.floor(min / 60);
|
|
17097
|
+
const remMin = min % 60;
|
|
17098
|
+
return `${hr}h${String(remMin).padStart(2, "0")}m`;
|
|
17099
|
+
}
|
|
17100
|
+
function formatHistoryTable(rows) {
|
|
17101
|
+
if (rows.length === 0) {
|
|
17102
|
+
return "No upgrade history yet. Run `olam upgrade` to create your first record.";
|
|
17103
|
+
}
|
|
17104
|
+
const lines = [];
|
|
17105
|
+
lines.push("TIMESTAMP SHA STATUS DURATION FAILED-STEP");
|
|
17106
|
+
lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
17107
|
+
for (const r of rows) {
|
|
17108
|
+
const ts = r.ts.slice(0, 19).replace("T", " ");
|
|
17109
|
+
const sha = r.sha_target.slice(0, 8);
|
|
17110
|
+
const statusIcon = r.status === "success" ? "\u2713 success" : r.status === "rolled_back" ? "\u21A9 rolled_back" : "\u2717 failed";
|
|
17111
|
+
const dur = formatDuration(r.ended_at - r.started_at);
|
|
17112
|
+
const failed = r.failed_step ?? "";
|
|
17113
|
+
lines.push(
|
|
17114
|
+
`${ts.padEnd(28)}${sha.padEnd(10)}${statusIcon.padEnd(15)}${dur.padEnd(11)}${failed}`
|
|
17115
|
+
);
|
|
17116
|
+
}
|
|
17117
|
+
return lines.join("\n");
|
|
17118
|
+
}
|
|
17119
|
+
function formatHistoryJson(rows) {
|
|
17120
|
+
return rows.map((r) => JSON.stringify(r)).join("\n");
|
|
17121
|
+
}
|
|
17122
|
+
|
|
17123
|
+
// src/commands/upgrade-history.ts
|
|
17124
|
+
function parseHistoryOpts(raw) {
|
|
17125
|
+
const rawLimit = raw.n;
|
|
17126
|
+
const limit = typeof rawLimit === "number" ? rawLimit : typeof rawLimit === "string" ? Number.parseInt(rawLimit, 10) : 10;
|
|
17127
|
+
return {
|
|
17128
|
+
limit: Number.isFinite(limit) && limit > 0 ? limit : 10,
|
|
17129
|
+
json: raw.json === true
|
|
17130
|
+
};
|
|
17131
|
+
}
|
|
17132
|
+
function handleHistory(opts) {
|
|
17133
|
+
const rows = readUpgradeLog(opts.limit);
|
|
17134
|
+
if (opts.json) {
|
|
17135
|
+
process.stdout.write(formatHistoryJson(rows) + "\n");
|
|
17136
|
+
return;
|
|
17137
|
+
}
|
|
17138
|
+
if (rows.length === 0) {
|
|
17139
|
+
printInfo("Log file", UPGRADE_LOG_PATH);
|
|
17140
|
+
process.stdout.write(formatHistoryTable(rows) + "\n");
|
|
17141
|
+
return;
|
|
17142
|
+
}
|
|
17143
|
+
printInfo("Log file", UPGRADE_LOG_PATH);
|
|
17144
|
+
process.stdout.write(formatHistoryTable(rows) + "\n");
|
|
17145
|
+
}
|
|
17146
|
+
|
|
17147
|
+
// src/commands/upgrade.ts
|
|
17148
|
+
init_auth();
|
|
17149
|
+
var AUTH_HEALTH_URL2 = "http://127.0.0.1:9999/health";
|
|
16847
17150
|
function isNodeModulesInSync(cwd) {
|
|
16848
|
-
const lockPath =
|
|
16849
|
-
const markerPath =
|
|
16850
|
-
if (!
|
|
17151
|
+
const lockPath = path28.join(cwd, "package-lock.json");
|
|
17152
|
+
const markerPath = path28.join(cwd, "node_modules", ".package-lock.json");
|
|
17153
|
+
if (!fs24.existsSync(lockPath) || !fs24.existsSync(markerPath)) return false;
|
|
16851
17154
|
try {
|
|
16852
|
-
const lockStat =
|
|
16853
|
-
const markerStat =
|
|
17155
|
+
const lockStat = fs24.statSync(lockPath);
|
|
17156
|
+
const markerStat = fs24.statSync(markerPath);
|
|
16854
17157
|
return markerStat.mtimeMs >= lockStat.mtimeMs;
|
|
16855
17158
|
} catch {
|
|
16856
17159
|
return false;
|
|
@@ -16866,8 +17169,8 @@ function shouldSkipInstall(opts, cwd) {
|
|
|
16866
17169
|
return { skip: false };
|
|
16867
17170
|
}
|
|
16868
17171
|
function validateRepoRoot(cwd) {
|
|
16869
|
-
const marker =
|
|
16870
|
-
if (!
|
|
17172
|
+
const marker = path28.join(cwd, "packages/host-cp/compose.yaml");
|
|
17173
|
+
if (!fs24.existsSync(marker)) {
|
|
16871
17174
|
return {
|
|
16872
17175
|
ok: false,
|
|
16873
17176
|
error: `Not an olam repo root (expected ${marker}).
|
|
@@ -16877,11 +17180,19 @@ Run \`olam upgrade\` from the root of your olam checkout.`
|
|
|
16877
17180
|
return { ok: true };
|
|
16878
17181
|
}
|
|
16879
17182
|
function parseUpgradeOpts(raw) {
|
|
17183
|
+
const rawN = raw.n;
|
|
17184
|
+
const historyN = typeof rawN === "number" ? rawN : typeof rawN === "string" ? Number.parseInt(rawN, 10) : 10;
|
|
16880
17185
|
return {
|
|
16881
17186
|
yes: raw.yes === true,
|
|
16882
17187
|
skipImage: raw.skipImage === true,
|
|
16883
17188
|
skipInstall: raw.skipInstall === true,
|
|
16884
|
-
branch: raw.branch ?? null
|
|
17189
|
+
branch: raw.branch ?? null,
|
|
17190
|
+
rollback: raw.rollback === true,
|
|
17191
|
+
force: raw.force === true,
|
|
17192
|
+
noCache: raw.noCache === true,
|
|
17193
|
+
history: raw.history === true,
|
|
17194
|
+
historyN: Number.isFinite(historyN) && historyN > 0 ? historyN : 10,
|
|
17195
|
+
historyJson: raw.json === true
|
|
16885
17196
|
};
|
|
16886
17197
|
}
|
|
16887
17198
|
function extractBundleHash(indexHtml) {
|
|
@@ -16891,7 +17202,7 @@ function extractBundleHash(indexHtml) {
|
|
|
16891
17202
|
function runStep2(label, cmd, args, opts = {}) {
|
|
16892
17203
|
const start = Date.now();
|
|
16893
17204
|
process.stdout.write(` ${pc15.dim(label.padEnd(34))}`);
|
|
16894
|
-
const result =
|
|
17205
|
+
const result = spawnSync7(cmd, [...args], {
|
|
16895
17206
|
encoding: "utf-8",
|
|
16896
17207
|
stdio: ["ignore", "pipe", "pipe"],
|
|
16897
17208
|
cwd: opts.cwd ?? process.cwd(),
|
|
@@ -16910,7 +17221,7 @@ function runStep2(label, cmd, args, opts = {}) {
|
|
|
16910
17221
|
};
|
|
16911
17222
|
}
|
|
16912
17223
|
function isGitDirty(cwd) {
|
|
16913
|
-
const result =
|
|
17224
|
+
const result = spawnSync7("git", ["status", "--porcelain"], {
|
|
16914
17225
|
encoding: "utf-8",
|
|
16915
17226
|
stdio: ["ignore", "pipe", "pipe"],
|
|
16916
17227
|
cwd
|
|
@@ -16918,13 +17229,194 @@ function isGitDirty(cwd) {
|
|
|
16918
17229
|
return (result.stdout ?? "").trim().length > 0;
|
|
16919
17230
|
}
|
|
16920
17231
|
function hasGitUpstream(cwd) {
|
|
16921
|
-
const result =
|
|
17232
|
+
const result = spawnSync7("git", ["rev-parse", "--abbrev-ref", "@{u}"], {
|
|
16922
17233
|
encoding: "utf-8",
|
|
16923
17234
|
stdio: ["ignore", "pipe", "pipe"],
|
|
16924
17235
|
cwd
|
|
16925
17236
|
});
|
|
16926
17237
|
return result.status === 0;
|
|
16927
17238
|
}
|
|
17239
|
+
function captureHeadSha(cwd) {
|
|
17240
|
+
const result = spawnSync7("git", ["rev-parse", "HEAD"], {
|
|
17241
|
+
encoding: "utf-8",
|
|
17242
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
17243
|
+
cwd
|
|
17244
|
+
});
|
|
17245
|
+
if (result.status !== 0) return null;
|
|
17246
|
+
const sha = (result.stdout ?? "").trim();
|
|
17247
|
+
if (!/^[0-9a-f]{40}$/.test(sha)) return null;
|
|
17248
|
+
return sha;
|
|
17249
|
+
}
|
|
17250
|
+
function abbreviateSha(sha) {
|
|
17251
|
+
return sha.slice(0, 8);
|
|
17252
|
+
}
|
|
17253
|
+
function imageExists(tag) {
|
|
17254
|
+
try {
|
|
17255
|
+
const result = spawnSync7("docker", ["image", "inspect", "--format", "{{.Id}}", tag], {
|
|
17256
|
+
encoding: "utf-8",
|
|
17257
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
17258
|
+
});
|
|
17259
|
+
return result.status === 0;
|
|
17260
|
+
} catch {
|
|
17261
|
+
return false;
|
|
17262
|
+
}
|
|
17263
|
+
}
|
|
17264
|
+
function checkRollbackSetExists(plan) {
|
|
17265
|
+
const missing = plan.filter((p) => !imageExists(p.rollback)).map((p) => p.rollback);
|
|
17266
|
+
if (missing.length === 0) return null;
|
|
17267
|
+
return missing.join(", ");
|
|
17268
|
+
}
|
|
17269
|
+
function smokeImage(image, targetSha) {
|
|
17270
|
+
const createResult = spawnSync7("docker", ["create", "--name", `olam-smoke-${Date.now()}`, image], {
|
|
17271
|
+
encoding: "utf-8",
|
|
17272
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
17273
|
+
});
|
|
17274
|
+
if (createResult.status !== 0) {
|
|
17275
|
+
return {
|
|
17276
|
+
image,
|
|
17277
|
+
ok: false,
|
|
17278
|
+
bakedSha: null,
|
|
17279
|
+
error: `docker create failed: ${(createResult.stderr ?? "").trim()}`
|
|
17280
|
+
};
|
|
17281
|
+
}
|
|
17282
|
+
const containerId = (createResult.stdout ?? "").trim();
|
|
17283
|
+
const inspectResult = spawnSync7(
|
|
17284
|
+
"docker",
|
|
17285
|
+
["inspect", "--format", '{{index .Config.Labels "olam_build_sha"}}', image],
|
|
17286
|
+
{
|
|
17287
|
+
encoding: "utf-8",
|
|
17288
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
17289
|
+
}
|
|
17290
|
+
);
|
|
17291
|
+
if (containerId.length > 0) {
|
|
17292
|
+
spawnSync7("docker", ["rm", "-f", containerId], {
|
|
17293
|
+
encoding: "utf-8",
|
|
17294
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
17295
|
+
});
|
|
17296
|
+
}
|
|
17297
|
+
if (inspectResult.status !== 0) {
|
|
17298
|
+
return {
|
|
17299
|
+
image,
|
|
17300
|
+
ok: false,
|
|
17301
|
+
bakedSha: null,
|
|
17302
|
+
error: `docker inspect failed: ${(inspectResult.stderr ?? "").trim()}`
|
|
17303
|
+
};
|
|
17304
|
+
}
|
|
17305
|
+
const bakedSha = (inspectResult.stdout ?? "").trim();
|
|
17306
|
+
if (bakedSha.length === 0) {
|
|
17307
|
+
return {
|
|
17308
|
+
image,
|
|
17309
|
+
ok: false,
|
|
17310
|
+
bakedSha: null,
|
|
17311
|
+
error: "olam_build_sha label is missing or empty"
|
|
17312
|
+
};
|
|
17313
|
+
}
|
|
17314
|
+
if (bakedSha !== targetSha) {
|
|
17315
|
+
return {
|
|
17316
|
+
image,
|
|
17317
|
+
ok: false,
|
|
17318
|
+
bakedSha,
|
|
17319
|
+
error: `baked SHA ${abbreviateSha(bakedSha)} \u2260 target SHA ${abbreviateSha(targetSha)}`
|
|
17320
|
+
};
|
|
17321
|
+
}
|
|
17322
|
+
return { image, ok: true, bakedSha };
|
|
17323
|
+
}
|
|
17324
|
+
var PRODUCTION_SWAP_PLAN = [
|
|
17325
|
+
{ transient: "olam-auth:olam-next", canonical: "olam-auth:local", rollback: "olam-auth:olam-rollback" },
|
|
17326
|
+
{ transient: "olam-devbox:olam-next", canonical: "olam-devbox:latest", rollback: "olam-devbox:olam-rollback" },
|
|
17327
|
+
{ transient: "olam-host-cp:olam-next", canonical: "olam-host-cp:latest", rollback: "olam-host-cp:olam-rollback" }
|
|
17328
|
+
];
|
|
17329
|
+
function dockerTag(source, dest) {
|
|
17330
|
+
try {
|
|
17331
|
+
const result = spawnSync7("docker", ["tag", source, dest], {
|
|
17332
|
+
encoding: "utf-8",
|
|
17333
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
17334
|
+
});
|
|
17335
|
+
if (result.status === 0 && result.error === void 0) return { ok: true };
|
|
17336
|
+
return {
|
|
17337
|
+
ok: false,
|
|
17338
|
+
error: (result.stderr ?? "").trim() || result.error?.message || "docker tag failed"
|
|
17339
|
+
};
|
|
17340
|
+
} catch (err) {
|
|
17341
|
+
return {
|
|
17342
|
+
ok: false,
|
|
17343
|
+
error: err instanceof Error ? `spawnSync threw: ${err.message}` : "spawnSync threw"
|
|
17344
|
+
};
|
|
17345
|
+
}
|
|
17346
|
+
}
|
|
17347
|
+
function performAtomicSwap(plan) {
|
|
17348
|
+
const steps = plan.map((p) => ({
|
|
17349
|
+
image: p.canonical,
|
|
17350
|
+
rollbackSaved: false,
|
|
17351
|
+
canonicalAdvanced: false
|
|
17352
|
+
}));
|
|
17353
|
+
for (let i = 0; i < plan.length; i++) {
|
|
17354
|
+
const p = plan[i];
|
|
17355
|
+
const r = dockerTag(p.canonical, p.rollback);
|
|
17356
|
+
steps[i] = {
|
|
17357
|
+
...steps[i],
|
|
17358
|
+
rollbackSaved: r.ok,
|
|
17359
|
+
...r.error !== void 0 && { rollbackError: r.error }
|
|
17360
|
+
};
|
|
17361
|
+
}
|
|
17362
|
+
let advanceFailed = false;
|
|
17363
|
+
let firstFailureIdx = -1;
|
|
17364
|
+
for (let i = 0; i < plan.length; i++) {
|
|
17365
|
+
const p = plan[i];
|
|
17366
|
+
if (advanceFailed) {
|
|
17367
|
+
steps[i] = { ...steps[i], canonicalAdvanced: false };
|
|
17368
|
+
continue;
|
|
17369
|
+
}
|
|
17370
|
+
const r = dockerTag(p.transient, p.canonical);
|
|
17371
|
+
steps[i] = {
|
|
17372
|
+
...steps[i],
|
|
17373
|
+
canonicalAdvanced: r.ok,
|
|
17374
|
+
...r.error !== void 0 && { canonicalError: r.error }
|
|
17375
|
+
};
|
|
17376
|
+
if (!r.ok) {
|
|
17377
|
+
advanceFailed = true;
|
|
17378
|
+
firstFailureIdx = i;
|
|
17379
|
+
}
|
|
17380
|
+
}
|
|
17381
|
+
const allAdvanced = steps.every((s) => s.canonicalAdvanced);
|
|
17382
|
+
const noneAdvanced = steps.every((s) => !s.canonicalAdvanced);
|
|
17383
|
+
const partialAdvance = !allAdvanced && !noneAdvanced;
|
|
17384
|
+
const rollbackCoherent = steps.every((s) => s.rollbackSaved);
|
|
17385
|
+
let summary;
|
|
17386
|
+
if (allAdvanced) {
|
|
17387
|
+
const rollbacks = steps.filter((s) => s.rollbackSaved).length;
|
|
17388
|
+
summary = `Swapped ${plan.length} canonical tags; ${rollbacks} :olam-rollback preserved`;
|
|
17389
|
+
} else if (partialAdvance) {
|
|
17390
|
+
const advanced = steps.filter((s) => s.canonicalAdvanced).length;
|
|
17391
|
+
const failedStep = steps[firstFailureIdx];
|
|
17392
|
+
const recoveryHint = rollbackCoherent ? `Run \`olam upgrade --rollback\` to restore coherent prior state.` : `Rollback set INCOHERENT (${steps.filter((s) => s.rollbackSaved).length} of ${plan.length} :olam-rollback tags written). Manual recovery required: inspect images and re-tag canonical from a known-good source.`;
|
|
17393
|
+
summary = `PARTIAL: ${advanced} of ${plan.length} canonical tags advanced before failure on ${failedStep?.image}: ${failedStep?.canonicalError}. ${recoveryHint}`;
|
|
17394
|
+
} else {
|
|
17395
|
+
const failedStep = steps[firstFailureIdx];
|
|
17396
|
+
summary = `Failed on first canonical-advance (${failedStep?.image}): ${failedStep?.canonicalError}. Canonical tags untouched.`;
|
|
17397
|
+
}
|
|
17398
|
+
return {
|
|
17399
|
+
ok: allAdvanced,
|
|
17400
|
+
steps,
|
|
17401
|
+
partialAdvance,
|
|
17402
|
+
rollbackCoherent,
|
|
17403
|
+
summary
|
|
17404
|
+
};
|
|
17405
|
+
}
|
|
17406
|
+
function performRollbackSwap(plan) {
|
|
17407
|
+
const results = [];
|
|
17408
|
+
for (const p of plan) {
|
|
17409
|
+
const r = dockerTag(p.rollback, p.canonical);
|
|
17410
|
+
results.push({
|
|
17411
|
+
image: p.canonical,
|
|
17412
|
+
ok: r.ok,
|
|
17413
|
+
...r.error !== void 0 && { error: r.error }
|
|
17414
|
+
});
|
|
17415
|
+
}
|
|
17416
|
+
const allOk = results.every((r) => r.ok);
|
|
17417
|
+
const summary = allOk ? `Rolled back ${plan.length} canonical tags from :olam-rollback` : `PARTIAL rollback: ${results.filter((r) => r.ok).length} of ${plan.length} succeeded; failed: ${results.filter((r) => !r.ok).map((r) => r.image).join(", ")}`;
|
|
17418
|
+
return { ok: allOk, results, summary };
|
|
17419
|
+
}
|
|
16928
17420
|
async function confirm2(message) {
|
|
16929
17421
|
if (!process.stdin.isTTY) return true;
|
|
16930
17422
|
const { createInterface: createInterface2 } = await import("node:readline");
|
|
@@ -16950,10 +17442,87 @@ async function waitForHealth(timeoutMs = 1e4) {
|
|
|
16950
17442
|
}
|
|
16951
17443
|
return false;
|
|
16952
17444
|
}
|
|
17445
|
+
async function waitForVersionMatch(targetSha, timeoutMs = 6e4, pollIntervalMs = 1e3) {
|
|
17446
|
+
const deadline = Date.now() + timeoutMs;
|
|
17447
|
+
let lastSnapshot = null;
|
|
17448
|
+
while (Date.now() < deadline) {
|
|
17449
|
+
try {
|
|
17450
|
+
const res = await fetch("http://127.0.0.1:19000/api/version/status", {
|
|
17451
|
+
signal: AbortSignal.timeout(2e3)
|
|
17452
|
+
});
|
|
17453
|
+
if (res.ok) {
|
|
17454
|
+
const snapshot = await res.json();
|
|
17455
|
+
lastSnapshot = snapshot;
|
|
17456
|
+
if (snapshot.hostCp?.running === targetSha && snapshot.authService?.running === targetSha && snapshot.devbox?.running === targetSha) {
|
|
17457
|
+
return { matched: true, snapshot };
|
|
17458
|
+
}
|
|
17459
|
+
}
|
|
17460
|
+
} catch {
|
|
17461
|
+
}
|
|
17462
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
17463
|
+
}
|
|
17464
|
+
return { matched: false, snapshot: lastSnapshot };
|
|
17465
|
+
}
|
|
17466
|
+
function formatVersionMismatch(targetSha, snapshot) {
|
|
17467
|
+
if (!snapshot) return "No /api/version/status response received within timeout.";
|
|
17468
|
+
const lines = [];
|
|
17469
|
+
for (const [name, comp] of [
|
|
17470
|
+
["host-cp", snapshot.hostCp],
|
|
17471
|
+
["auth-service", snapshot.authService],
|
|
17472
|
+
["devbox", snapshot.devbox]
|
|
17473
|
+
]) {
|
|
17474
|
+
const match2 = comp?.running === targetSha;
|
|
17475
|
+
lines.push(` ${match2 ? "\u2713" : "\u2717"} ${name}: running=${abbreviateSha(comp?.running ?? "unknown")} target=${abbreviateSha(targetSha)}`);
|
|
17476
|
+
}
|
|
17477
|
+
return lines.join("\n");
|
|
17478
|
+
}
|
|
17479
|
+
async function waitForAuthHealthLocal(timeoutMs = 15e3) {
|
|
17480
|
+
const deadline = Date.now() + timeoutMs;
|
|
17481
|
+
while (Date.now() < deadline) {
|
|
17482
|
+
try {
|
|
17483
|
+
const res = await fetch(AUTH_HEALTH_URL2, { signal: AbortSignal.timeout(2e3) });
|
|
17484
|
+
if (res.ok) return true;
|
|
17485
|
+
} catch {
|
|
17486
|
+
}
|
|
17487
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
17488
|
+
}
|
|
17489
|
+
return false;
|
|
17490
|
+
}
|
|
17491
|
+
async function recreateAuthService() {
|
|
17492
|
+
const start = Date.now();
|
|
17493
|
+
try {
|
|
17494
|
+
spawnSync7("docker", ["stop", "olam-auth"], {
|
|
17495
|
+
encoding: "utf-8",
|
|
17496
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
17497
|
+
});
|
|
17498
|
+
spawnSync7("docker", ["rm", "olam-auth"], {
|
|
17499
|
+
encoding: "utf-8",
|
|
17500
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
17501
|
+
});
|
|
17502
|
+
const controller = new AuthContainerController();
|
|
17503
|
+
controller.start();
|
|
17504
|
+
const healthy = await waitForAuthHealthLocal(15e3);
|
|
17505
|
+
const durationMs = Date.now() - start;
|
|
17506
|
+
if (!healthy) {
|
|
17507
|
+
return {
|
|
17508
|
+
ok: false,
|
|
17509
|
+
durationMs,
|
|
17510
|
+
error: "auth-service /health did not respond within 15s after recreate"
|
|
17511
|
+
};
|
|
17512
|
+
}
|
|
17513
|
+
return { ok: true, durationMs };
|
|
17514
|
+
} catch (err) {
|
|
17515
|
+
return {
|
|
17516
|
+
ok: false,
|
|
17517
|
+
durationMs: Date.now() - start,
|
|
17518
|
+
error: err instanceof Error ? err.message : String(err)
|
|
17519
|
+
};
|
|
17520
|
+
}
|
|
17521
|
+
}
|
|
16953
17522
|
function readBundleHash(cwd) {
|
|
16954
|
-
const indexPath =
|
|
16955
|
-
if (!
|
|
16956
|
-
return extractBundleHash(
|
|
17523
|
+
const indexPath = path28.join(cwd, "packages/control-plane/public/index.html");
|
|
17524
|
+
if (!fs24.existsSync(indexPath)) return null;
|
|
17525
|
+
return extractBundleHash(fs24.readFileSync(indexPath, "utf-8"));
|
|
16957
17526
|
}
|
|
16958
17527
|
async function handleUpgrade(opts) {
|
|
16959
17528
|
const cwd = process.cwd();
|
|
@@ -16963,6 +17532,10 @@ async function handleUpgrade(opts) {
|
|
|
16963
17532
|
process.exitCode = 1;
|
|
16964
17533
|
return;
|
|
16965
17534
|
}
|
|
17535
|
+
if (opts.history) {
|
|
17536
|
+
handleHistory(parseHistoryOpts({ n: opts.historyN, json: opts.historyJson }));
|
|
17537
|
+
return;
|
|
17538
|
+
}
|
|
16966
17539
|
printHeader("olam upgrade");
|
|
16967
17540
|
const steps = [
|
|
16968
17541
|
"git fetch origin --prune",
|
|
@@ -16971,9 +17544,13 @@ async function handleUpgrade(opts) {
|
|
|
16971
17544
|
"npm run build (TS workspaces)",
|
|
16972
17545
|
"vite build (SPA)",
|
|
16973
17546
|
...opts.skipImage ? [] : [
|
|
16974
|
-
"bash build-
|
|
16975
|
-
"
|
|
16976
|
-
"
|
|
17547
|
+
"bash build-auth.sh (auth-service image)",
|
|
17548
|
+
"bash build-devbox.sh (devbox image)",
|
|
17549
|
+
"bash build-host-cp.sh (host-cp image)",
|
|
17550
|
+
"smoke (docker create + inspect)",
|
|
17551
|
+
"atomic 6-tag swap (canonical -> :olam-rollback; :olam-next -> canonical)",
|
|
17552
|
+
"docker compose --force-recreate host-cp + AuthContainerController.start auth",
|
|
17553
|
+
"poll /api/version/status until SHAs match"
|
|
16977
17554
|
]
|
|
16978
17555
|
];
|
|
16979
17556
|
printInfo("Steps", steps.join(", "));
|
|
@@ -16988,6 +17565,138 @@ async function handleUpgrade(opts) {
|
|
|
16988
17565
|
return;
|
|
16989
17566
|
}
|
|
16990
17567
|
}
|
|
17568
|
+
if (opts.rollback) {
|
|
17569
|
+
return await handleRollback();
|
|
17570
|
+
}
|
|
17571
|
+
const lock = acquireLock();
|
|
17572
|
+
if (!lock.acquired) {
|
|
17573
|
+
printError(formatRefusalMessage(lock, LOCK_FILE_PATH));
|
|
17574
|
+
process.exitCode = 1;
|
|
17575
|
+
return;
|
|
17576
|
+
}
|
|
17577
|
+
let signalReleased = false;
|
|
17578
|
+
const releaseOnSignal = (signal) => {
|
|
17579
|
+
if (signalReleased) return;
|
|
17580
|
+
signalReleased = true;
|
|
17581
|
+
try {
|
|
17582
|
+
releaseLock();
|
|
17583
|
+
} catch {
|
|
17584
|
+
}
|
|
17585
|
+
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
17586
|
+
};
|
|
17587
|
+
process.once("SIGINT", releaseOnSignal);
|
|
17588
|
+
process.once("SIGTERM", releaseOnSignal);
|
|
17589
|
+
const logRow = {
|
|
17590
|
+
started_at: Date.now(),
|
|
17591
|
+
durations_ms: {},
|
|
17592
|
+
sha_target: "",
|
|
17593
|
+
failed_step: null,
|
|
17594
|
+
status: "failed"
|
|
17595
|
+
// default; flipped to 'success' on clean exit
|
|
17596
|
+
};
|
|
17597
|
+
try {
|
|
17598
|
+
await runUpgradeStepsWithLockHeld(opts, cwd, logRow);
|
|
17599
|
+
if (process.exitCode !== 1) logRow.status = "success";
|
|
17600
|
+
} finally {
|
|
17601
|
+
const ended_at = Date.now();
|
|
17602
|
+
const row = {
|
|
17603
|
+
ts: new Date(ended_at).toISOString(),
|
|
17604
|
+
started_at: logRow.started_at,
|
|
17605
|
+
ended_at,
|
|
17606
|
+
sha_target: logRow.sha_target,
|
|
17607
|
+
status: logRow.status,
|
|
17608
|
+
failed_step: logRow.failed_step,
|
|
17609
|
+
durations_ms: logRow.durations_ms
|
|
17610
|
+
};
|
|
17611
|
+
appendUpgradeLog(row);
|
|
17612
|
+
releaseLock();
|
|
17613
|
+
process.removeListener("SIGINT", releaseOnSignal);
|
|
17614
|
+
process.removeListener("SIGTERM", releaseOnSignal);
|
|
17615
|
+
}
|
|
17616
|
+
}
|
|
17617
|
+
async function handleRollback() {
|
|
17618
|
+
printHeader("olam upgrade --rollback");
|
|
17619
|
+
const missing = checkRollbackSetExists(PRODUCTION_SWAP_PLAN);
|
|
17620
|
+
if (missing !== null) {
|
|
17621
|
+
printError(
|
|
17622
|
+
`No rollback-set available \u2014 missing :olam-rollback tag(s): ${missing}
|
|
17623
|
+
|
|
17624
|
+
A rollback-set is created by the FIRST successful \`olam upgrade\`. If this
|
|
17625
|
+
is your first install, run \`olam upgrade\` to populate the rollback set.
|
|
17626
|
+
If a previous upgrade was incomplete, the rollback set may be partial;
|
|
17627
|
+
manually inspect images with \`docker images olam-*:olam-rollback\`.`
|
|
17628
|
+
);
|
|
17629
|
+
process.exitCode = 1;
|
|
17630
|
+
return;
|
|
17631
|
+
}
|
|
17632
|
+
const lock = acquireLock();
|
|
17633
|
+
if (!lock.acquired) {
|
|
17634
|
+
printError(formatRefusalMessage(lock, LOCK_FILE_PATH));
|
|
17635
|
+
process.exitCode = 1;
|
|
17636
|
+
return;
|
|
17637
|
+
}
|
|
17638
|
+
let signalReleased = false;
|
|
17639
|
+
const releaseOnSignal = (signal) => {
|
|
17640
|
+
if (signalReleased) return;
|
|
17641
|
+
signalReleased = true;
|
|
17642
|
+
try {
|
|
17643
|
+
releaseLock();
|
|
17644
|
+
} catch {
|
|
17645
|
+
}
|
|
17646
|
+
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
17647
|
+
};
|
|
17648
|
+
process.once("SIGINT", releaseOnSignal);
|
|
17649
|
+
process.once("SIGTERM", releaseOnSignal);
|
|
17650
|
+
try {
|
|
17651
|
+
process.stdout.write(` ${pc15.dim("rollback retag (3 ops)".padEnd(34))}`);
|
|
17652
|
+
const swapStart = Date.now();
|
|
17653
|
+
const swapResult = performRollbackSwap(PRODUCTION_SWAP_PLAN);
|
|
17654
|
+
const swapDur = `${((Date.now() - swapStart) / 1e3).toFixed(1)}s`;
|
|
17655
|
+
process.stdout.write(`${swapResult.ok ? pc15.green("\u2713") : pc15.red("\u2717")} ${swapDur}
|
|
17656
|
+
`);
|
|
17657
|
+
if (!swapResult.ok) {
|
|
17658
|
+
printError(`Rollback retag failed: ${swapResult.summary}`);
|
|
17659
|
+
process.exitCode = 1;
|
|
17660
|
+
return;
|
|
17661
|
+
}
|
|
17662
|
+
printInfo("Rollback", swapResult.summary);
|
|
17663
|
+
const cwd = process.cwd();
|
|
17664
|
+
const composeFile = path28.join(cwd, "packages/host-cp/compose.yaml");
|
|
17665
|
+
const authSecret = readAuthSecret2();
|
|
17666
|
+
process.stdout.write(` ${pc15.dim("docker compose recreate host-cp".padEnd(34))}`);
|
|
17667
|
+
const composeStart = Date.now();
|
|
17668
|
+
const composeResult = runCompose(["up", "-d", "--force-recreate", "host-cp"], composeFile, buildComposeEnv(authSecret));
|
|
17669
|
+
const composeDur = `${((Date.now() - composeStart) / 1e3).toFixed(1)}s`;
|
|
17670
|
+
process.stdout.write(`${composeResult.ok ? pc15.green("\u2713") : pc15.red("\u2717")} ${composeDur}
|
|
17671
|
+
`);
|
|
17672
|
+
if (!composeResult.ok) {
|
|
17673
|
+
printError(
|
|
17674
|
+
`Rollback compose recreate failed:
|
|
17675
|
+
${composeResult.stderr}
|
|
17676
|
+
Canonical tags are at :olam-rollback (good); container restart pending. Manually: \`docker compose -f packages/host-cp/compose.yaml up -d --force-recreate host-cp\`.`
|
|
17677
|
+
);
|
|
17678
|
+
process.exitCode = 1;
|
|
17679
|
+
return;
|
|
17680
|
+
}
|
|
17681
|
+
process.stdout.write(` ${pc15.dim("recreate auth-service".padEnd(34))}`);
|
|
17682
|
+
const authResult = await recreateAuthService();
|
|
17683
|
+
const authDur = `${(authResult.durationMs / 1e3).toFixed(1)}s`;
|
|
17684
|
+
process.stdout.write(`${authResult.ok ? pc15.green("\u2713") : pc15.red("\u2717")} ${authDur}
|
|
17685
|
+
`);
|
|
17686
|
+
if (!authResult.ok) {
|
|
17687
|
+
printError(`Auth-service recreate failed: ${authResult.error ?? "unknown"}`);
|
|
17688
|
+
process.exitCode = 1;
|
|
17689
|
+
return;
|
|
17690
|
+
}
|
|
17691
|
+
process.stdout.write("\n");
|
|
17692
|
+
printSuccess("Rollback complete \u2014 canonical tags restored from :olam-rollback");
|
|
17693
|
+
} finally {
|
|
17694
|
+
releaseLock();
|
|
17695
|
+
process.removeListener("SIGINT", releaseOnSignal);
|
|
17696
|
+
process.removeListener("SIGTERM", releaseOnSignal);
|
|
17697
|
+
}
|
|
17698
|
+
}
|
|
17699
|
+
async function runUpgradeStepsWithLockHeld(opts, cwd, logRow) {
|
|
16991
17700
|
if (opts.branch !== null) {
|
|
16992
17701
|
if (isGitDirty(cwd)) {
|
|
16993
17702
|
printError(
|
|
@@ -17045,6 +17754,17 @@ If there are conflicts, resolve them manually then re-run \`olam upgrade\`.`
|
|
|
17045
17754
|
process.exitCode = 1;
|
|
17046
17755
|
return;
|
|
17047
17756
|
}
|
|
17757
|
+
const _targetSha = captureHeadSha(cwd);
|
|
17758
|
+
logRow.sha_target = _targetSha ?? "";
|
|
17759
|
+
if (_targetSha === null) {
|
|
17760
|
+
logRow.failed_step = "capture HEAD SHA";
|
|
17761
|
+
printError(
|
|
17762
|
+
"Failed to capture HEAD SHA via `git rev-parse HEAD`. Aborting upgrade.\nRe-run from a clean git checkout; ensure `git rev-parse HEAD` returns a 40-char SHA."
|
|
17763
|
+
);
|
|
17764
|
+
process.exitCode = 1;
|
|
17765
|
+
return;
|
|
17766
|
+
}
|
|
17767
|
+
printInfo("Target SHA", abbreviateSha(_targetSha));
|
|
17048
17768
|
const installDecision = shouldSkipInstall(opts, cwd);
|
|
17049
17769
|
if (installDecision.skip) {
|
|
17050
17770
|
printInfo("npm install", `skipped \u2014 ${installDecision.reason}`);
|
|
@@ -17082,7 +17802,7 @@ ${buildResult.stderr}`);
|
|
|
17082
17802
|
return;
|
|
17083
17803
|
}
|
|
17084
17804
|
const authSecret = readAuthSecret2();
|
|
17085
|
-
const spaDir =
|
|
17805
|
+
const spaDir = path28.join(cwd, "packages/control-plane/app");
|
|
17086
17806
|
const spaResult = runStep2(
|
|
17087
17807
|
"vite build (SPA)",
|
|
17088
17808
|
"npx",
|
|
@@ -17104,21 +17824,107 @@ ${spaResult.stderr}`);
|
|
|
17104
17824
|
printTimings2(timings);
|
|
17105
17825
|
return;
|
|
17106
17826
|
}
|
|
17107
|
-
const
|
|
17108
|
-
|
|
17109
|
-
|
|
17110
|
-
|
|
17111
|
-
|
|
17112
|
-
{
|
|
17113
|
-
|
|
17114
|
-
|
|
17115
|
-
|
|
17116
|
-
|
|
17117
|
-
|
|
17827
|
+
const olamTagEnv = { OLAM_TAG: "olam-next" };
|
|
17828
|
+
if (opts.noCache) {
|
|
17829
|
+
olamTagEnv.DOCKER_BUILD_NO_CACHE = "1";
|
|
17830
|
+
}
|
|
17831
|
+
const buildScripts = [
|
|
17832
|
+
{ label: "bash build-auth.sh", relPath: "packages/adapters/src/docker/build-auth.sh", tee: false },
|
|
17833
|
+
{ label: "bash build-devbox.sh", relPath: "packages/adapters/src/docker/build-devbox.sh", tee: true },
|
|
17834
|
+
{ label: "bash build-host-cp.sh", relPath: "packages/adapters/src/docker/build-host-cp.sh", tee: false }
|
|
17835
|
+
];
|
|
17836
|
+
for (const step of buildScripts) {
|
|
17837
|
+
const scriptPath = path28.join(cwd, step.relPath);
|
|
17838
|
+
if (step.tee) {
|
|
17839
|
+
process.stdout.write(` ${pc15.dim(step.label.padEnd(34))}
|
|
17840
|
+
`);
|
|
17841
|
+
const start = Date.now();
|
|
17842
|
+
const result = spawnSync7("bash", [scriptPath], {
|
|
17843
|
+
stdio: "inherit",
|
|
17844
|
+
cwd,
|
|
17845
|
+
env: { ...process.env, ...olamTagEnv }
|
|
17846
|
+
});
|
|
17847
|
+
const durationMs = Date.now() - start;
|
|
17848
|
+
const ok = result.status === 0 && result.error === void 0;
|
|
17849
|
+
const dur = `${(durationMs / 1e3).toFixed(1)}s`;
|
|
17850
|
+
process.stdout.write(` ${pc15.dim(step.label.padEnd(34))}${ok ? pc15.green("\u2713") : pc15.red("\u2717")} ${dur}
|
|
17851
|
+
`);
|
|
17852
|
+
timings.push({ label: step.label, durationMs });
|
|
17853
|
+
if (!ok) {
|
|
17854
|
+
printError(`${step.label} failed (see output above for details).`);
|
|
17855
|
+
process.exitCode = 1;
|
|
17856
|
+
return;
|
|
17857
|
+
}
|
|
17858
|
+
} else {
|
|
17859
|
+
const result = runStep2(step.label, "bash", [scriptPath], {
|
|
17860
|
+
cwd,
|
|
17861
|
+
env: olamTagEnv
|
|
17862
|
+
});
|
|
17863
|
+
timings.push({ label: step.label, durationMs: result.durationMs });
|
|
17864
|
+
logRow.durations_ms[step.label] = result.durationMs;
|
|
17865
|
+
if (!result.ok) {
|
|
17866
|
+
logRow.failed_step = step.label;
|
|
17867
|
+
printError(`${step.label} failed:
|
|
17868
|
+
${result.stderr.split("\n").slice(-3).join("\n")}`);
|
|
17869
|
+
process.exitCode = 1;
|
|
17870
|
+
return;
|
|
17871
|
+
}
|
|
17872
|
+
}
|
|
17873
|
+
}
|
|
17874
|
+
for (const t of timings) logRow.durations_ms[t.label] = t.durationMs;
|
|
17875
|
+
const smokeStart = Date.now();
|
|
17876
|
+
process.stdout.write(` ${pc15.dim("smoke (docker create + inspect)".padEnd(34))}`);
|
|
17877
|
+
const smokeImages = [
|
|
17878
|
+
"olam-auth:olam-next",
|
|
17879
|
+
"olam-devbox:olam-next",
|
|
17880
|
+
"olam-host-cp:olam-next"
|
|
17881
|
+
];
|
|
17882
|
+
const smokeResults = smokeImages.map((img) => smokeImage(img, _targetSha));
|
|
17883
|
+
const smokeFailures = smokeResults.filter((r) => !r.ok);
|
|
17884
|
+
const smokeDurationMs = Date.now() - smokeStart;
|
|
17885
|
+
const smokeDur = `${(smokeDurationMs / 1e3).toFixed(1)}s`;
|
|
17886
|
+
process.stdout.write(`${smokeFailures.length === 0 ? pc15.green("\u2713") : pc15.red("\u2717")} ${smokeDur}
|
|
17887
|
+
`);
|
|
17888
|
+
timings.push({ label: "smoke", durationMs: smokeDurationMs });
|
|
17889
|
+
if (smokeFailures.length > 0) {
|
|
17890
|
+
printError(
|
|
17891
|
+
`Smoke failed for ${smokeFailures.length} of ${smokeResults.length} images:
|
|
17892
|
+
` + smokeFailures.map((r) => ` - ${r.image}: ${r.error}`).join("\n") + "\nCanonical tags (`:latest`/`:local`) untouched. Investigate the failed image(s), then re-run `olam upgrade` (--no-cache if cache-poisoning suspected)."
|
|
17893
|
+
);
|
|
17894
|
+
process.exitCode = 1;
|
|
17895
|
+
return;
|
|
17896
|
+
}
|
|
17897
|
+
const swapBoundarySha = captureHeadSha(cwd);
|
|
17898
|
+
if (swapBoundarySha !== null && swapBoundarySha !== _targetSha && !opts.force) {
|
|
17899
|
+
printError(
|
|
17900
|
+
`HEAD drifted during build window:
|
|
17901
|
+
captured (after pull): ${abbreviateSha(_targetSha)}
|
|
17902
|
+
current at swap: ${abbreviateSha(swapBoundarySha)}
|
|
17903
|
+
|
|
17904
|
+
Operator-driven \`git checkout\` or \`git reset\` triggered drift.
|
|
17905
|
+
Recovery options:
|
|
17906
|
+
\u2022 Re-run \`olam upgrade\` (will rebuild against current HEAD).
|
|
17907
|
+
\u2022 Pass \`--force\` to swap anyway (canonical advances to the
|
|
17908
|
+
captured-at-pull SHA, NOT current HEAD).`
|
|
17909
|
+
);
|
|
17910
|
+
process.exitCode = 1;
|
|
17911
|
+
return;
|
|
17912
|
+
}
|
|
17913
|
+
process.stdout.write(` ${pc15.dim("atomic 6-tag swap".padEnd(34))}`);
|
|
17914
|
+
const swapStart = Date.now();
|
|
17915
|
+
const swapResult = performAtomicSwap(PRODUCTION_SWAP_PLAN);
|
|
17916
|
+
const swapDurationMs = Date.now() - swapStart;
|
|
17917
|
+
const swapDur = `${(swapDurationMs / 1e3).toFixed(1)}s`;
|
|
17918
|
+
process.stdout.write(`${swapResult.ok ? pc15.green("\u2713") : pc15.red("\u2717")} ${swapDur}
|
|
17919
|
+
`);
|
|
17920
|
+
timings.push({ label: "atomic swap", durationMs: swapDurationMs });
|
|
17921
|
+
if (!swapResult.ok) {
|
|
17922
|
+
printError(`Atomic swap failed: ${swapResult.summary}`);
|
|
17118
17923
|
process.exitCode = 1;
|
|
17119
17924
|
return;
|
|
17120
17925
|
}
|
|
17121
|
-
|
|
17926
|
+
printInfo("Swap", swapResult.summary);
|
|
17927
|
+
const composeFile = path28.join(cwd, "packages/host-cp/compose.yaml");
|
|
17122
17928
|
process.stdout.write(` ${pc15.dim("docker compose recreate".padEnd(34))}`);
|
|
17123
17929
|
const composeStart = Date.now();
|
|
17124
17930
|
const composeResult = runCompose(
|
|
@@ -17133,8 +17939,33 @@ ${imageResult.stderr}`);
|
|
|
17133
17939
|
`);
|
|
17134
17940
|
timings.push({ label: "container recreate", durationMs: composeDurationMs });
|
|
17135
17941
|
if (!composeOk) {
|
|
17136
|
-
printError(
|
|
17137
|
-
|
|
17942
|
+
printError(
|
|
17943
|
+
`docker compose up --force-recreate failed:
|
|
17944
|
+
${composeResult.stderr}
|
|
17945
|
+
|
|
17946
|
+
Canonical tags advanced to new SHA but the stack failed to start.
|
|
17947
|
+
Recovery options:
|
|
17948
|
+
\u2022 Run \`olam upgrade --rollback\` to restore the prior :olam-rollback set, then investigate.
|
|
17949
|
+
\u2022 Manually \`docker logs olam-host-cp\` to diagnose; if recoverable, retry recreate without rollback.`
|
|
17950
|
+
);
|
|
17951
|
+
process.exitCode = 1;
|
|
17952
|
+
return;
|
|
17953
|
+
}
|
|
17954
|
+
process.stdout.write(` ${pc15.dim("recreate auth-service".padEnd(34))}`);
|
|
17955
|
+
const authResult = await recreateAuthService();
|
|
17956
|
+
const authDur = `${(authResult.durationMs / 1e3).toFixed(1)}s`;
|
|
17957
|
+
process.stdout.write(`${authResult.ok ? pc15.green("\u2713") : pc15.red("\u2717")} ${authDur}
|
|
17958
|
+
`);
|
|
17959
|
+
timings.push({ label: "auth recreate", durationMs: authResult.durationMs });
|
|
17960
|
+
if (!authResult.ok) {
|
|
17961
|
+
printError(
|
|
17962
|
+
`Auth-service recreate failed: ${authResult.error ?? "unknown"}
|
|
17963
|
+
|
|
17964
|
+
Canonical tags advanced to new SHA; host-cp recreated but auth-service is broken.
|
|
17965
|
+
Recovery options:
|
|
17966
|
+
\u2022 Run \`olam upgrade --rollback\` to restore the prior :olam-rollback set + working stack.
|
|
17967
|
+
\u2022 Manually: \`docker logs olam-auth\` to diagnose; \`olam auth up\` to restart.`
|
|
17968
|
+
);
|
|
17138
17969
|
process.exitCode = 1;
|
|
17139
17970
|
return;
|
|
17140
17971
|
}
|
|
@@ -17147,7 +17978,23 @@ ${composeResult.stderr}`);
|
|
|
17147
17978
|
`);
|
|
17148
17979
|
timings.push({ label: "/health", durationMs: healthDurationMs });
|
|
17149
17980
|
if (!healthy) {
|
|
17150
|
-
printWarning(
|
|
17981
|
+
printWarning(
|
|
17982
|
+
"Host CP started but /health did not respond within 10s.\n \u2022 Check: docker logs olam-host-cp\n \u2022 If the new SHA is broken: `olam upgrade --rollback` restores the prior set in <30s."
|
|
17983
|
+
);
|
|
17984
|
+
}
|
|
17985
|
+
process.stdout.write(` ${pc15.dim("verify /version/status round-trip".padEnd(34))}`);
|
|
17986
|
+
const versionStart = Date.now();
|
|
17987
|
+
const versionMatch = await waitForVersionMatch(_targetSha, 6e4);
|
|
17988
|
+
const versionDurationMs = Date.now() - versionStart;
|
|
17989
|
+
const versionDur = `${(versionDurationMs / 1e3).toFixed(1)}s`;
|
|
17990
|
+
process.stdout.write(`${versionMatch.matched ? pc15.green("\u2713") : pc15.yellow("?")} ${versionDur}
|
|
17991
|
+
`);
|
|
17992
|
+
timings.push({ label: "/version/status round-trip", durationMs: versionDurationMs });
|
|
17993
|
+
if (!versionMatch.matched) {
|
|
17994
|
+
printWarning(
|
|
17995
|
+
`Version round-trip incomplete after ${(versionDurationMs / 1e3).toFixed(0)}s:
|
|
17996
|
+
` + formatVersionMismatch(_targetSha, versionMatch.snapshot) + "\n \u2022 Banner may still show UPDATE AVAILABLE until host-cp's next poll cycle (~60s).\n \u2022 If the mismatch persists, `olam upgrade --rollback` restores the prior :olam-rollback set."
|
|
17997
|
+
);
|
|
17151
17998
|
}
|
|
17152
17999
|
process.stdout.write("\n");
|
|
17153
18000
|
printSuccess("Upgrade complete");
|
|
@@ -17164,10 +18011,22 @@ function printTimings2(timings) {
|
|
|
17164
18011
|
printInfo("total", `${(total / 1e3).toFixed(1)}s`);
|
|
17165
18012
|
}
|
|
17166
18013
|
function registerUpgrade(program2) {
|
|
17167
|
-
program2.command("upgrade").description("Self-upgrade the local Olam dev stack (pull + rebuild + restart
|
|
18014
|
+
program2.command("upgrade").description("Self-upgrade the local Olam dev stack (pull + rebuild + restart all three components)").option("-y, --yes", "Skip the confirmation prompt").option("--skip-image", "Skip docker image rebuild + container recreate (source rebuild only)").option(
|
|
17168
18015
|
"--skip-install",
|
|
17169
18016
|
"Skip npm install entirely (use existing node_modules as-is). Useful when a native-module build failure blocks the normal upgrade path."
|
|
17170
|
-
).option("--branch <name>", "Switch to this branch before pulling (refuses if working tree is dirty)").
|
|
18017
|
+
).option("--branch <name>", "Switch to this branch before pulling (refuses if working tree is dirty)").option(
|
|
18018
|
+
"--rollback",
|
|
18019
|
+
"Restore canonical tags from the :olam-rollback set (created by the prior successful upgrade).\n No git pull, no build, no smoke \u2014 just retag + recreate."
|
|
18020
|
+
).option(
|
|
18021
|
+
"--force",
|
|
18022
|
+
"Bypass HEAD-drift refusal at the swap boundary. Swap advances canonical to the\n captured-at-pull SHA even if current HEAD differs."
|
|
18023
|
+
).option(
|
|
18024
|
+
"--no-cache",
|
|
18025
|
+
"Pass --no-cache to all three build scripts (DOCKER_BUILD_NO_CACHE=1).\n Useful when retrying after a cache-poisoning failure."
|
|
18026
|
+
).option(
|
|
18027
|
+
"--history",
|
|
18028
|
+
"Print the upgrade history (~/.olam/upgrade.log) and exit.\n No upgrade is performed."
|
|
18029
|
+
).option("-n <count>", "Number of history rows to print (default 10)", "10").option("--json", "Emit history as JSONL instead of a table").action(async (opts) => {
|
|
17171
18030
|
await handleUpgrade(parseUpgradeOpts(opts));
|
|
17172
18031
|
});
|
|
17173
18032
|
}
|
|
@@ -17301,7 +18160,7 @@ function registerLogs(program2) {
|
|
|
17301
18160
|
// src/commands/ps.ts
|
|
17302
18161
|
init_context();
|
|
17303
18162
|
import pc17 from "picocolors";
|
|
17304
|
-
import { spawnSync as
|
|
18163
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
17305
18164
|
var SAFE_IDENT4 = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
17306
18165
|
function parseDockerTop(stdout) {
|
|
17307
18166
|
const trimmed = stdout.trim();
|
|
@@ -17401,7 +18260,7 @@ function registerPs(program2) {
|
|
|
17401
18260
|
const containerName = `olam-${worldId}-devbox`;
|
|
17402
18261
|
let watchInterval;
|
|
17403
18262
|
function fetchAndPrint() {
|
|
17404
|
-
const result =
|
|
18263
|
+
const result = spawnSync8(
|
|
17405
18264
|
"docker",
|
|
17406
18265
|
["top", containerName, "pid", "user", "pcpu", "pmem", "stime", "stat", "cmd"],
|
|
17407
18266
|
{ encoding: "utf-8", timeout: 3e3 }
|
|
@@ -17437,20 +18296,20 @@ ${pc17.dim(`world: ${worldId} sort: ${sortKey} refresh: 5s Ctrl-C to exit`)}
|
|
|
17437
18296
|
}
|
|
17438
18297
|
|
|
17439
18298
|
// src/commands/keys.ts
|
|
17440
|
-
import * as
|
|
17441
|
-
import * as
|
|
17442
|
-
import * as
|
|
18299
|
+
import * as fs25 from "node:fs";
|
|
18300
|
+
import * as os15 from "node:os";
|
|
18301
|
+
import * as path29 from "node:path";
|
|
17443
18302
|
import YAML4 from "yaml";
|
|
17444
18303
|
function olamHome2() {
|
|
17445
|
-
return process.env.OLAM_HOME ??
|
|
18304
|
+
return process.env.OLAM_HOME ?? path29.join(os15.homedir(), ".olam");
|
|
17446
18305
|
}
|
|
17447
18306
|
function keysFilePath() {
|
|
17448
|
-
return
|
|
18307
|
+
return path29.join(olamHome2(), "keys.yaml");
|
|
17449
18308
|
}
|
|
17450
18309
|
function readKeysFile() {
|
|
17451
18310
|
const filePath = keysFilePath();
|
|
17452
|
-
if (!
|
|
17453
|
-
const raw =
|
|
18311
|
+
if (!fs25.existsSync(filePath)) return null;
|
|
18312
|
+
const raw = fs25.readFileSync(filePath, "utf-8").trim();
|
|
17454
18313
|
if (raw.length === 0) return null;
|
|
17455
18314
|
try {
|
|
17456
18315
|
const parsed = YAML4.parse(raw);
|
|
@@ -17466,13 +18325,13 @@ function readKeysFile() {
|
|
|
17466
18325
|
}
|
|
17467
18326
|
function writeKeysFile(keys) {
|
|
17468
18327
|
const dir = olamHome2();
|
|
17469
|
-
if (!
|
|
17470
|
-
|
|
18328
|
+
if (!fs25.existsSync(dir)) {
|
|
18329
|
+
fs25.mkdirSync(dir, { recursive: true });
|
|
17471
18330
|
}
|
|
17472
18331
|
const filePath = keysFilePath();
|
|
17473
18332
|
const content = YAML4.stringify(keys);
|
|
17474
|
-
|
|
17475
|
-
|
|
18333
|
+
fs25.writeFileSync(filePath, content, { encoding: "utf-8", mode: 384 });
|
|
18334
|
+
fs25.chmodSync(filePath, 384);
|
|
17476
18335
|
}
|
|
17477
18336
|
function redact(value) {
|
|
17478
18337
|
if (value.length <= 8) return value + "...";
|
|
@@ -17515,7 +18374,7 @@ function registerKeys(program2) {
|
|
|
17515
18374
|
}
|
|
17516
18375
|
const { [key]: _removed, ...rest } = existing;
|
|
17517
18376
|
if (Object.keys(rest).length === 0) {
|
|
17518
|
-
|
|
18377
|
+
fs25.unlinkSync(keysFilePath());
|
|
17519
18378
|
} else {
|
|
17520
18379
|
writeKeysFile(rest);
|
|
17521
18380
|
}
|
|
@@ -17538,26 +18397,26 @@ function registerKeys(program2) {
|
|
|
17538
18397
|
}
|
|
17539
18398
|
|
|
17540
18399
|
// src/commands/world-snapshot.ts
|
|
17541
|
-
import * as
|
|
17542
|
-
import * as
|
|
18400
|
+
import * as fs27 from "node:fs";
|
|
18401
|
+
import * as path31 from "node:path";
|
|
17543
18402
|
import { execSync as execSync9 } from "node:child_process";
|
|
17544
18403
|
import pc18 from "picocolors";
|
|
17545
18404
|
|
|
17546
18405
|
// ../core/src/world/snapshot.ts
|
|
17547
18406
|
import * as crypto6 from "node:crypto";
|
|
17548
|
-
import * as
|
|
17549
|
-
import * as
|
|
17550
|
-
import * as
|
|
18407
|
+
import * as fs26 from "node:fs";
|
|
18408
|
+
import * as os16 from "node:os";
|
|
18409
|
+
import * as path30 from "node:path";
|
|
17551
18410
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
17552
18411
|
function snapshotsDir() {
|
|
17553
|
-
return process.env["OLAM_SNAPSHOTS_DIR"] ??
|
|
18412
|
+
return process.env["OLAM_SNAPSHOTS_DIR"] ?? path30.join(os16.homedir(), ".olam", "snapshots");
|
|
17554
18413
|
}
|
|
17555
18414
|
function snapshotKindDir(worldId, kind) {
|
|
17556
|
-
return
|
|
18415
|
+
return path30.join(snapshotsDir(), worldId, kind);
|
|
17557
18416
|
}
|
|
17558
18417
|
function snapshotTarPath(worldId, kind, repoName, hash) {
|
|
17559
18418
|
const base = repoName ? `${repoName}-${hash}` : hash;
|
|
17560
|
-
return
|
|
18419
|
+
return path30.join(snapshotKindDir(worldId, kind), `${base}.tar.gz`);
|
|
17561
18420
|
}
|
|
17562
18421
|
function manifestPath(tarPath) {
|
|
17563
18422
|
return tarPath.replace(/\.tar\.gz$/, ".manifest.json");
|
|
@@ -17574,16 +18433,16 @@ function hashBuffers(entries) {
|
|
|
17574
18433
|
return hash.digest("hex").slice(0, 12);
|
|
17575
18434
|
}
|
|
17576
18435
|
function computeGemsFingerprint(repoDir) {
|
|
17577
|
-
const lockfile =
|
|
17578
|
-
if (!
|
|
17579
|
-
return hashBuffers([{ path: "Gemfile.lock", content:
|
|
18436
|
+
const lockfile = path30.join(repoDir, "Gemfile.lock");
|
|
18437
|
+
if (!fs26.existsSync(lockfile)) return null;
|
|
18438
|
+
return hashBuffers([{ path: "Gemfile.lock", content: fs26.readFileSync(lockfile) }]);
|
|
17580
18439
|
}
|
|
17581
18440
|
function computeNodeFingerprint(repoDir) {
|
|
17582
18441
|
const candidates = ["yarn.lock", "pnpm-lock.yaml", "package-lock.json"];
|
|
17583
18442
|
for (const name of candidates) {
|
|
17584
|
-
const lockfile =
|
|
17585
|
-
if (
|
|
17586
|
-
return hashBuffers([{ path: name, content:
|
|
18443
|
+
const lockfile = path30.join(repoDir, name);
|
|
18444
|
+
if (fs26.existsSync(lockfile)) {
|
|
18445
|
+
return hashBuffers([{ path: name, content: fs26.readFileSync(lockfile) }]);
|
|
17587
18446
|
}
|
|
17588
18447
|
}
|
|
17589
18448
|
return null;
|
|
@@ -17593,59 +18452,59 @@ function computePgFingerprint(repoDirs) {
|
|
|
17593
18452
|
const entries = [];
|
|
17594
18453
|
for (const repoDir of repoDirs) {
|
|
17595
18454
|
for (const pattern of patterns) {
|
|
17596
|
-
const filePath =
|
|
17597
|
-
if (
|
|
17598
|
-
entries.push({ path: filePath, content:
|
|
18455
|
+
const filePath = path30.join(repoDir, pattern);
|
|
18456
|
+
if (fs26.existsSync(filePath)) {
|
|
18457
|
+
entries.push({ path: filePath, content: fs26.readFileSync(filePath) });
|
|
17599
18458
|
}
|
|
17600
18459
|
}
|
|
17601
18460
|
}
|
|
17602
18461
|
return entries.length > 0 ? hashBuffers(entries) : null;
|
|
17603
18462
|
}
|
|
17604
18463
|
function packTarball(srcDir, destPath, opts = {}) {
|
|
17605
|
-
|
|
18464
|
+
fs26.mkdirSync(path30.dirname(destPath), { recursive: true });
|
|
17606
18465
|
const tmp = `${destPath}.tmp`;
|
|
17607
18466
|
const args = [];
|
|
17608
18467
|
if (opts.followSymlinks) args.push("-h");
|
|
17609
18468
|
args.push("-czf", tmp, "-C", srcDir, ".");
|
|
17610
18469
|
try {
|
|
17611
18470
|
execFileSync4("tar", args, { stdio: "pipe" });
|
|
17612
|
-
|
|
18471
|
+
fs26.renameSync(tmp, destPath);
|
|
17613
18472
|
} catch (err) {
|
|
17614
18473
|
try {
|
|
17615
|
-
|
|
18474
|
+
fs26.rmSync(tmp, { force: true });
|
|
17616
18475
|
} catch {
|
|
17617
18476
|
}
|
|
17618
18477
|
throw err;
|
|
17619
18478
|
}
|
|
17620
18479
|
}
|
|
17621
18480
|
function writeManifest(manifest, tarPath) {
|
|
17622
|
-
|
|
18481
|
+
fs26.writeFileSync(manifestPath(tarPath), JSON.stringify(manifest, null, 2), "utf-8");
|
|
17623
18482
|
}
|
|
17624
18483
|
function readManifest(tarPath) {
|
|
17625
18484
|
const mPath = manifestPath(tarPath);
|
|
17626
|
-
if (!
|
|
18485
|
+
if (!fs26.existsSync(mPath)) return null;
|
|
17627
18486
|
try {
|
|
17628
|
-
return JSON.parse(
|
|
18487
|
+
return JSON.parse(fs26.readFileSync(mPath, "utf-8"));
|
|
17629
18488
|
} catch {
|
|
17630
18489
|
return null;
|
|
17631
18490
|
}
|
|
17632
18491
|
}
|
|
17633
18492
|
function listSnapshots(worldIdFilter) {
|
|
17634
18493
|
const root = snapshotsDir();
|
|
17635
|
-
if (!
|
|
18494
|
+
if (!fs26.existsSync(root)) return [];
|
|
17636
18495
|
const now = Date.now();
|
|
17637
18496
|
const results = [];
|
|
17638
|
-
const worlds = worldIdFilter ? [worldIdFilter] :
|
|
18497
|
+
const worlds = worldIdFilter ? [worldIdFilter] : fs26.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
17639
18498
|
for (const worldId of worlds) {
|
|
17640
|
-
const worldDir =
|
|
17641
|
-
if (!
|
|
18499
|
+
const worldDir = path30.join(root, worldId);
|
|
18500
|
+
if (!fs26.existsSync(worldDir) || !fs26.statSync(worldDir).isDirectory()) continue;
|
|
17642
18501
|
for (const kind of ["gems", "node", "pg"]) {
|
|
17643
|
-
const kindDir =
|
|
17644
|
-
if (!
|
|
17645
|
-
const tarballs =
|
|
18502
|
+
const kindDir = path30.join(worldDir, kind);
|
|
18503
|
+
if (!fs26.existsSync(kindDir)) continue;
|
|
18504
|
+
const tarballs = fs26.readdirSync(kindDir).filter((f) => f.endsWith(".tar.gz"));
|
|
17646
18505
|
for (const tarFile of tarballs) {
|
|
17647
|
-
const tarPath =
|
|
17648
|
-
const stat =
|
|
18506
|
+
const tarPath = path30.join(kindDir, tarFile);
|
|
18507
|
+
const stat = fs26.statSync(tarPath);
|
|
17649
18508
|
const manifest = readManifest(tarPath);
|
|
17650
18509
|
if (!manifest) continue;
|
|
17651
18510
|
results.push({ manifest, tarPath, ageMs: now - stat.mtimeMs });
|
|
@@ -17724,17 +18583,17 @@ function resolveKinds(arg) {
|
|
|
17724
18583
|
return [];
|
|
17725
18584
|
}
|
|
17726
18585
|
async function captureGems(worldId, workspacePath, repo) {
|
|
17727
|
-
const repoDir =
|
|
18586
|
+
const repoDir = path31.join(workspacePath, repo);
|
|
17728
18587
|
const fingerprint = computeGemsFingerprint(repoDir);
|
|
17729
18588
|
if (!fingerprint) {
|
|
17730
18589
|
return { ok: false, tarPath: "", msg: "no Gemfile.lock \u2014 layer does not apply" };
|
|
17731
18590
|
}
|
|
17732
18591
|
const tarPath = snapshotTarPath(worldId, "gems", repo, fingerprint);
|
|
17733
|
-
const vendorBundle =
|
|
17734
|
-
if (
|
|
18592
|
+
const vendorBundle = path31.join(repoDir, "vendor", "bundle");
|
|
18593
|
+
if (fs27.existsSync(vendorBundle)) {
|
|
17735
18594
|
try {
|
|
17736
18595
|
packTarball(vendorBundle, tarPath);
|
|
17737
|
-
const stat =
|
|
18596
|
+
const stat = fs27.statSync(tarPath);
|
|
17738
18597
|
const manifest = {
|
|
17739
18598
|
kind: "gems",
|
|
17740
18599
|
worldId,
|
|
@@ -17767,10 +18626,10 @@ async function captureGems(worldId, workspacePath, repo) {
|
|
|
17767
18626
|
`docker exec ${containerName} sh -c 'mkdir -p "$(dirname ${tmpTar})" && tar -czf ${tmpTar}.tmp -C ${bundlePath} . && mv ${tmpTar}.tmp ${tmpTar}'`,
|
|
17768
18627
|
{ stdio: "pipe", timeout: 12e4 }
|
|
17769
18628
|
);
|
|
17770
|
-
|
|
18629
|
+
fs27.mkdirSync(path31.dirname(tarPath), { recursive: true });
|
|
17771
18630
|
execSync9(`docker cp ${containerName}:${tmpTar} "${tarPath}"`, { stdio: "pipe", timeout: 12e4 });
|
|
17772
18631
|
execSync9(`docker exec ${containerName} rm -f ${tmpTar}`, { stdio: "pipe" });
|
|
17773
|
-
const stat =
|
|
18632
|
+
const stat = fs27.statSync(tarPath);
|
|
17774
18633
|
const manifest = {
|
|
17775
18634
|
kind: "gems",
|
|
17776
18635
|
worldId,
|
|
@@ -17787,19 +18646,19 @@ async function captureGems(worldId, workspacePath, repo) {
|
|
|
17787
18646
|
}
|
|
17788
18647
|
}
|
|
17789
18648
|
async function captureNode(worldId, workspacePath, repo) {
|
|
17790
|
-
const repoDir =
|
|
18649
|
+
const repoDir = path31.join(workspacePath, repo);
|
|
17791
18650
|
const fingerprint = computeNodeFingerprint(repoDir);
|
|
17792
18651
|
if (!fingerprint) {
|
|
17793
18652
|
return { ok: false, tarPath: "", msg: "no lockfile \u2014 layer does not apply" };
|
|
17794
18653
|
}
|
|
17795
|
-
const nodeModules =
|
|
17796
|
-
if (!
|
|
18654
|
+
const nodeModules = path31.join(repoDir, "node_modules");
|
|
18655
|
+
if (!fs27.existsSync(nodeModules)) {
|
|
17797
18656
|
return { ok: false, tarPath: "", msg: "node_modules not installed yet" };
|
|
17798
18657
|
}
|
|
17799
18658
|
const tarPath = snapshotTarPath(worldId, "node", repo, fingerprint);
|
|
17800
18659
|
try {
|
|
17801
18660
|
packTarball(nodeModules, tarPath);
|
|
17802
|
-
const stat =
|
|
18661
|
+
const stat = fs27.statSync(tarPath);
|
|
17803
18662
|
const manifest = {
|
|
17804
18663
|
kind: "node",
|
|
17805
18664
|
worldId,
|
|
@@ -17816,7 +18675,7 @@ async function captureNode(worldId, workspacePath, repo) {
|
|
|
17816
18675
|
}
|
|
17817
18676
|
}
|
|
17818
18677
|
async function capturePg(worldId, workspacePath, repoNames) {
|
|
17819
|
-
const repoDirs = repoNames.map((r) =>
|
|
18678
|
+
const repoDirs = repoNames.map((r) => path31.join(workspacePath, r));
|
|
17820
18679
|
const fingerprint = computePgFingerprint(repoDirs);
|
|
17821
18680
|
if (!fingerprint) {
|
|
17822
18681
|
return { ok: false, tarPath: "", msg: "no Gemfile.lock / schema.rb \u2014 layer does not apply" };
|
|
@@ -17831,13 +18690,13 @@ async function capturePg(worldId, workspacePath, repoNames) {
|
|
|
17831
18690
|
}
|
|
17832
18691
|
try {
|
|
17833
18692
|
execSync9(`docker stop ${containerName}`, { stdio: "pipe", timeout: 3e4 });
|
|
17834
|
-
|
|
18693
|
+
fs27.mkdirSync(path31.dirname(tarPath), { recursive: true });
|
|
17835
18694
|
execSync9(
|
|
17836
|
-
`docker run --rm -v "${volumeName2}:/pgdata:ro" -v "${
|
|
18695
|
+
`docker run --rm -v "${volumeName2}:/pgdata:ro" -v "${path31.dirname(tarPath)}:/dest" alpine sh -c 'tar -czf /dest/${path31.basename(tarPath)}.tmp -C /pgdata . && mv /dest/${path31.basename(tarPath)}.tmp /dest/${path31.basename(tarPath)}'`,
|
|
17837
18696
|
{ stdio: "pipe", timeout: 18e4 }
|
|
17838
18697
|
);
|
|
17839
18698
|
execSync9(`docker start ${containerName}`, { stdio: "pipe", timeout: 3e4 });
|
|
17840
|
-
const stat =
|
|
18699
|
+
const stat = fs27.statSync(tarPath);
|
|
17841
18700
|
const manifest = {
|
|
17842
18701
|
kind: "pg",
|
|
17843
18702
|
worldId,
|
|
@@ -17911,35 +18770,35 @@ function formatAge2(ms) {
|
|
|
17911
18770
|
|
|
17912
18771
|
// src/commands/refresh.ts
|
|
17913
18772
|
init_context();
|
|
17914
|
-
import * as
|
|
17915
|
-
import * as
|
|
17916
|
-
import * as
|
|
17917
|
-
import { spawnSync as
|
|
18773
|
+
import * as fs29 from "node:fs";
|
|
18774
|
+
import * as os17 from "node:os";
|
|
18775
|
+
import * as path33 from "node:path";
|
|
18776
|
+
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
17918
18777
|
import ora5 from "ora";
|
|
17919
18778
|
|
|
17920
18779
|
// src/commands/refresh-helpers.ts
|
|
17921
|
-
import * as
|
|
17922
|
-
import * as
|
|
18780
|
+
import * as fs28 from "node:fs";
|
|
18781
|
+
import * as path32 from "node:path";
|
|
17923
18782
|
function collectCpSourceFiles(standaloneDir) {
|
|
17924
|
-
if (!
|
|
18783
|
+
if (!fs28.existsSync(standaloneDir)) {
|
|
17925
18784
|
throw new Error(`CP standalone dir not found: ${standaloneDir}`);
|
|
17926
18785
|
}
|
|
17927
18786
|
const entries = [];
|
|
17928
|
-
const topLevel =
|
|
17929
|
-
const stat =
|
|
18787
|
+
const topLevel = fs28.readdirSync(standaloneDir).filter((f) => {
|
|
18788
|
+
const stat = fs28.statSync(path32.join(standaloneDir, f));
|
|
17930
18789
|
return stat.isFile() && f.endsWith(".mjs") && !f.endsWith(".test.mjs");
|
|
17931
18790
|
}).sort();
|
|
17932
18791
|
for (const f of topLevel) {
|
|
17933
|
-
entries.push({ srcPath:
|
|
18792
|
+
entries.push({ srcPath: path32.join(standaloneDir, f), destRelPath: f });
|
|
17934
18793
|
}
|
|
17935
|
-
const libDir =
|
|
17936
|
-
if (
|
|
17937
|
-
const libFiles =
|
|
17938
|
-
const stat =
|
|
18794
|
+
const libDir = path32.join(standaloneDir, "lib");
|
|
18795
|
+
if (fs28.existsSync(libDir) && fs28.statSync(libDir).isDirectory()) {
|
|
18796
|
+
const libFiles = fs28.readdirSync(libDir).filter((f) => {
|
|
18797
|
+
const stat = fs28.statSync(path32.join(libDir, f));
|
|
17939
18798
|
return stat.isFile() && f.endsWith(".mjs") && !f.endsWith(".test.mjs");
|
|
17940
18799
|
}).sort();
|
|
17941
18800
|
for (const f of libFiles) {
|
|
17942
|
-
entries.push({ srcPath:
|
|
18801
|
+
entries.push({ srcPath: path32.join(libDir, f), destRelPath: `lib/${f}` });
|
|
17943
18802
|
}
|
|
17944
18803
|
}
|
|
17945
18804
|
return entries;
|
|
@@ -17958,7 +18817,7 @@ var RESTART_TIMEOUT_S = 30;
|
|
|
17958
18817
|
var HEALTH_POLL_MS = 500;
|
|
17959
18818
|
var HEALTH_TIMEOUT_MS = 3e4;
|
|
17960
18819
|
function docker(args) {
|
|
17961
|
-
const result =
|
|
18820
|
+
const result = spawnSync9("docker", args, {
|
|
17962
18821
|
encoding: "utf-8",
|
|
17963
18822
|
stdio: ["ignore", "pipe", "pipe"]
|
|
17964
18823
|
});
|
|
@@ -17997,16 +18856,16 @@ async function refreshWorld(worldId, portOffset, standaloneDir, opts) {
|
|
|
17997
18856
|
error: err instanceof Error ? err.message : String(err)
|
|
17998
18857
|
};
|
|
17999
18858
|
}
|
|
18000
|
-
const stagingDir =
|
|
18001
|
-
|
|
18859
|
+
const stagingDir = fs29.mkdtempSync(
|
|
18860
|
+
path33.join(os17.tmpdir(), `olam-refresh-${worldId}-`)
|
|
18002
18861
|
);
|
|
18003
18862
|
try {
|
|
18004
18863
|
const hasLib = entries.some((e) => e.destRelPath.startsWith("lib/"));
|
|
18005
18864
|
if (hasLib) {
|
|
18006
|
-
|
|
18865
|
+
fs29.mkdirSync(path33.join(stagingDir, "lib"), { recursive: true });
|
|
18007
18866
|
}
|
|
18008
18867
|
for (const { srcPath, destRelPath } of entries) {
|
|
18009
|
-
|
|
18868
|
+
fs29.copyFileSync(srcPath, path33.join(stagingDir, destRelPath));
|
|
18010
18869
|
}
|
|
18011
18870
|
const cpResult = docker([
|
|
18012
18871
|
"cp",
|
|
@@ -18021,7 +18880,7 @@ async function refreshWorld(worldId, portOffset, standaloneDir, opts) {
|
|
|
18021
18880
|
};
|
|
18022
18881
|
}
|
|
18023
18882
|
} finally {
|
|
18024
|
-
|
|
18883
|
+
fs29.rmSync(stagingDir, { recursive: true, force: true });
|
|
18025
18884
|
}
|
|
18026
18885
|
if (opts.restart) {
|
|
18027
18886
|
const restartResult = docker([
|
|
@@ -18058,11 +18917,11 @@ function registerRefresh(program2) {
|
|
|
18058
18917
|
process.exitCode = 1;
|
|
18059
18918
|
return;
|
|
18060
18919
|
}
|
|
18061
|
-
const standaloneDir =
|
|
18920
|
+
const standaloneDir = path33.join(
|
|
18062
18921
|
process.cwd(),
|
|
18063
18922
|
"packages/control-plane/standalone"
|
|
18064
18923
|
);
|
|
18065
|
-
if (!
|
|
18924
|
+
if (!fs29.existsSync(standaloneDir)) {
|
|
18066
18925
|
printError(
|
|
18067
18926
|
`CP standalone source not found at ${standaloneDir}.
|
|
18068
18927
|
Run \`olam refresh\` from the olam repo root.`
|
|
@@ -18141,18 +19000,18 @@ Run \`olam refresh\` from the olam repo root.`
|
|
|
18141
19000
|
}
|
|
18142
19001
|
|
|
18143
19002
|
// src/pleri-config.ts
|
|
18144
|
-
import * as
|
|
18145
|
-
import * as
|
|
19003
|
+
import * as fs30 from "node:fs";
|
|
19004
|
+
import * as path34 from "node:path";
|
|
18146
19005
|
function isPleriConfigured(configDir = process.env.OLAM_CONFIG_DIR ?? ".olam") {
|
|
18147
19006
|
if (process.env.PLERI_BASE_URL) {
|
|
18148
19007
|
return true;
|
|
18149
19008
|
}
|
|
18150
|
-
const configPath =
|
|
18151
|
-
if (!
|
|
19009
|
+
const configPath = path34.join(configDir, "config.yaml");
|
|
19010
|
+
if (!fs30.existsSync(configPath)) {
|
|
18152
19011
|
return false;
|
|
18153
19012
|
}
|
|
18154
19013
|
try {
|
|
18155
|
-
const contents =
|
|
19014
|
+
const contents = fs30.readFileSync(configPath, "utf8");
|
|
18156
19015
|
return /^[^#\n]*\bpleri:/m.test(contents);
|
|
18157
19016
|
} catch {
|
|
18158
19017
|
return false;
|
|
@@ -18161,7 +19020,24 @@ function isPleriConfigured(configDir = process.env.OLAM_CONFIG_DIR ?? ".olam") {
|
|
|
18161
19020
|
|
|
18162
19021
|
// src/index.ts
|
|
18163
19022
|
var program = new Command();
|
|
18164
|
-
|
|
19023
|
+
function readCliVersion() {
|
|
19024
|
+
try {
|
|
19025
|
+
const here = path35.dirname(fileURLToPath3(import.meta.url));
|
|
19026
|
+
for (const candidate of [
|
|
19027
|
+
path35.join(here, "package.json"),
|
|
19028
|
+
path35.join(here, "..", "package.json"),
|
|
19029
|
+
path35.join(here, "..", "..", "package.json")
|
|
19030
|
+
]) {
|
|
19031
|
+
if (fs31.existsSync(candidate)) {
|
|
19032
|
+
const pkg = JSON.parse(fs31.readFileSync(candidate, "utf-8"));
|
|
19033
|
+
if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
|
|
19034
|
+
}
|
|
19035
|
+
}
|
|
19036
|
+
} catch {
|
|
19037
|
+
}
|
|
19038
|
+
return "0.0.0-unknown";
|
|
19039
|
+
}
|
|
19040
|
+
program.name("olam").description("Olam \u2014 isolated development worlds with thought graph capture").version(readCliVersion());
|
|
18165
19041
|
registerInit(program);
|
|
18166
19042
|
registerInstall(program);
|
|
18167
19043
|
registerAuth(program);
|