Files
msd-core/src/installer-migrations/000-first-time-baseline.cts
Jakub Zych a9a7a328e6 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.
2026-10-06 01:47:40 +02:00

260 lines
8.8 KiB
TypeScript

/**
* Installer migration: record first-time installer migration baseline.
*
* ADR-457 build-at-publish: the hand-written
* bin/lib/installer-migrations/000-first-time-baseline.cjs collapsed to a
* TypeScript source of truth. Behaviour is preserved byte-for-behaviour from
* the prior hand-written .cjs; only types are added.
*/
import fs from 'node:fs';
import path from 'node:path';
const BASELINE_MIGRATION_ID = '2026-05-11-first-time-baseline-scan';
// Runtime install surfaces must stay aligned with:
// - docs/installer-migrations.md#runtime-configuration-contract-registry
// - docs/ARCHITECTURE.md#runtime-install-contract-matrix
const RUNTIME_SURFACES: Record<string, string[]> = {
claude: ['msd-core', 'commands/msd', 'skills', 'agents', 'hooks', 'settings.json'],
codex: ['msd-core', 'skills', 'agents', 'hooks', 'config.toml', 'hooks.json'],
gemini: ['msd-core', 'commands/msd', 'hooks'],
opencode: ['msd-core', 'command', 'skills', 'agents'],
kilo: ['msd-core', 'command', 'skills', 'agents'],
copilot: ['msd-core', 'skills', 'agents'],
antigravity: ['msd-core', 'skills', 'agents'],
cursor: ['msd-core', 'skills', 'agents', 'hooks', 'hooks.json'],
windsurf: ['msd-core', 'skills', 'agents', 'rules'],
augment: ['msd-core', 'skills', 'agents'],
trae: ['msd-core', 'skills', 'agents', 'rules'],
qwen: ['msd-core', 'skills', 'agents'],
hermes: ['msd-core', 'skills/msd', 'agents'],
cline: ['msd-core', 'skills', 'agents'],
codebuddy: ['msd-core', 'skills', 'agents'],
};
const COMMON_SURFACES = ['msd-core', 'skills', 'agents', 'hooks'];
const INTERNAL_TOP_LEVEL_NAMES = new Set([
'msd-file-manifest.json',
'msd-install-state.json',
'msd-migration-backups',
'msd-migration-journal',
]);
const USER_OWNED_PATHS = new Set([
'msd-core/USER-PROFILE.md',
'commands/msd/dev-preferences.md',
'skills/msd-dev-preferences/SKILL.md',
]);
let knownGeneratedAgentNames: Set<string> | null = null;
function normalizeRelPath(relPath: string): string {
return relPath.replace(/\\/g, '/').replace(/^\/+/, '');
}
function baselineInstallSurfaces(runtime: string | undefined): string[] {
if (runtime && RUNTIME_SURFACES[runtime]) return RUNTIME_SURFACES[runtime];
return COMMON_SURFACES;
}
function walkFiles(root: string, relDir: string, files: Set<string>): void {
const dir = path.join(root, relDir);
if (!fs.existsSync(dir)) return;
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const relPath = path.posix.join(relDir, entry.name);
if (relDir === '' && INTERNAL_TOP_LEVEL_NAMES.has(entry.name)) continue;
if (entry.isDirectory()) {
walkFiles(root, relPath, files);
} else if (entry.isFile()) {
files.add(normalizeRelPath(relPath));
}
}
}
function scanBaselineFiles(configDir: string, runtime: string | undefined): string[] {
const relPaths = new Set<string>();
for (const surface of baselineInstallSurfaces(runtime)) {
const normalized = normalizeRelPath(surface);
const fullPath = path.join(configDir, normalized);
if (!fs.existsSync(fullPath)) continue;
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
walkFiles(configDir, normalized, relPaths);
} else if (stat.isFile() && !INTERNAL_TOP_LEVEL_NAMES.has(normalized)) {
relPaths.add(normalized);
}
}
return [...relPaths];
}
function isUserOwnedBaselinePath(relPath: string): boolean {
if (USER_OWNED_PATHS.has(relPath)) return true;
const parts = relPath.split('/');
if (parts[0] === 'skills' && parts[1] && !parts[1].startsWith('msd-')) return true;
if (parts[0] === 'agents' && parts[1] && !parts[1].startsWith('msd-')) return true;
return false;
}
function listKnownGeneratedAgentNames(): Set<string> {
if (knownGeneratedAgentNames) return knownGeneratedAgentNames;
knownGeneratedAgentNames = new Set<string>();
const agentsDir = path.resolve(__dirname, '..', '..', '..', '..', 'agents');
try {
for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
if (entry.isFile() && entry.name.startsWith('msd-') && entry.name.endsWith('.md')) {
knownGeneratedAgentNames.add(entry.name.replace(/\.md$/, ''));
}
}
} catch {
// If the source agent directory is unavailable, fail closed and treat
// MSD-looking agent files as user-choice artifacts.
}
return knownGeneratedAgentNames;
}
function isKnownGeneratedAgentPath(relPath: string, runtime: string | undefined): boolean {
const parts = relPath.split('/');
if (parts.length !== 2 || parts[0] !== 'agents') return false;
const fileName = parts[1];
const extension = path.posix.extname(fileName);
if (extension !== '.md' && !(runtime === 'codex' && extension === '.toml')) return false;
const agentName = fileName.slice(0, -extension.length);
return listKnownGeneratedAgentNames().has(agentName);
}
function isStaleMsdLookingPath(relPath: string): boolean {
const baseName = path.posix.basename(relPath);
if (/^msd[-_]/.test(baseName)) return true;
const parts = relPath.split('/');
if ((parts[0] === 'skills' || parts[0] === 'agents') && parts[1] && parts[1].startsWith('msd-')) {
return true;
}
return false;
}
interface BaselineAction {
type: string;
relPath: string;
reason: string;
classification?: string;
originalHash?: string | null;
currentHash?: string | null;
prompt?: string;
choices?: string[];
}
function baselineActionRank(action: BaselineAction): number {
if (action.type === 'record-baseline') return 0;
if (action.type === 'baseline-preserve-user') return 1;
return 2;
}
interface ClassifiedArtifact {
classification: string;
originalHash?: string | null;
currentHash?: string | null;
[key: string]: unknown;
}
interface PlanContext {
configDir: string;
runtime?: string;
baselineScan?: boolean;
classifyArtifact: (relPath: string) => ClassifiedArtifact;
}
interface InstallerMigration {
id: string;
title: string;
description: string;
introducedIn: string;
scopes: string[];
destructive: boolean;
plan: (ctx: PlanContext) => BaselineAction[];
}
const migration: InstallerMigration = {
id: BASELINE_MIGRATION_ID,
title: 'Record first-time installer migration baseline',
description: 'Classify existing install surfaces before destructive installer migrations run.',
introducedIn: '1.50.0',
scopes: ['global', 'local'],
destructive: false,
plan: ({ configDir, runtime, baselineScan, classifyArtifact }: PlanContext): BaselineAction[] => {
if (!baselineScan) return [];
const actions: BaselineAction[] = [];
for (const relPath of scanBaselineFiles(configDir, runtime)) {
// docs/installer-migrations.md#baseline-preserve-user keeps user-owned
// artifacts out of destructive migration flow.
if (isUserOwnedBaselinePath(relPath)) {
actions.push({
type: 'baseline-preserve-user',
relPath,
reason: 'known user-owned artifact preserved by first-time migration baseline',
classification: 'user-owned',
originalHash: null,
currentHash: null,
});
continue;
}
const artifact = classifyArtifact(relPath);
if (artifact.classification === 'managed-pristine' || artifact.classification === 'managed-modified') {
actions.push({
type: 'record-baseline',
relPath,
reason: 'existing manifest-managed file included in first-time migration baseline',
});
continue;
}
const currentHash = artifact.currentHash ?? null;
if (isKnownGeneratedAgentPath(relPath, runtime)) {
actions.push({
type: 'record-baseline',
relPath,
reason: 'known installer-generated agent included in first-time migration baseline',
classification: artifact.classification,
originalHash: artifact.originalHash ?? null,
currentHash,
});
continue;
}
if (isStaleMsdLookingPath(relPath)) {
actions.push({
type: 'prompt-user',
relPath,
reason: 'MSD-looking file is not proven manifest-managed and needs explicit user choice',
classification: 'stale-msd-looking',
originalHash: artifact.originalHash ?? null,
currentHash,
prompt: 'Choose whether to remove this stale-looking MSD artifact or keep it as user-owned.',
choices: ['keep', 'remove'],
});
continue;
}
actions.push({
type: 'baseline-preserve-user',
relPath,
reason: 'unknown install-surface file preserved by first-time migration baseline',
classification: artifact.classification,
originalHash: artifact.originalHash ?? null,
currentHash,
});
}
return actions.sort(
(left, right) =>
baselineActionRank(left) - baselineActionRank(right) || left.relPath.localeCompare(right.relPath)
);
},
};
export = migration;