@distributionos/cli 0.1.0

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 ADDED
@@ -0,0 +1,487 @@
1
+ import { CLI_LOCAL_VERSION, DEFAULT_API_BASE } from './constants.js';
2
+ import {
3
+ ensureAnalyticsSite,
4
+ reportImplementation,
5
+ resolveAuthToken,
6
+ verifyAnalyticsInstall,
7
+ } from './api.js';
8
+ import { loginWithOAuth } from './oauth.js';
9
+ import { createSetupPlan, formatPlanText } from './plan.js';
10
+ import { applySetupPlan } from './mutate.js';
11
+ import { submitInitialization } from './initialization.js';
12
+ import { compareValidationResults, inspectInstallArtifacts, runValidationPlan } from './validation.js';
13
+
14
+ export async function runCli(argv, runtime) {
15
+ const stdout = runtime.stdout;
16
+ const stderr = runtime.stderr;
17
+ let args;
18
+ try {
19
+ args = parseArgs(argv.slice(2));
20
+ } catch (error) {
21
+ stderr.write(`${error instanceof Error ? error.message : 'Invalid arguments'}\n\n`);
22
+ stderr.write(helpText());
23
+ return 1;
24
+ }
25
+
26
+ if (args.version) {
27
+ stdout.write(`${CLI_LOCAL_VERSION}\n`);
28
+ return 0;
29
+ }
30
+
31
+ if (args.help || !args.command) {
32
+ stdout.write(helpText());
33
+ return 0;
34
+ }
35
+
36
+ if (args.command === 'login') {
37
+ if (!args.appId) {
38
+ stderr.write('Missing required --app <appId>.\n\n');
39
+ stderr.write(helpText());
40
+ return 1;
41
+ }
42
+
43
+ try {
44
+ const credential = await loginWithOAuth({
45
+ appId: args.appId,
46
+ apiBase: args.apiBase ?? DEFAULT_API_BASE,
47
+ env: runtime.env,
48
+ fetch: runtime.fetch,
49
+ stdout,
50
+ openBrowser: !args.noOpen,
51
+ });
52
+ stdout.write(`DistributionOS OAuth connected. Access token expires ${credential.expiresAt}.\n`);
53
+ return 0;
54
+ } catch (error) {
55
+ stderr.write(`${error instanceof Error ? error.message : 'OAuth login failed'}\n`);
56
+ return 1;
57
+ }
58
+ }
59
+
60
+ if (args.command === 'verify') {
61
+ if (!args.appId) {
62
+ stderr.write('Missing required --app <appId>.\n\n');
63
+ stderr.write(helpText());
64
+ return 1;
65
+ }
66
+ if (!args.url) {
67
+ stderr.write('Missing required --url <liveUrl>.\n\n');
68
+ stderr.write(helpText());
69
+ return 1;
70
+ }
71
+
72
+ const authReady = await ensureCliAuth({ args, runtime, stderr });
73
+ if (!authReady) return 1;
74
+
75
+ try {
76
+ const result = await verifyAnalyticsInstall({
77
+ appId: args.appId,
78
+ apiBase: args.apiBase ?? DEFAULT_API_BASE,
79
+ env: runtime.env,
80
+ fetch: runtime.fetch,
81
+ url: args.url,
82
+ expectedContentId: args.contentId,
83
+ contractVersion: args.contractVersion,
84
+ });
85
+ stdout.write(args.json ? `${JSON.stringify(result, null, 2)}\n` : formatVerifyResult(result));
86
+ return 0;
87
+ } catch (error) {
88
+ stderr.write(`${error instanceof Error ? error.message : 'Analytics verification failed'}\n`);
89
+ return 1;
90
+ }
91
+ }
92
+
93
+ if (args.command === 'report-implementation') {
94
+ if (!args.appId) {
95
+ stderr.write('Missing required --app <appId>.\n\n');
96
+ stderr.write(helpText());
97
+ return 1;
98
+ }
99
+ if (!args.artifactId) {
100
+ stderr.write('Missing required --artifact <artifactId>.\n\n');
101
+ stderr.write(helpText());
102
+ return 1;
103
+ }
104
+
105
+ const authReady = await ensureCliAuth({ args, runtime, stderr });
106
+ if (!authReady) return 1;
107
+
108
+ try {
109
+ const result = await reportImplementation({
110
+ appId: args.appId,
111
+ artifactId: args.artifactId,
112
+ apiBase: args.apiBase ?? DEFAULT_API_BASE,
113
+ env: runtime.env,
114
+ fetch: runtime.fetch,
115
+ payload: buildImplementationReportPayload(args),
116
+ });
117
+ stdout.write(args.json ? `${JSON.stringify(result, null, 2)}\n` : formatReportResult(result));
118
+ return 0;
119
+ } catch (error) {
120
+ stderr.write(`${error instanceof Error ? error.message : 'Implementation report failed'}\n`);
121
+ return 1;
122
+ }
123
+ }
124
+
125
+ if (args.command !== 'setup') {
126
+ stderr.write(`Unknown command: ${args.command}\n\n`);
127
+ stderr.write(helpText());
128
+ return 1;
129
+ }
130
+
131
+ if (!args.appId) {
132
+ stderr.write('Missing required --app <appId>.\n\n');
133
+ stderr.write(helpText());
134
+ return 1;
135
+ }
136
+
137
+ if (!args.noFetch) {
138
+ const authReady = await ensureCliAuth({ args, runtime, stderr });
139
+ if (!authReady) return 1;
140
+ }
141
+
142
+ let plan = await createSetupPlan({
143
+ appId: args.appId,
144
+ cwd: args.cwd ?? runtime.cwd,
145
+ apiBase: args.apiBase ?? DEFAULT_API_BASE,
146
+ env: runtime.env,
147
+ fetch: runtime.fetch,
148
+ noFetch: args.noFetch,
149
+ apply: args.apply,
150
+ });
151
+
152
+ if (!args.apply && !args.yes) {
153
+ stdout.write(args.json ? `${JSON.stringify(plan, null, 2)}\n` : formatPlanText(plan));
154
+ if (args.json) return 0;
155
+ const confirmed = await promptForApply(runtime, stdout);
156
+ if (!confirmed) return 0;
157
+ args.apply = true;
158
+ }
159
+
160
+ if (args.yes) {
161
+ args.apply = true;
162
+ }
163
+
164
+ if (args.apply && plan.repo.git.dirty && !args.allowDirty) {
165
+ stderr.write('Refusing to apply changes to a dirty worktree. Commit/stash changes or pass --allow-dirty.\n');
166
+ return 1;
167
+ }
168
+
169
+ if (args.apply && !args.skipAnalytics) {
170
+ const ensured = await ensureAnalyticsSite({
171
+ appId: args.appId,
172
+ apiBase: args.apiBase ?? DEFAULT_API_BASE,
173
+ env: runtime.env,
174
+ fetch: runtime.fetch,
175
+ domain: args.domain,
176
+ ownerVisitExclusionEnabled: true,
177
+ });
178
+ if (ensured.status === 'failed') {
179
+ stderr.write(`Analytics tracker setup skipped: ${ensured.error}\n`);
180
+ }
181
+ }
182
+
183
+ plan = await createSetupPlan({
184
+ appId: args.appId,
185
+ cwd: args.cwd ?? runtime.cwd,
186
+ apiBase: args.apiBase ?? DEFAULT_API_BASE,
187
+ env: runtime.env,
188
+ fetch: runtime.fetch,
189
+ noFetch: args.noFetch,
190
+ apply: args.apply,
191
+ });
192
+
193
+ if (!args.apply) {
194
+ stdout.write(args.json ? `${JSON.stringify(plan, null, 2)}\n` : formatPlanText(plan));
195
+ return 0;
196
+ }
197
+
198
+ try {
199
+ const baselineValidation = args.skipValidate ? [] : await runValidationPlan(plan);
200
+ const changes = await applySetupPlan(plan, {
201
+ allowDirty: args.allowDirty,
202
+ skipAnalytics: args.skipAnalytics,
203
+ });
204
+ const postValidation = args.skipValidate ? [] : await runValidationPlan(plan);
205
+ const validation = compareValidationResults(baselineValidation, postValidation);
206
+ const inspection = args.skipAnalytics ? [] : await inspectInstallArtifacts(plan);
207
+ const token = await resolveAuthToken({
208
+ env: runtime.env,
209
+ apiBase: args.apiBase ?? DEFAULT_API_BASE,
210
+ appId: args.appId,
211
+ fetchImpl: runtime.fetch,
212
+ });
213
+ const initialization = token
214
+ ? await submitInitialization({
215
+ plan,
216
+ token: token.value,
217
+ apiBase: args.apiBase ?? DEFAULT_API_BASE,
218
+ fetchImpl: runtime.fetch,
219
+ domain: args.domain,
220
+ })
221
+ : null;
222
+
223
+ stdout.write(args.json
224
+ ? `${JSON.stringify({ plan, changes, initialization, validation, inspection }, null, 2)}\n`
225
+ : formatApplyResult(plan, changes, initialization, validation, inspection, args.skipValidate));
226
+ return hasBlockingApplyFailure(validation, inspection) ? 1 : 0;
227
+ } catch (error) {
228
+ stderr.write(`${error instanceof Error ? error.message : 'Setup apply failed'}\n`);
229
+ return 1;
230
+ }
231
+ }
232
+
233
+ function hasBlockingApplyFailure(validation, inspection) {
234
+ return validation.some((result) => result.introducedFailure) ||
235
+ inspection.some((result) => result.status === 'failed');
236
+ }
237
+
238
+ export function parseArgs(rawArgs) {
239
+ const parsed = {
240
+ command: null,
241
+ appId: null,
242
+ cwd: null,
243
+ apiBase: null,
244
+ domain: null,
245
+ json: false,
246
+ noFetch: false,
247
+ apply: false,
248
+ allowDirty: false,
249
+ skipAnalytics: false,
250
+ skipValidate: false,
251
+ noOpen: false,
252
+ noLogin: false,
253
+ yes: false,
254
+ url: null,
255
+ contentId: null,
256
+ contractVersion: null,
257
+ artifactId: null,
258
+ status: null,
259
+ summary: null,
260
+ analyticsOptOutReason: null,
261
+ help: false,
262
+ version: false,
263
+ };
264
+
265
+ const args = [...rawArgs];
266
+ if (args[0] === '--help' || args[0] === '-h') {
267
+ parsed.help = true;
268
+ args.shift();
269
+ } else if (args[0] === '--version' || args[0] === '-v') {
270
+ parsed.version = true;
271
+ args.shift();
272
+ } else {
273
+ parsed.command = args.shift() ?? null;
274
+ }
275
+
276
+ for (let index = 0; index < args.length; index += 1) {
277
+ const arg = args[index];
278
+ if (arg === '--help' || arg === '-h') parsed.help = true;
279
+ else if (arg === '--version' || arg === '-v') parsed.version = true;
280
+ else if (arg === '--json') parsed.json = true;
281
+ else if (arg === '--no-fetch') parsed.noFetch = true;
282
+ else if (arg === '--apply') parsed.apply = true;
283
+ else if (arg === '--allow-dirty') parsed.allowDirty = true;
284
+ else if (arg === '--skip-analytics') parsed.skipAnalytics = true;
285
+ else if (arg === '--skip-validate') parsed.skipValidate = true;
286
+ else if (arg === '--no-open') parsed.noOpen = true;
287
+ else if (arg === '--no-login') parsed.noLogin = true;
288
+ else if (arg === '--yes' || arg === '-y') parsed.yes = true;
289
+ else if (arg === '--app') parsed.appId = args[++index] ?? null;
290
+ else if (arg.startsWith('--app=')) parsed.appId = arg.slice('--app='.length);
291
+ else if (arg === '--cwd') parsed.cwd = args[++index] ?? null;
292
+ else if (arg.startsWith('--cwd=')) parsed.cwd = arg.slice('--cwd='.length);
293
+ else if (arg === '--api-base') parsed.apiBase = args[++index] ?? null;
294
+ else if (arg.startsWith('--api-base=')) parsed.apiBase = arg.slice('--api-base='.length);
295
+ else if (arg === '--domain') parsed.domain = args[++index] ?? null;
296
+ else if (arg.startsWith('--domain=')) parsed.domain = arg.slice('--domain='.length);
297
+ else if (arg === '--url') parsed.url = args[++index] ?? null;
298
+ else if (arg.startsWith('--url=')) parsed.url = arg.slice('--url='.length);
299
+ else if (arg === '--content-id') parsed.contentId = args[++index] ?? null;
300
+ else if (arg.startsWith('--content-id=')) parsed.contentId = arg.slice('--content-id='.length);
301
+ else if (arg === '--contract-version') parsed.contractVersion = args[++index] ?? null;
302
+ else if (arg.startsWith('--contract-version=')) parsed.contractVersion = arg.slice('--contract-version='.length);
303
+ else if (arg === '--artifact') parsed.artifactId = args[++index] ?? null;
304
+ else if (arg.startsWith('--artifact=')) parsed.artifactId = arg.slice('--artifact='.length);
305
+ else if (arg === '--status') parsed.status = args[++index] ?? null;
306
+ else if (arg.startsWith('--status=')) parsed.status = arg.slice('--status='.length);
307
+ else if (arg === '--summary') parsed.summary = args[++index] ?? null;
308
+ else if (arg.startsWith('--summary=')) parsed.summary = arg.slice('--summary='.length);
309
+ else if (arg === '--analytics-opt-out') parsed.analyticsOptOutReason = args[++index] ?? null;
310
+ else if (arg.startsWith('--analytics-opt-out=')) parsed.analyticsOptOutReason = arg.slice('--analytics-opt-out='.length);
311
+ else throw new Error(`Unknown argument: ${arg}`);
312
+ }
313
+
314
+ return parsed;
315
+ }
316
+
317
+ function helpText() {
318
+ return [
319
+ 'DistributionOS CLI',
320
+ '',
321
+ 'Usage:',
322
+ ' distributionos setup --app <appId> [--api-base <url>] [--cwd <path>] [--json] [--no-fetch]',
323
+ ' distributionos setup --app <appId> --apply [--domain <domain>] [--allow-dirty]',
324
+ ' distributionos login --app <appId> [--api-base <url>]',
325
+ ' distributionos verify --app <appId> --url <liveUrl> [--content-id <id>]',
326
+ ' distributionos report-implementation --app <appId> --artifact <artifactId> [--url <liveUrl>] [--summary <text>]',
327
+ '',
328
+ 'Default mode prints a dry-run plan. Interactive terminals can confirm the plan before mutation; non-interactive runs need --apply or --yes.',
329
+ '',
330
+ 'Authentication:',
331
+ ' Happy path uses DistributionOS MCP OAuth and stores app-scoped credentials outside the repo.',
332
+ ' DISTRIBUTIONOS_API_KEY, DOS_API_KEY, or DISTRIBUTIONOS_TOKEN are advanced fallbacks.',
333
+ ' Tokens are never printed or written to committed files.',
334
+ '',
335
+ ].join('\n');
336
+ }
337
+
338
+ async function ensureCliAuth({ args, runtime, stderr }) {
339
+ const existingToken = await resolveAuthToken({
340
+ env: runtime.env,
341
+ apiBase: args.apiBase ?? DEFAULT_API_BASE,
342
+ appId: args.appId,
343
+ fetchImpl: runtime.fetch,
344
+ });
345
+ if (existingToken || args.noLogin) return true;
346
+
347
+ try {
348
+ await loginWithOAuth({
349
+ appId: args.appId,
350
+ apiBase: args.apiBase ?? DEFAULT_API_BASE,
351
+ env: runtime.env,
352
+ fetch: runtime.fetch,
353
+ stdout: runtime.stdout,
354
+ openBrowser: !args.noOpen,
355
+ });
356
+ return true;
357
+ } catch (error) {
358
+ stderr.write(`${error instanceof Error ? error.message : 'OAuth login failed'}\n`);
359
+ return false;
360
+ }
361
+ }
362
+
363
+ async function promptForApply(runtime, stdout) {
364
+ const stdin = runtime.stdin;
365
+ const isInteractive =
366
+ Boolean(stdin?.isTTY) &&
367
+ Boolean(runtime.stdout?.isTTY) &&
368
+ typeof stdin.once === 'function' &&
369
+ typeof stdin.resume === 'function';
370
+
371
+ if (!isInteractive) return false;
372
+
373
+ stdout.write('\nApply this plan now? Type yes to continue: ');
374
+ const answer = await new Promise((resolve) => {
375
+ stdin.once('data', (chunk) => resolve(String(chunk)));
376
+ stdin.resume();
377
+ });
378
+ return answer.trim().toLowerCase() === 'yes';
379
+ }
380
+
381
+ function buildImplementationReportPayload(args) {
382
+ return {
383
+ ...(args.url ? { publishedUrl: args.url } : {}),
384
+ status: args.status ?? 'live',
385
+ ...(args.contentId ? { analyticsIds: { dosContentId: args.contentId } } : {}),
386
+ ...(args.analyticsOptOutReason ? { analyticsOptOutReason: args.analyticsOptOutReason } : {}),
387
+ metadata: {
388
+ reportedBy: 'distributionos-cli',
389
+ ...(args.summary ? { summary: args.summary } : {}),
390
+ },
391
+ };
392
+ }
393
+
394
+ function formatVerifyResult(result) {
395
+ const lines = [
396
+ 'DistributionOS analytics verification',
397
+ `- Status: ${result.status ?? 'unknown'}`,
398
+ `- URL: ${result.checkedUrl ?? 'unknown'}`,
399
+ `- Tracking state: ${result.trackingStatus?.state ?? result.trackingStatus?.status ?? 'unknown'}`,
400
+ '',
401
+ 'Checks',
402
+ ];
403
+
404
+ for (const [key, value] of Object.entries(result.checks ?? {})) {
405
+ lines.push(`- ${key}: ${value ? 'yes' : 'no'}`);
406
+ }
407
+
408
+ if (Array.isArray(result.notes) && result.notes.length > 0) {
409
+ lines.push('', 'Notes', ...result.notes.map((note) => `- ${note}`));
410
+ }
411
+
412
+ return `${lines.join('\n')}\n`;
413
+ }
414
+
415
+ function formatReportResult(result) {
416
+ const artifact = result.artifact ?? result;
417
+ const lines = [
418
+ 'DistributionOS implementation report',
419
+ `- Status: ${artifact.status ?? result.status ?? 'submitted'}`,
420
+ `- Artifact: ${artifact.id ?? result.id ?? 'updated'}`,
421
+ ];
422
+ if (artifact.publishedUrl ?? result.publishedUrl) {
423
+ lines.push(`- Published URL: ${artifact.publishedUrl ?? result.publishedUrl}`);
424
+ }
425
+ const followUp = artifact.implementationFollowUp ?? result.implementationFollowUp;
426
+ if (followUp?.googleSearchConsoleInspectionUrl) {
427
+ lines.push(`- Search Console inspection: ${followUp.googleSearchConsoleInspectionUrl}`);
428
+ }
429
+ return `${lines.join('\n')}\n`;
430
+ }
431
+
432
+ function formatApplyResult(plan, changes, initialization, validation, inspection, skipValidate) {
433
+ const lines = [
434
+ formatPlanText({ ...plan, mode: 'apply' }).trimEnd(),
435
+ '',
436
+ 'Applied changes',
437
+ ];
438
+
439
+ if (changes.length === 0) {
440
+ lines.push('- No file changes were needed.');
441
+ } else {
442
+ for (const change of changes) {
443
+ lines.push(change.changed
444
+ ? `- ${change.type}: ${change.action} ${change.file}`
445
+ : `- ${change.type}: ${change.reason}`);
446
+ }
447
+ }
448
+
449
+ lines.push(initialization?.reviewUrl
450
+ ? `- Initialization submitted. Review URL: ${initialization.reviewUrl}`
451
+ : '- Initialization was not submitted because no auth token was available.');
452
+
453
+ lines.push('', 'Validation');
454
+ if (skipValidate) {
455
+ lines.push('- Skipped by --skip-validate.');
456
+ } else if (!validation.length) {
457
+ lines.push('- No build/test/lint commands were detected.');
458
+ } else {
459
+ for (const result of validation) {
460
+ const status = result.exitCode === 0
461
+ ? 'passed'
462
+ : result.introducedFailure
463
+ ? 'failed after CLI changes'
464
+ : result.preExistingFailure
465
+ ? 'failed before CLI changes too'
466
+ : 'failed';
467
+ lines.push(`- ${result.name}: ${status} (${result.command})`);
468
+ if (result.exitCode !== 0 && (result.stderr || result.stdout)) {
469
+ lines.push(` Last output: ${oneLine(result.stderr || result.stdout)}`);
470
+ }
471
+ }
472
+ }
473
+ lines.push('', 'Install inspection');
474
+ if (!inspection.length) {
475
+ lines.push('- No install artifacts needed inspection.');
476
+ } else {
477
+ for (const result of inspection) {
478
+ lines.push(`- ${result.name}: ${result.status} - ${result.message}`);
479
+ }
480
+ }
481
+ lines.push('- Nothing was committed, pushed, or deployed.');
482
+ return `${lines.join('\n')}\n`;
483
+ }
484
+
485
+ function oneLine(value) {
486
+ return String(value).split(/\r?\n/).filter(Boolean).slice(-1)[0]?.slice(0, 240) ?? '';
487
+ }
@@ -0,0 +1,60 @@
1
+ export const CLI_PACKAGE_NAME = '@distributionos/cli';
2
+ export const CLI_LOCAL_VERSION = '0.0.0-local';
3
+ export const DEFAULT_API_BASE = 'https://distributionos.dev';
4
+ export const MCP_SERVER_URL = 'https://distributionos.dev/api/mcp';
5
+ export const CURRENT_BOOTSTRAP_VERSION = '2026.06.13';
6
+ export const CURRENT_ANALYTICS_CONTRACT_VERSION = '2026.06.13';
7
+
8
+ export const TOKEN_ENV_NAMES = [
9
+ 'DISTRIBUTIONOS_API_KEY',
10
+ 'DOS_API_KEY',
11
+ 'DISTRIBUTIONOS_TOKEN',
12
+ ];
13
+
14
+ export const PUBLIC_ROUTE_PATTERNS = [
15
+ '/',
16
+ '/blog/**',
17
+ '/articles/**',
18
+ '/posts/**',
19
+ '/docs/**',
20
+ '/guides/**',
21
+ '/resources/**',
22
+ '/pricing/**',
23
+ '/features/**',
24
+ '/use-cases/**',
25
+ '/case-studies/**',
26
+ '/about/**',
27
+ '/contact/**',
28
+ '/changelog/**',
29
+ ];
30
+
31
+ export const PRIVATE_ROUTE_PATTERNS = [
32
+ '/dashboard/**',
33
+ '/account/**',
34
+ '/billing/**',
35
+ '/settings/**',
36
+ '/auth/**',
37
+ '/login/**',
38
+ '/signup/**',
39
+ '/admin/**',
40
+ '/api/**',
41
+ '/app/**',
42
+ '/apps/**',
43
+ '/product/**',
44
+ '/products/**',
45
+ '/project/**',
46
+ '/projects/**',
47
+ '/workspace/**',
48
+ '/workspaces/**',
49
+ '/customer/**',
50
+ '/customers/**',
51
+ '/client/**',
52
+ '/clients/**',
53
+ '/user/**',
54
+ '/users/**',
55
+ '/redirect/**',
56
+ '/go/**',
57
+ '/out/**',
58
+ '/checkout/**',
59
+ '/portal/**',
60
+ ];