@distributionos/cli 0.1.16 → 0.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.
- package/package.json +1 -1
- package/src/cli.js +202 -37
- package/src/constants.js +1 -1
- package/src/setup-report.js +18 -18
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { CLI_LOCAL_VERSION, DEFAULT_API_BASE } from './constants.js';
|
|
2
3
|
import {
|
|
3
4
|
ensureAnalyticsSite,
|
|
4
5
|
reportSetupInstallation,
|
|
@@ -11,7 +12,11 @@ import { createSetupPlan, formatPlanText } from './plan.js';
|
|
|
11
12
|
import { applySetupPlan } from './mutate.js';
|
|
12
13
|
import { submitInitialization } from './initialization.js';
|
|
13
14
|
import { compareValidationResults, inspectInstallArtifacts, runValidationPlan } from './validation.js';
|
|
14
|
-
import { buildSetupReport } from './setup-report.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']);
|
|
15
20
|
|
|
16
21
|
export async function runCli(argv, runtime) {
|
|
17
22
|
const stdout = runtime.stdout;
|
|
@@ -130,15 +135,18 @@ export async function runCli(argv, runtime) {
|
|
|
130
135
|
return 1;
|
|
131
136
|
}
|
|
132
137
|
|
|
133
|
-
if (!args.appId) {
|
|
134
|
-
stderr.write('Missing required --app <appId>.\n\n');
|
|
135
|
-
stderr.write(helpText());
|
|
136
|
-
return 1;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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;
|
|
142
150
|
}
|
|
143
151
|
|
|
144
152
|
let plan = await createSetupPlan({
|
|
@@ -241,7 +249,8 @@ export async function runCli(argv, runtime) {
|
|
|
241
249
|
changes,
|
|
242
250
|
validation,
|
|
243
251
|
inspection,
|
|
244
|
-
requiresAgentRestart:
|
|
252
|
+
requiresAgentRestart: Boolean(agentSetup.requiresAgentRestart) ||
|
|
253
|
+
changes.some((change) => (change.type === 'mcp' || change.type === 'codex-mcp') && change.changed),
|
|
245
254
|
},
|
|
246
255
|
});
|
|
247
256
|
const token = await resolveAuthToken({
|
|
@@ -345,10 +354,12 @@ export function parseArgs(rawArgs) {
|
|
|
345
354
|
skipAnalytics: false,
|
|
346
355
|
skipValidate: false,
|
|
347
356
|
noOpen: false,
|
|
348
|
-
noLogin: false,
|
|
349
|
-
skipSetupReport: false,
|
|
350
|
-
|
|
351
|
-
|
|
357
|
+
noLogin: false,
|
|
358
|
+
skipSetupReport: false,
|
|
359
|
+
skipAgentSetup: false,
|
|
360
|
+
yes: false,
|
|
361
|
+
agent: null,
|
|
362
|
+
url: null,
|
|
352
363
|
contentId: null,
|
|
353
364
|
contractVersion: null,
|
|
354
365
|
artifactId: null,
|
|
@@ -380,10 +391,13 @@ export function parseArgs(rawArgs) {
|
|
|
380
391
|
else if (arg === '--allow-dirty') parsed.allowDirty = true;
|
|
381
392
|
else if (arg === '--skip-analytics') parsed.skipAnalytics = true;
|
|
382
393
|
else if (arg === '--skip-validate') parsed.skipValidate = true;
|
|
383
|
-
else if (arg === '--no-open') parsed.noOpen = true;
|
|
384
|
-
else if (arg === '--no-login') parsed.noLogin = true;
|
|
385
|
-
else if (arg === '--skip-setup-report') parsed.skipSetupReport = true;
|
|
386
|
-
else if (arg === '--
|
|
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));
|
|
387
401
|
else if (arg === '--app') parsed.appId = args[++index] ?? null;
|
|
388
402
|
else if (arg.startsWith('--app=')) parsed.appId = arg.slice('--app='.length);
|
|
389
403
|
else if (arg === '--cwd') parsed.cwd = args[++index] ?? null;
|
|
@@ -412,27 +426,178 @@ export function parseArgs(rawArgs) {
|
|
|
412
426
|
return parsed;
|
|
413
427
|
}
|
|
414
428
|
|
|
415
|
-
function helpText() {
|
|
416
|
-
return [
|
|
417
|
-
'DistributionOS CLI',
|
|
418
|
-
'',
|
|
419
|
-
'Usage:',
|
|
420
|
-
' distributionos setup --app <appId> [--api-base <url>] [--cwd <path>] [--json] [--no-fetch]',
|
|
421
|
-
' distributionos setup --app <appId> --apply [--domain <domain>] [--allow-dirty] [--skip-setup-report]',
|
|
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]',
|
|
422
436
|
' distributionos login --app <appId> [--api-base <url>]',
|
|
423
437
|
' distributionos verify --app <appId> --url <liveUrl> [--content-id <id>]',
|
|
424
438
|
' distributionos report-implementation --app <appId> --artifact <artifactId> [--url <liveUrl>] [--summary <text>]',
|
|
425
439
|
'',
|
|
426
|
-
'Default mode reviews the setup plan without changing files. Run again with --apply after the plan looks right.',
|
|
427
|
-
'
|
|
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.',
|
|
428
443
|
'',
|
|
429
444
|
'Authentication:',
|
|
430
445
|
' Happy path uses DistributionOS MCP OAuth and stores app-scoped credentials outside the repo.',
|
|
431
446
|
' DISTRIBUTIONOS_API_KEY, DOS_API_KEY, or DISTRIBUTIONOS_TOKEN are advanced fallbacks.',
|
|
432
447
|
' Tokens are never printed or written to committed files.',
|
|
433
448
|
'',
|
|
434
|
-
].join('\n');
|
|
435
|
-
}
|
|
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 };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
stdout.write('Codex MCP setup\n');
|
|
466
|
+
const getResult = await runCodexCommand(runtime, ['mcp', 'get', DISTRIBUTIONOS_MCP_NAME]);
|
|
467
|
+
if (getResult.ok) {
|
|
468
|
+
if (!getResult.stdout.includes(DISTRIBUTIONOS_MCP_URL)) {
|
|
469
|
+
stderr.write([
|
|
470
|
+
'Codex already has a distributionos MCP server, but it does not point to the expected DistributionOS URL.',
|
|
471
|
+
`Expected: ${DISTRIBUTIONOS_MCP_URL}`,
|
|
472
|
+
'Run `codex mcp remove distributionos`, then rerun this setup command.',
|
|
473
|
+
'',
|
|
474
|
+
].join('\n'));
|
|
475
|
+
return { ok: false, requiresAgentRestart: false };
|
|
476
|
+
}
|
|
477
|
+
stdout.write('- DistributionOS MCP server is already registered in Codex.\n');
|
|
478
|
+
} else if (isCommandMissing(getResult.error)) {
|
|
479
|
+
stderr.write([
|
|
480
|
+
'Codex CLI was not found, so DistributionOS cannot connect Codex automatically.',
|
|
481
|
+
'Open this repo in Codex Desktop or install the Codex CLI, then rerun setup.',
|
|
482
|
+
'',
|
|
483
|
+
].join('\n'));
|
|
484
|
+
return { ok: false, requiresAgentRestart: false };
|
|
485
|
+
} else {
|
|
486
|
+
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 });
|
|
488
|
+
if (!addResult.ok) {
|
|
489
|
+
stderr.write(formatCodexCommandFailure('codex mcp add distributionos', addResult));
|
|
490
|
+
return { ok: false, requiresAgentRestart: false };
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
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 });
|
|
496
|
+
if (!loginResult.ok) {
|
|
497
|
+
stderr.write(formatCodexCommandFailure('codex mcp login distributionos', loginResult));
|
|
498
|
+
return { ok: false, requiresAgentRestart: true };
|
|
499
|
+
}
|
|
500
|
+
stdout.write('- Codex MCP OAuth step completed.\n\n');
|
|
501
|
+
return { ok: true, requiresAgentRestart: true };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function formatCodexCommandFailure(label, result) {
|
|
505
|
+
const output = oneLine(result.stderr || result.stdout || result.error?.message || '');
|
|
506
|
+
return [
|
|
507
|
+
`${label} did not complete, so Codex cannot use DistributionOS tools yet.`,
|
|
508
|
+
output ? `Last output: ${output}` : '',
|
|
509
|
+
'Run the command above manually, approve the browser sign-in, then rerun DistributionOS setup.',
|
|
510
|
+
'',
|
|
511
|
+
].filter(Boolean).join('\n');
|
|
512
|
+
}
|
|
513
|
+
|
|
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
|
+
}
|
|
525
|
+
|
|
526
|
+
async function runCodexCommand(runtime, args, options = {}) {
|
|
527
|
+
return runExternalCommand(runtime, 'codex', args, options);
|
|
528
|
+
}
|
|
529
|
+
|
|
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
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
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;
|
|
564
|
+
}
|
|
565
|
+
|
|
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
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function resolveCommand(command, args, runtime) {
|
|
592
|
+
const platform = runtime.platform ?? process.platform;
|
|
593
|
+
if (platform === 'win32') {
|
|
594
|
+
return {
|
|
595
|
+
command: runtime.env?.ComSpec ?? process.env.ComSpec ?? 'cmd.exe',
|
|
596
|
+
args: ['/d', '/s', '/c', command, ...args],
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
return { command, args };
|
|
600
|
+
}
|
|
436
601
|
|
|
437
602
|
async function ensureCliAuth({ args, runtime, stderr }) {
|
|
438
603
|
const existingToken = await resolveAuthToken({
|
|
@@ -584,12 +749,12 @@ function formatApplyResult(plan, changes, initialization, validation, inspection
|
|
|
584
749
|
lines.push('', 'Next step');
|
|
585
750
|
if (reviewUrl) {
|
|
586
751
|
lines.push(
|
|
587
|
-
'- Open this DistributionOS review page now:',
|
|
588
|
-
` ${reviewUrl}`,
|
|
589
|
-
'- 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.',
|
|
590
|
-
'- Claude Code users: if .mcp.json was created or updated, restart or reconnect Claude Code and approve the project MCP server if asked.',
|
|
591
|
-
'- Other MCP-aware agents: reconnect the agent from this repo so it can load the DistributionOS MCP server.',
|
|
592
|
-
'- On the review page, copy the connection check prompt into the fresh/reconnected agent chat.',
|
|
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.',
|
|
756
|
+
'- Other MCP-aware agents: reconnect the agent from this repo so it can load the DistributionOS MCP server.',
|
|
757
|
+
'- On the review page, copy the connection check prompt into the fresh/reconnected agent chat.',
|
|
593
758
|
'- If your agent only sees DistributionOS authentication tools, copy the auth prompt from the review page and complete OAuth before continuing.',
|
|
594
759
|
'- Then copy the verification prompt from the review page and ask your agent to call check_distributionos_connection, get_agent_instructions, and get_analytics_summary.',
|
|
595
760
|
'- DistributionOS will wait for a successful app-scoped MCP call before you continue to Brain Doc review.',
|
package/src/constants.js
CHANGED
|
@@ -8,7 +8,7 @@ export const DEFAULT_API_BASE = 'https://distributionos.dev';
|
|
|
8
8
|
export const MCP_SERVER_URL = 'https://distributionos.dev/api/mcp';
|
|
9
9
|
export const CURRENT_BOOTSTRAP_VERSION = '2026.06.27';
|
|
10
10
|
export const CURRENT_ANALYTICS_CONTRACT_VERSION = '2026.06.20';
|
|
11
|
-
export const CURRENT_SETUP_CONTRACT_VERSION = '2026.07.01';
|
|
11
|
+
export const CURRENT_SETUP_CONTRACT_VERSION = '2026.07.01';
|
|
12
12
|
|
|
13
13
|
export const TOKEN_ENV_NAMES = [
|
|
14
14
|
'DISTRIBUTIONOS_API_KEY',
|
package/src/setup-report.js
CHANGED
|
@@ -35,24 +35,24 @@ export async function buildSetupReport(plan, phase = 'plan', details = {}) {
|
|
|
35
35
|
reason: plan.bootstrap.reason,
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
if (plan.mcp?.action === 'none') {
|
|
39
|
-
detectedCapabilities.add('agent.mcp.remoteConfig');
|
|
40
|
-
}
|
|
41
|
-
if (plan.codexMcp?.action === 'none') {
|
|
42
|
-
detectedCapabilities.add('agent.mcp.codexConfig');
|
|
43
|
-
}
|
|
44
|
-
if (plan.codexMcp?.action === 'manual') {
|
|
45
|
-
blockedReason = plan.codexMcp.reason;
|
|
46
|
-
} else if (plan.mcp?.action === 'manual') {
|
|
47
|
-
blockedReason = plan.mcp.reason;
|
|
48
|
-
}
|
|
49
|
-
if (plan.codexMcp) {
|
|
50
|
-
managedFiles.push({
|
|
51
|
-
path: plan.codexMcp.path,
|
|
52
|
-
type: 'agent.mcp.codexConfig',
|
|
53
|
-
action: plan.codexMcp.action,
|
|
54
|
-
status: plan.codexMcp.action === 'none' ? 'current' : plan.codexMcp.action === 'manual' ? 'blocked_manual' : 'planned',
|
|
55
|
-
reason: plan.codexMcp.reason,
|
|
38
|
+
if (plan.mcp?.action === 'none') {
|
|
39
|
+
detectedCapabilities.add('agent.mcp.remoteConfig');
|
|
40
|
+
}
|
|
41
|
+
if (plan.codexMcp?.action === 'none') {
|
|
42
|
+
detectedCapabilities.add('agent.mcp.codexConfig');
|
|
43
|
+
}
|
|
44
|
+
if (plan.codexMcp?.action === 'manual') {
|
|
45
|
+
blockedReason = plan.codexMcp.reason;
|
|
46
|
+
} else if (plan.mcp?.action === 'manual') {
|
|
47
|
+
blockedReason = plan.mcp.reason;
|
|
48
|
+
}
|
|
49
|
+
if (plan.codexMcp) {
|
|
50
|
+
managedFiles.push({
|
|
51
|
+
path: plan.codexMcp.path,
|
|
52
|
+
type: 'agent.mcp.codexConfig',
|
|
53
|
+
action: plan.codexMcp.action,
|
|
54
|
+
status: plan.codexMcp.action === 'none' ? 'current' : plan.codexMcp.action === 'manual' ? 'blocked_manual' : 'planned',
|
|
55
|
+
reason: plan.codexMcp.reason,
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
if (plan.mcp) {
|