@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
|
@@ -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
|
|
151
|
-
import { dirname as dirname3, resolve as
|
|
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
|
|
155
|
-
import { dirname as dirname2, relative, resolve as
|
|
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(
|
|
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 =
|
|
194
|
-
const existing =
|
|
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 =
|
|
226
|
-
const configPath =
|
|
227
|
-
if (
|
|
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(
|
|
238
|
-
mkdirSync2(
|
|
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(
|
|
241
|
-
writeIfMissing(
|
|
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 (
|
|
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
|
|
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 ??
|
|
345
|
-
await new Promise((
|
|
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
|
-
|
|
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
|
|
758
|
+
return resolve5(dirname4(cfg.configPath), requested);
|
|
581
759
|
}
|
|
582
760
|
async function safeText(res) {
|
|
583
761
|
try {
|
|
584
|
-
return
|
|
762
|
+
return redactSecrets((await res.text()).slice(0, 500));
|
|
585
763
|
} catch {
|
|
586
764
|
return "";
|
|
587
765
|
}
|
|
588
766
|
}
|
|
589
|
-
|
|
590
|
-
|
|
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
|
|
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(
|
|
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-
|
|
1122
|
+
//# sourceMappingURL=chunk-5J4LKP37.js.map
|