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:
112
src/surface.cts
112
src/surface.cts
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* Runtime surface module — ADR-0011 Phase 2 (Option B).
|
||||
*
|
||||
* Manages the runtime enable/disable surface state (the `.gsd-surface.json` marker in
|
||||
* Manages the runtime enable/disable surface state (the `.msd-surface.json` marker in
|
||||
* each runtime's config dir root (e.g., ~/.claude)) independently of the install-time profile marker
|
||||
* (`.gsd-profile`). Runtime config locations are resolved by callers.
|
||||
* (`.msd-profile`). Runtime config locations are resolved by callers.
|
||||
*
|
||||
* Effective skill set = base profile ∪ explicitAdds − disabledClusters − explicitRemoves,
|
||||
* then transitively closed via the manifest.
|
||||
@@ -72,8 +72,8 @@ import runtimeArtifactInstallPlan = require('./runtime-artifact-install-plan.cjs
|
||||
import retiredArtifactCleanup = require('./retired-artifact-cleanup.cjs');
|
||||
const { assertDestWithinConfigHome } = runtimeArtifactInstallPlan;
|
||||
|
||||
const SURFACE_FILE_NAME = '.gsd-surface.json';
|
||||
const INSTALL_MANIFEST_FILE_NAME = 'gsd-file-manifest.json';
|
||||
const SURFACE_FILE_NAME = '.msd-surface.json';
|
||||
const INSTALL_MANIFEST_FILE_NAME = 'msd-file-manifest.json';
|
||||
|
||||
/** Read only the installer-owned relative-include style, failing closed. */
|
||||
function readRelativeIncludePrefix(runtimeConfigDir: string): string | undefined {
|
||||
@@ -206,7 +206,7 @@ function clustersToSkills(clusterNames: string[], clusterMap: ClusterMap | Recor
|
||||
* Supports:
|
||||
* - canonical Map<string, string[]>
|
||||
* - legacy plain object map: { [stem]: string[] }
|
||||
* - parsed gsd-file-manifest.json object ({ files: { ... } }) by rebuilding
|
||||
* - parsed msd-file-manifest.json object ({ files: { ... } }) by rebuilding
|
||||
* the dependency manifest from the install source tree
|
||||
*/
|
||||
function normalizeSkillManifest(runtimeConfigDir: string, manifest: Map<string, string[]> | object | null | undefined): Map<string, string[]> {
|
||||
@@ -223,7 +223,7 @@ function normalizeSkillManifest(runtimeConfigDir: string, manifest: Map<string,
|
||||
return new Map(arrayEntries.map(([stem, deps]) => [stem, deps]));
|
||||
}
|
||||
|
||||
// Parsed gsd-file-manifest.json shape: rebuild the dependency map from source.
|
||||
// Parsed msd-file-manifest.json shape: rebuild the dependency map from source.
|
||||
const manifestObj = manifest as Record<string, unknown>;
|
||||
if (manifestObj['files'] && typeof manifestObj['files'] === 'object') {
|
||||
const srcCommandsDir = findInstallSourceRoot(runtimeConfigDir);
|
||||
@@ -285,7 +285,7 @@ function resolveSurface(runtimeConfigDir: string, manifest: Map<string, string[]
|
||||
const skillManifest = normalizeSkillManifest(runtimeConfigDir, manifest);
|
||||
const surface = surfaceOverride === undefined ? readSurface(runtimeConfigDir) : surfaceOverride;
|
||||
|
||||
// Determine base profile name: from surface state or from .gsd-profile marker
|
||||
// Determine base profile name: from surface state or from .msd-profile marker
|
||||
const baseProfileName = (surface && surface.baseProfile)
|
||||
? surface.baseProfile
|
||||
: (readActiveProfile(runtimeConfigDir) || 'full');
|
||||
@@ -306,7 +306,7 @@ function resolveSurface(runtimeConfigDir: string, manifest: Map<string, string[]
|
||||
if (!key.startsWith('_calls_agents_')) skills.add(key);
|
||||
}
|
||||
// Issue #2045 (DEFECT 1): third-party capability skills live at
|
||||
// ~/.gsd/capabilities/<id>/skills/<stem>/SKILL.md — NOT in the runtime skills
|
||||
// ~/.msd/capabilities/<id>/skills/<stem>/SKILL.md — NOT in the runtime skills
|
||||
// dir → never in skillManifest → never in the Set → surfaced:false. The
|
||||
// overlay-aware registry's `capabilityClusters` already covers accepted
|
||||
// overlay caps (composed by loadRegistry({includeInstalled})), so union its
|
||||
@@ -390,7 +390,7 @@ function applySurface(runtimeConfigDir: string, layout: Layout, manifest: Map<st
|
||||
throw new TypeError('applySurface runtimeConfigDir must match layout.configDir');
|
||||
}
|
||||
// #3712: the dest selection below prefers `kind.home` over layout.configDir and
|
||||
// then hands it to the destructive _syncGsdDir, so surface apply is a third
|
||||
// then hands it to the destructive _syncMsdDir, so surface apply is a third
|
||||
// escape route into the developer's real home alongside install/uninstall.
|
||||
testHomeGuard.assertTestHomeSandboxed('applySurface', layout.runtime, layout.kinds, {
|
||||
os: deps.os, env: deps.env,
|
||||
@@ -422,7 +422,7 @@ function applySurface(runtimeConfigDir: string, layout: Layout, manifest: Map<st
|
||||
const _isOpencode = layout.runtime === 'opencode';
|
||||
const _isWindowsHost = (opts?.platform ?? process.platform) === 'win32';
|
||||
// #4377: style is an install-time fact, not a process environment setting.
|
||||
// A later gsd-tools surface apply runs in another process, so it must reuse
|
||||
// A later msd-tools surface apply runs in another process, so it must reuse
|
||||
// the persisted, root-relative prefix the installer actually emitted.
|
||||
const _relativeIncludePrefix = readRelativeIncludePrefix(runtimeConfigDir);
|
||||
const _pathPrefix = runtimeArtifactConversion._computePathPrefix({ isGlobal: _isGlobal, isOpencode: _isOpencode, isWindowsHost: _isWindowsHost, resolvedTarget: _resolvedTarget, homeDir: _homeDir, projectRelative: !_isGlobal && typeof _relativeIncludePrefix === 'string', projectRelativePath: _relativeIncludePrefix, localDirName: runtimeArtifactConversion._localIncludeDirName(layout.runtime) });
|
||||
@@ -500,7 +500,7 @@ function applySurface(runtimeConfigDir: string, layout: Layout, manifest: Map<st
|
||||
// and the persisted surface state untouched.
|
||||
retiredArtifactCleanup.pruneRetiredRuntimeArtifacts(layout.runtime, layout.configDir);
|
||||
for (const item of stagedKinds) {
|
||||
_syncGsdDir(item.staged, item.dest, item.kind, skillManifest, layout.runtime);
|
||||
_syncMsdDir(item.staged, item.dest, item.kind, skillManifest, layout.runtime);
|
||||
}
|
||||
|
||||
if (hasCandidateState) {
|
||||
@@ -519,21 +519,21 @@ function applySurface(runtimeConfigDir: string, layout: Layout, manifest: Map<st
|
||||
}
|
||||
|
||||
/**
|
||||
* Prune GSD-managed skill directories from a skills directory.
|
||||
* Prune MSD-managed skill directories from a skills directory.
|
||||
*
|
||||
* Removes every directory in `skillsDir` that is GSD-owned but NOT listed
|
||||
* in `retainedNames`. User-owned dirs (not matching the GSD ownership criteria)
|
||||
* Removes every directory in `skillsDir` that is MSD-owned but NOT listed
|
||||
* in `retainedNames`. User-owned dirs (not matching the MSD ownership criteria)
|
||||
* are always preserved.
|
||||
*
|
||||
* Ownership criteria:
|
||||
* - Non-empty prefix (e.g. 'gsd-'): dir name starts with that prefix AND
|
||||
* - Non-empty prefix (e.g. 'msd-'): dir name starts with that prefix AND
|
||||
* EITHER appears in the manifest (first-party membership) OR carries the
|
||||
* persisted `CAPABILITY_SKILL_MARKER` file (#2322 HIGH-3: a third-party
|
||||
* capability skill, self-certifying and independent of current registry
|
||||
* state — so an uninstalled/unsurfaced capability's stale skill is still
|
||||
* prunable even though it no longer appears in any registry view). Dirs
|
||||
* that match the prefix but satisfy NEITHER are treated as user-owned and
|
||||
* preserved — this prevents data loss for user-created gsd-* directories.
|
||||
* preserved — this prevents data loss for user-created msd-* directories.
|
||||
* A warning is written to stderr when such a dir is encountered.
|
||||
* - Empty prefix (Hermes): dir name appears as a canonical skill stem in the
|
||||
* manifest. User dirs not in the manifest are preserved. (Hermes does not
|
||||
@@ -542,12 +542,12 @@ function applySurface(runtimeConfigDir: string, layout: Layout, manifest: Map<st
|
||||
* - Empty prefix without manifest, or manifest not a Map: conservative; no
|
||||
* dirs are removed.
|
||||
*
|
||||
* This is the single point of truth for skill-dir pruning. Both _syncGsdDir
|
||||
* This is the single point of truth for skill-dir pruning. Both _syncMsdDir
|
||||
* (surface apply) and callers that need stand-alone pruning use this function.
|
||||
*
|
||||
* @param skillsDir directory that contains the gsd-STEM sub-dirs
|
||||
* @param retainedNames set of directory names to keep (e.g. 'gsd-help')
|
||||
* @param prefix GSD dir prefix, e.g. 'gsd-' (or '' for Hermes)
|
||||
* @param skillsDir directory that contains the msd-STEM sub-dirs
|
||||
* @param retainedNames set of directory names to keep (e.g. 'msd-help')
|
||||
* @param prefix MSD dir prefix, e.g. 'msd-' (or '' for Hermes)
|
||||
* @param manifest optional; required for Hermes empty-prefix case
|
||||
* and for manifest-membership gate in prefixed case.
|
||||
* Must be a Map; any other type is treated as missing.
|
||||
@@ -569,7 +569,7 @@ function pruneSkillDirs(skillsDir: string, retainedNames: Set<string>, prefix: s
|
||||
const entryPath = path.join(skillsDir, entry);
|
||||
if (!fs.statSync(entryPath).isDirectory()) continue;
|
||||
|
||||
let isGsdOwned: boolean;
|
||||
let isMsdOwned: boolean;
|
||||
if (prefix !== '') {
|
||||
if (!entry.startsWith(prefix)) {
|
||||
// Does not match prefix at all — user-owned, preserve.
|
||||
@@ -577,11 +577,11 @@ function pruneSkillDirs(skillsDir: string, retainedNames: Set<string>, prefix: s
|
||||
}
|
||||
// #2322: an entry in THIS apply's retained set is unambiguously wanted —
|
||||
// check that BEFORE the first-party-manifest-membership gate below. The
|
||||
// manifest only ever knows gsd-core's own bundled stems; a materialized
|
||||
// manifest only ever knows msd-core's own bundled stems; a materialized
|
||||
// third-party capability skill (retained via the resolved profile's
|
||||
// registry union, #2045/#2322) has no manifest entry at all, so without
|
||||
// this early check it fell into the "unknown, preserve with warning"
|
||||
// branch on EVERY apply — misreporting a live, GSD-managed capability
|
||||
// branch on EVERY apply — misreporting a live, MSD-managed capability
|
||||
// skill as "user-owned or unknown" noise. This does not change any
|
||||
// deletion outcome (a retained entry was always preserved — see the
|
||||
// `retainedNames.has(entry)` check further below); it only short-
|
||||
@@ -589,14 +589,14 @@ function pruneSkillDirs(skillsDir: string, retainedNames: Set<string>, prefix: s
|
||||
// this apply, are wanted.
|
||||
if (retainedNames.has(entry)) continue;
|
||||
// Finding 1 fix: prefix match is necessary but NOT sufficient.
|
||||
// The dir must also be in the manifest to be considered GSD-owned.
|
||||
// A user-created gsd-* dir that isn't in the manifest is preserved with a warning.
|
||||
// The dir must also be in the manifest to be considered MSD-owned.
|
||||
// A user-created msd-* dir that isn't in the manifest is preserved with a warning.
|
||||
const stem = entry.slice(prefix.length);
|
||||
if (canonicalStems && canonicalStems.has(stem)) {
|
||||
isGsdOwned = true;
|
||||
isMsdOwned = true;
|
||||
} else if (fs.existsSync(path.join(entryPath, CAPABILITY_SKILL_MARKER))) {
|
||||
// #2322 HIGH-3: not a first-party stem, but self-certified as a
|
||||
// GSD-managed THIRD-PARTY capability skill via the persisted marker
|
||||
// MSD-managed THIRD-PARTY capability skill via the persisted marker
|
||||
// (written by install-profiles.cts stageSkillsForRuntimeAsSkills at
|
||||
// stage time). Without this, an orphaned capability skill — its
|
||||
// owning capability uninstalled/unsurfaced and no longer appearing in
|
||||
@@ -606,27 +606,27 @@ function pruneSkillDirs(skillsDir: string, retainedNames: Set<string>, prefix: s
|
||||
// instructions from the agent's context. The marker makes ownership
|
||||
// self-certifying at prune time, independent of current registry
|
||||
// state (or even of whether a manifest was supplied at all).
|
||||
isGsdOwned = true;
|
||||
isMsdOwned = true;
|
||||
} else if (!canonicalStems) {
|
||||
// No manifest available and no capability marker: cannot confirm
|
||||
// ownership — preserve conservatively (silent).
|
||||
continue;
|
||||
} else {
|
||||
process.stderr.write(
|
||||
`[gsd] Warning: ${entry} matches GSD prefix '${prefix}' but is not in the manifest — preserving (user-owned or unknown)\n`
|
||||
`[msd] Warning: ${entry} matches MSD prefix '${prefix}' but is not in the manifest — preserving (user-owned or unknown)\n`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
} else if (canonicalStems) {
|
||||
// Hermes: GSD-owned iff the directory name appears in the canonical manifest.
|
||||
isGsdOwned = canonicalStems.has(entry);
|
||||
// Hermes: MSD-owned iff the directory name appears in the canonical manifest.
|
||||
isMsdOwned = canonicalStems.has(entry);
|
||||
} else {
|
||||
// No manifest available: be conservative, don't remove anything.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isGsdOwned) continue; // Hermes path only: preserve user-owned dirs not in manifest
|
||||
if (retainedNames.has(entry)) continue; // GSD-owned and in retain set
|
||||
if (!isMsdOwned) continue; // Hermes path only: preserve user-owned dirs not in manifest
|
||||
if (retainedNames.has(entry)) continue; // MSD-owned and in retain set
|
||||
try {
|
||||
fs.rmSync(entryPath, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
@@ -639,22 +639,22 @@ function pruneSkillDirs(skillsDir: string, retainedNames: Set<string>, prefix: s
|
||||
* Sync destination directory from staged source.
|
||||
*
|
||||
* For 'commands' kind: iterate *.md files in destDir, remove if not in staged set.
|
||||
* For 'agents' kind: same, but only remove files starting with 'gsd-' prefix.
|
||||
* For 'agents' kind: same, but only remove files starting with 'msd-' prefix.
|
||||
* For 'skills' kind: iterate directories in destDir matching kind.prefix; add missing
|
||||
* by copying recursively; remove dirs not in staged set. Preserves dirs not matching
|
||||
* the prefix (user-owned skills). Pruning is delegated to pruneSkillDirs().
|
||||
*
|
||||
* For Hermes (empty prefix): uses manifest membership to discriminate GSD-owned vs
|
||||
* user-owned dirs. GSD-owned = stem in manifest; removal targets = in manifest AND
|
||||
* For Hermes (empty prefix): uses manifest membership to discriminate MSD-owned vs
|
||||
* user-owned dirs. MSD-owned = stem in manifest; removal targets = in manifest AND
|
||||
* not in staged set. User-owned (not in manifest) are always preserved.
|
||||
*/
|
||||
function _syncGsdDir(stagedDir: string, destDir: string, kind: ArtifactKind | string, manifest?: Map<string, string[]>, runtime?: string): void {
|
||||
function _syncMsdDir(stagedDir: string, destDir: string, kind: ArtifactKind | string, manifest?: Map<string, string[]>, runtime?: string): void {
|
||||
if (!fs.existsSync(stagedDir)) return;
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
|
||||
// Normalize: allow legacy string context for backward-compat with internal callers
|
||||
const kindName = (typeof kind === 'string') ? kind : kind.kind;
|
||||
const kindPrefix = (typeof kind === 'object' && kind !== null) ? kind.prefix : 'gsd-';
|
||||
const kindPrefix = (typeof kind === 'object' && kind !== null) ? kind.prefix : 'msd-';
|
||||
|
||||
// #1575 / #2103: agent files are renamed .md -> <agentFileExtension> at copy
|
||||
// time when the runtime's descriptor declares hostBehaviors.agentFileExtension
|
||||
@@ -666,23 +666,23 @@ function _syncGsdDir(stagedDir: string, destDir: string, kind: ArtifactKind | st
|
||||
const isRenamedAgents = !!_agentExt && kindName === 'agents';
|
||||
|
||||
if (kindName === 'kimi-agents') {
|
||||
// #4211: Kimi's managed tree is `gsd.yaml` + `gsd.md` + `subagents/gsd-*.{yaml,md}`
|
||||
// #4211: Kimi's managed tree is `msd.yaml` + `msd.md` + `subagents/msd-*.{yaml,md}`
|
||||
// (runtime-artifact-layout.cts kimiAgentsKind), and install copies it
|
||||
// RECURSIVELY (_copyStaged in src/install-engine.cts). Surface apply fell
|
||||
// through to the flat command/agent branch below, which reads only `*.md`
|
||||
// at the top level: the YAML half and the whole subagents/ subtree were
|
||||
// dropped, and `gsd.md` was written as `gsdgsd.md` (the flat branch
|
||||
// dropped, and `msd.md` was written as `msdmsd.md` (the flat branch
|
||||
// re-applies kind.prefix to a name that already carries it). A surface
|
||||
// change could therefore corrupt Kimi's installed artifacts while still
|
||||
// reporting success.
|
||||
fs.cpSync(stagedDir, destDir, { recursive: true });
|
||||
|
||||
// Prune GSD-owned files the new surface no longer stages, with exactly the
|
||||
// ownership rule install's _removeGsdEntries applies to this kind: the two
|
||||
// root files, and `gsd-`-prefixed .yaml/.md under subagents/. Everything
|
||||
// Prune MSD-owned files the new surface no longer stages, with exactly the
|
||||
// ownership rule install's _removeMsdEntries applies to this kind: the two
|
||||
// root files, and `msd-`-prefixed .yaml/.md under subagents/. Everything
|
||||
// else in the directory is user-owned and is preserved.
|
||||
const _rootStaged = new Set(fs.readdirSync(stagedDir));
|
||||
for (const fileName of ['gsd.yaml', 'gsd.md']) {
|
||||
for (const fileName of ['msd.yaml', 'msd.md']) {
|
||||
if (!_rootStaged.has(fileName)) {
|
||||
try { fs.rmSync(path.join(destDir, fileName), { force: true }); } catch { /* ignore */ }
|
||||
}
|
||||
@@ -695,7 +695,7 @@ function _syncGsdDir(stagedDir: string, destDir: string, kind: ArtifactKind | st
|
||||
if (fs.existsSync(_destSubagentsDir)) {
|
||||
for (const entry of fs.readdirSync(_destSubagentsDir, { 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;
|
||||
if (_stagedSubagents.has(entry.name)) continue;
|
||||
try { fs.rmSync(path.join(_destSubagentsDir, entry.name), { force: true }); } catch { /* ignore */ }
|
||||
@@ -719,16 +719,16 @@ function _syncGsdDir(stagedDir: string, destDir: string, kind: ArtifactKind | st
|
||||
fs.cpSync(path.join(stagedDir, dirName), destSubDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Prune GSD-owned dirs that are no longer in the staged set.
|
||||
// Prune MSD-owned dirs that are no longer in the staged set.
|
||||
// pruneSkillDirs() is the single point of truth for this logic.
|
||||
pruneSkillDirs(destDir, stagedDirs, kindPrefix, manifest);
|
||||
} else {
|
||||
// commands / agents kind: mirror installRuntimeArtifacts (_copyStaged /
|
||||
// _removeGsdEntries in bin/install.js) so surface produces the SAME files as a
|
||||
// _removeMsdEntries 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
|
||||
// the msd- prefix on copy; namespaced command dirs (commands/msd) and agents
|
||||
// keep their staged names. Copying staged names verbatim previously diverged
|
||||
// from install and orphaned the installed gsd-*.md files, and the unscoped
|
||||
// from install and orphaned the installed msd-*.md files, and the unscoped
|
||||
// prune deleted user-owned command files.
|
||||
//
|
||||
// Single source of truth: runtimeArtifactLayout.isNamespacedByDir (#2871
|
||||
@@ -749,15 +749,15 @@ function _syncGsdDir(stagedDir: string, destDir: string, kind: ArtifactKind | st
|
||||
stagedDestNames.add(destName);
|
||||
}
|
||||
|
||||
// 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 (copilot: gsd-*.agent.md)
|
||||
// - flat command dirs: only `${kindPrefix}`-prefixed are GSD-owned
|
||||
// - namespaced command dirs: the whole dir is GSD-owned
|
||||
// Prune stale MSD-owned files not in the staged set, preserving user-owned files
|
||||
// (mirrors install's prefix-scoped _removeMsdEntries):
|
||||
// - agents: only msd-* are MSD-owned (copilot: msd-*.agent.md)
|
||||
// - flat command dirs: only `${kindPrefix}`-prefixed are MSD-owned
|
||||
// - namespaced command dirs: the whole dir is MSD-owned
|
||||
const shouldPruneAgents = !(kindName === 'agents' && (!manifest || manifest.size === 0));
|
||||
if (shouldPruneAgents) {
|
||||
for (const file of fs.readdirSync(destDir).filter(f => f.endsWith('.md'))) {
|
||||
if (kindName === 'agents' && !file.startsWith('gsd-')) continue;
|
||||
if (kindName === 'agents' && !file.startsWith('msd-')) continue;
|
||||
if (kindName === 'commands' && !namespacedByDir && kindPrefix && !file.startsWith(kindPrefix)) continue;
|
||||
if (!stagedDestNames.has(file)) {
|
||||
try { fs.unlinkSync(path.join(destDir, file)); } catch { /* ignore */ }
|
||||
@@ -821,5 +821,5 @@ export = {
|
||||
listSurface,
|
||||
// Exported for testing and for callers that need stand-alone pruning
|
||||
pruneSkillDirs,
|
||||
_syncGsdDir,
|
||||
_syncMsdDir,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user