alexandr 0.3.3 → 0.3.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alexandr",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Run the alexandr workspace runtime locally (npx alexandr up), and develop alexandr apps against a workspace (alexandr app link | dev | build | deploy).",
5
5
  "type": "module",
6
6
  "bin": {
package/src/docker.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // `docker compose` against the published kernel image. Pure Node child_process.
3
3
 
4
4
  import { spawnSync } from "node:child_process";
5
+ import fs from "node:fs";
5
6
  import path from "node:path";
6
7
 
7
8
  import { EXIT } from "./exit.js";
@@ -38,12 +39,44 @@ function baseArgs(dir, projectName) {
38
39
  ];
39
40
  }
40
41
 
42
+ /**
43
+ * ⚠⚠ THE INSTANCE'S `.env` IS THE ONLY SOURCE COMPOSE INTERPOLATES FROM. Compose gives the
44
+ * PROCESS environment precedence over the project's `.env`, and the updater sidecar's own
45
+ * environment is that very `.env`, frozen at the container's start (`env_file` on the service).
46
+ * So the recipe run from inside pinned `ALEXANDR_IMAGE=v0.0.13` in the file, then compose pulled
47
+ * and recreated v0.0.12 from the sidecar's stale variable — "Updated: 0.0.12 → 0.0.12", every
48
+ * press, including the one the 2026-09-08 E2E called a success (found on the first human press,
49
+ * aos, 2026-09-09). The same shadowing bites a host shell that exported `ALEXANDR_IMAGE` once.
50
+ * Every key the file defines is dropped from the child's environment; explicit `overrides`
51
+ * (a caller's deliberate one-off) still win.
52
+ */
53
+ export function composeEnv(dir, base = process.env, overrides = {}) {
54
+ const env = { ...base };
55
+ for (const key of envFileKeys(dir)) delete env[key];
56
+ return { ...env, ...overrides };
57
+ }
58
+
59
+ // The KEYS the instance's `.env` defines (uncommented lines only) — a hand copy of the
60
+ // instance module's parser, kept here because that module imports this one.
61
+ function envFileKeys(dir) {
62
+ try {
63
+ return fs
64
+ .readFileSync(path.join(dir, ".env"), "utf8")
65
+ .split("\n")
66
+ .map((line) => line.match(/^\s*([A-Z0-9_]+)\s*=/))
67
+ .filter(Boolean)
68
+ .map((m) => m[1]);
69
+ } catch {
70
+ return [];
71
+ }
72
+ }
73
+
41
74
  // Run a compose subcommand, inheriting stdio by default (streams to the user).
42
75
  export function compose(dir, projectName, args, opts = {}) {
43
76
  const r = spawnSync("docker", [...baseArgs(dir, projectName), ...args], {
44
77
  stdio: opts.stdio ?? "inherit",
45
78
  encoding: "utf8",
46
- env: { ...process.env, ...(opts.env || {}) },
79
+ env: composeEnv(dir, process.env, opts.env || {}),
47
80
  });
48
81
  return { status: r.error ? 127 : r.status ?? 1 };
49
82
  }
@@ -52,7 +85,7 @@ export function compose(dir, projectName, args, opts = {}) {
52
85
  export function composeCapture(dir, projectName, args, env = {}) {
53
86
  const r = spawnSync("docker", [...baseArgs(dir, projectName), ...args], {
54
87
  encoding: "utf8",
55
- env: { ...process.env, ...env },
88
+ env: composeEnv(dir, process.env, env),
56
89
  });
57
90
  return {
58
91
  status: r.error ? 127 : r.status ?? 1,
package/src/updater.js CHANGED
@@ -124,6 +124,17 @@ export function createUpdaterHandler(deps) {
124
124
  } catch (e) {
125
125
  result = { ok: false, error: e?.message || String(e) };
126
126
  }
127
+ // ⚠ THE RECEIPT IS THE VERDICT, NOT THE EXIT CODE. `alexandr update` exits 0 after
128
+ // recreating whatever image compose resolved; when that is not the target, the box is
129
+ // "updated" onto the version it already ran (the 2026-09-09 press: `Updated: 0.0.12 →
130
+ // 0.0.12` with 0.0.13 asked for — the sidecar's own environment shadowed the pin). A done
131
+ // that names the wrong version is a failure with words, and the sheet offers the way back.
132
+ if (result.ok && !rollback && to && result.to && result.to !== to) {
133
+ result = {
134
+ ok: false,
135
+ error: `the box came back on ${result.to}, not ${to} — the recipe recreated the image it already ran`,
136
+ };
137
+ }
127
138
  if (result.ok) {
128
139
  write({ state: "done", phase: "done", to: result.to ?? to, from: result.from ?? null, rollback, startedAt, finishedAt: now() });
129
140
  } else {