From 2f8638a3e29d481385c7af5491a171ec74e46b60 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 16 Aug 2026 13:42:58 -0700 Subject: [PATCH] fix(setup): Windows runtime-asset copies prune nested gitignored build output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _link_skill_runtime_assets' exclusion list filters DIRECT children only, so the Windows cp -R real-copy path swept NESTED gitignored build output into the installed skill dirs — concretely, ios-qa/scripts/gen-accessors-tool/ .build is 252MB per install. The IS_WINDOWS real-copy branch now prunes nested node_modules/.build/dist post-copy (find -prune -exec rm -rf). Scoped to _link_skill_runtime_assets ONLY: the generic _link_or_copy stays untouched because runtime roots (browse/, design/) intentionally copy their dist/ binaries. On Unix the assets are symlinks into the working tree, and the prune is gated on the real-copy shape so it can never delete build output from the repo through a link — both directions pinned in test/setup-windows-rerun-refresh.test.ts with fixture trees. Co-Authored-By: Claude Fable 5 --- setup | 10 ++++ test/setup-windows-rerun-refresh.test.ts | 65 ++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/setup b/setup index 5328d188c..d4378e783 100755 --- a/setup +++ b/setup @@ -767,6 +767,16 @@ _link_skill_runtime_assets() { rm -rf "$dst_dir/$asset_name" fi _link_or_copy "$asset" "$dst_dir/$asset_name" + # P5: the exclusion list above filters DIRECT children only, but the + # Windows cp -R copy sweeps NESTED gitignored build output too (concrete: + # ios-qa/scripts/gen-accessors-tool/.build is 252MB). Prune post-copy — + # a rendered skill install is never a build root, so nested + # node_modules/.build/dist are dead weight. ONLY here: the generic + # _link_or_copy stays untouched because runtime roots (browse/, design/) + # intentionally copy their dist/ binaries. + if [ "$IS_WINDOWS" -eq 1 ] && [ -d "$dst_dir/$asset_name" ] && [ ! -L "$dst_dir/$asset_name" ]; then + find "$dst_dir/$asset_name" -type d \( -name node_modules -o -name .build -o -name dist \) -prune -exec rm -rf {} + 2>/dev/null || true + fi done } diff --git a/test/setup-windows-rerun-refresh.test.ts b/test/setup-windows-rerun-refresh.test.ts index 57e87e458..3e729ec55 100644 --- a/test/setup-windows-rerun-refresh.test.ts +++ b/test/setup-windows-rerun-refresh.test.ts @@ -139,6 +139,71 @@ describe('setup: Windows re-run refresh — behavior fixture (#2444)', () => { } }); + test('IS_WINDOWS=1: nested gitignored build output does NOT survive the runtime-asset copy (P5)', () => { + // The exclusion list in _link_skill_runtime_assets filters direct + // children only; cp -R swept NESTED node_modules/.build/dist too + // (concrete: ios-qa/scripts/gen-accessors-tool/.build, 252MB). The + // Windows branch prunes them post-copy; real asset files at every level + // survive. + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-rerun-prune-')); + try { + const src = path.join(tmp, 'skill-src'); + const dst = path.join(tmp, 'skill-dst'); + fs.mkdirSync(path.join(src, 'scripts', 'gen-tool', '.build'), { recursive: true }); + fs.mkdirSync(path.join(src, 'scripts', 'gen-tool', 'node_modules', 'dep'), { recursive: true }); + fs.mkdirSync(path.join(src, 'scripts', 'gen-tool', 'dist'), { recursive: true }); + fs.mkdirSync(dst, { recursive: true }); + fs.writeFileSync(path.join(src, 'scripts', 'runner.sh'), 'echo run\n'); + fs.writeFileSync(path.join(src, 'scripts', 'gen-tool', 'main.swift'), 'source\n'); + fs.writeFileSync(path.join(src, 'scripts', 'gen-tool', '.build', 'blob.bin'), '#'.repeat(4096)); + fs.writeFileSync(path.join(src, 'scripts', 'gen-tool', 'node_modules', 'dep', 'index.js'), 'x\n'); + fs.writeFileSync(path.join(src, 'scripts', 'gen-tool', 'dist', 'compiled'), 'bin\n'); + + const r = runInstaller( + '1', + ['_link_skill_runtime_assets'], + `_link_skill_runtime_assets "${src}" "${dst}"`, + ); + expect(r.status).toBe(0); + // Real assets at both levels survive… + expect(fs.readFileSync(path.join(dst, 'scripts', 'runner.sh'), 'utf-8')).toBe('echo run\n'); + expect(fs.readFileSync(path.join(dst, 'scripts', 'gen-tool', 'main.swift'), 'utf-8')).toBe('source\n'); + // …nested build output does not. + expect(fs.existsSync(path.join(dst, 'scripts', 'gen-tool', '.build'))).toBe(false); + expect(fs.existsSync(path.join(dst, 'scripts', 'gen-tool', 'node_modules'))).toBe(false); + expect(fs.existsSync(path.join(dst, 'scripts', 'gen-tool', 'dist'))).toBe(false); + // The source tree is untouched — the prune runs on the COPY only. + expect(fs.existsSync(path.join(src, 'scripts', 'gen-tool', '.build', 'blob.bin'))).toBe(true); + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } + }); + + test('IS_WINDOWS=0: the Unix symlink path never prunes through into the source', () => { + // On Unix the asset is a SYMLINK into the working tree; pruning through + // it would delete real build output from the repo. The prune is gated on + // the Windows real-copy shape ([ -d ] && [ ! -L ]). + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-rerun-prune-unix-')); + try { + const src = path.join(tmp, 'skill-src'); + const dst = path.join(tmp, 'skill-dst'); + fs.mkdirSync(path.join(src, 'scripts', 'gen-tool', '.build'), { recursive: true }); + fs.mkdirSync(dst, { recursive: true }); + fs.writeFileSync(path.join(src, 'scripts', 'gen-tool', '.build', 'blob.bin'), 'keep'); + + const r = runInstaller( + '0', + ['_link_skill_runtime_assets'], + `_link_skill_runtime_assets "${src}" "${dst}"`, + ); + expect(r.status).toBe(0); + expect(fs.lstatSync(path.join(dst, 'scripts')).isSymbolicLink()).toBe(true); + expect(fs.existsSync(path.join(src, 'scripts', 'gen-tool', '.build', 'blob.bin'))).toBe(true); + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } + }); + test('IS_WINDOWS=1: the gstack sidecar dir is still skipped by the skill loop', () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-rerun-skip-')); try {