amalgm 0.1.167 → 0.1.168

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/lib/supervisor.js CHANGED
@@ -47,6 +47,7 @@ const {
47
47
  runtimeServiceScripts,
48
48
  } = require('./runtime-manifest');
49
49
  const {
50
+ UPDATE_TARGET_MISMATCH,
50
51
  checkForUpdate,
51
52
  shouldAutoUpdate,
52
53
  updatePackage,
@@ -58,19 +59,6 @@ const PORT_ALLOCATION_LOCK_FILE = path.join(PORT_ALLOCATION_LOCK_DIR, 'owner.jso
58
59
  const PORT_ALLOCATION_LOCK_TIMEOUT_MS = 45_000;
59
60
  const PORT_ALLOCATION_LOCK_STALE_MS = 120_000;
60
61
 
61
- // npm installs land in the npm prefix, which is only the tree this process runs
62
- // from when the runtime was started from that prefix. When the service points at
63
- // another install (e.g. the CLI bundled inside the desktop app), the update can
64
- // never apply by restarting, so restarting would loop forever.
65
- function installedPackageVersionOnDisk() {
66
- try {
67
- const raw = fs.readFileSync(path.join(PACKAGE_ROOT_DIR, 'package.json'), 'utf8');
68
- return JSON.parse(raw).version || null;
69
- } catch {
70
- return null;
71
- }
72
- }
73
-
74
62
  const CHILD_RESTART_POLICY = {
75
63
  initialDelayMs: 1000,
76
64
  maxDelayMs: 30000,
@@ -838,6 +826,7 @@ function createAutoUpdateManager({ ports, runtimeToken, tunnels, foreground, req
838
826
  if (stopped || inFlight || installedUpdate) return;
839
827
  inFlight = true;
840
828
  const checkedAt = new Date().toISOString();
829
+ let check = null;
841
830
  try {
842
831
  writeAutoUpdateState({
843
832
  status: 'checking',
@@ -848,7 +837,7 @@ function createAutoUpdateManager({ ports, runtimeToken, tunnels, foreground, req
848
837
  last_checked_at: checkedAt,
849
838
  last_error: null,
850
839
  });
851
- const check = await checkForUpdate({ currentVersion: PACKAGE_VERSION });
840
+ check = await checkForUpdate({ currentVersion: PACKAGE_VERSION });
852
841
  if (!check.updateAvailable) {
853
842
  writeAutoUpdateState({
854
843
  status: 'current',
@@ -893,10 +882,13 @@ function createAutoUpdateManager({ ports, runtimeToken, tunnels, foreground, req
893
882
  if (trimmed) log(trimmed, stream);
894
883
  },
895
884
  });
896
- const diskVersion = installedPackageVersionOnDisk();
897
- if (diskVersion !== check.targetVersion) {
885
+ installedUpdate = true;
886
+ await waitForIdleAndRestart(check);
887
+ } catch (error) {
888
+ // Restarting cannot apply an update that landed outside the tree this
889
+ // process runs from, so restarts are suppressed to avoid a restart loop.
890
+ if (error?.code === UPDATE_TARGET_MISMATCH && check) {
898
891
  suppressedTarget = check.targetVersion;
899
- const reason = `installed ${check.targetVersion}, but this runtime still runs ${diskVersion || PACKAGE_VERSION} from ${PACKAGE_ROOT_DIR}; restart suppressed to avoid a restart loop`;
900
892
  writeAutoUpdateState({
901
893
  status: 'restart_suppressed',
902
894
  current_version: PACKAGE_VERSION,
@@ -905,14 +897,11 @@ function createAutoUpdateManager({ ports, runtimeToken, tunnels, foreground, req
905
897
  critical: isCriticalUpdate(check),
906
898
  service_managed: serviceManaged,
907
899
  last_checked_at: checkedAt,
908
- last_error: reason,
900
+ last_error: error.message,
909
901
  });
910
- log(reason, 'stderr');
902
+ log(error.message, 'stderr');
911
903
  return;
912
904
  }
913
- installedUpdate = true;
914
- await waitForIdleAndRestart(check);
915
- } catch (error) {
916
905
  writeAutoUpdateState({
917
906
  status: 'error',
918
907
  current_version: PACKAGE_VERSION,
package/lib/updater.js CHANGED
@@ -1,16 +1,19 @@
1
1
  'use strict';
2
2
 
3
3
  const { spawn } = require('child_process');
4
+ const fs = require('fs');
4
5
  const os = require('os');
5
6
  const path = require('path');
6
7
  const {
7
8
  AMALGM_RUNTIME_LABEL,
9
+ PACKAGE_ROOT,
8
10
  } = require('./paths');
9
11
  const {
10
12
  npmTagForRuntimeLabel,
11
13
  } = require('./runtime-identity');
12
14
 
13
15
  const PACKAGE_VERSION = require('../package.json').version;
16
+ const UPDATE_TARGET_MISMATCH = 'AMALGM_UPDATE_TARGET_MISMATCH';
14
17
  const DEFAULT_VIEW_TIMEOUT_MS = 15_000;
15
18
  const DEFAULT_INSTALL_TIMEOUT_MS = 10 * 60_000;
16
19
  const DEFAULT_NPM_UPDATE_CACHE = path.join(os.tmpdir(), 'amalgm-npm-cache');
@@ -64,6 +67,41 @@ function npmCommand() {
64
67
  return process.platform === 'win32' ? 'npm.cmd' : 'npm';
65
68
  }
66
69
 
70
+ // npm lays out global installs as <prefix>/lib/node_modules/<name> on POSIX and
71
+ // <prefix>/node_modules/<name> on Windows. Walking that layout backwards from
72
+ // the package root this process runs from recovers the prefix that owns the
73
+ // running install, so updates land in the tree the service actually boots —
74
+ // not whatever prefix the inherited environment points at (toolchain dirs on
75
+ // Fly machines, the desktop app bundle on Mac). Returns null when the root is
76
+ // not a global-install layout (e.g. a repo checkout).
77
+ function globalPrefixForPackageRoot(packageRoot = PACKAGE_ROOT) {
78
+ const root = path.resolve(packageRoot);
79
+ const nodeModules = path.dirname(root);
80
+ if (path.basename(nodeModules) !== 'node_modules') return null;
81
+ const parent = path.dirname(nodeModules);
82
+ return path.basename(parent) === 'lib' ? path.dirname(parent) : parent;
83
+ }
84
+
85
+ function packageVersionOnDisk(packageRoot = PACKAGE_ROOT) {
86
+ try {
87
+ const raw = fs.readFileSync(path.join(path.resolve(packageRoot), 'package.json'), 'utf8');
88
+ return JSON.parse(raw).version || null;
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+
94
+ function npmInstallArgs(tag, options = {}) {
95
+ const args = ['install', '-g', `amalgm@${tag}`];
96
+ const prefix = options.npmPrefix !== undefined
97
+ ? options.npmPrefix
98
+ : globalPrefixForPackageRoot(options.packageRoot);
99
+ // The --prefix flag outranks every npm config layer, including an inherited
100
+ // npm_config_prefix pointing at a tree this process does not run from.
101
+ if (prefix) args.push('--prefix', prefix);
102
+ return args;
103
+ }
104
+
67
105
  function npmInstallTag(options = {}) {
68
106
  const explicit = options.tag || options.channel;
69
107
  if (explicit && explicit !== true) return String(explicit).replace(/^@/, '');
@@ -157,7 +195,7 @@ async function npmPackageVersionForTag(tag, options = {}) {
157
195
  }
158
196
 
159
197
  async function installNpmPackageTag(tag, options = {}) {
160
- const result = await runNpm(['install', '-g', `amalgm@${tag}`], {
198
+ const result = await runNpm(npmInstallArgs(tag, options), {
161
199
  timeoutMs: options.timeoutMs || DEFAULT_INSTALL_TIMEOUT_MS,
162
200
  env: options.env,
163
201
  onOutput: options.onOutput,
@@ -187,6 +225,18 @@ async function updatePackage(options = {}) {
187
225
  const check = options.check || await checkForUpdate(options);
188
226
  if (!check.updateAvailable) return { ...check, updated: false };
189
227
  await installNpmPackageTag(check.tag, options);
228
+ const packageRoot = path.resolve(options.packageRoot || PACKAGE_ROOT);
229
+ const diskVersion = packageVersionOnDisk(packageRoot);
230
+ if (diskVersion !== check.targetVersion) {
231
+ const error = new Error(
232
+ `npm reported installing amalgm@${check.tag} (${check.targetVersion}), but the install this runtime boots from at ${packageRoot} still carries ${diskVersion || 'no readable version'}. The update landed in a different npm prefix and will never take effect here.`,
233
+ );
234
+ error.code = UPDATE_TARGET_MISMATCH;
235
+ error.targetVersion = check.targetVersion;
236
+ error.installedVersion = diskVersion;
237
+ error.packageRoot = packageRoot;
238
+ throw error;
239
+ }
190
240
  return { ...check, updated: true };
191
241
  }
192
242
 
@@ -211,14 +261,18 @@ module.exports = {
211
261
  _test: {
212
262
  npmCommand,
213
263
  npmEnv,
264
+ npmInstallArgs,
214
265
  npmSpawnOptions,
215
266
  },
267
+ UPDATE_TARGET_MISMATCH,
216
268
  checkForUpdate,
217
269
  compareVersions,
218
270
  envDisablesAutoUpdate,
271
+ globalPrefixForPackageRoot,
219
272
  installNpmPackageTag,
220
273
  npmInstallTag,
221
274
  npmPackageVersionForTag,
275
+ packageVersionOnDisk,
222
276
  parseSemver,
223
277
  shouldAutoUpdate,
224
278
  updatePackage,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.167",
3
+ "version": "0.1.168",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,