@distributionos/cli 0.1.1 → 0.1.3
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 +35 -15
- package/src/constants.js +9 -9
- package/src/oauth.js +30 -17
- package/src/plan.js +90 -30
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,
|
|
@@ -325,7 +344,8 @@ function helpText() {
|
|
|
325
344
|
' distributionos verify --app <appId> --url <liveUrl> [--content-id <id>]',
|
|
326
345
|
' distributionos report-implementation --app <appId> --artifact <artifactId> [--url <liveUrl>] [--summary <text>]',
|
|
327
346
|
'',
|
|
328
|
-
'Default mode
|
|
347
|
+
'Default mode reviews the setup plan without changing files. Run again with --apply after the plan looks right.',
|
|
348
|
+
'If the repo has existing uncommitted changes, commit/stash first or use --allow-dirty when you recognize them.',
|
|
329
349
|
'',
|
|
330
350
|
'Authentication:',
|
|
331
351
|
' Happy path uses DistributionOS MCP OAuth and stores app-scoped credentials outside the repo.',
|
package/src/constants.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
|
|
3
|
-
const cliPackageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
4
|
-
|
|
5
|
-
export const CLI_PACKAGE_NAME = '@distributionos/cli';
|
|
6
|
-
export const CLI_LOCAL_VERSION = typeof cliPackageJson.version === 'string' ? cliPackageJson.version : '0.0.0-local';
|
|
7
|
-
export const DEFAULT_API_BASE = 'https://distributionos.dev';
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
const cliPackageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
4
|
+
|
|
5
|
+
export const CLI_PACKAGE_NAME = '@distributionos/cli';
|
|
6
|
+
export const CLI_LOCAL_VERSION = typeof cliPackageJson.version === 'string' ? cliPackageJson.version : '0.0.0-local';
|
|
7
|
+
export const DEFAULT_API_BASE = 'https://distributionos.dev';
|
|
8
8
|
export const MCP_SERVER_URL = 'https://distributionos.dev/api/mcp';
|
|
9
|
-
export const CURRENT_BOOTSTRAP_VERSION = '2026.06.
|
|
10
|
-
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';
|
|
11
11
|
|
|
12
12
|
export const TOKEN_ENV_NAMES = [
|
|
13
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
|
@@ -89,11 +89,11 @@ function sanitizePackageJson(packageJson) {
|
|
|
89
89
|
|
|
90
90
|
export function formatPlanText(plan) {
|
|
91
91
|
const lines = [
|
|
92
|
-
'DistributionOS CLI setup plan',
|
|
93
|
-
`App: ${plan.appId}`,
|
|
94
|
-
plan.mode === 'apply'
|
|
95
|
-
? '
|
|
96
|
-
: '
|
|
92
|
+
'DistributionOS CLI setup plan',
|
|
93
|
+
`App: ${plan.appId}`,
|
|
94
|
+
plan.mode === 'apply'
|
|
95
|
+
? 'Step: apply approved setup'
|
|
96
|
+
: 'Step: review setup plan (no files changed yet)',
|
|
97
97
|
`Package: ${plan.packageName}`,
|
|
98
98
|
`Future command: ${plan.futureCommand}`,
|
|
99
99
|
`Local command: ${plan.localCommand}`,
|
|
@@ -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}`,
|
|
@@ -138,12 +139,47 @@ export function formatPlanText(plan) {
|
|
|
138
139
|
lines.push('', 'Future-compatible route gate config', plan.analytics.configSnippet);
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
if (plan.warnings.length > 0) {
|
|
142
|
-
lines.push('', 'Warnings', ...plan.warnings.map(item => `- ${item}`));
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
142
|
+
if (plan.warnings.length > 0) {
|
|
143
|
+
lines.push('', 'Warnings', ...plan.warnings.map(item => `- ${item}`));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
lines.push(...nextStepLines(plan));
|
|
147
|
+
|
|
148
|
+
return `${lines.join('\n')}\n`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function nextStepLines(plan) {
|
|
152
|
+
if (plan.mode === 'apply') return [];
|
|
153
|
+
|
|
154
|
+
const applyCommand = `${plan.futureCommand} --apply`;
|
|
155
|
+
const lines = [
|
|
156
|
+
'',
|
|
157
|
+
'Next step: install approved setup',
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
if (plan.repo.git.dirty) {
|
|
161
|
+
lines.push(
|
|
162
|
+
'This repo has existing uncommitted changes.',
|
|
163
|
+
'Recommended: commit or stash those changes, then run:',
|
|
164
|
+
` ${applyCommand}`,
|
|
165
|
+
'',
|
|
166
|
+
'If you recognize the existing changes and want DistributionOS to edit only its managed files, run:',
|
|
167
|
+
` ${applyCommand} --allow-dirty`,
|
|
168
|
+
);
|
|
169
|
+
} else {
|
|
170
|
+
lines.push(
|
|
171
|
+
'If the planned changes look right, run:',
|
|
172
|
+
` ${applyCommand}`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
lines.push(
|
|
177
|
+
'',
|
|
178
|
+
'DistributionOS expects to edit only the managed agent instruction block and the supported analytics layout. Review git diff before committing or deploying.',
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
return lines;
|
|
182
|
+
}
|
|
147
183
|
|
|
148
184
|
function buildAnalyticsPlan({ analyticsContract, repo, instructionPackVersion }) {
|
|
149
185
|
const layoutTarget = selectAnalyticsLayoutTarget(repo);
|
|
@@ -209,11 +245,14 @@ function selectAnalyticsLayoutTarget(repo) {
|
|
|
209
245
|
return candidates[0] ?? null;
|
|
210
246
|
}
|
|
211
247
|
|
|
212
|
-
function buildWarnings({ repo, remote }) {
|
|
213
|
-
const warnings = [];
|
|
214
|
-
if (
|
|
215
|
-
warnings.push('
|
|
216
|
-
}
|
|
248
|
+
function buildWarnings({ repo, remote }) {
|
|
249
|
+
const warnings = [];
|
|
250
|
+
if (remote.access?.reason === 'subscription_required') {
|
|
251
|
+
warnings.push('Active Builder or Distributor subscription required before applying setup.');
|
|
252
|
+
}
|
|
253
|
+
if (repo.git.dirty) {
|
|
254
|
+
warnings.push('Worktree has existing changes. Keep baseline failures separate before applying edits.');
|
|
255
|
+
}
|
|
217
256
|
if (!remote.instructions) {
|
|
218
257
|
warnings.push('Live DistributionOS instructions were not fetched. Connect MCP OAuth or provide a safe env token before applying app-specific setup.');
|
|
219
258
|
}
|
|
@@ -237,15 +276,36 @@ function buildValidationPlan(repo) {
|
|
|
237
276
|
};
|
|
238
277
|
}
|
|
239
278
|
|
|
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
|
-
|
|
279
|
+
function remoteSummary(plan) {
|
|
280
|
+
if (plan.remote.status === 'fetched') {
|
|
281
|
+
return `- Fetched live instructions and analytics contract using ${plan.remote.tokenName}.`;
|
|
282
|
+
}
|
|
283
|
+
if (plan.remote.status === 'partial') {
|
|
284
|
+
return `- Used ${plan.remote.tokenName}, but one or more DistributionOS requests failed.`;
|
|
285
|
+
}
|
|
286
|
+
if (plan.remote.status === 'skipped') {
|
|
287
|
+
return '- Remote fetch skipped. The setup plan used local fallback instructions and did not call DistributionOS APIs.';
|
|
288
|
+
}
|
|
289
|
+
return '- No OAuth or env token found. The setup plan used local fallback instructions and did not call DistributionOS APIs.';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function usageSummary(usage) {
|
|
293
|
+
if (!usage) return [];
|
|
294
|
+
const plan = usage.plan
|
|
295
|
+
? `- Plan: ${usage.plan.name}${usage.plan.priceLabel ? ` (${usage.plan.priceLabel})` : ''}`
|
|
296
|
+
: '- Plan: active account access';
|
|
297
|
+
const monthly = usage.monthly ?? {};
|
|
298
|
+
return [
|
|
299
|
+
plan,
|
|
300
|
+
`- Monthly caps: briefs ${meterText(monthly.brief_handoffs)}, images ${meterText(monthly.image_generations)}, research ${meterText(monthly.fresh_research_runs)}`,
|
|
301
|
+
];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function meterText(value) {
|
|
305
|
+
if (!value) return 'unknown';
|
|
306
|
+
const limit = value.limit < 0 ? 'unlimited' : value.limit;
|
|
307
|
+
return `${value.used ?? 0}/${limit}`;
|
|
308
|
+
}
|
|
249
309
|
|
|
250
310
|
function analyticsSummary(analytics) {
|
|
251
311
|
if (analytics.status === 'enabled') {
|