@odla-ai/cli 0.5.0 → 0.7.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 +59 -9
- package/dist/bin.cjs +1096 -402
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-7WZIZCGA.js → chunk-643B2AKG.js} +1100 -393
- package/dist/chunk-643B2AKG.js.map +1 -0
- package/dist/index.cjs +1104 -402
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +130 -5
- package/dist/index.d.ts +130 -5
- package/dist/index.js +9 -1
- package/llms.txt +191 -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,724 @@ 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_process4 = __toESM(require("process"), 1);
|
|
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/open.ts
|
|
67
|
+
var import_node_child_process = require("child_process");
|
|
68
|
+
var import_node_process = __toESM(require("process"), 1);
|
|
69
|
+
async function openUrl(url, options = {}) {
|
|
70
|
+
const command = openerFor(options.platform ?? import_node_process.default.platform);
|
|
71
|
+
const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
|
|
72
|
+
await new Promise((resolve7, reject) => {
|
|
73
|
+
const child = doSpawn(command.cmd, [...command.args, url], {
|
|
74
|
+
stdio: "ignore",
|
|
75
|
+
detached: true
|
|
76
|
+
});
|
|
77
|
+
child.once("error", reject);
|
|
78
|
+
child.once("spawn", () => {
|
|
79
|
+
child.unref();
|
|
80
|
+
resolve7();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function openerFor(platform) {
|
|
85
|
+
if (platform === "darwin") return { cmd: "open", args: [] };
|
|
86
|
+
if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
|
|
87
|
+
return { cmd: "xdg-open", args: [] };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/local.ts
|
|
91
|
+
var import_node_fs = require("fs");
|
|
92
|
+
var import_node_path = require("path");
|
|
93
|
+
var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
|
|
94
|
+
function readJsonFile(path) {
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function writePrivateJson(path, value) {
|
|
102
|
+
writePrivateText(path, `${JSON.stringify(value, null, 2)}
|
|
103
|
+
`);
|
|
104
|
+
}
|
|
105
|
+
function readCredentials(path) {
|
|
106
|
+
if (!(0, import_node_fs.existsSync)(path)) return null;
|
|
107
|
+
let value;
|
|
108
|
+
try {
|
|
109
|
+
value = JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
|
|
110
|
+
} catch {
|
|
111
|
+
throw new Error(`credentials file ${path} is not valid JSON; fix or remove it before provisioning`);
|
|
112
|
+
}
|
|
113
|
+
if (!value || typeof value !== "object" || typeof value.appId !== "string" || !value.envs || typeof value.envs !== "object") {
|
|
114
|
+
throw new Error(`credentials file ${path} has an invalid shape; fix or remove it before provisioning`);
|
|
115
|
+
}
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
function mergeCredential(current, update) {
|
|
119
|
+
const next = current ?? {
|
|
120
|
+
appId: update.appId,
|
|
121
|
+
platformUrl: update.platformUrl,
|
|
122
|
+
dbEndpoint: update.dbEndpoint,
|
|
123
|
+
updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
124
|
+
envs: {}
|
|
125
|
+
};
|
|
126
|
+
next.appId = update.appId;
|
|
127
|
+
next.platformUrl = update.platformUrl;
|
|
128
|
+
next.dbEndpoint = update.dbEndpoint;
|
|
129
|
+
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
130
|
+
next.envs[update.env] = {
|
|
131
|
+
...next.envs[update.env] ?? {},
|
|
132
|
+
tenantId: update.tenantId,
|
|
133
|
+
...update.dbKey ? { dbKey: update.dbKey } : {},
|
|
134
|
+
...update.o11yToken ? { o11yToken: update.o11yToken } : {}
|
|
135
|
+
};
|
|
136
|
+
return next;
|
|
137
|
+
}
|
|
138
|
+
function ensureGitignore(rootDir, localPaths = []) {
|
|
139
|
+
const path = (0, import_node_path.resolve)(rootDir, ".gitignore");
|
|
140
|
+
const existing = (0, import_node_fs.existsSync)(path) ? (0, import_node_fs.readFileSync)(path, "utf8") : "";
|
|
141
|
+
const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line) => !!line);
|
|
142
|
+
const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
|
|
143
|
+
const missing = wanted.filter((line) => !existing.split(/\r?\n/).includes(line));
|
|
144
|
+
if (missing.length === 0) return;
|
|
145
|
+
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
146
|
+
(0, import_node_fs.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
|
|
147
|
+
`);
|
|
148
|
+
}
|
|
149
|
+
function o11yDevVars(cfg) {
|
|
150
|
+
if (!cfg.services.includes("o11y")) return void 0;
|
|
151
|
+
return {
|
|
152
|
+
endpoint: cfg.o11y?.endpoint ?? "https://o11y.odla.ai",
|
|
153
|
+
service: cfg.o11y?.service ?? cfg.app.id,
|
|
154
|
+
...cfg.o11y?.version ? { version: cfg.o11y.version } : {}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function resolveWriteDevVarsTarget(cfg, requested) {
|
|
158
|
+
if (!requested) return null;
|
|
159
|
+
if (requested === true) return cfg.local.devVarsFile;
|
|
160
|
+
return (0, import_node_path.resolve)((0, import_node_path.dirname)(cfg.configPath), requested);
|
|
161
|
+
}
|
|
162
|
+
function writeDevVars(path, credentials, env, o11y) {
|
|
163
|
+
const entry = credentials.envs[env];
|
|
164
|
+
if (!entry) throw new Error(`no credentials for env "${env}" in ${path}`);
|
|
165
|
+
const lines = [`ODLA_PLATFORM="${credentials.platformUrl}"`];
|
|
166
|
+
if (entry.dbKey) lines.push(`ODLA_ENDPOINT="${credentials.dbEndpoint}"`);
|
|
167
|
+
lines.push(`ODLA_APP_ID="${credentials.appId}"`, `ODLA_ENV="${env}"`, `ODLA_TENANT="${entry.tenantId}"`);
|
|
168
|
+
if (entry.dbKey) lines.push(`ODLA_API_KEY="${entry.dbKey}"`);
|
|
169
|
+
if (o11y) {
|
|
170
|
+
lines.push(`ODLA_O11Y_ENDPOINT="${o11y.endpoint}"`, `ODLA_O11Y_SERVICE="${o11y.service}"`);
|
|
171
|
+
if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
|
|
172
|
+
if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
|
|
173
|
+
}
|
|
174
|
+
const existing = (0, import_node_fs.existsSync)(path) ? (0, import_node_fs.readFileSync)(path, "utf8") : "";
|
|
175
|
+
const retained = existing.split(/\r?\n/).filter((line) => !isManagedDevVar(line));
|
|
176
|
+
while (retained.at(-1) === "") retained.pop();
|
|
177
|
+
const prefix = retained.length ? `${retained.join("\n")}
|
|
178
|
+
|
|
179
|
+
` : "";
|
|
180
|
+
writePrivateText(path, `${prefix}${lines.join("\n")}
|
|
181
|
+
`);
|
|
182
|
+
}
|
|
183
|
+
var MANAGED_DEV_VARS = /* @__PURE__ */ new Set([
|
|
184
|
+
"ODLA_PLATFORM",
|
|
185
|
+
"ODLA_ENDPOINT",
|
|
186
|
+
"ODLA_APP_ID",
|
|
187
|
+
"ODLA_ENV",
|
|
188
|
+
"ODLA_TENANT",
|
|
189
|
+
"ODLA_API_KEY",
|
|
190
|
+
"ODLA_O11Y_ENDPOINT",
|
|
191
|
+
"ODLA_O11Y_SERVICE",
|
|
192
|
+
"ODLA_O11Y_VERSION",
|
|
193
|
+
"ODLA_O11Y_TOKEN"
|
|
194
|
+
]);
|
|
195
|
+
function isManagedDevVar(line) {
|
|
196
|
+
const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
|
|
197
|
+
return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
|
|
198
|
+
}
|
|
199
|
+
function writePrivateText(path, text) {
|
|
200
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
|
|
201
|
+
const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
202
|
+
(0, import_node_fs.writeFileSync)(temporary, text, { mode: 384 });
|
|
203
|
+
(0, import_node_fs.chmodSync)(temporary, 384);
|
|
204
|
+
(0, import_node_fs.renameSync)(temporary, path);
|
|
205
|
+
}
|
|
206
|
+
function gitignoreEntry(rootDir, path) {
|
|
207
|
+
const rel = (0, import_node_path.relative)((0, import_node_path.resolve)(rootDir), (0, import_node_path.resolve)(path));
|
|
208
|
+
if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path.isAbsolute)(rel)) return null;
|
|
209
|
+
return rel.replaceAll("\\", "/");
|
|
210
|
+
}
|
|
211
|
+
function displayPath(path, rootDir = process.cwd()) {
|
|
212
|
+
const rel = (0, import_node_path.relative)(rootDir, path);
|
|
213
|
+
return rel && !rel.startsWith("..") ? rel : path;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/token.ts
|
|
217
|
+
async function getDeveloperToken(cfg, options, doFetch, out) {
|
|
218
|
+
if (options.token) return options.token;
|
|
219
|
+
const audience = platformAudience(cfg.platformUrl);
|
|
220
|
+
if (import_node_process2.default.env.ODLA_DEV_TOKEN) {
|
|
221
|
+
const declared = import_node_process2.default.env.ODLA_DEV_TOKEN_AUDIENCE;
|
|
222
|
+
if (declared) {
|
|
223
|
+
if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
|
|
224
|
+
} else if (audience !== "https://odla.ai") {
|
|
225
|
+
throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
|
|
226
|
+
}
|
|
227
|
+
return import_node_process2.default.env.ODLA_DEV_TOKEN;
|
|
228
|
+
}
|
|
229
|
+
const cached = readJsonFile(cfg.local.tokenFile);
|
|
230
|
+
if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
231
|
+
out.log(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
232
|
+
return cached.token;
|
|
233
|
+
}
|
|
234
|
+
const browser = approvalBrowser(options);
|
|
235
|
+
const { token, expiresAt } = await (0, import_db.requestToken)({
|
|
236
|
+
endpoint: cfg.platformUrl,
|
|
237
|
+
label: `${cfg.app.id} provisioner`,
|
|
238
|
+
fetch: doFetch,
|
|
239
|
+
onCode: async ({ userCode, expiresIn }) => {
|
|
240
|
+
const approvalUrl = handshakeUrl(cfg.platformUrl, userCode);
|
|
241
|
+
out.log("");
|
|
242
|
+
out.log(`Approve code ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
|
|
243
|
+
if (browser.open) {
|
|
244
|
+
try {
|
|
245
|
+
await (options.openApprovalUrl ?? openUrl)(approvalUrl);
|
|
246
|
+
out.log(`auth: opened browser for approval${browser.mode === "auto" ? " (auto)" : ""}`);
|
|
247
|
+
} catch (err) {
|
|
248
|
+
out.log(`auth: could not open browser (${err instanceof Error ? err.message : String(err)})`);
|
|
249
|
+
}
|
|
250
|
+
} else if (browser.reason) {
|
|
251
|
+
out.log(`auth: browser launch skipped (${browser.reason})`);
|
|
252
|
+
}
|
|
253
|
+
out.log("");
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
writePrivateJson(cfg.local.tokenFile, { platform: audience, token, expiresAt });
|
|
257
|
+
out.log(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
258
|
+
return token;
|
|
259
|
+
}
|
|
260
|
+
function approvalBrowser(options) {
|
|
261
|
+
if (options.open === true) return { open: true, mode: "forced" };
|
|
262
|
+
if (options.open === false) return { open: false, reason: "disabled by --no-open" };
|
|
263
|
+
if (import_node_process2.default.env.CI) return { open: false, reason: "CI environment" };
|
|
264
|
+
const interactive = options.interactive ?? Boolean(import_node_process2.default.stdin.isTTY && import_node_process2.default.stdout.isTTY);
|
|
265
|
+
if (!interactive) return { open: false, reason: "non-interactive shell; pass --open to force" };
|
|
266
|
+
return { open: true, mode: "auto" };
|
|
267
|
+
}
|
|
268
|
+
function handshakeUrl(platformUrl, userCode) {
|
|
269
|
+
const url = new URL("/studio", platformUrl);
|
|
270
|
+
url.searchParams.set("code", userCode);
|
|
271
|
+
return url.toString();
|
|
272
|
+
}
|
|
273
|
+
function platformAudience(value) {
|
|
274
|
+
let url;
|
|
275
|
+
try {
|
|
276
|
+
url = new URL(value);
|
|
277
|
+
} catch {
|
|
278
|
+
throw new Error("platform must be an absolute URL");
|
|
279
|
+
}
|
|
280
|
+
const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
281
|
+
if (url.protocol !== "https:" && !(local && url.protocol === "http:")) {
|
|
282
|
+
throw new Error("platform credentials require HTTPS except for loopback development");
|
|
283
|
+
}
|
|
284
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
285
|
+
throw new Error("platform URL must not contain credentials, query, or fragment");
|
|
286
|
+
}
|
|
287
|
+
return url.origin;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/admin-ai-auth.ts
|
|
291
|
+
var import_node_fs2 = require("fs");
|
|
292
|
+
var import_node_process3 = __toESM(require("process"), 1);
|
|
293
|
+
var import_db2 = require("@odla-ai/db");
|
|
294
|
+
async function getScopedPlatformToken(options) {
|
|
295
|
+
return resolveAdminPlatformToken(options);
|
|
296
|
+
}
|
|
297
|
+
async function resolveAdminPlatformToken(options) {
|
|
298
|
+
const audience = platformAudience(options.platform);
|
|
299
|
+
if (options.token) return options.token;
|
|
300
|
+
const fromEnv = import_node_process3.default.env.ODLA_ADMIN_TOKEN;
|
|
301
|
+
if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
|
|
302
|
+
return scopedToken(
|
|
303
|
+
audience,
|
|
304
|
+
options.scope,
|
|
305
|
+
options,
|
|
306
|
+
options.fetch ?? fetch,
|
|
307
|
+
options.stdout ?? console
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
function audienceBoundEnvToken(token, platform) {
|
|
311
|
+
const audience = platformAudience(platform);
|
|
312
|
+
const declared = import_node_process3.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
|
|
313
|
+
if (declared) {
|
|
314
|
+
if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
|
|
315
|
+
} else if (audience !== "https://odla.ai") {
|
|
316
|
+
throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE is required for a non-default platform");
|
|
317
|
+
}
|
|
318
|
+
return token;
|
|
319
|
+
}
|
|
320
|
+
async function scopedToken(platform, scope, options, doFetch, out) {
|
|
321
|
+
const audience = platformAudience(platform);
|
|
322
|
+
const tokenFile = options.tokenFile ?? `${import_node_process3.default.cwd()}/.odla/admin-token.local.json`;
|
|
323
|
+
const cache = readJsonFile(tokenFile);
|
|
324
|
+
const cached = cache?.platform === audience ? cache.tokens?.[scope] : void 0;
|
|
325
|
+
if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
326
|
+
out.log(`auth: using cached ${scope} grant (${tokenFile})`);
|
|
327
|
+
return cached.token;
|
|
328
|
+
}
|
|
329
|
+
const { token, expiresAt } = await (0, import_db2.requestToken)({
|
|
330
|
+
endpoint: audience,
|
|
331
|
+
label: `odla CLI admin AI (${scope})`,
|
|
332
|
+
scopes: [scope],
|
|
333
|
+
fetch: doFetch,
|
|
334
|
+
onCode: async ({ userCode, expiresIn }) => {
|
|
335
|
+
const approvalUrl = handshakeUrl(audience, userCode);
|
|
336
|
+
out.log(`Approve scoped request ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
|
|
337
|
+
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);
|
|
338
|
+
if (shouldOpen) await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
const tokens = cache?.platform === audience ? { ...cache.tokens ?? {} } : {};
|
|
342
|
+
tokens[scope] = { token, expiresAt };
|
|
343
|
+
if ((0, import_node_fs2.existsSync)(`${import_node_process3.default.cwd()}/.git`)) ensureGitignore(import_node_process3.default.cwd(), [tokenFile]);
|
|
344
|
+
writePrivateJson(tokenFile, { platform: audience, tokens });
|
|
345
|
+
out.log(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
|
|
346
|
+
return token;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/admin-ai-audit.ts
|
|
350
|
+
function adminAiAuditQuery(filters) {
|
|
351
|
+
if (filters.limit === void 0) return "";
|
|
352
|
+
if (!Number.isSafeInteger(filters.limit) || filters.limit < 1 || filters.limit > 200) {
|
|
353
|
+
throw new Error("audit limit must be an integer from 1 to 200");
|
|
354
|
+
}
|
|
355
|
+
return `?limit=${filters.limit}`;
|
|
356
|
+
}
|
|
357
|
+
async function readAdminAiAudit(request) {
|
|
358
|
+
const response = await request.fetch(`${request.platform}/registry/platform/ai-audit${request.query}`, {
|
|
359
|
+
headers: request.headers
|
|
360
|
+
});
|
|
361
|
+
const body = await responseBody(response);
|
|
362
|
+
if (!response.ok) throw new Error(apiError(response.status, body));
|
|
363
|
+
if (request.json) {
|
|
364
|
+
request.stdout.log(JSON.stringify(body, null, 2));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const events = isRecord(body) && Array.isArray(body.events) ? body.events.filter(isRecord) : [];
|
|
368
|
+
request.stdout.log("when change target before -> after actor");
|
|
369
|
+
for (const event of events) {
|
|
370
|
+
const before = isRecord(event.oldPolicy) ? event.oldPolicy : void 0;
|
|
371
|
+
const after = isRecord(event.newPolicy) ? event.newPolicy : void 0;
|
|
372
|
+
const route = before && after ? `${String(before.provider)}/${String(before.model)}@v${String(before.version)} -> ${String(after.provider)}/${String(after.model)}@v${String(after.version)}` : "value not retained";
|
|
373
|
+
request.stdout.log([
|
|
374
|
+
timestamp(event.createdAt),
|
|
375
|
+
String(event.changeKind ?? ""),
|
|
376
|
+
String(event.purpose ?? event.provider ?? ""),
|
|
377
|
+
route,
|
|
378
|
+
`${String(event.actorType ?? "")}:${String(event.actorId ?? "")}`
|
|
379
|
+
].join(" "));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
async function responseBody(response) {
|
|
383
|
+
const text = await response.text();
|
|
384
|
+
if (!text) return {};
|
|
385
|
+
try {
|
|
386
|
+
return JSON.parse(text);
|
|
387
|
+
} catch {
|
|
388
|
+
return { message: text.slice(0, 300) };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function apiError(status, body) {
|
|
392
|
+
const error = isRecord(body) && isRecord(body.error) ? body.error : void 0;
|
|
393
|
+
const message = error && typeof error.message === "string" ? error.message : isRecord(body) && typeof body.message === "string" ? body.message : "request failed";
|
|
394
|
+
return `read System AI admin changes failed (${status}): ${message}`;
|
|
395
|
+
}
|
|
396
|
+
function timestamp(value) {
|
|
397
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return String(value ?? "");
|
|
398
|
+
const date = new Date(value);
|
|
399
|
+
return Number.isFinite(date.valueOf()) ? date.toISOString() : "";
|
|
400
|
+
}
|
|
401
|
+
function isRecord(value) {
|
|
402
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/admin-ai-policy.ts
|
|
406
|
+
var SYSTEM_AI_PURPOSES = ["o11y.triage", "security.discovery", "security.validation"];
|
|
407
|
+
function requireSystemAiPurpose(value) {
|
|
408
|
+
if (!SYSTEM_AI_PURPOSES.includes(value)) {
|
|
409
|
+
throw new Error(`purpose must be one of: ${SYSTEM_AI_PURPOSES.join(", ")}`);
|
|
410
|
+
}
|
|
411
|
+
return value;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/admin-ai-usage.ts
|
|
415
|
+
function adminAiUsageQuery(filters) {
|
|
416
|
+
const params = new URLSearchParams();
|
|
417
|
+
if (filters.appId?.trim()) params.set("appId", filters.appId.trim());
|
|
418
|
+
if (filters.env?.trim()) params.set("env", filters.env.trim());
|
|
419
|
+
if (filters.runId?.trim()) params.set("runId", filters.runId.trim());
|
|
420
|
+
if (filters.limit !== void 0) params.set("limit", String(usageLimit(filters.limit)));
|
|
421
|
+
const query = params.toString();
|
|
422
|
+
return query ? `?${query}` : "";
|
|
423
|
+
}
|
|
424
|
+
async function readAdminAiUsage(request) {
|
|
425
|
+
const res = await request.fetch(`${request.platform}/registry/platform/ai-usage${request.query}`, {
|
|
426
|
+
headers: request.headers
|
|
427
|
+
});
|
|
428
|
+
const body = await responseBody2(res);
|
|
429
|
+
if (!res.ok) throw new Error(apiError2("read platform AI usage", res.status, body));
|
|
430
|
+
if (request.json) request.stdout.log(JSON.stringify(body, null, 2));
|
|
431
|
+
else printUsage(body, request.stdout);
|
|
432
|
+
}
|
|
433
|
+
function usageLimit(value) {
|
|
434
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 500) {
|
|
435
|
+
throw new Error("usage limit must be an integer from 1 to 500");
|
|
436
|
+
}
|
|
437
|
+
return value;
|
|
438
|
+
}
|
|
439
|
+
function printUsage(body, out) {
|
|
440
|
+
const aggregates = isRecord2(body) && isRecord2(body.aggregates) && Array.isArray(body.aggregates.byStatus) ? body.aggregates.byStatus.filter(isRecord2) : [];
|
|
441
|
+
out.log(`All retained hosted-security status totals (up to ${String(isRecord2(body) ? body.retentionDays ?? 90 : 90)} days; o11y triage excluded)
|
|
442
|
+
status calls input tokens output tokens cost unpriced calls`);
|
|
443
|
+
for (const row of aggregates) {
|
|
444
|
+
out.log([
|
|
445
|
+
String(row.status ?? ""),
|
|
446
|
+
String(row.calls ?? ""),
|
|
447
|
+
String(row.input_tokens ?? ""),
|
|
448
|
+
String(row.output_tokens ?? ""),
|
|
449
|
+
formatMicrousd(row.cost_microusd),
|
|
450
|
+
String(row.unpriced_calls ?? "")
|
|
451
|
+
].join(" "));
|
|
452
|
+
}
|
|
453
|
+
const events = isRecord2(body) && Array.isArray(body.events) ? body.events.filter(isRecord2) : [];
|
|
454
|
+
out.log(`Latest ${String(isRecord2(body) ? body.eventLimit ?? events.length : events.length)} hosted-security events
|
|
455
|
+
when app/env run actor purpose/role route / policy tokens cost status`);
|
|
456
|
+
for (const event of events) {
|
|
457
|
+
const input = numeric(event.input_tokens);
|
|
458
|
+
const output = numeric(event.output_tokens);
|
|
459
|
+
const priced = event.priced === true || event.priced === 1;
|
|
460
|
+
out.log([
|
|
461
|
+
timestamp2(event.created_at),
|
|
462
|
+
`${String(event.app_id ?? "")}/${String(event.env ?? "")}`,
|
|
463
|
+
String(event.run_id ?? ""),
|
|
464
|
+
`${String(event.actor_type ?? "")}:${String(event.actor_id ?? "")}`,
|
|
465
|
+
`${String(event.purpose ?? "")}/${String(event.role ?? "")}`,
|
|
466
|
+
`${String(event.provider ?? "")}/${String(event.model ?? "")}@v${String(event.policy_version ?? "unknown")}`,
|
|
467
|
+
String(input + output),
|
|
468
|
+
priced ? formatMicrousd(event.cost_microusd) : "unpriced",
|
|
469
|
+
`${String(event.status ?? "")}${event.error_code ? `:${String(event.error_code)}` : ""}`
|
|
470
|
+
].join(" "));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function numeric(value) {
|
|
474
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
475
|
+
}
|
|
476
|
+
function formatMicrousd(value) {
|
|
477
|
+
return typeof value === "number" && Number.isFinite(value) ? `$${(value / 1e6).toFixed(6)}` : "unpriced";
|
|
478
|
+
}
|
|
479
|
+
function timestamp2(value) {
|
|
480
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return String(value ?? "");
|
|
481
|
+
const date = new Date(value);
|
|
482
|
+
return Number.isFinite(date.valueOf()) ? date.toISOString() : "";
|
|
483
|
+
}
|
|
484
|
+
async function responseBody2(res) {
|
|
485
|
+
const text = await res.text();
|
|
486
|
+
if (!text) return {};
|
|
487
|
+
try {
|
|
488
|
+
return JSON.parse(text);
|
|
489
|
+
} catch {
|
|
490
|
+
return { message: text.slice(0, 300) };
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function apiError2(action, status, body) {
|
|
494
|
+
const error = isRecord2(body) && isRecord2(body.error) ? body.error : void 0;
|
|
495
|
+
const message = error && typeof error.message === "string" ? error.message : isRecord2(body) && typeof body.message === "string" ? body.message : "request failed";
|
|
496
|
+
return `${action} failed (${status}): ${message}`;
|
|
497
|
+
}
|
|
498
|
+
function isRecord2(value) {
|
|
499
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// src/admin-ai.ts
|
|
503
|
+
async function adminAi(options) {
|
|
504
|
+
const platform = platformAudience(options.platform ?? import_node_process4.default.env.ODLA_PLATFORM ?? "https://odla.ai");
|
|
505
|
+
const doFetch = options.fetch ?? fetch;
|
|
506
|
+
const out = options.stdout ?? console;
|
|
507
|
+
const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
|
|
508
|
+
const auditQuery = options.action === "audit" ? adminAiAuditQuery(options) : void 0;
|
|
509
|
+
const scope = options.action === "set" ? "platform:ai:policy:write" : options.action === "credential-set" ? "platform:ai:credential:write" : options.action === "credentials" ? "platform:ai:credential:read" : options.action === "usage" ? "platform:ai:usage:read" : options.action === "audit" ? "platform:ai:audit:read" : "platform:ai:policy:read";
|
|
510
|
+
const token = await resolveAdminPlatformToken({
|
|
511
|
+
platform,
|
|
512
|
+
scope,
|
|
513
|
+
token: options.token,
|
|
514
|
+
open: options.open,
|
|
515
|
+
fetch: doFetch,
|
|
516
|
+
stdout: out,
|
|
517
|
+
openApprovalUrl: options.openApprovalUrl,
|
|
518
|
+
tokenFile: options.tokenFile
|
|
519
|
+
});
|
|
520
|
+
const headers = { authorization: `Bearer ${token}`, "content-type": "application/json" };
|
|
521
|
+
if (options.action === "usage") {
|
|
522
|
+
await readAdminAiUsage({ platform, query: usageQuery ?? "", headers, json: options.json, fetch: doFetch, stdout: out });
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (options.action === "audit") {
|
|
526
|
+
await readAdminAiAudit({ platform, query: auditQuery ?? "", headers, json: options.json, fetch: doFetch, stdout: out });
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (options.action === "credentials") {
|
|
530
|
+
const res2 = await doFetch(`${platform}/registry/platform/ai-credentials`, { headers });
|
|
531
|
+
const body2 = await responseBody3(res2);
|
|
532
|
+
if (!res2.ok) throw new Error(apiError3("read platform AI credential status", res2.status, body2));
|
|
533
|
+
const credentials = isRecord3(body2) && isRecord3(body2.credentials) ? body2.credentials : body2;
|
|
534
|
+
if (options.json) out.log(JSON.stringify({ credentials }, null, 2));
|
|
535
|
+
else for (const [provider, status] of Object.entries(isRecord3(credentials) ? credentials : {})) {
|
|
536
|
+
const label = isRecord3(status) && status.set === true ? status.readable === true ? "vault readable" : "stored; vault unreadable" : "not set";
|
|
537
|
+
out.log(`${provider} ${label}`);
|
|
538
|
+
}
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (options.action === "credential-set") {
|
|
542
|
+
const provider = requireProvider(options.credentialProvider);
|
|
543
|
+
const value = await credentialValue(options);
|
|
544
|
+
const res2 = await doFetch(`${platform}/registry/platform/ai-credentials/${encodeURIComponent(provider)}`, {
|
|
545
|
+
method: "PUT",
|
|
546
|
+
headers,
|
|
547
|
+
body: JSON.stringify({ value })
|
|
548
|
+
});
|
|
549
|
+
const body2 = await responseBody3(res2);
|
|
550
|
+
if (!res2.ok) throw new Error(apiError3(`store ${provider} platform credential`, res2.status, body2));
|
|
551
|
+
out.log(`stored ${provider} platform credential (write-only; value was not returned)`);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (options.action === "show" || options.action === "models") {
|
|
555
|
+
const res2 = await doFetch(`${platform}/registry/platform/ai-policies`, { headers });
|
|
556
|
+
const body2 = await responseBody3(res2);
|
|
557
|
+
if (!res2.ok) throw new Error(apiError3("read platform AI policies", res2.status, body2));
|
|
558
|
+
if (options.action === "models") {
|
|
559
|
+
const models = catalogModels(body2).filter((model) => !options.provider || model.provider === options.provider);
|
|
560
|
+
if (!models.length) throw new Error(options.provider ? `no System AI models found for provider ${options.provider}` : "platform returned no System AI models");
|
|
561
|
+
if (options.json) out.log(JSON.stringify({ models }, null, 2));
|
|
562
|
+
else for (const model of models) out.log(`${model.provider} ${model.id}`);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
const policies = policyArray(body2);
|
|
566
|
+
if (options.json) {
|
|
567
|
+
out.log(JSON.stringify({ policies, models: catalogModels(body2) }, null, 2));
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
out.log("purpose enabled provider model max calls max input bytes max output tokens");
|
|
571
|
+
for (const policy2 of policies) {
|
|
572
|
+
out.log([
|
|
573
|
+
policy2.purpose,
|
|
574
|
+
String(policy2.enabled),
|
|
575
|
+
policy2.provider,
|
|
576
|
+
policy2.model,
|
|
577
|
+
String(policy2.maxCallsPerRun ?? ""),
|
|
578
|
+
String(policy2.maxInputBytes ?? ""),
|
|
579
|
+
String(policy2.maxOutputTokens ?? "")
|
|
580
|
+
].join(" "));
|
|
581
|
+
}
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (options.purpose === "security") {
|
|
585
|
+
const currentResponse = await doFetch(`${platform}/registry/platform/ai-policies`, { headers });
|
|
586
|
+
const currentBody = await responseBody3(currentResponse);
|
|
587
|
+
if (!currentResponse.ok) throw new Error(apiError3("read current security AI policies", currentResponse.status, currentBody));
|
|
588
|
+
const policies = policyArray(currentBody);
|
|
589
|
+
const discovery = policies.find((policy2) => policy2.purpose === "security.discovery");
|
|
590
|
+
const validation = policies.find((policy2) => policy2.purpose === "security.validation");
|
|
591
|
+
if (!discovery || !validation || !Number.isSafeInteger(discovery.version) || !Number.isSafeInteger(validation.version)) {
|
|
592
|
+
throw new Error("platform returned invalid security AI policy revisions");
|
|
593
|
+
}
|
|
594
|
+
const shared = {
|
|
595
|
+
...options.enabled === void 0 ? {} : { enabled: options.enabled },
|
|
596
|
+
...options.maxInputBytes === void 0 ? {} : { maxInputBytes: options.maxInputBytes },
|
|
597
|
+
...options.maxOutputTokens === void 0 ? {} : { maxOutputTokens: options.maxOutputTokens },
|
|
598
|
+
...options.maxCallsPerRun === void 0 ? {} : { maxCallsPerRun: options.maxCallsPerRun }
|
|
599
|
+
};
|
|
600
|
+
const discoveryUpdate = {
|
|
601
|
+
...shared,
|
|
602
|
+
...options.discoveryProvider?.trim() ? { provider: options.discoveryProvider.trim() } : {},
|
|
603
|
+
...options.discoveryModel?.trim() ? { model: options.discoveryModel.trim() } : {}
|
|
604
|
+
};
|
|
605
|
+
const validationUpdate = {
|
|
606
|
+
...shared,
|
|
607
|
+
...options.validationProvider?.trim() ? { provider: options.validationProvider.trim() } : {},
|
|
608
|
+
...options.validationModel?.trim() ? { model: options.validationModel.trim() } : {}
|
|
609
|
+
};
|
|
610
|
+
if (!Object.keys(discoveryUpdate).length && !Object.keys(validationUpdate).length) {
|
|
611
|
+
throw new Error("security set requires a route, enabled state, or budget change");
|
|
612
|
+
}
|
|
613
|
+
const res2 = await doFetch(`${platform}/registry/platform/ai-policies/security`, {
|
|
614
|
+
method: "PUT",
|
|
615
|
+
headers,
|
|
616
|
+
body: JSON.stringify({
|
|
617
|
+
expectedVersions: { discovery: discovery.version, validation: validation.version },
|
|
618
|
+
discovery: discoveryUpdate,
|
|
619
|
+
validation: validationUpdate
|
|
620
|
+
})
|
|
621
|
+
});
|
|
622
|
+
const response2 = await responseBody3(res2);
|
|
623
|
+
if (!res2.ok) throw new Error(apiError3("update security review routes", res2.status, response2));
|
|
624
|
+
out.log(options.json ? JSON.stringify(response2, null, 2) : "updated security review discovery and independent validation routes atomically");
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const purpose = requireSystemAiPurpose(options.purpose);
|
|
628
|
+
const body = {
|
|
629
|
+
...options.provider?.trim() ? { provider: options.provider.trim() } : {},
|
|
630
|
+
...options.model?.trim() ? { model: options.model.trim() } : {},
|
|
631
|
+
...options.enabled === void 0 ? {} : { enabled: options.enabled },
|
|
632
|
+
...options.maxInputBytes === void 0 ? {} : { maxInputBytes: options.maxInputBytes },
|
|
633
|
+
...options.maxOutputTokens === void 0 ? {} : { maxOutputTokens: options.maxOutputTokens },
|
|
634
|
+
...options.maxCallsPerRun === void 0 ? {} : { maxCallsPerRun: options.maxCallsPerRun }
|
|
635
|
+
};
|
|
636
|
+
if (!Object.keys(body).length) throw new Error("set requires a provider, model, enabled state, or budget change");
|
|
637
|
+
const res = await doFetch(`${platform}/registry/platform/ai-policies/${encodeURIComponent(purpose)}`, {
|
|
638
|
+
method: "PUT",
|
|
639
|
+
headers,
|
|
640
|
+
body: JSON.stringify(body)
|
|
641
|
+
});
|
|
642
|
+
const response = await responseBody3(res);
|
|
643
|
+
if (!res.ok) throw new Error(apiError3(`update ${purpose}`, res.status, response));
|
|
644
|
+
const policy = isRecord3(response) && isRecord3(response.policy) ? response.policy : isRecord3(response) ? response : {};
|
|
645
|
+
out.log(options.json ? JSON.stringify({ policy }, null, 2) : `updated ${purpose}: ${String(policy.provider ?? "unchanged")}/${String(policy.model ?? "unchanged")}`);
|
|
646
|
+
}
|
|
647
|
+
function requireProvider(value) {
|
|
648
|
+
if (value !== "anthropic" && value !== "openai" && value !== "google") {
|
|
649
|
+
throw new Error("provider must be one of: anthropic, openai, google");
|
|
650
|
+
}
|
|
651
|
+
return value;
|
|
652
|
+
}
|
|
653
|
+
async function credentialValue(options) {
|
|
654
|
+
if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
|
|
655
|
+
let value;
|
|
656
|
+
if (options.fromEnv) value = import_node_process4.default.env[options.fromEnv];
|
|
657
|
+
else if (options.stdin) value = await (options.readStdin ?? readStdin)();
|
|
658
|
+
else throw new Error("credential input required: use --from-env <NAME> or --stdin; values are never accepted as arguments");
|
|
659
|
+
value = value?.replace(/[\r\n]+$/, "");
|
|
660
|
+
if (!value) throw new Error(options.fromEnv ? `environment variable ${options.fromEnv} is empty or unset` : "stdin credential is empty");
|
|
661
|
+
if (new TextEncoder().encode(value).byteLength > 64 * 1024) throw new Error("credential exceeds 64 KiB");
|
|
662
|
+
return value;
|
|
663
|
+
}
|
|
664
|
+
async function readStdin() {
|
|
665
|
+
let value = "";
|
|
666
|
+
for await (const chunk of import_node_process4.default.stdin) {
|
|
667
|
+
value += String(chunk);
|
|
668
|
+
if (value.length > 64 * 1024) throw new Error("credential exceeds 64 KiB");
|
|
669
|
+
}
|
|
670
|
+
return value;
|
|
671
|
+
}
|
|
672
|
+
function policyArray(body) {
|
|
673
|
+
if (isRecord3(body) && Array.isArray(body.policies)) return body.policies.filter(isRecord3);
|
|
674
|
+
if (isRecord3(body) && isRecord3(body.policies)) return Object.values(body.policies).filter(isRecord3);
|
|
675
|
+
throw new Error("platform returned an invalid AI policy response");
|
|
676
|
+
}
|
|
677
|
+
function catalogModels(body) {
|
|
678
|
+
if (!isRecord3(body) || !isRecord3(body.catalog) || !Array.isArray(body.catalog.models)) {
|
|
679
|
+
throw new Error("platform returned an invalid System AI model catalog");
|
|
680
|
+
}
|
|
681
|
+
return body.catalog.models.filter((value) => isRecord3(value) && typeof value.id === "string" && typeof value.provider === "string");
|
|
682
|
+
}
|
|
683
|
+
async function responseBody3(res) {
|
|
684
|
+
const text = await res.text();
|
|
685
|
+
if (!text) return {};
|
|
686
|
+
try {
|
|
687
|
+
return JSON.parse(text);
|
|
688
|
+
} catch {
|
|
689
|
+
return { message: text.slice(0, 300) };
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
function apiError3(action, status, body) {
|
|
693
|
+
const error = isRecord3(body) && isRecord3(body.error) ? body.error : void 0;
|
|
694
|
+
const message = error && typeof error.message === "string" ? error.message : isRecord3(body) && typeof body.message === "string" ? body.message : "request failed";
|
|
695
|
+
return `${action} failed (${status}): ${message}`;
|
|
696
|
+
}
|
|
697
|
+
function isRecord3(value) {
|
|
698
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// src/argv.ts
|
|
702
|
+
function parseArgv(argv) {
|
|
703
|
+
const positionals = [];
|
|
704
|
+
const options = {};
|
|
705
|
+
for (let i = 0; i < argv.length; i++) {
|
|
706
|
+
const arg = argv[i];
|
|
707
|
+
if (!arg.startsWith("--")) {
|
|
708
|
+
positionals.push(arg);
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
const eq = arg.indexOf("=");
|
|
712
|
+
const rawName = arg.slice(2, eq === -1 ? void 0 : eq);
|
|
713
|
+
if (!rawName) continue;
|
|
714
|
+
if (rawName.startsWith("no-")) {
|
|
715
|
+
options[rawName.slice(3)] = false;
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
if (eq !== -1) {
|
|
719
|
+
addOption(options, rawName, arg.slice(eq + 1));
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
const value = argv[i + 1];
|
|
723
|
+
if (value !== void 0 && !value.startsWith("--")) {
|
|
724
|
+
i++;
|
|
725
|
+
addOption(options, rawName, value);
|
|
726
|
+
} else {
|
|
727
|
+
addOption(options, rawName, true);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return { positionals, options };
|
|
731
|
+
}
|
|
732
|
+
function assertArgs(parsed, allowedOptions, maxPositionals) {
|
|
733
|
+
const allowed = new Set(allowedOptions);
|
|
734
|
+
for (const name of Object.keys(parsed.options)) {
|
|
735
|
+
if (!allowed.has(name)) throw new Error(`unknown option "--${name}"; run "odla-ai help" for supported options`);
|
|
736
|
+
}
|
|
737
|
+
if (parsed.positionals.length > maxPositionals) {
|
|
738
|
+
throw new Error(`unexpected argument "${parsed.positionals[maxPositionals]}"; run "odla-ai help"`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
function requiredString(value, name) {
|
|
742
|
+
const result = stringOpt(value);
|
|
743
|
+
if (!result) throw new Error(`${name} is required`);
|
|
744
|
+
return result;
|
|
745
|
+
}
|
|
746
|
+
function stringOpt(value) {
|
|
747
|
+
if (typeof value === "string") return value;
|
|
748
|
+
if (Array.isArray(value)) return value[value.length - 1];
|
|
749
|
+
return void 0;
|
|
750
|
+
}
|
|
751
|
+
function listOpt(value) {
|
|
752
|
+
if (value === void 0 || typeof value === "boolean") return void 0;
|
|
753
|
+
const values = Array.isArray(value) ? value : [value];
|
|
754
|
+
return values.flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
|
|
755
|
+
}
|
|
756
|
+
function boolOpt(value) {
|
|
757
|
+
if (typeof value === "boolean") return value;
|
|
758
|
+
if (typeof value === "string" && (value === "true" || value === "false")) return value === "true";
|
|
759
|
+
if (value === void 0) return void 0;
|
|
760
|
+
throw new Error("boolean option must be true or false");
|
|
761
|
+
}
|
|
762
|
+
function numberOpt(value, flag) {
|
|
763
|
+
if (value === void 0) return void 0;
|
|
764
|
+
const raw = stringOpt(value);
|
|
765
|
+
const parsed = raw === void 0 ? Number.NaN : Number(raw);
|
|
766
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) throw new Error(`${flag} requires a positive integer`);
|
|
767
|
+
return parsed;
|
|
768
|
+
}
|
|
769
|
+
function addOption(options, name, value) {
|
|
770
|
+
const current = options[name];
|
|
771
|
+
if (current === void 0) options[name] = value;
|
|
772
|
+
else if (Array.isArray(current)) current.push(String(value));
|
|
773
|
+
else options[name] = [String(current), String(value)];
|
|
774
|
+
}
|
|
54
775
|
|
|
55
776
|
// src/capabilities.ts
|
|
56
777
|
var CAPABILITIES = {
|
|
@@ -59,7 +780,9 @@ var CAPABILITIES = {
|
|
|
59
780
|
"issue, persist, and explicitly rotate configured service credentials",
|
|
60
781
|
"write local Worker values and push deployed Worker secrets through Wrangler stdin",
|
|
61
782
|
"push db schema/rules and configure platform AI, auth, and deployment links",
|
|
62
|
-
"validate config offline and smoke-test a provisioned db environment"
|
|
783
|
+
"validate config offline and smoke-test a provisioned db environment",
|
|
784
|
+
"run app-attributed hosted security discovery and independent validation without provider keys",
|
|
785
|
+
"let admins manage system AI policy and credentials, inspect attributed usage, and read immutable admin changes through separate short-lived capability-only approvals"
|
|
63
786
|
],
|
|
64
787
|
agent: [
|
|
65
788
|
"install and import the selected odla SDKs",
|
|
@@ -70,11 +793,13 @@ var CAPABILITIES = {
|
|
|
70
793
|
"approve the odla device code and one-off third-party logins",
|
|
71
794
|
"consent to production changes with --yes",
|
|
72
795
|
"request destructive credential rotation explicitly",
|
|
73
|
-
"review application semantics, security findings, releases, and merges"
|
|
796
|
+
"review application semantics, security findings, releases, and merges",
|
|
797
|
+
"approve redacted-source disclosure before hosted security reasoning"
|
|
74
798
|
],
|
|
75
799
|
studio: [
|
|
76
800
|
"view telemetry and environment state",
|
|
77
|
-
"perform manual credential recovery when the CLI's local shown-once copy is unavailable"
|
|
801
|
+
"perform manual credential recovery when the CLI's local shown-once copy is unavailable",
|
|
802
|
+
"configure system AI purposes and view app/environment/run-attributed usage"
|
|
78
803
|
]
|
|
79
804
|
};
|
|
80
805
|
function printCapabilities(json = false, out = console) {
|
|
@@ -95,28 +820,28 @@ function printGroup(out, heading, items) {
|
|
|
95
820
|
}
|
|
96
821
|
|
|
97
822
|
// src/config.ts
|
|
98
|
-
var
|
|
99
|
-
var
|
|
823
|
+
var import_node_fs3 = require("fs");
|
|
824
|
+
var import_node_path2 = require("path");
|
|
100
825
|
var import_node_url = require("url");
|
|
101
826
|
var DEFAULT_PLATFORM = "https://odla.ai";
|
|
102
827
|
var DEFAULT_ENVS = ["dev"];
|
|
103
828
|
var DEFAULT_SERVICES = ["db", "ai"];
|
|
104
829
|
async function loadProjectConfig(configPath = "odla.config.mjs") {
|
|
105
|
-
const resolved = (0,
|
|
106
|
-
if (!(0,
|
|
830
|
+
const resolved = (0, import_node_path2.resolve)(configPath);
|
|
831
|
+
if (!(0, import_node_fs3.existsSync)(resolved)) {
|
|
107
832
|
throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
|
|
108
833
|
}
|
|
109
834
|
const raw = await loadConfigModule(resolved);
|
|
110
|
-
const rootDir = (0,
|
|
835
|
+
const rootDir = (0, import_node_path2.dirname)(resolved);
|
|
111
836
|
validateRawConfig(raw, resolved);
|
|
112
837
|
const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
|
|
113
838
|
const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
|
|
114
839
|
const envs = unique(raw.envs?.length ? raw.envs : DEFAULT_ENVS);
|
|
115
840
|
const services = unique(raw.services?.length ? raw.services : DEFAULT_SERVICES);
|
|
116
841
|
const local = {
|
|
117
|
-
tokenFile: (0,
|
|
118
|
-
credentialsFile: (0,
|
|
119
|
-
devVarsFile: (0,
|
|
842
|
+
tokenFile: (0, import_node_path2.resolve)(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
|
|
843
|
+
credentialsFile: (0, import_node_path2.resolve)(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
|
|
844
|
+
devVarsFile: (0, import_node_path2.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
|
|
120
845
|
gitignore: raw.local?.gitignore ?? true
|
|
121
846
|
};
|
|
122
847
|
return {
|
|
@@ -133,9 +858,9 @@ async function loadProjectConfig(configPath = "odla.config.mjs") {
|
|
|
133
858
|
async function resolveDataExport(cfg, value, names) {
|
|
134
859
|
if (value === void 0 || value === null || value === false) return void 0;
|
|
135
860
|
if (typeof value !== "string") return value;
|
|
136
|
-
const target = (0,
|
|
861
|
+
const target = (0, import_node_path2.isAbsolute)(value) ? value : (0, import_node_path2.resolve)(cfg.rootDir, value);
|
|
137
862
|
if (target.endsWith(".json")) {
|
|
138
|
-
return JSON.parse((0,
|
|
863
|
+
return JSON.parse((0, import_node_fs3.readFileSync)(target, "utf8"));
|
|
139
864
|
}
|
|
140
865
|
const mod = await import((0, import_node_url.pathToFileURL)(target).href);
|
|
141
866
|
for (const name of names) {
|
|
@@ -186,175 +911,49 @@ function validId(value) {
|
|
|
186
911
|
return typeof value === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value);
|
|
187
912
|
}
|
|
188
913
|
async function loadConfigModule(path) {
|
|
189
|
-
if (path.endsWith(".json")) return JSON.parse((0,
|
|
914
|
+
if (path.endsWith(".json")) return JSON.parse((0, import_node_fs3.readFileSync)(path, "utf8"));
|
|
190
915
|
const mod = await import(`${(0, import_node_url.pathToFileURL)(path).href}?t=${Date.now()}`);
|
|
191
916
|
const value = mod.default ?? mod.config;
|
|
192
917
|
if (typeof value === "function") return await value();
|
|
193
918
|
return value;
|
|
194
919
|
}
|
|
195
|
-
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]);
|
|
334
|
-
}
|
|
335
|
-
function writePrivateText(path, text) {
|
|
336
|
-
(0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(path), { recursive: true });
|
|
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);
|
|
920
|
+
function trimSlash(value) {
|
|
921
|
+
return value.replace(/\/+$/, "");
|
|
341
922
|
}
|
|
342
|
-
function
|
|
343
|
-
|
|
344
|
-
if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path2.isAbsolute)(rel)) return null;
|
|
345
|
-
return rel.replaceAll("\\", "/");
|
|
923
|
+
function unique(values) {
|
|
924
|
+
return [...new Set(values.filter(Boolean))];
|
|
346
925
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
926
|
+
|
|
927
|
+
// src/doctor-checks.ts
|
|
928
|
+
var import_node_child_process3 = require("child_process");
|
|
929
|
+
var import_node_fs5 = require("fs");
|
|
930
|
+
var import_node_path4 = require("path");
|
|
931
|
+
|
|
932
|
+
// src/redact.ts
|
|
933
|
+
var REPLACEMENTS = [
|
|
934
|
+
[/odla_(sk|dev)_[A-Za-z0-9._-]+/g, "odla_$1_[redacted]"],
|
|
935
|
+
[/\bsk_(live|test)_[A-Za-z0-9]+/g, "sk_$1_[redacted]"],
|
|
936
|
+
[/\bsk-[A-Za-z0-9._-]+/g, "sk-[redacted]"],
|
|
937
|
+
[/\bwhsec_[A-Za-z0-9+/=]+/g, "whsec_[redacted]"],
|
|
938
|
+
[/\bo11y_[A-Za-z0-9]+/g, "o11y_[redacted]"],
|
|
939
|
+
[/\b(ghp|gho|github_pat)_[A-Za-z0-9_]+/g, "$1_[redacted]"],
|
|
940
|
+
[/\bAKIA[A-Z0-9]{12,}/g, "AKIA[redacted]"]
|
|
941
|
+
];
|
|
942
|
+
function redactSecrets(value) {
|
|
943
|
+
let result = value;
|
|
944
|
+
for (const [pattern, replacement] of REPLACEMENTS) result = result.replace(pattern, replacement);
|
|
945
|
+
return result;
|
|
946
|
+
}
|
|
947
|
+
function looksSecret(value) {
|
|
948
|
+
return redactSecrets(value) !== value || value.includes("-----BEGIN");
|
|
350
949
|
}
|
|
351
950
|
|
|
352
951
|
// src/wrangler.ts
|
|
353
|
-
var
|
|
354
|
-
var
|
|
952
|
+
var import_node_child_process2 = require("child_process");
|
|
953
|
+
var import_node_fs4 = require("fs");
|
|
355
954
|
var import_node_path3 = require("path");
|
|
356
955
|
var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
|
|
357
|
-
const child = (0,
|
|
956
|
+
const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
358
957
|
let stdout = "";
|
|
359
958
|
let stderr = "";
|
|
360
959
|
child.stdout.on("data", (chunk) => stdout += chunk.toString());
|
|
@@ -367,14 +966,14 @@ var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"]
|
|
|
367
966
|
function findWranglerConfig(rootDir) {
|
|
368
967
|
for (const name of WRANGLER_CONFIG_FILES) {
|
|
369
968
|
const path = (0, import_node_path3.join)(rootDir, name);
|
|
370
|
-
if ((0,
|
|
969
|
+
if ((0, import_node_fs4.existsSync)(path)) return path;
|
|
371
970
|
}
|
|
372
971
|
return null;
|
|
373
972
|
}
|
|
374
973
|
function readWranglerConfig(path) {
|
|
375
974
|
if (path.endsWith(".toml")) return null;
|
|
376
975
|
try {
|
|
377
|
-
return JSON.parse(stripJsonComments((0,
|
|
976
|
+
return JSON.parse(stripJsonComments((0, import_node_fs4.readFileSync)(path, "utf8")));
|
|
378
977
|
} catch {
|
|
379
978
|
return null;
|
|
380
979
|
}
|
|
@@ -445,7 +1044,7 @@ function lintRules(rules, entities, publicRead) {
|
|
|
445
1044
|
}
|
|
446
1045
|
return warnings;
|
|
447
1046
|
}
|
|
448
|
-
var defaultExec = (cmd, args, cwd) => (0,
|
|
1047
|
+
var defaultExec = (cmd, args, cwd) => (0, import_node_child_process3.execFileSync)(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
449
1048
|
function trackedSecretFiles(rootDir, exec = defaultExec, localPaths = []) {
|
|
450
1049
|
let output;
|
|
451
1050
|
try {
|
|
@@ -475,7 +1074,7 @@ function wranglerWarnings(rootDir) {
|
|
|
475
1074
|
const dir = (0, import_node_path4.resolve)(rootDir, assets.directory);
|
|
476
1075
|
if (dir === (0, import_node_path4.resolve)(rootDir)) {
|
|
477
1076
|
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,
|
|
1077
|
+
} else if ((0, import_node_fs5.existsSync)((0, import_node_path4.join)(dir, "node_modules"))) {
|
|
479
1078
|
warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
|
|
480
1079
|
}
|
|
481
1080
|
}
|
|
@@ -511,12 +1110,12 @@ function o11yProjectWarnings(rootDir) {
|
|
|
511
1110
|
return warnings;
|
|
512
1111
|
}
|
|
513
1112
|
const main = typeof config.main === "string" ? (0, import_node_path4.resolve)(rootDir, config.main) : null;
|
|
514
|
-
if (!main || !(0,
|
|
1113
|
+
if (!main || !(0, import_node_fs5.existsSync)(main)) {
|
|
515
1114
|
warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
|
|
516
1115
|
} else {
|
|
517
1116
|
let source = "";
|
|
518
1117
|
try {
|
|
519
|
-
source = (0,
|
|
1118
|
+
source = (0, import_node_fs5.readFileSync)(main, "utf8");
|
|
520
1119
|
} catch {
|
|
521
1120
|
}
|
|
522
1121
|
if (!/\bwithObservability\b/.test(source)) {
|
|
@@ -531,7 +1130,7 @@ function o11yProjectWarnings(rootDir) {
|
|
|
531
1130
|
}
|
|
532
1131
|
function readPackageJson(rootDir) {
|
|
533
1132
|
try {
|
|
534
|
-
return JSON.parse((0,
|
|
1133
|
+
return JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(rootDir, "package.json"), "utf8"));
|
|
535
1134
|
} catch {
|
|
536
1135
|
return null;
|
|
537
1136
|
}
|
|
@@ -598,13 +1197,13 @@ async function doctor(options) {
|
|
|
598
1197
|
}
|
|
599
1198
|
|
|
600
1199
|
// src/init.ts
|
|
601
|
-
var
|
|
1200
|
+
var import_node_fs6 = require("fs");
|
|
602
1201
|
var import_node_path5 = require("path");
|
|
603
1202
|
function initProject(options) {
|
|
604
1203
|
const out = options.stdout ?? console;
|
|
605
1204
|
const rootDir = (0, import_node_path5.resolve)(options.rootDir ?? process.cwd());
|
|
606
1205
|
const configPath = (0, import_node_path5.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
|
|
607
|
-
if ((0,
|
|
1206
|
+
if ((0, import_node_fs6.existsSync)(configPath) && !options.force) {
|
|
608
1207
|
throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
|
|
609
1208
|
}
|
|
610
1209
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
|
|
@@ -613,10 +1212,10 @@ function initProject(options) {
|
|
|
613
1212
|
const envs = options.envs?.length ? options.envs : ["dev"];
|
|
614
1213
|
const services = options.services?.length ? options.services : ["db", "ai"];
|
|
615
1214
|
const aiProvider = options.aiProvider ?? "anthropic";
|
|
616
|
-
(0,
|
|
617
|
-
(0,
|
|
618
|
-
(0,
|
|
619
|
-
(0,
|
|
1215
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path5.dirname)(configPath), { recursive: true });
|
|
1216
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path5.resolve)(rootDir, "src/odla"), { recursive: true });
|
|
1217
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path5.resolve)(rootDir, ".odla"), { recursive: true });
|
|
1218
|
+
(0, import_node_fs6.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
|
|
620
1219
|
writeIfMissing((0, import_node_path5.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
|
|
621
1220
|
writeIfMissing((0, import_node_path5.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
|
|
622
1221
|
ensureGitignore(rootDir);
|
|
@@ -625,8 +1224,8 @@ function initProject(options) {
|
|
|
625
1224
|
out.log("updated .gitignore for local odla credentials");
|
|
626
1225
|
}
|
|
627
1226
|
function writeIfMissing(path, text) {
|
|
628
|
-
if ((0,
|
|
629
|
-
(0,
|
|
1227
|
+
if ((0, import_node_fs6.existsSync)(path)) return;
|
|
1228
|
+
(0, import_node_fs6.writeFileSync)(path, text);
|
|
630
1229
|
}
|
|
631
1230
|
function configTemplate(input) {
|
|
632
1231
|
return `export default {
|
|
@@ -715,84 +1314,7 @@ function relativeDisplay(path, rootDir) {
|
|
|
715
1314
|
// src/provision.ts
|
|
716
1315
|
var import_apps2 = require("@odla-ai/apps");
|
|
717
1316
|
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
|
-
}
|
|
1317
|
+
var import_node_process5 = __toESM(require("process"), 1);
|
|
796
1318
|
|
|
797
1319
|
// src/provision-credentials.ts
|
|
798
1320
|
var import_apps = require("@odla-ai/apps");
|
|
@@ -1096,7 +1618,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
1096
1618
|
out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
|
|
1097
1619
|
}
|
|
1098
1620
|
if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
|
|
1099
|
-
const key =
|
|
1621
|
+
const key = import_node_process5.default.env[cfg.ai.keyEnv];
|
|
1100
1622
|
if (key) {
|
|
1101
1623
|
const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
|
|
1102
1624
|
await (0, import_ai.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
|
|
@@ -1156,10 +1678,170 @@ async function safeText2(res) {
|
|
|
1156
1678
|
}
|
|
1157
1679
|
}
|
|
1158
1680
|
|
|
1681
|
+
// src/security.ts
|
|
1682
|
+
var import_node_path6 = require("path");
|
|
1683
|
+
var import_security = require("@odla-ai/security");
|
|
1684
|
+
var import_node = require("@odla-ai/security/node");
|
|
1685
|
+
async function runHostedSecurity(options) {
|
|
1686
|
+
if (options.sourceDisclosureAck !== "redacted") {
|
|
1687
|
+
throw new Error("Hosted security requires sourceDisclosureAck=redacted before repository source may leave this process");
|
|
1688
|
+
}
|
|
1689
|
+
const selfAudit = options.selfAudit === true;
|
|
1690
|
+
const cfg = selfAudit ? void 0 : await loadProjectConfig(options.configPath ?? "odla.config.mjs");
|
|
1691
|
+
const appId = selfAudit ? "odla-ai" : cfg.app.id;
|
|
1692
|
+
const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
|
|
1693
|
+
const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
|
|
1694
|
+
const target = (0, import_node_path6.resolve)(options.target ?? cfg?.rootDir ?? ".");
|
|
1695
|
+
const output = (0, import_node_path6.resolve)(options.out ?? (0, import_node_path6.resolve)(target, ".odla/security/hosted"));
|
|
1696
|
+
const outputRelative = (0, import_node_path6.relative)(target, output).split(import_node_path6.sep).join("/");
|
|
1697
|
+
if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
|
|
1698
|
+
const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
|
|
1699
|
+
const tokenRequest = {
|
|
1700
|
+
platform,
|
|
1701
|
+
appId,
|
|
1702
|
+
env,
|
|
1703
|
+
selfAudit,
|
|
1704
|
+
scope: selfAudit ? "platform:security:self" : null
|
|
1705
|
+
};
|
|
1706
|
+
const token = await injectedToken(options, tokenRequest);
|
|
1707
|
+
const snapshot = await (0, import_node.snapshotDirectory)(target, {
|
|
1708
|
+
exclude: !outputRelative.startsWith("../") && !(0, import_node_path6.isAbsolute)(outputRelative) ? [outputRelative] : []
|
|
1709
|
+
});
|
|
1710
|
+
const hosted = await (0, import_security.createPlatformSecurityReasoners)({
|
|
1711
|
+
platform,
|
|
1712
|
+
token,
|
|
1713
|
+
appId,
|
|
1714
|
+
env,
|
|
1715
|
+
repository: snapshot.repository,
|
|
1716
|
+
revision: snapshot.revision,
|
|
1717
|
+
snapshotDigest: snapshot.digest,
|
|
1718
|
+
sourceDisclosure: "redacted",
|
|
1719
|
+
clientRunId: options.runId ?? crypto.randomUUID(),
|
|
1720
|
+
...selfAudit ? { selfAudit: { enabled: true, subject: "service:odla-security-self-audit" } } : {},
|
|
1721
|
+
fetch: options.fetch,
|
|
1722
|
+
signal: options.signal
|
|
1723
|
+
});
|
|
1724
|
+
const harness = (0, import_security.createSecurityHarness)({
|
|
1725
|
+
profile,
|
|
1726
|
+
store: new import_node.FileRunStore((0, import_node_path6.resolve)(output, "state")),
|
|
1727
|
+
discoveryReasoner: hosted.discoveryReasoner,
|
|
1728
|
+
validationReasoner: hosted.validationReasoner,
|
|
1729
|
+
policy: {
|
|
1730
|
+
modelSourceDisclosure: "redacted",
|
|
1731
|
+
active: false,
|
|
1732
|
+
allowNetwork: false
|
|
1733
|
+
}
|
|
1734
|
+
});
|
|
1735
|
+
const report = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
|
|
1736
|
+
await (0, import_node.writeSecurityArtifacts)(output, report);
|
|
1737
|
+
const reportDigest = await (0, import_security.securityFingerprint)(report);
|
|
1738
|
+
await hosted.complete({
|
|
1739
|
+
reportDigest,
|
|
1740
|
+
coverageStatus: report.coverageStatus,
|
|
1741
|
+
confirmed: report.metrics.confirmed,
|
|
1742
|
+
candidates: report.metrics.candidates
|
|
1743
|
+
}, { signal: options.signal });
|
|
1744
|
+
printSummary(options.stdout ?? console, appId, env, hosted.run, report, output);
|
|
1745
|
+
return Object.freeze({ report, run: hosted.run, output });
|
|
1746
|
+
}
|
|
1747
|
+
function selectEnv(requested, declared, configPath, rootDir) {
|
|
1748
|
+
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
1749
|
+
if (!env || !declared.includes(env)) {
|
|
1750
|
+
const shown = (0, import_node_path6.relative)(rootDir, configPath) || configPath;
|
|
1751
|
+
throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
|
|
1752
|
+
}
|
|
1753
|
+
return env;
|
|
1754
|
+
}
|
|
1755
|
+
async function injectedToken(options, request) {
|
|
1756
|
+
const value = options.token ?? await options.getToken?.(Object.freeze({ ...request }));
|
|
1757
|
+
if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
|
|
1758
|
+
throw new Error(
|
|
1759
|
+
request.selfAudit ? "Self-audit requires an injected, scoped platform security token" : "Hosted security requires an injected app developer token or getToken callback"
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1762
|
+
return value;
|
|
1763
|
+
}
|
|
1764
|
+
function profileFor(name, maxHuntTasks) {
|
|
1765
|
+
const profile = name === "odla" ? (0, import_security.odlaProfile)() : name === "cloudflare-app" ? (0, import_security.cloudflareAppProfile)() : name === "generic" ? (0, import_security.genericProfile)() : void 0;
|
|
1766
|
+
if (!profile) throw new Error(`unknown security profile "${String(name)}"`);
|
|
1767
|
+
if (maxHuntTasks === void 0) return profile;
|
|
1768
|
+
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
1769
|
+
return { ...profile, maxHuntTasks };
|
|
1770
|
+
}
|
|
1771
|
+
function printSummary(out, appId, env, run, report, output) {
|
|
1772
|
+
const complete = report.coverage.filter((cell) => cell.state === "complete").length;
|
|
1773
|
+
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
1774
|
+
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
1775
|
+
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
1776
|
+
out.log(` coverage: ${report.coverageStatus} ${complete}/${report.coverage.length} blocked=${report.metrics.blockedCells} shallow=${report.metrics.shallowCells} unscheduled=${report.metrics.unscheduledCells} budget_exhausted=${report.metrics.budgetExhaustedCells}`);
|
|
1777
|
+
if (report.callBudget) out.log(` calls: discovery=${formatBudget(report.callBudget.discovery)} validation=${formatBudget(report.callBudget.validation)}`);
|
|
1778
|
+
out.log(` findings: confirmed=${report.metrics.confirmed} needs_reproduction=${report.metrics.needsReproduction} candidates=${report.metrics.candidates}`);
|
|
1779
|
+
out.log(` report: ${(0, import_node_path6.resolve)(output, "REPORT.md")}`);
|
|
1780
|
+
}
|
|
1781
|
+
function formatBudget(usage) {
|
|
1782
|
+
return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
// src/help.ts
|
|
1786
|
+
var import_node_fs7 = require("fs");
|
|
1787
|
+
function cliVersion() {
|
|
1788
|
+
const pkg = JSON.parse((0, import_node_fs7.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
|
|
1789
|
+
return pkg.version ?? "unknown";
|
|
1790
|
+
}
|
|
1791
|
+
function printHelp() {
|
|
1792
|
+
console.log(`odla-ai
|
|
1793
|
+
|
|
1794
|
+
Usage:
|
|
1795
|
+
odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
|
|
1796
|
+
odla-ai init --app-id <id> --name <name> [--services db,ai,o11y] [--env dev --env prod]
|
|
1797
|
+
odla-ai doctor [--config odla.config.mjs]
|
|
1798
|
+
odla-ai capabilities [--json]
|
|
1799
|
+
odla-ai admin ai show [--platform https://odla.ai] [--json]
|
|
1800
|
+
odla-ai admin ai models [--provider <id>] [--json]
|
|
1801
|
+
odla-ai admin ai set <purpose> [--provider <id>] [--model <id>] [--enabled|--no-enabled]
|
|
1802
|
+
odla-ai admin ai set security --discovery-provider <id> --discovery-model <id>
|
|
1803
|
+
--validation-provider <id> --validation-model <id> [--enabled|--no-enabled]
|
|
1804
|
+
odla-ai admin ai credentials [--json]
|
|
1805
|
+
odla-ai admin ai credential set <provider> (--from-env <NAME>|--stdin)
|
|
1806
|
+
odla-ai admin ai usage [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
|
|
1807
|
+
odla-ai admin ai audit [--limit <1-200>] [--json]
|
|
1808
|
+
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
1809
|
+
odla-ai security run [target] --self --ack-redacted-source
|
|
1810
|
+
odla-ai provision [--config odla.config.mjs] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
1811
|
+
odla-ai smoke [--config odla.config.mjs] [--env dev]
|
|
1812
|
+
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
1813
|
+
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
1814
|
+
odla-ai version
|
|
1815
|
+
|
|
1816
|
+
Commands:
|
|
1817
|
+
setup Install offline odla runbooks for common coding-agent harnesses.
|
|
1818
|
+
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
1819
|
+
doctor Validate and summarize the project config without network calls.
|
|
1820
|
+
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
1821
|
+
admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
|
|
1822
|
+
security Run hosted discovery + independent validation with app/run attribution.
|
|
1823
|
+
provision Register services, configure them, persist credentials, optionally push secrets.
|
|
1824
|
+
smoke Verify local credentials, public-config, live schema, and db aggregate.
|
|
1825
|
+
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
1826
|
+
copilot, gemini, or agents (repeatable or comma-separated).
|
|
1827
|
+
secrets Push configured db/o11y secrets into the Worker via wrangler stdin.
|
|
1828
|
+
version Print the CLI version.
|
|
1829
|
+
|
|
1830
|
+
Safety:
|
|
1831
|
+
New projects target dev only. Add prod explicitly to odla.config.mjs and pass
|
|
1832
|
+
--yes to provision it; use --dry-run first to inspect the resolved plan.
|
|
1833
|
+
Provision caches the approved developer token and service credentials under
|
|
1834
|
+
.odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
|
|
1835
|
+
preflights Wrangler before any shown-once issuance or destructive rotation.
|
|
1836
|
+
Provision opens the approval page automatically in interactive terminals;
|
|
1837
|
+
use --open to force browser launch or --no-open to suppress it.
|
|
1838
|
+
`);
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1159
1841
|
// src/skill.ts
|
|
1160
|
-
var
|
|
1842
|
+
var import_node_fs8 = require("fs");
|
|
1161
1843
|
var import_node_os = require("os");
|
|
1162
|
-
var
|
|
1844
|
+
var import_node_path7 = require("path");
|
|
1163
1845
|
var import_node_url2 = require("url");
|
|
1164
1846
|
|
|
1165
1847
|
// src/skill-adapters.ts
|
|
@@ -1220,8 +1902,8 @@ function installSkill(options = {}) {
|
|
|
1220
1902
|
const files = listFiles(sourceDir);
|
|
1221
1903
|
if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
|
|
1222
1904
|
const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
|
|
1223
|
-
const root = (0,
|
|
1224
|
-
const home = (0,
|
|
1905
|
+
const root = (0, import_node_path7.resolve)(options.dir ?? process.cwd());
|
|
1906
|
+
const home = (0, import_node_path7.resolve)(options.homeDir ?? (0, import_node_os.homedir)());
|
|
1225
1907
|
const plans = /* @__PURE__ */ new Map();
|
|
1226
1908
|
const targets = /* @__PURE__ */ new Map();
|
|
1227
1909
|
const rememberTarget = (harness, target) => {
|
|
@@ -1235,48 +1917,48 @@ function installSkill(options = {}) {
|
|
|
1235
1917
|
plans.set(target, { target, content, boundary, managedMerge });
|
|
1236
1918
|
};
|
|
1237
1919
|
const planSkillTree = (targetDir2, boundary = root) => {
|
|
1238
|
-
for (const rel of files) plan((0,
|
|
1920
|
+
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
1921
|
};
|
|
1240
1922
|
let targetDir;
|
|
1241
1923
|
if (options.global) {
|
|
1242
|
-
const claudeRoot = (0,
|
|
1243
|
-
const codexRoot = (0,
|
|
1924
|
+
const claudeRoot = (0, import_node_path7.join)(home, ".claude", "skills");
|
|
1925
|
+
const codexRoot = (0, import_node_path7.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path7.join)(home, ".codex"), "skills");
|
|
1244
1926
|
targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
|
|
1245
1927
|
for (const harness of harnesses) {
|
|
1246
1928
|
const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
|
|
1247
|
-
planSkillTree(skillRoot, harness === "claude" ? home : (0,
|
|
1929
|
+
planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path7.dirname)((0, import_node_path7.dirname)(codexRoot)));
|
|
1248
1930
|
rememberTarget(harness, skillRoot);
|
|
1249
1931
|
}
|
|
1250
1932
|
} else {
|
|
1251
|
-
const sharedRoot = (0,
|
|
1933
|
+
const sharedRoot = (0, import_node_path7.join)(root, ".agents", "skills");
|
|
1252
1934
|
planSkillTree(sharedRoot);
|
|
1253
|
-
const claudeRoot = (0,
|
|
1935
|
+
const claudeRoot = (0, import_node_path7.join)(root, ".claude", "skills");
|
|
1254
1936
|
targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
|
|
1255
1937
|
for (const harness of harnesses) rememberTarget(harness, sharedRoot);
|
|
1256
1938
|
if (harnesses.includes("claude")) {
|
|
1257
1939
|
for (const skill of skillNames(files)) {
|
|
1258
|
-
const canonical = (0,
|
|
1259
|
-
plan((0,
|
|
1940
|
+
const canonical = (0, import_node_fs8.readFileSync)((0, import_node_path7.join)(sourceDir, skill, "SKILL.md"), "utf8");
|
|
1941
|
+
plan((0, import_node_path7.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
|
|
1260
1942
|
}
|
|
1261
1943
|
rememberTarget("claude", claudeRoot);
|
|
1262
1944
|
}
|
|
1263
1945
|
if (harnesses.includes("cursor")) {
|
|
1264
|
-
const cursorRule = (0,
|
|
1946
|
+
const cursorRule = (0, import_node_path7.join)(root, ".cursor", "rules", "odla.mdc");
|
|
1265
1947
|
plan(cursorRule, CURSOR_RULE);
|
|
1266
1948
|
rememberTarget("cursor", cursorRule);
|
|
1267
1949
|
}
|
|
1268
1950
|
if (harnesses.includes("agents")) {
|
|
1269
|
-
const agentsFile = (0,
|
|
1951
|
+
const agentsFile = (0, import_node_path7.join)(root, "AGENTS.md");
|
|
1270
1952
|
plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
1271
1953
|
rememberTarget("agents", agentsFile);
|
|
1272
1954
|
}
|
|
1273
1955
|
if (harnesses.includes("copilot")) {
|
|
1274
|
-
const copilotFile = (0,
|
|
1956
|
+
const copilotFile = (0, import_node_path7.join)(root, ".github", "copilot-instructions.md");
|
|
1275
1957
|
plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
1276
1958
|
rememberTarget("copilot", copilotFile);
|
|
1277
1959
|
}
|
|
1278
1960
|
if (harnesses.includes("gemini")) {
|
|
1279
|
-
const geminiFile = (0,
|
|
1961
|
+
const geminiFile = (0, import_node_path7.join)(root, "GEMINI.md");
|
|
1280
1962
|
plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
1281
1963
|
rememberTarget("gemini", geminiFile);
|
|
1282
1964
|
}
|
|
@@ -1290,11 +1972,11 @@ function installSkill(options = {}) {
|
|
|
1290
1972
|
conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
|
|
1291
1973
|
continue;
|
|
1292
1974
|
}
|
|
1293
|
-
if (!(0,
|
|
1975
|
+
if (!(0, import_node_fs8.existsSync)(file.target)) {
|
|
1294
1976
|
writtenPaths.add(file.target);
|
|
1295
1977
|
continue;
|
|
1296
1978
|
}
|
|
1297
|
-
const current = (0,
|
|
1979
|
+
const current = (0, import_node_fs8.readFileSync)(file.target, "utf8");
|
|
1298
1980
|
if (current === file.content) {
|
|
1299
1981
|
unchangedPaths.add(file.target);
|
|
1300
1982
|
} else if (file.managedMerge || options.force) {
|
|
@@ -1311,9 +1993,9 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
1311
1993
|
);
|
|
1312
1994
|
}
|
|
1313
1995
|
for (const file of plans.values()) {
|
|
1314
|
-
if (!(0,
|
|
1315
|
-
(0,
|
|
1316
|
-
(0,
|
|
1996
|
+
if (!(0, import_node_fs8.existsSync)(file.target) || (0, import_node_fs8.readFileSync)(file.target, "utf8") !== file.content) {
|
|
1997
|
+
(0, import_node_fs8.mkdirSync)((0, import_node_path7.dirname)(file.target), { recursive: true });
|
|
1998
|
+
(0, import_node_fs8.writeFileSync)(file.target, file.content);
|
|
1317
1999
|
}
|
|
1318
2000
|
}
|
|
1319
2001
|
const skills = skillNames(files);
|
|
@@ -1332,7 +2014,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
1332
2014
|
};
|
|
1333
2015
|
}
|
|
1334
2016
|
function pathsUnder(root, paths) {
|
|
1335
|
-
return [...paths].map((path) => (0,
|
|
2017
|
+
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
2018
|
}
|
|
1337
2019
|
function normalizeHarnesses(values, global) {
|
|
1338
2020
|
const requested = values?.length ? values : ["claude"];
|
|
@@ -1354,9 +2036,9 @@ function normalizeHarnesses(values, global) {
|
|
|
1354
2036
|
function managedFileContent(path, block, force, boundary) {
|
|
1355
2037
|
const symlink = symlinkedComponent(boundary, path);
|
|
1356
2038
|
if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
|
|
1357
|
-
if (!(0,
|
|
2039
|
+
if (!(0, import_node_fs8.existsSync)(path)) return `${block}
|
|
1358
2040
|
`;
|
|
1359
|
-
const current = (0,
|
|
2041
|
+
const current = (0, import_node_fs8.readFileSync)(path, "utf8");
|
|
1360
2042
|
const start = "<!-- odla-ai agent setup:start -->";
|
|
1361
2043
|
const end = "<!-- odla-ai agent setup:end -->";
|
|
1362
2044
|
const startAt = current.indexOf(start);
|
|
@@ -1377,15 +2059,15 @@ function managedFileContent(path, block, force, boundary) {
|
|
|
1377
2059
|
return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
|
|
1378
2060
|
}
|
|
1379
2061
|
function symlinkedComponent(boundary, target) {
|
|
1380
|
-
const rel = (0,
|
|
1381
|
-
if (rel === ".." || rel.startsWith(`..${
|
|
2062
|
+
const rel = (0, import_node_path7.relative)(boundary, target);
|
|
2063
|
+
if (rel === ".." || rel.startsWith(`..${import_node_path7.sep}`) || (0, import_node_path7.isAbsolute)(rel)) {
|
|
1382
2064
|
throw new Error(`agent setup target escapes its install root: ${target}`);
|
|
1383
2065
|
}
|
|
1384
2066
|
let current = boundary;
|
|
1385
|
-
for (const part of rel.split(
|
|
1386
|
-
current = (0,
|
|
2067
|
+
for (const part of rel.split(import_node_path7.sep).filter(Boolean)) {
|
|
2068
|
+
current = (0, import_node_path7.join)(current, part);
|
|
1387
2069
|
try {
|
|
1388
|
-
if ((0,
|
|
2070
|
+
if ((0, import_node_fs8.lstatSync)(current).isSymbolicLink()) return current;
|
|
1389
2071
|
} catch (error) {
|
|
1390
2072
|
if (error.code !== "ENOENT") throw error;
|
|
1391
2073
|
}
|
|
@@ -1396,13 +2078,13 @@ function skillNames(files) {
|
|
|
1396
2078
|
return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
|
|
1397
2079
|
}
|
|
1398
2080
|
function listFiles(dir) {
|
|
1399
|
-
if (!(0,
|
|
2081
|
+
if (!(0, import_node_fs8.existsSync)(dir)) return [];
|
|
1400
2082
|
const results = [];
|
|
1401
2083
|
const walk = (current) => {
|
|
1402
|
-
for (const entry of (0,
|
|
1403
|
-
const path = (0,
|
|
2084
|
+
for (const entry of (0, import_node_fs8.readdirSync)(current, { withFileTypes: true })) {
|
|
2085
|
+
const path = (0, import_node_path7.join)(current, entry.name);
|
|
1404
2086
|
if (entry.isDirectory()) walk(path);
|
|
1405
|
-
else results.push((0,
|
|
2087
|
+
else results.push((0, import_node_path7.relative)(dir, path));
|
|
1406
2088
|
}
|
|
1407
2089
|
};
|
|
1408
2090
|
walk(dir);
|
|
@@ -1527,6 +2209,119 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
1527
2209
|
printCapabilities(parsed.options.json === true);
|
|
1528
2210
|
return;
|
|
1529
2211
|
}
|
|
2212
|
+
if (command === "admin") {
|
|
2213
|
+
const area = parsed.positionals[1];
|
|
2214
|
+
const action = parsed.positionals[2];
|
|
2215
|
+
const credentialSet = action === "credential" && parsed.positionals[3] === "set";
|
|
2216
|
+
const credentials = action === "credentials";
|
|
2217
|
+
const models = action === "models";
|
|
2218
|
+
const usage = action === "usage";
|
|
2219
|
+
const audit = action === "audit";
|
|
2220
|
+
if (area !== "ai" || action !== "show" && action !== "set" && !credentialSet && !credentials && !models && !usage && !audit) {
|
|
2221
|
+
throw new Error('unknown admin command. Try "odla-ai admin ai show".');
|
|
2222
|
+
}
|
|
2223
|
+
assertArgs(parsed, [
|
|
2224
|
+
"platform",
|
|
2225
|
+
"json",
|
|
2226
|
+
"open",
|
|
2227
|
+
"provider",
|
|
2228
|
+
"model",
|
|
2229
|
+
"enabled",
|
|
2230
|
+
"max-input-bytes",
|
|
2231
|
+
"max-output-tokens",
|
|
2232
|
+
"max-calls-per-run",
|
|
2233
|
+
"from-env",
|
|
2234
|
+
"stdin",
|
|
2235
|
+
"discovery-provider",
|
|
2236
|
+
"discovery-model",
|
|
2237
|
+
"validation-provider",
|
|
2238
|
+
"validation-model",
|
|
2239
|
+
"app-id",
|
|
2240
|
+
"env",
|
|
2241
|
+
"run-id",
|
|
2242
|
+
"limit"
|
|
2243
|
+
], credentialSet ? 5 : action === "set" ? 4 : 3);
|
|
2244
|
+
await adminAi({
|
|
2245
|
+
action: credentialSet ? "credential-set" : credentials ? "credentials" : models ? "models" : usage ? "usage" : audit ? "audit" : action,
|
|
2246
|
+
purpose: action === "set" ? parsed.positionals[3] : void 0,
|
|
2247
|
+
provider: stringOpt(parsed.options.provider),
|
|
2248
|
+
model: stringOpt(parsed.options.model),
|
|
2249
|
+
enabled: boolOpt(parsed.options.enabled),
|
|
2250
|
+
maxInputBytes: numberOpt(parsed.options["max-input-bytes"], "--max-input-bytes"),
|
|
2251
|
+
maxOutputTokens: numberOpt(parsed.options["max-output-tokens"], "--max-output-tokens"),
|
|
2252
|
+
maxCallsPerRun: numberOpt(parsed.options["max-calls-per-run"], "--max-calls-per-run"),
|
|
2253
|
+
platform: stringOpt(parsed.options.platform),
|
|
2254
|
+
json: parsed.options.json === true,
|
|
2255
|
+
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
2256
|
+
credentialProvider: credentialSet ? parsed.positionals[4] : void 0,
|
|
2257
|
+
fromEnv: stringOpt(parsed.options["from-env"]),
|
|
2258
|
+
stdin: parsed.options.stdin === true,
|
|
2259
|
+
discoveryProvider: stringOpt(parsed.options["discovery-provider"]),
|
|
2260
|
+
discoveryModel: stringOpt(parsed.options["discovery-model"]),
|
|
2261
|
+
validationProvider: stringOpt(parsed.options["validation-provider"]),
|
|
2262
|
+
validationModel: stringOpt(parsed.options["validation-model"]),
|
|
2263
|
+
appId: stringOpt(parsed.options["app-id"]),
|
|
2264
|
+
env: stringOpt(parsed.options.env),
|
|
2265
|
+
runId: stringOpt(parsed.options["run-id"]),
|
|
2266
|
+
limit: numberOpt(parsed.options.limit, "--limit")
|
|
2267
|
+
});
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2270
|
+
if (command === "security") {
|
|
2271
|
+
const sub = parsed.positionals[1];
|
|
2272
|
+
if (sub !== "run") throw new Error('unknown security command. Try "odla-ai security run".');
|
|
2273
|
+
assertArgs(parsed, [
|
|
2274
|
+
"config",
|
|
2275
|
+
"env",
|
|
2276
|
+
"out",
|
|
2277
|
+
"profile",
|
|
2278
|
+
"max-hunt-tasks",
|
|
2279
|
+
"run-id",
|
|
2280
|
+
"platform",
|
|
2281
|
+
"self",
|
|
2282
|
+
"ack-redacted-source",
|
|
2283
|
+
"open",
|
|
2284
|
+
"fail-on",
|
|
2285
|
+
"fail-on-candidates",
|
|
2286
|
+
"allow-incomplete"
|
|
2287
|
+
], 3);
|
|
2288
|
+
const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
2289
|
+
const selfAudit = parsed.options.self === true;
|
|
2290
|
+
const open = parsed.options.open === false ? false : parsed.options.open === true ? true : void 0;
|
|
2291
|
+
const platform = stringOpt(parsed.options.platform);
|
|
2292
|
+
const result = await runHostedSecurity({
|
|
2293
|
+
configPath,
|
|
2294
|
+
selfAudit,
|
|
2295
|
+
target: parsed.positionals[2],
|
|
2296
|
+
out: stringOpt(parsed.options.out),
|
|
2297
|
+
env: stringOpt(parsed.options.env),
|
|
2298
|
+
profile: securityProfile(stringOpt(parsed.options.profile)),
|
|
2299
|
+
maxHuntTasks: numberOpt(parsed.options["max-hunt-tasks"], "--max-hunt-tasks"),
|
|
2300
|
+
runId: stringOpt(parsed.options["run-id"]),
|
|
2301
|
+
platform,
|
|
2302
|
+
sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
|
|
2303
|
+
getToken: async (request) => {
|
|
2304
|
+
if (request.scope === "platform:security:self") {
|
|
2305
|
+
return getScopedPlatformToken({ platform: request.platform, scope: request.scope, open });
|
|
2306
|
+
}
|
|
2307
|
+
const cfg = await loadProjectConfig(configPath);
|
|
2308
|
+
if (platformAudience(cfg.platformUrl) !== platformAudience(request.platform)) {
|
|
2309
|
+
throw new Error("--platform cannot reuse a project developer token from another platform; update odla.config.mjs and authenticate there");
|
|
2310
|
+
}
|
|
2311
|
+
return getDeveloperToken(cfg, { configPath, open }, fetch, console);
|
|
2312
|
+
}
|
|
2313
|
+
});
|
|
2314
|
+
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
2315
|
+
const candidateValue = parsed.options["fail-on-candidates"];
|
|
2316
|
+
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
2317
|
+
const confirmed = (0, import_security2.findingsAtOrAbove)(result.report, failOn);
|
|
2318
|
+
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(result.report, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
|
|
2319
|
+
const incomplete = result.report.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
|
|
2320
|
+
if (confirmed.length || leads.length || incomplete) {
|
|
2321
|
+
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
2322
|
+
}
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
1530
2325
|
if (command === "provision") {
|
|
1531
2326
|
assertArgs(
|
|
1532
2327
|
parsed,
|
|
@@ -1607,71 +2402,14 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
1607
2402
|
}
|
|
1608
2403
|
throw new Error(`unknown command "${command}". Run "odla-ai help".`);
|
|
1609
2404
|
}
|
|
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;
|
|
2405
|
+
function securityProfile(value) {
|
|
2406
|
+
if (value === void 0) return void 0;
|
|
2407
|
+
if (value === "odla" || value === "cloudflare-app" || value === "generic") return value;
|
|
2408
|
+
throw new Error("--profile must be odla, cloudflare-app, or generic");
|
|
1670
2409
|
}
|
|
1671
|
-
function
|
|
1672
|
-
if (
|
|
1673
|
-
|
|
1674
|
-
return values.flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
|
|
2410
|
+
function severityOpt(value, flag) {
|
|
2411
|
+
if (["informational", "low", "medium", "high", "critical"].includes(value)) return value;
|
|
2412
|
+
throw new Error(`${flag} must be informational, low, medium, high, or critical`);
|
|
1675
2413
|
}
|
|
1676
2414
|
function harnessList(parsed) {
|
|
1677
2415
|
const selected = [
|
|
@@ -1688,57 +2426,21 @@ function harnessOption(value, flag) {
|
|
|
1688
2426
|
if (parts.some((item) => !item.trim())) throw new Error(`${flag} requires a harness name`);
|
|
1689
2427
|
return parts.map((item) => item.trim());
|
|
1690
2428
|
}
|
|
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
2429
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1732
2430
|
0 && (module.exports = {
|
|
1733
2431
|
AGENT_HARNESSES,
|
|
1734
2432
|
CAPABILITIES,
|
|
2433
|
+
SYSTEM_AI_PURPOSES,
|
|
2434
|
+
adminAi,
|
|
1735
2435
|
doctor,
|
|
2436
|
+
getScopedPlatformToken,
|
|
1736
2437
|
initProject,
|
|
1737
2438
|
installSkill,
|
|
1738
2439
|
printCapabilities,
|
|
1739
2440
|
provision,
|
|
1740
2441
|
redactSecrets,
|
|
1741
2442
|
runCli,
|
|
2443
|
+
runHostedSecurity,
|
|
1742
2444
|
secretsPush,
|
|
1743
2445
|
smoke
|
|
1744
2446
|
});
|