diff --git a/bin/install.js b/bin/install.js index 6787fba..d409546 100755 --- a/bin/install.js +++ b/bin/install.js @@ -30,6 +30,7 @@ ${green} ███████╗███████╗███████ const args = process.argv.slice(2); const hasHelp = args.includes('--help') || args.includes('-h'); const hasLocal = args.includes('--local') || args.includes('-l'); +const hasSkillsDir = args.includes('--skills-dir'); // Parse --config-dir argument function parseConfigDirArg() { @@ -49,6 +50,26 @@ function parseConfigDirArg() { return null; } +/** + * Parse --dir argument (used with --skills-dir) + */ +function parseDirArg() { + const idx = args.findIndex(arg => arg === '--dir'); + if (idx !== -1) { + const nextArg = args[idx + 1]; + if (!nextArg || nextArg.startsWith('-')) { + console.error(` ${yellow}--dir requires a path argument${reset}`); + process.exit(1); + } + return nextArg; + } + const dirArg = args.find(arg => arg.startsWith('--dir=')); + if (dirArg) { + return dirArg.split('=').slice(1).join('='); + } + return null; +} + /** * Expand ~ to home directory */ @@ -77,6 +98,36 @@ function copyDir(srcDir, destDir, skipDirs = []) { } } +/** + * Recursively copy directory, rewriting framework refs in text files. + * Replaces @~/.claude/-framework/ and @./.claude/-framework/ with + * ${CLAUDE_PLUGIN_ROOT}/-framework/ so the plugin is self-contained. + */ +function copyDirWithRefRewrite(srcDir, destDir, skipDirs = []) { + fs.mkdirSync(destDir, { recursive: true }); + const entries = fs.readdirSync(srcDir, { withFileTypes: true }); + for (const entry of entries) { + if (skipDirs.includes(entry.name)) continue; + const srcPath = path.join(srcDir, entry.name); + const destPath = path.join(destDir, entry.name); + if (entry.isDirectory()) { + copyDirWithRefRewrite(srcPath, destPath, skipDirs); + } else { + const content = fs.readFileSync(srcPath, 'utf8'); + // Rewrite @~/.claude/-framework/ → ${CLAUDE_PLUGIN_ROOT}/-framework/ + // Rewrite @./.claude/-framework/ → ${CLAUDE_PLUGIN_ROOT}/-framework/ + const rewritten = content + .replace(/@~\/\.claude\/([^/\s]+)/g, '${CLAUDE_PLUGIN_ROOT}/$1') + .replace(/@\.\/\.claude\/([^/\s]+)/g, '${CLAUDE_PLUGIN_ROOT}/$1'); + if (rewritten !== content) { + fs.writeFileSync(destPath, rewritten, 'utf8'); + } else { + fs.copyFileSync(srcPath, destPath); + } + } + } +} + /** * Count files recursively */ @@ -94,6 +145,15 @@ function countFiles(dir, ext) { return count; } +/** + * Derive a short plugin name from the package name. + * "@chrisai/seed" → "seed", "seed" → "seed" + */ +function shortName(pkgName) { + const parts = pkgName.split('/'); + return parts[parts.length - 1]; +} + console.log(banner); // Show help @@ -103,6 +163,8 @@ if (hasHelp) { ${yellow}Options:${reset} ${cyan}-l, --local${reset} Install to ./.claude/commands/ instead of global ${cyan}-c, --config-dir ${reset} Specify custom Claude config directory + ${cyan}--skills-dir${reset} Install as a Claude Code skills-directory plugin + ${cyan} --dir ${reset} Target directory for --skills-dir (default: ./.claude/skills/seed/) ${cyan}-h, --help${reset} Show this help message ${yellow}Examples:${reset} @@ -112,6 +174,12 @@ if (hasHelp) { ${dim}# Install to current project only${reset} npx @chrisai/seed --local + ${dim}# Install as a skills-directory plugin (default target: .claude/skills/seed/)${reset} + npx @chrisai/seed --skills-dir + + ${dim}# Install as a skills-directory plugin to a custom path${reset} + npx @chrisai/seed --skills-dir --dir /path/to/.claude/skills/seed + ${yellow}What gets installed:${reset} ${cyan}commands/seed/${reset} seed.md Entry point (routing + persona) @@ -123,6 +191,94 @@ if (hasHelp) { process.exit(0); } +// ─── Skills-dir install mode ─────────────────────────────────────────────── + +if (hasSkillsDir) { + const short = shortName(pkg.name); + const explicitDir = expandTilde(parseDirArg()); + const skillsTarget = explicitDir || path.join(process.cwd(), '.claude', 'skills', short); + + const locationLabel = skillsTarget.replace(os.homedir(), '~').replace(process.cwd(), '.'); + + // Check if already installed + if (fs.existsSync(skillsTarget)) { + console.log(` ${yellow}Existing skills-dir installation found at ${locationLabel}${reset}`); + console.log(` Updating...`); + fs.rmSync(skillsTarget, { recursive: true, force: true }); + } + + console.log(` Installing as skills-directory plugin to ${cyan}${locationLabel}${reset}\n`); + + // Write .claude-plugin/plugin.json + const pluginMetaDir = path.join(skillsTarget, '.claude-plugin'); + fs.mkdirSync(pluginMetaDir, { recursive: true }); + const pluginJson = { + name: short, + version: pkg.version, + description: pkg.description, + }; + fs.writeFileSync( + path.join(pluginMetaDir, 'plugin.json'), + JSON.stringify(pluginJson, null, 2) + '\n', + 'utf8' + ); + console.log(` ${green}+${reset} .claude-plugin/plugin.json ${dim}(name=${short}, version=${pkg.version})${reset}`); + + // This package has commands/ only — copy them with ref rewriting + const src = path.join(__dirname, '..'); + const commandsSrc = path.join(src, 'commands') || null; + const seedCommandSrc = path.join(src); // seed files live at root level, installed under commands/seed/ + const commandsDest = path.join(skillsTarget, 'commands', short); + + fs.mkdirSync(commandsDest, { recursive: true }); + + // Copy entry point + fs.copyFileSync(path.join(src, 'seed.md'), path.join(commandsDest, 'seed.md')); + console.log(` ${green}+${reset} commands/${short}/seed.md ${dim}(entry point)${reset}`); + + // Copy tasks + const tasksSrc = path.join(src, 'tasks'); + const tasksDest = path.join(commandsDest, 'tasks'); + copyDirWithRefRewrite(tasksSrc, tasksDest); + const taskCount = countFiles(tasksSrc, '.md'); + console.log(` ${green}+${reset} commands/${short}/tasks/ ${dim}(${taskCount} task files)${reset}`); + + // Copy data + const dataSrc = path.join(src, 'data'); + const dataDest = path.join(commandsDest, 'data'); + copyDirWithRefRewrite(dataSrc, dataDest); + const dataCount = countFiles(dataSrc, '.md'); + const typeCount = fs.readdirSync(dataSrc, { withFileTypes: true }).filter(e => e.isDirectory()).length; + console.log(` ${green}+${reset} commands/${short}/data/ ${dim}(${typeCount} types, ${dataCount} files)${reset}`); + + // Copy templates + const templatesSrc = path.join(src, 'templates'); + const templatesDest = path.join(commandsDest, 'templates'); + copyDirWithRefRewrite(templatesSrc, templatesDest); + const templateCount = countFiles(templatesSrc, '.md'); + console.log(` ${green}+${reset} commands/${short}/templates/ ${dim}(${templateCount} planning templates)${reset}`); + + // Copy checklists + const checklistsSrc = path.join(src, 'checklists'); + const checklistsDest = path.join(commandsDest, 'checklists'); + copyDirWithRefRewrite(checklistsSrc, checklistsDest); + console.log(` ${green}+${reset} commands/${short}/checklists/ ${dim}(planning quality gate)${reset}`); + + console.log(` + ${green}Done!${reset} Plugin installed as ${cyan}${short}@skills-dir${reset} + + ${yellow}Notes:${reset} + - This plugin loads ${dim}next session${reset} (skills-dir plugins require a Claude Code restart) + - Requires ${dim}workspace trust${reset} to be enabled for this directory + - For ${dim}Claude Code Cloud${reset}: commit ${cyan}.claude/skills/${short}/${reset} to your repository + so team members and cloud agents can load the plugin automatically + - Access commands via ${cyan}/${short}${reset} once loaded +`); + process.exit(0); +} + +// ─── Standard install mode ──────────────────────────────────────────────── + // Determine install target const explicitConfigDir = parseConfigDirArg(); const configDir = expandTilde(explicitConfigDir) || expandTilde(process.env.CLAUDE_CONFIG_DIR);