mirror of
https://github.com/jparkerweb/plan2code.git
synced 2026-07-21 10:23:23 -07:00
2647 lines
101 KiB
JavaScript
2647 lines
101 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
/**
|
||
* ██████╗ ██╗ █████╗ ███╗ ██╗██████╗ ██████╗ ██████╗ ██████╗ ███████╗
|
||
* ██╔══██╗██║ ██╔══██╗████╗ ██║╚════██╗██╔════╝██╔═══██╗██╔══██╗██╔════╝
|
||
* ██████╔╝██║ ███████║██╔██╗ ██║ █████╔╝██║ ██║ ██║██║ ██║█████╗
|
||
* ██╔═══╝ ██║ ██╔══██║██║╚██╗██║██╔═══╝ ██║ ██║ ██║██║ ██║██╔══╝
|
||
* ██║ ███████╗██║ ██║██║ ╚████║███████╗╚██████╗╚██████╔╝██████╔╝███████╗
|
||
* ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝
|
||
*
|
||
* GLOBAL INSTALLATION SYSTEM
|
||
*
|
||
* DESCRIPTION:
|
||
* Install Plan2Code prompts to user's home directory for global use.
|
||
* Generates distribution files on-the-fly from source prompts in src/.
|
||
* Always runs interactively — any CLI arguments are silently ignored.
|
||
*/
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const os = require('os');
|
||
const readline = require('readline');
|
||
|
||
// Load project version metadata
|
||
let projectVersion = { name: 'Plan2Code', version: 'unknown' };
|
||
try {
|
||
const versionPath = path.join(__dirname, 'version.json');
|
||
if (fs.existsSync(versionPath)) {
|
||
const versionData = JSON.parse(fs.readFileSync(versionPath, 'utf8'));
|
||
projectVersion = { name: versionData.name, version: versionData.version };
|
||
}
|
||
} catch (err) {
|
||
// Silently continue with default version if file is not found or invalid
|
||
}
|
||
|
||
// Color codes for display (ANSI escape sequences)
|
||
// Use these in console.log() for colored output
|
||
const COLORS = {
|
||
RESET: '\x1b[0m',
|
||
BRIGHT: '\x1b[1m',
|
||
DIM: '\x1b[2m',
|
||
|
||
// Colors
|
||
GREEN: '\x1b[32m', // Success
|
||
YELLOW: '\x1b[33m', // Warning
|
||
RED: '\x1b[31m', // Error
|
||
CYAN: '\x1b[36m', // Info/Headers
|
||
BLUE: '\x1b[34m', // Data/Paths
|
||
MAGENTA: '\x1b[35m', // Special/Highlight
|
||
|
||
// Background colors
|
||
BG_BLACK: '\x1b[40m',
|
||
BG_GREEN: '\x1b[42m',
|
||
BG_YELLOW: '\x1b[43m',
|
||
BG_RED: '\x1b[41m',
|
||
};
|
||
|
||
// Symbols for status reporting
|
||
const SYMBOLS = {
|
||
SUCCESS: '▰▰▰', // Success
|
||
ACTIVE: '►►►', // Active/In Progress
|
||
WARNING: '⚠ ⚠ ⚠', // Warning
|
||
ERROR: '✖✖✖', // Error
|
||
INFO: '◆', // Info
|
||
SELECT: '◉', // Selection marker
|
||
COMPLETE: '█', // Complete
|
||
PENDING: '░', // Pending
|
||
DIVIDER: '═', // Divider
|
||
CORNER_TL: '╔', // Box corners
|
||
CORNER_TR: '╗',
|
||
CORNER_BL: '╚',
|
||
CORNER_BR: '╝',
|
||
HORIZONTAL: '═',
|
||
VERTICAL: '║',
|
||
TEE_RIGHT: '╠',
|
||
TEE_LEFT: '╣',
|
||
};
|
||
|
||
// Plan2Code Mascot - appears during user interactions
|
||
const MASCOT = {
|
||
// Full mascot for headers
|
||
full: [
|
||
' ╭───╮ ',
|
||
' │ ● │ ',
|
||
' │ ◡ │ ',
|
||
' ╰───╯ ',
|
||
],
|
||
// Mini mascot for inline use
|
||
mini: '(◉‿◉)',
|
||
// Waving mascot for greetings
|
||
wave: [
|
||
' ╭───╮ ',
|
||
' │ ● │ /',
|
||
' │ ◡ │ ',
|
||
' ╰───╯ ',
|
||
],
|
||
// Thinking mascot for prompts
|
||
thinking: [
|
||
' ╭───╮ ',
|
||
' │ ● │ ?',
|
||
' │ ~ │ ',
|
||
' ╰───╯ ',
|
||
],
|
||
};
|
||
|
||
// Source directory containing pre-formatted global installation files
|
||
const SOURCE_BASE = 'dist/global-commands';
|
||
|
||
// Source prompts directory
|
||
const SRC_DIR = path.join(__dirname, 'src');
|
||
|
||
// ============================================================================
|
||
// SYNC PROMPTS CONFIGURATION (merged from sync-prompts.js)
|
||
// ============================================================================
|
||
|
||
// Configuration for source prompts
|
||
const SOURCE_PROMPTS = [
|
||
{
|
||
source: 'plan2code-init.md',
|
||
stepNumber: 'init',
|
||
name: 'init',
|
||
displayName: 'Init Mode',
|
||
description: 'Generate AGENTS.md file for project-specific guidance',
|
||
isUtility: true
|
||
},
|
||
{
|
||
source: 'plan2code-init-update.md',
|
||
stepNumber: 'update',
|
||
name: 'init-update',
|
||
displayName: 'Init Update Mode',
|
||
description: 'Update existing AGENTS.md with new learnings',
|
||
isUtility: true
|
||
},
|
||
{
|
||
source: 'plan2code-quick-task.md',
|
||
stepNumber: 0,
|
||
name: 'quick-task',
|
||
displayName: 'Quick Task Mode',
|
||
description: 'Lightweight planning for small tasks'
|
||
},
|
||
{
|
||
source: 'plan2code-1-plan.md',
|
||
stepNumber: 1,
|
||
name: 'plan',
|
||
displayName: 'Planning Mode',
|
||
description: 'Requirements analysis and architecture design'
|
||
},
|
||
{
|
||
source: 'plan2code-1b-revise-plan.md',
|
||
stepNumber: '1b',
|
||
name: 'revise-plan',
|
||
displayName: 'Revision Mode',
|
||
description: 'Modify specs mid-implementation when requirements change'
|
||
},
|
||
{
|
||
source: 'plan2code-2-document.md',
|
||
stepNumber: 2,
|
||
name: 'document',
|
||
displayName: 'Documentation Mode',
|
||
description: 'Transform planning output into structured implementation docs'
|
||
},
|
||
{
|
||
source: 'plan2code-3-implement.md',
|
||
stepNumber: 3,
|
||
name: 'implement',
|
||
displayName: 'Implementation Mode',
|
||
description: 'Execute implementation phase by phase'
|
||
},
|
||
{
|
||
source: 'plan2code-review.md',
|
||
stepNumber: 'review',
|
||
name: 'review',
|
||
displayName: 'Review Mode',
|
||
description: 'Comprehensive post-implementation review with spec compliance checking',
|
||
isUtility: true
|
||
},
|
||
{
|
||
source: 'plan2code-4-finalize.md',
|
||
stepNumber: 4,
|
||
name: 'finalize',
|
||
displayName: 'Finalization Mode',
|
||
description: 'Validate, summarize, and archive completed work'
|
||
},
|
||
{
|
||
source: 'plan2code-handoff.md',
|
||
stepNumber: 'handoff',
|
||
name: 'handoff',
|
||
displayName: 'Handoff Mode',
|
||
description: 'Compact the conversation into a self-contained handoff document for a fresh session',
|
||
isUtility: true
|
||
}
|
||
];
|
||
|
||
// Helper function to generate destination filename based on stepNumber
|
||
function generateFilename(prompt, extension = '.md') {
|
||
if (prompt.isUtility || prompt.stepNumber === 0 || prompt.stepNumber === 'init') {
|
||
return `plan2code-${prompt.name}${extension}`;
|
||
}
|
||
return `plan2code-${prompt.stepNumber}-${prompt.name}${extension}`;
|
||
}
|
||
|
||
// Helper function to generate skill name (normalizes double hyphens to single)
|
||
function generateSkillName(prompt) {
|
||
return generateFilename(prompt, '').replace(/--+/g, '-');
|
||
}
|
||
|
||
// Helper function to generate step label for descriptions
|
||
function generateStepLabel(prompt) {
|
||
if (prompt.stepNumber === 'init') return 'Init';
|
||
if (prompt.stepNumber === 'update') return 'Update';
|
||
if (prompt.stepNumber === 'review') return 'Review';
|
||
if (prompt.stepNumber === 'handoff') return 'Handoff';
|
||
return `Step ${prompt.stepNumber}`;
|
||
}
|
||
|
||
// Helper function to generate YAML frontmatter for SKILL.md files
|
||
function generateSkillHeader(prompt, disableModelInvocation = false) {
|
||
const lines = [
|
||
'---',
|
||
`name: ${generateSkillName(prompt)}`,
|
||
`description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - user-initiated workflow step. Do not invoke autonomously."`,
|
||
];
|
||
if (disableModelInvocation) lines.push('disable-model-invocation: true');
|
||
lines.push('---');
|
||
return lines.join('\n');
|
||
}
|
||
|
||
// Helper function to generate complete .toml file content for Gemini CLI
|
||
function generateTomlContent(prompt, sourceContent) {
|
||
const stepLabel = generateStepLabel(prompt);
|
||
const desc = `Plan2Code ${stepLabel}: ${prompt.displayName} - user-initiated ${prompt.description || 'workflow step'}`;
|
||
return `description = "${desc}"\nprompt = '''\n${sourceContent}\n'''\n`;
|
||
}
|
||
|
||
// Destination configurations for project-level installation (local)
|
||
const LOCAL_DESTINATIONS = [
|
||
{
|
||
name: 'Claude Code Commands',
|
||
dir: '.claude/commands',
|
||
filePattern: (prompt) => generateFilename(prompt, '.md'),
|
||
header: null
|
||
},
|
||
{
|
||
name: 'Cursor Commands',
|
||
dir: '.cursor/commands',
|
||
filePattern: (prompt) => generateFilename(prompt, '.md'),
|
||
header: null
|
||
},
|
||
{
|
||
name: 'GitHub Copilot Prompts',
|
||
dir: '.github/prompts',
|
||
filePattern: (prompt) => generateFilename(prompt, '.prompt.md'),
|
||
header: (prompt) => [
|
||
'---',
|
||
'mode: agent',
|
||
`description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`,
|
||
'---'
|
||
].join('\n')
|
||
},
|
||
{
|
||
name: 'GitHub Copilot Agents',
|
||
dir: '.github/agents',
|
||
filePattern: (prompt) => generateFilename(prompt, '.agent.md'),
|
||
header: (prompt) => [
|
||
'---',
|
||
`description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`,
|
||
'---'
|
||
].join('\n')
|
||
},
|
||
{
|
||
name: 'Continue Prompts',
|
||
dir: '.continue/prompts',
|
||
filePattern: (prompt) => generateFilename(prompt, '.prompt.md'),
|
||
header: (prompt) => [
|
||
'---',
|
||
`name: ${prompt.name}`,
|
||
`description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`,
|
||
'---'
|
||
].join('\n')
|
||
},
|
||
{
|
||
name: 'Windsurf Workflows',
|
||
dir: '.windsurf/workflows',
|
||
filePattern: (prompt) => generateFilename(prompt, '.md'),
|
||
header: (prompt) => [
|
||
'---',
|
||
`description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`,
|
||
'---'
|
||
].join('\n')
|
||
},
|
||
{
|
||
name: 'Windsurf Global Workflows',
|
||
dir: '.codeium/windsurf/global_workflows',
|
||
filePattern: (prompt) => generateFilename(prompt, '.md'),
|
||
header: (prompt) => [
|
||
'---',
|
||
`description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`,
|
||
'---'
|
||
].join('\n')
|
||
},
|
||
{
|
||
name: 'Agent Workflows',
|
||
dir: '.agent/workflows',
|
||
filePattern: (prompt) => generateFilename(prompt, '.md'),
|
||
header: (prompt) => [
|
||
'---',
|
||
`description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`,
|
||
'---'
|
||
].join('\n')
|
||
},
|
||
{
|
||
name: 'Codeium Global Workflows',
|
||
dir: '.codeium/global_workflows',
|
||
filePattern: (prompt) => generateFilename(prompt, '.md'),
|
||
header: (prompt) => [
|
||
'---',
|
||
`description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`,
|
||
'---'
|
||
].join('\n')
|
||
},
|
||
{
|
||
name: 'Pi Prompts',
|
||
dir: '.pi/prompts',
|
||
filePattern: (prompt) => generateFilename(prompt, '.md'),
|
||
header: (prompt) => [
|
||
'---',
|
||
`description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`,
|
||
'---'
|
||
].join('\n')
|
||
},
|
||
{
|
||
name: 'Claude Code Skills',
|
||
dir: '.claude/skills',
|
||
type: 'skill',
|
||
header: (prompt) => generateSkillHeader(prompt, true),
|
||
},
|
||
{
|
||
name: 'Agent Skills (Amp · Devin · Gemini CLI · OpenCode)',
|
||
dir: '.agents/skills',
|
||
type: 'skill',
|
||
header: (prompt) => generateSkillHeader(prompt, false),
|
||
},
|
||
{
|
||
name: 'Gemini CLI',
|
||
dir: '.gemini/commands',
|
||
type: 'toml',
|
||
filePattern: (prompt) => `${generateFilename(prompt, '')}.toml`,
|
||
}
|
||
];
|
||
|
||
// Destination configurations for global (home directory) installation
|
||
const GLOBAL_DESTINATIONS = [
|
||
{
|
||
name: 'Claude Code (Global)',
|
||
dir: '.claude/commands',
|
||
filePattern: (prompt) => generateFilename(prompt, '.md'),
|
||
header: null
|
||
},
|
||
{
|
||
name: 'Copilot CLI (Global)',
|
||
dir: '.copilot/agents',
|
||
filePattern: (prompt) => generateFilename(prompt, '.md'),
|
||
header: (prompt) => [
|
||
'---',
|
||
`description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`,
|
||
'---'
|
||
].join('\n')
|
||
},
|
||
{
|
||
name: 'Cursor (Global)',
|
||
dir: '.cursor/commands',
|
||
filePattern: (prompt) => generateFilename(prompt, '.md'),
|
||
header: null
|
||
},
|
||
{
|
||
name: 'Continue (Global)',
|
||
dir: '.continue/prompts',
|
||
filePattern: (prompt) => generateFilename(prompt, '.prompt.md'),
|
||
header: (prompt) => [
|
||
'---',
|
||
`name: ${prompt.name}`,
|
||
`description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`,
|
||
'---'
|
||
].join('\n')
|
||
},
|
||
{
|
||
name: 'Windsurf (Global)',
|
||
dir: '.codeium/windsurf/global_workflows',
|
||
filePattern: (prompt) => generateFilename(prompt, '.md'),
|
||
header: (prompt) => [
|
||
'---',
|
||
`description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`,
|
||
'---'
|
||
].join('\n')
|
||
},
|
||
{
|
||
name: 'VS Code Copilot (Global)',
|
||
dir: 'vscode-copilot-prompts',
|
||
filePattern: (prompt) => generateFilename(prompt, '.prompt.md'),
|
||
header: (prompt) => [
|
||
'---',
|
||
'agent: agent',
|
||
`description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`,
|
||
'---'
|
||
].join('\n')
|
||
},
|
||
{
|
||
name: 'Codeium (Global)',
|
||
dir: '.codeium/global_workflows',
|
||
filePattern: (prompt) => generateFilename(prompt, '.md'),
|
||
header: (prompt) => [
|
||
'---',
|
||
`description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`,
|
||
'---'
|
||
].join('\n')
|
||
},
|
||
{
|
||
name: 'Pi (Global)',
|
||
dir: '.pi/agent/prompts',
|
||
filePattern: (prompt) => generateFilename(prompt, '.md'),
|
||
header: (prompt) => [
|
||
'---',
|
||
`description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`,
|
||
'---'
|
||
].join('\n')
|
||
},
|
||
{
|
||
name: 'Claude Code Skills',
|
||
dir: '.claude/skills',
|
||
type: 'skill',
|
||
header: (prompt) => generateSkillHeader(prompt, true),
|
||
},
|
||
{
|
||
name: 'Agent Skills (Amp · Devin · Gemini CLI · OpenCode · Zed)',
|
||
dir: '.agents/skills',
|
||
type: 'skill',
|
||
header: (prompt) => generateSkillHeader(prompt, true),
|
||
},
|
||
{
|
||
name: 'Crush',
|
||
dir: 'crush-skills',
|
||
type: 'skill',
|
||
header: (prompt) => generateSkillHeader(prompt, false),
|
||
},
|
||
{
|
||
name: 'Gemini CLI',
|
||
dir: '.gemini/commands',
|
||
type: 'toml',
|
||
filePattern: (prompt) => `${generateFilename(prompt, '')}.toml`,
|
||
}
|
||
];
|
||
|
||
// Helper function to get VS Code Copilot prompts directory based on platform
|
||
function getVSCodeCopilotDir() {
|
||
const platform = process.platform;
|
||
if (platform === 'win32') {
|
||
// Windows: %APPDATA%\Code\User\prompts
|
||
return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'Code', 'User', 'prompts');
|
||
} else if (platform === 'darwin') {
|
||
// macOS: ~/Library/Application Support/Code/User/prompts
|
||
return path.join(os.homedir(), 'Library', 'Application Support', 'Code', 'User', 'prompts');
|
||
} else {
|
||
// Linux: ~/.config/Code/User/prompts
|
||
return path.join(os.homedir(), '.config', 'Code', 'User', 'prompts');
|
||
}
|
||
}
|
||
|
||
// Helper function to get Crush skills directory based on platform
|
||
function getCrushSkillsDir() {
|
||
const homedir = os.homedir();
|
||
if (process.platform === 'win32') {
|
||
const localAppData = process.env.LOCALAPPDATA || path.join(homedir, 'AppData', 'Local');
|
||
return path.join(localAppData, 'crush', 'skills');
|
||
}
|
||
return path.join(homedir, '.config', 'crush', 'skills');
|
||
}
|
||
|
||
// Helper function to get Agent Skills global directory based on platform
|
||
function getAgentSkillsDir() {
|
||
const homedir = os.homedir();
|
||
return path.join(homedir, '.agents', 'skills');
|
||
}
|
||
|
||
// Helper function to resolve target directory (handles both static and dynamic paths)
|
||
function resolveTargetDir(target) {
|
||
if (typeof target.dir === 'function') {
|
||
return target.dir();
|
||
}
|
||
return path.join(os.homedir(), target.dir);
|
||
}
|
||
|
||
// Helper function to get display path for a target
|
||
function getDisplayPath(target) {
|
||
if (target.displayPath) {
|
||
return target.displayPath;
|
||
}
|
||
return `~/${target.dir}/`;
|
||
}
|
||
|
||
// Helper function to pad strings with ANSI codes correctly
|
||
function stripAnsi(str) {
|
||
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
||
}
|
||
|
||
function padEndVisible(str, length, char = ' ') {
|
||
const visibleLength = stripAnsi(str).length;
|
||
const paddingNeeded = Math.max(0, length - visibleLength);
|
||
return str + char.repeat(paddingNeeded);
|
||
}
|
||
|
||
// Legacy targets — paths that were previously installed but are no longer active install targets
|
||
const LEGACY_TARGETS = [
|
||
{
|
||
dir: '.claude/commands',
|
||
filePattern: /^plan2code-.*\.md$/,
|
||
type: 'flat',
|
||
},
|
||
];
|
||
|
||
// Installation targets
|
||
const INSTALL_TARGETS = [
|
||
{
|
||
name: 'Claude Code',
|
||
id: 'claude',
|
||
dir: '.claude/skills',
|
||
type: 'skill',
|
||
filePattern: /^plan2code-/,
|
||
icon: '◉ '
|
||
},
|
||
{
|
||
name: 'Copilot CLI',
|
||
id: 'copilot',
|
||
dir: '.copilot/agents',
|
||
filePattern: /^plan2code-.*\.md$/,
|
||
icon: '◉ '
|
||
},
|
||
{
|
||
name: 'Cursor',
|
||
id: 'cursor',
|
||
dir: '.cursor/commands',
|
||
filePattern: /^plan2code-.*\.md$/,
|
||
icon: '◉ '
|
||
},
|
||
{
|
||
name: 'Continue',
|
||
id: 'continue',
|
||
dir: '.continue/prompts',
|
||
filePattern: /^plan2code-.*\.prompt\.md$/,
|
||
icon: '◉ '
|
||
},
|
||
{
|
||
name: 'Windsurf',
|
||
id: 'windsurf',
|
||
dir: '.codeium/windsurf/global_workflows',
|
||
filePattern: /^plan2code-.*\.md$/,
|
||
icon: '◉ '
|
||
},
|
||
{
|
||
name: 'Codeium (IntelliJ)',
|
||
id: 'codeium',
|
||
dir: '.codeium/global_workflows',
|
||
filePattern: /^plan2code-.*\.md$/,
|
||
icon: '◉ '
|
||
},
|
||
{
|
||
name: 'VS Code Copilot',
|
||
id: 'vscode-copilot',
|
||
dir: getVSCodeCopilotDir,
|
||
sourceDir: 'vscode-copilot-prompts',
|
||
displayPath: process.platform === 'win32'
|
||
? '%APPDATA%\\Code\\User\\prompts\\'
|
||
: process.platform === 'darwin'
|
||
? '~/Library/Application Support/Code/User/prompts/'
|
||
: '~/.config/Code/User/prompts/',
|
||
filePattern: /^plan2code-.*\.prompt\.md$/,
|
||
icon: '◉ '
|
||
},
|
||
{
|
||
name: 'Agent Skills (Amp · Devin · Gemini CLI · OpenCode · Zed)',
|
||
id: 'agent-skills',
|
||
dir: getAgentSkillsDir,
|
||
sourceDir: '.agents/skills',
|
||
type: 'skill',
|
||
filePattern: /^plan2code-/,
|
||
displayPath: '~/.agents/skills',
|
||
icon: '◉ '
|
||
},
|
||
{
|
||
name: 'Crush',
|
||
id: 'crush',
|
||
dir: getCrushSkillsDir,
|
||
sourceDir: 'crush-skills',
|
||
type: 'skill',
|
||
filePattern: /^plan2code-/,
|
||
displayPath: process.platform === 'win32'
|
||
? '%LOCALAPPDATA%\\crush\\skills'
|
||
: '~/.config/crush/skills',
|
||
icon: '◉ '
|
||
},
|
||
{
|
||
name: 'Gemini CLI',
|
||
id: 'gemini',
|
||
dir: '.gemini/commands',
|
||
type: 'toml',
|
||
filePattern: /^plan2code-.*\.toml$/,
|
||
icon: '◉ '
|
||
},
|
||
{
|
||
name: 'Pi',
|
||
id: 'pi',
|
||
dir: '.pi/agent/prompts',
|
||
filePattern: /^plan2code-.*\.md$/,
|
||
icon: '◉ '
|
||
}
|
||
];
|
||
|
||
// ============================================================================
|
||
// DISPLAY FUNCTIONS
|
||
// ============================================================================
|
||
|
||
/**
|
||
* Display mascot with optional message
|
||
*/
|
||
function displayMascot(variant = 'full', message = '') {
|
||
const mascotLines = MASCOT[variant] || MASCOT.full;
|
||
console.log('');
|
||
mascotLines.forEach(line => {
|
||
console.log(`${COLORS.MAGENTA}${line}${COLORS.RESET}`);
|
||
});
|
||
if (message) {
|
||
console.log(`${COLORS.CYAN}${message}${COLORS.RESET}`);
|
||
}
|
||
console.log('');
|
||
}
|
||
|
||
/**
|
||
* Display header
|
||
*/
|
||
function displayHeader() {
|
||
console.log('');
|
||
console.log(`${COLORS.CYAN}${COLORS.BRIGHT}`);
|
||
console.log('╔═════════════════════════════════════════════════════════╗');
|
||
console.log('║ ╭───╮ ║');
|
||
console.log('║ │ ● │ Hi! I\'m Planny! ║');
|
||
console.log('║ │ ◡ │ Nice to meet you ║');
|
||
console.log('║ ╰───╯ Welcome to Plan2Code! ║');
|
||
console.log('║ ║');
|
||
console.log('║ G L O B A L I N S T A L L A T I O N S Y S T E M ║');
|
||
console.log('║ https://jparkerweb.github.io/plan2code ║');
|
||
console.log('║ ║');
|
||
console.log('╚═════════════════════════════════════════════════════════╝');
|
||
console.log(COLORS.RESET);
|
||
console.log('');
|
||
}
|
||
|
||
/**
|
||
* Display section header with border
|
||
*/
|
||
function displaySectionHeader(title, mode = '') {
|
||
const innerWidth = 75;
|
||
|
||
// Center the title with ═ padding
|
||
const titleContent = `[ ${title} ]`;
|
||
const titlePadTotal = innerWidth - titleContent.length;
|
||
const titlePadLeft = Math.floor(titlePadTotal / 2);
|
||
const titlePadRight = titlePadTotal - titlePadLeft;
|
||
const titleLine = '═'.repeat(titlePadLeft) + titleContent + '═'.repeat(titlePadRight);
|
||
|
||
console.log(`${COLORS.CYAN}${COLORS.BRIGHT}`);
|
||
console.log('╔═══════════════════════════════════════════════════════════════════════════╗');
|
||
console.log(`║${' '.repeat(innerWidth)}║`);
|
||
console.log(`║${titleLine}║`);
|
||
if (mode) {
|
||
// Center the mode text with space padding
|
||
const modePadTotal = innerWidth - mode.length;
|
||
const modePadLeft = Math.floor(modePadTotal / 2);
|
||
const modePadRight = modePadTotal - modePadLeft;
|
||
console.log(`║${' '.repeat(modePadLeft)}${mode}${' '.repeat(modePadRight)}║`);
|
||
}
|
||
console.log(`║${' '.repeat(75)}║`);
|
||
console.log('╚═══════════════════════════════════════════════════════════════════════════╝');
|
||
console.log(COLORS.RESET);
|
||
}
|
||
|
||
/**
|
||
* Display operation status box
|
||
*/
|
||
function displayStatusBox(title, lines) {
|
||
console.log(`${COLORS.BLUE}`);
|
||
console.log('┌─────────────────────────────────────────────────────────────────────────┐');
|
||
console.log(`│ ${COLORS.BRIGHT}${title}${COLORS.RESET}${COLORS.BLUE}${' '.repeat(72 - title.length)}│`);
|
||
console.log('├─────────────────────────────────────────────────────────────────────────┤');
|
||
|
||
lines.forEach(line => {
|
||
const cleanLine = line.replace(/\x1b\[[0-9;]*m/g, ''); // Remove color codes for length calc
|
||
const padding = ' '.repeat(Math.max(0, 72 - cleanLine.length));
|
||
console.log(`│ ${line}${padding}│`);
|
||
});
|
||
|
||
console.log('└─────────────────────────────────────────────────────────────────────────┘');
|
||
console.log(COLORS.RESET);
|
||
}
|
||
|
||
/**
|
||
* Display progress indicator
|
||
*/
|
||
function displayProgress(current, total, label) {
|
||
const barLength = 30;
|
||
const filled = Math.floor((current / total) * barLength);
|
||
const empty = barLength - filled;
|
||
const percent = Math.floor((current / total) * 100);
|
||
|
||
const bar = `${COLORS.GREEN}${'█'.repeat(filled)}${COLORS.DIM}${'░'.repeat(empty)}${COLORS.RESET}`;
|
||
process.stdout.write(`\r${COLORS.CYAN}[${bar}${COLORS.CYAN}]${COLORS.RESET} ${percent}% ${label}`);
|
||
}
|
||
|
||
// ============================================================================
|
||
// SYNC PROMPTS FUNCTIONS
|
||
// ============================================================================
|
||
|
||
/**
|
||
* Helper function to recursively delete directories
|
||
*/
|
||
function deleteDirectory(dirPath) {
|
||
if (!fs.existsSync(dirPath)) {
|
||
return;
|
||
}
|
||
try {
|
||
fs.rmSync(dirPath, { recursive: true, force: true });
|
||
} catch (err) {
|
||
console.error(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Could not delete directory ${dirPath}: ${err.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Write a file to a destination with optional YAML header
|
||
*/
|
||
function writeToDestination(rootDir, baseDir, dest, prompt, sourceContent, stats, quiet = false) {
|
||
const destDir = path.join(rootDir, baseDir, dest.dir);
|
||
const destFile = dest.filePattern(prompt);
|
||
const destPath = path.join(destDir, destFile);
|
||
|
||
// Build the output content
|
||
let outputContent;
|
||
if (dest.header) {
|
||
const header = dest.header(prompt);
|
||
outputContent = header + '\n\n' + sourceContent;
|
||
} else {
|
||
outputContent = sourceContent;
|
||
}
|
||
|
||
// Ensure destination directory exists
|
||
if (!fs.existsSync(destDir)) {
|
||
fs.mkdirSync(destDir, { recursive: true });
|
||
}
|
||
|
||
// Check if file needs updating
|
||
let needsUpdate = true;
|
||
if (fs.existsSync(destPath)) {
|
||
try {
|
||
const existingContent = fs.readFileSync(destPath, 'utf8');
|
||
needsUpdate = existingContent !== outputContent;
|
||
} catch (err) {
|
||
// File exists but can't be read, will try to write
|
||
}
|
||
}
|
||
|
||
if (needsUpdate) {
|
||
try {
|
||
fs.writeFileSync(destPath, outputContent, 'utf8');
|
||
if (!quiet) {
|
||
console.log(` ${COLORS.GREEN}▰▰▰${COLORS.RESET} ${destFile}`);
|
||
}
|
||
} catch (err) {
|
||
console.error(` ${COLORS.RED}✖✖✖${COLORS.RESET} ${destFile}: ${err.message}`);
|
||
stats.errors++;
|
||
return;
|
||
}
|
||
stats.updated++;
|
||
} else {
|
||
stats.skipped++;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Write a skill file to a destination as <skill-name>/SKILL.md
|
||
*/
|
||
function writeSkillToDestination(rootDir, baseDir, dest, prompt, sourceContent, stats, quiet = false) {
|
||
const skillName = generateSkillName(prompt);
|
||
const destDir = path.join(rootDir, baseDir, dest.dir, skillName);
|
||
const filePath = path.join(destDir, 'SKILL.md');
|
||
|
||
// Build the output content
|
||
let outputContent;
|
||
if (dest.header) {
|
||
outputContent = dest.header(prompt) + '\n\n' + sourceContent;
|
||
} else {
|
||
outputContent = sourceContent;
|
||
}
|
||
|
||
// Ensure destination directory exists
|
||
fs.mkdirSync(destDir, { recursive: true });
|
||
|
||
// Check if file needs updating
|
||
let needsUpdate = true;
|
||
if (fs.existsSync(filePath)) {
|
||
try {
|
||
const existingContent = fs.readFileSync(filePath, 'utf8');
|
||
needsUpdate = existingContent !== outputContent;
|
||
} catch (err) {
|
||
// File exists but can't be read, will try to write
|
||
}
|
||
}
|
||
|
||
if (needsUpdate) {
|
||
try {
|
||
fs.writeFileSync(filePath, outputContent, 'utf8');
|
||
if (!quiet) {
|
||
console.log(` ${COLORS.GREEN}▰▰▰${COLORS.RESET} ${skillName}/SKILL.md`);
|
||
}
|
||
} catch (err) {
|
||
console.error(` ${COLORS.RED}✖✖✖${COLORS.RESET} ${skillName}/SKILL.md: ${err.message}`);
|
||
stats.errors++;
|
||
return;
|
||
}
|
||
stats.updated++;
|
||
} else {
|
||
stats.skipped++;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Recursively copy a directory and all its contents (files and subdirectories).
|
||
*/
|
||
function copyDirRecursive(src, dest, stats) {
|
||
fs.mkdirSync(dest, { recursive: true });
|
||
const entries = fs.readdirSync(src);
|
||
for (const entry of entries) {
|
||
const srcPath = path.join(src, entry);
|
||
const destPath = path.join(dest, entry);
|
||
if (fs.statSync(srcPath).isDirectory()) {
|
||
copyDirRecursive(srcPath, destPath, stats);
|
||
} else {
|
||
fs.copyFileSync(srcPath, destPath);
|
||
stats.copied++;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Inline reference file content into orchestrator content.
|
||
* Replaces each `Read references/X.md` directive with the file's actual content,
|
||
* wrapped in markers. Used for TOML targets (Gemini CLI), which cannot resolve
|
||
* relative file reads at runtime.
|
||
*/
|
||
function inlineReferenceContent(sourceContent, srcRefDir) {
|
||
if (!fs.existsSync(srcRefDir)) return sourceContent;
|
||
return sourceContent.replace(
|
||
/^Read\s+references\/([^\s]+\.md)(.*)$/gm,
|
||
(match, filename, suffix) => {
|
||
const refPath = path.join(srcRefDir, filename);
|
||
if (!fs.existsSync(refPath)) return match;
|
||
const refContent = fs.readFileSync(refPath, 'utf8');
|
||
return `<!-- BEGIN inlined references/${filename}${suffix} -->\n${refContent}\n<!-- END inlined references/${filename} -->`;
|
||
}
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Copy a reference directory (e.g., plan2code-review-references/) to a destination.
|
||
* Used by syncPrompts() to distribute reference files alongside orchestrator files.
|
||
*/
|
||
function copyReferenceDirectory(srcRefDir, destRefDir, stats, quiet = false) {
|
||
const copyStats = { copied: 0 };
|
||
try {
|
||
copyDirRecursive(srcRefDir, destRefDir, copyStats);
|
||
} catch (err) {
|
||
console.error(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Failed to copy reference directory: ${err.message}`);
|
||
stats.errors++;
|
||
return;
|
||
}
|
||
stats.updated += copyStats.copied;
|
||
if (!quiet) {
|
||
const refDirName = path.basename(destRefDir);
|
||
const files = fs.readdirSync(srcRefDir);
|
||
for (const file of files) {
|
||
console.log(` ${COLORS.GREEN}▰▰▰${COLORS.RESET} ${refDirName}/${file}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Write a TOML file to a destination for Gemini CLI
|
||
*/
|
||
function writeTomlToDestination(rootDir, baseDir, dest, prompt, sourceContent, stats, quiet = false) {
|
||
const filename = dest.filePattern(prompt);
|
||
const destDir = path.join(rootDir, baseDir, dest.dir);
|
||
const filePath = path.join(destDir, filename);
|
||
|
||
// Build TOML content
|
||
const tomlContent = generateTomlContent(prompt, sourceContent);
|
||
|
||
// Ensure destination directory exists
|
||
fs.mkdirSync(destDir, { recursive: true });
|
||
|
||
// Check if file needs updating
|
||
let needsUpdate = true;
|
||
if (fs.existsSync(filePath)) {
|
||
try {
|
||
const existingContent = fs.readFileSync(filePath, 'utf8');
|
||
needsUpdate = existingContent !== tomlContent;
|
||
} catch (err) {
|
||
// File exists but can't be read, will try to write
|
||
}
|
||
}
|
||
|
||
if (needsUpdate) {
|
||
try {
|
||
fs.writeFileSync(filePath, tomlContent, 'utf8');
|
||
if (!quiet) {
|
||
console.log(` ${COLORS.GREEN}▰▰▰${COLORS.RESET} ${filename}`);
|
||
}
|
||
} catch (err) {
|
||
console.error(` ${COLORS.RED}✖✖✖${COLORS.RESET} ${filename}: ${err.message}`);
|
||
stats.errors++;
|
||
return;
|
||
}
|
||
stats.updated++;
|
||
} else {
|
||
stats.skipped++;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Sync prompts from src/ to dist/ directories
|
||
* This generates the distribution files before installation
|
||
*/
|
||
function syncPrompts(quiet = false) {
|
||
const stats = { updated: 0, skipped: 0, errors: 0 };
|
||
const projectRoot = __dirname;
|
||
|
||
if (!quiet) {
|
||
console.log(`${COLORS.CYAN}${SYMBOLS.ACTIVE} GENERATING DISTRIBUTION FILES${COLORS.RESET}\n`);
|
||
}
|
||
|
||
// Clean up old dist directories
|
||
const localDistPath = path.join(projectRoot, 'dist/local-commands');
|
||
const globalDistPath = path.join(projectRoot, 'dist/global-commands');
|
||
|
||
if (fs.existsSync(localDistPath)) {
|
||
if (!quiet) console.log(` ${COLORS.YELLOW}░░░${COLORS.RESET} Cleaning: dist/local-commands/`);
|
||
deleteDirectory(localDistPath);
|
||
}
|
||
|
||
if (fs.existsSync(globalDistPath)) {
|
||
if (!quiet) console.log(` ${COLORS.YELLOW}░░░${COLORS.RESET} Cleaning: dist/global-commands/`);
|
||
deleteDirectory(globalDistPath);
|
||
}
|
||
|
||
// Process each source prompt
|
||
for (const prompt of SOURCE_PROMPTS) {
|
||
const sourcePath = path.join(SRC_DIR, prompt.source);
|
||
|
||
// Read source content
|
||
let sourceContent;
|
||
try {
|
||
sourceContent = fs.readFileSync(sourcePath, 'utf8');
|
||
} catch (err) {
|
||
console.error(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Could not read: src/${prompt.source}`);
|
||
stats.errors++;
|
||
continue;
|
||
}
|
||
|
||
// Detect reference directory (e.g., plan2code-review-references/)
|
||
const refDirName = prompt.source.replace(/\.md$/, '-references');
|
||
const srcRefDir = path.join(SRC_DIR, refDirName);
|
||
const hasRefDir = fs.existsSync(srcRefDir);
|
||
|
||
// Write to dist/local-commands/ and dist/global-commands/
|
||
const distBases = [
|
||
{ base: 'dist/local-commands', destinations: LOCAL_DESTINATIONS },
|
||
{ base: 'dist/global-commands', destinations: GLOBAL_DESTINATIONS },
|
||
];
|
||
|
||
for (const { base, destinations } of distBases) {
|
||
for (const dest of destinations) {
|
||
if (dest.type === 'skill') {
|
||
writeSkillToDestination(projectRoot, base, dest, prompt, sourceContent, stats, quiet);
|
||
if (hasRefDir) {
|
||
const skillName = generateSkillName(prompt);
|
||
const destRefDir = path.join(projectRoot, base, dest.dir, skillName, 'references');
|
||
copyReferenceDirectory(srcRefDir, destRefDir, stats, quiet);
|
||
}
|
||
} else if (dest.type === 'toml') {
|
||
// TOML targets (Gemini CLI) cannot resolve relative file reads at runtime,
|
||
// so inline reference content directly into the prompt body.
|
||
const contentForToml = hasRefDir
|
||
? inlineReferenceContent(sourceContent, srcRefDir)
|
||
: sourceContent;
|
||
writeTomlToDestination(projectRoot, base, dest, prompt, contentForToml, stats, quiet);
|
||
} else {
|
||
// For flat-file destinations, rewrite Read directive paths to sibling directory name
|
||
const contentForDest = hasRefDir
|
||
? sourceContent.replace(/^(Read\s+)references\//gm, '$1' + refDirName + '/')
|
||
: sourceContent;
|
||
writeToDestination(projectRoot, base, dest, prompt, contentForDest, stats, quiet);
|
||
if (hasRefDir) {
|
||
const destRefDir = path.join(projectRoot, base, dest.dir, refDirName);
|
||
copyReferenceDirectory(srcRefDir, destRefDir, stats, quiet);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!quiet) {
|
||
console.log('');
|
||
console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Generated ${stats.updated} files (${stats.skipped} unchanged)`);
|
||
if (stats.errors > 0) {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} ${stats.errors} errors`);
|
||
}
|
||
console.log('');
|
||
}
|
||
|
||
return stats.errors === 0;
|
||
}
|
||
|
||
// ============================================================================
|
||
// TARGET SELECTION
|
||
// ============================================================================
|
||
|
||
/**
|
||
* Get targets to process based on selected platforms
|
||
*/
|
||
function getTargets(platformIds = null) {
|
||
if (platformIds && platformIds.length > 0) {
|
||
return INSTALL_TARGETS.filter(t => platformIds.includes(t.id));
|
||
}
|
||
return INSTALL_TARGETS;
|
||
}
|
||
|
||
// ============================================================================
|
||
// INSTALLATION
|
||
// ============================================================================
|
||
|
||
/**
|
||
* Recursively copy skill subdirectories matching /^plan2code-/ from source to dest
|
||
*/
|
||
function copySkillDirectory(sourcePath, destPath, stats) {
|
||
fs.mkdirSync(destPath, { recursive: true });
|
||
const entries = fs.readdirSync(sourcePath);
|
||
for (const entry of entries) {
|
||
if (!/^plan2code-/.test(entry)) continue;
|
||
const srcSubdir = path.join(sourcePath, entry);
|
||
if (!fs.statSync(srcSubdir).isDirectory()) continue;
|
||
const destSubdir = path.join(destPath, entry);
|
||
fs.mkdirSync(destSubdir, { recursive: true });
|
||
const files = fs.readdirSync(srcSubdir);
|
||
for (const file of files) {
|
||
const srcEntry = path.join(srcSubdir, file);
|
||
const destEntry = path.join(destSubdir, file);
|
||
if (fs.statSync(srcEntry).isDirectory()) {
|
||
// Recursively copy nested subdirectories (e.g., references/)
|
||
copyDirRecursive(srcEntry, destEntry, stats);
|
||
} else {
|
||
fs.copyFileSync(srcEntry, destEntry);
|
||
stats.copied++;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Remove files/dirs matching LEGACY_TARGETS patterns (silent cleanup)
|
||
*/
|
||
function cleanLegacyTargets() {
|
||
const homedir = os.homedir();
|
||
for (const target of LEGACY_TARGETS) {
|
||
const targetDir = path.join(homedir, target.dir);
|
||
if (!fs.existsSync(targetDir)) continue;
|
||
const entries = fs.readdirSync(targetDir);
|
||
for (const entry of entries) {
|
||
if (!target.filePattern.test(entry)) continue;
|
||
const entryPath = path.join(targetDir, entry);
|
||
if (target.type === 'skill') {
|
||
fs.rmSync(entryPath, { recursive: true, force: true });
|
||
} else {
|
||
fs.unlinkSync(entryPath);
|
||
}
|
||
}
|
||
// Clean up any orphaned reference directories
|
||
for (const entry of entries) {
|
||
if (/^plan2code-.*-references$/.test(entry)) {
|
||
const entryPath = path.join(targetDir, entry);
|
||
if (fs.existsSync(entryPath) && fs.statSync(entryPath).isDirectory()) {
|
||
fs.rmSync(entryPath, { recursive: true, force: true });
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Execute installation
|
||
*/
|
||
async function install(targets = null) {
|
||
const projectRoot = __dirname;
|
||
const homeDir = os.homedir();
|
||
const targetList = targets || getTargets();
|
||
|
||
let totalInstalled = 0;
|
||
let totalErrors = 0;
|
||
|
||
displaySectionHeader(' INSTALLING FILES ');
|
||
|
||
// Generate distribution files first
|
||
if (!syncPrompts(true)) {
|
||
console.log(`${COLORS.RED}${SYMBOLS.ERROR} Failed to generate distribution files${COLORS.RESET}`);
|
||
return 1;
|
||
}
|
||
console.log(`${COLORS.GREEN}${SYMBOLS.SUCCESS} Distribution files generated${COLORS.RESET}\n`);
|
||
|
||
// Clean up legacy install targets before installing new ones
|
||
cleanLegacyTargets();
|
||
|
||
console.log('');
|
||
displayStatusBox('PARAMETERS', [
|
||
`${COLORS.CYAN}Source:${COLORS.RESET} ${SOURCE_BASE}/`,
|
||
`${COLORS.CYAN}Home Directory:${COLORS.RESET} ${homeDir}`,
|
||
`${COLORS.CYAN}Platforms:${COLORS.RESET} ${targetList.length}`,
|
||
`${COLORS.CYAN}Mode:${COLORS.RESET} INSTALL`,
|
||
]);
|
||
console.log('');
|
||
|
||
console.log(`${COLORS.YELLOW}${SYMBOLS.ACTIVE} STARTING INSTALLATION${COLORS.RESET}\n`);
|
||
|
||
for (const target of targetList) {
|
||
// Handle both static dir strings and dynamic dir functions
|
||
const sourceSubDir = target.sourceDir || target.dir;
|
||
const sourcePath = path.join(projectRoot, SOURCE_BASE, typeof sourceSubDir === 'function' ? sourceSubDir() : sourceSubDir);
|
||
const destPath = resolveTargetDir(target);
|
||
const displayPathStr = getDisplayPath(target);
|
||
|
||
console.log(`${COLORS.CYAN}╔═══════════════════════════════════════════════════════════════════════════╗${COLORS.RESET}`);
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}${target.icon} ${target.name}${COLORS.RESET}`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.DIM}Path: ${displayPathStr}${COLORS.RESET}`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(`${COLORS.CYAN}╚═══════════════════════════════════════════════════════════════════════════╝${COLORS.RESET}`);
|
||
|
||
// Check if source directory exists
|
||
if (!fs.existsSync(sourcePath)) {
|
||
const sourceDisplayDir = target.sourceDir || (typeof target.dir === 'string' ? target.dir : 'vscode-copilot-prompts');
|
||
console.log(` ${COLORS.YELLOW}${SYMBOLS.WARNING} WARNING${COLORS.RESET} Source not found: ${SOURCE_BASE}/${sourceDisplayDir}`);
|
||
console.log(` ${COLORS.YELLOW}${SYMBOLS.INFO}${COLORS.RESET} Distribution files may be missing. Check src/ directory for source prompts.\n`);
|
||
totalErrors++;
|
||
continue;
|
||
}
|
||
|
||
// Get files/entries to install
|
||
let files;
|
||
try {
|
||
files = fs.readdirSync(sourcePath).filter(f => target.filePattern.test(f));
|
||
} catch (err) {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR} ERROR${COLORS.RESET} Failed to read source directory: ${err.message}\n`);
|
||
totalErrors++;
|
||
continue;
|
||
}
|
||
|
||
if (files.length === 0) {
|
||
const sourceDisplayDir = target.sourceDir || (typeof target.dir === 'string' ? target.dir : 'vscode-copilot-prompts');
|
||
console.log(` ${COLORS.YELLOW}${SYMBOLS.WARNING} WARNING${COLORS.RESET} No Plan2Code files found in ${SOURCE_BASE}/${sourceDisplayDir}\n`);
|
||
continue;
|
||
}
|
||
|
||
// Create destination directory if needed
|
||
if (!fs.existsSync(destPath)) {
|
||
try {
|
||
fs.mkdirSync(destPath, { recursive: true });
|
||
console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS} CREATED${COLORS.RESET} Directory: ${displayPathStr}`);
|
||
} catch (err) {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR} ERROR${COLORS.RESET} Cannot create directory: ${err.message}\n`);
|
||
totalErrors++;
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// Clean up old files/dirs before copying new ones
|
||
try {
|
||
const existingFiles = fs.readdirSync(destPath).filter(f => target.filePattern.test(f));
|
||
if (existingFiles.length > 0) {
|
||
console.log(` ${COLORS.YELLOW}${SYMBOLS.ACTIVE} CLEANING${COLORS.RESET} ${existingFiles.length} existing file(s)...`);
|
||
for (const oldFile of existingFiles) {
|
||
const oldFilePath = path.join(destPath, oldFile);
|
||
try {
|
||
if (target.type === 'skill') {
|
||
fs.rmSync(oldFilePath, { recursive: true, force: true });
|
||
} else {
|
||
fs.unlinkSync(oldFilePath);
|
||
}
|
||
console.log(` ${COLORS.YELLOW}░░░${COLORS.RESET} Removed: ${oldFile}`);
|
||
} catch (err) {
|
||
console.log(` ${COLORS.RED}✖✖✖${COLORS.RESET} Failed to remove ${oldFile}: ${err.message}`);
|
||
}
|
||
}
|
||
}
|
||
// Also clean old reference directories for flat-file targets
|
||
if (target.type !== 'skill') {
|
||
const existingRefDirs = fs.readdirSync(destPath).filter(f =>
|
||
/^plan2code-.*-references$/.test(f) &&
|
||
fs.existsSync(path.join(destPath, f)) &&
|
||
fs.statSync(path.join(destPath, f)).isDirectory()
|
||
);
|
||
for (const refDir of existingRefDirs) {
|
||
fs.rmSync(path.join(destPath, refDir), { recursive: true, force: true });
|
||
console.log(` ${COLORS.YELLOW}░░░${COLORS.RESET} Removed: ${refDir}/`);
|
||
}
|
||
}
|
||
} catch (err) {
|
||
// Directory might be newly created, ignore read errors
|
||
}
|
||
|
||
// Copy files/skill directories
|
||
if (target.type === 'skill') {
|
||
const copyStats = { copied: 0 };
|
||
console.log(` ${COLORS.CYAN}${SYMBOLS.ACTIVE} COPYING${COLORS.RESET} skill directories...`);
|
||
try {
|
||
copySkillDirectory(sourcePath, destPath, copyStats);
|
||
console.log(` ${COLORS.GREEN}▰▰▰${COLORS.RESET} ${copyStats.copied} file(s) across ${files.length} skill(s)`);
|
||
totalInstalled += copyStats.copied;
|
||
} catch (err) {
|
||
console.log(` ${COLORS.RED}✖✖✖${COLORS.RESET} Copy failed: ${err.message}`);
|
||
totalErrors++;
|
||
}
|
||
} else {
|
||
console.log(` ${COLORS.CYAN}${SYMBOLS.ACTIVE} COPYING${COLORS.RESET} ${files.length} file(s)...`);
|
||
for (const file of files) {
|
||
const srcFile = path.join(sourcePath, file);
|
||
const destFile = path.join(destPath, file);
|
||
|
||
try {
|
||
fs.copyFileSync(srcFile, destFile);
|
||
console.log(` ${COLORS.GREEN}▰▰▰${COLORS.RESET} ${file}`);
|
||
totalInstalled++;
|
||
} catch (err) {
|
||
console.log(` ${COLORS.RED}✖✖✖${COLORS.RESET} ${file}: ${err.message}`);
|
||
totalErrors++;
|
||
}
|
||
}
|
||
|
||
// Copy sibling reference directories for flat-file targets.
|
||
// Source-of-truth: scan sourcePath for *-references directories rather than deriving
|
||
// names from prompt filenames (extension assumptions break when new file types are added).
|
||
const refDirs = fs.readdirSync(sourcePath).filter(f =>
|
||
/^plan2code-.*-references$/.test(f) &&
|
||
fs.statSync(path.join(sourcePath, f)).isDirectory()
|
||
);
|
||
for (const refDirName of refDirs) {
|
||
const srcRefDir = path.join(sourcePath, refDirName);
|
||
const destRefDir = path.join(destPath, refDirName);
|
||
const refStats = { copied: 0 };
|
||
try {
|
||
copyDirRecursive(srcRefDir, destRefDir, refStats);
|
||
} catch (err) {
|
||
console.log(` ${COLORS.RED}✖✖✖${COLORS.RESET} Failed to copy ${refDirName}/: ${err.message}`);
|
||
totalErrors++;
|
||
continue;
|
||
}
|
||
console.log(` ${COLORS.GREEN}▰▰▰${COLORS.RESET} ${refDirName}/ (${refStats.copied} reference files)`);
|
||
totalInstalled += refStats.copied;
|
||
}
|
||
}
|
||
console.log('');
|
||
}
|
||
|
||
// Summary
|
||
console.log(`${COLORS.CYAN}╔═══════════════════════════════════════════════════════════════════════════╗${COLORS.RESET}`);
|
||
console.log(`${COLORS.CYAN}║${COLORS.RESET}${' '.repeat(75)}${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(`${COLORS.CYAN}║${COLORS.RESET}${' '.repeat(31)}${COLORS.BRIGHT}S U M M A R Y${COLORS.RESET}${' '.repeat(31)}${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(`${COLORS.CYAN}║${COLORS.RESET}${' '.repeat(75)}${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(`${COLORS.CYAN}╠═══════════════════════════════════════════════════════════════════════════╣${COLORS.RESET}`);
|
||
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.GREEN}Mode:${COLORS.RESET} INSTALL`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
|
||
const statusColor = totalErrors > 0 ? COLORS.RED : COLORS.GREEN;
|
||
const statusText = totalErrors > 0 ? 'COMPLETED WITH ERRORS' : 'SUCCESS';
|
||
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${statusColor}Status:${COLORS.RESET} ${statusColor}${statusText}${COLORS.RESET}`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} Files Installed: ${COLORS.GREEN}${totalInstalled}${COLORS.RESET}`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
|
||
|
||
if (totalErrors > 0) {
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} Errors: ${COLORS.RED}${totalErrors}${COLORS.RESET}`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
}
|
||
|
||
console.log(`${COLORS.CYAN}║${COLORS.RESET}${' '.repeat(75)}${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(`${COLORS.CYAN}╚═══════════════════════════════════════════════════════════════════════════╝${COLORS.RESET}`);
|
||
|
||
// Show celebratory or sad mascot based on result
|
||
console.log('');
|
||
if (totalErrors === 0) {
|
||
console.log(`${COLORS.GREEN} ╭───╮${COLORS.RESET} ${COLORS.BRIGHT}All done! Happy coding!${COLORS.RESET}`);
|
||
console.log(`${COLORS.GREEN} │ ${COLORS.CYAN}★${COLORS.GREEN} │${COLORS.RESET} ${COLORS.BRIGHT}If this is your first time using Plan2Code, read the docs here:${COLORS.RESET}`);
|
||
console.log(`${COLORS.GREEN} │ ${COLORS.BRIGHT}◡${COLORS.GREEN} │${COLORS.RESET} ${COLORS.BRIGHT}https://github.com/jparkerweb/plan2code${COLORS.RESET}`);
|
||
console.log(`${COLORS.GREEN} ╰───╯${COLORS.RESET}`);
|
||
}
|
||
|
||
console.log('');
|
||
|
||
return totalErrors === 0 ? 0 : 1;
|
||
}
|
||
|
||
// ============================================================================
|
||
// UNINSTALLATION
|
||
// ============================================================================
|
||
|
||
/**
|
||
* Execute uninstallation
|
||
*/
|
||
function uninstallFiles(targets = null) {
|
||
const homeDir = os.homedir();
|
||
const targetList = targets || getTargets();
|
||
|
||
let totalRemoved = 0;
|
||
let totalErrors = 0;
|
||
|
||
displaySectionHeader('UNINSTALLATION', '[ REMOVING FILES ]');
|
||
|
||
console.log('');
|
||
displayStatusBox('PARAMETERS', [
|
||
`${COLORS.CYAN}Home Directory:${COLORS.RESET} ${homeDir}`,
|
||
`${COLORS.CYAN}Platforms:${COLORS.RESET} ${targetList.length}`,
|
||
`${COLORS.CYAN}Mode:${COLORS.RESET} UNINSTALL`,
|
||
]);
|
||
console.log('');
|
||
|
||
// Clean up legacy install targets
|
||
cleanLegacyTargets();
|
||
|
||
console.log(`${COLORS.YELLOW}${SYMBOLS.ACTIVE} STARTING UNINSTALLATION${COLORS.RESET}\n`);
|
||
|
||
for (const target of targetList) {
|
||
const destPath = resolveTargetDir(target);
|
||
const displayPathStr = getDisplayPath(target);
|
||
|
||
console.log(`${COLORS.CYAN}╔═══════════════════════════════════════════════════════════════════════════╗${COLORS.RESET}`);
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}${target.icon} ${target.name}${COLORS.RESET}`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.DIM}Path: ${displayPathStr}${COLORS.RESET}`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(`${COLORS.CYAN}╚═══════════════════════════════════════════════════════════════════════════╝${COLORS.RESET}`);
|
||
|
||
// Check if destination directory exists
|
||
if (!fs.existsSync(destPath)) {
|
||
console.log(` ${COLORS.DIM}${SYMBOLS.INFO} Directory not found - nothing to remove\n${COLORS.RESET}`);
|
||
continue;
|
||
}
|
||
|
||
// Get files/entries to remove
|
||
let files;
|
||
try {
|
||
files = fs.readdirSync(destPath).filter(f => target.filePattern.test(f));
|
||
} catch (err) {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR} ERROR${COLORS.RESET} Failed to read directory: ${err.message}\n`);
|
||
totalErrors++;
|
||
continue;
|
||
}
|
||
|
||
if (files.length === 0) {
|
||
console.log(` ${COLORS.DIM}${SYMBOLS.INFO} No Plan2Code files found${COLORS.RESET}`);
|
||
} else {
|
||
// Remove files/skill directories
|
||
console.log(` ${COLORS.CYAN}${SYMBOLS.ACTIVE} REMOVING${COLORS.RESET} ${files.length} file(s)...`);
|
||
for (const file of files) {
|
||
const destFile = path.join(destPath, file);
|
||
|
||
try {
|
||
if (target.type === 'skill') {
|
||
// recursive: true handles nested subdirectories (e.g., references/)
|
||
fs.rmSync(destFile, { recursive: true, force: true });
|
||
} else {
|
||
fs.unlinkSync(destFile);
|
||
}
|
||
console.log(` ${COLORS.GREEN}▰▰▰${COLORS.RESET} Removed: ${file}`);
|
||
totalRemoved++;
|
||
} catch (err) {
|
||
console.log(` ${COLORS.RED}✖✖✖${COLORS.RESET} ${file}: ${err.message}`);
|
||
totalErrors++;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Remove sibling reference directories for flat-file targets.
|
||
// Runs unconditionally so orphaned reference dirs are cleaned even if prompt files were already removed.
|
||
if (target.type !== 'skill') {
|
||
try {
|
||
const refDirs = fs.readdirSync(destPath).filter(f =>
|
||
/^plan2code-.*-references$/.test(f) &&
|
||
fs.existsSync(path.join(destPath, f)) &&
|
||
fs.statSync(path.join(destPath, f)).isDirectory()
|
||
);
|
||
for (const refDir of refDirs) {
|
||
fs.rmSync(path.join(destPath, refDir), { recursive: true, force: true });
|
||
console.log(` ${COLORS.GREEN}▰▰▰${COLORS.RESET} Removed: ${refDir}/`);
|
||
totalRemoved++;
|
||
}
|
||
} catch (err) {
|
||
// Directory may already be removed, ignore
|
||
}
|
||
}
|
||
console.log('');
|
||
}
|
||
|
||
// Summary
|
||
console.log(`${COLORS.CYAN}╔═══════════════════════════════════════════════════════════════════════════╗${COLORS.RESET}`);
|
||
console.log(`${COLORS.CYAN}║${COLORS.RESET}${' '.repeat(75)}${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(`${COLORS.CYAN}║${COLORS.RESET}${' '.repeat(31)}${COLORS.BRIGHT}S U M M A R Y${COLORS.RESET}${' '.repeat(31)}${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(`${COLORS.CYAN}║${COLORS.RESET}${' '.repeat(75)}${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(`${COLORS.CYAN}╠═══════════════════════════════════════════════════════════════════════════╣${COLORS.RESET}`);
|
||
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.GREEN}Mode:${COLORS.RESET} UNINSTALL`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
|
||
const statusColor = totalErrors > 0 ? COLORS.RED : COLORS.GREEN;
|
||
const statusText = totalErrors > 0 ? 'COMPLETED WITH ERRORS' : 'SUCCESS';
|
||
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${statusColor}Status:${COLORS.RESET} ${statusColor}${statusText}${COLORS.RESET}`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} Files Removed: ${COLORS.GREEN}${totalRemoved}${COLORS.RESET}`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
|
||
if (totalErrors > 0) {
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} Errors: ${COLORS.RED}${totalErrors}${COLORS.RESET}`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
}
|
||
|
||
console.log(`${COLORS.CYAN}║${COLORS.RESET}${' '.repeat(75)}${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(`${COLORS.CYAN}╚═══════════════════════════════════════════════════════════════════════════╝${COLORS.RESET}`);
|
||
|
||
console.log('');
|
||
|
||
return totalErrors === 0 ? 0 : 1;
|
||
}
|
||
|
||
// ============================================================================
|
||
// LOCAL INSTALLATION INSTRUCTIONS
|
||
// ============================================================================
|
||
|
||
/**
|
||
* Display local installation instructions for manual copy/paste
|
||
*/
|
||
function displayLocalInstructions() {
|
||
const projectRoot = __dirname;
|
||
|
||
displaySectionHeader('LOCAL INSTALLATION', '[ MANUAL COPY/PASTE INSTRUCTIONS ]');
|
||
|
||
// Generate dist files first
|
||
console.log(`${COLORS.CYAN}${SYMBOLS.ACTIVE} GENERATING DISTRIBUTION FILES${COLORS.RESET}\n`);
|
||
if (!syncPrompts(true)) {
|
||
console.log(`${COLORS.RED}${SYMBOLS.ERROR} Failed to generate distribution files${COLORS.RESET}`);
|
||
return 1;
|
||
}
|
||
console.log(`${COLORS.GREEN}${SYMBOLS.SUCCESS} Distribution files generated${COLORS.RESET}\n`);
|
||
|
||
const localDistPath = path.join(projectRoot, 'dist', 'local-commands');
|
||
|
||
console.log(`${COLORS.GREEN}${SYMBOLS.ACTIVE} LOCAL INSTALLATION INSTRUCTIONS${COLORS.RESET}\n`);
|
||
|
||
displayStatusBox('OVERVIEW', [
|
||
`Local installation copies prompts to your ${COLORS.BRIGHT}project directory${COLORS.RESET}`,
|
||
`(not your home directory). This is useful for project-specific setups.`,
|
||
'',
|
||
`Files have been generated in:`,
|
||
`${COLORS.CYAN}${localDistPath}${COLORS.RESET}`,
|
||
]);
|
||
|
||
console.log('');
|
||
console.log(`${COLORS.CYAN}╔═══════════════════════════════════════════════════════════════════════════╗${COLORS.RESET}`);
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}COPY THE APPROPRIATE FOLDER TO YOUR PROJECT ROOT${COLORS.RESET}`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(`${COLORS.CYAN}╠═══════════════════════════════════════════════════════════════════════════╣${COLORS.RESET}`);
|
||
|
||
// Show local destinations
|
||
const localDirs = [
|
||
{ name: 'Claude Code', dir: '.claude/commands/', desc: 'Slash commands for Claude Code' },
|
||
{ name: 'Cursor', dir: '.cursor/commands/', desc: 'Slash commands for Cursor IDE' },
|
||
{ name: 'GitHub Copilot', dir: '.github/prompts/', desc: 'Prompts for GitHub Copilot' },
|
||
{ name: 'Continue', dir: '.continue/prompts/', desc: 'Prompts for Continue extension' },
|
||
{ name: 'Windsurf', dir: '.windsurf/workflows/', desc: 'Workflows for Windsurf' },
|
||
{ name: 'Codeium (IntelliJ)', dir: '.codeium/global_workflows/', desc: 'Codeium global workflows' },
|
||
{ name: 'Pi', dir: '.pi/prompts/', desc: 'Prompt templates for pi.dev coding agent' },
|
||
];
|
||
|
||
localDirs.forEach((item, i) => {
|
||
const num = `${COLORS.BRIGHT}${i + 1}.${COLORS.RESET}`;
|
||
const name = `${COLORS.GREEN}${item.name.padEnd(16)}${COLORS.RESET}`;
|
||
const dir = `${COLORS.DIM}${item.dir}${COLORS.RESET}`;
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${num} ${name} ${dir}`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
});
|
||
|
||
console.log(`${COLORS.CYAN}╚═══════════════════════════════════════════════════════════════════════════╝${COLORS.RESET}`);
|
||
|
||
console.log('');
|
||
displayStatusBox('EXAMPLE COMMANDS', [
|
||
`${COLORS.BRIGHT}For Claude Code:${COLORS.RESET}`,
|
||
`${COLORS.GREEN}cp -r dist/local-commands/.claude YOUR_PROJECT/${COLORS.RESET}`,
|
||
'',
|
||
`${COLORS.BRIGHT}For Cursor:${COLORS.RESET}`,
|
||
`${COLORS.GREEN}cp -r dist/local-commands/.cursor YOUR_PROJECT/${COLORS.RESET}`,
|
||
'',
|
||
`${COLORS.BRIGHT}For GitHub Copilot:${COLORS.RESET}`,
|
||
`${COLORS.GREEN}cp -r dist/local-commands/.github YOUR_PROJECT/${COLORS.RESET}`,
|
||
'',
|
||
`${COLORS.BRIGHT}For Continue:${COLORS.RESET}`,
|
||
`${COLORS.GREEN}cp -r dist/local-commands/.continue YOUR_PROJECT/${COLORS.RESET}`,
|
||
]);
|
||
|
||
console.log('');
|
||
console.log(`${COLORS.DIM}The dist/local-commands/ folder contains all necessary files organized by tool.`);
|
||
console.log(`Copy the appropriate subfolder(s) to your project root directory.${COLORS.RESET}`);
|
||
console.log('');
|
||
|
||
return 0;
|
||
}
|
||
|
||
// ============================================================================
|
||
// INTERACTIVE MENU
|
||
// ============================================================================
|
||
|
||
/**
|
||
* Run interactive menu interface
|
||
*/
|
||
function runInteractive() {
|
||
const rl = readline.createInterface({
|
||
input: process.stdin,
|
||
output: process.stdout
|
||
});
|
||
|
||
const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
|
||
|
||
async function main() {
|
||
// Task 2.1: Main menu display
|
||
displayHeader();
|
||
|
||
console.log(`${COLORS.BLUE}${COLORS.BRIGHT}Version Info:${COLORS.RESET} ${projectVersion.name} ${projectVersion.version}`);
|
||
console.log('');
|
||
|
||
console.log(`${COLORS.CYAN}╔═════════════════════════════════════════════════════════╗${COLORS.RESET}`);
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}INSTALL PLAN2CODE${COLORS.RESET}`, 58) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(`${COLORS.CYAN}╠═════════════════════════════════════════════════════════╣${COLORS.RESET}`);
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}I.${COLORS.RESET} ${COLORS.GREEN}INSTALL${COLORS.RESET} Install Plan2Code for all platforms`, 58) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}A.${COLORS.RESET} ${COLORS.MAGENTA}ALL${COLORS.RESET} Install Plan2Code + dev tools`, 58) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}U.${COLORS.RESET} ${COLORS.RED}UNINSTALL${COLORS.RESET} Remove Plan2Code files`, 58) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}C.${COLORS.RESET} ${COLORS.BLUE}CUSTOM${COLORS.RESET} Advanced options`, 58) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}Q.${COLORS.RESET} ${COLORS.DIM}QUIT${COLORS.RESET} Exit`, 58) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(`${COLORS.CYAN}╚═════════════════════════════════════════════════════════╝${COLORS.RESET}`);
|
||
console.log('');
|
||
|
||
// Input prompt
|
||
console.log('');
|
||
const answer = await question(`${COLORS.CYAN}${SYMBOLS.SELECT} SELECT OPTION${COLORS.RESET} (I, A, U, C, Q) [I]: `);
|
||
const input = answer.trim().toUpperCase() || 'I';
|
||
|
||
// I path — Install prompts + loop
|
||
if (input === 'I') {
|
||
rl.close();
|
||
const loopResult = installPlan2CodeLoop();
|
||
console.log('');
|
||
const installResult = await install();
|
||
const exitCode = loopResult !== 0 ? loopResult : installResult;
|
||
process.exit(exitCode);
|
||
}
|
||
|
||
// A path — Install Everything (prompts + loop + bot + metrics + Claude status line)
|
||
if (input === 'A') {
|
||
rl.close();
|
||
const loopResult = installPlan2CodeLoop();
|
||
console.log('');
|
||
const botResult = installPlan2CodeBot();
|
||
console.log('');
|
||
const metricsResult = installPlan2CodeMetrics();
|
||
console.log('');
|
||
const statusLineResult = await installStatusLine();
|
||
console.log('');
|
||
const installResult = await install();
|
||
const exitCode = loopResult !== 0 ? loopResult : botResult !== 0 ? botResult : metricsResult !== 0 ? metricsResult : statusLineResult !== 0 ? statusLineResult : installResult;
|
||
process.exit(exitCode);
|
||
}
|
||
|
||
// U path — Uninstall All
|
||
if (input === 'U') {
|
||
console.log('');
|
||
console.log(`${COLORS.RED} o o${COLORS.RESET}`);
|
||
console.log(`${COLORS.RED} ╲ ╱ ${COLORS.YELLOW}!${COLORS.RESET}`);
|
||
console.log(`${COLORS.RED} ╭───╮${COLORS.RESET}`);
|
||
console.log(`${COLORS.RED} │ ${COLORS.YELLOW}○${COLORS.RED} │${COLORS.RESET}`);
|
||
console.log(`${COLORS.RED} │ ${COLORS.YELLOW}~${COLORS.RED} │${COLORS.RESET} ${COLORS.DIM}Are you sure? This will remove Plan2Code from all platforms.${COLORS.RESET}`);
|
||
console.log(`${COLORS.RED} ├───┤${COLORS.RESET}`);
|
||
console.log(`${COLORS.RED} │ · │${COLORS.RESET}`);
|
||
console.log(`${COLORS.RED} ╰───╯${COLORS.RESET}`);
|
||
console.log('');
|
||
const confirmAnswer = await question(`${COLORS.RED}${SYMBOLS.SELECT} CONFIRM UNINSTALL${COLORS.RESET} (Y/N) [N]: `);
|
||
const confirmInput = confirmAnswer.trim().toUpperCase() || 'N';
|
||
|
||
if (confirmInput === 'Y') {
|
||
rl.close();
|
||
const uninstallResult = uninstallFiles();
|
||
const loopResult = uninstallPlan2CodeLoop();
|
||
const metricsResult = uninstallPlan2CodeMetrics();
|
||
const botResult = uninstallPlan2CodeBot();
|
||
const statusLineResult = uninstallStatusLine();
|
||
const exitCode = uninstallResult !== 0 ? uninstallResult : loopResult !== 0 ? loopResult : metricsResult !== 0 ? metricsResult : botResult !== 0 ? botResult : statusLineResult;
|
||
process.exit(exitCode);
|
||
} else {
|
||
console.log(`\n${COLORS.YELLOW}${SYMBOLS.WARNING} CANCELLED${COLORS.RESET}\n`);
|
||
rl.close();
|
||
process.exit(0);
|
||
}
|
||
}
|
||
|
||
// C path — CUSTOM sub-menu
|
||
if (input === 'C') {
|
||
console.log('');
|
||
console.log(`${COLORS.CYAN}╔════════════════════════════════════════════════════════════════╗${COLORS.RESET}`);
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}CUSTOM OPTIONS${COLORS.RESET}`, 65) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(`${COLORS.CYAN}╠════════════════════════════════════════════════════════════════╣${COLORS.RESET}`);
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}L.${COLORS.RESET} ${COLORS.BLUE}LOCAL${COLORS.RESET} Show local (project) install instructions`, 65) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}O.${COLORS.RESET} ${COLORS.GREEN}LOOP CLI${COLORS.RESET} Install plan2code-loop CLI only`, 65) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}M.${COLORS.RESET} ${COLORS.GREEN}METRICS${COLORS.RESET} Install plan2code-metrics CLI only`, 65) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}S.${COLORS.RESET} ${COLORS.GREEN}STATUS${COLORS.RESET} Install Claude Code status line`, 65) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}B.${COLORS.RESET} ${COLORS.GREEN}BOT${COLORS.RESET} Install plan2code-bot CLI only`, 65) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}Q.${COLORS.RESET} ${COLORS.DIM}BACK${COLORS.RESET} Return to main menu`, 65) + `${COLORS.CYAN}║${COLORS.RESET}`);
|
||
console.log(`${COLORS.CYAN}╚════════════════════════════════════════════════════════════════╝${COLORS.RESET}`);
|
||
console.log('');
|
||
const customAnswer = await question(`${COLORS.CYAN}${SYMBOLS.SELECT} SELECT OPTION${COLORS.RESET} (L, O, M, S, B, Q) [Q]: `);
|
||
const customInput = customAnswer.trim().toUpperCase() || 'Q';
|
||
|
||
// C > L — show local install instructions
|
||
if (customInput === 'L') {
|
||
rl.close();
|
||
process.exit(displayLocalInstructions());
|
||
}
|
||
|
||
// C > O — install loop CLI only
|
||
if (customInput === 'O') {
|
||
rl.close();
|
||
const result = installPlan2CodeLoop();
|
||
process.exit(result);
|
||
}
|
||
|
||
// C > M — install metrics CLI only
|
||
if (customInput === 'M') {
|
||
rl.close();
|
||
const result = installPlan2CodeMetrics();
|
||
process.exit(result);
|
||
}
|
||
|
||
// C > S — install status line
|
||
if (customInput === 'S') {
|
||
rl.close();
|
||
const result = await installStatusLine();
|
||
process.exit(result);
|
||
}
|
||
|
||
// C > B — install bot CLI only
|
||
if (customInput === 'B') {
|
||
rl.close();
|
||
const result = installPlan2CodeBot();
|
||
process.exit(result);
|
||
}
|
||
|
||
// C > Q — back to main menu
|
||
return main();
|
||
}
|
||
|
||
// Q — exit
|
||
if (input === 'Q') {
|
||
console.log(`\n${COLORS.YELLOW}${SYMBOLS.WARNING} CANCELLED${COLORS.RESET}\n`);
|
||
rl.close();
|
||
process.exit(0);
|
||
}
|
||
|
||
// Invalid input — loop back
|
||
console.log(`\n${COLORS.RED}${SYMBOLS.ERROR} Invalid option. Please choose I, A, U, C, or Q.${COLORS.RESET}\n`);
|
||
return main();
|
||
}
|
||
|
||
main().catch(err => {
|
||
console.error(`${COLORS.RED}${SYMBOLS.ERROR} ERROR:${COLORS.RESET}`, err.message);
|
||
rl.close();
|
||
process.exit(1);
|
||
});
|
||
}
|
||
|
||
// ============================================================================
|
||
// PLAN2CODE-LOOP INSTALLATION
|
||
// ============================================================================
|
||
|
||
const { execSync } = require('child_process');
|
||
|
||
/**
|
||
* Build plan2code-loop package
|
||
*/
|
||
function buildPlan2CodeLoop() {
|
||
const loopDir = path.join(__dirname, 'plan2code-loop');
|
||
|
||
// Check if directory exists
|
||
if (!fs.existsSync(loopDir)) {
|
||
console.log(`${COLORS.YELLOW}${SYMBOLS.WARNING}${COLORS.RESET} plan2code-loop directory not found`);
|
||
return false;
|
||
}
|
||
|
||
console.log(`${COLORS.CYAN}${SYMBOLS.ACTIVE} Building plan2code-loop...${COLORS.RESET}`);
|
||
|
||
try {
|
||
// Install dependencies
|
||
console.log(` ${COLORS.DIM}Installing dependencies...${COLORS.RESET}`);
|
||
execSync('npm install', { cwd: loopDir, stdio: 'pipe' });
|
||
|
||
// Build
|
||
console.log(` ${COLORS.DIM}Running build...${COLORS.RESET}`);
|
||
execSync('npm run build', { cwd: loopDir, stdio: 'pipe' });
|
||
|
||
console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Build complete`);
|
||
return true;
|
||
} catch (error) {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Build failed: ${error.message}`);
|
||
return false;
|
||
}
|
||
}
|
||
/**
|
||
* Clean up existing plan2code-loop symlinks/files before creating new ones
|
||
* This prevents the Windows lstat error with stale symlinks
|
||
*/
|
||
function cleanupExistingSymlinks() {
|
||
console.log(` ${COLORS.DIM}Cleaning up existing symlinks...${COLORS.RESET}`);
|
||
|
||
try {
|
||
// Get npm global prefix - more reliable than npm bin -g on Windows
|
||
const npmPrefix = execSync('npm config get prefix', { encoding: 'utf8' }).trim();
|
||
|
||
// On Windows, bin files are directly in prefix; on Unix they're in prefix/bin
|
||
const npmBinGlobal = process.platform === 'win32' ? npmPrefix : path.join(npmPrefix, 'bin');
|
||
const npmRootGlobal = path.join(npmPrefix, 'node_modules');
|
||
|
||
// Files/symlinks to clean up in npm bin directory
|
||
const binFiles = [
|
||
path.join(npmBinGlobal, 'plan2code-loop'),
|
||
path.join(npmBinGlobal, 'plan2code-loop.cmd'),
|
||
path.join(npmBinGlobal, 'plan2code-loop.ps1')
|
||
];
|
||
|
||
// Symlink in node_modules - this is the main culprit for the lstat error
|
||
const nodeModulesLink = path.join(npmRootGlobal, 'plan2code-loop');
|
||
|
||
// Remove bin files first
|
||
for (const file of binFiles) {
|
||
try {
|
||
const stats = fs.lstatSync(file);
|
||
if (stats) {
|
||
fs.unlinkSync(file);
|
||
console.log(` ${COLORS.DIM}Removed: ${path.basename(file)}${COLORS.RESET}`);
|
||
}
|
||
} catch (err) {
|
||
if (err.code !== 'ENOENT') {
|
||
// File exists but can't be removed normally, try rmSync
|
||
try {
|
||
fs.rmSync(file, { force: true, recursive: true });
|
||
console.log(` ${COLORS.DIM}Removed: ${path.basename(file)}${COLORS.RESET}`);
|
||
} catch (rmErr) {
|
||
console.log(` ${COLORS.DIM}Could not remove: ${path.basename(file)}${COLORS.RESET}`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Remove node_modules symlink - this is critical for fixing the lstat error
|
||
try {
|
||
const stats = fs.lstatSync(nodeModulesLink);
|
||
if (stats) {
|
||
// On Windows, junction points need special handling
|
||
if (process.platform === 'win32') {
|
||
// Try using rmdir for junction points
|
||
try {
|
||
fs.rmdirSync(nodeModulesLink);
|
||
console.log(` ${COLORS.DIM}Removed: node_modules/plan2code-loop (junction)${COLORS.RESET}`);
|
||
} catch (rmdirErr) {
|
||
// Fall back to rmSync
|
||
fs.rmSync(nodeModulesLink, { force: true, recursive: true });
|
||
console.log(` ${COLORS.DIM}Removed: node_modules/plan2code-loop${COLORS.RESET}`);
|
||
}
|
||
} else {
|
||
fs.rmSync(nodeModulesLink, { force: true, recursive: true });
|
||
console.log(` ${COLORS.DIM}Removed: node_modules/plan2code-loop${COLORS.RESET}`);
|
||
}
|
||
}
|
||
} catch (err) {
|
||
if (err.code !== 'ENOENT') {
|
||
console.log(` ${COLORS.DIM}Could not remove node_modules symlink: ${err.message}${COLORS.RESET}`);
|
||
}
|
||
}
|
||
|
||
return true;
|
||
} catch (error) {
|
||
// Non-fatal - npm link might still work
|
||
console.log(` ${COLORS.DIM}Cleanup skipped: ${error.message}${COLORS.RESET}`);
|
||
return true;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Create global symlink for plan2code-loop
|
||
*/
|
||
function createGlobalSymlink() {
|
||
const loopDir = path.join(__dirname, 'plan2code-loop');
|
||
const distBin = path.join(loopDir, 'dist', 'bin', 'plan2code-loop.js');
|
||
// Check if built binary exists
|
||
if (!fs.existsSync(distBin)) {
|
||
console.log(`${COLORS.YELLOW}${SYMBOLS.WARNING}${COLORS.RESET} plan2code-loop binary not found. Run build first.`);
|
||
return false;
|
||
}
|
||
console.log(`${COLORS.CYAN}${SYMBOLS.ACTIVE} Creating global symlink...${COLORS.RESET}`);
|
||
|
||
// Clean up existing symlinks first to prevent Windows lstat errors
|
||
cleanupExistingSymlinks();
|
||
|
||
// On Windows, skip npm link entirely and create batch file directly
|
||
// npm link has persistent issues with junctions and lstat errors on Windows
|
||
if (process.platform === 'win32') {
|
||
try {
|
||
const npmPrefix = execSync('npm config get prefix', { encoding: 'utf8' }).trim();
|
||
const symlinkCmd = path.join(npmPrefix, 'plan2code-loop.cmd');
|
||
const symlinkPs1 = path.join(npmPrefix, 'plan2code-loop.ps1');
|
||
|
||
// Create batch file wrapper for cmd.exe
|
||
const batchContent = `@echo off\r\nnode "${distBin}" %*\r\n`;
|
||
fs.writeFileSync(symlinkCmd, batchContent);
|
||
console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Created: plan2code-loop.cmd`);
|
||
|
||
// Create PowerShell wrapper for better shell support
|
||
const ps1Content = `#!/usr/bin/env pwsh\r\nnode "${distBin}" $args\r\n`;
|
||
fs.writeFileSync(symlinkPs1, ps1Content);
|
||
console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Created: plan2code-loop.ps1`);
|
||
|
||
console.log(` ${COLORS.DIM}Run 'plan2code-loop --help' from any directory${COLORS.RESET}`);
|
||
return true;
|
||
} catch (error) {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Failed to create wrappers: ${error.message}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// On Unix systems, use npm link as it works reliably
|
||
try {
|
||
execSync('npm link', { cwd: loopDir, stdio: 'pipe' });
|
||
console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Global symlink created`);
|
||
console.log(` ${COLORS.DIM}Run 'plan2code-loop --help' from any directory${COLORS.RESET}`);
|
||
return true;
|
||
} catch (error) {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Symlink failed: ${error.message}`);
|
||
return false;
|
||
}
|
||
}
|
||
/**
|
||
* Remove global symlink for plan2code-loop
|
||
*/
|
||
function removeGlobalSymlink() {
|
||
console.log(`${COLORS.CYAN}${SYMBOLS.ACTIVE} Removing global symlink...${COLORS.RESET}`);
|
||
|
||
let removedCount = 0;
|
||
|
||
try {
|
||
// Get npm global prefix
|
||
const npmPrefix = execSync('npm config get prefix', { encoding: 'utf8' }).trim();
|
||
|
||
// On Windows, bin files are directly in prefix; on Unix they're in prefix/bin
|
||
const npmBinGlobal = process.platform === 'win32' ? npmPrefix : path.join(npmPrefix, 'bin');
|
||
const npmRootGlobal = path.join(npmPrefix, 'node_modules');
|
||
|
||
// Files to remove
|
||
const filesToRemove = [
|
||
{ path: path.join(npmBinGlobal, 'plan2code-loop'), name: 'plan2code-loop' },
|
||
{ path: path.join(npmBinGlobal, 'plan2code-loop.cmd'), name: 'plan2code-loop.cmd' },
|
||
{ path: path.join(npmBinGlobal, 'plan2code-loop.ps1'), name: 'plan2code-loop.ps1' },
|
||
{ path: path.join(npmRootGlobal, 'plan2code-loop'), name: 'node_modules/plan2code-loop' }
|
||
];
|
||
|
||
for (const file of filesToRemove) {
|
||
try {
|
||
fs.lstatSync(file.path);
|
||
fs.rmSync(file.path, { force: true, recursive: true });
|
||
console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Removed: ${file.name}`);
|
||
removedCount++;
|
||
} catch (err) {
|
||
if (err.code !== 'ENOENT') {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Failed to remove ${file.name}: ${err.message}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (removedCount === 0) {
|
||
console.log(` ${COLORS.DIM}${SYMBOLS.INFO} No files found to remove${COLORS.RESET}`);
|
||
}
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Uninstall failed: ${error.message}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Install plan2code-loop (build + symlink)
|
||
*/
|
||
function installPlan2CodeLoop() {
|
||
displaySectionHeader('PLAN2CODE-LOOP', '[ BUILD & INSTALL ]');
|
||
|
||
const buildSuccess = buildPlan2CodeLoop();
|
||
if (!buildSuccess) {
|
||
return 1;
|
||
}
|
||
|
||
const symlinkSuccess = createGlobalSymlink();
|
||
if (!symlinkSuccess) {
|
||
return 1;
|
||
}
|
||
|
||
console.log('');
|
||
console.log(`${COLORS.GREEN}${SYMBOLS.SUCCESS} plan2code-loop installed successfully!${COLORS.RESET}`);
|
||
console.log('');
|
||
console.log(`${COLORS.CYAN}Usage:${COLORS.RESET}`);
|
||
console.log(` ${COLORS.BRIGHT}plan2code-loop${COLORS.RESET} Start the autonomous loop`);
|
||
console.log(` ${COLORS.BRIGHT}plan2code-loop -c${COLORS.RESET} Resume previous session`);
|
||
console.log(` ${COLORS.BRIGHT}plan2code-loop -v${COLORS.RESET} Verbose mode`);
|
||
console.log(` ${COLORS.BRIGHT}plan2code-loop --help${COLORS.RESET} Show all options`);
|
||
console.log('');
|
||
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* Uninstall plan2code-loop
|
||
*/
|
||
function uninstallPlan2CodeLoop() {
|
||
displaySectionHeader('PLAN2CODE-LOOP', '[ UNINSTALL ]');
|
||
|
||
removeGlobalSymlink();
|
||
|
||
console.log('');
|
||
console.log(`${COLORS.GREEN}${SYMBOLS.SUCCESS} plan2code-loop uninstalled${COLORS.RESET}`);
|
||
console.log('');
|
||
|
||
return 0;
|
||
}
|
||
|
||
// ============================================================================
|
||
// PLAN2CODE-METRICS INSTALLATION
|
||
// ============================================================================
|
||
|
||
/**
|
||
* Build plan2code-metrics package
|
||
*/
|
||
function buildPlan2CodeMetrics() {
|
||
const metricsDir = path.join(__dirname, 'plan2code-metrics');
|
||
|
||
if (!fs.existsSync(metricsDir)) {
|
||
console.log(`${COLORS.YELLOW}${SYMBOLS.WARNING}${COLORS.RESET} plan2code-metrics directory not found`);
|
||
return false;
|
||
}
|
||
|
||
console.log(`${COLORS.CYAN}${SYMBOLS.ACTIVE} Building plan2code-metrics...${COLORS.RESET}`);
|
||
|
||
try {
|
||
console.log(` ${COLORS.DIM}Installing dependencies...${COLORS.RESET}`);
|
||
execSync('npm install', { cwd: metricsDir, stdio: 'pipe' });
|
||
|
||
console.log(` ${COLORS.DIM}Running build...${COLORS.RESET}`);
|
||
execSync('npm run build', { cwd: metricsDir, stdio: 'pipe' });
|
||
|
||
console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Build complete`);
|
||
return true;
|
||
} catch (error) {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Build failed: ${error.message}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Clean up existing plan2code-metrics symlinks/files before creating new ones
|
||
*/
|
||
function cleanupExistingMetricsSymlinks() {
|
||
console.log(` ${COLORS.DIM}Cleaning up existing symlinks...${COLORS.RESET}`);
|
||
|
||
try {
|
||
const npmPrefix = execSync('npm config get prefix', { encoding: 'utf8' }).trim();
|
||
const npmBinGlobal = process.platform === 'win32' ? npmPrefix : path.join(npmPrefix, 'bin');
|
||
const npmRootGlobal = path.join(npmPrefix, 'node_modules');
|
||
|
||
const binFiles = [
|
||
path.join(npmBinGlobal, 'plan2code-metrics'),
|
||
path.join(npmBinGlobal, 'plan2code-metrics.cmd'),
|
||
path.join(npmBinGlobal, 'plan2code-metrics.ps1')
|
||
];
|
||
|
||
const nodeModulesLink = path.join(npmRootGlobal, 'plan2code-metrics');
|
||
|
||
for (const file of binFiles) {
|
||
try {
|
||
const stats = fs.lstatSync(file);
|
||
if (stats) {
|
||
fs.unlinkSync(file);
|
||
console.log(` ${COLORS.DIM}Removed: ${path.basename(file)}${COLORS.RESET}`);
|
||
}
|
||
} catch (err) {
|
||
if (err.code !== 'ENOENT') {
|
||
try {
|
||
fs.rmSync(file, { force: true, recursive: true });
|
||
console.log(` ${COLORS.DIM}Removed: ${path.basename(file)}${COLORS.RESET}`);
|
||
} catch (rmErr) {
|
||
console.log(` ${COLORS.DIM}Could not remove: ${path.basename(file)}${COLORS.RESET}`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
try {
|
||
const stats = fs.lstatSync(nodeModulesLink);
|
||
if (stats) {
|
||
if (process.platform === 'win32') {
|
||
try {
|
||
fs.rmdirSync(nodeModulesLink);
|
||
console.log(` ${COLORS.DIM}Removed: node_modules/plan2code-metrics (junction)${COLORS.RESET}`);
|
||
} catch (rmdirErr) {
|
||
fs.rmSync(nodeModulesLink, { force: true, recursive: true });
|
||
console.log(` ${COLORS.DIM}Removed: node_modules/plan2code-metrics${COLORS.RESET}`);
|
||
}
|
||
} else {
|
||
fs.rmSync(nodeModulesLink, { force: true, recursive: true });
|
||
console.log(` ${COLORS.DIM}Removed: node_modules/plan2code-metrics${COLORS.RESET}`);
|
||
}
|
||
}
|
||
} catch (err) {
|
||
if (err.code !== 'ENOENT') {
|
||
console.log(` ${COLORS.DIM}Could not remove node_modules symlink: ${err.message}${COLORS.RESET}`);
|
||
}
|
||
}
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.log(` ${COLORS.DIM}Cleanup skipped: ${error.message}${COLORS.RESET}`);
|
||
return true;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Create global symlink for plan2code-metrics
|
||
*/
|
||
function createMetricsGlobalSymlink() {
|
||
const metricsDir = path.join(__dirname, 'plan2code-metrics');
|
||
const distBin = path.join(metricsDir, 'dist', 'bin', 'plan2code-metrics.js');
|
||
|
||
if (!fs.existsSync(distBin)) {
|
||
console.log(`${COLORS.YELLOW}${SYMBOLS.WARNING}${COLORS.RESET} plan2code-metrics binary not found. Run build first.`);
|
||
return false;
|
||
}
|
||
|
||
console.log(`${COLORS.CYAN}${SYMBOLS.ACTIVE} Creating global symlink...${COLORS.RESET}`);
|
||
|
||
cleanupExistingMetricsSymlinks();
|
||
|
||
if (process.platform === 'win32') {
|
||
try {
|
||
const npmPrefix = execSync('npm config get prefix', { encoding: 'utf8' }).trim();
|
||
const symlinkCmd = path.join(npmPrefix, 'plan2code-metrics.cmd');
|
||
const symlinkPs1 = path.join(npmPrefix, 'plan2code-metrics.ps1');
|
||
|
||
const batchContent = `@echo off\r\nnode "${distBin}" %*\r\n`;
|
||
fs.writeFileSync(symlinkCmd, batchContent);
|
||
console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Created: plan2code-metrics.cmd`);
|
||
|
||
const ps1Content = `#!/usr/bin/env pwsh\r\nnode "${distBin}" $args\r\n`;
|
||
fs.writeFileSync(symlinkPs1, ps1Content);
|
||
console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Created: plan2code-metrics.ps1`);
|
||
|
||
console.log(` ${COLORS.DIM}Run 'plan2code-metrics' from any directory${COLORS.RESET}`);
|
||
return true;
|
||
} catch (error) {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Failed to create wrappers: ${error.message}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
try {
|
||
execSync('npm link', { cwd: metricsDir, stdio: 'pipe' });
|
||
console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Global symlink created`);
|
||
console.log(` ${COLORS.DIM}Run 'plan2code-metrics' from any directory${COLORS.RESET}`);
|
||
return true;
|
||
} catch (error) {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Symlink failed: ${error.message}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Remove global symlink for plan2code-metrics
|
||
*/
|
||
function removeMetricsGlobalSymlink() {
|
||
console.log(`${COLORS.CYAN}${SYMBOLS.ACTIVE} Removing global symlink...${COLORS.RESET}`);
|
||
|
||
let removedCount = 0;
|
||
|
||
try {
|
||
const npmPrefix = execSync('npm config get prefix', { encoding: 'utf8' }).trim();
|
||
const npmBinGlobal = process.platform === 'win32' ? npmPrefix : path.join(npmPrefix, 'bin');
|
||
const npmRootGlobal = path.join(npmPrefix, 'node_modules');
|
||
|
||
const filesToRemove = [
|
||
{ path: path.join(npmBinGlobal, 'plan2code-metrics'), name: 'plan2code-metrics' },
|
||
{ path: path.join(npmBinGlobal, 'plan2code-metrics.cmd'), name: 'plan2code-metrics.cmd' },
|
||
{ path: path.join(npmBinGlobal, 'plan2code-metrics.ps1'), name: 'plan2code-metrics.ps1' },
|
||
{ path: path.join(npmRootGlobal, 'plan2code-metrics'), name: 'node_modules/plan2code-metrics' }
|
||
];
|
||
|
||
for (const file of filesToRemove) {
|
||
try {
|
||
fs.lstatSync(file.path);
|
||
fs.rmSync(file.path, { force: true, recursive: true });
|
||
console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Removed: ${file.name}`);
|
||
removedCount++;
|
||
} catch (err) {
|
||
if (err.code !== 'ENOENT') {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Failed to remove ${file.name}: ${err.message}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (removedCount === 0) {
|
||
console.log(` ${COLORS.DIM}${SYMBOLS.INFO} No files found to remove${COLORS.RESET}`);
|
||
}
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Uninstall failed: ${error.message}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Install plan2code-metrics (build + symlink)
|
||
*/
|
||
function installPlan2CodeMetrics() {
|
||
displaySectionHeader('PLAN2CODE-METRICS', '[ BUILD & INSTALL ]');
|
||
|
||
const buildSuccess = buildPlan2CodeMetrics();
|
||
if (!buildSuccess) {
|
||
return 1;
|
||
}
|
||
|
||
const symlinkSuccess = createMetricsGlobalSymlink();
|
||
if (!symlinkSuccess) {
|
||
return 1;
|
||
}
|
||
|
||
console.log('');
|
||
console.log(`${COLORS.GREEN}${SYMBOLS.SUCCESS} plan2code-metrics installed successfully!${COLORS.RESET}`);
|
||
console.log('');
|
||
console.log(`${COLORS.CYAN}Usage:${COLORS.RESET}`);
|
||
console.log(` ${COLORS.BRIGHT}plan2code-metrics${COLORS.RESET} Start the interactive metrics CLI`);
|
||
console.log('');
|
||
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* Uninstall plan2code-metrics
|
||
*/
|
||
function uninstallPlan2CodeMetrics() {
|
||
displaySectionHeader('PLAN2CODE-METRICS', '[ UNINSTALL ]');
|
||
|
||
removeMetricsGlobalSymlink();
|
||
|
||
console.log('');
|
||
console.log(`${COLORS.GREEN}${SYMBOLS.SUCCESS} plan2code-metrics uninstalled${COLORS.RESET}`);
|
||
console.log('');
|
||
|
||
return 0;
|
||
}
|
||
|
||
// ============================================================================
|
||
// STATUS LINE INSTALLATION
|
||
// ============================================================================
|
||
|
||
/**
|
||
* Status line source layout — single-file design, no bundling step.
|
||
* `statusline.js` is self-contained (config loader, formatters, entry point)
|
||
* and copied verbatim to ~/.claude/plan2code-statusline.js on install.
|
||
*/
|
||
function getStatusLineSrcDir() {
|
||
return path.join(__dirname, 'src', 'statusline-claude');
|
||
}
|
||
|
||
/**
|
||
* Install status line (copy + configure settings).
|
||
* Async because it may prompt when a custom (non-plan2code) statusLine exists.
|
||
*/
|
||
async function installStatusLine({ quiet = false } = {}) {
|
||
if (!quiet) {
|
||
displaySectionHeader('CLAUDE STATUS LINE', '[ INSTALL ]');
|
||
} else {
|
||
console.log(` ${COLORS.CYAN}${SYMBOLS.ACTIVE} INSTALLING${COLORS.RESET} Claude Status Line...`);
|
||
}
|
||
|
||
const srcDir = getStatusLineSrcDir();
|
||
if (!fs.existsSync(srcDir)) {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} src/statusline-claude directory not found`);
|
||
return 1;
|
||
}
|
||
|
||
const homeDir = os.homedir();
|
||
const claudeDir = path.join(homeDir, '.claude');
|
||
|
||
try {
|
||
fs.mkdirSync(claudeDir, { recursive: true });
|
||
} catch (error) {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Cannot create ~/.claude/: ${error.message}`);
|
||
return 1;
|
||
}
|
||
|
||
// Copy script directly from src (no bundling — statusline.js is self-contained)
|
||
const statuslineDest = path.join(claudeDir, 'plan2code-statusline.js');
|
||
const configDest = path.join(claudeDir, 'statusline-config.json');
|
||
|
||
try {
|
||
fs.copyFileSync(path.join(srcDir, 'statusline.js'), statuslineDest);
|
||
if (!quiet) console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Installed: ~/.claude/plan2code-statusline.js`);
|
||
} catch (error) {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Failed to copy script: ${error.message}`);
|
||
return 1;
|
||
}
|
||
|
||
// Preserve existing user config
|
||
if (!fs.existsSync(configDest)) {
|
||
try {
|
||
fs.copyFileSync(path.join(srcDir, 'statusline-config.json'), configDest);
|
||
if (!quiet) console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Created: ~/.claude/statusline-config.json`);
|
||
} catch (error) {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Failed to copy config: ${error.message}`);
|
||
return 1;
|
||
}
|
||
} else {
|
||
if (!quiet) console.log(` ${COLORS.DIM}${SYMBOLS.INFO} Preserved existing: ~/.claude/statusline-config.json${COLORS.RESET}`);
|
||
}
|
||
|
||
// Make executable on non-Windows
|
||
if (process.platform !== 'win32') {
|
||
try {
|
||
fs.chmodSync(statuslineDest, 0o755);
|
||
} catch {}
|
||
}
|
||
|
||
// Configure Claude Code settings.json
|
||
let settingsOk = false;
|
||
const settingsPath = path.join(claudeDir, 'settings.json');
|
||
const expectedCommand = `node "${statuslineDest.replace(/\\/g, '/')}"`;
|
||
|
||
try {
|
||
let settings = {};
|
||
if (fs.existsSync(settingsPath)) {
|
||
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||
}
|
||
|
||
// Detect existing statusLine — skip prompt if ours or absent
|
||
if (settings.statusLine && settings.statusLine.command) {
|
||
const existingCmd = settings.statusLine.command;
|
||
const isPlan2Code = existingCmd.includes('plan2code-statusline');
|
||
|
||
if (!isPlan2Code) {
|
||
// Custom statusLine detected — prompt before overwriting
|
||
console.log(` ${COLORS.YELLOW}${SYMBOLS.WARNING}${COLORS.RESET} Existing statusLine detected in settings.json:`);
|
||
console.log(` ${COLORS.DIM} ${existingCmd}${COLORS.RESET}`);
|
||
|
||
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||
const answer = await new Promise(resolve => {
|
||
rl2.question(` ${COLORS.CYAN}Replace with Plan2Code status line?${COLORS.RESET} (Y/n): `, resolve);
|
||
});
|
||
rl2.close();
|
||
|
||
if (answer.trim().toLowerCase() === 'n') {
|
||
console.log(` ${COLORS.DIM}${SYMBOLS.INFO} Skipped — scripts installed but settings.json unchanged${COLORS.RESET}`);
|
||
console.log('');
|
||
return 0;
|
||
}
|
||
|
||
// Backup existing config before replacing
|
||
const backupPath = path.join(claudeDir, 'statusline-previous.json');
|
||
fs.writeFileSync(backupPath, JSON.stringify({ statusLine: settings.statusLine }, null, 2));
|
||
console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Backed up existing config: ~/.claude/statusline-previous.json`);
|
||
}
|
||
}
|
||
|
||
settings.statusLine = {
|
||
type: 'command',
|
||
command: expectedCommand,
|
||
};
|
||
// Atomic write: temp file + rename so a crash/power-loss never leaves
|
||
// settings.json truncated.
|
||
const tmpSettings = settingsPath + '.tmp';
|
||
fs.writeFileSync(tmpSettings, JSON.stringify(settings, null, 2));
|
||
fs.renameSync(tmpSettings, settingsPath);
|
||
if (!quiet) console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Configured: ~/.claude/settings.json (statusLine)`);
|
||
settingsOk = true;
|
||
} catch (error) {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Failed to update settings.json: ${error.message}`);
|
||
}
|
||
|
||
// Clean up legacy filenames from previous installs
|
||
for (const legacy of ['statusline.js', 'plan-usage.js', 'plan2code-plan-usage.js', 'statusline-cache.json']) {
|
||
const legacyPath = path.join(claudeDir, legacy);
|
||
try {
|
||
if (fs.existsSync(legacyPath)) {
|
||
fs.unlinkSync(legacyPath);
|
||
if (!quiet) console.log(` ${COLORS.DIM}${SYMBOLS.INFO} Removed legacy: ${legacy}${COLORS.RESET}`);
|
||
}
|
||
} catch {}
|
||
}
|
||
|
||
if (settingsOk) {
|
||
if (!quiet) {
|
||
console.log('');
|
||
console.log(`${COLORS.GREEN}${SYMBOLS.SUCCESS} Claude Status Line installed successfully!${COLORS.RESET}`);
|
||
console.log('');
|
||
console.log(`${COLORS.CYAN}Usage:${COLORS.RESET}`);
|
||
console.log(` ${COLORS.DIM}Restart Claude Code to see the status bar${COLORS.RESET}`);
|
||
console.log(` ${COLORS.DIM}Config: ~/.claude/statusline-config.json${COLORS.RESET}`);
|
||
console.log('');
|
||
} else {
|
||
console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Claude Status Line → ${COLORS.DIM}~/.claude/plan2code-statusline.js${COLORS.RESET}`);
|
||
}
|
||
} else {
|
||
console.log('');
|
||
console.log(` ${COLORS.YELLOW}${SYMBOLS.WARNING}${COLORS.RESET} Scripts installed but settings.json not updated.`);
|
||
console.log(` ${COLORS.DIM}Add statusLine config to ~/.claude/settings.json manually.${COLORS.RESET}`);
|
||
console.log('');
|
||
}
|
||
|
||
return settingsOk ? 0 : 1;
|
||
}
|
||
|
||
/**
|
||
* Uninstall status line
|
||
*/
|
||
function uninstallStatusLine() {
|
||
displaySectionHeader('CLAUDE STATUS LINE', '[ UNINSTALL ]');
|
||
|
||
const homeDir = os.homedir();
|
||
const claudeDir = path.join(homeDir, '.claude');
|
||
const settingsPath = path.join(claudeDir, 'settings.json');
|
||
|
||
// Remove statusLine from settings.json (only if it points to our bundle)
|
||
try {
|
||
if (fs.existsSync(settingsPath)) {
|
||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||
if (settings.statusLine) {
|
||
const existingCmd = settings.statusLine.command || '';
|
||
if (existingCmd.includes('plan2code-statusline')) {
|
||
delete settings.statusLine;
|
||
const tmpSettings = settingsPath + '.tmp';
|
||
fs.writeFileSync(tmpSettings, JSON.stringify(settings, null, 2));
|
||
fs.renameSync(tmpSettings, settingsPath);
|
||
console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Removed statusLine from settings.json`);
|
||
} else {
|
||
console.log(` ${COLORS.DIM}${SYMBOLS.INFO} Preserved non-plan2code statusLine in settings.json${COLORS.RESET}`);
|
||
console.log(` ${COLORS.DIM} ${existingCmd}${COLORS.RESET}`);
|
||
}
|
||
} else {
|
||
console.log(` ${COLORS.DIM}${SYMBOLS.INFO} No statusLine config in settings.json${COLORS.RESET}`);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Failed to update settings.json: ${error.message}`);
|
||
}
|
||
|
||
// Remove script files (preserve config)
|
||
const filesToRemove = [
|
||
{ path: path.join(claudeDir, 'plan2code-statusline.js'), name: 'plan2code-statusline.js' },
|
||
{ path: path.join(claudeDir, 'plan2code-plan-usage.js'), name: 'plan2code-plan-usage.js' },
|
||
{ path: path.join(claudeDir, 'statusline.js'), name: 'statusline.js (legacy)' },
|
||
{ path: path.join(claudeDir, 'plan-usage.js'), name: 'plan-usage.js (legacy)' },
|
||
{ path: path.join(claudeDir, 'statusline-cache.json'), name: 'statusline-cache.json' },
|
||
];
|
||
|
||
let removedCount = 0;
|
||
for (const file of filesToRemove) {
|
||
try {
|
||
if (fs.existsSync(file.path)) {
|
||
fs.unlinkSync(file.path);
|
||
console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Removed: ${file.name}`);
|
||
removedCount++;
|
||
}
|
||
} catch (error) {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Failed to remove ${file.name}: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
if (removedCount === 0) {
|
||
console.log(` ${COLORS.DIM}${SYMBOLS.INFO} No status line files found${COLORS.RESET}`);
|
||
}
|
||
|
||
console.log(` ${COLORS.DIM}${SYMBOLS.INFO} Preserved: statusline-config.json${COLORS.RESET}`);
|
||
console.log('');
|
||
console.log(`${COLORS.GREEN}${SYMBOLS.SUCCESS} Status line uninstalled${COLORS.RESET}`);
|
||
console.log('');
|
||
|
||
return 0;
|
||
}
|
||
|
||
// ============================================================================
|
||
// PLAN2CODE-BOT INSTALLATION
|
||
// ============================================================================
|
||
|
||
/**
|
||
* Build plan2code-bot package
|
||
*/
|
||
function buildPlan2CodeBot() {
|
||
const botDir = path.join(__dirname, 'plan2code-bot');
|
||
|
||
if (!fs.existsSync(botDir)) {
|
||
console.log(`${COLORS.YELLOW}${SYMBOLS.WARNING}${COLORS.RESET} plan2code-bot directory not found`);
|
||
return false;
|
||
}
|
||
|
||
console.log(`${COLORS.CYAN}${SYMBOLS.ACTIVE} Building plan2code-bot...${COLORS.RESET}`);
|
||
|
||
try {
|
||
console.log(` ${COLORS.DIM}Installing dependencies...${COLORS.RESET}`);
|
||
execSync('npm install', { cwd: botDir, stdio: 'pipe' });
|
||
|
||
console.log(` ${COLORS.DIM}Running build...${COLORS.RESET}`);
|
||
execSync('npm run build', { cwd: botDir, stdio: 'pipe' });
|
||
|
||
console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Build complete`);
|
||
return true;
|
||
} catch (error) {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Build failed: ${error.message}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Clean up existing plan2code-bot symlinks/files before creating new ones
|
||
*/
|
||
function cleanupExistingBotSymlinks() {
|
||
console.log(` ${COLORS.DIM}Cleaning up existing symlinks...${COLORS.RESET}`);
|
||
|
||
try {
|
||
const npmPrefix = execSync('npm config get prefix', { encoding: 'utf8' }).trim();
|
||
const npmBinGlobal = process.platform === 'win32' ? npmPrefix : path.join(npmPrefix, 'bin');
|
||
const npmRootGlobal = path.join(npmPrefix, 'node_modules');
|
||
|
||
const binFiles = [
|
||
path.join(npmBinGlobal, 'plan2code-bot'),
|
||
path.join(npmBinGlobal, 'plan2code-bot.cmd'),
|
||
path.join(npmBinGlobal, 'plan2code-bot.ps1')
|
||
];
|
||
|
||
const nodeModulesLink = path.join(npmRootGlobal, 'plan2code-bot');
|
||
|
||
for (const file of binFiles) {
|
||
try {
|
||
const stats = fs.lstatSync(file);
|
||
if (stats) {
|
||
fs.unlinkSync(file);
|
||
console.log(` ${COLORS.DIM}Removed: ${path.basename(file)}${COLORS.RESET}`);
|
||
}
|
||
} catch (err) {
|
||
if (err.code !== 'ENOENT') {
|
||
try {
|
||
fs.rmSync(file, { force: true, recursive: true });
|
||
console.log(` ${COLORS.DIM}Removed: ${path.basename(file)}${COLORS.RESET}`);
|
||
} catch (rmErr) {
|
||
console.log(` ${COLORS.DIM}Could not remove: ${path.basename(file)}${COLORS.RESET}`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
try {
|
||
const stats = fs.lstatSync(nodeModulesLink);
|
||
if (stats) {
|
||
if (process.platform === 'win32') {
|
||
try {
|
||
fs.rmdirSync(nodeModulesLink);
|
||
console.log(` ${COLORS.DIM}Removed: node_modules/plan2code-bot (junction)${COLORS.RESET}`);
|
||
} catch (rmdirErr) {
|
||
fs.rmSync(nodeModulesLink, { force: true, recursive: true });
|
||
console.log(` ${COLORS.DIM}Removed: node_modules/plan2code-bot${COLORS.RESET}`);
|
||
}
|
||
} else {
|
||
fs.rmSync(nodeModulesLink, { force: true, recursive: true });
|
||
console.log(` ${COLORS.DIM}Removed: node_modules/plan2code-bot${COLORS.RESET}`);
|
||
}
|
||
}
|
||
} catch (err) {
|
||
if (err.code !== 'ENOENT') {
|
||
console.log(` ${COLORS.DIM}Could not remove node_modules symlink: ${err.message}${COLORS.RESET}`);
|
||
}
|
||
}
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.log(` ${COLORS.DIM}Cleanup skipped: ${error.message}${COLORS.RESET}`);
|
||
return true;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Create global symlink for plan2code-bot
|
||
*/
|
||
function createBotGlobalSymlink() {
|
||
const botDir = path.join(__dirname, 'plan2code-bot');
|
||
const distBin = path.join(botDir, 'dist', 'bin', 'plan2code-bot.js');
|
||
|
||
if (!fs.existsSync(distBin)) {
|
||
console.log(`${COLORS.YELLOW}${SYMBOLS.WARNING}${COLORS.RESET} plan2code-bot binary not found. Run build first.`);
|
||
return false;
|
||
}
|
||
|
||
console.log(`${COLORS.CYAN}${SYMBOLS.ACTIVE} Creating global symlink...${COLORS.RESET}`);
|
||
|
||
cleanupExistingBotSymlinks();
|
||
|
||
if (process.platform === 'win32') {
|
||
try {
|
||
const npmPrefix = execSync('npm config get prefix', { encoding: 'utf8' }).trim();
|
||
const symlinkCmd = path.join(npmPrefix, 'plan2code-bot.cmd');
|
||
const symlinkPs1 = path.join(npmPrefix, 'plan2code-bot.ps1');
|
||
|
||
const batchContent = `@echo off\r\nnode "${distBin}" %*\r\n`;
|
||
fs.writeFileSync(symlinkCmd, batchContent);
|
||
console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Created: plan2code-bot.cmd`);
|
||
|
||
const ps1Content = `#!/usr/bin/env pwsh\r\nnode "${distBin}" $args\r\n`;
|
||
fs.writeFileSync(symlinkPs1, ps1Content);
|
||
console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Created: plan2code-bot.ps1`);
|
||
|
||
console.log(` ${COLORS.DIM}Run 'plan2code-bot' from any directory${COLORS.RESET}`);
|
||
return true;
|
||
} catch (error) {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Failed to create wrappers: ${error.message}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
try {
|
||
execSync('npm link', { cwd: botDir, stdio: 'pipe' });
|
||
console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Global symlink created`);
|
||
console.log(` ${COLORS.DIM}Run 'plan2code-bot' from any directory${COLORS.RESET}`);
|
||
return true;
|
||
} catch (error) {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Symlink failed: ${error.message}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Remove global symlink for plan2code-bot
|
||
*/
|
||
function removeBotGlobalSymlink() {
|
||
console.log(`${COLORS.CYAN}${SYMBOLS.ACTIVE} Removing global symlink...${COLORS.RESET}`);
|
||
|
||
let removedCount = 0;
|
||
|
||
try {
|
||
const npmPrefix = execSync('npm config get prefix', { encoding: 'utf8' }).trim();
|
||
const npmBinGlobal = process.platform === 'win32' ? npmPrefix : path.join(npmPrefix, 'bin');
|
||
const npmRootGlobal = path.join(npmPrefix, 'node_modules');
|
||
|
||
const filesToRemove = [
|
||
{ path: path.join(npmBinGlobal, 'plan2code-bot'), name: 'plan2code-bot' },
|
||
{ path: path.join(npmBinGlobal, 'plan2code-bot.cmd'), name: 'plan2code-bot.cmd' },
|
||
{ path: path.join(npmBinGlobal, 'plan2code-bot.ps1'), name: 'plan2code-bot.ps1' },
|
||
{ path: path.join(npmRootGlobal, 'plan2code-bot'), name: 'node_modules/plan2code-bot' }
|
||
];
|
||
|
||
for (const file of filesToRemove) {
|
||
try {
|
||
fs.lstatSync(file.path);
|
||
fs.rmSync(file.path, { force: true, recursive: true });
|
||
console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Removed: ${file.name}`);
|
||
removedCount++;
|
||
} catch (err) {
|
||
if (err.code !== 'ENOENT') {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Failed to remove ${file.name}: ${err.message}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (removedCount === 0) {
|
||
console.log(` ${COLORS.DIM}${SYMBOLS.INFO} No files found to remove${COLORS.RESET}`);
|
||
}
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Uninstall failed: ${error.message}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Install plan2code-bot (build + symlink)
|
||
*/
|
||
function installPlan2CodeBot() {
|
||
displaySectionHeader('PLAN2CODE-BOT', '[ BUILD & INSTALL ]');
|
||
|
||
const buildSuccess = buildPlan2CodeBot();
|
||
if (!buildSuccess) {
|
||
return 1;
|
||
}
|
||
|
||
const symlinkSuccess = createBotGlobalSymlink();
|
||
if (!symlinkSuccess) {
|
||
return 1;
|
||
}
|
||
|
||
console.log('');
|
||
console.log(`${COLORS.GREEN}${SYMBOLS.SUCCESS} plan2code-bot installed successfully!${COLORS.RESET}`);
|
||
console.log('');
|
||
console.log(`${COLORS.CYAN}Usage:${COLORS.RESET}`);
|
||
console.log(` ${COLORS.BRIGHT}plan2code-bot${COLORS.RESET} Run autonomous workflow test`);
|
||
console.log('');
|
||
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* Uninstall plan2code-bot
|
||
*/
|
||
function uninstallPlan2CodeBot() {
|
||
displaySectionHeader('PLAN2CODE-BOT', '[ UNINSTALL ]');
|
||
|
||
removeBotGlobalSymlink();
|
||
|
||
console.log('');
|
||
console.log(`${COLORS.GREEN}${SYMBOLS.SUCCESS} plan2code-bot uninstalled${COLORS.RESET}`);
|
||
console.log('');
|
||
|
||
return 0;
|
||
}
|
||
|
||
// ============================================================================
|
||
// MAIN
|
||
// ============================================================================
|
||
|
||
// Always run interactive menu
|
||
runInteractive();
|