xtrm-cli 0.5.0 → 0.5.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.pi/structured-returns/0e4a7405-1ac3-4ae1-8dbc-d31507b2e2e4.combined.log +17 -0
  2. package/.pi/structured-returns/0e4a7405-1ac3-4ae1-8dbc-d31507b2e2e4.stderr.log +0 -0
  3. package/.pi/structured-returns/0e4a7405-1ac3-4ae1-8dbc-d31507b2e2e4.stdout.log +17 -0
  4. package/dist/index.cjs +969 -1059
  5. package/dist/index.cjs.map +1 -1
  6. package/package.json +1 -1
  7. package/src/commands/clean.ts +7 -6
  8. package/src/commands/debug.ts +255 -0
  9. package/src/commands/docs.ts +180 -0
  10. package/src/commands/help.ts +92 -171
  11. package/src/commands/init.ts +9 -32
  12. package/src/commands/install-pi.ts +9 -16
  13. package/src/commands/install.ts +150 -2
  14. package/src/commands/pi-install.ts +10 -44
  15. package/src/core/context.ts +4 -52
  16. package/src/core/diff.ts +3 -16
  17. package/src/core/preflight.ts +0 -1
  18. package/src/index.ts +7 -4
  19. package/src/types/config.ts +0 -2
  20. package/src/utils/config-injector.ts +3 -3
  21. package/src/utils/pi-extensions.ts +41 -0
  22. package/src/utils/worktree-session.ts +86 -50
  23. package/test/extensions/beads-claim-lifecycle.test.ts +93 -0
  24. package/test/extensions/beads-parity.test.ts +94 -0
  25. package/test/extensions/extension-harness.ts +5 -5
  26. package/test/extensions/quality-gates-parity.test.ts +89 -0
  27. package/test/extensions/session-flow.test.ts +91 -0
  28. package/test/extensions/xtrm-loader.test.ts +38 -20
  29. package/test/install-pi.test.ts +22 -11
  30. package/test/pi-extensions.test.ts +50 -0
  31. package/test/session-launcher.test.ts +28 -38
  32. package/extensions/beads.ts +0 -109
  33. package/extensions/core/adapter.ts +0 -45
  34. package/extensions/core/lib.ts +0 -3
  35. package/extensions/core/logger.ts +0 -45
  36. package/extensions/core/runner.ts +0 -71
  37. package/extensions/custom-footer.ts +0 -160
  38. package/extensions/main-guard-post-push.ts +0 -44
  39. package/extensions/main-guard.ts +0 -126
  40. package/extensions/minimal-mode.ts +0 -201
  41. package/extensions/quality-gates.ts +0 -67
  42. package/extensions/service-skills.ts +0 -150
  43. package/extensions/xtrm-loader.ts +0 -89
  44. package/hooks/gitnexus-impact-reminder.py +0 -13
  45. package/src/commands/finish.ts +0 -25
  46. package/src/core/session-state.ts +0 -139
  47. package/src/core/xtrm-finish.ts +0 -267
  48. package/src/tests/session-flow-parity.test.ts +0 -118
  49. package/src/tests/session-state.test.ts +0 -124
  50. package/src/tests/xtrm-finish.test.ts +0 -148
@@ -1,180 +1,101 @@
1
1
  import { Command } from 'commander';
2
- import kleur from 'kleur';
3
2
 
4
- import path from 'path';
5
- import fs from 'fs-extra';
6
- import { findRepoRoot } from '../utils/repo-root.js';
7
- declare const __dirname: string;
8
-
9
- const HOOK_CATALOG: Array<{ file: string; event: string; desc: string; beads?: true; sessionFlow?: true }> = [
10
- { file: 'main-guard.mjs', event: 'PreToolUse', desc: 'Blocks direct edits / unsafe Bash on protected branches' },
11
- { file: 'main-guard-post-push.mjs', event: 'PostToolUse', desc: 'After feature-branch push, reminds PR/merge/sync steps' },
12
- { file: 'serena-workflow-reminder.py', event: 'SessionStart', desc: 'Injects Serena semantic editing workflow reminder' },
13
- { file: 'gitnexus/gitnexus-hook.cjs', event: 'PostToolUse', desc: 'Adds GitNexus context for search and Serena tooling' },
14
- { file: 'agent_context.py', event: 'Support module', desc: 'Shared hook I/O helper used by Python hook scripts' },
15
- { file: 'beads-edit-gate.mjs', event: 'PreToolUse', desc: 'Blocks file edits if no beads issue is claimed', beads: true },
16
- { file: 'beads-commit-gate.mjs', event: 'PreToolUse', desc: 'Blocks commits when no beads issue is in progress', beads: true },
17
- { file: 'beads-memory-gate.mjs', event: 'Stop', desc: 'Prompts memory save when claim was closed', beads: true },
18
- { file: 'beads-compact-save.mjs', event: 'PreCompact', desc: 'Saves in_progress issue + session-state bundle', beads: true },
19
- { file: 'beads-compact-restore.mjs', event: 'SessionStart', desc: 'Restores compacted issue + session-state bundle', beads: true },
20
- { file: 'beads-claim-sync.mjs', event: 'PostToolUse', desc: 'Auto-claim sync + worktree/session-state bootstrap', sessionFlow: true },
21
- { file: 'beads-stop-gate.mjs', event: 'Stop', desc: 'Blocks stop for waiting-merge/conflicting/pending-cleanup', sessionFlow: true },
22
- { file: 'branch-state.mjs', event: 'UserPromptSubmit', desc: 'Injects current git branch into prompt context' },
23
- { file: 'quality-check.cjs', event: 'PostToolUse', desc: 'Runs JS/TS quality checks on mutating edits' },
24
- { file: 'quality-check.py', event: 'PostToolUse', desc: 'Runs Python quality checks on mutating edits' },
25
- ];
26
-
27
- async function readSkillsFromDir(dir: string): Promise<Array<{ name: string; desc: string }>> {
28
- if (!(await fs.pathExists(dir))) return [];
29
- const entries = await fs.readdir(dir);
30
- const skills: Array<{ name: string; desc: string }> = [];
31
- for (const name of entries.sort()) {
32
- const skillMd = path.join(dir, name, 'SKILL.md');
33
- if (!(await fs.pathExists(skillMd))) continue;
34
- const content = await fs.readFile(skillMd, 'utf8');
35
- const m = content.match(/^description:\s*(.+)$/m);
36
- skills.push({ name, desc: m ? m[1].replace(/^["']|["']$/g, '').trim() : '' });
37
- }
38
- return skills;
39
- }
40
-
41
- async function readProjectSkillsFromDir(dir: string): Promise<Array<{ name: string; desc: string }>> {
42
- if (!(await fs.pathExists(dir))) return [];
43
- const entries = await fs.readdir(dir);
44
- const skills: Array<{ name: string; desc: string }> = [];
45
- for (const name of entries.sort()) {
46
- const readme = path.join(dir, name, 'README.md');
47
- if (!(await fs.pathExists(readme))) continue;
48
- const content = await fs.readFile(readme, 'utf8');
49
- const descLine = content.split('\n').find(line => {
50
- const trimmed = line.trim();
51
- return Boolean(trimmed) && !trimmed.startsWith('#') && !trimmed.startsWith('[') && !trimmed.startsWith('<');
52
- }) || '';
53
- skills.push({ name, desc: descLine.replace(/[*_`]/g, '').trim() });
54
- }
55
- return skills;
56
- }
57
-
58
- function resolvePkgRootFallback(): string | null {
59
- const candidates = [
60
- path.resolve(__dirname, '../..'),
61
- path.resolve(__dirname, '../../..'),
62
- ];
63
- const match = candidates.find(candidate =>
64
- fs.existsSync(path.join(candidate, 'skills')) || fs.existsSync(path.join(candidate, 'project-skills'))
65
- );
66
- return match || null;
67
- }
68
-
69
- function col(s: string, width: number): string {
70
- return s.length >= width ? s.slice(0, width - 1) + '\u2026' : s.padEnd(width);
3
+ function section(title: string, lines: string[]): string {
4
+ return [title, ...lines, ''].join('\n');
71
5
  }
72
6
 
73
7
  export function createHelpCommand(): Command {
74
8
  return new Command('help')
75
- .description('Show help information and component catalogue')
9
+ .description('Show rich CLI help in a plain text format')
76
10
  .action(async () => {
77
- let repoRoot: string;
78
- try { repoRoot = await findRepoRoot(); } catch { repoRoot = ''; }
79
- const pkgRoot = resolvePkgRootFallback();
80
-
81
- const skillsRoot = repoRoot || pkgRoot || '';
82
- const projectSkillsRoot = repoRoot || pkgRoot || '';
83
- const skills = skillsRoot ? await readSkillsFromDir(path.join(skillsRoot, 'skills')) : [];
84
- const projectSkills = projectSkillsRoot ? await readProjectSkillsFromDir(path.join(projectSkillsRoot, 'project-skills')) : [];
85
-
86
- const W = 80;
87
- const hr = kleur.dim('-'.repeat(W));
88
- const section = (title: string) => `\n${kleur.bold().cyan(title)}\n${hr}`;
89
-
90
- const installSection = [
91
- section('INSTALL COMMANDS'),
92
- '',
93
- ` ${kleur.bold('xtrm install all')}`,
94
- ` ${kleur.dim('Global install: skills + all hooks (including beads gates) + MCP servers.')}`,
95
- ` ${kleur.dim('Checks for beads+dolt and prompts to install if missing.')}`,
96
- '',
97
- ` ${kleur.bold('xtrm install basic')}`,
98
- ` ${kleur.dim('Global install: skills + general hooks + MCP servers.')}`,
99
- ` ${kleur.dim('No beads dependency -- safe to run with zero external deps.')}`,
100
- '',
101
- ` ${kleur.bold('xtrm install project')} ${kleur.dim('<tool-name | all>')}`,
102
- ` ${kleur.dim('Project-scoped install into .claude/ of current git root.')}`,
103
- ` ${kleur.dim('Run xtrm install project list to see available project skills.')}`,
104
- '',
105
- ` ${kleur.dim('Default target directories:')}`,
106
- ` ${kleur.dim('~/.claude/hooks (global hook scripts)')}`,
107
- ` ${kleur.dim('~/.claude/skills (global Claude skills)')}`,
108
- ` ${kleur.dim('~/.agents/skills (agents skills cache mirror)')}`,
109
- '',
110
- ` ${kleur.dim('Flags (all profiles): --dry-run --yes / -y --no-mcp --force --prune --backport')}`,
111
- ].join('\n');
112
-
113
- const general = HOOK_CATALOG.filter(h => !h.beads && !h.sessionFlow);
114
- const beads = HOOK_CATALOG.filter(h => h.beads);
115
- const sessionFlow = HOOK_CATALOG.filter(h => h.sessionFlow);
116
- const hookRows = (hooks: typeof HOOK_CATALOG) =>
117
- hooks.map(h =>
118
- ` ${kleur.white(col(h.file, 34))}${kleur.yellow(col(h.event, 20))}${kleur.dim(h.desc)}`
119
- ).join('\n');
120
-
121
- const hooksSection = [
122
- section('GLOBAL HOOKS'),
123
- '',
124
- kleur.dim(' ' + col('File', 34) + col('Event', 20) + 'Description'),
125
- '',
126
- hookRows(general),
127
- '',
128
- ` ${kleur.dim('beads gate hooks (xtrm install all -- require beads+dolt):')}`,
129
- hookRows(beads),
130
- '',
131
- ` ${kleur.dim('session-flow hooks (xtrm finish lifecycle):')}`,
132
- hookRows(sessionFlow),
133
- ].join('\n');
134
-
135
- const skillRows = skills.map(s => {
136
- const desc = s.desc.length > 46 ? s.desc.slice(0, 45) + '\u2026' : s.desc;
137
- return ` ${kleur.white(col(s.name, 30))}${kleur.dim(desc)}`;
138
- }).join('\n');
139
-
140
- const skillsSection = [
141
- section(`SKILLS ${kleur.dim('(' + skills.length + ' available)')}`),
142
- '',
143
- skills.length ? skillRows : kleur.dim(' (none found -- run from repo root to see skills)'),
144
- ].join('\n');
145
-
146
- const psRows = projectSkills.map(s =>
147
- ` ${kleur.white(col(s.name, 30))}${kleur.dim(s.desc)}`
148
- ).join('\n');
149
-
150
- const psSection = [
151
- section('PROJECT SKILLS + HOOKS'),
152
- '',
153
- projectSkills.length ? psRows : kleur.dim(' (none found in package)'),
154
- '',
155
- ` ${kleur.dim('Install: xtrm install project <name> | xtrm install project list')}`,
156
- ` ${kleur.dim('Each project skill can install .claude/skills plus project hooks/settings.')}`,
157
- ].join('\n');
158
-
159
- const otherSection = [
160
- section('OTHER COMMANDS'),
161
- '',
162
- ` ${kleur.bold('xtrm status')} ${kleur.dim('Show pending changes without applying them')}`,
163
- ` ${kleur.bold('xtrm finish')} ${kleur.dim('Run blocking session closure lifecycle (PR + cleanup)')}`,
164
- ` ${kleur.bold('xtrm reset')} ${kleur.dim('Clear saved preferences and start fresh')}`,
165
- ` ${kleur.bold('xtrm help')} ${kleur.dim('Show this overview')}`,
166
- ].join('\n');
167
-
168
- const resourcesSection = [
169
- section('RESOURCES'),
170
- '',
171
- ` Repository https://github.com/Jaggerxtrm/xtrm-tools`,
172
- ` Issues https://github.com/Jaggerxtrm/xtrm-tools/issues`,
173
- '',
174
- ` ${kleur.dim("Run 'xtrm <command> --help' for command-specific options.")}`,
175
- '',
176
- ].join('\n');
177
-
178
- console.log([installSection, hooksSection, skillsSection, psSection, otherSection, resourcesSection].join('\n'));
11
+ const blocks: string[] = [];
12
+
13
+ blocks.push(section('XTRM CLI', [
14
+ ' xtrm and xt are equivalent commands.',
15
+ ' Use xt for short workflow commands (xt claude, xt pi, xt end).',
16
+ ]));
17
+
18
+ blocks.push(section('USAGE', [
19
+ ' xtrm <command> [subcommand] [options]',
20
+ ' xt <command> [subcommand] [options]',
21
+ ]));
22
+
23
+ blocks.push(section('CORE WORKFLOW', [
24
+ ' 1) Start a runtime session in a worktree:',
25
+ ' xt claude [name] or xt pi [name]',
26
+ ' 2) Do your work in that worktree/branch.',
27
+ ' 3) Finish with:',
28
+ ' xt end',
29
+ ' 4) Manage old worktrees when needed:',
30
+ ' xt worktree list | xt worktree clean',
31
+ ]));
32
+
33
+ blocks.push(section('PRIMARY COMMANDS', [
34
+ ' xtrm install [target-selector] [options]',
35
+ ' Install/sync tools, hooks, skills, and MCP wiring.',
36
+ ' Options: --dry-run, --yes/-y, --prune, --backport',
37
+ '',
38
+ ' xtrm status [--json]',
39
+ ' Show pending changes for detected environments.',
40
+ '',
41
+ ' xtrm clean [options]',
42
+ ' Remove orphaned hooks/skills and stale hook wiring entries.',
43
+ ' Options: --dry-run, --hooks-only, --skills-only, --yes/-y',
44
+ '',
45
+ ' xtrm init',
46
+ ' Initialize project-level workflow setup.',
47
+ '',
48
+ ' xtrm docs show [filter] [--raw] [--json]',
49
+ ' Show frontmatter for README/CHANGELOG/docs/*.md.',
50
+ '',
51
+ ' xtrm debug [options]',
52
+ ' Stream xtrm event log (tool calls, gates, session/bd lifecycle).',
53
+ ' Options: --follow, --all, --session <id>, --type <domain>, --json',
54
+ '',
55
+ ' xtrm reset',
56
+ ' Clear saved CLI preferences.',
57
+ '',
58
+ ' xtrm help',
59
+ ' Show this help page.',
60
+ ]));
61
+
62
+ blocks.push(section('RUNTIME COMMANDS', [
63
+ ' xt claude [name]',
64
+ ' Launch Claude in a sandboxed xt/<name> worktree.',
65
+ ' xt claude install [--dry-run]',
66
+ ' Install/refresh xtrm Claude plugin + official plugins.',
67
+ ' xt claude status | xt claude doctor | xt claude reload',
68
+ '',
69
+ ' xt pi [name]',
70
+ ' Launch Pi in a sandboxed xt/<name> worktree.',
71
+ ' xt pi install [--dry-run]',
72
+ ' Non-interactive extension sync + package install.',
73
+ ' xt pi setup',
74
+ ' Interactive first-time setup.',
75
+ ' xt pi status | xt pi doctor | xt pi reload',
76
+ ]));
77
+
78
+ blocks.push(section('WORKTREE COMMANDS', [
79
+ ' xt worktree list',
80
+ ' List active xt/* worktrees and merge status.',
81
+ ' xt worktree clean [--yes/-y]',
82
+ ' Remove worktrees already merged into main.',
83
+ ' xt worktree remove <name> [--yes/-y]',
84
+ ' Remove a specific xt worktree by name or path.',
85
+ ]));
86
+
87
+ blocks.push(section('SESSION CLOSE', [
88
+ ' xt end [options]',
89
+ ' Rebase to origin/main, push, open PR, link issues, and optionally clean worktree.',
90
+ ' Options: --draft, --keep, --yes/-y',
91
+ ]));
92
+
93
+ blocks.push(section('NOTES', [
94
+ ' - Banner is shown only for xtrm install.',
95
+ ' - For command-level details, run: xtrm <command> --help',
96
+ ' - For subcommand details, run: xtrm <command> <subcommand> --help',
97
+ ]));
98
+
99
+ process.stdout.write(blocks.join('\n'));
179
100
  });
180
101
  }
@@ -566,8 +566,6 @@ export async function installProjectSkill(toolName: string, projectRootOverride?
566
566
  console.log(`${kleur.green(' ✓')} settings.json (hooks merged)`);
567
567
  }
568
568
 
569
- await syncProjectMcpServers(projectRoot);
570
-
571
569
  // Step 2: Skill Copy
572
570
  if (await fs.pathExists(skillSkillsDir)) {
573
571
  console.log(kleur.bold('\n── Installing Skills ─────────────────────'));
@@ -729,15 +727,14 @@ export function buildProjectInitGuide(): string {
729
727
  kleur.dim(' xtrm init (alias: xtrm project init)'),
730
728
  kleur.dim(' - Initializes beads workspace (bd init)'),
731
729
  kleur.dim(' - Refreshes GitNexus index if missing/stale'),
732
- kleur.dim(' - Syncs project-scoped MCP entries'),
733
- kleur.dim(' - Detects TS/Python/Docker project signals'),
734
- kleur.dim(' - Scaffolds service-registry.json when Docker services are detected'),
730
+ kleur.dim(' - Injects XTRM workflow headers into AGENTS.md + CLAUDE.md'),
731
+ kleur.dim(' - Detects TypeScript/Python project signals'),
735
732
  '',
736
733
  `${kleur.cyan('2) What is already global (no per-project install needed):')}`,
737
- kleur.dim(' - quality gates hooks (formerly installed via quality-gates)'),
738
- kleur.dim(' - service-skills routing and drift checks (formerly service-skills-set)'),
739
- kleur.dim(' - main-guard + beads workflow gates'),
740
- kleur.dim(' - optional TDD strategy guidance (legacy name: tdd-guard)'),
734
+ kleur.dim(' - quality gates hooks (ESLint/tsc/ruff/mypy on every edit)'),
735
+ kleur.dim(' - beads workflow gates (edit/commit/stop/memory enforcement)'),
736
+ kleur.dim(' - session-flow gates (claim sync, stop gate, xt end reminder)'),
737
+ kleur.dim(' - service-skills routing and drift checks'),
741
738
  '',
742
739
  `${kleur.cyan('3) Configure repo quality tools (hooks enforce what exists):')}`,
743
740
  kleur.dim(' - TS: eslint + prettier + tsc'),
@@ -749,12 +746,9 @@ export function buildProjectInitGuide(): string {
749
746
  kleur.dim(' - During work: keep issue status current; create discovered follow-ups'),
750
747
  kleur.dim(' - Finish work: bd close <id> --reason "Done" --json'),
751
748
  '',
752
- `${kleur.cyan('5) Git workflow (main-guard expected path):')}`,
753
- kleur.dim(' - git checkout -b feature/<name>'),
754
- kleur.dim(' - commit on feature branch only'),
755
- kleur.dim(' - git push -u origin feature/<name>'),
756
- kleur.dim(' - gh pr create --fill && gh pr merge --squash'),
757
- kleur.dim(' - git checkout main && git pull --ff-only'),
749
+ `${kleur.cyan('5) Git workflow:')}`,
750
+ kleur.dim(' - bd close <id> --reason "..." ← closes issue + auto-commits'),
751
+ kleur.dim(' - xt end ← push, PR, merge, worktree cleanup'),
758
752
  '',
759
753
  ];
760
754
 
@@ -781,31 +775,14 @@ async function bootstrapProjectInit(): Promise<void> {
781
775
  await runBdInitForProject(projectRoot);
782
776
  await injectProjectInstructionHeaders(projectRoot);
783
777
  await runGitNexusInitForProject(projectRoot);
784
- await syncProjectMcpServers(projectRoot);
785
-
786
- if (detected.dockerServices.length > 0) {
787
- const { generated, registryPath } = await ensureServiceRegistry(projectRoot, detected.dockerServices);
788
- detected.generatedRegistry = generated;
789
- detected.registryPath = registryPath;
790
- if (generated) {
791
- console.log(`${kleur.green(' ✓')} service registry scaffolded at ${path.relative(projectRoot, registryPath)}`);
792
- } else {
793
- console.log(kleur.dim(' ✓ service-registry.json already includes detected services'));
794
- }
795
- }
796
778
 
797
779
  const projectTypes: string[] = [];
798
780
  if (detected.hasTypeScript) projectTypes.push('TypeScript');
799
781
  if (detected.hasPython) projectTypes.push('Python');
800
- if (detected.dockerServices.length > 0) projectTypes.push('Docker');
801
782
 
802
783
  console.log(kleur.bold('\nProject initialized.'));
803
784
  console.log(kleur.white(` Quality gates active globally.`));
804
785
  console.log(kleur.white(` Project types: ${projectTypes.length > 0 ? projectTypes.join(', ') : 'none detected'}.`));
805
- console.log(kleur.white(` Services detected: ${detected.dockerServices.length > 0 ? detected.dockerServices.join(', ') : 'none'}.`));
806
- if (detected.registryPath) {
807
- console.log(kleur.dim(` Service registry: ${detected.registryPath}`));
808
- }
809
786
  console.log('');
810
787
  }
811
788
 
@@ -7,6 +7,7 @@ import { spawnSync } from 'node:child_process';
7
7
  import { homedir } from 'node:os';
8
8
  import { findRepoRoot } from '../utils/repo-root.js';
9
9
  import { t, sym } from '../utils/theme.js';
10
+ import { syncManagedPiExtensions } from '../utils/pi-extensions.js';
10
11
 
11
12
  const PI_AGENT_DIR = process.env.PI_AGENT_DIR || path.join(homedir(), '.pi', 'agent');
12
13
 
@@ -240,22 +241,14 @@ export function createInstallPiCommand(): Command {
240
241
  console.log(t.success(` ${sym.ok} ${name}`));
241
242
  }
242
243
 
243
- await fs.copy(path.join(piConfigDir, 'extensions'), path.join(PI_AGENT_DIR, 'extensions'), { overwrite: true });
244
- console.log(t.success(` ${sym.ok} extensions/`));
245
-
246
- // Register each extension with pi install -l
247
- const extDirs = await listExtensionDirs(path.join(PI_AGENT_DIR, 'extensions'));
248
- if (extDirs.length > 0) {
249
- console.log(kleur.dim(`\n Registering ${extDirs.length} extensions...`));
250
- for (const extName of extDirs) {
251
- const extPath = path.join(PI_AGENT_DIR, 'extensions', extName);
252
- const r = spawnSync('pi', ['install', '-l', extPath], { stdio: 'pipe', encoding: 'utf8' });
253
- if (r.status === 0) {
254
- console.log(t.success(` ${sym.ok} ${extName} registered`));
255
- } else {
256
- console.log(kleur.yellow(` ⚠ ${extName} — registration failed`));
257
- }
258
- }
244
+ const managedPackages = await syncManagedPiExtensions({
245
+ sourceDir: path.join(piConfigDir, 'extensions'),
246
+ targetDir: path.join(PI_AGENT_DIR, 'extensions'),
247
+ dryRun: false,
248
+ log: (message) => console.log(kleur.dim(` ${message}`)),
249
+ });
250
+ if (managedPackages > 0) {
251
+ console.log(t.success(` ${sym.ok} extensions/ (${managedPackages} packages)`));
259
252
  }
260
253
 
261
254
  console.log(t.bold('\n npm Packages\n'));
@@ -3,6 +3,7 @@ import kleur from 'kleur';
3
3
  import prompts from 'prompts';
4
4
  import { Listr } from 'listr2';
5
5
  import fs from 'fs-extra';
6
+ import os from 'os';
6
7
  import { getContext } from '../core/context.js';
7
8
  import { calculateDiff, PruneModeReadError } from '../core/diff.js';
8
9
  import { executeSync, syncMcpForTargets } from '../core/sync-executor.js';
@@ -119,6 +120,15 @@ function isGitnexusInstalled(): boolean {
119
120
  }
120
121
  }
121
122
 
123
+ function isDeepwikiInstalled(): boolean {
124
+ try {
125
+ execSync('deepwiki --version', { stdio: 'ignore' });
126
+ return true;
127
+ } catch {
128
+ return false;
129
+ }
130
+ }
131
+
122
132
  async function needsSettingsSync(repoRoot: string, target: string): Promise<boolean> {
123
133
  const normalizedTarget = target.replace(/\\/g, '/').toLowerCase();
124
134
  if (normalizedTarget.includes('.agents/skills')) return false;
@@ -191,17 +201,121 @@ export async function installOfficialClaudePlugins(dryRun: boolean): Promise<voi
191
201
  console.log(t.success(` ✓ Official plugins ready (${installedCount} installed, ${alreadyInstalledCount} already present)\n`));
192
202
  }
193
203
 
204
+ async function cleanStalePrePluginFiles(repoRoot: string, dryRun: boolean): Promise<void> {
205
+ const home = os.homedir();
206
+ const staleHooksDir = path.join(home, '.claude', 'hooks');
207
+ const staleSkillsDir = path.join(home, '.claude', 'skills');
208
+ const settingsPath = path.join(home, '.claude', 'settings.json');
209
+
210
+ const removed: string[] = [];
211
+
212
+ // Remove stale hook files managed by xtrm-tools (those matching repo hooks/)
213
+ const repoHooksDir = path.join(repoRoot, 'hooks');
214
+ if (await fs.pathExists(repoHooksDir) && await fs.pathExists(staleHooksDir)) {
215
+ const repoHookNames = (await fs.readdir(repoHooksDir)).filter(n => n !== 'README.md' && n !== 'hooks.json');
216
+ for (const name of repoHookNames) {
217
+ const staleFile = path.join(staleHooksDir, name);
218
+ if (await fs.pathExists(staleFile)) {
219
+ if (dryRun) {
220
+ console.log(t.accent(` [DRY RUN] Would remove stale hook: ~/.claude/hooks/${name}`));
221
+ } else {
222
+ await fs.remove(staleFile);
223
+ console.log(t.muted(` ✗ Removed stale hook: ~/.claude/hooks/${name}`));
224
+ }
225
+ removed.push(`hooks/${name}`);
226
+ }
227
+ }
228
+ }
229
+
230
+ // Remove stale skill directories managed by xtrm-tools (those matching repo skills/)
231
+ const repoSkillsDir = path.join(repoRoot, 'skills');
232
+ if (await fs.pathExists(repoSkillsDir) && await fs.pathExists(staleSkillsDir)) {
233
+ const repoSkillNames = (await fs.readdir(repoSkillsDir)).filter(n => !n.startsWith('.'));
234
+ for (const name of repoSkillNames) {
235
+ const staleDir = path.join(staleSkillsDir, name);
236
+ if (await fs.pathExists(staleDir)) {
237
+ if (dryRun) {
238
+ console.log(t.accent(` [DRY RUN] Would remove stale skill: ~/.claude/skills/${name}`));
239
+ } else {
240
+ await fs.remove(staleDir);
241
+ console.log(t.muted(` ✗ Removed stale skill: ~/.claude/skills/${name}`));
242
+ }
243
+ removed.push(`skills/${name}`);
244
+ }
245
+ }
246
+ }
247
+
248
+ // Clean stale settings.json hook entries pointing to ~/.claude/hooks/ (not ${CLAUDE_PLUGIN_ROOT})
249
+ if (await fs.pathExists(settingsPath)) {
250
+ let settings: any;
251
+ try {
252
+ settings = await fs.readJson(settingsPath);
253
+ } catch {
254
+ settings = null;
255
+ }
256
+ if (settings && settings.hooks && typeof settings.hooks === 'object') {
257
+ let settingsModified = false;
258
+ for (const [event, matchers] of Object.entries(settings.hooks)) {
259
+ if (!Array.isArray(matchers)) continue;
260
+ const cleanedMatchers = (matchers as any[]).filter((matcher: any) => {
261
+ const hooks = Array.isArray(matcher?.hooks) ? matcher.hooks : [];
262
+ const staleHooks = hooks.filter((h: any) => {
263
+ const cmd: string = typeof h?.command === 'string' ? h.command : '';
264
+ return cmd.includes('/.claude/hooks/') && !cmd.includes('${CLAUDE_PLUGIN_ROOT}');
265
+ });
266
+ if (staleHooks.length > 0) {
267
+ for (const h of staleHooks) {
268
+ const msg = `settings.json [${event}] hook: ${h.command}`;
269
+ if (dryRun) {
270
+ console.log(t.accent(` [DRY RUN] Would remove stale ${msg}`));
271
+ } else {
272
+ console.log(t.muted(` ✗ Removed stale ${msg}`));
273
+ }
274
+ removed.push(msg);
275
+ }
276
+ // Remove stale hooks from matcher; drop matcher if empty
277
+ const remainingHooks = hooks.filter((h: any) => {
278
+ const cmd: string = typeof h?.command === 'string' ? h.command : '';
279
+ return !(cmd.includes('/.claude/hooks/') && !cmd.includes('${CLAUDE_PLUGIN_ROOT}'));
280
+ });
281
+ if (remainingHooks.length === 0) return false;
282
+ matcher.hooks = remainingHooks;
283
+ settingsModified = true;
284
+ return true;
285
+ }
286
+ return true;
287
+ });
288
+ if (cleanedMatchers.length !== matchers.length) {
289
+ settings.hooks[event] = cleanedMatchers;
290
+ settingsModified = true;
291
+ }
292
+ }
293
+ if (settingsModified && !dryRun) {
294
+ await fs.writeJson(settingsPath, settings, { spaces: 2 });
295
+ }
296
+ }
297
+ }
298
+
299
+ if (removed.length === 0) {
300
+ console.log(t.success(' ✓ No stale pre-plugin files found'));
301
+ }
302
+ }
303
+
194
304
  export async function installPlugin(repoRoot: string, dryRun: boolean): Promise<void> {
195
305
  console.log(t.bold('\n ⚙ xtrm-tools (Claude Code plugin)'));
196
306
 
197
307
  if (dryRun) {
198
308
  console.log(t.accent(' [DRY RUN] Would register xtrm-tools marketplace and install plugin\n'));
309
+ await cleanStalePrePluginFiles(repoRoot, true);
199
310
  await installOfficialClaudePlugins(true);
200
311
  return;
201
312
  }
202
313
 
203
- // Register marketplace (re-register to pick up any path changes)
204
- spawnSync('claude', ['plugin', 'marketplace', 'add', repoRoot, '--scope', 'user'], { stdio: 'pipe' });
314
+ // Register marketplace using the xtrm-tools package root.
315
+ // __dirname in the built CJS bundle is cli/dist/, so ../../ is the package root.
316
+ // Do NOT use repoRoot here — that is the user's project, not the xtrm-tools package.
317
+ const xtrmPkgRoot = path.resolve(__dirname, '..', '..');
318
+ spawnSync('claude', ['plugin', 'marketplace', 'add', xtrmPkgRoot, '--scope', 'user'], { stdio: 'pipe' });
205
319
 
206
320
  // Always uninstall + reinstall to refresh the cached copy from the live repo
207
321
  const listResult = spawnSync('claude', ['plugin', 'list'], { encoding: 'utf8', stdio: 'pipe' });
@@ -211,6 +325,10 @@ export async function installPlugin(repoRoot: string, dryRun: boolean): Promise<
211
325
  spawnSync('claude', ['plugin', 'install', 'xtrm-tools@xtrm-tools', '--scope', 'user'], { stdio: 'inherit' });
212
326
 
213
327
  console.log(t.success(' ✓ xtrm-tools plugin installed'));
328
+ console.log(t.warning(' ↻ Restart Claude Code for the new plugin hooks to take effect'));
329
+
330
+ // Clean up stale pre-plugin files from ~/.claude/hooks/ and ~/.claude/skills/
331
+ await cleanStalePrePluginFiles(repoRoot, dryRun);
214
332
 
215
333
  await installOfficialClaudePlugins(false);
216
334
  }
@@ -312,6 +430,36 @@ export function createInstallCommand(): Command {
312
430
  }
313
431
  }
314
432
 
433
+ // deepwiki CLI
434
+ if (!backport) {
435
+ console.log(t.bold('\n ⚙ deepwiki (AI-powered repo documentation)'));
436
+
437
+ const deepwikiOk = isDeepwikiInstalled();
438
+
439
+ if (deepwikiOk) {
440
+ console.log(t.success(' ✓ deepwiki already installed\n'));
441
+ } else {
442
+ let doInstall = effectiveYes;
443
+ if (!effectiveYes) {
444
+ const { install } = await prompts({
445
+ type: 'confirm',
446
+ name: 'install',
447
+ message: 'Install @seflless/deepwiki?',
448
+ initial: true,
449
+ });
450
+ doInstall = install;
451
+ }
452
+
453
+ if (doInstall) {
454
+ console.log(t.muted('\n Installing @seflless/deepwiki...'));
455
+ spawnSync('npm', ['install', '-g', '@seflless/deepwiki'], { stdio: 'inherit' });
456
+ console.log(t.success(' ✓ deepwiki installed\n'));
457
+ } else {
458
+ console.log(t.muted(' ℹ Skipped.\n'));
459
+ }
460
+ }
461
+ }
462
+
315
463
  // Claude Code: install via plugin (no hook/settings wiring needed)
316
464
  if (!backport) {
317
465
  for (const _claudeTarget of claudeTargets) {