From 871d11614eb2a9634047246b9842b2d49afc78cb Mon Sep 17 00:00:00 2001 From: phuthuycoding Date: Mon, 1 Jun 2026 10:22:32 +0700 Subject: [PATCH] refactor(install): split install.ts into focused modules Break the 840-line install.ts into src/commands/install/ submodules and collapse the near-identical Codex/Antigravity logic into one parameterized skill-editor module (~200 lines of duplication removed). - print/transform/prompts/usage: shared helpers - native: Claude symlink/copy install - skill-editor: unified Codex + Antigravity (target-parameterized) - generic-editor: Cursor/Windsurf rules-file merge - index: installCommand orchestrator; install.ts is now a barrel re-export so bin/cli.js and src/index.ts paths are unchanged Also fix the post-install usage banner, which had drifted from assets/: correct agent/command/skill counts, add /marketing, namespace skills, and cover all editors (Claude, Codex, Antigravity, Cursor, Windsurf). Add the missing editors to the interactive target menu and postinstall examples. Behavior is unchanged; verified with tsc and smoke tests of all three install strategies. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/commands/install.ts | 832 +------------------------ src/commands/install/generic-editor.ts | 63 ++ src/commands/install/index.ts | 86 +++ src/commands/install/native.ts | 100 +++ src/commands/install/print.ts | 30 + src/commands/install/prompts.ts | 48 ++ src/commands/install/skill-editor.ts | 175 ++++++ src/commands/install/transform.ts | 60 ++ src/commands/install/usage.ts | 78 +++ src/commands/postinstall.ts | 5 +- 10 files changed, 645 insertions(+), 832 deletions(-) create mode 100644 src/commands/install/generic-editor.ts create mode 100644 src/commands/install/index.ts create mode 100644 src/commands/install/native.ts create mode 100644 src/commands/install/print.ts create mode 100644 src/commands/install/prompts.ts create mode 100644 src/commands/install/skill-editor.ts create mode 100644 src/commands/install/transform.ts create mode 100644 src/commands/install/usage.ts diff --git a/src/commands/install.ts b/src/commands/install.ts index a5145d3..0d7911e 100644 --- a/src/commands/install.ts +++ b/src/commands/install.ts @@ -1,831 +1 @@ -import chalk from 'chalk'; -import inquirer from 'inquirer'; -import path from 'path'; -import fs from 'fs'; -import type { CommandOptions, EditorTarget, FileResult, Scope } from '../types.js'; -import { - ASSETS_DIR, - EDITOR_CONFIGS, - isSymlinkSupported, - ensureDir, - createSymlink, - copyFile, - copyDir, - getAgentsDir, - getCommandsDir, - getSkillsDir, - getArchitectureDir, - getClaudeDir, - getCodexDir, - getAntigravityDir, - getEditorDir, - getEditorConfig, - getFiles, - getDirs, - mergeAgentsToFile, -} from '../utils/symlink.js'; -import { addTarget } from '../utils/config.js'; - -const printHeader = (): void => { - console.log(''); - console.log(chalk.cyan('════════════════════════════════════════')); - console.log(chalk.cyan(' MoiCle Installer')); - console.log(chalk.cyan('════════════════════════════════════════')); - console.log(''); -}; - -const printSummary = (results: FileResult[]): void => { - const created = results.filter((r) => r.status === 'created').length; - const updated = results.filter((r) => r.status === 'updated').length; - const exists = results.filter((r) => r.status === 'exists').length; - const skipped = results.filter((r) => r.status === 'skipped').length; - const errors = results.filter((r) => r.status === 'error').length; - - if (created > 0) console.log(chalk.green(` Created: ${created}`)); - if (updated > 0) console.log(chalk.yellow(` Updated: ${updated}`)); - if (exists > 0) console.log(chalk.gray(` Already exists: ${exists}`)); - if (skipped > 0) console.log(chalk.gray(` Skipped: ${skipped}`)); - if (errors > 0) console.log(chalk.red(` Errors: ${errors}`)); -}; - -const installAgents = async (targetDir: string, useSymlink = true): Promise => { - const results: FileResult[] = []; - - ensureDir(targetDir); - - const developersDir = path.join(ASSETS_DIR, 'agents', 'developers'); - if (fs.existsSync(developersDir)) { - const files = getFiles(developersDir); - for (const file of files) { - const target = path.join(targetDir, path.basename(file)); - const result = useSymlink ? createSymlink(file, target) : copyFile(file, target); - results.push(result); - } - } - - const utilitiesDir = path.join(ASSETS_DIR, 'agents', 'utilities'); - if (fs.existsSync(utilitiesDir)) { - const files = getFiles(utilitiesDir); - for (const file of files) { - const target = path.join(targetDir, path.basename(file)); - const result = useSymlink ? createSymlink(file, target) : copyFile(file, target); - results.push(result); - } - } - - console.log(chalk.green(` ✓ Agents installed to ${chalk.cyan(targetDir)}`)); - printSummary(results); - - return results; -}; - -const installCommands = async (targetDir: string, useSymlink = true): Promise => { - const results: FileResult[] = []; - - ensureDir(targetDir); - - const commandsDir = path.join(ASSETS_DIR, 'commands'); - if (fs.existsSync(commandsDir)) { - const files = getFiles(commandsDir); - for (const file of files) { - const target = path.join(targetDir, path.basename(file)); - const result = useSymlink ? createSymlink(file, target) : copyFile(file, target); - results.push(result); - } - } - - console.log(chalk.green(` ✓ Commands installed to ${chalk.cyan(targetDir)}`)); - printSummary(results); - - return results; -}; - -const installSkills = async (targetDir: string, useSymlink = true): Promise => { - const results: FileResult[] = []; - - ensureDir(targetDir); - - const skillsDir = path.join(ASSETS_DIR, 'skills'); - if (fs.existsSync(skillsDir)) { - const dirs = getDirs(skillsDir); - for (const dir of dirs) { - const target = path.join(targetDir, path.basename(dir)); - if (useSymlink) { - const result = createSymlink(dir, target); - results.push(result); - } else { - const result = copyDir(dir, target); - results.push(result); - } - } - } - - console.log(chalk.green(` ✓ Skills installed to ${chalk.cyan(targetDir)}`)); - printSummary(results); - - return results; -}; - -const installArchitecture = async ( - targetDir: string, - useSymlink = true -): Promise => { - const results: FileResult[] = []; - - ensureDir(targetDir); - - const archDir = path.join(ASSETS_DIR, 'architecture'); - if (fs.existsSync(archDir)) { - const files = getFiles(archDir); - for (const file of files) { - const target = path.join(targetDir, path.basename(file)); - const result = useSymlink ? createSymlink(file, target) : copyFile(file, target); - results.push(result); - } - } - - console.log(chalk.green(` ✓ Architecture installed to ${chalk.cyan(targetDir)}`)); - printSummary(results); - - return results; -}; - -const installScope = async (scope: Scope, useSymlink: boolean): Promise => { - const isGlobal = scope === 'global'; - const label = isGlobal ? 'Global' : 'Project'; - const targetPath = isGlobal ? '~/.claude/' : `${process.cwd()}/.claude/`; - - console.log(''); - console.log(chalk.cyan(`>>> ${label} Installation`)); - console.log(chalk.gray(` Target: ${targetPath}`)); - console.log(''); - - const claudeDir = getClaudeDir(scope); - ensureDir(claudeDir); - - await installAgents(getAgentsDir(scope), useSymlink); - if (isGlobal) { - await installCommands(getCommandsDir(scope), useSymlink); - } - await installSkills(getSkillsDir(scope), useSymlink); - await installArchitecture(getArchitectureDir(scope), useSymlink); - - if (!isGlobal) { - console.log(chalk.gray(' Note: Commands are installed globally only')); - } - - console.log(''); - console.log(chalk.green(`✓ ${label} installation complete!`)); -}; - -const rewriteClaudePaths = (content: string, target: 'claude' | 'codex' | 'antigravity'): string => { - if (target === 'claude') { - return content; - } - if (target === 'antigravity') { - return content - .replace(/~\/\.claude\//g, '~/.gemini/') - .replace(/\.claude\//g, '.gemini/') - .replace(/Claude Code/g, 'Antigravity') - .replace(/CLAUDE\.md/g, 'GEMINI.md'); - } - - return content - .replace(/~\/\.claude\//g, '~/.codex/') - .replace(/\.claude\//g, '.codex/') - .replace(/Claude Code/g, 'Codex CLI') - .replace(/CLAUDE\.md/g, 'AGENTS.md'); -}; - -const ensureCodexSkillDir = (baseDir: string, name: string): string => { - const skillDir = path.join(baseDir, name); - ensureDir(skillDir); - return skillDir; -}; - -const installCodexSkillFolder = ( - sourceDir: string, - targetSkillsDir: string -): FileResult => { - const skillName = path.basename(sourceDir); - const targetDir = ensureCodexSkillDir(targetSkillsDir, skillName); - const sourceFiles = getFiles(sourceDir, 8); - - let status: FileResult['status'] = 'created'; - for (const file of sourceFiles) { - const relativePath = path.relative(sourceDir, file); - const targetFile = path.join(targetDir, relativePath); - ensureDir(path.dirname(targetFile)); - - const content = rewriteClaudePaths(fs.readFileSync(file, 'utf-8'), 'codex'); - const existed = fs.existsSync(targetFile); - if (existed && fs.readFileSync(targetFile, 'utf-8') === content) { - status = status === 'created' ? 'exists' : status; - continue; - } - - fs.writeFileSync(targetFile, content); - if (existed) { - status = 'updated'; - } - } - - return { status, name: skillName }; -}; - -const buildGeneratedCodexSkill = ( - name: string, - description: string, - body: string -): string => `--- -name: ${name} -description: ${description} ---- - -${body} -`; - -const extractFrontmatter = ( - content: string -): { frontmatter: string | null; body: string; description?: string } => { - const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/); - if (!match) { - return { frontmatter: null, body: content }; - } - - const frontmatter = match[1]; - const body = match[2]; - const descriptionMatch = frontmatter.match(/^description:\s*(.+)$/m); - - return { - frontmatter, - body, - description: descriptionMatch?.[1]?.trim(), - }; -}; - -const installGeneratedCodexSkill = ( - targetSkillsDir: string, - name: string, - description: string, - body: string -): FileResult => { - const skillDir = ensureCodexSkillDir(targetSkillsDir, name); - const targetFile = path.join(skillDir, 'SKILL.md'); - const content = buildGeneratedCodexSkill(name, description, rewriteClaudePaths(body, 'codex')); - - if (fs.existsSync(targetFile)) { - if (fs.readFileSync(targetFile, 'utf-8') === content) { - return { status: 'exists', name }; - } - fs.writeFileSync(targetFile, content); - return { status: 'updated', name }; - } - - fs.writeFileSync(targetFile, content); - return { status: 'created', name }; -}; - -const installDirectCodexSkill = ( - targetSkillsDir: string, - name: string, - content: string -): FileResult => { - const skillDir = ensureCodexSkillDir(targetSkillsDir, name); - const targetFile = path.join(skillDir, 'SKILL.md'); - const rewritten = rewriteClaudePaths(content, 'codex'); - - if (fs.existsSync(targetFile)) { - if (fs.readFileSync(targetFile, 'utf-8') === rewritten) { - return { status: 'exists', name }; - } - fs.writeFileSync(targetFile, rewritten); - return { status: 'updated', name }; - } - - fs.writeFileSync(targetFile, rewritten); - return { status: 'created', name }; -}; - -const installCodexArchitecture = (targetDir: string): FileResult[] => { - const results: FileResult[] = []; - const archDir = path.join(ASSETS_DIR, 'architecture'); - const targetArchDir = path.join(targetDir, 'architecture'); - - ensureDir(targetArchDir); - - if (!fs.existsSync(archDir)) { - return results; - } - - for (const file of getFiles(archDir)) { - const targetFile = path.join(targetArchDir, path.basename(file)); - const content = rewriteClaudePaths(fs.readFileSync(file, 'utf-8'), 'codex'); - if (fs.existsSync(targetFile)) { - if (fs.readFileSync(targetFile, 'utf-8') === content) { - results.push({ status: 'exists', name: path.basename(file) }); - continue; - } - fs.writeFileSync(targetFile, content); - results.push({ status: 'updated', name: path.basename(file) }); - continue; - } - - fs.writeFileSync(targetFile, content); - results.push({ status: 'created', name: path.basename(file) }); - } - - return results; -}; - -const installCodexSkills = (targetDir: string): FileResult[] => { - const results: FileResult[] = []; - const targetSkillsDir = path.join(targetDir, 'skills'); - ensureDir(targetSkillsDir); - - const skillsDir = path.join(ASSETS_DIR, 'skills'); - if (fs.existsSync(skillsDir)) { - for (const dir of getDirs(skillsDir)) { - results.push(installCodexSkillFolder(dir, targetSkillsDir)); - } - } - - const commandsDir = path.join(ASSETS_DIR, 'commands'); - if (fs.existsSync(commandsDir)) { - for (const file of getFiles(commandsDir)) { - const name = path.basename(file, '.md'); - const content = fs.readFileSync(file, 'utf-8'); - results.push(installDirectCodexSkill(targetSkillsDir, name, content)); - } - } - - const agentDirs = ['developers', 'utilities']; - for (const dirName of agentDirs) { - const sourceDir = path.join(ASSETS_DIR, 'agents', dirName); - if (!fs.existsSync(sourceDir)) { - continue; - } - - for (const file of getFiles(sourceDir)) { - const name = path.basename(file, '.md'); - const rawContent = fs.readFileSync(file, 'utf-8'); - const parsed = extractFrontmatter(rawContent); - const description = parsed.description - ? parsed.description - : dirName === 'developers' - ? `Imported MoiCle developer persona for ${name}. Use when the task matches this stack specialist.` - : `Imported MoiCle utility persona for ${name}. Use when the task matches this specialist.`; - - results.push(installGeneratedCodexSkill(targetSkillsDir, name, description, parsed.body.trimStart())); - } - } - - return results; -}; - -const installCodexScope = async (scope: Scope): Promise => { - const isGlobal = scope === 'global'; - const label = isGlobal ? 'Global' : 'Project'; - const targetPath = isGlobal ? '~/.codex/' : `${process.cwd()}/.codex/`; - - console.log(''); - console.log(chalk.cyan(`>>> ${label} Codex Installation`)); - console.log(chalk.gray(` Target: ${targetPath}`)); - console.log(''); - - const codexDir = getCodexDir(scope); - ensureDir(codexDir); - - const archResults = installCodexArchitecture(codexDir); - console.log(chalk.green(` ✓ Architecture installed to ${chalk.cyan(path.join(codexDir, 'architecture'))}`)); - printSummary(archResults); - - const skillResults = installCodexSkills(codexDir); - console.log(chalk.green(` ✓ Codex skills installed to ${chalk.cyan(path.join(codexDir, 'skills'))}`)); - printSummary(skillResults); - - console.log(''); - console.log(chalk.green(`✓ ${label} Codex installation complete!`)); -}; - -const ensureAntigravitySkillDir = (baseDir: string, name: string): string => { - const skillDir = path.join(baseDir, name); - ensureDir(skillDir); - return skillDir; -}; - -const installAntigravitySkillFolder = ( - sourceDir: string, - targetSkillsDir: string -): FileResult => { - const skillName = path.basename(sourceDir); - const targetDir = ensureAntigravitySkillDir(targetSkillsDir, skillName); - const sourceFiles = getFiles(sourceDir, 8); - - let status: FileResult['status'] = 'created'; - for (const file of sourceFiles) { - const relativePath = path.relative(sourceDir, file); - const targetFile = path.join(targetDir, relativePath); - ensureDir(path.dirname(targetFile)); - - const content = rewriteClaudePaths(fs.readFileSync(file, 'utf-8'), 'antigravity'); - const existed = fs.existsSync(targetFile); - if (existed && fs.readFileSync(targetFile, 'utf-8') === content) { - status = status === 'created' ? 'exists' : status; - continue; - } - - fs.writeFileSync(targetFile, content); - if (existed) { - status = 'updated'; - } - } - - return { status, name: skillName }; -}; - -const buildGeneratedAntigravitySkill = ( - name: string, - description: string, - body: string -): string => `--- -name: ${name} -description: ${description} ---- - -${body} -`; - -const installGeneratedAntigravitySkill = ( - targetSkillsDir: string, - name: string, - description: string, - body: string -): FileResult => { - const skillDir = ensureAntigravitySkillDir(targetSkillsDir, name); - const targetFile = path.join(skillDir, 'SKILL.md'); - const content = buildGeneratedAntigravitySkill(name, description, rewriteClaudePaths(body, 'antigravity')); - - if (fs.existsSync(targetFile)) { - if (fs.readFileSync(targetFile, 'utf-8') === content) { - return { status: 'exists', name }; - } - fs.writeFileSync(targetFile, content); - return { status: 'updated', name }; - } - - fs.writeFileSync(targetFile, content); - return { status: 'created', name }; -}; - -const installDirectAntigravitySkill = ( - targetSkillsDir: string, - name: string, - content: string -): FileResult => { - const skillDir = ensureAntigravitySkillDir(targetSkillsDir, name); - const targetFile = path.join(skillDir, 'SKILL.md'); - const rewritten = rewriteClaudePaths(content, 'antigravity'); - - if (fs.existsSync(targetFile)) { - if (fs.readFileSync(targetFile, 'utf-8') === rewritten) { - return { status: 'exists', name }; - } - fs.writeFileSync(targetFile, rewritten); - return { status: 'updated', name }; - } - - fs.writeFileSync(targetFile, rewritten); - return { status: 'created', name }; -}; - -const installAntigravityArchitecture = (targetDir: string): FileResult[] => { - const results: FileResult[] = []; - const archDir = path.join(ASSETS_DIR, 'architecture'); - const targetArchDir = path.join(targetDir, 'architecture'); - - ensureDir(targetArchDir); - - if (!fs.existsSync(archDir)) { - return results; - } - - for (const file of getFiles(archDir)) { - const targetFile = path.join(targetArchDir, path.basename(file)); - const content = rewriteClaudePaths(fs.readFileSync(file, 'utf-8'), 'antigravity'); - if (fs.existsSync(targetFile)) { - if (fs.readFileSync(targetFile, 'utf-8') === content) { - results.push({ status: 'exists', name: path.basename(file) }); - continue; - } - fs.writeFileSync(targetFile, content); - results.push({ status: 'updated', name: path.basename(file) }); - continue; - } - - fs.writeFileSync(targetFile, content); - results.push({ status: 'created', name: path.basename(file) }); - } - - return results; -}; - -const installAntigravitySkills = (targetDir: string): FileResult[] => { - const results: FileResult[] = []; - const targetSkillsDir = path.join(targetDir, 'skills'); - ensureDir(targetSkillsDir); - - const skillsDir = path.join(ASSETS_DIR, 'skills'); - if (fs.existsSync(skillsDir)) { - for (const dir of getDirs(skillsDir)) { - results.push(installAntigravitySkillFolder(dir, targetSkillsDir)); - } - } - - const commandsDir = path.join(ASSETS_DIR, 'commands'); - if (fs.existsSync(commandsDir)) { - for (const file of getFiles(commandsDir)) { - const name = path.basename(file, '.md'); - const content = fs.readFileSync(file, 'utf-8'); - results.push(installDirectAntigravitySkill(targetSkillsDir, name, content)); - } - } - - const agentDirs = ['developers', 'utilities']; - for (const dirName of agentDirs) { - const sourceDir = path.join(ASSETS_DIR, 'agents', dirName); - if (!fs.existsSync(sourceDir)) { - continue; - } - - for (const file of getFiles(sourceDir)) { - const name = path.basename(file, '.md'); - const rawContent = fs.readFileSync(file, 'utf-8'); - const parsed = extractFrontmatter(rawContent); - const description = parsed.description - ? parsed.description - : dirName === 'developers' - ? `Imported MoiCle developer persona for ${name}. Use when the task matches this stack specialist.` - : `Imported MoiCle utility persona for ${name}. Use when the task matches this specialist.`; - - results.push(installGeneratedAntigravitySkill(targetSkillsDir, name, description, parsed.body.trimStart())); - } - } - - return results; -}; - -const installAntigravityScope = async (scope: Scope): Promise => { - const isGlobal = scope === 'global'; - const label = isGlobal ? 'Global' : 'Project'; - const targetPath = isGlobal ? '~/.gemini/' : `${process.cwd()}/.gemini/`; - - console.log(''); - console.log(chalk.cyan(`>>> ${label} Antigravity Installation`)); - console.log(chalk.gray(` Target: ${targetPath}`)); - console.log(''); - - const antigravityDir = getAntigravityDir(scope); - ensureDir(antigravityDir); - - const archResults = installAntigravityArchitecture(antigravityDir); - console.log(chalk.green(` ✓ Architecture installed to ${chalk.cyan(path.join(antigravityDir, 'architecture'))}`)); - printSummary(archResults); - - const skillResults = installAntigravitySkills(antigravityDir); - console.log(chalk.green(` ✓ Antigravity skills installed to ${chalk.cyan(path.join(antigravityDir, 'skills'))}`)); - printSummary(skillResults); - - console.log(''); - console.log(chalk.green(`✓ ${label} Antigravity installation complete!`)); -}; - -const showTargetMenu = async (): Promise => { - const { target } = await inquirer.prompt([ - { - type: 'list', - name: 'target', - message: 'Which editor would you like to configure?', - choices: [ - { name: 'Claude Code', value: 'claude' }, - { name: 'Codex CLI', value: 'codex' }, - { name: 'Antigravity', value: 'antigravity' }, - ], - }, - ]); - - return target; -}; - -const showInteractiveMenu = async ( - target: 'claude' | 'codex' | 'antigravity' -): Promise<'global' | 'project' | 'all'> => { - const globalPath = target === 'claude' ? '~/.claude/' : target === 'codex' ? '~/.codex/' : '~/.gemini/'; - const projectPath = target === 'claude' ? './.claude/' : target === 'codex' ? './.codex/' : './.gemini/'; - - const { installType } = await inquirer.prompt([ - { - type: 'list', - name: 'installType', - message: 'Where would you like to install?', - choices: [ - { name: `Global (${globalPath}) - Available for all projects`, value: 'global' }, - { name: `Project (${projectPath}) - This project only`, value: 'project' }, - { name: 'Both - Global and current project', value: 'all' }, - ], - }, - ]); - - return installType; -}; - -const installArchitectureForEditor = (targetDir: string): FileResult[] => { - const results: FileResult[] = []; - const archDir = path.join(ASSETS_DIR, 'architecture'); - const targetArchDir = path.join(targetDir, 'architecture'); - - ensureDir(targetArchDir); - - if (fs.existsSync(archDir)) { - const files = getFiles(archDir); - for (const file of files) { - const target = path.join(targetArchDir, path.basename(file)); - const result = copyFile(file, target); - results.push(result); - } - } - - return results; -}; - -const installForOtherEditor = async ( - target: EditorTarget, - scope: Scope -): Promise => { - const config = getEditorConfig(target); - const results: FileResult[] = []; - - console.log(''); - console.log(chalk.cyan(`>>> ${config.name} Installation`)); - console.log(chalk.gray(` Target: ${getEditorDir(target, scope)}`)); - console.log(''); - - const targetDir = getEditorDir(target, scope); - ensureDir(targetDir); - - const archResults = installArchitectureForEditor(targetDir); - results.push(...archResults); - console.log(chalk.green(` ✓ Architecture installed to ${chalk.cyan(targetDir + '/architecture')}`)); - - if (config.rulesFile) { - const rulesFilePath = path.join(targetDir, config.rulesFile); - const result = mergeAgentsToFile(rulesFilePath, target); - results.push(result); - console.log(chalk.green(` ✓ Agents merged to ${chalk.cyan(config.rulesFile)}`)); - } - - printSummary(results); - - console.log(''); - console.log(chalk.green(`✓ ${config.name} installation complete!`)); - - return results; -}; - -export const installCommand = async (options: CommandOptions): Promise => { - printHeader(); - - if (!fs.existsSync(ASSETS_DIR)) { - console.log(chalk.red('Error: Assets directory not found.')); - console.log(chalk.gray(`Expected: ${ASSETS_DIR}`)); - process.exit(1); - } - - let targets: EditorTarget[] = []; - - let useSymlink: boolean; - if (options.symlink === true) { - useSymlink = true; - } else if (options.symlink === false) { - useSymlink = false; - } else { - useSymlink = isSymlinkSupported(); - } - - const strategyLabel = useSymlink ? 'symlinks' : 'file copy'; - const isAutoDetected = options.symlink === undefined; - if (isAutoDetected) { - console.log(chalk.gray(` Auto-detected file strategy: ${strategyLabel} (${process.platform})`)); - } else { - console.log(chalk.gray(` File strategy: ${strategyLabel} (user override)`)); - } - console.log(''); - - if (options.target) { - targets = [options.target]; - } else { - targets = [await showTargetMenu()]; - } - - for (const target of targets) { - addTarget(target); - - if (target === 'claude' || target === 'codex' || target === 'antigravity') { - let installType: 'global' | 'project' | 'all'; - - if (options.global) { - installType = 'global'; - } else if (options.project) { - installType = 'project'; - } else if (options.all) { - installType = 'all'; - } else { - installType = await showInteractiveMenu(target); - } - - switch (installType) { - case 'global': - if (target === 'claude') { - await installScope('global', useSymlink); - } else if (target === 'codex') { - await installCodexScope('global'); - } else { - await installAntigravityScope('global'); - } - break; - case 'project': - if (target === 'claude') { - await installScope('project', false); - } else if (target === 'codex') { - await installCodexScope('project'); - } else { - await installAntigravityScope('project'); - } - break; - case 'all': - if (target === 'claude') { - await installScope('global', useSymlink); - await installScope('project', false); - } else if (target === 'codex') { - await installCodexScope('global'); - await installCodexScope('project'); - } else { - await installAntigravityScope('global'); - await installAntigravityScope('project'); - } - break; - } - } else { - await installForOtherEditor(target, 'global'); - } - } - - console.log(''); - console.log(chalk.cyan('════════════════════════════════════════')); - console.log(chalk.cyan(' Usage')); - console.log(chalk.cyan('════════════════════════════════════════')); - console.log(''); - - if (targets.includes('claude')) { - console.log(chalk.bold(' Claude Code:')); - console.log(' Agents:'); - console.log(chalk.gray(' @clean-architect Clean Architecture expert')); - console.log(chalk.gray(' @code-reviewer Code review expert')); - console.log(chalk.gray(' @test-writer Test writing expert')); - console.log(''); - console.log(' Commands:'); - console.log(chalk.gray(' /bootstrap Create new project')); - console.log(chalk.gray(' /brainstorm Brainstorm ideas')); - console.log(chalk.gray(' /doc Generate documentation')); - console.log(''); - console.log(' Skills (auto-triggered):'); - console.log(chalk.gray(' new-feature Feature development')); - console.log(chalk.gray(' hotfix Bug fix with rollback')); - console.log(''); - } - - if (targets.includes('codex')) { - console.log(chalk.bold(' Codex CLI:')); - console.log(chalk.gray(' Skills installed under ~/.codex/skills or ./.codex/skills')); - console.log(chalk.gray(' Architecture docs installed under ~/.codex/architecture or ./.codex/architecture')); - console.log(chalk.gray(' Restart Codex after global skill installation to pick up new skills')); - console.log(''); - } - - if (targets.includes('antigravity')) { - console.log(chalk.bold(' Antigravity:')); - console.log(chalk.gray(' Skills installed under ~/.gemini/skills or ./.gemini/skills')); - console.log(chalk.gray(' Architecture docs installed under ~/.gemini/architecture or ./.gemini/architecture')); - console.log(''); - } - - const otherTargets = targets.filter((t) => t !== 'claude' && t !== 'codex' && t !== 'antigravity'); - if (otherTargets.length > 0) { - console.log(chalk.bold(' Other Editors:')); - for (const target of otherTargets) { - const config = EDITOR_CONFIGS[target]; - console.log(chalk.gray(` ${config.name}: Agents merged into ${config.rulesFile}`)); - } - console.log(''); - } -}; +export { installCommand } from './install/index.js'; diff --git a/src/commands/install/generic-editor.ts b/src/commands/install/generic-editor.ts new file mode 100644 index 0000000..c9a1d36 --- /dev/null +++ b/src/commands/install/generic-editor.ts @@ -0,0 +1,63 @@ +import chalk from 'chalk'; +import path from 'path'; +import fs from 'fs'; +import type { EditorTarget, FileResult, Scope } from '../../types.js'; +import { + ASSETS_DIR, + ensureDir, + copyFile, + getEditorConfig, + getEditorDir, + getFiles, + mergeAgentsToFile, +} from '../../utils/symlink.js'; +import { printSummary } from './print.js'; + +/** + * Rules-file editors (Cursor, Windsurf). These do not support discrete + * agents/commands/skills — agent personas are merged into a single rules file + * (AGENTS.md / global_rules.md), and architecture docs are copied alongside. + */ + +const installArchitectureForEditor = (targetDir: string): FileResult[] => { + const archDir = path.join(ASSETS_DIR, 'architecture'); + const targetArchDir = path.join(targetDir, 'architecture'); + ensureDir(targetArchDir); + + if (!fs.existsSync(archDir)) { + return []; + } + + return getFiles(archDir).map((file) => + copyFile(file, path.join(targetArchDir, path.basename(file))) + ); +}; + +export const installForOtherEditor = async (target: EditorTarget, scope: Scope): Promise => { + const config = getEditorConfig(target); + const targetDir = getEditorDir(target, scope); + const results: FileResult[] = []; + + console.log(''); + console.log(chalk.cyan(`>>> ${config.name} Installation`)); + console.log(chalk.gray(` Target: ${targetDir}`)); + console.log(''); + + ensureDir(targetDir); + + results.push(...installArchitectureForEditor(targetDir)); + console.log(chalk.green(` ✓ Architecture installed to ${chalk.cyan(path.join(targetDir, 'architecture'))}`)); + + if (config.rulesFile) { + const result = mergeAgentsToFile(path.join(targetDir, config.rulesFile), target); + results.push(result); + console.log(chalk.green(` ✓ Agents merged to ${chalk.cyan(config.rulesFile)}`)); + } + + printSummary(results); + + console.log(''); + console.log(chalk.green(`✓ ${config.name} installation complete!`)); + + return results; +}; diff --git a/src/commands/install/index.ts b/src/commands/install/index.ts new file mode 100644 index 0000000..e90e02a --- /dev/null +++ b/src/commands/install/index.ts @@ -0,0 +1,86 @@ +import chalk from 'chalk'; +import fs from 'fs'; +import type { CommandOptions, EditorTarget, Scope } from '../../types.js'; +import { ASSETS_DIR, isSymlinkSupported } from '../../utils/symlink.js'; +import { addTarget } from '../../utils/config.js'; +import { printHeader } from './print.js'; +import { installScope } from './native.js'; +import { installSkillEditorScope } from './skill-editor.js'; +import { installForOtherEditor } from './generic-editor.js'; +import { showTargetMenu, showInteractiveMenu } from './prompts.js'; +import { printUsage } from './usage.js'; + +/** Editors that branch into a global/project/all scope flow. */ +type ScopedTarget = 'claude' | 'codex' | 'antigravity'; +const isScopedTarget = (target: EditorTarget): target is ScopedTarget => + target === 'claude' || target === 'codex' || target === 'antigravity'; + +const resolveStrategy = (options: CommandOptions): boolean => { + if (options.symlink === true) return true; + if (options.symlink === false) return false; + return isSymlinkSupported(); +}; + +const resolveInstallType = async ( + options: CommandOptions, + target: ScopedTarget +): Promise<'global' | 'project' | 'all'> => { + if (options.global) return 'global'; + if (options.project) return 'project'; + if (options.all) return 'all'; + return showInteractiveMenu(target); +}; + +/** Install a scoped target (claude/codex/antigravity) into one scope. */ +const installScopedTarget = async ( + target: ScopedTarget, + scope: Scope, + useSymlink: boolean +): Promise => { + if (target === 'claude') { + // Symlinks only make sense for the global, shared install. + await installScope(scope, scope === 'global' ? useSymlink : false); + } else { + await installSkillEditorScope(scope, target); + } +}; + +export const installCommand = async (options: CommandOptions): Promise => { + printHeader(); + + if (!fs.existsSync(ASSETS_DIR)) { + console.log(chalk.red('Error: Assets directory not found.')); + console.log(chalk.gray(`Expected: ${ASSETS_DIR}`)); + process.exit(1); + } + + const useSymlink = resolveStrategy(options); + const strategyLabel = useSymlink ? 'symlinks' : 'file copy'; + if (options.symlink === undefined) { + console.log(chalk.gray(` Auto-detected file strategy: ${strategyLabel} (${process.platform})`)); + } else { + console.log(chalk.gray(` File strategy: ${strategyLabel} (user override)`)); + } + console.log(''); + + const targets: EditorTarget[] = options.target ? [options.target] : [await showTargetMenu()]; + + for (const target of targets) { + addTarget(target); + + if (!isScopedTarget(target)) { + await installForOtherEditor(target, 'global'); + continue; + } + + const installType = await resolveInstallType(options, target); + if (installType === 'global' || installType === 'all') { + await installScopedTarget(target, 'global', useSymlink); + } + if (installType === 'project' || installType === 'all') { + await installScopedTarget(target, 'project', useSymlink); + } + } + + printUsage(targets); +}; diff --git a/src/commands/install/native.ts b/src/commands/install/native.ts new file mode 100644 index 0000000..6f4dfc2 --- /dev/null +++ b/src/commands/install/native.ts @@ -0,0 +1,100 @@ +import chalk from 'chalk'; +import path from 'path'; +import fs from 'fs'; +import type { FileResult, Scope } from '../../types.js'; +import { + ASSETS_DIR, + ensureDir, + createSymlink, + copyFile, + copyDir, + getAgentsDir, + getCommandsDir, + getSkillsDir, + getArchitectureDir, + getClaudeDir, + getFiles, + getDirs, +} from '../../utils/symlink.js'; +import { printInstalled } from './print.js'; + +/** + * Claude native install: assets are symlinked (or copied) verbatim into + * ~/.claude/{agents,commands,skills,architecture}. This is the only target + * that supports symlinks and the full agents/commands/skills layout. + */ + +/** Symlink or copy every file from a source dir into the target dir. */ +const linkFiles = (sourceDir: string, targetDir: string, useSymlink: boolean): FileResult[] => { + if (!fs.existsSync(sourceDir)) { + return []; + } + return getFiles(sourceDir).map((file) => { + const target = path.join(targetDir, path.basename(file)); + return useSymlink ? createSymlink(file, target) : copyFile(file, target); + }); +}; + +const installAgents = (targetDir: string, useSymlink: boolean): FileResult[] => { + ensureDir(targetDir); + const results = [ + ...linkFiles(path.join(ASSETS_DIR, 'agents', 'developers'), targetDir, useSymlink), + ...linkFiles(path.join(ASSETS_DIR, 'agents', 'utilities'), targetDir, useSymlink), + ]; + printInstalled('Agents', targetDir, results); + return results; +}; + +const installCommands = (targetDir: string, useSymlink: boolean): FileResult[] => { + ensureDir(targetDir); + const results = linkFiles(path.join(ASSETS_DIR, 'commands'), targetDir, useSymlink); + printInstalled('Commands', targetDir, results); + return results; +}; + +const installSkills = (targetDir: string, useSymlink: boolean): FileResult[] => { + ensureDir(targetDir); + const skillsDir = path.join(ASSETS_DIR, 'skills'); + const results = fs.existsSync(skillsDir) + ? getDirs(skillsDir).map((dir) => { + const target = path.join(targetDir, path.basename(dir)); + return useSymlink ? createSymlink(dir, target) : copyDir(dir, target); + }) + : []; + printInstalled('Skills', targetDir, results); + return results; +}; + +const installArchitecture = (targetDir: string, useSymlink: boolean): FileResult[] => { + ensureDir(targetDir); + const results = linkFiles(path.join(ASSETS_DIR, 'architecture'), targetDir, useSymlink); + printInstalled('Architecture', targetDir, results); + return results; +}; + +export const installScope = async (scope: Scope, useSymlink: boolean): Promise => { + const isGlobal = scope === 'global'; + const label = isGlobal ? 'Global' : 'Project'; + const targetPath = isGlobal ? '~/.claude/' : `${process.cwd()}/.claude/`; + + console.log(''); + console.log(chalk.cyan(`>>> ${label} Installation`)); + console.log(chalk.gray(` Target: ${targetPath}`)); + console.log(''); + + ensureDir(getClaudeDir(scope)); + + installAgents(getAgentsDir(scope), useSymlink); + if (isGlobal) { + installCommands(getCommandsDir(scope), useSymlink); + } + installSkills(getSkillsDir(scope), useSymlink); + installArchitecture(getArchitectureDir(scope), useSymlink); + + if (!isGlobal) { + console.log(chalk.gray(' Note: Commands are installed globally only')); + } + + console.log(''); + console.log(chalk.green(`✓ ${label} installation complete!`)); +}; diff --git a/src/commands/install/print.ts b/src/commands/install/print.ts new file mode 100644 index 0000000..7edefb3 --- /dev/null +++ b/src/commands/install/print.ts @@ -0,0 +1,30 @@ +import chalk from 'chalk'; +import type { FileResult } from '../../types.js'; + +export const printHeader = (): void => { + console.log(''); + console.log(chalk.cyan('════════════════════════════════════════')); + console.log(chalk.cyan(' MoiCle Installer')); + console.log(chalk.cyan('════════════════════════════════════════')); + console.log(''); +}; + +export const printSummary = (results: FileResult[]): void => { + const created = results.filter((r) => r.status === 'created').length; + const updated = results.filter((r) => r.status === 'updated').length; + const exists = results.filter((r) => r.status === 'exists').length; + const skipped = results.filter((r) => r.status === 'skipped').length; + const errors = results.filter((r) => r.status === 'error').length; + + if (created > 0) console.log(chalk.green(` Created: ${created}`)); + if (updated > 0) console.log(chalk.yellow(` Updated: ${updated}`)); + if (exists > 0) console.log(chalk.gray(` Already exists: ${exists}`)); + if (skipped > 0) console.log(chalk.gray(` Skipped: ${skipped}`)); + if (errors > 0) console.log(chalk.red(` Errors: ${errors}`)); +}; + +/** Print "✓ installed to " followed by the status summary. */ +export const printInstalled = (what: string, targetDir: string, results: FileResult[]): void => { + console.log(chalk.green(` ✓ ${what} installed to ${chalk.cyan(targetDir)}`)); + printSummary(results); +}; diff --git a/src/commands/install/prompts.ts b/src/commands/install/prompts.ts new file mode 100644 index 0000000..3673ee4 --- /dev/null +++ b/src/commands/install/prompts.ts @@ -0,0 +1,48 @@ +import inquirer from 'inquirer'; +import type { EditorTarget } from '../../types.js'; + +export const showTargetMenu = async (): Promise => { + const { target } = await inquirer.prompt([ + { + type: 'list', + name: 'target', + message: 'Which editor would you like to configure?', + choices: [ + { name: 'Claude Code', value: 'claude' }, + { name: 'Codex CLI', value: 'codex' }, + { name: 'Antigravity', value: 'antigravity' }, + { name: 'Cursor', value: 'cursor' }, + { name: 'Windsurf', value: 'windsurf' }, + ], + }, + ]); + + return target; +}; + +const SCOPE_PATHS: Record<'claude' | 'codex' | 'antigravity', { global: string; project: string }> = { + claude: { global: '~/.claude/', project: './.claude/' }, + codex: { global: '~/.codex/', project: './.codex/' }, + antigravity: { global: '~/.gemini/', project: './.gemini/' }, +}; + +export const showInteractiveMenu = async ( + target: 'claude' | 'codex' | 'antigravity' +): Promise<'global' | 'project' | 'all'> => { + const { global: globalPath, project: projectPath } = SCOPE_PATHS[target]; + + const { installType } = await inquirer.prompt([ + { + type: 'list', + name: 'installType', + message: 'Where would you like to install?', + choices: [ + { name: `Global (${globalPath}) - Available for all projects`, value: 'global' }, + { name: `Project (${projectPath}) - This project only`, value: 'project' }, + { name: 'Both - Global and current project', value: 'all' }, + ], + }, + ]); + + return installType; +}; diff --git a/src/commands/install/skill-editor.ts b/src/commands/install/skill-editor.ts new file mode 100644 index 0000000..9c04a7b --- /dev/null +++ b/src/commands/install/skill-editor.ts @@ -0,0 +1,175 @@ +import chalk from 'chalk'; +import path from 'path'; +import fs from 'fs'; +import type { FileResult, Scope } from '../../types.js'; +import { ASSETS_DIR, ensureDir, getEditorConfig, getEditorDir, getFiles, getDirs } from '../../utils/symlink.js'; +import { printSummary } from './print.js'; +import { + type SkillEditorTarget, + rewriteClaudePaths, + extractFrontmatter, + buildGeneratedSkill, +} from './transform.js'; + +/** + * Skill-based editors (Codex, Antigravity). Both consume a flat `skills/` folder + * of rewritten SKILL.md files plus an `architecture/` folder. The two editors are + * identical apart from the rewrite target, so everything here is parameterized by + * `target` — there is no per-editor branching. + */ + +/** Write content only if it differs; report created/updated/exists. */ +const writeIfChanged = (targetFile: string, content: string, name: string): FileResult => { + if (fs.existsSync(targetFile)) { + if (fs.readFileSync(targetFile, 'utf-8') === content) { + return { status: 'exists', name }; + } + fs.writeFileSync(targetFile, content); + return { status: 'updated', name }; + } + fs.writeFileSync(targetFile, content); + return { status: 'created', name }; +}; + +const ensureSkillDir = (baseDir: string, name: string): string => { + const skillDir = path.join(baseDir, name); + ensureDir(skillDir); + return skillDir; +}; + +/** Copy a Claude skill folder (SKILL.md + assets) with paths rewritten. */ +const installSkillFolder = ( + sourceDir: string, + targetSkillsDir: string, + target: SkillEditorTarget +): FileResult => { + const skillName = path.basename(sourceDir); + const targetDir = ensureSkillDir(targetSkillsDir, skillName); + + let status: FileResult['status'] = 'created'; + for (const file of getFiles(sourceDir, 8)) { + const targetFile = path.join(targetDir, path.relative(sourceDir, file)); + ensureDir(path.dirname(targetFile)); + + const content = rewriteClaudePaths(fs.readFileSync(file, 'utf-8'), target); + const existed = fs.existsSync(targetFile); + if (existed && fs.readFileSync(targetFile, 'utf-8') === content) { + status = status === 'created' ? 'exists' : status; + continue; + } + + fs.writeFileSync(targetFile, content); + if (existed) { + status = 'updated'; + } + } + + return { status, name: skillName }; +}; + +/** Wrap an agent/command body as a generated SKILL.md. */ +const installGeneratedSkill = ( + targetSkillsDir: string, + name: string, + description: string, + body: string, + target: SkillEditorTarget +): FileResult => { + const skillDir = ensureSkillDir(targetSkillsDir, name); + const content = buildGeneratedSkill(name, description, rewriteClaudePaths(body, target)); + return writeIfChanged(path.join(skillDir, 'SKILL.md'), content, name); +}; + +/** Copy a command file straight into its own SKILL.md (frontmatter already present). */ +const installDirectSkill = ( + targetSkillsDir: string, + name: string, + content: string, + target: SkillEditorTarget +): FileResult => { + const skillDir = ensureSkillDir(targetSkillsDir, name); + return writeIfChanged(path.join(skillDir, 'SKILL.md'), rewriteClaudePaths(content, target), name); +}; + +const installEditorArchitecture = (targetDir: string, target: SkillEditorTarget): FileResult[] => { + const archDir = path.join(ASSETS_DIR, 'architecture'); + const targetArchDir = path.join(targetDir, 'architecture'); + ensureDir(targetArchDir); + + if (!fs.existsSync(archDir)) { + return []; + } + + return getFiles(archDir).map((file) => { + const content = rewriteClaudePaths(fs.readFileSync(file, 'utf-8'), target); + return writeIfChanged(path.join(targetArchDir, path.basename(file)), content, path.basename(file)); + }); +}; + +const installEditorSkills = (targetDir: string, target: SkillEditorTarget): FileResult[] => { + const results: FileResult[] = []; + const targetSkillsDir = path.join(targetDir, 'skills'); + ensureDir(targetSkillsDir); + + // 1. Skill folders → copied verbatim (paths rewritten). + const skillsDir = path.join(ASSETS_DIR, 'skills'); + if (fs.existsSync(skillsDir)) { + for (const dir of getDirs(skillsDir)) { + results.push(installSkillFolder(dir, targetSkillsDir, target)); + } + } + + // 2. Commands → each becomes a SKILL.md (frontmatter already present). + const commandsDir = path.join(ASSETS_DIR, 'commands'); + if (fs.existsSync(commandsDir)) { + for (const file of getFiles(commandsDir)) { + const name = path.basename(file, '.md'); + results.push(installDirectSkill(targetSkillsDir, name, fs.readFileSync(file, 'utf-8'), target)); + } + } + + // 3. Agent personas → wrapped as generated SKILL.md. + for (const dirName of ['developers', 'utilities'] as const) { + const sourceDir = path.join(ASSETS_DIR, 'agents', dirName); + if (!fs.existsSync(sourceDir)) { + continue; + } + for (const file of getFiles(sourceDir)) { + const name = path.basename(file, '.md'); + const parsed = extractFrontmatter(fs.readFileSync(file, 'utf-8')); + const description = + parsed.description ?? + (dirName === 'developers' + ? `Imported MoiCle developer persona for ${name}. Use when the task matches this stack specialist.` + : `Imported MoiCle utility persona for ${name}. Use when the task matches this specialist.`); + results.push(installGeneratedSkill(targetSkillsDir, name, description, parsed.body.trimStart(), target)); + } + } + + return results; +}; + +export const installSkillEditorScope = async (scope: Scope, target: SkillEditorTarget): Promise => { + const isGlobal = scope === 'global'; + const label = isGlobal ? 'Global' : 'Project'; + const name = getEditorConfig(target).name; + const baseDir = getEditorDir(target, scope); + + console.log(''); + console.log(chalk.cyan(`>>> ${label} ${name} Installation`)); + console.log(chalk.gray(` Target: ${baseDir}`)); + console.log(''); + + ensureDir(baseDir); + + const archResults = installEditorArchitecture(baseDir, target); + console.log(chalk.green(` ✓ Architecture installed to ${chalk.cyan(path.join(baseDir, 'architecture'))}`)); + printSummary(archResults); + + const skillResults = installEditorSkills(baseDir, target); + console.log(chalk.green(` ✓ ${name} skills installed to ${chalk.cyan(path.join(baseDir, 'skills'))}`)); + printSummary(skillResults); + + console.log(''); + console.log(chalk.green(`✓ ${label} ${name} installation complete!`)); +}; diff --git a/src/commands/install/transform.ts b/src/commands/install/transform.ts new file mode 100644 index 0000000..6522315 --- /dev/null +++ b/src/commands/install/transform.ts @@ -0,0 +1,60 @@ +/** + * Content transforms shared by the skill-based editors (Codex, Antigravity). + * Claude assets are authored as-is; other editors need paths/brand names rewritten + * and agent personas wrapped as SKILL.md files. + */ + +/** Skill-based editors that consume rewritten SKILL.md folders. */ +export type SkillEditorTarget = 'codex' | 'antigravity'; + +const REWRITE_RULES: Record> = { + codex: [ + [/~\/\.claude\//g, '~/.codex/'], + [/\.claude\//g, '.codex/'], + [/Claude Code/g, 'Codex CLI'], + [/CLAUDE\.md/g, 'AGENTS.md'], + ], + antigravity: [ + [/~\/\.claude\//g, '~/.gemini/'], + [/\.claude\//g, '.gemini/'], + [/Claude Code/g, 'Antigravity'], + [/CLAUDE\.md/g, 'GEMINI.md'], + ], +}; + +export const rewriteClaudePaths = ( + content: string, + target: 'claude' | SkillEditorTarget +): string => { + if (target === 'claude') { + return content; + } + return REWRITE_RULES[target].reduce((acc, [pattern, replacement]) => acc.replace(pattern, replacement), content); +}; + +export const extractFrontmatter = ( + content: string +): { frontmatter: string | null; body: string; description?: string } => { + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/); + if (!match) { + return { frontmatter: null, body: content }; + } + + const frontmatter = match[1]; + const body = match[2]; + const descriptionMatch = frontmatter.match(/^description:\s*(.+)$/m); + + return { + frontmatter, + body, + description: descriptionMatch?.[1]?.trim(), + }; +}; + +export const buildGeneratedSkill = (name: string, description: string, body: string): string => `--- +name: ${name} +description: ${description} +--- + +${body} +`; diff --git a/src/commands/install/usage.ts b/src/commands/install/usage.ts new file mode 100644 index 0000000..d5d25a0 --- /dev/null +++ b/src/commands/install/usage.ts @@ -0,0 +1,78 @@ +import chalk from 'chalk'; +import type { EditorTarget } from '../../types.js'; +import { EDITOR_CONFIGS } from '../../utils/symlink.js'; + +/** + * Final "Usage" banner. Counts/names are kept in sync with assets/ here so the + * post-install summary never drifts from what was actually installed. + */ + +const printClaudeUsage = (): void => { + console.log(chalk.bold(' Claude Code:')); + console.log(' Agents (16):'); + console.log(chalk.gray(' Developers @flutter-mobile-dev, @go-backend-dev, @laravel-backend-dev,')); + console.log(chalk.gray(' @nodejs-backend-dev, @react-frontend-dev, @remix-fullstack-dev')); + console.log(chalk.gray(' Utilities @api-designer, @clean-architect, @code-reviewer, @db-designer,')); + console.log(chalk.gray(' @devops, @docs-writer, @perf-optimizer, @refactor,')); + console.log(chalk.gray(' @security-audit, @test-writer')); + console.log(''); + console.log(' Commands (4):'); + console.log(chalk.gray(' /bootstrap Create new project')); + console.log(chalk.gray(' /brainstorm Brainstorm ideas')); + console.log(chalk.gray(' /doc Generate documentation')); + console.log(chalk.gray(' /marketing Go-to-market plan')); + console.log(''); + console.log(' Skills (21, auto-triggered, namespaced /group:action):'); + console.log(chalk.gray(' /feature:* new, refactor, api, deprecate')); + console.log(chalk.gray(' /fix:* hotfix, root-cause, incident, pr-comment')); + console.log(chalk.gray(' /review:* branch, pr, architect, tdd')); + console.log(chalk.gray(' /research:* web, spike, onboarding')); + console.log(chalk.gray(' /docs:* write, sync')); + console.log(chalk.gray(' /marketing:* content, seo-blog, logo, video')); + console.log(''); + console.log(chalk.gray(' Run "moicle list" to see everything installed.')); + console.log(''); +}; + +/** Codex & Antigravity: everything ships as SKILL.md (agents + commands + skills). */ +const printSkillEditorUsage = (target: 'codex' | 'antigravity'): void => { + const name = EDITOR_CONFIGS[target].name; + const home = target === 'codex' ? '~/.codex' : '~/.gemini'; + const local = target === 'codex' ? './.codex' : './.gemini'; + console.log(chalk.bold(` ${name}:`)); + console.log(chalk.gray(` MoiCle's 16 agents, 4 commands & 21 skills installed as SKILL.md files`)); + console.log(chalk.gray(` Skills under ${home}/skills or ${local}/skills`)); + console.log(chalk.gray(` Architecture docs under ${home}/architecture or ${local}/architecture`)); + if (target === 'codex') { + console.log(chalk.gray(' Restart Codex after global skill installation to pick up new skills')); + } + console.log(''); +}; + +export const printUsage = (targets: EditorTarget[]): void => { + console.log(''); + console.log(chalk.cyan('════════════════════════════════════════')); + console.log(chalk.cyan(' Usage')); + console.log(chalk.cyan('════════════════════════════════════════')); + console.log(''); + + if (targets.includes('claude')) { + printClaudeUsage(); + } + if (targets.includes('codex')) { + printSkillEditorUsage('codex'); + } + if (targets.includes('antigravity')) { + printSkillEditorUsage('antigravity'); + } + + const rulesFileTargets = targets.filter((t) => t === 'cursor' || t === 'windsurf'); + if (rulesFileTargets.length > 0) { + console.log(chalk.bold(' Rules-file Editors:')); + for (const target of rulesFileTargets) { + const config = EDITOR_CONFIGS[target]; + console.log(chalk.gray(` ${config.name}: 16 agents merged into ${config.rulesFile} + architecture docs`)); + } + console.log(''); + } +}; diff --git a/src/commands/postinstall.ts b/src/commands/postinstall.ts index 940d968..09c3dcc 100644 --- a/src/commands/postinstall.ts +++ b/src/commands/postinstall.ts @@ -14,7 +14,10 @@ export const postinstallCommand = async (): Promise => { console.log(chalk.gray(' moicle install --global # Install to ~/.claude/')); console.log(chalk.gray(' moicle install --project # Install to ./.claude/')); console.log(chalk.gray(' moicle install --all # Install to both')); - console.log(chalk.gray(' moicle install --target codex --global # Install Codex skills to ~/.codex/')); + console.log(chalk.gray(' moicle install --target codex --global # Codex skills → ~/.codex/')); + console.log(chalk.gray(' moicle install --target antigravity --global # Antigravity skills → ~/.gemini/')); + console.log(chalk.gray(' moicle install --target cursor # Cursor rules → ~/.cursor/')); + console.log(chalk.gray(' moicle install --target windsurf # Windsurf rules')); console.log(''); console.log('Other commands:'); console.log(chalk.gray(' moicle list # List installed items'));