@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@distributionos/cli",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
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,5 @@
1
- import { CLI_LOCAL_VERSION, DEFAULT_API_BASE } from './constants.js';
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
- if (!args.noFetch) {
140
- const authReady = await ensureCliAuth({ args, runtime, stderr });
141
- if (!authReady) return 1;
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: changes.some((change) => (change.type === 'mcp' || change.type === 'codex-mcp') && change.changed),
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
- yes: false,
351
- url: null,
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 === '--yes' || arg === '-y') parsed.yes = 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));
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
- 'If the repo has existing uncommitted changes, commit/stash first or use --allow-dirty when you recognize them.',
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',
@@ -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) {