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.
Files changed (93) hide show
  1. package/.gemini/settings.json +39 -0
  2. package/dist/index.cjs +57378 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +2 -0
  5. package/extensions/beads.ts +109 -0
  6. package/extensions/core/adapter.ts +45 -0
  7. package/extensions/core/lib.ts +3 -0
  8. package/extensions/core/logger.ts +45 -0
  9. package/extensions/core/runner.ts +71 -0
  10. package/extensions/custom-footer.ts +160 -0
  11. package/extensions/main-guard-post-push.ts +44 -0
  12. package/extensions/main-guard.ts +126 -0
  13. package/extensions/minimal-mode.ts +201 -0
  14. package/extensions/quality-gates.ts +67 -0
  15. package/extensions/service-skills.ts +150 -0
  16. package/extensions/xtrm-loader.ts +89 -0
  17. package/hooks/gitnexus-impact-reminder.py +13 -0
  18. package/lib/atomic-config.js +236 -0
  19. package/lib/config-adapter.js +231 -0
  20. package/lib/config-injector.js +80 -0
  21. package/lib/context.js +73 -0
  22. package/lib/diff.js +142 -0
  23. package/lib/env-manager.js +160 -0
  24. package/lib/sync-mcp-cli.js +345 -0
  25. package/lib/sync.js +227 -0
  26. package/package.json +47 -0
  27. package/src/adapters/base.ts +29 -0
  28. package/src/adapters/claude.ts +38 -0
  29. package/src/adapters/registry.ts +21 -0
  30. package/src/commands/claude.ts +122 -0
  31. package/src/commands/clean.ts +371 -0
  32. package/src/commands/end.ts +239 -0
  33. package/src/commands/finish.ts +25 -0
  34. package/src/commands/help.ts +180 -0
  35. package/src/commands/init.ts +959 -0
  36. package/src/commands/install-pi.ts +276 -0
  37. package/src/commands/install-service-skills.ts +281 -0
  38. package/src/commands/install.ts +427 -0
  39. package/src/commands/pi-install.ts +119 -0
  40. package/src/commands/pi.ts +128 -0
  41. package/src/commands/reset.ts +12 -0
  42. package/src/commands/status.ts +170 -0
  43. package/src/commands/worktree.ts +193 -0
  44. package/src/core/context.ts +141 -0
  45. package/src/core/diff.ts +174 -0
  46. package/src/core/interactive-plan.ts +165 -0
  47. package/src/core/manifest.ts +26 -0
  48. package/src/core/preflight.ts +142 -0
  49. package/src/core/rollback.ts +32 -0
  50. package/src/core/session-state.ts +139 -0
  51. package/src/core/sync-executor.ts +427 -0
  52. package/src/core/xtrm-finish.ts +267 -0
  53. package/src/index.ts +87 -0
  54. package/src/tests/policy-parity.test.ts +204 -0
  55. package/src/tests/session-flow-parity.test.ts +118 -0
  56. package/src/tests/session-state.test.ts +124 -0
  57. package/src/tests/xtrm-finish.test.ts +148 -0
  58. package/src/types/config.ts +51 -0
  59. package/src/types/models.ts +52 -0
  60. package/src/utils/atomic-config.ts +467 -0
  61. package/src/utils/banner.ts +194 -0
  62. package/src/utils/config-adapter.ts +90 -0
  63. package/src/utils/config-injector.ts +81 -0
  64. package/src/utils/env-manager.ts +193 -0
  65. package/src/utils/hash.ts +42 -0
  66. package/src/utils/repo-root.ts +39 -0
  67. package/src/utils/sync-mcp-cli.ts +395 -0
  68. package/src/utils/theme.ts +37 -0
  69. package/src/utils/worktree-session.ts +93 -0
  70. package/test/atomic-config-prune.test.ts +101 -0
  71. package/test/atomic-config.test.ts +138 -0
  72. package/test/clean.test.ts +172 -0
  73. package/test/config-schema.test.ts +52 -0
  74. package/test/context.test.ts +33 -0
  75. package/test/end-worktree.test.ts +168 -0
  76. package/test/extensions/beads.test.ts +166 -0
  77. package/test/extensions/extension-harness.ts +85 -0
  78. package/test/extensions/main-guard.test.ts +77 -0
  79. package/test/extensions/minimal-mode.test.ts +107 -0
  80. package/test/extensions/quality-gates.test.ts +79 -0
  81. package/test/extensions/service-skills.test.ts +84 -0
  82. package/test/extensions/xtrm-loader.test.ts +53 -0
  83. package/test/hooks/quality-check-hooks.test.ts +45 -0
  84. package/test/hooks.test.ts +1075 -0
  85. package/test/install-pi.test.ts +185 -0
  86. package/test/install-project.test.ts +378 -0
  87. package/test/install-service-skills.test.ts +131 -0
  88. package/test/install-surface.test.ts +72 -0
  89. package/test/runtime-subcommands.test.ts +121 -0
  90. package/test/session-launcher.test.ts +139 -0
  91. package/tsconfig.json +22 -0
  92. package/tsup.config.ts +17 -0
  93. 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
+ }