@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/README.md +7 -7
- package/package.json +1 -1
- package/src/api.js +125 -125
- package/src/bootstrap.js +15 -6
- package/src/cli.js +216 -213
- package/src/constants.js +8 -8
- package/src/initialization.js +63 -63
- package/src/mcp-config.js +319 -187
- package/src/mutate.js +46 -43
- package/src/oauth.js +30 -30
- package/src/plan.js +175 -169
- package/src/setup-report.js +247 -216
- package/src/validation.js +108 -0
package/src/mutate.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { applyProjectMcpConfig } from './mcp-config.js';
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { applyCodexMcpConfig, applyProjectMcpConfig } from './mcp-config.js';
|
|
4
4
|
|
|
5
5
|
const BOOTSTRAP_BLOCK_RE =
|
|
6
6
|
/<!--\s*DISTRIBUTIONOS:START[\s\S]*?DISTRIBUTIONOS:END\s*-->/i;
|
|
@@ -18,16 +18,19 @@ export async function applySetupPlan(plan, options = {}) {
|
|
|
18
18
|
const bootstrap = await applyBootstrap(plan.cwd, plan.bootstrap);
|
|
19
19
|
if (bootstrap.changed) changes.push(bootstrap);
|
|
20
20
|
|
|
21
|
-
if (!options.skipAnalytics) {
|
|
22
|
-
const analytics = await applyAnalytics(plan.cwd, plan.analytics);
|
|
23
|
-
if (analytics.changed || analytics.reason) changes.push(analytics);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
if (!options.skipAnalytics) {
|
|
22
|
+
const analytics = await applyAnalytics(plan.cwd, plan.analytics);
|
|
23
|
+
if (analytics.changed || analytics.reason) changes.push(analytics);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const codexMcp = await applyCodexMcpConfig(plan.cwd, plan.codexMcp);
|
|
27
|
+
if (codexMcp.changed || codexMcp.reason) changes.push(codexMcp);
|
|
28
|
+
|
|
29
|
+
const mcp = await applyProjectMcpConfig(plan.cwd, plan.mcp);
|
|
30
|
+
if (mcp.changed || mcp.reason) changes.push(mcp);
|
|
31
|
+
|
|
32
|
+
return changes;
|
|
33
|
+
}
|
|
31
34
|
|
|
32
35
|
async function applyBootstrap(cwd, bootstrap) {
|
|
33
36
|
const targetPath = safeResolve(cwd, bootstrap.target);
|
|
@@ -95,9 +98,9 @@ async function applyNextAppRouterAnalytics(cwd, analytics, scriptSrc) {
|
|
|
95
98
|
contractVersion: analytics.contractVersion,
|
|
96
99
|
configScript: analytics.configScript,
|
|
97
100
|
});
|
|
98
|
-
const next = ANALYTICS_BLOCK_RE.test(existing)
|
|
99
|
-
? existing.replace(ANALYTICS_BLOCK_RE, `\n${block}\n`)
|
|
100
|
-
: insertIntoNextAppRouterLayout(existing, block);
|
|
101
|
+
const next = ANALYTICS_BLOCK_RE.test(existing)
|
|
102
|
+
? existing.replace(ANALYTICS_BLOCK_RE, `\n${block}\n`)
|
|
103
|
+
: insertIntoNextAppRouterLayout(existing, block);
|
|
101
104
|
|
|
102
105
|
if (next === existing) {
|
|
103
106
|
return { type: 'analytics', changed: false, file: analytics.layoutTarget, reason: 'No analytics change needed.' };
|
|
@@ -160,33 +163,33 @@ function buildHtmlAnalyticsBlock({ scriptSrc, contractVersion, configScript }) {
|
|
|
160
163
|
].join('\n');
|
|
161
164
|
}
|
|
162
165
|
|
|
163
|
-
function insertBeforeHeadClose(content, block) {
|
|
164
|
-
const headClose = content.match(/^(\s*)<\/head>/im);
|
|
165
|
-
if (headClose?.index === undefined) {
|
|
166
|
-
throw new Error('Could not find </head> in shared layout.');
|
|
167
|
-
}
|
|
168
|
-
return `${content.slice(0, headClose.index)}${block}\n${content.slice(headClose.index)}`;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function insertIntoNextAppRouterLayout(content, block) {
|
|
172
|
-
const headClose = content.match(/^(\s*)<\/head>/im);
|
|
173
|
-
if (headClose?.index !== undefined) {
|
|
174
|
-
return `${content.slice(0, headClose.index)}${block}\n${content.slice(headClose.index)}`;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const bodyOpen = content.match(/^(\s*)<body(\s|>)/im);
|
|
178
|
-
if (bodyOpen?.index === undefined) {
|
|
179
|
-
throw new Error('Could not find <body> in shared layout.');
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const indent = bodyOpen[1] || ' ';
|
|
183
|
-
const headBlock = [
|
|
184
|
-
`${indent}<head>`,
|
|
185
|
-
block,
|
|
186
|
-
`${indent}</head>`,
|
|
187
|
-
].join('\n');
|
|
188
|
-
return `${content.slice(0, bodyOpen.index)}${headBlock}\n${content.slice(bodyOpen.index)}`;
|
|
189
|
-
}
|
|
166
|
+
function insertBeforeHeadClose(content, block) {
|
|
167
|
+
const headClose = content.match(/^(\s*)<\/head>/im);
|
|
168
|
+
if (headClose?.index === undefined) {
|
|
169
|
+
throw new Error('Could not find </head> in shared layout.');
|
|
170
|
+
}
|
|
171
|
+
return `${content.slice(0, headClose.index)}${block}\n${content.slice(headClose.index)}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function insertIntoNextAppRouterLayout(content, block) {
|
|
175
|
+
const headClose = content.match(/^(\s*)<\/head>/im);
|
|
176
|
+
if (headClose?.index !== undefined) {
|
|
177
|
+
return `${content.slice(0, headClose.index)}${block}\n${content.slice(headClose.index)}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const bodyOpen = content.match(/^(\s*)<body(\s|>)/im);
|
|
181
|
+
if (bodyOpen?.index === undefined) {
|
|
182
|
+
throw new Error('Could not find <body> in shared layout.');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const indent = bodyOpen[1] || ' ';
|
|
186
|
+
const headBlock = [
|
|
187
|
+
`${indent}<head>`,
|
|
188
|
+
block,
|
|
189
|
+
`${indent}</head>`,
|
|
190
|
+
].join('\n');
|
|
191
|
+
return `${content.slice(0, bodyOpen.index)}${headBlock}\n${content.slice(bodyOpen.index)}`;
|
|
192
|
+
}
|
|
190
193
|
|
|
191
194
|
function indentLines(value, prefix) {
|
|
192
195
|
return String(value).split(/\r?\n/).map(line => `${prefix}${line}`).join('\n');
|
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
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
|
65
|
-
'
|
|
66
|
-
'
|
|
67
|
-
'
|
|
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.
|
|
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
|
-
`-
|
|
149
|
-
`-
|
|
150
|
-
`- Auth mode: ${plan.mcp.authMode}; no API keys or OAuth tokens are written.`,
|
|
151
|
-
`- ${plan.
|
|
152
|
-
|
|
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
|
|
204
|
-
'After apply, return to the DistributionOS onboarding page. It will wait for a real MCP
|
|
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
|
|
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';
|