diff --git a/CHANGELOG.md b/CHANGELOG.md index fb79922..21993fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 18f83d1..5c316e7 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/RELEASE.md b/RELEASE.md index 55750d9..c540c59 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -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`: diff --git a/package-lock.json b/package-lock.json index 3dc428b..b8f47c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@matterailab/orbcode", - "version": "7.0.1", + "version": "7.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@matterailab/orbcode", - "version": "7.0.1", + "version": "7.0.5", "license": "MIT", "dependencies": { "@ai-sdk/anthropic": "^3.0.85", @@ -14,6 +14,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", @@ -39,8 +40,7 @@ "node": ">=20.0.0" }, "optionalDependencies": { - "@ff-labs/fff-node": "0.10.1", - "@vscode/ripgrep": "1.18.0" + "@ff-labs/fff-node": "0.10.1" } }, "node_modules/@ai-sdk/anthropic": { @@ -1407,7 +1407,6 @@ "resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.18.0.tgz", "integrity": "sha512-ns5lWe44tSfbTMbVUsyB+I1819PVSw4AdpgK0RNkzfWfwy6+3IUNSxwSrfTno1/oWaS/hERNz+XLWVyga2aJBQ==", "license": "MIT", - "optional": true, "optionalDependencies": { "@vscode/ripgrep-darwin-arm64": "1.18.0", "@vscode/ripgrep-darwin-x64": "1.18.0", diff --git a/package.json b/package.json index 20dbf7c..16787d8 100644 --- a/package.json +++ b/package.json @@ -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": { @@ -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", @@ -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", diff --git a/src/utils/autoUpdate.ts b/src/utils/autoUpdate.ts index 6f59f56..89416bc 100644 --- a/src/utils/autoUpdate.ts +++ b/src/utils/autoUpdate.ts @@ -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. */ @@ -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 { +/** + * 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 { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), REGISTRY_TIMEOUT_MS); try { @@ -163,14 +168,21 @@ async function fetchReleaseInfo(pkg: string, version: string): Promise; versions?: Record; }; - 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 { @@ -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; }