diff --git a/packages/codegraph/entrypoint.sh b/packages/codegraph/entrypoint.sh index 24fc35d..cc44711 100644 --- a/packages/codegraph/entrypoint.sh +++ b/packages/codegraph/entrypoint.sh @@ -39,13 +39,19 @@ run_import() { fi # Run import (additive — never prunes on boot; use `import-skills --full` manually for that) - node dist/cli.js import-skills "$DATA_DIR" 2>&1 + BEFORE=$(sqlite3 "$DATA_DIR/.codegraph/graph.db" "SELECT COUNT(*) FROM skills;" 2>/dev/null || echo "?") + if node dist/cli.js import-skills "$DATA_DIR" 2>&1; then + AFTER=$(sqlite3 "$DATA_DIR/.codegraph/graph.db" "SELECT COUNT(*) FROM skills;" 2>/dev/null || echo "?") + echo "Skill catalog: ${BEFORE} -> ${AFTER} skills" + # Record the imported bundle hash ONLY on success, so a failed import retries + # on the next boot instead of being permanently masked by a stale hash. + [ -n "$BUNDLE_HASH" ] && printf '%s' "$BUNDLE_HASH" > "$HASH_FILE" + else + echo "WARNING: skill import FAILED (exit $?) — catalog.hash left unchanged so the next boot retries." >&2 + fi # Cleanup symlinks (data stays in SQLite) rm -rf "$DATA_DIR/.opencode" "$DATA_DIR/.agents" - - # Record the imported bundle hash so unchanged redeploys skip the import. - [ -n "$BUNDLE_HASH" ] && printf '%s' "$BUNDLE_HASH" > "$HASH_FILE" } if [ "$SKILL_COUNT" = "0" ] || [ "$SKILL_COUNT" = "" ]; then diff --git a/packages/storage/src/import-skills.ts b/packages/storage/src/import-skills.ts index 2af68ba..d35a64c 100644 --- a/packages/storage/src/import-skills.ts +++ b/packages/storage/src/import-skills.ts @@ -368,7 +368,13 @@ export function importSkills( // Optional full-sync: deprecate active skills that vanished from the bundle. let pruned = 0 - if (opts.prune) { + if (opts.prune && skills.length === 0) { + // Safety guard: an empty discovered set almost always means the bundle path + // was wrong (e.g. `import-skills /data` without the .opencode/.agents symlinks + // that entrypoint.sh sets up), NOT an intent to deprecate the entire catalog. + // Refuse to prune — otherwise a misconfigured path silently wipes every skill. + console.warn('[import-skills] --full: 0 skills discovered — skipping prune to avoid deprecating the whole catalog. Check the workspace path.') + } else if (opts.prune) { const discovered = new Set(skills.map((s) => s.name)) const activeNames = (db .prepare(`SELECT name FROM skills WHERE status = 'active' AND category NOT IN ('System','Lifecycle')`) diff --git a/packages/storage/tests/import-skills.test.ts b/packages/storage/tests/import-skills.test.ts index 441f772..a490bd3 100644 --- a/packages/storage/tests/import-skills.test.ts +++ b/packages/storage/tests/import-skills.test.ts @@ -109,4 +109,29 @@ describe('importSkills()', () => { db.close() } }) + + it('--full does NOT prune when the bundle is empty (0 skills discovered)', () => { + const workspace = makeWorkspace() // no .claude/skill — nothing to discover + importSkills(workspace) // creates the DB, imports 0 skills + + // Seed an active catalog skill directly (simulates an existing prod catalog). + const dbPath = path.join(workspace, '.codegraph', 'graph.db') + const seed = new Database(dbPath) + seed.prepare( + `INSERT INTO skills (name, category, description, content, type, tags, lines, updated_at, status) + VALUES ('keepme', 'Backend', 'd', '#', 'domain', '[]', 1, ?, 'active')`, + ).run(new Date().toISOString()) + seed.close() + + // --full against an empty bundle must NOT deprecate the existing catalog. + const result = importSkills(workspace, { prune: true }) + expect(result.pruned).toBe(0) + + const db = new Database(dbPath) + try { + expect((db.prepare("SELECT status AS s FROM skills WHERE name = 'keepme'").get() as { s: string }).s).toBe('active') + } finally { + db.close() + } + }) })