xtrm-cli 0.5.0
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 +57378 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2 -0
- package/extensions/beads.ts +109 -0
- package/extensions/core/adapter.ts +45 -0
- package/extensions/core/lib.ts +3 -0
- package/extensions/core/logger.ts +45 -0
- package/extensions/core/runner.ts +71 -0
- package/extensions/custom-footer.ts +160 -0
- package/extensions/main-guard-post-push.ts +44 -0
- package/extensions/main-guard.ts +126 -0
- package/extensions/minimal-mode.ts +201 -0
- package/extensions/quality-gates.ts +67 -0
- package/extensions/service-skills.ts +150 -0
- package/extensions/xtrm-loader.ts +89 -0
- package/hooks/gitnexus-impact-reminder.py +13 -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/package.json +47 -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/claude.ts +122 -0
- package/src/commands/clean.ts +371 -0
- package/src/commands/end.ts +239 -0
- package/src/commands/finish.ts +25 -0
- package/src/commands/help.ts +180 -0
- package/src/commands/init.ts +959 -0
- package/src/commands/install-pi.ts +276 -0
- package/src/commands/install-service-skills.ts +281 -0
- package/src/commands/install.ts +427 -0
- package/src/commands/pi-install.ts +119 -0
- package/src/commands/pi.ts +128 -0
- package/src/commands/reset.ts +12 -0
- package/src/commands/status.ts +170 -0
- package/src/commands/worktree.ts +193 -0
- package/src/core/context.ts +141 -0
- package/src/core/diff.ts +174 -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/session-state.ts +139 -0
- package/src/core/sync-executor.ts +427 -0
- package/src/core/xtrm-finish.ts +267 -0
- package/src/index.ts +87 -0
- package/src/tests/policy-parity.test.ts +204 -0
- package/src/tests/session-flow-parity.test.ts +118 -0
- package/src/tests/session-state.test.ts +124 -0
- package/src/tests/xtrm-finish.test.ts +148 -0
- package/src/types/config.ts +51 -0
- package/src/types/models.ts +52 -0
- package/src/utils/atomic-config.ts +467 -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 +395 -0
- package/src/utils/theme.ts +37 -0
- package/src/utils/worktree-session.ts +93 -0
- package/test/atomic-config-prune.test.ts +101 -0
- package/test/atomic-config.test.ts +138 -0
- package/test/clean.test.ts +172 -0
- package/test/config-schema.test.ts +52 -0
- package/test/context.test.ts +33 -0
- package/test/end-worktree.test.ts +168 -0
- package/test/extensions/beads.test.ts +166 -0
- package/test/extensions/extension-harness.ts +85 -0
- package/test/extensions/main-guard.test.ts +77 -0
- package/test/extensions/minimal-mode.test.ts +107 -0
- package/test/extensions/quality-gates.test.ts +79 -0
- package/test/extensions/service-skills.test.ts +84 -0
- package/test/extensions/xtrm-loader.test.ts +53 -0
- package/test/hooks/quality-check-hooks.test.ts +45 -0
- package/test/hooks.test.ts +1075 -0
- package/test/install-pi.test.ts +185 -0
- package/test/install-project.test.ts +378 -0
- package/test/install-service-skills.test.ts +131 -0
- package/test/install-surface.test.ts +72 -0
- package/test/runtime-subcommands.test.ts +121 -0
- package/test/session-launcher.test.ts +139 -0
- package/tsconfig.json +22 -0
- package/tsup.config.ts +17 -0
- package/vitest.config.ts +10 -0
package/lib/sync.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import kleur from 'kleur';
|
|
4
|
+
import { transformGeminiConfig, transformSkillToCommand } from './transform-gemini.js';
|
|
5
|
+
import { safeMergeConfig } from './atomic-config.js';
|
|
6
|
+
import { ConfigAdapter } from './config-adapter.js';
|
|
7
|
+
import { syncMcpServersWithCli, loadCanonicalMcpConfig, detectAgent } from './sync-mcp-cli.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Execute a sync plan based on changeset and mode
|
|
11
|
+
*/
|
|
12
|
+
export async function executeSync(repoRoot, systemRoot, changeSet, mode, actionType, isDryRun = false) {
|
|
13
|
+
const isClaude = systemRoot.includes('.claude') || systemRoot.includes('Claude');
|
|
14
|
+
const isQwen = systemRoot.includes('.qwen') || systemRoot.includes('Qwen');
|
|
15
|
+
const isGemini = systemRoot.includes('.gemini') || systemRoot.includes('Gemini');
|
|
16
|
+
const categories = ['skills', 'hooks', 'config'];
|
|
17
|
+
|
|
18
|
+
if (isQwen) {
|
|
19
|
+
categories.push('qwen-commands');
|
|
20
|
+
} else if (isGemini) {
|
|
21
|
+
categories.push('commands', 'antigravity-workflows');
|
|
22
|
+
} else if (!isClaude) {
|
|
23
|
+
categories.push('commands');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let count = 0;
|
|
27
|
+
const adapter = new ConfigAdapter(systemRoot);
|
|
28
|
+
|
|
29
|
+
// Special handling for agents with official MCP CLI: Always sync MCP servers
|
|
30
|
+
// Supported: claude, gemini, qwen
|
|
31
|
+
const agent = detectAgent(systemRoot);
|
|
32
|
+
if (agent && actionType === 'sync') {
|
|
33
|
+
console.log(kleur.gray(` --> ${agent} MCP servers (via ${agent} mcp CLI)`));
|
|
34
|
+
|
|
35
|
+
// Load canonical MCP config
|
|
36
|
+
const canonicalConfig = loadCanonicalMcpConfig(repoRoot);
|
|
37
|
+
|
|
38
|
+
// Sync using official CLI
|
|
39
|
+
await syncMcpServersWithCli(agent, canonicalConfig, isDryRun, mode === 'prune');
|
|
40
|
+
|
|
41
|
+
count++;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const category of categories) {
|
|
45
|
+
const itemsToProcess = [];
|
|
46
|
+
|
|
47
|
+
if (actionType === 'sync') {
|
|
48
|
+
itemsToProcess.push(...changeSet[category].missing);
|
|
49
|
+
itemsToProcess.push(...changeSet[category].outdated);
|
|
50
|
+
|
|
51
|
+
// PRUNE: Handle removals from system if no longer in repo
|
|
52
|
+
if (mode === 'prune') {
|
|
53
|
+
for (const itemToDelete of changeSet[category].drifted || []) {
|
|
54
|
+
const dest = path.join(systemRoot, category, itemToDelete);
|
|
55
|
+
console.log(kleur.red(` [x] PRUNING ${category}/${itemToDelete}`));
|
|
56
|
+
if (!isDryRun) await fs.remove(dest);
|
|
57
|
+
count++;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} else if (actionType === 'backport') {
|
|
61
|
+
itemsToProcess.push(...changeSet[category].drifted);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const item of itemsToProcess) {
|
|
65
|
+
let src, dest;
|
|
66
|
+
|
|
67
|
+
if (category === 'config' && item === 'settings.json' && actionType === 'sync') {
|
|
68
|
+
src = path.join(repoRoot, 'config', 'settings.json');
|
|
69
|
+
dest = path.join(systemRoot, 'settings.json');
|
|
70
|
+
|
|
71
|
+
console.log(kleur.gray(` --> config/settings.json`));
|
|
72
|
+
|
|
73
|
+
// Skip settings.json sync for agents with official MCP CLI
|
|
74
|
+
// MCP servers are managed via CLI, hooks are not supported
|
|
75
|
+
if (agent) {
|
|
76
|
+
console.log(kleur.gray(` (Skipped: ${agent} uses ${agent} mcp CLI for MCP servers)`));
|
|
77
|
+
count++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const repoConfig = await fs.readJson(src);
|
|
82
|
+
let finalRepoConfig = resolveConfigPaths(repoConfig, systemRoot);
|
|
83
|
+
|
|
84
|
+
// Inject Hooks
|
|
85
|
+
const hooksSrc = path.join(repoRoot, 'config', 'hooks.json');
|
|
86
|
+
if (await fs.pathExists(hooksSrc)) {
|
|
87
|
+
const hooksRaw = await fs.readJson(hooksSrc);
|
|
88
|
+
const hooksAdapted = adapter.adaptHooksConfig(hooksRaw);
|
|
89
|
+
if (hooksAdapted.hooks) {
|
|
90
|
+
finalRepoConfig.hooks = { ...(finalRepoConfig.hooks || {}), ...hooksAdapted.hooks };
|
|
91
|
+
if (!isDryRun) console.log(kleur.dim(` (Injected hooks)`));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (fs.existsSync(dest)) {
|
|
96
|
+
const localConfig = await fs.readJson(dest);
|
|
97
|
+
const resolvedLocalConfig = resolveConfigPaths(localConfig, systemRoot);
|
|
98
|
+
|
|
99
|
+
// Handle PRUNE mode for mcpServers and hooks
|
|
100
|
+
if (mode === 'prune') {
|
|
101
|
+
// Remove local MCP servers NOT in our canonical source
|
|
102
|
+
if (localConfig.mcpServers && finalRepoConfig.mcpServers) {
|
|
103
|
+
const canonicalServers = new Set(Object.keys(finalRepoConfig.mcpServers));
|
|
104
|
+
for (const serverName of Object.keys(localConfig.mcpServers)) {
|
|
105
|
+
if (!canonicalServers.has(serverName)) {
|
|
106
|
+
delete localConfig.mcpServers[serverName];
|
|
107
|
+
if (!isDryRun) console.log(kleur.red(` (Pruned local MCP server: ${serverName})`));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const mergeResult = await safeMergeConfig(dest, finalRepoConfig, {
|
|
114
|
+
backupOnSuccess: true,
|
|
115
|
+
preserveComments: true,
|
|
116
|
+
dryRun: isDryRun,
|
|
117
|
+
resolvedLocalConfig: resolvedLocalConfig
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (mergeResult.updated) {
|
|
121
|
+
console.log(kleur.blue(` (Configuration safely merged)`));
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
if (!isDryRun) {
|
|
125
|
+
await fs.ensureDir(path.dirname(dest));
|
|
126
|
+
await fs.writeJson(dest, finalRepoConfig, { spaces: 2 });
|
|
127
|
+
}
|
|
128
|
+
console.log(kleur.green(` (Created new configuration)`));
|
|
129
|
+
}
|
|
130
|
+
count++;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Standard file sync for other items
|
|
135
|
+
const repoPath = category === 'commands' ? path.join(repoRoot, '.gemini', 'commands') :
|
|
136
|
+
category === 'qwen-commands' ? path.join(repoRoot, '.qwen', 'commands') :
|
|
137
|
+
category === 'antigravity-workflows' ? path.join(repoRoot, '.gemini', 'antigravity', 'global_workflows') :
|
|
138
|
+
path.join(repoRoot, category);
|
|
139
|
+
|
|
140
|
+
const systemPath = category === 'qwen-commands' ? path.join(systemRoot, 'commands') :
|
|
141
|
+
category === 'antigravity-workflows' ? path.join(systemRoot, '.gemini', 'antigravity', 'global_workflows') :
|
|
142
|
+
path.join(systemRoot, category);
|
|
143
|
+
|
|
144
|
+
if (actionType === 'backport') {
|
|
145
|
+
src = path.join(systemPath, item);
|
|
146
|
+
dest = path.join(repoPath, item);
|
|
147
|
+
} else {
|
|
148
|
+
src = path.join(repoPath, item);
|
|
149
|
+
dest = path.join(systemPath, item);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log(kleur.gray(` ${actionType === 'backport' ? '<--' : '-->'} ${category}/${item}`));
|
|
153
|
+
|
|
154
|
+
if (mode === 'symlink' && actionType === 'sync' && category !== 'config') {
|
|
155
|
+
if (!isDryRun) {
|
|
156
|
+
await fs.remove(dest);
|
|
157
|
+
await fs.ensureSymlink(src, dest);
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
if (!isDryRun) {
|
|
161
|
+
await fs.remove(dest);
|
|
162
|
+
await fs.copy(src, dest);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Gemini Skill -> Command transformation
|
|
167
|
+
if (category === 'skills' && !isClaude && actionType === 'sync') {
|
|
168
|
+
const skillMdPath = path.join(src, 'SKILL.md');
|
|
169
|
+
if (fs.existsSync(skillMdPath)) {
|
|
170
|
+
const result = await transformSkillToCommand(skillMdPath);
|
|
171
|
+
if (result && !isDryRun) {
|
|
172
|
+
const commandDest = path.join(systemRoot, 'commands', `${result.commandName}.toml`);
|
|
173
|
+
await fs.ensureDir(path.dirname(commandDest));
|
|
174
|
+
await fs.writeFile(commandDest, result.toml);
|
|
175
|
+
console.log(kleur.cyan(` (Auto-generated slash command: /${result.commandName})`));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
count++;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Final Step: Write Sync Manifest
|
|
185
|
+
if (!isDryRun && actionType === 'sync') {
|
|
186
|
+
const manifestPath = path.join(systemRoot, '.jaggers-sync-manifest.json');
|
|
187
|
+
const manifest = {
|
|
188
|
+
lastSync: new Date().toISOString(),
|
|
189
|
+
repoRoot,
|
|
190
|
+
items: count
|
|
191
|
+
};
|
|
192
|
+
await fs.writeJson(manifestPath, manifest, { spaces: 2 });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return count;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Recursively resolves paths in the config to match the target directory
|
|
200
|
+
*
|
|
201
|
+
* This function corrects hardcoded paths (e.g. /home/dawid/...) to match the current user's home directory.
|
|
202
|
+
* It's applied to both repository config AND local config to ensure existing installations get updated.
|
|
203
|
+
*
|
|
204
|
+
* @param {Object} config - The configuration object to process
|
|
205
|
+
* @param {string} targetDir - The target directory (e.g. /home/jagger/.claude)
|
|
206
|
+
* @returns {Object} - New config object with resolved paths
|
|
207
|
+
*/
|
|
208
|
+
function resolveConfigPaths(config, targetDir) {
|
|
209
|
+
const newConfig = JSON.parse(JSON.stringify(config));
|
|
210
|
+
|
|
211
|
+
function recursiveReplace(obj) {
|
|
212
|
+
for (const key in obj) {
|
|
213
|
+
if (typeof obj[key] === 'string') {
|
|
214
|
+
// Match absolute paths containing /hooks/ and replace the prefix with targetDir/hooks
|
|
215
|
+
if (obj[key].match(/\/[^\s"']+\/hooks\//)) {
|
|
216
|
+
const hooksDir = path.join(targetDir, 'hooks');
|
|
217
|
+
obj[key] = obj[key].replace(/(\/[^\s"']+\/hooks\/)/g, `${hooksDir}/`);
|
|
218
|
+
}
|
|
219
|
+
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
|
|
220
|
+
recursiveReplace(obj[key]);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
recursiveReplace(newConfig);
|
|
226
|
+
return newConfig;
|
|
227
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "xtrm-cli",
|
|
3
|
+
"version": "0.5.0",
|
|
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
|
+
"xt": "dist/index.cjs"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsup",
|
|
13
|
+
"dev": "tsx src/index.ts",
|
|
14
|
+
"typecheck": "tsc --noEmit",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"start": "node dist/index.cjs"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"boxen": "^8.0.1",
|
|
20
|
+
"cli-table3": "^0.6.5",
|
|
21
|
+
"commander": "^14.0.3",
|
|
22
|
+
"comment-json": "^4.2.3",
|
|
23
|
+
"conf": "^12.0.0",
|
|
24
|
+
"dotenv": "^16.4.5",
|
|
25
|
+
"fs-extra": "^11.2.0",
|
|
26
|
+
"kleur": "^4.1.5",
|
|
27
|
+
"listr2": "^10.1.1",
|
|
28
|
+
"minimist": "^1.2.8",
|
|
29
|
+
"ora": "^9.3.0",
|
|
30
|
+
"project": "^0.1.6",
|
|
31
|
+
"prompts": "^2.4.2",
|
|
32
|
+
"tdd-guard": "^1.1.0",
|
|
33
|
+
"zod": "^4.3.6"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/fs-extra": "^11.0.4",
|
|
37
|
+
"@types/node": "^25.3.0",
|
|
38
|
+
"tdd-guard-vitest": "^0.1.6",
|
|
39
|
+
"tsup": "^8.5.1",
|
|
40
|
+
"tsx": "^4.21.0",
|
|
41
|
+
"typescript": "^5.9.3",
|
|
42
|
+
"vitest": "^4.0.18"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=20.0.0"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -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,122 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import kleur from 'kleur';
|
|
3
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import { findRepoRoot } from '../utils/repo-root.js';
|
|
7
|
+
import { t } from '../utils/theme.js';
|
|
8
|
+
import { installPlugin } from './install.js';
|
|
9
|
+
import { launchWorktreeSession } from '../utils/worktree-session.js';
|
|
10
|
+
|
|
11
|
+
export function createClaudeCommand(): Command {
|
|
12
|
+
const cmd = new Command('claude')
|
|
13
|
+
.description('Launch a Claude session in a sandboxed worktree, or manage the Claude runtime')
|
|
14
|
+
.argument('[name]', 'Optional session name — used as xt/<name> branch (random if omitted)')
|
|
15
|
+
.action(async (name: string | undefined) => {
|
|
16
|
+
await launchWorktreeSession({ runtime: 'claude', name });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
cmd.command('install')
|
|
20
|
+
.description('Install/refresh the xtrm-tools Claude plugin and official plugins')
|
|
21
|
+
.option('--dry-run', 'Preview without making changes', false)
|
|
22
|
+
.action(async (opts) => {
|
|
23
|
+
const repoRoot = await findRepoRoot();
|
|
24
|
+
await installPlugin(repoRoot, opts.dryRun);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
cmd.command('reload')
|
|
28
|
+
.alias('reinstall')
|
|
29
|
+
.description('Reinstall Claude plugin from live repo (refreshes cached copy)')
|
|
30
|
+
.action(async () => {
|
|
31
|
+
const repoRoot = await findRepoRoot();
|
|
32
|
+
await installPlugin(repoRoot, false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
cmd.command('status')
|
|
36
|
+
.description('Show Claude CLI version, plugin status, and hook wiring')
|
|
37
|
+
.action(async () => {
|
|
38
|
+
console.log(t.bold('\n Claude Code Status\n'));
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const version = execSync('claude --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
42
|
+
console.log(t.success(` ✓ claude CLI: ${version}`));
|
|
43
|
+
} catch {
|
|
44
|
+
console.log(kleur.red(' ✗ claude CLI not found'));
|
|
45
|
+
console.log('');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const listResult = spawnSync('claude', ['plugin', 'list'], { encoding: 'utf8', stdio: 'pipe' });
|
|
50
|
+
const pluginOutput = listResult.stdout ?? '';
|
|
51
|
+
if (pluginOutput.includes('xtrm-tools')) {
|
|
52
|
+
console.log(t.success(' ✓ xtrm-tools plugin installed'));
|
|
53
|
+
} else {
|
|
54
|
+
console.log(kleur.yellow(' ⚠ xtrm-tools plugin not installed — run: xt claude install'));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
execSync('bd --version', { stdio: 'ignore' });
|
|
59
|
+
console.log(t.success(' ✓ beads (bd) available'));
|
|
60
|
+
} catch {
|
|
61
|
+
console.log(kleur.dim(' ○ beads (bd) not installed'));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log('');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
cmd.command('doctor')
|
|
68
|
+
.description('Run diagnostic checks on Claude Code setup')
|
|
69
|
+
.action(async () => {
|
|
70
|
+
console.log(t.bold('\n Claude Code Doctor\n'));
|
|
71
|
+
|
|
72
|
+
let allOk = true;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
execSync('claude --version', { stdio: 'ignore' });
|
|
76
|
+
console.log(t.success(' ✓ claude CLI available'));
|
|
77
|
+
} catch {
|
|
78
|
+
console.log(kleur.red(' ✗ claude CLI not found — install Claude Code'));
|
|
79
|
+
allOk = false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const listResult = spawnSync('claude', ['plugin', 'list'], { encoding: 'utf8', stdio: 'pipe' });
|
|
83
|
+
if (listResult.stdout?.includes('xtrm-tools')) {
|
|
84
|
+
console.log(t.success(' ✓ xtrm-tools plugin installed'));
|
|
85
|
+
} else {
|
|
86
|
+
console.log(kleur.yellow(' ⚠ xtrm-tools plugin missing — run: xt claude install'));
|
|
87
|
+
allOk = false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
execSync('bd --version', { stdio: 'ignore' });
|
|
92
|
+
console.log(t.success(' ✓ beads (bd) installed'));
|
|
93
|
+
} catch {
|
|
94
|
+
console.log(kleur.yellow(' ⚠ beads not installed — run: npm install -g @beads/bd'));
|
|
95
|
+
allOk = false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
execSync('dolt version', { stdio: 'ignore' });
|
|
100
|
+
console.log(t.success(' ✓ dolt installed'));
|
|
101
|
+
} catch {
|
|
102
|
+
console.log(kleur.yellow(' ⚠ dolt not installed — required for beads storage'));
|
|
103
|
+
allOk = false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
execSync('gitnexus --version', { stdio: 'ignore' });
|
|
108
|
+
console.log(t.success(' ✓ gitnexus installed'));
|
|
109
|
+
} catch {
|
|
110
|
+
console.log(kleur.dim(' ○ gitnexus not installed (optional) — npm install -g gitnexus'));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log('');
|
|
114
|
+
if (allOk) {
|
|
115
|
+
console.log(t.boldGreen(' ✓ All checks passed\n'));
|
|
116
|
+
} else {
|
|
117
|
+
console.log(kleur.yellow(' ⚠ Some checks failed — see above\n'));
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return cmd;
|
|
122
|
+
}
|