@odla-ai/cli 0.30.0 → 0.30.1
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 +19 -6
- package/dist/bin.cjs +96 -148
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-PPSXHZJI.js → chunk-7V7WVTDT.js} +71 -123
- package/dist/chunk-7V7WVTDT.js.map +1 -0
- package/dist/{cli-CKBUVTJM.js → cli-SZUWA6EF.js} +2 -2
- package/dist/index.cjs +84 -136
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -7
- package/dist/index.d.ts +8 -7
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/skills/odla/SKILL.md +6 -4
- package/dist/chunk-PPSXHZJI.js.map +0 -1
- /package/dist/{cli-CKBUVTJM.js.map → cli-SZUWA6EF.js.map} +0 -0
package/README.md
CHANGED
|
@@ -511,12 +511,12 @@ credential set.
|
|
|
511
511
|
and attempts to open it — including from CI, SSH, display-less, scripted,
|
|
512
512
|
and agent-driven shells. Only `--no-open` suppresses the attempt. Browser
|
|
513
513
|
launch is best-effort: an agent with browser control must open that exact URL
|
|
514
|
-
immediately; otherwise it gives the URL to the human verbatim. The
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
514
|
+
immediately; otherwise it gives the URL to the human verbatim. The device
|
|
515
|
+
code stays only in the running process. Outside an interactive terminal the
|
|
516
|
+
approval wait is capped (90 seconds by default; `--wait <seconds>`
|
|
517
|
+
overrides), and a still-pending handshake exits with code 75. Rerunning
|
|
518
|
+
always requests and opens a new code; it never resumes an older request from
|
|
519
|
+
`.odla/`.
|
|
520
520
|
2. Creates the platform app if needed. For a new id, this consumes the exact-id
|
|
521
521
|
reservation approved in step 1; it is not ambient project-creation authority.
|
|
522
522
|
3. Enables configured services in every configured environment. Calendar
|
|
@@ -569,6 +569,19 @@ The inventory contains only receipt, target, state, and timestamp metadata.
|
|
|
569
569
|
Revocation uses the receipt's exact DB and o11y credential ids; no value is
|
|
570
570
|
retrieved or rotated.
|
|
571
571
|
|
|
572
|
+
Verify a deployment that intentionally keeps db/o11y credentials only on the
|
|
573
|
+
Worker:
|
|
574
|
+
|
|
575
|
+
```sh
|
|
576
|
+
npx @odla-ai/cli smoke --env dev --runtime
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
Runtime smoke never reads `.odla/credentials.local.json`. It checks Registry
|
|
580
|
+
public config, the configured Worker link, anonymous integration probes, and
|
|
581
|
+
owner-visible calendar health. Direct schema and aggregate reads remain part
|
|
582
|
+
of ordinary local-credential smoke because a runtime credential is deliberately
|
|
583
|
+
not retrievable from the Worker.
|
|
584
|
+
|
|
572
585
|
### Application capability integrations
|
|
573
586
|
|
|
574
587
|
An integration is an npm capability composed into an app, not a hosted
|
package/dist/bin.cjs
CHANGED
|
@@ -305,10 +305,51 @@ var init_handshake_approval = __esm({
|
|
|
305
305
|
}
|
|
306
306
|
});
|
|
307
307
|
|
|
308
|
+
// src/handshake-state.ts
|
|
309
|
+
function handshakeFile(cfg) {
|
|
310
|
+
return (0, import_node_path2.join)((0, import_node_path2.dirname)(cfg.local.tokenFile), "handshake.local.json");
|
|
311
|
+
}
|
|
312
|
+
function clearPendingHandshake(path) {
|
|
313
|
+
(0, import_node_fs4.rmSync)(path, { force: true });
|
|
314
|
+
}
|
|
315
|
+
function minutesLeft(expiresAt) {
|
|
316
|
+
return Math.max(1, Math.round((expiresAt - Date.now()) / 6e4));
|
|
317
|
+
}
|
|
318
|
+
function approvalHint(pending) {
|
|
319
|
+
return `approve code ${pending.userCode} at ${pending.approvalUrl} (${minutesLeft(pending.expiresAt)}m left)`;
|
|
320
|
+
}
|
|
321
|
+
function approvalReminder(out, pending, periodMs = 3e4) {
|
|
322
|
+
const timer = setInterval(() => {
|
|
323
|
+
for (const line of reminderLines({
|
|
324
|
+
userCode: pending.userCode,
|
|
325
|
+
approvalUrl: pending.approvalUrl,
|
|
326
|
+
minutesLeft: minutesLeft(pending.expiresAt)
|
|
327
|
+
}))
|
|
328
|
+
out.log(line);
|
|
329
|
+
}, periodMs);
|
|
330
|
+
timer.unref?.();
|
|
331
|
+
return () => clearInterval(timer);
|
|
332
|
+
}
|
|
333
|
+
function handshakeWaitMs(waitSeconds, interactive = import_node_process3.default.stdout.isTTY === true) {
|
|
334
|
+
if (waitSeconds !== void 0) return waitSeconds * 1e3;
|
|
335
|
+
return interactive ? void 0 : 9e4;
|
|
336
|
+
}
|
|
337
|
+
var import_node_fs4, import_node_path2, import_node_process3;
|
|
338
|
+
var init_handshake_state = __esm({
|
|
339
|
+
"src/handshake-state.ts"() {
|
|
340
|
+
"use strict";
|
|
341
|
+
init_cjs_shims();
|
|
342
|
+
import_node_fs4 = require("fs");
|
|
343
|
+
import_node_path2 = require("path");
|
|
344
|
+
import_node_process3 = __toESM(require("process"), 1);
|
|
345
|
+
init_approval_prompt();
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
308
349
|
// src/local.ts
|
|
309
350
|
function readJsonFile(path) {
|
|
310
351
|
try {
|
|
311
|
-
return JSON.parse((0,
|
|
352
|
+
return JSON.parse((0, import_node_fs5.readFileSync)(path, "utf8"));
|
|
312
353
|
} catch {
|
|
313
354
|
return null;
|
|
314
355
|
}
|
|
@@ -318,10 +359,10 @@ function writePrivateJson(path, value2) {
|
|
|
318
359
|
`);
|
|
319
360
|
}
|
|
320
361
|
function readCredentials(path) {
|
|
321
|
-
if (!(0,
|
|
362
|
+
if (!(0, import_node_fs5.existsSync)(path)) return null;
|
|
322
363
|
let value2;
|
|
323
364
|
try {
|
|
324
|
-
value2 = JSON.parse((0,
|
|
365
|
+
value2 = JSON.parse((0, import_node_fs5.readFileSync)(path, "utf8"));
|
|
325
366
|
} catch {
|
|
326
367
|
throw new Error(`credentials file ${path} is not valid JSON; fix or remove it before provisioning`);
|
|
327
368
|
}
|
|
@@ -351,14 +392,14 @@ function mergeCredential(current, update) {
|
|
|
351
392
|
return next;
|
|
352
393
|
}
|
|
353
394
|
function ensureGitignore(rootDir, localPaths = []) {
|
|
354
|
-
const path = (0,
|
|
355
|
-
const existing = (0,
|
|
395
|
+
const path = (0, import_node_path3.resolve)(rootDir, ".gitignore");
|
|
396
|
+
const existing = (0, import_node_fs5.existsSync)(path) ? (0, import_node_fs5.readFileSync)(path, "utf8") : "";
|
|
356
397
|
const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line) => !!line);
|
|
357
398
|
const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
|
|
358
399
|
const missing = wanted.filter((line) => !existing.split(/\r?\n/).includes(line));
|
|
359
400
|
if (missing.length === 0) return;
|
|
360
401
|
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
361
|
-
(0,
|
|
402
|
+
(0, import_node_fs5.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
|
|
362
403
|
`);
|
|
363
404
|
}
|
|
364
405
|
function o11yDevVars(cfg) {
|
|
@@ -372,7 +413,7 @@ function o11yDevVars(cfg) {
|
|
|
372
413
|
function resolveWriteDevVarsTarget(cfg, requested) {
|
|
373
414
|
if (!requested) return null;
|
|
374
415
|
if (requested === true) return cfg.local.devVarsFile;
|
|
375
|
-
return (0,
|
|
416
|
+
return (0, import_node_path3.resolve)((0, import_node_path3.dirname)(cfg.configPath), requested);
|
|
376
417
|
}
|
|
377
418
|
function writeDevVars(path, credentials, env, o11y) {
|
|
378
419
|
const entry = credentials.envs[env];
|
|
@@ -386,7 +427,7 @@ function writeDevVars(path, credentials, env, o11y) {
|
|
|
386
427
|
if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
|
|
387
428
|
if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
|
|
388
429
|
}
|
|
389
|
-
const existing = (0,
|
|
430
|
+
const existing = (0, import_node_fs5.existsSync)(path) ? (0, import_node_fs5.readFileSync)(path, "utf8") : "";
|
|
390
431
|
const retained = existing.split(/\r?\n/).filter((line) => !isManagedDevVar(line));
|
|
391
432
|
while (retained.at(-1) === "") retained.pop();
|
|
392
433
|
const prefix = retained.length ? `${retained.join("\n")}
|
|
@@ -400,28 +441,28 @@ function isManagedDevVar(line) {
|
|
|
400
441
|
return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
|
|
401
442
|
}
|
|
402
443
|
function writePrivateText(path, text2) {
|
|
403
|
-
(0,
|
|
444
|
+
(0, import_node_fs5.mkdirSync)((0, import_node_path3.dirname)(path), { recursive: true });
|
|
404
445
|
const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
405
|
-
(0,
|
|
406
|
-
(0,
|
|
407
|
-
(0,
|
|
446
|
+
(0, import_node_fs5.writeFileSync)(temporary, text2, { mode: 384 });
|
|
447
|
+
(0, import_node_fs5.chmodSync)(temporary, 384);
|
|
448
|
+
(0, import_node_fs5.renameSync)(temporary, path);
|
|
408
449
|
}
|
|
409
450
|
function gitignoreEntry(rootDir, path) {
|
|
410
|
-
const rel = (0,
|
|
411
|
-
if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0,
|
|
451
|
+
const rel = (0, import_node_path3.relative)((0, import_node_path3.resolve)(rootDir), (0, import_node_path3.resolve)(path));
|
|
452
|
+
if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path3.isAbsolute)(rel)) return null;
|
|
412
453
|
return rel.replaceAll("\\", "/");
|
|
413
454
|
}
|
|
414
455
|
function displayPath(path, rootDir = process.cwd()) {
|
|
415
|
-
const rel = (0,
|
|
456
|
+
const rel = (0, import_node_path3.relative)(rootDir, path);
|
|
416
457
|
return rel && !rel.startsWith("..") ? rel : path;
|
|
417
458
|
}
|
|
418
|
-
var
|
|
459
|
+
var import_node_fs5, import_node_path3, GITIGNORE_LINES, MANAGED_DEV_VARS;
|
|
419
460
|
var init_local = __esm({
|
|
420
461
|
"src/local.ts"() {
|
|
421
462
|
"use strict";
|
|
422
463
|
init_cjs_shims();
|
|
423
|
-
|
|
424
|
-
|
|
464
|
+
import_node_fs5 = require("fs");
|
|
465
|
+
import_node_path3 = require("path");
|
|
425
466
|
GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
|
|
426
467
|
MANAGED_DEV_VARS = /* @__PURE__ */ new Set([
|
|
427
468
|
"ODLA_PLATFORM",
|
|
@@ -438,71 +479,6 @@ var init_local = __esm({
|
|
|
438
479
|
}
|
|
439
480
|
});
|
|
440
481
|
|
|
441
|
-
// src/handshake-state.ts
|
|
442
|
-
function handshakeFile(cfg) {
|
|
443
|
-
return (0, import_node_path3.join)((0, import_node_path3.dirname)(cfg.local.tokenFile), "handshake.local.json");
|
|
444
|
-
}
|
|
445
|
-
function readPendingHandshake(path, platform, email, requiredGrant) {
|
|
446
|
-
const pending = readJsonFile(path);
|
|
447
|
-
if (!pending || pending.platform !== platform || pending.email !== email) return null;
|
|
448
|
-
if (typeof pending.userCode !== "string" || typeof pending.deviceCode !== "string") return null;
|
|
449
|
-
if (typeof pending.expiresAt !== "number" || pending.expiresAt <= Date.now() + RESUME_MARGIN_MS) return null;
|
|
450
|
-
if (requiredGrant && !grantCovers(pending, requiredGrant)) return null;
|
|
451
|
-
return {
|
|
452
|
-
interval: typeof pending.interval === "number" ? pending.interval : 3,
|
|
453
|
-
...pending,
|
|
454
|
-
approvalUrl: handshakeUrl(platform, pending.userCode)
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
function grantCovers(stored, required) {
|
|
458
|
-
if (required.optionalProjectCapabilities.length === 0) return true;
|
|
459
|
-
return required.projectIds.every((id) => stored.projectIds?.includes(id)) && required.optionalProjectCapabilities.every(
|
|
460
|
-
(capability) => stored.optionalProjectCapabilities?.includes(capability)
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
function writePendingHandshake(path, pending) {
|
|
464
|
-
writePrivateJson(path, pending);
|
|
465
|
-
}
|
|
466
|
-
function clearPendingHandshake(path) {
|
|
467
|
-
(0, import_node_fs5.rmSync)(path, { force: true });
|
|
468
|
-
}
|
|
469
|
-
function minutesLeft(expiresAt) {
|
|
470
|
-
return Math.max(1, Math.round((expiresAt - Date.now()) / 6e4));
|
|
471
|
-
}
|
|
472
|
-
function approvalHint(pending) {
|
|
473
|
-
return `approve code ${pending.userCode} at ${pending.approvalUrl} (${minutesLeft(pending.expiresAt)}m left)`;
|
|
474
|
-
}
|
|
475
|
-
function approvalReminder(out, pending, periodMs = 3e4) {
|
|
476
|
-
const timer = setInterval(() => {
|
|
477
|
-
for (const line of reminderLines({
|
|
478
|
-
userCode: pending.userCode,
|
|
479
|
-
approvalUrl: pending.approvalUrl,
|
|
480
|
-
minutesLeft: minutesLeft(pending.expiresAt)
|
|
481
|
-
}))
|
|
482
|
-
out.log(line);
|
|
483
|
-
}, periodMs);
|
|
484
|
-
timer.unref?.();
|
|
485
|
-
return () => clearInterval(timer);
|
|
486
|
-
}
|
|
487
|
-
function handshakeWaitMs(waitSeconds, interactive = import_node_process3.default.stdout.isTTY === true) {
|
|
488
|
-
if (waitSeconds !== void 0) return waitSeconds * 1e3;
|
|
489
|
-
return interactive ? void 0 : 9e4;
|
|
490
|
-
}
|
|
491
|
-
var import_node_fs5, import_node_path3, import_node_process3, RESUME_MARGIN_MS;
|
|
492
|
-
var init_handshake_state = __esm({
|
|
493
|
-
"src/handshake-state.ts"() {
|
|
494
|
-
"use strict";
|
|
495
|
-
init_cjs_shims();
|
|
496
|
-
import_node_fs5 = require("fs");
|
|
497
|
-
import_node_path3 = require("path");
|
|
498
|
-
import_node_process3 = __toESM(require("process"), 1);
|
|
499
|
-
init_local();
|
|
500
|
-
init_approval_prompt();
|
|
501
|
-
init_handshake_approval();
|
|
502
|
-
RESUME_MARGIN_MS = 5e3;
|
|
503
|
-
}
|
|
504
|
-
});
|
|
505
|
-
|
|
506
482
|
// src/token.ts
|
|
507
483
|
async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {}) {
|
|
508
484
|
const audience = platformAudience(cfg.platformUrl);
|
|
@@ -541,8 +517,8 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
|
|
|
541
517
|
grantIntent
|
|
542
518
|
};
|
|
543
519
|
const waitMs = handshakeWaitMs(options.wait);
|
|
544
|
-
|
|
545
|
-
const { token, expiresAt } = await
|
|
520
|
+
clearPendingHandshake(ctx.pendingFile);
|
|
521
|
+
const { token, expiresAt } = await freshHandshake(ctx, waitMs);
|
|
546
522
|
clearPendingHandshake(ctx.pendingFile);
|
|
547
523
|
writePrivateJson(cfg.local.tokenFile, {
|
|
548
524
|
platform: audience,
|
|
@@ -555,47 +531,6 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
|
|
|
555
531
|
out.error(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
556
532
|
return token;
|
|
557
533
|
}
|
|
558
|
-
async function resumePendingHandshake(ctx, waitMs) {
|
|
559
|
-
const pending = readPendingHandshake(
|
|
560
|
-
ctx.pendingFile,
|
|
561
|
-
ctx.audience,
|
|
562
|
-
ctx.email,
|
|
563
|
-
ctx.grantIntent
|
|
564
|
-
);
|
|
565
|
-
if (!pending) return null;
|
|
566
|
-
ctx.out.error("");
|
|
567
|
-
ctx.out.error(`auth: resuming pending handshake \u2014 ${approvalHint(pending)}`);
|
|
568
|
-
await presentHandshakeApproval(ctx.out, {
|
|
569
|
-
userCode: pending.userCode,
|
|
570
|
-
approvalUrl: pending.approvalUrl,
|
|
571
|
-
minutesLeft: Math.max(1, Math.floor((pending.expiresAt - Date.now()) / 6e4)),
|
|
572
|
-
purpose: `sign this terminal in as ${ctx.email}`
|
|
573
|
-
}, ctx.options);
|
|
574
|
-
ctx.out.error("");
|
|
575
|
-
const stopReminder = approvalReminder(ctx.out, pending);
|
|
576
|
-
try {
|
|
577
|
-
return await (0, import_db.collectToken)({
|
|
578
|
-
endpoint: ctx.cfg.platformUrl,
|
|
579
|
-
deviceCode: pending.deviceCode,
|
|
580
|
-
expiresAt: pending.expiresAt,
|
|
581
|
-
interval: pending.interval,
|
|
582
|
-
waitMs,
|
|
583
|
-
fetch: ctx.doFetch
|
|
584
|
-
});
|
|
585
|
-
} catch (err) {
|
|
586
|
-
const code = err instanceof import_db.OdlaError ? err.code : void 0;
|
|
587
|
-
if (code === "handshake_pending") throw stillPending(pending, ctx.email);
|
|
588
|
-
if (code === "handshake_expired" || code === "handshake_timeout") {
|
|
589
|
-
clearPendingHandshake(ctx.pendingFile);
|
|
590
|
-
ctx.out.error("auth: pending handshake lapsed unapproved; starting a fresh one");
|
|
591
|
-
return null;
|
|
592
|
-
}
|
|
593
|
-
if (code === "handshake_denied") clearPendingHandshake(ctx.pendingFile);
|
|
594
|
-
throw err;
|
|
595
|
-
} finally {
|
|
596
|
-
stopReminder();
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
534
|
async function freshHandshake(ctx, waitMs) {
|
|
600
535
|
let started;
|
|
601
536
|
let stopReminder;
|
|
@@ -623,7 +558,6 @@ async function freshHandshake(ctx, waitMs) {
|
|
|
623
558
|
projectIds: ctx.grantIntent.projectIds,
|
|
624
559
|
optionalProjectCapabilities: ctx.grantIntent.optionalProjectCapabilities
|
|
625
560
|
};
|
|
626
|
-
writePendingHandshake(ctx.pendingFile, started);
|
|
627
561
|
await presentHandshakeApproval(ctx.out, {
|
|
628
562
|
userCode,
|
|
629
563
|
approvalUrl,
|
|
@@ -659,7 +593,7 @@ function projectAgentHandle(appId) {
|
|
|
659
593
|
function stillPending(pending, email) {
|
|
660
594
|
return new import_db.OdlaError(
|
|
661
595
|
"handshake_pending",
|
|
662
|
-
`handshake still pending \u2014 ask ${email} to ${approvalHint(pending)}
|
|
596
|
+
`handshake still pending \u2014 ask ${email} to ${approvalHint(pending)}; if this command exits, re-run it to request a new code`,
|
|
663
597
|
{ retryable: true }
|
|
664
598
|
);
|
|
665
599
|
}
|
|
@@ -5436,28 +5370,31 @@ async function smoke(options) {
|
|
|
5436
5370
|
const out = options.stdout ?? console;
|
|
5437
5371
|
const cfg = await loadProjectConfig(options.configPath);
|
|
5438
5372
|
const env = resolveEnv(cfg, options.env);
|
|
5439
|
-
const
|
|
5440
|
-
|
|
5373
|
+
const runtime = options.runtime === true;
|
|
5374
|
+
const credentials = runtime ? null : readCredentials(cfg.local.credentialsFile);
|
|
5375
|
+
if (!runtime && !credentials) {
|
|
5441
5376
|
throw new Error(`local credentials missing: ${displayPath(cfg.local.credentialsFile, cfg.rootDir)}. Run "odla-ai provision --write-dev-vars".`);
|
|
5442
5377
|
}
|
|
5443
|
-
if (credentials.appId !== cfg.app.id) {
|
|
5378
|
+
if (credentials && credentials.appId !== cfg.app.id) {
|
|
5444
5379
|
throw new Error(`local credentials are for app "${credentials.appId}", but config app is "${cfg.app.id}"`);
|
|
5445
5380
|
}
|
|
5446
|
-
const entry = credentials
|
|
5447
|
-
if (!entry?.tenantId) {
|
|
5381
|
+
const entry = credentials?.envs[env];
|
|
5382
|
+
if (!runtime && !entry?.tenantId) {
|
|
5448
5383
|
throw new Error(`local credentials have no tenant for env "${env}". Run "odla-ai provision --write-dev-vars".`);
|
|
5449
5384
|
}
|
|
5450
5385
|
const hasDb = cfg.services.includes("db");
|
|
5451
5386
|
const hasO11y = cfg.services.includes("o11y");
|
|
5452
|
-
if (hasDb && !entry
|
|
5387
|
+
if (!runtime && hasDb && !entry?.dbKey) {
|
|
5453
5388
|
throw new Error(`local credentials have no db key for env "${env}". Run "odla-ai provision --write-dev-vars".`);
|
|
5454
5389
|
}
|
|
5455
|
-
if (hasO11y && !entry
|
|
5390
|
+
if (!runtime && hasO11y && !entry?.o11yToken) {
|
|
5456
5391
|
throw new Error(`local credentials have no o11y token for env "${env}". Run "odla-ai provision --write-dev-vars".`);
|
|
5457
5392
|
}
|
|
5458
5393
|
const doFetch = options.fetch ?? fetch;
|
|
5394
|
+
const tenantId = entry?.tenantId ?? resolveTenant(cfg, env).tenant;
|
|
5459
5395
|
out.log(`smoke: ${cfg.app.id}/${env}`);
|
|
5460
|
-
out.log(`
|
|
5396
|
+
out.log(` mode: ${runtime ? "runtime (Worker-held credentials)" : "local credentials"}`);
|
|
5397
|
+
out.log(` tenant: ${tenantId}`);
|
|
5461
5398
|
const publicConfig = await getJson(doFetch, publicConfigUrl(cfg.platformUrl, cfg.app.id, env), void 0);
|
|
5462
5399
|
out.log(` public-config: ok`);
|
|
5463
5400
|
if (cfg.services.includes("ai") && cfg.ai?.provider) {
|
|
@@ -5471,7 +5408,7 @@ async function smoke(options) {
|
|
|
5471
5408
|
if (mode !== "hosted") throw new Error(`ai mode mismatch: expected "hosted", public-config has "${String(mode ?? "none")}"`);
|
|
5472
5409
|
out.log(" ai: hosted");
|
|
5473
5410
|
}
|
|
5474
|
-
if (hasO11y) out.log(` o11y: credentials present`);
|
|
5411
|
+
if (hasO11y) out.log(runtime ? ` o11y: Worker-held credential` : ` o11y: credentials present`);
|
|
5475
5412
|
if (cfg.services.includes("calendar")) {
|
|
5476
5413
|
const token = await getDeveloperToken(
|
|
5477
5414
|
cfg,
|
|
@@ -5490,10 +5427,10 @@ async function smoke(options) {
|
|
|
5490
5427
|
out.log(` calendar: ${status.status}, bookable (booking \u2192 ${status.bookingCalendarId ?? "primary"})`);
|
|
5491
5428
|
}
|
|
5492
5429
|
const database = await resolveDatabaseConfig(cfg);
|
|
5493
|
-
if (hasDb) {
|
|
5430
|
+
if (hasDb && !runtime) {
|
|
5494
5431
|
const expectedSchema = database.schema;
|
|
5495
5432
|
const expectedEntities = serializedEntities(expectedSchema);
|
|
5496
|
-
const liveSchemaPayload = await getJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(
|
|
5433
|
+
const liveSchemaPayload = await getJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, entry.dbKey);
|
|
5497
5434
|
const liveSchema = liveSchemaPayload.schema ?? liveSchemaPayload;
|
|
5498
5435
|
const liveEntities = serializedEntities(liveSchema);
|
|
5499
5436
|
if (expectedEntities.length) {
|
|
@@ -5503,7 +5440,7 @@ async function smoke(options) {
|
|
|
5503
5440
|
out.log(` schema: ${liveEntities.length} entities`);
|
|
5504
5441
|
const aggregateEntity = expectedEntities[0] ?? liveEntities[0];
|
|
5505
5442
|
if (aggregateEntity) {
|
|
5506
|
-
const aggregate = await postJson2(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(
|
|
5443
|
+
const aggregate = await postJson2(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/aggregate`, entry.dbKey, {
|
|
5507
5444
|
ns: aggregateEntity,
|
|
5508
5445
|
aggregate: { count: true }
|
|
5509
5446
|
});
|
|
@@ -5512,9 +5449,20 @@ async function smoke(options) {
|
|
|
5512
5449
|
} else {
|
|
5513
5450
|
out.log(` aggregate: skipped (schema has no entities)`);
|
|
5514
5451
|
}
|
|
5452
|
+
} else if (hasDb) {
|
|
5453
|
+
out.log(` db: Worker-held credential (direct schema/aggregate skipped)`);
|
|
5515
5454
|
} else {
|
|
5516
5455
|
out.log(` db: skipped (not enabled)`);
|
|
5517
5456
|
}
|
|
5457
|
+
if (runtime) {
|
|
5458
|
+
const link = cfg.links?.[env];
|
|
5459
|
+
if (!link) throw new Error(`runtime smoke requires links.${env} in odla.config.mjs`);
|
|
5460
|
+
const res = await doFetch(link, { redirect: "manual" });
|
|
5461
|
+
if (res.status < 200 || res.status >= 500) {
|
|
5462
|
+
throw new Error(`runtime target ${new URL(link).origin} returned ${res.status}`);
|
|
5463
|
+
}
|
|
5464
|
+
out.log(` runtime target: ${res.status}`);
|
|
5465
|
+
}
|
|
5518
5466
|
const probes = database.integrations.flatMap(
|
|
5519
5467
|
(integration) => (integration.probes ?? []).map((probe) => ({ integration: integration.id, probe }))
|
|
5520
5468
|
);
|
|
@@ -5729,10 +5677,11 @@ async function projectCommand(command, parsed, deps) {
|
|
|
5729
5677
|
return true;
|
|
5730
5678
|
}
|
|
5731
5679
|
if (command === "smoke") {
|
|
5732
|
-
assertArgs(parsed, ["config", "env", "token", "email", "open"], 1);
|
|
5680
|
+
assertArgs(parsed, ["config", "env", "runtime", "token", "email", "open"], 1);
|
|
5733
5681
|
await smoke({
|
|
5734
5682
|
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
5735
5683
|
env: stringOpt(parsed.options.env),
|
|
5684
|
+
runtime: parsed.options.runtime === true,
|
|
5736
5685
|
token: stringOpt(parsed.options.token),
|
|
5737
5686
|
email: stringOpt(parsed.options.email),
|
|
5738
5687
|
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
@@ -9727,7 +9676,7 @@ Usage:
|
|
|
9727
9676
|
odla-ai provision [--live] [--config odla.config.mjs] [--email <odla-account>] [--request-grant] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
9728
9677
|
odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
|
|
9729
9678
|
odla-ai credentials revoke <receipt-id> [--config odla.config.mjs] [--json]
|
|
9730
|
-
odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
|
|
9679
|
+
odla-ai smoke [--config odla.config.mjs] [--env dev] [--runtime] [--email <odla-account>] [--no-open]
|
|
9731
9680
|
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
9732
9681
|
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
9733
9682
|
odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
|
|
@@ -9831,7 +9780,7 @@ Commands:
|
|
|
9831
9780
|
"provision --live --yes" initializes only the live instance of
|
|
9832
9781
|
an existing sandbox app and enables every configured service;
|
|
9833
9782
|
no edit to the dev-first envs list is required.
|
|
9834
|
-
smoke Verify
|
|
9783
|
+
smoke Verify local service access, or use --runtime for a Worker-held credential deployment.
|
|
9835
9784
|
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
9836
9785
|
copilot, gemini, or agents (repeatable or comma-separated).
|
|
9837
9786
|
secrets Push configured db/o11y secrets into the Worker via wrangler
|
|
@@ -9871,12 +9820,11 @@ Safety:
|
|
|
9871
9820
|
attempts to open it \u2014 including in CI, SSH, display-less, and agent-driven
|
|
9872
9821
|
shells. Only --no-open suppresses the attempt. Browser launch is best-effort:
|
|
9873
9822
|
agents with browser control must open that exact URL immediately; otherwise
|
|
9874
|
-
they must give it to the human verbatim. A
|
|
9875
|
-
|
|
9876
|
-
the same code. Outside an interactive terminal the wait is capped (90s by
|
|
9823
|
+
they must give it to the human verbatim. A device code remains only in the
|
|
9824
|
+
running process. Outside an interactive terminal the wait is capped (90s by
|
|
9877
9825
|
default, --wait <seconds> to change); a still-pending handshake then exits
|
|
9878
|
-
with code 75
|
|
9879
|
-
|
|
9826
|
+
with code 75. Rerunning always requests and opens a new code; older clients'
|
|
9827
|
+
persisted pending state is discarded.
|
|
9880
9828
|
A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
|
|
9881
9829
|
The email is a non-secret identity hint: never provide a password or session
|
|
9882
9830
|
token. It is the email shown by the signed-in odla account \u2014 never infer it
|