refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD across contents and paths, upstream package/repo coordinates -> @golem15/msd-core and golem15com/msd-core. Deep links into upstream history, sibling upstream packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is. Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line, package/plugin identity, regenerated lockfile, install-tree fixtures, derived registries and benchmark baseline; migration checksum baseline re-locked (MSD keeps its own install state, so no install had applied the old sums); sort-order and regex-escaped expectations in tests adjusted.
This commit is contained in:
@@ -78,7 +78,7 @@ function withInstallerPackageSource<T>(
|
||||
// Reuse the existing compatibility-marker contract through the existing fs
|
||||
// seam. The marker exists only in this synchronous call tree: no disk state,
|
||||
// layout export, stage argument, or caller-settable authority flag is added.
|
||||
const markerPath = path.resolve(configDir, '.gsd-source');
|
||||
const markerPath = path.resolve(configDir, '.msd-source');
|
||||
const packageCommandsRoot = runtimeArtifactLayout.findInstallSourceRoot();
|
||||
const markerBytes = Buffer.from(packageCommandsRoot + '\n');
|
||||
const base = installFs();
|
||||
@@ -102,7 +102,7 @@ function withInstallerPackageSource<T>(
|
||||
|
||||
function isRuntimeSurfaceSourceUnavailable(message: string): boolean {
|
||||
return message.startsWith('Runtime Surface source is unavailable or incomplete for ') &&
|
||||
message.endsWith('install or upgrade gsd-core before materializing this surface.');
|
||||
message.endsWith('install or upgrade msd-core before materializing this surface.');
|
||||
}
|
||||
|
||||
function assertCorpusTreeHasNoSymlinks(root: string): void {
|
||||
@@ -148,7 +148,7 @@ function syncRuntimeSurfaceCorpus(source: string, destination: string, configDir
|
||||
}
|
||||
assertCorpusTreeHasNoSymlinks(destination);
|
||||
|
||||
// Remove only paths the previous manifest proves GSD owned and which the
|
||||
// Remove only paths the previous manifest proves MSD owned and which the
|
||||
// executing package no longer ships. Unknown neighbouring files survive.
|
||||
for (const relative of previousOwnedCorpusFiles(configDir, manifestPrefix)) {
|
||||
const sourceEntry = path.join(source, ...relative.split('/'));
|
||||
@@ -178,7 +178,7 @@ function syncRuntimeSurfaceCorpus(source: string, destination: string, configDir
|
||||
* Provision the raw, installation-owned input needed to re-materialize a
|
||||
* global Runtime Surface after the executing package tree disappears.
|
||||
*
|
||||
* The corpus deliberately lives below the already-installed `gsd-core/`
|
||||
* The corpus deliberately lives below the already-installed `msd-core/`
|
||||
* tree and is accepted only after its manifest ownership and hashes verify.
|
||||
* The compatibility marker does not replace that installed-corpus authority.
|
||||
*/
|
||||
@@ -196,16 +196,16 @@ function provisionRuntimeSurfaceCorpus(
|
||||
}
|
||||
if (required.size === 0) return;
|
||||
|
||||
const corpusRoot = path.join(configDir, 'gsd-core');
|
||||
const corpusRoot = path.join(configDir, 'msd-core');
|
||||
if (required.has('commands')) {
|
||||
const source = runtimeArtifactLayout.findInstallSourceRoot();
|
||||
const destination = path.join(corpusRoot, 'commands', 'gsd');
|
||||
syncRuntimeSurfaceCorpus(source, destination, configDir, 'gsd-core/commands/gsd/');
|
||||
const destination = path.join(corpusRoot, 'commands', 'msd');
|
||||
syncRuntimeSurfaceCorpus(source, destination, configDir, 'msd-core/commands/msd/');
|
||||
}
|
||||
if (required.has('agents')) {
|
||||
const source = path.join(executingPackageRoot(), 'agents');
|
||||
const destination = path.join(corpusRoot, 'agents');
|
||||
syncRuntimeSurfaceCorpus(source, destination, configDir, 'gsd-core/agents/');
|
||||
syncRuntimeSurfaceCorpus(source, destination, configDir, 'msd-core/agents/');
|
||||
}
|
||||
|
||||
const markerFile = _hostBehaviors(layout.runtime).sourceMarkerFile;
|
||||
@@ -215,7 +215,7 @@ function provisionRuntimeSurfaceCorpus(
|
||||
if (hasExistingSymlinkBetween(path.resolve(configDir), markerPath, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
||||
throw new Error(`compatibility marker "${markerPath}" contains an untrusted symlink`);
|
||||
}
|
||||
installFs().writeFileSync(markerPath, path.join(corpusRoot, 'commands', 'gsd') + '\n', 'utf8');
|
||||
installFs().writeFileSync(markerPath, path.join(corpusRoot, 'commands', 'msd') + '\n', 'utf8');
|
||||
} catch {
|
||||
// The existing installer marker writer owns the user-facing warning and
|
||||
// keeps marker failure non-fatal. The installed corpus remains usable
|
||||
@@ -231,13 +231,13 @@ function executingPackageRoot(): string { return path.dirname(path.dirname(runti
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Single source of truth for user-owned artifacts inside gsd-core/.
|
||||
* Single source of truth for user-owned artifacts inside msd-core/.
|
||||
*
|
||||
* These files are created/refreshed by user-facing workflows (e.g.
|
||||
* /gsd-profile-user) and must be preserved across reinstalls. Critically, they
|
||||
* MUST be excluded from gsd-file-manifest.json — otherwise saveLocalPatches()
|
||||
* /msd-profile-user) and must be preserved across reinstalls. Critically, they
|
||||
* MUST be excluded from msd-file-manifest.json — otherwise saveLocalPatches()
|
||||
* will compare a refreshed file against a stale manifest hash and emit a
|
||||
* spurious "locally modified GSD file" warning (bug #2771).
|
||||
* spurious "locally modified MSD file" warning (bug #2771).
|
||||
*
|
||||
* Invariant: a file is either distribution (manifest-tracked, diff'd against
|
||||
* manifest) or user artifact (preserved across installs, never diff'd). Never
|
||||
@@ -245,7 +245,7 @@ function executingPackageRoot(): string { return path.dirname(path.dirname(runti
|
||||
* writeManifest must agree on this list, which is why it lives here as a
|
||||
* single constant.
|
||||
*
|
||||
* Paths are relative to the gsd-core/ directory.
|
||||
* Paths are relative to the msd-core/ directory.
|
||||
*/
|
||||
const USER_OWNED_ARTIFACTS: string[] = ['USER-PROFILE.md'];
|
||||
|
||||
@@ -333,7 +333,7 @@ const SKILLS_CONVERTER_REGISTRY: Record<string, (content: string, skillName: str
|
||||
* like `../../etc`. This is the line `resolvedFullPath !== resolvedRoot
|
||||
* && !resolvedFullPath.startsWith(resolvedRoot + path.sep)` below.
|
||||
* (b) A symlink's resolved real path equals the install root itself — this
|
||||
* would let `_removeGsdEntries` (the prune pass) wipe the install root,
|
||||
* would let `_removeMsdEntries` (the prune pass) wipe the install root,
|
||||
* which is the config-root-wipe threat from #1704 threat model item (b).
|
||||
*
|
||||
* What opt-in RELAXES specifically: the "pre-existing symlink that points
|
||||
@@ -349,7 +349,7 @@ const SKILLS_CONVERTER_REGISTRY: Record<string, (content: string, skillName: str
|
||||
* @returns true when the caller MUST refuse; false when writes may proceed.
|
||||
*/
|
||||
function isSymlinkedDestOptIn(): boolean {
|
||||
const v = process.env.GSD_ALLOW_SYMLINKED_DEST;
|
||||
const v = process.env.MSD_ALLOW_SYMLINKED_DEST;
|
||||
return v === '1' || v === 'true';
|
||||
}
|
||||
|
||||
@@ -461,7 +461,7 @@ function hasExistingSymlinkBetween(
|
||||
if (segmentLstat.isSymbolicLink()) {
|
||||
if (!allowFollow) return true;
|
||||
// Opt-in active: follow the symlink. Refuse if the resolved target is the
|
||||
// install root itself (threat (b) — would let _removeGsdEntries wipe the
|
||||
// install root itself (threat (b) — would let _removeMsdEntries wipe the
|
||||
// root). Other targets are acceptable per the user's explicit opt-in. A
|
||||
// broken symlink (realpathSync throws) is still refused.
|
||||
//
|
||||
@@ -504,18 +504,18 @@ function hasExistingSymlinkBetween(
|
||||
* (test-matrix E1/E4) — this module never reimplements either decision, only
|
||||
* reuses them (user-artifact-staging.cts's own module doc, "Confinement").
|
||||
*
|
||||
* Fixed location: `<configDir>/.gsd-staging/user-artifacts/` — a sibling of
|
||||
* Fixed location: `<configDir>/.msd-staging/user-artifacts/` — a sibling of
|
||||
* every directory this phase's four call sites wipe, so staging survives all
|
||||
* of them while staying inside configDir (40-design.md "Staging location").
|
||||
*/
|
||||
function _resolveUserArtifactStagingRoot(configDir: string): string {
|
||||
const stagingRoot = runtimeArtifactInstallPlan.assertDestWithinConfigHome(
|
||||
configDir,
|
||||
path.posix.join('.gsd-staging', 'user-artifacts'),
|
||||
path.posix.join('.msd-staging', 'user-artifacts'),
|
||||
);
|
||||
if (hasExistingSymlinkBetween(path.resolve(configDir), stagingRoot, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
||||
throw new Error(
|
||||
`_resolveUserArtifactStagingRoot: staging root "${stagingRoot}" contains a symlink the install root "${configDir}" does not trust — refusing to stage. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`,
|
||||
`_resolveUserArtifactStagingRoot: staging root "${stagingRoot}" contains a symlink the install root "${configDir}" does not trust — refusing to stage. If this is an intentional user-owned symlink layout, re-run with MSD_ALLOW_SYMLINKED_DEST=1.`,
|
||||
);
|
||||
}
|
||||
return stagingRoot;
|
||||
@@ -523,12 +523,12 @@ function _resolveUserArtifactStagingRoot(configDir: string): string {
|
||||
|
||||
/**
|
||||
* Degrade-not-abort wrapper over `_resolveUserArtifactStagingRoot` (defect
|
||||
* fix — a hostile/broken `.gsd-staging` path, or a symlinked configDir
|
||||
* itself, e.g. nix-darwin/dotfiles-managed `~/.claude`, GSD_ALLOW_SYMLINKED_DEST's
|
||||
* fix — a hostile/broken `.msd-staging` path, or a symlinked configDir
|
||||
* itself, e.g. nix-darwin/dotfiles-managed `~/.claude`, MSD_ALLOW_SYMLINKED_DEST's
|
||||
* own population) must never brick the command it is called from. Before
|
||||
* this fix `_resolveUserArtifactStagingRoot` was called UNGUARDED as the
|
||||
* first statement of both `install()` and `uninstall()` (bin/install.js) —
|
||||
* `ln -s /nonexistent ~/.claude/.gsd-staging` killed both commands,
|
||||
* `ln -s /nonexistent ~/.claude/.msd-staging` killed both commands,
|
||||
* including uninstall, the remedy for the first problem.
|
||||
*
|
||||
* Returns `null` (never throws) when staging is unavailable, logging ONE
|
||||
@@ -544,7 +544,7 @@ function _tryResolveUserArtifactStagingRoot(configDir: string): string | null {
|
||||
return _resolveUserArtifactStagingRoot(configDir);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
` [gsd] user-artifact staging unavailable for "${configDir}" (${(err as Error).message}) — proceeding without durable staging for this step.`,
|
||||
` [msd] user-artifact staging unavailable for "${configDir}" (${(err as Error).message}) — proceeding without durable staging for this step.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
@@ -555,13 +555,13 @@ function _tryResolveUserArtifactStagingRoot(configDir: string): string | null {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Migrate a legacy dev-preferences.md (saved from commands/gsd/) into the
|
||||
* Migrate a legacy dev-preferences.md (saved from commands/msd/) into the
|
||||
* runtime-aware SKILL.md location used by the writer after #2973.
|
||||
*
|
||||
* For runtimes with a nested skills layout (e.g. Hermes: skills/gsd/<stem>/),
|
||||
* the target is <configDir>/skills/gsd/dev-preferences/SKILL.md.
|
||||
* For runtimes with a flat skills layout (prefix='gsd-'), the target is
|
||||
* <configDir>/skills/gsd-dev-preferences/SKILL.md.
|
||||
* For runtimes with a nested skills layout (e.g. Hermes: skills/msd/<stem>/),
|
||||
* the target is <configDir>/skills/msd/dev-preferences/SKILL.md.
|
||||
* For runtimes with a flat skills layout (prefix='msd-'), the target is
|
||||
* <configDir>/skills/msd-dev-preferences/SKILL.md.
|
||||
*
|
||||
* Skips silently if no legacy file was preserved, or if a SKILL.md already
|
||||
* exists at the new location (don't clobber user-customized skill content
|
||||
@@ -619,7 +619,7 @@ function _resolveDevPreferencesSkillTarget(targetDir: string, runtime?: string,
|
||||
const layout: any = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, targetDir, scope as any);
|
||||
const skillsKindEntry = layout.kinds.find((k: any) => k.kind === 'skills');
|
||||
if (!skillsKindEntry) return null; // runtime has no skills layout at this scope (e.g. cline local)
|
||||
const stemName = skillsKindEntry.prefix === '' ? 'dev-preferences' : 'gsd-dev-preferences';
|
||||
const stemName = skillsKindEntry.prefix === '' ? 'dev-preferences' : 'msd-dev-preferences';
|
||||
// #2911: same destination-root defect as _copyStaged/applySurface — honor
|
||||
// skillsKindEntry.home as a FALLBACK-preferred override (e.g. Codex skills
|
||||
// -> $HOME/.agents) instead of always resolving against targetDir, so a
|
||||
@@ -630,7 +630,7 @@ function _resolveDevPreferencesSkillTarget(targetDir: string, runtime?: string,
|
||||
skillDir = path.join(runtimeArtifactInstallPlan.assertDestWithinConfigHome(installRoot, skillsKindEntry.destSubpath), stemName);
|
||||
} else {
|
||||
// Legacy fallback for callers that have not yet been updated to pass runtime
|
||||
skillDir = path.join(runtimeArtifactInstallPlan.assertDestWithinConfigHome(targetDir, 'skills'), 'gsd-dev-preferences');
|
||||
skillDir = path.join(runtimeArtifactInstallPlan.assertDestWithinConfigHome(targetDir, 'skills'), 'msd-dev-preferences');
|
||||
}
|
||||
return { skillFile: path.join(skillDir, 'SKILL.md'), installRoot, hasHomeOverride };
|
||||
}
|
||||
@@ -678,7 +678,7 @@ function migrateLegacyDevPreferencesToSkill(
|
||||
// Security fix: `existsSync` FOLLOWS symlinks and reports `false` for a
|
||||
// DANGLING one, so the prior `existsSync(skillFile)` check never even saw a
|
||||
// dangling symlink planted AT the leaf (e.g.
|
||||
// `<installRoot>/skills/gsd-dev-preferences/SKILL.md ->
|
||||
// `<installRoot>/skills/msd-dev-preferences/SKILL.md ->
|
||||
// ~/.ssh/authorized_keys`) — it fell through past this "already migrated"
|
||||
// bail, past the symlink-escape guard below (which only walks to `skillDir`,
|
||||
// the parent DIRECTORY, and never lstats the leaf FILE itself), and into
|
||||
@@ -698,10 +698,10 @@ function migrateLegacyDevPreferencesToSkill(
|
||||
}
|
||||
// Symlink-escape guard: reject if any path component between installRoot and
|
||||
// skillDir is a symlink that would redirect writes outside the install root.
|
||||
// #2393: honor GSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
|
||||
// #2393: honor MSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
|
||||
if (hasExistingSymlinkBetween(path.resolve(installRoot), skillDir, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
||||
throw new Error(
|
||||
`migrateLegacyDevPreferencesToSkill: skillDir "${skillDir}" contains a symlink the install root "${installRoot}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`,
|
||||
`migrateLegacyDevPreferencesToSkill: skillDir "${skillDir}" contains a symlink the install root "${installRoot}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with MSD_ALLOW_SYMLINKED_DEST=1.`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
@@ -725,9 +725,9 @@ function migrateLegacyDevPreferencesToSkill(
|
||||
* the whole dir into destDir.
|
||||
* For commands/agents kind: iterate .md files and write them into destDir.
|
||||
* - commands: write as `${prefix}${stem}.md` unless destSubpath already
|
||||
* encodes the GSD namespace as its last segment (e.g. `commands/gsd`), in
|
||||
* encodes the MSD namespace as its last segment (e.g. `commands/msd`), in
|
||||
* which case write as `${stem}.md` (directory IS the namespace).
|
||||
* - agents: write as-is (files already carry their own `gsd-` prefix).
|
||||
* - agents: write as-is (files already carry their own `msd-` prefix).
|
||||
* For kimi-agents kind: recursively copy generated YAML/prompt files.
|
||||
*/
|
||||
function _copyStaged(stagedDir: string, destDir: string, kind: any, configDir: string, runtime?: string): void {
|
||||
@@ -753,10 +753,10 @@ function _copyStaged(stagedDir: string, destDir: string, kind: any, configDir: s
|
||||
const resolvedDest = runtimeArtifactInstallPlan.assertDestWithinConfigHome(installRoot, destDir);
|
||||
// Symlink-escape guard: reject if any path component between the install root and
|
||||
// destDir is a symlink that would redirect writes outside the install root.
|
||||
// #2393: honor GSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
|
||||
// #2393: honor MSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
|
||||
if (hasExistingSymlinkBetween(path.resolve(installRoot), resolvedDest, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
||||
throw new Error(
|
||||
`_copyStaged: destDir "${destDir}" contains a symlink the install root "${installRoot}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`,
|
||||
`_copyStaged: destDir "${destDir}" contains a symlink the install root "${installRoot}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with MSD_ALLOW_SYMLINKED_DEST=1.`,
|
||||
);
|
||||
}
|
||||
// Use the validated absolute path for the actual writes below.
|
||||
@@ -765,7 +765,7 @@ function _copyStaged(stagedDir: string, destDir: string, kind: any, configDir: s
|
||||
installFs().mkdirSync(destDir, { recursive: true });
|
||||
|
||||
if (kind.kind === 'skills') {
|
||||
// Each child of stagedDir is a prefixed skill directory: gsd-help/, etc.
|
||||
// Each child of stagedDir is a prefixed skill directory: msd-help/, etc.
|
||||
for (const entry of installFs().readdirSync(stagedDir, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const src = path.join(stagedDir, entry.name);
|
||||
@@ -783,7 +783,7 @@ function _copyStaged(stagedDir: string, destDir: string, kind: any, configDir: s
|
||||
// commands or agents
|
||||
const entries = installFs().readdirSync(stagedDir, { withFileTypes: true });
|
||||
// For commands: apply prefix unless the destSubpath's last segment already
|
||||
// represents the GSD namespace (e.g. 'commands/gsd' → last segment 'gsd').
|
||||
// represents the MSD namespace (e.g. 'commands/msd' → last segment 'msd').
|
||||
// Single source of truth: runtimeArtifactLayout.isNamespacedByDir (#2871
|
||||
// Phase 2 review finding — this rule previously drifted independently
|
||||
// across install-engine.cts / surface.cts / runtime-artifact-layout.cts).
|
||||
@@ -805,7 +805,7 @@ function _copyStaged(stagedDir: string, destDir: string, kind: any, configDir: s
|
||||
|
||||
let destName: string;
|
||||
if (kind.kind === 'agents') {
|
||||
// Agent files already carry the gsd- prefix in the source dir.
|
||||
// Agent files already carry the msd- prefix in the source dir.
|
||||
// #2099: descriptor-driven via hostBehaviors.agentFileExtension (was
|
||||
// hardcoded `runtime === 'copilot'`). copilot declares '.agent.md';
|
||||
// every other runtime's descriptor leaves this unset, so destName falls
|
||||
@@ -829,26 +829,26 @@ function _copyStaged(stagedDir: string, destDir: string, kind: any, configDir: s
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// _removeGsdEntries
|
||||
// _removeMsdEntries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Remove GSD-prefixed entries from destDir matching kind.prefix.
|
||||
* Remove MSD-prefixed entries from destDir matching kind.prefix.
|
||||
* For the prefix='' case: the destSubpath IS the namespace — remove the entire
|
||||
* destDir. (No current runtime uses prefix='' after #947 reversed Hermes; kept
|
||||
* as a defensive guard for future runtimes.)
|
||||
*/
|
||||
function _removeGsdEntries(destDir: string, kind: any): void {
|
||||
function _removeMsdEntries(destDir: string, kind: any): void {
|
||||
if (!installFs().existsSync(destDir)) return;
|
||||
if (kind.kind === 'kimi-agents') {
|
||||
for (const fileName of ['gsd.yaml', 'gsd.md']) {
|
||||
for (const fileName of ['msd.yaml', 'msd.md']) {
|
||||
installFs().rmSync(path.join(destDir, fileName), { force: true });
|
||||
}
|
||||
const subagentsDir = path.join(destDir, 'subagents');
|
||||
if (installFs().existsSync(subagentsDir)) {
|
||||
for (const entry of installFs().readdirSync(subagentsDir, { withFileTypes: true })) {
|
||||
if (!entry.isFile()) continue;
|
||||
if (!entry.name.startsWith('gsd-')) continue;
|
||||
if (!entry.name.startsWith('msd-')) continue;
|
||||
if (!entry.name.endsWith('.yaml') && !entry.name.endsWith('.md')) continue;
|
||||
installFs().rmSync(path.join(subagentsDir, entry.name), { force: true });
|
||||
}
|
||||
@@ -856,8 +856,8 @@ function _removeGsdEntries(destDir: string, kind: any): void {
|
||||
return;
|
||||
}
|
||||
if (kind.prefix === '') {
|
||||
// Whole-namespace removal (Hermes nested case — destSubpath is skills/gsd)
|
||||
// The directory itself is the GSD namespace, so remove it entirely.
|
||||
// Whole-namespace removal (Hermes nested case — destSubpath is skills/msd)
|
||||
// The directory itself is the MSD namespace, so remove it entirely.
|
||||
installFs().rmSync(destDir, { recursive: true, force: true });
|
||||
return;
|
||||
}
|
||||
@@ -906,28 +906,28 @@ function _restoreDir(dir: string, snapshot: Map<string, Buffer>): void {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* After the layout-driven install loop writes new gsd-<stem>/ dirs to
|
||||
* skills/gsd/, remove any pre-existing bare-stem dirs (skills/gsd/<stem>/)
|
||||
* that correspond to the newly installed gsd-<stem> entries.
|
||||
* After the layout-driven install loop writes new msd-<stem>/ dirs to
|
||||
* skills/msd/, remove any pre-existing bare-stem dirs (skills/msd/<stem>/)
|
||||
* that correspond to the newly installed msd-<stem> entries.
|
||||
*
|
||||
* @param nestedGsdDir absolute path to skills/gsd/ category dir
|
||||
* @param nestedMsdDir absolute path to skills/msd/ category dir
|
||||
*/
|
||||
function _removeHermesBareStemDirs(nestedGsdDir: string): void {
|
||||
if (!installFs().existsSync(nestedGsdDir)) return;
|
||||
const entries = installFs().readdirSync(nestedGsdDir, { withFileTypes: true });
|
||||
function _removeHermesBareStemDirs(nestedMsdDir: string): void {
|
||||
if (!installFs().existsSync(nestedMsdDir)) return;
|
||||
const entries = installFs().readdirSync(nestedMsdDir, { withFileTypes: true });
|
||||
|
||||
// Collect the set of stems that were installed as gsd-<stem>/ this run.
|
||||
// Collect the set of stems that were installed as msd-<stem>/ this run.
|
||||
const installedStems = new Set<string>();
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
|
||||
installedStems.add(entry.name.slice('gsd-'.length)); // e.g. 'quick', 'dev-preferences'
|
||||
if (entry.isDirectory() && entry.name.startsWith('msd-')) {
|
||||
installedStems.add(entry.name.slice('msd-'.length)); // e.g. 'quick', 'dev-preferences'
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any bare <stem>/ dir for which gsd-<stem>/ was just installed.
|
||||
// Remove any bare <stem>/ dir for which msd-<stem>/ was just installed.
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && !entry.name.startsWith('gsd-') && installedStems.has(entry.name)) {
|
||||
installFs().rmSync(path.join(nestedGsdDir, entry.name), { recursive: true });
|
||||
if (entry.isDirectory() && !entry.name.startsWith('msd-') && installedStems.has(entry.name)) {
|
||||
installFs().rmSync(path.join(nestedMsdDir, entry.name), { recursive: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -945,22 +945,22 @@ function _removeHermesBareStemDirs(nestedGsdDir: string): void {
|
||||
* @param scope
|
||||
*/
|
||||
function _runLegacyInstallMigrations(runtime: string, configDir: string, scope: string = 'global'): void {
|
||||
const legacyCommandsGsd = path.join(configDir, 'commands', 'gsd');
|
||||
const legacyCommandsMsd = path.join(configDir, 'commands', 'msd');
|
||||
|
||||
// Claude / Qwen / Hermes: clean up legacy commands/gsd/ and preserve dev-preferences
|
||||
// Claude / Qwen / Hermes: clean up legacy commands/msd/ and preserve dev-preferences
|
||||
// for migration. The actual migration call is deferred to after all layout cleanup so
|
||||
// that for Hermes the flat skills/gsd-*/ removal (below) does not delete the freshly
|
||||
// created skills/gsd-dev-preferences/ skill dir.
|
||||
// that for Hermes the flat skills/msd-*/ removal (below) does not delete the freshly
|
||||
// created skills/msd-dev-preferences/ skill dir.
|
||||
let stagedLegacyArtifacts: ReturnType<typeof userArtifactStaging.stageUserArtifacts> | null = null;
|
||||
if (_hostBehaviors(runtime).legacyCommandsGsdInstallMigration) {
|
||||
if (installFs().existsSync(legacyCommandsGsd)) {
|
||||
if (_hostBehaviors(runtime).legacyCommandsMsdInstallMigration) {
|
||||
if (installFs().existsSync(legacyCommandsMsd)) {
|
||||
// #2875: staging root resolved lazily, only when there is actually
|
||||
// something to stage — reused below by every other call site sharing
|
||||
// this configDir.
|
||||
// #2875 defect fix: DEGRADE, never abort the whole install, when the
|
||||
// staging root itself cannot be resolved (e.g. a hostile/broken
|
||||
// `.gsd-staging` symlink) — skip this legacy-migration block entirely
|
||||
// rather than wipe legacyCommandsGsd without a durable backup (module
|
||||
// `.msd-staging` symlink) — skip this legacy-migration block entirely
|
||||
// rather than wipe legacyCommandsMsd without a durable backup (module
|
||||
// doc "Failure posture": a wipe having staged nothing is worse than no
|
||||
// staging at all). The stale legacy dir is simply left in place for a
|
||||
// future successful run.
|
||||
@@ -970,28 +970,28 @@ function _runLegacyInstallMigrations(runtime: string, configDir: string, scope:
|
||||
// crash anywhere in this function — including the Hermes flat-skills
|
||||
// wipe further down, previously inside the same in-memory-only window
|
||||
// — survives via recoverOrphanedUserArtifacts on the next run.
|
||||
stagedLegacyArtifacts = userArtifactStaging.stageUserArtifacts(legacyCommandsGsd, ['dev-preferences.md'], stagingRoot);
|
||||
installFs().rmSync(legacyCommandsGsd, { recursive: true });
|
||||
stagedLegacyArtifacts = userArtifactStaging.stageUserArtifacts(legacyCommandsMsd, ['dev-preferences.md'], stagingRoot);
|
||||
installFs().rmSync(legacyCommandsMsd, { recursive: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hermes: remove pre-#2841 flat skills/gsd-*/ entries that lived alongside
|
||||
// the new skills/gsd/ nested layout.
|
||||
// Hermes: remove pre-#2841 flat skills/msd-*/ entries that lived alongside
|
||||
// the new skills/msd/ nested layout.
|
||||
if (runtime === 'hermes') {
|
||||
const flatSkillsDir = path.join(configDir, 'skills');
|
||||
if (installFs().existsSync(flatSkillsDir)) {
|
||||
for (const entry of installFs().readdirSync(flatSkillsDir, { withFileTypes: true })) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('msd-')) {
|
||||
installFs().rmSync(path.join(flatSkillsDir, entry.name), { recursive: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hermes: bare-stem skills/gsd/<stem>/ cleanup is deferred to AFTER the
|
||||
// Hermes: bare-stem skills/msd/<stem>/ cleanup is deferred to AFTER the
|
||||
// layout-driven install loop in installRuntimeArtifacts, where the exact set
|
||||
// of staged gsd-<stem>/ dirs is known. Removing here (before staging) would
|
||||
// require readGsdCommandNames() which misses skills like 'dev-preferences'
|
||||
// of staged msd-<stem>/ dirs is known. Removing here (before staging) would
|
||||
// require readMsdCommandNames() which misses skills like 'dev-preferences'
|
||||
// that are not in the commands directory. See _removeHermesBareStemDirs().
|
||||
}
|
||||
|
||||
@@ -1035,7 +1035,7 @@ function _runLegacyInstallMigrations(runtime: string, configDir: string, scope:
|
||||
// BRICKED the command): migrateLegacyDevPreferencesToSkill correctly
|
||||
// THROWS when it finds a planted/dangling symlink at the skill-file leaf
|
||||
// (security fix — refusing to write through it is correct) but by this
|
||||
// point legacyCommandsGsd has ALREADY been wiped (rmSync above) and
|
||||
// point legacyCommandsMsd has ALREADY been wiped (rmSync above) and
|
||||
// stagedLegacyArtifacts is the only surviving copy. An unguarded throw
|
||||
// here propagated straight out of installRuntimeArtifacts, aborting the
|
||||
// whole install/uninstall WITHOUT ever reaching the restore-or-discard
|
||||
@@ -1043,7 +1043,7 @@ function _runLegacyInstallMigrations(runtime: string, configDir: string, scope:
|
||||
// hit the same throw again (same brick-the-command failure mode this
|
||||
// module's "DEGRADE, never abort" posture, see
|
||||
// _tryResolveUserArtifactStagingRoot above, already closed for a broken
|
||||
// `.gsd-staging` path). Degrade identically: catch, warn once, and treat
|
||||
// `.msd-staging` path). Degrade identically: catch, warn once, and treat
|
||||
// the batch as unmigrated so the restore branch below fires.
|
||||
let migrated = false;
|
||||
let migrationRefused = false;
|
||||
@@ -1051,7 +1051,7 @@ function _runLegacyInstallMigrations(runtime: string, configDir: string, scope:
|
||||
migrated = migrateLegacyDevPreferencesToSkill(configDir, savedLegacyArtifacts, runtime, scope);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
` [gsd] dev-preferences.md migration skipped for "${configDir}" (${(err as Error).message}) — restoring the legacy copy instead.`,
|
||||
` [msd] dev-preferences.md migration skipped for "${configDir}" (${(err as Error).message}) — restoring the legacy copy instead.`,
|
||||
);
|
||||
migrationRefused = true;
|
||||
}
|
||||
@@ -1078,8 +1078,8 @@ function _runLegacyInstallMigrations(runtime: string, configDir: string, scope:
|
||||
if (!nothingLeftUnmigrated && stagedLegacyArtifacts.names.length > 0) {
|
||||
// Put the whole batch back where it came from rather than losing
|
||||
// whatever migration did not (or could not) account for.
|
||||
installFs().mkdirSync(legacyCommandsGsd, { recursive: true });
|
||||
userArtifactStaging.restoreStagedUserArtifacts(legacyCommandsGsd, stagedLegacyArtifacts);
|
||||
installFs().mkdirSync(legacyCommandsMsd, { recursive: true });
|
||||
userArtifactStaging.restoreStagedUserArtifacts(legacyCommandsMsd, stagedLegacyArtifacts);
|
||||
}
|
||||
userArtifactStaging.discardStagedUserArtifacts(stagedLegacyArtifacts);
|
||||
}
|
||||
@@ -1095,16 +1095,16 @@ function _runLegacyInstallMigrations(runtime: string, configDir: string, scope:
|
||||
* @returns staged legacy artifacts for post-removal migration, or null
|
||||
*/
|
||||
function _runLegacyUninstallCleanup(runtime: string, configDir: string, scope: string = 'global'): ReturnType<typeof userArtifactStaging.stageUserArtifacts> | null {
|
||||
// commands/gsd/ is a legacy location for Qwen, Hermes, and all Claude installs.
|
||||
// Prior to #1367 fix, Claude-local used commands/gsd/<cmd>.md (colon-namespaced).
|
||||
// After #1367, Claude-local uses flat commands/gsd-<cmd>.md. The inline uninstall
|
||||
// commands/msd/ is a legacy location for Qwen, Hermes, and all Claude installs.
|
||||
// Prior to #1367 fix, Claude-local used commands/msd/<cmd>.md (colon-namespaced).
|
||||
// After #1367, Claude-local uses flat commands/msd-<cmd>.md. The inline uninstall
|
||||
// block (1c) handles removal of flat files; this function handles the legacy
|
||||
// commands/gsd/ directory for all Claude scopes (global was already included,
|
||||
// commands/msd/ directory for all Claude scopes (global was already included,
|
||||
// local is now added since that layout is also legacy post-#1367).
|
||||
// #2973 / Codex review (bd1f06c9): preserve user-owned dev-preferences.md
|
||||
// before destructive wipe. Migration to skills/gsd-dev-preferences/SKILL.md
|
||||
// before destructive wipe. Migration to skills/msd-dev-preferences/SKILL.md
|
||||
// is deferred and returned so the caller can apply it AFTER layout-driven
|
||||
// removal — this prevents the layout's gsd-* prefix removal from wiping the
|
||||
// removal — this prevents the layout's msd-* prefix removal from wiping the
|
||||
// freshly created skill dir (same pattern as _runLegacyInstallMigrations).
|
||||
// #2875 (#1874-F19): staged DURABLY to disk (userArtifactStaging), not just
|
||||
// an in-memory Map — this function's own wipe below is raw `fs`, left
|
||||
@@ -1114,12 +1114,12 @@ function _runLegacyUninstallCleanup(runtime: string, configDir: string, scope: s
|
||||
// module does (ambient default: real fs here, since this call is never
|
||||
// wrapped in withInstallFs).
|
||||
let stagedLegacyArtifacts: ReturnType<typeof userArtifactStaging.stageUserArtifacts> | null = null;
|
||||
// commands/gsd/ is a legacy location for Qwen, Hermes, and Claude global.
|
||||
// commands/msd/ is a legacy location for Qwen, Hermes, and Claude global.
|
||||
// Claude local is intentionally excluded: the inline uninstall block (1c) handles
|
||||
// commands/gsd/ for claude local, preserving dev-preferences.md by restoring it
|
||||
// commands/msd/ for claude local, preserving dev-preferences.md by restoring it
|
||||
// to the same location (#1423). Using migrateLegacyDevPreferencesToSkill here
|
||||
// (which would redirect to skills/) conflicts with the test contract for local installs.
|
||||
const _lu = _hostBehaviors(runtime).legacyCommandsGsdUninstall;
|
||||
const _lu = _hostBehaviors(runtime).legacyCommandsMsdUninstall;
|
||||
// #2870: `scope` keeps its exported `string = 'global'` signature (no
|
||||
// signature change), but every real caller — `uninstallRuntimeArtifacts`'s
|
||||
// own required `scope` param, always fed a validated 'global' | 'local'
|
||||
@@ -1128,42 +1128,42 @@ function _runLegacyUninstallCleanup(runtime: string, configDir: string, scope: s
|
||||
// `= 'global'` default already reproduces today's behavior for an omitted
|
||||
// scope, so the cast below is safe: `isGlobalScope` never sees a value
|
||||
// outside its union here.
|
||||
const isLegacyCommandsGsd = _lu === true || (_lu === 'global' && isGlobalScope(scope as InstallScope));
|
||||
if (isLegacyCommandsGsd) {
|
||||
const legacyCommandsGsd = path.join(configDir, 'commands', 'gsd');
|
||||
if (fs.existsSync(legacyCommandsGsd)) {
|
||||
const isLegacyCommandsMsd = _lu === true || (_lu === 'global' && isGlobalScope(scope as InstallScope));
|
||||
if (isLegacyCommandsMsd) {
|
||||
const legacyCommandsMsd = path.join(configDir, 'commands', 'msd');
|
||||
if (fs.existsSync(legacyCommandsMsd)) {
|
||||
// #2875 defect fix: DEGRADE, never abort uninstall, when the staging
|
||||
// root cannot be resolved — skip this legacy-cleanup block (leave the
|
||||
// stale dir in place) rather than wipe without a durable backup.
|
||||
// Uninstall in particular must always be able to proceed past this
|
||||
// point regardless of a hostile/broken `.gsd-staging` path.
|
||||
// point regardless of a hostile/broken `.msd-staging` path.
|
||||
const stagingRoot = _tryResolveUserArtifactStagingRoot(configDir);
|
||||
if (stagingRoot !== null) {
|
||||
stagedLegacyArtifacts = userArtifactStaging.stageUserArtifacts(legacyCommandsGsd, ['dev-preferences.md'], stagingRoot);
|
||||
fs.rmSync(legacyCommandsGsd, { recursive: true });
|
||||
stagedLegacyArtifacts = userArtifactStaging.stageUserArtifacts(legacyCommandsMsd, ['dev-preferences.md'], stagingRoot);
|
||||
fs.rmSync(legacyCommandsMsd, { recursive: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hermes: pre-#2841 flat skills/gsd-*/ entries
|
||||
// Hermes: pre-#2841 flat skills/msd-*/ entries
|
||||
if (runtime === 'hermes') {
|
||||
const flatSkillsDir = path.join(configDir, 'skills');
|
||||
if (fs.existsSync(flatSkillsDir)) {
|
||||
for (const entry of fs.readdirSync(flatSkillsDir, { withFileTypes: true })) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('msd-')) {
|
||||
fs.rmSync(path.join(flatSkillsDir, entry.name), { recursive: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hermes: pre-#947 bare-stem skills/gsd/<stem>/ entries (dirs that do NOT
|
||||
// start with 'gsd-') — the #3664 layout used prefix='' so GSD-owned skills
|
||||
// had bare names (e.g. skills/gsd/help/). These are stale on uninstall.
|
||||
const nestedGsdDirForUninstall = path.join(configDir, 'skills', 'gsd');
|
||||
if (fs.existsSync(nestedGsdDirForUninstall)) {
|
||||
for (const entry of fs.readdirSync(nestedGsdDirForUninstall, { withFileTypes: true })) {
|
||||
if (entry.isDirectory() && !entry.name.startsWith('gsd-')) {
|
||||
fs.rmSync(path.join(nestedGsdDirForUninstall, entry.name), { recursive: true });
|
||||
// Hermes: pre-#947 bare-stem skills/msd/<stem>/ entries (dirs that do NOT
|
||||
// start with 'msd-') — the #3664 layout used prefix='' so MSD-owned skills
|
||||
// had bare names (e.g. skills/msd/help/). These are stale on uninstall.
|
||||
const nestedMsdDirForUninstall = path.join(configDir, 'skills', 'msd');
|
||||
if (fs.existsSync(nestedMsdDirForUninstall)) {
|
||||
for (const entry of fs.readdirSync(nestedMsdDirForUninstall, { withFileTypes: true })) {
|
||||
if (entry.isDirectory() && !entry.name.startsWith('msd-')) {
|
||||
fs.rmSync(path.join(nestedMsdDirForUninstall, entry.name), { recursive: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1324,17 +1324,17 @@ function installRuntimeArtifacts(
|
||||
// were never real to begin with; it cannot make this `if` pass for a
|
||||
// path the real filesystem would refuse.
|
||||
const installRoot = (kind && typeof kind.home === 'string' && kind.home !== '') ? kind.home : configDir;
|
||||
// #2393: honor GSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
|
||||
// #2393: honor MSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
|
||||
// Threat model from #1704 / ADR-1239 Phase B preserved: path-traversal and
|
||||
// resolved-target-equals-root still refuse regardless of opt-in.
|
||||
if (hasExistingSymlinkBetween(path.resolve(installRoot), dest, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
||||
throw new Error(
|
||||
`installRuntimeArtifacts: destDir "${dest}" contains a symlink the install root "${installRoot}" does not trust — refusing to create. If this is an intentional user-owned symlink layout (e.g. externalized skills/hooks dir, multi-account configHome, or a dotfiles-managed configHome), re-run with GSD_ALLOW_SYMLINKED_DEST=1.`,
|
||||
`installRuntimeArtifacts: destDir "${dest}" contains a symlink the install root "${installRoot}" does not trust — refusing to create. If this is an intentional user-owned symlink layout (e.g. externalized skills/hooks dir, multi-account configHome, or a dotfiles-managed configHome), re-run with MSD_ALLOW_SYMLINKED_DEST=1.`,
|
||||
);
|
||||
}
|
||||
// #2875 defect fix (--minimal regression closed): a restricted profile
|
||||
// (e.g. --minimal) can legitimately stage ZERO agents — no skill in
|
||||
// the profile's closure references a gsd-* role. The pre-#2875-Part-2
|
||||
// the profile's closure references a msd-* role. The pre-#2875-Part-2
|
||||
// inline agent-staging loop this generic layout loop's agents handling
|
||||
// replaced never created `agents/` at all under a minimal install (the
|
||||
// now-deleted `isMinimalMode` branch skipped the whole step); this
|
||||
@@ -1356,16 +1356,16 @@ function installRuntimeArtifacts(
|
||||
// above `installAgentsKindStandalone`) skipped this kind's stale-
|
||||
// agent prune along with the write whenever a restricted profile
|
||||
// (e.g. --minimal) staged zero agents — that also skipped
|
||||
// `_removeGsdEntries`, so a full -> minimal downgrade left every
|
||||
// previously-installed gsd-*.md/.toml agent file in place. The
|
||||
// `_removeMsdEntries`, so a full -> minimal downgrade left every
|
||||
// previously-installed msd-*.md/.toml agent file in place. The
|
||||
// deleted pre-#2875 inline loop never did that: its stale-cleanup
|
||||
// pre-pass ran UNCONDITIONALLY, and only the *write* of new agent
|
||||
// files was gated on minimal mode. Restore that split here: prune
|
||||
// first (no-ops via `_removeGsdEntries`'s own existsSync check when
|
||||
// first (no-ops via `_removeMsdEntries`'s own existsSync check when
|
||||
// `dest` was never created, so a fresh install with nothing staged
|
||||
// still never creates it below), then skip mkdir/copy when there is
|
||||
// nothing to write.
|
||||
_removeGsdEntries(dest, kind);
|
||||
_removeMsdEntries(dest, kind);
|
||||
if (stagedAgentFiles.length === 0) {
|
||||
continue;
|
||||
}
|
||||
@@ -1373,21 +1373,21 @@ function installRuntimeArtifacts(
|
||||
installFs().mkdirSync(dest, { recursive: true });
|
||||
const preserved: string[] = [];
|
||||
if (kind.kind === 'skills' && installFs().existsSync(dest)) {
|
||||
// Pre-prune: snapshot user-owned content before _removeGsdEntries wipes it,
|
||||
// Pre-prune: snapshot user-owned content before _removeMsdEntries wipes it,
|
||||
// then restore after. This preserves user dirs across a wipe-and-replace
|
||||
// install (#2973 / #3664).
|
||||
//
|
||||
// All runtimes (incl. Hermes after #947) use prefix='gsd-'.
|
||||
// _removeGsdEntries removes only gsd-* entries; non-gsd-* user dirs are
|
||||
// untouched. Preserve the explicit user-owned GSD-prefixed skill
|
||||
// gsd-dev-preferences, which GSD does not reinstall from source but must
|
||||
// All runtimes (incl. Hermes after #947) use prefix='msd-'.
|
||||
// _removeMsdEntries removes only msd-* entries; non-msd-* user dirs are
|
||||
// untouched. Preserve the explicit user-owned MSD-prefixed skill
|
||||
// msd-dev-preferences, which MSD does not reinstall from source but must
|
||||
// survive the prune (#2973).
|
||||
const toPreserve = new Map<string, Map<string, Buffer>>(); // dirName -> Map<relPath, Buffer>
|
||||
|
||||
{
|
||||
// Preserve explicitly user-owned GSD-prefixed skill dirs.
|
||||
// gsd-dev-preferences is the sole user-customisable skill in this category.
|
||||
const USER_OWNED_SKILL_DIRS = ['gsd-dev-preferences'];
|
||||
// Preserve explicitly user-owned MSD-prefixed skill dirs.
|
||||
// msd-dev-preferences is the sole user-customisable skill in this category.
|
||||
const USER_OWNED_SKILL_DIRS = ['msd-dev-preferences'];
|
||||
for (const dirName of USER_OWNED_SKILL_DIRS) {
|
||||
const skillDir = path.join(dest, dirName);
|
||||
if (!installFs().existsSync(skillDir)) continue;
|
||||
@@ -1396,7 +1396,7 @@ function installRuntimeArtifacts(
|
||||
}
|
||||
}
|
||||
|
||||
_removeGsdEntries(dest, kind);
|
||||
_removeMsdEntries(dest, kind);
|
||||
_copyStaged(item.sourceDir, dest, kind, configDir, runtime);
|
||||
|
||||
// Restore user-owned dirs after the prune+copy
|
||||
@@ -1406,8 +1406,8 @@ function installRuntimeArtifacts(
|
||||
}
|
||||
} else {
|
||||
// For non-skills kinds (commands, agents): no user content to preserve;
|
||||
// just prune stale gsd-* entries and copy new ones.
|
||||
_removeGsdEntries(dest, kind);
|
||||
// just prune stale msd-* entries and copy new ones.
|
||||
_removeMsdEntries(dest, kind);
|
||||
_copyStaged(item.sourceDir, dest, kind, configDir, runtime);
|
||||
}
|
||||
executedKinds.push({ kind: item.kind, sourceDir: item.sourceDir, destDir: dest, preserved });
|
||||
@@ -1427,20 +1427,20 @@ function installRuntimeArtifacts(
|
||||
}
|
||||
}
|
||||
|
||||
// Hermes: after the install loop has written all gsd-<stem>/ dirs to
|
||||
// skills/gsd/, remove any stale bare-stem dirs (skills/gsd/<stem>/) that
|
||||
// correspond to the newly installed gsd-<stem> entries. This is the robust
|
||||
// replacement for the readGsdCommandNames()-based pre-install cleanup that
|
||||
// Hermes: after the install loop has written all msd-<stem>/ dirs to
|
||||
// skills/msd/, remove any stale bare-stem dirs (skills/msd/<stem>/) that
|
||||
// correspond to the newly installed msd-<stem> entries. This is the robust
|
||||
// replacement for the readMsdCommandNames()-based pre-install cleanup that
|
||||
// missed skills like 'dev-preferences' (#947 adversarial review).
|
||||
//
|
||||
// We run this AFTER the install loop so the installed set is authoritative:
|
||||
// every gsd-<stem>/ present now was written this run (or was there before
|
||||
// with the same prefix). User-owned bare dirs with no gsd-<stem> counterpart
|
||||
// every msd-<stem>/ present now was written this run (or was there before
|
||||
// with the same prefix). User-owned bare dirs with no msd-<stem> counterpart
|
||||
// are untouched.
|
||||
let hermesBareStemCleanup = false;
|
||||
if (runtime === 'hermes') {
|
||||
const nestedGsdDirForCleanup = path.join(configDir, 'skills', 'gsd');
|
||||
_removeHermesBareStemDirs(nestedGsdDirForCleanup);
|
||||
const nestedMsdDirForCleanup = path.join(configDir, 'skills', 'msd');
|
||||
_removeHermesBareStemDirs(nestedMsdDirForCleanup);
|
||||
hermesBareStemCleanup = true;
|
||||
}
|
||||
|
||||
@@ -1450,7 +1450,7 @@ function installRuntimeArtifacts(
|
||||
// need their declared hostBehaviors.nativePlugin file copied into configDir.
|
||||
// findInstallSourceRoot resolves the repo/package root independent of
|
||||
// configDir contents (marker check, then a walk-up from __dirname), so this
|
||||
// is safe even when configDir has no .gsd-source marker (artifactLayout: []).
|
||||
// is safe even when configDir has no .msd-source marker (artifactLayout: []).
|
||||
let nativePluginInstalled = false;
|
||||
if (behaviors.nativePlugin) {
|
||||
// Native plugin sources live only in the executing package, never in the
|
||||
@@ -1502,7 +1502,7 @@ function installRuntimeArtifacts(
|
||||
* this bespoke OpenCode/Kilo writer never called. Absent -> no third-party
|
||||
* skills staged (fail closed), matching the seam's own optional-registry
|
||||
* contract.
|
||||
* @returns number of gsd-* skill directories written
|
||||
* @returns number of msd-* skill directories written
|
||||
*/
|
||||
function installOpencodeFamilySkills(
|
||||
runtime: string,
|
||||
@@ -1549,20 +1549,20 @@ function installOpencodeFamilySkills(
|
||||
const dest = runtimeArtifactInstallPlan.assertDestWithinConfigHome(installRoot, skillsKindEntry.destSubpath);
|
||||
// Symlink-escape guard: reject if any path component between installRoot and
|
||||
// dest is a symlink that would redirect writes outside the install root.
|
||||
// #2393: honor GSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
|
||||
// #2393: honor MSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
|
||||
if (hasExistingSymlinkBetween(path.resolve(installRoot), dest, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
||||
throw new Error(
|
||||
`installOpencodeFamilySkills: destDir "${dest}" contains a symlink the install root "${installRoot}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`,
|
||||
`installOpencodeFamilySkills: destDir "${dest}" contains a symlink the install root "${installRoot}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with MSD_ALLOW_SYMLINKED_DEST=1.`,
|
||||
);
|
||||
}
|
||||
installFs().mkdirSync(dest, { recursive: true });
|
||||
|
||||
// Preserve user-owned GSD-prefixed skill dirs across the gsd-* prune.
|
||||
// gsd-dev-preferences is generated by the user (via generate-dev-preferences)
|
||||
// and lives at <configDir>/skills/gsd-dev-preferences — _removeGsdEntries
|
||||
// Preserve user-owned MSD-prefixed skill dirs across the msd-* prune.
|
||||
// msd-dev-preferences is generated by the user (via generate-dev-preferences)
|
||||
// and lives at <configDir>/skills/msd-dev-preferences — _removeMsdEntries
|
||||
// would otherwise wipe it. Mirrors the preservation in installRuntimeArtifacts
|
||||
// (#2973).
|
||||
const USER_OWNED_SKILL_DIRS = ['gsd-dev-preferences'];
|
||||
const USER_OWNED_SKILL_DIRS = ['msd-dev-preferences'];
|
||||
const toPreserve = new Map<string, Map<string, Buffer>>(); // dirName -> Map<relPath, Buffer>
|
||||
for (const dirName of USER_OWNED_SKILL_DIRS) {
|
||||
const skillDir = path.join(dest, dirName);
|
||||
@@ -1571,7 +1571,7 @@ function installOpencodeFamilySkills(
|
||||
if (snap.size > 0) toPreserve.set(dirName, snap);
|
||||
}
|
||||
|
||||
_removeGsdEntries(dest, skillsKindEntry);
|
||||
_removeMsdEntries(dest, skillsKindEntry);
|
||||
|
||||
let count = 0;
|
||||
const firstPartyStems = new Set<string>();
|
||||
@@ -1665,7 +1665,7 @@ function installOpencodeFamilySkills(
|
||||
* via its OWN bespoke writers and never called `resolveRuntimeArtifactLayout`
|
||||
* for agents at all before this function existed. Declaring a
|
||||
* `capability.json` `agents` entry for them without this would be inert
|
||||
* on the real install path while live on `/gsd:surface` (#1879-F15).
|
||||
* on the real install path while live on `/msd:surface` (#1879-F15).
|
||||
* 2. **Claude local** (`bin/install.js`'s `install()`, `_isSkillsRuntime ===
|
||||
* false` branch) — `hostBehaviors.localInstallStyle === 'legacy-flat'`
|
||||
* routes claude-local's commands/skills through `copyWithPathReplacement`
|
||||
@@ -1713,7 +1713,7 @@ function installAgentsKindStandalone(
|
||||
const agentsKindEntry = layout.kinds.find((kind) => kind.kind === 'agents');
|
||||
if (!agentsKindEntry) return null;
|
||||
// #3712: this writer selects `agentsKindEntry.home` over targetDir below and then
|
||||
// prunes that destination via _removeGsdEntries, so it is a fifth route into the
|
||||
// prunes that destination via _removeMsdEntries, so it is a fifth route into the
|
||||
// developer's real home. No agents kind declares a `home` override today, so like
|
||||
// installOpencodeFamilySkills it is guarded against a descriptor change rather
|
||||
// than a present escape. Scoped to the AGENTS kind alone: passing the whole
|
||||
@@ -1741,28 +1741,28 @@ function installAgentsKindStandalone(
|
||||
const installRoot: string = (typeof agentsKindEntry.home === 'string' && agentsKindEntry.home !== '') ? agentsKindEntry.home : targetDir;
|
||||
const dest = runtimeArtifactInstallPlan.assertDestWithinConfigHome(installRoot, agentsKindEntry.destSubpath);
|
||||
// Symlink-escape guard — same gate _copyStaged/installOpencodeFamilySkills apply
|
||||
// to their own writes (#2393 GSD_ALLOW_SYMLINKED_DEST opt-in preserved). Runs
|
||||
// to their own writes (#2393 MSD_ALLOW_SYMLINKED_DEST opt-in preserved). Runs
|
||||
// even when nothing will be written this call — the stale-agent prune below
|
||||
// (`_removeGsdEntries`) still touches `dest` whenever it already exists.
|
||||
// (`_removeMsdEntries`) still touches `dest` whenever it already exists.
|
||||
if (hasExistingSymlinkBetween(path.resolve(installRoot), dest, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
||||
throw new Error(
|
||||
`installAgentsKindStandalone: destDir "${dest}" contains a symlink the install root "${installRoot}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`,
|
||||
`installAgentsKindStandalone: destDir "${dest}" contains a symlink the install root "${installRoot}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with MSD_ALLOW_SYMLINKED_DEST=1.`,
|
||||
);
|
||||
}
|
||||
|
||||
// #2875 defect fix, corrected: the ORIGINAL fix returned `null` (no-op)
|
||||
// whenever a restricted profile (e.g. --minimal) staged ZERO agents,
|
||||
// which — because that early return sat ABOVE the prune call — also
|
||||
// skipped `_removeGsdEntries`, leaving every previously-installed
|
||||
// gsd-*.md/.toml agent file in place on a full -> minimal downgrade. The
|
||||
// skipped `_removeMsdEntries`, leaving every previously-installed
|
||||
// msd-*.md/.toml agent file in place on a full -> minimal downgrade. The
|
||||
// deleted pre-#2875 inline loop never did that: its stale-cleanup pre-pass
|
||||
// ran UNCONDITIONALLY (removing gsd-*.md, plus .toml for codex), and only
|
||||
// ran UNCONDITIONALLY (removing msd-*.md, plus .toml for codex), and only
|
||||
// the *write* of new agent files was gated on minimal mode. Restore that
|
||||
// split: prune first — a no-op via `_removeGsdEntries`'s own existsSync
|
||||
// split: prune first — a no-op via `_removeMsdEntries`'s own existsSync
|
||||
// check when `dest` was never created, so a fresh install with nothing
|
||||
// staged still never creates it below — then skip mkdir/copy (and return
|
||||
// `null`, matching the doc comment above) when there is nothing to write.
|
||||
_removeGsdEntries(dest, agentsKindEntry);
|
||||
_removeMsdEntries(dest, agentsKindEntry);
|
||||
if (stagedAgentFiles.length === 0) return null;
|
||||
|
||||
installFs().mkdirSync(dest, { recursive: true });
|
||||
@@ -1777,7 +1777,7 @@ function installAgentsKindStandalone(
|
||||
|
||||
/**
|
||||
* Install the flattened commands surface for an OpenCode-family runtime
|
||||
* (OpenCode/Kilo): commands/gsd/**\/*.md -> command/gsd-<...>.md, with
|
||||
* (OpenCode/Kilo): commands/msd/**\/*.md -> command/msd-<...>.md, with
|
||||
* per-runtime frontmatter conversion and path-prefix/attribution rewrites.
|
||||
*
|
||||
* Mirrors bin/install.js's copyFlattenedCommands VERBATIM (ADR-1239 /
|
||||
@@ -1786,10 +1786,10 @@ function installAgentsKindStandalone(
|
||||
*
|
||||
* @param runtime - 'opencode' or 'kilo'
|
||||
* @param destDir - destination directory for flattened commands (recurses with the same destDir)
|
||||
* @param srcDir - source directory to walk (commands/gsd/, recursing into subdirectories)
|
||||
* @param srcDir - source directory to walk (commands/msd/, recursing into subdirectories)
|
||||
* @param pathPrefix - computed config-path prefix for body rewrites
|
||||
* @param resolveAttribution - injection: (runtime) => attribution string | undefined
|
||||
* @param prefix - filename prefix accumulator (defaults to 'gsd'; grows on recursion)
|
||||
* @param prefix - filename prefix accumulator (defaults to 'msd'; grows on recursion)
|
||||
*/
|
||||
function installOpencodeFamilyCommands(
|
||||
runtime: string,
|
||||
@@ -1797,11 +1797,11 @@ function installOpencodeFamilyCommands(
|
||||
srcDir: string,
|
||||
pathPrefix: string,
|
||||
resolveAttribution: ResolveAttribution = () => undefined,
|
||||
prefix: string = 'gsd',
|
||||
prefix: string = 'msd',
|
||||
): void {
|
||||
if (!installFs().existsSync(srcDir)) return;
|
||||
|
||||
// Remove old gsd-*.md files before copying new ones
|
||||
// Remove old msd-*.md files before copying new ones
|
||||
if (installFs().existsSync(destDir)) {
|
||||
for (const file of installFs().readdirSync(destDir)) {
|
||||
if (file.startsWith(`${prefix}-`) && file.endsWith('.md')) installFs().unlinkSync(path.join(destDir, file));
|
||||
@@ -1857,7 +1857,7 @@ function installOpencodeFamilyCommands(
|
||||
* @param runtime - canonical runtime id (only used for the assertDestWithinConfigHome guard)
|
||||
* @param configDir - resolved runtime config directory
|
||||
* @param behaviors - the runtime's hostBehaviors descriptor
|
||||
* @param src - repo/package root (two levels up from the commands/gsd source dir)
|
||||
* @param src - repo/package root (two levels up from the commands/msd source dir)
|
||||
*/
|
||||
function _installNativePluginIfDeclared(
|
||||
runtime: string,
|
||||
@@ -1891,14 +1891,14 @@ function _installNativePluginIfDeclared(
|
||||
// plugin discovery: OpenCode auto-discovers `plugins/*.{ts,js}` and pi's
|
||||
// isExtensionFile() accepts only `.ts`/`.js` (see installer-migration
|
||||
// 006), so a package.json here is never treated as a plugin. Never
|
||||
// written over a package.json GSD does not own — but when one is already
|
||||
// written over a package.json MSD does not own — but when one is already
|
||||
// there, say so: the adapter is CommonJS and will not load under a
|
||||
// foreign `"type": "module"`, and a silent no-op would leave every guard
|
||||
// the adapter spawns dead with no diagnostic (the #2305 failure shape).
|
||||
const markerOutcome = ensureCommonJsMarker(path.dirname(destPath));
|
||||
if (markerOutcome === 'preserved-foreign') {
|
||||
console.warn(
|
||||
` ⚠ ${np.dir}/package.json is not GSD's CommonJS marker — left untouched. `
|
||||
` ⚠ ${np.dir}/package.json is not MSD's CommonJS marker — left untouched. `
|
||||
+ `If it declares "type": "module", ${np.file} will not load.`,
|
||||
);
|
||||
} else if (markerOutcome === 'failed') {
|
||||
@@ -1930,7 +1930,7 @@ function _installNativePluginIfDeclared(
|
||||
* generic branch, which combined-family runtimes otherwise skip entirely).
|
||||
*
|
||||
* Ownership safety mirrors installer-migrations 003
|
||||
* (rename-get-shit-done-to-gsd-core): only files present, and unchanged or
|
||||
* (rename-get-shit-done-to-msd-core): only files present, and unchanged or
|
||||
* locally modified, in the PRIOR install manifest under the legacy
|
||||
* `command/<file>` key are removed here — the materialization call
|
||||
* immediately following writes the current command set fresh into the new
|
||||
@@ -1976,7 +1976,7 @@ function _migrateLegacyOpencodeCommandDir(runtime: string, configDir: string, be
|
||||
return;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
// command/ is a flat directory of gsd-*.md files; skip anything that
|
||||
// command/ is a flat directory of msd-*.md files; skip anything that
|
||||
// isn't a plain file (nested dirs, symlinks) rather than guess intent.
|
||||
if (!entry.isFile()) continue;
|
||||
const relPath = `${LEGACY_NAME}/${entry.name}`;
|
||||
@@ -1984,7 +1984,7 @@ function _migrateLegacyOpencodeCommandDir(runtime: string, configDir: string, be
|
||||
if (classification === 'managed-pristine' || classification === 'managed-modified') {
|
||||
try { installFs().unlinkSync(path.join(legacyDir, entry.name)); } catch { /* best-effort */ }
|
||||
}
|
||||
// 'unknown' (not manifest-tracked) is left untouched — GSD cannot prove
|
||||
// 'unknown' (not manifest-tracked) is left untouched — MSD cannot prove
|
||||
// ownership, so it must never be deleted as collateral damage.
|
||||
}
|
||||
|
||||
@@ -2038,14 +2038,14 @@ function installOpencodeFamilyArtifacts(
|
||||
// ternary (both real call sites and every test call site), so the cast is
|
||||
// safe: `isGlobalScope` never sees a value outside its union here.
|
||||
const isGlobal = isGlobalScope(scope as InstallScope);
|
||||
// findInstallSourceRoot resolves DIRECTLY to the commands/gsd source dir
|
||||
// (via the .gsd-source marker or a walk-up from __dirname) — every other
|
||||
// findInstallSourceRoot resolves DIRECTLY to the commands/msd source dir
|
||||
// (via the .msd-source marker or a walk-up from __dirname) — every other
|
||||
// call site in runtime-artifact-layout.cts feeds its return value straight
|
||||
// into stageSkillsForProfile/stageSkillsForRuntimeAsSkills. The repo/package
|
||||
// root (needed below for the native plugin source) is two levels up.
|
||||
const commandsGsdDir = runtimeArtifactLayout.findInstallSourceRoot(configDir);
|
||||
const commandsMsdDir = runtimeArtifactLayout.findInstallSourceRoot(configDir);
|
||||
const src = packageRoot ?? executingPackageRoot();
|
||||
const rawCommandsDir = installProfiles.stageSkillsForProfile(commandsGsdDir, resolvedProfile);
|
||||
const rawCommandsDir = installProfiles.stageSkillsForProfile(commandsMsdDir, resolvedProfile);
|
||||
|
||||
const pathPrefix = (runtimeArtifactConversion as any)._computePathPrefix({
|
||||
isGlobal,
|
||||
@@ -2104,7 +2104,7 @@ function installOpencodeFamilyArtifacts(
|
||||
/**
|
||||
* Layout-driven uninstall orchestrator.
|
||||
* Runs legacy cleanup first, then uses resolveRuntimeArtifactLayout to
|
||||
* determine which GSD-owned entries to remove.
|
||||
* determine which MSD-owned entries to remove.
|
||||
*
|
||||
* @param runtime canonical runtime ID
|
||||
* @param configDir resolved runtime config directory
|
||||
@@ -2123,14 +2123,14 @@ function uninstallRuntimeArtifacts(
|
||||
retiredArtifactCleanup.pruneRetiredRuntimeArtifacts(runtime, configDir);
|
||||
|
||||
// Legacy cleanup before layout-driven removal (scope-aware to avoid
|
||||
// removing Claude local commands/gsd/ which is the primary install dir).
|
||||
// removing Claude local commands/msd/ which is the primary install dir).
|
||||
// Returns staged user artifacts so we can migrate AFTER layout removal
|
||||
// (the layout's gsd-* prefix pass would wipe a skill dir created here).
|
||||
// (the layout's msd-* prefix pass would wipe a skill dir created here).
|
||||
const stagedLegacyArtifacts = _runLegacyUninstallCleanup(runtime, configDir, scope);
|
||||
|
||||
const layout: any = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, configDir, scope as any);
|
||||
// #3712: uninstall resolves the SAME `kind.home` override as install and then
|
||||
// prunes it via _removeGsdEntries below, so it is a second escape route into
|
||||
// prunes it via _removeMsdEntries below, so it is a second escape route into
|
||||
// the developer's real home, not a read-only path. Guard it identically.
|
||||
testHomeGuard.assertTestHomeSandboxed('uninstallRuntimeArtifacts', runtime, layout?.kinds, {
|
||||
os: deps.os, env: deps.env,
|
||||
@@ -2142,29 +2142,29 @@ function uninstallRuntimeArtifacts(
|
||||
if (!kind) {
|
||||
throw new Error(`Runtime artifact uninstall plan referenced unknown kind: ${item.kind}`);
|
||||
}
|
||||
_removeGsdEntries(item.destDir, kind);
|
||||
_removeMsdEntries(item.destDir, kind);
|
||||
}
|
||||
|
||||
// Hermes: after removing gsd-* skill dirs from skills/gsd/, also remove
|
||||
// the GSD-managed DESCRIPTION.md and then the category dir itself if it
|
||||
// contains no user content (#947). _removeGsdEntries removed gsd-* dirs
|
||||
// Hermes: after removing msd-* skill dirs from skills/msd/, also remove
|
||||
// the MSD-managed DESCRIPTION.md and then the category dir itself if it
|
||||
// contains no user content (#947). _removeMsdEntries removed msd-* dirs
|
||||
// but left the category container and DESCRIPTION.md intact.
|
||||
if (runtime === 'hermes') {
|
||||
const nestedGsdDir = path.join(configDir, 'skills', 'gsd');
|
||||
if (fs.existsSync(nestedGsdDir)) {
|
||||
// Remove GSD-owned DESCRIPTION.md (written by writeHermesCategoryDescription)
|
||||
fs.rmSync(path.join(nestedGsdDir, 'DESCRIPTION.md'), { force: true });
|
||||
const nestedMsdDir = path.join(configDir, 'skills', 'msd');
|
||||
if (fs.existsSync(nestedMsdDir)) {
|
||||
// Remove MSD-owned DESCRIPTION.md (written by writeHermesCategoryDescription)
|
||||
fs.rmSync(path.join(nestedMsdDir, 'DESCRIPTION.md'), { force: true });
|
||||
// Remove the category dir if empty (no user content remaining)
|
||||
const remaining = fs.readdirSync(nestedGsdDir, { withFileTypes: true });
|
||||
const remaining = fs.readdirSync(nestedMsdDir, { withFileTypes: true });
|
||||
if (remaining.length === 0) {
|
||||
fs.rmSync(nestedGsdDir, { recursive: true, force: true });
|
||||
fs.rmSync(nestedMsdDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #2973 / Codex review (bd1f06c9): migrate dev-preferences.md to the
|
||||
// runtime-aware SKILL.md location after all layout-driven removal is
|
||||
// complete. Do NOT restore to commands/gsd/ — the user is uninstalling.
|
||||
// complete. Do NOT restore to commands/msd/ — the user is uninstalling.
|
||||
if (stagedLegacyArtifacts) {
|
||||
// #2875: read the content back from the DISK-staged copy, matching
|
||||
// _runLegacyInstallMigrations's call site — never restored on failure
|
||||
@@ -2177,7 +2177,7 @@ function uninstallRuntimeArtifacts(
|
||||
// symlinked user artifact is recreated AS a symlink in the staging tree,
|
||||
// never copied by content) would previously have its REFERENT's bytes
|
||||
// read here and land in SKILL.md verbatim — e.g. a symlink to
|
||||
// `~/.ssh/id_rsa` gets its private key content written into a file GSD
|
||||
// `~/.ssh/id_rsa` gets its private key content written into a file MSD
|
||||
// loads into agent context. A symlink to a DIRECTORY instead throws
|
||||
// EISDIR uncaught out of this function, which the caller never expected
|
||||
// and which left the staged entry undiscarded (re-materializing on the
|
||||
@@ -2225,7 +2225,7 @@ export = {
|
||||
USER_OWNED_ARTIFACTS,
|
||||
_runLegacyInstallMigrations,
|
||||
_runLegacyUninstallCleanup,
|
||||
_removeGsdEntries,
|
||||
_removeMsdEntries,
|
||||
_snapshotDir,
|
||||
_restoreDir,
|
||||
_removeHermesBareStemDirs,
|
||||
|
||||
Reference in New Issue
Block a user