@distributionos/cli 0.1.0 → 0.1.2
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 +6 -6
- package/package.json +1 -1
- package/src/api.js +88 -55
- package/src/cli.js +33 -14
- package/src/constants.js +7 -3
- package/src/oauth.js +30 -17
- package/src/plan.js +44 -19
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# DistributionOS CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Installer for connecting an app repo to DistributionOS.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npx @distributionos/cli setup --app <appId>
|
|
@@ -8,11 +8,11 @@ npx @distributionos/cli setup --app <appId>
|
|
|
8
8
|
|
|
9
9
|
Default setup opens the DistributionOS MCP OAuth approval flow when needed, then prints a plan first. In an interactive terminal, approve the plan to apply the managed setup changes. In non-interactive agent runs, use `--apply` only after reviewing the dry-run output.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
Agents that support installable skills can also load the public DistributionOS agent skill before using the CLI:
|
|
12
|
+
|
|
13
|
+
https://github.com/lawfan1026/distributionos-agent-skill
|
|
14
|
+
|
|
15
|
+
The CLI remains the default setup path. The skill is a public agent-readable guide for when to use the CLI, MCP, API fallback, analytics install, verification, and implementation reporting.
|
|
16
16
|
|
|
17
17
|
## Commands
|
|
18
18
|
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { DEFAULT_API_BASE, TOKEN_ENV_NAMES } from './constants.js';
|
|
2
|
-
import { getStoredCredential } from './auth-store.js';
|
|
3
|
-
import { isCredentialFresh, refreshOAuthCredential } from './oauth.js';
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
import { getStoredCredential } from './auth-store.js';
|
|
3
|
+
import { isCredentialFresh, refreshOAuthCredential } from './oauth.js';
|
|
4
|
+
|
|
5
|
+
export class DistributionOsApiError extends Error {
|
|
6
|
+
constructor(message, { status = 0, code = null } = {}) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'DistributionOsApiError';
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.code = code;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveEnvToken(env) {
|
|
15
|
+
for (const name of TOKEN_ENV_NAMES) {
|
|
16
|
+
const value = env?.[name]?.trim();
|
|
17
|
+
if (value) return { source: 'env', name, value };
|
|
18
|
+
}
|
|
10
19
|
return null;
|
|
11
20
|
}
|
|
12
21
|
|
|
@@ -39,48 +48,69 @@ export async function fetchDistributionOsSetupContext(input) {
|
|
|
39
48
|
apiBase: input.apiBase ?? DEFAULT_API_BASE,
|
|
40
49
|
appId: input.appId,
|
|
41
50
|
fetchImpl: input.fetch,
|
|
42
|
-
});
|
|
43
|
-
if (!token || input.noFetch) {
|
|
44
|
-
return {
|
|
45
|
-
status: 'not_authenticated',
|
|
46
|
-
tokenName: token?.name ?? null,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
51
|
+
});
|
|
52
|
+
if (!token || input.noFetch) {
|
|
53
|
+
return {
|
|
54
|
+
status: input.noFetch ? 'skipped' : 'not_authenticated',
|
|
55
|
+
tokenName: token?.name ?? null,
|
|
56
|
+
access: {
|
|
57
|
+
allowed: false,
|
|
58
|
+
reason: input.noFetch ? 'remote_fetch_skipped' : 'not_authenticated',
|
|
59
|
+
billingUrl: `${normalizeApiBase(input.apiBase ?? DEFAULT_API_BASE)}/dashboard/account/billing`,
|
|
60
|
+
},
|
|
61
|
+
usage: null,
|
|
62
|
+
instructions: null,
|
|
63
|
+
analyticsContract: null,
|
|
64
|
+
errors: [],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const apiBase = normalizeApiBase(input.apiBase ?? DEFAULT_API_BASE);
|
|
69
|
+
const errors = [];
|
|
70
|
+
let subscriptionRequired = false;
|
|
71
|
+
const capture = (label) => (error) => {
|
|
72
|
+
if (error instanceof DistributionOsApiError && (error.status === 402 || error.code === 'SUBSCRIPTION_REQUIRED')) {
|
|
73
|
+
subscriptionRequired = true;
|
|
74
|
+
}
|
|
75
|
+
errors.push(`${label} failed: ${error.message}`);
|
|
76
|
+
return null;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const [instructions, analyticsContract, usage] = await Promise.all([
|
|
80
|
+
fetchJson({
|
|
81
|
+
fetchImpl: input.fetch,
|
|
82
|
+
apiBase,
|
|
83
|
+
token: token.value,
|
|
84
|
+
path: `/api/v1/apps/${encodeURIComponent(input.appId)}/agent/instructions`,
|
|
85
|
+
}).catch(capture('Agent instructions fetch')),
|
|
86
|
+
fetchJson({
|
|
87
|
+
fetchImpl: input.fetch,
|
|
88
|
+
apiBase,
|
|
89
|
+
token: token.value,
|
|
90
|
+
path: `/api/v1/apps/${encodeURIComponent(input.appId)}/analytics/contract`,
|
|
91
|
+
}).catch(capture('Analytics contract fetch')),
|
|
92
|
+
fetchJson({
|
|
93
|
+
fetchImpl: input.fetch,
|
|
94
|
+
apiBase,
|
|
95
|
+
token: token.value,
|
|
96
|
+
path: `/api/v1/apps/${encodeURIComponent(input.appId)}/usage`,
|
|
97
|
+
}).catch(capture('Usage fetch')),
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
status: errors.length === 0 ? 'fetched' : 'partial',
|
|
102
|
+
tokenName: token.name,
|
|
103
|
+
access: {
|
|
104
|
+
allowed: !subscriptionRequired,
|
|
105
|
+
reason: subscriptionRequired ? 'subscription_required' : errors.length === 0 ? 'active' : 'partial',
|
|
106
|
+
billingUrl: `${apiBase}/dashboard/account/billing`,
|
|
107
|
+
},
|
|
108
|
+
usage,
|
|
109
|
+
instructions,
|
|
110
|
+
analyticsContract,
|
|
111
|
+
errors,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
84
114
|
|
|
85
115
|
export async function ensureAnalyticsSite(input) {
|
|
86
116
|
const token = await resolveAuthToken({
|
|
@@ -198,11 +228,14 @@ async function fetchJson({ fetchImpl, apiBase, token, path }) {
|
|
|
198
228
|
},
|
|
199
229
|
});
|
|
200
230
|
const body = await response.json().catch(() => null);
|
|
201
|
-
if (!response.ok || body?.success === false) {
|
|
202
|
-
throw new
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
231
|
+
if (!response.ok || body?.success === false) {
|
|
232
|
+
throw new DistributionOsApiError(body?.error || `HTTP ${response.status}`, {
|
|
233
|
+
status: response.status,
|
|
234
|
+
code: body?.code ?? null,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return body?.data ?? body;
|
|
238
|
+
}
|
|
206
239
|
|
|
207
240
|
function normalizeApiBase(value) {
|
|
208
241
|
return String(value || DEFAULT_API_BASE).replace(/\/+$/, '');
|
package/src/cli.js
CHANGED
|
@@ -157,14 +157,19 @@ export async function runCli(argv, runtime) {
|
|
|
157
157
|
args.apply = true;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
if (args.yes) {
|
|
161
|
-
args.apply = true;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (args.apply && plan
|
|
165
|
-
stderr.write(
|
|
166
|
-
return 1;
|
|
167
|
-
}
|
|
160
|
+
if (args.yes) {
|
|
161
|
+
args.apply = true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (args.apply && hasRemoteAccessBlock(plan)) {
|
|
165
|
+
stderr.write(formatRemoteAccessBlock(plan));
|
|
166
|
+
return 1;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (args.apply && plan.repo.git.dirty && !args.allowDirty) {
|
|
170
|
+
stderr.write('Refusing to apply changes to a dirty worktree. Commit/stash changes or pass --allow-dirty.\n');
|
|
171
|
+
return 1;
|
|
172
|
+
}
|
|
168
173
|
|
|
169
174
|
if (args.apply && !args.skipAnalytics) {
|
|
170
175
|
const ensured = await ensureAnalyticsSite({
|
|
@@ -230,12 +235,26 @@ export async function runCli(argv, runtime) {
|
|
|
230
235
|
}
|
|
231
236
|
}
|
|
232
237
|
|
|
233
|
-
function hasBlockingApplyFailure(validation, inspection) {
|
|
234
|
-
return validation.some((result) => result.introducedFailure) ||
|
|
235
|
-
inspection.some((result) => result.status === 'failed');
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
238
|
+
function hasBlockingApplyFailure(validation, inspection) {
|
|
239
|
+
return validation.some((result) => result.introducedFailure) ||
|
|
240
|
+
inspection.some((result) => result.status === 'failed');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function hasRemoteAccessBlock(plan) {
|
|
244
|
+
return plan.remote?.access?.reason === 'subscription_required';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function formatRemoteAccessBlock(plan) {
|
|
248
|
+
const billingUrl = plan.remote?.access?.billingUrl ?? `${plan.apiBase ?? DEFAULT_API_BASE}/dashboard/account/billing`;
|
|
249
|
+
return [
|
|
250
|
+
'DistributionOS setup requires an active Builder or Distributor subscription before applying changes.',
|
|
251
|
+
`Manage billing: ${billingUrl}`,
|
|
252
|
+
'No files were changed.',
|
|
253
|
+
'',
|
|
254
|
+
].join('\n');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function parseArgs(rawArgs) {
|
|
239
258
|
const parsed = {
|
|
240
259
|
command: null,
|
|
241
260
|
appId: null,
|
package/src/constants.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
const cliPackageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
4
|
+
|
|
1
5
|
export const CLI_PACKAGE_NAME = '@distributionos/cli';
|
|
2
|
-
export const CLI_LOCAL_VERSION = '0.0.0-local';
|
|
6
|
+
export const CLI_LOCAL_VERSION = typeof cliPackageJson.version === 'string' ? cliPackageJson.version : '0.0.0-local';
|
|
3
7
|
export const DEFAULT_API_BASE = 'https://distributionos.dev';
|
|
4
8
|
export const MCP_SERVER_URL = 'https://distributionos.dev/api/mcp';
|
|
5
|
-
export const CURRENT_BOOTSTRAP_VERSION = '2026.06.
|
|
6
|
-
export const CURRENT_ANALYTICS_CONTRACT_VERSION = '2026.06.
|
|
9
|
+
export const CURRENT_BOOTSTRAP_VERSION = '2026.06.14';
|
|
10
|
+
export const CURRENT_ANALYTICS_CONTRACT_VERSION = '2026.06.14';
|
|
7
11
|
|
|
8
12
|
export const TOKEN_ENV_NAMES = [
|
|
9
13
|
'DISTRIBUTIONOS_API_KEY',
|
package/src/oauth.js
CHANGED
|
@@ -27,11 +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
|
-
|
|
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'));
|
|
35
38
|
if (options.openBrowser !== false) openUrl(authorizeUrl);
|
|
36
39
|
|
|
37
40
|
const authResult = await callback.waitForCallback();
|
|
@@ -204,18 +207,28 @@ async function createCallbackServer() {
|
|
|
204
207
|
};
|
|
205
208
|
}
|
|
206
209
|
|
|
207
|
-
function
|
|
208
|
-
const command =
|
|
209
|
-
? 'cmd'
|
|
210
|
-
:
|
|
211
|
-
? 'open'
|
|
212
|
-
: 'xdg-open';
|
|
213
|
-
const args =
|
|
214
|
-
? ['/c', 'start', '""', url]
|
|
215
|
-
: [url];
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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,
|
|
219
232
|
stdio: 'ignore',
|
|
220
233
|
windowsHide: true,
|
|
221
234
|
});
|
package/src/plan.js
CHANGED
|
@@ -106,11 +106,12 @@ export function formatPlanText(plan) {
|
|
|
106
106
|
`- Layout candidates: ${formatList(plan.repo.layoutCandidates)}`,
|
|
107
107
|
`- Content files found: ${plan.repo.contentFiles.length}`,
|
|
108
108
|
`- Deploy hints: ${formatList(plan.repo.deployHints)}`,
|
|
109
|
-
'',
|
|
110
|
-
'DistributionOS access',
|
|
111
|
-
remoteSummary(plan),
|
|
112
|
-
...plan.remote.
|
|
113
|
-
|
|
109
|
+
'',
|
|
110
|
+
'DistributionOS access',
|
|
111
|
+
remoteSummary(plan),
|
|
112
|
+
...usageSummary(plan.remote.usage),
|
|
113
|
+
...plan.remote.errors.map(error => `- ${error}`),
|
|
114
|
+
'',
|
|
114
115
|
'Planned changes',
|
|
115
116
|
`1. Agent bootstrap: ${plan.bootstrap.action} ${plan.bootstrap.target}`,
|
|
116
117
|
` ${plan.bootstrap.reason}`,
|
|
@@ -209,11 +210,14 @@ function selectAnalyticsLayoutTarget(repo) {
|
|
|
209
210
|
return candidates[0] ?? null;
|
|
210
211
|
}
|
|
211
212
|
|
|
212
|
-
function buildWarnings({ repo, remote }) {
|
|
213
|
-
const warnings = [];
|
|
214
|
-
if (
|
|
215
|
-
warnings.push('
|
|
216
|
-
}
|
|
213
|
+
function buildWarnings({ repo, remote }) {
|
|
214
|
+
const warnings = [];
|
|
215
|
+
if (remote.access?.reason === 'subscription_required') {
|
|
216
|
+
warnings.push('Active Builder or Distributor subscription required before applying setup.');
|
|
217
|
+
}
|
|
218
|
+
if (repo.git.dirty) {
|
|
219
|
+
warnings.push('Worktree has existing changes. Keep baseline failures separate before applying edits.');
|
|
220
|
+
}
|
|
217
221
|
if (!remote.instructions) {
|
|
218
222
|
warnings.push('Live DistributionOS instructions were not fetched. Connect MCP OAuth or provide a safe env token before applying app-specific setup.');
|
|
219
223
|
}
|
|
@@ -237,15 +241,36 @@ function buildValidationPlan(repo) {
|
|
|
237
241
|
};
|
|
238
242
|
}
|
|
239
243
|
|
|
240
|
-
function remoteSummary(plan) {
|
|
241
|
-
if (plan.remote.status === 'fetched') {
|
|
242
|
-
return `- Fetched live instructions and analytics contract using ${plan.remote.tokenName}.`;
|
|
243
|
-
}
|
|
244
|
-
if (plan.remote.status === 'partial') {
|
|
245
|
-
return `- Used ${plan.remote.tokenName}, but one or more DistributionOS requests failed.`;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
244
|
+
function remoteSummary(plan) {
|
|
245
|
+
if (plan.remote.status === 'fetched') {
|
|
246
|
+
return `- Fetched live instructions and analytics contract using ${plan.remote.tokenName}.`;
|
|
247
|
+
}
|
|
248
|
+
if (plan.remote.status === 'partial') {
|
|
249
|
+
return `- Used ${plan.remote.tokenName}, but one or more DistributionOS requests failed.`;
|
|
250
|
+
}
|
|
251
|
+
if (plan.remote.status === 'skipped') {
|
|
252
|
+
return '- Remote fetch skipped. Dry-run used local fallback instructions and did not call DistributionOS APIs.';
|
|
253
|
+
}
|
|
254
|
+
return '- No OAuth or env token found. Dry-run used local fallback instructions and did not call DistributionOS APIs.';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function usageSummary(usage) {
|
|
258
|
+
if (!usage) return [];
|
|
259
|
+
const plan = usage.plan
|
|
260
|
+
? `- Plan: ${usage.plan.name}${usage.plan.priceLabel ? ` (${usage.plan.priceLabel})` : ''}`
|
|
261
|
+
: '- Plan: active account access';
|
|
262
|
+
const monthly = usage.monthly ?? {};
|
|
263
|
+
return [
|
|
264
|
+
plan,
|
|
265
|
+
`- Monthly caps: briefs ${meterText(monthly.brief_handoffs)}, images ${meterText(monthly.image_generations)}, research ${meterText(monthly.fresh_research_runs)}`,
|
|
266
|
+
];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function meterText(value) {
|
|
270
|
+
if (!value) return 'unknown';
|
|
271
|
+
const limit = value.limit < 0 ? 'unlimited' : value.limit;
|
|
272
|
+
return `${value.used ?? 0}/${limit}`;
|
|
273
|
+
}
|
|
249
274
|
|
|
250
275
|
function analyticsSummary(analytics) {
|
|
251
276
|
if (analytics.status === 'enabled') {
|