@odla-ai/cli 0.11.2 → 0.12.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 +6 -1
- package/dist/bin.cjs +566 -332
- 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-Y4HWUEFF.js} +488 -256
- package/dist/chunk-Y4HWUEFF.js.map +1 -0
- package/dist/index.cjs +569 -335
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -51
- package/dist/index.d.ts +50 -51
- package/dist/index.js +5 -5
- package/package.json +1 -1
- package/skills/odla/SKILL.md +18 -10
- package/skills/odla/references/build.md +17 -14
- package/skills/odla/references/sdks.md +29 -17
- package/skills/odla-migrate/SKILL.md +8 -6
- 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,77 +994,26 @@ async function adminCommand(parsed) {
|
|
|
875
994
|
});
|
|
876
995
|
}
|
|
877
996
|
|
|
878
|
-
// src/
|
|
879
|
-
var
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
"register the app and enable configured services per environment",
|
|
883
|
-
"issue, persist, and explicitly rotate configured service credentials",
|
|
884
|
-
"write local Worker values and push deployed Worker secrets through Wrangler stdin",
|
|
885
|
-
"store tenant-vault secrets (including the Clerk webhook secret and reserved Clerk secret key) write-only from stdin or an env var",
|
|
886
|
-
"push db schema/rules and configure platform AI, auth, and deployment links",
|
|
887
|
-
"apply read-only Google calendar mirror and public booking-page config, then drive state-bound consent, status, discovery, resync, and disconnect flows",
|
|
888
|
-
"validate config offline and smoke-test a provisioned db environment",
|
|
889
|
-
"run app-attributed hosted security discovery and independent validation without provider keys",
|
|
890
|
-
"connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
|
|
891
|
-
"let admins manage system AI policy and credentials, inspect attributed usage, and read immutable admin changes through separate short-lived capability-only approvals"
|
|
892
|
-
],
|
|
893
|
-
agent: [
|
|
894
|
-
"install and import the selected odla SDKs",
|
|
895
|
-
"wrap the Worker with withObservability and choose useful telemetry",
|
|
896
|
-
"make application-specific schema, rules, auth, UI, and migration decisions",
|
|
897
|
-
"wire @odla-ai/calendar into trusted Worker code and keep the app admin key out of browsers"
|
|
898
|
-
],
|
|
899
|
-
human: [
|
|
900
|
-
"provide the existing odla account email, then sign in and explicitly review/approve the exact device code",
|
|
901
|
-
"review mirrored calendar/attendee disclosure and complete the server-issued Google consent flow",
|
|
902
|
-
"consent to production changes with --yes",
|
|
903
|
-
"request destructive credential rotation explicitly",
|
|
904
|
-
"review application semantics, security findings, releases, and merges",
|
|
905
|
-
"approve redacted-source disclosure before hosted security reasoning",
|
|
906
|
-
"approve GitHub App repository access separately from redacted-snippet disclosure"
|
|
907
|
-
],
|
|
908
|
-
studio: [
|
|
909
|
-
"view telemetry and environment state",
|
|
910
|
-
"let signed-in users inventory/revoke their own agent grants and admins audit/global-revoke them",
|
|
911
|
-
"review calendar connection, granted read scope, selected calendars, and sync health without exposing provider tokens",
|
|
912
|
-
"perform manual credential recovery when the CLI's local shown-once copy is unavailable",
|
|
913
|
-
"configure system AI purposes and view app/environment/run-attributed usage",
|
|
914
|
-
"connect/revoke GitHub security sources and inspect ref-to-SHA jobs, routes, retention, coverage, and normalized reports"
|
|
915
|
-
]
|
|
916
|
-
};
|
|
917
|
-
function printCapabilities(json = false, out = console) {
|
|
918
|
-
if (json) {
|
|
919
|
-
out.log(JSON.stringify(CAPABILITIES, null, 2));
|
|
920
|
-
return;
|
|
921
|
-
}
|
|
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
|
-
}
|
|
928
|
-
function printGroup(out, heading, items) {
|
|
929
|
-
out.log(`${heading}:`);
|
|
930
|
-
for (const item of items) out.log(` - ${item}`);
|
|
931
|
-
out.log("");
|
|
932
|
-
}
|
|
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");
|
|
933
1001
|
|
|
934
1002
|
// src/config.ts
|
|
935
|
-
var
|
|
936
|
-
var
|
|
1003
|
+
var import_node_fs4 = require("fs");
|
|
1004
|
+
var import_node_path3 = require("path");
|
|
937
1005
|
var import_node_url = require("url");
|
|
938
1006
|
var DEFAULT_PLATFORM = "https://odla.ai";
|
|
939
1007
|
var DEFAULT_ENVS = ["dev"];
|
|
940
1008
|
var DEFAULT_SERVICES = ["db", "ai"];
|
|
941
|
-
var
|
|
1009
|
+
var GOOGLE_CALENDAR_EVENTS_SCOPE = "https://www.googleapis.com/auth/calendar.events";
|
|
942
1010
|
async function loadProjectConfig(configPath = "odla.config.mjs") {
|
|
943
|
-
const resolved = (0,
|
|
944
|
-
if (!(0,
|
|
1011
|
+
const resolved = (0, import_node_path3.resolve)(configPath);
|
|
1012
|
+
if (!(0, import_node_fs4.existsSync)(resolved)) {
|
|
945
1013
|
throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
|
|
946
1014
|
}
|
|
947
1015
|
const raw = await loadConfigModule(resolved);
|
|
948
|
-
const rootDir = (0,
|
|
1016
|
+
const rootDir = (0, import_node_path3.dirname)(resolved);
|
|
949
1017
|
validateRawConfig(raw, resolved);
|
|
950
1018
|
const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
|
|
951
1019
|
const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
|
|
@@ -953,9 +1021,9 @@ async function loadProjectConfig(configPath = "odla.config.mjs") {
|
|
|
953
1021
|
const services = unique(raw.services?.length ? raw.services : DEFAULT_SERVICES);
|
|
954
1022
|
validateCalendarConfig(raw, envs, services, resolved);
|
|
955
1023
|
const local = {
|
|
956
|
-
tokenFile: (0,
|
|
957
|
-
credentialsFile: (0,
|
|
958
|
-
devVarsFile: (0,
|
|
1024
|
+
tokenFile: (0, import_node_path3.resolve)(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
|
|
1025
|
+
credentialsFile: (0, import_node_path3.resolve)(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
|
|
1026
|
+
devVarsFile: (0, import_node_path3.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
|
|
959
1027
|
gitignore: raw.local?.gitignore ?? true
|
|
960
1028
|
};
|
|
961
1029
|
return {
|
|
@@ -972,9 +1040,9 @@ async function loadProjectConfig(configPath = "odla.config.mjs") {
|
|
|
972
1040
|
async function resolveDataExport(cfg, value, names) {
|
|
973
1041
|
if (value === void 0 || value === null || value === false) return void 0;
|
|
974
1042
|
if (typeof value !== "string") return value;
|
|
975
|
-
const target = (0,
|
|
1043
|
+
const target = (0, import_node_path3.isAbsolute)(value) ? value : (0, import_node_path3.resolve)(cfg.rootDir, value);
|
|
976
1044
|
if (target.endsWith(".json")) {
|
|
977
|
-
return JSON.parse((0,
|
|
1045
|
+
return JSON.parse((0, import_node_fs4.readFileSync)(target, "utf8"));
|
|
978
1046
|
}
|
|
979
1047
|
const mod = await import((0, import_node_url.pathToFileURL)(target).href);
|
|
980
1048
|
for (const name of names) {
|
|
@@ -1001,16 +1069,14 @@ function calendarServiceConfig(cfg, env) {
|
|
|
1001
1069
|
if (!cfg.envs.includes(env)) throw new Error(`calendar env "${env}" is not declared in config envs`);
|
|
1002
1070
|
const google = cfg.calendar?.google;
|
|
1003
1071
|
if (!google) throw new Error("calendar.google is required when the calendar service is enabled");
|
|
1072
|
+
const availability = unique(
|
|
1073
|
+
(google.availabilityCalendars?.[env] ?? google.calendars?.[env]).map((id) => id.trim())
|
|
1074
|
+
);
|
|
1004
1075
|
return {
|
|
1005
1076
|
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"
|
|
1077
|
+
access: "book",
|
|
1078
|
+
bookingCalendarId: google.bookingCalendar?.[env]?.trim() ?? availability[0],
|
|
1079
|
+
availabilityCalendars: availability
|
|
1014
1080
|
};
|
|
1015
1081
|
}
|
|
1016
1082
|
function calendarBookingPageUrl(cfg, env) {
|
|
@@ -1059,20 +1125,39 @@ function validateCalendarConfig(cfg, envs, services, path) {
|
|
|
1059
1125
|
assertOnly(cfg.calendar, ["google"], `${path}: calendar`);
|
|
1060
1126
|
if (!isRecord4(cfg.calendar.google)) throw new Error(`${path}: calendar.google must be an object`);
|
|
1061
1127
|
const google = cfg.calendar.google;
|
|
1062
|
-
assertOnly(
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1128
|
+
assertOnly(
|
|
1129
|
+
google,
|
|
1130
|
+
["availabilityCalendars", "calendars", "bookingCalendar", "bookingPageUrl"],
|
|
1131
|
+
`${path}: calendar.google`
|
|
1132
|
+
);
|
|
1133
|
+
const availabilityKey = google.availabilityCalendars !== void 0 ? "availabilityCalendars" : google.calendars !== void 0 ? "calendars" : null;
|
|
1134
|
+
if (!availabilityKey || google.availabilityCalendars !== void 0 && google.calendars !== void 0) {
|
|
1135
|
+
throw new Error(`${path}: calendar.google requires exactly one of availabilityCalendars or calendars (legacy)`);
|
|
1136
|
+
}
|
|
1137
|
+
const availability = google[availabilityKey];
|
|
1138
|
+
if (!isRecord4(availability)) throw new Error(`${path}: calendar.google.${availabilityKey} must map env names to calendar ids`);
|
|
1139
|
+
const unknownEnv = Object.keys(availability).find((env) => !envs.includes(env));
|
|
1140
|
+
if (unknownEnv) throw new Error(`${path}: calendar.google.${availabilityKey}.${unknownEnv} is not in config envs`);
|
|
1066
1141
|
for (const env of envs) {
|
|
1067
|
-
const ids =
|
|
1142
|
+
const ids = availability[env];
|
|
1068
1143
|
if (!Array.isArray(ids) || ids.length === 0) {
|
|
1069
|
-
throw new Error(`${path}: calendar.google
|
|
1144
|
+
throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must be a non-empty array`);
|
|
1070
1145
|
}
|
|
1071
1146
|
if (ids.length > 10) {
|
|
1072
|
-
throw new Error(`${path}: calendar.google
|
|
1147
|
+
throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must contain at most 10 calendar ids`);
|
|
1073
1148
|
}
|
|
1074
1149
|
if (ids.some((id) => !safeText(id, 1024))) {
|
|
1075
|
-
throw new Error(`${path}: calendar.google
|
|
1150
|
+
throw new Error(`${path}: calendar.google.${availabilityKey}.${env} contains an invalid calendar id`);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
if (google.bookingCalendar !== void 0) {
|
|
1154
|
+
if (!isRecord4(google.bookingCalendar)) throw new Error(`${path}: calendar.google.bookingCalendar must map env names to one calendar id`);
|
|
1155
|
+
const unknownBookingEnv = Object.keys(google.bookingCalendar).find((env) => !envs.includes(env));
|
|
1156
|
+
if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingCalendar.${unknownBookingEnv} is not in config envs`);
|
|
1157
|
+
for (const [env, value] of Object.entries(google.bookingCalendar)) {
|
|
1158
|
+
if (!safeText(value, 1024)) {
|
|
1159
|
+
throw new Error(`${path}: calendar.google.bookingCalendar.${env} must be a calendar id`);
|
|
1160
|
+
}
|
|
1076
1161
|
}
|
|
1077
1162
|
}
|
|
1078
1163
|
if (google.bookingPageUrl !== void 0) {
|
|
@@ -1085,21 +1170,6 @@ function validateCalendarConfig(cfg, envs, services, path) {
|
|
|
1085
1170
|
}
|
|
1086
1171
|
}
|
|
1087
1172
|
}
|
|
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
1173
|
if (enabled && !services.includes("db")) throw new Error(`${path}: calendar service requires the db service`);
|
|
1104
1174
|
}
|
|
1105
1175
|
function assertOnly(value, allowed, label) {
|
|
@@ -1125,7 +1195,7 @@ function validId(value) {
|
|
|
1125
1195
|
return typeof value === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value);
|
|
1126
1196
|
}
|
|
1127
1197
|
async function loadConfigModule(path) {
|
|
1128
|
-
if (path.endsWith(".json")) return JSON.parse((0,
|
|
1198
|
+
if (path.endsWith(".json")) return JSON.parse((0, import_node_fs4.readFileSync)(path, "utf8"));
|
|
1129
1199
|
const mod = await import(`${(0, import_node_url.pathToFileURL)(path).href}?t=${Date.now()}`);
|
|
1130
1200
|
const value = mod.default ?? mod.config;
|
|
1131
1201
|
if (typeof value === "function") return await value();
|
|
@@ -1138,10 +1208,187 @@ function unique(values) {
|
|
|
1138
1208
|
return [...new Set(values.filter(Boolean))];
|
|
1139
1209
|
}
|
|
1140
1210
|
|
|
1211
|
+
// src/app-export.ts
|
|
1212
|
+
async function appExport(options) {
|
|
1213
|
+
const cfg = await loadProjectConfig(options.configPath);
|
|
1214
|
+
const out = options.stdout ?? console;
|
|
1215
|
+
const doFetch = options.fetch ?? fetch;
|
|
1216
|
+
const env = options.env ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0]);
|
|
1217
|
+
if (!env || !cfg.envs.includes(env)) throw new Error(`env "${env ?? ""}" is not declared in ${options.configPath}`);
|
|
1218
|
+
const tenant = env === "prod" ? cfg.app.id : `${cfg.app.id}--${env}`;
|
|
1219
|
+
const token = await getDeveloperToken(
|
|
1220
|
+
cfg,
|
|
1221
|
+
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
1222
|
+
doFetch,
|
|
1223
|
+
out
|
|
1224
|
+
);
|
|
1225
|
+
const auth = { authorization: `Bearer ${token}` };
|
|
1226
|
+
const base = `${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenant)}`;
|
|
1227
|
+
if (options.fresh) {
|
|
1228
|
+
const res = await doFetch(`${base}/export`, { method: "POST", headers: auth });
|
|
1229
|
+
const body = await res.json().catch(() => ({}));
|
|
1230
|
+
if (!res.ok) throw new Error(`export failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? res.status}`);
|
|
1231
|
+
}
|
|
1232
|
+
const list = await doFetch(`${base}/backups`, { headers: auth });
|
|
1233
|
+
if (!list.ok) throw new Error(`couldn't list backups (${list.status})`);
|
|
1234
|
+
const { backups } = await list.json();
|
|
1235
|
+
const newest = backups[0];
|
|
1236
|
+
if (!newest) {
|
|
1237
|
+
throw new Error(
|
|
1238
|
+
`${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)`
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
const download = await doFetch(`${base}/backups/${newest.id}/download`, { headers: auth });
|
|
1242
|
+
if (!download.ok || !download.body) throw new Error(`download failed (${download.status})`);
|
|
1243
|
+
const file = options.out ?? `${tenant}-${new Date(newest.created_at).toISOString().slice(0, 10)}-tx${newest.max_tx}.jsonl.gz`;
|
|
1244
|
+
await (0, import_promises.pipeline)(import_node_stream.Readable.fromWeb(download.body), (0, import_node_fs5.createWriteStream)(file));
|
|
1245
|
+
if (options.json) out.log(JSON.stringify({ file, backup: newest }, null, 2));
|
|
1246
|
+
else {
|
|
1247
|
+
out.log(`${tenant}: wrote ${file} (${newest.bytes} bytes, ${newest.kind} snapshot at tx ${newest.max_tx})`);
|
|
1248
|
+
out.log(`sha256 ${download.headers.get("x-odla-sha256") ?? newest.sha256}`);
|
|
1249
|
+
}
|
|
1250
|
+
return { file, backup: newest };
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// src/app-lifecycle.ts
|
|
1254
|
+
async function lifecycleCall(action, options) {
|
|
1255
|
+
const cfg = await loadProjectConfig(options.configPath);
|
|
1256
|
+
const out = options.stdout ?? console;
|
|
1257
|
+
const doFetch = options.fetch ?? fetch;
|
|
1258
|
+
const token = await getDeveloperToken(
|
|
1259
|
+
cfg,
|
|
1260
|
+
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
1261
|
+
doFetch,
|
|
1262
|
+
out
|
|
1263
|
+
);
|
|
1264
|
+
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/${action}`, {
|
|
1265
|
+
method: "POST",
|
|
1266
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }
|
|
1267
|
+
});
|
|
1268
|
+
const body = await res.json().catch(() => ({}));
|
|
1269
|
+
if (!res.ok || !body.ok) {
|
|
1270
|
+
throw new Error(`${action} failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? `registry returned ${res.status}`}`);
|
|
1271
|
+
}
|
|
1272
|
+
return body;
|
|
1273
|
+
}
|
|
1274
|
+
async function appArchive(options) {
|
|
1275
|
+
if (options.yes !== true) {
|
|
1276
|
+
throw new Error(
|
|
1277
|
+
"app archive suspends EVERY environment: API keys stop working and all services refuse requests until restored. All data is retained. Pass --yes to proceed."
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
const out = options.stdout ?? console;
|
|
1281
|
+
const body = await lifecycleCall("archive", options);
|
|
1282
|
+
if (options.json) out.log(JSON.stringify(body, null, 2));
|
|
1283
|
+
else if (body.operation?.state === "noop") out.log(`${body.app?.appId}: already archived`);
|
|
1284
|
+
else out.log(`${body.app?.appId}: archived \u2014 data retained; run \`odla-ai app restore\` (or use Studio) to bring it back`);
|
|
1285
|
+
}
|
|
1286
|
+
async function appRestore(options) {
|
|
1287
|
+
const out = options.stdout ?? console;
|
|
1288
|
+
const body = await lifecycleCall("restore", options);
|
|
1289
|
+
if (options.json) out.log(JSON.stringify(body, null, 2));
|
|
1290
|
+
else if (body.operation?.state === "noop") out.log(`${body.app?.appId}: already active`);
|
|
1291
|
+
else out.log(`${body.app?.appId}: restored \u2014 every service's data plane is live again`);
|
|
1292
|
+
}
|
|
1293
|
+
async function appCommand(parsed, dependencies = {}) {
|
|
1294
|
+
const sub = parsed.positionals[1];
|
|
1295
|
+
if (sub === "export") {
|
|
1296
|
+
assertArgs(parsed, ["config", "env", "token", "email", "fresh", "out", "json"], 2);
|
|
1297
|
+
await appExport({
|
|
1298
|
+
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
1299
|
+
env: stringOpt(parsed.options.env),
|
|
1300
|
+
token: stringOpt(parsed.options.token),
|
|
1301
|
+
email: stringOpt(parsed.options.email),
|
|
1302
|
+
fresh: parsed.options.fresh === true,
|
|
1303
|
+
out: stringOpt(parsed.options.out),
|
|
1304
|
+
json: parsed.options.json === true,
|
|
1305
|
+
fetch: dependencies.fetch,
|
|
1306
|
+
stdout: dependencies.stdout
|
|
1307
|
+
});
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
if (sub !== "archive" && sub !== "restore") {
|
|
1311
|
+
throw new Error(
|
|
1312
|
+
`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.)`
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
assertArgs(parsed, ["config", "token", "email", "yes", "json"], 2);
|
|
1316
|
+
const options = {
|
|
1317
|
+
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
1318
|
+
token: stringOpt(parsed.options.token),
|
|
1319
|
+
email: stringOpt(parsed.options.email),
|
|
1320
|
+
yes: parsed.options.yes === true,
|
|
1321
|
+
json: parsed.options.json === true,
|
|
1322
|
+
fetch: dependencies.fetch,
|
|
1323
|
+
stdout: dependencies.stdout
|
|
1324
|
+
};
|
|
1325
|
+
if (sub === "archive") await appArchive(options);
|
|
1326
|
+
else await appRestore(options);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// src/capabilities.ts
|
|
1330
|
+
var CAPABILITIES = {
|
|
1331
|
+
cli: [
|
|
1332
|
+
"start an email-bound device request without accepting a password or user session token",
|
|
1333
|
+
"register the app and enable configured services per environment",
|
|
1334
|
+
"issue, persist, and explicitly rotate configured service credentials",
|
|
1335
|
+
"write local Worker values and push deployed Worker secrets through Wrangler stdin",
|
|
1336
|
+
"store tenant-vault secrets (including the Clerk webhook secret and reserved Clerk secret key) write-only from stdin or an env var",
|
|
1337
|
+
"push db schema/rules and configure platform AI, auth, and deployment links",
|
|
1338
|
+
"apply Google Calendar booking config, then drive state-bound consent, status, discovery, and disconnect flows (bookings run live through the platform proxy)",
|
|
1339
|
+
"validate config offline and smoke-test a provisioned db environment",
|
|
1340
|
+
"run app-attributed hosted security discovery and independent validation without provider keys",
|
|
1341
|
+
"connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
|
|
1342
|
+
"let admins manage system AI policy and credentials, inspect attributed usage, and read immutable admin changes through separate short-lived capability-only approvals"
|
|
1343
|
+
],
|
|
1344
|
+
agent: [
|
|
1345
|
+
"install and import the selected odla SDKs",
|
|
1346
|
+
"wrap the Worker with withObservability and choose useful telemetry",
|
|
1347
|
+
"make application-specific schema, rules, auth, UI, and migration decisions",
|
|
1348
|
+
"wire @odla-ai/calendar into trusted Worker code and keep the app admin key out of browsers"
|
|
1349
|
+
],
|
|
1350
|
+
human: [
|
|
1351
|
+
"provide the existing odla account email, then sign in and explicitly review/approve the exact device code",
|
|
1352
|
+
"review mirrored calendar/attendee disclosure and complete the server-issued Google consent flow",
|
|
1353
|
+
"consent to production changes with --yes",
|
|
1354
|
+
"request destructive credential rotation explicitly",
|
|
1355
|
+
"review application semantics, security findings, releases, and merges",
|
|
1356
|
+
"approve redacted-source disclosure before hosted security reasoning",
|
|
1357
|
+
"approve GitHub App repository access separately from redacted-snippet disclosure"
|
|
1358
|
+
],
|
|
1359
|
+
studio: [
|
|
1360
|
+
"view telemetry and environment state",
|
|
1361
|
+
"let signed-in users inventory/revoke their own agent grants and admins audit/global-revoke them",
|
|
1362
|
+
"review calendar connection, granted read scope, selected calendars, and sync health without exposing provider tokens",
|
|
1363
|
+
"perform manual credential recovery when the CLI's local shown-once copy is unavailable",
|
|
1364
|
+
"configure system AI purposes and view app/environment/run-attributed usage",
|
|
1365
|
+
"connect/revoke GitHub security sources and inspect ref-to-SHA jobs, routes, retention, coverage, and normalized reports"
|
|
1366
|
+
]
|
|
1367
|
+
};
|
|
1368
|
+
function printCapabilities(json = false, out = console) {
|
|
1369
|
+
if (json) {
|
|
1370
|
+
out.log(JSON.stringify(CAPABILITIES, null, 2));
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
out.log("odla-ai responsibility boundary\n");
|
|
1374
|
+
printGroup(out, "CLI automates", CAPABILITIES.cli);
|
|
1375
|
+
printGroup(out, "Coding agent changes source", CAPABILITIES.agent);
|
|
1376
|
+
printGroup(out, "Human checkpoints", CAPABILITIES.human);
|
|
1377
|
+
printGroup(out, "Studio", CAPABILITIES.studio);
|
|
1378
|
+
}
|
|
1379
|
+
function printGroup(out, heading, items) {
|
|
1380
|
+
out.log(`${heading}:`);
|
|
1381
|
+
for (const item of items) out.log(` - ${item}`);
|
|
1382
|
+
out.log("");
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1141
1385
|
// src/calendar-errors.ts
|
|
1142
1386
|
var PLATFORM_NOT_READY_CODES = /* @__PURE__ */ new Set([
|
|
1143
1387
|
"calendar_google_oauth_not_configured",
|
|
1144
1388
|
"calendar_token_vault_not_configured",
|
|
1389
|
+
// Current spelling (booking-era key verifier) plus the retired mirror-era
|
|
1390
|
+
// ingress code, so the CLI stays resumable against either platform version.
|
|
1391
|
+
"calendar_db_verifier_not_configured",
|
|
1145
1392
|
"calendar_db_ingress_not_configured"
|
|
1146
1393
|
]);
|
|
1147
1394
|
var CalendarRequestError = class extends Error {
|
|
@@ -1183,8 +1430,6 @@ function looksSecret(value) {
|
|
|
1183
1430
|
var CALENDAR_STATES = [
|
|
1184
1431
|
"not_connected",
|
|
1185
1432
|
"authorizing",
|
|
1186
|
-
"needs_sync",
|
|
1187
|
-
"initial_sync",
|
|
1188
1433
|
"healthy",
|
|
1189
1434
|
"degraded",
|
|
1190
1435
|
"disconnected",
|
|
@@ -1232,9 +1477,6 @@ async function pollCalendarConnection(ctx, attemptId) {
|
|
|
1232
1477
|
ctx.env
|
|
1233
1478
|
);
|
|
1234
1479
|
}
|
|
1235
|
-
async function requestCalendarResync(ctx) {
|
|
1236
|
-
return parseCalendarStatus(await calendarJson(ctx, "/resync", { method: "POST", body: "{}" }), ctx.env);
|
|
1237
|
-
}
|
|
1238
1480
|
async function requestCalendarDisconnect(ctx) {
|
|
1239
1481
|
return parseCalendarStatus(
|
|
1240
1482
|
await calendarJson(ctx, "/disconnect", { method: "POST", body: JSON.stringify({ purge: false }) }),
|
|
@@ -1245,10 +1487,8 @@ function parseCalendarStatus(raw, env) {
|
|
|
1245
1487
|
const outer = wrapped(raw, "calendar");
|
|
1246
1488
|
const value = record(outer.attempt) ?? record(outer.status) ?? outer;
|
|
1247
1489
|
const connection = record(value.connection) ?? {};
|
|
1248
|
-
const sync = record(value.sync) ?? record(connection.sync) ?? {};
|
|
1249
1490
|
const config = record(value.config) ?? record(outer.config) ?? {};
|
|
1250
1491
|
const googleConfig = record(config.google) ?? config;
|
|
1251
|
-
const watches = record(value.watches) ?? {};
|
|
1252
1492
|
const stateValue = calendarState(value.status ?? value.state ?? connection.status ?? connection.state);
|
|
1253
1493
|
if (!stateValue) {
|
|
1254
1494
|
throw new Error("calendar status returned an invalid connection state");
|
|
@@ -1258,31 +1498,29 @@ function parseCalendarStatus(raw, env) {
|
|
|
1258
1498
|
throw new Error("calendar status returned an unsupported provider");
|
|
1259
1499
|
}
|
|
1260
1500
|
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");
|
|
1501
|
+
if (accessValue !== void 0 && accessValue !== "book" && accessValue !== "read") {
|
|
1502
|
+
throw new Error("calendar status returned unsupported access");
|
|
1265
1503
|
}
|
|
1266
1504
|
const errorValue = record(value.error) ?? record(connection.error);
|
|
1267
1505
|
const errorCode = textField(value.lastErrorCode, 128);
|
|
1268
1506
|
const bookingPageValue = Object.hasOwn(value, "bookingPageUrl") ? value.bookingPageUrl : Object.hasOwn(config, "bookingPageUrl") ? config.bookingPageUrl : googleConfig.bookingPageUrl;
|
|
1507
|
+
const connected = typeof (value.connected ?? connection.connected) === "boolean" ? Boolean(value.connected ?? connection.connected) : ["healthy", "degraded"].includes(stateValue);
|
|
1508
|
+
const bookingCalendarId = textField(value.bookingCalendarId ?? config.bookingCalendarId, 1024);
|
|
1269
1509
|
return {
|
|
1270
1510
|
env,
|
|
1271
1511
|
enabled: typeof value.enabled === "boolean" ? value.enabled : true,
|
|
1272
|
-
connected
|
|
1512
|
+
connected,
|
|
1513
|
+
writable: typeof value.writable === "boolean" ? value.writable : stateValue === "healthy",
|
|
1273
1514
|
provider: providerValue === "google" ? "google" : null,
|
|
1274
1515
|
status: stateValue,
|
|
1275
|
-
...accessValue
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1516
|
+
...accessValue !== void 0 ? { access: "book" } : {},
|
|
1517
|
+
...bookingCalendarId ? { bookingCalendarId } : {},
|
|
1518
|
+
calendars: calendarIds(
|
|
1519
|
+
value.availabilityCalendars ?? config.availabilityCalendars ?? value.configuredCalendars ?? value.calendars ?? config.calendars ?? googleConfig.calendars
|
|
1520
|
+
),
|
|
1279
1521
|
...optionalNullableUrl("bookingPageUrl", bookingPageValue),
|
|
1280
1522
|
grantedScopes: stringList(value.grantedScopes ?? connection.grantedScopes ?? connection.scopes),
|
|
1281
1523
|
...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
1524
|
...errorValue || errorCode ? { error: {
|
|
1287
1525
|
...textField(errorValue?.code, 128) ? { code: textField(errorValue?.code, 128) } : {},
|
|
1288
1526
|
...textField(errorValue?.message, 500) ? { message: textField(errorValue?.message, 500) } : {},
|
|
@@ -1297,7 +1535,9 @@ function calendarState(value) {
|
|
|
1297
1535
|
disabled: "not_connected",
|
|
1298
1536
|
disconnected: "disconnected",
|
|
1299
1537
|
connecting: "authorizing",
|
|
1300
|
-
syncing: "
|
|
1538
|
+
syncing: "healthy",
|
|
1539
|
+
needs_sync: "healthy",
|
|
1540
|
+
initial_sync: "healthy",
|
|
1301
1541
|
ready: "healthy",
|
|
1302
1542
|
error: "failed"
|
|
1303
1543
|
};
|
|
@@ -1365,13 +1605,6 @@ function optionalText(key, value, max) {
|
|
|
1365
1605
|
const parsed = textField(value, max);
|
|
1366
1606
|
return parsed ? { [key]: parsed } : {};
|
|
1367
1607
|
}
|
|
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
1608
|
function optionalNullableUrl(key, value) {
|
|
1376
1609
|
if (value === null) return { [key]: null };
|
|
1377
1610
|
const parsed = textField(value, 4096);
|
|
@@ -1423,7 +1656,7 @@ async function calendarConnect(options) {
|
|
|
1423
1656
|
const page = calendarBookingPageUrl(cfg, ctx.env);
|
|
1424
1657
|
const applied = page === void 0 ? await readCalendarStatus(ctx) : await applyCalendarSettings(ctx, page);
|
|
1425
1658
|
const connectOptions = connectionOptions(options, out);
|
|
1426
|
-
return await continueConnectedCalendar(ctx, applied, connectOptions
|
|
1659
|
+
return await continueConnectedCalendar(ctx, applied, connectOptions) ?? connectWithContext(ctx, connectOptions);
|
|
1427
1660
|
}
|
|
1428
1661
|
async function applyCalendarBookingPage(ctx, bookingPageUrl, out) {
|
|
1429
1662
|
if (bookingPageUrl === void 0) return null;
|
|
@@ -1431,25 +1664,18 @@ async function applyCalendarBookingPage(ctx, bookingPageUrl, out) {
|
|
|
1431
1664
|
out.log(`${ctx.env}: calendar booking page ${bookingPageUrl ? "set" : "cleared"}`);
|
|
1432
1665
|
return status;
|
|
1433
1666
|
}
|
|
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
1667
|
async function calendarDisconnect(options) {
|
|
1442
1668
|
if (!options.yes) throw new Error("calendar disconnect requires --yes");
|
|
1443
1669
|
const { ctx, out } = await lifecycleContext(options);
|
|
1444
1670
|
const status = await requestCalendarDisconnect(ctx);
|
|
1445
|
-
out.log(`${ctx.env}: calendar disconnected;
|
|
1671
|
+
out.log(`${ctx.env}: calendar disconnected; no calendar data was stored`);
|
|
1446
1672
|
return status;
|
|
1447
1673
|
}
|
|
1448
1674
|
async function ensureCalendarConnected(ctx, options) {
|
|
1449
1675
|
const out = options.stdout ?? console;
|
|
1450
1676
|
const current = await readCalendarStatus(ctx);
|
|
1451
1677
|
const connectOptions = connectionOptions(options, out);
|
|
1452
|
-
return await continueConnectedCalendar(ctx, current, connectOptions
|
|
1678
|
+
return await continueConnectedCalendar(ctx, current, connectOptions) ?? connectWithContext(ctx, connectOptions);
|
|
1453
1679
|
}
|
|
1454
1680
|
async function lifecycleContext(options) {
|
|
1455
1681
|
const cfg = await loadProjectConfig(options.configPath);
|
|
@@ -1485,25 +1711,20 @@ function connectionOptions(options, out) {
|
|
|
1485
1711
|
signal: options.signal
|
|
1486
1712
|
};
|
|
1487
1713
|
}
|
|
1488
|
-
async function continueConnectedCalendar(ctx, current, options
|
|
1489
|
-
if (current.status === "healthy"
|
|
1490
|
-
options.out.log(`${ctx.env}: calendar
|
|
1714
|
+
async function continueConnectedCalendar(ctx, current, options) {
|
|
1715
|
+
if (current.status === "healthy") {
|
|
1716
|
+
options.out.log(`${ctx.env}: calendar already connected (healthy)`);
|
|
1491
1717
|
return current;
|
|
1492
1718
|
}
|
|
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;
|
|
1719
|
+
if (current.status === "degraded") {
|
|
1720
|
+
options.out.log(`${ctx.env}: calendar grant predates booking scopes \u2014 Google re-consent required`);
|
|
1721
|
+
return null;
|
|
1500
1722
|
}
|
|
1501
1723
|
if (current.status === "failed" && current.connected) {
|
|
1502
1724
|
const reason = current.error?.message ?? current.error?.code;
|
|
1503
|
-
throw new Error(`calendar connection failed${reason ? `: ${reason}` : ""}; inspect status and
|
|
1725
|
+
throw new Error(`calendar connection failed${reason ? `: ${reason}` : ""}; inspect status and reconnect explicitly`);
|
|
1504
1726
|
}
|
|
1505
|
-
|
|
1506
|
-
if (!waitingForExisting) return null;
|
|
1727
|
+
if (current.status !== "authorizing") return null;
|
|
1507
1728
|
const now = options.now ?? Date.now;
|
|
1508
1729
|
const deadline = now() + pollTimeout(options.pollTimeoutMs);
|
|
1509
1730
|
const wait = options.wait ?? waitForCalendarPoll;
|
|
@@ -1515,17 +1736,16 @@ async function continueConnectedCalendar(ctx, current, options, reuseDegraded) {
|
|
|
1515
1736
|
options.out.log(`${ctx.env}: calendar ${status.status.replaceAll("_", " ")}`);
|
|
1516
1737
|
prior = status.status;
|
|
1517
1738
|
}
|
|
1518
|
-
if (status.status === "healthy"
|
|
1739
|
+
if (status.status === "healthy") return status;
|
|
1519
1740
|
if (status.status === "degraded") return null;
|
|
1520
|
-
if (status.status === "needs_sync") return requestCalendarResync(ctx);
|
|
1521
1741
|
if (status.status === "failed" && status.connected) {
|
|
1522
1742
|
const reason = status.error?.message ?? status.error?.code;
|
|
1523
|
-
throw new Error(`calendar connection failed${reason ? `: ${reason}` : ""}; inspect status and
|
|
1743
|
+
throw new Error(`calendar connection failed${reason ? `: ${reason}` : ""}; inspect status and reconnect explicitly`);
|
|
1524
1744
|
}
|
|
1525
|
-
if (status.status !== "authorizing"
|
|
1745
|
+
if (status.status !== "authorizing") return null;
|
|
1526
1746
|
await wait(Math.min(2e3, Math.max(0, deadline - now())), options.signal);
|
|
1527
1747
|
}
|
|
1528
|
-
throw new Error('Existing Google Calendar authorization
|
|
1748
|
+
throw new Error('Existing Google Calendar authorization timed out; run "odla-ai calendar status" and retry');
|
|
1529
1749
|
}
|
|
1530
1750
|
async function connectWithContext(ctx, options) {
|
|
1531
1751
|
const attempt = await startCalendarConnection(ctx);
|
|
@@ -1547,14 +1767,17 @@ async function connectWithContext(ctx, options) {
|
|
|
1547
1767
|
options.out.log(`${ctx.env}: calendar ${status.status.replaceAll("_", " ")}`);
|
|
1548
1768
|
prior = status.status;
|
|
1549
1769
|
}
|
|
1550
|
-
if (status.status === "healthy"
|
|
1770
|
+
if (status.status === "healthy") return status;
|
|
1771
|
+
if (status.status === "degraded") {
|
|
1772
|
+
throw new Error("calendar connected without booking scopes; re-run connect and grant calendar access");
|
|
1773
|
+
}
|
|
1551
1774
|
if (status.status === "failed" || status.status === "disconnected" || status.status === "not_connected") {
|
|
1552
1775
|
const reason = status.error?.message ?? status.error?.code;
|
|
1553
1776
|
throw new Error(`calendar connection ${status.status}${reason ? `: ${reason}` : ""}`);
|
|
1554
1777
|
}
|
|
1555
1778
|
await wait(Math.min(2e3, Math.max(0, deadline - now())), options.signal);
|
|
1556
1779
|
}
|
|
1557
|
-
throw new Error('Google Calendar consent
|
|
1780
|
+
throw new Error('Google Calendar consent timed out; run "odla-ai calendar status" and retry connect');
|
|
1558
1781
|
}
|
|
1559
1782
|
function trustedCalendarConsentUrl(platform, value) {
|
|
1560
1783
|
const origin = platformAudience(platform);
|
|
@@ -1584,26 +1807,22 @@ function printStatus(status, json, out) {
|
|
|
1584
1807
|
return;
|
|
1585
1808
|
}
|
|
1586
1809
|
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
|
-
}
|
|
1810
|
+
out.log(` bookable: ${status.writable ? "yes" : status.connected ? "no \u2014 reconnect to grant booking scopes" : "no \u2014 not connected"}`);
|
|
1811
|
+
if (status.bookingCalendarId) out.log(` booking calendar: ${status.bookingCalendarId}`);
|
|
1812
|
+
out.log(` availability calendars: ${status.calendars.length ? status.calendars.join(", ") : "none"}`);
|
|
1592
1813
|
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
1814
|
if (status.error?.code || status.error?.message) out.log(` error: ${status.error.code ?? "error"}${status.error.message ? ` \u2014 ${status.error.message}` : ""}`);
|
|
1596
1815
|
}
|
|
1597
1816
|
|
|
1598
1817
|
// src/doctor-checks.ts
|
|
1599
1818
|
var import_node_child_process3 = require("child_process");
|
|
1600
|
-
var
|
|
1601
|
-
var
|
|
1819
|
+
var import_node_fs7 = require("fs");
|
|
1820
|
+
var import_node_path5 = require("path");
|
|
1602
1821
|
|
|
1603
1822
|
// src/wrangler.ts
|
|
1604
1823
|
var import_node_child_process2 = require("child_process");
|
|
1605
|
-
var
|
|
1606
|
-
var
|
|
1824
|
+
var import_node_fs6 = require("fs");
|
|
1825
|
+
var import_node_path4 = require("path");
|
|
1607
1826
|
var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
|
|
1608
1827
|
const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
1609
1828
|
let stdout = "";
|
|
@@ -1617,15 +1836,15 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) =>
|
|
|
1617
1836
|
var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
|
|
1618
1837
|
function findWranglerConfig(rootDir) {
|
|
1619
1838
|
for (const name of WRANGLER_CONFIG_FILES) {
|
|
1620
|
-
const path = (0,
|
|
1621
|
-
if ((0,
|
|
1839
|
+
const path = (0, import_node_path4.join)(rootDir, name);
|
|
1840
|
+
if ((0, import_node_fs6.existsSync)(path)) return path;
|
|
1622
1841
|
}
|
|
1623
1842
|
return null;
|
|
1624
1843
|
}
|
|
1625
1844
|
function readWranglerConfig(path) {
|
|
1626
1845
|
if (path.endsWith(".toml")) return null;
|
|
1627
1846
|
try {
|
|
1628
|
-
return JSON.parse(stripJsonComments((0,
|
|
1847
|
+
return JSON.parse(stripJsonComments((0, import_node_fs6.readFileSync)(path, "utf8")));
|
|
1629
1848
|
} catch {
|
|
1630
1849
|
return null;
|
|
1631
1850
|
}
|
|
@@ -1723,10 +1942,10 @@ function wranglerWarnings(rootDir) {
|
|
|
1723
1942
|
for (const { label, block } of blocks) {
|
|
1724
1943
|
const assets = block.assets;
|
|
1725
1944
|
if (assets?.directory) {
|
|
1726
|
-
const dir = (0,
|
|
1727
|
-
if (dir === (0,
|
|
1945
|
+
const dir = (0, import_node_path5.resolve)(rootDir, assets.directory);
|
|
1946
|
+
if (dir === (0, import_node_path5.resolve)(rootDir)) {
|
|
1728
1947
|
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,
|
|
1948
|
+
} else if ((0, import_node_fs7.existsSync)((0, import_node_path5.join)(dir, "node_modules"))) {
|
|
1730
1949
|
warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
|
|
1731
1950
|
}
|
|
1732
1951
|
}
|
|
@@ -1761,13 +1980,13 @@ function o11yProjectWarnings(rootDir) {
|
|
|
1761
1980
|
warnings.push("cannot verify o11y Worker instrumentation \u2014 add a parseable wrangler.jsonc/json config");
|
|
1762
1981
|
return warnings;
|
|
1763
1982
|
}
|
|
1764
|
-
const main = typeof config.main === "string" ? (0,
|
|
1765
|
-
if (!main || !(0,
|
|
1983
|
+
const main = typeof config.main === "string" ? (0, import_node_path5.resolve)(rootDir, config.main) : null;
|
|
1984
|
+
if (!main || !(0, import_node_fs7.existsSync)(main)) {
|
|
1766
1985
|
warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
|
|
1767
1986
|
} else {
|
|
1768
1987
|
let source = "";
|
|
1769
1988
|
try {
|
|
1770
|
-
source = (0,
|
|
1989
|
+
source = (0, import_node_fs7.readFileSync)(main, "utf8");
|
|
1771
1990
|
} catch {
|
|
1772
1991
|
}
|
|
1773
1992
|
if (!/\bwithObservability\b/.test(source)) {
|
|
@@ -1791,7 +2010,7 @@ function calendarProjectWarnings(rootDir) {
|
|
|
1791
2010
|
}
|
|
1792
2011
|
function readPackageJson(rootDir) {
|
|
1793
2012
|
try {
|
|
1794
|
-
return JSON.parse((0,
|
|
2013
|
+
return JSON.parse((0, import_node_fs7.readFileSync)((0, import_node_path5.join)(rootDir, "package.json"), "utf8"));
|
|
1795
2014
|
} catch {
|
|
1796
2015
|
return null;
|
|
1797
2016
|
}
|
|
@@ -1815,8 +2034,11 @@ async function doctor(options) {
|
|
|
1815
2034
|
out.log(`rules: ${rules ? `${Object.keys(rules).length} namespaces` : "none"}`);
|
|
1816
2035
|
out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "not configured" : "not enabled"}`);
|
|
1817
2036
|
if (cfg.services.includes("calendar")) {
|
|
1818
|
-
const calendar = cfg.envs.map((env) =>
|
|
1819
|
-
|
|
2037
|
+
const calendar = cfg.envs.map((env) => {
|
|
2038
|
+
const resolved = calendarServiceConfig(cfg, env);
|
|
2039
|
+
return `${env}:${resolved.bookingCalendarId}+${resolved.availabilityCalendars.length}`;
|
|
2040
|
+
}).join(", ");
|
|
2041
|
+
out.log(`calendar: google/booking (${calendar})`);
|
|
1820
2042
|
const pages = cfg.envs.map((env) => `${env}:${calendarBookingPageUrl(cfg, env) ?? "none"}`).join(", ");
|
|
1821
2043
|
out.log(`booking pages: ${pages}`);
|
|
1822
2044
|
} else {
|
|
@@ -1868,9 +2090,9 @@ async function doctor(options) {
|
|
|
1868
2090
|
}
|
|
1869
2091
|
|
|
1870
2092
|
// src/help.ts
|
|
1871
|
-
var
|
|
2093
|
+
var import_node_fs8 = require("fs");
|
|
1872
2094
|
function cliVersion() {
|
|
1873
|
-
const pkg = JSON.parse((0,
|
|
2095
|
+
const pkg = JSON.parse((0, import_node_fs8.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
|
|
1874
2096
|
return pkg.version ?? "unknown";
|
|
1875
2097
|
}
|
|
1876
2098
|
function printHelp() {
|
|
@@ -1883,8 +2105,10 @@ Usage:
|
|
|
1883
2105
|
odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
|
|
1884
2106
|
odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
|
|
1885
2107
|
odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
|
|
1886
|
-
odla-ai calendar resync [--env dev] [--email <odla-account>] [--yes]
|
|
1887
2108
|
odla-ai calendar disconnect [--env dev] [--email <odla-account>] --yes
|
|
2109
|
+
odla-ai app archive [--config odla.config.mjs] [--email <odla-account>] [--json] --yes
|
|
2110
|
+
odla-ai app restore [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
2111
|
+
odla-ai app export [--env dev] [--fresh] [--out <file>] [--email <odla-account>] [--json]
|
|
1888
2112
|
odla-ai capabilities [--json]
|
|
1889
2113
|
odla-ai admin ai show [--platform https://odla.ai] [--email <odla-account>] [--json]
|
|
1890
2114
|
odla-ai admin ai models [--provider <id>] [--json]
|
|
@@ -1904,7 +2128,7 @@ Usage:
|
|
|
1904
2128
|
odla-ai security report <job-id> [--json]
|
|
1905
2129
|
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
1906
2130
|
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]
|
|
2131
|
+
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
2132
|
odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
|
|
1909
2133
|
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
1910
2134
|
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
@@ -1916,7 +2140,14 @@ Commands:
|
|
|
1916
2140
|
setup Install offline odla runbooks for common coding-agent harnesses.
|
|
1917
2141
|
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
1918
2142
|
doctor Validate and summarize the project config without network calls.
|
|
1919
|
-
calendar Inspect, connect,
|
|
2143
|
+
calendar Inspect, connect, or disconnect the live Google booking connection.
|
|
2144
|
+
app Archive (suspend, data retained), restore, or export the app.
|
|
2145
|
+
Archiving takes every environment's data plane down until restored;
|
|
2146
|
+
permanent deletion has NO CLI \u2014 it requires a signed-in owner in
|
|
2147
|
+
Studio, and no machine or agent credential can purge. "app export"
|
|
2148
|
+
downloads a portable gzipped-JSONL snapshot of one environment's
|
|
2149
|
+
database (--fresh takes a new snapshot first) \u2014 your data is
|
|
2150
|
+
always yours to take.
|
|
1920
2151
|
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
1921
2152
|
admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
|
|
1922
2153
|
security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
|
|
@@ -1938,6 +2169,12 @@ Safety:
|
|
|
1938
2169
|
Provision opens the approval page in your browser automatically whenever the
|
|
1939
2170
|
machine can show one, including agent-driven runs; only CI, SSH, and
|
|
1940
2171
|
display-less hosts skip it. Use --open to force or --no-open to suppress.
|
|
2172
|
+
Browser launch is best-effort: the printed approval URL is authoritative and
|
|
2173
|
+
agents must relay it to the human verbatim. A started handshake is persisted
|
|
2174
|
+
under .odla/, so a command killed mid-wait loses nothing \u2014 rerunning resumes
|
|
2175
|
+
the same code. Outside an interactive terminal the wait is capped (90s by
|
|
2176
|
+
default, --wait <seconds> to change); a still-pending handshake then exits
|
|
2177
|
+
with code 75: relay the URL, wait for approval, and re-run to collect.
|
|
1941
2178
|
A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
|
|
1942
2179
|
The email is a non-secret identity hint: never provide a password or session
|
|
1943
2180
|
token. The matching account must already exist, be signed in, explicitly
|
|
@@ -1974,13 +2211,13 @@ function harnessOption(value, flag) {
|
|
|
1974
2211
|
}
|
|
1975
2212
|
|
|
1976
2213
|
// src/init.ts
|
|
1977
|
-
var
|
|
1978
|
-
var
|
|
2214
|
+
var import_node_fs9 = require("fs");
|
|
2215
|
+
var import_node_path6 = require("path");
|
|
1979
2216
|
function initProject(options) {
|
|
1980
2217
|
const out = options.stdout ?? console;
|
|
1981
|
-
const rootDir = (0,
|
|
1982
|
-
const configPath = (0,
|
|
1983
|
-
if ((0,
|
|
2218
|
+
const rootDir = (0, import_node_path6.resolve)(options.rootDir ?? process.cwd());
|
|
2219
|
+
const configPath = (0, import_node_path6.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
|
|
2220
|
+
if ((0, import_node_fs9.existsSync)(configPath) && !options.force) {
|
|
1984
2221
|
throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
|
|
1985
2222
|
}
|
|
1986
2223
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
|
|
@@ -1990,29 +2227,29 @@ function initProject(options) {
|
|
|
1990
2227
|
const services = options.services?.length ? options.services : ["db", "ai"];
|
|
1991
2228
|
if (services.includes("calendar") && !services.includes("db")) throw new Error("--services calendar requires db");
|
|
1992
2229
|
const aiProvider = options.aiProvider ?? "anthropic";
|
|
1993
|
-
(0,
|
|
1994
|
-
(0,
|
|
1995
|
-
(0,
|
|
1996
|
-
(0,
|
|
1997
|
-
writeIfMissing((0,
|
|
1998
|
-
writeIfMissing((0,
|
|
2230
|
+
(0, import_node_fs9.mkdirSync)((0, import_node_path6.dirname)(configPath), { recursive: true });
|
|
2231
|
+
(0, import_node_fs9.mkdirSync)((0, import_node_path6.resolve)(rootDir, "src/odla"), { recursive: true });
|
|
2232
|
+
(0, import_node_fs9.mkdirSync)((0, import_node_path6.resolve)(rootDir, ".odla"), { recursive: true });
|
|
2233
|
+
(0, import_node_fs9.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
|
|
2234
|
+
writeIfMissing((0, import_node_path6.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
|
|
2235
|
+
writeIfMissing((0, import_node_path6.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
|
|
1999
2236
|
ensureGitignore(rootDir);
|
|
2000
2237
|
out.log(`created ${relativeDisplay(configPath, rootDir)}`);
|
|
2001
2238
|
out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
|
|
2002
2239
|
out.log("updated .gitignore for local odla credentials");
|
|
2003
2240
|
}
|
|
2004
2241
|
function writeIfMissing(path, text) {
|
|
2005
|
-
if ((0,
|
|
2006
|
-
(0,
|
|
2242
|
+
if ((0, import_node_fs9.existsSync)(path)) return;
|
|
2243
|
+
(0, import_node_fs9.writeFileSync)(path, text);
|
|
2007
2244
|
}
|
|
2008
2245
|
function configTemplate(input) {
|
|
2009
2246
|
const calendar = input.services.includes("calendar") ? ` calendar: {
|
|
2010
2247
|
google: {
|
|
2011
|
-
|
|
2248
|
+
// Calendars consulted for availability; bookings land on bookingCalendar
|
|
2249
|
+
// (defaults to the first availability calendar). Google consent happens
|
|
2250
|
+
// in the browser during provision; bookings write live through odla.ai.
|
|
2251
|
+
availabilityCalendars: ${JSON.stringify(Object.fromEntries(input.envs.map((env) => [env, ["primary"]])))},
|
|
2012
2252
|
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
2253
|
},
|
|
2017
2254
|
},
|
|
2018
2255
|
` : "";
|
|
@@ -2103,7 +2340,7 @@ function relativeDisplay(path, rootDir) {
|
|
|
2103
2340
|
// src/provision.ts
|
|
2104
2341
|
var import_apps2 = require("@odla-ai/apps");
|
|
2105
2342
|
var import_ai = require("@odla-ai/ai");
|
|
2106
|
-
var
|
|
2343
|
+
var import_node_process7 = __toESM(require("process"), 1);
|
|
2107
2344
|
|
|
2108
2345
|
// src/provision-credentials.ts
|
|
2109
2346
|
var import_apps = require("@odla-ai/apps");
|
|
@@ -2308,7 +2545,7 @@ async function provision(options) {
|
|
|
2308
2545
|
if (cfg.services.includes("calendar")) {
|
|
2309
2546
|
for (const env of cfg.envs) {
|
|
2310
2547
|
const calendar = calendarServiceConfig(cfg, env);
|
|
2311
|
-
out.log(` calendar.${env}: google/
|
|
2548
|
+
out.log(` calendar.${env}: google/book \u2192 ${calendar.bookingCalendarId}; availability ${calendar.availabilityCalendars.join(", ")} (${GOOGLE_CALENDAR_EVENTS_SCOPE})`);
|
|
2312
2549
|
const bookingPage = calendarBookingPageUrl(cfg, env);
|
|
2313
2550
|
out.log(` calendar.${env}.bookingPageUrl: ${bookingPage === void 0 ? "unchanged" : bookingPage ?? "clear"}`);
|
|
2314
2551
|
}
|
|
@@ -2331,7 +2568,7 @@ async function provision(options) {
|
|
|
2331
2568
|
}
|
|
2332
2569
|
const devVarsTarget = resolveWriteDevVarsTarget(cfg, options.writeDevVars);
|
|
2333
2570
|
if (cfg.local.gitignore) {
|
|
2334
|
-
ensureGitignore(cfg.rootDir, [cfg.local.tokenFile, cfg.local.credentialsFile, ...devVarsTarget ? [devVarsTarget] : []]);
|
|
2571
|
+
ensureGitignore(cfg.rootDir, [cfg.local.tokenFile, handshakeFile(cfg), cfg.local.credentialsFile, ...devVarsTarget ? [devVarsTarget] : []]);
|
|
2335
2572
|
}
|
|
2336
2573
|
if (options.pushSecrets) {
|
|
2337
2574
|
await preflightSecretsPush({ configPath: cfg.configPath, runner: options.secretRunner });
|
|
@@ -2421,7 +2658,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
2421
2658
|
out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
|
|
2422
2659
|
}
|
|
2423
2660
|
if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
|
|
2424
|
-
const key =
|
|
2661
|
+
const key = import_node_process7.default.env[cfg.ai.keyEnv];
|
|
2425
2662
|
if (key) {
|
|
2426
2663
|
const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
|
|
2427
2664
|
await (0, import_ai.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
|
|
@@ -2563,7 +2800,7 @@ async function resolveVaultWrite(options) {
|
|
|
2563
2800
|
}
|
|
2564
2801
|
|
|
2565
2802
|
// src/security-command-context.ts
|
|
2566
|
-
var
|
|
2803
|
+
var import_promises2 = require("readline/promises");
|
|
2567
2804
|
async function hostedSecurityContext(parsed, dependencies) {
|
|
2568
2805
|
const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
2569
2806
|
const cfg = await loadProjectConfig(configPath);
|
|
@@ -2589,7 +2826,7 @@ async function hostedSecurityContext(parsed, dependencies) {
|
|
|
2589
2826
|
async function interactiveConfirmation(message, dependencies) {
|
|
2590
2827
|
if (dependencies.confirm) return dependencies.confirm(message);
|
|
2591
2828
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
2592
|
-
const prompt = (0,
|
|
2829
|
+
const prompt = (0, import_promises2.createInterface)({ input: process.stdin, output: process.stdout });
|
|
2593
2830
|
try {
|
|
2594
2831
|
const answer = await prompt.question(`${message} [y/N] `);
|
|
2595
2832
|
return /^y(?:es)?$/i.test(answer.trim());
|
|
@@ -2719,7 +2956,7 @@ function hostedSeverity(value, flag) {
|
|
|
2719
2956
|
var import_security2 = require("@odla-ai/security");
|
|
2720
2957
|
|
|
2721
2958
|
// src/security.ts
|
|
2722
|
-
var
|
|
2959
|
+
var import_node_path7 = require("path");
|
|
2723
2960
|
var import_security = require("@odla-ai/security");
|
|
2724
2961
|
var import_node = require("@odla-ai/security/node");
|
|
2725
2962
|
async function runHostedSecurity(options) {
|
|
@@ -2731,9 +2968,9 @@ async function runHostedSecurity(options) {
|
|
|
2731
2968
|
const appId = selfAudit ? "odla-ai" : cfg.app.id;
|
|
2732
2969
|
const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
|
|
2733
2970
|
const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
|
|
2734
|
-
const target = (0,
|
|
2735
|
-
const output = (0,
|
|
2736
|
-
const outputRelative = (0,
|
|
2971
|
+
const target = (0, import_node_path7.resolve)(options.target ?? cfg?.rootDir ?? ".");
|
|
2972
|
+
const output = (0, import_node_path7.resolve)(options.out ?? (0, import_node_path7.resolve)(target, ".odla/security/hosted"));
|
|
2973
|
+
const outputRelative = (0, import_node_path7.relative)(target, output).split(import_node_path7.sep).join("/");
|
|
2737
2974
|
if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
|
|
2738
2975
|
const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
|
|
2739
2976
|
const tokenRequest = {
|
|
@@ -2745,7 +2982,7 @@ async function runHostedSecurity(options) {
|
|
|
2745
2982
|
};
|
|
2746
2983
|
const token = await injectedToken(options, tokenRequest);
|
|
2747
2984
|
const snapshot = await (0, import_node.snapshotDirectory)(target, {
|
|
2748
|
-
exclude: !outputRelative.startsWith("../") && !(0,
|
|
2985
|
+
exclude: !outputRelative.startsWith("../") && !(0, import_node_path7.isAbsolute)(outputRelative) ? [outputRelative] : []
|
|
2749
2986
|
});
|
|
2750
2987
|
const hosted = await (0, import_security.createPlatformSecurityReasoners)({
|
|
2751
2988
|
platform,
|
|
@@ -2763,7 +3000,7 @@ async function runHostedSecurity(options) {
|
|
|
2763
3000
|
});
|
|
2764
3001
|
const harness = (0, import_security.createSecurityHarness)({
|
|
2765
3002
|
profile,
|
|
2766
|
-
store: new import_node.FileRunStore((0,
|
|
3003
|
+
store: new import_node.FileRunStore((0, import_node_path7.resolve)(output, "state")),
|
|
2767
3004
|
discoveryReasoner: hosted.discoveryReasoner,
|
|
2768
3005
|
validationReasoner: hosted.validationReasoner,
|
|
2769
3006
|
policy: {
|
|
@@ -2787,7 +3024,7 @@ async function runHostedSecurity(options) {
|
|
|
2787
3024
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
2788
3025
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
2789
3026
|
if (!env || !declared.includes(env)) {
|
|
2790
|
-
const shown = (0,
|
|
3027
|
+
const shown = (0, import_node_path7.relative)(rootDir, configPath) || configPath;
|
|
2791
3028
|
throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
|
|
2792
3029
|
}
|
|
2793
3030
|
return env;
|
|
@@ -2816,7 +3053,7 @@ function printSummary(out, appId, env, run, report, output) {
|
|
|
2816
3053
|
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
3054
|
if (report.callBudget) out.log(` calls: discovery=${formatBudget(report.callBudget.discovery)} validation=${formatBudget(report.callBudget.validation)}`);
|
|
2818
3055
|
out.log(` findings: confirmed=${report.metrics.confirmed} needs_reproduction=${report.metrics.needsReproduction} candidates=${report.metrics.candidates}`);
|
|
2819
|
-
out.log(` report: ${(0,
|
|
3056
|
+
out.log(` report: ${(0, import_node_path7.resolve)(output, "REPORT.md")}`);
|
|
2820
3057
|
}
|
|
2821
3058
|
function formatBudget(usage) {
|
|
2822
3059
|
return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
|
|
@@ -3479,9 +3716,9 @@ async function securityStatus(parsed, dependencies) {
|
|
|
3479
3716
|
}
|
|
3480
3717
|
|
|
3481
3718
|
// src/skill.ts
|
|
3482
|
-
var
|
|
3719
|
+
var import_node_fs10 = require("fs");
|
|
3483
3720
|
var import_node_os = require("os");
|
|
3484
|
-
var
|
|
3721
|
+
var import_node_path8 = require("path");
|
|
3485
3722
|
var import_node_url2 = require("url");
|
|
3486
3723
|
|
|
3487
3724
|
// src/skill-adapters.ts
|
|
@@ -3542,8 +3779,8 @@ function installSkill(options = {}) {
|
|
|
3542
3779
|
const files = listFiles(sourceDir);
|
|
3543
3780
|
if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
|
|
3544
3781
|
const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
|
|
3545
|
-
const root = (0,
|
|
3546
|
-
const home = (0,
|
|
3782
|
+
const root = (0, import_node_path8.resolve)(options.dir ?? process.cwd());
|
|
3783
|
+
const home = (0, import_node_path8.resolve)(options.homeDir ?? (0, import_node_os.homedir)());
|
|
3547
3784
|
const plans = /* @__PURE__ */ new Map();
|
|
3548
3785
|
const targets = /* @__PURE__ */ new Map();
|
|
3549
3786
|
const rememberTarget = (harness, target) => {
|
|
@@ -3557,48 +3794,48 @@ function installSkill(options = {}) {
|
|
|
3557
3794
|
plans.set(target, { target, content, boundary, managedMerge });
|
|
3558
3795
|
};
|
|
3559
3796
|
const planSkillTree = (targetDir2, boundary = root) => {
|
|
3560
|
-
for (const rel of files) plan((0,
|
|
3797
|
+
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
3798
|
};
|
|
3562
3799
|
let targetDir;
|
|
3563
3800
|
if (options.global) {
|
|
3564
|
-
const claudeRoot = (0,
|
|
3565
|
-
const codexRoot = (0,
|
|
3801
|
+
const claudeRoot = (0, import_node_path8.join)(home, ".claude", "skills");
|
|
3802
|
+
const codexRoot = (0, import_node_path8.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path8.join)(home, ".codex"), "skills");
|
|
3566
3803
|
targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
|
|
3567
3804
|
for (const harness of harnesses) {
|
|
3568
3805
|
const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
|
|
3569
|
-
planSkillTree(skillRoot, harness === "claude" ? home : (0,
|
|
3806
|
+
planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path8.dirname)((0, import_node_path8.dirname)(codexRoot)));
|
|
3570
3807
|
rememberTarget(harness, skillRoot);
|
|
3571
3808
|
}
|
|
3572
3809
|
} else {
|
|
3573
|
-
const sharedRoot = (0,
|
|
3810
|
+
const sharedRoot = (0, import_node_path8.join)(root, ".agents", "skills");
|
|
3574
3811
|
planSkillTree(sharedRoot);
|
|
3575
|
-
const claudeRoot = (0,
|
|
3812
|
+
const claudeRoot = (0, import_node_path8.join)(root, ".claude", "skills");
|
|
3576
3813
|
targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
|
|
3577
3814
|
for (const harness of harnesses) rememberTarget(harness, sharedRoot);
|
|
3578
3815
|
if (harnesses.includes("claude")) {
|
|
3579
3816
|
for (const skill of skillNames(files)) {
|
|
3580
|
-
const canonical = (0,
|
|
3581
|
-
plan((0,
|
|
3817
|
+
const canonical = (0, import_node_fs10.readFileSync)((0, import_node_path8.join)(sourceDir, skill, "SKILL.md"), "utf8");
|
|
3818
|
+
plan((0, import_node_path8.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
|
|
3582
3819
|
}
|
|
3583
3820
|
rememberTarget("claude", claudeRoot);
|
|
3584
3821
|
}
|
|
3585
3822
|
if (harnesses.includes("cursor")) {
|
|
3586
|
-
const cursorRule = (0,
|
|
3823
|
+
const cursorRule = (0, import_node_path8.join)(root, ".cursor", "rules", "odla.mdc");
|
|
3587
3824
|
plan(cursorRule, CURSOR_RULE);
|
|
3588
3825
|
rememberTarget("cursor", cursorRule);
|
|
3589
3826
|
}
|
|
3590
3827
|
if (harnesses.includes("agents")) {
|
|
3591
|
-
const agentsFile = (0,
|
|
3828
|
+
const agentsFile = (0, import_node_path8.join)(root, "AGENTS.md");
|
|
3592
3829
|
plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
3593
3830
|
rememberTarget("agents", agentsFile);
|
|
3594
3831
|
}
|
|
3595
3832
|
if (harnesses.includes("copilot")) {
|
|
3596
|
-
const copilotFile = (0,
|
|
3833
|
+
const copilotFile = (0, import_node_path8.join)(root, ".github", "copilot-instructions.md");
|
|
3597
3834
|
plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
3598
3835
|
rememberTarget("copilot", copilotFile);
|
|
3599
3836
|
}
|
|
3600
3837
|
if (harnesses.includes("gemini")) {
|
|
3601
|
-
const geminiFile = (0,
|
|
3838
|
+
const geminiFile = (0, import_node_path8.join)(root, "GEMINI.md");
|
|
3602
3839
|
plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
3603
3840
|
rememberTarget("gemini", geminiFile);
|
|
3604
3841
|
}
|
|
@@ -3612,11 +3849,11 @@ function installSkill(options = {}) {
|
|
|
3612
3849
|
conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
|
|
3613
3850
|
continue;
|
|
3614
3851
|
}
|
|
3615
|
-
if (!(0,
|
|
3852
|
+
if (!(0, import_node_fs10.existsSync)(file.target)) {
|
|
3616
3853
|
writtenPaths.add(file.target);
|
|
3617
3854
|
continue;
|
|
3618
3855
|
}
|
|
3619
|
-
const current = (0,
|
|
3856
|
+
const current = (0, import_node_fs10.readFileSync)(file.target, "utf8");
|
|
3620
3857
|
if (current === file.content) {
|
|
3621
3858
|
unchangedPaths.add(file.target);
|
|
3622
3859
|
} else if (file.managedMerge || options.force) {
|
|
@@ -3633,9 +3870,9 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
3633
3870
|
);
|
|
3634
3871
|
}
|
|
3635
3872
|
for (const file of plans.values()) {
|
|
3636
|
-
if (!(0,
|
|
3637
|
-
(0,
|
|
3638
|
-
(0,
|
|
3873
|
+
if (!(0, import_node_fs10.existsSync)(file.target) || (0, import_node_fs10.readFileSync)(file.target, "utf8") !== file.content) {
|
|
3874
|
+
(0, import_node_fs10.mkdirSync)((0, import_node_path8.dirname)(file.target), { recursive: true });
|
|
3875
|
+
(0, import_node_fs10.writeFileSync)(file.target, file.content);
|
|
3639
3876
|
}
|
|
3640
3877
|
}
|
|
3641
3878
|
const skills = skillNames(files);
|
|
@@ -3654,7 +3891,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
3654
3891
|
};
|
|
3655
3892
|
}
|
|
3656
3893
|
function pathsUnder(root, paths) {
|
|
3657
|
-
return [...paths].map((path) => (0,
|
|
3894
|
+
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
3895
|
}
|
|
3659
3896
|
function normalizeHarnesses(values, global) {
|
|
3660
3897
|
const requested = values?.length ? values : ["claude"];
|
|
@@ -3676,9 +3913,9 @@ function normalizeHarnesses(values, global) {
|
|
|
3676
3913
|
function managedFileContent(path, block, force, boundary) {
|
|
3677
3914
|
const symlink = symlinkedComponent(boundary, path);
|
|
3678
3915
|
if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
|
|
3679
|
-
if (!(0,
|
|
3916
|
+
if (!(0, import_node_fs10.existsSync)(path)) return `${block}
|
|
3680
3917
|
`;
|
|
3681
|
-
const current = (0,
|
|
3918
|
+
const current = (0, import_node_fs10.readFileSync)(path, "utf8");
|
|
3682
3919
|
const start = "<!-- odla-ai agent setup:start -->";
|
|
3683
3920
|
const end = "<!-- odla-ai agent setup:end -->";
|
|
3684
3921
|
const startAt = current.indexOf(start);
|
|
@@ -3699,15 +3936,15 @@ function managedFileContent(path, block, force, boundary) {
|
|
|
3699
3936
|
return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
|
|
3700
3937
|
}
|
|
3701
3938
|
function symlinkedComponent(boundary, target) {
|
|
3702
|
-
const rel = (0,
|
|
3703
|
-
if (rel === ".." || rel.startsWith(`..${
|
|
3939
|
+
const rel = (0, import_node_path8.relative)(boundary, target);
|
|
3940
|
+
if (rel === ".." || rel.startsWith(`..${import_node_path8.sep}`) || (0, import_node_path8.isAbsolute)(rel)) {
|
|
3704
3941
|
throw new Error(`agent setup target escapes its install root: ${target}`);
|
|
3705
3942
|
}
|
|
3706
3943
|
let current = boundary;
|
|
3707
|
-
for (const part of rel.split(
|
|
3708
|
-
current = (0,
|
|
3944
|
+
for (const part of rel.split(import_node_path8.sep).filter(Boolean)) {
|
|
3945
|
+
current = (0, import_node_path8.join)(current, part);
|
|
3709
3946
|
try {
|
|
3710
|
-
if ((0,
|
|
3947
|
+
if ((0, import_node_fs10.lstatSync)(current).isSymbolicLink()) return current;
|
|
3711
3948
|
} catch (error) {
|
|
3712
3949
|
if (error.code !== "ENOENT") throw error;
|
|
3713
3950
|
}
|
|
@@ -3718,13 +3955,13 @@ function skillNames(files) {
|
|
|
3718
3955
|
return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
|
|
3719
3956
|
}
|
|
3720
3957
|
function listFiles(dir) {
|
|
3721
|
-
if (!(0,
|
|
3958
|
+
if (!(0, import_node_fs10.existsSync)(dir)) return [];
|
|
3722
3959
|
const results = [];
|
|
3723
3960
|
const walk = (current) => {
|
|
3724
|
-
for (const entry of (0,
|
|
3725
|
-
const path = (0,
|
|
3961
|
+
for (const entry of (0, import_node_fs10.readdirSync)(current, { withFileTypes: true })) {
|
|
3962
|
+
const path = (0, import_node_path8.join)(current, entry.name);
|
|
3726
3963
|
if (entry.isDirectory()) walk(path);
|
|
3727
|
-
else results.push((0,
|
|
3964
|
+
else results.push((0, import_node_path8.relative)(dir, path));
|
|
3728
3965
|
}
|
|
3729
3966
|
};
|
|
3730
3967
|
walk(dir);
|
|
@@ -3775,7 +4012,7 @@ async function smoke(options) {
|
|
|
3775
4012
|
);
|
|
3776
4013
|
const status = await readCalendarStatus({ platform: cfg.platformUrl, appId: cfg.app.id, env, token, fetch: doFetch });
|
|
3777
4014
|
assertCalendarHealthy(status, calendarServiceConfig(cfg, env));
|
|
3778
|
-
out.log(` calendar: ${status.status},
|
|
4015
|
+
out.log(` calendar: ${status.status}, bookable (booking \u2192 ${status.bookingCalendarId ?? "primary"})`);
|
|
3779
4016
|
}
|
|
3780
4017
|
const expectedSchema = await resolveDataExport(cfg, cfg.db?.schema, ["schema", "SCHEMA"]);
|
|
3781
4018
|
const expectedEntities = serializedEntities(expectedSchema);
|
|
@@ -3798,14 +4035,6 @@ async function smoke(options) {
|
|
|
3798
4035
|
} else {
|
|
3799
4036
|
out.log(` aggregate: skipped (schema has no entities)`);
|
|
3800
4037
|
}
|
|
3801
|
-
if (cfg.services.includes("calendar")) {
|
|
3802
|
-
const bookings = await postJson2(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/aggregate`, entry.dbKey, {
|
|
3803
|
-
ns: "$bookings",
|
|
3804
|
-
aggregate: { count: true }
|
|
3805
|
-
});
|
|
3806
|
-
const count = bookings.aggregate?.count;
|
|
3807
|
-
out.log(` bookings: ${String(count ?? "ok")}`);
|
|
3808
|
-
}
|
|
3809
4038
|
out.log("ok");
|
|
3810
4039
|
}
|
|
3811
4040
|
function assertCalendarHealthy(status, expected) {
|
|
@@ -3813,14 +4042,11 @@ function assertCalendarHealthy(status, expected) {
|
|
|
3813
4042
|
if (status.status !== "healthy") {
|
|
3814
4043
|
throw new Error(`calendar connection is ${status.status}${status.error?.message ? `: ${status.error.message}` : ""}`);
|
|
3815
4044
|
}
|
|
3816
|
-
if (status.
|
|
3817
|
-
const
|
|
3818
|
-
if (!
|
|
3819
|
-
const missing = expected.
|
|
4045
|
+
if (!status.writable) throw new Error('calendar grant does not cover booking writes; run "odla-ai calendar connect" to re-consent');
|
|
4046
|
+
const hasEventsScope = status.grantedScopes.some((scope) => scope === GOOGLE_CALENDAR_EVENTS_SCOPE);
|
|
4047
|
+
if (!hasEventsScope) throw new Error("calendar connection is missing calendar.events consent");
|
|
4048
|
+
const missing = expected.availabilityCalendars.filter((id) => !status.calendars.includes(id));
|
|
3820
4049
|
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
4050
|
}
|
|
3825
4051
|
async function getJson(doFetch, url, bearer) {
|
|
3826
4052
|
const res = await doFetch(url, {
|
|
@@ -3852,6 +4078,9 @@ async function safeText4(res) {
|
|
|
3852
4078
|
}
|
|
3853
4079
|
|
|
3854
4080
|
// src/cli.ts
|
|
4081
|
+
function exitCodeFor(err) {
|
|
4082
|
+
return err?.code === "handshake_pending" ? 75 : 1;
|
|
4083
|
+
}
|
|
3855
4084
|
async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
3856
4085
|
const parsed = parseArgv(argv);
|
|
3857
4086
|
const command = parsed.positionals[0] ?? "help";
|
|
@@ -3904,6 +4133,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
3904
4133
|
await adminCommand(parsed);
|
|
3905
4134
|
return;
|
|
3906
4135
|
}
|
|
4136
|
+
if (command === "app") {
|
|
4137
|
+
await appCommand(parsed, dependencies);
|
|
4138
|
+
return;
|
|
4139
|
+
}
|
|
3907
4140
|
if (command === "security") {
|
|
3908
4141
|
await securityCommand(parsed, dependencies);
|
|
3909
4142
|
return;
|
|
@@ -4000,6 +4233,7 @@ async function provisionCommand(parsed, dependencies) {
|
|
|
4000
4233
|
"token",
|
|
4001
4234
|
"email",
|
|
4002
4235
|
"open",
|
|
4236
|
+
"wait",
|
|
4003
4237
|
"yes"
|
|
4004
4238
|
], 1);
|
|
4005
4239
|
const writeDevVars2 = parsed.options["write-dev-vars"];
|
|
@@ -4014,6 +4248,7 @@ async function provisionCommand(parsed, dependencies) {
|
|
|
4014
4248
|
token: stringOpt(parsed.options.token),
|
|
4015
4249
|
email: stringOpt(parsed.options.email),
|
|
4016
4250
|
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
4251
|
+
wait: numberOpt(parsed.options.wait, "--wait"),
|
|
4017
4252
|
yes: parsed.options.yes === true,
|
|
4018
4253
|
fetch: dependencies.fetch,
|
|
4019
4254
|
openApprovalUrl: dependencies.openUrl,
|
|
@@ -4023,7 +4258,7 @@ async function provisionCommand(parsed, dependencies) {
|
|
|
4023
4258
|
}
|
|
4024
4259
|
async function calendarCommand(parsed, dependencies) {
|
|
4025
4260
|
const sub = parsed.positionals[1];
|
|
4026
|
-
if (sub !== "status" && sub !== "calendars" && sub !== "connect" && sub !== "
|
|
4261
|
+
if (sub !== "status" && sub !== "calendars" && sub !== "connect" && sub !== "disconnect") {
|
|
4027
4262
|
throw new Error(`unknown calendar subcommand "${sub ?? ""}". Try "odla-ai calendar status --env dev".`);
|
|
4028
4263
|
}
|
|
4029
4264
|
assertArgs(parsed, ["config", "env", "json", "token", "email", "open", "yes"], 2);
|
|
@@ -4044,26 +4279,25 @@ async function calendarCommand(parsed, dependencies) {
|
|
|
4044
4279
|
if (sub === "status") await calendarStatus(options);
|
|
4045
4280
|
else if (sub === "calendars") await calendarCalendars(options);
|
|
4046
4281
|
else if (sub === "connect") await calendarConnect(options);
|
|
4047
|
-
else if (sub === "resync") await calendarResync(options);
|
|
4048
4282
|
else await calendarDisconnect(options);
|
|
4049
4283
|
}
|
|
4050
4284
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4051
4285
|
0 && (module.exports = {
|
|
4052
4286
|
AGENT_HARNESSES,
|
|
4053
4287
|
CAPABILITIES,
|
|
4054
|
-
|
|
4288
|
+
GOOGLE_CALENDAR_EVENTS_SCOPE,
|
|
4055
4289
|
SYSTEM_AI_PURPOSES,
|
|
4056
4290
|
adminAi,
|
|
4057
4291
|
calendarBookingPageUrl,
|
|
4058
4292
|
calendarCalendars,
|
|
4059
4293
|
calendarConnect,
|
|
4060
4294
|
calendarDisconnect,
|
|
4061
|
-
calendarResync,
|
|
4062
4295
|
calendarServiceConfig,
|
|
4063
4296
|
calendarStatus,
|
|
4064
4297
|
connectGitHubSecuritySource,
|
|
4065
4298
|
disconnectGitHubSecuritySource,
|
|
4066
4299
|
doctor,
|
|
4300
|
+
exitCodeFor,
|
|
4067
4301
|
followHostedSecurityJob,
|
|
4068
4302
|
getHostedSecurityIntent,
|
|
4069
4303
|
getHostedSecurityJob,
|