@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/bin.cjs
CHANGED
|
@@ -28,35 +28,16 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
|
|
|
28
28
|
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
29
29
|
|
|
30
30
|
// src/admin-ai.ts
|
|
31
|
-
var
|
|
31
|
+
var import_node_process6 = __toESM(require("process"), 1);
|
|
32
32
|
|
|
33
33
|
// src/token.ts
|
|
34
34
|
var import_db = require("@odla-ai/db");
|
|
35
|
-
var
|
|
35
|
+
var import_node_process3 = __toESM(require("process"), 1);
|
|
36
36
|
|
|
37
|
-
// src/
|
|
38
|
-
var
|
|
37
|
+
// src/handshake-state.ts
|
|
38
|
+
var import_node_fs2 = require("fs");
|
|
39
|
+
var import_node_path2 = require("path");
|
|
39
40
|
var import_node_process = __toESM(require("process"), 1);
|
|
40
|
-
async function openUrl(url, options = {}) {
|
|
41
|
-
const command = openerFor(options.platform ?? import_node_process.default.platform);
|
|
42
|
-
const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
|
|
43
|
-
await new Promise((resolve7, reject) => {
|
|
44
|
-
const child = doSpawn(command.cmd, [...command.args, url], {
|
|
45
|
-
stdio: "ignore",
|
|
46
|
-
detached: true
|
|
47
|
-
});
|
|
48
|
-
child.once("error", reject);
|
|
49
|
-
child.once("spawn", () => {
|
|
50
|
-
child.unref();
|
|
51
|
-
resolve7();
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
function openerFor(platform) {
|
|
56
|
-
if (platform === "darwin") return { cmd: "open", args: [] };
|
|
57
|
-
if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
|
|
58
|
-
return { cmd: "xdg-open", args: [] };
|
|
59
|
-
}
|
|
60
41
|
|
|
61
42
|
// src/local.ts
|
|
62
43
|
var import_node_fs = require("fs");
|
|
@@ -184,54 +165,191 @@ function displayPath(path, rootDir = process.cwd()) {
|
|
|
184
165
|
return rel && !rel.startsWith("..") ? rel : path;
|
|
185
166
|
}
|
|
186
167
|
|
|
168
|
+
// src/handshake-state.ts
|
|
169
|
+
function handshakeFile(cfg) {
|
|
170
|
+
return (0, import_node_path2.join)((0, import_node_path2.dirname)(cfg.local.tokenFile), "handshake.local.json");
|
|
171
|
+
}
|
|
172
|
+
var RESUME_MARGIN_MS = 5e3;
|
|
173
|
+
function readPendingHandshake(path, platform, email) {
|
|
174
|
+
const pending = readJsonFile(path);
|
|
175
|
+
if (!pending || pending.platform !== platform || pending.email !== email) return null;
|
|
176
|
+
if (typeof pending.userCode !== "string" || typeof pending.deviceCode !== "string" || typeof pending.approvalUrl !== "string") return null;
|
|
177
|
+
if (typeof pending.expiresAt !== "number" || pending.expiresAt <= Date.now() + RESUME_MARGIN_MS) return null;
|
|
178
|
+
return { interval: 3, ...pending };
|
|
179
|
+
}
|
|
180
|
+
function writePendingHandshake(path, pending) {
|
|
181
|
+
writePrivateJson(path, pending);
|
|
182
|
+
}
|
|
183
|
+
function clearPendingHandshake(path) {
|
|
184
|
+
(0, import_node_fs2.rmSync)(path, { force: true });
|
|
185
|
+
}
|
|
186
|
+
function minutesLeft(expiresAt) {
|
|
187
|
+
return Math.max(1, Math.round((expiresAt - Date.now()) / 6e4));
|
|
188
|
+
}
|
|
189
|
+
function approvalHint(pending) {
|
|
190
|
+
return `approve code ${pending.userCode} at ${pending.approvalUrl} (${minutesLeft(pending.expiresAt)}m left)`;
|
|
191
|
+
}
|
|
192
|
+
function approvalReminder(out, pending, periodMs = 3e4) {
|
|
193
|
+
const timer = setInterval(() => out.log(`auth: still waiting \u2014 ${approvalHint(pending)}`), periodMs);
|
|
194
|
+
timer.unref?.();
|
|
195
|
+
return () => clearInterval(timer);
|
|
196
|
+
}
|
|
197
|
+
function handshakeWaitMs(waitSeconds, interactive = import_node_process.default.stdout.isTTY === true) {
|
|
198
|
+
if (waitSeconds !== void 0) return waitSeconds * 1e3;
|
|
199
|
+
return interactive ? void 0 : 9e4;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/open.ts
|
|
203
|
+
var import_node_child_process = require("child_process");
|
|
204
|
+
var import_node_process2 = __toESM(require("process"), 1);
|
|
205
|
+
async function openUrl(url, options = {}) {
|
|
206
|
+
const command = openerFor(options.platform ?? import_node_process2.default.platform);
|
|
207
|
+
const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
|
|
208
|
+
await new Promise((resolve7, reject) => {
|
|
209
|
+
const child = doSpawn(command.cmd, [...command.args, url], {
|
|
210
|
+
stdio: "ignore",
|
|
211
|
+
detached: true
|
|
212
|
+
});
|
|
213
|
+
child.once("error", reject);
|
|
214
|
+
child.once("spawn", () => {
|
|
215
|
+
child.unref();
|
|
216
|
+
resolve7();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
function openerFor(platform) {
|
|
221
|
+
if (platform === "darwin") return { cmd: "open", args: [] };
|
|
222
|
+
if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
|
|
223
|
+
return { cmd: "xdg-open", args: [] };
|
|
224
|
+
}
|
|
225
|
+
|
|
187
226
|
// src/token.ts
|
|
188
227
|
async function getDeveloperToken(cfg, options, doFetch, out) {
|
|
189
228
|
if (options.token) return options.token;
|
|
190
229
|
const audience = platformAudience(cfg.platformUrl);
|
|
191
|
-
if (
|
|
192
|
-
const declared =
|
|
230
|
+
if (import_node_process3.default.env.ODLA_DEV_TOKEN) {
|
|
231
|
+
const declared = import_node_process3.default.env.ODLA_DEV_TOKEN_AUDIENCE;
|
|
193
232
|
if (declared) {
|
|
194
233
|
if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
|
|
195
234
|
} else if (audience !== "https://odla.ai") {
|
|
196
235
|
throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
|
|
197
236
|
}
|
|
198
|
-
return
|
|
237
|
+
return import_node_process3.default.env.ODLA_DEV_TOKEN;
|
|
199
238
|
}
|
|
200
239
|
const cached = readJsonFile(cfg.local.tokenFile);
|
|
201
240
|
if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
202
241
|
out.log(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
203
242
|
return cached.token;
|
|
204
243
|
}
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
out.log(`auth: opened browser for approval${browser.mode === "auto" ? " (auto)" : ""}`);
|
|
220
|
-
} catch (err) {
|
|
221
|
-
out.log(`auth: could not open browser (${err instanceof Error ? err.message : String(err)})`);
|
|
222
|
-
}
|
|
223
|
-
} else if (browser.reason) {
|
|
224
|
-
out.log(`auth: browser launch skipped (${browser.reason})`);
|
|
225
|
-
}
|
|
226
|
-
out.log("");
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
writePrivateJson(cfg.local.tokenFile, { platform: audience, email, token, expiresAt });
|
|
244
|
+
const ctx = {
|
|
245
|
+
cfg,
|
|
246
|
+
options,
|
|
247
|
+
doFetch,
|
|
248
|
+
out,
|
|
249
|
+
audience,
|
|
250
|
+
browser: approvalBrowser(options),
|
|
251
|
+
email: handshakeEmail(options.email, cached?.platform === audience ? cached.email : void 0),
|
|
252
|
+
pendingFile: handshakeFile(cfg)
|
|
253
|
+
};
|
|
254
|
+
const waitMs = handshakeWaitMs(options.wait);
|
|
255
|
+
const { token, expiresAt } = await resumePendingHandshake(ctx, waitMs) ?? await freshHandshake(ctx, waitMs);
|
|
256
|
+
clearPendingHandshake(ctx.pendingFile);
|
|
257
|
+
writePrivateJson(cfg.local.tokenFile, { platform: audience, email: ctx.email, token, expiresAt });
|
|
230
258
|
out.log(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
231
259
|
return token;
|
|
232
260
|
}
|
|
261
|
+
async function resumePendingHandshake(ctx, waitMs) {
|
|
262
|
+
const pending = readPendingHandshake(ctx.pendingFile, ctx.audience, ctx.email);
|
|
263
|
+
if (!pending) return null;
|
|
264
|
+
ctx.out.log("");
|
|
265
|
+
ctx.out.log(`auth: resuming pending handshake \u2014 ${approvalHint(pending)}`);
|
|
266
|
+
await launchApproval(ctx, pending.approvalUrl);
|
|
267
|
+
ctx.out.log("");
|
|
268
|
+
const stopReminder = approvalReminder(ctx.out, pending);
|
|
269
|
+
try {
|
|
270
|
+
return await (0, import_db.collectToken)({
|
|
271
|
+
endpoint: ctx.cfg.platformUrl,
|
|
272
|
+
deviceCode: pending.deviceCode,
|
|
273
|
+
expiresAt: pending.expiresAt,
|
|
274
|
+
interval: pending.interval,
|
|
275
|
+
waitMs,
|
|
276
|
+
fetch: ctx.doFetch
|
|
277
|
+
});
|
|
278
|
+
} catch (err) {
|
|
279
|
+
const code = err instanceof import_db.OdlaError ? err.code : void 0;
|
|
280
|
+
if (code === "handshake_pending") throw stillPending(pending, ctx.email);
|
|
281
|
+
if (code === "handshake_expired" || code === "handshake_timeout") {
|
|
282
|
+
clearPendingHandshake(ctx.pendingFile);
|
|
283
|
+
ctx.out.log("auth: pending handshake lapsed unapproved; starting a fresh one");
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
if (code === "handshake_denied") clearPendingHandshake(ctx.pendingFile);
|
|
287
|
+
throw err;
|
|
288
|
+
} finally {
|
|
289
|
+
stopReminder();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async function freshHandshake(ctx, waitMs) {
|
|
293
|
+
let started;
|
|
294
|
+
let stopReminder;
|
|
295
|
+
try {
|
|
296
|
+
return await (0, import_db.requestToken)({
|
|
297
|
+
endpoint: ctx.cfg.platformUrl,
|
|
298
|
+
email: ctx.email,
|
|
299
|
+
label: `${ctx.cfg.app.id} provisioner`,
|
|
300
|
+
fetch: ctx.doFetch,
|
|
301
|
+
waitMs,
|
|
302
|
+
onCode: async ({ userCode, deviceCode, expiresIn, interval, verificationUriComplete }) => {
|
|
303
|
+
const approvalUrl = verificationUriComplete ?? handshakeUrl(ctx.cfg.platformUrl, userCode);
|
|
304
|
+
started = {
|
|
305
|
+
platform: ctx.audience,
|
|
306
|
+
email: ctx.email,
|
|
307
|
+
userCode,
|
|
308
|
+
deviceCode,
|
|
309
|
+
approvalUrl,
|
|
310
|
+
interval,
|
|
311
|
+
expiresAt: Date.now() + expiresIn * 1e3
|
|
312
|
+
};
|
|
313
|
+
writePendingHandshake(ctx.pendingFile, started);
|
|
314
|
+
ctx.out.log("");
|
|
315
|
+
ctx.out.log(`Review, then ${approvalHint(started)}.`);
|
|
316
|
+
await launchApproval(ctx, approvalUrl);
|
|
317
|
+
ctx.out.log("");
|
|
318
|
+
stopReminder = approvalReminder(ctx.out, started);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
} catch (err) {
|
|
322
|
+
const code = err instanceof import_db.OdlaError ? err.code : void 0;
|
|
323
|
+
if (code === "handshake_pending" && started) throw stillPending(started, ctx.email);
|
|
324
|
+
if (code === "handshake_denied" || code === "handshake_expired" || code === "handshake_timeout") {
|
|
325
|
+
clearPendingHandshake(ctx.pendingFile);
|
|
326
|
+
}
|
|
327
|
+
throw err;
|
|
328
|
+
} finally {
|
|
329
|
+
stopReminder?.();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
async function launchApproval(ctx, approvalUrl) {
|
|
333
|
+
if (ctx.browser.open) {
|
|
334
|
+
try {
|
|
335
|
+
await (ctx.options.openApprovalUrl ?? openUrl)(approvalUrl);
|
|
336
|
+
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`);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
ctx.out.log(`auth: could not open browser (${err instanceof Error ? err.message : String(err)})`);
|
|
339
|
+
}
|
|
340
|
+
} else if (ctx.browser.reason) {
|
|
341
|
+
ctx.out.log(`auth: browser launch skipped (${ctx.browser.reason}) \u2014 show the human the URL above`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function stillPending(pending, email) {
|
|
345
|
+
return new import_db.OdlaError(
|
|
346
|
+
"handshake_pending",
|
|
347
|
+
`handshake still pending \u2014 ask ${email} to ${approvalHint(pending)}, then re-run this command; it resumes the same handshake and collects the token`,
|
|
348
|
+
{ retryable: true }
|
|
349
|
+
);
|
|
350
|
+
}
|
|
233
351
|
function handshakeEmail(value, cached) {
|
|
234
|
-
const email = (value ??
|
|
352
|
+
const email = (value ?? import_node_process3.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
|
|
235
353
|
if (email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
236
354
|
throw new Error("a fresh odla handshake requires --email <account> or ODLA_USER_EMAIL");
|
|
237
355
|
}
|
|
@@ -240,10 +358,11 @@ function handshakeEmail(value, cached) {
|
|
|
240
358
|
function approvalBrowser(options, host = {}) {
|
|
241
359
|
if (options.open === true) return { open: true, mode: "forced" };
|
|
242
360
|
if (options.open === false) return { open: false, reason: "disabled by --no-open" };
|
|
243
|
-
const env = host.env ??
|
|
361
|
+
const env = host.env ?? import_node_process3.default.env;
|
|
362
|
+
if (env.VITEST || env.NODE_ENV === "test") return { open: false, reason: "test environment" };
|
|
244
363
|
if (env.CI) return { open: false, reason: "CI environment" };
|
|
245
364
|
if (env.SSH_CONNECTION || env.SSH_TTY) return { open: false, reason: "SSH session; pass --open to force" };
|
|
246
|
-
const platform = host.platform ??
|
|
365
|
+
const platform = host.platform ?? import_node_process3.default.platform;
|
|
247
366
|
if (platform !== "darwin" && platform !== "win32" && !env.DISPLAY && !env.WAYLAND_DISPLAY) {
|
|
248
367
|
return { open: false, reason: "no graphical display; pass --open to force" };
|
|
249
368
|
}
|
|
@@ -272,12 +391,12 @@ function platformAudience(value) {
|
|
|
272
391
|
}
|
|
273
392
|
|
|
274
393
|
// src/secret-input.ts
|
|
275
|
-
var
|
|
394
|
+
var import_node_process4 = __toESM(require("process"), 1);
|
|
276
395
|
var MAX_BYTES = 64 * 1024;
|
|
277
396
|
async function secretInputValue(options, kind = "credential") {
|
|
278
397
|
if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
|
|
279
398
|
let value;
|
|
280
|
-
if (options.fromEnv) value =
|
|
399
|
+
if (options.fromEnv) value = import_node_process4.default.env[options.fromEnv];
|
|
281
400
|
else if (options.stdin) value = await (options.readStdin ?? (() => readSecretStream(kind)))();
|
|
282
401
|
else throw new Error(`${kind} input required: use --from-env <NAME> or --stdin; values are never accepted as arguments`);
|
|
283
402
|
value = value?.replace(/[\r\n]+$/, "");
|
|
@@ -285,7 +404,7 @@ async function secretInputValue(options, kind = "credential") {
|
|
|
285
404
|
if (new TextEncoder().encode(value).byteLength > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
|
|
286
405
|
return value;
|
|
287
406
|
}
|
|
288
|
-
async function readSecretStream(kind, stream =
|
|
407
|
+
async function readSecretStream(kind, stream = import_node_process4.default.stdin) {
|
|
289
408
|
let value = "";
|
|
290
409
|
for await (const chunk of stream) {
|
|
291
410
|
value += String(chunk);
|
|
@@ -295,8 +414,8 @@ async function readSecretStream(kind, stream = import_node_process3.default.stdi
|
|
|
295
414
|
}
|
|
296
415
|
|
|
297
416
|
// src/admin-ai-auth.ts
|
|
298
|
-
var
|
|
299
|
-
var
|
|
417
|
+
var import_node_fs3 = require("fs");
|
|
418
|
+
var import_node_process5 = __toESM(require("process"), 1);
|
|
300
419
|
var import_db2 = require("@odla-ai/db");
|
|
301
420
|
async function getScopedPlatformToken(options) {
|
|
302
421
|
return resolveAdminPlatformToken(options);
|
|
@@ -304,7 +423,7 @@ async function getScopedPlatformToken(options) {
|
|
|
304
423
|
async function resolveAdminPlatformToken(options) {
|
|
305
424
|
const audience = platformAudience(options.platform);
|
|
306
425
|
if (options.token) return options.token;
|
|
307
|
-
const fromEnv =
|
|
426
|
+
const fromEnv = import_node_process5.default.env.ODLA_ADMIN_TOKEN;
|
|
308
427
|
if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
|
|
309
428
|
return scopedToken(
|
|
310
429
|
audience,
|
|
@@ -316,7 +435,7 @@ async function resolveAdminPlatformToken(options) {
|
|
|
316
435
|
}
|
|
317
436
|
function audienceBoundEnvToken(token, platform) {
|
|
318
437
|
const audience = platformAudience(platform);
|
|
319
|
-
const declared =
|
|
438
|
+
const declared = import_node_process5.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
|
|
320
439
|
if (declared) {
|
|
321
440
|
if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
|
|
322
441
|
} else if (audience !== "https://odla.ai") {
|
|
@@ -326,7 +445,7 @@ function audienceBoundEnvToken(token, platform) {
|
|
|
326
445
|
}
|
|
327
446
|
async function scopedToken(platform, scope, options, doFetch, out) {
|
|
328
447
|
const audience = platformAudience(platform);
|
|
329
|
-
const tokenFile = options.tokenFile ?? `${
|
|
448
|
+
const tokenFile = options.tokenFile ?? `${import_node_process5.default.cwd()}/.odla/admin-token.local.json`;
|
|
330
449
|
const cache = readJsonFile(tokenFile);
|
|
331
450
|
const cached = cache?.platform === audience ? cache.tokens?.[scope] : void 0;
|
|
332
451
|
if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
@@ -350,7 +469,7 @@ async function scopedToken(platform, scope, options, doFetch, out) {
|
|
|
350
469
|
});
|
|
351
470
|
const tokens = cache?.platform === audience ? { ...cache.tokens ?? {} } : {};
|
|
352
471
|
tokens[scope] = { token, expiresAt };
|
|
353
|
-
if ((0,
|
|
472
|
+
if ((0, import_node_fs3.existsSync)(`${import_node_process5.default.cwd()}/.git`)) ensureGitignore(import_node_process5.default.cwd(), [tokenFile]);
|
|
354
473
|
writePrivateJson(tokenFile, { platform: audience, email, tokens });
|
|
355
474
|
out.log(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
|
|
356
475
|
return token;
|
|
@@ -511,7 +630,7 @@ function isRecord2(value) {
|
|
|
511
630
|
|
|
512
631
|
// src/admin-ai.ts
|
|
513
632
|
async function adminAi(options) {
|
|
514
|
-
const platform = platformAudience(options.platform ??
|
|
633
|
+
const platform = platformAudience(options.platform ?? import_node_process6.default.env.ODLA_PLATFORM ?? "https://odla.ai");
|
|
515
634
|
const doFetch = options.fetch ?? fetch;
|
|
516
635
|
const out = options.stdout ?? console;
|
|
517
636
|
const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
|
|
@@ -826,77 +945,26 @@ async function adminCommand(parsed) {
|
|
|
826
945
|
});
|
|
827
946
|
}
|
|
828
947
|
|
|
829
|
-
// src/
|
|
830
|
-
var
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
"register the app and enable configured services per environment",
|
|
834
|
-
"issue, persist, and explicitly rotate configured service credentials",
|
|
835
|
-
"write local Worker values and push deployed Worker secrets through Wrangler stdin",
|
|
836
|
-
"store tenant-vault secrets (including the Clerk webhook secret and reserved Clerk secret key) write-only from stdin or an env var",
|
|
837
|
-
"push db schema/rules and configure platform AI, auth, and deployment links",
|
|
838
|
-
"apply read-only Google calendar mirror and public booking-page config, then drive state-bound consent, status, discovery, resync, and disconnect flows",
|
|
839
|
-
"validate config offline and smoke-test a provisioned db environment",
|
|
840
|
-
"run app-attributed hosted security discovery and independent validation without provider keys",
|
|
841
|
-
"connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
|
|
842
|
-
"let admins manage system AI policy and credentials, inspect attributed usage, and read immutable admin changes through separate short-lived capability-only approvals"
|
|
843
|
-
],
|
|
844
|
-
agent: [
|
|
845
|
-
"install and import the selected odla SDKs",
|
|
846
|
-
"wrap the Worker with withObservability and choose useful telemetry",
|
|
847
|
-
"make application-specific schema, rules, auth, UI, and migration decisions",
|
|
848
|
-
"wire @odla-ai/calendar into trusted Worker code and keep the app admin key out of browsers"
|
|
849
|
-
],
|
|
850
|
-
human: [
|
|
851
|
-
"provide the existing odla account email, then sign in and explicitly review/approve the exact device code",
|
|
852
|
-
"review mirrored calendar/attendee disclosure and complete the server-issued Google consent flow",
|
|
853
|
-
"consent to production changes with --yes",
|
|
854
|
-
"request destructive credential rotation explicitly",
|
|
855
|
-
"review application semantics, security findings, releases, and merges",
|
|
856
|
-
"approve redacted-source disclosure before hosted security reasoning",
|
|
857
|
-
"approve GitHub App repository access separately from redacted-snippet disclosure"
|
|
858
|
-
],
|
|
859
|
-
studio: [
|
|
860
|
-
"view telemetry and environment state",
|
|
861
|
-
"let signed-in users inventory/revoke their own agent grants and admins audit/global-revoke them",
|
|
862
|
-
"review calendar connection, granted read scope, selected calendars, and sync health without exposing provider tokens",
|
|
863
|
-
"perform manual credential recovery when the CLI's local shown-once copy is unavailable",
|
|
864
|
-
"configure system AI purposes and view app/environment/run-attributed usage",
|
|
865
|
-
"connect/revoke GitHub security sources and inspect ref-to-SHA jobs, routes, retention, coverage, and normalized reports"
|
|
866
|
-
]
|
|
867
|
-
};
|
|
868
|
-
function printCapabilities(json = false, out = console) {
|
|
869
|
-
if (json) {
|
|
870
|
-
out.log(JSON.stringify(CAPABILITIES, null, 2));
|
|
871
|
-
return;
|
|
872
|
-
}
|
|
873
|
-
out.log("odla-ai responsibility boundary\n");
|
|
874
|
-
printGroup(out, "CLI automates", CAPABILITIES.cli);
|
|
875
|
-
printGroup(out, "Coding agent changes source", CAPABILITIES.agent);
|
|
876
|
-
printGroup(out, "Human checkpoints", CAPABILITIES.human);
|
|
877
|
-
printGroup(out, "Studio", CAPABILITIES.studio);
|
|
878
|
-
}
|
|
879
|
-
function printGroup(out, heading, items) {
|
|
880
|
-
out.log(`${heading}:`);
|
|
881
|
-
for (const item of items) out.log(` - ${item}`);
|
|
882
|
-
out.log("");
|
|
883
|
-
}
|
|
948
|
+
// src/app-export.ts
|
|
949
|
+
var import_node_fs5 = require("fs");
|
|
950
|
+
var import_node_stream = require("stream");
|
|
951
|
+
var import_promises = require("stream/promises");
|
|
884
952
|
|
|
885
953
|
// src/config.ts
|
|
886
|
-
var
|
|
887
|
-
var
|
|
954
|
+
var import_node_fs4 = require("fs");
|
|
955
|
+
var import_node_path3 = require("path");
|
|
888
956
|
var import_node_url = require("url");
|
|
889
957
|
var DEFAULT_PLATFORM = "https://odla.ai";
|
|
890
958
|
var DEFAULT_ENVS = ["dev"];
|
|
891
959
|
var DEFAULT_SERVICES = ["db", "ai"];
|
|
892
|
-
var
|
|
960
|
+
var GOOGLE_CALENDAR_EVENTS_SCOPE = "https://www.googleapis.com/auth/calendar.events";
|
|
893
961
|
async function loadProjectConfig(configPath = "odla.config.mjs") {
|
|
894
|
-
const resolved = (0,
|
|
895
|
-
if (!(0,
|
|
962
|
+
const resolved = (0, import_node_path3.resolve)(configPath);
|
|
963
|
+
if (!(0, import_node_fs4.existsSync)(resolved)) {
|
|
896
964
|
throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
|
|
897
965
|
}
|
|
898
966
|
const raw = await loadConfigModule(resolved);
|
|
899
|
-
const rootDir = (0,
|
|
967
|
+
const rootDir = (0, import_node_path3.dirname)(resolved);
|
|
900
968
|
validateRawConfig(raw, resolved);
|
|
901
969
|
const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
|
|
902
970
|
const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
|
|
@@ -904,9 +972,9 @@ async function loadProjectConfig(configPath = "odla.config.mjs") {
|
|
|
904
972
|
const services = unique(raw.services?.length ? raw.services : DEFAULT_SERVICES);
|
|
905
973
|
validateCalendarConfig(raw, envs, services, resolved);
|
|
906
974
|
const local = {
|
|
907
|
-
tokenFile: (0,
|
|
908
|
-
credentialsFile: (0,
|
|
909
|
-
devVarsFile: (0,
|
|
975
|
+
tokenFile: (0, import_node_path3.resolve)(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
|
|
976
|
+
credentialsFile: (0, import_node_path3.resolve)(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
|
|
977
|
+
devVarsFile: (0, import_node_path3.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
|
|
910
978
|
gitignore: raw.local?.gitignore ?? true
|
|
911
979
|
};
|
|
912
980
|
return {
|
|
@@ -923,9 +991,9 @@ async function loadProjectConfig(configPath = "odla.config.mjs") {
|
|
|
923
991
|
async function resolveDataExport(cfg, value, names) {
|
|
924
992
|
if (value === void 0 || value === null || value === false) return void 0;
|
|
925
993
|
if (typeof value !== "string") return value;
|
|
926
|
-
const target = (0,
|
|
994
|
+
const target = (0, import_node_path3.isAbsolute)(value) ? value : (0, import_node_path3.resolve)(cfg.rootDir, value);
|
|
927
995
|
if (target.endsWith(".json")) {
|
|
928
|
-
return JSON.parse((0,
|
|
996
|
+
return JSON.parse((0, import_node_fs4.readFileSync)(target, "utf8"));
|
|
929
997
|
}
|
|
930
998
|
const mod = await import((0, import_node_url.pathToFileURL)(target).href);
|
|
931
999
|
for (const name of names) {
|
|
@@ -952,16 +1020,14 @@ function calendarServiceConfig(cfg, env) {
|
|
|
952
1020
|
if (!cfg.envs.includes(env)) throw new Error(`calendar env "${env}" is not declared in config envs`);
|
|
953
1021
|
const google = cfg.calendar?.google;
|
|
954
1022
|
if (!google) throw new Error("calendar.google is required when the calendar service is enabled");
|
|
1023
|
+
const availability = unique(
|
|
1024
|
+
(google.availabilityCalendars?.[env] ?? google.calendars?.[env]).map((id) => id.trim())
|
|
1025
|
+
);
|
|
955
1026
|
return {
|
|
956
1027
|
provider: "google",
|
|
957
|
-
access: "
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
organizerSelf: google.match?.organizerSelf ?? true,
|
|
961
|
-
requireAttendees: google.match?.requireAttendees ?? true,
|
|
962
|
-
...google.match?.summaryPrefix !== void 0 ? { summaryPrefix: google.match.summaryPrefix.trim() } : {}
|
|
963
|
-
},
|
|
964
|
-
attendeePolicy: google.attendeePolicy ?? "full"
|
|
1028
|
+
access: "book",
|
|
1029
|
+
bookingCalendarId: google.bookingCalendar?.[env]?.trim() ?? availability[0],
|
|
1030
|
+
availabilityCalendars: availability
|
|
965
1031
|
};
|
|
966
1032
|
}
|
|
967
1033
|
function calendarBookingPageUrl(cfg, env) {
|
|
@@ -1010,20 +1076,39 @@ function validateCalendarConfig(cfg, envs, services, path) {
|
|
|
1010
1076
|
assertOnly(cfg.calendar, ["google"], `${path}: calendar`);
|
|
1011
1077
|
if (!isRecord4(cfg.calendar.google)) throw new Error(`${path}: calendar.google must be an object`);
|
|
1012
1078
|
const google = cfg.calendar.google;
|
|
1013
|
-
assertOnly(
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1079
|
+
assertOnly(
|
|
1080
|
+
google,
|
|
1081
|
+
["availabilityCalendars", "calendars", "bookingCalendar", "bookingPageUrl"],
|
|
1082
|
+
`${path}: calendar.google`
|
|
1083
|
+
);
|
|
1084
|
+
const availabilityKey = google.availabilityCalendars !== void 0 ? "availabilityCalendars" : google.calendars !== void 0 ? "calendars" : null;
|
|
1085
|
+
if (!availabilityKey || google.availabilityCalendars !== void 0 && google.calendars !== void 0) {
|
|
1086
|
+
throw new Error(`${path}: calendar.google requires exactly one of availabilityCalendars or calendars (legacy)`);
|
|
1087
|
+
}
|
|
1088
|
+
const availability = google[availabilityKey];
|
|
1089
|
+
if (!isRecord4(availability)) throw new Error(`${path}: calendar.google.${availabilityKey} must map env names to calendar ids`);
|
|
1090
|
+
const unknownEnv = Object.keys(availability).find((env) => !envs.includes(env));
|
|
1091
|
+
if (unknownEnv) throw new Error(`${path}: calendar.google.${availabilityKey}.${unknownEnv} is not in config envs`);
|
|
1017
1092
|
for (const env of envs) {
|
|
1018
|
-
const ids =
|
|
1093
|
+
const ids = availability[env];
|
|
1019
1094
|
if (!Array.isArray(ids) || ids.length === 0) {
|
|
1020
|
-
throw new Error(`${path}: calendar.google
|
|
1095
|
+
throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must be a non-empty array`);
|
|
1021
1096
|
}
|
|
1022
1097
|
if (ids.length > 10) {
|
|
1023
|
-
throw new Error(`${path}: calendar.google
|
|
1098
|
+
throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must contain at most 10 calendar ids`);
|
|
1024
1099
|
}
|
|
1025
1100
|
if (ids.some((id) => !safeText(id, 1024))) {
|
|
1026
|
-
throw new Error(`${path}: calendar.google
|
|
1101
|
+
throw new Error(`${path}: calendar.google.${availabilityKey}.${env} contains an invalid calendar id`);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
if (google.bookingCalendar !== void 0) {
|
|
1105
|
+
if (!isRecord4(google.bookingCalendar)) throw new Error(`${path}: calendar.google.bookingCalendar must map env names to one calendar id`);
|
|
1106
|
+
const unknownBookingEnv = Object.keys(google.bookingCalendar).find((env) => !envs.includes(env));
|
|
1107
|
+
if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingCalendar.${unknownBookingEnv} is not in config envs`);
|
|
1108
|
+
for (const [env, value] of Object.entries(google.bookingCalendar)) {
|
|
1109
|
+
if (!safeText(value, 1024)) {
|
|
1110
|
+
throw new Error(`${path}: calendar.google.bookingCalendar.${env} must be a calendar id`);
|
|
1111
|
+
}
|
|
1027
1112
|
}
|
|
1028
1113
|
}
|
|
1029
1114
|
if (google.bookingPageUrl !== void 0) {
|
|
@@ -1036,21 +1121,6 @@ function validateCalendarConfig(cfg, envs, services, path) {
|
|
|
1036
1121
|
}
|
|
1037
1122
|
}
|
|
1038
1123
|
}
|
|
1039
|
-
if (google.attendeePolicy !== void 0 && google.attendeePolicy !== "full" && google.attendeePolicy !== "hashed") {
|
|
1040
|
-
throw new Error(`${path}: calendar.google.attendeePolicy must be "full" or "hashed"`);
|
|
1041
|
-
}
|
|
1042
|
-
if (google.match !== void 0) {
|
|
1043
|
-
if (!isRecord4(google.match)) throw new Error(`${path}: calendar.google.match must be an object`);
|
|
1044
|
-
assertOnly(google.match, ["summaryPrefix", "organizerSelf", "requireAttendees"], `${path}: calendar.google.match`);
|
|
1045
|
-
if (google.match.summaryPrefix !== void 0 && !safeText(google.match.summaryPrefix, 256)) {
|
|
1046
|
-
throw new Error(`${path}: calendar.google.match.summaryPrefix must be 1-256 printable characters`);
|
|
1047
|
-
}
|
|
1048
|
-
for (const name of ["organizerSelf", "requireAttendees"]) {
|
|
1049
|
-
if (google.match[name] !== void 0 && typeof google.match[name] !== "boolean") {
|
|
1050
|
-
throw new Error(`${path}: calendar.google.match.${name} must be boolean`);
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
1124
|
if (enabled && !services.includes("db")) throw new Error(`${path}: calendar service requires the db service`);
|
|
1055
1125
|
}
|
|
1056
1126
|
function assertOnly(value, allowed, label) {
|
|
@@ -1076,7 +1146,7 @@ function validId(value) {
|
|
|
1076
1146
|
return typeof value === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value);
|
|
1077
1147
|
}
|
|
1078
1148
|
async function loadConfigModule(path) {
|
|
1079
|
-
if (path.endsWith(".json")) return JSON.parse((0,
|
|
1149
|
+
if (path.endsWith(".json")) return JSON.parse((0, import_node_fs4.readFileSync)(path, "utf8"));
|
|
1080
1150
|
const mod = await import(`${(0, import_node_url.pathToFileURL)(path).href}?t=${Date.now()}`);
|
|
1081
1151
|
const value = mod.default ?? mod.config;
|
|
1082
1152
|
if (typeof value === "function") return await value();
|
|
@@ -1089,10 +1159,187 @@ function unique(values) {
|
|
|
1089
1159
|
return [...new Set(values.filter(Boolean))];
|
|
1090
1160
|
}
|
|
1091
1161
|
|
|
1162
|
+
// src/app-export.ts
|
|
1163
|
+
async function appExport(options) {
|
|
1164
|
+
const cfg = await loadProjectConfig(options.configPath);
|
|
1165
|
+
const out = options.stdout ?? console;
|
|
1166
|
+
const doFetch = options.fetch ?? fetch;
|
|
1167
|
+
const env = options.env ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0]);
|
|
1168
|
+
if (!env || !cfg.envs.includes(env)) throw new Error(`env "${env ?? ""}" is not declared in ${options.configPath}`);
|
|
1169
|
+
const tenant = env === "prod" ? cfg.app.id : `${cfg.app.id}--${env}`;
|
|
1170
|
+
const token = await getDeveloperToken(
|
|
1171
|
+
cfg,
|
|
1172
|
+
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
1173
|
+
doFetch,
|
|
1174
|
+
out
|
|
1175
|
+
);
|
|
1176
|
+
const auth = { authorization: `Bearer ${token}` };
|
|
1177
|
+
const base = `${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenant)}`;
|
|
1178
|
+
if (options.fresh) {
|
|
1179
|
+
const res = await doFetch(`${base}/export`, { method: "POST", headers: auth });
|
|
1180
|
+
const body = await res.json().catch(() => ({}));
|
|
1181
|
+
if (!res.ok) throw new Error(`export failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? res.status}`);
|
|
1182
|
+
}
|
|
1183
|
+
const list = await doFetch(`${base}/backups`, { headers: auth });
|
|
1184
|
+
if (!list.ok) throw new Error(`couldn't list backups (${list.status})`);
|
|
1185
|
+
const { backups } = await list.json();
|
|
1186
|
+
const newest = backups[0];
|
|
1187
|
+
if (!newest) {
|
|
1188
|
+
throw new Error(
|
|
1189
|
+
`${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)`
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
const download = await doFetch(`${base}/backups/${newest.id}/download`, { headers: auth });
|
|
1193
|
+
if (!download.ok || !download.body) throw new Error(`download failed (${download.status})`);
|
|
1194
|
+
const file = options.out ?? `${tenant}-${new Date(newest.created_at).toISOString().slice(0, 10)}-tx${newest.max_tx}.jsonl.gz`;
|
|
1195
|
+
await (0, import_promises.pipeline)(import_node_stream.Readable.fromWeb(download.body), (0, import_node_fs5.createWriteStream)(file));
|
|
1196
|
+
if (options.json) out.log(JSON.stringify({ file, backup: newest }, null, 2));
|
|
1197
|
+
else {
|
|
1198
|
+
out.log(`${tenant}: wrote ${file} (${newest.bytes} bytes, ${newest.kind} snapshot at tx ${newest.max_tx})`);
|
|
1199
|
+
out.log(`sha256 ${download.headers.get("x-odla-sha256") ?? newest.sha256}`);
|
|
1200
|
+
}
|
|
1201
|
+
return { file, backup: newest };
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// src/app-lifecycle.ts
|
|
1205
|
+
async function lifecycleCall(action, options) {
|
|
1206
|
+
const cfg = await loadProjectConfig(options.configPath);
|
|
1207
|
+
const out = options.stdout ?? console;
|
|
1208
|
+
const doFetch = options.fetch ?? fetch;
|
|
1209
|
+
const token = await getDeveloperToken(
|
|
1210
|
+
cfg,
|
|
1211
|
+
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
1212
|
+
doFetch,
|
|
1213
|
+
out
|
|
1214
|
+
);
|
|
1215
|
+
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/${action}`, {
|
|
1216
|
+
method: "POST",
|
|
1217
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }
|
|
1218
|
+
});
|
|
1219
|
+
const body = await res.json().catch(() => ({}));
|
|
1220
|
+
if (!res.ok || !body.ok) {
|
|
1221
|
+
throw new Error(`${action} failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? `registry returned ${res.status}`}`);
|
|
1222
|
+
}
|
|
1223
|
+
return body;
|
|
1224
|
+
}
|
|
1225
|
+
async function appArchive(options) {
|
|
1226
|
+
if (options.yes !== true) {
|
|
1227
|
+
throw new Error(
|
|
1228
|
+
"app archive suspends EVERY environment: API keys stop working and all services refuse requests until restored. All data is retained. Pass --yes to proceed."
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
const out = options.stdout ?? console;
|
|
1232
|
+
const body = await lifecycleCall("archive", options);
|
|
1233
|
+
if (options.json) out.log(JSON.stringify(body, null, 2));
|
|
1234
|
+
else if (body.operation?.state === "noop") out.log(`${body.app?.appId}: already archived`);
|
|
1235
|
+
else out.log(`${body.app?.appId}: archived \u2014 data retained; run \`odla-ai app restore\` (or use Studio) to bring it back`);
|
|
1236
|
+
}
|
|
1237
|
+
async function appRestore(options) {
|
|
1238
|
+
const out = options.stdout ?? console;
|
|
1239
|
+
const body = await lifecycleCall("restore", options);
|
|
1240
|
+
if (options.json) out.log(JSON.stringify(body, null, 2));
|
|
1241
|
+
else if (body.operation?.state === "noop") out.log(`${body.app?.appId}: already active`);
|
|
1242
|
+
else out.log(`${body.app?.appId}: restored \u2014 every service's data plane is live again`);
|
|
1243
|
+
}
|
|
1244
|
+
async function appCommand(parsed, dependencies = {}) {
|
|
1245
|
+
const sub = parsed.positionals[1];
|
|
1246
|
+
if (sub === "export") {
|
|
1247
|
+
assertArgs(parsed, ["config", "env", "token", "email", "fresh", "out", "json"], 2);
|
|
1248
|
+
await appExport({
|
|
1249
|
+
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
1250
|
+
env: stringOpt(parsed.options.env),
|
|
1251
|
+
token: stringOpt(parsed.options.token),
|
|
1252
|
+
email: stringOpt(parsed.options.email),
|
|
1253
|
+
fresh: parsed.options.fresh === true,
|
|
1254
|
+
out: stringOpt(parsed.options.out),
|
|
1255
|
+
json: parsed.options.json === true,
|
|
1256
|
+
fetch: dependencies.fetch,
|
|
1257
|
+
stdout: dependencies.stdout
|
|
1258
|
+
});
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
if (sub !== "archive" && sub !== "restore") {
|
|
1262
|
+
throw new Error(
|
|
1263
|
+
`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.)`
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
assertArgs(parsed, ["config", "token", "email", "yes", "json"], 2);
|
|
1267
|
+
const options = {
|
|
1268
|
+
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
1269
|
+
token: stringOpt(parsed.options.token),
|
|
1270
|
+
email: stringOpt(parsed.options.email),
|
|
1271
|
+
yes: parsed.options.yes === true,
|
|
1272
|
+
json: parsed.options.json === true,
|
|
1273
|
+
fetch: dependencies.fetch,
|
|
1274
|
+
stdout: dependencies.stdout
|
|
1275
|
+
};
|
|
1276
|
+
if (sub === "archive") await appArchive(options);
|
|
1277
|
+
else await appRestore(options);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// src/capabilities.ts
|
|
1281
|
+
var CAPABILITIES = {
|
|
1282
|
+
cli: [
|
|
1283
|
+
"start an email-bound device request without accepting a password or user session token",
|
|
1284
|
+
"register the app and enable configured services per environment",
|
|
1285
|
+
"issue, persist, and explicitly rotate configured service credentials",
|
|
1286
|
+
"write local Worker values and push deployed Worker secrets through Wrangler stdin",
|
|
1287
|
+
"store tenant-vault secrets (including the Clerk webhook secret and reserved Clerk secret key) write-only from stdin or an env var",
|
|
1288
|
+
"push db schema/rules and configure platform AI, auth, and deployment links",
|
|
1289
|
+
"apply Google Calendar booking config, then drive state-bound consent, status, discovery, and disconnect flows (bookings run live through the platform proxy)",
|
|
1290
|
+
"validate config offline and smoke-test a provisioned db environment",
|
|
1291
|
+
"run app-attributed hosted security discovery and independent validation without provider keys",
|
|
1292
|
+
"connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
|
|
1293
|
+
"let admins manage system AI policy and credentials, inspect attributed usage, and read immutable admin changes through separate short-lived capability-only approvals"
|
|
1294
|
+
],
|
|
1295
|
+
agent: [
|
|
1296
|
+
"install and import the selected odla SDKs",
|
|
1297
|
+
"wrap the Worker with withObservability and choose useful telemetry",
|
|
1298
|
+
"make application-specific schema, rules, auth, UI, and migration decisions",
|
|
1299
|
+
"wire @odla-ai/calendar into trusted Worker code and keep the app admin key out of browsers"
|
|
1300
|
+
],
|
|
1301
|
+
human: [
|
|
1302
|
+
"provide the existing odla account email, then sign in and explicitly review/approve the exact device code",
|
|
1303
|
+
"review mirrored calendar/attendee disclosure and complete the server-issued Google consent flow",
|
|
1304
|
+
"consent to production changes with --yes",
|
|
1305
|
+
"request destructive credential rotation explicitly",
|
|
1306
|
+
"review application semantics, security findings, releases, and merges",
|
|
1307
|
+
"approve redacted-source disclosure before hosted security reasoning",
|
|
1308
|
+
"approve GitHub App repository access separately from redacted-snippet disclosure"
|
|
1309
|
+
],
|
|
1310
|
+
studio: [
|
|
1311
|
+
"view telemetry and environment state",
|
|
1312
|
+
"let signed-in users inventory/revoke their own agent grants and admins audit/global-revoke them",
|
|
1313
|
+
"review calendar connection, granted read scope, selected calendars, and sync health without exposing provider tokens",
|
|
1314
|
+
"perform manual credential recovery when the CLI's local shown-once copy is unavailable",
|
|
1315
|
+
"configure system AI purposes and view app/environment/run-attributed usage",
|
|
1316
|
+
"connect/revoke GitHub security sources and inspect ref-to-SHA jobs, routes, retention, coverage, and normalized reports"
|
|
1317
|
+
]
|
|
1318
|
+
};
|
|
1319
|
+
function printCapabilities(json = false, out = console) {
|
|
1320
|
+
if (json) {
|
|
1321
|
+
out.log(JSON.stringify(CAPABILITIES, null, 2));
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
out.log("odla-ai responsibility boundary\n");
|
|
1325
|
+
printGroup(out, "CLI automates", CAPABILITIES.cli);
|
|
1326
|
+
printGroup(out, "Coding agent changes source", CAPABILITIES.agent);
|
|
1327
|
+
printGroup(out, "Human checkpoints", CAPABILITIES.human);
|
|
1328
|
+
printGroup(out, "Studio", CAPABILITIES.studio);
|
|
1329
|
+
}
|
|
1330
|
+
function printGroup(out, heading, items) {
|
|
1331
|
+
out.log(`${heading}:`);
|
|
1332
|
+
for (const item of items) out.log(` - ${item}`);
|
|
1333
|
+
out.log("");
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1092
1336
|
// src/calendar-errors.ts
|
|
1093
1337
|
var PLATFORM_NOT_READY_CODES = /* @__PURE__ */ new Set([
|
|
1094
1338
|
"calendar_google_oauth_not_configured",
|
|
1095
1339
|
"calendar_token_vault_not_configured",
|
|
1340
|
+
// Current spelling (booking-era key verifier) plus the retired mirror-era
|
|
1341
|
+
// ingress code, so the CLI stays resumable against either platform version.
|
|
1342
|
+
"calendar_db_verifier_not_configured",
|
|
1096
1343
|
"calendar_db_ingress_not_configured"
|
|
1097
1344
|
]);
|
|
1098
1345
|
var CalendarRequestError = class extends Error {
|
|
@@ -1134,8 +1381,6 @@ function looksSecret(value) {
|
|
|
1134
1381
|
var CALENDAR_STATES = [
|
|
1135
1382
|
"not_connected",
|
|
1136
1383
|
"authorizing",
|
|
1137
|
-
"needs_sync",
|
|
1138
|
-
"initial_sync",
|
|
1139
1384
|
"healthy",
|
|
1140
1385
|
"degraded",
|
|
1141
1386
|
"disconnected",
|
|
@@ -1183,9 +1428,6 @@ async function pollCalendarConnection(ctx, attemptId) {
|
|
|
1183
1428
|
ctx.env
|
|
1184
1429
|
);
|
|
1185
1430
|
}
|
|
1186
|
-
async function requestCalendarResync(ctx) {
|
|
1187
|
-
return parseCalendarStatus(await calendarJson(ctx, "/resync", { method: "POST", body: "{}" }), ctx.env);
|
|
1188
|
-
}
|
|
1189
1431
|
async function requestCalendarDisconnect(ctx) {
|
|
1190
1432
|
return parseCalendarStatus(
|
|
1191
1433
|
await calendarJson(ctx, "/disconnect", { method: "POST", body: JSON.stringify({ purge: false }) }),
|
|
@@ -1196,10 +1438,8 @@ function parseCalendarStatus(raw, env) {
|
|
|
1196
1438
|
const outer = wrapped(raw, "calendar");
|
|
1197
1439
|
const value = record(outer.attempt) ?? record(outer.status) ?? outer;
|
|
1198
1440
|
const connection = record(value.connection) ?? {};
|
|
1199
|
-
const sync = record(value.sync) ?? record(connection.sync) ?? {};
|
|
1200
1441
|
const config = record(value.config) ?? record(outer.config) ?? {};
|
|
1201
1442
|
const googleConfig = record(config.google) ?? config;
|
|
1202
|
-
const watches = record(value.watches) ?? {};
|
|
1203
1443
|
const stateValue = calendarState(value.status ?? value.state ?? connection.status ?? connection.state);
|
|
1204
1444
|
if (!stateValue) {
|
|
1205
1445
|
throw new Error("calendar status returned an invalid connection state");
|
|
@@ -1209,31 +1449,29 @@ function parseCalendarStatus(raw, env) {
|
|
|
1209
1449
|
throw new Error("calendar status returned an unsupported provider");
|
|
1210
1450
|
}
|
|
1211
1451
|
const accessValue = value.access ?? connection.access ?? config.access ?? googleConfig.access;
|
|
1212
|
-
if (accessValue !== void 0 && accessValue !== "
|
|
1213
|
-
|
|
1214
|
-
if (policyValue !== void 0 && policyValue !== "full" && policyValue !== "hashed") {
|
|
1215
|
-
throw new Error("calendar status returned an invalid attendee policy");
|
|
1452
|
+
if (accessValue !== void 0 && accessValue !== "book" && accessValue !== "read") {
|
|
1453
|
+
throw new Error("calendar status returned unsupported access");
|
|
1216
1454
|
}
|
|
1217
1455
|
const errorValue = record(value.error) ?? record(connection.error);
|
|
1218
1456
|
const errorCode = textField(value.lastErrorCode, 128);
|
|
1219
1457
|
const bookingPageValue = Object.hasOwn(value, "bookingPageUrl") ? value.bookingPageUrl : Object.hasOwn(config, "bookingPageUrl") ? config.bookingPageUrl : googleConfig.bookingPageUrl;
|
|
1458
|
+
const connected = typeof (value.connected ?? connection.connected) === "boolean" ? Boolean(value.connected ?? connection.connected) : ["healthy", "degraded"].includes(stateValue);
|
|
1459
|
+
const bookingCalendarId = textField(value.bookingCalendarId ?? config.bookingCalendarId, 1024);
|
|
1220
1460
|
return {
|
|
1221
1461
|
env,
|
|
1222
1462
|
enabled: typeof value.enabled === "boolean" ? value.enabled : true,
|
|
1223
|
-
connected
|
|
1463
|
+
connected,
|
|
1464
|
+
writable: typeof value.writable === "boolean" ? value.writable : stateValue === "healthy",
|
|
1224
1465
|
provider: providerValue === "google" ? "google" : null,
|
|
1225
1466
|
status: stateValue,
|
|
1226
|
-
...accessValue
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1467
|
+
...accessValue !== void 0 ? { access: "book" } : {},
|
|
1468
|
+
...bookingCalendarId ? { bookingCalendarId } : {},
|
|
1469
|
+
calendars: calendarIds(
|
|
1470
|
+
value.availabilityCalendars ?? config.availabilityCalendars ?? value.configuredCalendars ?? value.calendars ?? config.calendars ?? googleConfig.calendars
|
|
1471
|
+
),
|
|
1230
1472
|
...optionalNullableUrl("bookingPageUrl", bookingPageValue),
|
|
1231
1473
|
grantedScopes: stringList(value.grantedScopes ?? connection.grantedScopes ?? connection.scopes),
|
|
1232
1474
|
...optionalText("attemptId", value.attemptId ?? connection.attemptId, 180),
|
|
1233
|
-
...optionalNumber("lastSuccessfulSyncAt", value.lastSuccessfulSyncAt ?? sync.lastSuccessfulSyncAt),
|
|
1234
|
-
...optionalNumber("lastFullSyncAt", value.lastFullSyncAt ?? sync.lastFullSyncAt),
|
|
1235
|
-
...optionalNumber("watchExpiresAt", value.watchExpiresAt ?? sync.watchExpiresAt ?? watches.nextExpirationAt),
|
|
1236
|
-
...optionalInteger("bookingCount", value.bookingCount ?? sync.bookingCount),
|
|
1237
1475
|
...errorValue || errorCode ? { error: {
|
|
1238
1476
|
...textField(errorValue?.code, 128) ? { code: textField(errorValue?.code, 128) } : {},
|
|
1239
1477
|
...textField(errorValue?.message, 500) ? { message: textField(errorValue?.message, 500) } : {},
|
|
@@ -1248,7 +1486,9 @@ function calendarState(value) {
|
|
|
1248
1486
|
disabled: "not_connected",
|
|
1249
1487
|
disconnected: "disconnected",
|
|
1250
1488
|
connecting: "authorizing",
|
|
1251
|
-
syncing: "
|
|
1489
|
+
syncing: "healthy",
|
|
1490
|
+
needs_sync: "healthy",
|
|
1491
|
+
initial_sync: "healthy",
|
|
1252
1492
|
ready: "healthy",
|
|
1253
1493
|
error: "failed"
|
|
1254
1494
|
};
|
|
@@ -1316,13 +1556,6 @@ function optionalText(key, value, max) {
|
|
|
1316
1556
|
const parsed = textField(value, max);
|
|
1317
1557
|
return parsed ? { [key]: parsed } : {};
|
|
1318
1558
|
}
|
|
1319
|
-
function optionalNumber(key, value) {
|
|
1320
|
-
const parsed = timestamp3(value);
|
|
1321
|
-
return parsed === void 0 ? {} : { [key]: parsed };
|
|
1322
|
-
}
|
|
1323
|
-
function optionalInteger(key, value) {
|
|
1324
|
-
return Number.isSafeInteger(value) && value >= 0 ? { [key]: value } : {};
|
|
1325
|
-
}
|
|
1326
1559
|
function optionalNullableUrl(key, value) {
|
|
1327
1560
|
if (value === null) return { [key]: null };
|
|
1328
1561
|
const parsed = textField(value, 4096);
|
|
@@ -1374,7 +1607,7 @@ async function calendarConnect(options) {
|
|
|
1374
1607
|
const page = calendarBookingPageUrl(cfg, ctx.env);
|
|
1375
1608
|
const applied = page === void 0 ? await readCalendarStatus(ctx) : await applyCalendarSettings(ctx, page);
|
|
1376
1609
|
const connectOptions = connectionOptions(options, out);
|
|
1377
|
-
return await continueConnectedCalendar(ctx, applied, connectOptions
|
|
1610
|
+
return await continueConnectedCalendar(ctx, applied, connectOptions) ?? connectWithContext(ctx, connectOptions);
|
|
1378
1611
|
}
|
|
1379
1612
|
async function applyCalendarBookingPage(ctx, bookingPageUrl, out) {
|
|
1380
1613
|
if (bookingPageUrl === void 0) return null;
|
|
@@ -1382,25 +1615,18 @@ async function applyCalendarBookingPage(ctx, bookingPageUrl, out) {
|
|
|
1382
1615
|
out.log(`${ctx.env}: calendar booking page ${bookingPageUrl ? "set" : "cleared"}`);
|
|
1383
1616
|
return status;
|
|
1384
1617
|
}
|
|
1385
|
-
async function calendarResync(options) {
|
|
1386
|
-
const { ctx, out } = await lifecycleContext(options);
|
|
1387
|
-
productionConsent(ctx.env, options.yes, "resync calendar");
|
|
1388
|
-
const status = await requestCalendarResync(ctx);
|
|
1389
|
-
out.log(`${ctx.env}: calendar resync requested (${status.status})`);
|
|
1390
|
-
return status;
|
|
1391
|
-
}
|
|
1392
1618
|
async function calendarDisconnect(options) {
|
|
1393
1619
|
if (!options.yes) throw new Error("calendar disconnect requires --yes");
|
|
1394
1620
|
const { ctx, out } = await lifecycleContext(options);
|
|
1395
1621
|
const status = await requestCalendarDisconnect(ctx);
|
|
1396
|
-
out.log(`${ctx.env}: calendar disconnected;
|
|
1622
|
+
out.log(`${ctx.env}: calendar disconnected; no calendar data was stored`);
|
|
1397
1623
|
return status;
|
|
1398
1624
|
}
|
|
1399
1625
|
async function ensureCalendarConnected(ctx, options) {
|
|
1400
1626
|
const out = options.stdout ?? console;
|
|
1401
1627
|
const current = await readCalendarStatus(ctx);
|
|
1402
1628
|
const connectOptions = connectionOptions(options, out);
|
|
1403
|
-
return await continueConnectedCalendar(ctx, current, connectOptions
|
|
1629
|
+
return await continueConnectedCalendar(ctx, current, connectOptions) ?? connectWithContext(ctx, connectOptions);
|
|
1404
1630
|
}
|
|
1405
1631
|
async function lifecycleContext(options) {
|
|
1406
1632
|
const cfg = await loadProjectConfig(options.configPath);
|
|
@@ -1436,25 +1662,20 @@ function connectionOptions(options, out) {
|
|
|
1436
1662
|
signal: options.signal
|
|
1437
1663
|
};
|
|
1438
1664
|
}
|
|
1439
|
-
async function continueConnectedCalendar(ctx, current, options
|
|
1440
|
-
if (current.status === "healthy"
|
|
1441
|
-
options.out.log(`${ctx.env}: calendar
|
|
1665
|
+
async function continueConnectedCalendar(ctx, current, options) {
|
|
1666
|
+
if (current.status === "healthy") {
|
|
1667
|
+
options.out.log(`${ctx.env}: calendar already connected (healthy)`);
|
|
1442
1668
|
return current;
|
|
1443
1669
|
}
|
|
1444
|
-
if (current.status === "degraded")
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
options.out.log(`${ctx.env}: calendar configuration needs sync`);
|
|
1448
|
-
const synced = await requestCalendarResync(ctx);
|
|
1449
|
-
options.out.log(`${ctx.env}: calendar ${synced.status.replaceAll("_", " ")}`);
|
|
1450
|
-
return synced;
|
|
1670
|
+
if (current.status === "degraded") {
|
|
1671
|
+
options.out.log(`${ctx.env}: calendar grant predates booking scopes \u2014 Google re-consent required`);
|
|
1672
|
+
return null;
|
|
1451
1673
|
}
|
|
1452
1674
|
if (current.status === "failed" && current.connected) {
|
|
1453
1675
|
const reason = current.error?.message ?? current.error?.code;
|
|
1454
|
-
throw new Error(`calendar connection failed${reason ? `: ${reason}` : ""}; inspect status and
|
|
1676
|
+
throw new Error(`calendar connection failed${reason ? `: ${reason}` : ""}; inspect status and reconnect explicitly`);
|
|
1455
1677
|
}
|
|
1456
|
-
|
|
1457
|
-
if (!waitingForExisting) return null;
|
|
1678
|
+
if (current.status !== "authorizing") return null;
|
|
1458
1679
|
const now = options.now ?? Date.now;
|
|
1459
1680
|
const deadline = now() + pollTimeout(options.pollTimeoutMs);
|
|
1460
1681
|
const wait = options.wait ?? waitForCalendarPoll;
|
|
@@ -1466,17 +1687,16 @@ async function continueConnectedCalendar(ctx, current, options, reuseDegraded) {
|
|
|
1466
1687
|
options.out.log(`${ctx.env}: calendar ${status.status.replaceAll("_", " ")}`);
|
|
1467
1688
|
prior = status.status;
|
|
1468
1689
|
}
|
|
1469
|
-
if (status.status === "healthy"
|
|
1690
|
+
if (status.status === "healthy") return status;
|
|
1470
1691
|
if (status.status === "degraded") return null;
|
|
1471
|
-
if (status.status === "needs_sync") return requestCalendarResync(ctx);
|
|
1472
1692
|
if (status.status === "failed" && status.connected) {
|
|
1473
1693
|
const reason = status.error?.message ?? status.error?.code;
|
|
1474
|
-
throw new Error(`calendar connection failed${reason ? `: ${reason}` : ""}; inspect status and
|
|
1694
|
+
throw new Error(`calendar connection failed${reason ? `: ${reason}` : ""}; inspect status and reconnect explicitly`);
|
|
1475
1695
|
}
|
|
1476
|
-
if (status.status !== "authorizing"
|
|
1696
|
+
if (status.status !== "authorizing") return null;
|
|
1477
1697
|
await wait(Math.min(2e3, Math.max(0, deadline - now())), options.signal);
|
|
1478
1698
|
}
|
|
1479
|
-
throw new Error('Existing Google Calendar authorization
|
|
1699
|
+
throw new Error('Existing Google Calendar authorization timed out; run "odla-ai calendar status" and retry');
|
|
1480
1700
|
}
|
|
1481
1701
|
async function connectWithContext(ctx, options) {
|
|
1482
1702
|
const attempt = await startCalendarConnection(ctx);
|
|
@@ -1498,14 +1718,17 @@ async function connectWithContext(ctx, options) {
|
|
|
1498
1718
|
options.out.log(`${ctx.env}: calendar ${status.status.replaceAll("_", " ")}`);
|
|
1499
1719
|
prior = status.status;
|
|
1500
1720
|
}
|
|
1501
|
-
if (status.status === "healthy"
|
|
1721
|
+
if (status.status === "healthy") return status;
|
|
1722
|
+
if (status.status === "degraded") {
|
|
1723
|
+
throw new Error("calendar connected without booking scopes; re-run connect and grant calendar access");
|
|
1724
|
+
}
|
|
1502
1725
|
if (status.status === "failed" || status.status === "disconnected" || status.status === "not_connected") {
|
|
1503
1726
|
const reason = status.error?.message ?? status.error?.code;
|
|
1504
1727
|
throw new Error(`calendar connection ${status.status}${reason ? `: ${reason}` : ""}`);
|
|
1505
1728
|
}
|
|
1506
1729
|
await wait(Math.min(2e3, Math.max(0, deadline - now())), options.signal);
|
|
1507
1730
|
}
|
|
1508
|
-
throw new Error('Google Calendar consent
|
|
1731
|
+
throw new Error('Google Calendar consent timed out; run "odla-ai calendar status" and retry connect');
|
|
1509
1732
|
}
|
|
1510
1733
|
function trustedCalendarConsentUrl(platform, value) {
|
|
1511
1734
|
const origin = platformAudience(platform);
|
|
@@ -1535,26 +1758,22 @@ function printStatus(status, json, out) {
|
|
|
1535
1758
|
return;
|
|
1536
1759
|
}
|
|
1537
1760
|
out.log(`calendar: ${status.provider ?? "none"}/${status.status} (${status.env})`);
|
|
1538
|
-
out.log(`
|
|
1539
|
-
out.log(`
|
|
1540
|
-
|
|
1541
|
-
out.log(` attendee policy: ${status.attendeePolicy}${status.attendeePolicyLocked ? " (locked)" : ""}`);
|
|
1542
|
-
}
|
|
1761
|
+
out.log(` bookable: ${status.writable ? "yes" : status.connected ? "no \u2014 reconnect to grant booking scopes" : "no \u2014 not connected"}`);
|
|
1762
|
+
if (status.bookingCalendarId) out.log(` booking calendar: ${status.bookingCalendarId}`);
|
|
1763
|
+
out.log(` availability calendars: ${status.calendars.length ? status.calendars.join(", ") : "none"}`);
|
|
1543
1764
|
if (status.bookingPageUrl !== void 0) out.log(` booking page: ${status.bookingPageUrl ?? "not configured"}`);
|
|
1544
|
-
out.log(` last sync: ${status.lastSuccessfulSyncAt === void 0 ? "never" : new Date(status.lastSuccessfulSyncAt).toISOString()}`);
|
|
1545
|
-
if (status.bookingCount !== void 0) out.log(` bookings: ${status.bookingCount}`);
|
|
1546
1765
|
if (status.error?.code || status.error?.message) out.log(` error: ${status.error.code ?? "error"}${status.error.message ? ` \u2014 ${status.error.message}` : ""}`);
|
|
1547
1766
|
}
|
|
1548
1767
|
|
|
1549
1768
|
// src/doctor-checks.ts
|
|
1550
1769
|
var import_node_child_process3 = require("child_process");
|
|
1551
|
-
var
|
|
1552
|
-
var
|
|
1770
|
+
var import_node_fs7 = require("fs");
|
|
1771
|
+
var import_node_path5 = require("path");
|
|
1553
1772
|
|
|
1554
1773
|
// src/wrangler.ts
|
|
1555
1774
|
var import_node_child_process2 = require("child_process");
|
|
1556
|
-
var
|
|
1557
|
-
var
|
|
1775
|
+
var import_node_fs6 = require("fs");
|
|
1776
|
+
var import_node_path4 = require("path");
|
|
1558
1777
|
var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
|
|
1559
1778
|
const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
1560
1779
|
let stdout = "";
|
|
@@ -1568,15 +1787,15 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) =>
|
|
|
1568
1787
|
var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
|
|
1569
1788
|
function findWranglerConfig(rootDir) {
|
|
1570
1789
|
for (const name of WRANGLER_CONFIG_FILES) {
|
|
1571
|
-
const path = (0,
|
|
1572
|
-
if ((0,
|
|
1790
|
+
const path = (0, import_node_path4.join)(rootDir, name);
|
|
1791
|
+
if ((0, import_node_fs6.existsSync)(path)) return path;
|
|
1573
1792
|
}
|
|
1574
1793
|
return null;
|
|
1575
1794
|
}
|
|
1576
1795
|
function readWranglerConfig(path) {
|
|
1577
1796
|
if (path.endsWith(".toml")) return null;
|
|
1578
1797
|
try {
|
|
1579
|
-
return JSON.parse(stripJsonComments((0,
|
|
1798
|
+
return JSON.parse(stripJsonComments((0, import_node_fs6.readFileSync)(path, "utf8")));
|
|
1580
1799
|
} catch {
|
|
1581
1800
|
return null;
|
|
1582
1801
|
}
|
|
@@ -1674,10 +1893,10 @@ function wranglerWarnings(rootDir) {
|
|
|
1674
1893
|
for (const { label, block } of blocks) {
|
|
1675
1894
|
const assets = block.assets;
|
|
1676
1895
|
if (assets?.directory) {
|
|
1677
|
-
const dir = (0,
|
|
1678
|
-
if (dir === (0,
|
|
1896
|
+
const dir = (0, import_node_path5.resolve)(rootDir, assets.directory);
|
|
1897
|
+
if (dir === (0, import_node_path5.resolve)(rootDir)) {
|
|
1679
1898
|
warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
|
|
1680
|
-
} else if ((0,
|
|
1899
|
+
} else if ((0, import_node_fs7.existsSync)((0, import_node_path5.join)(dir, "node_modules"))) {
|
|
1681
1900
|
warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
|
|
1682
1901
|
}
|
|
1683
1902
|
}
|
|
@@ -1712,13 +1931,13 @@ function o11yProjectWarnings(rootDir) {
|
|
|
1712
1931
|
warnings.push("cannot verify o11y Worker instrumentation \u2014 add a parseable wrangler.jsonc/json config");
|
|
1713
1932
|
return warnings;
|
|
1714
1933
|
}
|
|
1715
|
-
const main = typeof config.main === "string" ? (0,
|
|
1716
|
-
if (!main || !(0,
|
|
1934
|
+
const main = typeof config.main === "string" ? (0, import_node_path5.resolve)(rootDir, config.main) : null;
|
|
1935
|
+
if (!main || !(0, import_node_fs7.existsSync)(main)) {
|
|
1717
1936
|
warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
|
|
1718
1937
|
} else {
|
|
1719
1938
|
let source = "";
|
|
1720
1939
|
try {
|
|
1721
|
-
source = (0,
|
|
1940
|
+
source = (0, import_node_fs7.readFileSync)(main, "utf8");
|
|
1722
1941
|
} catch {
|
|
1723
1942
|
}
|
|
1724
1943
|
if (!/\bwithObservability\b/.test(source)) {
|
|
@@ -1742,7 +1961,7 @@ function calendarProjectWarnings(rootDir) {
|
|
|
1742
1961
|
}
|
|
1743
1962
|
function readPackageJson(rootDir) {
|
|
1744
1963
|
try {
|
|
1745
|
-
return JSON.parse((0,
|
|
1964
|
+
return JSON.parse((0, import_node_fs7.readFileSync)((0, import_node_path5.join)(rootDir, "package.json"), "utf8"));
|
|
1746
1965
|
} catch {
|
|
1747
1966
|
return null;
|
|
1748
1967
|
}
|
|
@@ -1766,8 +1985,11 @@ async function doctor(options) {
|
|
|
1766
1985
|
out.log(`rules: ${rules ? `${Object.keys(rules).length} namespaces` : "none"}`);
|
|
1767
1986
|
out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "not configured" : "not enabled"}`);
|
|
1768
1987
|
if (cfg.services.includes("calendar")) {
|
|
1769
|
-
const calendar = cfg.envs.map((env) =>
|
|
1770
|
-
|
|
1988
|
+
const calendar = cfg.envs.map((env) => {
|
|
1989
|
+
const resolved = calendarServiceConfig(cfg, env);
|
|
1990
|
+
return `${env}:${resolved.bookingCalendarId}+${resolved.availabilityCalendars.length}`;
|
|
1991
|
+
}).join(", ");
|
|
1992
|
+
out.log(`calendar: google/booking (${calendar})`);
|
|
1771
1993
|
const pages = cfg.envs.map((env) => `${env}:${calendarBookingPageUrl(cfg, env) ?? "none"}`).join(", ");
|
|
1772
1994
|
out.log(`booking pages: ${pages}`);
|
|
1773
1995
|
} else {
|
|
@@ -1819,9 +2041,9 @@ async function doctor(options) {
|
|
|
1819
2041
|
}
|
|
1820
2042
|
|
|
1821
2043
|
// src/help.ts
|
|
1822
|
-
var
|
|
2044
|
+
var import_node_fs8 = require("fs");
|
|
1823
2045
|
function cliVersion() {
|
|
1824
|
-
const pkg = JSON.parse((0,
|
|
2046
|
+
const pkg = JSON.parse((0, import_node_fs8.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
|
|
1825
2047
|
return pkg.version ?? "unknown";
|
|
1826
2048
|
}
|
|
1827
2049
|
function printHelp() {
|
|
@@ -1834,8 +2056,10 @@ Usage:
|
|
|
1834
2056
|
odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
|
|
1835
2057
|
odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
|
|
1836
2058
|
odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
|
|
1837
|
-
odla-ai calendar resync [--env dev] [--email <odla-account>] [--yes]
|
|
1838
2059
|
odla-ai calendar disconnect [--env dev] [--email <odla-account>] --yes
|
|
2060
|
+
odla-ai app archive [--config odla.config.mjs] [--email <odla-account>] [--json] --yes
|
|
2061
|
+
odla-ai app restore [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
2062
|
+
odla-ai app export [--env dev] [--fresh] [--out <file>] [--email <odla-account>] [--json]
|
|
1839
2063
|
odla-ai capabilities [--json]
|
|
1840
2064
|
odla-ai admin ai show [--platform https://odla.ai] [--email <odla-account>] [--json]
|
|
1841
2065
|
odla-ai admin ai models [--provider <id>] [--json]
|
|
@@ -1855,7 +2079,7 @@ Usage:
|
|
|
1855
2079
|
odla-ai security report <job-id> [--json]
|
|
1856
2080
|
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
1857
2081
|
odla-ai security run [target] --self --ack-redacted-source
|
|
1858
|
-
odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
2082
|
+
odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
1859
2083
|
odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
|
|
1860
2084
|
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
1861
2085
|
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
@@ -1867,7 +2091,14 @@ Commands:
|
|
|
1867
2091
|
setup Install offline odla runbooks for common coding-agent harnesses.
|
|
1868
2092
|
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
1869
2093
|
doctor Validate and summarize the project config without network calls.
|
|
1870
|
-
calendar Inspect, connect,
|
|
2094
|
+
calendar Inspect, connect, or disconnect the live Google booking connection.
|
|
2095
|
+
app Archive (suspend, data retained), restore, or export the app.
|
|
2096
|
+
Archiving takes every environment's data plane down until restored;
|
|
2097
|
+
permanent deletion has NO CLI \u2014 it requires a signed-in owner in
|
|
2098
|
+
Studio, and no machine or agent credential can purge. "app export"
|
|
2099
|
+
downloads a portable gzipped-JSONL snapshot of one environment's
|
|
2100
|
+
database (--fresh takes a new snapshot first) \u2014 your data is
|
|
2101
|
+
always yours to take.
|
|
1871
2102
|
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
1872
2103
|
admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
|
|
1873
2104
|
security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
|
|
@@ -1889,6 +2120,12 @@ Safety:
|
|
|
1889
2120
|
Provision opens the approval page in your browser automatically whenever the
|
|
1890
2121
|
machine can show one, including agent-driven runs; only CI, SSH, and
|
|
1891
2122
|
display-less hosts skip it. Use --open to force or --no-open to suppress.
|
|
2123
|
+
Browser launch is best-effort: the printed approval URL is authoritative and
|
|
2124
|
+
agents must relay it to the human verbatim. A started handshake is persisted
|
|
2125
|
+
under .odla/, so a command killed mid-wait loses nothing \u2014 rerunning resumes
|
|
2126
|
+
the same code. Outside an interactive terminal the wait is capped (90s by
|
|
2127
|
+
default, --wait <seconds> to change); a still-pending handshake then exits
|
|
2128
|
+
with code 75: relay the URL, wait for approval, and re-run to collect.
|
|
1892
2129
|
A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
|
|
1893
2130
|
The email is a non-secret identity hint: never provide a password or session
|
|
1894
2131
|
token. The matching account must already exist, be signed in, explicitly
|
|
@@ -1925,13 +2162,13 @@ function harnessOption(value, flag) {
|
|
|
1925
2162
|
}
|
|
1926
2163
|
|
|
1927
2164
|
// src/init.ts
|
|
1928
|
-
var
|
|
1929
|
-
var
|
|
2165
|
+
var import_node_fs9 = require("fs");
|
|
2166
|
+
var import_node_path6 = require("path");
|
|
1930
2167
|
function initProject(options) {
|
|
1931
2168
|
const out = options.stdout ?? console;
|
|
1932
|
-
const rootDir = (0,
|
|
1933
|
-
const configPath = (0,
|
|
1934
|
-
if ((0,
|
|
2169
|
+
const rootDir = (0, import_node_path6.resolve)(options.rootDir ?? process.cwd());
|
|
2170
|
+
const configPath = (0, import_node_path6.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
|
|
2171
|
+
if ((0, import_node_fs9.existsSync)(configPath) && !options.force) {
|
|
1935
2172
|
throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
|
|
1936
2173
|
}
|
|
1937
2174
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
|
|
@@ -1941,29 +2178,29 @@ function initProject(options) {
|
|
|
1941
2178
|
const services = options.services?.length ? options.services : ["db", "ai"];
|
|
1942
2179
|
if (services.includes("calendar") && !services.includes("db")) throw new Error("--services calendar requires db");
|
|
1943
2180
|
const aiProvider = options.aiProvider ?? "anthropic";
|
|
1944
|
-
(0,
|
|
1945
|
-
(0,
|
|
1946
|
-
(0,
|
|
1947
|
-
(0,
|
|
1948
|
-
writeIfMissing((0,
|
|
1949
|
-
writeIfMissing((0,
|
|
2181
|
+
(0, import_node_fs9.mkdirSync)((0, import_node_path6.dirname)(configPath), { recursive: true });
|
|
2182
|
+
(0, import_node_fs9.mkdirSync)((0, import_node_path6.resolve)(rootDir, "src/odla"), { recursive: true });
|
|
2183
|
+
(0, import_node_fs9.mkdirSync)((0, import_node_path6.resolve)(rootDir, ".odla"), { recursive: true });
|
|
2184
|
+
(0, import_node_fs9.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
|
|
2185
|
+
writeIfMissing((0, import_node_path6.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
|
|
2186
|
+
writeIfMissing((0, import_node_path6.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
|
|
1950
2187
|
ensureGitignore(rootDir);
|
|
1951
2188
|
out.log(`created ${relativeDisplay(configPath, rootDir)}`);
|
|
1952
2189
|
out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
|
|
1953
2190
|
out.log("updated .gitignore for local odla credentials");
|
|
1954
2191
|
}
|
|
1955
2192
|
function writeIfMissing(path, text) {
|
|
1956
|
-
if ((0,
|
|
1957
|
-
(0,
|
|
2193
|
+
if ((0, import_node_fs9.existsSync)(path)) return;
|
|
2194
|
+
(0, import_node_fs9.writeFileSync)(path, text);
|
|
1958
2195
|
}
|
|
1959
2196
|
function configTemplate(input) {
|
|
1960
2197
|
const calendar = input.services.includes("calendar") ? ` calendar: {
|
|
1961
2198
|
google: {
|
|
1962
|
-
|
|
2199
|
+
// Calendars consulted for availability; bookings land on bookingCalendar
|
|
2200
|
+
// (defaults to the first availability calendar). Google consent happens
|
|
2201
|
+
// in the browser during provision; bookings write live through odla.ai.
|
|
2202
|
+
availabilityCalendars: ${JSON.stringify(Object.fromEntries(input.envs.map((env) => [env, ["primary"]])))},
|
|
1963
2203
|
bookingPageUrl: ${JSON.stringify(Object.fromEntries(input.envs.map((env) => [env, null])))},
|
|
1964
|
-
match: { organizerSelf: true, requireAttendees: true },
|
|
1965
|
-
attendeePolicy: "full",
|
|
1966
|
-
// Read-only in this release. Google consent happens in Studio during provision.
|
|
1967
2204
|
},
|
|
1968
2205
|
},
|
|
1969
2206
|
` : "";
|
|
@@ -2054,7 +2291,7 @@ function relativeDisplay(path, rootDir) {
|
|
|
2054
2291
|
// src/provision.ts
|
|
2055
2292
|
var import_apps2 = require("@odla-ai/apps");
|
|
2056
2293
|
var import_ai = require("@odla-ai/ai");
|
|
2057
|
-
var
|
|
2294
|
+
var import_node_process7 = __toESM(require("process"), 1);
|
|
2058
2295
|
|
|
2059
2296
|
// src/provision-credentials.ts
|
|
2060
2297
|
var import_apps = require("@odla-ai/apps");
|
|
@@ -2259,7 +2496,7 @@ async function provision(options) {
|
|
|
2259
2496
|
if (cfg.services.includes("calendar")) {
|
|
2260
2497
|
for (const env of cfg.envs) {
|
|
2261
2498
|
const calendar = calendarServiceConfig(cfg, env);
|
|
2262
|
-
out.log(` calendar.${env}: google/
|
|
2499
|
+
out.log(` calendar.${env}: google/book \u2192 ${calendar.bookingCalendarId}; availability ${calendar.availabilityCalendars.join(", ")} (${GOOGLE_CALENDAR_EVENTS_SCOPE})`);
|
|
2263
2500
|
const bookingPage = calendarBookingPageUrl(cfg, env);
|
|
2264
2501
|
out.log(` calendar.${env}.bookingPageUrl: ${bookingPage === void 0 ? "unchanged" : bookingPage ?? "clear"}`);
|
|
2265
2502
|
}
|
|
@@ -2282,7 +2519,7 @@ async function provision(options) {
|
|
|
2282
2519
|
}
|
|
2283
2520
|
const devVarsTarget = resolveWriteDevVarsTarget(cfg, options.writeDevVars);
|
|
2284
2521
|
if (cfg.local.gitignore) {
|
|
2285
|
-
ensureGitignore(cfg.rootDir, [cfg.local.tokenFile, cfg.local.credentialsFile, ...devVarsTarget ? [devVarsTarget] : []]);
|
|
2522
|
+
ensureGitignore(cfg.rootDir, [cfg.local.tokenFile, handshakeFile(cfg), cfg.local.credentialsFile, ...devVarsTarget ? [devVarsTarget] : []]);
|
|
2286
2523
|
}
|
|
2287
2524
|
if (options.pushSecrets) {
|
|
2288
2525
|
await preflightSecretsPush({ configPath: cfg.configPath, runner: options.secretRunner });
|
|
@@ -2372,7 +2609,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
2372
2609
|
out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
|
|
2373
2610
|
}
|
|
2374
2611
|
if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
|
|
2375
|
-
const key =
|
|
2612
|
+
const key = import_node_process7.default.env[cfg.ai.keyEnv];
|
|
2376
2613
|
if (key) {
|
|
2377
2614
|
const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
|
|
2378
2615
|
await (0, import_ai.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
|
|
@@ -2514,7 +2751,7 @@ async function resolveVaultWrite(options) {
|
|
|
2514
2751
|
}
|
|
2515
2752
|
|
|
2516
2753
|
// src/security-command-context.ts
|
|
2517
|
-
var
|
|
2754
|
+
var import_promises2 = require("readline/promises");
|
|
2518
2755
|
async function hostedSecurityContext(parsed, dependencies) {
|
|
2519
2756
|
const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
2520
2757
|
const cfg = await loadProjectConfig(configPath);
|
|
@@ -2540,7 +2777,7 @@ async function hostedSecurityContext(parsed, dependencies) {
|
|
|
2540
2777
|
async function interactiveConfirmation(message, dependencies) {
|
|
2541
2778
|
if (dependencies.confirm) return dependencies.confirm(message);
|
|
2542
2779
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
2543
|
-
const prompt = (0,
|
|
2780
|
+
const prompt = (0, import_promises2.createInterface)({ input: process.stdin, output: process.stdout });
|
|
2544
2781
|
try {
|
|
2545
2782
|
const answer = await prompt.question(`${message} [y/N] `);
|
|
2546
2783
|
return /^y(?:es)?$/i.test(answer.trim());
|
|
@@ -2670,7 +2907,7 @@ function hostedSeverity(value, flag) {
|
|
|
2670
2907
|
var import_security2 = require("@odla-ai/security");
|
|
2671
2908
|
|
|
2672
2909
|
// src/security.ts
|
|
2673
|
-
var
|
|
2910
|
+
var import_node_path7 = require("path");
|
|
2674
2911
|
var import_security = require("@odla-ai/security");
|
|
2675
2912
|
var import_node = require("@odla-ai/security/node");
|
|
2676
2913
|
async function runHostedSecurity(options) {
|
|
@@ -2682,9 +2919,9 @@ async function runHostedSecurity(options) {
|
|
|
2682
2919
|
const appId = selfAudit ? "odla-ai" : cfg.app.id;
|
|
2683
2920
|
const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
|
|
2684
2921
|
const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
|
|
2685
|
-
const target = (0,
|
|
2686
|
-
const output = (0,
|
|
2687
|
-
const outputRelative = (0,
|
|
2922
|
+
const target = (0, import_node_path7.resolve)(options.target ?? cfg?.rootDir ?? ".");
|
|
2923
|
+
const output = (0, import_node_path7.resolve)(options.out ?? (0, import_node_path7.resolve)(target, ".odla/security/hosted"));
|
|
2924
|
+
const outputRelative = (0, import_node_path7.relative)(target, output).split(import_node_path7.sep).join("/");
|
|
2688
2925
|
if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
|
|
2689
2926
|
const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
|
|
2690
2927
|
const tokenRequest = {
|
|
@@ -2696,7 +2933,7 @@ async function runHostedSecurity(options) {
|
|
|
2696
2933
|
};
|
|
2697
2934
|
const token = await injectedToken(options, tokenRequest);
|
|
2698
2935
|
const snapshot = await (0, import_node.snapshotDirectory)(target, {
|
|
2699
|
-
exclude: !outputRelative.startsWith("../") && !(0,
|
|
2936
|
+
exclude: !outputRelative.startsWith("../") && !(0, import_node_path7.isAbsolute)(outputRelative) ? [outputRelative] : []
|
|
2700
2937
|
});
|
|
2701
2938
|
const hosted = await (0, import_security.createPlatformSecurityReasoners)({
|
|
2702
2939
|
platform,
|
|
@@ -2714,7 +2951,7 @@ async function runHostedSecurity(options) {
|
|
|
2714
2951
|
});
|
|
2715
2952
|
const harness = (0, import_security.createSecurityHarness)({
|
|
2716
2953
|
profile,
|
|
2717
|
-
store: new import_node.FileRunStore((0,
|
|
2954
|
+
store: new import_node.FileRunStore((0, import_node_path7.resolve)(output, "state")),
|
|
2718
2955
|
discoveryReasoner: hosted.discoveryReasoner,
|
|
2719
2956
|
validationReasoner: hosted.validationReasoner,
|
|
2720
2957
|
policy: {
|
|
@@ -2738,7 +2975,7 @@ async function runHostedSecurity(options) {
|
|
|
2738
2975
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
2739
2976
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
2740
2977
|
if (!env || !declared.includes(env)) {
|
|
2741
|
-
const shown = (0,
|
|
2978
|
+
const shown = (0, import_node_path7.relative)(rootDir, configPath) || configPath;
|
|
2742
2979
|
throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
|
|
2743
2980
|
}
|
|
2744
2981
|
return env;
|
|
@@ -2767,7 +3004,7 @@ function printSummary(out, appId, env, run, report, output) {
|
|
|
2767
3004
|
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}`);
|
|
2768
3005
|
if (report.callBudget) out.log(` calls: discovery=${formatBudget(report.callBudget.discovery)} validation=${formatBudget(report.callBudget.validation)}`);
|
|
2769
3006
|
out.log(` findings: confirmed=${report.metrics.confirmed} needs_reproduction=${report.metrics.needsReproduction} candidates=${report.metrics.candidates}`);
|
|
2770
|
-
out.log(` report: ${(0,
|
|
3007
|
+
out.log(` report: ${(0, import_node_path7.resolve)(output, "REPORT.md")}`);
|
|
2771
3008
|
}
|
|
2772
3009
|
function formatBudget(usage) {
|
|
2773
3010
|
return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
|
|
@@ -3419,9 +3656,9 @@ async function securityStatus(parsed, dependencies) {
|
|
|
3419
3656
|
}
|
|
3420
3657
|
|
|
3421
3658
|
// src/skill.ts
|
|
3422
|
-
var
|
|
3659
|
+
var import_node_fs10 = require("fs");
|
|
3423
3660
|
var import_node_os = require("os");
|
|
3424
|
-
var
|
|
3661
|
+
var import_node_path8 = require("path");
|
|
3425
3662
|
var import_node_url2 = require("url");
|
|
3426
3663
|
|
|
3427
3664
|
// src/skill-adapters.ts
|
|
@@ -3482,8 +3719,8 @@ function installSkill(options = {}) {
|
|
|
3482
3719
|
const files = listFiles(sourceDir);
|
|
3483
3720
|
if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
|
|
3484
3721
|
const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
|
|
3485
|
-
const root = (0,
|
|
3486
|
-
const home = (0,
|
|
3722
|
+
const root = (0, import_node_path8.resolve)(options.dir ?? process.cwd());
|
|
3723
|
+
const home = (0, import_node_path8.resolve)(options.homeDir ?? (0, import_node_os.homedir)());
|
|
3487
3724
|
const plans = /* @__PURE__ */ new Map();
|
|
3488
3725
|
const targets = /* @__PURE__ */ new Map();
|
|
3489
3726
|
const rememberTarget = (harness, target) => {
|
|
@@ -3497,48 +3734,48 @@ function installSkill(options = {}) {
|
|
|
3497
3734
|
plans.set(target, { target, content, boundary, managedMerge });
|
|
3498
3735
|
};
|
|
3499
3736
|
const planSkillTree = (targetDir2, boundary = root) => {
|
|
3500
|
-
for (const rel of files) plan((0,
|
|
3737
|
+
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);
|
|
3501
3738
|
};
|
|
3502
3739
|
let targetDir;
|
|
3503
3740
|
if (options.global) {
|
|
3504
|
-
const claudeRoot = (0,
|
|
3505
|
-
const codexRoot = (0,
|
|
3741
|
+
const claudeRoot = (0, import_node_path8.join)(home, ".claude", "skills");
|
|
3742
|
+
const codexRoot = (0, import_node_path8.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path8.join)(home, ".codex"), "skills");
|
|
3506
3743
|
targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
|
|
3507
3744
|
for (const harness of harnesses) {
|
|
3508
3745
|
const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
|
|
3509
|
-
planSkillTree(skillRoot, harness === "claude" ? home : (0,
|
|
3746
|
+
planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path8.dirname)((0, import_node_path8.dirname)(codexRoot)));
|
|
3510
3747
|
rememberTarget(harness, skillRoot);
|
|
3511
3748
|
}
|
|
3512
3749
|
} else {
|
|
3513
|
-
const sharedRoot = (0,
|
|
3750
|
+
const sharedRoot = (0, import_node_path8.join)(root, ".agents", "skills");
|
|
3514
3751
|
planSkillTree(sharedRoot);
|
|
3515
|
-
const claudeRoot = (0,
|
|
3752
|
+
const claudeRoot = (0, import_node_path8.join)(root, ".claude", "skills");
|
|
3516
3753
|
targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
|
|
3517
3754
|
for (const harness of harnesses) rememberTarget(harness, sharedRoot);
|
|
3518
3755
|
if (harnesses.includes("claude")) {
|
|
3519
3756
|
for (const skill of skillNames(files)) {
|
|
3520
|
-
const canonical = (0,
|
|
3521
|
-
plan((0,
|
|
3757
|
+
const canonical = (0, import_node_fs10.readFileSync)((0, import_node_path8.join)(sourceDir, skill, "SKILL.md"), "utf8");
|
|
3758
|
+
plan((0, import_node_path8.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
|
|
3522
3759
|
}
|
|
3523
3760
|
rememberTarget("claude", claudeRoot);
|
|
3524
3761
|
}
|
|
3525
3762
|
if (harnesses.includes("cursor")) {
|
|
3526
|
-
const cursorRule = (0,
|
|
3763
|
+
const cursorRule = (0, import_node_path8.join)(root, ".cursor", "rules", "odla.mdc");
|
|
3527
3764
|
plan(cursorRule, CURSOR_RULE);
|
|
3528
3765
|
rememberTarget("cursor", cursorRule);
|
|
3529
3766
|
}
|
|
3530
3767
|
if (harnesses.includes("agents")) {
|
|
3531
|
-
const agentsFile = (0,
|
|
3768
|
+
const agentsFile = (0, import_node_path8.join)(root, "AGENTS.md");
|
|
3532
3769
|
plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
3533
3770
|
rememberTarget("agents", agentsFile);
|
|
3534
3771
|
}
|
|
3535
3772
|
if (harnesses.includes("copilot")) {
|
|
3536
|
-
const copilotFile = (0,
|
|
3773
|
+
const copilotFile = (0, import_node_path8.join)(root, ".github", "copilot-instructions.md");
|
|
3537
3774
|
plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
3538
3775
|
rememberTarget("copilot", copilotFile);
|
|
3539
3776
|
}
|
|
3540
3777
|
if (harnesses.includes("gemini")) {
|
|
3541
|
-
const geminiFile = (0,
|
|
3778
|
+
const geminiFile = (0, import_node_path8.join)(root, "GEMINI.md");
|
|
3542
3779
|
plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
3543
3780
|
rememberTarget("gemini", geminiFile);
|
|
3544
3781
|
}
|
|
@@ -3552,11 +3789,11 @@ function installSkill(options = {}) {
|
|
|
3552
3789
|
conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
|
|
3553
3790
|
continue;
|
|
3554
3791
|
}
|
|
3555
|
-
if (!(0,
|
|
3792
|
+
if (!(0, import_node_fs10.existsSync)(file.target)) {
|
|
3556
3793
|
writtenPaths.add(file.target);
|
|
3557
3794
|
continue;
|
|
3558
3795
|
}
|
|
3559
|
-
const current = (0,
|
|
3796
|
+
const current = (0, import_node_fs10.readFileSync)(file.target, "utf8");
|
|
3560
3797
|
if (current === file.content) {
|
|
3561
3798
|
unchangedPaths.add(file.target);
|
|
3562
3799
|
} else if (file.managedMerge || options.force) {
|
|
@@ -3573,9 +3810,9 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
3573
3810
|
);
|
|
3574
3811
|
}
|
|
3575
3812
|
for (const file of plans.values()) {
|
|
3576
|
-
if (!(0,
|
|
3577
|
-
(0,
|
|
3578
|
-
(0,
|
|
3813
|
+
if (!(0, import_node_fs10.existsSync)(file.target) || (0, import_node_fs10.readFileSync)(file.target, "utf8") !== file.content) {
|
|
3814
|
+
(0, import_node_fs10.mkdirSync)((0, import_node_path8.dirname)(file.target), { recursive: true });
|
|
3815
|
+
(0, import_node_fs10.writeFileSync)(file.target, file.content);
|
|
3579
3816
|
}
|
|
3580
3817
|
}
|
|
3581
3818
|
const skills = skillNames(files);
|
|
@@ -3594,7 +3831,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
3594
3831
|
};
|
|
3595
3832
|
}
|
|
3596
3833
|
function pathsUnder(root, paths) {
|
|
3597
|
-
return [...paths].map((path) => (0,
|
|
3834
|
+
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();
|
|
3598
3835
|
}
|
|
3599
3836
|
function normalizeHarnesses(values, global) {
|
|
3600
3837
|
const requested = values?.length ? values : ["claude"];
|
|
@@ -3616,9 +3853,9 @@ function normalizeHarnesses(values, global) {
|
|
|
3616
3853
|
function managedFileContent(path, block, force, boundary) {
|
|
3617
3854
|
const symlink = symlinkedComponent(boundary, path);
|
|
3618
3855
|
if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
|
|
3619
|
-
if (!(0,
|
|
3856
|
+
if (!(0, import_node_fs10.existsSync)(path)) return `${block}
|
|
3620
3857
|
`;
|
|
3621
|
-
const current = (0,
|
|
3858
|
+
const current = (0, import_node_fs10.readFileSync)(path, "utf8");
|
|
3622
3859
|
const start = "<!-- odla-ai agent setup:start -->";
|
|
3623
3860
|
const end = "<!-- odla-ai agent setup:end -->";
|
|
3624
3861
|
const startAt = current.indexOf(start);
|
|
@@ -3639,15 +3876,15 @@ function managedFileContent(path, block, force, boundary) {
|
|
|
3639
3876
|
return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
|
|
3640
3877
|
}
|
|
3641
3878
|
function symlinkedComponent(boundary, target) {
|
|
3642
|
-
const rel = (0,
|
|
3643
|
-
if (rel === ".." || rel.startsWith(`..${
|
|
3879
|
+
const rel = (0, import_node_path8.relative)(boundary, target);
|
|
3880
|
+
if (rel === ".." || rel.startsWith(`..${import_node_path8.sep}`) || (0, import_node_path8.isAbsolute)(rel)) {
|
|
3644
3881
|
throw new Error(`agent setup target escapes its install root: ${target}`);
|
|
3645
3882
|
}
|
|
3646
3883
|
let current = boundary;
|
|
3647
|
-
for (const part of rel.split(
|
|
3648
|
-
current = (0,
|
|
3884
|
+
for (const part of rel.split(import_node_path8.sep).filter(Boolean)) {
|
|
3885
|
+
current = (0, import_node_path8.join)(current, part);
|
|
3649
3886
|
try {
|
|
3650
|
-
if ((0,
|
|
3887
|
+
if ((0, import_node_fs10.lstatSync)(current).isSymbolicLink()) return current;
|
|
3651
3888
|
} catch (error) {
|
|
3652
3889
|
if (error.code !== "ENOENT") throw error;
|
|
3653
3890
|
}
|
|
@@ -3658,13 +3895,13 @@ function skillNames(files) {
|
|
|
3658
3895
|
return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
|
|
3659
3896
|
}
|
|
3660
3897
|
function listFiles(dir) {
|
|
3661
|
-
if (!(0,
|
|
3898
|
+
if (!(0, import_node_fs10.existsSync)(dir)) return [];
|
|
3662
3899
|
const results = [];
|
|
3663
3900
|
const walk = (current) => {
|
|
3664
|
-
for (const entry of (0,
|
|
3665
|
-
const path = (0,
|
|
3901
|
+
for (const entry of (0, import_node_fs10.readdirSync)(current, { withFileTypes: true })) {
|
|
3902
|
+
const path = (0, import_node_path8.join)(current, entry.name);
|
|
3666
3903
|
if (entry.isDirectory()) walk(path);
|
|
3667
|
-
else results.push((0,
|
|
3904
|
+
else results.push((0, import_node_path8.relative)(dir, path));
|
|
3668
3905
|
}
|
|
3669
3906
|
};
|
|
3670
3907
|
walk(dir);
|
|
@@ -3715,7 +3952,7 @@ async function smoke(options) {
|
|
|
3715
3952
|
);
|
|
3716
3953
|
const status = await readCalendarStatus({ platform: cfg.platformUrl, appId: cfg.app.id, env, token, fetch: doFetch });
|
|
3717
3954
|
assertCalendarHealthy(status, calendarServiceConfig(cfg, env));
|
|
3718
|
-
out.log(` calendar: ${status.status},
|
|
3955
|
+
out.log(` calendar: ${status.status}, bookable (booking \u2192 ${status.bookingCalendarId ?? "primary"})`);
|
|
3719
3956
|
}
|
|
3720
3957
|
const expectedSchema = await resolveDataExport(cfg, cfg.db?.schema, ["schema", "SCHEMA"]);
|
|
3721
3958
|
const expectedEntities = serializedEntities(expectedSchema);
|
|
@@ -3738,14 +3975,6 @@ async function smoke(options) {
|
|
|
3738
3975
|
} else {
|
|
3739
3976
|
out.log(` aggregate: skipped (schema has no entities)`);
|
|
3740
3977
|
}
|
|
3741
|
-
if (cfg.services.includes("calendar")) {
|
|
3742
|
-
const bookings = await postJson2(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/aggregate`, entry.dbKey, {
|
|
3743
|
-
ns: "$bookings",
|
|
3744
|
-
aggregate: { count: true }
|
|
3745
|
-
});
|
|
3746
|
-
const count = bookings.aggregate?.count;
|
|
3747
|
-
out.log(` bookings: ${String(count ?? "ok")}`);
|
|
3748
|
-
}
|
|
3749
3978
|
out.log("ok");
|
|
3750
3979
|
}
|
|
3751
3980
|
function assertCalendarHealthy(status, expected) {
|
|
@@ -3753,14 +3982,11 @@ function assertCalendarHealthy(status, expected) {
|
|
|
3753
3982
|
if (status.status !== "healthy") {
|
|
3754
3983
|
throw new Error(`calendar connection is ${status.status}${status.error?.message ? `: ${status.error.message}` : ""}`);
|
|
3755
3984
|
}
|
|
3756
|
-
if (status.
|
|
3757
|
-
const
|
|
3758
|
-
if (!
|
|
3759
|
-
const missing = expected.
|
|
3985
|
+
if (!status.writable) throw new Error('calendar grant does not cover booking writes; run "odla-ai calendar connect" to re-consent');
|
|
3986
|
+
const hasEventsScope = status.grantedScopes.some((scope) => scope === GOOGLE_CALENDAR_EVENTS_SCOPE);
|
|
3987
|
+
if (!hasEventsScope) throw new Error("calendar connection is missing calendar.events consent");
|
|
3988
|
+
const missing = expected.availabilityCalendars.filter((id) => !status.calendars.includes(id));
|
|
3760
3989
|
if (missing.length) throw new Error(`calendar connection is missing configured calendars: ${missing.join(", ")}`);
|
|
3761
|
-
if (status.lastSuccessfulSyncAt === void 0) throw new Error("calendar has never completed a sync");
|
|
3762
|
-
if (Date.now() - status.lastSuccessfulSyncAt > 30 * 6e4) throw new Error("calendar sync is more than 30 minutes stale");
|
|
3763
|
-
if (status.watchExpiresAt !== void 0 && status.watchExpiresAt <= Date.now()) throw new Error("calendar watch channel is expired");
|
|
3764
3990
|
}
|
|
3765
3991
|
async function getJson(doFetch, url, bearer) {
|
|
3766
3992
|
const res = await doFetch(url, {
|
|
@@ -3792,6 +4018,9 @@ async function safeText4(res) {
|
|
|
3792
4018
|
}
|
|
3793
4019
|
|
|
3794
4020
|
// src/cli.ts
|
|
4021
|
+
function exitCodeFor(err) {
|
|
4022
|
+
return err?.code === "handshake_pending" ? 75 : 1;
|
|
4023
|
+
}
|
|
3795
4024
|
async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
3796
4025
|
const parsed = parseArgv(argv);
|
|
3797
4026
|
const command = parsed.positionals[0] ?? "help";
|
|
@@ -3844,6 +4073,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
3844
4073
|
await adminCommand(parsed);
|
|
3845
4074
|
return;
|
|
3846
4075
|
}
|
|
4076
|
+
if (command === "app") {
|
|
4077
|
+
await appCommand(parsed, dependencies);
|
|
4078
|
+
return;
|
|
4079
|
+
}
|
|
3847
4080
|
if (command === "security") {
|
|
3848
4081
|
await securityCommand(parsed, dependencies);
|
|
3849
4082
|
return;
|
|
@@ -3940,6 +4173,7 @@ async function provisionCommand(parsed, dependencies) {
|
|
|
3940
4173
|
"token",
|
|
3941
4174
|
"email",
|
|
3942
4175
|
"open",
|
|
4176
|
+
"wait",
|
|
3943
4177
|
"yes"
|
|
3944
4178
|
], 1);
|
|
3945
4179
|
const writeDevVars2 = parsed.options["write-dev-vars"];
|
|
@@ -3954,6 +4188,7 @@ async function provisionCommand(parsed, dependencies) {
|
|
|
3954
4188
|
token: stringOpt(parsed.options.token),
|
|
3955
4189
|
email: stringOpt(parsed.options.email),
|
|
3956
4190
|
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
4191
|
+
wait: numberOpt(parsed.options.wait, "--wait"),
|
|
3957
4192
|
yes: parsed.options.yes === true,
|
|
3958
4193
|
fetch: dependencies.fetch,
|
|
3959
4194
|
openApprovalUrl: dependencies.openUrl,
|
|
@@ -3963,7 +4198,7 @@ async function provisionCommand(parsed, dependencies) {
|
|
|
3963
4198
|
}
|
|
3964
4199
|
async function calendarCommand(parsed, dependencies) {
|
|
3965
4200
|
const sub = parsed.positionals[1];
|
|
3966
|
-
if (sub !== "status" && sub !== "calendars" && sub !== "connect" && sub !== "
|
|
4201
|
+
if (sub !== "status" && sub !== "calendars" && sub !== "connect" && sub !== "disconnect") {
|
|
3967
4202
|
throw new Error(`unknown calendar subcommand "${sub ?? ""}". Try "odla-ai calendar status --env dev".`);
|
|
3968
4203
|
}
|
|
3969
4204
|
assertArgs(parsed, ["config", "env", "json", "token", "email", "open", "yes"], 2);
|
|
@@ -3984,13 +4219,12 @@ async function calendarCommand(parsed, dependencies) {
|
|
|
3984
4219
|
if (sub === "status") await calendarStatus(options);
|
|
3985
4220
|
else if (sub === "calendars") await calendarCalendars(options);
|
|
3986
4221
|
else if (sub === "connect") await calendarConnect(options);
|
|
3987
|
-
else if (sub === "resync") await calendarResync(options);
|
|
3988
4222
|
else await calendarDisconnect(options);
|
|
3989
4223
|
}
|
|
3990
4224
|
|
|
3991
4225
|
// src/bin.ts
|
|
3992
4226
|
runCli().catch((err) => {
|
|
3993
4227
|
console.error(`odla-ai: ${err instanceof Error ? err.message : String(err)}`);
|
|
3994
|
-
process.exitCode =
|
|
4228
|
+
process.exitCode = exitCodeFor(err);
|
|
3995
4229
|
});
|
|
3996
4230
|
//# sourceMappingURL=bin.cjs.map
|