@odla-ai/cli 0.38.2 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -97,12 +97,12 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
97
97
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
98
98
 
99
99
  // src/admin-ai.ts
100
- var import_node_process8 = __toESM(require("process"), 1);
100
+ var import_node_process12 = __toESM(require("process"), 1);
101
101
 
102
102
  // src/token.ts
103
103
  var import_db = require("@odla-ai/db");
104
104
  var import_node_crypto = require("crypto");
105
- var import_node_process5 = __toESM(require("process"), 1);
105
+ var import_node_process9 = __toESM(require("process"), 1);
106
106
 
107
107
  // src/handshake-approval.ts
108
108
  var import_node_process2 = __toESM(require("process"), 1);
@@ -237,30 +237,13 @@ function handshakeWaitMs(waitSeconds, interactive = import_node_process3.default
237
237
  }
238
238
 
239
239
  // src/cached-credential.ts
240
- var import_node_fs2 = require("fs");
241
- var noted = null;
242
- function noteCachedCredential(tokenFile) {
243
- noted = tokenFile;
244
- }
245
- function isCredentialRejection(error) {
246
- const message2 = error instanceof Error ? error.message : String(error ?? "");
247
- return /\((401|403)\)\s*$/.test(message2.trim());
248
- }
249
- function explainRejectedCredential(error) {
250
- const tokenFile = noted;
251
- if (!tokenFile || !isCredentialRejection(error)) return null;
252
- noted = null;
253
- (0, import_node_fs2.rmSync)(tokenFile, { force: true });
254
- return [
255
- "auth: the cached credential was rejected by odla, so it was revoked before its cached expiry.",
256
- " The usual cause is a newer sign-in for this project: collecting a handshake retires the",
257
- " principal's other credentials, so a second terminal or worktree supersedes this one.",
258
- ` Discarded ${tokenFile}; re-run this command to request a fresh approval.`
259
- ].join("\n");
260
- }
240
+ var import_node_fs5 = require("fs");
241
+
242
+ // src/auth-guidance.ts
243
+ var import_node_process6 = __toESM(require("process"), 1);
261
244
 
262
245
  // src/device-session.ts
263
- var import_node_fs3 = require("fs");
246
+ var import_node_fs2 = require("fs");
264
247
  var import_node_os = require("os");
265
248
  var import_node_path2 = require("path");
266
249
  var import_node_process4 = __toESM(require("process"), 1);
@@ -269,9 +252,9 @@ function deviceCredentialPath(env = import_node_process4.default.env) {
269
252
  }
270
253
  function readDeviceCredential(platform, env = import_node_process4.default.env) {
271
254
  const path = deviceCredentialPath(env);
272
- if (!(0, import_node_fs3.existsSync)(path)) return null;
255
+ if (!(0, import_node_fs2.existsSync)(path)) return null;
273
256
  try {
274
- const parsed = JSON.parse((0, import_node_fs3.readFileSync)(path, "utf8"));
257
+ const parsed = JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
275
258
  if (typeof parsed.token !== "string" || !parsed.token.startsWith("odla_device_")) return null;
276
259
  if (parsed.platform !== platform) return null;
277
260
  return { ...parsed, token: parsed.token, platform: parsed.platform };
@@ -293,12 +276,71 @@ async function mintDeviceSession(platformUrl, credential2, doFetch) {
293
276
  `device session failed: ${detail} (${response2.status})` + (revocable ? " \u2014 if this machine's enrollment was revoked or has expired, enroll it again in Studio" : "")
294
277
  );
295
278
  }
296
- return { token: body.token, expiresAt: body.expiresAt ?? Date.now() };
279
+ return {
280
+ token: body.token,
281
+ expiresAt: body.expiresAt ?? Date.now(),
282
+ // Absent from a registry that predates rolling expiry and scoped devices.
283
+ // Left undefined rather than defaulted, so a caller can tell "the platform
284
+ // did not say" from "the platform said none".
285
+ ...typeof body.deviceExpiresAt === "number" ? { deviceExpiresAt: body.deviceExpiresAt } : {},
286
+ ...Array.isArray(body.appIds) ? { appIds: body.appIds } : {},
287
+ ...Array.isArray(body.capabilities) ? { capabilities: body.capabilities } : {},
288
+ ...Array.isArray(body.scopes) ? { scopes: body.scopes } : {}
289
+ };
290
+ }
291
+
292
+ // src/odla-home.ts
293
+ var import_node_fs3 = require("fs");
294
+ var import_node_os2 = require("os");
295
+ var import_node_path3 = require("path");
296
+ var import_node_process5 = __toESM(require("process"), 1);
297
+ function odlaHome(env = import_node_process5.default.env) {
298
+ return env.ODLA_HOME ?? (0, import_node_path3.join)(env.HOME ?? (0, import_node_os2.homedir)(), ".odla");
299
+ }
300
+ function odlaHomePath(segments, env = import_node_process5.default.env) {
301
+ return (0, import_node_path3.join)(odlaHome(env), ...segments);
302
+ }
303
+ function identityFile(env) {
304
+ return odlaHomePath(["identity.json"], env);
305
+ }
306
+ function deviceSessionFile(env) {
307
+ return odlaHomePath(["session.json"], env);
308
+ }
309
+ function appTokenFile(appId, env) {
310
+ return odlaHomePath(["apps", safeSegment(appId), "dev-token.json"], env);
311
+ }
312
+ function appCredentialsFile(appId, env) {
313
+ return odlaHomePath(["apps", safeSegment(appId), "credentials.json"], env);
314
+ }
315
+ function scopedTokenFile(env) {
316
+ return odlaHomePath(["admin-token.local.json"], env);
317
+ }
318
+ function pmContextFile(env) {
319
+ return odlaHomePath(["pm-context.json"], env);
320
+ }
321
+ function adoptRepoLocalCache(legacyPath, machinePath, out) {
322
+ if (!(0, import_node_fs3.existsSync)(legacyPath) || legacyPath === machinePath) return false;
323
+ const superseded = (0, import_node_fs3.existsSync)(machinePath);
324
+ if (!superseded) {
325
+ (0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(machinePath), { recursive: true });
326
+ (0, import_node_fs3.copyFileSync)(legacyPath, machinePath);
327
+ (0, import_node_fs3.chmodSync)(machinePath, 384);
328
+ }
329
+ (0, import_node_fs3.rmSync)(legacyPath, { force: true });
330
+ out?.error(
331
+ superseded ? `auth: removed superseded ${legacyPath}; this machine's credentials live in ${odlaHome()}` : `auth: moved ${legacyPath} into ${machinePath}; credentials are per machine now, not per worktree`
332
+ );
333
+ return true;
334
+ }
335
+ function safeSegment(value2) {
336
+ const clean4 = value2.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "");
337
+ if (!clean4) throw new Error(`"${value2}" is not a usable app id`);
338
+ return clean4;
297
339
  }
298
340
 
299
341
  // src/local.ts
300
342
  var import_node_fs4 = require("fs");
301
- var import_node_path3 = require("path");
343
+ var import_node_path4 = require("path");
302
344
  var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
303
345
  function readJsonFile(path) {
304
346
  try {
@@ -345,7 +387,7 @@ function mergeCredential(current, update) {
345
387
  return next;
346
388
  }
347
389
  function ensureGitignore(rootDir, localPaths = []) {
348
- const path = (0, import_node_path3.resolve)(rootDir, ".gitignore");
390
+ const path = (0, import_node_path4.resolve)(rootDir, ".gitignore");
349
391
  const existing = (0, import_node_fs4.existsSync)(path) ? (0, import_node_fs4.readFileSync)(path, "utf8") : "";
350
392
  const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line2) => !!line2);
351
393
  const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
@@ -366,7 +408,7 @@ function o11yDevVars(cfg) {
366
408
  function resolveWriteDevVarsTarget(cfg, requested) {
367
409
  if (!requested) return null;
368
410
  if (requested === true) return cfg.local.devVarsFile;
369
- return (0, import_node_path3.resolve)((0, import_node_path3.dirname)(cfg.configPath), requested);
411
+ return (0, import_node_path4.resolve)((0, import_node_path4.dirname)(cfg.configPath), requested);
370
412
  }
371
413
  function writeDevVars(path, credentials, env, o11y) {
372
414
  const entry = credentials.envs[env];
@@ -406,22 +448,110 @@ function isManagedDevVar(line2) {
406
448
  return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
407
449
  }
408
450
  function writePrivateText(path, text3) {
409
- (0, import_node_fs4.mkdirSync)((0, import_node_path3.dirname)(path), { recursive: true });
451
+ (0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(path), { recursive: true });
410
452
  const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
411
453
  (0, import_node_fs4.writeFileSync)(temporary, text3, { mode: 384 });
412
454
  (0, import_node_fs4.chmodSync)(temporary, 384);
413
455
  (0, import_node_fs4.renameSync)(temporary, path);
414
456
  }
415
457
  function gitignoreEntry(rootDir, path) {
416
- const rel = (0, import_node_path3.relative)((0, import_node_path3.resolve)(rootDir), (0, import_node_path3.resolve)(path));
417
- if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path3.isAbsolute)(rel)) return null;
458
+ const rel = (0, import_node_path4.relative)((0, import_node_path4.resolve)(rootDir), (0, import_node_path4.resolve)(path));
459
+ if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path4.isAbsolute)(rel)) return null;
418
460
  return rel.replaceAll("\\", "/");
419
461
  }
420
462
  function displayPath(path, rootDir = process.cwd()) {
421
- const rel = (0, import_node_path3.relative)(rootDir, path);
463
+ const rel = (0, import_node_path4.relative)(rootDir, path);
422
464
  return rel && !rel.startsWith("..") ? rel : path;
423
465
  }
424
466
 
467
+ // src/auth-guidance.ts
468
+ var ENROL_EVERYTHING = "npx odla-ai device enroll --all-apps --capability all --no-open --wait 600";
469
+ var ENROL_PLATFORM_WIDE = "npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600";
470
+ function machineAuthState(audience, env = import_node_process6.default.env) {
471
+ const device = readDeviceCredential(audience, env);
472
+ if (!device) return { enrolled: false };
473
+ const session = readJsonFile(deviceSessionFile(env));
474
+ const current = session?.platform === audience && session.deviceId === device.deviceId ? session : void 0;
475
+ return {
476
+ enrolled: true,
477
+ ...device.name ? { deviceName: device.name } : {},
478
+ ...current?.appIds ? { appIds: current.appIds } : {},
479
+ ...current?.capabilities ? { capabilities: current.capabilities } : {},
480
+ ...current?.scopes ? { scopes: current.scopes } : {},
481
+ ...current?.deviceExpiresAt ? { lapsesAt: current.deviceExpiresAt } : {}
482
+ };
483
+ }
484
+ function scopeInterruptionNotice(scope, state2) {
485
+ const platformScope = scope.startsWith("platform:");
486
+ const reason = !state2.enrolled ? "this machine is not enrolled, so every privileged command needs its own browser approval" : `this machine is enrolled but its approval did not include "${scope}"`;
487
+ return [
488
+ `odla: ${reason}.`,
489
+ ` Approve this one now, then end the interruptions with:`,
490
+ ` ${platformScope ? ENROL_PLATFORM_WIDE : ENROL_EVERYTHING}`,
491
+ platformScope ? " A platform scope needs an administrator's approval; an app owner's cannot carry it." : " One approval, every app you own, and it rolls forward while you keep working."
492
+ ].join("\n");
493
+ }
494
+ function lapseNotice(state2, now = Date.now()) {
495
+ if (!state2.enrolled || !state2.lapsesAt) return null;
496
+ const days = Math.floor((state2.lapsesAt - now) / (24 * 60 * 60 * 1e3));
497
+ if (days < 0) return "this machine's enrollment has lapsed; the next command will ask for approval";
498
+ return `idle for ${days} more day${days === 1 ? "" : "s"} before this machine needs approving again (using it resets the clock)`;
499
+ }
500
+
501
+ // src/cached-credential.ts
502
+ var noted = null;
503
+ function noteCachedCredential(tokenFile) {
504
+ noted = tokenFile;
505
+ }
506
+ function isCredentialRejection(error) {
507
+ const message2 = error instanceof Error ? error.message : String(error ?? "");
508
+ return /\((401|403)\)\s*$/.test(message2.trim());
509
+ }
510
+ function explainRejectedCredential(error) {
511
+ const tokenFile = noted;
512
+ if (!tokenFile || !isCredentialRejection(error)) return null;
513
+ noted = null;
514
+ (0, import_node_fs5.rmSync)(tokenFile, { force: true });
515
+ return [
516
+ "auth: the cached credential was rejected by odla, so it was revoked before its cached expiry.",
517
+ " The usual cause is a newer sign-in for this account: collecting a handshake retires the",
518
+ " principal's other collected credentials, so a second machine supersedes this one.",
519
+ ` Discarded ${tokenFile}; re-run this command to request a fresh approval.`,
520
+ ` To stop needing one: ${ENROL_EVERYTHING}`
521
+ ].join("\n");
522
+ }
523
+
524
+ // src/device-session-cache.ts
525
+ var import_node_process7 = __toESM(require("process"), 1);
526
+ var SKEW_MS = 6e4;
527
+ async function deviceSessionToken(platformUrl, audience, credential2, doFetch, env = import_node_process7.default.env) {
528
+ const path = deviceSessionFile(env);
529
+ const cached = readJsonFile(path);
530
+ if (cached?.token && cached.platform === audience && cached.deviceId === credential2.deviceId && (cached.expiresAt ?? 0) > Date.now() + SKEW_MS) return cached;
531
+ const minted = await mintDeviceSession(platformUrl, credential2, doFetch);
532
+ const session = {
533
+ ...minted,
534
+ platform: audience,
535
+ ...credential2.deviceId ? { deviceId: credential2.deviceId } : {}
536
+ };
537
+ writePrivateJson(path, session);
538
+ return session;
539
+ }
540
+
541
+ // src/machine-identity.ts
542
+ var import_node_process8 = __toESM(require("process"), 1);
543
+ function readMachineIdentity(audience, env = import_node_process8.default.env) {
544
+ const stored = readJsonFile(identityFile(env));
545
+ if (!stored || typeof stored.email !== "string" || !stored.email) return null;
546
+ return stored.platform === audience ? { platform: audience, email: stored.email } : null;
547
+ }
548
+ function rememberMachineIdentity(audience, email, env = import_node_process8.default.env) {
549
+ if (!email) return;
550
+ const existing = readMachineIdentity(audience, env);
551
+ if (existing?.email === email) return;
552
+ writePrivateJson(identityFile(env), { platform: audience, email });
553
+ }
554
+
425
555
  // src/token.ts
426
556
  async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {}) {
427
557
  const audience = platformAudience(cfg.platformUrl);
@@ -430,19 +560,19 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
430
560
  const cached = readJsonFile(cfg.local.tokenFile);
431
561
  if (!grantRequest.forceReview && !grantRequest.freshLogin) {
432
562
  if (options.token) return options.token;
433
- if (import_node_process5.default.env.ODLA_DEV_TOKEN) {
434
- const declared = import_node_process5.default.env.ODLA_DEV_TOKEN_AUDIENCE;
563
+ if (import_node_process9.default.env.ODLA_DEV_TOKEN) {
564
+ const declared = import_node_process9.default.env.ODLA_DEV_TOKEN_AUDIENCE;
435
565
  if (declared) {
436
566
  if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
437
567
  } else if (audience !== "https://odla.ai") {
438
568
  throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
439
569
  }
440
- return import_node_process5.default.env.ODLA_DEV_TOKEN;
570
+ return import_node_process9.default.env.ODLA_DEV_TOKEN;
441
571
  }
442
572
  const device = readDeviceCredential(audience);
443
573
  if (device) {
444
- const session = await mintDeviceSession(cfg.platformUrl, device, doFetch);
445
- out.error(`auth: session minted by this enrolled device (${displayPath(deviceCredentialPath(), cfg.rootDir)})`);
574
+ const session = await deviceSessionToken(cfg.platformUrl, audience, device, doFetch);
575
+ out.error(`auth: session held by this enrolled device (${displayPath(deviceCredentialPath(), cfg.rootDir)})`);
446
576
  return session.token;
447
577
  }
448
578
  if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4 && cachedGrantCovers(cached, grantIntent)) {
@@ -462,7 +592,10 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
462
592
  doFetch,
463
593
  out,
464
594
  audience,
465
- email: handshakeEmail(options.email, cached?.platform === audience ? cached.email : void 0),
595
+ email: handshakeEmail(
596
+ options.email,
597
+ (cached?.platform === audience ? cached.email : void 0) ?? readMachineIdentity(audience)?.email
598
+ ),
466
599
  pendingFile: handshakeFile(cfg),
467
600
  grantIntent
468
601
  };
@@ -479,6 +612,7 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
479
612
  expiresAt
480
613
  });
481
614
  out.error(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
615
+ rememberMachineIdentity(audience, ctx.email);
482
616
  return token;
483
617
  }
484
618
  async function freshHandshake(ctx, waitMs) {
@@ -548,7 +682,7 @@ function stillPending(pending, email) {
548
682
  );
549
683
  }
550
684
  function handshakeEmail(value2, cached) {
551
- const email = (value2 ?? import_node_process5.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
685
+ const email = (value2 ?? import_node_process9.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
552
686
  if (/@users\.noreply\.github\.com$/i.test(email)) {
553
687
  throw new Error(
554
688
  `"${email}" is a GitHub commit identity, not an odla account email; use --email <signed-in-odla-account> or ODLA_USER_EMAIL`
@@ -579,12 +713,12 @@ function platformAudience(value2) {
579
713
  }
580
714
 
581
715
  // src/secret-input.ts
582
- var import_node_process6 = __toESM(require("process"), 1);
716
+ var import_node_process10 = __toESM(require("process"), 1);
583
717
  var MAX_BYTES = 64 * 1024;
584
718
  async function secretInputValue(options, kind = "credential") {
585
719
  if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
586
720
  let value2;
587
- if (options.fromEnv) value2 = import_node_process6.default.env[options.fromEnv];
721
+ if (options.fromEnv) value2 = import_node_process10.default.env[options.fromEnv];
588
722
  else if (options.stdin) value2 = await (options.readStdin ?? (() => readSecretStream(kind)))();
589
723
  else throw new Error(`${kind} input required: use --from-env <NAME> or --stdin; values are never accepted as arguments`);
590
724
  value2 = value2?.replace(/[\r\n]+$/, "");
@@ -592,7 +726,7 @@ async function secretInputValue(options, kind = "credential") {
592
726
  if (new TextEncoder().encode(value2).byteLength > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
593
727
  return value2;
594
728
  }
595
- async function readSecretStream(kind, stream = import_node_process6.default.stdin) {
729
+ async function readSecretStream(kind, stream = import_node_process10.default.stdin) {
596
730
  let value2 = "";
597
731
  for await (const chunk of stream) {
598
732
  value2 += String(chunk);
@@ -602,9 +736,8 @@ async function readSecretStream(kind, stream = import_node_process6.default.stdi
602
736
  }
603
737
 
604
738
  // src/admin-ai-auth.ts
605
- var import_node_fs5 = require("fs");
606
- var import_node_path4 = require("path");
607
- var import_node_process7 = __toESM(require("process"), 1);
739
+ var import_node_path5 = require("path");
740
+ var import_node_process11 = __toESM(require("process"), 1);
608
741
  var import_db2 = require("@odla-ai/db");
609
742
  async function getScopedPlatformToken(options) {
610
743
  return resolveAdminPlatformToken(options);
@@ -612,7 +745,7 @@ async function getScopedPlatformToken(options) {
612
745
  async function resolveAdminPlatformToken(options) {
613
746
  const audience = platformAudience(options.platform);
614
747
  if (options.token) return options.token;
615
- const fromEnv = import_node_process7.default.env.ODLA_ADMIN_TOKEN;
748
+ const fromEnv = import_node_process11.default.env.ODLA_ADMIN_TOKEN;
616
749
  if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
617
750
  return scopedToken(
618
751
  audience,
@@ -624,7 +757,7 @@ async function resolveAdminPlatformToken(options) {
624
757
  }
625
758
  function audienceBoundEnvToken(token, platform) {
626
759
  const audience = platformAudience(platform);
627
- const declared = import_node_process7.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
760
+ const declared = import_node_process11.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
628
761
  if (declared) {
629
762
  if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
630
763
  } else if (audience !== "https://odla.ai") {
@@ -651,15 +784,28 @@ var SCOPE_PURPOSE = {
651
784
  };
652
785
  async function scopedToken(platform, scope, options, doFetch, out) {
653
786
  const audience = platformAudience(platform);
654
- const rootDir = options.rootDir ?? import_node_process7.default.cwd();
655
- const tokenFile = options.tokenFile ?? (0, import_node_path4.join)(rootDir, ".odla/admin-token.local.json");
787
+ const rootDir = options.rootDir ?? import_node_process11.default.cwd();
788
+ const tokenFile = options.tokenFile ?? scopedTokenFile();
789
+ adoptRepoLocalCache((0, import_node_path5.join)(rootDir, ".odla/admin-token.local.json"), tokenFile, out);
790
+ const device = readDeviceCredential(audience);
791
+ if (device && options.cache !== false) {
792
+ const session = await deviceSessionToken(platform, audience, device, doFetch);
793
+ if (session.scopes?.includes(scope)) {
794
+ out.error(`auth: ${scope} held by this enrolled device`);
795
+ return session.token;
796
+ }
797
+ }
656
798
  const cache2 = options.cache === false ? null : readJsonFile(tokenFile);
657
799
  const cached = cache2?.platform === audience ? cache2.tokens?.[scope] : void 0;
658
800
  if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
659
801
  out.error(`auth: using cached ${scope} grant (${tokenFile})`);
660
802
  return cached.token;
661
803
  }
662
- const email = handshakeEmail(options.email, cache2?.platform === audience ? cache2.email : void 0);
804
+ out.error(scopeInterruptionNotice(scope, machineAuthState(audience)));
805
+ const email = handshakeEmail(
806
+ options.email,
807
+ (cache2?.platform === audience ? cache2.email : void 0) ?? readMachineIdentity(audience)?.email
808
+ );
663
809
  const { token, expiresAt } = await (0, import_db2.requestToken)({
664
810
  endpoint: audience,
665
811
  email,
@@ -679,8 +825,8 @@ async function scopedToken(platform, scope, options, doFetch, out) {
679
825
  if (options.cache !== false) {
680
826
  const tokens = cache2?.platform === audience ? { ...cache2.tokens ?? {} } : {};
681
827
  tokens[scope] = { token, expiresAt };
682
- if ((0, import_node_fs5.existsSync)((0, import_node_path4.join)(rootDir, ".git"))) ensureGitignore(rootDir, [tokenFile]);
683
828
  writePrivateJson(tokenFile, { platform: audience, email, tokens });
829
+ rememberMachineIdentity(audience, email);
684
830
  out.error(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
685
831
  } else {
686
832
  out.error(`auth: ${scope} grant is in memory only; its credential record remains in odla-ai/db`);
@@ -856,7 +1002,7 @@ function isRecord2(value2) {
856
1002
 
857
1003
  // src/admin-ai.ts
858
1004
  async function adminAi(options) {
859
- const platform = platformAudience(options.platform ?? import_node_process8.default.env.ODLA_PLATFORM ?? "https://odla.ai");
1005
+ const platform = platformAudience(options.platform ?? import_node_process12.default.env.ODLA_PLATFORM ?? "https://odla.ai");
860
1006
  const doFetch = options.fetch ?? fetch;
861
1007
  const out = options.stdout ?? console;
862
1008
  const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
@@ -1172,12 +1318,12 @@ async function adminSpend(parsed, ctx) {
1172
1318
 
1173
1319
  // src/operator-context.ts
1174
1320
  var import_node_fs8 = require("fs");
1175
- var import_node_path7 = require("path");
1176
- var import_node_process10 = __toESM(require("process"), 1);
1321
+ var import_node_path8 = require("path");
1322
+ var import_node_process14 = __toESM(require("process"), 1);
1177
1323
 
1178
1324
  // src/config.ts
1179
1325
  var import_node_fs6 = require("fs");
1180
- var import_node_path5 = require("path");
1326
+ var import_node_path6 = require("path");
1181
1327
  var import_node_url = require("url");
1182
1328
  var import_apps = require("@odla-ai/apps");
1183
1329
 
@@ -1581,13 +1727,17 @@ var DEFAULT_ENVS = ["dev"];
1581
1727
  var DEFAULT_SERVICES = ["db", "ai"];
1582
1728
  var configImportSerial = 0;
1583
1729
  var GOOGLE_CALENDAR_EVENTS_SCOPE = "https://www.googleapis.com/auth/calendar.events";
1730
+ var stderr = { error: (message2) => {
1731
+ process.stderr.write(`${message2}
1732
+ `);
1733
+ } };
1584
1734
  async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1585
- const resolved = (0, import_node_path5.resolve)(configPath);
1735
+ const resolved = (0, import_node_path6.resolve)(configPath);
1586
1736
  if (!(0, import_node_fs6.existsSync)(resolved)) {
1587
1737
  throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
1588
1738
  }
1589
1739
  const raw = await loadConfigModule(resolved);
1590
- const rootDir = (0, import_node_path5.dirname)(resolved);
1740
+ const rootDir = (0, import_node_path6.dirname)(resolved);
1591
1741
  validateRawConfig(raw, resolved);
1592
1742
  const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
1593
1743
  const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
@@ -1597,11 +1747,14 @@ async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1597
1747
  validateCalendarConfig(raw, unique3([...envs, ...options.additionalEnvs ?? []]), services, resolved);
1598
1748
  validateMonitoringConfig(raw, unique3([...envs, ...options.additionalEnvs ?? []]), services, resolved);
1599
1749
  const local = {
1600
- tokenFile: (0, import_node_path5.resolve)(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
1601
- credentialsFile: (0, import_node_path5.resolve)(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
1602
- devVarsFile: (0, import_node_path5.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
1750
+ tokenFile: raw.local?.tokenFile ? (0, import_node_path6.resolve)(rootDir, raw.local.tokenFile) : appTokenFile(raw.app.id),
1751
+ credentialsFile: raw.local?.credentialsFile ? (0, import_node_path6.resolve)(rootDir, raw.local.credentialsFile) : appCredentialsFile(raw.app.id),
1752
+ devVarsFile: (0, import_node_path6.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
1603
1753
  gitignore: raw.local?.gitignore ?? true
1604
1754
  };
1755
+ adoptRepoLocalCache((0, import_node_path6.resolve)(rootDir, ".odla/dev-token.json"), local.tokenFile, stderr);
1756
+ adoptRepoLocalCache((0, import_node_path6.resolve)(rootDir, ".odla/credentials.local.json"), local.credentialsFile, stderr);
1757
+ (0, import_node_fs6.rmSync)((0, import_node_path6.resolve)(rootDir, ".odla/handshake.local.json"), { force: true });
1605
1758
  return {
1606
1759
  ...raw,
1607
1760
  configPath: resolved,
@@ -1616,7 +1769,7 @@ async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1616
1769
  async function resolveDataExport(cfg, value2, names) {
1617
1770
  if (value2 === void 0 || value2 === null || value2 === false) return void 0;
1618
1771
  if (typeof value2 !== "string") return value2;
1619
- const target = (0, import_node_path5.isAbsolute)(value2) ? value2 : (0, import_node_path5.resolve)(cfg.rootDir, value2);
1772
+ const target = (0, import_node_path6.isAbsolute)(value2) ? value2 : (0, import_node_path6.resolve)(cfg.rootDir, value2);
1620
1773
  if (target.endsWith(".json")) {
1621
1774
  return JSON.parse((0, import_node_fs6.readFileSync)(target, "utf8"));
1622
1775
  }
@@ -1710,17 +1863,17 @@ function unique3(values) {
1710
1863
 
1711
1864
  // src/operator-profiles.ts
1712
1865
  var import_node_fs7 = require("fs");
1713
- var import_node_os2 = require("os");
1714
- var import_node_path6 = require("path");
1715
- var import_node_process9 = __toESM(require("process"), 1);
1866
+ var import_node_os3 = require("os");
1867
+ var import_node_path7 = require("path");
1868
+ var import_node_process13 = __toESM(require("process"), 1);
1716
1869
  function operatorProfileFile() {
1717
- return (0, import_node_path6.resolve)(
1718
- clean(import_node_process9.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".odla", "contexts.json")
1870
+ return (0, import_node_path7.resolve)(
1871
+ clean(import_node_process13.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path7.join)((0, import_node_os3.homedir)(), ".odla", "contexts.json")
1719
1872
  );
1720
1873
  }
1721
1874
  function resolveOperatorProfile(parsed) {
1722
1875
  const fromFlag = clean(stringOpt(parsed.options.context));
1723
- const fromEnvironment = clean(import_node_process9.default.env.ODLA_CONTEXT);
1876
+ const fromEnvironment = clean(import_node_process13.default.env.ODLA_CONTEXT);
1724
1877
  const name = fromFlag ?? fromEnvironment ?? null;
1725
1878
  const file = operatorProfileFile();
1726
1879
  if (!name) {
@@ -1760,10 +1913,10 @@ function removeOperatorProfile(name, file = operatorProfileFile()) {
1760
1913
  return true;
1761
1914
  }
1762
1915
  function operatorCredentialFiles(selection) {
1763
- const base = selection.name ? (0, import_node_path6.join)((0, import_node_path6.dirname)(selection.file), "profiles", selection.name) : (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".odla");
1916
+ const base = selection.name ? (0, import_node_path7.join)((0, import_node_path7.dirname)(selection.file), "profiles", selection.name) : (0, import_node_path7.join)((0, import_node_os3.homedir)(), ".odla");
1764
1917
  return {
1765
- developer: (0, import_node_path6.join)(base, "dev-token.json"),
1766
- scoped: (0, import_node_path6.join)(base, "admin-token.local.json")
1918
+ developer: (0, import_node_path7.join)(base, "dev-token.json"),
1919
+ scoped: (0, import_node_path7.join)(base, "admin-token.local.json")
1767
1920
  };
1768
1921
  }
1769
1922
  function assertOperatorName(value2, label) {
@@ -1841,7 +1994,7 @@ var DEFAULT_PLATFORM2 = "https://odla.ai";
1841
1994
  async function resolveOperatorContext(parsed, options = {}) {
1842
1995
  const profile = resolveOperatorProfile(parsed);
1843
1996
  const configArgument = stringOpt(parsed.options.config) ?? "odla.config.mjs";
1844
- const configPath = (0, import_node_path7.resolve)(configArgument);
1997
+ const configPath = (0, import_node_path8.resolve)(configArgument);
1845
1998
  const explicitConfig = parsed.options.config !== void 0;
1846
1999
  const hasConfig = (0, import_node_fs8.existsSync)(configPath);
1847
2000
  if (!hasConfig && (!options.allowMissingConfig || explicitConfig)) {
@@ -1849,13 +2002,13 @@ async function resolveOperatorContext(parsed, options = {}) {
1849
2002
  }
1850
2003
  const loaded = hasConfig ? await loadProjectConfig(configArgument) : void 0;
1851
2004
  const platformFlag = clean2(stringOpt(parsed.options.platform));
1852
- const platformEnvironment = clean2(import_node_process10.default.env.ODLA_PLATFORM_URL);
2005
+ const platformEnvironment = clean2(import_node_process14.default.env.ODLA_PLATFORM_URL);
1853
2006
  const platformValue = platformAudience(
1854
2007
  platformFlag ?? platformEnvironment ?? profile.value?.platform ?? loaded?.platformUrl ?? DEFAULT_PLATFORM2
1855
2008
  );
1856
2009
  const platformSource = platformFlag ? "flag" : platformEnvironment ? "environment" : profile.value ? "profile" : loaded ? "config" : "default";
1857
2010
  const appFlag = clean2(stringOpt(parsed.options.app));
1858
- const appEnvironment = clean2(import_node_process10.default.env.ODLA_APP_ID);
2011
+ const appEnvironment = clean2(import_node_process14.default.env.ODLA_APP_ID);
1859
2012
  const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
1860
2013
  const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
1861
2014
  if (appValue) {
@@ -1869,16 +2022,16 @@ async function resolveOperatorContext(parsed, options = {}) {
1869
2022
  );
1870
2023
  }
1871
2024
  const envFlag = clean2(stringOpt(parsed.options.env));
1872
- const envEnvironment = clean2(import_node_process10.default.env.ODLA_ENV);
2025
+ const envEnvironment = clean2(import_node_process14.default.env.ODLA_ENV);
1873
2026
  const environmentValue = envFlag ?? envEnvironment ?? profile.value?.environment ?? options.defaultEnvironment ?? null;
1874
2027
  const environmentSource = envFlag ? "flag" : envEnvironment ? "environment" : profile.value?.environment ? "profile" : options.defaultEnvironment ? "default" : "unresolved";
1875
2028
  if (environmentValue) {
1876
2029
  assertOperatorName(environmentValue, "environment");
1877
2030
  }
1878
- const rootDir = loaded?.rootDir ?? import_node_process10.default.cwd();
2031
+ const rootDir = loaded?.rootDir ?? import_node_process14.default.cwd();
1879
2032
  const profileCredentials = operatorCredentialFiles(profile);
1880
- const tokenFile = clean2(import_node_process10.default.env.ODLA_DEV_TOKEN_FILE) ? (0, import_node_path7.resolve)(import_node_process10.default.env.ODLA_DEV_TOKEN_FILE) : profile.name ? profileCredentials.developer : loaded?.local.tokenFile ?? profileCredentials.developer;
1881
- const scopedTokenFile = clean2(import_node_process10.default.env.ODLA_ADMIN_TOKEN_FILE) ? (0, import_node_path7.resolve)(import_node_process10.default.env.ODLA_ADMIN_TOKEN_FILE) : profile.name ? profileCredentials.scoped : loaded ? (0, import_node_path7.join)(loaded.rootDir, ".odla", "admin-token.local.json") : profileCredentials.scoped;
2033
+ const tokenFile = clean2(import_node_process14.default.env.ODLA_DEV_TOKEN_FILE) ? (0, import_node_path8.resolve)(import_node_process14.default.env.ODLA_DEV_TOKEN_FILE) : profile.name ? profileCredentials.developer : loaded?.local.tokenFile ?? profileCredentials.developer;
2034
+ const scopedTokenFile2 = clean2(import_node_process14.default.env.ODLA_ADMIN_TOKEN_FILE) ? (0, import_node_path8.resolve)(import_node_process14.default.env.ODLA_ADMIN_TOKEN_FILE) : profile.name ? profileCredentials.scoped : loaded ? (0, import_node_path8.join)(loaded.rootDir, ".odla", "admin-token.local.json") : profileCredentials.scoped;
1882
2035
  const cfg = loaded ? {
1883
2036
  ...loaded,
1884
2037
  platformUrl: platformValue,
@@ -1900,8 +2053,8 @@ async function resolveOperatorContext(parsed, options = {}) {
1900
2053
  services: [],
1901
2054
  local: {
1902
2055
  tokenFile,
1903
- credentialsFile: (0, import_node_path7.join)(rootDir, ".odla", "credentials.local.json"),
1904
- devVarsFile: (0, import_node_path7.join)(rootDir, ".dev.vars"),
2056
+ credentialsFile: (0, import_node_path8.join)(rootDir, ".odla", "credentials.local.json"),
2057
+ devVarsFile: (0, import_node_path8.join)(rootDir, ".dev.vars"),
1905
2058
  gitignore: true
1906
2059
  }
1907
2060
  };
@@ -1925,7 +2078,7 @@ async function resolveOperatorContext(parsed, options = {}) {
1925
2078
  },
1926
2079
  credentials: {
1927
2080
  developerTokenFile: tokenFile,
1928
- scopedTokenFile
2081
+ scopedTokenFile: scopedTokenFile2
1929
2082
  }
1930
2083
  };
1931
2084
  }
@@ -2023,7 +2176,7 @@ async function adminCommand(parsed, deps = {}) {
2023
2176
  }
2024
2177
 
2025
2178
  // src/auth-command.ts
2026
- var import_node_process11 = __toESM(require("process"), 1);
2179
+ var import_node_process15 = __toESM(require("process"), 1);
2027
2180
 
2028
2181
  // src/whoami-command.ts
2029
2182
  var text2 = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
@@ -2159,6 +2312,7 @@ async function whoamiCommand(parsed, deps = {}) {
2159
2312
  } else {
2160
2313
  out.log("projects: (none \u2014 every pm and discuss call will be refused)");
2161
2314
  }
2315
+ printMachineBlock(cfg.platformUrl, out);
2162
2316
  if (!identity.admin) {
2163
2317
  if (identity.scopes.includes("platform:runbook:write")) {
2164
2318
  out.log("\nThis exact scope can read and edit all platform runbook content.");
@@ -2169,6 +2323,21 @@ async function whoamiCommand(parsed, deps = {}) {
2169
2323
  }
2170
2324
  }
2171
2325
  }
2326
+ function printMachineBlock(platformUrl, out) {
2327
+ const state2 = machineAuthState(platformAudience(platformUrl));
2328
+ if (!state2.enrolled) {
2329
+ out.log("\nmachine: not enrolled \u2014 every privileged command needs its own browser approval.");
2330
+ out.log(` End that with:
2331
+ ${ENROL_EVERYTHING}`);
2332
+ return;
2333
+ }
2334
+ const reach = state2.appIds?.includes("*") ? "every app you own" : state2.appIds?.join(", ");
2335
+ out.log(`
2336
+ machine: enrolled${state2.deviceName ? ` as "${state2.deviceName}"` : ""}${reach ? ` for ${reach}` : ""}`);
2337
+ if (state2.scopes?.length) out.log(` carrying ${state2.scopes.join(", ")}`);
2338
+ const lapse = lapseNotice(state2);
2339
+ if (lapse) out.log(` ${lapse}`);
2340
+ }
2172
2341
 
2173
2342
  // src/auth-command.ts
2174
2343
  async function authCommand(parsed, deps = {}) {
@@ -2193,7 +2362,7 @@ async function authCommand(parsed, deps = {}) {
2193
2362
  const { cfg } = context;
2194
2363
  const out = deps.stdout ?? console;
2195
2364
  const doFetch = deps.fetch ?? fetch;
2196
- const email = stringOpt(parsed.options.email) ?? import_node_process11.default.env.ODLA_USER_EMAIL?.trim();
2365
+ const email = stringOpt(parsed.options.email) ?? import_node_process15.default.env.ODLA_USER_EMAIL?.trim();
2197
2366
  if (!email) {
2198
2367
  throw new Error(
2199
2368
  "auth login requires --email <odla-account> or ODLA_USER_EMAIL; confirm the signed-in odla email instead of using git or GitHub identity"
@@ -2554,7 +2723,7 @@ async function appCommand(parsed, dependencies = {}) {
2554
2723
 
2555
2724
  // src/brand-command.ts
2556
2725
  var import_promises = require("fs/promises");
2557
- var import_node_path8 = require("path");
2726
+ var import_node_path9 = require("path");
2558
2727
 
2559
2728
  // src/brand-design-unpack.ts
2560
2729
  var import_node_zlib = require("zlib");
@@ -2656,15 +2825,15 @@ function describeUnpack(result, outDir) {
2656
2825
  // src/brand-command.ts
2657
2826
  var USAGE = "usage: odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]";
2658
2827
  async function readBundle(source, deps) {
2659
- if (source !== "-") return (0, import_promises.readFile)((0, import_node_path8.resolve)(source), "utf8");
2828
+ if (source !== "-") return (0, import_promises.readFile)((0, import_node_path9.resolve)(source), "utf8");
2660
2829
  const readStdin = deps.readStdin;
2661
2830
  if (!readStdin) throw new Error("reading a bundle from stdin is not supported here");
2662
2831
  return readStdin();
2663
2832
  }
2664
2833
  async function writeAll(result, outDir) {
2665
2834
  for (const file of result.files) {
2666
- const target = (0, import_node_path8.resolve)(outDir, file.path);
2667
- await (0, import_promises.mkdir)((0, import_node_path8.dirname)(target), { recursive: true });
2835
+ const target = (0, import_node_path9.resolve)(outDir, file.path);
2836
+ await (0, import_promises.mkdir)((0, import_node_path9.dirname)(target), { recursive: true });
2668
2837
  await (0, import_promises.writeFile)(target, file.bytes);
2669
2838
  }
2670
2839
  }
@@ -2672,7 +2841,7 @@ async function designUnpack(parsed, deps) {
2672
2841
  assertArgs(parsed, ["out", "json"], 4);
2673
2842
  const source = parsed.positionals[3];
2674
2843
  if (!source) throw new Error(USAGE);
2675
- const outDir = (0, import_node_path8.resolve)(stringOpt(parsed.options.out) ?? "design");
2844
+ const outDir = (0, import_node_path9.resolve)(stringOpt(parsed.options.out) ?? "design");
2676
2845
  const result = unpackDesign(await readBundle(source, deps));
2677
2846
  await writeAll(result, outDir);
2678
2847
  const out = deps.stdout ?? console;
@@ -3277,7 +3446,7 @@ async function safeText4(response2) {
3277
3446
 
3278
3447
  // src/config-operation-command.ts
3279
3448
  var import_apps6 = require("@odla-ai/apps");
3280
- var import_node_path9 = require("path");
3449
+ var import_node_path10 = require("path");
3281
3450
 
3282
3451
  // src/version.ts
3283
3452
  var import_node_fs10 = require("fs");
@@ -3707,7 +3876,7 @@ async function operationClient(cfg, options, purpose) {
3707
3876
  platform: cfg.platformUrl,
3708
3877
  scope: "app:config:write",
3709
3878
  token: options.token,
3710
- tokenFile: (0, import_node_path9.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
3879
+ tokenFile: (0, import_node_path10.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
3711
3880
  rootDir: cfg.rootDir,
3712
3881
  email: options.email,
3713
3882
  open: options.open,
@@ -3762,7 +3931,7 @@ function record4(value2) {
3762
3931
 
3763
3932
  // src/config-reconcile-command.ts
3764
3933
  var import_apps8 = require("@odla-ai/apps");
3765
- var import_node_path10 = require("path");
3934
+ var import_node_path11 = require("path");
3766
3935
 
3767
3936
  // src/config-reconcile.ts
3768
3937
  var import_apps7 = require("@odla-ai/apps");
@@ -4058,7 +4227,7 @@ async function inspectConfig(options) {
4058
4227
  platform: cfg.platformUrl,
4059
4228
  scope: "app:config:read",
4060
4229
  token: options.token,
4061
- tokenFile: (0, import_node_path10.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
4230
+ tokenFile: (0, import_node_path11.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
4062
4231
  rootDir: cfg.rootDir,
4063
4232
  email: options.email,
4064
4233
  open: options.open,
@@ -4191,26 +4360,26 @@ function quoteArg2(value2) {
4191
4360
  // src/doctor-checks.ts
4192
4361
  var import_node_child_process3 = require("child_process");
4193
4362
  var import_node_fs13 = require("fs");
4194
- var import_node_path12 = require("path");
4363
+ var import_node_path13 = require("path");
4195
4364
 
4196
4365
  // src/wrangler.ts
4197
4366
  var import_node_child_process2 = require("child_process");
4198
4367
  var import_node_fs12 = require("fs");
4199
- var import_node_path11 = require("path");
4368
+ var import_node_path12 = require("path");
4200
4369
  var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
4201
4370
  const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
4202
4371
  let stdout = "";
4203
- let stderr = "";
4372
+ let stderr2 = "";
4204
4373
  child.stdout.on("data", (chunk) => stdout += chunk.toString());
4205
- child.stderr.on("data", (chunk) => stderr += chunk.toString());
4374
+ child.stderr.on("data", (chunk) => stderr2 += chunk.toString());
4206
4375
  child.on("error", reject);
4207
- child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout, stderr }));
4376
+ child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout, stderr: stderr2 }));
4208
4377
  child.stdin.end(opts?.input ?? "");
4209
4378
  });
4210
4379
  var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
4211
4380
  function findWranglerConfig(rootDir) {
4212
4381
  for (const name of WRANGLER_CONFIG_FILES) {
4213
- const path = (0, import_node_path11.join)(rootDir, name);
4382
+ const path = (0, import_node_path12.join)(rootDir, name);
4214
4383
  if ((0, import_node_fs12.existsSync)(path)) return path;
4215
4384
  }
4216
4385
  return null;
@@ -4361,21 +4530,21 @@ function wranglerWarnings(rootDir) {
4361
4530
  const blocks = [{ label: "", block: config }];
4362
4531
  const envs = config.env;
4363
4532
  if (envs && typeof envs === "object") {
4364
- for (const [name, block] of Object.entries(envs)) {
4365
- if (block && typeof block === "object") blocks.push({ label: `env.${name}.`, block });
4533
+ for (const [name, block2] of Object.entries(envs)) {
4534
+ if (block2 && typeof block2 === "object") blocks.push({ label: `env.${name}.`, block: block2 });
4366
4535
  }
4367
4536
  }
4368
- for (const { label, block } of blocks) {
4369
- const assets = block.assets;
4537
+ for (const { label, block: block2 } of blocks) {
4538
+ const assets = block2.assets;
4370
4539
  if (assets?.directory) {
4371
- const dir = (0, import_node_path12.resolve)(rootDir, assets.directory);
4372
- if (dir === (0, import_node_path12.resolve)(rootDir)) {
4540
+ const dir = (0, import_node_path13.resolve)(rootDir, assets.directory);
4541
+ if (dir === (0, import_node_path13.resolve)(rootDir)) {
4373
4542
  warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
4374
- } else if ((0, import_node_fs13.existsSync)((0, import_node_path12.join)(dir, "node_modules"))) {
4543
+ } else if ((0, import_node_fs13.existsSync)((0, import_node_path13.join)(dir, "node_modules"))) {
4375
4544
  warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
4376
4545
  }
4377
4546
  }
4378
- const vars = block.vars;
4547
+ const vars = block2.vars;
4379
4548
  if (vars && typeof vars === "object") {
4380
4549
  for (const [name, value2] of Object.entries(vars)) {
4381
4550
  if (name === "ODLA_API_KEY" || name === "ODLA_O11Y_TOKEN" || typeof value2 === "string" && looksSecret(value2)) {
@@ -4406,7 +4575,7 @@ function o11yProjectWarnings(rootDir) {
4406
4575
  warnings.push("cannot verify o11y Worker instrumentation \u2014 add a parseable wrangler.jsonc/json config");
4407
4576
  return warnings;
4408
4577
  }
4409
- const main = typeof config.main === "string" ? (0, import_node_path12.resolve)(rootDir, config.main) : null;
4578
+ const main = typeof config.main === "string" ? (0, import_node_path13.resolve)(rootDir, config.main) : null;
4410
4579
  if (!main || !(0, import_node_fs13.existsSync)(main)) {
4411
4580
  warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
4412
4581
  } else {
@@ -4436,7 +4605,7 @@ function calendarProjectWarnings(rootDir) {
4436
4605
  }
4437
4606
  function readPackageJson(rootDir) {
4438
4607
  try {
4439
- return JSON.parse((0, import_node_fs13.readFileSync)((0, import_node_path12.join)(rootDir, "package.json"), "utf8"));
4608
+ return JSON.parse((0, import_node_fs13.readFileSync)((0, import_node_path13.join)(rootDir, "package.json"), "utf8"));
4440
4609
  } catch {
4441
4610
  return null;
4442
4611
  }
@@ -4731,12 +4900,12 @@ function harnessOption(value2, flag) {
4731
4900
 
4732
4901
  // src/init.ts
4733
4902
  var import_node_fs14 = require("fs");
4734
- var import_node_path13 = require("path");
4903
+ var import_node_path14 = require("path");
4735
4904
  var import_apps9 = require("@odla-ai/apps");
4736
4905
  function initProject(options) {
4737
4906
  const out = options.stdout ?? console;
4738
- const rootDir = (0, import_node_path13.resolve)(options.rootDir ?? process.cwd());
4739
- const configPath = (0, import_node_path13.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
4907
+ const rootDir = (0, import_node_path14.resolve)(options.rootDir ?? process.cwd());
4908
+ const configPath = (0, import_node_path14.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
4740
4909
  if ((0, import_node_fs14.existsSync)(configPath) && !options.force) {
4741
4910
  throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
4742
4911
  }
@@ -4753,12 +4922,12 @@ function initProject(options) {
4753
4922
  }
4754
4923
  }
4755
4924
  const aiProvider = options.aiProvider;
4756
- (0, import_node_fs14.mkdirSync)((0, import_node_path13.dirname)(configPath), { recursive: true });
4757
- (0, import_node_fs14.mkdirSync)((0, import_node_path13.resolve)(rootDir, "src/odla"), { recursive: true });
4758
- (0, import_node_fs14.mkdirSync)((0, import_node_path13.resolve)(rootDir, ".odla"), { recursive: true });
4925
+ (0, import_node_fs14.mkdirSync)((0, import_node_path14.dirname)(configPath), { recursive: true });
4926
+ (0, import_node_fs14.mkdirSync)((0, import_node_path14.resolve)(rootDir, "src/odla"), { recursive: true });
4927
+ (0, import_node_fs14.mkdirSync)((0, import_node_path14.resolve)(rootDir, ".odla"), { recursive: true });
4759
4928
  (0, import_node_fs14.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
4760
- writeIfMissing((0, import_node_path13.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
4761
- writeIfMissing((0, import_node_path13.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
4929
+ writeIfMissing((0, import_node_path14.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
4930
+ writeIfMissing((0, import_node_path14.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
4762
4931
  ensureGitignore(rootDir);
4763
4932
  out.log(`created ${relativeDisplay(configPath, rootDir)}`);
4764
4933
  out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
@@ -4825,8 +4994,10 @@ ${calendar}
4825
4994
  // prod: "https://example.com",
4826
4995
  },
4827
4996
  local: {
4828
- tokenFile: ".odla/dev-token.json",
4829
- credentialsFile: ".odla/credentials.local.json",
4997
+ // Credentials live in ~/.odla, per machine, so every worktree of this app
4998
+ // shares one approval instead of asking for its own. Pinning tokenFile or
4999
+ // credentialsFile here still works and still overrides that \u2014 it just puts
5000
+ // this checkout back on its own island.
4830
5001
  devVarsFile: ".dev.vars",
4831
5002
  },
4832
5003
  };
@@ -5063,8 +5234,8 @@ function printReport(report5, out) {
5063
5234
 
5064
5235
  // src/skill.ts
5065
5236
  var import_node_fs15 = require("fs");
5066
- var import_node_os3 = require("os");
5067
- var import_node_path14 = require("path");
5237
+ var import_node_os4 = require("os");
5238
+ var import_node_path15 = require("path");
5068
5239
  var import_node_url2 = require("url");
5069
5240
 
5070
5241
  // src/skill-adapters.ts
@@ -5163,8 +5334,8 @@ function installSkill(options = {}) {
5163
5334
  const files = listFiles(sourceDir);
5164
5335
  if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
5165
5336
  const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
5166
- const root = (0, import_node_path14.resolve)(options.dir ?? process.cwd());
5167
- const home = (0, import_node_path14.resolve)(options.homeDir ?? (0, import_node_os3.homedir)());
5337
+ const root = (0, import_node_path15.resolve)(options.dir ?? process.cwd());
5338
+ const home = (0, import_node_path15.resolve)(options.homeDir ?? (0, import_node_os4.homedir)());
5168
5339
  const plans = /* @__PURE__ */ new Map();
5169
5340
  const targets = /* @__PURE__ */ new Map();
5170
5341
  const rememberTarget = (harness, target) => {
@@ -5178,48 +5349,48 @@ function installSkill(options = {}) {
5178
5349
  plans.set(target, { target, content: content2, boundary, managedMerge });
5179
5350
  };
5180
5351
  const planSkillTree = (targetDir2, boundary = root) => {
5181
- for (const rel of files) plan((0, import_node_path14.join)(targetDir2, rel), (0, import_node_fs15.readFileSync)((0, import_node_path14.join)(sourceDir, rel), "utf8"), false, boundary);
5352
+ for (const rel of files) plan((0, import_node_path15.join)(targetDir2, rel), (0, import_node_fs15.readFileSync)((0, import_node_path15.join)(sourceDir, rel), "utf8"), false, boundary);
5182
5353
  };
5183
5354
  let targetDir;
5184
5355
  if (options.global) {
5185
- const claudeRoot = (0, import_node_path14.join)(home, ".claude", "skills");
5186
- const codexRoot = (0, import_node_path14.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path14.join)(home, ".codex"), "skills");
5356
+ const claudeRoot = (0, import_node_path15.join)(home, ".claude", "skills");
5357
+ const codexRoot = (0, import_node_path15.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path15.join)(home, ".codex"), "skills");
5187
5358
  targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
5188
5359
  for (const harness of harnesses) {
5189
5360
  const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
5190
- planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path14.dirname)((0, import_node_path14.dirname)(codexRoot)));
5361
+ planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path15.dirname)((0, import_node_path15.dirname)(codexRoot)));
5191
5362
  rememberTarget(harness, skillRoot);
5192
5363
  }
5193
5364
  } else {
5194
- const sharedRoot = (0, import_node_path14.join)(root, ".agents", "skills");
5365
+ const sharedRoot = (0, import_node_path15.join)(root, ".agents", "skills");
5195
5366
  planSkillTree(sharedRoot);
5196
- const claudeRoot = (0, import_node_path14.join)(root, ".claude", "skills");
5367
+ const claudeRoot = (0, import_node_path15.join)(root, ".claude", "skills");
5197
5368
  targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
5198
5369
  for (const harness of harnesses) rememberTarget(harness, sharedRoot);
5199
5370
  if (harnesses.includes("claude")) {
5200
5371
  for (const skill of skillNames(files)) {
5201
- const canonical2 = (0, import_node_fs15.readFileSync)((0, import_node_path14.join)(sourceDir, skill, "SKILL.md"), "utf8");
5202
- plan((0, import_node_path14.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical2));
5372
+ const canonical2 = (0, import_node_fs15.readFileSync)((0, import_node_path15.join)(sourceDir, skill, "SKILL.md"), "utf8");
5373
+ plan((0, import_node_path15.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical2));
5203
5374
  }
5204
5375
  rememberTarget("claude", claudeRoot);
5205
5376
  }
5206
5377
  if (harnesses.includes("cursor")) {
5207
- const cursorRule = (0, import_node_path14.join)(root, ".cursor", "rules", "odla.mdc");
5378
+ const cursorRule = (0, import_node_path15.join)(root, ".cursor", "rules", "odla.mdc");
5208
5379
  plan(cursorRule, CURSOR_RULE);
5209
5380
  rememberTarget("cursor", cursorRule);
5210
5381
  }
5211
5382
  if (harnesses.includes("agents")) {
5212
- const agentsFile = (0, import_node_path14.join)(root, "AGENTS.md");
5383
+ const agentsFile = (0, import_node_path15.join)(root, "AGENTS.md");
5213
5384
  plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
5214
5385
  rememberTarget("agents", agentsFile);
5215
5386
  }
5216
5387
  if (harnesses.includes("copilot")) {
5217
- const copilotFile = (0, import_node_path14.join)(root, ".github", "copilot-instructions.md");
5388
+ const copilotFile = (0, import_node_path15.join)(root, ".github", "copilot-instructions.md");
5218
5389
  plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
5219
5390
  rememberTarget("copilot", copilotFile);
5220
5391
  }
5221
5392
  if (harnesses.includes("gemini")) {
5222
- const geminiFile = (0, import_node_path14.join)(root, "GEMINI.md");
5393
+ const geminiFile = (0, import_node_path15.join)(root, "GEMINI.md");
5223
5394
  plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
5224
5395
  rememberTarget("gemini", geminiFile);
5225
5396
  }
@@ -5255,7 +5426,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
5255
5426
  }
5256
5427
  for (const file of plans.values()) {
5257
5428
  if (!(0, import_node_fs15.existsSync)(file.target) || (0, import_node_fs15.readFileSync)(file.target, "utf8") !== file.content) {
5258
- (0, import_node_fs15.mkdirSync)((0, import_node_path14.dirname)(file.target), { recursive: true });
5429
+ (0, import_node_fs15.mkdirSync)((0, import_node_path15.dirname)(file.target), { recursive: true });
5259
5430
  (0, import_node_fs15.writeFileSync)(file.target, file.content);
5260
5431
  }
5261
5432
  }
@@ -5275,7 +5446,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
5275
5446
  };
5276
5447
  }
5277
5448
  function pathsUnder(root, paths) {
5278
- return [...paths].map((path) => (0, import_node_path14.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path14.sep}`) && !(0, import_node_path14.isAbsolute)(path)).sort();
5449
+ return [...paths].map((path) => (0, import_node_path15.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path15.sep}`) && !(0, import_node_path15.isAbsolute)(path)).sort();
5279
5450
  }
5280
5451
  function normalizeHarnesses(values, global) {
5281
5452
  const requested = values?.length ? values : ["claude"];
@@ -5294,10 +5465,10 @@ function normalizeHarnesses(values, global) {
5294
5465
  }
5295
5466
  return expanded;
5296
5467
  }
5297
- function managedFileContent(path, block, force, boundary) {
5468
+ function managedFileContent(path, block2, force, boundary) {
5298
5469
  const symlink = symlinkedComponent(boundary, path);
5299
5470
  if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
5300
- if (!(0, import_node_fs15.existsSync)(path)) return `${block}
5471
+ if (!(0, import_node_fs15.existsSync)(path)) return `${block2}
5301
5472
  `;
5302
5473
  const current = (0, import_node_fs15.readFileSync)(path, "utf8");
5303
5474
  const start = "<!-- odla-ai agent setup:start -->";
@@ -5309,24 +5480,24 @@ function managedFileContent(path, block, force, boundary) {
5309
5480
  }
5310
5481
  if (startAt === -1) {
5311
5482
  const separator = current.length === 0 || current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n";
5312
- return `${current}${separator}${block}
5483
+ return `${current}${separator}${block2}
5313
5484
  `;
5314
5485
  }
5315
5486
  const afterEnd = endAt + end.length;
5316
5487
  const existing = current.slice(startAt, afterEnd);
5317
- if (existing !== block && !force) {
5488
+ if (existing !== block2 && !force) {
5318
5489
  throw new Error(`odla-managed section modified locally in ${path}; re-run with --force to replace that section`);
5319
5490
  }
5320
- return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
5491
+ return `${current.slice(0, startAt)}${block2}${current.slice(afterEnd)}`;
5321
5492
  }
5322
5493
  function symlinkedComponent(boundary, target) {
5323
- const rel = (0, import_node_path14.relative)(boundary, target);
5324
- if (rel === ".." || rel.startsWith(`..${import_node_path14.sep}`) || (0, import_node_path14.isAbsolute)(rel)) {
5494
+ const rel = (0, import_node_path15.relative)(boundary, target);
5495
+ if (rel === ".." || rel.startsWith(`..${import_node_path15.sep}`) || (0, import_node_path15.isAbsolute)(rel)) {
5325
5496
  throw new Error(`agent setup target escapes its install root: ${target}`);
5326
5497
  }
5327
5498
  let current = boundary;
5328
- for (const part of rel.split(import_node_path14.sep).filter(Boolean)) {
5329
- current = (0, import_node_path14.join)(current, part);
5499
+ for (const part of rel.split(import_node_path15.sep).filter(Boolean)) {
5500
+ current = (0, import_node_path15.join)(current, part);
5330
5501
  try {
5331
5502
  if ((0, import_node_fs15.lstatSync)(current).isSymbolicLink()) return current;
5332
5503
  } catch (error) {
@@ -5343,9 +5514,9 @@ function listFiles(dir) {
5343
5514
  const results = [];
5344
5515
  const walk = (current) => {
5345
5516
  for (const entry of (0, import_node_fs15.readdirSync)(current, { withFileTypes: true })) {
5346
- const path = (0, import_node_path14.join)(current, entry.name);
5517
+ const path = (0, import_node_path15.join)(current, entry.name);
5347
5518
  if (entry.isDirectory()) walk(path);
5348
- else results.push((0, import_node_path14.relative)(dir, path));
5519
+ else results.push((0, import_node_path15.relative)(dir, path));
5349
5520
  }
5350
5521
  };
5351
5522
  walk(dir);
@@ -5701,8 +5872,8 @@ async function projectCommand(command, parsed, deps) {
5701
5872
 
5702
5873
  // src/code-connect.ts
5703
5874
  var import_node_fs16 = require("fs");
5704
- var import_node_os4 = require("os");
5705
- var import_node_path15 = require("path");
5875
+ var import_node_os5 = require("os");
5876
+ var import_node_path16 = require("path");
5706
5877
 
5707
5878
  // ../harness/dist/chunk-LNQNFGQC.js
5708
5879
  var HARNESS_PROTOCOL_VERSION = 1;
@@ -5808,7 +5979,7 @@ function allowedWorkspacePath(relativePath) {
5808
5979
  async function gitOutput(cwd, args, maxBytes) {
5809
5980
  const child = (0, import_child_process2.spawn)("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], shell: false });
5810
5981
  const stdout = [];
5811
- const stderr = [];
5982
+ const stderr2 = [];
5812
5983
  let bytes = 0;
5813
5984
  child.stdout.on("data", (chunk) => {
5814
5985
  bytes += chunk.byteLength;
@@ -5816,20 +5987,20 @@ async function gitOutput(cwd, args, maxBytes) {
5816
5987
  else stdout.push(chunk);
5817
5988
  });
5818
5989
  child.stderr.on("data", (chunk) => {
5819
- if (stderr.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr.push(chunk);
5990
+ if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
5820
5991
  });
5821
5992
  const code = await new Promise((accept, reject) => {
5822
5993
  child.once("error", reject);
5823
5994
  child.once("exit", accept);
5824
5995
  });
5825
5996
  if (bytes > maxBytes) throw new Error(`git output exceeds ${maxBytes} bytes`);
5826
- if (code !== 0) throw new Error(`git command failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
5997
+ if (code !== 0) throw new Error(`git command failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
5827
5998
  return Buffer.concat(stdout);
5828
5999
  }
5829
6000
  async function gitBlobs(cwd, entries, maxBytes) {
5830
6001
  const child = (0, import_child_process2.spawn)("git", ["cat-file", "--batch"], { cwd, stdio: ["pipe", "pipe", "pipe"], shell: false });
5831
6002
  const stdout = [];
5832
- const stderr = [];
6003
+ const stderr2 = [];
5833
6004
  let bytes = 0;
5834
6005
  child.stdout.on("data", (chunk) => {
5835
6006
  bytes += chunk.byteLength;
@@ -5837,7 +6008,7 @@ async function gitBlobs(cwd, entries, maxBytes) {
5837
6008
  else stdout.push(chunk);
5838
6009
  });
5839
6010
  child.stderr.on("data", (chunk) => {
5840
- if (stderr.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr.push(chunk);
6011
+ if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
5841
6012
  });
5842
6013
  child.stdin.end(`${entries.map((entry) => entry.hash).join("\n")}
5843
6014
  `);
@@ -5846,7 +6017,7 @@ async function gitBlobs(cwd, entries, maxBytes) {
5846
6017
  child.once("exit", accept);
5847
6018
  });
5848
6019
  if (bytes > maxBytes + entries.length * 100) throw new Error(`Git tree exceeds ${maxBytes} bytes`);
5849
- if (code !== 0) throw new Error(`git object read failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
6020
+ if (code !== 0) throw new Error(`git object read failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
5850
6021
  const output = Buffer.concat(stdout);
5851
6022
  const blobs = [];
5852
6023
  let offset = 0;
@@ -5943,7 +6114,7 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
5943
6114
  shell: false
5944
6115
  });
5945
6116
  const stdout = [];
5946
- const stderr = [];
6117
+ const stderr2 = [];
5947
6118
  let outputBytes = 0;
5948
6119
  child.stdout.on("data", (chunk) => {
5949
6120
  outputBytes += chunk.byteLength;
@@ -5951,14 +6122,14 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
5951
6122
  else stdout.push(chunk);
5952
6123
  });
5953
6124
  child.stderr.on("data", (chunk) => {
5954
- if (stderr.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr.push(chunk);
6125
+ if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
5955
6126
  });
5956
6127
  const code = await new Promise((accept, reject) => {
5957
6128
  child.once("error", reject);
5958
6129
  child.once("exit", accept);
5959
6130
  });
5960
6131
  if (outputBytes > 8 * 1024 * 1024) throw new Error("git file inventory exceeds 8 MiB");
5961
- if (code !== 0) throw new Error(`git file inventory failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
6132
+ if (code !== 0) throw new Error(`git file inventory failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
5962
6133
  const paths = Buffer.concat(stdout).toString("utf8").split("\0").filter(Boolean).sort();
5963
6134
  if (paths.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
5964
6135
  const root = (0, import_path4.resolve)(sourceDir);
@@ -6003,7 +6174,7 @@ async function captureGitDiff(root, maxBytes) {
6003
6174
  "workspace"
6004
6175
  ], { cwd: root, stdio: ["ignore", "pipe", "pipe"], shell: false });
6005
6176
  const stdout = [];
6006
- const stderr = [];
6177
+ const stderr2 = [];
6007
6178
  let bytes = 0;
6008
6179
  child.stdout.on("data", (chunk) => {
6009
6180
  bytes += chunk.byteLength;
@@ -6011,7 +6182,7 @@ async function captureGitDiff(root, maxBytes) {
6011
6182
  else stdout.push(chunk);
6012
6183
  });
6013
6184
  child.stderr.on("data", (chunk) => {
6014
- if (stderr.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr.push(chunk);
6185
+ if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
6015
6186
  });
6016
6187
  const code = await new Promise((accept, reject) => {
6017
6188
  child.once("error", reject);
@@ -6019,7 +6190,7 @@ async function captureGitDiff(root, maxBytes) {
6019
6190
  });
6020
6191
  if (bytes > maxBytes) throw new Error(`patch exceeds ${maxBytes} bytes`);
6021
6192
  if (code !== 0 && code !== 1) {
6022
- throw new Error(`git diff failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
6193
+ throw new Error(`git diff failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
6023
6194
  }
6024
6195
  return Buffer.concat(stdout).toString("utf8").replaceAll("a/baseline/", "a/").replaceAll("a/workspace/", "a/").replaceAll("b/baseline/", "b/").replaceAll("b/workspace/", "b/").replaceAll("--- a/baseline", "--- a").replaceAll("+++ b/workspace", "+++ b");
6025
6196
  }
@@ -6823,11 +6994,11 @@ function rollup(graph, kind, options = {}) {
6823
6994
  }
6824
6995
 
6825
6996
  // ../graph/dist/code/index.js
6826
- function dirname8(path) {
6997
+ function dirname9(path) {
6827
6998
  const at = path.lastIndexOf("/");
6828
6999
  return at <= 0 ? "." : path.slice(0, at);
6829
7000
  }
6830
- function join12(base, specifier) {
7001
+ function join13(base, specifier) {
6831
7002
  const parts = [];
6832
7003
  const segments = `${base === "." ? "" : `${base}/`}${specifier}`.split("/");
6833
7004
  for (const segment of segments) {
@@ -6851,7 +7022,7 @@ var BARE_IMPORT = /^\s*import\s*["']([^"']+)["']/gm;
6851
7022
  var isSourcePath = (path) => SOURCE.test(path);
6852
7023
  function resolveImport(fromPath, specifier, known) {
6853
7024
  if (!specifier.startsWith(".")) return null;
6854
- const base = join12(dirname8(fromPath), specifier);
7025
+ const base = join13(dirname9(fromPath), specifier);
6855
7026
  const candidates = [
6856
7027
  base,
6857
7028
  base.replace(/\.js$/, ".ts"),
@@ -7382,13 +7553,13 @@ function gitApply(cwd, patch2, check) {
7382
7553
  stdio: ["pipe", "ignore", "pipe"],
7383
7554
  env: { PATH: process.env.PATH ?? "", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null" }
7384
7555
  });
7385
- let stderr = "";
7556
+ let stderr2 = "";
7386
7557
  child.stderr.setEncoding("utf8");
7387
7558
  child.stderr.on("data", (text3) => {
7388
- if (stderr.length < 4e3) stderr += text3.slice(0, 4e3);
7559
+ if (stderr2.length < 4e3) stderr2 += text3.slice(0, 4e3);
7389
7560
  });
7390
7561
  child.once("error", reject);
7391
- child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(describePatchFailure(patch2, stderr.trim().slice(0, 500)))));
7562
+ child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(describePatchFailure(patch2, stderr2.trim().slice(0, 500)))));
7392
7563
  child.stdin.end(patch2);
7393
7564
  });
7394
7565
  }
@@ -7501,7 +7672,7 @@ function execute(engine, args, name, recipe2, signal) {
7501
7672
  const started = Date.now();
7502
7673
  const child = (0, import_child_process5.spawn)(engine, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
7503
7674
  const stdout = [];
7504
- const stderr = [];
7675
+ const stderr2 = [];
7505
7676
  let bytes = 0;
7506
7677
  let outputLimitExceeded = false;
7507
7678
  let timedOut = false;
@@ -7522,7 +7693,7 @@ function execute(engine, args, name, recipe2, signal) {
7522
7693
  else target.push(chunk);
7523
7694
  };
7524
7695
  child.stdout.on("data", collect(stdout));
7525
- child.stderr.on("data", collect(stderr));
7696
+ child.stderr.on("data", collect(stderr2));
7526
7697
  const abort = () => stop("abort");
7527
7698
  signal?.addEventListener("abort", abort, { once: true });
7528
7699
  if (signal?.aborted) abort();
@@ -7538,7 +7709,7 @@ function execute(engine, args, name, recipe2, signal) {
7538
7709
  accept({
7539
7710
  exitCode: code ?? 1,
7540
7711
  stdout: Buffer.concat(stdout).toString("utf8"),
7541
- stderr: Buffer.concat(stderr).toString("utf8"),
7712
+ stderr: Buffer.concat(stderr2).toString("utf8"),
7542
7713
  durationMs: Date.now() - started,
7543
7714
  outputLimitExceeded,
7544
7715
  timedOut
@@ -7690,11 +7861,11 @@ function checkedResult(result, maximumOutputBytes) {
7690
7861
  }
7691
7862
  function boundedLogs(result, maximum) {
7692
7863
  const stdout = Buffer.from(result.stdout);
7693
- const stderr = Buffer.from(result.stderr);
7864
+ const stderr2 = Buffer.from(result.stderr);
7694
7865
  const first = stdout.subarray(0, maximum);
7695
7866
  return {
7696
7867
  stdout: first.toString("utf8"),
7697
- stderr: stderr.subarray(0, Math.max(0, maximum - first.byteLength)).toString("utf8")
7868
+ stderr: stderr2.subarray(0, Math.max(0, maximum - first.byteLength)).toString("utf8")
7698
7869
  };
7699
7870
  }
7700
7871
  function digestPolicy(policy) {
@@ -9764,7 +9935,7 @@ var CODE_BUILD_RECIPES = Object.freeze([{
9764
9935
  // src/code-connect.ts
9765
9936
  async function codeConnect(options) {
9766
9937
  const cwd = options.cwd ?? process.cwd();
9767
- const configPath = (0, import_node_path15.resolve)(cwd, options.configPath);
9938
+ const configPath = (0, import_node_path16.resolve)(cwd, options.configPath);
9768
9939
  const cfg = (0, import_node_fs16.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
9769
9940
  const requestedAppId = options.appId?.trim();
9770
9941
  if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
@@ -9794,7 +9965,7 @@ async function codeConnect(options) {
9794
9965
  const doFetch = options.fetch ?? fetch;
9795
9966
  const engine = await (options.selectEngine ?? selectContainerEngine)(options.engine ?? "auto");
9796
9967
  const hostPlatform = process.platform === "darwin" ? "macos" : "linux";
9797
- const hostName = (options.name ?? (0, import_node_os4.hostname)()).trim();
9968
+ const hostName = (options.name ?? (0, import_node_os5.hostname)()).trim();
9798
9969
  if (!hostName || hostName.length > 120) throw new Error("--name must contain 1 to 120 characters");
9799
9970
  const repository = await inferGitHubRepository(cwd, options.readGitOrigin);
9800
9971
  const localSource = await (options.prepareLocalSource ?? prepareCodeLocalSource)(
@@ -9804,7 +9975,8 @@ async function codeConnect(options) {
9804
9975
  );
9805
9976
  try {
9806
9977
  const descriptor2 = localSource.descriptor;
9807
- const approval = await (options.getToken ?? getScopedPlatformToken)({
9978
+ const device = options.getToken ? null : readDeviceCredential(platform);
9979
+ const authorization = device ? (await mintDeviceSession(platform, device, doFetch)).token : await (options.getToken ?? getScopedPlatformToken)({
9808
9980
  platform,
9809
9981
  scope: "app:code:host:connect",
9810
9982
  email: options.email,
@@ -9818,7 +9990,7 @@ async function codeConnect(options) {
9818
9990
  const target = appId ? { appId } : { repository };
9819
9991
  const response2 = await doFetch(`${platform}/registry/code/hosts/connect`, {
9820
9992
  method: "POST",
9821
- headers: { authorization: `Bearer ${approval}`, "content-type": "application/json" },
9993
+ headers: { authorization: `Bearer ${authorization}`, "content-type": "application/json" },
9822
9994
  body: JSON.stringify({ ...target, env: appEnv, name: hostName, platform: hostPlatform, slots }),
9823
9995
  redirect: "error",
9824
9996
  signal: options.signal
@@ -9831,8 +10003,8 @@ async function codeConnect(options) {
9831
10003
  platform: hostPlatform,
9832
10004
  arch: process.arch,
9833
10005
  engines: [engine],
9834
- cpuCount: (0, import_node_os4.cpus)().length,
9835
- memoryBytes: (0, import_node_os4.totalmem)(),
10006
+ cpuCount: (0, import_node_os5.cpus)().length,
10007
+ memoryBytes: (0, import_node_os5.totalmem)(),
9836
10008
  source: descriptor2,
9837
10009
  images: {
9838
10010
  ready: true,
@@ -10199,13 +10371,13 @@ async function codeCommand(parsed, dependencies) {
10199
10371
  }
10200
10372
 
10201
10373
  // src/operator-credentials.ts
10202
- var import_node_process12 = __toESM(require("process"), 1);
10374
+ var import_node_process16 = __toESM(require("process"), 1);
10203
10375
  function developerTokenStatus(context, parsed, now = Date.now()) {
10204
10376
  const cached = readJsonFile(context.cfg.local.tokenFile);
10205
10377
  const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
10206
10378
  const source = clean3(
10207
10379
  stringOpt(parsed.options.token)
10208
- ) ? "flag" : clean3(import_node_process12.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
10380
+ ) ? "flag" : clean3(import_node_process16.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
10209
10381
  return {
10210
10382
  source,
10211
10383
  cacheFile: context.cfg.local.tokenFile,
@@ -10399,6 +10571,27 @@ async function credentialCommand(parsed, deps = {}) {
10399
10571
  }
10400
10572
 
10401
10573
  // src/help-usage.ts
10574
+ var AUTH_SECTION = `
10575
+ Enrol this machine once, then stop asking:
10576
+ npx odla-ai device enroll --all-apps --capability all --no-open --wait 600
10577
+ One browser approval. Afterwards every worktree on this machine mints its
10578
+ own short-lived credentials with nobody's attention, for every app you
10579
+ own \u2014 including apps you create later. The window rolls forward each time
10580
+ you use it, so continuous work never interrupts anyone; only a real gap
10581
+ does. Give the human the printed /studio?code= URL, keep the process
10582
+ alive, and wait on it.
10583
+
10584
+ npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600
10585
+ The same thing across all of odla, for weeks. Needs a platform
10586
+ administrator's approval \u2014 an app owner's cannot carry platform scopes.
10587
+
10588
+ npx odla-ai whoami what this machine holds and when it lapses
10589
+ npx odla-ai device list every machine you have enrolled
10590
+
10591
+ Enrollment is the only human decision here. Revoking a machine, purging an app,
10592
+ transferring ownership, and rotating credentials still need a signed-in human in
10593
+ Studio, and no machine credential can do them however wide its approval was.
10594
+ `;
10402
10595
  var USAGE_SECTION = `
10403
10596
  Start here:
10404
10597
  odla-ai runbook ask "<question>" The current procedure, from odla's own
@@ -10437,7 +10630,7 @@ Usage:
10437
10630
  odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]
10438
10631
  odla-ai pm project list [--app <product-id>] [--status <s>] [--json]
10439
10632
  odla-ai pm project add --app <product-id> --name <name> [--description <text>] [--json]
10440
- odla-ai pm project use <project-id> [--json] [saved locally in this worktree]
10633
+ odla-ai pm project use <project-id> [--json] [saved for this app, on this machine]
10441
10634
  odla-ai pm goal list [--app <id>] [--project <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
10442
10635
  odla-ai pm task list [--app <id>] [--column <backlog|ready|doing|review|done>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
10443
10636
  odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
@@ -10529,7 +10722,8 @@ Usage:
10529
10722
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
10530
10723
  odla-ai security run [target] --self --ack-redacted-source
10531
10724
  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]
10532
- odla-ai device enroll [--app <id>[,<id>...]] [--name <label>] [--capability <c>[,<c>...]] [--device-ttl <30d|6w|2y|forever>] [--email <odla-account>] [--no-open] [--json]
10725
+ odla-ai device enroll [--all-apps|--app <id>[,<id>...]] [--capability all|<c>[,<c>...]] [--platform-wide]
10726
+ [--name <label>] [--device-ttl <30d|6w|2y|forever>] [--email <odla-account>] [--no-open] [--wait <seconds>] [--json]
10533
10727
  odla-ai device list [--email <odla-account>] [--json]
10534
10728
  odla-ai device revoke <device-id> [--email <odla-account>] [--json]
10535
10729
  odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
@@ -10543,8 +10737,11 @@ Usage:
10543
10737
 
10544
10738
  // src/help.ts
10545
10739
  function printHelp(output = console) {
10546
- output.log(`odla-ai
10547
- ${USAGE_SECTION}
10740
+ output.log(helpText());
10741
+ }
10742
+ function helpText() {
10743
+ return `odla-ai
10744
+ ${AUTH_SECTION}${USAGE_SECTION}
10548
10745
  Commands:
10549
10746
  auth Start a fresh, exact-project agent authorization for human review.
10550
10747
  The email is the signed-in odla account, never git or GitHub
@@ -10617,8 +10814,9 @@ Commands:
10617
10814
  security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
10618
10815
  pm Project management (via @odla-ai/pm): Products contain Projects;
10619
10816
  projects contain goals, kanban tasks, decisions, and bugs. Use
10620
- "pm project list|add|use" to select worktree-local context, or
10621
- pass --app/--project explicitly. Same device-grant auth as "app".
10817
+ "pm project list|add|use" selects a project for this app on this
10818
+ machine \u2014 every worktree shares it \u2014 or pass --app/--project
10819
+ explicitly. Same device-grant auth as "app".
10622
10820
  Status changes and comments post to each item's @odla-ai/chat
10623
10821
  discussion thread.
10624
10822
  NOTE: "--column ready" is OWNER-ONLY. Creating a task in Ready
@@ -10647,9 +10845,20 @@ Commands:
10647
10845
  platform Read canonical fleet health, releases, provider load/freshness,
10648
10846
  explicit unknowns, and next actions through a read-only grant.
10649
10847
  device Enrol THIS machine once, then stop asking. A human approves the
10650
- enrollment in the browser; from then on this terminal mints its
10651
- own short-lived credentials for the named projects with nobody's
10652
- attention, until the device expires or is revoked.
10848
+ enrollment in the browser; from then on EVERY worktree on this
10849
+ machine mints its own short-lived credentials with nobody's
10850
+ attention, until the device is revoked or goes unused.
10851
+ "--all-apps" covers every app you own, now and later, so creating
10852
+ an app costs no new approval. "--capability all" takes everything
10853
+ that approval is allowed to carry, so a capability you did not
10854
+ think to name is not a 403 next week. "--platform-wide" is the
10855
+ administrator's version, across all of odla.
10856
+ The expiry is a GAP, not a clock: each use rolls it forward, so
10857
+ only going quiet brings a human back into the loop \u2014 which is
10858
+ where anything that changed can be explained.
10859
+ "device list" shows what each machine holds and when it lapses;
10860
+ revoking one takes down every credential it ever minted, and is
10861
+ deliberately a signed-in human's decision in Studio.
10653
10862
  provision Register services, compose integrations, persist credentials, optionally push secrets.
10654
10863
  "provision --live --yes" initializes only the live instance of
10655
10864
  an existing sandbox app and enables every configured service;
@@ -10719,10 +10928,12 @@ Safety:
10719
10928
  release. A confirmed stale client stops with a safe npx rerun command; a
10720
10929
  workspace-linked client also identifies the worktree that must be updated.
10721
10930
  Run Code from a GitHub checkout already connected to an app in Studio; an
10722
- odla.config.mjs may select the app explicitly but is not required. Code host
10723
- approval and credential hashes live in odla-ai/db. The host
10724
- credential is never written under .odla/; it exists only in the foreground
10725
- "code connect" process and is rotated by the next approved connection.
10931
+ odla.config.mjs may select the app explicitly but is not required. With an
10932
+ enrolled code.session device, the Studio repository selection authorizes the
10933
+ host's first connection and reconnects without another human approval. Without
10934
+ an enrolled device, "code connect" falls back to the reviewed handshake. Host
10935
+ credential hashes live in odla-ai/db; the credential itself is never written
10936
+ under .odla/, exists only in the foreground process, and rotates on reconnect.
10726
10937
  "code repository bind" takes owner/name and resolves the two GitHub integers the
10727
10938
  bind route wants across every installation you have, refusing an ambiguous match
10728
10939
  rather than choosing one \u2014 the same repository name under two organizations is
@@ -10743,7 +10954,46 @@ Safety:
10743
10954
  Run security plan first to inspect the admin-selected providers, models,
10744
10955
  per-route bounds, credential readiness, retention, no-execution boundary,
10745
10956
  and digest that binds consent to that exact plan.
10746
- `);
10957
+ `;
10958
+ }
10959
+
10960
+ // src/help-command.ts
10961
+ function printCommandHelp(command, output = console) {
10962
+ const lines = helpText().split("\n");
10963
+ const usage = allBlocks(lines, new RegExp(`^ odla-ai ${escapeRe(command)}(\\s|$)`));
10964
+ const prose = block(lines, (line2) => new RegExp(`^ ${escapeRe(command)}\\s\\s+\\S`).test(line2));
10965
+ if (usage.length === 0 && prose.length === 0) {
10966
+ output.log(`odla-ai: no command "${command}". Run "odla-ai help" for all of them.`);
10967
+ return;
10968
+ }
10969
+ output.log([
10970
+ ...prose.length ? [prose.join("\n"), ""] : [],
10971
+ ...usage.length ? ["Usage:", ...usage, ""] : [],
10972
+ AUTH_SECTION.trimEnd()
10973
+ ].join("\n"));
10974
+ }
10975
+ function allBlocks(lines, pattern) {
10976
+ const out = [];
10977
+ for (let i = 0; i < lines.length; i++) {
10978
+ if (!pattern.test(lines[i])) continue;
10979
+ out.push(...block(lines.slice(i), (line2) => line2 === lines[i]));
10980
+ }
10981
+ return out;
10982
+ }
10983
+ function block(lines, starts) {
10984
+ const first = lines.findIndex(starts);
10985
+ if (first === -1) return [];
10986
+ const indent = lines[first].length - lines[first].trimStart().length;
10987
+ const out = [lines[first]];
10988
+ for (const line2 of lines.slice(first + 1)) {
10989
+ if (!line2.trim()) break;
10990
+ if (line2.length - line2.trimStart().length <= indent) break;
10991
+ out.push(line2);
10992
+ }
10993
+ return out;
10994
+ }
10995
+ function escapeRe(value2) {
10996
+ return value2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10747
10997
  }
10748
10998
 
10749
10999
  // src/discuss-principals.ts
@@ -11900,14 +12150,36 @@ async function pmWatch(ctx, parsed) {
11900
12150
  }
11901
12151
 
11902
12152
  // src/pm-project-context.ts
11903
- var import_node_path16 = require("path");
11904
- var pmProjectContextFile = (rootDir) => (0, import_node_path16.resolve)(rootDir, ".odla", "pm-project.local.json");
12153
+ var import_node_fs17 = require("fs");
12154
+ var import_node_path17 = require("path");
12155
+ var pmProjectContextFile = () => pmContextFile();
11905
12156
  function readPmProjectContext(rootDir) {
11906
- const value2 = readJsonFile(pmProjectContextFile(rootDir));
11907
- return value2 && typeof value2.appId === "string" && typeof value2.projectId === "string" ? value2 : null;
12157
+ adoptLegacySelection(rootDir);
12158
+ const entries = Object.values(readSelections()).filter(isSelection);
12159
+ return entries.sort((a, b) => b.selectedAt.localeCompare(a.selectedAt))[0] ?? null;
11908
12160
  }
11909
12161
  function writePmProjectContext(rootDir, value2) {
11910
- writePrivateJson(pmProjectContextFile(rootDir), { ...value2, selectedAt: (/* @__PURE__ */ new Date()).toISOString() });
12162
+ adoptLegacySelection(rootDir);
12163
+ writePrivateJson(pmProjectContextFile(), {
12164
+ ...readSelections(),
12165
+ [value2.appId]: { ...value2, selectedAt: (/* @__PURE__ */ new Date()).toISOString() }
12166
+ });
12167
+ }
12168
+ function adoptLegacySelection(rootDir) {
12169
+ const legacy = (0, import_node_path17.resolve)(rootDir, ".odla", "pm-project.local.json");
12170
+ if (!(0, import_node_fs17.existsSync)(legacy)) return;
12171
+ const previous = readJsonFile(legacy);
12172
+ (0, import_node_fs17.rmSync)(legacy, { force: true });
12173
+ if (!isSelection(previous)) return;
12174
+ const selections = readSelections();
12175
+ if (selections[previous.appId]) return;
12176
+ writePrivateJson(pmProjectContextFile(), { ...selections, [previous.appId]: previous });
12177
+ }
12178
+ function readSelections() {
12179
+ return readJsonFile(pmProjectContextFile()) ?? {};
12180
+ }
12181
+ function isSelection(value2) {
12182
+ return !!value2 && typeof value2.appId === "string" && typeof value2.projectId === "string" && typeof value2.selectedAt === "string";
11911
12183
  }
11912
12184
 
11913
12185
  // src/pm-project-actions.ts
@@ -12932,7 +13204,7 @@ function percent(value2) {
12932
13204
  // src/provision.ts
12933
13205
  var import_apps13 = require("@odla-ai/apps");
12934
13206
  var import_ai5 = require("@odla-ai/ai");
12935
- var import_node_process13 = __toESM(require("process"), 1);
13207
+ var import_node_process17 = __toESM(require("process"), 1);
12936
13208
 
12937
13209
  // src/integration-provision.ts
12938
13210
  var import_db3 = require("@odla-ai/db");
@@ -13370,7 +13642,7 @@ async function provision(options) {
13370
13642
  await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
13371
13643
  }
13372
13644
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
13373
- const key = import_node_process13.default.env[cfg.ai.keyEnv];
13645
+ const key = import_node_process17.default.env[cfg.ai.keyEnv];
13374
13646
  if (key) {
13375
13647
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
13376
13648
  await (0, import_ai5.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -13411,8 +13683,8 @@ async function provision(options) {
13411
13683
  }
13412
13684
 
13413
13685
  // src/record.ts
13414
- var import_node_fs17 = require("fs");
13415
- var import_node_process14 = __toESM(require("process"), 1);
13686
+ var import_node_fs18 = require("fs");
13687
+ var import_node_process18 = __toESM(require("process"), 1);
13416
13688
 
13417
13689
  // src/surface.ts
13418
13690
  var PM_ACTIONS = {
@@ -13594,7 +13866,7 @@ function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
13594
13866
 
13595
13867
  // src/record.ts
13596
13868
  function recordInvocation(parsed) {
13597
- const file = import_node_process14.default.env.ODLA_CLI_RECORD;
13869
+ const file = import_node_process18.default.env.ODLA_CLI_RECORD;
13598
13870
  if (!file) return;
13599
13871
  try {
13600
13872
  const entry = {
@@ -13602,7 +13874,7 @@ function recordInvocation(parsed) {
13602
13874
  options: Object.entries(parsed.options).map(([name, value2]) => value2 === false ? `no-${name}` : name).sort()
13603
13875
  };
13604
13876
  if (!entry.path.length) return;
13605
- (0, import_node_fs17.appendFileSync)(file, `${JSON.stringify(entry)}
13877
+ (0, import_node_fs18.appendFileSync)(file, `${JSON.stringify(entry)}
13606
13878
  `);
13607
13879
  } catch {
13608
13880
  }
@@ -13631,6 +13903,9 @@ function renderAdvisories(out, advisories, env = process.env) {
13631
13903
  }
13632
13904
  }
13633
13905
 
13906
+ // src/device-command.ts
13907
+ var import_db4 = require("@odla-ai/db");
13908
+
13634
13909
  // src/device-ttl.ts
13635
13910
  var OWNER_DEVICE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
13636
13911
  var DAY_MS = 24 * 60 * 60 * 1e3;
@@ -13650,10 +13925,26 @@ function parseDeviceTtl(raw) {
13650
13925
  }
13651
13926
 
13652
13927
  // src/device-command.ts
13653
- var import_node_fs18 = require("fs");
13654
- var import_node_path17 = require("path");
13655
- var import_node_process15 = __toESM(require("process"), 1);
13928
+ var import_node_fs19 = require("fs");
13929
+ var import_node_path18 = require("path");
13930
+ var import_node_process19 = __toESM(require("process"), 1);
13656
13931
  async function deviceCommand(parsed, deps) {
13932
+ assertArgs(parsed, [
13933
+ "app",
13934
+ "all-apps",
13935
+ "platform-wide",
13936
+ "name",
13937
+ "capability",
13938
+ "device-ttl",
13939
+ "email",
13940
+ "open",
13941
+ "json",
13942
+ "config",
13943
+ "token",
13944
+ "context",
13945
+ "platform",
13946
+ "wait"
13947
+ ], 3);
13657
13948
  const action2 = parsed.positionals[1] ?? "";
13658
13949
  const out = deps.stdout ?? console;
13659
13950
  const doFetch = deps.fetch ?? fetch;
@@ -13666,10 +13957,12 @@ async function deviceCommand(parsed, deps) {
13666
13957
  }
13667
13958
  async function enroll(parsed, deps, cfg, doFetch, out, json) {
13668
13959
  const name = stringOpt(parsed.options.name) ?? defaultDeviceName();
13669
- const apps = (stringOpt(parsed.options.app) ?? cfg.app.id).split(",").map((id2) => id2.trim()).filter(Boolean);
13670
- if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026]");
13960
+ const platformWide = parsed.options["platform-wide"] === true;
13961
+ const apps = platformWide || parsed.options["all-apps"] === true ? [import_db4.ALL_OWNED_APPS] : (stringOpt(parsed.options.app) ?? cfg.app.id).split(",").map((id2) => id2.trim()).filter(Boolean);
13962
+ if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026], or --all-apps");
13671
13963
  const deviceTtlMs = parseDeviceTtl(parsed.options["device-ttl"]);
13672
- const extended = deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
13964
+ const extended = platformWide || deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
13965
+ const { capabilities, scopes } = requestedEnvelope(parsed, platformWide);
13673
13966
  const token = await scopedToken2(
13674
13967
  parsed,
13675
13968
  deps,
@@ -13684,9 +13977,10 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
13684
13977
  headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
13685
13978
  body: JSON.stringify({
13686
13979
  name,
13687
- platform: import_node_process15.default.platform,
13980
+ platform: import_node_process19.default.platform,
13688
13981
  appIds: apps,
13689
- ...parsed.options.capability ? { capabilities: String(parsed.options.capability).split(",").map((c) => c.trim()).filter(Boolean) } : {},
13982
+ ...capabilities ? { capabilities } : {},
13983
+ ...scopes ? { scopes } : {},
13690
13984
  ...deviceTtlMs === void 0 ? {} : { deviceTtlMs }
13691
13985
  })
13692
13986
  });
@@ -13695,20 +13989,53 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
13695
13989
  throw new Error(`device enroll failed: ${body.error?.message ?? `registry returned ${response2.status}`} (${response2.status})`);
13696
13990
  }
13697
13991
  const path = deviceCredentialPath();
13698
- (0, import_node_fs18.mkdirSync)((0, import_node_path17.dirname)(path), { recursive: true });
13699
- (0, import_node_fs18.writeFileSync)(path, JSON.stringify({
13992
+ (0, import_node_fs19.mkdirSync)((0, import_node_path18.dirname)(path), { recursive: true });
13993
+ (0, import_node_fs19.writeFileSync)(path, JSON.stringify({
13700
13994
  token: body.token,
13701
13995
  platform: cfg.platformUrl.replace(/\/$/, ""),
13702
13996
  deviceId: body.device.deviceId,
13703
13997
  name
13704
13998
  }, null, 2));
13705
- (0, import_node_fs18.chmodSync)(path, 384);
13706
- out.error(`device: enrolled "${name}" for ${body.device.appIds.join(", ")}; credential written to ${path}`);
13707
- out.error("device: this terminal will mint its own credentials from now on \u2014 no further approvals.");
13999
+ (0, import_node_fs19.chmodSync)(path, 384);
14000
+ rememberMachineIdentity(cfg.platformUrl.replace(/\/$/, ""), stringOpt(parsed.options.email));
14001
+ const reach = body.device.appIds.includes(import_db4.ALL_OWNED_APPS) ? "every app you own, now and later" : body.device.appIds.join(", ");
14002
+ out.error(`device: enrolled "${name}" for ${reach}; credential written to ${path}`);
14003
+ out.error(
14004
+ "device: every worktree on this machine mints its own credentials from now on \u2014 no further approvals,"
14005
+ );
14006
+ out.error(
14007
+ `device: and the clock resets each time you use it. Going quiet for ${describeWindow(body.device.expiresAt)} is what ends it.`
14008
+ );
13708
14009
  if (json) {
13709
- out.log(JSON.stringify({ deviceId: body.device.deviceId, name, appIds: body.device.appIds, expiresAt: body.device.expiresAt }, null, 2));
14010
+ out.log(JSON.stringify({
14011
+ deviceId: body.device.deviceId,
14012
+ name,
14013
+ appIds: body.device.appIds,
14014
+ capabilities: body.device.capabilities ?? [],
14015
+ scopes: body.device.scopes ?? [],
14016
+ expiresAt: body.device.expiresAt,
14017
+ hardExpiresAt: body.device.hardExpiresAt ?? null
14018
+ }, null, 2));
13710
14019
  }
13711
14020
  }
14021
+ function requestedEnvelope(parsed, platformWide) {
14022
+ const raw = stringOpt(parsed.options.capability);
14023
+ const everything = platformWide || raw?.trim().toLowerCase() === "all";
14024
+ if (everything) {
14025
+ return {
14026
+ capabilities: [...import_db4.OPTIONAL_AGENT_PROJECT_CAPABILITIES],
14027
+ scopes: platformWide ? [...import_db4.ADMIN_DEVICE_SCOPES] : [...import_db4.OWNER_DEVICE_SCOPES]
14028
+ };
14029
+ }
14030
+ const named = raw?.split(",").map((c) => c.trim()).filter(Boolean);
14031
+ return named?.length ? { capabilities: named } : {};
14032
+ }
14033
+ function describeWindow(expiresAt, now = Date.now()) {
14034
+ const days = Math.max(1, Math.round((expiresAt - now) / (24 * 60 * 60 * 1e3)));
14035
+ if (days >= 365) return `${Math.round(days / 365)} year${days >= 730 ? "s" : ""}`;
14036
+ if (days % 7 === 0) return `${days / 7} week${days > 7 ? "s" : ""}`;
14037
+ return `${days} day${days === 1 ? "" : "s"}`;
14038
+ }
13712
14039
  async function list2(parsed, deps, cfg, doFetch, out, json) {
13713
14040
  const token = await scopedToken2(parsed, deps, cfg, doFetch, out, "odla CLI (device list)");
13714
14041
  const response2 = await doFetch(`${cfg.platformUrl}/registry/devices`, {
@@ -13722,12 +14049,17 @@ async function list2(parsed, deps, cfg, doFetch, out, json) {
13722
14049
  if (body.devices.length === 0) return out.log("no enrolled devices");
13723
14050
  for (const device of body.devices) {
13724
14051
  const state2 = device.revokedAt ? "revoked" : device.expiresAt <= Date.now() ? "expired" : "active";
13725
- out.log(`${device.deviceId} ${state2.padEnd(7)} ${device.name} [${device.appIds.join(", ")}]`);
14052
+ const reach = device.appIds.includes("*") ? "every app you own" : device.appIds.join(", ");
14053
+ const gap = state2 === "active" ? ` idle ${describeWindow(device.expiresAt)} left` : "";
14054
+ out.log(`${device.deviceId} ${state2.padEnd(7)} ${device.name} [${reach}]${gap}`);
13726
14055
  }
13727
14056
  }
13728
14057
  async function revoke(parsed, deps, cfg, doFetch, out, json) {
13729
14058
  const deviceId = parsed.positionals[2];
13730
14059
  if (!deviceId) throw new Error("device revoke needs the device id from `odla-ai device list`");
14060
+ out.error(
14061
+ `device: revoking is a signed-in human's decision; if this is refused, open ${cfg.platformUrl}/studio and revoke it there.`
14062
+ );
13731
14063
  const token = await scopedToken2(parsed, deps, cfg, doFetch, out, "odla CLI (device revoke)");
13732
14064
  const response2 = await doFetch(`${cfg.platformUrl}/registry/devices/${encodeURIComponent(deviceId)}/revoke`, {
13733
14065
  method: "POST",
@@ -13746,7 +14078,7 @@ async function scopedToken2(parsed, deps, cfg, doFetch, out, label, scope = "app
13746
14078
  // A device is granted the apps named in ONE approval, so --app is a list here.
13747
14079
  allowAppList: true
13748
14080
  });
13749
- const scopedTokenFile = credentials.scopedTokenFile;
14081
+ const scopedTokenFile2 = credentials.scopedTokenFile;
13750
14082
  return getScopedPlatformToken({
13751
14083
  platform: cfg.platformUrl,
13752
14084
  scope,
@@ -13757,16 +14089,16 @@ async function scopedToken2(parsed, deps, cfg, doFetch, out, label, scope = "app
13757
14089
  open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
13758
14090
  openApprovalUrl: deps.openUrl,
13759
14091
  rootDir: cfg.rootDir,
13760
- tokenFile: scopedTokenFile,
14092
+ tokenFile: scopedTokenFile2,
13761
14093
  ...stringOpt(parsed.options.token) ? { token: stringOpt(parsed.options.token) } : {}
13762
14094
  });
13763
14095
  }
13764
14096
  function defaultDeviceName() {
13765
- return `${import_node_process15.default.env.HOSTNAME ?? import_node_process15.default.env.HOST ?? "machine"}-${import_node_process15.default.platform}`;
14097
+ return `${import_node_process19.default.env.HOSTNAME ?? import_node_process19.default.env.HOST ?? "machine"}-${import_node_process19.default.platform}`;
13766
14098
  }
13767
14099
 
13768
14100
  // src/runbook-actions.ts
13769
- var import_node_fs19 = require("fs");
14101
+ var import_node_fs20 = require("fs");
13770
14102
 
13771
14103
  // src/runbook-requires.ts
13772
14104
  var SPEC = /^(@?[\w./-]+?)@(\d+\.\d+\.\d+(?:[\w.-]*)?)$/;
@@ -13862,7 +14194,7 @@ async function bySlug(ctx, slug) {
13862
14194
  function readBody(file, inline) {
13863
14195
  if (inline !== void 0) return inline;
13864
14196
  if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
13865
- return (0, import_node_fs19.readFileSync)(file === "-" ? 0 : file, "utf8");
14197
+ return (0, import_node_fs20.readFileSync)(file === "-" ? 0 : file, "utf8");
13866
14198
  }
13867
14199
  var stamp = (ms) => ms ? new Date(ms).toISOString().slice(0, 16).replace("T", " ") : "";
13868
14200
  async function runbookList(ctx, all, query) {
@@ -13954,8 +14286,8 @@ async function runbookRemove(ctx, slug) {
13954
14286
  }
13955
14287
 
13956
14288
  // src/runbook-import.ts
13957
- var import_node_fs20 = require("fs");
13958
- var import_node_path18 = require("path");
14289
+ var import_node_fs21 = require("fs");
14290
+ var import_node_path19 = require("path");
13959
14291
  function parseRunbook(text3, slug) {
13960
14292
  let rest = text3;
13961
14293
  const meta = {};
@@ -13980,12 +14312,12 @@ function parseRunbook(text3, slug) {
13980
14312
  };
13981
14313
  }
13982
14314
  function readRunbookDir(dir) {
13983
- if (!(0, import_node_fs20.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
13984
- const files = (0, import_node_fs20.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
14315
+ if (!(0, import_node_fs21.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
14316
+ const files = (0, import_node_fs21.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
13985
14317
  if (!files.length) throw new Error(`no .md files in ${dir}`);
13986
14318
  return files.map((file) => {
13987
- const slug = (0, import_node_path18.basename)(file, ".md");
13988
- const parsed = parseRunbook((0, import_node_fs20.readFileSync)((0, import_node_path18.join)(dir, file), "utf8"), slug);
14319
+ const slug = (0, import_node_path19.basename)(file, ".md");
14320
+ const parsed = parseRunbook((0, import_node_fs21.readFileSync)((0, import_node_path19.join)(dir, file), "utf8"), slug);
13989
14321
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
13990
14322
  });
13991
14323
  }
@@ -14053,8 +14385,8 @@ async function upsert(ctx, r, visibility) {
14053
14385
 
14054
14386
  // src/runbook-impact.ts
14055
14387
  var import_node_child_process6 = require("child_process");
14056
- var import_node_fs21 = require("fs");
14057
- var import_node_path19 = require("path");
14388
+ var import_node_fs22 = require("fs");
14389
+ var import_node_path20 = require("path");
14058
14390
 
14059
14391
  // src/runbook-impact-scan.ts
14060
14392
  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$]*)/;
@@ -14223,10 +14555,10 @@ ${body.split("\n").map((line2) => `+${line2}`).join("\n")}
14223
14555
  }
14224
14556
  function manifestLabeller(root) {
14225
14557
  return (workspace) => {
14226
- const manifest = (0, import_node_path19.join)(root, workspace, "package.json");
14227
- if (!(0, import_node_fs21.existsSync)(manifest)) return void 0;
14558
+ const manifest = (0, import_node_path20.join)(root, workspace, "package.json");
14559
+ if (!(0, import_node_fs22.existsSync)(manifest)) return void 0;
14228
14560
  try {
14229
- const name = JSON.parse((0, import_node_fs21.readFileSync)(manifest, "utf8")).name;
14561
+ const name = JSON.parse((0, import_node_fs22.readFileSync)(manifest, "utf8")).name;
14230
14562
  return typeof name === "string" ? name : void 0;
14231
14563
  } catch {
14232
14564
  return void 0;
@@ -14293,7 +14625,7 @@ function report4(ctx, impacts) {
14293
14625
  async function runbookImpact(ctx, options, deps = {}) {
14294
14626
  const cwd = deps.cwd ?? process.cwd();
14295
14627
  const runGit = deps.runGit ?? gitRunner(cwd);
14296
- const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs21.readFileSync)((0, import_node_path19.join)(cwd, path), "utf8"));
14628
+ const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs22.readFileSync)((0, import_node_path20.join)(cwd, path), "utf8"));
14297
14629
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
14298
14630
  if (!surfaces.length) {
14299
14631
  return ctx.out.log(
@@ -14420,12 +14752,12 @@ async function runbookComment(ctx, slug, body) {
14420
14752
 
14421
14753
  // src/runbook-editor.ts
14422
14754
  var import_node_child_process7 = require("child_process");
14423
- var import_node_fs22 = require("fs");
14424
- var import_node_os5 = require("os");
14425
- var import_node_path20 = require("path");
14426
- var import_node_process16 = __toESM(require("process"), 1);
14755
+ var import_node_fs23 = require("fs");
14756
+ var import_node_os6 = require("os");
14757
+ var import_node_path21 = require("path");
14758
+ var import_node_process20 = __toESM(require("process"), 1);
14427
14759
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
14428
- function resolveEditor(env = import_node_process16.default.env) {
14760
+ function resolveEditor(env = import_node_process20.default.env) {
14429
14761
  for (const name of EDITOR_ENV) {
14430
14762
  const value2 = env[name];
14431
14763
  if (value2 && value2.trim()) return value2.trim();
@@ -14439,8 +14771,8 @@ function defaultRun(command, path) {
14439
14771
  return result.status ?? 0;
14440
14772
  }
14441
14773
  function editText(initial, slug, deps = {}) {
14442
- const env = deps.env ?? import_node_process16.default.env;
14443
- const interactive = deps.interactive ?? (() => Boolean(import_node_process16.default.stdin.isTTY));
14774
+ const env = deps.env ?? import_node_process20.default.env;
14775
+ const interactive = deps.interactive ?? (() => Boolean(import_node_process20.default.stdin.isTTY));
14444
14776
  const editor = resolveEditor(env);
14445
14777
  if (!editor)
14446
14778
  throw new Error(
@@ -14448,16 +14780,16 @@ function editText(initial, slug, deps = {}) {
14448
14780
  );
14449
14781
  if (!interactive())
14450
14782
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
14451
- const dir = (0, import_node_fs22.mkdtempSync)((0, import_node_path20.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
14452
- const file = (0, import_node_path20.join)(dir, `${slug}.md`);
14783
+ const dir = (0, import_node_fs23.mkdtempSync)((0, import_node_path21.join)((0, import_node_os6.tmpdir)(), "odla-runbook-"));
14784
+ const file = (0, import_node_path21.join)(dir, `${slug}.md`);
14453
14785
  try {
14454
- (0, import_node_fs22.writeFileSync)(file, initial, { mode: 384 });
14786
+ (0, import_node_fs23.writeFileSync)(file, initial, { mode: 384 });
14455
14787
  const code = defaultRunOrInjected(deps)(editor, file);
14456
14788
  if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
14457
- const edited = (0, import_node_fs22.readFileSync)(file, "utf8");
14789
+ const edited = (0, import_node_fs23.readFileSync)(file, "utf8");
14458
14790
  return edited === initial ? null : edited;
14459
14791
  } finally {
14460
- (0, import_node_fs22.rmSync)(dir, { recursive: true, force: true });
14792
+ (0, import_node_fs23.rmSync)(dir, { recursive: true, force: true });
14461
14793
  }
14462
14794
  }
14463
14795
  var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
@@ -14795,7 +15127,7 @@ function hostedSeverity(value2, flag) {
14795
15127
  var import_security2 = require("@odla-ai/security");
14796
15128
 
14797
15129
  // src/security.ts
14798
- var import_node_path21 = require("path");
15130
+ var import_node_path22 = require("path");
14799
15131
  var import_security = require("@odla-ai/security");
14800
15132
  var import_node3 = require("@odla-ai/security/node");
14801
15133
  async function runHostedSecurity(options) {
@@ -14807,9 +15139,9 @@ async function runHostedSecurity(options) {
14807
15139
  const appId = selfAudit ? "odla-ai" : cfg.app.id;
14808
15140
  const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
14809
15141
  const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
14810
- const target = (0, import_node_path21.resolve)(options.target ?? cfg?.rootDir ?? ".");
14811
- const output = (0, import_node_path21.resolve)(options.out ?? (0, import_node_path21.resolve)(target, ".odla/security/hosted"));
14812
- const outputRelative = (0, import_node_path21.relative)(target, output).split(import_node_path21.sep).join("/");
15142
+ const target = (0, import_node_path22.resolve)(options.target ?? cfg?.rootDir ?? ".");
15143
+ const output = (0, import_node_path22.resolve)(options.out ?? (0, import_node_path22.resolve)(target, ".odla/security/hosted"));
15144
+ const outputRelative = (0, import_node_path22.relative)(target, output).split(import_node_path22.sep).join("/");
14813
15145
  if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
14814
15146
  const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
14815
15147
  const tokenRequest = {
@@ -14821,7 +15153,7 @@ async function runHostedSecurity(options) {
14821
15153
  };
14822
15154
  const token = await injectedToken(options, tokenRequest);
14823
15155
  const snapshot = await (0, import_node3.snapshotDirectory)(target, {
14824
- exclude: !outputRelative.startsWith("../") && !(0, import_node_path21.isAbsolute)(outputRelative) ? [outputRelative] : []
15156
+ exclude: !outputRelative.startsWith("../") && !(0, import_node_path22.isAbsolute)(outputRelative) ? [outputRelative] : []
14825
15157
  });
14826
15158
  const hosted = await (0, import_security.createPlatformSecurityReasoners)({
14827
15159
  platform,
@@ -14839,7 +15171,7 @@ async function runHostedSecurity(options) {
14839
15171
  });
14840
15172
  const harness = (0, import_security.createSecurityHarness)({
14841
15173
  profile,
14842
- store: new import_node3.FileRunStore((0, import_node_path21.resolve)(output, "state")),
15174
+ store: new import_node3.FileRunStore((0, import_node_path22.resolve)(output, "state")),
14843
15175
  discoveryReasoner: hosted.discoveryReasoner,
14844
15176
  validationReasoner: hosted.validationReasoner,
14845
15177
  policy: {
@@ -14863,7 +15195,7 @@ async function runHostedSecurity(options) {
14863
15195
  function selectEnv(requested, declared, configPath, rootDir) {
14864
15196
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
14865
15197
  if (!env || !declared.includes(env)) {
14866
- const shown = (0, import_node_path21.relative)(rootDir, configPath) || configPath;
15198
+ const shown = (0, import_node_path22.relative)(rootDir, configPath) || configPath;
14867
15199
  throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
14868
15200
  }
14869
15201
  return env;
@@ -14892,7 +15224,7 @@ function printSummary(out, appId, env, run, report5, output) {
14892
15224
  out.log(` coverage: ${report5.coverageStatus} ${complete}/${report5.coverage.length} blocked=${report5.metrics.blockedCells} shallow=${report5.metrics.shallowCells} unscheduled=${report5.metrics.unscheduledCells} budget_exhausted=${report5.metrics.budgetExhaustedCells}`);
14893
15225
  if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
14894
15226
  out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
14895
- out.log(` report: ${(0, import_node_path21.resolve)(output, "REPORT.md")}`);
15227
+ out.log(` report: ${(0, import_node_path22.resolve)(output, "REPORT.md")}`);
14896
15228
  }
14897
15229
  function formatBudget(usage) {
14898
15230
  return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
@@ -15373,8 +15705,10 @@ async function dispatchCli(argv, dependencies) {
15373
15705
  return;
15374
15706
  }
15375
15707
  if (command === "help" || command === "--help" || command === "-h") {
15376
- assertArgs(parsed, ["help"], 1);
15377
- printHelp(runtime.stdout);
15708
+ assertArgs(parsed, ["help"], 2);
15709
+ const topic = parsed.positionals[1];
15710
+ if (topic) printCommandHelp(topic, runtime.stdout);
15711
+ else printHelp(runtime.stdout);
15378
15712
  return;
15379
15713
  }
15380
15714
  if (command === "whoami") {