@odla-ai/cli 0.35.1 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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";
@@ -149,13 +149,72 @@ function handshakeWaitMs(waitSeconds, interactive = process4.stdout.isTTY === tr
149
149
  return interactive ? void 0 : 9e4;
150
150
  }
151
151
 
152
+ // src/cached-credential.ts
153
+ import { rmSync as rmSync2 } from "fs";
154
+ var noted = null;
155
+ function noteCachedCredential(tokenFile) {
156
+ noted = tokenFile;
157
+ }
158
+ function isCredentialRejection(error) {
159
+ const message2 = error instanceof Error ? error.message : String(error ?? "");
160
+ return /\((401|403)\)\s*$/.test(message2.trim());
161
+ }
162
+ function explainRejectedCredential(error) {
163
+ const tokenFile = noted;
164
+ if (!tokenFile || !isCredentialRejection(error)) return null;
165
+ noted = null;
166
+ rmSync2(tokenFile, { force: true });
167
+ return [
168
+ "auth: the cached credential was rejected by odla, so it was revoked before its cached expiry.",
169
+ " The usual cause is a newer sign-in for this project: collecting a handshake retires the",
170
+ " principal's other credentials, so a second terminal or worktree supersedes this one.",
171
+ ` Discarded ${tokenFile}; re-run this command to request a fresh approval.`
172
+ ].join("\n");
173
+ }
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
+
152
211
  // src/local.ts
153
- import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
212
+ import { chmodSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
154
213
  import { dirname as dirname2, isAbsolute, relative, resolve } from "path";
155
214
  var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
156
215
  function readJsonFile(path) {
157
216
  try {
158
- return JSON.parse(readFileSync(path, "utf8"));
217
+ return JSON.parse(readFileSync2(path, "utf8"));
159
218
  } catch {
160
219
  return null;
161
220
  }
@@ -165,10 +224,10 @@ function writePrivateJson(path, value2) {
165
224
  `);
166
225
  }
167
226
  function readCredentials(path) {
168
- if (!existsSync(path)) return null;
227
+ if (!existsSync2(path)) return null;
169
228
  let value2;
170
229
  try {
171
- value2 = JSON.parse(readFileSync(path, "utf8"));
230
+ value2 = JSON.parse(readFileSync2(path, "utf8"));
172
231
  } catch {
173
232
  throw new Error(`credentials file ${path} is not valid JSON; fix or remove it before provisioning`);
174
233
  }
@@ -199,7 +258,7 @@ function mergeCredential(current, update) {
199
258
  }
200
259
  function ensureGitignore(rootDir, localPaths = []) {
201
260
  const path = resolve(rootDir, ".gitignore");
202
- const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
261
+ const existing = existsSync2(path) ? readFileSync2(path, "utf8") : "";
203
262
  const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line2) => !!line2);
204
263
  const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
205
264
  const missing = wanted.filter((line2) => !existing.split(/\r?\n/).includes(line2));
@@ -233,7 +292,7 @@ function writeDevVars(path, credentials, env, o11y) {
233
292
  if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
234
293
  if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
235
294
  }
236
- const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
295
+ const existing = existsSync2(path) ? readFileSync2(path, "utf8") : "";
237
296
  const retained = existing.split(/\r?\n/).filter((line2) => !isManagedDevVar(line2));
238
297
  while (retained.at(-1) === "") retained.pop();
239
298
  const prefix = retained.length ? `${retained.join("\n")}
@@ -283,17 +342,24 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
283
342
  const cached = readJsonFile(cfg.local.tokenFile);
284
343
  if (!grantRequest.forceReview && !grantRequest.freshLogin) {
285
344
  if (options.token) return options.token;
286
- if (process5.env.ODLA_DEV_TOKEN) {
287
- const declared = process5.env.ODLA_DEV_TOKEN_AUDIENCE;
345
+ if (process6.env.ODLA_DEV_TOKEN) {
346
+ const declared = process6.env.ODLA_DEV_TOKEN_AUDIENCE;
288
347
  if (declared) {
289
348
  if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
290
349
  } else if (audience !== "https://odla.ai") {
291
350
  throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
292
351
  }
293
- 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;
294
359
  }
295
360
  if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4 && cachedGrantCovers(cached, grantIntent)) {
296
361
  out.error(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
362
+ noteCachedCredential(cfg.local.tokenFile);
297
363
  return cached.token;
298
364
  }
299
365
  } else {
@@ -394,7 +460,7 @@ function stillPending(pending, email) {
394
460
  );
395
461
  }
396
462
  function handshakeEmail(value2, cached) {
397
- const email = (value2 ?? process5.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
463
+ const email = (value2 ?? process6.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
398
464
  if (/@users\.noreply\.github\.com$/i.test(email)) {
399
465
  throw new Error(
400
466
  `"${email}" is a GitHub commit identity, not an odla account email; use --email <signed-in-odla-account> or ODLA_USER_EMAIL`
@@ -425,12 +491,12 @@ function platformAudience(value2) {
425
491
  }
426
492
 
427
493
  // src/secret-input.ts
428
- import process6 from "process";
494
+ import process7 from "process";
429
495
  var MAX_BYTES = 64 * 1024;
430
496
  async function secretInputValue(options, kind = "credential") {
431
497
  if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
432
498
  let value2;
433
- if (options.fromEnv) value2 = process6.env[options.fromEnv];
499
+ if (options.fromEnv) value2 = process7.env[options.fromEnv];
434
500
  else if (options.stdin) value2 = await (options.readStdin ?? (() => readSecretStream(kind)))();
435
501
  else throw new Error(`${kind} input required: use --from-env <NAME> or --stdin; values are never accepted as arguments`);
436
502
  value2 = value2?.replace(/[\r\n]+$/, "");
@@ -438,7 +504,7 @@ async function secretInputValue(options, kind = "credential") {
438
504
  if (new TextEncoder().encode(value2).byteLength > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
439
505
  return value2;
440
506
  }
441
- async function readSecretStream(kind, stream = process6.stdin) {
507
+ async function readSecretStream(kind, stream = process7.stdin) {
442
508
  let value2 = "";
443
509
  for await (const chunk of stream) {
444
510
  value2 += String(chunk);
@@ -448,9 +514,9 @@ async function readSecretStream(kind, stream = process6.stdin) {
448
514
  }
449
515
 
450
516
  // src/admin-ai-auth.ts
451
- import { existsSync as existsSync2 } from "fs";
452
- import { join as join2 } from "path";
453
- import process7 from "process";
517
+ import { existsSync as existsSync3 } from "fs";
518
+ import { join as join3 } from "path";
519
+ import process8 from "process";
454
520
  import { requestToken as requestToken2 } from "@odla-ai/db";
455
521
  async function getScopedPlatformToken(options) {
456
522
  return resolveAdminPlatformToken(options);
@@ -458,7 +524,7 @@ async function getScopedPlatformToken(options) {
458
524
  async function resolveAdminPlatformToken(options) {
459
525
  const audience = platformAudience(options.platform);
460
526
  if (options.token) return options.token;
461
- const fromEnv = process7.env.ODLA_ADMIN_TOKEN;
527
+ const fromEnv = process8.env.ODLA_ADMIN_TOKEN;
462
528
  if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
463
529
  return scopedToken(
464
530
  audience,
@@ -470,7 +536,7 @@ async function resolveAdminPlatformToken(options) {
470
536
  }
471
537
  function audienceBoundEnvToken(token, platform) {
472
538
  const audience = platformAudience(platform);
473
- const declared = process7.env.ODLA_ADMIN_TOKEN_AUDIENCE;
539
+ const declared = process8.env.ODLA_ADMIN_TOKEN_AUDIENCE;
474
540
  if (declared) {
475
541
  if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
476
542
  } else if (audience !== "https://odla.ai") {
@@ -483,6 +549,7 @@ var SCOPE_PURPOSE = {
483
549
  "app:config:read": "compare checked-in intent with an exact-id app Registry configuration",
484
550
  "app:config:write": "apply or inspect one revision-bound configuration operation for an app you own",
485
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",
486
553
  "platform:ai:policy:write": "change System AI model routing",
487
554
  "platform:ai:policy:read": "read System AI model routing",
488
555
  "platform:ai:credential:write": "replace a stored AI provider key",
@@ -495,8 +562,8 @@ var SCOPE_PURPOSE = {
495
562
  };
496
563
  async function scopedToken(platform, scope, options, doFetch, out) {
497
564
  const audience = platformAudience(platform);
498
- const rootDir = options.rootDir ?? process7.cwd();
499
- 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");
500
567
  const cache2 = options.cache === false ? null : readJsonFile(tokenFile);
501
568
  const cached = cache2?.platform === audience ? cache2.tokens?.[scope] : void 0;
502
569
  if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
@@ -523,7 +590,7 @@ async function scopedToken(platform, scope, options, doFetch, out) {
523
590
  if (options.cache !== false) {
524
591
  const tokens = cache2?.platform === audience ? { ...cache2.tokens ?? {} } : {};
525
592
  tokens[scope] = { token, expiresAt };
526
- if (existsSync2(join2(rootDir, ".git"))) ensureGitignore(rootDir, [tokenFile]);
593
+ if (existsSync3(join3(rootDir, ".git"))) ensureGitignore(rootDir, [tokenFile]);
527
594
  writePrivateJson(tokenFile, { platform: audience, email, tokens });
528
595
  out.error(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
529
596
  } else {
@@ -700,7 +767,7 @@ function isRecord2(value2) {
700
767
 
701
768
  // src/admin-ai.ts
702
769
  async function adminAi(options) {
703
- 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");
704
771
  const doFetch = options.fetch ?? fetch;
705
772
  const out = options.stdout ?? console;
706
773
  const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
@@ -955,12 +1022,12 @@ function addOption(options, name, value2) {
955
1022
  }
956
1023
 
957
1024
  // src/operator-context.ts
958
- import { existsSync as existsSync5 } from "fs";
959
- import { join as join4, resolve as resolve4 } from "path";
960
- 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";
961
1028
 
962
1029
  // src/config.ts
963
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1030
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
964
1031
  import { dirname as dirname3, isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
965
1032
  import { pathToFileURL } from "url";
966
1033
  import { appServiceDefinition, appServiceIds } from "@odla-ai/apps";
@@ -1367,7 +1434,7 @@ var configImportSerial = 0;
1367
1434
  var GOOGLE_CALENDAR_EVENTS_SCOPE = "https://www.googleapis.com/auth/calendar.events";
1368
1435
  async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1369
1436
  const resolved = resolve2(configPath);
1370
- if (!existsSync3(resolved)) {
1437
+ if (!existsSync4(resolved)) {
1371
1438
  throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
1372
1439
  }
1373
1440
  const raw = await loadConfigModule(resolved);
@@ -1402,7 +1469,7 @@ async function resolveDataExport(cfg, value2, names) {
1402
1469
  if (typeof value2 !== "string") return value2;
1403
1470
  const target = isAbsolute2(value2) ? value2 : resolve2(cfg.rootDir, value2);
1404
1471
  if (target.endsWith(".json")) {
1405
- return JSON.parse(readFileSync2(target, "utf8"));
1472
+ return JSON.parse(readFileSync3(target, "utf8"));
1406
1473
  }
1407
1474
  const mod = await import(pathToFileURL(target).href);
1408
1475
  for (const name of names) {
@@ -1478,7 +1545,7 @@ function validId2(value2) {
1478
1545
  return typeof value2 === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value2);
1479
1546
  }
1480
1547
  async function loadConfigModule(path) {
1481
- if (path.endsWith(".json")) return JSON.parse(readFileSync2(path, "utf8"));
1548
+ if (path.endsWith(".json")) return JSON.parse(readFileSync3(path, "utf8"));
1482
1549
  const nonce = `${Date.now()}-${configImportSerial++}`;
1483
1550
  const mod = await import(`${pathToFileURL(path).href}?reload=${nonce}`);
1484
1551
  const value2 = mod.default ?? mod.config;
@@ -1493,18 +1560,18 @@ function unique3(values) {
1493
1560
  }
1494
1561
 
1495
1562
  // src/operator-profiles.ts
1496
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1497
- import { homedir } from "os";
1498
- import { dirname as dirname4, join as join3, resolve as resolve3 } from "path";
1499
- 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";
1500
1567
  function operatorProfileFile() {
1501
1568
  return resolve3(
1502
- clean(process9.env.ODLA_CONTEXT_FILE) ?? join3(homedir(), ".odla", "contexts.json")
1569
+ clean(process10.env.ODLA_CONTEXT_FILE) ?? join4(homedir2(), ".odla", "contexts.json")
1503
1570
  );
1504
1571
  }
1505
1572
  function resolveOperatorProfile(parsed) {
1506
1573
  const fromFlag = clean(stringOpt(parsed.options.context));
1507
- const fromEnvironment = clean(process9.env.ODLA_CONTEXT);
1574
+ const fromEnvironment = clean(process10.env.ODLA_CONTEXT);
1508
1575
  const name = fromFlag ?? fromEnvironment ?? null;
1509
1576
  const file = operatorProfileFile();
1510
1577
  if (!name) {
@@ -1544,10 +1611,10 @@ function removeOperatorProfile(name, file = operatorProfileFile()) {
1544
1611
  return true;
1545
1612
  }
1546
1613
  function operatorCredentialFiles(selection) {
1547
- 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");
1548
1615
  return {
1549
- developer: join3(base, "dev-token.json"),
1550
- scoped: join3(base, "admin-token.local.json")
1616
+ developer: join4(base, "dev-token.json"),
1617
+ scoped: join4(base, "admin-token.local.json")
1551
1618
  };
1552
1619
  }
1553
1620
  function assertOperatorName(value2, label) {
@@ -1558,10 +1625,10 @@ function assertOperatorName(value2, label) {
1558
1625
  }
1559
1626
  }
1560
1627
  function readOperatorProfiles(file) {
1561
- if (!existsSync4(file)) return emptyProfiles();
1628
+ if (!existsSync5(file)) return emptyProfiles();
1562
1629
  let raw;
1563
1630
  try {
1564
- raw = JSON.parse(readFileSync3(file, "utf8"));
1631
+ raw = JSON.parse(readFileSync4(file, "utf8"));
1565
1632
  } catch {
1566
1633
  throw new Error(`operator context file ${file} is not valid JSON`);
1567
1634
  }
@@ -1627,19 +1694,19 @@ async function resolveOperatorContext(parsed, options = {}) {
1627
1694
  const configArgument = stringOpt(parsed.options.config) ?? "odla.config.mjs";
1628
1695
  const configPath = resolve4(configArgument);
1629
1696
  const explicitConfig = parsed.options.config !== void 0;
1630
- const hasConfig = existsSync5(configPath);
1697
+ const hasConfig = existsSync6(configPath);
1631
1698
  if (!hasConfig && (!options.allowMissingConfig || explicitConfig)) {
1632
1699
  await loadProjectConfig(configArgument);
1633
1700
  }
1634
1701
  const loaded = hasConfig ? await loadProjectConfig(configArgument) : void 0;
1635
1702
  const platformFlag = clean2(stringOpt(parsed.options.platform));
1636
- const platformEnvironment = clean2(process10.env.ODLA_PLATFORM_URL);
1703
+ const platformEnvironment = clean2(process11.env.ODLA_PLATFORM_URL);
1637
1704
  const platformValue = platformAudience(
1638
1705
  platformFlag ?? platformEnvironment ?? profile.value?.platform ?? loaded?.platformUrl ?? DEFAULT_PLATFORM2
1639
1706
  );
1640
1707
  const platformSource = platformFlag ? "flag" : platformEnvironment ? "environment" : profile.value ? "profile" : loaded ? "config" : "default";
1641
1708
  const appFlag = clean2(stringOpt(parsed.options.app));
1642
- const appEnvironment = clean2(process10.env.ODLA_APP_ID);
1709
+ const appEnvironment = clean2(process11.env.ODLA_APP_ID);
1643
1710
  const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
1644
1711
  const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
1645
1712
  if (appValue) assertOperatorName(appValue, "app");
@@ -1649,16 +1716,16 @@ async function resolveOperatorContext(parsed, options = {}) {
1649
1716
  );
1650
1717
  }
1651
1718
  const envFlag = clean2(stringOpt(parsed.options.env));
1652
- const envEnvironment = clean2(process10.env.ODLA_ENV);
1719
+ const envEnvironment = clean2(process11.env.ODLA_ENV);
1653
1720
  const environmentValue = envFlag ?? envEnvironment ?? profile.value?.environment ?? options.defaultEnvironment ?? null;
1654
1721
  const environmentSource = envFlag ? "flag" : envEnvironment ? "environment" : profile.value?.environment ? "profile" : options.defaultEnvironment ? "default" : "unresolved";
1655
1722
  if (environmentValue) {
1656
1723
  assertOperatorName(environmentValue, "environment");
1657
1724
  }
1658
- const rootDir = loaded?.rootDir ?? process10.cwd();
1725
+ const rootDir = loaded?.rootDir ?? process11.cwd();
1659
1726
  const profileCredentials = operatorCredentialFiles(profile);
1660
- 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;
1661
- 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;
1662
1729
  const cfg = loaded ? {
1663
1730
  ...loaded,
1664
1731
  platformUrl: platformValue,
@@ -1680,8 +1747,8 @@ async function resolveOperatorContext(parsed, options = {}) {
1680
1747
  services: [],
1681
1748
  local: {
1682
1749
  tokenFile,
1683
- credentialsFile: join4(rootDir, ".odla", "credentials.local.json"),
1684
- devVarsFile: join4(rootDir, ".dev.vars"),
1750
+ credentialsFile: join5(rootDir, ".odla", "credentials.local.json"),
1751
+ devVarsFile: join5(rootDir, ".dev.vars"),
1685
1752
  gitignore: true
1686
1753
  }
1687
1754
  };
@@ -1778,7 +1845,7 @@ async function adminCommand(parsed, deps = {}) {
1778
1845
  }
1779
1846
 
1780
1847
  // src/auth-command.ts
1781
- import process11 from "process";
1848
+ import process12 from "process";
1782
1849
 
1783
1850
  // src/whoami-command.ts
1784
1851
  var text2 = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
@@ -1940,7 +2007,7 @@ async function authCommand(parsed, deps = {}) {
1940
2007
  const { cfg } = context;
1941
2008
  const out = deps.stdout ?? console;
1942
2009
  const doFetch = deps.fetch ?? fetch;
1943
- 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();
1944
2011
  if (!email) {
1945
2012
  throw new Error(
1946
2013
  "auth login requires --email <odla-account> or ODLA_USER_EMAIL; confirm the signed-in odla email instead of using git or GitHub identity"
@@ -2093,7 +2160,7 @@ async function appExport(options) {
2093
2160
  }
2094
2161
 
2095
2162
  // src/app-import.ts
2096
- import { readFileSync as readFileSync4 } from "fs";
2163
+ import { readFileSync as readFileSync5 } from "fs";
2097
2164
  import {
2098
2165
  buildImportOps,
2099
2166
  parseImport,
@@ -2115,7 +2182,7 @@ async function appImport(options) {
2115
2182
  const out = options.stdout ?? console;
2116
2183
  const say = options.json ? (line2) => out.error(line2) : (line2) => out.log(line2);
2117
2184
  const { tenant } = resolveTenant(cfg, options.env);
2118
- 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");
2119
2186
  const { format, sources } = parseImport(text3, options.ns);
2120
2187
  if (format === "namespace-map" && options.ns) {
2121
2188
  throw new Error("--ns cannot be combined with a {namespace: rows} file \u2014 the file already names each namespace");
@@ -2986,7 +3053,7 @@ import {
2986
3053
  AppsError,
2987
3054
  createAppsClient
2988
3055
  } from "@odla-ai/apps";
2989
- import { join as join5 } from "path";
3056
+ import { join as join6 } from "path";
2990
3057
 
2991
3058
  // src/config-operation-error.ts
2992
3059
  var ConfigOperationCommandError = class extends Error {
@@ -3002,7 +3069,7 @@ var ConfigOperationCommandError = class extends Error {
3002
3069
  import {
3003
3070
  appServiceDefinition as appServiceDefinition2
3004
3071
  } from "@odla-ai/apps";
3005
- import { readFileSync as readFileSync5 } from "fs";
3072
+ import { readFileSync as readFileSync6 } from "fs";
3006
3073
 
3007
3074
  // src/config-reconcile-digest.ts
3008
3075
  import { createHash as createHash2 } from "crypto";
@@ -3038,7 +3105,7 @@ var SERVICE = /^[a-z][a-z0-9-]{0,39}$/;
3038
3105
  function readPlan(path) {
3039
3106
  let value2;
3040
3107
  try {
3041
- const raw = readFileSync5(path, "utf8");
3108
+ const raw = readFileSync6(path, "utf8");
3042
3109
  if (Buffer.byteLength(raw) > 128 * 1024) throw new Error("plan exceeds 128 KiB");
3043
3110
  value2 = JSON.parse(raw);
3044
3111
  } catch (error) {
@@ -3411,7 +3478,7 @@ async function operationClient(cfg, options, purpose) {
3411
3478
  platform: cfg.platformUrl,
3412
3479
  scope: "app:config:write",
3413
3480
  token: options.token,
3414
- tokenFile: join5(cfg.rootDir, ".odla", "admin-token.local.json"),
3481
+ tokenFile: join6(cfg.rootDir, ".odla", "admin-token.local.json"),
3415
3482
  rootDir: cfg.rootDir,
3416
3483
  email: options.email,
3417
3484
  open: options.open,
@@ -3466,7 +3533,7 @@ function record4(value2) {
3466
3533
 
3467
3534
  // src/config-reconcile-command.ts
3468
3535
  import { createAppsClient as createAppsClient2, studioAppSettingsPath } from "@odla-ai/apps";
3469
- import { join as join6 } from "path";
3536
+ import { join as join7 } from "path";
3470
3537
 
3471
3538
  // src/config-reconcile.ts
3472
3539
  import { appServiceIds as appServiceIds2, orderAppServices as orderAppServices2 } from "@odla-ai/apps";
@@ -3762,7 +3829,7 @@ async function inspectConfig(options) {
3762
3829
  platform: cfg.platformUrl,
3763
3830
  scope: "app:config:read",
3764
3831
  token: options.token,
3765
- tokenFile: join6(cfg.rootDir, ".odla", "admin-token.local.json"),
3832
+ tokenFile: join7(cfg.rootDir, ".odla", "admin-token.local.json"),
3766
3833
  rootDir: cfg.rootDir,
3767
3834
  email: options.email,
3768
3835
  open: options.open,
@@ -3894,13 +3961,13 @@ function quoteArg2(value2) {
3894
3961
 
3895
3962
  // src/doctor-checks.ts
3896
3963
  import { execFileSync } from "child_process";
3897
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
3898
- 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";
3899
3966
 
3900
3967
  // src/wrangler.ts
3901
3968
  import { spawn as spawn2 } from "child_process";
3902
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
3903
- import { join as join7 } from "path";
3969
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
3970
+ import { join as join8 } from "path";
3904
3971
  var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
3905
3972
  const child = spawn2(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
3906
3973
  let stdout = "";
@@ -3914,15 +3981,15 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) =>
3914
3981
  var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
3915
3982
  function findWranglerConfig(rootDir) {
3916
3983
  for (const name of WRANGLER_CONFIG_FILES) {
3917
- const path = join7(rootDir, name);
3918
- if (existsSync6(path)) return path;
3984
+ const path = join8(rootDir, name);
3985
+ if (existsSync7(path)) return path;
3919
3986
  }
3920
3987
  return null;
3921
3988
  }
3922
3989
  function readWranglerConfig(path) {
3923
3990
  if (path.endsWith(".toml")) return null;
3924
3991
  try {
3925
- return JSON.parse(stripJsonComments(readFileSync6(path, "utf8")));
3992
+ return JSON.parse(stripJsonComments(readFileSync7(path, "utf8")));
3926
3993
  } catch {
3927
3994
  return null;
3928
3995
  }
@@ -4075,7 +4142,7 @@ function wranglerWarnings(rootDir) {
4075
4142
  const dir = resolve6(rootDir, assets.directory);
4076
4143
  if (dir === resolve6(rootDir)) {
4077
4144
  warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
4078
- } else if (existsSync7(join8(dir, "node_modules"))) {
4145
+ } else if (existsSync8(join9(dir, "node_modules"))) {
4079
4146
  warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
4080
4147
  }
4081
4148
  }
@@ -4111,12 +4178,12 @@ function o11yProjectWarnings(rootDir) {
4111
4178
  return warnings;
4112
4179
  }
4113
4180
  const main = typeof config.main === "string" ? resolve6(rootDir, config.main) : null;
4114
- if (!main || !existsSync7(main)) {
4181
+ if (!main || !existsSync8(main)) {
4115
4182
  warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
4116
4183
  } else {
4117
4184
  let source = "";
4118
4185
  try {
4119
- source = readFileSync7(main, "utf8");
4186
+ source = readFileSync8(main, "utf8");
4120
4187
  } catch {
4121
4188
  }
4122
4189
  if (!/\bwithObservability\b/.test(source)) {
@@ -4140,7 +4207,7 @@ function calendarProjectWarnings(rootDir) {
4140
4207
  }
4141
4208
  function readPackageJson(rootDir) {
4142
4209
  try {
4143
- return JSON.parse(readFileSync7(join8(rootDir, "package.json"), "utf8"));
4210
+ return JSON.parse(readFileSync8(join9(rootDir, "package.json"), "utf8"));
4144
4211
  } catch {
4145
4212
  return null;
4146
4213
  }
@@ -4434,14 +4501,14 @@ function harnessOption(value2, flag) {
4434
4501
  }
4435
4502
 
4436
4503
  // src/init.ts
4437
- 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";
4438
4505
  import { dirname as dirname6, resolve as resolve7 } from "path";
4439
4506
  import { appServiceDefinition as appServiceDefinition3, appServiceIds as appServiceIds3 } from "@odla-ai/apps";
4440
4507
  function initProject(options) {
4441
4508
  const out = options.stdout ?? console;
4442
4509
  const rootDir = resolve7(options.rootDir ?? process.cwd());
4443
4510
  const configPath = resolve7(rootDir, options.configPath ?? "odla.config.mjs");
4444
- if (existsSync8(configPath) && !options.force) {
4511
+ if (existsSync9(configPath) && !options.force) {
4445
4512
  throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
4446
4513
  }
4447
4514
  if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
@@ -4469,7 +4536,7 @@ function initProject(options) {
4469
4536
  out.log("updated .gitignore for local odla credentials");
4470
4537
  }
4471
4538
  function writeIfMissing(path, text3) {
4472
- if (existsSync8(path)) return;
4539
+ if (existsSync9(path)) return;
4473
4540
  writeFileSync2(path, text3);
4474
4541
  }
4475
4542
  function configTemplate(input) {
@@ -4766,9 +4833,9 @@ function printReport(report5, out) {
4766
4833
  }
4767
4834
 
4768
4835
  // src/skill.ts
4769
- import { existsSync as existsSync9, lstatSync, mkdirSync as mkdirSync3, readFileSync as readFileSync8, readdirSync, writeFileSync as writeFileSync3 } from "fs";
4770
- import { homedir as homedir2 } from "os";
4771
- 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";
4772
4839
  import { fileURLToPath } from "url";
4773
4840
 
4774
4841
  // src/skill-adapters.ts
@@ -4868,7 +4935,7 @@ function installSkill(options = {}) {
4868
4935
  if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
4869
4936
  const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
4870
4937
  const root = resolve8(options.dir ?? process.cwd());
4871
- const home = resolve8(options.homeDir ?? homedir2());
4938
+ const home = resolve8(options.homeDir ?? homedir3());
4872
4939
  const plans = /* @__PURE__ */ new Map();
4873
4940
  const targets = /* @__PURE__ */ new Map();
4874
4941
  const rememberTarget = (harness, target) => {
@@ -4882,12 +4949,12 @@ function installSkill(options = {}) {
4882
4949
  plans.set(target, { target, content: content2, boundary, managedMerge });
4883
4950
  };
4884
4951
  const planSkillTree = (targetDir2, boundary = root) => {
4885
- 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);
4886
4953
  };
4887
4954
  let targetDir;
4888
4955
  if (options.global) {
4889
- const claudeRoot = join9(home, ".claude", "skills");
4890
- 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");
4891
4958
  targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
4892
4959
  for (const harness of harnesses) {
4893
4960
  const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
@@ -4895,35 +4962,35 @@ function installSkill(options = {}) {
4895
4962
  rememberTarget(harness, skillRoot);
4896
4963
  }
4897
4964
  } else {
4898
- const sharedRoot = join9(root, ".agents", "skills");
4965
+ const sharedRoot = join10(root, ".agents", "skills");
4899
4966
  planSkillTree(sharedRoot);
4900
- const claudeRoot = join9(root, ".claude", "skills");
4967
+ const claudeRoot = join10(root, ".claude", "skills");
4901
4968
  targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
4902
4969
  for (const harness of harnesses) rememberTarget(harness, sharedRoot);
4903
4970
  if (harnesses.includes("claude")) {
4904
4971
  for (const skill of skillNames(files)) {
4905
- const canonical2 = readFileSync8(join9(sourceDir, skill, "SKILL.md"), "utf8");
4906
- 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));
4907
4974
  }
4908
4975
  rememberTarget("claude", claudeRoot);
4909
4976
  }
4910
4977
  if (harnesses.includes("cursor")) {
4911
- const cursorRule = join9(root, ".cursor", "rules", "odla.mdc");
4978
+ const cursorRule = join10(root, ".cursor", "rules", "odla.mdc");
4912
4979
  plan(cursorRule, CURSOR_RULE);
4913
4980
  rememberTarget("cursor", cursorRule);
4914
4981
  }
4915
4982
  if (harnesses.includes("agents")) {
4916
- const agentsFile = join9(root, "AGENTS.md");
4983
+ const agentsFile = join10(root, "AGENTS.md");
4917
4984
  plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
4918
4985
  rememberTarget("agents", agentsFile);
4919
4986
  }
4920
4987
  if (harnesses.includes("copilot")) {
4921
- const copilotFile = join9(root, ".github", "copilot-instructions.md");
4988
+ const copilotFile = join10(root, ".github", "copilot-instructions.md");
4922
4989
  plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
4923
4990
  rememberTarget("copilot", copilotFile);
4924
4991
  }
4925
4992
  if (harnesses.includes("gemini")) {
4926
- const geminiFile = join9(root, "GEMINI.md");
4993
+ const geminiFile = join10(root, "GEMINI.md");
4927
4994
  plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
4928
4995
  rememberTarget("gemini", geminiFile);
4929
4996
  }
@@ -4937,11 +5004,11 @@ function installSkill(options = {}) {
4937
5004
  conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
4938
5005
  continue;
4939
5006
  }
4940
- if (!existsSync9(file.target)) {
5007
+ if (!existsSync10(file.target)) {
4941
5008
  writtenPaths.add(file.target);
4942
5009
  continue;
4943
5010
  }
4944
- const current = readFileSync8(file.target, "utf8");
5011
+ const current = readFileSync9(file.target, "utf8");
4945
5012
  if (current === file.content) {
4946
5013
  unchangedPaths.add(file.target);
4947
5014
  } else if (file.managedMerge || options.force) {
@@ -4958,7 +5025,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
4958
5025
  );
4959
5026
  }
4960
5027
  for (const file of plans.values()) {
4961
- if (!existsSync9(file.target) || readFileSync8(file.target, "utf8") !== file.content) {
5028
+ if (!existsSync10(file.target) || readFileSync9(file.target, "utf8") !== file.content) {
4962
5029
  mkdirSync3(dirname7(file.target), { recursive: true });
4963
5030
  writeFileSync3(file.target, file.content);
4964
5031
  }
@@ -5001,9 +5068,9 @@ function normalizeHarnesses(values, global) {
5001
5068
  function managedFileContent(path, block, force, boundary) {
5002
5069
  const symlink = symlinkedComponent(boundary, path);
5003
5070
  if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
5004
- if (!existsSync9(path)) return `${block}
5071
+ if (!existsSync10(path)) return `${block}
5005
5072
  `;
5006
- const current = readFileSync8(path, "utf8");
5073
+ const current = readFileSync9(path, "utf8");
5007
5074
  const start = "<!-- odla-ai agent setup:start -->";
5008
5075
  const end = "<!-- odla-ai agent setup:end -->";
5009
5076
  const startAt = current.indexOf(start);
@@ -5030,7 +5097,7 @@ function symlinkedComponent(boundary, target) {
5030
5097
  }
5031
5098
  let current = boundary;
5032
5099
  for (const part of rel.split(sep).filter(Boolean)) {
5033
- current = join9(current, part);
5100
+ current = join10(current, part);
5034
5101
  try {
5035
5102
  if (lstatSync(current).isSymbolicLink()) return current;
5036
5103
  } catch (error) {
@@ -5043,11 +5110,11 @@ function skillNames(files) {
5043
5110
  return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
5044
5111
  }
5045
5112
  function listFiles(dir) {
5046
- if (!existsSync9(dir)) return [];
5113
+ if (!existsSync10(dir)) return [];
5047
5114
  const results = [];
5048
5115
  const walk = (current) => {
5049
5116
  for (const entry of readdirSync(current, { withFileTypes: true })) {
5050
- const path = join9(current, entry.name);
5117
+ const path = join10(current, entry.name);
5051
5118
  if (entry.isDirectory()) walk(path);
5052
5119
  else results.push(relative2(dir, path));
5053
5120
  }
@@ -5404,7 +5471,7 @@ async function projectCommand(command, parsed, deps) {
5404
5471
  }
5405
5472
 
5406
5473
  // src/code-connect.ts
5407
- import { existsSync as existsSync10 } from "fs";
5474
+ import { existsSync as existsSync11 } from "fs";
5408
5475
  import { cpus, hostname, totalmem } from "os";
5409
5476
  import { resolve as resolve11 } from "path";
5410
5477
 
@@ -5415,7 +5482,7 @@ var HARNESS_PROTOCOL_VERSION = 1;
5415
5482
  import { execFile, spawn as spawn3 } from "child_process";
5416
5483
  import { constants } from "fs";
5417
5484
  import { access } from "fs/promises";
5418
- import { delimiter, join as join10 } from "path";
5485
+ import { delimiter, join as join11 } from "path";
5419
5486
  import { getgid, getuid } from "process";
5420
5487
  import { mkdir as mkdir2, mkdtemp, realpath, rm, writeFile as writeFile2 } from "fs/promises";
5421
5488
  import { tmpdir } from "os";
@@ -5433,7 +5500,7 @@ function assertPinnedImage(image) {
5433
5500
  async function commandAvailable(engine) {
5434
5501
  for (const directory of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
5435
5502
  try {
5436
- await access(join10(directory, engine), constants.X_OK);
5503
+ await access(join11(directory, engine), constants.X_OK);
5437
5504
  return true;
5438
5505
  } catch {
5439
5506
  }
@@ -6131,7 +6198,7 @@ import { randomUUID } from "crypto";
6131
6198
  import { createHash as createHash22, randomUUID as randomUUID2 } from "crypto";
6132
6199
  import { createReadStream } from "fs";
6133
6200
  import { lstat as lstat22 } from "fs/promises";
6134
- import { join as join12 } from "path";
6201
+ import { join as join13 } from "path";
6135
6202
  import { mkdir as mkdir3, mkdtemp as mkdtemp3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
6136
6203
  import { tmpdir as tmpdir3 } from "os";
6137
6204
  import { dirname as dirname9, join as join23, resolve as resolve32, sep as sep23 } from "path";
@@ -6518,8 +6585,8 @@ function rollup(graph, kind, options = {}) {
6518
6585
  for (const node of nodesOfKind(graph, kind)) {
6519
6586
  if (options.prefix && !node.name.startsWith(options.prefix)) continue;
6520
6587
  const key = node.name.split(separator).slice(0, depth).join(separator);
6521
- const list2 = groups.get(key);
6522
- if (list2) list2.push(node);
6588
+ const list3 = groups.get(key);
6589
+ if (list3) list3.push(node);
6523
6590
  else groups.set(key, [node]);
6524
6591
  }
6525
6592
  return [...groups].map(([prefix, nodes]) => ({
@@ -6534,7 +6601,7 @@ function dirname8(path) {
6534
6601
  const at = path.lastIndexOf("/");
6535
6602
  return at <= 0 ? "." : path.slice(0, at);
6536
6603
  }
6537
- function join11(base, specifier) {
6604
+ function join12(base, specifier) {
6538
6605
  const parts = [];
6539
6606
  const segments = `${base === "." ? "" : `${base}/`}${specifier}`.split("/");
6540
6607
  for (const segment of segments) {
@@ -6558,7 +6625,7 @@ var BARE_IMPORT = /^\s*import\s*["']([^"']+)["']/gm;
6558
6625
  var isSourcePath = (path) => SOURCE.test(path);
6559
6626
  function resolveImport(fromPath, specifier, known) {
6560
6627
  if (!specifier.startsWith(".")) return null;
6561
- const base = join11(dirname8(fromPath), specifier);
6628
+ const base = join12(dirname8(fromPath), specifier);
6562
6629
  const candidates = [
6563
6630
  base,
6564
6631
  base.replace(/\.js$/, ".ts"),
@@ -7361,7 +7428,7 @@ async function inspectArtifacts(workspaceDir, recipe2) {
7361
7428
  const receipts = [];
7362
7429
  for (const artifact of recipe2.expectedArtifacts ?? []) {
7363
7430
  try {
7364
- const path = join12(workspaceDir, artifact.path);
7431
+ const path = join13(workspaceDir, artifact.path);
7365
7432
  const info = await lstat22(path);
7366
7433
  if (!info.isFile() || info.isSymbolicLink()) {
7367
7434
  receipts.push({ artifactId: artifact.id, status: "invalid", bytes: null, digest: null });
@@ -9457,7 +9524,7 @@ var CODE_BUILD_RECIPES = Object.freeze([{
9457
9524
  async function codeConnect(options) {
9458
9525
  const cwd = options.cwd ?? process.cwd();
9459
9526
  const configPath = resolve11(cwd, options.configPath);
9460
- const cfg = existsSync10(configPath) ? await loadProjectConfig(configPath) : null;
9527
+ const cfg = existsSync11(configPath) ? await loadProjectConfig(configPath) : null;
9461
9528
  const requestedAppId = options.appId?.trim();
9462
9529
  if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
9463
9530
  throw new Error("--app-id must be a valid odla app id");
@@ -9891,13 +9958,13 @@ async function codeCommand(parsed, dependencies) {
9891
9958
  }
9892
9959
 
9893
9960
  // src/operator-credentials.ts
9894
- import process12 from "process";
9961
+ import process13 from "process";
9895
9962
  function developerTokenStatus(context, parsed, now = Date.now()) {
9896
9963
  const cached = readJsonFile(context.cfg.local.tokenFile);
9897
9964
  const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
9898
9965
  const source = clean3(
9899
9966
  stringOpt(parsed.options.token)
9900
- ) ? "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";
9901
9968
  return {
9902
9969
  source,
9903
9970
  cacheFile: context.cfg.local.tokenFile,
@@ -10219,6 +10286,9 @@ Usage:
10219
10286
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
10220
10287
  odla-ai security run [target] --self --ack-redacted-source
10221
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]
10222
10292
  odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
10223
10293
  odla-ai credentials revoke <receipt-id> [--config odla.config.mjs] [--json]
10224
10294
  odla-ai smoke [--config odla.config.mjs] [--env dev] [--runtime] [--email <odla-account>] [--no-open]
@@ -10324,6 +10394,10 @@ Commands:
10324
10394
  stable status, incident, and report JSON to agents and CI.
10325
10395
  platform Read canonical fleet health, releases, provider load/freshness,
10326
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.
10327
10401
  provision Register services, compose integrations, persist credentials, optionally push secrets.
10328
10402
  "provision --live --yes" initializes only the live instance of
10329
10403
  an existing sandbox app and enables every configured service;
@@ -12606,7 +12680,7 @@ function percent(value2) {
12606
12680
  // src/provision.ts
12607
12681
  import { AppsError as AppsError2, createAppsClient as createAppsClient3, orderAppServices as orderAppServices3, tenantIdFor as tenantIdFor6 } from "@odla-ai/apps";
12608
12682
  import { putSecret as putSecret2 } from "@odla-ai/ai";
12609
- import process13 from "process";
12683
+ import process14 from "process";
12610
12684
 
12611
12685
  // src/integration-provision.ts
12612
12686
  import { uuidv7 } from "@odla-ai/db";
@@ -13042,7 +13116,7 @@ async function provision(options) {
13042
13116
  await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
13043
13117
  }
13044
13118
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
13045
- const key = process13.env[cfg.ai.keyEnv];
13119
+ const key = process14.env[cfg.ai.keyEnv];
13046
13120
  if (key) {
13047
13121
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
13048
13122
  await putSecret2({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -13084,7 +13158,7 @@ async function provision(options) {
13084
13158
 
13085
13159
  // src/record.ts
13086
13160
  import { appendFileSync } from "fs";
13087
- import process14 from "process";
13161
+ import process15 from "process";
13088
13162
 
13089
13163
  // src/surface.ts
13090
13164
  var PM_ACTIONS = {
@@ -13154,6 +13228,7 @@ var COMMAND_SURFACE = {
13154
13228
  config: { diff: {}, plan: {}, apply: {} },
13155
13229
  context: { show: {}, list: {}, save: {}, remove: {} },
13156
13230
  credentials: { list: {}, revoke: {} },
13231
+ device: { enroll: {}, list: {}, revoke: {} },
13157
13232
  // `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
13158
13233
  discuss: {
13159
13234
  groups: {},
@@ -13265,7 +13340,7 @@ function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
13265
13340
 
13266
13341
  // src/record.ts
13267
13342
  function recordInvocation(parsed) {
13268
- const file = process14.env.ODLA_CLI_RECORD;
13343
+ const file = process15.env.ODLA_CLI_RECORD;
13269
13344
  if (!file) return;
13270
13345
  try {
13271
13346
  const entry = {
@@ -13279,8 +13354,132 @@ function recordInvocation(parsed) {
13279
13354
  }
13280
13355
  }
13281
13356
 
13357
+ // src/advisory-output.ts
13358
+ import { formatAdvisory, parseAdvisories } from "@odla-ai/apps";
13359
+ function advisoryCollectingFetch(inner, sink) {
13360
+ return (async (input, init) => {
13361
+ const response2 = await inner(input, init);
13362
+ try {
13363
+ sink.push(...parseAdvisories(response2));
13364
+ } catch {
13365
+ }
13366
+ return response2;
13367
+ });
13368
+ }
13369
+ function renderAdvisories(out, advisories, env = process.env) {
13370
+ if (env.ODLA_NO_ADVISORIES) return;
13371
+ const seen = /* @__PURE__ */ new Set();
13372
+ for (const advisory of advisories) {
13373
+ const key = `${advisory.code}:${advisory.message}`;
13374
+ if (seen.has(key)) continue;
13375
+ seen.add(key);
13376
+ out.error(formatAdvisory(advisory));
13377
+ }
13378
+ }
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
+
13282
13481
  // src/runbook-actions.ts
13283
- import { readFileSync as readFileSync9 } from "fs";
13482
+ import { readFileSync as readFileSync10 } from "fs";
13284
13483
  var PLATFORM_SCOPE = "$platform";
13285
13484
  async function call(ctx, method, path, body) {
13286
13485
  const res = await ctx.doFetch(`${ctx.platformUrl.replace(/\/$/, "")}/registry/pm${path}`, {
@@ -13327,7 +13526,7 @@ async function bySlug(ctx, slug) {
13327
13526
  function readBody(file, inline) {
13328
13527
  if (inline !== void 0) return inline;
13329
13528
  if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
13330
- return readFileSync9(file === "-" ? 0 : file, "utf8");
13529
+ return readFileSync10(file === "-" ? 0 : file, "utf8");
13331
13530
  }
13332
13531
  var stamp = (ms) => ms ? new Date(ms).toISOString().slice(0, 16).replace("T", " ") : "";
13333
13532
  async function runbookList(ctx, all, query) {
@@ -13419,8 +13618,8 @@ async function runbookRemove(ctx, slug) {
13419
13618
  }
13420
13619
 
13421
13620
  // src/runbook-import.ts
13422
- import { readFileSync as readFileSync10, readdirSync as readdirSync2, statSync } from "fs";
13423
- 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";
13424
13623
  function parseRunbook(text3, slug) {
13425
13624
  let rest = text3;
13426
13625
  const meta = {};
@@ -13450,7 +13649,7 @@ function readRunbookDir(dir) {
13450
13649
  if (!files.length) throw new Error(`no .md files in ${dir}`);
13451
13650
  return files.map((file) => {
13452
13651
  const slug = basename2(file, ".md");
13453
- const parsed = parseRunbook(readFileSync10(join13(dir, file), "utf8"), slug);
13652
+ const parsed = parseRunbook(readFileSync11(join14(dir, file), "utf8"), slug);
13454
13653
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
13455
13654
  });
13456
13655
  }
@@ -13523,8 +13722,8 @@ async function upsert(ctx, r, visibility) {
13523
13722
 
13524
13723
  // src/runbook-impact.ts
13525
13724
  import { execFileSync as execFileSync2 } from "child_process";
13526
- import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
13527
- import { join as join14 } from "path";
13725
+ import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
13726
+ import { join as join15 } from "path";
13528
13727
 
13529
13728
  // src/runbook-impact-scan.ts
13530
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$]*)/;
@@ -13693,10 +13892,10 @@ ${body.split("\n").map((line2) => `+${line2}`).join("\n")}
13693
13892
  }
13694
13893
  function manifestLabeller(root) {
13695
13894
  return (workspace) => {
13696
- const manifest = join14(root, workspace, "package.json");
13697
- if (!existsSync11(manifest)) return void 0;
13895
+ const manifest = join15(root, workspace, "package.json");
13896
+ if (!existsSync12(manifest)) return void 0;
13698
13897
  try {
13699
- const name = JSON.parse(readFileSync11(manifest, "utf8")).name;
13898
+ const name = JSON.parse(readFileSync12(manifest, "utf8")).name;
13700
13899
  return typeof name === "string" ? name : void 0;
13701
13900
  } catch {
13702
13901
  return void 0;
@@ -13763,7 +13962,7 @@ function report4(ctx, impacts) {
13763
13962
  async function runbookImpact(ctx, options, deps = {}) {
13764
13963
  const cwd = deps.cwd ?? process.cwd();
13765
13964
  const runGit = deps.runGit ?? gitRunner(cwd);
13766
- const read3 = deps.readRepoFile ?? ((path) => readFileSync11(join14(cwd, path), "utf8"));
13965
+ const read3 = deps.readRepoFile ?? ((path) => readFileSync12(join15(cwd, path), "utf8"));
13767
13966
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
13768
13967
  if (!surfaces.length) {
13769
13968
  return ctx.out.log(
@@ -13896,12 +14095,12 @@ async function runbookComment(ctx, slug, body) {
13896
14095
 
13897
14096
  // src/runbook-editor.ts
13898
14097
  import { spawnSync } from "child_process";
13899
- import { mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
14098
+ import { mkdtempSync, readFileSync as readFileSync13, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
13900
14099
  import { tmpdir as tmpdir4 } from "os";
13901
- import { join as join15 } from "path";
13902
- import process15 from "process";
14100
+ import { join as join16 } from "path";
14101
+ import process17 from "process";
13903
14102
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
13904
- function resolveEditor(env = process15.env) {
14103
+ function resolveEditor(env = process17.env) {
13905
14104
  for (const name of EDITOR_ENV) {
13906
14105
  const value2 = env[name];
13907
14106
  if (value2 && value2.trim()) return value2.trim();
@@ -13915,8 +14114,8 @@ function defaultRun(command, path) {
13915
14114
  return result.status ?? 0;
13916
14115
  }
13917
14116
  function editText(initial, slug, deps = {}) {
13918
- const env = deps.env ?? process15.env;
13919
- const interactive = deps.interactive ?? (() => Boolean(process15.stdin.isTTY));
14117
+ const env = deps.env ?? process17.env;
14118
+ const interactive = deps.interactive ?? (() => Boolean(process17.stdin.isTTY));
13920
14119
  const editor = resolveEditor(env);
13921
14120
  if (!editor)
13922
14121
  throw new Error(
@@ -13924,16 +14123,16 @@ function editText(initial, slug, deps = {}) {
13924
14123
  );
13925
14124
  if (!interactive())
13926
14125
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
13927
- const dir = mkdtempSync(join15(tmpdir4(), "odla-runbook-"));
13928
- const file = join15(dir, `${slug}.md`);
14126
+ const dir = mkdtempSync(join16(tmpdir4(), "odla-runbook-"));
14127
+ const file = join16(dir, `${slug}.md`);
13929
14128
  try {
13930
- writeFileSync4(file, initial, { mode: 384 });
14129
+ writeFileSync5(file, initial, { mode: 384 });
13931
14130
  const code = defaultRunOrInjected(deps)(editor, file);
13932
14131
  if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
13933
- const edited = readFileSync12(file, "utf8");
14132
+ const edited = readFileSync13(file, "utf8");
13934
14133
  return edited === initial ? null : edited;
13935
14134
  } finally {
13936
- rmSync2(dir, { recursive: true, force: true });
14135
+ rmSync3(dir, { recursive: true, force: true });
13937
14136
  }
13938
14137
  }
13939
14138
  var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
@@ -14821,6 +15020,23 @@ async function securityStatus(parsed, dependencies) {
14821
15020
 
14822
15021
  // src/cli.ts
14823
15022
  async function runCli(argv = process.argv.slice(2), dependencies = {}) {
15023
+ const out = redactingOutput(dependencies.stdout ?? console);
15024
+ const advisories = [];
15025
+ const withAdvisoryReader = {
15026
+ ...dependencies,
15027
+ fetch: advisoryCollectingFetch(dependencies.fetch ?? fetch, advisories)
15028
+ };
15029
+ try {
15030
+ return await dispatchCli(argv, withAdvisoryReader);
15031
+ } catch (error) {
15032
+ const explanation = explainRejectedCredential(error);
15033
+ if (explanation) out.error(explanation);
15034
+ throw error;
15035
+ } finally {
15036
+ renderAdvisories(out, advisories);
15037
+ }
15038
+ }
15039
+ async function dispatchCli(argv, dependencies) {
14824
15040
  const runtime = {
14825
15041
  ...dependencies,
14826
15042
  stdout: redactingOutput(dependencies.stdout ?? console)
@@ -14850,6 +15066,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
14850
15066
  await contextCommand(parsed, runtime);
14851
15067
  return;
14852
15068
  }
15069
+ if (command === "device") {
15070
+ await deviceCommand(parsed, runtime);
15071
+ return;
15072
+ }
14853
15073
  if (command === "credentials") {
14854
15074
  await credentialCommand(parsed, runtime);
14855
15075
  return;
@@ -15045,4 +15265,4 @@ export {
15045
15265
  isTerminalHostedSecurityStatus,
15046
15266
  runCli
15047
15267
  };
15048
- //# sourceMappingURL=chunk-SRL2TN24.js.map
15268
+ //# sourceMappingURL=chunk-DBZQIMES.js.map