diff --git a/.claude/package-manager.json b/.claude/package-manager.json new file mode 100644 index 0000000..4df6381 --- /dev/null +++ b/.claude/package-manager.json @@ -0,0 +1,4 @@ +{ + "packageManager": "bun", + "setAt": "2026-01-23T02:09:58.819Z" +} \ No newline at end of file diff --git a/README.md b/README.md index 81cb583..eebc681 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,41 @@ This repo is the raw code only. The guides explain everything. --- +## Cross-Platform Support + +This plugin now fully supports **Windows, macOS, and Linux**. All hooks and scripts have been rewritten in Node.js for maximum compatibility. + +### Package Manager Detection + +The plugin automatically detects your preferred package manager (npm, pnpm, yarn, or bun) with the following priority: + +1. **Environment variable**: `CLAUDE_PACKAGE_MANAGER` +2. **Project config**: `.claude/package-manager.json` +3. **package.json**: `packageManager` field +4. **Lock file**: Detection from package-lock.json, yarn.lock, pnpm-lock.yaml, or bun.lockb +5. **Global config**: `~/.claude/package-manager.json` +6. **Fallback**: First available package manager + +To set your preferred package manager: + +```bash +# Via environment variable +export CLAUDE_PACKAGE_MANAGER=pnpm + +# Via global config +node scripts/setup-package-manager.js --global pnpm + +# Via project config +node scripts/setup-package-manager.js --project bun + +# Detect current setting +node scripts/setup-package-manager.js --detect +``` + +Or use the `/setup-pm` command in Claude Code. + +--- + ## What's Inside This repo is a **Claude Code plugin** - install it directly or copy components manually. @@ -88,6 +123,7 @@ everything-claude-code/ | |-- learn.md # /learn - Extract patterns mid-session (Longform Guide) | |-- checkpoint.md # /checkpoint - Save verification state (Longform Guide) | |-- verify.md # /verify - Run verification loop (Longform Guide) +| |-- setup-pm.md # /setup-pm - Configure package manager (NEW) | |-- rules/ # Always-follow guidelines (copy to ~/.claude/rules/) | |-- security.md # Mandatory security checks @@ -102,6 +138,23 @@ everything-claude-code/ | |-- memory-persistence/ # Session lifecycle hooks (Longform Guide) | |-- strategic-compact/ # Compaction suggestions (Longform Guide) | +|-- scripts/ # Cross-platform Node.js scripts (NEW) +| |-- lib/ # Shared utilities +| | |-- utils.js # Cross-platform file/path/system utilities +| | |-- package-manager.js # Package manager detection and selection +| |-- hooks/ # Hook implementations +| | |-- session-start.js # Load context on session start +| | |-- session-end.js # Save state on session end +| | |-- pre-compact.js # Pre-compaction state saving +| | |-- suggest-compact.js # Strategic compaction suggestions +| | |-- evaluate-session.js # Extract patterns from sessions +| |-- setup-package-manager.js # Interactive PM setup +| +|-- tests/ # Test suite (NEW) +| |-- lib/ # Library tests +| |-- hooks/ # Hook tests +| |-- run-all.js # Run all tests +| |-- contexts/ # Dynamic system prompt injection contexts (Longform Guide) | |-- dev.md # Development mode context | |-- review.md # Code review mode context @@ -246,6 +299,22 @@ Rules are always-follow guidelines. Keep them modular: --- +## Running Tests + +The plugin includes a comprehensive test suite: + +```bash +# Run all tests +node tests/run-all.js + +# Run individual test files +node tests/lib/utils.test.js +node tests/lib/package-manager.test.js +node tests/hooks/hooks.test.js +``` + +--- + ## Contributing **Contributions are welcome and encouraged.** diff --git a/commands/setup-pm.md b/commands/setup-pm.md new file mode 100644 index 0000000..87224b9 --- /dev/null +++ b/commands/setup-pm.md @@ -0,0 +1,80 @@ +--- +description: Configure your preferred package manager (npm/pnpm/yarn/bun) +disable-model-invocation: true +--- + +# Package Manager Setup + +Configure your preferred package manager for this project or globally. + +## Usage + +```bash +# Detect current package manager +node scripts/setup-package-manager.js --detect + +# Set global preference +node scripts/setup-package-manager.js --global pnpm + +# Set project preference +node scripts/setup-package-manager.js --project bun + +# List available package managers +node scripts/setup-package-manager.js --list +``` + +## Detection Priority + +When determining which package manager to use, the following order is checked: + +1. **Environment variable**: `CLAUDE_PACKAGE_MANAGER` +2. **Project config**: `.claude/package-manager.json` +3. **package.json**: `packageManager` field +4. **Lock file**: Presence of package-lock.json, yarn.lock, pnpm-lock.yaml, or bun.lockb +5. **Global config**: `~/.claude/package-manager.json` +6. **Fallback**: First available package manager (pnpm > bun > yarn > npm) + +## Configuration Files + +### Global Configuration +```json +// ~/.claude/package-manager.json +{ + "packageManager": "pnpm" +} +``` + +### Project Configuration +```json +// .claude/package-manager.json +{ + "packageManager": "bun" +} +``` + +### package.json +```json +{ + "packageManager": "pnpm@8.6.0" +} +``` + +## Environment Variable + +Set `CLAUDE_PACKAGE_MANAGER` to override all other detection methods: + +```bash +# Windows (PowerShell) +$env:CLAUDE_PACKAGE_MANAGER = "pnpm" + +# macOS/Linux +export CLAUDE_PACKAGE_MANAGER=pnpm +``` + +## Run the Detection + +To see current package manager detection results, run: + +```bash +node scripts/setup-package-manager.js --detect +``` diff --git a/hooks/hooks.json b/hooks/hooks.json index 3cc1314..ea9cdc6 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -7,17 +7,17 @@ "hooks": [ { "type": "command", - "command": "#!/bin/bash\ninput=$(cat)\ncmd=$(echo \"$input\" | jq -r '.tool_input.command // \"\"')\n\n# Block dev servers that aren't run in tmux\necho '[Hook] BLOCKED: Dev server must run in tmux for log access' >&2\necho '[Hook] Use this command instead:' >&2\necho \"[Hook] tmux new-session -d -s dev 'npm run dev'\" >&2\necho '[Hook] Then: tmux attach -t dev' >&2\nexit 1" + "command": "node -e \"console.error('[Hook] BLOCKED: Dev server must run in tmux for log access');console.error('[Hook] Use: tmux new-session -d -s dev \\\"npm run dev\\\"');console.error('[Hook] Then: tmux attach -t dev');process.exit(1)\"" } ], "description": "Block dev servers outside tmux - ensures you can access logs" }, { - "matcher": "tool == \"Bash\" && tool_input.command matches \"(npm (install|test)|pnpm (install|test)|yarn (install|test)|bun (install|test)|cargo build|make|docker|pytest|vitest|playwright)\"", + "matcher": "tool == \"Bash\" && tool_input.command matches \"(npm (install|test)|pnpm (install|test)|yarn (install|test)?|bun (install|test)|cargo build|make|docker|pytest|vitest|playwright)\"", "hooks": [ { "type": "command", - "command": "#!/bin/bash\ninput=$(cat)\nif [ -z \"$TMUX\" ]; then\n echo '[Hook] Consider running in tmux for session persistence' >&2\n echo '[Hook] tmux new -s dev | tmux attach -t dev' >&2\nfi\necho \"$input\"" + "command": "node -e \"if(!process.env.TMUX){console.error('[Hook] Consider running in tmux for session persistence');console.error('[Hook] tmux new -s dev | tmux attach -t dev')}\"" } ], "description": "Reminder to use tmux for long-running commands" @@ -27,17 +27,17 @@ "hooks": [ { "type": "command", - "command": "#!/bin/bash\n# Open editor for review before pushing\necho '[Hook] Review changes before push...' >&2\n# Uncomment your preferred editor:\n# zed . 2>/dev/null\n# code . 2>/dev/null\n# cursor . 2>/dev/null\necho '[Hook] Press Enter to continue with push or Ctrl+C to abort...' >&2\nread -r" + "command": "node -e \"console.error('[Hook] Review changes before push...');console.error('[Hook] Continuing with push (remove this hook to add interactive review)')\"" } ], - "description": "Pause before git push to review changes" + "description": "Reminder before git push to review changes" }, { "matcher": "tool == \"Write\" && tool_input.file_path matches \"\\\\.(md|txt)$\" && !(tool_input.file_path matches \"README\\\\.md|CLAUDE\\\\.md|AGENTS\\\\.md|CONTRIBUTING\\\\.md\")", "hooks": [ { "type": "command", - "command": "#!/bin/bash\n# Block creation of unnecessary documentation files\ninput=$(cat)\nfile_path=$(echo \"$input\" | jq -r '.tool_input.file_path // \"\"')\n\nif [[ \"$file_path\" =~ \\.(md|txt)$ ]] && [[ ! \"$file_path\" =~ (README|CLAUDE|AGENTS|CONTRIBUTING)\\.md$ ]]; then\n echo \"[Hook] BLOCKED: Unnecessary documentation file creation\" >&2\n echo \"[Hook] File: $file_path\" >&2\n echo \"[Hook] Use README.md for documentation instead\" >&2\n exit 1\nfi\n\necho \"$input\"" + "command": "node -e \"const fs=require('fs');let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const p=i.tool_input?.file_path||'';if(/\\.(md|txt)$/.test(p)&&!/(README|CLAUDE|AGENTS|CONTRIBUTING)\\.md$/.test(p)){console.error('[Hook] BLOCKED: Unnecessary documentation file creation');console.error('[Hook] File: '+p);console.error('[Hook] Use README.md for documentation instead');process.exit(1)}console.log(d)})\"" } ], "description": "Block creation of random .md files - keeps docs consolidated" @@ -47,7 +47,7 @@ "hooks": [ { "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/strategic-compact/suggest-compact.sh" + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/suggest-compact.js\"" } ], "description": "Suggest manual compaction at logical intervals" @@ -59,7 +59,7 @@ "hooks": [ { "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/memory-persistence/pre-compact.sh" + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/pre-compact.js\"" } ], "description": "Save state before context compaction" @@ -71,10 +71,10 @@ "hooks": [ { "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/memory-persistence/session-start.sh" + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/session-start.js\"" } ], - "description": "Load previous context on new session" + "description": "Load previous context and detect package manager on new session" } ], "PostToolUse": [ @@ -83,7 +83,7 @@ "hooks": [ { "type": "command", - "command": "#!/bin/bash\n# Auto-detect PR creation and log useful info\ninput=$(cat)\ncmd=$(echo \"$input\" | jq -r '.tool_input.command')\n\nif echo \"$cmd\" | grep -qE 'gh pr create'; then\n output=$(echo \"$input\" | jq -r '.tool_output.output // \"\"')\n pr_url=$(echo \"$output\" | grep -oE 'https://github.com/[^/]+/[^/]+/pull/[0-9]+')\n \n if [ -n \"$pr_url\" ]; then\n echo \"[Hook] PR created: $pr_url\" >&2\n echo \"[Hook] Checking GitHub Actions status...\" >&2\n repo=$(echo \"$pr_url\" | sed -E 's|https://github.com/([^/]+/[^/]+)/pull/[0-9]+|\\1|')\n pr_num=$(echo \"$pr_url\" | sed -E 's|.*/pull/([0-9]+)|\\1|')\n echo \"[Hook] To review PR: gh pr review $pr_num --repo $repo\" >&2\n fi\nfi\n\necho \"$input\"" + "command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const cmd=i.tool_input?.command||'';if(/gh pr create/.test(cmd)){const out=i.tool_output?.output||'';const m=out.match(/https:\\/\\/github.com\\/[^/]+\\/[^/]+\\/pull\\/\\d+/);if(m){console.error('[Hook] PR created: '+m[0]);const repo=m[0].replace(/https:\\/\\/github.com\\/([^/]+\\/[^/]+)\\/pull\\/\\d+/,'$1');const pr=m[0].replace(/.*\\/pull\\/(\\d+)/,'$1');console.error('[Hook] To review: gh pr review '+pr+' --repo '+repo)}}console.log(d)})\"" } ], "description": "Log PR URL and provide review command after PR creation" @@ -93,7 +93,7 @@ "hooks": [ { "type": "command", - "command": "#!/bin/bash\n# Auto-format with Prettier after editing JS/TS files\ninput=$(cat)\nfile_path=$(echo \"$input\" | jq -r '.tool_input.file_path // \"\"')\n\nif [ -n \"$file_path\" ] && [ -f \"$file_path\" ]; then\n if command -v prettier >/dev/null 2>&1; then\n prettier --write \"$file_path\" 2>&1 | head -5 >&2\n fi\nfi\n\necho \"$input\"" + "command": "node -e \"const{execSync}=require('child_process');const fs=require('fs');let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const p=i.tool_input?.file_path;if(p&&fs.existsSync(p)){try{execSync('npx prettier --write \"'+p+'\"',{stdio:['pipe','pipe','pipe']})}catch(e){}}console.log(d)})\"" } ], "description": "Auto-format JS/TS files with Prettier after edits" @@ -103,7 +103,7 @@ "hooks": [ { "type": "command", - "command": "#!/bin/bash\n# Run TypeScript check after editing TS files\ninput=$(cat)\nfile_path=$(echo \"$input\" | jq -r '.tool_input.file_path // \"\"')\n\nif [ -n \"$file_path\" ] && [ -f \"$file_path\" ]; then\n dir=$(dirname \"$file_path\")\n project_root=\"$dir\"\n while [ \"$project_root\" != \"/\" ] && [ ! -f \"$project_root/package.json\" ]; do\n project_root=$(dirname \"$project_root\")\n done\n \n if [ -f \"$project_root/tsconfig.json\" ]; then\n cd \"$project_root\" && npx tsc --noEmit --pretty false 2>&1 | grep \"$file_path\" | head -10 >&2 || true\n fi\nfi\n\necho \"$input\"" + "command": "node -e \"const{execSync}=require('child_process');const fs=require('fs');const path=require('path');let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const p=i.tool_input?.file_path;if(p&&fs.existsSync(p)){let dir=path.dirname(p);while(dir!==path.dirname(dir)&&!fs.existsSync(path.join(dir,'tsconfig.json'))){dir=path.dirname(dir)}if(fs.existsSync(path.join(dir,'tsconfig.json'))){try{const r=execSync('npx tsc --noEmit --pretty false 2>&1',{cwd:dir,encoding:'utf8',stdio:['pipe','pipe','pipe']});const lines=r.split('\\n').filter(l=>l.includes(p)).slice(0,10);if(lines.length)console.error(lines.join('\\n'))}catch(e){const lines=(e.stdout||'').split('\\n').filter(l=>l.includes(p)).slice(0,10);if(lines.length)console.error(lines.join('\\n'))}}}console.log(d)})\"" } ], "description": "TypeScript check after editing .ts/.tsx files" @@ -113,7 +113,7 @@ "hooks": [ { "type": "command", - "command": "#!/bin/bash\n# Warn about console.log in edited files\ninput=$(cat)\nfile_path=$(echo \"$input\" | jq -r '.tool_input.file_path // \"\"')\n\nif [ -n \"$file_path\" ] && [ -f \"$file_path\" ]; then\n console_logs=$(grep -n \"console\\\\.log\" \"$file_path\" 2>/dev/null || true)\n \n if [ -n \"$console_logs\" ]; then\n echo \"[Hook] WARNING: console.log found in $file_path\" >&2\n echo \"$console_logs\" | head -5 >&2\n echo \"[Hook] Remove console.log before committing\" >&2\n fi\nfi\n\necho \"$input\"" + "command": "node -e \"const fs=require('fs');let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const p=i.tool_input?.file_path;if(p&&fs.existsSync(p)){const c=fs.readFileSync(p,'utf8');const lines=c.split('\\n');const matches=[];lines.forEach((l,idx)=>{if(/console\\.log/.test(l))matches.push((idx+1)+': '+l.trim())});if(matches.length){console.error('[Hook] WARNING: console.log found in '+p);matches.slice(0,5).forEach(m=>console.error(m));console.error('[Hook] Remove console.log before committing')}}console.log(d)})\"" } ], "description": "Warn about console.log statements after edits" @@ -125,7 +125,7 @@ "hooks": [ { "type": "command", - "command": "#!/bin/bash\n# Final check for console.logs in modified files\ninput=$(cat)\n\nif git rev-parse --git-dir > /dev/null 2>&1; then\n modified_files=$(git diff --name-only HEAD 2>/dev/null | grep -E '\\.(ts|tsx|js|jsx)$' || true)\n \n if [ -n \"$modified_files\" ]; then\n has_console=false\n while IFS= read -r file; do\n if [ -f \"$file\" ]; then\n if grep -q \"console\\.log\" \"$file\" 2>/dev/null; then\n echo \"[Hook] WARNING: console.log found in $file\" >&2\n has_console=true\n fi\n fi\n done <<< \"$modified_files\"\n \n if [ \"$has_console\" = true ]; then\n echo \"[Hook] Remove console.log statements before committing\" >&2\n fi\n fi\nfi\n\necho \"$input\"" + "command": "node -e \"const{execSync}=require('child_process');const fs=require('fs');let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{execSync('git rev-parse --git-dir',{stdio:'pipe'})}catch{console.log(d);process.exit(0)}try{const files=execSync('git diff --name-only HEAD',{encoding:'utf8',stdio:['pipe','pipe','pipe']}).split('\\n').filter(f=>/\\.(ts|tsx|js|jsx)$/.test(f)&&fs.existsSync(f));let hasConsole=false;for(const f of files){if(fs.readFileSync(f,'utf8').includes('console.log')){console.error('[Hook] WARNING: console.log found in '+f);hasConsole=true}}if(hasConsole)console.error('[Hook] Remove console.log statements before committing')}catch(e){}console.log(d)})\"" } ], "description": "Check for console.log in modified files after each response" @@ -137,7 +137,7 @@ "hooks": [ { "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/memory-persistence/session-end.sh" + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/session-end.js\"" } ], "description": "Persist session state on end" @@ -147,7 +147,7 @@ "hooks": [ { "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning/evaluate-session.sh" + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/evaluate-session.js\"" } ], "description": "Evaluate session for extractable patterns" diff --git a/scripts/hooks/evaluate-session.js b/scripts/hooks/evaluate-session.js new file mode 100644 index 0000000..3cfaf2c --- /dev/null +++ b/scripts/hooks/evaluate-session.js @@ -0,0 +1,78 @@ +#!/usr/bin/env node +/** + * Continuous Learning - Session Evaluator + * + * Cross-platform (Windows, macOS, Linux) + * + * Runs on Stop hook to extract reusable patterns from Claude Code sessions + * + * Why Stop hook instead of UserPromptSubmit: + * - Stop runs once at session end (lightweight) + * - UserPromptSubmit runs every message (heavy, adds latency) + */ + +const path = require('path'); +const fs = require('fs'); +const { + getLearnedSkillsDir, + ensureDir, + readFile, + countInFile, + log +} = require('../lib/utils'); + +async function main() { + // Get script directory to find config + const scriptDir = __dirname; + const configFile = path.join(scriptDir, '..', '..', 'skills', 'continuous-learning', 'config.json'); + + // Default configuration + let minSessionLength = 10; + let learnedSkillsPath = getLearnedSkillsDir(); + + // Load config if exists + const configContent = readFile(configFile); + if (configContent) { + try { + const config = JSON.parse(configContent); + minSessionLength = config.min_session_length || 10; + + if (config.learned_skills_path) { + // Handle ~ in path + learnedSkillsPath = config.learned_skills_path.replace(/^~/, require('os').homedir()); + } + } catch { + // Invalid config, use defaults + } + } + + // Ensure learned skills directory exists + ensureDir(learnedSkillsPath); + + // Get transcript path from environment (set by Claude Code) + const transcriptPath = process.env.CLAUDE_TRANSCRIPT_PATH; + + if (!transcriptPath || !fs.existsSync(transcriptPath)) { + process.exit(0); + } + + // Count user messages in session + const messageCount = countInFile(transcriptPath, /"type":"user"/g); + + // Skip short sessions + if (messageCount < minSessionLength) { + log(`[ContinuousLearning] Session too short (${messageCount} messages), skipping`); + process.exit(0); + } + + // Signal to Claude that session should be evaluated for extractable patterns + log(`[ContinuousLearning] Session has ${messageCount} messages - evaluate for extractable patterns`); + log(`[ContinuousLearning] Save learned skills to: ${learnedSkillsPath}`); + + process.exit(0); +} + +main().catch(err => { + console.error('[ContinuousLearning] Error:', err.message); + process.exit(0); +}); diff --git a/scripts/hooks/pre-compact.js b/scripts/hooks/pre-compact.js new file mode 100644 index 0000000..591e086 --- /dev/null +++ b/scripts/hooks/pre-compact.js @@ -0,0 +1,48 @@ +#!/usr/bin/env node +/** + * PreCompact Hook - Save state before context compaction + * + * Cross-platform (Windows, macOS, Linux) + * + * Runs before Claude compacts context, giving you a chance to + * preserve important state that might get lost in summarization. + */ + +const path = require('path'); +const { + getSessionsDir, + getDateTimeString, + getTimeString, + findFiles, + ensureDir, + appendFile, + log +} = require('../lib/utils'); + +async function main() { + const sessionsDir = getSessionsDir(); + const compactionLog = path.join(sessionsDir, 'compaction-log.txt'); + + ensureDir(sessionsDir); + + // Log compaction event with timestamp + const timestamp = getDateTimeString(); + appendFile(compactionLog, `[${timestamp}] Context compaction triggered\n`); + + // If there's an active session file, note the compaction + const sessions = findFiles(sessionsDir, '*.tmp'); + + if (sessions.length > 0) { + const activeSession = sessions[0].path; + const timeStr = getTimeString(); + appendFile(activeSession, `\n---\n**[Compaction occurred at ${timeStr}]** - Context was summarized\n`); + } + + log('[PreCompact] State saved before compaction'); + process.exit(0); +} + +main().catch(err => { + console.error('[PreCompact] Error:', err.message); + process.exit(0); +}); diff --git a/scripts/hooks/session-end.js b/scripts/hooks/session-end.js new file mode 100644 index 0000000..4017d02 --- /dev/null +++ b/scripts/hooks/session-end.js @@ -0,0 +1,82 @@ +#!/usr/bin/env node +/** + * Stop Hook (Session End) - Persist learnings when session ends + * + * Cross-platform (Windows, macOS, Linux) + * + * Runs when Claude session ends. Creates/updates session log file + * with timestamp for continuity tracking. + */ + +const path = require('path'); +const fs = require('fs'); +const { + getSessionsDir, + getDateString, + getTimeString, + ensureDir, + readFile, + writeFile, + replaceInFile, + log +} = require('../lib/utils'); + +async function main() { + const sessionsDir = getSessionsDir(); + const today = getDateString(); + const sessionFile = path.join(sessionsDir, `${today}-session.tmp`); + + ensureDir(sessionsDir); + + const currentTime = getTimeString(); + + // If session file exists for today, update the end time + if (fs.existsSync(sessionFile)) { + const success = replaceInFile( + sessionFile, + /\*\*Last Updated:\*\*.*/, + `**Last Updated:** ${currentTime}` + ); + + if (success) { + log(`[SessionEnd] Updated session file: ${sessionFile}`); + } + } else { + // Create new session file with template + const template = `# Session: ${today} +**Date:** ${today} +**Started:** ${currentTime} +**Last Updated:** ${currentTime} + +--- + +## Current State + +[Session context goes here] + +### Completed +- [ ] + +### In Progress +- [ ] + +### Notes for Next Session +- + +### Context to Load +\`\`\` +[relevant files] +\`\`\` +`; + + writeFile(sessionFile, template); + log(`[SessionEnd] Created session file: ${sessionFile}`); + } + + process.exit(0); +} + +main().catch(err => { + console.error('[SessionEnd] Error:', err.message); + process.exit(0); +}); diff --git a/scripts/hooks/session-start.js b/scripts/hooks/session-start.js new file mode 100644 index 0000000..9693421 --- /dev/null +++ b/scripts/hooks/session-start.js @@ -0,0 +1,61 @@ +#!/usr/bin/env node +/** + * SessionStart Hook - Load previous context on new session + * + * Cross-platform (Windows, macOS, Linux) + * + * Runs when a new Claude session starts. Checks for recent session + * files and notifies Claude of available context to load. + */ + +const path = require('path'); +const { + getSessionsDir, + getLearnedSkillsDir, + findFiles, + ensureDir, + log +} = require('../lib/utils'); +const { getPackageManager, getSelectionPrompt } = require('../lib/package-manager'); + +async function main() { + const sessionsDir = getSessionsDir(); + const learnedDir = getLearnedSkillsDir(); + + // Ensure directories exist + ensureDir(sessionsDir); + ensureDir(learnedDir); + + // Check for recent session files (last 7 days) + const recentSessions = findFiles(sessionsDir, '*.tmp', { maxAge: 7 }); + + if (recentSessions.length > 0) { + const latest = recentSessions[0]; + log(`[SessionStart] Found ${recentSessions.length} recent session(s)`); + log(`[SessionStart] Latest: ${latest.path}`); + } + + // Check for learned skills + const learnedSkills = findFiles(learnedDir, '*.md'); + + if (learnedSkills.length > 0) { + log(`[SessionStart] ${learnedSkills.length} learned skill(s) available in ${learnedDir}`); + } + + // Detect and report package manager + const pm = getPackageManager(); + log(`[SessionStart] Package manager: ${pm.name} (${pm.source})`); + + // If package manager was detected via fallback, show selection prompt + if (pm.source === 'fallback' || pm.source === 'default') { + log('[SessionStart] No package manager preference found.'); + log(getSelectionPrompt()); + } + + process.exit(0); +} + +main().catch(err => { + console.error('[SessionStart] Error:', err.message); + process.exit(0); // Don't block on errors +}); diff --git a/scripts/hooks/suggest-compact.js b/scripts/hooks/suggest-compact.js new file mode 100644 index 0000000..ae690b7 --- /dev/null +++ b/scripts/hooks/suggest-compact.js @@ -0,0 +1,60 @@ +#!/usr/bin/env node +/** + * Strategic Compact Suggester + * + * Cross-platform (Windows, macOS, Linux) + * + * Runs on PreToolUse or periodically to suggest manual compaction at logical intervals + * + * Why manual over auto-compact: + * - Auto-compact happens at arbitrary points, often mid-task + * - Strategic compacting preserves context through logical phases + * - Compact after exploration, before execution + * - Compact after completing a milestone, before starting next + */ + +const path = require('path'); +const fs = require('fs'); +const { + getTempDir, + readFile, + writeFile, + log +} = require('../lib/utils'); + +async function main() { + // Track tool call count (increment in a temp file) + // Use a session-specific counter file based on PID from parent process + // or session ID from environment + const sessionId = process.env.CLAUDE_SESSION_ID || process.ppid || 'default'; + const counterFile = path.join(getTempDir(), `claude-tool-count-${sessionId}`); + const threshold = parseInt(process.env.COMPACT_THRESHOLD || '50', 10); + + let count = 1; + + // Read existing count or start at 1 + const existing = readFile(counterFile); + if (existing) { + count = parseInt(existing.trim(), 10) + 1; + } + + // Save updated count + writeFile(counterFile, String(count)); + + // Suggest compact after threshold tool calls + if (count === threshold) { + log(`[StrategicCompact] ${threshold} tool calls reached - consider /compact if transitioning phases`); + } + + // Suggest at regular intervals after threshold + if (count > threshold && count % 25 === 0) { + log(`[StrategicCompact] ${count} tool calls - good checkpoint for /compact if context is stale`); + } + + process.exit(0); +} + +main().catch(err => { + console.error('[StrategicCompact] Error:', err.message); + process.exit(0); +}); diff --git a/scripts/lib/package-manager.js b/scripts/lib/package-manager.js new file mode 100644 index 0000000..0b95056 --- /dev/null +++ b/scripts/lib/package-manager.js @@ -0,0 +1,390 @@ +/** + * Package Manager Detection and Selection + * Automatically detects the preferred package manager or lets user choose + * + * Supports: npm, pnpm, yarn, bun + */ + +const fs = require('fs'); +const path = require('path'); +const { commandExists, getClaudeDir, readFile, writeFile, log, runCommand } = require('./utils'); + +// Package manager definitions +const PACKAGE_MANAGERS = { + npm: { + name: 'npm', + lockFile: 'package-lock.json', + installCmd: 'npm install', + runCmd: 'npm run', + execCmd: 'npx', + testCmd: 'npm test', + buildCmd: 'npm run build', + devCmd: 'npm run dev' + }, + pnpm: { + name: 'pnpm', + lockFile: 'pnpm-lock.yaml', + installCmd: 'pnpm install', + runCmd: 'pnpm', + execCmd: 'pnpm dlx', + testCmd: 'pnpm test', + buildCmd: 'pnpm build', + devCmd: 'pnpm dev' + }, + yarn: { + name: 'yarn', + lockFile: 'yarn.lock', + installCmd: 'yarn', + runCmd: 'yarn', + execCmd: 'yarn dlx', + testCmd: 'yarn test', + buildCmd: 'yarn build', + devCmd: 'yarn dev' + }, + bun: { + name: 'bun', + lockFile: 'bun.lockb', + installCmd: 'bun install', + runCmd: 'bun run', + execCmd: 'bunx', + testCmd: 'bun test', + buildCmd: 'bun run build', + devCmd: 'bun run dev' + } +}; + +// Priority order for detection +const DETECTION_PRIORITY = ['pnpm', 'bun', 'yarn', 'npm']; + +// Config file path +function getConfigPath() { + return path.join(getClaudeDir(), 'package-manager.json'); +} + +/** + * Load saved package manager configuration + */ +function loadConfig() { + const configPath = getConfigPath(); + const content = readFile(configPath); + + if (content) { + try { + return JSON.parse(content); + } catch { + return null; + } + } + return null; +} + +/** + * Save package manager configuration + */ +function saveConfig(config) { + const configPath = getConfigPath(); + writeFile(configPath, JSON.stringify(config, null, 2)); +} + +/** + * Detect package manager from lock file in project directory + */ +function detectFromLockFile(projectDir = process.cwd()) { + for (const pmName of DETECTION_PRIORITY) { + const pm = PACKAGE_MANAGERS[pmName]; + const lockFilePath = path.join(projectDir, pm.lockFile); + + if (fs.existsSync(lockFilePath)) { + return pmName; + } + } + return null; +} + +/** + * Detect package manager from package.json packageManager field + */ +function detectFromPackageJson(projectDir = process.cwd()) { + const packageJsonPath = path.join(projectDir, 'package.json'); + const content = readFile(packageJsonPath); + + if (content) { + try { + const pkg = JSON.parse(content); + if (pkg.packageManager) { + // Format: "pnpm@8.6.0" or just "pnpm" + const pmName = pkg.packageManager.split('@')[0]; + if (PACKAGE_MANAGERS[pmName]) { + return pmName; + } + } + } catch { + // Invalid package.json + } + } + return null; +} + +/** + * Get available package managers (installed on system) + */ +function getAvailablePackageManagers() { + const available = []; + + for (const pmName of Object.keys(PACKAGE_MANAGERS)) { + if (commandExists(pmName)) { + available.push(pmName); + } + } + + return available; +} + +/** + * Get the package manager to use for current project + * + * Detection priority: + * 1. Environment variable CLAUDE_PACKAGE_MANAGER + * 2. Project-specific config (in .claude/package-manager.json) + * 3. package.json packageManager field + * 4. Lock file detection + * 5. Global user preference (in ~/.claude/package-manager.json) + * 6. First available package manager (by priority) + * + * @param {object} options - { projectDir, fallbackOrder } + * @returns {object} - { name, config, source } + */ +function getPackageManager(options = {}) { + const { projectDir = process.cwd(), fallbackOrder = DETECTION_PRIORITY } = options; + + // 1. Check environment variable + const envPm = process.env.CLAUDE_PACKAGE_MANAGER; + if (envPm && PACKAGE_MANAGERS[envPm]) { + return { + name: envPm, + config: PACKAGE_MANAGERS[envPm], + source: 'environment' + }; + } + + // 2. Check project-specific config + const projectConfigPath = path.join(projectDir, '.claude', 'package-manager.json'); + const projectConfig = readFile(projectConfigPath); + if (projectConfig) { + try { + const config = JSON.parse(projectConfig); + if (config.packageManager && PACKAGE_MANAGERS[config.packageManager]) { + return { + name: config.packageManager, + config: PACKAGE_MANAGERS[config.packageManager], + source: 'project-config' + }; + } + } catch { + // Invalid config + } + } + + // 3. Check package.json packageManager field + const fromPackageJson = detectFromPackageJson(projectDir); + if (fromPackageJson) { + return { + name: fromPackageJson, + config: PACKAGE_MANAGERS[fromPackageJson], + source: 'package.json' + }; + } + + // 4. Check lock file + const fromLockFile = detectFromLockFile(projectDir); + if (fromLockFile) { + return { + name: fromLockFile, + config: PACKAGE_MANAGERS[fromLockFile], + source: 'lock-file' + }; + } + + // 5. Check global user preference + const globalConfig = loadConfig(); + if (globalConfig && globalConfig.packageManager && PACKAGE_MANAGERS[globalConfig.packageManager]) { + return { + name: globalConfig.packageManager, + config: PACKAGE_MANAGERS[globalConfig.packageManager], + source: 'global-config' + }; + } + + // 6. Use first available package manager + const available = getAvailablePackageManagers(); + for (const pmName of fallbackOrder) { + if (available.includes(pmName)) { + return { + name: pmName, + config: PACKAGE_MANAGERS[pmName], + source: 'fallback' + }; + } + } + + // Default to npm (always available with Node.js) + return { + name: 'npm', + config: PACKAGE_MANAGERS.npm, + source: 'default' + }; +} + +/** + * Set user's preferred package manager (global) + */ +function setPreferredPackageManager(pmName) { + if (!PACKAGE_MANAGERS[pmName]) { + throw new Error(`Unknown package manager: ${pmName}`); + } + + const config = loadConfig() || {}; + config.packageManager = pmName; + config.setAt = new Date().toISOString(); + saveConfig(config); + + return config; +} + +/** + * Set project's preferred package manager + */ +function setProjectPackageManager(pmName, projectDir = process.cwd()) { + if (!PACKAGE_MANAGERS[pmName]) { + throw new Error(`Unknown package manager: ${pmName}`); + } + + const configDir = path.join(projectDir, '.claude'); + const configPath = path.join(configDir, 'package-manager.json'); + + const config = { + packageManager: pmName, + setAt: new Date().toISOString() + }; + + writeFile(configPath, JSON.stringify(config, null, 2)); + return config; +} + +/** + * Get the command to run a script + * @param {string} script - Script name (e.g., "dev", "build", "test") + * @param {object} options - { projectDir } + */ +function getRunCommand(script, options = {}) { + const pm = getPackageManager(options); + + switch (script) { + case 'install': + return pm.config.installCmd; + case 'test': + return pm.config.testCmd; + case 'build': + return pm.config.buildCmd; + case 'dev': + return pm.config.devCmd; + default: + return `${pm.config.runCmd} ${script}`; + } +} + +/** + * Get the command to execute a package binary + * @param {string} binary - Binary name (e.g., "prettier", "eslint") + * @param {string} args - Arguments to pass + */ +function getExecCommand(binary, args = '', options = {}) { + const pm = getPackageManager(options); + return `${pm.config.execCmd} ${binary}${args ? ' ' + args : ''}`; +} + +/** + * Interactive prompt for package manager selection + * Returns a message for Claude to show to user + */ +function getSelectionPrompt() { + const available = getAvailablePackageManagers(); + const current = getPackageManager(); + + let message = '[PackageManager] Available package managers:\n'; + + for (const pmName of available) { + const indicator = pmName === current.name ? ' (current)' : ''; + message += ` - ${pmName}${indicator}\n`; + } + + message += '\nTo set your preferred package manager:\n'; + message += ' - Global: Set CLAUDE_PACKAGE_MANAGER environment variable\n'; + message += ' - Or add to ~/.claude/package-manager.json: {"packageManager": "pnpm"}\n'; + message += ' - Or add to package.json: {"packageManager": "pnpm@8"}\n'; + + return message; +} + +/** + * Generate a regex pattern that matches commands for all package managers + * @param {string} action - Action pattern (e.g., "run dev", "install", "test") + */ +function getCommandPattern(action) { + const patterns = []; + + if (action === 'dev') { + patterns.push( + 'npm run dev', + 'pnpm( run)? dev', + 'yarn dev', + 'bun run dev' + ); + } else if (action === 'install') { + patterns.push( + 'npm install', + 'pnpm install', + 'yarn( install)?', + 'bun install' + ); + } else if (action === 'test') { + patterns.push( + 'npm test', + 'pnpm test', + 'yarn test', + 'bun test' + ); + } else if (action === 'build') { + patterns.push( + 'npm run build', + 'pnpm( run)? build', + 'yarn build', + 'bun run build' + ); + } else { + // Generic run command + patterns.push( + `npm run ${action}`, + `pnpm( run)? ${action}`, + `yarn ${action}`, + `bun run ${action}` + ); + } + + return `(${patterns.join('|')})`; +} + +module.exports = { + PACKAGE_MANAGERS, + DETECTION_PRIORITY, + getPackageManager, + setPreferredPackageManager, + setProjectPackageManager, + getAvailablePackageManagers, + detectFromLockFile, + detectFromPackageJson, + getRunCommand, + getExecCommand, + getSelectionPrompt, + getCommandPattern +}; diff --git a/scripts/lib/utils.js b/scripts/lib/utils.js new file mode 100644 index 0000000..23172c3 --- /dev/null +++ b/scripts/lib/utils.js @@ -0,0 +1,368 @@ +/** + * Cross-platform utility functions for Claude Code hooks and scripts + * Works on Windows, macOS, and Linux + */ + +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const { execSync, spawnSync } = require('child_process'); + +// Platform detection +const isWindows = process.platform === 'win32'; +const isMacOS = process.platform === 'darwin'; +const isLinux = process.platform === 'linux'; + +/** + * Get the user's home directory (cross-platform) + */ +function getHomeDir() { + return os.homedir(); +} + +/** + * Get the Claude config directory + */ +function getClaudeDir() { + return path.join(getHomeDir(), '.claude'); +} + +/** + * Get the sessions directory + */ +function getSessionsDir() { + return path.join(getClaudeDir(), 'sessions'); +} + +/** + * Get the learned skills directory + */ +function getLearnedSkillsDir() { + return path.join(getClaudeDir(), 'skills', 'learned'); +} + +/** + * Get the temp directory (cross-platform) + */ +function getTempDir() { + return os.tmpdir(); +} + +/** + * Ensure a directory exists (create if not) + */ +function ensureDir(dirPath) { + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + return dirPath; +} + +/** + * Get current date in YYYY-MM-DD format + */ +function getDateString() { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +} + +/** + * Get current time in HH:MM format + */ +function getTimeString() { + const now = new Date(); + const hours = String(now.getHours()).padStart(2, '0'); + const minutes = String(now.getMinutes()).padStart(2, '0'); + return `${hours}:${minutes}`; +} + +/** + * Get current datetime in YYYY-MM-DD HH:MM:SS format + */ +function getDateTimeString() { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + const hours = String(now.getHours()).padStart(2, '0'); + const minutes = String(now.getMinutes()).padStart(2, '0'); + const seconds = String(now.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; +} + +/** + * Find files matching a pattern in a directory (cross-platform alternative to find) + * @param {string} dir - Directory to search + * @param {string} pattern - File pattern (e.g., "*.tmp", "*.md") + * @param {object} options - Options { maxAge: days, recursive: boolean } + */ +function findFiles(dir, pattern, options = {}) { + const { maxAge = null, recursive = false } = options; + const results = []; + + if (!fs.existsSync(dir)) { + return results; + } + + const regexPattern = pattern + .replace(/\./g, '\\.') + .replace(/\*/g, '.*') + .replace(/\?/g, '.'); + const regex = new RegExp(`^${regexPattern}$`); + + function searchDir(currentDir) { + try { + const entries = fs.readdirSync(currentDir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(currentDir, entry.name); + + if (entry.isFile() && regex.test(entry.name)) { + if (maxAge !== null) { + const stats = fs.statSync(fullPath); + const ageInDays = (Date.now() - stats.mtimeMs) / (1000 * 60 * 60 * 24); + if (ageInDays <= maxAge) { + results.push({ path: fullPath, mtime: stats.mtimeMs }); + } + } else { + const stats = fs.statSync(fullPath); + results.push({ path: fullPath, mtime: stats.mtimeMs }); + } + } else if (entry.isDirectory() && recursive) { + searchDir(fullPath); + } + } + } catch (err) { + // Ignore permission errors + } + } + + searchDir(dir); + + // Sort by modification time (newest first) + results.sort((a, b) => b.mtime - a.mtime); + + return results; +} + +/** + * Read JSON from stdin (for hook input) + */ +async function readStdinJson() { + return new Promise((resolve, reject) => { + let data = ''; + + process.stdin.setEncoding('utf8'); + process.stdin.on('data', chunk => { + data += chunk; + }); + + process.stdin.on('end', () => { + try { + if (data.trim()) { + resolve(JSON.parse(data)); + } else { + resolve({}); + } + } catch (err) { + reject(err); + } + }); + + process.stdin.on('error', reject); + }); +} + +/** + * Log to stderr (visible to user in Claude Code) + */ +function log(message) { + console.error(message); +} + +/** + * Output to stdout (returned to Claude) + */ +function output(data) { + if (typeof data === 'object') { + console.log(JSON.stringify(data)); + } else { + console.log(data); + } +} + +/** + * Read a text file safely + */ +function readFile(filePath) { + try { + return fs.readFileSync(filePath, 'utf8'); + } catch { + return null; + } +} + +/** + * Write a text file + */ +function writeFile(filePath, content) { + ensureDir(path.dirname(filePath)); + fs.writeFileSync(filePath, content, 'utf8'); +} + +/** + * Append to a text file + */ +function appendFile(filePath, content) { + ensureDir(path.dirname(filePath)); + fs.appendFileSync(filePath, content, 'utf8'); +} + +/** + * Check if a command exists in PATH + */ +function commandExists(cmd) { + try { + if (isWindows) { + execSync(`where ${cmd}`, { stdio: 'pipe' }); + } else { + execSync(`which ${cmd}`, { stdio: 'pipe' }); + } + return true; + } catch { + return false; + } +} + +/** + * Run a command and return output + */ +function runCommand(cmd, options = {}) { + try { + const result = execSync(cmd, { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + ...options + }); + return { success: true, output: result.trim() }; + } catch (err) { + return { success: false, output: err.stderr || err.message }; + } +} + +/** + * Check if current directory is a git repository + */ +function isGitRepo() { + return runCommand('git rev-parse --git-dir').success; +} + +/** + * Get git modified files + */ +function getGitModifiedFiles(patterns = []) { + if (!isGitRepo()) return []; + + const result = runCommand('git diff --name-only HEAD'); + if (!result.success) return []; + + let files = result.output.split('\n').filter(Boolean); + + if (patterns.length > 0) { + files = files.filter(file => { + return patterns.some(pattern => { + const regex = new RegExp(pattern); + return regex.test(file); + }); + }); + } + + return files; +} + +/** + * Replace text in a file (cross-platform sed alternative) + */ +function replaceInFile(filePath, search, replace) { + const content = readFile(filePath); + if (content === null) return false; + + const newContent = content.replace(search, replace); + writeFile(filePath, newContent); + return true; +} + +/** + * Count occurrences of a pattern in a file + */ +function countInFile(filePath, pattern) { + const content = readFile(filePath); + if (content === null) return 0; + + const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern, 'g'); + const matches = content.match(regex); + return matches ? matches.length : 0; +} + +/** + * Search for pattern in file and return matching lines with line numbers + */ +function grepFile(filePath, pattern) { + const content = readFile(filePath); + if (content === null) return []; + + const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern); + const lines = content.split('\n'); + const results = []; + + lines.forEach((line, index) => { + if (regex.test(line)) { + results.push({ lineNumber: index + 1, content: line }); + } + }); + + return results; +} + +module.exports = { + // Platform info + isWindows, + isMacOS, + isLinux, + + // Directories + getHomeDir, + getClaudeDir, + getSessionsDir, + getLearnedSkillsDir, + getTempDir, + ensureDir, + + // Date/Time + getDateString, + getTimeString, + getDateTimeString, + + // File operations + findFiles, + readFile, + writeFile, + appendFile, + replaceInFile, + countInFile, + grepFile, + + // Hook I/O + readStdinJson, + log, + output, + + // System + commandExists, + runCommand, + isGitRepo, + getGitModifiedFiles +}; diff --git a/scripts/setup-package-manager.js b/scripts/setup-package-manager.js new file mode 100644 index 0000000..f765891 --- /dev/null +++ b/scripts/setup-package-manager.js @@ -0,0 +1,206 @@ +#!/usr/bin/env node +/** + * Package Manager Setup Script + * + * Interactive script to configure preferred package manager. + * Can be run directly or via the /setup-pm command. + * + * Usage: + * node scripts/setup-package-manager.js [pm-name] + * node scripts/setup-package-manager.js --detect + * node scripts/setup-package-manager.js --global pnpm + * node scripts/setup-package-manager.js --project bun + */ + +const { + PACKAGE_MANAGERS, + getPackageManager, + setPreferredPackageManager, + setProjectPackageManager, + getAvailablePackageManagers, + detectFromLockFile, + detectFromPackageJson, + getSelectionPrompt +} = require('./lib/package-manager'); +const { log } = require('./lib/utils'); + +function showHelp() { + console.log(` +Package Manager Setup for Claude Code + +Usage: + node scripts/setup-package-manager.js [options] [package-manager] + +Options: + --detect Detect and show current package manager + --global Set global preference (saves to ~/.claude/package-manager.json) + --project Set project preference (saves to .claude/package-manager.json) + --list List available package managers + --help Show this help message + +Package Managers: + npm Node Package Manager (default with Node.js) + pnpm Fast, disk space efficient package manager + yarn Classic Yarn package manager + bun All-in-one JavaScript runtime & toolkit + +Examples: + # Detect current package manager + node scripts/setup-package-manager.js --detect + + # Set pnpm as global preference + node scripts/setup-package-manager.js --global pnpm + + # Set bun for current project + node scripts/setup-package-manager.js --project bun + + # List available package managers + node scripts/setup-package-manager.js --list +`); +} + +function detectAndShow() { + const pm = getPackageManager(); + const available = getAvailablePackageManagers(); + const fromLock = detectFromLockFile(); + const fromPkg = detectFromPackageJson(); + + console.log('\n=== Package Manager Detection ===\n'); + + console.log('Current selection:'); + console.log(` Package Manager: ${pm.name}`); + console.log(` Source: ${pm.source}`); + console.log(''); + + console.log('Detection results:'); + console.log(` From package.json: ${fromPkg || 'not specified'}`); + console.log(` From lock file: ${fromLock || 'not found'}`); + console.log(` Environment var: ${process.env.CLAUDE_PACKAGE_MANAGER || 'not set'}`); + console.log(''); + + console.log('Available package managers:'); + for (const pmName of Object.keys(PACKAGE_MANAGERS)) { + const installed = available.includes(pmName); + const indicator = installed ? '✓' : '✗'; + const current = pmName === pm.name ? ' (current)' : ''; + console.log(` ${indicator} ${pmName}${current}`); + } + + console.log(''); + console.log('Commands:'); + console.log(` Install: ${pm.config.installCmd}`); + console.log(` Run script: ${pm.config.runCmd}