@defai.digital/cli 13.1.16 → 13.1.17

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.
@@ -1,54 +1,22 @@
1
1
  /**
2
- * Setup command - Interactive setup wizard for AutomatosX
2
+ * Setup command - Global setup wizard for AutomatosX
3
+ *
4
+ * Creates global configuration with detected providers.
5
+ * For per-project initialization (MCP registration), use `ax init`.
3
6
  *
4
7
  * Features:
5
8
  * - Provider detection
6
- * - Configuration creation
9
+ * - Global configuration creation
7
10
  * - Non-interactive mode for CI
8
11
  */
9
12
  import { exec } from 'node:child_process';
10
13
  import { promisify } from 'node:util';
11
- import { mkdir, writeFile, readFile, access } from 'node:fs/promises';
12
- import { join } from 'node:path';
13
14
  import { createConfigStore, initConfigDirectory, } from '@defai.digital/config-domain';
14
- import { DEFAULT_CONFIG, KNOWN_PROVIDERS, PROVIDER_DEFAULTS, DATA_DIR_NAME, CONFIG_FILENAME, TIMEOUT_SETUP_ADD, TIMEOUT_SETUP_REMOVE, TIMEOUT_HEALTH_CHECK, TIMEOUT_WORKFLOW_STEP, DEFAULT_SCHEMA_VERSION, ITERATE_MAX_DEFAULT, ITERATE_TIMEOUT_DEFAULT, } from '@defai.digital/contracts';
15
- import { CONTEXT_DIRECTORY } from '@defai.digital/context-domain';
16
- import { PROVIDER_CHECKS, checkProviderCLI, } from './doctor.js';
15
+ import { DEFAULT_CONFIG, KNOWN_PROVIDERS, PROVIDER_DEFAULTS, TIMEOUT_HEALTH_CHECK, TIMEOUT_WORKFLOW_STEP, } from '@defai.digital/contracts';
17
16
  const execAsync = promisify(exec);
18
17
  // ============================================================================
19
18
  // Constants
20
19
  // ============================================================================
21
- // ============================================================================
22
- // MCP Constants
23
- // ============================================================================
24
- /** MCP server name registered with provider CLIs */
25
- const MCP_SERVER_NAME = 'automatosx';
26
- /** MCP subcommands */
27
- const MCP_COMMANDS = {
28
- add: 'mcp add',
29
- remove: 'mcp remove',
30
- serverArgs: 'mcp server',
31
- };
32
- /** MCP command flags for different formats */
33
- const MCP_FLAGS = {
34
- /** Claude uses -s local for user-scope config */
35
- claudeScope: '-s local',
36
- /** ax-wrapper uses -c for command */
37
- command: '-c',
38
- /** ax-wrapper uses -a for arguments */
39
- args: '-a',
40
- };
41
- /** Pattern to detect successful MCP server addition in output */
42
- const MCP_SUCCESS_PATTERN = /Added MCP server|server.*added|successfully added/i;
43
- // ============================================================================
44
- // CLI Constants
45
- // ============================================================================
46
- /** Fallback CLI command when binary path cannot be determined */
47
- const CLI_FALLBACK_COMMAND = 'ax';
48
- /** Node.js executable for running scripts */
49
- const NODE_EXECUTABLE = 'node';
50
- /** Conventions file name in context directory */
51
- const CONVENTIONS_FILENAME = 'conventions.md';
52
20
  /** Provider name display width for aligned output */
53
21
  const PROVIDER_DISPLAY_WIDTH = 10;
54
22
  /** Default error message when error type is unknown */
@@ -59,8 +27,6 @@ const STDERR_REDIRECT = '2>&1';
59
27
  const SEMVER_PATTERN = /(\d+\.\d+\.\d+)/;
60
28
  /** Platform-specific command to find executables */
61
29
  const WHICH_COMMAND = process.platform === 'win32' ? 'where' : 'which';
62
- /** JSON formatting indentation */
63
- const JSON_INDENT = 2;
64
30
  /** Exit codes for CLI commands */
65
31
  const EXIT_CODE = {
66
32
  SUCCESS: 0,
@@ -71,11 +37,6 @@ const CONFIG_SCOPE = {
71
37
  GLOBAL: 'global',
72
38
  LOCAL: 'local',
73
39
  };
74
- /** Health check status values */
75
- const HEALTH_STATUS = {
76
- PASS: 'pass',
77
- FAIL: 'fail',
78
- };
79
40
  /** CLI argument flags */
80
41
  const CLI_FLAGS = {
81
42
  force: ['--force', '-f'],
@@ -83,7 +44,6 @@ const CLI_FLAGS = {
83
44
  silent: ['--silent', '-s'],
84
45
  local: ['--local', '-l'],
85
46
  global: ['--global', '-g'],
86
- skipProject: ['--skip-project', '--no-project'],
87
47
  };
88
48
  /** Terminal color codes */
89
49
  const COLORS = {
@@ -102,258 +62,6 @@ const ICONS = {
102
62
  warn: `${COLORS.yellow}\u26A0${COLORS.reset}`,
103
63
  arrow: `${COLORS.cyan}\u2192${COLORS.reset}`,
104
64
  };
105
- /** MCP configuration for each provider (null = provider doesn't support MCP) */
106
- const PROVIDER_MCP_CONFIGS = {
107
- claude: { cliName: 'claude', format: 'claude' },
108
- gemini: { cliName: 'gemini', format: 'standard' },
109
- codex: { cliName: 'codex', format: 'standard' },
110
- qwen: { cliName: 'qwen', format: 'standard' },
111
- glm: { cliName: 'ax-glm', format: 'ax-wrapper' },
112
- grok: { cliName: 'ax-grok', format: 'ax-wrapper' },
113
- 'ax-cli': null, // ax-cli doesn't support MCP yet
114
- };
115
- /**
116
- * Get the absolute path to the CLI binary.
117
- * Uses process.argv[1] which works for both global and local installations.
118
- */
119
- function getCLIBinaryPath() {
120
- const binaryPath = process.argv[1];
121
- return binaryPath || CLI_FALLBACK_COMMAND;
122
- }
123
- /**
124
- * Check if a path is absolute (Unix or Windows style)
125
- */
126
- function isAbsolutePath(filePath) {
127
- return filePath.startsWith('/') || filePath.includes('\\');
128
- }
129
- /**
130
- * Build the MCP server command parts based on binary path.
131
- * Returns { executable, arguments } for use in MCP add commands.
132
- */
133
- function buildMCPServerCommand(binaryPath) {
134
- if (isAbsolutePath(binaryPath)) {
135
- return { executable: NODE_EXECUTABLE, arguments: `"${binaryPath}" ${MCP_COMMANDS.serverArgs}` };
136
- }
137
- return { executable: binaryPath, arguments: MCP_COMMANDS.serverArgs };
138
- }
139
- /**
140
- * Get the CLI command to add AutomatosX MCP server for a provider.
141
- * Uses provider's native CLI to ensure proper formatting and validation.
142
- */
143
- function buildMCPAddCommand(providerId) {
144
- const mcpConfig = PROVIDER_MCP_CONFIGS[providerId];
145
- if (!mcpConfig)
146
- return null;
147
- const binaryPath = getCLIBinaryPath();
148
- const { executable, arguments: execArgs } = buildMCPServerCommand(binaryPath);
149
- const { cliName, format } = mcpConfig;
150
- switch (format) {
151
- case 'standard':
152
- return `${cliName} ${MCP_COMMANDS.add} ${MCP_SERVER_NAME} ${executable} ${execArgs}`;
153
- case 'claude':
154
- return `${cliName} ${MCP_COMMANDS.add} ${MCP_SERVER_NAME} ${MCP_FLAGS.claudeScope} ${executable} ${execArgs}`;
155
- case 'ax-wrapper': {
156
- const command = isAbsolutePath(binaryPath) ? NODE_EXECUTABLE : CLI_FALLBACK_COMMAND;
157
- const commandArgs = isAbsolutePath(binaryPath) ? `${binaryPath} ${MCP_COMMANDS.serverArgs}` : MCP_COMMANDS.serverArgs;
158
- return `${cliName} ${MCP_COMMANDS.add} ${MCP_SERVER_NAME} ${MCP_FLAGS.command} ${command} ${MCP_FLAGS.args} ${commandArgs}`;
159
- }
160
- default:
161
- return null;
162
- }
163
- }
164
- /**
165
- * Get the CLI command to remove MCP server for a provider.
166
- */
167
- function buildMCPRemoveCommand(providerId) {
168
- const mcpConfig = PROVIDER_MCP_CONFIGS[providerId];
169
- if (!mcpConfig)
170
- return null;
171
- const scopeFlag = mcpConfig.format === 'claude' ? ` ${MCP_FLAGS.claudeScope}` : '';
172
- return `${mcpConfig.cliName} ${MCP_COMMANDS.remove} ${MCP_SERVER_NAME}${scopeFlag}`;
173
- }
174
- /**
175
- * Get the settings file path for ax-wrapper providers.
176
- * ax-glm and ax-grok store configs in .ax-glm/ and .ax-grok/ directories.
177
- */
178
- function getAxWrapperSettingsPath(cliName) {
179
- // For project-local config, use current working directory
180
- return join(process.cwd(), `.${cliName}`, 'settings.json');
181
- }
182
- /**
183
- * Configure MCP for ax-wrapper providers by writing config file directly.
184
- * This is needed because ax-wrapper CLIs default to 'content-length' framing,
185
- * but automatosx MCP server uses 'ndjson' framing (MCP SDK default).
186
- */
187
- async function configureAxWrapperMCP(cliName) {
188
- const binaryPath = getCLIBinaryPath();
189
- const settingsPath = getAxWrapperSettingsPath(cliName);
190
- const settingsDir = join(process.cwd(), `.${cliName}`);
191
- try {
192
- // Ensure settings directory exists
193
- await mkdir(settingsDir, { recursive: true });
194
- // Read existing config or create new one
195
- let existingConfig = { mcpServers: {} };
196
- try {
197
- const content = await readFile(settingsPath, 'utf-8');
198
- existingConfig = JSON.parse(content);
199
- if (!existingConfig.mcpServers) {
200
- existingConfig.mcpServers = {};
201
- }
202
- }
203
- catch {
204
- // File doesn't exist or is invalid - use empty config
205
- }
206
- // Build args based on binary path
207
- const args = isAbsolutePath(binaryPath)
208
- ? [binaryPath, 'mcp', 'server']
209
- : ['mcp', 'server'];
210
- const command = isAbsolutePath(binaryPath) ? NODE_EXECUTABLE : binaryPath;
211
- // Add automatosx MCP server config with ndjson framing
212
- existingConfig.mcpServers[MCP_SERVER_NAME] = {
213
- name: MCP_SERVER_NAME,
214
- transport: {
215
- type: 'stdio',
216
- command,
217
- args,
218
- env: {},
219
- framing: 'ndjson',
220
- },
221
- };
222
- // Write updated config
223
- await writeFile(settingsPath, JSON.stringify(existingConfig, null, JSON_INDENT) + '\n');
224
- return { success: true, skipped: false };
225
- }
226
- catch (err) {
227
- return {
228
- success: false,
229
- skipped: false,
230
- error: err instanceof Error ? err.message : FALLBACK_ERROR_MESSAGE,
231
- };
232
- }
233
- }
234
- /**
235
- * Check if command output indicates successful MCP server addition.
236
- * Some providers output success message even when validation times out.
237
- */
238
- function isMCPAdditionSuccessful(commandOutput) {
239
- return MCP_SUCCESS_PATTERN.test(commandOutput);
240
- }
241
- /**
242
- * Extract clean error message from exec error.
243
- */
244
- function extractErrorMessage(rawError) {
245
- if (rawError.includes('Command failed')) {
246
- return rawError.split('\n').pop() || rawError;
247
- }
248
- return rawError;
249
- }
250
- /**
251
- * Configure MCP for a detected provider.
252
- *
253
- * For ax-wrapper providers (ax-glm, ax-grok):
254
- * - Write config file directly with ndjson framing (required for MCP SDK compatibility)
255
- *
256
- * For other providers:
257
- * - Use native CLI commands (remove-then-add for clean state)
258
- */
259
- async function configureMCPForProvider(providerId) {
260
- const mcpConfig = PROVIDER_MCP_CONFIGS[providerId];
261
- if (!mcpConfig) {
262
- return { success: true, skipped: true };
263
- }
264
- // For ax-wrapper providers, write config file directly with ndjson framing
265
- // This is needed because ax-wrapper CLIs default to 'content-length' framing,
266
- // but automatosx MCP server uses 'ndjson' framing (MCP SDK default)
267
- if (mcpConfig.format === 'ax-wrapper') {
268
- return configureAxWrapperMCP(mcpConfig.cliName);
269
- }
270
- const addCommand = buildMCPAddCommand(providerId);
271
- const removeCommand = buildMCPRemoveCommand(providerId);
272
- if (!addCommand) {
273
- return { success: true, skipped: true };
274
- }
275
- try {
276
- // Step 1: Remove existing config for clean state (ignore if not exists)
277
- if (removeCommand) {
278
- try {
279
- await execAsync(`${removeCommand} ${STDERR_REDIRECT}`, { timeout: TIMEOUT_SETUP_REMOVE });
280
- }
281
- catch {
282
- // Server might not exist - that's expected
283
- }
284
- }
285
- // Step 2: Add MCP server using provider's native CLI
286
- const { stdout, stderr } = await execAsync(`${addCommand} ${STDERR_REDIRECT}`, { timeout: TIMEOUT_SETUP_ADD });
287
- const commandOutput = `${stdout}${stderr}`;
288
- if (isMCPAdditionSuccessful(commandOutput)) {
289
- return { success: true, skipped: false };
290
- }
291
- return { success: true, skipped: false };
292
- }
293
- catch (err) {
294
- // Node's exec error includes stdout/stderr as properties
295
- const execResult = err;
296
- const errorMsg = execResult.message || FALLBACK_ERROR_MESSAGE;
297
- const fullOutput = `${execResult.stdout || ''}${execResult.stderr || ''}${errorMsg}`;
298
- // Check if server was added despite command failure (validation timeout)
299
- if (isMCPAdditionSuccessful(fullOutput)) {
300
- return { success: true, skipped: false };
301
- }
302
- return {
303
- success: false,
304
- skipped: false,
305
- error: extractErrorMessage(errorMsg),
306
- };
307
- }
308
- }
309
- /**
310
- * Use ax doctor logic to check which provider CLIs are installed
311
- * This ensures consistent detection between 'ax doctor' and 'ax setup'
312
- */
313
- async function getInstalledProviderCLIs() {
314
- const results = new Map();
315
- // Run doctor-style checks for all providers
316
- for (const provider of PROVIDER_CHECKS) {
317
- const checkResult = await checkProviderCLI(provider);
318
- results.set(provider.id, checkResult);
319
- }
320
- return results;
321
- }
322
- /**
323
- * Configure MCP for all detected providers using their native CLI commands.
324
- *
325
- * Uses 'ax doctor' check logic to determine which CLIs are installed.
326
- * Only configures MCP for providers that pass the doctor check.
327
- */
328
- async function configureMCPForAllProviders() {
329
- const result = {
330
- configured: [],
331
- skipped: [],
332
- notInstalled: [],
333
- failed: [],
334
- };
335
- const installedProviders = await getInstalledProviderCLIs();
336
- for (const [providerId, healthCheck] of installedProviders) {
337
- if (healthCheck.status === HEALTH_STATUS.FAIL) {
338
- result.notInstalled.push(providerId);
339
- continue;
340
- }
341
- const configResult = await configureMCPForProvider(providerId);
342
- if (configResult.skipped) {
343
- result.skipped.push(providerId);
344
- }
345
- else if (configResult.success) {
346
- result.configured.push(providerId);
347
- }
348
- else {
349
- result.failed.push({
350
- providerId,
351
- error: configResult.error || FALLBACK_ERROR_MESSAGE,
352
- });
353
- }
354
- }
355
- return result;
356
- }
357
65
  // ============================================================================
358
66
  // Provider Detection
359
67
  // ============================================================================
@@ -416,121 +124,48 @@ async function detectAllProviders() {
416
124
  const results = await Promise.all(KNOWN_PROVIDERS.map((id) => detectProvider(id)));
417
125
  return results;
418
126
  }
419
- // ============================================================================
420
- // Project Structure Creation
421
- // ============================================================================
422
- /**
423
- * Template for project conventions file
424
- */
425
- const CONVENTIONS_TEMPLATE = `# Project Conventions
426
-
427
- ## Code Style
428
- <!-- Describe your coding standards -->
429
- - Example: Use TypeScript strict mode
430
- - Example: Prefer functional components over class components
431
- - Example: Use named exports over default exports
432
-
433
- ## Architecture
434
- <!-- Describe your project structure -->
435
- - Example: Domain-driven design with packages/core/*/
436
- - Example: Contract-first: all types in packages/contracts/
437
- - Example: No circular dependencies between packages
438
-
439
- ## Testing
440
- <!-- Describe testing practices -->
441
- - Example: Use vitest for unit tests
442
- - Example: Co-locate tests with source: *.test.ts
443
- - Example: Mock external dependencies, not internal modules
444
-
445
- ## Naming Conventions
446
- <!-- Describe naming conventions -->
447
- - Example: Use camelCase for variables and functions
448
- - Example: Use PascalCase for types and classes
449
- - Example: Prefix interfaces with I (e.g., IUserService)
450
- `;
451
- /**
452
- * Template for project config.json
453
- */
454
- const PROJECT_CONFIG_TEMPLATE = {
455
- version: DEFAULT_SCHEMA_VERSION,
456
- iterate: {
457
- maxIterations: ITERATE_MAX_DEFAULT,
458
- maxTimeMs: ITERATE_TIMEOUT_DEFAULT,
459
- autoConfirm: false,
460
- },
461
- };
462
- /**
463
- * Creates the .automatosx/ project structure
464
- */
465
- async function createProjectStructure(projectDir, force) {
466
- const created = [];
467
- const skipped = [];
468
- const automatosxDir = join(projectDir, DATA_DIR_NAME);
469
- const contextDir = join(automatosxDir, CONTEXT_DIRECTORY);
470
- const configPath = join(automatosxDir, CONFIG_FILENAME);
471
- const conventionsPath = join(contextDir, CONVENTIONS_FILENAME);
472
- // Create data directory
473
- try {
474
- await mkdir(automatosxDir, { recursive: true });
475
- }
476
- catch {
477
- // Directory may already exist
478
- }
479
- // Create context directory
480
- try {
481
- await mkdir(contextDir, { recursive: true });
482
- }
483
- catch {
484
- // Directory may already exist
485
- }
486
- // Create project config.json
487
- const configExists = await fileExists(configPath);
488
- if (!configExists || force) {
489
- await writeFile(configPath, JSON.stringify(PROJECT_CONFIG_TEMPLATE, null, JSON_INDENT) + '\n');
490
- created.push(`${DATA_DIR_NAME}/${CONFIG_FILENAME}`);
491
- }
492
- else {
493
- skipped.push(`${DATA_DIR_NAME}/${CONFIG_FILENAME} (already exists)`);
494
- }
495
- // Create conventions template
496
- const conventionsExists = await fileExists(conventionsPath);
497
- if (!conventionsExists || force) {
498
- await writeFile(conventionsPath, CONVENTIONS_TEMPLATE);
499
- created.push(`${DATA_DIR_NAME}/${CONTEXT_DIRECTORY}/${CONVENTIONS_FILENAME}`);
500
- }
501
- else {
502
- skipped.push(`${DATA_DIR_NAME}/${CONTEXT_DIRECTORY}/${CONVENTIONS_FILENAME} (already exists)`);
503
- }
504
- return { created, skipped };
505
- }
506
- /**
507
- * Check if a file exists
508
- */
509
- async function fileExists(path) {
510
- try {
511
- await access(path);
512
- return true;
513
- }
514
- catch {
515
- return false;
516
- }
517
- }
518
127
  /**
519
128
  * Runs the setup process
520
129
  */
521
130
  async function runSetup(options) {
522
131
  const configStore = createConfigStore();
523
132
  const configFilePath = configStore.getPath(options.scope);
133
+ // Detect providers first (needed for comparison)
134
+ const allProviders = await detectAllProviders();
135
+ const availableProviders = allProviders.filter((provider) => provider.detected);
136
+ const detectedProviderIds = new Set(availableProviders.map((p) => p.providerId));
524
137
  // Check if config already exists
525
138
  const configExists = await configStore.exists(options.scope);
526
139
  if (configExists && !options.force) {
527
- throw new Error(`Configuration already exists at ${configFilePath}. Use --force to overwrite.`);
140
+ // Read existing config and compare
141
+ const existingConfig = await configStore.read(options.scope);
142
+ const existingProviderIds = new Set(Object.keys(existingConfig?.providers ?? {}).filter((id) => existingConfig?.providers[id]?.enabled));
143
+ // Find differences
144
+ const newProviders = [...detectedProviderIds].filter((id) => !existingProviderIds.has(id));
145
+ const removedProviders = [...existingProviderIds].filter((id) => !detectedProviderIds.has(id));
146
+ // If no changes, return "up to date"
147
+ if (newProviders.length === 0 && removedProviders.length === 0) {
148
+ return {
149
+ success: true,
150
+ config: existingConfig,
151
+ providers: allProviders,
152
+ configPath: configFilePath,
153
+ alreadyUpToDate: true,
154
+ };
155
+ }
156
+ // Changes detected but no --force flag
157
+ return {
158
+ success: true,
159
+ config: existingConfig,
160
+ providers: allProviders,
161
+ configPath: configFilePath,
162
+ alreadyUpToDate: false,
163
+ newProvidersDetected: newProviders,
164
+ removedProviders: removedProviders,
165
+ };
528
166
  }
529
167
  // Initialize directory
530
168
  await initConfigDirectory(options.scope);
531
- // Detect providers
532
- const allProviders = await detectAllProviders();
533
- const availableProviders = allProviders.filter((provider) => provider.detected);
534
169
  // Build providers config record
535
170
  const providersConfig = {};
536
171
  for (const provider of availableProviders) {
@@ -577,7 +212,6 @@ function parseSetupArgs(args) {
577
212
  let nonInteractive = false;
578
213
  let silent = false;
579
214
  let scope = CONFIG_SCOPE.GLOBAL;
580
- let skipProjectStructure = false;
581
215
  for (const arg of args) {
582
216
  if (matchesFlag(arg, CLI_FLAGS.force)) {
583
217
  force = true;
@@ -587,8 +221,8 @@ function parseSetupArgs(args) {
587
221
  }
588
222
  else if (matchesFlag(arg, CLI_FLAGS.silent)) {
589
223
  silent = true;
590
- nonInteractive = true; // Silent implies non-interactive
591
- force = true; // Silent implies force (use defaults, overwrite if exists)
224
+ nonInteractive = true;
225
+ force = true;
592
226
  }
593
227
  else if (matchesFlag(arg, CLI_FLAGS.local)) {
594
228
  scope = CONFIG_SCOPE.LOCAL;
@@ -596,11 +230,8 @@ function parseSetupArgs(args) {
596
230
  else if (matchesFlag(arg, CLI_FLAGS.global)) {
597
231
  scope = CONFIG_SCOPE.GLOBAL;
598
232
  }
599
- else if (matchesFlag(arg, CLI_FLAGS.skipProject)) {
600
- skipProjectStructure = true;
601
- }
602
233
  }
603
- return { force, nonInteractive, silent, scope, skipProjectStructure };
234
+ return { force, nonInteractive, silent, scope };
604
235
  }
605
236
  /**
606
237
  * Formats provider detection result for display.
@@ -613,6 +244,12 @@ function formatProviderResult(detection) {
613
244
  }
614
245
  /**
615
246
  * Setup command handler
247
+ *
248
+ * Global setup for AutomatosX:
249
+ * 1. Detects installed provider CLIs
250
+ * 2. Creates global configuration
251
+ *
252
+ * For per-project initialization (MCP registration), use `ax init`.
616
253
  */
617
254
  export async function setupCommand(args, options) {
618
255
  const setupOptions = parseSetupArgs(args);
@@ -633,62 +270,104 @@ export async function setupCommand(args, options) {
633
270
  outputLines.push(` ${ICONS.check} Node.js: ${process.version}`);
634
271
  outputLines.push('');
635
272
  }
636
- // Step 2: Detect providers
273
+ // Step 2 & 3: Detect providers and create/check configuration
637
274
  if (showOutput) {
638
- outputLines.push(`${COLORS.bold}Step 2: Provider Detection${COLORS.reset}`);
275
+ outputLines.push(`${COLORS.bold}Step 2: Provider Detection & Configuration${COLORS.reset}`);
639
276
  }
640
- const detectedProviders = await detectAllProviders();
277
+ const setupResult = await runSetup(setupOptions);
278
+ const detectedProviders = setupResult.providers;
641
279
  if (showOutput) {
642
280
  for (const provider of detectedProviders) {
643
281
  outputLines.push(formatProviderResult(provider));
644
282
  }
645
283
  outputLines.push('');
646
284
  }
647
- // Step 3: Create configuration
648
- if (showOutput) {
649
- outputLines.push(`${COLORS.bold}Step 3: Creating Configuration${COLORS.reset}`);
650
- }
651
- const setupResult = await runSetup(setupOptions);
652
- if (showOutput) {
653
- outputLines.push(` ${ICONS.check} Configuration saved to: ${setupResult.configPath}`);
654
- outputLines.push('');
655
- }
656
- // Step 4: Create project structure (unless skipped)
657
- let projectStructure;
658
- if (!setupOptions.skipProjectStructure) {
285
+ // Handle idempotent cases
286
+ if (setupResult.alreadyUpToDate) {
659
287
  if (showOutput) {
660
- outputLines.push(`${COLORS.bold}Step 4: Project Structure${COLORS.reset}`);
288
+ outputLines.push(`${ICONS.check} Configuration is up to date at: ${setupResult.configPath}`);
289
+ outputLines.push('');
290
+ outputLines.push(`${COLORS.bold}Summary${COLORS.reset}`);
291
+ outputLines.push(` No changes needed - providers match existing configuration`);
292
+ outputLines.push('');
293
+ outputLines.push(`${COLORS.bold}Next Steps${COLORS.reset}`);
294
+ outputLines.push(` 1. Run ${COLORS.cyan}ax doctor${COLORS.reset} to verify providers are working`);
295
+ outputLines.push(` 2. Run ${COLORS.cyan}ax init${COLORS.reset} in your project directory to register MCP`);
296
+ outputLines.push('');
661
297
  }
662
- projectStructure = await createProjectStructure(process.cwd(), setupOptions.force);
298
+ if (isJsonFormat) {
299
+ return {
300
+ success: true,
301
+ message: undefined,
302
+ data: {
303
+ success: true,
304
+ configPath: setupResult.configPath,
305
+ alreadyUpToDate: true,
306
+ providers: {
307
+ detected: detectedProviders
308
+ .filter((provider) => provider.detected)
309
+ .map((provider) => provider.providerId),
310
+ enabled: Object.keys(setupResult.config.providers).filter((id) => setupResult.config.providers[id]?.enabled),
311
+ },
312
+ defaultProvider: setupResult.config.defaultProvider,
313
+ version: setupResult.config.version,
314
+ },
315
+ exitCode: EXIT_CODE.SUCCESS,
316
+ };
317
+ }
318
+ return {
319
+ success: true,
320
+ message: outputLines.join('\n'),
321
+ data: undefined,
322
+ exitCode: EXIT_CODE.SUCCESS,
323
+ };
324
+ }
325
+ // Changes detected but no --force
326
+ if (setupResult.newProvidersDetected || setupResult.removedProviders) {
663
327
  if (showOutput) {
664
- for (const filePath of projectStructure.created) {
665
- outputLines.push(` ${ICONS.check} Created ${filePath}`);
328
+ outputLines.push(`${ICONS.warn} Configuration exists but providers have changed:`);
329
+ if (setupResult.newProvidersDetected && setupResult.newProvidersDetected.length > 0) {
330
+ outputLines.push(` ${COLORS.green}+ New:${COLORS.reset} ${setupResult.newProvidersDetected.join(', ')}`);
666
331
  }
667
- for (const filePath of projectStructure.skipped) {
668
- outputLines.push(` ${ICONS.warn} Skipped ${filePath}`);
332
+ if (setupResult.removedProviders && setupResult.removedProviders.length > 0) {
333
+ outputLines.push(` ${COLORS.red}- Removed:${COLORS.reset} ${setupResult.removedProviders.join(', ')}`);
669
334
  }
670
335
  outputLines.push('');
336
+ outputLines.push(`Run ${COLORS.cyan}ax setup --force${COLORS.reset} to update configuration.`);
337
+ outputLines.push('');
671
338
  }
339
+ if (isJsonFormat) {
340
+ return {
341
+ success: true,
342
+ message: undefined,
343
+ data: {
344
+ success: true,
345
+ configPath: setupResult.configPath,
346
+ alreadyUpToDate: false,
347
+ changesDetected: {
348
+ newProviders: setupResult.newProvidersDetected,
349
+ removedProviders: setupResult.removedProviders,
350
+ },
351
+ providers: {
352
+ detected: detectedProviders
353
+ .filter((provider) => provider.detected)
354
+ .map((provider) => provider.providerId),
355
+ enabled: Object.keys(setupResult.config.providers).filter((id) => setupResult.config.providers[id]?.enabled),
356
+ },
357
+ },
358
+ exitCode: EXIT_CODE.SUCCESS,
359
+ };
360
+ }
361
+ return {
362
+ success: true,
363
+ message: outputLines.join('\n'),
364
+ data: undefined,
365
+ exitCode: EXIT_CODE.SUCCESS,
366
+ };
672
367
  }
673
- // Step 5: Configure MCP for all detected providers
674
- if (showOutput) {
675
- outputLines.push(`${COLORS.bold}Step 5: MCP Configuration${COLORS.reset}`);
676
- outputLines.push(` ${COLORS.dim}Using 'ax doctor' check to verify installed CLIs...${COLORS.reset}`);
677
- }
678
- const mcpResult = await configureMCPForAllProviders();
368
+ // Normal setup (new configuration created)
679
369
  if (showOutput) {
680
- for (const providerId of mcpResult.configured) {
681
- outputLines.push(` ${ICONS.check} ${providerId}: AutomatosX MCP configured`);
682
- }
683
- for (const providerId of mcpResult.notInstalled) {
684
- outputLines.push(` ${COLORS.dim} - ${providerId}: CLI not installed, skipped${COLORS.reset}`);
685
- }
686
- for (const { providerId, error } of mcpResult.failed) {
687
- outputLines.push(` ${ICONS.warn} ${providerId}: ${error}`);
688
- }
689
- if (mcpResult.configured.length === 0 && mcpResult.failed.length === 0 && mcpResult.notInstalled.length === 0) {
690
- outputLines.push(` ${ICONS.warn} No providers detected for MCP configuration`);
691
- }
370
+ outputLines.push(`${ICONS.check} Configuration saved to: ${setupResult.configPath}`);
692
371
  outputLines.push('');
693
372
  }
694
373
  if (showOutput) {
@@ -701,15 +380,10 @@ export async function setupCommand(args, options) {
701
380
  if (setupResult.config.defaultProvider !== undefined) {
702
381
  outputLines.push(` Default provider: ${setupResult.config.defaultProvider}`);
703
382
  }
704
- if (projectStructure !== undefined) {
705
- outputLines.push(` Project files created: ${projectStructure.created.length}`);
706
- }
707
- outputLines.push(` MCP configured: ${mcpResult.configured.length} provider(s)`);
708
383
  outputLines.push('');
709
384
  outputLines.push(`${COLORS.bold}Next Steps${COLORS.reset}`);
710
- outputLines.push(` 1. Run ${COLORS.cyan}ax doctor${COLORS.reset} to verify installation`);
711
- outputLines.push(` 2. Edit ${COLORS.cyan}${DATA_DIR_NAME}/${CONTEXT_DIRECTORY}/${CONVENTIONS_FILENAME}${COLORS.reset} to add your project conventions`);
712
- outputLines.push(` 3. Run ${COLORS.cyan}ax call --iterate <provider> "task"${COLORS.reset} to use iterate mode`);
385
+ outputLines.push(` 1. Run ${COLORS.cyan}ax doctor${COLORS.reset} to verify providers are working`);
386
+ outputLines.push(` 2. Run ${COLORS.cyan}ax init${COLORS.reset} in your project directory to register MCP`);
713
387
  outputLines.push('');
714
388
  }
715
389
  if (isJsonFormat) {
@@ -720,23 +394,13 @@ export async function setupCommand(args, options) {
720
394
  success: true,
721
395
  configPath: setupResult.configPath,
722
396
  providers: {
723
- detected: detectedProviders.filter((provider) => provider.detected).map((provider) => provider.providerId),
724
- enabled: Object.keys(setupResult.config.providers).filter(id => setupResult.config.providers[id]?.enabled),
397
+ detected: detectedProviders
398
+ .filter((provider) => provider.detected)
399
+ .map((provider) => provider.providerId),
400
+ enabled: Object.keys(setupResult.config.providers).filter((id) => setupResult.config.providers[id]?.enabled),
725
401
  },
726
402
  defaultProvider: setupResult.config.defaultProvider,
727
403
  version: setupResult.config.version,
728
- projectStructure: projectStructure !== undefined
729
- ? {
730
- created: projectStructure.created,
731
- skipped: projectStructure.skipped,
732
- }
733
- : undefined,
734
- mcpConfiguration: {
735
- configured: mcpResult.configured,
736
- skipped: mcpResult.skipped,
737
- notInstalled: mcpResult.notInstalled,
738
- failed: mcpResult.failed,
739
- },
740
404
  },
741
405
  exitCode: EXIT_CODE.SUCCESS,
742
406
  };