2025-12-23 15:42:35 -08:00
#!/usr/bin/env node
/**
* ██████╗ ██╗ █████╗ ███╗ ██╗██████╗ ██████╗ ██████╗ ██████╗ ███████╗
* ██╔══██╗██║ ██╔══██╗████╗ ██║╚════██╗██╔════╝██╔═══██╗██╔══██╗██╔════╝
* ██████╔╝██║ ███████║██╔██╗ ██║ █████╔╝██║ ██║ ██║██║ ██║█████╗
* ██╔═══╝ ██║ ██╔══██║██║╚██╗██║██╔═══╝ ██║ ██║ ██║██║ ██║██╔══╝
* ██║ ███████╗██║ ██║██║ ╚████║███████╗╚██████╗╚██████╔╝██████╔╝███████╗
* ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝
*
* GLOBAL INSTALLATION SYSTEM
*
* DESCRIPTION:
2026-08-20 13:42:31 -07:00
* Install Plan2Code skills globally or into the current project via skills.sh.
* Builds the committed skills/ artifact from source prompts in src/.
* Supports non-interactive --build-skills and --verify-skills maintenance hooks.
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
2026-08-20 13:42:31 -07:00
// Generated skills directory — the committed, canonical Agent Skills build of src/.
// `skills add` consumes this directory; nothing else is platform-specific any more.
const SKILLS _DIR _NAME = 'skills' ;
const SKILLS _DIR = path . join ( _ _dirname , SKILLS _DIR _NAME ) ;
2025-12-23 15:42:35 -08:00
// 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-08-08 12:39:13 -07:00
{
source : 'plan2code-0-pathfinder.md' ,
stepNumber : '0' ,
name : 'pathfinder' ,
displayName : 'Pathfinder Mode' ,
description : 'charting of a foggy idea as a map of decision questions, cleared one at a time'
} ,
2025-12-23 15:42:35 -08:00
{
2026-05-23 06:58:20 -07:00
source : 'plan2code-quick-task.md' ,
2026-08-08 12:39:13 -07:00
stepNumber : 'quick' ,
2025-12-23 15:42:35 -08:00
name : 'quick-task' ,
displayName : 'Quick Task Mode' ,
2026-08-08 12:39:13 -07:00
description : 'Lightweight planning for small tasks' ,
isUtility : true
2025-12-23 15:42:35 -08:00
} ,
{
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'
2026-07-20 22:31:37 -07:00
} ,
{
source : 'plan2code-handoff.md' ,
stepNumber : 'handoff' ,
name : 'handoff' ,
displayName : 'Handoff Mode' ,
description : 'Compact the conversation into a self-contained handoff document for a fresh session' ,
isUtility : true
2025-12-23 15:42:35 -08:00
}
] ;
// 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' ;
2026-07-20 22:36:35 -07:00
if ( prompt . stepNumber === 'update' ) return 'Update' ;
2026-06-01 16:39:35 -07:00
if ( prompt . stepNumber === 'review' ) return 'Review' ;
2026-07-20 22:31:37 -07:00
if ( prompt . stepNumber === 'handoff' ) return 'Handoff' ;
2026-08-08 12:39:13 -07:00
if ( prompt . stepNumber === 'quick' ) return 'Quick Task' ;
2026-06-01 16:39:35 -07:00
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' ) ;
}
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' ) ;
}
2025-12-23 15:42:35 -08:00
// 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-08-20 13:42:31 -07:00
// Legacy install paths — directories that pre-2.2 installs wrote Plan2Code files into.
// 2.2.0 delegates all distribution to the `skills` CLI, so these are cleanup-only: install and
// uninstall both sweep them so files from earlier versions don't linger and shadow the new
// skills. Do NOT add entries here as install targets; the `skills` CLI owns installation.
//
// `dir` is either a path relative to the home directory or a function returning an absolute path.
// `type: 'skill'` entries are directories (removed recursively); the rest are flat files.
const LEGACY _PATHS = [
// Claude Code — pre-v2 slash commands
{ name : 'Claude Code commands' , dir : '.claude/commands' , filePattern : /^plan2code-.*\.md$/ } ,
// Copilot CLI agents
{ name : 'Copilot CLI agents' , dir : '.copilot/agents' , filePattern : /^plan2code-.*\.md$/ } ,
// Cursor slash commands
{ name : 'Cursor commands' , dir : '.cursor/commands' , filePattern : /^plan2code-.*\.md$/ } ,
// Continue prompts
{ name : 'Continue prompts' , dir : '.continue/prompts' , filePattern : /^plan2code-.*\.prompt\.md$/ } ,
// Windsurf global workflows
{ name : 'Windsurf workflows' , dir : '.codeium/windsurf/global_workflows' , filePattern : /^plan2code-.*\.md$/ } ,
// Codeium (IntelliJ) global workflows
{ name : 'Codeium workflows' , dir : '.codeium/global_workflows' , filePattern : /^plan2code-.*\.md$/ } ,
// Pi prompts
{ name : 'Pi prompts' , dir : '.pi/agent/prompts' , filePattern : /^plan2code-.*\.md$/ } ,
// Gemini CLI — removed as a target in v2.0.0, still cleaned up
{ name : 'Gemini CLI commands' , dir : '.gemini/commands' , filePattern : /^plan2code-.*\.toml$/ } ,
// VS Code Copilot prompts (platform-specific location)
{ name : 'VS Code Copilot prompts' , dir : getVSCodeCopilotDir , filePattern : /^plan2code-.*\.prompt\.md$/ } ,
// Skill directories that pre-2.2 installs wrote as real copies. The skills CLI owns these
// paths now (~/.agents/skills is its store; the rest are links into it), but it won't
// recognize hand-copied directories from an earlier version, so sweep them too. Safe to run
// before `skills add`, which recreates whatever it needs.
{ name : 'Crush skills' , dir : getCrushSkillsDir , filePattern : /^plan2code-/ , type : 'skill' } ,
{ name : 'Claude Code skills' , dir : '.claude/skills' , filePattern : /^plan2code-/ , type : 'skill' } ,
{ name : 'Agent skills store' , dir : '.agents/skills' , filePattern : /^plan2code-/ , type : 'skill' } ,
2026-02-22 12:54:33 -08:00
] ;
2026-08-20 13:42:31 -07:00
// Reference directories that flat-file targets received as siblings (e.g. plan2code-review-references/).
const LEGACY _REFERENCE _DIR = /^plan2code-.*-references$/ ;
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 ║' ) ;
2026-08-08 12:39:13 -07:00
console . log ( '║ https://github.com/jparkerweb/plan2code ║' ) ;
2026-02-22 12:54:33 -08:00
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 } ` ) ;
}
}
2026-08-20 13:42:31 -07:00
// ============================================================================
// SKILLS BUILD (src/ -> skills/)
// ============================================================================
2025-12-23 15:42:35 -08:00
2026-08-20 13:42:31 -07:00
function listFilesRecursive ( dir , prefix = '' ) {
const out = [ ] ;
for ( const entry of fs . readdirSync ( dir ) ) {
const abs = path . join ( dir , entry ) ;
const rel = prefix ? path . join ( prefix , entry ) : entry ;
if ( fs . statSync ( abs ) . isDirectory ( ) ) out . push ( ... listFilesRecursive ( abs , rel ) ) ;
else out . push ( rel ) ;
2025-12-23 15:42:35 -08:00
}
2026-08-20 13:42:31 -07:00
return out ;
}
function computeExpectedSkills ( ) {
const expected = new Map ( ) ;
const errors = [ ] ;
2025-12-23 15:42:35 -08:00
2026-08-20 13:42:31 -07:00
for ( const prompt of SOURCE _PROMPTS ) {
const sourcePath = path . join ( SRC _DIR , prompt . source ) ;
let sourceContent ;
2025-12-23 15:42:35 -08:00
try {
2026-08-20 13:42:31 -07:00
sourceContent = fs . readFileSync ( sourcePath , 'utf8' ) ;
2025-12-23 15:42:35 -08:00
} catch ( err ) {
2026-08-20 13:42:31 -07:00
errors . push ( ` Could not read src/ ${ prompt . source } : ${ err . message } ` ) ;
continue ;
2025-12-23 15:42:35 -08:00
}
2026-08-20 13:42:31 -07:00
const skillName = generateSkillName ( prompt ) ;
expected . set (
` ${ SKILLS _DIR _NAME } / ${ skillName } /SKILL.md ` ,
generateSkillHeader ( prompt , true ) + '\n\n' + sourceContent
) ;
const srcRefDir = path . join ( SRC _DIR , prompt . source . replace ( /\.md$/ , '-references' ) ) ;
if ( ! fs . existsSync ( srcRefDir ) ) continue ;
for ( const relPath of listFilesRecursive ( srcRefDir ) ) {
expected . set (
` ${ SKILLS _DIR _NAME } / ${ skillName } /references/ ${ relPath . split ( path . sep ) . join ( '/' ) } ` ,
fs . readFileSync ( path . join ( srcRefDir , relPath ) , 'utf8' )
) ;
2026-02-22 12:54:33 -08:00
}
}
2026-08-20 13:42:31 -07:00
return { expected , errors } ;
2026-02-22 12:54:33 -08:00
}
2026-08-20 13:42:31 -07:00
function buildSkills ( quiet = false ) {
const { expected , errors } = computeExpectedSkills ( ) ;
const stats = { written : 0 , unchanged : 0 , pruned : 0 , errors : errors . length } ;
for ( const message of errors ) console . error ( ` ${ COLORS . RED } ${ SYMBOLS . ERROR } ${ COLORS . RESET } ${ message } ` ) ;
if ( ! quiet ) console . log ( ` ${ COLORS . CYAN } ${ SYMBOLS . ACTIVE } BUILDING SKILLS ${ COLORS . RESET } ${ COLORS . DIM } src/ -> ${ SKILLS _DIR _NAME } / ${ COLORS . RESET } \n ` ) ;
const expectedSkillNames = new Set ( SOURCE _PROMPTS . map ( generateSkillName ) ) ;
if ( fs . existsSync ( SKILLS _DIR ) ) {
for ( const entry of fs . readdirSync ( SKILLS _DIR ) ) {
const entryPath = path . join ( SKILLS _DIR , entry ) ;
if ( ! fs . statSync ( entryPath ) . isDirectory ( ) || expectedSkillNames . has ( entry ) ) continue ;
deleteDirectory ( entryPath ) ;
stats . pruned ++ ;
if ( ! quiet ) console . log ( ` ${ COLORS . YELLOW } ░░░ ${ COLORS . RESET } Pruned: ${ entry } / ` ) ;
}
2026-02-22 12:54:33 -08:00
}
2026-08-20 13:42:31 -07:00
for ( const skillName of expectedSkillNames ) {
const skillDir = path . join ( SKILLS _DIR , skillName ) ;
if ( ! fs . existsSync ( skillDir ) ) continue ;
for ( const relPath of listFilesRecursive ( skillDir ) ) {
const key = ` ${ SKILLS _DIR _NAME } / ${ skillName } / ${ relPath . split ( path . sep ) . join ( '/' ) } ` ;
if ( expected . has ( key ) ) continue ;
fs . rmSync ( path . join ( skillDir , relPath ) , { force : true } ) ;
stats . pruned ++ ;
if ( ! quiet ) console . log ( ` ${ COLORS . YELLOW } ░░░ ${ COLORS . RESET } Pruned: ${ key } ` ) ;
2026-02-22 12:54:33 -08:00
}
}
2026-08-20 13:42:31 -07:00
for ( const [ relPath , content ] of expected ) {
const absPath = path . join ( _ _dirname , relPath . split ( '/' ) . join ( path . sep ) ) ;
let current = null ;
if ( fs . existsSync ( absPath ) ) {
try { current = fs . readFileSync ( absPath , 'utf8' ) ; } catch { }
}
if ( current === content ) {
stats . unchanged ++ ;
continue ;
}
2026-02-22 12:54:33 -08:00
try {
2026-08-20 13:42:31 -07:00
fs . mkdirSync ( path . dirname ( absPath ) , { recursive : true } ) ;
fs . writeFileSync ( absPath , content , 'utf8' ) ;
stats . written ++ ;
if ( ! quiet ) console . log ( ` ${ COLORS . GREEN } ▰▰▰ ${ COLORS . RESET } ${ relPath } ` ) ;
2026-02-22 12:54:33 -08:00
} catch ( err ) {
2026-08-20 13:42:31 -07:00
console . error ( ` ${ COLORS . RED } ✖✖✖ ${ COLORS . RESET } ${ relPath } : ${ err . message } ` ) ;
2026-02-22 12:54:33 -08:00
stats . errors ++ ;
}
}
2026-08-20 13:42:31 -07:00
if ( ! quiet ) {
console . log ( ` \n ${ COLORS . GREEN } ${ SYMBOLS . SUCCESS } ${ COLORS . RESET } ${ SOURCE _PROMPTS . length } skill(s): ${ stats . written } written, ${ stats . unchanged } unchanged, ${ stats . pruned } pruned ` ) ;
if ( stats . errors > 0 ) console . log ( ` ${ COLORS . RED } ${ SYMBOLS . ERROR } ${ COLORS . RESET } ${ stats . errors } error(s) ` ) ;
console . log ( '' ) ;
2026-05-23 06:58:20 -07:00
}
2026-08-20 13:42:31 -07:00
return stats . errors === 0 ;
2026-05-23 06:58:20 -07:00
}
2026-08-20 13:42:31 -07:00
function verifySkillsSync ( ) {
const { expected , errors } = computeExpectedSkills ( ) ;
const problems = [ ... errors ] ;
for ( const [ relPath , content ] of expected ) {
const absPath = path . join ( _ _dirname , relPath . split ( '/' ) . join ( path . sep ) ) ;
if ( ! fs . existsSync ( absPath ) ) problems . push ( ` missing: ${ relPath } ` ) ;
else if ( fs . readFileSync ( absPath , 'utf8' ) !== content ) problems . push ( ` out of date: ${ relPath } ` ) ;
}
if ( fs . existsSync ( SKILLS _DIR ) ) {
for ( const relPath of listFilesRecursive ( SKILLS _DIR ) ) {
const key = ` ${ SKILLS _DIR _NAME } / ${ relPath . split ( path . sep ) . join ( '/' ) } ` ;
if ( ! expected . has ( key ) ) problems . push ( ` unexpected: ${ key } ` ) ;
2026-05-23 06:58:20 -07:00
}
}
2026-08-20 13:42:31 -07:00
if ( problems . length > 0 ) {
console . error ( ` ${ SKILLS _DIR _NAME } / is out of sync with src/: \n ` ) ;
for ( const problem of problems ) console . error ( ` ${ problem } ` ) ;
console . error ( ` \n ${ problems . length } problem(s). Run 'npm run build:skills' and commit the result. ` ) ;
return false ;
}
console . log ( ` ${ SKILLS _DIR _NAME } / matches src/ — ${ expected . size } file(s) across ${ SOURCE _PROMPTS . length } skill(s) ✓ ` ) ;
return true ;
2026-05-23 06:58:20 -07:00
}
2026-08-20 13:42:31 -07:00
function cleanLegacyPaths ( quiet = false ) {
const stats = { removed : 0 , errors : 0 } ;
for ( const legacy of LEGACY _PATHS ) {
const dir = typeof legacy . dir === 'function' ? legacy . dir ( ) : path . join ( os . homedir ( ) , legacy . dir ) ;
if ( ! fs . existsSync ( dir ) ) continue ;
let entries ;
try { entries = fs . readdirSync ( dir ) ; } catch { continue ; }
for ( const entry of entries ) {
const isMatch = legacy . filePattern . test ( entry ) ;
const isReferenceDir = LEGACY _REFERENCE _DIR . test ( entry ) ;
if ( ! isMatch && ! isReferenceDir ) continue ;
try {
const entryPath = path . join ( dir , entry ) ;
if ( isReferenceDir || legacy . type === 'skill' || fs . statSync ( entryPath ) . isDirectory ( ) ) {
fs . rmSync ( entryPath , { recursive : true , force : true } ) ;
} else {
fs . unlinkSync ( entryPath ) ;
}
stats . removed ++ ;
if ( ! quiet ) console . log ( ` ${ COLORS . YELLOW } ░░░ ${ COLORS . RESET } ${ legacy . name } : removed ${ entry } ` ) ;
} catch ( err ) {
stats . errors ++ ;
if ( ! quiet ) console . log ( ` ${ COLORS . RED } ✖✖✖ ${ COLORS . RESET } ${ legacy . name } : could not remove ${ entry } — ${ err . message } ` ) ;
}
}
2025-12-23 15:42:35 -08:00
}
2026-08-20 13:42:31 -07:00
return stats ;
}
2025-12-23 15:42:35 -08:00
2026-08-20 13:42:31 -07:00
// ============================================================================
// SKILLS CLI DELEGATION
// ============================================================================
2025-12-23 15:42:35 -08:00
2026-08-20 13:42:31 -07:00
const SKILLS _CLI = 'npx --yes skills' ;
const BENIGN _SKILLS _FAILURE = /does not support (global|project) skill installation/i ;
2025-12-23 15:42:35 -08:00
2026-08-20 13:42:31 -07:00
function execSkillsCli ( args ) {
const options = {
encoding : 'utf8' ,
stdio : [ 'pipe' , 'pipe' , 'pipe' ] ,
env : { ... process . env , npm _config _loglevel : 'error' } ,
} ;
try {
return { ok : true , output : String ( execSync ( ` ${ SKILLS _CLI } ${ args } ` , options ) ) . trim ( ) } ;
} catch ( err ) {
return { ok : false , output : ` ${ err . stdout || '' } \n ${ err . stderr || '' } ` . trim ( ) } ;
2025-12-23 15:42:35 -08:00
}
2026-08-20 13:42:31 -07:00
}
2025-12-23 15:42:35 -08:00
2026-08-20 13:42:31 -07:00
function runSkillsCli ( args ) {
const result = execSkillsCli ( args ) ;
return result . ok ? result . output : null ;
}
2026-05-23 06:58:20 -07:00
2026-08-20 13:42:31 -07:00
function skillNameArgs ( ) {
return ` -s ${ SOURCE _PROMPTS . map ( generateSkillName ) . join ( ' ' ) } ` ;
}
2026-05-23 06:58:20 -07:00
2026-08-20 13:42:31 -07:00
function extractSkillsFailures ( output ) {
const failures = [ ] ;
for ( const rawLine of stripAnsi ( output ) . split ( '\n' ) ) {
if ( ! rawLine . includes ( '✗' ) ) continue ;
const message = rawLine . slice ( rawLine . indexOf ( '✗' ) + 1 ) . trim ( ) ;
if ( message && ! BENIGN _SKILLS _FAILURE . test ( message ) ) failures . push ( message ) ;
2025-12-23 15:42:35 -08:00
}
2026-08-20 13:42:31 -07:00
return failures ;
}
2025-12-23 15:42:35 -08:00
2026-08-20 13:42:31 -07:00
function reportSkillsOutput ( result ) {
if ( ! result . ok ) {
console . log ( ` \n ${ stripAnsi ( result . output ) } ` ) ;
return ;
}
for ( const failure of extractSkillsFailures ( result . output ) ) {
console . log ( ` ${ COLORS . YELLOW } ${ SYMBOLS . WARNING } ${ COLORS . RESET } ${ failure } ` ) ;
2025-12-23 15:42:35 -08:00
}
}
2026-08-20 13:42:31 -07:00
function ensureSkillsCli ( ) {
const version = runSkillsCli ( '--version' ) ;
if ( version !== null ) {
console . log ( ` ${ COLORS . GREEN } ${ SYMBOLS . SUCCESS } ${ COLORS . RESET } skills CLI ${ COLORS . DIM } ${ version . split ( '\n' ) . pop ( ) } ${ COLORS . RESET } ` ) ;
return true ;
}
console . log ( ` ${ COLORS . RED } ${ SYMBOLS . ERROR } ${ COLORS . RESET } Could not run the ${ COLORS . BRIGHT } skills ${ COLORS . RESET } CLI. ` ) ;
console . log ( ` \n ${ COLORS . DIM } Plan2Code installs through skills.sh, which needs Node 18+ and network access. ${ COLORS . RESET } ` ) ;
console . log ( ` ${ COLORS . DIM } Check that this works, then re-run the installer: ${ COLORS . RESET } ` ) ;
console . log ( ` ${ COLORS . CYAN } npx --yes skills --version ${ COLORS . RESET } \n ` ) ;
return false ;
2025-12-23 15:42:35 -08:00
}
2026-08-20 13:42:31 -07:00
function listInstalledSkills ( scope ) {
const json = runSkillsCli ( ` list ${ scope === 'global' ? '-g' : '' } --json ` . trim ( ) ) ;
if ( ! json ) return [ ] ;
try {
const parsed = JSON . parse ( json ) ;
if ( ! Array . isArray ( parsed ) ) return [ ] ;
return parsed . map ( skill => skill && skill . name )
. filter ( name => typeof name === 'string' && name . startsWith ( 'plan2code-' ) ) ;
} catch {
return [ ] ;
2026-02-22 12:54:33 -08:00
}
}
2026-08-20 13:42:31 -07:00
function removeInstalledSkills ( scope , { quiet = false } = { } ) {
const installed = listInstalledSkills ( scope ) ;
if ( installed . length === 0 ) {
if ( ! quiet ) console . log ( ` ${ COLORS . DIM } ${ SYMBOLS . INFO } No Plan2Code skills currently installed ${ COLORS . RESET } ` ) ;
return 0 ;
}
const scopeFlag = scope === 'global' ? '-g ' : '' ;
let removed = 0 ;
for ( const name of installed ) {
if ( runSkillsCli ( ` remove ${ name } ${ scopeFlag } -y ` ) === null ) {
if ( ! quiet ) console . log ( ` ${ COLORS . RED } ✖✖✖ ${ COLORS . RESET } Could not remove ${ name } ` ) ;
continue ;
2026-05-23 06:58:20 -07:00
}
2026-08-20 13:42:31 -07:00
removed ++ ;
if ( ! quiet ) console . log ( ` ${ COLORS . YELLOW } ░░░ ${ COLORS . RESET } Removed: ${ name } ` ) ;
2026-02-22 12:54:33 -08:00
}
2026-08-20 13:42:31 -07:00
return removed ;
2026-02-22 12:54:33 -08:00
}
2026-08-20 13:42:31 -07:00
// ============================================================================
// INSTALLATION
// ============================================================================
2025-12-23 15:42:35 -08:00
/**
* Execute installation
*/
2026-08-20 13:42:31 -07:00
async function install ( ) {
displaySectionHeader ( ' INSTALLING SKILLS ' ) ;
if ( ! buildSkills ( true ) ) {
console . log ( ` ${ COLORS . RED } ${ SYMBOLS . ERROR } Failed to build ${ SKILLS _DIR _NAME } / from src/ ${ COLORS . RESET } ` ) ;
2025-12-23 15:42:35 -08:00
return 1 ;
}
2026-08-20 13:42:31 -07:00
console . log ( ` ${ COLORS . GREEN } ${ SYMBOLS . SUCCESS } Built ${ SOURCE _PROMPTS . length } skills from src/ ${ COLORS . RESET } ` ) ;
if ( ! ensureSkillsCli ( ) ) return 1 ;
2026-02-22 12:54:33 -08:00
2025-12-23 15:42:35 -08:00
console . log ( '' ) ;
displayStatusBox ( 'PARAMETERS' , [
2026-08-20 13:42:31 -07:00
` ${ COLORS . CYAN } Source: ${ COLORS . RESET } ${ SKILLS _DIR _NAME } / ` ,
` ${ COLORS . CYAN } Skills: ${ COLORS . RESET } ${ SOURCE _PROMPTS . length } ` ,
` ${ COLORS . CYAN } Scope: ${ COLORS . RESET } GLOBAL ${ COLORS . DIM } (skills.sh default agent set) ${ COLORS . RESET } ` ,
` ${ COLORS . CYAN } Store: ${ COLORS . RESET } ${ path . join ( os . homedir ( ) , '.agents' , 'skills' ) } ` ,
2026-02-22 12:54:33 -08:00
` ${ COLORS . CYAN } Mode: ${ COLORS . RESET } INSTALL ` ,
2025-12-23 15:42:35 -08:00
] ) ;
2026-08-20 13:42:31 -07:00
console . log ( ` \n ${ COLORS . YELLOW } ${ SYMBOLS . ACTIVE } CLEANING PREVIOUS INSTALLS (PLEASE WAIT ⌛) ${ COLORS . RESET } \n ` ) ;
removeInstalledSkills ( 'global' ) ;
const legacy = cleanLegacyPaths ( ) ;
if ( legacy . removed > 0 ) console . log ( ` ${ COLORS . GREEN } ${ SYMBOLS . SUCCESS } ${ COLORS . RESET } Cleaned ${ legacy . removed } legacy file(s) from pre-2.2 installs ` ) ;
console . log ( ` \n ${ COLORS . YELLOW } ${ SYMBOLS . ACTIVE } INSTALLING VIA SKILLS.SH ${ COLORS . RESET } \n ` ) ;
console . log ( ` ${ COLORS . DIM } Installing ${ SOURCE _PROMPTS . length } skills... ${ COLORS . RESET } ` ) ;
const result = execSkillsCli ( ` add " ${ SKILLS _DIR } " -g ${ skillNameArgs ( ) } -y ` ) ;
reportSkillsOutput ( result ) ;
const failed = ! result . ok ;
console . log ( ` \n ${ COLORS . CYAN } ╔═══════════════════════════════════════════════════════════════════════════╗ ${ COLORS . RESET } ` ) ;
2025-12-23 15:42:35 -08:00
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 } ` ) ;
2026-08-20 13:42:31 -07:00
const statusColor = failed ? COLORS . RED : COLORS . GREEN ;
const statusText = failed ? 'FAILED' : 'SUCCESS' ;
2025-12-23 15:42:35 -08:00
console . log ( padEndVisible ( ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } ${ statusColor } Status: ${ COLORS . RESET } ${ statusColor } ${ statusText } ${ COLORS . RESET } ` , 76 ) + ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } ` ) ;
2026-08-20 13:42:31 -07:00
const installedNames = failed ? [ ] : listInstalledSkills ( 'global' ) ;
console . log ( padEndVisible ( ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } Skills Installed: ${ COLORS . GREEN } ${ installedNames . length } ${ COLORS . RESET } ` , 76 ) + ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } ` ) ;
2025-12-23 15:42:35 -08:00
console . log ( ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } ${ ' ' . repeat ( 75 ) } ${ COLORS . CYAN } ║ ${ COLORS . RESET } ` ) ;
2026-08-20 13:42:31 -07:00
console . log ( ` ${ COLORS . CYAN } ╚═══════════════════════════════════════════════════════════════════════════╝ ${ COLORS . RESET } \n ` ) ;
2025-12-23 15:42:35 -08:00
2026-08-20 13:42:31 -07:00
if ( failed ) {
console . log ( ` ${ COLORS . RED } ${ SYMBOLS . ERROR } skills add failed. Re-run, or install by hand: ${ COLORS . RESET } ` ) ;
console . log ( ` ${ COLORS . CYAN } npx --yes skills add " ${ SKILLS _DIR } " -g ${ skillNameArgs ( ) } -y ${ COLORS . RESET } \n ` ) ;
return 1 ;
2025-12-23 15:42:35 -08:00
}
2026-08-20 13:42:31 -07:00
console . log ( ` ${ COLORS . GREEN } ╭───╮ ${ COLORS . RESET } ${ COLORS . BRIGHT } All done! Happy coding! ${ COLORS . RESET } ` ) ;
console . log ( ` ${ COLORS . GREEN } │ ${ COLORS . CYAN } ★ ${ COLORS . GREEN } │ ${ COLORS . RESET } ${ COLORS . BRIGHT } If this is your first time using Plan2Code, read the docs here: ${ COLORS . RESET } ` ) ;
console . log ( ` ${ COLORS . GREEN } │ ${ COLORS . BRIGHT } ◡ ${ COLORS . GREEN } │ ${ COLORS . RESET } ${ COLORS . BRIGHT } https://github.com/jparkerweb/plan2code ${ COLORS . RESET } ` ) ;
console . log ( ` ${ COLORS . GREEN } ╰───╯ ${ COLORS . RESET } ` ) ;
console . log ( ` \n ${ COLORS . DIM } Update later with: ${ COLORS . RESET } ${ COLORS . CYAN } npx skills update -g ${ COLORS . RESET } \n ` ) ;
return 0 ;
2025-12-23 15:42:35 -08:00
}
// ============================================================================
// UNINSTALLATION
// ============================================================================
/**
* Execute uninstallation
*/
2026-08-20 13:42:31 -07:00
function uninstallSkills ( ) {
displaySectionHeader ( 'UNINSTALLATION' , '[ REMOVING SKILLS ]' ) ;
2025-12-23 15:42:35 -08:00
console . log ( '' ) ;
displayStatusBox ( 'PARAMETERS' , [
2026-08-20 13:42:31 -07:00
` ${ COLORS . CYAN } Home Directory: ${ COLORS . RESET } ${ os . homedir ( ) } ` ,
` ${ COLORS . CYAN } Scope: ${ COLORS . RESET } GLOBAL ` ,
2026-02-22 12:54:33 -08:00
` ${ COLORS . CYAN } Mode: ${ COLORS . RESET } UNINSTALL ` ,
2025-12-23 15:42:35 -08:00
] ) ;
2026-08-20 13:42:31 -07:00
let removed = 0 ;
let errors = 0 ;
if ( ensureSkillsCli ( ) ) {
console . log ( ` \n ${ COLORS . YELLOW } ${ SYMBOLS . ACTIVE } REMOVING SKILLS ${ COLORS . RESET } \n ` ) ;
removed += removeInstalledSkills ( 'global' ) ;
} else {
errors ++ ;
2025-12-23 15:42:35 -08:00
}
2026-08-20 13:42:31 -07:00
console . log ( ` \n ${ COLORS . YELLOW } ${ SYMBOLS . ACTIVE } CLEANING LEGACY PATHS ${ COLORS . RESET } \n ` ) ;
const legacy = cleanLegacyPaths ( ) ;
if ( legacy . removed === 0 ) console . log ( ` ${ COLORS . DIM } ${ SYMBOLS . INFO } Nothing left over from earlier versions ${ COLORS . RESET } ` ) ;
removed += legacy . removed ;
errors += legacy . errors ;
console . log ( ` \n ${ COLORS . CYAN } ╔═══════════════════════════════════════════════════════════════════════════╗ ${ COLORS . RESET } ` ) ;
2025-12-23 15:42:35 -08:00
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 } ` ) ;
2026-08-20 13:42:31 -07:00
const statusColor = errors > 0 ? COLORS . RED : COLORS . GREEN ;
const statusText = errors > 0 ? 'COMPLETED WITH ERRORS' : 'SUCCESS' ;
2025-12-23 15:42:35 -08:00
console . log ( padEndVisible ( ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } ${ statusColor } Status: ${ COLORS . RESET } ${ statusColor } ${ statusText } ${ COLORS . RESET } ` , 76 ) + ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } ` ) ;
2026-08-20 13:42:31 -07:00
console . log ( padEndVisible ( ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } Items Removed: ${ COLORS . GREEN } ${ removed } ${ COLORS . RESET } ` , 76 ) + ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } ` ) ;
if ( errors > 0 ) console . log ( padEndVisible ( ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } Errors: ${ COLORS . RED } ${ errors } ${ COLORS . RESET } ` , 76 ) + ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } ` ) ;
2025-12-23 15:42:35 -08:00
console . log ( ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } ${ ' ' . repeat ( 75 ) } ${ COLORS . CYAN } ║ ${ COLORS . RESET } ` ) ;
2026-08-20 13:42:31 -07:00
console . log ( ` ${ COLORS . CYAN } ╚═══════════════════════════════════════════════════════════════════════════╝ ${ COLORS . RESET } \n ` ) ;
return errors > 0 ? 1 : 0 ;
2025-12-23 15:42:35 -08:00
}
// ============================================================================
2026-08-20 13:42:31 -07:00
// PROJECT INSTALLATION
2025-12-23 15:42:35 -08:00
// ============================================================================
/**
2026-08-20 13:42:31 -07:00
* Install skills into the current project instead of globally.
2025-12-23 15:42:35 -08:00
*/
2026-08-20 13:42:31 -07:00
function installProjectSkills ( ) {
const projectDir = process . cwd ( ) ;
displaySectionHeader ( 'PROJECT INSTALLATION' , '[ CURRENT DIRECTORY ]' ) ;
if ( ! buildSkills ( true ) ) {
console . log ( ` ${ COLORS . RED } ${ SYMBOLS . ERROR } Failed to build ${ SKILLS _DIR _NAME } / from src/ ${ COLORS . RESET } ` ) ;
2025-12-23 15:42:35 -08:00
return 1 ;
}
2026-08-20 13:42:31 -07:00
console . log ( ` ${ COLORS . GREEN } ${ SYMBOLS . SUCCESS } Built ${ SOURCE _PROMPTS . length } skills from src/ ${ COLORS . RESET } ` ) ;
if ( ! ensureSkillsCli ( ) ) return 1 ;
2025-12-23 15:42:35 -08:00
2026-08-20 13:42:31 -07:00
console . log ( '' ) ;
2025-12-23 15:42:35 -08:00
displayStatusBox ( 'OVERVIEW' , [
2026-08-20 13:42:31 -07:00
` Installs into ${ COLORS . BRIGHT } this project ${ COLORS . RESET } rather than your home directory, so the ` ,
` skills travel with the repo and are visible to everyone who clones it. ` ,
2025-12-23 15:42:35 -08:00
'' ,
2026-08-20 13:42:31 -07:00
` ${ COLORS . CYAN } Project: ${ COLORS . RESET } ${ projectDir } ` ,
` ${ COLORS . CYAN } Skills: ${ COLORS . RESET } ${ SOURCE _PROMPTS . length } ` ,
2025-12-23 15:42:35 -08:00
] ) ;
console . log ( '' ) ;
2026-08-20 13:42:31 -07:00
if ( projectDir === _ _dirname ) {
console . log ( ` ${ COLORS . YELLOW } ${ SYMBOLS . WARNING } ${ COLORS . RESET } This is the Plan2Code repo itself — installing here is rarely what you want. \n ` ) ;
}
2025-12-23 15:42:35 -08:00
2026-08-20 13:42:31 -07:00
console . log ( ` ${ COLORS . YELLOW } ${ SYMBOLS . ACTIVE } INSTALLING VIA SKILLS.SH ${ COLORS . RESET } \n ` ) ;
console . log ( ` ${ COLORS . DIM } Installing ${ SOURCE _PROMPTS . length } skills... ${ COLORS . RESET } ` ) ;
const result = execSkillsCli ( ` add " ${ SKILLS _DIR } " ${ skillNameArgs ( ) } -y ` ) ;
reportSkillsOutput ( result ) ;
2025-12-23 15:42:35 -08:00
console . log ( '' ) ;
2026-08-20 13:42:31 -07:00
if ( ! result . ok ) {
console . log ( ` ${ COLORS . RED } ${ SYMBOLS . ERROR } skills add failed. Run it by hand from your project root: ${ COLORS . RESET } ` ) ;
console . log ( ` ${ COLORS . CYAN } npx --yes skills add " ${ SKILLS _DIR } " ${ skillNameArgs ( ) } -y ${ COLORS . RESET } \n ` ) ;
return 1 ;
}
console . log ( ` ${ COLORS . GREEN } ${ SYMBOLS . SUCCESS } Installed into ${ projectDir } ${ COLORS . RESET } \n ` ) ;
console . log ( ` ${ COLORS . DIM } Check the new directories aren't gitignored, or your agents won't see them. ${ COLORS . RESET } \n ` ) ;
2025-12-23 15:42:35 -08:00
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-08-08 12:39:13 -07:00
// 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 } ` ) ;
2026-08-20 13:42:31 -07:00
console . log ( padEndVisible ( ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } ${ COLORS . BRIGHT } I. ${ COLORS . RESET } ${ COLORS . GREEN } INSTALL ${ COLORS . RESET } Install Plan2Code skills everywhere ` , 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-08-20 13:42:31 -07:00
console . log ( padEndVisible ( ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } ${ COLORS . BRIGHT } U. ${ COLORS . RESET } ${ COLORS . RED } UNINSTALL ${ COLORS . RESET } Remove Plan2Code skills and dev tools ` , 58 ) + ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } ` ) ;
2026-02-22 12:54:33 -08:00
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-08-20 13:42:31 -07:00
// I path — skills only. The loop remains optional via A or Custom → O.
2026-02-22 12:54:33 -08:00
if ( input === 'I' ) {
2026-01-27 12:11:00 -08:00
rl . close ( ) ;
2026-08-20 13:42:31 -07:00
process . exit ( await install ( ) ) ;
2026-05-11 13:14:35 -07:00
}
2026-08-20 13:42:31 -07:00
// A path — Install Everything (skills + 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-01-27 12:11:00 -08:00
console . log ( ` ${ COLORS . RED } ╭───╮ ${ COLORS . RESET } ` ) ;
2026-08-20 13:42:31 -07:00
console . log ( ` ${ COLORS . RED } │ ${ COLORS . YELLOW } ○ ${ COLORS . RED } │ ${ COLORS . RESET } ${ COLORS . YELLOW } ! ${ 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 } ` ) ;
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-08-20 13:42:31 -07:00
const uninstallResult = uninstallSkills ( ) ;
2026-02-22 12:54:33 -08:00
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 } ` ) ;
2026-08-20 13:42:31 -07:00
console . log ( padEndVisible ( ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } ${ COLORS . BRIGHT } L. ${ COLORS . RESET } ${ COLORS . BLUE } LOCAL ${ COLORS . RESET } Install skills into the current project only ` , 65 ) + ` ${ COLORS . CYAN } ║ ${ COLORS . RESET } ` ) ;
2026-02-22 12:54:33 -08:00
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-08-20 13:42:31 -07:00
// C > L — install skills into the current project
2026-02-22 12:54:33 -08:00
if ( customInput === 'L' ) {
rl . close ( ) ;
2026-08-20 13:42:31 -07:00
process . exit ( installProjectSkills ( ) ) ;
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-08-20 13:42:31 -07:00
// Non-interactive build hooks for the committed skills/ artifact.
const argv = process . argv . slice ( 2 ) ;
if ( argv . includes ( '--build-skills' ) ) {
process . exit ( buildSkills ( ) ? 0 : 1 ) ;
}
if ( argv . includes ( '--verify-skills' ) ) {
process . exit ( verifySkillsSync ( ) ? 0 : 1 ) ;
}
if ( argv . length > 0 ) {
console . error ( ` Unknown option(s): ${ argv . join ( ' ' ) } ` ) ;
console . error ( 'Usage: node install.js [--build-skills | --verify-skills]' ) ;
console . error ( 'Run with no options for the interactive installer.' ) ;
process . exit ( 1 ) ;
}
2026-02-22 12:54:33 -08:00
runInteractive ( ) ;