@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/README.md +38 -0
- package/bin/distributionos.js +14 -0
- package/package.json +27 -0
- package/src/api.js +209 -0
- package/src/auth-store.js +52 -0
- package/src/bootstrap.js +107 -0
- package/src/cli.js +487 -0
- package/src/constants.js +60 -0
- package/src/detect.js +279 -0
- package/src/initialization.js +148 -0
- package/src/mutate.js +193 -0
- package/src/oauth.js +238 -0
- package/src/plan.js +265 -0
- package/src/privacy.js +62 -0
- package/src/validation.js +259 -0
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# DistributionOS CLI
|
|
2
|
+
|
|
3
|
+
Local installer for connecting an app repo to DistributionOS.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx @distributionos/cli setup --app <appId>
|
|
7
|
+
```
|
|
8
|
+
|
|
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
|
+
|
|
11
|
+
Before the package is published, test the local package from another repo with:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm exec --yes --package "C:\Users\lawre\DistributionOS\packages\cli" -- distributionos setup --app <appId> --api-base http://localhost:3005
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
distributionos setup --app <appId>
|
|
21
|
+
distributionos setup --app <appId> --apply
|
|
22
|
+
distributionos login --app <appId>
|
|
23
|
+
distributionos verify --app <appId> --url <liveUrl> [--content-id <id>]
|
|
24
|
+
distributionos report-implementation --app <appId> --artifact <artifactId> [--url <liveUrl>]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Safety
|
|
28
|
+
|
|
29
|
+
- Uses DistributionOS MCP OAuth as the happy path.
|
|
30
|
+
- Stores OAuth credentials outside the app repo.
|
|
31
|
+
- Accepts API-key environment variables only as an advanced fallback.
|
|
32
|
+
- Does not write secrets into committed files.
|
|
33
|
+
- Scans repo structure without reading `.env` or credential-looking files.
|
|
34
|
+
- Redacts secret-looking validation output before printing or serializing results.
|
|
35
|
+
- Mutates only the managed DistributionOS bootstrap block and supported analytics install.
|
|
36
|
+
- Auto-installs analytics for Next App Router layouts and Vite/CRA-style `index.html` entrypoints.
|
|
37
|
+
- Refuses dirty worktrees unless `--allow-dirty` is explicit.
|
|
38
|
+
- Does not commit, push, or deploy.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { runCli } from '../src/cli.js';
|
|
4
|
+
|
|
5
|
+
const code = await runCli(process.argv, {
|
|
6
|
+
cwd: process.cwd(),
|
|
7
|
+
env: process.env,
|
|
8
|
+
stdin: process.stdin,
|
|
9
|
+
stdout: process.stdout,
|
|
10
|
+
stderr: process.stderr,
|
|
11
|
+
fetch: globalThis.fetch,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
process.exitCode = code;
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@distributionos/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DistributionOS repo setup CLI for agent onboarding and first-party analytics.",
|
|
5
|
+
"homepage": "https://distributionos.dev",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/lawfan1026/DistributionOS.git",
|
|
9
|
+
"directory": "packages/cli"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"files": [
|
|
16
|
+
"bin",
|
|
17
|
+
"src",
|
|
18
|
+
"!src/**/*.test.js"
|
|
19
|
+
],
|
|
20
|
+
"bin": {
|
|
21
|
+
"distributionos": "bin/distributionos.js",
|
|
22
|
+
"dos": "bin/distributionos.js"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18.18"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
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 function resolveEnvToken(env) {
|
|
6
|
+
for (const name of TOKEN_ENV_NAMES) {
|
|
7
|
+
const value = env?.[name]?.trim();
|
|
8
|
+
if (value) return { source: 'env', name, value };
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function resolveAuthToken({ env, apiBase, appId, fetchImpl }) {
|
|
14
|
+
const envToken = resolveEnvToken(env);
|
|
15
|
+
if (envToken) return envToken;
|
|
16
|
+
|
|
17
|
+
const credential = await getStoredCredential({ apiBase, appId, env });
|
|
18
|
+
if (!credential) return null;
|
|
19
|
+
if (isCredentialFresh(credential)) {
|
|
20
|
+
return { source: 'oauth', name: 'stored OAuth token', value: credential.accessToken };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const refreshed = await refreshOAuthCredential({
|
|
24
|
+
credential,
|
|
25
|
+
apiBase,
|
|
26
|
+
appId,
|
|
27
|
+
fetchImpl,
|
|
28
|
+
env,
|
|
29
|
+
}).catch(() => null);
|
|
30
|
+
|
|
31
|
+
return refreshed?.accessToken
|
|
32
|
+
? { source: 'oauth', name: 'refreshed OAuth token', value: refreshed.accessToken }
|
|
33
|
+
: null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function fetchDistributionOsSetupContext(input) {
|
|
37
|
+
const token = await resolveAuthToken({
|
|
38
|
+
env: input.env,
|
|
39
|
+
apiBase: input.apiBase ?? DEFAULT_API_BASE,
|
|
40
|
+
appId: input.appId,
|
|
41
|
+
fetchImpl: input.fetch,
|
|
42
|
+
});
|
|
43
|
+
if (!token || input.noFetch) {
|
|
44
|
+
return {
|
|
45
|
+
status: 'not_authenticated',
|
|
46
|
+
tokenName: token?.name ?? null,
|
|
47
|
+
instructions: null,
|
|
48
|
+
analyticsContract: null,
|
|
49
|
+
errors: [],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const apiBase = normalizeApiBase(input.apiBase ?? DEFAULT_API_BASE);
|
|
54
|
+
const errors = [];
|
|
55
|
+
const [instructions, analyticsContract] = await Promise.all([
|
|
56
|
+
fetchJson({
|
|
57
|
+
fetchImpl: input.fetch,
|
|
58
|
+
apiBase,
|
|
59
|
+
token: token.value,
|
|
60
|
+
path: `/api/v1/apps/${encodeURIComponent(input.appId)}/agent/instructions`,
|
|
61
|
+
}).catch(error => {
|
|
62
|
+
errors.push(`Agent instructions fetch failed: ${error.message}`);
|
|
63
|
+
return null;
|
|
64
|
+
}),
|
|
65
|
+
fetchJson({
|
|
66
|
+
fetchImpl: input.fetch,
|
|
67
|
+
apiBase,
|
|
68
|
+
token: token.value,
|
|
69
|
+
path: `/api/v1/apps/${encodeURIComponent(input.appId)}/analytics/contract`,
|
|
70
|
+
}).catch(error => {
|
|
71
|
+
errors.push(`Analytics contract fetch failed: ${error.message}`);
|
|
72
|
+
return null;
|
|
73
|
+
}),
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
status: errors.length === 0 ? 'fetched' : 'partial',
|
|
78
|
+
tokenName: token.name,
|
|
79
|
+
instructions,
|
|
80
|
+
analyticsContract,
|
|
81
|
+
errors,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function ensureAnalyticsSite(input) {
|
|
86
|
+
const token = await resolveAuthToken({
|
|
87
|
+
env: input.env,
|
|
88
|
+
apiBase: input.apiBase ?? DEFAULT_API_BASE,
|
|
89
|
+
appId: input.appId,
|
|
90
|
+
fetchImpl: input.fetch,
|
|
91
|
+
});
|
|
92
|
+
if (!token) {
|
|
93
|
+
return {
|
|
94
|
+
status: 'not_authenticated',
|
|
95
|
+
setup: null,
|
|
96
|
+
error: 'No stored OAuth token or env token was available.',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const apiBase = normalizeApiBase(input.apiBase ?? DEFAULT_API_BASE);
|
|
101
|
+
const response = await input.fetch(`${apiBase}/api/v1/apps/${encodeURIComponent(input.appId)}/analytics/sites`, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: {
|
|
104
|
+
Authorization: `Bearer ${token.value}`,
|
|
105
|
+
'Content-Type': 'application/json',
|
|
106
|
+
Accept: 'application/json',
|
|
107
|
+
},
|
|
108
|
+
body: JSON.stringify({
|
|
109
|
+
...(input.domain ? { domain: input.domain } : {}),
|
|
110
|
+
ownerVisitExclusionEnabled: input.ownerVisitExclusionEnabled,
|
|
111
|
+
}),
|
|
112
|
+
});
|
|
113
|
+
const body = await response.json().catch(() => null);
|
|
114
|
+
if (!response.ok || body?.success === false) {
|
|
115
|
+
return {
|
|
116
|
+
status: 'failed',
|
|
117
|
+
setup: null,
|
|
118
|
+
error: body?.error || `HTTP ${response.status}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
status: 'ready',
|
|
124
|
+
setup: body?.data ?? body,
|
|
125
|
+
error: null,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function verifyAnalyticsInstall(input) {
|
|
130
|
+
const token = await resolveAuthToken({
|
|
131
|
+
env: input.env,
|
|
132
|
+
apiBase: input.apiBase ?? DEFAULT_API_BASE,
|
|
133
|
+
appId: input.appId,
|
|
134
|
+
fetchImpl: input.fetch,
|
|
135
|
+
});
|
|
136
|
+
if (!token) {
|
|
137
|
+
throw new Error('No stored OAuth token or env token was available.');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const apiBase = normalizeApiBase(input.apiBase ?? DEFAULT_API_BASE);
|
|
141
|
+
const response = await input.fetch(`${apiBase}/api/v1/apps/${encodeURIComponent(input.appId)}/analytics/verify`, {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
headers: {
|
|
144
|
+
Authorization: `Bearer ${token.value}`,
|
|
145
|
+
'Content-Type': 'application/json',
|
|
146
|
+
Accept: 'application/json',
|
|
147
|
+
},
|
|
148
|
+
body: JSON.stringify({
|
|
149
|
+
url: input.url,
|
|
150
|
+
...(input.expectedContentId ? { expectedContentId: input.expectedContentId } : {}),
|
|
151
|
+
...(input.contractVersion ? { contractVersion: input.contractVersion } : {}),
|
|
152
|
+
}),
|
|
153
|
+
});
|
|
154
|
+
const body = await response.json().catch(() => null);
|
|
155
|
+
if (!response.ok || body?.success === false) {
|
|
156
|
+
throw new Error(body?.error || `Analytics verification failed with HTTP ${response.status}`);
|
|
157
|
+
}
|
|
158
|
+
return body?.data ?? body;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function reportImplementation(input) {
|
|
162
|
+
const token = await resolveAuthToken({
|
|
163
|
+
env: input.env,
|
|
164
|
+
apiBase: input.apiBase ?? DEFAULT_API_BASE,
|
|
165
|
+
appId: input.appId,
|
|
166
|
+
fetchImpl: input.fetch,
|
|
167
|
+
});
|
|
168
|
+
if (!token) {
|
|
169
|
+
throw new Error('No stored OAuth token or env token was available.');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const apiBase = normalizeApiBase(input.apiBase ?? DEFAULT_API_BASE);
|
|
173
|
+
const response = await input.fetch(`${apiBase}/api/v1/apps/${encodeURIComponent(input.appId)}/artifacts/${encodeURIComponent(input.artifactId)}/report-implementation`, {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers: {
|
|
176
|
+
Authorization: `Bearer ${token.value}`,
|
|
177
|
+
'Content-Type': 'application/json',
|
|
178
|
+
Accept: 'application/json',
|
|
179
|
+
},
|
|
180
|
+
body: JSON.stringify(input.payload),
|
|
181
|
+
});
|
|
182
|
+
const body = await response.json().catch(() => null);
|
|
183
|
+
if (!response.ok || body?.success === false) {
|
|
184
|
+
throw new Error(body?.error || `Implementation report failed with HTTP ${response.status}`);
|
|
185
|
+
}
|
|
186
|
+
return body?.data ?? body;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function fetchJson({ fetchImpl, apiBase, token, path }) {
|
|
190
|
+
if (typeof fetchImpl !== 'function') {
|
|
191
|
+
throw new Error('fetch is unavailable in this Node runtime');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const response = await fetchImpl(`${apiBase}${path}`, {
|
|
195
|
+
headers: {
|
|
196
|
+
Authorization: `Bearer ${token}`,
|
|
197
|
+
Accept: 'application/json',
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
const body = await response.json().catch(() => null);
|
|
201
|
+
if (!response.ok || body?.success === false) {
|
|
202
|
+
throw new Error(body?.error || `HTTP ${response.status}`);
|
|
203
|
+
}
|
|
204
|
+
return body?.data ?? body;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function normalizeApiBase(value) {
|
|
208
|
+
return String(value || DEFAULT_API_BASE).replace(/\/+$/, '');
|
|
209
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = '.distributionos';
|
|
6
|
+
const CONFIG_FILE = 'config.json';
|
|
7
|
+
|
|
8
|
+
export function getConfigPath(env = process.env) {
|
|
9
|
+
if (env.DISTRIBUTIONOS_CONFIG) return env.DISTRIBUTIONOS_CONFIG;
|
|
10
|
+
return path.join(os.homedir(), CONFIG_DIR, CONFIG_FILE);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function loadConfig(env = process.env) {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(await fs.readFile(getConfigPath(env), 'utf8'));
|
|
16
|
+
} catch {
|
|
17
|
+
return { credentials: {} };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function saveConfig(config, env = process.env) {
|
|
22
|
+
const configPath = getConfigPath(env);
|
|
23
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
24
|
+
await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function getStoredCredential({ apiBase, appId, env }) {
|
|
28
|
+
const config = await loadConfig(env);
|
|
29
|
+
return config.credentials?.[credentialKey(apiBase, appId)] ?? null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function storeCredential({ apiBase, appId, credential, env }) {
|
|
33
|
+
const config = await loadConfig(env);
|
|
34
|
+
config.credentials = config.credentials ?? {};
|
|
35
|
+
config.credentials[credentialKey(apiBase, appId)] = {
|
|
36
|
+
...credential,
|
|
37
|
+
apiBase,
|
|
38
|
+
appId,
|
|
39
|
+
updatedAt: new Date().toISOString(),
|
|
40
|
+
};
|
|
41
|
+
await saveConfig(config, env);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function removeCredential({ apiBase, appId, env }) {
|
|
45
|
+
const config = await loadConfig(env);
|
|
46
|
+
if (config.credentials) delete config.credentials[credentialKey(apiBase, appId)];
|
|
47
|
+
await saveConfig(config, env);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function credentialKey(apiBase, appId) {
|
|
51
|
+
return `${String(apiBase).replace(/\/+$/, '')}|${appId}`;
|
|
52
|
+
}
|
package/src/bootstrap.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CLI_PACKAGE_NAME,
|
|
3
|
+
CURRENT_BOOTSTRAP_VERSION,
|
|
4
|
+
MCP_SERVER_URL,
|
|
5
|
+
} from './constants.js';
|
|
6
|
+
|
|
7
|
+
const BOOTSTRAP_BLOCK_RE =
|
|
8
|
+
/<!--\s*DISTRIBUTIONOS:START[\s\S]*?DISTRIBUTIONOS:END\s*-->/i;
|
|
9
|
+
|
|
10
|
+
export function buildSetupCommand(appId, packageName = CLI_PACKAGE_NAME) {
|
|
11
|
+
return `npx ${packageName} setup --app ${shellToken(appId)}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildLocalBootstrapBlock(version = CURRENT_BOOTSTRAP_VERSION, appId) {
|
|
15
|
+
const appIdAttribute = appId ? ` appId=${appId}` : '';
|
|
16
|
+
const instructionCall = appId
|
|
17
|
+
? `distributionos.get_agent_instructions({ appId: '${appId}', bootstrapVersion: '${version}' })`
|
|
18
|
+
: `distributionos.get_agent_instructions({ bootstrapVersion: '${version}' })`;
|
|
19
|
+
|
|
20
|
+
return [
|
|
21
|
+
`<!-- DISTRIBUTIONOS:START version=${version}${appIdAttribute} -->`,
|
|
22
|
+
'## DistributionOS Agent Sync',
|
|
23
|
+
'',
|
|
24
|
+
'This repo is linked to DistributionOS.',
|
|
25
|
+
'',
|
|
26
|
+
'Hard preflight: before editing files, creating assets, generating images, or changing copy for SEO, blog, launch, content, visual asset, competitor, social, email, or growth work, fetch the current instructions from DistributionOS:',
|
|
27
|
+
'',
|
|
28
|
+
instructionCall,
|
|
29
|
+
'',
|
|
30
|
+
'This preflight applies even when the user does not mention DistributionOS by name. Do not start from local repo files alone for those tasks.',
|
|
31
|
+
'',
|
|
32
|
+
'For existing blog posts or DistributionOS artifacts, fetch the artifact/context first. Use DistributionOS image/media workflows before local image tools.',
|
|
33
|
+
'',
|
|
34
|
+
'When you create a plan, strategy, roadmap, campaign, content ideas, keyword priorities, or next-step recommendations, save it to DistributionOS as proposed work for review before executing.',
|
|
35
|
+
'',
|
|
36
|
+
'When you publish, ship, schedule, or implement a DistributionOS artifact, report the implementation back to DistributionOS before your final response.',
|
|
37
|
+
'',
|
|
38
|
+
'If analytics is enabled, public blog posts, landing pages, CTAs, email links, social links, launch links, paid links, and campaigns must use the DistributionOS analytics contract by default: call get_analytics_install_instructions, add page/link markers, use create_tracked_link for campaign links, verify_analytics_install after deploy, or report a specific analyticsOptOutReason.',
|
|
39
|
+
'',
|
|
40
|
+
'For SEO blog/page publishes, verify 200, canonical, sitemap inclusion, robots/indexability, one H1, meta description, and schema before reporting.',
|
|
41
|
+
'',
|
|
42
|
+
'Never send secrets, env vars, API keys, private customer data, raw logs, or internal-only implementation details.',
|
|
43
|
+
'<!-- DISTRIBUTIONOS:END -->',
|
|
44
|
+
].join('\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getBootstrapAction(instructionFiles, recommendedBlock) {
|
|
48
|
+
const existingWithBlock = instructionFiles.find((file) =>
|
|
49
|
+
BOOTSTRAP_BLOCK_RE.test(file.content ?? ''),
|
|
50
|
+
);
|
|
51
|
+
const preferred = existingWithBlock ?? instructionFiles[0] ?? {
|
|
52
|
+
path: 'AGENTS.md',
|
|
53
|
+
exists: false,
|
|
54
|
+
content: '',
|
|
55
|
+
priority: 100,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (!preferred.exists) {
|
|
59
|
+
return {
|
|
60
|
+
action: 'create',
|
|
61
|
+
target: preferred.path,
|
|
62
|
+
reason: 'No agent instruction file was found. AGENTS.md is the safest cross-agent default.',
|
|
63
|
+
recommendedBlock,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const currentBlock = preferred.content.match(BOOTSTRAP_BLOCK_RE)?.[0] ?? null;
|
|
68
|
+
if (!currentBlock) {
|
|
69
|
+
return {
|
|
70
|
+
action: 'append',
|
|
71
|
+
target: preferred.path,
|
|
72
|
+
reason: 'Agent instruction file exists but does not contain the managed DistributionOS block.',
|
|
73
|
+
recommendedBlock,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (currentBlock.trim() === recommendedBlock.trim()) {
|
|
78
|
+
return {
|
|
79
|
+
action: 'none',
|
|
80
|
+
target: preferred.path,
|
|
81
|
+
reason: 'Managed DistributionOS bootstrap block is already current.',
|
|
82
|
+
recommendedBlock,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
action: 'update',
|
|
88
|
+
target: preferred.path,
|
|
89
|
+
reason: 'Managed DistributionOS bootstrap block exists but differs from the current recommendation.',
|
|
90
|
+
recommendedBlock,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function buildFallbackMcpInstructions(appId) {
|
|
95
|
+
return [
|
|
96
|
+
`App ID: ${appId}`,
|
|
97
|
+
`DistributionOS MCP server: ${MCP_SERVER_URL}`,
|
|
98
|
+
'Use OAuth sign-in when prompted, then call get_agent_instructions and submit_initialization_context.',
|
|
99
|
+
'If OAuth is unavailable, create an app-scoped write API key in DistributionOS and expose it only as DISTRIBUTIONOS_API_KEY for the command invocation.',
|
|
100
|
+
'Never write the API key into AGENTS.md, CLAUDE.md, README, committed MCP config, or any tracked file.',
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function shellToken(value) {
|
|
105
|
+
if (/^[a-zA-Z0-9._:-]+$/.test(value)) return value;
|
|
106
|
+
return JSON.stringify(value);
|
|
107
|
+
}
|