@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/index.cjs
CHANGED
|
@@ -33,13 +33,17 @@ var index_exports = {};
|
|
|
33
33
|
__export(index_exports, {
|
|
34
34
|
AGENT_HARNESSES: () => AGENT_HARNESSES,
|
|
35
35
|
CAPABILITIES: () => CAPABILITIES,
|
|
36
|
+
SYSTEM_AI_PURPOSES: () => SYSTEM_AI_PURPOSES,
|
|
37
|
+
adminAi: () => adminAi,
|
|
36
38
|
doctor: () => doctor,
|
|
39
|
+
getScopedPlatformToken: () => getScopedPlatformToken,
|
|
37
40
|
initProject: () => initProject,
|
|
38
41
|
installSkill: () => installSkill,
|
|
39
42
|
printCapabilities: () => printCapabilities,
|
|
40
43
|
provision: () => provision,
|
|
41
44
|
redactSecrets: () => redactSecrets,
|
|
42
45
|
runCli: () => runCli,
|
|
46
|
+
runHostedSecurity: () => runHostedSecurity,
|
|
43
47
|
secretsPush: () => secretsPush,
|
|
44
48
|
smoke: () => smoke
|
|
45
49
|
});
|
|
@@ -50,7 +54,459 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
|
|
|
50
54
|
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
51
55
|
|
|
52
56
|
// src/cli.ts
|
|
53
|
-
var
|
|
57
|
+
var import_security2 = require("@odla-ai/security");
|
|
58
|
+
|
|
59
|
+
// src/admin-ai.ts
|
|
60
|
+
var import_node_process3 = __toESM(require("process"), 1);
|
|
61
|
+
var import_node_fs2 = require("fs");
|
|
62
|
+
var import_db2 = require("@odla-ai/db");
|
|
63
|
+
|
|
64
|
+
// src/open.ts
|
|
65
|
+
var import_node_child_process = require("child_process");
|
|
66
|
+
var import_node_process = __toESM(require("process"), 1);
|
|
67
|
+
async function openUrl(url, options = {}) {
|
|
68
|
+
const command = openerFor(options.platform ?? import_node_process.default.platform);
|
|
69
|
+
const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
|
|
70
|
+
await new Promise((resolve7, reject) => {
|
|
71
|
+
const child = doSpawn(command.cmd, [...command.args, url], {
|
|
72
|
+
stdio: "ignore",
|
|
73
|
+
detached: true
|
|
74
|
+
});
|
|
75
|
+
child.once("error", reject);
|
|
76
|
+
child.once("spawn", () => {
|
|
77
|
+
child.unref();
|
|
78
|
+
resolve7();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function openerFor(platform) {
|
|
83
|
+
if (platform === "darwin") return { cmd: "open", args: [] };
|
|
84
|
+
if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
|
|
85
|
+
return { cmd: "xdg-open", args: [] };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/token.ts
|
|
89
|
+
var import_db = require("@odla-ai/db");
|
|
90
|
+
var import_node_process2 = __toESM(require("process"), 1);
|
|
91
|
+
|
|
92
|
+
// src/local.ts
|
|
93
|
+
var import_node_fs = require("fs");
|
|
94
|
+
var import_node_path = require("path");
|
|
95
|
+
var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
|
|
96
|
+
function readJsonFile(path) {
|
|
97
|
+
try {
|
|
98
|
+
return JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function writePrivateJson(path, value) {
|
|
104
|
+
writePrivateText(path, `${JSON.stringify(value, null, 2)}
|
|
105
|
+
`);
|
|
106
|
+
}
|
|
107
|
+
function readCredentials(path) {
|
|
108
|
+
if (!(0, import_node_fs.existsSync)(path)) return null;
|
|
109
|
+
let value;
|
|
110
|
+
try {
|
|
111
|
+
value = JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
|
|
112
|
+
} catch {
|
|
113
|
+
throw new Error(`credentials file ${path} is not valid JSON; fix or remove it before provisioning`);
|
|
114
|
+
}
|
|
115
|
+
if (!value || typeof value !== "object" || typeof value.appId !== "string" || !value.envs || typeof value.envs !== "object") {
|
|
116
|
+
throw new Error(`credentials file ${path} has an invalid shape; fix or remove it before provisioning`);
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
function mergeCredential(current, update) {
|
|
121
|
+
const next = current ?? {
|
|
122
|
+
appId: update.appId,
|
|
123
|
+
platformUrl: update.platformUrl,
|
|
124
|
+
dbEndpoint: update.dbEndpoint,
|
|
125
|
+
updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
126
|
+
envs: {}
|
|
127
|
+
};
|
|
128
|
+
next.appId = update.appId;
|
|
129
|
+
next.platformUrl = update.platformUrl;
|
|
130
|
+
next.dbEndpoint = update.dbEndpoint;
|
|
131
|
+
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
132
|
+
next.envs[update.env] = {
|
|
133
|
+
...next.envs[update.env] ?? {},
|
|
134
|
+
tenantId: update.tenantId,
|
|
135
|
+
...update.dbKey ? { dbKey: update.dbKey } : {},
|
|
136
|
+
...update.o11yToken ? { o11yToken: update.o11yToken } : {}
|
|
137
|
+
};
|
|
138
|
+
return next;
|
|
139
|
+
}
|
|
140
|
+
function ensureGitignore(rootDir, localPaths = []) {
|
|
141
|
+
const path = (0, import_node_path.resolve)(rootDir, ".gitignore");
|
|
142
|
+
const existing = (0, import_node_fs.existsSync)(path) ? (0, import_node_fs.readFileSync)(path, "utf8") : "";
|
|
143
|
+
const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line) => !!line);
|
|
144
|
+
const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
|
|
145
|
+
const missing = wanted.filter((line) => !existing.split(/\r?\n/).includes(line));
|
|
146
|
+
if (missing.length === 0) return;
|
|
147
|
+
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
148
|
+
(0, import_node_fs.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
|
|
149
|
+
`);
|
|
150
|
+
}
|
|
151
|
+
function o11yDevVars(cfg) {
|
|
152
|
+
if (!cfg.services.includes("o11y")) return void 0;
|
|
153
|
+
return {
|
|
154
|
+
endpoint: cfg.o11y?.endpoint ?? "https://o11y.odla.ai",
|
|
155
|
+
service: cfg.o11y?.service ?? cfg.app.id,
|
|
156
|
+
...cfg.o11y?.version ? { version: cfg.o11y.version } : {}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function resolveWriteDevVarsTarget(cfg, requested) {
|
|
160
|
+
if (!requested) return null;
|
|
161
|
+
if (requested === true) return cfg.local.devVarsFile;
|
|
162
|
+
return (0, import_node_path.resolve)((0, import_node_path.dirname)(cfg.configPath), requested);
|
|
163
|
+
}
|
|
164
|
+
function writeDevVars(path, credentials, env, o11y) {
|
|
165
|
+
const entry = credentials.envs[env];
|
|
166
|
+
if (!entry) throw new Error(`no credentials for env "${env}" in ${path}`);
|
|
167
|
+
const lines = [`ODLA_PLATFORM="${credentials.platformUrl}"`];
|
|
168
|
+
if (entry.dbKey) lines.push(`ODLA_ENDPOINT="${credentials.dbEndpoint}"`);
|
|
169
|
+
lines.push(`ODLA_APP_ID="${credentials.appId}"`, `ODLA_ENV="${env}"`, `ODLA_TENANT="${entry.tenantId}"`);
|
|
170
|
+
if (entry.dbKey) lines.push(`ODLA_API_KEY="${entry.dbKey}"`);
|
|
171
|
+
if (o11y) {
|
|
172
|
+
lines.push(`ODLA_O11Y_ENDPOINT="${o11y.endpoint}"`, `ODLA_O11Y_SERVICE="${o11y.service}"`);
|
|
173
|
+
if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
|
|
174
|
+
if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
|
|
175
|
+
}
|
|
176
|
+
const existing = (0, import_node_fs.existsSync)(path) ? (0, import_node_fs.readFileSync)(path, "utf8") : "";
|
|
177
|
+
const retained = existing.split(/\r?\n/).filter((line) => !isManagedDevVar(line));
|
|
178
|
+
while (retained.at(-1) === "") retained.pop();
|
|
179
|
+
const prefix = retained.length ? `${retained.join("\n")}
|
|
180
|
+
|
|
181
|
+
` : "";
|
|
182
|
+
writePrivateText(path, `${prefix}${lines.join("\n")}
|
|
183
|
+
`);
|
|
184
|
+
}
|
|
185
|
+
var MANAGED_DEV_VARS = /* @__PURE__ */ new Set([
|
|
186
|
+
"ODLA_PLATFORM",
|
|
187
|
+
"ODLA_ENDPOINT",
|
|
188
|
+
"ODLA_APP_ID",
|
|
189
|
+
"ODLA_ENV",
|
|
190
|
+
"ODLA_TENANT",
|
|
191
|
+
"ODLA_API_KEY",
|
|
192
|
+
"ODLA_O11Y_ENDPOINT",
|
|
193
|
+
"ODLA_O11Y_SERVICE",
|
|
194
|
+
"ODLA_O11Y_VERSION",
|
|
195
|
+
"ODLA_O11Y_TOKEN"
|
|
196
|
+
]);
|
|
197
|
+
function isManagedDevVar(line) {
|
|
198
|
+
const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
|
|
199
|
+
return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
|
|
200
|
+
}
|
|
201
|
+
function writePrivateText(path, text) {
|
|
202
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
|
|
203
|
+
const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
204
|
+
(0, import_node_fs.writeFileSync)(temporary, text, { mode: 384 });
|
|
205
|
+
(0, import_node_fs.chmodSync)(temporary, 384);
|
|
206
|
+
(0, import_node_fs.renameSync)(temporary, path);
|
|
207
|
+
}
|
|
208
|
+
function gitignoreEntry(rootDir, path) {
|
|
209
|
+
const rel = (0, import_node_path.relative)((0, import_node_path.resolve)(rootDir), (0, import_node_path.resolve)(path));
|
|
210
|
+
if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path.isAbsolute)(rel)) return null;
|
|
211
|
+
return rel.replaceAll("\\", "/");
|
|
212
|
+
}
|
|
213
|
+
function displayPath(path, rootDir = process.cwd()) {
|
|
214
|
+
const rel = (0, import_node_path.relative)(rootDir, path);
|
|
215
|
+
return rel && !rel.startsWith("..") ? rel : path;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/token.ts
|
|
219
|
+
async function getDeveloperToken(cfg, options, doFetch, out) {
|
|
220
|
+
if (options.token) return options.token;
|
|
221
|
+
if (import_node_process2.default.env.ODLA_DEV_TOKEN) return import_node_process2.default.env.ODLA_DEV_TOKEN;
|
|
222
|
+
const cached = readJsonFile(cfg.local.tokenFile);
|
|
223
|
+
if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
224
|
+
out.log(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
225
|
+
return cached.token;
|
|
226
|
+
}
|
|
227
|
+
const browser = approvalBrowser(options);
|
|
228
|
+
const { token, expiresAt } = await (0, import_db.requestToken)({
|
|
229
|
+
endpoint: cfg.platformUrl,
|
|
230
|
+
label: `${cfg.app.id} provisioner`,
|
|
231
|
+
fetch: doFetch,
|
|
232
|
+
onCode: async ({ userCode, expiresIn }) => {
|
|
233
|
+
const approvalUrl = handshakeUrl(cfg.platformUrl, userCode);
|
|
234
|
+
out.log("");
|
|
235
|
+
out.log(`Approve code ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
|
|
236
|
+
if (browser.open) {
|
|
237
|
+
try {
|
|
238
|
+
await (options.openApprovalUrl ?? openUrl)(approvalUrl);
|
|
239
|
+
out.log(`auth: opened browser for approval${browser.mode === "auto" ? " (auto)" : ""}`);
|
|
240
|
+
} catch (err) {
|
|
241
|
+
out.log(`auth: could not open browser (${err instanceof Error ? err.message : String(err)})`);
|
|
242
|
+
}
|
|
243
|
+
} else if (browser.reason) {
|
|
244
|
+
out.log(`auth: browser launch skipped (${browser.reason})`);
|
|
245
|
+
}
|
|
246
|
+
out.log("");
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
writePrivateJson(cfg.local.tokenFile, { token, expiresAt });
|
|
250
|
+
out.log(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
251
|
+
return token;
|
|
252
|
+
}
|
|
253
|
+
function approvalBrowser(options) {
|
|
254
|
+
if (options.open === true) return { open: true, mode: "forced" };
|
|
255
|
+
if (options.open === false) return { open: false, reason: "disabled by --no-open" };
|
|
256
|
+
if (import_node_process2.default.env.CI) return { open: false, reason: "CI environment" };
|
|
257
|
+
const interactive = options.interactive ?? Boolean(import_node_process2.default.stdin.isTTY && import_node_process2.default.stdout.isTTY);
|
|
258
|
+
if (!interactive) return { open: false, reason: "non-interactive shell; pass --open to force" };
|
|
259
|
+
return { open: true, mode: "auto" };
|
|
260
|
+
}
|
|
261
|
+
function handshakeUrl(platformUrl, userCode) {
|
|
262
|
+
const url = new URL("/studio", platformUrl);
|
|
263
|
+
url.searchParams.set("code", userCode);
|
|
264
|
+
return url.toString();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/admin-ai.ts
|
|
268
|
+
var SYSTEM_AI_PURPOSES = ["o11y.triage", "security.discovery", "security.validation"];
|
|
269
|
+
async function getScopedPlatformToken(options) {
|
|
270
|
+
const fromEnv = import_node_process3.default.env.ODLA_ADMIN_TOKEN;
|
|
271
|
+
if (fromEnv) return fromEnv;
|
|
272
|
+
return scopedToken(
|
|
273
|
+
options.platform.replace(/\/$/, ""),
|
|
274
|
+
options.scope,
|
|
275
|
+
{ action: "show", ...options },
|
|
276
|
+
options.fetch ?? fetch,
|
|
277
|
+
options.stdout ?? console
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
async function adminAi(options) {
|
|
281
|
+
const platform = (options.platform ?? import_node_process3.default.env.ODLA_PLATFORM ?? "https://odla.ai").replace(/\/$/, "");
|
|
282
|
+
const doFetch = options.fetch ?? fetch;
|
|
283
|
+
const out = options.stdout ?? console;
|
|
284
|
+
const scope = options.action === "set" || options.action === "credential-set" ? "platform:ai:write" : "platform:ai:read";
|
|
285
|
+
const token = options.token ?? import_node_process3.default.env.ODLA_ADMIN_TOKEN ?? await scopedToken(platform, scope, options, doFetch, out);
|
|
286
|
+
const headers = { authorization: `Bearer ${token}`, "content-type": "application/json" };
|
|
287
|
+
if (options.action === "credentials") {
|
|
288
|
+
const res2 = await doFetch(`${platform}/registry/platform/ai-credentials`, { headers });
|
|
289
|
+
const body2 = await responseBody(res2);
|
|
290
|
+
if (!res2.ok) throw new Error(apiError("read platform AI credential status", res2.status, body2));
|
|
291
|
+
const credentials = isRecord(body2) && isRecord(body2.credentials) ? body2.credentials : body2;
|
|
292
|
+
if (options.json) out.log(JSON.stringify({ credentials }, null, 2));
|
|
293
|
+
else for (const [provider, status] of Object.entries(isRecord(credentials) ? credentials : {})) {
|
|
294
|
+
out.log(`${provider} ${isRecord(status) && status.set === true ? "set" : "not set"}`);
|
|
295
|
+
}
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (options.action === "credential-set") {
|
|
299
|
+
const provider = requireProvider(options.credentialProvider);
|
|
300
|
+
const value = await credentialValue(options);
|
|
301
|
+
const res2 = await doFetch(`${platform}/registry/platform/ai-credentials/${encodeURIComponent(provider)}`, {
|
|
302
|
+
method: "PUT",
|
|
303
|
+
headers,
|
|
304
|
+
body: JSON.stringify({ value })
|
|
305
|
+
});
|
|
306
|
+
const body2 = await responseBody(res2);
|
|
307
|
+
if (!res2.ok) throw new Error(apiError(`store ${provider} platform credential`, res2.status, body2));
|
|
308
|
+
out.log(`stored ${provider} platform credential (write-only; value was not returned)`);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (options.action === "show") {
|
|
312
|
+
const res2 = await doFetch(`${platform}/registry/platform/ai-policies`, { headers });
|
|
313
|
+
const body2 = await responseBody(res2);
|
|
314
|
+
if (!res2.ok) throw new Error(apiError("read platform AI policies", res2.status, body2));
|
|
315
|
+
const policies = policyArray(body2);
|
|
316
|
+
if (options.json) {
|
|
317
|
+
out.log(JSON.stringify({ policies }, null, 2));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
out.log("purpose enabled provider model max calls max input bytes max output tokens");
|
|
321
|
+
for (const policy2 of policies) {
|
|
322
|
+
out.log([
|
|
323
|
+
policy2.purpose,
|
|
324
|
+
String(policy2.enabled),
|
|
325
|
+
policy2.provider,
|
|
326
|
+
policy2.model,
|
|
327
|
+
String(policy2.maxCallsPerRun ?? ""),
|
|
328
|
+
String(policy2.maxInputBytes ?? ""),
|
|
329
|
+
String(policy2.maxOutputTokens ?? "")
|
|
330
|
+
].join(" "));
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const purpose = requirePurpose(options.purpose);
|
|
335
|
+
if (!options.provider?.trim()) throw new Error("--provider is required");
|
|
336
|
+
if (!options.model?.trim()) throw new Error("--model is required");
|
|
337
|
+
const body = {
|
|
338
|
+
provider: options.provider.trim(),
|
|
339
|
+
model: options.model.trim(),
|
|
340
|
+
...options.enabled === void 0 ? {} : { enabled: options.enabled },
|
|
341
|
+
...options.maxInputBytes === void 0 ? {} : { maxInputBytes: options.maxInputBytes },
|
|
342
|
+
...options.maxOutputTokens === void 0 ? {} : { maxOutputTokens: options.maxOutputTokens },
|
|
343
|
+
...options.maxCallsPerRun === void 0 ? {} : { maxCallsPerRun: options.maxCallsPerRun }
|
|
344
|
+
};
|
|
345
|
+
const res = await doFetch(`${platform}/registry/platform/ai-policies/${encodeURIComponent(purpose)}`, {
|
|
346
|
+
method: "PUT",
|
|
347
|
+
headers,
|
|
348
|
+
body: JSON.stringify(body)
|
|
349
|
+
});
|
|
350
|
+
const response = await responseBody(res);
|
|
351
|
+
if (!res.ok) throw new Error(apiError(`update ${purpose}`, res.status, response));
|
|
352
|
+
const policy = isRecord(response) && isRecord(response.policy) ? response.policy : isRecord(response) ? response : {};
|
|
353
|
+
out.log(options.json ? JSON.stringify({ policy }, null, 2) : `updated ${purpose}: ${String(policy.provider ?? body.provider)}/${String(policy.model ?? body.model)}`);
|
|
354
|
+
}
|
|
355
|
+
async function scopedToken(platform, scope, options, doFetch, out) {
|
|
356
|
+
const tokenFile = options.tokenFile ?? `${import_node_process3.default.cwd()}/.odla/admin-token.local.json`;
|
|
357
|
+
const cache = readJsonFile(tokenFile);
|
|
358
|
+
const cached = cache?.platform === platform ? cache.tokens?.[scope] : void 0;
|
|
359
|
+
if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
360
|
+
out.log(`auth: using cached ${scope} grant (${tokenFile})`);
|
|
361
|
+
return cached.token;
|
|
362
|
+
}
|
|
363
|
+
const { token, expiresAt } = await (0, import_db2.requestToken)({
|
|
364
|
+
endpoint: platform,
|
|
365
|
+
label: `odla CLI admin AI (${scope})`,
|
|
366
|
+
scopes: [scope],
|
|
367
|
+
fetch: doFetch,
|
|
368
|
+
onCode: async ({ userCode, expiresIn }) => {
|
|
369
|
+
const approvalUrl = handshakeUrl(platform, userCode);
|
|
370
|
+
out.log(`Approve scoped request ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
|
|
371
|
+
const shouldOpen = options.open === true || options.open !== false && !import_node_process3.default.env.CI && Boolean(import_node_process3.default.stdin.isTTY && import_node_process3.default.stdout.isTTY);
|
|
372
|
+
if (shouldOpen) await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
const tokens = cache?.platform === platform ? { ...cache.tokens ?? {} } : {};
|
|
376
|
+
tokens[scope] = { token, expiresAt };
|
|
377
|
+
if ((0, import_node_fs2.existsSync)(`${import_node_process3.default.cwd()}/.git`)) ensureGitignore(import_node_process3.default.cwd(), [tokenFile]);
|
|
378
|
+
writePrivateJson(tokenFile, { platform, tokens });
|
|
379
|
+
out.log(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
|
|
380
|
+
return token;
|
|
381
|
+
}
|
|
382
|
+
function requirePurpose(value) {
|
|
383
|
+
if (!SYSTEM_AI_PURPOSES.includes(value)) {
|
|
384
|
+
throw new Error(`purpose must be one of: ${SYSTEM_AI_PURPOSES.join(", ")}`);
|
|
385
|
+
}
|
|
386
|
+
return value;
|
|
387
|
+
}
|
|
388
|
+
function requireProvider(value) {
|
|
389
|
+
if (value !== "anthropic" && value !== "openai" && value !== "google") {
|
|
390
|
+
throw new Error("provider must be one of: anthropic, openai, google");
|
|
391
|
+
}
|
|
392
|
+
return value;
|
|
393
|
+
}
|
|
394
|
+
async function credentialValue(options) {
|
|
395
|
+
if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
|
|
396
|
+
let value;
|
|
397
|
+
if (options.fromEnv) value = import_node_process3.default.env[options.fromEnv];
|
|
398
|
+
else if (options.stdin) value = await (options.readStdin ?? readStdin)();
|
|
399
|
+
else throw new Error("credential input required: use --from-env <NAME> or --stdin; values are never accepted as arguments");
|
|
400
|
+
value = value?.replace(/[\r\n]+$/, "");
|
|
401
|
+
if (!value) throw new Error(options.fromEnv ? `environment variable ${options.fromEnv} is empty or unset` : "stdin credential is empty");
|
|
402
|
+
if (new TextEncoder().encode(value).byteLength > 64 * 1024) throw new Error("credential exceeds 64 KiB");
|
|
403
|
+
return value;
|
|
404
|
+
}
|
|
405
|
+
async function readStdin() {
|
|
406
|
+
let value = "";
|
|
407
|
+
for await (const chunk of import_node_process3.default.stdin) {
|
|
408
|
+
value += String(chunk);
|
|
409
|
+
if (value.length > 64 * 1024) throw new Error("credential exceeds 64 KiB");
|
|
410
|
+
}
|
|
411
|
+
return value;
|
|
412
|
+
}
|
|
413
|
+
function policyArray(body) {
|
|
414
|
+
if (isRecord(body) && Array.isArray(body.policies)) return body.policies.filter(isRecord);
|
|
415
|
+
if (isRecord(body) && isRecord(body.policies)) return Object.values(body.policies).filter(isRecord);
|
|
416
|
+
throw new Error("platform returned an invalid AI policy response");
|
|
417
|
+
}
|
|
418
|
+
async function responseBody(res) {
|
|
419
|
+
const text = await res.text();
|
|
420
|
+
if (!text) return {};
|
|
421
|
+
try {
|
|
422
|
+
return JSON.parse(text);
|
|
423
|
+
} catch {
|
|
424
|
+
return { message: text.slice(0, 300) };
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
function apiError(action, status, body) {
|
|
428
|
+
const error = isRecord(body) && isRecord(body.error) ? body.error : void 0;
|
|
429
|
+
const message = error && typeof error.message === "string" ? error.message : isRecord(body) && typeof body.message === "string" ? body.message : "request failed";
|
|
430
|
+
return `${action} failed (${status}): ${message}`;
|
|
431
|
+
}
|
|
432
|
+
function isRecord(value) {
|
|
433
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/argv.ts
|
|
437
|
+
function parseArgv(argv) {
|
|
438
|
+
const positionals = [];
|
|
439
|
+
const options = {};
|
|
440
|
+
for (let i = 0; i < argv.length; i++) {
|
|
441
|
+
const arg = argv[i];
|
|
442
|
+
if (!arg.startsWith("--")) {
|
|
443
|
+
positionals.push(arg);
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
const eq = arg.indexOf("=");
|
|
447
|
+
const rawName = arg.slice(2, eq === -1 ? void 0 : eq);
|
|
448
|
+
if (!rawName) continue;
|
|
449
|
+
if (rawName.startsWith("no-")) {
|
|
450
|
+
options[rawName.slice(3)] = false;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (eq !== -1) {
|
|
454
|
+
addOption(options, rawName, arg.slice(eq + 1));
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const value = argv[i + 1];
|
|
458
|
+
if (value !== void 0 && !value.startsWith("--")) {
|
|
459
|
+
i++;
|
|
460
|
+
addOption(options, rawName, value);
|
|
461
|
+
} else {
|
|
462
|
+
addOption(options, rawName, true);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return { positionals, options };
|
|
466
|
+
}
|
|
467
|
+
function assertArgs(parsed, allowedOptions, maxPositionals) {
|
|
468
|
+
const allowed = new Set(allowedOptions);
|
|
469
|
+
for (const name of Object.keys(parsed.options)) {
|
|
470
|
+
if (!allowed.has(name)) throw new Error(`unknown option "--${name}"; run "odla-ai help" for supported options`);
|
|
471
|
+
}
|
|
472
|
+
if (parsed.positionals.length > maxPositionals) {
|
|
473
|
+
throw new Error(`unexpected argument "${parsed.positionals[maxPositionals]}"; run "odla-ai help"`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function requiredString(value, name) {
|
|
477
|
+
const result = stringOpt(value);
|
|
478
|
+
if (!result) throw new Error(`${name} is required`);
|
|
479
|
+
return result;
|
|
480
|
+
}
|
|
481
|
+
function stringOpt(value) {
|
|
482
|
+
if (typeof value === "string") return value;
|
|
483
|
+
if (Array.isArray(value)) return value[value.length - 1];
|
|
484
|
+
return void 0;
|
|
485
|
+
}
|
|
486
|
+
function listOpt(value) {
|
|
487
|
+
if (value === void 0 || typeof value === "boolean") return void 0;
|
|
488
|
+
const values = Array.isArray(value) ? value : [value];
|
|
489
|
+
return values.flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
|
|
490
|
+
}
|
|
491
|
+
function boolOpt(value) {
|
|
492
|
+
if (typeof value === "boolean") return value;
|
|
493
|
+
if (typeof value === "string" && (value === "true" || value === "false")) return value === "true";
|
|
494
|
+
if (value === void 0) return void 0;
|
|
495
|
+
throw new Error("boolean option must be true or false");
|
|
496
|
+
}
|
|
497
|
+
function numberOpt(value, flag) {
|
|
498
|
+
if (value === void 0) return void 0;
|
|
499
|
+
const raw = stringOpt(value);
|
|
500
|
+
const parsed = raw === void 0 ? Number.NaN : Number(raw);
|
|
501
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) throw new Error(`${flag} requires a positive integer`);
|
|
502
|
+
return parsed;
|
|
503
|
+
}
|
|
504
|
+
function addOption(options, name, value) {
|
|
505
|
+
const current = options[name];
|
|
506
|
+
if (current === void 0) options[name] = value;
|
|
507
|
+
else if (Array.isArray(current)) current.push(String(value));
|
|
508
|
+
else options[name] = [String(current), String(value)];
|
|
509
|
+
}
|
|
54
510
|
|
|
55
511
|
// src/capabilities.ts
|
|
56
512
|
var CAPABILITIES = {
|
|
@@ -59,7 +515,9 @@ var CAPABILITIES = {
|
|
|
59
515
|
"issue, persist, and explicitly rotate configured service credentials",
|
|
60
516
|
"write local Worker values and push deployed Worker secrets through Wrangler stdin",
|
|
61
517
|
"push db schema/rules and configure platform AI, auth, and deployment links",
|
|
62
|
-
"validate config offline and smoke-test a provisioned db environment"
|
|
518
|
+
"validate config offline and smoke-test a provisioned db environment",
|
|
519
|
+
"run app-attributed hosted security discovery and independent validation without provider keys",
|
|
520
|
+
"let admins manage system AI routes/credentials through short-lived exact-scope approval"
|
|
63
521
|
],
|
|
64
522
|
agent: [
|
|
65
523
|
"install and import the selected odla SDKs",
|
|
@@ -70,11 +528,13 @@ var CAPABILITIES = {
|
|
|
70
528
|
"approve the odla device code and one-off third-party logins",
|
|
71
529
|
"consent to production changes with --yes",
|
|
72
530
|
"request destructive credential rotation explicitly",
|
|
73
|
-
"review application semantics, security findings, releases, and merges"
|
|
531
|
+
"review application semantics, security findings, releases, and merges",
|
|
532
|
+
"approve redacted-source disclosure before hosted security reasoning"
|
|
74
533
|
],
|
|
75
534
|
studio: [
|
|
76
535
|
"view telemetry and environment state",
|
|
77
|
-
"perform manual credential recovery when the CLI's local shown-once copy is unavailable"
|
|
536
|
+
"perform manual credential recovery when the CLI's local shown-once copy is unavailable",
|
|
537
|
+
"configure system AI purposes and view app/environment/run-attributed usage"
|
|
78
538
|
]
|
|
79
539
|
};
|
|
80
540
|
function printCapabilities(json = false, out = console) {
|
|
@@ -95,28 +555,28 @@ function printGroup(out, heading, items) {
|
|
|
95
555
|
}
|
|
96
556
|
|
|
97
557
|
// src/config.ts
|
|
98
|
-
var
|
|
99
|
-
var
|
|
558
|
+
var import_node_fs3 = require("fs");
|
|
559
|
+
var import_node_path2 = require("path");
|
|
100
560
|
var import_node_url = require("url");
|
|
101
561
|
var DEFAULT_PLATFORM = "https://odla.ai";
|
|
102
562
|
var DEFAULT_ENVS = ["dev"];
|
|
103
563
|
var DEFAULT_SERVICES = ["db", "ai"];
|
|
104
564
|
async function loadProjectConfig(configPath = "odla.config.mjs") {
|
|
105
|
-
const resolved = (0,
|
|
106
|
-
if (!(0,
|
|
565
|
+
const resolved = (0, import_node_path2.resolve)(configPath);
|
|
566
|
+
if (!(0, import_node_fs3.existsSync)(resolved)) {
|
|
107
567
|
throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
|
|
108
568
|
}
|
|
109
569
|
const raw = await loadConfigModule(resolved);
|
|
110
|
-
const rootDir = (0,
|
|
570
|
+
const rootDir = (0, import_node_path2.dirname)(resolved);
|
|
111
571
|
validateRawConfig(raw, resolved);
|
|
112
572
|
const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
|
|
113
573
|
const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
|
|
114
574
|
const envs = unique(raw.envs?.length ? raw.envs : DEFAULT_ENVS);
|
|
115
575
|
const services = unique(raw.services?.length ? raw.services : DEFAULT_SERVICES);
|
|
116
576
|
const local = {
|
|
117
|
-
tokenFile: (0,
|
|
118
|
-
credentialsFile: (0,
|
|
119
|
-
devVarsFile: (0,
|
|
577
|
+
tokenFile: (0, import_node_path2.resolve)(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
|
|
578
|
+
credentialsFile: (0, import_node_path2.resolve)(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
|
|
579
|
+
devVarsFile: (0, import_node_path2.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
|
|
120
580
|
gitignore: raw.local?.gitignore ?? true
|
|
121
581
|
};
|
|
122
582
|
return {
|
|
@@ -133,9 +593,9 @@ async function loadProjectConfig(configPath = "odla.config.mjs") {
|
|
|
133
593
|
async function resolveDataExport(cfg, value, names) {
|
|
134
594
|
if (value === void 0 || value === null || value === false) return void 0;
|
|
135
595
|
if (typeof value !== "string") return value;
|
|
136
|
-
const target = (0,
|
|
596
|
+
const target = (0, import_node_path2.isAbsolute)(value) ? value : (0, import_node_path2.resolve)(cfg.rootDir, value);
|
|
137
597
|
if (target.endsWith(".json")) {
|
|
138
|
-
return JSON.parse((0,
|
|
598
|
+
return JSON.parse((0, import_node_fs3.readFileSync)(target, "utf8"));
|
|
139
599
|
}
|
|
140
600
|
const mod = await import((0, import_node_url.pathToFileURL)(target).href);
|
|
141
601
|
for (const name of names) {
|
|
@@ -186,175 +646,49 @@ function validId(value) {
|
|
|
186
646
|
return typeof value === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value);
|
|
187
647
|
}
|
|
188
648
|
async function loadConfigModule(path) {
|
|
189
|
-
if (path.endsWith(".json")) return JSON.parse((0,
|
|
649
|
+
if (path.endsWith(".json")) return JSON.parse((0, import_node_fs3.readFileSync)(path, "utf8"));
|
|
190
650
|
const mod = await import(`${(0, import_node_url.pathToFileURL)(path).href}?t=${Date.now()}`);
|
|
191
651
|
const value = mod.default ?? mod.config;
|
|
192
652
|
if (typeof value === "function") return await value();
|
|
193
653
|
return value;
|
|
194
654
|
}
|
|
195
655
|
function trimSlash(value) {
|
|
196
|
-
return value.replace(/\/+$/, "");
|
|
197
|
-
}
|
|
198
|
-
function unique(values) {
|
|
199
|
-
return [...new Set(values.filter(Boolean))];
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// src/doctor-checks.ts
|
|
203
|
-
var import_node_child_process2 = require("child_process");
|
|
204
|
-
var import_node_fs4 = require("fs");
|
|
205
|
-
var import_node_path4 = require("path");
|
|
206
|
-
|
|
207
|
-
// src/redact.ts
|
|
208
|
-
var REPLACEMENTS = [
|
|
209
|
-
[/odla_(sk|dev)_[A-Za-z0-9._-]+/g, "odla_$1_[redacted]"],
|
|
210
|
-
[/\bsk_(live|test)_[A-Za-z0-9]+/g, "sk_$1_[redacted]"],
|
|
211
|
-
[/\bsk-[A-Za-z0-9._-]+/g, "sk-[redacted]"],
|
|
212
|
-
[/\bwhsec_[A-Za-z0-9+/=]+/g, "whsec_[redacted]"],
|
|
213
|
-
[/\bo11y_[A-Za-z0-9]+/g, "o11y_[redacted]"],
|
|
214
|
-
[/\b(ghp|gho|github_pat)_[A-Za-z0-9_]+/g, "$1_[redacted]"],
|
|
215
|
-
[/\bAKIA[A-Z0-9]{12,}/g, "AKIA[redacted]"]
|
|
216
|
-
];
|
|
217
|
-
function redactSecrets(value) {
|
|
218
|
-
let result = value;
|
|
219
|
-
for (const [pattern, replacement] of REPLACEMENTS) result = result.replace(pattern, replacement);
|
|
220
|
-
return result;
|
|
221
|
-
}
|
|
222
|
-
function looksSecret(value) {
|
|
223
|
-
return redactSecrets(value) !== value || value.includes("-----BEGIN");
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// src/local.ts
|
|
227
|
-
var import_node_fs2 = require("fs");
|
|
228
|
-
var import_node_path2 = require("path");
|
|
229
|
-
var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
|
|
230
|
-
function readJsonFile(path) {
|
|
231
|
-
try {
|
|
232
|
-
return JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
|
|
233
|
-
} catch {
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
function writePrivateJson(path, value) {
|
|
238
|
-
writePrivateText(path, `${JSON.stringify(value, null, 2)}
|
|
239
|
-
`);
|
|
240
|
-
}
|
|
241
|
-
function readCredentials(path) {
|
|
242
|
-
if (!(0, import_node_fs2.existsSync)(path)) return null;
|
|
243
|
-
let value;
|
|
244
|
-
try {
|
|
245
|
-
value = JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
|
|
246
|
-
} catch {
|
|
247
|
-
throw new Error(`credentials file ${path} is not valid JSON; fix or remove it before provisioning`);
|
|
248
|
-
}
|
|
249
|
-
if (!value || typeof value !== "object" || typeof value.appId !== "string" || !value.envs || typeof value.envs !== "object") {
|
|
250
|
-
throw new Error(`credentials file ${path} has an invalid shape; fix or remove it before provisioning`);
|
|
251
|
-
}
|
|
252
|
-
return value;
|
|
253
|
-
}
|
|
254
|
-
function mergeCredential(current, update) {
|
|
255
|
-
const next = current ?? {
|
|
256
|
-
appId: update.appId,
|
|
257
|
-
platformUrl: update.platformUrl,
|
|
258
|
-
dbEndpoint: update.dbEndpoint,
|
|
259
|
-
updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
260
|
-
envs: {}
|
|
261
|
-
};
|
|
262
|
-
next.appId = update.appId;
|
|
263
|
-
next.platformUrl = update.platformUrl;
|
|
264
|
-
next.dbEndpoint = update.dbEndpoint;
|
|
265
|
-
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
266
|
-
next.envs[update.env] = {
|
|
267
|
-
...next.envs[update.env] ?? {},
|
|
268
|
-
tenantId: update.tenantId,
|
|
269
|
-
...update.dbKey ? { dbKey: update.dbKey } : {},
|
|
270
|
-
...update.o11yToken ? { o11yToken: update.o11yToken } : {}
|
|
271
|
-
};
|
|
272
|
-
return next;
|
|
273
|
-
}
|
|
274
|
-
function ensureGitignore(rootDir, localPaths = []) {
|
|
275
|
-
const path = (0, import_node_path2.resolve)(rootDir, ".gitignore");
|
|
276
|
-
const existing = (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : "";
|
|
277
|
-
const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line) => !!line);
|
|
278
|
-
const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
|
|
279
|
-
const missing = wanted.filter((line) => !existing.split(/\r?\n/).includes(line));
|
|
280
|
-
if (missing.length === 0) return;
|
|
281
|
-
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
282
|
-
(0, import_node_fs2.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
|
|
283
|
-
`);
|
|
284
|
-
}
|
|
285
|
-
function o11yDevVars(cfg) {
|
|
286
|
-
if (!cfg.services.includes("o11y")) return void 0;
|
|
287
|
-
return {
|
|
288
|
-
endpoint: cfg.o11y?.endpoint ?? "https://o11y.odla.ai",
|
|
289
|
-
service: cfg.o11y?.service ?? cfg.app.id,
|
|
290
|
-
...cfg.o11y?.version ? { version: cfg.o11y.version } : {}
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
function resolveWriteDevVarsTarget(cfg, requested) {
|
|
294
|
-
if (!requested) return null;
|
|
295
|
-
if (requested === true) return cfg.local.devVarsFile;
|
|
296
|
-
return (0, import_node_path2.resolve)((0, import_node_path2.dirname)(cfg.configPath), requested);
|
|
297
|
-
}
|
|
298
|
-
function writeDevVars(path, credentials, env, o11y) {
|
|
299
|
-
const entry = credentials.envs[env];
|
|
300
|
-
if (!entry) throw new Error(`no credentials for env "${env}" in ${path}`);
|
|
301
|
-
const lines = [`ODLA_PLATFORM="${credentials.platformUrl}"`];
|
|
302
|
-
if (entry.dbKey) lines.push(`ODLA_ENDPOINT="${credentials.dbEndpoint}"`);
|
|
303
|
-
lines.push(`ODLA_APP_ID="${credentials.appId}"`, `ODLA_ENV="${env}"`, `ODLA_TENANT="${entry.tenantId}"`);
|
|
304
|
-
if (entry.dbKey) lines.push(`ODLA_API_KEY="${entry.dbKey}"`);
|
|
305
|
-
if (o11y) {
|
|
306
|
-
lines.push(`ODLA_O11Y_ENDPOINT="${o11y.endpoint}"`, `ODLA_O11Y_SERVICE="${o11y.service}"`);
|
|
307
|
-
if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
|
|
308
|
-
if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
|
|
309
|
-
}
|
|
310
|
-
const existing = (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : "";
|
|
311
|
-
const retained = existing.split(/\r?\n/).filter((line) => !isManagedDevVar(line));
|
|
312
|
-
while (retained.at(-1) === "") retained.pop();
|
|
313
|
-
const prefix = retained.length ? `${retained.join("\n")}
|
|
314
|
-
|
|
315
|
-
` : "";
|
|
316
|
-
writePrivateText(path, `${prefix}${lines.join("\n")}
|
|
317
|
-
`);
|
|
318
|
-
}
|
|
319
|
-
var MANAGED_DEV_VARS = /* @__PURE__ */ new Set([
|
|
320
|
-
"ODLA_PLATFORM",
|
|
321
|
-
"ODLA_ENDPOINT",
|
|
322
|
-
"ODLA_APP_ID",
|
|
323
|
-
"ODLA_ENV",
|
|
324
|
-
"ODLA_TENANT",
|
|
325
|
-
"ODLA_API_KEY",
|
|
326
|
-
"ODLA_O11Y_ENDPOINT",
|
|
327
|
-
"ODLA_O11Y_SERVICE",
|
|
328
|
-
"ODLA_O11Y_VERSION",
|
|
329
|
-
"ODLA_O11Y_TOKEN"
|
|
330
|
-
]);
|
|
331
|
-
function isManagedDevVar(line) {
|
|
332
|
-
const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
|
|
333
|
-
return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
|
|
656
|
+
return value.replace(/\/+$/, "");
|
|
334
657
|
}
|
|
335
|
-
function
|
|
336
|
-
|
|
337
|
-
const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
338
|
-
(0, import_node_fs2.writeFileSync)(temporary, text, { mode: 384 });
|
|
339
|
-
(0, import_node_fs2.chmodSync)(temporary, 384);
|
|
340
|
-
(0, import_node_fs2.renameSync)(temporary, path);
|
|
658
|
+
function unique(values) {
|
|
659
|
+
return [...new Set(values.filter(Boolean))];
|
|
341
660
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
661
|
+
|
|
662
|
+
// src/doctor-checks.ts
|
|
663
|
+
var import_node_child_process3 = require("child_process");
|
|
664
|
+
var import_node_fs5 = require("fs");
|
|
665
|
+
var import_node_path4 = require("path");
|
|
666
|
+
|
|
667
|
+
// src/redact.ts
|
|
668
|
+
var REPLACEMENTS = [
|
|
669
|
+
[/odla_(sk|dev)_[A-Za-z0-9._-]+/g, "odla_$1_[redacted]"],
|
|
670
|
+
[/\bsk_(live|test)_[A-Za-z0-9]+/g, "sk_$1_[redacted]"],
|
|
671
|
+
[/\bsk-[A-Za-z0-9._-]+/g, "sk-[redacted]"],
|
|
672
|
+
[/\bwhsec_[A-Za-z0-9+/=]+/g, "whsec_[redacted]"],
|
|
673
|
+
[/\bo11y_[A-Za-z0-9]+/g, "o11y_[redacted]"],
|
|
674
|
+
[/\b(ghp|gho|github_pat)_[A-Za-z0-9_]+/g, "$1_[redacted]"],
|
|
675
|
+
[/\bAKIA[A-Z0-9]{12,}/g, "AKIA[redacted]"]
|
|
676
|
+
];
|
|
677
|
+
function redactSecrets(value) {
|
|
678
|
+
let result = value;
|
|
679
|
+
for (const [pattern, replacement] of REPLACEMENTS) result = result.replace(pattern, replacement);
|
|
680
|
+
return result;
|
|
346
681
|
}
|
|
347
|
-
function
|
|
348
|
-
|
|
349
|
-
return rel && !rel.startsWith("..") ? rel : path;
|
|
682
|
+
function looksSecret(value) {
|
|
683
|
+
return redactSecrets(value) !== value || value.includes("-----BEGIN");
|
|
350
684
|
}
|
|
351
685
|
|
|
352
686
|
// src/wrangler.ts
|
|
353
|
-
var
|
|
354
|
-
var
|
|
687
|
+
var import_node_child_process2 = require("child_process");
|
|
688
|
+
var import_node_fs4 = require("fs");
|
|
355
689
|
var import_node_path3 = require("path");
|
|
356
690
|
var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
|
|
357
|
-
const child = (0,
|
|
691
|
+
const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
358
692
|
let stdout = "";
|
|
359
693
|
let stderr = "";
|
|
360
694
|
child.stdout.on("data", (chunk) => stdout += chunk.toString());
|
|
@@ -367,14 +701,14 @@ var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"]
|
|
|
367
701
|
function findWranglerConfig(rootDir) {
|
|
368
702
|
for (const name of WRANGLER_CONFIG_FILES) {
|
|
369
703
|
const path = (0, import_node_path3.join)(rootDir, name);
|
|
370
|
-
if ((0,
|
|
704
|
+
if ((0, import_node_fs4.existsSync)(path)) return path;
|
|
371
705
|
}
|
|
372
706
|
return null;
|
|
373
707
|
}
|
|
374
708
|
function readWranglerConfig(path) {
|
|
375
709
|
if (path.endsWith(".toml")) return null;
|
|
376
710
|
try {
|
|
377
|
-
return JSON.parse(stripJsonComments((0,
|
|
711
|
+
return JSON.parse(stripJsonComments((0, import_node_fs4.readFileSync)(path, "utf8")));
|
|
378
712
|
} catch {
|
|
379
713
|
return null;
|
|
380
714
|
}
|
|
@@ -445,7 +779,7 @@ function lintRules(rules, entities, publicRead) {
|
|
|
445
779
|
}
|
|
446
780
|
return warnings;
|
|
447
781
|
}
|
|
448
|
-
var defaultExec = (cmd, args, cwd) => (0,
|
|
782
|
+
var defaultExec = (cmd, args, cwd) => (0, import_node_child_process3.execFileSync)(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
449
783
|
function trackedSecretFiles(rootDir, exec = defaultExec, localPaths = []) {
|
|
450
784
|
let output;
|
|
451
785
|
try {
|
|
@@ -475,7 +809,7 @@ function wranglerWarnings(rootDir) {
|
|
|
475
809
|
const dir = (0, import_node_path4.resolve)(rootDir, assets.directory);
|
|
476
810
|
if (dir === (0, import_node_path4.resolve)(rootDir)) {
|
|
477
811
|
warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
|
|
478
|
-
} else if ((0,
|
|
812
|
+
} else if ((0, import_node_fs5.existsSync)((0, import_node_path4.join)(dir, "node_modules"))) {
|
|
479
813
|
warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
|
|
480
814
|
}
|
|
481
815
|
}
|
|
@@ -511,12 +845,12 @@ function o11yProjectWarnings(rootDir) {
|
|
|
511
845
|
return warnings;
|
|
512
846
|
}
|
|
513
847
|
const main = typeof config.main === "string" ? (0, import_node_path4.resolve)(rootDir, config.main) : null;
|
|
514
|
-
if (!main || !(0,
|
|
848
|
+
if (!main || !(0, import_node_fs5.existsSync)(main)) {
|
|
515
849
|
warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
|
|
516
850
|
} else {
|
|
517
851
|
let source = "";
|
|
518
852
|
try {
|
|
519
|
-
source = (0,
|
|
853
|
+
source = (0, import_node_fs5.readFileSync)(main, "utf8");
|
|
520
854
|
} catch {
|
|
521
855
|
}
|
|
522
856
|
if (!/\bwithObservability\b/.test(source)) {
|
|
@@ -531,7 +865,7 @@ function o11yProjectWarnings(rootDir) {
|
|
|
531
865
|
}
|
|
532
866
|
function readPackageJson(rootDir) {
|
|
533
867
|
try {
|
|
534
|
-
return JSON.parse((0,
|
|
868
|
+
return JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(rootDir, "package.json"), "utf8"));
|
|
535
869
|
} catch {
|
|
536
870
|
return null;
|
|
537
871
|
}
|
|
@@ -598,13 +932,13 @@ async function doctor(options) {
|
|
|
598
932
|
}
|
|
599
933
|
|
|
600
934
|
// src/init.ts
|
|
601
|
-
var
|
|
935
|
+
var import_node_fs6 = require("fs");
|
|
602
936
|
var import_node_path5 = require("path");
|
|
603
937
|
function initProject(options) {
|
|
604
938
|
const out = options.stdout ?? console;
|
|
605
939
|
const rootDir = (0, import_node_path5.resolve)(options.rootDir ?? process.cwd());
|
|
606
940
|
const configPath = (0, import_node_path5.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
|
|
607
|
-
if ((0,
|
|
941
|
+
if ((0, import_node_fs6.existsSync)(configPath) && !options.force) {
|
|
608
942
|
throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
|
|
609
943
|
}
|
|
610
944
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
|
|
@@ -613,10 +947,10 @@ function initProject(options) {
|
|
|
613
947
|
const envs = options.envs?.length ? options.envs : ["dev"];
|
|
614
948
|
const services = options.services?.length ? options.services : ["db", "ai"];
|
|
615
949
|
const aiProvider = options.aiProvider ?? "anthropic";
|
|
616
|
-
(0,
|
|
617
|
-
(0,
|
|
618
|
-
(0,
|
|
619
|
-
(0,
|
|
950
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path5.dirname)(configPath), { recursive: true });
|
|
951
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path5.resolve)(rootDir, "src/odla"), { recursive: true });
|
|
952
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path5.resolve)(rootDir, ".odla"), { recursive: true });
|
|
953
|
+
(0, import_node_fs6.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
|
|
620
954
|
writeIfMissing((0, import_node_path5.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
|
|
621
955
|
writeIfMissing((0, import_node_path5.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
|
|
622
956
|
ensureGitignore(rootDir);
|
|
@@ -625,8 +959,8 @@ function initProject(options) {
|
|
|
625
959
|
out.log("updated .gitignore for local odla credentials");
|
|
626
960
|
}
|
|
627
961
|
function writeIfMissing(path, text) {
|
|
628
|
-
if ((0,
|
|
629
|
-
(0,
|
|
962
|
+
if ((0, import_node_fs6.existsSync)(path)) return;
|
|
963
|
+
(0, import_node_fs6.writeFileSync)(path, text);
|
|
630
964
|
}
|
|
631
965
|
function configTemplate(input) {
|
|
632
966
|
return `export default {
|
|
@@ -715,84 +1049,7 @@ function relativeDisplay(path, rootDir) {
|
|
|
715
1049
|
// src/provision.ts
|
|
716
1050
|
var import_apps2 = require("@odla-ai/apps");
|
|
717
1051
|
var import_ai = require("@odla-ai/ai");
|
|
718
|
-
var
|
|
719
|
-
|
|
720
|
-
// src/token.ts
|
|
721
|
-
var import_db = require("@odla-ai/db");
|
|
722
|
-
var import_node_process2 = __toESM(require("process"), 1);
|
|
723
|
-
|
|
724
|
-
// src/open.ts
|
|
725
|
-
var import_node_child_process3 = require("child_process");
|
|
726
|
-
var import_node_process = __toESM(require("process"), 1);
|
|
727
|
-
async function openUrl(url, options = {}) {
|
|
728
|
-
const command = openerFor(options.platform ?? import_node_process.default.platform);
|
|
729
|
-
const doSpawn = options.spawnImpl ?? import_node_child_process3.spawn;
|
|
730
|
-
await new Promise((resolve6, reject) => {
|
|
731
|
-
const child = doSpawn(command.cmd, [...command.args, url], {
|
|
732
|
-
stdio: "ignore",
|
|
733
|
-
detached: true
|
|
734
|
-
});
|
|
735
|
-
child.once("error", reject);
|
|
736
|
-
child.once("spawn", () => {
|
|
737
|
-
child.unref();
|
|
738
|
-
resolve6();
|
|
739
|
-
});
|
|
740
|
-
});
|
|
741
|
-
}
|
|
742
|
-
function openerFor(platform) {
|
|
743
|
-
if (platform === "darwin") return { cmd: "open", args: [] };
|
|
744
|
-
if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
|
|
745
|
-
return { cmd: "xdg-open", args: [] };
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
// src/token.ts
|
|
749
|
-
async function getDeveloperToken(cfg, options, doFetch, out) {
|
|
750
|
-
if (options.token) return options.token;
|
|
751
|
-
if (import_node_process2.default.env.ODLA_DEV_TOKEN) return import_node_process2.default.env.ODLA_DEV_TOKEN;
|
|
752
|
-
const cached = readJsonFile(cfg.local.tokenFile);
|
|
753
|
-
if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
754
|
-
out.log(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
755
|
-
return cached.token;
|
|
756
|
-
}
|
|
757
|
-
const browser = approvalBrowser(options);
|
|
758
|
-
const { token, expiresAt } = await (0, import_db.requestToken)({
|
|
759
|
-
endpoint: cfg.platformUrl,
|
|
760
|
-
label: `${cfg.app.id} provisioner`,
|
|
761
|
-
fetch: doFetch,
|
|
762
|
-
onCode: async ({ userCode, expiresIn }) => {
|
|
763
|
-
const approvalUrl = handshakeUrl(cfg.platformUrl, userCode);
|
|
764
|
-
out.log("");
|
|
765
|
-
out.log(`Approve code ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
|
|
766
|
-
if (browser.open) {
|
|
767
|
-
try {
|
|
768
|
-
await (options.openApprovalUrl ?? openUrl)(approvalUrl);
|
|
769
|
-
out.log(`auth: opened browser for approval${browser.mode === "auto" ? " (auto)" : ""}`);
|
|
770
|
-
} catch (err) {
|
|
771
|
-
out.log(`auth: could not open browser (${err instanceof Error ? err.message : String(err)})`);
|
|
772
|
-
}
|
|
773
|
-
} else if (browser.reason) {
|
|
774
|
-
out.log(`auth: browser launch skipped (${browser.reason})`);
|
|
775
|
-
}
|
|
776
|
-
out.log("");
|
|
777
|
-
}
|
|
778
|
-
});
|
|
779
|
-
writePrivateJson(cfg.local.tokenFile, { token, expiresAt });
|
|
780
|
-
out.log(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
781
|
-
return token;
|
|
782
|
-
}
|
|
783
|
-
function approvalBrowser(options) {
|
|
784
|
-
if (options.open === true) return { open: true, mode: "forced" };
|
|
785
|
-
if (options.open === false) return { open: false, reason: "disabled by --no-open" };
|
|
786
|
-
if (import_node_process2.default.env.CI) return { open: false, reason: "CI environment" };
|
|
787
|
-
const interactive = options.interactive ?? Boolean(import_node_process2.default.stdin.isTTY && import_node_process2.default.stdout.isTTY);
|
|
788
|
-
if (!interactive) return { open: false, reason: "non-interactive shell; pass --open to force" };
|
|
789
|
-
return { open: true, mode: "auto" };
|
|
790
|
-
}
|
|
791
|
-
function handshakeUrl(platformUrl, userCode) {
|
|
792
|
-
const url = new URL("/studio", platformUrl);
|
|
793
|
-
url.searchParams.set("code", userCode);
|
|
794
|
-
return url.toString();
|
|
795
|
-
}
|
|
1052
|
+
var import_node_process4 = __toESM(require("process"), 1);
|
|
796
1053
|
|
|
797
1054
|
// src/provision-credentials.ts
|
|
798
1055
|
var import_apps = require("@odla-ai/apps");
|
|
@@ -1096,7 +1353,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
1096
1353
|
out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
|
|
1097
1354
|
}
|
|
1098
1355
|
if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
|
|
1099
|
-
const key =
|
|
1356
|
+
const key = import_node_process4.default.env[cfg.ai.keyEnv];
|
|
1100
1357
|
if (key) {
|
|
1101
1358
|
const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
|
|
1102
1359
|
await (0, import_ai.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
|
|
@@ -1156,10 +1413,161 @@ async function safeText2(res) {
|
|
|
1156
1413
|
}
|
|
1157
1414
|
}
|
|
1158
1415
|
|
|
1416
|
+
// src/security.ts
|
|
1417
|
+
var import_node_path6 = require("path");
|
|
1418
|
+
var import_security = require("@odla-ai/security");
|
|
1419
|
+
var import_node = require("@odla-ai/security/node");
|
|
1420
|
+
async function runHostedSecurity(options) {
|
|
1421
|
+
if (options.sourceDisclosureAck !== "redacted") {
|
|
1422
|
+
throw new Error("Hosted security requires sourceDisclosureAck=redacted before repository source may leave this process");
|
|
1423
|
+
}
|
|
1424
|
+
const selfAudit = options.selfAudit === true;
|
|
1425
|
+
const cfg = selfAudit ? void 0 : await loadProjectConfig(options.configPath ?? "odla.config.mjs");
|
|
1426
|
+
const appId = selfAudit ? "odla-ai" : cfg.app.id;
|
|
1427
|
+
const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
|
|
1428
|
+
const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
|
|
1429
|
+
const target = (0, import_node_path6.resolve)(options.target ?? cfg?.rootDir ?? ".");
|
|
1430
|
+
const output = (0, import_node_path6.resolve)(options.out ?? (0, import_node_path6.resolve)(target, ".odla/security/hosted"));
|
|
1431
|
+
const outputRelative = (0, import_node_path6.relative)(target, output).split(import_node_path6.sep).join("/");
|
|
1432
|
+
if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
|
|
1433
|
+
const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
|
|
1434
|
+
const tokenRequest = {
|
|
1435
|
+
platform,
|
|
1436
|
+
appId,
|
|
1437
|
+
env,
|
|
1438
|
+
selfAudit,
|
|
1439
|
+
scope: selfAudit ? "platform:security:self" : null
|
|
1440
|
+
};
|
|
1441
|
+
const token = await injectedToken(options, tokenRequest);
|
|
1442
|
+
const snapshot = await (0, import_node.snapshotDirectory)(target, {
|
|
1443
|
+
exclude: !outputRelative.startsWith("../") && !(0, import_node_path6.isAbsolute)(outputRelative) ? [outputRelative] : []
|
|
1444
|
+
});
|
|
1445
|
+
const hosted = await (0, import_security.createPlatformSecurityReasoners)({
|
|
1446
|
+
platform,
|
|
1447
|
+
token,
|
|
1448
|
+
appId,
|
|
1449
|
+
env,
|
|
1450
|
+
repository: snapshot.repository,
|
|
1451
|
+
revision: snapshot.revision,
|
|
1452
|
+
snapshotDigest: snapshot.digest,
|
|
1453
|
+
sourceDisclosure: "redacted",
|
|
1454
|
+
clientRunId: options.runId ?? crypto.randomUUID(),
|
|
1455
|
+
...selfAudit ? { selfAudit: { enabled: true, subject: "service:odla-security-self-audit" } } : {},
|
|
1456
|
+
fetch: options.fetch,
|
|
1457
|
+
signal: options.signal
|
|
1458
|
+
});
|
|
1459
|
+
const harness = (0, import_security.createSecurityHarness)({
|
|
1460
|
+
profile,
|
|
1461
|
+
store: new import_node.FileRunStore((0, import_node_path6.resolve)(output, "state")),
|
|
1462
|
+
discoveryReasoner: hosted.discoveryReasoner,
|
|
1463
|
+
validationReasoner: hosted.validationReasoner,
|
|
1464
|
+
policy: {
|
|
1465
|
+
modelSourceDisclosure: "redacted",
|
|
1466
|
+
active: false,
|
|
1467
|
+
allowNetwork: false
|
|
1468
|
+
}
|
|
1469
|
+
});
|
|
1470
|
+
const report = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
|
|
1471
|
+
await (0, import_node.writeSecurityArtifacts)(output, report);
|
|
1472
|
+
const reportDigest = await (0, import_security.securityFingerprint)(report);
|
|
1473
|
+
await hosted.complete({
|
|
1474
|
+
reportDigest,
|
|
1475
|
+
coverageStatus: report.coverageStatus,
|
|
1476
|
+
confirmed: report.metrics.confirmed,
|
|
1477
|
+
candidates: report.metrics.candidates
|
|
1478
|
+
}, { signal: options.signal });
|
|
1479
|
+
printSummary(options.stdout ?? console, appId, env, hosted.run, report, output);
|
|
1480
|
+
return Object.freeze({ report, run: hosted.run, output });
|
|
1481
|
+
}
|
|
1482
|
+
function selectEnv(requested, declared, configPath, rootDir) {
|
|
1483
|
+
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
1484
|
+
if (!env || !declared.includes(env)) {
|
|
1485
|
+
const shown = (0, import_node_path6.relative)(rootDir, configPath) || configPath;
|
|
1486
|
+
throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
|
|
1487
|
+
}
|
|
1488
|
+
return env;
|
|
1489
|
+
}
|
|
1490
|
+
async function injectedToken(options, request) {
|
|
1491
|
+
const value = options.token ?? await options.getToken?.(Object.freeze({ ...request }));
|
|
1492
|
+
if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
|
|
1493
|
+
throw new Error(
|
|
1494
|
+
request.selfAudit ? "Self-audit requires an injected, scoped platform security token" : "Hosted security requires an injected app developer token or getToken callback"
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
return value;
|
|
1498
|
+
}
|
|
1499
|
+
function profileFor(name, maxHuntTasks) {
|
|
1500
|
+
const profile = name === "odla" ? (0, import_security.odlaProfile)() : name === "cloudflare-app" ? (0, import_security.cloudflareAppProfile)() : name === "generic" ? (0, import_security.genericProfile)() : void 0;
|
|
1501
|
+
if (!profile) throw new Error(`unknown security profile "${String(name)}"`);
|
|
1502
|
+
if (maxHuntTasks === void 0) return profile;
|
|
1503
|
+
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
1504
|
+
return { ...profile, maxHuntTasks };
|
|
1505
|
+
}
|
|
1506
|
+
function printSummary(out, appId, env, run, report, output) {
|
|
1507
|
+
const complete = report.coverage.filter((cell) => cell.state === "complete").length;
|
|
1508
|
+
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
1509
|
+
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
1510
|
+
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
1511
|
+
out.log(` coverage: ${report.coverageStatus} ${complete}/${report.coverage.length} blocked=${report.metrics.blockedCells} shallow=${report.metrics.shallowCells} unscheduled=${report.metrics.unscheduledCells}`);
|
|
1512
|
+
out.log(` findings: confirmed=${report.metrics.confirmed} needs_reproduction=${report.metrics.needsReproduction} candidates=${report.metrics.candidates}`);
|
|
1513
|
+
out.log(` report: ${(0, import_node_path6.resolve)(output, "REPORT.md")}`);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// src/help.ts
|
|
1517
|
+
var import_node_fs7 = require("fs");
|
|
1518
|
+
function cliVersion() {
|
|
1519
|
+
const pkg = JSON.parse((0, import_node_fs7.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
|
|
1520
|
+
return pkg.version ?? "unknown";
|
|
1521
|
+
}
|
|
1522
|
+
function printHelp() {
|
|
1523
|
+
console.log(`odla-ai
|
|
1524
|
+
|
|
1525
|
+
Usage:
|
|
1526
|
+
odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
|
|
1527
|
+
odla-ai init --app-id <id> --name <name> [--services db,ai,o11y] [--env dev --env prod]
|
|
1528
|
+
odla-ai doctor [--config odla.config.mjs]
|
|
1529
|
+
odla-ai capabilities [--json]
|
|
1530
|
+
odla-ai admin ai show [--platform https://odla.ai] [--json]
|
|
1531
|
+
odla-ai admin ai set <purpose> --provider <id> --model <id> [--enabled|--no-enabled]
|
|
1532
|
+
odla-ai admin ai credentials [--json]
|
|
1533
|
+
odla-ai admin ai credential set <provider> (--from-env <NAME>|--stdin)
|
|
1534
|
+
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
1535
|
+
odla-ai security run [target] --self --ack-redacted-source
|
|
1536
|
+
odla-ai provision [--config odla.config.mjs] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
1537
|
+
odla-ai smoke [--config odla.config.mjs] [--env dev]
|
|
1538
|
+
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
1539
|
+
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
1540
|
+
odla-ai version
|
|
1541
|
+
|
|
1542
|
+
Commands:
|
|
1543
|
+
setup Install offline odla runbooks for common coding-agent harnesses.
|
|
1544
|
+
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
1545
|
+
doctor Validate and summarize the project config without network calls.
|
|
1546
|
+
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
1547
|
+
admin Manage platform-funded AI routing with a narrow admin-approved device grant.
|
|
1548
|
+
security Run hosted discovery + independent validation with app/run attribution.
|
|
1549
|
+
provision Register services, configure them, persist credentials, optionally push secrets.
|
|
1550
|
+
smoke Verify local credentials, public-config, live schema, and db aggregate.
|
|
1551
|
+
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
1552
|
+
copilot, gemini, or agents (repeatable or comma-separated).
|
|
1553
|
+
secrets Push configured db/o11y secrets into the Worker via wrangler stdin.
|
|
1554
|
+
version Print the CLI version.
|
|
1555
|
+
|
|
1556
|
+
Safety:
|
|
1557
|
+
New projects target dev only. Add prod explicitly to odla.config.mjs and pass
|
|
1558
|
+
--yes to provision it; use --dry-run first to inspect the resolved plan.
|
|
1559
|
+
Provision caches the approved developer token and service credentials under
|
|
1560
|
+
.odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
|
|
1561
|
+
preflights Wrangler before any shown-once issuance or destructive rotation.
|
|
1562
|
+
Provision opens the approval page automatically in interactive terminals;
|
|
1563
|
+
use --open to force browser launch or --no-open to suppress it.
|
|
1564
|
+
`);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1159
1567
|
// src/skill.ts
|
|
1160
|
-
var
|
|
1568
|
+
var import_node_fs8 = require("fs");
|
|
1161
1569
|
var import_node_os = require("os");
|
|
1162
|
-
var
|
|
1570
|
+
var import_node_path7 = require("path");
|
|
1163
1571
|
var import_node_url2 = require("url");
|
|
1164
1572
|
|
|
1165
1573
|
// src/skill-adapters.ts
|
|
@@ -1220,8 +1628,8 @@ function installSkill(options = {}) {
|
|
|
1220
1628
|
const files = listFiles(sourceDir);
|
|
1221
1629
|
if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
|
|
1222
1630
|
const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
|
|
1223
|
-
const root = (0,
|
|
1224
|
-
const home = (0,
|
|
1631
|
+
const root = (0, import_node_path7.resolve)(options.dir ?? process.cwd());
|
|
1632
|
+
const home = (0, import_node_path7.resolve)(options.homeDir ?? (0, import_node_os.homedir)());
|
|
1225
1633
|
const plans = /* @__PURE__ */ new Map();
|
|
1226
1634
|
const targets = /* @__PURE__ */ new Map();
|
|
1227
1635
|
const rememberTarget = (harness, target) => {
|
|
@@ -1235,48 +1643,48 @@ function installSkill(options = {}) {
|
|
|
1235
1643
|
plans.set(target, { target, content, boundary, managedMerge });
|
|
1236
1644
|
};
|
|
1237
1645
|
const planSkillTree = (targetDir2, boundary = root) => {
|
|
1238
|
-
for (const rel of files) plan((0,
|
|
1646
|
+
for (const rel of files) plan((0, import_node_path7.join)(targetDir2, rel), (0, import_node_fs8.readFileSync)((0, import_node_path7.join)(sourceDir, rel), "utf8"), false, boundary);
|
|
1239
1647
|
};
|
|
1240
1648
|
let targetDir;
|
|
1241
1649
|
if (options.global) {
|
|
1242
|
-
const claudeRoot = (0,
|
|
1243
|
-
const codexRoot = (0,
|
|
1650
|
+
const claudeRoot = (0, import_node_path7.join)(home, ".claude", "skills");
|
|
1651
|
+
const codexRoot = (0, import_node_path7.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path7.join)(home, ".codex"), "skills");
|
|
1244
1652
|
targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
|
|
1245
1653
|
for (const harness of harnesses) {
|
|
1246
1654
|
const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
|
|
1247
|
-
planSkillTree(skillRoot, harness === "claude" ? home : (0,
|
|
1655
|
+
planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path7.dirname)((0, import_node_path7.dirname)(codexRoot)));
|
|
1248
1656
|
rememberTarget(harness, skillRoot);
|
|
1249
1657
|
}
|
|
1250
1658
|
} else {
|
|
1251
|
-
const sharedRoot = (0,
|
|
1659
|
+
const sharedRoot = (0, import_node_path7.join)(root, ".agents", "skills");
|
|
1252
1660
|
planSkillTree(sharedRoot);
|
|
1253
|
-
const claudeRoot = (0,
|
|
1661
|
+
const claudeRoot = (0, import_node_path7.join)(root, ".claude", "skills");
|
|
1254
1662
|
targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
|
|
1255
1663
|
for (const harness of harnesses) rememberTarget(harness, sharedRoot);
|
|
1256
1664
|
if (harnesses.includes("claude")) {
|
|
1257
1665
|
for (const skill of skillNames(files)) {
|
|
1258
|
-
const canonical = (0,
|
|
1259
|
-
plan((0,
|
|
1666
|
+
const canonical = (0, import_node_fs8.readFileSync)((0, import_node_path7.join)(sourceDir, skill, "SKILL.md"), "utf8");
|
|
1667
|
+
plan((0, import_node_path7.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
|
|
1260
1668
|
}
|
|
1261
1669
|
rememberTarget("claude", claudeRoot);
|
|
1262
1670
|
}
|
|
1263
1671
|
if (harnesses.includes("cursor")) {
|
|
1264
|
-
const cursorRule = (0,
|
|
1672
|
+
const cursorRule = (0, import_node_path7.join)(root, ".cursor", "rules", "odla.mdc");
|
|
1265
1673
|
plan(cursorRule, CURSOR_RULE);
|
|
1266
1674
|
rememberTarget("cursor", cursorRule);
|
|
1267
1675
|
}
|
|
1268
1676
|
if (harnesses.includes("agents")) {
|
|
1269
|
-
const agentsFile = (0,
|
|
1677
|
+
const agentsFile = (0, import_node_path7.join)(root, "AGENTS.md");
|
|
1270
1678
|
plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
1271
1679
|
rememberTarget("agents", agentsFile);
|
|
1272
1680
|
}
|
|
1273
1681
|
if (harnesses.includes("copilot")) {
|
|
1274
|
-
const copilotFile = (0,
|
|
1682
|
+
const copilotFile = (0, import_node_path7.join)(root, ".github", "copilot-instructions.md");
|
|
1275
1683
|
plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
1276
1684
|
rememberTarget("copilot", copilotFile);
|
|
1277
1685
|
}
|
|
1278
1686
|
if (harnesses.includes("gemini")) {
|
|
1279
|
-
const geminiFile = (0,
|
|
1687
|
+
const geminiFile = (0, import_node_path7.join)(root, "GEMINI.md");
|
|
1280
1688
|
plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
1281
1689
|
rememberTarget("gemini", geminiFile);
|
|
1282
1690
|
}
|
|
@@ -1290,11 +1698,11 @@ function installSkill(options = {}) {
|
|
|
1290
1698
|
conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
|
|
1291
1699
|
continue;
|
|
1292
1700
|
}
|
|
1293
|
-
if (!(0,
|
|
1701
|
+
if (!(0, import_node_fs8.existsSync)(file.target)) {
|
|
1294
1702
|
writtenPaths.add(file.target);
|
|
1295
1703
|
continue;
|
|
1296
1704
|
}
|
|
1297
|
-
const current = (0,
|
|
1705
|
+
const current = (0, import_node_fs8.readFileSync)(file.target, "utf8");
|
|
1298
1706
|
if (current === file.content) {
|
|
1299
1707
|
unchangedPaths.add(file.target);
|
|
1300
1708
|
} else if (file.managedMerge || options.force) {
|
|
@@ -1311,9 +1719,9 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
1311
1719
|
);
|
|
1312
1720
|
}
|
|
1313
1721
|
for (const file of plans.values()) {
|
|
1314
|
-
if (!(0,
|
|
1315
|
-
(0,
|
|
1316
|
-
(0,
|
|
1722
|
+
if (!(0, import_node_fs8.existsSync)(file.target) || (0, import_node_fs8.readFileSync)(file.target, "utf8") !== file.content) {
|
|
1723
|
+
(0, import_node_fs8.mkdirSync)((0, import_node_path7.dirname)(file.target), { recursive: true });
|
|
1724
|
+
(0, import_node_fs8.writeFileSync)(file.target, file.content);
|
|
1317
1725
|
}
|
|
1318
1726
|
}
|
|
1319
1727
|
const skills = skillNames(files);
|
|
@@ -1332,7 +1740,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
1332
1740
|
};
|
|
1333
1741
|
}
|
|
1334
1742
|
function pathsUnder(root, paths) {
|
|
1335
|
-
return [...paths].map((path) => (0,
|
|
1743
|
+
return [...paths].map((path) => (0, import_node_path7.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path7.sep}`) && !(0, import_node_path7.isAbsolute)(path)).sort();
|
|
1336
1744
|
}
|
|
1337
1745
|
function normalizeHarnesses(values, global) {
|
|
1338
1746
|
const requested = values?.length ? values : ["claude"];
|
|
@@ -1354,9 +1762,9 @@ function normalizeHarnesses(values, global) {
|
|
|
1354
1762
|
function managedFileContent(path, block, force, boundary) {
|
|
1355
1763
|
const symlink = symlinkedComponent(boundary, path);
|
|
1356
1764
|
if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
|
|
1357
|
-
if (!(0,
|
|
1765
|
+
if (!(0, import_node_fs8.existsSync)(path)) return `${block}
|
|
1358
1766
|
`;
|
|
1359
|
-
const current = (0,
|
|
1767
|
+
const current = (0, import_node_fs8.readFileSync)(path, "utf8");
|
|
1360
1768
|
const start = "<!-- odla-ai agent setup:start -->";
|
|
1361
1769
|
const end = "<!-- odla-ai agent setup:end -->";
|
|
1362
1770
|
const startAt = current.indexOf(start);
|
|
@@ -1377,15 +1785,15 @@ function managedFileContent(path, block, force, boundary) {
|
|
|
1377
1785
|
return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
|
|
1378
1786
|
}
|
|
1379
1787
|
function symlinkedComponent(boundary, target) {
|
|
1380
|
-
const rel = (0,
|
|
1381
|
-
if (rel === ".." || rel.startsWith(`..${
|
|
1788
|
+
const rel = (0, import_node_path7.relative)(boundary, target);
|
|
1789
|
+
if (rel === ".." || rel.startsWith(`..${import_node_path7.sep}`) || (0, import_node_path7.isAbsolute)(rel)) {
|
|
1382
1790
|
throw new Error(`agent setup target escapes its install root: ${target}`);
|
|
1383
1791
|
}
|
|
1384
1792
|
let current = boundary;
|
|
1385
|
-
for (const part of rel.split(
|
|
1386
|
-
current = (0,
|
|
1793
|
+
for (const part of rel.split(import_node_path7.sep).filter(Boolean)) {
|
|
1794
|
+
current = (0, import_node_path7.join)(current, part);
|
|
1387
1795
|
try {
|
|
1388
|
-
if ((0,
|
|
1796
|
+
if ((0, import_node_fs8.lstatSync)(current).isSymbolicLink()) return current;
|
|
1389
1797
|
} catch (error) {
|
|
1390
1798
|
if (error.code !== "ENOENT") throw error;
|
|
1391
1799
|
}
|
|
@@ -1396,13 +1804,13 @@ function skillNames(files) {
|
|
|
1396
1804
|
return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
|
|
1397
1805
|
}
|
|
1398
1806
|
function listFiles(dir) {
|
|
1399
|
-
if (!(0,
|
|
1807
|
+
if (!(0, import_node_fs8.existsSync)(dir)) return [];
|
|
1400
1808
|
const results = [];
|
|
1401
1809
|
const walk = (current) => {
|
|
1402
|
-
for (const entry of (0,
|
|
1403
|
-
const path = (0,
|
|
1810
|
+
for (const entry of (0, import_node_fs8.readdirSync)(current, { withFileTypes: true })) {
|
|
1811
|
+
const path = (0, import_node_path7.join)(current, entry.name);
|
|
1404
1812
|
if (entry.isDirectory()) walk(path);
|
|
1405
|
-
else results.push((0,
|
|
1813
|
+
else results.push((0, import_node_path7.relative)(dir, path));
|
|
1406
1814
|
}
|
|
1407
1815
|
};
|
|
1408
1816
|
walk(dir);
|
|
@@ -1527,6 +1935,99 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
1527
1935
|
printCapabilities(parsed.options.json === true);
|
|
1528
1936
|
return;
|
|
1529
1937
|
}
|
|
1938
|
+
if (command === "admin") {
|
|
1939
|
+
const area = parsed.positionals[1];
|
|
1940
|
+
const action = parsed.positionals[2];
|
|
1941
|
+
const credentialSet = action === "credential" && parsed.positionals[3] === "set";
|
|
1942
|
+
const credentials = action === "credentials";
|
|
1943
|
+
if (area !== "ai" || action !== "show" && action !== "set" && !credentialSet && !credentials) {
|
|
1944
|
+
throw new Error('unknown admin command. Try "odla-ai admin ai show".');
|
|
1945
|
+
}
|
|
1946
|
+
assertArgs(parsed, [
|
|
1947
|
+
"platform",
|
|
1948
|
+
"token",
|
|
1949
|
+
"json",
|
|
1950
|
+
"open",
|
|
1951
|
+
"provider",
|
|
1952
|
+
"model",
|
|
1953
|
+
"enabled",
|
|
1954
|
+
"max-input-bytes",
|
|
1955
|
+
"max-output-tokens",
|
|
1956
|
+
"max-calls-per-run",
|
|
1957
|
+
"from-env",
|
|
1958
|
+
"stdin"
|
|
1959
|
+
], credentialSet ? 5 : action === "set" ? 4 : 3);
|
|
1960
|
+
await adminAi({
|
|
1961
|
+
action: credentialSet ? "credential-set" : credentials ? "credentials" : action,
|
|
1962
|
+
purpose: action === "set" ? parsed.positionals[3] : void 0,
|
|
1963
|
+
provider: stringOpt(parsed.options.provider),
|
|
1964
|
+
model: stringOpt(parsed.options.model),
|
|
1965
|
+
enabled: boolOpt(parsed.options.enabled),
|
|
1966
|
+
maxInputBytes: numberOpt(parsed.options["max-input-bytes"], "--max-input-bytes"),
|
|
1967
|
+
maxOutputTokens: numberOpt(parsed.options["max-output-tokens"], "--max-output-tokens"),
|
|
1968
|
+
maxCallsPerRun: numberOpt(parsed.options["max-calls-per-run"], "--max-calls-per-run"),
|
|
1969
|
+
platform: stringOpt(parsed.options.platform),
|
|
1970
|
+
token: stringOpt(parsed.options.token),
|
|
1971
|
+
json: parsed.options.json === true,
|
|
1972
|
+
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
1973
|
+
credentialProvider: credentialSet ? parsed.positionals[4] : void 0,
|
|
1974
|
+
fromEnv: stringOpt(parsed.options["from-env"]),
|
|
1975
|
+
stdin: parsed.options.stdin === true
|
|
1976
|
+
});
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
if (command === "security") {
|
|
1980
|
+
const sub = parsed.positionals[1];
|
|
1981
|
+
if (sub !== "run") throw new Error('unknown security command. Try "odla-ai security run".');
|
|
1982
|
+
assertArgs(parsed, [
|
|
1983
|
+
"config",
|
|
1984
|
+
"env",
|
|
1985
|
+
"out",
|
|
1986
|
+
"profile",
|
|
1987
|
+
"max-hunt-tasks",
|
|
1988
|
+
"run-id",
|
|
1989
|
+
"platform",
|
|
1990
|
+
"self",
|
|
1991
|
+
"ack-redacted-source",
|
|
1992
|
+
"open",
|
|
1993
|
+
"fail-on",
|
|
1994
|
+
"fail-on-candidates",
|
|
1995
|
+
"allow-incomplete"
|
|
1996
|
+
], 3);
|
|
1997
|
+
const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
1998
|
+
const selfAudit = parsed.options.self === true;
|
|
1999
|
+
const open = parsed.options.open === false ? false : parsed.options.open === true ? true : void 0;
|
|
2000
|
+
const platform = stringOpt(parsed.options.platform);
|
|
2001
|
+
const result = await runHostedSecurity({
|
|
2002
|
+
configPath,
|
|
2003
|
+
selfAudit,
|
|
2004
|
+
target: parsed.positionals[2],
|
|
2005
|
+
out: stringOpt(parsed.options.out),
|
|
2006
|
+
env: stringOpt(parsed.options.env),
|
|
2007
|
+
profile: securityProfile(stringOpt(parsed.options.profile)),
|
|
2008
|
+
maxHuntTasks: numberOpt(parsed.options["max-hunt-tasks"], "--max-hunt-tasks"),
|
|
2009
|
+
runId: stringOpt(parsed.options["run-id"]),
|
|
2010
|
+
platform,
|
|
2011
|
+
sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
|
|
2012
|
+
getToken: async (request) => {
|
|
2013
|
+
if (request.scope === "platform:security:self") {
|
|
2014
|
+
return getScopedPlatformToken({ platform: request.platform, scope: request.scope, open });
|
|
2015
|
+
}
|
|
2016
|
+
const cfg = await loadProjectConfig(configPath);
|
|
2017
|
+
return getDeveloperToken(cfg, { configPath, open }, fetch, console);
|
|
2018
|
+
}
|
|
2019
|
+
});
|
|
2020
|
+
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
2021
|
+
const candidateValue = parsed.options["fail-on-candidates"];
|
|
2022
|
+
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
2023
|
+
const confirmed = (0, import_security2.findingsAtOrAbove)(result.report, failOn);
|
|
2024
|
+
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(result.report, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
|
|
2025
|
+
const incomplete = result.report.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
|
|
2026
|
+
if (confirmed.length || leads.length || incomplete) {
|
|
2027
|
+
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
2028
|
+
}
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
1530
2031
|
if (command === "provision") {
|
|
1531
2032
|
assertArgs(
|
|
1532
2033
|
parsed,
|
|
@@ -1607,71 +2108,14 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
1607
2108
|
}
|
|
1608
2109
|
throw new Error(`unknown command "${command}". Run "odla-ai help".`);
|
|
1609
2110
|
}
|
|
1610
|
-
function
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
throw new Error(`unknown option "--${name}"; run "odla-ai help" for supported options`);
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
if (parsed.positionals.length > maxPositionals) {
|
|
1618
|
-
throw new Error(`unexpected argument "${parsed.positionals[maxPositionals]}"; run "odla-ai help"`);
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1621
|
-
function parseArgv(argv) {
|
|
1622
|
-
const positionals = [];
|
|
1623
|
-
const options = {};
|
|
1624
|
-
for (let i = 0; i < argv.length; i++) {
|
|
1625
|
-
const arg = argv[i];
|
|
1626
|
-
if (!arg.startsWith("--")) {
|
|
1627
|
-
positionals.push(arg);
|
|
1628
|
-
continue;
|
|
1629
|
-
}
|
|
1630
|
-
const eq = arg.indexOf("=");
|
|
1631
|
-
const rawName = arg.slice(2, eq === -1 ? void 0 : eq);
|
|
1632
|
-
if (!rawName) continue;
|
|
1633
|
-
if (rawName.startsWith("no-")) {
|
|
1634
|
-
options[rawName.slice(3)] = false;
|
|
1635
|
-
continue;
|
|
1636
|
-
}
|
|
1637
|
-
if (eq !== -1) {
|
|
1638
|
-
addOption(options, rawName, arg.slice(eq + 1));
|
|
1639
|
-
continue;
|
|
1640
|
-
}
|
|
1641
|
-
const value = argv[i + 1];
|
|
1642
|
-
if (value !== void 0 && !value.startsWith("--")) {
|
|
1643
|
-
i++;
|
|
1644
|
-
addOption(options, rawName, value);
|
|
1645
|
-
} else {
|
|
1646
|
-
addOption(options, rawName, true);
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
return { positionals, options };
|
|
1650
|
-
}
|
|
1651
|
-
function addOption(options, name, value) {
|
|
1652
|
-
const cur = options[name];
|
|
1653
|
-
if (cur === void 0) {
|
|
1654
|
-
options[name] = value;
|
|
1655
|
-
} else if (Array.isArray(cur)) {
|
|
1656
|
-
cur.push(String(value));
|
|
1657
|
-
} else {
|
|
1658
|
-
options[name] = [String(cur), String(value)];
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1661
|
-
function requiredString(value, name) {
|
|
1662
|
-
const s = stringOpt(value);
|
|
1663
|
-
if (!s) throw new Error(`${name} is required`);
|
|
1664
|
-
return s;
|
|
1665
|
-
}
|
|
1666
|
-
function stringOpt(value) {
|
|
1667
|
-
if (typeof value === "string") return value;
|
|
1668
|
-
if (Array.isArray(value)) return value[value.length - 1];
|
|
1669
|
-
return void 0;
|
|
2111
|
+
function securityProfile(value) {
|
|
2112
|
+
if (value === void 0) return void 0;
|
|
2113
|
+
if (value === "odla" || value === "cloudflare-app" || value === "generic") return value;
|
|
2114
|
+
throw new Error("--profile must be odla, cloudflare-app, or generic");
|
|
1670
2115
|
}
|
|
1671
|
-
function
|
|
1672
|
-
if (
|
|
1673
|
-
|
|
1674
|
-
return values.flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
|
|
2116
|
+
function severityOpt(value, flag) {
|
|
2117
|
+
if (["informational", "low", "medium", "high", "critical"].includes(value)) return value;
|
|
2118
|
+
throw new Error(`${flag} must be informational, low, medium, high, or critical`);
|
|
1675
2119
|
}
|
|
1676
2120
|
function harnessList(parsed) {
|
|
1677
2121
|
const selected = [
|
|
@@ -1688,57 +2132,21 @@ function harnessOption(value, flag) {
|
|
|
1688
2132
|
if (parts.some((item) => !item.trim())) throw new Error(`${flag} requires a harness name`);
|
|
1689
2133
|
return parts.map((item) => item.trim());
|
|
1690
2134
|
}
|
|
1691
|
-
function cliVersion() {
|
|
1692
|
-
const pkg = JSON.parse((0, import_node_fs7.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
|
|
1693
|
-
return pkg.version ?? "unknown";
|
|
1694
|
-
}
|
|
1695
|
-
function printHelp() {
|
|
1696
|
-
console.log(`odla-ai
|
|
1697
|
-
|
|
1698
|
-
Usage:
|
|
1699
|
-
odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
|
|
1700
|
-
odla-ai init --app-id <id> --name <name> [--services db,ai,o11y] [--env dev --env prod]
|
|
1701
|
-
odla-ai doctor [--config odla.config.mjs]
|
|
1702
|
-
odla-ai capabilities [--json]
|
|
1703
|
-
odla-ai provision [--config odla.config.mjs] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
1704
|
-
odla-ai smoke [--config odla.config.mjs] [--env dev]
|
|
1705
|
-
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
1706
|
-
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
1707
|
-
odla-ai version
|
|
1708
|
-
|
|
1709
|
-
Commands:
|
|
1710
|
-
setup Install offline odla runbooks for common coding-agent harnesses.
|
|
1711
|
-
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
1712
|
-
doctor Validate and summarize the project config without network calls.
|
|
1713
|
-
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
1714
|
-
provision Register services, configure them, persist credentials, optionally push secrets.
|
|
1715
|
-
smoke Verify local credentials, public-config, live schema, and db aggregate.
|
|
1716
|
-
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
1717
|
-
copilot, gemini, or agents (repeatable or comma-separated).
|
|
1718
|
-
secrets Push configured db/o11y secrets into the Worker via wrangler stdin.
|
|
1719
|
-
version Print the CLI version.
|
|
1720
|
-
|
|
1721
|
-
Safety:
|
|
1722
|
-
New projects target dev only. Add prod explicitly to odla.config.mjs and pass
|
|
1723
|
-
--yes to provision it; use --dry-run first to inspect the resolved plan.
|
|
1724
|
-
Provision caches the approved developer token and service credentials under
|
|
1725
|
-
.odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
|
|
1726
|
-
preflights Wrangler before any shown-once issuance or destructive rotation.
|
|
1727
|
-
Provision opens the approval page automatically in interactive terminals;
|
|
1728
|
-
use --open to force browser launch or --no-open to suppress it.
|
|
1729
|
-
`);
|
|
1730
|
-
}
|
|
1731
2135
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1732
2136
|
0 && (module.exports = {
|
|
1733
2137
|
AGENT_HARNESSES,
|
|
1734
2138
|
CAPABILITIES,
|
|
2139
|
+
SYSTEM_AI_PURPOSES,
|
|
2140
|
+
adminAi,
|
|
1735
2141
|
doctor,
|
|
2142
|
+
getScopedPlatformToken,
|
|
1736
2143
|
initProject,
|
|
1737
2144
|
installSkill,
|
|
1738
2145
|
printCapabilities,
|
|
1739
2146
|
provision,
|
|
1740
2147
|
redactSecrets,
|
|
1741
2148
|
runCli,
|
|
2149
|
+
runHostedSecurity,
|
|
1742
2150
|
secretsPush,
|
|
1743
2151
|
smoke
|
|
1744
2152
|
});
|