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.
- package/.pi/structured-returns/0e4a7405-1ac3-4ae1-8dbc-d31507b2e2e4.combined.log +17 -0
- package/.pi/structured-returns/0e4a7405-1ac3-4ae1-8dbc-d31507b2e2e4.stderr.log +0 -0
- package/.pi/structured-returns/0e4a7405-1ac3-4ae1-8dbc-d31507b2e2e4.stdout.log +17 -0
- package/dist/index.cjs +969 -1059
- package/dist/index.cjs.map +1 -1
- package/package.json +1 -1
- package/src/commands/clean.ts +7 -6
- package/src/commands/debug.ts +255 -0
- package/src/commands/docs.ts +180 -0
- package/src/commands/help.ts +92 -171
- package/src/commands/init.ts +9 -32
- package/src/commands/install-pi.ts +9 -16
- package/src/commands/install.ts +150 -2
- package/src/commands/pi-install.ts +10 -44
- package/src/core/context.ts +4 -52
- package/src/core/diff.ts +3 -16
- package/src/core/preflight.ts +0 -1
- package/src/index.ts +7 -4
- package/src/types/config.ts +0 -2
- package/src/utils/config-injector.ts +3 -3
- package/src/utils/pi-extensions.ts +41 -0
- package/src/utils/worktree-session.ts +86 -50
- package/test/extensions/beads-claim-lifecycle.test.ts +93 -0
- package/test/extensions/beads-parity.test.ts +94 -0
- package/test/extensions/extension-harness.ts +5 -5
- package/test/extensions/quality-gates-parity.test.ts +89 -0
- package/test/extensions/session-flow.test.ts +91 -0
- package/test/extensions/xtrm-loader.test.ts +38 -20
- package/test/install-pi.test.ts +22 -11
- package/test/pi-extensions.test.ts +50 -0
- package/test/session-launcher.test.ts +28 -38
- package/extensions/beads.ts +0 -109
- package/extensions/core/adapter.ts +0 -45
- package/extensions/core/lib.ts +0 -3
- package/extensions/core/logger.ts +0 -45
- package/extensions/core/runner.ts +0 -71
- package/extensions/custom-footer.ts +0 -160
- package/extensions/main-guard-post-push.ts +0 -44
- package/extensions/main-guard.ts +0 -126
- package/extensions/minimal-mode.ts +0 -201
- package/extensions/quality-gates.ts +0 -67
- package/extensions/service-skills.ts +0 -150
- package/extensions/xtrm-loader.ts +0 -89
- package/hooks/gitnexus-impact-reminder.py +0 -13
- package/src/commands/finish.ts +0 -25
- package/src/core/session-state.ts +0 -139
- package/src/core/xtrm-finish.ts +0 -267
- package/src/tests/session-flow-parity.test.ts +0 -118
- package/src/tests/session-state.test.ts +0 -124
- package/src/tests/xtrm-finish.test.ts +0 -148
package/src/commands/help.ts
CHANGED
|
@@ -1,180 +1,101 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import kleur from 'kleur';
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
9
|
+
.description('Show rich CLI help in a plain text format')
|
|
76
10
|
.action(async () => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
'',
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
'',
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
'',
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
'',
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
'',
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
'',
|
|
124
|
-
|
|
125
|
-
'',
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
'',
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
'',
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
]
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
'',
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
}
|
package/src/commands/init.ts
CHANGED
|
@@ -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(' -
|
|
733
|
-
kleur.dim(' - Detects
|
|
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 (
|
|
738
|
-
kleur.dim(' -
|
|
739
|
-
kleur.dim(' -
|
|
740
|
-
kleur.dim(' -
|
|
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
|
|
753
|
-
kleur.dim(' -
|
|
754
|
-
kleur.dim(' -
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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'));
|
package/src/commands/install.ts
CHANGED
|
@@ -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
|
|
204
|
-
|
|
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) {
|