@odla-ai/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,863 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/index.ts
32
+ var index_exports = {};
33
+ __export(index_exports, {
34
+ doctor: () => doctor,
35
+ initProject: () => initProject,
36
+ provision: () => provision,
37
+ runCli: () => runCli,
38
+ smoke: () => smoke
39
+ });
40
+ module.exports = __toCommonJS(index_exports);
41
+
42
+ // ../../node_modules/tsup/assets/cjs_shims.js
43
+ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
44
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
45
+
46
+ // src/cli.ts
47
+ var import_node_fs4 = require("fs");
48
+
49
+ // src/config.ts
50
+ var import_node_fs = require("fs");
51
+ var import_node_path = require("path");
52
+ var import_node_url = require("url");
53
+ var DEFAULT_PLATFORM = "https://odla.ai";
54
+ var DEFAULT_ENVS = ["prod", "dev"];
55
+ var DEFAULT_SERVICES = ["db", "ai"];
56
+ async function loadProjectConfig(configPath = "odla.config.mjs") {
57
+ const resolved = (0, import_node_path.resolve)(configPath);
58
+ if (!(0, import_node_fs.existsSync)(resolved)) {
59
+ throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
60
+ }
61
+ const raw = await loadConfigModule(resolved);
62
+ const rootDir = (0, import_node_path.dirname)(resolved);
63
+ validateRawConfig(raw, resolved);
64
+ const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
65
+ const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
66
+ const envs = unique(raw.envs?.length ? raw.envs : DEFAULT_ENVS);
67
+ const services = unique(raw.services?.length ? raw.services : DEFAULT_SERVICES);
68
+ const local = {
69
+ tokenFile: (0, import_node_path.resolve)(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
70
+ credentialsFile: (0, import_node_path.resolve)(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
71
+ devVarsFile: (0, import_node_path.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
72
+ gitignore: raw.local?.gitignore ?? true
73
+ };
74
+ return {
75
+ ...raw,
76
+ configPath: resolved,
77
+ rootDir,
78
+ platformUrl,
79
+ dbEndpoint,
80
+ envs,
81
+ services,
82
+ local
83
+ };
84
+ }
85
+ async function resolveDataExport(cfg, value, names) {
86
+ if (value === void 0 || value === null || value === false) return void 0;
87
+ if (typeof value !== "string") return value;
88
+ const target = (0, import_node_path.isAbsolute)(value) ? value : (0, import_node_path.resolve)(cfg.rootDir, value);
89
+ if (target.endsWith(".json")) {
90
+ return JSON.parse((0, import_node_fs.readFileSync)(target, "utf8"));
91
+ }
92
+ const mod = await import((0, import_node_url.pathToFileURL)(target).href);
93
+ for (const name of names) {
94
+ if (mod[name] !== void 0) return mod[name];
95
+ }
96
+ if (mod.default !== void 0) return mod.default;
97
+ throw new Error(`${value} did not export ${names.join(", ")} or default`);
98
+ }
99
+ function buildPlan(cfg) {
100
+ return {
101
+ appId: cfg.app.id,
102
+ appName: cfg.app.name,
103
+ platformUrl: cfg.platformUrl,
104
+ dbEndpoint: cfg.dbEndpoint,
105
+ envs: cfg.envs,
106
+ services: cfg.services,
107
+ hasSchema: !!cfg.db?.schema,
108
+ hasRules: !!cfg.db?.rules || !!cfg.db?.schema && cfg.db.defaultRules !== false,
109
+ aiProvider: cfg.ai?.provider
110
+ };
111
+ }
112
+ function rulesFromSchema(schema) {
113
+ const entities = serializedEntities(schema);
114
+ return Object.fromEntries(
115
+ entities.map((name) => [name, { view: "false", create: "false", update: "false", delete: "false" }])
116
+ );
117
+ }
118
+ function serializedEntities(schema) {
119
+ if (!schema || typeof schema !== "object") return [];
120
+ const entities = schema.entities;
121
+ if (!entities || typeof entities !== "object" || Array.isArray(entities)) return [];
122
+ return Object.keys(entities);
123
+ }
124
+ function envValue(value) {
125
+ if (!value) return void 0;
126
+ if (value.startsWith("$")) return process.env[value.slice(1)];
127
+ return value;
128
+ }
129
+ function validateRawConfig(raw, path) {
130
+ if (!raw || typeof raw !== "object") throw new Error(`${path} must export an object`);
131
+ const cfg = raw;
132
+ if (!cfg.app || typeof cfg.app !== "object") throw new Error(`${path}: missing app object`);
133
+ if (!validId(cfg.app.id)) throw new Error(`${path}: app.id must be lowercase letters, numbers, and hyphens`);
134
+ if (!cfg.app.name || typeof cfg.app.name !== "string") throw new Error(`${path}: app.name is required`);
135
+ if (cfg.envs?.some((env) => !validId(env))) throw new Error(`${path}: env names must be lowercase letters, numbers, and hyphens`);
136
+ }
137
+ function validId(value) {
138
+ return typeof value === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value);
139
+ }
140
+ async function loadConfigModule(path) {
141
+ if (path.endsWith(".json")) return JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
142
+ const mod = await import(`${(0, import_node_url.pathToFileURL)(path).href}?t=${Date.now()}`);
143
+ const value = mod.default ?? mod.config;
144
+ if (typeof value === "function") return await value();
145
+ return value;
146
+ }
147
+ function trimSlash(value) {
148
+ return value.replace(/\/+$/, "");
149
+ }
150
+ function unique(values) {
151
+ return [...new Set(values.filter(Boolean))];
152
+ }
153
+
154
+ // src/doctor.ts
155
+ async function doctor(options) {
156
+ const out = options.stdout ?? console;
157
+ const cfg = await loadProjectConfig(options.configPath);
158
+ const plan = buildPlan(cfg);
159
+ const schema = await resolveDataExport(cfg, cfg.db?.schema, ["schema", "SCHEMA"]);
160
+ const configuredRules = await resolveDataExport(cfg, cfg.db?.rules, ["rules", "RULES"]);
161
+ const rules = configuredRules ?? (schema && cfg.db?.defaultRules !== false ? rulesFromSchema(schema) : void 0);
162
+ const entities = serializedEntities(schema);
163
+ out.log(`config: ${cfg.configPath}`);
164
+ out.log(`app: ${plan.appName} (${plan.appId})`);
165
+ out.log(`envs: ${plan.envs.join(", ")}`);
166
+ out.log(`svc: ${plan.services.join(", ")}`);
167
+ out.log(`schema: ${schema ? `${entities.length} entities` : "none"}`);
168
+ out.log(`rules: ${rules ? `${Object.keys(rules).length} namespaces` : "none"}`);
169
+ out.log(`ai: ${cfg.ai?.provider ?? "not configured"}`);
170
+ const warnings = [];
171
+ if (schema && entities.length === 0) warnings.push("schema has no entities");
172
+ if (rules) {
173
+ for (const ns of Object.keys(rules)) {
174
+ if (entities.length > 0 && !entities.includes(ns)) warnings.push(`rules include "${ns}" but schema has no matching entity`);
175
+ }
176
+ }
177
+ if (cfg.services.includes("ai") && !cfg.ai?.provider) warnings.push("ai service is enabled but ai.provider is not set");
178
+ if (cfg.auth?.clerk) {
179
+ for (const [env, value] of Object.entries(cfg.auth.clerk)) {
180
+ if (typeof value === "string" && value.startsWith("$") && !process.env[value.slice(1)]) {
181
+ warnings.push(`auth.clerk.${env} references unset env var ${value}`);
182
+ }
183
+ }
184
+ }
185
+ if (cfg.ai?.keyEnv && !process.env[cfg.ai.keyEnv]) warnings.push(`${cfg.ai.keyEnv} is not set; provision will skip provider key storage`);
186
+ if (warnings.length) {
187
+ out.log("");
188
+ out.log("warnings:");
189
+ for (const warning of warnings) out.log(` - ${warning}`);
190
+ } else {
191
+ out.log("ok");
192
+ }
193
+ }
194
+
195
+ // src/init.ts
196
+ var import_node_fs3 = require("fs");
197
+ var import_node_path3 = require("path");
198
+
199
+ // src/local.ts
200
+ var import_node_fs2 = require("fs");
201
+ var import_node_path2 = require("path");
202
+ var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
203
+ function readJsonFile(path) {
204
+ try {
205
+ return JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
206
+ } catch {
207
+ return null;
208
+ }
209
+ }
210
+ function writePrivateJson(path, value) {
211
+ (0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(path), { recursive: true });
212
+ (0, import_node_fs2.writeFileSync)(path, `${JSON.stringify(value, null, 2)}
213
+ `);
214
+ (0, import_node_fs2.chmodSync)(path, 384);
215
+ }
216
+ function readCredentials(path) {
217
+ return readJsonFile(path);
218
+ }
219
+ function mergeCredential(current, update) {
220
+ const next = current ?? {
221
+ appId: update.appId,
222
+ platformUrl: update.platformUrl,
223
+ dbEndpoint: update.dbEndpoint,
224
+ updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
225
+ envs: {}
226
+ };
227
+ next.appId = update.appId;
228
+ next.platformUrl = update.platformUrl;
229
+ next.dbEndpoint = update.dbEndpoint;
230
+ next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
231
+ next.envs[update.env] = {
232
+ ...next.envs[update.env] ?? {},
233
+ tenantId: update.tenantId,
234
+ ...update.dbKey ? { dbKey: update.dbKey } : {}
235
+ };
236
+ return next;
237
+ }
238
+ function ensureGitignore(rootDir) {
239
+ const path = (0, import_node_path2.resolve)(rootDir, ".gitignore");
240
+ const existing = (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : "";
241
+ const missing = GITIGNORE_LINES.filter((line) => !existing.split(/\r?\n/).includes(line));
242
+ if (missing.length === 0) return;
243
+ const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
244
+ (0, import_node_fs2.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
245
+ `);
246
+ }
247
+ function writeDevVars(path, credentials, env) {
248
+ const entry = credentials.envs[env];
249
+ if (!entry?.dbKey) throw new Error(`no db key for env "${env}" in ${path}`);
250
+ const lines = [
251
+ `ODLA_PLATFORM="${credentials.platformUrl}"`,
252
+ `ODLA_ENDPOINT="${credentials.dbEndpoint}"`,
253
+ `ODLA_APP_ID="${credentials.appId}"`,
254
+ `ODLA_ENV="${env}"`,
255
+ `ODLA_TENANT="${entry.tenantId}"`,
256
+ `ODLA_API_KEY="${entry.dbKey}"`
257
+ ];
258
+ (0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(path), { recursive: true });
259
+ (0, import_node_fs2.writeFileSync)(path, `${lines.join("\n")}
260
+ `);
261
+ (0, import_node_fs2.chmodSync)(path, 384);
262
+ }
263
+ function displayPath(path, rootDir = process.cwd()) {
264
+ const rel = (0, import_node_path2.relative)(rootDir, path);
265
+ return rel && !rel.startsWith("..") ? rel : path;
266
+ }
267
+
268
+ // src/init.ts
269
+ function initProject(options) {
270
+ const out = options.stdout ?? console;
271
+ const rootDir = (0, import_node_path3.resolve)(options.rootDir ?? process.cwd());
272
+ const configPath = (0, import_node_path3.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
273
+ if ((0, import_node_fs3.existsSync)(configPath) && !options.force) {
274
+ throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
275
+ }
276
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
277
+ throw new Error("--app-id must be lowercase letters, numbers, and hyphens");
278
+ }
279
+ const envs = options.envs?.length ? options.envs : ["prod", "dev"];
280
+ const services = options.services?.length ? options.services : ["db", "ai"];
281
+ const aiProvider = options.aiProvider ?? "anthropic";
282
+ (0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(configPath), { recursive: true });
283
+ (0, import_node_fs3.mkdirSync)((0, import_node_path3.resolve)(rootDir, "src/odla"), { recursive: true });
284
+ (0, import_node_fs3.mkdirSync)((0, import_node_path3.resolve)(rootDir, ".odla"), { recursive: true });
285
+ (0, import_node_fs3.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
286
+ writeIfMissing((0, import_node_path3.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
287
+ writeIfMissing((0, import_node_path3.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
288
+ ensureGitignore(rootDir);
289
+ out.log(`created ${relativeDisplay(configPath, rootDir)}`);
290
+ out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
291
+ out.log("updated .gitignore for local odla credentials");
292
+ }
293
+ function writeIfMissing(path, text) {
294
+ if ((0, import_node_fs3.existsSync)(path)) return;
295
+ (0, import_node_fs3.writeFileSync)(path, text);
296
+ }
297
+ function configTemplate(input) {
298
+ return `export default {
299
+ platformUrl: process.env.ODLA_PLATFORM_URL ?? "https://odla.ai",
300
+ dbEndpoint: process.env.ODLA_ENDPOINT ?? process.env.ODLA_DB_ENDPOINT ?? "https://db.odla.ai",
301
+ app: {
302
+ id: "${input.appId}",
303
+ name: "${input.name.replace(/"/g, '\\"')}",
304
+ },
305
+ envs: ${JSON.stringify(input.envs)},
306
+ services: ${JSON.stringify(input.services)},
307
+ db: {
308
+ schema: "./src/odla/schema.mjs",
309
+ rules: "./src/odla/rules.mjs",
310
+ // When rules is omitted, the CLI generates deny-all rules from schema.
311
+ defaultRules: "deny",
312
+ },
313
+ ai: {
314
+ provider: process.env.ODLA_AI_PROVIDER ?? "${input.aiProvider}",
315
+ // Optional: set this env var while running provision to store the provider
316
+ // key in the platform vault for each tenant.
317
+ keyEnv: "${defaultKeyEnv(input.aiProvider)}",
318
+ },
319
+ auth: {
320
+ clerk: {
321
+ // dev: "$CLERK_PUBLISHABLE_KEY",
322
+ // prod: "$CLERK_PUBLISHABLE_KEY",
323
+ },
324
+ },
325
+ links: {
326
+ // dev: "https://dev.example.com",
327
+ // prod: "https://example.com",
328
+ },
329
+ local: {
330
+ tokenFile: ".odla/dev-token.json",
331
+ credentialsFile: ".odla/credentials.local.json",
332
+ devVarsFile: ".dev.vars",
333
+ },
334
+ };
335
+ `;
336
+ }
337
+ function schemaTemplate() {
338
+ return `// Replace this starter schema with your app's odla-db schema.
339
+ // Knowledge-graph projects can export @odla-ai/kg's toSerializedSchema(ontology).
340
+ export const schema = {
341
+ entities: {
342
+ notes: {
343
+ attrs: {
344
+ id: { type: "string", unique: true, indexed: true, optional: false },
345
+ text: { type: "string", unique: false, indexed: false, optional: false },
346
+ createdAt: { type: "number", unique: false, indexed: true, optional: false },
347
+ },
348
+ },
349
+ },
350
+ links: {},
351
+ };
352
+ `;
353
+ }
354
+ function rulesTemplate() {
355
+ return `// odla-db is default-deny. The starter keeps runtime data backend-only.
356
+ export const rules = {
357
+ notes: {
358
+ view: "false",
359
+ create: "false",
360
+ update: "false",
361
+ delete: "false",
362
+ },
363
+ };
364
+ `;
365
+ }
366
+ function defaultKeyEnv(provider) {
367
+ if (provider === "openai") return "OPENAI_API_KEY";
368
+ if (provider === "google") return "GOOGLE_API_KEY";
369
+ return "ANTHROPIC_API_KEY";
370
+ }
371
+ function relativeDisplay(path, rootDir) {
372
+ return path.startsWith(rootDir) ? path.slice(rootDir.length + 1) : path;
373
+ }
374
+
375
+ // src/provision.ts
376
+ var import_apps = require("@odla-ai/apps");
377
+ var import_ai = require("@odla-ai/ai");
378
+ var import_node_path4 = require("path");
379
+ var import_node_process3 = __toESM(require("process"), 1);
380
+
381
+ // src/token.ts
382
+ var import_db = require("@odla-ai/db");
383
+ var import_node_process2 = __toESM(require("process"), 1);
384
+
385
+ // src/open.ts
386
+ var import_node_child_process = require("child_process");
387
+ var import_node_process = __toESM(require("process"), 1);
388
+ async function openUrl(url, options = {}) {
389
+ const command = openerFor(options.platform ?? import_node_process.default.platform);
390
+ const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
391
+ await new Promise((resolve5, reject) => {
392
+ const child = doSpawn(command.cmd, [...command.args, url], {
393
+ stdio: "ignore",
394
+ detached: true
395
+ });
396
+ child.once("error", reject);
397
+ child.once("spawn", () => {
398
+ child.unref();
399
+ resolve5();
400
+ });
401
+ });
402
+ }
403
+ function openerFor(platform) {
404
+ if (platform === "darwin") return { cmd: "open", args: [] };
405
+ if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
406
+ return { cmd: "xdg-open", args: [] };
407
+ }
408
+
409
+ // src/token.ts
410
+ async function getDeveloperToken(cfg, options, doFetch, out) {
411
+ if (options.token) return options.token;
412
+ if (import_node_process2.default.env.ODLA_DEV_TOKEN) return import_node_process2.default.env.ODLA_DEV_TOKEN;
413
+ const cached = readJsonFile(cfg.local.tokenFile);
414
+ if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
415
+ out.log(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
416
+ return cached.token;
417
+ }
418
+ const browser = approvalBrowser(options);
419
+ const { token, expiresAt } = await (0, import_db.requestToken)({
420
+ endpoint: cfg.platformUrl,
421
+ label: `${cfg.app.id} provisioner`,
422
+ fetch: doFetch,
423
+ onCode: async ({ userCode, expiresIn }) => {
424
+ const approvalUrl = handshakeUrl(cfg.platformUrl, userCode);
425
+ out.log("");
426
+ out.log(`Approve code ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
427
+ if (browser.open) {
428
+ try {
429
+ await (options.openApprovalUrl ?? openUrl)(approvalUrl);
430
+ out.log(`auth: opened browser for approval${browser.mode === "auto" ? " (auto)" : ""}`);
431
+ } catch (err) {
432
+ out.log(`auth: could not open browser (${err instanceof Error ? err.message : String(err)})`);
433
+ }
434
+ } else if (browser.reason) {
435
+ out.log(`auth: browser launch skipped (${browser.reason})`);
436
+ }
437
+ out.log("");
438
+ }
439
+ });
440
+ writePrivateJson(cfg.local.tokenFile, { token, expiresAt });
441
+ out.log(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
442
+ return token;
443
+ }
444
+ function approvalBrowser(options) {
445
+ if (options.open === true) return { open: true, mode: "forced" };
446
+ if (options.open === false) return { open: false, reason: "disabled by --no-open" };
447
+ if (import_node_process2.default.env.CI) return { open: false, reason: "CI environment" };
448
+ const interactive = options.interactive ?? Boolean(import_node_process2.default.stdin.isTTY && import_node_process2.default.stdout.isTTY);
449
+ if (!interactive) return { open: false, reason: "non-interactive shell; pass --open to force" };
450
+ return { open: true, mode: "auto" };
451
+ }
452
+ function handshakeUrl(platformUrl, userCode) {
453
+ const url = new URL("/handshakes", platformUrl);
454
+ url.searchParams.set("code", userCode);
455
+ return url.toString();
456
+ }
457
+
458
+ // src/provision.ts
459
+ async function provision(options) {
460
+ const out = options.stdout ?? console;
461
+ const cfg = await loadProjectConfig(options.configPath);
462
+ const plan = buildPlan(cfg);
463
+ out.log(`odla-ai: ${plan.appName} (${plan.appId})`);
464
+ out.log(` platform: ${plan.platformUrl}`);
465
+ out.log(` db: ${plan.dbEndpoint}`);
466
+ out.log(` envs: ${plan.envs.join(", ")}`);
467
+ out.log(` services: ${plan.services.join(", ")}`);
468
+ const schema = await resolveDataExport(cfg, cfg.db?.schema, ["schema", "SCHEMA"]);
469
+ const configuredRules = await resolveDataExport(cfg, cfg.db?.rules, ["rules", "RULES"]);
470
+ const rules = configuredRules ?? (schema && cfg.db?.defaultRules !== false ? rulesFromSchema(schema) : void 0);
471
+ if (options.dryRun) {
472
+ out.log("dry run: no network calls or file writes");
473
+ out.log(` schema: ${schema ? "yes" : "no"}`);
474
+ out.log(` rules: ${rules ? Object.keys(rules).length : 0} namespaces`);
475
+ out.log(` ai: ${cfg.ai?.provider ?? "not configured"}`);
476
+ return;
477
+ }
478
+ const doFetch = options.fetch ?? fetch;
479
+ const token = await getDeveloperToken(cfg, options, doFetch, out);
480
+ const auth = { authorization: `Bearer ${token}`, "content-type": "application/json" };
481
+ const apps = (0, import_apps.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
482
+ const existing = await readRegistryApp(cfg, token, doFetch);
483
+ if (existing) {
484
+ out.log(`app: ${cfg.app.id} already exists`);
485
+ } else {
486
+ await apps.createApp({ name: cfg.app.name, appId: cfg.app.id });
487
+ out.log(`app: created ${cfg.app.id}`);
488
+ }
489
+ for (const env of cfg.envs) {
490
+ for (const service of cfg.services) {
491
+ if (service === "ai") {
492
+ if (cfg.ai?.provider) {
493
+ await apps.setAi(cfg.app.id, env, { provider: cfg.ai.provider, ...cfg.ai.model ? { model: cfg.ai.model } : {} });
494
+ out.log(`${env}: ai configured (${cfg.ai.provider}${cfg.ai.model ? `/${cfg.ai.model}` : ""})`);
495
+ } else {
496
+ await apps.setService(cfg.app.id, "ai", true, { env });
497
+ out.log(`${env}: ai enabled`);
498
+ }
499
+ } else {
500
+ await apps.setService(cfg.app.id, service, true, { env });
501
+ out.log(`${env}: ${service} enabled`);
502
+ }
503
+ }
504
+ const authConfig = normalizeClerkConfig(cfg.auth?.clerk?.[env]);
505
+ if (authConfig?.publishableKey) {
506
+ await apps.setAuth(cfg.app.id, env, authConfig);
507
+ out.log(`${env}: auth configured (clerk ${authConfig.mode ?? "client"})`);
508
+ }
509
+ const link = cfg.links?.[env];
510
+ if (link !== void 0) {
511
+ await apps.setLink(cfg.app.id, env, link ?? null);
512
+ out.log(`${env}: link ${link ? "set" : "cleared"}`);
513
+ }
514
+ }
515
+ let credentials = readCredentials(cfg.local.credentialsFile);
516
+ for (const env of cfg.envs) {
517
+ const tenantId = (0, import_apps.tenantIdFor)(cfg.app.id, env);
518
+ let dbKey = !options.rotateKeys ? credentials?.envs[env]?.dbKey : void 0;
519
+ if (dbKey) {
520
+ out.log(`${env}: reusing local db key for ${tenantId}`);
521
+ } else {
522
+ dbKey = await mintDbKey({ cfg, tenantId, env, token, auth, fetch: doFetch });
523
+ out.log(`${env}: minted db key for ${tenantId}`);
524
+ }
525
+ if (schema) {
526
+ await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
527
+ out.log(`${env}: schema pushed`);
528
+ }
529
+ if (rules) {
530
+ await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/admin/rules`, token, rules);
531
+ out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
532
+ }
533
+ if (cfg.ai?.provider && cfg.ai.keyEnv) {
534
+ const key = import_node_process3.default.env[cfg.ai.keyEnv];
535
+ if (key) {
536
+ const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
537
+ await (0, import_ai.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
538
+ out.log(`${env}: ${cfg.ai.provider} key stored in vault (${secretName})`);
539
+ } else {
540
+ out.log(`${env}: ${cfg.ai.keyEnv} not set; skipped provider key storage`);
541
+ }
542
+ }
543
+ credentials = mergeCredential(credentials, {
544
+ appId: cfg.app.id,
545
+ platformUrl: cfg.platformUrl,
546
+ dbEndpoint: cfg.dbEndpoint,
547
+ env,
548
+ tenantId,
549
+ dbKey
550
+ });
551
+ }
552
+ if (cfg.local.gitignore) ensureGitignore(cfg.rootDir);
553
+ if (options.writeCredentials !== false && credentials) {
554
+ writePrivateJson(cfg.local.credentialsFile, credentials);
555
+ out.log(`credentials: wrote ${displayPath(cfg.local.credentialsFile, cfg.rootDir)} (0600, gitignored)`);
556
+ }
557
+ const devVarsTarget = resolveWriteDevVarsTarget(cfg, options.writeDevVars);
558
+ if (devVarsTarget && credentials) {
559
+ const env = cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
560
+ writeDevVars(devVarsTarget, credentials, env);
561
+ out.log(`dev vars: wrote ${displayPath(devVarsTarget, cfg.rootDir)} for ${env}`);
562
+ }
563
+ }
564
+ async function readRegistryApp(cfg, token, doFetch) {
565
+ const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}`, {
566
+ headers: { authorization: `Bearer ${token}` }
567
+ });
568
+ if (res.status === 404) return null;
569
+ if (!res.ok) return null;
570
+ const json = await res.json();
571
+ return json.app ?? null;
572
+ }
573
+ async function mintDbKey(opts) {
574
+ let res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(opts.tenantId)}/keys`, {
575
+ method: "POST",
576
+ headers: opts.auth,
577
+ body: "{}"
578
+ });
579
+ if (res.status === 404) {
580
+ const created = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps`, {
581
+ method: "POST",
582
+ headers: opts.auth,
583
+ body: JSON.stringify({
584
+ name: opts.env === "prod" ? opts.cfg.app.name : `${opts.cfg.app.name} (${opts.env})`,
585
+ appId: opts.tenantId
586
+ })
587
+ });
588
+ if (!created.ok) throw new Error(`db app create (${opts.tenantId}) failed: ${created.status} ${await safeText(created)}`);
589
+ res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(opts.tenantId)}/keys`, {
590
+ method: "POST",
591
+ headers: opts.auth,
592
+ body: "{}"
593
+ });
594
+ }
595
+ if (!res.ok) throw new Error(`db key mint (${opts.tenantId}) failed: ${res.status} ${await safeText(res)}`);
596
+ const body = await res.json();
597
+ if (!body.key) throw new Error(`db key mint (${opts.tenantId}) returned no key`);
598
+ return body.key;
599
+ }
600
+ async function postJson(doFetch, url, bearer, body) {
601
+ const res = await doFetch(url, {
602
+ method: "POST",
603
+ headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
604
+ body: JSON.stringify(body)
605
+ });
606
+ if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText(res)}`);
607
+ }
608
+ function normalizeClerkConfig(value) {
609
+ if (!value) return null;
610
+ if (typeof value === "string") {
611
+ const publishableKey2 = envValue(value);
612
+ return publishableKey2 ? { publishableKey: publishableKey2 } : null;
613
+ }
614
+ if (typeof value !== "object") return null;
615
+ const cfg = value;
616
+ const publishableKey = envValue(cfg.publishableKey);
617
+ return publishableKey ? { publishableKey, ...cfg.audience ? { audience: cfg.audience } : {}, ...cfg.mode ? { mode: cfg.mode } : {} } : null;
618
+ }
619
+ function defaultSecretName(provider) {
620
+ const names = import_ai.DEFAULT_SECRET_NAMES;
621
+ return names[provider] ?? `${provider}_api_key`;
622
+ }
623
+ function resolveWriteDevVarsTarget(cfg, requested) {
624
+ if (!requested) return null;
625
+ if (requested === true) return cfg.local.devVarsFile;
626
+ return (0, import_node_path4.resolve)((0, import_node_path4.dirname)(cfg.configPath), requested);
627
+ }
628
+ async function safeText(res) {
629
+ try {
630
+ return redact((await res.text()).slice(0, 500));
631
+ } catch {
632
+ return "";
633
+ }
634
+ }
635
+ function redact(value) {
636
+ return value.replace(/odla_(sk|dev)_[A-Za-z0-9._-]+/g, "odla_$1_[redacted]").replace(/sk-[A-Za-z0-9._-]+/g, "sk-[redacted]");
637
+ }
638
+
639
+ // src/smoke.ts
640
+ async function smoke(options) {
641
+ const out = options.stdout ?? console;
642
+ const cfg = await loadProjectConfig(options.configPath);
643
+ const env = options.env ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod");
644
+ if (!cfg.envs.includes(env)) throw new Error(`env "${env}" is not declared in ${displayPath(cfg.configPath, cfg.rootDir)}`);
645
+ const credentials = readCredentials(cfg.local.credentialsFile);
646
+ if (!credentials) {
647
+ throw new Error(`local credentials missing: ${displayPath(cfg.local.credentialsFile, cfg.rootDir)}. Run "odla-ai provision --write-dev-vars".`);
648
+ }
649
+ if (credentials.appId !== cfg.app.id) {
650
+ throw new Error(`local credentials are for app "${credentials.appId}", but config app is "${cfg.app.id}"`);
651
+ }
652
+ const entry = credentials.envs[env];
653
+ if (!entry?.tenantId || !entry.dbKey) {
654
+ throw new Error(`local credentials have no db key for env "${env}". Run "odla-ai provision --write-dev-vars".`);
655
+ }
656
+ const doFetch = options.fetch ?? fetch;
657
+ out.log(`smoke: ${cfg.app.id}/${env}`);
658
+ out.log(` tenant: ${entry.tenantId}`);
659
+ const publicConfig = await getJson(doFetch, publicConfigUrl(cfg.platformUrl, cfg.app.id, env), void 0);
660
+ out.log(` public-config: ok`);
661
+ if (cfg.ai?.provider) {
662
+ const provider = publicConfig.ai?.provider ?? null;
663
+ if (provider !== cfg.ai.provider) {
664
+ throw new Error(`ai provider mismatch: expected "${cfg.ai.provider}", public-config has "${provider ?? "none"}"`);
665
+ }
666
+ out.log(` ai: ${provider}`);
667
+ }
668
+ const expectedSchema = await resolveDataExport(cfg, cfg.db?.schema, ["schema", "SCHEMA"]);
669
+ const expectedEntities = serializedEntities(expectedSchema);
670
+ const liveSchemaPayload = await getJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/schema`, entry.dbKey);
671
+ const liveSchema = liveSchemaPayload.schema ?? liveSchemaPayload;
672
+ const liveEntities = serializedEntities(liveSchema);
673
+ if (expectedEntities.length) {
674
+ const missing = expectedEntities.filter((entity) => !liveEntities.includes(entity));
675
+ if (missing.length) throw new Error(`live schema is missing expected entities: ${missing.join(", ")}`);
676
+ }
677
+ out.log(` schema: ${liveEntities.length} entities`);
678
+ const aggregateEntity = expectedEntities[0] ?? liveEntities[0];
679
+ if (aggregateEntity) {
680
+ const aggregate = await postJson2(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/aggregate`, entry.dbKey, {
681
+ ns: aggregateEntity,
682
+ aggregate: { count: true }
683
+ });
684
+ const count = aggregate.aggregate?.count;
685
+ out.log(` aggregate: ${aggregateEntity}.count=${String(count ?? "ok")}`);
686
+ } else {
687
+ out.log(` aggregate: skipped (schema has no entities)`);
688
+ }
689
+ out.log("ok");
690
+ }
691
+ async function getJson(doFetch, url, bearer) {
692
+ const res = await doFetch(url, {
693
+ headers: bearer ? { authorization: `Bearer ${bearer}` } : void 0
694
+ });
695
+ if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText2(res)}`);
696
+ return res.json();
697
+ }
698
+ async function postJson2(doFetch, url, bearer, body) {
699
+ const res = await doFetch(url, {
700
+ method: "POST",
701
+ headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
702
+ body: JSON.stringify(body)
703
+ });
704
+ if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText2(res)}`);
705
+ return res.json();
706
+ }
707
+ function publicConfigUrl(platformUrl, appId, env) {
708
+ const url = new URL(`/registry/apps/${encodeURIComponent(appId)}/public-config`, platformUrl);
709
+ url.searchParams.set("env", env);
710
+ return url.toString();
711
+ }
712
+ async function safeText2(res) {
713
+ try {
714
+ return (await res.text()).slice(0, 500);
715
+ } catch {
716
+ return "";
717
+ }
718
+ }
719
+
720
+ // src/cli.ts
721
+ async function runCli(argv = process.argv.slice(2)) {
722
+ const parsed = parseArgv(argv);
723
+ const command = parsed.positionals[0] ?? "help";
724
+ if (command === "version" || command === "-v" || parsed.options.version === true) {
725
+ console.log(cliVersion());
726
+ return;
727
+ }
728
+ if (command === "help" || command === "--help" || command === "-h") {
729
+ printHelp();
730
+ return;
731
+ }
732
+ if (command === "init") {
733
+ initProject({
734
+ appId: requiredString(parsed.options["app-id"], "--app-id"),
735
+ name: requiredString(parsed.options.name, "--name"),
736
+ configPath: stringOpt(parsed.options.config),
737
+ envs: listOpt(parsed.options.env),
738
+ services: listOpt(parsed.options.services),
739
+ aiProvider: stringOpt(parsed.options["ai-provider"]),
740
+ force: parsed.options.force === true
741
+ });
742
+ return;
743
+ }
744
+ if (command === "doctor") {
745
+ await doctor({ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs" });
746
+ return;
747
+ }
748
+ if (command === "provision") {
749
+ const writeDevVars2 = parsed.options["write-dev-vars"];
750
+ const opts = {
751
+ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
752
+ dryRun: parsed.options["dry-run"] === true,
753
+ rotateKeys: parsed.options["rotate-keys"] === true,
754
+ writeCredentials: parsed.options["write-credentials"] !== false,
755
+ writeDevVars: typeof writeDevVars2 === "string" ? writeDevVars2 : writeDevVars2 === true,
756
+ token: stringOpt(parsed.options.token),
757
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
758
+ yes: parsed.options.yes === true
759
+ };
760
+ await provision(opts);
761
+ return;
762
+ }
763
+ if (command === "smoke") {
764
+ await smoke({
765
+ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
766
+ env: stringOpt(parsed.options.env)
767
+ });
768
+ return;
769
+ }
770
+ throw new Error(`unknown command "${command}". Run "odla-ai help".`);
771
+ }
772
+ function parseArgv(argv) {
773
+ const positionals = [];
774
+ const options = {};
775
+ for (let i = 0; i < argv.length; i++) {
776
+ const arg = argv[i];
777
+ if (!arg.startsWith("--")) {
778
+ positionals.push(arg);
779
+ continue;
780
+ }
781
+ const eq = arg.indexOf("=");
782
+ const rawName = arg.slice(2, eq === -1 ? void 0 : eq);
783
+ if (!rawName) continue;
784
+ if (rawName.startsWith("no-")) {
785
+ options[rawName.slice(3)] = false;
786
+ continue;
787
+ }
788
+ if (eq !== -1) {
789
+ addOption(options, rawName, arg.slice(eq + 1));
790
+ continue;
791
+ }
792
+ const value = argv[i + 1];
793
+ if (value !== void 0 && !value.startsWith("--")) {
794
+ i++;
795
+ addOption(options, rawName, value);
796
+ } else {
797
+ addOption(options, rawName, true);
798
+ }
799
+ }
800
+ return { positionals, options };
801
+ }
802
+ function addOption(options, name, value) {
803
+ const cur = options[name];
804
+ if (cur === void 0) {
805
+ options[name] = value;
806
+ } else if (Array.isArray(cur)) {
807
+ cur.push(String(value));
808
+ } else {
809
+ options[name] = [String(cur), String(value)];
810
+ }
811
+ }
812
+ function requiredString(value, name) {
813
+ const s = stringOpt(value);
814
+ if (!s) throw new Error(`${name} is required`);
815
+ return s;
816
+ }
817
+ function stringOpt(value) {
818
+ if (typeof value === "string") return value;
819
+ if (Array.isArray(value)) return value[value.length - 1];
820
+ return void 0;
821
+ }
822
+ function listOpt(value) {
823
+ if (value === void 0 || value === false || value === true) return void 0;
824
+ const values = Array.isArray(value) ? value : [value];
825
+ return values.flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
826
+ }
827
+ function cliVersion() {
828
+ const pkg = JSON.parse((0, import_node_fs4.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
829
+ return pkg.version ?? "unknown";
830
+ }
831
+ function printHelp() {
832
+ console.log(`odla-ai
833
+
834
+ Usage:
835
+ odla-ai init --app-id <id> --name <name> [--services db,ai] [--env dev --env prod]
836
+ odla-ai doctor [--config odla.config.mjs]
837
+ odla-ai provision [--config odla.config.mjs] [--dry-run] [--open|--no-open] [--rotate-keys] [--write-dev-vars[=path]]
838
+ odla-ai smoke [--config odla.config.mjs] [--env dev]
839
+ odla-ai version
840
+
841
+ Commands:
842
+ init Create a generic odla.config.mjs plus starter schema/rules files.
843
+ doctor Validate and summarize the project config without network calls.
844
+ provision Register the app, enable services, push schema/rules, configure AI/auth.
845
+ smoke Verify local credentials, public-config, live schema, and db aggregate.
846
+ version Print the CLI version.
847
+
848
+ Safety:
849
+ Provision caches the approved developer token and local db keys under .odla/
850
+ with mode 0600, and init adds those paths to .gitignore.
851
+ Provision opens the approval page automatically in interactive terminals;
852
+ use --open to force browser launch or --no-open to suppress it.
853
+ `);
854
+ }
855
+ // Annotate the CommonJS export names for ESM import in node:
856
+ 0 && (module.exports = {
857
+ doctor,
858
+ initProject,
859
+ provision,
860
+ runCli,
861
+ smoke
862
+ });
863
+ //# sourceMappingURL=index.cjs.map