Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion apps/cli/src/commands/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,18 @@ function renderSelfUpdate(info: SelfUpdateInfo): void {
} else if (info.installFailed) {
console.log(`${warn()} self-update failed — try: ${bold(manual)}`);
} else if (info.updated) {
// Report the version that actually landed on disk, not the requested target.
const to = info.installedVersion ?? info.latestInMajor;
console.log(`${check()} Updated mx to ${bold(`v${to}`)} ${dim(`(was v${info.current})`)}`);
} else if (info.staleRegistry) {
// A newer version was advertised but the install couldn't fetch it — almost
// always a lagging registry mirror. Say so and point at a direct install.
const got = info.installedVersion ?? info.current;
console.log(
`${check()} Updated mx to ${bold(`v${info.latestInMajor}`)} ${dim(`(was v${info.current})`)}`,
`${warn()} Registry advertises ${bold(`v${info.latestInMajor}`)} but installed ${bold(`v${got}`)} — your npm registry may be lagging.`,
);
console.log(
` ${dim('Install it directly:')} ${bold(`npm i -g ${info.package}@${info.latestInMajor} --registry https://registry.npmjs.org`)}`,
);
} else {
console.log(`${check()} mx is up to date ${dim(`(v${info.current}, latest in v${curMajor}.x)`)}`);
Expand Down
62 changes: 59 additions & 3 deletions apps/cli/src/selfupdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,34 @@ function latestOverall(): string | null {
return typeof v === 'string' ? v : null;
}

/**
* The version of `@rousan/mx` **actually installed** in the global prefix right
* now, read from `npm ls -g` (not the registry). This is the ground truth after
* an install: `npm i` and `npm view` can disagree when a registry advertises a
* version whose tarball it can't yet serve (a lagging mirror / proxy), so we must
* re-read what really landed rather than trust the requested target.
*
* `npm ls -g` can exit non-zero for unrelated reasons (peer warnings, extraneous
* packages) while still printing valid JSON, so we parse stdout regardless of
* status.
*
* @returns The installed version string, or null if it can't be determined.
*/
function installedGlobalVersion(): string | null {
const r = spawnSync('npm', ['ls', '-g', PKG, '--json', '--depth=0'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
});
if (!r.stdout) return null;
try {
const parsed = JSON.parse(r.stdout) as { dependencies?: Record<string, { version?: unknown }> };
const v = parsed.dependencies?.[PKG]?.version;
return typeof v === 'string' ? v : null;
} catch {
return null;
}
}

/**
* Outcome of a self-update attempt, surfaced to the CLI renderer / porcelain.
*/
Expand All @@ -82,10 +110,23 @@ export interface SelfUpdateInfo {
npmAvailable: boolean;
/** Newest version within the current major, or null if unknown. */
latestInMajor: string | null;
/** True when an install ran and succeeded. */
/**
* The version actually installed in the global prefix after the install ran
* (read from `npm ls -g`, not the requested target), or null if no install ran
* or it couldn't be determined.
*/
installedVersion: string | null;
/** True when an install ran and a genuinely newer version actually landed. */
updated: boolean;
/** True when an install was attempted but failed. */
installFailed: boolean;
/**
* True when a newer in-major version was advertised and the install exited 0,
* but no newer version actually landed — the registry advertised a version it
* couldn't serve (typically a lagging mirror / virtual-repo proxy). Distinct
* from `updated` (real upgrade) and `installFailed` (npm errored).
*/
staleRegistry: boolean;
/** A newer major version number available beyond the current one, or null. */
newMajor: number | null;
}
Expand All @@ -110,8 +151,10 @@ export function selfUpdate(porcelain: boolean): SelfUpdateInfo {
current,
npmAvailable: false,
latestInMajor: null,
installedVersion: null,
updated: false,
installFailed: false,
staleRegistry: false,
newMajor: null,
};

Expand All @@ -130,8 +173,21 @@ export function selfUpdate(porcelain: boolean): SelfUpdateInfo {
const r = spawnSync('npm', ['i', '-g', `${PKG}@^${curMajor}`], {
stdio: porcelain ? ['ignore', 'pipe', 'pipe'] : 'inherit',
});
if (r.status === 0) info.updated = true;
else info.installFailed = true;
if (r.status === 0) {
// npm exited 0, but that alone doesn't prove the target landed — re-read
// the version actually installed. Only a genuinely newer version counts as
// an update; if the same (or older) version came back despite a newer one
// being advertised, the registry served something it couldn't fully deliver
// (a lagging mirror), which we flag separately instead of claiming success.
info.installedVersion = installedGlobalVersion();
if (info.installedVersion && compareVersions(info.installedVersion, current) > 0) {
info.updated = true;
} else {
info.staleRegistry = true;
}
} else {
info.installFailed = true;
}
}
return info;
}
2 changes: 2 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ A newer major is available: @rousan/mx@3.

Crossing a major is always a deliberate user action — `mx update` never does it automatically (a new major implies a runtime migration). `mx update` is **not** subject to the version gate: you may need it precisely because your runtime is a newer major than the CLI on `$PATH`. If `npm` is missing or the install fails, it prints the manual command for you to run instead.

**It reports the version that actually installed** — read back from `npm ls -g`, not the target it requested. This matters behind a lagging registry mirror or virtual-repo proxy, which can *advertise* a version (via `npm view`) whose tarball it can't yet serve: `npm i` then reinstalls the same version while still exiting 0. In that case `mx update` says the registry advertises a newer version but a newer one didn't install, and points you at a direct install (`npm i -g @rousan/mx@<v> --registry https://registry.npmjs.org`) — it does **not** falsely claim the upgrade, and the auto-sync only runs when a genuinely newer version landed.

**Auto-sync after update.** When an in-major update actually installs a newer version, `mx update` then runs **`mx sync`** automatically so the runtime picks up the new version's templates and scaffolding (the runtime `CLAUDE.md`, shipped `bin/` utilities, per-work/per-repo files). It does this by shelling out to the freshly-installed global `mx` — the running process is still the pre-update code, so an in-process sync would stamp the *old* templates. Since the update stays in-major, the runtime version still matches and sync isn't gated. It's best-effort: if the sync can't run (e.g. no runtime at the resolved path) `mx update` prints a hint rather than failing. In `--porcelain` mode the sync runs silently so the update's JSON stays the only object on stdout. (Nothing happens when you're already on the latest in-major version.)

### `mx migrate [--dry-run]`
Expand Down
2 changes: 1 addition & 1 deletion npm/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rousan/mx",
"version": "4.7.0",
"version": "4.7.1",
"description": "mx — run several features in parallel across shared repos using git worktrees",
"type": "module",
"bin": {
Expand Down
Loading