@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/README.md
CHANGED
|
@@ -46,6 +46,8 @@ npx odla-ai doctor
|
|
|
46
46
|
npx odla-ai provision --dry-run
|
|
47
47
|
npx odla-ai provision --write-dev-vars
|
|
48
48
|
npx odla-ai smoke --env dev
|
|
49
|
+
npx odla-ai secrets push --env dev
|
|
50
|
+
npx odla-ai skill install
|
|
49
51
|
npx odla-ai version
|
|
50
52
|
```
|
|
51
53
|
|
|
@@ -81,6 +83,30 @@ odla-db schema with the tenant key, compares expected schema entities, and runs
|
|
|
81
83
|
a count aggregate against the first entity. It fails early with a clear message
|
|
82
84
|
when provisioning has not written `.odla/credentials.local.json`.
|
|
83
85
|
|
|
86
|
+
`secrets push --env <env>` moves the env's odla-db key from
|
|
87
|
+
`.odla/credentials.local.json` into the deployed Worker by piping it over
|
|
88
|
+
stdin to `wrangler secret put ODLA_API_KEY` — the value never appears on
|
|
89
|
+
argv, in output, or in an agent's transcript. It preflights `wrangler whoami`
|
|
90
|
+
and the presence of a wrangler config file. The env `prod`/`production`
|
|
91
|
+
targets the top-level wrangler environment (no `--env` flag is passed to
|
|
92
|
+
wrangler) and requires `--yes`; every other env maps to wrangler's
|
|
93
|
+
`--env <name>`. `--dry-run` prints a redacted plan without spawning anything.
|
|
94
|
+
|
|
95
|
+
`skill install` copies the Claude Code skills bundled with this package
|
|
96
|
+
(currently `odla-migrate`, the phased GitHub Pages → odla migration
|
|
97
|
+
procedure) into the project's `.claude/skills/`, or into `~/.claude/skills/`
|
|
98
|
+
with `--global`. Re-runs are idempotent; a locally modified skill file is
|
|
99
|
+
never overwritten without `--force`.
|
|
100
|
+
|
|
101
|
+
`doctor` also lints for common footguns: permission rules that are literally
|
|
102
|
+
`"true"` (public writes always warn; a public `view` warns unless the
|
|
103
|
+
namespace is listed in `db.publicRead`), schema entities missing from the
|
|
104
|
+
rules map, credential files (`.dev.vars`, `.odla/*`) tracked by git, a
|
|
105
|
+
wrangler assets directory pointed at the project root or at a directory
|
|
106
|
+
containing `node_modules` (the `wrangler dev` "spawn EBADF" footgun),
|
|
107
|
+
secret-shaped values in wrangler `vars`, and a package.json script literally
|
|
108
|
+
named `deploy` (CI may auto-deploy it).
|
|
109
|
+
|
|
84
110
|
## Config
|
|
85
111
|
|
|
86
112
|
```js
|
|
@@ -94,6 +120,7 @@ export default {
|
|
|
94
120
|
schema: "./src/odla/schema.mjs",
|
|
95
121
|
rules: "./src/odla/rules.mjs",
|
|
96
122
|
defaultRules: "deny",
|
|
123
|
+
publicRead: [], // namespaces where a `view: "true"` rule is intentional
|
|
97
124
|
},
|
|
98
125
|
ai: {
|
|
99
126
|
provider: process.env.ODLA_AI_PROVIDER ?? "anthropic",
|
package/dist/bin.cjs
CHANGED
|
@@ -28,7 +28,7 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
|
|
|
28
28
|
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
29
29
|
|
|
30
30
|
// src/cli.ts
|
|
31
|
-
var
|
|
31
|
+
var import_node_fs7 = require("fs");
|
|
32
32
|
|
|
33
33
|
// src/config.ts
|
|
34
34
|
var import_node_fs = require("fs");
|
|
@@ -135,6 +135,181 @@ function unique(values) {
|
|
|
135
135
|
return [...new Set(values.filter(Boolean))];
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
// src/doctor-checks.ts
|
|
139
|
+
var import_node_child_process2 = require("child_process");
|
|
140
|
+
var import_node_fs3 = require("fs");
|
|
141
|
+
var import_node_path3 = require("path");
|
|
142
|
+
|
|
143
|
+
// src/redact.ts
|
|
144
|
+
var REPLACEMENTS = [
|
|
145
|
+
[/odla_(sk|dev)_[A-Za-z0-9._-]+/g, "odla_$1_[redacted]"],
|
|
146
|
+
[/\bsk_(live|test)_[A-Za-z0-9]+/g, "sk_$1_[redacted]"],
|
|
147
|
+
[/\bsk-[A-Za-z0-9._-]+/g, "sk-[redacted]"],
|
|
148
|
+
[/\bwhsec_[A-Za-z0-9+/=]+/g, "whsec_[redacted]"],
|
|
149
|
+
[/\b(ghp|gho|github_pat)_[A-Za-z0-9_]+/g, "$1_[redacted]"],
|
|
150
|
+
[/\bAKIA[A-Z0-9]{12,}/g, "AKIA[redacted]"]
|
|
151
|
+
];
|
|
152
|
+
function redactSecrets(value) {
|
|
153
|
+
let result = value;
|
|
154
|
+
for (const [pattern, replacement] of REPLACEMENTS) result = result.replace(pattern, replacement);
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
function looksSecret(value) {
|
|
158
|
+
return redactSecrets(value) !== value || value.includes("-----BEGIN");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/wrangler.ts
|
|
162
|
+
var import_node_child_process = require("child_process");
|
|
163
|
+
var import_node_fs2 = require("fs");
|
|
164
|
+
var import_node_path2 = require("path");
|
|
165
|
+
var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
|
|
166
|
+
const child = (0, import_node_child_process.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
167
|
+
let stdout = "";
|
|
168
|
+
let stderr = "";
|
|
169
|
+
child.stdout.on("data", (chunk) => stdout += chunk.toString());
|
|
170
|
+
child.stderr.on("data", (chunk) => stderr += chunk.toString());
|
|
171
|
+
child.on("error", reject);
|
|
172
|
+
child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout, stderr }));
|
|
173
|
+
child.stdin.end(opts?.input ?? "");
|
|
174
|
+
});
|
|
175
|
+
var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
|
|
176
|
+
function findWranglerConfig(rootDir) {
|
|
177
|
+
for (const name of WRANGLER_CONFIG_FILES) {
|
|
178
|
+
const path = (0, import_node_path2.join)(rootDir, name);
|
|
179
|
+
if ((0, import_node_fs2.existsSync)(path)) return path;
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
function readWranglerConfig(path) {
|
|
184
|
+
if (path.endsWith(".toml")) return null;
|
|
185
|
+
try {
|
|
186
|
+
return JSON.parse(stripJsonComments((0, import_node_fs2.readFileSync)(path, "utf8")));
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function stripJsonComments(text) {
|
|
192
|
+
let result = "";
|
|
193
|
+
let inString = false;
|
|
194
|
+
for (let i = 0; i < text.length; i++) {
|
|
195
|
+
const ch = text[i];
|
|
196
|
+
if (inString) {
|
|
197
|
+
result += ch;
|
|
198
|
+
if (ch === "\\") {
|
|
199
|
+
result += text[i + 1] ?? "";
|
|
200
|
+
i++;
|
|
201
|
+
} else if (ch === '"') {
|
|
202
|
+
inString = false;
|
|
203
|
+
}
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (ch === '"') {
|
|
207
|
+
inString = true;
|
|
208
|
+
result += ch;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (ch === "/" && text[i + 1] === "/") {
|
|
212
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
213
|
+
result += "\n";
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (ch === "/" && text[i + 1] === "*") {
|
|
217
|
+
i += 2;
|
|
218
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
|
|
219
|
+
i++;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
result += ch;
|
|
223
|
+
}
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
async function wranglerLoggedIn(run, cwd) {
|
|
227
|
+
try {
|
|
228
|
+
const result = await run("npx", ["wrangler", "whoami"], { cwd });
|
|
229
|
+
return result.code === 0 && !/not authenticated/i.test(`${result.stdout}${result.stderr}`);
|
|
230
|
+
} catch {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function wranglerPutSecret(run, opts) {
|
|
235
|
+
const args = ["wrangler", "secret", "put", opts.name, ...opts.env ? ["--env", opts.env] : []];
|
|
236
|
+
return run("npx", args, { input: opts.value, cwd: opts.cwd });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/doctor-checks.ts
|
|
240
|
+
function lintRules(rules, entities, publicRead) {
|
|
241
|
+
const warnings = [];
|
|
242
|
+
if (!rules) return warnings;
|
|
243
|
+
for (const [ns, actions] of Object.entries(rules)) {
|
|
244
|
+
for (const [action, expr] of Object.entries(actions)) {
|
|
245
|
+
if (typeof expr !== "string" || expr.trim() !== "true") continue;
|
|
246
|
+
if (action === "view" && publicRead.includes(ns)) continue;
|
|
247
|
+
warnings.push(
|
|
248
|
+
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`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
for (const entity of entities) {
|
|
253
|
+
if (!(entity in rules)) warnings.push(`schema entity "${entity}" has no rules entry (all client access denied)`);
|
|
254
|
+
}
|
|
255
|
+
return warnings;
|
|
256
|
+
}
|
|
257
|
+
var defaultExec = (cmd, args, cwd) => (0, import_node_child_process2.execFileSync)(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
258
|
+
function trackedSecretFiles(rootDir, exec = defaultExec) {
|
|
259
|
+
let output;
|
|
260
|
+
try {
|
|
261
|
+
output = exec("git", ["ls-files", "--", ".dev.vars", ".odla"], rootDir);
|
|
262
|
+
} catch {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
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`);
|
|
266
|
+
}
|
|
267
|
+
function wranglerWarnings(rootDir) {
|
|
268
|
+
const configPath = findWranglerConfig(rootDir);
|
|
269
|
+
if (!configPath) return [];
|
|
270
|
+
const config = readWranglerConfig(configPath);
|
|
271
|
+
if (!config) return [];
|
|
272
|
+
const warnings = [];
|
|
273
|
+
const blocks = [{ label: "", block: config }];
|
|
274
|
+
const envs = config.env;
|
|
275
|
+
if (envs && typeof envs === "object") {
|
|
276
|
+
for (const [name, block] of Object.entries(envs)) {
|
|
277
|
+
if (block && typeof block === "object") blocks.push({ label: `env.${name}.`, block });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
for (const { label, block } of blocks) {
|
|
281
|
+
const assets = block.assets;
|
|
282
|
+
if (assets?.directory) {
|
|
283
|
+
const dir = (0, import_node_path3.resolve)(rootDir, assets.directory);
|
|
284
|
+
if (dir === (0, import_node_path3.resolve)(rootDir)) {
|
|
285
|
+
warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
|
|
286
|
+
} else if ((0, import_node_fs3.existsSync)((0, import_node_path3.join)(dir, "node_modules"))) {
|
|
287
|
+
warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const vars = block.vars;
|
|
291
|
+
if (vars && typeof vars === "object") {
|
|
292
|
+
for (const [name, value] of Object.entries(vars)) {
|
|
293
|
+
if (name === "ODLA_API_KEY" || typeof value === "string" && looksSecret(value)) {
|
|
294
|
+
warnings.push(`wrangler ${label}vars.${name} looks like a secret \u2014 use "odla-ai secrets push" / wrangler secret put, never vars`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const pkg = readPackageJson(rootDir);
|
|
300
|
+
if (pkg && typeof pkg.scripts === "object" && pkg.scripts && "deploy" in pkg.scripts) {
|
|
301
|
+
warnings.push(`package.json has a script named "deploy" \u2014 CI may auto-deploy it; rename to deploy:app`);
|
|
302
|
+
}
|
|
303
|
+
return warnings;
|
|
304
|
+
}
|
|
305
|
+
function readPackageJson(rootDir) {
|
|
306
|
+
try {
|
|
307
|
+
return JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path3.join)(rootDir, "package.json"), "utf8"));
|
|
308
|
+
} catch {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
138
313
|
// src/doctor.ts
|
|
139
314
|
async function doctor(options) {
|
|
140
315
|
const out = options.stdout ?? console;
|
|
@@ -167,6 +342,9 @@ async function doctor(options) {
|
|
|
167
342
|
}
|
|
168
343
|
}
|
|
169
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));
|
|
170
348
|
if (warnings.length) {
|
|
171
349
|
out.log("");
|
|
172
350
|
out.log("warnings:");
|
|
@@ -177,25 +355,25 @@ async function doctor(options) {
|
|
|
177
355
|
}
|
|
178
356
|
|
|
179
357
|
// src/init.ts
|
|
180
|
-
var
|
|
181
|
-
var
|
|
358
|
+
var import_node_fs5 = require("fs");
|
|
359
|
+
var import_node_path5 = require("path");
|
|
182
360
|
|
|
183
361
|
// src/local.ts
|
|
184
|
-
var
|
|
185
|
-
var
|
|
362
|
+
var import_node_fs4 = require("fs");
|
|
363
|
+
var import_node_path4 = require("path");
|
|
186
364
|
var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
|
|
187
365
|
function readJsonFile(path) {
|
|
188
366
|
try {
|
|
189
|
-
return JSON.parse((0,
|
|
367
|
+
return JSON.parse((0, import_node_fs4.readFileSync)(path, "utf8"));
|
|
190
368
|
} catch {
|
|
191
369
|
return null;
|
|
192
370
|
}
|
|
193
371
|
}
|
|
194
372
|
function writePrivateJson(path, value) {
|
|
195
|
-
(0,
|
|
196
|
-
(0,
|
|
373
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(path), { recursive: true });
|
|
374
|
+
(0, import_node_fs4.writeFileSync)(path, `${JSON.stringify(value, null, 2)}
|
|
197
375
|
`);
|
|
198
|
-
(0,
|
|
376
|
+
(0, import_node_fs4.chmodSync)(path, 384);
|
|
199
377
|
}
|
|
200
378
|
function readCredentials(path) {
|
|
201
379
|
return readJsonFile(path);
|
|
@@ -220,12 +398,12 @@ function mergeCredential(current, update) {
|
|
|
220
398
|
return next;
|
|
221
399
|
}
|
|
222
400
|
function ensureGitignore(rootDir) {
|
|
223
|
-
const path = (0,
|
|
224
|
-
const existing = (0,
|
|
401
|
+
const path = (0, import_node_path4.resolve)(rootDir, ".gitignore");
|
|
402
|
+
const existing = (0, import_node_fs4.existsSync)(path) ? (0, import_node_fs4.readFileSync)(path, "utf8") : "";
|
|
225
403
|
const missing = GITIGNORE_LINES.filter((line) => !existing.split(/\r?\n/).includes(line));
|
|
226
404
|
if (missing.length === 0) return;
|
|
227
405
|
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
228
|
-
(0,
|
|
406
|
+
(0, import_node_fs4.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
|
|
229
407
|
`);
|
|
230
408
|
}
|
|
231
409
|
function writeDevVars(path, credentials, env) {
|
|
@@ -239,22 +417,22 @@ function writeDevVars(path, credentials, env) {
|
|
|
239
417
|
`ODLA_TENANT="${entry.tenantId}"`,
|
|
240
418
|
`ODLA_API_KEY="${entry.dbKey}"`
|
|
241
419
|
];
|
|
242
|
-
(0,
|
|
243
|
-
(0,
|
|
420
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(path), { recursive: true });
|
|
421
|
+
(0, import_node_fs4.writeFileSync)(path, `${lines.join("\n")}
|
|
244
422
|
`);
|
|
245
|
-
(0,
|
|
423
|
+
(0, import_node_fs4.chmodSync)(path, 384);
|
|
246
424
|
}
|
|
247
425
|
function displayPath(path, rootDir = process.cwd()) {
|
|
248
|
-
const rel = (0,
|
|
426
|
+
const rel = (0, import_node_path4.relative)(rootDir, path);
|
|
249
427
|
return rel && !rel.startsWith("..") ? rel : path;
|
|
250
428
|
}
|
|
251
429
|
|
|
252
430
|
// src/init.ts
|
|
253
431
|
function initProject(options) {
|
|
254
432
|
const out = options.stdout ?? console;
|
|
255
|
-
const rootDir = (0,
|
|
256
|
-
const configPath = (0,
|
|
257
|
-
if ((0,
|
|
433
|
+
const rootDir = (0, import_node_path5.resolve)(options.rootDir ?? process.cwd());
|
|
434
|
+
const configPath = (0, import_node_path5.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
|
|
435
|
+
if ((0, import_node_fs5.existsSync)(configPath) && !options.force) {
|
|
258
436
|
throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
|
|
259
437
|
}
|
|
260
438
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
|
|
@@ -263,20 +441,20 @@ function initProject(options) {
|
|
|
263
441
|
const envs = options.envs?.length ? options.envs : ["prod", "dev"];
|
|
264
442
|
const services = options.services?.length ? options.services : ["db", "ai"];
|
|
265
443
|
const aiProvider = options.aiProvider ?? "anthropic";
|
|
266
|
-
(0,
|
|
267
|
-
(0,
|
|
268
|
-
(0,
|
|
269
|
-
(0,
|
|
270
|
-
writeIfMissing((0,
|
|
271
|
-
writeIfMissing((0,
|
|
444
|
+
(0, import_node_fs5.mkdirSync)((0, import_node_path5.dirname)(configPath), { recursive: true });
|
|
445
|
+
(0, import_node_fs5.mkdirSync)((0, import_node_path5.resolve)(rootDir, "src/odla"), { recursive: true });
|
|
446
|
+
(0, import_node_fs5.mkdirSync)((0, import_node_path5.resolve)(rootDir, ".odla"), { recursive: true });
|
|
447
|
+
(0, import_node_fs5.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
|
|
448
|
+
writeIfMissing((0, import_node_path5.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
|
|
449
|
+
writeIfMissing((0, import_node_path5.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
|
|
272
450
|
ensureGitignore(rootDir);
|
|
273
451
|
out.log(`created ${relativeDisplay(configPath, rootDir)}`);
|
|
274
452
|
out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
|
|
275
453
|
out.log("updated .gitignore for local odla credentials");
|
|
276
454
|
}
|
|
277
455
|
function writeIfMissing(path, text) {
|
|
278
|
-
if ((0,
|
|
279
|
-
(0,
|
|
456
|
+
if ((0, import_node_fs5.existsSync)(path)) return;
|
|
457
|
+
(0, import_node_fs5.writeFileSync)(path, text);
|
|
280
458
|
}
|
|
281
459
|
function configTemplate(input) {
|
|
282
460
|
return `export default {
|
|
@@ -359,7 +537,7 @@ function relativeDisplay(path, rootDir) {
|
|
|
359
537
|
// src/provision.ts
|
|
360
538
|
var import_apps = require("@odla-ai/apps");
|
|
361
539
|
var import_ai = require("@odla-ai/ai");
|
|
362
|
-
var
|
|
540
|
+
var import_node_path6 = require("path");
|
|
363
541
|
var import_node_process3 = __toESM(require("process"), 1);
|
|
364
542
|
|
|
365
543
|
// src/token.ts
|
|
@@ -367,12 +545,12 @@ var import_db = require("@odla-ai/db");
|
|
|
367
545
|
var import_node_process2 = __toESM(require("process"), 1);
|
|
368
546
|
|
|
369
547
|
// src/open.ts
|
|
370
|
-
var
|
|
548
|
+
var import_node_child_process3 = require("child_process");
|
|
371
549
|
var import_node_process = __toESM(require("process"), 1);
|
|
372
550
|
async function openUrl(url, options = {}) {
|
|
373
551
|
const command = openerFor(options.platform ?? import_node_process.default.platform);
|
|
374
|
-
const doSpawn = options.spawnImpl ??
|
|
375
|
-
await new Promise((
|
|
552
|
+
const doSpawn = options.spawnImpl ?? import_node_child_process3.spawn;
|
|
553
|
+
await new Promise((resolve7, reject) => {
|
|
376
554
|
const child = doSpawn(command.cmd, [...command.args, url], {
|
|
377
555
|
stdio: "ignore",
|
|
378
556
|
detached: true
|
|
@@ -380,7 +558,7 @@ async function openUrl(url, options = {}) {
|
|
|
380
558
|
child.once("error", reject);
|
|
381
559
|
child.once("spawn", () => {
|
|
382
560
|
child.unref();
|
|
383
|
-
|
|
561
|
+
resolve7();
|
|
384
562
|
});
|
|
385
563
|
});
|
|
386
564
|
}
|
|
@@ -607,17 +785,115 @@ function defaultSecretName(provider) {
|
|
|
607
785
|
function resolveWriteDevVarsTarget(cfg, requested) {
|
|
608
786
|
if (!requested) return null;
|
|
609
787
|
if (requested === true) return cfg.local.devVarsFile;
|
|
610
|
-
return (0,
|
|
788
|
+
return (0, import_node_path6.resolve)((0, import_node_path6.dirname)(cfg.configPath), requested);
|
|
611
789
|
}
|
|
612
790
|
async function safeText(res) {
|
|
613
791
|
try {
|
|
614
|
-
return
|
|
792
|
+
return redactSecrets((await res.text()).slice(0, 500));
|
|
615
793
|
} catch {
|
|
616
794
|
return "";
|
|
617
795
|
}
|
|
618
796
|
}
|
|
619
|
-
|
|
620
|
-
|
|
797
|
+
|
|
798
|
+
// src/secrets.ts
|
|
799
|
+
var PROD_ENV_NAMES = /* @__PURE__ */ new Set(["prod", "production"]);
|
|
800
|
+
async function secretsPush(options) {
|
|
801
|
+
const out = options.stdout ?? console;
|
|
802
|
+
const cfg = await loadProjectConfig(options.configPath);
|
|
803
|
+
const env = options.env;
|
|
804
|
+
if (!cfg.envs.includes(env)) {
|
|
805
|
+
throw new Error(`env "${env}" is not in config envs (${cfg.envs.join(", ")})`);
|
|
806
|
+
}
|
|
807
|
+
if (PROD_ENV_NAMES.has(env) && !options.yes) {
|
|
808
|
+
throw new Error(`refusing to push a secret to "${env}" without --yes`);
|
|
809
|
+
}
|
|
810
|
+
const credentialsPath = displayPath(cfg.local.credentialsFile, cfg.rootDir);
|
|
811
|
+
const credentials = readCredentials(cfg.local.credentialsFile);
|
|
812
|
+
if (!credentials) throw new Error(`no credentials at ${credentialsPath} \u2014 run "odla-ai provision" first`);
|
|
813
|
+
if (credentials.appId !== cfg.app.id) {
|
|
814
|
+
throw new Error(`credentials at ${credentialsPath} are for "${credentials.appId}", not "${cfg.app.id}"`);
|
|
815
|
+
}
|
|
816
|
+
const dbKey = credentials.envs[env]?.dbKey;
|
|
817
|
+
if (!dbKey) throw new Error(`no db key for env "${env}" in ${credentialsPath} \u2014 run "odla-ai provision" first`);
|
|
818
|
+
const wranglerConfig = findWranglerConfig(cfg.rootDir);
|
|
819
|
+
if (!wranglerConfig) {
|
|
820
|
+
throw new Error(`no wrangler config found in ${cfg.rootDir} (wrangler.jsonc, wrangler.json, or wrangler.toml)`);
|
|
821
|
+
}
|
|
822
|
+
const wranglerEnv = PROD_ENV_NAMES.has(env) ? void 0 : env;
|
|
823
|
+
const target = wranglerEnv ? `wrangler env "${wranglerEnv}"` : "the top-level (prod) wrangler env";
|
|
824
|
+
if (options.dryRun) {
|
|
825
|
+
out.log(`dry run: would push ODLA_API_KEY (${redactSecrets(dbKey)}) to ${target}`);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
const run = options.runner ?? defaultRunner;
|
|
829
|
+
if (!await wranglerLoggedIn(run, cfg.rootDir)) {
|
|
830
|
+
throw new Error(`wrangler is not logged in \u2014 run "wrangler login" (a browser step for the human)`);
|
|
831
|
+
}
|
|
832
|
+
const result = await wranglerPutSecret(run, { name: "ODLA_API_KEY", value: dbKey, env: wranglerEnv, cwd: cfg.rootDir });
|
|
833
|
+
if (result.code !== 0) {
|
|
834
|
+
throw new Error(`wrangler secret put failed (exit ${result.code}): ${redactSecrets(`${result.stderr || result.stdout}`.trim())}`);
|
|
835
|
+
}
|
|
836
|
+
out.log(`ODLA_API_KEY pushed to ${target} (value read from ${credentialsPath}, never echoed)`);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// src/skill.ts
|
|
840
|
+
var import_node_fs6 = require("fs");
|
|
841
|
+
var import_node_os = require("os");
|
|
842
|
+
var import_node_path7 = require("path");
|
|
843
|
+
var import_node_url2 = require("url");
|
|
844
|
+
function installSkill(options = {}) {
|
|
845
|
+
const out = options.stdout ?? console;
|
|
846
|
+
const sourceDir = options.sourceDir ?? (0, import_node_url2.fileURLToPath)(new URL("../skills", importMetaUrl));
|
|
847
|
+
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");
|
|
848
|
+
const files = listFiles(sourceDir);
|
|
849
|
+
if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
|
|
850
|
+
const written = [];
|
|
851
|
+
const unchanged = [];
|
|
852
|
+
const conflicts = [];
|
|
853
|
+
for (const rel of files) {
|
|
854
|
+
const target = (0, import_node_path7.join)(targetDir, rel);
|
|
855
|
+
const source = (0, import_node_fs6.readFileSync)((0, import_node_path7.join)(sourceDir, rel), "utf8");
|
|
856
|
+
if ((0, import_node_fs6.existsSync)(target)) {
|
|
857
|
+
const current = (0, import_node_fs6.readFileSync)(target, "utf8");
|
|
858
|
+
if (current === source) {
|
|
859
|
+
unchanged.push(rel);
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
if (!options.force) {
|
|
863
|
+
conflicts.push(rel);
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
written.push(rel);
|
|
868
|
+
}
|
|
869
|
+
if (conflicts.length > 0) {
|
|
870
|
+
throw new Error(
|
|
871
|
+
`skill files modified locally (re-run with --force to overwrite):
|
|
872
|
+
${conflicts.map((f) => ` - ${(0, import_node_path7.join)(targetDir, f)}`).join("\n")}`
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
for (const rel of written) {
|
|
876
|
+
const target = (0, import_node_path7.join)(targetDir, rel);
|
|
877
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path7.join)(target, ".."), { recursive: true });
|
|
878
|
+
(0, import_node_fs6.writeFileSync)(target, (0, import_node_fs6.readFileSync)((0, import_node_path7.join)(sourceDir, rel), "utf8"));
|
|
879
|
+
}
|
|
880
|
+
const skills = [...new Set(files.map((f) => f.split(/[\\/]/)[0]))].sort();
|
|
881
|
+
out.log(`skills: ${skills.join(", ")}`);
|
|
882
|
+
out.log(`installed ${written.length} file(s) to ${targetDir}${unchanged.length ? ` (${unchanged.length} unchanged)` : ""}`);
|
|
883
|
+
return { targetDir, written, unchanged };
|
|
884
|
+
}
|
|
885
|
+
function listFiles(dir) {
|
|
886
|
+
if (!(0, import_node_fs6.existsSync)(dir)) return [];
|
|
887
|
+
const results = [];
|
|
888
|
+
const walk = (current) => {
|
|
889
|
+
for (const entry of (0, import_node_fs6.readdirSync)(current, { withFileTypes: true })) {
|
|
890
|
+
const path = (0, import_node_path7.join)(current, entry.name);
|
|
891
|
+
if (entry.isDirectory()) walk(path);
|
|
892
|
+
else results.push((0, import_node_path7.relative)(dir, path));
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
walk(dir);
|
|
896
|
+
return results.sort();
|
|
621
897
|
}
|
|
622
898
|
|
|
623
899
|
// src/smoke.ts
|
|
@@ -751,6 +1027,27 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
751
1027
|
});
|
|
752
1028
|
return;
|
|
753
1029
|
}
|
|
1030
|
+
if (command === "skill") {
|
|
1031
|
+
const sub = parsed.positionals[1];
|
|
1032
|
+
if (sub !== "install") throw new Error(`unknown skill subcommand "${sub ?? ""}". Try "odla-ai skill install".`);
|
|
1033
|
+
installSkill({
|
|
1034
|
+
dir: stringOpt(parsed.options.dir),
|
|
1035
|
+
global: parsed.options.global === true,
|
|
1036
|
+
force: parsed.options.force === true
|
|
1037
|
+
});
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
if (command === "secrets") {
|
|
1041
|
+
const sub = parsed.positionals[1];
|
|
1042
|
+
if (sub !== "push") throw new Error(`unknown secrets subcommand "${sub ?? ""}". Try "odla-ai secrets push --env dev".`);
|
|
1043
|
+
await secretsPush({
|
|
1044
|
+
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
1045
|
+
env: requiredString(parsed.options.env, "--env"),
|
|
1046
|
+
dryRun: parsed.options["dry-run"] === true,
|
|
1047
|
+
yes: parsed.options.yes === true
|
|
1048
|
+
});
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
754
1051
|
throw new Error(`unknown command "${command}". Run "odla-ai help".`);
|
|
755
1052
|
}
|
|
756
1053
|
function parseArgv(argv) {
|
|
@@ -809,7 +1106,7 @@ function listOpt(value) {
|
|
|
809
1106
|
return values.flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
|
|
810
1107
|
}
|
|
811
1108
|
function cliVersion() {
|
|
812
|
-
const pkg = JSON.parse((0,
|
|
1109
|
+
const pkg = JSON.parse((0, import_node_fs7.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
|
|
813
1110
|
return pkg.version ?? "unknown";
|
|
814
1111
|
}
|
|
815
1112
|
function printHelp() {
|
|
@@ -820,6 +1117,8 @@ Usage:
|
|
|
820
1117
|
odla-ai doctor [--config odla.config.mjs]
|
|
821
1118
|
odla-ai provision [--config odla.config.mjs] [--dry-run] [--open|--no-open] [--rotate-keys] [--write-dev-vars[=path]]
|
|
822
1119
|
odla-ai smoke [--config odla.config.mjs] [--env dev]
|
|
1120
|
+
odla-ai skill install [--dir <project>] [--global] [--force]
|
|
1121
|
+
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
823
1122
|
odla-ai version
|
|
824
1123
|
|
|
825
1124
|
Commands:
|
|
@@ -827,6 +1126,8 @@ Commands:
|
|
|
827
1126
|
doctor Validate and summarize the project config without network calls.
|
|
828
1127
|
provision Register the app, enable services, push schema/rules, configure AI/auth.
|
|
829
1128
|
smoke Verify local credentials, public-config, live schema, and db aggregate.
|
|
1129
|
+
skill Install the bundled Claude Code skills into .claude/skills/.
|
|
1130
|
+
secrets Push the env's db key into the Worker via wrangler, stdin-piped.
|
|
830
1131
|
version Print the CLI version.
|
|
831
1132
|
|
|
832
1133
|
Safety:
|