Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions bin/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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
*/
Expand Down Expand Up @@ -77,6 +98,36 @@ function copyDir(srcDir, destDir, skipDirs = []) {
}
}

/**
* Recursively copy directory, rewriting framework refs in text files.
* Replaces @~/.claude/<x>-framework/ and @./.claude/<x>-framework/ with
* ${CLAUDE_PLUGIN_ROOT}/<x>-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/<x>-framework/ → ${CLAUDE_PLUGIN_ROOT}/<x>-framework/
// Rewrite @./.claude/<x>-framework/ → ${CLAUDE_PLUGIN_ROOT}/<x>-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
*/
Expand All @@ -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
Expand All @@ -103,6 +163,8 @@ if (hasHelp) {
${yellow}Options:${reset}
${cyan}-l, --local${reset} Install to ./.claude/commands/ instead of global
${cyan}-c, --config-dir <path>${reset} Specify custom Claude config directory
${cyan}--skills-dir${reset} Install as a Claude Code skills-directory plugin
${cyan} --dir <path>${reset} Target directory for --skills-dir (default: ./.claude/skills/seed/)
${cyan}-h, --help${reset} Show this help message

${yellow}Examples:${reset}
Expand All @@ -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)
Expand All @@ -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);
Expand Down