dxai-cli 1.0.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/LICENSE +21 -0
- package/README.md +172 -0
- package/bin/cli.js +272 -0
- package/package.json +64 -0
- package/src/auto-update.js +106 -0
- package/src/branding.js +80 -0
- package/src/cleanup.js +615 -0
- package/src/config-remover.js +325 -0
- package/src/config-writer.js +781 -0
- package/src/detect-project.js +316 -0
- package/src/detect.js +587 -0
- package/src/fs-atomic.js +35 -0
- package/src/handshake.js +123 -0
- package/src/index.js +966 -0
- package/src/inspect.js +283 -0
- package/src/manifest.js +179 -0
- package/src/mcp-cmd.js +282 -0
- package/src/net.js +72 -0
- package/src/profile.js +139 -0
- package/src/registry/automation-tools.js +6 -0
- package/src/registry/data/automation-tools.json +37 -0
- package/src/registry/data/mcp-servers.json +451 -0
- package/src/registry/data/skills.json +132 -0
- package/src/registry/loader.js +102 -0
- package/src/registry/mcp-registry.js +292 -0
- package/src/registry/mcp-servers.js +72 -0
- package/src/registry/skills.js +10 -0
- package/src/registry/stacks.js +769 -0
- package/src/registry/validate.js +209 -0
- package/src/rollback.js +182 -0
- package/src/runtime.js +40 -0
- package/src/select.js +72 -0
- package/src/update.js +126 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,966 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
printBanner, sectionHeader, successMsg, warnMsg,
|
|
9
|
+
errorMsg, infoMsg, theme, quiet, startSpinner, reportMcpResults,
|
|
10
|
+
} from './branding.js';
|
|
11
|
+
import {
|
|
12
|
+
detectOS, checkPrerequisites, detectAgents,
|
|
13
|
+
printDetectionResults, AGENT_DEFINITIONS, INSTALL_COMMANDS,
|
|
14
|
+
detectAutomationTools,
|
|
15
|
+
} from './detect.js';
|
|
16
|
+
import { MCP_SERVERS, MCP_CATEGORIES } from './registry/mcp-servers.js';
|
|
17
|
+
import { SKILLS, SKILL_CATEGORIES } from './registry/skills.js';
|
|
18
|
+
import { AUTOMATION_TOOLS, AUTOMATION_TOOL_CATEGORIES } from './registry/automation-tools.js';
|
|
19
|
+
import { TECH_STACKS, CURSOR_RULES, CURSOR_COMMANDS } from './registry/stacks.js';
|
|
20
|
+
import { detectProject } from './detect-project.js';
|
|
21
|
+
import {
|
|
22
|
+
writeMcpConfigs, writeProjectMcpConfigs, writeCursorRules, writeCursorCommands,
|
|
23
|
+
writeCursorIgnore, writeProjectInstructions, installSkills,
|
|
24
|
+
writeGitattributes, writeEditorconfig, writeAgentsMd,
|
|
25
|
+
previewMcpConfigs,
|
|
26
|
+
} from './config-writer.js';
|
|
27
|
+
import { normalizeOptions } from './runtime.js';
|
|
28
|
+
import { resolveSelection, buildCatalogChoices, confirm } from './select.js';
|
|
29
|
+
import { parseSafeCommand } from './registry/validate.js';
|
|
30
|
+
import { maybeRefreshCatalog } from './auto-update.js';
|
|
31
|
+
import { resolveProfile, readProfile, mergeWithProfile, saveProfile, listProfiles } from './profile.js';
|
|
32
|
+
import {
|
|
33
|
+
recordSystemMcp, recordSystemSkills, recordSystemTools, recordProjectMcp, recordProjectSkills, recordProjectFiles,
|
|
34
|
+
} from './manifest.js';
|
|
35
|
+
|
|
36
|
+
// Count per-step failures captured inside a flow's result maps, so a partially
|
|
37
|
+
// failed setup can exit non-zero instead of silently reporting success.
|
|
38
|
+
function countResultErrors({ mcpResults, toolResults, skillResults } = {}) {
|
|
39
|
+
let n = 0;
|
|
40
|
+
for (const r of Object.values(mcpResults || {})) n += (r.errors || []).length;
|
|
41
|
+
n += (toolResults?.errors || []).length;
|
|
42
|
+
n += (skillResults?.errors || []).length;
|
|
43
|
+
return n;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// In non-interactive mode, fall back to defaults instead of prompting.
|
|
47
|
+
// Exported so the fast-path `dxai add` command can reuse the same input flow.
|
|
48
|
+
export async function collectMcpInputs(selectedMcpIds, mcpRegistry, runtime) {
|
|
49
|
+
const inputs = {};
|
|
50
|
+
for (const id of selectedMcpIds) {
|
|
51
|
+
const server = mcpRegistry.find((s) => s.id === id);
|
|
52
|
+
if (!server?.requiresInput) continue;
|
|
53
|
+
|
|
54
|
+
if (runtime.nonInteractive) {
|
|
55
|
+
const auto = {};
|
|
56
|
+
for (const [key, def] of Object.entries(server.requiresInput)) {
|
|
57
|
+
if (def.default === undefined || def.default === null) {
|
|
58
|
+
throw new Error(`MCP server "${id}" requires input "${key}" but no default is set; cannot run non-interactively without a value.`);
|
|
59
|
+
}
|
|
60
|
+
auto[key] = def.default;
|
|
61
|
+
}
|
|
62
|
+
inputs[id] = auto;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const questions = Object.entries(server.requiresInput).map(([key, def]) => ({
|
|
67
|
+
type: 'input',
|
|
68
|
+
name: key,
|
|
69
|
+
message: `[${server.name}] ${def.prompt}`,
|
|
70
|
+
default: def.default,
|
|
71
|
+
validate: (v) => (v && v.trim().length > 0) || 'Required.',
|
|
72
|
+
}));
|
|
73
|
+
inputs[id] = await inquirer.prompt(questions);
|
|
74
|
+
}
|
|
75
|
+
return inputs;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── MCP servers — shared selection (system + project flows) ──
|
|
79
|
+
function mcpServersFor(selectedAgentIds) {
|
|
80
|
+
return MCP_SERVERS.filter((s) => selectedAgentIds.some((aid) => s.configs[aid]));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function recommendedMcpIds(selectedAgentIds) {
|
|
84
|
+
return mcpServersFor(selectedAgentIds).filter((s) => s.recommended).map((s) => s.id);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function promptMcpServers(selectedAgentIds, message) {
|
|
88
|
+
const choices = buildCatalogChoices(MCP_CATEGORIES, mcpServersFor(selectedAgentIds), {
|
|
89
|
+
decorate: (s) => ({ note: s.requiresEnv ? chalk.dim(' (needs API key)') : '' }),
|
|
90
|
+
});
|
|
91
|
+
const { picked } = await inquirer.prompt([
|
|
92
|
+
{ type: 'checkbox', name: 'picked', message, choices, pageSize: 25, loop: false },
|
|
93
|
+
]);
|
|
94
|
+
return picked;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Shared: Banner + Detection + Agent Selection ──
|
|
98
|
+
async function sharedSetup(runtime) {
|
|
99
|
+
const osInfo = detectOS();
|
|
100
|
+
const prereqs = checkPrerequisites();
|
|
101
|
+
const agents = detectAgents(osInfo.home);
|
|
102
|
+
|
|
103
|
+
quiet(runtime, () => {
|
|
104
|
+
sectionHeader('Environment Detection');
|
|
105
|
+
printDetectionResults(osInfo, prereqs, agents);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (!prereqs.node.installed) {
|
|
109
|
+
if (runtime.json) {
|
|
110
|
+
throw new Error('Node.js is required.');
|
|
111
|
+
}
|
|
112
|
+
console.log();
|
|
113
|
+
errorMsg('Node.js is required. Install it from https://nodejs.org');
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Agent selection — flag, then default to detected, then prompt.
|
|
118
|
+
const selectedAgentIds = await resolveSelection({
|
|
119
|
+
flag: runtime.agents && runtime.agents.length > 0 ? runtime.agents : undefined,
|
|
120
|
+
knownIds: AGENT_DEFINITIONS.map((a) => a.id),
|
|
121
|
+
label: 'agent ID',
|
|
122
|
+
requireNonEmpty: true,
|
|
123
|
+
nonInteractive: runtime.nonInteractive,
|
|
124
|
+
defaults: () => {
|
|
125
|
+
const detected = agents.filter((a) => a.installed).map((a) => a.id);
|
|
126
|
+
if (detected.length === 0) {
|
|
127
|
+
throw new Error('No agents detected. Pass --agents to choose explicitly.');
|
|
128
|
+
}
|
|
129
|
+
return detected;
|
|
130
|
+
},
|
|
131
|
+
prompt: async () => {
|
|
132
|
+
console.log();
|
|
133
|
+
sectionHeader('Select Your AI Tools');
|
|
134
|
+
console.log();
|
|
135
|
+
|
|
136
|
+
const agentChoices = AGENT_DEFINITIONS.map((def) => {
|
|
137
|
+
const detected = agents.find((a) => a.id === def.id);
|
|
138
|
+
const status = detected?.installed ? chalk.green(' (detected)') : '';
|
|
139
|
+
const notice = def.notice ? chalk.yellow(` ⚠ ${def.notice}`) : '';
|
|
140
|
+
return {
|
|
141
|
+
name: `${def.name}${status} — ${def.description}${notice}`,
|
|
142
|
+
value: def.id,
|
|
143
|
+
checked: detected?.installed || false,
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const { picked } = await inquirer.prompt([
|
|
148
|
+
{
|
|
149
|
+
type: 'checkbox',
|
|
150
|
+
name: 'picked',
|
|
151
|
+
message: 'Which AI tools do you use? (Space to toggle, Enter to confirm)',
|
|
152
|
+
choices: agentChoices,
|
|
153
|
+
loop: false,
|
|
154
|
+
validate: (ans) => ans.length > 0 || 'Please select at least one tool.',
|
|
155
|
+
},
|
|
156
|
+
]);
|
|
157
|
+
return picked;
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const selectedAgents = AGENT_DEFINITIONS.filter((a) => selectedAgentIds.includes(a.id));
|
|
162
|
+
|
|
163
|
+
// Offer install commands for missing agents — interactive only.
|
|
164
|
+
if (!runtime.nonInteractive) {
|
|
165
|
+
const missingAgents = selectedAgents.filter((def) => {
|
|
166
|
+
const detected = agents.find((a) => a.id === def.id);
|
|
167
|
+
return !detected?.installed;
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (missingAgents.length > 0) {
|
|
171
|
+
console.log();
|
|
172
|
+
warnMsg(`Not installed: ${missingAgents.map((a) => a.name).join(', ')}`);
|
|
173
|
+
|
|
174
|
+
if (await confirm('Would you like install commands for the missing tools?')) {
|
|
175
|
+
for (const agent of missingAgents) {
|
|
176
|
+
const cmd = INSTALL_COMMANDS[agent.id]?.[osInfo.name] || 'See official documentation';
|
|
177
|
+
console.log(theme.dim(` ${agent.name}: `) + theme.accent(cmd));
|
|
178
|
+
}
|
|
179
|
+
console.log();
|
|
180
|
+
infoMsg('Install them and re-run dxai, or continue to configure anyway.');
|
|
181
|
+
|
|
182
|
+
if (!(await confirm('Continue with setup for selected tools?'))) {
|
|
183
|
+
console.log();
|
|
184
|
+
infoMsg('Run dxai again after installing your tools. Bye!');
|
|
185
|
+
process.exit(0);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { osInfo, prereqs, agents, selectedAgents, selectedAgentIds };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── Skills — shared selection + install (used by system and project modes) ──
|
|
195
|
+
// Skills are downloaded into a project-level directory (.agents/skills, read
|
|
196
|
+
// natively by Codex, Cursor, Devin and Antigravity; mirrored to .claude/skills
|
|
197
|
+
// for Claude Code), so they're meaningful in both the system and project flows. These helpers keep the two call sites consistent.
|
|
198
|
+
async function selectSkills(runtime, headerLabel, { recommendByDefault = true } = {}) {
|
|
199
|
+
return resolveSelection({
|
|
200
|
+
flag: runtime.skills,
|
|
201
|
+
knownIds: SKILLS.map((s) => s.id),
|
|
202
|
+
label: 'skill ID',
|
|
203
|
+
nonInteractive: runtime.nonInteractive,
|
|
204
|
+
// No explicit --skills: system mode seeds the recommended set; project mode
|
|
205
|
+
// stays empty so `dxai project --yes` doesn't trigger unexpected downloads.
|
|
206
|
+
defaults: () => (recommendByDefault ? SKILLS.filter((s) => s.recommended).map((s) => s.id) : []),
|
|
207
|
+
prompt: async () => {
|
|
208
|
+
quiet(runtime, () => {
|
|
209
|
+
console.log();
|
|
210
|
+
sectionHeader(headerLabel);
|
|
211
|
+
console.log();
|
|
212
|
+
infoMsg('Skills are downloaded into this project\'s .agents/skills folder (mirrored to .claude/skills for Claude Code)');
|
|
213
|
+
console.log();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const skillChoices = buildCatalogChoices(SKILL_CATEGORIES, SKILLS);
|
|
217
|
+
const { picked } = await inquirer.prompt([
|
|
218
|
+
{
|
|
219
|
+
type: 'checkbox',
|
|
220
|
+
name: 'picked',
|
|
221
|
+
message: 'Select agent skills to install:',
|
|
222
|
+
choices: skillChoices,
|
|
223
|
+
pageSize: 20,
|
|
224
|
+
loop: false,
|
|
225
|
+
},
|
|
226
|
+
]);
|
|
227
|
+
return picked;
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Install the chosen skills and report results. `record` persists them to the
|
|
233
|
+
// appropriate manifest (system vs project). Mutates `result.skillResults`.
|
|
234
|
+
async function installAndReportSkills(selectedSkillIds, selectedAgents, runtime, result, record) {
|
|
235
|
+
if (selectedSkillIds.length === 0) return;
|
|
236
|
+
const spinner = startSpinner(runtime, 'Installing agent skills...');
|
|
237
|
+
try {
|
|
238
|
+
const skillResults = await installSkills(selectedSkillIds, SKILLS, selectedAgents);
|
|
239
|
+
spinner?.stop();
|
|
240
|
+
result.skillResults = skillResults;
|
|
241
|
+
record(skillResults);
|
|
242
|
+
|
|
243
|
+
quiet(runtime, () => {
|
|
244
|
+
if (skillResults.installed.length > 0) {
|
|
245
|
+
successMsg(`Skills installed: ${skillResults.installed.join(', ')}`);
|
|
246
|
+
infoMsg(`Skills directory: ${skillResults.directory}`);
|
|
247
|
+
}
|
|
248
|
+
for (const err of skillResults.errors) {
|
|
249
|
+
warnMsg(`Skill "${err.name}": ${err.error}`);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
} catch (err) {
|
|
253
|
+
spinner?.stop();
|
|
254
|
+
result.errorCount = (result.errorCount || 0) + 1;
|
|
255
|
+
if (runtime.json) throw err;
|
|
256
|
+
errorMsg(`Skills installation failed: ${err.message}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── System Mode — global/user-level configs ──
|
|
261
|
+
async function runSystem(ctx, runtime) {
|
|
262
|
+
const { osInfo, selectedAgents, selectedAgentIds } = ctx;
|
|
263
|
+
|
|
264
|
+
// ── MCP Server Selection ──
|
|
265
|
+
const selectedMcpIds = await resolveSelection({
|
|
266
|
+
flag: runtime.mcp,
|
|
267
|
+
knownIds: MCP_SERVERS.map((s) => s.id),
|
|
268
|
+
label: 'MCP server ID',
|
|
269
|
+
nonInteractive: runtime.nonInteractive,
|
|
270
|
+
defaults: () => recommendedMcpIds(selectedAgentIds),
|
|
271
|
+
prompt: async () => {
|
|
272
|
+
quiet(runtime, () => {
|
|
273
|
+
console.log();
|
|
274
|
+
sectionHeader('Select MCP Servers (Global)');
|
|
275
|
+
console.log();
|
|
276
|
+
infoMsg('★ = recommended • Servers are configured globally for all your selected tools');
|
|
277
|
+
console.log();
|
|
278
|
+
});
|
|
279
|
+
return promptMcpServers(selectedAgentIds, 'Select MCP servers to install globally:');
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// ── Suggested Automation Tools ──
|
|
284
|
+
const selectedToolIds = await resolveSelection({
|
|
285
|
+
flag: runtime.tools,
|
|
286
|
+
knownIds: AUTOMATION_TOOLS.map((t) => t.id),
|
|
287
|
+
label: 'automation tool ID',
|
|
288
|
+
nonInteractive: runtime.nonInteractive,
|
|
289
|
+
defaults: () => AUTOMATION_TOOLS.filter((t) => t.recommended).map((t) => t.id),
|
|
290
|
+
prompt: async () => {
|
|
291
|
+
const detectedTools = detectAutomationTools(AUTOMATION_TOOLS);
|
|
292
|
+
|
|
293
|
+
quiet(runtime, () => {
|
|
294
|
+
console.log();
|
|
295
|
+
sectionHeader('Suggested Automation Tools');
|
|
296
|
+
console.log();
|
|
297
|
+
infoMsg('★ = suggested • Standalone CLIs that give your AI agents browser & device control');
|
|
298
|
+
console.log();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const toolChoices = buildCatalogChoices(AUTOMATION_TOOL_CATEGORIES, detectedTools, {
|
|
302
|
+
decorate: (t) => ({ status: t.installed ? chalk.green(' (detected)') : '' }),
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const { picked } = await inquirer.prompt([
|
|
306
|
+
{
|
|
307
|
+
type: 'checkbox',
|
|
308
|
+
name: 'picked',
|
|
309
|
+
message: 'Select automation tools to install:',
|
|
310
|
+
choices: toolChoices,
|
|
311
|
+
loop: false,
|
|
312
|
+
},
|
|
313
|
+
]);
|
|
314
|
+
return picked;
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// ── Agent Skills Selection ──
|
|
319
|
+
// Note: skills always land in the *current project's* skills folder (that's
|
|
320
|
+
// where the agents read them from) — the header says so rather than "Global".
|
|
321
|
+
const selectedSkillIds = await selectSkills(runtime, 'Select Agent Skills');
|
|
322
|
+
|
|
323
|
+
const mcpInputs = await collectMcpInputs(selectedMcpIds, MCP_SERVERS, runtime);
|
|
324
|
+
|
|
325
|
+
// ── Summary & Confirmation ──
|
|
326
|
+
quiet(runtime, () => {
|
|
327
|
+
console.log();
|
|
328
|
+
sectionHeader('System Setup Summary');
|
|
329
|
+
console.log();
|
|
330
|
+
console.log(theme.label(' Tools: ') + selectedAgents.map((a) => a.name).join(', '));
|
|
331
|
+
console.log(theme.label(' MCP: ') + (selectedMcpIds.length > 0 ? selectedMcpIds.join(', ') : 'none'));
|
|
332
|
+
console.log(theme.label(' Automation: ') + (selectedToolIds.length > 0 ? selectedToolIds.join(', ') : 'none'));
|
|
333
|
+
console.log(theme.label(' Skills: ') + (selectedSkillIds.length > 0 ? selectedSkillIds.join(', ') : 'none'));
|
|
334
|
+
console.log();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
if (!runtime.nonInteractive && !(await confirm('Proceed with system setup?'))) {
|
|
338
|
+
infoMsg('Setup cancelled. Run dxai again anytime.');
|
|
339
|
+
process.exit(0);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ── Dry-run short-circuit ──
|
|
343
|
+
if (runtime.dryRun) {
|
|
344
|
+
const previews = selectedMcpIds.length > 0
|
|
345
|
+
? previewMcpConfigs(selectedAgents, selectedMcpIds, MCP_SERVERS, mcpInputs)
|
|
346
|
+
: {};
|
|
347
|
+
|
|
348
|
+
quiet(runtime, () => {
|
|
349
|
+
sectionHeader('Dry run — no changes written');
|
|
350
|
+
console.log();
|
|
351
|
+
for (const p of Object.values(previews)) {
|
|
352
|
+
const tag = p.exists ? theme.dim('(merge)') : theme.dim('(create)');
|
|
353
|
+
console.log(` ${theme.label(p.agent)} ${tag} → ${p.path}`);
|
|
354
|
+
if (p.wouldAdd.length) console.log(` ${theme.success('+ would add:')} ${p.wouldAdd.join(', ')}`);
|
|
355
|
+
if (p.wouldSkip.length) console.log(` ${theme.dim('· already present:')} ${p.wouldSkip.join(', ')}`);
|
|
356
|
+
}
|
|
357
|
+
if (selectedToolIds.length > 0) {
|
|
358
|
+
const detectedTools = detectAutomationTools(AUTOMATION_TOOLS);
|
|
359
|
+
console.log();
|
|
360
|
+
infoMsg('Automation tools:');
|
|
361
|
+
for (const toolId of selectedToolIds) {
|
|
362
|
+
const tool = AUTOMATION_TOOLS.find((t) => t.id === toolId);
|
|
363
|
+
const detected = detectedTools.find((t) => t.id === toolId);
|
|
364
|
+
const status = detected?.installed ? theme.dim('(already installed)') : theme.success('(will install)');
|
|
365
|
+
const cmd = tool.installCommand[osInfo.name] || tool.installCommand.macOS;
|
|
366
|
+
console.log(` ${tool.name} ${status} — ${theme.dim(cmd)}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (selectedSkillIds.length > 0) {
|
|
370
|
+
console.log();
|
|
371
|
+
infoMsg(`Would install skills: ${selectedSkillIds.join(', ')}`);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
selectedMcpIds,
|
|
377
|
+
selectedSkillIds,
|
|
378
|
+
selectedToolIds,
|
|
379
|
+
needsEnv: [],
|
|
380
|
+
mcpResults: null,
|
|
381
|
+
skillResults: null,
|
|
382
|
+
toolResults: null,
|
|
383
|
+
dryResult: { mode: 'system', dryRun: true, agents: selectedAgentIds, mcp: selectedMcpIds, skills: selectedSkillIds, tools: selectedToolIds, previews },
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ── Execute ──
|
|
388
|
+
quiet(runtime, () => {
|
|
389
|
+
console.log();
|
|
390
|
+
sectionHeader('Configuring (System)');
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const result = { mcpResults: null, skillResults: null, toolResults: null, errorCount: 0 };
|
|
394
|
+
|
|
395
|
+
if (selectedMcpIds.length > 0) {
|
|
396
|
+
const spinner = startSpinner(runtime, 'Writing global MCP server configs...');
|
|
397
|
+
try {
|
|
398
|
+
const mcpResults = writeMcpConfigs(selectedAgents, selectedMcpIds, MCP_SERVERS, mcpInputs);
|
|
399
|
+
spinner?.stop();
|
|
400
|
+
result.mcpResults = mcpResults;
|
|
401
|
+
recordSystemMcp(mcpResults);
|
|
402
|
+
|
|
403
|
+
quiet(runtime, () => reportMcpResults(mcpResults));
|
|
404
|
+
} catch (err) {
|
|
405
|
+
spinner?.stop();
|
|
406
|
+
result.errorCount++;
|
|
407
|
+
if (runtime.json) throw err;
|
|
408
|
+
errorMsg(`MCP config failed: ${err.message}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (selectedToolIds.length > 0) {
|
|
413
|
+
const detectedTools = detectAutomationTools(AUTOMATION_TOOLS);
|
|
414
|
+
const toolResults = { installed: [], skipped: [], errors: [] };
|
|
415
|
+
|
|
416
|
+
const spinner = startSpinner(runtime, 'Installing automation tools...');
|
|
417
|
+
|
|
418
|
+
for (const toolId of selectedToolIds) {
|
|
419
|
+
const tool = AUTOMATION_TOOLS.find((t) => t.id === toolId);
|
|
420
|
+
const detected = detectedTools.find((t) => t.id === toolId);
|
|
421
|
+
|
|
422
|
+
if (detected?.installed) {
|
|
423
|
+
toolResults.skipped.push(toolId);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const installCmd = tool.installCommand[osInfo.name] || tool.installCommand.macOS;
|
|
428
|
+
try {
|
|
429
|
+
// installCmd is registry data (untrusted). Parse it to argv and run
|
|
430
|
+
// without a shell so it can never be more than an allowlisted binary
|
|
431
|
+
// plus plain arguments — no metacharacter injection.
|
|
432
|
+
const { command, args } = parseSafeCommand(installCmd);
|
|
433
|
+
execFileSync(command, args, { stdio: 'pipe', timeout: 60000 });
|
|
434
|
+
toolResults.installed.push(toolId);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
toolResults.errors.push({ id: toolId, name: tool.name, error: err.message, command: installCmd });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
spinner?.stop();
|
|
441
|
+
result.toolResults = toolResults;
|
|
442
|
+
recordSystemTools(toolResults);
|
|
443
|
+
|
|
444
|
+
quiet(runtime, () => {
|
|
445
|
+
if (toolResults.installed.length > 0) {
|
|
446
|
+
successMsg(`Automation tools installed: ${toolResults.installed.join(', ')}`);
|
|
447
|
+
}
|
|
448
|
+
if (toolResults.skipped.length > 0) {
|
|
449
|
+
infoMsg(`Already installed: ${toolResults.skipped.join(', ')}`);
|
|
450
|
+
}
|
|
451
|
+
for (const err of toolResults.errors) {
|
|
452
|
+
warnMsg(`${err.name}: install failed. Run manually: ${err.command}`);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
await installAndReportSkills(selectedSkillIds, selectedAgents, runtime, result, recordSystemSkills);
|
|
458
|
+
|
|
459
|
+
const selectedServers = selectedMcpIds.map((id) => MCP_SERVERS.find((s) => s.id === id)).filter(Boolean);
|
|
460
|
+
const needsEnv = selectedServers.filter((s) => s.requiresEnv);
|
|
461
|
+
|
|
462
|
+
return { selectedMcpIds, selectedSkillIds, selectedToolIds, needsEnv, ...result };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ── Project Mode — cwd project configs ──
|
|
466
|
+
async function runProject(ctx, runtime, { handleSkills = false } = {}) {
|
|
467
|
+
const { selectedAgents, selectedAgentIds } = ctx;
|
|
468
|
+
const hasCursor = selectedAgentIds.includes('cursor');
|
|
469
|
+
|
|
470
|
+
// ── Project Detection ──
|
|
471
|
+
const profile = detectProject(process.cwd());
|
|
472
|
+
|
|
473
|
+
quiet(runtime, () => {
|
|
474
|
+
console.log();
|
|
475
|
+
if (profile.exists) {
|
|
476
|
+
sectionHeader('Project Detected');
|
|
477
|
+
console.log();
|
|
478
|
+
const maturityDetail = profile.git.isRepo
|
|
479
|
+
? `${profile.maturity} (${profile.git.commitCount} commits, ${profile.git.ageInDays} days)`
|
|
480
|
+
: profile.maturity;
|
|
481
|
+
successMsg(`Maturity: ${maturityDetail}`);
|
|
482
|
+
if (profile.detectedStacks.length > 0) successMsg(`Detected stacks: ${profile.detectedStacks.join(', ')}`);
|
|
483
|
+
if (profile.tooling.linter) successMsg(`Linter: ${profile.tooling.linter.type}`);
|
|
484
|
+
if (profile.tooling.formatter) successMsg(`Formatter: ${profile.tooling.formatter.type}`);
|
|
485
|
+
if (profile.tooling.testFramework) successMsg(`Tests: ${profile.tooling.testFramework.type}`);
|
|
486
|
+
if (profile.tooling.ci) successMsg(`CI: ${profile.tooling.ci.type}`);
|
|
487
|
+
if (profile.monorepo.detected) successMsg(`Monorepo: ${profile.monorepo.type}`);
|
|
488
|
+
const cmdEntries = Object.entries(profile.commands);
|
|
489
|
+
if (cmdEntries.length > 0) {
|
|
490
|
+
infoMsg(`Commands: ${cmdEntries.map(([k, v]) => `${k}="${v}"`).join(', ')}`);
|
|
491
|
+
}
|
|
492
|
+
} else {
|
|
493
|
+
sectionHeader('New Project');
|
|
494
|
+
console.log();
|
|
495
|
+
infoMsg('No project files detected — configuring for a greenfield project.');
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// ── Tech Stack Selection ──
|
|
500
|
+
const selectedStackIds = await resolveSelection({
|
|
501
|
+
flag: runtime.stack,
|
|
502
|
+
knownIds: TECH_STACKS.map((s) => s.id),
|
|
503
|
+
label: 'stack ID',
|
|
504
|
+
requireNonEmpty: true,
|
|
505
|
+
nonInteractive: runtime.nonInteractive,
|
|
506
|
+
defaults: () => (profile.detectedStacks.length > 0 ? profile.detectedStacks : ['node']),
|
|
507
|
+
prompt: async () => {
|
|
508
|
+
quiet(runtime, () => {
|
|
509
|
+
console.log();
|
|
510
|
+
sectionHeader('Select Your Tech Stack');
|
|
511
|
+
console.log();
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
const { picked } = await inquirer.prompt([
|
|
515
|
+
{
|
|
516
|
+
type: 'checkbox',
|
|
517
|
+
name: 'picked',
|
|
518
|
+
message: 'What do you work with? (Space to toggle)',
|
|
519
|
+
choices: TECH_STACKS.map((s) => ({
|
|
520
|
+
name: s.label + (profile.detectedStacks.includes(s.id) ? chalk.green(' (detected)') : ''),
|
|
521
|
+
value: s.id,
|
|
522
|
+
checked: profile.detectedStacks.includes(s.id),
|
|
523
|
+
})),
|
|
524
|
+
loop: false,
|
|
525
|
+
validate: (ans) => ans.length > 0 || 'Please select at least one stack.',
|
|
526
|
+
},
|
|
527
|
+
]);
|
|
528
|
+
return picked;
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// ── Project Features Checklist ──
|
|
533
|
+
const allFeatures = [];
|
|
534
|
+
if (hasCursor) {
|
|
535
|
+
allFeatures.push('cursor-rules', 'cursor-commands', 'cursor-ignore');
|
|
536
|
+
}
|
|
537
|
+
const agentsWithProjectMcp = selectedAgents.filter((a) => typeof a.projectMcpPath === 'function');
|
|
538
|
+
if (agentsWithProjectMcp.length > 0) allFeatures.push('project-mcp');
|
|
539
|
+
allFeatures.push('agent-instructions', 'agents-md', 'gitattributes', 'editorconfig');
|
|
540
|
+
|
|
541
|
+
const selectedFeatures = await resolveSelection({
|
|
542
|
+
flag: runtime.features,
|
|
543
|
+
knownIds: allFeatures,
|
|
544
|
+
label: 'feature ID',
|
|
545
|
+
nonInteractive: runtime.nonInteractive,
|
|
546
|
+
defaults: () => [...allFeatures],
|
|
547
|
+
prompt: async () => {
|
|
548
|
+
quiet(runtime, () => {
|
|
549
|
+
console.log();
|
|
550
|
+
sectionHeader('Project Configuration');
|
|
551
|
+
console.log();
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
const featureChoices = [];
|
|
555
|
+
if (hasCursor) {
|
|
556
|
+
featureChoices.push(
|
|
557
|
+
{ name: 'Cursor Rules — stack-specific .mdc rule files', value: 'cursor-rules', checked: true },
|
|
558
|
+
{ name: 'Cursor Commands — /pr, /fix-issue, /review, /test-all, /refactor (as .cursor/skills)', value: 'cursor-commands', checked: true },
|
|
559
|
+
{ name: '.cursorignore — exclude noise from AI context', value: 'cursor-ignore', checked: true },
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
if (agentsWithProjectMcp.length > 0) {
|
|
563
|
+
featureChoices.push({ name: 'Project-level MCP — .vscode/mcp.json, .cursor/mcp.json', value: 'project-mcp', checked: true });
|
|
564
|
+
}
|
|
565
|
+
featureChoices.push(
|
|
566
|
+
{ name: 'CLAUDE.md / GEMINI.md — agent instruction files', value: 'agent-instructions', checked: true },
|
|
567
|
+
{ name: 'AGENTS.md — agent rules + project context (Codex CLI & other AGENTS.md-aware agents)', value: 'agents-md', checked: true },
|
|
568
|
+
{ name: '.gitattributes — AI-friendly git config', value: 'gitattributes', checked: true },
|
|
569
|
+
{ name: '.editorconfig — consistent formatting', value: 'editorconfig', checked: true },
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
const { picked } = await inquirer.prompt([
|
|
573
|
+
{
|
|
574
|
+
type: 'checkbox',
|
|
575
|
+
name: 'picked',
|
|
576
|
+
message: 'Which project configs should we set up? (Space to toggle)',
|
|
577
|
+
choices: featureChoices,
|
|
578
|
+
loop: false,
|
|
579
|
+
},
|
|
580
|
+
]);
|
|
581
|
+
return picked;
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// ── Project-level MCP servers ──
|
|
586
|
+
let projectMcpIds = [];
|
|
587
|
+
if (selectedFeatures.includes('project-mcp')) {
|
|
588
|
+
projectMcpIds = await resolveSelection({
|
|
589
|
+
flag: runtime.mcp,
|
|
590
|
+
knownIds: MCP_SERVERS.map((s) => s.id),
|
|
591
|
+
label: 'MCP server ID',
|
|
592
|
+
nonInteractive: runtime.nonInteractive,
|
|
593
|
+
defaults: () => recommendedMcpIds(selectedAgentIds),
|
|
594
|
+
prompt: async () => {
|
|
595
|
+
console.log();
|
|
596
|
+
infoMsg('Select MCP servers for project-level config (these go into the repo):');
|
|
597
|
+
console.log();
|
|
598
|
+
return promptMcpServers(selectedAgentIds, 'Select MCP servers for project config:');
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const projectMcpInputs = await collectMcpInputs(projectMcpIds, MCP_SERVERS, runtime);
|
|
604
|
+
|
|
605
|
+
// ── Agent Skills Selection ──
|
|
606
|
+
// Skills install into a project-level directory (.agents/skills, read natively
|
|
607
|
+
// by Codex), so they belong to project setup. When running in "both" mode the
|
|
608
|
+
// system flow already handles skills, so we only prompt/install here when this
|
|
609
|
+
// flow owns them (project-only mode).
|
|
610
|
+
let selectedSkillIds = [];
|
|
611
|
+
if (handleSkills) {
|
|
612
|
+
selectedSkillIds = await selectSkills(runtime, 'Select Agent Skills', { recommendByDefault: false });
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// ── Summary & Confirmation ──
|
|
616
|
+
quiet(runtime, () => {
|
|
617
|
+
console.log();
|
|
618
|
+
sectionHeader('Project Setup Summary');
|
|
619
|
+
console.log();
|
|
620
|
+
console.log(theme.label(' Tools: ') + selectedAgents.map((a) => a.name).join(', '));
|
|
621
|
+
console.log(theme.label(' Stack: ') + selectedStackIds.join(', '));
|
|
622
|
+
console.log(theme.label(' Features: ') + (selectedFeatures.length > 0 ? selectedFeatures.join(', ') : 'none'));
|
|
623
|
+
if (projectMcpIds.length > 0) {
|
|
624
|
+
console.log(theme.label(' Proj MCP: ') + projectMcpIds.join(', '));
|
|
625
|
+
}
|
|
626
|
+
if (handleSkills) {
|
|
627
|
+
console.log(theme.label(' Skills: ') + (selectedSkillIds.length > 0 ? selectedSkillIds.join(', ') : 'none'));
|
|
628
|
+
}
|
|
629
|
+
console.log();
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
if (!runtime.nonInteractive && !(await confirm('Proceed with project setup?'))) {
|
|
633
|
+
infoMsg('Setup cancelled. Run dxai again anytime.');
|
|
634
|
+
process.exit(0);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// ── Dry-run short-circuit ──
|
|
638
|
+
if (runtime.dryRun) {
|
|
639
|
+
quiet(runtime, () => {
|
|
640
|
+
sectionHeader('Dry run — no changes written');
|
|
641
|
+
infoMsg(`Would generate features: ${selectedFeatures.join(', ') || 'none'}`);
|
|
642
|
+
if (projectMcpIds.length > 0) infoMsg(`Would write project MCP: ${projectMcpIds.join(', ')}`);
|
|
643
|
+
if (handleSkills && selectedSkillIds.length > 0) infoMsg(`Would install skills: ${selectedSkillIds.join(', ')}`);
|
|
644
|
+
});
|
|
645
|
+
return {
|
|
646
|
+
selectedStackIds,
|
|
647
|
+
selectedFeatures,
|
|
648
|
+
projectMcpIds,
|
|
649
|
+
selectedSkillIds,
|
|
650
|
+
dryRun: true,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// ── Execute ──
|
|
655
|
+
quiet(runtime, () => {
|
|
656
|
+
console.log();
|
|
657
|
+
sectionHeader('Configuring (Project)');
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
let projectMcpResults = null;
|
|
661
|
+
let projectErrorCount = 0;
|
|
662
|
+
if (selectedFeatures.includes('project-mcp') && projectMcpIds.length > 0) {
|
|
663
|
+
const spinner = startSpinner(runtime, 'Writing project-level MCP configs...');
|
|
664
|
+
try {
|
|
665
|
+
const mcpResults = writeProjectMcpConfigs(agentsWithProjectMcp, projectMcpIds, MCP_SERVERS, projectMcpInputs);
|
|
666
|
+
spinner?.stop();
|
|
667
|
+
projectMcpResults = mcpResults;
|
|
668
|
+
recordProjectMcp(mcpResults);
|
|
669
|
+
quiet(runtime, () => reportMcpResults(mcpResults));
|
|
670
|
+
} catch (err) {
|
|
671
|
+
spinner?.stop();
|
|
672
|
+
projectErrorCount++;
|
|
673
|
+
if (runtime.json) throw err;
|
|
674
|
+
errorMsg(`Project MCP config failed: ${err.message}`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const writtenFiles = [];
|
|
679
|
+
|
|
680
|
+
if (selectedFeatures.includes('cursor-rules')) {
|
|
681
|
+
const written = writeCursorRules(selectedStackIds, CURSOR_RULES, profile);
|
|
682
|
+
for (const name of written) writtenFiles.push(path.join('.cursor', 'rules', name));
|
|
683
|
+
quiet(runtime, () => {
|
|
684
|
+
if (written.length > 0) successMsg(`Cursor rules created: ${written.join(', ')}`);
|
|
685
|
+
else infoMsg('Cursor rules already exist, skipped');
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (selectedFeatures.includes('cursor-commands')) {
|
|
690
|
+
const written = writeCursorCommands(CURSOR_COMMANDS);
|
|
691
|
+
for (const name of written) writtenFiles.push(path.join('.cursor', 'skills', name));
|
|
692
|
+
quiet(runtime, () => {
|
|
693
|
+
if (written.length > 0) successMsg(`Cursor commands created: ${written.join(', ')}`);
|
|
694
|
+
else infoMsg('Cursor commands already exist, skipped');
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (selectedFeatures.includes('cursor-ignore')) {
|
|
699
|
+
const created = writeCursorIgnore();
|
|
700
|
+
if (created) writtenFiles.push('.cursorignore');
|
|
701
|
+
quiet(runtime, () => {
|
|
702
|
+
if (created) successMsg('.cursorignore created');
|
|
703
|
+
else infoMsg('.cursorignore already exists, skipped');
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (selectedFeatures.includes('agent-instructions')) {
|
|
708
|
+
const instructionFiles = writeProjectInstructions(selectedAgents, selectedStackIds, profile, {
|
|
709
|
+
importAgentsMd: selectedFeatures.includes('agents-md') || fs.existsSync(path.join(process.cwd(), 'AGENTS.md')),
|
|
710
|
+
});
|
|
711
|
+
for (const name of instructionFiles) writtenFiles.push(name);
|
|
712
|
+
quiet(runtime, () => {
|
|
713
|
+
if (instructionFiles.length > 0) successMsg(`Project instructions created: ${instructionFiles.join(', ')}`);
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (selectedFeatures.includes('agents-md')) {
|
|
718
|
+
const created = writeAgentsMd(selectedStackIds, profile);
|
|
719
|
+
if (created) writtenFiles.push('AGENTS.md');
|
|
720
|
+
quiet(runtime, () => {
|
|
721
|
+
if (created) successMsg('AGENTS.md created');
|
|
722
|
+
else infoMsg('AGENTS.md already exists, skipped');
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (selectedFeatures.includes('gitattributes')) {
|
|
727
|
+
const created = writeGitattributes();
|
|
728
|
+
if (created) writtenFiles.push('.gitattributes');
|
|
729
|
+
quiet(runtime, () => {
|
|
730
|
+
if (created) successMsg('.gitattributes created');
|
|
731
|
+
else infoMsg('.gitattributes already exists, skipped');
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (selectedFeatures.includes('editorconfig')) {
|
|
736
|
+
const created = writeEditorconfig();
|
|
737
|
+
if (created) writtenFiles.push('.editorconfig');
|
|
738
|
+
quiet(runtime, () => {
|
|
739
|
+
if (created) successMsg('.editorconfig created');
|
|
740
|
+
else infoMsg('.editorconfig already exists, skipped');
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (writtenFiles.length > 0) recordProjectFiles(writtenFiles);
|
|
745
|
+
|
|
746
|
+
const projectResult = {
|
|
747
|
+
selectedStackIds, selectedFeatures, projectMcpIds, selectedSkillIds,
|
|
748
|
+
mcpResults: projectMcpResults, errorCount: projectErrorCount,
|
|
749
|
+
};
|
|
750
|
+
if (handleSkills) {
|
|
751
|
+
await installAndReportSkills(selectedSkillIds, selectedAgents, runtime, projectResult, recordProjectSkills);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return projectResult;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// ── Main Entry Point ──
|
|
758
|
+
export async function run(mode, opts = {}) {
|
|
759
|
+
// Profile resolution. Semantics:
|
|
760
|
+
// opts.profile === false → --no-profile, skip auto-discovery
|
|
761
|
+
// opts.profile === undefined → no flag, auto-discover defaults
|
|
762
|
+
// opts.profile === '<string>' → explicit profile name or path
|
|
763
|
+
let profilePath = null;
|
|
764
|
+
let mergedOpts = opts;
|
|
765
|
+
if (opts.profile !== false) {
|
|
766
|
+
const explicit = typeof opts.profile === 'string' ? opts.profile : null;
|
|
767
|
+
profilePath = resolveProfile(explicit);
|
|
768
|
+
if (explicit && !profilePath) {
|
|
769
|
+
throw new Error(`Profile not found: ${explicit}`);
|
|
770
|
+
}
|
|
771
|
+
if (profilePath) {
|
|
772
|
+
const profile = readProfile(profilePath);
|
|
773
|
+
mergedOpts = mergeWithProfile(opts, profile);
|
|
774
|
+
if (!mode && profile.mode) mode = profile.mode;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const runtime = normalizeOptions(mergedOpts);
|
|
779
|
+
|
|
780
|
+
quiet(runtime, () => printBanner());
|
|
781
|
+
if (profilePath) quiet(runtime, () => infoMsg(`Loaded profile: ${profilePath}`));
|
|
782
|
+
|
|
783
|
+
// Periodically refresh the catalog cache (opt-out; skips in --json/CI). Updates the
|
|
784
|
+
// on-disk cache for the next run — see src/auto-update.js. Best-effort: a failure
|
|
785
|
+
// here (e.g. an unwritable cache dir) must never abort the user's setup.
|
|
786
|
+
try {
|
|
787
|
+
await maybeRefreshCatalog(runtime);
|
|
788
|
+
} catch { /* non-fatal background refresh */ }
|
|
789
|
+
|
|
790
|
+
if (!mode) {
|
|
791
|
+
if (runtime.nonInteractive) {
|
|
792
|
+
// Default to "both" in non-interactive mode.
|
|
793
|
+
mode = 'both';
|
|
794
|
+
} else {
|
|
795
|
+
console.log();
|
|
796
|
+
const { selectedMode } = await inquirer.prompt([
|
|
797
|
+
{
|
|
798
|
+
type: 'list',
|
|
799
|
+
name: 'selectedMode',
|
|
800
|
+
message: 'What would you like to set up?',
|
|
801
|
+
choices: [
|
|
802
|
+
{ name: 'System — global IDE configs, MCP servers, agent skills', value: 'system' },
|
|
803
|
+
{ name: 'Project — AI-friendly project config (rules, CLAUDE.md, skills, etc.)', value: 'project' },
|
|
804
|
+
{ name: 'Both — full system + project setup', value: 'both' },
|
|
805
|
+
],
|
|
806
|
+
},
|
|
807
|
+
]);
|
|
808
|
+
mode = selectedMode;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const runBoth = mode === 'both';
|
|
813
|
+
const ctx = await sharedSetup(runtime);
|
|
814
|
+
|
|
815
|
+
let systemResult = null;
|
|
816
|
+
if (mode === 'system' || runBoth) {
|
|
817
|
+
systemResult = await runSystem(ctx, runtime);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
let projectResult = null;
|
|
821
|
+
if (mode === 'project' || runBoth) {
|
|
822
|
+
// In "both" mode the system flow already installs skills; let the project
|
|
823
|
+
// flow own them only when running project-only, to avoid double installs.
|
|
824
|
+
projectResult = await runProject(ctx, runtime, { handleSkills: mode === 'project' });
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// A partially failed setup must not exit 0 — CI callers rely on the code.
|
|
828
|
+
const errorCount =
|
|
829
|
+
(systemResult?.errorCount || 0) + countResultErrors(systemResult || {}) +
|
|
830
|
+
(projectResult?.errorCount || 0) + countResultErrors(projectResult || {});
|
|
831
|
+
if (errorCount > 0) process.exitCode = 1;
|
|
832
|
+
|
|
833
|
+
if (runtime.json) {
|
|
834
|
+
const out = {
|
|
835
|
+
ok: errorCount === 0,
|
|
836
|
+
errorCount,
|
|
837
|
+
mode,
|
|
838
|
+
dryRun: runtime.dryRun,
|
|
839
|
+
agents: ctx.selectedAgentIds,
|
|
840
|
+
system: systemResult ? {
|
|
841
|
+
mcp: systemResult.selectedMcpIds || [],
|
|
842
|
+
skills: systemResult.selectedSkillIds || [],
|
|
843
|
+
tools: systemResult.selectedToolIds || [],
|
|
844
|
+
needsEnv: (systemResult.needsEnv || []).map((s) => s.id),
|
|
845
|
+
results: {
|
|
846
|
+
mcp: systemResult.mcpResults || null,
|
|
847
|
+
skills: systemResult.skillResults || null,
|
|
848
|
+
tools: systemResult.toolResults || null,
|
|
849
|
+
},
|
|
850
|
+
...(systemResult.dryResult ? { previews: systemResult.dryResult.previews } : {}),
|
|
851
|
+
} : null,
|
|
852
|
+
project: projectResult ? {
|
|
853
|
+
stack: projectResult.selectedStackIds || [],
|
|
854
|
+
features: projectResult.selectedFeatures || [],
|
|
855
|
+
projectMcp: projectResult.projectMcpIds || [],
|
|
856
|
+
skills: projectResult.selectedSkillIds || [],
|
|
857
|
+
results: {
|
|
858
|
+
mcp: projectResult.mcpResults || null,
|
|
859
|
+
skills: projectResult.skillResults || null,
|
|
860
|
+
},
|
|
861
|
+
} : null,
|
|
862
|
+
};
|
|
863
|
+
process.stdout.write(JSON.stringify(out, null, 2) + '\n');
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
console.log();
|
|
868
|
+
console.log(theme.dim(' ─────────────────────────────'));
|
|
869
|
+
console.log(theme.highlight(' ✨ dxai setup complete!'));
|
|
870
|
+
console.log();
|
|
871
|
+
|
|
872
|
+
if (systemResult?.needsEnv?.length > 0) {
|
|
873
|
+
warnMsg('Some MCP servers need API keys. Add these to your environment:');
|
|
874
|
+
for (const s of systemResult.needsEnv) {
|
|
875
|
+
for (const [envVar, desc] of Object.entries(s.requiresEnv)) {
|
|
876
|
+
console.log(theme.dim(` export ${envVar}="..." `) + chalk.dim(`# ${desc}`));
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
console.log();
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
infoMsg('Next steps:');
|
|
883
|
+
let step = 1;
|
|
884
|
+
if (systemResult) {
|
|
885
|
+
console.log(theme.dim(` ${step++}. Restart your IDE/agent to pick up new MCP configs`));
|
|
886
|
+
}
|
|
887
|
+
if (projectResult) {
|
|
888
|
+
console.log(theme.dim(` ${step++}. Customize the generated project files for your codebase`));
|
|
889
|
+
if (projectResult.selectedFeatures?.includes('agent-instructions')) {
|
|
890
|
+
console.log(theme.dim(` ${step++}. Edit CLAUDE.md / GEMINI.md with project-specific instructions`));
|
|
891
|
+
}
|
|
892
|
+
if (projectResult.selectedFeatures?.includes('agents-md')) {
|
|
893
|
+
console.log(theme.dim(` ${step++}. Fill in AGENTS.md with your project's architecture details`));
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
console.log(theme.dim(` ${step}. Re-run ${chalk.cyan('npx dxai-cli')} anytime to add more tools`));
|
|
897
|
+
console.log();
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// ── Apply a saved profile (sugar for run + --yes) ──
|
|
901
|
+
export async function apply(nameOrPath, opts = {}) {
|
|
902
|
+
const profilePath = resolveProfile(nameOrPath);
|
|
903
|
+
if (!profilePath) {
|
|
904
|
+
throw new Error(
|
|
905
|
+
nameOrPath
|
|
906
|
+
? `Profile not found: ${nameOrPath}`
|
|
907
|
+
: 'No profile found. Looked in ./.dxai/profile.json, ~/.dxai/config.json, ~/.dxairc.'
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
const profile = readProfile(profilePath);
|
|
911
|
+
const merged = mergeWithProfile({ ...opts, yes: true }, profile);
|
|
912
|
+
const mode = merged.mode || 'both';
|
|
913
|
+
// Pass the resolved path so run() doesn't re-discover (and respects this exact one).
|
|
914
|
+
await run(mode, { ...merged, profile: profilePath });
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// ── Save current selections as a named profile ──
|
|
918
|
+
export async function saveProfileCmd(nameOrPath, opts = {}) {
|
|
919
|
+
const data = {
|
|
920
|
+
mode: opts.mode,
|
|
921
|
+
agents: opts.agents,
|
|
922
|
+
mcp: opts.mcp,
|
|
923
|
+
skills: opts.skills,
|
|
924
|
+
features: opts.features,
|
|
925
|
+
stack: opts.stack,
|
|
926
|
+
mcpInputs: opts.mcpInputs,
|
|
927
|
+
};
|
|
928
|
+
const empty = Object.values(data).every((v) => v === undefined);
|
|
929
|
+
if (empty) {
|
|
930
|
+
throw new Error(
|
|
931
|
+
'No selections to save. Pass values via flags, e.g.:\n' +
|
|
932
|
+
' dxai save-profile myteam --agents cursor --mcp github,playwright --features cursor-rules,agents-md'
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
let target;
|
|
937
|
+
if (opts.here) target = { here: true };
|
|
938
|
+
else if (opts.path) target = { path: opts.path };
|
|
939
|
+
else target = { user: true, name: nameOrPath || 'default' };
|
|
940
|
+
|
|
941
|
+
const written = saveProfile(data, target);
|
|
942
|
+
if (!opts.json) {
|
|
943
|
+
successMsg(`Profile saved → ${written}`);
|
|
944
|
+
} else {
|
|
945
|
+
process.stdout.write(JSON.stringify({ ok: true, path: written, profile: data }, null, 2) + '\n');
|
|
946
|
+
}
|
|
947
|
+
return written;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// ── List discoverable profiles ──
|
|
951
|
+
export async function listProfilesCmd(opts = {}) {
|
|
952
|
+
const profiles = listProfiles();
|
|
953
|
+
if (opts.json) {
|
|
954
|
+
process.stdout.write(JSON.stringify({ ok: true, profiles }, null, 2) + '\n');
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
if (profiles.length === 0) {
|
|
958
|
+
infoMsg('No profiles found.');
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
sectionHeader('Profiles');
|
|
962
|
+
for (const p of profiles) {
|
|
963
|
+
console.log(` ${theme.label(p.name.padEnd(20))} ${theme.dim(p.scope.padEnd(10))} ${theme.dim(p.path)}`);
|
|
964
|
+
}
|
|
965
|
+
console.log();
|
|
966
|
+
}
|