@odla-ai/cli 0.35.1 → 0.36.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/dist/index.cjs CHANGED
@@ -97,12 +97,12 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
97
97
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
98
98
 
99
99
  // src/admin-ai.ts
100
- var import_node_process7 = __toESM(require("process"), 1);
100
+ var import_node_process8 = __toESM(require("process"), 1);
101
101
 
102
102
  // src/token.ts
103
103
  var import_db = require("@odla-ai/db");
104
104
  var import_node_crypto = require("crypto");
105
- var import_node_process4 = __toESM(require("process"), 1);
105
+ var import_node_process5 = __toESM(require("process"), 1);
106
106
 
107
107
  // src/handshake-approval.ts
108
108
  var import_node_process2 = __toESM(require("process"), 1);
@@ -236,13 +236,72 @@ function handshakeWaitMs(waitSeconds, interactive = import_node_process3.default
236
236
  return interactive ? void 0 : 9e4;
237
237
  }
238
238
 
239
- // src/local.ts
239
+ // src/cached-credential.ts
240
240
  var import_node_fs2 = require("fs");
241
+ var noted = null;
242
+ function noteCachedCredential(tokenFile) {
243
+ noted = tokenFile;
244
+ }
245
+ function isCredentialRejection(error) {
246
+ const message2 = error instanceof Error ? error.message : String(error ?? "");
247
+ return /\((401|403)\)\s*$/.test(message2.trim());
248
+ }
249
+ function explainRejectedCredential(error) {
250
+ const tokenFile = noted;
251
+ if (!tokenFile || !isCredentialRejection(error)) return null;
252
+ noted = null;
253
+ (0, import_node_fs2.rmSync)(tokenFile, { force: true });
254
+ return [
255
+ "auth: the cached credential was rejected by odla, so it was revoked before its cached expiry.",
256
+ " The usual cause is a newer sign-in for this project: collecting a handshake retires the",
257
+ " principal's other credentials, so a second terminal or worktree supersedes this one.",
258
+ ` Discarded ${tokenFile}; re-run this command to request a fresh approval.`
259
+ ].join("\n");
260
+ }
261
+
262
+ // src/device-session.ts
263
+ var import_node_fs3 = require("fs");
264
+ var import_node_os = require("os");
241
265
  var import_node_path2 = require("path");
266
+ var import_node_process4 = __toESM(require("process"), 1);
267
+ function deviceCredentialPath(env = import_node_process4.default.env) {
268
+ return env.ODLA_DEVICE_CREDENTIAL ?? (0, import_node_path2.join)(env.HOME ?? (0, import_node_os.homedir)(), ".odla", "device.json");
269
+ }
270
+ function readDeviceCredential(platform, env = import_node_process4.default.env) {
271
+ const path = deviceCredentialPath(env);
272
+ if (!(0, import_node_fs3.existsSync)(path)) return null;
273
+ try {
274
+ const parsed = JSON.parse((0, import_node_fs3.readFileSync)(path, "utf8"));
275
+ if (typeof parsed.token !== "string" || !parsed.token.startsWith("odla_device_")) return null;
276
+ if (parsed.platform !== platform) return null;
277
+ return { ...parsed, token: parsed.token, platform: parsed.platform };
278
+ } catch {
279
+ return null;
280
+ }
281
+ }
282
+ async function mintDeviceSession(platformUrl, credential2, doFetch) {
283
+ const response2 = await doFetch(`${platformUrl.replace(/\/$/, "")}/registry/devices/session`, {
284
+ method: "POST",
285
+ headers: { authorization: `Bearer ${credential2.token}`, "content-type": "application/json" },
286
+ body: "{}"
287
+ });
288
+ const body = await response2.json().catch(() => ({}));
289
+ if (!response2.ok || typeof body.token !== "string") {
290
+ const detail = body.error?.message ?? `registry returned ${response2.status}`;
291
+ throw new Error(
292
+ `device session failed: ${detail} (${response2.status}) \u2014 if this machine's enrollment was revoked or has expired, enroll it again in Studio`
293
+ );
294
+ }
295
+ return { token: body.token, expiresAt: body.expiresAt ?? Date.now() };
296
+ }
297
+
298
+ // src/local.ts
299
+ var import_node_fs4 = require("fs");
300
+ var import_node_path3 = require("path");
242
301
  var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
243
302
  function readJsonFile(path) {
244
303
  try {
245
- return JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
304
+ return JSON.parse((0, import_node_fs4.readFileSync)(path, "utf8"));
246
305
  } catch {
247
306
  return null;
248
307
  }
@@ -252,10 +311,10 @@ function writePrivateJson(path, value2) {
252
311
  `);
253
312
  }
254
313
  function readCredentials(path) {
255
- if (!(0, import_node_fs2.existsSync)(path)) return null;
314
+ if (!(0, import_node_fs4.existsSync)(path)) return null;
256
315
  let value2;
257
316
  try {
258
- value2 = JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
317
+ value2 = JSON.parse((0, import_node_fs4.readFileSync)(path, "utf8"));
259
318
  } catch {
260
319
  throw new Error(`credentials file ${path} is not valid JSON; fix or remove it before provisioning`);
261
320
  }
@@ -285,14 +344,14 @@ function mergeCredential(current, update) {
285
344
  return next;
286
345
  }
287
346
  function ensureGitignore(rootDir, localPaths = []) {
288
- const path = (0, import_node_path2.resolve)(rootDir, ".gitignore");
289
- const existing = (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : "";
347
+ const path = (0, import_node_path3.resolve)(rootDir, ".gitignore");
348
+ const existing = (0, import_node_fs4.existsSync)(path) ? (0, import_node_fs4.readFileSync)(path, "utf8") : "";
290
349
  const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line2) => !!line2);
291
350
  const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
292
351
  const missing = wanted.filter((line2) => !existing.split(/\r?\n/).includes(line2));
293
352
  if (missing.length === 0) return;
294
353
  const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
295
- (0, import_node_fs2.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
354
+ (0, import_node_fs4.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
296
355
  `);
297
356
  }
298
357
  function o11yDevVars(cfg) {
@@ -306,7 +365,7 @@ function o11yDevVars(cfg) {
306
365
  function resolveWriteDevVarsTarget(cfg, requested) {
307
366
  if (!requested) return null;
308
367
  if (requested === true) return cfg.local.devVarsFile;
309
- return (0, import_node_path2.resolve)((0, import_node_path2.dirname)(cfg.configPath), requested);
368
+ return (0, import_node_path3.resolve)((0, import_node_path3.dirname)(cfg.configPath), requested);
310
369
  }
311
370
  function writeDevVars(path, credentials, env, o11y) {
312
371
  const entry = credentials.envs[env];
@@ -320,7 +379,7 @@ function writeDevVars(path, credentials, env, o11y) {
320
379
  if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
321
380
  if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
322
381
  }
323
- const existing = (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : "";
382
+ const existing = (0, import_node_fs4.existsSync)(path) ? (0, import_node_fs4.readFileSync)(path, "utf8") : "";
324
383
  const retained = existing.split(/\r?\n/).filter((line2) => !isManagedDevVar(line2));
325
384
  while (retained.at(-1) === "") retained.pop();
326
385
  const prefix = retained.length ? `${retained.join("\n")}
@@ -346,19 +405,19 @@ function isManagedDevVar(line2) {
346
405
  return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
347
406
  }
348
407
  function writePrivateText(path, text3) {
349
- (0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(path), { recursive: true });
408
+ (0, import_node_fs4.mkdirSync)((0, import_node_path3.dirname)(path), { recursive: true });
350
409
  const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
351
- (0, import_node_fs2.writeFileSync)(temporary, text3, { mode: 384 });
352
- (0, import_node_fs2.chmodSync)(temporary, 384);
353
- (0, import_node_fs2.renameSync)(temporary, path);
410
+ (0, import_node_fs4.writeFileSync)(temporary, text3, { mode: 384 });
411
+ (0, import_node_fs4.chmodSync)(temporary, 384);
412
+ (0, import_node_fs4.renameSync)(temporary, path);
354
413
  }
355
414
  function gitignoreEntry(rootDir, path) {
356
- const rel = (0, import_node_path2.relative)((0, import_node_path2.resolve)(rootDir), (0, import_node_path2.resolve)(path));
357
- if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path2.isAbsolute)(rel)) return null;
415
+ const rel = (0, import_node_path3.relative)((0, import_node_path3.resolve)(rootDir), (0, import_node_path3.resolve)(path));
416
+ if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path3.isAbsolute)(rel)) return null;
358
417
  return rel.replaceAll("\\", "/");
359
418
  }
360
419
  function displayPath(path, rootDir = process.cwd()) {
361
- const rel = (0, import_node_path2.relative)(rootDir, path);
420
+ const rel = (0, import_node_path3.relative)(rootDir, path);
362
421
  return rel && !rel.startsWith("..") ? rel : path;
363
422
  }
364
423
 
@@ -370,17 +429,24 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
370
429
  const cached = readJsonFile(cfg.local.tokenFile);
371
430
  if (!grantRequest.forceReview && !grantRequest.freshLogin) {
372
431
  if (options.token) return options.token;
373
- if (import_node_process4.default.env.ODLA_DEV_TOKEN) {
374
- const declared = import_node_process4.default.env.ODLA_DEV_TOKEN_AUDIENCE;
432
+ if (import_node_process5.default.env.ODLA_DEV_TOKEN) {
433
+ const declared = import_node_process5.default.env.ODLA_DEV_TOKEN_AUDIENCE;
375
434
  if (declared) {
376
435
  if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
377
436
  } else if (audience !== "https://odla.ai") {
378
437
  throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
379
438
  }
380
- return import_node_process4.default.env.ODLA_DEV_TOKEN;
439
+ return import_node_process5.default.env.ODLA_DEV_TOKEN;
440
+ }
441
+ const device = readDeviceCredential(audience);
442
+ if (device) {
443
+ const session = await mintDeviceSession(cfg.platformUrl, device, doFetch);
444
+ out.error(`auth: session minted by this enrolled device (${displayPath(deviceCredentialPath(), cfg.rootDir)})`);
445
+ return session.token;
381
446
  }
382
447
  if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4 && cachedGrantCovers(cached, grantIntent)) {
383
448
  out.error(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
449
+ noteCachedCredential(cfg.local.tokenFile);
384
450
  return cached.token;
385
451
  }
386
452
  } else {
@@ -481,7 +547,7 @@ function stillPending(pending, email) {
481
547
  );
482
548
  }
483
549
  function handshakeEmail(value2, cached) {
484
- const email = (value2 ?? import_node_process4.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
550
+ const email = (value2 ?? import_node_process5.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
485
551
  if (/@users\.noreply\.github\.com$/i.test(email)) {
486
552
  throw new Error(
487
553
  `"${email}" is a GitHub commit identity, not an odla account email; use --email <signed-in-odla-account> or ODLA_USER_EMAIL`
@@ -512,12 +578,12 @@ function platformAudience(value2) {
512
578
  }
513
579
 
514
580
  // src/secret-input.ts
515
- var import_node_process5 = __toESM(require("process"), 1);
581
+ var import_node_process6 = __toESM(require("process"), 1);
516
582
  var MAX_BYTES = 64 * 1024;
517
583
  async function secretInputValue(options, kind = "credential") {
518
584
  if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
519
585
  let value2;
520
- if (options.fromEnv) value2 = import_node_process5.default.env[options.fromEnv];
586
+ if (options.fromEnv) value2 = import_node_process6.default.env[options.fromEnv];
521
587
  else if (options.stdin) value2 = await (options.readStdin ?? (() => readSecretStream(kind)))();
522
588
  else throw new Error(`${kind} input required: use --from-env <NAME> or --stdin; values are never accepted as arguments`);
523
589
  value2 = value2?.replace(/[\r\n]+$/, "");
@@ -525,7 +591,7 @@ async function secretInputValue(options, kind = "credential") {
525
591
  if (new TextEncoder().encode(value2).byteLength > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
526
592
  return value2;
527
593
  }
528
- async function readSecretStream(kind, stream = import_node_process5.default.stdin) {
594
+ async function readSecretStream(kind, stream = import_node_process6.default.stdin) {
529
595
  let value2 = "";
530
596
  for await (const chunk of stream) {
531
597
  value2 += String(chunk);
@@ -535,9 +601,9 @@ async function readSecretStream(kind, stream = import_node_process5.default.stdi
535
601
  }
536
602
 
537
603
  // src/admin-ai-auth.ts
538
- var import_node_fs3 = require("fs");
539
- var import_node_path3 = require("path");
540
- var import_node_process6 = __toESM(require("process"), 1);
604
+ var import_node_fs5 = require("fs");
605
+ var import_node_path4 = require("path");
606
+ var import_node_process7 = __toESM(require("process"), 1);
541
607
  var import_db2 = require("@odla-ai/db");
542
608
  async function getScopedPlatformToken(options) {
543
609
  return resolveAdminPlatformToken(options);
@@ -545,7 +611,7 @@ async function getScopedPlatformToken(options) {
545
611
  async function resolveAdminPlatformToken(options) {
546
612
  const audience = platformAudience(options.platform);
547
613
  if (options.token) return options.token;
548
- const fromEnv = import_node_process6.default.env.ODLA_ADMIN_TOKEN;
614
+ const fromEnv = import_node_process7.default.env.ODLA_ADMIN_TOKEN;
549
615
  if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
550
616
  return scopedToken(
551
617
  audience,
@@ -557,7 +623,7 @@ async function resolveAdminPlatformToken(options) {
557
623
  }
558
624
  function audienceBoundEnvToken(token, platform) {
559
625
  const audience = platformAudience(platform);
560
- const declared = import_node_process6.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
626
+ const declared = import_node_process7.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
561
627
  if (declared) {
562
628
  if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
563
629
  } else if (audience !== "https://odla.ai") {
@@ -570,6 +636,7 @@ var SCOPE_PURPOSE = {
570
636
  "app:config:read": "compare checked-in intent with an exact-id app Registry configuration",
571
637
  "app:config:write": "apply or inspect one revision-bound configuration operation for an app you own",
572
638
  "platform:runbook:write": "read and edit all of odla's operational runbooks, including admin-visible content",
639
+ "app:device:enroll": "enrol this machine so it can mint its own short-lived credentials without asking you again",
573
640
  "platform:ai:policy:write": "change System AI model routing",
574
641
  "platform:ai:policy:read": "read System AI model routing",
575
642
  "platform:ai:credential:write": "replace a stored AI provider key",
@@ -582,8 +649,8 @@ var SCOPE_PURPOSE = {
582
649
  };
583
650
  async function scopedToken(platform, scope, options, doFetch, out) {
584
651
  const audience = platformAudience(platform);
585
- const rootDir = options.rootDir ?? import_node_process6.default.cwd();
586
- const tokenFile = options.tokenFile ?? (0, import_node_path3.join)(rootDir, ".odla/admin-token.local.json");
652
+ const rootDir = options.rootDir ?? import_node_process7.default.cwd();
653
+ const tokenFile = options.tokenFile ?? (0, import_node_path4.join)(rootDir, ".odla/admin-token.local.json");
587
654
  const cache2 = options.cache === false ? null : readJsonFile(tokenFile);
588
655
  const cached = cache2?.platform === audience ? cache2.tokens?.[scope] : void 0;
589
656
  if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
@@ -610,7 +677,7 @@ async function scopedToken(platform, scope, options, doFetch, out) {
610
677
  if (options.cache !== false) {
611
678
  const tokens = cache2?.platform === audience ? { ...cache2.tokens ?? {} } : {};
612
679
  tokens[scope] = { token, expiresAt };
613
- if ((0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, ".git"))) ensureGitignore(rootDir, [tokenFile]);
680
+ if ((0, import_node_fs5.existsSync)((0, import_node_path4.join)(rootDir, ".git"))) ensureGitignore(rootDir, [tokenFile]);
614
681
  writePrivateJson(tokenFile, { platform: audience, email, tokens });
615
682
  out.error(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
616
683
  } else {
@@ -787,7 +854,7 @@ function isRecord2(value2) {
787
854
 
788
855
  // src/admin-ai.ts
789
856
  async function adminAi(options) {
790
- const platform = platformAudience(options.platform ?? import_node_process7.default.env.ODLA_PLATFORM ?? "https://odla.ai");
857
+ const platform = platformAudience(options.platform ?? import_node_process8.default.env.ODLA_PLATFORM ?? "https://odla.ai");
791
858
  const doFetch = options.fetch ?? fetch;
792
859
  const out = options.stdout ?? console;
793
860
  const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
@@ -1042,13 +1109,13 @@ function addOption(options, name, value2) {
1042
1109
  }
1043
1110
 
1044
1111
  // src/operator-context.ts
1045
- var import_node_fs6 = require("fs");
1046
- var import_node_path6 = require("path");
1047
- var import_node_process9 = __toESM(require("process"), 1);
1112
+ var import_node_fs8 = require("fs");
1113
+ var import_node_path7 = require("path");
1114
+ var import_node_process10 = __toESM(require("process"), 1);
1048
1115
 
1049
1116
  // src/config.ts
1050
- var import_node_fs4 = require("fs");
1051
- var import_node_path4 = require("path");
1117
+ var import_node_fs6 = require("fs");
1118
+ var import_node_path5 = require("path");
1052
1119
  var import_node_url = require("url");
1053
1120
  var import_apps = require("@odla-ai/apps");
1054
1121
 
@@ -1453,12 +1520,12 @@ var DEFAULT_SERVICES = ["db", "ai"];
1453
1520
  var configImportSerial = 0;
1454
1521
  var GOOGLE_CALENDAR_EVENTS_SCOPE = "https://www.googleapis.com/auth/calendar.events";
1455
1522
  async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1456
- const resolved = (0, import_node_path4.resolve)(configPath);
1457
- if (!(0, import_node_fs4.existsSync)(resolved)) {
1523
+ const resolved = (0, import_node_path5.resolve)(configPath);
1524
+ if (!(0, import_node_fs6.existsSync)(resolved)) {
1458
1525
  throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
1459
1526
  }
1460
1527
  const raw = await loadConfigModule(resolved);
1461
- const rootDir = (0, import_node_path4.dirname)(resolved);
1528
+ const rootDir = (0, import_node_path5.dirname)(resolved);
1462
1529
  validateRawConfig(raw, resolved);
1463
1530
  const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
1464
1531
  const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
@@ -1468,9 +1535,9 @@ async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1468
1535
  validateCalendarConfig(raw, unique3([...envs, ...options.additionalEnvs ?? []]), services, resolved);
1469
1536
  validateMonitoringConfig(raw, unique3([...envs, ...options.additionalEnvs ?? []]), services, resolved);
1470
1537
  const local = {
1471
- tokenFile: (0, import_node_path4.resolve)(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
1472
- credentialsFile: (0, import_node_path4.resolve)(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
1473
- devVarsFile: (0, import_node_path4.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
1538
+ tokenFile: (0, import_node_path5.resolve)(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
1539
+ credentialsFile: (0, import_node_path5.resolve)(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
1540
+ devVarsFile: (0, import_node_path5.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
1474
1541
  gitignore: raw.local?.gitignore ?? true
1475
1542
  };
1476
1543
  return {
@@ -1487,9 +1554,9 @@ async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1487
1554
  async function resolveDataExport(cfg, value2, names) {
1488
1555
  if (value2 === void 0 || value2 === null || value2 === false) return void 0;
1489
1556
  if (typeof value2 !== "string") return value2;
1490
- const target = (0, import_node_path4.isAbsolute)(value2) ? value2 : (0, import_node_path4.resolve)(cfg.rootDir, value2);
1557
+ const target = (0, import_node_path5.isAbsolute)(value2) ? value2 : (0, import_node_path5.resolve)(cfg.rootDir, value2);
1491
1558
  if (target.endsWith(".json")) {
1492
- return JSON.parse((0, import_node_fs4.readFileSync)(target, "utf8"));
1559
+ return JSON.parse((0, import_node_fs6.readFileSync)(target, "utf8"));
1493
1560
  }
1494
1561
  const mod = await import((0, import_node_url.pathToFileURL)(target).href);
1495
1562
  for (const name of names) {
@@ -1565,7 +1632,7 @@ function validId2(value2) {
1565
1632
  return typeof value2 === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value2);
1566
1633
  }
1567
1634
  async function loadConfigModule(path) {
1568
- if (path.endsWith(".json")) return JSON.parse((0, import_node_fs4.readFileSync)(path, "utf8"));
1635
+ if (path.endsWith(".json")) return JSON.parse((0, import_node_fs6.readFileSync)(path, "utf8"));
1569
1636
  const nonce = `${Date.now()}-${configImportSerial++}`;
1570
1637
  const mod = await import(`${(0, import_node_url.pathToFileURL)(path).href}?reload=${nonce}`);
1571
1638
  const value2 = mod.default ?? mod.config;
@@ -1580,18 +1647,18 @@ function unique3(values) {
1580
1647
  }
1581
1648
 
1582
1649
  // src/operator-profiles.ts
1583
- var import_node_fs5 = require("fs");
1584
- var import_node_os = require("os");
1585
- var import_node_path5 = require("path");
1586
- var import_node_process8 = __toESM(require("process"), 1);
1650
+ var import_node_fs7 = require("fs");
1651
+ var import_node_os2 = require("os");
1652
+ var import_node_path6 = require("path");
1653
+ var import_node_process9 = __toESM(require("process"), 1);
1587
1654
  function operatorProfileFile() {
1588
- return (0, import_node_path5.resolve)(
1589
- clean(import_node_process8.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path5.join)((0, import_node_os.homedir)(), ".odla", "contexts.json")
1655
+ return (0, import_node_path6.resolve)(
1656
+ clean(import_node_process9.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".odla", "contexts.json")
1590
1657
  );
1591
1658
  }
1592
1659
  function resolveOperatorProfile(parsed) {
1593
1660
  const fromFlag = clean(stringOpt(parsed.options.context));
1594
- const fromEnvironment = clean(import_node_process8.default.env.ODLA_CONTEXT);
1661
+ const fromEnvironment = clean(import_node_process9.default.env.ODLA_CONTEXT);
1595
1662
  const name = fromFlag ?? fromEnvironment ?? null;
1596
1663
  const file = operatorProfileFile();
1597
1664
  if (!name) {
@@ -1631,10 +1698,10 @@ function removeOperatorProfile(name, file = operatorProfileFile()) {
1631
1698
  return true;
1632
1699
  }
1633
1700
  function operatorCredentialFiles(selection) {
1634
- const base = selection.name ? (0, import_node_path5.join)((0, import_node_path5.dirname)(selection.file), "profiles", selection.name) : (0, import_node_path5.join)((0, import_node_os.homedir)(), ".odla");
1701
+ const base = selection.name ? (0, import_node_path6.join)((0, import_node_path6.dirname)(selection.file), "profiles", selection.name) : (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".odla");
1635
1702
  return {
1636
- developer: (0, import_node_path5.join)(base, "dev-token.json"),
1637
- scoped: (0, import_node_path5.join)(base, "admin-token.local.json")
1703
+ developer: (0, import_node_path6.join)(base, "dev-token.json"),
1704
+ scoped: (0, import_node_path6.join)(base, "admin-token.local.json")
1638
1705
  };
1639
1706
  }
1640
1707
  function assertOperatorName(value2, label) {
@@ -1645,10 +1712,10 @@ function assertOperatorName(value2, label) {
1645
1712
  }
1646
1713
  }
1647
1714
  function readOperatorProfiles(file) {
1648
- if (!(0, import_node_fs5.existsSync)(file)) return emptyProfiles();
1715
+ if (!(0, import_node_fs7.existsSync)(file)) return emptyProfiles();
1649
1716
  let raw;
1650
1717
  try {
1651
- raw = JSON.parse((0, import_node_fs5.readFileSync)(file, "utf8"));
1718
+ raw = JSON.parse((0, import_node_fs7.readFileSync)(file, "utf8"));
1652
1719
  } catch {
1653
1720
  throw new Error(`operator context file ${file} is not valid JSON`);
1654
1721
  }
@@ -1712,21 +1779,21 @@ var DEFAULT_PLATFORM2 = "https://odla.ai";
1712
1779
  async function resolveOperatorContext(parsed, options = {}) {
1713
1780
  const profile = resolveOperatorProfile(parsed);
1714
1781
  const configArgument = stringOpt(parsed.options.config) ?? "odla.config.mjs";
1715
- const configPath = (0, import_node_path6.resolve)(configArgument);
1782
+ const configPath = (0, import_node_path7.resolve)(configArgument);
1716
1783
  const explicitConfig = parsed.options.config !== void 0;
1717
- const hasConfig = (0, import_node_fs6.existsSync)(configPath);
1784
+ const hasConfig = (0, import_node_fs8.existsSync)(configPath);
1718
1785
  if (!hasConfig && (!options.allowMissingConfig || explicitConfig)) {
1719
1786
  await loadProjectConfig(configArgument);
1720
1787
  }
1721
1788
  const loaded = hasConfig ? await loadProjectConfig(configArgument) : void 0;
1722
1789
  const platformFlag = clean2(stringOpt(parsed.options.platform));
1723
- const platformEnvironment = clean2(import_node_process9.default.env.ODLA_PLATFORM_URL);
1790
+ const platformEnvironment = clean2(import_node_process10.default.env.ODLA_PLATFORM_URL);
1724
1791
  const platformValue = platformAudience(
1725
1792
  platformFlag ?? platformEnvironment ?? profile.value?.platform ?? loaded?.platformUrl ?? DEFAULT_PLATFORM2
1726
1793
  );
1727
1794
  const platformSource = platformFlag ? "flag" : platformEnvironment ? "environment" : profile.value ? "profile" : loaded ? "config" : "default";
1728
1795
  const appFlag = clean2(stringOpt(parsed.options.app));
1729
- const appEnvironment = clean2(import_node_process9.default.env.ODLA_APP_ID);
1796
+ const appEnvironment = clean2(import_node_process10.default.env.ODLA_APP_ID);
1730
1797
  const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
1731
1798
  const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
1732
1799
  if (appValue) assertOperatorName(appValue, "app");
@@ -1736,16 +1803,16 @@ async function resolveOperatorContext(parsed, options = {}) {
1736
1803
  );
1737
1804
  }
1738
1805
  const envFlag = clean2(stringOpt(parsed.options.env));
1739
- const envEnvironment = clean2(import_node_process9.default.env.ODLA_ENV);
1806
+ const envEnvironment = clean2(import_node_process10.default.env.ODLA_ENV);
1740
1807
  const environmentValue = envFlag ?? envEnvironment ?? profile.value?.environment ?? options.defaultEnvironment ?? null;
1741
1808
  const environmentSource = envFlag ? "flag" : envEnvironment ? "environment" : profile.value?.environment ? "profile" : options.defaultEnvironment ? "default" : "unresolved";
1742
1809
  if (environmentValue) {
1743
1810
  assertOperatorName(environmentValue, "environment");
1744
1811
  }
1745
- const rootDir = loaded?.rootDir ?? import_node_process9.default.cwd();
1812
+ const rootDir = loaded?.rootDir ?? import_node_process10.default.cwd();
1746
1813
  const profileCredentials = operatorCredentialFiles(profile);
1747
- const tokenFile = clean2(import_node_process9.default.env.ODLA_DEV_TOKEN_FILE) ? (0, import_node_path6.resolve)(import_node_process9.default.env.ODLA_DEV_TOKEN_FILE) : profile.name ? profileCredentials.developer : loaded?.local.tokenFile ?? profileCredentials.developer;
1748
- const scopedTokenFile = clean2(import_node_process9.default.env.ODLA_ADMIN_TOKEN_FILE) ? (0, import_node_path6.resolve)(import_node_process9.default.env.ODLA_ADMIN_TOKEN_FILE) : profile.name ? profileCredentials.scoped : loaded ? (0, import_node_path6.join)(loaded.rootDir, ".odla", "admin-token.local.json") : profileCredentials.scoped;
1814
+ const tokenFile = clean2(import_node_process10.default.env.ODLA_DEV_TOKEN_FILE) ? (0, import_node_path7.resolve)(import_node_process10.default.env.ODLA_DEV_TOKEN_FILE) : profile.name ? profileCredentials.developer : loaded?.local.tokenFile ?? profileCredentials.developer;
1815
+ const scopedTokenFile = clean2(import_node_process10.default.env.ODLA_ADMIN_TOKEN_FILE) ? (0, import_node_path7.resolve)(import_node_process10.default.env.ODLA_ADMIN_TOKEN_FILE) : profile.name ? profileCredentials.scoped : loaded ? (0, import_node_path7.join)(loaded.rootDir, ".odla", "admin-token.local.json") : profileCredentials.scoped;
1749
1816
  const cfg = loaded ? {
1750
1817
  ...loaded,
1751
1818
  platformUrl: platformValue,
@@ -1767,8 +1834,8 @@ async function resolveOperatorContext(parsed, options = {}) {
1767
1834
  services: [],
1768
1835
  local: {
1769
1836
  tokenFile,
1770
- credentialsFile: (0, import_node_path6.join)(rootDir, ".odla", "credentials.local.json"),
1771
- devVarsFile: (0, import_node_path6.join)(rootDir, ".dev.vars"),
1837
+ credentialsFile: (0, import_node_path7.join)(rootDir, ".odla", "credentials.local.json"),
1838
+ devVarsFile: (0, import_node_path7.join)(rootDir, ".dev.vars"),
1772
1839
  gitignore: true
1773
1840
  }
1774
1841
  };
@@ -1865,7 +1932,7 @@ async function adminCommand(parsed, deps = {}) {
1865
1932
  }
1866
1933
 
1867
1934
  // src/auth-command.ts
1868
- var import_node_process10 = __toESM(require("process"), 1);
1935
+ var import_node_process11 = __toESM(require("process"), 1);
1869
1936
 
1870
1937
  // src/whoami-command.ts
1871
1938
  var text2 = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
@@ -2027,7 +2094,7 @@ async function authCommand(parsed, deps = {}) {
2027
2094
  const { cfg } = context;
2028
2095
  const out = deps.stdout ?? console;
2029
2096
  const doFetch = deps.fetch ?? fetch;
2030
- const email = stringOpt(parsed.options.email) ?? import_node_process10.default.env.ODLA_USER_EMAIL?.trim();
2097
+ const email = stringOpt(parsed.options.email) ?? import_node_process11.default.env.ODLA_USER_EMAIL?.trim();
2031
2098
  if (!email) {
2032
2099
  throw new Error(
2033
2100
  "auth login requires --email <odla-account> or ODLA_USER_EMAIL; confirm the signed-in odla email instead of using git or GitHub identity"
@@ -2180,7 +2247,7 @@ async function appExport(options) {
2180
2247
  }
2181
2248
 
2182
2249
  // src/app-import.ts
2183
- var import_node_fs7 = require("fs");
2250
+ var import_node_fs9 = require("fs");
2184
2251
  var import_import = require("@odla-ai/db/import");
2185
2252
  function chooseIdMode(options, rows) {
2186
2253
  const chosen = [options.idField && "field", options.key && "key", options.generateIds && "generate"].filter(Boolean);
@@ -2198,7 +2265,7 @@ async function appImport(options) {
2198
2265
  const out = options.stdout ?? console;
2199
2266
  const say = options.json ? (line2) => out.error(line2) : (line2) => out.log(line2);
2200
2267
  const { tenant } = resolveTenant(cfg, options.env);
2201
- const text3 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs7.readFileSync)(0, "utf8")))() : (0, import_node_fs7.readFileSync)(options.file, "utf8");
2268
+ const text3 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs9.readFileSync)(0, "utf8")))() : (0, import_node_fs9.readFileSync)(options.file, "utf8");
2202
2269
  const { format, sources } = (0, import_import.parseImport)(text3, options.ns);
2203
2270
  if (format === "namespace-map" && options.ns) {
2204
2271
  throw new Error("--ns cannot be combined with a {namespace: rows} file \u2014 the file already names each namespace");
@@ -2388,7 +2455,7 @@ async function appCommand(parsed, dependencies = {}) {
2388
2455
 
2389
2456
  // src/brand-command.ts
2390
2457
  var import_promises = require("fs/promises");
2391
- var import_node_path7 = require("path");
2458
+ var import_node_path8 = require("path");
2392
2459
 
2393
2460
  // src/brand-design-unpack.ts
2394
2461
  var import_node_zlib = require("zlib");
@@ -2490,15 +2557,15 @@ function describeUnpack(result, outDir) {
2490
2557
  // src/brand-command.ts
2491
2558
  var USAGE = "usage: odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]";
2492
2559
  async function readBundle(source, deps) {
2493
- if (source !== "-") return (0, import_promises.readFile)((0, import_node_path7.resolve)(source), "utf8");
2560
+ if (source !== "-") return (0, import_promises.readFile)((0, import_node_path8.resolve)(source), "utf8");
2494
2561
  const readStdin = deps.readStdin;
2495
2562
  if (!readStdin) throw new Error("reading a bundle from stdin is not supported here");
2496
2563
  return readStdin();
2497
2564
  }
2498
2565
  async function writeAll(result, outDir) {
2499
2566
  for (const file of result.files) {
2500
- const target = (0, import_node_path7.resolve)(outDir, file.path);
2501
- await (0, import_promises.mkdir)((0, import_node_path7.dirname)(target), { recursive: true });
2567
+ const target = (0, import_node_path8.resolve)(outDir, file.path);
2568
+ await (0, import_promises.mkdir)((0, import_node_path8.dirname)(target), { recursive: true });
2502
2569
  await (0, import_promises.writeFile)(target, file.bytes);
2503
2570
  }
2504
2571
  }
@@ -2506,7 +2573,7 @@ async function designUnpack(parsed, deps) {
2506
2573
  assertArgs(parsed, ["out", "json"], 4);
2507
2574
  const source = parsed.positionals[3];
2508
2575
  if (!source) throw new Error(USAGE);
2509
- const outDir = (0, import_node_path7.resolve)(stringOpt(parsed.options.out) ?? "design");
2576
+ const outDir = (0, import_node_path8.resolve)(stringOpt(parsed.options.out) ?? "design");
2510
2577
  const result = unpackDesign(await readBundle(source, deps));
2511
2578
  await writeAll(result, outDir);
2512
2579
  const out = deps.stdout ?? console;
@@ -3111,12 +3178,12 @@ async function safeText4(response2) {
3111
3178
 
3112
3179
  // src/config-operation-command.ts
3113
3180
  var import_apps6 = require("@odla-ai/apps");
3114
- var import_node_path8 = require("path");
3181
+ var import_node_path9 = require("path");
3115
3182
 
3116
3183
  // src/version.ts
3117
- var import_node_fs8 = require("fs");
3184
+ var import_node_fs10 = require("fs");
3118
3185
  function cliVersion() {
3119
- const pkg = JSON.parse((0, import_node_fs8.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
3186
+ const pkg = JSON.parse((0, import_node_fs10.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
3120
3187
  return pkg.version ?? "unknown";
3121
3188
  }
3122
3189
 
@@ -3132,7 +3199,7 @@ var ConfigOperationCommandError = class extends Error {
3132
3199
 
3133
3200
  // src/config-operation-validate.ts
3134
3201
  var import_apps3 = require("@odla-ai/apps");
3135
- var import_node_fs9 = require("fs");
3202
+ var import_node_fs11 = require("fs");
3136
3203
 
3137
3204
  // src/config-reconcile-digest.ts
3138
3205
  var import_node_crypto2 = require("crypto");
@@ -3168,7 +3235,7 @@ var SERVICE = /^[a-z][a-z0-9-]{0,39}$/;
3168
3235
  function readPlan(path) {
3169
3236
  let value2;
3170
3237
  try {
3171
- const raw = (0, import_node_fs9.readFileSync)(path, "utf8");
3238
+ const raw = (0, import_node_fs11.readFileSync)(path, "utf8");
3172
3239
  if (Buffer.byteLength(raw) > 128 * 1024) throw new Error("plan exceeds 128 KiB");
3173
3240
  value2 = JSON.parse(raw);
3174
3241
  } catch (error) {
@@ -3541,7 +3608,7 @@ async function operationClient(cfg, options, purpose) {
3541
3608
  platform: cfg.platformUrl,
3542
3609
  scope: "app:config:write",
3543
3610
  token: options.token,
3544
- tokenFile: (0, import_node_path8.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
3611
+ tokenFile: (0, import_node_path9.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
3545
3612
  rootDir: cfg.rootDir,
3546
3613
  email: options.email,
3547
3614
  open: options.open,
@@ -3596,7 +3663,7 @@ function record4(value2) {
3596
3663
 
3597
3664
  // src/config-reconcile-command.ts
3598
3665
  var import_apps8 = require("@odla-ai/apps");
3599
- var import_node_path9 = require("path");
3666
+ var import_node_path10 = require("path");
3600
3667
 
3601
3668
  // src/config-reconcile.ts
3602
3669
  var import_apps7 = require("@odla-ai/apps");
@@ -3892,7 +3959,7 @@ async function inspectConfig(options) {
3892
3959
  platform: cfg.platformUrl,
3893
3960
  scope: "app:config:read",
3894
3961
  token: options.token,
3895
- tokenFile: (0, import_node_path9.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
3962
+ tokenFile: (0, import_node_path10.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
3896
3963
  rootDir: cfg.rootDir,
3897
3964
  email: options.email,
3898
3965
  open: options.open,
@@ -4024,13 +4091,13 @@ function quoteArg2(value2) {
4024
4091
 
4025
4092
  // src/doctor-checks.ts
4026
4093
  var import_node_child_process3 = require("child_process");
4027
- var import_node_fs11 = require("fs");
4028
- var import_node_path11 = require("path");
4094
+ var import_node_fs13 = require("fs");
4095
+ var import_node_path12 = require("path");
4029
4096
 
4030
4097
  // src/wrangler.ts
4031
4098
  var import_node_child_process2 = require("child_process");
4032
- var import_node_fs10 = require("fs");
4033
- var import_node_path10 = require("path");
4099
+ var import_node_fs12 = require("fs");
4100
+ var import_node_path11 = require("path");
4034
4101
  var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
4035
4102
  const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
4036
4103
  let stdout = "";
@@ -4044,15 +4111,15 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) =>
4044
4111
  var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
4045
4112
  function findWranglerConfig(rootDir) {
4046
4113
  for (const name of WRANGLER_CONFIG_FILES) {
4047
- const path = (0, import_node_path10.join)(rootDir, name);
4048
- if ((0, import_node_fs10.existsSync)(path)) return path;
4114
+ const path = (0, import_node_path11.join)(rootDir, name);
4115
+ if ((0, import_node_fs12.existsSync)(path)) return path;
4049
4116
  }
4050
4117
  return null;
4051
4118
  }
4052
4119
  function readWranglerConfig(path) {
4053
4120
  if (path.endsWith(".toml")) return null;
4054
4121
  try {
4055
- return JSON.parse(stripJsonComments((0, import_node_fs10.readFileSync)(path, "utf8")));
4122
+ return JSON.parse(stripJsonComments((0, import_node_fs12.readFileSync)(path, "utf8")));
4056
4123
  } catch {
4057
4124
  return null;
4058
4125
  }
@@ -4202,10 +4269,10 @@ function wranglerWarnings(rootDir) {
4202
4269
  for (const { label, block } of blocks) {
4203
4270
  const assets = block.assets;
4204
4271
  if (assets?.directory) {
4205
- const dir = (0, import_node_path11.resolve)(rootDir, assets.directory);
4206
- if (dir === (0, import_node_path11.resolve)(rootDir)) {
4272
+ const dir = (0, import_node_path12.resolve)(rootDir, assets.directory);
4273
+ if (dir === (0, import_node_path12.resolve)(rootDir)) {
4207
4274
  warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
4208
- } else if ((0, import_node_fs11.existsSync)((0, import_node_path11.join)(dir, "node_modules"))) {
4275
+ } else if ((0, import_node_fs13.existsSync)((0, import_node_path12.join)(dir, "node_modules"))) {
4209
4276
  warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
4210
4277
  }
4211
4278
  }
@@ -4240,13 +4307,13 @@ function o11yProjectWarnings(rootDir) {
4240
4307
  warnings.push("cannot verify o11y Worker instrumentation \u2014 add a parseable wrangler.jsonc/json config");
4241
4308
  return warnings;
4242
4309
  }
4243
- const main = typeof config.main === "string" ? (0, import_node_path11.resolve)(rootDir, config.main) : null;
4244
- if (!main || !(0, import_node_fs11.existsSync)(main)) {
4310
+ const main = typeof config.main === "string" ? (0, import_node_path12.resolve)(rootDir, config.main) : null;
4311
+ if (!main || !(0, import_node_fs13.existsSync)(main)) {
4245
4312
  warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
4246
4313
  } else {
4247
4314
  let source = "";
4248
4315
  try {
4249
- source = (0, import_node_fs11.readFileSync)(main, "utf8");
4316
+ source = (0, import_node_fs13.readFileSync)(main, "utf8");
4250
4317
  } catch {
4251
4318
  }
4252
4319
  if (!/\bwithObservability\b/.test(source)) {
@@ -4270,7 +4337,7 @@ function calendarProjectWarnings(rootDir) {
4270
4337
  }
4271
4338
  function readPackageJson(rootDir) {
4272
4339
  try {
4273
- return JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path11.join)(rootDir, "package.json"), "utf8"));
4340
+ return JSON.parse((0, import_node_fs13.readFileSync)((0, import_node_path12.join)(rootDir, "package.json"), "utf8"));
4274
4341
  } catch {
4275
4342
  return null;
4276
4343
  }
@@ -4564,14 +4631,14 @@ function harnessOption(value2, flag) {
4564
4631
  }
4565
4632
 
4566
4633
  // src/init.ts
4567
- var import_node_fs12 = require("fs");
4568
- var import_node_path12 = require("path");
4634
+ var import_node_fs14 = require("fs");
4635
+ var import_node_path13 = require("path");
4569
4636
  var import_apps9 = require("@odla-ai/apps");
4570
4637
  function initProject(options) {
4571
4638
  const out = options.stdout ?? console;
4572
- const rootDir = (0, import_node_path12.resolve)(options.rootDir ?? process.cwd());
4573
- const configPath = (0, import_node_path12.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
4574
- if ((0, import_node_fs12.existsSync)(configPath) && !options.force) {
4639
+ const rootDir = (0, import_node_path13.resolve)(options.rootDir ?? process.cwd());
4640
+ const configPath = (0, import_node_path13.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
4641
+ if ((0, import_node_fs14.existsSync)(configPath) && !options.force) {
4575
4642
  throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
4576
4643
  }
4577
4644
  if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
@@ -4587,20 +4654,20 @@ function initProject(options) {
4587
4654
  }
4588
4655
  }
4589
4656
  const aiProvider = options.aiProvider;
4590
- (0, import_node_fs12.mkdirSync)((0, import_node_path12.dirname)(configPath), { recursive: true });
4591
- (0, import_node_fs12.mkdirSync)((0, import_node_path12.resolve)(rootDir, "src/odla"), { recursive: true });
4592
- (0, import_node_fs12.mkdirSync)((0, import_node_path12.resolve)(rootDir, ".odla"), { recursive: true });
4593
- (0, import_node_fs12.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
4594
- writeIfMissing((0, import_node_path12.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
4595
- writeIfMissing((0, import_node_path12.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
4657
+ (0, import_node_fs14.mkdirSync)((0, import_node_path13.dirname)(configPath), { recursive: true });
4658
+ (0, import_node_fs14.mkdirSync)((0, import_node_path13.resolve)(rootDir, "src/odla"), { recursive: true });
4659
+ (0, import_node_fs14.mkdirSync)((0, import_node_path13.resolve)(rootDir, ".odla"), { recursive: true });
4660
+ (0, import_node_fs14.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
4661
+ writeIfMissing((0, import_node_path13.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
4662
+ writeIfMissing((0, import_node_path13.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
4596
4663
  ensureGitignore(rootDir);
4597
4664
  out.log(`created ${relativeDisplay(configPath, rootDir)}`);
4598
4665
  out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
4599
4666
  out.log("updated .gitignore for local odla credentials");
4600
4667
  }
4601
4668
  function writeIfMissing(path, text3) {
4602
- if ((0, import_node_fs12.existsSync)(path)) return;
4603
- (0, import_node_fs12.writeFileSync)(path, text3);
4669
+ if ((0, import_node_fs14.existsSync)(path)) return;
4670
+ (0, import_node_fs14.writeFileSync)(path, text3);
4604
4671
  }
4605
4672
  function configTemplate(input) {
4606
4673
  const calendar = input.services.includes("calendar") ? ` calendar: {
@@ -4896,9 +4963,9 @@ function printReport(report5, out) {
4896
4963
  }
4897
4964
 
4898
4965
  // src/skill.ts
4899
- var import_node_fs13 = require("fs");
4900
- var import_node_os2 = require("os");
4901
- var import_node_path13 = require("path");
4966
+ var import_node_fs15 = require("fs");
4967
+ var import_node_os3 = require("os");
4968
+ var import_node_path14 = require("path");
4902
4969
  var import_node_url2 = require("url");
4903
4970
 
4904
4971
  // src/skill-adapters.ts
@@ -4997,8 +5064,8 @@ function installSkill(options = {}) {
4997
5064
  const files = listFiles(sourceDir);
4998
5065
  if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
4999
5066
  const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
5000
- const root = (0, import_node_path13.resolve)(options.dir ?? process.cwd());
5001
- const home = (0, import_node_path13.resolve)(options.homeDir ?? (0, import_node_os2.homedir)());
5067
+ const root = (0, import_node_path14.resolve)(options.dir ?? process.cwd());
5068
+ const home = (0, import_node_path14.resolve)(options.homeDir ?? (0, import_node_os3.homedir)());
5002
5069
  const plans = /* @__PURE__ */ new Map();
5003
5070
  const targets = /* @__PURE__ */ new Map();
5004
5071
  const rememberTarget = (harness, target) => {
@@ -5012,48 +5079,48 @@ function installSkill(options = {}) {
5012
5079
  plans.set(target, { target, content: content2, boundary, managedMerge });
5013
5080
  };
5014
5081
  const planSkillTree = (targetDir2, boundary = root) => {
5015
- for (const rel of files) plan((0, import_node_path13.join)(targetDir2, rel), (0, import_node_fs13.readFileSync)((0, import_node_path13.join)(sourceDir, rel), "utf8"), false, boundary);
5082
+ for (const rel of files) plan((0, import_node_path14.join)(targetDir2, rel), (0, import_node_fs15.readFileSync)((0, import_node_path14.join)(sourceDir, rel), "utf8"), false, boundary);
5016
5083
  };
5017
5084
  let targetDir;
5018
5085
  if (options.global) {
5019
- const claudeRoot = (0, import_node_path13.join)(home, ".claude", "skills");
5020
- const codexRoot = (0, import_node_path13.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path13.join)(home, ".codex"), "skills");
5086
+ const claudeRoot = (0, import_node_path14.join)(home, ".claude", "skills");
5087
+ const codexRoot = (0, import_node_path14.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path14.join)(home, ".codex"), "skills");
5021
5088
  targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
5022
5089
  for (const harness of harnesses) {
5023
5090
  const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
5024
- planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path13.dirname)((0, import_node_path13.dirname)(codexRoot)));
5091
+ planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path14.dirname)((0, import_node_path14.dirname)(codexRoot)));
5025
5092
  rememberTarget(harness, skillRoot);
5026
5093
  }
5027
5094
  } else {
5028
- const sharedRoot = (0, import_node_path13.join)(root, ".agents", "skills");
5095
+ const sharedRoot = (0, import_node_path14.join)(root, ".agents", "skills");
5029
5096
  planSkillTree(sharedRoot);
5030
- const claudeRoot = (0, import_node_path13.join)(root, ".claude", "skills");
5097
+ const claudeRoot = (0, import_node_path14.join)(root, ".claude", "skills");
5031
5098
  targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
5032
5099
  for (const harness of harnesses) rememberTarget(harness, sharedRoot);
5033
5100
  if (harnesses.includes("claude")) {
5034
5101
  for (const skill of skillNames(files)) {
5035
- const canonical2 = (0, import_node_fs13.readFileSync)((0, import_node_path13.join)(sourceDir, skill, "SKILL.md"), "utf8");
5036
- plan((0, import_node_path13.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical2));
5102
+ const canonical2 = (0, import_node_fs15.readFileSync)((0, import_node_path14.join)(sourceDir, skill, "SKILL.md"), "utf8");
5103
+ plan((0, import_node_path14.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical2));
5037
5104
  }
5038
5105
  rememberTarget("claude", claudeRoot);
5039
5106
  }
5040
5107
  if (harnesses.includes("cursor")) {
5041
- const cursorRule = (0, import_node_path13.join)(root, ".cursor", "rules", "odla.mdc");
5108
+ const cursorRule = (0, import_node_path14.join)(root, ".cursor", "rules", "odla.mdc");
5042
5109
  plan(cursorRule, CURSOR_RULE);
5043
5110
  rememberTarget("cursor", cursorRule);
5044
5111
  }
5045
5112
  if (harnesses.includes("agents")) {
5046
- const agentsFile = (0, import_node_path13.join)(root, "AGENTS.md");
5113
+ const agentsFile = (0, import_node_path14.join)(root, "AGENTS.md");
5047
5114
  plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
5048
5115
  rememberTarget("agents", agentsFile);
5049
5116
  }
5050
5117
  if (harnesses.includes("copilot")) {
5051
- const copilotFile = (0, import_node_path13.join)(root, ".github", "copilot-instructions.md");
5118
+ const copilotFile = (0, import_node_path14.join)(root, ".github", "copilot-instructions.md");
5052
5119
  plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
5053
5120
  rememberTarget("copilot", copilotFile);
5054
5121
  }
5055
5122
  if (harnesses.includes("gemini")) {
5056
- const geminiFile = (0, import_node_path13.join)(root, "GEMINI.md");
5123
+ const geminiFile = (0, import_node_path14.join)(root, "GEMINI.md");
5057
5124
  plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
5058
5125
  rememberTarget("gemini", geminiFile);
5059
5126
  }
@@ -5067,11 +5134,11 @@ function installSkill(options = {}) {
5067
5134
  conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
5068
5135
  continue;
5069
5136
  }
5070
- if (!(0, import_node_fs13.existsSync)(file.target)) {
5137
+ if (!(0, import_node_fs15.existsSync)(file.target)) {
5071
5138
  writtenPaths.add(file.target);
5072
5139
  continue;
5073
5140
  }
5074
- const current = (0, import_node_fs13.readFileSync)(file.target, "utf8");
5141
+ const current = (0, import_node_fs15.readFileSync)(file.target, "utf8");
5075
5142
  if (current === file.content) {
5076
5143
  unchangedPaths.add(file.target);
5077
5144
  } else if (file.managedMerge || options.force) {
@@ -5088,9 +5155,9 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
5088
5155
  );
5089
5156
  }
5090
5157
  for (const file of plans.values()) {
5091
- if (!(0, import_node_fs13.existsSync)(file.target) || (0, import_node_fs13.readFileSync)(file.target, "utf8") !== file.content) {
5092
- (0, import_node_fs13.mkdirSync)((0, import_node_path13.dirname)(file.target), { recursive: true });
5093
- (0, import_node_fs13.writeFileSync)(file.target, file.content);
5158
+ if (!(0, import_node_fs15.existsSync)(file.target) || (0, import_node_fs15.readFileSync)(file.target, "utf8") !== file.content) {
5159
+ (0, import_node_fs15.mkdirSync)((0, import_node_path14.dirname)(file.target), { recursive: true });
5160
+ (0, import_node_fs15.writeFileSync)(file.target, file.content);
5094
5161
  }
5095
5162
  }
5096
5163
  const skills = skillNames(files);
@@ -5109,7 +5176,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
5109
5176
  };
5110
5177
  }
5111
5178
  function pathsUnder(root, paths) {
5112
- return [...paths].map((path) => (0, import_node_path13.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path13.sep}`) && !(0, import_node_path13.isAbsolute)(path)).sort();
5179
+ return [...paths].map((path) => (0, import_node_path14.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path14.sep}`) && !(0, import_node_path14.isAbsolute)(path)).sort();
5113
5180
  }
5114
5181
  function normalizeHarnesses(values, global) {
5115
5182
  const requested = values?.length ? values : ["claude"];
@@ -5131,9 +5198,9 @@ function normalizeHarnesses(values, global) {
5131
5198
  function managedFileContent(path, block, force, boundary) {
5132
5199
  const symlink = symlinkedComponent(boundary, path);
5133
5200
  if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
5134
- if (!(0, import_node_fs13.existsSync)(path)) return `${block}
5201
+ if (!(0, import_node_fs15.existsSync)(path)) return `${block}
5135
5202
  `;
5136
- const current = (0, import_node_fs13.readFileSync)(path, "utf8");
5203
+ const current = (0, import_node_fs15.readFileSync)(path, "utf8");
5137
5204
  const start = "<!-- odla-ai agent setup:start -->";
5138
5205
  const end = "<!-- odla-ai agent setup:end -->";
5139
5206
  const startAt = current.indexOf(start);
@@ -5154,15 +5221,15 @@ function managedFileContent(path, block, force, boundary) {
5154
5221
  return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
5155
5222
  }
5156
5223
  function symlinkedComponent(boundary, target) {
5157
- const rel = (0, import_node_path13.relative)(boundary, target);
5158
- if (rel === ".." || rel.startsWith(`..${import_node_path13.sep}`) || (0, import_node_path13.isAbsolute)(rel)) {
5224
+ const rel = (0, import_node_path14.relative)(boundary, target);
5225
+ if (rel === ".." || rel.startsWith(`..${import_node_path14.sep}`) || (0, import_node_path14.isAbsolute)(rel)) {
5159
5226
  throw new Error(`agent setup target escapes its install root: ${target}`);
5160
5227
  }
5161
5228
  let current = boundary;
5162
- for (const part of rel.split(import_node_path13.sep).filter(Boolean)) {
5163
- current = (0, import_node_path13.join)(current, part);
5229
+ for (const part of rel.split(import_node_path14.sep).filter(Boolean)) {
5230
+ current = (0, import_node_path14.join)(current, part);
5164
5231
  try {
5165
- if ((0, import_node_fs13.lstatSync)(current).isSymbolicLink()) return current;
5232
+ if ((0, import_node_fs15.lstatSync)(current).isSymbolicLink()) return current;
5166
5233
  } catch (error) {
5167
5234
  if (error.code !== "ENOENT") throw error;
5168
5235
  }
@@ -5173,13 +5240,13 @@ function skillNames(files) {
5173
5240
  return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
5174
5241
  }
5175
5242
  function listFiles(dir) {
5176
- if (!(0, import_node_fs13.existsSync)(dir)) return [];
5243
+ if (!(0, import_node_fs15.existsSync)(dir)) return [];
5177
5244
  const results = [];
5178
5245
  const walk = (current) => {
5179
- for (const entry of (0, import_node_fs13.readdirSync)(current, { withFileTypes: true })) {
5180
- const path = (0, import_node_path13.join)(current, entry.name);
5246
+ for (const entry of (0, import_node_fs15.readdirSync)(current, { withFileTypes: true })) {
5247
+ const path = (0, import_node_path14.join)(current, entry.name);
5181
5248
  if (entry.isDirectory()) walk(path);
5182
- else results.push((0, import_node_path13.relative)(dir, path));
5249
+ else results.push((0, import_node_path14.relative)(dir, path));
5183
5250
  }
5184
5251
  };
5185
5252
  walk(dir);
@@ -5534,9 +5601,9 @@ async function projectCommand(command, parsed, deps) {
5534
5601
  }
5535
5602
 
5536
5603
  // src/code-connect.ts
5537
- var import_node_fs14 = require("fs");
5538
- var import_node_os3 = require("os");
5539
- var import_node_path14 = require("path");
5604
+ var import_node_fs16 = require("fs");
5605
+ var import_node_os4 = require("os");
5606
+ var import_node_path15 = require("path");
5540
5607
 
5541
5608
  // ../harness/dist/chunk-3QP4VDQS.js
5542
5609
  var HARNESS_PROTOCOL_VERSION = 1;
@@ -6645,8 +6712,8 @@ function rollup(graph, kind, options = {}) {
6645
6712
  for (const node of nodesOfKind(graph, kind)) {
6646
6713
  if (options.prefix && !node.name.startsWith(options.prefix)) continue;
6647
6714
  const key = node.name.split(separator).slice(0, depth).join(separator);
6648
- const list2 = groups.get(key);
6649
- if (list2) list2.push(node);
6715
+ const list3 = groups.get(key);
6716
+ if (list3) list3.push(node);
6650
6717
  else groups.set(key, [node]);
6651
6718
  }
6652
6719
  return [...groups].map(([prefix, nodes]) => ({
@@ -6661,7 +6728,7 @@ function dirname8(path) {
6661
6728
  const at = path.lastIndexOf("/");
6662
6729
  return at <= 0 ? "." : path.slice(0, at);
6663
6730
  }
6664
- function join11(base, specifier) {
6731
+ function join12(base, specifier) {
6665
6732
  const parts = [];
6666
6733
  const segments = `${base === "." ? "" : `${base}/`}${specifier}`.split("/");
6667
6734
  for (const segment of segments) {
@@ -6685,7 +6752,7 @@ var BARE_IMPORT = /^\s*import\s*["']([^"']+)["']/gm;
6685
6752
  var isSourcePath = (path) => SOURCE.test(path);
6686
6753
  function resolveImport(fromPath, specifier, known) {
6687
6754
  if (!specifier.startsWith(".")) return null;
6688
- const base = join11(dirname8(fromPath), specifier);
6755
+ const base = join12(dirname8(fromPath), specifier);
6689
6756
  const candidates = [
6690
6757
  base,
6691
6758
  base.replace(/\.js$/, ".ts"),
@@ -9583,8 +9650,8 @@ var CODE_BUILD_RECIPES = Object.freeze([{
9583
9650
  // src/code-connect.ts
9584
9651
  async function codeConnect(options) {
9585
9652
  const cwd = options.cwd ?? process.cwd();
9586
- const configPath = (0, import_node_path14.resolve)(cwd, options.configPath);
9587
- const cfg = (0, import_node_fs14.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
9653
+ const configPath = (0, import_node_path15.resolve)(cwd, options.configPath);
9654
+ const cfg = (0, import_node_fs16.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
9588
9655
  const requestedAppId = options.appId?.trim();
9589
9656
  if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
9590
9657
  throw new Error("--app-id must be a valid odla app id");
@@ -9613,7 +9680,7 @@ async function codeConnect(options) {
9613
9680
  const doFetch = options.fetch ?? fetch;
9614
9681
  const engine = await (options.selectEngine ?? selectContainerEngine)(options.engine ?? "auto");
9615
9682
  const hostPlatform = process.platform === "darwin" ? "macos" : "linux";
9616
- const hostName = (options.name ?? (0, import_node_os3.hostname)()).trim();
9683
+ const hostName = (options.name ?? (0, import_node_os4.hostname)()).trim();
9617
9684
  if (!hostName || hostName.length > 120) throw new Error("--name must contain 1 to 120 characters");
9618
9685
  const repository = await inferGitHubRepository(cwd, options.readGitOrigin);
9619
9686
  const localSource = await (options.prepareLocalSource ?? prepareCodeLocalSource)(
@@ -9650,8 +9717,8 @@ async function codeConnect(options) {
9650
9717
  platform: hostPlatform,
9651
9718
  arch: process.arch,
9652
9719
  engines: [engine],
9653
- cpuCount: (0, import_node_os3.cpus)().length,
9654
- memoryBytes: (0, import_node_os3.totalmem)(),
9720
+ cpuCount: (0, import_node_os4.cpus)().length,
9721
+ memoryBytes: (0, import_node_os4.totalmem)(),
9655
9722
  source: descriptor2,
9656
9723
  images: {
9657
9724
  ready: true,
@@ -10018,13 +10085,13 @@ async function codeCommand(parsed, dependencies) {
10018
10085
  }
10019
10086
 
10020
10087
  // src/operator-credentials.ts
10021
- var import_node_process11 = __toESM(require("process"), 1);
10088
+ var import_node_process12 = __toESM(require("process"), 1);
10022
10089
  function developerTokenStatus(context, parsed, now = Date.now()) {
10023
10090
  const cached = readJsonFile(context.cfg.local.tokenFile);
10024
10091
  const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
10025
10092
  const source = clean3(
10026
10093
  stringOpt(parsed.options.token)
10027
- ) ? "flag" : clean3(import_node_process11.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
10094
+ ) ? "flag" : clean3(import_node_process12.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
10028
10095
  return {
10029
10096
  source,
10030
10097
  cacheFile: context.cfg.local.tokenFile,
@@ -10346,6 +10413,9 @@ Usage:
10346
10413
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
10347
10414
  odla-ai security run [target] --self --ack-redacted-source
10348
10415
  odla-ai provision [--live] [--config odla.config.mjs] [--email <odla-account>] [--request-grant] [--no-open] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
10416
+ odla-ai device enroll [--app <id>[,<id>...]] [--name <label>] [--capability <c>[,<c>...]] [--email <odla-account>] [--no-open] [--json]
10417
+ odla-ai device list [--email <odla-account>] [--json]
10418
+ odla-ai device revoke <device-id> [--email <odla-account>] [--json]
10349
10419
  odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
10350
10420
  odla-ai credentials revoke <receipt-id> [--config odla.config.mjs] [--json]
10351
10421
  odla-ai smoke [--config odla.config.mjs] [--env dev] [--runtime] [--email <odla-account>] [--no-open]
@@ -10451,6 +10521,10 @@ Commands:
10451
10521
  stable status, incident, and report JSON to agents and CI.
10452
10522
  platform Read canonical fleet health, releases, provider load/freshness,
10453
10523
  explicit unknowns, and next actions through a read-only grant.
10524
+ device Enrol THIS machine once, then stop asking. A human approves the
10525
+ enrollment in the browser; from then on this terminal mints its
10526
+ own short-lived credentials for the named projects with nobody's
10527
+ attention, until the device expires or is revoked.
10454
10528
  provision Register services, compose integrations, persist credentials, optionally push secrets.
10455
10529
  "provision --live --yes" initializes only the live instance of
10456
10530
  an existing sandbox app and enables every configured service;
@@ -11701,8 +11775,8 @@ async function pmWatch(ctx, parsed) {
11701
11775
  }
11702
11776
 
11703
11777
  // src/pm-project-context.ts
11704
- var import_node_path15 = require("path");
11705
- var pmProjectContextFile = (rootDir) => (0, import_node_path15.resolve)(rootDir, ".odla", "pm-project.local.json");
11778
+ var import_node_path16 = require("path");
11779
+ var pmProjectContextFile = (rootDir) => (0, import_node_path16.resolve)(rootDir, ".odla", "pm-project.local.json");
11706
11780
  function readPmProjectContext(rootDir) {
11707
11781
  const value2 = readJsonFile(pmProjectContextFile(rootDir));
11708
11782
  return value2 && typeof value2.appId === "string" && typeof value2.projectId === "string" ? value2 : null;
@@ -12733,7 +12807,7 @@ function percent(value2) {
12733
12807
  // src/provision.ts
12734
12808
  var import_apps13 = require("@odla-ai/apps");
12735
12809
  var import_ai5 = require("@odla-ai/ai");
12736
- var import_node_process12 = __toESM(require("process"), 1);
12810
+ var import_node_process13 = __toESM(require("process"), 1);
12737
12811
 
12738
12812
  // src/integration-provision.ts
12739
12813
  var import_db3 = require("@odla-ai/db");
@@ -13169,7 +13243,7 @@ async function provision(options) {
13169
13243
  await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
13170
13244
  }
13171
13245
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
13172
- const key = import_node_process12.default.env[cfg.ai.keyEnv];
13246
+ const key = import_node_process13.default.env[cfg.ai.keyEnv];
13173
13247
  if (key) {
13174
13248
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
13175
13249
  await (0, import_ai5.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -13210,8 +13284,8 @@ async function provision(options) {
13210
13284
  }
13211
13285
 
13212
13286
  // src/record.ts
13213
- var import_node_fs15 = require("fs");
13214
- var import_node_process13 = __toESM(require("process"), 1);
13287
+ var import_node_fs17 = require("fs");
13288
+ var import_node_process14 = __toESM(require("process"), 1);
13215
13289
 
13216
13290
  // src/surface.ts
13217
13291
  var PM_ACTIONS = {
@@ -13281,6 +13355,7 @@ var COMMAND_SURFACE = {
13281
13355
  config: { diff: {}, plan: {}, apply: {} },
13282
13356
  context: { show: {}, list: {}, save: {}, remove: {} },
13283
13357
  credentials: { list: {}, revoke: {} },
13358
+ device: { enroll: {}, list: {}, revoke: {} },
13284
13359
  // `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
13285
13360
  discuss: {
13286
13361
  groups: {},
@@ -13392,7 +13467,7 @@ function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
13392
13467
 
13393
13468
  // src/record.ts
13394
13469
  function recordInvocation(parsed) {
13395
- const file = import_node_process13.default.env.ODLA_CLI_RECORD;
13470
+ const file = import_node_process14.default.env.ODLA_CLI_RECORD;
13396
13471
  if (!file) return;
13397
13472
  try {
13398
13473
  const entry = {
@@ -13400,14 +13475,138 @@ function recordInvocation(parsed) {
13400
13475
  options: Object.entries(parsed.options).map(([name, value2]) => value2 === false ? `no-${name}` : name).sort()
13401
13476
  };
13402
13477
  if (!entry.path.length) return;
13403
- (0, import_node_fs15.appendFileSync)(file, `${JSON.stringify(entry)}
13478
+ (0, import_node_fs17.appendFileSync)(file, `${JSON.stringify(entry)}
13404
13479
  `);
13405
13480
  } catch {
13406
13481
  }
13407
13482
  }
13408
13483
 
13484
+ // src/advisory-output.ts
13485
+ var import_apps14 = require("@odla-ai/apps");
13486
+ function advisoryCollectingFetch(inner, sink) {
13487
+ return (async (input, init) => {
13488
+ const response2 = await inner(input, init);
13489
+ try {
13490
+ sink.push(...(0, import_apps14.parseAdvisories)(response2));
13491
+ } catch {
13492
+ }
13493
+ return response2;
13494
+ });
13495
+ }
13496
+ function renderAdvisories(out, advisories, env = process.env) {
13497
+ if (env.ODLA_NO_ADVISORIES) return;
13498
+ const seen = /* @__PURE__ */ new Set();
13499
+ for (const advisory of advisories) {
13500
+ const key = `${advisory.code}:${advisory.message}`;
13501
+ if (seen.has(key)) continue;
13502
+ seen.add(key);
13503
+ out.error((0, import_apps14.formatAdvisory)(advisory));
13504
+ }
13505
+ }
13506
+
13507
+ // src/device-command.ts
13508
+ var import_node_fs18 = require("fs");
13509
+ var import_node_path17 = require("path");
13510
+ var import_node_process15 = __toESM(require("process"), 1);
13511
+ async function deviceCommand(parsed, deps) {
13512
+ const action2 = parsed.positionals[1] ?? "";
13513
+ const out = deps.stdout ?? console;
13514
+ const doFetch = deps.fetch ?? fetch;
13515
+ const cfg = await loadProjectConfig(stringOpt(parsed.options.config));
13516
+ const json = parsed.options.json === true;
13517
+ if (action2 === "enroll") return enroll(parsed, deps, cfg, doFetch, out, json);
13518
+ if (action2 === "list") return list2(parsed, deps, cfg, doFetch, out, json);
13519
+ if (action2 === "revoke") return revoke(parsed, deps, cfg, doFetch, out, json);
13520
+ throw new Error('odla-ai device expects "enroll", "list", or "revoke"');
13521
+ }
13522
+ async function enroll(parsed, deps, cfg, doFetch, out, json) {
13523
+ const name = stringOpt(parsed.options.name) ?? defaultDeviceName();
13524
+ const apps = (stringOpt(parsed.options.app) ?? cfg.app.id).split(",").map((id2) => id2.trim()).filter(Boolean);
13525
+ if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026]");
13526
+ const token = await scopedToken2(parsed, deps, cfg, doFetch, out, `odla CLI (enroll ${name})`);
13527
+ const response2 = await doFetch(`${cfg.platformUrl}/registry/devices`, {
13528
+ method: "POST",
13529
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
13530
+ body: JSON.stringify({
13531
+ name,
13532
+ platform: import_node_process15.default.platform,
13533
+ appIds: apps,
13534
+ ...parsed.options.capability ? { capabilities: String(parsed.options.capability).split(",").map((c) => c.trim()).filter(Boolean) } : {}
13535
+ })
13536
+ });
13537
+ const body = await response2.json().catch(() => ({}));
13538
+ if (!response2.ok || !body.token || !body.device) {
13539
+ throw new Error(`device enroll failed: ${body.error?.message ?? `registry returned ${response2.status}`} (${response2.status})`);
13540
+ }
13541
+ const path = deviceCredentialPath();
13542
+ (0, import_node_fs18.mkdirSync)((0, import_node_path17.dirname)(path), { recursive: true });
13543
+ (0, import_node_fs18.writeFileSync)(path, JSON.stringify({
13544
+ token: body.token,
13545
+ platform: cfg.platformUrl.replace(/\/$/, ""),
13546
+ deviceId: body.device.deviceId,
13547
+ name
13548
+ }, null, 2));
13549
+ (0, import_node_fs18.chmodSync)(path, 384);
13550
+ out.error(`device: enrolled "${name}" for ${body.device.appIds.join(", ")}; credential written to ${path}`);
13551
+ out.error("device: this terminal will mint its own credentials from now on \u2014 no further approvals.");
13552
+ if (json) {
13553
+ out.log(JSON.stringify({ deviceId: body.device.deviceId, name, appIds: body.device.appIds, expiresAt: body.device.expiresAt }, null, 2));
13554
+ }
13555
+ }
13556
+ async function list2(parsed, deps, cfg, doFetch, out, json) {
13557
+ const token = await scopedToken2(parsed, deps, cfg, doFetch, out, "odla CLI (device list)");
13558
+ const response2 = await doFetch(`${cfg.platformUrl}/registry/devices`, {
13559
+ headers: { authorization: `Bearer ${token}` }
13560
+ });
13561
+ const body = await response2.json().catch(() => ({}));
13562
+ if (!response2.ok || !body.devices) {
13563
+ throw new Error(`device list failed: ${body.error?.message ?? `registry returned ${response2.status}`} (${response2.status})`);
13564
+ }
13565
+ if (json) return out.log(JSON.stringify(body.devices, null, 2));
13566
+ if (body.devices.length === 0) return out.log("no enrolled devices");
13567
+ for (const device of body.devices) {
13568
+ const state2 = device.revokedAt ? "revoked" : device.expiresAt <= Date.now() ? "expired" : "active";
13569
+ out.log(`${device.deviceId} ${state2.padEnd(7)} ${device.name} [${device.appIds.join(", ")}]`);
13570
+ }
13571
+ }
13572
+ async function revoke(parsed, deps, cfg, doFetch, out, json) {
13573
+ const deviceId = parsed.positionals[2];
13574
+ if (!deviceId) throw new Error("device revoke needs the device id from `odla-ai device list`");
13575
+ const token = await scopedToken2(parsed, deps, cfg, doFetch, out, "odla CLI (device revoke)");
13576
+ const response2 = await doFetch(`${cfg.platformUrl}/registry/devices/${encodeURIComponent(deviceId)}/revoke`, {
13577
+ method: "POST",
13578
+ headers: { authorization: `Bearer ${token}` }
13579
+ });
13580
+ if (!response2.ok) {
13581
+ const body = await response2.json().catch(() => ({}));
13582
+ throw new Error(`device revoke failed: ${body.error?.message ?? `registry returned ${response2.status}`} (${response2.status})`);
13583
+ }
13584
+ out.error(`device: revoked ${deviceId}; every credential it minted is revoked with it`);
13585
+ if (json) out.log(JSON.stringify({ deviceId, revoked: true }, null, 2));
13586
+ }
13587
+ async function scopedToken2(parsed, deps, cfg, doFetch, out, label) {
13588
+ const { credentials } = await resolveOperatorContext(parsed, { allowMissingConfig: true });
13589
+ const scopedTokenFile = credentials.scopedTokenFile;
13590
+ return getScopedPlatformToken({
13591
+ platform: cfg.platformUrl,
13592
+ scope: "app:device:enroll",
13593
+ email: stringOpt(parsed.options.email),
13594
+ label,
13595
+ fetch: doFetch,
13596
+ stdout: out,
13597
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
13598
+ openApprovalUrl: deps.openUrl,
13599
+ rootDir: cfg.rootDir,
13600
+ tokenFile: scopedTokenFile,
13601
+ ...stringOpt(parsed.options.token) ? { token: stringOpt(parsed.options.token) } : {}
13602
+ });
13603
+ }
13604
+ function defaultDeviceName() {
13605
+ return `${import_node_process15.default.env.HOSTNAME ?? import_node_process15.default.env.HOST ?? "machine"}-${import_node_process15.default.platform}`;
13606
+ }
13607
+
13409
13608
  // src/runbook-actions.ts
13410
- var import_node_fs16 = require("fs");
13609
+ var import_node_fs19 = require("fs");
13411
13610
 
13412
13611
  // src/runbook-requires.ts
13413
13612
  var SPEC = /^(@?[\w./-]+?)@(\d+\.\d+\.\d+(?:[\w.-]*)?)$/;
@@ -13503,7 +13702,7 @@ async function bySlug(ctx, slug) {
13503
13702
  function readBody(file, inline) {
13504
13703
  if (inline !== void 0) return inline;
13505
13704
  if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
13506
- return (0, import_node_fs16.readFileSync)(file === "-" ? 0 : file, "utf8");
13705
+ return (0, import_node_fs19.readFileSync)(file === "-" ? 0 : file, "utf8");
13507
13706
  }
13508
13707
  var stamp = (ms) => ms ? new Date(ms).toISOString().slice(0, 16).replace("T", " ") : "";
13509
13708
  async function runbookList(ctx, all, query) {
@@ -13595,8 +13794,8 @@ async function runbookRemove(ctx, slug) {
13595
13794
  }
13596
13795
 
13597
13796
  // src/runbook-import.ts
13598
- var import_node_fs17 = require("fs");
13599
- var import_node_path16 = require("path");
13797
+ var import_node_fs20 = require("fs");
13798
+ var import_node_path18 = require("path");
13600
13799
  function parseRunbook(text3, slug) {
13601
13800
  let rest = text3;
13602
13801
  const meta = {};
@@ -13621,12 +13820,12 @@ function parseRunbook(text3, slug) {
13621
13820
  };
13622
13821
  }
13623
13822
  function readRunbookDir(dir) {
13624
- if (!(0, import_node_fs17.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
13625
- const files = (0, import_node_fs17.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
13823
+ if (!(0, import_node_fs20.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
13824
+ const files = (0, import_node_fs20.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
13626
13825
  if (!files.length) throw new Error(`no .md files in ${dir}`);
13627
13826
  return files.map((file) => {
13628
- const slug = (0, import_node_path16.basename)(file, ".md");
13629
- const parsed = parseRunbook((0, import_node_fs17.readFileSync)((0, import_node_path16.join)(dir, file), "utf8"), slug);
13827
+ const slug = (0, import_node_path18.basename)(file, ".md");
13828
+ const parsed = parseRunbook((0, import_node_fs20.readFileSync)((0, import_node_path18.join)(dir, file), "utf8"), slug);
13630
13829
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
13631
13830
  });
13632
13831
  }
@@ -13699,8 +13898,8 @@ async function upsert(ctx, r, visibility) {
13699
13898
 
13700
13899
  // src/runbook-impact.ts
13701
13900
  var import_node_child_process6 = require("child_process");
13702
- var import_node_fs18 = require("fs");
13703
- var import_node_path17 = require("path");
13901
+ var import_node_fs21 = require("fs");
13902
+ var import_node_path19 = require("path");
13704
13903
 
13705
13904
  // src/runbook-impact-scan.ts
13706
13905
  var DECL = /^[+-]\s*export\s+(?:declare\s+)?(?:default\s+)?(?:abstract\s+)?(?:async\s+)?(?:const|let|var|function|class|interface|type|enum)\s+([A-Za-z_$][\w$]*)/;
@@ -13869,10 +14068,10 @@ ${body.split("\n").map((line2) => `+${line2}`).join("\n")}
13869
14068
  }
13870
14069
  function manifestLabeller(root) {
13871
14070
  return (workspace) => {
13872
- const manifest = (0, import_node_path17.join)(root, workspace, "package.json");
13873
- if (!(0, import_node_fs18.existsSync)(manifest)) return void 0;
14071
+ const manifest = (0, import_node_path19.join)(root, workspace, "package.json");
14072
+ if (!(0, import_node_fs21.existsSync)(manifest)) return void 0;
13874
14073
  try {
13875
- const name = JSON.parse((0, import_node_fs18.readFileSync)(manifest, "utf8")).name;
14074
+ const name = JSON.parse((0, import_node_fs21.readFileSync)(manifest, "utf8")).name;
13876
14075
  return typeof name === "string" ? name : void 0;
13877
14076
  } catch {
13878
14077
  return void 0;
@@ -13939,7 +14138,7 @@ function report4(ctx, impacts) {
13939
14138
  async function runbookImpact(ctx, options, deps = {}) {
13940
14139
  const cwd = deps.cwd ?? process.cwd();
13941
14140
  const runGit = deps.runGit ?? gitRunner(cwd);
13942
- const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs18.readFileSync)((0, import_node_path17.join)(cwd, path), "utf8"));
14141
+ const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs21.readFileSync)((0, import_node_path19.join)(cwd, path), "utf8"));
13943
14142
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
13944
14143
  if (!surfaces.length) {
13945
14144
  return ctx.out.log(
@@ -14072,12 +14271,12 @@ async function runbookComment(ctx, slug, body) {
14072
14271
 
14073
14272
  // src/runbook-editor.ts
14074
14273
  var import_node_child_process7 = require("child_process");
14075
- var import_node_fs19 = require("fs");
14076
- var import_node_os4 = require("os");
14077
- var import_node_path18 = require("path");
14078
- var import_node_process14 = __toESM(require("process"), 1);
14274
+ var import_node_fs22 = require("fs");
14275
+ var import_node_os5 = require("os");
14276
+ var import_node_path20 = require("path");
14277
+ var import_node_process16 = __toESM(require("process"), 1);
14079
14278
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
14080
- function resolveEditor(env = import_node_process14.default.env) {
14279
+ function resolveEditor(env = import_node_process16.default.env) {
14081
14280
  for (const name of EDITOR_ENV) {
14082
14281
  const value2 = env[name];
14083
14282
  if (value2 && value2.trim()) return value2.trim();
@@ -14091,8 +14290,8 @@ function defaultRun(command, path) {
14091
14290
  return result.status ?? 0;
14092
14291
  }
14093
14292
  function editText(initial, slug, deps = {}) {
14094
- const env = deps.env ?? import_node_process14.default.env;
14095
- const interactive = deps.interactive ?? (() => Boolean(import_node_process14.default.stdin.isTTY));
14293
+ const env = deps.env ?? import_node_process16.default.env;
14294
+ const interactive = deps.interactive ?? (() => Boolean(import_node_process16.default.stdin.isTTY));
14096
14295
  const editor = resolveEditor(env);
14097
14296
  if (!editor)
14098
14297
  throw new Error(
@@ -14100,16 +14299,16 @@ function editText(initial, slug, deps = {}) {
14100
14299
  );
14101
14300
  if (!interactive())
14102
14301
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
14103
- const dir = (0, import_node_fs19.mkdtempSync)((0, import_node_path18.join)((0, import_node_os4.tmpdir)(), "odla-runbook-"));
14104
- const file = (0, import_node_path18.join)(dir, `${slug}.md`);
14302
+ const dir = (0, import_node_fs22.mkdtempSync)((0, import_node_path20.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
14303
+ const file = (0, import_node_path20.join)(dir, `${slug}.md`);
14105
14304
  try {
14106
- (0, import_node_fs19.writeFileSync)(file, initial, { mode: 384 });
14305
+ (0, import_node_fs22.writeFileSync)(file, initial, { mode: 384 });
14107
14306
  const code = defaultRunOrInjected(deps)(editor, file);
14108
14307
  if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
14109
- const edited = (0, import_node_fs19.readFileSync)(file, "utf8");
14308
+ const edited = (0, import_node_fs22.readFileSync)(file, "utf8");
14110
14309
  return edited === initial ? null : edited;
14111
14310
  } finally {
14112
- (0, import_node_fs19.rmSync)(dir, { recursive: true, force: true });
14311
+ (0, import_node_fs22.rmSync)(dir, { recursive: true, force: true });
14113
14312
  }
14114
14313
  }
14115
14314
  var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
@@ -14453,7 +14652,7 @@ function hostedSeverity(value2, flag) {
14453
14652
  var import_security2 = require("@odla-ai/security");
14454
14653
 
14455
14654
  // src/security.ts
14456
- var import_node_path19 = require("path");
14655
+ var import_node_path21 = require("path");
14457
14656
  var import_security = require("@odla-ai/security");
14458
14657
  var import_node3 = require("@odla-ai/security/node");
14459
14658
  async function runHostedSecurity(options) {
@@ -14465,9 +14664,9 @@ async function runHostedSecurity(options) {
14465
14664
  const appId = selfAudit ? "odla-ai" : cfg.app.id;
14466
14665
  const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
14467
14666
  const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
14468
- const target = (0, import_node_path19.resolve)(options.target ?? cfg?.rootDir ?? ".");
14469
- const output = (0, import_node_path19.resolve)(options.out ?? (0, import_node_path19.resolve)(target, ".odla/security/hosted"));
14470
- const outputRelative = (0, import_node_path19.relative)(target, output).split(import_node_path19.sep).join("/");
14667
+ const target = (0, import_node_path21.resolve)(options.target ?? cfg?.rootDir ?? ".");
14668
+ const output = (0, import_node_path21.resolve)(options.out ?? (0, import_node_path21.resolve)(target, ".odla/security/hosted"));
14669
+ const outputRelative = (0, import_node_path21.relative)(target, output).split(import_node_path21.sep).join("/");
14471
14670
  if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
14472
14671
  const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
14473
14672
  const tokenRequest = {
@@ -14479,7 +14678,7 @@ async function runHostedSecurity(options) {
14479
14678
  };
14480
14679
  const token = await injectedToken(options, tokenRequest);
14481
14680
  const snapshot = await (0, import_node3.snapshotDirectory)(target, {
14482
- exclude: !outputRelative.startsWith("../") && !(0, import_node_path19.isAbsolute)(outputRelative) ? [outputRelative] : []
14681
+ exclude: !outputRelative.startsWith("../") && !(0, import_node_path21.isAbsolute)(outputRelative) ? [outputRelative] : []
14483
14682
  });
14484
14683
  const hosted = await (0, import_security.createPlatformSecurityReasoners)({
14485
14684
  platform,
@@ -14497,7 +14696,7 @@ async function runHostedSecurity(options) {
14497
14696
  });
14498
14697
  const harness = (0, import_security.createSecurityHarness)({
14499
14698
  profile,
14500
- store: new import_node3.FileRunStore((0, import_node_path19.resolve)(output, "state")),
14699
+ store: new import_node3.FileRunStore((0, import_node_path21.resolve)(output, "state")),
14501
14700
  discoveryReasoner: hosted.discoveryReasoner,
14502
14701
  validationReasoner: hosted.validationReasoner,
14503
14702
  policy: {
@@ -14521,7 +14720,7 @@ async function runHostedSecurity(options) {
14521
14720
  function selectEnv(requested, declared, configPath, rootDir) {
14522
14721
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
14523
14722
  if (!env || !declared.includes(env)) {
14524
- const shown = (0, import_node_path19.relative)(rootDir, configPath) || configPath;
14723
+ const shown = (0, import_node_path21.relative)(rootDir, configPath) || configPath;
14525
14724
  throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
14526
14725
  }
14527
14726
  return env;
@@ -14550,7 +14749,7 @@ function printSummary(out, appId, env, run, report5, output) {
14550
14749
  out.log(` coverage: ${report5.coverageStatus} ${complete}/${report5.coverage.length} blocked=${report5.metrics.blockedCells} shallow=${report5.metrics.shallowCells} unscheduled=${report5.metrics.unscheduledCells} budget_exhausted=${report5.metrics.budgetExhaustedCells}`);
14551
14750
  if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
14552
14751
  out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
14553
- out.log(` report: ${(0, import_node_path19.resolve)(output, "REPORT.md")}`);
14752
+ out.log(` report: ${(0, import_node_path21.resolve)(output, "REPORT.md")}`);
14554
14753
  }
14555
14754
  function formatBudget(usage) {
14556
14755
  return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
@@ -15001,6 +15200,23 @@ function exitCodeFor(err) {
15001
15200
 
15002
15201
  // src/cli.ts
15003
15202
  async function runCli(argv = process.argv.slice(2), dependencies = {}) {
15203
+ const out = redactingOutput(dependencies.stdout ?? console);
15204
+ const advisories = [];
15205
+ const withAdvisoryReader = {
15206
+ ...dependencies,
15207
+ fetch: advisoryCollectingFetch(dependencies.fetch ?? fetch, advisories)
15208
+ };
15209
+ try {
15210
+ return await dispatchCli(argv, withAdvisoryReader);
15211
+ } catch (error) {
15212
+ const explanation = explainRejectedCredential(error);
15213
+ if (explanation) out.error(explanation);
15214
+ throw error;
15215
+ } finally {
15216
+ renderAdvisories(out, advisories);
15217
+ }
15218
+ }
15219
+ async function dispatchCli(argv, dependencies) {
15004
15220
  const runtime = {
15005
15221
  ...dependencies,
15006
15222
  stdout: redactingOutput(dependencies.stdout ?? console)
@@ -15030,6 +15246,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
15030
15246
  await contextCommand(parsed, runtime);
15031
15247
  return;
15032
15248
  }
15249
+ if (command === "device") {
15250
+ await deviceCommand(parsed, runtime);
15251
+ return;
15252
+ }
15033
15253
  if (command === "credentials") {
15034
15254
  await credentialCommand(parsed, runtime);
15035
15255
  return;