@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/README.md +27 -0
- package/dist/bin.cjs +338 -37
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-AXCZKIVY.js → chunk-5J4LKP37.js} +331 -27
- package/dist/chunk-5J4LKP37.js.map +1 -0
- package/dist/index.cjs +344 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -1
- package/dist/index.d.ts +57 -1
- package/dist/index.js +7 -1
- package/llms.txt +85 -2
- package/package.json +2 -1
- package/skills/odla-migrate/SKILL.md +89 -0
- package/skills/odla-migrate/references/phase-0-preflight.md +45 -0
- package/skills/odla-migrate/references/phase-1-static.md +48 -0
- package/skills/odla-migrate/references/phase-2-db.md +59 -0
- package/skills/odla-migrate/references/phase-3-auth.md +52 -0
- package/skills/odla-migrate/references/phase-4-ai.md +44 -0
- package/skills/odla-migrate/references/phase-5-cutover.md +53 -0
- package/skills/odla-migrate/references/secrets-map.md +43 -0
- package/skills/odla-migrate/references/troubleshooting.md +69 -0
- package/dist/chunk-AXCZKIVY.js.map +0 -1
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
|
|
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
|
|
197
|
-
var
|
|
377
|
+
var import_node_fs5 = require("fs");
|
|
378
|
+
var import_node_path5 = require("path");
|
|
198
379
|
|
|
199
380
|
// src/local.ts
|
|
200
|
-
var
|
|
201
|
-
var
|
|
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,
|
|
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,
|
|
212
|
-
(0,
|
|
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,
|
|
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,
|
|
240
|
-
const existing = (0,
|
|
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,
|
|
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,
|
|
259
|
-
(0,
|
|
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,
|
|
442
|
+
(0, import_node_fs4.chmodSync)(path, 384);
|
|
262
443
|
}
|
|
263
444
|
function displayPath(path, rootDir = process.cwd()) {
|
|
264
|
-
const rel = (0,
|
|
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,
|
|
272
|
-
const configPath = (0,
|
|
273
|
-
if ((0,
|
|
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,
|
|
283
|
-
(0,
|
|
284
|
-
(0,
|
|
285
|
-
(0,
|
|
286
|
-
writeIfMissing((0,
|
|
287
|
-
writeIfMissing((0,
|
|
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,
|
|
295
|
-
(0,
|
|
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
|
|
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
|
|
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 ??
|
|
391
|
-
await new Promise((
|
|
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
|
-
|
|
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,
|
|
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
|
|
811
|
+
return redactSecrets((await res.text()).slice(0, 500));
|
|
631
812
|
} catch {
|
|
632
813
|
return "";
|
|
633
814
|
}
|
|
634
815
|
}
|
|
635
|
-
|
|
636
|
-
|
|
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,
|
|
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
|