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,534 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import kleur from 'kleur';
|
|
3
|
+
import prompts from 'prompts';
|
|
4
|
+
import { Listr } from 'listr2';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import { getContext } from '../core/context.js';
|
|
7
|
+
import { calculateDiff, PruneModeReadError } from '../core/diff.js';
|
|
8
|
+
import { executeSync } from '../core/sync-executor.js';
|
|
9
|
+
import { findRepoRoot } from '../utils/repo-root.js';
|
|
10
|
+
import { t, sym } from '../utils/theme.js';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { createInstallProjectCommand } from './install-project.js';
|
|
13
|
+
|
|
14
|
+
interface TargetChanges {
|
|
15
|
+
target: string;
|
|
16
|
+
changeSet: any;
|
|
17
|
+
totalChanges: number;
|
|
18
|
+
skippedDrifted: string[];
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface DiffCtx {
|
|
23
|
+
allChanges: TargetChanges[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
import type { ChangeSet } from '../types/config.js';
|
|
27
|
+
|
|
28
|
+
function renderPlanTable(allChanges: TargetChanges[]): void {
|
|
29
|
+
const Table = require('cli-table3');
|
|
30
|
+
|
|
31
|
+
const table = new Table({
|
|
32
|
+
head: [
|
|
33
|
+
t.header('Target'),
|
|
34
|
+
t.header(kleur.green('+ New')),
|
|
35
|
+
t.header(kleur.yellow('↑ Update')),
|
|
36
|
+
t.header('Total'),
|
|
37
|
+
],
|
|
38
|
+
style: { head: [], border: [] },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
for (const { target, changeSet, totalChanges } of allChanges) {
|
|
42
|
+
const missing = Object.values(changeSet).reduce((s: number, c: any) => s + c.missing.length, 0) as number;
|
|
43
|
+
const outdated = Object.values(changeSet).reduce((s: number, c: any) => s + c.outdated.length, 0) as number;
|
|
44
|
+
|
|
45
|
+
table.push([
|
|
46
|
+
kleur.white(formatTargetLabel(target)),
|
|
47
|
+
missing > 0 ? kleur.green(String(missing)) : t.label('—'),
|
|
48
|
+
outdated > 0 ? kleur.yellow(String(outdated)) : t.label('—'),
|
|
49
|
+
kleur.bold().white(String(totalChanges)),
|
|
50
|
+
]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log('\n' + table.toString() + '\n');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function renderSummaryCard(
|
|
57
|
+
allChanges: TargetChanges[],
|
|
58
|
+
totalCount: number,
|
|
59
|
+
allSkipped: string[],
|
|
60
|
+
isDryRun: boolean,
|
|
61
|
+
): Promise<void> {
|
|
62
|
+
const boxen = (await import('boxen')).default;
|
|
63
|
+
|
|
64
|
+
const hasDrift = allSkipped.length > 0;
|
|
65
|
+
const lines = [
|
|
66
|
+
hasDrift ? t.boldGreen(' ✓ Install complete') + t.warning(' (with skipped drift)') : t.boldGreen(' ✓ Install complete'),
|
|
67
|
+
'',
|
|
68
|
+
` ${t.label('Targets')} ${allChanges.length} environment${allChanges.length !== 1 ? 's' : ''}`,
|
|
69
|
+
` ${t.label('Installed')} ${totalCount} item${totalCount !== 1 ? 's' : ''}`,
|
|
70
|
+
...(hasDrift ? [
|
|
71
|
+
` ${t.label('Skipped')} ${kleur.yellow(String(allSkipped.length))} drifted (local changes preserved)`,
|
|
72
|
+
` ${t.label('Hint')} run ${t.accent('xtrm install --backport')} to push them back`,
|
|
73
|
+
] : []),
|
|
74
|
+
...(isDryRun ? ['', t.accent(' Dry run — no changes written')] : []),
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
console.log('\n' + boxen(lines.join('\n'), {
|
|
78
|
+
padding: { top: 1, bottom: 1, left: 1, right: 3 },
|
|
79
|
+
borderStyle: 'round',
|
|
80
|
+
borderColor: hasDrift ? 'yellow' : 'green',
|
|
81
|
+
}) + '\n');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
import { execSync } from 'child_process';
|
|
85
|
+
|
|
86
|
+
import { spawnSync } from 'child_process';
|
|
87
|
+
const BEADS_HOOK_PATTERN = /^beads-/;
|
|
88
|
+
|
|
89
|
+
function formatTargetLabel(target: string): string {
|
|
90
|
+
const normalized = target.replace(/\\/g, '/').toLowerCase();
|
|
91
|
+
if (normalized.endsWith('/.agents/skills') || normalized.includes('/.agents/skills/')) return '~/.agents/skills';
|
|
92
|
+
if (normalized.endsWith('/.claude') || normalized.includes('/.claude/')) return '~/.claude';
|
|
93
|
+
return path.basename(target);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function filterBeadsFromChangeSet(changeSet: ChangeSet): ChangeSet {
|
|
97
|
+
return {
|
|
98
|
+
...changeSet,
|
|
99
|
+
hooks: {
|
|
100
|
+
...changeSet.hooks,
|
|
101
|
+
missing: changeSet.hooks.missing.filter(h => !BEADS_HOOK_PATTERN.test(h)),
|
|
102
|
+
outdated: changeSet.hooks.outdated.filter(h => !BEADS_HOOK_PATTERN.test(h)),
|
|
103
|
+
drifted: changeSet.hooks.drifted.filter(h => !BEADS_HOOK_PATTERN.test(h)),
|
|
104
|
+
total: changeSet.hooks.total,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isBeadsInstalled(): boolean {
|
|
110
|
+
try {
|
|
111
|
+
execSync('bd --version', { stdio: 'ignore' });
|
|
112
|
+
return true;
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isDoltInstalled(): boolean {
|
|
119
|
+
try {
|
|
120
|
+
execSync('dolt version', { stdio: 'ignore' });
|
|
121
|
+
return true;
|
|
122
|
+
} catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface GlobalInstallFlags {
|
|
128
|
+
dryRun: boolean;
|
|
129
|
+
yes: boolean;
|
|
130
|
+
noMcp: boolean;
|
|
131
|
+
force: boolean;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function needsSettingsSync(repoRoot: string, target: string): Promise<boolean> {
|
|
135
|
+
const normalizedTarget = target.replace(/\\/g, '/').toLowerCase();
|
|
136
|
+
if (normalizedTarget.includes('.agents/skills')) return false;
|
|
137
|
+
|
|
138
|
+
const hooksTemplatePath = path.join(repoRoot, 'config', 'hooks.json');
|
|
139
|
+
if (!await fs.pathExists(hooksTemplatePath)) return false;
|
|
140
|
+
|
|
141
|
+
const requiredEvents = Object.keys((await fs.readJson(hooksTemplatePath)).hooks ?? {});
|
|
142
|
+
if (requiredEvents.length === 0) return false;
|
|
143
|
+
|
|
144
|
+
const targetSettingsPath = path.join(target, 'settings.json');
|
|
145
|
+
if (!await fs.pathExists(targetSettingsPath)) return true;
|
|
146
|
+
|
|
147
|
+
let settings: any = {};
|
|
148
|
+
try {
|
|
149
|
+
settings = await fs.readJson(targetSettingsPath);
|
|
150
|
+
} catch {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const targetHooks = settings?.hooks;
|
|
155
|
+
if (!targetHooks || typeof targetHooks !== 'object' || Object.keys(targetHooks).length === 0) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return requiredEvents.some((event) => !(event in targetHooks));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function runGlobalInstall(
|
|
163
|
+
flags: GlobalInstallFlags,
|
|
164
|
+
installOpts: { excludeBeads?: boolean; checkBeads?: boolean } = {},
|
|
165
|
+
): Promise<void> {
|
|
166
|
+
const { dryRun, yes, noMcp, force } = flags;
|
|
167
|
+
const effectiveYes = yes || process.argv.includes('--yes') || process.argv.includes('-y');
|
|
168
|
+
const repoRoot = await findRepoRoot();
|
|
169
|
+
const ctx = await getContext({ selector: 'all', createMissingDirs: !dryRun });
|
|
170
|
+
const { targets, syncMode } = ctx;
|
|
171
|
+
|
|
172
|
+
let skipBeads = installOpts.excludeBeads ?? false;
|
|
173
|
+
|
|
174
|
+
if (installOpts.checkBeads && !skipBeads) {
|
|
175
|
+
console.log(t.bold('\n ⚙ beads + dolt (workflow enforcement backend)'));
|
|
176
|
+
console.log(t.muted(' beads is a git-backed issue tracker; dolt is its SQL+git storage backend.'));
|
|
177
|
+
console.log(t.muted(' Without them the gate hooks install but provide no enforcement.\n'));
|
|
178
|
+
|
|
179
|
+
const beadsOk = isBeadsInstalled();
|
|
180
|
+
const doltOk = isDoltInstalled();
|
|
181
|
+
|
|
182
|
+
if (beadsOk && doltOk) {
|
|
183
|
+
console.log(t.success(' ✓ beads + dolt already installed\n'));
|
|
184
|
+
} else {
|
|
185
|
+
const missing = [!beadsOk && 'bd', !doltOk && 'dolt'].filter(Boolean).join(', ');
|
|
186
|
+
|
|
187
|
+
let doInstall = effectiveYes;
|
|
188
|
+
if (!effectiveYes) {
|
|
189
|
+
const { install } = await prompts({
|
|
190
|
+
type: 'confirm',
|
|
191
|
+
name: 'install',
|
|
192
|
+
message: `Install beads + dolt? (${missing} not found) — required for workflow enforcement hooks`,
|
|
193
|
+
initial: true,
|
|
194
|
+
});
|
|
195
|
+
doInstall = install;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (doInstall) {
|
|
199
|
+
if (!beadsOk) {
|
|
200
|
+
console.log(t.muted('\n Installing @beads/bd...'));
|
|
201
|
+
spawnSync('npm', ['install', '-g', '@beads/bd'], { stdio: 'inherit' });
|
|
202
|
+
console.log(t.success(' ✓ bd installed'));
|
|
203
|
+
}
|
|
204
|
+
if (!doltOk) {
|
|
205
|
+
console.log(t.muted('\n Installing dolt...'));
|
|
206
|
+
if (process.platform === 'darwin') {
|
|
207
|
+
spawnSync('brew', ['install', 'dolt'], { stdio: 'inherit' });
|
|
208
|
+
} else {
|
|
209
|
+
spawnSync('sudo', ['bash', '-c',
|
|
210
|
+
'curl -L https://github.com/dolthub/dolt/releases/latest/download/install.sh | bash',
|
|
211
|
+
], { stdio: 'inherit' });
|
|
212
|
+
}
|
|
213
|
+
console.log(t.success(' ✓ dolt installed'));
|
|
214
|
+
}
|
|
215
|
+
console.log('');
|
|
216
|
+
} else {
|
|
217
|
+
console.log(t.muted(' ℹ Skipping beads gate hooks. Re-run xtrm install all after installing beads+dolt.\n'));
|
|
218
|
+
skipBeads = true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const diffTasks = new Listr<DiffCtx>(
|
|
224
|
+
targets.map(target => ({
|
|
225
|
+
title: formatTargetLabel(target),
|
|
226
|
+
task: async (listCtx, task) => {
|
|
227
|
+
try {
|
|
228
|
+
let changeSet = await calculateDiff(repoRoot, target, false);
|
|
229
|
+
if (skipBeads) {
|
|
230
|
+
changeSet = filterBeadsFromChangeSet(changeSet);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const hasSettingsDiff =
|
|
234
|
+
changeSet.config.missing.includes('settings.json') ||
|
|
235
|
+
changeSet.config.outdated.includes('settings.json') ||
|
|
236
|
+
changeSet.config.drifted.includes('settings.json');
|
|
237
|
+
|
|
238
|
+
if (!hasSettingsDiff && await needsSettingsSync(repoRoot, target)) {
|
|
239
|
+
changeSet.config.outdated.push('settings.json');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const totalChanges = Object.values(changeSet).reduce(
|
|
243
|
+
(sum, c: any) => sum + c.missing.length + c.outdated.length + c.drifted.length, 0,
|
|
244
|
+
);
|
|
245
|
+
task.title = `${formatTargetLabel(target)}${t.muted(` — ${totalChanges} change${totalChanges !== 1 ? 's' : ''}`)}`;
|
|
246
|
+
if (totalChanges > 0) {
|
|
247
|
+
listCtx.allChanges.push({ target, changeSet, totalChanges, skippedDrifted: [] });
|
|
248
|
+
}
|
|
249
|
+
} catch (err) {
|
|
250
|
+
if (err instanceof PruneModeReadError) {
|
|
251
|
+
task.title = `${formatTargetLabel(target)} ${kleur.red('(skipped — cannot read in prune mode)')}`;
|
|
252
|
+
} else {
|
|
253
|
+
throw err;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
})),
|
|
258
|
+
{ concurrent: true, exitOnError: false },
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const diffCtx = await diffTasks.run({ allChanges: [] });
|
|
262
|
+
const allChanges = diffCtx.allChanges;
|
|
263
|
+
|
|
264
|
+
if (allChanges.length === 0) {
|
|
265
|
+
console.log('\n' + t.boldGreen('✓ Files are up-to-date') + '\n');
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
renderPlanTable(allChanges);
|
|
270
|
+
|
|
271
|
+
if (dryRun) {
|
|
272
|
+
console.log(t.accent('💡 Dry run — no changes written\n'));
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!effectiveYes) {
|
|
277
|
+
const totalChangesCount = allChanges.reduce((s, c) => s + c.totalChanges, 0);
|
|
278
|
+
const { confirm } = await prompts({
|
|
279
|
+
type: 'confirm',
|
|
280
|
+
name: 'confirm',
|
|
281
|
+
message: `Proceed with install (${totalChangesCount} total changes)?`,
|
|
282
|
+
initial: true,
|
|
283
|
+
});
|
|
284
|
+
if (!confirm) {
|
|
285
|
+
console.log(t.muted(' Install cancelled.\n'));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let totalCount = 0;
|
|
291
|
+
|
|
292
|
+
for (const { target, changeSet, skippedDrifted } of allChanges) {
|
|
293
|
+
console.log(t.bold(`\n ${sym.arrow} ${formatTargetLabel(target)}`));
|
|
294
|
+
|
|
295
|
+
const count = await executeSync(repoRoot, target, changeSet, syncMode, 'sync', dryRun, undefined, {
|
|
296
|
+
skipMcp: noMcp,
|
|
297
|
+
force,
|
|
298
|
+
});
|
|
299
|
+
totalCount += count;
|
|
300
|
+
|
|
301
|
+
for (const [category, cat] of Object.entries(changeSet)) {
|
|
302
|
+
const c = cat as any;
|
|
303
|
+
if (c.drifted.length > 0 && !force) {
|
|
304
|
+
skippedDrifted.push(...c.drifted.map((item: string) => `${category}/${item}`));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
console.log(t.success(` ${sym.ok} ${count} item${count !== 1 ? 's' : ''} installed`));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const allSkipped = allChanges.flatMap(c => c.skippedDrifted);
|
|
312
|
+
await renderSummaryCard(allChanges, totalCount, allSkipped, dryRun);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function createInstallAllCommand(): Command {
|
|
316
|
+
return new Command('all')
|
|
317
|
+
.description('Install everything: skills, all hooks (including beads gates), and MCP servers')
|
|
318
|
+
.option('--dry-run', 'Preview changes without making any modifications', false)
|
|
319
|
+
.option('-y, --yes', 'Skip confirmation prompts', false)
|
|
320
|
+
.option('--no-mcp', 'Skip MCP server registration', false)
|
|
321
|
+
.option('--force', 'Overwrite locally drifted files', false)
|
|
322
|
+
.action(async (opts) => {
|
|
323
|
+
await runGlobalInstall(
|
|
324
|
+
{ dryRun: opts.dryRun, yes: opts.yes, noMcp: opts.mcp === false, force: opts.force },
|
|
325
|
+
{ checkBeads: true },
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function createInstallBasicCommand(): Command {
|
|
331
|
+
return new Command('basic')
|
|
332
|
+
.description('Install skills, general hooks, and MCP servers (no beads gate hooks)')
|
|
333
|
+
.option('--dry-run', 'Preview changes without making any modifications', false)
|
|
334
|
+
.option('-y, --yes', 'Skip confirmation prompts', false)
|
|
335
|
+
.option('--no-mcp', 'Skip MCP server registration', false)
|
|
336
|
+
.option('--force', 'Overwrite locally drifted files', false)
|
|
337
|
+
.action(async (opts) => {
|
|
338
|
+
await runGlobalInstall(
|
|
339
|
+
{ dryRun: opts.dryRun, yes: opts.yes, noMcp: opts.mcp === false, force: opts.force },
|
|
340
|
+
{ excludeBeads: true },
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function createInstallCommand(): Command {
|
|
346
|
+
const installCmd = new Command('install')
|
|
347
|
+
.description('Install Claude Code tools (skills, hooks, MCP servers)')
|
|
348
|
+
.argument('[target-selector]', 'Install targets: use "*" or "all" to skip interactive target selection')
|
|
349
|
+
.option('--dry-run', 'Preview changes without making any modifications', false)
|
|
350
|
+
.option('-y, --yes', 'Skip confirmation prompts', false)
|
|
351
|
+
.option('--prune', 'Remove items not in the canonical repository', false)
|
|
352
|
+
.option('--backport', 'Backport drifted local changes back to the repository', false)
|
|
353
|
+
.action(async (targetSelector, opts) => {
|
|
354
|
+
const { dryRun, yes, prune, backport } = opts;
|
|
355
|
+
const effectiveYes = yes || process.argv.includes('--yes') || process.argv.includes('-y');
|
|
356
|
+
const syncType: 'sync' | 'backport' = backport ? 'backport' : 'sync';
|
|
357
|
+
const actionLabel = backport ? 'backport' : 'install';
|
|
358
|
+
|
|
359
|
+
const repoRoot = await findRepoRoot();
|
|
360
|
+
const ctx = await getContext({
|
|
361
|
+
selector: targetSelector,
|
|
362
|
+
createMissingDirs: !dryRun,
|
|
363
|
+
});
|
|
364
|
+
const { targets, syncMode } = ctx;
|
|
365
|
+
let skipBeads = false;
|
|
366
|
+
|
|
367
|
+
if (!backport) {
|
|
368
|
+
console.log(t.bold('\n ⚙ beads + dolt (workflow enforcement backend)'));
|
|
369
|
+
console.log(t.muted(' beads is a git-backed issue tracker; dolt is its SQL+git storage backend.'));
|
|
370
|
+
console.log(t.muted(' Without them the gate hooks install but provide no enforcement.\n'));
|
|
371
|
+
|
|
372
|
+
const beadsOk = isBeadsInstalled();
|
|
373
|
+
const doltOk = isDoltInstalled();
|
|
374
|
+
|
|
375
|
+
if (beadsOk && doltOk) {
|
|
376
|
+
console.log(t.success(' ✓ beads + dolt already installed\n'));
|
|
377
|
+
} else {
|
|
378
|
+
const missing = [!beadsOk && 'bd', !doltOk && 'dolt'].filter(Boolean).join(', ');
|
|
379
|
+
|
|
380
|
+
let doInstall = effectiveYes;
|
|
381
|
+
if (!effectiveYes) {
|
|
382
|
+
const { install } = await prompts({
|
|
383
|
+
type: 'confirm',
|
|
384
|
+
name: 'install',
|
|
385
|
+
message: `Install beads + dolt? (${missing} not found) — required for workflow enforcement hooks`,
|
|
386
|
+
initial: true,
|
|
387
|
+
});
|
|
388
|
+
doInstall = install;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (doInstall) {
|
|
392
|
+
if (!beadsOk) {
|
|
393
|
+
console.log(t.muted('\n Installing @beads/bd...'));
|
|
394
|
+
spawnSync('npm', ['install', '-g', '@beads/bd'], { stdio: 'inherit' });
|
|
395
|
+
console.log(t.success(' ✓ bd installed'));
|
|
396
|
+
}
|
|
397
|
+
if (!doltOk) {
|
|
398
|
+
console.log(t.muted('\n Installing dolt...'));
|
|
399
|
+
if (process.platform === 'darwin') {
|
|
400
|
+
spawnSync('brew', ['install', 'dolt'], { stdio: 'inherit' });
|
|
401
|
+
} else {
|
|
402
|
+
spawnSync('sudo', ['bash', '-c',
|
|
403
|
+
'curl -L https://github.com/dolthub/dolt/releases/latest/download/install.sh | bash',
|
|
404
|
+
], { stdio: 'inherit' });
|
|
405
|
+
}
|
|
406
|
+
console.log(t.success(' ✓ dolt installed'));
|
|
407
|
+
}
|
|
408
|
+
console.log('');
|
|
409
|
+
} else {
|
|
410
|
+
console.log(t.muted(' ℹ Skipping beads gate hooks for this install run.\n'));
|
|
411
|
+
skipBeads = true;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Phase 1: Diff (concurrent via listr2)
|
|
417
|
+
const diffTasks = new Listr<DiffCtx>(
|
|
418
|
+
targets.map(target => ({
|
|
419
|
+
title: formatTargetLabel(target),
|
|
420
|
+
task: async (listCtx, task) => {
|
|
421
|
+
try {
|
|
422
|
+
let changeSet = await calculateDiff(repoRoot, target, prune);
|
|
423
|
+
if (skipBeads) {
|
|
424
|
+
changeSet = filterBeadsFromChangeSet(changeSet);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (syncType === 'sync' && !prune) {
|
|
428
|
+
const hasSettingsDiff =
|
|
429
|
+
changeSet.config.missing.includes('settings.json') ||
|
|
430
|
+
changeSet.config.outdated.includes('settings.json') ||
|
|
431
|
+
changeSet.config.drifted.includes('settings.json');
|
|
432
|
+
|
|
433
|
+
if (!hasSettingsDiff && await needsSettingsSync(repoRoot, target)) {
|
|
434
|
+
changeSet.config.outdated.push('settings.json');
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const totalChanges = Object.values(changeSet).reduce(
|
|
439
|
+
(sum, c: any) => sum + c.missing.length + c.outdated.length + c.drifted.length, 0,
|
|
440
|
+
);
|
|
441
|
+
task.title = `${formatTargetLabel(target)}${t.muted(` — ${totalChanges} change${totalChanges !== 1 ? 's' : ''}`)}`;
|
|
442
|
+
if (totalChanges > 0) {
|
|
443
|
+
listCtx.allChanges.push({ target, changeSet, totalChanges, skippedDrifted: [] });
|
|
444
|
+
}
|
|
445
|
+
} catch (err) {
|
|
446
|
+
if (err instanceof PruneModeReadError) {
|
|
447
|
+
task.title = `${formatTargetLabel(target)} ${kleur.red('(skipped — cannot read in prune mode)')}`;
|
|
448
|
+
} else {
|
|
449
|
+
throw err;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
})),
|
|
454
|
+
{ concurrent: true, exitOnError: false },
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
const diffCtx = await diffTasks.run({ allChanges: [] });
|
|
458
|
+
const allChanges = diffCtx.allChanges;
|
|
459
|
+
|
|
460
|
+
// MCP sync always runs regardless of file changes
|
|
461
|
+
if (!backport && !dryRun) {
|
|
462
|
+
const emptyChangeSet = {
|
|
463
|
+
skills: { missing: [] as string[], outdated: [] as string[], drifted: [] as string[], total: 0 },
|
|
464
|
+
hooks: { missing: [] as string[], outdated: [] as string[], drifted: [] as string[], total: 0 },
|
|
465
|
+
config: { missing: [] as string[], outdated: [] as string[], drifted: [] as string[], total: 0 },
|
|
466
|
+
commands: { missing: [] as string[], outdated: [] as string[], drifted: [] as string[], total: 0 },
|
|
467
|
+
'qwen-commands': { missing: [] as string[], outdated: [] as string[], drifted: [] as string[], total: 0 },
|
|
468
|
+
'antigravity-workflows': { missing: [] as string[], outdated: [] as string[], drifted: [] as string[], total: 0 },
|
|
469
|
+
};
|
|
470
|
+
for (const target of targets) {
|
|
471
|
+
console.log(t.bold(`\n ${sym.arrow} ${formatTargetLabel(target)}`));
|
|
472
|
+
await executeSync(repoRoot, target, emptyChangeSet, syncMode, 'sync', false);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (allChanges.length === 0) {
|
|
477
|
+
console.log('\n' + t.boldGreen('✓ Files are up-to-date') + '\n');
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Phase 2: Plan table
|
|
482
|
+
renderPlanTable(allChanges);
|
|
483
|
+
|
|
484
|
+
if (dryRun) {
|
|
485
|
+
console.log(t.accent('💡 Dry run — no changes written\n'));
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Phase 3: Confirmation
|
|
490
|
+
if (!effectiveYes) {
|
|
491
|
+
const totalChangesCount = allChanges.reduce((s, c) => s + c.totalChanges, 0);
|
|
492
|
+
const { confirm } = await prompts({
|
|
493
|
+
type: 'confirm',
|
|
494
|
+
name: 'confirm',
|
|
495
|
+
message: `Proceed with ${actionLabel} (${totalChangesCount} total changes)?`,
|
|
496
|
+
initial: true,
|
|
497
|
+
});
|
|
498
|
+
if (!confirm) {
|
|
499
|
+
console.log(t.muted(' Install cancelled.\n'));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Phase 4: Execute
|
|
505
|
+
let totalCount = 0;
|
|
506
|
+
|
|
507
|
+
for (const { target, changeSet, skippedDrifted } of allChanges) {
|
|
508
|
+
console.log(t.bold(`\n ${sym.arrow} ${formatTargetLabel(target)}`));
|
|
509
|
+
|
|
510
|
+
const count = await executeSync(repoRoot, target, changeSet, syncMode, syncType, dryRun);
|
|
511
|
+
totalCount += count;
|
|
512
|
+
|
|
513
|
+
for (const [category, cat] of Object.entries(changeSet)) {
|
|
514
|
+
const c = cat as any;
|
|
515
|
+
if (c.drifted.length > 0 && syncType === 'sync') {
|
|
516
|
+
skippedDrifted.push(...c.drifted.map((item: string) => `${category}/${item}`));
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
console.log(t.success(` ${sym.ok} ${count} item${count !== 1 ? 's' : ''} installed`));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Phase 5: Summary card
|
|
524
|
+
const allSkipped = allChanges.flatMap(c => c.skippedDrifted);
|
|
525
|
+
await renderSummaryCard(allChanges, totalCount, allSkipped, dryRun);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// Add subcommands
|
|
529
|
+
installCmd.addCommand(createInstallAllCommand());
|
|
530
|
+
installCmd.addCommand(createInstallBasicCommand());
|
|
531
|
+
installCmd.addCommand(createInstallProjectCommand());
|
|
532
|
+
|
|
533
|
+
return installCmd;
|
|
534
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import kleur from 'kleur';
|
|
3
|
+
import { resetContext } from '../core/context.js';
|
|
4
|
+
|
|
5
|
+
export function createResetCommand(): Command {
|
|
6
|
+
return new Command('reset')
|
|
7
|
+
.description('Reset CLI configuration (clears saved sync mode and preferences)')
|
|
8
|
+
.action(() => {
|
|
9
|
+
resetContext();
|
|
10
|
+
console.log(kleur.green('✓ Configuration reset. Run sync again to reconfigure.'));
|
|
11
|
+
});
|
|
12
|
+
}
|