@distributionos/cli 0.1.15 → 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/src/cli.js CHANGED
@@ -1,17 +1,22 @@
1
+ import { spawn } from 'node:child_process';
1
2
  import { CLI_LOCAL_VERSION, DEFAULT_API_BASE } from './constants.js';
2
- import {
3
- ensureAnalyticsSite,
4
- reportSetupInstallation,
5
- reportImplementation,
6
- resolveAuthToken,
7
- verifyAnalyticsInstall,
8
- } from './api.js';
3
+ import {
4
+ ensureAnalyticsSite,
5
+ reportSetupInstallation,
6
+ reportImplementation,
7
+ resolveAuthToken,
8
+ verifyAnalyticsInstall,
9
+ } from './api.js';
9
10
  import { loginWithOAuth } from './oauth.js';
10
11
  import { createSetupPlan, formatPlanText } from './plan.js';
11
12
  import { applySetupPlan } from './mutate.js';
12
- import { submitInitialization } from './initialization.js';
13
- import { compareValidationResults, inspectInstallArtifacts, runValidationPlan } from './validation.js';
13
+ import { submitInitialization } from './initialization.js';
14
+ import { compareValidationResults, inspectInstallArtifacts, runValidationPlan } from './validation.js';
14
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,32 +135,35 @@ 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
- let plan = await createSetupPlan({
145
- appId: args.appId,
146
- cwd: args.cwd ?? runtime.cwd,
147
- apiBase: args.apiBase ?? DEFAULT_API_BASE,
152
+ let plan = await createSetupPlan({
153
+ appId: args.appId,
154
+ cwd: args.cwd ?? runtime.cwd,
155
+ apiBase: args.apiBase ?? DEFAULT_API_BASE,
148
156
  env: runtime.env,
149
157
  fetch: runtime.fetch,
150
- noFetch: args.noFetch,
151
- apply: args.apply,
152
- });
153
- plan.setupReport = await submitSetupReportSafely({
154
- args,
155
- runtime,
156
- plan,
157
- phase: 'plan',
158
- });
158
+ noFetch: args.noFetch,
159
+ apply: args.apply,
160
+ });
161
+ plan.setupReport = await submitSetupReportSafely({
162
+ args,
163
+ runtime,
164
+ plan,
165
+ phase: 'plan',
166
+ });
159
167
 
160
168
  if (!args.apply && !args.yes) {
161
169
  stdout.write(args.json ? `${JSON.stringify(plan, null, 2)}\n` : formatPlanText(plan));
@@ -165,19 +173,19 @@ export async function runCli(argv, runtime) {
165
173
  args.apply = true;
166
174
  }
167
175
 
168
- if (args.yes) {
169
- args.apply = true;
170
- }
171
-
172
- if (args.apply && hasRemoteAccessBlock(plan)) {
173
- stderr.write(formatRemoteAccessBlock(plan));
174
- return 1;
175
- }
176
-
177
- if (args.apply && plan.repo.git.dirty && !args.allowDirty) {
178
- stderr.write('Refusing to apply changes to a dirty worktree. Commit/stash changes or pass --allow-dirty.\n');
179
- return 1;
180
- }
176
+ if (args.yes) {
177
+ args.apply = true;
178
+ }
179
+
180
+ if (args.apply && hasRemoteAccessBlock(plan)) {
181
+ stderr.write(formatRemoteAccessBlock(plan));
182
+ return 1;
183
+ }
184
+
185
+ if (args.apply && plan.repo.git.dirty && !args.allowDirty) {
186
+ stderr.write('Refusing to apply changes to a dirty worktree. Commit/stash changes or pass --allow-dirty.\n');
187
+ return 1;
188
+ }
181
189
 
182
190
  if (args.apply && !args.skipAnalytics) {
183
191
  const ensured = await ensureAnalyticsSite({
@@ -193,21 +201,21 @@ export async function runCli(argv, runtime) {
193
201
  }
194
202
  }
195
203
 
196
- plan = await createSetupPlan({
197
- appId: args.appId,
198
- cwd: args.cwd ?? runtime.cwd,
199
- apiBase: args.apiBase ?? DEFAULT_API_BASE,
204
+ plan = await createSetupPlan({
205
+ appId: args.appId,
206
+ cwd: args.cwd ?? runtime.cwd,
207
+ apiBase: args.apiBase ?? DEFAULT_API_BASE,
200
208
  env: runtime.env,
201
209
  fetch: runtime.fetch,
202
- noFetch: args.noFetch,
203
- apply: args.apply,
204
- });
205
- plan.setupReport = await submitSetupReportSafely({
206
- args,
207
- runtime,
208
- plan,
209
- phase: args.apply ? 'apply_started' : 'plan',
210
- });
210
+ noFetch: args.noFetch,
211
+ apply: args.apply,
212
+ });
213
+ plan.setupReport = await submitSetupReportSafely({
214
+ args,
215
+ runtime,
216
+ plan,
217
+ phase: args.apply ? 'apply_started' : 'plan',
218
+ });
211
219
 
212
220
  if (!args.apply) {
213
221
  stdout.write(args.json ? `${JSON.stringify(plan, null, 2)}\n` : formatPlanText(plan));
@@ -219,119 +227,120 @@ export async function runCli(argv, runtime) {
219
227
  const changes = await applySetupPlan(plan, {
220
228
  allowDirty: args.allowDirty,
221
229
  skipAnalytics: args.skipAnalytics,
222
- });
223
- const postValidation = args.skipValidate ? [] : await runValidationPlan(plan);
224
- const validation = compareValidationResults(baselineValidation, postValidation);
225
- const inspection = args.skipAnalytics ? [] : await inspectInstallArtifacts(plan);
226
- const completedPlan = await createSetupPlan({
227
- appId: args.appId,
228
- cwd: args.cwd ?? runtime.cwd,
229
- apiBase: args.apiBase ?? DEFAULT_API_BASE,
230
- env: runtime.env,
231
- fetch: runtime.fetch,
232
- noFetch: args.noFetch,
233
- apply: true,
234
- });
235
- completedPlan.setupReport = await submitSetupReportSafely({
236
- args,
237
- runtime,
238
- plan: completedPlan,
239
- phase: 'apply_completed',
240
- details: {
241
- changes,
230
+ });
231
+ const postValidation = args.skipValidate ? [] : await runValidationPlan(plan);
232
+ const validation = compareValidationResults(baselineValidation, postValidation);
233
+ const inspection = args.skipAnalytics ? [] : await inspectInstallArtifacts(plan);
234
+ const completedPlan = await createSetupPlan({
235
+ appId: args.appId,
236
+ cwd: args.cwd ?? runtime.cwd,
237
+ apiBase: args.apiBase ?? DEFAULT_API_BASE,
238
+ env: runtime.env,
239
+ fetch: runtime.fetch,
240
+ noFetch: args.noFetch,
241
+ apply: true,
242
+ });
243
+ completedPlan.setupReport = await submitSetupReportSafely({
244
+ args,
245
+ runtime,
246
+ plan: completedPlan,
247
+ phase: 'apply_completed',
248
+ details: {
249
+ changes,
242
250
  validation,
243
251
  inspection,
244
- requiresAgentRestart: changes.some((change) => change.type === '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
- const token = await resolveAuthToken({
248
- env: runtime.env,
249
- apiBase: args.apiBase ?? DEFAULT_API_BASE,
250
- appId: args.appId,
251
- fetchImpl: runtime.fetch,
256
+ const token = await resolveAuthToken({
257
+ env: runtime.env,
258
+ apiBase: args.apiBase ?? DEFAULT_API_BASE,
259
+ appId: args.appId,
260
+ fetchImpl: runtime.fetch,
252
261
  });
253
- const initialization = token
254
- ? await submitInitialization({
255
- plan: completedPlan,
256
- token: token.value,
257
- apiBase: args.apiBase ?? DEFAULT_API_BASE,
258
- fetchImpl: runtime.fetch,
259
- domain: args.domain,
260
- })
261
- : null;
262
-
263
- stdout.write(args.json
264
- ? `${JSON.stringify({ plan: completedPlan, changes, initialization, validation, inspection }, null, 2)}\n`
265
- : formatApplyResult(completedPlan, changes, initialization, validation, inspection, args.skipValidate, args.apiBase ?? DEFAULT_API_BASE));
262
+ const initialization = token
263
+ ? await submitInitialization({
264
+ plan: completedPlan,
265
+ token: token.value,
266
+ apiBase: args.apiBase ?? DEFAULT_API_BASE,
267
+ fetchImpl: runtime.fetch,
268
+ domain: args.domain,
269
+ })
270
+ : null;
271
+
272
+ stdout.write(args.json
273
+ ? `${JSON.stringify({ plan: completedPlan, changes, initialization, validation, inspection }, null, 2)}\n`
274
+ : formatApplyResult(completedPlan, changes, initialization, validation, inspection, args.skipValidate, args.apiBase ?? DEFAULT_API_BASE));
266
275
  return hasBlockingApplyFailure(validation, inspection) ? 1 : 0;
267
- } catch (error) {
268
- await submitSetupReportSafely({
269
- args,
270
- runtime,
271
- plan,
272
- phase: 'apply_failed',
273
- details: {
274
- applyResult: {
275
- validation: [{
276
- name: 'apply',
277
- status: 'failed',
278
- }],
279
- },
280
- },
281
- });
282
- stderr.write(`${error instanceof Error ? error.message : 'Setup apply failed'}\n`);
283
- return 1;
284
- }
285
- }
286
-
287
- async function submitSetupReportSafely({ args, runtime, plan, phase, details = {} }) {
288
- if (args.noFetch || args.skipSetupReport) {
289
- return null;
290
- }
291
-
292
- try {
293
- const report = await buildSetupReport(plan, phase, details);
294
- const result = await reportSetupInstallation({
295
- appId: args.appId,
296
- apiBase: args.apiBase ?? DEFAULT_API_BASE,
297
- env: runtime.env,
298
- fetch: runtime.fetch,
299
- report,
300
- });
301
- return {
302
- status: 'submitted',
303
- phase,
304
- result,
305
- };
306
- } catch (error) {
307
- return {
308
- status: 'failed',
309
- phase,
310
- error: error instanceof Error ? error.message : 'Setup report failed',
311
- };
312
- }
313
- }
314
-
315
- function hasBlockingApplyFailure(validation, inspection) {
316
- return validation.some((result) => result.introducedFailure) ||
317
- inspection.some((result) => result.status === 'failed');
318
- }
319
-
320
- function hasRemoteAccessBlock(plan) {
321
- return plan.remote?.access?.reason === 'subscription_required';
322
- }
323
-
324
- function formatRemoteAccessBlock(plan) {
325
- const billingUrl = plan.remote?.access?.billingUrl ?? `${plan.apiBase ?? DEFAULT_API_BASE}/dashboard/account/billing`;
326
- return [
327
- 'DistributionOS setup requires an active Builder or Distributor subscription before applying changes.',
328
- `Manage billing: ${billingUrl}`,
329
- 'No files were changed.',
330
- '',
331
- ].join('\n');
332
- }
333
-
334
- export function parseArgs(rawArgs) {
276
+ } catch (error) {
277
+ await submitSetupReportSafely({
278
+ args,
279
+ runtime,
280
+ plan,
281
+ phase: 'apply_failed',
282
+ details: {
283
+ applyResult: {
284
+ validation: [{
285
+ name: 'apply',
286
+ status: 'failed',
287
+ }],
288
+ },
289
+ },
290
+ });
291
+ stderr.write(`${error instanceof Error ? error.message : 'Setup apply failed'}\n`);
292
+ return 1;
293
+ }
294
+ }
295
+
296
+ async function submitSetupReportSafely({ args, runtime, plan, phase, details = {} }) {
297
+ if (args.noFetch || args.skipSetupReport) {
298
+ return null;
299
+ }
300
+
301
+ try {
302
+ const report = await buildSetupReport(plan, phase, details);
303
+ const result = await reportSetupInstallation({
304
+ appId: args.appId,
305
+ apiBase: args.apiBase ?? DEFAULT_API_BASE,
306
+ env: runtime.env,
307
+ fetch: runtime.fetch,
308
+ report,
309
+ });
310
+ return {
311
+ status: 'submitted',
312
+ phase,
313
+ result,
314
+ };
315
+ } catch (error) {
316
+ return {
317
+ status: 'failed',
318
+ phase,
319
+ error: error instanceof Error ? error.message : 'Setup report failed',
320
+ };
321
+ }
322
+ }
323
+
324
+ function hasBlockingApplyFailure(validation, inspection) {
325
+ return validation.some((result) => result.introducedFailure) ||
326
+ inspection.some((result) => result.status === 'failed');
327
+ }
328
+
329
+ function hasRemoteAccessBlock(plan) {
330
+ return plan.remote?.access?.reason === 'subscription_required';
331
+ }
332
+
333
+ function formatRemoteAccessBlock(plan) {
334
+ const billingUrl = plan.remote?.access?.billingUrl ?? `${plan.apiBase ?? DEFAULT_API_BASE}/dashboard/account/billing`;
335
+ return [
336
+ 'DistributionOS setup requires an active Builder or Distributor subscription before applying changes.',
337
+ `Manage billing: ${billingUrl}`,
338
+ 'No files were changed.',
339
+ '',
340
+ ].join('\n');
341
+ }
342
+
343
+ export function parseArgs(rawArgs) {
335
344
  const parsed = {
336
345
  command: null,
337
346
  appId: null,
@@ -343,12 +352,14 @@ export function parseArgs(rawArgs) {
343
352
  apply: false,
344
353
  allowDirty: false,
345
354
  skipAnalytics: false,
346
- skipValidate: false,
347
- noOpen: false,
355
+ skipValidate: false,
356
+ noOpen: false,
348
357
  noLogin: false,
349
358
  skipSetupReport: false,
359
+ skipAgentSetup: false,
350
360
  yes: false,
351
- url: null,
361
+ agent: null,
362
+ url: null,
352
363
  contentId: null,
353
364
  contractVersion: null,
354
365
  artifactId: null,
@@ -379,11 +390,14 @@ export function parseArgs(rawArgs) {
379
390
  else if (arg === '--apply') parsed.apply = true;
380
391
  else if (arg === '--allow-dirty') parsed.allowDirty = true;
381
392
  else if (arg === '--skip-analytics') parsed.skipAnalytics = true;
382
- else if (arg === '--skip-validate') parsed.skipValidate = true;
393
+ else if (arg === '--skip-validate') parsed.skipValidate = true;
383
394
  else if (arg === '--no-open') parsed.noOpen = true;
384
395
  else if (arg === '--no-login') parsed.noLogin = true;
385
396
  else if (arg === '--skip-setup-report') parsed.skipSetupReport = true;
397
+ else if (arg === '--skip-agent-setup') parsed.skipAgentSetup = true;
386
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,18 +426,19 @@ export function parseArgs(rawArgs) {
412
426
  return parsed;
413
427
  }
414
428
 
415
- function helpText() {
416
- return [
417
- 'DistributionOS CLI',
418
- '',
429
+ function helpText() {
430
+ return [
431
+ 'DistributionOS CLI',
432
+ '',
419
433
  '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]',
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
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.',
427
442
  'If the repo has existing uncommitted changes, commit/stash first or use --allow-dirty when you recognize them.',
428
443
  '',
429
444
  'Authentication:',
@@ -431,8 +446,158 @@ function helpText() {
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({
@@ -528,13 +693,13 @@ function formatReportResult(result) {
528
693
  return `${lines.join('\n')}\n`;
529
694
  }
530
695
 
531
- function formatApplyResult(plan, changes, initialization, validation, inspection, skipValidate, apiBase = DEFAULT_API_BASE) {
532
- const reviewUrl = initialization?.reviewUrl
533
- ? absoluteDistributionOsUrl(initialization.reviewUrl, apiBase)
534
- : null;
535
- const lines = [
536
- formatPlanText({ ...plan, mode: 'apply' }).trimEnd(),
537
- '',
696
+ function formatApplyResult(plan, changes, initialization, validation, inspection, skipValidate, apiBase = DEFAULT_API_BASE) {
697
+ const reviewUrl = initialization?.reviewUrl
698
+ ? absoluteDistributionOsUrl(initialization.reviewUrl, apiBase)
699
+ : null;
700
+ const lines = [
701
+ formatPlanText({ ...plan, mode: 'apply' }).trimEnd(),
702
+ '',
538
703
  'Applied changes',
539
704
  ];
540
705
 
@@ -548,9 +713,9 @@ function formatApplyResult(plan, changes, initialization, validation, inspection
548
713
  }
549
714
  }
550
715
 
551
- lines.push(reviewUrl
552
- ? `- Initialization submitted. Review URL: ${reviewUrl}`
553
- : '- Initialization was not submitted because no auth token was available.');
716
+ lines.push(reviewUrl
717
+ ? `- Initialization submitted. Review URL: ${reviewUrl}`
718
+ : '- Initialization was not submitted because no auth token was available.');
554
719
 
555
720
  lines.push('', 'Validation');
556
721
  if (skipValidate) {
@@ -579,37 +744,39 @@ function formatApplyResult(plan, changes, initialization, validation, inspection
579
744
  for (const result of inspection) {
580
745
  lines.push(`- ${result.name}: ${result.status} - ${result.message}`);
581
746
  }
582
- }
583
- lines.push('- Nothing was committed, pushed, or deployed.');
584
- lines.push('', 'Next step');
585
- if (reviewUrl) {
586
- lines.push(
587
- '- Open this DistributionOS review page now:',
588
- ` ${reviewUrl}`,
589
- '- If .mcp.json was created or updated, restart or reconnect your agent session so it can load the DistributionOS MCP server.',
590
- '- On the review page, copy the tool-list prompt first so your agent can show whether DistributionOS MCP loaded.',
591
- '- If your agent only sees DistributionOS authentication tools, copy the auth prompt from the review page and complete OAuth before continuing.',
592
- '- 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.',
593
- '- DistributionOS will wait for a successful app-scoped MCP call before you continue to Brain Doc review.',
594
- '- After review, come back to this repo and inspect git diff before committing, pushing, or deploying.',
595
- );
596
- } else {
597
- lines.push(
598
- '- Reconnect DistributionOS OAuth or provide an app-scoped token, then rerun setup so initialization can be submitted.',
599
- '- No DistributionOS review page is ready yet.',
600
- );
601
- }
602
- return `${lines.join('\n')}\n`;
603
- }
604
-
605
- function oneLine(value) {
606
- return String(value).split(/\r?\n/).filter(Boolean).slice(-1)[0]?.slice(0, 240) ?? '';
607
- }
608
-
609
- function absoluteDistributionOsUrl(pathOrUrl, apiBase) {
610
- try {
611
- return new URL(pathOrUrl, apiBase || DEFAULT_API_BASE).toString();
612
- } catch {
613
- return String(pathOrUrl);
614
- }
615
- }
747
+ }
748
+ lines.push('- Nothing was committed, pushed, or deployed.');
749
+ lines.push('', 'Next step');
750
+ if (reviewUrl) {
751
+ 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.',
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.',
758
+ '- If your agent only sees DistributionOS authentication tools, copy the auth prompt from the review page and complete OAuth before continuing.',
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.',
760
+ '- DistributionOS will wait for a successful app-scoped MCP call before you continue to Brain Doc review.',
761
+ '- After review, come back to this repo and inspect git diff before committing, pushing, or deploying.',
762
+ );
763
+ } else {
764
+ lines.push(
765
+ '- Reconnect DistributionOS OAuth or provide an app-scoped token, then rerun setup so initialization can be submitted.',
766
+ '- No DistributionOS review page is ready yet.',
767
+ );
768
+ }
769
+ return `${lines.join('\n')}\n`;
770
+ }
771
+
772
+ function oneLine(value) {
773
+ return String(value).split(/\r?\n/).filter(Boolean).slice(-1)[0]?.slice(0, 240) ?? '';
774
+ }
775
+
776
+ function absoluteDistributionOsUrl(pathOrUrl, apiBase) {
777
+ try {
778
+ return new URL(pathOrUrl, apiBase || DEFAULT_API_BASE).toString();
779
+ } catch {
780
+ return String(pathOrUrl);
781
+ }
782
+ }