* fix(#816): mirror install command-prefix handling in _syncGsdDir applySurface() via _syncGsdDir handled command artifacts differently from a fresh install. For flat command dirs (cursor/augment/opencode/kilo) install's _copyStaged adds kind.prefix (gsd-<stem>.md) and _removeGsdEntries prunes prefix-scoped, but _syncGsdDir copied staged files verbatim (unprefixed) and pruned by exact name. So every /gsd:surface toggle wrote wrong filenames, orphaned the installed gsd-*.md, and deleted user-authored command files. _syncGsdDir's commands/agents branch now mirrors install: - flat command dirs get the gsd- prefix on copy; namespaced dirs (commands/gsd) and agents keep staged names, using install's namespacedByDir rule - prune is prefix-scoped so user files in shared flat dirs are preserved; namespaced commands/gsd stays membership-pruned so superseded commands are still removed on profile shrink The naming rule is intentionally re-implemented (not via require('bin/install.js') to avoid its module-load banner side-effect); a strict parity test asserts applySurface and installRuntimeArtifacts produce identical command filenames for opencode/kilo/cursor/augment/gemini, guarding against drift. Closes #816 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#816): add changeset fragment for PR #822 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:
@@ -410,23 +410,41 @@ function _syncGsdDir(stagedDir: string, destDir: string, kind: ArtifactKind | st
|
||||
// pruneSkillDirs() is the single point of truth for this logic.
|
||||
pruneSkillDirs(destDir, stagedDirs, kindPrefix, manifest);
|
||||
} else {
|
||||
// commands / agents kind: work with .md files
|
||||
const stagedFiles = new Set<string>(
|
||||
fs.readdirSync(stagedDir).filter(f => f.endsWith('.md'))
|
||||
);
|
||||
// commands / agents kind: mirror installRuntimeArtifacts (_copyStaged /
|
||||
// _removeGsdEntries in bin/install.js) so surface produces the SAME files as a
|
||||
// fresh install (#816). Flat command dirs (opencode/cursor/augment/kilo) take
|
||||
// the gsd- prefix on copy; namespaced command dirs (commands/gsd) and agents
|
||||
// keep their staged names. Copying staged names verbatim previously diverged
|
||||
// from install and orphaned the installed gsd-*.md files, and the unscoped
|
||||
// prune deleted user-owned command files.
|
||||
//
|
||||
// NOTE: the destName rule below intentionally mirrors bin/install.js
|
||||
// `_copyStaged` (the `namespacedByDir` decision). Keep them in sync.
|
||||
const destLast = (typeof kind === 'object' && kind !== null && kind.destSubpath)
|
||||
? path.basename(kind.destSubpath)
|
||||
: '';
|
||||
const prefixStem = kindPrefix ? kindPrefix.replace(/-$/, '') : '';
|
||||
const namespacedByDir = kindName === 'commands' && destLast === prefixStem;
|
||||
|
||||
// Copy files from staged to dest (overwrite to keep content current)
|
||||
const stagedFiles = fs.readdirSync(stagedDir).filter(f => f.endsWith('.md'));
|
||||
const stagedDestNames = new Set<string>();
|
||||
for (const file of stagedFiles) {
|
||||
fs.copyFileSync(path.join(stagedDir, file), path.join(destDir, file));
|
||||
const destName = (kindName === 'agents' || namespacedByDir)
|
||||
? file
|
||||
: `${kindPrefix}${file.slice(0, -3)}.md`;
|
||||
fs.copyFileSync(path.join(stagedDir, file), path.join(destDir, destName));
|
||||
stagedDestNames.add(destName);
|
||||
}
|
||||
|
||||
// Remove gsd-only files from dest that aren't in staged set
|
||||
// For commands dir: all .md files are gsd skills
|
||||
// For agents dir: only gsd-* files
|
||||
const destEntries = fs.readdirSync(destDir).filter(f => f.endsWith('.md'));
|
||||
for (const file of destEntries) {
|
||||
// Prune stale GSD-owned files not in the staged set, preserving user-owned files
|
||||
// (mirrors install's prefix-scoped _removeGsdEntries):
|
||||
// - agents: only gsd-* are GSD-owned
|
||||
// - flat command dirs: only `${kindPrefix}`-prefixed are GSD-owned
|
||||
// - namespaced command dirs: the whole dir is GSD-owned
|
||||
for (const file of fs.readdirSync(destDir).filter(f => f.endsWith('.md'))) {
|
||||
if (kindName === 'agents' && !file.startsWith('gsd-')) continue;
|
||||
if (!stagedFiles.has(file)) {
|
||||
if (kindName === 'commands' && !namespacedByDir && kindPrefix && !file.startsWith(kindPrefix)) continue;
|
||||
if (!stagedDestNames.has(file)) {
|
||||
try { fs.unlinkSync(path.join(destDir, file)); } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user