diff --git a/__tests__/domains/config/merger/conflict-resolver.test.ts b/__tests__/domains/config/merger/conflict-resolver.test.ts index 6d272c61..acb51a3d 100644 --- a/__tests__/domains/config/merger/conflict-resolver.test.ts +++ b/__tests__/domains/config/merger/conflict-resolver.test.ts @@ -11,8 +11,10 @@ function createMergeResult(): MergeResult { hooksAdded: 0, hooksPreserved: 0, hooksSkipped: 0, + hooksRemoved: 0, mcpServersPreserved: 0, mcpServersSkipped: 0, + mcpServersRemoved: 0, conflictsDetected: [], newlyInstalledHooks: [], newlyInstalledServers: [], diff --git a/__tests__/domains/config/merger/merge-engine.test.ts b/__tests__/domains/config/merger/merge-engine.test.ts new file mode 100644 index 00000000..ba45b4aa --- /dev/null +++ b/__tests__/domains/config/merger/merge-engine.test.ts @@ -0,0 +1,150 @@ +import { describe, expect, it } from "bun:test"; +import { mergeHooks, mergeMcp, mergeSettings } from "@/domains/config/merger/merge-engine.js"; +import type { HookEntry, MergeResult, SettingsJson } from "@/domains/config/merger/types.js"; + +function createMergeResult(): MergeResult { + return { + merged: {}, + hooksAdded: 0, + hooksPreserved: 0, + hooksSkipped: 0, + hooksRemoved: 0, + mcpServersPreserved: 0, + mcpServersSkipped: 0, + mcpServersRemoved: 0, + conflictsDetected: [], + newlyInstalledHooks: [], + newlyInstalledServers: [], + hooksByOrigin: new Map(), + }; +} + +describe("merge-engine deprecation removal", () => { + describe("mergeHooks removes deprecated hooks", () => { + it("should remove hook in installed but not in source", () => { + const sourceHooks: Record = { + SessionStart: [{ type: "command", command: "node new-hook.js" }], + }; + const destHooks: Record = { + SessionStart: [ + { type: "command", command: "node new-hook.js" }, + { type: "command", command: "node deprecated-hook.js" }, + ], + }; + const result = createMergeResult(); + + const merged = mergeHooks(sourceHooks, destHooks, result, { + installedSettings: { + hooks: ["node deprecated-hook.js"], + }, + }); + + // deprecated-hook.js should be removed + expect(merged.SessionStart).toHaveLength(1); + expect(result.hooksRemoved).toBe(1); + expect(result.removedHooks).toContain("node deprecated-hook.js"); + }); + + it("should preserve user-added hook not in installed", () => { + const sourceHooks: Record = { + SessionStart: [{ type: "command", command: "node ck-hook.js" }], + }; + const destHooks: Record = { + SessionStart: [ + { type: "command", command: "node ck-hook.js" }, + { type: "command", command: "node user-hook.js" }, + ], + }; + const result = createMergeResult(); + + const merged = mergeHooks(sourceHooks, destHooks, result, { + installedSettings: { + hooks: ["node ck-hook.js"], // user-hook.js not in installed = user added it + }, + }); + + // user-hook.js should be preserved + expect(merged.SessionStart).toHaveLength(2); + expect(result.hooksRemoved).toBe(0); + }); + + it("should not remove anything with empty installedSettings", () => { + const sourceHooks: Record = { + SessionStart: [{ type: "command", command: "node new-hook.js" }], + }; + const destHooks: Record = { + SessionStart: [{ type: "command", command: "node old-hook.js" }], + }; + const result = createMergeResult(); + + const merged = mergeHooks(sourceHooks, destHooks, result, { + installedSettings: { hooks: [] }, + }); + + // old-hook.js preserved (fresh install scenario) + expect(merged.SessionStart).toHaveLength(2); + expect(result.hooksRemoved).toBe(0); + }); + }); + + describe("mergeMcp removes deprecated servers", () => { + it("should remove server in installed but not in source", () => { + const sourceMcp: SettingsJson["mcp"] = { + servers: { "new-server": { command: "npx new" } }, + }; + const destMcp: SettingsJson["mcp"] = { + servers: { + "new-server": { command: "npx new" }, + "deprecated-server": { command: "npx old" }, + }, + }; + const result = createMergeResult(); + + const merged = mergeMcp(sourceMcp, destMcp, result, { + installedSettings: { + mcpServers: ["deprecated-server"], + }, + }); + + expect(merged?.servers).not.toHaveProperty("deprecated-server"); + expect(merged?.servers).toHaveProperty("new-server"); + expect(result.mcpServersRemoved).toBe(1); + expect(result.removedMcpServers).toContain("deprecated-server"); + }); + + it("should preserve user-added server not in installed", () => { + const sourceMcp: SettingsJson["mcp"] = { + servers: { "ck-server": { command: "npx ck" } }, + }; + const destMcp: SettingsJson["mcp"] = { + servers: { + "ck-server": { command: "npx ck" }, + "user-server": { command: "npx user" }, + }, + }; + const result = createMergeResult(); + + const merged = mergeMcp(sourceMcp, destMcp, result, { + installedSettings: { + mcpServers: ["ck-server"], // user-server not in installed + }, + }); + + expect(merged?.servers).toHaveProperty("ck-server"); + expect(merged?.servers).toHaveProperty("user-server"); + expect(result.mcpServersRemoved).toBe(0); + }); + }); + + describe("mergeSettings initializes removal counters", () => { + it("should initialize hooksRemoved and mcpServersRemoved to 0", () => { + const source: SettingsJson = {}; + const dest: SettingsJson = {}; + + const result = mergeSettings(source, dest); + + expect(result.hooksRemoved).toBe(0); + expect(result.mcpServersRemoved).toBe(0); + }); + }); +}); diff --git a/src/__tests__/services/transformers/content-transformer.test.ts b/src/__tests__/services/transformers/content-transformer.test.ts new file mode 100644 index 00000000..96ac7f81 --- /dev/null +++ b/src/__tests__/services/transformers/content-transformer.test.ts @@ -0,0 +1,216 @@ +/** + * Tests for content-transformer.ts + * + * Verifies that command references are properly transformed + * when --prefix flag is used. + */ + +import { describe, expect, it } from "bun:test"; +import { transformCommandContent } from "@/services/transformers/commands-prefix/content-transformer.js"; + +describe("transformCommandContent", () => { + describe("basic command transformations", () => { + it("transforms /plan: to /ck:plan:", () => { + const input = "Execute `/plan:fast` to create a plan"; + const { transformed, changes } = transformCommandContent(input); + expect(transformed).toBe("Execute `/ck:plan:fast` to create a plan"); + expect(changes).toBe(1); + }); + + it("transforms /fix: to /ck:fix:", () => { + const input = "Use `/fix:types` for TypeScript errors"; + const { transformed, changes } = transformCommandContent(input); + expect(transformed).toBe("Use `/ck:fix:types` for TypeScript errors"); + expect(changes).toBe(1); + }); + + it("transforms /code: to /ck:code:", () => { + const input = "Run `/code:auto` to implement"; + const { transformed, changes } = transformCommandContent(input); + expect(transformed).toBe("Run `/ck:code:auto` to implement"); + expect(changes).toBe(1); + }); + + it("transforms /review: to /ck:review:", () => { + const input = "Use `/review:codebase` for analysis"; + const { transformed, changes } = transformCommandContent(input); + expect(transformed).toBe("Use `/ck:review:codebase` for analysis"); + expect(changes).toBe(1); + }); + + it("transforms /cook: to /ck:cook:", () => { + const input = "Try `/cook:auto` for quick implementation"; + const { transformed, changes } = transformCommandContent(input); + expect(transformed).toBe("Try `/ck:cook:auto` for quick implementation"); + expect(changes).toBe(1); + }); + + it("transforms /brainstorm to /ck:brainstorm", () => { + const input = "Start with `/brainstorm` to explore options"; + const { transformed, changes } = transformCommandContent(input); + expect(transformed).toBe("Start with `/ck:brainstorm` to explore options"); + expect(changes).toBe(1); + }); + + it("transforms /test to /ck:test", () => { + const input = "Run `/test` to verify"; + const { transformed, changes } = transformCommandContent(input); + expect(transformed).toBe("Run `/ck:test` to verify"); + expect(changes).toBe(1); + }); + + it("transforms /preview to /ck:preview", () => { + const input = "Use `/preview` to see changes"; + const { transformed, changes } = transformCommandContent(input); + expect(transformed).toBe("Use `/ck:preview` to see changes"); + expect(changes).toBe(1); + }); + + it("transforms /kanban to /ck:kanban", () => { + const input = "Open `/kanban` dashboard"; + const { transformed, changes } = transformCommandContent(input); + expect(transformed).toBe("Open `/ck:kanban` dashboard"); + expect(changes).toBe(1); + }); + + it("transforms /journal to /ck:journal", () => { + const input = "Write with `/journal`"; + const { transformed, changes } = transformCommandContent(input); + expect(transformed).toBe("Write with `/ck:journal`"); + expect(changes).toBe(1); + }); + + it("transforms /debug to /ck:debug", () => { + const input = "Use `/debug` for more info"; + const { transformed, changes } = transformCommandContent(input); + expect(transformed).toBe("Use `/ck:debug` for more info"); + expect(changes).toBe(1); + }); + + it("transforms /watzup to /ck:watzup", () => { + const input = "Check `/watzup` for changes"; + const { transformed, changes } = transformCommandContent(input); + expect(transformed).toBe("Check `/ck:watzup` for changes"); + expect(changes).toBe(1); + }); + }); + + describe("multiple transformations", () => { + it("transforms multiple commands in same content", () => { + const input = "Use `/plan:fast` then `/code:auto` then `/fix:types`"; + const { transformed, changes } = transformCommandContent(input); + expect(transformed).toBe("Use `/ck:plan:fast` then `/ck:code:auto` then `/ck:fix:types`"); + expect(changes).toBe(3); + }); + + it("transforms commands across multiple lines", () => { + const input = `1. Run /plan:hard +2. Execute /code:parallel +3. Verify with /review:codebase`; + const { transformed, changes } = transformCommandContent(input); + expect(transformed).toBe(`1. Run /ck:plan:hard +2. Execute /ck:code:parallel +3. Verify with /ck:review:codebase`); + expect(changes).toBe(3); + }); + }); + + describe("edge cases - should NOT transform", () => { + it("does not transform URLs containing command-like paths", () => { + const input = "Visit https://example.com/plan:something"; + const { transformed, changes } = transformCommandContent(input); + expect(transformed).toBe(input); + expect(changes).toBe(0); + }); + + it("does not transform already-prefixed commands", () => { + const input = "Use `/ck:plan:fast` (already prefixed)"; + const { transformed, changes } = transformCommandContent(input); + expect(transformed).toBe(input); + expect(changes).toBe(0); + }); + + it("does not transform word boundaries incorrectly", () => { + const input = "The planning process uses /plan:fast"; + const { transformed, changes } = transformCommandContent(input); + expect(transformed).toBe("The planning process uses /ck:plan:fast"); + expect(changes).toBe(1); + }); + + it("does not transform partial matches in middle of words", () => { + const input = "This is someplan:thing"; + const { transformed, changes } = transformCommandContent(input); + expect(transformed).toBe(input); + expect(changes).toBe(0); + }); + }); + + describe("context preservation", () => { + it("preserves backtick wrapping", () => { + const input = "Run ``/plan:fast`` command"; + const { transformed } = transformCommandContent(input); + expect(transformed).toContain("/ck:plan:fast"); + }); + + it("preserves markdown formatting", () => { + const input = "**Important:** Use `/fix:hard` for complex issues"; + const { transformed } = transformCommandContent(input); + expect(transformed).toBe("**Important:** Use `/ck:fix:hard` for complex issues"); + }); + + it("handles commands at start of line", () => { + const input = "/plan:fast is the command"; + const { transformed, changes } = transformCommandContent(input); + expect(transformed).toBe("/ck:plan:fast is the command"); + expect(changes).toBe(1); + }); + + it("handles commands at end of line", () => { + const input = "Use this command: /brainstorm"; + const { transformed, changes } = transformCommandContent(input); + expect(transformed).toBe("Use this command: /ck:brainstorm"); + expect(changes).toBe(1); + }); + }); + + describe("real-world content examples", () => { + it("transforms markdown file content", () => { + const input = `## Workflow + +- Decide to use \`/plan:fast\` or \`/plan:hard\` SlashCommands based on the complexity. +- Execute SlashCommand: \`/plan:fast \` or \`/plan:hard \``; + + const { transformed, changes } = transformCommandContent(input); + expect(transformed).toContain("/ck:plan:fast"); + expect(transformed).toContain("/ck:plan:hard"); + expect(changes).toBe(4); + }); + + it("transforms agent definition content", () => { + const input = + "Use the **Skill tool** to invoke `/plan:fast` or `/plan:hard` SlashCommand based on complexity."; + + const { transformed, changes } = transformCommandContent(input); + expect(transformed).toBe( + "Use the **Skill tool** to invoke `/ck:plan:fast` or `/ck:plan:hard` SlashCommand based on complexity.", + ); + expect(changes).toBe(2); + }); + }); + + describe("no changes needed", () => { + it("returns 0 changes for content without commands", () => { + const input = "This is regular content without any slash commands"; + const { transformed, changes } = transformCommandContent(input); + expect(transformed).toBe(input); + expect(changes).toBe(0); + }); + + it("returns 0 changes for empty content", () => { + const input = ""; + const { transformed, changes } = transformCommandContent(input); + expect(transformed).toBe(""); + expect(changes).toBe(0); + }); + }); +}); diff --git a/src/domains/config/merger/merge-engine.ts b/src/domains/config/merger/merge-engine.ts index a5f61ec0..7b3021d9 100644 --- a/src/domains/config/merger/merge-engine.ts +++ b/src/domains/config/merger/merge-engine.ts @@ -1,14 +1,16 @@ /** * Core merge logic for settings */ -import { logger } from "@/shared/logger.js"; +import { logger, normalizeCommand } from "@/shared"; import { mergeHookEntries } from "./conflict-resolver.js"; +import { extractCommands } from "./diff-calculator.js"; import type { HookConfig, HookEntry, MergeOptions, MergeResult, SettingsJson } from "./types.js"; /** * Merge hooks configurations * User hooks are preserved, CK hooks are added (deduplicated by command) * Respects user deletions when installedSettings is provided + * Removes deprecated hooks (in installed but not in source) */ export function mergeHooks( sourceHooks: Record, @@ -20,6 +22,12 @@ export function mergeHooks( const installedHooks = options?.installedSettings?.hooks ?? []; const sourceKit = options?.sourceKit; + // Extract all commands from source for deprecation check + const sourceCommands = new Set(); + for (const entries of Object.values(sourceHooks)) { + extractCommands(entries, sourceCommands); + } + for (const [eventName, sourceEntries] of Object.entries(sourceHooks)) { const destEntries = destHooks[eventName] || []; merged[eventName] = mergeHookEntries( @@ -32,13 +40,85 @@ export function mergeHooks( ); } + // Remove deprecated hooks: in installedHooks but NOT in source + // These are hooks that CK previously installed but no longer ships + if (installedHooks.length > 0) { + const deprecatedHooks = installedHooks.filter( + (hook) => !sourceCommands.has(normalizeCommand(hook)), + ); + + if (deprecatedHooks.length > 0) { + result.removedHooks = result.removedHooks ?? []; + + for (const [eventName, entries] of Object.entries(merged)) { + const filtered = removeDeprecatedFromEntries( + entries as (HookConfig | HookEntry)[], + deprecatedHooks, + result, + ); + if (filtered.length > 0) { + merged[eventName] = filtered; + } else { + // Remove empty event arrays + delete merged[eventName]; + } + } + } + } + return merged; } +/** + * Remove deprecated hooks from entries array + * Returns filtered entries with deprecated hooks removed + */ +function removeDeprecatedFromEntries( + entries: (HookConfig | HookEntry)[], + deprecatedHooks: string[], + result: MergeResult, +): (HookConfig | HookEntry)[] { + const deprecatedSet = new Set(deprecatedHooks.map((h) => normalizeCommand(h))); + const filtered: (HookConfig | HookEntry)[] = []; + + for (const entry of entries) { + if ("hooks" in entry && entry.hooks) { + // HookConfig with hooks array - filter individual hooks + const remainingHooks = entry.hooks.filter((h) => { + if (h.command && deprecatedSet.has(normalizeCommand(h.command))) { + result.hooksRemoved++; + result.removedHooks?.push(h.command); + logger.info(`Removed deprecated hook: ${h.command.slice(0, 60)}...`); + return false; + } + return true; + }); + if (remainingHooks.length > 0) { + filtered.push({ ...entry, hooks: remainingHooks }); + } + } else if ("command" in entry) { + // Single HookEntry + if (deprecatedSet.has(normalizeCommand(entry.command))) { + result.hooksRemoved++; + result.removedHooks?.push(entry.command); + logger.info(`Removed deprecated hook: ${entry.command.slice(0, 60)}...`); + } else { + filtered.push(entry); + } + } else { + // Unknown structure, keep it + filtered.push(entry); + } + } + + return filtered; +} + /** * Merge MCP configurations * User servers are preserved, new CK servers are added * Respects user deletions when installedSettings is provided + * Removes deprecated servers (in installed but not in source) */ export function mergeMcp( sourceMcp: SettingsJson["mcp"], @@ -135,6 +215,31 @@ export function mergeMcp( } } + // Remove deprecated servers: in installedServers but NOT in source + // These are servers that CK previously installed but no longer ships + if (installedServers.length > 0 && merged.servers) { + const sourceServerNames = new Set(Object.keys(sourceMcp.servers || {})); + const deprecatedServers = installedServers.filter((server) => !sourceServerNames.has(server)); + + if (deprecatedServers.length > 0) { + result.removedMcpServers = result.removedMcpServers ?? []; + + for (const serverName of deprecatedServers) { + if (serverName in merged.servers) { + delete merged.servers[serverName]; + result.mcpServersRemoved++; + result.removedMcpServers.push(serverName); + logger.info(`Removed deprecated MCP server: ${serverName}`); + } + } + + // Clean up empty servers object + if (merged.servers && Object.keys(merged.servers).length === 0) { + merged.servers = undefined; + } + } + } + // Copy other MCP keys that don't exist for (const key of Object.keys(sourceMcp)) { if (key !== "servers" && !(key in merged)) { @@ -163,8 +268,10 @@ export function mergeSettings( hooksAdded: 0, hooksPreserved: 0, hooksSkipped: 0, + hooksRemoved: 0, mcpServersPreserved: 0, mcpServersSkipped: 0, + mcpServersRemoved: 0, conflictsDetected: [], newlyInstalledHooks: [], newlyInstalledServers: [], diff --git a/src/domains/config/merger/types.ts b/src/domains/config/merger/types.ts index ee00003f..f8e48cd0 100644 --- a/src/domains/config/merger/types.ts +++ b/src/domains/config/merger/types.ts @@ -58,8 +58,10 @@ export interface MergeResult { hooksAdded: number; hooksPreserved: number; hooksSkipped: number; // Hooks skipped because user removed them + hooksRemoved: number; // Hooks removed because kit no longer ships them mcpServersPreserved: number; mcpServersSkipped: number; // Servers skipped because user removed them + mcpServersRemoved: number; // Servers removed because kit no longer ships them mcpServersOverwritten?: number; // Servers overwritten due to timestamp comparison conflictsDetected: string[]; // Track what was actually installed (for persistence) @@ -70,6 +72,9 @@ export interface MergeResult { /** Conflict resolution tracking for summary display */ hookConflicts?: HookConflictInfo[]; mcpConflicts?: McpConflictInfo[]; + /** Deprecated entries removed during this merge */ + removedHooks?: string[]; + removedMcpServers?: string[]; } // Options for merge operations diff --git a/src/services/transformers/commands-prefix.ts b/src/services/transformers/commands-prefix.ts index efe9f087..1996fe7e 100644 --- a/src/services/transformers/commands-prefix.ts +++ b/src/services/transformers/commands-prefix.ts @@ -5,12 +5,21 @@ * Moves all command files from `.claude/commands/**\/*` to `.claude/commands/ck/**\/*` * This enables all slash commands to have a /ck: prefix (e.g., /ck:plan, /ck:fix) * + * Also transforms command references in file contents: + * - `/plan:fast` → `/ck:plan:fast` + * - `/fix:types` → `/ck:fix:types` + * - etc. + * * This file re-exports all public APIs from the modular implementation. */ // Re-export types export type { CleanupOptions } from "./commands-prefix/prefix-utils.js"; export type { CleanupResult } from "./commands-prefix/prefix-cleaner.js"; +export type { + ContentTransformOptions, + ContentTransformResult, +} from "./commands-prefix/content-transformer.js"; // Import functions for class-based API import { applyPrefix } from "./commands-prefix/prefix-applier.js"; diff --git a/src/services/transformers/commands-prefix/content-transformer.ts b/src/services/transformers/commands-prefix/content-transformer.ts new file mode 100644 index 00000000..c457f8e1 --- /dev/null +++ b/src/services/transformers/commands-prefix/content-transformer.ts @@ -0,0 +1,192 @@ +/** + * Content Transformer for Command Prefix + * + * Transforms slash command references in file contents when --prefix is applied. + * Changes `/plan:fast` → `/ck:plan:fast`, `/fix:types` → `/ck:fix:types`, etc. + * + * This complements prefix-applier.ts which only handles directory restructuring. + */ + +import { readFile, readdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { logger } from "@/shared/logger.js"; + +export interface ContentTransformOptions { + verbose?: boolean; + dryRun?: boolean; +} + +export interface ContentTransformResult { + filesTransformed: number; + totalReplacements: number; +} + +/** + * File extensions to process for content transformation + */ +const TRANSFORMABLE_EXTENSIONS = new Set([ + ".md", + ".txt", + ".json", + ".yaml", + ".yml", + ".ts", + ".js", + ".mjs", + ".cjs", + ".py", +]); + +/** + * Slash command prefixes to transform + * These are ClaudeKit commands (not built-in Claude commands like /tasks, /help) + */ +const COMMAND_ROOTS = [ + // Primary workflow commands + "plan", + "fix", + "code", + "review", + "cook", + "brainstorm", + // Integration & setup + "integrate", + "bootstrap", + "worktree", + "scout", + // Utility commands + "test", + "debug", + "preview", + "kanban", + "journal", + "watzup", +]; + +/** + * Build regex patterns for command transformation + * + * Matches patterns like: + * - `/plan:fast` → `/ck:plan:fast` + * - `/fix:types` → `/ck:fix:types` + * - `/brainstorm` → `/ck:brainstorm` (commands without sub-commands) + * - backtick-wrapped: `\`/plan:fast\`` → `\`/ck:plan:fast\`` + * + * Does NOT match: + * - URLs like `https://example.com/plan:` + * - Already prefixed like `/ck:plan:` + */ +function buildCommandPatterns(): Array<{ regex: RegExp; replacement: string }> { + const patterns: Array<{ regex: RegExp; replacement: string }> = []; + + for (const cmd of COMMAND_ROOTS) { + // Pattern 1: /cmd: or /cmd followed by word boundary (for commands with sub-commands) + // Negative lookbehind (?.,;:!?]|$)`, "g"), + replacement: `$1ck:${cmd}`, + }); + } + + return patterns; +} + +/** + * Transform content by replacing command references + */ +export function transformCommandContent(content: string): { transformed: string; changes: number } { + let changes = 0; + let transformed = content; + + const patterns = buildCommandPatterns(); + + for (const { regex, replacement } of patterns) { + regex.lastIndex = 0; + const matches = transformed.match(regex); + if (matches) { + changes += matches.length; + regex.lastIndex = 0; + transformed = transformed.replace(regex, replacement); + } + } + + return { transformed, changes }; +} + +/** + * Check if a file should be transformed based on extension + */ +function shouldTransformFile(filename: string): boolean { + const ext = filename.toLowerCase().slice(filename.lastIndexOf(".")); + return TRANSFORMABLE_EXTENSIONS.has(ext); +} + +/** + * Recursively transform command references in all files + * + * @param directory - Root directory to process (typically extractDir/.claude) + * @param options - Transform options + * @returns Statistics about transformations made + */ +export async function transformCommandReferences( + directory: string, + options: ContentTransformOptions = {}, +): Promise { + let filesTransformed = 0; + let totalReplacements = 0; + + async function processDirectory(dir: string): Promise { + const entries = await readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = join(dir, entry.name); + + if (entry.isDirectory()) { + // Skip node_modules and hidden directories (except .claude) + if ( + entry.name === "node_modules" || + (entry.name.startsWith(".") && entry.name !== ".claude") + ) { + continue; + } + await processDirectory(fullPath); + } else if (entry.isFile() && shouldTransformFile(entry.name)) { + try { + const content = await readFile(fullPath, "utf-8"); + const { transformed, changes } = transformCommandContent(content); + + if (changes > 0) { + if (options.dryRun) { + logger.debug(`[dry-run] Would transform ${changes} command ref(s) in ${fullPath}`); + } else { + await writeFile(fullPath, transformed, "utf-8"); + if (options.verbose) { + logger.verbose(`Transformed ${changes} command ref(s) in ${fullPath}`); + } + } + filesTransformed++; + totalReplacements += changes; + } + } catch (error) { + // Skip files that can't be read (binary, permissions, etc.) + logger.debug( + `Skipped ${fullPath}: ${error instanceof Error ? error.message : "unknown"}`, + ); + } + } + } + } + + await processDirectory(directory); + + return { filesTransformed, totalReplacements }; +} diff --git a/src/services/transformers/commands-prefix/prefix-applier.ts b/src/services/transformers/commands-prefix/prefix-applier.ts index a7ff9c74..9d1b9a78 100644 --- a/src/services/transformers/commands-prefix/prefix-applier.ts +++ b/src/services/transformers/commands-prefix/prefix-applier.ts @@ -1,14 +1,16 @@ /** * Prefix Applier * - * Handles applying /ck: prefix to slash commands by reorganizing - * the commands directory structure. + * Handles applying /ck: prefix to slash commands by: + * 1. Reorganizing the commands directory structure + * 2. Transforming command references in file contents */ import { lstat, mkdir, readdir, stat } from "node:fs/promises"; import { join } from "node:path"; import { logger } from "@/shared/logger.js"; import { copy, move, pathExists, remove } from "fs-extra"; +import { transformCommandReferences } from "./content-transformer.js"; import { validatePath } from "./prefix-utils.js"; /** @@ -119,7 +121,22 @@ export async function applyPrefix(extractDir: string): Promise { // Cleanup backup after successful operation await remove(backupDir); - logger.success("Successfully applied /ck: prefix to all commands"); + logger.success("Successfully reorganized commands to /ck: prefix"); + + // Transform command references in file contents + const claudeDir = join(extractDir, ".claude"); + logger.info("Transforming command references in file contents..."); + const transformResult = await transformCommandReferences(claudeDir, { + verbose: logger.isVerbose(), + }); + + if (transformResult.totalReplacements > 0) { + logger.success( + `Transformed ${transformResult.totalReplacements} command ref(s) in ${transformResult.filesTransformed} file(s)`, + ); + } else { + logger.verbose("No command references needed transformation"); + } } catch (error) { // Restore backup if exists if (await pathExists(backupDir)) {