pan-wizard 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +772 -0
  3. package/agents/pan-debugger.md +1246 -0
  4. package/agents/pan-document_code.md +965 -0
  5. package/agents/pan-executor.md +469 -0
  6. package/agents/pan-integration-checker.md +443 -0
  7. package/agents/pan-phase-researcher.md +572 -0
  8. package/agents/pan-plan-checker.md +763 -0
  9. package/agents/pan-planner.md +1297 -0
  10. package/agents/pan-project-researcher.md +647 -0
  11. package/agents/pan-research-synthesizer.md +239 -0
  12. package/agents/pan-reviewer.md +112 -0
  13. package/agents/pan-roadmapper.md +642 -0
  14. package/agents/pan-verifier.md +672 -0
  15. package/assets/pan-logo-2000-transparent.svg +30 -0
  16. package/assets/pan-logo-2000.svg +43 -0
  17. package/assets/terminal.svg +119 -0
  18. package/bin/install-lib.cjs +616 -0
  19. package/bin/install.js +1936 -0
  20. package/commands/pan/add-phase.md +44 -0
  21. package/commands/pan/assumptions.md +47 -0
  22. package/commands/pan/audit-deployment.md +378 -0
  23. package/commands/pan/debug.md +168 -0
  24. package/commands/pan/discord.md +19 -0
  25. package/commands/pan/discuss-phase.md +84 -0
  26. package/commands/pan/exec-phase.md +45 -0
  27. package/commands/pan/focus-auto.md +323 -0
  28. package/commands/pan/focus-design.md +816 -0
  29. package/commands/pan/focus-exec.md +316 -0
  30. package/commands/pan/focus-plan.md +101 -0
  31. package/commands/pan/focus-scan.md +272 -0
  32. package/commands/pan/focus-sync.md +104 -0
  33. package/commands/pan/health.md +23 -0
  34. package/commands/pan/help.md +23 -0
  35. package/commands/pan/insert-phase.md +33 -0
  36. package/commands/pan/map-codebase.md +72 -0
  37. package/commands/pan/milestone-audit.md +37 -0
  38. package/commands/pan/milestone-cleanup.md +19 -0
  39. package/commands/pan/milestone-done.md +137 -0
  40. package/commands/pan/milestone-gaps.md +35 -0
  41. package/commands/pan/milestone-new.md +45 -0
  42. package/commands/pan/new-project.md +43 -0
  43. package/commands/pan/patches.md +110 -0
  44. package/commands/pan/pause.md +39 -0
  45. package/commands/pan/phase-budget.md +23 -0
  46. package/commands/pan/phase-tests.md +42 -0
  47. package/commands/pan/plan-phase.md +46 -0
  48. package/commands/pan/profile.md +36 -0
  49. package/commands/pan/progress.md +25 -0
  50. package/commands/pan/quick.md +42 -0
  51. package/commands/pan/remove-phase.md +32 -0
  52. package/commands/pan/research-phase.md +190 -0
  53. package/commands/pan/resume.md +41 -0
  54. package/commands/pan/retro.md +33 -0
  55. package/commands/pan/settings.md +37 -0
  56. package/commands/pan/todo-add.md +48 -0
  57. package/commands/pan/todo-check.md +46 -0
  58. package/commands/pan/update.md +38 -0
  59. package/commands/pan/verify-phase.md +39 -0
  60. package/hooks/dist/pan-check-update.js +62 -0
  61. package/hooks/dist/pan-context-monitor.js +122 -0
  62. package/hooks/dist/pan-statusline.js +108 -0
  63. package/package.json +66 -0
  64. package/pan-wizard-core/bin/lib/codebase.cjs +746 -0
  65. package/pan-wizard-core/bin/lib/commands.cjs +1435 -0
  66. package/pan-wizard-core/bin/lib/config.cjs +611 -0
  67. package/pan-wizard-core/bin/lib/constants.cjs +696 -0
  68. package/pan-wizard-core/bin/lib/context-budget.cjs +150 -0
  69. package/pan-wizard-core/bin/lib/core.cjs +650 -0
  70. package/pan-wizard-core/bin/lib/focus.cjs +900 -0
  71. package/pan-wizard-core/bin/lib/frontmatter.cjs +442 -0
  72. package/pan-wizard-core/bin/lib/init.cjs +881 -0
  73. package/pan-wizard-core/bin/lib/milestone.cjs +276 -0
  74. package/pan-wizard-core/bin/lib/phase.cjs +1212 -0
  75. package/pan-wizard-core/bin/lib/roadmap.cjs +470 -0
  76. package/pan-wizard-core/bin/lib/state.cjs +1029 -0
  77. package/pan-wizard-core/bin/lib/template.cjs +314 -0
  78. package/pan-wizard-core/bin/lib/utils.cjs +171 -0
  79. package/pan-wizard-core/bin/lib/verify.cjs +1808 -0
  80. package/pan-wizard-core/bin/pan-tools.cjs +773 -0
  81. package/pan-wizard-core/references/checkpoints.md +776 -0
  82. package/pan-wizard-core/references/continuation-format.md +249 -0
  83. package/pan-wizard-core/references/decimal-phase-calculation.md +65 -0
  84. package/pan-wizard-core/references/git-integration.md +248 -0
  85. package/pan-wizard-core/references/git-planning-commit.md +38 -0
  86. package/pan-wizard-core/references/model-profile-resolution.md +34 -0
  87. package/pan-wizard-core/references/model-profiles.md +111 -0
  88. package/pan-wizard-core/references/phase-argument-parsing.md +61 -0
  89. package/pan-wizard-core/references/planning-config.md +196 -0
  90. package/pan-wizard-core/references/questioning.md +145 -0
  91. package/pan-wizard-core/references/tdd.md +263 -0
  92. package/pan-wizard-core/references/ui-brand.md +160 -0
  93. package/pan-wizard-core/references/verification-patterns.md +612 -0
  94. package/pan-wizard-core/templates/codebase/architecture.md +283 -0
  95. package/pan-wizard-core/templates/codebase/best-practices.md +133 -0
  96. package/pan-wizard-core/templates/codebase/concerns.md +325 -0
  97. package/pan-wizard-core/templates/codebase/conventions.md +307 -0
  98. package/pan-wizard-core/templates/codebase/integrations.md +305 -0
  99. package/pan-wizard-core/templates/codebase/relationships.md +124 -0
  100. package/pan-wizard-core/templates/codebase/stack.md +199 -0
  101. package/pan-wizard-core/templates/codebase/structure.md +298 -0
  102. package/pan-wizard-core/templates/codebase/testing.md +480 -0
  103. package/pan-wizard-core/templates/config.json +37 -0
  104. package/pan-wizard-core/templates/context.md +283 -0
  105. package/pan-wizard-core/templates/continue-here.md +78 -0
  106. package/pan-wizard-core/templates/debug-subagent-prompt.md +91 -0
  107. package/pan-wizard-core/templates/debug.md +164 -0
  108. package/pan-wizard-core/templates/discovery.md +146 -0
  109. package/pan-wizard-core/templates/milestone-archive.md +123 -0
  110. package/pan-wizard-core/templates/milestone.md +115 -0
  111. package/pan-wizard-core/templates/phase-prompt.md +593 -0
  112. package/pan-wizard-core/templates/planner-subagent-prompt.md +117 -0
  113. package/pan-wizard-core/templates/project.md +184 -0
  114. package/pan-wizard-core/templates/requirements.md +231 -0
  115. package/pan-wizard-core/templates/research-project/architecture.md +204 -0
  116. package/pan-wizard-core/templates/research-project/features.md +147 -0
  117. package/pan-wizard-core/templates/research-project/pitfalls.md +200 -0
  118. package/pan-wizard-core/templates/research-project/stack.md +120 -0
  119. package/pan-wizard-core/templates/research-project/summary.md +170 -0
  120. package/pan-wizard-core/templates/research.md +552 -0
  121. package/pan-wizard-core/templates/retrospective.md +54 -0
  122. package/pan-wizard-core/templates/roadmap.md +202 -0
  123. package/pan-wizard-core/templates/standards.md +24 -0
  124. package/pan-wizard-core/templates/state.md +176 -0
  125. package/pan-wizard-core/templates/summary-complex.md +59 -0
  126. package/pan-wizard-core/templates/summary-minimal.md +41 -0
  127. package/pan-wizard-core/templates/summary-standard.md +49 -0
  128. package/pan-wizard-core/templates/summary.md +249 -0
  129. package/pan-wizard-core/templates/uat.md +247 -0
  130. package/pan-wizard-core/templates/user-setup.md +311 -0
  131. package/pan-wizard-core/templates/validation.md +76 -0
  132. package/pan-wizard-core/templates/verification-report.md +322 -0
  133. package/pan-wizard-core/workflows/add-phase.md +111 -0
  134. package/pan-wizard-core/workflows/assumptions.md +178 -0
  135. package/pan-wizard-core/workflows/diagnose-issues.md +219 -0
  136. package/pan-wizard-core/workflows/discuss-phase.md +542 -0
  137. package/pan-wizard-core/workflows/exec-phase.md +572 -0
  138. package/pan-wizard-core/workflows/execute-plan.md +448 -0
  139. package/pan-wizard-core/workflows/health.md +156 -0
  140. package/pan-wizard-core/workflows/help.md +431 -0
  141. package/pan-wizard-core/workflows/insert-phase.md +129 -0
  142. package/pan-wizard-core/workflows/map-codebase.md +401 -0
  143. package/pan-wizard-core/workflows/milestone-audit.md +297 -0
  144. package/pan-wizard-core/workflows/milestone-cleanup.md +152 -0
  145. package/pan-wizard-core/workflows/milestone-gaps.md +274 -0
  146. package/pan-wizard-core/workflows/milestone-new.md +382 -0
  147. package/pan-wizard-core/workflows/new-project.md +1178 -0
  148. package/pan-wizard-core/workflows/pause.md +122 -0
  149. package/pan-wizard-core/workflows/phase-tests.md +388 -0
  150. package/pan-wizard-core/workflows/plan-phase.md +569 -0
  151. package/pan-wizard-core/workflows/profile.md +115 -0
  152. package/pan-wizard-core/workflows/progress.md +381 -0
  153. package/pan-wizard-core/workflows/quick.md +453 -0
  154. package/pan-wizard-core/workflows/remove-phase.md +154 -0
  155. package/pan-wizard-core/workflows/research-phase.md +73 -0
  156. package/pan-wizard-core/workflows/resume-project.md +306 -0
  157. package/pan-wizard-core/workflows/retro.md +121 -0
  158. package/pan-wizard-core/workflows/settings.md +213 -0
  159. package/pan-wizard-core/workflows/todo-add.md +157 -0
  160. package/pan-wizard-core/workflows/todo-check.md +176 -0
  161. package/pan-wizard-core/workflows/transition.md +544 -0
  162. package/pan-wizard-core/workflows/update.md +219 -0
  163. package/pan-wizard-core/workflows/verify-phase.md +301 -0
  164. package/scripts/build-hooks.js +43 -0
package/bin/install.js ADDED
@@ -0,0 +1,1936 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const readline = require('readline');
7
+ const crypto = require('crypto');
8
+
9
+ // Extracted pure functions (testable independently)
10
+ const lib = require('./install-lib.cjs');
11
+ const {
12
+ colorNameToHex, claudeToOpencodeTools, claudeToGeminiTools, claudeToCopilotTools,
13
+ getDirName, getConfigDirFromHome, expandTilde, toSingleLine, yamlQuote, buildHookCommand,
14
+ extractFrontmatterAndBody, extractFrontmatterField,
15
+ convertToolName, convertGeminiToolName, convertCopilotToolName,
16
+ convertSlashCommandsToCodexSkillMentions, convertSlashCommandsToCopilotSkillMentions,
17
+ convertClaudeToCodexMarkdown, convertClaudeToCopilotMarkdown,
18
+ convertClaudeToOpencodeFrontmatter, convertClaudeToGeminiToml, convertClaudeToGeminiAgent,
19
+ rewriteAskUserQuestionForCopilot, stripSubTags,
20
+ getCodexSkillAdapterHeader, convertClaudeCommandToCodexSkill,
21
+ getCopilotSkillAdapterHeader, convertClaudeCommandToCopilotSkill, convertClaudeToCopilotAgent,
22
+ processAttribution, parseJsonc,
23
+ } = lib;
24
+
25
+ // Colors
26
+ const cyan = '\x1b[36m';
27
+ const green = '\x1b[32m';
28
+ const yellow = '\x1b[33m';
29
+ const red = '\x1b[31m';
30
+ const dim = '\x1b[2m';
31
+ const reset = '\x1b[0m';
32
+
33
+ // Get version from package.json
34
+ const pkg = require('../package.json');
35
+
36
+ // Source repo root — prevent installing PAN into its own source directory
37
+ const PAN_SOURCE_ROOT = path.resolve(__dirname, '..');
38
+
39
+ // Parse args
40
+ const args = process.argv.slice(2);
41
+ const hasGlobal = args.includes('--global') || args.includes('-g');
42
+ const hasLocal = args.includes('--local') || args.includes('-l');
43
+ const hasOpencode = args.includes('--opencode');
44
+ const hasClaude = args.includes('--claude');
45
+ const hasGemini = args.includes('--gemini');
46
+ const hasCodex = args.includes('--codex');
47
+ const hasCopilot = args.includes('--copilot');
48
+ const hasBoth = args.includes('--both'); // Legacy flag, keeps working
49
+ const hasAll = args.includes('--all');
50
+ const hasUninstall = args.includes('--uninstall') || args.includes('-u');
51
+
52
+ // Runtime selection - can be set by flags or interactive prompt
53
+ let selectedRuntimes = [];
54
+ if (hasAll) {
55
+ selectedRuntimes = ['claude', 'opencode', 'gemini', 'codex', 'copilot'];
56
+ } else if (hasBoth) {
57
+ selectedRuntimes = ['claude', 'opencode'];
58
+ } else {
59
+ if (hasOpencode) selectedRuntimes.push('opencode');
60
+ if (hasClaude) selectedRuntimes.push('claude');
61
+ if (hasGemini) selectedRuntimes.push('gemini');
62
+ if (hasCodex) selectedRuntimes.push('codex');
63
+ if (hasCopilot) selectedRuntimes.push('copilot');
64
+ }
65
+
66
+ /**
67
+ * Get the global config directory for OpenCode
68
+ * OpenCode follows XDG Base Directory spec and uses ~/.config/opencode/
69
+ * Priority: OPENCODE_CONFIG_DIR > dirname(OPENCODE_CONFIG) > XDG_CONFIG_HOME/opencode > ~/.config/opencode
70
+ */
71
+ function getOpencodeGlobalDir() {
72
+ // 1. Explicit OPENCODE_CONFIG_DIR env var
73
+ if (process.env.OPENCODE_CONFIG_DIR) {
74
+ return expandTilde(process.env.OPENCODE_CONFIG_DIR);
75
+ }
76
+
77
+ // 2. OPENCODE_CONFIG env var (use its directory)
78
+ if (process.env.OPENCODE_CONFIG) {
79
+ return path.dirname(expandTilde(process.env.OPENCODE_CONFIG));
80
+ }
81
+
82
+ // 3. XDG_CONFIG_HOME/opencode
83
+ if (process.env.XDG_CONFIG_HOME) {
84
+ return path.join(expandTilde(process.env.XDG_CONFIG_HOME), 'opencode');
85
+ }
86
+
87
+ // 4. Default: ~/.config/opencode (XDG default)
88
+ return path.join(os.homedir(), '.config', 'opencode');
89
+ }
90
+
91
+ /**
92
+ * Get the global config directory for a runtime
93
+ * @param {string} runtime - 'claude', 'opencode', 'gemini', or 'codex'
94
+ * @param {string|null} explicitDir - Explicit directory from --config-dir flag
95
+ */
96
+ function getGlobalDir(runtime, explicitDir = null) {
97
+ if (runtime === 'opencode') {
98
+ // For OpenCode, --config-dir overrides env vars
99
+ if (explicitDir) {
100
+ return expandTilde(explicitDir);
101
+ }
102
+ return getOpencodeGlobalDir();
103
+ }
104
+
105
+ if (runtime === 'gemini') {
106
+ // Gemini: --config-dir > GEMINI_CONFIG_DIR > ~/.gemini
107
+ if (explicitDir) {
108
+ return expandTilde(explicitDir);
109
+ }
110
+ if (process.env.GEMINI_CONFIG_DIR) {
111
+ return expandTilde(process.env.GEMINI_CONFIG_DIR);
112
+ }
113
+ return path.join(os.homedir(), '.gemini');
114
+ }
115
+
116
+ if (runtime === 'codex') {
117
+ // Codex: --config-dir > CODEX_HOME > ~/.codex
118
+ if (explicitDir) {
119
+ return expandTilde(explicitDir);
120
+ }
121
+ if (process.env.CODEX_HOME) {
122
+ return expandTilde(process.env.CODEX_HOME);
123
+ }
124
+ return path.join(os.homedir(), '.codex');
125
+ }
126
+
127
+ if (runtime === 'copilot') {
128
+ // Copilot CLI: --config-dir > COPILOT_CONFIG_DIR > ~/.copilot
129
+ if (explicitDir) {
130
+ return expandTilde(explicitDir);
131
+ }
132
+ if (process.env.COPILOT_CONFIG_DIR) {
133
+ return expandTilde(process.env.COPILOT_CONFIG_DIR);
134
+ }
135
+ return path.join(os.homedir(), '.copilot');
136
+ }
137
+
138
+ // Claude Code: --config-dir > CLAUDE_CONFIG_DIR > ~/.claude
139
+ if (explicitDir) {
140
+ return expandTilde(explicitDir);
141
+ }
142
+ if (process.env.CLAUDE_CONFIG_DIR) {
143
+ return expandTilde(process.env.CLAUDE_CONFIG_DIR);
144
+ }
145
+ return path.join(os.homedir(), '.claude');
146
+ }
147
+
148
+ const banner = '\n' +
149
+ cyan + ' ██████╗ █████╗ ███╗ ██╗\n' +
150
+ ' ██╔══██╗██╔══██╗████╗ ██║\n' +
151
+ ' ██████╔╝███████║██╔██╗ ██║\n' +
152
+ ' ██╔═══╝ ██╔══██║██║╚██╗██║\n' +
153
+ ' ██║ ██║ ██║██║ ╚████║\n' +
154
+ ' ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝' + reset + '\n' +
155
+ '\n' +
156
+ ' PAN Wizard ' + dim + 'v' + pkg.version + reset + '\n' +
157
+ ' A lightweight workflow automation and context engineering\n' +
158
+ ' system for Claude Code, OpenCode, Gemini, Codex, and Copilot CLI.\n';
159
+
160
+ // Parse --config-dir argument
161
+ function parseConfigDirArg() {
162
+ const configDirIndex = args.findIndex(arg => arg === '--config-dir' || arg === '-c');
163
+ if (configDirIndex !== -1) {
164
+ const nextArg = args[configDirIndex + 1];
165
+ // Error if --config-dir is provided without a value or next arg is another flag
166
+ if (!nextArg || nextArg.startsWith('-')) {
167
+ console.error(` ${yellow}--config-dir requires a path argument${reset}`);
168
+ process.exit(1);
169
+ }
170
+ return nextArg;
171
+ }
172
+ // Also handle --config-dir=value format
173
+ const configDirArg = args.find(arg => arg.startsWith('--config-dir=') || arg.startsWith('-c='));
174
+ if (configDirArg) {
175
+ const value = configDirArg.split('=')[1];
176
+ if (!value) {
177
+ console.error(` ${yellow}--config-dir requires a non-empty path${reset}`);
178
+ process.exit(1);
179
+ }
180
+ return value;
181
+ }
182
+ return null;
183
+ }
184
+ const explicitConfigDir = parseConfigDirArg();
185
+ const hasHelp = args.includes('--help') || args.includes('-h');
186
+ const forceStatusline = args.includes('--force-statusline');
187
+
188
+ console.log(banner);
189
+
190
+ // Show help if requested
191
+ if (hasHelp) {
192
+ console.log(` ${yellow}Usage:${reset} npx pan-wizard [options]\n\n ${yellow}Options:${reset}\n ${cyan}-l, --local${reset} Install locally to current directory (default)\n ${cyan}-g, --global${reset} Install globally to config directory\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall PAN (remove all PAN files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx pan-wizard\n\n ${dim}# Install for Claude Code in current project (default)${reset}\n npx pan-wizard --claude --local\n\n ${dim}# Install for all runtimes in current project${reset}\n npx pan-wizard --all --local\n\n ${dim}# Install globally (available in all projects)${reset}\n npx pan-wizard --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx pan-wizard --gemini --global\n\n ${dim}# Install to custom config directory${reset}\n npx pan-wizard --codex --global --config-dir ~/.codex-work\n\n ${dim}# Uninstall PAN from Codex globally${reset}\n npx pan-wizard --codex --global --uninstall\n\n ${yellow}Notes:${reset}\n By default, PAN installs into the current project directory only.\n Use --global to install system-wide (writes to ~/.claude, ~/.gemini, etc.).\n The --config-dir option takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME.\n`);
193
+ process.exit(0);
194
+ }
195
+
196
+ /**
197
+ * Read and parse settings.json, returning empty object if it doesn't exist
198
+ */
199
+ function readSettings(settingsPath) {
200
+ try {
201
+ return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
202
+ } catch {
203
+ return {};
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Write settings.json with proper formatting
209
+ */
210
+ function writeSettings(settingsPath, settings) {
211
+ try {
212
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
213
+ } catch (e) {
214
+ console.error(` ${yellow}⚠${reset} Failed to write settings: ${e.message}`);
215
+ }
216
+ }
217
+
218
+ // Cache for attribution settings (populated once per runtime during install)
219
+ const attributionCache = new Map();
220
+
221
+ /**
222
+ * Get commit attribution setting for a runtime
223
+ * @param {string} runtime - 'claude', 'opencode', 'gemini', or 'codex'
224
+ * @returns {null|undefined|string} null = remove, undefined = keep default, string = custom
225
+ */
226
+ function getCommitAttribution(runtime) {
227
+ // Return cached value if available
228
+ if (attributionCache.has(runtime)) {
229
+ return attributionCache.get(runtime);
230
+ }
231
+
232
+ let result;
233
+
234
+ if (runtime === 'opencode') {
235
+ const config = readSettings(path.join(getGlobalDir('opencode', null), 'opencode.json'));
236
+ result = config.disable_ai_attribution === true ? null : undefined;
237
+ } else if (runtime === 'gemini') {
238
+ // Gemini: check gemini settings.json for attribution config
239
+ const settings = readSettings(path.join(getGlobalDir('gemini', explicitConfigDir), 'settings.json'));
240
+ if (!settings.attribution || settings.attribution.commit === undefined) {
241
+ result = undefined;
242
+ } else if (settings.attribution.commit === '') {
243
+ result = null;
244
+ } else {
245
+ result = settings.attribution.commit;
246
+ }
247
+ } else if (runtime === 'claude') {
248
+ // Claude Code
249
+ const settings = readSettings(path.join(getGlobalDir('claude', explicitConfigDir), 'settings.json'));
250
+ if (!settings.attribution || settings.attribution.commit === undefined) {
251
+ result = undefined;
252
+ } else if (settings.attribution.commit === '') {
253
+ result = null;
254
+ } else {
255
+ result = settings.attribution.commit;
256
+ }
257
+ } else if (runtime === 'copilot') {
258
+ // Copilot CLI: check config.json for attribution setting
259
+ const config = readSettings(path.join(getGlobalDir('copilot', explicitConfigDir), 'config.json'));
260
+ if (!config.attribution || config.attribution.commit === undefined) {
261
+ result = undefined;
262
+ } else if (config.attribution.commit === '') {
263
+ result = null;
264
+ } else {
265
+ result = config.attribution.commit;
266
+ }
267
+ } else {
268
+ // Codex currently has no attribution setting equivalent
269
+ result = undefined;
270
+ }
271
+
272
+ // Cache and return
273
+ attributionCache.set(runtime, result);
274
+ return result;
275
+ }
276
+
277
+ // processAttribution, parseJsonc, and all pure converter functions
278
+ // are provided by install-lib.cjs (see require at top of file).
279
+
280
+ /**
281
+ * Copy commands to a flat structure for OpenCode
282
+ * OpenCode expects: command/pan-help.md (invoked as /pan-help)
283
+ * Source structure: commands/pan/help.md
284
+ *
285
+ * @param {string} srcDir - Source directory (e.g., commands/pan/)
286
+ * @param {string} destDir - Destination directory (e.g., command/)
287
+ * @param {string} prefix - Prefix for filenames (e.g., 'pan')
288
+ * @param {string} pathPrefix - Path prefix for file references
289
+ * @param {string} runtime - Target runtime ('claude' or 'opencode')
290
+ */
291
+ function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) {
292
+ if (!fs.existsSync(srcDir)) {
293
+ return;
294
+ }
295
+
296
+ // Remove old pan-*.md files before copying new ones
297
+ if (fs.existsSync(destDir)) {
298
+ for (const file of fs.readdirSync(destDir)) {
299
+ if (file.startsWith(`${prefix}-`) && file.endsWith('.md')) {
300
+ try { fs.unlinkSync(path.join(destDir, file)); } catch {}
301
+ }
302
+ }
303
+ } else {
304
+ try { fs.mkdirSync(destDir, { recursive: true }); } catch {}
305
+ }
306
+
307
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
308
+
309
+ for (const entry of entries) {
310
+ const srcPath = path.join(srcDir, entry.name);
311
+
312
+ if (entry.isDirectory()) {
313
+ // Recurse into subdirectories, adding to prefix
314
+ // e.g., commands/pan/debug/start.md -> command/pan-debug-start.md
315
+ copyFlattenedCommands(srcPath, destDir, `${prefix}-${entry.name}`, pathPrefix, runtime);
316
+ } else if (entry.name.endsWith('.md')) {
317
+ // Flatten: help.md -> pan-help.md
318
+ const baseName = entry.name.replace('.md', '');
319
+ const destName = `${prefix}-${baseName}.md`;
320
+ const destPath = path.join(destDir, destName);
321
+
322
+ let content = fs.readFileSync(srcPath, 'utf8');
323
+ const globalClaudeRegex = /~\/\.claude\//g;
324
+ const localClaudeRegex = /\.\/\.claude\//g;
325
+ const opencodeDirRegex = /~\/\.opencode\//g;
326
+ content = content.replace(globalClaudeRegex, pathPrefix);
327
+ content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
328
+ content = content.replace(opencodeDirRegex, pathPrefix);
329
+ content = processAttribution(content, getCommitAttribution(runtime));
330
+ content = convertClaudeToOpencodeFrontmatter(content);
331
+
332
+ fs.writeFileSync(destPath, content);
333
+ }
334
+ }
335
+ }
336
+
337
+ function listCodexSkillNames(skillsDir, prefix = 'pan-') {
338
+ if (!fs.existsSync(skillsDir)) return [];
339
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
340
+ return entries
341
+ .filter(entry => entry.isDirectory() && entry.name.startsWith(prefix))
342
+ .filter(entry => fs.existsSync(path.join(skillsDir, entry.name, 'SKILL.md')))
343
+ .map(entry => entry.name)
344
+ .sort();
345
+ }
346
+
347
+ function copyCommandsAsCodexSkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
348
+ if (!fs.existsSync(srcDir)) {
349
+ return;
350
+ }
351
+
352
+ try { fs.mkdirSync(skillsDir, { recursive: true }); } catch {}
353
+
354
+ // Remove previous PAN Codex skills to avoid stale command skills.
355
+ const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
356
+ for (const entry of existing) {
357
+ if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
358
+ try { fs.rmSync(path.join(skillsDir, entry.name), { recursive: true }); } catch {}
359
+ }
360
+ }
361
+
362
+ function recurse(currentSrcDir, currentPrefix) {
363
+ const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true });
364
+
365
+ for (const entry of entries) {
366
+ const srcPath = path.join(currentSrcDir, entry.name);
367
+ if (entry.isDirectory()) {
368
+ recurse(srcPath, `${currentPrefix}-${entry.name}`);
369
+ continue;
370
+ }
371
+
372
+ if (!entry.name.endsWith('.md')) {
373
+ continue;
374
+ }
375
+
376
+ const baseName = entry.name.replace('.md', '');
377
+ const skillName = `${currentPrefix}-${baseName}`;
378
+ const skillDir = path.join(skillsDir, skillName);
379
+ fs.mkdirSync(skillDir, { recursive: true });
380
+
381
+ let content = fs.readFileSync(srcPath, 'utf8');
382
+ const globalClaudeRegex = /~\/\.claude\//g;
383
+ const localClaudeRegex = /\.\/\.claude\//g;
384
+ const codexDirRegex = /~\/\.codex\//g;
385
+ content = content.replace(globalClaudeRegex, pathPrefix);
386
+ content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
387
+ content = content.replace(codexDirRegex, pathPrefix);
388
+ content = processAttribution(content, getCommitAttribution(runtime));
389
+ content = convertClaudeCommandToCodexSkill(content, skillName);
390
+
391
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
392
+ }
393
+ }
394
+
395
+ recurse(srcDir, prefix);
396
+ }
397
+
398
+ /**
399
+ * Copy PAN commands as Copilot CLI skills.
400
+ * Creates skills/pan-{name}/SKILL.md directory structure.
401
+ * Similar to copyCommandsAsCodexSkills but uses Copilot CLI converters.
402
+ */
403
+ function copyCommandsAsCopilotSkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
404
+ if (!fs.existsSync(srcDir)) {
405
+ return;
406
+ }
407
+
408
+ try { fs.mkdirSync(skillsDir, { recursive: true }); } catch {}
409
+
410
+ // Remove previous PAN Copilot skills to avoid stale command skills
411
+ const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
412
+ for (const entry of existing) {
413
+ if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
414
+ try { fs.rmSync(path.join(skillsDir, entry.name), { recursive: true }); } catch {}
415
+ }
416
+ }
417
+
418
+ function recurse(currentSrcDir, currentPrefix) {
419
+ const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true });
420
+
421
+ for (const entry of entries) {
422
+ const srcPath = path.join(currentSrcDir, entry.name);
423
+ if (entry.isDirectory()) {
424
+ recurse(srcPath, `${currentPrefix}-${entry.name}`);
425
+ continue;
426
+ }
427
+
428
+ if (!entry.name.endsWith('.md')) {
429
+ continue;
430
+ }
431
+
432
+ const baseName = entry.name.replace('.md', '');
433
+ const skillName = `${currentPrefix}-${baseName}`;
434
+ const skillDir = path.join(skillsDir, skillName);
435
+ fs.mkdirSync(skillDir, { recursive: true });
436
+
437
+ let content = fs.readFileSync(srcPath, 'utf8');
438
+ const globalClaudeRegex = /~\/\.claude\//g;
439
+ const localClaudeRegex = /\.\/\.claude\//g;
440
+ content = content.replace(globalClaudeRegex, pathPrefix);
441
+ content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
442
+ content = processAttribution(content, getCommitAttribution(runtime));
443
+ content = convertClaudeCommandToCopilotSkill(content, skillName);
444
+
445
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
446
+ }
447
+ }
448
+
449
+ recurse(srcDir, prefix);
450
+ }
451
+
452
+ /**
453
+ * Recursively copy directory, replacing paths in .md files
454
+ * Deletes existing destDir first to remove orphaned files from previous versions
455
+ * @param {string} srcDir - Source directory
456
+ * @param {string} destDir - Destination directory
457
+ * @param {string} pathPrefix - Path prefix for file references
458
+ * @param {string} runtime - Target runtime ('claude', 'opencode', 'gemini', 'codex', 'copilot')
459
+ */
460
+ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand = false) {
461
+ const isOpencode = runtime === 'opencode';
462
+ const isCodex = runtime === 'codex';
463
+ const dirName = getDirName(runtime);
464
+
465
+ // Clean install: remove existing destination to prevent orphaned files
466
+ try {
467
+ if (fs.existsSync(destDir)) {
468
+ fs.rmSync(destDir, { recursive: true });
469
+ }
470
+ fs.mkdirSync(destDir, { recursive: true });
471
+ } catch {}
472
+
473
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
474
+
475
+ for (const entry of entries) {
476
+ const srcPath = path.join(srcDir, entry.name);
477
+ const destPath = path.join(destDir, entry.name);
478
+
479
+ if (entry.isDirectory()) {
480
+ copyWithPathReplacement(srcPath, destPath, pathPrefix, runtime, isCommand);
481
+ } else if (entry.name.endsWith('.md')) {
482
+ try {
483
+ // Replace ~/.claude/ and ./.claude/ with runtime-appropriate paths
484
+ let content = fs.readFileSync(srcPath, 'utf8');
485
+ const globalClaudeRegex = /~\/\.claude\//g;
486
+ const localClaudeRegex = /\.\/\.claude\//g;
487
+ content = content.replace(globalClaudeRegex, pathPrefix);
488
+ content = content.replace(localClaudeRegex, `./${dirName}/`);
489
+ content = processAttribution(content, getCommitAttribution(runtime));
490
+
491
+ // Convert frontmatter for opencode compatibility
492
+ if (isOpencode) {
493
+ content = convertClaudeToOpencodeFrontmatter(content);
494
+ fs.writeFileSync(destPath, content);
495
+ } else if (runtime === 'gemini') {
496
+ if (isCommand) {
497
+ // Convert to TOML for Gemini (strip <sub> tags — terminals can't render subscript)
498
+ content = stripSubTags(content);
499
+ const tomlContent = convertClaudeToGeminiToml(content);
500
+ // Replace extension with .toml
501
+ const tomlPath = destPath.replace(/\.md$/, '.toml');
502
+ fs.writeFileSync(tomlPath, tomlContent);
503
+ } else {
504
+ fs.writeFileSync(destPath, content);
505
+ }
506
+ } else if (isCodex) {
507
+ content = convertClaudeToCodexMarkdown(content);
508
+ fs.writeFileSync(destPath, content);
509
+ } else if (runtime === 'copilot') {
510
+ content = convertClaudeToCopilotMarkdown(content);
511
+ fs.writeFileSync(destPath, content);
512
+ } else {
513
+ fs.writeFileSync(destPath, content);
514
+ }
515
+ } catch {}
516
+ } else {
517
+ try { fs.copyFileSync(srcPath, destPath); } catch {}
518
+ }
519
+ }
520
+ }
521
+
522
+ /**
523
+ * Clean up orphaned files from previous PAN versions
524
+ */
525
+ function cleanupOrphanedFiles(configDir) {
526
+ const orphanedFiles = [
527
+ 'hooks/pan-notify.sh', // Removed in v1.6.x
528
+ 'hooks/statusline.js', // Renamed to pan-statusline.js in v1.9.0
529
+ ];
530
+
531
+ for (const relPath of orphanedFiles) {
532
+ const fullPath = path.join(configDir, relPath);
533
+ if (fs.existsSync(fullPath)) {
534
+ fs.unlinkSync(fullPath);
535
+ console.log(` ${green}✓${reset} Removed orphaned ${relPath}`);
536
+ }
537
+ }
538
+ }
539
+
540
+ /**
541
+ * Clean up orphaned hook registrations from settings.json
542
+ */
543
+ function cleanupOrphanedHooks(settings) {
544
+ const orphanedHookPatterns = [
545
+ 'pan-notify.sh', // Removed in v1.6.x
546
+ 'hooks/statusline.js', // Renamed to pan-statusline.js in v1.9.0
547
+ 'pan-intel-index.js', // Removed in v1.9.2
548
+ 'pan-intel-session.js', // Removed in v1.9.2
549
+ 'pan-intel-prune.js', // Removed in v1.9.2
550
+ ];
551
+
552
+ let cleanedHooks = false;
553
+
554
+ // Check all hook event types (Stop, SessionStart, etc.)
555
+ if (settings.hooks) {
556
+ for (const eventType of Object.keys(settings.hooks)) {
557
+ const hookEntries = settings.hooks[eventType];
558
+ if (Array.isArray(hookEntries)) {
559
+ // Filter out entries that contain orphaned hooks
560
+ const filtered = hookEntries.filter(entry => {
561
+ if (entry.hooks && Array.isArray(entry.hooks)) {
562
+ // Check if any hook in this entry matches orphaned patterns
563
+ const hasOrphaned = entry.hooks.some(h =>
564
+ h.command && orphanedHookPatterns.some(pattern => h.command.includes(pattern))
565
+ );
566
+ if (hasOrphaned) {
567
+ cleanedHooks = true;
568
+ return false; // Remove this entry
569
+ }
570
+ }
571
+ return true; // Keep this entry
572
+ });
573
+ settings.hooks[eventType] = filtered;
574
+ }
575
+ }
576
+ }
577
+
578
+ if (cleanedHooks) {
579
+ console.log(` ${green}✓${reset} Removed orphaned hook registrations`);
580
+ }
581
+
582
+ // Fix #330: Update statusLine if it points to old statusline.js path
583
+ if (settings.statusLine && settings.statusLine.command &&
584
+ settings.statusLine.command.includes('statusline.js') &&
585
+ !settings.statusLine.command.includes('pan-statusline.js')) {
586
+ // Replace old path with new path
587
+ settings.statusLine.command = settings.statusLine.command.replace(
588
+ /statusline\.js/,
589
+ 'pan-statusline.js'
590
+ );
591
+ console.log(` ${green}✓${reset} Updated statusline path (statusline.js → pan-statusline.js)`);
592
+ }
593
+
594
+ return settings;
595
+ }
596
+
597
+ /**
598
+ * Uninstall PAN from the specified directory for a specific runtime
599
+ * Removes only PAN-specific files/directories, preserves user content
600
+ * @param {boolean} isGlobal - Whether to uninstall from global or local
601
+ * @param {string} runtime - Target runtime ('claude', 'opencode', 'gemini', 'codex')
602
+ */
603
+ function uninstall(isGlobal, runtime = 'claude') {
604
+ const isOpencode = runtime === 'opencode';
605
+ const isCodex = runtime === 'codex';
606
+ const isCopilot = runtime === 'copilot';
607
+ const dirName = getDirName(runtime);
608
+
609
+ // Get the target directory based on runtime and install type
610
+ const targetDir = isGlobal
611
+ ? getGlobalDir(runtime, explicitConfigDir)
612
+ : path.join(process.cwd(), dirName);
613
+
614
+ const locationLabel = isGlobal
615
+ ? targetDir.replace(os.homedir(), '~')
616
+ : targetDir.replace(process.cwd(), '.');
617
+
618
+ let runtimeLabel = 'Claude Code';
619
+ if (runtime === 'opencode') runtimeLabel = 'OpenCode';
620
+ if (runtime === 'gemini') runtimeLabel = 'Gemini';
621
+ if (runtime === 'codex') runtimeLabel = 'Codex';
622
+ if (runtime === 'copilot') runtimeLabel = 'GitHub Copilot CLI';
623
+
624
+ // Guard: never uninstall from the PAN source repository itself
625
+ if (path.resolve(process.cwd()) === PAN_SOURCE_ROOT) {
626
+ console.error(`\n ${red}✗${reset} Refusing to uninstall from PAN's own source repository.`);
627
+ console.error(` Run from your target project directory instead.\n`);
628
+ process.exit(1);
629
+ }
630
+
631
+ console.log(` Uninstalling PAN from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`);
632
+
633
+ // Check if target directory exists
634
+ if (!fs.existsSync(targetDir)) {
635
+ console.log(` ${yellow}⚠${reset} Directory does not exist: ${locationLabel}`);
636
+ console.log(` Nothing to uninstall.\n`);
637
+ return;
638
+ }
639
+
640
+ let removedCount = 0;
641
+
642
+ // 1. Remove PAN commands/skills
643
+ if (isOpencode) {
644
+ // OpenCode: remove command/pan-*.md files
645
+ const commandDir = path.join(targetDir, 'command');
646
+ if (fs.existsSync(commandDir)) {
647
+ const files = fs.readdirSync(commandDir);
648
+ for (const file of files) {
649
+ if (file.startsWith('pan-') && file.endsWith('.md')) {
650
+ try { fs.unlinkSync(path.join(commandDir, file)); } catch {}
651
+ removedCount++;
652
+ }
653
+ }
654
+ console.log(` ${green}✓${reset} Removed PAN commands from command/`);
655
+ }
656
+ } else if (isCodex || isCopilot) {
657
+ // Codex & Copilot CLI: remove skills/pan-*/SKILL.md skill directories
658
+ const skillsDir = path.join(targetDir, 'skills');
659
+ if (fs.existsSync(skillsDir)) {
660
+ let skillCount = 0;
661
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
662
+ for (const entry of entries) {
663
+ if (entry.isDirectory() && entry.name.startsWith('pan-')) {
664
+ try { fs.rmSync(path.join(skillsDir, entry.name), { recursive: true }); } catch {}
665
+ skillCount++;
666
+ }
667
+ }
668
+ if (skillCount > 0) {
669
+ removedCount++;
670
+ console.log(` ${green}✓${reset} Removed ${skillCount} ${isCopilot ? 'Copilot CLI' : 'Codex'} skills`);
671
+ }
672
+ }
673
+ } else {
674
+ // Claude Code & Gemini: remove commands/pan/ directory
675
+ const panCommandsDir = path.join(targetDir, 'commands', 'pan');
676
+ if (fs.existsSync(panCommandsDir)) {
677
+ try { fs.rmSync(panCommandsDir, { recursive: true }); } catch {}
678
+ removedCount++;
679
+ console.log(` ${green}✓${reset} Removed commands/pan/`);
680
+ }
681
+ }
682
+
683
+ // 2. Remove pan-wizard-core directory
684
+ const panDir = path.join(targetDir, 'pan-wizard-core');
685
+ if (fs.existsSync(panDir)) {
686
+ try { fs.rmSync(panDir, { recursive: true }); } catch {}
687
+ removedCount++;
688
+ console.log(` ${green}✓${reset} Removed pan-wizard-core/`);
689
+ }
690
+
691
+ // 3. Remove PAN agents (pan-*.md files only)
692
+ const agentsDir = path.join(targetDir, 'agents');
693
+ if (fs.existsSync(agentsDir)) {
694
+ const files = fs.readdirSync(agentsDir);
695
+ let agentCount = 0;
696
+ for (const file of files) {
697
+ if (file.startsWith('pan-') && file.endsWith('.md')) {
698
+ try { fs.unlinkSync(path.join(agentsDir, file)); } catch {}
699
+ agentCount++;
700
+ }
701
+ }
702
+ if (agentCount > 0) {
703
+ removedCount++;
704
+ console.log(` ${green}✓${reset} Removed ${agentCount} PAN agents`);
705
+ }
706
+ }
707
+
708
+ // 4. Remove PAN hooks
709
+ const hooksDir = path.join(targetDir, 'hooks');
710
+ if (fs.existsSync(hooksDir)) {
711
+ const panHooks = ['pan-statusline.js', 'pan-check-update.js', 'pan-check-update.sh', 'pan-context-monitor.js'];
712
+ let hookCount = 0;
713
+ for (const hook of panHooks) {
714
+ const hookPath = path.join(hooksDir, hook);
715
+ if (fs.existsSync(hookPath)) {
716
+ try { fs.unlinkSync(hookPath); } catch {}
717
+ hookCount++;
718
+ }
719
+ }
720
+ if (hookCount > 0) {
721
+ removedCount++;
722
+ console.log(` ${green}✓${reset} Removed ${hookCount} PAN hooks`);
723
+ }
724
+ }
725
+
726
+ // 5. Remove PAN package.json (CommonJS mode marker)
727
+ const pkgJsonPath = path.join(targetDir, 'package.json');
728
+ if (fs.existsSync(pkgJsonPath)) {
729
+ try {
730
+ const content = fs.readFileSync(pkgJsonPath, 'utf8').trim();
731
+ // Only remove if it's our minimal CommonJS marker (handle formatting variations)
732
+ const normalized = content.replace(/\s+/g, '');
733
+ if (normalized === '{"type":"commonjs"}') {
734
+ fs.unlinkSync(pkgJsonPath);
735
+ removedCount++;
736
+ console.log(` ${green}✓${reset} Removed PAN package.json`);
737
+ }
738
+ } catch (e) {
739
+ // Ignore read errors
740
+ }
741
+ }
742
+
743
+ // 6a. Clean up Copilot CLI config.json
744
+ if (isCopilot) {
745
+ const configPath = path.join(targetDir, 'config.json');
746
+ if (fs.existsSync(configPath)) {
747
+ try {
748
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
749
+ let configModified = false;
750
+
751
+ // Remove PAN statusline
752
+ if (config.statusLine && config.statusLine.command &&
753
+ config.statusLine.command.includes('pan-statusline')) {
754
+ delete config.statusLine;
755
+ configModified = true;
756
+ console.log(` ${green}✓${reset} Removed PAN statusline from config`);
757
+ }
758
+
759
+ // Remove PAN hooks from sessionStart
760
+ if (config.hooks && config.hooks.sessionStart) {
761
+ const before = config.hooks.sessionStart.length;
762
+ config.hooks.sessionStart = config.hooks.sessionStart.filter(h =>
763
+ !(h.command && h.command.includes('pan-check-update'))
764
+ );
765
+ if (config.hooks.sessionStart.length < before) {
766
+ configModified = true;
767
+ console.log(` ${green}✓${reset} Removed PAN hooks from config`);
768
+ }
769
+ if (config.hooks.sessionStart.length === 0) {
770
+ delete config.hooks.sessionStart;
771
+ }
772
+ }
773
+
774
+ // Remove PAN hooks from postToolUse
775
+ if (config.hooks && config.hooks.postToolUse) {
776
+ const before = config.hooks.postToolUse.length;
777
+ config.hooks.postToolUse = config.hooks.postToolUse.filter(h =>
778
+ !(h.command && h.command.includes('pan-context-monitor'))
779
+ );
780
+ if (config.hooks.postToolUse.length < before) {
781
+ configModified = true;
782
+ console.log(` ${green}✓${reset} Removed context monitor from config`);
783
+ }
784
+ if (config.hooks.postToolUse.length === 0) {
785
+ delete config.hooks.postToolUse;
786
+ }
787
+ }
788
+
789
+ if (config.hooks && Object.keys(config.hooks).length === 0) {
790
+ delete config.hooks;
791
+ }
792
+
793
+ if (configModified) {
794
+ if (Object.keys(config).length === 0) {
795
+ fs.unlinkSync(configPath);
796
+ console.log(` ${green}✓${reset} Removed empty config.json`);
797
+ } else {
798
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
799
+ }
800
+ removedCount++;
801
+ }
802
+ } catch (e) {
803
+ // Ignore JSON parse errors
804
+ }
805
+ }
806
+ }
807
+
808
+ // 6b. Clean up settings.json (remove PAN hooks and statusline)
809
+ const settingsPath = path.join(targetDir, 'settings.json');
810
+ if (fs.existsSync(settingsPath)) {
811
+ let settings = readSettings(settingsPath);
812
+ let settingsModified = false;
813
+
814
+ // Remove PAN statusline if it references our hook
815
+ if (settings.statusLine && settings.statusLine.command &&
816
+ settings.statusLine.command.includes('pan-statusline')) {
817
+ delete settings.statusLine;
818
+ settingsModified = true;
819
+ console.log(` ${green}✓${reset} Removed PAN statusline from settings`);
820
+ }
821
+
822
+ // Remove PAN hooks from SessionStart
823
+ if (settings.hooks && settings.hooks.SessionStart) {
824
+ const before = settings.hooks.SessionStart.length;
825
+ settings.hooks.SessionStart = settings.hooks.SessionStart.filter(entry => {
826
+ if (entry.hooks && Array.isArray(entry.hooks)) {
827
+ // Filter out PAN hooks
828
+ const hasPanHook = entry.hooks.some(h =>
829
+ h.command && (h.command.includes('pan-check-update') || h.command.includes('pan-statusline'))
830
+ );
831
+ return !hasPanHook;
832
+ }
833
+ return true;
834
+ });
835
+ if (settings.hooks.SessionStart.length < before) {
836
+ settingsModified = true;
837
+ console.log(` ${green}✓${reset} Removed PAN hooks from settings`);
838
+ }
839
+ // Clean up empty array
840
+ if (settings.hooks.SessionStart.length === 0) {
841
+ delete settings.hooks.SessionStart;
842
+ }
843
+ }
844
+
845
+ // Remove PAN hooks from PostToolUse
846
+ if (settings.hooks && settings.hooks.PostToolUse) {
847
+ const before = settings.hooks.PostToolUse.length;
848
+ settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(entry => {
849
+ if (entry.hooks && Array.isArray(entry.hooks)) {
850
+ const hasPanHook = entry.hooks.some(h =>
851
+ h.command && h.command.includes('pan-context-monitor')
852
+ );
853
+ return !hasPanHook;
854
+ }
855
+ return true;
856
+ });
857
+ if (settings.hooks.PostToolUse.length < before) {
858
+ settingsModified = true;
859
+ console.log(` ${green}✓${reset} Removed context monitor hook from settings`);
860
+ }
861
+ if (settings.hooks.PostToolUse.length === 0) {
862
+ delete settings.hooks.PostToolUse;
863
+ }
864
+ }
865
+
866
+ // Clean up empty hooks object
867
+ if (settings.hooks && Object.keys(settings.hooks).length === 0) {
868
+ delete settings.hooks;
869
+ }
870
+
871
+ // Remove Gemini experimental.enableAgents if PAN set it
872
+ if (settings.experimental && settings.experimental.enableAgents === true) {
873
+ delete settings.experimental.enableAgents;
874
+ if (Object.keys(settings.experimental).length === 0) {
875
+ delete settings.experimental;
876
+ }
877
+ settingsModified = true;
878
+ console.log(` ${green}✓${reset} Removed experimental agents flag from settings`);
879
+ }
880
+
881
+ if (settingsModified) {
882
+ // If settings is now empty (only PAN entries existed), remove the file
883
+ if (Object.keys(settings).length === 0) {
884
+ fs.unlinkSync(settingsPath);
885
+ console.log(` ${green}✓${reset} Removed empty settings.json`);
886
+ } else {
887
+ writeSettings(settingsPath, settings);
888
+ }
889
+ removedCount++;
890
+ }
891
+ }
892
+
893
+ // 6. For OpenCode, clean up permissions from opencode.json
894
+ if (isOpencode) {
895
+ // For local uninstalls, clean up ./.opencode/opencode.json
896
+ // For global uninstalls, clean up ~/.config/opencode/opencode.json
897
+ const opencodeConfigDir = isGlobal
898
+ ? getOpencodeGlobalDir()
899
+ : path.join(process.cwd(), '.opencode');
900
+ const configPath = path.join(opencodeConfigDir, 'opencode.json');
901
+ if (fs.existsSync(configPath)) {
902
+ try {
903
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
904
+ let modified = false;
905
+
906
+ // Remove PAN permission entries
907
+ if (config.permission) {
908
+ for (const permType of ['read', 'external_directory']) {
909
+ if (config.permission[permType]) {
910
+ const keys = Object.keys(config.permission[permType]);
911
+ for (const key of keys) {
912
+ if (key.includes('pan-wizard-core')) {
913
+ delete config.permission[permType][key];
914
+ modified = true;
915
+ }
916
+ }
917
+ // Clean up empty objects
918
+ if (Object.keys(config.permission[permType]).length === 0) {
919
+ delete config.permission[permType];
920
+ }
921
+ }
922
+ }
923
+ if (Object.keys(config.permission).length === 0) {
924
+ delete config.permission;
925
+ }
926
+ }
927
+
928
+ if (modified) {
929
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
930
+ removedCount++;
931
+ console.log(` ${green}✓${reset} Removed PAN permissions from opencode.json`);
932
+ }
933
+ } catch (e) {
934
+ // Ignore JSON parse errors
935
+ }
936
+ }
937
+ }
938
+
939
+ // 7. Remove pan-file-manifest.json
940
+ const manifestPath = path.join(targetDir, MANIFEST_NAME);
941
+ if (fs.existsSync(manifestPath)) {
942
+ try { fs.unlinkSync(manifestPath); } catch {}
943
+ removedCount++;
944
+ console.log(` ${green}✓${reset} Removed ${MANIFEST_NAME}`);
945
+ }
946
+
947
+ // 8. Clean up empty PAN directories
948
+ const dirsToClean = [
949
+ path.join(targetDir, 'agents'),
950
+ path.join(targetDir, 'hooks'),
951
+ path.join(targetDir, 'skills'),
952
+ path.join(targetDir, 'commands', 'pan'),
953
+ path.join(targetDir, 'commands'),
954
+ ];
955
+ for (const dir of dirsToClean) {
956
+ try {
957
+ if (fs.existsSync(dir)) {
958
+ const remaining = fs.readdirSync(dir);
959
+ if (remaining.length === 0) {
960
+ fs.rmdirSync(dir);
961
+ }
962
+ }
963
+ } catch {}
964
+ }
965
+
966
+ if (removedCount === 0) {
967
+ console.log(` ${yellow}⚠${reset} No PAN files found to remove.`);
968
+ }
969
+
970
+ console.log(`
971
+ ${green}Done!${reset} PAN has been uninstalled from ${runtimeLabel}.
972
+ Your other files and settings have been preserved.
973
+ `);
974
+ }
975
+
976
+
977
+ /**
978
+ * Configure OpenCode permissions to allow reading PAN reference docs
979
+ * This prevents permission prompts when PAN accesses the pan-wizard-core directory
980
+ * @param {boolean} isGlobal - Whether this is a global or local install
981
+ */
982
+ function configureOpencodePermissions(isGlobal = true) {
983
+ // For local installs, use ./.opencode/opencode.json
984
+ // For global installs, use ~/.config/opencode/opencode.json
985
+ const opencodeConfigDir = isGlobal
986
+ ? getOpencodeGlobalDir()
987
+ : path.join(process.cwd(), '.opencode');
988
+ const configPath = path.join(opencodeConfigDir, 'opencode.json');
989
+
990
+ // Ensure config directory exists
991
+ fs.mkdirSync(opencodeConfigDir, { recursive: true });
992
+
993
+ // Read existing config or create empty object
994
+ let config = {};
995
+ if (fs.existsSync(configPath)) {
996
+ try {
997
+ const content = fs.readFileSync(configPath, 'utf8');
998
+ config = parseJsonc(content);
999
+ } catch (e) {
1000
+ // Cannot parse - DO NOT overwrite user's config
1001
+ console.log(` ${yellow}⚠${reset} Could not parse opencode.json - skipping permission config`);
1002
+ console.log(` ${dim}Reason: ${e.message}${reset}`);
1003
+ console.log(` ${dim}Your config was NOT modified. Fix the syntax manually if needed.${reset}`);
1004
+ return;
1005
+ }
1006
+ }
1007
+
1008
+ // Ensure permission structure exists
1009
+ if (!config.permission) {
1010
+ config.permission = {};
1011
+ }
1012
+
1013
+ // Build the PAN path using the actual config directory
1014
+ // Use ~ shorthand if it's in the default location, otherwise use full path
1015
+ const defaultConfigDir = path.join(os.homedir(), '.config', 'opencode');
1016
+ const panPath = opencodeConfigDir === defaultConfigDir
1017
+ ? '~/.config/opencode/pan-wizard/*'
1018
+ : `${opencodeConfigDir.replace(/\\/g, '/')}/pan-wizard-core/*`;
1019
+
1020
+ let modified = false;
1021
+
1022
+ // Configure read permission
1023
+ if (!config.permission.read || typeof config.permission.read !== 'object') {
1024
+ config.permission.read = {};
1025
+ }
1026
+ if (config.permission.read[panPath] !== 'allow') {
1027
+ config.permission.read[panPath] = 'allow';
1028
+ modified = true;
1029
+ }
1030
+
1031
+ // Configure external_directory permission (the safety guard for paths outside project)
1032
+ if (!config.permission.external_directory || typeof config.permission.external_directory !== 'object') {
1033
+ config.permission.external_directory = {};
1034
+ }
1035
+ if (config.permission.external_directory[panPath] !== 'allow') {
1036
+ config.permission.external_directory[panPath] = 'allow';
1037
+ modified = true;
1038
+ }
1039
+
1040
+ if (!modified) {
1041
+ return; // Already configured
1042
+ }
1043
+
1044
+ // Write config back
1045
+ try {
1046
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
1047
+ console.log(` ${green}✓${reset} Configured read permission for PAN docs`);
1048
+ } catch (e) {
1049
+ console.error(` ${yellow}⚠${reset} Failed to write config: ${e.message}`);
1050
+ }
1051
+ }
1052
+
1053
+ /**
1054
+ * Verify a directory exists and contains files
1055
+ */
1056
+ function verifyInstalled(dirPath, description) {
1057
+ if (!fs.existsSync(dirPath)) {
1058
+ console.error(` ${yellow}✗${reset} Failed to install ${description}: directory not created`);
1059
+ return false;
1060
+ }
1061
+ try {
1062
+ const entries = fs.readdirSync(dirPath);
1063
+ if (entries.length === 0) {
1064
+ console.error(` ${yellow}✗${reset} Failed to install ${description}: directory is empty`);
1065
+ return false;
1066
+ }
1067
+ } catch (e) {
1068
+ console.error(` ${yellow}✗${reset} Failed to install ${description}: ${e.message}`);
1069
+ return false;
1070
+ }
1071
+ return true;
1072
+ }
1073
+
1074
+ /**
1075
+ * Verify a file exists
1076
+ */
1077
+ function verifyFileInstalled(filePath, description) {
1078
+ if (!fs.existsSync(filePath)) {
1079
+ console.error(` ${yellow}✗${reset} Failed to install ${description}: file not created`);
1080
+ return false;
1081
+ }
1082
+ return true;
1083
+ }
1084
+
1085
+ /**
1086
+ * Install to the specified directory for a specific runtime
1087
+ * @param {boolean} isGlobal - Whether to install globally or locally
1088
+ * @param {string} runtime - Target runtime ('claude', 'opencode', 'gemini', 'codex')
1089
+ */
1090
+
1091
+ // ──────────────────────────────────────────────────────
1092
+ // Local Patch Persistence
1093
+ // ──────────────────────────────────────────────────────
1094
+
1095
+ const PATCHES_DIR_NAME = 'pan-local-patches';
1096
+ const MANIFEST_NAME = 'pan-file-manifest.json';
1097
+
1098
+ /**
1099
+ * Compute SHA256 hash of file contents
1100
+ */
1101
+ function fileHash(filePath) {
1102
+ try {
1103
+ const content = fs.readFileSync(filePath);
1104
+ return crypto.createHash('sha256').update(content).digest('hex');
1105
+ } catch (e) {
1106
+ return null;
1107
+ }
1108
+ }
1109
+
1110
+ /**
1111
+ * Recursively collect all files in dir with their hashes
1112
+ */
1113
+ function generateManifest(dir, baseDir) {
1114
+ if (!baseDir) baseDir = dir;
1115
+ const manifest = {};
1116
+ if (!fs.existsSync(dir)) return manifest;
1117
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
1118
+ for (const entry of entries) {
1119
+ const fullPath = path.join(dir, entry.name);
1120
+ const relPath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
1121
+ if (entry.isDirectory()) {
1122
+ Object.assign(manifest, generateManifest(fullPath, baseDir));
1123
+ } else {
1124
+ manifest[relPath] = fileHash(fullPath);
1125
+ }
1126
+ }
1127
+ return manifest;
1128
+ }
1129
+
1130
+ /**
1131
+ * Write file manifest after installation for future modification detection
1132
+ */
1133
+ function writeManifest(configDir, runtime = 'claude') {
1134
+ const isOpencode = runtime === 'opencode';
1135
+ const isCodex = runtime === 'codex';
1136
+ const isCopilot = runtime === 'copilot';
1137
+ const panDir = path.join(configDir, 'pan-wizard-core');
1138
+ const commandsDir = path.join(configDir, 'commands', 'pan');
1139
+ const opencodeCommandDir = path.join(configDir, 'command');
1140
+ const codexSkillsDir = path.join(configDir, 'skills');
1141
+ const agentsDir = path.join(configDir, 'agents');
1142
+ const manifest = { version: pkg.version, timestamp: new Date().toISOString(), files: {} };
1143
+
1144
+ const panHashes = generateManifest(panDir);
1145
+ for (const [rel, hash] of Object.entries(panHashes)) {
1146
+ manifest.files['pan-wizard-core/' + rel] = hash;
1147
+ }
1148
+ if (!isOpencode && !isCodex && fs.existsSync(commandsDir)) {
1149
+ const cmdHashes = generateManifest(commandsDir);
1150
+ for (const [rel, hash] of Object.entries(cmdHashes)) {
1151
+ manifest.files['commands/pan/' + rel] = hash;
1152
+ }
1153
+ }
1154
+ if (isOpencode && fs.existsSync(opencodeCommandDir)) {
1155
+ for (const file of fs.readdirSync(opencodeCommandDir)) {
1156
+ if (file.startsWith('pan-') && file.endsWith('.md')) {
1157
+ manifest.files['command/' + file] = fileHash(path.join(opencodeCommandDir, file));
1158
+ }
1159
+ }
1160
+ }
1161
+ if ((isCodex || isCopilot) && fs.existsSync(codexSkillsDir)) {
1162
+ for (const skillName of listCodexSkillNames(codexSkillsDir)) {
1163
+ const skillRoot = path.join(codexSkillsDir, skillName);
1164
+ const skillHashes = generateManifest(skillRoot);
1165
+ for (const [rel, hash] of Object.entries(skillHashes)) {
1166
+ manifest.files[`skills/${skillName}/${rel}`] = hash;
1167
+ }
1168
+ }
1169
+ }
1170
+ if (fs.existsSync(agentsDir)) {
1171
+ for (const file of fs.readdirSync(agentsDir)) {
1172
+ if (file.startsWith('pan-') && file.endsWith('.md')) {
1173
+ manifest.files['agents/' + file] = fileHash(path.join(agentsDir, file));
1174
+ }
1175
+ }
1176
+ }
1177
+ // Track hook scripts in manifest for modification detection during upgrades
1178
+ const hooksDir = path.join(configDir, 'hooks');
1179
+ if (fs.existsSync(hooksDir)) {
1180
+ for (const file of fs.readdirSync(hooksDir)) {
1181
+ if (file.startsWith('pan-') && file.endsWith('.js')) {
1182
+ manifest.files['hooks/' + file] = fileHash(path.join(hooksDir, file));
1183
+ }
1184
+ }
1185
+ }
1186
+
1187
+ try {
1188
+ fs.writeFileSync(path.join(configDir, MANIFEST_NAME), JSON.stringify(manifest, null, 2));
1189
+ } catch (e) {
1190
+ console.error(` ${yellow}⚠${reset} Failed to write manifest: ${e.message}`);
1191
+ }
1192
+ return manifest;
1193
+ }
1194
+
1195
+ /**
1196
+ * Detect user-modified PAN files by comparing against install manifest.
1197
+ * Backs up modified files to pan-local-patches/ for reapply after update.
1198
+ */
1199
+ function saveLocalPatches(configDir) {
1200
+ const manifestPath = path.join(configDir, MANIFEST_NAME);
1201
+ if (!fs.existsSync(manifestPath)) return [];
1202
+
1203
+ let manifest;
1204
+ try { manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); } catch { return []; }
1205
+
1206
+ const patchesDir = path.join(configDir, PATCHES_DIR_NAME);
1207
+ const modified = [];
1208
+
1209
+ for (const [relPath, originalHash] of Object.entries(manifest.files || {})) {
1210
+ const fullPath = path.join(configDir, relPath);
1211
+ if (!fs.existsSync(fullPath)) continue;
1212
+ const currentHash = fileHash(fullPath);
1213
+ if (currentHash !== originalHash) {
1214
+ const backupPath = path.join(patchesDir, relPath);
1215
+ fs.mkdirSync(path.dirname(backupPath), { recursive: true });
1216
+ fs.copyFileSync(fullPath, backupPath);
1217
+ modified.push(relPath);
1218
+ }
1219
+ }
1220
+
1221
+ if (modified.length > 0) {
1222
+ const meta = {
1223
+ backed_up_at: new Date().toISOString(),
1224
+ from_version: manifest.version,
1225
+ files: modified
1226
+ };
1227
+ fs.writeFileSync(path.join(patchesDir, 'backup-meta.json'), JSON.stringify(meta, null, 2));
1228
+ console.log(' ' + yellow + 'i' + reset + ' Found ' + modified.length + ' locally modified PAN file(s) — backed up to ' + PATCHES_DIR_NAME + '/');
1229
+ for (const f of modified) {
1230
+ console.log(' ' + dim + f + reset);
1231
+ }
1232
+ }
1233
+ return modified;
1234
+ }
1235
+
1236
+ /**
1237
+ * After install, report backed-up patches for user to reapply.
1238
+ */
1239
+ function reportLocalPatches(configDir, runtime = 'claude') {
1240
+ const patchesDir = path.join(configDir, PATCHES_DIR_NAME);
1241
+ const metaPath = path.join(patchesDir, 'backup-meta.json');
1242
+ if (!fs.existsSync(metaPath)) return [];
1243
+
1244
+ let meta;
1245
+ try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf8')); } catch { return []; }
1246
+
1247
+ if (meta.files && meta.files.length > 0) {
1248
+ const reapplyCommand = runtime === 'opencode'
1249
+ ? '/pan-patches'
1250
+ : runtime === 'codex'
1251
+ ? '$pan-patches'
1252
+ : '/pan:patches';
1253
+ console.log('');
1254
+ console.log(' ' + yellow + 'Local patches detected' + reset + ' (from v' + meta.from_version + '):');
1255
+ for (const f of meta.files) {
1256
+ console.log(' ' + cyan + f + reset);
1257
+ }
1258
+ console.log('');
1259
+ console.log(' Your modifications are saved in ' + cyan + PATCHES_DIR_NAME + '/' + reset);
1260
+ console.log(' Run ' + cyan + reapplyCommand + reset + ' to merge them into the new version.');
1261
+ console.log(' Or manually compare and merge the files.');
1262
+ console.log('');
1263
+ }
1264
+ return meta.files || [];
1265
+ }
1266
+
1267
+ function install(isGlobal, runtime = 'claude') {
1268
+ const isOpencode = runtime === 'opencode';
1269
+ const isGemini = runtime === 'gemini';
1270
+ const isCodex = runtime === 'codex';
1271
+ const isCopilot = runtime === 'copilot';
1272
+ const dirName = getDirName(runtime);
1273
+ const src = path.join(__dirname, '..');
1274
+
1275
+ // Get the target directory based on runtime and install type
1276
+ const targetDir = isGlobal
1277
+ ? getGlobalDir(runtime, explicitConfigDir)
1278
+ : path.join(process.cwd(), dirName);
1279
+
1280
+ const locationLabel = isGlobal
1281
+ ? targetDir.replace(os.homedir(), '~')
1282
+ : targetDir.replace(process.cwd(), '.');
1283
+
1284
+ // Path prefix for file references in markdown content
1285
+ // For global installs: use full path
1286
+ // For local installs: use relative
1287
+ const pathPrefix = isGlobal
1288
+ ? `${targetDir.replace(/\\/g, '/')}/`
1289
+ : `./${dirName}/`;
1290
+
1291
+ let runtimeLabel = 'Claude Code';
1292
+ if (isOpencode) runtimeLabel = 'OpenCode';
1293
+ if (isGemini) runtimeLabel = 'Gemini';
1294
+ if (isCodex) runtimeLabel = 'Codex';
1295
+ if (isCopilot) runtimeLabel = 'GitHub Copilot CLI';
1296
+
1297
+ // Guard: never install into the PAN source repository itself
1298
+ if (path.resolve(process.cwd()) === PAN_SOURCE_ROOT) {
1299
+ console.error(`\n ${red}✗${reset} Refusing to install PAN into its own source repository.`);
1300
+ console.error(` Run the installer from your target project directory instead.\n`);
1301
+ console.error(` Example: cd /path/to/my-project && node ${path.resolve(__dirname, 'install.js')} --claude --local\n`);
1302
+ process.exit(1);
1303
+ }
1304
+
1305
+ console.log(` Installing for ${cyan}${runtimeLabel}${reset} to ${cyan}${locationLabel}${reset}\n`);
1306
+
1307
+ // Early writability check — fail fast before any file operations
1308
+ try {
1309
+ fs.mkdirSync(targetDir, { recursive: true });
1310
+ const probe = path.join(targetDir, '.pan-write-test');
1311
+ fs.writeFileSync(probe, '');
1312
+ fs.unlinkSync(probe);
1313
+ } catch (e) {
1314
+ console.error(` ${red}✗${reset} Cannot write to ${locationLabel}: ${e.message}`);
1315
+ console.error(` Check directory permissions and try again.`);
1316
+ process.exit(1);
1317
+ }
1318
+
1319
+ // Track installation failures
1320
+ const failures = [];
1321
+
1322
+ // Save any locally modified PAN files before they get wiped
1323
+ saveLocalPatches(targetDir);
1324
+
1325
+ // Clean up orphaned files from previous versions
1326
+ cleanupOrphanedFiles(targetDir);
1327
+
1328
+ // OpenCode uses command/ (flat), Codex uses skills/, Claude/Gemini use commands/pan/
1329
+ try {
1330
+ if (isOpencode) {
1331
+ // OpenCode: flat structure in command/ directory
1332
+ const commandDir = path.join(targetDir, 'command');
1333
+ fs.mkdirSync(commandDir, { recursive: true });
1334
+
1335
+ // Copy commands/pan/*.md as command/pan-*.md (flatten structure)
1336
+ const panSrc = path.join(src, 'commands', 'pan');
1337
+ copyFlattenedCommands(panSrc, commandDir, 'pan', pathPrefix, runtime);
1338
+ if (verifyInstalled(commandDir, 'command/pan-*')) {
1339
+ const count = fs.readdirSync(commandDir).filter(f => f.startsWith('pan-')).length;
1340
+ console.log(` ${green}✓${reset} Installed ${count} commands to command/`);
1341
+ } else {
1342
+ failures.push('command/pan-*');
1343
+ }
1344
+ } else if (isCodex) {
1345
+ const skillsDir = path.join(targetDir, 'skills');
1346
+ const panSrc = path.join(src, 'commands', 'pan');
1347
+ copyCommandsAsCodexSkills(panSrc, skillsDir, 'pan', pathPrefix, runtime);
1348
+ const installedSkillNames = listCodexSkillNames(skillsDir);
1349
+ if (installedSkillNames.length > 0) {
1350
+ console.log(` ${green}✓${reset} Installed ${installedSkillNames.length} skills to skills/`);
1351
+ } else {
1352
+ failures.push('skills/pan-*');
1353
+ }
1354
+ } else if (isCopilot) {
1355
+ const skillsDir = path.join(targetDir, 'skills');
1356
+ const panSrc = path.join(src, 'commands', 'pan');
1357
+ copyCommandsAsCopilotSkills(panSrc, skillsDir, 'pan', pathPrefix, runtime);
1358
+ const installedSkills = fs.readdirSync(skillsDir, { withFileTypes: true })
1359
+ .filter(e => e.isDirectory() && e.name.startsWith('pan-'));
1360
+ if (installedSkills.length > 0) {
1361
+ console.log(` ${green}✓${reset} Installed ${installedSkills.length} skills to skills/`);
1362
+ } else {
1363
+ failures.push('skills/pan-*');
1364
+ }
1365
+ } else {
1366
+ // Claude Code & Gemini: nested structure in commands/ directory
1367
+ const commandsDir = path.join(targetDir, 'commands');
1368
+ fs.mkdirSync(commandsDir, { recursive: true });
1369
+
1370
+ const panSrc = path.join(src, 'commands', 'pan');
1371
+ const panDest = path.join(commandsDir, 'pan');
1372
+ copyWithPathReplacement(panSrc, panDest, pathPrefix, runtime, true);
1373
+ if (verifyInstalled(panDest, 'commands/pan')) {
1374
+ console.log(` ${green}✓${reset} Installed commands/pan`);
1375
+ } else {
1376
+ failures.push('commands/pan');
1377
+ }
1378
+ }
1379
+ } catch (e) {
1380
+ console.error(` ${yellow}✗${reset} Commands install failed: ${e.message}`);
1381
+ failures.push('commands');
1382
+ }
1383
+
1384
+ // Copy pan-wizard-core skill with path replacement
1385
+ try {
1386
+ const skillSrc = path.join(src, 'pan-wizard-core');
1387
+ const skillDest = path.join(targetDir, 'pan-wizard-core');
1388
+ copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime);
1389
+ if (verifyInstalled(skillDest, 'pan-wizard-core')) {
1390
+ console.log(` ${green}✓${reset} Installed pan-wizard-core`);
1391
+ } else {
1392
+ failures.push('pan-wizard-core');
1393
+ }
1394
+ } catch (e) {
1395
+ console.error(` ${yellow}✗${reset} pan-wizard-core install failed: ${e.message}`);
1396
+ failures.push('pan-wizard-core');
1397
+ }
1398
+
1399
+ // Copy agents to agents directory
1400
+ try {
1401
+ const agentsSrc = path.join(src, 'agents');
1402
+ if (fs.existsSync(agentsSrc)) {
1403
+ const agentsDest = path.join(targetDir, 'agents');
1404
+ fs.mkdirSync(agentsDest, { recursive: true });
1405
+
1406
+ // Remove old PAN agents (pan-*.md and pan-*.agent.md) before copying new ones
1407
+ if (fs.existsSync(agentsDest)) {
1408
+ for (const file of fs.readdirSync(agentsDest)) {
1409
+ if (file.startsWith('pan-') && file.endsWith('.md')) {
1410
+ fs.unlinkSync(path.join(agentsDest, file));
1411
+ }
1412
+ }
1413
+ }
1414
+
1415
+ // Copy new agents
1416
+ const agentEntries = fs.readdirSync(agentsSrc, { withFileTypes: true });
1417
+ for (const entry of agentEntries) {
1418
+ if (entry.isFile() && entry.name.endsWith('.md')) {
1419
+ let content = fs.readFileSync(path.join(agentsSrc, entry.name), 'utf8');
1420
+ // Always replace ~/.claude/ as it is the source of truth in the repo
1421
+ const dirRegex = /~\/\.claude\//g;
1422
+ content = content.replace(dirRegex, pathPrefix);
1423
+ content = processAttribution(content, getCommitAttribution(runtime));
1424
+ // Convert frontmatter for runtime compatibility
1425
+ if (isOpencode) {
1426
+ content = convertClaudeToOpencodeFrontmatter(content);
1427
+ } else if (isGemini) {
1428
+ content = convertClaudeToGeminiAgent(content);
1429
+ } else if (isCodex) {
1430
+ content = convertClaudeToCodexMarkdown(content);
1431
+ } else if (isCopilot) {
1432
+ content = convertClaudeToCopilotAgent(content);
1433
+ }
1434
+ // Copilot CLI uses .agent.md extension; others use .md
1435
+ const destName = isCopilot
1436
+ ? entry.name.replace(/\.md$/, '.agent.md')
1437
+ : entry.name;
1438
+ fs.writeFileSync(path.join(agentsDest, destName), content);
1439
+ }
1440
+ }
1441
+ if (verifyInstalled(agentsDest, 'agents')) {
1442
+ console.log(` ${green}✓${reset} Installed agents`);
1443
+ } else {
1444
+ failures.push('agents');
1445
+ }
1446
+ }
1447
+ } catch (e) {
1448
+ console.error(` ${yellow}✗${reset} Agents install failed: ${e.message}`);
1449
+ failures.push('agents');
1450
+ }
1451
+
1452
+ // Copy CHANGELOG.md
1453
+ try {
1454
+ const changelogSrc = path.join(src, 'CHANGELOG.md');
1455
+ const changelogDest = path.join(targetDir, 'pan-wizard-core', 'CHANGELOG.md');
1456
+ if (fs.existsSync(changelogSrc)) {
1457
+ fs.copyFileSync(changelogSrc, changelogDest);
1458
+ if (verifyFileInstalled(changelogDest, 'CHANGELOG.md')) {
1459
+ console.log(` ${green}✓${reset} Installed CHANGELOG.md`);
1460
+ } else {
1461
+ failures.push('CHANGELOG.md');
1462
+ }
1463
+ }
1464
+ } catch (e) {
1465
+ console.error(` ${yellow}✗${reset} CHANGELOG install failed: ${e.message}`);
1466
+ failures.push('CHANGELOG.md');
1467
+ }
1468
+
1469
+ // Write VERSION file (with upgrade/same/downgrade detection)
1470
+ try {
1471
+ const versionDest = path.join(targetDir, 'pan-wizard-core', 'VERSION');
1472
+ let versionMsg = `${pkg.version}`;
1473
+ try {
1474
+ const prev = fs.readFileSync(versionDest, 'utf8').trim();
1475
+ if (prev && prev !== pkg.version) {
1476
+ versionMsg = `${prev} → ${pkg.version}`;
1477
+ } else if (prev === pkg.version) {
1478
+ versionMsg = `${pkg.version} (reinstall)`;
1479
+ }
1480
+ } catch (_) { /* first install */ }
1481
+ fs.writeFileSync(versionDest, pkg.version + '\n');
1482
+ if (verifyFileInstalled(versionDest, 'VERSION')) {
1483
+ console.log(` ${green}✓${reset} Wrote VERSION (${versionMsg})`);
1484
+ } else {
1485
+ failures.push('VERSION');
1486
+ }
1487
+ } catch (e) {
1488
+ console.error(` ${yellow}✗${reset} VERSION write failed: ${e.message}`);
1489
+ failures.push('VERSION');
1490
+ }
1491
+
1492
+ if (!isCodex) {
1493
+ // Write package.json to force CommonJS mode for PAN scripts
1494
+ // Prevents "require is not defined" errors when project has "type": "module"
1495
+ // Node.js walks up looking for package.json - this stops inheritance from project
1496
+ try {
1497
+ const pkgJsonDest = path.join(targetDir, 'package.json');
1498
+ fs.writeFileSync(pkgJsonDest, '{"type":"commonjs"}\n');
1499
+ console.log(` ${green}✓${reset} Wrote package.json (CommonJS mode)`);
1500
+ } catch (e) {
1501
+ console.error(` ${yellow}✗${reset} package.json write failed: ${e.message}`);
1502
+ failures.push('package.json');
1503
+ }
1504
+
1505
+ // Copy hooks from dist/ (bundled with dependencies)
1506
+ // Template paths for the target runtime (replaces '.claude' with correct config dir)
1507
+ try {
1508
+ const hooksSrc = path.join(src, 'hooks', 'dist');
1509
+ if (fs.existsSync(hooksSrc)) {
1510
+ const hooksDest = path.join(targetDir, 'hooks');
1511
+ fs.mkdirSync(hooksDest, { recursive: true });
1512
+ const hookEntries = fs.readdirSync(hooksSrc);
1513
+ const configDirReplacement = getConfigDirFromHome(runtime, isGlobal);
1514
+ for (const entry of hookEntries) {
1515
+ const srcFile = path.join(hooksSrc, entry);
1516
+ if (fs.statSync(srcFile).isFile()) {
1517
+ const destFile = path.join(hooksDest, entry);
1518
+ // Template .js files to replace '.claude' with runtime-specific config dir
1519
+ if (entry.endsWith('.js')) {
1520
+ let content = fs.readFileSync(srcFile, 'utf8');
1521
+ content = content.replace(/'\.claude'/g, configDirReplacement);
1522
+ fs.writeFileSync(destFile, content);
1523
+ } else {
1524
+ fs.copyFileSync(srcFile, destFile);
1525
+ }
1526
+ }
1527
+ }
1528
+ if (verifyInstalled(hooksDest, 'hooks')) {
1529
+ console.log(` ${green}✓${reset} Installed hooks (bundled)`);
1530
+ } else {
1531
+ failures.push('hooks');
1532
+ }
1533
+ }
1534
+ } catch (e) {
1535
+ console.error(` ${yellow}✗${reset} Hooks install failed: ${e.message}`);
1536
+ failures.push('hooks');
1537
+ }
1538
+ }
1539
+
1540
+ if (failures.length > 0) {
1541
+ console.error(`\n ${yellow}Installation incomplete!${reset} Failed: ${failures.join(', ')}`);
1542
+ process.exit(1);
1543
+ }
1544
+
1545
+ // Write file manifest for future modification detection
1546
+ writeManifest(targetDir, runtime);
1547
+ console.log(` ${green}✓${reset} Wrote file manifest (${MANIFEST_NAME})`);
1548
+
1549
+ // Report any backed-up local patches
1550
+ reportLocalPatches(targetDir, runtime);
1551
+
1552
+ if (isCodex) {
1553
+ return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
1554
+ }
1555
+
1556
+ const updateCheckCommand = isGlobal
1557
+ ? buildHookCommand(targetDir, 'pan-check-update.js')
1558
+ : 'node ' + dirName + '/hooks/pan-check-update.js';
1559
+ const contextMonitorCommand = isGlobal
1560
+ ? buildHookCommand(targetDir, 'pan-context-monitor.js')
1561
+ : 'node ' + dirName + '/hooks/pan-context-monitor.js';
1562
+ const statuslineCommand = isGlobal
1563
+ ? buildHookCommand(targetDir, 'pan-statusline.js')
1564
+ : 'node ' + dirName + '/hooks/pan-statusline.js';
1565
+
1566
+ // Copilot CLI uses config.json with its own hook format
1567
+ if (isCopilot) {
1568
+ const configPath = path.join(targetDir, 'config.json');
1569
+ const config = readSettings(configPath);
1570
+
1571
+ if (!config.hooks) {
1572
+ config.hooks = {};
1573
+ }
1574
+
1575
+ // Register sessionStart hook for update checking
1576
+ if (!config.hooks.sessionStart) {
1577
+ config.hooks.sessionStart = [];
1578
+ }
1579
+ const hasPanUpdateHook = config.hooks.sessionStart.some(h =>
1580
+ h.command && h.command.includes('pan-check-update')
1581
+ );
1582
+ if (!hasPanUpdateHook) {
1583
+ config.hooks.sessionStart.push({
1584
+ command: updateCheckCommand
1585
+ });
1586
+ console.log(` ${green}✓${reset} Configured update check hook`);
1587
+ }
1588
+
1589
+ // Register postToolUse hook for context monitoring
1590
+ if (!config.hooks.postToolUse) {
1591
+ config.hooks.postToolUse = [];
1592
+ }
1593
+ const hasContextHook = config.hooks.postToolUse.some(h =>
1594
+ h.command && h.command.includes('pan-context-monitor')
1595
+ );
1596
+ if (!hasContextHook) {
1597
+ config.hooks.postToolUse.push({
1598
+ command: contextMonitorCommand
1599
+ });
1600
+ console.log(` ${green}✓${reset} Configured context window monitor hook`);
1601
+ }
1602
+
1603
+ writeSettings(configPath, config);
1604
+ return { settingsPath: configPath, settings: config, statuslineCommand, runtime };
1605
+ }
1606
+
1607
+ // Configure statusline and hooks in settings.json
1608
+ // Claude Code, Gemini, OpenCode use settings.json
1609
+ const settingsPath = path.join(targetDir, 'settings.json');
1610
+ const settings = cleanupOrphanedHooks(readSettings(settingsPath));
1611
+
1612
+ // Enable experimental agents for Gemini CLI (required for custom sub-agents)
1613
+ if (isGemini) {
1614
+ if (!settings.experimental) {
1615
+ settings.experimental = {};
1616
+ }
1617
+ if (!settings.experimental.enableAgents) {
1618
+ settings.experimental.enableAgents = true;
1619
+ console.log(` ${green}✓${reset} Enabled experimental agents`);
1620
+ }
1621
+ }
1622
+
1623
+ // Configure SessionStart hook for update checking (skip for opencode)
1624
+ if (!isOpencode) {
1625
+ if (!settings.hooks) {
1626
+ settings.hooks = {};
1627
+ }
1628
+ if (!settings.hooks.SessionStart) {
1629
+ settings.hooks.SessionStart = [];
1630
+ }
1631
+
1632
+ const hasPanUpdateHook = settings.hooks.SessionStart.some(entry =>
1633
+ entry.hooks && entry.hooks.some(h => h.command && h.command.includes('pan-check-update'))
1634
+ );
1635
+
1636
+ if (!hasPanUpdateHook) {
1637
+ settings.hooks.SessionStart.push({
1638
+ hooks: [
1639
+ {
1640
+ type: 'command',
1641
+ command: updateCheckCommand
1642
+ }
1643
+ ]
1644
+ });
1645
+ console.log(` ${green}✓${reset} Configured update check hook`);
1646
+ }
1647
+
1648
+ // Configure PostToolUse hook for context window monitoring
1649
+ if (!settings.hooks.PostToolUse) {
1650
+ settings.hooks.PostToolUse = [];
1651
+ }
1652
+
1653
+ const hasContextMonitorHook = settings.hooks.PostToolUse.some(entry =>
1654
+ entry.hooks && entry.hooks.some(h => h.command && h.command.includes('pan-context-monitor'))
1655
+ );
1656
+
1657
+ if (!hasContextMonitorHook) {
1658
+ settings.hooks.PostToolUse.push({
1659
+ hooks: [
1660
+ {
1661
+ type: 'command',
1662
+ command: contextMonitorCommand
1663
+ }
1664
+ ]
1665
+ });
1666
+ console.log(` ${green}✓${reset} Configured context window monitor hook`);
1667
+ }
1668
+ }
1669
+
1670
+ return { settingsPath, settings, statuslineCommand, runtime };
1671
+ }
1672
+
1673
+ /**
1674
+ * Apply statusline config, then print completion message
1675
+ */
1676
+ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = 'claude', isGlobal = true) {
1677
+ const isOpencode = runtime === 'opencode';
1678
+ const isCodex = runtime === 'codex';
1679
+ const isCopilot = runtime === 'copilot';
1680
+
1681
+ if (shouldInstallStatusline && !isOpencode && !isCodex) {
1682
+ if (isCopilot) {
1683
+ // Copilot CLI: statusline stored in config.json
1684
+ settings.statusLine = {
1685
+ command: statuslineCommand
1686
+ };
1687
+ } else {
1688
+ settings.statusLine = {
1689
+ type: 'command',
1690
+ command: statuslineCommand
1691
+ };
1692
+ }
1693
+ console.log(` ${green}✓${reset} Configured statusline`);
1694
+ }
1695
+
1696
+ // Write settings/config when runtime supports it
1697
+ if (!isCodex) {
1698
+ writeSettings(settingsPath, settings);
1699
+ }
1700
+
1701
+ // Configure OpenCode permissions
1702
+ if (isOpencode) {
1703
+ configureOpencodePermissions(isGlobal);
1704
+ }
1705
+
1706
+ let program = 'Claude Code';
1707
+ if (runtime === 'opencode') program = 'OpenCode';
1708
+ if (runtime === 'gemini') program = 'Gemini';
1709
+ if (runtime === 'codex') program = 'Codex';
1710
+ if (runtime === 'copilot') program = 'GitHub Copilot CLI';
1711
+
1712
+ let command = '/pan:new-project';
1713
+ if (runtime === 'opencode') command = '/pan-new-project';
1714
+ if (runtime === 'codex') command = '$pan-new-project';
1715
+ if (runtime === 'copilot') command = '/pan-new-project';
1716
+ console.log(`
1717
+ ${green}Done!${reset} Open a blank directory in ${program} and run ${cyan}${command}${reset}.
1718
+
1719
+ ${cyan}Join the community:${reset} https://discord.gg/pan-wizard
1720
+ `);
1721
+ }
1722
+
1723
+ /**
1724
+ * Handle statusline configuration with optional prompt
1725
+ */
1726
+ function handleStatusline(settings, isInteractive, callback) {
1727
+ const hasExisting = settings.statusLine != null;
1728
+
1729
+ if (!hasExisting) {
1730
+ callback(true);
1731
+ return;
1732
+ }
1733
+
1734
+ if (forceStatusline) {
1735
+ callback(true);
1736
+ return;
1737
+ }
1738
+
1739
+ if (!isInteractive) {
1740
+ console.log(` ${yellow}⚠${reset} Skipping statusline (already configured)`);
1741
+ console.log(` Use ${cyan}--force-statusline${reset} to replace\n`);
1742
+ callback(false);
1743
+ return;
1744
+ }
1745
+
1746
+ const existingCmd = settings.statusLine.command || settings.statusLine.url || '(custom)';
1747
+
1748
+ const rl = readline.createInterface({
1749
+ input: process.stdin,
1750
+ output: process.stdout
1751
+ });
1752
+
1753
+ console.log(`
1754
+ ${yellow}⚠${reset} Existing statusline detected\n
1755
+ Your current statusline:
1756
+ ${dim}command: ${existingCmd}${reset}
1757
+
1758
+ PAN includes a statusline showing:
1759
+ • Model name
1760
+ • Current task (from todo list)
1761
+ • Context window usage (color-coded)
1762
+
1763
+ ${cyan}1${reset}) Keep existing
1764
+ ${cyan}2${reset}) Replace with PAN statusline
1765
+ `);
1766
+
1767
+ rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
1768
+ rl.close();
1769
+ const choice = answer.trim() || '1';
1770
+ callback(choice === '2');
1771
+ });
1772
+ }
1773
+
1774
+ /**
1775
+ * Prompt for runtime selection
1776
+ */
1777
+ function promptRuntime(callback) {
1778
+ const rl = readline.createInterface({
1779
+ input: process.stdin,
1780
+ output: process.stdout
1781
+ });
1782
+
1783
+ let answered = false;
1784
+
1785
+ rl.on('close', () => {
1786
+ if (!answered) {
1787
+ answered = true;
1788
+ console.log(`\n ${yellow}Installation cancelled${reset}\n`);
1789
+ process.exit(0);
1790
+ }
1791
+ });
1792
+
1793
+ console.log(` ${yellow}Which runtime(s) would you like to install for?${reset}\n\n ${cyan}1${reset}) Claude Code ${dim}(~/.claude)${reset}
1794
+ ${cyan}2${reset}) OpenCode ${dim}(~/.config/opencode)${reset} - open source, free models
1795
+ ${cyan}3${reset}) Gemini ${dim}(~/.gemini)${reset}
1796
+ ${cyan}4${reset}) Codex ${dim}(~/.codex)${reset}
1797
+ ${cyan}5${reset}) Copilot CLI ${dim}(~/.copilot)${reset} - GitHub's terminal agent
1798
+ ${cyan}6${reset}) All
1799
+ `);
1800
+
1801
+ rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
1802
+ answered = true;
1803
+ rl.close();
1804
+ const choice = answer.trim() || '1';
1805
+ if (choice === '6') {
1806
+ callback(['claude', 'opencode', 'gemini', 'codex', 'copilot']);
1807
+ } else if (choice === '5') {
1808
+ callback(['copilot']);
1809
+ } else if (choice === '4') {
1810
+ callback(['codex']);
1811
+ } else if (choice === '3') {
1812
+ callback(['gemini']);
1813
+ } else if (choice === '2') {
1814
+ callback(['opencode']);
1815
+ } else {
1816
+ callback(['claude']);
1817
+ }
1818
+ });
1819
+ }
1820
+
1821
+ /**
1822
+ * Prompt for install location
1823
+ */
1824
+ function promptLocation(runtimes) {
1825
+ if (!process.stdin.isTTY) {
1826
+ console.log(` ${yellow}Non-interactive terminal detected, defaulting to local install${reset}\n`);
1827
+ installAllRuntimes(runtimes, false, false);
1828
+ return;
1829
+ }
1830
+
1831
+ const rl = readline.createInterface({
1832
+ input: process.stdin,
1833
+ output: process.stdout
1834
+ });
1835
+
1836
+ let answered = false;
1837
+
1838
+ rl.on('close', () => {
1839
+ if (!answered) {
1840
+ answered = true;
1841
+ console.log(`\n ${yellow}Installation cancelled${reset}\n`);
1842
+ process.exit(0);
1843
+ }
1844
+ });
1845
+
1846
+ const pathExamples = runtimes.map(r => {
1847
+ const globalPath = getGlobalDir(r, explicitConfigDir);
1848
+ return globalPath.replace(os.homedir(), '~');
1849
+ }).join(', ');
1850
+
1851
+ const localExamples = runtimes.map(r => `./${getDirName(r)}`).join(', ');
1852
+
1853
+ console.log(` ${yellow}Where would you like to install?${reset}\n\n ${cyan}1${reset}) Local ${dim}(${localExamples})${reset} - this project only
1854
+ ${cyan}2${reset}) Global ${dim}(${pathExamples})${reset} - available in all projects
1855
+ `);
1856
+
1857
+ rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
1858
+ answered = true;
1859
+ rl.close();
1860
+ const choice = answer.trim() || '1';
1861
+ const isGlobal = choice === '2';
1862
+ installAllRuntimes(runtimes, isGlobal, true);
1863
+ });
1864
+ }
1865
+
1866
+ /**
1867
+ * Install PAN for all selected runtimes
1868
+ */
1869
+ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
1870
+ const results = [];
1871
+
1872
+ for (const runtime of runtimes) {
1873
+ const result = install(isGlobal, runtime);
1874
+ results.push(result);
1875
+ }
1876
+
1877
+ const statuslineRuntimes = ['claude', 'gemini', 'copilot'];
1878
+ const primaryStatuslineResult = results.find(r => statuslineRuntimes.includes(r.runtime));
1879
+
1880
+ const finalize = (shouldInstallStatusline) => {
1881
+ for (const result of results) {
1882
+ const useStatusline = statuslineRuntimes.includes(result.runtime) && shouldInstallStatusline;
1883
+ finishInstall(
1884
+ result.settingsPath,
1885
+ result.settings,
1886
+ result.statuslineCommand,
1887
+ useStatusline,
1888
+ result.runtime,
1889
+ isGlobal
1890
+ );
1891
+ }
1892
+ };
1893
+
1894
+ if (primaryStatuslineResult) {
1895
+ handleStatusline(primaryStatuslineResult.settings, isInteractive, finalize);
1896
+ } else {
1897
+ finalize(false);
1898
+ }
1899
+ }
1900
+
1901
+ // Main logic
1902
+ if (hasGlobal && hasLocal) {
1903
+ console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
1904
+ process.exit(1);
1905
+ } else if (explicitConfigDir && hasLocal) {
1906
+ console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
1907
+ process.exit(1);
1908
+ } else if (hasUninstall) {
1909
+ if (!hasGlobal && !hasLocal) {
1910
+ console.error(` ${yellow}--uninstall requires --global or --local${reset}`);
1911
+ process.exit(1);
1912
+ }
1913
+ const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : ['claude'];
1914
+ for (const runtime of runtimes) {
1915
+ uninstall(hasGlobal, runtime);
1916
+ }
1917
+ } else if (selectedRuntimes.length > 0) {
1918
+ if (!hasGlobal && !hasLocal) {
1919
+ promptLocation(selectedRuntimes);
1920
+ } else {
1921
+ installAllRuntimes(selectedRuntimes, hasGlobal, false);
1922
+ }
1923
+ } else if (hasGlobal || hasLocal) {
1924
+ // Default to Claude if no runtime specified but location is
1925
+ installAllRuntimes(['claude'], hasGlobal, false);
1926
+ } else {
1927
+ // Interactive
1928
+ if (!process.stdin.isTTY) {
1929
+ console.log(` ${yellow}Non-interactive terminal detected, defaulting to Claude Code local install${reset}\n`);
1930
+ installAllRuntimes(['claude'], false, false);
1931
+ } else {
1932
+ promptRuntime((runtimes) => {
1933
+ promptLocation(runtimes);
1934
+ });
1935
+ }
1936
+ }