mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-20 11:52:20 +02:00
fix(setup): never link over, copy over, or reap a skill gstack does not own (#2119)
The relink gate alone left three destructive sites open: - link_claude_skill_dirs runs BEFORE relink on every ./setup and used `ln -snf` (Linux replaces a user's real SKILL.md with a symlink into gstack) or, on Windows, rm -rf + cp followed by a marker that made the user's directory "ours" on the next flip. It and _install_alias_skill_md now consult _claude_entry_is_ours first and skip loudly. - cleanup_prefixed_claude_symlinks kept a bare name-match deletion and a `*gstack*` substring match. Symlink arms use anchored `gstack/` segment patterns; the Windows real-file arm proves provenance (marker, byte-identity with our source, or the full two-line gen-skill-docs banner within the first 40 lines, never a one-line substring another generator could emit). cleanup_old_claude_symlinks uses the same banner rule. - gstack-relink's fast path judged absolute targets before canonicalizing, so `/x/gstack/../foreign/SKILL.md` counted as ours; dot-segment targets now canonicalize first. Its banner rule matches setup's. The `.gstack-owned` marker records the owning payload's realpath. Entries skipped by setup or relink are listed in the final setup summary. Chromium bootstrap refinements from the pre-landing review: an INT/TERM trap kills the installer's process tree; the Windows npm chain no longer masks an install failure; GSTACK_SKIP_PLAYWRIGHT=1 is reported as a choice rather than a failure and sends no telemetry; the timeout knob is normalized (0, 000, non-numeric, or more than nine digits fall back to the 600s default instead of killing on the first poll or never killing). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
b8d347df35
commit
2548634d96
@@ -24,7 +24,7 @@ function extractFn(name: string): string {
|
||||
}
|
||||
|
||||
function cleanupBody(): string {
|
||||
return extractFn('cleanup_old_claude_symlinks');
|
||||
return extractFn('_gstack_generated_header') + extractFn('cleanup_old_claude_symlinks');
|
||||
}
|
||||
|
||||
describe('setup: cleanup_old_claude_symlinks — static (#2204)', () => {
|
||||
@@ -68,6 +68,7 @@ describe.skipIf(process.platform === 'win32')('setup: cleanup_old_claude_symlink
|
||||
const script = [
|
||||
'set -e',
|
||||
`IS_WINDOWS=${opts.isWindows ?? '0'}`,
|
||||
extractFn('_gstack_generated_header'),
|
||||
extractFn('cleanup_old_claude_symlinks'),
|
||||
`cleanup_old_claude_symlinks "${gstackArg}" "${skills}"`,
|
||||
].join('\n');
|
||||
@@ -264,7 +265,7 @@ describe.skipIf(process.platform === 'win32')('setup: cleanup_old_claude_symlink
|
||||
// marker, a byte-identical copy of the payload source, or gen-skill-docs'
|
||||
// AUTO-GENERATED header (legacy copies made before the marker existed).
|
||||
test('Windows real-file leftover is removed only when provably gstack-owned', () => {
|
||||
const generated = '---\nname: ship\n---\n<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->\n# ship\n';
|
||||
const generated = '---\nname: ship\n---\n<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->\n<!-- Regenerate: bun run gen:skill-docs -->\n# ship\n';
|
||||
const r = runCleanup({
|
||||
isWindows: '1',
|
||||
payload: true,
|
||||
@@ -296,4 +297,30 @@ describe.skipIf(process.platform === 'win32')('setup: cleanup_old_claude_symlink
|
||||
fs.rmSync(r.tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// The Windows arm is the ONLY path that touches a real-file SKILL.md. On
|
||||
// Unix a same-name real-file skill must survive even when the payload names
|
||||
// it and even when its bytes are identical to the payload source.
|
||||
test('Unix (IS_WINDOWS=0): a same-name real-file skill is never reaped, even if byte-identical to the payload', () => {
|
||||
const r = runCleanup({
|
||||
isWindows: '0',
|
||||
payload: true,
|
||||
plant(skills, payload) {
|
||||
fs.mkdirSync(path.join(payload, 'qa'));
|
||||
fs.writeFileSync(path.join(payload, 'qa', 'SKILL.md'), '---\nname: qa\n---\n');
|
||||
fs.mkdirSync(path.join(skills, 'qa'));
|
||||
fs.copyFileSync(path.join(payload, 'qa', 'SKILL.md'), path.join(skills, 'qa', 'SKILL.md'));
|
||||
fs.mkdirSync(path.join(skills, 'ship'));
|
||||
fs.writeFileSync(path.join(skills, 'ship', 'SKILL.md'), '---\nname: ship\n---\n');
|
||||
fs.writeFileSync(path.join(skills, 'ship', '.gstack-owned'), '');
|
||||
},
|
||||
});
|
||||
try {
|
||||
expect(r.status).toBe(0);
|
||||
expect(r.stdout).toBe('');
|
||||
expect(r.names).toEqual(['gstack', 'qa', 'ship']);
|
||||
} finally {
|
||||
fs.rmSync(r.tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user