From 35b2b2b525f5b8a920291f8c1d8f8258a4ce27d1 Mon Sep 17 00:00:00 2001 From: Fauzaan Date: Tue, 24 Mar 2026 16:26:01 +0530 Subject: [PATCH] feat: support offline mode with env var version pinning and cache fallback When HOLISTICS_CLI_CORE_VERSION is set, skip npm registry calls entirely and use the specified version from cache. When the env var is not set and npm is unreachable, fall back to the most recent cached version instead of crashing. Closes #15 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/downloader.ts | 42 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/src/downloader.ts b/src/downloader.ts index cd7703d..c843f62 100644 --- a/src/downloader.ts +++ b/src/downloader.ts @@ -1,8 +1,8 @@ -import { writeFile, mkdir, stat } from "fs/promises"; +import { writeFile, mkdir, stat, readdir } from "fs/promises"; import { fetch } from "undici"; import { extract } from "tar"; import { join } from "path"; -import { getModulePath, ensureCacheDir } from "./cache"; +import { getCacheDir, getModulePath, ensureCacheDir } from "./cache"; async function downloadAndExtract(pkg: string, version: string) { const modulePath = getModulePath(pkg, version); @@ -24,18 +24,48 @@ async function downloadAndExtract(pkg: string, version: string) { } async function getLatestVersion(pkg: string): Promise { - // console.log(`Fetching latest version of ${pkg}...`); const res = await fetch(`https://registry.npmjs.org/${pkg}`); if (!res.ok) throw new Error(`Failed to fetch package info: ${res.statusText}`); - + const data = await res.json(); const ver = data["dist-tags"]?.latest || ""; - // console.log(`Latest version is ${ver}`); return ver; } +async function findCachedVersion(pkg: string): Promise { + const cacheDir = getCacheDir(); + const prefix = `${pkg}@`; + try { + const entries = await readdir(cacheDir); + const matches = entries + .filter(e => e.startsWith(prefix)) + .map(e => e.slice(prefix.length)) + .sort() + .reverse(); + return matches.length > 0 ? matches[0] : null; + } catch { + return null; + } +} + export async function ensureModule(pkg: string, version?: string) { - if (!version) version = await getLatestVersion(pkg); + // Allow pinning the version via environment variable to skip npm registry calls entirely + if (!version) version = process.env.HOLISTICS_CLI_CORE_VERSION; + + if (!version) { + try { + version = await getLatestVersion(pkg); + } catch (err) { + // Offline fallback: if npm is unreachable, try to use a cached version + const cached = await findCachedVersion(pkg); + if (cached) { + console.log(`Network unavailable, using cached version ${pkg}@${cached}`); + return getModulePath(pkg, cached); + } + throw err; + } + } + const modulePath = getModulePath(pkg, version); try {