From 6b4b8f5e9925881b7c7deb0b6ebea8b4d4468081 Mon Sep 17 00:00:00 2001 From: Alex Machin Date: Mon, 30 Mar 2026 18:55:10 +0100 Subject: [PATCH] fix(intent): read local package.json version before npm registry fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #103 — fetchNpmVersion hardcodes registry.npmjs.org, which returns 404 for private/scoped packages (GitHub Packages, Artifactory, etc.), breaking version drift detection. Changes: - Add readLocalVersion() to read package.json from packageDir - Add fetchCurrentVersion() that tries local first, then npm registry - Update checkStaleness() to use the new fallback chain - Add test: private package with npm 404 → reads local version - Add test: local package.json takes precedence over npm registry --- .changeset/fix-local-version-fallback.md | 5 +++ packages/intent/src/staleness.ts | 24 +++++++++++-- packages/intent/tests/staleness.test.ts | 43 ++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 .changeset/fix-local-version-fallback.md diff --git a/.changeset/fix-local-version-fallback.md b/.changeset/fix-local-version-fallback.md new file mode 100644 index 0000000..32f0096 --- /dev/null +++ b/.changeset/fix-local-version-fallback.md @@ -0,0 +1,5 @@ +--- +'@tanstack/intent': patch +--- + +Read local package.json version before falling back to npm registry in `intent stale`. This fixes version drift detection for packages not published to public registry.npmjs.org (e.g. GitHub Packages, Artifactory, private registries). diff --git a/packages/intent/src/staleness.ts b/packages/intent/src/staleness.ts index 679c2ec..a5ff641 100644 --- a/packages/intent/src/staleness.ts +++ b/packages/intent/src/staleness.ts @@ -34,9 +34,20 @@ function classifyVersionDrift( } // --------------------------------------------------------------------------- -// npm version fetching +// Version resolution // --------------------------------------------------------------------------- +function readLocalVersion(packageDir: string): string | null { + try { + const pkgJson = JSON.parse( + readFileSync(join(packageDir, 'package.json'), 'utf8'), + ) as Record + return typeof pkgJson.version === 'string' ? pkgJson.version : null + } catch { + return null + } +} + async function fetchNpmVersion(packageName: string): Promise { try { const res = await fetch( @@ -50,6 +61,13 @@ async function fetchNpmVersion(packageName: string): Promise { } } +async function fetchCurrentVersion( + packageDir: string, + packageName: string, +): Promise { + return readLocalVersion(packageDir) ?? (await fetchNpmVersion(packageName)) +} + // --------------------------------------------------------------------------- // Sync state // --------------------------------------------------------------------------- @@ -141,8 +159,8 @@ export async function checkStaleness( const skillVersion = skillMetas.find((s) => s.libraryVersion)?.libraryVersion ?? null - // Fetch current npm version - const currentVersion = await fetchNpmVersion(library) + // Resolve current version: prefer local package.json, fall back to npm registry + const currentVersion = await fetchCurrentVersion(packageDir, library) // Classify drift const versionDrift = diff --git a/packages/intent/tests/staleness.test.ts b/packages/intent/tests/staleness.test.ts index 03d265a..1a44047 100644 --- a/packages/intent/tests/staleness.test.ts +++ b/packages/intent/tests/staleness.test.ts @@ -309,4 +309,47 @@ describe('checkStaleness', () => { expect(report.skills).toHaveLength(1) expect(requireFirstSkill(report).needsReview).toBe(false) }) + + it('reads version from local package.json when npm fetch fails', async () => { + writeFileSync( + join(tmpDir, 'package.json'), + JSON.stringify({ name: '@private/lib', version: '2.5.0' }), + ) + + writeSkill(tmpDir, 'core', { + name: 'core', + description: 'Core', + library_version: '2.0.0', + }) + + mockFetchNotOk() + + const report = await checkStaleness(tmpDir, '@private/lib') + expect(report.currentVersion).toBe('2.5.0') + expect(report.versionDrift).toBe('minor') + const skill = requireFirstSkill(report) + expect(skill.needsReview).toBe(true) + expect(skill.reasons[0]).toContain('version drift') + }) + + it('prefers local package.json over npm registry', async () => { + writeFileSync( + join(tmpDir, 'package.json'), + JSON.stringify({ name: '@example/lib', version: '3.0.0' }), + ) + + writeSkill(tmpDir, 'core', { + name: 'core', + description: 'Core', + library_version: '2.0.0', + }) + + // npm returns an older published version + mockFetchVersion('2.5.0') + + const report = await checkStaleness(tmpDir, '@example/lib') + // Local package.json should take precedence + expect(report.currentVersion).toBe('3.0.0') + expect(report.versionDrift).toBe('major') + }) })