feat(#607): rebuild get-shit-done-cc → gsd-core migration (per-package cache + installer auto-cleanup + --dry-run) (#611)

* feat(#607): rebuild get-shit-done-cc → gsd-core migration

Leftover get-shit-done-cc installs poisoned the shared update cache,
causing a permanent false "update available". Rebuild the migration so a
stale old install is both harmless and actively removed.

- Per-package update cache filename (gsd-update-check-<slug>.json) in the
  shared ~/.cache/gsd dir, single-sourced via package-identity; writers
  stamp package_name and readers reject foreign/absent lineage. Multi-
  runtime visibility preserved (same shared dir + filename across runtimes).
- New get-shit-done/bin/lib/legacy-cleanup.cjs seam: detects code-file
  references to the old package + the legacy fixed-name cache across home
  runtime dirs; installer auto-cleans on every install; --dry-run previews
  and mutates nothing. User hooks and dev-preferences are never touched.
- update.md cache-clear globs gsd-update-check*.json across ALL supported
  runtimes (adds cursor/windsurf/augment/trae/qwen/hermes/codebuddy/cline).
- Fix worker MODULE_NOT_FOUND post-install (ship managed-hooks-registry.cjs
  + degrade gracefully) so the per-package cache is always written.
- Diataxis how-to: docs/cleanup-get-shit-done-cc.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#607): add changeset for PR #611

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#607): set USERPROFILE alongside HOME in dry-run install test for Windows

os.homedir() reads USERPROFILE on win32, so HOME-only isolation let the
spawned installer scan the real runner home on windows-latest. Set both.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-02 12:19:57 -04:00
committed by GitHub
parent df04aae5e4
commit 91f5bd7cb1
26 changed files with 1546 additions and 75 deletions

View File

@@ -1265,26 +1265,62 @@ describe('shared cache directory (#1421)', () => {
);
});
test('gsd-statusline.js checks shared cache first, falls back to legacy (#1421)', () => {
const content = fs.readFileSync(
test('gsd-statusline.js reads the per-package shared cache and rejects foreign lineage (#1421/#607)', () => {
const { evaluateUpdateCache } = require('../hooks/gsd-statusline.js');
const { updateCacheFileName, PACKAGE_NAME } = require('../get-shit-done/bin/lib/package-identity.cjs');
// Per-package filename embeds the package identity — no generic fallback
assert.strictEqual(
updateCacheFileName,
'gsd-update-check-opengsd-gsd-core.json',
'updateCacheFileName must be the per-package filename'
);
// The statusline must NOT reference a legacyCacheFile — the legacy fallback was removed
// allow-test-rule: architectural-invariant
const statuslineSrc = fs.readFileSync(
path.join(__dirname, '..', 'hooks', 'gsd-statusline.js'), 'utf-8'
);
// Statusline must check the shared cache path first
assert.ok(
content.includes("path.join(homeDir, '.cache', 'gsd', 'gsd-update-check.json')"),
'statusline must check shared cache at ~/.cache/gsd/gsd-update-check.json'
!statuslineSrc.includes('legacyCacheFile'),
'gsd-statusline.js must not reference legacyCacheFile — legacy fallback was removed in #607'
);
// Must fall back to legacy runtime-specific cache for backward compat
assert.ok(
content.includes("path.join(claudeDir, 'cache', 'gsd-update-check.json')"),
'statusline must fall back to legacy cache at claudeDir/cache/gsd-update-check.json'
statuslineSrc.includes(updateCacheFileName) || statuslineSrc.includes('updateCacheFileName'),
'gsd-statusline.js must reference the per-package updateCacheFileName'
);
// Shared cache must be checked before legacy (existsSync order matters)
const sharedIdx = content.indexOf('sharedCacheFile');
const legacyIdx = content.indexOf('legacyCacheFile');
assert.ok(
sharedIdx < legacyIdx,
'shared cache must be defined and checked before legacy cache'
// evaluateUpdateCache: foreign package_name → no update shown
assert.deepStrictEqual(
evaluateUpdateCache({ package_name: 'other-package', update_available: true }),
{ showUpdate: false, staleWarning: 'none' },
'foreign package_name must be rejected (lineage guard)'
);
// evaluateUpdateCache: absent package_name → no update shown
assert.deepStrictEqual(
evaluateUpdateCache({ update_available: true }),
{ showUpdate: false, staleWarning: 'none' },
'absent package_name must be rejected (lineage guard)'
);
// evaluateUpdateCache: null cache → no update shown
assert.deepStrictEqual(
evaluateUpdateCache(null),
{ showUpdate: false, staleWarning: 'none' },
'null cache must return no-update'
);
// evaluateUpdateCache: matching package_name + update_available:true → show update
const result = evaluateUpdateCache({ package_name: PACKAGE_NAME, update_available: true });
assert.strictEqual(result.showUpdate, true,
'matching package_name with update_available:true must set showUpdate=true'
);
// evaluateUpdateCache: matching package_name + update_available:false → no update
const noUpdate = evaluateUpdateCache({ package_name: PACKAGE_NAME, update_available: false });
assert.strictEqual(noUpdate.showUpdate, false,
'matching package_name with update_available:false must not show update'
);
});
});