@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/oauth.js CHANGED
@@ -27,14 +27,14 @@ export async function loginWithOAuth(options) {
27
27
  state,
28
28
  });
29
29
 
30
- options.stdout?.write([
31
- 'Open this DistributionOS authorization URL:',
32
- authorizeUrl,
33
- '',
34
- 'Keep this terminal running until the browser says the CLI is connected.',
35
- 'If the browser opens a failed connection page, press Ctrl+C here and rerun with --no-open, then paste the full printed URL into your browser.',
36
- '',
37
- ].join('\n'));
30
+ options.stdout?.write([
31
+ 'Open this DistributionOS authorization URL:',
32
+ authorizeUrl,
33
+ '',
34
+ 'Keep this terminal running until the browser says the CLI is connected.',
35
+ 'If the browser opens a failed connection page, press Ctrl+C here and rerun with --no-open, then paste the full printed URL into your browser.',
36
+ '',
37
+ ].join('\n'));
38
38
  if (options.openBrowser !== false) openUrl(authorizeUrl);
39
39
 
40
40
  const authResult = await callback.waitForCallback();
@@ -207,28 +207,28 @@ async function createCallbackServer() {
207
207
  };
208
208
  }
209
209
 
210
- export function getOpenUrlCommand(url, platform = process.platform) {
211
- const command = platform === 'win32'
212
- ? 'cmd'
213
- : platform === 'darwin'
214
- ? 'open'
215
- : 'xdg-open';
216
- const args = platform === 'win32'
217
- ? ['/c', 'start', '""', quoteWindowsStartUrl(url)]
218
- : [url];
219
- return { command, args };
220
- }
221
-
222
- function quoteWindowsStartUrl(url) {
223
- const safeUrl = String(url).replace(/"/g, '%22');
224
- return `"${safeUrl}"`;
225
- }
226
-
227
- function openUrl(url) {
228
- const { command, args } = getOpenUrlCommand(url);
229
- try {
230
- const child = spawn(command, args, {
231
- detached: true,
210
+ export function getOpenUrlCommand(url, platform = process.platform) {
211
+ const command = platform === 'win32'
212
+ ? 'cmd'
213
+ : platform === 'darwin'
214
+ ? 'open'
215
+ : 'xdg-open';
216
+ const args = platform === 'win32'
217
+ ? ['/c', 'start', '""', quoteWindowsStartUrl(url)]
218
+ : [url];
219
+ return { command, args };
220
+ }
221
+
222
+ function quoteWindowsStartUrl(url) {
223
+ const safeUrl = String(url).replace(/"/g, '%22');
224
+ return `"${safeUrl}"`;
225
+ }
226
+
227
+ function openUrl(url) {
228
+ const { command, args } = getOpenUrlCommand(url);
229
+ try {
230
+ const child = spawn(command, args, {
231
+ detached: true,
232
232
  stdio: 'ignore',
233
233
  windowsHide: true,
234
234
  });
package/src/plan.js CHANGED
@@ -10,16 +10,16 @@ import {
10
10
  buildSetupCommand,
11
11
  getBootstrapAction,
12
12
  } from './bootstrap.js';
13
- import { scanRepo } from './detect.js';
14
- import { buildProjectMcpConfigPlan } from './mcp-config.js';
15
- import {
16
- buildAnalyticsRouteConfigScript,
17
- buildAnalyticsRouteConfigSnippet,
18
- } from './privacy.js';
19
- import {
20
- extractBootstrapMetadata,
21
- formatSetupReportSummary,
22
- } from './setup-report.js';
13
+ import { scanRepo } from './detect.js';
14
+ import { buildCodexMcpConfigPlan, buildProjectMcpConfigPlan } from './mcp-config.js';
15
+ import {
16
+ buildAnalyticsRouteConfigScript,
17
+ buildAnalyticsRouteConfigSnippet,
18
+ } from './privacy.js';
19
+ import {
20
+ extractBootstrapMetadata,
21
+ formatSetupReportSummary,
22
+ } from './setup-report.js';
23
23
 
24
24
  export async function createSetupPlan(options) {
25
25
  const cwd = options.cwd;
@@ -38,14 +38,15 @@ export async function createSetupPlan(options) {
38
38
  remote.instructions?.recommendedBootstrapBlock ??
39
39
  buildLocalBootstrapBlock(instructionPackVersion, appId);
40
40
  const bootstrap = getBootstrapAction(repo.instructionFiles, recommendedBootstrapBlock);
41
- const analyticsContract = remote.analyticsContract;
42
- const planRepo = sanitizeRepoForPlan(repo);
43
- const mcp = await buildProjectMcpConfigPlan(cwd);
44
-
45
- return {
41
+ const analyticsContract = remote.analyticsContract;
42
+ const planRepo = sanitizeRepoForPlan(repo);
43
+ const codexMcp = await buildCodexMcpConfigPlan(cwd);
44
+ const mcp = await buildProjectMcpConfigPlan(cwd);
45
+
46
+ return {
46
47
  appId,
47
48
  cwd,
48
- mode: options.apply ? 'apply' : 'review',
49
+ mode: options.apply ? 'apply' : 'review',
49
50
  packageName: CLI_PACKAGE_NAME,
50
51
  futureCommand: buildSetupCommand(appId),
51
52
  localCommand: `npm run cli:setup -- --app ${appId}`,
@@ -53,41 +54,43 @@ export async function createSetupPlan(options) {
53
54
  repo: planRepo,
54
55
  remote,
55
56
  instructionPackVersion,
56
- bootstrap,
57
- mcp,
58
- analytics: buildAnalyticsPlan({ analyticsContract, repo: planRepo, instructionPackVersion }),
57
+ bootstrap,
58
+ codexMcp,
59
+ mcp,
60
+ analytics: buildAnalyticsPlan({ analyticsContract, repo: planRepo, instructionPackVersion }),
59
61
  fallbackInstructions: buildFallbackMcpInstructions(appId),
60
62
  warnings: buildWarnings({ repo: planRepo, remote }),
61
63
  validation: buildValidationPlan(planRepo),
62
- deploymentChecklist: [
63
- 'Review this plan and any proposed file edits.',
64
- 'Apply reviewed bootstrap, analytics, and non-secret project MCP config changes only.',
65
- 'Restart or reconnect your agent if the project-local MCP config changed.',
66
- 'Return to the DistributionOS onboarding page and verify the agent MCP connection before reviewing generated context.',
67
- 'Run baseline tests before mutation when the worktree is messy.',
64
+ deploymentChecklist: [
65
+ 'Review this plan and any proposed file edits.',
66
+ 'Apply reviewed bootstrap, analytics, and non-secret agent MCP config changes only.',
67
+ 'For Codex, start a new Codex chat in this repo after the Codex MCP config changes.',
68
+ 'For Claude Code, restart or reconnect Claude Code if the project-local MCP config changed.',
69
+ 'Return to the DistributionOS onboarding page and verify the agent MCP connection before reviewing generated context.',
70
+ 'Run baseline tests before mutation when the worktree is messy.',
68
71
  'Run build and test commands after mutation.',
69
72
  'Do not deploy until a human reviews the diff.',
70
73
  'After deployment, call verify_analytics_install on representative public URLs.',
71
- 'Call report_implementation for shipped artifacts or analytics opt-outs.',
72
- ],
73
- setupReport: null,
74
- };
75
- }
74
+ 'Call report_implementation for shipped artifacts or analytics opt-outs.',
75
+ ],
76
+ setupReport: null,
77
+ };
78
+ }
76
79
 
77
80
  function sanitizeRepoForPlan(repo) {
78
81
  return {
79
82
  ...repo,
80
83
  packageJson: sanitizePackageJson(repo.packageJson),
81
- instructionFiles: (repo.instructionFiles ?? []).map(file => ({
82
- path: file.path,
83
- priority: file.priority,
84
- exists: file.exists,
85
- hasManagedDistributionOsBlock: extractBootstrapMetadata(file.content).hasManagedDistributionOsBlock,
86
- managedDistributionOsVersion: extractBootstrapMetadata(file.content).version,
87
- managedDistributionOsAppId: extractBootstrapMetadata(file.content).appId,
88
- contentOmitted: true,
89
- })),
90
- };
84
+ instructionFiles: (repo.instructionFiles ?? []).map(file => ({
85
+ path: file.path,
86
+ priority: file.priority,
87
+ exists: file.exists,
88
+ hasManagedDistributionOsBlock: extractBootstrapMetadata(file.content).hasManagedDistributionOsBlock,
89
+ managedDistributionOsVersion: extractBootstrapMetadata(file.content).version,
90
+ managedDistributionOsAppId: extractBootstrapMetadata(file.content).appId,
91
+ contentOmitted: true,
92
+ })),
93
+ };
91
94
  }
92
95
 
93
96
  function sanitizePackageJson(packageJson) {
@@ -101,11 +104,11 @@ function sanitizePackageJson(packageJson) {
101
104
 
102
105
  export function formatPlanText(plan) {
103
106
  const lines = [
104
- 'DistributionOS CLI setup plan',
105
- `App: ${plan.appId}`,
106
- plan.mode === 'apply'
107
- ? 'Step: apply approved setup'
108
- : 'Step: review setup plan (no files changed yet)',
107
+ 'DistributionOS CLI setup plan',
108
+ `App: ${plan.appId}`,
109
+ plan.mode === 'apply'
110
+ ? 'Step: apply approved setup'
111
+ : 'Step: review setup plan (no files changed yet)',
109
112
  `Package: ${plan.packageName}`,
110
113
  `Future command: ${plan.futureCommand}`,
111
114
  `Local command: ${plan.localCommand}`,
@@ -118,19 +121,20 @@ export function formatPlanText(plan) {
118
121
  `- Layout candidates: ${formatList(plan.repo.layoutCandidates)}`,
119
122
  `- Content files found: ${plan.repo.contentFiles.length}`,
120
123
  `- Deploy hints: ${formatList(plan.repo.deployHints)}`,
121
- '',
122
- 'DistributionOS access',
123
- remoteSummary(plan),
124
- ...usageSummary(plan.remote.usage),
125
- ...plan.remote.errors.map(error => `- ${error}`),
126
- '',
124
+ '',
125
+ 'DistributionOS access',
126
+ remoteSummary(plan),
127
+ ...usageSummary(plan.remote.usage),
128
+ ...plan.remote.errors.map(error => `- ${error}`),
129
+ '',
127
130
  'Planned changes',
128
- `1. Agent bootstrap: ${plan.bootstrap.action} ${plan.bootstrap.target}`,
129
- ` ${plan.bootstrap.reason}`,
130
- `2. Agent instructions: ${plan.remote.instructions ? 'use fetched current instruction pack' : 'use fallback MCP/OAuth instructions until authenticated'}`,
131
- `3. Analytics: ${analyticsSummary(plan.analytics)}`,
132
- `4. Agent MCP config: ${mcpSummary(plan.mcp)}`,
133
- '',
131
+ `1. Agent bootstrap: ${plan.bootstrap.action} ${plan.bootstrap.target}`,
132
+ ` ${plan.bootstrap.reason}`,
133
+ `2. Agent instructions: ${plan.remote.instructions ? 'use fetched current instruction pack' : 'use fallback MCP/OAuth instructions until authenticated'}`,
134
+ `3. Analytics: ${analyticsSummary(plan.analytics)}`,
135
+ `4. Codex MCP config: ${mcpSummary(plan.codexMcp)}`,
136
+ `5. Claude MCP config: ${mcpSummary(plan.mcp)}`,
137
+ '',
134
138
  'Route privacy review',
135
139
  `- Allowed public candidates: ${formatList(plan.repo.routePrivacy.allowed, 8)}`,
136
140
  `- Blocked private candidates: ${formatList(plan.repo.routePrivacy.blocked, 8)}`,
@@ -141,71 +145,72 @@ export function formatPlanText(plan) {
141
145
  `- Test: ${plan.validation.test ?? 'not detected'}`,
142
146
  `- Lint: ${plan.validation.lint ?? 'not detected'}`,
143
147
  '',
144
- 'Fallback MCP/OAuth instructions',
145
- ...plan.fallbackInstructions.map(item => `- ${item}`),
146
- '',
147
- 'Agent live-data connection',
148
- `- Project config: ${plan.mcp.path}`,
149
- `- Planned action: ${plan.mcp.action}`,
150
- `- Auth mode: ${plan.mcp.authMode}; no API keys or OAuth tokens are written.`,
151
- `- ${plan.mcp.reason}`,
152
- '- Verification is separate from install: after apply, restart or reconnect your agent and ask it to call check_distributionos_connection.',
153
- '',
148
+ 'Fallback MCP/OAuth instructions',
149
+ ...plan.fallbackInstructions.map(item => `- ${item}`),
150
+ '',
151
+ 'Agent live-data connection',
152
+ `- Codex config: ${plan.codexMcp.path} (${plan.codexMcp.action})`,
153
+ `- Claude/project config: ${plan.mcp.path} (${plan.mcp.action})`,
154
+ `- Auth mode: ${plan.mcp.authMode}; no API keys or OAuth tokens are written.`,
155
+ `- Codex: ${plan.codexMcp.reason}`,
156
+ `- Claude/other MCP hosts: ${plan.mcp.reason}`,
157
+ '- Verification is separate from install: after apply, start a fresh agent chat or reconnect the agent and ask it to call check_distributionos_connection.',
158
+ '',
154
159
  'Deployment checklist',
155
160
  ...plan.deploymentChecklist.map(item => `- ${item}`),
156
161
  ];
157
162
 
158
- if (plan.analytics.configSnippet) {
159
- lines.push('', 'Future-compatible route gate config', plan.analytics.configSnippet);
160
- }
161
-
162
- const setupReportLines = formatSetupReportSummary(plan.setupReport);
163
- if (setupReportLines.length > 0) {
164
- lines.push('', ...setupReportLines);
165
- }
166
-
167
- if (plan.warnings.length > 0) {
168
- lines.push('', 'Warnings', ...plan.warnings.map(item => `- ${item}`));
169
- }
170
-
171
- lines.push(...nextStepLines(plan));
172
-
173
- return `${lines.join('\n')}\n`;
174
- }
175
-
176
- function nextStepLines(plan) {
177
- if (plan.mode === 'apply') return [];
178
-
179
- const applyCommand = `${plan.futureCommand} --apply`;
180
- const lines = [
181
- '',
182
- 'Next step: install approved setup',
183
- ];
184
-
185
- if (plan.repo.git.dirty) {
186
- lines.push(
187
- 'This repo has existing uncommitted changes.',
188
- 'Recommended: commit or stash those changes, then run:',
189
- ` ${applyCommand}`,
190
- '',
191
- 'If you recognize the existing changes and want DistributionOS to edit only its managed files, run:',
192
- ` ${applyCommand} --allow-dirty`,
193
- );
194
- } else {
195
- lines.push(
196
- 'If the planned changes look right, run:',
197
- ` ${applyCommand}`,
198
- );
199
- }
200
-
201
- lines.push(
202
- '',
203
- 'DistributionOS expects to edit only the managed agent instruction block, supported analytics layout, and non-secret project MCP config. Review git diff before committing or deploying.',
204
- 'After apply, return to the DistributionOS onboarding page. It will show the exact restart, auth, and verification prompts and will wait for a real MCP call before letting you continue.',
205
- );
206
-
207
- return lines;
208
- }
163
+ if (plan.analytics.configSnippet) {
164
+ lines.push('', 'Future-compatible route gate config', plan.analytics.configSnippet);
165
+ }
166
+
167
+ const setupReportLines = formatSetupReportSummary(plan.setupReport);
168
+ if (setupReportLines.length > 0) {
169
+ lines.push('', ...setupReportLines);
170
+ }
171
+
172
+ if (plan.warnings.length > 0) {
173
+ lines.push('', 'Warnings', ...plan.warnings.map(item => `- ${item}`));
174
+ }
175
+
176
+ lines.push(...nextStepLines(plan));
177
+
178
+ return `${lines.join('\n')}\n`;
179
+ }
180
+
181
+ function nextStepLines(plan) {
182
+ if (plan.mode === 'apply') return [];
183
+
184
+ const applyCommand = `${plan.futureCommand} --apply`;
185
+ const lines = [
186
+ '',
187
+ 'Next step: install approved setup',
188
+ ];
189
+
190
+ if (plan.repo.git.dirty) {
191
+ lines.push(
192
+ 'This repo has existing uncommitted changes.',
193
+ 'Recommended: commit or stash those changes, then run:',
194
+ ` ${applyCommand}`,
195
+ '',
196
+ 'If you recognize the existing changes and want DistributionOS to edit only its managed files, run:',
197
+ ` ${applyCommand} --allow-dirty`,
198
+ );
199
+ } else {
200
+ lines.push(
201
+ 'If the planned changes look right, run:',
202
+ ` ${applyCommand}`,
203
+ );
204
+ }
205
+
206
+ lines.push(
207
+ '',
208
+ 'DistributionOS expects to edit only the managed agent instruction block, supported analytics layout, and non-secret agent MCP config. Review git diff before committing or deploying.',
209
+ 'After apply, return to the DistributionOS onboarding page. It will show the exact new-chat/reconnect, auth, and verification prompts and will wait for a real MCP call before letting you continue.',
210
+ );
211
+
212
+ return lines;
213
+ }
209
214
 
210
215
  function buildAnalyticsPlan({ analyticsContract, repo, instructionPackVersion }) {
211
216
  const layoutTarget = selectAnalyticsLayoutTarget(repo);
@@ -251,6 +256,7 @@ function buildAnalyticsPlan({ analyticsContract, repo, instructionPackVersion })
251
256
  'Preserve clean canonical URLs.',
252
257
  'Backfill public blog and landing pages with stable dosContentId markers.',
253
258
  'Add data-dos-event and data-dos-link-id only to primary CTAs or campaign links.',
259
+ 'Wire checkout_started before public checkout redirects and success-only signup/waitlist/email events where safe.',
254
260
  ],
255
261
  };
256
262
  }
@@ -271,14 +277,14 @@ function selectAnalyticsLayoutTarget(repo) {
271
277
  return candidates[0] ?? null;
272
278
  }
273
279
 
274
- function buildWarnings({ repo, remote }) {
275
- const warnings = [];
276
- if (remote.access?.reason === 'subscription_required') {
277
- warnings.push('Active Builder or Distributor subscription required before applying setup.');
278
- }
279
- if (repo.git.dirty) {
280
- warnings.push('Worktree has existing changes. Keep baseline failures separate before applying edits.');
281
- }
280
+ function buildWarnings({ repo, remote }) {
281
+ const warnings = [];
282
+ if (remote.access?.reason === 'subscription_required') {
283
+ warnings.push('Active Builder or Distributor subscription required before applying setup.');
284
+ }
285
+ if (repo.git.dirty) {
286
+ warnings.push('Worktree has existing changes. Keep baseline failures separate before applying edits.');
287
+ }
282
288
  if (!remote.instructions) {
283
289
  warnings.push('Live DistributionOS instructions were not fetched. Connect MCP OAuth or provide a safe env token before applying app-specific setup.');
284
290
  }
@@ -302,54 +308,54 @@ function buildValidationPlan(repo) {
302
308
  };
303
309
  }
304
310
 
305
- function remoteSummary(plan) {
306
- if (plan.remote.status === 'fetched') {
307
- return `- Fetched live instructions and analytics contract using ${plan.remote.tokenName}.`;
308
- }
309
- if (plan.remote.status === 'partial') {
310
- return `- Used ${plan.remote.tokenName}, but one or more DistributionOS requests failed.`;
311
- }
312
- if (plan.remote.status === 'skipped') {
313
- return '- Remote fetch skipped. The setup plan used local fallback instructions and did not call DistributionOS APIs.';
314
- }
315
- return '- No OAuth or env token found. The setup plan used local fallback instructions and did not call DistributionOS APIs.';
316
- }
317
-
318
- function usageSummary(usage) {
319
- if (!usage) return [];
320
- const plan = usage.plan
321
- ? `- Plan: ${usage.plan.name}${usage.plan.priceLabel ? ` (${usage.plan.priceLabel})` : ''}`
322
- : '- Plan: active account access';
323
- const monthly = usage.monthly ?? {};
324
- return [
325
- plan,
326
- `- Monthly caps: briefs ${meterText(monthly.brief_handoffs)}, images ${meterText(monthly.image_generations)}, research ${meterText(monthly.fresh_research_runs)}`,
327
- ];
328
- }
329
-
330
- function meterText(value) {
331
- if (!value) return 'unknown';
332
- const limit = value.limit < 0 ? 'unlimited' : value.limit;
333
- return `${value.used ?? 0}/${limit}`;
334
- }
311
+ function remoteSummary(plan) {
312
+ if (plan.remote.status === 'fetched') {
313
+ return `- Fetched live instructions and analytics contract using ${plan.remote.tokenName}.`;
314
+ }
315
+ if (plan.remote.status === 'partial') {
316
+ return `- Used ${plan.remote.tokenName}, but one or more DistributionOS requests failed.`;
317
+ }
318
+ if (plan.remote.status === 'skipped') {
319
+ return '- Remote fetch skipped. The setup plan used local fallback instructions and did not call DistributionOS APIs.';
320
+ }
321
+ return '- No OAuth or env token found. The setup plan used local fallback instructions and did not call DistributionOS APIs.';
322
+ }
323
+
324
+ function usageSummary(usage) {
325
+ if (!usage) return [];
326
+ const plan = usage.plan
327
+ ? `- Plan: ${usage.plan.name}${usage.plan.priceLabel ? ` (${usage.plan.priceLabel})` : ''}`
328
+ : '- Plan: active account access';
329
+ const monthly = usage.monthly ?? {};
330
+ return [
331
+ plan,
332
+ `- Monthly caps: briefs ${meterText(monthly.brief_handoffs)}, images ${meterText(monthly.image_generations)}, research ${meterText(monthly.fresh_research_runs)}`,
333
+ ];
334
+ }
335
335
 
336
- function analyticsSummary(analytics) {
336
+ function meterText(value) {
337
+ if (!value) return 'unknown';
338
+ const limit = value.limit < 0 ? 'unlimited' : value.limit;
339
+ return `${value.used ?? 0}/${limit}`;
340
+ }
341
+
342
+ function analyticsSummary(analytics) {
337
343
  if (analytics.status === 'enabled') {
338
344
  return `contract ${analytics.contractVersion}; install script in ${analytics.layoutTarget ?? 'reviewed shared layout'}`;
339
345
  }
340
- if (analytics.status === 'disabled') {
341
- return 'tracker will be created or fetched during apply before analytics is installed';
342
- }
346
+ if (analytics.status === 'disabled') {
347
+ return 'tracker will be created or fetched during apply before analytics is installed';
348
+ }
343
349
  return 'contract not fetched; print manual guidance only';
344
- }
345
-
346
- function mcpSummary(mcp) {
347
- if (!mcp) return 'manual instructions only';
348
- if (mcp.action === 'none') return `${mcp.path} already includes DistributionOS`;
349
- if (mcp.action === 'create') return `create ${mcp.path} with DistributionOS MCP server URL`;
350
- if (mcp.action === 'update') return `update ${mcp.path} with DistributionOS MCP server URL`;
351
- return `manual setup needed for ${mcp.path}`;
352
- }
350
+ }
351
+
352
+ function mcpSummary(mcp) {
353
+ if (!mcp) return 'manual instructions only';
354
+ if (mcp.action === 'none') return `${mcp.path} already includes DistributionOS`;
355
+ if (mcp.action === 'create') return `create ${mcp.path} with DistributionOS MCP server URL`;
356
+ if (mcp.action === 'update') return `update ${mcp.path} with DistributionOS MCP server URL`;
357
+ return `manual setup needed for ${mcp.path}`;
358
+ }
353
359
 
354
360
  function formatList(values, limit = 5) {
355
361
  if (!values || values.length === 0) return 'none';