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 = [
{
source : 'plan2code---init.md' ,
stepNumber : 'init' ,
name : 'init' ,
displayName : 'Init Mode' ,
description : 'Generate AGENTS.md file for project-specific guidance' ,
isUtility : true
} ,
{
source : 'plan2code---init-update.md' ,
stepNumber : 'update' ,
name : 'init-update' ,
displayName : 'Init Update Mode' ,
description : 'Update existing AGENTS.md with new learnings' ,
isUtility : true
} ,
{
source : 'plan2code---quick-task.md' ,
stepNumber : 0 ,
name : 'quick-task' ,
displayName : 'Quick Task Mode' ,
description : 'Lightweight planning for small tasks'
} ,
{
source : 'plan2code-1--plan.md' ,
stepNumber : 1 ,
name : 'plan' ,
displayName : 'Planning Mode' ,
description : 'Requirements analysis and architecture design'
} ,
{
source : 'plan2code-1b--revise-plan.md' ,
stepNumber : '1b' ,
name : 'revise-plan' ,
displayName : 'Revision Mode' ,
description : 'Modify specs mid-implementation when requirements change'
} ,
{
source : 'plan2code-2--document.md' ,
stepNumber : 2 ,
name : 'document' ,
displayName : 'Documentation Mode' ,
description : 'Transform planning output into structured implementation docs'
} ,
{
source : 'plan2code-3--implement.md' ,
stepNumber : 3 ,
name : 'implement' ,
displayName : 'Implementation Mode' ,
description : 'Execute implementation phase by phase'
} ,
{
source : 'plan2code-4--finalize.md' ,
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' ) {
return ` plan2code--- ${ prompt . name } ${ extension } ` ;
}
return ` plan2code- ${ prompt . stepNumber } -- ${ prompt . name } ${ extension } ` ;
}
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 , '-' ) ;
}
// Helper function to generate YAML frontmatter for SKILL.md files
function generateSkillHeader ( prompt , disableModelInvocation = false ) {
const lines = [
'---' ,
` name: ${ generateSkillName ( prompt ) } ` ,
` description: "Plan2Code ${ prompt . stepNumber === 'init' ? 'Init' : ` Step ${ prompt . stepNumber } ` } : ${ prompt . displayName } - user-initiated workflow step. Do not invoke autonomously." ` ,
] ;
if ( disableModelInvocation ) lines . push ( 'disable-model-invocation: true' ) ;
lines . push ( '---' ) ;
return lines . join ( '\n' ) ;
}
// Helper function to generate complete .toml file content for Gemini CLI
function generateTomlContent ( prompt , sourceContent ) {
const stepLabel = prompt . stepNumber === 'init' ? 'Init' : ` Step ${ prompt . stepNumber } ` ;
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' ,
` description: "Plan2Code ${ prompt . stepNumber === 'init' ? 'Init' : ` Step ${ prompt . stepNumber } ` } : ${ prompt . displayName } - ${ prompt . description } " ` ,
'---'
] . join ( '\n' )
} ,
{
name : 'GitHub Copilot Agents' ,
dir : '.github/agents' ,
filePattern : ( prompt ) => generateFilename ( prompt , '.agent.md' ) ,
header : ( prompt ) => [
'---' ,
` description: "Plan2Code ${ prompt . stepNumber === 'init' ? 'Init' : ` Step ${ prompt . stepNumber } ` } : ${ prompt . displayName } - ${ prompt . description } " ` ,
'---'
] . join ( '\n' )
} ,
{
name : 'Continue Prompts' ,
dir : '.continue/prompts' ,
filePattern : ( prompt ) => generateFilename ( prompt , '.prompt.md' ) ,
header : ( prompt ) => [
'---' ,
` name: ${ prompt . name } ` ,
` description: "Plan2Code ${ prompt . stepNumber === 'init' ? 'Init' : ` Step ${ prompt . stepNumber } ` } : ${ prompt . displayName } - ${ prompt . description } " ` ,
'---'
] . join ( '\n' )
} ,
{
name : 'Windsurf Workflows' ,
dir : '.windsurf/workflows' ,
filePattern : ( prompt ) => generateFilename ( prompt , '.md' ) ,
header : ( prompt ) => [
'---' ,
` description: "Plan2Code ${ prompt . stepNumber === 'init' ? 'Init' : ` Step ${ prompt . stepNumber } ` } : ${ prompt . displayName } - ${ prompt . description } " ` ,
'---'
] . join ( '\n' )
} ,
{
name : 'Windsurf Global Workflows' ,
dir : '.codeium/windsurf/global_workflows' ,
filePattern : ( prompt ) => generateFilename ( prompt , '.md' ) ,
header : ( prompt ) => [
'---' ,
` description: "Plan2Code ${ prompt . stepNumber === 'init' ? 'Init' : ` Step ${ prompt . stepNumber } ` } : ${ prompt . displayName } - ${ prompt . description } " ` ,
'---'
] . join ( '\n' )
} ,
{
name : 'Agent Workflows' ,
dir : '.agent/workflows' ,
filePattern : ( prompt ) => generateFilename ( prompt , '.md' ) ,
header : ( prompt ) => [
'---' ,
` description: "Plan2Code ${ prompt . stepNumber === 'init' ? 'Init' : ` Step ${ prompt . stepNumber } ` } : ${ prompt . displayName } - ${ prompt . description } " ` ,
'---'
] . join ( '\n' )
} ,
{
name : 'Codeium Global Workflows' ,
dir : '.codeium/global_workflows' ,
filePattern : ( prompt ) => generateFilename ( prompt , '.md' ) ,
header : ( prompt ) => [
'---' ,
` description: "Plan2Code ${ prompt . stepNumber === 'init' ? 'Init' : ` Step ${ prompt . stepNumber } ` } : ${ prompt . displayName } - ${ prompt . description } " ` ,
'---'
] . 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 ) => [
'---' ,
` description: "Plan2Code ${ prompt . stepNumber === 'init' ? 'Init' : ` Step ${ prompt . stepNumber } ` } : ${ prompt . displayName } - ${ prompt . description } " ` ,
'---'
] . join ( '\n' )
} ,
2026-02-22 12:54:33 -08:00
{
name : 'Claude Code Skills' ,
dir : '.claude/skills' ,
type : 'skill' ,
header : ( prompt ) => generateSkillHeader ( prompt , true ) ,
} ,
{
name : 'Agent Skills (Amp · Gemini CLI · OpenCode)' ,
dir : '.agents/skills' ,
type : 'skill' ,
header : ( prompt ) => generateSkillHeader ( prompt , false ) ,
} ,
{
name : 'Gemini CLI' ,
dir : '.gemini/commands' ,
type : 'toml' ,
filePattern : ( prompt ) => ` ${ generateFilename ( prompt , '' ) } .toml ` ,
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 ) => [
'---' ,
` description: "Plan2Code ${ prompt . stepNumber === 'init' ? 'Init' : ` Step ${ prompt . stepNumber } ` } : ${ prompt . displayName } - ${ prompt . description } " ` ,
'---'
] . join ( '\n' )
} ,
{
name : 'Cursor (Global)' ,
dir : '.cursor/commands' ,
filePattern : ( prompt ) => generateFilename ( prompt , '.md' ) ,
header : null
} ,
{
name : 'Continue (Global)' ,
dir : '.continue/prompts' ,
filePattern : ( prompt ) => generateFilename ( prompt , '.prompt.md' ) ,
header : ( prompt ) => [
'---' ,
` name: ${ prompt . name } ` ,
` description: "Plan2Code ${ prompt . stepNumber === 'init' ? 'Init' : ` Step ${ prompt . stepNumber } ` } : ${ prompt . displayName } - ${ prompt . description } " ` ,
'---'
] . join ( '\n' )
} ,
{
name : 'Windsurf (Global)' ,
dir : '.codeium/windsurf/global_workflows' ,
filePattern : ( prompt ) => generateFilename ( prompt , '.md' ) ,
header : ( prompt ) => [
'---' ,
` description: "Plan2Code ${ prompt . stepNumber === 'init' ? 'Init' : ` Step ${ prompt . stepNumber } ` } : ${ prompt . displayName } - ${ prompt . description } " ` ,
'---'
] . join ( '\n' )
} ,
{
name : 'VS Code Copilot (Global)' ,
dir : 'vscode-copilot-prompts' ,
filePattern : ( prompt ) => generateFilename ( prompt , '.prompt.md' ) ,
header : ( prompt ) => [
'---' ,
'agent: agent' ,
` description: "Plan2Code ${ prompt . stepNumber === 'init' ? 'Init' : ` Step ${ prompt . stepNumber } ` } : ${ prompt . displayName } - ${ prompt . description } " ` ,
'---'
] . join ( '\n' )
} ,
{
name : 'Codeium (Global)' ,
dir : '.codeium/global_workflows' ,
filePattern : ( prompt ) => generateFilename ( prompt , '.md' ) ,
header : ( prompt ) => [
'---' ,
` description: "Plan2Code ${ prompt . stepNumber === 'init' ? 'Init' : ` Step ${ prompt . stepNumber } ` } : ${ prompt . displayName } - ${ prompt . description } " ` ,
'---'
] . 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 ) => [
'---' ,
` description: "Plan2Code ${ prompt . stepNumber === 'init' ? 'Init' : ` Step ${ prompt . stepNumber } ` } : ${ prompt . displayName } - ${ prompt . description } " ` ,
'---'
] . join ( '\n' )
} ,
2026-02-22 12:54:33 -08:00
{
name : 'Claude Code Skills' ,
dir : '.claude/skills' ,
type : 'skill' ,
header : ( prompt ) => generateSkillHeader ( prompt , true ) ,
} ,
{
name : 'Agent Skills (Amp · Gemini CLI · OpenCode)' ,
dir : '.agents/skills' ,
type : 'skill' ,
header : ( prompt ) => generateSkillHeader ( prompt , false ) ,
} ,
{
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 ( ) ;
if ( process . platform === 'win32' ) {
const localAppData = process . env . LOCALAPPDATA || path . join ( homedir , 'AppData' , 'Local' ) ;
return path . join ( localAppData , 'agents' , 'skills' ) ;
}
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
} ,
{
name : 'Agent Skills (Amp · Gemini CLI · OpenCode)' ,
id : 'agent-skills' ,
dir : getAgentSkillsDir ,
sourceDir : '.agents/skills' ,
type : 'skill' ,
filePattern : /^plan2code-/ ,
displayPath : process . platform === 'win32'
? '%LOCALAPPDATA%\\agents\\skills'
: '~/.agents/skills' ,
icon : '◉ '
} ,
{
name : 'Crush' ,
id : 'crush' ,
dir : getCrushSkillsDir ,
sourceDir : 'crush-skills' ,
type : 'skill' ,
filePattern : /^plan2code-/ ,
displayPath : process . platform === 'win32'
? '%LOCALAPPDATA%\\crush\\skills'
: '~/.config/crush/skills' ,
icon : '◉ '
} ,
{
name : 'Gemini CLI' ,
id : 'gemini' ,
dir : '.gemini/commands' ,
type : 'toml' ,
filePattern : /^plan2code-.*\.toml$/ ,
icon : '◉ '
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 {
const files = fs . readdirSync ( dirPath ) ;
for ( const file of files ) {
const filePath = path . join ( dirPath , file ) ;
const stat = fs . statSync ( filePath ) ;
if ( stat . isDirectory ( ) ) {
deleteDirectory ( filePath ) ;
} else {
fs . unlinkSync ( filePath ) ;
}
}
fs . rmdirSync ( dirPath ) ;
} 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 ++ ;
}
}
/**
* 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 ;
}
// Write to dist/local-commands/
for ( const dest of LOCAL _DESTINATIONS ) {
2026-02-22 12:54:33 -08:00
if ( dest . type === 'skill' ) {
writeSkillToDestination ( projectRoot , 'dist/local-commands' , dest , prompt , sourceContent , stats , quiet ) ;
} else if ( dest . type === 'toml' ) {
writeTomlToDestination ( projectRoot , 'dist/local-commands' , dest , prompt , sourceContent , stats , quiet ) ;
} else {
writeToDestination ( projectRoot , 'dist/local-commands' , dest , prompt , sourceContent , stats , quiet ) ;
}
2025-12-23 15:42:35 -08:00
}
// Write to dist/global-commands/
for ( const dest of GLOBAL _DESTINATIONS ) {
2026-02-22 12:54:33 -08:00
if ( dest . type === 'skill' ) {
writeSkillToDestination ( projectRoot , 'dist/global-commands' , dest , prompt , sourceContent , stats , quiet ) ;
} else if ( dest . type === 'toml' ) {
writeTomlToDestination ( projectRoot , 'dist/global-commands' , dest , prompt , sourceContent , stats , quiet ) ;
} else {
writeToDestination ( projectRoot , 'dist/global-commands' , dest , prompt , sourceContent , stats , quiet ) ;
}
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 ) {
fs . copyFileSync ( path . join ( srcSubdir , file ) , path . join ( destSubdir , file ) ) ;
stats . copied ++ ;
}
}
}
/**
* Remove files/dirs matching LEGACY_TARGETS patterns (silent cleanup)
*/
function cleanLegacyTargets ( ) {
const homedir = os . homedir ( ) ;
for ( const target of LEGACY _TARGETS ) {
const targetDir = path . join ( homedir , target . dir ) ;
if ( ! fs . existsSync ( targetDir ) ) continue ;
const entries = fs . readdirSync ( targetDir ) ;
for ( const entry of entries ) {
if ( ! target . filePattern . test ( entry ) ) continue ;
const entryPath = path . join ( targetDir , entry ) ;
if ( target . type === 'skill' ) {
fs . rmSync ( entryPath , { recursive : true , force : true } ) ;
} else {
fs . unlinkSync ( entryPath ) ;
}
}
}
}
2025-12-23 15:42:35 -08:00
/**
* Execute installation
*/
function install ( targets = null ) {
const projectRoot = _ _dirname ;
const homeDir = os . homedir ( ) ;
const targetList = targets || getTargets ( ) ;
let totalInstalled = 0 ;
let totalSkipped = 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 } ` ) ;
}
}
}
} 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 ++ ;
}
}
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 } ` ) ;
if ( totalSkipped > 0 ) {
console . log ( padEndVisible ( ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } Files Skipped: ${ COLORS . YELLOW } ${ totalSkipped } ${ 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 } ` ) ;
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 ) {
console . log ( ` ${ COLORS . DIM } ${ SYMBOLS . INFO } No Plan2Code files found - nothing to remove \n ${ COLORS . RESET } ` ) ;
continue ;
}
2026-02-22 12:54:33 -08:00
// Remove files/skill directories
2025-12-23 15:42:35 -08:00
console . log ( ` ${ COLORS . CYAN } ${ SYMBOLS . ACTIVE } REMOVING ${ COLORS . RESET } ${ files . length } file(s)... ` ) ;
for ( const file of files ) {
const destFile = path . join ( destPath , file ) ;
try {
2026-02-22 12:54:33 -08:00
if ( target . type === 'skill' ) {
fs . rmSync ( destFile , { recursive : true , force : true } ) ;
} else {
2025-12-23 15:42:35 -08:00
fs . unlinkSync ( destFile ) ;
}
console . log ( ` ${ COLORS . GREEN } ▰▰▰ ${ COLORS . RESET } Removed: ${ file } ` ) ;
totalRemoved ++ ;
} catch ( err ) {
console . log ( ` ${ COLORS . RED } ✖✖✖ ${ COLORS . RESET } ${ file } : ${ err . message } ` ) ;
totalErrors ++ ;
}
}
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 } ` ) ;
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-02-22 12:54:33 -08:00
// Task 2.2: Input prompt
console . log ( '' ) ;
const answer = await question ( ` ${ COLORS . CYAN } ${ SYMBOLS . SELECT } SELECT OPTION ${ COLORS . RESET } (I, U, C, Q) [I]: ` ) ;
const input = answer . trim ( ) . toUpperCase ( ) || 'I' ;
2026-02-18 15:16:11 -08:00
2026-02-22 12:54:33 -08:00
// Task 2.3: I path — Install All
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-02-22 19:57:40 -08:00
const metricsResult = installPlan2CodeMetrics ( ) ;
console . log ( '' ) ;
2026-02-22 12:54:33 -08:00
const installResult = install ( ) ;
2026-02-22 19:57:40 -08:00
const exitCode = loopResult !== 0 ? loopResult : metricsResult !== 0 ? metricsResult : installResult ;
process . exit ( exitCode ) ;
2026-01-27 12:11:00 -08:00
}
2025-12-23 15:42:35 -08:00
2026-02-22 12:54:33 -08:00
// Task 2.4: 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 ( ) ;
const exitCode = uninstallResult !== 0 ? uninstallResult : loopResult !== 0 ? loopResult : metricsResult ;
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-02-22 12:54:33 -08:00
// Task 2.5: C path — CUSTOM sub-menu
if ( input === 'C' ) {
console . log ( '' ) ;
console . log ( ` ${ COLORS . CYAN } ╔════════════════════════════════════════════════════════════════╗ ${ COLORS . RESET } ` ) ;
console . log ( padEndVisible ( ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } ${ COLORS . BRIGHT } CUSTOM OPTIONS ${ COLORS . RESET } ` , 65 ) + ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } ` ) ;
console . log ( ` ${ COLORS . CYAN } ╠════════════════════════════════════════════════════════════════╣ ${ COLORS . RESET } ` ) ;
console . log ( padEndVisible ( ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } ${ COLORS . BRIGHT } L. ${ COLORS . RESET } ${ COLORS . BLUE } LOCAL ${ COLORS . RESET } Show local (project) install instructions ` , 65 ) + ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } ` ) ;
console . log ( padEndVisible ( ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } ${ COLORS . BRIGHT } O. ${ COLORS . RESET } ${ COLORS . GREEN } LOOP CLI ${ COLORS . RESET } Install plan2code-loop CLI only ` , 65 ) + ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } ` ) ;
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-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-02-22 19:57:40 -08:00
const customAnswer = await question ( ` ${ COLORS . CYAN } ${ SYMBOLS . SELECT } SELECT OPTION ${ COLORS . RESET } (L, O, M, 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-02-22 12:54:33 -08:00
// Task 2.6: C > L — show local install instructions
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-02-22 12:54:33 -08:00
// Task 2.7: C > O — install loop CLI only
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-02-22 12:54:33 -08:00
// Task 2.8: C > Q — back to main menu
return main ( ) ;
2025-12-23 15:42:35 -08:00
}
2026-02-22 12:54:33 -08:00
// Task 2.8: Q — exit
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-02-22 12:54:33 -08:00
// Task 2.8: Invalid input — loop back
console . log ( ` \n ${ COLORS . RED } ${ SYMBOLS . ERROR } Invalid option. Please choose I, U, C, or Q. ${ COLORS . RESET } \n ` ) ;
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-01-27 12:11:00 -08:00
const { execSync , spawn } = 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 ) ;
// File exists, try to remove it
try {
// Try rmdirSync first for junctions/symlinks
fs . rmdirSync ( file . path ) ;
console . log ( ` ${ COLORS . GREEN } ${ SYMBOLS . SUCCESS } ${ COLORS . RESET } Removed: ${ file . name } ` ) ;
removedCount ++ ;
} catch ( rmdirErr ) {
// Fall back to unlinkSync for regular files
try {
fs . unlinkSync ( file . path ) ;
console . log ( ` ${ COLORS . GREEN } ${ SYMBOLS . SUCCESS } ${ COLORS . RESET } Removed: ${ file . name } ` ) ;
removedCount ++ ;
} catch ( unlinkErr ) {
// Last resort: rmSync
fs . rmSync ( file . path , { force : true , recursive : true } ) ;
console . log ( ` ${ COLORS . GREEN } ${ SYMBOLS . SUCCESS } ${ COLORS . RESET } Removed: ${ file . name } ` ) ;
removedCount ++ ;
}
}
} catch ( err ) {
// File doesn't exist, skip it
}
}
if ( removedCount === 0 ) {
console . log ( ` ${ COLORS . DIM } ${ SYMBOLS . INFO } No files found to remove ${ COLORS . RESET } ` ) ;
}
return true ;
} catch ( error ) {
console . log ( ` ${ COLORS . RED } ${ SYMBOLS . ERROR } ${ COLORS . RESET } Uninstall failed: ${ error . message } ` ) ;
return false ;
}
}
/**
* Install plan2code-loop (build + symlink)
*/
function installPlan2CodeLoop ( ) {
displaySectionHeader ( 'PLAN2CODE-LOOP' , '[ BUILD & INSTALL ]' ) ;
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 ) ;
try {
fs . rmdirSync ( file . path ) ;
console . log ( ` ${ COLORS . GREEN } ${ SYMBOLS . SUCCESS } ${ COLORS . RESET } Removed: ${ file . name } ` ) ;
removedCount ++ ;
} catch ( rmdirErr ) {
try {
fs . unlinkSync ( file . path ) ;
console . log ( ` ${ COLORS . GREEN } ${ SYMBOLS . SUCCESS } ${ COLORS . RESET } Removed: ${ file . name } ` ) ;
removedCount ++ ;
} catch ( unlinkErr ) {
fs . rmSync ( file . path , { force : true , recursive : true } ) ;
console . log ( ` ${ COLORS . GREEN } ${ SYMBOLS . SUCCESS } ${ COLORS . RESET } Removed: ${ file . name } ` ) ;
removedCount ++ ;
}
}
} catch ( err ) {
// File doesn't exist, skip it
}
}
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 ;
}
2025-12-23 15:42:35 -08:00
// ============================================================================
// MAIN
// ============================================================================
2026-02-22 12:54:33 -08:00
// Always run interactive menu
runInteractive ( ) ;