@odla-ai/cli 0.1.0 → 0.2.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.
@@ -1,5 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/redact.ts
4
+ var REPLACEMENTS = [
5
+ [/odla_(sk|dev)_[A-Za-z0-9._-]+/g, "odla_$1_[redacted]"],
6
+ [/\bsk_(live|test)_[A-Za-z0-9]+/g, "sk_$1_[redacted]"],
7
+ [/\bsk-[A-Za-z0-9._-]+/g, "sk-[redacted]"],
8
+ [/\bwhsec_[A-Za-z0-9+/=]+/g, "whsec_[redacted]"],
9
+ [/\b(ghp|gho|github_pat)_[A-Za-z0-9_]+/g, "$1_[redacted]"],
10
+ [/\bAKIA[A-Z0-9]{12,}/g, "AKIA[redacted]"]
11
+ ];
12
+ function redactSecrets(value) {
13
+ let result = value;
14
+ for (const [pattern, replacement] of REPLACEMENTS) result = result.replace(pattern, replacement);
15
+ return result;
16
+ }
17
+ function looksSecret(value) {
18
+ return redactSecrets(value) !== value || value.includes("-----BEGIN");
19
+ }
20
+
3
21
  // src/config.ts
4
22
  import { existsSync, readFileSync } from "fs";
5
23
  import { dirname, isAbsolute, resolve } from "path";
@@ -105,6 +123,163 @@ function unique(values) {
105
123
  return [...new Set(values.filter(Boolean))];
106
124
  }
107
125
 
126
+ // src/doctor-checks.ts
127
+ import { execFileSync } from "child_process";
128
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
129
+ import { join as join2, resolve as resolve2 } from "path";
130
+
131
+ // src/wrangler.ts
132
+ import { spawn } from "child_process";
133
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
134
+ import { join } from "path";
135
+ var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
136
+ const child = spawn(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
137
+ let stdout = "";
138
+ let stderr = "";
139
+ child.stdout.on("data", (chunk) => stdout += chunk.toString());
140
+ child.stderr.on("data", (chunk) => stderr += chunk.toString());
141
+ child.on("error", reject);
142
+ child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout, stderr }));
143
+ child.stdin.end(opts?.input ?? "");
144
+ });
145
+ var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
146
+ function findWranglerConfig(rootDir) {
147
+ for (const name of WRANGLER_CONFIG_FILES) {
148
+ const path = join(rootDir, name);
149
+ if (existsSync2(path)) return path;
150
+ }
151
+ return null;
152
+ }
153
+ function readWranglerConfig(path) {
154
+ if (path.endsWith(".toml")) return null;
155
+ try {
156
+ return JSON.parse(stripJsonComments(readFileSync2(path, "utf8")));
157
+ } catch {
158
+ return null;
159
+ }
160
+ }
161
+ function stripJsonComments(text) {
162
+ let result = "";
163
+ let inString = false;
164
+ for (let i = 0; i < text.length; i++) {
165
+ const ch = text[i];
166
+ if (inString) {
167
+ result += ch;
168
+ if (ch === "\\") {
169
+ result += text[i + 1] ?? "";
170
+ i++;
171
+ } else if (ch === '"') {
172
+ inString = false;
173
+ }
174
+ continue;
175
+ }
176
+ if (ch === '"') {
177
+ inString = true;
178
+ result += ch;
179
+ continue;
180
+ }
181
+ if (ch === "/" && text[i + 1] === "/") {
182
+ while (i < text.length && text[i] !== "\n") i++;
183
+ result += "\n";
184
+ continue;
185
+ }
186
+ if (ch === "/" && text[i + 1] === "*") {
187
+ i += 2;
188
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
189
+ i++;
190
+ continue;
191
+ }
192
+ result += ch;
193
+ }
194
+ return result;
195
+ }
196
+ async function wranglerLoggedIn(run, cwd) {
197
+ try {
198
+ const result = await run("npx", ["wrangler", "whoami"], { cwd });
199
+ return result.code === 0 && !/not authenticated/i.test(`${result.stdout}${result.stderr}`);
200
+ } catch {
201
+ return false;
202
+ }
203
+ }
204
+ function wranglerPutSecret(run, opts) {
205
+ const args = ["wrangler", "secret", "put", opts.name, ...opts.env ? ["--env", opts.env] : []];
206
+ return run("npx", args, { input: opts.value, cwd: opts.cwd });
207
+ }
208
+
209
+ // src/doctor-checks.ts
210
+ function lintRules(rules, entities, publicRead) {
211
+ const warnings = [];
212
+ if (!rules) return warnings;
213
+ for (const [ns, actions] of Object.entries(rules)) {
214
+ for (const [action, expr] of Object.entries(actions)) {
215
+ if (typeof expr !== "string" || expr.trim() !== "true") continue;
216
+ if (action === "view" && publicRead.includes(ns)) continue;
217
+ warnings.push(
218
+ action === "view" ? `rules.${ns}.view is "true" \u2014 public read; add "${ns}" to db.publicRead if intended` : `rules.${ns}.${action} is "true" \u2014 any client can ${action} ${ns} rows`
219
+ );
220
+ }
221
+ }
222
+ for (const entity of entities) {
223
+ if (!(entity in rules)) warnings.push(`schema entity "${entity}" has no rules entry (all client access denied)`);
224
+ }
225
+ return warnings;
226
+ }
227
+ var defaultExec = (cmd, args, cwd) => execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
228
+ function trackedSecretFiles(rootDir, exec = defaultExec) {
229
+ let output;
230
+ try {
231
+ output = exec("git", ["ls-files", "--", ".dev.vars", ".odla"], rootDir);
232
+ } catch {
233
+ return [];
234
+ }
235
+ return output.split(/\r?\n/).filter(Boolean).map((path) => `${path} is tracked by git \u2014 run "git rm --cached ${path}" and commit; it belongs in .gitignore`);
236
+ }
237
+ function wranglerWarnings(rootDir) {
238
+ const configPath = findWranglerConfig(rootDir);
239
+ if (!configPath) return [];
240
+ const config = readWranglerConfig(configPath);
241
+ if (!config) return [];
242
+ const warnings = [];
243
+ const blocks = [{ label: "", block: config }];
244
+ const envs = config.env;
245
+ if (envs && typeof envs === "object") {
246
+ for (const [name, block] of Object.entries(envs)) {
247
+ if (block && typeof block === "object") blocks.push({ label: `env.${name}.`, block });
248
+ }
249
+ }
250
+ for (const { label, block } of blocks) {
251
+ const assets = block.assets;
252
+ if (assets?.directory) {
253
+ const dir = resolve2(rootDir, assets.directory);
254
+ if (dir === resolve2(rootDir)) {
255
+ warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
256
+ } else if (existsSync3(join2(dir, "node_modules"))) {
257
+ warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
258
+ }
259
+ }
260
+ const vars = block.vars;
261
+ if (vars && typeof vars === "object") {
262
+ for (const [name, value] of Object.entries(vars)) {
263
+ if (name === "ODLA_API_KEY" || typeof value === "string" && looksSecret(value)) {
264
+ warnings.push(`wrangler ${label}vars.${name} looks like a secret \u2014 use "odla-ai secrets push" / wrangler secret put, never vars`);
265
+ }
266
+ }
267
+ }
268
+ }
269
+ const pkg = readPackageJson(rootDir);
270
+ if (pkg && typeof pkg.scripts === "object" && pkg.scripts && "deploy" in pkg.scripts) {
271
+ warnings.push(`package.json has a script named "deploy" \u2014 CI may auto-deploy it; rename to deploy:app`);
272
+ }
273
+ return warnings;
274
+ }
275
+ function readPackageJson(rootDir) {
276
+ try {
277
+ return JSON.parse(readFileSync3(join2(rootDir, "package.json"), "utf8"));
278
+ } catch {
279
+ return null;
280
+ }
281
+ }
282
+
108
283
  // src/doctor.ts
109
284
  async function doctor(options) {
110
285
  const out = options.stdout ?? console;
@@ -137,6 +312,9 @@ async function doctor(options) {
137
312
  }
138
313
  }
139
314
  if (cfg.ai?.keyEnv && !process.env[cfg.ai.keyEnv]) warnings.push(`${cfg.ai.keyEnv} is not set; provision will skip provider key storage`);
315
+ warnings.push(...lintRules(rules, entities, cfg.db?.publicRead ?? []));
316
+ warnings.push(...trackedSecretFiles(cfg.rootDir));
317
+ warnings.push(...wranglerWarnings(cfg.rootDir));
140
318
  if (warnings.length) {
141
319
  out.log("");
142
320
  out.log("warnings:");
@@ -147,16 +325,16 @@ async function doctor(options) {
147
325
  }
148
326
 
149
327
  // src/init.ts
150
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
151
- import { dirname as dirname3, resolve as resolve3 } from "path";
328
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
329
+ import { dirname as dirname3, resolve as resolve4 } from "path";
152
330
 
153
331
  // src/local.ts
154
- import { chmodSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
155
- import { dirname as dirname2, relative, resolve as resolve2 } from "path";
332
+ import { chmodSync, existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "fs";
333
+ import { dirname as dirname2, relative, resolve as resolve3 } from "path";
156
334
  var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
157
335
  function readJsonFile(path) {
158
336
  try {
159
- return JSON.parse(readFileSync2(path, "utf8"));
337
+ return JSON.parse(readFileSync4(path, "utf8"));
160
338
  } catch {
161
339
  return null;
162
340
  }
@@ -190,8 +368,8 @@ function mergeCredential(current, update) {
190
368
  return next;
191
369
  }
192
370
  function ensureGitignore(rootDir) {
193
- const path = resolve2(rootDir, ".gitignore");
194
- const existing = existsSync2(path) ? readFileSync2(path, "utf8") : "";
371
+ const path = resolve3(rootDir, ".gitignore");
372
+ const existing = existsSync4(path) ? readFileSync4(path, "utf8") : "";
195
373
  const missing = GITIGNORE_LINES.filter((line) => !existing.split(/\r?\n/).includes(line));
196
374
  if (missing.length === 0) return;
197
375
  const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
@@ -222,9 +400,9 @@ function displayPath(path, rootDir = process.cwd()) {
222
400
  // src/init.ts
223
401
  function initProject(options) {
224
402
  const out = options.stdout ?? console;
225
- const rootDir = resolve3(options.rootDir ?? process.cwd());
226
- const configPath = resolve3(rootDir, options.configPath ?? "odla.config.mjs");
227
- if (existsSync3(configPath) && !options.force) {
403
+ const rootDir = resolve4(options.rootDir ?? process.cwd());
404
+ const configPath = resolve4(rootDir, options.configPath ?? "odla.config.mjs");
405
+ if (existsSync5(configPath) && !options.force) {
228
406
  throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
229
407
  }
230
408
  if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
@@ -234,18 +412,18 @@ function initProject(options) {
234
412
  const services = options.services?.length ? options.services : ["db", "ai"];
235
413
  const aiProvider = options.aiProvider ?? "anthropic";
236
414
  mkdirSync2(dirname3(configPath), { recursive: true });
237
- mkdirSync2(resolve3(rootDir, "src/odla"), { recursive: true });
238
- mkdirSync2(resolve3(rootDir, ".odla"), { recursive: true });
415
+ mkdirSync2(resolve4(rootDir, "src/odla"), { recursive: true });
416
+ mkdirSync2(resolve4(rootDir, ".odla"), { recursive: true });
239
417
  writeFileSync2(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
240
- writeIfMissing(resolve3(rootDir, "src/odla/schema.mjs"), schemaTemplate());
241
- writeIfMissing(resolve3(rootDir, "src/odla/rules.mjs"), rulesTemplate());
418
+ writeIfMissing(resolve4(rootDir, "src/odla/schema.mjs"), schemaTemplate());
419
+ writeIfMissing(resolve4(rootDir, "src/odla/rules.mjs"), rulesTemplate());
242
420
  ensureGitignore(rootDir);
243
421
  out.log(`created ${relativeDisplay(configPath, rootDir)}`);
244
422
  out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
245
423
  out.log("updated .gitignore for local odla credentials");
246
424
  }
247
425
  function writeIfMissing(path, text) {
248
- if (existsSync3(path)) return;
426
+ if (existsSync5(path)) return;
249
427
  writeFileSync2(path, text);
250
428
  }
251
429
  function configTemplate(input) {
@@ -329,7 +507,7 @@ function relativeDisplay(path, rootDir) {
329
507
  // src/provision.ts
330
508
  import { createAppsClient, tenantIdFor } from "@odla-ai/apps";
331
509
  import { DEFAULT_SECRET_NAMES, putSecret } from "@odla-ai/ai";
332
- import { dirname as dirname4, resolve as resolve4 } from "path";
510
+ import { dirname as dirname4, resolve as resolve5 } from "path";
333
511
  import process4 from "process";
334
512
 
335
513
  // src/token.ts
@@ -337,12 +515,12 @@ import { requestToken } from "@odla-ai/db";
337
515
  import process3 from "process";
338
516
 
339
517
  // src/open.ts
340
- import { spawn } from "child_process";
518
+ import { spawn as spawn2 } from "child_process";
341
519
  import process2 from "process";
342
520
  async function openUrl(url, options = {}) {
343
521
  const command = openerFor(options.platform ?? process2.platform);
344
- const doSpawn = options.spawnImpl ?? spawn;
345
- await new Promise((resolve5, reject) => {
522
+ const doSpawn = options.spawnImpl ?? spawn2;
523
+ await new Promise((resolve7, reject) => {
346
524
  const child = doSpawn(command.cmd, [...command.args, url], {
347
525
  stdio: "ignore",
348
526
  detached: true
@@ -350,7 +528,7 @@ async function openUrl(url, options = {}) {
350
528
  child.once("error", reject);
351
529
  child.once("spawn", () => {
352
530
  child.unref();
353
- resolve5();
531
+ resolve7();
354
532
  });
355
533
  });
356
534
  }
@@ -577,17 +755,115 @@ function defaultSecretName(provider) {
577
755
  function resolveWriteDevVarsTarget(cfg, requested) {
578
756
  if (!requested) return null;
579
757
  if (requested === true) return cfg.local.devVarsFile;
580
- return resolve4(dirname4(cfg.configPath), requested);
758
+ return resolve5(dirname4(cfg.configPath), requested);
581
759
  }
582
760
  async function safeText(res) {
583
761
  try {
584
- return redact((await res.text()).slice(0, 500));
762
+ return redactSecrets((await res.text()).slice(0, 500));
585
763
  } catch {
586
764
  return "";
587
765
  }
588
766
  }
589
- function redact(value) {
590
- return value.replace(/odla_(sk|dev)_[A-Za-z0-9._-]+/g, "odla_$1_[redacted]").replace(/sk-[A-Za-z0-9._-]+/g, "sk-[redacted]");
767
+
768
+ // src/secrets.ts
769
+ var PROD_ENV_NAMES = /* @__PURE__ */ new Set(["prod", "production"]);
770
+ async function secretsPush(options) {
771
+ const out = options.stdout ?? console;
772
+ const cfg = await loadProjectConfig(options.configPath);
773
+ const env = options.env;
774
+ if (!cfg.envs.includes(env)) {
775
+ throw new Error(`env "${env}" is not in config envs (${cfg.envs.join(", ")})`);
776
+ }
777
+ if (PROD_ENV_NAMES.has(env) && !options.yes) {
778
+ throw new Error(`refusing to push a secret to "${env}" without --yes`);
779
+ }
780
+ const credentialsPath = displayPath(cfg.local.credentialsFile, cfg.rootDir);
781
+ const credentials = readCredentials(cfg.local.credentialsFile);
782
+ if (!credentials) throw new Error(`no credentials at ${credentialsPath} \u2014 run "odla-ai provision" first`);
783
+ if (credentials.appId !== cfg.app.id) {
784
+ throw new Error(`credentials at ${credentialsPath} are for "${credentials.appId}", not "${cfg.app.id}"`);
785
+ }
786
+ const dbKey = credentials.envs[env]?.dbKey;
787
+ if (!dbKey) throw new Error(`no db key for env "${env}" in ${credentialsPath} \u2014 run "odla-ai provision" first`);
788
+ const wranglerConfig = findWranglerConfig(cfg.rootDir);
789
+ if (!wranglerConfig) {
790
+ throw new Error(`no wrangler config found in ${cfg.rootDir} (wrangler.jsonc, wrangler.json, or wrangler.toml)`);
791
+ }
792
+ const wranglerEnv = PROD_ENV_NAMES.has(env) ? void 0 : env;
793
+ const target = wranglerEnv ? `wrangler env "${wranglerEnv}"` : "the top-level (prod) wrangler env";
794
+ if (options.dryRun) {
795
+ out.log(`dry run: would push ODLA_API_KEY (${redactSecrets(dbKey)}) to ${target}`);
796
+ return;
797
+ }
798
+ const run = options.runner ?? defaultRunner;
799
+ if (!await wranglerLoggedIn(run, cfg.rootDir)) {
800
+ throw new Error(`wrangler is not logged in \u2014 run "wrangler login" (a browser step for the human)`);
801
+ }
802
+ const result = await wranglerPutSecret(run, { name: "ODLA_API_KEY", value: dbKey, env: wranglerEnv, cwd: cfg.rootDir });
803
+ if (result.code !== 0) {
804
+ throw new Error(`wrangler secret put failed (exit ${result.code}): ${redactSecrets(`${result.stderr || result.stdout}`.trim())}`);
805
+ }
806
+ out.log(`ODLA_API_KEY pushed to ${target} (value read from ${credentialsPath}, never echoed)`);
807
+ }
808
+
809
+ // src/skill.ts
810
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, readdirSync, writeFileSync as writeFileSync3 } from "fs";
811
+ import { homedir } from "os";
812
+ import { join as join3, relative as relative2, resolve as resolve6 } from "path";
813
+ import { fileURLToPath } from "url";
814
+ function installSkill(options = {}) {
815
+ const out = options.stdout ?? console;
816
+ const sourceDir = options.sourceDir ?? fileURLToPath(new URL("../skills", import.meta.url));
817
+ const targetDir = options.global ? join3(options.homeDir ?? homedir(), ".claude", "skills") : resolve6(options.dir ?? process.cwd(), ".claude", "skills");
818
+ const files = listFiles(sourceDir);
819
+ if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
820
+ const written = [];
821
+ const unchanged = [];
822
+ const conflicts = [];
823
+ for (const rel of files) {
824
+ const target = join3(targetDir, rel);
825
+ const source = readFileSync5(join3(sourceDir, rel), "utf8");
826
+ if (existsSync6(target)) {
827
+ const current = readFileSync5(target, "utf8");
828
+ if (current === source) {
829
+ unchanged.push(rel);
830
+ continue;
831
+ }
832
+ if (!options.force) {
833
+ conflicts.push(rel);
834
+ continue;
835
+ }
836
+ }
837
+ written.push(rel);
838
+ }
839
+ if (conflicts.length > 0) {
840
+ throw new Error(
841
+ `skill files modified locally (re-run with --force to overwrite):
842
+ ${conflicts.map((f) => ` - ${join3(targetDir, f)}`).join("\n")}`
843
+ );
844
+ }
845
+ for (const rel of written) {
846
+ const target = join3(targetDir, rel);
847
+ mkdirSync3(join3(target, ".."), { recursive: true });
848
+ writeFileSync3(target, readFileSync5(join3(sourceDir, rel), "utf8"));
849
+ }
850
+ const skills = [...new Set(files.map((f) => f.split(/[\\/]/)[0]))].sort();
851
+ out.log(`skills: ${skills.join(", ")}`);
852
+ out.log(`installed ${written.length} file(s) to ${targetDir}${unchanged.length ? ` (${unchanged.length} unchanged)` : ""}`);
853
+ return { targetDir, written, unchanged };
854
+ }
855
+ function listFiles(dir) {
856
+ if (!existsSync6(dir)) return [];
857
+ const results = [];
858
+ const walk = (current) => {
859
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
860
+ const path = join3(current, entry.name);
861
+ if (entry.isDirectory()) walk(path);
862
+ else results.push(relative2(dir, path));
863
+ }
864
+ };
865
+ walk(dir);
866
+ return results.sort();
591
867
  }
592
868
 
593
869
  // src/smoke.ts
@@ -672,7 +948,7 @@ async function safeText2(res) {
672
948
  }
673
949
 
674
950
  // src/cli.ts
675
- import { readFileSync as readFileSync3 } from "fs";
951
+ import { readFileSync as readFileSync6 } from "fs";
676
952
  async function runCli(argv = process.argv.slice(2)) {
677
953
  const parsed = parseArgv(argv);
678
954
  const command = parsed.positionals[0] ?? "help";
@@ -722,6 +998,27 @@ async function runCli(argv = process.argv.slice(2)) {
722
998
  });
723
999
  return;
724
1000
  }
1001
+ if (command === "skill") {
1002
+ const sub = parsed.positionals[1];
1003
+ if (sub !== "install") throw new Error(`unknown skill subcommand "${sub ?? ""}". Try "odla-ai skill install".`);
1004
+ installSkill({
1005
+ dir: stringOpt(parsed.options.dir),
1006
+ global: parsed.options.global === true,
1007
+ force: parsed.options.force === true
1008
+ });
1009
+ return;
1010
+ }
1011
+ if (command === "secrets") {
1012
+ const sub = parsed.positionals[1];
1013
+ if (sub !== "push") throw new Error(`unknown secrets subcommand "${sub ?? ""}". Try "odla-ai secrets push --env dev".`);
1014
+ await secretsPush({
1015
+ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
1016
+ env: requiredString(parsed.options.env, "--env"),
1017
+ dryRun: parsed.options["dry-run"] === true,
1018
+ yes: parsed.options.yes === true
1019
+ });
1020
+ return;
1021
+ }
725
1022
  throw new Error(`unknown command "${command}". Run "odla-ai help".`);
726
1023
  }
727
1024
  function parseArgv(argv) {
@@ -780,7 +1077,7 @@ function listOpt(value) {
780
1077
  return values.flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
781
1078
  }
782
1079
  function cliVersion() {
783
- const pkg = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8"));
1080
+ const pkg = JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf8"));
784
1081
  return pkg.version ?? "unknown";
785
1082
  }
786
1083
  function printHelp() {
@@ -791,6 +1088,8 @@ Usage:
791
1088
  odla-ai doctor [--config odla.config.mjs]
792
1089
  odla-ai provision [--config odla.config.mjs] [--dry-run] [--open|--no-open] [--rotate-keys] [--write-dev-vars[=path]]
793
1090
  odla-ai smoke [--config odla.config.mjs] [--env dev]
1091
+ odla-ai skill install [--dir <project>] [--global] [--force]
1092
+ odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
794
1093
  odla-ai version
795
1094
 
796
1095
  Commands:
@@ -798,6 +1097,8 @@ Commands:
798
1097
  doctor Validate and summarize the project config without network calls.
799
1098
  provision Register the app, enable services, push schema/rules, configure AI/auth.
800
1099
  smoke Verify local credentials, public-config, live schema, and db aggregate.
1100
+ skill Install the bundled Claude Code skills into .claude/skills/.
1101
+ secrets Push the env's db key into the Worker via wrangler, stdin-piped.
801
1102
  version Print the CLI version.
802
1103
 
803
1104
  Safety:
@@ -809,10 +1110,13 @@ Safety:
809
1110
  }
810
1111
 
811
1112
  export {
1113
+ redactSecrets,
812
1114
  doctor,
813
1115
  initProject,
814
1116
  provision,
1117
+ secretsPush,
1118
+ installSkill,
815
1119
  smoke,
816
1120
  runCli
817
1121
  };
818
- //# sourceMappingURL=chunk-AXCZKIVY.js.map
1122
+ //# sourceMappingURL=chunk-5J4LKP37.js.map