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:
@@ -19,9 +19,21 @@ const { isSemverNewer } = require('../get-shit-done/bin/lib/semver-compare.cjs')
|
||||
// in the installed tree (only a {"type":"commonjs"} marker ships), so the
|
||||
// background check never reported updates.
|
||||
const { checkLatestVersion } = require('../get-shit-done/bin/check-latest-version.cjs');
|
||||
const { PACKAGE_NAME } = require('../get-shit-done/bin/lib/package-identity.cjs');
|
||||
// Authoritative list of managed hooks — shared with tests to retire source-grep
|
||||
// assertions (pending-migration-to-typed-ir [#455]).
|
||||
const { MANAGED_HOOKS } = require('./managed-hooks-registry.cjs');
|
||||
// NOTE: managed-hooks-registry.cjs must be in HOOKS_TO_COPY (scripts/build-hooks.js)
|
||||
// so it is present in hooks/dist/ and ships to the installed runtime hooks/ dir.
|
||||
// If it is missing (e.g., installed from an older dist), catch and degrade gracefully
|
||||
// so the worker always proceeds to compute and write the result cache record.
|
||||
let MANAGED_HOOKS = [];
|
||||
try {
|
||||
({ MANAGED_HOOKS } = require('./managed-hooks-registry.cjs'));
|
||||
} catch (e) {
|
||||
// Module not found in installed runtime — stale-hook detection degrades to
|
||||
// no-op (empty list means no hooks are checked for staleness). The worker
|
||||
// still runs and writes package_name / installed / latest / update_available.
|
||||
}
|
||||
|
||||
const cacheFile = process.env.GSD_CACHE_FILE;
|
||||
const projectVersionFile = process.env.GSD_PROJECT_VERSION_FILE;
|
||||
@@ -88,6 +100,7 @@ const result = {
|
||||
latest: latest || 'unknown',
|
||||
checked: Math.floor(Date.now() / 1000),
|
||||
stale_hooks: staleHooks.length > 0 ? staleHooks : undefined,
|
||||
package_name: PACKAGE_NAME,
|
||||
};
|
||||
|
||||
if (cacheFile) {
|
||||
|
||||
@@ -8,6 +8,8 @@ const path = require('path');
|
||||
const os = require('os');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
const { updateCacheFileName } = require('../get-shit-done/bin/lib/package-identity.cjs');
|
||||
|
||||
const homeDir = os.homedir();
|
||||
const cwd = process.cwd();
|
||||
|
||||
@@ -33,7 +35,7 @@ const projectConfigDir = detectConfigDir(cwd);
|
||||
// resolution mismatches where check-update writes to one runtime's cache
|
||||
// but statusline reads from another (#1421).
|
||||
const cacheDir = path.join(homeDir, '.cache', 'gsd');
|
||||
const cacheFile = path.join(cacheDir, 'gsd-update-check.json');
|
||||
const cacheFile = path.join(cacheDir, updateCacheFileName);
|
||||
|
||||
// VERSION file locations (check project first, then global)
|
||||
const projectVersionFile = path.join(projectConfigDir, 'get-shit-done', 'VERSION');
|
||||
|
||||
@@ -7,6 +7,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { isSemverNewer } = require('../get-shit-done/bin/lib/semver-compare.cjs');
|
||||
const { PACKAGE_NAME, updateCacheFileName } = require('../get-shit-done/bin/lib/package-identity.cjs');
|
||||
|
||||
// --- Config + last-command readers ------------------------------------------
|
||||
|
||||
@@ -395,32 +396,22 @@ function runStatusline() {
|
||||
const gsdStateStr = task ? '' : formatGsdState(readGsdState(dir) || {});
|
||||
|
||||
// GSD update available?
|
||||
// Check shared cache first (#1421), fall back to runtime-specific cache for
|
||||
// backward compatibility with older gsd-check-update.js versions.
|
||||
// Read only the per-package shared cache file (#607). The legacy
|
||||
// runtime-specific fallback has been removed — the per-package filename
|
||||
// carries lineage and avoids multi-runtime resolution mismatches (#1421).
|
||||
let gsdUpdate = '';
|
||||
const sharedCacheFile = path.join(homeDir, '.cache', 'gsd', 'gsd-update-check.json');
|
||||
const legacyCacheFile = path.join(claudeDir, 'cache', 'gsd-update-check.json');
|
||||
const cacheFile = fs.existsSync(sharedCacheFile) ? sharedCacheFile : legacyCacheFile;
|
||||
const cacheFile = path.join(homeDir, '.cache', 'gsd', updateCacheFileName);
|
||||
if (fs.existsSync(cacheFile)) {
|
||||
try {
|
||||
const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
|
||||
if (cache.update_available) {
|
||||
const { showUpdate, staleWarning } = evaluateUpdateCache(cache);
|
||||
if (showUpdate) {
|
||||
gsdUpdate = '\x1b[33m⬆ /gsd:update\x1b[0m │ ';
|
||||
}
|
||||
if (cache.stale_hooks && cache.stale_hooks.length > 0) {
|
||||
// If installed version is ahead of npm latest, this is a dev install.
|
||||
// Running /gsd:update would downgrade — show a contextual warning instead.
|
||||
const isDevInstall = (
|
||||
cache.installed &&
|
||||
cache.latest &&
|
||||
cache.latest !== 'unknown' &&
|
||||
isInstalledAheadOfLatest(cache.installed, cache.latest)
|
||||
);
|
||||
if (isDevInstall) {
|
||||
gsdUpdate += '\x1b[33m⚠ dev install — re-run installer to sync hooks\x1b[0m │ ';
|
||||
} else {
|
||||
gsdUpdate += '\x1b[31m⚠ stale hooks — run /gsd:update\x1b[0m │ ';
|
||||
}
|
||||
if (staleWarning === 'dev') {
|
||||
gsdUpdate += '\x1b[33m⚠ dev install — re-run installer to sync hooks\x1b[0m │ ';
|
||||
} else if (staleWarning === 'stale') {
|
||||
gsdUpdate += '\x1b[31m⚠ stale hooks — run /gsd:update\x1b[0m │ ';
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
@@ -507,12 +498,39 @@ function isInstalledAheadOfLatest(installed, latest) {
|
||||
return isSemverNewer(installed, latest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure function: evaluate an update-check cache object and return display flags.
|
||||
* Applies lineage guard — if package_name is absent or foreign, treats cache as absent.
|
||||
*
|
||||
* @param {object|null} cache Parsed cache object, or null.
|
||||
* @returns {{ showUpdate: boolean, staleWarning: 'none'|'dev'|'stale' }}
|
||||
*/
|
||||
function evaluateUpdateCache(cache) {
|
||||
const none = { showUpdate: false, staleWarning: 'none' };
|
||||
if (!cache) return none;
|
||||
// Lineage guard: package_name must be present and match this package.
|
||||
if (!cache.package_name || cache.package_name !== PACKAGE_NAME) return none;
|
||||
const showUpdate = Boolean(cache.update_available);
|
||||
let staleWarning = 'none';
|
||||
if (cache.stale_hooks && cache.stale_hooks.length > 0) {
|
||||
const isDevInstall = (
|
||||
cache.installed &&
|
||||
cache.latest &&
|
||||
cache.latest !== 'unknown' &&
|
||||
isInstalledAheadOfLatest(cache.installed, cache.latest)
|
||||
);
|
||||
staleWarning = isDevInstall ? 'dev' : 'stale';
|
||||
}
|
||||
return { showUpdate, staleWarning };
|
||||
}
|
||||
|
||||
// Export helpers for unit tests. Harmless when run as a script.
|
||||
module.exports = {
|
||||
readGsdState, parseStateMd, formatGsdState,
|
||||
readGsdConfig, getConfigValue, readLastSlashCommand,
|
||||
composeStatusline,
|
||||
isInstalledAheadOfLatest,
|
||||
evaluateUpdateCache,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// gsd-hook-version: {{GSD_VERSION}}
|
||||
// SessionStart banner that surfaces GSD update availability when GSD's
|
||||
// statusline isn't installed. Reads the cache that
|
||||
// gsd-check-update-worker.js writes to ~/.cache/gsd/gsd-update-check.json.
|
||||
// gsd-check-update-worker.js writes to ~/.cache/gsd/<updateCacheFileName> (per-package).
|
||||
//
|
||||
// Opt-in by design: bin/install.js only registers this hook when the user
|
||||
// declines to install (or replace) the GSD statusline. The presence of the
|
||||
@@ -15,6 +15,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { PACKAGE_NAME, updateCacheFileName } = require('../get-shit-done/bin/lib/package-identity.cjs');
|
||||
|
||||
// Suppress repeat parse-error banners for 24 hours so a genuinely broken
|
||||
// cache file doesn't nag the user every session.
|
||||
@@ -37,6 +38,9 @@ function buildBannerOutput(state) {
|
||||
return { systemMessage: 'GSD update check failed.' };
|
||||
}
|
||||
if (!cache) return null;
|
||||
// Lineage guard: package_name must be present and match this package.
|
||||
// Absent package_name means the cache predates lineage tracking — treat as untrusted.
|
||||
if (!cache.package_name || cache.package_name !== PACKAGE_NAME) return null;
|
||||
if (!cache.update_available) return null;
|
||||
const installed = cache.installed || 'unknown';
|
||||
const latest = cache.latest || 'unknown';
|
||||
@@ -96,7 +100,7 @@ function recordFailureWarning(sentinelFile, nowSeconds) {
|
||||
|
||||
function main() {
|
||||
const cacheDir = path.join(os.homedir(), '.cache', 'gsd');
|
||||
const cacheFile = path.join(cacheDir, 'gsd-update-check.json');
|
||||
const cacheFile = path.join(cacheDir, updateCacheFileName);
|
||||
const sentinelFile = path.join(cacheDir, 'banner-failure-warned-at');
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user