@odla-ai/cli 0.35.3 → 0.36.1

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