@odla-ai/cli 0.35.3 → 0.36.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/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);
@@ -259,13 +259,50 @@ function explainRejectedCredential(error) {
259
259
  ].join("\n");
260
260
  }
261
261
 
262
- // src/local.ts
262
+ // src/device-session.ts
263
263
  var import_node_fs3 = require("fs");
264
+ var import_node_os = require("os");
264
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 revocable = response2.status === 401 || response2.status === 403 || response2.status === 404;
291
+ const detail = body.error?.message ?? (response2.ok ? `registry returned ${response2.status} with no session token` : `registry returned ${response2.status}`);
292
+ throw new Error(
293
+ `device session failed: ${detail} (${response2.status})` + (revocable ? " \u2014 if this machine's enrollment was revoked or has expired, enroll it again in Studio" : "")
294
+ );
295
+ }
296
+ return { token: body.token, expiresAt: body.expiresAt ?? Date.now() };
297
+ }
298
+
299
+ // src/local.ts
300
+ var import_node_fs4 = require("fs");
301
+ var import_node_path3 = require("path");
265
302
  var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
266
303
  function readJsonFile(path) {
267
304
  try {
268
- return JSON.parse((0, import_node_fs3.readFileSync)(path, "utf8"));
305
+ return JSON.parse((0, import_node_fs4.readFileSync)(path, "utf8"));
269
306
  } catch {
270
307
  return null;
271
308
  }
@@ -275,10 +312,10 @@ function writePrivateJson(path, value2) {
275
312
  `);
276
313
  }
277
314
  function readCredentials(path) {
278
- if (!(0, import_node_fs3.existsSync)(path)) return null;
315
+ if (!(0, import_node_fs4.existsSync)(path)) return null;
279
316
  let value2;
280
317
  try {
281
- value2 = JSON.parse((0, import_node_fs3.readFileSync)(path, "utf8"));
318
+ value2 = JSON.parse((0, import_node_fs4.readFileSync)(path, "utf8"));
282
319
  } catch {
283
320
  throw new Error(`credentials file ${path} is not valid JSON; fix or remove it before provisioning`);
284
321
  }
@@ -308,14 +345,14 @@ function mergeCredential(current, update) {
308
345
  return next;
309
346
  }
310
347
  function ensureGitignore(rootDir, localPaths = []) {
311
- const path = (0, import_node_path2.resolve)(rootDir, ".gitignore");
312
- const existing = (0, import_node_fs3.existsSync)(path) ? (0, import_node_fs3.readFileSync)(path, "utf8") : "";
348
+ const path = (0, import_node_path3.resolve)(rootDir, ".gitignore");
349
+ const existing = (0, import_node_fs4.existsSync)(path) ? (0, import_node_fs4.readFileSync)(path, "utf8") : "";
313
350
  const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line2) => !!line2);
314
351
  const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
315
352
  const missing = wanted.filter((line2) => !existing.split(/\r?\n/).includes(line2));
316
353
  if (missing.length === 0) return;
317
354
  const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
318
- (0, import_node_fs3.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
355
+ (0, import_node_fs4.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
319
356
  `);
320
357
  }
321
358
  function o11yDevVars(cfg) {
@@ -329,7 +366,7 @@ function o11yDevVars(cfg) {
329
366
  function resolveWriteDevVarsTarget(cfg, requested) {
330
367
  if (!requested) return null;
331
368
  if (requested === true) return cfg.local.devVarsFile;
332
- return (0, import_node_path2.resolve)((0, import_node_path2.dirname)(cfg.configPath), requested);
369
+ return (0, import_node_path3.resolve)((0, import_node_path3.dirname)(cfg.configPath), requested);
333
370
  }
334
371
  function writeDevVars(path, credentials, env, o11y) {
335
372
  const entry = credentials.envs[env];
@@ -343,7 +380,7 @@ function writeDevVars(path, credentials, env, o11y) {
343
380
  if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
344
381
  if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
345
382
  }
346
- const existing = (0, import_node_fs3.existsSync)(path) ? (0, import_node_fs3.readFileSync)(path, "utf8") : "";
383
+ const existing = (0, import_node_fs4.existsSync)(path) ? (0, import_node_fs4.readFileSync)(path, "utf8") : "";
347
384
  const retained = existing.split(/\r?\n/).filter((line2) => !isManagedDevVar(line2));
348
385
  while (retained.at(-1) === "") retained.pop();
349
386
  const prefix = retained.length ? `${retained.join("\n")}
@@ -369,19 +406,19 @@ function isManagedDevVar(line2) {
369
406
  return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
370
407
  }
371
408
  function writePrivateText(path, text3) {
372
- (0, import_node_fs3.mkdirSync)((0, import_node_path2.dirname)(path), { recursive: true });
409
+ (0, import_node_fs4.mkdirSync)((0, import_node_path3.dirname)(path), { recursive: true });
373
410
  const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
374
- (0, import_node_fs3.writeFileSync)(temporary, text3, { mode: 384 });
375
- (0, import_node_fs3.chmodSync)(temporary, 384);
376
- (0, import_node_fs3.renameSync)(temporary, path);
411
+ (0, import_node_fs4.writeFileSync)(temporary, text3, { mode: 384 });
412
+ (0, import_node_fs4.chmodSync)(temporary, 384);
413
+ (0, import_node_fs4.renameSync)(temporary, path);
377
414
  }
378
415
  function gitignoreEntry(rootDir, path) {
379
- const rel = (0, import_node_path2.relative)((0, import_node_path2.resolve)(rootDir), (0, import_node_path2.resolve)(path));
380
- if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path2.isAbsolute)(rel)) return null;
416
+ const rel = (0, import_node_path3.relative)((0, import_node_path3.resolve)(rootDir), (0, import_node_path3.resolve)(path));
417
+ if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path3.isAbsolute)(rel)) return null;
381
418
  return rel.replaceAll("\\", "/");
382
419
  }
383
420
  function displayPath(path, rootDir = process.cwd()) {
384
- const rel = (0, import_node_path2.relative)(rootDir, path);
421
+ const rel = (0, import_node_path3.relative)(rootDir, path);
385
422
  return rel && !rel.startsWith("..") ? rel : path;
386
423
  }
387
424
 
@@ -393,14 +430,20 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
393
430
  const cached = readJsonFile(cfg.local.tokenFile);
394
431
  if (!grantRequest.forceReview && !grantRequest.freshLogin) {
395
432
  if (options.token) return options.token;
396
- if (import_node_process4.default.env.ODLA_DEV_TOKEN) {
397
- const declared = import_node_process4.default.env.ODLA_DEV_TOKEN_AUDIENCE;
433
+ if (import_node_process5.default.env.ODLA_DEV_TOKEN) {
434
+ const declared = import_node_process5.default.env.ODLA_DEV_TOKEN_AUDIENCE;
398
435
  if (declared) {
399
436
  if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
400
437
  } else if (audience !== "https://odla.ai") {
401
438
  throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
402
439
  }
403
- return import_node_process4.default.env.ODLA_DEV_TOKEN;
440
+ return import_node_process5.default.env.ODLA_DEV_TOKEN;
441
+ }
442
+ const device = readDeviceCredential(audience);
443
+ if (device) {
444
+ const session = await mintDeviceSession(cfg.platformUrl, device, doFetch);
445
+ out.error(`auth: session minted by this enrolled device (${displayPath(deviceCredentialPath(), cfg.rootDir)})`);
446
+ return session.token;
404
447
  }
405
448
  if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4 && cachedGrantCovers(cached, grantIntent)) {
406
449
  out.error(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
@@ -505,7 +548,7 @@ function stillPending(pending, email) {
505
548
  );
506
549
  }
507
550
  function handshakeEmail(value2, cached) {
508
- const email = (value2 ?? import_node_process4.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
551
+ const email = (value2 ?? import_node_process5.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
509
552
  if (/@users\.noreply\.github\.com$/i.test(email)) {
510
553
  throw new Error(
511
554
  `"${email}" is a GitHub commit identity, not an odla account email; use --email <signed-in-odla-account> or ODLA_USER_EMAIL`
@@ -536,12 +579,12 @@ function platformAudience(value2) {
536
579
  }
537
580
 
538
581
  // src/secret-input.ts
539
- var import_node_process5 = __toESM(require("process"), 1);
582
+ var import_node_process6 = __toESM(require("process"), 1);
540
583
  var MAX_BYTES = 64 * 1024;
541
584
  async function secretInputValue(options, kind = "credential") {
542
585
  if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
543
586
  let value2;
544
- if (options.fromEnv) value2 = import_node_process5.default.env[options.fromEnv];
587
+ if (options.fromEnv) value2 = import_node_process6.default.env[options.fromEnv];
545
588
  else if (options.stdin) value2 = await (options.readStdin ?? (() => readSecretStream(kind)))();
546
589
  else throw new Error(`${kind} input required: use --from-env <NAME> or --stdin; values are never accepted as arguments`);
547
590
  value2 = value2?.replace(/[\r\n]+$/, "");
@@ -549,7 +592,7 @@ async function secretInputValue(options, kind = "credential") {
549
592
  if (new TextEncoder().encode(value2).byteLength > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
550
593
  return value2;
551
594
  }
552
- async function readSecretStream(kind, stream = import_node_process5.default.stdin) {
595
+ async function readSecretStream(kind, stream = import_node_process6.default.stdin) {
553
596
  let value2 = "";
554
597
  for await (const chunk of stream) {
555
598
  value2 += String(chunk);
@@ -559,9 +602,9 @@ async function readSecretStream(kind, stream = import_node_process5.default.stdi
559
602
  }
560
603
 
561
604
  // src/admin-ai-auth.ts
562
- var import_node_fs4 = require("fs");
563
- var import_node_path3 = require("path");
564
- var import_node_process6 = __toESM(require("process"), 1);
605
+ var import_node_fs5 = require("fs");
606
+ var import_node_path4 = require("path");
607
+ var import_node_process7 = __toESM(require("process"), 1);
565
608
  var import_db2 = require("@odla-ai/db");
566
609
  async function getScopedPlatformToken(options) {
567
610
  return resolveAdminPlatformToken(options);
@@ -569,7 +612,7 @@ async function getScopedPlatformToken(options) {
569
612
  async function resolveAdminPlatformToken(options) {
570
613
  const audience = platformAudience(options.platform);
571
614
  if (options.token) return options.token;
572
- const fromEnv = import_node_process6.default.env.ODLA_ADMIN_TOKEN;
615
+ const fromEnv = import_node_process7.default.env.ODLA_ADMIN_TOKEN;
573
616
  if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
574
617
  return scopedToken(
575
618
  audience,
@@ -581,7 +624,7 @@ async function resolveAdminPlatformToken(options) {
581
624
  }
582
625
  function audienceBoundEnvToken(token, platform) {
583
626
  const audience = platformAudience(platform);
584
- const declared = import_node_process6.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
627
+ const declared = import_node_process7.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
585
628
  if (declared) {
586
629
  if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
587
630
  } else if (audience !== "https://odla.ai") {
@@ -594,6 +637,7 @@ var SCOPE_PURPOSE = {
594
637
  "app:config:read": "compare checked-in intent with an exact-id app Registry configuration",
595
638
  "app:config:write": "apply or inspect one revision-bound configuration operation for an app you own",
596
639
  "platform:runbook:write": "read and edit all of odla's operational runbooks, including admin-visible content",
640
+ "app:device:enroll": "enrol this machine so it can mint its own short-lived credentials without asking you again",
597
641
  "platform:ai:policy:write": "change System AI model routing",
598
642
  "platform:ai:policy:read": "read System AI model routing",
599
643
  "platform:ai:credential:write": "replace a stored AI provider key",
@@ -606,8 +650,8 @@ var SCOPE_PURPOSE = {
606
650
  };
607
651
  async function scopedToken(platform, scope, options, doFetch, out) {
608
652
  const audience = platformAudience(platform);
609
- const rootDir = options.rootDir ?? import_node_process6.default.cwd();
610
- const tokenFile = options.tokenFile ?? (0, import_node_path3.join)(rootDir, ".odla/admin-token.local.json");
653
+ const rootDir = options.rootDir ?? import_node_process7.default.cwd();
654
+ const tokenFile = options.tokenFile ?? (0, import_node_path4.join)(rootDir, ".odla/admin-token.local.json");
611
655
  const cache2 = options.cache === false ? null : readJsonFile(tokenFile);
612
656
  const cached = cache2?.platform === audience ? cache2.tokens?.[scope] : void 0;
613
657
  if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
@@ -634,7 +678,7 @@ async function scopedToken(platform, scope, options, doFetch, out) {
634
678
  if (options.cache !== false) {
635
679
  const tokens = cache2?.platform === audience ? { ...cache2.tokens ?? {} } : {};
636
680
  tokens[scope] = { token, expiresAt };
637
- if ((0, import_node_fs4.existsSync)((0, import_node_path3.join)(rootDir, ".git"))) ensureGitignore(rootDir, [tokenFile]);
681
+ if ((0, import_node_fs5.existsSync)((0, import_node_path4.join)(rootDir, ".git"))) ensureGitignore(rootDir, [tokenFile]);
638
682
  writePrivateJson(tokenFile, { platform: audience, email, tokens });
639
683
  out.error(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
640
684
  } else {
@@ -811,7 +855,7 @@ function isRecord2(value2) {
811
855
 
812
856
  // src/admin-ai.ts
813
857
  async function adminAi(options) {
814
- const platform = platformAudience(options.platform ?? import_node_process7.default.env.ODLA_PLATFORM ?? "https://odla.ai");
858
+ const platform = platformAudience(options.platform ?? import_node_process8.default.env.ODLA_PLATFORM ?? "https://odla.ai");
815
859
  const doFetch = options.fetch ?? fetch;
816
860
  const out = options.stdout ?? console;
817
861
  const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
@@ -1066,13 +1110,13 @@ function addOption(options, name, value2) {
1066
1110
  }
1067
1111
 
1068
1112
  // src/operator-context.ts
1069
- var import_node_fs7 = require("fs");
1070
- var import_node_path6 = require("path");
1071
- var import_node_process9 = __toESM(require("process"), 1);
1113
+ var import_node_fs8 = require("fs");
1114
+ var import_node_path7 = require("path");
1115
+ var import_node_process10 = __toESM(require("process"), 1);
1072
1116
 
1073
1117
  // src/config.ts
1074
- var import_node_fs5 = require("fs");
1075
- var import_node_path4 = require("path");
1118
+ var import_node_fs6 = require("fs");
1119
+ var import_node_path5 = require("path");
1076
1120
  var import_node_url = require("url");
1077
1121
  var import_apps = require("@odla-ai/apps");
1078
1122
 
@@ -1477,12 +1521,12 @@ var DEFAULT_SERVICES = ["db", "ai"];
1477
1521
  var configImportSerial = 0;
1478
1522
  var GOOGLE_CALENDAR_EVENTS_SCOPE = "https://www.googleapis.com/auth/calendar.events";
1479
1523
  async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1480
- const resolved = (0, import_node_path4.resolve)(configPath);
1481
- if (!(0, import_node_fs5.existsSync)(resolved)) {
1524
+ const resolved = (0, import_node_path5.resolve)(configPath);
1525
+ if (!(0, import_node_fs6.existsSync)(resolved)) {
1482
1526
  throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
1483
1527
  }
1484
1528
  const raw = await loadConfigModule(resolved);
1485
- const rootDir = (0, import_node_path4.dirname)(resolved);
1529
+ const rootDir = (0, import_node_path5.dirname)(resolved);
1486
1530
  validateRawConfig(raw, resolved);
1487
1531
  const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
1488
1532
  const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
@@ -1492,9 +1536,9 @@ async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1492
1536
  validateCalendarConfig(raw, unique3([...envs, ...options.additionalEnvs ?? []]), services, resolved);
1493
1537
  validateMonitoringConfig(raw, unique3([...envs, ...options.additionalEnvs ?? []]), services, resolved);
1494
1538
  const local = {
1495
- tokenFile: (0, import_node_path4.resolve)(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
1496
- credentialsFile: (0, import_node_path4.resolve)(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
1497
- devVarsFile: (0, import_node_path4.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
1539
+ tokenFile: (0, import_node_path5.resolve)(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
1540
+ credentialsFile: (0, import_node_path5.resolve)(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
1541
+ devVarsFile: (0, import_node_path5.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
1498
1542
  gitignore: raw.local?.gitignore ?? true
1499
1543
  };
1500
1544
  return {
@@ -1511,9 +1555,9 @@ async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1511
1555
  async function resolveDataExport(cfg, value2, names) {
1512
1556
  if (value2 === void 0 || value2 === null || value2 === false) return void 0;
1513
1557
  if (typeof value2 !== "string") return value2;
1514
- const target = (0, import_node_path4.isAbsolute)(value2) ? value2 : (0, import_node_path4.resolve)(cfg.rootDir, value2);
1558
+ const target = (0, import_node_path5.isAbsolute)(value2) ? value2 : (0, import_node_path5.resolve)(cfg.rootDir, value2);
1515
1559
  if (target.endsWith(".json")) {
1516
- return JSON.parse((0, import_node_fs5.readFileSync)(target, "utf8"));
1560
+ return JSON.parse((0, import_node_fs6.readFileSync)(target, "utf8"));
1517
1561
  }
1518
1562
  const mod = await import((0, import_node_url.pathToFileURL)(target).href);
1519
1563
  for (const name of names) {
@@ -1589,7 +1633,7 @@ function validId2(value2) {
1589
1633
  return typeof value2 === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value2);
1590
1634
  }
1591
1635
  async function loadConfigModule(path) {
1592
- if (path.endsWith(".json")) return JSON.parse((0, import_node_fs5.readFileSync)(path, "utf8"));
1636
+ if (path.endsWith(".json")) return JSON.parse((0, import_node_fs6.readFileSync)(path, "utf8"));
1593
1637
  const nonce = `${Date.now()}-${configImportSerial++}`;
1594
1638
  const mod = await import(`${(0, import_node_url.pathToFileURL)(path).href}?reload=${nonce}`);
1595
1639
  const value2 = mod.default ?? mod.config;
@@ -1604,18 +1648,18 @@ function unique3(values) {
1604
1648
  }
1605
1649
 
1606
1650
  // src/operator-profiles.ts
1607
- var import_node_fs6 = require("fs");
1608
- var import_node_os = require("os");
1609
- var import_node_path5 = require("path");
1610
- var import_node_process8 = __toESM(require("process"), 1);
1651
+ var import_node_fs7 = require("fs");
1652
+ var import_node_os2 = require("os");
1653
+ var import_node_path6 = require("path");
1654
+ var import_node_process9 = __toESM(require("process"), 1);
1611
1655
  function operatorProfileFile() {
1612
- return (0, import_node_path5.resolve)(
1613
- clean(import_node_process8.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path5.join)((0, import_node_os.homedir)(), ".odla", "contexts.json")
1656
+ return (0, import_node_path6.resolve)(
1657
+ clean(import_node_process9.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".odla", "contexts.json")
1614
1658
  );
1615
1659
  }
1616
1660
  function resolveOperatorProfile(parsed) {
1617
1661
  const fromFlag = clean(stringOpt(parsed.options.context));
1618
- const fromEnvironment = clean(import_node_process8.default.env.ODLA_CONTEXT);
1662
+ const fromEnvironment = clean(import_node_process9.default.env.ODLA_CONTEXT);
1619
1663
  const name = fromFlag ?? fromEnvironment ?? null;
1620
1664
  const file = operatorProfileFile();
1621
1665
  if (!name) {
@@ -1655,10 +1699,10 @@ function removeOperatorProfile(name, file = operatorProfileFile()) {
1655
1699
  return true;
1656
1700
  }
1657
1701
  function operatorCredentialFiles(selection) {
1658
- 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");
1702
+ 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");
1659
1703
  return {
1660
- developer: (0, import_node_path5.join)(base, "dev-token.json"),
1661
- scoped: (0, import_node_path5.join)(base, "admin-token.local.json")
1704
+ developer: (0, import_node_path6.join)(base, "dev-token.json"),
1705
+ scoped: (0, import_node_path6.join)(base, "admin-token.local.json")
1662
1706
  };
1663
1707
  }
1664
1708
  function assertOperatorName(value2, label) {
@@ -1669,10 +1713,10 @@ function assertOperatorName(value2, label) {
1669
1713
  }
1670
1714
  }
1671
1715
  function readOperatorProfiles(file) {
1672
- if (!(0, import_node_fs6.existsSync)(file)) return emptyProfiles();
1716
+ if (!(0, import_node_fs7.existsSync)(file)) return emptyProfiles();
1673
1717
  let raw;
1674
1718
  try {
1675
- raw = JSON.parse((0, import_node_fs6.readFileSync)(file, "utf8"));
1719
+ raw = JSON.parse((0, import_node_fs7.readFileSync)(file, "utf8"));
1676
1720
  } catch {
1677
1721
  throw new Error(`operator context file ${file} is not valid JSON`);
1678
1722
  }
@@ -1736,21 +1780,21 @@ var DEFAULT_PLATFORM2 = "https://odla.ai";
1736
1780
  async function resolveOperatorContext(parsed, options = {}) {
1737
1781
  const profile = resolveOperatorProfile(parsed);
1738
1782
  const configArgument = stringOpt(parsed.options.config) ?? "odla.config.mjs";
1739
- const configPath = (0, import_node_path6.resolve)(configArgument);
1783
+ const configPath = (0, import_node_path7.resolve)(configArgument);
1740
1784
  const explicitConfig = parsed.options.config !== void 0;
1741
- const hasConfig = (0, import_node_fs7.existsSync)(configPath);
1785
+ const hasConfig = (0, import_node_fs8.existsSync)(configPath);
1742
1786
  if (!hasConfig && (!options.allowMissingConfig || explicitConfig)) {
1743
1787
  await loadProjectConfig(configArgument);
1744
1788
  }
1745
1789
  const loaded = hasConfig ? await loadProjectConfig(configArgument) : void 0;
1746
1790
  const platformFlag = clean2(stringOpt(parsed.options.platform));
1747
- const platformEnvironment = clean2(import_node_process9.default.env.ODLA_PLATFORM_URL);
1791
+ const platformEnvironment = clean2(import_node_process10.default.env.ODLA_PLATFORM_URL);
1748
1792
  const platformValue = platformAudience(
1749
1793
  platformFlag ?? platformEnvironment ?? profile.value?.platform ?? loaded?.platformUrl ?? DEFAULT_PLATFORM2
1750
1794
  );
1751
1795
  const platformSource = platformFlag ? "flag" : platformEnvironment ? "environment" : profile.value ? "profile" : loaded ? "config" : "default";
1752
1796
  const appFlag = clean2(stringOpt(parsed.options.app));
1753
- const appEnvironment = clean2(import_node_process9.default.env.ODLA_APP_ID);
1797
+ const appEnvironment = clean2(import_node_process10.default.env.ODLA_APP_ID);
1754
1798
  const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
1755
1799
  const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
1756
1800
  if (appValue) assertOperatorName(appValue, "app");
@@ -1760,16 +1804,16 @@ async function resolveOperatorContext(parsed, options = {}) {
1760
1804
  );
1761
1805
  }
1762
1806
  const envFlag = clean2(stringOpt(parsed.options.env));
1763
- const envEnvironment = clean2(import_node_process9.default.env.ODLA_ENV);
1807
+ const envEnvironment = clean2(import_node_process10.default.env.ODLA_ENV);
1764
1808
  const environmentValue = envFlag ?? envEnvironment ?? profile.value?.environment ?? options.defaultEnvironment ?? null;
1765
1809
  const environmentSource = envFlag ? "flag" : envEnvironment ? "environment" : profile.value?.environment ? "profile" : options.defaultEnvironment ? "default" : "unresolved";
1766
1810
  if (environmentValue) {
1767
1811
  assertOperatorName(environmentValue, "environment");
1768
1812
  }
1769
- const rootDir = loaded?.rootDir ?? import_node_process9.default.cwd();
1813
+ const rootDir = loaded?.rootDir ?? import_node_process10.default.cwd();
1770
1814
  const profileCredentials = operatorCredentialFiles(profile);
1771
- 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;
1772
- 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;
1815
+ 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;
1816
+ 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;
1773
1817
  const cfg = loaded ? {
1774
1818
  ...loaded,
1775
1819
  platformUrl: platformValue,
@@ -1791,8 +1835,8 @@ async function resolveOperatorContext(parsed, options = {}) {
1791
1835
  services: [],
1792
1836
  local: {
1793
1837
  tokenFile,
1794
- credentialsFile: (0, import_node_path6.join)(rootDir, ".odla", "credentials.local.json"),
1795
- devVarsFile: (0, import_node_path6.join)(rootDir, ".dev.vars"),
1838
+ credentialsFile: (0, import_node_path7.join)(rootDir, ".odla", "credentials.local.json"),
1839
+ devVarsFile: (0, import_node_path7.join)(rootDir, ".dev.vars"),
1796
1840
  gitignore: true
1797
1841
  }
1798
1842
  };
@@ -1889,7 +1933,7 @@ async function adminCommand(parsed, deps = {}) {
1889
1933
  }
1890
1934
 
1891
1935
  // src/auth-command.ts
1892
- var import_node_process10 = __toESM(require("process"), 1);
1936
+ var import_node_process11 = __toESM(require("process"), 1);
1893
1937
 
1894
1938
  // src/whoami-command.ts
1895
1939
  var text2 = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
@@ -2051,7 +2095,7 @@ async function authCommand(parsed, deps = {}) {
2051
2095
  const { cfg } = context;
2052
2096
  const out = deps.stdout ?? console;
2053
2097
  const doFetch = deps.fetch ?? fetch;
2054
- const email = stringOpt(parsed.options.email) ?? import_node_process10.default.env.ODLA_USER_EMAIL?.trim();
2098
+ const email = stringOpt(parsed.options.email) ?? import_node_process11.default.env.ODLA_USER_EMAIL?.trim();
2055
2099
  if (!email) {
2056
2100
  throw new Error(
2057
2101
  "auth login requires --email <odla-account> or ODLA_USER_EMAIL; confirm the signed-in odla email instead of using git or GitHub identity"
@@ -2204,7 +2248,7 @@ async function appExport(options) {
2204
2248
  }
2205
2249
 
2206
2250
  // src/app-import.ts
2207
- var import_node_fs8 = require("fs");
2251
+ var import_node_fs9 = require("fs");
2208
2252
  var import_import = require("@odla-ai/db/import");
2209
2253
  function chooseIdMode(options, rows) {
2210
2254
  const chosen = [options.idField && "field", options.key && "key", options.generateIds && "generate"].filter(Boolean);
@@ -2222,7 +2266,7 @@ async function appImport(options) {
2222
2266
  const out = options.stdout ?? console;
2223
2267
  const say = options.json ? (line2) => out.error(line2) : (line2) => out.log(line2);
2224
2268
  const { tenant } = resolveTenant(cfg, options.env);
2225
- const text3 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs8.readFileSync)(0, "utf8")))() : (0, import_node_fs8.readFileSync)(options.file, "utf8");
2269
+ const text3 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs9.readFileSync)(0, "utf8")))() : (0, import_node_fs9.readFileSync)(options.file, "utf8");
2226
2270
  const { format, sources } = (0, import_import.parseImport)(text3, options.ns);
2227
2271
  if (format === "namespace-map" && options.ns) {
2228
2272
  throw new Error("--ns cannot be combined with a {namespace: rows} file \u2014 the file already names each namespace");
@@ -2412,7 +2456,7 @@ async function appCommand(parsed, dependencies = {}) {
2412
2456
 
2413
2457
  // src/brand-command.ts
2414
2458
  var import_promises = require("fs/promises");
2415
- var import_node_path7 = require("path");
2459
+ var import_node_path8 = require("path");
2416
2460
 
2417
2461
  // src/brand-design-unpack.ts
2418
2462
  var import_node_zlib = require("zlib");
@@ -2514,15 +2558,15 @@ function describeUnpack(result, outDir) {
2514
2558
  // src/brand-command.ts
2515
2559
  var USAGE = "usage: odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]";
2516
2560
  async function readBundle(source, deps) {
2517
- if (source !== "-") return (0, import_promises.readFile)((0, import_node_path7.resolve)(source), "utf8");
2561
+ if (source !== "-") return (0, import_promises.readFile)((0, import_node_path8.resolve)(source), "utf8");
2518
2562
  const readStdin = deps.readStdin;
2519
2563
  if (!readStdin) throw new Error("reading a bundle from stdin is not supported here");
2520
2564
  return readStdin();
2521
2565
  }
2522
2566
  async function writeAll(result, outDir) {
2523
2567
  for (const file of result.files) {
2524
- const target = (0, import_node_path7.resolve)(outDir, file.path);
2525
- await (0, import_promises.mkdir)((0, import_node_path7.dirname)(target), { recursive: true });
2568
+ const target = (0, import_node_path8.resolve)(outDir, file.path);
2569
+ await (0, import_promises.mkdir)((0, import_node_path8.dirname)(target), { recursive: true });
2526
2570
  await (0, import_promises.writeFile)(target, file.bytes);
2527
2571
  }
2528
2572
  }
@@ -2530,7 +2574,7 @@ async function designUnpack(parsed, deps) {
2530
2574
  assertArgs(parsed, ["out", "json"], 4);
2531
2575
  const source = parsed.positionals[3];
2532
2576
  if (!source) throw new Error(USAGE);
2533
- const outDir = (0, import_node_path7.resolve)(stringOpt(parsed.options.out) ?? "design");
2577
+ const outDir = (0, import_node_path8.resolve)(stringOpt(parsed.options.out) ?? "design");
2534
2578
  const result = unpackDesign(await readBundle(source, deps));
2535
2579
  await writeAll(result, outDir);
2536
2580
  const out = deps.stdout ?? console;
@@ -3135,12 +3179,12 @@ async function safeText4(response2) {
3135
3179
 
3136
3180
  // src/config-operation-command.ts
3137
3181
  var import_apps6 = require("@odla-ai/apps");
3138
- var import_node_path8 = require("path");
3182
+ var import_node_path9 = require("path");
3139
3183
 
3140
3184
  // src/version.ts
3141
- var import_node_fs9 = require("fs");
3185
+ var import_node_fs10 = require("fs");
3142
3186
  function cliVersion() {
3143
- const pkg = JSON.parse((0, import_node_fs9.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
3187
+ const pkg = JSON.parse((0, import_node_fs10.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
3144
3188
  return pkg.version ?? "unknown";
3145
3189
  }
3146
3190
 
@@ -3156,7 +3200,7 @@ var ConfigOperationCommandError = class extends Error {
3156
3200
 
3157
3201
  // src/config-operation-validate.ts
3158
3202
  var import_apps3 = require("@odla-ai/apps");
3159
- var import_node_fs10 = require("fs");
3203
+ var import_node_fs11 = require("fs");
3160
3204
 
3161
3205
  // src/config-reconcile-digest.ts
3162
3206
  var import_node_crypto2 = require("crypto");
@@ -3192,7 +3236,7 @@ var SERVICE = /^[a-z][a-z0-9-]{0,39}$/;
3192
3236
  function readPlan(path) {
3193
3237
  let value2;
3194
3238
  try {
3195
- const raw = (0, import_node_fs10.readFileSync)(path, "utf8");
3239
+ const raw = (0, import_node_fs11.readFileSync)(path, "utf8");
3196
3240
  if (Buffer.byteLength(raw) > 128 * 1024) throw new Error("plan exceeds 128 KiB");
3197
3241
  value2 = JSON.parse(raw);
3198
3242
  } catch (error) {
@@ -3565,7 +3609,7 @@ async function operationClient(cfg, options, purpose) {
3565
3609
  platform: cfg.platformUrl,
3566
3610
  scope: "app:config:write",
3567
3611
  token: options.token,
3568
- tokenFile: (0, import_node_path8.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
3612
+ tokenFile: (0, import_node_path9.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
3569
3613
  rootDir: cfg.rootDir,
3570
3614
  email: options.email,
3571
3615
  open: options.open,
@@ -3620,7 +3664,7 @@ function record4(value2) {
3620
3664
 
3621
3665
  // src/config-reconcile-command.ts
3622
3666
  var import_apps8 = require("@odla-ai/apps");
3623
- var import_node_path9 = require("path");
3667
+ var import_node_path10 = require("path");
3624
3668
 
3625
3669
  // src/config-reconcile.ts
3626
3670
  var import_apps7 = require("@odla-ai/apps");
@@ -3916,7 +3960,7 @@ async function inspectConfig(options) {
3916
3960
  platform: cfg.platformUrl,
3917
3961
  scope: "app:config:read",
3918
3962
  token: options.token,
3919
- tokenFile: (0, import_node_path9.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
3963
+ tokenFile: (0, import_node_path10.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
3920
3964
  rootDir: cfg.rootDir,
3921
3965
  email: options.email,
3922
3966
  open: options.open,
@@ -4048,13 +4092,13 @@ function quoteArg2(value2) {
4048
4092
 
4049
4093
  // src/doctor-checks.ts
4050
4094
  var import_node_child_process3 = require("child_process");
4051
- var import_node_fs12 = require("fs");
4052
- var import_node_path11 = require("path");
4095
+ var import_node_fs13 = require("fs");
4096
+ var import_node_path12 = require("path");
4053
4097
 
4054
4098
  // src/wrangler.ts
4055
4099
  var import_node_child_process2 = require("child_process");
4056
- var import_node_fs11 = require("fs");
4057
- var import_node_path10 = require("path");
4100
+ var import_node_fs12 = require("fs");
4101
+ var import_node_path11 = require("path");
4058
4102
  var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
4059
4103
  const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
4060
4104
  let stdout = "";
@@ -4068,15 +4112,15 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) =>
4068
4112
  var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
4069
4113
  function findWranglerConfig(rootDir) {
4070
4114
  for (const name of WRANGLER_CONFIG_FILES) {
4071
- const path = (0, import_node_path10.join)(rootDir, name);
4072
- if ((0, import_node_fs11.existsSync)(path)) return path;
4115
+ const path = (0, import_node_path11.join)(rootDir, name);
4116
+ if ((0, import_node_fs12.existsSync)(path)) return path;
4073
4117
  }
4074
4118
  return null;
4075
4119
  }
4076
4120
  function readWranglerConfig(path) {
4077
4121
  if (path.endsWith(".toml")) return null;
4078
4122
  try {
4079
- return JSON.parse(stripJsonComments((0, import_node_fs11.readFileSync)(path, "utf8")));
4123
+ return JSON.parse(stripJsonComments((0, import_node_fs12.readFileSync)(path, "utf8")));
4080
4124
  } catch {
4081
4125
  return null;
4082
4126
  }
@@ -4226,10 +4270,10 @@ function wranglerWarnings(rootDir) {
4226
4270
  for (const { label, block } of blocks) {
4227
4271
  const assets = block.assets;
4228
4272
  if (assets?.directory) {
4229
- const dir = (0, import_node_path11.resolve)(rootDir, assets.directory);
4230
- if (dir === (0, import_node_path11.resolve)(rootDir)) {
4273
+ const dir = (0, import_node_path12.resolve)(rootDir, assets.directory);
4274
+ if (dir === (0, import_node_path12.resolve)(rootDir)) {
4231
4275
  warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
4232
- } else if ((0, import_node_fs12.existsSync)((0, import_node_path11.join)(dir, "node_modules"))) {
4276
+ } else if ((0, import_node_fs13.existsSync)((0, import_node_path12.join)(dir, "node_modules"))) {
4233
4277
  warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
4234
4278
  }
4235
4279
  }
@@ -4264,13 +4308,13 @@ function o11yProjectWarnings(rootDir) {
4264
4308
  warnings.push("cannot verify o11y Worker instrumentation \u2014 add a parseable wrangler.jsonc/json config");
4265
4309
  return warnings;
4266
4310
  }
4267
- const main = typeof config.main === "string" ? (0, import_node_path11.resolve)(rootDir, config.main) : null;
4268
- if (!main || !(0, import_node_fs12.existsSync)(main)) {
4311
+ const main = typeof config.main === "string" ? (0, import_node_path12.resolve)(rootDir, config.main) : null;
4312
+ if (!main || !(0, import_node_fs13.existsSync)(main)) {
4269
4313
  warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
4270
4314
  } else {
4271
4315
  let source = "";
4272
4316
  try {
4273
- source = (0, import_node_fs12.readFileSync)(main, "utf8");
4317
+ source = (0, import_node_fs13.readFileSync)(main, "utf8");
4274
4318
  } catch {
4275
4319
  }
4276
4320
  if (!/\bwithObservability\b/.test(source)) {
@@ -4294,7 +4338,7 @@ function calendarProjectWarnings(rootDir) {
4294
4338
  }
4295
4339
  function readPackageJson(rootDir) {
4296
4340
  try {
4297
- return JSON.parse((0, import_node_fs12.readFileSync)((0, import_node_path11.join)(rootDir, "package.json"), "utf8"));
4341
+ return JSON.parse((0, import_node_fs13.readFileSync)((0, import_node_path12.join)(rootDir, "package.json"), "utf8"));
4298
4342
  } catch {
4299
4343
  return null;
4300
4344
  }
@@ -4588,14 +4632,14 @@ function harnessOption(value2, flag) {
4588
4632
  }
4589
4633
 
4590
4634
  // src/init.ts
4591
- var import_node_fs13 = require("fs");
4592
- var import_node_path12 = require("path");
4635
+ var import_node_fs14 = require("fs");
4636
+ var import_node_path13 = require("path");
4593
4637
  var import_apps9 = require("@odla-ai/apps");
4594
4638
  function initProject(options) {
4595
4639
  const out = options.stdout ?? console;
4596
- const rootDir = (0, import_node_path12.resolve)(options.rootDir ?? process.cwd());
4597
- const configPath = (0, import_node_path12.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
4598
- if ((0, import_node_fs13.existsSync)(configPath) && !options.force) {
4640
+ const rootDir = (0, import_node_path13.resolve)(options.rootDir ?? process.cwd());
4641
+ const configPath = (0, import_node_path13.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
4642
+ if ((0, import_node_fs14.existsSync)(configPath) && !options.force) {
4599
4643
  throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
4600
4644
  }
4601
4645
  if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
@@ -4611,20 +4655,20 @@ function initProject(options) {
4611
4655
  }
4612
4656
  }
4613
4657
  const aiProvider = options.aiProvider;
4614
- (0, import_node_fs13.mkdirSync)((0, import_node_path12.dirname)(configPath), { recursive: true });
4615
- (0, import_node_fs13.mkdirSync)((0, import_node_path12.resolve)(rootDir, "src/odla"), { recursive: true });
4616
- (0, import_node_fs13.mkdirSync)((0, import_node_path12.resolve)(rootDir, ".odla"), { recursive: true });
4617
- (0, import_node_fs13.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
4618
- writeIfMissing((0, import_node_path12.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
4619
- writeIfMissing((0, import_node_path12.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
4658
+ (0, import_node_fs14.mkdirSync)((0, import_node_path13.dirname)(configPath), { recursive: true });
4659
+ (0, import_node_fs14.mkdirSync)((0, import_node_path13.resolve)(rootDir, "src/odla"), { recursive: true });
4660
+ (0, import_node_fs14.mkdirSync)((0, import_node_path13.resolve)(rootDir, ".odla"), { recursive: true });
4661
+ (0, import_node_fs14.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
4662
+ writeIfMissing((0, import_node_path13.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
4663
+ writeIfMissing((0, import_node_path13.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
4620
4664
  ensureGitignore(rootDir);
4621
4665
  out.log(`created ${relativeDisplay(configPath, rootDir)}`);
4622
4666
  out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
4623
4667
  out.log("updated .gitignore for local odla credentials");
4624
4668
  }
4625
4669
  function writeIfMissing(path, text3) {
4626
- if ((0, import_node_fs13.existsSync)(path)) return;
4627
- (0, import_node_fs13.writeFileSync)(path, text3);
4670
+ if ((0, import_node_fs14.existsSync)(path)) return;
4671
+ (0, import_node_fs14.writeFileSync)(path, text3);
4628
4672
  }
4629
4673
  function configTemplate(input) {
4630
4674
  const calendar = input.services.includes("calendar") ? ` calendar: {
@@ -4920,9 +4964,9 @@ function printReport(report5, out) {
4920
4964
  }
4921
4965
 
4922
4966
  // src/skill.ts
4923
- var import_node_fs14 = require("fs");
4924
- var import_node_os2 = require("os");
4925
- var import_node_path13 = require("path");
4967
+ var import_node_fs15 = require("fs");
4968
+ var import_node_os3 = require("os");
4969
+ var import_node_path14 = require("path");
4926
4970
  var import_node_url2 = require("url");
4927
4971
 
4928
4972
  // src/skill-adapters.ts
@@ -5021,8 +5065,8 @@ function installSkill(options = {}) {
5021
5065
  const files = listFiles(sourceDir);
5022
5066
  if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
5023
5067
  const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
5024
- const root = (0, import_node_path13.resolve)(options.dir ?? process.cwd());
5025
- const home = (0, import_node_path13.resolve)(options.homeDir ?? (0, import_node_os2.homedir)());
5068
+ const root = (0, import_node_path14.resolve)(options.dir ?? process.cwd());
5069
+ const home = (0, import_node_path14.resolve)(options.homeDir ?? (0, import_node_os3.homedir)());
5026
5070
  const plans = /* @__PURE__ */ new Map();
5027
5071
  const targets = /* @__PURE__ */ new Map();
5028
5072
  const rememberTarget = (harness, target) => {
@@ -5036,48 +5080,48 @@ function installSkill(options = {}) {
5036
5080
  plans.set(target, { target, content: content2, boundary, managedMerge });
5037
5081
  };
5038
5082
  const planSkillTree = (targetDir2, boundary = root) => {
5039
- for (const rel of files) plan((0, import_node_path13.join)(targetDir2, rel), (0, import_node_fs14.readFileSync)((0, import_node_path13.join)(sourceDir, rel), "utf8"), false, boundary);
5083
+ 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);
5040
5084
  };
5041
5085
  let targetDir;
5042
5086
  if (options.global) {
5043
- const claudeRoot = (0, import_node_path13.join)(home, ".claude", "skills");
5044
- const codexRoot = (0, import_node_path13.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path13.join)(home, ".codex"), "skills");
5087
+ const claudeRoot = (0, import_node_path14.join)(home, ".claude", "skills");
5088
+ const codexRoot = (0, import_node_path14.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path14.join)(home, ".codex"), "skills");
5045
5089
  targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
5046
5090
  for (const harness of harnesses) {
5047
5091
  const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
5048
- planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path13.dirname)((0, import_node_path13.dirname)(codexRoot)));
5092
+ planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path14.dirname)((0, import_node_path14.dirname)(codexRoot)));
5049
5093
  rememberTarget(harness, skillRoot);
5050
5094
  }
5051
5095
  } else {
5052
- const sharedRoot = (0, import_node_path13.join)(root, ".agents", "skills");
5096
+ const sharedRoot = (0, import_node_path14.join)(root, ".agents", "skills");
5053
5097
  planSkillTree(sharedRoot);
5054
- const claudeRoot = (0, import_node_path13.join)(root, ".claude", "skills");
5098
+ const claudeRoot = (0, import_node_path14.join)(root, ".claude", "skills");
5055
5099
  targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
5056
5100
  for (const harness of harnesses) rememberTarget(harness, sharedRoot);
5057
5101
  if (harnesses.includes("claude")) {
5058
5102
  for (const skill of skillNames(files)) {
5059
- const canonical2 = (0, import_node_fs14.readFileSync)((0, import_node_path13.join)(sourceDir, skill, "SKILL.md"), "utf8");
5060
- plan((0, import_node_path13.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical2));
5103
+ const canonical2 = (0, import_node_fs15.readFileSync)((0, import_node_path14.join)(sourceDir, skill, "SKILL.md"), "utf8");
5104
+ plan((0, import_node_path14.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical2));
5061
5105
  }
5062
5106
  rememberTarget("claude", claudeRoot);
5063
5107
  }
5064
5108
  if (harnesses.includes("cursor")) {
5065
- const cursorRule = (0, import_node_path13.join)(root, ".cursor", "rules", "odla.mdc");
5109
+ const cursorRule = (0, import_node_path14.join)(root, ".cursor", "rules", "odla.mdc");
5066
5110
  plan(cursorRule, CURSOR_RULE);
5067
5111
  rememberTarget("cursor", cursorRule);
5068
5112
  }
5069
5113
  if (harnesses.includes("agents")) {
5070
- const agentsFile = (0, import_node_path13.join)(root, "AGENTS.md");
5114
+ const agentsFile = (0, import_node_path14.join)(root, "AGENTS.md");
5071
5115
  plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
5072
5116
  rememberTarget("agents", agentsFile);
5073
5117
  }
5074
5118
  if (harnesses.includes("copilot")) {
5075
- const copilotFile = (0, import_node_path13.join)(root, ".github", "copilot-instructions.md");
5119
+ const copilotFile = (0, import_node_path14.join)(root, ".github", "copilot-instructions.md");
5076
5120
  plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
5077
5121
  rememberTarget("copilot", copilotFile);
5078
5122
  }
5079
5123
  if (harnesses.includes("gemini")) {
5080
- const geminiFile = (0, import_node_path13.join)(root, "GEMINI.md");
5124
+ const geminiFile = (0, import_node_path14.join)(root, "GEMINI.md");
5081
5125
  plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
5082
5126
  rememberTarget("gemini", geminiFile);
5083
5127
  }
@@ -5091,11 +5135,11 @@ function installSkill(options = {}) {
5091
5135
  conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
5092
5136
  continue;
5093
5137
  }
5094
- if (!(0, import_node_fs14.existsSync)(file.target)) {
5138
+ if (!(0, import_node_fs15.existsSync)(file.target)) {
5095
5139
  writtenPaths.add(file.target);
5096
5140
  continue;
5097
5141
  }
5098
- const current = (0, import_node_fs14.readFileSync)(file.target, "utf8");
5142
+ const current = (0, import_node_fs15.readFileSync)(file.target, "utf8");
5099
5143
  if (current === file.content) {
5100
5144
  unchangedPaths.add(file.target);
5101
5145
  } else if (file.managedMerge || options.force) {
@@ -5112,9 +5156,9 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
5112
5156
  );
5113
5157
  }
5114
5158
  for (const file of plans.values()) {
5115
- if (!(0, import_node_fs14.existsSync)(file.target) || (0, import_node_fs14.readFileSync)(file.target, "utf8") !== file.content) {
5116
- (0, import_node_fs14.mkdirSync)((0, import_node_path13.dirname)(file.target), { recursive: true });
5117
- (0, import_node_fs14.writeFileSync)(file.target, file.content);
5159
+ if (!(0, import_node_fs15.existsSync)(file.target) || (0, import_node_fs15.readFileSync)(file.target, "utf8") !== file.content) {
5160
+ (0, import_node_fs15.mkdirSync)((0, import_node_path14.dirname)(file.target), { recursive: true });
5161
+ (0, import_node_fs15.writeFileSync)(file.target, file.content);
5118
5162
  }
5119
5163
  }
5120
5164
  const skills = skillNames(files);
@@ -5133,7 +5177,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
5133
5177
  };
5134
5178
  }
5135
5179
  function pathsUnder(root, paths) {
5136
- 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();
5180
+ 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();
5137
5181
  }
5138
5182
  function normalizeHarnesses(values, global) {
5139
5183
  const requested = values?.length ? values : ["claude"];
@@ -5155,9 +5199,9 @@ function normalizeHarnesses(values, global) {
5155
5199
  function managedFileContent(path, block, force, boundary) {
5156
5200
  const symlink = symlinkedComponent(boundary, path);
5157
5201
  if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
5158
- if (!(0, import_node_fs14.existsSync)(path)) return `${block}
5202
+ if (!(0, import_node_fs15.existsSync)(path)) return `${block}
5159
5203
  `;
5160
- const current = (0, import_node_fs14.readFileSync)(path, "utf8");
5204
+ const current = (0, import_node_fs15.readFileSync)(path, "utf8");
5161
5205
  const start = "<!-- odla-ai agent setup:start -->";
5162
5206
  const end = "<!-- odla-ai agent setup:end -->";
5163
5207
  const startAt = current.indexOf(start);
@@ -5178,15 +5222,15 @@ function managedFileContent(path, block, force, boundary) {
5178
5222
  return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
5179
5223
  }
5180
5224
  function symlinkedComponent(boundary, target) {
5181
- const rel = (0, import_node_path13.relative)(boundary, target);
5182
- if (rel === ".." || rel.startsWith(`..${import_node_path13.sep}`) || (0, import_node_path13.isAbsolute)(rel)) {
5225
+ const rel = (0, import_node_path14.relative)(boundary, target);
5226
+ if (rel === ".." || rel.startsWith(`..${import_node_path14.sep}`) || (0, import_node_path14.isAbsolute)(rel)) {
5183
5227
  throw new Error(`agent setup target escapes its install root: ${target}`);
5184
5228
  }
5185
5229
  let current = boundary;
5186
- for (const part of rel.split(import_node_path13.sep).filter(Boolean)) {
5187
- current = (0, import_node_path13.join)(current, part);
5230
+ for (const part of rel.split(import_node_path14.sep).filter(Boolean)) {
5231
+ current = (0, import_node_path14.join)(current, part);
5188
5232
  try {
5189
- if ((0, import_node_fs14.lstatSync)(current).isSymbolicLink()) return current;
5233
+ if ((0, import_node_fs15.lstatSync)(current).isSymbolicLink()) return current;
5190
5234
  } catch (error) {
5191
5235
  if (error.code !== "ENOENT") throw error;
5192
5236
  }
@@ -5197,13 +5241,13 @@ function skillNames(files) {
5197
5241
  return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
5198
5242
  }
5199
5243
  function listFiles(dir) {
5200
- if (!(0, import_node_fs14.existsSync)(dir)) return [];
5244
+ if (!(0, import_node_fs15.existsSync)(dir)) return [];
5201
5245
  const results = [];
5202
5246
  const walk = (current) => {
5203
- for (const entry of (0, import_node_fs14.readdirSync)(current, { withFileTypes: true })) {
5204
- const path = (0, import_node_path13.join)(current, entry.name);
5247
+ for (const entry of (0, import_node_fs15.readdirSync)(current, { withFileTypes: true })) {
5248
+ const path = (0, import_node_path14.join)(current, entry.name);
5205
5249
  if (entry.isDirectory()) walk(path);
5206
- else results.push((0, import_node_path13.relative)(dir, path));
5250
+ else results.push((0, import_node_path14.relative)(dir, path));
5207
5251
  }
5208
5252
  };
5209
5253
  walk(dir);
@@ -5558,9 +5602,9 @@ async function projectCommand(command, parsed, deps) {
5558
5602
  }
5559
5603
 
5560
5604
  // src/code-connect.ts
5561
- var import_node_fs15 = require("fs");
5562
- var import_node_os3 = require("os");
5563
- var import_node_path14 = require("path");
5605
+ var import_node_fs16 = require("fs");
5606
+ var import_node_os4 = require("os");
5607
+ var import_node_path15 = require("path");
5564
5608
 
5565
5609
  // ../harness/dist/chunk-3QP4VDQS.js
5566
5610
  var HARNESS_PROTOCOL_VERSION = 1;
@@ -6669,8 +6713,8 @@ function rollup(graph, kind, options = {}) {
6669
6713
  for (const node of nodesOfKind(graph, kind)) {
6670
6714
  if (options.prefix && !node.name.startsWith(options.prefix)) continue;
6671
6715
  const key = node.name.split(separator).slice(0, depth).join(separator);
6672
- const list2 = groups.get(key);
6673
- if (list2) list2.push(node);
6716
+ const list3 = groups.get(key);
6717
+ if (list3) list3.push(node);
6674
6718
  else groups.set(key, [node]);
6675
6719
  }
6676
6720
  return [...groups].map(([prefix, nodes]) => ({
@@ -6685,7 +6729,7 @@ function dirname8(path) {
6685
6729
  const at = path.lastIndexOf("/");
6686
6730
  return at <= 0 ? "." : path.slice(0, at);
6687
6731
  }
6688
- function join11(base, specifier) {
6732
+ function join12(base, specifier) {
6689
6733
  const parts = [];
6690
6734
  const segments = `${base === "." ? "" : `${base}/`}${specifier}`.split("/");
6691
6735
  for (const segment of segments) {
@@ -6709,7 +6753,7 @@ var BARE_IMPORT = /^\s*import\s*["']([^"']+)["']/gm;
6709
6753
  var isSourcePath = (path) => SOURCE.test(path);
6710
6754
  function resolveImport(fromPath, specifier, known) {
6711
6755
  if (!specifier.startsWith(".")) return null;
6712
- const base = join11(dirname8(fromPath), specifier);
6756
+ const base = join12(dirname8(fromPath), specifier);
6713
6757
  const candidates = [
6714
6758
  base,
6715
6759
  base.replace(/\.js$/, ".ts"),
@@ -9607,8 +9651,8 @@ var CODE_BUILD_RECIPES = Object.freeze([{
9607
9651
  // src/code-connect.ts
9608
9652
  async function codeConnect(options) {
9609
9653
  const cwd = options.cwd ?? process.cwd();
9610
- const configPath = (0, import_node_path14.resolve)(cwd, options.configPath);
9611
- const cfg = (0, import_node_fs15.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
9654
+ const configPath = (0, import_node_path15.resolve)(cwd, options.configPath);
9655
+ const cfg = (0, import_node_fs16.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
9612
9656
  const requestedAppId = options.appId?.trim();
9613
9657
  if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
9614
9658
  throw new Error("--app-id must be a valid odla app id");
@@ -9637,7 +9681,7 @@ async function codeConnect(options) {
9637
9681
  const doFetch = options.fetch ?? fetch;
9638
9682
  const engine = await (options.selectEngine ?? selectContainerEngine)(options.engine ?? "auto");
9639
9683
  const hostPlatform = process.platform === "darwin" ? "macos" : "linux";
9640
- const hostName = (options.name ?? (0, import_node_os3.hostname)()).trim();
9684
+ const hostName = (options.name ?? (0, import_node_os4.hostname)()).trim();
9641
9685
  if (!hostName || hostName.length > 120) throw new Error("--name must contain 1 to 120 characters");
9642
9686
  const repository = await inferGitHubRepository(cwd, options.readGitOrigin);
9643
9687
  const localSource = await (options.prepareLocalSource ?? prepareCodeLocalSource)(
@@ -9674,8 +9718,8 @@ async function codeConnect(options) {
9674
9718
  platform: hostPlatform,
9675
9719
  arch: process.arch,
9676
9720
  engines: [engine],
9677
- cpuCount: (0, import_node_os3.cpus)().length,
9678
- memoryBytes: (0, import_node_os3.totalmem)(),
9721
+ cpuCount: (0, import_node_os4.cpus)().length,
9722
+ memoryBytes: (0, import_node_os4.totalmem)(),
9679
9723
  source: descriptor2,
9680
9724
  images: {
9681
9725
  ready: true,
@@ -10042,13 +10086,13 @@ async function codeCommand(parsed, dependencies) {
10042
10086
  }
10043
10087
 
10044
10088
  // src/operator-credentials.ts
10045
- var import_node_process11 = __toESM(require("process"), 1);
10089
+ var import_node_process12 = __toESM(require("process"), 1);
10046
10090
  function developerTokenStatus(context, parsed, now = Date.now()) {
10047
10091
  const cached = readJsonFile(context.cfg.local.tokenFile);
10048
10092
  const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
10049
10093
  const source = clean3(
10050
10094
  stringOpt(parsed.options.token)
10051
- ) ? "flag" : clean3(import_node_process11.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
10095
+ ) ? "flag" : clean3(import_node_process12.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
10052
10096
  return {
10053
10097
  source,
10054
10098
  cacheFile: context.cfg.local.tokenFile,
@@ -10370,6 +10414,9 @@ Usage:
10370
10414
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
10371
10415
  odla-ai security run [target] --self --ack-redacted-source
10372
10416
  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]
10417
+ odla-ai device enroll [--app <id>[,<id>...]] [--name <label>] [--capability <c>[,<c>...]] [--email <odla-account>] [--no-open] [--json]
10418
+ odla-ai device list [--email <odla-account>] [--json]
10419
+ odla-ai device revoke <device-id> [--email <odla-account>] [--json]
10373
10420
  odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
10374
10421
  odla-ai credentials revoke <receipt-id> [--config odla.config.mjs] [--json]
10375
10422
  odla-ai smoke [--config odla.config.mjs] [--env dev] [--runtime] [--email <odla-account>] [--no-open]
@@ -10475,6 +10522,10 @@ Commands:
10475
10522
  stable status, incident, and report JSON to agents and CI.
10476
10523
  platform Read canonical fleet health, releases, provider load/freshness,
10477
10524
  explicit unknowns, and next actions through a read-only grant.
10525
+ device Enrol THIS machine once, then stop asking. A human approves the
10526
+ enrollment in the browser; from then on this terminal mints its
10527
+ own short-lived credentials for the named projects with nobody's
10528
+ attention, until the device expires or is revoked.
10478
10529
  provision Register services, compose integrations, persist credentials, optionally push secrets.
10479
10530
  "provision --live --yes" initializes only the live instance of
10480
10531
  an existing sandbox app and enables every configured service;
@@ -11725,8 +11776,8 @@ async function pmWatch(ctx, parsed) {
11725
11776
  }
11726
11777
 
11727
11778
  // src/pm-project-context.ts
11728
- var import_node_path15 = require("path");
11729
- var pmProjectContextFile = (rootDir) => (0, import_node_path15.resolve)(rootDir, ".odla", "pm-project.local.json");
11779
+ var import_node_path16 = require("path");
11780
+ var pmProjectContextFile = (rootDir) => (0, import_node_path16.resolve)(rootDir, ".odla", "pm-project.local.json");
11730
11781
  function readPmProjectContext(rootDir) {
11731
11782
  const value2 = readJsonFile(pmProjectContextFile(rootDir));
11732
11783
  return value2 && typeof value2.appId === "string" && typeof value2.projectId === "string" ? value2 : null;
@@ -12757,7 +12808,7 @@ function percent(value2) {
12757
12808
  // src/provision.ts
12758
12809
  var import_apps13 = require("@odla-ai/apps");
12759
12810
  var import_ai5 = require("@odla-ai/ai");
12760
- var import_node_process12 = __toESM(require("process"), 1);
12811
+ var import_node_process13 = __toESM(require("process"), 1);
12761
12812
 
12762
12813
  // src/integration-provision.ts
12763
12814
  var import_db3 = require("@odla-ai/db");
@@ -13193,7 +13244,7 @@ async function provision(options) {
13193
13244
  await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
13194
13245
  }
13195
13246
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
13196
- const key = import_node_process12.default.env[cfg.ai.keyEnv];
13247
+ const key = import_node_process13.default.env[cfg.ai.keyEnv];
13197
13248
  if (key) {
13198
13249
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
13199
13250
  await (0, import_ai5.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -13234,8 +13285,8 @@ async function provision(options) {
13234
13285
  }
13235
13286
 
13236
13287
  // src/record.ts
13237
- var import_node_fs16 = require("fs");
13238
- var import_node_process13 = __toESM(require("process"), 1);
13288
+ var import_node_fs17 = require("fs");
13289
+ var import_node_process14 = __toESM(require("process"), 1);
13239
13290
 
13240
13291
  // src/surface.ts
13241
13292
  var PM_ACTIONS = {
@@ -13305,6 +13356,7 @@ var COMMAND_SURFACE = {
13305
13356
  config: { diff: {}, plan: {}, apply: {} },
13306
13357
  context: { show: {}, list: {}, save: {}, remove: {} },
13307
13358
  credentials: { list: {}, revoke: {} },
13359
+ device: { enroll: {}, list: {}, revoke: {} },
13308
13360
  // `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
13309
13361
  discuss: {
13310
13362
  groups: {},
@@ -13416,7 +13468,7 @@ function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
13416
13468
 
13417
13469
  // src/record.ts
13418
13470
  function recordInvocation(parsed) {
13419
- const file = import_node_process13.default.env.ODLA_CLI_RECORD;
13471
+ const file = import_node_process14.default.env.ODLA_CLI_RECORD;
13420
13472
  if (!file) return;
13421
13473
  try {
13422
13474
  const entry = {
@@ -13424,7 +13476,7 @@ function recordInvocation(parsed) {
13424
13476
  options: Object.entries(parsed.options).map(([name, value2]) => value2 === false ? `no-${name}` : name).sort()
13425
13477
  };
13426
13478
  if (!entry.path.length) return;
13427
- (0, import_node_fs16.appendFileSync)(file, `${JSON.stringify(entry)}
13479
+ (0, import_node_fs17.appendFileSync)(file, `${JSON.stringify(entry)}
13428
13480
  `);
13429
13481
  } catch {
13430
13482
  }
@@ -13453,8 +13505,109 @@ function renderAdvisories(out, advisories, env = process.env) {
13453
13505
  }
13454
13506
  }
13455
13507
 
13508
+ // src/device-command.ts
13509
+ var import_node_fs18 = require("fs");
13510
+ var import_node_path17 = require("path");
13511
+ var import_node_process15 = __toESM(require("process"), 1);
13512
+ async function deviceCommand(parsed, deps) {
13513
+ const action2 = parsed.positionals[1] ?? "";
13514
+ const out = deps.stdout ?? console;
13515
+ const doFetch = deps.fetch ?? fetch;
13516
+ const cfg = await loadProjectConfig(stringOpt(parsed.options.config));
13517
+ const json = parsed.options.json === true;
13518
+ if (action2 === "enroll") return enroll(parsed, deps, cfg, doFetch, out, json);
13519
+ if (action2 === "list") return list2(parsed, deps, cfg, doFetch, out, json);
13520
+ if (action2 === "revoke") return revoke(parsed, deps, cfg, doFetch, out, json);
13521
+ throw new Error('odla-ai device expects "enroll", "list", or "revoke"');
13522
+ }
13523
+ async function enroll(parsed, deps, cfg, doFetch, out, json) {
13524
+ const name = stringOpt(parsed.options.name) ?? defaultDeviceName();
13525
+ const apps = (stringOpt(parsed.options.app) ?? cfg.app.id).split(",").map((id2) => id2.trim()).filter(Boolean);
13526
+ if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026]");
13527
+ const token = await scopedToken2(parsed, deps, cfg, doFetch, out, `odla CLI (enroll ${name})`);
13528
+ const response2 = await doFetch(`${cfg.platformUrl}/registry/devices`, {
13529
+ method: "POST",
13530
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
13531
+ body: JSON.stringify({
13532
+ name,
13533
+ platform: import_node_process15.default.platform,
13534
+ appIds: apps,
13535
+ ...parsed.options.capability ? { capabilities: String(parsed.options.capability).split(",").map((c) => c.trim()).filter(Boolean) } : {}
13536
+ })
13537
+ });
13538
+ const body = await response2.json().catch(() => ({}));
13539
+ if (!response2.ok || !body.token || !body.device) {
13540
+ throw new Error(`device enroll failed: ${body.error?.message ?? `registry returned ${response2.status}`} (${response2.status})`);
13541
+ }
13542
+ const path = deviceCredentialPath();
13543
+ (0, import_node_fs18.mkdirSync)((0, import_node_path17.dirname)(path), { recursive: true });
13544
+ (0, import_node_fs18.writeFileSync)(path, JSON.stringify({
13545
+ token: body.token,
13546
+ platform: cfg.platformUrl.replace(/\/$/, ""),
13547
+ deviceId: body.device.deviceId,
13548
+ name
13549
+ }, null, 2));
13550
+ (0, import_node_fs18.chmodSync)(path, 384);
13551
+ out.error(`device: enrolled "${name}" for ${body.device.appIds.join(", ")}; credential written to ${path}`);
13552
+ out.error("device: this terminal will mint its own credentials from now on \u2014 no further approvals.");
13553
+ if (json) {
13554
+ out.log(JSON.stringify({ deviceId: body.device.deviceId, name, appIds: body.device.appIds, expiresAt: body.device.expiresAt }, null, 2));
13555
+ }
13556
+ }
13557
+ async function list2(parsed, deps, cfg, doFetch, out, json) {
13558
+ const token = await scopedToken2(parsed, deps, cfg, doFetch, out, "odla CLI (device list)");
13559
+ const response2 = await doFetch(`${cfg.platformUrl}/registry/devices`, {
13560
+ headers: { authorization: `Bearer ${token}` }
13561
+ });
13562
+ const body = await response2.json().catch(() => ({}));
13563
+ if (!response2.ok || !body.devices) {
13564
+ throw new Error(`device list failed: ${body.error?.message ?? `registry returned ${response2.status}`} (${response2.status})`);
13565
+ }
13566
+ if (json) return out.log(JSON.stringify(body.devices, null, 2));
13567
+ if (body.devices.length === 0) return out.log("no enrolled devices");
13568
+ for (const device of body.devices) {
13569
+ const state2 = device.revokedAt ? "revoked" : device.expiresAt <= Date.now() ? "expired" : "active";
13570
+ out.log(`${device.deviceId} ${state2.padEnd(7)} ${device.name} [${device.appIds.join(", ")}]`);
13571
+ }
13572
+ }
13573
+ async function revoke(parsed, deps, cfg, doFetch, out, json) {
13574
+ const deviceId = parsed.positionals[2];
13575
+ if (!deviceId) throw new Error("device revoke needs the device id from `odla-ai device list`");
13576
+ const token = await scopedToken2(parsed, deps, cfg, doFetch, out, "odla CLI (device revoke)");
13577
+ const response2 = await doFetch(`${cfg.platformUrl}/registry/devices/${encodeURIComponent(deviceId)}/revoke`, {
13578
+ method: "POST",
13579
+ headers: { authorization: `Bearer ${token}` }
13580
+ });
13581
+ if (!response2.ok) {
13582
+ const body = await response2.json().catch(() => ({}));
13583
+ throw new Error(`device revoke failed: ${body.error?.message ?? `registry returned ${response2.status}`} (${response2.status})`);
13584
+ }
13585
+ out.error(`device: revoked ${deviceId}; every credential it minted is revoked with it`);
13586
+ if (json) out.log(JSON.stringify({ deviceId, revoked: true }, null, 2));
13587
+ }
13588
+ async function scopedToken2(parsed, deps, cfg, doFetch, out, label) {
13589
+ const { credentials } = await resolveOperatorContext(parsed, { allowMissingConfig: true });
13590
+ const scopedTokenFile = credentials.scopedTokenFile;
13591
+ return getScopedPlatformToken({
13592
+ platform: cfg.platformUrl,
13593
+ scope: "app:device:enroll",
13594
+ email: stringOpt(parsed.options.email),
13595
+ label,
13596
+ fetch: doFetch,
13597
+ stdout: out,
13598
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
13599
+ openApprovalUrl: deps.openUrl,
13600
+ rootDir: cfg.rootDir,
13601
+ tokenFile: scopedTokenFile,
13602
+ ...stringOpt(parsed.options.token) ? { token: stringOpt(parsed.options.token) } : {}
13603
+ });
13604
+ }
13605
+ function defaultDeviceName() {
13606
+ return `${import_node_process15.default.env.HOSTNAME ?? import_node_process15.default.env.HOST ?? "machine"}-${import_node_process15.default.platform}`;
13607
+ }
13608
+
13456
13609
  // src/runbook-actions.ts
13457
- var import_node_fs17 = require("fs");
13610
+ var import_node_fs19 = require("fs");
13458
13611
 
13459
13612
  // src/runbook-requires.ts
13460
13613
  var SPEC = /^(@?[\w./-]+?)@(\d+\.\d+\.\d+(?:[\w.-]*)?)$/;
@@ -13550,7 +13703,7 @@ async function bySlug(ctx, slug) {
13550
13703
  function readBody(file, inline) {
13551
13704
  if (inline !== void 0) return inline;
13552
13705
  if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
13553
- return (0, import_node_fs17.readFileSync)(file === "-" ? 0 : file, "utf8");
13706
+ return (0, import_node_fs19.readFileSync)(file === "-" ? 0 : file, "utf8");
13554
13707
  }
13555
13708
  var stamp = (ms) => ms ? new Date(ms).toISOString().slice(0, 16).replace("T", " ") : "";
13556
13709
  async function runbookList(ctx, all, query) {
@@ -13642,8 +13795,8 @@ async function runbookRemove(ctx, slug) {
13642
13795
  }
13643
13796
 
13644
13797
  // src/runbook-import.ts
13645
- var import_node_fs18 = require("fs");
13646
- var import_node_path16 = require("path");
13798
+ var import_node_fs20 = require("fs");
13799
+ var import_node_path18 = require("path");
13647
13800
  function parseRunbook(text3, slug) {
13648
13801
  let rest = text3;
13649
13802
  const meta = {};
@@ -13668,12 +13821,12 @@ function parseRunbook(text3, slug) {
13668
13821
  };
13669
13822
  }
13670
13823
  function readRunbookDir(dir) {
13671
- if (!(0, import_node_fs18.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
13672
- const files = (0, import_node_fs18.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
13824
+ if (!(0, import_node_fs20.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
13825
+ const files = (0, import_node_fs20.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
13673
13826
  if (!files.length) throw new Error(`no .md files in ${dir}`);
13674
13827
  return files.map((file) => {
13675
- const slug = (0, import_node_path16.basename)(file, ".md");
13676
- const parsed = parseRunbook((0, import_node_fs18.readFileSync)((0, import_node_path16.join)(dir, file), "utf8"), slug);
13828
+ const slug = (0, import_node_path18.basename)(file, ".md");
13829
+ const parsed = parseRunbook((0, import_node_fs20.readFileSync)((0, import_node_path18.join)(dir, file), "utf8"), slug);
13677
13830
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
13678
13831
  });
13679
13832
  }
@@ -13746,8 +13899,8 @@ async function upsert(ctx, r, visibility) {
13746
13899
 
13747
13900
  // src/runbook-impact.ts
13748
13901
  var import_node_child_process6 = require("child_process");
13749
- var import_node_fs19 = require("fs");
13750
- var import_node_path17 = require("path");
13902
+ var import_node_fs21 = require("fs");
13903
+ var import_node_path19 = require("path");
13751
13904
 
13752
13905
  // src/runbook-impact-scan.ts
13753
13906
  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$]*)/;
@@ -13916,10 +14069,10 @@ ${body.split("\n").map((line2) => `+${line2}`).join("\n")}
13916
14069
  }
13917
14070
  function manifestLabeller(root) {
13918
14071
  return (workspace) => {
13919
- const manifest = (0, import_node_path17.join)(root, workspace, "package.json");
13920
- if (!(0, import_node_fs19.existsSync)(manifest)) return void 0;
14072
+ const manifest = (0, import_node_path19.join)(root, workspace, "package.json");
14073
+ if (!(0, import_node_fs21.existsSync)(manifest)) return void 0;
13921
14074
  try {
13922
- const name = JSON.parse((0, import_node_fs19.readFileSync)(manifest, "utf8")).name;
14075
+ const name = JSON.parse((0, import_node_fs21.readFileSync)(manifest, "utf8")).name;
13923
14076
  return typeof name === "string" ? name : void 0;
13924
14077
  } catch {
13925
14078
  return void 0;
@@ -13986,7 +14139,7 @@ function report4(ctx, impacts) {
13986
14139
  async function runbookImpact(ctx, options, deps = {}) {
13987
14140
  const cwd = deps.cwd ?? process.cwd();
13988
14141
  const runGit = deps.runGit ?? gitRunner(cwd);
13989
- const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs19.readFileSync)((0, import_node_path17.join)(cwd, path), "utf8"));
14142
+ const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs21.readFileSync)((0, import_node_path19.join)(cwd, path), "utf8"));
13990
14143
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
13991
14144
  if (!surfaces.length) {
13992
14145
  return ctx.out.log(
@@ -14119,12 +14272,12 @@ async function runbookComment(ctx, slug, body) {
14119
14272
 
14120
14273
  // src/runbook-editor.ts
14121
14274
  var import_node_child_process7 = require("child_process");
14122
- var import_node_fs20 = require("fs");
14123
- var import_node_os4 = require("os");
14124
- var import_node_path18 = require("path");
14125
- var import_node_process14 = __toESM(require("process"), 1);
14275
+ var import_node_fs22 = require("fs");
14276
+ var import_node_os5 = require("os");
14277
+ var import_node_path20 = require("path");
14278
+ var import_node_process16 = __toESM(require("process"), 1);
14126
14279
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
14127
- function resolveEditor(env = import_node_process14.default.env) {
14280
+ function resolveEditor(env = import_node_process16.default.env) {
14128
14281
  for (const name of EDITOR_ENV) {
14129
14282
  const value2 = env[name];
14130
14283
  if (value2 && value2.trim()) return value2.trim();
@@ -14138,8 +14291,8 @@ function defaultRun(command, path) {
14138
14291
  return result.status ?? 0;
14139
14292
  }
14140
14293
  function editText(initial, slug, deps = {}) {
14141
- const env = deps.env ?? import_node_process14.default.env;
14142
- const interactive = deps.interactive ?? (() => Boolean(import_node_process14.default.stdin.isTTY));
14294
+ const env = deps.env ?? import_node_process16.default.env;
14295
+ const interactive = deps.interactive ?? (() => Boolean(import_node_process16.default.stdin.isTTY));
14143
14296
  const editor = resolveEditor(env);
14144
14297
  if (!editor)
14145
14298
  throw new Error(
@@ -14147,16 +14300,16 @@ function editText(initial, slug, deps = {}) {
14147
14300
  );
14148
14301
  if (!interactive())
14149
14302
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
14150
- const dir = (0, import_node_fs20.mkdtempSync)((0, import_node_path18.join)((0, import_node_os4.tmpdir)(), "odla-runbook-"));
14151
- const file = (0, import_node_path18.join)(dir, `${slug}.md`);
14303
+ const dir = (0, import_node_fs22.mkdtempSync)((0, import_node_path20.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
14304
+ const file = (0, import_node_path20.join)(dir, `${slug}.md`);
14152
14305
  try {
14153
- (0, import_node_fs20.writeFileSync)(file, initial, { mode: 384 });
14306
+ (0, import_node_fs22.writeFileSync)(file, initial, { mode: 384 });
14154
14307
  const code = defaultRunOrInjected(deps)(editor, file);
14155
14308
  if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
14156
- const edited = (0, import_node_fs20.readFileSync)(file, "utf8");
14309
+ const edited = (0, import_node_fs22.readFileSync)(file, "utf8");
14157
14310
  return edited === initial ? null : edited;
14158
14311
  } finally {
14159
- (0, import_node_fs20.rmSync)(dir, { recursive: true, force: true });
14312
+ (0, import_node_fs22.rmSync)(dir, { recursive: true, force: true });
14160
14313
  }
14161
14314
  }
14162
14315
  var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
@@ -14500,7 +14653,7 @@ function hostedSeverity(value2, flag) {
14500
14653
  var import_security2 = require("@odla-ai/security");
14501
14654
 
14502
14655
  // src/security.ts
14503
- var import_node_path19 = require("path");
14656
+ var import_node_path21 = require("path");
14504
14657
  var import_security = require("@odla-ai/security");
14505
14658
  var import_node3 = require("@odla-ai/security/node");
14506
14659
  async function runHostedSecurity(options) {
@@ -14512,9 +14665,9 @@ async function runHostedSecurity(options) {
14512
14665
  const appId = selfAudit ? "odla-ai" : cfg.app.id;
14513
14666
  const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
14514
14667
  const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
14515
- const target = (0, import_node_path19.resolve)(options.target ?? cfg?.rootDir ?? ".");
14516
- const output = (0, import_node_path19.resolve)(options.out ?? (0, import_node_path19.resolve)(target, ".odla/security/hosted"));
14517
- const outputRelative = (0, import_node_path19.relative)(target, output).split(import_node_path19.sep).join("/");
14668
+ const target = (0, import_node_path21.resolve)(options.target ?? cfg?.rootDir ?? ".");
14669
+ const output = (0, import_node_path21.resolve)(options.out ?? (0, import_node_path21.resolve)(target, ".odla/security/hosted"));
14670
+ const outputRelative = (0, import_node_path21.relative)(target, output).split(import_node_path21.sep).join("/");
14518
14671
  if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
14519
14672
  const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
14520
14673
  const tokenRequest = {
@@ -14526,7 +14679,7 @@ async function runHostedSecurity(options) {
14526
14679
  };
14527
14680
  const token = await injectedToken(options, tokenRequest);
14528
14681
  const snapshot = await (0, import_node3.snapshotDirectory)(target, {
14529
- exclude: !outputRelative.startsWith("../") && !(0, import_node_path19.isAbsolute)(outputRelative) ? [outputRelative] : []
14682
+ exclude: !outputRelative.startsWith("../") && !(0, import_node_path21.isAbsolute)(outputRelative) ? [outputRelative] : []
14530
14683
  });
14531
14684
  const hosted = await (0, import_security.createPlatformSecurityReasoners)({
14532
14685
  platform,
@@ -14544,7 +14697,7 @@ async function runHostedSecurity(options) {
14544
14697
  });
14545
14698
  const harness = (0, import_security.createSecurityHarness)({
14546
14699
  profile,
14547
- store: new import_node3.FileRunStore((0, import_node_path19.resolve)(output, "state")),
14700
+ store: new import_node3.FileRunStore((0, import_node_path21.resolve)(output, "state")),
14548
14701
  discoveryReasoner: hosted.discoveryReasoner,
14549
14702
  validationReasoner: hosted.validationReasoner,
14550
14703
  policy: {
@@ -14568,7 +14721,7 @@ async function runHostedSecurity(options) {
14568
14721
  function selectEnv(requested, declared, configPath, rootDir) {
14569
14722
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
14570
14723
  if (!env || !declared.includes(env)) {
14571
- const shown = (0, import_node_path19.relative)(rootDir, configPath) || configPath;
14724
+ const shown = (0, import_node_path21.relative)(rootDir, configPath) || configPath;
14572
14725
  throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
14573
14726
  }
14574
14727
  return env;
@@ -14597,7 +14750,7 @@ function printSummary(out, appId, env, run, report5, output) {
14597
14750
  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}`);
14598
14751
  if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
14599
14752
  out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
14600
- out.log(` report: ${(0, import_node_path19.resolve)(output, "REPORT.md")}`);
14753
+ out.log(` report: ${(0, import_node_path21.resolve)(output, "REPORT.md")}`);
14601
14754
  }
14602
14755
  function formatBudget(usage) {
14603
14756
  return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
@@ -15094,6 +15247,10 @@ async function dispatchCli(argv, dependencies) {
15094
15247
  await contextCommand(parsed, runtime);
15095
15248
  return;
15096
15249
  }
15250
+ if (command === "device") {
15251
+ await deviceCommand(parsed, runtime);
15252
+ return;
15253
+ }
15097
15254
  if (command === "credentials") {
15098
15255
  await credentialCommand(parsed, runtime);
15099
15256
  return;