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