pan-wizard 3.7.10 → 3.10.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 +24 -2
- package/agents/pan-conductor.md +1 -2
- package/agents/pan-counterfactual.md +1 -2
- package/agents/pan-debugger.md +1 -2
- package/agents/pan-distiller.md +1 -2
- package/agents/pan-document_code.md +1 -0
- package/agents/pan-executor.md +1 -0
- package/agents/pan-experiment-runner.md +1 -2
- package/agents/pan-hardener.md +1 -2
- package/agents/pan-integration-checker.md +1 -2
- package/agents/pan-knowledge.md +1 -2
- package/agents/pan-meta-reviewer.md +1 -2
- package/agents/pan-optimizer.md +1 -0
- package/agents/pan-phase-researcher.md +1 -0
- package/agents/pan-plan-checker.md +1 -2
- package/agents/pan-planner.md +1 -0
- package/agents/pan-previewer.md +1 -2
- package/agents/pan-project-researcher.md +6 -0
- package/agents/pan-research-synthesizer.md +7 -0
- package/agents/pan-reviewer.md +2 -3
- package/agents/pan-roadmapper.md +1 -0
- package/agents/pan-verifier.md +1 -2
- package/bin/install-lib.cjs +661 -46
- package/bin/install.js +722 -116
- package/commands/pan/experiment.md +2 -0
- package/commands/pan/links.md +102 -0
- package/commands/pan/profile.md +2 -0
- package/hooks/dist/pan-cost-logger.js +22 -7
- package/package.json +5 -4
- package/pan-wizard-core/bin/lib/codebase.cjs +2 -0
- package/pan-wizard-core/bin/lib/commands-learnings.cjs +544 -0
- package/pan-wizard-core/bin/lib/commands.cjs +12 -523
- package/pan-wizard-core/bin/lib/core.cjs +69 -0
- package/pan-wizard-core/bin/lib/cost.cjs +62 -8
- package/pan-wizard-core/bin/lib/experiment.cjs +1 -0
- package/pan-wizard-core/bin/lib/git.cjs +6 -1
- package/pan-wizard-core/bin/lib/links.cjs +549 -0
- package/pan-wizard-core/bin/lib/lock.cjs +108 -0
- package/pan-wizard-core/bin/lib/milestone.cjs +3 -2
- package/pan-wizard-core/bin/lib/phase-remove.cjs +392 -0
- package/pan-wizard-core/bin/lib/phase.cjs +4 -369
- package/pan-wizard-core/bin/lib/runner.cjs +6 -0
- package/pan-wizard-core/bin/lib/state.cjs +10 -1
- package/pan-wizard-core/bin/lib/verify-deploy.cjs +181 -0
- package/pan-wizard-core/bin/lib/verify-drift.cjs +255 -0
- package/pan-wizard-core/bin/lib/verify-preflight.cjs +261 -0
- package/pan-wizard-core/bin/lib/verify-retro.cjs +177 -0
- package/pan-wizard-core/bin/lib/verify.cjs +33 -797
- package/pan-wizard-core/bin/pan-tools.cjs +35 -1
- package/pan-wizard-core/workflows/plan-phase.md +11 -0
- package/scripts/build-plugin.js +105 -0
- package/scripts/git-hooks/pre-commit +40 -0
- package/scripts/install-git-hooks.js +64 -0
- package/scripts/release-check.js +13 -2
package/bin/install.js
CHANGED
|
@@ -18,9 +18,13 @@ const {
|
|
|
18
18
|
convertClaudeToOpencodeFrontmatter, convertClaudeToGeminiToml, convertClaudeToGeminiAgent,
|
|
19
19
|
rewriteAskUserQuestionForCopilot, stripSubTags,
|
|
20
20
|
getCodexSkillAdapterHeader, convertClaudeCommandToCodexSkill,
|
|
21
|
+
convertClaudeCommandToUnifiedSkill,
|
|
21
22
|
getCopilotSkillAdapterHeader, convertClaudeCommandToCopilotSkill, convertClaudeToCopilotAgent,
|
|
22
23
|
processAttribution, parseJsonc,
|
|
23
24
|
detectModelCapabilities, buildClaudeSkillShim, stripThinkingFrontmatter,
|
|
25
|
+
geminiTransitionNotice,
|
|
26
|
+
convertClaudeAgentToCodexToml, codexTrustNotice,
|
|
27
|
+
buildCopilotHooksConfig,
|
|
24
28
|
} = lib;
|
|
25
29
|
|
|
26
30
|
// Colors
|
|
@@ -104,6 +108,18 @@ function getOpencodeGlobalDir() {
|
|
|
104
108
|
* @param {string} runtime - 'claude', 'opencode', 'gemini', or 'codex'
|
|
105
109
|
* @param {string|null} explicitDir - Explicit directory from --config-dir flag
|
|
106
110
|
*/
|
|
111
|
+
/**
|
|
112
|
+
* Codex skills root. Codex reads skills from the shared `.agents/skills/`
|
|
113
|
+
* tree — repo scope `$REPO_ROOT/.agents/skills`, user scope `~/.agents/skills`.
|
|
114
|
+
* `$CODEX_HOME/skills` is NOT a documented read location (verified against
|
|
115
|
+
* developers.openai.com/codex/skills, 2026-06).
|
|
116
|
+
*/
|
|
117
|
+
function getCodexSkillsRoot(isGlobal) {
|
|
118
|
+
return isGlobal
|
|
119
|
+
? path.join(os.homedir(), '.agents', 'skills')
|
|
120
|
+
: path.join(process.cwd(), '.agents', 'skills');
|
|
121
|
+
}
|
|
122
|
+
|
|
107
123
|
function getGlobalDir(runtime, explicitDir = null) {
|
|
108
124
|
if (runtime === 'opencode') {
|
|
109
125
|
// For OpenCode, --config-dir overrides env vars
|
|
@@ -195,12 +211,15 @@ function parseConfigDirArg() {
|
|
|
195
211
|
const explicitConfigDir = parseConfigDirArg();
|
|
196
212
|
const hasHelp = args.includes('--help') || args.includes('-h');
|
|
197
213
|
const forceStatusline = args.includes('--force-statusline');
|
|
214
|
+
// ADR-0028 Phase 1 (opt-in): compile commands once into the shared
|
|
215
|
+
// .agents/skills/ tree for every runtime instead of per-runtime command trees.
|
|
216
|
+
const unifiedSkills = args.includes('--unified-skills');
|
|
198
217
|
|
|
199
218
|
console.log(banner);
|
|
200
219
|
|
|
201
220
|
// Show help if requested
|
|
202
221
|
if (hasHelp) {
|
|
203
|
-
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`);
|
|
222
|
+
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 ${cyan}--unified-skills${reset} Install commands as one shared .agents/skills/ tree (ADR-0028 alpha)\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`);
|
|
204
223
|
process.exit(0);
|
|
205
224
|
}
|
|
206
225
|
|
|
@@ -266,8 +285,13 @@ function getCommitAttribution(runtime) {
|
|
|
266
285
|
result = settings.attribution.commit;
|
|
267
286
|
}
|
|
268
287
|
} else if (runtime === 'copilot') {
|
|
269
|
-
// Copilot CLI:
|
|
270
|
-
|
|
288
|
+
// Copilot CLI: user-editable settings live in settings.json; config.json is
|
|
289
|
+
// legacy (auto-migrated by the CLI, now internal state) — fall back for old installs
|
|
290
|
+
const copilotDir = getGlobalDir('copilot', explicitConfigDir);
|
|
291
|
+
let config = readSettings(path.join(copilotDir, 'settings.json'));
|
|
292
|
+
if (!config.attribution) {
|
|
293
|
+
config = readSettings(path.join(copilotDir, 'config.json'));
|
|
294
|
+
}
|
|
271
295
|
if (!config.attribution || config.attribution.commit === undefined) {
|
|
272
296
|
result = undefined;
|
|
273
297
|
} else if (config.attribution.commit === '') {
|
|
@@ -290,11 +314,11 @@ function getCommitAttribution(runtime) {
|
|
|
290
314
|
|
|
291
315
|
/**
|
|
292
316
|
* Copy commands to a flat structure for OpenCode
|
|
293
|
-
* OpenCode expects:
|
|
317
|
+
* OpenCode expects: commands/pan-help.md (invoked as /pan-help)
|
|
294
318
|
* Source structure: commands/pan/help.md
|
|
295
|
-
*
|
|
319
|
+
*
|
|
296
320
|
* @param {string} srcDir - Source directory (e.g., commands/pan/)
|
|
297
|
-
* @param {string} destDir - Destination directory (e.g.,
|
|
321
|
+
* @param {string} destDir - Destination directory (e.g., commands/)
|
|
298
322
|
* @param {string} prefix - Prefix for filenames (e.g., 'pan')
|
|
299
323
|
* @param {string} pathPrefix - Path prefix for file references
|
|
300
324
|
* @param {string} runtime - Target runtime ('claude' or 'opencode')
|
|
@@ -308,11 +332,11 @@ function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) {
|
|
|
308
332
|
if (fs.existsSync(destDir)) {
|
|
309
333
|
for (const file of fs.readdirSync(destDir)) {
|
|
310
334
|
if (file.startsWith(`${prefix}-`) && file.endsWith('.md')) {
|
|
311
|
-
try { fs.unlinkSync(path.join(destDir, file)); } catch {}
|
|
335
|
+
try { fs.unlinkSync(path.join(destDir, file)); } catch (err) { pushInstallWarning('staleCleanup', file, err); }
|
|
312
336
|
}
|
|
313
337
|
}
|
|
314
338
|
} else {
|
|
315
|
-
try { fs.mkdirSync(destDir, { recursive: true }); } catch {}
|
|
339
|
+
try { fs.mkdirSync(destDir, { recursive: true }); } catch (err) { pushInstallWarning('mkdir', destDir, err); }
|
|
316
340
|
}
|
|
317
341
|
|
|
318
342
|
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
@@ -360,13 +384,13 @@ function copyCommandsAsCodexSkills(srcDir, skillsDir, prefix, pathPrefix, runtim
|
|
|
360
384
|
return;
|
|
361
385
|
}
|
|
362
386
|
|
|
363
|
-
try { fs.mkdirSync(skillsDir, { recursive: true }); } catch {}
|
|
387
|
+
try { fs.mkdirSync(skillsDir, { recursive: true }); } catch (err) { pushInstallWarning('mkdir', skillsDir, err); }
|
|
364
388
|
|
|
365
389
|
// Remove previous PAN Codex skills to avoid stale command skills.
|
|
366
390
|
const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
367
391
|
for (const entry of existing) {
|
|
368
392
|
if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
|
|
369
|
-
try { fs.rmSync(path.join(skillsDir, entry.name), { recursive: true }); } catch {}
|
|
393
|
+
try { fs.rmSync(path.join(skillsDir, entry.name), { recursive: true }); } catch (err) { pushInstallWarning('staleCleanup', entry.name, err); }
|
|
370
394
|
}
|
|
371
395
|
}
|
|
372
396
|
|
|
@@ -409,6 +433,187 @@ function copyCommandsAsCodexSkills(srcDir, skillsDir, prefix, pathPrefix, runtim
|
|
|
409
433
|
recurse(srcDir, prefix);
|
|
410
434
|
}
|
|
411
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Copy PAN commands as runtime-neutral unified skills (ADR-0028 Phase 1).
|
|
438
|
+
* Same tree layout as the Codex path (.agents/skills/pan-{name}/SKILL.md) but
|
|
439
|
+
* the content carries the runtime-neutral adapter so every runtime can consume it.
|
|
440
|
+
* Core references resolve against the shared .agents/pan-wizard-core/ copy
|
|
441
|
+
* (Phase 2), so the compiled skills are identical regardless of which runtime
|
|
442
|
+
* installed them — only the rare agent-file references stay runtime-local.
|
|
443
|
+
*/
|
|
444
|
+
function copyCommandsAsUnifiedSkills(srcDir, skillsDir, prefix, pathPrefix, corePrefix, runtime) {
|
|
445
|
+
if (!fs.existsSync(srcDir)) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
try { fs.mkdirSync(skillsDir, { recursive: true }); } catch (err) { pushInstallWarning('mkdir', skillsDir, err); }
|
|
450
|
+
|
|
451
|
+
// Remove previous PAN skills to avoid stale command skills.
|
|
452
|
+
const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
453
|
+
for (const entry of existing) {
|
|
454
|
+
if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
|
|
455
|
+
try { fs.rmSync(path.join(skillsDir, entry.name), { recursive: true }); } catch (err) { pushInstallWarning('staleCleanup', entry.name, err); }
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function recurse(currentSrcDir, currentPrefix) {
|
|
460
|
+
const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true });
|
|
461
|
+
|
|
462
|
+
for (const entry of entries) {
|
|
463
|
+
const srcPath = path.join(currentSrcDir, entry.name);
|
|
464
|
+
if (entry.isDirectory()) {
|
|
465
|
+
recurse(srcPath, `${currentPrefix}-${entry.name}`);
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (!entry.name.endsWith('.md')) {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const baseName = entry.name.replace('.md', '');
|
|
474
|
+
const skillName = `${currentPrefix}-${baseName}`;
|
|
475
|
+
const skillDir = path.join(skillsDir, skillName);
|
|
476
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
477
|
+
|
|
478
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
479
|
+
// Core + agent-definition references → shared .agents/ copies (specific,
|
|
480
|
+
// before the generic rewrites); everything else .claude-scoped → the
|
|
481
|
+
// installing runtime. Agent refs point at the canonical reference copies
|
|
482
|
+
// shipped with the shared core — the runtime's own agents dir may carry
|
|
483
|
+
// a different format (Codex TOML, Copilot .agent.md).
|
|
484
|
+
content = content.replace(/~\/\.claude\/pan-wizard-core\//g, `${corePrefix}pan-wizard-core/`);
|
|
485
|
+
content = content.replace(/\.\/\.claude\/pan-wizard-core\//g, `${corePrefix}pan-wizard-core/`);
|
|
486
|
+
content = content.replace(/~\/\.claude\/agents\//g, `${corePrefix}pan-wizard-core/agents/`);
|
|
487
|
+
content = content.replace(/\.\/\.claude\/agents\//g, `${corePrefix}pan-wizard-core/agents/`);
|
|
488
|
+
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
489
|
+
content = content.replace(/\.\/\.claude\//g, `./${getDirName(runtime)}/`);
|
|
490
|
+
// Not every runtime puts a `pan-tools` bin on PATH — invoke via node.
|
|
491
|
+
const panToolsPath = `${corePrefix}pan-wizard-core/bin/pan-tools.cjs`;
|
|
492
|
+
content = content.replace(/\bpan-tools\b(?=\s+[a-z])/g, `node ${panToolsPath}`);
|
|
493
|
+
content = processAttribution(content, getCommitAttribution(runtime));
|
|
494
|
+
content = convertClaudeCommandToUnifiedSkill(content, skillName);
|
|
495
|
+
|
|
496
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
recurse(srcDir, prefix);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Remove a runtime's proprietary command surface after a --unified-skills
|
|
505
|
+
* install so commands don't resolve twice (ADR-0028 Phase 1 sweep).
|
|
506
|
+
*/
|
|
507
|
+
function sweepProprietaryCommandSurfaces(targetDir, runtime) {
|
|
508
|
+
if (runtime === 'opencode') {
|
|
509
|
+
for (const dirName of ['commands', 'command']) {
|
|
510
|
+
const commandDir = path.join(targetDir, dirName);
|
|
511
|
+
try {
|
|
512
|
+
for (const file of fs.readdirSync(commandDir)) {
|
|
513
|
+
if (file.startsWith('pan-') && file.endsWith('.md')) {
|
|
514
|
+
try { fs.unlinkSync(path.join(commandDir, file)); } catch {}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
} catch { /* dir absent — nothing to sweep */ }
|
|
518
|
+
}
|
|
519
|
+
} else if (runtime === 'copilot') {
|
|
520
|
+
const skillsDir = path.join(targetDir, 'skills');
|
|
521
|
+
try {
|
|
522
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
523
|
+
if (entry.isDirectory() && entry.name.startsWith('pan-')) {
|
|
524
|
+
try { fs.rmSync(path.join(skillsDir, entry.name), { recursive: true }); } catch {}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
} catch { /* dir absent — nothing to sweep */ }
|
|
528
|
+
} else if (runtime === 'codex') {
|
|
529
|
+
// Legacy .codex/skills location (dead read path; swept on upgrade anyway)
|
|
530
|
+
const legacySkillsDir = path.join(targetDir, 'skills');
|
|
531
|
+
try {
|
|
532
|
+
for (const entry of fs.readdirSync(legacySkillsDir, { withFileTypes: true })) {
|
|
533
|
+
if (entry.isDirectory() && entry.name.startsWith('pan-')) {
|
|
534
|
+
try { fs.rmSync(path.join(legacySkillsDir, entry.name), { recursive: true }); } catch {}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
} catch { /* dir absent — nothing to sweep */ }
|
|
538
|
+
} else {
|
|
539
|
+
// Claude Code & Gemini: nested commands/pan tree (+ Claude skill shims)
|
|
540
|
+
try { fs.rmSync(path.join(targetDir, 'commands', 'pan'), { recursive: true }); } catch {}
|
|
541
|
+
if (runtime === 'claude') {
|
|
542
|
+
const skillsDir = path.join(targetDir, 'skills');
|
|
543
|
+
try {
|
|
544
|
+
for (const file of fs.readdirSync(skillsDir)) {
|
|
545
|
+
if (file.startsWith('pan-') && file.endsWith('.md')) {
|
|
546
|
+
try { fs.unlinkSync(path.join(skillsDir, file)); } catch {}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
} catch { /* dir absent — nothing to sweep */ }
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Copy pan-wizard-core into the shared .agents/ root for unified installs
|
|
556
|
+
* (ADR-0028 Phase 2). Core-internal references resolve against the shared
|
|
557
|
+
* prefix, so the copy is runtime-neutral and rewrites across runtimes are
|
|
558
|
+
* idempotent; the few non-core references (agent files, cache paths) resolve
|
|
559
|
+
* against the installing runtime. Skill mentions are normalized to the
|
|
560
|
+
* neutral /pan-{name} form to match the unified SKILL.md content.
|
|
561
|
+
*/
|
|
562
|
+
function copySharedCore(srcDir, destDir, corePrefix, runtimePathPrefix, runtime) {
|
|
563
|
+
try {
|
|
564
|
+
if (fs.existsSync(destDir)) {
|
|
565
|
+
fs.rmSync(destDir, { recursive: true });
|
|
566
|
+
}
|
|
567
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
568
|
+
} catch (err) {
|
|
569
|
+
throw new Error(`copySharedCore: cannot prepare ${destDir}: ${err.message}`);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const dirName = getDirName(runtime);
|
|
573
|
+
|
|
574
|
+
(function recurse(currentSrc, currentDest) {
|
|
575
|
+
for (const entry of fs.readdirSync(currentSrc, { withFileTypes: true })) {
|
|
576
|
+
const srcPath = path.join(currentSrc, entry.name);
|
|
577
|
+
const destPath = path.join(currentDest, entry.name);
|
|
578
|
+
|
|
579
|
+
if (entry.isDirectory()) {
|
|
580
|
+
try { fs.mkdirSync(destPath, { recursive: true }); } catch (err) { pushInstallWarning('copySharedCore(mkdir)', destPath, err); }
|
|
581
|
+
recurse(srcPath, destPath);
|
|
582
|
+
} else if (entry.name.endsWith('.md')) {
|
|
583
|
+
try {
|
|
584
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
585
|
+
content = content.replace(/~\/\.claude\/pan-wizard-core\//g, `${corePrefix}pan-wizard-core/`);
|
|
586
|
+
content = content.replace(/\.\/\.claude\/pan-wizard-core\//g, `${corePrefix}pan-wizard-core/`);
|
|
587
|
+
// Agent-definition refs → the canonical reference copies in the
|
|
588
|
+
// shared core (runtime agents dirs carry runtime-specific formats).
|
|
589
|
+
content = content.replace(/~\/\.claude\/agents\//g, `${corePrefix}pan-wizard-core/agents/`);
|
|
590
|
+
content = content.replace(/\.\/\.claude\/agents\//g, `${corePrefix}pan-wizard-core/agents/`);
|
|
591
|
+
content = content.replace(/~\/\.claude\//g, runtimePathPrefix);
|
|
592
|
+
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
593
|
+
content = processAttribution(content, getCommitAttribution(runtime));
|
|
594
|
+
content = convertSlashCommandsToCopilotSkillMentions(content);
|
|
595
|
+
fs.writeFileSync(destPath, content);
|
|
596
|
+
} catch (err) {
|
|
597
|
+
pushInstallWarning('copySharedCore(md)', destPath, err);
|
|
598
|
+
}
|
|
599
|
+
} else {
|
|
600
|
+
try {
|
|
601
|
+
fs.copyFileSync(srcPath, destPath);
|
|
602
|
+
} catch (err) {
|
|
603
|
+
pushInstallWarning('copySharedCore(copy)', destPath, err);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
})(srcDir, destDir);
|
|
608
|
+
|
|
609
|
+
// learnings/internal is source-only — strip it like the per-runtime copy does
|
|
610
|
+
try {
|
|
611
|
+
fs.rmSync(path.join(destDir, 'learnings', 'internal'), { recursive: true, force: true });
|
|
612
|
+
} catch (err) {
|
|
613
|
+
if (err.code !== 'ENOENT') pushInstallWarning('stripInternalLearnings', 'learnings/internal', err);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
412
617
|
/**
|
|
413
618
|
* Copy PAN commands as Copilot CLI skills.
|
|
414
619
|
* Creates skills/pan-{name}/SKILL.md directory structure.
|
|
@@ -419,13 +624,13 @@ function copyCommandsAsCopilotSkills(srcDir, skillsDir, prefix, pathPrefix, runt
|
|
|
419
624
|
return;
|
|
420
625
|
}
|
|
421
626
|
|
|
422
|
-
try { fs.mkdirSync(skillsDir, { recursive: true }); } catch {}
|
|
627
|
+
try { fs.mkdirSync(skillsDir, { recursive: true }); } catch (err) { pushInstallWarning('mkdir', skillsDir, err); }
|
|
423
628
|
|
|
424
629
|
// Remove previous PAN Copilot skills to avoid stale command skills
|
|
425
630
|
const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
426
631
|
for (const entry of existing) {
|
|
427
632
|
if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
|
|
428
|
-
try { fs.rmSync(path.join(skillsDir, entry.name), { recursive: true }); } catch {}
|
|
633
|
+
try { fs.rmSync(path.join(skillsDir, entry.name), { recursive: true }); } catch (err) { pushInstallWarning('staleCleanup', entry.name, err); }
|
|
429
634
|
}
|
|
430
635
|
}
|
|
431
636
|
|
|
@@ -670,25 +875,54 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
670
875
|
|
|
671
876
|
let removedCount = 0;
|
|
672
877
|
|
|
878
|
+
// ADR-0028 Phase 2 ref-counting: another runtime still tracking the shared
|
|
879
|
+
// .agents tree (manifest keys reaching outside its config dir) keeps the
|
|
880
|
+
// tree alive when this runtime uninstalls.
|
|
881
|
+
function otherRuntimeTrackingShared(currentRuntime) {
|
|
882
|
+
const others = ['claude', 'codex', 'gemini', 'opencode', 'copilot']
|
|
883
|
+
.filter(r => r !== currentRuntime);
|
|
884
|
+
for (const rt of others) {
|
|
885
|
+
const dir = isGlobal
|
|
886
|
+
? getGlobalDir(rt, null)
|
|
887
|
+
: path.join(process.cwd(), getDirName(rt));
|
|
888
|
+
try {
|
|
889
|
+
const m = JSON.parse(fs.readFileSync(path.join(dir, MANIFEST_NAME), 'utf8'));
|
|
890
|
+
if (Object.keys(m.files || {}).some(k => k.startsWith('../'))) return rt;
|
|
891
|
+
} catch { /* no manifest for that runtime */ }
|
|
892
|
+
}
|
|
893
|
+
return null;
|
|
894
|
+
}
|
|
895
|
+
|
|
673
896
|
// 1. Remove PAN commands/skills
|
|
674
897
|
if (isOpencode) {
|
|
675
|
-
// OpenCode: remove
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
898
|
+
// OpenCode: remove commands/pan-*.md files (plus legacy singular command/
|
|
899
|
+
// left by pre-plural installs).
|
|
900
|
+
for (const dirName of ['commands', 'command']) {
|
|
901
|
+
const commandDir = path.join(targetDir, dirName);
|
|
902
|
+
if (fs.existsSync(commandDir)) {
|
|
903
|
+
const files = fs.readdirSync(commandDir);
|
|
904
|
+
let removedHere = 0;
|
|
905
|
+
for (const file of files) {
|
|
906
|
+
if (file.startsWith('pan-') && file.endsWith('.md')) {
|
|
907
|
+
try { fs.unlinkSync(path.join(commandDir, file)); } catch {}
|
|
908
|
+
removedCount++;
|
|
909
|
+
removedHere++;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
if (removedHere > 0) {
|
|
913
|
+
console.log(` ${green}✓${reset} Removed PAN commands from ${dirName}/`);
|
|
683
914
|
}
|
|
684
915
|
}
|
|
685
|
-
console.log(` ${green}✓${reset} Removed PAN commands from command/`);
|
|
686
916
|
}
|
|
687
917
|
} else if (isCodex || isCopilot) {
|
|
688
|
-
// Codex & Copilot CLI: remove skills/pan-*/SKILL.md skill directories
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
918
|
+
// Codex & Copilot CLI: remove skills/pan-*/SKILL.md skill directories from
|
|
919
|
+
// the runtime-local locations. The shared .agents/skills tree (Codex's
|
|
920
|
+
// primary surface, plus any --unified-skills install) is handled by the
|
|
921
|
+
// ref-counted sweep below.
|
|
922
|
+
const skillDirs = [path.join(targetDir, 'skills')];
|
|
923
|
+
let skillCount = 0;
|
|
924
|
+
for (const skillsDir of skillDirs) {
|
|
925
|
+
if (!fs.existsSync(skillsDir)) continue;
|
|
692
926
|
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
693
927
|
for (const entry of entries) {
|
|
694
928
|
if (entry.isDirectory() && entry.name.startsWith('pan-')) {
|
|
@@ -696,10 +930,10 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
696
930
|
skillCount++;
|
|
697
931
|
}
|
|
698
932
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
}
|
|
933
|
+
}
|
|
934
|
+
if (skillCount > 0) {
|
|
935
|
+
removedCount++;
|
|
936
|
+
console.log(` ${green}✓${reset} Removed ${skillCount} ${isCopilot ? 'Copilot CLI' : 'Codex'} skills`);
|
|
703
937
|
}
|
|
704
938
|
} else {
|
|
705
939
|
// Claude Code & Gemini: remove commands/pan/ directory
|
|
@@ -733,6 +967,57 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
733
967
|
}
|
|
734
968
|
}
|
|
735
969
|
|
|
970
|
+
// ADR-0028 unified tree: sweep the shared .agents tree when this runtime
|
|
971
|
+
// tracks it (manifest ../ keys; Codex always uses it) AND no other runtime
|
|
972
|
+
// still does (Phase 2 ref-counting). The last tracker also removes the
|
|
973
|
+
// shared core and prunes empty .agents directories.
|
|
974
|
+
{
|
|
975
|
+
let manifestHasShared = false;
|
|
976
|
+
try {
|
|
977
|
+
const m = JSON.parse(fs.readFileSync(path.join(targetDir, MANIFEST_NAME), 'utf8'));
|
|
978
|
+
manifestHasShared = Object.keys(m.files || {}).some(k => k.startsWith('../'));
|
|
979
|
+
} catch { /* no manifest — nothing tracked out-of-tree */ }
|
|
980
|
+
|
|
981
|
+
if (isCodex || manifestHasShared) {
|
|
982
|
+
const stillTracking = otherRuntimeTrackingShared(runtime);
|
|
983
|
+
if (stillTracking) {
|
|
984
|
+
console.log(` ${dim}ℹ Shared .agents/skills left in place — still tracked by ${stillTracking}${reset}`);
|
|
985
|
+
} else {
|
|
986
|
+
const sharedDir = getCodexSkillsRoot(isGlobal);
|
|
987
|
+
let sharedCount = 0;
|
|
988
|
+
try {
|
|
989
|
+
for (const entry of fs.readdirSync(sharedDir, { withFileTypes: true })) {
|
|
990
|
+
if (entry.isDirectory() && entry.name.startsWith('pan-')) {
|
|
991
|
+
try { fs.rmSync(path.join(sharedDir, entry.name), { recursive: true }); } catch {}
|
|
992
|
+
sharedCount++;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
} catch { /* shared tree absent */ }
|
|
996
|
+
if (sharedCount > 0) {
|
|
997
|
+
removedCount++;
|
|
998
|
+
console.log(` ${green}✓${reset} Removed ${sharedCount} unified skills from .agents/skills/`);
|
|
999
|
+
}
|
|
1000
|
+
// Shared core ships only with --unified-skills installs; remove it
|
|
1001
|
+
// alongside the last tracked skills sweep.
|
|
1002
|
+
const agentsRoot = path.dirname(sharedDir);
|
|
1003
|
+
const sharedCore = path.join(agentsRoot, 'pan-wizard-core');
|
|
1004
|
+
if (fs.existsSync(sharedCore)) {
|
|
1005
|
+
try {
|
|
1006
|
+
fs.rmSync(sharedCore, { recursive: true });
|
|
1007
|
+
removedCount++;
|
|
1008
|
+
console.log(` ${green}✓${reset} Removed shared pan-wizard-core from .agents/`);
|
|
1009
|
+
} catch { /* best-effort */ }
|
|
1010
|
+
}
|
|
1011
|
+
// Prune now-empty shared directories (never directories with foreign content)
|
|
1012
|
+
for (const dir of [sharedDir, agentsRoot]) {
|
|
1013
|
+
try {
|
|
1014
|
+
if (fs.readdirSync(dir).length === 0) fs.rmdirSync(dir);
|
|
1015
|
+
} catch { /* non-empty or absent — keep */ }
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
736
1021
|
// 2. Remove pan-wizard-core directory
|
|
737
1022
|
const panDir = path.join(targetDir, 'pan-wizard-core');
|
|
738
1023
|
if (fs.existsSync(panDir)) {
|
|
@@ -741,13 +1026,13 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
741
1026
|
console.log(` ${green}✓${reset} Removed pan-wizard-core/`);
|
|
742
1027
|
}
|
|
743
1028
|
|
|
744
|
-
// 3. Remove PAN agents (pan-*.md files only)
|
|
1029
|
+
// 3. Remove PAN agents (pan-*.md / pan-*.agent.md / pan-*.toml files only)
|
|
745
1030
|
const agentsDir = path.join(targetDir, 'agents');
|
|
746
1031
|
if (fs.existsSync(agentsDir)) {
|
|
747
1032
|
const files = fs.readdirSync(agentsDir);
|
|
748
1033
|
let agentCount = 0;
|
|
749
1034
|
for (const file of files) {
|
|
750
|
-
if (file.startsWith('pan-') && file.endsWith('.md')) {
|
|
1035
|
+
if (file.startsWith('pan-') && (file.endsWith('.md') || file.endsWith('.toml'))) {
|
|
751
1036
|
try { fs.unlinkSync(path.join(agentsDir, file)); } catch {}
|
|
752
1037
|
agentCount++;
|
|
753
1038
|
}
|
|
@@ -758,10 +1043,10 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
758
1043
|
}
|
|
759
1044
|
}
|
|
760
1045
|
|
|
761
|
-
// 4. Remove PAN hooks
|
|
1046
|
+
// 4. Remove PAN hooks (scripts + Copilot CLI hooks config file)
|
|
762
1047
|
const hooksDir = path.join(targetDir, 'hooks');
|
|
763
1048
|
if (fs.existsSync(hooksDir)) {
|
|
764
|
-
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'];
|
|
1049
|
+
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', 'pan.json'];
|
|
765
1050
|
let hookCount = 0;
|
|
766
1051
|
for (const hook of panHooks) {
|
|
767
1052
|
const hookPath = path.join(hooksDir, hook);
|
|
@@ -776,6 +1061,44 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
776
1061
|
}
|
|
777
1062
|
}
|
|
778
1063
|
|
|
1064
|
+
// 4a. Remove native workflow scripts (Claude-only surface)
|
|
1065
|
+
const workflowsDir = path.join(targetDir, 'workflows');
|
|
1066
|
+
if (fs.existsSync(workflowsDir)) {
|
|
1067
|
+
let wfCount = 0;
|
|
1068
|
+
for (const file of fs.readdirSync(workflowsDir)) {
|
|
1069
|
+
if (file.startsWith('pan-') && file.endsWith('.js')) {
|
|
1070
|
+
try { fs.unlinkSync(path.join(workflowsDir, file)); } catch {}
|
|
1071
|
+
wfCount++;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
if (wfCount > 0) {
|
|
1075
|
+
removedCount++;
|
|
1076
|
+
console.log(` ${green}✓${reset} Removed ${wfCount} native workflows`);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// 4b. Codex: strip PAN entries from the shared .codex/hooks.json (foreign
|
|
1081
|
+
// hook registrations are preserved; the file is deleted only when nothing
|
|
1082
|
+
// but PAN content remained).
|
|
1083
|
+
if (isCodex) {
|
|
1084
|
+
const hooksJsonPath = path.join(targetDir, 'hooks.json');
|
|
1085
|
+
try {
|
|
1086
|
+
const rawText = fs.readFileSync(hooksJsonPath, 'utf8');
|
|
1087
|
+
const before = JSON.stringify(JSON.parse(rawText));
|
|
1088
|
+
// removeCodexPanHooks mutates its argument — parse a fresh copy for it.
|
|
1089
|
+
const stripped = lib.removeCodexPanHooks(JSON.parse(rawText));
|
|
1090
|
+
if (stripped === null) {
|
|
1091
|
+
fs.unlinkSync(hooksJsonPath);
|
|
1092
|
+
removedCount++;
|
|
1093
|
+
console.log(` ${green}✓${reset} Removed hooks.json (only PAN hooks remained)`);
|
|
1094
|
+
} else if (JSON.stringify(stripped) !== before) {
|
|
1095
|
+
fs.writeFileSync(hooksJsonPath, JSON.stringify(stripped, null, 2) + '\n');
|
|
1096
|
+
removedCount++;
|
|
1097
|
+
console.log(` ${green}✓${reset} Removed PAN hooks from hooks.json`);
|
|
1098
|
+
}
|
|
1099
|
+
} catch { /* absent or unparseable — nothing to strip */ }
|
|
1100
|
+
}
|
|
1101
|
+
|
|
779
1102
|
// 5. Remove PAN package.json (CommonJS mode marker)
|
|
780
1103
|
const pkgJsonPath = path.join(targetDir, 'package.json');
|
|
781
1104
|
if (fs.existsSync(pkgJsonPath)) {
|
|
@@ -856,6 +1179,35 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
856
1179
|
// Ignore JSON parse errors
|
|
857
1180
|
}
|
|
858
1181
|
}
|
|
1182
|
+
|
|
1183
|
+
// Clean PAN statusline from the documented settings read paths:
|
|
1184
|
+
// settings.json (global ~/.copilot/) and copilot/settings.json (.github/).
|
|
1185
|
+
for (const { settingsPath, removableDir } of [
|
|
1186
|
+
{ settingsPath: path.join(targetDir, 'settings.json'), removableDir: false },
|
|
1187
|
+
{ settingsPath: path.join(targetDir, 'copilot', 'settings.json'), removableDir: true },
|
|
1188
|
+
]) {
|
|
1189
|
+
try {
|
|
1190
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
1191
|
+
if (settings.statusLine && settings.statusLine.command &&
|
|
1192
|
+
settings.statusLine.command.includes('pan-statusline')) {
|
|
1193
|
+
delete settings.statusLine;
|
|
1194
|
+
if (Object.keys(settings).length === 0) {
|
|
1195
|
+
fs.unlinkSync(settingsPath);
|
|
1196
|
+
if (removableDir) {
|
|
1197
|
+
// Only the nested .github/copilot/ dir PAN may have created —
|
|
1198
|
+
// never the user's ~/.copilot home.
|
|
1199
|
+
try { fs.rmdirSync(path.dirname(settingsPath)); } catch { /* non-empty — keep */ }
|
|
1200
|
+
}
|
|
1201
|
+
} else {
|
|
1202
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
1203
|
+
}
|
|
1204
|
+
console.log(` ${green}✓${reset} Removed PAN statusline from settings`);
|
|
1205
|
+
removedCount++;
|
|
1206
|
+
}
|
|
1207
|
+
} catch {
|
|
1208
|
+
// Missing file or parse error — nothing to clean
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
859
1211
|
}
|
|
860
1212
|
|
|
861
1213
|
// 6b. Clean up settings.json (remove PAN hooks and statusline)
|
|
@@ -916,6 +1268,27 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
916
1268
|
}
|
|
917
1269
|
}
|
|
918
1270
|
|
|
1271
|
+
// Remove PAN hooks from SubagentStop (cost logger v3.4+, trace logger v3.5+)
|
|
1272
|
+
if (settings.hooks && settings.hooks.SubagentStop) {
|
|
1273
|
+
const before = settings.hooks.SubagentStop.length;
|
|
1274
|
+
settings.hooks.SubagentStop = settings.hooks.SubagentStop.filter(entry => {
|
|
1275
|
+
if (entry.hooks && Array.isArray(entry.hooks)) {
|
|
1276
|
+
const hasPanHook = entry.hooks.some(h =>
|
|
1277
|
+
h.command && (h.command.includes('pan-cost-logger') || h.command.includes('pan-trace-logger'))
|
|
1278
|
+
);
|
|
1279
|
+
return !hasPanHook;
|
|
1280
|
+
}
|
|
1281
|
+
return true;
|
|
1282
|
+
});
|
|
1283
|
+
if (settings.hooks.SubagentStop.length < before) {
|
|
1284
|
+
settingsModified = true;
|
|
1285
|
+
console.log(` ${green}✓${reset} Removed cost/trace logger hooks from settings`);
|
|
1286
|
+
}
|
|
1287
|
+
if (settings.hooks.SubagentStop.length === 0) {
|
|
1288
|
+
delete settings.hooks.SubagentStop;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
919
1292
|
// Clean up empty hooks object
|
|
920
1293
|
if (settings.hooks && Object.keys(settings.hooks).length === 0) {
|
|
921
1294
|
delete settings.hooks;
|
|
@@ -997,13 +1370,42 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
997
1370
|
console.log(` ${green}✓${reset} Removed ${MANIFEST_NAME}`);
|
|
998
1371
|
}
|
|
999
1372
|
|
|
1373
|
+
// 7b. AGENTS.md / CLAUDE.md rules layer (ADR-0028 Phase 3): strip the PAN
|
|
1374
|
+
// marker block when this was the last PAN runtime in the project (the
|
|
1375
|
+
// manifest scan runs AFTER this runtime's manifest was removed above, so
|
|
1376
|
+
// any hit means another runtime still needs the section). Local only.
|
|
1377
|
+
if (!isGlobal) {
|
|
1378
|
+
const anotherRuntimeInstalled = ['.claude', '.codex', '.gemini', '.opencode', '.github']
|
|
1379
|
+
.some(d => fs.existsSync(path.join(process.cwd(), d, MANIFEST_NAME)));
|
|
1380
|
+
if (!anotherRuntimeInstalled) {
|
|
1381
|
+
for (const fileName of ['AGENTS.md', 'CLAUDE.md']) {
|
|
1382
|
+
const filePath = path.join(process.cwd(), fileName);
|
|
1383
|
+
try {
|
|
1384
|
+
const existing = fs.readFileSync(filePath, 'utf8');
|
|
1385
|
+
const stripped = lib.removeAgentsMdSection(existing);
|
|
1386
|
+
if (stripped === existing) continue; // no PAN block — leave untouched
|
|
1387
|
+
if (stripped === null) {
|
|
1388
|
+
fs.unlinkSync(filePath);
|
|
1389
|
+
console.log(` ${green}✓${reset} Removed ${fileName} (only the PAN section remained)`);
|
|
1390
|
+
} else {
|
|
1391
|
+
fs.writeFileSync(filePath, stripped);
|
|
1392
|
+
console.log(` ${green}✓${reset} Removed PAN section from ${fileName}`);
|
|
1393
|
+
}
|
|
1394
|
+
removedCount++;
|
|
1395
|
+
} catch { /* file absent — nothing to strip */ }
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1000
1400
|
// 8. Clean up empty PAN directories
|
|
1001
1401
|
const dirsToClean = [
|
|
1002
1402
|
path.join(targetDir, 'agents'),
|
|
1003
1403
|
path.join(targetDir, 'hooks'),
|
|
1004
1404
|
path.join(targetDir, 'skills'),
|
|
1405
|
+
path.join(targetDir, 'workflows'),
|
|
1005
1406
|
path.join(targetDir, 'commands', 'pan'),
|
|
1006
1407
|
path.join(targetDir, 'commands'),
|
|
1408
|
+
path.join(targetDir, 'command'), // legacy OpenCode singular dir
|
|
1007
1409
|
];
|
|
1008
1410
|
for (const dir of dirsToClean) {
|
|
1009
1411
|
try {
|
|
@@ -1183,14 +1585,23 @@ function generateManifest(dir, baseDir) {
|
|
|
1183
1585
|
/**
|
|
1184
1586
|
* Write file manifest after installation for future modification detection
|
|
1185
1587
|
*/
|
|
1186
|
-
function writeManifest(configDir, runtime = 'claude') {
|
|
1588
|
+
function writeManifest(configDir, runtime = 'claude', isGlobal = false) {
|
|
1187
1589
|
const isOpencode = runtime === 'opencode';
|
|
1188
1590
|
const isCodex = runtime === 'codex';
|
|
1189
1591
|
const isCopilot = runtime === 'copilot';
|
|
1190
1592
|
const panDir = path.join(configDir, 'pan-wizard-core');
|
|
1191
1593
|
const commandsDir = path.join(configDir, 'commands', 'pan');
|
|
1192
|
-
const opencodeCommandDir = path.join(configDir, '
|
|
1193
|
-
|
|
1594
|
+
const opencodeCommandDir = path.join(configDir, 'commands');
|
|
1595
|
+
// Codex skills (and every runtime under --unified-skills, ADR-0028) live in
|
|
1596
|
+
// the shared .agents/skills tree (outside configDir); Copilot otherwise uses
|
|
1597
|
+
// configDir/skills. Out-of-tree manifest keys are stored relative to
|
|
1598
|
+
// configDir (e.g. "../.agents/skills/...") so existing
|
|
1599
|
+
// path.join(configDir, key) consumers resolve them.
|
|
1600
|
+
const skillsShared = isCodex || unifiedSkills;
|
|
1601
|
+
const codexSkillsDir = skillsShared ? getCodexSkillsRoot(isGlobal) : path.join(configDir, 'skills');
|
|
1602
|
+
const codexSkillsPrefix = skillsShared
|
|
1603
|
+
? path.relative(configDir, codexSkillsDir).replace(/\\/g, '/')
|
|
1604
|
+
: 'skills';
|
|
1194
1605
|
const agentsDir = path.join(configDir, 'agents');
|
|
1195
1606
|
const manifest = { version: pkg.version, timestamp: new Date().toISOString(), files: {} };
|
|
1196
1607
|
|
|
@@ -1198,31 +1609,31 @@ function writeManifest(configDir, runtime = 'claude') {
|
|
|
1198
1609
|
for (const [rel, hash] of Object.entries(panHashes)) {
|
|
1199
1610
|
manifest.files['pan-wizard-core/' + rel] = hash;
|
|
1200
1611
|
}
|
|
1201
|
-
if (!isOpencode && !isCodex && fs.existsSync(commandsDir)) {
|
|
1612
|
+
if (!isOpencode && !isCodex && !unifiedSkills && fs.existsSync(commandsDir)) {
|
|
1202
1613
|
const cmdHashes = generateManifest(commandsDir);
|
|
1203
1614
|
for (const [rel, hash] of Object.entries(cmdHashes)) {
|
|
1204
1615
|
manifest.files['commands/pan/' + rel] = hash;
|
|
1205
1616
|
}
|
|
1206
1617
|
}
|
|
1207
|
-
if (isOpencode && fs.existsSync(opencodeCommandDir)) {
|
|
1618
|
+
if (isOpencode && !unifiedSkills && fs.existsSync(opencodeCommandDir)) {
|
|
1208
1619
|
for (const file of fs.readdirSync(opencodeCommandDir)) {
|
|
1209
1620
|
if (file.startsWith('pan-') && file.endsWith('.md')) {
|
|
1210
|
-
manifest.files['
|
|
1621
|
+
manifest.files['commands/' + file] = fileHash(path.join(opencodeCommandDir, file));
|
|
1211
1622
|
}
|
|
1212
1623
|
}
|
|
1213
1624
|
}
|
|
1214
|
-
if ((isCodex || isCopilot) && fs.existsSync(codexSkillsDir)) {
|
|
1625
|
+
if ((isCodex || isCopilot || unifiedSkills) && fs.existsSync(codexSkillsDir)) {
|
|
1215
1626
|
for (const skillName of listCodexSkillNames(codexSkillsDir)) {
|
|
1216
1627
|
const skillRoot = path.join(codexSkillsDir, skillName);
|
|
1217
1628
|
const skillHashes = generateManifest(skillRoot);
|
|
1218
1629
|
for (const [rel, hash] of Object.entries(skillHashes)) {
|
|
1219
|
-
manifest.files[
|
|
1630
|
+
manifest.files[`${codexSkillsPrefix}/${skillName}/${rel}`] = hash;
|
|
1220
1631
|
}
|
|
1221
1632
|
}
|
|
1222
1633
|
}
|
|
1223
1634
|
if (fs.existsSync(agentsDir)) {
|
|
1224
1635
|
for (const file of fs.readdirSync(agentsDir)) {
|
|
1225
|
-
if (file.startsWith('pan-') && file.endsWith('.md')) {
|
|
1636
|
+
if (file.startsWith('pan-') && (file.endsWith('.md') || file.endsWith('.toml'))) {
|
|
1226
1637
|
manifest.files['agents/' + file] = fileHash(path.join(agentsDir, file));
|
|
1227
1638
|
}
|
|
1228
1639
|
}
|
|
@@ -1236,6 +1647,15 @@ function writeManifest(configDir, runtime = 'claude') {
|
|
|
1236
1647
|
}
|
|
1237
1648
|
}
|
|
1238
1649
|
}
|
|
1650
|
+
// Track native workflow scripts (Claude-only surface)
|
|
1651
|
+
const workflowsDir = path.join(configDir, 'workflows');
|
|
1652
|
+
if (fs.existsSync(workflowsDir)) {
|
|
1653
|
+
for (const file of fs.readdirSync(workflowsDir)) {
|
|
1654
|
+
if (file.startsWith('pan-') && file.endsWith('.js')) {
|
|
1655
|
+
manifest.files['workflows/' + file] = fileHash(path.join(workflowsDir, file));
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1239
1659
|
|
|
1240
1660
|
try {
|
|
1241
1661
|
fs.writeFileSync(path.join(configDir, MANIFEST_NAME), JSON.stringify(manifest, null, 2));
|
|
@@ -1260,6 +1680,11 @@ function saveLocalPatches(configDir) {
|
|
|
1260
1680
|
const modified = [];
|
|
1261
1681
|
|
|
1262
1682
|
for (const [relPath, originalHash] of Object.entries(manifest.files || {})) {
|
|
1683
|
+
// Keys reaching outside configDir (Codex skills in ../.agents/skills/)
|
|
1684
|
+
// can't be backed up under patchesDir — path.join would collapse the
|
|
1685
|
+
// `..` and write outside the patches tree. Skip them; they're still
|
|
1686
|
+
// overwritten cleanly on reinstall.
|
|
1687
|
+
if (relPath.split('/').includes('..')) continue;
|
|
1263
1688
|
const fullPath = path.join(configDir, relPath);
|
|
1264
1689
|
if (!fs.existsSync(fullPath)) continue;
|
|
1265
1690
|
const currentHash = fileHash(fullPath);
|
|
@@ -1378,31 +1803,108 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
1378
1803
|
// Clean up orphaned files from previous versions
|
|
1379
1804
|
cleanupOrphanedFiles(targetDir);
|
|
1380
1805
|
|
|
1381
|
-
// OpenCode uses
|
|
1806
|
+
// OpenCode uses commands/ (flat), Codex uses skills/, Claude/Gemini use commands/pan/
|
|
1382
1807
|
try {
|
|
1383
|
-
if (
|
|
1384
|
-
//
|
|
1385
|
-
|
|
1808
|
+
if (unifiedSkills) {
|
|
1809
|
+
// ADR-0028 Phase 1: every runtime consumes one runtime-neutral
|
|
1810
|
+
// .agents/skills/ tree; the proprietary command surface is swept so
|
|
1811
|
+
// commands don't resolve twice.
|
|
1812
|
+
const skillsDir = getCodexSkillsRoot(isGlobal);
|
|
1813
|
+
const agentsRoot = path.dirname(skillsDir);
|
|
1814
|
+
const corePrefix = isGlobal
|
|
1815
|
+
? `${agentsRoot.replace(/\\/g, '/')}/`
|
|
1816
|
+
: './.agents/';
|
|
1817
|
+
|
|
1818
|
+
// ADR-0028 Phase 2: shared runtime-neutral core. Skills resolve
|
|
1819
|
+
// pan-tools against this copy regardless of which runtime installed
|
|
1820
|
+
// last; the per-runtime core remains for agents/hooks.
|
|
1821
|
+
const sharedCoreDest = path.join(agentsRoot, 'pan-wizard-core');
|
|
1822
|
+
copySharedCore(path.join(src, 'pan-wizard-core'), sharedCoreDest, corePrefix, pathPrefix, runtime);
|
|
1823
|
+
try { fs.writeFileSync(path.join(sharedCoreDest, 'VERSION'), pkg.version); } catch { /* non-fatal */ }
|
|
1824
|
+
|
|
1825
|
+
// Canonical agent-definition reference copies (ADR-0028 agent-ref
|
|
1826
|
+
// canonicalization): shared content references these instead of the
|
|
1827
|
+
// runtime's agents dir, whose files carry runtime-specific formats.
|
|
1828
|
+
// These are reading material for agents, not runtime registrations —
|
|
1829
|
+
// the per-runtime installed agents still drive subagent spawning.
|
|
1830
|
+
try {
|
|
1831
|
+
const agentsRefDir = path.join(sharedCoreDest, 'agents');
|
|
1832
|
+
fs.mkdirSync(agentsRefDir, { recursive: true });
|
|
1833
|
+
const agentsSrc = path.join(src, 'agents');
|
|
1834
|
+
for (const f of fs.readdirSync(agentsSrc).filter(n => n.endsWith('.md'))) {
|
|
1835
|
+
let content = fs.readFileSync(path.join(agentsSrc, f), 'utf8');
|
|
1836
|
+
content = content.replace(/~\/\.claude\/pan-wizard-core\//g, `${corePrefix}pan-wizard-core/`);
|
|
1837
|
+
content = content.replace(/\.\/\.claude\/pan-wizard-core\//g, `${corePrefix}pan-wizard-core/`);
|
|
1838
|
+
content = convertSlashCommandsToCopilotSkillMentions(content);
|
|
1839
|
+
fs.writeFileSync(path.join(agentsRefDir, f), content);
|
|
1840
|
+
}
|
|
1841
|
+
} catch (e) {
|
|
1842
|
+
pushInstallWarning('unifiedAgentRefs', 'pan-wizard-core/agents', e);
|
|
1843
|
+
}
|
|
1844
|
+
console.log(` ${green}✓${reset} Installed shared pan-wizard-core to ${isGlobal ? '~/.agents/' : '.agents/'}`);
|
|
1845
|
+
|
|
1846
|
+
const panSrc = path.join(src, 'commands', 'pan');
|
|
1847
|
+
copyCommandsAsUnifiedSkills(panSrc, skillsDir, 'pan', pathPrefix, corePrefix, runtime);
|
|
1848
|
+
sweepProprietaryCommandSurfaces(targetDir, runtime);
|
|
1849
|
+
|
|
1850
|
+
const installedSkillNames = listCodexSkillNames(skillsDir);
|
|
1851
|
+
if (installedSkillNames.length > 0) {
|
|
1852
|
+
const label = isGlobal ? '~/.agents/skills/' : '.agents/skills/';
|
|
1853
|
+
console.log(` ${green}✓${reset} Installed ${installedSkillNames.length} unified skills to ${label} (ADR-0028)`);
|
|
1854
|
+
} else {
|
|
1855
|
+
failures.push('.agents/skills/pan-* (unified)');
|
|
1856
|
+
}
|
|
1857
|
+
} else if (isOpencode) {
|
|
1858
|
+
// OpenCode: flat structure in commands/ directory. Plural since
|
|
1859
|
+
// OpenCode 2026 releases — singular command/ is back-compat only.
|
|
1860
|
+
const commandDir = path.join(targetDir, 'commands');
|
|
1386
1861
|
fs.mkdirSync(commandDir, { recursive: true });
|
|
1387
1862
|
|
|
1388
|
-
// Copy commands/pan/*.md as
|
|
1863
|
+
// Copy commands/pan/*.md as commands/pan-*.md (flatten structure)
|
|
1389
1864
|
const panSrc = path.join(src, 'commands', 'pan');
|
|
1390
1865
|
copyFlattenedCommands(panSrc, commandDir, 'pan', pathPrefix, runtime);
|
|
1391
|
-
|
|
1866
|
+
|
|
1867
|
+
// Upgrade path: remove PAN files left in the legacy singular command/
|
|
1868
|
+
// directory by older installs so commands don't resolve twice.
|
|
1869
|
+
const legacyCommandDir = path.join(targetDir, 'command');
|
|
1870
|
+
if (fs.existsSync(legacyCommandDir)) {
|
|
1871
|
+
for (const file of fs.readdirSync(legacyCommandDir)) {
|
|
1872
|
+
if (file.startsWith('pan-') && file.endsWith('.md')) {
|
|
1873
|
+
try { fs.unlinkSync(path.join(legacyCommandDir, file)); } catch {}
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
if (verifyInstalled(commandDir, 'commands/pan-*')) {
|
|
1392
1879
|
const count = fs.readdirSync(commandDir).filter(f => f.startsWith('pan-')).length;
|
|
1393
|
-
console.log(` ${green}✓${reset} Installed ${count} commands to
|
|
1880
|
+
console.log(` ${green}✓${reset} Installed ${count} commands to commands/`);
|
|
1394
1881
|
} else {
|
|
1395
|
-
failures.push('
|
|
1882
|
+
failures.push('commands/pan-*');
|
|
1396
1883
|
}
|
|
1397
1884
|
} else if (isCodex) {
|
|
1398
|
-
|
|
1885
|
+
// Codex reads skills from the shared .agents/skills tree (repo or user
|
|
1886
|
+
// scope) — $CODEX_HOME/skills is no longer a read location (2026-06).
|
|
1887
|
+
const skillsDir = getCodexSkillsRoot(isGlobal);
|
|
1399
1888
|
const panSrc = path.join(src, 'commands', 'pan');
|
|
1400
1889
|
copyCommandsAsCodexSkills(panSrc, skillsDir, 'pan', pathPrefix, runtime);
|
|
1890
|
+
|
|
1891
|
+
// Upgrade path: remove pan-* skills left in the legacy .codex/skills/
|
|
1892
|
+
// location by older installs (dead directory for current Codex).
|
|
1893
|
+
const legacySkillsDir = path.join(targetDir, 'skills');
|
|
1894
|
+
if (fs.existsSync(legacySkillsDir)) {
|
|
1895
|
+
for (const entry of fs.readdirSync(legacySkillsDir, { withFileTypes: true })) {
|
|
1896
|
+
if (entry.isDirectory() && entry.name.startsWith('pan-')) {
|
|
1897
|
+
try { fs.rmSync(path.join(legacySkillsDir, entry.name), { recursive: true }); } catch {}
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1401
1902
|
const installedSkillNames = listCodexSkillNames(skillsDir);
|
|
1402
1903
|
if (installedSkillNames.length > 0) {
|
|
1403
|
-
|
|
1904
|
+
const label = isGlobal ? '~/.agents/skills/' : '.agents/skills/';
|
|
1905
|
+
console.log(` ${green}✓${reset} Installed ${installedSkillNames.length} skills to ${label}`);
|
|
1404
1906
|
} else {
|
|
1405
|
-
failures.push('skills/pan-*');
|
|
1907
|
+
failures.push('.agents/skills/pan-*');
|
|
1406
1908
|
}
|
|
1407
1909
|
} else if (isCopilot) {
|
|
1408
1910
|
const skillsDir = path.join(targetDir, 'skills');
|
|
@@ -1474,8 +1976,9 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
1474
1976
|
if (fs.existsSync(internalLearningsDir)) {
|
|
1475
1977
|
try {
|
|
1476
1978
|
fs.rmSync(internalLearningsDir, { recursive: true, force: true });
|
|
1477
|
-
} catch {
|
|
1478
|
-
//
|
|
1979
|
+
} catch (err) {
|
|
1980
|
+
// Surface it: a failed strip would ship source-only internal learnings.
|
|
1981
|
+
if (err.code !== 'ENOENT') pushInstallWarning('stripInternalLearnings', 'learnings/internal', err);
|
|
1479
1982
|
}
|
|
1480
1983
|
}
|
|
1481
1984
|
|
|
@@ -1496,10 +1999,10 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
1496
1999
|
const agentsDest = path.join(targetDir, 'agents');
|
|
1497
2000
|
fs.mkdirSync(agentsDest, { recursive: true });
|
|
1498
2001
|
|
|
1499
|
-
// Remove old PAN agents (pan-*.md
|
|
2002
|
+
// Remove old PAN agents (pan-*.md, pan-*.agent.md, pan-*.toml) before copying new ones
|
|
1500
2003
|
if (fs.existsSync(agentsDest)) {
|
|
1501
2004
|
for (const file of fs.readdirSync(agentsDest)) {
|
|
1502
|
-
if (file.startsWith('pan-') && file.endsWith('.md')) {
|
|
2005
|
+
if (file.startsWith('pan-') && (file.endsWith('.md') || file.endsWith('.toml'))) {
|
|
1503
2006
|
fs.unlinkSync(path.join(agentsDest, file));
|
|
1504
2007
|
}
|
|
1505
2008
|
}
|
|
@@ -1514,6 +2017,18 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
1514
2017
|
const dirRegex = /~\/\.claude\//g;
|
|
1515
2018
|
content = content.replace(dirRegex, pathPrefix);
|
|
1516
2019
|
content = processAttribution(content, getCommitAttribution(runtime));
|
|
2020
|
+
// Codex custom agents are standalone TOML files (2026-06 format);
|
|
2021
|
+
// markdown in .codex/agents/ is not recognized. Handle before
|
|
2022
|
+
// stripThinkingFrontmatter so `effort:` survives to be mapped to
|
|
2023
|
+
// Codex's native model_reasoning_effort field.
|
|
2024
|
+
if (isCodex) {
|
|
2025
|
+
const toml = convertClaudeAgentToCodexToml(stripSubTags(content));
|
|
2026
|
+
if (toml) {
|
|
2027
|
+
const tomlName = entry.name.replace(/\.md$/, '.toml');
|
|
2028
|
+
fs.writeFileSync(path.join(agentsDest, tomlName), toml);
|
|
2029
|
+
}
|
|
2030
|
+
continue;
|
|
2031
|
+
}
|
|
1517
2032
|
// E-3 (per-runtime): strip unsupported thinking frontmatter and inject
|
|
1518
2033
|
// prose preamble for runtimes without native extended thinking.
|
|
1519
2034
|
content = stripThinkingFrontmatter(content, runtime);
|
|
@@ -1522,8 +2037,6 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
1522
2037
|
content = convertClaudeToOpencodeFrontmatter(content);
|
|
1523
2038
|
} else if (isGemini) {
|
|
1524
2039
|
content = convertClaudeToGeminiAgent(content);
|
|
1525
|
-
} else if (isCodex) {
|
|
1526
|
-
content = convertClaudeToCodexMarkdown(content);
|
|
1527
2040
|
} else if (isCopilot) {
|
|
1528
2041
|
content = convertClaudeToCopilotAgent(content);
|
|
1529
2042
|
}
|
|
@@ -1599,9 +2112,10 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
1599
2112
|
}
|
|
1600
2113
|
}
|
|
1601
2114
|
|
|
1602
|
-
if (!
|
|
2115
|
+
if (!isOpencode) {
|
|
1603
2116
|
// Copy hooks from dist/ (bundled with dependencies)
|
|
1604
|
-
// Hooks are
|
|
2117
|
+
// Hooks are supported by Claude Code, Gemini, Copilot CLI, and (since
|
|
2118
|
+
// 2026-06) Codex via .codex/hooks.json. OpenCode has no hook support.
|
|
1605
2119
|
// Template paths for the target runtime (replaces '.claude' with correct config dir)
|
|
1606
2120
|
try {
|
|
1607
2121
|
const hooksSrc = path.join(src, 'hooks', 'dist');
|
|
@@ -1636,15 +2150,60 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
1636
2150
|
}
|
|
1637
2151
|
}
|
|
1638
2152
|
|
|
2153
|
+
// Native Claude Code workflows (2026-06): deterministic orchestration
|
|
2154
|
+
// scripts for PAN's fan-out-shaped protocols. Claude-only — no other
|
|
2155
|
+
// runtime has an equivalent discovery surface.
|
|
2156
|
+
if (runtime === 'claude') {
|
|
2157
|
+
try {
|
|
2158
|
+
const workflowsDir = path.join(targetDir, 'workflows');
|
|
2159
|
+
fs.mkdirSync(workflowsDir, { recursive: true });
|
|
2160
|
+
const scripts = lib.buildNativeWorkflowScripts();
|
|
2161
|
+
for (const { name, content } of scripts) {
|
|
2162
|
+
fs.writeFileSync(path.join(workflowsDir, name), content);
|
|
2163
|
+
}
|
|
2164
|
+
console.log(` ${green}✓${reset} Installed ${scripts.length} native workflows to workflows/`);
|
|
2165
|
+
} catch (e) {
|
|
2166
|
+
pushInstallWarning('nativeWorkflows', 'workflows/pan-*', e);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
|
|
1639
2170
|
if (failures.length > 0) {
|
|
1640
2171
|
console.error(`\n ${yellow}Installation incomplete!${reset} Failed: ${failures.join(', ')}`);
|
|
1641
2172
|
process.exit(1);
|
|
1642
2173
|
}
|
|
1643
2174
|
|
|
1644
2175
|
// Write file manifest for future modification detection
|
|
1645
|
-
const manifest = writeManifest(targetDir, runtime);
|
|
2176
|
+
const manifest = writeManifest(targetDir, runtime, isGlobal);
|
|
1646
2177
|
console.log(` ${green}✓${reset} Wrote file manifest (${MANIFEST_NAME})`);
|
|
1647
2178
|
|
|
2179
|
+
// AGENTS.md universal rules layer (ADR-0028 Phase 3): contribute one
|
|
2180
|
+
// marker-fenced PAN section to the project's AGENTS.md (read natively by
|
|
2181
|
+
// every PAN runtime), and bridge CLAUDE.md to it via @AGENTS.md for the
|
|
2182
|
+
// Claude runtime. Project-scoped — local installs only; user content
|
|
2183
|
+
// outside the markers is never touched.
|
|
2184
|
+
if (!isGlobal) {
|
|
2185
|
+
try {
|
|
2186
|
+
const agentsMdPath = path.join(process.cwd(), 'AGENTS.md');
|
|
2187
|
+
let agentsExisting = null;
|
|
2188
|
+
try { agentsExisting = fs.readFileSync(agentsMdPath, 'utf8'); } catch { /* absent */ }
|
|
2189
|
+
fs.writeFileSync(agentsMdPath, lib.upsertAgentsMdSection(agentsExisting, lib.buildAgentsMdSection()));
|
|
2190
|
+
console.log(` ${green}✓${reset} ${agentsExisting === null ? 'Created' : 'Updated'} AGENTS.md (PAN section)`);
|
|
2191
|
+
|
|
2192
|
+
if (runtime === 'claude') {
|
|
2193
|
+
const claudeMdPath = path.join(process.cwd(), 'CLAUDE.md');
|
|
2194
|
+
let claudeExisting = null;
|
|
2195
|
+
try { claudeExisting = fs.readFileSync(claudeMdPath, 'utf8'); } catch { /* absent */ }
|
|
2196
|
+
const bridged = lib.ensureClaudeMdImport(claudeExisting);
|
|
2197
|
+
if (bridged !== claudeExisting) {
|
|
2198
|
+
fs.writeFileSync(claudeMdPath, bridged);
|
|
2199
|
+
console.log(` ${green}✓${reset} Bridged CLAUDE.md to AGENTS.md (@AGENTS.md import)`);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
} catch (e) {
|
|
2203
|
+
pushInstallWarning('agentsMd', 'AGENTS.md', e);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
|
|
1648
2207
|
// Report any backed-up local patches
|
|
1649
2208
|
reportLocalPatches(targetDir, runtime);
|
|
1650
2209
|
|
|
@@ -1657,12 +2216,17 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
1657
2216
|
for (const w of verifyResult.warnings) console.error(` - ${w}`);
|
|
1658
2217
|
}
|
|
1659
2218
|
if (!verifyResult.ok) {
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
2219
|
+
const emptyFiles = verifyResult.empty || [];
|
|
2220
|
+
const problems = [
|
|
2221
|
+
...verifyResult.missing.map(m => `${m} (missing)`),
|
|
2222
|
+
...emptyFiles.map(e => `${e} (empty — copy failed)`),
|
|
2223
|
+
];
|
|
2224
|
+
console.error(`\n ${red}✖ Install verification FAILED — ${verifyResult.missing.length} missing, ${emptyFiles.length} empty:${reset}`);
|
|
2225
|
+
// Limit output to first 20 to avoid screen-flooding
|
|
2226
|
+
const sample = problems.slice(0, 20);
|
|
1663
2227
|
for (const m of sample) console.error(` - ${m}`);
|
|
1664
|
-
if (
|
|
1665
|
-
console.error(` ... and ${
|
|
2228
|
+
if (problems.length > sample.length) {
|
|
2229
|
+
console.error(` ... and ${problems.length - sample.length} more`);
|
|
1666
2230
|
}
|
|
1667
2231
|
console.error(`\n Run ${cyan}pan-tools validate deployment${reset} for full diagnostics, then re-run the installer.\n`);
|
|
1668
2232
|
process.exit(1);
|
|
@@ -1682,10 +2246,6 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
1682
2246
|
}
|
|
1683
2247
|
}
|
|
1684
2248
|
|
|
1685
|
-
if (isCodex) {
|
|
1686
|
-
return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
2249
|
const updateCheckCommand = isGlobal
|
|
1690
2250
|
? buildHookCommand(targetDir, 'pan-check-update.js')
|
|
1691
2251
|
: 'node ' + dirName + '/hooks/pan-check-update.js';
|
|
@@ -1702,45 +2262,76 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
1702
2262
|
? buildHookCommand(targetDir, 'pan-trace-logger.js')
|
|
1703
2263
|
: 'node ' + dirName + '/hooks/pan-trace-logger.js';
|
|
1704
2264
|
|
|
1705
|
-
|
|
2265
|
+
if (isCodex) {
|
|
2266
|
+
// Codex hooks (2026-06): Claude-compatible PascalCase events in the shared
|
|
2267
|
+
// .codex/hooks.json — merge PAN entries non-destructively (foreign hooks
|
|
2268
|
+
// preserved, reinstall idempotent). Project-scoped hooks load once the
|
|
2269
|
+
// project is trusted; codexTrustNotice() already covers that.
|
|
2270
|
+
try {
|
|
2271
|
+
const hooksJsonPath = path.join(targetDir, 'hooks.json');
|
|
2272
|
+
let existing = null;
|
|
2273
|
+
try { existing = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8')); } catch { /* absent or invalid — start fresh */ }
|
|
2274
|
+
const merged = lib.mergeCodexHooksConfig(existing, {
|
|
2275
|
+
updateCheckCommand, contextMonitorCommand, costLoggerCommand, traceLoggerCommand,
|
|
2276
|
+
});
|
|
2277
|
+
fs.writeFileSync(hooksJsonPath, JSON.stringify(merged, null, 2) + '\n');
|
|
2278
|
+
console.log(` ${green}✓${reset} Configured hooks (.codex/hooks.json: update check, context monitor, cost + trace loggers)`);
|
|
2279
|
+
} catch (e) {
|
|
2280
|
+
pushInstallWarning('codexHooks', 'hooks.json', e);
|
|
2281
|
+
}
|
|
2282
|
+
return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
// Copilot CLI reads hook config from .github/hooks/*.json (version:1 schema),
|
|
2286
|
+
// NOT from config.json. Write a dedicated PAN hooks file and migrate away
|
|
2287
|
+
// from any legacy config.json registration.
|
|
1706
2288
|
if (isCopilot) {
|
|
2289
|
+
const hooksConfigPath = path.join(targetDir, 'hooks', 'pan.json');
|
|
2290
|
+
const hooksConfig = buildCopilotHooksConfig({ updateCheckCommand, contextMonitorCommand, costLoggerCommand, traceLoggerCommand });
|
|
2291
|
+
try {
|
|
2292
|
+
fs.mkdirSync(path.dirname(hooksConfigPath), { recursive: true });
|
|
2293
|
+
fs.writeFileSync(hooksConfigPath, JSON.stringify(hooksConfig, null, 2) + '\n');
|
|
2294
|
+
console.log(` ${green}✓${reset} Configured hooks (.github/hooks/pan.json: update check, context monitor, cost + trace loggers)`);
|
|
2295
|
+
} catch (e) {
|
|
2296
|
+
console.error(` ${yellow}✗${reset} Failed to write Copilot hooks config: ${e.message}`);
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
// Upgrade cleanup: strip PAN hook + statusline registrations left in
|
|
2300
|
+
// config.json by pre-2026-06 installs. Copilot CLI never read them there —
|
|
2301
|
+
// user-editable settings live in settings.json (~/.copilot/ global,
|
|
2302
|
+
// .github/copilot/ repo-level); config.json is internal CLI state.
|
|
1707
2303
|
const configPath = path.join(targetDir, 'config.json');
|
|
1708
2304
|
const config = readSettings(configPath);
|
|
1709
|
-
|
|
1710
|
-
if (
|
|
1711
|
-
|
|
2305
|
+
let legacyModified = false;
|
|
2306
|
+
if (config.hooks) {
|
|
2307
|
+
for (const evt of ['sessionStart', 'postToolUse']) {
|
|
2308
|
+
if (Array.isArray(config.hooks[evt])) {
|
|
2309
|
+
config.hooks[evt] = config.hooks[evt].filter(h =>
|
|
2310
|
+
!(h.command && (h.command.includes('pan-check-update') || h.command.includes('pan-context-monitor')))
|
|
2311
|
+
);
|
|
2312
|
+
if (config.hooks[evt].length === 0) delete config.hooks[evt];
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
if (Object.keys(config.hooks).length === 0) delete config.hooks;
|
|
2316
|
+
legacyModified = true;
|
|
1712
2317
|
}
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
2318
|
+
if (config.statusLine && config.statusLine.command &&
|
|
2319
|
+
config.statusLine.command.includes('pan-statusline')) {
|
|
2320
|
+
delete config.statusLine;
|
|
2321
|
+
legacyModified = true;
|
|
1717
2322
|
}
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
);
|
|
1721
|
-
if (!hasPanUpdateHook) {
|
|
1722
|
-
config.hooks.sessionStart.push({
|
|
1723
|
-
command: updateCheckCommand
|
|
1724
|
-
});
|
|
1725
|
-
console.log(` ${green}✓${reset} Configured update check hook`);
|
|
2323
|
+
if (legacyModified) {
|
|
2324
|
+
writeSettings(configPath, config);
|
|
1726
2325
|
}
|
|
1727
2326
|
|
|
1728
|
-
//
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
);
|
|
1735
|
-
if (!hasContextHook) {
|
|
1736
|
-
config.hooks.postToolUse.push({
|
|
1737
|
-
command: contextMonitorCommand
|
|
1738
|
-
});
|
|
1739
|
-
console.log(` ${green}✓${reset} Configured context window monitor hook`);
|
|
1740
|
-
}
|
|
2327
|
+
// Documented Copilot CLI settings read paths: ~/.copilot/settings.json
|
|
2328
|
+
// (global) or .github/copilot/settings.json (repo-level, committed).
|
|
2329
|
+
const copilotSettingsPath = isGlobal
|
|
2330
|
+
? path.join(targetDir, 'settings.json')
|
|
2331
|
+
: path.join(targetDir, 'copilot', 'settings.json');
|
|
2332
|
+
const copilotSettings = readSettings(copilotSettingsPath);
|
|
1741
2333
|
|
|
1742
|
-
|
|
1743
|
-
return { settingsPath: configPath, settings: config, statuslineCommand, runtime };
|
|
2334
|
+
return { settingsPath: copilotSettingsPath, settings: copilotSettings, statuslineCommand, runtime };
|
|
1744
2335
|
}
|
|
1745
2336
|
|
|
1746
2337
|
// Configure statusline and hooks in settings.json
|
|
@@ -1856,22 +2447,25 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
1856
2447
|
const isCopilot = runtime === 'copilot';
|
|
1857
2448
|
|
|
1858
2449
|
if (shouldInstallStatusline && !isOpencode && !isCodex) {
|
|
2450
|
+
// Same schema everywhere — Copilot CLI also uses {type: "command", command}
|
|
2451
|
+
// in settings.json (statusline is experimental there as of 2026-05).
|
|
2452
|
+
settings.statusLine = {
|
|
2453
|
+
type: 'command',
|
|
2454
|
+
command: statuslineCommand
|
|
2455
|
+
};
|
|
2456
|
+
console.log(` ${green}✓${reset} Configured statusline`);
|
|
1859
2457
|
if (isCopilot) {
|
|
1860
|
-
|
|
1861
|
-
settings.statusLine = {
|
|
1862
|
-
command: statuslineCommand
|
|
1863
|
-
};
|
|
1864
|
-
} else {
|
|
1865
|
-
settings.statusLine = {
|
|
1866
|
-
type: 'command',
|
|
1867
|
-
command: statuslineCommand
|
|
1868
|
-
};
|
|
2458
|
+
console.log(` ${dim}ℹ Copilot CLI statusline is experimental — if it doesn't render, start with 'copilot --experimental'${reset}`);
|
|
1869
2459
|
}
|
|
1870
|
-
console.log(` ${green}✓${reset} Configured statusline`);
|
|
1871
2460
|
}
|
|
1872
2461
|
|
|
1873
|
-
// Write settings/config when runtime supports it
|
|
1874
|
-
|
|
2462
|
+
// Write settings/config when runtime supports it. For Copilot, skip the
|
|
2463
|
+
// write when there is nothing to persist (avoids creating an empty
|
|
2464
|
+
// .github/copilot/settings.json).
|
|
2465
|
+
if (!isCodex && !(isCopilot && Object.keys(settings).length === 0)) {
|
|
2466
|
+
if (isCopilot) {
|
|
2467
|
+
try { fs.mkdirSync(path.dirname(settingsPath), { recursive: true }); } catch { /* surfaced by writeSettings */ }
|
|
2468
|
+
}
|
|
1875
2469
|
writeSettings(settingsPath, settings);
|
|
1876
2470
|
}
|
|
1877
2471
|
|
|
@@ -1896,8 +2490,8 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
1896
2490
|
!caps.has_thinking ? 'extended thinking (E-3, E-10, E-11)' : null,
|
|
1897
2491
|
].filter(Boolean).join(', ');
|
|
1898
2492
|
console.log(`
|
|
1899
|
-
${yellow}ℹ${reset} PAN 2.10+ is tuned for Opus 4.7. Default model "${modelField}" lacks: ${missing}.
|
|
1900
|
-
Features degrade gracefully, but upgrade to claude-opus-4-
|
|
2493
|
+
${yellow}ℹ${reset} PAN 2.10+ is tuned for Opus 4.7+ class models. Default model "${modelField}" lacks: ${missing}.
|
|
2494
|
+
Features degrade gracefully, but upgrade to claude-opus-4-8 (or claude-fable-5) for best results.`);
|
|
1901
2495
|
}
|
|
1902
2496
|
}
|
|
1903
2497
|
} catch {
|
|
@@ -1905,6 +2499,18 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
1905
2499
|
}
|
|
1906
2500
|
}
|
|
1907
2501
|
|
|
2502
|
+
// Gemini CLI → Antigravity transition (2026-06): informational, non-blocking.
|
|
2503
|
+
if (runtime === 'gemini' && !args.includes('--skip-warnings')) {
|
|
2504
|
+
console.log(`\n ${yellow}ℹ${reset} ${geminiTransitionNotice().split('\n').join('\n ')}`);
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
// Codex project-trust gate (2026-06): project-scoped .codex/ config only
|
|
2508
|
+
// loads once the project is trusted. Local installs only — personal
|
|
2509
|
+
// ~/.codex/ config is not gated.
|
|
2510
|
+
if (runtime === 'codex' && !isGlobal && !args.includes('--skip-warnings')) {
|
|
2511
|
+
console.log(`\n ${yellow}ℹ${reset} ${codexTrustNotice().split('\n').join('\n ')}`);
|
|
2512
|
+
}
|
|
2513
|
+
|
|
1908
2514
|
let program = 'Claude Code';
|
|
1909
2515
|
if (runtime === 'opencode') program = 'OpenCode';
|
|
1910
2516
|
if (runtime === 'gemini') program = 'Gemini';
|