@odla-ai/cli 0.29.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/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, import_node_fs4.readFileSync)(path, "utf8"));
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, import_node_fs4.existsSync)(path)) return null;
362
+ if (!(0, import_node_fs5.existsSync)(path)) return null;
322
363
  let value2;
323
364
  try {
324
- value2 = JSON.parse((0, import_node_fs4.readFileSync)(path, "utf8"));
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, import_node_path2.resolve)(rootDir, ".gitignore");
355
- const existing = (0, import_node_fs4.existsSync)(path) ? (0, import_node_fs4.readFileSync)(path, "utf8") : "";
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, import_node_fs4.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
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, import_node_path2.resolve)((0, import_node_path2.dirname)(cfg.configPath), requested);
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, import_node_fs4.existsSync)(path) ? (0, import_node_fs4.readFileSync)(path, "utf8") : "";
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, import_node_fs4.mkdirSync)((0, import_node_path2.dirname)(path), { recursive: true });
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, import_node_fs4.writeFileSync)(temporary, text2, { mode: 384 });
406
- (0, import_node_fs4.chmodSync)(temporary, 384);
407
- (0, import_node_fs4.renameSync)(temporary, path);
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, import_node_path2.relative)((0, import_node_path2.resolve)(rootDir), (0, import_node_path2.resolve)(path));
411
- if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path2.isAbsolute)(rel)) return null;
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, import_node_path2.relative)(rootDir, path);
456
+ const rel = (0, import_node_path3.relative)(rootDir, path);
416
457
  return rel && !rel.startsWith("..") ? rel : path;
417
458
  }
418
- var import_node_fs4, import_node_path2, GITIGNORE_LINES, MANAGED_DEV_VARS;
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
- import_node_fs4 = require("fs");
424
- import_node_path2 = require("path");
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
- if (grantRequest.freshLogin) clearPendingHandshake(ctx.pendingFile);
545
- const { token, expiresAt } = await resumePendingHandshake(ctx, waitMs) ?? await freshHandshake(ctx, waitMs);
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)}, then re-run this command; it resumes the same handshake and collects the token`,
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
  }
@@ -4376,6 +4310,41 @@ async function wranglerLoggedIn(run, cwd) {
4376
4310
  return false;
4377
4311
  }
4378
4312
  }
4313
+ async function wranglerRuntimeTarget(run, opts) {
4314
+ const configPath = findWranglerConfig(opts.cwd);
4315
+ if (!configPath) throw new Error(`no wrangler config found in ${opts.cwd}`);
4316
+ const config = readWranglerConfig(configPath);
4317
+ if (!config) throw new Error("runtime credential delivery requires wrangler.json or wrangler.jsonc");
4318
+ const baseName = typeof config.name === "string" ? config.name : "";
4319
+ const envs = config.env && typeof config.env === "object" && !Array.isArray(config.env) ? config.env : {};
4320
+ const envConfig = opts.env && envs[opts.env] && typeof envs[opts.env] === "object" ? envs[opts.env] : {};
4321
+ const explicitName = typeof envConfig.name === "string" ? envConfig.name : "";
4322
+ const serviceEnvironments = config.legacy_env === false;
4323
+ if (serviceEnvironments && explicitName) {
4324
+ throw new Error("env.<name>.name is not allowed when wrangler legacy_env is false");
4325
+ }
4326
+ const scriptName = serviceEnvironments ? baseName : explicitName || (opts.env ? `${baseName}-${opts.env}` : baseName);
4327
+ if (!/^[a-z0-9][a-z0-9_-]{0,62}$/.test(scriptName)) {
4328
+ throw new Error("wrangler config must resolve an exact Worker name before credentials are issued");
4329
+ }
4330
+ const configuredAccount = typeof envConfig.account_id === "string" ? envConfig.account_id : typeof config.account_id === "string" ? config.account_id : "";
4331
+ const whoami = await run("npx", ["wrangler", "whoami"], { cwd: opts.cwd });
4332
+ if (whoami.code !== 0 || /not authenticated/i.test(`${whoami.stdout}${whoami.stderr}`)) {
4333
+ throw new Error(`wrangler is not logged in \u2014 run "wrangler login" (a browser step for the human)`);
4334
+ }
4335
+ const discovered = [...new Set(`${whoami.stdout}
4336
+ ${whoami.stderr}`.match(/\b[a-f0-9]{32}\b/gi)?.map((id) => id.toLowerCase()) ?? [])];
4337
+ const accountId = configuredAccount.toLowerCase() || (discovered.length === 1 ? discovered[0] : "");
4338
+ if (!/^[a-f0-9]{32}$/.test(accountId) || configuredAccount && discovered.length > 0 && !discovered.includes(accountId)) {
4339
+ throw new Error("Wrangler account is ambiguous or does not match account_id in the config");
4340
+ }
4341
+ return {
4342
+ provider: "cloudflare",
4343
+ accountId,
4344
+ scriptName,
4345
+ ...opts.env ? { environment: opts.env } : {}
4346
+ };
4347
+ }
4379
4348
  function wranglerPutSecret(run, opts) {
4380
4349
  const args = [
4381
4350
  "wrangler",
@@ -4387,6 +4356,16 @@ function wranglerPutSecret(run, opts) {
4387
4356
  ];
4388
4357
  return run("npx", args, { input: opts.value, cwd: opts.cwd });
4389
4358
  }
4359
+ function wranglerBulkSecrets(run, opts) {
4360
+ const args = [
4361
+ "wrangler",
4362
+ "secret",
4363
+ "bulk",
4364
+ ...opts.env ? ["--env", opts.env] : [],
4365
+ ...opts.configPath ? ["--config", opts.configPath] : []
4366
+ ];
4367
+ return run("npx", args, { input: JSON.stringify(opts.secrets), cwd: opts.cwd });
4368
+ }
4390
4369
  var import_node_child_process2, import_node_fs12, import_node_path11, defaultRunner, WRANGLER_CONFIG_FILES;
4391
4370
  var init_wrangler = __esm({
4392
4371
  "src/wrangler.ts"() {
@@ -4947,9 +4926,6 @@ var init_init = __esm({
4947
4926
  async function secretsPush(options) {
4948
4927
  await secretsPushImpl(options, true);
4949
4928
  }
4950
- async function secretsPushAfterPreflight(options) {
4951
- await secretsPushImpl(options, false);
4952
- }
4953
4929
  async function secretsPushImpl(options, preflight) {
4954
4930
  const out = options.stdout ?? console;
4955
4931
  const cfg = await loadProjectConfig(options.configPath);
@@ -5394,28 +5370,31 @@ async function smoke(options) {
5394
5370
  const out = options.stdout ?? console;
5395
5371
  const cfg = await loadProjectConfig(options.configPath);
5396
5372
  const env = resolveEnv(cfg, options.env);
5397
- const credentials = readCredentials(cfg.local.credentialsFile);
5398
- if (!credentials) {
5373
+ const runtime = options.runtime === true;
5374
+ const credentials = runtime ? null : readCredentials(cfg.local.credentialsFile);
5375
+ if (!runtime && !credentials) {
5399
5376
  throw new Error(`local credentials missing: ${displayPath(cfg.local.credentialsFile, cfg.rootDir)}. Run "odla-ai provision --write-dev-vars".`);
5400
5377
  }
5401
- if (credentials.appId !== cfg.app.id) {
5378
+ if (credentials && credentials.appId !== cfg.app.id) {
5402
5379
  throw new Error(`local credentials are for app "${credentials.appId}", but config app is "${cfg.app.id}"`);
5403
5380
  }
5404
- const entry = credentials.envs[env];
5405
- if (!entry?.tenantId) {
5381
+ const entry = credentials?.envs[env];
5382
+ if (!runtime && !entry?.tenantId) {
5406
5383
  throw new Error(`local credentials have no tenant for env "${env}". Run "odla-ai provision --write-dev-vars".`);
5407
5384
  }
5408
5385
  const hasDb = cfg.services.includes("db");
5409
5386
  const hasO11y = cfg.services.includes("o11y");
5410
- if (hasDb && !entry.dbKey) {
5387
+ if (!runtime && hasDb && !entry?.dbKey) {
5411
5388
  throw new Error(`local credentials have no db key for env "${env}". Run "odla-ai provision --write-dev-vars".`);
5412
5389
  }
5413
- if (hasO11y && !entry.o11yToken) {
5390
+ if (!runtime && hasO11y && !entry?.o11yToken) {
5414
5391
  throw new Error(`local credentials have no o11y token for env "${env}". Run "odla-ai provision --write-dev-vars".`);
5415
5392
  }
5416
5393
  const doFetch = options.fetch ?? fetch;
5394
+ const tenantId = entry?.tenantId ?? resolveTenant(cfg, env).tenant;
5417
5395
  out.log(`smoke: ${cfg.app.id}/${env}`);
5418
- out.log(` tenant: ${entry.tenantId}`);
5396
+ out.log(` mode: ${runtime ? "runtime (Worker-held credentials)" : "local credentials"}`);
5397
+ out.log(` tenant: ${tenantId}`);
5419
5398
  const publicConfig = await getJson(doFetch, publicConfigUrl(cfg.platformUrl, cfg.app.id, env), void 0);
5420
5399
  out.log(` public-config: ok`);
5421
5400
  if (cfg.services.includes("ai") && cfg.ai?.provider) {
@@ -5429,7 +5408,7 @@ async function smoke(options) {
5429
5408
  if (mode !== "hosted") throw new Error(`ai mode mismatch: expected "hosted", public-config has "${String(mode ?? "none")}"`);
5430
5409
  out.log(" ai: hosted");
5431
5410
  }
5432
- if (hasO11y) out.log(` o11y: credentials present`);
5411
+ if (hasO11y) out.log(runtime ? ` o11y: Worker-held credential` : ` o11y: credentials present`);
5433
5412
  if (cfg.services.includes("calendar")) {
5434
5413
  const token = await getDeveloperToken(
5435
5414
  cfg,
@@ -5448,10 +5427,10 @@ async function smoke(options) {
5448
5427
  out.log(` calendar: ${status.status}, bookable (booking \u2192 ${status.bookingCalendarId ?? "primary"})`);
5449
5428
  }
5450
5429
  const database = await resolveDatabaseConfig(cfg);
5451
- if (hasDb) {
5430
+ if (hasDb && !runtime) {
5452
5431
  const expectedSchema = database.schema;
5453
5432
  const expectedEntities = serializedEntities(expectedSchema);
5454
- const liveSchemaPayload = await getJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/schema`, entry.dbKey);
5433
+ const liveSchemaPayload = await getJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, entry.dbKey);
5455
5434
  const liveSchema = liveSchemaPayload.schema ?? liveSchemaPayload;
5456
5435
  const liveEntities = serializedEntities(liveSchema);
5457
5436
  if (expectedEntities.length) {
@@ -5461,7 +5440,7 @@ async function smoke(options) {
5461
5440
  out.log(` schema: ${liveEntities.length} entities`);
5462
5441
  const aggregateEntity = expectedEntities[0] ?? liveEntities[0];
5463
5442
  if (aggregateEntity) {
5464
- const aggregate = await postJson2(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/aggregate`, entry.dbKey, {
5443
+ const aggregate = await postJson2(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/aggregate`, entry.dbKey, {
5465
5444
  ns: aggregateEntity,
5466
5445
  aggregate: { count: true }
5467
5446
  });
@@ -5470,9 +5449,20 @@ async function smoke(options) {
5470
5449
  } else {
5471
5450
  out.log(` aggregate: skipped (schema has no entities)`);
5472
5451
  }
5452
+ } else if (hasDb) {
5453
+ out.log(` db: Worker-held credential (direct schema/aggregate skipped)`);
5473
5454
  } else {
5474
5455
  out.log(` db: skipped (not enabled)`);
5475
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
+ }
5476
5466
  const probes = database.integrations.flatMap(
5477
5467
  (integration) => (integration.probes ?? []).map((probe) => ({ integration: integration.id, probe }))
5478
5468
  );
@@ -5687,10 +5677,11 @@ async function projectCommand(command, parsed, deps) {
5687
5677
  return true;
5688
5678
  }
5689
5679
  if (command === "smoke") {
5690
- assertArgs(parsed, ["config", "env", "token", "email", "open"], 1);
5680
+ assertArgs(parsed, ["config", "env", "runtime", "token", "email", "open"], 1);
5691
5681
  await smoke({
5692
5682
  configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
5693
5683
  env: stringOpt(parsed.options.env),
5684
+ runtime: parsed.options.runtime === true,
5694
5685
  token: stringOpt(parsed.options.token),
5695
5686
  email: stringOpt(parsed.options.email),
5696
5687
  open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
@@ -9501,6 +9492,65 @@ var init_context_command = __esm({
9501
9492
  }
9502
9493
  });
9503
9494
 
9495
+ // src/credential-command.ts
9496
+ async function responseError(response2) {
9497
+ return redactSecrets((await response2.text()).slice(0, 1e3));
9498
+ }
9499
+ async function credentialCommand(parsed, deps = {}) {
9500
+ const action2 = parsed.positionals[1] ?? "list";
9501
+ if (action2 !== "list" && action2 !== "revoke") {
9502
+ throw new Error(`unknown credentials action "${action2}". Try "odla-ai credentials list".`);
9503
+ }
9504
+ assertArgs(parsed, ["config", "env", "all", "json", "token", "email", "open"], action2 === "revoke" ? 3 : 2);
9505
+ const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
9506
+ const doFetch = deps.fetch ?? fetch;
9507
+ const out = deps.stdout ?? console;
9508
+ const token = await getDeveloperToken(cfg, {
9509
+ configPath: cfg.configPath,
9510
+ token: stringOpt(parsed.options.token),
9511
+ email: stringOpt(parsed.options.email),
9512
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
9513
+ openApprovalUrl: deps.openUrl
9514
+ }, doFetch, out, { optionalProjectCapabilities: ["app.manage"] });
9515
+ const base = `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/runtime-credentials`;
9516
+ if (action2 === "revoke") {
9517
+ const id = parsed.positionals[2];
9518
+ if (!id) throw new Error("credentials revoke requires the exact receipt id from credentials list");
9519
+ const response3 = await doFetch(`${base}/${encodeURIComponent(id)}`, {
9520
+ method: "DELETE",
9521
+ headers: { authorization: `Bearer ${token}` }
9522
+ });
9523
+ if (!response3.ok) throw new Error(`runtime credential revoke failed (${response3.status}): ${await responseError(response3)}`);
9524
+ const body2 = await response3.json();
9525
+ return out.log(parsed.options.json === true ? JSON.stringify(body2, null, 2) : `revoked ${body2.receipt.id} (${body2.receipt.env} ${body2.receipt.target.scriptName})`);
9526
+ }
9527
+ const query = new URLSearchParams();
9528
+ const requestedEnv = stringOpt(parsed.options.env);
9529
+ if (requestedEnv) query.set("env", requestedEnv);
9530
+ const response2 = await doFetch(`${base}${query.size ? `?${query}` : ""}`, {
9531
+ headers: { authorization: `Bearer ${token}` }
9532
+ });
9533
+ if (!response2.ok) throw new Error(`runtime credential inventory failed (${response2.status}): ${await responseError(response2)}`);
9534
+ const body = await response2.json();
9535
+ const credentials = parsed.options.all === true ? body.credentials : body.credentials.filter((item) => item.state === "committed");
9536
+ if (parsed.options.json === true) return out.log(JSON.stringify({ credentials }, null, 2));
9537
+ if (!credentials.length) return out.log("no active runtime credential receipts");
9538
+ out.log("RECEIPT ENV TARGET CREATED");
9539
+ for (const item of credentials) {
9540
+ out.log(`${item.id} ${item.env} ${item.target.accountId ?? "local"}/${item.target.scriptName} ${new Date(item.createdAt).toISOString()}`);
9541
+ }
9542
+ }
9543
+ var init_credential_command = __esm({
9544
+ "src/credential-command.ts"() {
9545
+ "use strict";
9546
+ init_cjs_shims();
9547
+ init_argv();
9548
+ init_config();
9549
+ init_redact();
9550
+ init_token();
9551
+ }
9552
+ });
9553
+
9504
9554
  // src/help-usage.ts
9505
9555
  var USAGE_SECTION;
9506
9556
  var init_help_usage = __esm({
@@ -9624,7 +9674,9 @@ Usage:
9624
9674
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
9625
9675
  odla-ai security run [target] --self --ack-redacted-source
9626
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]
9627
- odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
9677
+ odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
9678
+ odla-ai credentials revoke <receipt-id> [--config odla.config.mjs] [--json]
9679
+ odla-ai smoke [--config odla.config.mjs] [--env dev] [--runtime] [--email <odla-account>] [--no-open]
9628
9680
  odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
9629
9681
  odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
9630
9682
  odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
@@ -9728,7 +9780,7 @@ Commands:
9728
9780
  "provision --live --yes" initializes only the live instance of
9729
9781
  an existing sandbox app and enables every configured service;
9730
9782
  no edit to the dev-first envs list is required.
9731
- smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
9783
+ smoke Verify local service access, or use --runtime for a Worker-held credential deployment.
9732
9784
  skill Same installer; --agent accepts all, claude, codex, cursor,
9733
9785
  copilot, gemini, or agents (repeatable or comma-separated).
9734
9786
  secrets Push configured db/o11y secrets into the Worker via wrangler
@@ -9748,9 +9800,13 @@ Safety:
9748
9800
  pinned versions of every external @odla-ai runtime module before importing
9749
9801
  the command graph. A stale workspace module blocks with its resolved path and
9750
9802
  tells the agent to update/rebase, npm ci, and rebuild.
9751
- Provision caches the approved developer token and service credentials under
9752
- .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
9753
- preflights Wrangler before any shown-once issuance or destructive rotation.
9803
+ Provision caches the approved developer token under .odla/ with mode 0600.
9804
+ Local-only development provisioning may also cache that developer's service
9805
+ credentials there. \`provision --push-secrets\` instead stages a fresh,
9806
+ independently revocable credential set through the approved handshake,
9807
+ transfers the complete set to the exact Worker with \`wrangler secret bulk\`
9808
+ over stdin, and never writes its plaintext under .odla. Secret push preflights
9809
+ Wrangler before shown-once issuance; it never rotates sibling credentials.
9754
9810
  Projectless PM, Discussions, o11y, runbook, and identity commands use
9755
9811
  --platform/--app/--env, ODLA_PLATFORM_URL/ODLA_APP_ID/ODLA_ENV, and
9756
9812
  ODLA_DEV_TOKEN. Save non-secret scope metadata with "context save", then
@@ -9764,12 +9820,11 @@ Safety:
9764
9820
  attempts to open it \u2014 including in CI, SSH, display-less, and agent-driven
9765
9821
  shells. Only --no-open suppresses the attempt. Browser launch is best-effort:
9766
9822
  agents with browser control must open that exact URL immediately; otherwise
9767
- they must give it to the human verbatim. A started handshake is persisted
9768
- under .odla/, so a command killed mid-wait loses nothing \u2014 rerunning resumes
9769
- 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
9770
9825
  default, --wait <seconds> to change); a still-pending handshake then exits
9771
- with code 75: open the same URL (or relay it if browser control is unavailable),
9772
- wait for approval, and re-run to collect.
9826
+ with code 75. Rerunning always requests and opens a new code; older clients'
9827
+ persisted pending state is discarded.
9773
9828
  A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
9774
9829
  The email is a non-secret identity hint: never provide a password or session
9775
9830
  token. It is the email shown by the signed-in odla account \u2014 never infer it
@@ -11829,7 +11884,7 @@ async function issueO11yToken(opts) {
11829
11884
  );
11830
11885
  if (res.status === 409 && !opts.rotateO11y) {
11831
11886
  throw new Error(
11832
- `o11y token already exists for env "${opts.env}", but its shown-once value is not in the local credentials file; run "odla-ai provision --rotate-o11y-token --push-secrets" to replace it explicitly`
11887
+ `o11y token already exists for env "${opts.env}", but its shown-once value is not in the local credentials file; run "odla-ai provision --push-secrets" to install a separate runtime credential without rotating siblings`
11833
11888
  );
11834
11889
  }
11835
11890
  if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText7(res)}`);
@@ -11855,6 +11910,97 @@ var init_provision_credentials = __esm({
11855
11910
  }
11856
11911
  });
11857
11912
 
11913
+ // src/runtime-credentials.ts
11914
+ function runtimeUrl(cfg, suffix = "") {
11915
+ return `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/runtime-credentials${suffix}`;
11916
+ }
11917
+ async function safeError(response2) {
11918
+ const text2 = await response2.text();
11919
+ return redactSecrets(text2.slice(0, 1e3));
11920
+ }
11921
+ async function finish(doFetch, cfg, token, sessionId, method) {
11922
+ return doFetch(runtimeUrl(cfg, `/${encodeURIComponent(sessionId)}`), {
11923
+ method,
11924
+ headers: { authorization: `Bearer ${token}` }
11925
+ });
11926
+ }
11927
+ async function deliverRuntimeCredentials(cfg, options) {
11928
+ const doFetch = options.fetch ?? fetch;
11929
+ const run = options.runner ?? defaultRunner;
11930
+ const wranglerEnv = options.env === "prod" || options.env === "production" ? void 0 : options.env;
11931
+ const target = await wranglerRuntimeTarget(run, {
11932
+ cwd: cfg.rootDir,
11933
+ env: wranglerEnv
11934
+ });
11935
+ const issue = await doFetch(runtimeUrl(cfg), {
11936
+ method: "POST",
11937
+ headers: {
11938
+ authorization: `Bearer ${options.developerToken}`,
11939
+ "content-type": "application/json"
11940
+ },
11941
+ body: JSON.stringify({
11942
+ env: options.env,
11943
+ idempotencyKey: `wrangler:${(0, import_node_crypto5.randomUUID)()}`,
11944
+ target
11945
+ })
11946
+ });
11947
+ const body = await issue.json().catch(() => null);
11948
+ if (!issue.ok || typeof body?.sessionId !== "string" || !body.secrets || typeof body.secrets !== "object") {
11949
+ const detail = body?.error?.code ?? body?.error?.message ?? issue.status;
11950
+ throw new Error(`runtime credential staging failed: ${String(detail)}`);
11951
+ }
11952
+ const secrets = body.secrets;
11953
+ const values = {};
11954
+ for (const name of ["ODLA_API_KEY", "ODLA_O11Y_TOKEN"]) {
11955
+ const value2 = secrets[name];
11956
+ if (typeof value2 === "string" && value2) values[name] = value2;
11957
+ }
11958
+ if (Object.keys(values).length === 0) {
11959
+ await finish(doFetch, cfg, options.developerToken, body.sessionId, "DELETE");
11960
+ throw new Error("runtime credential staging returned no configured service credentials");
11961
+ }
11962
+ const pushed = await wranglerBulkSecrets(run, {
11963
+ secrets: values,
11964
+ env: wranglerEnv,
11965
+ cwd: cfg.rootDir
11966
+ });
11967
+ if (pushed.code !== 0) {
11968
+ await finish(doFetch, cfg, options.developerToken, body.sessionId, "DELETE");
11969
+ const detail = redactSecrets(`${pushed.stderr || pushed.stdout}`.trim());
11970
+ throw new Error(`wrangler secret bulk failed (exit ${pushed.code}): ${detail}`);
11971
+ }
11972
+ const committed = await finish(
11973
+ doFetch,
11974
+ cfg,
11975
+ options.developerToken,
11976
+ body.sessionId,
11977
+ "POST"
11978
+ );
11979
+ if (!committed.ok) {
11980
+ await finish(doFetch, cfg, options.developerToken, body.sessionId, "DELETE");
11981
+ throw new Error(`runtime credential receipt commit failed: ${await safeError(committed)}`);
11982
+ }
11983
+ options.stdout?.log(
11984
+ `${options.env}: installed ${Object.keys(values).join(" + ")} on ${target.accountId}/${target.scriptName}${target.environment ? ` (${target.environment})` : ""}; plaintext stayed in memory`
11985
+ );
11986
+ return {
11987
+ sessionId: body.sessionId,
11988
+ target,
11989
+ ...values.ODLA_API_KEY ? { dbKey: values.ODLA_API_KEY } : {},
11990
+ ...values.ODLA_O11Y_TOKEN ? { o11yToken: values.ODLA_O11Y_TOKEN } : {}
11991
+ };
11992
+ }
11993
+ var import_node_crypto5;
11994
+ var init_runtime_credentials = __esm({
11995
+ "src/runtime-credentials.ts"() {
11996
+ "use strict";
11997
+ init_cjs_shims();
11998
+ import_node_crypto5 = require("crypto");
11999
+ init_redact();
12000
+ init_wrangler();
12001
+ }
12002
+ });
12003
+
11858
12004
  // src/provision-live.ts
11859
12005
  function liveProvisionConfig(cfg) {
11860
12006
  if (!cfg.envs.includes("dev")) {
@@ -11894,8 +12040,10 @@ async function provision(options) {
11894
12040
  if (options.rotateO11yToken && !hasO11y) {
11895
12041
  throw new Error("--rotate-o11y-token requires the o11y service in odla.config.mjs");
11896
12042
  }
11897
- if (options.pushSecrets && options.writeCredentials === false) {
11898
- throw new Error("--push-secrets cannot be combined with --no-write-credentials");
12043
+ if (options.pushSecrets && (options.rotateKeys || options.rotateO11yToken)) {
12044
+ throw new Error(
12045
+ "--push-secrets always issues an independent runtime credential set; do not combine it with destructive rotation flags"
12046
+ );
11899
12047
  }
11900
12048
  out.log(`odla-ai: ${plan.appName} (${plan.appId})`);
11901
12049
  out.log(` platform: ${plan.platformUrl}`);
@@ -11932,7 +12080,7 @@ async function provision(options) {
11932
12080
  out.log(` secrets: ${options.pushSecrets ? "push configured Worker secrets" : "local only"}`);
11933
12081
  return;
11934
12082
  }
11935
- let credentials = readCredentials(cfg.local.credentialsFile);
12083
+ let credentials = options.pushSecrets ? null : readCredentials(cfg.local.credentialsFile);
11936
12084
  if (credentials && credentials.appId !== cfg.app.id) {
11937
12085
  throw new Error(
11938
12086
  `credentials at ${displayPath(cfg.local.credentialsFile, cfg.rootDir)} are for "${credentials.appId}", not "${cfg.app.id}"`
@@ -11941,7 +12089,7 @@ async function provision(options) {
11941
12089
  const rotatesO11y = !!(options.rotateKeys || options.rotateO11yToken);
11942
12090
  const missingO11y = cfg.envs.some((env) => !credentials?.envs[env]?.o11yToken);
11943
12091
  const missingDb = cfg.envs.some((env) => !credentials?.envs[env]?.dbKey);
11944
- const losesShownOnceCredential = hasDb && (!!options.rotateKeys || missingDb) || hasO11y && (rotatesO11y || missingO11y);
12092
+ const losesShownOnceCredential = !options.pushSecrets && (hasDb && (!!options.rotateKeys || missingDb) || hasO11y && (rotatesO11y || missingO11y));
11945
12093
  if (options.writeCredentials === false && losesShownOnceCredential) {
11946
12094
  throw new Error("credential issuance/rotation requires the private credentials file; remove --no-write-credentials");
11947
12095
  }
@@ -12015,38 +12163,42 @@ async function provision(options) {
12015
12163
  await applyCalendarBookingPage(calendarCtx, calendarBookingPageUrl(cfg, env), out);
12016
12164
  }
12017
12165
  }
12166
+ let devVarsCredentials = credentials;
12018
12167
  for (const env of cfg.envs) {
12019
12168
  const tenantId = (0, import_apps12.tenantIdFor)(cfg.app.id, env);
12020
- credentials = await provisionEnvCredentials({
12021
- cfg,
12022
- env,
12023
- developerToken: token,
12024
- credentials,
12025
- rotateDb: !!options.rotateKeys,
12026
- rotateO11y: rotatesO11y,
12027
- write: options.writeCredentials !== false,
12028
- fetch: doFetch,
12029
- stdout: out
12030
- });
12031
- const dbKey = credentials.envs[env]?.dbKey;
12169
+ let dbKey;
12032
12170
  if (options.pushSecrets) {
12033
- try {
12034
- await secretsPushAfterPreflight({
12035
- configPath: cfg.configPath,
12036
- env,
12037
- yes: options.yes,
12038
- runner: options.secretRunner,
12039
- stdout: out
12040
- });
12041
- } catch (error) {
12042
- const message2 = error instanceof Error ? error.message : String(error);
12043
- const consent = env === "prod" || env === "production" ? " --yes" : "";
12044
- throw new Error(
12045
- `${message2}
12046
- ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}${consent}" without issuing or rotating again`,
12047
- { cause: error }
12048
- );
12049
- }
12171
+ const delivered = await deliverRuntimeCredentials(cfg, {
12172
+ env,
12173
+ developerToken: token,
12174
+ fetch: doFetch,
12175
+ runner: options.secretRunner,
12176
+ stdout: out
12177
+ });
12178
+ dbKey = delivered.dbKey;
12179
+ devVarsCredentials = mergeCredential(devVarsCredentials, {
12180
+ appId: cfg.app.id,
12181
+ platformUrl: cfg.platformUrl,
12182
+ dbEndpoint: cfg.dbEndpoint,
12183
+ env,
12184
+ tenantId,
12185
+ ...delivered.dbKey ? { dbKey: delivered.dbKey } : {},
12186
+ ...delivered.o11yToken ? { o11yToken: delivered.o11yToken } : {}
12187
+ });
12188
+ } else {
12189
+ credentials = await provisionEnvCredentials({
12190
+ cfg,
12191
+ env,
12192
+ developerToken: token,
12193
+ credentials,
12194
+ rotateDb: !!options.rotateKeys,
12195
+ rotateO11y: rotatesO11y,
12196
+ write: options.writeCredentials !== false,
12197
+ fetch: doFetch,
12198
+ stdout: out
12199
+ });
12200
+ devVarsCredentials = credentials;
12201
+ dbKey = credentials.envs[env]?.dbKey;
12050
12202
  }
12051
12203
  if (schema && dbKey) {
12052
12204
  await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
@@ -12070,13 +12222,13 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
12070
12222
  }
12071
12223
  }
12072
12224
  }
12073
- if (options.writeCredentials !== false && credentials) {
12225
+ if (!options.pushSecrets && options.writeCredentials !== false && credentials) {
12074
12226
  const ignored = cfg.local.gitignore && gitignoreEntry(cfg.rootDir, cfg.local.credentialsFile);
12075
12227
  out.log(`credentials: wrote ${displayPath(cfg.local.credentialsFile, cfg.rootDir)} (0600${ignored ? ", gitignored" : ""})`);
12076
12228
  }
12077
- if (devVarsTarget && credentials) {
12229
+ if (devVarsTarget && devVarsCredentials) {
12078
12230
  const env = cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
12079
- writeDevVars(devVarsTarget, credentials, env, o11yDevVars(cfg));
12231
+ writeDevVars(devVarsTarget, devVarsCredentials, env, o11yDevVars(cfg));
12080
12232
  out.log(`dev vars: wrote ${displayPath(devVarsTarget, cfg.rootDir)} for ${env}`);
12081
12233
  }
12082
12234
  if (cfg.services.includes("calendar")) {
@@ -12116,6 +12268,7 @@ var init_provision = __esm({
12116
12268
  init_token();
12117
12269
  init_local();
12118
12270
  init_provision_credentials();
12271
+ init_runtime_credentials();
12119
12272
  init_provision_live();
12120
12273
  init_provision_helpers();
12121
12274
  init_secrets();
@@ -12226,6 +12379,7 @@ var init_surface = __esm({
12226
12379
  code: { connect: {} },
12227
12380
  config: { diff: {}, plan: {}, apply: {} },
12228
12381
  context: { show: {}, list: {}, save: {}, remove: {} },
12382
+ credentials: { list: {}, revoke: {} },
12229
12383
  // `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
12230
12384
  discuss: {
12231
12385
  groups: {},
@@ -14022,6 +14176,10 @@ async function runCli(argv2 = process.argv.slice(2), dependencies = {}) {
14022
14176
  await contextCommand(parsed, runtime);
14023
14177
  return;
14024
14178
  }
14179
+ if (command === "credentials") {
14180
+ await credentialCommand(parsed, runtime);
14181
+ return;
14182
+ }
14025
14183
  if (command === "runbook") {
14026
14184
  await runbookCommand(parsed, runtime);
14027
14185
  return;
@@ -14164,6 +14322,7 @@ var init_cli = __esm({
14164
14322
  init_cli_project();
14165
14323
  init_code_command();
14166
14324
  init_context_command();
14325
+ init_credential_command();
14167
14326
  init_help();
14168
14327
  init_version();
14169
14328
  init_discuss_command();