@distributionos/cli 0.1.14 → 0.1.16

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,17 @@
1
- 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';
1
+ 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';
9
9
  import { loginWithOAuth } from './oauth.js';
10
10
  import { createSetupPlan, formatPlanText } from './plan.js';
11
11
  import { applySetupPlan } from './mutate.js';
12
- import { submitInitialization } from './initialization.js';
13
- import { compareValidationResults, inspectInstallArtifacts, runValidationPlan } from './validation.js';
14
- import { buildSetupReport } from './setup-report.js';
12
+ import { submitInitialization } from './initialization.js';
13
+ import { compareValidationResults, inspectInstallArtifacts, runValidationPlan } from './validation.js';
14
+ import { buildSetupReport } from './setup-report.js';
15
15
 
16
16
  export async function runCli(argv, runtime) {
17
17
  const stdout = runtime.stdout;
@@ -141,21 +141,21 @@ export async function runCli(argv, runtime) {
141
141
  if (!authReady) return 1;
142
142
  }
143
143
 
144
- let plan = await createSetupPlan({
145
- appId: args.appId,
146
- cwd: args.cwd ?? runtime.cwd,
147
- apiBase: args.apiBase ?? DEFAULT_API_BASE,
144
+ let plan = await createSetupPlan({
145
+ appId: args.appId,
146
+ cwd: args.cwd ?? runtime.cwd,
147
+ apiBase: args.apiBase ?? DEFAULT_API_BASE,
148
148
  env: runtime.env,
149
149
  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
- });
150
+ noFetch: args.noFetch,
151
+ apply: args.apply,
152
+ });
153
+ plan.setupReport = await submitSetupReportSafely({
154
+ args,
155
+ runtime,
156
+ plan,
157
+ phase: 'plan',
158
+ });
159
159
 
160
160
  if (!args.apply && !args.yes) {
161
161
  stdout.write(args.json ? `${JSON.stringify(plan, null, 2)}\n` : formatPlanText(plan));
@@ -165,19 +165,19 @@ export async function runCli(argv, runtime) {
165
165
  args.apply = true;
166
166
  }
167
167
 
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
- }
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
+ }
181
181
 
182
182
  if (args.apply && !args.skipAnalytics) {
183
183
  const ensured = await ensureAnalyticsSite({
@@ -193,21 +193,21 @@ export async function runCli(argv, runtime) {
193
193
  }
194
194
  }
195
195
 
196
- plan = await createSetupPlan({
197
- appId: args.appId,
198
- cwd: args.cwd ?? runtime.cwd,
199
- apiBase: args.apiBase ?? DEFAULT_API_BASE,
196
+ plan = await createSetupPlan({
197
+ appId: args.appId,
198
+ cwd: args.cwd ?? runtime.cwd,
199
+ apiBase: args.apiBase ?? DEFAULT_API_BASE,
200
200
  env: runtime.env,
201
201
  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
- });
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
+ });
211
211
 
212
212
  if (!args.apply) {
213
213
  stdout.write(args.json ? `${JSON.stringify(plan, null, 2)}\n` : formatPlanText(plan));
@@ -219,119 +219,119 @@ export async function runCli(argv, runtime) {
219
219
  const changes = await applySetupPlan(plan, {
220
220
  allowDirty: args.allowDirty,
221
221
  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,
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,
242
242
  validation,
243
243
  inspection,
244
- requiresAgentRestart: changes.some((change) => change.type === 'mcp' && change.changed),
244
+ requiresAgentRestart: changes.some((change) => (change.type === 'mcp' || change.type === 'codex-mcp') && change.changed),
245
245
  },
246
246
  });
247
- const token = await resolveAuthToken({
248
- env: runtime.env,
249
- apiBase: args.apiBase ?? DEFAULT_API_BASE,
250
- appId: args.appId,
251
- fetchImpl: runtime.fetch,
247
+ const token = await resolveAuthToken({
248
+ env: runtime.env,
249
+ apiBase: args.apiBase ?? DEFAULT_API_BASE,
250
+ appId: args.appId,
251
+ fetchImpl: runtime.fetch,
252
252
  });
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));
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));
266
266
  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) {
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) {
335
335
  const parsed = {
336
336
  command: null,
337
337
  appId: null,
@@ -343,11 +343,11 @@ export function parseArgs(rawArgs) {
343
343
  apply: false,
344
344
  allowDirty: false,
345
345
  skipAnalytics: false,
346
- skipValidate: false,
347
- noOpen: false,
348
- noLogin: false,
349
- skipSetupReport: false,
350
- yes: false,
346
+ skipValidate: false,
347
+ noOpen: false,
348
+ noLogin: false,
349
+ skipSetupReport: false,
350
+ yes: false,
351
351
  url: null,
352
352
  contentId: null,
353
353
  contractVersion: null,
@@ -379,11 +379,11 @@ export function parseArgs(rawArgs) {
379
379
  else if (arg === '--apply') parsed.apply = true;
380
380
  else if (arg === '--allow-dirty') parsed.allowDirty = true;
381
381
  else if (arg === '--skip-analytics') parsed.skipAnalytics = true;
382
- 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;
382
+ 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;
387
387
  else if (arg === '--app') parsed.appId = args[++index] ?? null;
388
388
  else if (arg.startsWith('--app=')) parsed.appId = arg.slice('--app='.length);
389
389
  else if (arg === '--cwd') parsed.cwd = args[++index] ?? null;
@@ -416,15 +416,15 @@ function helpText() {
416
416
  return [
417
417
  'DistributionOS CLI',
418
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]',
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]',
422
422
  ' distributionos login --app <appId> [--api-base <url>]',
423
423
  ' distributionos verify --app <appId> --url <liveUrl> [--content-id <id>]',
424
424
  ' distributionos report-implementation --app <appId> --artifact <artifactId> [--url <liveUrl>] [--summary <text>]',
425
425
  '',
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.',
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.',
428
428
  '',
429
429
  'Authentication:',
430
430
  ' Happy path uses DistributionOS MCP OAuth and stores app-scoped credentials outside the repo.',
@@ -528,13 +528,13 @@ function formatReportResult(result) {
528
528
  return `${lines.join('\n')}\n`;
529
529
  }
530
530
 
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
- '',
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
+ '',
538
538
  'Applied changes',
539
539
  ];
540
540
 
@@ -548,9 +548,9 @@ function formatApplyResult(plan, changes, initialization, validation, inspection
548
548
  }
549
549
  }
550
550
 
551
- lines.push(reviewUrl
552
- ? `- Initialization submitted. Review URL: ${reviewUrl}`
553
- : '- Initialization was not submitted because no auth token was available.');
551
+ lines.push(reviewUrl
552
+ ? `- Initialization submitted. Review URL: ${reviewUrl}`
553
+ : '- Initialization was not submitted because no auth token was available.');
554
554
 
555
555
  lines.push('', 'Validation');
556
556
  if (skipValidate) {
@@ -579,36 +579,39 @@ function formatApplyResult(plan, changes, initialization, validation, inspection
579
579
  for (const result of inspection) {
580
580
  lines.push(`- ${result.name}: ${result.status} - ${result.message}`);
581
581
  }
582
- }
583
- lines.push('- Nothing was committed, pushed, or deployed.');
584
- lines.push('', 'Next step');
585
- if (reviewUrl) {
586
- lines.push(
582
+ }
583
+ lines.push('- Nothing was committed, pushed, or deployed.');
584
+ lines.push('', 'Next step');
585
+ if (reviewUrl) {
586
+ lines.push(
587
587
  '- Open this DistributionOS review page now:',
588
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 verification prompt and ask your agent to run it.',
591
- '- DistributionOS will wait for a successful app-scoped MCP/API call before you continue to Brain Doc review.',
592
- '- Verification prompt: "Check DistributionOS connection, fetch the latest agent instructions, then tell me today\'s analytics summary for this app."',
593
- '- After review, come back to this repo and inspect git diff before committing, pushing, or deploying.',
594
- );
595
- } else {
596
- lines.push(
597
- '- Reconnect DistributionOS OAuth or provide an app-scoped token, then rerun setup so initialization can be submitted.',
598
- '- No DistributionOS review page is ready yet.',
599
- );
600
- }
601
- return `${lines.join('\n')}\n`;
602
- }
603
-
604
- function oneLine(value) {
605
- return String(value).split(/\r?\n/).filter(Boolean).slice(-1)[0]?.slice(0, 240) ?? '';
606
- }
607
-
608
- function absoluteDistributionOsUrl(pathOrUrl, apiBase) {
609
- try {
610
- return new URL(pathOrUrl, apiBase || DEFAULT_API_BASE).toString();
611
- } catch {
612
- return String(pathOrUrl);
613
- }
614
- }
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.',
593
+ '- If your agent only sees DistributionOS authentication tools, copy the auth prompt from the review page and complete OAuth before continuing.',
594
+ '- 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
+ '- DistributionOS will wait for a successful app-scoped MCP call before you continue to Brain Doc review.',
596
+ '- After review, come back to this repo and inspect git diff before committing, pushing, or deploying.',
597
+ );
598
+ } else {
599
+ lines.push(
600
+ '- Reconnect DistributionOS OAuth or provide an app-scoped token, then rerun setup so initialization can be submitted.',
601
+ '- No DistributionOS review page is ready yet.',
602
+ );
603
+ }
604
+ return `${lines.join('\n')}\n`;
605
+ }
606
+
607
+ function oneLine(value) {
608
+ return String(value).split(/\r?\n/).filter(Boolean).slice(-1)[0]?.slice(0, 240) ?? '';
609
+ }
610
+
611
+ function absoluteDistributionOsUrl(pathOrUrl, apiBase) {
612
+ try {
613
+ return new URL(pathOrUrl, apiBase || DEFAULT_API_BASE).toString();
614
+ } catch {
615
+ return String(pathOrUrl);
616
+ }
617
+ }
package/src/constants.js CHANGED
@@ -6,9 +6,9 @@ export const CLI_PACKAGE_NAME = '@distributionos/cli';
6
6
  export const CLI_LOCAL_VERSION = typeof cliPackageJson.version === 'string' ? cliPackageJson.version : '0.0.0-local';
7
7
  export const DEFAULT_API_BASE = 'https://distributionos.dev';
8
8
  export const MCP_SERVER_URL = 'https://distributionos.dev/api/mcp';
9
- export const CURRENT_BOOTSTRAP_VERSION = '2026.06.14';
10
- export const CURRENT_ANALYTICS_CONTRACT_VERSION = '2026.06.14';
11
- export const CURRENT_SETUP_CONTRACT_VERSION = '2026.06.18';
9
+ export const CURRENT_BOOTSTRAP_VERSION = '2026.06.27';
10
+ export const CURRENT_ANALYTICS_CONTRACT_VERSION = '2026.06.20';
11
+ export const CURRENT_SETUP_CONTRACT_VERSION = '2026.07.01';
12
12
 
13
13
  export const TOKEN_ENV_NAMES = [
14
14
  'DISTRIBUTIONOS_API_KEY',
@@ -49,11 +49,11 @@ export const PRIVATE_ROUTE_PATTERNS = [
49
49
  '/products/**',
50
50
  '/project/**',
51
51
  '/projects/**',
52
- '/workspace/**',
53
- '/workspaces/**',
54
- '/workflow/**',
55
- '/workflows/**',
56
- '/customer/**',
52
+ '/workspace/**',
53
+ '/workspaces/**',
54
+ '/workflow/**',
55
+ '/workflows/**',
56
+ '/customer/**',
57
57
  '/customers/**',
58
58
  '/client/**',
59
59
  '/clients/**',