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