@distributionos/cli 0.1.17 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +260 -150
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@distributionos/cli",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "DistributionOS repo setup CLI for agent onboarding and first-party analytics.",
5
5
  "homepage": "https://distributionos.dev",
6
6
  "repository": {
package/src/cli.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { existsSync, readdirSync, statSync } from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
2
5
  import { CLI_LOCAL_VERSION, DEFAULT_API_BASE } from './constants.js';
3
6
  import {
4
7
  ensureAnalyticsSite,
@@ -12,11 +15,11 @@ import { createSetupPlan, formatPlanText } from './plan.js';
12
15
  import { applySetupPlan } from './mutate.js';
13
16
  import { submitInitialization } from './initialization.js';
14
17
  import { compareValidationResults, inspectInstallArtifacts, runValidationPlan } from './validation.js';
15
- import { buildSetupReport } from './setup-report.js';
16
-
17
- const DISTRIBUTIONOS_MCP_URL = 'https://distributionos.dev/api/mcp';
18
- const DISTRIBUTIONOS_MCP_NAME = 'distributionos';
19
- const AGENT_CHOICES = new Set(['codex', 'claude', 'terminal', 'other']);
18
+ import { buildSetupReport } from './setup-report.js';
19
+
20
+ const DISTRIBUTIONOS_MCP_URL = 'https://distributionos.dev/api/mcp';
21
+ const DISTRIBUTIONOS_MCP_NAME = 'distributionos';
22
+ const AGENT_CHOICES = new Set(['codex', 'claude', 'terminal', 'other']);
20
23
 
21
24
  export async function runCli(argv, runtime) {
22
25
  const stdout = runtime.stdout;
@@ -135,18 +138,18 @@ export async function runCli(argv, runtime) {
135
138
  return 1;
136
139
  }
137
140
 
138
- if (!args.appId) {
139
- stderr.write('Missing required --app <appId>.\n\n');
140
- stderr.write(helpText());
141
- return 1;
142
- }
143
-
144
- const agentSetup = await ensureRequestedAgentSetup({ args, runtime, stdout, stderr });
145
- if (!agentSetup.ok) return 1;
146
-
147
- if (!args.noFetch) {
148
- const authReady = await ensureCliAuth({ args, runtime, stderr });
149
- if (!authReady) return 1;
141
+ if (!args.appId) {
142
+ stderr.write('Missing required --app <appId>.\n\n');
143
+ stderr.write(helpText());
144
+ return 1;
145
+ }
146
+
147
+ const agentSetup = await ensureRequestedAgentSetup({ args, runtime, stdout, stderr });
148
+ if (!agentSetup.ok) return 1;
149
+
150
+ if (!args.noFetch) {
151
+ const authReady = await ensureCliAuth({ args, runtime, stderr });
152
+ if (!authReady) return 1;
150
153
  }
151
154
 
152
155
  let plan = await createSetupPlan({
@@ -247,12 +250,12 @@ export async function runCli(argv, runtime) {
247
250
  phase: 'apply_completed',
248
251
  details: {
249
252
  changes,
250
- validation,
251
- inspection,
252
- requiresAgentRestart: Boolean(agentSetup.requiresAgentRestart) ||
253
- changes.some((change) => (change.type === 'mcp' || change.type === 'codex-mcp') && change.changed),
254
- },
255
- });
253
+ validation,
254
+ inspection,
255
+ requiresAgentRestart: Boolean(agentSetup.requiresAgentRestart) ||
256
+ changes.some((change) => (change.type === 'mcp' || change.type === 'codex-mcp') && change.changed),
257
+ },
258
+ });
256
259
  const token = await resolveAuthToken({
257
260
  env: runtime.env,
258
261
  apiBase: args.apiBase ?? DEFAULT_API_BASE,
@@ -354,12 +357,12 @@ export function parseArgs(rawArgs) {
354
357
  skipAnalytics: false,
355
358
  skipValidate: false,
356
359
  noOpen: false,
357
- noLogin: false,
358
- skipSetupReport: false,
359
- skipAgentSetup: false,
360
- yes: false,
361
- agent: null,
362
- url: null,
360
+ noLogin: false,
361
+ skipSetupReport: false,
362
+ skipAgentSetup: false,
363
+ yes: false,
364
+ agent: null,
365
+ url: null,
363
366
  contentId: null,
364
367
  contractVersion: null,
365
368
  artifactId: null,
@@ -391,13 +394,13 @@ export function parseArgs(rawArgs) {
391
394
  else if (arg === '--allow-dirty') parsed.allowDirty = true;
392
395
  else if (arg === '--skip-analytics') parsed.skipAnalytics = true;
393
396
  else if (arg === '--skip-validate') parsed.skipValidate = true;
394
- else if (arg === '--no-open') parsed.noOpen = true;
395
- else if (arg === '--no-login') parsed.noLogin = true;
396
- else if (arg === '--skip-setup-report') parsed.skipSetupReport = true;
397
- else if (arg === '--skip-agent-setup') parsed.skipAgentSetup = true;
398
- else if (arg === '--yes' || arg === '-y') parsed.yes = true;
399
- else if (arg === '--agent') parsed.agent = parseAgent(args[++index] ?? null);
400
- else if (arg.startsWith('--agent=')) parsed.agent = parseAgent(arg.slice('--agent='.length));
397
+ else if (arg === '--no-open') parsed.noOpen = true;
398
+ else if (arg === '--no-login') parsed.noLogin = true;
399
+ else if (arg === '--skip-setup-report') parsed.skipSetupReport = true;
400
+ else if (arg === '--skip-agent-setup') parsed.skipAgentSetup = true;
401
+ else if (arg === '--yes' || arg === '-y') parsed.yes = true;
402
+ else if (arg === '--agent') parsed.agent = parseAgent(args[++index] ?? null);
403
+ else if (arg.startsWith('--agent=')) parsed.agent = parseAgent(arg.slice('--agent='.length));
401
404
  else if (arg === '--app') parsed.appId = args[++index] ?? null;
402
405
  else if (arg.startsWith('--app=')) parsed.appId = arg.slice('--app='.length);
403
406
  else if (arg === '--cwd') parsed.cwd = args[++index] ?? null;
@@ -426,178 +429,284 @@ export function parseArgs(rawArgs) {
426
429
  return parsed;
427
430
  }
428
431
 
429
- function helpText() {
430
- return [
431
- 'DistributionOS CLI',
432
- '',
433
- 'Usage:',
434
- ' distributionos setup --app <appId> [--agent codex|claude|terminal|other] [--api-base <url>] [--cwd <path>] [--json] [--no-fetch]',
435
- ' distributionos setup --app <appId> --apply [--agent codex|claude|terminal|other] [--domain <domain>] [--allow-dirty] [--skip-agent-setup] [--skip-setup-report]',
432
+ function helpText() {
433
+ return [
434
+ 'DistributionOS CLI',
435
+ '',
436
+ 'Usage:',
437
+ ' distributionos setup --app <appId> [--agent codex|claude|terminal|other] [--api-base <url>] [--cwd <path>] [--json] [--no-fetch]',
438
+ ' distributionos setup --app <appId> --apply [--agent codex|claude|terminal|other] [--domain <domain>] [--allow-dirty] [--skip-agent-setup] [--skip-setup-report]',
436
439
  ' distributionos login --app <appId> [--api-base <url>]',
437
440
  ' distributionos verify --app <appId> --url <liveUrl> [--content-id <id>]',
438
441
  ' distributionos report-implementation --app <appId> --artifact <artifactId> [--url <liveUrl>] [--summary <text>]',
439
442
  '',
440
- 'Default mode reviews the setup plan without changing files. Run again with --apply after the plan looks right.',
441
- 'For Codex, --agent codex registers the DistributionOS MCP server and runs codex mcp login before repo setup.',
442
- 'If the repo has existing uncommitted changes, commit/stash first or use --allow-dirty when you recognize them.',
443
+ 'Default mode reviews the setup plan without changing files. Run again with --apply after the plan looks right.',
444
+ 'For Codex, --agent codex registers the DistributionOS MCP server, uses the Codex Desktop CLI when available, and runs MCP login before repo setup.',
445
+ 'If the repo has existing uncommitted changes, commit/stash first or use --allow-dirty when you recognize them.',
443
446
  '',
444
447
  'Authentication:',
445
448
  ' Happy path uses DistributionOS MCP OAuth and stores app-scoped credentials outside the repo.',
446
449
  ' DISTRIBUTIONOS_API_KEY, DOS_API_KEY, or DISTRIBUTIONOS_TOKEN are advanced fallbacks.',
447
450
  ' Tokens are never printed or written to committed files.',
448
451
  '',
449
- ].join('\n');
450
- }
451
-
452
- function parseAgent(value) {
453
- const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
454
- if (!AGENT_CHOICES.has(normalized)) {
455
- throw new Error('Invalid --agent value. Use codex, claude, terminal, or other.');
456
- }
457
- return normalized;
458
- }
459
-
460
- async function ensureRequestedAgentSetup({ args, runtime, stdout, stderr }) {
461
- if (args.skipAgentSetup || args.agent !== 'codex') {
462
- return { ok: true, requiresAgentRestart: false };
452
+ ].join('\n');
453
+ }
454
+
455
+ function parseAgent(value) {
456
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
457
+ if (!AGENT_CHOICES.has(normalized)) {
458
+ throw new Error('Invalid --agent value. Use codex, claude, terminal, or other.');
459
+ }
460
+ return normalized;
461
+ }
462
+
463
+ async function ensureRequestedAgentSetup({ args, runtime, stdout, stderr }) {
464
+ if (args.skipAgentSetup || args.agent !== 'codex') {
465
+ return { ok: true, requiresAgentRestart: false };
463
466
  }
464
467
 
465
468
  stdout.write('Codex MCP setup\n');
466
- const getResult = await runCodexCommand(runtime, ['mcp', 'get', DISTRIBUTIONOS_MCP_NAME]);
469
+ const codexCommand = resolveCodexCommand(runtime);
470
+ stdout.write(`- Using ${codexCommand.label}.\n`);
471
+ const getResult = await runCodexCommand(runtime, codexCommand, ['mcp', 'get', DISTRIBUTIONOS_MCP_NAME]);
467
472
  if (getResult.ok) {
468
473
  if (!getResult.stdout.includes(DISTRIBUTIONOS_MCP_URL)) {
469
474
  stderr.write([
470
475
  'Codex already has a distributionos MCP server, but it does not point to the expected DistributionOS URL.',
471
476
  `Expected: ${DISTRIBUTIONOS_MCP_URL}`,
472
- 'Run `codex mcp remove distributionos`, then rerun this setup command.',
477
+ `Codex CLI used: ${codexCommand.command}`,
478
+ 'Remove the old distributionos MCP server from Codex, then rerun this setup command.',
473
479
  '',
474
480
  ].join('\n'));
475
481
  return { ok: false, requiresAgentRestart: false };
476
- }
477
- stdout.write('- DistributionOS MCP server is already registered in Codex.\n');
482
+ }
483
+ stdout.write('- DistributionOS MCP server is already registered in Codex.\n');
478
484
  } else if (isCommandMissing(getResult.error)) {
479
485
  stderr.write([
480
486
  'Codex CLI was not found, so DistributionOS cannot connect Codex automatically.',
481
487
  'Open this repo in Codex Desktop or install the Codex CLI, then rerun setup.',
482
- '',
483
- ].join('\n'));
484
- return { ok: false, requiresAgentRestart: false };
488
+ '',
489
+ ].join('\n'));
490
+ return { ok: false, requiresAgentRestart: false };
485
491
  } else {
486
492
  stdout.write('- Adding DistributionOS MCP server to Codex.\n');
487
- const addResult = await runCodexCommand(runtime, ['mcp', 'add', DISTRIBUTIONOS_MCP_NAME, '--url', DISTRIBUTIONOS_MCP_URL], { stream: true });
493
+ const addResult = await runCodexCommand(runtime, codexCommand, ['mcp', 'add', DISTRIBUTIONOS_MCP_NAME, '--url', DISTRIBUTIONOS_MCP_URL], { stream: true });
488
494
  if (!addResult.ok) {
489
- stderr.write(formatCodexCommandFailure('codex mcp add distributionos', addResult));
495
+ stderr.write(formatCodexCommandFailure('Codex MCP add', addResult, codexCommand));
490
496
  return { ok: false, requiresAgentRestart: false };
491
497
  }
492
498
  }
493
499
 
494
500
  stdout.write('- Starting Codex MCP OAuth. Approve the browser sign-in if Codex asks.\n');
495
- const loginResult = await runCodexCommand(runtime, ['mcp', 'login', DISTRIBUTIONOS_MCP_NAME], { stream: true });
501
+ const loginResult = await runCodexCommand(runtime, codexCommand, ['mcp', 'login', DISTRIBUTIONOS_MCP_NAME], { stream: true });
496
502
  if (!loginResult.ok) {
497
- stderr.write(formatCodexCommandFailure('codex mcp login distributionos', loginResult));
503
+ stderr.write(formatCodexCommandFailure('Codex MCP login', loginResult, codexCommand));
504
+ return { ok: false, requiresAgentRestart: true };
505
+ }
506
+ const listResult = await runCodexCommand(runtime, codexCommand, ['mcp', 'list']);
507
+ if (!listResult.ok) {
508
+ stderr.write(formatCodexCommandFailure('Codex MCP verification', listResult, codexCommand));
498
509
  return { ok: false, requiresAgentRestart: true };
499
510
  }
500
- stdout.write('- Codex MCP OAuth step completed.\n\n');
511
+ if (!codexMcpListShowsDistributionOs(listResult.stdout)) {
512
+ stderr.write([
513
+ 'Codex MCP login finished, but this Codex runtime does not list DistributionOS yet.',
514
+ `Codex CLI used: ${codexCommand.command}`,
515
+ 'Fully quit and reopen Codex Desktop, then rerun the Codex setup message from the app repo.',
516
+ '',
517
+ ].join('\n'));
518
+ return { ok: false, requiresAgentRestart: true };
519
+ }
520
+ if (codexMcpListShowsDistributionOsNotLoggedIn(listResult.stdout)) {
521
+ stderr.write([
522
+ 'Codex MCP login finished, but this Codex runtime still reports DistributionOS as Not logged in.',
523
+ `Codex CLI used: ${codexCommand.command}`,
524
+ 'Rerun the Codex setup message and approve the current browser sign-in while the command is waiting.',
525
+ '',
526
+ ].join('\n'));
527
+ return { ok: false, requiresAgentRestart: true };
528
+ }
529
+ stdout.write('- Codex MCP OAuth step completed and verified.\n\n');
501
530
  return { ok: true, requiresAgentRestart: true };
502
531
  }
503
532
 
504
- function formatCodexCommandFailure(label, result) {
533
+ function formatCodexCommandFailure(label, result, codexCommand) {
505
534
  const output = oneLine(result.stderr || result.stdout || result.error?.message || '');
506
535
  return [
507
536
  `${label} did not complete, so Codex cannot use DistributionOS tools yet.`,
508
537
  output ? `Last output: ${output}` : '',
509
- 'Run the command above manually, approve the browser sign-in, then rerun DistributionOS setup.',
538
+ codexCommand?.command ? `Codex CLI used: ${codexCommand.command}` : '',
539
+ 'Rerun the Codex setup message, approve the browser sign-in, and wait for the command to say the Codex MCP OAuth step completed and verified.',
510
540
  '',
511
541
  ].filter(Boolean).join('\n');
512
542
  }
543
+
544
+ function isCommandMissing(error) {
545
+ const message = error?.message ?? '';
546
+ const stderr = error?.stderr ?? '';
547
+ return (
548
+ error?.code === 'ENOENT' ||
549
+ error?.code === 'EPERM' ||
550
+ /spawn\s+(ENOENT|EPERM)/i.test(message) ||
551
+ /not recognized as an internal/i.test(message) ||
552
+ /not recognized as an internal/i.test(stderr)
553
+ );
554
+ }
555
+
556
+ function resolveCodexCommand(runtime) {
557
+ const env = runtime?.env ?? process.env;
558
+ const configured = typeof env.CODEX_CLI_PATH === 'string' ? env.CODEX_CLI_PATH.trim() : '';
559
+ if (configured) {
560
+ return { command: configured, label: `Codex CLI at ${configured}` };
561
+ }
513
562
 
514
- function isCommandMissing(error) {
515
- const message = error?.message ?? '';
516
- const stderr = error?.stderr ?? '';
517
- return (
518
- error?.code === 'ENOENT' ||
519
- error?.code === 'EPERM' ||
520
- /spawn\s+(ENOENT|EPERM)/i.test(message) ||
521
- /not recognized as an internal/i.test(message) ||
522
- /not recognized as an internal/i.test(stderr)
523
- );
524
- }
563
+ const desktopCommand = findCodexDesktopCommand(runtime);
564
+ if (desktopCommand) {
565
+ return { command: desktopCommand, label: `Codex Desktop CLI at ${desktopCommand}` };
566
+ }
525
567
 
526
- async function runCodexCommand(runtime, args, options = {}) {
527
- return runExternalCommand(runtime, 'codex', args, options);
568
+ return { command: 'codex', label: 'Codex CLI from PATH' };
528
569
  }
529
570
 
530
- async function runExternalCommand(runtime, command, args, options = {}) {
531
- if (typeof runtime.execFile === 'function') {
532
- try {
533
- const result = await runtime.execFile(command, args, options);
534
- return {
535
- ok: true,
536
- stdout: result?.stdout ?? '',
537
- stderr: result?.stderr ?? '',
538
- };
539
- } catch (error) {
540
- return {
541
- ok: false,
542
- stdout: error?.stdout ?? '',
543
- stderr: error?.stderr ?? '',
544
- error,
545
- };
546
- }
571
+ function findCodexDesktopCommand(runtime) {
572
+ if (Array.isArray(runtime?.codexCommandCandidates)) {
573
+ return runtime.codexCommandCandidates.find((candidate) => typeof candidate === 'string' && candidate.trim()) ?? null;
547
574
  }
548
575
 
549
- const resolved = resolveCommand(command, args, runtime);
550
- return new Promise((resolve) => {
551
- let stdoutText = '';
552
- let stderrText = '';
553
- let child;
554
- try {
555
- child = spawn(resolved.command, resolved.args, {
556
- cwd: runtime.cwd,
557
- env: runtime.env,
558
- windowsHide: true,
559
- stdio: ['inherit', 'pipe', 'pipe'],
560
- });
561
- } catch (error) {
562
- resolve({ ok: false, stdout: '', stderr: '', error });
563
- return;
576
+ const env = runtime?.env ?? process.env;
577
+ const homeDir = typeof env.USERPROFILE === 'string' && env.USERPROFILE
578
+ ? env.USERPROFILE
579
+ : os.homedir();
580
+ const localAppData = typeof env.LOCALAPPDATA === 'string' && env.LOCALAPPDATA
581
+ ? env.LOCALAPPDATA
582
+ : path.join(homeDir, 'AppData', 'Local');
583
+ const codexBinRoot = path.join(localAppData, 'OpenAI', 'Codex', 'bin');
584
+ const executableName = process.platform === 'win32' ? 'codex.exe' : 'codex';
585
+ const candidates = [];
586
+
587
+ const directCandidate = path.join(codexBinRoot, executableName);
588
+ if (existsSync(directCandidate)) {
589
+ candidates.push(directCandidate);
590
+ }
591
+
592
+ try {
593
+ for (const entry of readdirSync(codexBinRoot, { withFileTypes: true })) {
594
+ if (!entry.isDirectory()) continue;
595
+ const candidate = path.join(codexBinRoot, entry.name, executableName);
596
+ if (existsSync(candidate)) {
597
+ candidates.push(candidate);
598
+ }
564
599
  }
600
+ } catch {
601
+ return candidates[0] ?? null;
602
+ }
565
603
 
566
- child.stdout?.on('data', (chunk) => {
567
- const text = String(chunk);
568
- stdoutText += text;
569
- if (options.stream) runtime.stdout?.write(text);
570
- });
571
- child.stderr?.on('data', (chunk) => {
572
- const text = String(chunk);
573
- stderrText += text;
574
- if (options.stream) runtime.stderr?.write(text);
575
- });
576
- child.on('error', (error) => {
577
- resolve({ ok: false, stdout: stdoutText, stderr: stderrText, error });
578
- });
579
- child.on('close', (code) => {
580
- const ok = code === 0;
581
- resolve({
582
- ok,
583
- stdout: stdoutText,
584
- stderr: stderrText,
585
- error: ok ? null : Object.assign(new Error(`${command} exited with code ${code}`), { code }),
586
- });
587
- });
588
- });
604
+ candidates.sort((left, right) => fileModifiedAt(right) - fileModifiedAt(left));
605
+ return candidates[0] ?? null;
589
606
  }
590
607
 
608
+ function fileModifiedAt(filePath) {
609
+ try {
610
+ return statSync(filePath).mtimeMs;
611
+ } catch {
612
+ return 0;
613
+ }
614
+ }
615
+
616
+ function codexMcpListShowsDistributionOs(output) {
617
+ return Boolean(findCodexMcpListDistributionOsLine(output));
618
+ }
619
+
620
+ function codexMcpListShowsDistributionOsNotLoggedIn(output) {
621
+ const line = findCodexMcpListDistributionOsLine(output);
622
+ return Boolean(line && /not\s+logged\s+in/i.test(line));
623
+ }
624
+
625
+ function findCodexMcpListDistributionOsLine(output) {
626
+ return String(output)
627
+ .split(/\r?\n/)
628
+ .map((line) => line.trim())
629
+ .find((line) => /\bdistributionos\b/i.test(line)) ?? null;
630
+ }
631
+
632
+ async function runCodexCommand(runtime, codexCommand, args, options = {}) {
633
+ return runExternalCommand(runtime, codexCommand.command, args, options);
634
+ }
635
+
636
+ async function runExternalCommand(runtime, command, args, options = {}) {
637
+ if (typeof runtime.execFile === 'function') {
638
+ try {
639
+ const result = await runtime.execFile(command, args, options);
640
+ return {
641
+ ok: true,
642
+ stdout: result?.stdout ?? '',
643
+ stderr: result?.stderr ?? '',
644
+ };
645
+ } catch (error) {
646
+ return {
647
+ ok: false,
648
+ stdout: error?.stdout ?? '',
649
+ stderr: error?.stderr ?? '',
650
+ error,
651
+ };
652
+ }
653
+ }
654
+
655
+ const resolved = resolveCommand(command, args, runtime);
656
+ return new Promise((resolve) => {
657
+ let stdoutText = '';
658
+ let stderrText = '';
659
+ let child;
660
+ try {
661
+ child = spawn(resolved.command, resolved.args, {
662
+ cwd: runtime.cwd,
663
+ env: runtime.env,
664
+ windowsHide: true,
665
+ stdio: ['inherit', 'pipe', 'pipe'],
666
+ });
667
+ } catch (error) {
668
+ resolve({ ok: false, stdout: '', stderr: '', error });
669
+ return;
670
+ }
671
+
672
+ child.stdout?.on('data', (chunk) => {
673
+ const text = String(chunk);
674
+ stdoutText += text;
675
+ if (options.stream) runtime.stdout?.write(text);
676
+ });
677
+ child.stderr?.on('data', (chunk) => {
678
+ const text = String(chunk);
679
+ stderrText += text;
680
+ if (options.stream) runtime.stderr?.write(text);
681
+ });
682
+ child.on('error', (error) => {
683
+ resolve({ ok: false, stdout: stdoutText, stderr: stderrText, error });
684
+ });
685
+ child.on('close', (code) => {
686
+ const ok = code === 0;
687
+ resolve({
688
+ ok,
689
+ stdout: stdoutText,
690
+ stderr: stderrText,
691
+ error: ok ? null : Object.assign(new Error(`${command} exited with code ${code}`), { code }),
692
+ });
693
+ });
694
+ });
695
+ }
696
+
591
697
  function resolveCommand(command, args, runtime) {
592
698
  const platform = runtime.platform ?? process.platform;
593
699
  if (platform === 'win32') {
700
+ if (path.isAbsolute(command) && /\.(exe|com)$/i.test(command)) {
701
+ return { command, args };
702
+ }
594
703
  return {
595
704
  command: runtime.env?.ComSpec ?? process.env.ComSpec ?? 'cmd.exe',
596
705
  args: ['/d', '/s', '/c', command, ...args],
597
- };
598
- }
599
- return { command, args };
600
- }
706
+ };
707
+ }
708
+ return { command, args };
709
+ }
601
710
 
602
711
  async function ensureCliAuth({ args, runtime, stderr }) {
603
712
  const existingToken = await resolveAuthToken({
@@ -749,10 +858,11 @@ function formatApplyResult(plan, changes, initialization, validation, inspection
749
858
  lines.push('', 'Next step');
750
859
  if (reviewUrl) {
751
860
  lines.push(
752
- '- Open this DistributionOS review page now:',
753
- ` ${reviewUrl}`,
754
- '- Codex users: make sure `codex mcp add distributionos --url https://distributionos.dev/api/mcp` and `codex mcp login distributionos` have run, then fully quit and reopen Codex Desktop before checking the connection.',
755
- '- Claude Code users: if .mcp.json was created or updated, restart or reconnect Claude Code and approve the project MCP server if asked.',
861
+ '- Open this DistributionOS review page now:',
862
+ ` ${reviewUrl}`,
863
+ '- Codex users: if you used the Codex setup option, the CLI already ran MCP add/login against the Codex runtime and verified it did not report DistributionOS as Not logged in.',
864
+ '- Then fully quit and reopen Codex Desktop before checking the connection.',
865
+ '- Claude Code users: if .mcp.json was created or updated, restart or reconnect Claude Code and approve the project MCP server if asked.',
756
866
  '- Other MCP-aware agents: reconnect the agent from this repo so it can load the DistributionOS MCP server.',
757
867
  '- On the review page, copy the connection check prompt into the fresh/reconnected agent chat.',
758
868
  '- If your agent only sees DistributionOS authentication tools, copy the auth prompt from the review page and complete OAuth before continuing.',