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