mirror of
https://github.com/jparkerweb/plan2code.git
synced 2026-09-17 16:22:23 -07:00
09aa565559
Removes the .gemini/commands/*.toml surface and the build-time inlining it required; every remaining target resolves reference directives at runtime. The uninstall entry is retained behind an uninstallOnly flag so .toml files from earlier versions can still be cleaned up. AI Assisted
2581 lines
99 KiB
JavaScript
2581 lines
99 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-0-pathfinder.md',
|
||
stepNumber: '0',
|
||
name: 'pathfinder',
|
||
displayName: 'Pathfinder Mode',
|
||
description: 'charting of a foggy idea as a map of decision questions, cleared one at a time'
|
||
},
|
||
{
|
||
source: 'plan2code-quick-task.md',
|
||
stepNumber: 'quick',
|
||
name: 'quick-task',
|
||
displayName: 'Quick Task Mode',
|
||
description: 'Lightweight planning for small tasks',
|
||
isUtility: true
|
||
},
|
||
{
|
||
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';
|
||
if (prompt.stepNumber === 'quick') return 'Quick Task';
|
||
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');
|
||
}
|
||
|
||
|
||
// 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 · OpenCode)',
|
||
dir: '.agents/skills',
|
||
type: 'skill',
|
||
header: (prompt) => generateSkillHeader(prompt, false),
|
||
}
|
||
];
|
||
|
||
// 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 · OpenCode · Zed)',
|
||
dir: '.agents/skills',
|
||
type: 'skill',
|
||
header: (prompt) => generateSkillHeader(prompt, true),
|
||
},
|
||
{
|
||
name: 'Crush',
|
||
dir: 'crush-skills',
|
||
type: 'skill',
|
||
header: (prompt) => generateSkillHeader(prompt, false),
|
||
},
|
||
];
|
||
|
||
// 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 · 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: '◉ '
|
||
},
|
||
// Gemini CLI is no longer an install target. This entry is RETAINED FOR UNINSTALL
|
||
// ONLY so `.gemini/commands/plan2code-*.toml` files written by earlier versions can
|
||
// still be cleaned up. Do not add a matching entry back to LOCAL_DESTINATIONS /
|
||
// GLOBAL_DESTINATIONS.
|
||
{
|
||
name: 'Gemini CLI (legacy — uninstall only)',
|
||
id: 'gemini',
|
||
dir: '.gemini/commands',
|
||
type: 'toml',
|
||
filePattern: /^plan2code-.*\.toml$/,
|
||
uninstallOnly: true,
|
||
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://github.com/jparkerweb/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++;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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 {
|
||
// 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, { includeLegacy = false } = {}) {
|
||
// Entries flagged `uninstallOnly` are platforms we no longer install to, kept so their
|
||
// files from earlier versions can still be cleaned up. Only uninstall may see them.
|
||
const pool = includeLegacy
|
||
? INSTALL_TARGETS
|
||
: INSTALL_TARGETS.filter(t => !t.uninstallOnly);
|
||
if (platformIds && platformIds.length > 0) {
|
||
return pool.filter(t => platformIds.includes(t.id));
|
||
}
|
||
return pool;
|
||
}
|
||
|
||
// ============================================================================
|
||
// 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();
|
||
// includeLegacy: uninstall must also clear platforms we no longer install to
|
||
const targetList = targets || getTargets(null, { includeLegacy: true });
|
||
|
||
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() {
|
||
// 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();
|