@odla-ai/cli 0.3.0 → 0.5.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 +104 -30
- package/REQUIREMENTS.md +18 -3
- package/dist/bin.cjs +790 -273
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-ZU6QJXRQ.js → chunk-7WZIZCGA.js} +790 -270
- package/dist/chunk-7WZIZCGA.js.map +1 -0
- package/dist/index.cjs +796 -273
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +205 -21
- package/dist/index.d.ts +205 -21
- package/dist/index.js +7 -1
- package/llms.txt +311 -53
- package/package.json +14 -6
- package/skills/odla/SKILL.md +103 -0
- package/skills/odla/references/build.md +93 -0
- package/skills/odla/references/sdks.md +94 -0
- package/skills/odla-migrate/SKILL.md +38 -13
- package/skills/odla-migrate/references/phase-0-preflight.md +0 -2
- package/skills/odla-migrate/references/phase-1-static.md +2 -4
- package/skills/odla-migrate/references/phase-2-db.md +25 -17
- package/skills/odla-migrate/references/phase-3-auth.md +106 -20
- package/skills/odla-migrate/references/phase-3b-user-sync.md +57 -0
- package/skills/odla-migrate/references/phase-4-ai.md +6 -5
- package/skills/odla-migrate/references/phase-5-cutover.md +43 -16
- package/skills/odla-migrate/references/secrets-map.md +21 -11
- package/skills/odla-migrate/references/troubleshooting.md +27 -13
- package/skills/odla-o11y-debug/SKILL.md +93 -0
- package/dist/chunk-ZU6QJXRQ.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -31,9 +31,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
// src/index.ts
|
|
32
32
|
var index_exports = {};
|
|
33
33
|
__export(index_exports, {
|
|
34
|
+
AGENT_HARNESSES: () => AGENT_HARNESSES,
|
|
35
|
+
CAPABILITIES: () => CAPABILITIES,
|
|
34
36
|
doctor: () => doctor,
|
|
35
37
|
initProject: () => initProject,
|
|
36
38
|
installSkill: () => installSkill,
|
|
39
|
+
printCapabilities: () => printCapabilities,
|
|
37
40
|
provision: () => provision,
|
|
38
41
|
redactSecrets: () => redactSecrets,
|
|
39
42
|
runCli: () => runCli,
|
|
@@ -49,12 +52,54 @@ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
|
49
52
|
// src/cli.ts
|
|
50
53
|
var import_node_fs7 = require("fs");
|
|
51
54
|
|
|
55
|
+
// src/capabilities.ts
|
|
56
|
+
var CAPABILITIES = {
|
|
57
|
+
cli: [
|
|
58
|
+
"register the app and enable configured services per environment",
|
|
59
|
+
"issue, persist, and explicitly rotate configured service credentials",
|
|
60
|
+
"write local Worker values and push deployed Worker secrets through Wrangler stdin",
|
|
61
|
+
"push db schema/rules and configure platform AI, auth, and deployment links",
|
|
62
|
+
"validate config offline and smoke-test a provisioned db environment"
|
|
63
|
+
],
|
|
64
|
+
agent: [
|
|
65
|
+
"install and import the selected odla SDKs",
|
|
66
|
+
"wrap the Worker with withObservability and choose useful telemetry",
|
|
67
|
+
"make application-specific schema, rules, auth, UI, and migration decisions"
|
|
68
|
+
],
|
|
69
|
+
human: [
|
|
70
|
+
"approve the odla device code and one-off third-party logins",
|
|
71
|
+
"consent to production changes with --yes",
|
|
72
|
+
"request destructive credential rotation explicitly",
|
|
73
|
+
"review application semantics, security findings, releases, and merges"
|
|
74
|
+
],
|
|
75
|
+
studio: [
|
|
76
|
+
"view telemetry and environment state",
|
|
77
|
+
"perform manual credential recovery when the CLI's local shown-once copy is unavailable"
|
|
78
|
+
]
|
|
79
|
+
};
|
|
80
|
+
function printCapabilities(json = false, out = console) {
|
|
81
|
+
if (json) {
|
|
82
|
+
out.log(JSON.stringify(CAPABILITIES, null, 2));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
out.log("odla-ai responsibility boundary\n");
|
|
86
|
+
printGroup(out, "CLI automates", CAPABILITIES.cli);
|
|
87
|
+
printGroup(out, "Coding agent changes source", CAPABILITIES.agent);
|
|
88
|
+
printGroup(out, "Human checkpoints", CAPABILITIES.human);
|
|
89
|
+
printGroup(out, "Studio", CAPABILITIES.studio);
|
|
90
|
+
}
|
|
91
|
+
function printGroup(out, heading, items) {
|
|
92
|
+
out.log(`${heading}:`);
|
|
93
|
+
for (const item of items) out.log(` - ${item}`);
|
|
94
|
+
out.log("");
|
|
95
|
+
}
|
|
96
|
+
|
|
52
97
|
// src/config.ts
|
|
53
98
|
var import_node_fs = require("fs");
|
|
54
99
|
var import_node_path = require("path");
|
|
55
100
|
var import_node_url = require("url");
|
|
56
101
|
var DEFAULT_PLATFORM = "https://odla.ai";
|
|
57
|
-
var DEFAULT_ENVS = ["
|
|
102
|
+
var DEFAULT_ENVS = ["dev"];
|
|
58
103
|
var DEFAULT_SERVICES = ["db", "ai"];
|
|
59
104
|
async function loadProjectConfig(configPath = "odla.config.mjs") {
|
|
60
105
|
const resolved = (0, import_node_path.resolve)(configPath);
|
|
@@ -156,8 +201,8 @@ function unique(values) {
|
|
|
156
201
|
|
|
157
202
|
// src/doctor-checks.ts
|
|
158
203
|
var import_node_child_process2 = require("child_process");
|
|
159
|
-
var
|
|
160
|
-
var
|
|
204
|
+
var import_node_fs4 = require("fs");
|
|
205
|
+
var import_node_path4 = require("path");
|
|
161
206
|
|
|
162
207
|
// src/redact.ts
|
|
163
208
|
var REPLACEMENTS = [
|
|
@@ -178,10 +223,136 @@ function looksSecret(value) {
|
|
|
178
223
|
return redactSecrets(value) !== value || value.includes("-----BEGIN");
|
|
179
224
|
}
|
|
180
225
|
|
|
181
|
-
// src/
|
|
182
|
-
var import_node_child_process = require("child_process");
|
|
226
|
+
// src/local.ts
|
|
183
227
|
var import_node_fs2 = require("fs");
|
|
184
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);
|
|
341
|
+
}
|
|
342
|
+
function gitignoreEntry(rootDir, path) {
|
|
343
|
+
const rel = (0, import_node_path2.relative)((0, import_node_path2.resolve)(rootDir), (0, import_node_path2.resolve)(path));
|
|
344
|
+
if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path2.isAbsolute)(rel)) return null;
|
|
345
|
+
return rel.replaceAll("\\", "/");
|
|
346
|
+
}
|
|
347
|
+
function displayPath(path, rootDir = process.cwd()) {
|
|
348
|
+
const rel = (0, import_node_path2.relative)(rootDir, path);
|
|
349
|
+
return rel && !rel.startsWith("..") ? rel : path;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// src/wrangler.ts
|
|
353
|
+
var import_node_child_process = require("child_process");
|
|
354
|
+
var import_node_fs3 = require("fs");
|
|
355
|
+
var import_node_path3 = require("path");
|
|
185
356
|
var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
|
|
186
357
|
const child = (0, import_node_child_process.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
187
358
|
let stdout = "";
|
|
@@ -195,15 +366,15 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) =>
|
|
|
195
366
|
var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
|
|
196
367
|
function findWranglerConfig(rootDir) {
|
|
197
368
|
for (const name of WRANGLER_CONFIG_FILES) {
|
|
198
|
-
const path = (0,
|
|
199
|
-
if ((0,
|
|
369
|
+
const path = (0, import_node_path3.join)(rootDir, name);
|
|
370
|
+
if ((0, import_node_fs3.existsSync)(path)) return path;
|
|
200
371
|
}
|
|
201
372
|
return null;
|
|
202
373
|
}
|
|
203
374
|
function readWranglerConfig(path) {
|
|
204
375
|
if (path.endsWith(".toml")) return null;
|
|
205
376
|
try {
|
|
206
|
-
return JSON.parse(stripJsonComments((0,
|
|
377
|
+
return JSON.parse(stripJsonComments((0, import_node_fs3.readFileSync)(path, "utf8")));
|
|
207
378
|
} catch {
|
|
208
379
|
return null;
|
|
209
380
|
}
|
|
@@ -275,10 +446,11 @@ function lintRules(rules, entities, publicRead) {
|
|
|
275
446
|
return warnings;
|
|
276
447
|
}
|
|
277
448
|
var defaultExec = (cmd, args, cwd) => (0, import_node_child_process2.execFileSync)(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
278
|
-
function trackedSecretFiles(rootDir, exec = defaultExec) {
|
|
449
|
+
function trackedSecretFiles(rootDir, exec = defaultExec, localPaths = []) {
|
|
279
450
|
let output;
|
|
280
451
|
try {
|
|
281
|
-
|
|
452
|
+
const custom = localPaths.map((path) => gitignoreEntry(rootDir, path)).filter((path) => !!path);
|
|
453
|
+
output = exec("git", ["ls-files", "--", ".dev.vars", ".odla", ...custom], rootDir);
|
|
282
454
|
} catch {
|
|
283
455
|
return [];
|
|
284
456
|
}
|
|
@@ -300,10 +472,10 @@ function wranglerWarnings(rootDir) {
|
|
|
300
472
|
for (const { label, block } of blocks) {
|
|
301
473
|
const assets = block.assets;
|
|
302
474
|
if (assets?.directory) {
|
|
303
|
-
const dir = (0,
|
|
304
|
-
if (dir === (0,
|
|
475
|
+
const dir = (0, import_node_path4.resolve)(rootDir, assets.directory);
|
|
476
|
+
if (dir === (0, import_node_path4.resolve)(rootDir)) {
|
|
305
477
|
warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
|
|
306
|
-
} else if ((0,
|
|
478
|
+
} else if ((0, import_node_fs4.existsSync)((0, import_node_path4.join)(dir, "node_modules"))) {
|
|
307
479
|
warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
|
|
308
480
|
}
|
|
309
481
|
}
|
|
@@ -322,96 +494,57 @@ function wranglerWarnings(rootDir) {
|
|
|
322
494
|
}
|
|
323
495
|
return warnings;
|
|
324
496
|
}
|
|
325
|
-
function
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
497
|
+
function o11yProjectWarnings(rootDir) {
|
|
498
|
+
const warnings = [];
|
|
499
|
+
const pkg = readPackageJson(rootDir);
|
|
500
|
+
const dependencies = pkg?.dependencies;
|
|
501
|
+
const devDependencies = pkg?.devDependencies;
|
|
502
|
+
if (!dependencies?.["@odla-ai/o11y"]) {
|
|
503
|
+
warnings.push(
|
|
504
|
+
devDependencies?.["@odla-ai/o11y"] ? "@odla-ai/o11y is a devDependency \u2014 move it to dependencies so the Worker can import it" : 'Worker instrumentation is missing @odla-ai/o11y \u2014 install it and wrap the handler with "withObservability"'
|
|
505
|
+
);
|
|
330
506
|
}
|
|
507
|
+
const configPath = findWranglerConfig(rootDir);
|
|
508
|
+
const config = configPath ? readWranglerConfig(configPath) : null;
|
|
509
|
+
if (!configPath || !config) {
|
|
510
|
+
warnings.push("cannot verify o11y Worker instrumentation \u2014 add a parseable wrangler.jsonc/json config");
|
|
511
|
+
return warnings;
|
|
512
|
+
}
|
|
513
|
+
const main = typeof config.main === "string" ? (0, import_node_path4.resolve)(rootDir, config.main) : null;
|
|
514
|
+
if (!main || !(0, import_node_fs4.existsSync)(main)) {
|
|
515
|
+
warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
|
|
516
|
+
} else {
|
|
517
|
+
let source = "";
|
|
518
|
+
try {
|
|
519
|
+
source = (0, import_node_fs4.readFileSync)(main, "utf8");
|
|
520
|
+
} catch {
|
|
521
|
+
}
|
|
522
|
+
if (!/\bwithObservability\b/.test(source)) {
|
|
523
|
+
warnings.push(`wrangler entrypoint ${config.main} does not reference withObservability`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const flags = Array.isArray(config.compatibility_flags) ? config.compatibility_flags : [];
|
|
527
|
+
if (!flags.includes("nodejs_compat")) {
|
|
528
|
+
warnings.push('wrangler compatibility_flags is missing "nodejs_compat" required by @odla-ai/o11y');
|
|
529
|
+
}
|
|
530
|
+
return warnings;
|
|
331
531
|
}
|
|
332
|
-
|
|
333
|
-
// src/local.ts
|
|
334
|
-
var import_node_fs4 = require("fs");
|
|
335
|
-
var import_node_path4 = require("path");
|
|
336
|
-
var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
|
|
337
|
-
function readJsonFile(path) {
|
|
532
|
+
function readPackageJson(rootDir) {
|
|
338
533
|
try {
|
|
339
|
-
return JSON.parse((0, import_node_fs4.readFileSync)(
|
|
534
|
+
return JSON.parse((0, import_node_fs4.readFileSync)((0, import_node_path4.join)(rootDir, "package.json"), "utf8"));
|
|
340
535
|
} catch {
|
|
341
536
|
return null;
|
|
342
537
|
}
|
|
343
538
|
}
|
|
344
|
-
function writePrivateJson(path, value) {
|
|
345
|
-
(0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(path), { recursive: true });
|
|
346
|
-
(0, import_node_fs4.writeFileSync)(path, `${JSON.stringify(value, null, 2)}
|
|
347
|
-
`);
|
|
348
|
-
(0, import_node_fs4.chmodSync)(path, 384);
|
|
349
|
-
}
|
|
350
|
-
function readCredentials(path) {
|
|
351
|
-
return readJsonFile(path);
|
|
352
|
-
}
|
|
353
|
-
function mergeCredential(current, update) {
|
|
354
|
-
const next = current ?? {
|
|
355
|
-
appId: update.appId,
|
|
356
|
-
platformUrl: update.platformUrl,
|
|
357
|
-
dbEndpoint: update.dbEndpoint,
|
|
358
|
-
updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
359
|
-
envs: {}
|
|
360
|
-
};
|
|
361
|
-
next.appId = update.appId;
|
|
362
|
-
next.platformUrl = update.platformUrl;
|
|
363
|
-
next.dbEndpoint = update.dbEndpoint;
|
|
364
|
-
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
365
|
-
next.envs[update.env] = {
|
|
366
|
-
...next.envs[update.env] ?? {},
|
|
367
|
-
tenantId: update.tenantId,
|
|
368
|
-
...update.dbKey ? { dbKey: update.dbKey } : {},
|
|
369
|
-
...update.o11yToken ? { o11yToken: update.o11yToken } : {}
|
|
370
|
-
};
|
|
371
|
-
return next;
|
|
372
|
-
}
|
|
373
|
-
function ensureGitignore(rootDir) {
|
|
374
|
-
const path = (0, import_node_path4.resolve)(rootDir, ".gitignore");
|
|
375
|
-
const existing = (0, import_node_fs4.existsSync)(path) ? (0, import_node_fs4.readFileSync)(path, "utf8") : "";
|
|
376
|
-
const missing = GITIGNORE_LINES.filter((line) => !existing.split(/\r?\n/).includes(line));
|
|
377
|
-
if (missing.length === 0) return;
|
|
378
|
-
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
379
|
-
(0, import_node_fs4.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
|
|
380
|
-
`);
|
|
381
|
-
}
|
|
382
|
-
function writeDevVars(path, credentials, env, o11y) {
|
|
383
|
-
const entry = credentials.envs[env];
|
|
384
|
-
if (!entry?.dbKey) throw new Error(`no db key for env "${env}" in ${path}`);
|
|
385
|
-
const lines = [
|
|
386
|
-
`ODLA_PLATFORM="${credentials.platformUrl}"`,
|
|
387
|
-
`ODLA_ENDPOINT="${credentials.dbEndpoint}"`,
|
|
388
|
-
`ODLA_APP_ID="${credentials.appId}"`,
|
|
389
|
-
`ODLA_ENV="${env}"`,
|
|
390
|
-
`ODLA_TENANT="${entry.tenantId}"`,
|
|
391
|
-
`ODLA_API_KEY="${entry.dbKey}"`
|
|
392
|
-
];
|
|
393
|
-
if (o11y) {
|
|
394
|
-
lines.push(`ODLA_O11Y_ENDPOINT="${o11y.endpoint}"`, `ODLA_O11Y_SERVICE="${o11y.service}"`);
|
|
395
|
-
if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
|
|
396
|
-
if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
|
|
397
|
-
}
|
|
398
|
-
(0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(path), { recursive: true });
|
|
399
|
-
(0, import_node_fs4.writeFileSync)(path, `${lines.join("\n")}
|
|
400
|
-
`);
|
|
401
|
-
(0, import_node_fs4.chmodSync)(path, 384);
|
|
402
|
-
}
|
|
403
|
-
function displayPath(path, rootDir = process.cwd()) {
|
|
404
|
-
const rel = (0, import_node_path4.relative)(rootDir, path);
|
|
405
|
-
return rel && !rel.startsWith("..") ? rel : path;
|
|
406
|
-
}
|
|
407
539
|
|
|
408
540
|
// src/doctor.ts
|
|
409
541
|
async function doctor(options) {
|
|
410
542
|
const out = options.stdout ?? console;
|
|
411
543
|
const cfg = await loadProjectConfig(options.configPath);
|
|
412
544
|
const plan = buildPlan(cfg);
|
|
413
|
-
const
|
|
414
|
-
const
|
|
545
|
+
const hasDb = cfg.services.includes("db");
|
|
546
|
+
const schema = hasDb ? await resolveDataExport(cfg, cfg.db?.schema, ["schema", "SCHEMA"]) : void 0;
|
|
547
|
+
const configuredRules = hasDb ? await resolveDataExport(cfg, cfg.db?.rules, ["rules", "RULES"]) : void 0;
|
|
415
548
|
const rules = configuredRules ?? (schema && cfg.db?.defaultRules !== false ? rulesFromSchema(schema) : void 0);
|
|
416
549
|
const entities = serializedEntities(schema);
|
|
417
550
|
out.log(`config: ${cfg.configPath}`);
|
|
@@ -420,7 +553,7 @@ async function doctor(options) {
|
|
|
420
553
|
out.log(`svc: ${plan.services.join(", ")}`);
|
|
421
554
|
out.log(`schema: ${schema ? `${entities.length} entities` : "none"}`);
|
|
422
555
|
out.log(`rules: ${rules ? `${Object.keys(rules).length} namespaces` : "none"}`);
|
|
423
|
-
out.log(`ai: ${cfg.ai?.provider ?? "not configured"}`);
|
|
556
|
+
out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "not configured" : "not enabled"}`);
|
|
424
557
|
const warnings = [];
|
|
425
558
|
if (schema && entities.length === 0) warnings.push("schema has no entities");
|
|
426
559
|
if (rules) {
|
|
@@ -436,7 +569,9 @@ async function doctor(options) {
|
|
|
436
569
|
}
|
|
437
570
|
}
|
|
438
571
|
}
|
|
439
|
-
if (cfg.ai?.keyEnv && !process.env[cfg.ai.keyEnv])
|
|
572
|
+
if (cfg.services.includes("ai") && cfg.ai?.keyEnv && !process.env[cfg.ai.keyEnv]) {
|
|
573
|
+
warnings.push(`${cfg.ai.keyEnv} is not set; provision will skip provider key storage`);
|
|
574
|
+
}
|
|
440
575
|
if (cfg.services.includes("o11y")) {
|
|
441
576
|
const credentials = readCredentials(cfg.local.credentialsFile);
|
|
442
577
|
for (const env of cfg.envs) {
|
|
@@ -444,9 +579,14 @@ async function doctor(options) {
|
|
|
444
579
|
warnings.push(`o11y is enabled but env "${env}" has no ingest token \u2014 run "odla-ai provision" to mint one`);
|
|
445
580
|
}
|
|
446
581
|
}
|
|
582
|
+
warnings.push(...o11yProjectWarnings(cfg.rootDir));
|
|
447
583
|
}
|
|
448
584
|
warnings.push(...lintRules(rules, entities, cfg.db?.publicRead ?? []));
|
|
449
|
-
warnings.push(...trackedSecretFiles(cfg.rootDir
|
|
585
|
+
warnings.push(...trackedSecretFiles(cfg.rootDir, void 0, [
|
|
586
|
+
cfg.local.tokenFile,
|
|
587
|
+
cfg.local.credentialsFile,
|
|
588
|
+
cfg.local.devVarsFile
|
|
589
|
+
]));
|
|
450
590
|
warnings.push(...wranglerWarnings(cfg.rootDir));
|
|
451
591
|
if (warnings.length) {
|
|
452
592
|
out.log("");
|
|
@@ -470,7 +610,7 @@ function initProject(options) {
|
|
|
470
610
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
|
|
471
611
|
throw new Error("--app-id must be lowercase letters, numbers, and hyphens");
|
|
472
612
|
}
|
|
473
|
-
const envs = options.envs?.length ? options.envs : ["
|
|
613
|
+
const envs = options.envs?.length ? options.envs : ["dev"];
|
|
474
614
|
const services = options.services?.length ? options.services : ["db", "ai"];
|
|
475
615
|
const aiProvider = options.aiProvider ?? "anthropic";
|
|
476
616
|
(0, import_node_fs5.mkdirSync)((0, import_node_path5.dirname)(configPath), { recursive: true });
|
|
@@ -573,9 +713,8 @@ function relativeDisplay(path, rootDir) {
|
|
|
573
713
|
}
|
|
574
714
|
|
|
575
715
|
// src/provision.ts
|
|
576
|
-
var
|
|
716
|
+
var import_apps2 = require("@odla-ai/apps");
|
|
577
717
|
var import_ai = require("@odla-ai/ai");
|
|
578
|
-
var import_node_path6 = require("path");
|
|
579
718
|
var import_node_process3 = __toESM(require("process"), 1);
|
|
580
719
|
|
|
581
720
|
// src/token.ts
|
|
@@ -588,7 +727,7 @@ var import_node_process = __toESM(require("process"), 1);
|
|
|
588
727
|
async function openUrl(url, options = {}) {
|
|
589
728
|
const command = openerFor(options.platform ?? import_node_process.default.platform);
|
|
590
729
|
const doSpawn = options.spawnImpl ?? import_node_child_process3.spawn;
|
|
591
|
-
await new Promise((
|
|
730
|
+
await new Promise((resolve6, reject) => {
|
|
592
731
|
const child = doSpawn(command.cmd, [...command.args, url], {
|
|
593
732
|
stdio: "ignore",
|
|
594
733
|
detached: true
|
|
@@ -596,7 +735,7 @@ async function openUrl(url, options = {}) {
|
|
|
596
735
|
child.once("error", reject);
|
|
597
736
|
child.once("spawn", () => {
|
|
598
737
|
child.unref();
|
|
599
|
-
|
|
738
|
+
resolve6();
|
|
600
739
|
});
|
|
601
740
|
});
|
|
602
741
|
}
|
|
@@ -650,35 +789,238 @@ function approvalBrowser(options) {
|
|
|
650
789
|
return { open: true, mode: "auto" };
|
|
651
790
|
}
|
|
652
791
|
function handshakeUrl(platformUrl, userCode) {
|
|
653
|
-
const url = new URL("/
|
|
792
|
+
const url = new URL("/studio", platformUrl);
|
|
654
793
|
url.searchParams.set("code", userCode);
|
|
655
794
|
return url.toString();
|
|
656
795
|
}
|
|
657
796
|
|
|
797
|
+
// src/provision-credentials.ts
|
|
798
|
+
var import_apps = require("@odla-ai/apps");
|
|
799
|
+
async function provisionEnvCredentials(opts) {
|
|
800
|
+
const tenantId = (0, import_apps.tenantIdFor)(opts.cfg.app.id, opts.env);
|
|
801
|
+
const prior = opts.credentials?.envs[opts.env];
|
|
802
|
+
let credentials = opts.credentials;
|
|
803
|
+
let dbKey = opts.cfg.services.includes("db") && !opts.rotateDb ? prior?.dbKey : void 0;
|
|
804
|
+
let o11yToken = opts.cfg.services.includes("o11y") && !opts.rotateO11y ? prior?.o11yToken : void 0;
|
|
805
|
+
if (opts.cfg.services.includes("db")) {
|
|
806
|
+
if (dbKey) {
|
|
807
|
+
opts.stdout.log(`${opts.env}: reusing local db key for ${tenantId}`);
|
|
808
|
+
} else {
|
|
809
|
+
dbKey = await mintDbKey(opts, tenantId);
|
|
810
|
+
credentials = save(opts, credentials, tenantId, { dbKey });
|
|
811
|
+
opts.stdout.log(`${opts.env}: minted db key for ${tenantId}`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (opts.cfg.services.includes("o11y")) {
|
|
815
|
+
if (o11yToken) {
|
|
816
|
+
opts.stdout.log(`${opts.env}: reusing local o11y ingest token`);
|
|
817
|
+
} else {
|
|
818
|
+
o11yToken = await issueO11yToken(opts);
|
|
819
|
+
credentials = save(opts, credentials, tenantId, { ...dbKey ? { dbKey } : {}, o11yToken });
|
|
820
|
+
opts.stdout.log(`${opts.env}: ${opts.rotateO11y ? "rotated" : "issued"} o11y ingest token`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return save(opts, credentials, tenantId, {
|
|
824
|
+
...dbKey ? { dbKey } : {},
|
|
825
|
+
...o11yToken ? { o11yToken } : {}
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
function save(opts, current, tenantId, values) {
|
|
829
|
+
const next = mergeCredential(current, {
|
|
830
|
+
appId: opts.cfg.app.id,
|
|
831
|
+
platformUrl: opts.cfg.platformUrl,
|
|
832
|
+
dbEndpoint: opts.cfg.dbEndpoint,
|
|
833
|
+
env: opts.env,
|
|
834
|
+
tenantId,
|
|
835
|
+
...values
|
|
836
|
+
});
|
|
837
|
+
if (opts.write) writePrivateJson(opts.cfg.local.credentialsFile, next);
|
|
838
|
+
return next;
|
|
839
|
+
}
|
|
840
|
+
async function mintDbKey(opts, tenantId) {
|
|
841
|
+
const headers = { authorization: `Bearer ${opts.developerToken}`, "content-type": "application/json" };
|
|
842
|
+
let res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/keys`, {
|
|
843
|
+
method: "POST",
|
|
844
|
+
headers,
|
|
845
|
+
body: "{}"
|
|
846
|
+
});
|
|
847
|
+
if (res.status === 404) {
|
|
848
|
+
const created = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps`, {
|
|
849
|
+
method: "POST",
|
|
850
|
+
headers,
|
|
851
|
+
body: JSON.stringify({
|
|
852
|
+
name: opts.env === "prod" ? opts.cfg.app.name : `${opts.cfg.app.name} (${opts.env})`,
|
|
853
|
+
appId: tenantId
|
|
854
|
+
})
|
|
855
|
+
});
|
|
856
|
+
if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText(created)}`);
|
|
857
|
+
res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/keys`, {
|
|
858
|
+
method: "POST",
|
|
859
|
+
headers,
|
|
860
|
+
body: "{}"
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText(res)}`);
|
|
864
|
+
const body = await res.json();
|
|
865
|
+
if (!body.key) throw new Error(`db key mint (${tenantId}) returned no key`);
|
|
866
|
+
return body.key;
|
|
867
|
+
}
|
|
868
|
+
async function issueO11yToken(opts) {
|
|
869
|
+
const suffix = opts.rotateO11y ? "/rotate" : "";
|
|
870
|
+
const res = await opts.fetch(
|
|
871
|
+
`${opts.cfg.platformUrl}/o11y/${encodeURIComponent(opts.cfg.app.id)}/token${suffix}?env=${encodeURIComponent(opts.env)}`,
|
|
872
|
+
{ method: "POST", headers: { authorization: `Bearer ${opts.developerToken}` } }
|
|
873
|
+
);
|
|
874
|
+
if (res.status === 409 && !opts.rotateO11y) {
|
|
875
|
+
throw new Error(
|
|
876
|
+
`o11y token already exists for env "${opts.env}", but its shown-once value is not in the local credentials file; run "odla-ai provision --rotate-o11y-token --push-secrets" to replace it explicitly`
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText(res)}`);
|
|
880
|
+
const body = await res.json();
|
|
881
|
+
if (!body.token) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) returned no token`);
|
|
882
|
+
return body.token;
|
|
883
|
+
}
|
|
884
|
+
async function safeText(res) {
|
|
885
|
+
try {
|
|
886
|
+
return redactSecrets((await res.text()).slice(0, 500));
|
|
887
|
+
} catch {
|
|
888
|
+
return "";
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// src/secrets.ts
|
|
893
|
+
var PROD_ENV_NAMES = /* @__PURE__ */ new Set(["prod", "production"]);
|
|
894
|
+
async function secretsPush(options) {
|
|
895
|
+
await secretsPushImpl(options, true);
|
|
896
|
+
}
|
|
897
|
+
async function secretsPushAfterPreflight(options) {
|
|
898
|
+
await secretsPushImpl(options, false);
|
|
899
|
+
}
|
|
900
|
+
async function secretsPushImpl(options, preflight) {
|
|
901
|
+
const out = options.stdout ?? console;
|
|
902
|
+
const cfg = await loadProjectConfig(options.configPath);
|
|
903
|
+
const env = options.env;
|
|
904
|
+
if (!cfg.envs.includes(env)) {
|
|
905
|
+
throw new Error(`env "${env}" is not in config envs (${cfg.envs.join(", ")})`);
|
|
906
|
+
}
|
|
907
|
+
if (PROD_ENV_NAMES.has(env) && !options.yes) {
|
|
908
|
+
throw new Error(`refusing to push a secret to "${env}" without --yes`);
|
|
909
|
+
}
|
|
910
|
+
const credentialsPath = displayPath(cfg.local.credentialsFile, cfg.rootDir);
|
|
911
|
+
const credentials = readCredentials(cfg.local.credentialsFile);
|
|
912
|
+
if (!credentials) throw new Error(`no credentials at ${credentialsPath} \u2014 run "odla-ai provision" first`);
|
|
913
|
+
if (credentials.appId !== cfg.app.id) {
|
|
914
|
+
throw new Error(`credentials at ${credentialsPath} are for "${credentials.appId}", not "${cfg.app.id}"`);
|
|
915
|
+
}
|
|
916
|
+
const entry = credentials.envs[env];
|
|
917
|
+
if (!entry) throw new Error(`no credentials for env "${env}" in ${credentialsPath} \u2014 run "odla-ai provision" first`);
|
|
918
|
+
const secrets = [];
|
|
919
|
+
if (cfg.services.includes("o11y")) {
|
|
920
|
+
if (!entry.o11yToken) throw new Error(`no o11y token for env "${env}" in ${credentialsPath} \u2014 run "odla-ai provision" first`);
|
|
921
|
+
secrets.push({ name: "ODLA_O11Y_TOKEN", value: entry.o11yToken });
|
|
922
|
+
}
|
|
923
|
+
if (cfg.services.includes("db")) {
|
|
924
|
+
if (!entry.dbKey) throw new Error(`no db key for env "${env}" in ${credentialsPath} \u2014 run "odla-ai provision" first`);
|
|
925
|
+
secrets.push({ name: "ODLA_API_KEY", value: entry.dbKey });
|
|
926
|
+
}
|
|
927
|
+
if (secrets.length === 0) {
|
|
928
|
+
out.log(`${env}: no configured Worker secrets to push`);
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
const wranglerEnv = PROD_ENV_NAMES.has(env) ? void 0 : env;
|
|
932
|
+
const target = wranglerEnv ? `wrangler env "${wranglerEnv}"` : "the top-level (prod) wrangler env";
|
|
933
|
+
if (options.dryRun) {
|
|
934
|
+
assertWranglerConfig(cfg);
|
|
935
|
+
for (const s of secrets) out.log(`dry run: would push ${s.name} (${redactSecrets(s.value)}) to ${target}`);
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const run = options.runner ?? defaultRunner;
|
|
939
|
+
if (preflight) await preflightLoadedConfig(cfg, run);
|
|
940
|
+
for (const s of secrets) {
|
|
941
|
+
const result = await wranglerPutSecret(run, { name: s.name, value: s.value, env: wranglerEnv, cwd: cfg.rootDir });
|
|
942
|
+
if (result.code !== 0) {
|
|
943
|
+
throw new Error(`wrangler secret put ${s.name} failed (exit ${result.code}): ${redactSecrets(`${result.stderr || result.stdout}`.trim())}`);
|
|
944
|
+
}
|
|
945
|
+
out.log(`${s.name} pushed to ${target} (value read from ${credentialsPath}, never echoed)`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
async function preflightSecretsPush(options) {
|
|
949
|
+
const cfg = await loadProjectConfig(options.configPath);
|
|
950
|
+
if (!cfg.services.some((service) => service === "db" || service === "o11y")) return;
|
|
951
|
+
await preflightLoadedConfig(cfg, options.runner ?? defaultRunner);
|
|
952
|
+
}
|
|
953
|
+
async function preflightLoadedConfig(cfg, run) {
|
|
954
|
+
assertWranglerConfig(cfg);
|
|
955
|
+
if (!await wranglerLoggedIn(run, cfg.rootDir)) {
|
|
956
|
+
throw new Error(`wrangler is not logged in \u2014 run "wrangler login" (a browser step for the human)`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
function assertWranglerConfig(cfg) {
|
|
960
|
+
if (!findWranglerConfig(cfg.rootDir)) {
|
|
961
|
+
throw new Error(`no wrangler config found in ${cfg.rootDir} (wrangler.jsonc, wrangler.json, or wrangler.toml)`);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
658
965
|
// src/provision.ts
|
|
659
966
|
async function provision(options) {
|
|
660
967
|
const out = options.stdout ?? console;
|
|
661
968
|
const cfg = await loadProjectConfig(options.configPath);
|
|
662
969
|
const plan = buildPlan(cfg);
|
|
970
|
+
const hasDb = cfg.services.includes("db");
|
|
971
|
+
const hasO11y = cfg.services.includes("o11y");
|
|
972
|
+
if (options.rotateO11yToken && !hasO11y) {
|
|
973
|
+
throw new Error("--rotate-o11y-token requires the o11y service in odla.config.mjs");
|
|
974
|
+
}
|
|
975
|
+
if (options.pushSecrets && options.writeCredentials === false) {
|
|
976
|
+
throw new Error("--push-secrets cannot be combined with --no-write-credentials");
|
|
977
|
+
}
|
|
663
978
|
out.log(`odla-ai: ${plan.appName} (${plan.appId})`);
|
|
664
979
|
out.log(` platform: ${plan.platformUrl}`);
|
|
665
980
|
out.log(` db: ${plan.dbEndpoint}`);
|
|
666
981
|
out.log(` envs: ${plan.envs.join(", ")}`);
|
|
667
982
|
out.log(` services: ${plan.services.join(", ")}`);
|
|
668
|
-
const
|
|
669
|
-
|
|
983
|
+
const productionEnvs = plan.envs.filter((env) => env === "prod" || env === "production");
|
|
984
|
+
if (!options.dryRun && productionEnvs.length > 0 && !options.yes) {
|
|
985
|
+
throw new Error(
|
|
986
|
+
`refusing to provision production env ${productionEnvs.join(", ")} without --yes; run --dry-run first`
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
const schema = hasDb ? await resolveDataExport(cfg, cfg.db?.schema, ["schema", "SCHEMA"]) : void 0;
|
|
990
|
+
const configuredRules = hasDb ? await resolveDataExport(cfg, cfg.db?.rules, ["rules", "RULES"]) : void 0;
|
|
670
991
|
const rules = configuredRules ?? (schema && cfg.db?.defaultRules !== false ? rulesFromSchema(schema) : void 0);
|
|
671
992
|
if (options.dryRun) {
|
|
672
993
|
out.log("dry run: no network calls or file writes");
|
|
673
994
|
out.log(` schema: ${schema ? "yes" : "no"}`);
|
|
674
995
|
out.log(` rules: ${rules ? Object.keys(rules).length : 0} namespaces`);
|
|
675
|
-
out.log(` ai: ${cfg.ai?.provider ?? "not configured"}`);
|
|
996
|
+
out.log(` ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "not configured" : "not enabled"}`);
|
|
997
|
+
out.log(` secrets: ${options.pushSecrets ? "push configured Worker secrets" : "local only"}`);
|
|
676
998
|
return;
|
|
677
999
|
}
|
|
1000
|
+
let credentials = readCredentials(cfg.local.credentialsFile);
|
|
1001
|
+
if (credentials && credentials.appId !== cfg.app.id) {
|
|
1002
|
+
throw new Error(
|
|
1003
|
+
`credentials at ${displayPath(cfg.local.credentialsFile, cfg.rootDir)} are for "${credentials.appId}", not "${cfg.app.id}"`
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
const rotatesO11y = !!(options.rotateKeys || options.rotateO11yToken);
|
|
1007
|
+
const missingO11y = cfg.envs.some((env) => !credentials?.envs[env]?.o11yToken);
|
|
1008
|
+
const missingDb = cfg.envs.some((env) => !credentials?.envs[env]?.dbKey);
|
|
1009
|
+
const losesShownOnceCredential = hasDb && (!!options.rotateKeys || missingDb) || hasO11y && (rotatesO11y || missingO11y);
|
|
1010
|
+
if (options.writeCredentials === false && losesShownOnceCredential) {
|
|
1011
|
+
throw new Error("credential issuance/rotation requires the private credentials file; remove --no-write-credentials");
|
|
1012
|
+
}
|
|
1013
|
+
const devVarsTarget = resolveWriteDevVarsTarget(cfg, options.writeDevVars);
|
|
1014
|
+
if (cfg.local.gitignore) {
|
|
1015
|
+
ensureGitignore(cfg.rootDir, [cfg.local.tokenFile, cfg.local.credentialsFile, ...devVarsTarget ? [devVarsTarget] : []]);
|
|
1016
|
+
}
|
|
1017
|
+
if (options.pushSecrets) {
|
|
1018
|
+
await preflightSecretsPush({ configPath: cfg.configPath, runner: options.secretRunner });
|
|
1019
|
+
out.log("secrets: Wrangler config and login preflight passed");
|
|
1020
|
+
}
|
|
678
1021
|
const doFetch = options.fetch ?? fetch;
|
|
679
1022
|
const token = await getDeveloperToken(cfg, options, doFetch, out);
|
|
680
|
-
const
|
|
681
|
-
const apps = (0, import_apps.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
|
|
1023
|
+
const apps = (0, import_apps2.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
|
|
682
1024
|
const existing = await readRegistryApp(cfg, token, doFetch);
|
|
683
1025
|
if (existing) {
|
|
684
1026
|
out.log(`app: ${cfg.app.id} already exists`);
|
|
@@ -712,26 +1054,40 @@ async function provision(options) {
|
|
|
712
1054
|
out.log(`${env}: link ${link ? "set" : "cleared"}`);
|
|
713
1055
|
}
|
|
714
1056
|
}
|
|
715
|
-
let credentials = readCredentials(cfg.local.credentialsFile);
|
|
716
1057
|
for (const env of cfg.envs) {
|
|
717
|
-
const tenantId = (0,
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
1058
|
+
const tenantId = (0, import_apps2.tenantIdFor)(cfg.app.id, env);
|
|
1059
|
+
credentials = await provisionEnvCredentials({
|
|
1060
|
+
cfg,
|
|
1061
|
+
env,
|
|
1062
|
+
developerToken: token,
|
|
1063
|
+
credentials,
|
|
1064
|
+
rotateDb: !!options.rotateKeys,
|
|
1065
|
+
rotateO11y: rotatesO11y,
|
|
1066
|
+
write: options.writeCredentials !== false,
|
|
1067
|
+
fetch: doFetch,
|
|
1068
|
+
stdout: out
|
|
1069
|
+
});
|
|
1070
|
+
const dbKey = credentials.envs[env]?.dbKey;
|
|
1071
|
+
if (options.pushSecrets) {
|
|
1072
|
+
try {
|
|
1073
|
+
await secretsPushAfterPreflight({
|
|
1074
|
+
configPath: cfg.configPath,
|
|
1075
|
+
env,
|
|
1076
|
+
yes: options.yes,
|
|
1077
|
+
runner: options.secretRunner,
|
|
1078
|
+
stdout: out
|
|
1079
|
+
});
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1082
|
+
const consent = env === "prod" || env === "production" ? " --yes" : "";
|
|
1083
|
+
throw new Error(
|
|
1084
|
+
`${message}
|
|
1085
|
+
${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}${consent}" without issuing or rotating again`,
|
|
1086
|
+
{ cause: error }
|
|
1087
|
+
);
|
|
732
1088
|
}
|
|
733
1089
|
}
|
|
734
|
-
if (schema) {
|
|
1090
|
+
if (schema && dbKey) {
|
|
735
1091
|
await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
|
|
736
1092
|
out.log(`${env}: schema pushed`);
|
|
737
1093
|
}
|
|
@@ -739,7 +1095,7 @@ async function provision(options) {
|
|
|
739
1095
|
await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/admin/rules`, token, rules);
|
|
740
1096
|
out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
|
|
741
1097
|
}
|
|
742
|
-
if (cfg.ai?.provider && cfg.ai.keyEnv) {
|
|
1098
|
+
if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
|
|
743
1099
|
const key = import_node_process3.default.env[cfg.ai.keyEnv];
|
|
744
1100
|
if (key) {
|
|
745
1101
|
const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
|
|
@@ -749,36 +1105,17 @@ async function provision(options) {
|
|
|
749
1105
|
out.log(`${env}: ${cfg.ai.keyEnv} not set; skipped provider key storage`);
|
|
750
1106
|
}
|
|
751
1107
|
}
|
|
752
|
-
credentials = mergeCredential(credentials, {
|
|
753
|
-
appId: cfg.app.id,
|
|
754
|
-
platformUrl: cfg.platformUrl,
|
|
755
|
-
dbEndpoint: cfg.dbEndpoint,
|
|
756
|
-
env,
|
|
757
|
-
tenantId,
|
|
758
|
-
dbKey,
|
|
759
|
-
...o11yToken ? { o11yToken } : {}
|
|
760
|
-
});
|
|
761
1108
|
}
|
|
762
|
-
if (cfg.local.gitignore) ensureGitignore(cfg.rootDir);
|
|
763
1109
|
if (options.writeCredentials !== false && credentials) {
|
|
764
|
-
|
|
765
|
-
out.log(`credentials: wrote ${displayPath(cfg.local.credentialsFile, cfg.rootDir)} (0600, gitignored)`);
|
|
1110
|
+
const ignored = cfg.local.gitignore && gitignoreEntry(cfg.rootDir, cfg.local.credentialsFile);
|
|
1111
|
+
out.log(`credentials: wrote ${displayPath(cfg.local.credentialsFile, cfg.rootDir)} (0600${ignored ? ", gitignored" : ""})`);
|
|
766
1112
|
}
|
|
767
|
-
const devVarsTarget = resolveWriteDevVarsTarget(cfg, options.writeDevVars);
|
|
768
1113
|
if (devVarsTarget && credentials) {
|
|
769
1114
|
const env = cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
|
|
770
1115
|
writeDevVars(devVarsTarget, credentials, env, o11yDevVars(cfg));
|
|
771
1116
|
out.log(`dev vars: wrote ${displayPath(devVarsTarget, cfg.rootDir)} for ${env}`);
|
|
772
1117
|
}
|
|
773
1118
|
}
|
|
774
|
-
function o11yDevVars(cfg) {
|
|
775
|
-
if (!cfg.services.includes("o11y")) return void 0;
|
|
776
|
-
return {
|
|
777
|
-
endpoint: cfg.o11y?.endpoint ?? "https://o11y.odla.ai",
|
|
778
|
-
service: cfg.o11y?.service ?? cfg.app.id,
|
|
779
|
-
...cfg.o11y?.version ? { version: cfg.o11y.version } : {}
|
|
780
|
-
};
|
|
781
|
-
}
|
|
782
1119
|
async function readRegistryApp(cfg, token, doFetch) {
|
|
783
1120
|
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}`, {
|
|
784
1121
|
headers: { authorization: `Bearer ${token}` }
|
|
@@ -788,50 +1125,13 @@ async function readRegistryApp(cfg, token, doFetch) {
|
|
|
788
1125
|
const json = await res.json();
|
|
789
1126
|
return json.app ?? null;
|
|
790
1127
|
}
|
|
791
|
-
async function mintDbKey(opts) {
|
|
792
|
-
let res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(opts.tenantId)}/keys`, {
|
|
793
|
-
method: "POST",
|
|
794
|
-
headers: opts.auth,
|
|
795
|
-
body: "{}"
|
|
796
|
-
});
|
|
797
|
-
if (res.status === 404) {
|
|
798
|
-
const created = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps`, {
|
|
799
|
-
method: "POST",
|
|
800
|
-
headers: opts.auth,
|
|
801
|
-
body: JSON.stringify({
|
|
802
|
-
name: opts.env === "prod" ? opts.cfg.app.name : `${opts.cfg.app.name} (${opts.env})`,
|
|
803
|
-
appId: opts.tenantId
|
|
804
|
-
})
|
|
805
|
-
});
|
|
806
|
-
if (!created.ok) throw new Error(`db app create (${opts.tenantId}) failed: ${created.status} ${await safeText(created)}`);
|
|
807
|
-
res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(opts.tenantId)}/keys`, {
|
|
808
|
-
method: "POST",
|
|
809
|
-
headers: opts.auth,
|
|
810
|
-
body: "{}"
|
|
811
|
-
});
|
|
812
|
-
}
|
|
813
|
-
if (!res.ok) throw new Error(`db key mint (${opts.tenantId}) failed: ${res.status} ${await safeText(res)}`);
|
|
814
|
-
const body = await res.json();
|
|
815
|
-
if (!body.key) throw new Error(`db key mint (${opts.tenantId}) returned no key`);
|
|
816
|
-
return body.key;
|
|
817
|
-
}
|
|
818
|
-
async function mintO11yToken(cfg, env, token, doFetch) {
|
|
819
|
-
const res = await doFetch(
|
|
820
|
-
`${cfg.platformUrl}/o11y/${encodeURIComponent(cfg.app.id)}/token?env=${encodeURIComponent(env)}`,
|
|
821
|
-
{ method: "POST", headers: { authorization: `Bearer ${token}` } }
|
|
822
|
-
);
|
|
823
|
-
if (!res.ok) throw new Error(`o11y token mint (${env}) failed: ${res.status} ${await safeText(res)}`);
|
|
824
|
-
const body = await res.json();
|
|
825
|
-
if (!body.token) throw new Error(`o11y token mint (${env}) returned no token`);
|
|
826
|
-
return body.token;
|
|
827
|
-
}
|
|
828
1128
|
async function postJson(doFetch, url, bearer, body) {
|
|
829
1129
|
const res = await doFetch(url, {
|
|
830
1130
|
method: "POST",
|
|
831
1131
|
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
832
1132
|
body: JSON.stringify(body)
|
|
833
1133
|
});
|
|
834
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await
|
|
1134
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText2(res)}`);
|
|
835
1135
|
}
|
|
836
1136
|
function normalizeClerkConfig(value) {
|
|
837
1137
|
if (!value) return null;
|
|
@@ -848,12 +1148,7 @@ function defaultSecretName(provider) {
|
|
|
848
1148
|
const names = import_ai.DEFAULT_SECRET_NAMES;
|
|
849
1149
|
return names[provider] ?? `${provider}_api_key`;
|
|
850
1150
|
}
|
|
851
|
-
function
|
|
852
|
-
if (!requested) return null;
|
|
853
|
-
if (requested === true) return cfg.local.devVarsFile;
|
|
854
|
-
return (0, import_node_path6.resolve)((0, import_node_path6.dirname)(cfg.configPath), requested);
|
|
855
|
-
}
|
|
856
|
-
async function safeText(res) {
|
|
1151
|
+
async function safeText2(res) {
|
|
857
1152
|
try {
|
|
858
1153
|
return redactSecrets((await res.text()).slice(0, 500));
|
|
859
1154
|
} catch {
|
|
@@ -861,106 +1156,253 @@ async function safeText(res) {
|
|
|
861
1156
|
}
|
|
862
1157
|
}
|
|
863
1158
|
|
|
864
|
-
// src/
|
|
865
|
-
var
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1159
|
+
// src/skill.ts
|
|
1160
|
+
var import_node_fs6 = require("fs");
|
|
1161
|
+
var import_node_os = require("os");
|
|
1162
|
+
var import_node_path6 = require("path");
|
|
1163
|
+
var import_node_url2 = require("url");
|
|
1164
|
+
|
|
1165
|
+
// src/skill-adapters.ts
|
|
1166
|
+
var PROJECT_INSTRUCTIONS = `<!-- odla-ai agent setup:start -->
|
|
1167
|
+
## odla agent workflow
|
|
1168
|
+
|
|
1169
|
+
For work that creates an odla app or adds odla services, read and follow
|
|
1170
|
+
\`.agents/skills/odla/SKILL.md\`. It dispatches an existing static site to
|
|
1171
|
+
\`.agents/skills/odla-migrate/SKILL.md\`. For production telemetry triage, use
|
|
1172
|
+
\`.agents/skills/odla-o11y-debug/SKILL.md\`.
|
|
1173
|
+
|
|
1174
|
+
The complete runbooks and their references are installed in this repository.
|
|
1175
|
+
Do not fetch online odla documentation as setup context. Network access is only
|
|
1176
|
+
needed when a runbook deliberately calls the odla service, npm, Cloudflare, or
|
|
1177
|
+
another configured provider.
|
|
1178
|
+
<!-- odla-ai agent setup:end -->`;
|
|
1179
|
+
var CURSOR_RULE = `---
|
|
1180
|
+
description: Build, migrate, provision, secure, or debug an odla app using the locally installed odla runbooks
|
|
1181
|
+
globs:
|
|
1182
|
+
alwaysApply: false
|
|
1183
|
+
---
|
|
1184
|
+
|
|
1185
|
+
${PROJECT_INSTRUCTIONS}
|
|
1186
|
+
`;
|
|
1187
|
+
function claudeAdapter(skill, canonical) {
|
|
1188
|
+
const match = canonical.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
1189
|
+
if (!match) throw new Error(`bundled skill ${skill} has no YAML frontmatter`);
|
|
1190
|
+
const lines = match[1].split(/\r?\n/);
|
|
1191
|
+
const frontmatter = [];
|
|
1192
|
+
let keepIndented = false;
|
|
1193
|
+
for (const line of lines) {
|
|
1194
|
+
if (line.startsWith("name:") || line.startsWith("description:")) {
|
|
1195
|
+
frontmatter.push(line);
|
|
1196
|
+
keepIndented = line.startsWith("description:");
|
|
1197
|
+
} else if (keepIndented && /^\s+/.test(line)) {
|
|
1198
|
+
frontmatter.push(line);
|
|
1199
|
+
} else {
|
|
1200
|
+
keepIndented = false;
|
|
905
1201
|
}
|
|
906
|
-
out.log(`${s.name} pushed to ${target} (value read from ${credentialsPath}, never echoed)`);
|
|
907
1202
|
}
|
|
1203
|
+
return `---
|
|
1204
|
+
${frontmatter.join("\n")}
|
|
1205
|
+
---
|
|
1206
|
+
|
|
1207
|
+
# ${skill} (odla adapter)
|
|
1208
|
+
|
|
1209
|
+
Read \`../../../.agents/skills/${skill}/SKILL.md\` completely and follow it. That
|
|
1210
|
+
canonical local skill owns every reference path and contains the full offline
|
|
1211
|
+
runbook. Do not substitute a remote documentation page for the installed copy.
|
|
1212
|
+
`;
|
|
908
1213
|
}
|
|
909
1214
|
|
|
910
1215
|
// src/skill.ts
|
|
911
|
-
var
|
|
912
|
-
var import_node_os = require("os");
|
|
913
|
-
var import_node_path7 = require("path");
|
|
914
|
-
var import_node_url2 = require("url");
|
|
1216
|
+
var AGENT_HARNESSES = ["claude", "codex", "cursor", "copilot", "gemini", "agents"];
|
|
915
1217
|
function installSkill(options = {}) {
|
|
916
1218
|
const out = options.stdout ?? console;
|
|
917
1219
|
const sourceDir = options.sourceDir ?? (0, import_node_url2.fileURLToPath)(new URL("../skills", importMetaUrl));
|
|
918
|
-
const targetDir = options.global ? (0, import_node_path7.join)(options.homeDir ?? (0, import_node_os.homedir)(), ".claude", "skills") : (0, import_node_path7.resolve)(options.dir ?? process.cwd(), ".claude", "skills");
|
|
919
1220
|
const files = listFiles(sourceDir);
|
|
920
1221
|
if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
|
|
921
|
-
const
|
|
922
|
-
const
|
|
923
|
-
const
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1222
|
+
const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
|
|
1223
|
+
const root = (0, import_node_path6.resolve)(options.dir ?? process.cwd());
|
|
1224
|
+
const home = (0, import_node_path6.resolve)(options.homeDir ?? (0, import_node_os.homedir)());
|
|
1225
|
+
const plans = /* @__PURE__ */ new Map();
|
|
1226
|
+
const targets = /* @__PURE__ */ new Map();
|
|
1227
|
+
const rememberTarget = (harness, target) => {
|
|
1228
|
+
const current = targets.get(harness) ?? /* @__PURE__ */ new Set();
|
|
1229
|
+
current.add(target);
|
|
1230
|
+
targets.set(harness, current);
|
|
1231
|
+
};
|
|
1232
|
+
const plan = (target, content, managedMerge = false, boundary = root) => {
|
|
1233
|
+
const existing = plans.get(target);
|
|
1234
|
+
if (existing && existing.content !== content) throw new Error(`internal agent setup conflict for ${target}`);
|
|
1235
|
+
plans.set(target, { target, content, boundary, managedMerge });
|
|
1236
|
+
};
|
|
1237
|
+
const planSkillTree = (targetDir2, boundary = root) => {
|
|
1238
|
+
for (const rel of files) plan((0, import_node_path6.join)(targetDir2, rel), (0, import_node_fs6.readFileSync)((0, import_node_path6.join)(sourceDir, rel), "utf8"), false, boundary);
|
|
1239
|
+
};
|
|
1240
|
+
let targetDir;
|
|
1241
|
+
if (options.global) {
|
|
1242
|
+
const claudeRoot = (0, import_node_path6.join)(home, ".claude", "skills");
|
|
1243
|
+
const codexRoot = (0, import_node_path6.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path6.join)(home, ".codex"), "skills");
|
|
1244
|
+
targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
|
|
1245
|
+
for (const harness of harnesses) {
|
|
1246
|
+
const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
|
|
1247
|
+
planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path6.dirname)((0, import_node_path6.dirname)(codexRoot)));
|
|
1248
|
+
rememberTarget(harness, skillRoot);
|
|
1249
|
+
}
|
|
1250
|
+
} else {
|
|
1251
|
+
const sharedRoot = (0, import_node_path6.join)(root, ".agents", "skills");
|
|
1252
|
+
planSkillTree(sharedRoot);
|
|
1253
|
+
const claudeRoot = (0, import_node_path6.join)(root, ".claude", "skills");
|
|
1254
|
+
targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
|
|
1255
|
+
for (const harness of harnesses) rememberTarget(harness, sharedRoot);
|
|
1256
|
+
if (harnesses.includes("claude")) {
|
|
1257
|
+
for (const skill of skillNames(files)) {
|
|
1258
|
+
const canonical = (0, import_node_fs6.readFileSync)((0, import_node_path6.join)(sourceDir, skill, "SKILL.md"), "utf8");
|
|
1259
|
+
plan((0, import_node_path6.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
|
|
936
1260
|
}
|
|
1261
|
+
rememberTarget("claude", claudeRoot);
|
|
1262
|
+
}
|
|
1263
|
+
if (harnesses.includes("cursor")) {
|
|
1264
|
+
const cursorRule = (0, import_node_path6.join)(root, ".cursor", "rules", "odla.mdc");
|
|
1265
|
+
plan(cursorRule, CURSOR_RULE);
|
|
1266
|
+
rememberTarget("cursor", cursorRule);
|
|
1267
|
+
}
|
|
1268
|
+
if (harnesses.includes("agents")) {
|
|
1269
|
+
const agentsFile = (0, import_node_path6.join)(root, "AGENTS.md");
|
|
1270
|
+
plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
1271
|
+
rememberTarget("agents", agentsFile);
|
|
1272
|
+
}
|
|
1273
|
+
if (harnesses.includes("copilot")) {
|
|
1274
|
+
const copilotFile = (0, import_node_path6.join)(root, ".github", "copilot-instructions.md");
|
|
1275
|
+
plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
1276
|
+
rememberTarget("copilot", copilotFile);
|
|
1277
|
+
}
|
|
1278
|
+
if (harnesses.includes("gemini")) {
|
|
1279
|
+
const geminiFile = (0, import_node_path6.join)(root, "GEMINI.md");
|
|
1280
|
+
plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
1281
|
+
rememberTarget("gemini", geminiFile);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
const writtenPaths = /* @__PURE__ */ new Set();
|
|
1285
|
+
const unchangedPaths = /* @__PURE__ */ new Set();
|
|
1286
|
+
const conflicts = [];
|
|
1287
|
+
for (const file of plans.values()) {
|
|
1288
|
+
const symlink = symlinkedComponent(file.boundary, file.target);
|
|
1289
|
+
if (symlink) {
|
|
1290
|
+
conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
if (!(0, import_node_fs6.existsSync)(file.target)) {
|
|
1294
|
+
writtenPaths.add(file.target);
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
const current = (0, import_node_fs6.readFileSync)(file.target, "utf8");
|
|
1298
|
+
if (current === file.content) {
|
|
1299
|
+
unchangedPaths.add(file.target);
|
|
1300
|
+
} else if (file.managedMerge || options.force) {
|
|
1301
|
+
writtenPaths.add(file.target);
|
|
1302
|
+
unchangedPaths.delete(file.target);
|
|
1303
|
+
} else {
|
|
1304
|
+
conflicts.push(file.target);
|
|
937
1305
|
}
|
|
938
|
-
written.push(rel);
|
|
939
1306
|
}
|
|
940
1307
|
if (conflicts.length > 0) {
|
|
941
1308
|
throw new Error(
|
|
942
|
-
`
|
|
943
|
-
${conflicts.map((f) => ` - ${
|
|
1309
|
+
`agent setup files modified locally (re-run with --force to replace only odla-managed content):
|
|
1310
|
+
${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
944
1311
|
);
|
|
945
1312
|
}
|
|
946
|
-
for (const
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1313
|
+
for (const file of plans.values()) {
|
|
1314
|
+
if (!(0, import_node_fs6.existsSync)(file.target) || (0, import_node_fs6.readFileSync)(file.target, "utf8") !== file.content) {
|
|
1315
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path6.dirname)(file.target), { recursive: true });
|
|
1316
|
+
(0, import_node_fs6.writeFileSync)(file.target, file.content);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
const skills = skillNames(files);
|
|
1320
|
+
const installations = harnesses.map((harness) => ({ harness, targets: [...targets.get(harness) ?? []] }));
|
|
1321
|
+
out.log(`agent harnesses: ${harnesses.join(", ")}`);
|
|
1322
|
+
out.log(`offline skills: ${skills.join(", ")}`);
|
|
1323
|
+
for (const installation of installations) out.log(`${installation.harness}: ${installation.targets.join(", ")}`);
|
|
1324
|
+
out.log(`installed ${writtenPaths.size} managed file(s)${unchangedPaths.size ? ` (${unchangedPaths.size} unchanged)` : ""}`);
|
|
1325
|
+
return {
|
|
1326
|
+
targetDir,
|
|
1327
|
+
written: pathsUnder(targetDir, writtenPaths),
|
|
1328
|
+
unchanged: pathsUnder(targetDir, unchangedPaths),
|
|
1329
|
+
writtenPaths: [...writtenPaths].sort(),
|
|
1330
|
+
unchangedPaths: [...unchangedPaths].sort(),
|
|
1331
|
+
harnesses: installations
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
function pathsUnder(root, paths) {
|
|
1335
|
+
return [...paths].map((path) => (0, import_node_path6.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path6.sep}`) && !(0, import_node_path6.isAbsolute)(path)).sort();
|
|
1336
|
+
}
|
|
1337
|
+
function normalizeHarnesses(values, global) {
|
|
1338
|
+
const requested = values?.length ? values : ["claude"];
|
|
1339
|
+
const allowed = /* @__PURE__ */ new Set([...AGENT_HARNESSES, "all"]);
|
|
1340
|
+
for (const harness of requested) {
|
|
1341
|
+
if (!allowed.has(harness)) {
|
|
1342
|
+
throw new Error(`unknown agent harness "${harness}"; choose all, ${AGENT_HARNESSES.join(", ")}`);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
const expanded = requested.includes("all") ? global ? ["claude", "codex"] : [...AGENT_HARNESSES] : [...new Set(requested)];
|
|
1346
|
+
if (global) {
|
|
1347
|
+
const unsupported = expanded.filter((harness) => harness !== "claude" && harness !== "codex");
|
|
1348
|
+
if (unsupported.length) {
|
|
1349
|
+
throw new Error(`--global supports claude and codex only; ${unsupported.join(", ")} require a project-local adapter`);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
return expanded;
|
|
1353
|
+
}
|
|
1354
|
+
function managedFileContent(path, block, force, boundary) {
|
|
1355
|
+
const symlink = symlinkedComponent(boundary, path);
|
|
1356
|
+
if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
|
|
1357
|
+
if (!(0, import_node_fs6.existsSync)(path)) return `${block}
|
|
1358
|
+
`;
|
|
1359
|
+
const current = (0, import_node_fs6.readFileSync)(path, "utf8");
|
|
1360
|
+
const start = "<!-- odla-ai agent setup:start -->";
|
|
1361
|
+
const end = "<!-- odla-ai agent setup:end -->";
|
|
1362
|
+
const startAt = current.indexOf(start);
|
|
1363
|
+
const endAt = current.indexOf(end);
|
|
1364
|
+
if (startAt === -1 !== (endAt === -1) || startAt !== -1 && endAt < startAt) {
|
|
1365
|
+
throw new Error(`malformed odla-managed section in ${path}`);
|
|
1366
|
+
}
|
|
1367
|
+
if (startAt === -1) {
|
|
1368
|
+
const separator = current.length === 0 || current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n";
|
|
1369
|
+
return `${current}${separator}${block}
|
|
1370
|
+
`;
|
|
1371
|
+
}
|
|
1372
|
+
const afterEnd = endAt + end.length;
|
|
1373
|
+
const existing = current.slice(startAt, afterEnd);
|
|
1374
|
+
if (existing !== block && !force) {
|
|
1375
|
+
throw new Error(`odla-managed section modified locally in ${path}; re-run with --force to replace that section`);
|
|
1376
|
+
}
|
|
1377
|
+
return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
|
|
1378
|
+
}
|
|
1379
|
+
function symlinkedComponent(boundary, target) {
|
|
1380
|
+
const rel = (0, import_node_path6.relative)(boundary, target);
|
|
1381
|
+
if (rel === ".." || rel.startsWith(`..${import_node_path6.sep}`) || (0, import_node_path6.isAbsolute)(rel)) {
|
|
1382
|
+
throw new Error(`agent setup target escapes its install root: ${target}`);
|
|
950
1383
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1384
|
+
let current = boundary;
|
|
1385
|
+
for (const part of rel.split(import_node_path6.sep).filter(Boolean)) {
|
|
1386
|
+
current = (0, import_node_path6.join)(current, part);
|
|
1387
|
+
try {
|
|
1388
|
+
if ((0, import_node_fs6.lstatSync)(current).isSymbolicLink()) return current;
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
if (error.code !== "ENOENT") throw error;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
return void 0;
|
|
1394
|
+
}
|
|
1395
|
+
function skillNames(files) {
|
|
1396
|
+
return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
|
|
955
1397
|
}
|
|
956
1398
|
function listFiles(dir) {
|
|
957
1399
|
if (!(0, import_node_fs6.existsSync)(dir)) return [];
|
|
958
1400
|
const results = [];
|
|
959
1401
|
const walk = (current) => {
|
|
960
1402
|
for (const entry of (0, import_node_fs6.readdirSync)(current, { withFileTypes: true })) {
|
|
961
|
-
const path = (0,
|
|
1403
|
+
const path = (0, import_node_path6.join)(current, entry.name);
|
|
962
1404
|
if (entry.isDirectory()) walk(path);
|
|
963
|
-
else results.push((0,
|
|
1405
|
+
else results.push((0, import_node_path6.relative)(dir, path));
|
|
964
1406
|
}
|
|
965
1407
|
};
|
|
966
1408
|
walk(dir);
|
|
@@ -1023,7 +1465,7 @@ async function getJson(doFetch, url, bearer) {
|
|
|
1023
1465
|
const res = await doFetch(url, {
|
|
1024
1466
|
headers: bearer ? { authorization: `Bearer ${bearer}` } : void 0
|
|
1025
1467
|
});
|
|
1026
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await
|
|
1468
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText3(res)}`);
|
|
1027
1469
|
return res.json();
|
|
1028
1470
|
}
|
|
1029
1471
|
async function postJson2(doFetch, url, bearer, body) {
|
|
@@ -1032,7 +1474,7 @@ async function postJson2(doFetch, url, bearer, body) {
|
|
|
1032
1474
|
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
1033
1475
|
body: JSON.stringify(body)
|
|
1034
1476
|
});
|
|
1035
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await
|
|
1477
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText3(res)}`);
|
|
1036
1478
|
return res.json();
|
|
1037
1479
|
}
|
|
1038
1480
|
function publicConfigUrl(platformUrl, appId, env) {
|
|
@@ -1040,7 +1482,7 @@ function publicConfigUrl(platformUrl, appId, env) {
|
|
|
1040
1482
|
url.searchParams.set("env", env);
|
|
1041
1483
|
return url.toString();
|
|
1042
1484
|
}
|
|
1043
|
-
async function
|
|
1485
|
+
async function safeText3(res) {
|
|
1044
1486
|
try {
|
|
1045
1487
|
return (await res.text()).slice(0, 500);
|
|
1046
1488
|
} catch {
|
|
@@ -1053,14 +1495,17 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
1053
1495
|
const parsed = parseArgv(argv);
|
|
1054
1496
|
const command = parsed.positionals[0] ?? "help";
|
|
1055
1497
|
if (command === "version" || command === "-v" || parsed.options.version === true) {
|
|
1498
|
+
assertArgs(parsed, ["version"], 1);
|
|
1056
1499
|
console.log(cliVersion());
|
|
1057
1500
|
return;
|
|
1058
1501
|
}
|
|
1059
1502
|
if (command === "help" || command === "--help" || command === "-h") {
|
|
1503
|
+
assertArgs(parsed, ["help"], 1);
|
|
1060
1504
|
printHelp();
|
|
1061
1505
|
return;
|
|
1062
1506
|
}
|
|
1063
1507
|
if (command === "init") {
|
|
1508
|
+
assertArgs(parsed, ["app-id", "name", "config", "env", "services", "ai-provider", "force"], 1);
|
|
1064
1509
|
initProject({
|
|
1065
1510
|
appId: requiredString(parsed.options["app-id"], "--app-id"),
|
|
1066
1511
|
name: requiredString(parsed.options.name, "--name"),
|
|
@@ -1073,15 +1518,39 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
1073
1518
|
return;
|
|
1074
1519
|
}
|
|
1075
1520
|
if (command === "doctor") {
|
|
1521
|
+
assertArgs(parsed, ["config"], 1);
|
|
1076
1522
|
await doctor({ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs" });
|
|
1077
1523
|
return;
|
|
1078
1524
|
}
|
|
1525
|
+
if (command === "capabilities") {
|
|
1526
|
+
assertArgs(parsed, ["json"], 1);
|
|
1527
|
+
printCapabilities(parsed.options.json === true);
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1079
1530
|
if (command === "provision") {
|
|
1531
|
+
assertArgs(
|
|
1532
|
+
parsed,
|
|
1533
|
+
[
|
|
1534
|
+
"config",
|
|
1535
|
+
"dry-run",
|
|
1536
|
+
"rotate-keys",
|
|
1537
|
+
"rotate-o11y-token",
|
|
1538
|
+
"push-secrets",
|
|
1539
|
+
"write-credentials",
|
|
1540
|
+
"write-dev-vars",
|
|
1541
|
+
"token",
|
|
1542
|
+
"open",
|
|
1543
|
+
"yes"
|
|
1544
|
+
],
|
|
1545
|
+
1
|
|
1546
|
+
);
|
|
1080
1547
|
const writeDevVars2 = parsed.options["write-dev-vars"];
|
|
1081
1548
|
const opts = {
|
|
1082
1549
|
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
1083
1550
|
dryRun: parsed.options["dry-run"] === true,
|
|
1084
1551
|
rotateKeys: parsed.options["rotate-keys"] === true,
|
|
1552
|
+
rotateO11yToken: parsed.options["rotate-o11y-token"] === true,
|
|
1553
|
+
pushSecrets: parsed.options["push-secrets"] === true,
|
|
1085
1554
|
writeCredentials: parsed.options["write-credentials"] !== false,
|
|
1086
1555
|
writeDevVars: typeof writeDevVars2 === "string" ? writeDevVars2 : writeDevVars2 === true,
|
|
1087
1556
|
token: stringOpt(parsed.options.token),
|
|
@@ -1092,18 +1561,34 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
1092
1561
|
return;
|
|
1093
1562
|
}
|
|
1094
1563
|
if (command === "smoke") {
|
|
1564
|
+
assertArgs(parsed, ["config", "env"], 1);
|
|
1095
1565
|
await smoke({
|
|
1096
1566
|
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
1097
1567
|
env: stringOpt(parsed.options.env)
|
|
1098
1568
|
});
|
|
1099
1569
|
return;
|
|
1100
1570
|
}
|
|
1571
|
+
if (command === "setup") {
|
|
1572
|
+
assertArgs(parsed, ["dir", "global", "force", "agent", "harness"], 1);
|
|
1573
|
+
installSkill({
|
|
1574
|
+
dir: stringOpt(parsed.options.dir),
|
|
1575
|
+
global: parsed.options.global === true,
|
|
1576
|
+
harnesses: harnessList(parsed),
|
|
1577
|
+
force: parsed.options.force === true
|
|
1578
|
+
});
|
|
1579
|
+
console.log(
|
|
1580
|
+
"\nOffline runbooks installed. Open this repo in your coding agent and ask it to set up odla.\nIts native adapter finds the local `odla` skill, chooses build or migration, and drives the CLI."
|
|
1581
|
+
);
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1101
1584
|
if (command === "skill") {
|
|
1102
1585
|
const sub = parsed.positionals[1];
|
|
1103
1586
|
if (sub !== "install") throw new Error(`unknown skill subcommand "${sub ?? ""}". Try "odla-ai skill install".`);
|
|
1587
|
+
assertArgs(parsed, ["dir", "global", "force", "agent", "harness"], 2);
|
|
1104
1588
|
installSkill({
|
|
1105
1589
|
dir: stringOpt(parsed.options.dir),
|
|
1106
1590
|
global: parsed.options.global === true,
|
|
1591
|
+
harnesses: harnessList(parsed),
|
|
1107
1592
|
force: parsed.options.force === true
|
|
1108
1593
|
});
|
|
1109
1594
|
return;
|
|
@@ -1111,6 +1596,7 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
1111
1596
|
if (command === "secrets") {
|
|
1112
1597
|
const sub = parsed.positionals[1];
|
|
1113
1598
|
if (sub !== "push") throw new Error(`unknown secrets subcommand "${sub ?? ""}". Try "odla-ai secrets push --env dev".`);
|
|
1599
|
+
assertArgs(parsed, ["config", "env", "dry-run", "yes"], 2);
|
|
1114
1600
|
await secretsPush({
|
|
1115
1601
|
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
1116
1602
|
env: requiredString(parsed.options.env, "--env"),
|
|
@@ -1121,6 +1607,17 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
1121
1607
|
}
|
|
1122
1608
|
throw new Error(`unknown command "${command}". Run "odla-ai help".`);
|
|
1123
1609
|
}
|
|
1610
|
+
function assertArgs(parsed, allowedOptions, maxPositionals) {
|
|
1611
|
+
const allowed = new Set(allowedOptions);
|
|
1612
|
+
for (const name of Object.keys(parsed.options)) {
|
|
1613
|
+
if (!allowed.has(name)) {
|
|
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
|
+
}
|
|
1124
1621
|
function parseArgv(argv) {
|
|
1125
1622
|
const positionals = [];
|
|
1126
1623
|
const options = {};
|
|
@@ -1176,6 +1673,21 @@ function listOpt(value) {
|
|
|
1176
1673
|
const values = Array.isArray(value) ? value : [value];
|
|
1177
1674
|
return values.flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
|
|
1178
1675
|
}
|
|
1676
|
+
function harnessList(parsed) {
|
|
1677
|
+
const selected = [
|
|
1678
|
+
...harnessOption(parsed.options.agent, "--agent"),
|
|
1679
|
+
...harnessOption(parsed.options.harness, "--harness")
|
|
1680
|
+
];
|
|
1681
|
+
return selected.length ? selected : ["all"];
|
|
1682
|
+
}
|
|
1683
|
+
function harnessOption(value, flag) {
|
|
1684
|
+
if (value === void 0) return [];
|
|
1685
|
+
if (typeof value === "boolean") throw new Error(`${flag} requires a harness name`);
|
|
1686
|
+
const raw = Array.isArray(value) ? value : [value];
|
|
1687
|
+
const parts = raw.flatMap((item) => item.split(","));
|
|
1688
|
+
if (parts.some((item) => !item.trim())) throw new Error(`${flag} requires a harness name`);
|
|
1689
|
+
return parts.map((item) => item.trim());
|
|
1690
|
+
}
|
|
1179
1691
|
function cliVersion() {
|
|
1180
1692
|
const pkg = JSON.parse((0, import_node_fs7.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
|
|
1181
1693
|
return pkg.version ?? "unknown";
|
|
@@ -1184,35 +1696,46 @@ function printHelp() {
|
|
|
1184
1696
|
console.log(`odla-ai
|
|
1185
1697
|
|
|
1186
1698
|
Usage:
|
|
1187
|
-
odla-ai
|
|
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]
|
|
1188
1701
|
odla-ai doctor [--config odla.config.mjs]
|
|
1189
|
-
odla-ai
|
|
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]
|
|
1190
1704
|
odla-ai smoke [--config odla.config.mjs] [--env dev]
|
|
1191
|
-
odla-ai skill install [--dir <project>] [--global] [--force]
|
|
1705
|
+
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
1192
1706
|
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
1193
1707
|
odla-ai version
|
|
1194
1708
|
|
|
1195
1709
|
Commands:
|
|
1710
|
+
setup Install offline odla runbooks for common coding-agent harnesses.
|
|
1196
1711
|
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
1197
1712
|
doctor Validate and summarize the project config without network calls.
|
|
1198
|
-
|
|
1713
|
+
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
1714
|
+
provision Register services, configure them, persist credentials, optionally push secrets.
|
|
1199
1715
|
smoke Verify local credentials, public-config, live schema, and db aggregate.
|
|
1200
|
-
skill
|
|
1201
|
-
|
|
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.
|
|
1202
1719
|
version Print the CLI version.
|
|
1203
1720
|
|
|
1204
1721
|
Safety:
|
|
1205
|
-
|
|
1206
|
-
|
|
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.
|
|
1207
1727
|
Provision opens the approval page automatically in interactive terminals;
|
|
1208
1728
|
use --open to force browser launch or --no-open to suppress it.
|
|
1209
1729
|
`);
|
|
1210
1730
|
}
|
|
1211
1731
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1212
1732
|
0 && (module.exports = {
|
|
1733
|
+
AGENT_HARNESSES,
|
|
1734
|
+
CAPABILITIES,
|
|
1213
1735
|
doctor,
|
|
1214
1736
|
initProject,
|
|
1215
1737
|
installSkill,
|
|
1738
|
+
printCapabilities,
|
|
1216
1739
|
provision,
|
|
1217
1740
|
redactSecrets,
|
|
1218
1741
|
runCli,
|