@odla-ai/cli 0.38.3 → 0.40.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/bin.cjs CHANGED
@@ -347,45 +347,15 @@ var init_handshake_state = __esm({
347
347
  }
348
348
  });
349
349
 
350
- // src/cached-credential.ts
351
- function noteCachedCredential(tokenFile) {
352
- noted = tokenFile;
353
- }
354
- function isCredentialRejection(error) {
355
- const message2 = error instanceof Error ? error.message : String(error ?? "");
356
- return /\((401|403)\)\s*$/.test(message2.trim());
357
- }
358
- function explainRejectedCredential(error) {
359
- const tokenFile = noted;
360
- if (!tokenFile || !isCredentialRejection(error)) return null;
361
- noted = null;
362
- (0, import_node_fs5.rmSync)(tokenFile, { force: true });
363
- return [
364
- "auth: the cached credential was rejected by odla, so it was revoked before its cached expiry.",
365
- " The usual cause is a newer sign-in for this project: collecting a handshake retires the",
366
- " principal's other credentials, so a second terminal or worktree supersedes this one.",
367
- ` Discarded ${tokenFile}; re-run this command to request a fresh approval.`
368
- ].join("\n");
369
- }
370
- var import_node_fs5, noted;
371
- var init_cached_credential = __esm({
372
- "src/cached-credential.ts"() {
373
- "use strict";
374
- init_cjs_shims();
375
- import_node_fs5 = require("fs");
376
- noted = null;
377
- }
378
- });
379
-
380
350
  // src/device-session.ts
381
351
  function deviceCredentialPath(env = import_node_process4.default.env) {
382
352
  return env.ODLA_DEVICE_CREDENTIAL ?? (0, import_node_path3.join)(env.HOME ?? (0, import_node_os.homedir)(), ".odla", "device.json");
383
353
  }
384
354
  function readDeviceCredential(platform, env = import_node_process4.default.env) {
385
355
  const path = deviceCredentialPath(env);
386
- if (!(0, import_node_fs6.existsSync)(path)) return null;
356
+ if (!(0, import_node_fs5.existsSync)(path)) return null;
387
357
  try {
388
- const parsed = JSON.parse((0, import_node_fs6.readFileSync)(path, "utf8"));
358
+ const parsed = JSON.parse((0, import_node_fs5.readFileSync)(path, "utf8"));
389
359
  if (typeof parsed.token !== "string" || !parsed.token.startsWith("odla_device_")) return null;
390
360
  if (parsed.platform !== platform) return null;
391
361
  return { ...parsed, token: parsed.token, platform: parsed.platform };
@@ -407,20 +377,91 @@ async function mintDeviceSession(platformUrl, credential2, doFetch) {
407
377
  `device session failed: ${detail} (${response2.status})` + (revocable ? " \u2014 if this machine's enrollment was revoked or has expired, enroll it again in Studio" : "")
408
378
  );
409
379
  }
410
- return { token: body.token, expiresAt: body.expiresAt ?? Date.now() };
380
+ return {
381
+ token: body.token,
382
+ expiresAt: body.expiresAt ?? Date.now(),
383
+ // Absent from a registry that predates rolling expiry and scoped devices.
384
+ // Left undefined rather than defaulted, so a caller can tell "the platform
385
+ // did not say" from "the platform said none".
386
+ ...typeof body.deviceExpiresAt === "number" ? { deviceExpiresAt: body.deviceExpiresAt } : {},
387
+ ...Array.isArray(body.appIds) ? { appIds: body.appIds } : {},
388
+ ...Array.isArray(body.capabilities) ? { capabilities: body.capabilities } : {},
389
+ ...Array.isArray(body.scopes) ? { scopes: body.scopes } : {}
390
+ };
411
391
  }
412
- var import_node_fs6, import_node_os, import_node_path3, import_node_process4;
392
+ var import_node_fs5, import_node_os, import_node_path3, import_node_process4;
413
393
  var init_device_session = __esm({
414
394
  "src/device-session.ts"() {
415
395
  "use strict";
416
396
  init_cjs_shims();
417
- import_node_fs6 = require("fs");
397
+ import_node_fs5 = require("fs");
418
398
  import_node_os = require("os");
419
399
  import_node_path3 = require("path");
420
400
  import_node_process4 = __toESM(require("process"), 1);
421
401
  }
422
402
  });
423
403
 
404
+ // src/odla-home.ts
405
+ function odlaHome(env = import_node_process5.default.env) {
406
+ if (env.VITEST && !env.ODLA_HOME) {
407
+ throw new Error(
408
+ "ODLA_HOME must be set under test \u2014 resolving the real ~/.odla would write to the developer's machine"
409
+ );
410
+ }
411
+ return env.ODLA_HOME ?? (0, import_node_path4.join)(env.HOME ?? (0, import_node_os2.homedir)(), ".odla");
412
+ }
413
+ function odlaHomePath(segments, env = import_node_process5.default.env) {
414
+ return (0, import_node_path4.join)(odlaHome(env), ...segments);
415
+ }
416
+ function identityFile(env) {
417
+ return odlaHomePath(["identity.json"], env);
418
+ }
419
+ function deviceSessionFile(env) {
420
+ return odlaHomePath(["session.json"], env);
421
+ }
422
+ function appTokenFile(appId, env) {
423
+ return odlaHomePath(["apps", safeSegment(appId), "dev-token.json"], env);
424
+ }
425
+ function appCredentialsFile(appId, env) {
426
+ return odlaHomePath(["apps", safeSegment(appId), "credentials.json"], env);
427
+ }
428
+ function scopedTokenFile(env) {
429
+ return odlaHomePath(["admin-token.local.json"], env);
430
+ }
431
+ function pmContextFile(env) {
432
+ return odlaHomePath(["pm-context.json"], env);
433
+ }
434
+ function adoptRepoLocalCache(legacyPath, machinePath, out) {
435
+ if (!(0, import_node_fs6.existsSync)(legacyPath) || legacyPath === machinePath) return false;
436
+ const superseded2 = (0, import_node_fs6.existsSync)(machinePath);
437
+ if (!superseded2) {
438
+ (0, import_node_fs6.mkdirSync)((0, import_node_path4.dirname)(machinePath), { recursive: true });
439
+ (0, import_node_fs6.copyFileSync)(legacyPath, machinePath);
440
+ (0, import_node_fs6.chmodSync)(machinePath, 384);
441
+ }
442
+ (0, import_node_fs6.rmSync)(legacyPath, { force: true });
443
+ out?.error(
444
+ superseded2 ? `auth: removed superseded ${legacyPath}; this machine's credentials live in ${odlaHome()}` : `auth: moved ${legacyPath} into ${machinePath}; credentials are per machine now, not per worktree`
445
+ );
446
+ return true;
447
+ }
448
+ function safeSegment(value2) {
449
+ const clean4 = value2.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "");
450
+ if (!clean4) throw new Error(`"${value2}" is not a usable app id`);
451
+ return clean4;
452
+ }
453
+ var import_node_fs6, import_node_os2, import_node_path4, import_node_process5;
454
+ var init_odla_home = __esm({
455
+ "src/odla-home.ts"() {
456
+ "use strict";
457
+ init_cjs_shims();
458
+ import_node_fs6 = require("fs");
459
+ import_node_os2 = require("os");
460
+ import_node_path4 = require("path");
461
+ import_node_process5 = __toESM(require("process"), 1);
462
+ }
463
+ });
464
+
424
465
  // src/local.ts
425
466
  function readJsonFile(path) {
426
467
  try {
@@ -467,7 +508,7 @@ function mergeCredential(current, update) {
467
508
  return next;
468
509
  }
469
510
  function ensureGitignore(rootDir, localPaths = []) {
470
- const path = (0, import_node_path4.resolve)(rootDir, ".gitignore");
511
+ const path = (0, import_node_path5.resolve)(rootDir, ".gitignore");
471
512
  const existing = (0, import_node_fs7.existsSync)(path) ? (0, import_node_fs7.readFileSync)(path, "utf8") : "";
472
513
  const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line2) => !!line2);
473
514
  const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
@@ -488,7 +529,7 @@ function o11yDevVars(cfg) {
488
529
  function resolveWriteDevVarsTarget(cfg, requested) {
489
530
  if (!requested) return null;
490
531
  if (requested === true) return cfg.local.devVarsFile;
491
- return (0, import_node_path4.resolve)((0, import_node_path4.dirname)(cfg.configPath), requested);
532
+ return (0, import_node_path5.resolve)((0, import_node_path5.dirname)(cfg.configPath), requested);
492
533
  }
493
534
  function writeDevVars(path, credentials, env, o11y) {
494
535
  const entry = credentials.envs[env];
@@ -516,28 +557,28 @@ function isManagedDevVar(line2) {
516
557
  return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
517
558
  }
518
559
  function writePrivateText(path, text3) {
519
- (0, import_node_fs7.mkdirSync)((0, import_node_path4.dirname)(path), { recursive: true });
560
+ (0, import_node_fs7.mkdirSync)((0, import_node_path5.dirname)(path), { recursive: true });
520
561
  const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
521
562
  (0, import_node_fs7.writeFileSync)(temporary, text3, { mode: 384 });
522
563
  (0, import_node_fs7.chmodSync)(temporary, 384);
523
564
  (0, import_node_fs7.renameSync)(temporary, path);
524
565
  }
525
566
  function gitignoreEntry(rootDir, path) {
526
- const rel = (0, import_node_path4.relative)((0, import_node_path4.resolve)(rootDir), (0, import_node_path4.resolve)(path));
527
- if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path4.isAbsolute)(rel)) return null;
567
+ const rel = (0, import_node_path5.relative)((0, import_node_path5.resolve)(rootDir), (0, import_node_path5.resolve)(path));
568
+ if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path5.isAbsolute)(rel)) return null;
528
569
  return rel.replaceAll("\\", "/");
529
570
  }
530
571
  function displayPath(path, rootDir = process.cwd()) {
531
- const rel = (0, import_node_path4.relative)(rootDir, path);
572
+ const rel = (0, import_node_path5.relative)(rootDir, path);
532
573
  return rel && !rel.startsWith("..") ? rel : path;
533
574
  }
534
- var import_node_fs7, import_node_path4, GITIGNORE_LINES, MANAGED_DEV_VARS;
575
+ var import_node_fs7, import_node_path5, GITIGNORE_LINES, MANAGED_DEV_VARS;
535
576
  var init_local = __esm({
536
577
  "src/local.ts"() {
537
578
  "use strict";
538
579
  init_cjs_shims();
539
580
  import_node_fs7 = require("fs");
540
- import_node_path4 = require("path");
581
+ import_node_path5 = require("path");
541
582
  GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
542
583
  MANAGED_DEV_VARS = /* @__PURE__ */ new Set([
543
584
  "ODLA_PLATFORM",
@@ -554,6 +595,147 @@ var init_local = __esm({
554
595
  }
555
596
  });
556
597
 
598
+ // src/auth-guidance.ts
599
+ function machineAuthState(audience, env = import_node_process6.default.env) {
600
+ const device = readDeviceCredential(audience, env);
601
+ if (!device) return { enrolled: false };
602
+ const session = readJsonFile(deviceSessionFile(env));
603
+ const current = session?.platform === audience && session.deviceId === device.deviceId ? session : void 0;
604
+ return {
605
+ enrolled: true,
606
+ ...device.name ? { deviceName: device.name } : {},
607
+ ...current?.appIds ? { appIds: current.appIds } : {},
608
+ ...current?.capabilities ? { capabilities: current.capabilities } : {},
609
+ ...current?.scopes ? { scopes: current.scopes } : {},
610
+ ...current?.deviceExpiresAt ? { lapsesAt: current.deviceExpiresAt } : {}
611
+ };
612
+ }
613
+ function scopeInterruptionNotice(scope, state2) {
614
+ const platformScope = scope.startsWith("platform:");
615
+ 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}"`;
616
+ return [
617
+ `odla: ${reason}.`,
618
+ ` Approve this one now, then end the interruptions with:`,
619
+ ` ${platformScope ? ENROL_PLATFORM_WIDE : ENROL_EVERYTHING}`,
620
+ 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."
621
+ ].join("\n");
622
+ }
623
+ function narrowEnrollmentNotice(envelope) {
624
+ const everyApp = envelope.appIds.includes("*");
625
+ if (everyApp && envelope.capabilities.length > 0 && envelope.scopes.length > 0) return null;
626
+ const missing = [
627
+ everyApp ? null : `only ${envelope.appIds.length} app${envelope.appIds.length === 1 ? "" : "s"} \u2014 a new one will need a new approval`,
628
+ envelope.capabilities.length ? null : "no optional capabilities \u2014 app.manage, crm.read and code.session are not included",
629
+ envelope.scopes.length ? null : "no platform scopes \u2014 runbook edits, config plans and host connect will each ask again"
630
+ ].filter(Boolean);
631
+ return [
632
+ `odla: this is a NARROW enrollment: ${missing.join("; ")}.`,
633
+ " That is a fine choice if you meant it. If you did not, drop the flags:",
634
+ ` ${ENROL_EVERYTHING}`
635
+ ].join("\n");
636
+ }
637
+ function lapseNotice(state2, now = Date.now()) {
638
+ if (!state2.enrolled || !state2.lapsesAt) return null;
639
+ const days = Math.floor((state2.lapsesAt - now) / (24 * 60 * 60 * 1e3));
640
+ if (days < 0) return "this machine's enrollment has lapsed; the next command will ask for approval";
641
+ return `idle for ${days} more day${days === 1 ? "" : "s"} before this machine needs approving again (using it resets the clock)`;
642
+ }
643
+ var import_node_process6, ENROL_EVERYTHING, ENROL_PLATFORM_WIDE;
644
+ var init_auth_guidance = __esm({
645
+ "src/auth-guidance.ts"() {
646
+ "use strict";
647
+ init_cjs_shims();
648
+ import_node_process6 = __toESM(require("process"), 1);
649
+ init_device_session();
650
+ init_odla_home();
651
+ init_local();
652
+ ENROL_EVERYTHING = "npx odla-ai device enroll --no-open --wait 600";
653
+ ENROL_PLATFORM_WIDE = "npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600";
654
+ }
655
+ });
656
+
657
+ // src/cached-credential.ts
658
+ function noteCachedCredential(tokenFile) {
659
+ noted = tokenFile;
660
+ }
661
+ function isCredentialRejection(error) {
662
+ const message2 = error instanceof Error ? error.message : String(error ?? "");
663
+ return /\((401|403)\)\s*$/.test(message2.trim());
664
+ }
665
+ function explainRejectedCredential(error) {
666
+ const tokenFile = noted;
667
+ if (!tokenFile || !isCredentialRejection(error)) return null;
668
+ noted = null;
669
+ (0, import_node_fs8.rmSync)(tokenFile, { force: true });
670
+ return [
671
+ "auth: the cached credential was rejected by odla, so it was revoked before its cached expiry.",
672
+ " The usual cause is a newer sign-in for this account: collecting a handshake retires the",
673
+ " principal's other collected credentials, so a second machine supersedes this one.",
674
+ ` Discarded ${tokenFile}; re-run this command to request a fresh approval.`,
675
+ ` To stop needing one: ${ENROL_EVERYTHING}`
676
+ ].join("\n");
677
+ }
678
+ var import_node_fs8, noted;
679
+ var init_cached_credential = __esm({
680
+ "src/cached-credential.ts"() {
681
+ "use strict";
682
+ init_cjs_shims();
683
+ import_node_fs8 = require("fs");
684
+ init_auth_guidance();
685
+ noted = null;
686
+ }
687
+ });
688
+
689
+ // src/device-session-cache.ts
690
+ async function deviceSessionToken(platformUrl, audience, credential2, doFetch, env = import_node_process7.default.env) {
691
+ const path = deviceSessionFile(env);
692
+ const cached = readJsonFile(path);
693
+ if (cached?.token && cached.platform === audience && cached.deviceId === credential2.deviceId && (cached.expiresAt ?? 0) > Date.now() + SKEW_MS) return cached;
694
+ const minted = await mintDeviceSession(platformUrl, credential2, doFetch);
695
+ const session = {
696
+ ...minted,
697
+ platform: audience,
698
+ ...credential2.deviceId ? { deviceId: credential2.deviceId } : {}
699
+ };
700
+ writePrivateJson(path, session);
701
+ return session;
702
+ }
703
+ var import_node_process7, SKEW_MS;
704
+ var init_device_session_cache = __esm({
705
+ "src/device-session-cache.ts"() {
706
+ "use strict";
707
+ init_cjs_shims();
708
+ import_node_process7 = __toESM(require("process"), 1);
709
+ init_odla_home();
710
+ init_local();
711
+ init_device_session();
712
+ SKEW_MS = 6e4;
713
+ }
714
+ });
715
+
716
+ // src/machine-identity.ts
717
+ function readMachineIdentity(audience, env = import_node_process8.default.env) {
718
+ const stored = readJsonFile(identityFile(env));
719
+ if (!stored || typeof stored.email !== "string" || !stored.email) return null;
720
+ return stored.platform === audience ? { platform: audience, email: stored.email } : null;
721
+ }
722
+ function rememberMachineIdentity(audience, email, env = import_node_process8.default.env) {
723
+ if (!email) return;
724
+ const existing = readMachineIdentity(audience, env);
725
+ if (existing?.email === email) return;
726
+ writePrivateJson(identityFile(env), { platform: audience, email });
727
+ }
728
+ var import_node_process8;
729
+ var init_machine_identity = __esm({
730
+ "src/machine-identity.ts"() {
731
+ "use strict";
732
+ init_cjs_shims();
733
+ import_node_process8 = __toESM(require("process"), 1);
734
+ init_odla_home();
735
+ init_local();
736
+ }
737
+ });
738
+
557
739
  // src/token.ts
558
740
  async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {}) {
559
741
  const audience = platformAudience(cfg.platformUrl);
@@ -562,19 +744,19 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
562
744
  const cached = readJsonFile(cfg.local.tokenFile);
563
745
  if (!grantRequest.forceReview && !grantRequest.freshLogin) {
564
746
  if (options.token) return options.token;
565
- if (import_node_process5.default.env.ODLA_DEV_TOKEN) {
566
- const declared = import_node_process5.default.env.ODLA_DEV_TOKEN_AUDIENCE;
747
+ if (import_node_process9.default.env.ODLA_DEV_TOKEN) {
748
+ const declared = import_node_process9.default.env.ODLA_DEV_TOKEN_AUDIENCE;
567
749
  if (declared) {
568
750
  if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
569
751
  } else if (audience !== "https://odla.ai") {
570
752
  throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
571
753
  }
572
- return import_node_process5.default.env.ODLA_DEV_TOKEN;
754
+ return import_node_process9.default.env.ODLA_DEV_TOKEN;
573
755
  }
574
756
  const device = readDeviceCredential(audience);
575
757
  if (device) {
576
- const session = await mintDeviceSession(cfg.platformUrl, device, doFetch);
577
- out.error(`auth: session minted by this enrolled device (${displayPath(deviceCredentialPath(), cfg.rootDir)})`);
758
+ const session = await deviceSessionToken(cfg.platformUrl, audience, device, doFetch);
759
+ out.error(`auth: session held by this enrolled device (${displayPath(deviceCredentialPath(), cfg.rootDir)})`);
578
760
  return session.token;
579
761
  }
580
762
  if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4 && cachedGrantCovers(cached, grantIntent)) {
@@ -594,7 +776,10 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
594
776
  doFetch,
595
777
  out,
596
778
  audience,
597
- email: handshakeEmail(options.email, cached?.platform === audience ? cached.email : void 0),
779
+ email: handshakeEmail(
780
+ options.email,
781
+ (cached?.platform === audience ? cached.email : void 0) ?? readMachineIdentity(audience)?.email
782
+ ),
598
783
  pendingFile: handshakeFile(cfg),
599
784
  grantIntent
600
785
  };
@@ -611,6 +796,7 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
611
796
  expiresAt
612
797
  });
613
798
  out.error(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
799
+ rememberMachineIdentity(audience, ctx.email);
614
800
  return token;
615
801
  }
616
802
  async function freshHandshake(ctx, waitMs) {
@@ -680,7 +866,7 @@ function stillPending(pending, email) {
680
866
  );
681
867
  }
682
868
  function handshakeEmail(value2, cached) {
683
- const email = (value2 ?? import_node_process5.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
869
+ const email = (value2 ?? import_node_process9.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
684
870
  if (/@users\.noreply\.github\.com$/i.test(email)) {
685
871
  throw new Error(
686
872
  `"${email}" is a GitHub commit identity, not an odla account email; use --email <signed-in-odla-account> or ODLA_USER_EMAIL`
@@ -709,18 +895,20 @@ function platformAudience(value2) {
709
895
  }
710
896
  return url.origin;
711
897
  }
712
- var import_db, import_node_crypto, import_node_process5;
898
+ var import_db, import_node_crypto, import_node_process9;
713
899
  var init_token = __esm({
714
900
  "src/token.ts"() {
715
901
  "use strict";
716
902
  init_cjs_shims();
717
903
  import_db = require("@odla-ai/db");
718
904
  import_node_crypto = require("crypto");
719
- import_node_process5 = __toESM(require("process"), 1);
905
+ import_node_process9 = __toESM(require("process"), 1);
720
906
  init_handshake_approval();
721
907
  init_handshake_state();
722
908
  init_cached_credential();
723
909
  init_device_session();
910
+ init_device_session_cache();
911
+ init_machine_identity();
724
912
  init_local();
725
913
  }
726
914
  });
@@ -729,7 +917,7 @@ var init_token = __esm({
729
917
  async function secretInputValue(options, kind = "credential") {
730
918
  if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
731
919
  let value2;
732
- if (options.fromEnv) value2 = import_node_process6.default.env[options.fromEnv];
920
+ if (options.fromEnv) value2 = import_node_process10.default.env[options.fromEnv];
733
921
  else if (options.stdin) value2 = await (options.readStdin ?? (() => readSecretStream(kind)))();
734
922
  else throw new Error(`${kind} input required: use --from-env <NAME> or --stdin; values are never accepted as arguments`);
735
923
  value2 = value2?.replace(/[\r\n]+$/, "");
@@ -737,7 +925,7 @@ async function secretInputValue(options, kind = "credential") {
737
925
  if (new TextEncoder().encode(value2).byteLength > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
738
926
  return value2;
739
927
  }
740
- async function readSecretStream(kind, stream = import_node_process6.default.stdin) {
928
+ async function readSecretStream(kind, stream = import_node_process10.default.stdin) {
741
929
  let value2 = "";
742
930
  for await (const chunk of stream) {
743
931
  value2 += String(chunk);
@@ -745,12 +933,12 @@ async function readSecretStream(kind, stream = import_node_process6.default.stdi
745
933
  }
746
934
  return value2;
747
935
  }
748
- var import_node_process6, MAX_BYTES;
936
+ var import_node_process10, MAX_BYTES;
749
937
  var init_secret_input = __esm({
750
938
  "src/secret-input.ts"() {
751
939
  "use strict";
752
940
  init_cjs_shims();
753
- import_node_process6 = __toESM(require("process"), 1);
941
+ import_node_process10 = __toESM(require("process"), 1);
754
942
  MAX_BYTES = 64 * 1024;
755
943
  }
756
944
  });
@@ -762,7 +950,7 @@ async function getScopedPlatformToken(options) {
762
950
  async function resolveAdminPlatformToken(options) {
763
951
  const audience = platformAudience(options.platform);
764
952
  if (options.token) return options.token;
765
- const fromEnv = import_node_process7.default.env.ODLA_ADMIN_TOKEN;
953
+ const fromEnv = import_node_process11.default.env.ODLA_ADMIN_TOKEN;
766
954
  if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
767
955
  return scopedToken(
768
956
  audience,
@@ -774,7 +962,7 @@ async function resolveAdminPlatformToken(options) {
774
962
  }
775
963
  function audienceBoundEnvToken(token, platform) {
776
964
  const audience = platformAudience(platform);
777
- const declared = import_node_process7.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
965
+ const declared = import_node_process11.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
778
966
  if (declared) {
779
967
  if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
780
968
  } else if (audience !== "https://odla.ai") {
@@ -784,15 +972,28 @@ function audienceBoundEnvToken(token, platform) {
784
972
  }
785
973
  async function scopedToken(platform, scope, options, doFetch, out) {
786
974
  const audience = platformAudience(platform);
787
- const rootDir = options.rootDir ?? import_node_process7.default.cwd();
788
- const tokenFile = options.tokenFile ?? (0, import_node_path5.join)(rootDir, ".odla/admin-token.local.json");
975
+ const rootDir = options.rootDir ?? import_node_process11.default.cwd();
976
+ const tokenFile = options.tokenFile ?? scopedTokenFile();
977
+ adoptRepoLocalCache((0, import_node_path6.join)(rootDir, ".odla/admin-token.local.json"), tokenFile, out);
978
+ const device = readDeviceCredential(audience);
979
+ if (device && options.cache !== false) {
980
+ const session = await deviceSessionToken(platform, audience, device, doFetch);
981
+ if (session.scopes?.includes(scope)) {
982
+ out.error(`auth: ${scope} held by this enrolled device`);
983
+ return session.token;
984
+ }
985
+ }
789
986
  const cache2 = options.cache === false ? null : readJsonFile(tokenFile);
790
987
  const cached = cache2?.platform === audience ? cache2.tokens?.[scope] : void 0;
791
988
  if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
792
989
  out.error(`auth: using cached ${scope} grant (${tokenFile})`);
793
990
  return cached.token;
794
991
  }
795
- const email = handshakeEmail(options.email, cache2?.platform === audience ? cache2.email : void 0);
992
+ out.error(scopeInterruptionNotice(scope, machineAuthState(audience)));
993
+ const email = handshakeEmail(
994
+ options.email,
995
+ (cache2?.platform === audience ? cache2.email : void 0) ?? readMachineIdentity(audience)?.email
996
+ );
796
997
  const { token, expiresAt } = await (0, import_db2.requestToken)({
797
998
  endpoint: audience,
798
999
  email,
@@ -812,26 +1013,30 @@ async function scopedToken(platform, scope, options, doFetch, out) {
812
1013
  if (options.cache !== false) {
813
1014
  const tokens = cache2?.platform === audience ? { ...cache2.tokens ?? {} } : {};
814
1015
  tokens[scope] = { token, expiresAt };
815
- if ((0, import_node_fs8.existsSync)((0, import_node_path5.join)(rootDir, ".git"))) ensureGitignore(rootDir, [tokenFile]);
816
1016
  writePrivateJson(tokenFile, { platform: audience, email, tokens });
1017
+ rememberMachineIdentity(audience, email);
817
1018
  out.error(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
818
1019
  } else {
819
1020
  out.error(`auth: ${scope} grant is in memory only; its credential record remains in odla-ai/db`);
820
1021
  }
821
1022
  return token;
822
1023
  }
823
- var import_node_fs8, import_node_path5, import_node_process7, import_db2, SCOPE_PURPOSE;
1024
+ var import_node_path6, import_node_process11, import_db2, SCOPE_PURPOSE;
824
1025
  var init_admin_ai_auth = __esm({
825
1026
  "src/admin-ai-auth.ts"() {
826
1027
  "use strict";
827
1028
  init_cjs_shims();
828
- import_node_fs8 = require("fs");
829
- import_node_path5 = require("path");
830
- import_node_process7 = __toESM(require("process"), 1);
1029
+ import_node_path6 = require("path");
1030
+ import_node_process11 = __toESM(require("process"), 1);
831
1031
  import_db2 = require("@odla-ai/db");
832
1032
  init_local();
833
1033
  init_handshake_approval();
834
1034
  init_token();
1035
+ init_auth_guidance();
1036
+ init_device_session_cache();
1037
+ init_device_session();
1038
+ init_machine_identity();
1039
+ init_odla_home();
835
1040
  SCOPE_PURPOSE = {
836
1041
  "platform:status:read": "read the platform fleet health and deployment snapshot",
837
1042
  "app:config:read": "compare checked-in intent with an exact-id app Registry configuration",
@@ -1047,7 +1252,7 @@ var init_admin_ai_usage = __esm({
1047
1252
 
1048
1253
  // src/admin-ai.ts
1049
1254
  async function adminAi(options) {
1050
- const platform = platformAudience(options.platform ?? import_node_process8.default.env.ODLA_PLATFORM ?? "https://odla.ai");
1255
+ const platform = platformAudience(options.platform ?? import_node_process12.default.env.ODLA_PLATFORM ?? "https://odla.ai");
1051
1256
  const doFetch = options.fetch ?? fetch;
1052
1257
  const out = options.stdout ?? console;
1053
1258
  const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
@@ -1225,12 +1430,12 @@ function apiError3(action2, status, body) {
1225
1430
  function isRecord3(value2) {
1226
1431
  return Boolean(value2) && typeof value2 === "object" && !Array.isArray(value2);
1227
1432
  }
1228
- var import_node_process8;
1433
+ var import_node_process12;
1229
1434
  var init_admin_ai = __esm({
1230
1435
  "src/admin-ai.ts"() {
1231
1436
  "use strict";
1232
1437
  init_cjs_shims();
1233
- import_node_process8 = __toESM(require("process"), 1);
1438
+ import_node_process12 = __toESM(require("process"), 1);
1234
1439
  init_token();
1235
1440
  init_secret_input();
1236
1441
  init_admin_ai_auth();
@@ -1812,12 +2017,12 @@ var init_monitoring_validation = __esm({
1812
2017
 
1813
2018
  // src/config.ts
1814
2019
  async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1815
- const resolved = (0, import_node_path6.resolve)(configPath);
2020
+ const resolved = (0, import_node_path7.resolve)(configPath);
1816
2021
  if (!(0, import_node_fs9.existsSync)(resolved)) {
1817
2022
  throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
1818
2023
  }
1819
2024
  const raw = await loadConfigModule(resolved);
1820
- const rootDir = (0, import_node_path6.dirname)(resolved);
2025
+ const rootDir = (0, import_node_path7.dirname)(resolved);
1821
2026
  validateRawConfig(raw, resolved);
1822
2027
  const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
1823
2028
  const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
@@ -1827,11 +2032,14 @@ async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1827
2032
  validateCalendarConfig(raw, unique3([...envs, ...options.additionalEnvs ?? []]), services, resolved);
1828
2033
  validateMonitoringConfig(raw, unique3([...envs, ...options.additionalEnvs ?? []]), services, resolved);
1829
2034
  const local = {
1830
- tokenFile: (0, import_node_path6.resolve)(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
1831
- credentialsFile: (0, import_node_path6.resolve)(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
1832
- devVarsFile: (0, import_node_path6.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
2035
+ tokenFile: raw.local?.tokenFile ? (0, import_node_path7.resolve)(rootDir, raw.local.tokenFile) : appTokenFile(raw.app.id),
2036
+ credentialsFile: raw.local?.credentialsFile ? (0, import_node_path7.resolve)(rootDir, raw.local.credentialsFile) : appCredentialsFile(raw.app.id),
2037
+ devVarsFile: (0, import_node_path7.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
1833
2038
  gitignore: raw.local?.gitignore ?? true
1834
2039
  };
2040
+ adoptRepoLocalCache((0, import_node_path7.resolve)(rootDir, ".odla/dev-token.json"), local.tokenFile, stderr);
2041
+ adoptRepoLocalCache((0, import_node_path7.resolve)(rootDir, ".odla/credentials.local.json"), local.credentialsFile, stderr);
2042
+ (0, import_node_fs9.rmSync)((0, import_node_path7.resolve)(rootDir, ".odla/handshake.local.json"), { force: true });
1835
2043
  return {
1836
2044
  ...raw,
1837
2045
  configPath: resolved,
@@ -1846,7 +2054,7 @@ async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1846
2054
  async function resolveDataExport(cfg, value2, names) {
1847
2055
  if (value2 === void 0 || value2 === null || value2 === false) return void 0;
1848
2056
  if (typeof value2 !== "string") return value2;
1849
- const target = (0, import_node_path6.isAbsolute)(value2) ? value2 : (0, import_node_path6.resolve)(cfg.rootDir, value2);
2057
+ const target = (0, import_node_path7.isAbsolute)(value2) ? value2 : (0, import_node_path7.resolve)(cfg.rootDir, value2);
1850
2058
  if (target.endsWith(".json")) {
1851
2059
  return JSON.parse((0, import_node_fs9.readFileSync)(target, "utf8"));
1852
2060
  }
@@ -1937,37 +2145,42 @@ function trimSlash(value2) {
1937
2145
  function unique3(values) {
1938
2146
  return [...new Set(values.filter(Boolean))];
1939
2147
  }
1940
- var import_node_fs9, import_node_path6, import_node_url, import_apps, DEFAULT_PLATFORM, DEFAULT_ENVS, DEFAULT_SERVICES, configImportSerial, GOOGLE_CALENDAR_EVENTS_SCOPE;
2148
+ var import_node_fs9, import_node_path7, import_node_url, import_apps, DEFAULT_PLATFORM, DEFAULT_ENVS, DEFAULT_SERVICES, configImportSerial, GOOGLE_CALENDAR_EVENTS_SCOPE, stderr;
1941
2149
  var init_config = __esm({
1942
2150
  "src/config.ts"() {
1943
2151
  "use strict";
1944
2152
  init_cjs_shims();
1945
2153
  import_node_fs9 = require("fs");
1946
- import_node_path6 = require("path");
2154
+ import_node_path7 = require("path");
1947
2155
  import_node_url = require("url");
1948
2156
  import_apps = require("@odla-ai/apps");
1949
2157
  init_ai_config_validation();
1950
2158
  init_calendar_config();
1951
2159
  init_integration_validation();
1952
2160
  init_monitoring_validation();
2161
+ init_odla_home();
1953
2162
  init_calendar_config();
1954
2163
  DEFAULT_PLATFORM = "https://odla.ai";
1955
2164
  DEFAULT_ENVS = ["dev"];
1956
2165
  DEFAULT_SERVICES = ["db", "ai"];
1957
2166
  configImportSerial = 0;
1958
2167
  GOOGLE_CALENDAR_EVENTS_SCOPE = "https://www.googleapis.com/auth/calendar.events";
2168
+ stderr = { error: (message2) => {
2169
+ process.stderr.write(`${message2}
2170
+ `);
2171
+ } };
1959
2172
  }
1960
2173
  });
1961
2174
 
1962
2175
  // src/operator-profiles.ts
1963
2176
  function operatorProfileFile() {
1964
- return (0, import_node_path7.resolve)(
1965
- clean(import_node_process9.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path7.join)((0, import_node_os2.homedir)(), ".odla", "contexts.json")
2177
+ return (0, import_node_path8.resolve)(
2178
+ clean(import_node_process13.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path8.join)((0, import_node_os3.homedir)(), ".odla", "contexts.json")
1966
2179
  );
1967
2180
  }
1968
2181
  function resolveOperatorProfile(parsed) {
1969
2182
  const fromFlag = clean(stringOpt(parsed.options.context));
1970
- const fromEnvironment = clean(import_node_process9.default.env.ODLA_CONTEXT);
2183
+ const fromEnvironment = clean(import_node_process13.default.env.ODLA_CONTEXT);
1971
2184
  const name = fromFlag ?? fromEnvironment ?? null;
1972
2185
  const file = operatorProfileFile();
1973
2186
  if (!name) {
@@ -2007,10 +2220,10 @@ function removeOperatorProfile(name, file = operatorProfileFile()) {
2007
2220
  return true;
2008
2221
  }
2009
2222
  function operatorCredentialFiles(selection) {
2010
- 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_os2.homedir)(), ".odla");
2223
+ const base = selection.name ? (0, import_node_path8.join)((0, import_node_path8.dirname)(selection.file), "profiles", selection.name) : (0, import_node_path8.join)((0, import_node_os3.homedir)(), ".odla");
2011
2224
  return {
2012
- developer: (0, import_node_path7.join)(base, "dev-token.json"),
2013
- scoped: (0, import_node_path7.join)(base, "admin-token.local.json")
2225
+ developer: (0, import_node_path8.join)(base, "dev-token.json"),
2226
+ scoped: (0, import_node_path8.join)(base, "admin-token.local.json")
2014
2227
  };
2015
2228
  }
2016
2229
  function assertOperatorName(value2, label) {
@@ -2082,15 +2295,15 @@ function clean(value2) {
2082
2295
  const normalized = value2?.trim();
2083
2296
  return normalized || void 0;
2084
2297
  }
2085
- var import_node_fs10, import_node_os2, import_node_path7, import_node_process9;
2298
+ var import_node_fs10, import_node_os3, import_node_path8, import_node_process13;
2086
2299
  var init_operator_profiles = __esm({
2087
2300
  "src/operator-profiles.ts"() {
2088
2301
  "use strict";
2089
2302
  init_cjs_shims();
2090
2303
  import_node_fs10 = require("fs");
2091
- import_node_os2 = require("os");
2092
- import_node_path7 = require("path");
2093
- import_node_process9 = __toESM(require("process"), 1);
2304
+ import_node_os3 = require("os");
2305
+ import_node_path8 = require("path");
2306
+ import_node_process13 = __toESM(require("process"), 1);
2094
2307
  init_argv();
2095
2308
  init_local();
2096
2309
  init_token();
@@ -2101,7 +2314,7 @@ var init_operator_profiles = __esm({
2101
2314
  async function resolveOperatorContext(parsed, options = {}) {
2102
2315
  const profile = resolveOperatorProfile(parsed);
2103
2316
  const configArgument = stringOpt(parsed.options.config) ?? "odla.config.mjs";
2104
- const configPath = (0, import_node_path8.resolve)(configArgument);
2317
+ const configPath = (0, import_node_path9.resolve)(configArgument);
2105
2318
  const explicitConfig = parsed.options.config !== void 0;
2106
2319
  const hasConfig = (0, import_node_fs11.existsSync)(configPath);
2107
2320
  if (!hasConfig && (!options.allowMissingConfig || explicitConfig)) {
@@ -2109,13 +2322,13 @@ async function resolveOperatorContext(parsed, options = {}) {
2109
2322
  }
2110
2323
  const loaded = hasConfig ? await loadProjectConfig(configArgument) : void 0;
2111
2324
  const platformFlag = clean2(stringOpt(parsed.options.platform));
2112
- const platformEnvironment = clean2(import_node_process10.default.env.ODLA_PLATFORM_URL);
2325
+ const platformEnvironment = clean2(import_node_process14.default.env.ODLA_PLATFORM_URL);
2113
2326
  const platformValue = platformAudience(
2114
2327
  platformFlag ?? platformEnvironment ?? profile.value?.platform ?? loaded?.platformUrl ?? DEFAULT_PLATFORM2
2115
2328
  );
2116
2329
  const platformSource = platformFlag ? "flag" : platformEnvironment ? "environment" : profile.value ? "profile" : loaded ? "config" : "default";
2117
2330
  const appFlag = clean2(stringOpt(parsed.options.app));
2118
- const appEnvironment = clean2(import_node_process10.default.env.ODLA_APP_ID);
2331
+ const appEnvironment = clean2(import_node_process14.default.env.ODLA_APP_ID);
2119
2332
  const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
2120
2333
  const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
2121
2334
  if (appValue) {
@@ -2129,16 +2342,16 @@ async function resolveOperatorContext(parsed, options = {}) {
2129
2342
  );
2130
2343
  }
2131
2344
  const envFlag = clean2(stringOpt(parsed.options.env));
2132
- const envEnvironment = clean2(import_node_process10.default.env.ODLA_ENV);
2345
+ const envEnvironment = clean2(import_node_process14.default.env.ODLA_ENV);
2133
2346
  const environmentValue = envFlag ?? envEnvironment ?? profile.value?.environment ?? options.defaultEnvironment ?? null;
2134
2347
  const environmentSource = envFlag ? "flag" : envEnvironment ? "environment" : profile.value?.environment ? "profile" : options.defaultEnvironment ? "default" : "unresolved";
2135
2348
  if (environmentValue) {
2136
2349
  assertOperatorName(environmentValue, "environment");
2137
2350
  }
2138
- const rootDir = loaded?.rootDir ?? import_node_process10.default.cwd();
2351
+ const rootDir = loaded?.rootDir ?? import_node_process14.default.cwd();
2139
2352
  const profileCredentials = operatorCredentialFiles(profile);
2140
- const tokenFile = clean2(import_node_process10.default.env.ODLA_DEV_TOKEN_FILE) ? (0, import_node_path8.resolve)(import_node_process10.default.env.ODLA_DEV_TOKEN_FILE) : profile.name ? profileCredentials.developer : loaded?.local.tokenFile ?? profileCredentials.developer;
2141
- const scopedTokenFile = clean2(import_node_process10.default.env.ODLA_ADMIN_TOKEN_FILE) ? (0, import_node_path8.resolve)(import_node_process10.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;
2353
+ const tokenFile = clean2(import_node_process14.default.env.ODLA_DEV_TOKEN_FILE) ? (0, import_node_path9.resolve)(import_node_process14.default.env.ODLA_DEV_TOKEN_FILE) : profile.name ? profileCredentials.developer : loaded?.local.tokenFile ?? profileCredentials.developer;
2354
+ const scopedTokenFile2 = clean2(import_node_process14.default.env.ODLA_ADMIN_TOKEN_FILE) ? (0, import_node_path9.resolve)(import_node_process14.default.env.ODLA_ADMIN_TOKEN_FILE) : profile.name ? profileCredentials.scoped : loaded ? (0, import_node_path9.join)(loaded.rootDir, ".odla", "admin-token.local.json") : profileCredentials.scoped;
2142
2355
  const cfg = loaded ? {
2143
2356
  ...loaded,
2144
2357
  platformUrl: platformValue,
@@ -2160,8 +2373,8 @@ async function resolveOperatorContext(parsed, options = {}) {
2160
2373
  services: [],
2161
2374
  local: {
2162
2375
  tokenFile,
2163
- credentialsFile: (0, import_node_path8.join)(rootDir, ".odla", "credentials.local.json"),
2164
- devVarsFile: (0, import_node_path8.join)(rootDir, ".dev.vars"),
2376
+ credentialsFile: (0, import_node_path9.join)(rootDir, ".odla", "credentials.local.json"),
2377
+ devVarsFile: (0, import_node_path9.join)(rootDir, ".dev.vars"),
2165
2378
  gitignore: true
2166
2379
  }
2167
2380
  };
@@ -2185,7 +2398,7 @@ async function resolveOperatorContext(parsed, options = {}) {
2185
2398
  },
2186
2399
  credentials: {
2187
2400
  developerTokenFile: tokenFile,
2188
- scopedTokenFile
2401
+ scopedTokenFile: scopedTokenFile2
2189
2402
  }
2190
2403
  };
2191
2404
  }
@@ -2193,14 +2406,14 @@ function clean2(value2) {
2193
2406
  const normalized = value2?.trim();
2194
2407
  return normalized || void 0;
2195
2408
  }
2196
- var import_node_fs11, import_node_path8, import_node_process10, DEFAULT_PLATFORM2;
2409
+ var import_node_fs11, import_node_path9, import_node_process14, DEFAULT_PLATFORM2;
2197
2410
  var init_operator_context = __esm({
2198
2411
  "src/operator-context.ts"() {
2199
2412
  "use strict";
2200
2413
  init_cjs_shims();
2201
2414
  import_node_fs11 = require("fs");
2202
- import_node_path8 = require("path");
2203
- import_node_process10 = __toESM(require("process"), 1);
2415
+ import_node_path9 = require("path");
2416
+ import_node_process14 = __toESM(require("process"), 1);
2204
2417
  init_argv();
2205
2418
  init_config();
2206
2419
  init_operator_profiles();
@@ -2442,6 +2655,7 @@ async function whoamiCommand(parsed, deps = {}) {
2442
2655
  } else {
2443
2656
  out.log("projects: (none \u2014 every pm and discuss call will be refused)");
2444
2657
  }
2658
+ printMachineBlock(cfg.platformUrl, out);
2445
2659
  if (!identity.admin) {
2446
2660
  if (identity.scopes.includes("platform:runbook:write")) {
2447
2661
  out.log("\nThis exact scope can read and edit all platform runbook content.");
@@ -2452,6 +2666,21 @@ async function whoamiCommand(parsed, deps = {}) {
2452
2666
  }
2453
2667
  }
2454
2668
  }
2669
+ function printMachineBlock(platformUrl, out) {
2670
+ const state2 = machineAuthState(platformAudience(platformUrl));
2671
+ if (!state2.enrolled) {
2672
+ out.log("\nmachine: not enrolled \u2014 every privileged command needs its own browser approval.");
2673
+ out.log(` End that with:
2674
+ ${ENROL_EVERYTHING}`);
2675
+ return;
2676
+ }
2677
+ const reach = state2.appIds?.includes("*") ? "every app you own" : state2.appIds?.join(", ");
2678
+ out.log(`
2679
+ machine: enrolled${state2.deviceName ? ` as "${state2.deviceName}"` : ""}${reach ? ` for ${reach}` : ""}`);
2680
+ if (state2.scopes?.length) out.log(` carrying ${state2.scopes.join(", ")}`);
2681
+ const lapse = lapseNotice(state2);
2682
+ if (lapse) out.log(` ${lapse}`);
2683
+ }
2455
2684
  var text2;
2456
2685
  var init_whoami_command = __esm({
2457
2686
  "src/whoami-command.ts"() {
@@ -2460,6 +2689,8 @@ var init_whoami_command = __esm({
2460
2689
  init_argv();
2461
2690
  init_operator_context();
2462
2691
  init_token();
2692
+ init_auth_guidance();
2693
+ init_token();
2463
2694
  text2 = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
2464
2695
  }
2465
2696
  });
@@ -2487,12 +2718,17 @@ async function authCommand(parsed, deps = {}) {
2487
2718
  const { cfg } = context;
2488
2719
  const out = deps.stdout ?? console;
2489
2720
  const doFetch = deps.fetch ?? fetch;
2490
- const email = stringOpt(parsed.options.email) ?? import_node_process11.default.env.ODLA_USER_EMAIL?.trim();
2721
+ const email = stringOpt(parsed.options.email) ?? import_node_process15.default.env.ODLA_USER_EMAIL?.trim();
2491
2722
  if (!email) {
2492
2723
  throw new Error(
2493
2724
  "auth login requires --email <odla-account> or ODLA_USER_EMAIL; confirm the signed-in odla email instead of using git or GitHub identity"
2494
2725
  );
2495
2726
  }
2727
+ if (!machineAuthState(platformAudience(cfg.platformUrl)).enrolled) {
2728
+ out.error("odla: this machine is not enrolled, so this approval buys one project until it lapses.");
2729
+ out.error(` For one approval that covers every app you own, with no repeats:
2730
+ ${ENROL_EVERYTHING}`);
2731
+ }
2496
2732
  const token = await getDeveloperToken(
2497
2733
  cfg,
2498
2734
  {
@@ -2521,15 +2757,16 @@ async function authCommand(parsed, deps = {}) {
2521
2757
  out.log(`Authorized ${identity.displayName}${handle} for ${cfg.app.id}.`);
2522
2758
  out.log(`odla account: ${identity.email ?? "not returned"}`);
2523
2759
  }
2524
- var import_node_process11;
2760
+ var import_node_process15;
2525
2761
  var init_auth_command = __esm({
2526
2762
  "src/auth-command.ts"() {
2527
2763
  "use strict";
2528
2764
  init_cjs_shims();
2529
- import_node_process11 = __toESM(require("process"), 1);
2765
+ import_node_process15 = __toESM(require("process"), 1);
2530
2766
  init_argv();
2531
2767
  init_operator_context();
2532
2768
  init_token();
2769
+ init_auth_guidance();
2533
2770
  init_whoami_command();
2534
2771
  }
2535
2772
  });
@@ -3043,15 +3280,15 @@ var init_brand_design_unpack = __esm({
3043
3280
 
3044
3281
  // src/brand-command.ts
3045
3282
  async function readBundle(source, deps) {
3046
- if (source !== "-") return (0, import_promises.readFile)((0, import_node_path9.resolve)(source), "utf8");
3283
+ if (source !== "-") return (0, import_promises.readFile)((0, import_node_path10.resolve)(source), "utf8");
3047
3284
  const readStdin = deps.readStdin;
3048
3285
  if (!readStdin) throw new Error("reading a bundle from stdin is not supported here");
3049
3286
  return readStdin();
3050
3287
  }
3051
3288
  async function writeAll(result, outDir) {
3052
3289
  for (const file of result.files) {
3053
- const target = (0, import_node_path9.resolve)(outDir, file.path);
3054
- await (0, import_promises.mkdir)((0, import_node_path9.dirname)(target), { recursive: true });
3290
+ const target = (0, import_node_path10.resolve)(outDir, file.path);
3291
+ await (0, import_promises.mkdir)((0, import_node_path10.dirname)(target), { recursive: true });
3055
3292
  await (0, import_promises.writeFile)(target, file.bytes);
3056
3293
  }
3057
3294
  }
@@ -3059,7 +3296,7 @@ async function designUnpack(parsed, deps) {
3059
3296
  assertArgs(parsed, ["out", "json"], 4);
3060
3297
  const source = parsed.positionals[3];
3061
3298
  if (!source) throw new Error(USAGE);
3062
- const outDir = (0, import_node_path9.resolve)(stringOpt(parsed.options.out) ?? "design");
3299
+ const outDir = (0, import_node_path10.resolve)(stringOpt(parsed.options.out) ?? "design");
3063
3300
  const result = unpackDesign(await readBundle(source, deps));
3064
3301
  await writeAll(result, outDir);
3065
3302
  const out = deps.stdout ?? console;
@@ -3085,13 +3322,13 @@ async function brandCommand(parsed, deps) {
3085
3322
  }
3086
3323
  throw new Error(USAGE);
3087
3324
  }
3088
- var import_promises, import_node_path9, USAGE;
3325
+ var import_promises, import_node_path10, USAGE;
3089
3326
  var init_brand_command = __esm({
3090
3327
  "src/brand-command.ts"() {
3091
3328
  "use strict";
3092
3329
  init_cjs_shims();
3093
3330
  import_promises = require("fs/promises");
3094
- import_node_path9 = require("path");
3331
+ import_node_path10 = require("path");
3095
3332
  init_argv();
3096
3333
  init_brand_design_unpack();
3097
3334
  USAGE = "usage: odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]";
@@ -4139,7 +4376,7 @@ async function operationClient(cfg, options, purpose) {
4139
4376
  platform: cfg.platformUrl,
4140
4377
  scope: "app:config:write",
4141
4378
  token: options.token,
4142
- tokenFile: (0, import_node_path10.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
4379
+ tokenFile: (0, import_node_path11.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
4143
4380
  rootDir: cfg.rootDir,
4144
4381
  email: options.email,
4145
4382
  open: options.open,
@@ -4191,13 +4428,13 @@ function normalizeRequestError(error) {
4191
4428
  function record4(value2) {
4192
4429
  return !!value2 && typeof value2 === "object" && !Array.isArray(value2);
4193
4430
  }
4194
- var import_apps6, import_node_path10, IDEMPOTENCY_KEY, DEFAULT_WAIT_SECONDS, DEFAULT_INTERVAL_SECONDS;
4431
+ var import_apps6, import_node_path11, IDEMPOTENCY_KEY, DEFAULT_WAIT_SECONDS, DEFAULT_INTERVAL_SECONDS;
4195
4432
  var init_config_operation_command = __esm({
4196
4433
  "src/config-operation-command.ts"() {
4197
4434
  "use strict";
4198
4435
  init_cjs_shims();
4199
4436
  import_apps6 = require("@odla-ai/apps");
4200
- import_node_path10 = require("path");
4437
+ import_node_path11 = require("path");
4201
4438
  init_admin_ai_auth();
4202
4439
  init_version();
4203
4440
  init_config();
@@ -4520,7 +4757,7 @@ async function inspectConfig(options) {
4520
4757
  platform: cfg.platformUrl,
4521
4758
  scope: "app:config:read",
4522
4759
  token: options.token,
4523
- tokenFile: (0, import_node_path11.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
4760
+ tokenFile: (0, import_node_path12.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
4524
4761
  rootDir: cfg.rootDir,
4525
4762
  email: options.email,
4526
4763
  open: options.open,
@@ -4649,13 +4886,13 @@ function studioSettingsUrl(reconciliation) {
4649
4886
  function quoteArg2(value2) {
4650
4887
  return `'${value2.replace(/'/g, `'\\''`)}'`;
4651
4888
  }
4652
- var import_apps8, import_node_path11;
4889
+ var import_apps8, import_node_path12;
4653
4890
  var init_config_reconcile_command = __esm({
4654
4891
  "src/config-reconcile-command.ts"() {
4655
4892
  "use strict";
4656
4893
  init_cjs_shims();
4657
4894
  import_apps8 = require("@odla-ai/apps");
4658
- import_node_path11 = require("path");
4895
+ import_node_path12 = require("path");
4659
4896
  init_admin_ai_auth();
4660
4897
  init_config();
4661
4898
  init_config_reconcile_digest();
@@ -4668,7 +4905,7 @@ var init_config_reconcile_command = __esm({
4668
4905
  // src/wrangler.ts
4669
4906
  function findWranglerConfig(rootDir) {
4670
4907
  for (const name of WRANGLER_CONFIG_FILES) {
4671
- const path = (0, import_node_path12.join)(rootDir, name);
4908
+ const path = (0, import_node_path13.join)(rootDir, name);
4672
4909
  if ((0, import_node_fs14.existsSync)(path)) return path;
4673
4910
  }
4674
4911
  return null;
@@ -4780,22 +5017,22 @@ function wranglerBulkSecrets(run, opts) {
4780
5017
  ];
4781
5018
  return run("npx", args, { input: JSON.stringify(opts.secrets), cwd: opts.cwd });
4782
5019
  }
4783
- var import_node_child_process2, import_node_fs14, import_node_path12, defaultRunner, WRANGLER_CONFIG_FILES;
5020
+ var import_node_child_process2, import_node_fs14, import_node_path13, defaultRunner, WRANGLER_CONFIG_FILES;
4784
5021
  var init_wrangler = __esm({
4785
5022
  "src/wrangler.ts"() {
4786
5023
  "use strict";
4787
5024
  init_cjs_shims();
4788
5025
  import_node_child_process2 = require("child_process");
4789
5026
  import_node_fs14 = require("fs");
4790
- import_node_path12 = require("path");
5027
+ import_node_path13 = require("path");
4791
5028
  defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
4792
5029
  const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
4793
5030
  let stdout = "";
4794
- let stderr = "";
5031
+ let stderr2 = "";
4795
5032
  child.stdout.on("data", (chunk) => stdout += chunk.toString());
4796
- child.stderr.on("data", (chunk) => stderr += chunk.toString());
5033
+ child.stderr.on("data", (chunk) => stderr2 += chunk.toString());
4797
5034
  child.on("error", reject);
4798
- child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout, stderr }));
5035
+ child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout, stderr: stderr2 }));
4799
5036
  child.stdin.end(opts?.input ?? "");
4800
5037
  });
4801
5038
  WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
@@ -4839,21 +5076,21 @@ function wranglerWarnings(rootDir) {
4839
5076
  const blocks = [{ label: "", block: config }];
4840
5077
  const envs = config.env;
4841
5078
  if (envs && typeof envs === "object") {
4842
- for (const [name, block] of Object.entries(envs)) {
4843
- if (block && typeof block === "object") blocks.push({ label: `env.${name}.`, block });
5079
+ for (const [name, block2] of Object.entries(envs)) {
5080
+ if (block2 && typeof block2 === "object") blocks.push({ label: `env.${name}.`, block: block2 });
4844
5081
  }
4845
5082
  }
4846
- for (const { label, block } of blocks) {
4847
- const assets = block.assets;
5083
+ for (const { label, block: block2 } of blocks) {
5084
+ const assets = block2.assets;
4848
5085
  if (assets?.directory) {
4849
- const dir = (0, import_node_path13.resolve)(rootDir, assets.directory);
4850
- if (dir === (0, import_node_path13.resolve)(rootDir)) {
5086
+ const dir = (0, import_node_path14.resolve)(rootDir, assets.directory);
5087
+ if (dir === (0, import_node_path14.resolve)(rootDir)) {
4851
5088
  warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
4852
- } else if ((0, import_node_fs15.existsSync)((0, import_node_path13.join)(dir, "node_modules"))) {
5089
+ } else if ((0, import_node_fs15.existsSync)((0, import_node_path14.join)(dir, "node_modules"))) {
4853
5090
  warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
4854
5091
  }
4855
5092
  }
4856
- const vars = block.vars;
5093
+ const vars = block2.vars;
4857
5094
  if (vars && typeof vars === "object") {
4858
5095
  for (const [name, value2] of Object.entries(vars)) {
4859
5096
  if (name === "ODLA_API_KEY" || name === "ODLA_O11Y_TOKEN" || typeof value2 === "string" && looksSecret(value2)) {
@@ -4884,7 +5121,7 @@ function o11yProjectWarnings(rootDir) {
4884
5121
  warnings.push("cannot verify o11y Worker instrumentation \u2014 add a parseable wrangler.jsonc/json config");
4885
5122
  return warnings;
4886
5123
  }
4887
- const main = typeof config.main === "string" ? (0, import_node_path13.resolve)(rootDir, config.main) : null;
5124
+ const main = typeof config.main === "string" ? (0, import_node_path14.resolve)(rootDir, config.main) : null;
4888
5125
  if (!main || !(0, import_node_fs15.existsSync)(main)) {
4889
5126
  warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
4890
5127
  } else {
@@ -4914,19 +5151,19 @@ function calendarProjectWarnings(rootDir) {
4914
5151
  }
4915
5152
  function readPackageJson(rootDir) {
4916
5153
  try {
4917
- return JSON.parse((0, import_node_fs15.readFileSync)((0, import_node_path13.join)(rootDir, "package.json"), "utf8"));
5154
+ return JSON.parse((0, import_node_fs15.readFileSync)((0, import_node_path14.join)(rootDir, "package.json"), "utf8"));
4918
5155
  } catch {
4919
5156
  return null;
4920
5157
  }
4921
5158
  }
4922
- var import_node_child_process3, import_node_fs15, import_node_path13, defaultExec;
5159
+ var import_node_child_process3, import_node_fs15, import_node_path14, defaultExec;
4923
5160
  var init_doctor_checks = __esm({
4924
5161
  "src/doctor-checks.ts"() {
4925
5162
  "use strict";
4926
5163
  init_cjs_shims();
4927
5164
  import_node_child_process3 = require("child_process");
4928
5165
  import_node_fs15 = require("fs");
4929
- import_node_path13 = require("path");
5166
+ import_node_path14 = require("path");
4930
5167
  init_redact();
4931
5168
  init_local();
4932
5169
  init_wrangler();
@@ -5263,8 +5500,8 @@ var init_harness_options = __esm({
5263
5500
  // src/init.ts
5264
5501
  function initProject(options) {
5265
5502
  const out = options.stdout ?? console;
5266
- const rootDir = (0, import_node_path14.resolve)(options.rootDir ?? process.cwd());
5267
- const configPath = (0, import_node_path14.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
5503
+ const rootDir = (0, import_node_path15.resolve)(options.rootDir ?? process.cwd());
5504
+ const configPath = (0, import_node_path15.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
5268
5505
  if ((0, import_node_fs16.existsSync)(configPath) && !options.force) {
5269
5506
  throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
5270
5507
  }
@@ -5281,12 +5518,12 @@ function initProject(options) {
5281
5518
  }
5282
5519
  }
5283
5520
  const aiProvider = options.aiProvider;
5284
- (0, import_node_fs16.mkdirSync)((0, import_node_path14.dirname)(configPath), { recursive: true });
5285
- (0, import_node_fs16.mkdirSync)((0, import_node_path14.resolve)(rootDir, "src/odla"), { recursive: true });
5286
- (0, import_node_fs16.mkdirSync)((0, import_node_path14.resolve)(rootDir, ".odla"), { recursive: true });
5521
+ (0, import_node_fs16.mkdirSync)((0, import_node_path15.dirname)(configPath), { recursive: true });
5522
+ (0, import_node_fs16.mkdirSync)((0, import_node_path15.resolve)(rootDir, "src/odla"), { recursive: true });
5523
+ (0, import_node_fs16.mkdirSync)((0, import_node_path15.resolve)(rootDir, ".odla"), { recursive: true });
5287
5524
  (0, import_node_fs16.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
5288
- writeIfMissing((0, import_node_path14.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
5289
- writeIfMissing((0, import_node_path14.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
5525
+ writeIfMissing((0, import_node_path15.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
5526
+ writeIfMissing((0, import_node_path15.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
5290
5527
  ensureGitignore(rootDir);
5291
5528
  out.log(`created ${relativeDisplay(configPath, rootDir)}`);
5292
5529
  out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
@@ -5353,8 +5590,10 @@ ${calendar}
5353
5590
  // prod: "https://example.com",
5354
5591
  },
5355
5592
  local: {
5356
- tokenFile: ".odla/dev-token.json",
5357
- credentialsFile: ".odla/credentials.local.json",
5593
+ // Credentials live in ~/.odla, per machine, so every worktree of this app
5594
+ // shares one approval instead of asking for its own. Pinning tokenFile or
5595
+ // credentialsFile here still works and still overrides that \u2014 it just puts
5596
+ // this checkout back on its own island.
5358
5597
  devVarsFile: ".dev.vars",
5359
5598
  },
5360
5599
  };
@@ -5397,13 +5636,13 @@ function defaultKeyEnv(provider) {
5397
5636
  function relativeDisplay(path, rootDir) {
5398
5637
  return path.startsWith(rootDir) ? path.slice(rootDir.length + 1) : path;
5399
5638
  }
5400
- var import_node_fs16, import_node_path14, import_apps9;
5639
+ var import_node_fs16, import_node_path15, import_apps9;
5401
5640
  var init_init = __esm({
5402
5641
  "src/init.ts"() {
5403
5642
  "use strict";
5404
5643
  init_cjs_shims();
5405
5644
  import_node_fs16 = require("fs");
5406
- import_node_path14 = require("path");
5645
+ import_node_path15 = require("path");
5407
5646
  import_apps9 = require("@odla-ai/apps");
5408
5647
  init_local();
5409
5648
  }
@@ -5736,8 +5975,8 @@ function installSkill(options = {}) {
5736
5975
  const files = listFiles(sourceDir);
5737
5976
  if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
5738
5977
  const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
5739
- const root = (0, import_node_path15.resolve)(options.dir ?? process.cwd());
5740
- const home = (0, import_node_path15.resolve)(options.homeDir ?? (0, import_node_os3.homedir)());
5978
+ const root = (0, import_node_path16.resolve)(options.dir ?? process.cwd());
5979
+ const home = (0, import_node_path16.resolve)(options.homeDir ?? (0, import_node_os4.homedir)());
5741
5980
  const plans = /* @__PURE__ */ new Map();
5742
5981
  const targets = /* @__PURE__ */ new Map();
5743
5982
  const rememberTarget = (harness, target) => {
@@ -5751,48 +5990,48 @@ function installSkill(options = {}) {
5751
5990
  plans.set(target, { target, content: content2, boundary, managedMerge });
5752
5991
  };
5753
5992
  const planSkillTree = (targetDir2, boundary = root) => {
5754
- for (const rel of files) plan((0, import_node_path15.join)(targetDir2, rel), (0, import_node_fs17.readFileSync)((0, import_node_path15.join)(sourceDir, rel), "utf8"), false, boundary);
5993
+ for (const rel of files) plan((0, import_node_path16.join)(targetDir2, rel), (0, import_node_fs17.readFileSync)((0, import_node_path16.join)(sourceDir, rel), "utf8"), false, boundary);
5755
5994
  };
5756
5995
  let targetDir;
5757
5996
  if (options.global) {
5758
- const claudeRoot = (0, import_node_path15.join)(home, ".claude", "skills");
5759
- const codexRoot = (0, import_node_path15.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path15.join)(home, ".codex"), "skills");
5997
+ const claudeRoot = (0, import_node_path16.join)(home, ".claude", "skills");
5998
+ const codexRoot = (0, import_node_path16.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path16.join)(home, ".codex"), "skills");
5760
5999
  targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
5761
6000
  for (const harness of harnesses) {
5762
6001
  const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
5763
- planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path15.dirname)((0, import_node_path15.dirname)(codexRoot)));
6002
+ planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path16.dirname)((0, import_node_path16.dirname)(codexRoot)));
5764
6003
  rememberTarget(harness, skillRoot);
5765
6004
  }
5766
6005
  } else {
5767
- const sharedRoot = (0, import_node_path15.join)(root, ".agents", "skills");
6006
+ const sharedRoot = (0, import_node_path16.join)(root, ".agents", "skills");
5768
6007
  planSkillTree(sharedRoot);
5769
- const claudeRoot = (0, import_node_path15.join)(root, ".claude", "skills");
6008
+ const claudeRoot = (0, import_node_path16.join)(root, ".claude", "skills");
5770
6009
  targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
5771
6010
  for (const harness of harnesses) rememberTarget(harness, sharedRoot);
5772
6011
  if (harnesses.includes("claude")) {
5773
6012
  for (const skill of skillNames(files)) {
5774
- const canonical2 = (0, import_node_fs17.readFileSync)((0, import_node_path15.join)(sourceDir, skill, "SKILL.md"), "utf8");
5775
- plan((0, import_node_path15.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical2));
6013
+ const canonical2 = (0, import_node_fs17.readFileSync)((0, import_node_path16.join)(sourceDir, skill, "SKILL.md"), "utf8");
6014
+ plan((0, import_node_path16.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical2));
5776
6015
  }
5777
6016
  rememberTarget("claude", claudeRoot);
5778
6017
  }
5779
6018
  if (harnesses.includes("cursor")) {
5780
- const cursorRule = (0, import_node_path15.join)(root, ".cursor", "rules", "odla.mdc");
6019
+ const cursorRule = (0, import_node_path16.join)(root, ".cursor", "rules", "odla.mdc");
5781
6020
  plan(cursorRule, CURSOR_RULE);
5782
6021
  rememberTarget("cursor", cursorRule);
5783
6022
  }
5784
6023
  if (harnesses.includes("agents")) {
5785
- const agentsFile = (0, import_node_path15.join)(root, "AGENTS.md");
6024
+ const agentsFile = (0, import_node_path16.join)(root, "AGENTS.md");
5786
6025
  plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
5787
6026
  rememberTarget("agents", agentsFile);
5788
6027
  }
5789
6028
  if (harnesses.includes("copilot")) {
5790
- const copilotFile = (0, import_node_path15.join)(root, ".github", "copilot-instructions.md");
6029
+ const copilotFile = (0, import_node_path16.join)(root, ".github", "copilot-instructions.md");
5791
6030
  plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
5792
6031
  rememberTarget("copilot", copilotFile);
5793
6032
  }
5794
6033
  if (harnesses.includes("gemini")) {
5795
- const geminiFile = (0, import_node_path15.join)(root, "GEMINI.md");
6034
+ const geminiFile = (0, import_node_path16.join)(root, "GEMINI.md");
5796
6035
  plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
5797
6036
  rememberTarget("gemini", geminiFile);
5798
6037
  }
@@ -5828,7 +6067,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
5828
6067
  }
5829
6068
  for (const file of plans.values()) {
5830
6069
  if (!(0, import_node_fs17.existsSync)(file.target) || (0, import_node_fs17.readFileSync)(file.target, "utf8") !== file.content) {
5831
- (0, import_node_fs17.mkdirSync)((0, import_node_path15.dirname)(file.target), { recursive: true });
6070
+ (0, import_node_fs17.mkdirSync)((0, import_node_path16.dirname)(file.target), { recursive: true });
5832
6071
  (0, import_node_fs17.writeFileSync)(file.target, file.content);
5833
6072
  }
5834
6073
  }
@@ -5848,7 +6087,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
5848
6087
  };
5849
6088
  }
5850
6089
  function pathsUnder(root, paths) {
5851
- 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();
6090
+ return [...paths].map((path) => (0, import_node_path16.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path16.sep}`) && !(0, import_node_path16.isAbsolute)(path)).sort();
5852
6091
  }
5853
6092
  function normalizeHarnesses(values, global) {
5854
6093
  const requested = values?.length ? values : ["claude"];
@@ -5867,10 +6106,10 @@ function normalizeHarnesses(values, global) {
5867
6106
  }
5868
6107
  return expanded;
5869
6108
  }
5870
- function managedFileContent(path, block, force, boundary) {
6109
+ function managedFileContent(path, block2, force, boundary) {
5871
6110
  const symlink = symlinkedComponent(boundary, path);
5872
6111
  if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
5873
- if (!(0, import_node_fs17.existsSync)(path)) return `${block}
6112
+ if (!(0, import_node_fs17.existsSync)(path)) return `${block2}
5874
6113
  `;
5875
6114
  const current = (0, import_node_fs17.readFileSync)(path, "utf8");
5876
6115
  const start = "<!-- odla-ai agent setup:start -->";
@@ -5882,24 +6121,24 @@ function managedFileContent(path, block, force, boundary) {
5882
6121
  }
5883
6122
  if (startAt === -1) {
5884
6123
  const separator = current.length === 0 || current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n";
5885
- return `${current}${separator}${block}
6124
+ return `${current}${separator}${block2}
5886
6125
  `;
5887
6126
  }
5888
6127
  const afterEnd = endAt + end.length;
5889
6128
  const existing = current.slice(startAt, afterEnd);
5890
- if (existing !== block && !force) {
6129
+ if (existing !== block2 && !force) {
5891
6130
  throw new Error(`odla-managed section modified locally in ${path}; re-run with --force to replace that section`);
5892
6131
  }
5893
- return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
6132
+ return `${current.slice(0, startAt)}${block2}${current.slice(afterEnd)}`;
5894
6133
  }
5895
6134
  function symlinkedComponent(boundary, target) {
5896
- const rel = (0, import_node_path15.relative)(boundary, target);
5897
- if (rel === ".." || rel.startsWith(`..${import_node_path15.sep}`) || (0, import_node_path15.isAbsolute)(rel)) {
6135
+ const rel = (0, import_node_path16.relative)(boundary, target);
6136
+ if (rel === ".." || rel.startsWith(`..${import_node_path16.sep}`) || (0, import_node_path16.isAbsolute)(rel)) {
5898
6137
  throw new Error(`agent setup target escapes its install root: ${target}`);
5899
6138
  }
5900
6139
  let current = boundary;
5901
- for (const part of rel.split(import_node_path15.sep).filter(Boolean)) {
5902
- current = (0, import_node_path15.join)(current, part);
6140
+ for (const part of rel.split(import_node_path16.sep).filter(Boolean)) {
6141
+ current = (0, import_node_path16.join)(current, part);
5903
6142
  try {
5904
6143
  if ((0, import_node_fs17.lstatSync)(current).isSymbolicLink()) return current;
5905
6144
  } catch (error) {
@@ -5916,22 +6155,22 @@ function listFiles(dir) {
5916
6155
  const results = [];
5917
6156
  const walk = (current) => {
5918
6157
  for (const entry of (0, import_node_fs17.readdirSync)(current, { withFileTypes: true })) {
5919
- const path = (0, import_node_path15.join)(current, entry.name);
6158
+ const path = (0, import_node_path16.join)(current, entry.name);
5920
6159
  if (entry.isDirectory()) walk(path);
5921
- else results.push((0, import_node_path15.relative)(dir, path));
6160
+ else results.push((0, import_node_path16.relative)(dir, path));
5922
6161
  }
5923
6162
  };
5924
6163
  walk(dir);
5925
6164
  return results.sort();
5926
6165
  }
5927
- var import_node_fs17, import_node_os3, import_node_path15, import_node_url2, AGENT_HARNESSES;
6166
+ var import_node_fs17, import_node_os4, import_node_path16, import_node_url2, AGENT_HARNESSES;
5928
6167
  var init_skill = __esm({
5929
6168
  "src/skill.ts"() {
5930
6169
  "use strict";
5931
6170
  init_cjs_shims();
5932
6171
  import_node_fs17 = require("fs");
5933
- import_node_os3 = require("os");
5934
- import_node_path15 = require("path");
6172
+ import_node_os4 = require("os");
6173
+ import_node_path16 = require("path");
5935
6174
  import_node_url2 = require("url");
5936
6175
  init_skill_adapters();
5937
6176
  AGENT_HARNESSES = ["claude", "codex", "cursor", "copilot", "gemini", "agents"];
@@ -6405,7 +6644,7 @@ function allowedWorkspacePath(relativePath) {
6405
6644
  async function gitOutput(cwd, args, maxBytes) {
6406
6645
  const child = (0, import_child_process2.spawn)("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], shell: false });
6407
6646
  const stdout = [];
6408
- const stderr = [];
6647
+ const stderr2 = [];
6409
6648
  let bytes = 0;
6410
6649
  child.stdout.on("data", (chunk) => {
6411
6650
  bytes += chunk.byteLength;
@@ -6413,20 +6652,20 @@ async function gitOutput(cwd, args, maxBytes) {
6413
6652
  else stdout.push(chunk);
6414
6653
  });
6415
6654
  child.stderr.on("data", (chunk) => {
6416
- if (stderr.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr.push(chunk);
6655
+ if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
6417
6656
  });
6418
6657
  const code = await new Promise((accept, reject) => {
6419
6658
  child.once("error", reject);
6420
6659
  child.once("exit", accept);
6421
6660
  });
6422
6661
  if (bytes > maxBytes) throw new Error(`git output exceeds ${maxBytes} bytes`);
6423
- if (code !== 0) throw new Error(`git command failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
6662
+ if (code !== 0) throw new Error(`git command failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
6424
6663
  return Buffer.concat(stdout);
6425
6664
  }
6426
6665
  async function gitBlobs(cwd, entries, maxBytes) {
6427
6666
  const child = (0, import_child_process2.spawn)("git", ["cat-file", "--batch"], { cwd, stdio: ["pipe", "pipe", "pipe"], shell: false });
6428
6667
  const stdout = [];
6429
- const stderr = [];
6668
+ const stderr2 = [];
6430
6669
  let bytes = 0;
6431
6670
  child.stdout.on("data", (chunk) => {
6432
6671
  bytes += chunk.byteLength;
@@ -6434,7 +6673,7 @@ async function gitBlobs(cwd, entries, maxBytes) {
6434
6673
  else stdout.push(chunk);
6435
6674
  });
6436
6675
  child.stderr.on("data", (chunk) => {
6437
- if (stderr.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr.push(chunk);
6676
+ if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
6438
6677
  });
6439
6678
  child.stdin.end(`${entries.map((entry) => entry.hash).join("\n")}
6440
6679
  `);
@@ -6443,7 +6682,7 @@ async function gitBlobs(cwd, entries, maxBytes) {
6443
6682
  child.once("exit", accept);
6444
6683
  });
6445
6684
  if (bytes > maxBytes + entries.length * 100) throw new Error(`Git tree exceeds ${maxBytes} bytes`);
6446
- if (code !== 0) throw new Error(`git object read failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
6685
+ if (code !== 0) throw new Error(`git object read failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
6447
6686
  const output = Buffer.concat(stdout);
6448
6687
  const blobs = [];
6449
6688
  let offset = 0;
@@ -6540,7 +6779,7 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
6540
6779
  shell: false
6541
6780
  });
6542
6781
  const stdout = [];
6543
- const stderr = [];
6782
+ const stderr2 = [];
6544
6783
  let outputBytes = 0;
6545
6784
  child.stdout.on("data", (chunk) => {
6546
6785
  outputBytes += chunk.byteLength;
@@ -6548,14 +6787,14 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
6548
6787
  else stdout.push(chunk);
6549
6788
  });
6550
6789
  child.stderr.on("data", (chunk) => {
6551
- if (stderr.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr.push(chunk);
6790
+ if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
6552
6791
  });
6553
6792
  const code = await new Promise((accept, reject) => {
6554
6793
  child.once("error", reject);
6555
6794
  child.once("exit", accept);
6556
6795
  });
6557
6796
  if (outputBytes > 8 * 1024 * 1024) throw new Error("git file inventory exceeds 8 MiB");
6558
- if (code !== 0) throw new Error(`git file inventory failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
6797
+ if (code !== 0) throw new Error(`git file inventory failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
6559
6798
  const paths = Buffer.concat(stdout).toString("utf8").split("\0").filter(Boolean).sort();
6560
6799
  if (paths.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
6561
6800
  const root = (0, import_path4.resolve)(sourceDir);
@@ -6600,7 +6839,7 @@ async function captureGitDiff(root, maxBytes) {
6600
6839
  "workspace"
6601
6840
  ], { cwd: root, stdio: ["ignore", "pipe", "pipe"], shell: false });
6602
6841
  const stdout = [];
6603
- const stderr = [];
6842
+ const stderr2 = [];
6604
6843
  let bytes = 0;
6605
6844
  child.stdout.on("data", (chunk) => {
6606
6845
  bytes += chunk.byteLength;
@@ -6608,7 +6847,7 @@ async function captureGitDiff(root, maxBytes) {
6608
6847
  else stdout.push(chunk);
6609
6848
  });
6610
6849
  child.stderr.on("data", (chunk) => {
6611
- if (stderr.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr.push(chunk);
6850
+ if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
6612
6851
  });
6613
6852
  const code = await new Promise((accept, reject) => {
6614
6853
  child.once("error", reject);
@@ -6616,7 +6855,7 @@ async function captureGitDiff(root, maxBytes) {
6616
6855
  });
6617
6856
  if (bytes > maxBytes) throw new Error(`patch exceeds ${maxBytes} bytes`);
6618
6857
  if (code !== 0 && code !== 1) {
6619
- throw new Error(`git diff failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
6858
+ throw new Error(`git diff failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
6620
6859
  }
6621
6860
  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");
6622
6861
  }
@@ -7504,11 +7743,11 @@ var init_dist2 = __esm({
7504
7743
  });
7505
7744
 
7506
7745
  // ../graph/dist/code/index.js
7507
- function dirname9(path) {
7746
+ function dirname10(path) {
7508
7747
  const at = path.lastIndexOf("/");
7509
7748
  return at <= 0 ? "." : path.slice(0, at);
7510
7749
  }
7511
- function join13(base, specifier) {
7750
+ function join14(base, specifier) {
7512
7751
  const parts = [];
7513
7752
  const segments = `${base === "." ? "" : `${base}/`}${specifier}`.split("/");
7514
7753
  for (const segment of segments) {
@@ -7520,7 +7759,7 @@ function join13(base, specifier) {
7520
7759
  }
7521
7760
  function resolveImport(fromPath, specifier, known) {
7522
7761
  if (!specifier.startsWith(".")) return null;
7523
- const base = join13(dirname9(fromPath), specifier);
7762
+ const base = join14(dirname10(fromPath), specifier);
7524
7763
  const candidates = [
7525
7764
  base,
7526
7765
  base.replace(/\.js$/, ".ts"),
@@ -8023,13 +8262,13 @@ function gitApply(cwd, patch2, check) {
8023
8262
  stdio: ["pipe", "ignore", "pipe"],
8024
8263
  env: { PATH: process.env.PATH ?? "", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null" }
8025
8264
  });
8026
- let stderr = "";
8265
+ let stderr2 = "";
8027
8266
  child.stderr.setEncoding("utf8");
8028
8267
  child.stderr.on("data", (text3) => {
8029
- if (stderr.length < 4e3) stderr += text3.slice(0, 4e3);
8268
+ if (stderr2.length < 4e3) stderr2 += text3.slice(0, 4e3);
8030
8269
  });
8031
8270
  child.once("error", reject);
8032
- child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(describePatchFailure(patch2, stderr.trim().slice(0, 500)))));
8271
+ child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(describePatchFailure(patch2, stderr2.trim().slice(0, 500)))));
8033
8272
  child.stdin.end(patch2);
8034
8273
  });
8035
8274
  }
@@ -8140,7 +8379,7 @@ function execute(engine, args, name, recipe2, signal) {
8140
8379
  const started = Date.now();
8141
8380
  const child = (0, import_child_process5.spawn)(engine, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
8142
8381
  const stdout = [];
8143
- const stderr = [];
8382
+ const stderr2 = [];
8144
8383
  let bytes = 0;
8145
8384
  let outputLimitExceeded = false;
8146
8385
  let timedOut = false;
@@ -8161,7 +8400,7 @@ function execute(engine, args, name, recipe2, signal) {
8161
8400
  else target.push(chunk);
8162
8401
  };
8163
8402
  child.stdout.on("data", collect(stdout));
8164
- child.stderr.on("data", collect(stderr));
8403
+ child.stderr.on("data", collect(stderr2));
8165
8404
  const abort = () => stop("abort");
8166
8405
  signal?.addEventListener("abort", abort, { once: true });
8167
8406
  if (signal?.aborted) abort();
@@ -8177,7 +8416,7 @@ function execute(engine, args, name, recipe2, signal) {
8177
8416
  accept({
8178
8417
  exitCode: code ?? 1,
8179
8418
  stdout: Buffer.concat(stdout).toString("utf8"),
8180
- stderr: Buffer.concat(stderr).toString("utf8"),
8419
+ stderr: Buffer.concat(stderr2).toString("utf8"),
8181
8420
  durationMs: Date.now() - started,
8182
8421
  outputLimitExceeded,
8183
8422
  timedOut
@@ -8323,11 +8562,11 @@ function checkedResult(result, maximumOutputBytes) {
8323
8562
  }
8324
8563
  function boundedLogs(result, maximum) {
8325
8564
  const stdout = Buffer.from(result.stdout);
8326
- const stderr = Buffer.from(result.stderr);
8565
+ const stderr2 = Buffer.from(result.stderr);
8327
8566
  const first = stdout.subarray(0, maximum);
8328
8567
  return {
8329
8568
  stdout: first.toString("utf8"),
8330
- stderr: stderr.subarray(0, Math.max(0, maximum - first.byteLength)).toString("utf8")
8569
+ stderr: stderr2.subarray(0, Math.max(0, maximum - first.byteLength)).toString("utf8")
8331
8570
  };
8332
8571
  }
8333
8572
  function digestPolicy(policy) {
@@ -10461,7 +10700,7 @@ var init_code_runtime_config = __esm({
10461
10700
  // src/code-connect.ts
10462
10701
  async function codeConnect(options) {
10463
10702
  const cwd = options.cwd ?? process.cwd();
10464
- const configPath = (0, import_node_path16.resolve)(cwd, options.configPath);
10703
+ const configPath = (0, import_node_path17.resolve)(cwd, options.configPath);
10465
10704
  const cfg = (0, import_node_fs18.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
10466
10705
  const requestedAppId = options.appId?.trim();
10467
10706
  if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
@@ -10491,7 +10730,7 @@ async function codeConnect(options) {
10491
10730
  const doFetch = options.fetch ?? fetch;
10492
10731
  const engine = await (options.selectEngine ?? selectContainerEngine)(options.engine ?? "auto");
10493
10732
  const hostPlatform = process.platform === "darwin" ? "macos" : "linux";
10494
- const hostName = (options.name ?? (0, import_node_os4.hostname)()).trim();
10733
+ const hostName = (options.name ?? (0, import_node_os5.hostname)()).trim();
10495
10734
  if (!hostName || hostName.length > 120) throw new Error("--name must contain 1 to 120 characters");
10496
10735
  const repository = await inferGitHubRepository(cwd, options.readGitOrigin);
10497
10736
  const localSource = await (options.prepareLocalSource ?? prepareCodeLocalSource)(
@@ -10529,8 +10768,8 @@ async function codeConnect(options) {
10529
10768
  platform: hostPlatform,
10530
10769
  arch: process.arch,
10531
10770
  engines: [engine],
10532
- cpuCount: (0, import_node_os4.cpus)().length,
10533
- memoryBytes: (0, import_node_os4.totalmem)(),
10771
+ cpuCount: (0, import_node_os5.cpus)().length,
10772
+ memoryBytes: (0, import_node_os5.totalmem)(),
10534
10773
  source: descriptor2,
10535
10774
  images: {
10536
10775
  ready: true,
@@ -10627,14 +10866,14 @@ function apiFailure(action2, status, value2) {
10627
10866
  function record6(value2) {
10628
10867
  return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
10629
10868
  }
10630
- var import_node_fs18, import_node_os4, import_node_path16;
10869
+ var import_node_fs18, import_node_os5, import_node_path17;
10631
10870
  var init_code_connect = __esm({
10632
10871
  "src/code-connect.ts"() {
10633
10872
  "use strict";
10634
10873
  init_cjs_shims();
10635
10874
  import_node_fs18 = require("fs");
10636
- import_node_os4 = require("os");
10637
- import_node_path16 = require("path");
10875
+ import_node_os5 = require("os");
10876
+ import_node_path17 = require("path");
10638
10877
  init_node();
10639
10878
  init_admin_ai_auth();
10640
10879
  init_config();
@@ -10952,7 +11191,7 @@ function developerTokenStatus(context, parsed, now = Date.now()) {
10952
11191
  const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
10953
11192
  const source = clean3(
10954
11193
  stringOpt(parsed.options.token)
10955
- ) ? "flag" : clean3(import_node_process12.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
11194
+ ) ? "flag" : clean3(import_node_process16.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
10956
11195
  return {
10957
11196
  source,
10958
11197
  cacheFile: context.cfg.local.tokenFile,
@@ -10963,12 +11202,12 @@ function clean3(value2) {
10963
11202
  const normalized = value2?.trim();
10964
11203
  return normalized || void 0;
10965
11204
  }
10966
- var import_node_process12;
11205
+ var import_node_process16;
10967
11206
  var init_operator_credentials = __esm({
10968
11207
  "src/operator-credentials.ts"() {
10969
11208
  "use strict";
10970
11209
  init_cjs_shims();
10971
- import_node_process12 = __toESM(require("process"), 1);
11210
+ import_node_process16 = __toESM(require("process"), 1);
10972
11211
  init_argv();
10973
11212
  init_local();
10974
11213
  }
@@ -11176,11 +11415,35 @@ var init_credential_command = __esm({
11176
11415
  });
11177
11416
 
11178
11417
  // src/help-usage.ts
11179
- var USAGE_SECTION;
11418
+ var AUTH_SECTION, USAGE_SECTION;
11180
11419
  var init_help_usage = __esm({
11181
11420
  "src/help-usage.ts"() {
11182
11421
  "use strict";
11183
11422
  init_cjs_shims();
11423
+ AUTH_SECTION = `
11424
+ Enrol this machine once, then stop asking:
11425
+ npx odla-ai device enroll --no-open --wait 600
11426
+ One browser approval, and no flags to remember: this covers every app you
11427
+ own \u2014 including apps you create later \u2014 with every capability that
11428
+ approval can carry. Afterwards every worktree on this machine mints its
11429
+ own short-lived credentials with nobody's attention. The window rolls
11430
+ forward each time you use it, so continuous work never interrupts anyone;
11431
+ only a real gap does. Give the human the printed /studio?code= URL, keep
11432
+ the process alive, and wait on it.
11433
+ Narrow it deliberately with --app <id> or --capability <c>; the CLI then
11434
+ says what that gave up.
11435
+
11436
+ npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600
11437
+ The same thing across all of odla, for weeks. Needs a platform
11438
+ administrator's approval \u2014 an app owner's cannot carry platform scopes.
11439
+
11440
+ npx odla-ai whoami what this machine holds and when it lapses
11441
+ npx odla-ai device list every machine you have enrolled
11442
+
11443
+ Enrollment is the only human decision here. Revoking a machine, purging an app,
11444
+ transferring ownership, and rotating credentials still need a signed-in human in
11445
+ Studio, and no machine credential can do them however wide its approval was.
11446
+ `;
11184
11447
  USAGE_SECTION = `
11185
11448
  Start here:
11186
11449
  odla-ai runbook ask "<question>" The current procedure, from odla's own
@@ -11219,7 +11482,7 @@ Usage:
11219
11482
  odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]
11220
11483
  odla-ai pm project list [--app <product-id>] [--status <s>] [--json]
11221
11484
  odla-ai pm project add --app <product-id> --name <name> [--description <text>] [--json]
11222
- odla-ai pm project use <project-id> [--json] [saved locally in this worktree]
11485
+ odla-ai pm project use <project-id> [--json] [saved for this app, on this machine]
11223
11486
  odla-ai pm goal list [--app <id>] [--project <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
11224
11487
  odla-ai pm task list [--app <id>] [--column <backlog|ready|doing|review|done>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
11225
11488
  odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
@@ -11311,7 +11574,8 @@ Usage:
11311
11574
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
11312
11575
  odla-ai security run [target] --self --ack-redacted-source
11313
11576
  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]
11314
- odla-ai device enroll [--app <id>[,<id>...]] [--name <label>] [--capability <c>[,<c>...]] [--device-ttl <30d|6w|2y|forever>] [--email <odla-account>] [--no-open] [--json]
11577
+ odla-ai device enroll [--app <id>[,<id>...]|--all-apps] [--capability all|<c>[,<c>...]] [--platform-wide]
11578
+ [--name <label>] [--device-ttl <30d|6w|2y|forever>] [--email <odla-account>] [--no-open] [--wait <seconds>] [--json]
11315
11579
  odla-ai device list [--email <odla-account>] [--json]
11316
11580
  odla-ai device revoke <device-id> [--email <odla-account>] [--json]
11317
11581
  odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
@@ -11327,8 +11591,11 @@ Usage:
11327
11591
 
11328
11592
  // src/help.ts
11329
11593
  function printHelp(output = console) {
11330
- output.log(`odla-ai
11331
- ${USAGE_SECTION}
11594
+ output.log(helpText());
11595
+ }
11596
+ function helpText() {
11597
+ return `odla-ai
11598
+ ${AUTH_SECTION}${USAGE_SECTION}
11332
11599
  Commands:
11333
11600
  auth Start a fresh, exact-project agent authorization for human review.
11334
11601
  The email is the signed-in odla account, never git or GitHub
@@ -11401,8 +11668,9 @@ Commands:
11401
11668
  security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
11402
11669
  pm Project management (via @odla-ai/pm): Products contain Projects;
11403
11670
  projects contain goals, kanban tasks, decisions, and bugs. Use
11404
- "pm project list|add|use" to select worktree-local context, or
11405
- pass --app/--project explicitly. Same device-grant auth as "app".
11671
+ "pm project list|add|use" selects a project for this app on this
11672
+ machine \u2014 every worktree shares it \u2014 or pass --app/--project
11673
+ explicitly. Same device-grant auth as "app".
11406
11674
  Status changes and comments post to each item's @odla-ai/chat
11407
11675
  discussion thread.
11408
11676
  NOTE: "--column ready" is OWNER-ONLY. Creating a task in Ready
@@ -11431,9 +11699,22 @@ Commands:
11431
11699
  platform Read canonical fleet health, releases, provider load/freshness,
11432
11700
  explicit unknowns, and next actions through a read-only grant.
11433
11701
  device Enrol THIS machine once, then stop asking. A human approves the
11434
- enrollment in the browser; from then on this terminal mints its
11435
- own short-lived credentials for the named projects with nobody's
11436
- attention, until the device expires or is revoked.
11702
+ enrollment in the browser; from then on EVERY worktree on this
11703
+ machine mints its own short-lived credentials with nobody's
11704
+ attention, until the device is revoked or goes unused.
11705
+ With no flags it covers every app you own, now and later, with
11706
+ every capability that approval can carry \u2014 so creating an app
11707
+ costs no new approval, and a capability nobody thought to name is
11708
+ not a 403 next week. "--app" or "--capability" narrow it
11709
+ deliberately, and the CLI says what that gave up.
11710
+ "--platform-wide" is the administrator's version, across all of
11711
+ odla.
11712
+ The expiry is a GAP, not a clock: each use rolls it forward, so
11713
+ only going quiet brings a human back into the loop \u2014 which is
11714
+ where anything that changed can be explained.
11715
+ "device list" shows what each machine holds and when it lapses;
11716
+ revoking one takes down every credential it ever minted, and is
11717
+ deliberately a signed-in human's decision in Studio.
11437
11718
  provision Register services, compose integrations, persist credentials, optionally push secrets.
11438
11719
  "provision --live --yes" initializes only the live instance of
11439
11720
  an existing sandbox app and enables every configured service;
@@ -11529,7 +11810,7 @@ Safety:
11529
11810
  Run security plan first to inspect the admin-selected providers, models,
11530
11811
  per-route bounds, credential readiness, retention, no-execution boundary,
11531
11812
  and digest that binds consent to that exact plan.
11532
- `);
11813
+ `;
11533
11814
  }
11534
11815
  var init_help = __esm({
11535
11816
  "src/help.ts"() {
@@ -11539,6 +11820,53 @@ var init_help = __esm({
11539
11820
  }
11540
11821
  });
11541
11822
 
11823
+ // src/help-command.ts
11824
+ function printCommandHelp(command, output = console) {
11825
+ const lines = helpText().split("\n");
11826
+ const usage = allBlocks(lines, new RegExp(`^ odla-ai ${escapeRe(command)}(\\s|$)`));
11827
+ const prose = block(lines, (line2) => new RegExp(`^ ${escapeRe(command)}\\s\\s+\\S`).test(line2));
11828
+ if (usage.length === 0 && prose.length === 0) {
11829
+ output.log(`odla-ai: no command "${command}". Run "odla-ai help" for all of them.`);
11830
+ return;
11831
+ }
11832
+ output.log([
11833
+ ...prose.length ? [prose.join("\n"), ""] : [],
11834
+ ...usage.length ? ["Usage:", ...usage, ""] : [],
11835
+ AUTH_SECTION.trimEnd()
11836
+ ].join("\n"));
11837
+ }
11838
+ function allBlocks(lines, pattern) {
11839
+ const out = [];
11840
+ for (let i = 0; i < lines.length; i++) {
11841
+ if (!pattern.test(lines[i])) continue;
11842
+ out.push(...block(lines.slice(i), (line2) => line2 === lines[i]));
11843
+ }
11844
+ return out;
11845
+ }
11846
+ function block(lines, starts) {
11847
+ const first = lines.findIndex(starts);
11848
+ if (first === -1) return [];
11849
+ const indent = lines[first].length - lines[first].trimStart().length;
11850
+ const out = [lines[first]];
11851
+ for (const line2 of lines.slice(first + 1)) {
11852
+ if (!line2.trim()) break;
11853
+ if (line2.length - line2.trimStart().length <= indent) break;
11854
+ out.push(line2);
11855
+ }
11856
+ return out;
11857
+ }
11858
+ function escapeRe(value2) {
11859
+ return value2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11860
+ }
11861
+ var init_help_command = __esm({
11862
+ "src/help-command.ts"() {
11863
+ "use strict";
11864
+ init_cjs_shims();
11865
+ init_help_usage();
11866
+ init_help();
11867
+ }
11868
+ });
11869
+
11542
11870
  // src/discuss-principals.ts
11543
11871
  function mergeDiscussPrincipals(target, source) {
11544
11872
  Object.assign(target.authors, source.authors ?? {});
@@ -12804,20 +13132,43 @@ var init_pm_watch = __esm({
12804
13132
 
12805
13133
  // src/pm-project-context.ts
12806
13134
  function readPmProjectContext(rootDir) {
12807
- const value2 = readJsonFile(pmProjectContextFile(rootDir));
12808
- return value2 && typeof value2.appId === "string" && typeof value2.projectId === "string" ? value2 : null;
13135
+ adoptLegacySelection(rootDir);
13136
+ const entries = Object.values(readSelections()).filter(isSelection);
13137
+ return entries.sort((a, b) => b.selectedAt.localeCompare(a.selectedAt))[0] ?? null;
12809
13138
  }
12810
13139
  function writePmProjectContext(rootDir, value2) {
12811
- writePrivateJson(pmProjectContextFile(rootDir), { ...value2, selectedAt: (/* @__PURE__ */ new Date()).toISOString() });
13140
+ adoptLegacySelection(rootDir);
13141
+ writePrivateJson(pmProjectContextFile(), {
13142
+ ...readSelections(),
13143
+ [value2.appId]: { ...value2, selectedAt: (/* @__PURE__ */ new Date()).toISOString() }
13144
+ });
13145
+ }
13146
+ function adoptLegacySelection(rootDir) {
13147
+ const legacy = (0, import_node_path18.resolve)(rootDir, ".odla", "pm-project.local.json");
13148
+ if (!(0, import_node_fs19.existsSync)(legacy)) return;
13149
+ const previous = readJsonFile(legacy);
13150
+ (0, import_node_fs19.rmSync)(legacy, { force: true });
13151
+ if (!isSelection(previous)) return;
13152
+ const selections = readSelections();
13153
+ if (selections[previous.appId]) return;
13154
+ writePrivateJson(pmProjectContextFile(), { ...selections, [previous.appId]: previous });
13155
+ }
13156
+ function readSelections() {
13157
+ return readJsonFile(pmProjectContextFile()) ?? {};
13158
+ }
13159
+ function isSelection(value2) {
13160
+ return !!value2 && typeof value2.appId === "string" && typeof value2.projectId === "string" && typeof value2.selectedAt === "string";
12812
13161
  }
12813
- var import_node_path17, pmProjectContextFile;
13162
+ var import_node_fs19, import_node_path18, pmProjectContextFile;
12814
13163
  var init_pm_project_context = __esm({
12815
13164
  "src/pm-project-context.ts"() {
12816
13165
  "use strict";
12817
13166
  init_cjs_shims();
12818
- import_node_path17 = require("path");
13167
+ import_node_fs19 = require("fs");
13168
+ import_node_path18 = require("path");
12819
13169
  init_local();
12820
- pmProjectContextFile = (rootDir) => (0, import_node_path17.resolve)(rootDir, ".odla", "pm-project.local.json");
13170
+ init_odla_home();
13171
+ pmProjectContextFile = () => pmContextFile();
12821
13172
  }
12822
13173
  });
12823
13174
 
@@ -14391,7 +14742,7 @@ async function provision(options) {
14391
14742
  await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
14392
14743
  }
14393
14744
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
14394
- const key = import_node_process13.default.env[cfg.ai.keyEnv];
14745
+ const key = import_node_process17.default.env[cfg.ai.keyEnv];
14395
14746
  if (key) {
14396
14747
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
14397
14748
  await (0, import_ai5.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -14430,14 +14781,14 @@ async function provision(options) {
14430
14781
  }
14431
14782
  }
14432
14783
  }
14433
- var import_apps13, import_ai5, import_node_process13;
14784
+ var import_apps13, import_ai5, import_node_process17;
14434
14785
  var init_provision = __esm({
14435
14786
  "src/provision.ts"() {
14436
14787
  "use strict";
14437
14788
  init_cjs_shims();
14438
14789
  import_apps13 = require("@odla-ai/apps");
14439
14790
  import_ai5 = require("@odla-ai/ai");
14440
- import_node_process13 = __toESM(require("process"), 1);
14791
+ import_node_process17 = __toESM(require("process"), 1);
14441
14792
  init_config();
14442
14793
  init_calendar();
14443
14794
  init_calendar_errors();
@@ -14632,7 +14983,7 @@ var init_surface = __esm({
14632
14983
 
14633
14984
  // src/record.ts
14634
14985
  function recordInvocation(parsed) {
14635
- const file = import_node_process14.default.env.ODLA_CLI_RECORD;
14986
+ const file = import_node_process18.default.env.ODLA_CLI_RECORD;
14636
14987
  if (!file) return;
14637
14988
  try {
14638
14989
  const entry = {
@@ -14640,18 +14991,18 @@ function recordInvocation(parsed) {
14640
14991
  options: Object.entries(parsed.options).map(([name, value2]) => value2 === false ? `no-${name}` : name).sort()
14641
14992
  };
14642
14993
  if (!entry.path.length) return;
14643
- (0, import_node_fs19.appendFileSync)(file, `${JSON.stringify(entry)}
14994
+ (0, import_node_fs20.appendFileSync)(file, `${JSON.stringify(entry)}
14644
14995
  `);
14645
14996
  } catch {
14646
14997
  }
14647
14998
  }
14648
- var import_node_fs19, import_node_process14;
14999
+ var import_node_fs20, import_node_process18;
14649
15000
  var init_record = __esm({
14650
15001
  "src/record.ts"() {
14651
15002
  "use strict";
14652
15003
  init_cjs_shims();
14653
- import_node_fs19 = require("fs");
14654
- import_node_process14 = __toESM(require("process"), 1);
15004
+ import_node_fs20 = require("fs");
15005
+ import_node_process18 = __toESM(require("process"), 1);
14655
15006
  init_surface();
14656
15007
  }
14657
15008
  });
@@ -14667,22 +15018,29 @@ function advisoryCollectingFetch(inner, sink) {
14667
15018
  return response2;
14668
15019
  });
14669
15020
  }
15021
+ function supersedeAdvisory(code) {
15022
+ superseded.add(code);
15023
+ }
14670
15024
  function renderAdvisories(out, advisories, env = process.env) {
15025
+ const retracted = new Set(superseded);
15026
+ superseded.clear();
14671
15027
  if (env.ODLA_NO_ADVISORIES) return;
14672
15028
  const seen = /* @__PURE__ */ new Set();
14673
15029
  for (const advisory of advisories) {
15030
+ if (retracted.has(advisory.code)) continue;
14674
15031
  const key = `${advisory.code}:${advisory.message}`;
14675
15032
  if (seen.has(key)) continue;
14676
15033
  seen.add(key);
14677
15034
  out.error((0, import_apps14.formatAdvisory)(advisory));
14678
15035
  }
14679
15036
  }
14680
- var import_apps14;
15037
+ var import_apps14, superseded;
14681
15038
  var init_advisory_output = __esm({
14682
15039
  "src/advisory-output.ts"() {
14683
15040
  "use strict";
14684
15041
  init_cjs_shims();
14685
15042
  import_apps14 = require("@odla-ai/apps");
15043
+ superseded = /* @__PURE__ */ new Set();
14686
15044
  }
14687
15045
  });
14688
15046
 
@@ -14713,6 +15071,22 @@ var init_device_ttl = __esm({
14713
15071
 
14714
15072
  // src/device-command.ts
14715
15073
  async function deviceCommand(parsed, deps) {
15074
+ assertArgs(parsed, [
15075
+ "app",
15076
+ "all-apps",
15077
+ "platform-wide",
15078
+ "name",
15079
+ "capability",
15080
+ "device-ttl",
15081
+ "email",
15082
+ "open",
15083
+ "json",
15084
+ "config",
15085
+ "token",
15086
+ "context",
15087
+ "platform",
15088
+ "wait"
15089
+ ], 3);
14716
15090
  const action2 = parsed.positionals[1] ?? "";
14717
15091
  const out = deps.stdout ?? console;
14718
15092
  const doFetch = deps.fetch ?? fetch;
@@ -14725,10 +15099,19 @@ async function deviceCommand(parsed, deps) {
14725
15099
  }
14726
15100
  async function enroll(parsed, deps, cfg, doFetch, out, json) {
14727
15101
  const name = stringOpt(parsed.options.name) ?? defaultDeviceName();
14728
- const apps = (stringOpt(parsed.options.app) ?? cfg.app.id).split(",").map((id2) => id2.trim()).filter(Boolean);
14729
- if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026]");
15102
+ const platformWide = parsed.options["platform-wide"] === true;
15103
+ const narrowed = stringOpt(parsed.options.app) !== void 0 || stringOpt(parsed.options.capability) !== void 0;
15104
+ const apps = platformWide || parsed.options["all-apps"] === true || !narrowed ? [import_db4.ALL_OWNED_APPS] : (stringOpt(parsed.options.app) ?? cfg.app.id).split(",").map((id2) => id2.trim()).filter(Boolean);
15105
+ if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026], or --all-apps");
14730
15106
  const deviceTtlMs = parseDeviceTtl(parsed.options["device-ttl"]);
14731
- const extended = deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
15107
+ const extended = platformWide || deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
15108
+ const { capabilities, scopes } = requestedEnvelope(parsed, platformWide, narrowed);
15109
+ const narrowNotice = narrowEnrollmentNotice({
15110
+ appIds: apps,
15111
+ capabilities: capabilities ?? [],
15112
+ scopes: scopes ?? []
15113
+ });
15114
+ if (narrowNotice) out.error(narrowNotice);
14732
15115
  const token = await scopedToken2(
14733
15116
  parsed,
14734
15117
  deps,
@@ -14743,9 +15126,10 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
14743
15126
  headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
14744
15127
  body: JSON.stringify({
14745
15128
  name,
14746
- platform: import_node_process15.default.platform,
15129
+ platform: import_node_process19.default.platform,
14747
15130
  appIds: apps,
14748
- ...parsed.options.capability ? { capabilities: String(parsed.options.capability).split(",").map((c) => c.trim()).filter(Boolean) } : {},
15131
+ ...capabilities ? { capabilities } : {},
15132
+ ...scopes ? { scopes } : {},
14749
15133
  ...deviceTtlMs === void 0 ? {} : { deviceTtlMs }
14750
15134
  })
14751
15135
  });
@@ -14754,19 +15138,54 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
14754
15138
  throw new Error(`device enroll failed: ${body.error?.message ?? `registry returned ${response2.status}`} (${response2.status})`);
14755
15139
  }
14756
15140
  const path = deviceCredentialPath();
14757
- (0, import_node_fs20.mkdirSync)((0, import_node_path18.dirname)(path), { recursive: true });
14758
- (0, import_node_fs20.writeFileSync)(path, JSON.stringify({
15141
+ (0, import_node_fs21.mkdirSync)((0, import_node_path19.dirname)(path), { recursive: true });
15142
+ (0, import_node_fs21.writeFileSync)(path, JSON.stringify({
14759
15143
  token: body.token,
14760
15144
  platform: cfg.platformUrl.replace(/\/$/, ""),
14761
15145
  deviceId: body.device.deviceId,
14762
15146
  name
14763
15147
  }, null, 2));
14764
- (0, import_node_fs20.chmodSync)(path, 384);
14765
- out.error(`device: enrolled "${name}" for ${body.device.appIds.join(", ")}; credential written to ${path}`);
14766
- out.error("device: this terminal will mint its own credentials from now on \u2014 no further approvals.");
15148
+ (0, import_node_fs21.chmodSync)(path, 384);
15149
+ rememberMachineIdentity(cfg.platformUrl.replace(/\/$/, ""), stringOpt(parsed.options.email));
15150
+ supersedeAdvisory("credential.expiring");
15151
+ const reach = body.device.appIds.includes(import_db4.ALL_OWNED_APPS) ? "every app you own, now and later" : body.device.appIds.join(", ");
15152
+ out.error(`device: enrolled "${name}" for ${reach}; credential written to ${path}`);
15153
+ if (narrowNotice) out.error(narrowNotice);
15154
+ out.error(
15155
+ "device: every worktree on this machine mints its own credentials from now on \u2014 no further approvals,"
15156
+ );
15157
+ out.error(
15158
+ `device: and the clock resets each time you use it. Going quiet for ${describeWindow(body.device.expiresAt)} is what ends it.`
15159
+ );
14767
15160
  if (json) {
14768
- out.log(JSON.stringify({ deviceId: body.device.deviceId, name, appIds: body.device.appIds, expiresAt: body.device.expiresAt }, null, 2));
15161
+ out.log(JSON.stringify({
15162
+ deviceId: body.device.deviceId,
15163
+ name,
15164
+ appIds: body.device.appIds,
15165
+ capabilities: body.device.capabilities ?? [],
15166
+ scopes: body.device.scopes ?? [],
15167
+ expiresAt: body.device.expiresAt,
15168
+ hardExpiresAt: body.device.hardExpiresAt ?? null
15169
+ }, null, 2));
15170
+ }
15171
+ }
15172
+ function requestedEnvelope(parsed, platformWide, narrowed) {
15173
+ const raw = stringOpt(parsed.options.capability);
15174
+ const everything = platformWide || !narrowed || raw?.trim().toLowerCase() === "all";
15175
+ if (everything) {
15176
+ return {
15177
+ capabilities: [...import_db4.OPTIONAL_AGENT_PROJECT_CAPABILITIES],
15178
+ scopes: platformWide ? [...import_db4.ADMIN_DEVICE_SCOPES] : [...import_db4.OWNER_DEVICE_SCOPES]
15179
+ };
14769
15180
  }
15181
+ const named = raw?.split(",").map((c) => c.trim()).filter(Boolean);
15182
+ return named?.length ? { capabilities: named } : {};
15183
+ }
15184
+ function describeWindow(expiresAt, now = Date.now()) {
15185
+ const days = Math.max(1, Math.round((expiresAt - now) / (24 * 60 * 60 * 1e3)));
15186
+ if (days >= 365) return `${Math.round(days / 365)} year${days >= 730 ? "s" : ""}`;
15187
+ if (days % 7 === 0) return `${days / 7} week${days > 7 ? "s" : ""}`;
15188
+ return `${days} day${days === 1 ? "" : "s"}`;
14770
15189
  }
14771
15190
  async function list2(parsed, deps, cfg, doFetch, out, json) {
14772
15191
  const token = await scopedToken2(parsed, deps, cfg, doFetch, out, "odla CLI (device list)");
@@ -14781,12 +15200,17 @@ async function list2(parsed, deps, cfg, doFetch, out, json) {
14781
15200
  if (body.devices.length === 0) return out.log("no enrolled devices");
14782
15201
  for (const device of body.devices) {
14783
15202
  const state2 = device.revokedAt ? "revoked" : device.expiresAt <= Date.now() ? "expired" : "active";
14784
- out.log(`${device.deviceId} ${state2.padEnd(7)} ${device.name} [${device.appIds.join(", ")}]`);
15203
+ const reach = device.appIds.includes("*") ? "every app you own" : device.appIds.join(", ");
15204
+ const gap = state2 === "active" ? ` idle ${describeWindow(device.expiresAt)} left` : "";
15205
+ out.log(`${device.deviceId} ${state2.padEnd(7)} ${device.name} [${reach}]${gap}`);
14785
15206
  }
14786
15207
  }
14787
15208
  async function revoke(parsed, deps, cfg, doFetch, out, json) {
14788
15209
  const deviceId = parsed.positionals[2];
14789
15210
  if (!deviceId) throw new Error("device revoke needs the device id from `odla-ai device list`");
15211
+ out.error(
15212
+ `device: revoking is a signed-in human's decision; if this is refused, open ${cfg.platformUrl}/studio and revoke it there.`
15213
+ );
14790
15214
  const token = await scopedToken2(parsed, deps, cfg, doFetch, out, "odla CLI (device revoke)");
14791
15215
  const response2 = await doFetch(`${cfg.platformUrl}/registry/devices/${encodeURIComponent(deviceId)}/revoke`, {
14792
15216
  method: "POST",
@@ -14805,7 +15229,7 @@ async function scopedToken2(parsed, deps, cfg, doFetch, out, label, scope = "app
14805
15229
  // A device is granted the apps named in ONE approval, so --app is a list here.
14806
15230
  allowAppList: true
14807
15231
  });
14808
- const scopedTokenFile = credentials.scopedTokenFile;
15232
+ const scopedTokenFile2 = credentials.scopedTokenFile;
14809
15233
  return getScopedPlatformToken({
14810
15234
  platform: cfg.platformUrl,
14811
15235
  scope,
@@ -14816,22 +15240,26 @@ async function scopedToken2(parsed, deps, cfg, doFetch, out, label, scope = "app
14816
15240
  open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
14817
15241
  openApprovalUrl: deps.openUrl,
14818
15242
  rootDir: cfg.rootDir,
14819
- tokenFile: scopedTokenFile,
15243
+ tokenFile: scopedTokenFile2,
14820
15244
  ...stringOpt(parsed.options.token) ? { token: stringOpt(parsed.options.token) } : {}
14821
15245
  });
14822
15246
  }
14823
15247
  function defaultDeviceName() {
14824
- return `${import_node_process15.default.env.HOSTNAME ?? import_node_process15.default.env.HOST ?? "machine"}-${import_node_process15.default.platform}`;
15248
+ return `${import_node_process19.default.env.HOSTNAME ?? import_node_process19.default.env.HOST ?? "machine"}-${import_node_process19.default.platform}`;
14825
15249
  }
14826
- var import_node_fs20, import_node_path18, import_node_process15;
15250
+ var import_db4, import_node_fs21, import_node_path19, import_node_process19;
14827
15251
  var init_device_command = __esm({
14828
15252
  "src/device-command.ts"() {
14829
15253
  "use strict";
14830
15254
  init_cjs_shims();
15255
+ import_db4 = require("@odla-ai/db");
14831
15256
  init_device_ttl();
14832
- import_node_fs20 = require("fs");
14833
- import_node_path18 = require("path");
14834
- import_node_process15 = __toESM(require("process"), 1);
15257
+ init_machine_identity();
15258
+ init_auth_guidance();
15259
+ init_advisory_output();
15260
+ import_node_fs21 = require("fs");
15261
+ import_node_path19 = require("path");
15262
+ import_node_process19 = __toESM(require("process"), 1);
14835
15263
  init_argv();
14836
15264
  init_admin_ai_auth();
14837
15265
  init_device_session();
@@ -14886,7 +15314,7 @@ async function bySlug(ctx, slug) {
14886
15314
  function readBody(file, inline) {
14887
15315
  if (inline !== void 0) return inline;
14888
15316
  if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
14889
- return (0, import_node_fs21.readFileSync)(file === "-" ? 0 : file, "utf8");
15317
+ return (0, import_node_fs22.readFileSync)(file === "-" ? 0 : file, "utf8");
14890
15318
  }
14891
15319
  async function runbookList(ctx, all, query) {
14892
15320
  const params = new URLSearchParams();
@@ -14975,12 +15403,12 @@ async function runbookRemove(ctx, slug) {
14975
15403
  await call2(ctx, "DELETE", `/runbook/${encodeURIComponent(runbook.id)}`);
14976
15404
  ctx.out.log(`removed ${slug}`);
14977
15405
  }
14978
- var import_node_fs21, PLATFORM_SCOPE, stamp;
15406
+ var import_node_fs22, PLATFORM_SCOPE, stamp;
14979
15407
  var init_runbook_actions = __esm({
14980
15408
  "src/runbook-actions.ts"() {
14981
15409
  "use strict";
14982
15410
  init_cjs_shims();
14983
- import_node_fs21 = require("fs");
15411
+ import_node_fs22 = require("fs");
14984
15412
  init_version();
14985
15413
  init_runbook_requires();
14986
15414
  PLATFORM_SCOPE = "$platform";
@@ -15013,12 +15441,12 @@ function parseRunbook(text3, slug) {
15013
15441
  };
15014
15442
  }
15015
15443
  function readRunbookDir(dir) {
15016
- if (!(0, import_node_fs22.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
15017
- const files = (0, import_node_fs22.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
15444
+ if (!(0, import_node_fs23.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
15445
+ const files = (0, import_node_fs23.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
15018
15446
  if (!files.length) throw new Error(`no .md files in ${dir}`);
15019
15447
  return files.map((file) => {
15020
- const slug = (0, import_node_path19.basename)(file, ".md");
15021
- const parsed = parseRunbook((0, import_node_fs22.readFileSync)((0, import_node_path19.join)(dir, file), "utf8"), slug);
15448
+ const slug = (0, import_node_path20.basename)(file, ".md");
15449
+ const parsed = parseRunbook((0, import_node_fs23.readFileSync)((0, import_node_path20.join)(dir, file), "utf8"), slug);
15022
15450
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
15023
15451
  });
15024
15452
  }
@@ -15083,13 +15511,13 @@ async function upsert(ctx, r, visibility) {
15083
15511
  );
15084
15512
  return "updated";
15085
15513
  }
15086
- var import_node_fs22, import_node_path19;
15514
+ var import_node_fs23, import_node_path20;
15087
15515
  var init_runbook_import = __esm({
15088
15516
  "src/runbook-import.ts"() {
15089
15517
  "use strict";
15090
15518
  init_cjs_shims();
15091
- import_node_fs22 = require("fs");
15092
- import_node_path19 = require("path");
15519
+ import_node_fs23 = require("fs");
15520
+ import_node_path20 = require("path");
15093
15521
  init_runbook_actions();
15094
15522
  }
15095
15523
  });
@@ -15267,10 +15695,10 @@ ${body.split("\n").map((line2) => `+${line2}`).join("\n")}
15267
15695
  }
15268
15696
  function manifestLabeller(root) {
15269
15697
  return (workspace) => {
15270
- const manifest = (0, import_node_path20.join)(root, workspace, "package.json");
15271
- if (!(0, import_node_fs23.existsSync)(manifest)) return void 0;
15698
+ const manifest = (0, import_node_path21.join)(root, workspace, "package.json");
15699
+ if (!(0, import_node_fs24.existsSync)(manifest)) return void 0;
15272
15700
  try {
15273
- const name = JSON.parse((0, import_node_fs23.readFileSync)(manifest, "utf8")).name;
15701
+ const name = JSON.parse((0, import_node_fs24.readFileSync)(manifest, "utf8")).name;
15274
15702
  return typeof name === "string" ? name : void 0;
15275
15703
  } catch {
15276
15704
  return void 0;
@@ -15336,7 +15764,7 @@ function report4(ctx, impacts) {
15336
15764
  async function runbookImpact(ctx, options, deps = {}) {
15337
15765
  const cwd = deps.cwd ?? process.cwd();
15338
15766
  const runGit = deps.runGit ?? gitRunner(cwd);
15339
- const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs23.readFileSync)((0, import_node_path20.join)(cwd, path), "utf8"));
15767
+ const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs24.readFileSync)((0, import_node_path21.join)(cwd, path), "utf8"));
15340
15768
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
15341
15769
  if (!surfaces.length) {
15342
15770
  return ctx.out.log(
@@ -15347,14 +15775,14 @@ async function runbookImpact(ctx, options, deps = {}) {
15347
15775
  if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
15348
15776
  report4(ctx, impacts);
15349
15777
  }
15350
- var import_node_child_process6, import_node_fs23, import_node_path20, SOURCE3, editHint;
15778
+ var import_node_child_process6, import_node_fs24, import_node_path21, SOURCE3, editHint;
15351
15779
  var init_runbook_impact = __esm({
15352
15780
  "src/runbook-impact.ts"() {
15353
15781
  "use strict";
15354
15782
  init_cjs_shims();
15355
15783
  import_node_child_process6 = require("child_process");
15356
- import_node_fs23 = require("fs");
15357
- import_node_path20 = require("path");
15784
+ import_node_fs24 = require("fs");
15785
+ import_node_path21 = require("path");
15358
15786
  init_runbook_impact_scan();
15359
15787
  init_runbook_actions();
15360
15788
  SOURCE3 = /\.(ts|tsx|js|jsx|mts|cts)$/;
@@ -15494,7 +15922,7 @@ var init_runbook_search_command = __esm({
15494
15922
  });
15495
15923
 
15496
15924
  // src/runbook-editor.ts
15497
- function resolveEditor(env = import_node_process16.default.env) {
15925
+ function resolveEditor(env = import_node_process20.default.env) {
15498
15926
  for (const name of EDITOR_ENV) {
15499
15927
  const value2 = env[name];
15500
15928
  if (value2 && value2.trim()) return value2.trim();
@@ -15508,8 +15936,8 @@ function defaultRun(command, path) {
15508
15936
  return result.status ?? 0;
15509
15937
  }
15510
15938
  function editText(initial, slug, deps = {}) {
15511
- const env = deps.env ?? import_node_process16.default.env;
15512
- const interactive = deps.interactive ?? (() => Boolean(import_node_process16.default.stdin.isTTY));
15939
+ const env = deps.env ?? import_node_process20.default.env;
15940
+ const interactive = deps.interactive ?? (() => Boolean(import_node_process20.default.stdin.isTTY));
15513
15941
  const editor = resolveEditor(env);
15514
15942
  if (!editor)
15515
15943
  throw new Error(
@@ -15517,28 +15945,28 @@ function editText(initial, slug, deps = {}) {
15517
15945
  );
15518
15946
  if (!interactive())
15519
15947
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
15520
- const dir = (0, import_node_fs24.mkdtempSync)((0, import_node_path21.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
15521
- const file = (0, import_node_path21.join)(dir, `${slug}.md`);
15948
+ const dir = (0, import_node_fs25.mkdtempSync)((0, import_node_path22.join)((0, import_node_os6.tmpdir)(), "odla-runbook-"));
15949
+ const file = (0, import_node_path22.join)(dir, `${slug}.md`);
15522
15950
  try {
15523
- (0, import_node_fs24.writeFileSync)(file, initial, { mode: 384 });
15951
+ (0, import_node_fs25.writeFileSync)(file, initial, { mode: 384 });
15524
15952
  const code = defaultRunOrInjected(deps)(editor, file);
15525
15953
  if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
15526
- const edited = (0, import_node_fs24.readFileSync)(file, "utf8");
15954
+ const edited = (0, import_node_fs25.readFileSync)(file, "utf8");
15527
15955
  return edited === initial ? null : edited;
15528
15956
  } finally {
15529
- (0, import_node_fs24.rmSync)(dir, { recursive: true, force: true });
15957
+ (0, import_node_fs25.rmSync)(dir, { recursive: true, force: true });
15530
15958
  }
15531
15959
  }
15532
- var import_node_child_process7, import_node_fs24, import_node_os5, import_node_path21, import_node_process16, EDITOR_ENV, defaultRunOrInjected;
15960
+ var import_node_child_process7, import_node_fs25, import_node_os6, import_node_path22, import_node_process20, EDITOR_ENV, defaultRunOrInjected;
15533
15961
  var init_runbook_editor = __esm({
15534
15962
  "src/runbook-editor.ts"() {
15535
15963
  "use strict";
15536
15964
  init_cjs_shims();
15537
15965
  import_node_child_process7 = require("child_process");
15538
- import_node_fs24 = require("fs");
15539
- import_node_os5 = require("os");
15540
- import_node_path21 = require("path");
15541
- import_node_process16 = __toESM(require("process"), 1);
15966
+ import_node_fs25 = require("fs");
15967
+ import_node_os6 = require("os");
15968
+ import_node_path22 = require("path");
15969
+ import_node_process20 = __toESM(require("process"), 1);
15542
15970
  EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
15543
15971
  defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
15544
15972
  }
@@ -15927,9 +16355,9 @@ async function runHostedSecurity(options) {
15927
16355
  const appId = selfAudit ? "odla-ai" : cfg.app.id;
15928
16356
  const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
15929
16357
  const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
15930
- const target = (0, import_node_path22.resolve)(options.target ?? cfg?.rootDir ?? ".");
15931
- const output = (0, import_node_path22.resolve)(options.out ?? (0, import_node_path22.resolve)(target, ".odla/security/hosted"));
15932
- const outputRelative = (0, import_node_path22.relative)(target, output).split(import_node_path22.sep).join("/");
16358
+ const target = (0, import_node_path23.resolve)(options.target ?? cfg?.rootDir ?? ".");
16359
+ const output = (0, import_node_path23.resolve)(options.out ?? (0, import_node_path23.resolve)(target, ".odla/security/hosted"));
16360
+ const outputRelative = (0, import_node_path23.relative)(target, output).split(import_node_path23.sep).join("/");
15933
16361
  if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
15934
16362
  const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
15935
16363
  const tokenRequest = {
@@ -15941,7 +16369,7 @@ async function runHostedSecurity(options) {
15941
16369
  };
15942
16370
  const token = await injectedToken(options, tokenRequest);
15943
16371
  const snapshot = await (0, import_node3.snapshotDirectory)(target, {
15944
- exclude: !outputRelative.startsWith("../") && !(0, import_node_path22.isAbsolute)(outputRelative) ? [outputRelative] : []
16372
+ exclude: !outputRelative.startsWith("../") && !(0, import_node_path23.isAbsolute)(outputRelative) ? [outputRelative] : []
15945
16373
  });
15946
16374
  const hosted = await (0, import_security.createPlatformSecurityReasoners)({
15947
16375
  platform,
@@ -15959,7 +16387,7 @@ async function runHostedSecurity(options) {
15959
16387
  });
15960
16388
  const harness = (0, import_security.createSecurityHarness)({
15961
16389
  profile,
15962
- store: new import_node3.FileRunStore((0, import_node_path22.resolve)(output, "state")),
16390
+ store: new import_node3.FileRunStore((0, import_node_path23.resolve)(output, "state")),
15963
16391
  discoveryReasoner: hosted.discoveryReasoner,
15964
16392
  validationReasoner: hosted.validationReasoner,
15965
16393
  policy: {
@@ -15983,7 +16411,7 @@ async function runHostedSecurity(options) {
15983
16411
  function selectEnv(requested, declared, configPath, rootDir) {
15984
16412
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
15985
16413
  if (!env || !declared.includes(env)) {
15986
- const shown = (0, import_node_path22.relative)(rootDir, configPath) || configPath;
16414
+ const shown = (0, import_node_path23.relative)(rootDir, configPath) || configPath;
15987
16415
  throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
15988
16416
  }
15989
16417
  return env;
@@ -16012,17 +16440,17 @@ function printSummary(out, appId, env, run, report5, output) {
16012
16440
  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}`);
16013
16441
  if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
16014
16442
  out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
16015
- out.log(` report: ${(0, import_node_path22.resolve)(output, "REPORT.md")}`);
16443
+ out.log(` report: ${(0, import_node_path23.resolve)(output, "REPORT.md")}`);
16016
16444
  }
16017
16445
  function formatBudget(usage) {
16018
16446
  return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
16019
16447
  }
16020
- var import_node_path22, import_security, import_node3;
16448
+ var import_node_path23, import_security, import_node3;
16021
16449
  var init_security = __esm({
16022
16450
  "src/security.ts"() {
16023
16451
  "use strict";
16024
16452
  init_cjs_shims();
16025
- import_node_path22 = require("path");
16453
+ import_node_path23 = require("path");
16026
16454
  import_security = require("@odla-ai/security");
16027
16455
  import_node3 = require("@odla-ai/security/node");
16028
16456
  init_config();
@@ -16540,8 +16968,10 @@ async function dispatchCli(argv2, dependencies) {
16540
16968
  return;
16541
16969
  }
16542
16970
  if (command === "help" || command === "--help" || command === "-h") {
16543
- assertArgs(parsed, ["help"], 1);
16544
- printHelp(runtime.stdout);
16971
+ assertArgs(parsed, ["help"], 2);
16972
+ const topic = parsed.positionals[1];
16973
+ if (topic) printCommandHelp(topic, runtime.stdout);
16974
+ else printHelp(runtime.stdout);
16545
16975
  return;
16546
16976
  }
16547
16977
  if (command === "whoami") {
@@ -16712,6 +17142,7 @@ var init_cli = __esm({
16712
17142
  init_context_command();
16713
17143
  init_credential_command();
16714
17144
  init_help();
17145
+ init_help_command();
16715
17146
  init_version();
16716
17147
  init_discuss_command();
16717
17148
  init_pm_command();