Files
msd-core/sdk/src/query/detect-custom-files.ts
Tom Boucher d49e8872b5 fix(3317): SDK detect-custom-files now scans skills/ (parity with CJS port) (#3318)
* test(3317): red — SDK detect-custom-files must scan skills/

Mirrors tests/bug-2942-detect-custom-skills.test.cjs on the SDK side.
The SDK's GSD_MANAGED_DIRS array omits 'skills', so user-added skills
under <config-dir>/skills/<name>/ are never returned and get destroyed
on /gsd-update. New vitest covers:

- detects custom skill at skills/<name>/SKILL.md
- does not flag manifest-tracked skill as custom
- still detects custom files under get-shit-done/workflows/ (regression)
- custom_count matches custom_files.length across multiple skills

Fails 2/4 against current SDK source. Will pass after fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(3317): SDK detect-custom-files now scans skills/ (parity with CJS)

The SDK port of detect-custom-files declared GSD_MANAGED_DIRS without
'skills', while the canonical bin/gsd-tools.cjs port (which the SDK
docstring explicitly cites as its source) had the entry. Because
update.md prefers gsd-sdk over the CJS shim, real-world users with the
SDK installed never had their custom skills detected — the installer's
'Installed N skills to skills/' step then wiped any non-manifest skill
without backing it up to gsd-user-files-backup/. Real-world incident:
skills/gsd-roadmap/SKILL.md (fully user-owned) destroyed during the
1.40.0 → 1.41.0 update, recoverable only via Time Machine.

One-line fix adds 'skills' to the SDK's GSD_MANAGED_DIRS, matching the
CJS source the port was supposed to mirror. The 4 vitest cases added in
the prior commit now all pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: correct changeset pr number

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 23:28:10 -04:00

97 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Detect user-added files under GSD-managed install dirs not listed in the manifest.
*
* Port of `detect-custom-files` from `get-shit-done/bin/gsd-tools.cjs` (lines 1161–1239).
*/
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { join, relative, resolve } from 'node:path';
import type { QueryHandler } from './utils.js';
const GSD_MANAGED_DIRS = [
'get-shit-done',
'agents',
join('commands', 'gsd'),
'hooks',
'skills',
];
function walkDir(dir: string, baseDir: string): string[] {
const results: string[] = [];
if (!existsSync(dir)) return results;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
results.push(...walkDir(fullPath, baseDir));
} else {
const relPath = relative(baseDir, fullPath).split('\\').join('/');
results.push(relPath);
}
}
return results;
}
/**
* Args: `--config-dir <path>` (required) — runtime config directory to scan.
*/
export const detectCustomFiles: QueryHandler = async (args) => {
const configDirIdx = args.indexOf('--config-dir');
const configDir = configDirIdx !== -1 ? args[configDirIdx + 1] : null;
if (!configDir) {
return { data: { error: 'Usage: detect-custom-files --config-dir <path>' } };
}
const resolvedConfigDir = resolve(configDir);
if (!existsSync(resolvedConfigDir)) {
return { data: { error: `Config directory not found: ${resolvedConfigDir}` } };
}
const manifestPath = join(resolvedConfigDir, 'gsd-file-manifest.json');
if (!existsSync(manifestPath)) {
return {
data: {
custom_files: [] as string[],
custom_count: 0,
manifest_found: false,
},
};
}
let manifest: { version?: string; files?: Record<string, unknown> };
try {
manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as { version?: string; files?: Record<string, unknown> };
} catch {
return {
data: {
custom_files: [] as string[],
custom_count: 0,
manifest_found: false,
error: 'manifest parse error',
},
};
}
const manifestKeys = new Set(Object.keys(manifest.files || {}));
const customFiles: string[] = [];
for (const managedDir of GSD_MANAGED_DIRS) {
const absDir = join(resolvedConfigDir, managedDir);
if (!existsSync(absDir)) continue;
for (const relPath of walkDir(absDir, resolvedConfigDir)) {
if (!manifestKeys.has(relPath)) {
customFiles.push(relPath);
}
}
}
return {
data: {
custom_files: customFiles,
custom_count: customFiles.length,
manifest_found: true,
manifest_version: manifest.version ?? null,
},
};
};