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
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import kleur from 'kleur';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import { spawnSync } from 'child_process';
|
|
6
|
+
|
|
7
|
+
declare const __dirname: string;
|
|
8
|
+
function resolvePkgRoot(): string {
|
|
9
|
+
const candidates = [
|
|
10
|
+
path.resolve(__dirname, '../..'),
|
|
11
|
+
path.resolve(__dirname, '../../..'),
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const match = candidates.find(candidate => fs.existsSync(path.join(candidate, 'project-skills')));
|
|
15
|
+
if (!match) {
|
|
16
|
+
throw new Error('Unable to locate project-skills directory from CLI runtime.');
|
|
17
|
+
}
|
|
18
|
+
return match;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const PKG_ROOT = resolvePkgRoot();
|
|
22
|
+
const SKILLS_SRC = path.join(PKG_ROOT, 'project-skills', 'service-skills-set', '.claude');
|
|
23
|
+
|
|
24
|
+
const TRINITY = [
|
|
25
|
+
'creating-service-skills',
|
|
26
|
+
'using-service-skills',
|
|
27
|
+
'updating-service-skills',
|
|
28
|
+
'scoping-service-skills',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const SETTINGS_HOOKS: Record<string, unknown[]> = {
|
|
32
|
+
SessionStart: [
|
|
33
|
+
{
|
|
34
|
+
hooks: [{
|
|
35
|
+
type: 'command',
|
|
36
|
+
command: 'python3 "$CLAUDE_PROJECT_DIR/.claude/skills/using-service-skills/scripts/cataloger.py"',
|
|
37
|
+
}],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
PreToolUse: [
|
|
41
|
+
{
|
|
42
|
+
matcher: 'Read|Write|Edit|Glob|Grep|Bash|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol',
|
|
43
|
+
hooks: [{
|
|
44
|
+
type: 'command',
|
|
45
|
+
command: 'python3 "$CLAUDE_PROJECT_DIR/.claude/skills/using-service-skills/scripts/skill_activator.py"',
|
|
46
|
+
}],
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
PostToolUse: [
|
|
50
|
+
{
|
|
51
|
+
matcher: 'Write|Edit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol',
|
|
52
|
+
hooks: [{
|
|
53
|
+
type: 'command',
|
|
54
|
+
command: 'python3 "$CLAUDE_PROJECT_DIR/.claude/skills/updating-service-skills/scripts/drift_detector.py" check-hook',
|
|
55
|
+
timeout: 10,
|
|
56
|
+
}],
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const MARKER_DOC = '# [jaggers] doc-reminder';
|
|
62
|
+
const MARKER_STALENESS = '# [jaggers] skill-staleness';
|
|
63
|
+
|
|
64
|
+
// ─── Pure functions (exported for testing) ───────────────────────────────────
|
|
65
|
+
|
|
66
|
+
export function mergeSettingsHooks(existing: Record<string, unknown>): {
|
|
67
|
+
result: Record<string, unknown>;
|
|
68
|
+
added: string[];
|
|
69
|
+
skipped: string[];
|
|
70
|
+
} {
|
|
71
|
+
const result = { ...existing };
|
|
72
|
+
const hooks = (result.hooks ?? {}) as Record<string, unknown>;
|
|
73
|
+
result.hooks = hooks;
|
|
74
|
+
|
|
75
|
+
const added: string[] = [];
|
|
76
|
+
const skipped: string[] = [];
|
|
77
|
+
|
|
78
|
+
for (const [event, config] of Object.entries(SETTINGS_HOOKS)) {
|
|
79
|
+
if (event in hooks) {
|
|
80
|
+
skipped.push(event);
|
|
81
|
+
} else {
|
|
82
|
+
hooks[event] = config;
|
|
83
|
+
added.push(event);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { result, added, skipped };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function installSkills(projectRoot: string, skillsSrc: string = SKILLS_SRC): Promise<{ skill: string; status: 'installed' | 'updated' }[]> {
|
|
91
|
+
const results: { skill: string; status: 'installed' | 'updated' }[] = [];
|
|
92
|
+
for (const skill of TRINITY) {
|
|
93
|
+
const src = path.join(skillsSrc, skill);
|
|
94
|
+
const dest = path.join(projectRoot, '.claude', 'skills', skill);
|
|
95
|
+
const existed = await fs.pathExists(dest);
|
|
96
|
+
if (existed) {
|
|
97
|
+
await fs.remove(dest);
|
|
98
|
+
}
|
|
99
|
+
await fs.copy(src, dest, {
|
|
100
|
+
filter: (src: string) => !src.includes('.Zone.Identifier'),
|
|
101
|
+
});
|
|
102
|
+
results.push({ skill, status: existed ? 'updated' : 'installed' });
|
|
103
|
+
}
|
|
104
|
+
return results;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function installGitHooks(projectRoot: string, skillsSrc: string = SKILLS_SRC): Promise<{
|
|
108
|
+
hookFiles: { name: string; status: 'added' | 'already-present' }[];
|
|
109
|
+
}> {
|
|
110
|
+
// Copy git-hook scripts into target project (no back-reference to jaggers package path)
|
|
111
|
+
const gitHooksSrc = path.join(skillsSrc, 'git-hooks');
|
|
112
|
+
const gitHooksDest = path.join(projectRoot, '.claude', 'git-hooks');
|
|
113
|
+
await fs.copy(gitHooksSrc, gitHooksDest, { overwrite: true });
|
|
114
|
+
|
|
115
|
+
const docScript = path.join(projectRoot, '.claude', 'git-hooks', 'doc_reminder.py');
|
|
116
|
+
const stalenessScript = path.join(projectRoot, '.claude', 'git-hooks', 'skill_staleness.py');
|
|
117
|
+
|
|
118
|
+
const preCommit = path.join(projectRoot, '.githooks', 'pre-commit');
|
|
119
|
+
const prePush = path.join(projectRoot, '.githooks', 'pre-push');
|
|
120
|
+
|
|
121
|
+
for (const hookPath of [preCommit, prePush]) {
|
|
122
|
+
if (!await fs.pathExists(hookPath)) {
|
|
123
|
+
await fs.mkdirp(path.dirname(hookPath));
|
|
124
|
+
await fs.writeFile(hookPath, '#!/usr/bin/env bash\n', { mode: 0o755 });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const snippets: [string, string, string][] = [
|
|
129
|
+
[
|
|
130
|
+
preCommit,
|
|
131
|
+
MARKER_DOC,
|
|
132
|
+
`\n${MARKER_DOC}\nif command -v python3 &>/dev/null && [ -f "${docScript}" ]; then\n python3 "${docScript}" || true\nfi\n`,
|
|
133
|
+
],
|
|
134
|
+
[
|
|
135
|
+
prePush,
|
|
136
|
+
MARKER_STALENESS,
|
|
137
|
+
`\n${MARKER_STALENESS}\nif command -v python3 &>/dev/null && [ -f "${stalenessScript}" ]; then\n python3 "${stalenessScript}" || true\nfi\n`,
|
|
138
|
+
],
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
const hookFiles: { name: string; status: 'added' | 'already-present' }[] = [];
|
|
142
|
+
let anyAdded = false;
|
|
143
|
+
|
|
144
|
+
for (const [hookPath, marker, snippet] of snippets) {
|
|
145
|
+
const content = await fs.readFile(hookPath, 'utf8');
|
|
146
|
+
const name = path.basename(hookPath);
|
|
147
|
+
if (!content.includes(marker)) {
|
|
148
|
+
await fs.writeFile(hookPath, content + snippet);
|
|
149
|
+
hookFiles.push({ name, status: 'added' });
|
|
150
|
+
anyAdded = true;
|
|
151
|
+
} else {
|
|
152
|
+
hookFiles.push({ name, status: 'already-present' });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (anyAdded) {
|
|
157
|
+
const gitHooksDir = path.join(projectRoot, '.git', 'hooks');
|
|
158
|
+
await fs.mkdirp(gitHooksDir);
|
|
159
|
+
for (const [src, name] of [[preCommit, 'pre-commit'], [prePush, 'pre-push']] as const) {
|
|
160
|
+
if (await fs.pathExists(src)) {
|
|
161
|
+
const dest = path.join(gitHooksDir, name);
|
|
162
|
+
await fs.copy(src, dest, { overwrite: true });
|
|
163
|
+
await fs.chmod(dest, 0o755);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { hookFiles };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function installSettings(projectRoot: string): Promise<{ added: string[]; skipped: string[] }> {
|
|
172
|
+
const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
|
|
173
|
+
await fs.mkdirp(path.dirname(settingsPath));
|
|
174
|
+
|
|
175
|
+
let existing: Record<string, unknown> = {};
|
|
176
|
+
if (await fs.pathExists(settingsPath)) {
|
|
177
|
+
try {
|
|
178
|
+
existing = JSON.parse(await fs.readFile(settingsPath, 'utf8'));
|
|
179
|
+
} catch {
|
|
180
|
+
// malformed JSON — start fresh
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const { result, added, skipped } = mergeSettingsHooks(existing);
|
|
185
|
+
await fs.writeFile(settingsPath, JSON.stringify(result, null, 2) + '\n');
|
|
186
|
+
return { added, skipped };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function getProjectRoot(pkgRoot: string): string {
|
|
190
|
+
const result = spawnSync('git', ['rev-parse', '--show-toplevel'], {
|
|
191
|
+
encoding: 'utf8',
|
|
192
|
+
timeout: 5000,
|
|
193
|
+
});
|
|
194
|
+
if (result.status !== 0) {
|
|
195
|
+
throw new Error('Not inside a git repository. Run this command from your target project directory.');
|
|
196
|
+
}
|
|
197
|
+
const root = path.resolve(result.stdout.trim());
|
|
198
|
+
if (root === path.resolve(pkgRoot)) {
|
|
199
|
+
throw new Error('Run this from inside your TARGET project, not the jaggers-agent-tools repo itself.');
|
|
200
|
+
}
|
|
201
|
+
return root;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function createInstallServiceSkillsCommand(): Command {
|
|
205
|
+
return new Command('install-project-skill')
|
|
206
|
+
.description('Install the Service Skill Trinity into the current project')
|
|
207
|
+
.action(async () => {
|
|
208
|
+
let projectRoot: string;
|
|
209
|
+
try {
|
|
210
|
+
projectRoot = getProjectRoot(PKG_ROOT);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
console.error(kleur.red(`\n✗ ${(err as Error).message}\n`));
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
console.log(kleur.dim(`\n Installing into: ${projectRoot}\n`));
|
|
217
|
+
|
|
218
|
+
console.log(kleur.bold('── Skills ──────────────────────────────'));
|
|
219
|
+
const skillResults = await installSkills(projectRoot);
|
|
220
|
+
for (const { skill, status } of skillResults) {
|
|
221
|
+
const icon = status === 'installed' ? kleur.green(' ✓') : kleur.yellow(' ↺');
|
|
222
|
+
console.log(`${icon} .claude/skills/${skill}/`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
console.log(kleur.bold('\n── settings.json ───────────────────────'));
|
|
226
|
+
const { added, skipped } = await installSettings(projectRoot);
|
|
227
|
+
for (const event of added) {
|
|
228
|
+
console.log(`${kleur.green(' ✓')} added hook: ${event}`);
|
|
229
|
+
}
|
|
230
|
+
for (const event of skipped) {
|
|
231
|
+
console.log(`${kleur.yellow(' ○')} already present: ${event} (not overwritten)`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.log(kleur.bold('\n── Git hooks ───────────────────────────'));
|
|
235
|
+
const { hookFiles } = await installGitHooks(projectRoot);
|
|
236
|
+
for (const { name, status } of hookFiles) {
|
|
237
|
+
if (status === 'added') {
|
|
238
|
+
console.log(`${kleur.green(' ✓')} .githooks/${name}`);
|
|
239
|
+
} else {
|
|
240
|
+
console.log(`${kleur.yellow(' ○')} already installed: ${name}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (hookFiles.some(h => h.status === 'added')) {
|
|
244
|
+
console.log(`${kleur.green(' ✓')} activated in .git/hooks/`);
|
|
245
|
+
}
|
|
246
|
+
console.log(`${kleur.green(' ✓')} scripts → .claude/git-hooks/`);
|
|
247
|
+
|
|
248
|
+
console.log(kleur.green('\n Done.'));
|
|
249
|
+
console.log(kleur.dim(' Hooks active: SessionStart · PreToolUse · PostToolUse · pre-commit · pre-push\n'));
|
|
250
|
+
});
|
|
251
|
+
}
|