@odla-ai/cli 0.11.2 → 0.13.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 +49 -8
- package/REQUIREMENTS.md +11 -3
- package/dist/bin.cjs +874 -382
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +3 -2
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-DC4MIB4X.js → chunk-I7XDJZB6.js} +791 -301
- package/dist/chunk-I7XDJZB6.js.map +1 -0
- package/dist/index.cjs +877 -385
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -62
- package/dist/index.d.ts +114 -62
- package/dist/index.js +5 -5
- package/package.json +1 -1
- package/skills/odla/SKILL.md +23 -13
- package/skills/odla/references/build.md +26 -16
- package/skills/odla/references/sdks.md +54 -17
- package/skills/odla-migrate/SKILL.md +8 -6
- package/skills/odla-migrate/references/phase-2-db.md +2 -2
- package/skills/odla-migrate/references/phase-2b-calendar.md +130 -42
- package/skills/odla-migrate/references/secrets-map.md +3 -0
- package/skills/odla-migrate/references/troubleshooting.md +16 -5
- package/dist/chunk-DC4MIB4X.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -33,19 +33,19 @@ var index_exports = {};
|
|
|
33
33
|
__export(index_exports, {
|
|
34
34
|
AGENT_HARNESSES: () => AGENT_HARNESSES,
|
|
35
35
|
CAPABILITIES: () => CAPABILITIES,
|
|
36
|
-
|
|
36
|
+
GOOGLE_CALENDAR_EVENTS_SCOPE: () => GOOGLE_CALENDAR_EVENTS_SCOPE,
|
|
37
37
|
SYSTEM_AI_PURPOSES: () => SYSTEM_AI_PURPOSES,
|
|
38
38
|
adminAi: () => adminAi,
|
|
39
39
|
calendarBookingPageUrl: () => calendarBookingPageUrl,
|
|
40
40
|
calendarCalendars: () => calendarCalendars,
|
|
41
41
|
calendarConnect: () => calendarConnect,
|
|
42
42
|
calendarDisconnect: () => calendarDisconnect,
|
|
43
|
-
calendarResync: () => calendarResync,
|
|
44
43
|
calendarServiceConfig: () => calendarServiceConfig,
|
|
45
44
|
calendarStatus: () => calendarStatus,
|
|
46
45
|
connectGitHubSecuritySource: () => connectGitHubSecuritySource,
|
|
47
46
|
disconnectGitHubSecuritySource: () => disconnectGitHubSecuritySource,
|
|
48
47
|
doctor: () => doctor,
|
|
48
|
+
exitCodeFor: () => exitCodeFor,
|
|
49
49
|
followHostedSecurityJob: () => followHostedSecurityJob,
|
|
50
50
|
getHostedSecurityIntent: () => getHostedSecurityIntent,
|
|
51
51
|
getHostedSecurityJob: () => getHostedSecurityJob,
|
|
@@ -77,35 +77,16 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
|
|
|
77
77
|
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
78
78
|
|
|
79
79
|
// src/admin-ai.ts
|
|
80
|
-
var
|
|
80
|
+
var import_node_process6 = __toESM(require("process"), 1);
|
|
81
81
|
|
|
82
82
|
// src/token.ts
|
|
83
83
|
var import_db = require("@odla-ai/db");
|
|
84
|
-
var
|
|
84
|
+
var import_node_process3 = __toESM(require("process"), 1);
|
|
85
85
|
|
|
86
|
-
// src/
|
|
87
|
-
var
|
|
86
|
+
// src/handshake-state.ts
|
|
87
|
+
var import_node_fs2 = require("fs");
|
|
88
|
+
var import_node_path2 = require("path");
|
|
88
89
|
var import_node_process = __toESM(require("process"), 1);
|
|
89
|
-
async function openUrl(url, options = {}) {
|
|
90
|
-
const command = openerFor(options.platform ?? import_node_process.default.platform);
|
|
91
|
-
const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
|
|
92
|
-
await new Promise((resolve7, reject) => {
|
|
93
|
-
const child = doSpawn(command.cmd, [...command.args, url], {
|
|
94
|
-
stdio: "ignore",
|
|
95
|
-
detached: true
|
|
96
|
-
});
|
|
97
|
-
child.once("error", reject);
|
|
98
|
-
child.once("spawn", () => {
|
|
99
|
-
child.unref();
|
|
100
|
-
resolve7();
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
function openerFor(platform) {
|
|
105
|
-
if (platform === "darwin") return { cmd: "open", args: [] };
|
|
106
|
-
if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
|
|
107
|
-
return { cmd: "xdg-open", args: [] };
|
|
108
|
-
}
|
|
109
90
|
|
|
110
91
|
// src/local.ts
|
|
111
92
|
var import_node_fs = require("fs");
|
|
@@ -233,54 +214,191 @@ function displayPath(path, rootDir = process.cwd()) {
|
|
|
233
214
|
return rel && !rel.startsWith("..") ? rel : path;
|
|
234
215
|
}
|
|
235
216
|
|
|
217
|
+
// src/handshake-state.ts
|
|
218
|
+
function handshakeFile(cfg) {
|
|
219
|
+
return (0, import_node_path2.join)((0, import_node_path2.dirname)(cfg.local.tokenFile), "handshake.local.json");
|
|
220
|
+
}
|
|
221
|
+
var RESUME_MARGIN_MS = 5e3;
|
|
222
|
+
function readPendingHandshake(path, platform, email) {
|
|
223
|
+
const pending = readJsonFile(path);
|
|
224
|
+
if (!pending || pending.platform !== platform || pending.email !== email) return null;
|
|
225
|
+
if (typeof pending.userCode !== "string" || typeof pending.deviceCode !== "string" || typeof pending.approvalUrl !== "string") return null;
|
|
226
|
+
if (typeof pending.expiresAt !== "number" || pending.expiresAt <= Date.now() + RESUME_MARGIN_MS) return null;
|
|
227
|
+
return { interval: 3, ...pending };
|
|
228
|
+
}
|
|
229
|
+
function writePendingHandshake(path, pending) {
|
|
230
|
+
writePrivateJson(path, pending);
|
|
231
|
+
}
|
|
232
|
+
function clearPendingHandshake(path) {
|
|
233
|
+
(0, import_node_fs2.rmSync)(path, { force: true });
|
|
234
|
+
}
|
|
235
|
+
function minutesLeft(expiresAt) {
|
|
236
|
+
return Math.max(1, Math.round((expiresAt - Date.now()) / 6e4));
|
|
237
|
+
}
|
|
238
|
+
function approvalHint(pending) {
|
|
239
|
+
return `approve code ${pending.userCode} at ${pending.approvalUrl} (${minutesLeft(pending.expiresAt)}m left)`;
|
|
240
|
+
}
|
|
241
|
+
function approvalReminder(out, pending, periodMs = 3e4) {
|
|
242
|
+
const timer = setInterval(() => out.log(`auth: still waiting \u2014 ${approvalHint(pending)}`), periodMs);
|
|
243
|
+
timer.unref?.();
|
|
244
|
+
return () => clearInterval(timer);
|
|
245
|
+
}
|
|
246
|
+
function handshakeWaitMs(waitSeconds, interactive = import_node_process.default.stdout.isTTY === true) {
|
|
247
|
+
if (waitSeconds !== void 0) return waitSeconds * 1e3;
|
|
248
|
+
return interactive ? void 0 : 9e4;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/open.ts
|
|
252
|
+
var import_node_child_process = require("child_process");
|
|
253
|
+
var import_node_process2 = __toESM(require("process"), 1);
|
|
254
|
+
async function openUrl(url, options = {}) {
|
|
255
|
+
const command = openerFor(options.platform ?? import_node_process2.default.platform);
|
|
256
|
+
const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
|
|
257
|
+
await new Promise((resolve7, reject) => {
|
|
258
|
+
const child = doSpawn(command.cmd, [...command.args, url], {
|
|
259
|
+
stdio: "ignore",
|
|
260
|
+
detached: true
|
|
261
|
+
});
|
|
262
|
+
child.once("error", reject);
|
|
263
|
+
child.once("spawn", () => {
|
|
264
|
+
child.unref();
|
|
265
|
+
resolve7();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
function openerFor(platform) {
|
|
270
|
+
if (platform === "darwin") return { cmd: "open", args: [] };
|
|
271
|
+
if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
|
|
272
|
+
return { cmd: "xdg-open", args: [] };
|
|
273
|
+
}
|
|
274
|
+
|
|
236
275
|
// src/token.ts
|
|
237
276
|
async function getDeveloperToken(cfg, options, doFetch, out) {
|
|
238
277
|
if (options.token) return options.token;
|
|
239
278
|
const audience = platformAudience(cfg.platformUrl);
|
|
240
|
-
if (
|
|
241
|
-
const declared =
|
|
279
|
+
if (import_node_process3.default.env.ODLA_DEV_TOKEN) {
|
|
280
|
+
const declared = import_node_process3.default.env.ODLA_DEV_TOKEN_AUDIENCE;
|
|
242
281
|
if (declared) {
|
|
243
282
|
if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
|
|
244
283
|
} else if (audience !== "https://odla.ai") {
|
|
245
284
|
throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
|
|
246
285
|
}
|
|
247
|
-
return
|
|
286
|
+
return import_node_process3.default.env.ODLA_DEV_TOKEN;
|
|
248
287
|
}
|
|
249
288
|
const cached = readJsonFile(cfg.local.tokenFile);
|
|
250
289
|
if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
251
290
|
out.log(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
252
291
|
return cached.token;
|
|
253
292
|
}
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
out.log(`auth: opened browser for approval${browser.mode === "auto" ? " (auto)" : ""}`);
|
|
269
|
-
} catch (err) {
|
|
270
|
-
out.log(`auth: could not open browser (${err instanceof Error ? err.message : String(err)})`);
|
|
271
|
-
}
|
|
272
|
-
} else if (browser.reason) {
|
|
273
|
-
out.log(`auth: browser launch skipped (${browser.reason})`);
|
|
274
|
-
}
|
|
275
|
-
out.log("");
|
|
276
|
-
}
|
|
277
|
-
});
|
|
278
|
-
writePrivateJson(cfg.local.tokenFile, { platform: audience, email, token, expiresAt });
|
|
293
|
+
const ctx = {
|
|
294
|
+
cfg,
|
|
295
|
+
options,
|
|
296
|
+
doFetch,
|
|
297
|
+
out,
|
|
298
|
+
audience,
|
|
299
|
+
browser: approvalBrowser(options),
|
|
300
|
+
email: handshakeEmail(options.email, cached?.platform === audience ? cached.email : void 0),
|
|
301
|
+
pendingFile: handshakeFile(cfg)
|
|
302
|
+
};
|
|
303
|
+
const waitMs = handshakeWaitMs(options.wait);
|
|
304
|
+
const { token, expiresAt } = await resumePendingHandshake(ctx, waitMs) ?? await freshHandshake(ctx, waitMs);
|
|
305
|
+
clearPendingHandshake(ctx.pendingFile);
|
|
306
|
+
writePrivateJson(cfg.local.tokenFile, { platform: audience, email: ctx.email, token, expiresAt });
|
|
279
307
|
out.log(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
280
308
|
return token;
|
|
281
309
|
}
|
|
310
|
+
async function resumePendingHandshake(ctx, waitMs) {
|
|
311
|
+
const pending = readPendingHandshake(ctx.pendingFile, ctx.audience, ctx.email);
|
|
312
|
+
if (!pending) return null;
|
|
313
|
+
ctx.out.log("");
|
|
314
|
+
ctx.out.log(`auth: resuming pending handshake \u2014 ${approvalHint(pending)}`);
|
|
315
|
+
await launchApproval(ctx, pending.approvalUrl);
|
|
316
|
+
ctx.out.log("");
|
|
317
|
+
const stopReminder = approvalReminder(ctx.out, pending);
|
|
318
|
+
try {
|
|
319
|
+
return await (0, import_db.collectToken)({
|
|
320
|
+
endpoint: ctx.cfg.platformUrl,
|
|
321
|
+
deviceCode: pending.deviceCode,
|
|
322
|
+
expiresAt: pending.expiresAt,
|
|
323
|
+
interval: pending.interval,
|
|
324
|
+
waitMs,
|
|
325
|
+
fetch: ctx.doFetch
|
|
326
|
+
});
|
|
327
|
+
} catch (err) {
|
|
328
|
+
const code = err instanceof import_db.OdlaError ? err.code : void 0;
|
|
329
|
+
if (code === "handshake_pending") throw stillPending(pending, ctx.email);
|
|
330
|
+
if (code === "handshake_expired" || code === "handshake_timeout") {
|
|
331
|
+
clearPendingHandshake(ctx.pendingFile);
|
|
332
|
+
ctx.out.log("auth: pending handshake lapsed unapproved; starting a fresh one");
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
if (code === "handshake_denied") clearPendingHandshake(ctx.pendingFile);
|
|
336
|
+
throw err;
|
|
337
|
+
} finally {
|
|
338
|
+
stopReminder();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async function freshHandshake(ctx, waitMs) {
|
|
342
|
+
let started;
|
|
343
|
+
let stopReminder;
|
|
344
|
+
try {
|
|
345
|
+
return await (0, import_db.requestToken)({
|
|
346
|
+
endpoint: ctx.cfg.platformUrl,
|
|
347
|
+
email: ctx.email,
|
|
348
|
+
label: `${ctx.cfg.app.id} provisioner`,
|
|
349
|
+
fetch: ctx.doFetch,
|
|
350
|
+
waitMs,
|
|
351
|
+
onCode: async ({ userCode, deviceCode, expiresIn, interval, verificationUriComplete }) => {
|
|
352
|
+
const approvalUrl = verificationUriComplete ?? handshakeUrl(ctx.cfg.platformUrl, userCode);
|
|
353
|
+
started = {
|
|
354
|
+
platform: ctx.audience,
|
|
355
|
+
email: ctx.email,
|
|
356
|
+
userCode,
|
|
357
|
+
deviceCode,
|
|
358
|
+
approvalUrl,
|
|
359
|
+
interval,
|
|
360
|
+
expiresAt: Date.now() + expiresIn * 1e3
|
|
361
|
+
};
|
|
362
|
+
writePendingHandshake(ctx.pendingFile, started);
|
|
363
|
+
ctx.out.log("");
|
|
364
|
+
ctx.out.log(`Review, then ${approvalHint(started)}.`);
|
|
365
|
+
await launchApproval(ctx, approvalUrl);
|
|
366
|
+
ctx.out.log("");
|
|
367
|
+
stopReminder = approvalReminder(ctx.out, started);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
} catch (err) {
|
|
371
|
+
const code = err instanceof import_db.OdlaError ? err.code : void 0;
|
|
372
|
+
if (code === "handshake_pending" && started) throw stillPending(started, ctx.email);
|
|
373
|
+
if (code === "handshake_denied" || code === "handshake_expired" || code === "handshake_timeout") {
|
|
374
|
+
clearPendingHandshake(ctx.pendingFile);
|
|
375
|
+
}
|
|
376
|
+
throw err;
|
|
377
|
+
} finally {
|
|
378
|
+
stopReminder?.();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async function launchApproval(ctx, approvalUrl) {
|
|
382
|
+
if (ctx.browser.open) {
|
|
383
|
+
try {
|
|
384
|
+
await (ctx.options.openApprovalUrl ?? openUrl)(approvalUrl);
|
|
385
|
+
ctx.out.log(`auth: asked the OS to open a browser${ctx.browser.mode === "auto" ? " (auto)" : ""} \u2014 if no tab appeared, use the URL above`);
|
|
386
|
+
} catch (err) {
|
|
387
|
+
ctx.out.log(`auth: could not open browser (${err instanceof Error ? err.message : String(err)})`);
|
|
388
|
+
}
|
|
389
|
+
} else if (ctx.browser.reason) {
|
|
390
|
+
ctx.out.log(`auth: browser launch skipped (${ctx.browser.reason}) \u2014 show the human the URL above`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
function stillPending(pending, email) {
|
|
394
|
+
return new import_db.OdlaError(
|
|
395
|
+
"handshake_pending",
|
|
396
|
+
`handshake still pending \u2014 ask ${email} to ${approvalHint(pending)}, then re-run this command; it resumes the same handshake and collects the token`,
|
|
397
|
+
{ retryable: true }
|
|
398
|
+
);
|
|
399
|
+
}
|
|
282
400
|
function handshakeEmail(value, cached) {
|
|
283
|
-
const email = (value ??
|
|
401
|
+
const email = (value ?? import_node_process3.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
|
|
284
402
|
if (email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
285
403
|
throw new Error("a fresh odla handshake requires --email <account> or ODLA_USER_EMAIL");
|
|
286
404
|
}
|
|
@@ -289,10 +407,11 @@ function handshakeEmail(value, cached) {
|
|
|
289
407
|
function approvalBrowser(options, host = {}) {
|
|
290
408
|
if (options.open === true) return { open: true, mode: "forced" };
|
|
291
409
|
if (options.open === false) return { open: false, reason: "disabled by --no-open" };
|
|
292
|
-
const env = host.env ??
|
|
410
|
+
const env = host.env ?? import_node_process3.default.env;
|
|
411
|
+
if (env.VITEST || env.NODE_ENV === "test") return { open: false, reason: "test environment" };
|
|
293
412
|
if (env.CI) return { open: false, reason: "CI environment" };
|
|
294
413
|
if (env.SSH_CONNECTION || env.SSH_TTY) return { open: false, reason: "SSH session; pass --open to force" };
|
|
295
|
-
const platform = host.platform ??
|
|
414
|
+
const platform = host.platform ?? import_node_process3.default.platform;
|
|
296
415
|
if (platform !== "darwin" && platform !== "win32" && !env.DISPLAY && !env.WAYLAND_DISPLAY) {
|
|
297
416
|
return { open: false, reason: "no graphical display; pass --open to force" };
|
|
298
417
|
}
|
|
@@ -321,12 +440,12 @@ function platformAudience(value) {
|
|
|
321
440
|
}
|
|
322
441
|
|
|
323
442
|
// src/secret-input.ts
|
|
324
|
-
var
|
|
443
|
+
var import_node_process4 = __toESM(require("process"), 1);
|
|
325
444
|
var MAX_BYTES = 64 * 1024;
|
|
326
445
|
async function secretInputValue(options, kind = "credential") {
|
|
327
446
|
if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
|
|
328
447
|
let value;
|
|
329
|
-
if (options.fromEnv) value =
|
|
448
|
+
if (options.fromEnv) value = import_node_process4.default.env[options.fromEnv];
|
|
330
449
|
else if (options.stdin) value = await (options.readStdin ?? (() => readSecretStream(kind)))();
|
|
331
450
|
else throw new Error(`${kind} input required: use --from-env <NAME> or --stdin; values are never accepted as arguments`);
|
|
332
451
|
value = value?.replace(/[\r\n]+$/, "");
|
|
@@ -334,7 +453,7 @@ async function secretInputValue(options, kind = "credential") {
|
|
|
334
453
|
if (new TextEncoder().encode(value).byteLength > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
|
|
335
454
|
return value;
|
|
336
455
|
}
|
|
337
|
-
async function readSecretStream(kind, stream =
|
|
456
|
+
async function readSecretStream(kind, stream = import_node_process4.default.stdin) {
|
|
338
457
|
let value = "";
|
|
339
458
|
for await (const chunk of stream) {
|
|
340
459
|
value += String(chunk);
|
|
@@ -344,8 +463,8 @@ async function readSecretStream(kind, stream = import_node_process3.default.stdi
|
|
|
344
463
|
}
|
|
345
464
|
|
|
346
465
|
// src/admin-ai-auth.ts
|
|
347
|
-
var
|
|
348
|
-
var
|
|
466
|
+
var import_node_fs3 = require("fs");
|
|
467
|
+
var import_node_process5 = __toESM(require("process"), 1);
|
|
349
468
|
var import_db2 = require("@odla-ai/db");
|
|
350
469
|
async function getScopedPlatformToken(options) {
|
|
351
470
|
return resolveAdminPlatformToken(options);
|
|
@@ -353,7 +472,7 @@ async function getScopedPlatformToken(options) {
|
|
|
353
472
|
async function resolveAdminPlatformToken(options) {
|
|
354
473
|
const audience = platformAudience(options.platform);
|
|
355
474
|
if (options.token) return options.token;
|
|
356
|
-
const fromEnv =
|
|
475
|
+
const fromEnv = import_node_process5.default.env.ODLA_ADMIN_TOKEN;
|
|
357
476
|
if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
|
|
358
477
|
return scopedToken(
|
|
359
478
|
audience,
|
|
@@ -365,7 +484,7 @@ async function resolveAdminPlatformToken(options) {
|
|
|
365
484
|
}
|
|
366
485
|
function audienceBoundEnvToken(token, platform) {
|
|
367
486
|
const audience = platformAudience(platform);
|
|
368
|
-
const declared =
|
|
487
|
+
const declared = import_node_process5.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
|
|
369
488
|
if (declared) {
|
|
370
489
|
if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
|
|
371
490
|
} else if (audience !== "https://odla.ai") {
|
|
@@ -375,7 +494,7 @@ function audienceBoundEnvToken(token, platform) {
|
|
|
375
494
|
}
|
|
376
495
|
async function scopedToken(platform, scope, options, doFetch, out) {
|
|
377
496
|
const audience = platformAudience(platform);
|
|
378
|
-
const tokenFile = options.tokenFile ?? `${
|
|
497
|
+
const tokenFile = options.tokenFile ?? `${import_node_process5.default.cwd()}/.odla/admin-token.local.json`;
|
|
379
498
|
const cache = readJsonFile(tokenFile);
|
|
380
499
|
const cached = cache?.platform === audience ? cache.tokens?.[scope] : void 0;
|
|
381
500
|
if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
@@ -399,7 +518,7 @@ async function scopedToken(platform, scope, options, doFetch, out) {
|
|
|
399
518
|
});
|
|
400
519
|
const tokens = cache?.platform === audience ? { ...cache.tokens ?? {} } : {};
|
|
401
520
|
tokens[scope] = { token, expiresAt };
|
|
402
|
-
if ((0,
|
|
521
|
+
if ((0, import_node_fs3.existsSync)(`${import_node_process5.default.cwd()}/.git`)) ensureGitignore(import_node_process5.default.cwd(), [tokenFile]);
|
|
403
522
|
writePrivateJson(tokenFile, { platform: audience, email, tokens });
|
|
404
523
|
out.log(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
|
|
405
524
|
return token;
|
|
@@ -560,7 +679,7 @@ function isRecord2(value) {
|
|
|
560
679
|
|
|
561
680
|
// src/admin-ai.ts
|
|
562
681
|
async function adminAi(options) {
|
|
563
|
-
const platform = platformAudience(options.platform ??
|
|
682
|
+
const platform = platformAudience(options.platform ?? import_node_process6.default.env.ODLA_PLATFORM ?? "https://odla.ai");
|
|
564
683
|
const doFetch = options.fetch ?? fetch;
|
|
565
684
|
const out = options.stdout ?? console;
|
|
566
685
|
const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
|
|
@@ -875,87 +994,107 @@ async function adminCommand(parsed) {
|
|
|
875
994
|
});
|
|
876
995
|
}
|
|
877
996
|
|
|
878
|
-
// src/
|
|
879
|
-
var
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
997
|
+
// src/app-export.ts
|
|
998
|
+
var import_node_fs5 = require("fs");
|
|
999
|
+
var import_node_stream = require("stream");
|
|
1000
|
+
var import_promises = require("stream/promises");
|
|
1001
|
+
|
|
1002
|
+
// src/config.ts
|
|
1003
|
+
var import_node_fs4 = require("fs");
|
|
1004
|
+
var import_node_path3 = require("path");
|
|
1005
|
+
var import_node_url = require("url");
|
|
1006
|
+
|
|
1007
|
+
// src/integration-validation.ts
|
|
1008
|
+
function validateIntegrations(cfg, path, defaultServices) {
|
|
1009
|
+
if (cfg.integrations === void 0) return;
|
|
1010
|
+
if (!Array.isArray(cfg.integrations)) throw new Error(`${path}: integrations must be an array`);
|
|
1011
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1012
|
+
for (const [index, integration] of cfg.integrations.entries()) {
|
|
1013
|
+
const at = `${path}: integrations[${index}]`;
|
|
1014
|
+
if (!isRecord4(integration)) throw new Error(`${at} must be an object`);
|
|
1015
|
+
if (!validId(integration.id)) throw new Error(`${at}.id must be lowercase letters, numbers, and hyphens`);
|
|
1016
|
+
if (ids.has(integration.id)) throw new Error(`${path}: duplicate integration id "${integration.id}"`);
|
|
1017
|
+
ids.add(integration.id);
|
|
1018
|
+
if (!safeText(integration.title, 200)) throw new Error(`${at}.title is required`);
|
|
1019
|
+
if (!safeText(integration.npm, 200)) throw new Error(`${at}.npm is required`);
|
|
1020
|
+
if (integration.schema !== void 0 && (!isRecord4(integration.schema) || !isRecord4(integration.schema.entities))) {
|
|
1021
|
+
throw new Error(`${at}.schema must contain an entities object`);
|
|
1022
|
+
}
|
|
1023
|
+
if (integration.rules !== void 0 && !isRecord4(integration.rules)) throw new Error(`${at}.rules must be an object`);
|
|
1024
|
+
validateSeeds(integration, at);
|
|
1025
|
+
validateProbes(integration, at);
|
|
1026
|
+
}
|
|
1027
|
+
const needsDb = cfg.integrations.some((integration) => integration.schema || integration.rules || integration.seeds?.length);
|
|
1028
|
+
const services = unique(cfg.services?.length ? cfg.services : defaultServices);
|
|
1029
|
+
if (needsDb && !services.includes("db")) throw new Error(`${path}: schema/rules/seed integrations require the db service`);
|
|
1030
|
+
}
|
|
1031
|
+
function validateSeeds(integration, at) {
|
|
1032
|
+
if (integration.seeds === void 0) return;
|
|
1033
|
+
if (!Array.isArray(integration.seeds)) throw new Error(`${at}.seeds must be an array`);
|
|
1034
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1035
|
+
for (const [index, seed] of integration.seeds.entries()) {
|
|
1036
|
+
const sat = `${at}.seeds[${index}]`;
|
|
1037
|
+
if (!isRecord4(seed) || !safeText(seed.id, 200) || !safeText(seed.ns, 200)) throw new Error(`${sat} requires id and ns`);
|
|
1038
|
+
if (ids.has(seed.id)) throw new Error(`${at} has duplicate seed id "${seed.id}"`);
|
|
1039
|
+
ids.add(seed.id);
|
|
1040
|
+
if (!isRecord4(seed.key) || !safeText(seed.key.attr, 200) || !safeText(seed.key.value, 2048)) {
|
|
1041
|
+
throw new Error(`${sat}.key requires string attr and value`);
|
|
1042
|
+
}
|
|
1043
|
+
if (!isRecord4(seed.attrs)) throw new Error(`${sat}.attrs must be an object`);
|
|
1044
|
+
if (Object.hasOwn(seed.attrs, seed.key.attr) && seed.attrs[seed.key.attr] !== seed.key.value) {
|
|
1045
|
+
throw new Error(`${sat}.attrs.${seed.key.attr} conflicts with its natural key`);
|
|
1046
|
+
}
|
|
921
1047
|
}
|
|
922
|
-
out.log("odla-ai responsibility boundary\n");
|
|
923
|
-
printGroup(out, "CLI automates", CAPABILITIES.cli);
|
|
924
|
-
printGroup(out, "Coding agent changes source", CAPABILITIES.agent);
|
|
925
|
-
printGroup(out, "Human checkpoints", CAPABILITIES.human);
|
|
926
|
-
printGroup(out, "Studio", CAPABILITIES.studio);
|
|
927
1048
|
}
|
|
928
|
-
function
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1049
|
+
function validateProbes(integration, at) {
|
|
1050
|
+
if (integration.probes === void 0) return;
|
|
1051
|
+
if (!Array.isArray(integration.probes)) throw new Error(`${at}.probes must be an array`);
|
|
1052
|
+
for (const [index, probe] of integration.probes.entries()) {
|
|
1053
|
+
const pat = `${at}.probes[${index}]`;
|
|
1054
|
+
if (!isRecord4(probe) || !safeProbePath(probe.path)) throw new Error(`${pat}.path must be an absolute path without query or fragment`);
|
|
1055
|
+
if (!Number.isInteger(probe.expectedStatus) || probe.expectedStatus < 100 || probe.expectedStatus > 599) {
|
|
1056
|
+
throw new Error(`${pat}.expectedStatus must be an HTTP status`);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
function isRecord4(value) {
|
|
1061
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1062
|
+
}
|
|
1063
|
+
function safeText(value, max) {
|
|
1064
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= max && !/[\u0000-\u001f\u007f]/.test(value);
|
|
1065
|
+
}
|
|
1066
|
+
function safeProbePath(value) {
|
|
1067
|
+
return typeof value === "string" && /^\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]*$/.test(value);
|
|
1068
|
+
}
|
|
1069
|
+
function validId(value) {
|
|
1070
|
+
return typeof value === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value);
|
|
1071
|
+
}
|
|
1072
|
+
function unique(values) {
|
|
1073
|
+
return [...new Set(values.filter(Boolean))];
|
|
932
1074
|
}
|
|
933
1075
|
|
|
934
1076
|
// src/config.ts
|
|
935
|
-
var import_node_fs3 = require("fs");
|
|
936
|
-
var import_node_path2 = require("path");
|
|
937
|
-
var import_node_url = require("url");
|
|
938
1077
|
var DEFAULT_PLATFORM = "https://odla.ai";
|
|
939
1078
|
var DEFAULT_ENVS = ["dev"];
|
|
940
1079
|
var DEFAULT_SERVICES = ["db", "ai"];
|
|
941
|
-
var
|
|
1080
|
+
var GOOGLE_CALENDAR_EVENTS_SCOPE = "https://www.googleapis.com/auth/calendar.events";
|
|
942
1081
|
async function loadProjectConfig(configPath = "odla.config.mjs") {
|
|
943
|
-
const resolved = (0,
|
|
944
|
-
if (!(0,
|
|
1082
|
+
const resolved = (0, import_node_path3.resolve)(configPath);
|
|
1083
|
+
if (!(0, import_node_fs4.existsSync)(resolved)) {
|
|
945
1084
|
throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
|
|
946
1085
|
}
|
|
947
1086
|
const raw = await loadConfigModule(resolved);
|
|
948
|
-
const rootDir = (0,
|
|
1087
|
+
const rootDir = (0, import_node_path3.dirname)(resolved);
|
|
949
1088
|
validateRawConfig(raw, resolved);
|
|
950
1089
|
const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
|
|
951
1090
|
const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
|
|
952
|
-
const envs =
|
|
953
|
-
const services =
|
|
1091
|
+
const envs = unique2(raw.envs?.length ? raw.envs : DEFAULT_ENVS);
|
|
1092
|
+
const services = unique2(raw.services?.length ? raw.services : DEFAULT_SERVICES);
|
|
954
1093
|
validateCalendarConfig(raw, envs, services, resolved);
|
|
955
1094
|
const local = {
|
|
956
|
-
tokenFile: (0,
|
|
957
|
-
credentialsFile: (0,
|
|
958
|
-
devVarsFile: (0,
|
|
1095
|
+
tokenFile: (0, import_node_path3.resolve)(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
|
|
1096
|
+
credentialsFile: (0, import_node_path3.resolve)(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
|
|
1097
|
+
devVarsFile: (0, import_node_path3.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
|
|
959
1098
|
gitignore: raw.local?.gitignore ?? true
|
|
960
1099
|
};
|
|
961
1100
|
return {
|
|
@@ -972,9 +1111,9 @@ async function loadProjectConfig(configPath = "odla.config.mjs") {
|
|
|
972
1111
|
async function resolveDataExport(cfg, value, names) {
|
|
973
1112
|
if (value === void 0 || value === null || value === false) return void 0;
|
|
974
1113
|
if (typeof value !== "string") return value;
|
|
975
|
-
const target = (0,
|
|
1114
|
+
const target = (0, import_node_path3.isAbsolute)(value) ? value : (0, import_node_path3.resolve)(cfg.rootDir, value);
|
|
976
1115
|
if (target.endsWith(".json")) {
|
|
977
|
-
return JSON.parse((0,
|
|
1116
|
+
return JSON.parse((0, import_node_fs4.readFileSync)(target, "utf8"));
|
|
978
1117
|
}
|
|
979
1118
|
const mod = await import((0, import_node_url.pathToFileURL)(target).href);
|
|
980
1119
|
for (const name of names) {
|
|
@@ -984,6 +1123,8 @@ async function resolveDataExport(cfg, value, names) {
|
|
|
984
1123
|
throw new Error(`${value} did not export ${names.join(", ")} or default`);
|
|
985
1124
|
}
|
|
986
1125
|
function buildPlan(cfg) {
|
|
1126
|
+
const integrationSchema = cfg.integrations?.some((integration) => integration.schema) ?? false;
|
|
1127
|
+
const integrationRules = cfg.integrations?.some((integration) => integration.rules) ?? false;
|
|
987
1128
|
return {
|
|
988
1129
|
appId: cfg.app.id,
|
|
989
1130
|
appName: cfg.app.name,
|
|
@@ -991,8 +1132,9 @@ function buildPlan(cfg) {
|
|
|
991
1132
|
dbEndpoint: cfg.dbEndpoint,
|
|
992
1133
|
envs: cfg.envs,
|
|
993
1134
|
services: cfg.services,
|
|
994
|
-
|
|
995
|
-
|
|
1135
|
+
integrations: (cfg.integrations ?? []).map((integration) => integration.id),
|
|
1136
|
+
hasSchema: !!cfg.db?.schema || integrationSchema,
|
|
1137
|
+
hasRules: !!cfg.db?.rules || integrationRules || (!!cfg.db?.schema || integrationSchema) && cfg.db?.defaultRules !== false,
|
|
996
1138
|
aiProvider: cfg.ai?.provider
|
|
997
1139
|
};
|
|
998
1140
|
}
|
|
@@ -1001,16 +1143,14 @@ function calendarServiceConfig(cfg, env) {
|
|
|
1001
1143
|
if (!cfg.envs.includes(env)) throw new Error(`calendar env "${env}" is not declared in config envs`);
|
|
1002
1144
|
const google = cfg.calendar?.google;
|
|
1003
1145
|
if (!google) throw new Error("calendar.google is required when the calendar service is enabled");
|
|
1146
|
+
const availability = unique2(
|
|
1147
|
+
(google.availabilityCalendars?.[env] ?? google.calendars?.[env]).map((id) => id.trim())
|
|
1148
|
+
);
|
|
1004
1149
|
return {
|
|
1005
1150
|
provider: "google",
|
|
1006
|
-
access: "
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
organizerSelf: google.match?.organizerSelf ?? true,
|
|
1010
|
-
requireAttendees: google.match?.requireAttendees ?? true,
|
|
1011
|
-
...google.match?.summaryPrefix !== void 0 ? { summaryPrefix: google.match.summaryPrefix.trim() } : {}
|
|
1012
|
-
},
|
|
1013
|
-
attendeePolicy: google.attendeePolicy ?? "full"
|
|
1151
|
+
access: "book",
|
|
1152
|
+
bookingCalendarId: google.bookingCalendar?.[env]?.trim() ?? availability[0],
|
|
1153
|
+
availabilityCalendars: availability
|
|
1014
1154
|
};
|
|
1015
1155
|
}
|
|
1016
1156
|
function calendarBookingPageUrl(cfg, env) {
|
|
@@ -1039,15 +1179,16 @@ function validateRawConfig(raw, path) {
|
|
|
1039
1179
|
if (!raw || typeof raw !== "object") throw new Error(`${path} must export an object`);
|
|
1040
1180
|
const cfg = raw;
|
|
1041
1181
|
if (!cfg.app || typeof cfg.app !== "object") throw new Error(`${path}: missing app object`);
|
|
1042
|
-
if (!
|
|
1182
|
+
if (!validId2(cfg.app.id)) throw new Error(`${path}: app.id must be lowercase letters, numbers, and hyphens`);
|
|
1043
1183
|
if (!cfg.app.name || typeof cfg.app.name !== "string") throw new Error(`${path}: app.name is required`);
|
|
1044
1184
|
if (cfg.envs !== void 0 && (!Array.isArray(cfg.envs) || cfg.envs.some((env) => typeof env !== "string"))) {
|
|
1045
1185
|
throw new Error(`${path}: envs must be an array of names`);
|
|
1046
1186
|
}
|
|
1047
|
-
if (cfg.envs?.some((env) => !
|
|
1187
|
+
if (cfg.envs?.some((env) => !validId2(env))) throw new Error(`${path}: env names must be lowercase letters, numbers, and hyphens`);
|
|
1048
1188
|
if (cfg.services !== void 0 && (!Array.isArray(cfg.services) || cfg.services.some((service) => typeof service !== "string" || !service.trim()))) {
|
|
1049
1189
|
throw new Error(`${path}: services must be an array of non-empty names`);
|
|
1050
1190
|
}
|
|
1191
|
+
validateIntegrations(cfg, path, DEFAULT_SERVICES);
|
|
1051
1192
|
}
|
|
1052
1193
|
function validateCalendarConfig(cfg, envs, services, path) {
|
|
1053
1194
|
const enabled = services.includes("calendar");
|
|
@@ -1055,28 +1196,47 @@ function validateCalendarConfig(cfg, envs, services, path) {
|
|
|
1055
1196
|
if (enabled) throw new Error(`${path}: calendar.google is required when services includes "calendar"`);
|
|
1056
1197
|
return;
|
|
1057
1198
|
}
|
|
1058
|
-
if (!
|
|
1199
|
+
if (!isRecord5(cfg.calendar)) throw new Error(`${path}: calendar must be an object`);
|
|
1059
1200
|
assertOnly(cfg.calendar, ["google"], `${path}: calendar`);
|
|
1060
|
-
if (!
|
|
1201
|
+
if (!isRecord5(cfg.calendar.google)) throw new Error(`${path}: calendar.google must be an object`);
|
|
1061
1202
|
const google = cfg.calendar.google;
|
|
1062
|
-
assertOnly(
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1203
|
+
assertOnly(
|
|
1204
|
+
google,
|
|
1205
|
+
["availabilityCalendars", "calendars", "bookingCalendar", "bookingPageUrl"],
|
|
1206
|
+
`${path}: calendar.google`
|
|
1207
|
+
);
|
|
1208
|
+
const availabilityKey = google.availabilityCalendars !== void 0 ? "availabilityCalendars" : google.calendars !== void 0 ? "calendars" : null;
|
|
1209
|
+
if (!availabilityKey || google.availabilityCalendars !== void 0 && google.calendars !== void 0) {
|
|
1210
|
+
throw new Error(`${path}: calendar.google requires exactly one of availabilityCalendars or calendars (legacy)`);
|
|
1211
|
+
}
|
|
1212
|
+
const availability = google[availabilityKey];
|
|
1213
|
+
if (!isRecord5(availability)) throw new Error(`${path}: calendar.google.${availabilityKey} must map env names to calendar ids`);
|
|
1214
|
+
const unknownEnv = Object.keys(availability).find((env) => !envs.includes(env));
|
|
1215
|
+
if (unknownEnv) throw new Error(`${path}: calendar.google.${availabilityKey}.${unknownEnv} is not in config envs`);
|
|
1066
1216
|
for (const env of envs) {
|
|
1067
|
-
const ids =
|
|
1217
|
+
const ids = availability[env];
|
|
1068
1218
|
if (!Array.isArray(ids) || ids.length === 0) {
|
|
1069
|
-
throw new Error(`${path}: calendar.google
|
|
1219
|
+
throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must be a non-empty array`);
|
|
1070
1220
|
}
|
|
1071
1221
|
if (ids.length > 10) {
|
|
1072
|
-
throw new Error(`${path}: calendar.google
|
|
1222
|
+
throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must contain at most 10 calendar ids`);
|
|
1073
1223
|
}
|
|
1074
|
-
if (ids.some((id) => !
|
|
1075
|
-
throw new Error(`${path}: calendar.google
|
|
1224
|
+
if (ids.some((id) => !safeText2(id, 1024))) {
|
|
1225
|
+
throw new Error(`${path}: calendar.google.${availabilityKey}.${env} contains an invalid calendar id`);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
if (google.bookingCalendar !== void 0) {
|
|
1229
|
+
if (!isRecord5(google.bookingCalendar)) throw new Error(`${path}: calendar.google.bookingCalendar must map env names to one calendar id`);
|
|
1230
|
+
const unknownBookingEnv = Object.keys(google.bookingCalendar).find((env) => !envs.includes(env));
|
|
1231
|
+
if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingCalendar.${unknownBookingEnv} is not in config envs`);
|
|
1232
|
+
for (const [env, value] of Object.entries(google.bookingCalendar)) {
|
|
1233
|
+
if (!safeText2(value, 1024)) {
|
|
1234
|
+
throw new Error(`${path}: calendar.google.bookingCalendar.${env} must be a calendar id`);
|
|
1235
|
+
}
|
|
1076
1236
|
}
|
|
1077
1237
|
}
|
|
1078
1238
|
if (google.bookingPageUrl !== void 0) {
|
|
1079
|
-
if (!
|
|
1239
|
+
if (!isRecord5(google.bookingPageUrl)) throw new Error(`${path}: calendar.google.bookingPageUrl must map env names to HTTPS URLs or null`);
|
|
1080
1240
|
const unknownBookingEnv = Object.keys(google.bookingPageUrl).find((env) => !envs.includes(env));
|
|
1081
1241
|
if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingPageUrl.${unknownBookingEnv} is not in config envs`);
|
|
1082
1242
|
for (const [env, value] of Object.entries(google.bookingPageUrl)) {
|
|
@@ -1085,31 +1245,16 @@ function validateCalendarConfig(cfg, envs, services, path) {
|
|
|
1085
1245
|
}
|
|
1086
1246
|
}
|
|
1087
1247
|
}
|
|
1088
|
-
if (google.attendeePolicy !== void 0 && google.attendeePolicy !== "full" && google.attendeePolicy !== "hashed") {
|
|
1089
|
-
throw new Error(`${path}: calendar.google.attendeePolicy must be "full" or "hashed"`);
|
|
1090
|
-
}
|
|
1091
|
-
if (google.match !== void 0) {
|
|
1092
|
-
if (!isRecord4(google.match)) throw new Error(`${path}: calendar.google.match must be an object`);
|
|
1093
|
-
assertOnly(google.match, ["summaryPrefix", "organizerSelf", "requireAttendees"], `${path}: calendar.google.match`);
|
|
1094
|
-
if (google.match.summaryPrefix !== void 0 && !safeText(google.match.summaryPrefix, 256)) {
|
|
1095
|
-
throw new Error(`${path}: calendar.google.match.summaryPrefix must be 1-256 printable characters`);
|
|
1096
|
-
}
|
|
1097
|
-
for (const name of ["organizerSelf", "requireAttendees"]) {
|
|
1098
|
-
if (google.match[name] !== void 0 && typeof google.match[name] !== "boolean") {
|
|
1099
|
-
throw new Error(`${path}: calendar.google.match.${name} must be boolean`);
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
1248
|
if (enabled && !services.includes("db")) throw new Error(`${path}: calendar service requires the db service`);
|
|
1104
1249
|
}
|
|
1105
1250
|
function assertOnly(value, allowed, label) {
|
|
1106
1251
|
const extra = Object.keys(value).find((key) => !allowed.includes(key));
|
|
1107
1252
|
if (extra) throw new Error(`${label}.${extra} is not supported`);
|
|
1108
1253
|
}
|
|
1109
|
-
function
|
|
1254
|
+
function isRecord5(value) {
|
|
1110
1255
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1111
1256
|
}
|
|
1112
|
-
function
|
|
1257
|
+
function safeText2(value, max) {
|
|
1113
1258
|
return typeof value === "string" && value.trim().length > 0 && value.length <= max && !/[\u0000-\u001f\u007f]/.test(value);
|
|
1114
1259
|
}
|
|
1115
1260
|
function safeHttpsUrl(value) {
|
|
@@ -1121,11 +1266,11 @@ function safeHttpsUrl(value) {
|
|
|
1121
1266
|
return false;
|
|
1122
1267
|
}
|
|
1123
1268
|
}
|
|
1124
|
-
function
|
|
1269
|
+
function validId2(value) {
|
|
1125
1270
|
return typeof value === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value);
|
|
1126
1271
|
}
|
|
1127
1272
|
async function loadConfigModule(path) {
|
|
1128
|
-
if (path.endsWith(".json")) return JSON.parse((0,
|
|
1273
|
+
if (path.endsWith(".json")) return JSON.parse((0, import_node_fs4.readFileSync)(path, "utf8"));
|
|
1129
1274
|
const mod = await import(`${(0, import_node_url.pathToFileURL)(path).href}?t=${Date.now()}`);
|
|
1130
1275
|
const value = mod.default ?? mod.config;
|
|
1131
1276
|
if (typeof value === "function") return await value();
|
|
@@ -1134,14 +1279,191 @@ async function loadConfigModule(path) {
|
|
|
1134
1279
|
function trimSlash(value) {
|
|
1135
1280
|
return value.replace(/\/+$/, "");
|
|
1136
1281
|
}
|
|
1137
|
-
function
|
|
1282
|
+
function unique2(values) {
|
|
1138
1283
|
return [...new Set(values.filter(Boolean))];
|
|
1139
1284
|
}
|
|
1140
1285
|
|
|
1286
|
+
// src/app-export.ts
|
|
1287
|
+
async function appExport(options) {
|
|
1288
|
+
const cfg = await loadProjectConfig(options.configPath);
|
|
1289
|
+
const out = options.stdout ?? console;
|
|
1290
|
+
const doFetch = options.fetch ?? fetch;
|
|
1291
|
+
const env = options.env ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0]);
|
|
1292
|
+
if (!env || !cfg.envs.includes(env)) throw new Error(`env "${env ?? ""}" is not declared in ${options.configPath}`);
|
|
1293
|
+
const tenant = env === "prod" ? cfg.app.id : `${cfg.app.id}--${env}`;
|
|
1294
|
+
const token = await getDeveloperToken(
|
|
1295
|
+
cfg,
|
|
1296
|
+
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
1297
|
+
doFetch,
|
|
1298
|
+
out
|
|
1299
|
+
);
|
|
1300
|
+
const auth = { authorization: `Bearer ${token}` };
|
|
1301
|
+
const base = `${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenant)}`;
|
|
1302
|
+
if (options.fresh) {
|
|
1303
|
+
const res = await doFetch(`${base}/export`, { method: "POST", headers: auth });
|
|
1304
|
+
const body = await res.json().catch(() => ({}));
|
|
1305
|
+
if (!res.ok) throw new Error(`export failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? res.status}`);
|
|
1306
|
+
}
|
|
1307
|
+
const list = await doFetch(`${base}/backups`, { headers: auth });
|
|
1308
|
+
if (!list.ok) throw new Error(`couldn't list backups (${list.status})`);
|
|
1309
|
+
const { backups } = await list.json();
|
|
1310
|
+
const newest = backups[0];
|
|
1311
|
+
if (!newest) {
|
|
1312
|
+
throw new Error(
|
|
1313
|
+
`${tenant} has no backups yet \u2014 run \`odla-ai app export --fresh\` to take one now (nightly snapshots appear after the first day with writes)`
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
const download = await doFetch(`${base}/backups/${newest.id}/download`, { headers: auth });
|
|
1317
|
+
if (!download.ok || !download.body) throw new Error(`download failed (${download.status})`);
|
|
1318
|
+
const file = options.out ?? `${tenant}-${new Date(newest.created_at).toISOString().slice(0, 10)}-tx${newest.max_tx}.jsonl.gz`;
|
|
1319
|
+
await (0, import_promises.pipeline)(import_node_stream.Readable.fromWeb(download.body), (0, import_node_fs5.createWriteStream)(file));
|
|
1320
|
+
if (options.json) out.log(JSON.stringify({ file, backup: newest }, null, 2));
|
|
1321
|
+
else {
|
|
1322
|
+
out.log(`${tenant}: wrote ${file} (${newest.bytes} bytes, ${newest.kind} snapshot at tx ${newest.max_tx})`);
|
|
1323
|
+
out.log(`sha256 ${download.headers.get("x-odla-sha256") ?? newest.sha256}`);
|
|
1324
|
+
}
|
|
1325
|
+
return { file, backup: newest };
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// src/app-lifecycle.ts
|
|
1329
|
+
async function lifecycleCall(action, options) {
|
|
1330
|
+
const cfg = await loadProjectConfig(options.configPath);
|
|
1331
|
+
const out = options.stdout ?? console;
|
|
1332
|
+
const doFetch = options.fetch ?? fetch;
|
|
1333
|
+
const token = await getDeveloperToken(
|
|
1334
|
+
cfg,
|
|
1335
|
+
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
1336
|
+
doFetch,
|
|
1337
|
+
out
|
|
1338
|
+
);
|
|
1339
|
+
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/${action}`, {
|
|
1340
|
+
method: "POST",
|
|
1341
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }
|
|
1342
|
+
});
|
|
1343
|
+
const body = await res.json().catch(() => ({}));
|
|
1344
|
+
if (!res.ok || !body.ok) {
|
|
1345
|
+
throw new Error(`${action} failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? `registry returned ${res.status}`}`);
|
|
1346
|
+
}
|
|
1347
|
+
return body;
|
|
1348
|
+
}
|
|
1349
|
+
async function appArchive(options) {
|
|
1350
|
+
if (options.yes !== true) {
|
|
1351
|
+
throw new Error(
|
|
1352
|
+
"app archive suspends EVERY environment: API keys stop working and all services refuse requests until restored. All data is retained. Pass --yes to proceed."
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
const out = options.stdout ?? console;
|
|
1356
|
+
const body = await lifecycleCall("archive", options);
|
|
1357
|
+
if (options.json) out.log(JSON.stringify(body, null, 2));
|
|
1358
|
+
else if (body.operation?.state === "noop") out.log(`${body.app?.appId}: already archived`);
|
|
1359
|
+
else out.log(`${body.app?.appId}: archived \u2014 data retained; run \`odla-ai app restore\` (or use Studio) to bring it back`);
|
|
1360
|
+
}
|
|
1361
|
+
async function appRestore(options) {
|
|
1362
|
+
const out = options.stdout ?? console;
|
|
1363
|
+
const body = await lifecycleCall("restore", options);
|
|
1364
|
+
if (options.json) out.log(JSON.stringify(body, null, 2));
|
|
1365
|
+
else if (body.operation?.state === "noop") out.log(`${body.app?.appId}: already active`);
|
|
1366
|
+
else out.log(`${body.app?.appId}: restored \u2014 every service's data plane is live again`);
|
|
1367
|
+
}
|
|
1368
|
+
async function appCommand(parsed, dependencies = {}) {
|
|
1369
|
+
const sub = parsed.positionals[1];
|
|
1370
|
+
if (sub === "export") {
|
|
1371
|
+
assertArgs(parsed, ["config", "env", "token", "email", "fresh", "out", "json"], 2);
|
|
1372
|
+
await appExport({
|
|
1373
|
+
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
1374
|
+
env: stringOpt(parsed.options.env),
|
|
1375
|
+
token: stringOpt(parsed.options.token),
|
|
1376
|
+
email: stringOpt(parsed.options.email),
|
|
1377
|
+
fresh: parsed.options.fresh === true,
|
|
1378
|
+
out: stringOpt(parsed.options.out),
|
|
1379
|
+
json: parsed.options.json === true,
|
|
1380
|
+
fetch: dependencies.fetch,
|
|
1381
|
+
stdout: dependencies.stdout
|
|
1382
|
+
});
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
if (sub !== "archive" && sub !== "restore") {
|
|
1386
|
+
throw new Error(
|
|
1387
|
+
`unknown app subcommand "${sub ?? ""}". Try "odla-ai app archive --yes", "odla-ai app restore", or "odla-ai app export". (Permanent deletion has no CLI: it requires a signed-in owner in Studio.)`
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
assertArgs(parsed, ["config", "token", "email", "yes", "json"], 2);
|
|
1391
|
+
const options = {
|
|
1392
|
+
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
1393
|
+
token: stringOpt(parsed.options.token),
|
|
1394
|
+
email: stringOpt(parsed.options.email),
|
|
1395
|
+
yes: parsed.options.yes === true,
|
|
1396
|
+
json: parsed.options.json === true,
|
|
1397
|
+
fetch: dependencies.fetch,
|
|
1398
|
+
stdout: dependencies.stdout
|
|
1399
|
+
};
|
|
1400
|
+
if (sub === "archive") await appArchive(options);
|
|
1401
|
+
else await appRestore(options);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// src/capabilities.ts
|
|
1405
|
+
var CAPABILITIES = {
|
|
1406
|
+
cli: [
|
|
1407
|
+
"start an email-bound device request without accepting a password or user session token",
|
|
1408
|
+
"register the app and enable configured services per environment",
|
|
1409
|
+
"issue, persist, and explicitly rotate configured service credentials",
|
|
1410
|
+
"write local Worker values and push deployed Worker secrets through Wrangler stdin",
|
|
1411
|
+
"store tenant-vault secrets (including the Clerk webhook secret and reserved Clerk secret key) write-only from stdin or an env var",
|
|
1412
|
+
"compose declared app-capability schema/rules, create guarded seeds when absent, and configure platform AI, auth, and deployment links",
|
|
1413
|
+
"apply Google Calendar booking config, then drive state-bound consent, status, discovery, and disconnect flows (bookings run live through the platform proxy)",
|
|
1414
|
+
"validate integration contracts offline and smoke-test a provisioned db environment plus anonymous capability routes",
|
|
1415
|
+
"run app-attributed hosted security discovery and independent validation without provider keys",
|
|
1416
|
+
"connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
|
|
1417
|
+
"let admins manage system AI policy and credentials, inspect attributed usage, and read immutable admin changes through separate short-lived capability-only approvals"
|
|
1418
|
+
],
|
|
1419
|
+
agent: [
|
|
1420
|
+
"install and import the selected odla SDKs",
|
|
1421
|
+
"wrap the Worker with withObservability and choose useful telemetry",
|
|
1422
|
+
"install capability packages, mount their runtime routes, and make application-specific schema, rules, auth, UI, and migration decisions",
|
|
1423
|
+
"wire @odla-ai/calendar into trusted Worker code and keep the app admin key out of browsers"
|
|
1424
|
+
],
|
|
1425
|
+
human: [
|
|
1426
|
+
"provide the existing odla account email, then sign in and explicitly review/approve the exact device code",
|
|
1427
|
+
"review mirrored calendar/attendee disclosure and complete the server-issued Google consent flow",
|
|
1428
|
+
"consent to production changes with --yes",
|
|
1429
|
+
"request destructive credential rotation explicitly",
|
|
1430
|
+
"review application semantics, security findings, releases, and merges",
|
|
1431
|
+
"approve redacted-source disclosure before hosted security reasoning",
|
|
1432
|
+
"approve GitHub App repository access separately from redacted-snippet disclosure"
|
|
1433
|
+
],
|
|
1434
|
+
studio: [
|
|
1435
|
+
"view telemetry and environment state",
|
|
1436
|
+
"let signed-in users inventory/revoke their own agent grants and admins audit/global-revoke them",
|
|
1437
|
+
"review calendar connection, granted read scope, selected calendars, and sync health without exposing provider tokens",
|
|
1438
|
+
"perform manual credential recovery when the CLI's local shown-once copy is unavailable",
|
|
1439
|
+
"configure system AI purposes and view app/environment/run-attributed usage",
|
|
1440
|
+
"connect/revoke GitHub security sources and inspect ref-to-SHA jobs, routes, retention, coverage, and normalized reports"
|
|
1441
|
+
]
|
|
1442
|
+
};
|
|
1443
|
+
function printCapabilities(json = false, out = console) {
|
|
1444
|
+
if (json) {
|
|
1445
|
+
out.log(JSON.stringify(CAPABILITIES, null, 2));
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
out.log("odla-ai responsibility boundary\n");
|
|
1449
|
+
printGroup(out, "CLI automates", CAPABILITIES.cli);
|
|
1450
|
+
printGroup(out, "Coding agent changes source", CAPABILITIES.agent);
|
|
1451
|
+
printGroup(out, "Human checkpoints", CAPABILITIES.human);
|
|
1452
|
+
printGroup(out, "Studio", CAPABILITIES.studio);
|
|
1453
|
+
}
|
|
1454
|
+
function printGroup(out, heading, items) {
|
|
1455
|
+
out.log(`${heading}:`);
|
|
1456
|
+
for (const item of items) out.log(` - ${item}`);
|
|
1457
|
+
out.log("");
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1141
1460
|
// src/calendar-errors.ts
|
|
1142
1461
|
var PLATFORM_NOT_READY_CODES = /* @__PURE__ */ new Set([
|
|
1143
1462
|
"calendar_google_oauth_not_configured",
|
|
1144
1463
|
"calendar_token_vault_not_configured",
|
|
1464
|
+
// Current spelling (booking-era key verifier) plus the retired mirror-era
|
|
1465
|
+
// ingress code, so the CLI stays resumable against either platform version.
|
|
1466
|
+
"calendar_db_verifier_not_configured",
|
|
1145
1467
|
"calendar_db_ingress_not_configured"
|
|
1146
1468
|
]);
|
|
1147
1469
|
var CalendarRequestError = class extends Error {
|
|
@@ -1183,8 +1505,6 @@ function looksSecret(value) {
|
|
|
1183
1505
|
var CALENDAR_STATES = [
|
|
1184
1506
|
"not_connected",
|
|
1185
1507
|
"authorizing",
|
|
1186
|
-
"needs_sync",
|
|
1187
|
-
"initial_sync",
|
|
1188
1508
|
"healthy",
|
|
1189
1509
|
"degraded",
|
|
1190
1510
|
"disconnected",
|
|
@@ -1232,9 +1552,6 @@ async function pollCalendarConnection(ctx, attemptId) {
|
|
|
1232
1552
|
ctx.env
|
|
1233
1553
|
);
|
|
1234
1554
|
}
|
|
1235
|
-
async function requestCalendarResync(ctx) {
|
|
1236
|
-
return parseCalendarStatus(await calendarJson(ctx, "/resync", { method: "POST", body: "{}" }), ctx.env);
|
|
1237
|
-
}
|
|
1238
1555
|
async function requestCalendarDisconnect(ctx) {
|
|
1239
1556
|
return parseCalendarStatus(
|
|
1240
1557
|
await calendarJson(ctx, "/disconnect", { method: "POST", body: JSON.stringify({ purge: false }) }),
|
|
@@ -1245,10 +1562,8 @@ function parseCalendarStatus(raw, env) {
|
|
|
1245
1562
|
const outer = wrapped(raw, "calendar");
|
|
1246
1563
|
const value = record(outer.attempt) ?? record(outer.status) ?? outer;
|
|
1247
1564
|
const connection = record(value.connection) ?? {};
|
|
1248
|
-
const sync = record(value.sync) ?? record(connection.sync) ?? {};
|
|
1249
1565
|
const config = record(value.config) ?? record(outer.config) ?? {};
|
|
1250
1566
|
const googleConfig = record(config.google) ?? config;
|
|
1251
|
-
const watches = record(value.watches) ?? {};
|
|
1252
1567
|
const stateValue = calendarState(value.status ?? value.state ?? connection.status ?? connection.state);
|
|
1253
1568
|
if (!stateValue) {
|
|
1254
1569
|
throw new Error("calendar status returned an invalid connection state");
|
|
@@ -1258,31 +1573,29 @@ function parseCalendarStatus(raw, env) {
|
|
|
1258
1573
|
throw new Error("calendar status returned an unsupported provider");
|
|
1259
1574
|
}
|
|
1260
1575
|
const accessValue = value.access ?? connection.access ?? config.access ?? googleConfig.access;
|
|
1261
|
-
if (accessValue !== void 0 && accessValue !== "
|
|
1262
|
-
|
|
1263
|
-
if (policyValue !== void 0 && policyValue !== "full" && policyValue !== "hashed") {
|
|
1264
|
-
throw new Error("calendar status returned an invalid attendee policy");
|
|
1576
|
+
if (accessValue !== void 0 && accessValue !== "book" && accessValue !== "read") {
|
|
1577
|
+
throw new Error("calendar status returned unsupported access");
|
|
1265
1578
|
}
|
|
1266
1579
|
const errorValue = record(value.error) ?? record(connection.error);
|
|
1267
1580
|
const errorCode = textField(value.lastErrorCode, 128);
|
|
1268
1581
|
const bookingPageValue = Object.hasOwn(value, "bookingPageUrl") ? value.bookingPageUrl : Object.hasOwn(config, "bookingPageUrl") ? config.bookingPageUrl : googleConfig.bookingPageUrl;
|
|
1582
|
+
const connected = typeof (value.connected ?? connection.connected) === "boolean" ? Boolean(value.connected ?? connection.connected) : ["healthy", "degraded"].includes(stateValue);
|
|
1583
|
+
const bookingCalendarId = textField(value.bookingCalendarId ?? config.bookingCalendarId, 1024);
|
|
1269
1584
|
return {
|
|
1270
1585
|
env,
|
|
1271
1586
|
enabled: typeof value.enabled === "boolean" ? value.enabled : true,
|
|
1272
|
-
connected
|
|
1587
|
+
connected,
|
|
1588
|
+
writable: typeof value.writable === "boolean" ? value.writable : stateValue === "healthy",
|
|
1273
1589
|
provider: providerValue === "google" ? "google" : null,
|
|
1274
1590
|
status: stateValue,
|
|
1275
|
-
...accessValue
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1591
|
+
...accessValue !== void 0 ? { access: "book" } : {},
|
|
1592
|
+
...bookingCalendarId ? { bookingCalendarId } : {},
|
|
1593
|
+
calendars: calendarIds(
|
|
1594
|
+
value.availabilityCalendars ?? config.availabilityCalendars ?? value.configuredCalendars ?? value.calendars ?? config.calendars ?? googleConfig.calendars
|
|
1595
|
+
),
|
|
1279
1596
|
...optionalNullableUrl("bookingPageUrl", bookingPageValue),
|
|
1280
1597
|
grantedScopes: stringList(value.grantedScopes ?? connection.grantedScopes ?? connection.scopes),
|
|
1281
1598
|
...optionalText("attemptId", value.attemptId ?? connection.attemptId, 180),
|
|
1282
|
-
...optionalNumber("lastSuccessfulSyncAt", value.lastSuccessfulSyncAt ?? sync.lastSuccessfulSyncAt),
|
|
1283
|
-
...optionalNumber("lastFullSyncAt", value.lastFullSyncAt ?? sync.lastFullSyncAt),
|
|
1284
|
-
...optionalNumber("watchExpiresAt", value.watchExpiresAt ?? sync.watchExpiresAt ?? watches.nextExpirationAt),
|
|
1285
|
-
...optionalInteger("bookingCount", value.bookingCount ?? sync.bookingCount),
|
|
1286
1599
|
...errorValue || errorCode ? { error: {
|
|
1287
1600
|
...textField(errorValue?.code, 128) ? { code: textField(errorValue?.code, 128) } : {},
|
|
1288
1601
|
...textField(errorValue?.message, 500) ? { message: textField(errorValue?.message, 500) } : {},
|
|
@@ -1297,7 +1610,9 @@ function calendarState(value) {
|
|
|
1297
1610
|
disabled: "not_connected",
|
|
1298
1611
|
disconnected: "disconnected",
|
|
1299
1612
|
connecting: "authorizing",
|
|
1300
|
-
syncing: "
|
|
1613
|
+
syncing: "healthy",
|
|
1614
|
+
needs_sync: "healthy",
|
|
1615
|
+
initial_sync: "healthy",
|
|
1301
1616
|
ready: "healthy",
|
|
1302
1617
|
error: "failed"
|
|
1303
1618
|
};
|
|
@@ -1365,13 +1680,6 @@ function optionalText(key, value, max) {
|
|
|
1365
1680
|
const parsed = textField(value, max);
|
|
1366
1681
|
return parsed ? { [key]: parsed } : {};
|
|
1367
1682
|
}
|
|
1368
|
-
function optionalNumber(key, value) {
|
|
1369
|
-
const parsed = timestamp3(value);
|
|
1370
|
-
return parsed === void 0 ? {} : { [key]: parsed };
|
|
1371
|
-
}
|
|
1372
|
-
function optionalInteger(key, value) {
|
|
1373
|
-
return Number.isSafeInteger(value) && value >= 0 ? { [key]: value } : {};
|
|
1374
|
-
}
|
|
1375
1683
|
function optionalNullableUrl(key, value) {
|
|
1376
1684
|
if (value === null) return { [key]: null };
|
|
1377
1685
|
const parsed = textField(value, 4096);
|
|
@@ -1423,7 +1731,7 @@ async function calendarConnect(options) {
|
|
|
1423
1731
|
const page = calendarBookingPageUrl(cfg, ctx.env);
|
|
1424
1732
|
const applied = page === void 0 ? await readCalendarStatus(ctx) : await applyCalendarSettings(ctx, page);
|
|
1425
1733
|
const connectOptions = connectionOptions(options, out);
|
|
1426
|
-
return await continueConnectedCalendar(ctx, applied, connectOptions
|
|
1734
|
+
return await continueConnectedCalendar(ctx, applied, connectOptions) ?? connectWithContext(ctx, connectOptions);
|
|
1427
1735
|
}
|
|
1428
1736
|
async function applyCalendarBookingPage(ctx, bookingPageUrl, out) {
|
|
1429
1737
|
if (bookingPageUrl === void 0) return null;
|
|
@@ -1431,25 +1739,18 @@ async function applyCalendarBookingPage(ctx, bookingPageUrl, out) {
|
|
|
1431
1739
|
out.log(`${ctx.env}: calendar booking page ${bookingPageUrl ? "set" : "cleared"}`);
|
|
1432
1740
|
return status;
|
|
1433
1741
|
}
|
|
1434
|
-
async function calendarResync(options) {
|
|
1435
|
-
const { ctx, out } = await lifecycleContext(options);
|
|
1436
|
-
productionConsent(ctx.env, options.yes, "resync calendar");
|
|
1437
|
-
const status = await requestCalendarResync(ctx);
|
|
1438
|
-
out.log(`${ctx.env}: calendar resync requested (${status.status})`);
|
|
1439
|
-
return status;
|
|
1440
|
-
}
|
|
1441
1742
|
async function calendarDisconnect(options) {
|
|
1442
1743
|
if (!options.yes) throw new Error("calendar disconnect requires --yes");
|
|
1443
1744
|
const { ctx, out } = await lifecycleContext(options);
|
|
1444
1745
|
const status = await requestCalendarDisconnect(ctx);
|
|
1445
|
-
out.log(`${ctx.env}: calendar disconnected;
|
|
1746
|
+
out.log(`${ctx.env}: calendar disconnected; no calendar data was stored`);
|
|
1446
1747
|
return status;
|
|
1447
1748
|
}
|
|
1448
1749
|
async function ensureCalendarConnected(ctx, options) {
|
|
1449
1750
|
const out = options.stdout ?? console;
|
|
1450
1751
|
const current = await readCalendarStatus(ctx);
|
|
1451
1752
|
const connectOptions = connectionOptions(options, out);
|
|
1452
|
-
return await continueConnectedCalendar(ctx, current, connectOptions
|
|
1753
|
+
return await continueConnectedCalendar(ctx, current, connectOptions) ?? connectWithContext(ctx, connectOptions);
|
|
1453
1754
|
}
|
|
1454
1755
|
async function lifecycleContext(options) {
|
|
1455
1756
|
const cfg = await loadProjectConfig(options.configPath);
|
|
@@ -1485,25 +1786,20 @@ function connectionOptions(options, out) {
|
|
|
1485
1786
|
signal: options.signal
|
|
1486
1787
|
};
|
|
1487
1788
|
}
|
|
1488
|
-
async function continueConnectedCalendar(ctx, current, options
|
|
1489
|
-
if (current.status === "healthy"
|
|
1490
|
-
options.out.log(`${ctx.env}: calendar
|
|
1789
|
+
async function continueConnectedCalendar(ctx, current, options) {
|
|
1790
|
+
if (current.status === "healthy") {
|
|
1791
|
+
options.out.log(`${ctx.env}: calendar already connected (healthy)`);
|
|
1491
1792
|
return current;
|
|
1492
1793
|
}
|
|
1493
|
-
if (current.status === "degraded")
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
options.out.log(`${ctx.env}: calendar configuration needs sync`);
|
|
1497
|
-
const synced = await requestCalendarResync(ctx);
|
|
1498
|
-
options.out.log(`${ctx.env}: calendar ${synced.status.replaceAll("_", " ")}`);
|
|
1499
|
-
return synced;
|
|
1794
|
+
if (current.status === "degraded") {
|
|
1795
|
+
options.out.log(`${ctx.env}: calendar grant predates booking scopes \u2014 Google re-consent required`);
|
|
1796
|
+
return null;
|
|
1500
1797
|
}
|
|
1501
1798
|
if (current.status === "failed" && current.connected) {
|
|
1502
1799
|
const reason = current.error?.message ?? current.error?.code;
|
|
1503
|
-
throw new Error(`calendar connection failed${reason ? `: ${reason}` : ""}; inspect status and
|
|
1800
|
+
throw new Error(`calendar connection failed${reason ? `: ${reason}` : ""}; inspect status and reconnect explicitly`);
|
|
1504
1801
|
}
|
|
1505
|
-
|
|
1506
|
-
if (!waitingForExisting) return null;
|
|
1802
|
+
if (current.status !== "authorizing") return null;
|
|
1507
1803
|
const now = options.now ?? Date.now;
|
|
1508
1804
|
const deadline = now() + pollTimeout(options.pollTimeoutMs);
|
|
1509
1805
|
const wait = options.wait ?? waitForCalendarPoll;
|
|
@@ -1515,17 +1811,16 @@ async function continueConnectedCalendar(ctx, current, options, reuseDegraded) {
|
|
|
1515
1811
|
options.out.log(`${ctx.env}: calendar ${status.status.replaceAll("_", " ")}`);
|
|
1516
1812
|
prior = status.status;
|
|
1517
1813
|
}
|
|
1518
|
-
if (status.status === "healthy"
|
|
1814
|
+
if (status.status === "healthy") return status;
|
|
1519
1815
|
if (status.status === "degraded") return null;
|
|
1520
|
-
if (status.status === "needs_sync") return requestCalendarResync(ctx);
|
|
1521
1816
|
if (status.status === "failed" && status.connected) {
|
|
1522
1817
|
const reason = status.error?.message ?? status.error?.code;
|
|
1523
|
-
throw new Error(`calendar connection failed${reason ? `: ${reason}` : ""}; inspect status and
|
|
1818
|
+
throw new Error(`calendar connection failed${reason ? `: ${reason}` : ""}; inspect status and reconnect explicitly`);
|
|
1524
1819
|
}
|
|
1525
|
-
if (status.status !== "authorizing"
|
|
1820
|
+
if (status.status !== "authorizing") return null;
|
|
1526
1821
|
await wait(Math.min(2e3, Math.max(0, deadline - now())), options.signal);
|
|
1527
1822
|
}
|
|
1528
|
-
throw new Error('Existing Google Calendar authorization
|
|
1823
|
+
throw new Error('Existing Google Calendar authorization timed out; run "odla-ai calendar status" and retry');
|
|
1529
1824
|
}
|
|
1530
1825
|
async function connectWithContext(ctx, options) {
|
|
1531
1826
|
const attempt = await startCalendarConnection(ctx);
|
|
@@ -1547,14 +1842,17 @@ async function connectWithContext(ctx, options) {
|
|
|
1547
1842
|
options.out.log(`${ctx.env}: calendar ${status.status.replaceAll("_", " ")}`);
|
|
1548
1843
|
prior = status.status;
|
|
1549
1844
|
}
|
|
1550
|
-
if (status.status === "healthy"
|
|
1845
|
+
if (status.status === "healthy") return status;
|
|
1846
|
+
if (status.status === "degraded") {
|
|
1847
|
+
throw new Error("calendar connected without booking scopes; re-run connect and grant calendar access");
|
|
1848
|
+
}
|
|
1551
1849
|
if (status.status === "failed" || status.status === "disconnected" || status.status === "not_connected") {
|
|
1552
1850
|
const reason = status.error?.message ?? status.error?.code;
|
|
1553
1851
|
throw new Error(`calendar connection ${status.status}${reason ? `: ${reason}` : ""}`);
|
|
1554
1852
|
}
|
|
1555
1853
|
await wait(Math.min(2e3, Math.max(0, deadline - now())), options.signal);
|
|
1556
1854
|
}
|
|
1557
|
-
throw new Error('Google Calendar consent
|
|
1855
|
+
throw new Error('Google Calendar consent timed out; run "odla-ai calendar status" and retry connect');
|
|
1558
1856
|
}
|
|
1559
1857
|
function trustedCalendarConsentUrl(platform, value) {
|
|
1560
1858
|
const origin = platformAudience(platform);
|
|
@@ -1584,26 +1882,22 @@ function printStatus(status, json, out) {
|
|
|
1584
1882
|
return;
|
|
1585
1883
|
}
|
|
1586
1884
|
out.log(`calendar: ${status.provider ?? "none"}/${status.status} (${status.env})`);
|
|
1587
|
-
out.log(`
|
|
1588
|
-
out.log(`
|
|
1589
|
-
|
|
1590
|
-
out.log(` attendee policy: ${status.attendeePolicy}${status.attendeePolicyLocked ? " (locked)" : ""}`);
|
|
1591
|
-
}
|
|
1885
|
+
out.log(` bookable: ${status.writable ? "yes" : status.connected ? "no \u2014 reconnect to grant booking scopes" : "no \u2014 not connected"}`);
|
|
1886
|
+
if (status.bookingCalendarId) out.log(` booking calendar: ${status.bookingCalendarId}`);
|
|
1887
|
+
out.log(` availability calendars: ${status.calendars.length ? status.calendars.join(", ") : "none"}`);
|
|
1592
1888
|
if (status.bookingPageUrl !== void 0) out.log(` booking page: ${status.bookingPageUrl ?? "not configured"}`);
|
|
1593
|
-
out.log(` last sync: ${status.lastSuccessfulSyncAt === void 0 ? "never" : new Date(status.lastSuccessfulSyncAt).toISOString()}`);
|
|
1594
|
-
if (status.bookingCount !== void 0) out.log(` bookings: ${status.bookingCount}`);
|
|
1595
1889
|
if (status.error?.code || status.error?.message) out.log(` error: ${status.error.code ?? "error"}${status.error.message ? ` \u2014 ${status.error.message}` : ""}`);
|
|
1596
1890
|
}
|
|
1597
1891
|
|
|
1598
1892
|
// src/doctor-checks.ts
|
|
1599
1893
|
var import_node_child_process3 = require("child_process");
|
|
1600
|
-
var
|
|
1601
|
-
var
|
|
1894
|
+
var import_node_fs7 = require("fs");
|
|
1895
|
+
var import_node_path5 = require("path");
|
|
1602
1896
|
|
|
1603
1897
|
// src/wrangler.ts
|
|
1604
1898
|
var import_node_child_process2 = require("child_process");
|
|
1605
|
-
var
|
|
1606
|
-
var
|
|
1899
|
+
var import_node_fs6 = require("fs");
|
|
1900
|
+
var import_node_path4 = require("path");
|
|
1607
1901
|
var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
|
|
1608
1902
|
const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
1609
1903
|
let stdout = "";
|
|
@@ -1617,15 +1911,15 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) =>
|
|
|
1617
1911
|
var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
|
|
1618
1912
|
function findWranglerConfig(rootDir) {
|
|
1619
1913
|
for (const name of WRANGLER_CONFIG_FILES) {
|
|
1620
|
-
const path = (0,
|
|
1621
|
-
if ((0,
|
|
1914
|
+
const path = (0, import_node_path4.join)(rootDir, name);
|
|
1915
|
+
if ((0, import_node_fs6.existsSync)(path)) return path;
|
|
1622
1916
|
}
|
|
1623
1917
|
return null;
|
|
1624
1918
|
}
|
|
1625
1919
|
function readWranglerConfig(path) {
|
|
1626
1920
|
if (path.endsWith(".toml")) return null;
|
|
1627
1921
|
try {
|
|
1628
|
-
return JSON.parse(stripJsonComments((0,
|
|
1922
|
+
return JSON.parse(stripJsonComments((0, import_node_fs6.readFileSync)(path, "utf8")));
|
|
1629
1923
|
} catch {
|
|
1630
1924
|
return null;
|
|
1631
1925
|
}
|
|
@@ -1723,10 +2017,10 @@ function wranglerWarnings(rootDir) {
|
|
|
1723
2017
|
for (const { label, block } of blocks) {
|
|
1724
2018
|
const assets = block.assets;
|
|
1725
2019
|
if (assets?.directory) {
|
|
1726
|
-
const dir = (0,
|
|
1727
|
-
if (dir === (0,
|
|
2020
|
+
const dir = (0, import_node_path5.resolve)(rootDir, assets.directory);
|
|
2021
|
+
if (dir === (0, import_node_path5.resolve)(rootDir)) {
|
|
1728
2022
|
warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
|
|
1729
|
-
} else if ((0,
|
|
2023
|
+
} else if ((0, import_node_fs7.existsSync)((0, import_node_path5.join)(dir, "node_modules"))) {
|
|
1730
2024
|
warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
|
|
1731
2025
|
}
|
|
1732
2026
|
}
|
|
@@ -1761,13 +2055,13 @@ function o11yProjectWarnings(rootDir) {
|
|
|
1761
2055
|
warnings.push("cannot verify o11y Worker instrumentation \u2014 add a parseable wrangler.jsonc/json config");
|
|
1762
2056
|
return warnings;
|
|
1763
2057
|
}
|
|
1764
|
-
const main = typeof config.main === "string" ? (0,
|
|
1765
|
-
if (!main || !(0,
|
|
2058
|
+
const main = typeof config.main === "string" ? (0, import_node_path5.resolve)(rootDir, config.main) : null;
|
|
2059
|
+
if (!main || !(0, import_node_fs7.existsSync)(main)) {
|
|
1766
2060
|
warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
|
|
1767
2061
|
} else {
|
|
1768
2062
|
let source = "";
|
|
1769
2063
|
try {
|
|
1770
|
-
source = (0,
|
|
2064
|
+
source = (0, import_node_fs7.readFileSync)(main, "utf8");
|
|
1771
2065
|
} catch {
|
|
1772
2066
|
}
|
|
1773
2067
|
if (!/\bwithObservability\b/.test(source)) {
|
|
@@ -1791,32 +2085,136 @@ function calendarProjectWarnings(rootDir) {
|
|
|
1791
2085
|
}
|
|
1792
2086
|
function readPackageJson(rootDir) {
|
|
1793
2087
|
try {
|
|
1794
|
-
return JSON.parse((0,
|
|
2088
|
+
return JSON.parse((0, import_node_fs7.readFileSync)((0, import_node_path5.join)(rootDir, "package.json"), "utf8"));
|
|
1795
2089
|
} catch {
|
|
1796
2090
|
return null;
|
|
1797
2091
|
}
|
|
1798
2092
|
}
|
|
1799
2093
|
|
|
2094
|
+
// src/integrations.ts
|
|
2095
|
+
var import_node_util = require("util");
|
|
2096
|
+
async function resolveDatabaseConfig(cfg, options = {}) {
|
|
2097
|
+
const integrations = cfg.integrations ?? [];
|
|
2098
|
+
if (!cfg.services.includes("db")) return { schema: void 0, rules: void 0, integrations };
|
|
2099
|
+
const authoredSchema = await resolveDataExport(cfg, cfg.db?.schema, ["schema", "SCHEMA"]);
|
|
2100
|
+
const schema = mergeSchemas(authoredSchema, integrations);
|
|
2101
|
+
if (options.validateSeeds !== false) assertSeedContracts(integrations, schema);
|
|
2102
|
+
const configuredRules = await resolveDataExport(cfg, cfg.db?.rules, ["rules", "RULES"]);
|
|
2103
|
+
const explicitRules = mergeRules(configuredRules, integrations);
|
|
2104
|
+
const rules = configuredRules === void 0 && schema && cfg.db?.defaultRules !== false ? { ...rulesFromSchema(schema), ...explicitRules ?? {} } : explicitRules;
|
|
2105
|
+
return { schema, rules, integrations };
|
|
2106
|
+
}
|
|
2107
|
+
function integrationWarnings(integrations, schema, rules) {
|
|
2108
|
+
const warnings = [];
|
|
2109
|
+
const entities = new Set(serializedEntities(schema));
|
|
2110
|
+
for (const integration of integrations) {
|
|
2111
|
+
const namespaces = Object.keys(integration.schema?.entities ?? {});
|
|
2112
|
+
for (const ns of namespaces) {
|
|
2113
|
+
if (!entities.has(ns)) warnings.push(`integration "${integration.id}" schema namespace "${ns}" is missing`);
|
|
2114
|
+
const rule = rules?.[ns];
|
|
2115
|
+
if (!rule) {
|
|
2116
|
+
warnings.push(`integration "${integration.id}" has no rules for "${ns}"`);
|
|
2117
|
+
continue;
|
|
2118
|
+
}
|
|
2119
|
+
for (const action of ["view", "create", "update", "delete"]) {
|
|
2120
|
+
if (rule[action] === void 0) warnings.push(`integration "${integration.id}" rule "${ns}.${action}" is missing`);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
for (const seed of integration.seeds ?? []) {
|
|
2124
|
+
if (!entities.has(seed.ns)) {
|
|
2125
|
+
warnings.push(`integration "${integration.id}" seed "${seed.id}" targets unknown namespace "${seed.ns}"`);
|
|
2126
|
+
} else if (!isUniqueAttr(schema, seed.ns, seed.key.attr)) {
|
|
2127
|
+
warnings.push(`integration "${integration.id}" seed "${seed.id}" key "${seed.ns}.${seed.key.attr}" is not declared unique`);
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
return warnings;
|
|
2132
|
+
}
|
|
2133
|
+
function mergeSchemas(base, integrations) {
|
|
2134
|
+
const fragments = integrations.filter((integration) => integration.schema);
|
|
2135
|
+
if (fragments.length === 0) return base;
|
|
2136
|
+
const merged = normalizeSchema(base);
|
|
2137
|
+
for (const integration of fragments) {
|
|
2138
|
+
const fragment = normalizeSchema(integration.schema);
|
|
2139
|
+
mergeMap(merged.entities, fragment.entities, `integration "${integration.id}" schema namespace`);
|
|
2140
|
+
mergeMap(merged.links, fragment.links, `integration "${integration.id}" schema link`);
|
|
2141
|
+
}
|
|
2142
|
+
return merged;
|
|
2143
|
+
}
|
|
2144
|
+
function mergeRules(base, integrations) {
|
|
2145
|
+
const fragments = integrations.filter((integration) => integration.rules);
|
|
2146
|
+
if (fragments.length === 0) return base;
|
|
2147
|
+
const merged = { ...base ?? {} };
|
|
2148
|
+
for (const integration of fragments) {
|
|
2149
|
+
mergeMap(merged, integration.rules ?? {}, `integration "${integration.id}" rule namespace`);
|
|
2150
|
+
}
|
|
2151
|
+
return merged;
|
|
2152
|
+
}
|
|
2153
|
+
function assertSeedContracts(integrations, schema) {
|
|
2154
|
+
const entities = new Set(serializedEntities(schema));
|
|
2155
|
+
for (const integration of integrations) {
|
|
2156
|
+
for (const seed of integration.seeds ?? []) {
|
|
2157
|
+
if (!entities.has(seed.ns)) {
|
|
2158
|
+
throw new Error(`integration "${integration.id}" seed "${seed.id}" targets unknown namespace "${seed.ns}"`);
|
|
2159
|
+
}
|
|
2160
|
+
if (!isUniqueAttr(schema, seed.ns, seed.key.attr)) {
|
|
2161
|
+
throw new Error(`integration "${integration.id}" seed "${seed.id}" key "${seed.ns}.${seed.key.attr}" must be declared unique`);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
function isUniqueAttr(schema, ns, attr) {
|
|
2167
|
+
if (!isRecord6(schema) || !isRecord6(schema.entities)) return false;
|
|
2168
|
+
const entity = schema.entities[ns];
|
|
2169
|
+
if (!isRecord6(entity) || !isRecord6(entity.attrs)) return false;
|
|
2170
|
+
const definition = entity.attrs[attr];
|
|
2171
|
+
return isRecord6(definition) && definition.unique === true;
|
|
2172
|
+
}
|
|
2173
|
+
function normalizeSchema(value) {
|
|
2174
|
+
if (value === void 0 || value === null) return { entities: {}, links: {} };
|
|
2175
|
+
if (!isRecord6(value) || !isRecord6(value.entities)) {
|
|
2176
|
+
throw new Error("db schema must be a serialized schema object with an entities map");
|
|
2177
|
+
}
|
|
2178
|
+
if (value.links !== void 0 && !isRecord6(value.links)) throw new Error("db schema links must be an object");
|
|
2179
|
+
return {
|
|
2180
|
+
entities: { ...value.entities },
|
|
2181
|
+
links: { ...value.links ?? {} }
|
|
2182
|
+
};
|
|
2183
|
+
}
|
|
2184
|
+
function mergeMap(target, fragment, label) {
|
|
2185
|
+
for (const [name, value] of Object.entries(fragment)) {
|
|
2186
|
+
if (Object.hasOwn(target, name) && !(0, import_node_util.isDeepStrictEqual)(target[name], value)) {
|
|
2187
|
+
throw new Error(`${label} "${name}" conflicts with an existing definition`);
|
|
2188
|
+
}
|
|
2189
|
+
target[name] = value;
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
function isRecord6(value) {
|
|
2193
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2194
|
+
}
|
|
2195
|
+
|
|
1800
2196
|
// src/doctor.ts
|
|
1801
2197
|
async function doctor(options) {
|
|
1802
2198
|
const out = options.stdout ?? console;
|
|
1803
2199
|
const cfg = await loadProjectConfig(options.configPath);
|
|
1804
2200
|
const plan = buildPlan(cfg);
|
|
1805
|
-
const
|
|
1806
|
-
const
|
|
1807
|
-
const configuredRules = hasDb ? await resolveDataExport(cfg, cfg.db?.rules, ["rules", "RULES"]) : void 0;
|
|
1808
|
-
const rules = configuredRules ?? (schema && cfg.db?.defaultRules !== false ? rulesFromSchema(schema) : void 0);
|
|
2201
|
+
const database = await resolveDatabaseConfig(cfg, { validateSeeds: false });
|
|
2202
|
+
const { schema, rules } = database;
|
|
1809
2203
|
const entities = serializedEntities(schema);
|
|
1810
2204
|
out.log(`config: ${cfg.configPath}`);
|
|
1811
2205
|
out.log(`app: ${plan.appName} (${plan.appId})`);
|
|
1812
2206
|
out.log(`envs: ${plan.envs.join(", ")}`);
|
|
1813
2207
|
out.log(`svc: ${plan.services.join(", ")}`);
|
|
2208
|
+
out.log(`integrations: ${plan.integrations.length ? plan.integrations.join(", ") : "none"}`);
|
|
1814
2209
|
out.log(`schema: ${schema ? `${entities.length} entities` : "none"}`);
|
|
1815
2210
|
out.log(`rules: ${rules ? `${Object.keys(rules).length} namespaces` : "none"}`);
|
|
1816
2211
|
out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "not configured" : "not enabled"}`);
|
|
1817
2212
|
if (cfg.services.includes("calendar")) {
|
|
1818
|
-
const calendar = cfg.envs.map((env) =>
|
|
1819
|
-
|
|
2213
|
+
const calendar = cfg.envs.map((env) => {
|
|
2214
|
+
const resolved = calendarServiceConfig(cfg, env);
|
|
2215
|
+
return `${env}:${resolved.bookingCalendarId}+${resolved.availabilityCalendars.length}`;
|
|
2216
|
+
}).join(", ");
|
|
2217
|
+
out.log(`calendar: google/booking (${calendar})`);
|
|
1820
2218
|
const pages = cfg.envs.map((env) => `${env}:${calendarBookingPageUrl(cfg, env) ?? "none"}`).join(", ");
|
|
1821
2219
|
out.log(`booking pages: ${pages}`);
|
|
1822
2220
|
} else {
|
|
@@ -1829,6 +2227,7 @@ async function doctor(options) {
|
|
|
1829
2227
|
if (entities.length > 0 && !entities.includes(ns)) warnings.push(`rules include "${ns}" but schema has no matching entity`);
|
|
1830
2228
|
}
|
|
1831
2229
|
}
|
|
2230
|
+
warnings.push(...integrationWarnings(database.integrations, schema, rules));
|
|
1832
2231
|
if (cfg.services.includes("ai") && !cfg.ai?.provider) warnings.push("ai service is enabled but ai.provider is not set");
|
|
1833
2232
|
if (cfg.auth?.clerk) {
|
|
1834
2233
|
for (const [env, value] of Object.entries(cfg.auth.clerk)) {
|
|
@@ -1868,9 +2267,9 @@ async function doctor(options) {
|
|
|
1868
2267
|
}
|
|
1869
2268
|
|
|
1870
2269
|
// src/help.ts
|
|
1871
|
-
var
|
|
2270
|
+
var import_node_fs8 = require("fs");
|
|
1872
2271
|
function cliVersion() {
|
|
1873
|
-
const pkg = JSON.parse((0,
|
|
2272
|
+
const pkg = JSON.parse((0, import_node_fs8.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
|
|
1874
2273
|
return pkg.version ?? "unknown";
|
|
1875
2274
|
}
|
|
1876
2275
|
function printHelp() {
|
|
@@ -1883,8 +2282,10 @@ Usage:
|
|
|
1883
2282
|
odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
|
|
1884
2283
|
odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
|
|
1885
2284
|
odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
|
|
1886
|
-
odla-ai calendar resync [--env dev] [--email <odla-account>] [--yes]
|
|
1887
2285
|
odla-ai calendar disconnect [--env dev] [--email <odla-account>] --yes
|
|
2286
|
+
odla-ai app archive [--config odla.config.mjs] [--email <odla-account>] [--json] --yes
|
|
2287
|
+
odla-ai app restore [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
2288
|
+
odla-ai app export [--env dev] [--fresh] [--out <file>] [--email <odla-account>] [--json]
|
|
1888
2289
|
odla-ai capabilities [--json]
|
|
1889
2290
|
odla-ai admin ai show [--platform https://odla.ai] [--email <odla-account>] [--json]
|
|
1890
2291
|
odla-ai admin ai models [--provider <id>] [--json]
|
|
@@ -1904,7 +2305,7 @@ Usage:
|
|
|
1904
2305
|
odla-ai security report <job-id> [--json]
|
|
1905
2306
|
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
1906
2307
|
odla-ai security run [target] --self --ack-redacted-source
|
|
1907
|
-
odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
2308
|
+
odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
1908
2309
|
odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
|
|
1909
2310
|
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
1910
2311
|
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
@@ -1916,12 +2317,19 @@ Commands:
|
|
|
1916
2317
|
setup Install offline odla runbooks for common coding-agent harnesses.
|
|
1917
2318
|
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
1918
2319
|
doctor Validate and summarize the project config without network calls.
|
|
1919
|
-
calendar Inspect, connect,
|
|
2320
|
+
calendar Inspect, connect, or disconnect the live Google booking connection.
|
|
2321
|
+
app Archive (suspend, data retained), restore, or export the app.
|
|
2322
|
+
Archiving takes every environment's data plane down until restored;
|
|
2323
|
+
permanent deletion has NO CLI \u2014 it requires a signed-in owner in
|
|
2324
|
+
Studio, and no machine or agent credential can purge. "app export"
|
|
2325
|
+
downloads a portable gzipped-JSONL snapshot of one environment's
|
|
2326
|
+
database (--fresh takes a new snapshot first) \u2014 your data is
|
|
2327
|
+
always yours to take.
|
|
1920
2328
|
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
1921
2329
|
admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
|
|
1922
2330
|
security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
|
|
1923
|
-
provision Register services,
|
|
1924
|
-
smoke Verify
|
|
2331
|
+
provision Register services, compose integrations, persist credentials, optionally push secrets.
|
|
2332
|
+
smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
|
|
1925
2333
|
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
1926
2334
|
copilot, gemini, or agents (repeatable or comma-separated).
|
|
1927
2335
|
secrets Push configured db/o11y secrets into the Worker via wrangler
|
|
@@ -1938,6 +2346,12 @@ Safety:
|
|
|
1938
2346
|
Provision opens the approval page in your browser automatically whenever the
|
|
1939
2347
|
machine can show one, including agent-driven runs; only CI, SSH, and
|
|
1940
2348
|
display-less hosts skip it. Use --open to force or --no-open to suppress.
|
|
2349
|
+
Browser launch is best-effort: the printed approval URL is authoritative and
|
|
2350
|
+
agents must relay it to the human verbatim. A started handshake is persisted
|
|
2351
|
+
under .odla/, so a command killed mid-wait loses nothing \u2014 rerunning resumes
|
|
2352
|
+
the same code. Outside an interactive terminal the wait is capped (90s by
|
|
2353
|
+
default, --wait <seconds> to change); a still-pending handshake then exits
|
|
2354
|
+
with code 75: relay the URL, wait for approval, and re-run to collect.
|
|
1941
2355
|
A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
|
|
1942
2356
|
The email is a non-secret identity hint: never provide a password or session
|
|
1943
2357
|
token. The matching account must already exist, be signed in, explicitly
|
|
@@ -1974,13 +2388,13 @@ function harnessOption(value, flag) {
|
|
|
1974
2388
|
}
|
|
1975
2389
|
|
|
1976
2390
|
// src/init.ts
|
|
1977
|
-
var
|
|
1978
|
-
var
|
|
2391
|
+
var import_node_fs9 = require("fs");
|
|
2392
|
+
var import_node_path6 = require("path");
|
|
1979
2393
|
function initProject(options) {
|
|
1980
2394
|
const out = options.stdout ?? console;
|
|
1981
|
-
const rootDir = (0,
|
|
1982
|
-
const configPath = (0,
|
|
1983
|
-
if ((0,
|
|
2395
|
+
const rootDir = (0, import_node_path6.resolve)(options.rootDir ?? process.cwd());
|
|
2396
|
+
const configPath = (0, import_node_path6.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
|
|
2397
|
+
if ((0, import_node_fs9.existsSync)(configPath) && !options.force) {
|
|
1984
2398
|
throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
|
|
1985
2399
|
}
|
|
1986
2400
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
|
|
@@ -1990,29 +2404,29 @@ function initProject(options) {
|
|
|
1990
2404
|
const services = options.services?.length ? options.services : ["db", "ai"];
|
|
1991
2405
|
if (services.includes("calendar") && !services.includes("db")) throw new Error("--services calendar requires db");
|
|
1992
2406
|
const aiProvider = options.aiProvider ?? "anthropic";
|
|
1993
|
-
(0,
|
|
1994
|
-
(0,
|
|
1995
|
-
(0,
|
|
1996
|
-
(0,
|
|
1997
|
-
writeIfMissing((0,
|
|
1998
|
-
writeIfMissing((0,
|
|
2407
|
+
(0, import_node_fs9.mkdirSync)((0, import_node_path6.dirname)(configPath), { recursive: true });
|
|
2408
|
+
(0, import_node_fs9.mkdirSync)((0, import_node_path6.resolve)(rootDir, "src/odla"), { recursive: true });
|
|
2409
|
+
(0, import_node_fs9.mkdirSync)((0, import_node_path6.resolve)(rootDir, ".odla"), { recursive: true });
|
|
2410
|
+
(0, import_node_fs9.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
|
|
2411
|
+
writeIfMissing((0, import_node_path6.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
|
|
2412
|
+
writeIfMissing((0, import_node_path6.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
|
|
1999
2413
|
ensureGitignore(rootDir);
|
|
2000
2414
|
out.log(`created ${relativeDisplay(configPath, rootDir)}`);
|
|
2001
2415
|
out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
|
|
2002
2416
|
out.log("updated .gitignore for local odla credentials");
|
|
2003
2417
|
}
|
|
2004
2418
|
function writeIfMissing(path, text) {
|
|
2005
|
-
if ((0,
|
|
2006
|
-
(0,
|
|
2419
|
+
if ((0, import_node_fs9.existsSync)(path)) return;
|
|
2420
|
+
(0, import_node_fs9.writeFileSync)(path, text);
|
|
2007
2421
|
}
|
|
2008
2422
|
function configTemplate(input) {
|
|
2009
2423
|
const calendar = input.services.includes("calendar") ? ` calendar: {
|
|
2010
2424
|
google: {
|
|
2011
|
-
|
|
2425
|
+
// Calendars consulted for availability; bookings land on bookingCalendar
|
|
2426
|
+
// (defaults to the first availability calendar). Google consent happens
|
|
2427
|
+
// in the browser during provision; bookings write live through odla.ai.
|
|
2428
|
+
availabilityCalendars: ${JSON.stringify(Object.fromEntries(input.envs.map((env) => [env, ["primary"]])))},
|
|
2012
2429
|
bookingPageUrl: ${JSON.stringify(Object.fromEntries(input.envs.map((env) => [env, null])))},
|
|
2013
|
-
match: { organizerSelf: true, requireAttendees: true },
|
|
2014
|
-
attendeePolicy: "full",
|
|
2015
|
-
// Read-only in this release. Google consent happens in Studio during provision.
|
|
2016
2430
|
},
|
|
2017
2431
|
},
|
|
2018
2432
|
` : "";
|
|
@@ -2025,6 +2439,9 @@ function configTemplate(input) {
|
|
|
2025
2439
|
},
|
|
2026
2440
|
envs: ${JSON.stringify(input.envs)},
|
|
2027
2441
|
services: ${JSON.stringify(input.services)},
|
|
2442
|
+
// App capabilities are npm modules, not hosted services. Import their
|
|
2443
|
+
// data-only descriptor and add it here (for example createCrmIntegration()).
|
|
2444
|
+
integrations: [],
|
|
2028
2445
|
db: {
|
|
2029
2446
|
schema: "./src/odla/schema.mjs",
|
|
2030
2447
|
rules: "./src/odla/rules.mjs",
|
|
@@ -2102,8 +2519,60 @@ function relativeDisplay(path, rootDir) {
|
|
|
2102
2519
|
|
|
2103
2520
|
// src/provision.ts
|
|
2104
2521
|
var import_apps2 = require("@odla-ai/apps");
|
|
2105
|
-
var
|
|
2106
|
-
var
|
|
2522
|
+
var import_ai2 = require("@odla-ai/ai");
|
|
2523
|
+
var import_node_process7 = __toESM(require("process"), 1);
|
|
2524
|
+
|
|
2525
|
+
// src/integration-provision.ts
|
|
2526
|
+
var import_db3 = require("@odla-ai/db");
|
|
2527
|
+
async function provisionIntegrationSeeds(doFetch, endpoint, tenantId, dbKey, integrations, env, out) {
|
|
2528
|
+
const base = `${endpoint}/app/${encodeURIComponent(tenantId)}`;
|
|
2529
|
+
for (const integration of integrations) {
|
|
2530
|
+
for (const seed of integration.seeds ?? []) {
|
|
2531
|
+
const payload = await postJson(doFetch, `${base}/query`, dbKey, {
|
|
2532
|
+
query: { [seed.ns]: { $: { where: { [seed.key.attr]: seed.key.value }, limit: 1 } } }
|
|
2533
|
+
});
|
|
2534
|
+
const rows = isRecord7(payload) && isRecord7(payload.result) ? payload.result[seed.ns] : void 0;
|
|
2535
|
+
if (!Array.isArray(rows)) {
|
|
2536
|
+
throw new Error(`${env}: integration ${integration.id} seed ${seed.id} query returned an invalid response`);
|
|
2537
|
+
}
|
|
2538
|
+
if (rows.length > 0) {
|
|
2539
|
+
out.log(`${env}: integration ${integration.id} seed ${seed.id} already exists`);
|
|
2540
|
+
continue;
|
|
2541
|
+
}
|
|
2542
|
+
await postJson(doFetch, `${base}/transact`, dbKey, {
|
|
2543
|
+
mutationId: `integration:${integration.id}:seed:${seed.id}`,
|
|
2544
|
+
ops: [{
|
|
2545
|
+
t: "update",
|
|
2546
|
+
ns: seed.ns,
|
|
2547
|
+
// A fresh id plus the declared-unique natural key is create-only: a
|
|
2548
|
+
// concurrent creator gets a unique violation instead of being overwritten.
|
|
2549
|
+
id: (0, import_db3.uuidv7)(),
|
|
2550
|
+
attrs: { ...seed.attrs, [seed.key.attr]: seed.key.value }
|
|
2551
|
+
}]
|
|
2552
|
+
});
|
|
2553
|
+
out.log(`${env}: integration ${integration.id} seed ${seed.id} created`);
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
async function postJson(doFetch, url, bearer, body) {
|
|
2558
|
+
const res = await doFetch(url, {
|
|
2559
|
+
method: "POST",
|
|
2560
|
+
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
2561
|
+
body: JSON.stringify(body)
|
|
2562
|
+
});
|
|
2563
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await responseText(res)}`);
|
|
2564
|
+
return res.json().catch(() => ({}));
|
|
2565
|
+
}
|
|
2566
|
+
function isRecord7(value) {
|
|
2567
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2568
|
+
}
|
|
2569
|
+
async function responseText(res) {
|
|
2570
|
+
try {
|
|
2571
|
+
return redactSecrets((await res.text()).slice(0, 500));
|
|
2572
|
+
} catch {
|
|
2573
|
+
return "";
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2107
2576
|
|
|
2108
2577
|
// src/provision-credentials.ts
|
|
2109
2578
|
var import_apps = require("@odla-ai/apps");
|
|
@@ -2164,14 +2633,14 @@ async function mintDbKey(opts, tenantId) {
|
|
|
2164
2633
|
appId: tenantId
|
|
2165
2634
|
})
|
|
2166
2635
|
});
|
|
2167
|
-
if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await
|
|
2636
|
+
if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText3(created)}`);
|
|
2168
2637
|
res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/keys`, {
|
|
2169
2638
|
method: "POST",
|
|
2170
2639
|
headers,
|
|
2171
2640
|
body: "{}"
|
|
2172
2641
|
});
|
|
2173
2642
|
}
|
|
2174
|
-
if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await
|
|
2643
|
+
if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText3(res)}`);
|
|
2175
2644
|
const body = await res.json();
|
|
2176
2645
|
if (!body.key) throw new Error(`db key mint (${tenantId}) returned no key`);
|
|
2177
2646
|
return body.key;
|
|
@@ -2187,12 +2656,12 @@ async function issueO11yToken(opts) {
|
|
|
2187
2656
|
`o11y token already exists for env "${opts.env}", but its shown-once value is not in the local credentials file; run "odla-ai provision --rotate-o11y-token --push-secrets" to replace it explicitly`
|
|
2188
2657
|
);
|
|
2189
2658
|
}
|
|
2190
|
-
if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await
|
|
2659
|
+
if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText3(res)}`);
|
|
2191
2660
|
const body = await res.json();
|
|
2192
2661
|
if (!body.token) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) returned no token`);
|
|
2193
2662
|
return body.token;
|
|
2194
2663
|
}
|
|
2195
|
-
async function
|
|
2664
|
+
async function safeText3(res) {
|
|
2196
2665
|
try {
|
|
2197
2666
|
return redactSecrets((await res.text()).slice(0, 500));
|
|
2198
2667
|
} catch {
|
|
@@ -2200,6 +2669,18 @@ async function safeText2(res) {
|
|
|
2200
2669
|
}
|
|
2201
2670
|
}
|
|
2202
2671
|
|
|
2672
|
+
// src/provision-helpers.ts
|
|
2673
|
+
var import_ai = require("@odla-ai/ai");
|
|
2674
|
+
function serviceRank(service) {
|
|
2675
|
+
if (service === "db") return 0;
|
|
2676
|
+
if (service === "calendar") return 2;
|
|
2677
|
+
return 1;
|
|
2678
|
+
}
|
|
2679
|
+
function defaultSecretName(provider) {
|
|
2680
|
+
const names = import_ai.DEFAULT_SECRET_NAMES;
|
|
2681
|
+
return names[provider] ?? `${provider}_api_key`;
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2203
2684
|
// src/secrets.ts
|
|
2204
2685
|
var PROD_ENV_NAMES = /* @__PURE__ */ new Set(["prod", "production"]);
|
|
2205
2686
|
async function secretsPush(options) {
|
|
@@ -2291,24 +2772,28 @@ async function provision(options) {
|
|
|
2291
2772
|
out.log(` db: ${plan.dbEndpoint}`);
|
|
2292
2773
|
out.log(` envs: ${plan.envs.join(", ")}`);
|
|
2293
2774
|
out.log(` services: ${plan.services.join(", ")}`);
|
|
2775
|
+
out.log(` integrations: ${plan.integrations.length ? plan.integrations.join(", ") : "none"}`);
|
|
2294
2776
|
const productionEnvs = plan.envs.filter((env) => env === "prod" || env === "production");
|
|
2295
2777
|
if (!options.dryRun && productionEnvs.length > 0 && !options.yes) {
|
|
2296
2778
|
throw new Error(
|
|
2297
2779
|
`refusing to provision production env ${productionEnvs.join(", ")} without --yes; run --dry-run first`
|
|
2298
2780
|
);
|
|
2299
2781
|
}
|
|
2300
|
-
const
|
|
2301
|
-
const
|
|
2302
|
-
const rules = configuredRules ?? (schema && cfg.db?.defaultRules !== false ? rulesFromSchema(schema) : void 0);
|
|
2782
|
+
const database = await resolveDatabaseConfig(cfg);
|
|
2783
|
+
const { schema, rules } = database;
|
|
2303
2784
|
if (options.dryRun) {
|
|
2304
2785
|
out.log("dry run: no network calls or file writes");
|
|
2305
2786
|
out.log(` schema: ${schema ? "yes" : "no"}`);
|
|
2306
2787
|
out.log(` rules: ${rules ? Object.keys(rules).length : 0} namespaces`);
|
|
2788
|
+
for (const integration of database.integrations) {
|
|
2789
|
+
const namespaces = Object.keys(integration.schema?.entities ?? {}).length;
|
|
2790
|
+
out.log(` integration.${integration.id}: ${namespaces} namespaces, ${integration.seeds?.length ?? 0} seeds, ${integration.probes?.length ?? 0} smoke probes`);
|
|
2791
|
+
}
|
|
2307
2792
|
out.log(` ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "not configured" : "not enabled"}`);
|
|
2308
2793
|
if (cfg.services.includes("calendar")) {
|
|
2309
2794
|
for (const env of cfg.envs) {
|
|
2310
2795
|
const calendar = calendarServiceConfig(cfg, env);
|
|
2311
|
-
out.log(` calendar.${env}: google/
|
|
2796
|
+
out.log(` calendar.${env}: google/book \u2192 ${calendar.bookingCalendarId}; availability ${calendar.availabilityCalendars.join(", ")} (${GOOGLE_CALENDAR_EVENTS_SCOPE})`);
|
|
2312
2797
|
const bookingPage = calendarBookingPageUrl(cfg, env);
|
|
2313
2798
|
out.log(` calendar.${env}.bookingPageUrl: ${bookingPage === void 0 ? "unchanged" : bookingPage ?? "clear"}`);
|
|
2314
2799
|
}
|
|
@@ -2331,7 +2816,7 @@ async function provision(options) {
|
|
|
2331
2816
|
}
|
|
2332
2817
|
const devVarsTarget = resolveWriteDevVarsTarget(cfg, options.writeDevVars);
|
|
2333
2818
|
if (cfg.local.gitignore) {
|
|
2334
|
-
ensureGitignore(cfg.rootDir, [cfg.local.tokenFile, cfg.local.credentialsFile, ...devVarsTarget ? [devVarsTarget] : []]);
|
|
2819
|
+
ensureGitignore(cfg.rootDir, [cfg.local.tokenFile, handshakeFile(cfg), cfg.local.credentialsFile, ...devVarsTarget ? [devVarsTarget] : []]);
|
|
2335
2820
|
}
|
|
2336
2821
|
if (options.pushSecrets) {
|
|
2337
2822
|
await preflightSecretsPush({ configPath: cfg.configPath, runner: options.secretRunner });
|
|
@@ -2413,18 +2898,21 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
2413
2898
|
}
|
|
2414
2899
|
}
|
|
2415
2900
|
if (schema && dbKey) {
|
|
2416
|
-
await
|
|
2901
|
+
await postJson2(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
|
|
2417
2902
|
out.log(`${env}: schema pushed`);
|
|
2418
2903
|
}
|
|
2419
2904
|
if (rules) {
|
|
2420
|
-
await
|
|
2905
|
+
await postJson2(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/admin/rules`, token, rules);
|
|
2421
2906
|
out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
|
|
2422
2907
|
}
|
|
2908
|
+
if (dbKey) {
|
|
2909
|
+
await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
|
|
2910
|
+
}
|
|
2423
2911
|
if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
|
|
2424
|
-
const key =
|
|
2912
|
+
const key = import_node_process7.default.env[cfg.ai.keyEnv];
|
|
2425
2913
|
if (key) {
|
|
2426
2914
|
const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
|
|
2427
|
-
await (0,
|
|
2915
|
+
await (0, import_ai2.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
|
|
2428
2916
|
out.log(`${env}: ${cfg.ai.provider} key stored in vault (${secretName})`);
|
|
2429
2917
|
} else {
|
|
2430
2918
|
out.log(`${env}: ${cfg.ai.keyEnv} not set; skipped provider key storage`);
|
|
@@ -2460,11 +2948,6 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
2460
2948
|
}
|
|
2461
2949
|
}
|
|
2462
2950
|
}
|
|
2463
|
-
function serviceRank(service) {
|
|
2464
|
-
if (service === "db") return 0;
|
|
2465
|
-
if (service === "calendar") return 2;
|
|
2466
|
-
return 1;
|
|
2467
|
-
}
|
|
2468
2951
|
async function readRegistryApp(cfg, token, doFetch) {
|
|
2469
2952
|
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}`, {
|
|
2470
2953
|
headers: { authorization: `Bearer ${token}` }
|
|
@@ -2474,13 +2957,13 @@ async function readRegistryApp(cfg, token, doFetch) {
|
|
|
2474
2957
|
const json = await res.json();
|
|
2475
2958
|
return json.app ?? null;
|
|
2476
2959
|
}
|
|
2477
|
-
async function
|
|
2960
|
+
async function postJson2(doFetch, url, bearer, body) {
|
|
2478
2961
|
const res = await doFetch(url, {
|
|
2479
2962
|
method: "POST",
|
|
2480
2963
|
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
2481
2964
|
body: JSON.stringify(body)
|
|
2482
2965
|
});
|
|
2483
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await
|
|
2966
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText4(res)}`);
|
|
2484
2967
|
}
|
|
2485
2968
|
function normalizeClerkConfig(value) {
|
|
2486
2969
|
if (!value) return null;
|
|
@@ -2493,11 +2976,7 @@ function normalizeClerkConfig(value) {
|
|
|
2493
2976
|
const publishableKey = envValue(cfg.publishableKey);
|
|
2494
2977
|
return publishableKey ? { publishableKey, ...cfg.audience ? { audience: cfg.audience } : {}, ...cfg.mode ? { mode: cfg.mode } : {} } : null;
|
|
2495
2978
|
}
|
|
2496
|
-
function
|
|
2497
|
-
const names = import_ai.DEFAULT_SECRET_NAMES;
|
|
2498
|
-
return names[provider] ?? `${provider}_api_key`;
|
|
2499
|
-
}
|
|
2500
|
-
async function safeText3(res) {
|
|
2979
|
+
async function safeText4(res) {
|
|
2501
2980
|
try {
|
|
2502
2981
|
return redactSecrets((await res.text()).slice(0, 500));
|
|
2503
2982
|
} catch {
|
|
@@ -2506,7 +2985,7 @@ async function safeText3(res) {
|
|
|
2506
2985
|
}
|
|
2507
2986
|
|
|
2508
2987
|
// src/secrets-set.ts
|
|
2509
|
-
var
|
|
2988
|
+
var import_ai3 = require("@odla-ai/ai");
|
|
2510
2989
|
var import_apps3 = require("@odla-ai/apps");
|
|
2511
2990
|
var PROD_ENV_NAMES2 = /* @__PURE__ */ new Set(["prod", "production"]);
|
|
2512
2991
|
async function secretsSet(options) {
|
|
@@ -2518,7 +2997,7 @@ async function secretsSet(options) {
|
|
|
2518
2997
|
const { cfg, tenantId, value, doFetch, out } = await resolveVaultWrite(options);
|
|
2519
2998
|
const token = await getDeveloperToken(cfg, options, doFetch, out);
|
|
2520
2999
|
try {
|
|
2521
|
-
await (0,
|
|
3000
|
+
await (0, import_ai3.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, name, value);
|
|
2522
3001
|
} catch (err) {
|
|
2523
3002
|
throw new Error(scrubValue(err instanceof Error ? err.message : String(err), value));
|
|
2524
3003
|
}
|
|
@@ -2563,7 +3042,7 @@ async function resolveVaultWrite(options) {
|
|
|
2563
3042
|
}
|
|
2564
3043
|
|
|
2565
3044
|
// src/security-command-context.ts
|
|
2566
|
-
var
|
|
3045
|
+
var import_promises2 = require("readline/promises");
|
|
2567
3046
|
async function hostedSecurityContext(parsed, dependencies) {
|
|
2568
3047
|
const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
2569
3048
|
const cfg = await loadProjectConfig(configPath);
|
|
@@ -2589,7 +3068,7 @@ async function hostedSecurityContext(parsed, dependencies) {
|
|
|
2589
3068
|
async function interactiveConfirmation(message, dependencies) {
|
|
2590
3069
|
if (dependencies.confirm) return dependencies.confirm(message);
|
|
2591
3070
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
2592
|
-
const prompt = (0,
|
|
3071
|
+
const prompt = (0, import_promises2.createInterface)({ input: process.stdin, output: process.stdout });
|
|
2593
3072
|
try {
|
|
2594
3073
|
const answer = await prompt.question(`${message} [y/N] `);
|
|
2595
3074
|
return /^y(?:es)?$/i.test(answer.trim());
|
|
@@ -2719,7 +3198,7 @@ function hostedSeverity(value, flag) {
|
|
|
2719
3198
|
var import_security2 = require("@odla-ai/security");
|
|
2720
3199
|
|
|
2721
3200
|
// src/security.ts
|
|
2722
|
-
var
|
|
3201
|
+
var import_node_path7 = require("path");
|
|
2723
3202
|
var import_security = require("@odla-ai/security");
|
|
2724
3203
|
var import_node = require("@odla-ai/security/node");
|
|
2725
3204
|
async function runHostedSecurity(options) {
|
|
@@ -2731,9 +3210,9 @@ async function runHostedSecurity(options) {
|
|
|
2731
3210
|
const appId = selfAudit ? "odla-ai" : cfg.app.id;
|
|
2732
3211
|
const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
|
|
2733
3212
|
const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
|
|
2734
|
-
const target = (0,
|
|
2735
|
-
const output = (0,
|
|
2736
|
-
const outputRelative = (0,
|
|
3213
|
+
const target = (0, import_node_path7.resolve)(options.target ?? cfg?.rootDir ?? ".");
|
|
3214
|
+
const output = (0, import_node_path7.resolve)(options.out ?? (0, import_node_path7.resolve)(target, ".odla/security/hosted"));
|
|
3215
|
+
const outputRelative = (0, import_node_path7.relative)(target, output).split(import_node_path7.sep).join("/");
|
|
2737
3216
|
if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
|
|
2738
3217
|
const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
|
|
2739
3218
|
const tokenRequest = {
|
|
@@ -2745,7 +3224,7 @@ async function runHostedSecurity(options) {
|
|
|
2745
3224
|
};
|
|
2746
3225
|
const token = await injectedToken(options, tokenRequest);
|
|
2747
3226
|
const snapshot = await (0, import_node.snapshotDirectory)(target, {
|
|
2748
|
-
exclude: !outputRelative.startsWith("../") && !(0,
|
|
3227
|
+
exclude: !outputRelative.startsWith("../") && !(0, import_node_path7.isAbsolute)(outputRelative) ? [outputRelative] : []
|
|
2749
3228
|
});
|
|
2750
3229
|
const hosted = await (0, import_security.createPlatformSecurityReasoners)({
|
|
2751
3230
|
platform,
|
|
@@ -2763,7 +3242,7 @@ async function runHostedSecurity(options) {
|
|
|
2763
3242
|
});
|
|
2764
3243
|
const harness = (0, import_security.createSecurityHarness)({
|
|
2765
3244
|
profile,
|
|
2766
|
-
store: new import_node.FileRunStore((0,
|
|
3245
|
+
store: new import_node.FileRunStore((0, import_node_path7.resolve)(output, "state")),
|
|
2767
3246
|
discoveryReasoner: hosted.discoveryReasoner,
|
|
2768
3247
|
validationReasoner: hosted.validationReasoner,
|
|
2769
3248
|
policy: {
|
|
@@ -2787,7 +3266,7 @@ async function runHostedSecurity(options) {
|
|
|
2787
3266
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
2788
3267
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
2789
3268
|
if (!env || !declared.includes(env)) {
|
|
2790
|
-
const shown = (0,
|
|
3269
|
+
const shown = (0, import_node_path7.relative)(rootDir, configPath) || configPath;
|
|
2791
3270
|
throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
|
|
2792
3271
|
}
|
|
2793
3272
|
return env;
|
|
@@ -2816,7 +3295,7 @@ function printSummary(out, appId, env, run, report, output) {
|
|
|
2816
3295
|
out.log(` coverage: ${report.coverageStatus} ${complete}/${report.coverage.length} blocked=${report.metrics.blockedCells} shallow=${report.metrics.shallowCells} unscheduled=${report.metrics.unscheduledCells} budget_exhausted=${report.metrics.budgetExhaustedCells}`);
|
|
2817
3296
|
if (report.callBudget) out.log(` calls: discovery=${formatBudget(report.callBudget.discovery)} validation=${formatBudget(report.callBudget.validation)}`);
|
|
2818
3297
|
out.log(` findings: confirmed=${report.metrics.confirmed} needs_reproduction=${report.metrics.needsReproduction} candidates=${report.metrics.candidates}`);
|
|
2819
|
-
out.log(` report: ${(0,
|
|
3298
|
+
out.log(` report: ${(0, import_node_path7.resolve)(output, "REPORT.md")}`);
|
|
2820
3299
|
}
|
|
2821
3300
|
function formatBudget(usage) {
|
|
2822
3301
|
return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
|
|
@@ -2824,7 +3303,7 @@ function formatBudget(usage) {
|
|
|
2824
3303
|
|
|
2825
3304
|
// src/security-hosted-github.ts
|
|
2826
3305
|
var import_node_child_process4 = require("child_process");
|
|
2827
|
-
var
|
|
3306
|
+
var import_node_util2 = require("util");
|
|
2828
3307
|
|
|
2829
3308
|
// src/security-hosted-request.ts
|
|
2830
3309
|
async function requestHostedSecurityJson(options, path, init, action) {
|
|
@@ -3028,7 +3507,7 @@ async function inferGitHubRepository(cwd = process.cwd(), readOrigin = defaultRe
|
|
|
3028
3507
|
return repositoryFromGitRemote(remote);
|
|
3029
3508
|
}
|
|
3030
3509
|
async function defaultReadOrigin(cwd) {
|
|
3031
|
-
const result = await (0,
|
|
3510
|
+
const result = await (0, import_node_util2.promisify)(import_node_child_process4.execFile)(
|
|
3032
3511
|
"git",
|
|
3033
3512
|
["remote", "get-url", "origin"],
|
|
3034
3513
|
{ cwd, encoding: "utf8" }
|
|
@@ -3479,9 +3958,9 @@ async function securityStatus(parsed, dependencies) {
|
|
|
3479
3958
|
}
|
|
3480
3959
|
|
|
3481
3960
|
// src/skill.ts
|
|
3482
|
-
var
|
|
3961
|
+
var import_node_fs10 = require("fs");
|
|
3483
3962
|
var import_node_os = require("os");
|
|
3484
|
-
var
|
|
3963
|
+
var import_node_path8 = require("path");
|
|
3485
3964
|
var import_node_url2 = require("url");
|
|
3486
3965
|
|
|
3487
3966
|
// src/skill-adapters.ts
|
|
@@ -3542,8 +4021,8 @@ function installSkill(options = {}) {
|
|
|
3542
4021
|
const files = listFiles(sourceDir);
|
|
3543
4022
|
if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
|
|
3544
4023
|
const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
|
|
3545
|
-
const root = (0,
|
|
3546
|
-
const home = (0,
|
|
4024
|
+
const root = (0, import_node_path8.resolve)(options.dir ?? process.cwd());
|
|
4025
|
+
const home = (0, import_node_path8.resolve)(options.homeDir ?? (0, import_node_os.homedir)());
|
|
3547
4026
|
const plans = /* @__PURE__ */ new Map();
|
|
3548
4027
|
const targets = /* @__PURE__ */ new Map();
|
|
3549
4028
|
const rememberTarget = (harness, target) => {
|
|
@@ -3557,48 +4036,48 @@ function installSkill(options = {}) {
|
|
|
3557
4036
|
plans.set(target, { target, content, boundary, managedMerge });
|
|
3558
4037
|
};
|
|
3559
4038
|
const planSkillTree = (targetDir2, boundary = root) => {
|
|
3560
|
-
for (const rel of files) plan((0,
|
|
4039
|
+
for (const rel of files) plan((0, import_node_path8.join)(targetDir2, rel), (0, import_node_fs10.readFileSync)((0, import_node_path8.join)(sourceDir, rel), "utf8"), false, boundary);
|
|
3561
4040
|
};
|
|
3562
4041
|
let targetDir;
|
|
3563
4042
|
if (options.global) {
|
|
3564
|
-
const claudeRoot = (0,
|
|
3565
|
-
const codexRoot = (0,
|
|
4043
|
+
const claudeRoot = (0, import_node_path8.join)(home, ".claude", "skills");
|
|
4044
|
+
const codexRoot = (0, import_node_path8.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path8.join)(home, ".codex"), "skills");
|
|
3566
4045
|
targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
|
|
3567
4046
|
for (const harness of harnesses) {
|
|
3568
4047
|
const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
|
|
3569
|
-
planSkillTree(skillRoot, harness === "claude" ? home : (0,
|
|
4048
|
+
planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path8.dirname)((0, import_node_path8.dirname)(codexRoot)));
|
|
3570
4049
|
rememberTarget(harness, skillRoot);
|
|
3571
4050
|
}
|
|
3572
4051
|
} else {
|
|
3573
|
-
const sharedRoot = (0,
|
|
4052
|
+
const sharedRoot = (0, import_node_path8.join)(root, ".agents", "skills");
|
|
3574
4053
|
planSkillTree(sharedRoot);
|
|
3575
|
-
const claudeRoot = (0,
|
|
4054
|
+
const claudeRoot = (0, import_node_path8.join)(root, ".claude", "skills");
|
|
3576
4055
|
targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
|
|
3577
4056
|
for (const harness of harnesses) rememberTarget(harness, sharedRoot);
|
|
3578
4057
|
if (harnesses.includes("claude")) {
|
|
3579
4058
|
for (const skill of skillNames(files)) {
|
|
3580
|
-
const canonical = (0,
|
|
3581
|
-
plan((0,
|
|
4059
|
+
const canonical = (0, import_node_fs10.readFileSync)((0, import_node_path8.join)(sourceDir, skill, "SKILL.md"), "utf8");
|
|
4060
|
+
plan((0, import_node_path8.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
|
|
3582
4061
|
}
|
|
3583
4062
|
rememberTarget("claude", claudeRoot);
|
|
3584
4063
|
}
|
|
3585
4064
|
if (harnesses.includes("cursor")) {
|
|
3586
|
-
const cursorRule = (0,
|
|
4065
|
+
const cursorRule = (0, import_node_path8.join)(root, ".cursor", "rules", "odla.mdc");
|
|
3587
4066
|
plan(cursorRule, CURSOR_RULE);
|
|
3588
4067
|
rememberTarget("cursor", cursorRule);
|
|
3589
4068
|
}
|
|
3590
4069
|
if (harnesses.includes("agents")) {
|
|
3591
|
-
const agentsFile = (0,
|
|
4070
|
+
const agentsFile = (0, import_node_path8.join)(root, "AGENTS.md");
|
|
3592
4071
|
plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
3593
4072
|
rememberTarget("agents", agentsFile);
|
|
3594
4073
|
}
|
|
3595
4074
|
if (harnesses.includes("copilot")) {
|
|
3596
|
-
const copilotFile = (0,
|
|
4075
|
+
const copilotFile = (0, import_node_path8.join)(root, ".github", "copilot-instructions.md");
|
|
3597
4076
|
plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
3598
4077
|
rememberTarget("copilot", copilotFile);
|
|
3599
4078
|
}
|
|
3600
4079
|
if (harnesses.includes("gemini")) {
|
|
3601
|
-
const geminiFile = (0,
|
|
4080
|
+
const geminiFile = (0, import_node_path8.join)(root, "GEMINI.md");
|
|
3602
4081
|
plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
3603
4082
|
rememberTarget("gemini", geminiFile);
|
|
3604
4083
|
}
|
|
@@ -3612,11 +4091,11 @@ function installSkill(options = {}) {
|
|
|
3612
4091
|
conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
|
|
3613
4092
|
continue;
|
|
3614
4093
|
}
|
|
3615
|
-
if (!(0,
|
|
4094
|
+
if (!(0, import_node_fs10.existsSync)(file.target)) {
|
|
3616
4095
|
writtenPaths.add(file.target);
|
|
3617
4096
|
continue;
|
|
3618
4097
|
}
|
|
3619
|
-
const current = (0,
|
|
4098
|
+
const current = (0, import_node_fs10.readFileSync)(file.target, "utf8");
|
|
3620
4099
|
if (current === file.content) {
|
|
3621
4100
|
unchangedPaths.add(file.target);
|
|
3622
4101
|
} else if (file.managedMerge || options.force) {
|
|
@@ -3633,9 +4112,9 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
3633
4112
|
);
|
|
3634
4113
|
}
|
|
3635
4114
|
for (const file of plans.values()) {
|
|
3636
|
-
if (!(0,
|
|
3637
|
-
(0,
|
|
3638
|
-
(0,
|
|
4115
|
+
if (!(0, import_node_fs10.existsSync)(file.target) || (0, import_node_fs10.readFileSync)(file.target, "utf8") !== file.content) {
|
|
4116
|
+
(0, import_node_fs10.mkdirSync)((0, import_node_path8.dirname)(file.target), { recursive: true });
|
|
4117
|
+
(0, import_node_fs10.writeFileSync)(file.target, file.content);
|
|
3639
4118
|
}
|
|
3640
4119
|
}
|
|
3641
4120
|
const skills = skillNames(files);
|
|
@@ -3654,7 +4133,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
3654
4133
|
};
|
|
3655
4134
|
}
|
|
3656
4135
|
function pathsUnder(root, paths) {
|
|
3657
|
-
return [...paths].map((path) => (0,
|
|
4136
|
+
return [...paths].map((path) => (0, import_node_path8.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path8.sep}`) && !(0, import_node_path8.isAbsolute)(path)).sort();
|
|
3658
4137
|
}
|
|
3659
4138
|
function normalizeHarnesses(values, global) {
|
|
3660
4139
|
const requested = values?.length ? values : ["claude"];
|
|
@@ -3676,9 +4155,9 @@ function normalizeHarnesses(values, global) {
|
|
|
3676
4155
|
function managedFileContent(path, block, force, boundary) {
|
|
3677
4156
|
const symlink = symlinkedComponent(boundary, path);
|
|
3678
4157
|
if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
|
|
3679
|
-
if (!(0,
|
|
4158
|
+
if (!(0, import_node_fs10.existsSync)(path)) return `${block}
|
|
3680
4159
|
`;
|
|
3681
|
-
const current = (0,
|
|
4160
|
+
const current = (0, import_node_fs10.readFileSync)(path, "utf8");
|
|
3682
4161
|
const start = "<!-- odla-ai agent setup:start -->";
|
|
3683
4162
|
const end = "<!-- odla-ai agent setup:end -->";
|
|
3684
4163
|
const startAt = current.indexOf(start);
|
|
@@ -3699,15 +4178,15 @@ function managedFileContent(path, block, force, boundary) {
|
|
|
3699
4178
|
return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
|
|
3700
4179
|
}
|
|
3701
4180
|
function symlinkedComponent(boundary, target) {
|
|
3702
|
-
const rel = (0,
|
|
3703
|
-
if (rel === ".." || rel.startsWith(`..${
|
|
4181
|
+
const rel = (0, import_node_path8.relative)(boundary, target);
|
|
4182
|
+
if (rel === ".." || rel.startsWith(`..${import_node_path8.sep}`) || (0, import_node_path8.isAbsolute)(rel)) {
|
|
3704
4183
|
throw new Error(`agent setup target escapes its install root: ${target}`);
|
|
3705
4184
|
}
|
|
3706
4185
|
let current = boundary;
|
|
3707
|
-
for (const part of rel.split(
|
|
3708
|
-
current = (0,
|
|
4186
|
+
for (const part of rel.split(import_node_path8.sep).filter(Boolean)) {
|
|
4187
|
+
current = (0, import_node_path8.join)(current, part);
|
|
3709
4188
|
try {
|
|
3710
|
-
if ((0,
|
|
4189
|
+
if ((0, import_node_fs10.lstatSync)(current).isSymbolicLink()) return current;
|
|
3711
4190
|
} catch (error) {
|
|
3712
4191
|
if (error.code !== "ENOENT") throw error;
|
|
3713
4192
|
}
|
|
@@ -3718,13 +4197,13 @@ function skillNames(files) {
|
|
|
3718
4197
|
return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
|
|
3719
4198
|
}
|
|
3720
4199
|
function listFiles(dir) {
|
|
3721
|
-
if (!(0,
|
|
4200
|
+
if (!(0, import_node_fs10.existsSync)(dir)) return [];
|
|
3722
4201
|
const results = [];
|
|
3723
4202
|
const walk = (current) => {
|
|
3724
|
-
for (const entry of (0,
|
|
3725
|
-
const path = (0,
|
|
4203
|
+
for (const entry of (0, import_node_fs10.readdirSync)(current, { withFileTypes: true })) {
|
|
4204
|
+
const path = (0, import_node_path8.join)(current, entry.name);
|
|
3726
4205
|
if (entry.isDirectory()) walk(path);
|
|
3727
|
-
else results.push((0,
|
|
4206
|
+
else results.push((0, import_node_path8.relative)(dir, path));
|
|
3728
4207
|
}
|
|
3729
4208
|
};
|
|
3730
4209
|
walk(dir);
|
|
@@ -3775,9 +4254,10 @@ async function smoke(options) {
|
|
|
3775
4254
|
);
|
|
3776
4255
|
const status = await readCalendarStatus({ platform: cfg.platformUrl, appId: cfg.app.id, env, token, fetch: doFetch });
|
|
3777
4256
|
assertCalendarHealthy(status, calendarServiceConfig(cfg, env));
|
|
3778
|
-
out.log(` calendar: ${status.status},
|
|
4257
|
+
out.log(` calendar: ${status.status}, bookable (booking \u2192 ${status.bookingCalendarId ?? "primary"})`);
|
|
3779
4258
|
}
|
|
3780
|
-
const
|
|
4259
|
+
const database = await resolveDatabaseConfig(cfg);
|
|
4260
|
+
const expectedSchema = database.schema;
|
|
3781
4261
|
const expectedEntities = serializedEntities(expectedSchema);
|
|
3782
4262
|
const liveSchemaPayload = await getJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/schema`, entry.dbKey);
|
|
3783
4263
|
const liveSchema = liveSchemaPayload.schema ?? liveSchemaPayload;
|
|
@@ -3789,7 +4269,7 @@ async function smoke(options) {
|
|
|
3789
4269
|
out.log(` schema: ${liveEntities.length} entities`);
|
|
3790
4270
|
const aggregateEntity = expectedEntities[0] ?? liveEntities[0];
|
|
3791
4271
|
if (aggregateEntity) {
|
|
3792
|
-
const aggregate = await
|
|
4272
|
+
const aggregate = await postJson3(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/aggregate`, entry.dbKey, {
|
|
3793
4273
|
ns: aggregateEntity,
|
|
3794
4274
|
aggregate: { count: true }
|
|
3795
4275
|
});
|
|
@@ -3798,13 +4278,20 @@ async function smoke(options) {
|
|
|
3798
4278
|
} else {
|
|
3799
4279
|
out.log(` aggregate: skipped (schema has no entities)`);
|
|
3800
4280
|
}
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
4281
|
+
const probes = database.integrations.flatMap(
|
|
4282
|
+
(integration) => (integration.probes ?? []).map((probe) => ({ integration: integration.id, probe }))
|
|
4283
|
+
);
|
|
4284
|
+
if (probes.length) {
|
|
4285
|
+
const link = cfg.links?.[env];
|
|
4286
|
+
if (!link) throw new Error(`integration smoke probes require links.${env} in odla.config.mjs`);
|
|
4287
|
+
for (const { integration, probe } of probes) {
|
|
4288
|
+
const probeUrl = new URL(probe.path, link).toString();
|
|
4289
|
+
const res = await doFetch(probeUrl, { redirect: "manual" });
|
|
4290
|
+
if (res.status !== probe.expectedStatus) {
|
|
4291
|
+
throw new Error(`integration "${integration}" probe ${probe.path} returned ${res.status}; expected ${probe.expectedStatus}`);
|
|
4292
|
+
}
|
|
4293
|
+
out.log(` integration.${integration}: ${probe.path} \u2192 ${res.status}`);
|
|
4294
|
+
}
|
|
3808
4295
|
}
|
|
3809
4296
|
out.log("ok");
|
|
3810
4297
|
}
|
|
@@ -3813,29 +4300,26 @@ function assertCalendarHealthy(status, expected) {
|
|
|
3813
4300
|
if (status.status !== "healthy") {
|
|
3814
4301
|
throw new Error(`calendar connection is ${status.status}${status.error?.message ? `: ${status.error.message}` : ""}`);
|
|
3815
4302
|
}
|
|
3816
|
-
if (status.
|
|
3817
|
-
const
|
|
3818
|
-
if (!
|
|
3819
|
-
const missing = expected.
|
|
4303
|
+
if (!status.writable) throw new Error('calendar grant does not cover booking writes; run "odla-ai calendar connect" to re-consent');
|
|
4304
|
+
const hasEventsScope = status.grantedScopes.some((scope) => scope === GOOGLE_CALENDAR_EVENTS_SCOPE);
|
|
4305
|
+
if (!hasEventsScope) throw new Error("calendar connection is missing calendar.events consent");
|
|
4306
|
+
const missing = expected.availabilityCalendars.filter((id) => !status.calendars.includes(id));
|
|
3820
4307
|
if (missing.length) throw new Error(`calendar connection is missing configured calendars: ${missing.join(", ")}`);
|
|
3821
|
-
if (status.lastSuccessfulSyncAt === void 0) throw new Error("calendar has never completed a sync");
|
|
3822
|
-
if (Date.now() - status.lastSuccessfulSyncAt > 30 * 6e4) throw new Error("calendar sync is more than 30 minutes stale");
|
|
3823
|
-
if (status.watchExpiresAt !== void 0 && status.watchExpiresAt <= Date.now()) throw new Error("calendar watch channel is expired");
|
|
3824
4308
|
}
|
|
3825
4309
|
async function getJson(doFetch, url, bearer) {
|
|
3826
4310
|
const res = await doFetch(url, {
|
|
3827
4311
|
headers: bearer ? { authorization: `Bearer ${bearer}` } : void 0
|
|
3828
4312
|
});
|
|
3829
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await
|
|
4313
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText5(res)}`);
|
|
3830
4314
|
return res.json();
|
|
3831
4315
|
}
|
|
3832
|
-
async function
|
|
4316
|
+
async function postJson3(doFetch, url, bearer, body) {
|
|
3833
4317
|
const res = await doFetch(url, {
|
|
3834
4318
|
method: "POST",
|
|
3835
4319
|
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
3836
4320
|
body: JSON.stringify(body)
|
|
3837
4321
|
});
|
|
3838
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await
|
|
4322
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText5(res)}`);
|
|
3839
4323
|
return res.json();
|
|
3840
4324
|
}
|
|
3841
4325
|
function publicConfigUrl(platformUrl, appId, env) {
|
|
@@ -3843,7 +4327,7 @@ function publicConfigUrl(platformUrl, appId, env) {
|
|
|
3843
4327
|
url.searchParams.set("env", env);
|
|
3844
4328
|
return url.toString();
|
|
3845
4329
|
}
|
|
3846
|
-
async function
|
|
4330
|
+
async function safeText5(res) {
|
|
3847
4331
|
try {
|
|
3848
4332
|
return (await res.text()).slice(0, 500);
|
|
3849
4333
|
} catch {
|
|
@@ -3852,6 +4336,9 @@ async function safeText4(res) {
|
|
|
3852
4336
|
}
|
|
3853
4337
|
|
|
3854
4338
|
// src/cli.ts
|
|
4339
|
+
function exitCodeFor(err) {
|
|
4340
|
+
return err?.code === "handshake_pending" ? 75 : 1;
|
|
4341
|
+
}
|
|
3855
4342
|
async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
3856
4343
|
const parsed = parseArgv(argv);
|
|
3857
4344
|
const command = parsed.positionals[0] ?? "help";
|
|
@@ -3904,6 +4391,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
3904
4391
|
await adminCommand(parsed);
|
|
3905
4392
|
return;
|
|
3906
4393
|
}
|
|
4394
|
+
if (command === "app") {
|
|
4395
|
+
await appCommand(parsed, dependencies);
|
|
4396
|
+
return;
|
|
4397
|
+
}
|
|
3907
4398
|
if (command === "security") {
|
|
3908
4399
|
await securityCommand(parsed, dependencies);
|
|
3909
4400
|
return;
|
|
@@ -4000,6 +4491,7 @@ async function provisionCommand(parsed, dependencies) {
|
|
|
4000
4491
|
"token",
|
|
4001
4492
|
"email",
|
|
4002
4493
|
"open",
|
|
4494
|
+
"wait",
|
|
4003
4495
|
"yes"
|
|
4004
4496
|
], 1);
|
|
4005
4497
|
const writeDevVars2 = parsed.options["write-dev-vars"];
|
|
@@ -4014,6 +4506,7 @@ async function provisionCommand(parsed, dependencies) {
|
|
|
4014
4506
|
token: stringOpt(parsed.options.token),
|
|
4015
4507
|
email: stringOpt(parsed.options.email),
|
|
4016
4508
|
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
4509
|
+
wait: numberOpt(parsed.options.wait, "--wait"),
|
|
4017
4510
|
yes: parsed.options.yes === true,
|
|
4018
4511
|
fetch: dependencies.fetch,
|
|
4019
4512
|
openApprovalUrl: dependencies.openUrl,
|
|
@@ -4023,7 +4516,7 @@ async function provisionCommand(parsed, dependencies) {
|
|
|
4023
4516
|
}
|
|
4024
4517
|
async function calendarCommand(parsed, dependencies) {
|
|
4025
4518
|
const sub = parsed.positionals[1];
|
|
4026
|
-
if (sub !== "status" && sub !== "calendars" && sub !== "connect" && sub !== "
|
|
4519
|
+
if (sub !== "status" && sub !== "calendars" && sub !== "connect" && sub !== "disconnect") {
|
|
4027
4520
|
throw new Error(`unknown calendar subcommand "${sub ?? ""}". Try "odla-ai calendar status --env dev".`);
|
|
4028
4521
|
}
|
|
4029
4522
|
assertArgs(parsed, ["config", "env", "json", "token", "email", "open", "yes"], 2);
|
|
@@ -4044,26 +4537,25 @@ async function calendarCommand(parsed, dependencies) {
|
|
|
4044
4537
|
if (sub === "status") await calendarStatus(options);
|
|
4045
4538
|
else if (sub === "calendars") await calendarCalendars(options);
|
|
4046
4539
|
else if (sub === "connect") await calendarConnect(options);
|
|
4047
|
-
else if (sub === "resync") await calendarResync(options);
|
|
4048
4540
|
else await calendarDisconnect(options);
|
|
4049
4541
|
}
|
|
4050
4542
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4051
4543
|
0 && (module.exports = {
|
|
4052
4544
|
AGENT_HARNESSES,
|
|
4053
4545
|
CAPABILITIES,
|
|
4054
|
-
|
|
4546
|
+
GOOGLE_CALENDAR_EVENTS_SCOPE,
|
|
4055
4547
|
SYSTEM_AI_PURPOSES,
|
|
4056
4548
|
adminAi,
|
|
4057
4549
|
calendarBookingPageUrl,
|
|
4058
4550
|
calendarCalendars,
|
|
4059
4551
|
calendarConnect,
|
|
4060
4552
|
calendarDisconnect,
|
|
4061
|
-
calendarResync,
|
|
4062
4553
|
calendarServiceConfig,
|
|
4063
4554
|
calendarStatus,
|
|
4064
4555
|
connectGitHubSecuritySource,
|
|
4065
4556
|
disconnectGitHubSecuritySource,
|
|
4066
4557
|
doctor,
|
|
4558
|
+
exitCodeFor,
|
|
4067
4559
|
followHostedSecurityJob,
|
|
4068
4560
|
getHostedSecurityIntent,
|
|
4069
4561
|
getHostedSecurityJob,
|