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,566 @@
|
|
|
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
|
+
import { installGitHooks as installServiceGitHooks } from './install-service-skills.js';
|
|
7
|
+
|
|
8
|
+
declare const __dirname: string;
|
|
9
|
+
function resolvePkgRoot(): string {
|
|
10
|
+
const candidates = [
|
|
11
|
+
path.resolve(__dirname, '../..'),
|
|
12
|
+
path.resolve(__dirname, '../../..'),
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const match = candidates.find(candidate => fs.existsSync(path.join(candidate, 'project-skills')));
|
|
16
|
+
if (!match) {
|
|
17
|
+
throw new Error('Unable to locate project-skills directory from CLI runtime.');
|
|
18
|
+
}
|
|
19
|
+
return match;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const PKG_ROOT = resolvePkgRoot();
|
|
23
|
+
const PROJECT_SKILLS_DIR = path.join(PKG_ROOT, 'project-skills');
|
|
24
|
+
const MCP_CORE_CONFIG_PATH = path.join(PKG_ROOT, 'config', 'mcp_servers.json');
|
|
25
|
+
const syncedProjectMcpRoots = new Set<string>();
|
|
26
|
+
|
|
27
|
+
function resolveEnvVars(value: string): string {
|
|
28
|
+
if (typeof value !== 'string') return value;
|
|
29
|
+
return value.replace(/\$\{([A-Z0-9_]+)\}/g, (_m, name) => process.env[name] || '');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function hasClaudeCli(): boolean {
|
|
33
|
+
const r = spawnSync('claude', ['--version'], { stdio: 'pipe' });
|
|
34
|
+
return r.status === 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildProjectMcpArgs(name: string, server: any): string[] | null {
|
|
38
|
+
const transport = server.type || (server.url?.includes('/sse') ? 'sse' : 'http');
|
|
39
|
+
|
|
40
|
+
if (server.command) {
|
|
41
|
+
const args = ['mcp', 'add', '-s', 'project'];
|
|
42
|
+
if (server.env && typeof server.env === 'object') {
|
|
43
|
+
for (const [k, v] of Object.entries(server.env)) {
|
|
44
|
+
args.push('-e', `${k}=${resolveEnvVars(String(v))}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
args.push(name, '--', server.command, ...((server.args || []) as string[]));
|
|
48
|
+
return args;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (server.url || server.serverUrl) {
|
|
52
|
+
const url = server.url || server.serverUrl;
|
|
53
|
+
const args = ['mcp', 'add', '-s', 'project', '--transport', transport, name, url];
|
|
54
|
+
if (server.headers && typeof server.headers === 'object') {
|
|
55
|
+
for (const [k, v] of Object.entries(server.headers)) {
|
|
56
|
+
args.push('--header', `${k}: ${resolveEnvVars(String(v))}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return args;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function syncProjectMcpServers(projectRoot: string): Promise<void> {
|
|
66
|
+
if (syncedProjectMcpRoots.has(projectRoot)) return;
|
|
67
|
+
syncedProjectMcpRoots.add(projectRoot);
|
|
68
|
+
|
|
69
|
+
if (!await fs.pathExists(MCP_CORE_CONFIG_PATH)) return;
|
|
70
|
+
|
|
71
|
+
console.log(kleur.bold('\n── Installing MCP (project scope) ─────────'));
|
|
72
|
+
|
|
73
|
+
if (!hasClaudeCli()) {
|
|
74
|
+
console.log(kleur.yellow(' ⚠ Claude CLI not found; skipping project-scope MCP registration.'));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const mcpConfig = await fs.readJson(MCP_CORE_CONFIG_PATH);
|
|
79
|
+
const servers = Object.entries(mcpConfig?.mcpServers ?? {}) as Array<[string, any]>;
|
|
80
|
+
if (servers.length === 0) {
|
|
81
|
+
console.log(kleur.dim(' ℹ No core MCP servers configured.'));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let added = 0;
|
|
86
|
+
let existing = 0;
|
|
87
|
+
let failed = 0;
|
|
88
|
+
|
|
89
|
+
for (const [name, server] of servers) {
|
|
90
|
+
const args = buildProjectMcpArgs(name, server);
|
|
91
|
+
if (!args) continue;
|
|
92
|
+
|
|
93
|
+
const r = spawnSync('claude', args, {
|
|
94
|
+
cwd: projectRoot,
|
|
95
|
+
encoding: 'utf8',
|
|
96
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (r.status === 0) {
|
|
100
|
+
added++;
|
|
101
|
+
console.log(`${kleur.green(' ✓')} ${name}`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const stderr = `${r.stderr || ''}`.toLowerCase();
|
|
106
|
+
if (stderr.includes('already exists') || stderr.includes('already configured')) {
|
|
107
|
+
existing++;
|
|
108
|
+
console.log(kleur.dim(` ✓ ${name} (already configured)`));
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
failed++;
|
|
113
|
+
console.log(kleur.red(` ✗ ${name} (${(r.stderr || r.stdout || 'failed').toString().trim()})`));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log(kleur.dim(` ↳ MCP project-scope result: ${added} added, ${existing} existing, ${failed} failed`));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function getAvailableProjectSkills(): Promise<string[]> {
|
|
120
|
+
if (!await fs.pathExists(PROJECT_SKILLS_DIR)) {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const entries = await fs.readdir(PROJECT_SKILLS_DIR);
|
|
125
|
+
const skills: string[] = [];
|
|
126
|
+
|
|
127
|
+
for (const entry of entries) {
|
|
128
|
+
const entryPath = path.join(PROJECT_SKILLS_DIR, entry);
|
|
129
|
+
const stat = await fs.stat(entryPath);
|
|
130
|
+
if (stat.isDirectory()) {
|
|
131
|
+
skills.push(entry);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return skills.sort();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Deep merge settings.json hooks without overwriting existing user hooks.
|
|
140
|
+
* Appends new hooks to existing events intelligently.
|
|
141
|
+
*/
|
|
142
|
+
export function deepMergeHooks(existing: Record<string, any>, incoming: Record<string, any>): Record<string, any> {
|
|
143
|
+
const result = { ...existing };
|
|
144
|
+
|
|
145
|
+
if (!result.hooks) result.hooks = {};
|
|
146
|
+
if (!incoming.hooks) return result;
|
|
147
|
+
|
|
148
|
+
for (const [event, incomingHooks] of Object.entries(incoming.hooks)) {
|
|
149
|
+
if (!result.hooks[event]) {
|
|
150
|
+
// Event doesn't exist — add it
|
|
151
|
+
result.hooks[event] = incomingHooks;
|
|
152
|
+
} else {
|
|
153
|
+
// Event exists — merge hooks intelligently
|
|
154
|
+
const existingEventHooks = Array.isArray(result.hooks[event]) ? result.hooks[event] : [result.hooks[event]];
|
|
155
|
+
const incomingEventHooks = Array.isArray(incomingHooks) ? incomingHooks : [incomingHooks];
|
|
156
|
+
|
|
157
|
+
const getCommand = (h: any) => h.command || h.hooks?.[0]?.command;
|
|
158
|
+
const mergeMatcher = (existingMatcher: string, incomingMatcher: string): string => {
|
|
159
|
+
const existingParts = existingMatcher.split('|').map((s: string) => s.trim()).filter(Boolean);
|
|
160
|
+
const incomingParts = incomingMatcher.split('|').map((s: string) => s.trim()).filter(Boolean);
|
|
161
|
+
const merged = [...existingParts];
|
|
162
|
+
for (const part of incomingParts) {
|
|
163
|
+
if (!merged.includes(part)) merged.push(part);
|
|
164
|
+
}
|
|
165
|
+
return merged.join('|');
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const mergedEventHooks = [...existingEventHooks];
|
|
169
|
+
for (const incomingHook of incomingEventHooks) {
|
|
170
|
+
const incomingCmd = getCommand(incomingHook);
|
|
171
|
+
if (!incomingCmd) {
|
|
172
|
+
mergedEventHooks.push(incomingHook);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const existingIndex = mergedEventHooks.findIndex((h: any) => getCommand(h) === incomingCmd);
|
|
177
|
+
if (existingIndex === -1) {
|
|
178
|
+
mergedEventHooks.push(incomingHook);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const existingHook = mergedEventHooks[existingIndex];
|
|
183
|
+
if (typeof existingHook.matcher === 'string' && typeof incomingHook.matcher === 'string') {
|
|
184
|
+
existingHook.matcher = mergeMatcher(existingHook.matcher, incomingHook.matcher);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
result.hooks[event] = mergedEventHooks;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function extractReadmeDescription(readmeContent: string): string {
|
|
196
|
+
const lines = readmeContent.split('\n');
|
|
197
|
+
const headingIndex = lines.findIndex(line => line.trim().startsWith('# '));
|
|
198
|
+
const searchStart = headingIndex >= 0 ? headingIndex + 1 : 0;
|
|
199
|
+
|
|
200
|
+
for (const rawLine of lines.slice(searchStart)) {
|
|
201
|
+
const line = rawLine.trim();
|
|
202
|
+
if (!line || line.startsWith('#') || line.startsWith('[![') || line.startsWith('<')) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return line
|
|
207
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
208
|
+
.replace(/[*_`]/g, '')
|
|
209
|
+
.trim();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return 'No description available';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Install a project skill package into the current project.
|
|
217
|
+
*/
|
|
218
|
+
export async function installProjectSkill(toolName: string, projectRootOverride?: string): Promise<void> {
|
|
219
|
+
const skillPath = path.join(PROJECT_SKILLS_DIR, toolName);
|
|
220
|
+
|
|
221
|
+
// Validation: Check if project skill exists
|
|
222
|
+
if (!await fs.pathExists(skillPath)) {
|
|
223
|
+
console.error(kleur.red(`\n✗ Project skill '${toolName}' not found.\n`));
|
|
224
|
+
console.error(kleur.dim(` Available project skills:\n`));
|
|
225
|
+
await listProjectSkills();
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Get target project root
|
|
230
|
+
const projectRoot = projectRootOverride ?? getProjectRoot();
|
|
231
|
+
const claudeDir = path.join(projectRoot, '.claude');
|
|
232
|
+
|
|
233
|
+
console.log(kleur.dim(`\n Installing project skill: ${kleur.cyan(toolName)}`));
|
|
234
|
+
console.log(kleur.dim(` Target: ${projectRoot}\n`));
|
|
235
|
+
|
|
236
|
+
const skillClaudeDir = path.join(skillPath, '.claude');
|
|
237
|
+
const skillSettingsPath = path.join(skillClaudeDir, 'settings.json');
|
|
238
|
+
const skillSkillsDir = path.join(skillClaudeDir, 'skills');
|
|
239
|
+
const skillReadmePath = path.join(skillPath, 'README.md');
|
|
240
|
+
|
|
241
|
+
// Step 1: Hook Injection (deep merge settings.json)
|
|
242
|
+
if (await fs.pathExists(skillSettingsPath)) {
|
|
243
|
+
console.log(kleur.bold('── Installing Hooks ──────────────────────'));
|
|
244
|
+
const targetSettingsPath = path.join(claudeDir, 'settings.json');
|
|
245
|
+
|
|
246
|
+
await fs.mkdirp(path.dirname(targetSettingsPath));
|
|
247
|
+
|
|
248
|
+
let existingSettings: Record<string, any> = {};
|
|
249
|
+
if (await fs.pathExists(targetSettingsPath)) {
|
|
250
|
+
try {
|
|
251
|
+
existingSettings = JSON.parse(await fs.readFile(targetSettingsPath, 'utf8'));
|
|
252
|
+
} catch {
|
|
253
|
+
// malformed JSON — start fresh
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const incomingSettings = JSON.parse(await fs.readFile(skillSettingsPath, 'utf8'));
|
|
258
|
+
const mergedSettings = deepMergeHooks(existingSettings, incomingSettings);
|
|
259
|
+
|
|
260
|
+
await fs.writeFile(targetSettingsPath, JSON.stringify(mergedSettings, null, 2) + '\n');
|
|
261
|
+
console.log(`${kleur.green(' ✓')} settings.json (hooks merged)`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
await syncProjectMcpServers(projectRoot);
|
|
265
|
+
|
|
266
|
+
// Step 2: Skill Copy
|
|
267
|
+
if (await fs.pathExists(skillSkillsDir)) {
|
|
268
|
+
console.log(kleur.bold('\n── Installing Skills ─────────────────────'));
|
|
269
|
+
const targetSkillsDir = path.join(claudeDir, 'skills');
|
|
270
|
+
|
|
271
|
+
const skillEntries = await fs.readdir(skillSkillsDir);
|
|
272
|
+
for (const entry of skillEntries) {
|
|
273
|
+
const src = path.join(skillSkillsDir, entry);
|
|
274
|
+
const dest = path.join(targetSkillsDir, entry);
|
|
275
|
+
await fs.copy(src, dest, {
|
|
276
|
+
filter: (src: string) => !src.includes('.Zone.Identifier'),
|
|
277
|
+
});
|
|
278
|
+
console.log(`${kleur.green(' ✓')} .claude/skills/${entry}/`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Step 2b: Copy additional Claude assets (hooks, docs, etc.) shipped with the skill
|
|
283
|
+
if (await fs.pathExists(skillClaudeDir)) {
|
|
284
|
+
const claudeEntries = await fs.readdir(skillClaudeDir);
|
|
285
|
+
|
|
286
|
+
for (const entry of claudeEntries) {
|
|
287
|
+
if (entry === 'settings.json' || entry === 'skills') {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const src = path.join(skillClaudeDir, entry);
|
|
292
|
+
const dest = path.join(claudeDir, entry);
|
|
293
|
+
await fs.copy(src, dest, {
|
|
294
|
+
filter: (src: string) => !src.includes('.Zone.Identifier'),
|
|
295
|
+
});
|
|
296
|
+
console.log(`${kleur.green(' ✓')} .claude/${entry}/`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Step 3: Documentation Copy
|
|
301
|
+
if (await fs.pathExists(skillReadmePath)) {
|
|
302
|
+
console.log(kleur.bold('\n── Installing Documentation ──────────────'));
|
|
303
|
+
const docsDir = path.join(claudeDir, 'docs');
|
|
304
|
+
await fs.mkdirp(docsDir);
|
|
305
|
+
|
|
306
|
+
const destReadme = path.join(docsDir, `${toolName}-readme.md`);
|
|
307
|
+
await fs.copy(skillReadmePath, destReadme);
|
|
308
|
+
console.log(`${kleur.green(' ✓')} .claude/docs/${toolName}-readme.md`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Step 4: Post-Install Guidance
|
|
312
|
+
if (toolName === 'service-skills-set') {
|
|
313
|
+
console.log(kleur.bold('\n── Installing Git Hooks ─────────────────'));
|
|
314
|
+
await installServiceGitHooks(projectRoot, skillClaudeDir);
|
|
315
|
+
console.log(`${kleur.green(' ✓')} .githooks/pre-commit`);
|
|
316
|
+
console.log(`${kleur.green(' ✓')} .githooks/pre-push`);
|
|
317
|
+
console.log(`${kleur.green(' ✓')} activated in .git/hooks/`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Step 5: Post-Install Guidance
|
|
321
|
+
console.log(kleur.bold('\n── Post-Install Steps ────────────────────'));
|
|
322
|
+
console.log(kleur.yellow('\n ⚠ IMPORTANT: Manual setup required!\n'));
|
|
323
|
+
console.log(kleur.white(` ${toolName} requires additional configuration.`));
|
|
324
|
+
console.log(kleur.white(` Please read: ${kleur.cyan('.claude/docs/' + toolName + '-readme.md')}\n`));
|
|
325
|
+
|
|
326
|
+
if (toolName === 'tdd-guard') {
|
|
327
|
+
console.log(kleur.white(' Example for Vitest:'));
|
|
328
|
+
console.log(kleur.dim(' npm install --save-dev tdd-guard-vitest\n'));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
console.log(kleur.green(' ✓ Installation complete!\n'));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export async function installAllProjectSkills(projectRootOverride?: string): Promise<void> {
|
|
335
|
+
const skills = await getAvailableProjectSkills();
|
|
336
|
+
|
|
337
|
+
if (skills.length === 0) {
|
|
338
|
+
console.log(kleur.dim(' No project skills available.\n'));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const projectRoot = projectRootOverride ?? getProjectRoot();
|
|
343
|
+
|
|
344
|
+
console.log(kleur.bold(`\nInstalling ${skills.length} project skills:\n`));
|
|
345
|
+
for (const skill of skills) {
|
|
346
|
+
console.log(kleur.dim(` • ${skill}`));
|
|
347
|
+
}
|
|
348
|
+
console.log('');
|
|
349
|
+
|
|
350
|
+
for (const skill of skills) {
|
|
351
|
+
await installProjectSkill(skill, projectRoot);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function buildProjectInitGuide(): string {
|
|
356
|
+
const lines = [
|
|
357
|
+
kleur.bold('\nProject Init — Recommended baseline\n'),
|
|
358
|
+
`${kleur.cyan('1) Install a quality gate skill (or equivalent checks):')}`,
|
|
359
|
+
kleur.dim(' - TypeScript projects: xtrm install project ts-quality-gate'),
|
|
360
|
+
kleur.dim(' - Python projects: xtrm install project py-quality-gate'),
|
|
361
|
+
kleur.dim(' - TDD workflow: xtrm install project tdd-guard'),
|
|
362
|
+
'',
|
|
363
|
+
`${kleur.cyan('2) Ensure your checks are actually configured in this repo:')}`,
|
|
364
|
+
kleur.dim(' - Testing: commands should run and fail when behavior regresses'),
|
|
365
|
+
kleur.dim(' - Linting/formatting: ESLint+Prettier (TS) or ruff (Python)'),
|
|
366
|
+
kleur.dim(' - Type checks: tsc (TS) or mypy/pyright (Python)'),
|
|
367
|
+
kleur.dim(' - Hooks only enforce what your project config defines'),
|
|
368
|
+
'',
|
|
369
|
+
`${kleur.cyan('3) Optional: Service Skills Set (service-skills-set)')}`,
|
|
370
|
+
kleur.dim(' - For multi-service/Docker repos with repeated operational workflows'),
|
|
371
|
+
kleur.dim(' - Adds project hooks + skills that route Claude to service-specific context'),
|
|
372
|
+
kleur.dim(' - Helps keep architecture knowledge persistent across sessions'),
|
|
373
|
+
'',
|
|
374
|
+
kleur.bold('Quick start commands:'),
|
|
375
|
+
kleur.dim(' xtrm install project list'),
|
|
376
|
+
kleur.dim(' xtrm install project ts-quality-gate # or py-quality-gate / tdd-guard'),
|
|
377
|
+
'',
|
|
378
|
+
];
|
|
379
|
+
|
|
380
|
+
return lines.join('\n');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function printProjectInitGuide(): Promise<void> {
|
|
384
|
+
console.log(buildProjectInitGuide());
|
|
385
|
+
await runBdInitForProject();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function installProjectByName(toolName: string): Promise<void> {
|
|
389
|
+
if (toolName === 'all' || toolName === '*') {
|
|
390
|
+
await installAllProjectSkills();
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
await installProjectSkill(toolName);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function runBdInitForProject(): Promise<void> {
|
|
397
|
+
let projectRoot: string;
|
|
398
|
+
try {
|
|
399
|
+
projectRoot = getProjectRoot();
|
|
400
|
+
} catch (err: any) {
|
|
401
|
+
console.log(kleur.yellow(`\n ⚠ Skipping bd init: ${err.message}\n`));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
console.log(kleur.bold('Running beads initialization (bd init)...'));
|
|
406
|
+
|
|
407
|
+
const result = spawnSync('bd', ['init'], {
|
|
408
|
+
cwd: projectRoot,
|
|
409
|
+
encoding: 'utf8',
|
|
410
|
+
timeout: 15000,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
if (result.error) {
|
|
414
|
+
console.log(kleur.yellow(` ⚠ Could not run bd init (${result.error.message})`));
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (result.status !== 0) {
|
|
419
|
+
const text = `${result.stdout || ''}\n${result.stderr || ''}`.toLowerCase();
|
|
420
|
+
if (text.includes('already initialized')) {
|
|
421
|
+
console.log(kleur.dim(' ✓ beads workspace already initialized'));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
425
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
426
|
+
console.log(kleur.yellow(` ⚠ bd init exited with code ${result.status}`));
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
431
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* List available project skills.
|
|
436
|
+
*/
|
|
437
|
+
async function listProjectSkills(): Promise<void> {
|
|
438
|
+
const entries = await getAvailableProjectSkills();
|
|
439
|
+
if (entries.length === 0) {
|
|
440
|
+
console.log(kleur.dim(' No project skills available.\n'));
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const skills: Array<{ name: string; description: string }> = [];
|
|
445
|
+
|
|
446
|
+
for (const entry of entries) {
|
|
447
|
+
const readmePath = path.join(PROJECT_SKILLS_DIR, entry, 'README.md');
|
|
448
|
+
let description = 'No description available';
|
|
449
|
+
|
|
450
|
+
if (await fs.pathExists(readmePath)) {
|
|
451
|
+
const readmeContent = await fs.readFile(readmePath, 'utf8');
|
|
452
|
+
description = extractReadmeDescription(readmeContent).slice(0, 80);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
skills.push({ name: entry, description });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (skills.length === 0) {
|
|
459
|
+
console.log(kleur.dim(' No project skills available.\n'));
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
console.log(kleur.bold('\nAvailable Project Skills:\n'));
|
|
464
|
+
|
|
465
|
+
// Dynamic import for Table
|
|
466
|
+
const Table = require('cli-table3');
|
|
467
|
+
const table = new Table({
|
|
468
|
+
head: [kleur.cyan('Skill'), kleur.cyan('Description')],
|
|
469
|
+
colWidths: [25, 60],
|
|
470
|
+
style: { head: [], border: [] },
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
for (const skill of skills) {
|
|
474
|
+
table.push([kleur.white(skill.name), kleur.dim(skill.description)]);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
console.log(table.toString());
|
|
478
|
+
|
|
479
|
+
console.log(kleur.bold('\n\nUsage:\n'));
|
|
480
|
+
console.log(kleur.dim(' xtrm install project <skill-name> Install a project skill'));
|
|
481
|
+
console.log(kleur.dim(' xtrm install project all Install all project skills'));
|
|
482
|
+
console.log(kleur.dim(' xtrm install project list List available skills\n'));
|
|
483
|
+
|
|
484
|
+
console.log(kleur.bold('Example:\n'));
|
|
485
|
+
console.log(kleur.dim(' xtrm install project tdd-guard\n'));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function getProjectRoot(): string {
|
|
489
|
+
const result = spawnSync('git', ['rev-parse', '--show-toplevel'], {
|
|
490
|
+
encoding: 'utf8',
|
|
491
|
+
timeout: 5000,
|
|
492
|
+
});
|
|
493
|
+
if (result.status !== 0) {
|
|
494
|
+
throw new Error('Not inside a git repository. Run this command from your target project directory.');
|
|
495
|
+
}
|
|
496
|
+
return path.resolve(result.stdout.trim());
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export function createInstallProjectCommand(): Command {
|
|
500
|
+
const installProjectCmd = new Command('project')
|
|
501
|
+
.description('Install a project-specific skill package');
|
|
502
|
+
|
|
503
|
+
// Subcommand: install project <tool-name>
|
|
504
|
+
installProjectCmd
|
|
505
|
+
.argument('<tool-name>', 'Name of the project skill to install')
|
|
506
|
+
.action(async (toolName: string) => {
|
|
507
|
+
try {
|
|
508
|
+
await installProjectByName(toolName);
|
|
509
|
+
} catch (err: any) {
|
|
510
|
+
console.error(kleur.red(`\n✗ ${err.message}\n`));
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// Subcommand: install project list
|
|
516
|
+
const listCmd = new Command('list')
|
|
517
|
+
.description('List available project skills')
|
|
518
|
+
.action(async () => {
|
|
519
|
+
await listProjectSkills();
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
const initCmd = new Command('init')
|
|
523
|
+
.description('Show project onboarding guidance (quality gates + service skills)')
|
|
524
|
+
.action(async () => {
|
|
525
|
+
await printProjectInitGuide();
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
installProjectCmd.addCommand(listCmd);
|
|
529
|
+
installProjectCmd.addCommand(initCmd);
|
|
530
|
+
|
|
531
|
+
return installProjectCmd;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function createProjectCommand(): Command {
|
|
535
|
+
const projectCmd = new Command('project')
|
|
536
|
+
.description('Project skill onboarding and installation helpers');
|
|
537
|
+
|
|
538
|
+
projectCmd
|
|
539
|
+
.command('init')
|
|
540
|
+
.description('Show project onboarding guidance (quality gates + service skills)')
|
|
541
|
+
.action(async () => {
|
|
542
|
+
await printProjectInitGuide();
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
projectCmd
|
|
546
|
+
.command('list')
|
|
547
|
+
.description('List available project skills')
|
|
548
|
+
.action(async () => {
|
|
549
|
+
await listProjectSkills();
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
projectCmd
|
|
553
|
+
.command('install')
|
|
554
|
+
.argument('<tool-name>', 'Name of the project skill to install')
|
|
555
|
+
.description('Alias for xtrm install project <tool-name>')
|
|
556
|
+
.action(async (toolName: string) => {
|
|
557
|
+
try {
|
|
558
|
+
await installProjectByName(toolName);
|
|
559
|
+
} catch (err: any) {
|
|
560
|
+
console.error(kleur.red(`\n✗ ${err.message}\n`));
|
|
561
|
+
process.exit(1);
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
return projectCmd;
|
|
566
|
+
}
|