@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.
package/dist/index.cjs CHANGED
@@ -33,8 +33,11 @@ var index_exports = {};
33
33
  __export(index_exports, {
34
34
  doctor: () => doctor,
35
35
  initProject: () => initProject,
36
+ installSkill: () => installSkill,
36
37
  provision: () => provision,
38
+ redactSecrets: () => redactSecrets,
37
39
  runCli: () => runCli,
40
+ secretsPush: () => secretsPush,
38
41
  smoke: () => smoke
39
42
  });
40
43
  module.exports = __toCommonJS(index_exports);
@@ -44,7 +47,7 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
44
47
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
45
48
 
46
49
  // src/cli.ts
47
- var import_node_fs4 = require("fs");
50
+ var import_node_fs7 = require("fs");
48
51
 
49
52
  // src/config.ts
50
53
  var import_node_fs = require("fs");
@@ -151,6 +154,181 @@ function unique(values) {
151
154
  return [...new Set(values.filter(Boolean))];
152
155
  }
153
156
 
157
+ // src/doctor-checks.ts
158
+ var import_node_child_process2 = require("child_process");
159
+ var import_node_fs3 = require("fs");
160
+ var import_node_path3 = require("path");
161
+
162
+ // src/redact.ts
163
+ var REPLACEMENTS = [
164
+ [/odla_(sk|dev)_[A-Za-z0-9._-]+/g, "odla_$1_[redacted]"],
165
+ [/\bsk_(live|test)_[A-Za-z0-9]+/g, "sk_$1_[redacted]"],
166
+ [/\bsk-[A-Za-z0-9._-]+/g, "sk-[redacted]"],
167
+ [/\bwhsec_[A-Za-z0-9+/=]+/g, "whsec_[redacted]"],
168
+ [/\b(ghp|gho|github_pat)_[A-Za-z0-9_]+/g, "$1_[redacted]"],
169
+ [/\bAKIA[A-Z0-9]{12,}/g, "AKIA[redacted]"]
170
+ ];
171
+ function redactSecrets(value) {
172
+ let result = value;
173
+ for (const [pattern, replacement] of REPLACEMENTS) result = result.replace(pattern, replacement);
174
+ return result;
175
+ }
176
+ function looksSecret(value) {
177
+ return redactSecrets(value) !== value || value.includes("-----BEGIN");
178
+ }
179
+
180
+ // src/wrangler.ts
181
+ var import_node_child_process = require("child_process");
182
+ var import_node_fs2 = require("fs");
183
+ var import_node_path2 = require("path");
184
+ var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
185
+ const child = (0, import_node_child_process.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
186
+ let stdout = "";
187
+ let stderr = "";
188
+ child.stdout.on("data", (chunk) => stdout += chunk.toString());
189
+ child.stderr.on("data", (chunk) => stderr += chunk.toString());
190
+ child.on("error", reject);
191
+ child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout, stderr }));
192
+ child.stdin.end(opts?.input ?? "");
193
+ });
194
+ var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
195
+ function findWranglerConfig(rootDir) {
196
+ for (const name of WRANGLER_CONFIG_FILES) {
197
+ const path = (0, import_node_path2.join)(rootDir, name);
198
+ if ((0, import_node_fs2.existsSync)(path)) return path;
199
+ }
200
+ return null;
201
+ }
202
+ function readWranglerConfig(path) {
203
+ if (path.endsWith(".toml")) return null;
204
+ try {
205
+ return JSON.parse(stripJsonComments((0, import_node_fs2.readFileSync)(path, "utf8")));
206
+ } catch {
207
+ return null;
208
+ }
209
+ }
210
+ function stripJsonComments(text) {
211
+ let result = "";
212
+ let inString = false;
213
+ for (let i = 0; i < text.length; i++) {
214
+ const ch = text[i];
215
+ if (inString) {
216
+ result += ch;
217
+ if (ch === "\\") {
218
+ result += text[i + 1] ?? "";
219
+ i++;
220
+ } else if (ch === '"') {
221
+ inString = false;
222
+ }
223
+ continue;
224
+ }
225
+ if (ch === '"') {
226
+ inString = true;
227
+ result += ch;
228
+ continue;
229
+ }
230
+ if (ch === "/" && text[i + 1] === "/") {
231
+ while (i < text.length && text[i] !== "\n") i++;
232
+ result += "\n";
233
+ continue;
234
+ }
235
+ if (ch === "/" && text[i + 1] === "*") {
236
+ i += 2;
237
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
238
+ i++;
239
+ continue;
240
+ }
241
+ result += ch;
242
+ }
243
+ return result;
244
+ }
245
+ async function wranglerLoggedIn(run, cwd) {
246
+ try {
247
+ const result = await run("npx", ["wrangler", "whoami"], { cwd });
248
+ return result.code === 0 && !/not authenticated/i.test(`${result.stdout}${result.stderr}`);
249
+ } catch {
250
+ return false;
251
+ }
252
+ }
253
+ function wranglerPutSecret(run, opts) {
254
+ const args = ["wrangler", "secret", "put", opts.name, ...opts.env ? ["--env", opts.env] : []];
255
+ return run("npx", args, { input: opts.value, cwd: opts.cwd });
256
+ }
257
+
258
+ // src/doctor-checks.ts
259
+ function lintRules(rules, entities, publicRead) {
260
+ const warnings = [];
261
+ if (!rules) return warnings;
262
+ for (const [ns, actions] of Object.entries(rules)) {
263
+ for (const [action, expr] of Object.entries(actions)) {
264
+ if (typeof expr !== "string" || expr.trim() !== "true") continue;
265
+ if (action === "view" && publicRead.includes(ns)) continue;
266
+ warnings.push(
267
+ 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`
268
+ );
269
+ }
270
+ }
271
+ for (const entity of entities) {
272
+ if (!(entity in rules)) warnings.push(`schema entity "${entity}" has no rules entry (all client access denied)`);
273
+ }
274
+ return warnings;
275
+ }
276
+ var defaultExec = (cmd, args, cwd) => (0, import_node_child_process2.execFileSync)(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
277
+ function trackedSecretFiles(rootDir, exec = defaultExec) {
278
+ let output;
279
+ try {
280
+ output = exec("git", ["ls-files", "--", ".dev.vars", ".odla"], rootDir);
281
+ } catch {
282
+ return [];
283
+ }
284
+ 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`);
285
+ }
286
+ function wranglerWarnings(rootDir) {
287
+ const configPath = findWranglerConfig(rootDir);
288
+ if (!configPath) return [];
289
+ const config = readWranglerConfig(configPath);
290
+ if (!config) return [];
291
+ const warnings = [];
292
+ const blocks = [{ label: "", block: config }];
293
+ const envs = config.env;
294
+ if (envs && typeof envs === "object") {
295
+ for (const [name, block] of Object.entries(envs)) {
296
+ if (block && typeof block === "object") blocks.push({ label: `env.${name}.`, block });
297
+ }
298
+ }
299
+ for (const { label, block } of blocks) {
300
+ const assets = block.assets;
301
+ if (assets?.directory) {
302
+ const dir = (0, import_node_path3.resolve)(rootDir, assets.directory);
303
+ if (dir === (0, import_node_path3.resolve)(rootDir)) {
304
+ warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
305
+ } else if ((0, import_node_fs3.existsSync)((0, import_node_path3.join)(dir, "node_modules"))) {
306
+ warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
307
+ }
308
+ }
309
+ const vars = block.vars;
310
+ if (vars && typeof vars === "object") {
311
+ for (const [name, value] of Object.entries(vars)) {
312
+ if (name === "ODLA_API_KEY" || typeof value === "string" && looksSecret(value)) {
313
+ warnings.push(`wrangler ${label}vars.${name} looks like a secret \u2014 use "odla-ai secrets push" / wrangler secret put, never vars`);
314
+ }
315
+ }
316
+ }
317
+ }
318
+ const pkg = readPackageJson(rootDir);
319
+ if (pkg && typeof pkg.scripts === "object" && pkg.scripts && "deploy" in pkg.scripts) {
320
+ warnings.push(`package.json has a script named "deploy" \u2014 CI may auto-deploy it; rename to deploy:app`);
321
+ }
322
+ return warnings;
323
+ }
324
+ function readPackageJson(rootDir) {
325
+ try {
326
+ return JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path3.join)(rootDir, "package.json"), "utf8"));
327
+ } catch {
328
+ return null;
329
+ }
330
+ }
331
+
154
332
  // src/doctor.ts
155
333
  async function doctor(options) {
156
334
  const out = options.stdout ?? console;
@@ -183,6 +361,9 @@ async function doctor(options) {
183
361
  }
184
362
  }
185
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));
186
367
  if (warnings.length) {
187
368
  out.log("");
188
369
  out.log("warnings:");
@@ -193,25 +374,25 @@ async function doctor(options) {
193
374
  }
194
375
 
195
376
  // src/init.ts
196
- var import_node_fs3 = require("fs");
197
- var import_node_path3 = require("path");
377
+ var import_node_fs5 = require("fs");
378
+ var import_node_path5 = require("path");
198
379
 
199
380
  // src/local.ts
200
- var import_node_fs2 = require("fs");
201
- var import_node_path2 = require("path");
381
+ var import_node_fs4 = require("fs");
382
+ var import_node_path4 = require("path");
202
383
  var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
203
384
  function readJsonFile(path) {
204
385
  try {
205
- return JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
386
+ return JSON.parse((0, import_node_fs4.readFileSync)(path, "utf8"));
206
387
  } catch {
207
388
  return null;
208
389
  }
209
390
  }
210
391
  function writePrivateJson(path, value) {
211
- (0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(path), { recursive: true });
212
- (0, import_node_fs2.writeFileSync)(path, `${JSON.stringify(value, null, 2)}
392
+ (0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(path), { recursive: true });
393
+ (0, import_node_fs4.writeFileSync)(path, `${JSON.stringify(value, null, 2)}
213
394
  `);
214
- (0, import_node_fs2.chmodSync)(path, 384);
395
+ (0, import_node_fs4.chmodSync)(path, 384);
215
396
  }
216
397
  function readCredentials(path) {
217
398
  return readJsonFile(path);
@@ -236,12 +417,12 @@ function mergeCredential(current, update) {
236
417
  return next;
237
418
  }
238
419
  function ensureGitignore(rootDir) {
239
- const path = (0, import_node_path2.resolve)(rootDir, ".gitignore");
240
- const existing = (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : "";
420
+ const path = (0, import_node_path4.resolve)(rootDir, ".gitignore");
421
+ const existing = (0, import_node_fs4.existsSync)(path) ? (0, import_node_fs4.readFileSync)(path, "utf8") : "";
241
422
  const missing = GITIGNORE_LINES.filter((line) => !existing.split(/\r?\n/).includes(line));
242
423
  if (missing.length === 0) return;
243
424
  const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
244
- (0, import_node_fs2.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
425
+ (0, import_node_fs4.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
245
426
  `);
246
427
  }
247
428
  function writeDevVars(path, credentials, env) {
@@ -255,22 +436,22 @@ function writeDevVars(path, credentials, env) {
255
436
  `ODLA_TENANT="${entry.tenantId}"`,
256
437
  `ODLA_API_KEY="${entry.dbKey}"`
257
438
  ];
258
- (0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(path), { recursive: true });
259
- (0, import_node_fs2.writeFileSync)(path, `${lines.join("\n")}
439
+ (0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(path), { recursive: true });
440
+ (0, import_node_fs4.writeFileSync)(path, `${lines.join("\n")}
260
441
  `);
261
- (0, import_node_fs2.chmodSync)(path, 384);
442
+ (0, import_node_fs4.chmodSync)(path, 384);
262
443
  }
263
444
  function displayPath(path, rootDir = process.cwd()) {
264
- const rel = (0, import_node_path2.relative)(rootDir, path);
445
+ const rel = (0, import_node_path4.relative)(rootDir, path);
265
446
  return rel && !rel.startsWith("..") ? rel : path;
266
447
  }
267
448
 
268
449
  // src/init.ts
269
450
  function initProject(options) {
270
451
  const out = options.stdout ?? console;
271
- const rootDir = (0, import_node_path3.resolve)(options.rootDir ?? process.cwd());
272
- const configPath = (0, import_node_path3.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
273
- if ((0, import_node_fs3.existsSync)(configPath) && !options.force) {
452
+ const rootDir = (0, import_node_path5.resolve)(options.rootDir ?? process.cwd());
453
+ const configPath = (0, import_node_path5.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
454
+ if ((0, import_node_fs5.existsSync)(configPath) && !options.force) {
274
455
  throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
275
456
  }
276
457
  if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
@@ -279,20 +460,20 @@ function initProject(options) {
279
460
  const envs = options.envs?.length ? options.envs : ["prod", "dev"];
280
461
  const services = options.services?.length ? options.services : ["db", "ai"];
281
462
  const aiProvider = options.aiProvider ?? "anthropic";
282
- (0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(configPath), { recursive: true });
283
- (0, import_node_fs3.mkdirSync)((0, import_node_path3.resolve)(rootDir, "src/odla"), { recursive: true });
284
- (0, import_node_fs3.mkdirSync)((0, import_node_path3.resolve)(rootDir, ".odla"), { recursive: true });
285
- (0, import_node_fs3.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
286
- writeIfMissing((0, import_node_path3.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
287
- writeIfMissing((0, import_node_path3.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
463
+ (0, import_node_fs5.mkdirSync)((0, import_node_path5.dirname)(configPath), { recursive: true });
464
+ (0, import_node_fs5.mkdirSync)((0, import_node_path5.resolve)(rootDir, "src/odla"), { recursive: true });
465
+ (0, import_node_fs5.mkdirSync)((0, import_node_path5.resolve)(rootDir, ".odla"), { recursive: true });
466
+ (0, import_node_fs5.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
467
+ writeIfMissing((0, import_node_path5.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
468
+ writeIfMissing((0, import_node_path5.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
288
469
  ensureGitignore(rootDir);
289
470
  out.log(`created ${relativeDisplay(configPath, rootDir)}`);
290
471
  out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
291
472
  out.log("updated .gitignore for local odla credentials");
292
473
  }
293
474
  function writeIfMissing(path, text) {
294
- if ((0, import_node_fs3.existsSync)(path)) return;
295
- (0, import_node_fs3.writeFileSync)(path, text);
475
+ if ((0, import_node_fs5.existsSync)(path)) return;
476
+ (0, import_node_fs5.writeFileSync)(path, text);
296
477
  }
297
478
  function configTemplate(input) {
298
479
  return `export default {
@@ -375,7 +556,7 @@ function relativeDisplay(path, rootDir) {
375
556
  // src/provision.ts
376
557
  var import_apps = require("@odla-ai/apps");
377
558
  var import_ai = require("@odla-ai/ai");
378
- var import_node_path4 = require("path");
559
+ var import_node_path6 = require("path");
379
560
  var import_node_process3 = __toESM(require("process"), 1);
380
561
 
381
562
  // src/token.ts
@@ -383,12 +564,12 @@ var import_db = require("@odla-ai/db");
383
564
  var import_node_process2 = __toESM(require("process"), 1);
384
565
 
385
566
  // src/open.ts
386
- var import_node_child_process = require("child_process");
567
+ var import_node_child_process3 = require("child_process");
387
568
  var import_node_process = __toESM(require("process"), 1);
388
569
  async function openUrl(url, options = {}) {
389
570
  const command = openerFor(options.platform ?? import_node_process.default.platform);
390
- const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
391
- await new Promise((resolve5, reject) => {
571
+ const doSpawn = options.spawnImpl ?? import_node_child_process3.spawn;
572
+ await new Promise((resolve7, reject) => {
392
573
  const child = doSpawn(command.cmd, [...command.args, url], {
393
574
  stdio: "ignore",
394
575
  detached: true
@@ -396,7 +577,7 @@ async function openUrl(url, options = {}) {
396
577
  child.once("error", reject);
397
578
  child.once("spawn", () => {
398
579
  child.unref();
399
- resolve5();
580
+ resolve7();
400
581
  });
401
582
  });
402
583
  }
@@ -623,17 +804,115 @@ function defaultSecretName(provider) {
623
804
  function resolveWriteDevVarsTarget(cfg, requested) {
624
805
  if (!requested) return null;
625
806
  if (requested === true) return cfg.local.devVarsFile;
626
- return (0, import_node_path4.resolve)((0, import_node_path4.dirname)(cfg.configPath), requested);
807
+ return (0, import_node_path6.resolve)((0, import_node_path6.dirname)(cfg.configPath), requested);
627
808
  }
628
809
  async function safeText(res) {
629
810
  try {
630
- return redact((await res.text()).slice(0, 500));
811
+ return redactSecrets((await res.text()).slice(0, 500));
631
812
  } catch {
632
813
  return "";
633
814
  }
634
815
  }
635
- function redact(value) {
636
- return value.replace(/odla_(sk|dev)_[A-Za-z0-9._-]+/g, "odla_$1_[redacted]").replace(/sk-[A-Za-z0-9._-]+/g, "sk-[redacted]");
816
+
817
+ // src/secrets.ts
818
+ var PROD_ENV_NAMES = /* @__PURE__ */ new Set(["prod", "production"]);
819
+ async function secretsPush(options) {
820
+ const out = options.stdout ?? console;
821
+ const cfg = await loadProjectConfig(options.configPath);
822
+ const env = options.env;
823
+ if (!cfg.envs.includes(env)) {
824
+ throw new Error(`env "${env}" is not in config envs (${cfg.envs.join(", ")})`);
825
+ }
826
+ if (PROD_ENV_NAMES.has(env) && !options.yes) {
827
+ throw new Error(`refusing to push a secret to "${env}" without --yes`);
828
+ }
829
+ const credentialsPath = displayPath(cfg.local.credentialsFile, cfg.rootDir);
830
+ const credentials = readCredentials(cfg.local.credentialsFile);
831
+ if (!credentials) throw new Error(`no credentials at ${credentialsPath} \u2014 run "odla-ai provision" first`);
832
+ if (credentials.appId !== cfg.app.id) {
833
+ throw new Error(`credentials at ${credentialsPath} are for "${credentials.appId}", not "${cfg.app.id}"`);
834
+ }
835
+ const dbKey = credentials.envs[env]?.dbKey;
836
+ if (!dbKey) throw new Error(`no db key for env "${env}" in ${credentialsPath} \u2014 run "odla-ai provision" first`);
837
+ const wranglerConfig = findWranglerConfig(cfg.rootDir);
838
+ if (!wranglerConfig) {
839
+ throw new Error(`no wrangler config found in ${cfg.rootDir} (wrangler.jsonc, wrangler.json, or wrangler.toml)`);
840
+ }
841
+ const wranglerEnv = PROD_ENV_NAMES.has(env) ? void 0 : env;
842
+ const target = wranglerEnv ? `wrangler env "${wranglerEnv}"` : "the top-level (prod) wrangler env";
843
+ if (options.dryRun) {
844
+ out.log(`dry run: would push ODLA_API_KEY (${redactSecrets(dbKey)}) to ${target}`);
845
+ return;
846
+ }
847
+ const run = options.runner ?? defaultRunner;
848
+ if (!await wranglerLoggedIn(run, cfg.rootDir)) {
849
+ throw new Error(`wrangler is not logged in \u2014 run "wrangler login" (a browser step for the human)`);
850
+ }
851
+ const result = await wranglerPutSecret(run, { name: "ODLA_API_KEY", value: dbKey, env: wranglerEnv, cwd: cfg.rootDir });
852
+ if (result.code !== 0) {
853
+ throw new Error(`wrangler secret put failed (exit ${result.code}): ${redactSecrets(`${result.stderr || result.stdout}`.trim())}`);
854
+ }
855
+ out.log(`ODLA_API_KEY pushed to ${target} (value read from ${credentialsPath}, never echoed)`);
856
+ }
857
+
858
+ // src/skill.ts
859
+ var import_node_fs6 = require("fs");
860
+ var import_node_os = require("os");
861
+ var import_node_path7 = require("path");
862
+ var import_node_url2 = require("url");
863
+ function installSkill(options = {}) {
864
+ const out = options.stdout ?? console;
865
+ const sourceDir = options.sourceDir ?? (0, import_node_url2.fileURLToPath)(new URL("../skills", importMetaUrl));
866
+ 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");
867
+ const files = listFiles(sourceDir);
868
+ if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
869
+ const written = [];
870
+ const unchanged = [];
871
+ const conflicts = [];
872
+ for (const rel of files) {
873
+ const target = (0, import_node_path7.join)(targetDir, rel);
874
+ const source = (0, import_node_fs6.readFileSync)((0, import_node_path7.join)(sourceDir, rel), "utf8");
875
+ if ((0, import_node_fs6.existsSync)(target)) {
876
+ const current = (0, import_node_fs6.readFileSync)(target, "utf8");
877
+ if (current === source) {
878
+ unchanged.push(rel);
879
+ continue;
880
+ }
881
+ if (!options.force) {
882
+ conflicts.push(rel);
883
+ continue;
884
+ }
885
+ }
886
+ written.push(rel);
887
+ }
888
+ if (conflicts.length > 0) {
889
+ throw new Error(
890
+ `skill files modified locally (re-run with --force to overwrite):
891
+ ${conflicts.map((f) => ` - ${(0, import_node_path7.join)(targetDir, f)}`).join("\n")}`
892
+ );
893
+ }
894
+ for (const rel of written) {
895
+ const target = (0, import_node_path7.join)(targetDir, rel);
896
+ (0, import_node_fs6.mkdirSync)((0, import_node_path7.join)(target, ".."), { recursive: true });
897
+ (0, import_node_fs6.writeFileSync)(target, (0, import_node_fs6.readFileSync)((0, import_node_path7.join)(sourceDir, rel), "utf8"));
898
+ }
899
+ const skills = [...new Set(files.map((f) => f.split(/[\\/]/)[0]))].sort();
900
+ out.log(`skills: ${skills.join(", ")}`);
901
+ out.log(`installed ${written.length} file(s) to ${targetDir}${unchanged.length ? ` (${unchanged.length} unchanged)` : ""}`);
902
+ return { targetDir, written, unchanged };
903
+ }
904
+ function listFiles(dir) {
905
+ if (!(0, import_node_fs6.existsSync)(dir)) return [];
906
+ const results = [];
907
+ const walk = (current) => {
908
+ for (const entry of (0, import_node_fs6.readdirSync)(current, { withFileTypes: true })) {
909
+ const path = (0, import_node_path7.join)(current, entry.name);
910
+ if (entry.isDirectory()) walk(path);
911
+ else results.push((0, import_node_path7.relative)(dir, path));
912
+ }
913
+ };
914
+ walk(dir);
915
+ return results.sort();
637
916
  }
638
917
 
639
918
  // src/smoke.ts
@@ -767,6 +1046,27 @@ async function runCli(argv = process.argv.slice(2)) {
767
1046
  });
768
1047
  return;
769
1048
  }
1049
+ if (command === "skill") {
1050
+ const sub = parsed.positionals[1];
1051
+ if (sub !== "install") throw new Error(`unknown skill subcommand "${sub ?? ""}". Try "odla-ai skill install".`);
1052
+ installSkill({
1053
+ dir: stringOpt(parsed.options.dir),
1054
+ global: parsed.options.global === true,
1055
+ force: parsed.options.force === true
1056
+ });
1057
+ return;
1058
+ }
1059
+ if (command === "secrets") {
1060
+ const sub = parsed.positionals[1];
1061
+ if (sub !== "push") throw new Error(`unknown secrets subcommand "${sub ?? ""}". Try "odla-ai secrets push --env dev".`);
1062
+ await secretsPush({
1063
+ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
1064
+ env: requiredString(parsed.options.env, "--env"),
1065
+ dryRun: parsed.options["dry-run"] === true,
1066
+ yes: parsed.options.yes === true
1067
+ });
1068
+ return;
1069
+ }
770
1070
  throw new Error(`unknown command "${command}". Run "odla-ai help".`);
771
1071
  }
772
1072
  function parseArgv(argv) {
@@ -825,7 +1125,7 @@ function listOpt(value) {
825
1125
  return values.flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
826
1126
  }
827
1127
  function cliVersion() {
828
- const pkg = JSON.parse((0, import_node_fs4.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
1128
+ const pkg = JSON.parse((0, import_node_fs7.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
829
1129
  return pkg.version ?? "unknown";
830
1130
  }
831
1131
  function printHelp() {
@@ -836,6 +1136,8 @@ Usage:
836
1136
  odla-ai doctor [--config odla.config.mjs]
837
1137
  odla-ai provision [--config odla.config.mjs] [--dry-run] [--open|--no-open] [--rotate-keys] [--write-dev-vars[=path]]
838
1138
  odla-ai smoke [--config odla.config.mjs] [--env dev]
1139
+ odla-ai skill install [--dir <project>] [--global] [--force]
1140
+ odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
839
1141
  odla-ai version
840
1142
 
841
1143
  Commands:
@@ -843,6 +1145,8 @@ Commands:
843
1145
  doctor Validate and summarize the project config without network calls.
844
1146
  provision Register the app, enable services, push schema/rules, configure AI/auth.
845
1147
  smoke Verify local credentials, public-config, live schema, and db aggregate.
1148
+ skill Install the bundled Claude Code skills into .claude/skills/.
1149
+ secrets Push the env's db key into the Worker via wrangler, stdin-piped.
846
1150
  version Print the CLI version.
847
1151
 
848
1152
  Safety:
@@ -856,8 +1160,11 @@ Safety:
856
1160
  0 && (module.exports = {
857
1161
  doctor,
858
1162
  initProject,
1163
+ installSkill,
859
1164
  provision,
1165
+ redactSecrets,
860
1166
  runCli,
1167
+ secretsPush,
861
1168
  smoke
862
1169
  });
863
1170
  //# sourceMappingURL=index.cjs.map