@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/index.cjs CHANGED
@@ -97,12 +97,12 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
97
97
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
98
98
 
99
99
  // src/admin-ai.ts
100
- var import_node_process8 = __toESM(require("process"), 1);
100
+ var import_node_process12 = __toESM(require("process"), 1);
101
101
 
102
102
  // src/token.ts
103
103
  var import_db = require("@odla-ai/db");
104
104
  var import_node_crypto = require("crypto");
105
- var import_node_process5 = __toESM(require("process"), 1);
105
+ var import_node_process9 = __toESM(require("process"), 1);
106
106
 
107
107
  // src/handshake-approval.ts
108
108
  var import_node_process2 = __toESM(require("process"), 1);
@@ -237,30 +237,13 @@ function handshakeWaitMs(waitSeconds, interactive = import_node_process3.default
237
237
  }
238
238
 
239
239
  // src/cached-credential.ts
240
- var import_node_fs2 = require("fs");
241
- var noted = null;
242
- function noteCachedCredential(tokenFile) {
243
- noted = tokenFile;
244
- }
245
- function isCredentialRejection(error) {
246
- const message2 = error instanceof Error ? error.message : String(error ?? "");
247
- return /\((401|403)\)\s*$/.test(message2.trim());
248
- }
249
- function explainRejectedCredential(error) {
250
- const tokenFile = noted;
251
- if (!tokenFile || !isCredentialRejection(error)) return null;
252
- noted = null;
253
- (0, import_node_fs2.rmSync)(tokenFile, { force: true });
254
- return [
255
- "auth: the cached credential was rejected by odla, so it was revoked before its cached expiry.",
256
- " The usual cause is a newer sign-in for this project: collecting a handshake retires the",
257
- " principal's other credentials, so a second terminal or worktree supersedes this one.",
258
- ` Discarded ${tokenFile}; re-run this command to request a fresh approval.`
259
- ].join("\n");
260
- }
240
+ var import_node_fs5 = require("fs");
241
+
242
+ // src/auth-guidance.ts
243
+ var import_node_process6 = __toESM(require("process"), 1);
261
244
 
262
245
  // src/device-session.ts
263
- var import_node_fs3 = require("fs");
246
+ var import_node_fs2 = require("fs");
264
247
  var import_node_os = require("os");
265
248
  var import_node_path2 = require("path");
266
249
  var import_node_process4 = __toESM(require("process"), 1);
@@ -269,9 +252,9 @@ function deviceCredentialPath(env = import_node_process4.default.env) {
269
252
  }
270
253
  function readDeviceCredential(platform, env = import_node_process4.default.env) {
271
254
  const path = deviceCredentialPath(env);
272
- if (!(0, import_node_fs3.existsSync)(path)) return null;
255
+ if (!(0, import_node_fs2.existsSync)(path)) return null;
273
256
  try {
274
- const parsed = JSON.parse((0, import_node_fs3.readFileSync)(path, "utf8"));
257
+ const parsed = JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
275
258
  if (typeof parsed.token !== "string" || !parsed.token.startsWith("odla_device_")) return null;
276
259
  if (parsed.platform !== platform) return null;
277
260
  return { ...parsed, token: parsed.token, platform: parsed.platform };
@@ -293,12 +276,76 @@ async function mintDeviceSession(platformUrl, credential2, doFetch) {
293
276
  `device session failed: ${detail} (${response2.status})` + (revocable ? " \u2014 if this machine's enrollment was revoked or has expired, enroll it again in Studio" : "")
294
277
  );
295
278
  }
296
- return { token: body.token, expiresAt: body.expiresAt ?? Date.now() };
279
+ return {
280
+ token: body.token,
281
+ expiresAt: body.expiresAt ?? Date.now(),
282
+ // Absent from a registry that predates rolling expiry and scoped devices.
283
+ // Left undefined rather than defaulted, so a caller can tell "the platform
284
+ // did not say" from "the platform said none".
285
+ ...typeof body.deviceExpiresAt === "number" ? { deviceExpiresAt: body.deviceExpiresAt } : {},
286
+ ...Array.isArray(body.appIds) ? { appIds: body.appIds } : {},
287
+ ...Array.isArray(body.capabilities) ? { capabilities: body.capabilities } : {},
288
+ ...Array.isArray(body.scopes) ? { scopes: body.scopes } : {}
289
+ };
290
+ }
291
+
292
+ // src/odla-home.ts
293
+ var import_node_fs3 = require("fs");
294
+ var import_node_os2 = require("os");
295
+ var import_node_path3 = require("path");
296
+ var import_node_process5 = __toESM(require("process"), 1);
297
+ function odlaHome(env = import_node_process5.default.env) {
298
+ if (env.VITEST && !env.ODLA_HOME) {
299
+ throw new Error(
300
+ "ODLA_HOME must be set under test \u2014 resolving the real ~/.odla would write to the developer's machine"
301
+ );
302
+ }
303
+ return env.ODLA_HOME ?? (0, import_node_path3.join)(env.HOME ?? (0, import_node_os2.homedir)(), ".odla");
304
+ }
305
+ function odlaHomePath(segments, env = import_node_process5.default.env) {
306
+ return (0, import_node_path3.join)(odlaHome(env), ...segments);
307
+ }
308
+ function identityFile(env) {
309
+ return odlaHomePath(["identity.json"], env);
310
+ }
311
+ function deviceSessionFile(env) {
312
+ return odlaHomePath(["session.json"], env);
313
+ }
314
+ function appTokenFile(appId, env) {
315
+ return odlaHomePath(["apps", safeSegment(appId), "dev-token.json"], env);
316
+ }
317
+ function appCredentialsFile(appId, env) {
318
+ return odlaHomePath(["apps", safeSegment(appId), "credentials.json"], env);
319
+ }
320
+ function scopedTokenFile(env) {
321
+ return odlaHomePath(["admin-token.local.json"], env);
322
+ }
323
+ function pmContextFile(env) {
324
+ return odlaHomePath(["pm-context.json"], env);
325
+ }
326
+ function adoptRepoLocalCache(legacyPath, machinePath, out) {
327
+ if (!(0, import_node_fs3.existsSync)(legacyPath) || legacyPath === machinePath) return false;
328
+ const superseded2 = (0, import_node_fs3.existsSync)(machinePath);
329
+ if (!superseded2) {
330
+ (0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(machinePath), { recursive: true });
331
+ (0, import_node_fs3.copyFileSync)(legacyPath, machinePath);
332
+ (0, import_node_fs3.chmodSync)(machinePath, 384);
333
+ }
334
+ (0, import_node_fs3.rmSync)(legacyPath, { force: true });
335
+ out?.error(
336
+ 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`
337
+ );
338
+ return true;
339
+ }
340
+ function safeSegment(value2) {
341
+ const clean4 = value2.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "");
342
+ if (!clean4) throw new Error(`"${value2}" is not a usable app id`);
343
+ return clean4;
297
344
  }
298
345
 
299
346
  // src/local.ts
300
347
  var import_node_fs4 = require("fs");
301
- var import_node_path3 = require("path");
348
+ var import_node_path4 = require("path");
302
349
  var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
303
350
  function readJsonFile(path) {
304
351
  try {
@@ -345,7 +392,7 @@ function mergeCredential(current, update) {
345
392
  return next;
346
393
  }
347
394
  function ensureGitignore(rootDir, localPaths = []) {
348
- const path = (0, import_node_path3.resolve)(rootDir, ".gitignore");
395
+ const path = (0, import_node_path4.resolve)(rootDir, ".gitignore");
349
396
  const existing = (0, import_node_fs4.existsSync)(path) ? (0, import_node_fs4.readFileSync)(path, "utf8") : "";
350
397
  const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line2) => !!line2);
351
398
  const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
@@ -366,7 +413,7 @@ function o11yDevVars(cfg) {
366
413
  function resolveWriteDevVarsTarget(cfg, requested) {
367
414
  if (!requested) return null;
368
415
  if (requested === true) return cfg.local.devVarsFile;
369
- return (0, import_node_path3.resolve)((0, import_node_path3.dirname)(cfg.configPath), requested);
416
+ return (0, import_node_path4.resolve)((0, import_node_path4.dirname)(cfg.configPath), requested);
370
417
  }
371
418
  function writeDevVars(path, credentials, env, o11y) {
372
419
  const entry = credentials.envs[env];
@@ -406,22 +453,124 @@ function isManagedDevVar(line2) {
406
453
  return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
407
454
  }
408
455
  function writePrivateText(path, text3) {
409
- (0, import_node_fs4.mkdirSync)((0, import_node_path3.dirname)(path), { recursive: true });
456
+ (0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(path), { recursive: true });
410
457
  const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
411
458
  (0, import_node_fs4.writeFileSync)(temporary, text3, { mode: 384 });
412
459
  (0, import_node_fs4.chmodSync)(temporary, 384);
413
460
  (0, import_node_fs4.renameSync)(temporary, path);
414
461
  }
415
462
  function gitignoreEntry(rootDir, path) {
416
- const rel = (0, import_node_path3.relative)((0, import_node_path3.resolve)(rootDir), (0, import_node_path3.resolve)(path));
417
- if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path3.isAbsolute)(rel)) return null;
463
+ const rel = (0, import_node_path4.relative)((0, import_node_path4.resolve)(rootDir), (0, import_node_path4.resolve)(path));
464
+ if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path4.isAbsolute)(rel)) return null;
418
465
  return rel.replaceAll("\\", "/");
419
466
  }
420
467
  function displayPath(path, rootDir = process.cwd()) {
421
- const rel = (0, import_node_path3.relative)(rootDir, path);
468
+ const rel = (0, import_node_path4.relative)(rootDir, path);
422
469
  return rel && !rel.startsWith("..") ? rel : path;
423
470
  }
424
471
 
472
+ // src/auth-guidance.ts
473
+ var ENROL_EVERYTHING = "npx odla-ai device enroll --no-open --wait 600";
474
+ var ENROL_PLATFORM_WIDE = "npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600";
475
+ function machineAuthState(audience, env = import_node_process6.default.env) {
476
+ const device = readDeviceCredential(audience, env);
477
+ if (!device) return { enrolled: false };
478
+ const session = readJsonFile(deviceSessionFile(env));
479
+ const current = session?.platform === audience && session.deviceId === device.deviceId ? session : void 0;
480
+ return {
481
+ enrolled: true,
482
+ ...device.name ? { deviceName: device.name } : {},
483
+ ...current?.appIds ? { appIds: current.appIds } : {},
484
+ ...current?.capabilities ? { capabilities: current.capabilities } : {},
485
+ ...current?.scopes ? { scopes: current.scopes } : {},
486
+ ...current?.deviceExpiresAt ? { lapsesAt: current.deviceExpiresAt } : {}
487
+ };
488
+ }
489
+ function scopeInterruptionNotice(scope, state2) {
490
+ const platformScope = scope.startsWith("platform:");
491
+ 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}"`;
492
+ return [
493
+ `odla: ${reason}.`,
494
+ ` Approve this one now, then end the interruptions with:`,
495
+ ` ${platformScope ? ENROL_PLATFORM_WIDE : ENROL_EVERYTHING}`,
496
+ 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."
497
+ ].join("\n");
498
+ }
499
+ function narrowEnrollmentNotice(envelope) {
500
+ const everyApp = envelope.appIds.includes("*");
501
+ if (everyApp && envelope.capabilities.length > 0 && envelope.scopes.length > 0) return null;
502
+ const missing = [
503
+ everyApp ? null : `only ${envelope.appIds.length} app${envelope.appIds.length === 1 ? "" : "s"} \u2014 a new one will need a new approval`,
504
+ envelope.capabilities.length ? null : "no optional capabilities \u2014 app.manage, crm.read and code.session are not included",
505
+ envelope.scopes.length ? null : "no platform scopes \u2014 runbook edits, config plans and host connect will each ask again"
506
+ ].filter(Boolean);
507
+ return [
508
+ `odla: this is a NARROW enrollment: ${missing.join("; ")}.`,
509
+ " That is a fine choice if you meant it. If you did not, drop the flags:",
510
+ ` ${ENROL_EVERYTHING}`
511
+ ].join("\n");
512
+ }
513
+ function lapseNotice(state2, now = Date.now()) {
514
+ if (!state2.enrolled || !state2.lapsesAt) return null;
515
+ const days = Math.floor((state2.lapsesAt - now) / (24 * 60 * 60 * 1e3));
516
+ if (days < 0) return "this machine's enrollment has lapsed; the next command will ask for approval";
517
+ return `idle for ${days} more day${days === 1 ? "" : "s"} before this machine needs approving again (using it resets the clock)`;
518
+ }
519
+
520
+ // src/cached-credential.ts
521
+ var noted = null;
522
+ function noteCachedCredential(tokenFile) {
523
+ noted = tokenFile;
524
+ }
525
+ function isCredentialRejection(error) {
526
+ const message2 = error instanceof Error ? error.message : String(error ?? "");
527
+ return /\((401|403)\)\s*$/.test(message2.trim());
528
+ }
529
+ function explainRejectedCredential(error) {
530
+ const tokenFile = noted;
531
+ if (!tokenFile || !isCredentialRejection(error)) return null;
532
+ noted = null;
533
+ (0, import_node_fs5.rmSync)(tokenFile, { force: true });
534
+ return [
535
+ "auth: the cached credential was rejected by odla, so it was revoked before its cached expiry.",
536
+ " The usual cause is a newer sign-in for this account: collecting a handshake retires the",
537
+ " principal's other collected credentials, so a second machine supersedes this one.",
538
+ ` Discarded ${tokenFile}; re-run this command to request a fresh approval.`,
539
+ ` To stop needing one: ${ENROL_EVERYTHING}`
540
+ ].join("\n");
541
+ }
542
+
543
+ // src/device-session-cache.ts
544
+ var import_node_process7 = __toESM(require("process"), 1);
545
+ var SKEW_MS = 6e4;
546
+ async function deviceSessionToken(platformUrl, audience, credential2, doFetch, env = import_node_process7.default.env) {
547
+ const path = deviceSessionFile(env);
548
+ const cached = readJsonFile(path);
549
+ if (cached?.token && cached.platform === audience && cached.deviceId === credential2.deviceId && (cached.expiresAt ?? 0) > Date.now() + SKEW_MS) return cached;
550
+ const minted = await mintDeviceSession(platformUrl, credential2, doFetch);
551
+ const session = {
552
+ ...minted,
553
+ platform: audience,
554
+ ...credential2.deviceId ? { deviceId: credential2.deviceId } : {}
555
+ };
556
+ writePrivateJson(path, session);
557
+ return session;
558
+ }
559
+
560
+ // src/machine-identity.ts
561
+ var import_node_process8 = __toESM(require("process"), 1);
562
+ function readMachineIdentity(audience, env = import_node_process8.default.env) {
563
+ const stored = readJsonFile(identityFile(env));
564
+ if (!stored || typeof stored.email !== "string" || !stored.email) return null;
565
+ return stored.platform === audience ? { platform: audience, email: stored.email } : null;
566
+ }
567
+ function rememberMachineIdentity(audience, email, env = import_node_process8.default.env) {
568
+ if (!email) return;
569
+ const existing = readMachineIdentity(audience, env);
570
+ if (existing?.email === email) return;
571
+ writePrivateJson(identityFile(env), { platform: audience, email });
572
+ }
573
+
425
574
  // src/token.ts
426
575
  async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {}) {
427
576
  const audience = platformAudience(cfg.platformUrl);
@@ -430,19 +579,19 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
430
579
  const cached = readJsonFile(cfg.local.tokenFile);
431
580
  if (!grantRequest.forceReview && !grantRequest.freshLogin) {
432
581
  if (options.token) return options.token;
433
- if (import_node_process5.default.env.ODLA_DEV_TOKEN) {
434
- const declared = import_node_process5.default.env.ODLA_DEV_TOKEN_AUDIENCE;
582
+ if (import_node_process9.default.env.ODLA_DEV_TOKEN) {
583
+ const declared = import_node_process9.default.env.ODLA_DEV_TOKEN_AUDIENCE;
435
584
  if (declared) {
436
585
  if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
437
586
  } else if (audience !== "https://odla.ai") {
438
587
  throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
439
588
  }
440
- return import_node_process5.default.env.ODLA_DEV_TOKEN;
589
+ return import_node_process9.default.env.ODLA_DEV_TOKEN;
441
590
  }
442
591
  const device = readDeviceCredential(audience);
443
592
  if (device) {
444
- const session = await mintDeviceSession(cfg.platformUrl, device, doFetch);
445
- out.error(`auth: session minted by this enrolled device (${displayPath(deviceCredentialPath(), cfg.rootDir)})`);
593
+ const session = await deviceSessionToken(cfg.platformUrl, audience, device, doFetch);
594
+ out.error(`auth: session held by this enrolled device (${displayPath(deviceCredentialPath(), cfg.rootDir)})`);
446
595
  return session.token;
447
596
  }
448
597
  if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4 && cachedGrantCovers(cached, grantIntent)) {
@@ -462,7 +611,10 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
462
611
  doFetch,
463
612
  out,
464
613
  audience,
465
- email: handshakeEmail(options.email, cached?.platform === audience ? cached.email : void 0),
614
+ email: handshakeEmail(
615
+ options.email,
616
+ (cached?.platform === audience ? cached.email : void 0) ?? readMachineIdentity(audience)?.email
617
+ ),
466
618
  pendingFile: handshakeFile(cfg),
467
619
  grantIntent
468
620
  };
@@ -479,6 +631,7 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
479
631
  expiresAt
480
632
  });
481
633
  out.error(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
634
+ rememberMachineIdentity(audience, ctx.email);
482
635
  return token;
483
636
  }
484
637
  async function freshHandshake(ctx, waitMs) {
@@ -548,7 +701,7 @@ function stillPending(pending, email) {
548
701
  );
549
702
  }
550
703
  function handshakeEmail(value2, cached) {
551
- const email = (value2 ?? import_node_process5.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
704
+ const email = (value2 ?? import_node_process9.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
552
705
  if (/@users\.noreply\.github\.com$/i.test(email)) {
553
706
  throw new Error(
554
707
  `"${email}" is a GitHub commit identity, not an odla account email; use --email <signed-in-odla-account> or ODLA_USER_EMAIL`
@@ -579,12 +732,12 @@ function platformAudience(value2) {
579
732
  }
580
733
 
581
734
  // src/secret-input.ts
582
- var import_node_process6 = __toESM(require("process"), 1);
735
+ var import_node_process10 = __toESM(require("process"), 1);
583
736
  var MAX_BYTES = 64 * 1024;
584
737
  async function secretInputValue(options, kind = "credential") {
585
738
  if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
586
739
  let value2;
587
- if (options.fromEnv) value2 = import_node_process6.default.env[options.fromEnv];
740
+ if (options.fromEnv) value2 = import_node_process10.default.env[options.fromEnv];
588
741
  else if (options.stdin) value2 = await (options.readStdin ?? (() => readSecretStream(kind)))();
589
742
  else throw new Error(`${kind} input required: use --from-env <NAME> or --stdin; values are never accepted as arguments`);
590
743
  value2 = value2?.replace(/[\r\n]+$/, "");
@@ -592,7 +745,7 @@ async function secretInputValue(options, kind = "credential") {
592
745
  if (new TextEncoder().encode(value2).byteLength > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
593
746
  return value2;
594
747
  }
595
- async function readSecretStream(kind, stream = import_node_process6.default.stdin) {
748
+ async function readSecretStream(kind, stream = import_node_process10.default.stdin) {
596
749
  let value2 = "";
597
750
  for await (const chunk of stream) {
598
751
  value2 += String(chunk);
@@ -602,9 +755,8 @@ async function readSecretStream(kind, stream = import_node_process6.default.stdi
602
755
  }
603
756
 
604
757
  // src/admin-ai-auth.ts
605
- var import_node_fs5 = require("fs");
606
- var import_node_path4 = require("path");
607
- var import_node_process7 = __toESM(require("process"), 1);
758
+ var import_node_path5 = require("path");
759
+ var import_node_process11 = __toESM(require("process"), 1);
608
760
  var import_db2 = require("@odla-ai/db");
609
761
  async function getScopedPlatformToken(options) {
610
762
  return resolveAdminPlatformToken(options);
@@ -612,7 +764,7 @@ async function getScopedPlatformToken(options) {
612
764
  async function resolveAdminPlatformToken(options) {
613
765
  const audience = platformAudience(options.platform);
614
766
  if (options.token) return options.token;
615
- const fromEnv = import_node_process7.default.env.ODLA_ADMIN_TOKEN;
767
+ const fromEnv = import_node_process11.default.env.ODLA_ADMIN_TOKEN;
616
768
  if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
617
769
  return scopedToken(
618
770
  audience,
@@ -624,7 +776,7 @@ async function resolveAdminPlatformToken(options) {
624
776
  }
625
777
  function audienceBoundEnvToken(token, platform) {
626
778
  const audience = platformAudience(platform);
627
- const declared = import_node_process7.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
779
+ const declared = import_node_process11.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
628
780
  if (declared) {
629
781
  if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
630
782
  } else if (audience !== "https://odla.ai") {
@@ -651,15 +803,28 @@ var SCOPE_PURPOSE = {
651
803
  };
652
804
  async function scopedToken(platform, scope, options, doFetch, out) {
653
805
  const audience = platformAudience(platform);
654
- const rootDir = options.rootDir ?? import_node_process7.default.cwd();
655
- const tokenFile = options.tokenFile ?? (0, import_node_path4.join)(rootDir, ".odla/admin-token.local.json");
806
+ const rootDir = options.rootDir ?? import_node_process11.default.cwd();
807
+ const tokenFile = options.tokenFile ?? scopedTokenFile();
808
+ adoptRepoLocalCache((0, import_node_path5.join)(rootDir, ".odla/admin-token.local.json"), tokenFile, out);
809
+ const device = readDeviceCredential(audience);
810
+ if (device && options.cache !== false) {
811
+ const session = await deviceSessionToken(platform, audience, device, doFetch);
812
+ if (session.scopes?.includes(scope)) {
813
+ out.error(`auth: ${scope} held by this enrolled device`);
814
+ return session.token;
815
+ }
816
+ }
656
817
  const cache2 = options.cache === false ? null : readJsonFile(tokenFile);
657
818
  const cached = cache2?.platform === audience ? cache2.tokens?.[scope] : void 0;
658
819
  if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
659
820
  out.error(`auth: using cached ${scope} grant (${tokenFile})`);
660
821
  return cached.token;
661
822
  }
662
- const email = handshakeEmail(options.email, cache2?.platform === audience ? cache2.email : void 0);
823
+ out.error(scopeInterruptionNotice(scope, machineAuthState(audience)));
824
+ const email = handshakeEmail(
825
+ options.email,
826
+ (cache2?.platform === audience ? cache2.email : void 0) ?? readMachineIdentity(audience)?.email
827
+ );
663
828
  const { token, expiresAt } = await (0, import_db2.requestToken)({
664
829
  endpoint: audience,
665
830
  email,
@@ -679,8 +844,8 @@ async function scopedToken(platform, scope, options, doFetch, out) {
679
844
  if (options.cache !== false) {
680
845
  const tokens = cache2?.platform === audience ? { ...cache2.tokens ?? {} } : {};
681
846
  tokens[scope] = { token, expiresAt };
682
- if ((0, import_node_fs5.existsSync)((0, import_node_path4.join)(rootDir, ".git"))) ensureGitignore(rootDir, [tokenFile]);
683
847
  writePrivateJson(tokenFile, { platform: audience, email, tokens });
848
+ rememberMachineIdentity(audience, email);
684
849
  out.error(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
685
850
  } else {
686
851
  out.error(`auth: ${scope} grant is in memory only; its credential record remains in odla-ai/db`);
@@ -856,7 +1021,7 @@ function isRecord2(value2) {
856
1021
 
857
1022
  // src/admin-ai.ts
858
1023
  async function adminAi(options) {
859
- const platform = platformAudience(options.platform ?? import_node_process8.default.env.ODLA_PLATFORM ?? "https://odla.ai");
1024
+ const platform = platformAudience(options.platform ?? import_node_process12.default.env.ODLA_PLATFORM ?? "https://odla.ai");
860
1025
  const doFetch = options.fetch ?? fetch;
861
1026
  const out = options.stdout ?? console;
862
1027
  const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
@@ -1172,12 +1337,12 @@ async function adminSpend(parsed, ctx) {
1172
1337
 
1173
1338
  // src/operator-context.ts
1174
1339
  var import_node_fs8 = require("fs");
1175
- var import_node_path7 = require("path");
1176
- var import_node_process10 = __toESM(require("process"), 1);
1340
+ var import_node_path8 = require("path");
1341
+ var import_node_process14 = __toESM(require("process"), 1);
1177
1342
 
1178
1343
  // src/config.ts
1179
1344
  var import_node_fs6 = require("fs");
1180
- var import_node_path5 = require("path");
1345
+ var import_node_path6 = require("path");
1181
1346
  var import_node_url = require("url");
1182
1347
  var import_apps = require("@odla-ai/apps");
1183
1348
 
@@ -1581,13 +1746,17 @@ var DEFAULT_ENVS = ["dev"];
1581
1746
  var DEFAULT_SERVICES = ["db", "ai"];
1582
1747
  var configImportSerial = 0;
1583
1748
  var GOOGLE_CALENDAR_EVENTS_SCOPE = "https://www.googleapis.com/auth/calendar.events";
1749
+ var stderr = { error: (message2) => {
1750
+ process.stderr.write(`${message2}
1751
+ `);
1752
+ } };
1584
1753
  async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1585
- const resolved = (0, import_node_path5.resolve)(configPath);
1754
+ const resolved = (0, import_node_path6.resolve)(configPath);
1586
1755
  if (!(0, import_node_fs6.existsSync)(resolved)) {
1587
1756
  throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
1588
1757
  }
1589
1758
  const raw = await loadConfigModule(resolved);
1590
- const rootDir = (0, import_node_path5.dirname)(resolved);
1759
+ const rootDir = (0, import_node_path6.dirname)(resolved);
1591
1760
  validateRawConfig(raw, resolved);
1592
1761
  const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
1593
1762
  const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
@@ -1597,11 +1766,14 @@ async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1597
1766
  validateCalendarConfig(raw, unique3([...envs, ...options.additionalEnvs ?? []]), services, resolved);
1598
1767
  validateMonitoringConfig(raw, unique3([...envs, ...options.additionalEnvs ?? []]), services, resolved);
1599
1768
  const local = {
1600
- tokenFile: (0, import_node_path5.resolve)(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
1601
- credentialsFile: (0, import_node_path5.resolve)(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
1602
- devVarsFile: (0, import_node_path5.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
1769
+ tokenFile: raw.local?.tokenFile ? (0, import_node_path6.resolve)(rootDir, raw.local.tokenFile) : appTokenFile(raw.app.id),
1770
+ credentialsFile: raw.local?.credentialsFile ? (0, import_node_path6.resolve)(rootDir, raw.local.credentialsFile) : appCredentialsFile(raw.app.id),
1771
+ devVarsFile: (0, import_node_path6.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
1603
1772
  gitignore: raw.local?.gitignore ?? true
1604
1773
  };
1774
+ adoptRepoLocalCache((0, import_node_path6.resolve)(rootDir, ".odla/dev-token.json"), local.tokenFile, stderr);
1775
+ adoptRepoLocalCache((0, import_node_path6.resolve)(rootDir, ".odla/credentials.local.json"), local.credentialsFile, stderr);
1776
+ (0, import_node_fs6.rmSync)((0, import_node_path6.resolve)(rootDir, ".odla/handshake.local.json"), { force: true });
1605
1777
  return {
1606
1778
  ...raw,
1607
1779
  configPath: resolved,
@@ -1616,7 +1788,7 @@ async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1616
1788
  async function resolveDataExport(cfg, value2, names) {
1617
1789
  if (value2 === void 0 || value2 === null || value2 === false) return void 0;
1618
1790
  if (typeof value2 !== "string") return value2;
1619
- const target = (0, import_node_path5.isAbsolute)(value2) ? value2 : (0, import_node_path5.resolve)(cfg.rootDir, value2);
1791
+ const target = (0, import_node_path6.isAbsolute)(value2) ? value2 : (0, import_node_path6.resolve)(cfg.rootDir, value2);
1620
1792
  if (target.endsWith(".json")) {
1621
1793
  return JSON.parse((0, import_node_fs6.readFileSync)(target, "utf8"));
1622
1794
  }
@@ -1710,17 +1882,17 @@ function unique3(values) {
1710
1882
 
1711
1883
  // src/operator-profiles.ts
1712
1884
  var import_node_fs7 = require("fs");
1713
- var import_node_os2 = require("os");
1714
- var import_node_path6 = require("path");
1715
- var import_node_process9 = __toESM(require("process"), 1);
1885
+ var import_node_os3 = require("os");
1886
+ var import_node_path7 = require("path");
1887
+ var import_node_process13 = __toESM(require("process"), 1);
1716
1888
  function operatorProfileFile() {
1717
- return (0, import_node_path6.resolve)(
1718
- clean(import_node_process9.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".odla", "contexts.json")
1889
+ return (0, import_node_path7.resolve)(
1890
+ clean(import_node_process13.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path7.join)((0, import_node_os3.homedir)(), ".odla", "contexts.json")
1719
1891
  );
1720
1892
  }
1721
1893
  function resolveOperatorProfile(parsed) {
1722
1894
  const fromFlag = clean(stringOpt(parsed.options.context));
1723
- const fromEnvironment = clean(import_node_process9.default.env.ODLA_CONTEXT);
1895
+ const fromEnvironment = clean(import_node_process13.default.env.ODLA_CONTEXT);
1724
1896
  const name = fromFlag ?? fromEnvironment ?? null;
1725
1897
  const file = operatorProfileFile();
1726
1898
  if (!name) {
@@ -1760,10 +1932,10 @@ function removeOperatorProfile(name, file = operatorProfileFile()) {
1760
1932
  return true;
1761
1933
  }
1762
1934
  function operatorCredentialFiles(selection) {
1763
- const base = selection.name ? (0, import_node_path6.join)((0, import_node_path6.dirname)(selection.file), "profiles", selection.name) : (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".odla");
1935
+ const base = selection.name ? (0, import_node_path7.join)((0, import_node_path7.dirname)(selection.file), "profiles", selection.name) : (0, import_node_path7.join)((0, import_node_os3.homedir)(), ".odla");
1764
1936
  return {
1765
- developer: (0, import_node_path6.join)(base, "dev-token.json"),
1766
- scoped: (0, import_node_path6.join)(base, "admin-token.local.json")
1937
+ developer: (0, import_node_path7.join)(base, "dev-token.json"),
1938
+ scoped: (0, import_node_path7.join)(base, "admin-token.local.json")
1767
1939
  };
1768
1940
  }
1769
1941
  function assertOperatorName(value2, label) {
@@ -1841,7 +2013,7 @@ var DEFAULT_PLATFORM2 = "https://odla.ai";
1841
2013
  async function resolveOperatorContext(parsed, options = {}) {
1842
2014
  const profile = resolveOperatorProfile(parsed);
1843
2015
  const configArgument = stringOpt(parsed.options.config) ?? "odla.config.mjs";
1844
- const configPath = (0, import_node_path7.resolve)(configArgument);
2016
+ const configPath = (0, import_node_path8.resolve)(configArgument);
1845
2017
  const explicitConfig = parsed.options.config !== void 0;
1846
2018
  const hasConfig = (0, import_node_fs8.existsSync)(configPath);
1847
2019
  if (!hasConfig && (!options.allowMissingConfig || explicitConfig)) {
@@ -1849,13 +2021,13 @@ async function resolveOperatorContext(parsed, options = {}) {
1849
2021
  }
1850
2022
  const loaded = hasConfig ? await loadProjectConfig(configArgument) : void 0;
1851
2023
  const platformFlag = clean2(stringOpt(parsed.options.platform));
1852
- const platformEnvironment = clean2(import_node_process10.default.env.ODLA_PLATFORM_URL);
2024
+ const platformEnvironment = clean2(import_node_process14.default.env.ODLA_PLATFORM_URL);
1853
2025
  const platformValue = platformAudience(
1854
2026
  platformFlag ?? platformEnvironment ?? profile.value?.platform ?? loaded?.platformUrl ?? DEFAULT_PLATFORM2
1855
2027
  );
1856
2028
  const platformSource = platformFlag ? "flag" : platformEnvironment ? "environment" : profile.value ? "profile" : loaded ? "config" : "default";
1857
2029
  const appFlag = clean2(stringOpt(parsed.options.app));
1858
- const appEnvironment = clean2(import_node_process10.default.env.ODLA_APP_ID);
2030
+ const appEnvironment = clean2(import_node_process14.default.env.ODLA_APP_ID);
1859
2031
  const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
1860
2032
  const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
1861
2033
  if (appValue) {
@@ -1869,16 +2041,16 @@ async function resolveOperatorContext(parsed, options = {}) {
1869
2041
  );
1870
2042
  }
1871
2043
  const envFlag = clean2(stringOpt(parsed.options.env));
1872
- const envEnvironment = clean2(import_node_process10.default.env.ODLA_ENV);
2044
+ const envEnvironment = clean2(import_node_process14.default.env.ODLA_ENV);
1873
2045
  const environmentValue = envFlag ?? envEnvironment ?? profile.value?.environment ?? options.defaultEnvironment ?? null;
1874
2046
  const environmentSource = envFlag ? "flag" : envEnvironment ? "environment" : profile.value?.environment ? "profile" : options.defaultEnvironment ? "default" : "unresolved";
1875
2047
  if (environmentValue) {
1876
2048
  assertOperatorName(environmentValue, "environment");
1877
2049
  }
1878
- const rootDir = loaded?.rootDir ?? import_node_process10.default.cwd();
2050
+ const rootDir = loaded?.rootDir ?? import_node_process14.default.cwd();
1879
2051
  const profileCredentials = operatorCredentialFiles(profile);
1880
- const tokenFile = clean2(import_node_process10.default.env.ODLA_DEV_TOKEN_FILE) ? (0, import_node_path7.resolve)(import_node_process10.default.env.ODLA_DEV_TOKEN_FILE) : profile.name ? profileCredentials.developer : loaded?.local.tokenFile ?? profileCredentials.developer;
1881
- const scopedTokenFile = clean2(import_node_process10.default.env.ODLA_ADMIN_TOKEN_FILE) ? (0, import_node_path7.resolve)(import_node_process10.default.env.ODLA_ADMIN_TOKEN_FILE) : profile.name ? profileCredentials.scoped : loaded ? (0, import_node_path7.join)(loaded.rootDir, ".odla", "admin-token.local.json") : profileCredentials.scoped;
2052
+ const tokenFile = clean2(import_node_process14.default.env.ODLA_DEV_TOKEN_FILE) ? (0, import_node_path8.resolve)(import_node_process14.default.env.ODLA_DEV_TOKEN_FILE) : profile.name ? profileCredentials.developer : loaded?.local.tokenFile ?? profileCredentials.developer;
2053
+ const scopedTokenFile2 = clean2(import_node_process14.default.env.ODLA_ADMIN_TOKEN_FILE) ? (0, import_node_path8.resolve)(import_node_process14.default.env.ODLA_ADMIN_TOKEN_FILE) : profile.name ? profileCredentials.scoped : loaded ? (0, import_node_path8.join)(loaded.rootDir, ".odla", "admin-token.local.json") : profileCredentials.scoped;
1882
2054
  const cfg = loaded ? {
1883
2055
  ...loaded,
1884
2056
  platformUrl: platformValue,
@@ -1900,8 +2072,8 @@ async function resolveOperatorContext(parsed, options = {}) {
1900
2072
  services: [],
1901
2073
  local: {
1902
2074
  tokenFile,
1903
- credentialsFile: (0, import_node_path7.join)(rootDir, ".odla", "credentials.local.json"),
1904
- devVarsFile: (0, import_node_path7.join)(rootDir, ".dev.vars"),
2075
+ credentialsFile: (0, import_node_path8.join)(rootDir, ".odla", "credentials.local.json"),
2076
+ devVarsFile: (0, import_node_path8.join)(rootDir, ".dev.vars"),
1905
2077
  gitignore: true
1906
2078
  }
1907
2079
  };
@@ -1925,7 +2097,7 @@ async function resolveOperatorContext(parsed, options = {}) {
1925
2097
  },
1926
2098
  credentials: {
1927
2099
  developerTokenFile: tokenFile,
1928
- scopedTokenFile
2100
+ scopedTokenFile: scopedTokenFile2
1929
2101
  }
1930
2102
  };
1931
2103
  }
@@ -2023,7 +2195,7 @@ async function adminCommand(parsed, deps = {}) {
2023
2195
  }
2024
2196
 
2025
2197
  // src/auth-command.ts
2026
- var import_node_process11 = __toESM(require("process"), 1);
2198
+ var import_node_process15 = __toESM(require("process"), 1);
2027
2199
 
2028
2200
  // src/whoami-command.ts
2029
2201
  var text2 = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
@@ -2159,6 +2331,7 @@ async function whoamiCommand(parsed, deps = {}) {
2159
2331
  } else {
2160
2332
  out.log("projects: (none \u2014 every pm and discuss call will be refused)");
2161
2333
  }
2334
+ printMachineBlock(cfg.platformUrl, out);
2162
2335
  if (!identity.admin) {
2163
2336
  if (identity.scopes.includes("platform:runbook:write")) {
2164
2337
  out.log("\nThis exact scope can read and edit all platform runbook content.");
@@ -2169,6 +2342,21 @@ async function whoamiCommand(parsed, deps = {}) {
2169
2342
  }
2170
2343
  }
2171
2344
  }
2345
+ function printMachineBlock(platformUrl, out) {
2346
+ const state2 = machineAuthState(platformAudience(platformUrl));
2347
+ if (!state2.enrolled) {
2348
+ out.log("\nmachine: not enrolled \u2014 every privileged command needs its own browser approval.");
2349
+ out.log(` End that with:
2350
+ ${ENROL_EVERYTHING}`);
2351
+ return;
2352
+ }
2353
+ const reach = state2.appIds?.includes("*") ? "every app you own" : state2.appIds?.join(", ");
2354
+ out.log(`
2355
+ machine: enrolled${state2.deviceName ? ` as "${state2.deviceName}"` : ""}${reach ? ` for ${reach}` : ""}`);
2356
+ if (state2.scopes?.length) out.log(` carrying ${state2.scopes.join(", ")}`);
2357
+ const lapse = lapseNotice(state2);
2358
+ if (lapse) out.log(` ${lapse}`);
2359
+ }
2172
2360
 
2173
2361
  // src/auth-command.ts
2174
2362
  async function authCommand(parsed, deps = {}) {
@@ -2193,12 +2381,17 @@ async function authCommand(parsed, deps = {}) {
2193
2381
  const { cfg } = context;
2194
2382
  const out = deps.stdout ?? console;
2195
2383
  const doFetch = deps.fetch ?? fetch;
2196
- const email = stringOpt(parsed.options.email) ?? import_node_process11.default.env.ODLA_USER_EMAIL?.trim();
2384
+ const email = stringOpt(parsed.options.email) ?? import_node_process15.default.env.ODLA_USER_EMAIL?.trim();
2197
2385
  if (!email) {
2198
2386
  throw new Error(
2199
2387
  "auth login requires --email <odla-account> or ODLA_USER_EMAIL; confirm the signed-in odla email instead of using git or GitHub identity"
2200
2388
  );
2201
2389
  }
2390
+ if (!machineAuthState(platformAudience(cfg.platformUrl)).enrolled) {
2391
+ out.error("odla: this machine is not enrolled, so this approval buys one project until it lapses.");
2392
+ out.error(` For one approval that covers every app you own, with no repeats:
2393
+ ${ENROL_EVERYTHING}`);
2394
+ }
2202
2395
  const token = await getDeveloperToken(
2203
2396
  cfg,
2204
2397
  {
@@ -2554,7 +2747,7 @@ async function appCommand(parsed, dependencies = {}) {
2554
2747
 
2555
2748
  // src/brand-command.ts
2556
2749
  var import_promises = require("fs/promises");
2557
- var import_node_path8 = require("path");
2750
+ var import_node_path9 = require("path");
2558
2751
 
2559
2752
  // src/brand-design-unpack.ts
2560
2753
  var import_node_zlib = require("zlib");
@@ -2656,15 +2849,15 @@ function describeUnpack(result, outDir) {
2656
2849
  // src/brand-command.ts
2657
2850
  var USAGE = "usage: odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]";
2658
2851
  async function readBundle(source, deps) {
2659
- if (source !== "-") return (0, import_promises.readFile)((0, import_node_path8.resolve)(source), "utf8");
2852
+ if (source !== "-") return (0, import_promises.readFile)((0, import_node_path9.resolve)(source), "utf8");
2660
2853
  const readStdin = deps.readStdin;
2661
2854
  if (!readStdin) throw new Error("reading a bundle from stdin is not supported here");
2662
2855
  return readStdin();
2663
2856
  }
2664
2857
  async function writeAll(result, outDir) {
2665
2858
  for (const file of result.files) {
2666
- const target = (0, import_node_path8.resolve)(outDir, file.path);
2667
- await (0, import_promises.mkdir)((0, import_node_path8.dirname)(target), { recursive: true });
2859
+ const target = (0, import_node_path9.resolve)(outDir, file.path);
2860
+ await (0, import_promises.mkdir)((0, import_node_path9.dirname)(target), { recursive: true });
2668
2861
  await (0, import_promises.writeFile)(target, file.bytes);
2669
2862
  }
2670
2863
  }
@@ -2672,7 +2865,7 @@ async function designUnpack(parsed, deps) {
2672
2865
  assertArgs(parsed, ["out", "json"], 4);
2673
2866
  const source = parsed.positionals[3];
2674
2867
  if (!source) throw new Error(USAGE);
2675
- const outDir = (0, import_node_path8.resolve)(stringOpt(parsed.options.out) ?? "design");
2868
+ const outDir = (0, import_node_path9.resolve)(stringOpt(parsed.options.out) ?? "design");
2676
2869
  const result = unpackDesign(await readBundle(source, deps));
2677
2870
  await writeAll(result, outDir);
2678
2871
  const out = deps.stdout ?? console;
@@ -3277,7 +3470,7 @@ async function safeText4(response2) {
3277
3470
 
3278
3471
  // src/config-operation-command.ts
3279
3472
  var import_apps6 = require("@odla-ai/apps");
3280
- var import_node_path9 = require("path");
3473
+ var import_node_path10 = require("path");
3281
3474
 
3282
3475
  // src/version.ts
3283
3476
  var import_node_fs10 = require("fs");
@@ -3707,7 +3900,7 @@ async function operationClient(cfg, options, purpose) {
3707
3900
  platform: cfg.platformUrl,
3708
3901
  scope: "app:config:write",
3709
3902
  token: options.token,
3710
- tokenFile: (0, import_node_path9.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
3903
+ tokenFile: (0, import_node_path10.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
3711
3904
  rootDir: cfg.rootDir,
3712
3905
  email: options.email,
3713
3906
  open: options.open,
@@ -3762,7 +3955,7 @@ function record4(value2) {
3762
3955
 
3763
3956
  // src/config-reconcile-command.ts
3764
3957
  var import_apps8 = require("@odla-ai/apps");
3765
- var import_node_path10 = require("path");
3958
+ var import_node_path11 = require("path");
3766
3959
 
3767
3960
  // src/config-reconcile.ts
3768
3961
  var import_apps7 = require("@odla-ai/apps");
@@ -4058,7 +4251,7 @@ async function inspectConfig(options) {
4058
4251
  platform: cfg.platformUrl,
4059
4252
  scope: "app:config:read",
4060
4253
  token: options.token,
4061
- tokenFile: (0, import_node_path10.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
4254
+ tokenFile: (0, import_node_path11.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
4062
4255
  rootDir: cfg.rootDir,
4063
4256
  email: options.email,
4064
4257
  open: options.open,
@@ -4191,26 +4384,26 @@ function quoteArg2(value2) {
4191
4384
  // src/doctor-checks.ts
4192
4385
  var import_node_child_process3 = require("child_process");
4193
4386
  var import_node_fs13 = require("fs");
4194
- var import_node_path12 = require("path");
4387
+ var import_node_path13 = require("path");
4195
4388
 
4196
4389
  // src/wrangler.ts
4197
4390
  var import_node_child_process2 = require("child_process");
4198
4391
  var import_node_fs12 = require("fs");
4199
- var import_node_path11 = require("path");
4392
+ var import_node_path12 = require("path");
4200
4393
  var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
4201
4394
  const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
4202
4395
  let stdout = "";
4203
- let stderr = "";
4396
+ let stderr2 = "";
4204
4397
  child.stdout.on("data", (chunk) => stdout += chunk.toString());
4205
- child.stderr.on("data", (chunk) => stderr += chunk.toString());
4398
+ child.stderr.on("data", (chunk) => stderr2 += chunk.toString());
4206
4399
  child.on("error", reject);
4207
- child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout, stderr }));
4400
+ child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout, stderr: stderr2 }));
4208
4401
  child.stdin.end(opts?.input ?? "");
4209
4402
  });
4210
4403
  var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
4211
4404
  function findWranglerConfig(rootDir) {
4212
4405
  for (const name of WRANGLER_CONFIG_FILES) {
4213
- const path = (0, import_node_path11.join)(rootDir, name);
4406
+ const path = (0, import_node_path12.join)(rootDir, name);
4214
4407
  if ((0, import_node_fs12.existsSync)(path)) return path;
4215
4408
  }
4216
4409
  return null;
@@ -4361,21 +4554,21 @@ function wranglerWarnings(rootDir) {
4361
4554
  const blocks = [{ label: "", block: config }];
4362
4555
  const envs = config.env;
4363
4556
  if (envs && typeof envs === "object") {
4364
- for (const [name, block] of Object.entries(envs)) {
4365
- if (block && typeof block === "object") blocks.push({ label: `env.${name}.`, block });
4557
+ for (const [name, block2] of Object.entries(envs)) {
4558
+ if (block2 && typeof block2 === "object") blocks.push({ label: `env.${name}.`, block: block2 });
4366
4559
  }
4367
4560
  }
4368
- for (const { label, block } of blocks) {
4369
- const assets = block.assets;
4561
+ for (const { label, block: block2 } of blocks) {
4562
+ const assets = block2.assets;
4370
4563
  if (assets?.directory) {
4371
- const dir = (0, import_node_path12.resolve)(rootDir, assets.directory);
4372
- if (dir === (0, import_node_path12.resolve)(rootDir)) {
4564
+ const dir = (0, import_node_path13.resolve)(rootDir, assets.directory);
4565
+ if (dir === (0, import_node_path13.resolve)(rootDir)) {
4373
4566
  warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
4374
- } else if ((0, import_node_fs13.existsSync)((0, import_node_path12.join)(dir, "node_modules"))) {
4567
+ } else if ((0, import_node_fs13.existsSync)((0, import_node_path13.join)(dir, "node_modules"))) {
4375
4568
  warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
4376
4569
  }
4377
4570
  }
4378
- const vars = block.vars;
4571
+ const vars = block2.vars;
4379
4572
  if (vars && typeof vars === "object") {
4380
4573
  for (const [name, value2] of Object.entries(vars)) {
4381
4574
  if (name === "ODLA_API_KEY" || name === "ODLA_O11Y_TOKEN" || typeof value2 === "string" && looksSecret(value2)) {
@@ -4406,7 +4599,7 @@ function o11yProjectWarnings(rootDir) {
4406
4599
  warnings.push("cannot verify o11y Worker instrumentation \u2014 add a parseable wrangler.jsonc/json config");
4407
4600
  return warnings;
4408
4601
  }
4409
- const main = typeof config.main === "string" ? (0, import_node_path12.resolve)(rootDir, config.main) : null;
4602
+ const main = typeof config.main === "string" ? (0, import_node_path13.resolve)(rootDir, config.main) : null;
4410
4603
  if (!main || !(0, import_node_fs13.existsSync)(main)) {
4411
4604
  warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
4412
4605
  } else {
@@ -4436,7 +4629,7 @@ function calendarProjectWarnings(rootDir) {
4436
4629
  }
4437
4630
  function readPackageJson(rootDir) {
4438
4631
  try {
4439
- return JSON.parse((0, import_node_fs13.readFileSync)((0, import_node_path12.join)(rootDir, "package.json"), "utf8"));
4632
+ return JSON.parse((0, import_node_fs13.readFileSync)((0, import_node_path13.join)(rootDir, "package.json"), "utf8"));
4440
4633
  } catch {
4441
4634
  return null;
4442
4635
  }
@@ -4731,12 +4924,12 @@ function harnessOption(value2, flag) {
4731
4924
 
4732
4925
  // src/init.ts
4733
4926
  var import_node_fs14 = require("fs");
4734
- var import_node_path13 = require("path");
4927
+ var import_node_path14 = require("path");
4735
4928
  var import_apps9 = require("@odla-ai/apps");
4736
4929
  function initProject(options) {
4737
4930
  const out = options.stdout ?? console;
4738
- const rootDir = (0, import_node_path13.resolve)(options.rootDir ?? process.cwd());
4739
- const configPath = (0, import_node_path13.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
4931
+ const rootDir = (0, import_node_path14.resolve)(options.rootDir ?? process.cwd());
4932
+ const configPath = (0, import_node_path14.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
4740
4933
  if ((0, import_node_fs14.existsSync)(configPath) && !options.force) {
4741
4934
  throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
4742
4935
  }
@@ -4753,12 +4946,12 @@ function initProject(options) {
4753
4946
  }
4754
4947
  }
4755
4948
  const aiProvider = options.aiProvider;
4756
- (0, import_node_fs14.mkdirSync)((0, import_node_path13.dirname)(configPath), { recursive: true });
4757
- (0, import_node_fs14.mkdirSync)((0, import_node_path13.resolve)(rootDir, "src/odla"), { recursive: true });
4758
- (0, import_node_fs14.mkdirSync)((0, import_node_path13.resolve)(rootDir, ".odla"), { recursive: true });
4949
+ (0, import_node_fs14.mkdirSync)((0, import_node_path14.dirname)(configPath), { recursive: true });
4950
+ (0, import_node_fs14.mkdirSync)((0, import_node_path14.resolve)(rootDir, "src/odla"), { recursive: true });
4951
+ (0, import_node_fs14.mkdirSync)((0, import_node_path14.resolve)(rootDir, ".odla"), { recursive: true });
4759
4952
  (0, import_node_fs14.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
4760
- writeIfMissing((0, import_node_path13.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
4761
- writeIfMissing((0, import_node_path13.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
4953
+ writeIfMissing((0, import_node_path14.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
4954
+ writeIfMissing((0, import_node_path14.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
4762
4955
  ensureGitignore(rootDir);
4763
4956
  out.log(`created ${relativeDisplay(configPath, rootDir)}`);
4764
4957
  out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
@@ -4825,8 +5018,10 @@ ${calendar}
4825
5018
  // prod: "https://example.com",
4826
5019
  },
4827
5020
  local: {
4828
- tokenFile: ".odla/dev-token.json",
4829
- credentialsFile: ".odla/credentials.local.json",
5021
+ // Credentials live in ~/.odla, per machine, so every worktree of this app
5022
+ // shares one approval instead of asking for its own. Pinning tokenFile or
5023
+ // credentialsFile here still works and still overrides that \u2014 it just puts
5024
+ // this checkout back on its own island.
4830
5025
  devVarsFile: ".dev.vars",
4831
5026
  },
4832
5027
  };
@@ -5063,8 +5258,8 @@ function printReport(report5, out) {
5063
5258
 
5064
5259
  // src/skill.ts
5065
5260
  var import_node_fs15 = require("fs");
5066
- var import_node_os3 = require("os");
5067
- var import_node_path14 = require("path");
5261
+ var import_node_os4 = require("os");
5262
+ var import_node_path15 = require("path");
5068
5263
  var import_node_url2 = require("url");
5069
5264
 
5070
5265
  // src/skill-adapters.ts
@@ -5163,8 +5358,8 @@ function installSkill(options = {}) {
5163
5358
  const files = listFiles(sourceDir);
5164
5359
  if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
5165
5360
  const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
5166
- const root = (0, import_node_path14.resolve)(options.dir ?? process.cwd());
5167
- const home = (0, import_node_path14.resolve)(options.homeDir ?? (0, import_node_os3.homedir)());
5361
+ const root = (0, import_node_path15.resolve)(options.dir ?? process.cwd());
5362
+ const home = (0, import_node_path15.resolve)(options.homeDir ?? (0, import_node_os4.homedir)());
5168
5363
  const plans = /* @__PURE__ */ new Map();
5169
5364
  const targets = /* @__PURE__ */ new Map();
5170
5365
  const rememberTarget = (harness, target) => {
@@ -5178,48 +5373,48 @@ function installSkill(options = {}) {
5178
5373
  plans.set(target, { target, content: content2, boundary, managedMerge });
5179
5374
  };
5180
5375
  const planSkillTree = (targetDir2, boundary = root) => {
5181
- for (const rel of files) plan((0, import_node_path14.join)(targetDir2, rel), (0, import_node_fs15.readFileSync)((0, import_node_path14.join)(sourceDir, rel), "utf8"), false, boundary);
5376
+ for (const rel of files) plan((0, import_node_path15.join)(targetDir2, rel), (0, import_node_fs15.readFileSync)((0, import_node_path15.join)(sourceDir, rel), "utf8"), false, boundary);
5182
5377
  };
5183
5378
  let targetDir;
5184
5379
  if (options.global) {
5185
- const claudeRoot = (0, import_node_path14.join)(home, ".claude", "skills");
5186
- const codexRoot = (0, import_node_path14.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path14.join)(home, ".codex"), "skills");
5380
+ const claudeRoot = (0, import_node_path15.join)(home, ".claude", "skills");
5381
+ const codexRoot = (0, import_node_path15.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path15.join)(home, ".codex"), "skills");
5187
5382
  targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
5188
5383
  for (const harness of harnesses) {
5189
5384
  const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
5190
- planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path14.dirname)((0, import_node_path14.dirname)(codexRoot)));
5385
+ planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path15.dirname)((0, import_node_path15.dirname)(codexRoot)));
5191
5386
  rememberTarget(harness, skillRoot);
5192
5387
  }
5193
5388
  } else {
5194
- const sharedRoot = (0, import_node_path14.join)(root, ".agents", "skills");
5389
+ const sharedRoot = (0, import_node_path15.join)(root, ".agents", "skills");
5195
5390
  planSkillTree(sharedRoot);
5196
- const claudeRoot = (0, import_node_path14.join)(root, ".claude", "skills");
5391
+ const claudeRoot = (0, import_node_path15.join)(root, ".claude", "skills");
5197
5392
  targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
5198
5393
  for (const harness of harnesses) rememberTarget(harness, sharedRoot);
5199
5394
  if (harnesses.includes("claude")) {
5200
5395
  for (const skill of skillNames(files)) {
5201
- const canonical2 = (0, import_node_fs15.readFileSync)((0, import_node_path14.join)(sourceDir, skill, "SKILL.md"), "utf8");
5202
- plan((0, import_node_path14.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical2));
5396
+ const canonical2 = (0, import_node_fs15.readFileSync)((0, import_node_path15.join)(sourceDir, skill, "SKILL.md"), "utf8");
5397
+ plan((0, import_node_path15.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical2));
5203
5398
  }
5204
5399
  rememberTarget("claude", claudeRoot);
5205
5400
  }
5206
5401
  if (harnesses.includes("cursor")) {
5207
- const cursorRule = (0, import_node_path14.join)(root, ".cursor", "rules", "odla.mdc");
5402
+ const cursorRule = (0, import_node_path15.join)(root, ".cursor", "rules", "odla.mdc");
5208
5403
  plan(cursorRule, CURSOR_RULE);
5209
5404
  rememberTarget("cursor", cursorRule);
5210
5405
  }
5211
5406
  if (harnesses.includes("agents")) {
5212
- const agentsFile = (0, import_node_path14.join)(root, "AGENTS.md");
5407
+ const agentsFile = (0, import_node_path15.join)(root, "AGENTS.md");
5213
5408
  plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
5214
5409
  rememberTarget("agents", agentsFile);
5215
5410
  }
5216
5411
  if (harnesses.includes("copilot")) {
5217
- const copilotFile = (0, import_node_path14.join)(root, ".github", "copilot-instructions.md");
5412
+ const copilotFile = (0, import_node_path15.join)(root, ".github", "copilot-instructions.md");
5218
5413
  plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
5219
5414
  rememberTarget("copilot", copilotFile);
5220
5415
  }
5221
5416
  if (harnesses.includes("gemini")) {
5222
- const geminiFile = (0, import_node_path14.join)(root, "GEMINI.md");
5417
+ const geminiFile = (0, import_node_path15.join)(root, "GEMINI.md");
5223
5418
  plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
5224
5419
  rememberTarget("gemini", geminiFile);
5225
5420
  }
@@ -5255,7 +5450,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
5255
5450
  }
5256
5451
  for (const file of plans.values()) {
5257
5452
  if (!(0, import_node_fs15.existsSync)(file.target) || (0, import_node_fs15.readFileSync)(file.target, "utf8") !== file.content) {
5258
- (0, import_node_fs15.mkdirSync)((0, import_node_path14.dirname)(file.target), { recursive: true });
5453
+ (0, import_node_fs15.mkdirSync)((0, import_node_path15.dirname)(file.target), { recursive: true });
5259
5454
  (0, import_node_fs15.writeFileSync)(file.target, file.content);
5260
5455
  }
5261
5456
  }
@@ -5275,7 +5470,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
5275
5470
  };
5276
5471
  }
5277
5472
  function pathsUnder(root, paths) {
5278
- return [...paths].map((path) => (0, import_node_path14.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path14.sep}`) && !(0, import_node_path14.isAbsolute)(path)).sort();
5473
+ return [...paths].map((path) => (0, import_node_path15.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path15.sep}`) && !(0, import_node_path15.isAbsolute)(path)).sort();
5279
5474
  }
5280
5475
  function normalizeHarnesses(values, global) {
5281
5476
  const requested = values?.length ? values : ["claude"];
@@ -5294,10 +5489,10 @@ function normalizeHarnesses(values, global) {
5294
5489
  }
5295
5490
  return expanded;
5296
5491
  }
5297
- function managedFileContent(path, block, force, boundary) {
5492
+ function managedFileContent(path, block2, force, boundary) {
5298
5493
  const symlink = symlinkedComponent(boundary, path);
5299
5494
  if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
5300
- if (!(0, import_node_fs15.existsSync)(path)) return `${block}
5495
+ if (!(0, import_node_fs15.existsSync)(path)) return `${block2}
5301
5496
  `;
5302
5497
  const current = (0, import_node_fs15.readFileSync)(path, "utf8");
5303
5498
  const start = "<!-- odla-ai agent setup:start -->";
@@ -5309,24 +5504,24 @@ function managedFileContent(path, block, force, boundary) {
5309
5504
  }
5310
5505
  if (startAt === -1) {
5311
5506
  const separator = current.length === 0 || current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n";
5312
- return `${current}${separator}${block}
5507
+ return `${current}${separator}${block2}
5313
5508
  `;
5314
5509
  }
5315
5510
  const afterEnd = endAt + end.length;
5316
5511
  const existing = current.slice(startAt, afterEnd);
5317
- if (existing !== block && !force) {
5512
+ if (existing !== block2 && !force) {
5318
5513
  throw new Error(`odla-managed section modified locally in ${path}; re-run with --force to replace that section`);
5319
5514
  }
5320
- return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
5515
+ return `${current.slice(0, startAt)}${block2}${current.slice(afterEnd)}`;
5321
5516
  }
5322
5517
  function symlinkedComponent(boundary, target) {
5323
- const rel = (0, import_node_path14.relative)(boundary, target);
5324
- if (rel === ".." || rel.startsWith(`..${import_node_path14.sep}`) || (0, import_node_path14.isAbsolute)(rel)) {
5518
+ const rel = (0, import_node_path15.relative)(boundary, target);
5519
+ if (rel === ".." || rel.startsWith(`..${import_node_path15.sep}`) || (0, import_node_path15.isAbsolute)(rel)) {
5325
5520
  throw new Error(`agent setup target escapes its install root: ${target}`);
5326
5521
  }
5327
5522
  let current = boundary;
5328
- for (const part of rel.split(import_node_path14.sep).filter(Boolean)) {
5329
- current = (0, import_node_path14.join)(current, part);
5523
+ for (const part of rel.split(import_node_path15.sep).filter(Boolean)) {
5524
+ current = (0, import_node_path15.join)(current, part);
5330
5525
  try {
5331
5526
  if ((0, import_node_fs15.lstatSync)(current).isSymbolicLink()) return current;
5332
5527
  } catch (error) {
@@ -5343,9 +5538,9 @@ function listFiles(dir) {
5343
5538
  const results = [];
5344
5539
  const walk = (current) => {
5345
5540
  for (const entry of (0, import_node_fs15.readdirSync)(current, { withFileTypes: true })) {
5346
- const path = (0, import_node_path14.join)(current, entry.name);
5541
+ const path = (0, import_node_path15.join)(current, entry.name);
5347
5542
  if (entry.isDirectory()) walk(path);
5348
- else results.push((0, import_node_path14.relative)(dir, path));
5543
+ else results.push((0, import_node_path15.relative)(dir, path));
5349
5544
  }
5350
5545
  };
5351
5546
  walk(dir);
@@ -5701,8 +5896,8 @@ async function projectCommand(command, parsed, deps) {
5701
5896
 
5702
5897
  // src/code-connect.ts
5703
5898
  var import_node_fs16 = require("fs");
5704
- var import_node_os4 = require("os");
5705
- var import_node_path15 = require("path");
5899
+ var import_node_os5 = require("os");
5900
+ var import_node_path16 = require("path");
5706
5901
 
5707
5902
  // ../harness/dist/chunk-LNQNFGQC.js
5708
5903
  var HARNESS_PROTOCOL_VERSION = 1;
@@ -5808,7 +6003,7 @@ function allowedWorkspacePath(relativePath) {
5808
6003
  async function gitOutput(cwd, args, maxBytes) {
5809
6004
  const child = (0, import_child_process2.spawn)("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], shell: false });
5810
6005
  const stdout = [];
5811
- const stderr = [];
6006
+ const stderr2 = [];
5812
6007
  let bytes = 0;
5813
6008
  child.stdout.on("data", (chunk) => {
5814
6009
  bytes += chunk.byteLength;
@@ -5816,20 +6011,20 @@ async function gitOutput(cwd, args, maxBytes) {
5816
6011
  else stdout.push(chunk);
5817
6012
  });
5818
6013
  child.stderr.on("data", (chunk) => {
5819
- if (stderr.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr.push(chunk);
6014
+ if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
5820
6015
  });
5821
6016
  const code = await new Promise((accept, reject) => {
5822
6017
  child.once("error", reject);
5823
6018
  child.once("exit", accept);
5824
6019
  });
5825
6020
  if (bytes > maxBytes) throw new Error(`git output exceeds ${maxBytes} bytes`);
5826
- if (code !== 0) throw new Error(`git command failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
6021
+ if (code !== 0) throw new Error(`git command failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
5827
6022
  return Buffer.concat(stdout);
5828
6023
  }
5829
6024
  async function gitBlobs(cwd, entries, maxBytes) {
5830
6025
  const child = (0, import_child_process2.spawn)("git", ["cat-file", "--batch"], { cwd, stdio: ["pipe", "pipe", "pipe"], shell: false });
5831
6026
  const stdout = [];
5832
- const stderr = [];
6027
+ const stderr2 = [];
5833
6028
  let bytes = 0;
5834
6029
  child.stdout.on("data", (chunk) => {
5835
6030
  bytes += chunk.byteLength;
@@ -5837,7 +6032,7 @@ async function gitBlobs(cwd, entries, maxBytes) {
5837
6032
  else stdout.push(chunk);
5838
6033
  });
5839
6034
  child.stderr.on("data", (chunk) => {
5840
- if (stderr.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr.push(chunk);
6035
+ if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
5841
6036
  });
5842
6037
  child.stdin.end(`${entries.map((entry) => entry.hash).join("\n")}
5843
6038
  `);
@@ -5846,7 +6041,7 @@ async function gitBlobs(cwd, entries, maxBytes) {
5846
6041
  child.once("exit", accept);
5847
6042
  });
5848
6043
  if (bytes > maxBytes + entries.length * 100) throw new Error(`Git tree exceeds ${maxBytes} bytes`);
5849
- if (code !== 0) throw new Error(`git object read failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
6044
+ if (code !== 0) throw new Error(`git object read failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
5850
6045
  const output = Buffer.concat(stdout);
5851
6046
  const blobs = [];
5852
6047
  let offset = 0;
@@ -5943,7 +6138,7 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
5943
6138
  shell: false
5944
6139
  });
5945
6140
  const stdout = [];
5946
- const stderr = [];
6141
+ const stderr2 = [];
5947
6142
  let outputBytes = 0;
5948
6143
  child.stdout.on("data", (chunk) => {
5949
6144
  outputBytes += chunk.byteLength;
@@ -5951,14 +6146,14 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
5951
6146
  else stdout.push(chunk);
5952
6147
  });
5953
6148
  child.stderr.on("data", (chunk) => {
5954
- if (stderr.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr.push(chunk);
6149
+ if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
5955
6150
  });
5956
6151
  const code = await new Promise((accept, reject) => {
5957
6152
  child.once("error", reject);
5958
6153
  child.once("exit", accept);
5959
6154
  });
5960
6155
  if (outputBytes > 8 * 1024 * 1024) throw new Error("git file inventory exceeds 8 MiB");
5961
- if (code !== 0) throw new Error(`git file inventory failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
6156
+ if (code !== 0) throw new Error(`git file inventory failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
5962
6157
  const paths = Buffer.concat(stdout).toString("utf8").split("\0").filter(Boolean).sort();
5963
6158
  if (paths.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
5964
6159
  const root = (0, import_path4.resolve)(sourceDir);
@@ -6003,7 +6198,7 @@ async function captureGitDiff(root, maxBytes) {
6003
6198
  "workspace"
6004
6199
  ], { cwd: root, stdio: ["ignore", "pipe", "pipe"], shell: false });
6005
6200
  const stdout = [];
6006
- const stderr = [];
6201
+ const stderr2 = [];
6007
6202
  let bytes = 0;
6008
6203
  child.stdout.on("data", (chunk) => {
6009
6204
  bytes += chunk.byteLength;
@@ -6011,7 +6206,7 @@ async function captureGitDiff(root, maxBytes) {
6011
6206
  else stdout.push(chunk);
6012
6207
  });
6013
6208
  child.stderr.on("data", (chunk) => {
6014
- if (stderr.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr.push(chunk);
6209
+ if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
6015
6210
  });
6016
6211
  const code = await new Promise((accept, reject) => {
6017
6212
  child.once("error", reject);
@@ -6019,7 +6214,7 @@ async function captureGitDiff(root, maxBytes) {
6019
6214
  });
6020
6215
  if (bytes > maxBytes) throw new Error(`patch exceeds ${maxBytes} bytes`);
6021
6216
  if (code !== 0 && code !== 1) {
6022
- throw new Error(`git diff failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
6217
+ throw new Error(`git diff failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
6023
6218
  }
6024
6219
  return Buffer.concat(stdout).toString("utf8").replaceAll("a/baseline/", "a/").replaceAll("a/workspace/", "a/").replaceAll("b/baseline/", "b/").replaceAll("b/workspace/", "b/").replaceAll("--- a/baseline", "--- a").replaceAll("+++ b/workspace", "+++ b");
6025
6220
  }
@@ -6823,11 +7018,11 @@ function rollup(graph, kind, options = {}) {
6823
7018
  }
6824
7019
 
6825
7020
  // ../graph/dist/code/index.js
6826
- function dirname8(path) {
7021
+ function dirname9(path) {
6827
7022
  const at = path.lastIndexOf("/");
6828
7023
  return at <= 0 ? "." : path.slice(0, at);
6829
7024
  }
6830
- function join12(base, specifier) {
7025
+ function join13(base, specifier) {
6831
7026
  const parts = [];
6832
7027
  const segments = `${base === "." ? "" : `${base}/`}${specifier}`.split("/");
6833
7028
  for (const segment of segments) {
@@ -6851,7 +7046,7 @@ var BARE_IMPORT = /^\s*import\s*["']([^"']+)["']/gm;
6851
7046
  var isSourcePath = (path) => SOURCE.test(path);
6852
7047
  function resolveImport(fromPath, specifier, known) {
6853
7048
  if (!specifier.startsWith(".")) return null;
6854
- const base = join12(dirname8(fromPath), specifier);
7049
+ const base = join13(dirname9(fromPath), specifier);
6855
7050
  const candidates = [
6856
7051
  base,
6857
7052
  base.replace(/\.js$/, ".ts"),
@@ -7382,13 +7577,13 @@ function gitApply(cwd, patch2, check) {
7382
7577
  stdio: ["pipe", "ignore", "pipe"],
7383
7578
  env: { PATH: process.env.PATH ?? "", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null" }
7384
7579
  });
7385
- let stderr = "";
7580
+ let stderr2 = "";
7386
7581
  child.stderr.setEncoding("utf8");
7387
7582
  child.stderr.on("data", (text3) => {
7388
- if (stderr.length < 4e3) stderr += text3.slice(0, 4e3);
7583
+ if (stderr2.length < 4e3) stderr2 += text3.slice(0, 4e3);
7389
7584
  });
7390
7585
  child.once("error", reject);
7391
- child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(describePatchFailure(patch2, stderr.trim().slice(0, 500)))));
7586
+ child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(describePatchFailure(patch2, stderr2.trim().slice(0, 500)))));
7392
7587
  child.stdin.end(patch2);
7393
7588
  });
7394
7589
  }
@@ -7501,7 +7696,7 @@ function execute(engine, args, name, recipe2, signal) {
7501
7696
  const started = Date.now();
7502
7697
  const child = (0, import_child_process5.spawn)(engine, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
7503
7698
  const stdout = [];
7504
- const stderr = [];
7699
+ const stderr2 = [];
7505
7700
  let bytes = 0;
7506
7701
  let outputLimitExceeded = false;
7507
7702
  let timedOut = false;
@@ -7522,7 +7717,7 @@ function execute(engine, args, name, recipe2, signal) {
7522
7717
  else target.push(chunk);
7523
7718
  };
7524
7719
  child.stdout.on("data", collect(stdout));
7525
- child.stderr.on("data", collect(stderr));
7720
+ child.stderr.on("data", collect(stderr2));
7526
7721
  const abort = () => stop("abort");
7527
7722
  signal?.addEventListener("abort", abort, { once: true });
7528
7723
  if (signal?.aborted) abort();
@@ -7538,7 +7733,7 @@ function execute(engine, args, name, recipe2, signal) {
7538
7733
  accept({
7539
7734
  exitCode: code ?? 1,
7540
7735
  stdout: Buffer.concat(stdout).toString("utf8"),
7541
- stderr: Buffer.concat(stderr).toString("utf8"),
7736
+ stderr: Buffer.concat(stderr2).toString("utf8"),
7542
7737
  durationMs: Date.now() - started,
7543
7738
  outputLimitExceeded,
7544
7739
  timedOut
@@ -7690,11 +7885,11 @@ function checkedResult(result, maximumOutputBytes) {
7690
7885
  }
7691
7886
  function boundedLogs(result, maximum) {
7692
7887
  const stdout = Buffer.from(result.stdout);
7693
- const stderr = Buffer.from(result.stderr);
7888
+ const stderr2 = Buffer.from(result.stderr);
7694
7889
  const first = stdout.subarray(0, maximum);
7695
7890
  return {
7696
7891
  stdout: first.toString("utf8"),
7697
- stderr: stderr.subarray(0, Math.max(0, maximum - first.byteLength)).toString("utf8")
7892
+ stderr: stderr2.subarray(0, Math.max(0, maximum - first.byteLength)).toString("utf8")
7698
7893
  };
7699
7894
  }
7700
7895
  function digestPolicy(policy) {
@@ -9764,7 +9959,7 @@ var CODE_BUILD_RECIPES = Object.freeze([{
9764
9959
  // src/code-connect.ts
9765
9960
  async function codeConnect(options) {
9766
9961
  const cwd = options.cwd ?? process.cwd();
9767
- const configPath = (0, import_node_path15.resolve)(cwd, options.configPath);
9962
+ const configPath = (0, import_node_path16.resolve)(cwd, options.configPath);
9768
9963
  const cfg = (0, import_node_fs16.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
9769
9964
  const requestedAppId = options.appId?.trim();
9770
9965
  if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
@@ -9794,7 +9989,7 @@ async function codeConnect(options) {
9794
9989
  const doFetch = options.fetch ?? fetch;
9795
9990
  const engine = await (options.selectEngine ?? selectContainerEngine)(options.engine ?? "auto");
9796
9991
  const hostPlatform = process.platform === "darwin" ? "macos" : "linux";
9797
- const hostName = (options.name ?? (0, import_node_os4.hostname)()).trim();
9992
+ const hostName = (options.name ?? (0, import_node_os5.hostname)()).trim();
9798
9993
  if (!hostName || hostName.length > 120) throw new Error("--name must contain 1 to 120 characters");
9799
9994
  const repository = await inferGitHubRepository(cwd, options.readGitOrigin);
9800
9995
  const localSource = await (options.prepareLocalSource ?? prepareCodeLocalSource)(
@@ -9832,8 +10027,8 @@ async function codeConnect(options) {
9832
10027
  platform: hostPlatform,
9833
10028
  arch: process.arch,
9834
10029
  engines: [engine],
9835
- cpuCount: (0, import_node_os4.cpus)().length,
9836
- memoryBytes: (0, import_node_os4.totalmem)(),
10030
+ cpuCount: (0, import_node_os5.cpus)().length,
10031
+ memoryBytes: (0, import_node_os5.totalmem)(),
9837
10032
  source: descriptor2,
9838
10033
  images: {
9839
10034
  ready: true,
@@ -10200,13 +10395,13 @@ async function codeCommand(parsed, dependencies) {
10200
10395
  }
10201
10396
 
10202
10397
  // src/operator-credentials.ts
10203
- var import_node_process12 = __toESM(require("process"), 1);
10398
+ var import_node_process16 = __toESM(require("process"), 1);
10204
10399
  function developerTokenStatus(context, parsed, now = Date.now()) {
10205
10400
  const cached = readJsonFile(context.cfg.local.tokenFile);
10206
10401
  const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
10207
10402
  const source = clean3(
10208
10403
  stringOpt(parsed.options.token)
10209
- ) ? "flag" : clean3(import_node_process12.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
10404
+ ) ? "flag" : clean3(import_node_process16.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
10210
10405
  return {
10211
10406
  source,
10212
10407
  cacheFile: context.cfg.local.tokenFile,
@@ -10400,6 +10595,30 @@ async function credentialCommand(parsed, deps = {}) {
10400
10595
  }
10401
10596
 
10402
10597
  // src/help-usage.ts
10598
+ var AUTH_SECTION = `
10599
+ Enrol this machine once, then stop asking:
10600
+ npx odla-ai device enroll --no-open --wait 600
10601
+ One browser approval, and no flags to remember: this covers every app you
10602
+ own \u2014 including apps you create later \u2014 with every capability that
10603
+ approval can carry. Afterwards every worktree on this machine mints its
10604
+ own short-lived credentials with nobody's attention. The window rolls
10605
+ forward each time you use it, so continuous work never interrupts anyone;
10606
+ only a real gap does. Give the human the printed /studio?code= URL, keep
10607
+ the process alive, and wait on it.
10608
+ Narrow it deliberately with --app <id> or --capability <c>; the CLI then
10609
+ says what that gave up.
10610
+
10611
+ npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600
10612
+ The same thing across all of odla, for weeks. Needs a platform
10613
+ administrator's approval \u2014 an app owner's cannot carry platform scopes.
10614
+
10615
+ npx odla-ai whoami what this machine holds and when it lapses
10616
+ npx odla-ai device list every machine you have enrolled
10617
+
10618
+ Enrollment is the only human decision here. Revoking a machine, purging an app,
10619
+ transferring ownership, and rotating credentials still need a signed-in human in
10620
+ Studio, and no machine credential can do them however wide its approval was.
10621
+ `;
10403
10622
  var USAGE_SECTION = `
10404
10623
  Start here:
10405
10624
  odla-ai runbook ask "<question>" The current procedure, from odla's own
@@ -10438,7 +10657,7 @@ Usage:
10438
10657
  odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]
10439
10658
  odla-ai pm project list [--app <product-id>] [--status <s>] [--json]
10440
10659
  odla-ai pm project add --app <product-id> --name <name> [--description <text>] [--json]
10441
- odla-ai pm project use <project-id> [--json] [saved locally in this worktree]
10660
+ odla-ai pm project use <project-id> [--json] [saved for this app, on this machine]
10442
10661
  odla-ai pm goal list [--app <id>] [--project <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
10443
10662
  odla-ai pm task list [--app <id>] [--column <backlog|ready|doing|review|done>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
10444
10663
  odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
@@ -10530,7 +10749,8 @@ Usage:
10530
10749
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
10531
10750
  odla-ai security run [target] --self --ack-redacted-source
10532
10751
  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]
10533
- odla-ai device enroll [--app <id>[,<id>...]] [--name <label>] [--capability <c>[,<c>...]] [--device-ttl <30d|6w|2y|forever>] [--email <odla-account>] [--no-open] [--json]
10752
+ odla-ai device enroll [--app <id>[,<id>...]|--all-apps] [--capability all|<c>[,<c>...]] [--platform-wide]
10753
+ [--name <label>] [--device-ttl <30d|6w|2y|forever>] [--email <odla-account>] [--no-open] [--wait <seconds>] [--json]
10534
10754
  odla-ai device list [--email <odla-account>] [--json]
10535
10755
  odla-ai device revoke <device-id> [--email <odla-account>] [--json]
10536
10756
  odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
@@ -10544,8 +10764,11 @@ Usage:
10544
10764
 
10545
10765
  // src/help.ts
10546
10766
  function printHelp(output = console) {
10547
- output.log(`odla-ai
10548
- ${USAGE_SECTION}
10767
+ output.log(helpText());
10768
+ }
10769
+ function helpText() {
10770
+ return `odla-ai
10771
+ ${AUTH_SECTION}${USAGE_SECTION}
10549
10772
  Commands:
10550
10773
  auth Start a fresh, exact-project agent authorization for human review.
10551
10774
  The email is the signed-in odla account, never git or GitHub
@@ -10618,8 +10841,9 @@ Commands:
10618
10841
  security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
10619
10842
  pm Project management (via @odla-ai/pm): Products contain Projects;
10620
10843
  projects contain goals, kanban tasks, decisions, and bugs. Use
10621
- "pm project list|add|use" to select worktree-local context, or
10622
- pass --app/--project explicitly. Same device-grant auth as "app".
10844
+ "pm project list|add|use" selects a project for this app on this
10845
+ machine \u2014 every worktree shares it \u2014 or pass --app/--project
10846
+ explicitly. Same device-grant auth as "app".
10623
10847
  Status changes and comments post to each item's @odla-ai/chat
10624
10848
  discussion thread.
10625
10849
  NOTE: "--column ready" is OWNER-ONLY. Creating a task in Ready
@@ -10648,9 +10872,22 @@ Commands:
10648
10872
  platform Read canonical fleet health, releases, provider load/freshness,
10649
10873
  explicit unknowns, and next actions through a read-only grant.
10650
10874
  device Enrol THIS machine once, then stop asking. A human approves the
10651
- enrollment in the browser; from then on this terminal mints its
10652
- own short-lived credentials for the named projects with nobody's
10653
- attention, until the device expires or is revoked.
10875
+ enrollment in the browser; from then on EVERY worktree on this
10876
+ machine mints its own short-lived credentials with nobody's
10877
+ attention, until the device is revoked or goes unused.
10878
+ With no flags it covers every app you own, now and later, with
10879
+ every capability that approval can carry \u2014 so creating an app
10880
+ costs no new approval, and a capability nobody thought to name is
10881
+ not a 403 next week. "--app" or "--capability" narrow it
10882
+ deliberately, and the CLI says what that gave up.
10883
+ "--platform-wide" is the administrator's version, across all of
10884
+ odla.
10885
+ The expiry is a GAP, not a clock: each use rolls it forward, so
10886
+ only going quiet brings a human back into the loop \u2014 which is
10887
+ where anything that changed can be explained.
10888
+ "device list" shows what each machine holds and when it lapses;
10889
+ revoking one takes down every credential it ever minted, and is
10890
+ deliberately a signed-in human's decision in Studio.
10654
10891
  provision Register services, compose integrations, persist credentials, optionally push secrets.
10655
10892
  "provision --live --yes" initializes only the live instance of
10656
10893
  an existing sandbox app and enables every configured service;
@@ -10746,7 +10983,46 @@ Safety:
10746
10983
  Run security plan first to inspect the admin-selected providers, models,
10747
10984
  per-route bounds, credential readiness, retention, no-execution boundary,
10748
10985
  and digest that binds consent to that exact plan.
10749
- `);
10986
+ `;
10987
+ }
10988
+
10989
+ // src/help-command.ts
10990
+ function printCommandHelp(command, output = console) {
10991
+ const lines = helpText().split("\n");
10992
+ const usage = allBlocks(lines, new RegExp(`^ odla-ai ${escapeRe(command)}(\\s|$)`));
10993
+ const prose = block(lines, (line2) => new RegExp(`^ ${escapeRe(command)}\\s\\s+\\S`).test(line2));
10994
+ if (usage.length === 0 && prose.length === 0) {
10995
+ output.log(`odla-ai: no command "${command}". Run "odla-ai help" for all of them.`);
10996
+ return;
10997
+ }
10998
+ output.log([
10999
+ ...prose.length ? [prose.join("\n"), ""] : [],
11000
+ ...usage.length ? ["Usage:", ...usage, ""] : [],
11001
+ AUTH_SECTION.trimEnd()
11002
+ ].join("\n"));
11003
+ }
11004
+ function allBlocks(lines, pattern) {
11005
+ const out = [];
11006
+ for (let i = 0; i < lines.length; i++) {
11007
+ if (!pattern.test(lines[i])) continue;
11008
+ out.push(...block(lines.slice(i), (line2) => line2 === lines[i]));
11009
+ }
11010
+ return out;
11011
+ }
11012
+ function block(lines, starts) {
11013
+ const first = lines.findIndex(starts);
11014
+ if (first === -1) return [];
11015
+ const indent = lines[first].length - lines[first].trimStart().length;
11016
+ const out = [lines[first]];
11017
+ for (const line2 of lines.slice(first + 1)) {
11018
+ if (!line2.trim()) break;
11019
+ if (line2.length - line2.trimStart().length <= indent) break;
11020
+ out.push(line2);
11021
+ }
11022
+ return out;
11023
+ }
11024
+ function escapeRe(value2) {
11025
+ return value2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10750
11026
  }
10751
11027
 
10752
11028
  // src/discuss-principals.ts
@@ -11903,14 +12179,36 @@ async function pmWatch(ctx, parsed) {
11903
12179
  }
11904
12180
 
11905
12181
  // src/pm-project-context.ts
11906
- var import_node_path16 = require("path");
11907
- var pmProjectContextFile = (rootDir) => (0, import_node_path16.resolve)(rootDir, ".odla", "pm-project.local.json");
12182
+ var import_node_fs17 = require("fs");
12183
+ var import_node_path17 = require("path");
12184
+ var pmProjectContextFile = () => pmContextFile();
11908
12185
  function readPmProjectContext(rootDir) {
11909
- const value2 = readJsonFile(pmProjectContextFile(rootDir));
11910
- return value2 && typeof value2.appId === "string" && typeof value2.projectId === "string" ? value2 : null;
12186
+ adoptLegacySelection(rootDir);
12187
+ const entries = Object.values(readSelections()).filter(isSelection);
12188
+ return entries.sort((a, b) => b.selectedAt.localeCompare(a.selectedAt))[0] ?? null;
11911
12189
  }
11912
12190
  function writePmProjectContext(rootDir, value2) {
11913
- writePrivateJson(pmProjectContextFile(rootDir), { ...value2, selectedAt: (/* @__PURE__ */ new Date()).toISOString() });
12191
+ adoptLegacySelection(rootDir);
12192
+ writePrivateJson(pmProjectContextFile(), {
12193
+ ...readSelections(),
12194
+ [value2.appId]: { ...value2, selectedAt: (/* @__PURE__ */ new Date()).toISOString() }
12195
+ });
12196
+ }
12197
+ function adoptLegacySelection(rootDir) {
12198
+ const legacy = (0, import_node_path17.resolve)(rootDir, ".odla", "pm-project.local.json");
12199
+ if (!(0, import_node_fs17.existsSync)(legacy)) return;
12200
+ const previous = readJsonFile(legacy);
12201
+ (0, import_node_fs17.rmSync)(legacy, { force: true });
12202
+ if (!isSelection(previous)) return;
12203
+ const selections = readSelections();
12204
+ if (selections[previous.appId]) return;
12205
+ writePrivateJson(pmProjectContextFile(), { ...selections, [previous.appId]: previous });
12206
+ }
12207
+ function readSelections() {
12208
+ return readJsonFile(pmProjectContextFile()) ?? {};
12209
+ }
12210
+ function isSelection(value2) {
12211
+ return !!value2 && typeof value2.appId === "string" && typeof value2.projectId === "string" && typeof value2.selectedAt === "string";
11914
12212
  }
11915
12213
 
11916
12214
  // src/pm-project-actions.ts
@@ -12935,7 +13233,7 @@ function percent(value2) {
12935
13233
  // src/provision.ts
12936
13234
  var import_apps13 = require("@odla-ai/apps");
12937
13235
  var import_ai5 = require("@odla-ai/ai");
12938
- var import_node_process13 = __toESM(require("process"), 1);
13236
+ var import_node_process17 = __toESM(require("process"), 1);
12939
13237
 
12940
13238
  // src/integration-provision.ts
12941
13239
  var import_db3 = require("@odla-ai/db");
@@ -13373,7 +13671,7 @@ async function provision(options) {
13373
13671
  await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
13374
13672
  }
13375
13673
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
13376
- const key = import_node_process13.default.env[cfg.ai.keyEnv];
13674
+ const key = import_node_process17.default.env[cfg.ai.keyEnv];
13377
13675
  if (key) {
13378
13676
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
13379
13677
  await (0, import_ai5.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -13414,8 +13712,8 @@ async function provision(options) {
13414
13712
  }
13415
13713
 
13416
13714
  // src/record.ts
13417
- var import_node_fs17 = require("fs");
13418
- var import_node_process14 = __toESM(require("process"), 1);
13715
+ var import_node_fs18 = require("fs");
13716
+ var import_node_process18 = __toESM(require("process"), 1);
13419
13717
 
13420
13718
  // src/surface.ts
13421
13719
  var PM_ACTIONS = {
@@ -13597,7 +13895,7 @@ function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
13597
13895
 
13598
13896
  // src/record.ts
13599
13897
  function recordInvocation(parsed) {
13600
- const file = import_node_process14.default.env.ODLA_CLI_RECORD;
13898
+ const file = import_node_process18.default.env.ODLA_CLI_RECORD;
13601
13899
  if (!file) return;
13602
13900
  try {
13603
13901
  const entry = {
@@ -13605,7 +13903,7 @@ function recordInvocation(parsed) {
13605
13903
  options: Object.entries(parsed.options).map(([name, value2]) => value2 === false ? `no-${name}` : name).sort()
13606
13904
  };
13607
13905
  if (!entry.path.length) return;
13608
- (0, import_node_fs17.appendFileSync)(file, `${JSON.stringify(entry)}
13906
+ (0, import_node_fs18.appendFileSync)(file, `${JSON.stringify(entry)}
13609
13907
  `);
13610
13908
  } catch {
13611
13909
  }
@@ -13623,10 +13921,17 @@ function advisoryCollectingFetch(inner, sink) {
13623
13921
  return response2;
13624
13922
  });
13625
13923
  }
13924
+ var superseded = /* @__PURE__ */ new Set();
13925
+ function supersedeAdvisory(code) {
13926
+ superseded.add(code);
13927
+ }
13626
13928
  function renderAdvisories(out, advisories, env = process.env) {
13929
+ const retracted = new Set(superseded);
13930
+ superseded.clear();
13627
13931
  if (env.ODLA_NO_ADVISORIES) return;
13628
13932
  const seen = /* @__PURE__ */ new Set();
13629
13933
  for (const advisory of advisories) {
13934
+ if (retracted.has(advisory.code)) continue;
13630
13935
  const key = `${advisory.code}:${advisory.message}`;
13631
13936
  if (seen.has(key)) continue;
13632
13937
  seen.add(key);
@@ -13634,6 +13939,9 @@ function renderAdvisories(out, advisories, env = process.env) {
13634
13939
  }
13635
13940
  }
13636
13941
 
13942
+ // src/device-command.ts
13943
+ var import_db4 = require("@odla-ai/db");
13944
+
13637
13945
  // src/device-ttl.ts
13638
13946
  var OWNER_DEVICE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
13639
13947
  var DAY_MS = 24 * 60 * 60 * 1e3;
@@ -13653,10 +13961,26 @@ function parseDeviceTtl(raw) {
13653
13961
  }
13654
13962
 
13655
13963
  // src/device-command.ts
13656
- var import_node_fs18 = require("fs");
13657
- var import_node_path17 = require("path");
13658
- var import_node_process15 = __toESM(require("process"), 1);
13964
+ var import_node_fs19 = require("fs");
13965
+ var import_node_path18 = require("path");
13966
+ var import_node_process19 = __toESM(require("process"), 1);
13659
13967
  async function deviceCommand(parsed, deps) {
13968
+ assertArgs(parsed, [
13969
+ "app",
13970
+ "all-apps",
13971
+ "platform-wide",
13972
+ "name",
13973
+ "capability",
13974
+ "device-ttl",
13975
+ "email",
13976
+ "open",
13977
+ "json",
13978
+ "config",
13979
+ "token",
13980
+ "context",
13981
+ "platform",
13982
+ "wait"
13983
+ ], 3);
13660
13984
  const action2 = parsed.positionals[1] ?? "";
13661
13985
  const out = deps.stdout ?? console;
13662
13986
  const doFetch = deps.fetch ?? fetch;
@@ -13669,10 +13993,19 @@ async function deviceCommand(parsed, deps) {
13669
13993
  }
13670
13994
  async function enroll(parsed, deps, cfg, doFetch, out, json) {
13671
13995
  const name = stringOpt(parsed.options.name) ?? defaultDeviceName();
13672
- const apps = (stringOpt(parsed.options.app) ?? cfg.app.id).split(",").map((id2) => id2.trim()).filter(Boolean);
13673
- if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026]");
13996
+ const platformWide = parsed.options["platform-wide"] === true;
13997
+ const narrowed = stringOpt(parsed.options.app) !== void 0 || stringOpt(parsed.options.capability) !== void 0;
13998
+ 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);
13999
+ if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026], or --all-apps");
13674
14000
  const deviceTtlMs = parseDeviceTtl(parsed.options["device-ttl"]);
13675
- const extended = deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
14001
+ const extended = platformWide || deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
14002
+ const { capabilities, scopes } = requestedEnvelope(parsed, platformWide, narrowed);
14003
+ const narrowNotice = narrowEnrollmentNotice({
14004
+ appIds: apps,
14005
+ capabilities: capabilities ?? [],
14006
+ scopes: scopes ?? []
14007
+ });
14008
+ if (narrowNotice) out.error(narrowNotice);
13676
14009
  const token = await scopedToken2(
13677
14010
  parsed,
13678
14011
  deps,
@@ -13687,9 +14020,10 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
13687
14020
  headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
13688
14021
  body: JSON.stringify({
13689
14022
  name,
13690
- platform: import_node_process15.default.platform,
14023
+ platform: import_node_process19.default.platform,
13691
14024
  appIds: apps,
13692
- ...parsed.options.capability ? { capabilities: String(parsed.options.capability).split(",").map((c) => c.trim()).filter(Boolean) } : {},
14025
+ ...capabilities ? { capabilities } : {},
14026
+ ...scopes ? { scopes } : {},
13693
14027
  ...deviceTtlMs === void 0 ? {} : { deviceTtlMs }
13694
14028
  })
13695
14029
  });
@@ -13698,19 +14032,54 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
13698
14032
  throw new Error(`device enroll failed: ${body.error?.message ?? `registry returned ${response2.status}`} (${response2.status})`);
13699
14033
  }
13700
14034
  const path = deviceCredentialPath();
13701
- (0, import_node_fs18.mkdirSync)((0, import_node_path17.dirname)(path), { recursive: true });
13702
- (0, import_node_fs18.writeFileSync)(path, JSON.stringify({
14035
+ (0, import_node_fs19.mkdirSync)((0, import_node_path18.dirname)(path), { recursive: true });
14036
+ (0, import_node_fs19.writeFileSync)(path, JSON.stringify({
13703
14037
  token: body.token,
13704
14038
  platform: cfg.platformUrl.replace(/\/$/, ""),
13705
14039
  deviceId: body.device.deviceId,
13706
14040
  name
13707
14041
  }, null, 2));
13708
- (0, import_node_fs18.chmodSync)(path, 384);
13709
- out.error(`device: enrolled "${name}" for ${body.device.appIds.join(", ")}; credential written to ${path}`);
13710
- out.error("device: this terminal will mint its own credentials from now on \u2014 no further approvals.");
14042
+ (0, import_node_fs19.chmodSync)(path, 384);
14043
+ rememberMachineIdentity(cfg.platformUrl.replace(/\/$/, ""), stringOpt(parsed.options.email));
14044
+ supersedeAdvisory("credential.expiring");
14045
+ const reach = body.device.appIds.includes(import_db4.ALL_OWNED_APPS) ? "every app you own, now and later" : body.device.appIds.join(", ");
14046
+ out.error(`device: enrolled "${name}" for ${reach}; credential written to ${path}`);
14047
+ if (narrowNotice) out.error(narrowNotice);
14048
+ out.error(
14049
+ "device: every worktree on this machine mints its own credentials from now on \u2014 no further approvals,"
14050
+ );
14051
+ out.error(
14052
+ `device: and the clock resets each time you use it. Going quiet for ${describeWindow(body.device.expiresAt)} is what ends it.`
14053
+ );
13711
14054
  if (json) {
13712
- out.log(JSON.stringify({ deviceId: body.device.deviceId, name, appIds: body.device.appIds, expiresAt: body.device.expiresAt }, null, 2));
14055
+ out.log(JSON.stringify({
14056
+ deviceId: body.device.deviceId,
14057
+ name,
14058
+ appIds: body.device.appIds,
14059
+ capabilities: body.device.capabilities ?? [],
14060
+ scopes: body.device.scopes ?? [],
14061
+ expiresAt: body.device.expiresAt,
14062
+ hardExpiresAt: body.device.hardExpiresAt ?? null
14063
+ }, null, 2));
14064
+ }
14065
+ }
14066
+ function requestedEnvelope(parsed, platformWide, narrowed) {
14067
+ const raw = stringOpt(parsed.options.capability);
14068
+ const everything = platformWide || !narrowed || raw?.trim().toLowerCase() === "all";
14069
+ if (everything) {
14070
+ return {
14071
+ capabilities: [...import_db4.OPTIONAL_AGENT_PROJECT_CAPABILITIES],
14072
+ scopes: platformWide ? [...import_db4.ADMIN_DEVICE_SCOPES] : [...import_db4.OWNER_DEVICE_SCOPES]
14073
+ };
13713
14074
  }
14075
+ const named = raw?.split(",").map((c) => c.trim()).filter(Boolean);
14076
+ return named?.length ? { capabilities: named } : {};
14077
+ }
14078
+ function describeWindow(expiresAt, now = Date.now()) {
14079
+ const days = Math.max(1, Math.round((expiresAt - now) / (24 * 60 * 60 * 1e3)));
14080
+ if (days >= 365) return `${Math.round(days / 365)} year${days >= 730 ? "s" : ""}`;
14081
+ if (days % 7 === 0) return `${days / 7} week${days > 7 ? "s" : ""}`;
14082
+ return `${days} day${days === 1 ? "" : "s"}`;
13714
14083
  }
13715
14084
  async function list2(parsed, deps, cfg, doFetch, out, json) {
13716
14085
  const token = await scopedToken2(parsed, deps, cfg, doFetch, out, "odla CLI (device list)");
@@ -13725,12 +14094,17 @@ async function list2(parsed, deps, cfg, doFetch, out, json) {
13725
14094
  if (body.devices.length === 0) return out.log("no enrolled devices");
13726
14095
  for (const device of body.devices) {
13727
14096
  const state2 = device.revokedAt ? "revoked" : device.expiresAt <= Date.now() ? "expired" : "active";
13728
- out.log(`${device.deviceId} ${state2.padEnd(7)} ${device.name} [${device.appIds.join(", ")}]`);
14097
+ const reach = device.appIds.includes("*") ? "every app you own" : device.appIds.join(", ");
14098
+ const gap = state2 === "active" ? ` idle ${describeWindow(device.expiresAt)} left` : "";
14099
+ out.log(`${device.deviceId} ${state2.padEnd(7)} ${device.name} [${reach}]${gap}`);
13729
14100
  }
13730
14101
  }
13731
14102
  async function revoke(parsed, deps, cfg, doFetch, out, json) {
13732
14103
  const deviceId = parsed.positionals[2];
13733
14104
  if (!deviceId) throw new Error("device revoke needs the device id from `odla-ai device list`");
14105
+ out.error(
14106
+ `device: revoking is a signed-in human's decision; if this is refused, open ${cfg.platformUrl}/studio and revoke it there.`
14107
+ );
13734
14108
  const token = await scopedToken2(parsed, deps, cfg, doFetch, out, "odla CLI (device revoke)");
13735
14109
  const response2 = await doFetch(`${cfg.platformUrl}/registry/devices/${encodeURIComponent(deviceId)}/revoke`, {
13736
14110
  method: "POST",
@@ -13749,7 +14123,7 @@ async function scopedToken2(parsed, deps, cfg, doFetch, out, label, scope = "app
13749
14123
  // A device is granted the apps named in ONE approval, so --app is a list here.
13750
14124
  allowAppList: true
13751
14125
  });
13752
- const scopedTokenFile = credentials.scopedTokenFile;
14126
+ const scopedTokenFile2 = credentials.scopedTokenFile;
13753
14127
  return getScopedPlatformToken({
13754
14128
  platform: cfg.platformUrl,
13755
14129
  scope,
@@ -13760,16 +14134,16 @@ async function scopedToken2(parsed, deps, cfg, doFetch, out, label, scope = "app
13760
14134
  open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
13761
14135
  openApprovalUrl: deps.openUrl,
13762
14136
  rootDir: cfg.rootDir,
13763
- tokenFile: scopedTokenFile,
14137
+ tokenFile: scopedTokenFile2,
13764
14138
  ...stringOpt(parsed.options.token) ? { token: stringOpt(parsed.options.token) } : {}
13765
14139
  });
13766
14140
  }
13767
14141
  function defaultDeviceName() {
13768
- return `${import_node_process15.default.env.HOSTNAME ?? import_node_process15.default.env.HOST ?? "machine"}-${import_node_process15.default.platform}`;
14142
+ return `${import_node_process19.default.env.HOSTNAME ?? import_node_process19.default.env.HOST ?? "machine"}-${import_node_process19.default.platform}`;
13769
14143
  }
13770
14144
 
13771
14145
  // src/runbook-actions.ts
13772
- var import_node_fs19 = require("fs");
14146
+ var import_node_fs20 = require("fs");
13773
14147
 
13774
14148
  // src/runbook-requires.ts
13775
14149
  var SPEC = /^(@?[\w./-]+?)@(\d+\.\d+\.\d+(?:[\w.-]*)?)$/;
@@ -13865,7 +14239,7 @@ async function bySlug(ctx, slug) {
13865
14239
  function readBody(file, inline) {
13866
14240
  if (inline !== void 0) return inline;
13867
14241
  if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
13868
- return (0, import_node_fs19.readFileSync)(file === "-" ? 0 : file, "utf8");
14242
+ return (0, import_node_fs20.readFileSync)(file === "-" ? 0 : file, "utf8");
13869
14243
  }
13870
14244
  var stamp = (ms) => ms ? new Date(ms).toISOString().slice(0, 16).replace("T", " ") : "";
13871
14245
  async function runbookList(ctx, all, query) {
@@ -13957,8 +14331,8 @@ async function runbookRemove(ctx, slug) {
13957
14331
  }
13958
14332
 
13959
14333
  // src/runbook-import.ts
13960
- var import_node_fs20 = require("fs");
13961
- var import_node_path18 = require("path");
14334
+ var import_node_fs21 = require("fs");
14335
+ var import_node_path19 = require("path");
13962
14336
  function parseRunbook(text3, slug) {
13963
14337
  let rest = text3;
13964
14338
  const meta = {};
@@ -13983,12 +14357,12 @@ function parseRunbook(text3, slug) {
13983
14357
  };
13984
14358
  }
13985
14359
  function readRunbookDir(dir) {
13986
- if (!(0, import_node_fs20.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
13987
- const files = (0, import_node_fs20.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
14360
+ if (!(0, import_node_fs21.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
14361
+ const files = (0, import_node_fs21.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
13988
14362
  if (!files.length) throw new Error(`no .md files in ${dir}`);
13989
14363
  return files.map((file) => {
13990
- const slug = (0, import_node_path18.basename)(file, ".md");
13991
- const parsed = parseRunbook((0, import_node_fs20.readFileSync)((0, import_node_path18.join)(dir, file), "utf8"), slug);
14364
+ const slug = (0, import_node_path19.basename)(file, ".md");
14365
+ const parsed = parseRunbook((0, import_node_fs21.readFileSync)((0, import_node_path19.join)(dir, file), "utf8"), slug);
13992
14366
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
13993
14367
  });
13994
14368
  }
@@ -14056,8 +14430,8 @@ async function upsert(ctx, r, visibility) {
14056
14430
 
14057
14431
  // src/runbook-impact.ts
14058
14432
  var import_node_child_process6 = require("child_process");
14059
- var import_node_fs21 = require("fs");
14060
- var import_node_path19 = require("path");
14433
+ var import_node_fs22 = require("fs");
14434
+ var import_node_path20 = require("path");
14061
14435
 
14062
14436
  // src/runbook-impact-scan.ts
14063
14437
  var DECL = /^[+-]\s*export\s+(?:declare\s+)?(?:default\s+)?(?:abstract\s+)?(?:async\s+)?(?:const|let|var|function|class|interface|type|enum)\s+([A-Za-z_$][\w$]*)/;
@@ -14226,10 +14600,10 @@ ${body.split("\n").map((line2) => `+${line2}`).join("\n")}
14226
14600
  }
14227
14601
  function manifestLabeller(root) {
14228
14602
  return (workspace) => {
14229
- const manifest = (0, import_node_path19.join)(root, workspace, "package.json");
14230
- if (!(0, import_node_fs21.existsSync)(manifest)) return void 0;
14603
+ const manifest = (0, import_node_path20.join)(root, workspace, "package.json");
14604
+ if (!(0, import_node_fs22.existsSync)(manifest)) return void 0;
14231
14605
  try {
14232
- const name = JSON.parse((0, import_node_fs21.readFileSync)(manifest, "utf8")).name;
14606
+ const name = JSON.parse((0, import_node_fs22.readFileSync)(manifest, "utf8")).name;
14233
14607
  return typeof name === "string" ? name : void 0;
14234
14608
  } catch {
14235
14609
  return void 0;
@@ -14296,7 +14670,7 @@ function report4(ctx, impacts) {
14296
14670
  async function runbookImpact(ctx, options, deps = {}) {
14297
14671
  const cwd = deps.cwd ?? process.cwd();
14298
14672
  const runGit = deps.runGit ?? gitRunner(cwd);
14299
- const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs21.readFileSync)((0, import_node_path19.join)(cwd, path), "utf8"));
14673
+ const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs22.readFileSync)((0, import_node_path20.join)(cwd, path), "utf8"));
14300
14674
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
14301
14675
  if (!surfaces.length) {
14302
14676
  return ctx.out.log(
@@ -14423,12 +14797,12 @@ async function runbookComment(ctx, slug, body) {
14423
14797
 
14424
14798
  // src/runbook-editor.ts
14425
14799
  var import_node_child_process7 = require("child_process");
14426
- var import_node_fs22 = require("fs");
14427
- var import_node_os5 = require("os");
14428
- var import_node_path20 = require("path");
14429
- var import_node_process16 = __toESM(require("process"), 1);
14800
+ var import_node_fs23 = require("fs");
14801
+ var import_node_os6 = require("os");
14802
+ var import_node_path21 = require("path");
14803
+ var import_node_process20 = __toESM(require("process"), 1);
14430
14804
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
14431
- function resolveEditor(env = import_node_process16.default.env) {
14805
+ function resolveEditor(env = import_node_process20.default.env) {
14432
14806
  for (const name of EDITOR_ENV) {
14433
14807
  const value2 = env[name];
14434
14808
  if (value2 && value2.trim()) return value2.trim();
@@ -14442,8 +14816,8 @@ function defaultRun(command, path) {
14442
14816
  return result.status ?? 0;
14443
14817
  }
14444
14818
  function editText(initial, slug, deps = {}) {
14445
- const env = deps.env ?? import_node_process16.default.env;
14446
- const interactive = deps.interactive ?? (() => Boolean(import_node_process16.default.stdin.isTTY));
14819
+ const env = deps.env ?? import_node_process20.default.env;
14820
+ const interactive = deps.interactive ?? (() => Boolean(import_node_process20.default.stdin.isTTY));
14447
14821
  const editor = resolveEditor(env);
14448
14822
  if (!editor)
14449
14823
  throw new Error(
@@ -14451,16 +14825,16 @@ function editText(initial, slug, deps = {}) {
14451
14825
  );
14452
14826
  if (!interactive())
14453
14827
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
14454
- const dir = (0, import_node_fs22.mkdtempSync)((0, import_node_path20.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
14455
- const file = (0, import_node_path20.join)(dir, `${slug}.md`);
14828
+ const dir = (0, import_node_fs23.mkdtempSync)((0, import_node_path21.join)((0, import_node_os6.tmpdir)(), "odla-runbook-"));
14829
+ const file = (0, import_node_path21.join)(dir, `${slug}.md`);
14456
14830
  try {
14457
- (0, import_node_fs22.writeFileSync)(file, initial, { mode: 384 });
14831
+ (0, import_node_fs23.writeFileSync)(file, initial, { mode: 384 });
14458
14832
  const code = defaultRunOrInjected(deps)(editor, file);
14459
14833
  if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
14460
- const edited = (0, import_node_fs22.readFileSync)(file, "utf8");
14834
+ const edited = (0, import_node_fs23.readFileSync)(file, "utf8");
14461
14835
  return edited === initial ? null : edited;
14462
14836
  } finally {
14463
- (0, import_node_fs22.rmSync)(dir, { recursive: true, force: true });
14837
+ (0, import_node_fs23.rmSync)(dir, { recursive: true, force: true });
14464
14838
  }
14465
14839
  }
14466
14840
  var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
@@ -14798,7 +15172,7 @@ function hostedSeverity(value2, flag) {
14798
15172
  var import_security2 = require("@odla-ai/security");
14799
15173
 
14800
15174
  // src/security.ts
14801
- var import_node_path21 = require("path");
15175
+ var import_node_path22 = require("path");
14802
15176
  var import_security = require("@odla-ai/security");
14803
15177
  var import_node3 = require("@odla-ai/security/node");
14804
15178
  async function runHostedSecurity(options) {
@@ -14810,9 +15184,9 @@ async function runHostedSecurity(options) {
14810
15184
  const appId = selfAudit ? "odla-ai" : cfg.app.id;
14811
15185
  const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
14812
15186
  const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
14813
- const target = (0, import_node_path21.resolve)(options.target ?? cfg?.rootDir ?? ".");
14814
- const output = (0, import_node_path21.resolve)(options.out ?? (0, import_node_path21.resolve)(target, ".odla/security/hosted"));
14815
- const outputRelative = (0, import_node_path21.relative)(target, output).split(import_node_path21.sep).join("/");
15187
+ const target = (0, import_node_path22.resolve)(options.target ?? cfg?.rootDir ?? ".");
15188
+ const output = (0, import_node_path22.resolve)(options.out ?? (0, import_node_path22.resolve)(target, ".odla/security/hosted"));
15189
+ const outputRelative = (0, import_node_path22.relative)(target, output).split(import_node_path22.sep).join("/");
14816
15190
  if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
14817
15191
  const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
14818
15192
  const tokenRequest = {
@@ -14824,7 +15198,7 @@ async function runHostedSecurity(options) {
14824
15198
  };
14825
15199
  const token = await injectedToken(options, tokenRequest);
14826
15200
  const snapshot = await (0, import_node3.snapshotDirectory)(target, {
14827
- exclude: !outputRelative.startsWith("../") && !(0, import_node_path21.isAbsolute)(outputRelative) ? [outputRelative] : []
15201
+ exclude: !outputRelative.startsWith("../") && !(0, import_node_path22.isAbsolute)(outputRelative) ? [outputRelative] : []
14828
15202
  });
14829
15203
  const hosted = await (0, import_security.createPlatformSecurityReasoners)({
14830
15204
  platform,
@@ -14842,7 +15216,7 @@ async function runHostedSecurity(options) {
14842
15216
  });
14843
15217
  const harness = (0, import_security.createSecurityHarness)({
14844
15218
  profile,
14845
- store: new import_node3.FileRunStore((0, import_node_path21.resolve)(output, "state")),
15219
+ store: new import_node3.FileRunStore((0, import_node_path22.resolve)(output, "state")),
14846
15220
  discoveryReasoner: hosted.discoveryReasoner,
14847
15221
  validationReasoner: hosted.validationReasoner,
14848
15222
  policy: {
@@ -14866,7 +15240,7 @@ async function runHostedSecurity(options) {
14866
15240
  function selectEnv(requested, declared, configPath, rootDir) {
14867
15241
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
14868
15242
  if (!env || !declared.includes(env)) {
14869
- const shown = (0, import_node_path21.relative)(rootDir, configPath) || configPath;
15243
+ const shown = (0, import_node_path22.relative)(rootDir, configPath) || configPath;
14870
15244
  throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
14871
15245
  }
14872
15246
  return env;
@@ -14895,7 +15269,7 @@ function printSummary(out, appId, env, run, report5, output) {
14895
15269
  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}`);
14896
15270
  if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
14897
15271
  out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
14898
- out.log(` report: ${(0, import_node_path21.resolve)(output, "REPORT.md")}`);
15272
+ out.log(` report: ${(0, import_node_path22.resolve)(output, "REPORT.md")}`);
14899
15273
  }
14900
15274
  function formatBudget(usage) {
14901
15275
  return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
@@ -15376,8 +15750,10 @@ async function dispatchCli(argv, dependencies) {
15376
15750
  return;
15377
15751
  }
15378
15752
  if (command === "help" || command === "--help" || command === "-h") {
15379
- assertArgs(parsed, ["help"], 1);
15380
- printHelp(runtime.stdout);
15753
+ assertArgs(parsed, ["help"], 2);
15754
+ const topic = parsed.positionals[1];
15755
+ if (topic) printCommandHelp(topic, runtime.stdout);
15756
+ else printHelp(runtime.stdout);
15381
15757
  return;
15382
15758
  }
15383
15759
  if (command === "whoami") {