@distributionos/cli 0.1.11 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api.js +38 -2
- package/src/bootstrap.js +4 -3
- package/src/cli.js +144 -64
- package/src/constants.js +1 -0
- package/src/mcp-config.js +24 -1
- package/src/plan.js +34 -19
- package/src/setup-report.js +222 -0
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -42,7 +42,7 @@ export async function resolveAuthToken({ env, apiBase, appId, fetchImpl }) {
|
|
|
42
42
|
: null;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export async function fetchDistributionOsSetupContext(input) {
|
|
45
|
+
export async function fetchDistributionOsSetupContext(input) {
|
|
46
46
|
const token = await resolveAuthToken({
|
|
47
47
|
env: input.env,
|
|
48
48
|
apiBase: input.apiBase ?? DEFAULT_API_BASE,
|
|
@@ -61,6 +61,7 @@ export async function fetchDistributionOsSetupContext(input) {
|
|
|
61
61
|
usage: null,
|
|
62
62
|
instructions: null,
|
|
63
63
|
analyticsContract: null,
|
|
64
|
+
setupContract: null,
|
|
64
65
|
errors: [],
|
|
65
66
|
};
|
|
66
67
|
}
|
|
@@ -76,7 +77,7 @@ export async function fetchDistributionOsSetupContext(input) {
|
|
|
76
77
|
return null;
|
|
77
78
|
};
|
|
78
79
|
|
|
79
|
-
const [instructions, analyticsContract, usage] = await Promise.all([
|
|
80
|
+
const [instructions, analyticsContract, usage, setupContract] = await Promise.all([
|
|
80
81
|
fetchJson({
|
|
81
82
|
fetchImpl: input.fetch,
|
|
82
83
|
apiBase,
|
|
@@ -95,6 +96,12 @@ export async function fetchDistributionOsSetupContext(input) {
|
|
|
95
96
|
token: token.value,
|
|
96
97
|
path: `/api/v1/apps/${encodeURIComponent(input.appId)}/usage`,
|
|
97
98
|
}).catch(capture('Usage fetch')),
|
|
99
|
+
fetchJson({
|
|
100
|
+
fetchImpl: input.fetch,
|
|
101
|
+
apiBase,
|
|
102
|
+
token: token.value,
|
|
103
|
+
path: `/api/v1/apps/${encodeURIComponent(input.appId)}/setup/contract`,
|
|
104
|
+
}).catch(capture('Setup contract fetch')),
|
|
98
105
|
]);
|
|
99
106
|
|
|
100
107
|
return {
|
|
@@ -108,9 +115,38 @@ export async function fetchDistributionOsSetupContext(input) {
|
|
|
108
115
|
usage,
|
|
109
116
|
instructions,
|
|
110
117
|
analyticsContract,
|
|
118
|
+
setupContract,
|
|
111
119
|
errors,
|
|
112
120
|
};
|
|
113
121
|
}
|
|
122
|
+
|
|
123
|
+
export async function reportSetupInstallation(input) {
|
|
124
|
+
const token = await resolveAuthToken({
|
|
125
|
+
env: input.env,
|
|
126
|
+
apiBase: input.apiBase ?? DEFAULT_API_BASE,
|
|
127
|
+
appId: input.appId,
|
|
128
|
+
fetchImpl: input.fetch,
|
|
129
|
+
});
|
|
130
|
+
if (!token) {
|
|
131
|
+
throw new Error('No stored OAuth token or env token was available.');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const apiBase = normalizeApiBase(input.apiBase ?? DEFAULT_API_BASE);
|
|
135
|
+
const response = await input.fetch(`${apiBase}/api/v1/apps/${encodeURIComponent(input.appId)}/setup/report`, {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: {
|
|
138
|
+
Authorization: `Bearer ${token.value}`,
|
|
139
|
+
'Content-Type': 'application/json',
|
|
140
|
+
Accept: 'application/json',
|
|
141
|
+
},
|
|
142
|
+
body: JSON.stringify(input.report),
|
|
143
|
+
});
|
|
144
|
+
const body = await response.json().catch(() => null);
|
|
145
|
+
if (!response.ok || body?.success === false) {
|
|
146
|
+
throw new Error(body?.error || `Setup report failed with HTTP ${response.status}`);
|
|
147
|
+
}
|
|
148
|
+
return body?.data ?? body;
|
|
149
|
+
}
|
|
114
150
|
|
|
115
151
|
export async function ensureAnalyticsSite(input) {
|
|
116
152
|
const token = await resolveAuthToken({
|
package/src/bootstrap.js
CHANGED
|
@@ -7,9 +7,10 @@ import {
|
|
|
7
7
|
const BOOTSTRAP_BLOCK_RE =
|
|
8
8
|
/<!--\s*DISTRIBUTIONOS:START[\s\S]*?DISTRIBUTIONOS:END\s*-->/i;
|
|
9
9
|
|
|
10
|
-
export function buildSetupCommand(appId, packageName = CLI_PACKAGE_NAME) {
|
|
11
|
-
|
|
12
|
-
}
|
|
10
|
+
export function buildSetupCommand(appId, packageName = CLI_PACKAGE_NAME) {
|
|
11
|
+
const packageSpec = packageName === CLI_PACKAGE_NAME ? `${packageName}@latest` : packageName;
|
|
12
|
+
return `npx --yes ${packageSpec} setup --app ${shellToken(appId)}`;
|
|
13
|
+
}
|
|
13
14
|
|
|
14
15
|
export function buildLocalBootstrapBlock(version = CURRENT_BOOTSTRAP_VERSION, appId) {
|
|
15
16
|
const appIdAttribute = appId ? ` appId=${appId}` : '';
|
package/src/cli.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import { CLI_LOCAL_VERSION, DEFAULT_API_BASE } from './constants.js';
|
|
2
|
-
import {
|
|
3
|
-
ensureAnalyticsSite,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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';
|
|
8
9
|
import { loginWithOAuth } from './oauth.js';
|
|
9
10
|
import { createSetupPlan, formatPlanText } from './plan.js';
|
|
10
11
|
import { applySetupPlan } from './mutate.js';
|
|
11
|
-
import { submitInitialization } from './initialization.js';
|
|
12
|
-
import { compareValidationResults, inspectInstallArtifacts, runValidationPlan } from './validation.js';
|
|
12
|
+
import { submitInitialization } from './initialization.js';
|
|
13
|
+
import { compareValidationResults, inspectInstallArtifacts, runValidationPlan } from './validation.js';
|
|
14
|
+
import { buildSetupReport } from './setup-report.js';
|
|
13
15
|
|
|
14
16
|
export async function runCli(argv, runtime) {
|
|
15
17
|
const stdout = runtime.stdout;
|
|
@@ -139,15 +141,21 @@ export async function runCli(argv, runtime) {
|
|
|
139
141
|
if (!authReady) return 1;
|
|
140
142
|
}
|
|
141
143
|
|
|
142
|
-
let plan = await createSetupPlan({
|
|
143
|
-
appId: args.appId,
|
|
144
|
-
cwd: args.cwd ?? runtime.cwd,
|
|
145
|
-
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,
|
|
146
148
|
env: runtime.env,
|
|
147
149
|
fetch: runtime.fetch,
|
|
148
|
-
noFetch: args.noFetch,
|
|
149
|
-
apply: args.apply,
|
|
150
|
-
});
|
|
150
|
+
noFetch: args.noFetch,
|
|
151
|
+
apply: args.apply,
|
|
152
|
+
});
|
|
153
|
+
plan.setupReport = await submitSetupReportSafely({
|
|
154
|
+
args,
|
|
155
|
+
runtime,
|
|
156
|
+
plan,
|
|
157
|
+
phase: 'plan',
|
|
158
|
+
});
|
|
151
159
|
|
|
152
160
|
if (!args.apply && !args.yes) {
|
|
153
161
|
stdout.write(args.json ? `${JSON.stringify(plan, null, 2)}\n` : formatPlanText(plan));
|
|
@@ -185,15 +193,21 @@ export async function runCli(argv, runtime) {
|
|
|
185
193
|
}
|
|
186
194
|
}
|
|
187
195
|
|
|
188
|
-
plan = await createSetupPlan({
|
|
189
|
-
appId: args.appId,
|
|
190
|
-
cwd: args.cwd ?? runtime.cwd,
|
|
191
|
-
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,
|
|
192
200
|
env: runtime.env,
|
|
193
201
|
fetch: runtime.fetch,
|
|
194
|
-
noFetch: args.noFetch,
|
|
195
|
-
apply: args.apply,
|
|
196
|
-
});
|
|
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
|
+
});
|
|
197
211
|
|
|
198
212
|
if (!args.apply) {
|
|
199
213
|
stdout.write(args.json ? `${JSON.stringify(plan, null, 2)}\n` : formatPlanText(plan));
|
|
@@ -205,36 +219,99 @@ export async function runCli(argv, runtime) {
|
|
|
205
219
|
const changes = await applySetupPlan(plan, {
|
|
206
220
|
allowDirty: args.allowDirty,
|
|
207
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,
|
|
242
|
+
validation,
|
|
243
|
+
inspection,
|
|
244
|
+
requiresAgentRestart: changes.some((change) => change.type === 'mcp' && change.changed),
|
|
245
|
+
},
|
|
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,
|
|
208
252
|
});
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
apiBase: args.apiBase ?? DEFAULT_API_BASE,
|
|
223
|
-
fetchImpl: runtime.fetch,
|
|
224
|
-
domain: args.domain,
|
|
225
|
-
})
|
|
226
|
-
: null;
|
|
227
|
-
|
|
228
|
-
stdout.write(args.json
|
|
229
|
-
? `${JSON.stringify({ plan, changes, initialization, validation, inspection }, null, 2)}\n`
|
|
230
|
-
: formatApplyResult(plan, 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));
|
|
231
266
|
return hasBlockingApplyFailure(validation, inspection) ? 1 : 0;
|
|
232
|
-
} catch (error) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
+
|
|
238
315
|
function hasBlockingApplyFailure(validation, inspection) {
|
|
239
316
|
return validation.some((result) => result.introducedFailure) ||
|
|
240
317
|
inspection.some((result) => result.status === 'failed');
|
|
@@ -266,10 +343,11 @@ export function parseArgs(rawArgs) {
|
|
|
266
343
|
apply: false,
|
|
267
344
|
allowDirty: false,
|
|
268
345
|
skipAnalytics: false,
|
|
269
|
-
skipValidate: false,
|
|
270
|
-
noOpen: false,
|
|
271
|
-
noLogin: false,
|
|
272
|
-
|
|
346
|
+
skipValidate: false,
|
|
347
|
+
noOpen: false,
|
|
348
|
+
noLogin: false,
|
|
349
|
+
skipSetupReport: false,
|
|
350
|
+
yes: false,
|
|
273
351
|
url: null,
|
|
274
352
|
contentId: null,
|
|
275
353
|
contractVersion: null,
|
|
@@ -301,10 +379,11 @@ export function parseArgs(rawArgs) {
|
|
|
301
379
|
else if (arg === '--apply') parsed.apply = true;
|
|
302
380
|
else if (arg === '--allow-dirty') parsed.allowDirty = true;
|
|
303
381
|
else if (arg === '--skip-analytics') parsed.skipAnalytics = true;
|
|
304
|
-
else if (arg === '--skip-validate') parsed.skipValidate = true;
|
|
305
|
-
else if (arg === '--no-open') parsed.noOpen = true;
|
|
306
|
-
else if (arg === '--no-login') parsed.noLogin = true;
|
|
307
|
-
else if (arg === '--
|
|
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;
|
|
308
387
|
else if (arg === '--app') parsed.appId = args[++index] ?? null;
|
|
309
388
|
else if (arg.startsWith('--app=')) parsed.appId = arg.slice('--app='.length);
|
|
310
389
|
else if (arg === '--cwd') parsed.cwd = args[++index] ?? null;
|
|
@@ -337,9 +416,9 @@ function helpText() {
|
|
|
337
416
|
return [
|
|
338
417
|
'DistributionOS CLI',
|
|
339
418
|
'',
|
|
340
|
-
'Usage:',
|
|
341
|
-
' distributionos setup --app <appId> [--api-base <url>] [--cwd <path>] [--json] [--no-fetch]',
|
|
342
|
-
' distributionos setup --app <appId> --apply [--domain <domain>] [--allow-dirty]',
|
|
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]',
|
|
343
422
|
' distributionos login --app <appId> [--api-base <url>]',
|
|
344
423
|
' distributionos verify --app <appId> --url <liveUrl> [--content-id <id>]',
|
|
345
424
|
' distributionos report-implementation --app <appId> --artifact <artifactId> [--url <liveUrl>] [--summary <text>]',
|
|
@@ -507,9 +586,10 @@ function formatApplyResult(plan, changes, initialization, validation, inspection
|
|
|
507
586
|
lines.push(
|
|
508
587
|
'- Open this DistributionOS review page now:',
|
|
509
588
|
` ${reviewUrl}`,
|
|
510
|
-
'- Review the generated Brain Doc and Brain Vault context before asking your agent to keep working.',
|
|
511
589
|
'- If .mcp.json was created or updated, restart or reconnect your agent session so it can load the DistributionOS MCP server.',
|
|
512
|
-
'-
|
|
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."',
|
|
513
593
|
'- After review, come back to this repo and inspect git diff before committing, pushing, or deploying.',
|
|
514
594
|
);
|
|
515
595
|
} else {
|
package/src/constants.js
CHANGED
|
@@ -8,6 +8,7 @@ export const DEFAULT_API_BASE = 'https://distributionos.dev';
|
|
|
8
8
|
export const MCP_SERVER_URL = 'https://distributionos.dev/api/mcp';
|
|
9
9
|
export const CURRENT_BOOTSTRAP_VERSION = '2026.06.14';
|
|
10
10
|
export const CURRENT_ANALYTICS_CONTRACT_VERSION = '2026.06.14';
|
|
11
|
+
export const CURRENT_SETUP_CONTRACT_VERSION = '2026.06.18';
|
|
11
12
|
|
|
12
13
|
export const TOKEN_ENV_NAMES = [
|
|
13
14
|
'DISTRIBUTIONOS_API_KEY',
|
package/src/mcp-config.js
CHANGED
|
@@ -62,8 +62,11 @@ export async function buildProjectMcpConfigPlan(cwd, serverUrl = MCP_SERVER_URL)
|
|
|
62
62
|
const existingUrl = isRecord(existing) && typeof existing.url === 'string'
|
|
63
63
|
? existing.url
|
|
64
64
|
: null;
|
|
65
|
+
const existingType = isRecord(existing) && typeof existing.type === 'string'
|
|
66
|
+
? existing.type
|
|
67
|
+
: null;
|
|
65
68
|
|
|
66
|
-
if (existingUrl === serverUrl) {
|
|
69
|
+
if (existingUrl === serverUrl && existingType === 'http') {
|
|
67
70
|
return {
|
|
68
71
|
...base,
|
|
69
72
|
supported: true,
|
|
@@ -72,6 +75,24 @@ export async function buildProjectMcpConfigPlan(cwd, serverUrl = MCP_SERVER_URL)
|
|
|
72
75
|
};
|
|
73
76
|
}
|
|
74
77
|
|
|
78
|
+
if (existingUrl === serverUrl && !existingType) {
|
|
79
|
+
return {
|
|
80
|
+
...base,
|
|
81
|
+
supported: true,
|
|
82
|
+
action: 'update',
|
|
83
|
+
reason: 'Add the Claude-compatible HTTP transport type to the DistributionOS MCP server entry.',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (existingUrl === serverUrl && existingType !== 'http') {
|
|
88
|
+
return {
|
|
89
|
+
...base,
|
|
90
|
+
supported: false,
|
|
91
|
+
action: 'manual',
|
|
92
|
+
reason: 'Existing distributionos MCP server uses a non-HTTP transport. The CLI will not overwrite it automatically.',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
75
96
|
if (existingUrl) {
|
|
76
97
|
return {
|
|
77
98
|
...base,
|
|
@@ -112,6 +133,7 @@ export async function applyProjectMcpConfig(cwd, plan) {
|
|
|
112
133
|
mcpServers: {
|
|
113
134
|
...(isRecord(existing.mcpServers) ? existing.mcpServers : {}),
|
|
114
135
|
[DISTRIBUTIONOS_MCP_SERVER_NAME]: {
|
|
136
|
+
type: 'http',
|
|
115
137
|
url: plan.serverUrl ?? MCP_SERVER_URL,
|
|
116
138
|
},
|
|
117
139
|
},
|
|
@@ -131,6 +153,7 @@ function recommendedMcpConfig(serverUrl) {
|
|
|
131
153
|
return {
|
|
132
154
|
mcpServers: {
|
|
133
155
|
[DISTRIBUTIONOS_MCP_SERVER_NAME]: {
|
|
156
|
+
type: 'http',
|
|
134
157
|
url: serverUrl,
|
|
135
158
|
},
|
|
136
159
|
},
|
package/src/plan.js
CHANGED
|
@@ -15,7 +15,11 @@ import { buildProjectMcpConfigPlan } from './mcp-config.js';
|
|
|
15
15
|
import {
|
|
16
16
|
buildAnalyticsRouteConfigScript,
|
|
17
17
|
buildAnalyticsRouteConfigSnippet,
|
|
18
|
-
} from './privacy.js';
|
|
18
|
+
} from './privacy.js';
|
|
19
|
+
import {
|
|
20
|
+
extractBootstrapMetadata,
|
|
21
|
+
formatSetupReportSummary,
|
|
22
|
+
} from './setup-report.js';
|
|
19
23
|
|
|
20
24
|
export async function createSetupPlan(options) {
|
|
21
25
|
const cwd = options.cwd;
|
|
@@ -55,31 +59,35 @@ export async function createSetupPlan(options) {
|
|
|
55
59
|
fallbackInstructions: buildFallbackMcpInstructions(appId),
|
|
56
60
|
warnings: buildWarnings({ repo: planRepo, remote }),
|
|
57
61
|
validation: buildValidationPlan(planRepo),
|
|
58
|
-
deploymentChecklist: [
|
|
59
|
-
'Review this plan and any proposed file edits.',
|
|
62
|
+
deploymentChecklist: [
|
|
63
|
+
'Review this plan and any proposed file edits.',
|
|
60
64
|
'Apply reviewed bootstrap, analytics, and non-secret project MCP config changes only.',
|
|
61
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.',
|
|
62
67
|
'Run baseline tests before mutation when the worktree is messy.',
|
|
63
68
|
'Run build and test commands after mutation.',
|
|
64
69
|
'Do not deploy until a human reviews the diff.',
|
|
65
70
|
'After deployment, call verify_analytics_install on representative public URLs.',
|
|
66
|
-
'Call report_implementation for shipped artifacts or analytics opt-outs.',
|
|
67
|
-
],
|
|
68
|
-
|
|
69
|
-
}
|
|
71
|
+
'Call report_implementation for shipped artifacts or analytics opt-outs.',
|
|
72
|
+
],
|
|
73
|
+
setupReport: null,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
70
76
|
|
|
71
77
|
function sanitizeRepoForPlan(repo) {
|
|
72
78
|
return {
|
|
73
79
|
...repo,
|
|
74
80
|
packageJson: sanitizePackageJson(repo.packageJson),
|
|
75
|
-
instructionFiles: (repo.instructionFiles ?? []).map(file => ({
|
|
76
|
-
path: file.path,
|
|
77
|
-
priority: file.priority,
|
|
78
|
-
exists: file.exists,
|
|
79
|
-
hasManagedDistributionOsBlock:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
+
};
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
function sanitizePackageJson(packageJson) {
|
|
@@ -141,15 +149,21 @@ export function formatPlanText(plan) {
|
|
|
141
149
|
`- Planned action: ${plan.mcp.action}`,
|
|
142
150
|
`- Auth mode: ${plan.mcp.authMode}; no API keys or OAuth tokens are written.`,
|
|
143
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.',
|
|
144
153
|
'',
|
|
145
154
|
'Deployment checklist',
|
|
146
155
|
...plan.deploymentChecklist.map(item => `- ${item}`),
|
|
147
156
|
];
|
|
148
157
|
|
|
149
|
-
if (plan.analytics.configSnippet) {
|
|
150
|
-
lines.push('', 'Future-compatible route gate config', plan.analytics.configSnippet);
|
|
151
|
-
}
|
|
152
|
-
|
|
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
|
+
|
|
153
167
|
if (plan.warnings.length > 0) {
|
|
154
168
|
lines.push('', 'Warnings', ...plan.warnings.map(item => `- ${item}`));
|
|
155
169
|
}
|
|
@@ -187,6 +201,7 @@ function nextStepLines(plan) {
|
|
|
187
201
|
lines.push(
|
|
188
202
|
'',
|
|
189
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 wait for a real MCP/API call before letting you continue.',
|
|
190
205
|
);
|
|
191
206
|
|
|
192
207
|
return lines;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
CLI_LOCAL_VERSION,
|
|
5
|
+
CURRENT_ANALYTICS_CONTRACT_VERSION,
|
|
6
|
+
CURRENT_BOOTSTRAP_VERSION,
|
|
7
|
+
CURRENT_SETUP_CONTRACT_VERSION,
|
|
8
|
+
MCP_SERVER_URL,
|
|
9
|
+
} from './constants.js';
|
|
10
|
+
|
|
11
|
+
const BOOTSTRAP_BLOCK_RE =
|
|
12
|
+
/<!--\s*DISTRIBUTIONOS:START([^>]*)-->[\s\S]*?<!--\s*DISTRIBUTIONOS:END\s*-->/i;
|
|
13
|
+
const ANALYTICS_BLOCK_RE =
|
|
14
|
+
/DISTRIBUTIONOS:START analytics[\s\S]*?DISTRIBUTIONOS:END analytics/i;
|
|
15
|
+
|
|
16
|
+
export async function buildSetupReport(plan, phase = 'plan', details = {}) {
|
|
17
|
+
const detectedCapabilities = new Set([
|
|
18
|
+
'analytics.verificationReady',
|
|
19
|
+
'setup.reporting',
|
|
20
|
+
]);
|
|
21
|
+
const managedFiles = [];
|
|
22
|
+
let blockedReason = null;
|
|
23
|
+
|
|
24
|
+
const bootstrapVersion = detectBootstrapVersion(plan);
|
|
25
|
+
if (bootstrapVersion && plan.bootstrap.action === 'none') {
|
|
26
|
+
detectedCapabilities.add('agent.bootstrap');
|
|
27
|
+
detectedCapabilities.add('agent.liveInstructions');
|
|
28
|
+
}
|
|
29
|
+
managedFiles.push({
|
|
30
|
+
path: plan.bootstrap.target,
|
|
31
|
+
type: 'agent.bootstrap',
|
|
32
|
+
action: plan.bootstrap.action,
|
|
33
|
+
status: plan.bootstrap.action === 'none' ? 'current' : 'planned',
|
|
34
|
+
reason: plan.bootstrap.reason,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (plan.mcp?.action === 'none') {
|
|
38
|
+
detectedCapabilities.add('agent.mcp.remoteConfig');
|
|
39
|
+
} else if (plan.mcp?.action === 'manual') {
|
|
40
|
+
blockedReason = plan.mcp.reason;
|
|
41
|
+
}
|
|
42
|
+
if (plan.mcp) {
|
|
43
|
+
managedFiles.push({
|
|
44
|
+
path: plan.mcp.path,
|
|
45
|
+
type: 'agent.mcp.remoteConfig',
|
|
46
|
+
action: plan.mcp.action,
|
|
47
|
+
status: plan.mcp.action === 'none' ? 'current' : plan.mcp.action === 'manual' ? 'blocked_manual' : 'planned',
|
|
48
|
+
reason: plan.mcp.reason,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const analytics = await inspectAnalyticsInstall(plan);
|
|
53
|
+
if (analytics.publicTracker) detectedCapabilities.add('analytics.publicTracker');
|
|
54
|
+
if (analytics.routePrivacy) detectedCapabilities.add('analytics.routePrivacy');
|
|
55
|
+
if (plan.analytics.layoutTarget) {
|
|
56
|
+
managedFiles.push({
|
|
57
|
+
path: plan.analytics.layoutTarget,
|
|
58
|
+
type: 'analytics.publicTracker',
|
|
59
|
+
action: analytics.publicTracker && analytics.routePrivacy ? 'none' : 'update',
|
|
60
|
+
status: analytics.publicTracker && analytics.routePrivacy ? 'current' : 'planned',
|
|
61
|
+
reason: analytics.reason,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (plan.remote?.tokenName && /oauth/i.test(plan.remote.tokenName)) {
|
|
66
|
+
detectedCapabilities.add('agent.oauthReady');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const applyResult = details.applyResult ?? buildApplyResult(details);
|
|
70
|
+
return {
|
|
71
|
+
phase,
|
|
72
|
+
cliVersion: CLI_LOCAL_VERSION,
|
|
73
|
+
localContractVersion: CURRENT_SETUP_CONTRACT_VERSION,
|
|
74
|
+
detectedCapabilities: [...detectedCapabilities].sort(),
|
|
75
|
+
...(blockedReason ? { blockedReason } : {}),
|
|
76
|
+
requiresAgentRestart: Boolean(details.requiresAgentRestart),
|
|
77
|
+
managedFiles: managedFiles.map(sanitizeManagedFile),
|
|
78
|
+
repo: {
|
|
79
|
+
framework: plan.repo.framework,
|
|
80
|
+
packageManager: plan.repo.packageManager,
|
|
81
|
+
layoutTarget: plan.analytics.layoutTarget ?? null,
|
|
82
|
+
routeCounts: {
|
|
83
|
+
allowed: plan.repo.routePrivacy.allowed.length,
|
|
84
|
+
blocked: plan.repo.routePrivacy.blocked.length,
|
|
85
|
+
review: plan.repo.routePrivacy.review.length,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
...(applyResult ? { applyResult } : {}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function formatSetupReportSummary(setupReport) {
|
|
93
|
+
if (!setupReport) return [];
|
|
94
|
+
const localSetup = setupReport.result?.localSetup ?? null;
|
|
95
|
+
const lines = [
|
|
96
|
+
'Local setup contract',
|
|
97
|
+
`- CLI version: ${CLI_LOCAL_VERSION}`,
|
|
98
|
+
`- Local contract: ${CURRENT_SETUP_CONTRACT_VERSION}`,
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
if (setupReport.status === 'skipped') {
|
|
102
|
+
lines.push('- Server report: skipped by --skip-setup-report');
|
|
103
|
+
return lines;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (setupReport.status === 'failed') {
|
|
107
|
+
lines.push(`- Server report: failed (${setupReport.error})`);
|
|
108
|
+
lines.push('- Local file changes are still reviewable; DistributionOS may not show the latest install state until reporting succeeds.');
|
|
109
|
+
return lines;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!localSetup) {
|
|
113
|
+
lines.push('- Server report: pending');
|
|
114
|
+
return lines;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
lines.push(`- Server status: ${localSetup.status}`);
|
|
118
|
+
lines.push(`- Missing capabilities: ${formatList(localSetup.missingCapabilities)}`);
|
|
119
|
+
if (localSetup.status !== 'current') {
|
|
120
|
+
lines.push(`- Review command: ${localSetup.updateCommand}`);
|
|
121
|
+
lines.push(`- Apply command: ${localSetup.applyCommand}`);
|
|
122
|
+
}
|
|
123
|
+
if (localSetup.requiresAgentRestart) {
|
|
124
|
+
lines.push('- Agent restart: required after apply so MCP changes load.');
|
|
125
|
+
}
|
|
126
|
+
return lines;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function detectBootstrapVersion(plan) {
|
|
130
|
+
const current = (plan.repo.instructionFiles ?? []).find((file) => file.hasManagedDistributionOsBlock);
|
|
131
|
+
return current?.managedDistributionOsVersion ??
|
|
132
|
+
(plan.bootstrap.action === 'none' ? CURRENT_BOOTSTRAP_VERSION : null);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function inspectAnalyticsInstall(plan) {
|
|
136
|
+
const target = plan.analytics.layoutTarget;
|
|
137
|
+
if (!target) {
|
|
138
|
+
return {
|
|
139
|
+
publicTracker: false,
|
|
140
|
+
routePrivacy: false,
|
|
141
|
+
reason: 'No supported shared analytics layout was detected.',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let content = '';
|
|
146
|
+
try {
|
|
147
|
+
content = await fs.readFile(path.join(plan.cwd, target), 'utf8');
|
|
148
|
+
} catch {
|
|
149
|
+
return {
|
|
150
|
+
publicTracker: false,
|
|
151
|
+
routePrivacy: false,
|
|
152
|
+
reason: 'Analytics layout could not be read.',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const managed = ANALYTICS_BLOCK_RE.test(content);
|
|
157
|
+
const publicTracker = managed && content.includes(`${new URL(MCP_SERVER_URL).origin}/api/analytics/script/`);
|
|
158
|
+
const contractVersion = plan.analytics.contractVersion || CURRENT_ANALYTICS_CONTRACT_VERSION;
|
|
159
|
+
const routePrivacy =
|
|
160
|
+
managed &&
|
|
161
|
+
content.includes('distributionOSAnalyticsConfig') &&
|
|
162
|
+
content.includes(contractVersion) &&
|
|
163
|
+
content.includes('/workflow/**') &&
|
|
164
|
+
content.includes('/workflows/**');
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
publicTracker,
|
|
168
|
+
routePrivacy,
|
|
169
|
+
reason: publicTracker && routePrivacy
|
|
170
|
+
? 'Managed analytics install is current.'
|
|
171
|
+
: 'Managed analytics install is missing or stale.',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function buildApplyResult(details) {
|
|
176
|
+
if (!details.changes && !details.validation && !details.inspection) return null;
|
|
177
|
+
return {
|
|
178
|
+
changedFiles: [...new Set((details.changes ?? [])
|
|
179
|
+
.filter((change) => change.changed && change.file)
|
|
180
|
+
.map((change) => change.file))],
|
|
181
|
+
validation: (details.validation ?? []).map((result) => ({
|
|
182
|
+
name: result.name,
|
|
183
|
+
status: result.exitCode === 0
|
|
184
|
+
? 'passed'
|
|
185
|
+
: result.introducedFailure
|
|
186
|
+
? 'failed_after_cli_changes'
|
|
187
|
+
: result.preExistingFailure
|
|
188
|
+
? 'pre_existing_failure'
|
|
189
|
+
: 'failed',
|
|
190
|
+
})),
|
|
191
|
+
inspection: (details.inspection ?? []).map((result) => ({
|
|
192
|
+
name: result.name,
|
|
193
|
+
status: result.status,
|
|
194
|
+
})),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function sanitizeManagedFile(file) {
|
|
199
|
+
return {
|
|
200
|
+
path: file.path,
|
|
201
|
+
type: file.type,
|
|
202
|
+
action: file.action,
|
|
203
|
+
status: file.status,
|
|
204
|
+
reason: file.reason,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function formatList(values) {
|
|
209
|
+
if (!Array.isArray(values) || values.length === 0) return 'none';
|
|
210
|
+
return values.join(', ');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function extractBootstrapMetadata(content) {
|
|
214
|
+
const match = String(content ?? '').match(BOOTSTRAP_BLOCK_RE);
|
|
215
|
+
if (!match) return { hasManagedDistributionOsBlock: false, version: null, appId: null };
|
|
216
|
+
const attrs = match[1] ?? '';
|
|
217
|
+
return {
|
|
218
|
+
hasManagedDistributionOsBlock: true,
|
|
219
|
+
version: attrs.match(/\bversion=([^\s>]+)/i)?.[1] ?? null,
|
|
220
|
+
appId: attrs.match(/\bappId=([^\s>]+)/i)?.[1] ?? null,
|
|
221
|
+
};
|
|
222
|
+
}
|