@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.
@@ -10,12 +10,12 @@ import {
10
10
  } from "./chunk-UKLSRQ5J.js";
11
11
 
12
12
  // src/admin-ai.ts
13
- import process8 from "process";
13
+ import process9 from "process";
14
14
 
15
15
  // src/token.ts
16
16
  import { OdlaError, requestToken } from "@odla-ai/db";
17
17
  import { createHash } from "crypto";
18
- import process5 from "process";
18
+ import process6 from "process";
19
19
 
20
20
  // src/handshake-approval.ts
21
21
  import process3 from "process";
@@ -172,13 +172,49 @@ function explainRejectedCredential(error) {
172
172
  ].join("\n");
173
173
  }
174
174
 
175
+ // src/device-session.ts
176
+ import { existsSync, readFileSync } from "fs";
177
+ import { homedir } from "os";
178
+ import { join as join2 } from "path";
179
+ import process5 from "process";
180
+ function deviceCredentialPath(env = process5.env) {
181
+ return env.ODLA_DEVICE_CREDENTIAL ?? join2(env.HOME ?? homedir(), ".odla", "device.json");
182
+ }
183
+ function readDeviceCredential(platform, env = process5.env) {
184
+ const path = deviceCredentialPath(env);
185
+ if (!existsSync(path)) return null;
186
+ try {
187
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
188
+ if (typeof parsed.token !== "string" || !parsed.token.startsWith("odla_device_")) return null;
189
+ if (parsed.platform !== platform) return null;
190
+ return { ...parsed, token: parsed.token, platform: parsed.platform };
191
+ } catch {
192
+ return null;
193
+ }
194
+ }
195
+ async function mintDeviceSession(platformUrl, credential2, doFetch) {
196
+ const response2 = await doFetch(`${platformUrl.replace(/\/$/, "")}/registry/devices/session`, {
197
+ method: "POST",
198
+ headers: { authorization: `Bearer ${credential2.token}`, "content-type": "application/json" },
199
+ body: "{}"
200
+ });
201
+ const body = await response2.json().catch(() => ({}));
202
+ if (!response2.ok || typeof body.token !== "string") {
203
+ const detail = body.error?.message ?? `registry returned ${response2.status}`;
204
+ throw new Error(
205
+ `device session failed: ${detail} (${response2.status}) \u2014 if this machine's enrollment was revoked or has expired, enroll it again in Studio`
206
+ );
207
+ }
208
+ return { token: body.token, expiresAt: body.expiresAt ?? Date.now() };
209
+ }
210
+
175
211
  // src/local.ts
176
- import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
212
+ import { chmodSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
177
213
  import { dirname as dirname2, isAbsolute, relative, resolve } from "path";
178
214
  var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
179
215
  function readJsonFile(path) {
180
216
  try {
181
- return JSON.parse(readFileSync(path, "utf8"));
217
+ return JSON.parse(readFileSync2(path, "utf8"));
182
218
  } catch {
183
219
  return null;
184
220
  }
@@ -188,10 +224,10 @@ function writePrivateJson(path, value2) {
188
224
  `);
189
225
  }
190
226
  function readCredentials(path) {
191
- if (!existsSync(path)) return null;
227
+ if (!existsSync2(path)) return null;
192
228
  let value2;
193
229
  try {
194
- value2 = JSON.parse(readFileSync(path, "utf8"));
230
+ value2 = JSON.parse(readFileSync2(path, "utf8"));
195
231
  } catch {
196
232
  throw new Error(`credentials file ${path} is not valid JSON; fix or remove it before provisioning`);
197
233
  }
@@ -222,7 +258,7 @@ function mergeCredential(current, update) {
222
258
  }
223
259
  function ensureGitignore(rootDir, localPaths = []) {
224
260
  const path = resolve(rootDir, ".gitignore");
225
- const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
261
+ const existing = existsSync2(path) ? readFileSync2(path, "utf8") : "";
226
262
  const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line2) => !!line2);
227
263
  const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
228
264
  const missing = wanted.filter((line2) => !existing.split(/\r?\n/).includes(line2));
@@ -256,7 +292,7 @@ function writeDevVars(path, credentials, env, o11y) {
256
292
  if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
257
293
  if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
258
294
  }
259
- const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
295
+ const existing = existsSync2(path) ? readFileSync2(path, "utf8") : "";
260
296
  const retained = existing.split(/\r?\n/).filter((line2) => !isManagedDevVar(line2));
261
297
  while (retained.at(-1) === "") retained.pop();
262
298
  const prefix = retained.length ? `${retained.join("\n")}
@@ -306,14 +342,20 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
306
342
  const cached = readJsonFile(cfg.local.tokenFile);
307
343
  if (!grantRequest.forceReview && !grantRequest.freshLogin) {
308
344
  if (options.token) return options.token;
309
- if (process5.env.ODLA_DEV_TOKEN) {
310
- const declared = process5.env.ODLA_DEV_TOKEN_AUDIENCE;
345
+ if (process6.env.ODLA_DEV_TOKEN) {
346
+ const declared = process6.env.ODLA_DEV_TOKEN_AUDIENCE;
311
347
  if (declared) {
312
348
  if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
313
349
  } else if (audience !== "https://odla.ai") {
314
350
  throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
315
351
  }
316
- return process5.env.ODLA_DEV_TOKEN;
352
+ return process6.env.ODLA_DEV_TOKEN;
353
+ }
354
+ const device = readDeviceCredential(audience);
355
+ if (device) {
356
+ const session = await mintDeviceSession(cfg.platformUrl, device, doFetch);
357
+ out.error(`auth: session minted by this enrolled device (${displayPath(deviceCredentialPath(), cfg.rootDir)})`);
358
+ return session.token;
317
359
  }
318
360
  if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4 && cachedGrantCovers(cached, grantIntent)) {
319
361
  out.error(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
@@ -418,7 +460,7 @@ function stillPending(pending, email) {
418
460
  );
419
461
  }
420
462
  function handshakeEmail(value2, cached) {
421
- const email = (value2 ?? process5.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
463
+ const email = (value2 ?? process6.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
422
464
  if (/@users\.noreply\.github\.com$/i.test(email)) {
423
465
  throw new Error(
424
466
  `"${email}" is a GitHub commit identity, not an odla account email; use --email <signed-in-odla-account> or ODLA_USER_EMAIL`
@@ -449,12 +491,12 @@ function platformAudience(value2) {
449
491
  }
450
492
 
451
493
  // src/secret-input.ts
452
- import process6 from "process";
494
+ import process7 from "process";
453
495
  var MAX_BYTES = 64 * 1024;
454
496
  async function secretInputValue(options, kind = "credential") {
455
497
  if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
456
498
  let value2;
457
- if (options.fromEnv) value2 = process6.env[options.fromEnv];
499
+ if (options.fromEnv) value2 = process7.env[options.fromEnv];
458
500
  else if (options.stdin) value2 = await (options.readStdin ?? (() => readSecretStream(kind)))();
459
501
  else throw new Error(`${kind} input required: use --from-env <NAME> or --stdin; values are never accepted as arguments`);
460
502
  value2 = value2?.replace(/[\r\n]+$/, "");
@@ -462,7 +504,7 @@ async function secretInputValue(options, kind = "credential") {
462
504
  if (new TextEncoder().encode(value2).byteLength > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
463
505
  return value2;
464
506
  }
465
- async function readSecretStream(kind, stream = process6.stdin) {
507
+ async function readSecretStream(kind, stream = process7.stdin) {
466
508
  let value2 = "";
467
509
  for await (const chunk of stream) {
468
510
  value2 += String(chunk);
@@ -472,9 +514,9 @@ async function readSecretStream(kind, stream = process6.stdin) {
472
514
  }
473
515
 
474
516
  // src/admin-ai-auth.ts
475
- import { existsSync as existsSync2 } from "fs";
476
- import { join as join2 } from "path";
477
- import process7 from "process";
517
+ import { existsSync as existsSync3 } from "fs";
518
+ import { join as join3 } from "path";
519
+ import process8 from "process";
478
520
  import { requestToken as requestToken2 } from "@odla-ai/db";
479
521
  async function getScopedPlatformToken(options) {
480
522
  return resolveAdminPlatformToken(options);
@@ -482,7 +524,7 @@ async function getScopedPlatformToken(options) {
482
524
  async function resolveAdminPlatformToken(options) {
483
525
  const audience = platformAudience(options.platform);
484
526
  if (options.token) return options.token;
485
- const fromEnv = process7.env.ODLA_ADMIN_TOKEN;
527
+ const fromEnv = process8.env.ODLA_ADMIN_TOKEN;
486
528
  if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
487
529
  return scopedToken(
488
530
  audience,
@@ -494,7 +536,7 @@ async function resolveAdminPlatformToken(options) {
494
536
  }
495
537
  function audienceBoundEnvToken(token, platform) {
496
538
  const audience = platformAudience(platform);
497
- const declared = process7.env.ODLA_ADMIN_TOKEN_AUDIENCE;
539
+ const declared = process8.env.ODLA_ADMIN_TOKEN_AUDIENCE;
498
540
  if (declared) {
499
541
  if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
500
542
  } else if (audience !== "https://odla.ai") {
@@ -507,6 +549,7 @@ var SCOPE_PURPOSE = {
507
549
  "app:config:read": "compare checked-in intent with an exact-id app Registry configuration",
508
550
  "app:config:write": "apply or inspect one revision-bound configuration operation for an app you own",
509
551
  "platform:runbook:write": "read and edit all of odla's operational runbooks, including admin-visible content",
552
+ "app:device:enroll": "enrol this machine so it can mint its own short-lived credentials without asking you again",
510
553
  "platform:ai:policy:write": "change System AI model routing",
511
554
  "platform:ai:policy:read": "read System AI model routing",
512
555
  "platform:ai:credential:write": "replace a stored AI provider key",
@@ -519,8 +562,8 @@ var SCOPE_PURPOSE = {
519
562
  };
520
563
  async function scopedToken(platform, scope, options, doFetch, out) {
521
564
  const audience = platformAudience(platform);
522
- const rootDir = options.rootDir ?? process7.cwd();
523
- const tokenFile = options.tokenFile ?? join2(rootDir, ".odla/admin-token.local.json");
565
+ const rootDir = options.rootDir ?? process8.cwd();
566
+ const tokenFile = options.tokenFile ?? join3(rootDir, ".odla/admin-token.local.json");
524
567
  const cache2 = options.cache === false ? null : readJsonFile(tokenFile);
525
568
  const cached = cache2?.platform === audience ? cache2.tokens?.[scope] : void 0;
526
569
  if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
@@ -547,7 +590,7 @@ async function scopedToken(platform, scope, options, doFetch, out) {
547
590
  if (options.cache !== false) {
548
591
  const tokens = cache2?.platform === audience ? { ...cache2.tokens ?? {} } : {};
549
592
  tokens[scope] = { token, expiresAt };
550
- if (existsSync2(join2(rootDir, ".git"))) ensureGitignore(rootDir, [tokenFile]);
593
+ if (existsSync3(join3(rootDir, ".git"))) ensureGitignore(rootDir, [tokenFile]);
551
594
  writePrivateJson(tokenFile, { platform: audience, email, tokens });
552
595
  out.error(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
553
596
  } else {
@@ -724,7 +767,7 @@ function isRecord2(value2) {
724
767
 
725
768
  // src/admin-ai.ts
726
769
  async function adminAi(options) {
727
- const platform = platformAudience(options.platform ?? process8.env.ODLA_PLATFORM ?? "https://odla.ai");
770
+ const platform = platformAudience(options.platform ?? process9.env.ODLA_PLATFORM ?? "https://odla.ai");
728
771
  const doFetch = options.fetch ?? fetch;
729
772
  const out = options.stdout ?? console;
730
773
  const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
@@ -979,12 +1022,12 @@ function addOption(options, name, value2) {
979
1022
  }
980
1023
 
981
1024
  // src/operator-context.ts
982
- import { existsSync as existsSync5 } from "fs";
983
- import { join as join4, resolve as resolve4 } from "path";
984
- import process10 from "process";
1025
+ import { existsSync as existsSync6 } from "fs";
1026
+ import { join as join5, resolve as resolve4 } from "path";
1027
+ import process11 from "process";
985
1028
 
986
1029
  // src/config.ts
987
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1030
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
988
1031
  import { dirname as dirname3, isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
989
1032
  import { pathToFileURL } from "url";
990
1033
  import { appServiceDefinition, appServiceIds } from "@odla-ai/apps";
@@ -1391,7 +1434,7 @@ var configImportSerial = 0;
1391
1434
  var GOOGLE_CALENDAR_EVENTS_SCOPE = "https://www.googleapis.com/auth/calendar.events";
1392
1435
  async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1393
1436
  const resolved = resolve2(configPath);
1394
- if (!existsSync3(resolved)) {
1437
+ if (!existsSync4(resolved)) {
1395
1438
  throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
1396
1439
  }
1397
1440
  const raw = await loadConfigModule(resolved);
@@ -1426,7 +1469,7 @@ async function resolveDataExport(cfg, value2, names) {
1426
1469
  if (typeof value2 !== "string") return value2;
1427
1470
  const target = isAbsolute2(value2) ? value2 : resolve2(cfg.rootDir, value2);
1428
1471
  if (target.endsWith(".json")) {
1429
- return JSON.parse(readFileSync2(target, "utf8"));
1472
+ return JSON.parse(readFileSync3(target, "utf8"));
1430
1473
  }
1431
1474
  const mod = await import(pathToFileURL(target).href);
1432
1475
  for (const name of names) {
@@ -1502,7 +1545,7 @@ function validId2(value2) {
1502
1545
  return typeof value2 === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value2);
1503
1546
  }
1504
1547
  async function loadConfigModule(path) {
1505
- if (path.endsWith(".json")) return JSON.parse(readFileSync2(path, "utf8"));
1548
+ if (path.endsWith(".json")) return JSON.parse(readFileSync3(path, "utf8"));
1506
1549
  const nonce = `${Date.now()}-${configImportSerial++}`;
1507
1550
  const mod = await import(`${pathToFileURL(path).href}?reload=${nonce}`);
1508
1551
  const value2 = mod.default ?? mod.config;
@@ -1517,18 +1560,18 @@ function unique3(values) {
1517
1560
  }
1518
1561
 
1519
1562
  // src/operator-profiles.ts
1520
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1521
- import { homedir } from "os";
1522
- import { dirname as dirname4, join as join3, resolve as resolve3 } from "path";
1523
- import process9 from "process";
1563
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1564
+ import { homedir as homedir2 } from "os";
1565
+ import { dirname as dirname4, join as join4, resolve as resolve3 } from "path";
1566
+ import process10 from "process";
1524
1567
  function operatorProfileFile() {
1525
1568
  return resolve3(
1526
- clean(process9.env.ODLA_CONTEXT_FILE) ?? join3(homedir(), ".odla", "contexts.json")
1569
+ clean(process10.env.ODLA_CONTEXT_FILE) ?? join4(homedir2(), ".odla", "contexts.json")
1527
1570
  );
1528
1571
  }
1529
1572
  function resolveOperatorProfile(parsed) {
1530
1573
  const fromFlag = clean(stringOpt(parsed.options.context));
1531
- const fromEnvironment = clean(process9.env.ODLA_CONTEXT);
1574
+ const fromEnvironment = clean(process10.env.ODLA_CONTEXT);
1532
1575
  const name = fromFlag ?? fromEnvironment ?? null;
1533
1576
  const file = operatorProfileFile();
1534
1577
  if (!name) {
@@ -1568,10 +1611,10 @@ function removeOperatorProfile(name, file = operatorProfileFile()) {
1568
1611
  return true;
1569
1612
  }
1570
1613
  function operatorCredentialFiles(selection) {
1571
- const base = selection.name ? join3(dirname4(selection.file), "profiles", selection.name) : join3(homedir(), ".odla");
1614
+ const base = selection.name ? join4(dirname4(selection.file), "profiles", selection.name) : join4(homedir2(), ".odla");
1572
1615
  return {
1573
- developer: join3(base, "dev-token.json"),
1574
- scoped: join3(base, "admin-token.local.json")
1616
+ developer: join4(base, "dev-token.json"),
1617
+ scoped: join4(base, "admin-token.local.json")
1575
1618
  };
1576
1619
  }
1577
1620
  function assertOperatorName(value2, label) {
@@ -1582,10 +1625,10 @@ function assertOperatorName(value2, label) {
1582
1625
  }
1583
1626
  }
1584
1627
  function readOperatorProfiles(file) {
1585
- if (!existsSync4(file)) return emptyProfiles();
1628
+ if (!existsSync5(file)) return emptyProfiles();
1586
1629
  let raw;
1587
1630
  try {
1588
- raw = JSON.parse(readFileSync3(file, "utf8"));
1631
+ raw = JSON.parse(readFileSync4(file, "utf8"));
1589
1632
  } catch {
1590
1633
  throw new Error(`operator context file ${file} is not valid JSON`);
1591
1634
  }
@@ -1651,19 +1694,19 @@ async function resolveOperatorContext(parsed, options = {}) {
1651
1694
  const configArgument = stringOpt(parsed.options.config) ?? "odla.config.mjs";
1652
1695
  const configPath = resolve4(configArgument);
1653
1696
  const explicitConfig = parsed.options.config !== void 0;
1654
- const hasConfig = existsSync5(configPath);
1697
+ const hasConfig = existsSync6(configPath);
1655
1698
  if (!hasConfig && (!options.allowMissingConfig || explicitConfig)) {
1656
1699
  await loadProjectConfig(configArgument);
1657
1700
  }
1658
1701
  const loaded = hasConfig ? await loadProjectConfig(configArgument) : void 0;
1659
1702
  const platformFlag = clean2(stringOpt(parsed.options.platform));
1660
- const platformEnvironment = clean2(process10.env.ODLA_PLATFORM_URL);
1703
+ const platformEnvironment = clean2(process11.env.ODLA_PLATFORM_URL);
1661
1704
  const platformValue = platformAudience(
1662
1705
  platformFlag ?? platformEnvironment ?? profile.value?.platform ?? loaded?.platformUrl ?? DEFAULT_PLATFORM2
1663
1706
  );
1664
1707
  const platformSource = platformFlag ? "flag" : platformEnvironment ? "environment" : profile.value ? "profile" : loaded ? "config" : "default";
1665
1708
  const appFlag = clean2(stringOpt(parsed.options.app));
1666
- const appEnvironment = clean2(process10.env.ODLA_APP_ID);
1709
+ const appEnvironment = clean2(process11.env.ODLA_APP_ID);
1667
1710
  const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
1668
1711
  const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
1669
1712
  if (appValue) assertOperatorName(appValue, "app");
@@ -1673,16 +1716,16 @@ async function resolveOperatorContext(parsed, options = {}) {
1673
1716
  );
1674
1717
  }
1675
1718
  const envFlag = clean2(stringOpt(parsed.options.env));
1676
- const envEnvironment = clean2(process10.env.ODLA_ENV);
1719
+ const envEnvironment = clean2(process11.env.ODLA_ENV);
1677
1720
  const environmentValue = envFlag ?? envEnvironment ?? profile.value?.environment ?? options.defaultEnvironment ?? null;
1678
1721
  const environmentSource = envFlag ? "flag" : envEnvironment ? "environment" : profile.value?.environment ? "profile" : options.defaultEnvironment ? "default" : "unresolved";
1679
1722
  if (environmentValue) {
1680
1723
  assertOperatorName(environmentValue, "environment");
1681
1724
  }
1682
- const rootDir = loaded?.rootDir ?? process10.cwd();
1725
+ const rootDir = loaded?.rootDir ?? process11.cwd();
1683
1726
  const profileCredentials = operatorCredentialFiles(profile);
1684
- const tokenFile = clean2(process10.env.ODLA_DEV_TOKEN_FILE) ? resolve4(process10.env.ODLA_DEV_TOKEN_FILE) : profile.name ? profileCredentials.developer : loaded?.local.tokenFile ?? profileCredentials.developer;
1685
- const scopedTokenFile = clean2(process10.env.ODLA_ADMIN_TOKEN_FILE) ? resolve4(process10.env.ODLA_ADMIN_TOKEN_FILE) : profile.name ? profileCredentials.scoped : loaded ? join4(loaded.rootDir, ".odla", "admin-token.local.json") : profileCredentials.scoped;
1727
+ const tokenFile = clean2(process11.env.ODLA_DEV_TOKEN_FILE) ? resolve4(process11.env.ODLA_DEV_TOKEN_FILE) : profile.name ? profileCredentials.developer : loaded?.local.tokenFile ?? profileCredentials.developer;
1728
+ const scopedTokenFile = clean2(process11.env.ODLA_ADMIN_TOKEN_FILE) ? resolve4(process11.env.ODLA_ADMIN_TOKEN_FILE) : profile.name ? profileCredentials.scoped : loaded ? join5(loaded.rootDir, ".odla", "admin-token.local.json") : profileCredentials.scoped;
1686
1729
  const cfg = loaded ? {
1687
1730
  ...loaded,
1688
1731
  platformUrl: platformValue,
@@ -1704,8 +1747,8 @@ async function resolveOperatorContext(parsed, options = {}) {
1704
1747
  services: [],
1705
1748
  local: {
1706
1749
  tokenFile,
1707
- credentialsFile: join4(rootDir, ".odla", "credentials.local.json"),
1708
- devVarsFile: join4(rootDir, ".dev.vars"),
1750
+ credentialsFile: join5(rootDir, ".odla", "credentials.local.json"),
1751
+ devVarsFile: join5(rootDir, ".dev.vars"),
1709
1752
  gitignore: true
1710
1753
  }
1711
1754
  };
@@ -1802,7 +1845,7 @@ async function adminCommand(parsed, deps = {}) {
1802
1845
  }
1803
1846
 
1804
1847
  // src/auth-command.ts
1805
- import process11 from "process";
1848
+ import process12 from "process";
1806
1849
 
1807
1850
  // src/whoami-command.ts
1808
1851
  var text2 = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
@@ -1964,7 +2007,7 @@ async function authCommand(parsed, deps = {}) {
1964
2007
  const { cfg } = context;
1965
2008
  const out = deps.stdout ?? console;
1966
2009
  const doFetch = deps.fetch ?? fetch;
1967
- const email = stringOpt(parsed.options.email) ?? process11.env.ODLA_USER_EMAIL?.trim();
2010
+ const email = stringOpt(parsed.options.email) ?? process12.env.ODLA_USER_EMAIL?.trim();
1968
2011
  if (!email) {
1969
2012
  throw new Error(
1970
2013
  "auth login requires --email <odla-account> or ODLA_USER_EMAIL; confirm the signed-in odla email instead of using git or GitHub identity"
@@ -2117,7 +2160,7 @@ async function appExport(options) {
2117
2160
  }
2118
2161
 
2119
2162
  // src/app-import.ts
2120
- import { readFileSync as readFileSync4 } from "fs";
2163
+ import { readFileSync as readFileSync5 } from "fs";
2121
2164
  import {
2122
2165
  buildImportOps,
2123
2166
  parseImport,
@@ -2139,7 +2182,7 @@ async function appImport(options) {
2139
2182
  const out = options.stdout ?? console;
2140
2183
  const say = options.json ? (line2) => out.error(line2) : (line2) => out.log(line2);
2141
2184
  const { tenant } = resolveTenant(cfg, options.env);
2142
- const text3 = options.file === "-" ? (options.readStdin ?? (() => readFileSync4(0, "utf8")))() : readFileSync4(options.file, "utf8");
2185
+ const text3 = options.file === "-" ? (options.readStdin ?? (() => readFileSync5(0, "utf8")))() : readFileSync5(options.file, "utf8");
2143
2186
  const { format, sources } = parseImport(text3, options.ns);
2144
2187
  if (format === "namespace-map" && options.ns) {
2145
2188
  throw new Error("--ns cannot be combined with a {namespace: rows} file \u2014 the file already names each namespace");
@@ -3010,7 +3053,7 @@ import {
3010
3053
  AppsError,
3011
3054
  createAppsClient
3012
3055
  } from "@odla-ai/apps";
3013
- import { join as join5 } from "path";
3056
+ import { join as join6 } from "path";
3014
3057
 
3015
3058
  // src/config-operation-error.ts
3016
3059
  var ConfigOperationCommandError = class extends Error {
@@ -3026,7 +3069,7 @@ var ConfigOperationCommandError = class extends Error {
3026
3069
  import {
3027
3070
  appServiceDefinition as appServiceDefinition2
3028
3071
  } from "@odla-ai/apps";
3029
- import { readFileSync as readFileSync5 } from "fs";
3072
+ import { readFileSync as readFileSync6 } from "fs";
3030
3073
 
3031
3074
  // src/config-reconcile-digest.ts
3032
3075
  import { createHash as createHash2 } from "crypto";
@@ -3062,7 +3105,7 @@ var SERVICE = /^[a-z][a-z0-9-]{0,39}$/;
3062
3105
  function readPlan(path) {
3063
3106
  let value2;
3064
3107
  try {
3065
- const raw = readFileSync5(path, "utf8");
3108
+ const raw = readFileSync6(path, "utf8");
3066
3109
  if (Buffer.byteLength(raw) > 128 * 1024) throw new Error("plan exceeds 128 KiB");
3067
3110
  value2 = JSON.parse(raw);
3068
3111
  } catch (error) {
@@ -3435,7 +3478,7 @@ async function operationClient(cfg, options, purpose) {
3435
3478
  platform: cfg.platformUrl,
3436
3479
  scope: "app:config:write",
3437
3480
  token: options.token,
3438
- tokenFile: join5(cfg.rootDir, ".odla", "admin-token.local.json"),
3481
+ tokenFile: join6(cfg.rootDir, ".odla", "admin-token.local.json"),
3439
3482
  rootDir: cfg.rootDir,
3440
3483
  email: options.email,
3441
3484
  open: options.open,
@@ -3490,7 +3533,7 @@ function record4(value2) {
3490
3533
 
3491
3534
  // src/config-reconcile-command.ts
3492
3535
  import { createAppsClient as createAppsClient2, studioAppSettingsPath } from "@odla-ai/apps";
3493
- import { join as join6 } from "path";
3536
+ import { join as join7 } from "path";
3494
3537
 
3495
3538
  // src/config-reconcile.ts
3496
3539
  import { appServiceIds as appServiceIds2, orderAppServices as orderAppServices2 } from "@odla-ai/apps";
@@ -3786,7 +3829,7 @@ async function inspectConfig(options) {
3786
3829
  platform: cfg.platformUrl,
3787
3830
  scope: "app:config:read",
3788
3831
  token: options.token,
3789
- tokenFile: join6(cfg.rootDir, ".odla", "admin-token.local.json"),
3832
+ tokenFile: join7(cfg.rootDir, ".odla", "admin-token.local.json"),
3790
3833
  rootDir: cfg.rootDir,
3791
3834
  email: options.email,
3792
3835
  open: options.open,
@@ -3918,13 +3961,13 @@ function quoteArg2(value2) {
3918
3961
 
3919
3962
  // src/doctor-checks.ts
3920
3963
  import { execFileSync } from "child_process";
3921
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
3922
- import { join as join8, resolve as resolve6 } from "path";
3964
+ import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
3965
+ import { join as join9, resolve as resolve6 } from "path";
3923
3966
 
3924
3967
  // src/wrangler.ts
3925
3968
  import { spawn as spawn2 } from "child_process";
3926
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
3927
- import { join as join7 } from "path";
3969
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
3970
+ import { join as join8 } from "path";
3928
3971
  var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
3929
3972
  const child = spawn2(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
3930
3973
  let stdout = "";
@@ -3938,15 +3981,15 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) =>
3938
3981
  var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
3939
3982
  function findWranglerConfig(rootDir) {
3940
3983
  for (const name of WRANGLER_CONFIG_FILES) {
3941
- const path = join7(rootDir, name);
3942
- if (existsSync6(path)) return path;
3984
+ const path = join8(rootDir, name);
3985
+ if (existsSync7(path)) return path;
3943
3986
  }
3944
3987
  return null;
3945
3988
  }
3946
3989
  function readWranglerConfig(path) {
3947
3990
  if (path.endsWith(".toml")) return null;
3948
3991
  try {
3949
- return JSON.parse(stripJsonComments(readFileSync6(path, "utf8")));
3992
+ return JSON.parse(stripJsonComments(readFileSync7(path, "utf8")));
3950
3993
  } catch {
3951
3994
  return null;
3952
3995
  }
@@ -4099,7 +4142,7 @@ function wranglerWarnings(rootDir) {
4099
4142
  const dir = resolve6(rootDir, assets.directory);
4100
4143
  if (dir === resolve6(rootDir)) {
4101
4144
  warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
4102
- } else if (existsSync7(join8(dir, "node_modules"))) {
4145
+ } else if (existsSync8(join9(dir, "node_modules"))) {
4103
4146
  warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
4104
4147
  }
4105
4148
  }
@@ -4135,12 +4178,12 @@ function o11yProjectWarnings(rootDir) {
4135
4178
  return warnings;
4136
4179
  }
4137
4180
  const main = typeof config.main === "string" ? resolve6(rootDir, config.main) : null;
4138
- if (!main || !existsSync7(main)) {
4181
+ if (!main || !existsSync8(main)) {
4139
4182
  warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
4140
4183
  } else {
4141
4184
  let source = "";
4142
4185
  try {
4143
- source = readFileSync7(main, "utf8");
4186
+ source = readFileSync8(main, "utf8");
4144
4187
  } catch {
4145
4188
  }
4146
4189
  if (!/\bwithObservability\b/.test(source)) {
@@ -4164,7 +4207,7 @@ function calendarProjectWarnings(rootDir) {
4164
4207
  }
4165
4208
  function readPackageJson(rootDir) {
4166
4209
  try {
4167
- return JSON.parse(readFileSync7(join8(rootDir, "package.json"), "utf8"));
4210
+ return JSON.parse(readFileSync8(join9(rootDir, "package.json"), "utf8"));
4168
4211
  } catch {
4169
4212
  return null;
4170
4213
  }
@@ -4458,14 +4501,14 @@ function harnessOption(value2, flag) {
4458
4501
  }
4459
4502
 
4460
4503
  // src/init.ts
4461
- import { existsSync as existsSync8, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
4504
+ import { existsSync as existsSync9, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
4462
4505
  import { dirname as dirname6, resolve as resolve7 } from "path";
4463
4506
  import { appServiceDefinition as appServiceDefinition3, appServiceIds as appServiceIds3 } from "@odla-ai/apps";
4464
4507
  function initProject(options) {
4465
4508
  const out = options.stdout ?? console;
4466
4509
  const rootDir = resolve7(options.rootDir ?? process.cwd());
4467
4510
  const configPath = resolve7(rootDir, options.configPath ?? "odla.config.mjs");
4468
- if (existsSync8(configPath) && !options.force) {
4511
+ if (existsSync9(configPath) && !options.force) {
4469
4512
  throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
4470
4513
  }
4471
4514
  if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
@@ -4493,7 +4536,7 @@ function initProject(options) {
4493
4536
  out.log("updated .gitignore for local odla credentials");
4494
4537
  }
4495
4538
  function writeIfMissing(path, text3) {
4496
- if (existsSync8(path)) return;
4539
+ if (existsSync9(path)) return;
4497
4540
  writeFileSync2(path, text3);
4498
4541
  }
4499
4542
  function configTemplate(input) {
@@ -4790,9 +4833,9 @@ function printReport(report5, out) {
4790
4833
  }
4791
4834
 
4792
4835
  // src/skill.ts
4793
- import { existsSync as existsSync9, lstatSync, mkdirSync as mkdirSync3, readFileSync as readFileSync8, readdirSync, writeFileSync as writeFileSync3 } from "fs";
4794
- import { homedir as homedir2 } from "os";
4795
- import { dirname as dirname7, isAbsolute as isAbsolute3, join as join9, relative as relative2, resolve as resolve8, sep } from "path";
4836
+ import { existsSync as existsSync10, lstatSync, mkdirSync as mkdirSync3, readFileSync as readFileSync9, readdirSync, writeFileSync as writeFileSync3 } from "fs";
4837
+ import { homedir as homedir3 } from "os";
4838
+ import { dirname as dirname7, isAbsolute as isAbsolute3, join as join10, relative as relative2, resolve as resolve8, sep } from "path";
4796
4839
  import { fileURLToPath } from "url";
4797
4840
 
4798
4841
  // src/skill-adapters.ts
@@ -4892,7 +4935,7 @@ function installSkill(options = {}) {
4892
4935
  if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
4893
4936
  const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
4894
4937
  const root = resolve8(options.dir ?? process.cwd());
4895
- const home = resolve8(options.homeDir ?? homedir2());
4938
+ const home = resolve8(options.homeDir ?? homedir3());
4896
4939
  const plans = /* @__PURE__ */ new Map();
4897
4940
  const targets = /* @__PURE__ */ new Map();
4898
4941
  const rememberTarget = (harness, target) => {
@@ -4906,12 +4949,12 @@ function installSkill(options = {}) {
4906
4949
  plans.set(target, { target, content: content2, boundary, managedMerge });
4907
4950
  };
4908
4951
  const planSkillTree = (targetDir2, boundary = root) => {
4909
- for (const rel of files) plan(join9(targetDir2, rel), readFileSync8(join9(sourceDir, rel), "utf8"), false, boundary);
4952
+ for (const rel of files) plan(join10(targetDir2, rel), readFileSync9(join10(sourceDir, rel), "utf8"), false, boundary);
4910
4953
  };
4911
4954
  let targetDir;
4912
4955
  if (options.global) {
4913
- const claudeRoot = join9(home, ".claude", "skills");
4914
- const codexRoot = resolve8(options.codexHomeDir ?? process.env.CODEX_HOME ?? join9(home, ".codex"), "skills");
4956
+ const claudeRoot = join10(home, ".claude", "skills");
4957
+ const codexRoot = resolve8(options.codexHomeDir ?? process.env.CODEX_HOME ?? join10(home, ".codex"), "skills");
4915
4958
  targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
4916
4959
  for (const harness of harnesses) {
4917
4960
  const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
@@ -4919,35 +4962,35 @@ function installSkill(options = {}) {
4919
4962
  rememberTarget(harness, skillRoot);
4920
4963
  }
4921
4964
  } else {
4922
- const sharedRoot = join9(root, ".agents", "skills");
4965
+ const sharedRoot = join10(root, ".agents", "skills");
4923
4966
  planSkillTree(sharedRoot);
4924
- const claudeRoot = join9(root, ".claude", "skills");
4967
+ const claudeRoot = join10(root, ".claude", "skills");
4925
4968
  targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
4926
4969
  for (const harness of harnesses) rememberTarget(harness, sharedRoot);
4927
4970
  if (harnesses.includes("claude")) {
4928
4971
  for (const skill of skillNames(files)) {
4929
- const canonical2 = readFileSync8(join9(sourceDir, skill, "SKILL.md"), "utf8");
4930
- plan(join9(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical2));
4972
+ const canonical2 = readFileSync9(join10(sourceDir, skill, "SKILL.md"), "utf8");
4973
+ plan(join10(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical2));
4931
4974
  }
4932
4975
  rememberTarget("claude", claudeRoot);
4933
4976
  }
4934
4977
  if (harnesses.includes("cursor")) {
4935
- const cursorRule = join9(root, ".cursor", "rules", "odla.mdc");
4978
+ const cursorRule = join10(root, ".cursor", "rules", "odla.mdc");
4936
4979
  plan(cursorRule, CURSOR_RULE);
4937
4980
  rememberTarget("cursor", cursorRule);
4938
4981
  }
4939
4982
  if (harnesses.includes("agents")) {
4940
- const agentsFile = join9(root, "AGENTS.md");
4983
+ const agentsFile = join10(root, "AGENTS.md");
4941
4984
  plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
4942
4985
  rememberTarget("agents", agentsFile);
4943
4986
  }
4944
4987
  if (harnesses.includes("copilot")) {
4945
- const copilotFile = join9(root, ".github", "copilot-instructions.md");
4988
+ const copilotFile = join10(root, ".github", "copilot-instructions.md");
4946
4989
  plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
4947
4990
  rememberTarget("copilot", copilotFile);
4948
4991
  }
4949
4992
  if (harnesses.includes("gemini")) {
4950
- const geminiFile = join9(root, "GEMINI.md");
4993
+ const geminiFile = join10(root, "GEMINI.md");
4951
4994
  plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
4952
4995
  rememberTarget("gemini", geminiFile);
4953
4996
  }
@@ -4961,11 +5004,11 @@ function installSkill(options = {}) {
4961
5004
  conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
4962
5005
  continue;
4963
5006
  }
4964
- if (!existsSync9(file.target)) {
5007
+ if (!existsSync10(file.target)) {
4965
5008
  writtenPaths.add(file.target);
4966
5009
  continue;
4967
5010
  }
4968
- const current = readFileSync8(file.target, "utf8");
5011
+ const current = readFileSync9(file.target, "utf8");
4969
5012
  if (current === file.content) {
4970
5013
  unchangedPaths.add(file.target);
4971
5014
  } else if (file.managedMerge || options.force) {
@@ -4982,7 +5025,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
4982
5025
  );
4983
5026
  }
4984
5027
  for (const file of plans.values()) {
4985
- if (!existsSync9(file.target) || readFileSync8(file.target, "utf8") !== file.content) {
5028
+ if (!existsSync10(file.target) || readFileSync9(file.target, "utf8") !== file.content) {
4986
5029
  mkdirSync3(dirname7(file.target), { recursive: true });
4987
5030
  writeFileSync3(file.target, file.content);
4988
5031
  }
@@ -5025,9 +5068,9 @@ function normalizeHarnesses(values, global) {
5025
5068
  function managedFileContent(path, block, force, boundary) {
5026
5069
  const symlink = symlinkedComponent(boundary, path);
5027
5070
  if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
5028
- if (!existsSync9(path)) return `${block}
5071
+ if (!existsSync10(path)) return `${block}
5029
5072
  `;
5030
- const current = readFileSync8(path, "utf8");
5073
+ const current = readFileSync9(path, "utf8");
5031
5074
  const start = "<!-- odla-ai agent setup:start -->";
5032
5075
  const end = "<!-- odla-ai agent setup:end -->";
5033
5076
  const startAt = current.indexOf(start);
@@ -5054,7 +5097,7 @@ function symlinkedComponent(boundary, target) {
5054
5097
  }
5055
5098
  let current = boundary;
5056
5099
  for (const part of rel.split(sep).filter(Boolean)) {
5057
- current = join9(current, part);
5100
+ current = join10(current, part);
5058
5101
  try {
5059
5102
  if (lstatSync(current).isSymbolicLink()) return current;
5060
5103
  } catch (error) {
@@ -5067,11 +5110,11 @@ function skillNames(files) {
5067
5110
  return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
5068
5111
  }
5069
5112
  function listFiles(dir) {
5070
- if (!existsSync9(dir)) return [];
5113
+ if (!existsSync10(dir)) return [];
5071
5114
  const results = [];
5072
5115
  const walk = (current) => {
5073
5116
  for (const entry of readdirSync(current, { withFileTypes: true })) {
5074
- const path = join9(current, entry.name);
5117
+ const path = join10(current, entry.name);
5075
5118
  if (entry.isDirectory()) walk(path);
5076
5119
  else results.push(relative2(dir, path));
5077
5120
  }
@@ -5428,7 +5471,7 @@ async function projectCommand(command, parsed, deps) {
5428
5471
  }
5429
5472
 
5430
5473
  // src/code-connect.ts
5431
- import { existsSync as existsSync10 } from "fs";
5474
+ import { existsSync as existsSync11 } from "fs";
5432
5475
  import { cpus, hostname, totalmem } from "os";
5433
5476
  import { resolve as resolve11 } from "path";
5434
5477
 
@@ -5439,7 +5482,7 @@ var HARNESS_PROTOCOL_VERSION = 1;
5439
5482
  import { execFile, spawn as spawn3 } from "child_process";
5440
5483
  import { constants } from "fs";
5441
5484
  import { access } from "fs/promises";
5442
- import { delimiter, join as join10 } from "path";
5485
+ import { delimiter, join as join11 } from "path";
5443
5486
  import { getgid, getuid } from "process";
5444
5487
  import { mkdir as mkdir2, mkdtemp, realpath, rm, writeFile as writeFile2 } from "fs/promises";
5445
5488
  import { tmpdir } from "os";
@@ -5457,7 +5500,7 @@ function assertPinnedImage(image) {
5457
5500
  async function commandAvailable(engine) {
5458
5501
  for (const directory of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
5459
5502
  try {
5460
- await access(join10(directory, engine), constants.X_OK);
5503
+ await access(join11(directory, engine), constants.X_OK);
5461
5504
  return true;
5462
5505
  } catch {
5463
5506
  }
@@ -6155,7 +6198,7 @@ import { randomUUID } from "crypto";
6155
6198
  import { createHash as createHash22, randomUUID as randomUUID2 } from "crypto";
6156
6199
  import { createReadStream } from "fs";
6157
6200
  import { lstat as lstat22 } from "fs/promises";
6158
- import { join as join12 } from "path";
6201
+ import { join as join13 } from "path";
6159
6202
  import { mkdir as mkdir3, mkdtemp as mkdtemp3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
6160
6203
  import { tmpdir as tmpdir3 } from "os";
6161
6204
  import { dirname as dirname9, join as join23, resolve as resolve32, sep as sep23 } from "path";
@@ -6542,8 +6585,8 @@ function rollup(graph, kind, options = {}) {
6542
6585
  for (const node of nodesOfKind(graph, kind)) {
6543
6586
  if (options.prefix && !node.name.startsWith(options.prefix)) continue;
6544
6587
  const key = node.name.split(separator).slice(0, depth).join(separator);
6545
- const list2 = groups.get(key);
6546
- if (list2) list2.push(node);
6588
+ const list3 = groups.get(key);
6589
+ if (list3) list3.push(node);
6547
6590
  else groups.set(key, [node]);
6548
6591
  }
6549
6592
  return [...groups].map(([prefix, nodes]) => ({
@@ -6558,7 +6601,7 @@ function dirname8(path) {
6558
6601
  const at = path.lastIndexOf("/");
6559
6602
  return at <= 0 ? "." : path.slice(0, at);
6560
6603
  }
6561
- function join11(base, specifier) {
6604
+ function join12(base, specifier) {
6562
6605
  const parts = [];
6563
6606
  const segments = `${base === "." ? "" : `${base}/`}${specifier}`.split("/");
6564
6607
  for (const segment of segments) {
@@ -6582,7 +6625,7 @@ var BARE_IMPORT = /^\s*import\s*["']([^"']+)["']/gm;
6582
6625
  var isSourcePath = (path) => SOURCE.test(path);
6583
6626
  function resolveImport(fromPath, specifier, known) {
6584
6627
  if (!specifier.startsWith(".")) return null;
6585
- const base = join11(dirname8(fromPath), specifier);
6628
+ const base = join12(dirname8(fromPath), specifier);
6586
6629
  const candidates = [
6587
6630
  base,
6588
6631
  base.replace(/\.js$/, ".ts"),
@@ -7385,7 +7428,7 @@ async function inspectArtifacts(workspaceDir, recipe2) {
7385
7428
  const receipts = [];
7386
7429
  for (const artifact of recipe2.expectedArtifacts ?? []) {
7387
7430
  try {
7388
- const path = join12(workspaceDir, artifact.path);
7431
+ const path = join13(workspaceDir, artifact.path);
7389
7432
  const info = await lstat22(path);
7390
7433
  if (!info.isFile() || info.isSymbolicLink()) {
7391
7434
  receipts.push({ artifactId: artifact.id, status: "invalid", bytes: null, digest: null });
@@ -9481,7 +9524,7 @@ var CODE_BUILD_RECIPES = Object.freeze([{
9481
9524
  async function codeConnect(options) {
9482
9525
  const cwd = options.cwd ?? process.cwd();
9483
9526
  const configPath = resolve11(cwd, options.configPath);
9484
- const cfg = existsSync10(configPath) ? await loadProjectConfig(configPath) : null;
9527
+ const cfg = existsSync11(configPath) ? await loadProjectConfig(configPath) : null;
9485
9528
  const requestedAppId = options.appId?.trim();
9486
9529
  if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
9487
9530
  throw new Error("--app-id must be a valid odla app id");
@@ -9915,13 +9958,13 @@ async function codeCommand(parsed, dependencies) {
9915
9958
  }
9916
9959
 
9917
9960
  // src/operator-credentials.ts
9918
- import process12 from "process";
9961
+ import process13 from "process";
9919
9962
  function developerTokenStatus(context, parsed, now = Date.now()) {
9920
9963
  const cached = readJsonFile(context.cfg.local.tokenFile);
9921
9964
  const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
9922
9965
  const source = clean3(
9923
9966
  stringOpt(parsed.options.token)
9924
- ) ? "flag" : clean3(process12.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
9967
+ ) ? "flag" : clean3(process13.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
9925
9968
  return {
9926
9969
  source,
9927
9970
  cacheFile: context.cfg.local.tokenFile,
@@ -10243,6 +10286,9 @@ Usage:
10243
10286
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
10244
10287
  odla-ai security run [target] --self --ack-redacted-source
10245
10288
  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]
10289
+ odla-ai device enroll [--app <id>[,<id>...]] [--name <label>] [--capability <c>[,<c>...]] [--email <odla-account>] [--no-open] [--json]
10290
+ odla-ai device list [--email <odla-account>] [--json]
10291
+ odla-ai device revoke <device-id> [--email <odla-account>] [--json]
10246
10292
  odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
10247
10293
  odla-ai credentials revoke <receipt-id> [--config odla.config.mjs] [--json]
10248
10294
  odla-ai smoke [--config odla.config.mjs] [--env dev] [--runtime] [--email <odla-account>] [--no-open]
@@ -10348,6 +10394,10 @@ Commands:
10348
10394
  stable status, incident, and report JSON to agents and CI.
10349
10395
  platform Read canonical fleet health, releases, provider load/freshness,
10350
10396
  explicit unknowns, and next actions through a read-only grant.
10397
+ device Enrol THIS machine once, then stop asking. A human approves the
10398
+ enrollment in the browser; from then on this terminal mints its
10399
+ own short-lived credentials for the named projects with nobody's
10400
+ attention, until the device expires or is revoked.
10351
10401
  provision Register services, compose integrations, persist credentials, optionally push secrets.
10352
10402
  "provision --live --yes" initializes only the live instance of
10353
10403
  an existing sandbox app and enables every configured service;
@@ -12630,7 +12680,7 @@ function percent(value2) {
12630
12680
  // src/provision.ts
12631
12681
  import { AppsError as AppsError2, createAppsClient as createAppsClient3, orderAppServices as orderAppServices3, tenantIdFor as tenantIdFor6 } from "@odla-ai/apps";
12632
12682
  import { putSecret as putSecret2 } from "@odla-ai/ai";
12633
- import process13 from "process";
12683
+ import process14 from "process";
12634
12684
 
12635
12685
  // src/integration-provision.ts
12636
12686
  import { uuidv7 } from "@odla-ai/db";
@@ -13066,7 +13116,7 @@ async function provision(options) {
13066
13116
  await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
13067
13117
  }
13068
13118
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
13069
- const key = process13.env[cfg.ai.keyEnv];
13119
+ const key = process14.env[cfg.ai.keyEnv];
13070
13120
  if (key) {
13071
13121
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
13072
13122
  await putSecret2({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -13108,7 +13158,7 @@ async function provision(options) {
13108
13158
 
13109
13159
  // src/record.ts
13110
13160
  import { appendFileSync } from "fs";
13111
- import process14 from "process";
13161
+ import process15 from "process";
13112
13162
 
13113
13163
  // src/surface.ts
13114
13164
  var PM_ACTIONS = {
@@ -13178,6 +13228,7 @@ var COMMAND_SURFACE = {
13178
13228
  config: { diff: {}, plan: {}, apply: {} },
13179
13229
  context: { show: {}, list: {}, save: {}, remove: {} },
13180
13230
  credentials: { list: {}, revoke: {} },
13231
+ device: { enroll: {}, list: {}, revoke: {} },
13181
13232
  // `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
13182
13233
  discuss: {
13183
13234
  groups: {},
@@ -13289,7 +13340,7 @@ function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
13289
13340
 
13290
13341
  // src/record.ts
13291
13342
  function recordInvocation(parsed) {
13292
- const file = process14.env.ODLA_CLI_RECORD;
13343
+ const file = process15.env.ODLA_CLI_RECORD;
13293
13344
  if (!file) return;
13294
13345
  try {
13295
13346
  const entry = {
@@ -13326,8 +13377,109 @@ function renderAdvisories(out, advisories, env = process.env) {
13326
13377
  }
13327
13378
  }
13328
13379
 
13380
+ // src/device-command.ts
13381
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
13382
+ import { dirname as dirname10 } from "path";
13383
+ import process16 from "process";
13384
+ async function deviceCommand(parsed, deps) {
13385
+ const action2 = parsed.positionals[1] ?? "";
13386
+ const out = deps.stdout ?? console;
13387
+ const doFetch = deps.fetch ?? fetch;
13388
+ const cfg = await loadProjectConfig(stringOpt(parsed.options.config));
13389
+ const json = parsed.options.json === true;
13390
+ if (action2 === "enroll") return enroll(parsed, deps, cfg, doFetch, out, json);
13391
+ if (action2 === "list") return list2(parsed, deps, cfg, doFetch, out, json);
13392
+ if (action2 === "revoke") return revoke(parsed, deps, cfg, doFetch, out, json);
13393
+ throw new Error('odla-ai device expects "enroll", "list", or "revoke"');
13394
+ }
13395
+ async function enroll(parsed, deps, cfg, doFetch, out, json) {
13396
+ const name = stringOpt(parsed.options.name) ?? defaultDeviceName();
13397
+ const apps = (stringOpt(parsed.options.app) ?? cfg.app.id).split(",").map((id2) => id2.trim()).filter(Boolean);
13398
+ if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026]");
13399
+ const token = await scopedToken2(parsed, deps, cfg, doFetch, out, `odla CLI (enroll ${name})`);
13400
+ const response2 = await doFetch(`${cfg.platformUrl}/registry/devices`, {
13401
+ method: "POST",
13402
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
13403
+ body: JSON.stringify({
13404
+ name,
13405
+ platform: process16.platform,
13406
+ appIds: apps,
13407
+ ...parsed.options.capability ? { capabilities: String(parsed.options.capability).split(",").map((c) => c.trim()).filter(Boolean) } : {}
13408
+ })
13409
+ });
13410
+ const body = await response2.json().catch(() => ({}));
13411
+ if (!response2.ok || !body.token || !body.device) {
13412
+ throw new Error(`device enroll failed: ${body.error?.message ?? `registry returned ${response2.status}`} (${response2.status})`);
13413
+ }
13414
+ const path = deviceCredentialPath();
13415
+ mkdirSync4(dirname10(path), { recursive: true });
13416
+ writeFileSync4(path, JSON.stringify({
13417
+ token: body.token,
13418
+ platform: cfg.platformUrl.replace(/\/$/, ""),
13419
+ deviceId: body.device.deviceId,
13420
+ name
13421
+ }, null, 2));
13422
+ chmodSync2(path, 384);
13423
+ out.error(`device: enrolled "${name}" for ${body.device.appIds.join(", ")}; credential written to ${path}`);
13424
+ out.error("device: this terminal will mint its own credentials from now on \u2014 no further approvals.");
13425
+ if (json) {
13426
+ out.log(JSON.stringify({ deviceId: body.device.deviceId, name, appIds: body.device.appIds, expiresAt: body.device.expiresAt }, null, 2));
13427
+ }
13428
+ }
13429
+ async function list2(parsed, deps, cfg, doFetch, out, json) {
13430
+ const token = await scopedToken2(parsed, deps, cfg, doFetch, out, "odla CLI (device list)");
13431
+ const response2 = await doFetch(`${cfg.platformUrl}/registry/devices`, {
13432
+ headers: { authorization: `Bearer ${token}` }
13433
+ });
13434
+ const body = await response2.json().catch(() => ({}));
13435
+ if (!response2.ok || !body.devices) {
13436
+ throw new Error(`device list failed: ${body.error?.message ?? `registry returned ${response2.status}`} (${response2.status})`);
13437
+ }
13438
+ if (json) return out.log(JSON.stringify(body.devices, null, 2));
13439
+ if (body.devices.length === 0) return out.log("no enrolled devices");
13440
+ for (const device of body.devices) {
13441
+ const state2 = device.revokedAt ? "revoked" : device.expiresAt <= Date.now() ? "expired" : "active";
13442
+ out.log(`${device.deviceId} ${state2.padEnd(7)} ${device.name} [${device.appIds.join(", ")}]`);
13443
+ }
13444
+ }
13445
+ async function revoke(parsed, deps, cfg, doFetch, out, json) {
13446
+ const deviceId = parsed.positionals[2];
13447
+ if (!deviceId) throw new Error("device revoke needs the device id from `odla-ai device list`");
13448
+ const token = await scopedToken2(parsed, deps, cfg, doFetch, out, "odla CLI (device revoke)");
13449
+ const response2 = await doFetch(`${cfg.platformUrl}/registry/devices/${encodeURIComponent(deviceId)}/revoke`, {
13450
+ method: "POST",
13451
+ headers: { authorization: `Bearer ${token}` }
13452
+ });
13453
+ if (!response2.ok) {
13454
+ const body = await response2.json().catch(() => ({}));
13455
+ throw new Error(`device revoke failed: ${body.error?.message ?? `registry returned ${response2.status}`} (${response2.status})`);
13456
+ }
13457
+ out.error(`device: revoked ${deviceId}; every credential it minted is revoked with it`);
13458
+ if (json) out.log(JSON.stringify({ deviceId, revoked: true }, null, 2));
13459
+ }
13460
+ async function scopedToken2(parsed, deps, cfg, doFetch, out, label) {
13461
+ const { credentials } = await resolveOperatorContext(parsed, { allowMissingConfig: true });
13462
+ const scopedTokenFile = credentials.scopedTokenFile;
13463
+ return getScopedPlatformToken({
13464
+ platform: cfg.platformUrl,
13465
+ scope: "app:device:enroll",
13466
+ email: stringOpt(parsed.options.email),
13467
+ label,
13468
+ fetch: doFetch,
13469
+ stdout: out,
13470
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
13471
+ openApprovalUrl: deps.openUrl,
13472
+ rootDir: cfg.rootDir,
13473
+ tokenFile: scopedTokenFile,
13474
+ ...stringOpt(parsed.options.token) ? { token: stringOpt(parsed.options.token) } : {}
13475
+ });
13476
+ }
13477
+ function defaultDeviceName() {
13478
+ return `${process16.env.HOSTNAME ?? process16.env.HOST ?? "machine"}-${process16.platform}`;
13479
+ }
13480
+
13329
13481
  // src/runbook-actions.ts
13330
- import { readFileSync as readFileSync9 } from "fs";
13482
+ import { readFileSync as readFileSync10 } from "fs";
13331
13483
  var PLATFORM_SCOPE = "$platform";
13332
13484
  async function call(ctx, method, path, body) {
13333
13485
  const res = await ctx.doFetch(`${ctx.platformUrl.replace(/\/$/, "")}/registry/pm${path}`, {
@@ -13374,7 +13526,7 @@ async function bySlug(ctx, slug) {
13374
13526
  function readBody(file, inline) {
13375
13527
  if (inline !== void 0) return inline;
13376
13528
  if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
13377
- return readFileSync9(file === "-" ? 0 : file, "utf8");
13529
+ return readFileSync10(file === "-" ? 0 : file, "utf8");
13378
13530
  }
13379
13531
  var stamp = (ms) => ms ? new Date(ms).toISOString().slice(0, 16).replace("T", " ") : "";
13380
13532
  async function runbookList(ctx, all, query) {
@@ -13466,8 +13618,8 @@ async function runbookRemove(ctx, slug) {
13466
13618
  }
13467
13619
 
13468
13620
  // src/runbook-import.ts
13469
- import { readFileSync as readFileSync10, readdirSync as readdirSync2, statSync } from "fs";
13470
- import { basename as basename2, join as join13 } from "path";
13621
+ import { readFileSync as readFileSync11, readdirSync as readdirSync2, statSync } from "fs";
13622
+ import { basename as basename2, join as join14 } from "path";
13471
13623
  function parseRunbook(text3, slug) {
13472
13624
  let rest = text3;
13473
13625
  const meta = {};
@@ -13497,7 +13649,7 @@ function readRunbookDir(dir) {
13497
13649
  if (!files.length) throw new Error(`no .md files in ${dir}`);
13498
13650
  return files.map((file) => {
13499
13651
  const slug = basename2(file, ".md");
13500
- const parsed = parseRunbook(readFileSync10(join13(dir, file), "utf8"), slug);
13652
+ const parsed = parseRunbook(readFileSync11(join14(dir, file), "utf8"), slug);
13501
13653
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
13502
13654
  });
13503
13655
  }
@@ -13570,8 +13722,8 @@ async function upsert(ctx, r, visibility) {
13570
13722
 
13571
13723
  // src/runbook-impact.ts
13572
13724
  import { execFileSync as execFileSync2 } from "child_process";
13573
- import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
13574
- import { join as join14 } from "path";
13725
+ import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
13726
+ import { join as join15 } from "path";
13575
13727
 
13576
13728
  // src/runbook-impact-scan.ts
13577
13729
  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$]*)/;
@@ -13740,10 +13892,10 @@ ${body.split("\n").map((line2) => `+${line2}`).join("\n")}
13740
13892
  }
13741
13893
  function manifestLabeller(root) {
13742
13894
  return (workspace) => {
13743
- const manifest = join14(root, workspace, "package.json");
13744
- if (!existsSync11(manifest)) return void 0;
13895
+ const manifest = join15(root, workspace, "package.json");
13896
+ if (!existsSync12(manifest)) return void 0;
13745
13897
  try {
13746
- const name = JSON.parse(readFileSync11(manifest, "utf8")).name;
13898
+ const name = JSON.parse(readFileSync12(manifest, "utf8")).name;
13747
13899
  return typeof name === "string" ? name : void 0;
13748
13900
  } catch {
13749
13901
  return void 0;
@@ -13810,7 +13962,7 @@ function report4(ctx, impacts) {
13810
13962
  async function runbookImpact(ctx, options, deps = {}) {
13811
13963
  const cwd = deps.cwd ?? process.cwd();
13812
13964
  const runGit = deps.runGit ?? gitRunner(cwd);
13813
- const read3 = deps.readRepoFile ?? ((path) => readFileSync11(join14(cwd, path), "utf8"));
13965
+ const read3 = deps.readRepoFile ?? ((path) => readFileSync12(join15(cwd, path), "utf8"));
13814
13966
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
13815
13967
  if (!surfaces.length) {
13816
13968
  return ctx.out.log(
@@ -13943,12 +14095,12 @@ async function runbookComment(ctx, slug, body) {
13943
14095
 
13944
14096
  // src/runbook-editor.ts
13945
14097
  import { spawnSync } from "child_process";
13946
- import { mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
14098
+ import { mkdtempSync, readFileSync as readFileSync13, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
13947
14099
  import { tmpdir as tmpdir4 } from "os";
13948
- import { join as join15 } from "path";
13949
- import process15 from "process";
14100
+ import { join as join16 } from "path";
14101
+ import process17 from "process";
13950
14102
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
13951
- function resolveEditor(env = process15.env) {
14103
+ function resolveEditor(env = process17.env) {
13952
14104
  for (const name of EDITOR_ENV) {
13953
14105
  const value2 = env[name];
13954
14106
  if (value2 && value2.trim()) return value2.trim();
@@ -13962,8 +14114,8 @@ function defaultRun(command, path) {
13962
14114
  return result.status ?? 0;
13963
14115
  }
13964
14116
  function editText(initial, slug, deps = {}) {
13965
- const env = deps.env ?? process15.env;
13966
- const interactive = deps.interactive ?? (() => Boolean(process15.stdin.isTTY));
14117
+ const env = deps.env ?? process17.env;
14118
+ const interactive = deps.interactive ?? (() => Boolean(process17.stdin.isTTY));
13967
14119
  const editor = resolveEditor(env);
13968
14120
  if (!editor)
13969
14121
  throw new Error(
@@ -13971,13 +14123,13 @@ function editText(initial, slug, deps = {}) {
13971
14123
  );
13972
14124
  if (!interactive())
13973
14125
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
13974
- const dir = mkdtempSync(join15(tmpdir4(), "odla-runbook-"));
13975
- const file = join15(dir, `${slug}.md`);
14126
+ const dir = mkdtempSync(join16(tmpdir4(), "odla-runbook-"));
14127
+ const file = join16(dir, `${slug}.md`);
13976
14128
  try {
13977
- writeFileSync4(file, initial, { mode: 384 });
14129
+ writeFileSync5(file, initial, { mode: 384 });
13978
14130
  const code = defaultRunOrInjected(deps)(editor, file);
13979
14131
  if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
13980
- const edited = readFileSync12(file, "utf8");
14132
+ const edited = readFileSync13(file, "utf8");
13981
14133
  return edited === initial ? null : edited;
13982
14134
  } finally {
13983
14135
  rmSync3(dir, { recursive: true, force: true });
@@ -14914,6 +15066,10 @@ async function dispatchCli(argv, dependencies) {
14914
15066
  await contextCommand(parsed, runtime);
14915
15067
  return;
14916
15068
  }
15069
+ if (command === "device") {
15070
+ await deviceCommand(parsed, runtime);
15071
+ return;
15072
+ }
14917
15073
  if (command === "credentials") {
14918
15074
  await credentialCommand(parsed, runtime);
14919
15075
  return;
@@ -15109,4 +15265,4 @@ export {
15109
15265
  isTerminalHostedSecurityStatus,
15110
15266
  runCli
15111
15267
  };
15112
- //# sourceMappingURL=chunk-PYR73XBD.js.map
15268
+ //# sourceMappingURL=chunk-DBZQIMES.js.map