fix(cli): resolve update paths relative to script + recursive backup (#3295) (#3313)

omniroute update always failed on a global install:
- getCurrentVersion() read package.json from process.cwd(), which on a global
  npm/brew install is the user's working dir, not the package root → null →
  'Could not determine current version'.
- createBackup() resolved bin/ from cwd too, and passed the 'cli' directory to
  copyFileSync → EISDIR, swallowed by the catch → 'Failed to create backup'.

Both now resolve package.json/bin relative to the script via import.meta.url,
and the backup uses cpSync({recursive:true}) so the cli/ directory is copied.

Co-authored-by: uniQta <uniQta@users.noreply.github.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-06 13:13:37 -03:00
committed by GitHub
parent 0fcfb159f2
commit 7a3eeaca48
3 changed files with 82 additions and 6 deletions

View File

@@ -20,6 +20,7 @@ _Development cycle in progress — entries are added as work merges into `releas
### 🔧 Bug Fixes
- **fix(dashboard):** keep no-auth providers (opencode, duckduckgo-web, theoldllm, veoaifree-web) visible under the "Show configured only" filter — they never create a connection row (`stats.total === 0`) but are always usable and already appear in `/v1/models`, so the filter now treats `displayAuthType === "no-auth"` as configured. (#3290 — thanks @uniQta)
- **fix(cli):** `omniroute update` no longer always fails on a global install — `getCurrentVersion()` and `createBackup()` now resolve `package.json`/`bin` relative to the script (`import.meta.url`) instead of `process.cwd()` (the user's working dir on a global npm/brew install → *"Could not determine current version"*), and the backup copies the `cli` directory with `cpSync({recursive:true})` instead of `copyFileSync`, which threw a swallowed `EISDIR`*"Failed to create backup. Aborting"*. (#3295 — thanks @uniQta)
- **fix(sse):** harden the passthrough stream against empty upstream responses — emit a synthetic retry chunk on an empty `choices: []` (fixes a Copilot Chat crash) and log empty post-`tool_calls` completions; also registers **MiniMax M3** (1M context) across 8 provider tiers. ([#3297](https://github.com/diegosouzapw/OmniRoute/pull/3297), #3110 — thanks @wilsonicdev)
- **fix(opencode-provider):** extract `contextLength` from the live `/v1/models` catalog (live > `modelContextLengths` > static map) so passthrough models outside the legacy 8-model map no longer silently truncate to OpenCode's 128K default. ([#3298](https://github.com/diegosouzapw/OmniRoute/pull/3298) — thanks @herjarsa / @diegosouzapw)
- **fix(dev):** auto-rebuild `better-sqlite3` on a Node ABI mismatch at `npm run dev` startup (nvm 22↔24) — dev-only, no-op on the healthy path, unrelated errors not swallowed. ([#3301](https://github.com/diegosouzapw/OmniRoute/pull/3301) — thanks @zhiru)

View File

@@ -1,16 +1,24 @@
import { printHeading, printInfo, printSuccess, printError } from "../io.mjs";
import { homedir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { t } from "../i18n.mjs";
const execFileAsync = promisify(execFile);
async function getCurrentVersion() {
// This file lives at <pkgRoot>/bin/cli/commands/update.mjs — resolve package
// paths relative to the script, NOT process.cwd(). On a global npm/brew install
// the user's cwd is not the package root, so cwd-relative lookups break (#3295).
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const PKG_ROOT = path.resolve(SCRIPT_DIR, "..", "..", "..");
const BIN_DIR = path.join(PKG_ROOT, "bin");
export async function getCurrentVersion() {
try {
const { readFileSync } = await import("node:fs");
const pkg = JSON.parse(readFileSync(path.join(process.cwd(), "package.json"), "utf-8"));
const pkg = JSON.parse(readFileSync(path.join(PKG_ROOT, "package.json"), "utf-8"));
return pkg.version;
} catch {
return null;
@@ -38,12 +46,12 @@ function compareVersions(a, b) {
return 0;
}
async function createBackup() {
const binPath = path.join(process.cwd(), "bin");
export async function createBackup() {
const binPath = BIN_DIR;
const backupDir = path.join(homedir(), ".omniroute", "backups", `omniroute-${Date.now()}`);
try {
const { mkdirSync, copyFileSync, existsSync } = await import("node:fs");
const { mkdirSync, cpSync, existsSync } = await import("node:fs");
if (!existsSync(binPath)) return null;
mkdirSync(backupDir, { recursive: true });
@@ -51,7 +59,9 @@ async function createBackup() {
for (const f of files) {
const src = path.join(binPath, f);
if (existsSync(src)) {
copyFileSync(src, path.join(backupDir, f));
// cpSync handles both files and directories; the old copyFileSync threw
// EISDIR on the "cli" directory, which was swallowed by the catch (#3295).
cpSync(src, path.join(backupDir, f), { recursive: true });
}
}
return backupDir;

View File

@@ -0,0 +1,65 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync, mkdtempSync, mkdirSync, existsSync, statSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
const update = await import("../../bin/cli/commands/update.mjs");
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const REAL_VERSION = JSON.parse(
readFileSync(path.join(REPO_ROOT, "package.json"), "utf-8")
).version;
// #3295 issue 1: getCurrentVersion() must resolve package.json relative to the
// script, not process.cwd(). When OmniRoute is installed globally, the user's
// cwd is not the package root, so a cwd-relative lookup returns null →
// "Could not determine current version".
test("getCurrentVersion resolves the real version from a foreign cwd (#3295)", async () => {
const originalCwd = process.cwd();
const foreignCwd = mkdtempSync(path.join(tmpdir(), "omniroute-cwd-"));
try {
process.chdir(foreignCwd); // no package.json here → cwd-relative lookup would fail
const version = await update.getCurrentVersion();
assert.equal(version, REAL_VERSION);
} finally {
process.chdir(originalCwd);
rmSync(foreignCwd, { recursive: true, force: true });
}
});
// #3295 issue 2: createBackup() must (a) resolve bin/ relative to the script,
// and (b) copy the "cli" directory recursively. The old copyFileSync(dir) threw
// EISDIR which the outer catch swallowed → "Failed to create backup. Aborting".
test("createBackup resolves bin/ from a foreign cwd and copies cli/ recursively (#3295)", async () => {
const originalCwd = process.cwd();
const originalHome = process.env.HOME;
const foreignCwd = mkdtempSync(path.join(tmpdir(), "omniroute-cwd-"));
const fakeHome = mkdtempSync(path.join(tmpdir(), "omniroute-home-"));
try {
process.chdir(foreignCwd); // no bin/ here → cwd-relative binPath would be missing
process.env.HOME = fakeHome; // redirect ~/.omniroute/backups
mkdirSync(fakeHome, { recursive: true });
const backupDir = await update.createBackup();
assert.ok(backupDir, "createBackup must return a path (not null)");
// omniroute.mjs is a real file in bin/ and must be copied
assert.ok(existsSync(path.join(backupDir, "omniroute.mjs")), "omniroute.mjs copied");
// "cli" is a directory — it must be copied recursively, not throw EISDIR
const cliBackup = path.join(backupDir, "cli");
assert.ok(existsSync(cliBackup), "cli/ directory copied");
assert.ok(statSync(cliBackup).isDirectory(), "cli/ backup is a directory");
assert.ok(
existsSync(path.join(cliBackup, "commands")),
"cli/ contents copied recursively"
);
} finally {
process.chdir(originalCwd);
if (originalHome === undefined) delete process.env.HOME;
else process.env.HOME = originalHome;
rmSync(foreignCwd, { recursive: true, force: true });
rmSync(fakeHome, { recursive: true, force: true });
}
});