xtrm-cli 2.1.4
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/.gemini/settings.json +39 -0
- package/dist/index.cjs +55937 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2 -0
- package/index.js +151 -0
- package/lib/atomic-config.js +236 -0
- package/lib/config-adapter.js +231 -0
- package/lib/config-injector.js +80 -0
- package/lib/context.js +73 -0
- package/lib/diff.js +142 -0
- package/lib/env-manager.js +160 -0
- package/lib/sync-mcp-cli.js +345 -0
- package/lib/sync.js +227 -0
- package/lib/transform-gemini.js +119 -0
- package/package.json +43 -0
- package/src/adapters/base.ts +29 -0
- package/src/adapters/claude.ts +38 -0
- package/src/adapters/registry.ts +21 -0
- package/src/commands/help.ts +171 -0
- package/src/commands/install-project.ts +566 -0
- package/src/commands/install-service-skills.ts +251 -0
- package/src/commands/install.ts +534 -0
- package/src/commands/reset.ts +12 -0
- package/src/commands/status.ts +170 -0
- package/src/core/context.ts +141 -0
- package/src/core/diff.ts +143 -0
- package/src/core/interactive-plan.ts +165 -0
- package/src/core/manifest.ts +26 -0
- package/src/core/preflight.ts +142 -0
- package/src/core/rollback.ts +32 -0
- package/src/core/sync-executor.ts +399 -0
- package/src/index.ts +69 -0
- package/src/types/config.ts +51 -0
- package/src/types/models.ts +52 -0
- package/src/utils/atomic-config.ts +222 -0
- package/src/utils/banner.ts +194 -0
- package/src/utils/config-adapter.ts +90 -0
- package/src/utils/config-injector.ts +81 -0
- package/src/utils/env-manager.ts +193 -0
- package/src/utils/hash.ts +42 -0
- package/src/utils/repo-root.ts +39 -0
- package/src/utils/sync-mcp-cli.ts +467 -0
- package/src/utils/theme.ts +37 -0
- package/test/context.test.ts +33 -0
- package/test/hooks.test.ts +277 -0
- package/test/install-project.test.ts +235 -0
- package/test/install-service-skills.test.ts +111 -0
- package/tsconfig.json +22 -0
- package/tsup.config.ts +17 -0
- package/vitest.config.ts +9 -0
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "xtrm-cli",
|
|
3
|
+
"version": "2.1.4",
|
|
4
|
+
"description": "Claude Code tools installer (skills, hooks, MCP servers)",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"xtrm": "./dist/index.cjs"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsup",
|
|
12
|
+
"dev": "tsx src/index.ts",
|
|
13
|
+
"typecheck": "tsc --noEmit",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"start": "node dist/index.cjs"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"boxen": "^8.0.1",
|
|
19
|
+
"cli-table3": "^0.6.5",
|
|
20
|
+
"commander": "^14.0.3",
|
|
21
|
+
"comment-json": "^4.2.3",
|
|
22
|
+
"conf": "^12.0.0",
|
|
23
|
+
"dotenv": "^16.4.5",
|
|
24
|
+
"fs-extra": "^11.2.0",
|
|
25
|
+
"kleur": "^4.1.5",
|
|
26
|
+
"listr2": "^10.1.1",
|
|
27
|
+
"minimist": "^1.2.8",
|
|
28
|
+
"ora": "^9.3.0",
|
|
29
|
+
"project": "^0.1.6",
|
|
30
|
+
"prompts": "^2.4.2",
|
|
31
|
+
"tdd-guard": "^1.1.0",
|
|
32
|
+
"zod": "^4.3.6"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/fs-extra": "^11.0.4",
|
|
36
|
+
"@types/node": "^25.3.0",
|
|
37
|
+
"tdd-guard-vitest": "^0.1.6",
|
|
38
|
+
"tsup": "^8.5.1",
|
|
39
|
+
"tsx": "^4.21.0",
|
|
40
|
+
"typescript": "^5.9.3",
|
|
41
|
+
"vitest": "^4.0.18"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Skill, MCPServer, Hook, Command } from '../types/models.js';
|
|
2
|
+
|
|
3
|
+
export interface AdapterCapabilities {
|
|
4
|
+
skills: boolean;
|
|
5
|
+
hooks: boolean;
|
|
6
|
+
mcp: boolean;
|
|
7
|
+
commands: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface AdapterConfig {
|
|
11
|
+
tool: string;
|
|
12
|
+
baseDir: string;
|
|
13
|
+
displayName: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export abstract class ToolAdapter {
|
|
17
|
+
abstract readonly toolName: string;
|
|
18
|
+
abstract readonly displayName: string;
|
|
19
|
+
abstract readonly config: AdapterConfig;
|
|
20
|
+
|
|
21
|
+
// Capabilities
|
|
22
|
+
abstract getCapabilities(): AdapterCapabilities;
|
|
23
|
+
|
|
24
|
+
// Paths
|
|
25
|
+
abstract getConfigDir(): string;
|
|
26
|
+
abstract getSkillsDir(): string;
|
|
27
|
+
abstract getHooksDir(): string;
|
|
28
|
+
abstract getCommandsDir(): string;
|
|
29
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { ToolAdapter, type AdapterCapabilities } from './base.js';
|
|
3
|
+
|
|
4
|
+
export class ClaudeAdapter extends ToolAdapter {
|
|
5
|
+
readonly toolName = 'claude-code';
|
|
6
|
+
readonly displayName = 'Claude Code';
|
|
7
|
+
readonly config: { tool: string; baseDir: string; displayName: string };
|
|
8
|
+
|
|
9
|
+
constructor(baseDir: string) {
|
|
10
|
+
super();
|
|
11
|
+
this.config = { tool: this.toolName, baseDir, displayName: this.displayName };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getConfigDir(): string {
|
|
15
|
+
return this.config.baseDir;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getSkillsDir(): string {
|
|
19
|
+
return join(this.config.baseDir, 'skills');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
getHooksDir(): string {
|
|
23
|
+
return join(this.config.baseDir, 'hooks');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getCommandsDir(): string {
|
|
27
|
+
return join(this.config.baseDir, 'commands'); // Though Claude doesn't strictly use bare commands like Gemini
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getCapabilities(): AdapterCapabilities {
|
|
31
|
+
return {
|
|
32
|
+
skills: true,
|
|
33
|
+
hooks: true,
|
|
34
|
+
mcp: true,
|
|
35
|
+
commands: false, // Claude uses Skills instead of Slash Commands natively
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ToolAdapter } from './base.js';
|
|
2
|
+
import { ClaudeAdapter } from './claude.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Adapter registry for Claude Code only.
|
|
6
|
+
*
|
|
7
|
+
* ARCHITECTURAL DECISION (v2.0.0): xtrm-tools now supports Claude Code exclusively.
|
|
8
|
+
* Gemini and Qwen adapters were removed due to fragile, undocumented hook ecosystems.
|
|
9
|
+
* See PROJECT-SKILLS-ARCHITECTURE.md Section 3.1 for details.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export function detectAdapter(systemRoot: string): ToolAdapter | null {
|
|
13
|
+
// Windows compatibility: Normalize backslashes before matching paths
|
|
14
|
+
const normalized = systemRoot.replace(/\\/g, '/').toLowerCase();
|
|
15
|
+
|
|
16
|
+
if (normalized.includes('.claude') || normalized.includes('/claude')) {
|
|
17
|
+
return new ClaudeAdapter(systemRoot);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import kleur from 'kleur';
|
|
3
|
+
|
|
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 }> = [
|
|
10
|
+
{ file: 'main-guard.mjs', event: 'PreToolUse', desc: 'Blocks direct edits on protected branches' },
|
|
11
|
+
{ file: 'skill-suggestion.py', event: 'UserPromptSubmit', desc: 'Suggests relevant skills based on user prompt' },
|
|
12
|
+
{ file: 'serena-workflow-reminder.py', event: 'SessionStart', desc: 'Injects Serena semantic editing workflow reminder' },
|
|
13
|
+
{ file: 'type-safety-enforcement.py', event: 'PreToolUse', desc: 'Prevents risky Bash and enforces safe edit patterns' },
|
|
14
|
+
{ file: 'gitnexus/gitnexus-hook.cjs', event: 'PreToolUse', desc: 'Adds GitNexus context for Grep/Glob/Bash searches' },
|
|
15
|
+
{ file: 'skill-discovery.py', event: 'UserPromptSubmit', desc: 'Discovers available skills for user requests' },
|
|
16
|
+
{ file: 'agent_context.py', event: 'Support module', desc: 'Shared hook I/O helper used by Python hook scripts' },
|
|
17
|
+
{ file: 'beads-edit-gate.mjs', event: 'PreToolUse', desc: 'Blocks file edits if no beads issue is claimed', beads: true },
|
|
18
|
+
{ file: 'beads-commit-gate.mjs', event: 'PreToolUse', desc: 'Blocks commits when no beads issue is in progress', beads: true },
|
|
19
|
+
{ file: 'beads-stop-gate.mjs', event: 'Stop', desc: 'Blocks session stop with an unclosed beads claim', beads: true },
|
|
20
|
+
{ file: 'beads-close-memory-prompt.mjs', event: 'PostToolUse', desc: 'Prompts memory save when closing a beads issue', beads: true },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
async function readSkillsFromDir(dir: string): Promise<Array<{ name: string; desc: string }>> {
|
|
24
|
+
if (!(await fs.pathExists(dir))) return [];
|
|
25
|
+
const entries = await fs.readdir(dir);
|
|
26
|
+
const skills: Array<{ name: string; desc: string }> = [];
|
|
27
|
+
for (const name of entries.sort()) {
|
|
28
|
+
const skillMd = path.join(dir, name, 'SKILL.md');
|
|
29
|
+
if (!(await fs.pathExists(skillMd))) continue;
|
|
30
|
+
const content = await fs.readFile(skillMd, 'utf8');
|
|
31
|
+
const m = content.match(/^description:\s*(.+)$/m);
|
|
32
|
+
skills.push({ name, desc: m ? m[1].replace(/^["']|["']$/g, '').trim() : '' });
|
|
33
|
+
}
|
|
34
|
+
return skills;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function readProjectSkillsFromDir(dir: string): Promise<Array<{ name: string; desc: string }>> {
|
|
38
|
+
if (!(await fs.pathExists(dir))) return [];
|
|
39
|
+
const entries = await fs.readdir(dir);
|
|
40
|
+
const skills: Array<{ name: string; desc: string }> = [];
|
|
41
|
+
for (const name of entries.sort()) {
|
|
42
|
+
const readme = path.join(dir, name, 'README.md');
|
|
43
|
+
if (!(await fs.pathExists(readme))) continue;
|
|
44
|
+
const content = await fs.readFile(readme, 'utf8');
|
|
45
|
+
const descLine = content.split('\n').find(line => {
|
|
46
|
+
const trimmed = line.trim();
|
|
47
|
+
return Boolean(trimmed) && !trimmed.startsWith('#') && !trimmed.startsWith('[') && !trimmed.startsWith('<');
|
|
48
|
+
}) || '';
|
|
49
|
+
skills.push({ name, desc: descLine.replace(/[*_`]/g, '').trim() });
|
|
50
|
+
}
|
|
51
|
+
return skills;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolvePkgRootFallback(): string | null {
|
|
55
|
+
const candidates = [
|
|
56
|
+
path.resolve(__dirname, '../..'),
|
|
57
|
+
path.resolve(__dirname, '../../..'),
|
|
58
|
+
];
|
|
59
|
+
const match = candidates.find(candidate =>
|
|
60
|
+
fs.existsSync(path.join(candidate, 'skills')) || fs.existsSync(path.join(candidate, 'project-skills'))
|
|
61
|
+
);
|
|
62
|
+
return match || null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function col(s: string, width: number): string {
|
|
66
|
+
return s.length >= width ? s.slice(0, width - 1) + '\u2026' : s.padEnd(width);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createHelpCommand(): Command {
|
|
70
|
+
return new Command('help')
|
|
71
|
+
.description('Show help information and component catalogue')
|
|
72
|
+
.action(async () => {
|
|
73
|
+
let repoRoot: string;
|
|
74
|
+
try { repoRoot = await findRepoRoot(); } catch { repoRoot = ''; }
|
|
75
|
+
const pkgRoot = resolvePkgRootFallback();
|
|
76
|
+
|
|
77
|
+
const skillsRoot = repoRoot || pkgRoot || '';
|
|
78
|
+
const projectSkillsRoot = repoRoot || pkgRoot || '';
|
|
79
|
+
const skills = skillsRoot ? await readSkillsFromDir(path.join(skillsRoot, 'skills')) : [];
|
|
80
|
+
const projectSkills = projectSkillsRoot ? await readProjectSkillsFromDir(path.join(projectSkillsRoot, 'project-skills')) : [];
|
|
81
|
+
|
|
82
|
+
const W = 80;
|
|
83
|
+
const hr = kleur.dim('-'.repeat(W));
|
|
84
|
+
const section = (title: string) => `\n${kleur.bold().cyan(title)}\n${hr}`;
|
|
85
|
+
|
|
86
|
+
const installSection = [
|
|
87
|
+
section('INSTALL COMMANDS'),
|
|
88
|
+
'',
|
|
89
|
+
` ${kleur.bold('xtrm install all')}`,
|
|
90
|
+
` ${kleur.dim('Global install: skills + all hooks (including beads gates) + MCP servers.')}`,
|
|
91
|
+
` ${kleur.dim('Checks for beads+dolt and prompts to install if missing.')}`,
|
|
92
|
+
'',
|
|
93
|
+
` ${kleur.bold('xtrm install basic')}`,
|
|
94
|
+
` ${kleur.dim('Global install: skills + general hooks + MCP servers.')}`,
|
|
95
|
+
` ${kleur.dim('No beads dependency -- safe to run with zero external deps.')}`,
|
|
96
|
+
'',
|
|
97
|
+
` ${kleur.bold('xtrm install project')} ${kleur.dim('<tool-name | all>')}`,
|
|
98
|
+
` ${kleur.dim('Project-scoped install into .claude/ of current git root.')}`,
|
|
99
|
+
` ${kleur.dim('Run xtrm install project list to see available project skills.')}`,
|
|
100
|
+
'',
|
|
101
|
+
` ${kleur.dim('Default target directories:')}`,
|
|
102
|
+
` ${kleur.dim('~/.claude/hooks (global hook scripts)')}`,
|
|
103
|
+
` ${kleur.dim('~/.claude/skills (global Claude skills)')}`,
|
|
104
|
+
` ${kleur.dim('~/.agents/skills (agents skills cache mirror)')}`,
|
|
105
|
+
'',
|
|
106
|
+
` ${kleur.dim('Flags (all profiles): --dry-run --yes / -y --no-mcp --force --prune --backport')}`,
|
|
107
|
+
].join('\n');
|
|
108
|
+
|
|
109
|
+
const general = HOOK_CATALOG.filter(h => !h.beads);
|
|
110
|
+
const beads = HOOK_CATALOG.filter(h => h.beads);
|
|
111
|
+
const hookRows = (hooks: typeof HOOK_CATALOG) =>
|
|
112
|
+
hooks.map(h =>
|
|
113
|
+
` ${kleur.white(col(h.file, 34))}${kleur.yellow(col(h.event, 20))}${kleur.dim(h.desc)}`
|
|
114
|
+
).join('\n');
|
|
115
|
+
|
|
116
|
+
const hooksSection = [
|
|
117
|
+
section('GLOBAL HOOKS'),
|
|
118
|
+
'',
|
|
119
|
+
kleur.dim(' ' + col('File', 34) + col('Event', 20) + 'Description'),
|
|
120
|
+
'',
|
|
121
|
+
hookRows(general),
|
|
122
|
+
'',
|
|
123
|
+
` ${kleur.dim('beads gate hooks (xtrm install all -- require beads+dolt):')}`,
|
|
124
|
+
hookRows(beads),
|
|
125
|
+
].join('\n');
|
|
126
|
+
|
|
127
|
+
const skillRows = skills.map(s => {
|
|
128
|
+
const desc = s.desc.length > 46 ? s.desc.slice(0, 45) + '\u2026' : s.desc;
|
|
129
|
+
return ` ${kleur.white(col(s.name, 30))}${kleur.dim(desc)}`;
|
|
130
|
+
}).join('\n');
|
|
131
|
+
|
|
132
|
+
const skillsSection = [
|
|
133
|
+
section(`SKILLS ${kleur.dim('(' + skills.length + ' available)')}`),
|
|
134
|
+
'',
|
|
135
|
+
skills.length ? skillRows : kleur.dim(' (none found -- run from repo root to see skills)'),
|
|
136
|
+
].join('\n');
|
|
137
|
+
|
|
138
|
+
const psRows = projectSkills.map(s =>
|
|
139
|
+
` ${kleur.white(col(s.name, 30))}${kleur.dim(s.desc)}`
|
|
140
|
+
).join('\n');
|
|
141
|
+
|
|
142
|
+
const psSection = [
|
|
143
|
+
section('PROJECT SKILLS + HOOKS'),
|
|
144
|
+
'',
|
|
145
|
+
projectSkills.length ? psRows : kleur.dim(' (none found in package)'),
|
|
146
|
+
'',
|
|
147
|
+
` ${kleur.dim('Install: xtrm install project <name> | xtrm install project list')}`,
|
|
148
|
+
` ${kleur.dim('Each project skill can install .claude/skills plus project hooks/settings.')}`,
|
|
149
|
+
].join('\n');
|
|
150
|
+
|
|
151
|
+
const otherSection = [
|
|
152
|
+
section('OTHER COMMANDS'),
|
|
153
|
+
'',
|
|
154
|
+
` ${kleur.bold('xtrm status')} ${kleur.dim('Show pending changes without applying them')}`,
|
|
155
|
+
` ${kleur.bold('xtrm reset')} ${kleur.dim('Clear saved preferences and start fresh')}`,
|
|
156
|
+
` ${kleur.bold('xtrm help')} ${kleur.dim('Show this overview')}`,
|
|
157
|
+
].join('\n');
|
|
158
|
+
|
|
159
|
+
const resourcesSection = [
|
|
160
|
+
section('RESOURCES'),
|
|
161
|
+
'',
|
|
162
|
+
` Repository https://github.com/Jaggerxtrm/xtrm-tools`,
|
|
163
|
+
` Issues https://github.com/Jaggerxtrm/xtrm-tools/issues`,
|
|
164
|
+
'',
|
|
165
|
+
` ${kleur.dim("Run 'xtrm <command> --help' for command-specific options.")}`,
|
|
166
|
+
'',
|
|
167
|
+
].join('\n');
|
|
168
|
+
|
|
169
|
+
console.log([installSection, hooksSection, skillsSection, psSection, otherSection, resourcesSection].join('\n'));
|
|
170
|
+
});
|
|
171
|
+
}
|