@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
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: path35, errorMaps, issueData } = params;
|
|
425
|
+
const fullPath = [...path35, ...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, path35, key) {
|
|
734
734
|
this._cachedPath = [];
|
|
735
735
|
this.parent = parent;
|
|
736
736
|
this.data = value;
|
|
737
|
-
this._path =
|
|
737
|
+
this._path = path35;
|
|
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, path35, 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: [...path35, key],
|
|
4233
4233
|
message: `forbidden key "${key}" (prototype-pollution surface)`
|
|
4234
4234
|
});
|
|
4235
4235
|
continue;
|
|
4236
4236
|
}
|
|
4237
|
-
if (rejectSource &&
|
|
4237
|
+
if (rejectSource && path35.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
|
+
[...path35, key],
|
|
4248
4248
|
ctx,
|
|
4249
4249
|
false
|
|
4250
4250
|
);
|
|
4251
4251
|
}
|
|
4252
4252
|
}
|
|
4253
|
-
function rejectForbiddenKeys(value,
|
|
4253
|
+
function rejectForbiddenKeys(value, path35, 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] ${path35}: 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] ${path35}: 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
|
+
`${path35}.${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, path35, body, attempt = 0) {
|
|
5212
|
+
const url = `${this.baseUrl}${path35}`;
|
|
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, path35, 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(path35, method, body) {
|
|
6680
|
+
const url = `${this.config.workerUrl}${path35}`;
|
|
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, {
|
|
@@ -12249,11 +12304,11 @@ var UnknownArchetypeError = class extends Error {
|
|
|
12249
12304
|
known;
|
|
12250
12305
|
};
|
|
12251
12306
|
var ArchetypeCycleError = class extends Error {
|
|
12252
|
-
constructor(
|
|
12307
|
+
constructor(path35) {
|
|
12253
12308
|
super(
|
|
12254
|
-
`Archetype inheritance cycle detected: ${
|
|
12309
|
+
`Archetype inheritance cycle detected: ${path35.join(" \u2192 ")} \u2192 ${path35[0] ?? "?"}`
|
|
12255
12310
|
);
|
|
12256
|
-
this.path =
|
|
12311
|
+
this.path = path35;
|
|
12257
12312
|
this.name = "ArchetypeCycleError";
|
|
12258
12313
|
}
|
|
12259
12314
|
path;
|
|
@@ -13072,10 +13127,10 @@ async function readHostCpToken2() {
|
|
|
13072
13127
|
if (!fs19.existsSync(tp)) return null;
|
|
13073
13128
|
return fs19.readFileSync(tp, "utf-8").trim();
|
|
13074
13129
|
}
|
|
13075
|
-
async function callHostCpProxy(method, worldId,
|
|
13130
|
+
async function callHostCpProxy(method, worldId, path35, body) {
|
|
13076
13131
|
const token = await readHostCpToken2();
|
|
13077
13132
|
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)}${
|
|
13133
|
+
const url = `http://127.0.0.1:${HOST_CP_PORT}/api/world/${encodeURIComponent(worldId)}${path35}`;
|
|
13079
13134
|
try {
|
|
13080
13135
|
const headers = {
|
|
13081
13136
|
Authorization: `Bearer ${token}`
|
|
@@ -13686,9 +13741,9 @@ function formatFreshnessWarning(result, image = DEFAULT_DEVBOX_IMAGE) {
|
|
|
13686
13741
|
"These source files have changed since the image was built; the",
|
|
13687
13742
|
"changes will NOT take effect in fresh worlds until you rebuild:"
|
|
13688
13743
|
];
|
|
13689
|
-
for (const { path:
|
|
13744
|
+
for (const { path: path35, mtimeMs } of result.newerSources) {
|
|
13690
13745
|
const when = new Date(mtimeMs).toISOString();
|
|
13691
|
-
lines.push(` \u2022 ${
|
|
13746
|
+
lines.push(` \u2022 ${path35} (modified ${when})`);
|
|
13692
13747
|
}
|
|
13693
13748
|
lines.push("");
|
|
13694
13749
|
lines.push("Rebuild with:");
|
|
@@ -13848,15 +13903,15 @@ init_context();
|
|
|
13848
13903
|
var HOST_CP_URL = "http://127.0.0.1:19000";
|
|
13849
13904
|
async function readHostCpTokenForCreate() {
|
|
13850
13905
|
try {
|
|
13851
|
-
const { default:
|
|
13852
|
-
const { default:
|
|
13853
|
-
const { default:
|
|
13854
|
-
const tp =
|
|
13855
|
-
process.env.OLAM_HOME ??
|
|
13906
|
+
const { default: fs31 } = await import("node:fs");
|
|
13907
|
+
const { default: os18 } = await import("node:os");
|
|
13908
|
+
const { default: path35 } = await import("node:path");
|
|
13909
|
+
const tp = path35.join(
|
|
13910
|
+
process.env.OLAM_HOME ?? path35.join(os18.homedir(), ".olam"),
|
|
13856
13911
|
"host-cp.token"
|
|
13857
13912
|
);
|
|
13858
|
-
if (!
|
|
13859
|
-
return
|
|
13913
|
+
if (!fs31.existsSync(tp)) return null;
|
|
13914
|
+
return fs31.readFileSync(tp, "utf-8").trim();
|
|
13860
13915
|
} catch {
|
|
13861
13916
|
return null;
|
|
13862
13917
|
}
|
|
@@ -13865,7 +13920,23 @@ function registerCreate(program2) {
|
|
|
13865
13920
|
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
13921
|
"--allow-bootstrap-failure",
|
|
13867
13922
|
"Treat bootstrap step failures as warnings instead of destroying the world (dogfood escape hatch for cross-repo seed coupling)"
|
|
13868
|
-
).action(async (opts) => {
|
|
13923
|
+
).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) => {
|
|
13924
|
+
const { resolveDevboxImageOverride: resolveDevboxImageOverride2, decideAllowlist: decideAllowlist2 } = await Promise.resolve().then(() => (init_registry_allowlist(), registry_allowlist_exports));
|
|
13925
|
+
const overrideRef = resolveDevboxImageOverride2(opts.devboxImage);
|
|
13926
|
+
if (overrideRef) {
|
|
13927
|
+
const decision = decideAllowlist2({
|
|
13928
|
+
imageRef: overrideRef,
|
|
13929
|
+
allowCustomRegistry: opts.allowCustomRegistry === true
|
|
13930
|
+
});
|
|
13931
|
+
if (!decision.accepted) {
|
|
13932
|
+
process.stderr.write(decision.stderrLine + "\n");
|
|
13933
|
+
process.exitCode = 1;
|
|
13934
|
+
return;
|
|
13935
|
+
}
|
|
13936
|
+
if (decision.stderrLine) {
|
|
13937
|
+
process.stderr.write(decision.stderrLine + "\n");
|
|
13938
|
+
}
|
|
13939
|
+
}
|
|
13869
13940
|
let resolvedName = opts.name;
|
|
13870
13941
|
let resolvedWorkspace = opts.workspace;
|
|
13871
13942
|
let resolvedRepos = opts.repos;
|
|
@@ -14170,12 +14241,12 @@ function defaultNameFromPrompt(prompt) {
|
|
|
14170
14241
|
}
|
|
14171
14242
|
async function readHostCpToken3() {
|
|
14172
14243
|
try {
|
|
14173
|
-
const { default:
|
|
14174
|
-
const { default:
|
|
14175
|
-
const { default:
|
|
14176
|
-
const tp =
|
|
14177
|
-
if (!
|
|
14178
|
-
const raw =
|
|
14244
|
+
const { default: fs31 } = await import("node:fs");
|
|
14245
|
+
const { default: os18 } = await import("node:os");
|
|
14246
|
+
const { default: path35 } = await import("node:path");
|
|
14247
|
+
const tp = path35.join(os18.homedir(), ".olam", "host-cp.token");
|
|
14248
|
+
if (!fs31.existsSync(tp)) return null;
|
|
14249
|
+
const raw = fs31.readFileSync(tp, "utf-8").trim();
|
|
14179
14250
|
return raw.length > 0 ? raw : null;
|
|
14180
14251
|
} catch {
|
|
14181
14252
|
return null;
|
|
@@ -16840,17 +16911,246 @@ function registerPolicyCheck(program2) {
|
|
|
16840
16911
|
}
|
|
16841
16912
|
|
|
16842
16913
|
// src/commands/upgrade.ts
|
|
16914
|
+
import * as fs24 from "node:fs";
|
|
16915
|
+
import * as path28 from "node:path";
|
|
16916
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
16917
|
+
import pc15 from "picocolors";
|
|
16918
|
+
|
|
16919
|
+
// src/commands/upgrade-lock.ts
|
|
16843
16920
|
import * as fs22 from "node:fs";
|
|
16921
|
+
import * as os13 from "node:os";
|
|
16844
16922
|
import * as path26 from "node:path";
|
|
16845
16923
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
16846
|
-
|
|
16924
|
+
var LOCK_FILE_PATH = path26.join(os13.homedir(), ".olam", ".upgrade.lock");
|
|
16925
|
+
var STALE_LOCK_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
16926
|
+
function readLockFile(lockPath) {
|
|
16927
|
+
try {
|
|
16928
|
+
if (!fs22.existsSync(lockPath)) return null;
|
|
16929
|
+
const raw = fs22.readFileSync(lockPath, "utf-8").trim();
|
|
16930
|
+
if (raw.length === 0) return null;
|
|
16931
|
+
const parsed = JSON.parse(raw);
|
|
16932
|
+
if (typeof parsed.pid !== "number" || typeof parsed.startTs !== "number") return null;
|
|
16933
|
+
return { pid: parsed.pid, startTs: parsed.startTs };
|
|
16934
|
+
} catch {
|
|
16935
|
+
return null;
|
|
16936
|
+
}
|
|
16937
|
+
}
|
|
16938
|
+
function isPidAlive(pid) {
|
|
16939
|
+
try {
|
|
16940
|
+
process.kill(pid, 0);
|
|
16941
|
+
return true;
|
|
16942
|
+
} catch {
|
|
16943
|
+
return false;
|
|
16944
|
+
}
|
|
16945
|
+
}
|
|
16946
|
+
var PS_UNAVAILABLE = "__ps_unavailable__";
|
|
16947
|
+
function getPidCommand(pid) {
|
|
16948
|
+
const result = spawnSync6("ps", ["-p", String(pid), "-o", "comm="], {
|
|
16949
|
+
encoding: "utf-8",
|
|
16950
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
16951
|
+
});
|
|
16952
|
+
if (result.status === null || result.error !== void 0) return PS_UNAVAILABLE;
|
|
16953
|
+
if (result.status !== 0) return null;
|
|
16954
|
+
const out = result.stdout.trim();
|
|
16955
|
+
return out.length === 0 ? null : out;
|
|
16956
|
+
}
|
|
16957
|
+
function isOlamUpgradeCommand(comm) {
|
|
16958
|
+
if (!comm) return false;
|
|
16959
|
+
if (comm === PS_UNAVAILABLE) return false;
|
|
16960
|
+
const base = comm.split("/").pop() ?? comm;
|
|
16961
|
+
const stripped = base.replace(/\s*\(.*\)\s*$/, "").trim();
|
|
16962
|
+
return stripped === "node" || stripped === "olam" || stripped === "olam-cli";
|
|
16963
|
+
}
|
|
16964
|
+
function isStaleLock(content, nowMs = Date.now()) {
|
|
16965
|
+
if (!content) return true;
|
|
16966
|
+
if (nowMs - content.startTs > STALE_LOCK_TIMEOUT_MS) return true;
|
|
16967
|
+
if (!isPidAlive(content.pid)) return true;
|
|
16968
|
+
const comm = getPidCommand(content.pid);
|
|
16969
|
+
if (comm === PS_UNAVAILABLE) return false;
|
|
16970
|
+
if (!isOlamUpgradeCommand(comm)) return true;
|
|
16971
|
+
return false;
|
|
16972
|
+
}
|
|
16973
|
+
function acquireLock(lockPath = LOCK_FILE_PATH, nowMs = Date.now()) {
|
|
16974
|
+
const dir = path26.dirname(lockPath);
|
|
16975
|
+
fs22.mkdirSync(dir, { recursive: true });
|
|
16976
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
16977
|
+
try {
|
|
16978
|
+
const fd = fs22.openSync(lockPath, "wx", 420);
|
|
16979
|
+
try {
|
|
16980
|
+
const content = { pid: process.pid, startTs: nowMs };
|
|
16981
|
+
fs22.writeSync(fd, JSON.stringify(content));
|
|
16982
|
+
} finally {
|
|
16983
|
+
fs22.closeSync(fd);
|
|
16984
|
+
}
|
|
16985
|
+
return { acquired: true, lockPath };
|
|
16986
|
+
} catch (err) {
|
|
16987
|
+
const code = err.code;
|
|
16988
|
+
if (code !== "EEXIST") throw err;
|
|
16989
|
+
const existing2 = readLockFile(lockPath);
|
|
16990
|
+
if (isStaleLock(existing2, nowMs)) {
|
|
16991
|
+
try {
|
|
16992
|
+
fs22.unlinkSync(lockPath);
|
|
16993
|
+
} catch (unlinkErr) {
|
|
16994
|
+
const ucode = unlinkErr.code;
|
|
16995
|
+
if (ucode !== "ENOENT") throw unlinkErr;
|
|
16996
|
+
}
|
|
16997
|
+
continue;
|
|
16998
|
+
}
|
|
16999
|
+
return {
|
|
17000
|
+
acquired: false,
|
|
17001
|
+
reason: "live",
|
|
17002
|
+
...existing2?.pid !== void 0 && { existingPid: existing2.pid },
|
|
17003
|
+
...existing2?.startTs !== void 0 && { existingStartTs: existing2.startTs }
|
|
17004
|
+
};
|
|
17005
|
+
}
|
|
17006
|
+
}
|
|
17007
|
+
const existing = readLockFile(lockPath);
|
|
17008
|
+
return {
|
|
17009
|
+
acquired: false,
|
|
17010
|
+
reason: "live",
|
|
17011
|
+
...existing?.pid !== void 0 && { existingPid: existing.pid },
|
|
17012
|
+
...existing?.startTs !== void 0 && { existingStartTs: existing.startTs }
|
|
17013
|
+
};
|
|
17014
|
+
}
|
|
17015
|
+
function releaseLock(lockPath = LOCK_FILE_PATH) {
|
|
17016
|
+
try {
|
|
17017
|
+
fs22.unlinkSync(lockPath);
|
|
17018
|
+
} catch (err) {
|
|
17019
|
+
const code = err.code;
|
|
17020
|
+
if (code !== "ENOENT") throw err;
|
|
17021
|
+
}
|
|
17022
|
+
}
|
|
17023
|
+
function formatRefusalMessage(result, lockPath = LOCK_FILE_PATH) {
|
|
17024
|
+
const pidStr = result.existingPid !== void 0 ? ` (pid ${result.existingPid})` : "";
|
|
17025
|
+
const lines = [
|
|
17026
|
+
`Upgrade in progress${pidStr}.`,
|
|
17027
|
+
"Wait for the running upgrade to finish, or:",
|
|
17028
|
+
" - Check progress: olam upgrade --history",
|
|
17029
|
+
` - If stale (crashed CLI): rm ${lockPath}`
|
|
17030
|
+
];
|
|
17031
|
+
return lines.join("\n");
|
|
17032
|
+
}
|
|
17033
|
+
|
|
17034
|
+
// src/commands/upgrade-log.ts
|
|
17035
|
+
import * as fs23 from "node:fs";
|
|
17036
|
+
import * as os14 from "node:os";
|
|
17037
|
+
import * as path27 from "node:path";
|
|
17038
|
+
function getUpgradeLogPath() {
|
|
17039
|
+
const home = process.env["HOME"] ?? os14.homedir();
|
|
17040
|
+
return path27.join(home, ".olam", "upgrade.log");
|
|
17041
|
+
}
|
|
17042
|
+
var UPGRADE_LOG_PATH = getUpgradeLogPath();
|
|
17043
|
+
function appendUpgradeLog(row, logPath = getUpgradeLogPath()) {
|
|
17044
|
+
try {
|
|
17045
|
+
fs23.mkdirSync(path27.dirname(logPath), { recursive: true });
|
|
17046
|
+
const line = JSON.stringify(row) + "\n";
|
|
17047
|
+
fs23.appendFileSync(logPath, line, { mode: 420 });
|
|
17048
|
+
} catch (err) {
|
|
17049
|
+
process.stderr.write(
|
|
17050
|
+
`[upgrade-log] failed to append: ${err instanceof Error ? err.message : String(err)}
|
|
17051
|
+
`
|
|
17052
|
+
);
|
|
17053
|
+
}
|
|
17054
|
+
}
|
|
17055
|
+
function readUpgradeLog(limit = 10, logPath = getUpgradeLogPath()) {
|
|
17056
|
+
if (!fs23.existsSync(logPath)) return [];
|
|
17057
|
+
let raw;
|
|
17058
|
+
try {
|
|
17059
|
+
raw = fs23.readFileSync(logPath, "utf-8");
|
|
17060
|
+
} catch (err) {
|
|
17061
|
+
process.stderr.write(
|
|
17062
|
+
`[upgrade-log] failed to read: ${err instanceof Error ? err.message : String(err)}
|
|
17063
|
+
`
|
|
17064
|
+
);
|
|
17065
|
+
return [];
|
|
17066
|
+
}
|
|
17067
|
+
const lines = raw.split("\n").filter((l) => l.length > 0);
|
|
17068
|
+
const rows = [];
|
|
17069
|
+
for (let i = 0; i < lines.length; i++) {
|
|
17070
|
+
const line = lines[i];
|
|
17071
|
+
try {
|
|
17072
|
+
const parsed = JSON.parse(line);
|
|
17073
|
+
if (typeof parsed.ts === "string" && typeof parsed.started_at === "number" && typeof parsed.status === "string") {
|
|
17074
|
+
rows.push(parsed);
|
|
17075
|
+
} else {
|
|
17076
|
+
process.stderr.write(`[upgrade-log] skipped malformed row at line ${i + 1}
|
|
17077
|
+
`);
|
|
17078
|
+
}
|
|
17079
|
+
} catch {
|
|
17080
|
+
process.stderr.write(`[upgrade-log] skipped corrupt JSON at line ${i + 1}
|
|
17081
|
+
`);
|
|
17082
|
+
}
|
|
17083
|
+
}
|
|
17084
|
+
return rows.slice(-Math.max(0, limit));
|
|
17085
|
+
}
|
|
17086
|
+
function formatDuration(ms) {
|
|
17087
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
17088
|
+
const totalSec = Math.round(ms / 1e3);
|
|
17089
|
+
if (totalSec < 60) return `${totalSec}s`;
|
|
17090
|
+
const min = Math.floor(totalSec / 60);
|
|
17091
|
+
const sec = totalSec % 60;
|
|
17092
|
+
if (min < 60) return `${min}m${String(sec).padStart(2, "0")}s`;
|
|
17093
|
+
const hr = Math.floor(min / 60);
|
|
17094
|
+
const remMin = min % 60;
|
|
17095
|
+
return `${hr}h${String(remMin).padStart(2, "0")}m`;
|
|
17096
|
+
}
|
|
17097
|
+
function formatHistoryTable(rows) {
|
|
17098
|
+
if (rows.length === 0) {
|
|
17099
|
+
return "No upgrade history yet. Run `olam upgrade` to create your first record.";
|
|
17100
|
+
}
|
|
17101
|
+
const lines = [];
|
|
17102
|
+
lines.push("TIMESTAMP SHA STATUS DURATION FAILED-STEP");
|
|
17103
|
+
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");
|
|
17104
|
+
for (const r of rows) {
|
|
17105
|
+
const ts = r.ts.slice(0, 19).replace("T", " ");
|
|
17106
|
+
const sha = r.sha_target.slice(0, 8);
|
|
17107
|
+
const statusIcon = r.status === "success" ? "\u2713 success" : r.status === "rolled_back" ? "\u21A9 rolled_back" : "\u2717 failed";
|
|
17108
|
+
const dur = formatDuration(r.ended_at - r.started_at);
|
|
17109
|
+
const failed = r.failed_step ?? "";
|
|
17110
|
+
lines.push(
|
|
17111
|
+
`${ts.padEnd(28)}${sha.padEnd(10)}${statusIcon.padEnd(15)}${dur.padEnd(11)}${failed}`
|
|
17112
|
+
);
|
|
17113
|
+
}
|
|
17114
|
+
return lines.join("\n");
|
|
17115
|
+
}
|
|
17116
|
+
function formatHistoryJson(rows) {
|
|
17117
|
+
return rows.map((r) => JSON.stringify(r)).join("\n");
|
|
17118
|
+
}
|
|
17119
|
+
|
|
17120
|
+
// src/commands/upgrade-history.ts
|
|
17121
|
+
function parseHistoryOpts(raw) {
|
|
17122
|
+
const rawLimit = raw.n;
|
|
17123
|
+
const limit = typeof rawLimit === "number" ? rawLimit : typeof rawLimit === "string" ? Number.parseInt(rawLimit, 10) : 10;
|
|
17124
|
+
return {
|
|
17125
|
+
limit: Number.isFinite(limit) && limit > 0 ? limit : 10,
|
|
17126
|
+
json: raw.json === true
|
|
17127
|
+
};
|
|
17128
|
+
}
|
|
17129
|
+
function handleHistory(opts) {
|
|
17130
|
+
const rows = readUpgradeLog(opts.limit);
|
|
17131
|
+
if (opts.json) {
|
|
17132
|
+
process.stdout.write(formatHistoryJson(rows) + "\n");
|
|
17133
|
+
return;
|
|
17134
|
+
}
|
|
17135
|
+
if (rows.length === 0) {
|
|
17136
|
+
printInfo("Log file", UPGRADE_LOG_PATH);
|
|
17137
|
+
process.stdout.write(formatHistoryTable(rows) + "\n");
|
|
17138
|
+
return;
|
|
17139
|
+
}
|
|
17140
|
+
printInfo("Log file", UPGRADE_LOG_PATH);
|
|
17141
|
+
process.stdout.write(formatHistoryTable(rows) + "\n");
|
|
17142
|
+
}
|
|
17143
|
+
|
|
17144
|
+
// src/commands/upgrade.ts
|
|
17145
|
+
init_auth();
|
|
17146
|
+
var AUTH_HEALTH_URL2 = "http://127.0.0.1:9999/health";
|
|
16847
17147
|
function isNodeModulesInSync(cwd) {
|
|
16848
|
-
const lockPath =
|
|
16849
|
-
const markerPath =
|
|
16850
|
-
if (!
|
|
17148
|
+
const lockPath = path28.join(cwd, "package-lock.json");
|
|
17149
|
+
const markerPath = path28.join(cwd, "node_modules", ".package-lock.json");
|
|
17150
|
+
if (!fs24.existsSync(lockPath) || !fs24.existsSync(markerPath)) return false;
|
|
16851
17151
|
try {
|
|
16852
|
-
const lockStat =
|
|
16853
|
-
const markerStat =
|
|
17152
|
+
const lockStat = fs24.statSync(lockPath);
|
|
17153
|
+
const markerStat = fs24.statSync(markerPath);
|
|
16854
17154
|
return markerStat.mtimeMs >= lockStat.mtimeMs;
|
|
16855
17155
|
} catch {
|
|
16856
17156
|
return false;
|
|
@@ -16866,8 +17166,8 @@ function shouldSkipInstall(opts, cwd) {
|
|
|
16866
17166
|
return { skip: false };
|
|
16867
17167
|
}
|
|
16868
17168
|
function validateRepoRoot(cwd) {
|
|
16869
|
-
const marker =
|
|
16870
|
-
if (!
|
|
17169
|
+
const marker = path28.join(cwd, "packages/host-cp/compose.yaml");
|
|
17170
|
+
if (!fs24.existsSync(marker)) {
|
|
16871
17171
|
return {
|
|
16872
17172
|
ok: false,
|
|
16873
17173
|
error: `Not an olam repo root (expected ${marker}).
|
|
@@ -16877,11 +17177,19 @@ Run \`olam upgrade\` from the root of your olam checkout.`
|
|
|
16877
17177
|
return { ok: true };
|
|
16878
17178
|
}
|
|
16879
17179
|
function parseUpgradeOpts(raw) {
|
|
17180
|
+
const rawN = raw.n;
|
|
17181
|
+
const historyN = typeof rawN === "number" ? rawN : typeof rawN === "string" ? Number.parseInt(rawN, 10) : 10;
|
|
16880
17182
|
return {
|
|
16881
17183
|
yes: raw.yes === true,
|
|
16882
17184
|
skipImage: raw.skipImage === true,
|
|
16883
17185
|
skipInstall: raw.skipInstall === true,
|
|
16884
|
-
branch: raw.branch ?? null
|
|
17186
|
+
branch: raw.branch ?? null,
|
|
17187
|
+
rollback: raw.rollback === true,
|
|
17188
|
+
force: raw.force === true,
|
|
17189
|
+
noCache: raw.noCache === true,
|
|
17190
|
+
history: raw.history === true,
|
|
17191
|
+
historyN: Number.isFinite(historyN) && historyN > 0 ? historyN : 10,
|
|
17192
|
+
historyJson: raw.json === true
|
|
16885
17193
|
};
|
|
16886
17194
|
}
|
|
16887
17195
|
function extractBundleHash(indexHtml) {
|
|
@@ -16891,7 +17199,7 @@ function extractBundleHash(indexHtml) {
|
|
|
16891
17199
|
function runStep2(label, cmd, args, opts = {}) {
|
|
16892
17200
|
const start = Date.now();
|
|
16893
17201
|
process.stdout.write(` ${pc15.dim(label.padEnd(34))}`);
|
|
16894
|
-
const result =
|
|
17202
|
+
const result = spawnSync7(cmd, [...args], {
|
|
16895
17203
|
encoding: "utf-8",
|
|
16896
17204
|
stdio: ["ignore", "pipe", "pipe"],
|
|
16897
17205
|
cwd: opts.cwd ?? process.cwd(),
|
|
@@ -16910,7 +17218,7 @@ function runStep2(label, cmd, args, opts = {}) {
|
|
|
16910
17218
|
};
|
|
16911
17219
|
}
|
|
16912
17220
|
function isGitDirty(cwd) {
|
|
16913
|
-
const result =
|
|
17221
|
+
const result = spawnSync7("git", ["status", "--porcelain"], {
|
|
16914
17222
|
encoding: "utf-8",
|
|
16915
17223
|
stdio: ["ignore", "pipe", "pipe"],
|
|
16916
17224
|
cwd
|
|
@@ -16918,13 +17226,194 @@ function isGitDirty(cwd) {
|
|
|
16918
17226
|
return (result.stdout ?? "").trim().length > 0;
|
|
16919
17227
|
}
|
|
16920
17228
|
function hasGitUpstream(cwd) {
|
|
16921
|
-
const result =
|
|
17229
|
+
const result = spawnSync7("git", ["rev-parse", "--abbrev-ref", "@{u}"], {
|
|
16922
17230
|
encoding: "utf-8",
|
|
16923
17231
|
stdio: ["ignore", "pipe", "pipe"],
|
|
16924
17232
|
cwd
|
|
16925
17233
|
});
|
|
16926
17234
|
return result.status === 0;
|
|
16927
17235
|
}
|
|
17236
|
+
function captureHeadSha(cwd) {
|
|
17237
|
+
const result = spawnSync7("git", ["rev-parse", "HEAD"], {
|
|
17238
|
+
encoding: "utf-8",
|
|
17239
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
17240
|
+
cwd
|
|
17241
|
+
});
|
|
17242
|
+
if (result.status !== 0) return null;
|
|
17243
|
+
const sha = (result.stdout ?? "").trim();
|
|
17244
|
+
if (!/^[0-9a-f]{40}$/.test(sha)) return null;
|
|
17245
|
+
return sha;
|
|
17246
|
+
}
|
|
17247
|
+
function abbreviateSha(sha) {
|
|
17248
|
+
return sha.slice(0, 8);
|
|
17249
|
+
}
|
|
17250
|
+
function imageExists(tag) {
|
|
17251
|
+
try {
|
|
17252
|
+
const result = spawnSync7("docker", ["image", "inspect", "--format", "{{.Id}}", tag], {
|
|
17253
|
+
encoding: "utf-8",
|
|
17254
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
17255
|
+
});
|
|
17256
|
+
return result.status === 0;
|
|
17257
|
+
} catch {
|
|
17258
|
+
return false;
|
|
17259
|
+
}
|
|
17260
|
+
}
|
|
17261
|
+
function checkRollbackSetExists(plan) {
|
|
17262
|
+
const missing = plan.filter((p) => !imageExists(p.rollback)).map((p) => p.rollback);
|
|
17263
|
+
if (missing.length === 0) return null;
|
|
17264
|
+
return missing.join(", ");
|
|
17265
|
+
}
|
|
17266
|
+
function smokeImage(image, targetSha) {
|
|
17267
|
+
const createResult = spawnSync7("docker", ["create", "--name", `olam-smoke-${Date.now()}`, image], {
|
|
17268
|
+
encoding: "utf-8",
|
|
17269
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
17270
|
+
});
|
|
17271
|
+
if (createResult.status !== 0) {
|
|
17272
|
+
return {
|
|
17273
|
+
image,
|
|
17274
|
+
ok: false,
|
|
17275
|
+
bakedSha: null,
|
|
17276
|
+
error: `docker create failed: ${(createResult.stderr ?? "").trim()}`
|
|
17277
|
+
};
|
|
17278
|
+
}
|
|
17279
|
+
const containerId = (createResult.stdout ?? "").trim();
|
|
17280
|
+
const inspectResult = spawnSync7(
|
|
17281
|
+
"docker",
|
|
17282
|
+
["inspect", "--format", '{{index .Config.Labels "olam_build_sha"}}', image],
|
|
17283
|
+
{
|
|
17284
|
+
encoding: "utf-8",
|
|
17285
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
17286
|
+
}
|
|
17287
|
+
);
|
|
17288
|
+
if (containerId.length > 0) {
|
|
17289
|
+
spawnSync7("docker", ["rm", "-f", containerId], {
|
|
17290
|
+
encoding: "utf-8",
|
|
17291
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
17292
|
+
});
|
|
17293
|
+
}
|
|
17294
|
+
if (inspectResult.status !== 0) {
|
|
17295
|
+
return {
|
|
17296
|
+
image,
|
|
17297
|
+
ok: false,
|
|
17298
|
+
bakedSha: null,
|
|
17299
|
+
error: `docker inspect failed: ${(inspectResult.stderr ?? "").trim()}`
|
|
17300
|
+
};
|
|
17301
|
+
}
|
|
17302
|
+
const bakedSha = (inspectResult.stdout ?? "").trim();
|
|
17303
|
+
if (bakedSha.length === 0) {
|
|
17304
|
+
return {
|
|
17305
|
+
image,
|
|
17306
|
+
ok: false,
|
|
17307
|
+
bakedSha: null,
|
|
17308
|
+
error: "olam_build_sha label is missing or empty"
|
|
17309
|
+
};
|
|
17310
|
+
}
|
|
17311
|
+
if (bakedSha !== targetSha) {
|
|
17312
|
+
return {
|
|
17313
|
+
image,
|
|
17314
|
+
ok: false,
|
|
17315
|
+
bakedSha,
|
|
17316
|
+
error: `baked SHA ${abbreviateSha(bakedSha)} \u2260 target SHA ${abbreviateSha(targetSha)}`
|
|
17317
|
+
};
|
|
17318
|
+
}
|
|
17319
|
+
return { image, ok: true, bakedSha };
|
|
17320
|
+
}
|
|
17321
|
+
var PRODUCTION_SWAP_PLAN = [
|
|
17322
|
+
{ transient: "olam-auth:olam-next", canonical: "olam-auth:local", rollback: "olam-auth:olam-rollback" },
|
|
17323
|
+
{ transient: "olam-devbox:olam-next", canonical: "olam-devbox:latest", rollback: "olam-devbox:olam-rollback" },
|
|
17324
|
+
{ transient: "olam-host-cp:olam-next", canonical: "olam-host-cp:latest", rollback: "olam-host-cp:olam-rollback" }
|
|
17325
|
+
];
|
|
17326
|
+
function dockerTag(source, dest) {
|
|
17327
|
+
try {
|
|
17328
|
+
const result = spawnSync7("docker", ["tag", source, dest], {
|
|
17329
|
+
encoding: "utf-8",
|
|
17330
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
17331
|
+
});
|
|
17332
|
+
if (result.status === 0 && result.error === void 0) return { ok: true };
|
|
17333
|
+
return {
|
|
17334
|
+
ok: false,
|
|
17335
|
+
error: (result.stderr ?? "").trim() || result.error?.message || "docker tag failed"
|
|
17336
|
+
};
|
|
17337
|
+
} catch (err) {
|
|
17338
|
+
return {
|
|
17339
|
+
ok: false,
|
|
17340
|
+
error: err instanceof Error ? `spawnSync threw: ${err.message}` : "spawnSync threw"
|
|
17341
|
+
};
|
|
17342
|
+
}
|
|
17343
|
+
}
|
|
17344
|
+
function performAtomicSwap(plan) {
|
|
17345
|
+
const steps = plan.map((p) => ({
|
|
17346
|
+
image: p.canonical,
|
|
17347
|
+
rollbackSaved: false,
|
|
17348
|
+
canonicalAdvanced: false
|
|
17349
|
+
}));
|
|
17350
|
+
for (let i = 0; i < plan.length; i++) {
|
|
17351
|
+
const p = plan[i];
|
|
17352
|
+
const r = dockerTag(p.canonical, p.rollback);
|
|
17353
|
+
steps[i] = {
|
|
17354
|
+
...steps[i],
|
|
17355
|
+
rollbackSaved: r.ok,
|
|
17356
|
+
...r.error !== void 0 && { rollbackError: r.error }
|
|
17357
|
+
};
|
|
17358
|
+
}
|
|
17359
|
+
let advanceFailed = false;
|
|
17360
|
+
let firstFailureIdx = -1;
|
|
17361
|
+
for (let i = 0; i < plan.length; i++) {
|
|
17362
|
+
const p = plan[i];
|
|
17363
|
+
if (advanceFailed) {
|
|
17364
|
+
steps[i] = { ...steps[i], canonicalAdvanced: false };
|
|
17365
|
+
continue;
|
|
17366
|
+
}
|
|
17367
|
+
const r = dockerTag(p.transient, p.canonical);
|
|
17368
|
+
steps[i] = {
|
|
17369
|
+
...steps[i],
|
|
17370
|
+
canonicalAdvanced: r.ok,
|
|
17371
|
+
...r.error !== void 0 && { canonicalError: r.error }
|
|
17372
|
+
};
|
|
17373
|
+
if (!r.ok) {
|
|
17374
|
+
advanceFailed = true;
|
|
17375
|
+
firstFailureIdx = i;
|
|
17376
|
+
}
|
|
17377
|
+
}
|
|
17378
|
+
const allAdvanced = steps.every((s) => s.canonicalAdvanced);
|
|
17379
|
+
const noneAdvanced = steps.every((s) => !s.canonicalAdvanced);
|
|
17380
|
+
const partialAdvance = !allAdvanced && !noneAdvanced;
|
|
17381
|
+
const rollbackCoherent = steps.every((s) => s.rollbackSaved);
|
|
17382
|
+
let summary;
|
|
17383
|
+
if (allAdvanced) {
|
|
17384
|
+
const rollbacks = steps.filter((s) => s.rollbackSaved).length;
|
|
17385
|
+
summary = `Swapped ${plan.length} canonical tags; ${rollbacks} :olam-rollback preserved`;
|
|
17386
|
+
} else if (partialAdvance) {
|
|
17387
|
+
const advanced = steps.filter((s) => s.canonicalAdvanced).length;
|
|
17388
|
+
const failedStep = steps[firstFailureIdx];
|
|
17389
|
+
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.`;
|
|
17390
|
+
summary = `PARTIAL: ${advanced} of ${plan.length} canonical tags advanced before failure on ${failedStep?.image}: ${failedStep?.canonicalError}. ${recoveryHint}`;
|
|
17391
|
+
} else {
|
|
17392
|
+
const failedStep = steps[firstFailureIdx];
|
|
17393
|
+
summary = `Failed on first canonical-advance (${failedStep?.image}): ${failedStep?.canonicalError}. Canonical tags untouched.`;
|
|
17394
|
+
}
|
|
17395
|
+
return {
|
|
17396
|
+
ok: allAdvanced,
|
|
17397
|
+
steps,
|
|
17398
|
+
partialAdvance,
|
|
17399
|
+
rollbackCoherent,
|
|
17400
|
+
summary
|
|
17401
|
+
};
|
|
17402
|
+
}
|
|
17403
|
+
function performRollbackSwap(plan) {
|
|
17404
|
+
const results = [];
|
|
17405
|
+
for (const p of plan) {
|
|
17406
|
+
const r = dockerTag(p.rollback, p.canonical);
|
|
17407
|
+
results.push({
|
|
17408
|
+
image: p.canonical,
|
|
17409
|
+
ok: r.ok,
|
|
17410
|
+
...r.error !== void 0 && { error: r.error }
|
|
17411
|
+
});
|
|
17412
|
+
}
|
|
17413
|
+
const allOk = results.every((r) => r.ok);
|
|
17414
|
+
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(", ")}`;
|
|
17415
|
+
return { ok: allOk, results, summary };
|
|
17416
|
+
}
|
|
16928
17417
|
async function confirm2(message) {
|
|
16929
17418
|
if (!process.stdin.isTTY) return true;
|
|
16930
17419
|
const { createInterface: createInterface2 } = await import("node:readline");
|
|
@@ -16950,10 +17439,87 @@ async function waitForHealth(timeoutMs = 1e4) {
|
|
|
16950
17439
|
}
|
|
16951
17440
|
return false;
|
|
16952
17441
|
}
|
|
17442
|
+
async function waitForVersionMatch(targetSha, timeoutMs = 6e4, pollIntervalMs = 1e3) {
|
|
17443
|
+
const deadline = Date.now() + timeoutMs;
|
|
17444
|
+
let lastSnapshot = null;
|
|
17445
|
+
while (Date.now() < deadline) {
|
|
17446
|
+
try {
|
|
17447
|
+
const res = await fetch("http://127.0.0.1:19000/api/version/status", {
|
|
17448
|
+
signal: AbortSignal.timeout(2e3)
|
|
17449
|
+
});
|
|
17450
|
+
if (res.ok) {
|
|
17451
|
+
const snapshot = await res.json();
|
|
17452
|
+
lastSnapshot = snapshot;
|
|
17453
|
+
if (snapshot.hostCp?.running === targetSha && snapshot.authService?.running === targetSha && snapshot.devbox?.running === targetSha) {
|
|
17454
|
+
return { matched: true, snapshot };
|
|
17455
|
+
}
|
|
17456
|
+
}
|
|
17457
|
+
} catch {
|
|
17458
|
+
}
|
|
17459
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
17460
|
+
}
|
|
17461
|
+
return { matched: false, snapshot: lastSnapshot };
|
|
17462
|
+
}
|
|
17463
|
+
function formatVersionMismatch(targetSha, snapshot) {
|
|
17464
|
+
if (!snapshot) return "No /api/version/status response received within timeout.";
|
|
17465
|
+
const lines = [];
|
|
17466
|
+
for (const [name, comp] of [
|
|
17467
|
+
["host-cp", snapshot.hostCp],
|
|
17468
|
+
["auth-service", snapshot.authService],
|
|
17469
|
+
["devbox", snapshot.devbox]
|
|
17470
|
+
]) {
|
|
17471
|
+
const match2 = comp?.running === targetSha;
|
|
17472
|
+
lines.push(` ${match2 ? "\u2713" : "\u2717"} ${name}: running=${abbreviateSha(comp?.running ?? "unknown")} target=${abbreviateSha(targetSha)}`);
|
|
17473
|
+
}
|
|
17474
|
+
return lines.join("\n");
|
|
17475
|
+
}
|
|
17476
|
+
async function waitForAuthHealthLocal(timeoutMs = 15e3) {
|
|
17477
|
+
const deadline = Date.now() + timeoutMs;
|
|
17478
|
+
while (Date.now() < deadline) {
|
|
17479
|
+
try {
|
|
17480
|
+
const res = await fetch(AUTH_HEALTH_URL2, { signal: AbortSignal.timeout(2e3) });
|
|
17481
|
+
if (res.ok) return true;
|
|
17482
|
+
} catch {
|
|
17483
|
+
}
|
|
17484
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
17485
|
+
}
|
|
17486
|
+
return false;
|
|
17487
|
+
}
|
|
17488
|
+
async function recreateAuthService() {
|
|
17489
|
+
const start = Date.now();
|
|
17490
|
+
try {
|
|
17491
|
+
spawnSync7("docker", ["stop", "olam-auth"], {
|
|
17492
|
+
encoding: "utf-8",
|
|
17493
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
17494
|
+
});
|
|
17495
|
+
spawnSync7("docker", ["rm", "olam-auth"], {
|
|
17496
|
+
encoding: "utf-8",
|
|
17497
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
17498
|
+
});
|
|
17499
|
+
const controller = new AuthContainerController();
|
|
17500
|
+
controller.start();
|
|
17501
|
+
const healthy = await waitForAuthHealthLocal(15e3);
|
|
17502
|
+
const durationMs = Date.now() - start;
|
|
17503
|
+
if (!healthy) {
|
|
17504
|
+
return {
|
|
17505
|
+
ok: false,
|
|
17506
|
+
durationMs,
|
|
17507
|
+
error: "auth-service /health did not respond within 15s after recreate"
|
|
17508
|
+
};
|
|
17509
|
+
}
|
|
17510
|
+
return { ok: true, durationMs };
|
|
17511
|
+
} catch (err) {
|
|
17512
|
+
return {
|
|
17513
|
+
ok: false,
|
|
17514
|
+
durationMs: Date.now() - start,
|
|
17515
|
+
error: err instanceof Error ? err.message : String(err)
|
|
17516
|
+
};
|
|
17517
|
+
}
|
|
17518
|
+
}
|
|
16953
17519
|
function readBundleHash(cwd) {
|
|
16954
|
-
const indexPath =
|
|
16955
|
-
if (!
|
|
16956
|
-
return extractBundleHash(
|
|
17520
|
+
const indexPath = path28.join(cwd, "packages/control-plane/public/index.html");
|
|
17521
|
+
if (!fs24.existsSync(indexPath)) return null;
|
|
17522
|
+
return extractBundleHash(fs24.readFileSync(indexPath, "utf-8"));
|
|
16957
17523
|
}
|
|
16958
17524
|
async function handleUpgrade(opts) {
|
|
16959
17525
|
const cwd = process.cwd();
|
|
@@ -16988,6 +17554,142 @@ async function handleUpgrade(opts) {
|
|
|
16988
17554
|
return;
|
|
16989
17555
|
}
|
|
16990
17556
|
}
|
|
17557
|
+
if (opts.history) {
|
|
17558
|
+
handleHistory(parseHistoryOpts({ n: opts.historyN, json: opts.historyJson }));
|
|
17559
|
+
return;
|
|
17560
|
+
}
|
|
17561
|
+
if (opts.rollback) {
|
|
17562
|
+
return await handleRollback();
|
|
17563
|
+
}
|
|
17564
|
+
const lock = acquireLock();
|
|
17565
|
+
if (!lock.acquired) {
|
|
17566
|
+
printError(formatRefusalMessage(lock, LOCK_FILE_PATH));
|
|
17567
|
+
process.exitCode = 1;
|
|
17568
|
+
return;
|
|
17569
|
+
}
|
|
17570
|
+
let signalReleased = false;
|
|
17571
|
+
const releaseOnSignal = (signal) => {
|
|
17572
|
+
if (signalReleased) return;
|
|
17573
|
+
signalReleased = true;
|
|
17574
|
+
try {
|
|
17575
|
+
releaseLock();
|
|
17576
|
+
} catch {
|
|
17577
|
+
}
|
|
17578
|
+
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
17579
|
+
};
|
|
17580
|
+
process.once("SIGINT", releaseOnSignal);
|
|
17581
|
+
process.once("SIGTERM", releaseOnSignal);
|
|
17582
|
+
const logRow = {
|
|
17583
|
+
started_at: Date.now(),
|
|
17584
|
+
durations_ms: {},
|
|
17585
|
+
sha_target: "",
|
|
17586
|
+
failed_step: null,
|
|
17587
|
+
status: "failed"
|
|
17588
|
+
// default; flipped to 'success' on clean exit
|
|
17589
|
+
};
|
|
17590
|
+
try {
|
|
17591
|
+
await runUpgradeStepsWithLockHeld(opts, cwd, logRow);
|
|
17592
|
+
if (process.exitCode !== 1) logRow.status = "success";
|
|
17593
|
+
} finally {
|
|
17594
|
+
const ended_at = Date.now();
|
|
17595
|
+
const row = {
|
|
17596
|
+
ts: new Date(ended_at).toISOString(),
|
|
17597
|
+
started_at: logRow.started_at,
|
|
17598
|
+
ended_at,
|
|
17599
|
+
sha_target: logRow.sha_target,
|
|
17600
|
+
status: logRow.status,
|
|
17601
|
+
failed_step: logRow.failed_step,
|
|
17602
|
+
durations_ms: logRow.durations_ms
|
|
17603
|
+
};
|
|
17604
|
+
appendUpgradeLog(row);
|
|
17605
|
+
releaseLock();
|
|
17606
|
+
process.removeListener("SIGINT", releaseOnSignal);
|
|
17607
|
+
process.removeListener("SIGTERM", releaseOnSignal);
|
|
17608
|
+
}
|
|
17609
|
+
}
|
|
17610
|
+
async function handleRollback() {
|
|
17611
|
+
printHeader("olam upgrade --rollback");
|
|
17612
|
+
const missing = checkRollbackSetExists(PRODUCTION_SWAP_PLAN);
|
|
17613
|
+
if (missing !== null) {
|
|
17614
|
+
printError(
|
|
17615
|
+
`No rollback-set available \u2014 missing :olam-rollback tag(s): ${missing}
|
|
17616
|
+
|
|
17617
|
+
A rollback-set is created by the FIRST successful \`olam upgrade\`. If this
|
|
17618
|
+
is your first install, run \`olam upgrade\` to populate the rollback set.
|
|
17619
|
+
If a previous upgrade was incomplete, the rollback set may be partial;
|
|
17620
|
+
manually inspect images with \`docker images olam-*:olam-rollback\`.`
|
|
17621
|
+
);
|
|
17622
|
+
process.exitCode = 1;
|
|
17623
|
+
return;
|
|
17624
|
+
}
|
|
17625
|
+
const lock = acquireLock();
|
|
17626
|
+
if (!lock.acquired) {
|
|
17627
|
+
printError(formatRefusalMessage(lock, LOCK_FILE_PATH));
|
|
17628
|
+
process.exitCode = 1;
|
|
17629
|
+
return;
|
|
17630
|
+
}
|
|
17631
|
+
let signalReleased = false;
|
|
17632
|
+
const releaseOnSignal = (signal) => {
|
|
17633
|
+
if (signalReleased) return;
|
|
17634
|
+
signalReleased = true;
|
|
17635
|
+
try {
|
|
17636
|
+
releaseLock();
|
|
17637
|
+
} catch {
|
|
17638
|
+
}
|
|
17639
|
+
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
17640
|
+
};
|
|
17641
|
+
process.once("SIGINT", releaseOnSignal);
|
|
17642
|
+
process.once("SIGTERM", releaseOnSignal);
|
|
17643
|
+
try {
|
|
17644
|
+
process.stdout.write(` ${pc15.dim("rollback retag (3 ops)".padEnd(34))}`);
|
|
17645
|
+
const swapStart = Date.now();
|
|
17646
|
+
const swapResult = performRollbackSwap(PRODUCTION_SWAP_PLAN);
|
|
17647
|
+
const swapDur = `${((Date.now() - swapStart) / 1e3).toFixed(1)}s`;
|
|
17648
|
+
process.stdout.write(`${swapResult.ok ? pc15.green("\u2713") : pc15.red("\u2717")} ${swapDur}
|
|
17649
|
+
`);
|
|
17650
|
+
if (!swapResult.ok) {
|
|
17651
|
+
printError(`Rollback retag failed: ${swapResult.summary}`);
|
|
17652
|
+
process.exitCode = 1;
|
|
17653
|
+
return;
|
|
17654
|
+
}
|
|
17655
|
+
printInfo("Rollback", swapResult.summary);
|
|
17656
|
+
const cwd = process.cwd();
|
|
17657
|
+
const composeFile = path28.join(cwd, "packages/host-cp/compose.yaml");
|
|
17658
|
+
const authSecret = readAuthSecret2();
|
|
17659
|
+
process.stdout.write(` ${pc15.dim("docker compose recreate host-cp".padEnd(34))}`);
|
|
17660
|
+
const composeStart = Date.now();
|
|
17661
|
+
const composeResult = runCompose(["up", "-d", "--force-recreate", "host-cp"], composeFile, buildComposeEnv(authSecret));
|
|
17662
|
+
const composeDur = `${((Date.now() - composeStart) / 1e3).toFixed(1)}s`;
|
|
17663
|
+
process.stdout.write(`${composeResult.ok ? pc15.green("\u2713") : pc15.red("\u2717")} ${composeDur}
|
|
17664
|
+
`);
|
|
17665
|
+
if (!composeResult.ok) {
|
|
17666
|
+
printError(
|
|
17667
|
+
`Rollback compose recreate failed:
|
|
17668
|
+
${composeResult.stderr}
|
|
17669
|
+
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\`.`
|
|
17670
|
+
);
|
|
17671
|
+
process.exitCode = 1;
|
|
17672
|
+
return;
|
|
17673
|
+
}
|
|
17674
|
+
process.stdout.write(` ${pc15.dim("recreate auth-service".padEnd(34))}`);
|
|
17675
|
+
const authResult = await recreateAuthService();
|
|
17676
|
+
const authDur = `${(authResult.durationMs / 1e3).toFixed(1)}s`;
|
|
17677
|
+
process.stdout.write(`${authResult.ok ? pc15.green("\u2713") : pc15.red("\u2717")} ${authDur}
|
|
17678
|
+
`);
|
|
17679
|
+
if (!authResult.ok) {
|
|
17680
|
+
printError(`Auth-service recreate failed: ${authResult.error ?? "unknown"}`);
|
|
17681
|
+
process.exitCode = 1;
|
|
17682
|
+
return;
|
|
17683
|
+
}
|
|
17684
|
+
process.stdout.write("\n");
|
|
17685
|
+
printSuccess("Rollback complete \u2014 canonical tags restored from :olam-rollback");
|
|
17686
|
+
} finally {
|
|
17687
|
+
releaseLock();
|
|
17688
|
+
process.removeListener("SIGINT", releaseOnSignal);
|
|
17689
|
+
process.removeListener("SIGTERM", releaseOnSignal);
|
|
17690
|
+
}
|
|
17691
|
+
}
|
|
17692
|
+
async function runUpgradeStepsWithLockHeld(opts, cwd, logRow) {
|
|
16991
17693
|
if (opts.branch !== null) {
|
|
16992
17694
|
if (isGitDirty(cwd)) {
|
|
16993
17695
|
printError(
|
|
@@ -17045,6 +17747,17 @@ If there are conflicts, resolve them manually then re-run \`olam upgrade\`.`
|
|
|
17045
17747
|
process.exitCode = 1;
|
|
17046
17748
|
return;
|
|
17047
17749
|
}
|
|
17750
|
+
const _targetSha = captureHeadSha(cwd);
|
|
17751
|
+
logRow.sha_target = _targetSha ?? "";
|
|
17752
|
+
if (_targetSha === null) {
|
|
17753
|
+
logRow.failed_step = "capture HEAD SHA";
|
|
17754
|
+
printError(
|
|
17755
|
+
"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."
|
|
17756
|
+
);
|
|
17757
|
+
process.exitCode = 1;
|
|
17758
|
+
return;
|
|
17759
|
+
}
|
|
17760
|
+
printInfo("Target SHA", abbreviateSha(_targetSha));
|
|
17048
17761
|
const installDecision = shouldSkipInstall(opts, cwd);
|
|
17049
17762
|
if (installDecision.skip) {
|
|
17050
17763
|
printInfo("npm install", `skipped \u2014 ${installDecision.reason}`);
|
|
@@ -17082,7 +17795,7 @@ ${buildResult.stderr}`);
|
|
|
17082
17795
|
return;
|
|
17083
17796
|
}
|
|
17084
17797
|
const authSecret = readAuthSecret2();
|
|
17085
|
-
const spaDir =
|
|
17798
|
+
const spaDir = path28.join(cwd, "packages/control-plane/app");
|
|
17086
17799
|
const spaResult = runStep2(
|
|
17087
17800
|
"vite build (SPA)",
|
|
17088
17801
|
"npx",
|
|
@@ -17104,21 +17817,107 @@ ${spaResult.stderr}`);
|
|
|
17104
17817
|
printTimings2(timings);
|
|
17105
17818
|
return;
|
|
17106
17819
|
}
|
|
17107
|
-
const
|
|
17108
|
-
|
|
17109
|
-
|
|
17110
|
-
|
|
17111
|
-
|
|
17112
|
-
{
|
|
17113
|
-
|
|
17114
|
-
|
|
17115
|
-
|
|
17116
|
-
|
|
17117
|
-
|
|
17820
|
+
const olamTagEnv = { OLAM_TAG: "olam-next" };
|
|
17821
|
+
if (opts.noCache) {
|
|
17822
|
+
olamTagEnv.DOCKER_BUILD_NO_CACHE = "1";
|
|
17823
|
+
}
|
|
17824
|
+
const buildScripts = [
|
|
17825
|
+
{ label: "bash build-auth.sh", relPath: "packages/adapters/src/docker/build-auth.sh", tee: false },
|
|
17826
|
+
{ label: "bash build-devbox.sh", relPath: "packages/adapters/src/docker/build-devbox.sh", tee: true },
|
|
17827
|
+
{ label: "bash build-host-cp.sh", relPath: "packages/adapters/src/docker/build-host-cp.sh", tee: false }
|
|
17828
|
+
];
|
|
17829
|
+
for (const step of buildScripts) {
|
|
17830
|
+
const scriptPath = path28.join(cwd, step.relPath);
|
|
17831
|
+
if (step.tee) {
|
|
17832
|
+
process.stdout.write(` ${pc15.dim(step.label.padEnd(34))}
|
|
17833
|
+
`);
|
|
17834
|
+
const start = Date.now();
|
|
17835
|
+
const result = spawnSync7("bash", [scriptPath], {
|
|
17836
|
+
stdio: "inherit",
|
|
17837
|
+
cwd,
|
|
17838
|
+
env: { ...process.env, ...olamTagEnv }
|
|
17839
|
+
});
|
|
17840
|
+
const durationMs = Date.now() - start;
|
|
17841
|
+
const ok = result.status === 0 && result.error === void 0;
|
|
17842
|
+
const dur = `${(durationMs / 1e3).toFixed(1)}s`;
|
|
17843
|
+
process.stdout.write(` ${pc15.dim(step.label.padEnd(34))}${ok ? pc15.green("\u2713") : pc15.red("\u2717")} ${dur}
|
|
17844
|
+
`);
|
|
17845
|
+
timings.push({ label: step.label, durationMs });
|
|
17846
|
+
if (!ok) {
|
|
17847
|
+
printError(`${step.label} failed (see output above for details).`);
|
|
17848
|
+
process.exitCode = 1;
|
|
17849
|
+
return;
|
|
17850
|
+
}
|
|
17851
|
+
} else {
|
|
17852
|
+
const result = runStep2(step.label, "bash", [scriptPath], {
|
|
17853
|
+
cwd,
|
|
17854
|
+
env: olamTagEnv
|
|
17855
|
+
});
|
|
17856
|
+
timings.push({ label: step.label, durationMs: result.durationMs });
|
|
17857
|
+
logRow.durations_ms[step.label] = result.durationMs;
|
|
17858
|
+
if (!result.ok) {
|
|
17859
|
+
logRow.failed_step = step.label;
|
|
17860
|
+
printError(`${step.label} failed:
|
|
17861
|
+
${result.stderr.split("\n").slice(-3).join("\n")}`);
|
|
17862
|
+
process.exitCode = 1;
|
|
17863
|
+
return;
|
|
17864
|
+
}
|
|
17865
|
+
}
|
|
17866
|
+
}
|
|
17867
|
+
for (const t of timings) logRow.durations_ms[t.label] = t.durationMs;
|
|
17868
|
+
const smokeStart = Date.now();
|
|
17869
|
+
process.stdout.write(` ${pc15.dim("smoke (docker create + inspect)".padEnd(34))}`);
|
|
17870
|
+
const smokeImages = [
|
|
17871
|
+
"olam-auth:olam-next",
|
|
17872
|
+
"olam-devbox:olam-next",
|
|
17873
|
+
"olam-host-cp:olam-next"
|
|
17874
|
+
];
|
|
17875
|
+
const smokeResults = smokeImages.map((img) => smokeImage(img, _targetSha));
|
|
17876
|
+
const smokeFailures = smokeResults.filter((r) => !r.ok);
|
|
17877
|
+
const smokeDurationMs = Date.now() - smokeStart;
|
|
17878
|
+
const smokeDur = `${(smokeDurationMs / 1e3).toFixed(1)}s`;
|
|
17879
|
+
process.stdout.write(`${smokeFailures.length === 0 ? pc15.green("\u2713") : pc15.red("\u2717")} ${smokeDur}
|
|
17880
|
+
`);
|
|
17881
|
+
timings.push({ label: "smoke", durationMs: smokeDurationMs });
|
|
17882
|
+
if (smokeFailures.length > 0) {
|
|
17883
|
+
printError(
|
|
17884
|
+
`Smoke failed for ${smokeFailures.length} of ${smokeResults.length} images:
|
|
17885
|
+
` + 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)."
|
|
17886
|
+
);
|
|
17118
17887
|
process.exitCode = 1;
|
|
17119
17888
|
return;
|
|
17120
17889
|
}
|
|
17121
|
-
const
|
|
17890
|
+
const swapBoundarySha = captureHeadSha(cwd);
|
|
17891
|
+
if (swapBoundarySha !== null && swapBoundarySha !== _targetSha && !opts.force) {
|
|
17892
|
+
printError(
|
|
17893
|
+
`HEAD drifted during build window:
|
|
17894
|
+
captured (after pull): ${abbreviateSha(_targetSha)}
|
|
17895
|
+
current at swap: ${abbreviateSha(swapBoundarySha)}
|
|
17896
|
+
|
|
17897
|
+
Operator-driven \`git checkout\` or \`git reset\` triggered drift.
|
|
17898
|
+
Recovery options:
|
|
17899
|
+
\u2022 Re-run \`olam upgrade\` (will rebuild against current HEAD).
|
|
17900
|
+
\u2022 Pass \`--force\` to swap anyway (canonical advances to the
|
|
17901
|
+
captured-at-pull SHA, NOT current HEAD).`
|
|
17902
|
+
);
|
|
17903
|
+
process.exitCode = 1;
|
|
17904
|
+
return;
|
|
17905
|
+
}
|
|
17906
|
+
process.stdout.write(` ${pc15.dim("atomic 6-tag swap".padEnd(34))}`);
|
|
17907
|
+
const swapStart = Date.now();
|
|
17908
|
+
const swapResult = performAtomicSwap(PRODUCTION_SWAP_PLAN);
|
|
17909
|
+
const swapDurationMs = Date.now() - swapStart;
|
|
17910
|
+
const swapDur = `${(swapDurationMs / 1e3).toFixed(1)}s`;
|
|
17911
|
+
process.stdout.write(`${swapResult.ok ? pc15.green("\u2713") : pc15.red("\u2717")} ${swapDur}
|
|
17912
|
+
`);
|
|
17913
|
+
timings.push({ label: "atomic swap", durationMs: swapDurationMs });
|
|
17914
|
+
if (!swapResult.ok) {
|
|
17915
|
+
printError(`Atomic swap failed: ${swapResult.summary}`);
|
|
17916
|
+
process.exitCode = 1;
|
|
17917
|
+
return;
|
|
17918
|
+
}
|
|
17919
|
+
printInfo("Swap", swapResult.summary);
|
|
17920
|
+
const composeFile = path28.join(cwd, "packages/host-cp/compose.yaml");
|
|
17122
17921
|
process.stdout.write(` ${pc15.dim("docker compose recreate".padEnd(34))}`);
|
|
17123
17922
|
const composeStart = Date.now();
|
|
17124
17923
|
const composeResult = runCompose(
|
|
@@ -17133,8 +17932,33 @@ ${imageResult.stderr}`);
|
|
|
17133
17932
|
`);
|
|
17134
17933
|
timings.push({ label: "container recreate", durationMs: composeDurationMs });
|
|
17135
17934
|
if (!composeOk) {
|
|
17136
|
-
printError(
|
|
17137
|
-
|
|
17935
|
+
printError(
|
|
17936
|
+
`docker compose up --force-recreate failed:
|
|
17937
|
+
${composeResult.stderr}
|
|
17938
|
+
|
|
17939
|
+
Canonical tags advanced to new SHA but the stack failed to start.
|
|
17940
|
+
Recovery options:
|
|
17941
|
+
\u2022 Run \`olam upgrade --rollback\` to restore the prior :olam-rollback set, then investigate.
|
|
17942
|
+
\u2022 Manually \`docker logs olam-host-cp\` to diagnose; if recoverable, retry recreate without rollback.`
|
|
17943
|
+
);
|
|
17944
|
+
process.exitCode = 1;
|
|
17945
|
+
return;
|
|
17946
|
+
}
|
|
17947
|
+
process.stdout.write(` ${pc15.dim("recreate auth-service".padEnd(34))}`);
|
|
17948
|
+
const authResult = await recreateAuthService();
|
|
17949
|
+
const authDur = `${(authResult.durationMs / 1e3).toFixed(1)}s`;
|
|
17950
|
+
process.stdout.write(`${authResult.ok ? pc15.green("\u2713") : pc15.red("\u2717")} ${authDur}
|
|
17951
|
+
`);
|
|
17952
|
+
timings.push({ label: "auth recreate", durationMs: authResult.durationMs });
|
|
17953
|
+
if (!authResult.ok) {
|
|
17954
|
+
printError(
|
|
17955
|
+
`Auth-service recreate failed: ${authResult.error ?? "unknown"}
|
|
17956
|
+
|
|
17957
|
+
Canonical tags advanced to new SHA; host-cp recreated but auth-service is broken.
|
|
17958
|
+
Recovery options:
|
|
17959
|
+
\u2022 Run \`olam upgrade --rollback\` to restore the prior :olam-rollback set + working stack.
|
|
17960
|
+
\u2022 Manually: \`docker logs olam-auth\` to diagnose; \`olam auth up\` to restart.`
|
|
17961
|
+
);
|
|
17138
17962
|
process.exitCode = 1;
|
|
17139
17963
|
return;
|
|
17140
17964
|
}
|
|
@@ -17147,7 +17971,23 @@ ${composeResult.stderr}`);
|
|
|
17147
17971
|
`);
|
|
17148
17972
|
timings.push({ label: "/health", durationMs: healthDurationMs });
|
|
17149
17973
|
if (!healthy) {
|
|
17150
|
-
printWarning(
|
|
17974
|
+
printWarning(
|
|
17975
|
+
"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."
|
|
17976
|
+
);
|
|
17977
|
+
}
|
|
17978
|
+
process.stdout.write(` ${pc15.dim("verify /version/status round-trip".padEnd(34))}`);
|
|
17979
|
+
const versionStart = Date.now();
|
|
17980
|
+
const versionMatch = await waitForVersionMatch(_targetSha, 6e4);
|
|
17981
|
+
const versionDurationMs = Date.now() - versionStart;
|
|
17982
|
+
const versionDur = `${(versionDurationMs / 1e3).toFixed(1)}s`;
|
|
17983
|
+
process.stdout.write(`${versionMatch.matched ? pc15.green("\u2713") : pc15.yellow("?")} ${versionDur}
|
|
17984
|
+
`);
|
|
17985
|
+
timings.push({ label: "/version/status round-trip", durationMs: versionDurationMs });
|
|
17986
|
+
if (!versionMatch.matched) {
|
|
17987
|
+
printWarning(
|
|
17988
|
+
`Version round-trip incomplete after ${(versionDurationMs / 1e3).toFixed(0)}s:
|
|
17989
|
+
` + 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."
|
|
17990
|
+
);
|
|
17151
17991
|
}
|
|
17152
17992
|
process.stdout.write("\n");
|
|
17153
17993
|
printSuccess("Upgrade complete");
|
|
@@ -17164,10 +18004,22 @@ function printTimings2(timings) {
|
|
|
17164
18004
|
printInfo("total", `${(total / 1e3).toFixed(1)}s`);
|
|
17165
18005
|
}
|
|
17166
18006
|
function registerUpgrade(program2) {
|
|
17167
|
-
program2.command("upgrade").description("Self-upgrade the local Olam dev stack (pull + rebuild + restart
|
|
18007
|
+
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
18008
|
"--skip-install",
|
|
17169
18009
|
"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)").
|
|
18010
|
+
).option("--branch <name>", "Switch to this branch before pulling (refuses if working tree is dirty)").option(
|
|
18011
|
+
"--rollback",
|
|
18012
|
+
"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."
|
|
18013
|
+
).option(
|
|
18014
|
+
"--force",
|
|
18015
|
+
"Bypass HEAD-drift refusal at the swap boundary. Swap advances canonical to the\n captured-at-pull SHA even if current HEAD differs."
|
|
18016
|
+
).option(
|
|
18017
|
+
"--no-cache",
|
|
18018
|
+
"Pass --no-cache to all three build scripts (DOCKER_BUILD_NO_CACHE=1).\n Useful when retrying after a cache-poisoning failure."
|
|
18019
|
+
).option(
|
|
18020
|
+
"--history",
|
|
18021
|
+
"Print the upgrade history (~/.olam/upgrade.log) and exit.\n No upgrade is performed."
|
|
18022
|
+
).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
18023
|
await handleUpgrade(parseUpgradeOpts(opts));
|
|
17172
18024
|
});
|
|
17173
18025
|
}
|
|
@@ -17301,7 +18153,7 @@ function registerLogs(program2) {
|
|
|
17301
18153
|
// src/commands/ps.ts
|
|
17302
18154
|
init_context();
|
|
17303
18155
|
import pc17 from "picocolors";
|
|
17304
|
-
import { spawnSync as
|
|
18156
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
17305
18157
|
var SAFE_IDENT4 = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
17306
18158
|
function parseDockerTop(stdout) {
|
|
17307
18159
|
const trimmed = stdout.trim();
|
|
@@ -17401,7 +18253,7 @@ function registerPs(program2) {
|
|
|
17401
18253
|
const containerName = `olam-${worldId}-devbox`;
|
|
17402
18254
|
let watchInterval;
|
|
17403
18255
|
function fetchAndPrint() {
|
|
17404
|
-
const result =
|
|
18256
|
+
const result = spawnSync8(
|
|
17405
18257
|
"docker",
|
|
17406
18258
|
["top", containerName, "pid", "user", "pcpu", "pmem", "stime", "stat", "cmd"],
|
|
17407
18259
|
{ encoding: "utf-8", timeout: 3e3 }
|
|
@@ -17437,20 +18289,20 @@ ${pc17.dim(`world: ${worldId} sort: ${sortKey} refresh: 5s Ctrl-C to exit`)}
|
|
|
17437
18289
|
}
|
|
17438
18290
|
|
|
17439
18291
|
// src/commands/keys.ts
|
|
17440
|
-
import * as
|
|
17441
|
-
import * as
|
|
17442
|
-
import * as
|
|
18292
|
+
import * as fs25 from "node:fs";
|
|
18293
|
+
import * as os15 from "node:os";
|
|
18294
|
+
import * as path29 from "node:path";
|
|
17443
18295
|
import YAML4 from "yaml";
|
|
17444
18296
|
function olamHome2() {
|
|
17445
|
-
return process.env.OLAM_HOME ??
|
|
18297
|
+
return process.env.OLAM_HOME ?? path29.join(os15.homedir(), ".olam");
|
|
17446
18298
|
}
|
|
17447
18299
|
function keysFilePath() {
|
|
17448
|
-
return
|
|
18300
|
+
return path29.join(olamHome2(), "keys.yaml");
|
|
17449
18301
|
}
|
|
17450
18302
|
function readKeysFile() {
|
|
17451
18303
|
const filePath = keysFilePath();
|
|
17452
|
-
if (!
|
|
17453
|
-
const raw =
|
|
18304
|
+
if (!fs25.existsSync(filePath)) return null;
|
|
18305
|
+
const raw = fs25.readFileSync(filePath, "utf-8").trim();
|
|
17454
18306
|
if (raw.length === 0) return null;
|
|
17455
18307
|
try {
|
|
17456
18308
|
const parsed = YAML4.parse(raw);
|
|
@@ -17466,13 +18318,13 @@ function readKeysFile() {
|
|
|
17466
18318
|
}
|
|
17467
18319
|
function writeKeysFile(keys) {
|
|
17468
18320
|
const dir = olamHome2();
|
|
17469
|
-
if (!
|
|
17470
|
-
|
|
18321
|
+
if (!fs25.existsSync(dir)) {
|
|
18322
|
+
fs25.mkdirSync(dir, { recursive: true });
|
|
17471
18323
|
}
|
|
17472
18324
|
const filePath = keysFilePath();
|
|
17473
18325
|
const content = YAML4.stringify(keys);
|
|
17474
|
-
|
|
17475
|
-
|
|
18326
|
+
fs25.writeFileSync(filePath, content, { encoding: "utf-8", mode: 384 });
|
|
18327
|
+
fs25.chmodSync(filePath, 384);
|
|
17476
18328
|
}
|
|
17477
18329
|
function redact(value) {
|
|
17478
18330
|
if (value.length <= 8) return value + "...";
|
|
@@ -17515,7 +18367,7 @@ function registerKeys(program2) {
|
|
|
17515
18367
|
}
|
|
17516
18368
|
const { [key]: _removed, ...rest } = existing;
|
|
17517
18369
|
if (Object.keys(rest).length === 0) {
|
|
17518
|
-
|
|
18370
|
+
fs25.unlinkSync(keysFilePath());
|
|
17519
18371
|
} else {
|
|
17520
18372
|
writeKeysFile(rest);
|
|
17521
18373
|
}
|
|
@@ -17538,26 +18390,26 @@ function registerKeys(program2) {
|
|
|
17538
18390
|
}
|
|
17539
18391
|
|
|
17540
18392
|
// src/commands/world-snapshot.ts
|
|
17541
|
-
import * as
|
|
17542
|
-
import * as
|
|
18393
|
+
import * as fs27 from "node:fs";
|
|
18394
|
+
import * as path31 from "node:path";
|
|
17543
18395
|
import { execSync as execSync9 } from "node:child_process";
|
|
17544
18396
|
import pc18 from "picocolors";
|
|
17545
18397
|
|
|
17546
18398
|
// ../core/src/world/snapshot.ts
|
|
17547
18399
|
import * as crypto6 from "node:crypto";
|
|
17548
|
-
import * as
|
|
17549
|
-
import * as
|
|
17550
|
-
import * as
|
|
18400
|
+
import * as fs26 from "node:fs";
|
|
18401
|
+
import * as os16 from "node:os";
|
|
18402
|
+
import * as path30 from "node:path";
|
|
17551
18403
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
17552
18404
|
function snapshotsDir() {
|
|
17553
|
-
return process.env["OLAM_SNAPSHOTS_DIR"] ??
|
|
18405
|
+
return process.env["OLAM_SNAPSHOTS_DIR"] ?? path30.join(os16.homedir(), ".olam", "snapshots");
|
|
17554
18406
|
}
|
|
17555
18407
|
function snapshotKindDir(worldId, kind) {
|
|
17556
|
-
return
|
|
18408
|
+
return path30.join(snapshotsDir(), worldId, kind);
|
|
17557
18409
|
}
|
|
17558
18410
|
function snapshotTarPath(worldId, kind, repoName, hash) {
|
|
17559
18411
|
const base = repoName ? `${repoName}-${hash}` : hash;
|
|
17560
|
-
return
|
|
18412
|
+
return path30.join(snapshotKindDir(worldId, kind), `${base}.tar.gz`);
|
|
17561
18413
|
}
|
|
17562
18414
|
function manifestPath(tarPath) {
|
|
17563
18415
|
return tarPath.replace(/\.tar\.gz$/, ".manifest.json");
|
|
@@ -17574,16 +18426,16 @@ function hashBuffers(entries) {
|
|
|
17574
18426
|
return hash.digest("hex").slice(0, 12);
|
|
17575
18427
|
}
|
|
17576
18428
|
function computeGemsFingerprint(repoDir) {
|
|
17577
|
-
const lockfile =
|
|
17578
|
-
if (!
|
|
17579
|
-
return hashBuffers([{ path: "Gemfile.lock", content:
|
|
18429
|
+
const lockfile = path30.join(repoDir, "Gemfile.lock");
|
|
18430
|
+
if (!fs26.existsSync(lockfile)) return null;
|
|
18431
|
+
return hashBuffers([{ path: "Gemfile.lock", content: fs26.readFileSync(lockfile) }]);
|
|
17580
18432
|
}
|
|
17581
18433
|
function computeNodeFingerprint(repoDir) {
|
|
17582
18434
|
const candidates = ["yarn.lock", "pnpm-lock.yaml", "package-lock.json"];
|
|
17583
18435
|
for (const name of candidates) {
|
|
17584
|
-
const lockfile =
|
|
17585
|
-
if (
|
|
17586
|
-
return hashBuffers([{ path: name, content:
|
|
18436
|
+
const lockfile = path30.join(repoDir, name);
|
|
18437
|
+
if (fs26.existsSync(lockfile)) {
|
|
18438
|
+
return hashBuffers([{ path: name, content: fs26.readFileSync(lockfile) }]);
|
|
17587
18439
|
}
|
|
17588
18440
|
}
|
|
17589
18441
|
return null;
|
|
@@ -17593,59 +18445,59 @@ function computePgFingerprint(repoDirs) {
|
|
|
17593
18445
|
const entries = [];
|
|
17594
18446
|
for (const repoDir of repoDirs) {
|
|
17595
18447
|
for (const pattern of patterns) {
|
|
17596
|
-
const filePath =
|
|
17597
|
-
if (
|
|
17598
|
-
entries.push({ path: filePath, content:
|
|
18448
|
+
const filePath = path30.join(repoDir, pattern);
|
|
18449
|
+
if (fs26.existsSync(filePath)) {
|
|
18450
|
+
entries.push({ path: filePath, content: fs26.readFileSync(filePath) });
|
|
17599
18451
|
}
|
|
17600
18452
|
}
|
|
17601
18453
|
}
|
|
17602
18454
|
return entries.length > 0 ? hashBuffers(entries) : null;
|
|
17603
18455
|
}
|
|
17604
18456
|
function packTarball(srcDir, destPath, opts = {}) {
|
|
17605
|
-
|
|
18457
|
+
fs26.mkdirSync(path30.dirname(destPath), { recursive: true });
|
|
17606
18458
|
const tmp = `${destPath}.tmp`;
|
|
17607
18459
|
const args = [];
|
|
17608
18460
|
if (opts.followSymlinks) args.push("-h");
|
|
17609
18461
|
args.push("-czf", tmp, "-C", srcDir, ".");
|
|
17610
18462
|
try {
|
|
17611
18463
|
execFileSync4("tar", args, { stdio: "pipe" });
|
|
17612
|
-
|
|
18464
|
+
fs26.renameSync(tmp, destPath);
|
|
17613
18465
|
} catch (err) {
|
|
17614
18466
|
try {
|
|
17615
|
-
|
|
18467
|
+
fs26.rmSync(tmp, { force: true });
|
|
17616
18468
|
} catch {
|
|
17617
18469
|
}
|
|
17618
18470
|
throw err;
|
|
17619
18471
|
}
|
|
17620
18472
|
}
|
|
17621
18473
|
function writeManifest(manifest, tarPath) {
|
|
17622
|
-
|
|
18474
|
+
fs26.writeFileSync(manifestPath(tarPath), JSON.stringify(manifest, null, 2), "utf-8");
|
|
17623
18475
|
}
|
|
17624
18476
|
function readManifest(tarPath) {
|
|
17625
18477
|
const mPath = manifestPath(tarPath);
|
|
17626
|
-
if (!
|
|
18478
|
+
if (!fs26.existsSync(mPath)) return null;
|
|
17627
18479
|
try {
|
|
17628
|
-
return JSON.parse(
|
|
18480
|
+
return JSON.parse(fs26.readFileSync(mPath, "utf-8"));
|
|
17629
18481
|
} catch {
|
|
17630
18482
|
return null;
|
|
17631
18483
|
}
|
|
17632
18484
|
}
|
|
17633
18485
|
function listSnapshots(worldIdFilter) {
|
|
17634
18486
|
const root = snapshotsDir();
|
|
17635
|
-
if (!
|
|
18487
|
+
if (!fs26.existsSync(root)) return [];
|
|
17636
18488
|
const now = Date.now();
|
|
17637
18489
|
const results = [];
|
|
17638
|
-
const worlds = worldIdFilter ? [worldIdFilter] :
|
|
18490
|
+
const worlds = worldIdFilter ? [worldIdFilter] : fs26.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
17639
18491
|
for (const worldId of worlds) {
|
|
17640
|
-
const worldDir =
|
|
17641
|
-
if (!
|
|
18492
|
+
const worldDir = path30.join(root, worldId);
|
|
18493
|
+
if (!fs26.existsSync(worldDir) || !fs26.statSync(worldDir).isDirectory()) continue;
|
|
17642
18494
|
for (const kind of ["gems", "node", "pg"]) {
|
|
17643
|
-
const kindDir =
|
|
17644
|
-
if (!
|
|
17645
|
-
const tarballs =
|
|
18495
|
+
const kindDir = path30.join(worldDir, kind);
|
|
18496
|
+
if (!fs26.existsSync(kindDir)) continue;
|
|
18497
|
+
const tarballs = fs26.readdirSync(kindDir).filter((f) => f.endsWith(".tar.gz"));
|
|
17646
18498
|
for (const tarFile of tarballs) {
|
|
17647
|
-
const tarPath =
|
|
17648
|
-
const stat =
|
|
18499
|
+
const tarPath = path30.join(kindDir, tarFile);
|
|
18500
|
+
const stat = fs26.statSync(tarPath);
|
|
17649
18501
|
const manifest = readManifest(tarPath);
|
|
17650
18502
|
if (!manifest) continue;
|
|
17651
18503
|
results.push({ manifest, tarPath, ageMs: now - stat.mtimeMs });
|
|
@@ -17724,17 +18576,17 @@ function resolveKinds(arg) {
|
|
|
17724
18576
|
return [];
|
|
17725
18577
|
}
|
|
17726
18578
|
async function captureGems(worldId, workspacePath, repo) {
|
|
17727
|
-
const repoDir =
|
|
18579
|
+
const repoDir = path31.join(workspacePath, repo);
|
|
17728
18580
|
const fingerprint = computeGemsFingerprint(repoDir);
|
|
17729
18581
|
if (!fingerprint) {
|
|
17730
18582
|
return { ok: false, tarPath: "", msg: "no Gemfile.lock \u2014 layer does not apply" };
|
|
17731
18583
|
}
|
|
17732
18584
|
const tarPath = snapshotTarPath(worldId, "gems", repo, fingerprint);
|
|
17733
|
-
const vendorBundle =
|
|
17734
|
-
if (
|
|
18585
|
+
const vendorBundle = path31.join(repoDir, "vendor", "bundle");
|
|
18586
|
+
if (fs27.existsSync(vendorBundle)) {
|
|
17735
18587
|
try {
|
|
17736
18588
|
packTarball(vendorBundle, tarPath);
|
|
17737
|
-
const stat =
|
|
18589
|
+
const stat = fs27.statSync(tarPath);
|
|
17738
18590
|
const manifest = {
|
|
17739
18591
|
kind: "gems",
|
|
17740
18592
|
worldId,
|
|
@@ -17767,10 +18619,10 @@ async function captureGems(worldId, workspacePath, repo) {
|
|
|
17767
18619
|
`docker exec ${containerName} sh -c 'mkdir -p "$(dirname ${tmpTar})" && tar -czf ${tmpTar}.tmp -C ${bundlePath} . && mv ${tmpTar}.tmp ${tmpTar}'`,
|
|
17768
18620
|
{ stdio: "pipe", timeout: 12e4 }
|
|
17769
18621
|
);
|
|
17770
|
-
|
|
18622
|
+
fs27.mkdirSync(path31.dirname(tarPath), { recursive: true });
|
|
17771
18623
|
execSync9(`docker cp ${containerName}:${tmpTar} "${tarPath}"`, { stdio: "pipe", timeout: 12e4 });
|
|
17772
18624
|
execSync9(`docker exec ${containerName} rm -f ${tmpTar}`, { stdio: "pipe" });
|
|
17773
|
-
const stat =
|
|
18625
|
+
const stat = fs27.statSync(tarPath);
|
|
17774
18626
|
const manifest = {
|
|
17775
18627
|
kind: "gems",
|
|
17776
18628
|
worldId,
|
|
@@ -17787,19 +18639,19 @@ async function captureGems(worldId, workspacePath, repo) {
|
|
|
17787
18639
|
}
|
|
17788
18640
|
}
|
|
17789
18641
|
async function captureNode(worldId, workspacePath, repo) {
|
|
17790
|
-
const repoDir =
|
|
18642
|
+
const repoDir = path31.join(workspacePath, repo);
|
|
17791
18643
|
const fingerprint = computeNodeFingerprint(repoDir);
|
|
17792
18644
|
if (!fingerprint) {
|
|
17793
18645
|
return { ok: false, tarPath: "", msg: "no lockfile \u2014 layer does not apply" };
|
|
17794
18646
|
}
|
|
17795
|
-
const nodeModules =
|
|
17796
|
-
if (!
|
|
18647
|
+
const nodeModules = path31.join(repoDir, "node_modules");
|
|
18648
|
+
if (!fs27.existsSync(nodeModules)) {
|
|
17797
18649
|
return { ok: false, tarPath: "", msg: "node_modules not installed yet" };
|
|
17798
18650
|
}
|
|
17799
18651
|
const tarPath = snapshotTarPath(worldId, "node", repo, fingerprint);
|
|
17800
18652
|
try {
|
|
17801
18653
|
packTarball(nodeModules, tarPath);
|
|
17802
|
-
const stat =
|
|
18654
|
+
const stat = fs27.statSync(tarPath);
|
|
17803
18655
|
const manifest = {
|
|
17804
18656
|
kind: "node",
|
|
17805
18657
|
worldId,
|
|
@@ -17816,7 +18668,7 @@ async function captureNode(worldId, workspacePath, repo) {
|
|
|
17816
18668
|
}
|
|
17817
18669
|
}
|
|
17818
18670
|
async function capturePg(worldId, workspacePath, repoNames) {
|
|
17819
|
-
const repoDirs = repoNames.map((r) =>
|
|
18671
|
+
const repoDirs = repoNames.map((r) => path31.join(workspacePath, r));
|
|
17820
18672
|
const fingerprint = computePgFingerprint(repoDirs);
|
|
17821
18673
|
if (!fingerprint) {
|
|
17822
18674
|
return { ok: false, tarPath: "", msg: "no Gemfile.lock / schema.rb \u2014 layer does not apply" };
|
|
@@ -17831,13 +18683,13 @@ async function capturePg(worldId, workspacePath, repoNames) {
|
|
|
17831
18683
|
}
|
|
17832
18684
|
try {
|
|
17833
18685
|
execSync9(`docker stop ${containerName}`, { stdio: "pipe", timeout: 3e4 });
|
|
17834
|
-
|
|
18686
|
+
fs27.mkdirSync(path31.dirname(tarPath), { recursive: true });
|
|
17835
18687
|
execSync9(
|
|
17836
|
-
`docker run --rm -v "${volumeName2}:/pgdata:ro" -v "${
|
|
18688
|
+
`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
18689
|
{ stdio: "pipe", timeout: 18e4 }
|
|
17838
18690
|
);
|
|
17839
18691
|
execSync9(`docker start ${containerName}`, { stdio: "pipe", timeout: 3e4 });
|
|
17840
|
-
const stat =
|
|
18692
|
+
const stat = fs27.statSync(tarPath);
|
|
17841
18693
|
const manifest = {
|
|
17842
18694
|
kind: "pg",
|
|
17843
18695
|
worldId,
|
|
@@ -17911,35 +18763,35 @@ function formatAge2(ms) {
|
|
|
17911
18763
|
|
|
17912
18764
|
// src/commands/refresh.ts
|
|
17913
18765
|
init_context();
|
|
17914
|
-
import * as
|
|
17915
|
-
import * as
|
|
17916
|
-
import * as
|
|
17917
|
-
import { spawnSync as
|
|
18766
|
+
import * as fs29 from "node:fs";
|
|
18767
|
+
import * as os17 from "node:os";
|
|
18768
|
+
import * as path33 from "node:path";
|
|
18769
|
+
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
17918
18770
|
import ora5 from "ora";
|
|
17919
18771
|
|
|
17920
18772
|
// src/commands/refresh-helpers.ts
|
|
17921
|
-
import * as
|
|
17922
|
-
import * as
|
|
18773
|
+
import * as fs28 from "node:fs";
|
|
18774
|
+
import * as path32 from "node:path";
|
|
17923
18775
|
function collectCpSourceFiles(standaloneDir) {
|
|
17924
|
-
if (!
|
|
18776
|
+
if (!fs28.existsSync(standaloneDir)) {
|
|
17925
18777
|
throw new Error(`CP standalone dir not found: ${standaloneDir}`);
|
|
17926
18778
|
}
|
|
17927
18779
|
const entries = [];
|
|
17928
|
-
const topLevel =
|
|
17929
|
-
const stat =
|
|
18780
|
+
const topLevel = fs28.readdirSync(standaloneDir).filter((f) => {
|
|
18781
|
+
const stat = fs28.statSync(path32.join(standaloneDir, f));
|
|
17930
18782
|
return stat.isFile() && f.endsWith(".mjs") && !f.endsWith(".test.mjs");
|
|
17931
18783
|
}).sort();
|
|
17932
18784
|
for (const f of topLevel) {
|
|
17933
|
-
entries.push({ srcPath:
|
|
18785
|
+
entries.push({ srcPath: path32.join(standaloneDir, f), destRelPath: f });
|
|
17934
18786
|
}
|
|
17935
|
-
const libDir =
|
|
17936
|
-
if (
|
|
17937
|
-
const libFiles =
|
|
17938
|
-
const stat =
|
|
18787
|
+
const libDir = path32.join(standaloneDir, "lib");
|
|
18788
|
+
if (fs28.existsSync(libDir) && fs28.statSync(libDir).isDirectory()) {
|
|
18789
|
+
const libFiles = fs28.readdirSync(libDir).filter((f) => {
|
|
18790
|
+
const stat = fs28.statSync(path32.join(libDir, f));
|
|
17939
18791
|
return stat.isFile() && f.endsWith(".mjs") && !f.endsWith(".test.mjs");
|
|
17940
18792
|
}).sort();
|
|
17941
18793
|
for (const f of libFiles) {
|
|
17942
|
-
entries.push({ srcPath:
|
|
18794
|
+
entries.push({ srcPath: path32.join(libDir, f), destRelPath: `lib/${f}` });
|
|
17943
18795
|
}
|
|
17944
18796
|
}
|
|
17945
18797
|
return entries;
|
|
@@ -17958,7 +18810,7 @@ var RESTART_TIMEOUT_S = 30;
|
|
|
17958
18810
|
var HEALTH_POLL_MS = 500;
|
|
17959
18811
|
var HEALTH_TIMEOUT_MS = 3e4;
|
|
17960
18812
|
function docker(args) {
|
|
17961
|
-
const result =
|
|
18813
|
+
const result = spawnSync9("docker", args, {
|
|
17962
18814
|
encoding: "utf-8",
|
|
17963
18815
|
stdio: ["ignore", "pipe", "pipe"]
|
|
17964
18816
|
});
|
|
@@ -17997,16 +18849,16 @@ async function refreshWorld(worldId, portOffset, standaloneDir, opts) {
|
|
|
17997
18849
|
error: err instanceof Error ? err.message : String(err)
|
|
17998
18850
|
};
|
|
17999
18851
|
}
|
|
18000
|
-
const stagingDir =
|
|
18001
|
-
|
|
18852
|
+
const stagingDir = fs29.mkdtempSync(
|
|
18853
|
+
path33.join(os17.tmpdir(), `olam-refresh-${worldId}-`)
|
|
18002
18854
|
);
|
|
18003
18855
|
try {
|
|
18004
18856
|
const hasLib = entries.some((e) => e.destRelPath.startsWith("lib/"));
|
|
18005
18857
|
if (hasLib) {
|
|
18006
|
-
|
|
18858
|
+
fs29.mkdirSync(path33.join(stagingDir, "lib"), { recursive: true });
|
|
18007
18859
|
}
|
|
18008
18860
|
for (const { srcPath, destRelPath } of entries) {
|
|
18009
|
-
|
|
18861
|
+
fs29.copyFileSync(srcPath, path33.join(stagingDir, destRelPath));
|
|
18010
18862
|
}
|
|
18011
18863
|
const cpResult = docker([
|
|
18012
18864
|
"cp",
|
|
@@ -18021,7 +18873,7 @@ async function refreshWorld(worldId, portOffset, standaloneDir, opts) {
|
|
|
18021
18873
|
};
|
|
18022
18874
|
}
|
|
18023
18875
|
} finally {
|
|
18024
|
-
|
|
18876
|
+
fs29.rmSync(stagingDir, { recursive: true, force: true });
|
|
18025
18877
|
}
|
|
18026
18878
|
if (opts.restart) {
|
|
18027
18879
|
const restartResult = docker([
|
|
@@ -18058,11 +18910,11 @@ function registerRefresh(program2) {
|
|
|
18058
18910
|
process.exitCode = 1;
|
|
18059
18911
|
return;
|
|
18060
18912
|
}
|
|
18061
|
-
const standaloneDir =
|
|
18913
|
+
const standaloneDir = path33.join(
|
|
18062
18914
|
process.cwd(),
|
|
18063
18915
|
"packages/control-plane/standalone"
|
|
18064
18916
|
);
|
|
18065
|
-
if (!
|
|
18917
|
+
if (!fs29.existsSync(standaloneDir)) {
|
|
18066
18918
|
printError(
|
|
18067
18919
|
`CP standalone source not found at ${standaloneDir}.
|
|
18068
18920
|
Run \`olam refresh\` from the olam repo root.`
|
|
@@ -18141,18 +18993,18 @@ Run \`olam refresh\` from the olam repo root.`
|
|
|
18141
18993
|
}
|
|
18142
18994
|
|
|
18143
18995
|
// src/pleri-config.ts
|
|
18144
|
-
import * as
|
|
18145
|
-
import * as
|
|
18996
|
+
import * as fs30 from "node:fs";
|
|
18997
|
+
import * as path34 from "node:path";
|
|
18146
18998
|
function isPleriConfigured(configDir = process.env.OLAM_CONFIG_DIR ?? ".olam") {
|
|
18147
18999
|
if (process.env.PLERI_BASE_URL) {
|
|
18148
19000
|
return true;
|
|
18149
19001
|
}
|
|
18150
|
-
const configPath =
|
|
18151
|
-
if (!
|
|
19002
|
+
const configPath = path34.join(configDir, "config.yaml");
|
|
19003
|
+
if (!fs30.existsSync(configPath)) {
|
|
18152
19004
|
return false;
|
|
18153
19005
|
}
|
|
18154
19006
|
try {
|
|
18155
|
-
const contents =
|
|
19007
|
+
const contents = fs30.readFileSync(configPath, "utf8");
|
|
18156
19008
|
return /^[^#\n]*\bpleri:/m.test(contents);
|
|
18157
19009
|
} catch {
|
|
18158
19010
|
return false;
|