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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [7.0.5] - 2026-10-14

### Changed

- **Background updates install after 15 minutes on npm instead of 24 hours.** `MIN_RELEASE_AGE_MS` (`src/utils/autoUpdate.ts`) is lowered from 24h to 15 min, so staged updates reach users the same day a release ships while still keeping a short window to catch and supersede a bad release.

### Fixed

- **The updater no longer stalls on a bad or incompatible `latest` release.** Instead of inspecting only the registry's `latest` tag, `resolveUpdateNotice` now collects every non-deprecated, non-prerelease release newer than the running version that has been public for at least the age window, and stages the newest one this Node version can run (`engines.node`), that is newer than what is already staged, and that has not previously failed to start (`isBadVersion`). A broken or engine-incompatible release at `latest` no longer blocks updates; the next eligible release is picked instead.
- **`@vscode/ripgrep` is now a regular dependency in `package.json`.** The 7.0.3 fix updated the changelog and PATH handling but left the package in `optionalDependencies`, so installs that skip optional dependencies (e.g. `--no-optional`) still shipped without the bundled `rg` binary. It now installs unconditionally on all platforms.

## [7.0.4] - 2026-10-13

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ orbcode
```

Updates install in the background: a new release (once it has been public for
24h) is downloaded into `~/.orbcode/versions/`, verified, and used from your
15 min) is downloaded into `~/.orbcode/versions/`, verified, and used from your
next launch — the banner just says to restart. If a new version fails to start,
OrbCode falls back to the previous one. To opt out, set
`ORBCODE_DISABLE_AUTOUPDATE=1` or `"autoUpdates": false` in
Expand Down
2 changes: 1 addition & 1 deletion RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ bumping the version is all that's needed; no source change required.

Installed CLIs stage new releases under `~/.orbcode/versions/` and roll back to
the previous version if a release fails to start (see `src/utils/autoUpdate.ts`
and `bin/select-version.js`). Releases auto-install only after 24h on npm, so
and `bin/select-version.js`). Releases auto-install only after 15 minutes on npm, so
**ship a fix within a day to keep a bad release from spreading**, and keep these
invariants so old and new versions can run against the same `~/.orbcode`:

Expand Down
9 changes: 4 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@matterailab/orbcode",
"version": "7.0.4",
"version": "7.0.5",
"description": "OrbCode CLI — agentic coding in your terminal, by MatterAI",
"type": "module",
"bin": {
Expand Down Expand Up @@ -58,6 +58,7 @@
"@modelcontextprotocol/sdk": "^1.32.1",
"@opentui/core": "0.4.3",
"@opentui/react": "0.4.3",
"@vscode/ripgrep": "1.18.0",
"ai": "^6.0.207",
"bun": "1.3.14",
"mammoth": "^1.12.0",
Expand All @@ -67,8 +68,7 @@
"picomatch": "4.0.4",
"react": "^19.2.0",
"read-excel-file": "^9.2.0",
"string-width": "^7.2.0",
"@vscode/ripgrep": "1.18.0"
"string-width": "^7.2.0"
},
"devDependencies": {
"@types/node": "^20.17.0",
Expand Down
62 changes: 41 additions & 21 deletions src/utils/autoUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const execFileAsync = promisify(execFile);

/** Only auto-install releases that have been public this long, so a bad
* release can be caught (and superseded) before it reaches everyone. */
export const MIN_RELEASE_AGE_MS = 24 * 60 * 60 * 1_000;
export const MIN_RELEASE_AGE_MS = 15 * 60 * 1_000;
/** After a failed install, stay quiet (and show the manual banner) this long. */
const FAILURE_BACKOFF_MS = 24 * 60 * 60 * 1_000;
/** Old version dirs are removed once they have been superseded this long. */
Expand Down Expand Up @@ -145,12 +145,17 @@ export function satisfiesNodeEngine(range: string | undefined, nodeVersion: stri
}

interface ReleaseInfo {
publishedAt: number | null;
version: string;
publishedAt: number;
nodeEngine?: string;
deprecated: boolean;
}

async function fetchReleaseInfo(pkg: string, version: string): Promise<ReleaseInfo | null> {
/**
* Every non-deprecated release newer than `current` that has been public for
* at least MIN_RELEASE_AGE_MS, newest first. Null when the registry can't be
* read.
*/
async function fetchEligibleReleases(pkg: string, current: string): Promise<ReleaseInfo[] | null> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), REGISTRY_TIMEOUT_MS);
try {
Expand All @@ -163,14 +168,21 @@ async function fetchReleaseInfo(pkg: string, version: string): Promise<ReleaseIn
time?: Record<string, string>;
versions?: Record<string, { engines?: { node?: unknown }; deprecated?: unknown }>;
};
const manifest = doc.versions?.[version];
if (!manifest) return null;
const published = Date.parse(doc.time?.[version] ?? "");
return {
publishedAt: Number.isFinite(published) ? published : null,
nodeEngine: typeof manifest.engines?.node === "string" ? manifest.engines.node : undefined,
deprecated: manifest.deprecated !== undefined && manifest.deprecated !== false,
};
const releases: ReleaseInfo[] = [];
for (const [version, manifest] of Object.entries(doc.versions ?? {})) {
// Registry data becomes a path and an npm argument later on.
if (!isVersionString(version) || version.includes("-")) continue;
if (compareVersions(version, current) <= 0) continue;
if (manifest.deprecated !== undefined && manifest.deprecated !== false) continue;
const published = Date.parse(doc.time?.[version] ?? "");
if (!Number.isFinite(published) || Date.now() - published < MIN_RELEASE_AGE_MS) continue;
releases.push({
version,
publishedAt: published,
nodeEngine: typeof manifest.engines?.node === "string" ? manifest.engines.node : undefined,
});
}
return releases.sort((a, b) => compareVersions(b.version, a.version));
} catch {
return null;
} finally {
Expand Down Expand Up @@ -473,28 +485,36 @@ export async function resolveUpdateNotice(
: null;
const latest = info.latest;
if (!info.updateAvailable || !latest) return stagedNotice ?? info;
// Registry data becomes a path and an npm argument below.
if (!isVersionString(latest)) return stagedNotice ?? quiet;
if (staged && compareVersions(staged, latest) >= 0) return stagedNotice ?? info;
if (readRecentFailure()) return info;

const release = await fetchReleaseInfo(pkg, latest);
// Registry hiccup or a pulled release: stay quiet and retry next launch.
if (!release || release.deprecated || release.publishedAt === null) return stagedNotice ?? quiet;
if (Date.now() - release.publishedAt < MIN_RELEASE_AGE_MS) return stagedNotice ?? quiet;
// Registry hiccup: stay quiet and retry next launch.
const releases = await fetchEligibleReleases(pkg, info.current);
if (!releases) return stagedNotice ?? quiet;

const nodePath = getNodePath()!;
const nodeVersion = await getNodeVersion(nodePath);
if (!nodeVersion || !satisfiesNodeEngine(release.nodeEngine, nodeVersion)) return info;
if (!nodeVersion) return info;
// Newest release past the age window that this Node can run and that
// isn't newer-than-nothing compared with what's already staged.
const target = releases.find(
(r) =>
satisfiesNodeEngine(r.nodeEngine, nodeVersion) &&
(!staged || compareVersions(r.version, staged) > 0) &&
!isBadVersion(r.version),
);
if (!target) return stagedNotice ?? quiet;
const version = target.version;

const error = await stageVersion(pkg, latest, nodePath);
const error = await stageVersion(pkg, version, nodePath);
if (error === "locked") return stagedNotice ?? quiet;
if (error) {
writeFailure(latest, error);
writeFailure(version, error);
return info;
}
clearFailure();
return { ...quiet, stagedVersion: latest };
return { ...quiet, stagedVersion: version };
} catch {
return info;
}
Expand Down
Loading