* 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>
126 lines
5.1 KiB
JavaScript
126 lines
5.1 KiB
JavaScript
#!/usr/bin/env node
|
|
'use strict';
|
|
|
|
/**
|
|
* Single source for GSD's published-package coordinates (issue #498).
|
|
*
|
|
* `deriveIdentity(pkg)` is the pure core: it turns a parsed package.json into
|
|
* the coordinate record every consumer needs. The generated runtime module
|
|
* `get-shit-done/bin/lib/package-identity.cjs` bakes those values at build
|
|
* time, because the installed tree carries only a synthetic
|
|
* `{"type":"commonjs"}` package.json (no `.name`) — so a runtime
|
|
* `require('package.json').name` resolves to `undefined` (the #378 bug this
|
|
* seam retires). Baking from package.json reconciles #378 (renames survive)
|
|
* with #2992 (the value is never an LLM runtime choice).
|
|
*/
|
|
|
|
/**
|
|
* Parse `owner/name` out of a package.json `repository.url`, stripping the
|
|
* `git+` prefix and `.git` suffix npm conventionally adds.
|
|
*/
|
|
function parseRepoSlug(repository) {
|
|
const url = typeof repository === 'string' ? repository : (repository && repository.url) || '';
|
|
const m = url.replace(/^git\+/, '').replace(/\.git$/, '').match(/github\.com[/:]([^/]+\/[^/]+)$/);
|
|
return m ? m[1] : '';
|
|
}
|
|
|
|
/**
|
|
* Pure: turn an npm package name into a filesystem-safe slug for cache filenames.
|
|
* Strips a leading `@`, replaces `/` with `-`, then collapses any run of
|
|
* characters that are NOT `[a-z0-9]` to a single `-`, and trims leading/trailing `-`.
|
|
*/
|
|
function slugifyPackageName(name) {
|
|
if (!name) return '';
|
|
return name
|
|
.replace(/^@/, '')
|
|
.replace(/\//g, '-')
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-+|-+$/g, '');
|
|
}
|
|
|
|
/**
|
|
* Pure: package.json object -> the package identity coordinates.
|
|
*/
|
|
function deriveIdentity(pkg = {}) {
|
|
const packageName = pkg.name || '';
|
|
const binName = pkg.bin ? Object.keys(pkg.bin)[0] || '' : '';
|
|
const repoSlug = parseRepoSlug(pkg.repository);
|
|
const repoUrl = repoSlug ? `https://github.com/${repoSlug}` : '';
|
|
const changelogRawUrl = repoSlug
|
|
? `https://raw.githubusercontent.com/${repoSlug}/main/CHANGELOG.md`
|
|
: '';
|
|
const cacheSlug = slugifyPackageName(packageName);
|
|
const updateCacheFileName = cacheSlug ? `gsd-update-check-${cacheSlug}.json` : 'gsd-update-check.json';
|
|
return { packageName, binName, repoSlug, repoUrl, changelogRawUrl, cacheSlug, updateCacheFileName };
|
|
}
|
|
|
|
/**
|
|
* Pure: format the `npx` fallback install command. Shape matches the literal
|
|
* the update workflow embeds: `npx -y --package=<pkg>@latest -- <bin>
|
|
* [--<runtime>] --<scope>`. The runtime flag is omitted when not supplied.
|
|
*
|
|
* This function is the canonical source — `render()` serializes it verbatim
|
|
* into the generated module, so the runtime copy can never drift from it.
|
|
*/
|
|
function formatManualInstall({ packageName, binName, scope, runtime } = {}) {
|
|
const runtimeFlag = runtime ? ` --${runtime}` : '';
|
|
return `npx -y --package=${packageName}@latest -- ${binName}${runtimeFlag} --${scope}`;
|
|
}
|
|
|
|
const GENERATED_HEADER =
|
|
'// @generated by scripts/generate-package-identity.cjs from package.json — DO NOT EDIT.\n' +
|
|
'// Single source for GSD package coordinates (issue #498). Regenerate with:\n' +
|
|
'// node scripts/generate-package-identity.cjs\n';
|
|
|
|
/**
|
|
* Render the generated runtime module text for a derived identity. Values are
|
|
* baked as literals; `formatManualInstall` is embedded by `.toString()` so the
|
|
* runtime command builder is byte-identical to the tested source above.
|
|
*/
|
|
function render(identity) {
|
|
const { packageName, binName, repoSlug, repoUrl, changelogRawUrl, cacheSlug, updateCacheFileName } = identity;
|
|
const j = (v) => JSON.stringify(v);
|
|
return (
|
|
GENERATED_HEADER +
|
|
"'use strict';\n\n" +
|
|
`const packageName = ${j(packageName)};\n` +
|
|
`const binName = ${j(binName)};\n` +
|
|
`const repoSlug = ${j(repoSlug)};\n` +
|
|
`const repoUrl = ${j(repoUrl)};\n` +
|
|
`const changelogRawUrl = ${j(changelogRawUrl)};\n` +
|
|
`const cacheSlug = ${j(cacheSlug)};\n` +
|
|
`const updateCacheFileName = ${j(updateCacheFileName)};\n\n` +
|
|
`${formatManualInstall.toString()}\n\n` +
|
|
'function manualInstallCommand(opts = {}) {\n' +
|
|
' return formatManualInstall({ packageName, binName, scope: opts.scope, runtime: opts.runtime });\n' +
|
|
'}\n\n' +
|
|
'module.exports = Object.freeze({\n' +
|
|
' packageName,\n' +
|
|
' // PACKAGE_NAME: back-compat alias for #516-era consumers. Baked here, so it\n' +
|
|
" // survives the installed tree's synthetic package.json (fixes the #378 undefined).\n" +
|
|
' PACKAGE_NAME: packageName,\n' +
|
|
' binName,\n' +
|
|
' repoSlug,\n' +
|
|
' repoUrl,\n' +
|
|
' changelogRawUrl,\n' +
|
|
' cacheSlug,\n' +
|
|
' updateCacheFileName,\n' +
|
|
' manualInstallCommand,\n' +
|
|
'});\n'
|
|
);
|
|
}
|
|
|
|
function main() {
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
|
const out = path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib', 'package-identity.cjs');
|
|
fs.writeFileSync(out, render(deriveIdentity(pkg)));
|
|
process.stdout.write(`wrote ${path.relative(path.join(__dirname, '..'), out)}\n`);
|
|
}
|
|
|
|
if (require.main === module) main();
|
|
|
|
module.exports = { deriveIdentity, parseRepoSlug, slugifyPackageName, formatManualInstall, render, main };
|