@odla-ai/cli 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -9
- package/dist/bin.cjs +800 -400
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-7WZIZCGA.js → chunk-OERLHVLH.js} +805 -392
- package/dist/chunk-OERLHVLH.js.map +1 -0
- package/dist/index.cjs +808 -400
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -5
- package/dist/index.d.ts +114 -5
- package/dist/index.js +9 -1
- package/llms.txt +146 -16
- package/package.json +5 -4
- package/skills/odla/SKILL.md +6 -2
- package/skills/odla/references/build.md +15 -2
- package/skills/odla/references/sdks.md +8 -5
- package/skills/odla-migrate/references/phase-2-db.md +2 -2
- package/skills/odla-migrate/references/phase-5-cutover.md +6 -2
- package/skills/odla-migrate/references/secrets-map.md +5 -0
- package/dist/chunk-7WZIZCGA.js.map +0 -1
package/dist/bin.cjs
CHANGED
|
@@ -28,7 +28,459 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
|
|
|
28
28
|
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
29
29
|
|
|
30
30
|
// src/cli.ts
|
|
31
|
-
var
|
|
31
|
+
var import_security2 = require("@odla-ai/security");
|
|
32
|
+
|
|
33
|
+
// src/admin-ai.ts
|
|
34
|
+
var import_node_process3 = __toESM(require("process"), 1);
|
|
35
|
+
var import_node_fs2 = require("fs");
|
|
36
|
+
var import_db2 = require("@odla-ai/db");
|
|
37
|
+
|
|
38
|
+
// src/open.ts
|
|
39
|
+
var import_node_child_process = require("child_process");
|
|
40
|
+
var import_node_process = __toESM(require("process"), 1);
|
|
41
|
+
async function openUrl(url, options = {}) {
|
|
42
|
+
const command = openerFor(options.platform ?? import_node_process.default.platform);
|
|
43
|
+
const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
|
|
44
|
+
await new Promise((resolve7, reject) => {
|
|
45
|
+
const child = doSpawn(command.cmd, [...command.args, url], {
|
|
46
|
+
stdio: "ignore",
|
|
47
|
+
detached: true
|
|
48
|
+
});
|
|
49
|
+
child.once("error", reject);
|
|
50
|
+
child.once("spawn", () => {
|
|
51
|
+
child.unref();
|
|
52
|
+
resolve7();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function openerFor(platform) {
|
|
57
|
+
if (platform === "darwin") return { cmd: "open", args: [] };
|
|
58
|
+
if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
|
|
59
|
+
return { cmd: "xdg-open", args: [] };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/token.ts
|
|
63
|
+
var import_db = require("@odla-ai/db");
|
|
64
|
+
var import_node_process2 = __toESM(require("process"), 1);
|
|
65
|
+
|
|
66
|
+
// src/local.ts
|
|
67
|
+
var import_node_fs = require("fs");
|
|
68
|
+
var import_node_path = require("path");
|
|
69
|
+
var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
|
|
70
|
+
function readJsonFile(path) {
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function writePrivateJson(path, value) {
|
|
78
|
+
writePrivateText(path, `${JSON.stringify(value, null, 2)}
|
|
79
|
+
`);
|
|
80
|
+
}
|
|
81
|
+
function readCredentials(path) {
|
|
82
|
+
if (!(0, import_node_fs.existsSync)(path)) return null;
|
|
83
|
+
let value;
|
|
84
|
+
try {
|
|
85
|
+
value = JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
|
|
86
|
+
} catch {
|
|
87
|
+
throw new Error(`credentials file ${path} is not valid JSON; fix or remove it before provisioning`);
|
|
88
|
+
}
|
|
89
|
+
if (!value || typeof value !== "object" || typeof value.appId !== "string" || !value.envs || typeof value.envs !== "object") {
|
|
90
|
+
throw new Error(`credentials file ${path} has an invalid shape; fix or remove it before provisioning`);
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
function mergeCredential(current, update) {
|
|
95
|
+
const next = current ?? {
|
|
96
|
+
appId: update.appId,
|
|
97
|
+
platformUrl: update.platformUrl,
|
|
98
|
+
dbEndpoint: update.dbEndpoint,
|
|
99
|
+
updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
100
|
+
envs: {}
|
|
101
|
+
};
|
|
102
|
+
next.appId = update.appId;
|
|
103
|
+
next.platformUrl = update.platformUrl;
|
|
104
|
+
next.dbEndpoint = update.dbEndpoint;
|
|
105
|
+
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
106
|
+
next.envs[update.env] = {
|
|
107
|
+
...next.envs[update.env] ?? {},
|
|
108
|
+
tenantId: update.tenantId,
|
|
109
|
+
...update.dbKey ? { dbKey: update.dbKey } : {},
|
|
110
|
+
...update.o11yToken ? { o11yToken: update.o11yToken } : {}
|
|
111
|
+
};
|
|
112
|
+
return next;
|
|
113
|
+
}
|
|
114
|
+
function ensureGitignore(rootDir, localPaths = []) {
|
|
115
|
+
const path = (0, import_node_path.resolve)(rootDir, ".gitignore");
|
|
116
|
+
const existing = (0, import_node_fs.existsSync)(path) ? (0, import_node_fs.readFileSync)(path, "utf8") : "";
|
|
117
|
+
const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line) => !!line);
|
|
118
|
+
const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
|
|
119
|
+
const missing = wanted.filter((line) => !existing.split(/\r?\n/).includes(line));
|
|
120
|
+
if (missing.length === 0) return;
|
|
121
|
+
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
122
|
+
(0, import_node_fs.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
|
|
123
|
+
`);
|
|
124
|
+
}
|
|
125
|
+
function o11yDevVars(cfg) {
|
|
126
|
+
if (!cfg.services.includes("o11y")) return void 0;
|
|
127
|
+
return {
|
|
128
|
+
endpoint: cfg.o11y?.endpoint ?? "https://o11y.odla.ai",
|
|
129
|
+
service: cfg.o11y?.service ?? cfg.app.id,
|
|
130
|
+
...cfg.o11y?.version ? { version: cfg.o11y.version } : {}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function resolveWriteDevVarsTarget(cfg, requested) {
|
|
134
|
+
if (!requested) return null;
|
|
135
|
+
if (requested === true) return cfg.local.devVarsFile;
|
|
136
|
+
return (0, import_node_path.resolve)((0, import_node_path.dirname)(cfg.configPath), requested);
|
|
137
|
+
}
|
|
138
|
+
function writeDevVars(path, credentials, env, o11y) {
|
|
139
|
+
const entry = credentials.envs[env];
|
|
140
|
+
if (!entry) throw new Error(`no credentials for env "${env}" in ${path}`);
|
|
141
|
+
const lines = [`ODLA_PLATFORM="${credentials.platformUrl}"`];
|
|
142
|
+
if (entry.dbKey) lines.push(`ODLA_ENDPOINT="${credentials.dbEndpoint}"`);
|
|
143
|
+
lines.push(`ODLA_APP_ID="${credentials.appId}"`, `ODLA_ENV="${env}"`, `ODLA_TENANT="${entry.tenantId}"`);
|
|
144
|
+
if (entry.dbKey) lines.push(`ODLA_API_KEY="${entry.dbKey}"`);
|
|
145
|
+
if (o11y) {
|
|
146
|
+
lines.push(`ODLA_O11Y_ENDPOINT="${o11y.endpoint}"`, `ODLA_O11Y_SERVICE="${o11y.service}"`);
|
|
147
|
+
if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
|
|
148
|
+
if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
|
|
149
|
+
}
|
|
150
|
+
const existing = (0, import_node_fs.existsSync)(path) ? (0, import_node_fs.readFileSync)(path, "utf8") : "";
|
|
151
|
+
const retained = existing.split(/\r?\n/).filter((line) => !isManagedDevVar(line));
|
|
152
|
+
while (retained.at(-1) === "") retained.pop();
|
|
153
|
+
const prefix = retained.length ? `${retained.join("\n")}
|
|
154
|
+
|
|
155
|
+
` : "";
|
|
156
|
+
writePrivateText(path, `${prefix}${lines.join("\n")}
|
|
157
|
+
`);
|
|
158
|
+
}
|
|
159
|
+
var MANAGED_DEV_VARS = /* @__PURE__ */ new Set([
|
|
160
|
+
"ODLA_PLATFORM",
|
|
161
|
+
"ODLA_ENDPOINT",
|
|
162
|
+
"ODLA_APP_ID",
|
|
163
|
+
"ODLA_ENV",
|
|
164
|
+
"ODLA_TENANT",
|
|
165
|
+
"ODLA_API_KEY",
|
|
166
|
+
"ODLA_O11Y_ENDPOINT",
|
|
167
|
+
"ODLA_O11Y_SERVICE",
|
|
168
|
+
"ODLA_O11Y_VERSION",
|
|
169
|
+
"ODLA_O11Y_TOKEN"
|
|
170
|
+
]);
|
|
171
|
+
function isManagedDevVar(line) {
|
|
172
|
+
const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
|
|
173
|
+
return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
|
|
174
|
+
}
|
|
175
|
+
function writePrivateText(path, text) {
|
|
176
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
|
|
177
|
+
const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
178
|
+
(0, import_node_fs.writeFileSync)(temporary, text, { mode: 384 });
|
|
179
|
+
(0, import_node_fs.chmodSync)(temporary, 384);
|
|
180
|
+
(0, import_node_fs.renameSync)(temporary, path);
|
|
181
|
+
}
|
|
182
|
+
function gitignoreEntry(rootDir, path) {
|
|
183
|
+
const rel = (0, import_node_path.relative)((0, import_node_path.resolve)(rootDir), (0, import_node_path.resolve)(path));
|
|
184
|
+
if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path.isAbsolute)(rel)) return null;
|
|
185
|
+
return rel.replaceAll("\\", "/");
|
|
186
|
+
}
|
|
187
|
+
function displayPath(path, rootDir = process.cwd()) {
|
|
188
|
+
const rel = (0, import_node_path.relative)(rootDir, path);
|
|
189
|
+
return rel && !rel.startsWith("..") ? rel : path;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/token.ts
|
|
193
|
+
async function getDeveloperToken(cfg, options, doFetch, out) {
|
|
194
|
+
if (options.token) return options.token;
|
|
195
|
+
if (import_node_process2.default.env.ODLA_DEV_TOKEN) return import_node_process2.default.env.ODLA_DEV_TOKEN;
|
|
196
|
+
const cached = readJsonFile(cfg.local.tokenFile);
|
|
197
|
+
if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
198
|
+
out.log(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
199
|
+
return cached.token;
|
|
200
|
+
}
|
|
201
|
+
const browser = approvalBrowser(options);
|
|
202
|
+
const { token, expiresAt } = await (0, import_db.requestToken)({
|
|
203
|
+
endpoint: cfg.platformUrl,
|
|
204
|
+
label: `${cfg.app.id} provisioner`,
|
|
205
|
+
fetch: doFetch,
|
|
206
|
+
onCode: async ({ userCode, expiresIn }) => {
|
|
207
|
+
const approvalUrl = handshakeUrl(cfg.platformUrl, userCode);
|
|
208
|
+
out.log("");
|
|
209
|
+
out.log(`Approve code ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
|
|
210
|
+
if (browser.open) {
|
|
211
|
+
try {
|
|
212
|
+
await (options.openApprovalUrl ?? openUrl)(approvalUrl);
|
|
213
|
+
out.log(`auth: opened browser for approval${browser.mode === "auto" ? " (auto)" : ""}`);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
out.log(`auth: could not open browser (${err instanceof Error ? err.message : String(err)})`);
|
|
216
|
+
}
|
|
217
|
+
} else if (browser.reason) {
|
|
218
|
+
out.log(`auth: browser launch skipped (${browser.reason})`);
|
|
219
|
+
}
|
|
220
|
+
out.log("");
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
writePrivateJson(cfg.local.tokenFile, { token, expiresAt });
|
|
224
|
+
out.log(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
225
|
+
return token;
|
|
226
|
+
}
|
|
227
|
+
function approvalBrowser(options) {
|
|
228
|
+
if (options.open === true) return { open: true, mode: "forced" };
|
|
229
|
+
if (options.open === false) return { open: false, reason: "disabled by --no-open" };
|
|
230
|
+
if (import_node_process2.default.env.CI) return { open: false, reason: "CI environment" };
|
|
231
|
+
const interactive = options.interactive ?? Boolean(import_node_process2.default.stdin.isTTY && import_node_process2.default.stdout.isTTY);
|
|
232
|
+
if (!interactive) return { open: false, reason: "non-interactive shell; pass --open to force" };
|
|
233
|
+
return { open: true, mode: "auto" };
|
|
234
|
+
}
|
|
235
|
+
function handshakeUrl(platformUrl, userCode) {
|
|
236
|
+
const url = new URL("/studio", platformUrl);
|
|
237
|
+
url.searchParams.set("code", userCode);
|
|
238
|
+
return url.toString();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/admin-ai.ts
|
|
242
|
+
var SYSTEM_AI_PURPOSES = ["o11y.triage", "security.discovery", "security.validation"];
|
|
243
|
+
async function getScopedPlatformToken(options) {
|
|
244
|
+
const fromEnv = import_node_process3.default.env.ODLA_ADMIN_TOKEN;
|
|
245
|
+
if (fromEnv) return fromEnv;
|
|
246
|
+
return scopedToken(
|
|
247
|
+
options.platform.replace(/\/$/, ""),
|
|
248
|
+
options.scope,
|
|
249
|
+
{ action: "show", ...options },
|
|
250
|
+
options.fetch ?? fetch,
|
|
251
|
+
options.stdout ?? console
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
async function adminAi(options) {
|
|
255
|
+
const platform = (options.platform ?? import_node_process3.default.env.ODLA_PLATFORM ?? "https://odla.ai").replace(/\/$/, "");
|
|
256
|
+
const doFetch = options.fetch ?? fetch;
|
|
257
|
+
const out = options.stdout ?? console;
|
|
258
|
+
const scope = options.action === "set" || options.action === "credential-set" ? "platform:ai:write" : "platform:ai:read";
|
|
259
|
+
const token = options.token ?? import_node_process3.default.env.ODLA_ADMIN_TOKEN ?? await scopedToken(platform, scope, options, doFetch, out);
|
|
260
|
+
const headers = { authorization: `Bearer ${token}`, "content-type": "application/json" };
|
|
261
|
+
if (options.action === "credentials") {
|
|
262
|
+
const res2 = await doFetch(`${platform}/registry/platform/ai-credentials`, { headers });
|
|
263
|
+
const body2 = await responseBody(res2);
|
|
264
|
+
if (!res2.ok) throw new Error(apiError("read platform AI credential status", res2.status, body2));
|
|
265
|
+
const credentials = isRecord(body2) && isRecord(body2.credentials) ? body2.credentials : body2;
|
|
266
|
+
if (options.json) out.log(JSON.stringify({ credentials }, null, 2));
|
|
267
|
+
else for (const [provider, status] of Object.entries(isRecord(credentials) ? credentials : {})) {
|
|
268
|
+
out.log(`${provider} ${isRecord(status) && status.set === true ? "set" : "not set"}`);
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (options.action === "credential-set") {
|
|
273
|
+
const provider = requireProvider(options.credentialProvider);
|
|
274
|
+
const value = await credentialValue(options);
|
|
275
|
+
const res2 = await doFetch(`${platform}/registry/platform/ai-credentials/${encodeURIComponent(provider)}`, {
|
|
276
|
+
method: "PUT",
|
|
277
|
+
headers,
|
|
278
|
+
body: JSON.stringify({ value })
|
|
279
|
+
});
|
|
280
|
+
const body2 = await responseBody(res2);
|
|
281
|
+
if (!res2.ok) throw new Error(apiError(`store ${provider} platform credential`, res2.status, body2));
|
|
282
|
+
out.log(`stored ${provider} platform credential (write-only; value was not returned)`);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (options.action === "show") {
|
|
286
|
+
const res2 = await doFetch(`${platform}/registry/platform/ai-policies`, { headers });
|
|
287
|
+
const body2 = await responseBody(res2);
|
|
288
|
+
if (!res2.ok) throw new Error(apiError("read platform AI policies", res2.status, body2));
|
|
289
|
+
const policies = policyArray(body2);
|
|
290
|
+
if (options.json) {
|
|
291
|
+
out.log(JSON.stringify({ policies }, null, 2));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
out.log("purpose enabled provider model max calls max input bytes max output tokens");
|
|
295
|
+
for (const policy2 of policies) {
|
|
296
|
+
out.log([
|
|
297
|
+
policy2.purpose,
|
|
298
|
+
String(policy2.enabled),
|
|
299
|
+
policy2.provider,
|
|
300
|
+
policy2.model,
|
|
301
|
+
String(policy2.maxCallsPerRun ?? ""),
|
|
302
|
+
String(policy2.maxInputBytes ?? ""),
|
|
303
|
+
String(policy2.maxOutputTokens ?? "")
|
|
304
|
+
].join(" "));
|
|
305
|
+
}
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const purpose = requirePurpose(options.purpose);
|
|
309
|
+
if (!options.provider?.trim()) throw new Error("--provider is required");
|
|
310
|
+
if (!options.model?.trim()) throw new Error("--model is required");
|
|
311
|
+
const body = {
|
|
312
|
+
provider: options.provider.trim(),
|
|
313
|
+
model: options.model.trim(),
|
|
314
|
+
...options.enabled === void 0 ? {} : { enabled: options.enabled },
|
|
315
|
+
...options.maxInputBytes === void 0 ? {} : { maxInputBytes: options.maxInputBytes },
|
|
316
|
+
...options.maxOutputTokens === void 0 ? {} : { maxOutputTokens: options.maxOutputTokens },
|
|
317
|
+
...options.maxCallsPerRun === void 0 ? {} : { maxCallsPerRun: options.maxCallsPerRun }
|
|
318
|
+
};
|
|
319
|
+
const res = await doFetch(`${platform}/registry/platform/ai-policies/${encodeURIComponent(purpose)}`, {
|
|
320
|
+
method: "PUT",
|
|
321
|
+
headers,
|
|
322
|
+
body: JSON.stringify(body)
|
|
323
|
+
});
|
|
324
|
+
const response = await responseBody(res);
|
|
325
|
+
if (!res.ok) throw new Error(apiError(`update ${purpose}`, res.status, response));
|
|
326
|
+
const policy = isRecord(response) && isRecord(response.policy) ? response.policy : isRecord(response) ? response : {};
|
|
327
|
+
out.log(options.json ? JSON.stringify({ policy }, null, 2) : `updated ${purpose}: ${String(policy.provider ?? body.provider)}/${String(policy.model ?? body.model)}`);
|
|
328
|
+
}
|
|
329
|
+
async function scopedToken(platform, scope, options, doFetch, out) {
|
|
330
|
+
const tokenFile = options.tokenFile ?? `${import_node_process3.default.cwd()}/.odla/admin-token.local.json`;
|
|
331
|
+
const cache = readJsonFile(tokenFile);
|
|
332
|
+
const cached = cache?.platform === platform ? cache.tokens?.[scope] : void 0;
|
|
333
|
+
if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
334
|
+
out.log(`auth: using cached ${scope} grant (${tokenFile})`);
|
|
335
|
+
return cached.token;
|
|
336
|
+
}
|
|
337
|
+
const { token, expiresAt } = await (0, import_db2.requestToken)({
|
|
338
|
+
endpoint: platform,
|
|
339
|
+
label: `odla CLI admin AI (${scope})`,
|
|
340
|
+
scopes: [scope],
|
|
341
|
+
fetch: doFetch,
|
|
342
|
+
onCode: async ({ userCode, expiresIn }) => {
|
|
343
|
+
const approvalUrl = handshakeUrl(platform, userCode);
|
|
344
|
+
out.log(`Approve scoped request ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
|
|
345
|
+
const shouldOpen = options.open === true || options.open !== false && !import_node_process3.default.env.CI && Boolean(import_node_process3.default.stdin.isTTY && import_node_process3.default.stdout.isTTY);
|
|
346
|
+
if (shouldOpen) await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
const tokens = cache?.platform === platform ? { ...cache.tokens ?? {} } : {};
|
|
350
|
+
tokens[scope] = { token, expiresAt };
|
|
351
|
+
if ((0, import_node_fs2.existsSync)(`${import_node_process3.default.cwd()}/.git`)) ensureGitignore(import_node_process3.default.cwd(), [tokenFile]);
|
|
352
|
+
writePrivateJson(tokenFile, { platform, tokens });
|
|
353
|
+
out.log(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
|
|
354
|
+
return token;
|
|
355
|
+
}
|
|
356
|
+
function requirePurpose(value) {
|
|
357
|
+
if (!SYSTEM_AI_PURPOSES.includes(value)) {
|
|
358
|
+
throw new Error(`purpose must be one of: ${SYSTEM_AI_PURPOSES.join(", ")}`);
|
|
359
|
+
}
|
|
360
|
+
return value;
|
|
361
|
+
}
|
|
362
|
+
function requireProvider(value) {
|
|
363
|
+
if (value !== "anthropic" && value !== "openai" && value !== "google") {
|
|
364
|
+
throw new Error("provider must be one of: anthropic, openai, google");
|
|
365
|
+
}
|
|
366
|
+
return value;
|
|
367
|
+
}
|
|
368
|
+
async function credentialValue(options) {
|
|
369
|
+
if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
|
|
370
|
+
let value;
|
|
371
|
+
if (options.fromEnv) value = import_node_process3.default.env[options.fromEnv];
|
|
372
|
+
else if (options.stdin) value = await (options.readStdin ?? readStdin)();
|
|
373
|
+
else throw new Error("credential input required: use --from-env <NAME> or --stdin; values are never accepted as arguments");
|
|
374
|
+
value = value?.replace(/[\r\n]+$/, "");
|
|
375
|
+
if (!value) throw new Error(options.fromEnv ? `environment variable ${options.fromEnv} is empty or unset` : "stdin credential is empty");
|
|
376
|
+
if (new TextEncoder().encode(value).byteLength > 64 * 1024) throw new Error("credential exceeds 64 KiB");
|
|
377
|
+
return value;
|
|
378
|
+
}
|
|
379
|
+
async function readStdin() {
|
|
380
|
+
let value = "";
|
|
381
|
+
for await (const chunk of import_node_process3.default.stdin) {
|
|
382
|
+
value += String(chunk);
|
|
383
|
+
if (value.length > 64 * 1024) throw new Error("credential exceeds 64 KiB");
|
|
384
|
+
}
|
|
385
|
+
return value;
|
|
386
|
+
}
|
|
387
|
+
function policyArray(body) {
|
|
388
|
+
if (isRecord(body) && Array.isArray(body.policies)) return body.policies.filter(isRecord);
|
|
389
|
+
if (isRecord(body) && isRecord(body.policies)) return Object.values(body.policies).filter(isRecord);
|
|
390
|
+
throw new Error("platform returned an invalid AI policy response");
|
|
391
|
+
}
|
|
392
|
+
async function responseBody(res) {
|
|
393
|
+
const text = await res.text();
|
|
394
|
+
if (!text) return {};
|
|
395
|
+
try {
|
|
396
|
+
return JSON.parse(text);
|
|
397
|
+
} catch {
|
|
398
|
+
return { message: text.slice(0, 300) };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function apiError(action, status, body) {
|
|
402
|
+
const error = isRecord(body) && isRecord(body.error) ? body.error : void 0;
|
|
403
|
+
const message = error && typeof error.message === "string" ? error.message : isRecord(body) && typeof body.message === "string" ? body.message : "request failed";
|
|
404
|
+
return `${action} failed (${status}): ${message}`;
|
|
405
|
+
}
|
|
406
|
+
function isRecord(value) {
|
|
407
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/argv.ts
|
|
411
|
+
function parseArgv(argv) {
|
|
412
|
+
const positionals = [];
|
|
413
|
+
const options = {};
|
|
414
|
+
for (let i = 0; i < argv.length; i++) {
|
|
415
|
+
const arg = argv[i];
|
|
416
|
+
if (!arg.startsWith("--")) {
|
|
417
|
+
positionals.push(arg);
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
const eq = arg.indexOf("=");
|
|
421
|
+
const rawName = arg.slice(2, eq === -1 ? void 0 : eq);
|
|
422
|
+
if (!rawName) continue;
|
|
423
|
+
if (rawName.startsWith("no-")) {
|
|
424
|
+
options[rawName.slice(3)] = false;
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (eq !== -1) {
|
|
428
|
+
addOption(options, rawName, arg.slice(eq + 1));
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const value = argv[i + 1];
|
|
432
|
+
if (value !== void 0 && !value.startsWith("--")) {
|
|
433
|
+
i++;
|
|
434
|
+
addOption(options, rawName, value);
|
|
435
|
+
} else {
|
|
436
|
+
addOption(options, rawName, true);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return { positionals, options };
|
|
440
|
+
}
|
|
441
|
+
function assertArgs(parsed, allowedOptions, maxPositionals) {
|
|
442
|
+
const allowed = new Set(allowedOptions);
|
|
443
|
+
for (const name of Object.keys(parsed.options)) {
|
|
444
|
+
if (!allowed.has(name)) throw new Error(`unknown option "--${name}"; run "odla-ai help" for supported options`);
|
|
445
|
+
}
|
|
446
|
+
if (parsed.positionals.length > maxPositionals) {
|
|
447
|
+
throw new Error(`unexpected argument "${parsed.positionals[maxPositionals]}"; run "odla-ai help"`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function requiredString(value, name) {
|
|
451
|
+
const result = stringOpt(value);
|
|
452
|
+
if (!result) throw new Error(`${name} is required`);
|
|
453
|
+
return result;
|
|
454
|
+
}
|
|
455
|
+
function stringOpt(value) {
|
|
456
|
+
if (typeof value === "string") return value;
|
|
457
|
+
if (Array.isArray(value)) return value[value.length - 1];
|
|
458
|
+
return void 0;
|
|
459
|
+
}
|
|
460
|
+
function listOpt(value) {
|
|
461
|
+
if (value === void 0 || typeof value === "boolean") return void 0;
|
|
462
|
+
const values = Array.isArray(value) ? value : [value];
|
|
463
|
+
return values.flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
|
|
464
|
+
}
|
|
465
|
+
function boolOpt(value) {
|
|
466
|
+
if (typeof value === "boolean") return value;
|
|
467
|
+
if (typeof value === "string" && (value === "true" || value === "false")) return value === "true";
|
|
468
|
+
if (value === void 0) return void 0;
|
|
469
|
+
throw new Error("boolean option must be true or false");
|
|
470
|
+
}
|
|
471
|
+
function numberOpt(value, flag) {
|
|
472
|
+
if (value === void 0) return void 0;
|
|
473
|
+
const raw = stringOpt(value);
|
|
474
|
+
const parsed = raw === void 0 ? Number.NaN : Number(raw);
|
|
475
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) throw new Error(`${flag} requires a positive integer`);
|
|
476
|
+
return parsed;
|
|
477
|
+
}
|
|
478
|
+
function addOption(options, name, value) {
|
|
479
|
+
const current = options[name];
|
|
480
|
+
if (current === void 0) options[name] = value;
|
|
481
|
+
else if (Array.isArray(current)) current.push(String(value));
|
|
482
|
+
else options[name] = [String(current), String(value)];
|
|
483
|
+
}
|
|
32
484
|
|
|
33
485
|
// src/capabilities.ts
|
|
34
486
|
var CAPABILITIES = {
|
|
@@ -37,7 +489,9 @@ var CAPABILITIES = {
|
|
|
37
489
|
"issue, persist, and explicitly rotate configured service credentials",
|
|
38
490
|
"write local Worker values and push deployed Worker secrets through Wrangler stdin",
|
|
39
491
|
"push db schema/rules and configure platform AI, auth, and deployment links",
|
|
40
|
-
"validate config offline and smoke-test a provisioned db environment"
|
|
492
|
+
"validate config offline and smoke-test a provisioned db environment",
|
|
493
|
+
"run app-attributed hosted security discovery and independent validation without provider keys",
|
|
494
|
+
"let admins manage system AI routes/credentials through short-lived exact-scope approval"
|
|
41
495
|
],
|
|
42
496
|
agent: [
|
|
43
497
|
"install and import the selected odla SDKs",
|
|
@@ -48,11 +502,13 @@ var CAPABILITIES = {
|
|
|
48
502
|
"approve the odla device code and one-off third-party logins",
|
|
49
503
|
"consent to production changes with --yes",
|
|
50
504
|
"request destructive credential rotation explicitly",
|
|
51
|
-
"review application semantics, security findings, releases, and merges"
|
|
505
|
+
"review application semantics, security findings, releases, and merges",
|
|
506
|
+
"approve redacted-source disclosure before hosted security reasoning"
|
|
52
507
|
],
|
|
53
508
|
studio: [
|
|
54
509
|
"view telemetry and environment state",
|
|
55
|
-
"perform manual credential recovery when the CLI's local shown-once copy is unavailable"
|
|
510
|
+
"perform manual credential recovery when the CLI's local shown-once copy is unavailable",
|
|
511
|
+
"configure system AI purposes and view app/environment/run-attributed usage"
|
|
56
512
|
]
|
|
57
513
|
};
|
|
58
514
|
function printCapabilities(json = false, out = console) {
|
|
@@ -73,28 +529,28 @@ function printGroup(out, heading, items) {
|
|
|
73
529
|
}
|
|
74
530
|
|
|
75
531
|
// src/config.ts
|
|
76
|
-
var
|
|
77
|
-
var
|
|
532
|
+
var import_node_fs3 = require("fs");
|
|
533
|
+
var import_node_path2 = require("path");
|
|
78
534
|
var import_node_url = require("url");
|
|
79
535
|
var DEFAULT_PLATFORM = "https://odla.ai";
|
|
80
536
|
var DEFAULT_ENVS = ["dev"];
|
|
81
537
|
var DEFAULT_SERVICES = ["db", "ai"];
|
|
82
538
|
async function loadProjectConfig(configPath = "odla.config.mjs") {
|
|
83
|
-
const resolved = (0,
|
|
84
|
-
if (!(0,
|
|
539
|
+
const resolved = (0, import_node_path2.resolve)(configPath);
|
|
540
|
+
if (!(0, import_node_fs3.existsSync)(resolved)) {
|
|
85
541
|
throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
|
|
86
542
|
}
|
|
87
543
|
const raw = await loadConfigModule(resolved);
|
|
88
|
-
const rootDir = (0,
|
|
544
|
+
const rootDir = (0, import_node_path2.dirname)(resolved);
|
|
89
545
|
validateRawConfig(raw, resolved);
|
|
90
546
|
const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
|
|
91
547
|
const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
|
|
92
548
|
const envs = unique(raw.envs?.length ? raw.envs : DEFAULT_ENVS);
|
|
93
549
|
const services = unique(raw.services?.length ? raw.services : DEFAULT_SERVICES);
|
|
94
550
|
const local = {
|
|
95
|
-
tokenFile: (0,
|
|
96
|
-
credentialsFile: (0,
|
|
97
|
-
devVarsFile: (0,
|
|
551
|
+
tokenFile: (0, import_node_path2.resolve)(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
|
|
552
|
+
credentialsFile: (0, import_node_path2.resolve)(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
|
|
553
|
+
devVarsFile: (0, import_node_path2.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
|
|
98
554
|
gitignore: raw.local?.gitignore ?? true
|
|
99
555
|
};
|
|
100
556
|
return {
|
|
@@ -111,9 +567,9 @@ async function loadProjectConfig(configPath = "odla.config.mjs") {
|
|
|
111
567
|
async function resolveDataExport(cfg, value, names) {
|
|
112
568
|
if (value === void 0 || value === null || value === false) return void 0;
|
|
113
569
|
if (typeof value !== "string") return value;
|
|
114
|
-
const target = (0,
|
|
570
|
+
const target = (0, import_node_path2.isAbsolute)(value) ? value : (0, import_node_path2.resolve)(cfg.rootDir, value);
|
|
115
571
|
if (target.endsWith(".json")) {
|
|
116
|
-
return JSON.parse((0,
|
|
572
|
+
return JSON.parse((0, import_node_fs3.readFileSync)(target, "utf8"));
|
|
117
573
|
}
|
|
118
574
|
const mod = await import((0, import_node_url.pathToFileURL)(target).href);
|
|
119
575
|
for (const name of names) {
|
|
@@ -164,175 +620,49 @@ function validId(value) {
|
|
|
164
620
|
return typeof value === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value);
|
|
165
621
|
}
|
|
166
622
|
async function loadConfigModule(path) {
|
|
167
|
-
if (path.endsWith(".json")) return JSON.parse((0,
|
|
623
|
+
if (path.endsWith(".json")) return JSON.parse((0, import_node_fs3.readFileSync)(path, "utf8"));
|
|
168
624
|
const mod = await import(`${(0, import_node_url.pathToFileURL)(path).href}?t=${Date.now()}`);
|
|
169
625
|
const value = mod.default ?? mod.config;
|
|
170
626
|
if (typeof value === "function") return await value();
|
|
171
627
|
return value;
|
|
172
628
|
}
|
|
173
629
|
function trimSlash(value) {
|
|
174
|
-
return value.replace(/\/+$/, "");
|
|
175
|
-
}
|
|
176
|
-
function unique(values) {
|
|
177
|
-
return [...new Set(values.filter(Boolean))];
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// src/doctor-checks.ts
|
|
181
|
-
var import_node_child_process2 = require("child_process");
|
|
182
|
-
var import_node_fs4 = require("fs");
|
|
183
|
-
var import_node_path4 = require("path");
|
|
184
|
-
|
|
185
|
-
// src/redact.ts
|
|
186
|
-
var REPLACEMENTS = [
|
|
187
|
-
[/odla_(sk|dev)_[A-Za-z0-9._-]+/g, "odla_$1_[redacted]"],
|
|
188
|
-
[/\bsk_(live|test)_[A-Za-z0-9]+/g, "sk_$1_[redacted]"],
|
|
189
|
-
[/\bsk-[A-Za-z0-9._-]+/g, "sk-[redacted]"],
|
|
190
|
-
[/\bwhsec_[A-Za-z0-9+/=]+/g, "whsec_[redacted]"],
|
|
191
|
-
[/\bo11y_[A-Za-z0-9]+/g, "o11y_[redacted]"],
|
|
192
|
-
[/\b(ghp|gho|github_pat)_[A-Za-z0-9_]+/g, "$1_[redacted]"],
|
|
193
|
-
[/\bAKIA[A-Z0-9]{12,}/g, "AKIA[redacted]"]
|
|
194
|
-
];
|
|
195
|
-
function redactSecrets(value) {
|
|
196
|
-
let result = value;
|
|
197
|
-
for (const [pattern, replacement] of REPLACEMENTS) result = result.replace(pattern, replacement);
|
|
198
|
-
return result;
|
|
199
|
-
}
|
|
200
|
-
function looksSecret(value) {
|
|
201
|
-
return redactSecrets(value) !== value || value.includes("-----BEGIN");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// src/local.ts
|
|
205
|
-
var import_node_fs2 = require("fs");
|
|
206
|
-
var import_node_path2 = require("path");
|
|
207
|
-
var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
|
|
208
|
-
function readJsonFile(path) {
|
|
209
|
-
try {
|
|
210
|
-
return JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
|
|
211
|
-
} catch {
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
function writePrivateJson(path, value) {
|
|
216
|
-
writePrivateText(path, `${JSON.stringify(value, null, 2)}
|
|
217
|
-
`);
|
|
218
|
-
}
|
|
219
|
-
function readCredentials(path) {
|
|
220
|
-
if (!(0, import_node_fs2.existsSync)(path)) return null;
|
|
221
|
-
let value;
|
|
222
|
-
try {
|
|
223
|
-
value = JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
|
|
224
|
-
} catch {
|
|
225
|
-
throw new Error(`credentials file ${path} is not valid JSON; fix or remove it before provisioning`);
|
|
226
|
-
}
|
|
227
|
-
if (!value || typeof value !== "object" || typeof value.appId !== "string" || !value.envs || typeof value.envs !== "object") {
|
|
228
|
-
throw new Error(`credentials file ${path} has an invalid shape; fix or remove it before provisioning`);
|
|
229
|
-
}
|
|
230
|
-
return value;
|
|
231
|
-
}
|
|
232
|
-
function mergeCredential(current, update) {
|
|
233
|
-
const next = current ?? {
|
|
234
|
-
appId: update.appId,
|
|
235
|
-
platformUrl: update.platformUrl,
|
|
236
|
-
dbEndpoint: update.dbEndpoint,
|
|
237
|
-
updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
238
|
-
envs: {}
|
|
239
|
-
};
|
|
240
|
-
next.appId = update.appId;
|
|
241
|
-
next.platformUrl = update.platformUrl;
|
|
242
|
-
next.dbEndpoint = update.dbEndpoint;
|
|
243
|
-
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
244
|
-
next.envs[update.env] = {
|
|
245
|
-
...next.envs[update.env] ?? {},
|
|
246
|
-
tenantId: update.tenantId,
|
|
247
|
-
...update.dbKey ? { dbKey: update.dbKey } : {},
|
|
248
|
-
...update.o11yToken ? { o11yToken: update.o11yToken } : {}
|
|
249
|
-
};
|
|
250
|
-
return next;
|
|
251
|
-
}
|
|
252
|
-
function ensureGitignore(rootDir, localPaths = []) {
|
|
253
|
-
const path = (0, import_node_path2.resolve)(rootDir, ".gitignore");
|
|
254
|
-
const existing = (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : "";
|
|
255
|
-
const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line) => !!line);
|
|
256
|
-
const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
|
|
257
|
-
const missing = wanted.filter((line) => !existing.split(/\r?\n/).includes(line));
|
|
258
|
-
if (missing.length === 0) return;
|
|
259
|
-
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
260
|
-
(0, import_node_fs2.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
|
|
261
|
-
`);
|
|
262
|
-
}
|
|
263
|
-
function o11yDevVars(cfg) {
|
|
264
|
-
if (!cfg.services.includes("o11y")) return void 0;
|
|
265
|
-
return {
|
|
266
|
-
endpoint: cfg.o11y?.endpoint ?? "https://o11y.odla.ai",
|
|
267
|
-
service: cfg.o11y?.service ?? cfg.app.id,
|
|
268
|
-
...cfg.o11y?.version ? { version: cfg.o11y.version } : {}
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
function resolveWriteDevVarsTarget(cfg, requested) {
|
|
272
|
-
if (!requested) return null;
|
|
273
|
-
if (requested === true) return cfg.local.devVarsFile;
|
|
274
|
-
return (0, import_node_path2.resolve)((0, import_node_path2.dirname)(cfg.configPath), requested);
|
|
275
|
-
}
|
|
276
|
-
function writeDevVars(path, credentials, env, o11y) {
|
|
277
|
-
const entry = credentials.envs[env];
|
|
278
|
-
if (!entry) throw new Error(`no credentials for env "${env}" in ${path}`);
|
|
279
|
-
const lines = [`ODLA_PLATFORM="${credentials.platformUrl}"`];
|
|
280
|
-
if (entry.dbKey) lines.push(`ODLA_ENDPOINT="${credentials.dbEndpoint}"`);
|
|
281
|
-
lines.push(`ODLA_APP_ID="${credentials.appId}"`, `ODLA_ENV="${env}"`, `ODLA_TENANT="${entry.tenantId}"`);
|
|
282
|
-
if (entry.dbKey) lines.push(`ODLA_API_KEY="${entry.dbKey}"`);
|
|
283
|
-
if (o11y) {
|
|
284
|
-
lines.push(`ODLA_O11Y_ENDPOINT="${o11y.endpoint}"`, `ODLA_O11Y_SERVICE="${o11y.service}"`);
|
|
285
|
-
if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
|
|
286
|
-
if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
|
|
287
|
-
}
|
|
288
|
-
const existing = (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : "";
|
|
289
|
-
const retained = existing.split(/\r?\n/).filter((line) => !isManagedDevVar(line));
|
|
290
|
-
while (retained.at(-1) === "") retained.pop();
|
|
291
|
-
const prefix = retained.length ? `${retained.join("\n")}
|
|
292
|
-
|
|
293
|
-
` : "";
|
|
294
|
-
writePrivateText(path, `${prefix}${lines.join("\n")}
|
|
295
|
-
`);
|
|
296
|
-
}
|
|
297
|
-
var MANAGED_DEV_VARS = /* @__PURE__ */ new Set([
|
|
298
|
-
"ODLA_PLATFORM",
|
|
299
|
-
"ODLA_ENDPOINT",
|
|
300
|
-
"ODLA_APP_ID",
|
|
301
|
-
"ODLA_ENV",
|
|
302
|
-
"ODLA_TENANT",
|
|
303
|
-
"ODLA_API_KEY",
|
|
304
|
-
"ODLA_O11Y_ENDPOINT",
|
|
305
|
-
"ODLA_O11Y_SERVICE",
|
|
306
|
-
"ODLA_O11Y_VERSION",
|
|
307
|
-
"ODLA_O11Y_TOKEN"
|
|
308
|
-
]);
|
|
309
|
-
function isManagedDevVar(line) {
|
|
310
|
-
const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
|
|
311
|
-
return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
|
|
630
|
+
return value.replace(/\/+$/, "");
|
|
312
631
|
}
|
|
313
|
-
function
|
|
314
|
-
|
|
315
|
-
const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
316
|
-
(0, import_node_fs2.writeFileSync)(temporary, text, { mode: 384 });
|
|
317
|
-
(0, import_node_fs2.chmodSync)(temporary, 384);
|
|
318
|
-
(0, import_node_fs2.renameSync)(temporary, path);
|
|
632
|
+
function unique(values) {
|
|
633
|
+
return [...new Set(values.filter(Boolean))];
|
|
319
634
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
635
|
+
|
|
636
|
+
// src/doctor-checks.ts
|
|
637
|
+
var import_node_child_process3 = require("child_process");
|
|
638
|
+
var import_node_fs5 = require("fs");
|
|
639
|
+
var import_node_path4 = require("path");
|
|
640
|
+
|
|
641
|
+
// src/redact.ts
|
|
642
|
+
var REPLACEMENTS = [
|
|
643
|
+
[/odla_(sk|dev)_[A-Za-z0-9._-]+/g, "odla_$1_[redacted]"],
|
|
644
|
+
[/\bsk_(live|test)_[A-Za-z0-9]+/g, "sk_$1_[redacted]"],
|
|
645
|
+
[/\bsk-[A-Za-z0-9._-]+/g, "sk-[redacted]"],
|
|
646
|
+
[/\bwhsec_[A-Za-z0-9+/=]+/g, "whsec_[redacted]"],
|
|
647
|
+
[/\bo11y_[A-Za-z0-9]+/g, "o11y_[redacted]"],
|
|
648
|
+
[/\b(ghp|gho|github_pat)_[A-Za-z0-9_]+/g, "$1_[redacted]"],
|
|
649
|
+
[/\bAKIA[A-Z0-9]{12,}/g, "AKIA[redacted]"]
|
|
650
|
+
];
|
|
651
|
+
function redactSecrets(value) {
|
|
652
|
+
let result = value;
|
|
653
|
+
for (const [pattern, replacement] of REPLACEMENTS) result = result.replace(pattern, replacement);
|
|
654
|
+
return result;
|
|
324
655
|
}
|
|
325
|
-
function
|
|
326
|
-
|
|
327
|
-
return rel && !rel.startsWith("..") ? rel : path;
|
|
656
|
+
function looksSecret(value) {
|
|
657
|
+
return redactSecrets(value) !== value || value.includes("-----BEGIN");
|
|
328
658
|
}
|
|
329
659
|
|
|
330
660
|
// src/wrangler.ts
|
|
331
|
-
var
|
|
332
|
-
var
|
|
661
|
+
var import_node_child_process2 = require("child_process");
|
|
662
|
+
var import_node_fs4 = require("fs");
|
|
333
663
|
var import_node_path3 = require("path");
|
|
334
664
|
var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
|
|
335
|
-
const child = (0,
|
|
665
|
+
const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
336
666
|
let stdout = "";
|
|
337
667
|
let stderr = "";
|
|
338
668
|
child.stdout.on("data", (chunk) => stdout += chunk.toString());
|
|
@@ -345,14 +675,14 @@ var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"]
|
|
|
345
675
|
function findWranglerConfig(rootDir) {
|
|
346
676
|
for (const name of WRANGLER_CONFIG_FILES) {
|
|
347
677
|
const path = (0, import_node_path3.join)(rootDir, name);
|
|
348
|
-
if ((0,
|
|
678
|
+
if ((0, import_node_fs4.existsSync)(path)) return path;
|
|
349
679
|
}
|
|
350
680
|
return null;
|
|
351
681
|
}
|
|
352
682
|
function readWranglerConfig(path) {
|
|
353
683
|
if (path.endsWith(".toml")) return null;
|
|
354
684
|
try {
|
|
355
|
-
return JSON.parse(stripJsonComments((0,
|
|
685
|
+
return JSON.parse(stripJsonComments((0, import_node_fs4.readFileSync)(path, "utf8")));
|
|
356
686
|
} catch {
|
|
357
687
|
return null;
|
|
358
688
|
}
|
|
@@ -423,7 +753,7 @@ function lintRules(rules, entities, publicRead) {
|
|
|
423
753
|
}
|
|
424
754
|
return warnings;
|
|
425
755
|
}
|
|
426
|
-
var defaultExec = (cmd, args, cwd) => (0,
|
|
756
|
+
var defaultExec = (cmd, args, cwd) => (0, import_node_child_process3.execFileSync)(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
427
757
|
function trackedSecretFiles(rootDir, exec = defaultExec, localPaths = []) {
|
|
428
758
|
let output;
|
|
429
759
|
try {
|
|
@@ -453,7 +783,7 @@ function wranglerWarnings(rootDir) {
|
|
|
453
783
|
const dir = (0, import_node_path4.resolve)(rootDir, assets.directory);
|
|
454
784
|
if (dir === (0, import_node_path4.resolve)(rootDir)) {
|
|
455
785
|
warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
|
|
456
|
-
} else if ((0,
|
|
786
|
+
} else if ((0, import_node_fs5.existsSync)((0, import_node_path4.join)(dir, "node_modules"))) {
|
|
457
787
|
warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
|
|
458
788
|
}
|
|
459
789
|
}
|
|
@@ -489,12 +819,12 @@ function o11yProjectWarnings(rootDir) {
|
|
|
489
819
|
return warnings;
|
|
490
820
|
}
|
|
491
821
|
const main = typeof config.main === "string" ? (0, import_node_path4.resolve)(rootDir, config.main) : null;
|
|
492
|
-
if (!main || !(0,
|
|
822
|
+
if (!main || !(0, import_node_fs5.existsSync)(main)) {
|
|
493
823
|
warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
|
|
494
824
|
} else {
|
|
495
825
|
let source = "";
|
|
496
826
|
try {
|
|
497
|
-
source = (0,
|
|
827
|
+
source = (0, import_node_fs5.readFileSync)(main, "utf8");
|
|
498
828
|
} catch {
|
|
499
829
|
}
|
|
500
830
|
if (!/\bwithObservability\b/.test(source)) {
|
|
@@ -509,7 +839,7 @@ function o11yProjectWarnings(rootDir) {
|
|
|
509
839
|
}
|
|
510
840
|
function readPackageJson(rootDir) {
|
|
511
841
|
try {
|
|
512
|
-
return JSON.parse((0,
|
|
842
|
+
return JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(rootDir, "package.json"), "utf8"));
|
|
513
843
|
} catch {
|
|
514
844
|
return null;
|
|
515
845
|
}
|
|
@@ -576,13 +906,13 @@ async function doctor(options) {
|
|
|
576
906
|
}
|
|
577
907
|
|
|
578
908
|
// src/init.ts
|
|
579
|
-
var
|
|
909
|
+
var import_node_fs6 = require("fs");
|
|
580
910
|
var import_node_path5 = require("path");
|
|
581
911
|
function initProject(options) {
|
|
582
912
|
const out = options.stdout ?? console;
|
|
583
913
|
const rootDir = (0, import_node_path5.resolve)(options.rootDir ?? process.cwd());
|
|
584
914
|
const configPath = (0, import_node_path5.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
|
|
585
|
-
if ((0,
|
|
915
|
+
if ((0, import_node_fs6.existsSync)(configPath) && !options.force) {
|
|
586
916
|
throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
|
|
587
917
|
}
|
|
588
918
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
|
|
@@ -591,10 +921,10 @@ function initProject(options) {
|
|
|
591
921
|
const envs = options.envs?.length ? options.envs : ["dev"];
|
|
592
922
|
const services = options.services?.length ? options.services : ["db", "ai"];
|
|
593
923
|
const aiProvider = options.aiProvider ?? "anthropic";
|
|
594
|
-
(0,
|
|
595
|
-
(0,
|
|
596
|
-
(0,
|
|
597
|
-
(0,
|
|
924
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path5.dirname)(configPath), { recursive: true });
|
|
925
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path5.resolve)(rootDir, "src/odla"), { recursive: true });
|
|
926
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path5.resolve)(rootDir, ".odla"), { recursive: true });
|
|
927
|
+
(0, import_node_fs6.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
|
|
598
928
|
writeIfMissing((0, import_node_path5.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
|
|
599
929
|
writeIfMissing((0, import_node_path5.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
|
|
600
930
|
ensureGitignore(rootDir);
|
|
@@ -603,8 +933,8 @@ function initProject(options) {
|
|
|
603
933
|
out.log("updated .gitignore for local odla credentials");
|
|
604
934
|
}
|
|
605
935
|
function writeIfMissing(path, text) {
|
|
606
|
-
if ((0,
|
|
607
|
-
(0,
|
|
936
|
+
if ((0, import_node_fs6.existsSync)(path)) return;
|
|
937
|
+
(0, import_node_fs6.writeFileSync)(path, text);
|
|
608
938
|
}
|
|
609
939
|
function configTemplate(input) {
|
|
610
940
|
return `export default {
|
|
@@ -693,84 +1023,7 @@ function relativeDisplay(path, rootDir) {
|
|
|
693
1023
|
// src/provision.ts
|
|
694
1024
|
var import_apps2 = require("@odla-ai/apps");
|
|
695
1025
|
var import_ai = require("@odla-ai/ai");
|
|
696
|
-
var
|
|
697
|
-
|
|
698
|
-
// src/token.ts
|
|
699
|
-
var import_db = require("@odla-ai/db");
|
|
700
|
-
var import_node_process2 = __toESM(require("process"), 1);
|
|
701
|
-
|
|
702
|
-
// src/open.ts
|
|
703
|
-
var import_node_child_process3 = require("child_process");
|
|
704
|
-
var import_node_process = __toESM(require("process"), 1);
|
|
705
|
-
async function openUrl(url, options = {}) {
|
|
706
|
-
const command = openerFor(options.platform ?? import_node_process.default.platform);
|
|
707
|
-
const doSpawn = options.spawnImpl ?? import_node_child_process3.spawn;
|
|
708
|
-
await new Promise((resolve6, reject) => {
|
|
709
|
-
const child = doSpawn(command.cmd, [...command.args, url], {
|
|
710
|
-
stdio: "ignore",
|
|
711
|
-
detached: true
|
|
712
|
-
});
|
|
713
|
-
child.once("error", reject);
|
|
714
|
-
child.once("spawn", () => {
|
|
715
|
-
child.unref();
|
|
716
|
-
resolve6();
|
|
717
|
-
});
|
|
718
|
-
});
|
|
719
|
-
}
|
|
720
|
-
function openerFor(platform) {
|
|
721
|
-
if (platform === "darwin") return { cmd: "open", args: [] };
|
|
722
|
-
if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
|
|
723
|
-
return { cmd: "xdg-open", args: [] };
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// src/token.ts
|
|
727
|
-
async function getDeveloperToken(cfg, options, doFetch, out) {
|
|
728
|
-
if (options.token) return options.token;
|
|
729
|
-
if (import_node_process2.default.env.ODLA_DEV_TOKEN) return import_node_process2.default.env.ODLA_DEV_TOKEN;
|
|
730
|
-
const cached = readJsonFile(cfg.local.tokenFile);
|
|
731
|
-
if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
732
|
-
out.log(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
733
|
-
return cached.token;
|
|
734
|
-
}
|
|
735
|
-
const browser = approvalBrowser(options);
|
|
736
|
-
const { token, expiresAt } = await (0, import_db.requestToken)({
|
|
737
|
-
endpoint: cfg.platformUrl,
|
|
738
|
-
label: `${cfg.app.id} provisioner`,
|
|
739
|
-
fetch: doFetch,
|
|
740
|
-
onCode: async ({ userCode, expiresIn }) => {
|
|
741
|
-
const approvalUrl = handshakeUrl(cfg.platformUrl, userCode);
|
|
742
|
-
out.log("");
|
|
743
|
-
out.log(`Approve code ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
|
|
744
|
-
if (browser.open) {
|
|
745
|
-
try {
|
|
746
|
-
await (options.openApprovalUrl ?? openUrl)(approvalUrl);
|
|
747
|
-
out.log(`auth: opened browser for approval${browser.mode === "auto" ? " (auto)" : ""}`);
|
|
748
|
-
} catch (err) {
|
|
749
|
-
out.log(`auth: could not open browser (${err instanceof Error ? err.message : String(err)})`);
|
|
750
|
-
}
|
|
751
|
-
} else if (browser.reason) {
|
|
752
|
-
out.log(`auth: browser launch skipped (${browser.reason})`);
|
|
753
|
-
}
|
|
754
|
-
out.log("");
|
|
755
|
-
}
|
|
756
|
-
});
|
|
757
|
-
writePrivateJson(cfg.local.tokenFile, { token, expiresAt });
|
|
758
|
-
out.log(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
759
|
-
return token;
|
|
760
|
-
}
|
|
761
|
-
function approvalBrowser(options) {
|
|
762
|
-
if (options.open === true) return { open: true, mode: "forced" };
|
|
763
|
-
if (options.open === false) return { open: false, reason: "disabled by --no-open" };
|
|
764
|
-
if (import_node_process2.default.env.CI) return { open: false, reason: "CI environment" };
|
|
765
|
-
const interactive = options.interactive ?? Boolean(import_node_process2.default.stdin.isTTY && import_node_process2.default.stdout.isTTY);
|
|
766
|
-
if (!interactive) return { open: false, reason: "non-interactive shell; pass --open to force" };
|
|
767
|
-
return { open: true, mode: "auto" };
|
|
768
|
-
}
|
|
769
|
-
function handshakeUrl(platformUrl, userCode) {
|
|
770
|
-
const url = new URL("/studio", platformUrl);
|
|
771
|
-
url.searchParams.set("code", userCode);
|
|
772
|
-
return url.toString();
|
|
773
|
-
}
|
|
1026
|
+
var import_node_process4 = __toESM(require("process"), 1);
|
|
774
1027
|
|
|
775
1028
|
// src/provision-credentials.ts
|
|
776
1029
|
var import_apps = require("@odla-ai/apps");
|
|
@@ -1074,7 +1327,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
1074
1327
|
out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
|
|
1075
1328
|
}
|
|
1076
1329
|
if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
|
|
1077
|
-
const key =
|
|
1330
|
+
const key = import_node_process4.default.env[cfg.ai.keyEnv];
|
|
1078
1331
|
if (key) {
|
|
1079
1332
|
const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
|
|
1080
1333
|
await (0, import_ai.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
|
|
@@ -1134,10 +1387,161 @@ async function safeText2(res) {
|
|
|
1134
1387
|
}
|
|
1135
1388
|
}
|
|
1136
1389
|
|
|
1390
|
+
// src/security.ts
|
|
1391
|
+
var import_node_path6 = require("path");
|
|
1392
|
+
var import_security = require("@odla-ai/security");
|
|
1393
|
+
var import_node = require("@odla-ai/security/node");
|
|
1394
|
+
async function runHostedSecurity(options) {
|
|
1395
|
+
if (options.sourceDisclosureAck !== "redacted") {
|
|
1396
|
+
throw new Error("Hosted security requires sourceDisclosureAck=redacted before repository source may leave this process");
|
|
1397
|
+
}
|
|
1398
|
+
const selfAudit = options.selfAudit === true;
|
|
1399
|
+
const cfg = selfAudit ? void 0 : await loadProjectConfig(options.configPath ?? "odla.config.mjs");
|
|
1400
|
+
const appId = selfAudit ? "odla-ai" : cfg.app.id;
|
|
1401
|
+
const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
|
|
1402
|
+
const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
|
|
1403
|
+
const target = (0, import_node_path6.resolve)(options.target ?? cfg?.rootDir ?? ".");
|
|
1404
|
+
const output = (0, import_node_path6.resolve)(options.out ?? (0, import_node_path6.resolve)(target, ".odla/security/hosted"));
|
|
1405
|
+
const outputRelative = (0, import_node_path6.relative)(target, output).split(import_node_path6.sep).join("/");
|
|
1406
|
+
if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
|
|
1407
|
+
const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
|
|
1408
|
+
const tokenRequest = {
|
|
1409
|
+
platform,
|
|
1410
|
+
appId,
|
|
1411
|
+
env,
|
|
1412
|
+
selfAudit,
|
|
1413
|
+
scope: selfAudit ? "platform:security:self" : null
|
|
1414
|
+
};
|
|
1415
|
+
const token = await injectedToken(options, tokenRequest);
|
|
1416
|
+
const snapshot = await (0, import_node.snapshotDirectory)(target, {
|
|
1417
|
+
exclude: !outputRelative.startsWith("../") && !(0, import_node_path6.isAbsolute)(outputRelative) ? [outputRelative] : []
|
|
1418
|
+
});
|
|
1419
|
+
const hosted = await (0, import_security.createPlatformSecurityReasoners)({
|
|
1420
|
+
platform,
|
|
1421
|
+
token,
|
|
1422
|
+
appId,
|
|
1423
|
+
env,
|
|
1424
|
+
repository: snapshot.repository,
|
|
1425
|
+
revision: snapshot.revision,
|
|
1426
|
+
snapshotDigest: snapshot.digest,
|
|
1427
|
+
sourceDisclosure: "redacted",
|
|
1428
|
+
clientRunId: options.runId ?? crypto.randomUUID(),
|
|
1429
|
+
...selfAudit ? { selfAudit: { enabled: true, subject: "service:odla-security-self-audit" } } : {},
|
|
1430
|
+
fetch: options.fetch,
|
|
1431
|
+
signal: options.signal
|
|
1432
|
+
});
|
|
1433
|
+
const harness = (0, import_security.createSecurityHarness)({
|
|
1434
|
+
profile,
|
|
1435
|
+
store: new import_node.FileRunStore((0, import_node_path6.resolve)(output, "state")),
|
|
1436
|
+
discoveryReasoner: hosted.discoveryReasoner,
|
|
1437
|
+
validationReasoner: hosted.validationReasoner,
|
|
1438
|
+
policy: {
|
|
1439
|
+
modelSourceDisclosure: "redacted",
|
|
1440
|
+
active: false,
|
|
1441
|
+
allowNetwork: false
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
1444
|
+
const report = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
|
|
1445
|
+
await (0, import_node.writeSecurityArtifacts)(output, report);
|
|
1446
|
+
const reportDigest = await (0, import_security.securityFingerprint)(report);
|
|
1447
|
+
await hosted.complete({
|
|
1448
|
+
reportDigest,
|
|
1449
|
+
coverageStatus: report.coverageStatus,
|
|
1450
|
+
confirmed: report.metrics.confirmed,
|
|
1451
|
+
candidates: report.metrics.candidates
|
|
1452
|
+
}, { signal: options.signal });
|
|
1453
|
+
printSummary(options.stdout ?? console, appId, env, hosted.run, report, output);
|
|
1454
|
+
return Object.freeze({ report, run: hosted.run, output });
|
|
1455
|
+
}
|
|
1456
|
+
function selectEnv(requested, declared, configPath, rootDir) {
|
|
1457
|
+
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
1458
|
+
if (!env || !declared.includes(env)) {
|
|
1459
|
+
const shown = (0, import_node_path6.relative)(rootDir, configPath) || configPath;
|
|
1460
|
+
throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
|
|
1461
|
+
}
|
|
1462
|
+
return env;
|
|
1463
|
+
}
|
|
1464
|
+
async function injectedToken(options, request) {
|
|
1465
|
+
const value = options.token ?? await options.getToken?.(Object.freeze({ ...request }));
|
|
1466
|
+
if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
|
|
1467
|
+
throw new Error(
|
|
1468
|
+
request.selfAudit ? "Self-audit requires an injected, scoped platform security token" : "Hosted security requires an injected app developer token or getToken callback"
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
return value;
|
|
1472
|
+
}
|
|
1473
|
+
function profileFor(name, maxHuntTasks) {
|
|
1474
|
+
const profile = name === "odla" ? (0, import_security.odlaProfile)() : name === "cloudflare-app" ? (0, import_security.cloudflareAppProfile)() : name === "generic" ? (0, import_security.genericProfile)() : void 0;
|
|
1475
|
+
if (!profile) throw new Error(`unknown security profile "${String(name)}"`);
|
|
1476
|
+
if (maxHuntTasks === void 0) return profile;
|
|
1477
|
+
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
1478
|
+
return { ...profile, maxHuntTasks };
|
|
1479
|
+
}
|
|
1480
|
+
function printSummary(out, appId, env, run, report, output) {
|
|
1481
|
+
const complete = report.coverage.filter((cell) => cell.state === "complete").length;
|
|
1482
|
+
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
1483
|
+
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
1484
|
+
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
1485
|
+
out.log(` coverage: ${report.coverageStatus} ${complete}/${report.coverage.length} blocked=${report.metrics.blockedCells} shallow=${report.metrics.shallowCells} unscheduled=${report.metrics.unscheduledCells}`);
|
|
1486
|
+
out.log(` findings: confirmed=${report.metrics.confirmed} needs_reproduction=${report.metrics.needsReproduction} candidates=${report.metrics.candidates}`);
|
|
1487
|
+
out.log(` report: ${(0, import_node_path6.resolve)(output, "REPORT.md")}`);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// src/help.ts
|
|
1491
|
+
var import_node_fs7 = require("fs");
|
|
1492
|
+
function cliVersion() {
|
|
1493
|
+
const pkg = JSON.parse((0, import_node_fs7.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
|
|
1494
|
+
return pkg.version ?? "unknown";
|
|
1495
|
+
}
|
|
1496
|
+
function printHelp() {
|
|
1497
|
+
console.log(`odla-ai
|
|
1498
|
+
|
|
1499
|
+
Usage:
|
|
1500
|
+
odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
|
|
1501
|
+
odla-ai init --app-id <id> --name <name> [--services db,ai,o11y] [--env dev --env prod]
|
|
1502
|
+
odla-ai doctor [--config odla.config.mjs]
|
|
1503
|
+
odla-ai capabilities [--json]
|
|
1504
|
+
odla-ai admin ai show [--platform https://odla.ai] [--json]
|
|
1505
|
+
odla-ai admin ai set <purpose> --provider <id> --model <id> [--enabled|--no-enabled]
|
|
1506
|
+
odla-ai admin ai credentials [--json]
|
|
1507
|
+
odla-ai admin ai credential set <provider> (--from-env <NAME>|--stdin)
|
|
1508
|
+
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
1509
|
+
odla-ai security run [target] --self --ack-redacted-source
|
|
1510
|
+
odla-ai provision [--config odla.config.mjs] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
1511
|
+
odla-ai smoke [--config odla.config.mjs] [--env dev]
|
|
1512
|
+
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
1513
|
+
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
1514
|
+
odla-ai version
|
|
1515
|
+
|
|
1516
|
+
Commands:
|
|
1517
|
+
setup Install offline odla runbooks for common coding-agent harnesses.
|
|
1518
|
+
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
1519
|
+
doctor Validate and summarize the project config without network calls.
|
|
1520
|
+
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
1521
|
+
admin Manage platform-funded AI routing with a narrow admin-approved device grant.
|
|
1522
|
+
security Run hosted discovery + independent validation with app/run attribution.
|
|
1523
|
+
provision Register services, configure them, persist credentials, optionally push secrets.
|
|
1524
|
+
smoke Verify local credentials, public-config, live schema, and db aggregate.
|
|
1525
|
+
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
1526
|
+
copilot, gemini, or agents (repeatable or comma-separated).
|
|
1527
|
+
secrets Push configured db/o11y secrets into the Worker via wrangler stdin.
|
|
1528
|
+
version Print the CLI version.
|
|
1529
|
+
|
|
1530
|
+
Safety:
|
|
1531
|
+
New projects target dev only. Add prod explicitly to odla.config.mjs and pass
|
|
1532
|
+
--yes to provision it; use --dry-run first to inspect the resolved plan.
|
|
1533
|
+
Provision caches the approved developer token and service credentials under
|
|
1534
|
+
.odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
|
|
1535
|
+
preflights Wrangler before any shown-once issuance or destructive rotation.
|
|
1536
|
+
Provision opens the approval page automatically in interactive terminals;
|
|
1537
|
+
use --open to force browser launch or --no-open to suppress it.
|
|
1538
|
+
`);
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1137
1541
|
// src/skill.ts
|
|
1138
|
-
var
|
|
1542
|
+
var import_node_fs8 = require("fs");
|
|
1139
1543
|
var import_node_os = require("os");
|
|
1140
|
-
var
|
|
1544
|
+
var import_node_path7 = require("path");
|
|
1141
1545
|
var import_node_url2 = require("url");
|
|
1142
1546
|
|
|
1143
1547
|
// src/skill-adapters.ts
|
|
@@ -1198,8 +1602,8 @@ function installSkill(options = {}) {
|
|
|
1198
1602
|
const files = listFiles(sourceDir);
|
|
1199
1603
|
if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
|
|
1200
1604
|
const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
|
|
1201
|
-
const root = (0,
|
|
1202
|
-
const home = (0,
|
|
1605
|
+
const root = (0, import_node_path7.resolve)(options.dir ?? process.cwd());
|
|
1606
|
+
const home = (0, import_node_path7.resolve)(options.homeDir ?? (0, import_node_os.homedir)());
|
|
1203
1607
|
const plans = /* @__PURE__ */ new Map();
|
|
1204
1608
|
const targets = /* @__PURE__ */ new Map();
|
|
1205
1609
|
const rememberTarget = (harness, target) => {
|
|
@@ -1213,48 +1617,48 @@ function installSkill(options = {}) {
|
|
|
1213
1617
|
plans.set(target, { target, content, boundary, managedMerge });
|
|
1214
1618
|
};
|
|
1215
1619
|
const planSkillTree = (targetDir2, boundary = root) => {
|
|
1216
|
-
for (const rel of files) plan((0,
|
|
1620
|
+
for (const rel of files) plan((0, import_node_path7.join)(targetDir2, rel), (0, import_node_fs8.readFileSync)((0, import_node_path7.join)(sourceDir, rel), "utf8"), false, boundary);
|
|
1217
1621
|
};
|
|
1218
1622
|
let targetDir;
|
|
1219
1623
|
if (options.global) {
|
|
1220
|
-
const claudeRoot = (0,
|
|
1221
|
-
const codexRoot = (0,
|
|
1624
|
+
const claudeRoot = (0, import_node_path7.join)(home, ".claude", "skills");
|
|
1625
|
+
const codexRoot = (0, import_node_path7.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path7.join)(home, ".codex"), "skills");
|
|
1222
1626
|
targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
|
|
1223
1627
|
for (const harness of harnesses) {
|
|
1224
1628
|
const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
|
|
1225
|
-
planSkillTree(skillRoot, harness === "claude" ? home : (0,
|
|
1629
|
+
planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path7.dirname)((0, import_node_path7.dirname)(codexRoot)));
|
|
1226
1630
|
rememberTarget(harness, skillRoot);
|
|
1227
1631
|
}
|
|
1228
1632
|
} else {
|
|
1229
|
-
const sharedRoot = (0,
|
|
1633
|
+
const sharedRoot = (0, import_node_path7.join)(root, ".agents", "skills");
|
|
1230
1634
|
planSkillTree(sharedRoot);
|
|
1231
|
-
const claudeRoot = (0,
|
|
1635
|
+
const claudeRoot = (0, import_node_path7.join)(root, ".claude", "skills");
|
|
1232
1636
|
targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
|
|
1233
1637
|
for (const harness of harnesses) rememberTarget(harness, sharedRoot);
|
|
1234
1638
|
if (harnesses.includes("claude")) {
|
|
1235
1639
|
for (const skill of skillNames(files)) {
|
|
1236
|
-
const canonical = (0,
|
|
1237
|
-
plan((0,
|
|
1640
|
+
const canonical = (0, import_node_fs8.readFileSync)((0, import_node_path7.join)(sourceDir, skill, "SKILL.md"), "utf8");
|
|
1641
|
+
plan((0, import_node_path7.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
|
|
1238
1642
|
}
|
|
1239
1643
|
rememberTarget("claude", claudeRoot);
|
|
1240
1644
|
}
|
|
1241
1645
|
if (harnesses.includes("cursor")) {
|
|
1242
|
-
const cursorRule = (0,
|
|
1646
|
+
const cursorRule = (0, import_node_path7.join)(root, ".cursor", "rules", "odla.mdc");
|
|
1243
1647
|
plan(cursorRule, CURSOR_RULE);
|
|
1244
1648
|
rememberTarget("cursor", cursorRule);
|
|
1245
1649
|
}
|
|
1246
1650
|
if (harnesses.includes("agents")) {
|
|
1247
|
-
const agentsFile = (0,
|
|
1651
|
+
const agentsFile = (0, import_node_path7.join)(root, "AGENTS.md");
|
|
1248
1652
|
plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
1249
1653
|
rememberTarget("agents", agentsFile);
|
|
1250
1654
|
}
|
|
1251
1655
|
if (harnesses.includes("copilot")) {
|
|
1252
|
-
const copilotFile = (0,
|
|
1656
|
+
const copilotFile = (0, import_node_path7.join)(root, ".github", "copilot-instructions.md");
|
|
1253
1657
|
plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
1254
1658
|
rememberTarget("copilot", copilotFile);
|
|
1255
1659
|
}
|
|
1256
1660
|
if (harnesses.includes("gemini")) {
|
|
1257
|
-
const geminiFile = (0,
|
|
1661
|
+
const geminiFile = (0, import_node_path7.join)(root, "GEMINI.md");
|
|
1258
1662
|
plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
1259
1663
|
rememberTarget("gemini", geminiFile);
|
|
1260
1664
|
}
|
|
@@ -1268,11 +1672,11 @@ function installSkill(options = {}) {
|
|
|
1268
1672
|
conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
|
|
1269
1673
|
continue;
|
|
1270
1674
|
}
|
|
1271
|
-
if (!(0,
|
|
1675
|
+
if (!(0, import_node_fs8.existsSync)(file.target)) {
|
|
1272
1676
|
writtenPaths.add(file.target);
|
|
1273
1677
|
continue;
|
|
1274
1678
|
}
|
|
1275
|
-
const current = (0,
|
|
1679
|
+
const current = (0, import_node_fs8.readFileSync)(file.target, "utf8");
|
|
1276
1680
|
if (current === file.content) {
|
|
1277
1681
|
unchangedPaths.add(file.target);
|
|
1278
1682
|
} else if (file.managedMerge || options.force) {
|
|
@@ -1289,9 +1693,9 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
1289
1693
|
);
|
|
1290
1694
|
}
|
|
1291
1695
|
for (const file of plans.values()) {
|
|
1292
|
-
if (!(0,
|
|
1293
|
-
(0,
|
|
1294
|
-
(0,
|
|
1696
|
+
if (!(0, import_node_fs8.existsSync)(file.target) || (0, import_node_fs8.readFileSync)(file.target, "utf8") !== file.content) {
|
|
1697
|
+
(0, import_node_fs8.mkdirSync)((0, import_node_path7.dirname)(file.target), { recursive: true });
|
|
1698
|
+
(0, import_node_fs8.writeFileSync)(file.target, file.content);
|
|
1295
1699
|
}
|
|
1296
1700
|
}
|
|
1297
1701
|
const skills = skillNames(files);
|
|
@@ -1310,7 +1714,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
1310
1714
|
};
|
|
1311
1715
|
}
|
|
1312
1716
|
function pathsUnder(root, paths) {
|
|
1313
|
-
return [...paths].map((path) => (0,
|
|
1717
|
+
return [...paths].map((path) => (0, import_node_path7.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path7.sep}`) && !(0, import_node_path7.isAbsolute)(path)).sort();
|
|
1314
1718
|
}
|
|
1315
1719
|
function normalizeHarnesses(values, global) {
|
|
1316
1720
|
const requested = values?.length ? values : ["claude"];
|
|
@@ -1332,9 +1736,9 @@ function normalizeHarnesses(values, global) {
|
|
|
1332
1736
|
function managedFileContent(path, block, force, boundary) {
|
|
1333
1737
|
const symlink = symlinkedComponent(boundary, path);
|
|
1334
1738
|
if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
|
|
1335
|
-
if (!(0,
|
|
1739
|
+
if (!(0, import_node_fs8.existsSync)(path)) return `${block}
|
|
1336
1740
|
`;
|
|
1337
|
-
const current = (0,
|
|
1741
|
+
const current = (0, import_node_fs8.readFileSync)(path, "utf8");
|
|
1338
1742
|
const start = "<!-- odla-ai agent setup:start -->";
|
|
1339
1743
|
const end = "<!-- odla-ai agent setup:end -->";
|
|
1340
1744
|
const startAt = current.indexOf(start);
|
|
@@ -1355,15 +1759,15 @@ function managedFileContent(path, block, force, boundary) {
|
|
|
1355
1759
|
return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
|
|
1356
1760
|
}
|
|
1357
1761
|
function symlinkedComponent(boundary, target) {
|
|
1358
|
-
const rel = (0,
|
|
1359
|
-
if (rel === ".." || rel.startsWith(`..${
|
|
1762
|
+
const rel = (0, import_node_path7.relative)(boundary, target);
|
|
1763
|
+
if (rel === ".." || rel.startsWith(`..${import_node_path7.sep}`) || (0, import_node_path7.isAbsolute)(rel)) {
|
|
1360
1764
|
throw new Error(`agent setup target escapes its install root: ${target}`);
|
|
1361
1765
|
}
|
|
1362
1766
|
let current = boundary;
|
|
1363
|
-
for (const part of rel.split(
|
|
1364
|
-
current = (0,
|
|
1767
|
+
for (const part of rel.split(import_node_path7.sep).filter(Boolean)) {
|
|
1768
|
+
current = (0, import_node_path7.join)(current, part);
|
|
1365
1769
|
try {
|
|
1366
|
-
if ((0,
|
|
1770
|
+
if ((0, import_node_fs8.lstatSync)(current).isSymbolicLink()) return current;
|
|
1367
1771
|
} catch (error) {
|
|
1368
1772
|
if (error.code !== "ENOENT") throw error;
|
|
1369
1773
|
}
|
|
@@ -1374,13 +1778,13 @@ function skillNames(files) {
|
|
|
1374
1778
|
return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
|
|
1375
1779
|
}
|
|
1376
1780
|
function listFiles(dir) {
|
|
1377
|
-
if (!(0,
|
|
1781
|
+
if (!(0, import_node_fs8.existsSync)(dir)) return [];
|
|
1378
1782
|
const results = [];
|
|
1379
1783
|
const walk = (current) => {
|
|
1380
|
-
for (const entry of (0,
|
|
1381
|
-
const path = (0,
|
|
1784
|
+
for (const entry of (0, import_node_fs8.readdirSync)(current, { withFileTypes: true })) {
|
|
1785
|
+
const path = (0, import_node_path7.join)(current, entry.name);
|
|
1382
1786
|
if (entry.isDirectory()) walk(path);
|
|
1383
|
-
else results.push((0,
|
|
1787
|
+
else results.push((0, import_node_path7.relative)(dir, path));
|
|
1384
1788
|
}
|
|
1385
1789
|
};
|
|
1386
1790
|
walk(dir);
|
|
@@ -1505,6 +1909,99 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
1505
1909
|
printCapabilities(parsed.options.json === true);
|
|
1506
1910
|
return;
|
|
1507
1911
|
}
|
|
1912
|
+
if (command === "admin") {
|
|
1913
|
+
const area = parsed.positionals[1];
|
|
1914
|
+
const action = parsed.positionals[2];
|
|
1915
|
+
const credentialSet = action === "credential" && parsed.positionals[3] === "set";
|
|
1916
|
+
const credentials = action === "credentials";
|
|
1917
|
+
if (area !== "ai" || action !== "show" && action !== "set" && !credentialSet && !credentials) {
|
|
1918
|
+
throw new Error('unknown admin command. Try "odla-ai admin ai show".');
|
|
1919
|
+
}
|
|
1920
|
+
assertArgs(parsed, [
|
|
1921
|
+
"platform",
|
|
1922
|
+
"token",
|
|
1923
|
+
"json",
|
|
1924
|
+
"open",
|
|
1925
|
+
"provider",
|
|
1926
|
+
"model",
|
|
1927
|
+
"enabled",
|
|
1928
|
+
"max-input-bytes",
|
|
1929
|
+
"max-output-tokens",
|
|
1930
|
+
"max-calls-per-run",
|
|
1931
|
+
"from-env",
|
|
1932
|
+
"stdin"
|
|
1933
|
+
], credentialSet ? 5 : action === "set" ? 4 : 3);
|
|
1934
|
+
await adminAi({
|
|
1935
|
+
action: credentialSet ? "credential-set" : credentials ? "credentials" : action,
|
|
1936
|
+
purpose: action === "set" ? parsed.positionals[3] : void 0,
|
|
1937
|
+
provider: stringOpt(parsed.options.provider),
|
|
1938
|
+
model: stringOpt(parsed.options.model),
|
|
1939
|
+
enabled: boolOpt(parsed.options.enabled),
|
|
1940
|
+
maxInputBytes: numberOpt(parsed.options["max-input-bytes"], "--max-input-bytes"),
|
|
1941
|
+
maxOutputTokens: numberOpt(parsed.options["max-output-tokens"], "--max-output-tokens"),
|
|
1942
|
+
maxCallsPerRun: numberOpt(parsed.options["max-calls-per-run"], "--max-calls-per-run"),
|
|
1943
|
+
platform: stringOpt(parsed.options.platform),
|
|
1944
|
+
token: stringOpt(parsed.options.token),
|
|
1945
|
+
json: parsed.options.json === true,
|
|
1946
|
+
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
1947
|
+
credentialProvider: credentialSet ? parsed.positionals[4] : void 0,
|
|
1948
|
+
fromEnv: stringOpt(parsed.options["from-env"]),
|
|
1949
|
+
stdin: parsed.options.stdin === true
|
|
1950
|
+
});
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
if (command === "security") {
|
|
1954
|
+
const sub = parsed.positionals[1];
|
|
1955
|
+
if (sub !== "run") throw new Error('unknown security command. Try "odla-ai security run".');
|
|
1956
|
+
assertArgs(parsed, [
|
|
1957
|
+
"config",
|
|
1958
|
+
"env",
|
|
1959
|
+
"out",
|
|
1960
|
+
"profile",
|
|
1961
|
+
"max-hunt-tasks",
|
|
1962
|
+
"run-id",
|
|
1963
|
+
"platform",
|
|
1964
|
+
"self",
|
|
1965
|
+
"ack-redacted-source",
|
|
1966
|
+
"open",
|
|
1967
|
+
"fail-on",
|
|
1968
|
+
"fail-on-candidates",
|
|
1969
|
+
"allow-incomplete"
|
|
1970
|
+
], 3);
|
|
1971
|
+
const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
1972
|
+
const selfAudit = parsed.options.self === true;
|
|
1973
|
+
const open = parsed.options.open === false ? false : parsed.options.open === true ? true : void 0;
|
|
1974
|
+
const platform = stringOpt(parsed.options.platform);
|
|
1975
|
+
const result = await runHostedSecurity({
|
|
1976
|
+
configPath,
|
|
1977
|
+
selfAudit,
|
|
1978
|
+
target: parsed.positionals[2],
|
|
1979
|
+
out: stringOpt(parsed.options.out),
|
|
1980
|
+
env: stringOpt(parsed.options.env),
|
|
1981
|
+
profile: securityProfile(stringOpt(parsed.options.profile)),
|
|
1982
|
+
maxHuntTasks: numberOpt(parsed.options["max-hunt-tasks"], "--max-hunt-tasks"),
|
|
1983
|
+
runId: stringOpt(parsed.options["run-id"]),
|
|
1984
|
+
platform,
|
|
1985
|
+
sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
|
|
1986
|
+
getToken: async (request) => {
|
|
1987
|
+
if (request.scope === "platform:security:self") {
|
|
1988
|
+
return getScopedPlatformToken({ platform: request.platform, scope: request.scope, open });
|
|
1989
|
+
}
|
|
1990
|
+
const cfg = await loadProjectConfig(configPath);
|
|
1991
|
+
return getDeveloperToken(cfg, { configPath, open }, fetch, console);
|
|
1992
|
+
}
|
|
1993
|
+
});
|
|
1994
|
+
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
1995
|
+
const candidateValue = parsed.options["fail-on-candidates"];
|
|
1996
|
+
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
1997
|
+
const confirmed = (0, import_security2.findingsAtOrAbove)(result.report, failOn);
|
|
1998
|
+
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(result.report, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
|
|
1999
|
+
const incomplete = result.report.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
|
|
2000
|
+
if (confirmed.length || leads.length || incomplete) {
|
|
2001
|
+
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
2002
|
+
}
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
1508
2005
|
if (command === "provision") {
|
|
1509
2006
|
assertArgs(
|
|
1510
2007
|
parsed,
|
|
@@ -1585,71 +2082,14 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
1585
2082
|
}
|
|
1586
2083
|
throw new Error(`unknown command "${command}". Run "odla-ai help".`);
|
|
1587
2084
|
}
|
|
1588
|
-
function
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
throw new Error(`unknown option "--${name}"; run "odla-ai help" for supported options`);
|
|
1593
|
-
}
|
|
1594
|
-
}
|
|
1595
|
-
if (parsed.positionals.length > maxPositionals) {
|
|
1596
|
-
throw new Error(`unexpected argument "${parsed.positionals[maxPositionals]}"; run "odla-ai help"`);
|
|
1597
|
-
}
|
|
1598
|
-
}
|
|
1599
|
-
function parseArgv(argv) {
|
|
1600
|
-
const positionals = [];
|
|
1601
|
-
const options = {};
|
|
1602
|
-
for (let i = 0; i < argv.length; i++) {
|
|
1603
|
-
const arg = argv[i];
|
|
1604
|
-
if (!arg.startsWith("--")) {
|
|
1605
|
-
positionals.push(arg);
|
|
1606
|
-
continue;
|
|
1607
|
-
}
|
|
1608
|
-
const eq = arg.indexOf("=");
|
|
1609
|
-
const rawName = arg.slice(2, eq === -1 ? void 0 : eq);
|
|
1610
|
-
if (!rawName) continue;
|
|
1611
|
-
if (rawName.startsWith("no-")) {
|
|
1612
|
-
options[rawName.slice(3)] = false;
|
|
1613
|
-
continue;
|
|
1614
|
-
}
|
|
1615
|
-
if (eq !== -1) {
|
|
1616
|
-
addOption(options, rawName, arg.slice(eq + 1));
|
|
1617
|
-
continue;
|
|
1618
|
-
}
|
|
1619
|
-
const value = argv[i + 1];
|
|
1620
|
-
if (value !== void 0 && !value.startsWith("--")) {
|
|
1621
|
-
i++;
|
|
1622
|
-
addOption(options, rawName, value);
|
|
1623
|
-
} else {
|
|
1624
|
-
addOption(options, rawName, true);
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
return { positionals, options };
|
|
1628
|
-
}
|
|
1629
|
-
function addOption(options, name, value) {
|
|
1630
|
-
const cur = options[name];
|
|
1631
|
-
if (cur === void 0) {
|
|
1632
|
-
options[name] = value;
|
|
1633
|
-
} else if (Array.isArray(cur)) {
|
|
1634
|
-
cur.push(String(value));
|
|
1635
|
-
} else {
|
|
1636
|
-
options[name] = [String(cur), String(value)];
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
function requiredString(value, name) {
|
|
1640
|
-
const s = stringOpt(value);
|
|
1641
|
-
if (!s) throw new Error(`${name} is required`);
|
|
1642
|
-
return s;
|
|
1643
|
-
}
|
|
1644
|
-
function stringOpt(value) {
|
|
1645
|
-
if (typeof value === "string") return value;
|
|
1646
|
-
if (Array.isArray(value)) return value[value.length - 1];
|
|
1647
|
-
return void 0;
|
|
2085
|
+
function securityProfile(value) {
|
|
2086
|
+
if (value === void 0) return void 0;
|
|
2087
|
+
if (value === "odla" || value === "cloudflare-app" || value === "generic") return value;
|
|
2088
|
+
throw new Error("--profile must be odla, cloudflare-app, or generic");
|
|
1648
2089
|
}
|
|
1649
|
-
function
|
|
1650
|
-
if (
|
|
1651
|
-
|
|
1652
|
-
return values.flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
|
|
2090
|
+
function severityOpt(value, flag) {
|
|
2091
|
+
if (["informational", "low", "medium", "high", "critical"].includes(value)) return value;
|
|
2092
|
+
throw new Error(`${flag} must be informational, low, medium, high, or critical`);
|
|
1653
2093
|
}
|
|
1654
2094
|
function harnessList(parsed) {
|
|
1655
2095
|
const selected = [
|
|
@@ -1666,46 +2106,6 @@ function harnessOption(value, flag) {
|
|
|
1666
2106
|
if (parts.some((item) => !item.trim())) throw new Error(`${flag} requires a harness name`);
|
|
1667
2107
|
return parts.map((item) => item.trim());
|
|
1668
2108
|
}
|
|
1669
|
-
function cliVersion() {
|
|
1670
|
-
const pkg = JSON.parse((0, import_node_fs7.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
|
|
1671
|
-
return pkg.version ?? "unknown";
|
|
1672
|
-
}
|
|
1673
|
-
function printHelp() {
|
|
1674
|
-
console.log(`odla-ai
|
|
1675
|
-
|
|
1676
|
-
Usage:
|
|
1677
|
-
odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
|
|
1678
|
-
odla-ai init --app-id <id> --name <name> [--services db,ai,o11y] [--env dev --env prod]
|
|
1679
|
-
odla-ai doctor [--config odla.config.mjs]
|
|
1680
|
-
odla-ai capabilities [--json]
|
|
1681
|
-
odla-ai provision [--config odla.config.mjs] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
1682
|
-
odla-ai smoke [--config odla.config.mjs] [--env dev]
|
|
1683
|
-
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
1684
|
-
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
1685
|
-
odla-ai version
|
|
1686
|
-
|
|
1687
|
-
Commands:
|
|
1688
|
-
setup Install offline odla runbooks for common coding-agent harnesses.
|
|
1689
|
-
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
1690
|
-
doctor Validate and summarize the project config without network calls.
|
|
1691
|
-
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
1692
|
-
provision Register services, configure them, persist credentials, optionally push secrets.
|
|
1693
|
-
smoke Verify local credentials, public-config, live schema, and db aggregate.
|
|
1694
|
-
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
1695
|
-
copilot, gemini, or agents (repeatable or comma-separated).
|
|
1696
|
-
secrets Push configured db/o11y secrets into the Worker via wrangler stdin.
|
|
1697
|
-
version Print the CLI version.
|
|
1698
|
-
|
|
1699
|
-
Safety:
|
|
1700
|
-
New projects target dev only. Add prod explicitly to odla.config.mjs and pass
|
|
1701
|
-
--yes to provision it; use --dry-run first to inspect the resolved plan.
|
|
1702
|
-
Provision caches the approved developer token and service credentials under
|
|
1703
|
-
.odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
|
|
1704
|
-
preflights Wrangler before any shown-once issuance or destructive rotation.
|
|
1705
|
-
Provision opens the approval page automatically in interactive terminals;
|
|
1706
|
-
use --open to force browser launch or --no-open to suppress it.
|
|
1707
|
-
`);
|
|
1708
|
-
}
|
|
1709
2109
|
|
|
1710
2110
|
// src/bin.ts
|
|
1711
2111
|
runCli().catch((err) => {
|