@odla-ai/cli 0.2.0 → 0.3.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/LICENSE +21 -0
- package/README.md +9 -1
- package/dist/bin.cjs +110 -58
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-5J4LKP37.js → chunk-ZU6QJXRQ.js} +111 -59
- package/dist/chunk-ZU6QJXRQ.js.map +1 -0
- package/dist/index.cjs +110 -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 +19 -2
- package/package.json +1 -1
- package/skills/odla-migrate/SKILL.md +4 -2
- 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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cory Ondrejka
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# @odla-ai/cli
|
|
2
2
|
|
|
3
|
+
> ⚠️ **Experimental — an agentic-coding experiment.** This package is built and
|
|
4
|
+
> operated by autonomous coding agents as an experiment in agentic loops. APIs
|
|
5
|
+
> change without notice and nothing here is production-hardened. **Use at your
|
|
6
|
+
> own risk.**
|
|
7
|
+
|
|
3
8
|
Project-neutral provisioning CLI for odla apps. It creates and validates an
|
|
4
9
|
`odla.config.mjs`, then uses that config to register an app, enable services,
|
|
5
10
|
push odla-db schema/rules, configure platform AI, configure Clerk auth, record
|
|
@@ -96,7 +101,10 @@ wrangler) and requires `--yes`; every other env maps to wrangler's
|
|
|
96
101
|
(currently `odla-migrate`, the phased GitHub Pages → odla migration
|
|
97
102
|
procedure) into the project's `.claude/skills/`, or into `~/.claude/skills/`
|
|
98
103
|
with `--global`. Re-runs are idempotent; a locally modified skill file is
|
|
99
|
-
never overwritten without `--force`.
|
|
104
|
+
never overwritten without `--force`. The skill's reference runbook is served
|
|
105
|
+
at <https://odla.ai/llms-migrate.txt>; the human companion — "Moving your site
|
|
106
|
+
to odla" (accounts, the approval code, costs) — lives in the odla docs after
|
|
107
|
+
sign-in.
|
|
100
108
|
|
|
101
109
|
`doctor` also lints for common footguns: permission rules that are literally
|
|
102
110
|
`"true"` (public writes always warn; a public `view` warns unless the
|
package/dist/bin.cjs
CHANGED
|
@@ -146,6 +146,7 @@ var REPLACEMENTS = [
|
|
|
146
146
|
[/\bsk_(live|test)_[A-Za-z0-9]+/g, "sk_$1_[redacted]"],
|
|
147
147
|
[/\bsk-[A-Za-z0-9._-]+/g, "sk-[redacted]"],
|
|
148
148
|
[/\bwhsec_[A-Za-z0-9+/=]+/g, "whsec_[redacted]"],
|
|
149
|
+
[/\bo11y_[A-Za-z0-9]+/g, "o11y_[redacted]"],
|
|
149
150
|
[/\b(ghp|gho|github_pat)_[A-Za-z0-9_]+/g, "$1_[redacted]"],
|
|
150
151
|
[/\bAKIA[A-Z0-9]{12,}/g, "AKIA[redacted]"]
|
|
151
152
|
];
|
|
@@ -290,7 +291,7 @@ function wranglerWarnings(rootDir) {
|
|
|
290
291
|
const vars = block.vars;
|
|
291
292
|
if (vars && typeof vars === "object") {
|
|
292
293
|
for (const [name, value] of Object.entries(vars)) {
|
|
293
|
-
if (name === "ODLA_API_KEY" || typeof value === "string" && looksSecret(value)) {
|
|
294
|
+
if (name === "ODLA_API_KEY" || name === "ODLA_O11Y_TOKEN" || typeof value === "string" && looksSecret(value)) {
|
|
294
295
|
warnings.push(`wrangler ${label}vars.${name} looks like a secret \u2014 use "odla-ai secrets push" / wrangler secret put, never vars`);
|
|
295
296
|
}
|
|
296
297
|
}
|
|
@@ -310,54 +311,6 @@ function readPackageJson(rootDir) {
|
|
|
310
311
|
}
|
|
311
312
|
}
|
|
312
313
|
|
|
313
|
-
// src/doctor.ts
|
|
314
|
-
async function doctor(options) {
|
|
315
|
-
const out = options.stdout ?? console;
|
|
316
|
-
const cfg = await loadProjectConfig(options.configPath);
|
|
317
|
-
const plan = buildPlan(cfg);
|
|
318
|
-
const schema = await resolveDataExport(cfg, cfg.db?.schema, ["schema", "SCHEMA"]);
|
|
319
|
-
const configuredRules = await resolveDataExport(cfg, cfg.db?.rules, ["rules", "RULES"]);
|
|
320
|
-
const rules = configuredRules ?? (schema && cfg.db?.defaultRules !== false ? rulesFromSchema(schema) : void 0);
|
|
321
|
-
const entities = serializedEntities(schema);
|
|
322
|
-
out.log(`config: ${cfg.configPath}`);
|
|
323
|
-
out.log(`app: ${plan.appName} (${plan.appId})`);
|
|
324
|
-
out.log(`envs: ${plan.envs.join(", ")}`);
|
|
325
|
-
out.log(`svc: ${plan.services.join(", ")}`);
|
|
326
|
-
out.log(`schema: ${schema ? `${entities.length} entities` : "none"}`);
|
|
327
|
-
out.log(`rules: ${rules ? `${Object.keys(rules).length} namespaces` : "none"}`);
|
|
328
|
-
out.log(`ai: ${cfg.ai?.provider ?? "not configured"}`);
|
|
329
|
-
const warnings = [];
|
|
330
|
-
if (schema && entities.length === 0) warnings.push("schema has no entities");
|
|
331
|
-
if (rules) {
|
|
332
|
-
for (const ns of Object.keys(rules)) {
|
|
333
|
-
if (entities.length > 0 && !entities.includes(ns)) warnings.push(`rules include "${ns}" but schema has no matching entity`);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
if (cfg.services.includes("ai") && !cfg.ai?.provider) warnings.push("ai service is enabled but ai.provider is not set");
|
|
337
|
-
if (cfg.auth?.clerk) {
|
|
338
|
-
for (const [env, value] of Object.entries(cfg.auth.clerk)) {
|
|
339
|
-
if (typeof value === "string" && value.startsWith("$") && !process.env[value.slice(1)]) {
|
|
340
|
-
warnings.push(`auth.clerk.${env} references unset env var ${value}`);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
if (cfg.ai?.keyEnv && !process.env[cfg.ai.keyEnv]) warnings.push(`${cfg.ai.keyEnv} is not set; provision will skip provider key storage`);
|
|
345
|
-
warnings.push(...lintRules(rules, entities, cfg.db?.publicRead ?? []));
|
|
346
|
-
warnings.push(...trackedSecretFiles(cfg.rootDir));
|
|
347
|
-
warnings.push(...wranglerWarnings(cfg.rootDir));
|
|
348
|
-
if (warnings.length) {
|
|
349
|
-
out.log("");
|
|
350
|
-
out.log("warnings:");
|
|
351
|
-
for (const warning of warnings) out.log(` - ${warning}`);
|
|
352
|
-
} else {
|
|
353
|
-
out.log("ok");
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// src/init.ts
|
|
358
|
-
var import_node_fs5 = require("fs");
|
|
359
|
-
var import_node_path5 = require("path");
|
|
360
|
-
|
|
361
314
|
// src/local.ts
|
|
362
315
|
var import_node_fs4 = require("fs");
|
|
363
316
|
var import_node_path4 = require("path");
|
|
@@ -393,7 +346,8 @@ function mergeCredential(current, update) {
|
|
|
393
346
|
next.envs[update.env] = {
|
|
394
347
|
...next.envs[update.env] ?? {},
|
|
395
348
|
tenantId: update.tenantId,
|
|
396
|
-
...update.dbKey ? { dbKey: update.dbKey } : {}
|
|
349
|
+
...update.dbKey ? { dbKey: update.dbKey } : {},
|
|
350
|
+
...update.o11yToken ? { o11yToken: update.o11yToken } : {}
|
|
397
351
|
};
|
|
398
352
|
return next;
|
|
399
353
|
}
|
|
@@ -406,7 +360,7 @@ function ensureGitignore(rootDir) {
|
|
|
406
360
|
(0, import_node_fs4.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
|
|
407
361
|
`);
|
|
408
362
|
}
|
|
409
|
-
function writeDevVars(path, credentials, env) {
|
|
363
|
+
function writeDevVars(path, credentials, env, o11y) {
|
|
410
364
|
const entry = credentials.envs[env];
|
|
411
365
|
if (!entry?.dbKey) throw new Error(`no db key for env "${env}" in ${path}`);
|
|
412
366
|
const lines = [
|
|
@@ -417,6 +371,11 @@ function writeDevVars(path, credentials, env) {
|
|
|
417
371
|
`ODLA_TENANT="${entry.tenantId}"`,
|
|
418
372
|
`ODLA_API_KEY="${entry.dbKey}"`
|
|
419
373
|
];
|
|
374
|
+
if (o11y) {
|
|
375
|
+
lines.push(`ODLA_O11Y_ENDPOINT="${o11y.endpoint}"`, `ODLA_O11Y_SERVICE="${o11y.service}"`);
|
|
376
|
+
if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
|
|
377
|
+
if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
|
|
378
|
+
}
|
|
420
379
|
(0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(path), { recursive: true });
|
|
421
380
|
(0, import_node_fs4.writeFileSync)(path, `${lines.join("\n")}
|
|
422
381
|
`);
|
|
@@ -427,7 +386,61 @@ function displayPath(path, rootDir = process.cwd()) {
|
|
|
427
386
|
return rel && !rel.startsWith("..") ? rel : path;
|
|
428
387
|
}
|
|
429
388
|
|
|
389
|
+
// src/doctor.ts
|
|
390
|
+
async function doctor(options) {
|
|
391
|
+
const out = options.stdout ?? console;
|
|
392
|
+
const cfg = await loadProjectConfig(options.configPath);
|
|
393
|
+
const plan = buildPlan(cfg);
|
|
394
|
+
const schema = await resolveDataExport(cfg, cfg.db?.schema, ["schema", "SCHEMA"]);
|
|
395
|
+
const configuredRules = await resolveDataExport(cfg, cfg.db?.rules, ["rules", "RULES"]);
|
|
396
|
+
const rules = configuredRules ?? (schema && cfg.db?.defaultRules !== false ? rulesFromSchema(schema) : void 0);
|
|
397
|
+
const entities = serializedEntities(schema);
|
|
398
|
+
out.log(`config: ${cfg.configPath}`);
|
|
399
|
+
out.log(`app: ${plan.appName} (${plan.appId})`);
|
|
400
|
+
out.log(`envs: ${plan.envs.join(", ")}`);
|
|
401
|
+
out.log(`svc: ${plan.services.join(", ")}`);
|
|
402
|
+
out.log(`schema: ${schema ? `${entities.length} entities` : "none"}`);
|
|
403
|
+
out.log(`rules: ${rules ? `${Object.keys(rules).length} namespaces` : "none"}`);
|
|
404
|
+
out.log(`ai: ${cfg.ai?.provider ?? "not configured"}`);
|
|
405
|
+
const warnings = [];
|
|
406
|
+
if (schema && entities.length === 0) warnings.push("schema has no entities");
|
|
407
|
+
if (rules) {
|
|
408
|
+
for (const ns of Object.keys(rules)) {
|
|
409
|
+
if (entities.length > 0 && !entities.includes(ns)) warnings.push(`rules include "${ns}" but schema has no matching entity`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (cfg.services.includes("ai") && !cfg.ai?.provider) warnings.push("ai service is enabled but ai.provider is not set");
|
|
413
|
+
if (cfg.auth?.clerk) {
|
|
414
|
+
for (const [env, value] of Object.entries(cfg.auth.clerk)) {
|
|
415
|
+
if (typeof value === "string" && value.startsWith("$") && !process.env[value.slice(1)]) {
|
|
416
|
+
warnings.push(`auth.clerk.${env} references unset env var ${value}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (cfg.ai?.keyEnv && !process.env[cfg.ai.keyEnv]) warnings.push(`${cfg.ai.keyEnv} is not set; provision will skip provider key storage`);
|
|
421
|
+
if (cfg.services.includes("o11y")) {
|
|
422
|
+
const credentials = readCredentials(cfg.local.credentialsFile);
|
|
423
|
+
for (const env of cfg.envs) {
|
|
424
|
+
if (!credentials?.envs[env]?.o11yToken) {
|
|
425
|
+
warnings.push(`o11y is enabled but env "${env}" has no ingest token \u2014 run "odla-ai provision" to mint one`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
warnings.push(...lintRules(rules, entities, cfg.db?.publicRead ?? []));
|
|
430
|
+
warnings.push(...trackedSecretFiles(cfg.rootDir));
|
|
431
|
+
warnings.push(...wranglerWarnings(cfg.rootDir));
|
|
432
|
+
if (warnings.length) {
|
|
433
|
+
out.log("");
|
|
434
|
+
out.log("warnings:");
|
|
435
|
+
for (const warning of warnings) out.log(` - ${warning}`);
|
|
436
|
+
} else {
|
|
437
|
+
out.log("ok");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
430
441
|
// src/init.ts
|
|
442
|
+
var import_node_fs5 = require("fs");
|
|
443
|
+
var import_node_path5 = require("path");
|
|
431
444
|
function initProject(options) {
|
|
432
445
|
const out = options.stdout ?? console;
|
|
433
446
|
const rootDir = (0, import_node_path5.resolve)(options.rootDir ?? process.cwd());
|
|
@@ -484,6 +497,12 @@ function configTemplate(input) {
|
|
|
484
497
|
// prod: "$CLERK_PUBLISHABLE_KEY",
|
|
485
498
|
},
|
|
486
499
|
},
|
|
500
|
+
// Add "o11y" to services to enable observability; provision then mints the
|
|
501
|
+
// ingest token and scaffolds the ODLA_O11Y_* vars into .dev.vars.
|
|
502
|
+
// o11y: {
|
|
503
|
+
// service: "${input.appId}", // defaults to the app id
|
|
504
|
+
// // endpoint: "https://o11y.odla.ai",
|
|
505
|
+
// },
|
|
487
506
|
links: {
|
|
488
507
|
// dev: "https://dev.example.com",
|
|
489
508
|
// prod: "https://example.com",
|
|
@@ -684,6 +703,15 @@ async function provision(options) {
|
|
|
684
703
|
dbKey = await mintDbKey({ cfg, tenantId, env, token, auth, fetch: doFetch });
|
|
685
704
|
out.log(`${env}: minted db key for ${tenantId}`);
|
|
686
705
|
}
|
|
706
|
+
let o11yToken = cfg.services.includes("o11y") && !options.rotateKeys ? credentials?.envs[env]?.o11yToken : void 0;
|
|
707
|
+
if (cfg.services.includes("o11y")) {
|
|
708
|
+
if (o11yToken) {
|
|
709
|
+
out.log(`${env}: reusing local o11y ingest token`);
|
|
710
|
+
} else {
|
|
711
|
+
o11yToken = await mintO11yToken(cfg, env, token, doFetch);
|
|
712
|
+
out.log(`${env}: minted o11y ingest token`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
687
715
|
if (schema) {
|
|
688
716
|
await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
|
|
689
717
|
out.log(`${env}: schema pushed`);
|
|
@@ -708,7 +736,8 @@ async function provision(options) {
|
|
|
708
736
|
dbEndpoint: cfg.dbEndpoint,
|
|
709
737
|
env,
|
|
710
738
|
tenantId,
|
|
711
|
-
dbKey
|
|
739
|
+
dbKey,
|
|
740
|
+
...o11yToken ? { o11yToken } : {}
|
|
712
741
|
});
|
|
713
742
|
}
|
|
714
743
|
if (cfg.local.gitignore) ensureGitignore(cfg.rootDir);
|
|
@@ -719,10 +748,18 @@ async function provision(options) {
|
|
|
719
748
|
const devVarsTarget = resolveWriteDevVarsTarget(cfg, options.writeDevVars);
|
|
720
749
|
if (devVarsTarget && credentials) {
|
|
721
750
|
const env = cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
|
|
722
|
-
writeDevVars(devVarsTarget, credentials, env);
|
|
751
|
+
writeDevVars(devVarsTarget, credentials, env, o11yDevVars(cfg));
|
|
723
752
|
out.log(`dev vars: wrote ${displayPath(devVarsTarget, cfg.rootDir)} for ${env}`);
|
|
724
753
|
}
|
|
725
754
|
}
|
|
755
|
+
function o11yDevVars(cfg) {
|
|
756
|
+
if (!cfg.services.includes("o11y")) return void 0;
|
|
757
|
+
return {
|
|
758
|
+
endpoint: cfg.o11y?.endpoint ?? "https://o11y.odla.ai",
|
|
759
|
+
service: cfg.o11y?.service ?? cfg.app.id,
|
|
760
|
+
...cfg.o11y?.version ? { version: cfg.o11y.version } : {}
|
|
761
|
+
};
|
|
762
|
+
}
|
|
726
763
|
async function readRegistryApp(cfg, token, doFetch) {
|
|
727
764
|
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}`, {
|
|
728
765
|
headers: { authorization: `Bearer ${token}` }
|
|
@@ -759,6 +796,16 @@ async function mintDbKey(opts) {
|
|
|
759
796
|
if (!body.key) throw new Error(`db key mint (${opts.tenantId}) returned no key`);
|
|
760
797
|
return body.key;
|
|
761
798
|
}
|
|
799
|
+
async function mintO11yToken(cfg, env, token, doFetch) {
|
|
800
|
+
const res = await doFetch(
|
|
801
|
+
`${cfg.platformUrl}/o11y/${encodeURIComponent(cfg.app.id)}/token?env=${encodeURIComponent(env)}`,
|
|
802
|
+
{ method: "POST", headers: { authorization: `Bearer ${token}` } }
|
|
803
|
+
);
|
|
804
|
+
if (!res.ok) throw new Error(`o11y token mint (${env}) failed: ${res.status} ${await safeText(res)}`);
|
|
805
|
+
const body = await res.json();
|
|
806
|
+
if (!body.token) throw new Error(`o11y token mint (${env}) returned no token`);
|
|
807
|
+
return body.token;
|
|
808
|
+
}
|
|
762
809
|
async function postJson(doFetch, url, bearer, body) {
|
|
763
810
|
const res = await doFetch(url, {
|
|
764
811
|
method: "POST",
|
|
@@ -821,19 +868,24 @@ async function secretsPush(options) {
|
|
|
821
868
|
}
|
|
822
869
|
const wranglerEnv = PROD_ENV_NAMES.has(env) ? void 0 : env;
|
|
823
870
|
const target = wranglerEnv ? `wrangler env "${wranglerEnv}"` : "the top-level (prod) wrangler env";
|
|
871
|
+
const secrets = [{ name: "ODLA_API_KEY", value: dbKey }];
|
|
872
|
+
const o11yToken = credentials.envs[env]?.o11yToken;
|
|
873
|
+
if (o11yToken) secrets.push({ name: "ODLA_O11Y_TOKEN", value: o11yToken });
|
|
824
874
|
if (options.dryRun) {
|
|
825
|
-
out.log(`dry run: would push
|
|
875
|
+
for (const s of secrets) out.log(`dry run: would push ${s.name} (${redactSecrets(s.value)}) to ${target}`);
|
|
826
876
|
return;
|
|
827
877
|
}
|
|
828
878
|
const run = options.runner ?? defaultRunner;
|
|
829
879
|
if (!await wranglerLoggedIn(run, cfg.rootDir)) {
|
|
830
880
|
throw new Error(`wrangler is not logged in \u2014 run "wrangler login" (a browser step for the human)`);
|
|
831
881
|
}
|
|
832
|
-
const
|
|
833
|
-
|
|
834
|
-
|
|
882
|
+
for (const s of secrets) {
|
|
883
|
+
const result = await wranglerPutSecret(run, { name: s.name, value: s.value, env: wranglerEnv, cwd: cfg.rootDir });
|
|
884
|
+
if (result.code !== 0) {
|
|
885
|
+
throw new Error(`wrangler secret put ${s.name} failed (exit ${result.code}): ${redactSecrets(`${result.stderr || result.stdout}`.trim())}`);
|
|
886
|
+
}
|
|
887
|
+
out.log(`${s.name} pushed to ${target} (value read from ${credentialsPath}, never echoed)`);
|
|
835
888
|
}
|
|
836
|
-
out.log(`ODLA_API_KEY pushed to ${target} (value read from ${credentialsPath}, never echoed)`);
|
|
837
889
|
}
|
|
838
890
|
|
|
839
891
|
// src/skill.ts
|