2025-12-23 15:42:35 -08:00
#!/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/.
2026-02-22 12:54:33 -08:00
* Always runs interactively — any CLI arguments are silently ignored.
2025-12-23 15:42:35 -08:00
*/
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 : '╣' ,
} ;
2026-01-27 12:11:00 -08:00
// Plan2Code Mascot - appears during user interactions
const MASCOT = {
// Full mascot for headers
full : [
' ╭───╮ ' ,
' │ ● │ ' ,
' │ ◡ │ ' ,
' ╰───╯ ' ,
] ,
// Mini mascot for inline use
mini : '(◉‿◉)' ,
// Waving mascot for greetings
wave : [
' ╭───╮ ' ,
2026-02-22 21:48:13 -08:00
' │ ● │ /' ,
2026-01-27 12:11:00 -08:00
' │ ◡ │ ' ,
' ╰───╯ ' ,
] ,
// Thinking mascot for prompts
thinking : [
' ╭───╮ ' ,
2026-02-22 21:48:13 -08:00
' │ ● │ ?' ,
2026-01-27 12:11:00 -08:00
' │ ~ │ ' ,
' ╰───╯ ' ,
] ,
} ;
2026-02-18 15:16:11 -08:00
2025-12-23 15:42:35 -08:00
// 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 = [
{
2026-05-23 06:58:20 -07:00
source : 'plan2code-init.md' ,
2025-12-23 15:42:35 -08:00
stepNumber : 'init' ,
name : 'init' ,
displayName : 'Init Mode' ,
description : 'Generate AGENTS.md file for project-specific guidance' ,
isUtility : true
} ,
{
2026-05-23 06:58:20 -07:00
source : 'plan2code-init-update.md' ,
2025-12-23 15:42:35 -08:00
stepNumber : 'update' ,
name : 'init-update' ,
displayName : 'Init Update Mode' ,
description : 'Update existing AGENTS.md with new learnings' ,
isUtility : true
} ,
{
2026-05-23 06:58:20 -07:00
source : 'plan2code-quick-task.md' ,
2025-12-23 15:42:35 -08:00
stepNumber : 0 ,
name : 'quick-task' ,
displayName : 'Quick Task Mode' ,
description : 'Lightweight planning for small tasks'
} ,
{
2026-05-23 06:58:20 -07:00
source : 'plan2code-1-plan.md' ,
2025-12-23 15:42:35 -08:00
stepNumber : 1 ,
name : 'plan' ,
displayName : 'Planning Mode' ,
description : 'Requirements analysis and architecture design'
} ,
{
2026-05-23 06:58:20 -07:00
source : 'plan2code-1b-revise-plan.md' ,
2025-12-23 15:42:35 -08:00
stepNumber : '1b' ,
name : 'revise-plan' ,
displayName : 'Revision Mode' ,
description : 'Modify specs mid-implementation when requirements change'
} ,
{
2026-05-23 06:58:20 -07:00
source : 'plan2code-2-document.md' ,
2025-12-23 15:42:35 -08:00
stepNumber : 2 ,
name : 'document' ,
displayName : 'Documentation Mode' ,
description : 'Transform planning output into structured implementation docs'
} ,
{
2026-05-23 06:58:20 -07:00
source : 'plan2code-3-implement.md' ,
2025-12-23 15:42:35 -08:00
stepNumber : 3 ,
name : 'implement' ,
displayName : 'Implementation Mode' ,
description : 'Execute implementation phase by phase'
} ,
{
2026-06-01 16:39:35 -07:00
source : 'plan2code-review.md' ,
stepNumber : 'review' ,
2026-05-23 06:58:20 -07:00
name : 'review' ,
displayName : 'Review Mode' ,
2026-06-01 16:39:35 -07:00
description : 'Comprehensive post-implementation review with spec compliance checking' ,
isUtility : true
2026-05-23 06:58:20 -07:00
} ,
{
source : 'plan2code-4-finalize.md' ,
2025-12-23 15:42:35 -08:00
stepNumber : 4 ,
name : 'finalize' ,
displayName : 'Finalization Mode' ,
description : 'Validate, summarize, and archive completed work'
}
] ;
// Helper function to generate destination filename based on stepNumber
function generateFilename ( prompt , extension = '.md' ) {
if ( prompt . isUtility || prompt . stepNumber === 0 || prompt . stepNumber === 'init' ) {
2026-05-23 06:58:20 -07:00
return ` plan2code- ${ prompt . name } ${ extension } ` ;
2025-12-23 15:42:35 -08:00
}
2026-05-23 06:58:20 -07:00
return ` plan2code- ${ prompt . stepNumber } - ${ prompt . name } ${ extension } ` ;
2025-12-23 15:42:35 -08:00
}
2026-02-22 12:54:33 -08:00
// Helper function to generate skill name (normalizes double hyphens to single)
function generateSkillName ( prompt ) {
return generateFilename ( prompt , '' ) . replace ( /--+/g , '-' ) ;
}
2026-06-01 16:39:35 -07:00
// Helper function to generate step label for descriptions
function generateStepLabel ( prompt ) {
if ( prompt . stepNumber === 'init' ) return 'Init' ;
if ( prompt . stepNumber === 'review' ) return 'Review' ;
return ` Step ${ prompt . stepNumber } ` ;
}
2026-02-22 12:54:33 -08:00
// Helper function to generate YAML frontmatter for SKILL.md files
function generateSkillHeader ( prompt , disableModelInvocation = false ) {
const lines = [
'---' ,
` name: ${ generateSkillName ( prompt ) } ` ,
2026-06-01 16:39:35 -07:00
` description: "Plan2Code ${ generateStepLabel ( prompt ) } : ${ prompt . displayName } - user-initiated workflow step. Do not invoke autonomously." ` ,
2026-02-22 12:54:33 -08:00
] ;
if ( disableModelInvocation ) lines . push ( 'disable-model-invocation: true' ) ;
lines . push ( '---' ) ;
return lines . join ( '\n' ) ;
}
// Helper function to generate complete .toml file content for Gemini CLI
function generateTomlContent ( prompt , sourceContent ) {
2026-06-01 16:39:35 -07:00
const stepLabel = generateStepLabel ( prompt ) ;
2026-02-22 12:54:33 -08:00
const desc = ` Plan2Code ${ stepLabel } : ${ prompt . displayName } - user-initiated ${ prompt . description || 'workflow step' } ` ;
return ` description = " ${ desc } " \n prompt = ''' \n ${ sourceContent } \n ''' \n ` ;
}
2025-12-23 15:42:35 -08:00
// 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' ,
2026-06-01 16:39:35 -07:00
` description: "Plan2Code ${ generateStepLabel ( prompt ) } : ${ prompt . displayName } - ${ prompt . description } " ` ,
2025-12-23 15:42:35 -08:00
'---'
] . join ( '\n' )
} ,
{
name : 'GitHub Copilot Agents' ,
dir : '.github/agents' ,
filePattern : ( prompt ) => generateFilename ( prompt , '.agent.md' ) ,
header : ( prompt ) => [
'---' ,
2026-06-01 16:39:35 -07:00
` description: "Plan2Code ${ generateStepLabel ( prompt ) } : ${ prompt . displayName } - ${ prompt . description } " ` ,
2025-12-23 15:42:35 -08:00
'---'
] . join ( '\n' )
} ,
{
name : 'Continue Prompts' ,
dir : '.continue/prompts' ,
filePattern : ( prompt ) => generateFilename ( prompt , '.prompt.md' ) ,
header : ( prompt ) => [
'---' ,
` name: ${ prompt . name } ` ,
2026-06-01 16:39:35 -07:00
` description: "Plan2Code ${ generateStepLabel ( prompt ) } : ${ prompt . displayName } - ${ prompt . description } " ` ,
2025-12-23 15:42:35 -08:00
'---'
] . join ( '\n' )
} ,
{
name : 'Windsurf Workflows' ,
dir : '.windsurf/workflows' ,
filePattern : ( prompt ) => generateFilename ( prompt , '.md' ) ,
header : ( prompt ) => [
'---' ,
2026-06-01 16:39:35 -07:00
` description: "Plan2Code ${ generateStepLabel ( prompt ) } : ${ prompt . displayName } - ${ prompt . description } " ` ,
2025-12-23 15:42:35 -08:00
'---'
] . join ( '\n' )
} ,
{
name : 'Windsurf Global Workflows' ,
dir : '.codeium/windsurf/global_workflows' ,
filePattern : ( prompt ) => generateFilename ( prompt , '.md' ) ,
header : ( prompt ) => [
'---' ,
2026-06-01 16:39:35 -07:00
` description: "Plan2Code ${ generateStepLabel ( prompt ) } : ${ prompt . displayName } - ${ prompt . description } " ` ,
2025-12-23 15:42:35 -08:00
'---'
] . join ( '\n' )
} ,
{
name : 'Agent Workflows' ,
dir : '.agent/workflows' ,
filePattern : ( prompt ) => generateFilename ( prompt , '.md' ) ,
header : ( prompt ) => [
'---' ,
2026-06-01 16:39:35 -07:00
` description: "Plan2Code ${ generateStepLabel ( prompt ) } : ${ prompt . displayName } - ${ prompt . description } " ` ,
2025-12-23 15:42:35 -08:00
'---'
] . join ( '\n' )
} ,
{
name : 'Codeium Global Workflows' ,
dir : '.codeium/global_workflows' ,
filePattern : ( prompt ) => generateFilename ( prompt , '.md' ) ,
header : ( prompt ) => [
'---' ,
2026-06-01 16:39:35 -07:00
` description: "Plan2Code ${ generateStepLabel ( prompt ) } : ${ prompt . displayName } - ${ prompt . description } " ` ,
2025-12-23 15:42:35 -08:00
'---'
] . join ( '\n' )
2026-02-22 12:54:33 -08:00
} ,
2026-03-01 12:24:11 -08:00
{
name : 'Pi Prompts' ,
dir : '.pi/prompts' ,
filePattern : ( prompt ) => generateFilename ( prompt , '.md' ) ,
header : ( prompt ) => [
'---' ,
2026-06-01 16:39:35 -07:00
` description: "Plan2Code ${ generateStepLabel ( prompt ) } : ${ prompt . displayName } - ${ prompt . description } " ` ,
2026-03-01 12:24:11 -08:00
'---'
] . join ( '\n' )
} ,
2026-02-22 12:54:33 -08:00
{
name : 'Claude Code Skills' ,
dir : '.claude/skills' ,
type : 'skill' ,
header : ( prompt ) => generateSkillHeader ( prompt , true ) ,
} ,
{
2026-05-23 06:58:20 -07:00
name : 'Agent Skills (Amp · Devin · Gemini CLI · OpenCode)' ,
2026-02-22 12:54:33 -08:00
dir : '.agents/skills' ,
type : 'skill' ,
header : ( prompt ) => generateSkillHeader ( prompt , false ) ,
} ,
{
name : 'Gemini CLI' ,
dir : '.gemini/commands' ,
type : 'toml' ,
filePattern : ( prompt ) => ` ${ generateFilename ( prompt , '' ) } .toml ` ,
2025-12-23 15:42:35 -08:00
}
] ;
// 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 ) => [
'---' ,
2026-06-01 16:39:35 -07:00
` description: "Plan2Code ${ generateStepLabel ( prompt ) } : ${ prompt . displayName } - ${ prompt . description } " ` ,
2025-12-23 15:42:35 -08:00
'---'
] . 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 } ` ,
2026-06-01 16:39:35 -07:00
` description: "Plan2Code ${ generateStepLabel ( prompt ) } : ${ prompt . displayName } - ${ prompt . description } " ` ,
2025-12-23 15:42:35 -08:00
'---'
] . join ( '\n' )
} ,
{
name : 'Windsurf (Global)' ,
dir : '.codeium/windsurf/global_workflows' ,
filePattern : ( prompt ) => generateFilename ( prompt , '.md' ) ,
header : ( prompt ) => [
'---' ,
2026-06-01 16:39:35 -07:00
` description: "Plan2Code ${ generateStepLabel ( prompt ) } : ${ prompt . displayName } - ${ prompt . description } " ` ,
2025-12-23 15:42:35 -08:00
'---'
] . join ( '\n' )
} ,
{
name : 'VS Code Copilot (Global)' ,
dir : 'vscode-copilot-prompts' ,
filePattern : ( prompt ) => generateFilename ( prompt , '.prompt.md' ) ,
header : ( prompt ) => [
'---' ,
'agent: agent' ,
2026-06-01 16:39:35 -07:00
` description: "Plan2Code ${ generateStepLabel ( prompt ) } : ${ prompt . displayName } - ${ prompt . description } " ` ,
2025-12-23 15:42:35 -08:00
'---'
] . join ( '\n' )
} ,
{
name : 'Codeium (Global)' ,
dir : '.codeium/global_workflows' ,
filePattern : ( prompt ) => generateFilename ( prompt , '.md' ) ,
header : ( prompt ) => [
'---' ,
2026-06-01 16:39:35 -07:00
` description: "Plan2Code ${ generateStepLabel ( prompt ) } : ${ prompt . displayName } - ${ prompt . description } " ` ,
2025-12-23 15:42:35 -08:00
'---'
] . join ( '\n' )
2026-02-22 12:54:33 -08:00
} ,
2026-03-01 12:24:11 -08:00
{
name : 'Pi (Global)' ,
dir : '.pi/agent/prompts' ,
filePattern : ( prompt ) => generateFilename ( prompt , '.md' ) ,
header : ( prompt ) => [
'---' ,
2026-06-01 16:39:35 -07:00
` description: "Plan2Code ${ generateStepLabel ( prompt ) } : ${ prompt . displayName } - ${ prompt . description } " ` ,
2026-03-01 12:24:11 -08:00
'---'
] . join ( '\n' )
} ,
2026-02-22 12:54:33 -08:00
{
name : 'Claude Code Skills' ,
dir : '.claude/skills' ,
type : 'skill' ,
header : ( prompt ) => generateSkillHeader ( prompt , true ) ,
} ,
{
2026-06-01 16:39:35 -07:00
name : 'Agent Skills (Amp · Devin · Gemini CLI · OpenCode · Zed)' ,
2026-02-22 12:54:33 -08:00
dir : '.agents/skills' ,
type : 'skill' ,
2026-06-01 16:39:35 -07:00
header : ( prompt ) => generateSkillHeader ( prompt , true ) ,
2026-02-22 12:54:33 -08:00
} ,
{
name : 'Crush' ,
dir : 'crush-skills' ,
type : 'skill' ,
header : ( prompt ) => generateSkillHeader ( prompt , false ) ,
} ,
{
name : 'Gemini CLI' ,
dir : '.gemini/commands' ,
type : 'toml' ,
filePattern : ( prompt ) => ` ${ generateFilename ( prompt , '' ) } .toml ` ,
2025-12-23 15:42:35 -08:00
}
] ;
// 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' ) ;
}
}
2026-02-22 12:54:33 -08:00
// 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' ) ;
}
2025-12-23 15:42:35 -08:00
// 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 ) ;
}
2026-02-22 12:54:33 -08:00
// 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' ,
} ,
] ;
2025-12-23 15:42:35 -08:00
// Installation targets
const INSTALL _TARGETS = [
{
name : 'Claude Code' ,
id : 'claude' ,
2026-02-22 12:54:33 -08:00
dir : '.claude/skills' ,
type : 'skill' ,
filePattern : /^plan2code-/ ,
2025-12-23 15:42:35 -08:00
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 : '◉ '
2026-02-22 12:54:33 -08:00
} ,
{
2026-06-01 16:39:35 -07:00
name : 'Agent Skills (Amp · Devin · Gemini CLI · OpenCode · Zed)' ,
2026-02-22 12:54:33 -08:00
id : 'agent-skills' ,
dir : getAgentSkillsDir ,
sourceDir : '.agents/skills' ,
type : 'skill' ,
filePattern : /^plan2code-/ ,
2026-06-01 16:39:35 -07:00
displayPath : '~/.agents/skills' ,
2026-02-22 12:54:33 -08:00
icon : '◉ '
} ,
{
name : 'Crush' ,
id : 'crush' ,
dir : getCrushSkillsDir ,
sourceDir : 'crush-skills' ,
type : 'skill' ,
filePattern : /^plan2code-/ ,
displayPath : process . platform === 'win32'
? '%LOCALAPPDATA%\\crush\\skills'
: '~/.config/crush/skills' ,
icon : '◉ '
} ,
{
name : 'Gemini CLI' ,
id : 'gemini' ,
dir : '.gemini/commands' ,
type : 'toml' ,
filePattern : /^plan2code-.*\.toml$/ ,
icon : '◉ '
2026-03-01 12:24:11 -08:00
} ,
{
name : 'Pi' ,
id : 'pi' ,
dir : '.pi/agent/prompts' ,
filePattern : /^plan2code-.*\.md$/ ,
icon : '◉ '
2025-12-23 15:42:35 -08:00
}
] ;
// ============================================================================
// DISPLAY FUNCTIONS
// ============================================================================
2026-01-27 12:11:00 -08:00
/**
* 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 ( '' ) ;
}
2025-12-23 15:42:35 -08:00
/**
* Display header
*/
function displayHeader ( ) {
console . log ( '' ) ;
2026-02-22 12:54:33 -08:00
console . log ( ` ${ COLORS . CYAN } ${ COLORS . BRIGHT } ` ) ;
console . log ( '╔═════════════════════════════════════════════════════════╗' ) ;
console . log ( '║ ╭───╮ ║' ) ;
console . log ( '║ │ ● │ Hi! I\'m Planny! ║' ) ;
console . log ( '║ │ ◡ │ Nice to meet you ║' ) ;
console . log ( '║ ╰───╯ Welcome to Plan2Code! ║' ) ;
console . log ( '║ ║' ) ;
console . log ( '║ G L O B A L I N S T A L L A T I O N S Y S T E M ║' ) ;
console . log ( '║ https://jparkerweb.github.io/plan2code ║' ) ;
console . log ( '║ ║' ) ;
console . log ( '╚═════════════════════════════════════════════════════════╝' ) ;
2025-12-23 15:42:35 -08:00
console . log ( COLORS . RESET ) ;
2026-02-22 12:54:33 -08:00
console . log ( '' ) ;
2025-12-23 15:42:35 -08:00
}
/**
* 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 {
2026-06-01 16:39:35 -07:00
fs . rmSync ( dirPath , { recursive : true , force : true } ) ;
2025-12-23 15:42:35 -08:00
} 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
2026-02-22 12:54:33 -08:00
if ( ! fs . existsSync ( destDir ) ) {
2025-12-23 15:42:35 -08:00
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 ) {
2026-02-22 12:54:33 -08:00
try {
fs . writeFileSync ( destPath , outputContent , 'utf8' ) ;
if ( ! quiet ) {
console . log ( ` ${ COLORS . GREEN } ▰▰▰ ${ COLORS . RESET } ${ destFile } ` ) ;
2025-12-23 15:42:35 -08:00
}
2026-02-22 12:54:33 -08:00
} 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 ++ ;
}
}
2026-05-23 06:58:20 -07:00
/**
* Recursively copy a directory and all its contents (files and subdirectories).
*/
function copyDirRecursive ( src , dest , stats ) {
fs . mkdirSync ( dest , { recursive : true } ) ;
const entries = fs . readdirSync ( src ) ;
for ( const entry of entries ) {
const srcPath = path . join ( src , entry ) ;
const destPath = path . join ( dest , entry ) ;
if ( fs . statSync ( srcPath ) . isDirectory ( ) ) {
copyDirRecursive ( srcPath , destPath , stats ) ;
} else {
fs . copyFileSync ( srcPath , destPath ) ;
stats . copied ++ ;
}
}
}
/**
* Inline reference file content into orchestrator content.
* Replaces each `Read references/X.md` directive with the file's actual content,
* wrapped in markers. Used for TOML targets (Gemini CLI), which cannot resolve
* relative file reads at runtime.
*/
function inlineReferenceContent ( sourceContent , srcRefDir ) {
if ( ! fs . existsSync ( srcRefDir ) ) return sourceContent ;
return sourceContent . replace (
/^Read\s+references\/([^\s]+\.md)(.*)$/gm ,
( match , filename , suffix ) => {
const refPath = path . join ( srcRefDir , filename ) ;
if ( ! fs . existsSync ( refPath ) ) return match ;
const refContent = fs . readFileSync ( refPath , 'utf8' ) ;
return ` <!-- BEGIN inlined references/ ${ filename } ${ suffix } --> \n ${ refContent } \n <!-- END inlined references/ ${ filename } --> ` ;
}
) ;
}
/**
2026-06-01 16:39:35 -07:00
* Copy a reference directory (e.g., plan2code-review-references/) to a destination.
2026-05-23 06:58:20 -07:00
* 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 } ` ) ;
}
}
}
2026-02-22 12:54:33 -08:00
/**
* Write a TOML file to a destination for Gemini CLI
*/
function writeTomlToDestination ( rootDir , baseDir , dest , prompt , sourceContent , stats , quiet = false ) {
const filename = dest . filePattern ( prompt ) ;
const destDir = path . join ( rootDir , baseDir , dest . dir ) ;
const filePath = path . join ( destDir , filename ) ;
// Build TOML content
const tomlContent = generateTomlContent ( prompt , sourceContent ) ;
// Ensure destination directory exists
fs . mkdirSync ( destDir , { recursive : true } ) ;
// Check if file needs updating
let needsUpdate = true ;
if ( fs . existsSync ( filePath ) ) {
try {
const existingContent = fs . readFileSync ( filePath , 'utf8' ) ;
needsUpdate = existingContent !== tomlContent ;
} catch ( err ) {
// File exists but can't be read, will try to write
}
}
if ( needsUpdate ) {
try {
fs . writeFileSync ( filePath , tomlContent , 'utf8' ) ;
if ( ! quiet ) {
console . log ( ` ${ COLORS . GREEN } ▰▰▰ ${ COLORS . RESET } ${ filename } ` ) ;
}
} catch ( err ) {
console . error ( ` ${ COLORS . RED } ✖✖✖ ${ COLORS . RESET } ${ filename } : ${ err . message } ` ) ;
stats . errors ++ ;
return ;
2025-12-23 15:42:35 -08:00
}
stats . updated ++ ;
} else {
stats . skipped ++ ;
}
}
/**
* Sync prompts from src/ to dist/ directories
* This generates the distribution files before installation
*/
function syncPrompts ( quiet = false ) {
const stats = { updated : 0 , skipped : 0 , errors : 0 } ;
const projectRoot = _ _dirname ;
if ( ! quiet ) {
console . log ( ` ${ COLORS . CYAN } ${ SYMBOLS . ACTIVE } GENERATING DISTRIBUTION FILES ${ COLORS . RESET } \n ` ) ;
}
// Clean up old dist directories
2026-02-22 12:54:33 -08:00
const localDistPath = path . join ( projectRoot , 'dist/local-commands' ) ;
const globalDistPath = path . join ( projectRoot , 'dist/global-commands' ) ;
2025-12-23 15:42:35 -08:00
2026-02-22 12:54:33 -08:00
if ( fs . existsSync ( localDistPath ) ) {
if ( ! quiet ) console . log ( ` ${ COLORS . YELLOW } ░░░ ${ COLORS . RESET } Cleaning: dist/local-commands/ ` ) ;
deleteDirectory ( localDistPath ) ;
}
2025-12-23 15:42:35 -08:00
2026-02-22 12:54:33 -08:00
if ( fs . existsSync ( globalDistPath ) ) {
if ( ! quiet ) console . log ( ` ${ COLORS . YELLOW } ░░░ ${ COLORS . RESET } Cleaning: dist/global-commands/ ` ) ;
deleteDirectory ( globalDistPath ) ;
2025-12-23 15:42:35 -08:00
}
// 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 ;
}
2026-06-01 16:39:35 -07:00
// Detect reference directory (e.g., plan2code-review-references/)
2026-05-23 06:58:20 -07:00
const refDirName = prompt . source . replace ( /\.md$/ , '-references' ) ;
const srcRefDir = path . join ( SRC _DIR , refDirName ) ;
const hasRefDir = fs . existsSync ( srcRefDir ) ;
// Write to dist/local-commands/ and dist/global-commands/
const distBases = [
{ base : 'dist/local-commands' , destinations : LOCAL _DESTINATIONS } ,
{ base : 'dist/global-commands' , destinations : GLOBAL _DESTINATIONS } ,
] ;
for ( const { base , destinations } of distBases ) {
for ( const dest of destinations ) {
if ( dest . type === 'skill' ) {
writeSkillToDestination ( projectRoot , base , dest , prompt , sourceContent , stats , quiet ) ;
if ( hasRefDir ) {
const skillName = generateSkillName ( prompt ) ;
const destRefDir = path . join ( projectRoot , base , dest . dir , skillName , 'references' ) ;
copyReferenceDirectory ( srcRefDir , destRefDir , stats , quiet ) ;
}
} else if ( dest . type === 'toml' ) {
// TOML targets (Gemini CLI) cannot resolve relative file reads at runtime,
// so inline reference content directly into the prompt body.
const contentForToml = hasRefDir
? inlineReferenceContent ( sourceContent , srcRefDir )
: sourceContent ;
writeTomlToDestination ( projectRoot , base , dest , prompt , contentForToml , stats , quiet ) ;
} else {
// For flat-file destinations, rewrite Read directive paths to sibling directory name
const contentForDest = hasRefDir
? sourceContent . replace ( /^(Read\s+)references\//gm , '$1' + refDirName + '/' )
: sourceContent ;
writeToDestination ( projectRoot , base , dest , prompt , contentForDest , stats , quiet ) ;
if ( hasRefDir ) {
const destRefDir = path . join ( projectRoot , base , dest . dir , refDirName ) ;
copyReferenceDirectory ( srcRefDir , destRefDir , stats , quiet ) ;
}
}
2026-02-22 12:54:33 -08:00
}
2025-12-23 15:42:35 -08:00
}
}
if ( ! quiet ) {
console . log ( '' ) ;
console . log ( ` ${ COLORS . GREEN } ${ SYMBOLS . SUCCESS } ${ COLORS . RESET } Generated ${ stats . updated } files ( ${ stats . skipped } unchanged) ` ) ;
if ( stats . errors > 0 ) {
console . log ( ` ${ COLORS . RED } ${ SYMBOLS . ERROR } ${ COLORS . RESET } ${ stats . errors } errors ` ) ;
}
console . log ( '' ) ;
}
return stats . errors === 0 ;
}
// ============================================================================
// TARGET SELECTION
// ============================================================================
/**
* Get targets to process based on selected platforms
*/
function getTargets ( platformIds = null ) {
if ( platformIds && platformIds . length > 0 ) {
return INSTALL _TARGETS . filter ( t => platformIds . includes ( t . id ) ) ;
}
return INSTALL _TARGETS ;
}
// ============================================================================
// INSTALLATION
// ============================================================================
2026-02-22 12:54:33 -08:00
/**
* 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 ) {
2026-05-23 06:58:20 -07:00
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 ++ ;
}
2026-02-22 12:54:33 -08:00
}
}
}
/**
* 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 ) ;
}
}
2026-05-23 06:58:20 -07:00
// 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 } ) ;
}
}
}
2026-02-22 12:54:33 -08:00
}
}
2025-12-23 15:42:35 -08:00
/**
* Execute installation
*/
2026-06-01 16:39:35 -07:00
async function install ( targets = null ) {
2025-12-23 15:42:35 -08:00
const projectRoot = _ _dirname ;
const homeDir = os . homedir ( ) ;
const targetList = targets || getTargets ( ) ;
let totalInstalled = 0 ;
let totalErrors = 0 ;
2026-02-22 12:54:33 -08:00
displaySectionHeader ( ' INSTALLING FILES ' ) ;
2025-12-23 15:42:35 -08:00
// 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 ` ) ;
2026-02-22 12:54:33 -08:00
// Clean up legacy install targets before installing new ones
cleanLegacyTargets ( ) ;
2025-12-23 15:42:35 -08:00
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 } ` ,
2026-02-22 12:54:33 -08:00
` ${ COLORS . CYAN } Mode: ${ COLORS . RESET } INSTALL ` ,
2025-12-23 15:42:35 -08:00
] ) ;
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 ;
}
2026-02-22 12:54:33 -08:00
// Get files/entries to install
2025-12-23 15:42:35 -08:00
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
2026-02-22 12:54:33 -08:00
if ( ! fs . existsSync ( destPath ) ) {
2025-12-23 15:42:35 -08:00
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 ;
}
}
2026-02-22 12:54:33 -08:00
// Clean up old files/dirs before copying new ones
2025-12-23 15:42:35 -08:00
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 {
2026-02-22 12:54:33 -08:00
if ( target . type === 'skill' ) {
fs . rmSync ( oldFilePath , { recursive : true , force : true } ) ;
} else {
2025-12-23 15:42:35 -08:00
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 } ` ) ;
}
}
}
2026-05-23 06:58:20 -07:00
// 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 } / ` ) ;
}
}
2025-12-23 15:42:35 -08:00
} catch ( err ) {
// Directory might be newly created, ignore read errors
}
2026-02-22 12:54:33 -08:00
// Copy files/skill directories
if ( target . type === 'skill' ) {
const copyStats = { copied : 0 } ;
console . log ( ` ${ COLORS . CYAN } ${ SYMBOLS . ACTIVE } COPYING ${ COLORS . RESET } skill directories... ` ) ;
2025-12-23 15:42:35 -08:00
try {
2026-02-22 12:54:33 -08:00
copySkillDirectory ( sourcePath , destPath , copyStats ) ;
console . log ( ` ${ COLORS . GREEN } ▰▰▰ ${ COLORS . RESET } ${ copyStats . copied } file(s) across ${ files . length } skill(s) ` ) ;
totalInstalled += copyStats . copied ;
2025-12-23 15:42:35 -08:00
} catch ( err ) {
2026-02-22 12:54:33 -08:00
console . log ( ` ${ COLORS . RED } ✖✖✖ ${ COLORS . RESET } Copy failed: ${ err . message } ` ) ;
2025-12-23 15:42:35 -08:00
totalErrors ++ ;
}
2026-02-22 12:54:33 -08:00
} 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 ++ ;
}
}
2026-05-23 06:58:20 -07:00
// 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 ;
}
2025-12-23 15:42:35 -08:00
}
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 } ` ) ;
2026-02-22 12:54:33 -08:00
console . log ( padEndVisible ( ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } ${ COLORS . GREEN } Mode: ${ COLORS . RESET } INSTALL ` , 76 ) + ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } ` ) ;
2025-12-23 15:42:35 -08:00
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 } ` ) ;
2026-06-01 16:39:35 -07:00
2025-12-23 15:42:35 -08:00
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 } ` ) ;
2026-01-27 12:11:00 -08:00
// Show celebratory or sad mascot based on result
console . log ( '' ) ;
2026-02-22 12:54:33 -08:00
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 } ` ) ;
2025-12-23 15:42:35 -08:00
}
console . log ( '' ) ;
return totalErrors === 0 ? 0 : 1 ;
}
// ============================================================================
// UNINSTALLATION
// ============================================================================
/**
* Execute uninstallation
*/
function uninstallFiles ( targets = null ) {
const homeDir = os . homedir ( ) ;
const targetList = targets || getTargets ( ) ;
let totalRemoved = 0 ;
let totalErrors = 0 ;
2026-02-22 12:54:33 -08:00
displaySectionHeader ( 'UNINSTALLATION' , '[ REMOVING FILES ]' ) ;
2025-12-23 15:42:35 -08:00
console . log ( '' ) ;
displayStatusBox ( 'PARAMETERS' , [
` ${ COLORS . CYAN } Home Directory: ${ COLORS . RESET } ${ homeDir } ` ,
` ${ COLORS . CYAN } Platforms: ${ COLORS . RESET } ${ targetList . length } ` ,
2026-02-22 12:54:33 -08:00
` ${ COLORS . CYAN } Mode: ${ COLORS . RESET } UNINSTALL ` ,
2025-12-23 15:42:35 -08:00
] ) ;
console . log ( '' ) ;
2026-02-22 12:54:33 -08:00
// Clean up legacy install targets
cleanLegacyTargets ( ) ;
2025-12-23 15:42:35 -08:00
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 ;
}
2026-02-22 12:54:33 -08:00
// Get files/entries to remove
2025-12-23 15:42:35 -08:00
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 ) {
2026-05-23 06:58:20 -07:00
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 ) ;
2025-12-23 15:42:35 -08:00
2026-05-23 06:58:20 -07:00
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 ++ ;
}
}
}
2025-12-23 15:42:35 -08:00
2026-05-23 06:58:20 -07:00
// 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' ) {
2025-12-23 15:42:35 -08:00
try {
2026-05-23 06:58:20 -07:00
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 ++ ;
2025-12-23 15:42:35 -08:00
}
} catch ( err ) {
2026-05-23 06:58:20 -07:00
// Directory may already be removed, ignore
2025-12-23 15:42:35 -08:00
}
}
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 } ` ) ;
2026-02-22 12:54:33 -08:00
console . log ( padEndVisible ( ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } ${ COLORS . GREEN } Mode: ${ COLORS . RESET } UNINSTALL ` , 76 ) + ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } ` ) ;
2025-12-23 15:42:35 -08:00
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' } ,
2026-01-27 12:11:00 -08:00
{ name : 'Codeium (IntelliJ)' , dir : '.codeium/global_workflows/' , desc : 'Codeium global workflows' } ,
2026-03-01 12:24:11 -08:00
{ name : 'Pi' , dir : '.pi/prompts/' , desc : 'Prompt templates for pi.dev coding agent' } ,
2025-12-23 15:42:35 -08:00
] ;
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 ( ) {
2026-02-22 12:54:33 -08:00
// Task 2.1: Main menu display
2025-12-23 15:42:35 -08:00
displayHeader ( ) ;
console . log ( ` ${ COLORS . BLUE } ${ COLORS . BRIGHT } Version Info: ${ COLORS . RESET } ${ projectVersion . name } ${ projectVersion . version } ` ) ;
console . log ( '' ) ;
2026-02-22 12:54:33 -08:00
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 } ` ) ;
2026-05-11 13:14:35 -07:00
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 } ` ) ;
2026-02-22 12:54:33 -08:00
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 } ` ) ;
2026-01-27 12:11:00 -08:00
console . log ( '' ) ;
2025-12-23 15:42:35 -08:00
2026-05-11 13:14:35 -07:00
// Input prompt
2026-02-22 12:54:33 -08:00
console . log ( '' ) ;
2026-05-11 13:14:35 -07:00
const answer = await question ( ` ${ COLORS . CYAN } ${ SYMBOLS . SELECT } SELECT OPTION ${ COLORS . RESET } (I, A, U, C, Q) [I]: ` ) ;
2026-02-22 12:54:33 -08:00
const input = answer . trim ( ) . toUpperCase ( ) || 'I' ;
2026-02-18 15:16:11 -08:00
2026-05-11 13:14:35 -07:00
// I path — Install prompts + loop
2026-02-22 12:54:33 -08:00
if ( input === 'I' ) {
2026-01-27 12:11:00 -08:00
rl . close ( ) ;
2026-02-22 12:54:33 -08:00
const loopResult = installPlan2CodeLoop ( ) ;
console . log ( '' ) ;
2026-06-01 16:39:35 -07:00
const installResult = await install ( ) ;
2026-05-11 13:14:35 -07:00
const exitCode = loopResult !== 0 ? loopResult : installResult ;
process . exit ( exitCode ) ;
}
2026-06-01 16:39:35 -07:00
// A path — Install Everything (prompts + loop + bot + metrics + Claude status line)
2026-05-11 13:14:35 -07:00
if ( input === 'A' ) {
rl . close ( ) ;
const loopResult = installPlan2CodeLoop ( ) ;
2026-02-22 19:57:40 -08:00
console . log ( '' ) ;
2026-03-01 21:30:45 -08:00
const botResult = installPlan2CodeBot ( ) ;
console . log ( '' ) ;
2026-05-11 13:14:35 -07:00
const metricsResult = installPlan2CodeMetrics ( ) ;
console . log ( '' ) ;
2026-06-01 16:39:35 -07:00
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 ;
2026-02-22 19:57:40 -08:00
process . exit ( exitCode ) ;
2026-01-27 12:11:00 -08:00
}
2025-12-23 15:42:35 -08:00
2026-05-11 13:14:35 -07:00
// U path — Uninstall All
2025-12-23 15:42:35 -08:00
if ( input === 'U' ) {
console . log ( '' ) ;
2026-02-22 12:54:33 -08:00
console . log ( ` ${ COLORS . RED } o o ${ COLORS . RESET } ` ) ;
console . log ( ` ${ COLORS . RED } ╲ ╱ ${ COLORS . YELLOW } ! ${ COLORS . RESET } ` ) ;
2026-01-27 12:11:00 -08:00
console . log ( ` ${ COLORS . RED } ╭───╮ ${ COLORS . RESET } ` ) ;
console . log ( ` ${ COLORS . RED } │ ${ COLORS . YELLOW } ○ ${ COLORS . RED } │ ${ COLORS . RESET } ` ) ;
2026-02-22 12:54:33 -08:00
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 } ` ) ;
2026-01-27 12:11:00 -08:00
console . log ( ` ${ COLORS . RED } ╰───╯ ${ COLORS . RESET } ` ) ;
console . log ( '' ) ;
2026-02-22 12:54:33 -08:00
const confirmAnswer = await question ( ` ${ COLORS . RED } ${ SYMBOLS . SELECT } CONFIRM UNINSTALL ${ COLORS . RESET } (Y/N) [N]: ` ) ;
const confirmInput = confirmAnswer . trim ( ) . toUpperCase ( ) || 'N' ;
2025-12-23 15:42:35 -08:00
2026-02-22 12:54:33 -08:00
if ( confirmInput === 'Y' ) {
2025-12-23 15:42:35 -08:00
rl . close ( ) ;
2026-02-22 12:54:33 -08:00
const uninstallResult = uninstallFiles ( ) ;
const loopResult = uninstallPlan2CodeLoop ( ) ;
2026-02-22 19:57:40 -08:00
const metricsResult = uninstallPlan2CodeMetrics ( ) ;
2026-03-01 21:30:45 -08:00
const botResult = uninstallPlan2CodeBot ( ) ;
2026-06-01 16:39:35 -07:00
const statusLineResult = uninstallStatusLine ( ) ;
const exitCode = uninstallResult !== 0 ? uninstallResult : loopResult !== 0 ? loopResult : metricsResult !== 0 ? metricsResult : botResult !== 0 ? botResult : statusLineResult ;
2026-02-22 19:57:40 -08:00
process . exit ( exitCode ) ;
2025-12-23 15:42:35 -08:00
} else {
console . log ( ` \n ${ COLORS . YELLOW } ${ SYMBOLS . WARNING } CANCELLED ${ COLORS . RESET } \n ` ) ;
rl . close ( ) ;
process . exit ( 0 ) ;
}
2026-02-22 12:54:33 -08:00
}
2025-12-23 15:42:35 -08:00
2026-05-11 13:14:35 -07:00
// C path — CUSTOM sub-menu
2026-02-22 12:54:33 -08:00
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 } ` ) ;
2026-02-22 19:57:40 -08:00
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 } ` ) ;
2026-06-01 16:39:35 -07:00
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 } ` ) ;
2026-03-01 21:30:45 -08:00
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 } ` ) ;
2026-02-22 12:54:33 -08:00
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 ( '' ) ;
2026-06-01 16:39:35 -07:00
const customAnswer = await question ( ` ${ COLORS . CYAN } ${ SYMBOLS . SELECT } SELECT OPTION ${ COLORS . RESET } (L, O, M, S, B, Q) [Q]: ` ) ;
2026-02-22 12:54:33 -08:00
const customInput = customAnswer . trim ( ) . toUpperCase ( ) || 'Q' ;
2026-02-03 20:32:18 -08:00
2026-05-11 13:14:35 -07:00
// C > L — show local install instructions
2026-02-22 12:54:33 -08:00
if ( customInput === 'L' ) {
rl . close ( ) ;
process . exit ( displayLocalInstructions ( ) ) ;
2026-01-27 12:11:00 -08:00
}
2026-02-03 20:32:18 -08:00
2026-05-11 13:14:35 -07:00
// C > O — install loop CLI only
2026-02-22 12:54:33 -08:00
if ( customInput === 'O' ) {
2025-12-23 15:42:35 -08:00
rl . close ( ) ;
2026-02-22 12:54:33 -08:00
const result = installPlan2CodeLoop ( ) ;
process . exit ( result ) ;
2025-12-23 15:42:35 -08:00
}
2026-02-22 19:57:40 -08:00
// C > M — install metrics CLI only
if ( customInput === 'M' ) {
rl . close ( ) ;
const result = installPlan2CodeMetrics ( ) ;
process . exit ( result ) ;
}
2026-06-01 16:39:35 -07:00
// C > S — install status line
if ( customInput === 'S' ) {
rl . close ( ) ;
const result = await installStatusLine ( ) ;
process . exit ( result ) ;
}
2026-03-01 21:30:45 -08:00
// C > B — install bot CLI only
if ( customInput === 'B' ) {
rl . close ( ) ;
const result = installPlan2CodeBot ( ) ;
process . exit ( result ) ;
}
2026-05-11 13:14:35 -07:00
// C > Q — back to main menu
2026-02-22 12:54:33 -08:00
return main ( ) ;
2025-12-23 15:42:35 -08:00
}
2026-05-11 13:14:35 -07:00
// Q — exit
2026-02-22 12:54:33 -08:00
if ( input === 'Q' ) {
2025-12-23 15:42:35 -08:00
console . log ( ` \n ${ COLORS . YELLOW } ${ SYMBOLS . WARNING } CANCELLED ${ COLORS . RESET } \n ` ) ;
rl . close ( ) ;
process . exit ( 0 ) ;
}
2026-05-11 13:14:35 -07:00
// Invalid input — loop back
console . log ( ` \n ${ COLORS . RED } ${ SYMBOLS . ERROR } Invalid option. Please choose I, A, U, C, or Q. ${ COLORS . RESET } \n ` ) ;
2026-02-22 12:54:33 -08:00
return main ( ) ;
2025-12-23 15:42:35 -08:00
}
main ( ) . catch ( err => {
console . error ( ` ${ COLORS . RED } ${ SYMBOLS . ERROR } ERROR: ${ COLORS . RESET } ` , err . message ) ;
rl . close ( ) ;
process . exit ( 1 ) ;
} ) ;
}
2026-01-27 12:11:00 -08:00
// ============================================================================
// PLAN2CODE-LOOP INSTALLATION
// ============================================================================
2026-02-03 20:32:18 -08:00
2026-06-01 16:39:35 -07:00
const { execSync } = require ( 'child_process' ) ;
2026-02-03 20:32:18 -08:00
2026-01-27 12:11:00 -08:00
/**
* Build plan2code-loop package
*/
function buildPlan2CodeLoop ( ) {
const loopDir = path . join ( _ _dirname , 'plan2code-loop' ) ;
2026-02-03 20:32:18 -08:00
2026-01-27 12:11:00 -08:00
// Check if directory exists
if ( ! fs . existsSync ( loopDir ) ) {
console . log ( ` ${ COLORS . YELLOW } ${ SYMBOLS . WARNING } ${ COLORS . RESET } plan2code-loop directory not found ` ) ;
return false ;
}
2026-02-03 20:32:18 -08:00
2026-01-27 12:11:00 -08:00
console . log ( ` ${ COLORS . CYAN } ${ SYMBOLS . ACTIVE } Building plan2code-loop... ${ COLORS . RESET } ` ) ;
2026-02-03 20:32:18 -08:00
2026-01-27 12:11:00 -08:00
try {
// Install dependencies
console . log ( ` ${ COLORS . DIM } Installing dependencies... ${ COLORS . RESET } ` ) ;
execSync ( 'npm install' , { cwd : loopDir , stdio : 'pipe' } ) ;
2026-02-03 20:32:18 -08:00
2026-01-27 12:11:00 -08:00
// Build
console . log ( ` ${ COLORS . DIM } Running build... ${ COLORS . RESET } ` ) ;
execSync ( 'npm run build' , { cwd : loopDir , stdio : 'pipe' } ) ;
2026-02-03 20:32:18 -08:00
2026-01-27 12:11:00 -08:00
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 \n node " ${ 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 \n node " ${ 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 ) ;
2026-06-01 16:39:35 -07:00
fs . rmSync ( file . path , { force : true , recursive : true } ) ;
console . log ( ` ${ COLORS . GREEN } ${ SYMBOLS . SUCCESS } ${ COLORS . RESET } Removed: ${ file . name } ` ) ;
removedCount ++ ;
2026-01-27 12:11:00 -08:00
} catch ( err ) {
2026-06-01 16:39:35 -07:00
if ( err . code !== 'ENOENT' ) {
console . log ( ` ${ COLORS . RED } ${ SYMBOLS . ERROR } ${ COLORS . RESET } Failed to remove ${ file . name } : ${ err . message } ` ) ;
}
2026-01-27 12:11:00 -08:00
}
}
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 ;
}
}
2026-06-01 16:39:35 -07:00
2026-01-27 12:11:00 -08:00
/**
* Install plan2code-loop (build + symlink)
*/
function installPlan2CodeLoop ( ) {
displaySectionHeader ( 'PLAN2CODE-LOOP' , '[ BUILD & INSTALL ]' ) ;
2026-02-03 20:32:18 -08:00
2026-01-27 12:11:00 -08:00
const buildSuccess = buildPlan2CodeLoop ( ) ;
if ( ! buildSuccess ) {
return 1 ;
}
2026-02-03 20:32:18 -08:00
2026-01-27 12:11:00 -08:00
const symlinkSuccess = createGlobalSymlink ( ) ;
if ( ! symlinkSuccess ) {
return 1 ;
}
2026-02-03 20:32:18 -08:00
2026-01-27 12:11:00 -08:00
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 ( '' ) ;
2026-02-03 20:32:18 -08:00
2026-01-27 12:11:00 -08:00
return 0 ;
}
2026-02-03 20:32:18 -08:00
2026-01-27 12:11:00 -08:00
/**
* Uninstall plan2code-loop
*/
function uninstallPlan2CodeLoop ( ) {
displaySectionHeader ( 'PLAN2CODE-LOOP' , '[ UNINSTALL ]' ) ;
2026-02-03 20:32:18 -08:00
2026-01-27 12:11:00 -08:00
removeGlobalSymlink ( ) ;
2026-02-03 20:32:18 -08:00
2026-01-27 12:11:00 -08:00
console . log ( '' ) ;
console . log ( ` ${ COLORS . GREEN } ${ SYMBOLS . SUCCESS } plan2code-loop uninstalled ${ COLORS . RESET } ` ) ;
console . log ( '' ) ;
2026-02-03 20:32:18 -08:00
2026-01-27 12:11:00 -08:00
return 0 ;
}
2026-02-03 20:32:18 -08:00
2026-02-22 19:57:40 -08:00
// ============================================================================
// 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 \n node " ${ 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 \n node " ${ 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 ) ;
2026-06-01 16:39:35 -07:00
fs . rmSync ( file . path , { force : true , recursive : true } ) ;
console . log ( ` ${ COLORS . GREEN } ${ SYMBOLS . SUCCESS } ${ COLORS . RESET } Removed: ${ file . name } ` ) ;
removedCount ++ ;
2026-02-22 19:57:40 -08:00
} catch ( err ) {
2026-06-01 16:39:35 -07:00
if ( err . code !== 'ENOENT' ) {
console . log ( ` ${ COLORS . RED } ${ SYMBOLS . ERROR } ${ COLORS . RESET } Failed to remove ${ file . name } : ${ err . message } ` ) ;
}
2026-02-22 19:57:40 -08:00
}
}
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 ;
}
2026-06-01 16:39:35 -07:00
// ============================================================================
// 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 ;
}
2026-03-01 21:30:45 -08:00
// ============================================================================
// 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 \n node " ${ 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 \n node " ${ 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 ) ;
2026-06-01 16:39:35 -07:00
fs . rmSync ( file . path , { force : true , recursive : true } ) ;
console . log ( ` ${ COLORS . GREEN } ${ SYMBOLS . SUCCESS } ${ COLORS . RESET } Removed: ${ file . name } ` ) ;
removedCount ++ ;
2026-03-01 21:30:45 -08:00
} catch ( err ) {
2026-06-01 16:39:35 -07:00
if ( err . code !== 'ENOENT' ) {
console . log ( ` ${ COLORS . RED } ${ SYMBOLS . ERROR } ${ COLORS . RESET } Failed to remove ${ file . name } : ${ err . message } ` ) ;
}
2026-03-01 21:30:45 -08:00
}
}
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 ;
}
2025-12-23 15:42:35 -08:00
// ============================================================================
// MAIN
// ============================================================================
2026-02-22 12:54:33 -08:00
// Always run interactive menu
runInteractive ( ) ;