@odla-ai/cli 0.4.0 → 0.6.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 +126 -31
- package/REQUIREMENTS.md +18 -3
- package/dist/bin.cjs +1340 -436
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/chunk-OERLHVLH.js +2107 -0
- package/dist/chunk-OERLHVLH.js.map +1 -0
- package/dist/index.cjs +1353 -435
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +314 -21
- package/dist/index.d.ts +314 -21
- package/dist/index.js +15 -1
- package/llms.txt +442 -54
- package/package.json +15 -6
- package/skills/odla/SKILL.md +43 -12
- package/skills/odla/references/build.md +65 -17
- package/skills/odla/references/sdks.md +36 -3
- package/skills/odla-migrate/SKILL.md +38 -13
- package/skills/odla-migrate/references/phase-0-preflight.md +0 -2
- package/skills/odla-migrate/references/phase-1-static.md +2 -4
- package/skills/odla-migrate/references/phase-2-db.md +25 -17
- package/skills/odla-migrate/references/phase-3-auth.md +106 -20
- package/skills/odla-migrate/references/phase-3b-user-sync.md +57 -0
- package/skills/odla-migrate/references/phase-4-ai.md +6 -5
- package/skills/odla-migrate/references/phase-5-cutover.md +47 -16
- package/skills/odla-migrate/references/secrets-map.md +26 -11
- package/skills/odla-migrate/references/troubleshooting.md +27 -13
- package/skills/odla-o11y-debug/SKILL.md +93 -0
- package/dist/chunk-UWT7C6VT.js +0 -1187
- package/dist/chunk-UWT7C6VT.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -31,12 +31,19 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
// src/index.ts
|
|
32
32
|
var index_exports = {};
|
|
33
33
|
__export(index_exports, {
|
|
34
|
+
AGENT_HARNESSES: () => AGENT_HARNESSES,
|
|
35
|
+
CAPABILITIES: () => CAPABILITIES,
|
|
36
|
+
SYSTEM_AI_PURPOSES: () => SYSTEM_AI_PURPOSES,
|
|
37
|
+
adminAi: () => adminAi,
|
|
34
38
|
doctor: () => doctor,
|
|
39
|
+
getScopedPlatformToken: () => getScopedPlatformToken,
|
|
35
40
|
initProject: () => initProject,
|
|
36
41
|
installSkill: () => installSkill,
|
|
42
|
+
printCapabilities: () => printCapabilities,
|
|
37
43
|
provision: () => provision,
|
|
38
44
|
redactSecrets: () => redactSecrets,
|
|
39
45
|
runCli: () => runCli,
|
|
46
|
+
runHostedSecurity: () => runHostedSecurity,
|
|
40
47
|
secretsPush: () => secretsPush,
|
|
41
48
|
smoke: () => smoke
|
|
42
49
|
});
|
|
@@ -47,31 +54,529 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
|
|
|
47
54
|
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
48
55
|
|
|
49
56
|
// src/cli.ts
|
|
50
|
-
var
|
|
57
|
+
var import_security2 = require("@odla-ai/security");
|
|
51
58
|
|
|
52
|
-
// src/
|
|
59
|
+
// src/admin-ai.ts
|
|
60
|
+
var import_node_process3 = __toESM(require("process"), 1);
|
|
61
|
+
var import_node_fs2 = require("fs");
|
|
62
|
+
var import_db2 = require("@odla-ai/db");
|
|
63
|
+
|
|
64
|
+
// src/open.ts
|
|
65
|
+
var import_node_child_process = require("child_process");
|
|
66
|
+
var import_node_process = __toESM(require("process"), 1);
|
|
67
|
+
async function openUrl(url, options = {}) {
|
|
68
|
+
const command = openerFor(options.platform ?? import_node_process.default.platform);
|
|
69
|
+
const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
|
|
70
|
+
await new Promise((resolve7, reject) => {
|
|
71
|
+
const child = doSpawn(command.cmd, [...command.args, url], {
|
|
72
|
+
stdio: "ignore",
|
|
73
|
+
detached: true
|
|
74
|
+
});
|
|
75
|
+
child.once("error", reject);
|
|
76
|
+
child.once("spawn", () => {
|
|
77
|
+
child.unref();
|
|
78
|
+
resolve7();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function openerFor(platform) {
|
|
83
|
+
if (platform === "darwin") return { cmd: "open", args: [] };
|
|
84
|
+
if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
|
|
85
|
+
return { cmd: "xdg-open", args: [] };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/token.ts
|
|
89
|
+
var import_db = require("@odla-ai/db");
|
|
90
|
+
var import_node_process2 = __toESM(require("process"), 1);
|
|
91
|
+
|
|
92
|
+
// src/local.ts
|
|
53
93
|
var import_node_fs = require("fs");
|
|
54
94
|
var import_node_path = require("path");
|
|
95
|
+
var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
|
|
96
|
+
function readJsonFile(path) {
|
|
97
|
+
try {
|
|
98
|
+
return JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function writePrivateJson(path, value) {
|
|
104
|
+
writePrivateText(path, `${JSON.stringify(value, null, 2)}
|
|
105
|
+
`);
|
|
106
|
+
}
|
|
107
|
+
function readCredentials(path) {
|
|
108
|
+
if (!(0, import_node_fs.existsSync)(path)) return null;
|
|
109
|
+
let value;
|
|
110
|
+
try {
|
|
111
|
+
value = JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
|
|
112
|
+
} catch {
|
|
113
|
+
throw new Error(`credentials file ${path} is not valid JSON; fix or remove it before provisioning`);
|
|
114
|
+
}
|
|
115
|
+
if (!value || typeof value !== "object" || typeof value.appId !== "string" || !value.envs || typeof value.envs !== "object") {
|
|
116
|
+
throw new Error(`credentials file ${path} has an invalid shape; fix or remove it before provisioning`);
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
function mergeCredential(current, update) {
|
|
121
|
+
const next = current ?? {
|
|
122
|
+
appId: update.appId,
|
|
123
|
+
platformUrl: update.platformUrl,
|
|
124
|
+
dbEndpoint: update.dbEndpoint,
|
|
125
|
+
updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
126
|
+
envs: {}
|
|
127
|
+
};
|
|
128
|
+
next.appId = update.appId;
|
|
129
|
+
next.platformUrl = update.platformUrl;
|
|
130
|
+
next.dbEndpoint = update.dbEndpoint;
|
|
131
|
+
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
132
|
+
next.envs[update.env] = {
|
|
133
|
+
...next.envs[update.env] ?? {},
|
|
134
|
+
tenantId: update.tenantId,
|
|
135
|
+
...update.dbKey ? { dbKey: update.dbKey } : {},
|
|
136
|
+
...update.o11yToken ? { o11yToken: update.o11yToken } : {}
|
|
137
|
+
};
|
|
138
|
+
return next;
|
|
139
|
+
}
|
|
140
|
+
function ensureGitignore(rootDir, localPaths = []) {
|
|
141
|
+
const path = (0, import_node_path.resolve)(rootDir, ".gitignore");
|
|
142
|
+
const existing = (0, import_node_fs.existsSync)(path) ? (0, import_node_fs.readFileSync)(path, "utf8") : "";
|
|
143
|
+
const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line) => !!line);
|
|
144
|
+
const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
|
|
145
|
+
const missing = wanted.filter((line) => !existing.split(/\r?\n/).includes(line));
|
|
146
|
+
if (missing.length === 0) return;
|
|
147
|
+
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
148
|
+
(0, import_node_fs.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
|
|
149
|
+
`);
|
|
150
|
+
}
|
|
151
|
+
function o11yDevVars(cfg) {
|
|
152
|
+
if (!cfg.services.includes("o11y")) return void 0;
|
|
153
|
+
return {
|
|
154
|
+
endpoint: cfg.o11y?.endpoint ?? "https://o11y.odla.ai",
|
|
155
|
+
service: cfg.o11y?.service ?? cfg.app.id,
|
|
156
|
+
...cfg.o11y?.version ? { version: cfg.o11y.version } : {}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function resolveWriteDevVarsTarget(cfg, requested) {
|
|
160
|
+
if (!requested) return null;
|
|
161
|
+
if (requested === true) return cfg.local.devVarsFile;
|
|
162
|
+
return (0, import_node_path.resolve)((0, import_node_path.dirname)(cfg.configPath), requested);
|
|
163
|
+
}
|
|
164
|
+
function writeDevVars(path, credentials, env, o11y) {
|
|
165
|
+
const entry = credentials.envs[env];
|
|
166
|
+
if (!entry) throw new Error(`no credentials for env "${env}" in ${path}`);
|
|
167
|
+
const lines = [`ODLA_PLATFORM="${credentials.platformUrl}"`];
|
|
168
|
+
if (entry.dbKey) lines.push(`ODLA_ENDPOINT="${credentials.dbEndpoint}"`);
|
|
169
|
+
lines.push(`ODLA_APP_ID="${credentials.appId}"`, `ODLA_ENV="${env}"`, `ODLA_TENANT="${entry.tenantId}"`);
|
|
170
|
+
if (entry.dbKey) lines.push(`ODLA_API_KEY="${entry.dbKey}"`);
|
|
171
|
+
if (o11y) {
|
|
172
|
+
lines.push(`ODLA_O11Y_ENDPOINT="${o11y.endpoint}"`, `ODLA_O11Y_SERVICE="${o11y.service}"`);
|
|
173
|
+
if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
|
|
174
|
+
if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
|
|
175
|
+
}
|
|
176
|
+
const existing = (0, import_node_fs.existsSync)(path) ? (0, import_node_fs.readFileSync)(path, "utf8") : "";
|
|
177
|
+
const retained = existing.split(/\r?\n/).filter((line) => !isManagedDevVar(line));
|
|
178
|
+
while (retained.at(-1) === "") retained.pop();
|
|
179
|
+
const prefix = retained.length ? `${retained.join("\n")}
|
|
180
|
+
|
|
181
|
+
` : "";
|
|
182
|
+
writePrivateText(path, `${prefix}${lines.join("\n")}
|
|
183
|
+
`);
|
|
184
|
+
}
|
|
185
|
+
var MANAGED_DEV_VARS = /* @__PURE__ */ new Set([
|
|
186
|
+
"ODLA_PLATFORM",
|
|
187
|
+
"ODLA_ENDPOINT",
|
|
188
|
+
"ODLA_APP_ID",
|
|
189
|
+
"ODLA_ENV",
|
|
190
|
+
"ODLA_TENANT",
|
|
191
|
+
"ODLA_API_KEY",
|
|
192
|
+
"ODLA_O11Y_ENDPOINT",
|
|
193
|
+
"ODLA_O11Y_SERVICE",
|
|
194
|
+
"ODLA_O11Y_VERSION",
|
|
195
|
+
"ODLA_O11Y_TOKEN"
|
|
196
|
+
]);
|
|
197
|
+
function isManagedDevVar(line) {
|
|
198
|
+
const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
|
|
199
|
+
return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
|
|
200
|
+
}
|
|
201
|
+
function writePrivateText(path, text) {
|
|
202
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
|
|
203
|
+
const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
204
|
+
(0, import_node_fs.writeFileSync)(temporary, text, { mode: 384 });
|
|
205
|
+
(0, import_node_fs.chmodSync)(temporary, 384);
|
|
206
|
+
(0, import_node_fs.renameSync)(temporary, path);
|
|
207
|
+
}
|
|
208
|
+
function gitignoreEntry(rootDir, path) {
|
|
209
|
+
const rel = (0, import_node_path.relative)((0, import_node_path.resolve)(rootDir), (0, import_node_path.resolve)(path));
|
|
210
|
+
if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path.isAbsolute)(rel)) return null;
|
|
211
|
+
return rel.replaceAll("\\", "/");
|
|
212
|
+
}
|
|
213
|
+
function displayPath(path, rootDir = process.cwd()) {
|
|
214
|
+
const rel = (0, import_node_path.relative)(rootDir, path);
|
|
215
|
+
return rel && !rel.startsWith("..") ? rel : path;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/token.ts
|
|
219
|
+
async function getDeveloperToken(cfg, options, doFetch, out) {
|
|
220
|
+
if (options.token) return options.token;
|
|
221
|
+
if (import_node_process2.default.env.ODLA_DEV_TOKEN) return import_node_process2.default.env.ODLA_DEV_TOKEN;
|
|
222
|
+
const cached = readJsonFile(cfg.local.tokenFile);
|
|
223
|
+
if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
224
|
+
out.log(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
225
|
+
return cached.token;
|
|
226
|
+
}
|
|
227
|
+
const browser = approvalBrowser(options);
|
|
228
|
+
const { token, expiresAt } = await (0, import_db.requestToken)({
|
|
229
|
+
endpoint: cfg.platformUrl,
|
|
230
|
+
label: `${cfg.app.id} provisioner`,
|
|
231
|
+
fetch: doFetch,
|
|
232
|
+
onCode: async ({ userCode, expiresIn }) => {
|
|
233
|
+
const approvalUrl = handshakeUrl(cfg.platformUrl, userCode);
|
|
234
|
+
out.log("");
|
|
235
|
+
out.log(`Approve code ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
|
|
236
|
+
if (browser.open) {
|
|
237
|
+
try {
|
|
238
|
+
await (options.openApprovalUrl ?? openUrl)(approvalUrl);
|
|
239
|
+
out.log(`auth: opened browser for approval${browser.mode === "auto" ? " (auto)" : ""}`);
|
|
240
|
+
} catch (err) {
|
|
241
|
+
out.log(`auth: could not open browser (${err instanceof Error ? err.message : String(err)})`);
|
|
242
|
+
}
|
|
243
|
+
} else if (browser.reason) {
|
|
244
|
+
out.log(`auth: browser launch skipped (${browser.reason})`);
|
|
245
|
+
}
|
|
246
|
+
out.log("");
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
writePrivateJson(cfg.local.tokenFile, { token, expiresAt });
|
|
250
|
+
out.log(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
251
|
+
return token;
|
|
252
|
+
}
|
|
253
|
+
function approvalBrowser(options) {
|
|
254
|
+
if (options.open === true) return { open: true, mode: "forced" };
|
|
255
|
+
if (options.open === false) return { open: false, reason: "disabled by --no-open" };
|
|
256
|
+
if (import_node_process2.default.env.CI) return { open: false, reason: "CI environment" };
|
|
257
|
+
const interactive = options.interactive ?? Boolean(import_node_process2.default.stdin.isTTY && import_node_process2.default.stdout.isTTY);
|
|
258
|
+
if (!interactive) return { open: false, reason: "non-interactive shell; pass --open to force" };
|
|
259
|
+
return { open: true, mode: "auto" };
|
|
260
|
+
}
|
|
261
|
+
function handshakeUrl(platformUrl, userCode) {
|
|
262
|
+
const url = new URL("/studio", platformUrl);
|
|
263
|
+
url.searchParams.set("code", userCode);
|
|
264
|
+
return url.toString();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/admin-ai.ts
|
|
268
|
+
var SYSTEM_AI_PURPOSES = ["o11y.triage", "security.discovery", "security.validation"];
|
|
269
|
+
async function getScopedPlatformToken(options) {
|
|
270
|
+
const fromEnv = import_node_process3.default.env.ODLA_ADMIN_TOKEN;
|
|
271
|
+
if (fromEnv) return fromEnv;
|
|
272
|
+
return scopedToken(
|
|
273
|
+
options.platform.replace(/\/$/, ""),
|
|
274
|
+
options.scope,
|
|
275
|
+
{ action: "show", ...options },
|
|
276
|
+
options.fetch ?? fetch,
|
|
277
|
+
options.stdout ?? console
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
async function adminAi(options) {
|
|
281
|
+
const platform = (options.platform ?? import_node_process3.default.env.ODLA_PLATFORM ?? "https://odla.ai").replace(/\/$/, "");
|
|
282
|
+
const doFetch = options.fetch ?? fetch;
|
|
283
|
+
const out = options.stdout ?? console;
|
|
284
|
+
const scope = options.action === "set" || options.action === "credential-set" ? "platform:ai:write" : "platform:ai:read";
|
|
285
|
+
const token = options.token ?? import_node_process3.default.env.ODLA_ADMIN_TOKEN ?? await scopedToken(platform, scope, options, doFetch, out);
|
|
286
|
+
const headers = { authorization: `Bearer ${token}`, "content-type": "application/json" };
|
|
287
|
+
if (options.action === "credentials") {
|
|
288
|
+
const res2 = await doFetch(`${platform}/registry/platform/ai-credentials`, { headers });
|
|
289
|
+
const body2 = await responseBody(res2);
|
|
290
|
+
if (!res2.ok) throw new Error(apiError("read platform AI credential status", res2.status, body2));
|
|
291
|
+
const credentials = isRecord(body2) && isRecord(body2.credentials) ? body2.credentials : body2;
|
|
292
|
+
if (options.json) out.log(JSON.stringify({ credentials }, null, 2));
|
|
293
|
+
else for (const [provider, status] of Object.entries(isRecord(credentials) ? credentials : {})) {
|
|
294
|
+
out.log(`${provider} ${isRecord(status) && status.set === true ? "set" : "not set"}`);
|
|
295
|
+
}
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (options.action === "credential-set") {
|
|
299
|
+
const provider = requireProvider(options.credentialProvider);
|
|
300
|
+
const value = await credentialValue(options);
|
|
301
|
+
const res2 = await doFetch(`${platform}/registry/platform/ai-credentials/${encodeURIComponent(provider)}`, {
|
|
302
|
+
method: "PUT",
|
|
303
|
+
headers,
|
|
304
|
+
body: JSON.stringify({ value })
|
|
305
|
+
});
|
|
306
|
+
const body2 = await responseBody(res2);
|
|
307
|
+
if (!res2.ok) throw new Error(apiError(`store ${provider} platform credential`, res2.status, body2));
|
|
308
|
+
out.log(`stored ${provider} platform credential (write-only; value was not returned)`);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (options.action === "show") {
|
|
312
|
+
const res2 = await doFetch(`${platform}/registry/platform/ai-policies`, { headers });
|
|
313
|
+
const body2 = await responseBody(res2);
|
|
314
|
+
if (!res2.ok) throw new Error(apiError("read platform AI policies", res2.status, body2));
|
|
315
|
+
const policies = policyArray(body2);
|
|
316
|
+
if (options.json) {
|
|
317
|
+
out.log(JSON.stringify({ policies }, null, 2));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
out.log("purpose enabled provider model max calls max input bytes max output tokens");
|
|
321
|
+
for (const policy2 of policies) {
|
|
322
|
+
out.log([
|
|
323
|
+
policy2.purpose,
|
|
324
|
+
String(policy2.enabled),
|
|
325
|
+
policy2.provider,
|
|
326
|
+
policy2.model,
|
|
327
|
+
String(policy2.maxCallsPerRun ?? ""),
|
|
328
|
+
String(policy2.maxInputBytes ?? ""),
|
|
329
|
+
String(policy2.maxOutputTokens ?? "")
|
|
330
|
+
].join(" "));
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const purpose = requirePurpose(options.purpose);
|
|
335
|
+
if (!options.provider?.trim()) throw new Error("--provider is required");
|
|
336
|
+
if (!options.model?.trim()) throw new Error("--model is required");
|
|
337
|
+
const body = {
|
|
338
|
+
provider: options.provider.trim(),
|
|
339
|
+
model: options.model.trim(),
|
|
340
|
+
...options.enabled === void 0 ? {} : { enabled: options.enabled },
|
|
341
|
+
...options.maxInputBytes === void 0 ? {} : { maxInputBytes: options.maxInputBytes },
|
|
342
|
+
...options.maxOutputTokens === void 0 ? {} : { maxOutputTokens: options.maxOutputTokens },
|
|
343
|
+
...options.maxCallsPerRun === void 0 ? {} : { maxCallsPerRun: options.maxCallsPerRun }
|
|
344
|
+
};
|
|
345
|
+
const res = await doFetch(`${platform}/registry/platform/ai-policies/${encodeURIComponent(purpose)}`, {
|
|
346
|
+
method: "PUT",
|
|
347
|
+
headers,
|
|
348
|
+
body: JSON.stringify(body)
|
|
349
|
+
});
|
|
350
|
+
const response = await responseBody(res);
|
|
351
|
+
if (!res.ok) throw new Error(apiError(`update ${purpose}`, res.status, response));
|
|
352
|
+
const policy = isRecord(response) && isRecord(response.policy) ? response.policy : isRecord(response) ? response : {};
|
|
353
|
+
out.log(options.json ? JSON.stringify({ policy }, null, 2) : `updated ${purpose}: ${String(policy.provider ?? body.provider)}/${String(policy.model ?? body.model)}`);
|
|
354
|
+
}
|
|
355
|
+
async function scopedToken(platform, scope, options, doFetch, out) {
|
|
356
|
+
const tokenFile = options.tokenFile ?? `${import_node_process3.default.cwd()}/.odla/admin-token.local.json`;
|
|
357
|
+
const cache = readJsonFile(tokenFile);
|
|
358
|
+
const cached = cache?.platform === platform ? cache.tokens?.[scope] : void 0;
|
|
359
|
+
if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
360
|
+
out.log(`auth: using cached ${scope} grant (${tokenFile})`);
|
|
361
|
+
return cached.token;
|
|
362
|
+
}
|
|
363
|
+
const { token, expiresAt } = await (0, import_db2.requestToken)({
|
|
364
|
+
endpoint: platform,
|
|
365
|
+
label: `odla CLI admin AI (${scope})`,
|
|
366
|
+
scopes: [scope],
|
|
367
|
+
fetch: doFetch,
|
|
368
|
+
onCode: async ({ userCode, expiresIn }) => {
|
|
369
|
+
const approvalUrl = handshakeUrl(platform, userCode);
|
|
370
|
+
out.log(`Approve scoped request ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
|
|
371
|
+
const shouldOpen = options.open === true || options.open !== false && !import_node_process3.default.env.CI && Boolean(import_node_process3.default.stdin.isTTY && import_node_process3.default.stdout.isTTY);
|
|
372
|
+
if (shouldOpen) await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
const tokens = cache?.platform === platform ? { ...cache.tokens ?? {} } : {};
|
|
376
|
+
tokens[scope] = { token, expiresAt };
|
|
377
|
+
if ((0, import_node_fs2.existsSync)(`${import_node_process3.default.cwd()}/.git`)) ensureGitignore(import_node_process3.default.cwd(), [tokenFile]);
|
|
378
|
+
writePrivateJson(tokenFile, { platform, tokens });
|
|
379
|
+
out.log(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
|
|
380
|
+
return token;
|
|
381
|
+
}
|
|
382
|
+
function requirePurpose(value) {
|
|
383
|
+
if (!SYSTEM_AI_PURPOSES.includes(value)) {
|
|
384
|
+
throw new Error(`purpose must be one of: ${SYSTEM_AI_PURPOSES.join(", ")}`);
|
|
385
|
+
}
|
|
386
|
+
return value;
|
|
387
|
+
}
|
|
388
|
+
function requireProvider(value) {
|
|
389
|
+
if (value !== "anthropic" && value !== "openai" && value !== "google") {
|
|
390
|
+
throw new Error("provider must be one of: anthropic, openai, google");
|
|
391
|
+
}
|
|
392
|
+
return value;
|
|
393
|
+
}
|
|
394
|
+
async function credentialValue(options) {
|
|
395
|
+
if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
|
|
396
|
+
let value;
|
|
397
|
+
if (options.fromEnv) value = import_node_process3.default.env[options.fromEnv];
|
|
398
|
+
else if (options.stdin) value = await (options.readStdin ?? readStdin)();
|
|
399
|
+
else throw new Error("credential input required: use --from-env <NAME> or --stdin; values are never accepted as arguments");
|
|
400
|
+
value = value?.replace(/[\r\n]+$/, "");
|
|
401
|
+
if (!value) throw new Error(options.fromEnv ? `environment variable ${options.fromEnv} is empty or unset` : "stdin credential is empty");
|
|
402
|
+
if (new TextEncoder().encode(value).byteLength > 64 * 1024) throw new Error("credential exceeds 64 KiB");
|
|
403
|
+
return value;
|
|
404
|
+
}
|
|
405
|
+
async function readStdin() {
|
|
406
|
+
let value = "";
|
|
407
|
+
for await (const chunk of import_node_process3.default.stdin) {
|
|
408
|
+
value += String(chunk);
|
|
409
|
+
if (value.length > 64 * 1024) throw new Error("credential exceeds 64 KiB");
|
|
410
|
+
}
|
|
411
|
+
return value;
|
|
412
|
+
}
|
|
413
|
+
function policyArray(body) {
|
|
414
|
+
if (isRecord(body) && Array.isArray(body.policies)) return body.policies.filter(isRecord);
|
|
415
|
+
if (isRecord(body) && isRecord(body.policies)) return Object.values(body.policies).filter(isRecord);
|
|
416
|
+
throw new Error("platform returned an invalid AI policy response");
|
|
417
|
+
}
|
|
418
|
+
async function responseBody(res) {
|
|
419
|
+
const text = await res.text();
|
|
420
|
+
if (!text) return {};
|
|
421
|
+
try {
|
|
422
|
+
return JSON.parse(text);
|
|
423
|
+
} catch {
|
|
424
|
+
return { message: text.slice(0, 300) };
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
function apiError(action, status, body) {
|
|
428
|
+
const error = isRecord(body) && isRecord(body.error) ? body.error : void 0;
|
|
429
|
+
const message = error && typeof error.message === "string" ? error.message : isRecord(body) && typeof body.message === "string" ? body.message : "request failed";
|
|
430
|
+
return `${action} failed (${status}): ${message}`;
|
|
431
|
+
}
|
|
432
|
+
function isRecord(value) {
|
|
433
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/argv.ts
|
|
437
|
+
function parseArgv(argv) {
|
|
438
|
+
const positionals = [];
|
|
439
|
+
const options = {};
|
|
440
|
+
for (let i = 0; i < argv.length; i++) {
|
|
441
|
+
const arg = argv[i];
|
|
442
|
+
if (!arg.startsWith("--")) {
|
|
443
|
+
positionals.push(arg);
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
const eq = arg.indexOf("=");
|
|
447
|
+
const rawName = arg.slice(2, eq === -1 ? void 0 : eq);
|
|
448
|
+
if (!rawName) continue;
|
|
449
|
+
if (rawName.startsWith("no-")) {
|
|
450
|
+
options[rawName.slice(3)] = false;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (eq !== -1) {
|
|
454
|
+
addOption(options, rawName, arg.slice(eq + 1));
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const value = argv[i + 1];
|
|
458
|
+
if (value !== void 0 && !value.startsWith("--")) {
|
|
459
|
+
i++;
|
|
460
|
+
addOption(options, rawName, value);
|
|
461
|
+
} else {
|
|
462
|
+
addOption(options, rawName, true);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return { positionals, options };
|
|
466
|
+
}
|
|
467
|
+
function assertArgs(parsed, allowedOptions, maxPositionals) {
|
|
468
|
+
const allowed = new Set(allowedOptions);
|
|
469
|
+
for (const name of Object.keys(parsed.options)) {
|
|
470
|
+
if (!allowed.has(name)) throw new Error(`unknown option "--${name}"; run "odla-ai help" for supported options`);
|
|
471
|
+
}
|
|
472
|
+
if (parsed.positionals.length > maxPositionals) {
|
|
473
|
+
throw new Error(`unexpected argument "${parsed.positionals[maxPositionals]}"; run "odla-ai help"`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function requiredString(value, name) {
|
|
477
|
+
const result = stringOpt(value);
|
|
478
|
+
if (!result) throw new Error(`${name} is required`);
|
|
479
|
+
return result;
|
|
480
|
+
}
|
|
481
|
+
function stringOpt(value) {
|
|
482
|
+
if (typeof value === "string") return value;
|
|
483
|
+
if (Array.isArray(value)) return value[value.length - 1];
|
|
484
|
+
return void 0;
|
|
485
|
+
}
|
|
486
|
+
function listOpt(value) {
|
|
487
|
+
if (value === void 0 || typeof value === "boolean") return void 0;
|
|
488
|
+
const values = Array.isArray(value) ? value : [value];
|
|
489
|
+
return values.flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
|
|
490
|
+
}
|
|
491
|
+
function boolOpt(value) {
|
|
492
|
+
if (typeof value === "boolean") return value;
|
|
493
|
+
if (typeof value === "string" && (value === "true" || value === "false")) return value === "true";
|
|
494
|
+
if (value === void 0) return void 0;
|
|
495
|
+
throw new Error("boolean option must be true or false");
|
|
496
|
+
}
|
|
497
|
+
function numberOpt(value, flag) {
|
|
498
|
+
if (value === void 0) return void 0;
|
|
499
|
+
const raw = stringOpt(value);
|
|
500
|
+
const parsed = raw === void 0 ? Number.NaN : Number(raw);
|
|
501
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) throw new Error(`${flag} requires a positive integer`);
|
|
502
|
+
return parsed;
|
|
503
|
+
}
|
|
504
|
+
function addOption(options, name, value) {
|
|
505
|
+
const current = options[name];
|
|
506
|
+
if (current === void 0) options[name] = value;
|
|
507
|
+
else if (Array.isArray(current)) current.push(String(value));
|
|
508
|
+
else options[name] = [String(current), String(value)];
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// src/capabilities.ts
|
|
512
|
+
var CAPABILITIES = {
|
|
513
|
+
cli: [
|
|
514
|
+
"register the app and enable configured services per environment",
|
|
515
|
+
"issue, persist, and explicitly rotate configured service credentials",
|
|
516
|
+
"write local Worker values and push deployed Worker secrets through Wrangler stdin",
|
|
517
|
+
"push db schema/rules and configure platform AI, auth, and deployment links",
|
|
518
|
+
"validate config offline and smoke-test a provisioned db environment",
|
|
519
|
+
"run app-attributed hosted security discovery and independent validation without provider keys",
|
|
520
|
+
"let admins manage system AI routes/credentials through short-lived exact-scope approval"
|
|
521
|
+
],
|
|
522
|
+
agent: [
|
|
523
|
+
"install and import the selected odla SDKs",
|
|
524
|
+
"wrap the Worker with withObservability and choose useful telemetry",
|
|
525
|
+
"make application-specific schema, rules, auth, UI, and migration decisions"
|
|
526
|
+
],
|
|
527
|
+
human: [
|
|
528
|
+
"approve the odla device code and one-off third-party logins",
|
|
529
|
+
"consent to production changes with --yes",
|
|
530
|
+
"request destructive credential rotation explicitly",
|
|
531
|
+
"review application semantics, security findings, releases, and merges",
|
|
532
|
+
"approve redacted-source disclosure before hosted security reasoning"
|
|
533
|
+
],
|
|
534
|
+
studio: [
|
|
535
|
+
"view telemetry and environment state",
|
|
536
|
+
"perform manual credential recovery when the CLI's local shown-once copy is unavailable",
|
|
537
|
+
"configure system AI purposes and view app/environment/run-attributed usage"
|
|
538
|
+
]
|
|
539
|
+
};
|
|
540
|
+
function printCapabilities(json = false, out = console) {
|
|
541
|
+
if (json) {
|
|
542
|
+
out.log(JSON.stringify(CAPABILITIES, null, 2));
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
out.log("odla-ai responsibility boundary\n");
|
|
546
|
+
printGroup(out, "CLI automates", CAPABILITIES.cli);
|
|
547
|
+
printGroup(out, "Coding agent changes source", CAPABILITIES.agent);
|
|
548
|
+
printGroup(out, "Human checkpoints", CAPABILITIES.human);
|
|
549
|
+
printGroup(out, "Studio", CAPABILITIES.studio);
|
|
550
|
+
}
|
|
551
|
+
function printGroup(out, heading, items) {
|
|
552
|
+
out.log(`${heading}:`);
|
|
553
|
+
for (const item of items) out.log(` - ${item}`);
|
|
554
|
+
out.log("");
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// src/config.ts
|
|
558
|
+
var import_node_fs3 = require("fs");
|
|
559
|
+
var import_node_path2 = require("path");
|
|
55
560
|
var import_node_url = require("url");
|
|
56
561
|
var DEFAULT_PLATFORM = "https://odla.ai";
|
|
57
|
-
var DEFAULT_ENVS = ["
|
|
562
|
+
var DEFAULT_ENVS = ["dev"];
|
|
58
563
|
var DEFAULT_SERVICES = ["db", "ai"];
|
|
59
564
|
async function loadProjectConfig(configPath = "odla.config.mjs") {
|
|
60
|
-
const resolved = (0,
|
|
61
|
-
if (!(0,
|
|
565
|
+
const resolved = (0, import_node_path2.resolve)(configPath);
|
|
566
|
+
if (!(0, import_node_fs3.existsSync)(resolved)) {
|
|
62
567
|
throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
|
|
63
568
|
}
|
|
64
569
|
const raw = await loadConfigModule(resolved);
|
|
65
|
-
const rootDir = (0,
|
|
570
|
+
const rootDir = (0, import_node_path2.dirname)(resolved);
|
|
66
571
|
validateRawConfig(raw, resolved);
|
|
67
572
|
const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
|
|
68
573
|
const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
|
|
69
574
|
const envs = unique(raw.envs?.length ? raw.envs : DEFAULT_ENVS);
|
|
70
575
|
const services = unique(raw.services?.length ? raw.services : DEFAULT_SERVICES);
|
|
71
576
|
const local = {
|
|
72
|
-
tokenFile: (0,
|
|
73
|
-
credentialsFile: (0,
|
|
74
|
-
devVarsFile: (0,
|
|
577
|
+
tokenFile: (0, import_node_path2.resolve)(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
|
|
578
|
+
credentialsFile: (0, import_node_path2.resolve)(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
|
|
579
|
+
devVarsFile: (0, import_node_path2.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
|
|
75
580
|
gitignore: raw.local?.gitignore ?? true
|
|
76
581
|
};
|
|
77
582
|
return {
|
|
@@ -88,9 +593,9 @@ async function loadProjectConfig(configPath = "odla.config.mjs") {
|
|
|
88
593
|
async function resolveDataExport(cfg, value, names) {
|
|
89
594
|
if (value === void 0 || value === null || value === false) return void 0;
|
|
90
595
|
if (typeof value !== "string") return value;
|
|
91
|
-
const target = (0,
|
|
596
|
+
const target = (0, import_node_path2.isAbsolute)(value) ? value : (0, import_node_path2.resolve)(cfg.rootDir, value);
|
|
92
597
|
if (target.endsWith(".json")) {
|
|
93
|
-
return JSON.parse((0,
|
|
598
|
+
return JSON.parse((0, import_node_fs3.readFileSync)(target, "utf8"));
|
|
94
599
|
}
|
|
95
600
|
const mod = await import((0, import_node_url.pathToFileURL)(target).href);
|
|
96
601
|
for (const name of names) {
|
|
@@ -141,7 +646,7 @@ function validId(value) {
|
|
|
141
646
|
return typeof value === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value);
|
|
142
647
|
}
|
|
143
648
|
async function loadConfigModule(path) {
|
|
144
|
-
if (path.endsWith(".json")) return JSON.parse((0,
|
|
649
|
+
if (path.endsWith(".json")) return JSON.parse((0, import_node_fs3.readFileSync)(path, "utf8"));
|
|
145
650
|
const mod = await import(`${(0, import_node_url.pathToFileURL)(path).href}?t=${Date.now()}`);
|
|
146
651
|
const value = mod.default ?? mod.config;
|
|
147
652
|
if (typeof value === "function") return await value();
|
|
@@ -155,9 +660,9 @@ function unique(values) {
|
|
|
155
660
|
}
|
|
156
661
|
|
|
157
662
|
// src/doctor-checks.ts
|
|
158
|
-
var
|
|
159
|
-
var
|
|
160
|
-
var
|
|
663
|
+
var import_node_child_process3 = require("child_process");
|
|
664
|
+
var import_node_fs5 = require("fs");
|
|
665
|
+
var import_node_path4 = require("path");
|
|
161
666
|
|
|
162
667
|
// src/redact.ts
|
|
163
668
|
var REPLACEMENTS = [
|
|
@@ -179,11 +684,11 @@ function looksSecret(value) {
|
|
|
179
684
|
}
|
|
180
685
|
|
|
181
686
|
// src/wrangler.ts
|
|
182
|
-
var
|
|
183
|
-
var
|
|
184
|
-
var
|
|
687
|
+
var import_node_child_process2 = require("child_process");
|
|
688
|
+
var import_node_fs4 = require("fs");
|
|
689
|
+
var import_node_path3 = require("path");
|
|
185
690
|
var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
|
|
186
|
-
const child = (0,
|
|
691
|
+
const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
187
692
|
let stdout = "";
|
|
188
693
|
let stderr = "";
|
|
189
694
|
child.stdout.on("data", (chunk) => stdout += chunk.toString());
|
|
@@ -195,15 +700,15 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) =>
|
|
|
195
700
|
var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
|
|
196
701
|
function findWranglerConfig(rootDir) {
|
|
197
702
|
for (const name of WRANGLER_CONFIG_FILES) {
|
|
198
|
-
const path = (0,
|
|
199
|
-
if ((0,
|
|
703
|
+
const path = (0, import_node_path3.join)(rootDir, name);
|
|
704
|
+
if ((0, import_node_fs4.existsSync)(path)) return path;
|
|
200
705
|
}
|
|
201
706
|
return null;
|
|
202
707
|
}
|
|
203
708
|
function readWranglerConfig(path) {
|
|
204
709
|
if (path.endsWith(".toml")) return null;
|
|
205
710
|
try {
|
|
206
|
-
return JSON.parse(stripJsonComments((0,
|
|
711
|
+
return JSON.parse(stripJsonComments((0, import_node_fs4.readFileSync)(path, "utf8")));
|
|
207
712
|
} catch {
|
|
208
713
|
return null;
|
|
209
714
|
}
|
|
@@ -274,11 +779,12 @@ function lintRules(rules, entities, publicRead) {
|
|
|
274
779
|
}
|
|
275
780
|
return warnings;
|
|
276
781
|
}
|
|
277
|
-
var defaultExec = (cmd, args, cwd) => (0,
|
|
278
|
-
function trackedSecretFiles(rootDir, exec = defaultExec) {
|
|
782
|
+
var defaultExec = (cmd, args, cwd) => (0, import_node_child_process3.execFileSync)(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
783
|
+
function trackedSecretFiles(rootDir, exec = defaultExec, localPaths = []) {
|
|
279
784
|
let output;
|
|
280
785
|
try {
|
|
281
|
-
|
|
786
|
+
const custom = localPaths.map((path) => gitignoreEntry(rootDir, path)).filter((path) => !!path);
|
|
787
|
+
output = exec("git", ["ls-files", "--", ".dev.vars", ".odla", ...custom], rootDir);
|
|
282
788
|
} catch {
|
|
283
789
|
return [];
|
|
284
790
|
}
|
|
@@ -300,10 +806,10 @@ function wranglerWarnings(rootDir) {
|
|
|
300
806
|
for (const { label, block } of blocks) {
|
|
301
807
|
const assets = block.assets;
|
|
302
808
|
if (assets?.directory) {
|
|
303
|
-
const dir = (0,
|
|
304
|
-
if (dir === (0,
|
|
809
|
+
const dir = (0, import_node_path4.resolve)(rootDir, assets.directory);
|
|
810
|
+
if (dir === (0, import_node_path4.resolve)(rootDir)) {
|
|
305
811
|
warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
|
|
306
|
-
} else if ((0,
|
|
812
|
+
} else if ((0, import_node_fs5.existsSync)((0, import_node_path4.join)(dir, "node_modules"))) {
|
|
307
813
|
warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
|
|
308
814
|
}
|
|
309
815
|
}
|
|
@@ -322,96 +828,57 @@ function wranglerWarnings(rootDir) {
|
|
|
322
828
|
}
|
|
323
829
|
return warnings;
|
|
324
830
|
}
|
|
325
|
-
function
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
831
|
+
function o11yProjectWarnings(rootDir) {
|
|
832
|
+
const warnings = [];
|
|
833
|
+
const pkg = readPackageJson(rootDir);
|
|
834
|
+
const dependencies = pkg?.dependencies;
|
|
835
|
+
const devDependencies = pkg?.devDependencies;
|
|
836
|
+
if (!dependencies?.["@odla-ai/o11y"]) {
|
|
837
|
+
warnings.push(
|
|
838
|
+
devDependencies?.["@odla-ai/o11y"] ? "@odla-ai/o11y is a devDependency \u2014 move it to dependencies so the Worker can import it" : 'Worker instrumentation is missing @odla-ai/o11y \u2014 install it and wrap the handler with "withObservability"'
|
|
839
|
+
);
|
|
330
840
|
}
|
|
841
|
+
const configPath = findWranglerConfig(rootDir);
|
|
842
|
+
const config = configPath ? readWranglerConfig(configPath) : null;
|
|
843
|
+
if (!configPath || !config) {
|
|
844
|
+
warnings.push("cannot verify o11y Worker instrumentation \u2014 add a parseable wrangler.jsonc/json config");
|
|
845
|
+
return warnings;
|
|
846
|
+
}
|
|
847
|
+
const main = typeof config.main === "string" ? (0, import_node_path4.resolve)(rootDir, config.main) : null;
|
|
848
|
+
if (!main || !(0, import_node_fs5.existsSync)(main)) {
|
|
849
|
+
warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
|
|
850
|
+
} else {
|
|
851
|
+
let source = "";
|
|
852
|
+
try {
|
|
853
|
+
source = (0, import_node_fs5.readFileSync)(main, "utf8");
|
|
854
|
+
} catch {
|
|
855
|
+
}
|
|
856
|
+
if (!/\bwithObservability\b/.test(source)) {
|
|
857
|
+
warnings.push(`wrangler entrypoint ${config.main} does not reference withObservability`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
const flags = Array.isArray(config.compatibility_flags) ? config.compatibility_flags : [];
|
|
861
|
+
if (!flags.includes("nodejs_compat")) {
|
|
862
|
+
warnings.push('wrangler compatibility_flags is missing "nodejs_compat" required by @odla-ai/o11y');
|
|
863
|
+
}
|
|
864
|
+
return warnings;
|
|
331
865
|
}
|
|
332
|
-
|
|
333
|
-
// src/local.ts
|
|
334
|
-
var import_node_fs4 = require("fs");
|
|
335
|
-
var import_node_path4 = require("path");
|
|
336
|
-
var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
|
|
337
|
-
function readJsonFile(path) {
|
|
866
|
+
function readPackageJson(rootDir) {
|
|
338
867
|
try {
|
|
339
|
-
return JSON.parse((0,
|
|
868
|
+
return JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(rootDir, "package.json"), "utf8"));
|
|
340
869
|
} catch {
|
|
341
870
|
return null;
|
|
342
871
|
}
|
|
343
872
|
}
|
|
344
|
-
function writePrivateJson(path, value) {
|
|
345
|
-
(0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(path), { recursive: true });
|
|
346
|
-
(0, import_node_fs4.writeFileSync)(path, `${JSON.stringify(value, null, 2)}
|
|
347
|
-
`);
|
|
348
|
-
(0, import_node_fs4.chmodSync)(path, 384);
|
|
349
|
-
}
|
|
350
|
-
function readCredentials(path) {
|
|
351
|
-
return readJsonFile(path);
|
|
352
|
-
}
|
|
353
|
-
function mergeCredential(current, update) {
|
|
354
|
-
const next = current ?? {
|
|
355
|
-
appId: update.appId,
|
|
356
|
-
platformUrl: update.platformUrl,
|
|
357
|
-
dbEndpoint: update.dbEndpoint,
|
|
358
|
-
updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
359
|
-
envs: {}
|
|
360
|
-
};
|
|
361
|
-
next.appId = update.appId;
|
|
362
|
-
next.platformUrl = update.platformUrl;
|
|
363
|
-
next.dbEndpoint = update.dbEndpoint;
|
|
364
|
-
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
365
|
-
next.envs[update.env] = {
|
|
366
|
-
...next.envs[update.env] ?? {},
|
|
367
|
-
tenantId: update.tenantId,
|
|
368
|
-
...update.dbKey ? { dbKey: update.dbKey } : {},
|
|
369
|
-
...update.o11yToken ? { o11yToken: update.o11yToken } : {}
|
|
370
|
-
};
|
|
371
|
-
return next;
|
|
372
|
-
}
|
|
373
|
-
function ensureGitignore(rootDir) {
|
|
374
|
-
const path = (0, import_node_path4.resolve)(rootDir, ".gitignore");
|
|
375
|
-
const existing = (0, import_node_fs4.existsSync)(path) ? (0, import_node_fs4.readFileSync)(path, "utf8") : "";
|
|
376
|
-
const missing = GITIGNORE_LINES.filter((line) => !existing.split(/\r?\n/).includes(line));
|
|
377
|
-
if (missing.length === 0) return;
|
|
378
|
-
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
379
|
-
(0, import_node_fs4.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
|
|
380
|
-
`);
|
|
381
|
-
}
|
|
382
|
-
function writeDevVars(path, credentials, env, o11y) {
|
|
383
|
-
const entry = credentials.envs[env];
|
|
384
|
-
if (!entry?.dbKey) throw new Error(`no db key for env "${env}" in ${path}`);
|
|
385
|
-
const lines = [
|
|
386
|
-
`ODLA_PLATFORM="${credentials.platformUrl}"`,
|
|
387
|
-
`ODLA_ENDPOINT="${credentials.dbEndpoint}"`,
|
|
388
|
-
`ODLA_APP_ID="${credentials.appId}"`,
|
|
389
|
-
`ODLA_ENV="${env}"`,
|
|
390
|
-
`ODLA_TENANT="${entry.tenantId}"`,
|
|
391
|
-
`ODLA_API_KEY="${entry.dbKey}"`
|
|
392
|
-
];
|
|
393
|
-
if (o11y) {
|
|
394
|
-
lines.push(`ODLA_O11Y_ENDPOINT="${o11y.endpoint}"`, `ODLA_O11Y_SERVICE="${o11y.service}"`);
|
|
395
|
-
if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
|
|
396
|
-
if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
|
|
397
|
-
}
|
|
398
|
-
(0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(path), { recursive: true });
|
|
399
|
-
(0, import_node_fs4.writeFileSync)(path, `${lines.join("\n")}
|
|
400
|
-
`);
|
|
401
|
-
(0, import_node_fs4.chmodSync)(path, 384);
|
|
402
|
-
}
|
|
403
|
-
function displayPath(path, rootDir = process.cwd()) {
|
|
404
|
-
const rel = (0, import_node_path4.relative)(rootDir, path);
|
|
405
|
-
return rel && !rel.startsWith("..") ? rel : path;
|
|
406
|
-
}
|
|
407
873
|
|
|
408
874
|
// src/doctor.ts
|
|
409
875
|
async function doctor(options) {
|
|
410
876
|
const out = options.stdout ?? console;
|
|
411
877
|
const cfg = await loadProjectConfig(options.configPath);
|
|
412
878
|
const plan = buildPlan(cfg);
|
|
413
|
-
const
|
|
414
|
-
const
|
|
879
|
+
const hasDb = cfg.services.includes("db");
|
|
880
|
+
const schema = hasDb ? await resolveDataExport(cfg, cfg.db?.schema, ["schema", "SCHEMA"]) : void 0;
|
|
881
|
+
const configuredRules = hasDb ? await resolveDataExport(cfg, cfg.db?.rules, ["rules", "RULES"]) : void 0;
|
|
415
882
|
const rules = configuredRules ?? (schema && cfg.db?.defaultRules !== false ? rulesFromSchema(schema) : void 0);
|
|
416
883
|
const entities = serializedEntities(schema);
|
|
417
884
|
out.log(`config: ${cfg.configPath}`);
|
|
@@ -420,7 +887,7 @@ async function doctor(options) {
|
|
|
420
887
|
out.log(`svc: ${plan.services.join(", ")}`);
|
|
421
888
|
out.log(`schema: ${schema ? `${entities.length} entities` : "none"}`);
|
|
422
889
|
out.log(`rules: ${rules ? `${Object.keys(rules).length} namespaces` : "none"}`);
|
|
423
|
-
out.log(`ai: ${cfg.ai?.provider ?? "not configured"}`);
|
|
890
|
+
out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "not configured" : "not enabled"}`);
|
|
424
891
|
const warnings = [];
|
|
425
892
|
if (schema && entities.length === 0) warnings.push("schema has no entities");
|
|
426
893
|
if (rules) {
|
|
@@ -436,7 +903,9 @@ async function doctor(options) {
|
|
|
436
903
|
}
|
|
437
904
|
}
|
|
438
905
|
}
|
|
439
|
-
if (cfg.ai?.keyEnv && !process.env[cfg.ai.keyEnv])
|
|
906
|
+
if (cfg.services.includes("ai") && cfg.ai?.keyEnv && !process.env[cfg.ai.keyEnv]) {
|
|
907
|
+
warnings.push(`${cfg.ai.keyEnv} is not set; provision will skip provider key storage`);
|
|
908
|
+
}
|
|
440
909
|
if (cfg.services.includes("o11y")) {
|
|
441
910
|
const credentials = readCredentials(cfg.local.credentialsFile);
|
|
442
911
|
for (const env of cfg.envs) {
|
|
@@ -444,9 +913,14 @@ async function doctor(options) {
|
|
|
444
913
|
warnings.push(`o11y is enabled but env "${env}" has no ingest token \u2014 run "odla-ai provision" to mint one`);
|
|
445
914
|
}
|
|
446
915
|
}
|
|
916
|
+
warnings.push(...o11yProjectWarnings(cfg.rootDir));
|
|
447
917
|
}
|
|
448
918
|
warnings.push(...lintRules(rules, entities, cfg.db?.publicRead ?? []));
|
|
449
|
-
warnings.push(...trackedSecretFiles(cfg.rootDir
|
|
919
|
+
warnings.push(...trackedSecretFiles(cfg.rootDir, void 0, [
|
|
920
|
+
cfg.local.tokenFile,
|
|
921
|
+
cfg.local.credentialsFile,
|
|
922
|
+
cfg.local.devVarsFile
|
|
923
|
+
]));
|
|
450
924
|
warnings.push(...wranglerWarnings(cfg.rootDir));
|
|
451
925
|
if (warnings.length) {
|
|
452
926
|
out.log("");
|
|
@@ -458,25 +932,25 @@ async function doctor(options) {
|
|
|
458
932
|
}
|
|
459
933
|
|
|
460
934
|
// src/init.ts
|
|
461
|
-
var
|
|
935
|
+
var import_node_fs6 = require("fs");
|
|
462
936
|
var import_node_path5 = require("path");
|
|
463
937
|
function initProject(options) {
|
|
464
938
|
const out = options.stdout ?? console;
|
|
465
939
|
const rootDir = (0, import_node_path5.resolve)(options.rootDir ?? process.cwd());
|
|
466
940
|
const configPath = (0, import_node_path5.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
|
|
467
|
-
if ((0,
|
|
941
|
+
if ((0, import_node_fs6.existsSync)(configPath) && !options.force) {
|
|
468
942
|
throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
|
|
469
943
|
}
|
|
470
944
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
|
|
471
945
|
throw new Error("--app-id must be lowercase letters, numbers, and hyphens");
|
|
472
946
|
}
|
|
473
|
-
const envs = options.envs?.length ? options.envs : ["
|
|
947
|
+
const envs = options.envs?.length ? options.envs : ["dev"];
|
|
474
948
|
const services = options.services?.length ? options.services : ["db", "ai"];
|
|
475
949
|
const aiProvider = options.aiProvider ?? "anthropic";
|
|
476
|
-
(0,
|
|
477
|
-
(0,
|
|
478
|
-
(0,
|
|
479
|
-
(0,
|
|
950
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path5.dirname)(configPath), { recursive: true });
|
|
951
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path5.resolve)(rootDir, "src/odla"), { recursive: true });
|
|
952
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path5.resolve)(rootDir, ".odla"), { recursive: true });
|
|
953
|
+
(0, import_node_fs6.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
|
|
480
954
|
writeIfMissing((0, import_node_path5.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
|
|
481
955
|
writeIfMissing((0, import_node_path5.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
|
|
482
956
|
ensureGitignore(rootDir);
|
|
@@ -485,8 +959,8 @@ function initProject(options) {
|
|
|
485
959
|
out.log("updated .gitignore for local odla credentials");
|
|
486
960
|
}
|
|
487
961
|
function writeIfMissing(path, text) {
|
|
488
|
-
if ((0,
|
|
489
|
-
(0,
|
|
962
|
+
if ((0, import_node_fs6.existsSync)(path)) return;
|
|
963
|
+
(0, import_node_fs6.writeFileSync)(path, text);
|
|
490
964
|
}
|
|
491
965
|
function configTemplate(input) {
|
|
492
966
|
return `export default {
|
|
@@ -573,86 +1047,176 @@ function relativeDisplay(path, rootDir) {
|
|
|
573
1047
|
}
|
|
574
1048
|
|
|
575
1049
|
// src/provision.ts
|
|
576
|
-
var
|
|
1050
|
+
var import_apps2 = require("@odla-ai/apps");
|
|
577
1051
|
var import_ai = require("@odla-ai/ai");
|
|
578
|
-
var
|
|
579
|
-
var import_node_process3 = __toESM(require("process"), 1);
|
|
1052
|
+
var import_node_process4 = __toESM(require("process"), 1);
|
|
580
1053
|
|
|
581
|
-
// src/
|
|
582
|
-
var
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
1054
|
+
// src/provision-credentials.ts
|
|
1055
|
+
var import_apps = require("@odla-ai/apps");
|
|
1056
|
+
async function provisionEnvCredentials(opts) {
|
|
1057
|
+
const tenantId = (0, import_apps.tenantIdFor)(opts.cfg.app.id, opts.env);
|
|
1058
|
+
const prior = opts.credentials?.envs[opts.env];
|
|
1059
|
+
let credentials = opts.credentials;
|
|
1060
|
+
let dbKey = opts.cfg.services.includes("db") && !opts.rotateDb ? prior?.dbKey : void 0;
|
|
1061
|
+
let o11yToken = opts.cfg.services.includes("o11y") && !opts.rotateO11y ? prior?.o11yToken : void 0;
|
|
1062
|
+
if (opts.cfg.services.includes("db")) {
|
|
1063
|
+
if (dbKey) {
|
|
1064
|
+
opts.stdout.log(`${opts.env}: reusing local db key for ${tenantId}`);
|
|
1065
|
+
} else {
|
|
1066
|
+
dbKey = await mintDbKey(opts, tenantId);
|
|
1067
|
+
credentials = save(opts, credentials, tenantId, { dbKey });
|
|
1068
|
+
opts.stdout.log(`${opts.env}: minted db key for ${tenantId}`);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
if (opts.cfg.services.includes("o11y")) {
|
|
1072
|
+
if (o11yToken) {
|
|
1073
|
+
opts.stdout.log(`${opts.env}: reusing local o11y ingest token`);
|
|
1074
|
+
} else {
|
|
1075
|
+
o11yToken = await issueO11yToken(opts);
|
|
1076
|
+
credentials = save(opts, credentials, tenantId, { ...dbKey ? { dbKey } : {}, o11yToken });
|
|
1077
|
+
opts.stdout.log(`${opts.env}: ${opts.rotateO11y ? "rotated" : "issued"} o11y ingest token`);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
return save(opts, credentials, tenantId, {
|
|
1081
|
+
...dbKey ? { dbKey } : {},
|
|
1082
|
+
...o11yToken ? { o11yToken } : {}
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
function save(opts, current, tenantId, values) {
|
|
1086
|
+
const next = mergeCredential(current, {
|
|
1087
|
+
appId: opts.cfg.app.id,
|
|
1088
|
+
platformUrl: opts.cfg.platformUrl,
|
|
1089
|
+
dbEndpoint: opts.cfg.dbEndpoint,
|
|
1090
|
+
env: opts.env,
|
|
1091
|
+
tenantId,
|
|
1092
|
+
...values
|
|
1093
|
+
});
|
|
1094
|
+
if (opts.write) writePrivateJson(opts.cfg.local.credentialsFile, next);
|
|
1095
|
+
return next;
|
|
1096
|
+
}
|
|
1097
|
+
async function mintDbKey(opts, tenantId) {
|
|
1098
|
+
const headers = { authorization: `Bearer ${opts.developerToken}`, "content-type": "application/json" };
|
|
1099
|
+
let res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/keys`, {
|
|
1100
|
+
method: "POST",
|
|
1101
|
+
headers,
|
|
1102
|
+
body: "{}"
|
|
1103
|
+
});
|
|
1104
|
+
if (res.status === 404) {
|
|
1105
|
+
const created = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps`, {
|
|
1106
|
+
method: "POST",
|
|
1107
|
+
headers,
|
|
1108
|
+
body: JSON.stringify({
|
|
1109
|
+
name: opts.env === "prod" ? opts.cfg.app.name : `${opts.cfg.app.name} (${opts.env})`,
|
|
1110
|
+
appId: tenantId
|
|
1111
|
+
})
|
|
595
1112
|
});
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
1113
|
+
if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText(created)}`);
|
|
1114
|
+
res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/keys`, {
|
|
1115
|
+
method: "POST",
|
|
1116
|
+
headers,
|
|
1117
|
+
body: "{}"
|
|
600
1118
|
});
|
|
601
|
-
}
|
|
1119
|
+
}
|
|
1120
|
+
if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText(res)}`);
|
|
1121
|
+
const body = await res.json();
|
|
1122
|
+
if (!body.key) throw new Error(`db key mint (${tenantId}) returned no key`);
|
|
1123
|
+
return body.key;
|
|
602
1124
|
}
|
|
603
|
-
function
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
1125
|
+
async function issueO11yToken(opts) {
|
|
1126
|
+
const suffix = opts.rotateO11y ? "/rotate" : "";
|
|
1127
|
+
const res = await opts.fetch(
|
|
1128
|
+
`${opts.cfg.platformUrl}/o11y/${encodeURIComponent(opts.cfg.app.id)}/token${suffix}?env=${encodeURIComponent(opts.env)}`,
|
|
1129
|
+
{ method: "POST", headers: { authorization: `Bearer ${opts.developerToken}` } }
|
|
1130
|
+
);
|
|
1131
|
+
if (res.status === 409 && !opts.rotateO11y) {
|
|
1132
|
+
throw new Error(
|
|
1133
|
+
`o11y token already exists for env "${opts.env}", but its shown-once value is not in the local credentials file; run "odla-ai provision --rotate-o11y-token --push-secrets" to replace it explicitly`
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText(res)}`);
|
|
1137
|
+
const body = await res.json();
|
|
1138
|
+
if (!body.token) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) returned no token`);
|
|
1139
|
+
return body.token;
|
|
1140
|
+
}
|
|
1141
|
+
async function safeText(res) {
|
|
1142
|
+
try {
|
|
1143
|
+
return redactSecrets((await res.text()).slice(0, 500));
|
|
1144
|
+
} catch {
|
|
1145
|
+
return "";
|
|
1146
|
+
}
|
|
607
1147
|
}
|
|
608
1148
|
|
|
609
|
-
// src/
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
1149
|
+
// src/secrets.ts
|
|
1150
|
+
var PROD_ENV_NAMES = /* @__PURE__ */ new Set(["prod", "production"]);
|
|
1151
|
+
async function secretsPush(options) {
|
|
1152
|
+
await secretsPushImpl(options, true);
|
|
1153
|
+
}
|
|
1154
|
+
async function secretsPushAfterPreflight(options) {
|
|
1155
|
+
await secretsPushImpl(options, false);
|
|
1156
|
+
}
|
|
1157
|
+
async function secretsPushImpl(options, preflight) {
|
|
1158
|
+
const out = options.stdout ?? console;
|
|
1159
|
+
const cfg = await loadProjectConfig(options.configPath);
|
|
1160
|
+
const env = options.env;
|
|
1161
|
+
if (!cfg.envs.includes(env)) {
|
|
1162
|
+
throw new Error(`env "${env}" is not in config envs (${cfg.envs.join(", ")})`);
|
|
617
1163
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
1164
|
+
if (PROD_ENV_NAMES.has(env) && !options.yes) {
|
|
1165
|
+
throw new Error(`refusing to push a secret to "${env}" without --yes`);
|
|
1166
|
+
}
|
|
1167
|
+
const credentialsPath = displayPath(cfg.local.credentialsFile, cfg.rootDir);
|
|
1168
|
+
const credentials = readCredentials(cfg.local.credentialsFile);
|
|
1169
|
+
if (!credentials) throw new Error(`no credentials at ${credentialsPath} \u2014 run "odla-ai provision" first`);
|
|
1170
|
+
if (credentials.appId !== cfg.app.id) {
|
|
1171
|
+
throw new Error(`credentials at ${credentialsPath} are for "${credentials.appId}", not "${cfg.app.id}"`);
|
|
1172
|
+
}
|
|
1173
|
+
const entry = credentials.envs[env];
|
|
1174
|
+
if (!entry) throw new Error(`no credentials for env "${env}" in ${credentialsPath} \u2014 run "odla-ai provision" first`);
|
|
1175
|
+
const secrets = [];
|
|
1176
|
+
if (cfg.services.includes("o11y")) {
|
|
1177
|
+
if (!entry.o11yToken) throw new Error(`no o11y token for env "${env}" in ${credentialsPath} \u2014 run "odla-ai provision" first`);
|
|
1178
|
+
secrets.push({ name: "ODLA_O11Y_TOKEN", value: entry.o11yToken });
|
|
1179
|
+
}
|
|
1180
|
+
if (cfg.services.includes("db")) {
|
|
1181
|
+
if (!entry.dbKey) throw new Error(`no db key for env "${env}" in ${credentialsPath} \u2014 run "odla-ai provision" first`);
|
|
1182
|
+
secrets.push({ name: "ODLA_API_KEY", value: entry.dbKey });
|
|
1183
|
+
}
|
|
1184
|
+
if (secrets.length === 0) {
|
|
1185
|
+
out.log(`${env}: no configured Worker secrets to push`);
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
const wranglerEnv = PROD_ENV_NAMES.has(env) ? void 0 : env;
|
|
1189
|
+
const target = wranglerEnv ? `wrangler env "${wranglerEnv}"` : "the top-level (prod) wrangler env";
|
|
1190
|
+
if (options.dryRun) {
|
|
1191
|
+
assertWranglerConfig(cfg);
|
|
1192
|
+
for (const s of secrets) out.log(`dry run: would push ${s.name} (${redactSecrets(s.value)}) to ${target}`);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
const run = options.runner ?? defaultRunner;
|
|
1196
|
+
if (preflight) await preflightLoadedConfig(cfg, run);
|
|
1197
|
+
for (const s of secrets) {
|
|
1198
|
+
const result = await wranglerPutSecret(run, { name: s.name, value: s.value, env: wranglerEnv, cwd: cfg.rootDir });
|
|
1199
|
+
if (result.code !== 0) {
|
|
1200
|
+
throw new Error(`wrangler secret put ${s.name} failed (exit ${result.code}): ${redactSecrets(`${result.stderr || result.stdout}`.trim())}`);
|
|
638
1201
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
out.log(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
642
|
-
return token;
|
|
1202
|
+
out.log(`${s.name} pushed to ${target} (value read from ${credentialsPath}, never echoed)`);
|
|
1203
|
+
}
|
|
643
1204
|
}
|
|
644
|
-
function
|
|
645
|
-
|
|
646
|
-
if (
|
|
647
|
-
|
|
648
|
-
const interactive = options.interactive ?? Boolean(import_node_process2.default.stdin.isTTY && import_node_process2.default.stdout.isTTY);
|
|
649
|
-
if (!interactive) return { open: false, reason: "non-interactive shell; pass --open to force" };
|
|
650
|
-
return { open: true, mode: "auto" };
|
|
1205
|
+
async function preflightSecretsPush(options) {
|
|
1206
|
+
const cfg = await loadProjectConfig(options.configPath);
|
|
1207
|
+
if (!cfg.services.some((service) => service === "db" || service === "o11y")) return;
|
|
1208
|
+
await preflightLoadedConfig(cfg, options.runner ?? defaultRunner);
|
|
651
1209
|
}
|
|
652
|
-
function
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
1210
|
+
async function preflightLoadedConfig(cfg, run) {
|
|
1211
|
+
assertWranglerConfig(cfg);
|
|
1212
|
+
if (!await wranglerLoggedIn(run, cfg.rootDir)) {
|
|
1213
|
+
throw new Error(`wrangler is not logged in \u2014 run "wrangler login" (a browser step for the human)`);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
function assertWranglerConfig(cfg) {
|
|
1217
|
+
if (!findWranglerConfig(cfg.rootDir)) {
|
|
1218
|
+
throw new Error(`no wrangler config found in ${cfg.rootDir} (wrangler.jsonc, wrangler.json, or wrangler.toml)`);
|
|
1219
|
+
}
|
|
656
1220
|
}
|
|
657
1221
|
|
|
658
1222
|
// src/provision.ts
|
|
@@ -660,25 +1224,60 @@ async function provision(options) {
|
|
|
660
1224
|
const out = options.stdout ?? console;
|
|
661
1225
|
const cfg = await loadProjectConfig(options.configPath);
|
|
662
1226
|
const plan = buildPlan(cfg);
|
|
1227
|
+
const hasDb = cfg.services.includes("db");
|
|
1228
|
+
const hasO11y = cfg.services.includes("o11y");
|
|
1229
|
+
if (options.rotateO11yToken && !hasO11y) {
|
|
1230
|
+
throw new Error("--rotate-o11y-token requires the o11y service in odla.config.mjs");
|
|
1231
|
+
}
|
|
1232
|
+
if (options.pushSecrets && options.writeCredentials === false) {
|
|
1233
|
+
throw new Error("--push-secrets cannot be combined with --no-write-credentials");
|
|
1234
|
+
}
|
|
663
1235
|
out.log(`odla-ai: ${plan.appName} (${plan.appId})`);
|
|
664
1236
|
out.log(` platform: ${plan.platformUrl}`);
|
|
665
1237
|
out.log(` db: ${plan.dbEndpoint}`);
|
|
666
1238
|
out.log(` envs: ${plan.envs.join(", ")}`);
|
|
667
1239
|
out.log(` services: ${plan.services.join(", ")}`);
|
|
668
|
-
const
|
|
669
|
-
|
|
1240
|
+
const productionEnvs = plan.envs.filter((env) => env === "prod" || env === "production");
|
|
1241
|
+
if (!options.dryRun && productionEnvs.length > 0 && !options.yes) {
|
|
1242
|
+
throw new Error(
|
|
1243
|
+
`refusing to provision production env ${productionEnvs.join(", ")} without --yes; run --dry-run first`
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
const schema = hasDb ? await resolveDataExport(cfg, cfg.db?.schema, ["schema", "SCHEMA"]) : void 0;
|
|
1247
|
+
const configuredRules = hasDb ? await resolveDataExport(cfg, cfg.db?.rules, ["rules", "RULES"]) : void 0;
|
|
670
1248
|
const rules = configuredRules ?? (schema && cfg.db?.defaultRules !== false ? rulesFromSchema(schema) : void 0);
|
|
671
1249
|
if (options.dryRun) {
|
|
672
1250
|
out.log("dry run: no network calls or file writes");
|
|
673
1251
|
out.log(` schema: ${schema ? "yes" : "no"}`);
|
|
674
1252
|
out.log(` rules: ${rules ? Object.keys(rules).length : 0} namespaces`);
|
|
675
|
-
out.log(` ai: ${cfg.ai?.provider ?? "not configured"}`);
|
|
1253
|
+
out.log(` ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "not configured" : "not enabled"}`);
|
|
1254
|
+
out.log(` secrets: ${options.pushSecrets ? "push configured Worker secrets" : "local only"}`);
|
|
676
1255
|
return;
|
|
677
1256
|
}
|
|
1257
|
+
let credentials = readCredentials(cfg.local.credentialsFile);
|
|
1258
|
+
if (credentials && credentials.appId !== cfg.app.id) {
|
|
1259
|
+
throw new Error(
|
|
1260
|
+
`credentials at ${displayPath(cfg.local.credentialsFile, cfg.rootDir)} are for "${credentials.appId}", not "${cfg.app.id}"`
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
const rotatesO11y = !!(options.rotateKeys || options.rotateO11yToken);
|
|
1264
|
+
const missingO11y = cfg.envs.some((env) => !credentials?.envs[env]?.o11yToken);
|
|
1265
|
+
const missingDb = cfg.envs.some((env) => !credentials?.envs[env]?.dbKey);
|
|
1266
|
+
const losesShownOnceCredential = hasDb && (!!options.rotateKeys || missingDb) || hasO11y && (rotatesO11y || missingO11y);
|
|
1267
|
+
if (options.writeCredentials === false && losesShownOnceCredential) {
|
|
1268
|
+
throw new Error("credential issuance/rotation requires the private credentials file; remove --no-write-credentials");
|
|
1269
|
+
}
|
|
1270
|
+
const devVarsTarget = resolveWriteDevVarsTarget(cfg, options.writeDevVars);
|
|
1271
|
+
if (cfg.local.gitignore) {
|
|
1272
|
+
ensureGitignore(cfg.rootDir, [cfg.local.tokenFile, cfg.local.credentialsFile, ...devVarsTarget ? [devVarsTarget] : []]);
|
|
1273
|
+
}
|
|
1274
|
+
if (options.pushSecrets) {
|
|
1275
|
+
await preflightSecretsPush({ configPath: cfg.configPath, runner: options.secretRunner });
|
|
1276
|
+
out.log("secrets: Wrangler config and login preflight passed");
|
|
1277
|
+
}
|
|
678
1278
|
const doFetch = options.fetch ?? fetch;
|
|
679
1279
|
const token = await getDeveloperToken(cfg, options, doFetch, out);
|
|
680
|
-
const
|
|
681
|
-
const apps = (0, import_apps.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
|
|
1280
|
+
const apps = (0, import_apps2.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
|
|
682
1281
|
const existing = await readRegistryApp(cfg, token, doFetch);
|
|
683
1282
|
if (existing) {
|
|
684
1283
|
out.log(`app: ${cfg.app.id} already exists`);
|
|
@@ -712,26 +1311,40 @@ async function provision(options) {
|
|
|
712
1311
|
out.log(`${env}: link ${link ? "set" : "cleared"}`);
|
|
713
1312
|
}
|
|
714
1313
|
}
|
|
715
|
-
let credentials = readCredentials(cfg.local.credentialsFile);
|
|
716
1314
|
for (const env of cfg.envs) {
|
|
717
|
-
const tenantId = (0,
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
1315
|
+
const tenantId = (0, import_apps2.tenantIdFor)(cfg.app.id, env);
|
|
1316
|
+
credentials = await provisionEnvCredentials({
|
|
1317
|
+
cfg,
|
|
1318
|
+
env,
|
|
1319
|
+
developerToken: token,
|
|
1320
|
+
credentials,
|
|
1321
|
+
rotateDb: !!options.rotateKeys,
|
|
1322
|
+
rotateO11y: rotatesO11y,
|
|
1323
|
+
write: options.writeCredentials !== false,
|
|
1324
|
+
fetch: doFetch,
|
|
1325
|
+
stdout: out
|
|
1326
|
+
});
|
|
1327
|
+
const dbKey = credentials.envs[env]?.dbKey;
|
|
1328
|
+
if (options.pushSecrets) {
|
|
1329
|
+
try {
|
|
1330
|
+
await secretsPushAfterPreflight({
|
|
1331
|
+
configPath: cfg.configPath,
|
|
1332
|
+
env,
|
|
1333
|
+
yes: options.yes,
|
|
1334
|
+
runner: options.secretRunner,
|
|
1335
|
+
stdout: out
|
|
1336
|
+
});
|
|
1337
|
+
} catch (error) {
|
|
1338
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1339
|
+
const consent = env === "prod" || env === "production" ? " --yes" : "";
|
|
1340
|
+
throw new Error(
|
|
1341
|
+
`${message}
|
|
1342
|
+
${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}${consent}" without issuing or rotating again`,
|
|
1343
|
+
{ cause: error }
|
|
1344
|
+
);
|
|
732
1345
|
}
|
|
733
1346
|
}
|
|
734
|
-
if (schema) {
|
|
1347
|
+
if (schema && dbKey) {
|
|
735
1348
|
await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
|
|
736
1349
|
out.log(`${env}: schema pushed`);
|
|
737
1350
|
}
|
|
@@ -739,8 +1352,8 @@ async function provision(options) {
|
|
|
739
1352
|
await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/admin/rules`, token, rules);
|
|
740
1353
|
out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
|
|
741
1354
|
}
|
|
742
|
-
if (cfg.ai?.provider && cfg.ai.keyEnv) {
|
|
743
|
-
const key =
|
|
1355
|
+
if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
|
|
1356
|
+
const key = import_node_process4.default.env[cfg.ai.keyEnv];
|
|
744
1357
|
if (key) {
|
|
745
1358
|
const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
|
|
746
1359
|
await (0, import_ai.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
|
|
@@ -749,36 +1362,17 @@ async function provision(options) {
|
|
|
749
1362
|
out.log(`${env}: ${cfg.ai.keyEnv} not set; skipped provider key storage`);
|
|
750
1363
|
}
|
|
751
1364
|
}
|
|
752
|
-
credentials = mergeCredential(credentials, {
|
|
753
|
-
appId: cfg.app.id,
|
|
754
|
-
platformUrl: cfg.platformUrl,
|
|
755
|
-
dbEndpoint: cfg.dbEndpoint,
|
|
756
|
-
env,
|
|
757
|
-
tenantId,
|
|
758
|
-
dbKey,
|
|
759
|
-
...o11yToken ? { o11yToken } : {}
|
|
760
|
-
});
|
|
761
1365
|
}
|
|
762
|
-
if (cfg.local.gitignore) ensureGitignore(cfg.rootDir);
|
|
763
1366
|
if (options.writeCredentials !== false && credentials) {
|
|
764
|
-
|
|
765
|
-
out.log(`credentials: wrote ${displayPath(cfg.local.credentialsFile, cfg.rootDir)} (0600, gitignored)`);
|
|
1367
|
+
const ignored = cfg.local.gitignore && gitignoreEntry(cfg.rootDir, cfg.local.credentialsFile);
|
|
1368
|
+
out.log(`credentials: wrote ${displayPath(cfg.local.credentialsFile, cfg.rootDir)} (0600${ignored ? ", gitignored" : ""})`);
|
|
766
1369
|
}
|
|
767
|
-
const devVarsTarget = resolveWriteDevVarsTarget(cfg, options.writeDevVars);
|
|
768
1370
|
if (devVarsTarget && credentials) {
|
|
769
1371
|
const env = cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
|
|
770
1372
|
writeDevVars(devVarsTarget, credentials, env, o11yDevVars(cfg));
|
|
771
1373
|
out.log(`dev vars: wrote ${displayPath(devVarsTarget, cfg.rootDir)} for ${env}`);
|
|
772
1374
|
}
|
|
773
1375
|
}
|
|
774
|
-
function o11yDevVars(cfg) {
|
|
775
|
-
if (!cfg.services.includes("o11y")) return void 0;
|
|
776
|
-
return {
|
|
777
|
-
endpoint: cfg.o11y?.endpoint ?? "https://o11y.odla.ai",
|
|
778
|
-
service: cfg.o11y?.service ?? cfg.app.id,
|
|
779
|
-
...cfg.o11y?.version ? { version: cfg.o11y.version } : {}
|
|
780
|
-
};
|
|
781
|
-
}
|
|
782
1376
|
async function readRegistryApp(cfg, token, doFetch) {
|
|
783
1377
|
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}`, {
|
|
784
1378
|
headers: { authorization: `Bearer ${token}` }
|
|
@@ -788,50 +1382,13 @@ async function readRegistryApp(cfg, token, doFetch) {
|
|
|
788
1382
|
const json = await res.json();
|
|
789
1383
|
return json.app ?? null;
|
|
790
1384
|
}
|
|
791
|
-
async function mintDbKey(opts) {
|
|
792
|
-
let res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(opts.tenantId)}/keys`, {
|
|
793
|
-
method: "POST",
|
|
794
|
-
headers: opts.auth,
|
|
795
|
-
body: "{}"
|
|
796
|
-
});
|
|
797
|
-
if (res.status === 404) {
|
|
798
|
-
const created = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps`, {
|
|
799
|
-
method: "POST",
|
|
800
|
-
headers: opts.auth,
|
|
801
|
-
body: JSON.stringify({
|
|
802
|
-
name: opts.env === "prod" ? opts.cfg.app.name : `${opts.cfg.app.name} (${opts.env})`,
|
|
803
|
-
appId: opts.tenantId
|
|
804
|
-
})
|
|
805
|
-
});
|
|
806
|
-
if (!created.ok) throw new Error(`db app create (${opts.tenantId}) failed: ${created.status} ${await safeText(created)}`);
|
|
807
|
-
res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(opts.tenantId)}/keys`, {
|
|
808
|
-
method: "POST",
|
|
809
|
-
headers: opts.auth,
|
|
810
|
-
body: "{}"
|
|
811
|
-
});
|
|
812
|
-
}
|
|
813
|
-
if (!res.ok) throw new Error(`db key mint (${opts.tenantId}) failed: ${res.status} ${await safeText(res)}`);
|
|
814
|
-
const body = await res.json();
|
|
815
|
-
if (!body.key) throw new Error(`db key mint (${opts.tenantId}) returned no key`);
|
|
816
|
-
return body.key;
|
|
817
|
-
}
|
|
818
|
-
async function mintO11yToken(cfg, env, token, doFetch) {
|
|
819
|
-
const res = await doFetch(
|
|
820
|
-
`${cfg.platformUrl}/o11y/${encodeURIComponent(cfg.app.id)}/token?env=${encodeURIComponent(env)}`,
|
|
821
|
-
{ method: "POST", headers: { authorization: `Bearer ${token}` } }
|
|
822
|
-
);
|
|
823
|
-
if (!res.ok) throw new Error(`o11y token mint (${env}) failed: ${res.status} ${await safeText(res)}`);
|
|
824
|
-
const body = await res.json();
|
|
825
|
-
if (!body.token) throw new Error(`o11y token mint (${env}) returned no token`);
|
|
826
|
-
return body.token;
|
|
827
|
-
}
|
|
828
1385
|
async function postJson(doFetch, url, bearer, body) {
|
|
829
1386
|
const res = await doFetch(url, {
|
|
830
1387
|
method: "POST",
|
|
831
1388
|
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
832
1389
|
body: JSON.stringify(body)
|
|
833
1390
|
});
|
|
834
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await
|
|
1391
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText2(res)}`);
|
|
835
1392
|
}
|
|
836
1393
|
function normalizeClerkConfig(value) {
|
|
837
1394
|
if (!value) return null;
|
|
@@ -848,12 +1405,7 @@ function defaultSecretName(provider) {
|
|
|
848
1405
|
const names = import_ai.DEFAULT_SECRET_NAMES;
|
|
849
1406
|
return names[provider] ?? `${provider}_api_key`;
|
|
850
1407
|
}
|
|
851
|
-
function
|
|
852
|
-
if (!requested) return null;
|
|
853
|
-
if (requested === true) return cfg.local.devVarsFile;
|
|
854
|
-
return (0, import_node_path6.resolve)((0, import_node_path6.dirname)(cfg.configPath), requested);
|
|
855
|
-
}
|
|
856
|
-
async function safeText(res) {
|
|
1408
|
+
async function safeText2(res) {
|
|
857
1409
|
try {
|
|
858
1410
|
return redactSecrets((await res.text()).slice(0, 500));
|
|
859
1411
|
} catch {
|
|
@@ -861,103 +1413,401 @@ async function safeText(res) {
|
|
|
861
1413
|
}
|
|
862
1414
|
}
|
|
863
1415
|
|
|
864
|
-
// src/
|
|
865
|
-
var
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
throw new Error(`env "${env}" is not in config envs (${cfg.envs.join(", ")})`);
|
|
872
|
-
}
|
|
873
|
-
if (PROD_ENV_NAMES.has(env) && !options.yes) {
|
|
874
|
-
throw new Error(`refusing to push a secret to "${env}" without --yes`);
|
|
875
|
-
}
|
|
876
|
-
const credentialsPath = displayPath(cfg.local.credentialsFile, cfg.rootDir);
|
|
877
|
-
const credentials = readCredentials(cfg.local.credentialsFile);
|
|
878
|
-
if (!credentials) throw new Error(`no credentials at ${credentialsPath} \u2014 run "odla-ai provision" first`);
|
|
879
|
-
if (credentials.appId !== cfg.app.id) {
|
|
880
|
-
throw new Error(`credentials at ${credentialsPath} are for "${credentials.appId}", not "${cfg.app.id}"`);
|
|
881
|
-
}
|
|
882
|
-
const dbKey = credentials.envs[env]?.dbKey;
|
|
883
|
-
if (!dbKey) throw new Error(`no db key for env "${env}" in ${credentialsPath} \u2014 run "odla-ai provision" first`);
|
|
884
|
-
const wranglerConfig = findWranglerConfig(cfg.rootDir);
|
|
885
|
-
if (!wranglerConfig) {
|
|
886
|
-
throw new Error(`no wrangler config found in ${cfg.rootDir} (wrangler.jsonc, wrangler.json, or wrangler.toml)`);
|
|
887
|
-
}
|
|
888
|
-
const wranglerEnv = PROD_ENV_NAMES.has(env) ? void 0 : env;
|
|
889
|
-
const target = wranglerEnv ? `wrangler env "${wranglerEnv}"` : "the top-level (prod) wrangler env";
|
|
890
|
-
const secrets = [{ name: "ODLA_API_KEY", value: dbKey }];
|
|
891
|
-
const o11yToken = credentials.envs[env]?.o11yToken;
|
|
892
|
-
if (o11yToken) secrets.push({ name: "ODLA_O11Y_TOKEN", value: o11yToken });
|
|
893
|
-
if (options.dryRun) {
|
|
894
|
-
for (const s of secrets) out.log(`dry run: would push ${s.name} (${redactSecrets(s.value)}) to ${target}`);
|
|
895
|
-
return;
|
|
896
|
-
}
|
|
897
|
-
const run = options.runner ?? defaultRunner;
|
|
898
|
-
if (!await wranglerLoggedIn(run, cfg.rootDir)) {
|
|
899
|
-
throw new Error(`wrangler is not logged in \u2014 run "wrangler login" (a browser step for the human)`);
|
|
1416
|
+
// src/security.ts
|
|
1417
|
+
var import_node_path6 = require("path");
|
|
1418
|
+
var import_security = require("@odla-ai/security");
|
|
1419
|
+
var import_node = require("@odla-ai/security/node");
|
|
1420
|
+
async function runHostedSecurity(options) {
|
|
1421
|
+
if (options.sourceDisclosureAck !== "redacted") {
|
|
1422
|
+
throw new Error("Hosted security requires sourceDisclosureAck=redacted before repository source may leave this process");
|
|
900
1423
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1424
|
+
const selfAudit = options.selfAudit === true;
|
|
1425
|
+
const cfg = selfAudit ? void 0 : await loadProjectConfig(options.configPath ?? "odla.config.mjs");
|
|
1426
|
+
const appId = selfAudit ? "odla-ai" : cfg.app.id;
|
|
1427
|
+
const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
|
|
1428
|
+
const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
|
|
1429
|
+
const target = (0, import_node_path6.resolve)(options.target ?? cfg?.rootDir ?? ".");
|
|
1430
|
+
const output = (0, import_node_path6.resolve)(options.out ?? (0, import_node_path6.resolve)(target, ".odla/security/hosted"));
|
|
1431
|
+
const outputRelative = (0, import_node_path6.relative)(target, output).split(import_node_path6.sep).join("/");
|
|
1432
|
+
if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
|
|
1433
|
+
const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
|
|
1434
|
+
const tokenRequest = {
|
|
1435
|
+
platform,
|
|
1436
|
+
appId,
|
|
1437
|
+
env,
|
|
1438
|
+
selfAudit,
|
|
1439
|
+
scope: selfAudit ? "platform:security:self" : null
|
|
1440
|
+
};
|
|
1441
|
+
const token = await injectedToken(options, tokenRequest);
|
|
1442
|
+
const snapshot = await (0, import_node.snapshotDirectory)(target, {
|
|
1443
|
+
exclude: !outputRelative.startsWith("../") && !(0, import_node_path6.isAbsolute)(outputRelative) ? [outputRelative] : []
|
|
1444
|
+
});
|
|
1445
|
+
const hosted = await (0, import_security.createPlatformSecurityReasoners)({
|
|
1446
|
+
platform,
|
|
1447
|
+
token,
|
|
1448
|
+
appId,
|
|
1449
|
+
env,
|
|
1450
|
+
repository: snapshot.repository,
|
|
1451
|
+
revision: snapshot.revision,
|
|
1452
|
+
snapshotDigest: snapshot.digest,
|
|
1453
|
+
sourceDisclosure: "redacted",
|
|
1454
|
+
clientRunId: options.runId ?? crypto.randomUUID(),
|
|
1455
|
+
...selfAudit ? { selfAudit: { enabled: true, subject: "service:odla-security-self-audit" } } : {},
|
|
1456
|
+
fetch: options.fetch,
|
|
1457
|
+
signal: options.signal
|
|
1458
|
+
});
|
|
1459
|
+
const harness = (0, import_security.createSecurityHarness)({
|
|
1460
|
+
profile,
|
|
1461
|
+
store: new import_node.FileRunStore((0, import_node_path6.resolve)(output, "state")),
|
|
1462
|
+
discoveryReasoner: hosted.discoveryReasoner,
|
|
1463
|
+
validationReasoner: hosted.validationReasoner,
|
|
1464
|
+
policy: {
|
|
1465
|
+
modelSourceDisclosure: "redacted",
|
|
1466
|
+
active: false,
|
|
1467
|
+
allowNetwork: false
|
|
905
1468
|
}
|
|
906
|
-
|
|
1469
|
+
});
|
|
1470
|
+
const report = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
|
|
1471
|
+
await (0, import_node.writeSecurityArtifacts)(output, report);
|
|
1472
|
+
const reportDigest = await (0, import_security.securityFingerprint)(report);
|
|
1473
|
+
await hosted.complete({
|
|
1474
|
+
reportDigest,
|
|
1475
|
+
coverageStatus: report.coverageStatus,
|
|
1476
|
+
confirmed: report.metrics.confirmed,
|
|
1477
|
+
candidates: report.metrics.candidates
|
|
1478
|
+
}, { signal: options.signal });
|
|
1479
|
+
printSummary(options.stdout ?? console, appId, env, hosted.run, report, output);
|
|
1480
|
+
return Object.freeze({ report, run: hosted.run, output });
|
|
1481
|
+
}
|
|
1482
|
+
function selectEnv(requested, declared, configPath, rootDir) {
|
|
1483
|
+
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
1484
|
+
if (!env || !declared.includes(env)) {
|
|
1485
|
+
const shown = (0, import_node_path6.relative)(rootDir, configPath) || configPath;
|
|
1486
|
+
throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
|
|
1487
|
+
}
|
|
1488
|
+
return env;
|
|
1489
|
+
}
|
|
1490
|
+
async function injectedToken(options, request) {
|
|
1491
|
+
const value = options.token ?? await options.getToken?.(Object.freeze({ ...request }));
|
|
1492
|
+
if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
|
|
1493
|
+
throw new Error(
|
|
1494
|
+
request.selfAudit ? "Self-audit requires an injected, scoped platform security token" : "Hosted security requires an injected app developer token or getToken callback"
|
|
1495
|
+
);
|
|
907
1496
|
}
|
|
1497
|
+
return value;
|
|
1498
|
+
}
|
|
1499
|
+
function profileFor(name, maxHuntTasks) {
|
|
1500
|
+
const profile = name === "odla" ? (0, import_security.odlaProfile)() : name === "cloudflare-app" ? (0, import_security.cloudflareAppProfile)() : name === "generic" ? (0, import_security.genericProfile)() : void 0;
|
|
1501
|
+
if (!profile) throw new Error(`unknown security profile "${String(name)}"`);
|
|
1502
|
+
if (maxHuntTasks === void 0) return profile;
|
|
1503
|
+
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
1504
|
+
return { ...profile, maxHuntTasks };
|
|
1505
|
+
}
|
|
1506
|
+
function printSummary(out, appId, env, run, report, output) {
|
|
1507
|
+
const complete = report.coverage.filter((cell) => cell.state === "complete").length;
|
|
1508
|
+
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
1509
|
+
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
1510
|
+
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
1511
|
+
out.log(` coverage: ${report.coverageStatus} ${complete}/${report.coverage.length} blocked=${report.metrics.blockedCells} shallow=${report.metrics.shallowCells} unscheduled=${report.metrics.unscheduledCells}`);
|
|
1512
|
+
out.log(` findings: confirmed=${report.metrics.confirmed} needs_reproduction=${report.metrics.needsReproduction} candidates=${report.metrics.candidates}`);
|
|
1513
|
+
out.log(` report: ${(0, import_node_path6.resolve)(output, "REPORT.md")}`);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// src/help.ts
|
|
1517
|
+
var import_node_fs7 = require("fs");
|
|
1518
|
+
function cliVersion() {
|
|
1519
|
+
const pkg = JSON.parse((0, import_node_fs7.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
|
|
1520
|
+
return pkg.version ?? "unknown";
|
|
1521
|
+
}
|
|
1522
|
+
function printHelp() {
|
|
1523
|
+
console.log(`odla-ai
|
|
1524
|
+
|
|
1525
|
+
Usage:
|
|
1526
|
+
odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
|
|
1527
|
+
odla-ai init --app-id <id> --name <name> [--services db,ai,o11y] [--env dev --env prod]
|
|
1528
|
+
odla-ai doctor [--config odla.config.mjs]
|
|
1529
|
+
odla-ai capabilities [--json]
|
|
1530
|
+
odla-ai admin ai show [--platform https://odla.ai] [--json]
|
|
1531
|
+
odla-ai admin ai set <purpose> --provider <id> --model <id> [--enabled|--no-enabled]
|
|
1532
|
+
odla-ai admin ai credentials [--json]
|
|
1533
|
+
odla-ai admin ai credential set <provider> (--from-env <NAME>|--stdin)
|
|
1534
|
+
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
1535
|
+
odla-ai security run [target] --self --ack-redacted-source
|
|
1536
|
+
odla-ai provision [--config odla.config.mjs] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
1537
|
+
odla-ai smoke [--config odla.config.mjs] [--env dev]
|
|
1538
|
+
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
1539
|
+
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
1540
|
+
odla-ai version
|
|
1541
|
+
|
|
1542
|
+
Commands:
|
|
1543
|
+
setup Install offline odla runbooks for common coding-agent harnesses.
|
|
1544
|
+
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
1545
|
+
doctor Validate and summarize the project config without network calls.
|
|
1546
|
+
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
1547
|
+
admin Manage platform-funded AI routing with a narrow admin-approved device grant.
|
|
1548
|
+
security Run hosted discovery + independent validation with app/run attribution.
|
|
1549
|
+
provision Register services, configure them, persist credentials, optionally push secrets.
|
|
1550
|
+
smoke Verify local credentials, public-config, live schema, and db aggregate.
|
|
1551
|
+
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
1552
|
+
copilot, gemini, or agents (repeatable or comma-separated).
|
|
1553
|
+
secrets Push configured db/o11y secrets into the Worker via wrangler stdin.
|
|
1554
|
+
version Print the CLI version.
|
|
1555
|
+
|
|
1556
|
+
Safety:
|
|
1557
|
+
New projects target dev only. Add prod explicitly to odla.config.mjs and pass
|
|
1558
|
+
--yes to provision it; use --dry-run first to inspect the resolved plan.
|
|
1559
|
+
Provision caches the approved developer token and service credentials under
|
|
1560
|
+
.odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
|
|
1561
|
+
preflights Wrangler before any shown-once issuance or destructive rotation.
|
|
1562
|
+
Provision opens the approval page automatically in interactive terminals;
|
|
1563
|
+
use --open to force browser launch or --no-open to suppress it.
|
|
1564
|
+
`);
|
|
908
1565
|
}
|
|
909
1566
|
|
|
910
1567
|
// src/skill.ts
|
|
911
|
-
var
|
|
1568
|
+
var import_node_fs8 = require("fs");
|
|
912
1569
|
var import_node_os = require("os");
|
|
913
1570
|
var import_node_path7 = require("path");
|
|
914
1571
|
var import_node_url2 = require("url");
|
|
1572
|
+
|
|
1573
|
+
// src/skill-adapters.ts
|
|
1574
|
+
var PROJECT_INSTRUCTIONS = `<!-- odla-ai agent setup:start -->
|
|
1575
|
+
## odla agent workflow
|
|
1576
|
+
|
|
1577
|
+
For work that creates an odla app or adds odla services, read and follow
|
|
1578
|
+
\`.agents/skills/odla/SKILL.md\`. It dispatches an existing static site to
|
|
1579
|
+
\`.agents/skills/odla-migrate/SKILL.md\`. For production telemetry triage, use
|
|
1580
|
+
\`.agents/skills/odla-o11y-debug/SKILL.md\`.
|
|
1581
|
+
|
|
1582
|
+
The complete runbooks and their references are installed in this repository.
|
|
1583
|
+
Do not fetch online odla documentation as setup context. Network access is only
|
|
1584
|
+
needed when a runbook deliberately calls the odla service, npm, Cloudflare, or
|
|
1585
|
+
another configured provider.
|
|
1586
|
+
<!-- odla-ai agent setup:end -->`;
|
|
1587
|
+
var CURSOR_RULE = `---
|
|
1588
|
+
description: Build, migrate, provision, secure, or debug an odla app using the locally installed odla runbooks
|
|
1589
|
+
globs:
|
|
1590
|
+
alwaysApply: false
|
|
1591
|
+
---
|
|
1592
|
+
|
|
1593
|
+
${PROJECT_INSTRUCTIONS}
|
|
1594
|
+
`;
|
|
1595
|
+
function claudeAdapter(skill, canonical) {
|
|
1596
|
+
const match = canonical.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
1597
|
+
if (!match) throw new Error(`bundled skill ${skill} has no YAML frontmatter`);
|
|
1598
|
+
const lines = match[1].split(/\r?\n/);
|
|
1599
|
+
const frontmatter = [];
|
|
1600
|
+
let keepIndented = false;
|
|
1601
|
+
for (const line of lines) {
|
|
1602
|
+
if (line.startsWith("name:") || line.startsWith("description:")) {
|
|
1603
|
+
frontmatter.push(line);
|
|
1604
|
+
keepIndented = line.startsWith("description:");
|
|
1605
|
+
} else if (keepIndented && /^\s+/.test(line)) {
|
|
1606
|
+
frontmatter.push(line);
|
|
1607
|
+
} else {
|
|
1608
|
+
keepIndented = false;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
return `---
|
|
1612
|
+
${frontmatter.join("\n")}
|
|
1613
|
+
---
|
|
1614
|
+
|
|
1615
|
+
# ${skill} (odla adapter)
|
|
1616
|
+
|
|
1617
|
+
Read \`../../../.agents/skills/${skill}/SKILL.md\` completely and follow it. That
|
|
1618
|
+
canonical local skill owns every reference path and contains the full offline
|
|
1619
|
+
runbook. Do not substitute a remote documentation page for the installed copy.
|
|
1620
|
+
`;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
// src/skill.ts
|
|
1624
|
+
var AGENT_HARNESSES = ["claude", "codex", "cursor", "copilot", "gemini", "agents"];
|
|
915
1625
|
function installSkill(options = {}) {
|
|
916
1626
|
const out = options.stdout ?? console;
|
|
917
1627
|
const sourceDir = options.sourceDir ?? (0, import_node_url2.fileURLToPath)(new URL("../skills", importMetaUrl));
|
|
918
|
-
const targetDir = options.global ? (0, import_node_path7.join)(options.homeDir ?? (0, import_node_os.homedir)(), ".claude", "skills") : (0, import_node_path7.resolve)(options.dir ?? process.cwd(), ".claude", "skills");
|
|
919
1628
|
const files = listFiles(sourceDir);
|
|
920
1629
|
if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
|
|
921
|
-
const
|
|
922
|
-
const
|
|
923
|
-
const
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1630
|
+
const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
|
|
1631
|
+
const root = (0, import_node_path7.resolve)(options.dir ?? process.cwd());
|
|
1632
|
+
const home = (0, import_node_path7.resolve)(options.homeDir ?? (0, import_node_os.homedir)());
|
|
1633
|
+
const plans = /* @__PURE__ */ new Map();
|
|
1634
|
+
const targets = /* @__PURE__ */ new Map();
|
|
1635
|
+
const rememberTarget = (harness, target) => {
|
|
1636
|
+
const current = targets.get(harness) ?? /* @__PURE__ */ new Set();
|
|
1637
|
+
current.add(target);
|
|
1638
|
+
targets.set(harness, current);
|
|
1639
|
+
};
|
|
1640
|
+
const plan = (target, content, managedMerge = false, boundary = root) => {
|
|
1641
|
+
const existing = plans.get(target);
|
|
1642
|
+
if (existing && existing.content !== content) throw new Error(`internal agent setup conflict for ${target}`);
|
|
1643
|
+
plans.set(target, { target, content, boundary, managedMerge });
|
|
1644
|
+
};
|
|
1645
|
+
const planSkillTree = (targetDir2, boundary = root) => {
|
|
1646
|
+
for (const rel of files) plan((0, import_node_path7.join)(targetDir2, rel), (0, import_node_fs8.readFileSync)((0, import_node_path7.join)(sourceDir, rel), "utf8"), false, boundary);
|
|
1647
|
+
};
|
|
1648
|
+
let targetDir;
|
|
1649
|
+
if (options.global) {
|
|
1650
|
+
const claudeRoot = (0, import_node_path7.join)(home, ".claude", "skills");
|
|
1651
|
+
const codexRoot = (0, import_node_path7.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path7.join)(home, ".codex"), "skills");
|
|
1652
|
+
targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
|
|
1653
|
+
for (const harness of harnesses) {
|
|
1654
|
+
const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
|
|
1655
|
+
planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path7.dirname)((0, import_node_path7.dirname)(codexRoot)));
|
|
1656
|
+
rememberTarget(harness, skillRoot);
|
|
1657
|
+
}
|
|
1658
|
+
} else {
|
|
1659
|
+
const sharedRoot = (0, import_node_path7.join)(root, ".agents", "skills");
|
|
1660
|
+
planSkillTree(sharedRoot);
|
|
1661
|
+
const claudeRoot = (0, import_node_path7.join)(root, ".claude", "skills");
|
|
1662
|
+
targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
|
|
1663
|
+
for (const harness of harnesses) rememberTarget(harness, sharedRoot);
|
|
1664
|
+
if (harnesses.includes("claude")) {
|
|
1665
|
+
for (const skill of skillNames(files)) {
|
|
1666
|
+
const canonical = (0, import_node_fs8.readFileSync)((0, import_node_path7.join)(sourceDir, skill, "SKILL.md"), "utf8");
|
|
1667
|
+
plan((0, import_node_path7.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
|
|
936
1668
|
}
|
|
1669
|
+
rememberTarget("claude", claudeRoot);
|
|
1670
|
+
}
|
|
1671
|
+
if (harnesses.includes("cursor")) {
|
|
1672
|
+
const cursorRule = (0, import_node_path7.join)(root, ".cursor", "rules", "odla.mdc");
|
|
1673
|
+
plan(cursorRule, CURSOR_RULE);
|
|
1674
|
+
rememberTarget("cursor", cursorRule);
|
|
1675
|
+
}
|
|
1676
|
+
if (harnesses.includes("agents")) {
|
|
1677
|
+
const agentsFile = (0, import_node_path7.join)(root, "AGENTS.md");
|
|
1678
|
+
plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
1679
|
+
rememberTarget("agents", agentsFile);
|
|
1680
|
+
}
|
|
1681
|
+
if (harnesses.includes("copilot")) {
|
|
1682
|
+
const copilotFile = (0, import_node_path7.join)(root, ".github", "copilot-instructions.md");
|
|
1683
|
+
plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
1684
|
+
rememberTarget("copilot", copilotFile);
|
|
1685
|
+
}
|
|
1686
|
+
if (harnesses.includes("gemini")) {
|
|
1687
|
+
const geminiFile = (0, import_node_path7.join)(root, "GEMINI.md");
|
|
1688
|
+
plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
1689
|
+
rememberTarget("gemini", geminiFile);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
const writtenPaths = /* @__PURE__ */ new Set();
|
|
1693
|
+
const unchangedPaths = /* @__PURE__ */ new Set();
|
|
1694
|
+
const conflicts = [];
|
|
1695
|
+
for (const file of plans.values()) {
|
|
1696
|
+
const symlink = symlinkedComponent(file.boundary, file.target);
|
|
1697
|
+
if (symlink) {
|
|
1698
|
+
conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
|
|
1699
|
+
continue;
|
|
1700
|
+
}
|
|
1701
|
+
if (!(0, import_node_fs8.existsSync)(file.target)) {
|
|
1702
|
+
writtenPaths.add(file.target);
|
|
1703
|
+
continue;
|
|
1704
|
+
}
|
|
1705
|
+
const current = (0, import_node_fs8.readFileSync)(file.target, "utf8");
|
|
1706
|
+
if (current === file.content) {
|
|
1707
|
+
unchangedPaths.add(file.target);
|
|
1708
|
+
} else if (file.managedMerge || options.force) {
|
|
1709
|
+
writtenPaths.add(file.target);
|
|
1710
|
+
unchangedPaths.delete(file.target);
|
|
1711
|
+
} else {
|
|
1712
|
+
conflicts.push(file.target);
|
|
937
1713
|
}
|
|
938
|
-
written.push(rel);
|
|
939
1714
|
}
|
|
940
1715
|
if (conflicts.length > 0) {
|
|
941
1716
|
throw new Error(
|
|
942
|
-
`
|
|
943
|
-
${conflicts.map((f) => ` - ${
|
|
1717
|
+
`agent setup files modified locally (re-run with --force to replace only odla-managed content):
|
|
1718
|
+
${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
944
1719
|
);
|
|
945
1720
|
}
|
|
946
|
-
for (const
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1721
|
+
for (const file of plans.values()) {
|
|
1722
|
+
if (!(0, import_node_fs8.existsSync)(file.target) || (0, import_node_fs8.readFileSync)(file.target, "utf8") !== file.content) {
|
|
1723
|
+
(0, import_node_fs8.mkdirSync)((0, import_node_path7.dirname)(file.target), { recursive: true });
|
|
1724
|
+
(0, import_node_fs8.writeFileSync)(file.target, file.content);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
const skills = skillNames(files);
|
|
1728
|
+
const installations = harnesses.map((harness) => ({ harness, targets: [...targets.get(harness) ?? []] }));
|
|
1729
|
+
out.log(`agent harnesses: ${harnesses.join(", ")}`);
|
|
1730
|
+
out.log(`offline skills: ${skills.join(", ")}`);
|
|
1731
|
+
for (const installation of installations) out.log(`${installation.harness}: ${installation.targets.join(", ")}`);
|
|
1732
|
+
out.log(`installed ${writtenPaths.size} managed file(s)${unchangedPaths.size ? ` (${unchangedPaths.size} unchanged)` : ""}`);
|
|
1733
|
+
return {
|
|
1734
|
+
targetDir,
|
|
1735
|
+
written: pathsUnder(targetDir, writtenPaths),
|
|
1736
|
+
unchanged: pathsUnder(targetDir, unchangedPaths),
|
|
1737
|
+
writtenPaths: [...writtenPaths].sort(),
|
|
1738
|
+
unchangedPaths: [...unchangedPaths].sort(),
|
|
1739
|
+
harnesses: installations
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
function pathsUnder(root, paths) {
|
|
1743
|
+
return [...paths].map((path) => (0, import_node_path7.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path7.sep}`) && !(0, import_node_path7.isAbsolute)(path)).sort();
|
|
1744
|
+
}
|
|
1745
|
+
function normalizeHarnesses(values, global) {
|
|
1746
|
+
const requested = values?.length ? values : ["claude"];
|
|
1747
|
+
const allowed = /* @__PURE__ */ new Set([...AGENT_HARNESSES, "all"]);
|
|
1748
|
+
for (const harness of requested) {
|
|
1749
|
+
if (!allowed.has(harness)) {
|
|
1750
|
+
throw new Error(`unknown agent harness "${harness}"; choose all, ${AGENT_HARNESSES.join(", ")}`);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
const expanded = requested.includes("all") ? global ? ["claude", "codex"] : [...AGENT_HARNESSES] : [...new Set(requested)];
|
|
1754
|
+
if (global) {
|
|
1755
|
+
const unsupported = expanded.filter((harness) => harness !== "claude" && harness !== "codex");
|
|
1756
|
+
if (unsupported.length) {
|
|
1757
|
+
throw new Error(`--global supports claude and codex only; ${unsupported.join(", ")} require a project-local adapter`);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
return expanded;
|
|
1761
|
+
}
|
|
1762
|
+
function managedFileContent(path, block, force, boundary) {
|
|
1763
|
+
const symlink = symlinkedComponent(boundary, path);
|
|
1764
|
+
if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
|
|
1765
|
+
if (!(0, import_node_fs8.existsSync)(path)) return `${block}
|
|
1766
|
+
`;
|
|
1767
|
+
const current = (0, import_node_fs8.readFileSync)(path, "utf8");
|
|
1768
|
+
const start = "<!-- odla-ai agent setup:start -->";
|
|
1769
|
+
const end = "<!-- odla-ai agent setup:end -->";
|
|
1770
|
+
const startAt = current.indexOf(start);
|
|
1771
|
+
const endAt = current.indexOf(end);
|
|
1772
|
+
if (startAt === -1 !== (endAt === -1) || startAt !== -1 && endAt < startAt) {
|
|
1773
|
+
throw new Error(`malformed odla-managed section in ${path}`);
|
|
1774
|
+
}
|
|
1775
|
+
if (startAt === -1) {
|
|
1776
|
+
const separator = current.length === 0 || current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n";
|
|
1777
|
+
return `${current}${separator}${block}
|
|
1778
|
+
`;
|
|
1779
|
+
}
|
|
1780
|
+
const afterEnd = endAt + end.length;
|
|
1781
|
+
const existing = current.slice(startAt, afterEnd);
|
|
1782
|
+
if (existing !== block && !force) {
|
|
1783
|
+
throw new Error(`odla-managed section modified locally in ${path}; re-run with --force to replace that section`);
|
|
1784
|
+
}
|
|
1785
|
+
return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
|
|
1786
|
+
}
|
|
1787
|
+
function symlinkedComponent(boundary, target) {
|
|
1788
|
+
const rel = (0, import_node_path7.relative)(boundary, target);
|
|
1789
|
+
if (rel === ".." || rel.startsWith(`..${import_node_path7.sep}`) || (0, import_node_path7.isAbsolute)(rel)) {
|
|
1790
|
+
throw new Error(`agent setup target escapes its install root: ${target}`);
|
|
1791
|
+
}
|
|
1792
|
+
let current = boundary;
|
|
1793
|
+
for (const part of rel.split(import_node_path7.sep).filter(Boolean)) {
|
|
1794
|
+
current = (0, import_node_path7.join)(current, part);
|
|
1795
|
+
try {
|
|
1796
|
+
if ((0, import_node_fs8.lstatSync)(current).isSymbolicLink()) return current;
|
|
1797
|
+
} catch (error) {
|
|
1798
|
+
if (error.code !== "ENOENT") throw error;
|
|
1799
|
+
}
|
|
950
1800
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
return
|
|
1801
|
+
return void 0;
|
|
1802
|
+
}
|
|
1803
|
+
function skillNames(files) {
|
|
1804
|
+
return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
|
|
955
1805
|
}
|
|
956
1806
|
function listFiles(dir) {
|
|
957
|
-
if (!(0,
|
|
1807
|
+
if (!(0, import_node_fs8.existsSync)(dir)) return [];
|
|
958
1808
|
const results = [];
|
|
959
1809
|
const walk = (current) => {
|
|
960
|
-
for (const entry of (0,
|
|
1810
|
+
for (const entry of (0, import_node_fs8.readdirSync)(current, { withFileTypes: true })) {
|
|
961
1811
|
const path = (0, import_node_path7.join)(current, entry.name);
|
|
962
1812
|
if (entry.isDirectory()) walk(path);
|
|
963
1813
|
else results.push((0, import_node_path7.relative)(dir, path));
|
|
@@ -1023,7 +1873,7 @@ async function getJson(doFetch, url, bearer) {
|
|
|
1023
1873
|
const res = await doFetch(url, {
|
|
1024
1874
|
headers: bearer ? { authorization: `Bearer ${bearer}` } : void 0
|
|
1025
1875
|
});
|
|
1026
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await
|
|
1876
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText3(res)}`);
|
|
1027
1877
|
return res.json();
|
|
1028
1878
|
}
|
|
1029
1879
|
async function postJson2(doFetch, url, bearer, body) {
|
|
@@ -1032,7 +1882,7 @@ async function postJson2(doFetch, url, bearer, body) {
|
|
|
1032
1882
|
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
1033
1883
|
body: JSON.stringify(body)
|
|
1034
1884
|
});
|
|
1035
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await
|
|
1885
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText3(res)}`);
|
|
1036
1886
|
return res.json();
|
|
1037
1887
|
}
|
|
1038
1888
|
function publicConfigUrl(platformUrl, appId, env) {
|
|
@@ -1040,7 +1890,7 @@ function publicConfigUrl(platformUrl, appId, env) {
|
|
|
1040
1890
|
url.searchParams.set("env", env);
|
|
1041
1891
|
return url.toString();
|
|
1042
1892
|
}
|
|
1043
|
-
async function
|
|
1893
|
+
async function safeText3(res) {
|
|
1044
1894
|
try {
|
|
1045
1895
|
return (await res.text()).slice(0, 500);
|
|
1046
1896
|
} catch {
|
|
@@ -1053,14 +1903,17 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
1053
1903
|
const parsed = parseArgv(argv);
|
|
1054
1904
|
const command = parsed.positionals[0] ?? "help";
|
|
1055
1905
|
if (command === "version" || command === "-v" || parsed.options.version === true) {
|
|
1906
|
+
assertArgs(parsed, ["version"], 1);
|
|
1056
1907
|
console.log(cliVersion());
|
|
1057
1908
|
return;
|
|
1058
1909
|
}
|
|
1059
1910
|
if (command === "help" || command === "--help" || command === "-h") {
|
|
1911
|
+
assertArgs(parsed, ["help"], 1);
|
|
1060
1912
|
printHelp();
|
|
1061
1913
|
return;
|
|
1062
1914
|
}
|
|
1063
1915
|
if (command === "init") {
|
|
1916
|
+
assertArgs(parsed, ["app-id", "name", "config", "env", "services", "ai-provider", "force"], 1);
|
|
1064
1917
|
initProject({
|
|
1065
1918
|
appId: requiredString(parsed.options["app-id"], "--app-id"),
|
|
1066
1919
|
name: requiredString(parsed.options.name, "--name"),
|
|
@@ -1073,15 +1926,132 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
1073
1926
|
return;
|
|
1074
1927
|
}
|
|
1075
1928
|
if (command === "doctor") {
|
|
1929
|
+
assertArgs(parsed, ["config"], 1);
|
|
1076
1930
|
await doctor({ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs" });
|
|
1077
1931
|
return;
|
|
1078
1932
|
}
|
|
1933
|
+
if (command === "capabilities") {
|
|
1934
|
+
assertArgs(parsed, ["json"], 1);
|
|
1935
|
+
printCapabilities(parsed.options.json === true);
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
if (command === "admin") {
|
|
1939
|
+
const area = parsed.positionals[1];
|
|
1940
|
+
const action = parsed.positionals[2];
|
|
1941
|
+
const credentialSet = action === "credential" && parsed.positionals[3] === "set";
|
|
1942
|
+
const credentials = action === "credentials";
|
|
1943
|
+
if (area !== "ai" || action !== "show" && action !== "set" && !credentialSet && !credentials) {
|
|
1944
|
+
throw new Error('unknown admin command. Try "odla-ai admin ai show".');
|
|
1945
|
+
}
|
|
1946
|
+
assertArgs(parsed, [
|
|
1947
|
+
"platform",
|
|
1948
|
+
"token",
|
|
1949
|
+
"json",
|
|
1950
|
+
"open",
|
|
1951
|
+
"provider",
|
|
1952
|
+
"model",
|
|
1953
|
+
"enabled",
|
|
1954
|
+
"max-input-bytes",
|
|
1955
|
+
"max-output-tokens",
|
|
1956
|
+
"max-calls-per-run",
|
|
1957
|
+
"from-env",
|
|
1958
|
+
"stdin"
|
|
1959
|
+
], credentialSet ? 5 : action === "set" ? 4 : 3);
|
|
1960
|
+
await adminAi({
|
|
1961
|
+
action: credentialSet ? "credential-set" : credentials ? "credentials" : action,
|
|
1962
|
+
purpose: action === "set" ? parsed.positionals[3] : void 0,
|
|
1963
|
+
provider: stringOpt(parsed.options.provider),
|
|
1964
|
+
model: stringOpt(parsed.options.model),
|
|
1965
|
+
enabled: boolOpt(parsed.options.enabled),
|
|
1966
|
+
maxInputBytes: numberOpt(parsed.options["max-input-bytes"], "--max-input-bytes"),
|
|
1967
|
+
maxOutputTokens: numberOpt(parsed.options["max-output-tokens"], "--max-output-tokens"),
|
|
1968
|
+
maxCallsPerRun: numberOpt(parsed.options["max-calls-per-run"], "--max-calls-per-run"),
|
|
1969
|
+
platform: stringOpt(parsed.options.platform),
|
|
1970
|
+
token: stringOpt(parsed.options.token),
|
|
1971
|
+
json: parsed.options.json === true,
|
|
1972
|
+
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
1973
|
+
credentialProvider: credentialSet ? parsed.positionals[4] : void 0,
|
|
1974
|
+
fromEnv: stringOpt(parsed.options["from-env"]),
|
|
1975
|
+
stdin: parsed.options.stdin === true
|
|
1976
|
+
});
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
if (command === "security") {
|
|
1980
|
+
const sub = parsed.positionals[1];
|
|
1981
|
+
if (sub !== "run") throw new Error('unknown security command. Try "odla-ai security run".');
|
|
1982
|
+
assertArgs(parsed, [
|
|
1983
|
+
"config",
|
|
1984
|
+
"env",
|
|
1985
|
+
"out",
|
|
1986
|
+
"profile",
|
|
1987
|
+
"max-hunt-tasks",
|
|
1988
|
+
"run-id",
|
|
1989
|
+
"platform",
|
|
1990
|
+
"self",
|
|
1991
|
+
"ack-redacted-source",
|
|
1992
|
+
"open",
|
|
1993
|
+
"fail-on",
|
|
1994
|
+
"fail-on-candidates",
|
|
1995
|
+
"allow-incomplete"
|
|
1996
|
+
], 3);
|
|
1997
|
+
const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
1998
|
+
const selfAudit = parsed.options.self === true;
|
|
1999
|
+
const open = parsed.options.open === false ? false : parsed.options.open === true ? true : void 0;
|
|
2000
|
+
const platform = stringOpt(parsed.options.platform);
|
|
2001
|
+
const result = await runHostedSecurity({
|
|
2002
|
+
configPath,
|
|
2003
|
+
selfAudit,
|
|
2004
|
+
target: parsed.positionals[2],
|
|
2005
|
+
out: stringOpt(parsed.options.out),
|
|
2006
|
+
env: stringOpt(parsed.options.env),
|
|
2007
|
+
profile: securityProfile(stringOpt(parsed.options.profile)),
|
|
2008
|
+
maxHuntTasks: numberOpt(parsed.options["max-hunt-tasks"], "--max-hunt-tasks"),
|
|
2009
|
+
runId: stringOpt(parsed.options["run-id"]),
|
|
2010
|
+
platform,
|
|
2011
|
+
sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
|
|
2012
|
+
getToken: async (request) => {
|
|
2013
|
+
if (request.scope === "platform:security:self") {
|
|
2014
|
+
return getScopedPlatformToken({ platform: request.platform, scope: request.scope, open });
|
|
2015
|
+
}
|
|
2016
|
+
const cfg = await loadProjectConfig(configPath);
|
|
2017
|
+
return getDeveloperToken(cfg, { configPath, open }, fetch, console);
|
|
2018
|
+
}
|
|
2019
|
+
});
|
|
2020
|
+
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
2021
|
+
const candidateValue = parsed.options["fail-on-candidates"];
|
|
2022
|
+
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
2023
|
+
const confirmed = (0, import_security2.findingsAtOrAbove)(result.report, failOn);
|
|
2024
|
+
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(result.report, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
|
|
2025
|
+
const incomplete = result.report.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
|
|
2026
|
+
if (confirmed.length || leads.length || incomplete) {
|
|
2027
|
+
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
2028
|
+
}
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
1079
2031
|
if (command === "provision") {
|
|
2032
|
+
assertArgs(
|
|
2033
|
+
parsed,
|
|
2034
|
+
[
|
|
2035
|
+
"config",
|
|
2036
|
+
"dry-run",
|
|
2037
|
+
"rotate-keys",
|
|
2038
|
+
"rotate-o11y-token",
|
|
2039
|
+
"push-secrets",
|
|
2040
|
+
"write-credentials",
|
|
2041
|
+
"write-dev-vars",
|
|
2042
|
+
"token",
|
|
2043
|
+
"open",
|
|
2044
|
+
"yes"
|
|
2045
|
+
],
|
|
2046
|
+
1
|
|
2047
|
+
);
|
|
1080
2048
|
const writeDevVars2 = parsed.options["write-dev-vars"];
|
|
1081
2049
|
const opts = {
|
|
1082
2050
|
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
1083
2051
|
dryRun: parsed.options["dry-run"] === true,
|
|
1084
2052
|
rotateKeys: parsed.options["rotate-keys"] === true,
|
|
2053
|
+
rotateO11yToken: parsed.options["rotate-o11y-token"] === true,
|
|
2054
|
+
pushSecrets: parsed.options["push-secrets"] === true,
|
|
1085
2055
|
writeCredentials: parsed.options["write-credentials"] !== false,
|
|
1086
2056
|
writeDevVars: typeof writeDevVars2 === "string" ? writeDevVars2 : writeDevVars2 === true,
|
|
1087
2057
|
token: stringOpt(parsed.options.token),
|
|
@@ -1092,6 +2062,7 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
1092
2062
|
return;
|
|
1093
2063
|
}
|
|
1094
2064
|
if (command === "smoke") {
|
|
2065
|
+
assertArgs(parsed, ["config", "env"], 1);
|
|
1095
2066
|
await smoke({
|
|
1096
2067
|
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
1097
2068
|
env: stringOpt(parsed.options.env)
|
|
@@ -1099,22 +2070,26 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
1099
2070
|
return;
|
|
1100
2071
|
}
|
|
1101
2072
|
if (command === "setup") {
|
|
2073
|
+
assertArgs(parsed, ["dir", "global", "force", "agent", "harness"], 1);
|
|
1102
2074
|
installSkill({
|
|
1103
2075
|
dir: stringOpt(parsed.options.dir),
|
|
1104
2076
|
global: parsed.options.global === true,
|
|
2077
|
+
harnesses: harnessList(parsed),
|
|
1105
2078
|
force: parsed.options.force === true
|
|
1106
2079
|
});
|
|
1107
2080
|
console.log(
|
|
1108
|
-
"\
|
|
2081
|
+
"\nOffline runbooks installed. Open this repo in your coding agent and ask it to set up odla.\nIts native adapter finds the local `odla` skill, chooses build or migration, and drives the CLI."
|
|
1109
2082
|
);
|
|
1110
2083
|
return;
|
|
1111
2084
|
}
|
|
1112
2085
|
if (command === "skill") {
|
|
1113
2086
|
const sub = parsed.positionals[1];
|
|
1114
2087
|
if (sub !== "install") throw new Error(`unknown skill subcommand "${sub ?? ""}". Try "odla-ai skill install".`);
|
|
2088
|
+
assertArgs(parsed, ["dir", "global", "force", "agent", "harness"], 2);
|
|
1115
2089
|
installSkill({
|
|
1116
2090
|
dir: stringOpt(parsed.options.dir),
|
|
1117
2091
|
global: parsed.options.global === true,
|
|
2092
|
+
harnesses: harnessList(parsed),
|
|
1118
2093
|
force: parsed.options.force === true
|
|
1119
2094
|
});
|
|
1120
2095
|
return;
|
|
@@ -1122,6 +2097,7 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
1122
2097
|
if (command === "secrets") {
|
|
1123
2098
|
const sub = parsed.positionals[1];
|
|
1124
2099
|
if (sub !== "push") throw new Error(`unknown secrets subcommand "${sub ?? ""}". Try "odla-ai secrets push --env dev".`);
|
|
2100
|
+
assertArgs(parsed, ["config", "env", "dry-run", "yes"], 2);
|
|
1125
2101
|
await secretsPush({
|
|
1126
2102
|
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
1127
2103
|
env: requiredString(parsed.options.env, "--env"),
|
|
@@ -1132,103 +2108,45 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
1132
2108
|
}
|
|
1133
2109
|
throw new Error(`unknown command "${command}". Run "odla-ai help".`);
|
|
1134
2110
|
}
|
|
1135
|
-
function
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
const arg = argv[i];
|
|
1140
|
-
if (!arg.startsWith("--")) {
|
|
1141
|
-
positionals.push(arg);
|
|
1142
|
-
continue;
|
|
1143
|
-
}
|
|
1144
|
-
const eq = arg.indexOf("=");
|
|
1145
|
-
const rawName = arg.slice(2, eq === -1 ? void 0 : eq);
|
|
1146
|
-
if (!rawName) continue;
|
|
1147
|
-
if (rawName.startsWith("no-")) {
|
|
1148
|
-
options[rawName.slice(3)] = false;
|
|
1149
|
-
continue;
|
|
1150
|
-
}
|
|
1151
|
-
if (eq !== -1) {
|
|
1152
|
-
addOption(options, rawName, arg.slice(eq + 1));
|
|
1153
|
-
continue;
|
|
1154
|
-
}
|
|
1155
|
-
const value = argv[i + 1];
|
|
1156
|
-
if (value !== void 0 && !value.startsWith("--")) {
|
|
1157
|
-
i++;
|
|
1158
|
-
addOption(options, rawName, value);
|
|
1159
|
-
} else {
|
|
1160
|
-
addOption(options, rawName, true);
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
return { positionals, options };
|
|
1164
|
-
}
|
|
1165
|
-
function addOption(options, name, value) {
|
|
1166
|
-
const cur = options[name];
|
|
1167
|
-
if (cur === void 0) {
|
|
1168
|
-
options[name] = value;
|
|
1169
|
-
} else if (Array.isArray(cur)) {
|
|
1170
|
-
cur.push(String(value));
|
|
1171
|
-
} else {
|
|
1172
|
-
options[name] = [String(cur), String(value)];
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
function requiredString(value, name) {
|
|
1176
|
-
const s = stringOpt(value);
|
|
1177
|
-
if (!s) throw new Error(`${name} is required`);
|
|
1178
|
-
return s;
|
|
1179
|
-
}
|
|
1180
|
-
function stringOpt(value) {
|
|
1181
|
-
if (typeof value === "string") return value;
|
|
1182
|
-
if (Array.isArray(value)) return value[value.length - 1];
|
|
1183
|
-
return void 0;
|
|
2111
|
+
function securityProfile(value) {
|
|
2112
|
+
if (value === void 0) return void 0;
|
|
2113
|
+
if (value === "odla" || value === "cloudflare-app" || value === "generic") return value;
|
|
2114
|
+
throw new Error("--profile must be odla, cloudflare-app, or generic");
|
|
1184
2115
|
}
|
|
1185
|
-
function
|
|
1186
|
-
if (
|
|
1187
|
-
|
|
1188
|
-
return values.flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
|
|
2116
|
+
function severityOpt(value, flag) {
|
|
2117
|
+
if (["informational", "low", "medium", "high", "critical"].includes(value)) return value;
|
|
2118
|
+
throw new Error(`${flag} must be informational, low, medium, high, or critical`);
|
|
1189
2119
|
}
|
|
1190
|
-
function
|
|
1191
|
-
const
|
|
1192
|
-
|
|
2120
|
+
function harnessList(parsed) {
|
|
2121
|
+
const selected = [
|
|
2122
|
+
...harnessOption(parsed.options.agent, "--agent"),
|
|
2123
|
+
...harnessOption(parsed.options.harness, "--harness")
|
|
2124
|
+
];
|
|
2125
|
+
return selected.length ? selected : ["all"];
|
|
1193
2126
|
}
|
|
1194
|
-
function
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
odla-ai provision [--config odla.config.mjs] [--dry-run] [--open|--no-open] [--rotate-keys] [--write-dev-vars[=path]]
|
|
1202
|
-
odla-ai smoke [--config odla.config.mjs] [--env dev]
|
|
1203
|
-
odla-ai skill install [--dir <project>] [--global] [--force]
|
|
1204
|
-
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
1205
|
-
odla-ai version
|
|
1206
|
-
|
|
1207
|
-
Commands:
|
|
1208
|
-
setup Install the odla skills so a coding agent can drive the rest.
|
|
1209
|
-
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
1210
|
-
doctor Validate and summarize the project config without network calls.
|
|
1211
|
-
provision Register the app, enable services, push schema/rules, configure AI/auth.
|
|
1212
|
-
smoke Verify local credentials, public-config, live schema, and db aggregate.
|
|
1213
|
-
skill Install the bundled Claude Code skills into .claude/skills/.
|
|
1214
|
-
secrets Push the env's db key into the Worker via wrangler, stdin-piped.
|
|
1215
|
-
version Print the CLI version.
|
|
1216
|
-
|
|
1217
|
-
Safety:
|
|
1218
|
-
Provision caches the approved developer token and local db keys under .odla/
|
|
1219
|
-
with mode 0600, and init adds those paths to .gitignore.
|
|
1220
|
-
Provision opens the approval page automatically in interactive terminals;
|
|
1221
|
-
use --open to force browser launch or --no-open to suppress it.
|
|
1222
|
-
`);
|
|
2127
|
+
function harnessOption(value, flag) {
|
|
2128
|
+
if (value === void 0) return [];
|
|
2129
|
+
if (typeof value === "boolean") throw new Error(`${flag} requires a harness name`);
|
|
2130
|
+
const raw = Array.isArray(value) ? value : [value];
|
|
2131
|
+
const parts = raw.flatMap((item) => item.split(","));
|
|
2132
|
+
if (parts.some((item) => !item.trim())) throw new Error(`${flag} requires a harness name`);
|
|
2133
|
+
return parts.map((item) => item.trim());
|
|
1223
2134
|
}
|
|
1224
2135
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1225
2136
|
0 && (module.exports = {
|
|
2137
|
+
AGENT_HARNESSES,
|
|
2138
|
+
CAPABILITIES,
|
|
2139
|
+
SYSTEM_AI_PURPOSES,
|
|
2140
|
+
adminAi,
|
|
1226
2141
|
doctor,
|
|
2142
|
+
getScopedPlatformToken,
|
|
1227
2143
|
initProject,
|
|
1228
2144
|
installSkill,
|
|
2145
|
+
printCapabilities,
|
|
1229
2146
|
provision,
|
|
1230
2147
|
redactSecrets,
|
|
1231
2148
|
runCli,
|
|
2149
|
+
runHostedSecurity,
|
|
1232
2150
|
secretsPush,
|
|
1233
2151
|
smoke
|
|
1234
2152
|
});
|