From 7c0fb556c05f3d4576dc401ef7cb8da466397274 Mon Sep 17 00:00:00 2001 From: Frederik Bosch <6979916+fbosch@users.noreply.github.com> Date: Tue, 3 Feb 2026 09:17:59 +0100 Subject: [PATCH 1/4] refactor(lock): rename lock file to docs-lock.json --- .gitignore | 2 +- AGENTS.md | 2 +- README.md | 2 +- scripts/benchmarks/run.mjs | 3 ++- src/api.ts | 1 + src/lock.ts | 2 +- src/paths.ts | 1 - src/status.ts | 4 ++-- tests/edge-cases.test.js | 10 +++++----- tests/fixtures/docs.lock | 17 ----------------- tests/fixtures/empty.docs-lock.json | 6 ++++++ tests/fixtures/empty.docs.lock | 6 ------ tests/integration-real-repos.test.js | 12 +++++++++--- tests/lock.test.js | 5 ++++- tests/sync-materialize.test.js | 9 ++++++--- tests/sync-offline-fail.test.js | 6 +++--- tests/sync-output.test.js | 4 ++-- tests/sync-tool-version.test.js | 7 +++++-- 18 files changed, 49 insertions(+), 50 deletions(-) delete mode 100644 tests/fixtures/docs.lock create mode 100644 tests/fixtures/empty.docs-lock.json delete mode 100644 tests/fixtures/empty.docs.lock diff --git a/.gitignore b/.gitignore index c2da450..ca914b1 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ pnpm-debug.log* npm-debug.log* yarn-debug.log* docs.config.json -docs.lock +docs-lock.json coverage TODO.md .docs/ diff --git a/AGENTS.md b/AGENTS.md index b89c1ef..bd6202c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,7 @@ pnpm. ## Cache layout - Materialized sources live at `.docs//`. -- Lock file lives next to `docs.config.json` as `docs.lock`. +- Lock file lives next to `docs.config.json` as `docs-lock.json`. ## CLI architecture diff --git a/README.md b/README.md index 9f8aa63..8d9e13d 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Documentation is cached in a gitignored location, exposed to agent and tool targ ## Features - **Local only**: Cache lives in the directory `.docs` (or a custom location) and _should_ be gitignored. -- **Deterministic**: `docs.lock` pins commits and file metadata. +- **Deterministic**: `docs-lock.json` pins commits and file metadata. - **Fast**: Local cache avoids network roundtrips after sync. - **Flexible**: Cache full repos or just the subdirectories you need. diff --git a/scripts/benchmarks/run.mjs b/scripts/benchmarks/run.mjs index 55b626c..55bdde4 100644 --- a/scripts/benchmarks/run.mjs +++ b/scripts/benchmarks/run.mjs @@ -18,6 +18,7 @@ import { verifyCache, } from "../../dist/api.mjs"; import { + DEFAULT_LOCK_FILENAME, readLock, resolveLockPath, validateLock, @@ -286,7 +287,7 @@ const main = async () => { () => { const tempLock = path.join( benchRoot, - `docs.lock.${Math.random().toString(36).slice(2)}`, + `${DEFAULT_LOCK_FILENAME}.${Math.random().toString(36).slice(2)}`, ); return writeLock(tempLock, lockData); }, diff --git a/src/api.ts b/src/api.ts index 505780a..a1748dd 100644 --- a/src/api.ts +++ b/src/api.ts @@ -5,6 +5,7 @@ export { loadConfig } from "./config"; export { redactRepoUrl } from "./git/redact"; export { enforceHostAllowlist, parseLsRemote } from "./git/resolve-remote"; export { initConfig } from "./init"; +export { DEFAULT_LOCK_FILENAME } from "./lock"; export { pruneCache } from "./prune"; export { removeSources } from "./remove"; export { resolveRepoInput } from "./resolve-repo"; diff --git a/src/lock.ts b/src/lock.ts index bfc09e5..4e5f262 100644 --- a/src/lock.ts +++ b/src/lock.ts @@ -19,7 +19,7 @@ export interface DocsCacheLock { sources: Record; } -export const DEFAULT_LOCK_FILENAME = "docs.lock"; +export const DEFAULT_LOCK_FILENAME = "docs-lock.json"; const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null && !Array.isArray(value); diff --git a/src/paths.ts b/src/paths.ts index ee4781f..f10c8be 100644 --- a/src/paths.ts +++ b/src/paths.ts @@ -1,6 +1,5 @@ import path from "node:path"; -export const DEFAULT_LOCK_FILENAME = "docs.lock"; export const DEFAULT_TOC_FILENAME = "TOC.md"; export const toPosixPath = (value: string) => value.replace(/\\/g, "/"); diff --git a/src/status.ts b/src/status.ts index 56dbef7..48113d2 100644 --- a/src/status.ts +++ b/src/status.ts @@ -2,7 +2,7 @@ import { access } from "node:fs/promises"; import pc from "picocolors"; import { symbols, ui } from "./cli/ui"; import { DEFAULT_CACHE_DIR, loadConfig } from "./config"; -import { readLock, resolveLockPath } from "./lock"; +import { DEFAULT_LOCK_FILENAME, readLock, resolveLockPath } from "./lock"; import { getCacheLayout, resolveCacheDir } from "./paths"; type StatusOptions = { @@ -81,7 +81,7 @@ export const printStatus = (status: Awaited>) => { : pc.yellow("missing"); ui.header("Cache", `${relCache} (${cacheState})`); - ui.header("Lock", `docs.lock (${lockState})`); + ui.header("Lock", `${DEFAULT_LOCK_FILENAME} (${lockState})`); if (status.sources.length === 0) { ui.line(); diff --git a/tests/edge-cases.test.js b/tests/edge-cases.test.js index 566f9db..eddf261 100644 --- a/tests/edge-cases.test.js +++ b/tests/edge-cases.test.js @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { test } from "node:test"; -import { loadConfig } from "../dist/api.mjs"; +import { DEFAULT_LOCK_FILENAME, loadConfig } from "../dist/api.mjs"; const writeConfig = async (data) => { const tmpRoot = path.join( @@ -221,7 +221,7 @@ test("lock file with invalid version", async () => { `docs-cache-lock-ver-${Date.now().toString(36)}`, ); await mkdir(tmpRoot, { recursive: true }); - const lockPath = path.join(tmpRoot, "docs.lock"); + const lockPath = path.join(tmpRoot, DEFAULT_LOCK_FILENAME); const invalidLock = { version: 2, @@ -248,7 +248,7 @@ test("lock file with missing required fields", async () => { `docs-cache-lock-miss-${Date.now().toString(36)}`, ); await mkdir(tmpRoot, { recursive: true }); - const lockPath = path.join(tmpRoot, "docs.lock"); + const lockPath = path.join(tmpRoot, DEFAULT_LOCK_FILENAME); const invalidLock = { version: 1, @@ -271,7 +271,7 @@ test("lock file with negative bytes", async () => { `docs-cache-lock-neg-${Date.now().toString(36)}`, ); await mkdir(tmpRoot, { recursive: true }); - const lockPath = path.join(tmpRoot, "docs.lock"); + const lockPath = path.join(tmpRoot, DEFAULT_LOCK_FILENAME); const invalidLock = { version: 1, @@ -306,7 +306,7 @@ test("lock file with corrupted JSON", async () => { `docs-cache-lock-corrupt-${Date.now().toString(36)}`, ); await mkdir(tmpRoot, { recursive: true }); - const lockPath = path.join(tmpRoot, "docs.lock"); + const lockPath = path.join(tmpRoot, DEFAULT_LOCK_FILENAME); await writeFile(lockPath, '{"version": 1, invalid', "utf8"); diff --git a/tests/fixtures/docs.lock b/tests/fixtures/docs.lock deleted file mode 100644 index 450f907..0000000 --- a/tests/fixtures/docs.lock +++ /dev/null @@ -1,17 +0,0 @@ -{ - "version": 1, - "generatedAt": "2026-01-30T12:00:00+01:00", - "toolVersion": "0.1.0", - "sources": { - "vitest": { - "repo": "https://github.com/vitest-dev/vitest.git", - "ref": "main", - "resolvedCommit": "0123456789abcdef0123456789abcdef01234567", - "bytes": 123456, - "fileCount": 512, - "manifestSha256": "abcd", - "rulesSha256": "efgh", - "updatedAt": "2026-01-30T12:00:00+01:00" - } - } -} diff --git a/tests/fixtures/empty.docs-lock.json b/tests/fixtures/empty.docs-lock.json new file mode 100644 index 0000000..e2c5d66 --- /dev/null +++ b/tests/fixtures/empty.docs-lock.json @@ -0,0 +1,6 @@ +{ + "version": 1, + "generatedAt": "2026-01-30T12:00:00+01:00", + "toolVersion": "0.1.0", + "sources": {} +} diff --git a/tests/fixtures/empty.docs.lock b/tests/fixtures/empty.docs.lock deleted file mode 100644 index 45ce5f8..0000000 --- a/tests/fixtures/empty.docs.lock +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": 1, - "generatedAt": "2026-01-30T12:00:00+01:00", - "toolVersion": "0.1.0", - "sources": {} -} diff --git a/tests/integration-real-repos.test.js b/tests/integration-real-repos.test.js index be04b28..1a1633a 100644 --- a/tests/integration-real-repos.test.js +++ b/tests/integration-real-repos.test.js @@ -5,7 +5,7 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { test } from "node:test"; -import { runSync } from "../dist/api.mjs"; +import { DEFAULT_LOCK_FILENAME, runSync } from "../dist/api.mjs"; const shouldRun = () => process.env.DOCS_CACHE_INTEGRATION === "1"; @@ -48,7 +48,10 @@ test("integration syncs a real repository", async (t) => { offline: false, failOnMiss: false, }); - const lockRaw = await readFile(path.join(tmpRoot, "docs.lock"), "utf8"); + const lockRaw = await readFile( + path.join(tmpRoot, DEFAULT_LOCK_FILENAME), + "utf8", + ); const lock = JSON.parse(lockRaw); assert.ok(lock.sources.gitignore); } finally { @@ -103,7 +106,10 @@ test("integration clears partial clone cache before sync", async (t) => { offline: false, failOnMiss: false, }); - const lockRaw = await readFile(path.join(tmpRoot, "docs.lock"), "utf8"); + const lockRaw = await readFile( + path.join(tmpRoot, DEFAULT_LOCK_FILENAME), + "utf8", + ); const lock = JSON.parse(lockRaw); assert.ok(lock.sources.gitignore); const configRaw = await readFile( diff --git a/tests/lock.test.js b/tests/lock.test.js index b30dc50..fff11b1 100644 --- a/tests/lock.test.js +++ b/tests/lock.test.js @@ -4,7 +4,6 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { test } from "node:test"; -const fixturePath = new URL("./fixtures/docs.lock", import.meta.url); const distPath = new URL("../dist/lock.mjs", import.meta.url); const loadLockModule = async () => { @@ -22,6 +21,10 @@ test("lock fixture is valid", async (t) => { t.skip("lock module not built yet"); return; } + const fixturePath = new URL( + `./fixtures/${module.DEFAULT_LOCK_FILENAME}`, + import.meta.url, + ); const raw = await readFile(fixturePath, "utf8"); const parsed = JSON.parse(raw.toString()); const lock = module.validateLock(parsed); diff --git a/tests/sync-materialize.test.js b/tests/sync-materialize.test.js index 6089dcc..546def3 100644 --- a/tests/sync-materialize.test.js +++ b/tests/sync-materialize.test.js @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { test } from "node:test"; -import { runSync } from "../dist/api.mjs"; +import { DEFAULT_LOCK_FILENAME, runSync } from "../dist/api.mjs"; const exists = async (target) => { try { @@ -73,7 +73,10 @@ test("sync materializes via mocked fetch", async () => { ); assert.equal(await exists(path.join(cacheDir, "local")), true); - const lockRaw = await readFile(path.join(tmpRoot, "docs.lock"), "utf8"); + const lockRaw = await readFile( + path.join(tmpRoot, DEFAULT_LOCK_FILENAME), + "utf8", + ); const lock = JSON.parse(lockRaw); assert.equal(lock.sources.local.resolvedCommit, "abc123"); assert.equal(lock.sources.local.fileCount, 1); @@ -105,7 +108,7 @@ test("sync re-materializes when docs missing even if commit unchanged", async () await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8"); await writeFile( - path.join(tmpRoot, "docs.lock"), + path.join(tmpRoot, DEFAULT_LOCK_FILENAME), JSON.stringify({ version: 1, generatedAt: new Date().toISOString(), diff --git a/tests/sync-offline-fail.test.js b/tests/sync-offline-fail.test.js index df39ad6..d662ce7 100644 --- a/tests/sync-offline-fail.test.js +++ b/tests/sync-offline-fail.test.js @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { test } from "node:test"; -import { runSync } from "../dist/api.mjs"; +import { DEFAULT_LOCK_FILENAME, runSync } from "../dist/api.mjs"; test("sync fails on missing required sources when failOnMiss true", async () => { const tmpRoot = path.join( @@ -58,7 +58,7 @@ test("sync offline uses lock entries without resolving remotes", async () => { await mkdir(tmpRoot, { recursive: true }); const cacheDir = path.join(tmpRoot, ".docs"); const configPath = path.join(tmpRoot, "docs.config.json"); - const lockPath = path.join(tmpRoot, "docs.lock"); + const lockPath = path.join(tmpRoot, DEFAULT_LOCK_FILENAME); const config = { $schema: @@ -128,7 +128,7 @@ test("sync offline fails when lock exists but cache missing", async () => { await mkdir(tmpRoot, { recursive: true }); const cacheDir = path.join(tmpRoot, ".docs"); const configPath = path.join(tmpRoot, "docs.config.json"); - const lockPath = path.join(tmpRoot, "docs.lock"); + const lockPath = path.join(tmpRoot, DEFAULT_LOCK_FILENAME); const config = { $schema: diff --git a/tests/sync-output.test.js b/tests/sync-output.test.js index b3886e3..9c7987d 100644 --- a/tests/sync-output.test.js +++ b/tests/sync-output.test.js @@ -2,14 +2,14 @@ import assert from "node:assert/strict"; import path from "node:path"; import { test } from "node:test"; -import { printSyncPlan } from "../dist/api.mjs"; +import { DEFAULT_LOCK_FILENAME, printSyncPlan } from "../dist/api.mjs"; test("printSyncPlan outputs summary and short hashes", () => { const cwd = process.cwd(); const plan = { configPath: path.join(cwd, "docs.config.json"), cacheDir: path.join(cwd, ".docs"), - lockPath: path.join(cwd, "docs.lock"), + lockPath: path.join(cwd, DEFAULT_LOCK_FILENAME), lockExists: true, results: [ { diff --git a/tests/sync-tool-version.test.js b/tests/sync-tool-version.test.js index 5047f5f..3ab6ff0 100644 --- a/tests/sync-tool-version.test.js +++ b/tests/sync-tool-version.test.js @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { test } from "node:test"; -import { runSync } from "../dist/api.mjs"; +import { DEFAULT_LOCK_FILENAME, runSync } from "../dist/api.mjs"; test("sync writes lock toolVersion from package.json", async () => { const tmpRoot = path.join( @@ -43,7 +43,10 @@ test("sync writes lock toolVersion from package.json", async () => { failOnMiss: false, }); - const lockRaw = await readFile(path.join(tmpRoot, "docs.lock"), "utf8"); + const lockRaw = await readFile( + path.join(tmpRoot, DEFAULT_LOCK_FILENAME), + "utf8", + ); const lock = JSON.parse(lockRaw); const pkgRaw = await readFile( path.resolve(process.cwd(), "package.json"), From 2aea48e5565e4036f98819753aaa3596d0666d75 Mon Sep 17 00:00:00 2001 From: Frederik Bosch <6979916+fbosch@users.noreply.github.com> Date: Tue, 3 Feb 2026 09:46:18 +0100 Subject: [PATCH 2/4] fix: missing lock fixture --- .gitignore | 2 +- tests/fixtures/docs-lock.json | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/docs-lock.json diff --git a/.gitignore b/.gitignore index ca914b1..4b2795d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ pnpm-debug.log* npm-debug.log* yarn-debug.log* docs.config.json -docs-lock.json +/docs-lock.json coverage TODO.md .docs/ diff --git a/tests/fixtures/docs-lock.json b/tests/fixtures/docs-lock.json new file mode 100644 index 0000000..c45932c --- /dev/null +++ b/tests/fixtures/docs-lock.json @@ -0,0 +1,17 @@ +{ + "version": 1, + "generatedAt": "2026-01-30T12:00:00+01:00", + "toolVersion": "0.1.0", + "sources": { + "vitest": { + "repo": "https://github.com/vitest-dev/vitest.git", + "ref": "main", + "resolvedCommit": "0123456789abcdef0123456789abcdef01234567", + "bytes": 123456, + "fileCount": 512, + "manifestSha256": "abcd", + "rulesSha256": "efgh", + "updatedAt": "2026-01-30T12:00:00+01:00" + } + } +} From ada6de03d48432c1b369efc7bf6dfbfa2416d36a Mon Sep 17 00:00:00 2001 From: Frederik Bosch <6979916+fbosch@users.noreply.github.com> Date: Tue, 3 Feb 2026 10:02:52 +0100 Subject: [PATCH 3/4] fix(git): add Path and PATHEXT to env --- src/git/fetch-source.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/git/fetch-source.ts b/src/git/fetch-source.ts index 70ccb71..97d8306 100644 --- a/src/git/fetch-source.ts +++ b/src/git/fetch-source.ts @@ -45,6 +45,8 @@ const git = async ( maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large repos env: { PATH: process.env.PATH, + Path: process.env.Path, + PATHEXT: process.env.PATHEXT, HOME: process.env.HOME, USER: process.env.USER, USERPROFILE: process.env.USERPROFILE, From 5832a1aa46d426f13d1d8291a931632cd6c75c8b Mon Sep 17 00:00:00 2001 From: Frederik Bosch <6979916+fbosch@users.noreply.github.com> Date: Tue, 3 Feb 2026 10:24:12 +0100 Subject: [PATCH 4/4] refactor(git): replace execFileAsync with execa --- package.json | 1 + pnpm-lock.yaml | 121 ++++++++++++++++++++++++++++++++++++++++ src/git/fetch-source.ts | 15 +++-- 3 files changed, 133 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 510ca23..81a81a6 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "dependencies": { "@clack/prompts": "^1.0.0", "cac": "^6.7.14", + "execa": "^9.6.1", "fast-glob": "^3.3.2", "picocolors": "^1.1.1", "picomatch": "^2.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e08191..81da0e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: cac: specifier: ^6.7.14 version: 6.7.14 + execa: + specifier: ^9.6.1 + version: 9.6.1 fast-glob: specifier: ^3.3.2 version: 3.3.3 @@ -506,6 +509,13 @@ packages: cpu: [x64] os: [win32] + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + '@size-limit/file@11.2.0': resolution: {integrity: sha512-OZHE3putEkQ/fgzz3Tp/0hSmfVo3wyTpOJSRNm6AmcwX4Nm9YtTfbQQ/hZRwbBFR23S7x2Sd9EbqYzngKwbRoA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -802,6 +812,10 @@ packages: eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + execa@9.6.1: + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} + engines: {node: ^18.19.0 || >=20.5.0} + exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} @@ -821,6 +835,10 @@ packages: picomatch: optional: true + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -855,6 +873,10 @@ packages: resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + giget@2.0.0: resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} hasBin: true @@ -881,6 +903,10 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} @@ -908,9 +934,21 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1051,6 +1089,10 @@ packages: node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -1080,6 +1122,10 @@ packages: package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1088,6 +1134,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -1306,6 +1356,10 @@ packages: resolution: {integrity: sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==} engines: {node: '>=20'} + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -1427,6 +1481,10 @@ packages: resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + stylehacks@7.0.7: resolution: {integrity: sha512-bJkD0JkEtbRrMFtwgpJyBbFIwfDDONQ1Ov3sDLZQP8HuJ73kBOyx66H4bOcAbVWmnfLdvQ0AJwXxOMkpujcO6g==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} @@ -1486,6 +1544,10 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + untyped@2.0.0: resolution: {integrity: sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g==} hasBin: true @@ -1541,6 +1603,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} @@ -1839,6 +1905,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.57.1': optional: true + '@sec-ant/readable-stream@0.4.1': {} + + '@sindresorhus/merge-streams@4.0.0': {} + '@size-limit/file@11.2.0(size-limit@11.2.0)': dependencies: size-limit: 11.2.0 @@ -2170,6 +2240,21 @@ snapshots: eventemitter3@5.0.4: {} + execa@9.6.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + exsolve@1.0.8: {} fast-glob@3.3.3: @@ -2188,6 +2273,10 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -2219,6 +2308,11 @@ snapshots: get-east-asian-width@1.4.0: {} + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + giget@2.0.0: dependencies: citty: 0.1.6 @@ -2251,6 +2345,8 @@ snapshots: html-escaper@2.0.2: {} + human-signals@8.0.1: {} + is-core-module@2.16.1: dependencies: hasown: 2.0.2 @@ -2271,10 +2367,16 @@ snapshots: is-number@7.0.0: {} + is-plain-obj@4.1.0: {} + is-reference@1.2.1: dependencies: '@types/estree': 1.0.8 + is-stream@4.0.1: {} + + is-unicode-supported@2.1.0: {} + isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -2410,6 +2512,11 @@ snapshots: node-releases@2.0.27: {} + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + nth-check@2.1.1: dependencies: boolbase: 1.0.0 @@ -2438,10 +2545,14 @@ snapshots: package-manager-detector@1.6.0: {} + parse-ms@4.0.0: {} + path-exists@4.0.0: {} path-key@3.1.1: {} + path-key@4.0.0: {} + path-parse@1.0.7: {} path-scurry@1.11.1: @@ -2642,6 +2753,10 @@ snapshots: pretty-bytes@7.1.0: {} + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + queue-microtask@1.2.3: {} rc9@2.1.2: @@ -2781,6 +2896,8 @@ snapshots: dependencies: ansi-regex: 6.2.2 + strip-final-newline@4.0.0: {} + stylehacks@7.0.7(postcss@8.5.6): dependencies: browserslist: 4.28.1 @@ -2862,6 +2979,8 @@ snapshots: undici-types@7.16.0: {} + unicorn-magic@0.3.0: {} + untyped@2.0.0: dependencies: citty: 0.1.6 @@ -2924,4 +3043,6 @@ snapshots: yocto-queue@0.1.0: {} + yoctocolors@2.1.2: {} + zod@4.3.6: {} diff --git a/src/git/fetch-source.ts b/src/git/fetch-source.ts index 97d8306..1345d91 100644 --- a/src/git/fetch-source.ts +++ b/src/git/fetch-source.ts @@ -6,6 +6,8 @@ import path from "node:path"; import { pathToFileURL } from "node:url"; import { promisify } from "node:util"; +import { execa } from "execa"; + import { getErrnoCode } from "../errors"; import { assertSafeSourceId } from "../source-id"; import { exists, resolveGitCacheDir } from "./cache-dir"; @@ -21,6 +23,11 @@ const git = async ( args: string[], options?: { cwd?: string; timeoutMs?: number; allowFileProtocol?: boolean }, ) => { + const pathValue = process.env.PATH ?? process.env.Path; + const pathExtValue = + process.env.PATHEXT ?? + (process.platform === "win32" ? ".COM;.EXE;.BAT;.CMD" : undefined); + const configs = [ "-c", "core.hooksPath=/dev/null", @@ -39,14 +46,14 @@ const git = async ( configs.push("-c", "protocol.file.allow=never"); } - await execFileAsync("git", [...configs, ...args], { + await execa("git", [...configs, ...args], { cwd: options?.cwd, timeout: options?.timeoutMs ?? DEFAULT_TIMEOUT_MS, maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large repos env: { - PATH: process.env.PATH, - Path: process.env.Path, - PATHEXT: process.env.PATHEXT, + ...process.env, + ...(pathValue ? { PATH: pathValue, Path: pathValue } : {}), + ...(pathExtValue ? { PATHEXT: pathExtValue } : {}), HOME: process.env.HOME, USER: process.env.USER, USERPROFILE: process.env.USERPROFILE,