@odla-ai/cli 0.2.1 → 0.4.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/dist/bin.cjs +123 -58
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-5J4LKP37.js → chunk-UWT7C6VT.js} +124 -59
- package/dist/chunk-UWT7C6VT.js.map +1 -0
- package/dist/index.cjs +123 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +1 -1
- package/llms.txt +10 -1
- package/package.json +1 -1
- package/skills/odla/SKILL.md +76 -0
- package/skills/odla/references/build.md +58 -0
- package/skills/odla/references/sdks.md +64 -0
- package/skills/odla-migrate/references/phase-3-auth.md +7 -2
- package/skills/odla-migrate/references/secrets-map.md +6 -4
- package/dist/chunk-5J4LKP37.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -165,6 +165,7 @@ var REPLACEMENTS = [
|
|
|
165
165
|
[/\bsk_(live|test)_[A-Za-z0-9]+/g, "sk_$1_[redacted]"],
|
|
166
166
|
[/\bsk-[A-Za-z0-9._-]+/g, "sk-[redacted]"],
|
|
167
167
|
[/\bwhsec_[A-Za-z0-9+/=]+/g, "whsec_[redacted]"],
|
|
168
|
+
[/\bo11y_[A-Za-z0-9]+/g, "o11y_[redacted]"],
|
|
168
169
|
[/\b(ghp|gho|github_pat)_[A-Za-z0-9_]+/g, "$1_[redacted]"],
|
|
169
170
|
[/\bAKIA[A-Z0-9]{12,}/g, "AKIA[redacted]"]
|
|
170
171
|
];
|
|
@@ -309,7 +310,7 @@ function wranglerWarnings(rootDir) {
|
|
|
309
310
|
const vars = block.vars;
|
|
310
311
|
if (vars && typeof vars === "object") {
|
|
311
312
|
for (const [name, value] of Object.entries(vars)) {
|
|
312
|
-
if (name === "ODLA_API_KEY" || typeof value === "string" && looksSecret(value)) {
|
|
313
|
+
if (name === "ODLA_API_KEY" || name === "ODLA_O11Y_TOKEN" || typeof value === "string" && looksSecret(value)) {
|
|
313
314
|
warnings.push(`wrangler ${label}vars.${name} looks like a secret \u2014 use "odla-ai secrets push" / wrangler secret put, never vars`);
|
|
314
315
|
}
|
|
315
316
|
}
|
|
@@ -329,54 +330,6 @@ function readPackageJson(rootDir) {
|
|
|
329
330
|
}
|
|
330
331
|
}
|
|
331
332
|
|
|
332
|
-
// src/doctor.ts
|
|
333
|
-
async function doctor(options) {
|
|
334
|
-
const out = options.stdout ?? console;
|
|
335
|
-
const cfg = await loadProjectConfig(options.configPath);
|
|
336
|
-
const plan = buildPlan(cfg);
|
|
337
|
-
const schema = await resolveDataExport(cfg, cfg.db?.schema, ["schema", "SCHEMA"]);
|
|
338
|
-
const configuredRules = await resolveDataExport(cfg, cfg.db?.rules, ["rules", "RULES"]);
|
|
339
|
-
const rules = configuredRules ?? (schema && cfg.db?.defaultRules !== false ? rulesFromSchema(schema) : void 0);
|
|
340
|
-
const entities = serializedEntities(schema);
|
|
341
|
-
out.log(`config: ${cfg.configPath}`);
|
|
342
|
-
out.log(`app: ${plan.appName} (${plan.appId})`);
|
|
343
|
-
out.log(`envs: ${plan.envs.join(", ")}`);
|
|
344
|
-
out.log(`svc: ${plan.services.join(", ")}`);
|
|
345
|
-
out.log(`schema: ${schema ? `${entities.length} entities` : "none"}`);
|
|
346
|
-
out.log(`rules: ${rules ? `${Object.keys(rules).length} namespaces` : "none"}`);
|
|
347
|
-
out.log(`ai: ${cfg.ai?.provider ?? "not configured"}`);
|
|
348
|
-
const warnings = [];
|
|
349
|
-
if (schema && entities.length === 0) warnings.push("schema has no entities");
|
|
350
|
-
if (rules) {
|
|
351
|
-
for (const ns of Object.keys(rules)) {
|
|
352
|
-
if (entities.length > 0 && !entities.includes(ns)) warnings.push(`rules include "${ns}" but schema has no matching entity`);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
if (cfg.services.includes("ai") && !cfg.ai?.provider) warnings.push("ai service is enabled but ai.provider is not set");
|
|
356
|
-
if (cfg.auth?.clerk) {
|
|
357
|
-
for (const [env, value] of Object.entries(cfg.auth.clerk)) {
|
|
358
|
-
if (typeof value === "string" && value.startsWith("$") && !process.env[value.slice(1)]) {
|
|
359
|
-
warnings.push(`auth.clerk.${env} references unset env var ${value}`);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
if (cfg.ai?.keyEnv && !process.env[cfg.ai.keyEnv]) warnings.push(`${cfg.ai.keyEnv} is not set; provision will skip provider key storage`);
|
|
364
|
-
warnings.push(...lintRules(rules, entities, cfg.db?.publicRead ?? []));
|
|
365
|
-
warnings.push(...trackedSecretFiles(cfg.rootDir));
|
|
366
|
-
warnings.push(...wranglerWarnings(cfg.rootDir));
|
|
367
|
-
if (warnings.length) {
|
|
368
|
-
out.log("");
|
|
369
|
-
out.log("warnings:");
|
|
370
|
-
for (const warning of warnings) out.log(` - ${warning}`);
|
|
371
|
-
} else {
|
|
372
|
-
out.log("ok");
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// src/init.ts
|
|
377
|
-
var import_node_fs5 = require("fs");
|
|
378
|
-
var import_node_path5 = require("path");
|
|
379
|
-
|
|
380
333
|
// src/local.ts
|
|
381
334
|
var import_node_fs4 = require("fs");
|
|
382
335
|
var import_node_path4 = require("path");
|
|
@@ -412,7 +365,8 @@ function mergeCredential(current, update) {
|
|
|
412
365
|
next.envs[update.env] = {
|
|
413
366
|
...next.envs[update.env] ?? {},
|
|
414
367
|
tenantId: update.tenantId,
|
|
415
|
-
...update.dbKey ? { dbKey: update.dbKey } : {}
|
|
368
|
+
...update.dbKey ? { dbKey: update.dbKey } : {},
|
|
369
|
+
...update.o11yToken ? { o11yToken: update.o11yToken } : {}
|
|
416
370
|
};
|
|
417
371
|
return next;
|
|
418
372
|
}
|
|
@@ -425,7 +379,7 @@ function ensureGitignore(rootDir) {
|
|
|
425
379
|
(0, import_node_fs4.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
|
|
426
380
|
`);
|
|
427
381
|
}
|
|
428
|
-
function writeDevVars(path, credentials, env) {
|
|
382
|
+
function writeDevVars(path, credentials, env, o11y) {
|
|
429
383
|
const entry = credentials.envs[env];
|
|
430
384
|
if (!entry?.dbKey) throw new Error(`no db key for env "${env}" in ${path}`);
|
|
431
385
|
const lines = [
|
|
@@ -436,6 +390,11 @@ function writeDevVars(path, credentials, env) {
|
|
|
436
390
|
`ODLA_TENANT="${entry.tenantId}"`,
|
|
437
391
|
`ODLA_API_KEY="${entry.dbKey}"`
|
|
438
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
|
+
}
|
|
439
398
|
(0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(path), { recursive: true });
|
|
440
399
|
(0, import_node_fs4.writeFileSync)(path, `${lines.join("\n")}
|
|
441
400
|
`);
|
|
@@ -446,7 +405,61 @@ function displayPath(path, rootDir = process.cwd()) {
|
|
|
446
405
|
return rel && !rel.startsWith("..") ? rel : path;
|
|
447
406
|
}
|
|
448
407
|
|
|
408
|
+
// src/doctor.ts
|
|
409
|
+
async function doctor(options) {
|
|
410
|
+
const out = options.stdout ?? console;
|
|
411
|
+
const cfg = await loadProjectConfig(options.configPath);
|
|
412
|
+
const plan = buildPlan(cfg);
|
|
413
|
+
const schema = await resolveDataExport(cfg, cfg.db?.schema, ["schema", "SCHEMA"]);
|
|
414
|
+
const configuredRules = await resolveDataExport(cfg, cfg.db?.rules, ["rules", "RULES"]);
|
|
415
|
+
const rules = configuredRules ?? (schema && cfg.db?.defaultRules !== false ? rulesFromSchema(schema) : void 0);
|
|
416
|
+
const entities = serializedEntities(schema);
|
|
417
|
+
out.log(`config: ${cfg.configPath}`);
|
|
418
|
+
out.log(`app: ${plan.appName} (${plan.appId})`);
|
|
419
|
+
out.log(`envs: ${plan.envs.join(", ")}`);
|
|
420
|
+
out.log(`svc: ${plan.services.join(", ")}`);
|
|
421
|
+
out.log(`schema: ${schema ? `${entities.length} entities` : "none"}`);
|
|
422
|
+
out.log(`rules: ${rules ? `${Object.keys(rules).length} namespaces` : "none"}`);
|
|
423
|
+
out.log(`ai: ${cfg.ai?.provider ?? "not configured"}`);
|
|
424
|
+
const warnings = [];
|
|
425
|
+
if (schema && entities.length === 0) warnings.push("schema has no entities");
|
|
426
|
+
if (rules) {
|
|
427
|
+
for (const ns of Object.keys(rules)) {
|
|
428
|
+
if (entities.length > 0 && !entities.includes(ns)) warnings.push(`rules include "${ns}" but schema has no matching entity`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (cfg.services.includes("ai") && !cfg.ai?.provider) warnings.push("ai service is enabled but ai.provider is not set");
|
|
432
|
+
if (cfg.auth?.clerk) {
|
|
433
|
+
for (const [env, value] of Object.entries(cfg.auth.clerk)) {
|
|
434
|
+
if (typeof value === "string" && value.startsWith("$") && !process.env[value.slice(1)]) {
|
|
435
|
+
warnings.push(`auth.clerk.${env} references unset env var ${value}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (cfg.ai?.keyEnv && !process.env[cfg.ai.keyEnv]) warnings.push(`${cfg.ai.keyEnv} is not set; provision will skip provider key storage`);
|
|
440
|
+
if (cfg.services.includes("o11y")) {
|
|
441
|
+
const credentials = readCredentials(cfg.local.credentialsFile);
|
|
442
|
+
for (const env of cfg.envs) {
|
|
443
|
+
if (!credentials?.envs[env]?.o11yToken) {
|
|
444
|
+
warnings.push(`o11y is enabled but env "${env}" has no ingest token \u2014 run "odla-ai provision" to mint one`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
warnings.push(...lintRules(rules, entities, cfg.db?.publicRead ?? []));
|
|
449
|
+
warnings.push(...trackedSecretFiles(cfg.rootDir));
|
|
450
|
+
warnings.push(...wranglerWarnings(cfg.rootDir));
|
|
451
|
+
if (warnings.length) {
|
|
452
|
+
out.log("");
|
|
453
|
+
out.log("warnings:");
|
|
454
|
+
for (const warning of warnings) out.log(` - ${warning}`);
|
|
455
|
+
} else {
|
|
456
|
+
out.log("ok");
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
449
460
|
// src/init.ts
|
|
461
|
+
var import_node_fs5 = require("fs");
|
|
462
|
+
var import_node_path5 = require("path");
|
|
450
463
|
function initProject(options) {
|
|
451
464
|
const out = options.stdout ?? console;
|
|
452
465
|
const rootDir = (0, import_node_path5.resolve)(options.rootDir ?? process.cwd());
|
|
@@ -503,6 +516,12 @@ function configTemplate(input) {
|
|
|
503
516
|
// prod: "$CLERK_PUBLISHABLE_KEY",
|
|
504
517
|
},
|
|
505
518
|
},
|
|
519
|
+
// Add "o11y" to services to enable observability; provision then mints the
|
|
520
|
+
// ingest token and scaffolds the ODLA_O11Y_* vars into .dev.vars.
|
|
521
|
+
// o11y: {
|
|
522
|
+
// service: "${input.appId}", // defaults to the app id
|
|
523
|
+
// // endpoint: "https://o11y.odla.ai",
|
|
524
|
+
// },
|
|
506
525
|
links: {
|
|
507
526
|
// dev: "https://dev.example.com",
|
|
508
527
|
// prod: "https://example.com",
|
|
@@ -703,6 +722,15 @@ async function provision(options) {
|
|
|
703
722
|
dbKey = await mintDbKey({ cfg, tenantId, env, token, auth, fetch: doFetch });
|
|
704
723
|
out.log(`${env}: minted db key for ${tenantId}`);
|
|
705
724
|
}
|
|
725
|
+
let o11yToken = cfg.services.includes("o11y") && !options.rotateKeys ? credentials?.envs[env]?.o11yToken : void 0;
|
|
726
|
+
if (cfg.services.includes("o11y")) {
|
|
727
|
+
if (o11yToken) {
|
|
728
|
+
out.log(`${env}: reusing local o11y ingest token`);
|
|
729
|
+
} else {
|
|
730
|
+
o11yToken = await mintO11yToken(cfg, env, token, doFetch);
|
|
731
|
+
out.log(`${env}: minted o11y ingest token`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
706
734
|
if (schema) {
|
|
707
735
|
await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
|
|
708
736
|
out.log(`${env}: schema pushed`);
|
|
@@ -727,7 +755,8 @@ async function provision(options) {
|
|
|
727
755
|
dbEndpoint: cfg.dbEndpoint,
|
|
728
756
|
env,
|
|
729
757
|
tenantId,
|
|
730
|
-
dbKey
|
|
758
|
+
dbKey,
|
|
759
|
+
...o11yToken ? { o11yToken } : {}
|
|
731
760
|
});
|
|
732
761
|
}
|
|
733
762
|
if (cfg.local.gitignore) ensureGitignore(cfg.rootDir);
|
|
@@ -738,10 +767,18 @@ async function provision(options) {
|
|
|
738
767
|
const devVarsTarget = resolveWriteDevVarsTarget(cfg, options.writeDevVars);
|
|
739
768
|
if (devVarsTarget && credentials) {
|
|
740
769
|
const env = cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
|
|
741
|
-
writeDevVars(devVarsTarget, credentials, env);
|
|
770
|
+
writeDevVars(devVarsTarget, credentials, env, o11yDevVars(cfg));
|
|
742
771
|
out.log(`dev vars: wrote ${displayPath(devVarsTarget, cfg.rootDir)} for ${env}`);
|
|
743
772
|
}
|
|
744
773
|
}
|
|
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
|
+
}
|
|
745
782
|
async function readRegistryApp(cfg, token, doFetch) {
|
|
746
783
|
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}`, {
|
|
747
784
|
headers: { authorization: `Bearer ${token}` }
|
|
@@ -778,6 +815,16 @@ async function mintDbKey(opts) {
|
|
|
778
815
|
if (!body.key) throw new Error(`db key mint (${opts.tenantId}) returned no key`);
|
|
779
816
|
return body.key;
|
|
780
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
|
+
}
|
|
781
828
|
async function postJson(doFetch, url, bearer, body) {
|
|
782
829
|
const res = await doFetch(url, {
|
|
783
830
|
method: "POST",
|
|
@@ -840,19 +887,24 @@ async function secretsPush(options) {
|
|
|
840
887
|
}
|
|
841
888
|
const wranglerEnv = PROD_ENV_NAMES.has(env) ? void 0 : env;
|
|
842
889
|
const target = wranglerEnv ? `wrangler env "${wranglerEnv}"` : "the top-level (prod) wrangler env";
|
|
890
|
+
const secrets = [{ name: "ODLA_API_KEY", value: dbKey }];
|
|
891
|
+
const o11yToken = credentials.envs[env]?.o11yToken;
|
|
892
|
+
if (o11yToken) secrets.push({ name: "ODLA_O11Y_TOKEN", value: o11yToken });
|
|
843
893
|
if (options.dryRun) {
|
|
844
|
-
out.log(`dry run: would push
|
|
894
|
+
for (const s of secrets) out.log(`dry run: would push ${s.name} (${redactSecrets(s.value)}) to ${target}`);
|
|
845
895
|
return;
|
|
846
896
|
}
|
|
847
897
|
const run = options.runner ?? defaultRunner;
|
|
848
898
|
if (!await wranglerLoggedIn(run, cfg.rootDir)) {
|
|
849
899
|
throw new Error(`wrangler is not logged in \u2014 run "wrangler login" (a browser step for the human)`);
|
|
850
900
|
}
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
|
|
901
|
+
for (const s of secrets) {
|
|
902
|
+
const result = await wranglerPutSecret(run, { name: s.name, value: s.value, env: wranglerEnv, cwd: cfg.rootDir });
|
|
903
|
+
if (result.code !== 0) {
|
|
904
|
+
throw new Error(`wrangler secret put ${s.name} failed (exit ${result.code}): ${redactSecrets(`${result.stderr || result.stdout}`.trim())}`);
|
|
905
|
+
}
|
|
906
|
+
out.log(`${s.name} pushed to ${target} (value read from ${credentialsPath}, never echoed)`);
|
|
854
907
|
}
|
|
855
|
-
out.log(`ODLA_API_KEY pushed to ${target} (value read from ${credentialsPath}, never echoed)`);
|
|
856
908
|
}
|
|
857
909
|
|
|
858
910
|
// src/skill.ts
|
|
@@ -1046,6 +1098,17 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
1046
1098
|
});
|
|
1047
1099
|
return;
|
|
1048
1100
|
}
|
|
1101
|
+
if (command === "setup") {
|
|
1102
|
+
installSkill({
|
|
1103
|
+
dir: stringOpt(parsed.options.dir),
|
|
1104
|
+
global: parsed.options.global === true,
|
|
1105
|
+
force: parsed.options.force === true
|
|
1106
|
+
});
|
|
1107
|
+
console.log(
|
|
1108
|
+
"\nSkills installed. Point your coding agent at this repo \u2014 the `odla` skill orients it\n(build a new app, or migrate an existing site) and drives `odla-ai init` / `provision`."
|
|
1109
|
+
);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1049
1112
|
if (command === "skill") {
|
|
1050
1113
|
const sub = parsed.positionals[1];
|
|
1051
1114
|
if (sub !== "install") throw new Error(`unknown skill subcommand "${sub ?? ""}". Try "odla-ai skill install".`);
|
|
@@ -1132,6 +1195,7 @@ function printHelp() {
|
|
|
1132
1195
|
console.log(`odla-ai
|
|
1133
1196
|
|
|
1134
1197
|
Usage:
|
|
1198
|
+
odla-ai setup [--dir <project>] [--global] [--force]
|
|
1135
1199
|
odla-ai init --app-id <id> --name <name> [--services db,ai] [--env dev --env prod]
|
|
1136
1200
|
odla-ai doctor [--config odla.config.mjs]
|
|
1137
1201
|
odla-ai provision [--config odla.config.mjs] [--dry-run] [--open|--no-open] [--rotate-keys] [--write-dev-vars[=path]]
|
|
@@ -1141,6 +1205,7 @@ Usage:
|
|
|
1141
1205
|
odla-ai version
|
|
1142
1206
|
|
|
1143
1207
|
Commands:
|
|
1208
|
+
setup Install the odla skills so a coding agent can drive the rest.
|
|
1144
1209
|
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
1145
1210
|
doctor Validate and summarize the project config without network calls.
|
|
1146
1211
|
provision Register the app, enable services, push schema/rules, configure AI/auth.
|