mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 22:48:57 +02:00
feat(setup,gen-skill-docs): prune renders of skills that no longer exist
setup gains _prune_stale_generated for every host tree and the doc generator removes gstack-* output dirs it did not write, so a skill removed from the source tree can never linger in an install. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -170,6 +170,12 @@ function parsePathFlag(flag: string): string | null {
|
||||
}
|
||||
const OUT_DIR: string | null = parsePathFlag('--out-dir');
|
||||
|
||||
// External-host outputs rendered in THIS run, keyed by host. Used after the
|
||||
// render to prune `gstack-*` output dirs whose skill no longer exists: the
|
||||
// generator never deleted, so a retired skill stayed rendered (and linked by
|
||||
// setup) forever, still reading config keys the DEFAULTS table had dropped.
|
||||
const RENDERED_EXTERNAL: Map<string, Set<string>> = new Map();
|
||||
|
||||
// #2692: callers that render into a TMP dir and atomically swap it into place
|
||||
// (bin/gstack-config gbrain-refresh, setup — the #2569 pattern) must pass the
|
||||
// FINAL directory here, or rewriteSectionBase bakes the tmp path
|
||||
@@ -799,6 +805,8 @@ function processExternalHost(
|
||||
const name = externalSkillName(skillDir === '.' ? '' : skillDir, frontmatterName);
|
||||
// --out-dir mirrors the host tree (outputs only; inputs read from ROOT).
|
||||
const outputDir = path.join(OUT_DIR ?? ROOT, hostConfig.hostSubdir, 'skills', name);
|
||||
if (!RENDERED_EXTERNAL.has(host)) RENDERED_EXTERNAL.set(host, new Set());
|
||||
RENDERED_EXTERNAL.get(host)!.add(name);
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
const outputPath = path.join(outputDir, 'SKILL.md');
|
||||
|
||||
@@ -1166,6 +1174,24 @@ if (!DRY_RUN) {
|
||||
} catch { /* non-fatal */ }
|
||||
}
|
||||
|
||||
// Prune stale external-host outputs. A run always renders every skill for the
|
||||
// chosen host(s) (there is no per-skill filter), so any `gstack-*` directory
|
||||
// left in <host>/skills/ that this run did not write belongs to a skill that
|
||||
// no longer exists. Symlinks (the `gstack` sidecar) and non-prefixed entries
|
||||
// are never touched.
|
||||
if (!DRY_RUN) {
|
||||
for (const [host, names] of RENDERED_EXTERNAL) {
|
||||
const skillsRoot = path.join(OUT_DIR ?? ROOT, getHostConfig(host as Host).hostSubdir, 'skills');
|
||||
let entries: fs.Dirent[] = [];
|
||||
try { entries = fs.readdirSync(skillsRoot, { withFileTypes: true }); } catch { continue; }
|
||||
for (const e of entries) {
|
||||
if (e.isSymbolicLink() || !e.isDirectory() || !e.name.startsWith('gstack-') || names.has(e.name)) continue;
|
||||
fs.rmSync(path.join(skillsRoot, e.name), { recursive: true, force: true });
|
||||
console.log(` pruned stale ${host} render: ${e.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Regenerate gstack/llms.txt — single-file capability index for AI agents.
|
||||
// Runs after SKILL.md generation so it sees current skill descriptions and
|
||||
// browse command list. Wrapped in an IIFE so the await-import doesn't make
|
||||
|
||||
Reference in New Issue
Block a user