@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.
- package/package.json +1 -1
- package/src/cli.js +260 -150
package/package.json
CHANGED
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
|
|
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
|
|
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
|
-
|
|
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('
|
|
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('
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
|
|
527
|
-
return runExternalCommand(runtime, 'codex', args, options);
|
|
568
|
+
return { command: 'codex', label: 'Codex CLI from PATH' };
|
|
528
569
|
}
|
|
529
570
|
|
|
530
|
-
|
|
531
|
-
if (
|
|
532
|
-
|
|
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
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
567
|
-
|
|
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:
|
|
755
|
-
'-
|
|
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.',
|