@odla-ai/cli 0.4.0 → 0.6.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 +126 -31
- package/REQUIREMENTS.md +18 -3
- package/dist/bin.cjs +1340 -436
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/chunk-OERLHVLH.js +2107 -0
- package/dist/chunk-OERLHVLH.js.map +1 -0
- package/dist/index.cjs +1353 -435
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +314 -21
- package/dist/index.d.ts +314 -21
- package/dist/index.js +15 -1
- package/llms.txt +442 -54
- package/package.json +15 -6
- package/skills/odla/SKILL.md +43 -12
- package/skills/odla/references/build.md +65 -17
- package/skills/odla/references/sdks.md +36 -3
- package/skills/odla-migrate/SKILL.md +38 -13
- package/skills/odla-migrate/references/phase-0-preflight.md +0 -2
- package/skills/odla-migrate/references/phase-1-static.md +2 -4
- package/skills/odla-migrate/references/phase-2-db.md +25 -17
- package/skills/odla-migrate/references/phase-3-auth.md +106 -20
- package/skills/odla-migrate/references/phase-3b-user-sync.md +57 -0
- package/skills/odla-migrate/references/phase-4-ai.md +6 -5
- package/skills/odla-migrate/references/phase-5-cutover.md +47 -16
- package/skills/odla-migrate/references/secrets-map.md +26 -11
- package/skills/odla-migrate/references/troubleshooting.md +27 -13
- package/skills/odla-o11y-debug/SKILL.md +93 -0
- package/dist/chunk-UWT7C6VT.js +0 -1187
- package/dist/chunk-UWT7C6VT.js.map +0 -1
package/dist/chunk-UWT7C6VT.js
DELETED
|
@@ -1,1187 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
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
|
-
[/\bo11y_[A-Za-z0-9]+/g, "o11y_[redacted]"],
|
|
10
|
-
[/\b(ghp|gho|github_pat)_[A-Za-z0-9_]+/g, "$1_[redacted]"],
|
|
11
|
-
[/\bAKIA[A-Z0-9]{12,}/g, "AKIA[redacted]"]
|
|
12
|
-
];
|
|
13
|
-
function redactSecrets(value) {
|
|
14
|
-
let result = value;
|
|
15
|
-
for (const [pattern, replacement] of REPLACEMENTS) result = result.replace(pattern, replacement);
|
|
16
|
-
return result;
|
|
17
|
-
}
|
|
18
|
-
function looksSecret(value) {
|
|
19
|
-
return redactSecrets(value) !== value || value.includes("-----BEGIN");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// src/config.ts
|
|
23
|
-
import { existsSync, readFileSync } from "fs";
|
|
24
|
-
import { dirname, isAbsolute, resolve } from "path";
|
|
25
|
-
import { pathToFileURL } from "url";
|
|
26
|
-
var DEFAULT_PLATFORM = "https://odla.ai";
|
|
27
|
-
var DEFAULT_ENVS = ["prod", "dev"];
|
|
28
|
-
var DEFAULT_SERVICES = ["db", "ai"];
|
|
29
|
-
async function loadProjectConfig(configPath = "odla.config.mjs") {
|
|
30
|
-
const resolved = resolve(configPath);
|
|
31
|
-
if (!existsSync(resolved)) {
|
|
32
|
-
throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
|
|
33
|
-
}
|
|
34
|
-
const raw = await loadConfigModule(resolved);
|
|
35
|
-
const rootDir = dirname(resolved);
|
|
36
|
-
validateRawConfig(raw, resolved);
|
|
37
|
-
const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
|
|
38
|
-
const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
|
|
39
|
-
const envs = unique(raw.envs?.length ? raw.envs : DEFAULT_ENVS);
|
|
40
|
-
const services = unique(raw.services?.length ? raw.services : DEFAULT_SERVICES);
|
|
41
|
-
const local = {
|
|
42
|
-
tokenFile: resolve(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
|
|
43
|
-
credentialsFile: resolve(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
|
|
44
|
-
devVarsFile: resolve(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
|
|
45
|
-
gitignore: raw.local?.gitignore ?? true
|
|
46
|
-
};
|
|
47
|
-
return {
|
|
48
|
-
...raw,
|
|
49
|
-
configPath: resolved,
|
|
50
|
-
rootDir,
|
|
51
|
-
platformUrl,
|
|
52
|
-
dbEndpoint,
|
|
53
|
-
envs,
|
|
54
|
-
services,
|
|
55
|
-
local
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
async function resolveDataExport(cfg, value, names) {
|
|
59
|
-
if (value === void 0 || value === null || value === false) return void 0;
|
|
60
|
-
if (typeof value !== "string") return value;
|
|
61
|
-
const target = isAbsolute(value) ? value : resolve(cfg.rootDir, value);
|
|
62
|
-
if (target.endsWith(".json")) {
|
|
63
|
-
return JSON.parse(readFileSync(target, "utf8"));
|
|
64
|
-
}
|
|
65
|
-
const mod = await import(pathToFileURL(target).href);
|
|
66
|
-
for (const name of names) {
|
|
67
|
-
if (mod[name] !== void 0) return mod[name];
|
|
68
|
-
}
|
|
69
|
-
if (mod.default !== void 0) return mod.default;
|
|
70
|
-
throw new Error(`${value} did not export ${names.join(", ")} or default`);
|
|
71
|
-
}
|
|
72
|
-
function buildPlan(cfg) {
|
|
73
|
-
return {
|
|
74
|
-
appId: cfg.app.id,
|
|
75
|
-
appName: cfg.app.name,
|
|
76
|
-
platformUrl: cfg.platformUrl,
|
|
77
|
-
dbEndpoint: cfg.dbEndpoint,
|
|
78
|
-
envs: cfg.envs,
|
|
79
|
-
services: cfg.services,
|
|
80
|
-
hasSchema: !!cfg.db?.schema,
|
|
81
|
-
hasRules: !!cfg.db?.rules || !!cfg.db?.schema && cfg.db.defaultRules !== false,
|
|
82
|
-
aiProvider: cfg.ai?.provider
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
function rulesFromSchema(schema) {
|
|
86
|
-
const entities = serializedEntities(schema);
|
|
87
|
-
return Object.fromEntries(
|
|
88
|
-
entities.map((name) => [name, { view: "false", create: "false", update: "false", delete: "false" }])
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
function serializedEntities(schema) {
|
|
92
|
-
if (!schema || typeof schema !== "object") return [];
|
|
93
|
-
const entities = schema.entities;
|
|
94
|
-
if (!entities || typeof entities !== "object" || Array.isArray(entities)) return [];
|
|
95
|
-
return Object.keys(entities);
|
|
96
|
-
}
|
|
97
|
-
function envValue(value) {
|
|
98
|
-
if (!value) return void 0;
|
|
99
|
-
if (value.startsWith("$")) return process.env[value.slice(1)];
|
|
100
|
-
return value;
|
|
101
|
-
}
|
|
102
|
-
function validateRawConfig(raw, path) {
|
|
103
|
-
if (!raw || typeof raw !== "object") throw new Error(`${path} must export an object`);
|
|
104
|
-
const cfg = raw;
|
|
105
|
-
if (!cfg.app || typeof cfg.app !== "object") throw new Error(`${path}: missing app object`);
|
|
106
|
-
if (!validId(cfg.app.id)) throw new Error(`${path}: app.id must be lowercase letters, numbers, and hyphens`);
|
|
107
|
-
if (!cfg.app.name || typeof cfg.app.name !== "string") throw new Error(`${path}: app.name is required`);
|
|
108
|
-
if (cfg.envs?.some((env) => !validId(env))) throw new Error(`${path}: env names must be lowercase letters, numbers, and hyphens`);
|
|
109
|
-
}
|
|
110
|
-
function validId(value) {
|
|
111
|
-
return typeof value === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value);
|
|
112
|
-
}
|
|
113
|
-
async function loadConfigModule(path) {
|
|
114
|
-
if (path.endsWith(".json")) return JSON.parse(readFileSync(path, "utf8"));
|
|
115
|
-
const mod = await import(`${pathToFileURL(path).href}?t=${Date.now()}`);
|
|
116
|
-
const value = mod.default ?? mod.config;
|
|
117
|
-
if (typeof value === "function") return await value();
|
|
118
|
-
return value;
|
|
119
|
-
}
|
|
120
|
-
function trimSlash(value) {
|
|
121
|
-
return value.replace(/\/+$/, "");
|
|
122
|
-
}
|
|
123
|
-
function unique(values) {
|
|
124
|
-
return [...new Set(values.filter(Boolean))];
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// src/doctor-checks.ts
|
|
128
|
-
import { execFileSync } from "child_process";
|
|
129
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
130
|
-
import { join as join2, resolve as resolve2 } from "path";
|
|
131
|
-
|
|
132
|
-
// src/wrangler.ts
|
|
133
|
-
import { spawn } from "child_process";
|
|
134
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
135
|
-
import { join } from "path";
|
|
136
|
-
var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
|
|
137
|
-
const child = spawn(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
138
|
-
let stdout = "";
|
|
139
|
-
let stderr = "";
|
|
140
|
-
child.stdout.on("data", (chunk) => stdout += chunk.toString());
|
|
141
|
-
child.stderr.on("data", (chunk) => stderr += chunk.toString());
|
|
142
|
-
child.on("error", reject);
|
|
143
|
-
child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout, stderr }));
|
|
144
|
-
child.stdin.end(opts?.input ?? "");
|
|
145
|
-
});
|
|
146
|
-
var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
|
|
147
|
-
function findWranglerConfig(rootDir) {
|
|
148
|
-
for (const name of WRANGLER_CONFIG_FILES) {
|
|
149
|
-
const path = join(rootDir, name);
|
|
150
|
-
if (existsSync2(path)) return path;
|
|
151
|
-
}
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
function readWranglerConfig(path) {
|
|
155
|
-
if (path.endsWith(".toml")) return null;
|
|
156
|
-
try {
|
|
157
|
-
return JSON.parse(stripJsonComments(readFileSync2(path, "utf8")));
|
|
158
|
-
} catch {
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
function stripJsonComments(text) {
|
|
163
|
-
let result = "";
|
|
164
|
-
let inString = false;
|
|
165
|
-
for (let i = 0; i < text.length; i++) {
|
|
166
|
-
const ch = text[i];
|
|
167
|
-
if (inString) {
|
|
168
|
-
result += ch;
|
|
169
|
-
if (ch === "\\") {
|
|
170
|
-
result += text[i + 1] ?? "";
|
|
171
|
-
i++;
|
|
172
|
-
} else if (ch === '"') {
|
|
173
|
-
inString = false;
|
|
174
|
-
}
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
if (ch === '"') {
|
|
178
|
-
inString = true;
|
|
179
|
-
result += ch;
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
if (ch === "/" && text[i + 1] === "/") {
|
|
183
|
-
while (i < text.length && text[i] !== "\n") i++;
|
|
184
|
-
result += "\n";
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
if (ch === "/" && text[i + 1] === "*") {
|
|
188
|
-
i += 2;
|
|
189
|
-
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
|
|
190
|
-
i++;
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
result += ch;
|
|
194
|
-
}
|
|
195
|
-
return result;
|
|
196
|
-
}
|
|
197
|
-
async function wranglerLoggedIn(run, cwd) {
|
|
198
|
-
try {
|
|
199
|
-
const result = await run("npx", ["wrangler", "whoami"], { cwd });
|
|
200
|
-
return result.code === 0 && !/not authenticated/i.test(`${result.stdout}${result.stderr}`);
|
|
201
|
-
} catch {
|
|
202
|
-
return false;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
function wranglerPutSecret(run, opts) {
|
|
206
|
-
const args = ["wrangler", "secret", "put", opts.name, ...opts.env ? ["--env", opts.env] : []];
|
|
207
|
-
return run("npx", args, { input: opts.value, cwd: opts.cwd });
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// src/doctor-checks.ts
|
|
211
|
-
function lintRules(rules, entities, publicRead) {
|
|
212
|
-
const warnings = [];
|
|
213
|
-
if (!rules) return warnings;
|
|
214
|
-
for (const [ns, actions] of Object.entries(rules)) {
|
|
215
|
-
for (const [action, expr] of Object.entries(actions)) {
|
|
216
|
-
if (typeof expr !== "string" || expr.trim() !== "true") continue;
|
|
217
|
-
if (action === "view" && publicRead.includes(ns)) continue;
|
|
218
|
-
warnings.push(
|
|
219
|
-
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`
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
for (const entity of entities) {
|
|
224
|
-
if (!(entity in rules)) warnings.push(`schema entity "${entity}" has no rules entry (all client access denied)`);
|
|
225
|
-
}
|
|
226
|
-
return warnings;
|
|
227
|
-
}
|
|
228
|
-
var defaultExec = (cmd, args, cwd) => execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
229
|
-
function trackedSecretFiles(rootDir, exec = defaultExec) {
|
|
230
|
-
let output;
|
|
231
|
-
try {
|
|
232
|
-
output = exec("git", ["ls-files", "--", ".dev.vars", ".odla"], rootDir);
|
|
233
|
-
} catch {
|
|
234
|
-
return [];
|
|
235
|
-
}
|
|
236
|
-
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`);
|
|
237
|
-
}
|
|
238
|
-
function wranglerWarnings(rootDir) {
|
|
239
|
-
const configPath = findWranglerConfig(rootDir);
|
|
240
|
-
if (!configPath) return [];
|
|
241
|
-
const config = readWranglerConfig(configPath);
|
|
242
|
-
if (!config) return [];
|
|
243
|
-
const warnings = [];
|
|
244
|
-
const blocks = [{ label: "", block: config }];
|
|
245
|
-
const envs = config.env;
|
|
246
|
-
if (envs && typeof envs === "object") {
|
|
247
|
-
for (const [name, block] of Object.entries(envs)) {
|
|
248
|
-
if (block && typeof block === "object") blocks.push({ label: `env.${name}.`, block });
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
for (const { label, block } of blocks) {
|
|
252
|
-
const assets = block.assets;
|
|
253
|
-
if (assets?.directory) {
|
|
254
|
-
const dir = resolve2(rootDir, assets.directory);
|
|
255
|
-
if (dir === resolve2(rootDir)) {
|
|
256
|
-
warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
|
|
257
|
-
} else if (existsSync3(join2(dir, "node_modules"))) {
|
|
258
|
-
warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
const vars = block.vars;
|
|
262
|
-
if (vars && typeof vars === "object") {
|
|
263
|
-
for (const [name, value] of Object.entries(vars)) {
|
|
264
|
-
if (name === "ODLA_API_KEY" || name === "ODLA_O11Y_TOKEN" || typeof value === "string" && looksSecret(value)) {
|
|
265
|
-
warnings.push(`wrangler ${label}vars.${name} looks like a secret \u2014 use "odla-ai secrets push" / wrangler secret put, never vars`);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
const pkg = readPackageJson(rootDir);
|
|
271
|
-
if (pkg && typeof pkg.scripts === "object" && pkg.scripts && "deploy" in pkg.scripts) {
|
|
272
|
-
warnings.push(`package.json has a script named "deploy" \u2014 CI may auto-deploy it; rename to deploy:app`);
|
|
273
|
-
}
|
|
274
|
-
return warnings;
|
|
275
|
-
}
|
|
276
|
-
function readPackageJson(rootDir) {
|
|
277
|
-
try {
|
|
278
|
-
return JSON.parse(readFileSync3(join2(rootDir, "package.json"), "utf8"));
|
|
279
|
-
} catch {
|
|
280
|
-
return null;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// src/local.ts
|
|
285
|
-
import { chmodSync, existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
286
|
-
import { dirname as dirname2, relative, resolve as resolve3 } from "path";
|
|
287
|
-
var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
|
|
288
|
-
function readJsonFile(path) {
|
|
289
|
-
try {
|
|
290
|
-
return JSON.parse(readFileSync4(path, "utf8"));
|
|
291
|
-
} catch {
|
|
292
|
-
return null;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
function writePrivateJson(path, value) {
|
|
296
|
-
mkdirSync(dirname2(path), { recursive: true });
|
|
297
|
-
writeFileSync(path, `${JSON.stringify(value, null, 2)}
|
|
298
|
-
`);
|
|
299
|
-
chmodSync(path, 384);
|
|
300
|
-
}
|
|
301
|
-
function readCredentials(path) {
|
|
302
|
-
return readJsonFile(path);
|
|
303
|
-
}
|
|
304
|
-
function mergeCredential(current, update) {
|
|
305
|
-
const next = current ?? {
|
|
306
|
-
appId: update.appId,
|
|
307
|
-
platformUrl: update.platformUrl,
|
|
308
|
-
dbEndpoint: update.dbEndpoint,
|
|
309
|
-
updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
310
|
-
envs: {}
|
|
311
|
-
};
|
|
312
|
-
next.appId = update.appId;
|
|
313
|
-
next.platformUrl = update.platformUrl;
|
|
314
|
-
next.dbEndpoint = update.dbEndpoint;
|
|
315
|
-
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
316
|
-
next.envs[update.env] = {
|
|
317
|
-
...next.envs[update.env] ?? {},
|
|
318
|
-
tenantId: update.tenantId,
|
|
319
|
-
...update.dbKey ? { dbKey: update.dbKey } : {},
|
|
320
|
-
...update.o11yToken ? { o11yToken: update.o11yToken } : {}
|
|
321
|
-
};
|
|
322
|
-
return next;
|
|
323
|
-
}
|
|
324
|
-
function ensureGitignore(rootDir) {
|
|
325
|
-
const path = resolve3(rootDir, ".gitignore");
|
|
326
|
-
const existing = existsSync4(path) ? readFileSync4(path, "utf8") : "";
|
|
327
|
-
const missing = GITIGNORE_LINES.filter((line) => !existing.split(/\r?\n/).includes(line));
|
|
328
|
-
if (missing.length === 0) return;
|
|
329
|
-
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
330
|
-
writeFileSync(path, `${existing}${prefix}${missing.join("\n")}
|
|
331
|
-
`);
|
|
332
|
-
}
|
|
333
|
-
function writeDevVars(path, credentials, env, o11y) {
|
|
334
|
-
const entry = credentials.envs[env];
|
|
335
|
-
if (!entry?.dbKey) throw new Error(`no db key for env "${env}" in ${path}`);
|
|
336
|
-
const lines = [
|
|
337
|
-
`ODLA_PLATFORM="${credentials.platformUrl}"`,
|
|
338
|
-
`ODLA_ENDPOINT="${credentials.dbEndpoint}"`,
|
|
339
|
-
`ODLA_APP_ID="${credentials.appId}"`,
|
|
340
|
-
`ODLA_ENV="${env}"`,
|
|
341
|
-
`ODLA_TENANT="${entry.tenantId}"`,
|
|
342
|
-
`ODLA_API_KEY="${entry.dbKey}"`
|
|
343
|
-
];
|
|
344
|
-
if (o11y) {
|
|
345
|
-
lines.push(`ODLA_O11Y_ENDPOINT="${o11y.endpoint}"`, `ODLA_O11Y_SERVICE="${o11y.service}"`);
|
|
346
|
-
if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
|
|
347
|
-
if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
|
|
348
|
-
}
|
|
349
|
-
mkdirSync(dirname2(path), { recursive: true });
|
|
350
|
-
writeFileSync(path, `${lines.join("\n")}
|
|
351
|
-
`);
|
|
352
|
-
chmodSync(path, 384);
|
|
353
|
-
}
|
|
354
|
-
function displayPath(path, rootDir = process.cwd()) {
|
|
355
|
-
const rel = relative(rootDir, path);
|
|
356
|
-
return rel && !rel.startsWith("..") ? rel : path;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// src/doctor.ts
|
|
360
|
-
async function doctor(options) {
|
|
361
|
-
const out = options.stdout ?? console;
|
|
362
|
-
const cfg = await loadProjectConfig(options.configPath);
|
|
363
|
-
const plan = buildPlan(cfg);
|
|
364
|
-
const schema = await resolveDataExport(cfg, cfg.db?.schema, ["schema", "SCHEMA"]);
|
|
365
|
-
const configuredRules = await resolveDataExport(cfg, cfg.db?.rules, ["rules", "RULES"]);
|
|
366
|
-
const rules = configuredRules ?? (schema && cfg.db?.defaultRules !== false ? rulesFromSchema(schema) : void 0);
|
|
367
|
-
const entities = serializedEntities(schema);
|
|
368
|
-
out.log(`config: ${cfg.configPath}`);
|
|
369
|
-
out.log(`app: ${plan.appName} (${plan.appId})`);
|
|
370
|
-
out.log(`envs: ${plan.envs.join(", ")}`);
|
|
371
|
-
out.log(`svc: ${plan.services.join(", ")}`);
|
|
372
|
-
out.log(`schema: ${schema ? `${entities.length} entities` : "none"}`);
|
|
373
|
-
out.log(`rules: ${rules ? `${Object.keys(rules).length} namespaces` : "none"}`);
|
|
374
|
-
out.log(`ai: ${cfg.ai?.provider ?? "not configured"}`);
|
|
375
|
-
const warnings = [];
|
|
376
|
-
if (schema && entities.length === 0) warnings.push("schema has no entities");
|
|
377
|
-
if (rules) {
|
|
378
|
-
for (const ns of Object.keys(rules)) {
|
|
379
|
-
if (entities.length > 0 && !entities.includes(ns)) warnings.push(`rules include "${ns}" but schema has no matching entity`);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
if (cfg.services.includes("ai") && !cfg.ai?.provider) warnings.push("ai service is enabled but ai.provider is not set");
|
|
383
|
-
if (cfg.auth?.clerk) {
|
|
384
|
-
for (const [env, value] of Object.entries(cfg.auth.clerk)) {
|
|
385
|
-
if (typeof value === "string" && value.startsWith("$") && !process.env[value.slice(1)]) {
|
|
386
|
-
warnings.push(`auth.clerk.${env} references unset env var ${value}`);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
if (cfg.ai?.keyEnv && !process.env[cfg.ai.keyEnv]) warnings.push(`${cfg.ai.keyEnv} is not set; provision will skip provider key storage`);
|
|
391
|
-
if (cfg.services.includes("o11y")) {
|
|
392
|
-
const credentials = readCredentials(cfg.local.credentialsFile);
|
|
393
|
-
for (const env of cfg.envs) {
|
|
394
|
-
if (!credentials?.envs[env]?.o11yToken) {
|
|
395
|
-
warnings.push(`o11y is enabled but env "${env}" has no ingest token \u2014 run "odla-ai provision" to mint one`);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
warnings.push(...lintRules(rules, entities, cfg.db?.publicRead ?? []));
|
|
400
|
-
warnings.push(...trackedSecretFiles(cfg.rootDir));
|
|
401
|
-
warnings.push(...wranglerWarnings(cfg.rootDir));
|
|
402
|
-
if (warnings.length) {
|
|
403
|
-
out.log("");
|
|
404
|
-
out.log("warnings:");
|
|
405
|
-
for (const warning of warnings) out.log(` - ${warning}`);
|
|
406
|
-
} else {
|
|
407
|
-
out.log("ok");
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// src/init.ts
|
|
412
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
413
|
-
import { dirname as dirname3, resolve as resolve4 } from "path";
|
|
414
|
-
function initProject(options) {
|
|
415
|
-
const out = options.stdout ?? console;
|
|
416
|
-
const rootDir = resolve4(options.rootDir ?? process.cwd());
|
|
417
|
-
const configPath = resolve4(rootDir, options.configPath ?? "odla.config.mjs");
|
|
418
|
-
if (existsSync5(configPath) && !options.force) {
|
|
419
|
-
throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
|
|
420
|
-
}
|
|
421
|
-
if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
|
|
422
|
-
throw new Error("--app-id must be lowercase letters, numbers, and hyphens");
|
|
423
|
-
}
|
|
424
|
-
const envs = options.envs?.length ? options.envs : ["prod", "dev"];
|
|
425
|
-
const services = options.services?.length ? options.services : ["db", "ai"];
|
|
426
|
-
const aiProvider = options.aiProvider ?? "anthropic";
|
|
427
|
-
mkdirSync2(dirname3(configPath), { recursive: true });
|
|
428
|
-
mkdirSync2(resolve4(rootDir, "src/odla"), { recursive: true });
|
|
429
|
-
mkdirSync2(resolve4(rootDir, ".odla"), { recursive: true });
|
|
430
|
-
writeFileSync2(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
|
|
431
|
-
writeIfMissing(resolve4(rootDir, "src/odla/schema.mjs"), schemaTemplate());
|
|
432
|
-
writeIfMissing(resolve4(rootDir, "src/odla/rules.mjs"), rulesTemplate());
|
|
433
|
-
ensureGitignore(rootDir);
|
|
434
|
-
out.log(`created ${relativeDisplay(configPath, rootDir)}`);
|
|
435
|
-
out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
|
|
436
|
-
out.log("updated .gitignore for local odla credentials");
|
|
437
|
-
}
|
|
438
|
-
function writeIfMissing(path, text) {
|
|
439
|
-
if (existsSync5(path)) return;
|
|
440
|
-
writeFileSync2(path, text);
|
|
441
|
-
}
|
|
442
|
-
function configTemplate(input) {
|
|
443
|
-
return `export default {
|
|
444
|
-
platformUrl: process.env.ODLA_PLATFORM_URL ?? "https://odla.ai",
|
|
445
|
-
dbEndpoint: process.env.ODLA_ENDPOINT ?? process.env.ODLA_DB_ENDPOINT ?? "https://db.odla.ai",
|
|
446
|
-
app: {
|
|
447
|
-
id: "${input.appId}",
|
|
448
|
-
name: "${input.name.replace(/"/g, '\\"')}",
|
|
449
|
-
},
|
|
450
|
-
envs: ${JSON.stringify(input.envs)},
|
|
451
|
-
services: ${JSON.stringify(input.services)},
|
|
452
|
-
db: {
|
|
453
|
-
schema: "./src/odla/schema.mjs",
|
|
454
|
-
rules: "./src/odla/rules.mjs",
|
|
455
|
-
// When rules is omitted, the CLI generates deny-all rules from schema.
|
|
456
|
-
defaultRules: "deny",
|
|
457
|
-
},
|
|
458
|
-
ai: {
|
|
459
|
-
provider: process.env.ODLA_AI_PROVIDER ?? "${input.aiProvider}",
|
|
460
|
-
// Optional: set this env var while running provision to store the provider
|
|
461
|
-
// key in the platform vault for each tenant.
|
|
462
|
-
keyEnv: "${defaultKeyEnv(input.aiProvider)}",
|
|
463
|
-
},
|
|
464
|
-
auth: {
|
|
465
|
-
clerk: {
|
|
466
|
-
// dev: "$CLERK_PUBLISHABLE_KEY",
|
|
467
|
-
// prod: "$CLERK_PUBLISHABLE_KEY",
|
|
468
|
-
},
|
|
469
|
-
},
|
|
470
|
-
// Add "o11y" to services to enable observability; provision then mints the
|
|
471
|
-
// ingest token and scaffolds the ODLA_O11Y_* vars into .dev.vars.
|
|
472
|
-
// o11y: {
|
|
473
|
-
// service: "${input.appId}", // defaults to the app id
|
|
474
|
-
// // endpoint: "https://o11y.odla.ai",
|
|
475
|
-
// },
|
|
476
|
-
links: {
|
|
477
|
-
// dev: "https://dev.example.com",
|
|
478
|
-
// prod: "https://example.com",
|
|
479
|
-
},
|
|
480
|
-
local: {
|
|
481
|
-
tokenFile: ".odla/dev-token.json",
|
|
482
|
-
credentialsFile: ".odla/credentials.local.json",
|
|
483
|
-
devVarsFile: ".dev.vars",
|
|
484
|
-
},
|
|
485
|
-
};
|
|
486
|
-
`;
|
|
487
|
-
}
|
|
488
|
-
function schemaTemplate() {
|
|
489
|
-
return `// Replace this starter schema with your app's odla-db schema.
|
|
490
|
-
// Knowledge-graph projects can export @odla-ai/kg's toSerializedSchema(ontology).
|
|
491
|
-
export const schema = {
|
|
492
|
-
entities: {
|
|
493
|
-
notes: {
|
|
494
|
-
attrs: {
|
|
495
|
-
id: { type: "string", unique: true, indexed: true, optional: false },
|
|
496
|
-
text: { type: "string", unique: false, indexed: false, optional: false },
|
|
497
|
-
createdAt: { type: "number", unique: false, indexed: true, optional: false },
|
|
498
|
-
},
|
|
499
|
-
},
|
|
500
|
-
},
|
|
501
|
-
links: {},
|
|
502
|
-
};
|
|
503
|
-
`;
|
|
504
|
-
}
|
|
505
|
-
function rulesTemplate() {
|
|
506
|
-
return `// odla-db is default-deny. The starter keeps runtime data backend-only.
|
|
507
|
-
export const rules = {
|
|
508
|
-
notes: {
|
|
509
|
-
view: "false",
|
|
510
|
-
create: "false",
|
|
511
|
-
update: "false",
|
|
512
|
-
delete: "false",
|
|
513
|
-
},
|
|
514
|
-
};
|
|
515
|
-
`;
|
|
516
|
-
}
|
|
517
|
-
function defaultKeyEnv(provider) {
|
|
518
|
-
if (provider === "openai") return "OPENAI_API_KEY";
|
|
519
|
-
if (provider === "google") return "GOOGLE_API_KEY";
|
|
520
|
-
return "ANTHROPIC_API_KEY";
|
|
521
|
-
}
|
|
522
|
-
function relativeDisplay(path, rootDir) {
|
|
523
|
-
return path.startsWith(rootDir) ? path.slice(rootDir.length + 1) : path;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// src/provision.ts
|
|
527
|
-
import { createAppsClient, tenantIdFor } from "@odla-ai/apps";
|
|
528
|
-
import { DEFAULT_SECRET_NAMES, putSecret } from "@odla-ai/ai";
|
|
529
|
-
import { dirname as dirname4, resolve as resolve5 } from "path";
|
|
530
|
-
import process4 from "process";
|
|
531
|
-
|
|
532
|
-
// src/token.ts
|
|
533
|
-
import { requestToken } from "@odla-ai/db";
|
|
534
|
-
import process3 from "process";
|
|
535
|
-
|
|
536
|
-
// src/open.ts
|
|
537
|
-
import { spawn as spawn2 } from "child_process";
|
|
538
|
-
import process2 from "process";
|
|
539
|
-
async function openUrl(url, options = {}) {
|
|
540
|
-
const command = openerFor(options.platform ?? process2.platform);
|
|
541
|
-
const doSpawn = options.spawnImpl ?? spawn2;
|
|
542
|
-
await new Promise((resolve7, reject) => {
|
|
543
|
-
const child = doSpawn(command.cmd, [...command.args, url], {
|
|
544
|
-
stdio: "ignore",
|
|
545
|
-
detached: true
|
|
546
|
-
});
|
|
547
|
-
child.once("error", reject);
|
|
548
|
-
child.once("spawn", () => {
|
|
549
|
-
child.unref();
|
|
550
|
-
resolve7();
|
|
551
|
-
});
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
function openerFor(platform) {
|
|
555
|
-
if (platform === "darwin") return { cmd: "open", args: [] };
|
|
556
|
-
if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
|
|
557
|
-
return { cmd: "xdg-open", args: [] };
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// src/token.ts
|
|
561
|
-
async function getDeveloperToken(cfg, options, doFetch, out) {
|
|
562
|
-
if (options.token) return options.token;
|
|
563
|
-
if (process3.env.ODLA_DEV_TOKEN) return process3.env.ODLA_DEV_TOKEN;
|
|
564
|
-
const cached = readJsonFile(cfg.local.tokenFile);
|
|
565
|
-
if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
566
|
-
out.log(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
567
|
-
return cached.token;
|
|
568
|
-
}
|
|
569
|
-
const browser = approvalBrowser(options);
|
|
570
|
-
const { token, expiresAt } = await requestToken({
|
|
571
|
-
endpoint: cfg.platformUrl,
|
|
572
|
-
label: `${cfg.app.id} provisioner`,
|
|
573
|
-
fetch: doFetch,
|
|
574
|
-
onCode: async ({ userCode, expiresIn }) => {
|
|
575
|
-
const approvalUrl = handshakeUrl(cfg.platformUrl, userCode);
|
|
576
|
-
out.log("");
|
|
577
|
-
out.log(`Approve code ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
|
|
578
|
-
if (browser.open) {
|
|
579
|
-
try {
|
|
580
|
-
await (options.openApprovalUrl ?? openUrl)(approvalUrl);
|
|
581
|
-
out.log(`auth: opened browser for approval${browser.mode === "auto" ? " (auto)" : ""}`);
|
|
582
|
-
} catch (err) {
|
|
583
|
-
out.log(`auth: could not open browser (${err instanceof Error ? err.message : String(err)})`);
|
|
584
|
-
}
|
|
585
|
-
} else if (browser.reason) {
|
|
586
|
-
out.log(`auth: browser launch skipped (${browser.reason})`);
|
|
587
|
-
}
|
|
588
|
-
out.log("");
|
|
589
|
-
}
|
|
590
|
-
});
|
|
591
|
-
writePrivateJson(cfg.local.tokenFile, { token, expiresAt });
|
|
592
|
-
out.log(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
593
|
-
return token;
|
|
594
|
-
}
|
|
595
|
-
function approvalBrowser(options) {
|
|
596
|
-
if (options.open === true) return { open: true, mode: "forced" };
|
|
597
|
-
if (options.open === false) return { open: false, reason: "disabled by --no-open" };
|
|
598
|
-
if (process3.env.CI) return { open: false, reason: "CI environment" };
|
|
599
|
-
const interactive = options.interactive ?? Boolean(process3.stdin.isTTY && process3.stdout.isTTY);
|
|
600
|
-
if (!interactive) return { open: false, reason: "non-interactive shell; pass --open to force" };
|
|
601
|
-
return { open: true, mode: "auto" };
|
|
602
|
-
}
|
|
603
|
-
function handshakeUrl(platformUrl, userCode) {
|
|
604
|
-
const url = new URL("/handshakes", platformUrl);
|
|
605
|
-
url.searchParams.set("code", userCode);
|
|
606
|
-
return url.toString();
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
// src/provision.ts
|
|
610
|
-
async function provision(options) {
|
|
611
|
-
const out = options.stdout ?? console;
|
|
612
|
-
const cfg = await loadProjectConfig(options.configPath);
|
|
613
|
-
const plan = buildPlan(cfg);
|
|
614
|
-
out.log(`odla-ai: ${plan.appName} (${plan.appId})`);
|
|
615
|
-
out.log(` platform: ${plan.platformUrl}`);
|
|
616
|
-
out.log(` db: ${plan.dbEndpoint}`);
|
|
617
|
-
out.log(` envs: ${plan.envs.join(", ")}`);
|
|
618
|
-
out.log(` services: ${plan.services.join(", ")}`);
|
|
619
|
-
const schema = await resolveDataExport(cfg, cfg.db?.schema, ["schema", "SCHEMA"]);
|
|
620
|
-
const configuredRules = await resolveDataExport(cfg, cfg.db?.rules, ["rules", "RULES"]);
|
|
621
|
-
const rules = configuredRules ?? (schema && cfg.db?.defaultRules !== false ? rulesFromSchema(schema) : void 0);
|
|
622
|
-
if (options.dryRun) {
|
|
623
|
-
out.log("dry run: no network calls or file writes");
|
|
624
|
-
out.log(` schema: ${schema ? "yes" : "no"}`);
|
|
625
|
-
out.log(` rules: ${rules ? Object.keys(rules).length : 0} namespaces`);
|
|
626
|
-
out.log(` ai: ${cfg.ai?.provider ?? "not configured"}`);
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
const doFetch = options.fetch ?? fetch;
|
|
630
|
-
const token = await getDeveloperToken(cfg, options, doFetch, out);
|
|
631
|
-
const auth = { authorization: `Bearer ${token}`, "content-type": "application/json" };
|
|
632
|
-
const apps = createAppsClient({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
|
|
633
|
-
const existing = await readRegistryApp(cfg, token, doFetch);
|
|
634
|
-
if (existing) {
|
|
635
|
-
out.log(`app: ${cfg.app.id} already exists`);
|
|
636
|
-
} else {
|
|
637
|
-
await apps.createApp({ name: cfg.app.name, appId: cfg.app.id });
|
|
638
|
-
out.log(`app: created ${cfg.app.id}`);
|
|
639
|
-
}
|
|
640
|
-
for (const env of cfg.envs) {
|
|
641
|
-
for (const service of cfg.services) {
|
|
642
|
-
if (service === "ai") {
|
|
643
|
-
if (cfg.ai?.provider) {
|
|
644
|
-
await apps.setAi(cfg.app.id, env, { provider: cfg.ai.provider, ...cfg.ai.model ? { model: cfg.ai.model } : {} });
|
|
645
|
-
out.log(`${env}: ai configured (${cfg.ai.provider}${cfg.ai.model ? `/${cfg.ai.model}` : ""})`);
|
|
646
|
-
} else {
|
|
647
|
-
await apps.setService(cfg.app.id, "ai", true, { env });
|
|
648
|
-
out.log(`${env}: ai enabled`);
|
|
649
|
-
}
|
|
650
|
-
} else {
|
|
651
|
-
await apps.setService(cfg.app.id, service, true, { env });
|
|
652
|
-
out.log(`${env}: ${service} enabled`);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
const authConfig = normalizeClerkConfig(cfg.auth?.clerk?.[env]);
|
|
656
|
-
if (authConfig?.publishableKey) {
|
|
657
|
-
await apps.setAuth(cfg.app.id, env, authConfig);
|
|
658
|
-
out.log(`${env}: auth configured (clerk ${authConfig.mode ?? "client"})`);
|
|
659
|
-
}
|
|
660
|
-
const link = cfg.links?.[env];
|
|
661
|
-
if (link !== void 0) {
|
|
662
|
-
await apps.setLink(cfg.app.id, env, link ?? null);
|
|
663
|
-
out.log(`${env}: link ${link ? "set" : "cleared"}`);
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
let credentials = readCredentials(cfg.local.credentialsFile);
|
|
667
|
-
for (const env of cfg.envs) {
|
|
668
|
-
const tenantId = tenantIdFor(cfg.app.id, env);
|
|
669
|
-
let dbKey = !options.rotateKeys ? credentials?.envs[env]?.dbKey : void 0;
|
|
670
|
-
if (dbKey) {
|
|
671
|
-
out.log(`${env}: reusing local db key for ${tenantId}`);
|
|
672
|
-
} else {
|
|
673
|
-
dbKey = await mintDbKey({ cfg, tenantId, env, token, auth, fetch: doFetch });
|
|
674
|
-
out.log(`${env}: minted db key for ${tenantId}`);
|
|
675
|
-
}
|
|
676
|
-
let o11yToken = cfg.services.includes("o11y") && !options.rotateKeys ? credentials?.envs[env]?.o11yToken : void 0;
|
|
677
|
-
if (cfg.services.includes("o11y")) {
|
|
678
|
-
if (o11yToken) {
|
|
679
|
-
out.log(`${env}: reusing local o11y ingest token`);
|
|
680
|
-
} else {
|
|
681
|
-
o11yToken = await mintO11yToken(cfg, env, token, doFetch);
|
|
682
|
-
out.log(`${env}: minted o11y ingest token`);
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
if (schema) {
|
|
686
|
-
await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
|
|
687
|
-
out.log(`${env}: schema pushed`);
|
|
688
|
-
}
|
|
689
|
-
if (rules) {
|
|
690
|
-
await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/admin/rules`, token, rules);
|
|
691
|
-
out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
|
|
692
|
-
}
|
|
693
|
-
if (cfg.ai?.provider && cfg.ai.keyEnv) {
|
|
694
|
-
const key = process4.env[cfg.ai.keyEnv];
|
|
695
|
-
if (key) {
|
|
696
|
-
const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
|
|
697
|
-
await putSecret({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
|
|
698
|
-
out.log(`${env}: ${cfg.ai.provider} key stored in vault (${secretName})`);
|
|
699
|
-
} else {
|
|
700
|
-
out.log(`${env}: ${cfg.ai.keyEnv} not set; skipped provider key storage`);
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
credentials = mergeCredential(credentials, {
|
|
704
|
-
appId: cfg.app.id,
|
|
705
|
-
platformUrl: cfg.platformUrl,
|
|
706
|
-
dbEndpoint: cfg.dbEndpoint,
|
|
707
|
-
env,
|
|
708
|
-
tenantId,
|
|
709
|
-
dbKey,
|
|
710
|
-
...o11yToken ? { o11yToken } : {}
|
|
711
|
-
});
|
|
712
|
-
}
|
|
713
|
-
if (cfg.local.gitignore) ensureGitignore(cfg.rootDir);
|
|
714
|
-
if (options.writeCredentials !== false && credentials) {
|
|
715
|
-
writePrivateJson(cfg.local.credentialsFile, credentials);
|
|
716
|
-
out.log(`credentials: wrote ${displayPath(cfg.local.credentialsFile, cfg.rootDir)} (0600, gitignored)`);
|
|
717
|
-
}
|
|
718
|
-
const devVarsTarget = resolveWriteDevVarsTarget(cfg, options.writeDevVars);
|
|
719
|
-
if (devVarsTarget && credentials) {
|
|
720
|
-
const env = cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
|
|
721
|
-
writeDevVars(devVarsTarget, credentials, env, o11yDevVars(cfg));
|
|
722
|
-
out.log(`dev vars: wrote ${displayPath(devVarsTarget, cfg.rootDir)} for ${env}`);
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
function o11yDevVars(cfg) {
|
|
726
|
-
if (!cfg.services.includes("o11y")) return void 0;
|
|
727
|
-
return {
|
|
728
|
-
endpoint: cfg.o11y?.endpoint ?? "https://o11y.odla.ai",
|
|
729
|
-
service: cfg.o11y?.service ?? cfg.app.id,
|
|
730
|
-
...cfg.o11y?.version ? { version: cfg.o11y.version } : {}
|
|
731
|
-
};
|
|
732
|
-
}
|
|
733
|
-
async function readRegistryApp(cfg, token, doFetch) {
|
|
734
|
-
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}`, {
|
|
735
|
-
headers: { authorization: `Bearer ${token}` }
|
|
736
|
-
});
|
|
737
|
-
if (res.status === 404) return null;
|
|
738
|
-
if (!res.ok) return null;
|
|
739
|
-
const json = await res.json();
|
|
740
|
-
return json.app ?? null;
|
|
741
|
-
}
|
|
742
|
-
async function mintDbKey(opts) {
|
|
743
|
-
let res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(opts.tenantId)}/keys`, {
|
|
744
|
-
method: "POST",
|
|
745
|
-
headers: opts.auth,
|
|
746
|
-
body: "{}"
|
|
747
|
-
});
|
|
748
|
-
if (res.status === 404) {
|
|
749
|
-
const created = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps`, {
|
|
750
|
-
method: "POST",
|
|
751
|
-
headers: opts.auth,
|
|
752
|
-
body: JSON.stringify({
|
|
753
|
-
name: opts.env === "prod" ? opts.cfg.app.name : `${opts.cfg.app.name} (${opts.env})`,
|
|
754
|
-
appId: opts.tenantId
|
|
755
|
-
})
|
|
756
|
-
});
|
|
757
|
-
if (!created.ok) throw new Error(`db app create (${opts.tenantId}) failed: ${created.status} ${await safeText(created)}`);
|
|
758
|
-
res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(opts.tenantId)}/keys`, {
|
|
759
|
-
method: "POST",
|
|
760
|
-
headers: opts.auth,
|
|
761
|
-
body: "{}"
|
|
762
|
-
});
|
|
763
|
-
}
|
|
764
|
-
if (!res.ok) throw new Error(`db key mint (${opts.tenantId}) failed: ${res.status} ${await safeText(res)}`);
|
|
765
|
-
const body = await res.json();
|
|
766
|
-
if (!body.key) throw new Error(`db key mint (${opts.tenantId}) returned no key`);
|
|
767
|
-
return body.key;
|
|
768
|
-
}
|
|
769
|
-
async function mintO11yToken(cfg, env, token, doFetch) {
|
|
770
|
-
const res = await doFetch(
|
|
771
|
-
`${cfg.platformUrl}/o11y/${encodeURIComponent(cfg.app.id)}/token?env=${encodeURIComponent(env)}`,
|
|
772
|
-
{ method: "POST", headers: { authorization: `Bearer ${token}` } }
|
|
773
|
-
);
|
|
774
|
-
if (!res.ok) throw new Error(`o11y token mint (${env}) failed: ${res.status} ${await safeText(res)}`);
|
|
775
|
-
const body = await res.json();
|
|
776
|
-
if (!body.token) throw new Error(`o11y token mint (${env}) returned no token`);
|
|
777
|
-
return body.token;
|
|
778
|
-
}
|
|
779
|
-
async function postJson(doFetch, url, bearer, body) {
|
|
780
|
-
const res = await doFetch(url, {
|
|
781
|
-
method: "POST",
|
|
782
|
-
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
783
|
-
body: JSON.stringify(body)
|
|
784
|
-
});
|
|
785
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText(res)}`);
|
|
786
|
-
}
|
|
787
|
-
function normalizeClerkConfig(value) {
|
|
788
|
-
if (!value) return null;
|
|
789
|
-
if (typeof value === "string") {
|
|
790
|
-
const publishableKey2 = envValue(value);
|
|
791
|
-
return publishableKey2 ? { publishableKey: publishableKey2 } : null;
|
|
792
|
-
}
|
|
793
|
-
if (typeof value !== "object") return null;
|
|
794
|
-
const cfg = value;
|
|
795
|
-
const publishableKey = envValue(cfg.publishableKey);
|
|
796
|
-
return publishableKey ? { publishableKey, ...cfg.audience ? { audience: cfg.audience } : {}, ...cfg.mode ? { mode: cfg.mode } : {} } : null;
|
|
797
|
-
}
|
|
798
|
-
function defaultSecretName(provider) {
|
|
799
|
-
const names = DEFAULT_SECRET_NAMES;
|
|
800
|
-
return names[provider] ?? `${provider}_api_key`;
|
|
801
|
-
}
|
|
802
|
-
function resolveWriteDevVarsTarget(cfg, requested) {
|
|
803
|
-
if (!requested) return null;
|
|
804
|
-
if (requested === true) return cfg.local.devVarsFile;
|
|
805
|
-
return resolve5(dirname4(cfg.configPath), requested);
|
|
806
|
-
}
|
|
807
|
-
async function safeText(res) {
|
|
808
|
-
try {
|
|
809
|
-
return redactSecrets((await res.text()).slice(0, 500));
|
|
810
|
-
} catch {
|
|
811
|
-
return "";
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
// src/secrets.ts
|
|
816
|
-
var PROD_ENV_NAMES = /* @__PURE__ */ new Set(["prod", "production"]);
|
|
817
|
-
async function secretsPush(options) {
|
|
818
|
-
const out = options.stdout ?? console;
|
|
819
|
-
const cfg = await loadProjectConfig(options.configPath);
|
|
820
|
-
const env = options.env;
|
|
821
|
-
if (!cfg.envs.includes(env)) {
|
|
822
|
-
throw new Error(`env "${env}" is not in config envs (${cfg.envs.join(", ")})`);
|
|
823
|
-
}
|
|
824
|
-
if (PROD_ENV_NAMES.has(env) && !options.yes) {
|
|
825
|
-
throw new Error(`refusing to push a secret to "${env}" without --yes`);
|
|
826
|
-
}
|
|
827
|
-
const credentialsPath = displayPath(cfg.local.credentialsFile, cfg.rootDir);
|
|
828
|
-
const credentials = readCredentials(cfg.local.credentialsFile);
|
|
829
|
-
if (!credentials) throw new Error(`no credentials at ${credentialsPath} \u2014 run "odla-ai provision" first`);
|
|
830
|
-
if (credentials.appId !== cfg.app.id) {
|
|
831
|
-
throw new Error(`credentials at ${credentialsPath} are for "${credentials.appId}", not "${cfg.app.id}"`);
|
|
832
|
-
}
|
|
833
|
-
const dbKey = credentials.envs[env]?.dbKey;
|
|
834
|
-
if (!dbKey) throw new Error(`no db key for env "${env}" in ${credentialsPath} \u2014 run "odla-ai provision" first`);
|
|
835
|
-
const wranglerConfig = findWranglerConfig(cfg.rootDir);
|
|
836
|
-
if (!wranglerConfig) {
|
|
837
|
-
throw new Error(`no wrangler config found in ${cfg.rootDir} (wrangler.jsonc, wrangler.json, or wrangler.toml)`);
|
|
838
|
-
}
|
|
839
|
-
const wranglerEnv = PROD_ENV_NAMES.has(env) ? void 0 : env;
|
|
840
|
-
const target = wranglerEnv ? `wrangler env "${wranglerEnv}"` : "the top-level (prod) wrangler env";
|
|
841
|
-
const secrets = [{ name: "ODLA_API_KEY", value: dbKey }];
|
|
842
|
-
const o11yToken = credentials.envs[env]?.o11yToken;
|
|
843
|
-
if (o11yToken) secrets.push({ name: "ODLA_O11Y_TOKEN", value: o11yToken });
|
|
844
|
-
if (options.dryRun) {
|
|
845
|
-
for (const s of secrets) out.log(`dry run: would push ${s.name} (${redactSecrets(s.value)}) to ${target}`);
|
|
846
|
-
return;
|
|
847
|
-
}
|
|
848
|
-
const run = options.runner ?? defaultRunner;
|
|
849
|
-
if (!await wranglerLoggedIn(run, cfg.rootDir)) {
|
|
850
|
-
throw new Error(`wrangler is not logged in \u2014 run "wrangler login" (a browser step for the human)`);
|
|
851
|
-
}
|
|
852
|
-
for (const s of secrets) {
|
|
853
|
-
const result = await wranglerPutSecret(run, { name: s.name, value: s.value, env: wranglerEnv, cwd: cfg.rootDir });
|
|
854
|
-
if (result.code !== 0) {
|
|
855
|
-
throw new Error(`wrangler secret put ${s.name} failed (exit ${result.code}): ${redactSecrets(`${result.stderr || result.stdout}`.trim())}`);
|
|
856
|
-
}
|
|
857
|
-
out.log(`${s.name} pushed to ${target} (value read from ${credentialsPath}, never echoed)`);
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
// src/skill.ts
|
|
862
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, readdirSync, writeFileSync as writeFileSync3 } from "fs";
|
|
863
|
-
import { homedir } from "os";
|
|
864
|
-
import { join as join3, relative as relative2, resolve as resolve6 } from "path";
|
|
865
|
-
import { fileURLToPath } from "url";
|
|
866
|
-
function installSkill(options = {}) {
|
|
867
|
-
const out = options.stdout ?? console;
|
|
868
|
-
const sourceDir = options.sourceDir ?? fileURLToPath(new URL("../skills", import.meta.url));
|
|
869
|
-
const targetDir = options.global ? join3(options.homeDir ?? homedir(), ".claude", "skills") : resolve6(options.dir ?? process.cwd(), ".claude", "skills");
|
|
870
|
-
const files = listFiles(sourceDir);
|
|
871
|
-
if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
|
|
872
|
-
const written = [];
|
|
873
|
-
const unchanged = [];
|
|
874
|
-
const conflicts = [];
|
|
875
|
-
for (const rel of files) {
|
|
876
|
-
const target = join3(targetDir, rel);
|
|
877
|
-
const source = readFileSync5(join3(sourceDir, rel), "utf8");
|
|
878
|
-
if (existsSync6(target)) {
|
|
879
|
-
const current = readFileSync5(target, "utf8");
|
|
880
|
-
if (current === source) {
|
|
881
|
-
unchanged.push(rel);
|
|
882
|
-
continue;
|
|
883
|
-
}
|
|
884
|
-
if (!options.force) {
|
|
885
|
-
conflicts.push(rel);
|
|
886
|
-
continue;
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
written.push(rel);
|
|
890
|
-
}
|
|
891
|
-
if (conflicts.length > 0) {
|
|
892
|
-
throw new Error(
|
|
893
|
-
`skill files modified locally (re-run with --force to overwrite):
|
|
894
|
-
${conflicts.map((f) => ` - ${join3(targetDir, f)}`).join("\n")}`
|
|
895
|
-
);
|
|
896
|
-
}
|
|
897
|
-
for (const rel of written) {
|
|
898
|
-
const target = join3(targetDir, rel);
|
|
899
|
-
mkdirSync3(join3(target, ".."), { recursive: true });
|
|
900
|
-
writeFileSync3(target, readFileSync5(join3(sourceDir, rel), "utf8"));
|
|
901
|
-
}
|
|
902
|
-
const skills = [...new Set(files.map((f) => f.split(/[\\/]/)[0]))].sort();
|
|
903
|
-
out.log(`skills: ${skills.join(", ")}`);
|
|
904
|
-
out.log(`installed ${written.length} file(s) to ${targetDir}${unchanged.length ? ` (${unchanged.length} unchanged)` : ""}`);
|
|
905
|
-
return { targetDir, written, unchanged };
|
|
906
|
-
}
|
|
907
|
-
function listFiles(dir) {
|
|
908
|
-
if (!existsSync6(dir)) return [];
|
|
909
|
-
const results = [];
|
|
910
|
-
const walk = (current) => {
|
|
911
|
-
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
912
|
-
const path = join3(current, entry.name);
|
|
913
|
-
if (entry.isDirectory()) walk(path);
|
|
914
|
-
else results.push(relative2(dir, path));
|
|
915
|
-
}
|
|
916
|
-
};
|
|
917
|
-
walk(dir);
|
|
918
|
-
return results.sort();
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
// src/smoke.ts
|
|
922
|
-
async function smoke(options) {
|
|
923
|
-
const out = options.stdout ?? console;
|
|
924
|
-
const cfg = await loadProjectConfig(options.configPath);
|
|
925
|
-
const env = options.env ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod");
|
|
926
|
-
if (!cfg.envs.includes(env)) throw new Error(`env "${env}" is not declared in ${displayPath(cfg.configPath, cfg.rootDir)}`);
|
|
927
|
-
const credentials = readCredentials(cfg.local.credentialsFile);
|
|
928
|
-
if (!credentials) {
|
|
929
|
-
throw new Error(`local credentials missing: ${displayPath(cfg.local.credentialsFile, cfg.rootDir)}. Run "odla-ai provision --write-dev-vars".`);
|
|
930
|
-
}
|
|
931
|
-
if (credentials.appId !== cfg.app.id) {
|
|
932
|
-
throw new Error(`local credentials are for app "${credentials.appId}", but config app is "${cfg.app.id}"`);
|
|
933
|
-
}
|
|
934
|
-
const entry = credentials.envs[env];
|
|
935
|
-
if (!entry?.tenantId || !entry.dbKey) {
|
|
936
|
-
throw new Error(`local credentials have no db key for env "${env}". Run "odla-ai provision --write-dev-vars".`);
|
|
937
|
-
}
|
|
938
|
-
const doFetch = options.fetch ?? fetch;
|
|
939
|
-
out.log(`smoke: ${cfg.app.id}/${env}`);
|
|
940
|
-
out.log(` tenant: ${entry.tenantId}`);
|
|
941
|
-
const publicConfig = await getJson(doFetch, publicConfigUrl(cfg.platformUrl, cfg.app.id, env), void 0);
|
|
942
|
-
out.log(` public-config: ok`);
|
|
943
|
-
if (cfg.ai?.provider) {
|
|
944
|
-
const provider = publicConfig.ai?.provider ?? null;
|
|
945
|
-
if (provider !== cfg.ai.provider) {
|
|
946
|
-
throw new Error(`ai provider mismatch: expected "${cfg.ai.provider}", public-config has "${provider ?? "none"}"`);
|
|
947
|
-
}
|
|
948
|
-
out.log(` ai: ${provider}`);
|
|
949
|
-
}
|
|
950
|
-
const expectedSchema = await resolveDataExport(cfg, cfg.db?.schema, ["schema", "SCHEMA"]);
|
|
951
|
-
const expectedEntities = serializedEntities(expectedSchema);
|
|
952
|
-
const liveSchemaPayload = await getJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/schema`, entry.dbKey);
|
|
953
|
-
const liveSchema = liveSchemaPayload.schema ?? liveSchemaPayload;
|
|
954
|
-
const liveEntities = serializedEntities(liveSchema);
|
|
955
|
-
if (expectedEntities.length) {
|
|
956
|
-
const missing = expectedEntities.filter((entity) => !liveEntities.includes(entity));
|
|
957
|
-
if (missing.length) throw new Error(`live schema is missing expected entities: ${missing.join(", ")}`);
|
|
958
|
-
}
|
|
959
|
-
out.log(` schema: ${liveEntities.length} entities`);
|
|
960
|
-
const aggregateEntity = expectedEntities[0] ?? liveEntities[0];
|
|
961
|
-
if (aggregateEntity) {
|
|
962
|
-
const aggregate = await postJson2(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/aggregate`, entry.dbKey, {
|
|
963
|
-
ns: aggregateEntity,
|
|
964
|
-
aggregate: { count: true }
|
|
965
|
-
});
|
|
966
|
-
const count = aggregate.aggregate?.count;
|
|
967
|
-
out.log(` aggregate: ${aggregateEntity}.count=${String(count ?? "ok")}`);
|
|
968
|
-
} else {
|
|
969
|
-
out.log(` aggregate: skipped (schema has no entities)`);
|
|
970
|
-
}
|
|
971
|
-
out.log("ok");
|
|
972
|
-
}
|
|
973
|
-
async function getJson(doFetch, url, bearer) {
|
|
974
|
-
const res = await doFetch(url, {
|
|
975
|
-
headers: bearer ? { authorization: `Bearer ${bearer}` } : void 0
|
|
976
|
-
});
|
|
977
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText2(res)}`);
|
|
978
|
-
return res.json();
|
|
979
|
-
}
|
|
980
|
-
async function postJson2(doFetch, url, bearer, body) {
|
|
981
|
-
const res = await doFetch(url, {
|
|
982
|
-
method: "POST",
|
|
983
|
-
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
984
|
-
body: JSON.stringify(body)
|
|
985
|
-
});
|
|
986
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText2(res)}`);
|
|
987
|
-
return res.json();
|
|
988
|
-
}
|
|
989
|
-
function publicConfigUrl(platformUrl, appId, env) {
|
|
990
|
-
const url = new URL(`/registry/apps/${encodeURIComponent(appId)}/public-config`, platformUrl);
|
|
991
|
-
url.searchParams.set("env", env);
|
|
992
|
-
return url.toString();
|
|
993
|
-
}
|
|
994
|
-
async function safeText2(res) {
|
|
995
|
-
try {
|
|
996
|
-
return (await res.text()).slice(0, 500);
|
|
997
|
-
} catch {
|
|
998
|
-
return "";
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
// src/cli.ts
|
|
1003
|
-
import { readFileSync as readFileSync6 } from "fs";
|
|
1004
|
-
async function runCli(argv = process.argv.slice(2)) {
|
|
1005
|
-
const parsed = parseArgv(argv);
|
|
1006
|
-
const command = parsed.positionals[0] ?? "help";
|
|
1007
|
-
if (command === "version" || command === "-v" || parsed.options.version === true) {
|
|
1008
|
-
console.log(cliVersion());
|
|
1009
|
-
return;
|
|
1010
|
-
}
|
|
1011
|
-
if (command === "help" || command === "--help" || command === "-h") {
|
|
1012
|
-
printHelp();
|
|
1013
|
-
return;
|
|
1014
|
-
}
|
|
1015
|
-
if (command === "init") {
|
|
1016
|
-
initProject({
|
|
1017
|
-
appId: requiredString(parsed.options["app-id"], "--app-id"),
|
|
1018
|
-
name: requiredString(parsed.options.name, "--name"),
|
|
1019
|
-
configPath: stringOpt(parsed.options.config),
|
|
1020
|
-
envs: listOpt(parsed.options.env),
|
|
1021
|
-
services: listOpt(parsed.options.services),
|
|
1022
|
-
aiProvider: stringOpt(parsed.options["ai-provider"]),
|
|
1023
|
-
force: parsed.options.force === true
|
|
1024
|
-
});
|
|
1025
|
-
return;
|
|
1026
|
-
}
|
|
1027
|
-
if (command === "doctor") {
|
|
1028
|
-
await doctor({ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs" });
|
|
1029
|
-
return;
|
|
1030
|
-
}
|
|
1031
|
-
if (command === "provision") {
|
|
1032
|
-
const writeDevVars2 = parsed.options["write-dev-vars"];
|
|
1033
|
-
const opts = {
|
|
1034
|
-
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
1035
|
-
dryRun: parsed.options["dry-run"] === true,
|
|
1036
|
-
rotateKeys: parsed.options["rotate-keys"] === true,
|
|
1037
|
-
writeCredentials: parsed.options["write-credentials"] !== false,
|
|
1038
|
-
writeDevVars: typeof writeDevVars2 === "string" ? writeDevVars2 : writeDevVars2 === true,
|
|
1039
|
-
token: stringOpt(parsed.options.token),
|
|
1040
|
-
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
1041
|
-
yes: parsed.options.yes === true
|
|
1042
|
-
};
|
|
1043
|
-
await provision(opts);
|
|
1044
|
-
return;
|
|
1045
|
-
}
|
|
1046
|
-
if (command === "smoke") {
|
|
1047
|
-
await smoke({
|
|
1048
|
-
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
1049
|
-
env: stringOpt(parsed.options.env)
|
|
1050
|
-
});
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
if (command === "setup") {
|
|
1054
|
-
installSkill({
|
|
1055
|
-
dir: stringOpt(parsed.options.dir),
|
|
1056
|
-
global: parsed.options.global === true,
|
|
1057
|
-
force: parsed.options.force === true
|
|
1058
|
-
});
|
|
1059
|
-
console.log(
|
|
1060
|
-
"\nSkills installed. Point your coding agent at this repo \u2014 the `odla` skill orients it\n(build a new app, or migrate an existing site) and drives `odla-ai init` / `provision`."
|
|
1061
|
-
);
|
|
1062
|
-
return;
|
|
1063
|
-
}
|
|
1064
|
-
if (command === "skill") {
|
|
1065
|
-
const sub = parsed.positionals[1];
|
|
1066
|
-
if (sub !== "install") throw new Error(`unknown skill subcommand "${sub ?? ""}". Try "odla-ai skill install".`);
|
|
1067
|
-
installSkill({
|
|
1068
|
-
dir: stringOpt(parsed.options.dir),
|
|
1069
|
-
global: parsed.options.global === true,
|
|
1070
|
-
force: parsed.options.force === true
|
|
1071
|
-
});
|
|
1072
|
-
return;
|
|
1073
|
-
}
|
|
1074
|
-
if (command === "secrets") {
|
|
1075
|
-
const sub = parsed.positionals[1];
|
|
1076
|
-
if (sub !== "push") throw new Error(`unknown secrets subcommand "${sub ?? ""}". Try "odla-ai secrets push --env dev".`);
|
|
1077
|
-
await secretsPush({
|
|
1078
|
-
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
1079
|
-
env: requiredString(parsed.options.env, "--env"),
|
|
1080
|
-
dryRun: parsed.options["dry-run"] === true,
|
|
1081
|
-
yes: parsed.options.yes === true
|
|
1082
|
-
});
|
|
1083
|
-
return;
|
|
1084
|
-
}
|
|
1085
|
-
throw new Error(`unknown command "${command}". Run "odla-ai help".`);
|
|
1086
|
-
}
|
|
1087
|
-
function parseArgv(argv) {
|
|
1088
|
-
const positionals = [];
|
|
1089
|
-
const options = {};
|
|
1090
|
-
for (let i = 0; i < argv.length; i++) {
|
|
1091
|
-
const arg = argv[i];
|
|
1092
|
-
if (!arg.startsWith("--")) {
|
|
1093
|
-
positionals.push(arg);
|
|
1094
|
-
continue;
|
|
1095
|
-
}
|
|
1096
|
-
const eq = arg.indexOf("=");
|
|
1097
|
-
const rawName = arg.slice(2, eq === -1 ? void 0 : eq);
|
|
1098
|
-
if (!rawName) continue;
|
|
1099
|
-
if (rawName.startsWith("no-")) {
|
|
1100
|
-
options[rawName.slice(3)] = false;
|
|
1101
|
-
continue;
|
|
1102
|
-
}
|
|
1103
|
-
if (eq !== -1) {
|
|
1104
|
-
addOption(options, rawName, arg.slice(eq + 1));
|
|
1105
|
-
continue;
|
|
1106
|
-
}
|
|
1107
|
-
const value = argv[i + 1];
|
|
1108
|
-
if (value !== void 0 && !value.startsWith("--")) {
|
|
1109
|
-
i++;
|
|
1110
|
-
addOption(options, rawName, value);
|
|
1111
|
-
} else {
|
|
1112
|
-
addOption(options, rawName, true);
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
return { positionals, options };
|
|
1116
|
-
}
|
|
1117
|
-
function addOption(options, name, value) {
|
|
1118
|
-
const cur = options[name];
|
|
1119
|
-
if (cur === void 0) {
|
|
1120
|
-
options[name] = value;
|
|
1121
|
-
} else if (Array.isArray(cur)) {
|
|
1122
|
-
cur.push(String(value));
|
|
1123
|
-
} else {
|
|
1124
|
-
options[name] = [String(cur), String(value)];
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
function requiredString(value, name) {
|
|
1128
|
-
const s = stringOpt(value);
|
|
1129
|
-
if (!s) throw new Error(`${name} is required`);
|
|
1130
|
-
return s;
|
|
1131
|
-
}
|
|
1132
|
-
function stringOpt(value) {
|
|
1133
|
-
if (typeof value === "string") return value;
|
|
1134
|
-
if (Array.isArray(value)) return value[value.length - 1];
|
|
1135
|
-
return void 0;
|
|
1136
|
-
}
|
|
1137
|
-
function listOpt(value) {
|
|
1138
|
-
if (value === void 0 || value === false || value === true) return void 0;
|
|
1139
|
-
const values = Array.isArray(value) ? value : [value];
|
|
1140
|
-
return values.flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
|
|
1141
|
-
}
|
|
1142
|
-
function cliVersion() {
|
|
1143
|
-
const pkg = JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf8"));
|
|
1144
|
-
return pkg.version ?? "unknown";
|
|
1145
|
-
}
|
|
1146
|
-
function printHelp() {
|
|
1147
|
-
console.log(`odla-ai
|
|
1148
|
-
|
|
1149
|
-
Usage:
|
|
1150
|
-
odla-ai setup [--dir <project>] [--global] [--force]
|
|
1151
|
-
odla-ai init --app-id <id> --name <name> [--services db,ai] [--env dev --env prod]
|
|
1152
|
-
odla-ai doctor [--config odla.config.mjs]
|
|
1153
|
-
odla-ai provision [--config odla.config.mjs] [--dry-run] [--open|--no-open] [--rotate-keys] [--write-dev-vars[=path]]
|
|
1154
|
-
odla-ai smoke [--config odla.config.mjs] [--env dev]
|
|
1155
|
-
odla-ai skill install [--dir <project>] [--global] [--force]
|
|
1156
|
-
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
1157
|
-
odla-ai version
|
|
1158
|
-
|
|
1159
|
-
Commands:
|
|
1160
|
-
setup Install the odla skills so a coding agent can drive the rest.
|
|
1161
|
-
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
1162
|
-
doctor Validate and summarize the project config without network calls.
|
|
1163
|
-
provision Register the app, enable services, push schema/rules, configure AI/auth.
|
|
1164
|
-
smoke Verify local credentials, public-config, live schema, and db aggregate.
|
|
1165
|
-
skill Install the bundled Claude Code skills into .claude/skills/.
|
|
1166
|
-
secrets Push the env's db key into the Worker via wrangler, stdin-piped.
|
|
1167
|
-
version Print the CLI version.
|
|
1168
|
-
|
|
1169
|
-
Safety:
|
|
1170
|
-
Provision caches the approved developer token and local db keys under .odla/
|
|
1171
|
-
with mode 0600, and init adds those paths to .gitignore.
|
|
1172
|
-
Provision opens the approval page automatically in interactive terminals;
|
|
1173
|
-
use --open to force browser launch or --no-open to suppress it.
|
|
1174
|
-
`);
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
export {
|
|
1178
|
-
redactSecrets,
|
|
1179
|
-
doctor,
|
|
1180
|
-
initProject,
|
|
1181
|
-
provision,
|
|
1182
|
-
secretsPush,
|
|
1183
|
-
installSkill,
|
|
1184
|
-
smoke,
|
|
1185
|
-
runCli
|
|
1186
|
-
};
|
|
1187
|
-
//# sourceMappingURL=chunk-UWT7C6VT.js.map
|