@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.
@@ -10,12 +10,12 @@ import {
10
10
  } from "./chunk-UKLSRQ5J.js";
11
11
 
12
12
  // src/admin-ai.ts
13
- import process9 from "process";
13
+ import process13 from "process";
14
14
 
15
15
  // src/token.ts
16
16
  import { OdlaError, requestToken } from "@odla-ai/db";
17
17
  import { createHash } from "crypto";
18
- import process6 from "process";
18
+ import process10 from "process";
19
19
 
20
20
  // src/handshake-approval.ts
21
21
  import process3 from "process";
@@ -150,27 +150,10 @@ function handshakeWaitMs(waitSeconds, interactive = process4.stdout.isTTY === tr
150
150
  }
151
151
 
152
152
  // src/cached-credential.ts
153
- import { rmSync as rmSync2 } from "fs";
154
- var noted = null;
155
- function noteCachedCredential(tokenFile) {
156
- noted = tokenFile;
157
- }
158
- function isCredentialRejection(error) {
159
- const message2 = error instanceof Error ? error.message : String(error ?? "");
160
- return /\((401|403)\)\s*$/.test(message2.trim());
161
- }
162
- function explainRejectedCredential(error) {
163
- const tokenFile = noted;
164
- if (!tokenFile || !isCredentialRejection(error)) return null;
165
- noted = null;
166
- rmSync2(tokenFile, { force: true });
167
- return [
168
- "auth: the cached credential was rejected by odla, so it was revoked before its cached expiry.",
169
- " The usual cause is a newer sign-in for this project: collecting a handshake retires the",
170
- " principal's other credentials, so a second terminal or worktree supersedes this one.",
171
- ` Discarded ${tokenFile}; re-run this command to request a fresh approval.`
172
- ].join("\n");
173
- }
153
+ import { rmSync as rmSync3 } from "fs";
154
+
155
+ // src/auth-guidance.ts
156
+ import process7 from "process";
174
157
 
175
158
  // src/device-session.ts
176
159
  import { existsSync, readFileSync } from "fs";
@@ -206,12 +189,76 @@ async function mintDeviceSession(platformUrl, credential2, doFetch) {
206
189
  `device session failed: ${detail} (${response2.status})` + (revocable ? " \u2014 if this machine's enrollment was revoked or has expired, enroll it again in Studio" : "")
207
190
  );
208
191
  }
209
- return { token: body.token, expiresAt: body.expiresAt ?? Date.now() };
192
+ return {
193
+ token: body.token,
194
+ expiresAt: body.expiresAt ?? Date.now(),
195
+ // Absent from a registry that predates rolling expiry and scoped devices.
196
+ // Left undefined rather than defaulted, so a caller can tell "the platform
197
+ // did not say" from "the platform said none".
198
+ ...typeof body.deviceExpiresAt === "number" ? { deviceExpiresAt: body.deviceExpiresAt } : {},
199
+ ...Array.isArray(body.appIds) ? { appIds: body.appIds } : {},
200
+ ...Array.isArray(body.capabilities) ? { capabilities: body.capabilities } : {},
201
+ ...Array.isArray(body.scopes) ? { scopes: body.scopes } : {}
202
+ };
203
+ }
204
+
205
+ // src/odla-home.ts
206
+ import { chmodSync, copyFileSync, existsSync as existsSync2, mkdirSync, rmSync as rmSync2 } from "fs";
207
+ import { homedir as homedir2 } from "os";
208
+ import { dirname as dirname2, join as join3 } from "path";
209
+ import process6 from "process";
210
+ function odlaHome(env = process6.env) {
211
+ if (env.VITEST && !env.ODLA_HOME) {
212
+ throw new Error(
213
+ "ODLA_HOME must be set under test \u2014 resolving the real ~/.odla would write to the developer's machine"
214
+ );
215
+ }
216
+ return env.ODLA_HOME ?? join3(env.HOME ?? homedir2(), ".odla");
217
+ }
218
+ function odlaHomePath(segments, env = process6.env) {
219
+ return join3(odlaHome(env), ...segments);
220
+ }
221
+ function identityFile(env) {
222
+ return odlaHomePath(["identity.json"], env);
223
+ }
224
+ function deviceSessionFile(env) {
225
+ return odlaHomePath(["session.json"], env);
226
+ }
227
+ function appTokenFile(appId, env) {
228
+ return odlaHomePath(["apps", safeSegment(appId), "dev-token.json"], env);
229
+ }
230
+ function appCredentialsFile(appId, env) {
231
+ return odlaHomePath(["apps", safeSegment(appId), "credentials.json"], env);
232
+ }
233
+ function scopedTokenFile(env) {
234
+ return odlaHomePath(["admin-token.local.json"], env);
235
+ }
236
+ function pmContextFile(env) {
237
+ return odlaHomePath(["pm-context.json"], env);
238
+ }
239
+ function adoptRepoLocalCache(legacyPath, machinePath, out) {
240
+ if (!existsSync2(legacyPath) || legacyPath === machinePath) return false;
241
+ const superseded2 = existsSync2(machinePath);
242
+ if (!superseded2) {
243
+ mkdirSync(dirname2(machinePath), { recursive: true });
244
+ copyFileSync(legacyPath, machinePath);
245
+ chmodSync(machinePath, 384);
246
+ }
247
+ rmSync2(legacyPath, { force: true });
248
+ out?.error(
249
+ 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`
250
+ );
251
+ return true;
252
+ }
253
+ function safeSegment(value2) {
254
+ const clean4 = value2.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "");
255
+ if (!clean4) throw new Error(`"${value2}" is not a usable app id`);
256
+ return clean4;
210
257
  }
211
258
 
212
259
  // src/local.ts
213
- import { chmodSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
214
- import { dirname as dirname2, isAbsolute, relative, resolve } from "path";
260
+ import { chmodSync as chmodSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
261
+ import { dirname as dirname3, isAbsolute, relative, resolve } from "path";
215
262
  var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
216
263
  function readJsonFile(path) {
217
264
  try {
@@ -225,7 +272,7 @@ function writePrivateJson(path, value2) {
225
272
  `);
226
273
  }
227
274
  function readCredentials(path) {
228
- if (!existsSync2(path)) return null;
275
+ if (!existsSync3(path)) return null;
229
276
  let value2;
230
277
  try {
231
278
  value2 = JSON.parse(readFileSync2(path, "utf8"));
@@ -259,7 +306,7 @@ function mergeCredential(current, update) {
259
306
  }
260
307
  function ensureGitignore(rootDir, localPaths = []) {
261
308
  const path = resolve(rootDir, ".gitignore");
262
- const existing = existsSync2(path) ? readFileSync2(path, "utf8") : "";
309
+ const existing = existsSync3(path) ? readFileSync2(path, "utf8") : "";
263
310
  const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line2) => !!line2);
264
311
  const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
265
312
  const missing = wanted.filter((line2) => !existing.split(/\r?\n/).includes(line2));
@@ -279,7 +326,7 @@ function o11yDevVars(cfg) {
279
326
  function resolveWriteDevVarsTarget(cfg, requested) {
280
327
  if (!requested) return null;
281
328
  if (requested === true) return cfg.local.devVarsFile;
282
- return resolve(dirname2(cfg.configPath), requested);
329
+ return resolve(dirname3(cfg.configPath), requested);
283
330
  }
284
331
  function writeDevVars(path, credentials, env, o11y) {
285
332
  const entry = credentials.envs[env];
@@ -293,7 +340,7 @@ function writeDevVars(path, credentials, env, o11y) {
293
340
  if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
294
341
  if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
295
342
  }
296
- const existing = existsSync2(path) ? readFileSync2(path, "utf8") : "";
343
+ const existing = existsSync3(path) ? readFileSync2(path, "utf8") : "";
297
344
  const retained = existing.split(/\r?\n/).filter((line2) => !isManagedDevVar(line2));
298
345
  while (retained.at(-1) === "") retained.pop();
299
346
  const prefix = retained.length ? `${retained.join("\n")}
@@ -319,10 +366,10 @@ function isManagedDevVar(line2) {
319
366
  return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
320
367
  }
321
368
  function writePrivateText(path, text3) {
322
- mkdirSync(dirname2(path), { recursive: true });
369
+ mkdirSync2(dirname3(path), { recursive: true });
323
370
  const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
324
371
  writeFileSync(temporary, text3, { mode: 384 });
325
- chmodSync(temporary, 384);
372
+ chmodSync2(temporary, 384);
326
373
  renameSync(temporary, path);
327
374
  }
328
375
  function gitignoreEntry(rootDir, path) {
@@ -335,6 +382,108 @@ function displayPath(path, rootDir = process.cwd()) {
335
382
  return rel && !rel.startsWith("..") ? rel : path;
336
383
  }
337
384
 
385
+ // src/auth-guidance.ts
386
+ var ENROL_EVERYTHING = "npx odla-ai device enroll --no-open --wait 600";
387
+ var ENROL_PLATFORM_WIDE = "npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600";
388
+ function machineAuthState(audience, env = process7.env) {
389
+ const device = readDeviceCredential(audience, env);
390
+ if (!device) return { enrolled: false };
391
+ const session = readJsonFile(deviceSessionFile(env));
392
+ const current = session?.platform === audience && session.deviceId === device.deviceId ? session : void 0;
393
+ return {
394
+ enrolled: true,
395
+ ...device.name ? { deviceName: device.name } : {},
396
+ ...current?.appIds ? { appIds: current.appIds } : {},
397
+ ...current?.capabilities ? { capabilities: current.capabilities } : {},
398
+ ...current?.scopes ? { scopes: current.scopes } : {},
399
+ ...current?.deviceExpiresAt ? { lapsesAt: current.deviceExpiresAt } : {}
400
+ };
401
+ }
402
+ function scopeInterruptionNotice(scope, state2) {
403
+ const platformScope = scope.startsWith("platform:");
404
+ 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}"`;
405
+ return [
406
+ `odla: ${reason}.`,
407
+ ` Approve this one now, then end the interruptions with:`,
408
+ ` ${platformScope ? ENROL_PLATFORM_WIDE : ENROL_EVERYTHING}`,
409
+ 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."
410
+ ].join("\n");
411
+ }
412
+ function narrowEnrollmentNotice(envelope) {
413
+ const everyApp = envelope.appIds.includes("*");
414
+ if (everyApp && envelope.capabilities.length > 0 && envelope.scopes.length > 0) return null;
415
+ const missing = [
416
+ everyApp ? null : `only ${envelope.appIds.length} app${envelope.appIds.length === 1 ? "" : "s"} \u2014 a new one will need a new approval`,
417
+ envelope.capabilities.length ? null : "no optional capabilities \u2014 app.manage, crm.read and code.session are not included",
418
+ envelope.scopes.length ? null : "no platform scopes \u2014 runbook edits, config plans and host connect will each ask again"
419
+ ].filter(Boolean);
420
+ return [
421
+ `odla: this is a NARROW enrollment: ${missing.join("; ")}.`,
422
+ " That is a fine choice if you meant it. If you did not, drop the flags:",
423
+ ` ${ENROL_EVERYTHING}`
424
+ ].join("\n");
425
+ }
426
+ function lapseNotice(state2, now = Date.now()) {
427
+ if (!state2.enrolled || !state2.lapsesAt) return null;
428
+ const days = Math.floor((state2.lapsesAt - now) / (24 * 60 * 60 * 1e3));
429
+ if (days < 0) return "this machine's enrollment has lapsed; the next command will ask for approval";
430
+ return `idle for ${days} more day${days === 1 ? "" : "s"} before this machine needs approving again (using it resets the clock)`;
431
+ }
432
+
433
+ // src/cached-credential.ts
434
+ var noted = null;
435
+ function noteCachedCredential(tokenFile) {
436
+ noted = tokenFile;
437
+ }
438
+ function isCredentialRejection(error) {
439
+ const message2 = error instanceof Error ? error.message : String(error ?? "");
440
+ return /\((401|403)\)\s*$/.test(message2.trim());
441
+ }
442
+ function explainRejectedCredential(error) {
443
+ const tokenFile = noted;
444
+ if (!tokenFile || !isCredentialRejection(error)) return null;
445
+ noted = null;
446
+ rmSync3(tokenFile, { force: true });
447
+ return [
448
+ "auth: the cached credential was rejected by odla, so it was revoked before its cached expiry.",
449
+ " The usual cause is a newer sign-in for this account: collecting a handshake retires the",
450
+ " principal's other collected credentials, so a second machine supersedes this one.",
451
+ ` Discarded ${tokenFile}; re-run this command to request a fresh approval.`,
452
+ ` To stop needing one: ${ENROL_EVERYTHING}`
453
+ ].join("\n");
454
+ }
455
+
456
+ // src/device-session-cache.ts
457
+ import process8 from "process";
458
+ var SKEW_MS = 6e4;
459
+ async function deviceSessionToken(platformUrl, audience, credential2, doFetch, env = process8.env) {
460
+ const path = deviceSessionFile(env);
461
+ const cached = readJsonFile(path);
462
+ if (cached?.token && cached.platform === audience && cached.deviceId === credential2.deviceId && (cached.expiresAt ?? 0) > Date.now() + SKEW_MS) return cached;
463
+ const minted = await mintDeviceSession(platformUrl, credential2, doFetch);
464
+ const session = {
465
+ ...minted,
466
+ platform: audience,
467
+ ...credential2.deviceId ? { deviceId: credential2.deviceId } : {}
468
+ };
469
+ writePrivateJson(path, session);
470
+ return session;
471
+ }
472
+
473
+ // src/machine-identity.ts
474
+ import process9 from "process";
475
+ function readMachineIdentity(audience, env = process9.env) {
476
+ const stored = readJsonFile(identityFile(env));
477
+ if (!stored || typeof stored.email !== "string" || !stored.email) return null;
478
+ return stored.platform === audience ? { platform: audience, email: stored.email } : null;
479
+ }
480
+ function rememberMachineIdentity(audience, email, env = process9.env) {
481
+ if (!email) return;
482
+ const existing = readMachineIdentity(audience, env);
483
+ if (existing?.email === email) return;
484
+ writePrivateJson(identityFile(env), { platform: audience, email });
485
+ }
486
+
338
487
  // src/token.ts
339
488
  async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {}) {
340
489
  const audience = platformAudience(cfg.platformUrl);
@@ -343,19 +492,19 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
343
492
  const cached = readJsonFile(cfg.local.tokenFile);
344
493
  if (!grantRequest.forceReview && !grantRequest.freshLogin) {
345
494
  if (options.token) return options.token;
346
- if (process6.env.ODLA_DEV_TOKEN) {
347
- const declared = process6.env.ODLA_DEV_TOKEN_AUDIENCE;
495
+ if (process10.env.ODLA_DEV_TOKEN) {
496
+ const declared = process10.env.ODLA_DEV_TOKEN_AUDIENCE;
348
497
  if (declared) {
349
498
  if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
350
499
  } else if (audience !== "https://odla.ai") {
351
500
  throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
352
501
  }
353
- return process6.env.ODLA_DEV_TOKEN;
502
+ return process10.env.ODLA_DEV_TOKEN;
354
503
  }
355
504
  const device = readDeviceCredential(audience);
356
505
  if (device) {
357
- const session = await mintDeviceSession(cfg.platformUrl, device, doFetch);
358
- out.error(`auth: session minted by this enrolled device (${displayPath(deviceCredentialPath(), cfg.rootDir)})`);
506
+ const session = await deviceSessionToken(cfg.platformUrl, audience, device, doFetch);
507
+ out.error(`auth: session held by this enrolled device (${displayPath(deviceCredentialPath(), cfg.rootDir)})`);
359
508
  return session.token;
360
509
  }
361
510
  if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4 && cachedGrantCovers(cached, grantIntent)) {
@@ -375,7 +524,10 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
375
524
  doFetch,
376
525
  out,
377
526
  audience,
378
- email: handshakeEmail(options.email, cached?.platform === audience ? cached.email : void 0),
527
+ email: handshakeEmail(
528
+ options.email,
529
+ (cached?.platform === audience ? cached.email : void 0) ?? readMachineIdentity(audience)?.email
530
+ ),
379
531
  pendingFile: handshakeFile(cfg),
380
532
  grantIntent
381
533
  };
@@ -392,6 +544,7 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
392
544
  expiresAt
393
545
  });
394
546
  out.error(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
547
+ rememberMachineIdentity(audience, ctx.email);
395
548
  return token;
396
549
  }
397
550
  async function freshHandshake(ctx, waitMs) {
@@ -461,7 +614,7 @@ function stillPending(pending, email) {
461
614
  );
462
615
  }
463
616
  function handshakeEmail(value2, cached) {
464
- const email = (value2 ?? process6.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
617
+ const email = (value2 ?? process10.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
465
618
  if (/@users\.noreply\.github\.com$/i.test(email)) {
466
619
  throw new Error(
467
620
  `"${email}" is a GitHub commit identity, not an odla account email; use --email <signed-in-odla-account> or ODLA_USER_EMAIL`
@@ -492,12 +645,12 @@ function platformAudience(value2) {
492
645
  }
493
646
 
494
647
  // src/secret-input.ts
495
- import process7 from "process";
648
+ import process11 from "process";
496
649
  var MAX_BYTES = 64 * 1024;
497
650
  async function secretInputValue(options, kind = "credential") {
498
651
  if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
499
652
  let value2;
500
- if (options.fromEnv) value2 = process7.env[options.fromEnv];
653
+ if (options.fromEnv) value2 = process11.env[options.fromEnv];
501
654
  else if (options.stdin) value2 = await (options.readStdin ?? (() => readSecretStream(kind)))();
502
655
  else throw new Error(`${kind} input required: use --from-env <NAME> or --stdin; values are never accepted as arguments`);
503
656
  value2 = value2?.replace(/[\r\n]+$/, "");
@@ -505,7 +658,7 @@ async function secretInputValue(options, kind = "credential") {
505
658
  if (new TextEncoder().encode(value2).byteLength > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
506
659
  return value2;
507
660
  }
508
- async function readSecretStream(kind, stream = process7.stdin) {
661
+ async function readSecretStream(kind, stream = process11.stdin) {
509
662
  let value2 = "";
510
663
  for await (const chunk of stream) {
511
664
  value2 += String(chunk);
@@ -515,9 +668,8 @@ async function readSecretStream(kind, stream = process7.stdin) {
515
668
  }
516
669
 
517
670
  // src/admin-ai-auth.ts
518
- import { existsSync as existsSync3 } from "fs";
519
- import { join as join3 } from "path";
520
- import process8 from "process";
671
+ import { join as join4 } from "path";
672
+ import process12 from "process";
521
673
  import { requestToken as requestToken2 } from "@odla-ai/db";
522
674
  async function getScopedPlatformToken(options) {
523
675
  return resolveAdminPlatformToken(options);
@@ -525,7 +677,7 @@ async function getScopedPlatformToken(options) {
525
677
  async function resolveAdminPlatformToken(options) {
526
678
  const audience = platformAudience(options.platform);
527
679
  if (options.token) return options.token;
528
- const fromEnv = process8.env.ODLA_ADMIN_TOKEN;
680
+ const fromEnv = process12.env.ODLA_ADMIN_TOKEN;
529
681
  if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
530
682
  return scopedToken(
531
683
  audience,
@@ -537,7 +689,7 @@ async function resolveAdminPlatformToken(options) {
537
689
  }
538
690
  function audienceBoundEnvToken(token, platform) {
539
691
  const audience = platformAudience(platform);
540
- const declared = process8.env.ODLA_ADMIN_TOKEN_AUDIENCE;
692
+ const declared = process12.env.ODLA_ADMIN_TOKEN_AUDIENCE;
541
693
  if (declared) {
542
694
  if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
543
695
  } else if (audience !== "https://odla.ai") {
@@ -564,15 +716,28 @@ var SCOPE_PURPOSE = {
564
716
  };
565
717
  async function scopedToken(platform, scope, options, doFetch, out) {
566
718
  const audience = platformAudience(platform);
567
- const rootDir = options.rootDir ?? process8.cwd();
568
- const tokenFile = options.tokenFile ?? join3(rootDir, ".odla/admin-token.local.json");
719
+ const rootDir = options.rootDir ?? process12.cwd();
720
+ const tokenFile = options.tokenFile ?? scopedTokenFile();
721
+ adoptRepoLocalCache(join4(rootDir, ".odla/admin-token.local.json"), tokenFile, out);
722
+ const device = readDeviceCredential(audience);
723
+ if (device && options.cache !== false) {
724
+ const session = await deviceSessionToken(platform, audience, device, doFetch);
725
+ if (session.scopes?.includes(scope)) {
726
+ out.error(`auth: ${scope} held by this enrolled device`);
727
+ return session.token;
728
+ }
729
+ }
569
730
  const cache2 = options.cache === false ? null : readJsonFile(tokenFile);
570
731
  const cached = cache2?.platform === audience ? cache2.tokens?.[scope] : void 0;
571
732
  if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
572
733
  out.error(`auth: using cached ${scope} grant (${tokenFile})`);
573
734
  return cached.token;
574
735
  }
575
- const email = handshakeEmail(options.email, cache2?.platform === audience ? cache2.email : void 0);
736
+ out.error(scopeInterruptionNotice(scope, machineAuthState(audience)));
737
+ const email = handshakeEmail(
738
+ options.email,
739
+ (cache2?.platform === audience ? cache2.email : void 0) ?? readMachineIdentity(audience)?.email
740
+ );
576
741
  const { token, expiresAt } = await requestToken2({
577
742
  endpoint: audience,
578
743
  email,
@@ -592,8 +757,8 @@ async function scopedToken(platform, scope, options, doFetch, out) {
592
757
  if (options.cache !== false) {
593
758
  const tokens = cache2?.platform === audience ? { ...cache2.tokens ?? {} } : {};
594
759
  tokens[scope] = { token, expiresAt };
595
- if (existsSync3(join3(rootDir, ".git"))) ensureGitignore(rootDir, [tokenFile]);
596
760
  writePrivateJson(tokenFile, { platform: audience, email, tokens });
761
+ rememberMachineIdentity(audience, email);
597
762
  out.error(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
598
763
  } else {
599
764
  out.error(`auth: ${scope} grant is in memory only; its credential record remains in odla-ai/db`);
@@ -769,7 +934,7 @@ function isRecord2(value2) {
769
934
 
770
935
  // src/admin-ai.ts
771
936
  async function adminAi(options) {
772
- const platform = platformAudience(options.platform ?? process9.env.ODLA_PLATFORM ?? "https://odla.ai");
937
+ const platform = platformAudience(options.platform ?? process13.env.ODLA_PLATFORM ?? "https://odla.ai");
773
938
  const doFetch = options.fetch ?? fetch;
774
939
  const out = options.stdout ?? console;
775
940
  const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
@@ -1085,12 +1250,12 @@ async function adminSpend(parsed, ctx) {
1085
1250
 
1086
1251
  // src/operator-context.ts
1087
1252
  import { existsSync as existsSync6 } from "fs";
1088
- import { join as join5, resolve as resolve4 } from "path";
1089
- import process11 from "process";
1253
+ import { join as join6, resolve as resolve4 } from "path";
1254
+ import process15 from "process";
1090
1255
 
1091
1256
  // src/config.ts
1092
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1093
- import { dirname as dirname3, isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
1257
+ import { existsSync as existsSync4, rmSync as rmSync4, readFileSync as readFileSync3 } from "fs";
1258
+ import { dirname as dirname4, isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
1094
1259
  import { pathToFileURL } from "url";
1095
1260
  import { appServiceDefinition, appServiceIds } from "@odla-ai/apps";
1096
1261
 
@@ -1494,13 +1659,17 @@ var DEFAULT_ENVS = ["dev"];
1494
1659
  var DEFAULT_SERVICES = ["db", "ai"];
1495
1660
  var configImportSerial = 0;
1496
1661
  var GOOGLE_CALENDAR_EVENTS_SCOPE = "https://www.googleapis.com/auth/calendar.events";
1662
+ var stderr = { error: (message2) => {
1663
+ process.stderr.write(`${message2}
1664
+ `);
1665
+ } };
1497
1666
  async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1498
1667
  const resolved = resolve2(configPath);
1499
1668
  if (!existsSync4(resolved)) {
1500
1669
  throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
1501
1670
  }
1502
1671
  const raw = await loadConfigModule(resolved);
1503
- const rootDir = dirname3(resolved);
1672
+ const rootDir = dirname4(resolved);
1504
1673
  validateRawConfig(raw, resolved);
1505
1674
  const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
1506
1675
  const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
@@ -1510,11 +1679,14 @@ async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1510
1679
  validateCalendarConfig(raw, unique3([...envs, ...options.additionalEnvs ?? []]), services, resolved);
1511
1680
  validateMonitoringConfig(raw, unique3([...envs, ...options.additionalEnvs ?? []]), services, resolved);
1512
1681
  const local = {
1513
- tokenFile: resolve2(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
1514
- credentialsFile: resolve2(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
1682
+ tokenFile: raw.local?.tokenFile ? resolve2(rootDir, raw.local.tokenFile) : appTokenFile(raw.app.id),
1683
+ credentialsFile: raw.local?.credentialsFile ? resolve2(rootDir, raw.local.credentialsFile) : appCredentialsFile(raw.app.id),
1515
1684
  devVarsFile: resolve2(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
1516
1685
  gitignore: raw.local?.gitignore ?? true
1517
1686
  };
1687
+ adoptRepoLocalCache(resolve2(rootDir, ".odla/dev-token.json"), local.tokenFile, stderr);
1688
+ adoptRepoLocalCache(resolve2(rootDir, ".odla/credentials.local.json"), local.credentialsFile, stderr);
1689
+ rmSync4(resolve2(rootDir, ".odla/handshake.local.json"), { force: true });
1518
1690
  return {
1519
1691
  ...raw,
1520
1692
  configPath: resolved,
@@ -1623,17 +1795,17 @@ function unique3(values) {
1623
1795
 
1624
1796
  // src/operator-profiles.ts
1625
1797
  import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1626
- import { homedir as homedir2 } from "os";
1627
- import { dirname as dirname4, join as join4, resolve as resolve3 } from "path";
1628
- import process10 from "process";
1798
+ import { homedir as homedir3 } from "os";
1799
+ import { dirname as dirname5, join as join5, resolve as resolve3 } from "path";
1800
+ import process14 from "process";
1629
1801
  function operatorProfileFile() {
1630
1802
  return resolve3(
1631
- clean(process10.env.ODLA_CONTEXT_FILE) ?? join4(homedir2(), ".odla", "contexts.json")
1803
+ clean(process14.env.ODLA_CONTEXT_FILE) ?? join5(homedir3(), ".odla", "contexts.json")
1632
1804
  );
1633
1805
  }
1634
1806
  function resolveOperatorProfile(parsed) {
1635
1807
  const fromFlag = clean(stringOpt(parsed.options.context));
1636
- const fromEnvironment = clean(process10.env.ODLA_CONTEXT);
1808
+ const fromEnvironment = clean(process14.env.ODLA_CONTEXT);
1637
1809
  const name = fromFlag ?? fromEnvironment ?? null;
1638
1810
  const file = operatorProfileFile();
1639
1811
  if (!name) {
@@ -1673,10 +1845,10 @@ function removeOperatorProfile(name, file = operatorProfileFile()) {
1673
1845
  return true;
1674
1846
  }
1675
1847
  function operatorCredentialFiles(selection) {
1676
- const base = selection.name ? join4(dirname4(selection.file), "profiles", selection.name) : join4(homedir2(), ".odla");
1848
+ const base = selection.name ? join5(dirname5(selection.file), "profiles", selection.name) : join5(homedir3(), ".odla");
1677
1849
  return {
1678
- developer: join4(base, "dev-token.json"),
1679
- scoped: join4(base, "admin-token.local.json")
1850
+ developer: join5(base, "dev-token.json"),
1851
+ scoped: join5(base, "admin-token.local.json")
1680
1852
  };
1681
1853
  }
1682
1854
  function assertOperatorName(value2, label) {
@@ -1762,13 +1934,13 @@ async function resolveOperatorContext(parsed, options = {}) {
1762
1934
  }
1763
1935
  const loaded = hasConfig ? await loadProjectConfig(configArgument) : void 0;
1764
1936
  const platformFlag = clean2(stringOpt(parsed.options.platform));
1765
- const platformEnvironment = clean2(process11.env.ODLA_PLATFORM_URL);
1937
+ const platformEnvironment = clean2(process15.env.ODLA_PLATFORM_URL);
1766
1938
  const platformValue = platformAudience(
1767
1939
  platformFlag ?? platformEnvironment ?? profile.value?.platform ?? loaded?.platformUrl ?? DEFAULT_PLATFORM2
1768
1940
  );
1769
1941
  const platformSource = platformFlag ? "flag" : platformEnvironment ? "environment" : profile.value ? "profile" : loaded ? "config" : "default";
1770
1942
  const appFlag = clean2(stringOpt(parsed.options.app));
1771
- const appEnvironment = clean2(process11.env.ODLA_APP_ID);
1943
+ const appEnvironment = clean2(process15.env.ODLA_APP_ID);
1772
1944
  const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
1773
1945
  const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
1774
1946
  if (appValue) {
@@ -1782,16 +1954,16 @@ async function resolveOperatorContext(parsed, options = {}) {
1782
1954
  );
1783
1955
  }
1784
1956
  const envFlag = clean2(stringOpt(parsed.options.env));
1785
- const envEnvironment = clean2(process11.env.ODLA_ENV);
1957
+ const envEnvironment = clean2(process15.env.ODLA_ENV);
1786
1958
  const environmentValue = envFlag ?? envEnvironment ?? profile.value?.environment ?? options.defaultEnvironment ?? null;
1787
1959
  const environmentSource = envFlag ? "flag" : envEnvironment ? "environment" : profile.value?.environment ? "profile" : options.defaultEnvironment ? "default" : "unresolved";
1788
1960
  if (environmentValue) {
1789
1961
  assertOperatorName(environmentValue, "environment");
1790
1962
  }
1791
- const rootDir = loaded?.rootDir ?? process11.cwd();
1963
+ const rootDir = loaded?.rootDir ?? process15.cwd();
1792
1964
  const profileCredentials = operatorCredentialFiles(profile);
1793
- const tokenFile = clean2(process11.env.ODLA_DEV_TOKEN_FILE) ? resolve4(process11.env.ODLA_DEV_TOKEN_FILE) : profile.name ? profileCredentials.developer : loaded?.local.tokenFile ?? profileCredentials.developer;
1794
- const scopedTokenFile = clean2(process11.env.ODLA_ADMIN_TOKEN_FILE) ? resolve4(process11.env.ODLA_ADMIN_TOKEN_FILE) : profile.name ? profileCredentials.scoped : loaded ? join5(loaded.rootDir, ".odla", "admin-token.local.json") : profileCredentials.scoped;
1965
+ const tokenFile = clean2(process15.env.ODLA_DEV_TOKEN_FILE) ? resolve4(process15.env.ODLA_DEV_TOKEN_FILE) : profile.name ? profileCredentials.developer : loaded?.local.tokenFile ?? profileCredentials.developer;
1966
+ const scopedTokenFile2 = clean2(process15.env.ODLA_ADMIN_TOKEN_FILE) ? resolve4(process15.env.ODLA_ADMIN_TOKEN_FILE) : profile.name ? profileCredentials.scoped : loaded ? join6(loaded.rootDir, ".odla", "admin-token.local.json") : profileCredentials.scoped;
1795
1967
  const cfg = loaded ? {
1796
1968
  ...loaded,
1797
1969
  platformUrl: platformValue,
@@ -1813,8 +1985,8 @@ async function resolveOperatorContext(parsed, options = {}) {
1813
1985
  services: [],
1814
1986
  local: {
1815
1987
  tokenFile,
1816
- credentialsFile: join5(rootDir, ".odla", "credentials.local.json"),
1817
- devVarsFile: join5(rootDir, ".dev.vars"),
1988
+ credentialsFile: join6(rootDir, ".odla", "credentials.local.json"),
1989
+ devVarsFile: join6(rootDir, ".dev.vars"),
1818
1990
  gitignore: true
1819
1991
  }
1820
1992
  };
@@ -1838,7 +2010,7 @@ async function resolveOperatorContext(parsed, options = {}) {
1838
2010
  },
1839
2011
  credentials: {
1840
2012
  developerTokenFile: tokenFile,
1841
- scopedTokenFile
2013
+ scopedTokenFile: scopedTokenFile2
1842
2014
  }
1843
2015
  };
1844
2016
  }
@@ -1936,7 +2108,7 @@ async function adminCommand(parsed, deps = {}) {
1936
2108
  }
1937
2109
 
1938
2110
  // src/auth-command.ts
1939
- import process12 from "process";
2111
+ import process16 from "process";
1940
2112
 
1941
2113
  // src/whoami-command.ts
1942
2114
  var text2 = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
@@ -2072,6 +2244,7 @@ async function whoamiCommand(parsed, deps = {}) {
2072
2244
  } else {
2073
2245
  out.log("projects: (none \u2014 every pm and discuss call will be refused)");
2074
2246
  }
2247
+ printMachineBlock(cfg.platformUrl, out);
2075
2248
  if (!identity.admin) {
2076
2249
  if (identity.scopes.includes("platform:runbook:write")) {
2077
2250
  out.log("\nThis exact scope can read and edit all platform runbook content.");
@@ -2082,6 +2255,21 @@ async function whoamiCommand(parsed, deps = {}) {
2082
2255
  }
2083
2256
  }
2084
2257
  }
2258
+ function printMachineBlock(platformUrl, out) {
2259
+ const state2 = machineAuthState(platformAudience(platformUrl));
2260
+ if (!state2.enrolled) {
2261
+ out.log("\nmachine: not enrolled \u2014 every privileged command needs its own browser approval.");
2262
+ out.log(` End that with:
2263
+ ${ENROL_EVERYTHING}`);
2264
+ return;
2265
+ }
2266
+ const reach = state2.appIds?.includes("*") ? "every app you own" : state2.appIds?.join(", ");
2267
+ out.log(`
2268
+ machine: enrolled${state2.deviceName ? ` as "${state2.deviceName}"` : ""}${reach ? ` for ${reach}` : ""}`);
2269
+ if (state2.scopes?.length) out.log(` carrying ${state2.scopes.join(", ")}`);
2270
+ const lapse = lapseNotice(state2);
2271
+ if (lapse) out.log(` ${lapse}`);
2272
+ }
2085
2273
 
2086
2274
  // src/auth-command.ts
2087
2275
  async function authCommand(parsed, deps = {}) {
@@ -2106,12 +2294,17 @@ async function authCommand(parsed, deps = {}) {
2106
2294
  const { cfg } = context;
2107
2295
  const out = deps.stdout ?? console;
2108
2296
  const doFetch = deps.fetch ?? fetch;
2109
- const email = stringOpt(parsed.options.email) ?? process12.env.ODLA_USER_EMAIL?.trim();
2297
+ const email = stringOpt(parsed.options.email) ?? process16.env.ODLA_USER_EMAIL?.trim();
2110
2298
  if (!email) {
2111
2299
  throw new Error(
2112
2300
  "auth login requires --email <odla-account> or ODLA_USER_EMAIL; confirm the signed-in odla email instead of using git or GitHub identity"
2113
2301
  );
2114
2302
  }
2303
+ if (!machineAuthState(platformAudience(cfg.platformUrl)).enrolled) {
2304
+ out.error("odla: this machine is not enrolled, so this approval buys one project until it lapses.");
2305
+ out.error(` For one approval that covers every app you own, with no repeats:
2306
+ ${ENROL_EVERYTHING}`);
2307
+ }
2115
2308
  const token = await getDeveloperToken(
2116
2309
  cfg,
2117
2310
  {
@@ -2471,7 +2664,7 @@ async function appCommand(parsed, dependencies = {}) {
2471
2664
 
2472
2665
  // src/brand-command.ts
2473
2666
  import { mkdir, readFile, writeFile } from "fs/promises";
2474
- import { dirname as dirname5, resolve as resolve5 } from "path";
2667
+ import { dirname as dirname6, resolve as resolve5 } from "path";
2475
2668
 
2476
2669
  // src/brand-design-unpack.ts
2477
2670
  import { gunzipSync } from "zlib";
@@ -2586,7 +2779,7 @@ async function readBundle(source, deps) {
2586
2779
  async function writeAll(result, outDir) {
2587
2780
  for (const file of result.files) {
2588
2781
  const target = resolve5(outDir, file.path);
2589
- await mkdir(dirname5(target), { recursive: true });
2782
+ await mkdir(dirname6(target), { recursive: true });
2590
2783
  await writeFile(target, file.bytes);
2591
2784
  }
2592
2785
  }
@@ -3152,7 +3345,7 @@ import {
3152
3345
  AppsError,
3153
3346
  createAppsClient
3154
3347
  } from "@odla-ai/apps";
3155
- import { join as join6 } from "path";
3348
+ import { join as join7 } from "path";
3156
3349
 
3157
3350
  // src/config-operation-error.ts
3158
3351
  var ConfigOperationCommandError = class extends Error {
@@ -3577,7 +3770,7 @@ async function operationClient(cfg, options, purpose) {
3577
3770
  platform: cfg.platformUrl,
3578
3771
  scope: "app:config:write",
3579
3772
  token: options.token,
3580
- tokenFile: join6(cfg.rootDir, ".odla", "admin-token.local.json"),
3773
+ tokenFile: join7(cfg.rootDir, ".odla", "admin-token.local.json"),
3581
3774
  rootDir: cfg.rootDir,
3582
3775
  email: options.email,
3583
3776
  open: options.open,
@@ -3632,7 +3825,7 @@ function record4(value2) {
3632
3825
 
3633
3826
  // src/config-reconcile-command.ts
3634
3827
  import { createAppsClient as createAppsClient2, studioAppSettingsPath } from "@odla-ai/apps";
3635
- import { join as join7 } from "path";
3828
+ import { join as join8 } from "path";
3636
3829
 
3637
3830
  // src/config-reconcile.ts
3638
3831
  import { appServiceIds as appServiceIds2, orderAppServices as orderAppServices2 } from "@odla-ai/apps";
@@ -3928,7 +4121,7 @@ async function inspectConfig(options) {
3928
4121
  platform: cfg.platformUrl,
3929
4122
  scope: "app:config:read",
3930
4123
  token: options.token,
3931
- tokenFile: join7(cfg.rootDir, ".odla", "admin-token.local.json"),
4124
+ tokenFile: join8(cfg.rootDir, ".odla", "admin-token.local.json"),
3932
4125
  rootDir: cfg.rootDir,
3933
4126
  email: options.email,
3934
4127
  open: options.open,
@@ -4061,26 +4254,26 @@ function quoteArg2(value2) {
4061
4254
  // src/doctor-checks.ts
4062
4255
  import { execFileSync } from "child_process";
4063
4256
  import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
4064
- import { join as join9, resolve as resolve6 } from "path";
4257
+ import { join as join10, resolve as resolve6 } from "path";
4065
4258
 
4066
4259
  // src/wrangler.ts
4067
4260
  import { spawn as spawn2 } from "child_process";
4068
4261
  import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
4069
- import { join as join8 } from "path";
4262
+ import { join as join9 } from "path";
4070
4263
  var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
4071
4264
  const child = spawn2(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
4072
4265
  let stdout = "";
4073
- let stderr = "";
4266
+ let stderr2 = "";
4074
4267
  child.stdout.on("data", (chunk) => stdout += chunk.toString());
4075
- child.stderr.on("data", (chunk) => stderr += chunk.toString());
4268
+ child.stderr.on("data", (chunk) => stderr2 += chunk.toString());
4076
4269
  child.on("error", reject);
4077
- child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout, stderr }));
4270
+ child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout, stderr: stderr2 }));
4078
4271
  child.stdin.end(opts?.input ?? "");
4079
4272
  });
4080
4273
  var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
4081
4274
  function findWranglerConfig(rootDir) {
4082
4275
  for (const name of WRANGLER_CONFIG_FILES) {
4083
- const path = join8(rootDir, name);
4276
+ const path = join9(rootDir, name);
4084
4277
  if (existsSync7(path)) return path;
4085
4278
  }
4086
4279
  return null;
@@ -4231,21 +4424,21 @@ function wranglerWarnings(rootDir) {
4231
4424
  const blocks = [{ label: "", block: config }];
4232
4425
  const envs = config.env;
4233
4426
  if (envs && typeof envs === "object") {
4234
- for (const [name, block] of Object.entries(envs)) {
4235
- if (block && typeof block === "object") blocks.push({ label: `env.${name}.`, block });
4427
+ for (const [name, block2] of Object.entries(envs)) {
4428
+ if (block2 && typeof block2 === "object") blocks.push({ label: `env.${name}.`, block: block2 });
4236
4429
  }
4237
4430
  }
4238
- for (const { label, block } of blocks) {
4239
- const assets = block.assets;
4431
+ for (const { label, block: block2 } of blocks) {
4432
+ const assets = block2.assets;
4240
4433
  if (assets?.directory) {
4241
4434
  const dir = resolve6(rootDir, assets.directory);
4242
4435
  if (dir === resolve6(rootDir)) {
4243
4436
  warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
4244
- } else if (existsSync8(join9(dir, "node_modules"))) {
4437
+ } else if (existsSync8(join10(dir, "node_modules"))) {
4245
4438
  warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
4246
4439
  }
4247
4440
  }
4248
- const vars = block.vars;
4441
+ const vars = block2.vars;
4249
4442
  if (vars && typeof vars === "object") {
4250
4443
  for (const [name, value2] of Object.entries(vars)) {
4251
4444
  if (name === "ODLA_API_KEY" || name === "ODLA_O11Y_TOKEN" || typeof value2 === "string" && looksSecret(value2)) {
@@ -4306,7 +4499,7 @@ function calendarProjectWarnings(rootDir) {
4306
4499
  }
4307
4500
  function readPackageJson(rootDir) {
4308
4501
  try {
4309
- return JSON.parse(readFileSync8(join9(rootDir, "package.json"), "utf8"));
4502
+ return JSON.parse(readFileSync8(join10(rootDir, "package.json"), "utf8"));
4310
4503
  } catch {
4311
4504
  return null;
4312
4505
  }
@@ -4600,8 +4793,8 @@ function harnessOption(value2, flag) {
4600
4793
  }
4601
4794
 
4602
4795
  // src/init.ts
4603
- import { existsSync as existsSync9, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
4604
- import { dirname as dirname6, resolve as resolve7 } from "path";
4796
+ import { existsSync as existsSync9, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
4797
+ import { dirname as dirname7, resolve as resolve7 } from "path";
4605
4798
  import { appServiceDefinition as appServiceDefinition3, appServiceIds as appServiceIds3 } from "@odla-ai/apps";
4606
4799
  function initProject(options) {
4607
4800
  const out = options.stdout ?? console;
@@ -4623,9 +4816,9 @@ function initProject(options) {
4623
4816
  }
4624
4817
  }
4625
4818
  const aiProvider = options.aiProvider;
4626
- mkdirSync2(dirname6(configPath), { recursive: true });
4627
- mkdirSync2(resolve7(rootDir, "src/odla"), { recursive: true });
4628
- mkdirSync2(resolve7(rootDir, ".odla"), { recursive: true });
4819
+ mkdirSync3(dirname7(configPath), { recursive: true });
4820
+ mkdirSync3(resolve7(rootDir, "src/odla"), { recursive: true });
4821
+ mkdirSync3(resolve7(rootDir, ".odla"), { recursive: true });
4629
4822
  writeFileSync2(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
4630
4823
  writeIfMissing(resolve7(rootDir, "src/odla/schema.mjs"), schemaTemplate());
4631
4824
  writeIfMissing(resolve7(rootDir, "src/odla/rules.mjs"), rulesTemplate());
@@ -4695,8 +4888,10 @@ ${calendar}
4695
4888
  // prod: "https://example.com",
4696
4889
  },
4697
4890
  local: {
4698
- tokenFile: ".odla/dev-token.json",
4699
- credentialsFile: ".odla/credentials.local.json",
4891
+ // Credentials live in ~/.odla, per machine, so every worktree of this app
4892
+ // shares one approval instead of asking for its own. Pinning tokenFile or
4893
+ // credentialsFile here still works and still overrides that \u2014 it just puts
4894
+ // this checkout back on its own island.
4700
4895
  devVarsFile: ".dev.vars",
4701
4896
  },
4702
4897
  };
@@ -4932,9 +5127,9 @@ function printReport(report5, out) {
4932
5127
  }
4933
5128
 
4934
5129
  // src/skill.ts
4935
- import { existsSync as existsSync10, lstatSync, mkdirSync as mkdirSync3, readFileSync as readFileSync9, readdirSync, writeFileSync as writeFileSync3 } from "fs";
4936
- import { homedir as homedir3 } from "os";
4937
- import { dirname as dirname7, isAbsolute as isAbsolute3, join as join10, relative as relative2, resolve as resolve8, sep } from "path";
5130
+ import { existsSync as existsSync10, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync9, readdirSync, writeFileSync as writeFileSync3 } from "fs";
5131
+ import { homedir as homedir4 } from "os";
5132
+ import { dirname as dirname8, isAbsolute as isAbsolute3, join as join11, relative as relative2, resolve as resolve8, sep } from "path";
4938
5133
  import { fileURLToPath } from "url";
4939
5134
 
4940
5135
  // src/skill-adapters.ts
@@ -5034,7 +5229,7 @@ function installSkill(options = {}) {
5034
5229
  if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
5035
5230
  const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
5036
5231
  const root = resolve8(options.dir ?? process.cwd());
5037
- const home = resolve8(options.homeDir ?? homedir3());
5232
+ const home = resolve8(options.homeDir ?? homedir4());
5038
5233
  const plans = /* @__PURE__ */ new Map();
5039
5234
  const targets = /* @__PURE__ */ new Map();
5040
5235
  const rememberTarget = (harness, target) => {
@@ -5048,48 +5243,48 @@ function installSkill(options = {}) {
5048
5243
  plans.set(target, { target, content: content2, boundary, managedMerge });
5049
5244
  };
5050
5245
  const planSkillTree = (targetDir2, boundary = root) => {
5051
- for (const rel of files) plan(join10(targetDir2, rel), readFileSync9(join10(sourceDir, rel), "utf8"), false, boundary);
5246
+ for (const rel of files) plan(join11(targetDir2, rel), readFileSync9(join11(sourceDir, rel), "utf8"), false, boundary);
5052
5247
  };
5053
5248
  let targetDir;
5054
5249
  if (options.global) {
5055
- const claudeRoot = join10(home, ".claude", "skills");
5056
- const codexRoot = resolve8(options.codexHomeDir ?? process.env.CODEX_HOME ?? join10(home, ".codex"), "skills");
5250
+ const claudeRoot = join11(home, ".claude", "skills");
5251
+ const codexRoot = resolve8(options.codexHomeDir ?? process.env.CODEX_HOME ?? join11(home, ".codex"), "skills");
5057
5252
  targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
5058
5253
  for (const harness of harnesses) {
5059
5254
  const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
5060
- planSkillTree(skillRoot, harness === "claude" ? home : dirname7(dirname7(codexRoot)));
5255
+ planSkillTree(skillRoot, harness === "claude" ? home : dirname8(dirname8(codexRoot)));
5061
5256
  rememberTarget(harness, skillRoot);
5062
5257
  }
5063
5258
  } else {
5064
- const sharedRoot = join10(root, ".agents", "skills");
5259
+ const sharedRoot = join11(root, ".agents", "skills");
5065
5260
  planSkillTree(sharedRoot);
5066
- const claudeRoot = join10(root, ".claude", "skills");
5261
+ const claudeRoot = join11(root, ".claude", "skills");
5067
5262
  targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
5068
5263
  for (const harness of harnesses) rememberTarget(harness, sharedRoot);
5069
5264
  if (harnesses.includes("claude")) {
5070
5265
  for (const skill of skillNames(files)) {
5071
- const canonical2 = readFileSync9(join10(sourceDir, skill, "SKILL.md"), "utf8");
5072
- plan(join10(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical2));
5266
+ const canonical2 = readFileSync9(join11(sourceDir, skill, "SKILL.md"), "utf8");
5267
+ plan(join11(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical2));
5073
5268
  }
5074
5269
  rememberTarget("claude", claudeRoot);
5075
5270
  }
5076
5271
  if (harnesses.includes("cursor")) {
5077
- const cursorRule = join10(root, ".cursor", "rules", "odla.mdc");
5272
+ const cursorRule = join11(root, ".cursor", "rules", "odla.mdc");
5078
5273
  plan(cursorRule, CURSOR_RULE);
5079
5274
  rememberTarget("cursor", cursorRule);
5080
5275
  }
5081
5276
  if (harnesses.includes("agents")) {
5082
- const agentsFile = join10(root, "AGENTS.md");
5277
+ const agentsFile = join11(root, "AGENTS.md");
5083
5278
  plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
5084
5279
  rememberTarget("agents", agentsFile);
5085
5280
  }
5086
5281
  if (harnesses.includes("copilot")) {
5087
- const copilotFile = join10(root, ".github", "copilot-instructions.md");
5282
+ const copilotFile = join11(root, ".github", "copilot-instructions.md");
5088
5283
  plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
5089
5284
  rememberTarget("copilot", copilotFile);
5090
5285
  }
5091
5286
  if (harnesses.includes("gemini")) {
5092
- const geminiFile = join10(root, "GEMINI.md");
5287
+ const geminiFile = join11(root, "GEMINI.md");
5093
5288
  plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
5094
5289
  rememberTarget("gemini", geminiFile);
5095
5290
  }
@@ -5125,7 +5320,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
5125
5320
  }
5126
5321
  for (const file of plans.values()) {
5127
5322
  if (!existsSync10(file.target) || readFileSync9(file.target, "utf8") !== file.content) {
5128
- mkdirSync3(dirname7(file.target), { recursive: true });
5323
+ mkdirSync4(dirname8(file.target), { recursive: true });
5129
5324
  writeFileSync3(file.target, file.content);
5130
5325
  }
5131
5326
  }
@@ -5164,10 +5359,10 @@ function normalizeHarnesses(values, global) {
5164
5359
  }
5165
5360
  return expanded;
5166
5361
  }
5167
- function managedFileContent(path, block, force, boundary) {
5362
+ function managedFileContent(path, block2, force, boundary) {
5168
5363
  const symlink = symlinkedComponent(boundary, path);
5169
5364
  if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
5170
- if (!existsSync10(path)) return `${block}
5365
+ if (!existsSync10(path)) return `${block2}
5171
5366
  `;
5172
5367
  const current = readFileSync9(path, "utf8");
5173
5368
  const start = "<!-- odla-ai agent setup:start -->";
@@ -5179,15 +5374,15 @@ function managedFileContent(path, block, force, boundary) {
5179
5374
  }
5180
5375
  if (startAt === -1) {
5181
5376
  const separator = current.length === 0 || current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n";
5182
- return `${current}${separator}${block}
5377
+ return `${current}${separator}${block2}
5183
5378
  `;
5184
5379
  }
5185
5380
  const afterEnd = endAt + end.length;
5186
5381
  const existing = current.slice(startAt, afterEnd);
5187
- if (existing !== block && !force) {
5382
+ if (existing !== block2 && !force) {
5188
5383
  throw new Error(`odla-managed section modified locally in ${path}; re-run with --force to replace that section`);
5189
5384
  }
5190
- return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
5385
+ return `${current.slice(0, startAt)}${block2}${current.slice(afterEnd)}`;
5191
5386
  }
5192
5387
  function symlinkedComponent(boundary, target) {
5193
5388
  const rel = relative2(boundary, target);
@@ -5196,7 +5391,7 @@ function symlinkedComponent(boundary, target) {
5196
5391
  }
5197
5392
  let current = boundary;
5198
5393
  for (const part of rel.split(sep).filter(Boolean)) {
5199
- current = join10(current, part);
5394
+ current = join11(current, part);
5200
5395
  try {
5201
5396
  if (lstatSync(current).isSymbolicLink()) return current;
5202
5397
  } catch (error) {
@@ -5213,7 +5408,7 @@ function listFiles(dir) {
5213
5408
  const results = [];
5214
5409
  const walk = (current) => {
5215
5410
  for (const entry of readdirSync(current, { withFileTypes: true })) {
5216
- const path = join10(current, entry.name);
5411
+ const path = join11(current, entry.name);
5217
5412
  if (entry.isDirectory()) walk(path);
5218
5413
  else results.push(relative2(dir, path));
5219
5414
  }
@@ -5581,7 +5776,7 @@ var HARNESS_PROTOCOL_VERSION = 1;
5581
5776
  import { execFile, spawn as spawn3 } from "child_process";
5582
5777
  import { constants } from "fs";
5583
5778
  import { access } from "fs/promises";
5584
- import { delimiter, join as join11 } from "path";
5779
+ import { delimiter, join as join12 } from "path";
5585
5780
  import { getgid, getuid } from "process";
5586
5781
  import { mkdir as mkdir2, mkdtemp, realpath, rm, writeFile as writeFile2 } from "fs/promises";
5587
5782
  import { tmpdir } from "os";
@@ -5599,7 +5794,7 @@ function assertPinnedImage(image) {
5599
5794
  async function commandAvailable(engine) {
5600
5795
  for (const directory of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
5601
5796
  try {
5602
- await access(join11(directory, engine), constants.X_OK);
5797
+ await access(join12(directory, engine), constants.X_OK);
5603
5798
  return true;
5604
5799
  } catch {
5605
5800
  }
@@ -5678,7 +5873,7 @@ function allowedWorkspacePath(relativePath) {
5678
5873
  async function gitOutput(cwd, args, maxBytes) {
5679
5874
  const child = spawn22("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], shell: false });
5680
5875
  const stdout = [];
5681
- const stderr = [];
5876
+ const stderr2 = [];
5682
5877
  let bytes = 0;
5683
5878
  child.stdout.on("data", (chunk) => {
5684
5879
  bytes += chunk.byteLength;
@@ -5686,20 +5881,20 @@ async function gitOutput(cwd, args, maxBytes) {
5686
5881
  else stdout.push(chunk);
5687
5882
  });
5688
5883
  child.stderr.on("data", (chunk) => {
5689
- if (stderr.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr.push(chunk);
5884
+ if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
5690
5885
  });
5691
5886
  const code = await new Promise((accept, reject) => {
5692
5887
  child.once("error", reject);
5693
5888
  child.once("exit", accept);
5694
5889
  });
5695
5890
  if (bytes > maxBytes) throw new Error(`git output exceeds ${maxBytes} bytes`);
5696
- if (code !== 0) throw new Error(`git command failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
5891
+ if (code !== 0) throw new Error(`git command failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
5697
5892
  return Buffer.concat(stdout);
5698
5893
  }
5699
5894
  async function gitBlobs(cwd, entries, maxBytes) {
5700
5895
  const child = spawn22("git", ["cat-file", "--batch"], { cwd, stdio: ["pipe", "pipe", "pipe"], shell: false });
5701
5896
  const stdout = [];
5702
- const stderr = [];
5897
+ const stderr2 = [];
5703
5898
  let bytes = 0;
5704
5899
  child.stdout.on("data", (chunk) => {
5705
5900
  bytes += chunk.byteLength;
@@ -5707,7 +5902,7 @@ async function gitBlobs(cwd, entries, maxBytes) {
5707
5902
  else stdout.push(chunk);
5708
5903
  });
5709
5904
  child.stderr.on("data", (chunk) => {
5710
- if (stderr.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr.push(chunk);
5905
+ if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
5711
5906
  });
5712
5907
  child.stdin.end(`${entries.map((entry) => entry.hash).join("\n")}
5713
5908
  `);
@@ -5716,7 +5911,7 @@ async function gitBlobs(cwd, entries, maxBytes) {
5716
5911
  child.once("exit", accept);
5717
5912
  });
5718
5913
  if (bytes > maxBytes + entries.length * 100) throw new Error(`Git tree exceeds ${maxBytes} bytes`);
5719
- if (code !== 0) throw new Error(`git object read failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
5914
+ if (code !== 0) throw new Error(`git object read failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
5720
5915
  const output = Buffer.concat(stdout);
5721
5916
  const blobs = [];
5722
5917
  let offset = 0;
@@ -5813,7 +6008,7 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
5813
6008
  shell: false
5814
6009
  });
5815
6010
  const stdout = [];
5816
- const stderr = [];
6011
+ const stderr2 = [];
5817
6012
  let outputBytes = 0;
5818
6013
  child.stdout.on("data", (chunk) => {
5819
6014
  outputBytes += chunk.byteLength;
@@ -5821,14 +6016,14 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
5821
6016
  else stdout.push(chunk);
5822
6017
  });
5823
6018
  child.stderr.on("data", (chunk) => {
5824
- if (stderr.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr.push(chunk);
6019
+ if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
5825
6020
  });
5826
6021
  const code = await new Promise((accept, reject) => {
5827
6022
  child.once("error", reject);
5828
6023
  child.once("exit", accept);
5829
6024
  });
5830
6025
  if (outputBytes > 8 * 1024 * 1024) throw new Error("git file inventory exceeds 8 MiB");
5831
- if (code !== 0) throw new Error(`git file inventory failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
6026
+ if (code !== 0) throw new Error(`git file inventory failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
5832
6027
  const paths = Buffer.concat(stdout).toString("utf8").split("\0").filter(Boolean).sort();
5833
6028
  if (paths.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
5834
6029
  const root = resolve22(sourceDir);
@@ -5873,7 +6068,7 @@ async function captureGitDiff(root, maxBytes) {
5873
6068
  "workspace"
5874
6069
  ], { cwd: root, stdio: ["ignore", "pipe", "pipe"], shell: false });
5875
6070
  const stdout = [];
5876
- const stderr = [];
6071
+ const stderr2 = [];
5877
6072
  let bytes = 0;
5878
6073
  child.stdout.on("data", (chunk) => {
5879
6074
  bytes += chunk.byteLength;
@@ -5881,7 +6076,7 @@ async function captureGitDiff(root, maxBytes) {
5881
6076
  else stdout.push(chunk);
5882
6077
  });
5883
6078
  child.stderr.on("data", (chunk) => {
5884
- if (stderr.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr.push(chunk);
6079
+ if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
5885
6080
  });
5886
6081
  const code = await new Promise((accept, reject) => {
5887
6082
  child.once("error", reject);
@@ -5889,7 +6084,7 @@ async function captureGitDiff(root, maxBytes) {
5889
6084
  });
5890
6085
  if (bytes > maxBytes) throw new Error(`patch exceeds ${maxBytes} bytes`);
5891
6086
  if (code !== 0 && code !== 1) {
5892
- throw new Error(`git diff failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
6087
+ throw new Error(`git diff failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
5893
6088
  }
5894
6089
  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");
5895
6090
  }
@@ -6297,10 +6492,10 @@ import { randomUUID } from "crypto";
6297
6492
  import { createHash as createHash22, randomUUID as randomUUID2 } from "crypto";
6298
6493
  import { createReadStream } from "fs";
6299
6494
  import { lstat as lstat22 } from "fs/promises";
6300
- import { join as join13 } from "path";
6495
+ import { join as join14 } from "path";
6301
6496
  import { mkdir as mkdir3, mkdtemp as mkdtemp3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
6302
6497
  import { tmpdir as tmpdir3 } from "os";
6303
- import { dirname as dirname9, join as join23, resolve as resolve32, sep as sep23 } from "path";
6498
+ import { dirname as dirname10, join as join23, resolve as resolve32, sep as sep23 } from "path";
6304
6499
  import {
6305
6500
  keepRecentExchanges,
6306
6501
  runAgent
@@ -6696,11 +6891,11 @@ function rollup(graph, kind, options = {}) {
6696
6891
  }
6697
6892
 
6698
6893
  // ../graph/dist/code/index.js
6699
- function dirname8(path) {
6894
+ function dirname9(path) {
6700
6895
  const at = path.lastIndexOf("/");
6701
6896
  return at <= 0 ? "." : path.slice(0, at);
6702
6897
  }
6703
- function join12(base, specifier) {
6898
+ function join13(base, specifier) {
6704
6899
  const parts = [];
6705
6900
  const segments = `${base === "." ? "" : `${base}/`}${specifier}`.split("/");
6706
6901
  for (const segment of segments) {
@@ -6724,7 +6919,7 @@ var BARE_IMPORT = /^\s*import\s*["']([^"']+)["']/gm;
6724
6919
  var isSourcePath = (path) => SOURCE.test(path);
6725
6920
  function resolveImport(fromPath, specifier, known) {
6726
6921
  if (!specifier.startsWith(".")) return null;
6727
- const base = join12(dirname8(fromPath), specifier);
6922
+ const base = join13(dirname9(fromPath), specifier);
6728
6923
  const candidates = [
6729
6924
  base,
6730
6925
  base.replace(/\.js$/, ".ts"),
@@ -7255,13 +7450,13 @@ function gitApply(cwd, patch2, check) {
7255
7450
  stdio: ["pipe", "ignore", "pipe"],
7256
7451
  env: { PATH: process.env.PATH ?? "", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null" }
7257
7452
  });
7258
- let stderr = "";
7453
+ let stderr2 = "";
7259
7454
  child.stderr.setEncoding("utf8");
7260
7455
  child.stderr.on("data", (text3) => {
7261
- if (stderr.length < 4e3) stderr += text3.slice(0, 4e3);
7456
+ if (stderr2.length < 4e3) stderr2 += text3.slice(0, 4e3);
7262
7457
  });
7263
7458
  child.once("error", reject);
7264
- child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(describePatchFailure(patch2, stderr.trim().slice(0, 500)))));
7459
+ child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(describePatchFailure(patch2, stderr2.trim().slice(0, 500)))));
7265
7460
  child.stdin.end(patch2);
7266
7461
  });
7267
7462
  }
@@ -7374,7 +7569,7 @@ function execute(engine, args, name, recipe2, signal) {
7374
7569
  const started = Date.now();
7375
7570
  const child = spawn23(engine, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
7376
7571
  const stdout = [];
7377
- const stderr = [];
7572
+ const stderr2 = [];
7378
7573
  let bytes = 0;
7379
7574
  let outputLimitExceeded = false;
7380
7575
  let timedOut = false;
@@ -7395,7 +7590,7 @@ function execute(engine, args, name, recipe2, signal) {
7395
7590
  else target.push(chunk);
7396
7591
  };
7397
7592
  child.stdout.on("data", collect(stdout));
7398
- child.stderr.on("data", collect(stderr));
7593
+ child.stderr.on("data", collect(stderr2));
7399
7594
  const abort = () => stop("abort");
7400
7595
  signal?.addEventListener("abort", abort, { once: true });
7401
7596
  if (signal?.aborted) abort();
@@ -7411,7 +7606,7 @@ function execute(engine, args, name, recipe2, signal) {
7411
7606
  accept({
7412
7607
  exitCode: code ?? 1,
7413
7608
  stdout: Buffer.concat(stdout).toString("utf8"),
7414
- stderr: Buffer.concat(stderr).toString("utf8"),
7609
+ stderr: Buffer.concat(stderr2).toString("utf8"),
7415
7610
  durationMs: Date.now() - started,
7416
7611
  outputLimitExceeded,
7417
7612
  timedOut
@@ -7527,7 +7722,7 @@ async function inspectArtifacts(workspaceDir, recipe2) {
7527
7722
  const receipts = [];
7528
7723
  for (const artifact of recipe2.expectedArtifacts ?? []) {
7529
7724
  try {
7530
- const path = join13(workspaceDir, artifact.path);
7725
+ const path = join14(workspaceDir, artifact.path);
7531
7726
  const info = await lstat22(path);
7532
7727
  if (!info.isFile() || info.isSymbolicLink()) {
7533
7728
  receipts.push({ artifactId: artifact.id, status: "invalid", bytes: null, digest: null });
@@ -7563,11 +7758,11 @@ function checkedResult(result, maximumOutputBytes) {
7563
7758
  }
7564
7759
  function boundedLogs(result, maximum) {
7565
7760
  const stdout = Buffer.from(result.stdout);
7566
- const stderr = Buffer.from(result.stderr);
7761
+ const stderr2 = Buffer.from(result.stderr);
7567
7762
  const first = stdout.subarray(0, maximum);
7568
7763
  return {
7569
7764
  stdout: first.toString("utf8"),
7570
- stderr: stderr.subarray(0, Math.max(0, maximum - first.byteLength)).toString("utf8")
7765
+ stderr: stderr2.subarray(0, Math.max(0, maximum - first.byteLength)).toString("utf8")
7571
7766
  };
7572
7767
  }
7573
7768
  function digestPolicy(policy) {
@@ -7813,7 +8008,7 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = tmpdir3()) {
7813
8008
  if (bytes > 16 * 1024 * 1024) throw new TypeError("Code source exceeds its byte bound");
7814
8009
  const target = resolve32(sourceDir, file.path);
7815
8010
  if (!target.startsWith(`${resolve32(sourceDir)}${sep23}`)) throw new TypeError("Code source path escapes its root");
7816
- await mkdir3(dirname9(target), { recursive: true });
8011
+ await mkdir3(dirname10(target), { recursive: true });
7817
8012
  await writeFile3(target, file.content, { flag: "wx", mode: 420 });
7818
8013
  }
7819
8014
  for (const reference of snapshot.references ?? []) {
@@ -7828,7 +8023,7 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = tmpdir3()) {
7828
8023
  if (bytes > 80 * 1024 * 1024) throw new TypeError("Code source set exceeds its byte bound");
7829
8024
  const target = resolve32(sourceDir, path);
7830
8025
  if (!target.startsWith(`${resolve32(sourceDir)}${sep23}`)) throw new TypeError("Code reference path escapes its root");
7831
- await mkdir3(dirname9(target), { recursive: true });
8026
+ await mkdir3(dirname10(target), { recursive: true });
7832
8027
  await writeFile3(target, file.content, { flag: "wx", mode: 292 });
7833
8028
  }
7834
8029
  }
@@ -7855,7 +8050,7 @@ async function attachCodeRuntimeReferences(workspace, references) {
7855
8050
  for (const root of [workspace.baselineDir, workspace.workspaceDir]) {
7856
8051
  const target = resolve32(root, path);
7857
8052
  if (!target.startsWith(`${resolve32(root)}${sep23}`)) throw new TypeError("Code reference path escapes its root");
7858
- await mkdir3(dirname9(target), { recursive: true });
8053
+ await mkdir3(dirname10(target), { recursive: true });
7859
8054
  await writeFile3(target, file.content, { flag: "wx", mode: 292 });
7860
8055
  }
7861
8056
  }
@@ -10073,13 +10268,13 @@ async function codeCommand(parsed, dependencies) {
10073
10268
  }
10074
10269
 
10075
10270
  // src/operator-credentials.ts
10076
- import process13 from "process";
10271
+ import process17 from "process";
10077
10272
  function developerTokenStatus(context, parsed, now = Date.now()) {
10078
10273
  const cached = readJsonFile(context.cfg.local.tokenFile);
10079
10274
  const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
10080
10275
  const source = clean3(
10081
10276
  stringOpt(parsed.options.token)
10082
- ) ? "flag" : clean3(process13.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
10277
+ ) ? "flag" : clean3(process17.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
10083
10278
  return {
10084
10279
  source,
10085
10280
  cacheFile: context.cfg.local.tokenFile,
@@ -10273,6 +10468,30 @@ async function credentialCommand(parsed, deps = {}) {
10273
10468
  }
10274
10469
 
10275
10470
  // src/help-usage.ts
10471
+ var AUTH_SECTION = `
10472
+ Enrol this machine once, then stop asking:
10473
+ npx odla-ai device enroll --no-open --wait 600
10474
+ One browser approval, and no flags to remember: this covers every app you
10475
+ own \u2014 including apps you create later \u2014 with every capability that
10476
+ approval can carry. Afterwards every worktree on this machine mints its
10477
+ own short-lived credentials with nobody's attention. The window rolls
10478
+ forward each time you use it, so continuous work never interrupts anyone;
10479
+ only a real gap does. Give the human the printed /studio?code= URL, keep
10480
+ the process alive, and wait on it.
10481
+ Narrow it deliberately with --app <id> or --capability <c>; the CLI then
10482
+ says what that gave up.
10483
+
10484
+ npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600
10485
+ The same thing across all of odla, for weeks. Needs a platform
10486
+ administrator's approval \u2014 an app owner's cannot carry platform scopes.
10487
+
10488
+ npx odla-ai whoami what this machine holds and when it lapses
10489
+ npx odla-ai device list every machine you have enrolled
10490
+
10491
+ Enrollment is the only human decision here. Revoking a machine, purging an app,
10492
+ transferring ownership, and rotating credentials still need a signed-in human in
10493
+ Studio, and no machine credential can do them however wide its approval was.
10494
+ `;
10276
10495
  var USAGE_SECTION = `
10277
10496
  Start here:
10278
10497
  odla-ai runbook ask "<question>" The current procedure, from odla's own
@@ -10311,7 +10530,7 @@ Usage:
10311
10530
  odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]
10312
10531
  odla-ai pm project list [--app <product-id>] [--status <s>] [--json]
10313
10532
  odla-ai pm project add --app <product-id> --name <name> [--description <text>] [--json]
10314
- odla-ai pm project use <project-id> [--json] [saved locally in this worktree]
10533
+ odla-ai pm project use <project-id> [--json] [saved for this app, on this machine]
10315
10534
  odla-ai pm goal list [--app <id>] [--project <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
10316
10535
  odla-ai pm task list [--app <id>] [--column <backlog|ready|doing|review|done>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
10317
10536
  odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
@@ -10403,7 +10622,8 @@ Usage:
10403
10622
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
10404
10623
  odla-ai security run [target] --self --ack-redacted-source
10405
10624
  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]
10406
- odla-ai device enroll [--app <id>[,<id>...]] [--name <label>] [--capability <c>[,<c>...]] [--device-ttl <30d|6w|2y|forever>] [--email <odla-account>] [--no-open] [--json]
10625
+ odla-ai device enroll [--app <id>[,<id>...]|--all-apps] [--capability all|<c>[,<c>...]] [--platform-wide]
10626
+ [--name <label>] [--device-ttl <30d|6w|2y|forever>] [--email <odla-account>] [--no-open] [--wait <seconds>] [--json]
10407
10627
  odla-ai device list [--email <odla-account>] [--json]
10408
10628
  odla-ai device revoke <device-id> [--email <odla-account>] [--json]
10409
10629
  odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
@@ -10417,8 +10637,11 @@ Usage:
10417
10637
 
10418
10638
  // src/help.ts
10419
10639
  function printHelp(output = console) {
10420
- output.log(`odla-ai
10421
- ${USAGE_SECTION}
10640
+ output.log(helpText());
10641
+ }
10642
+ function helpText() {
10643
+ return `odla-ai
10644
+ ${AUTH_SECTION}${USAGE_SECTION}
10422
10645
  Commands:
10423
10646
  auth Start a fresh, exact-project agent authorization for human review.
10424
10647
  The email is the signed-in odla account, never git or GitHub
@@ -10491,8 +10714,9 @@ Commands:
10491
10714
  security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
10492
10715
  pm Project management (via @odla-ai/pm): Products contain Projects;
10493
10716
  projects contain goals, kanban tasks, decisions, and bugs. Use
10494
- "pm project list|add|use" to select worktree-local context, or
10495
- pass --app/--project explicitly. Same device-grant auth as "app".
10717
+ "pm project list|add|use" selects a project for this app on this
10718
+ machine \u2014 every worktree shares it \u2014 or pass --app/--project
10719
+ explicitly. Same device-grant auth as "app".
10496
10720
  Status changes and comments post to each item's @odla-ai/chat
10497
10721
  discussion thread.
10498
10722
  NOTE: "--column ready" is OWNER-ONLY. Creating a task in Ready
@@ -10521,9 +10745,22 @@ Commands:
10521
10745
  platform Read canonical fleet health, releases, provider load/freshness,
10522
10746
  explicit unknowns, and next actions through a read-only grant.
10523
10747
  device Enrol THIS machine once, then stop asking. A human approves the
10524
- enrollment in the browser; from then on this terminal mints its
10525
- own short-lived credentials for the named projects with nobody's
10526
- attention, until the device expires or is revoked.
10748
+ enrollment in the browser; from then on EVERY worktree on this
10749
+ machine mints its own short-lived credentials with nobody's
10750
+ attention, until the device is revoked or goes unused.
10751
+ With no flags it covers every app you own, now and later, with
10752
+ every capability that approval can carry \u2014 so creating an app
10753
+ costs no new approval, and a capability nobody thought to name is
10754
+ not a 403 next week. "--app" or "--capability" narrow it
10755
+ deliberately, and the CLI says what that gave up.
10756
+ "--platform-wide" is the administrator's version, across all of
10757
+ odla.
10758
+ The expiry is a GAP, not a clock: each use rolls it forward, so
10759
+ only going quiet brings a human back into the loop \u2014 which is
10760
+ where anything that changed can be explained.
10761
+ "device list" shows what each machine holds and when it lapses;
10762
+ revoking one takes down every credential it ever minted, and is
10763
+ deliberately a signed-in human's decision in Studio.
10527
10764
  provision Register services, compose integrations, persist credentials, optionally push secrets.
10528
10765
  "provision --live --yes" initializes only the live instance of
10529
10766
  an existing sandbox app and enables every configured service;
@@ -10619,7 +10856,46 @@ Safety:
10619
10856
  Run security plan first to inspect the admin-selected providers, models,
10620
10857
  per-route bounds, credential readiness, retention, no-execution boundary,
10621
10858
  and digest that binds consent to that exact plan.
10622
- `);
10859
+ `;
10860
+ }
10861
+
10862
+ // src/help-command.ts
10863
+ function printCommandHelp(command, output = console) {
10864
+ const lines = helpText().split("\n");
10865
+ const usage = allBlocks(lines, new RegExp(`^ odla-ai ${escapeRe(command)}(\\s|$)`));
10866
+ const prose = block(lines, (line2) => new RegExp(`^ ${escapeRe(command)}\\s\\s+\\S`).test(line2));
10867
+ if (usage.length === 0 && prose.length === 0) {
10868
+ output.log(`odla-ai: no command "${command}". Run "odla-ai help" for all of them.`);
10869
+ return;
10870
+ }
10871
+ output.log([
10872
+ ...prose.length ? [prose.join("\n"), ""] : [],
10873
+ ...usage.length ? ["Usage:", ...usage, ""] : [],
10874
+ AUTH_SECTION.trimEnd()
10875
+ ].join("\n"));
10876
+ }
10877
+ function allBlocks(lines, pattern) {
10878
+ const out = [];
10879
+ for (let i = 0; i < lines.length; i++) {
10880
+ if (!pattern.test(lines[i])) continue;
10881
+ out.push(...block(lines.slice(i), (line2) => line2 === lines[i]));
10882
+ }
10883
+ return out;
10884
+ }
10885
+ function block(lines, starts) {
10886
+ const first = lines.findIndex(starts);
10887
+ if (first === -1) return [];
10888
+ const indent = lines[first].length - lines[first].trimStart().length;
10889
+ const out = [lines[first]];
10890
+ for (const line2 of lines.slice(first + 1)) {
10891
+ if (!line2.trim()) break;
10892
+ if (line2.length - line2.trimStart().length <= indent) break;
10893
+ out.push(line2);
10894
+ }
10895
+ return out;
10896
+ }
10897
+ function escapeRe(value2) {
10898
+ return value2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10623
10899
  }
10624
10900
 
10625
10901
  // src/discuss-principals.ts
@@ -11776,14 +12052,36 @@ async function pmWatch(ctx, parsed) {
11776
12052
  }
11777
12053
 
11778
12054
  // src/pm-project-context.ts
12055
+ import { existsSync as existsSync12, rmSync as rmSync5 } from "fs";
11779
12056
  import { resolve as resolve12 } from "path";
11780
- var pmProjectContextFile = (rootDir) => resolve12(rootDir, ".odla", "pm-project.local.json");
12057
+ var pmProjectContextFile = () => pmContextFile();
11781
12058
  function readPmProjectContext(rootDir) {
11782
- const value2 = readJsonFile(pmProjectContextFile(rootDir));
11783
- return value2 && typeof value2.appId === "string" && typeof value2.projectId === "string" ? value2 : null;
12059
+ adoptLegacySelection(rootDir);
12060
+ const entries = Object.values(readSelections()).filter(isSelection);
12061
+ return entries.sort((a, b) => b.selectedAt.localeCompare(a.selectedAt))[0] ?? null;
11784
12062
  }
11785
12063
  function writePmProjectContext(rootDir, value2) {
11786
- writePrivateJson(pmProjectContextFile(rootDir), { ...value2, selectedAt: (/* @__PURE__ */ new Date()).toISOString() });
12064
+ adoptLegacySelection(rootDir);
12065
+ writePrivateJson(pmProjectContextFile(), {
12066
+ ...readSelections(),
12067
+ [value2.appId]: { ...value2, selectedAt: (/* @__PURE__ */ new Date()).toISOString() }
12068
+ });
12069
+ }
12070
+ function adoptLegacySelection(rootDir) {
12071
+ const legacy = resolve12(rootDir, ".odla", "pm-project.local.json");
12072
+ if (!existsSync12(legacy)) return;
12073
+ const previous = readJsonFile(legacy);
12074
+ rmSync5(legacy, { force: true });
12075
+ if (!isSelection(previous)) return;
12076
+ const selections = readSelections();
12077
+ if (selections[previous.appId]) return;
12078
+ writePrivateJson(pmProjectContextFile(), { ...selections, [previous.appId]: previous });
12079
+ }
12080
+ function readSelections() {
12081
+ return readJsonFile(pmProjectContextFile()) ?? {};
12082
+ }
12083
+ function isSelection(value2) {
12084
+ return !!value2 && typeof value2.appId === "string" && typeof value2.projectId === "string" && typeof value2.selectedAt === "string";
11787
12085
  }
11788
12086
 
11789
12087
  // src/pm-project-actions.ts
@@ -12808,7 +13106,7 @@ function percent(value2) {
12808
13106
  // src/provision.ts
12809
13107
  import { AppsError as AppsError2, createAppsClient as createAppsClient3, orderAppServices as orderAppServices3, tenantIdFor as tenantIdFor6 } from "@odla-ai/apps";
12810
13108
  import { putSecret as putSecret2 } from "@odla-ai/ai";
12811
- import process14 from "process";
13109
+ import process18 from "process";
12812
13110
 
12813
13111
  // src/integration-provision.ts
12814
13112
  import { uuidv7 } from "@odla-ai/db";
@@ -13246,7 +13544,7 @@ async function provision(options) {
13246
13544
  await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
13247
13545
  }
13248
13546
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
13249
- const key = process14.env[cfg.ai.keyEnv];
13547
+ const key = process18.env[cfg.ai.keyEnv];
13250
13548
  if (key) {
13251
13549
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
13252
13550
  await putSecret2({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -13288,7 +13586,7 @@ async function provision(options) {
13288
13586
 
13289
13587
  // src/record.ts
13290
13588
  import { appendFileSync } from "fs";
13291
- import process15 from "process";
13589
+ import process19 from "process";
13292
13590
 
13293
13591
  // src/surface.ts
13294
13592
  var PM_ACTIONS = {
@@ -13470,7 +13768,7 @@ function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
13470
13768
 
13471
13769
  // src/record.ts
13472
13770
  function recordInvocation(parsed) {
13473
- const file = process15.env.ODLA_CLI_RECORD;
13771
+ const file = process19.env.ODLA_CLI_RECORD;
13474
13772
  if (!file) return;
13475
13773
  try {
13476
13774
  const entry = {
@@ -13496,10 +13794,17 @@ function advisoryCollectingFetch(inner, sink) {
13496
13794
  return response2;
13497
13795
  });
13498
13796
  }
13797
+ var superseded = /* @__PURE__ */ new Set();
13798
+ function supersedeAdvisory(code) {
13799
+ superseded.add(code);
13800
+ }
13499
13801
  function renderAdvisories(out, advisories, env = process.env) {
13802
+ const retracted = new Set(superseded);
13803
+ superseded.clear();
13500
13804
  if (env.ODLA_NO_ADVISORIES) return;
13501
13805
  const seen = /* @__PURE__ */ new Set();
13502
13806
  for (const advisory of advisories) {
13807
+ if (retracted.has(advisory.code)) continue;
13503
13808
  const key = `${advisory.code}:${advisory.message}`;
13504
13809
  if (seen.has(key)) continue;
13505
13810
  seen.add(key);
@@ -13507,6 +13812,14 @@ function renderAdvisories(out, advisories, env = process.env) {
13507
13812
  }
13508
13813
  }
13509
13814
 
13815
+ // src/device-command.ts
13816
+ import {
13817
+ ADMIN_DEVICE_SCOPES,
13818
+ ALL_OWNED_APPS,
13819
+ OPTIONAL_AGENT_PROJECT_CAPABILITIES,
13820
+ OWNER_DEVICE_SCOPES
13821
+ } from "@odla-ai/db";
13822
+
13510
13823
  // src/device-ttl.ts
13511
13824
  var OWNER_DEVICE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
13512
13825
  var DAY_MS = 24 * 60 * 60 * 1e3;
@@ -13526,10 +13839,26 @@ function parseDeviceTtl(raw) {
13526
13839
  }
13527
13840
 
13528
13841
  // src/device-command.ts
13529
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
13530
- import { dirname as dirname10 } from "path";
13531
- import process16 from "process";
13842
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "fs";
13843
+ import { dirname as dirname11 } from "path";
13844
+ import process20 from "process";
13532
13845
  async function deviceCommand(parsed, deps) {
13846
+ assertArgs(parsed, [
13847
+ "app",
13848
+ "all-apps",
13849
+ "platform-wide",
13850
+ "name",
13851
+ "capability",
13852
+ "device-ttl",
13853
+ "email",
13854
+ "open",
13855
+ "json",
13856
+ "config",
13857
+ "token",
13858
+ "context",
13859
+ "platform",
13860
+ "wait"
13861
+ ], 3);
13533
13862
  const action2 = parsed.positionals[1] ?? "";
13534
13863
  const out = deps.stdout ?? console;
13535
13864
  const doFetch = deps.fetch ?? fetch;
@@ -13542,10 +13871,19 @@ async function deviceCommand(parsed, deps) {
13542
13871
  }
13543
13872
  async function enroll(parsed, deps, cfg, doFetch, out, json) {
13544
13873
  const name = stringOpt(parsed.options.name) ?? defaultDeviceName();
13545
- const apps = (stringOpt(parsed.options.app) ?? cfg.app.id).split(",").map((id2) => id2.trim()).filter(Boolean);
13546
- if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026]");
13874
+ const platformWide = parsed.options["platform-wide"] === true;
13875
+ const narrowed = stringOpt(parsed.options.app) !== void 0 || stringOpt(parsed.options.capability) !== void 0;
13876
+ const apps = platformWide || parsed.options["all-apps"] === true || !narrowed ? [ALL_OWNED_APPS] : (stringOpt(parsed.options.app) ?? cfg.app.id).split(",").map((id2) => id2.trim()).filter(Boolean);
13877
+ if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026], or --all-apps");
13547
13878
  const deviceTtlMs = parseDeviceTtl(parsed.options["device-ttl"]);
13548
- const extended = deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
13879
+ const extended = platformWide || deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
13880
+ const { capabilities, scopes } = requestedEnvelope(parsed, platformWide, narrowed);
13881
+ const narrowNotice = narrowEnrollmentNotice({
13882
+ appIds: apps,
13883
+ capabilities: capabilities ?? [],
13884
+ scopes: scopes ?? []
13885
+ });
13886
+ if (narrowNotice) out.error(narrowNotice);
13549
13887
  const token = await scopedToken2(
13550
13888
  parsed,
13551
13889
  deps,
@@ -13560,9 +13898,10 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
13560
13898
  headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
13561
13899
  body: JSON.stringify({
13562
13900
  name,
13563
- platform: process16.platform,
13901
+ platform: process20.platform,
13564
13902
  appIds: apps,
13565
- ...parsed.options.capability ? { capabilities: String(parsed.options.capability).split(",").map((c) => c.trim()).filter(Boolean) } : {},
13903
+ ...capabilities ? { capabilities } : {},
13904
+ ...scopes ? { scopes } : {},
13566
13905
  ...deviceTtlMs === void 0 ? {} : { deviceTtlMs }
13567
13906
  })
13568
13907
  });
@@ -13571,19 +13910,54 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
13571
13910
  throw new Error(`device enroll failed: ${body.error?.message ?? `registry returned ${response2.status}`} (${response2.status})`);
13572
13911
  }
13573
13912
  const path = deviceCredentialPath();
13574
- mkdirSync4(dirname10(path), { recursive: true });
13913
+ mkdirSync5(dirname11(path), { recursive: true });
13575
13914
  writeFileSync4(path, JSON.stringify({
13576
13915
  token: body.token,
13577
13916
  platform: cfg.platformUrl.replace(/\/$/, ""),
13578
13917
  deviceId: body.device.deviceId,
13579
13918
  name
13580
13919
  }, null, 2));
13581
- chmodSync2(path, 384);
13582
- out.error(`device: enrolled "${name}" for ${body.device.appIds.join(", ")}; credential written to ${path}`);
13583
- out.error("device: this terminal will mint its own credentials from now on \u2014 no further approvals.");
13920
+ chmodSync3(path, 384);
13921
+ rememberMachineIdentity(cfg.platformUrl.replace(/\/$/, ""), stringOpt(parsed.options.email));
13922
+ supersedeAdvisory("credential.expiring");
13923
+ const reach = body.device.appIds.includes(ALL_OWNED_APPS) ? "every app you own, now and later" : body.device.appIds.join(", ");
13924
+ out.error(`device: enrolled "${name}" for ${reach}; credential written to ${path}`);
13925
+ if (narrowNotice) out.error(narrowNotice);
13926
+ out.error(
13927
+ "device: every worktree on this machine mints its own credentials from now on \u2014 no further approvals,"
13928
+ );
13929
+ out.error(
13930
+ `device: and the clock resets each time you use it. Going quiet for ${describeWindow(body.device.expiresAt)} is what ends it.`
13931
+ );
13584
13932
  if (json) {
13585
- out.log(JSON.stringify({ deviceId: body.device.deviceId, name, appIds: body.device.appIds, expiresAt: body.device.expiresAt }, null, 2));
13933
+ out.log(JSON.stringify({
13934
+ deviceId: body.device.deviceId,
13935
+ name,
13936
+ appIds: body.device.appIds,
13937
+ capabilities: body.device.capabilities ?? [],
13938
+ scopes: body.device.scopes ?? [],
13939
+ expiresAt: body.device.expiresAt,
13940
+ hardExpiresAt: body.device.hardExpiresAt ?? null
13941
+ }, null, 2));
13942
+ }
13943
+ }
13944
+ function requestedEnvelope(parsed, platformWide, narrowed) {
13945
+ const raw = stringOpt(parsed.options.capability);
13946
+ const everything = platformWide || !narrowed || raw?.trim().toLowerCase() === "all";
13947
+ if (everything) {
13948
+ return {
13949
+ capabilities: [...OPTIONAL_AGENT_PROJECT_CAPABILITIES],
13950
+ scopes: platformWide ? [...ADMIN_DEVICE_SCOPES] : [...OWNER_DEVICE_SCOPES]
13951
+ };
13586
13952
  }
13953
+ const named = raw?.split(",").map((c) => c.trim()).filter(Boolean);
13954
+ return named?.length ? { capabilities: named } : {};
13955
+ }
13956
+ function describeWindow(expiresAt, now = Date.now()) {
13957
+ const days = Math.max(1, Math.round((expiresAt - now) / (24 * 60 * 60 * 1e3)));
13958
+ if (days >= 365) return `${Math.round(days / 365)} year${days >= 730 ? "s" : ""}`;
13959
+ if (days % 7 === 0) return `${days / 7} week${days > 7 ? "s" : ""}`;
13960
+ return `${days} day${days === 1 ? "" : "s"}`;
13587
13961
  }
13588
13962
  async function list2(parsed, deps, cfg, doFetch, out, json) {
13589
13963
  const token = await scopedToken2(parsed, deps, cfg, doFetch, out, "odla CLI (device list)");
@@ -13598,12 +13972,17 @@ async function list2(parsed, deps, cfg, doFetch, out, json) {
13598
13972
  if (body.devices.length === 0) return out.log("no enrolled devices");
13599
13973
  for (const device of body.devices) {
13600
13974
  const state2 = device.revokedAt ? "revoked" : device.expiresAt <= Date.now() ? "expired" : "active";
13601
- out.log(`${device.deviceId} ${state2.padEnd(7)} ${device.name} [${device.appIds.join(", ")}]`);
13975
+ const reach = device.appIds.includes("*") ? "every app you own" : device.appIds.join(", ");
13976
+ const gap = state2 === "active" ? ` idle ${describeWindow(device.expiresAt)} left` : "";
13977
+ out.log(`${device.deviceId} ${state2.padEnd(7)} ${device.name} [${reach}]${gap}`);
13602
13978
  }
13603
13979
  }
13604
13980
  async function revoke(parsed, deps, cfg, doFetch, out, json) {
13605
13981
  const deviceId = parsed.positionals[2];
13606
13982
  if (!deviceId) throw new Error("device revoke needs the device id from `odla-ai device list`");
13983
+ out.error(
13984
+ `device: revoking is a signed-in human's decision; if this is refused, open ${cfg.platformUrl}/studio and revoke it there.`
13985
+ );
13607
13986
  const token = await scopedToken2(parsed, deps, cfg, doFetch, out, "odla CLI (device revoke)");
13608
13987
  const response2 = await doFetch(`${cfg.platformUrl}/registry/devices/${encodeURIComponent(deviceId)}/revoke`, {
13609
13988
  method: "POST",
@@ -13622,7 +14001,7 @@ async function scopedToken2(parsed, deps, cfg, doFetch, out, label, scope = "app
13622
14001
  // A device is granted the apps named in ONE approval, so --app is a list here.
13623
14002
  allowAppList: true
13624
14003
  });
13625
- const scopedTokenFile = credentials.scopedTokenFile;
14004
+ const scopedTokenFile2 = credentials.scopedTokenFile;
13626
14005
  return getScopedPlatformToken({
13627
14006
  platform: cfg.platformUrl,
13628
14007
  scope,
@@ -13633,12 +14012,12 @@ async function scopedToken2(parsed, deps, cfg, doFetch, out, label, scope = "app
13633
14012
  open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
13634
14013
  openApprovalUrl: deps.openUrl,
13635
14014
  rootDir: cfg.rootDir,
13636
- tokenFile: scopedTokenFile,
14015
+ tokenFile: scopedTokenFile2,
13637
14016
  ...stringOpt(parsed.options.token) ? { token: stringOpt(parsed.options.token) } : {}
13638
14017
  });
13639
14018
  }
13640
14019
  function defaultDeviceName() {
13641
- return `${process16.env.HOSTNAME ?? process16.env.HOST ?? "machine"}-${process16.platform}`;
14020
+ return `${process20.env.HOSTNAME ?? process20.env.HOST ?? "machine"}-${process20.platform}`;
13642
14021
  }
13643
14022
 
13644
14023
  // src/runbook-actions.ts
@@ -13782,7 +14161,7 @@ async function runbookRemove(ctx, slug) {
13782
14161
 
13783
14162
  // src/runbook-import.ts
13784
14163
  import { readFileSync as readFileSync11, readdirSync as readdirSync2, statSync } from "fs";
13785
- import { basename as basename2, join as join14 } from "path";
14164
+ import { basename as basename2, join as join15 } from "path";
13786
14165
  function parseRunbook(text3, slug) {
13787
14166
  let rest = text3;
13788
14167
  const meta = {};
@@ -13812,7 +14191,7 @@ function readRunbookDir(dir) {
13812
14191
  if (!files.length) throw new Error(`no .md files in ${dir}`);
13813
14192
  return files.map((file) => {
13814
14193
  const slug = basename2(file, ".md");
13815
- const parsed = parseRunbook(readFileSync11(join14(dir, file), "utf8"), slug);
14194
+ const parsed = parseRunbook(readFileSync11(join15(dir, file), "utf8"), slug);
13816
14195
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
13817
14196
  });
13818
14197
  }
@@ -13880,8 +14259,8 @@ async function upsert(ctx, r, visibility) {
13880
14259
 
13881
14260
  // src/runbook-impact.ts
13882
14261
  import { execFileSync as execFileSync2 } from "child_process";
13883
- import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
13884
- import { join as join15 } from "path";
14262
+ import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
14263
+ import { join as join16 } from "path";
13885
14264
 
13886
14265
  // src/runbook-impact-scan.ts
13887
14266
  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$]*)/;
@@ -14050,8 +14429,8 @@ ${body.split("\n").map((line2) => `+${line2}`).join("\n")}
14050
14429
  }
14051
14430
  function manifestLabeller(root) {
14052
14431
  return (workspace) => {
14053
- const manifest = join15(root, workspace, "package.json");
14054
- if (!existsSync12(manifest)) return void 0;
14432
+ const manifest = join16(root, workspace, "package.json");
14433
+ if (!existsSync13(manifest)) return void 0;
14055
14434
  try {
14056
14435
  const name = JSON.parse(readFileSync12(manifest, "utf8")).name;
14057
14436
  return typeof name === "string" ? name : void 0;
@@ -14120,7 +14499,7 @@ function report4(ctx, impacts) {
14120
14499
  async function runbookImpact(ctx, options, deps = {}) {
14121
14500
  const cwd = deps.cwd ?? process.cwd();
14122
14501
  const runGit = deps.runGit ?? gitRunner(cwd);
14123
- const read3 = deps.readRepoFile ?? ((path) => readFileSync12(join15(cwd, path), "utf8"));
14502
+ const read3 = deps.readRepoFile ?? ((path) => readFileSync12(join16(cwd, path), "utf8"));
14124
14503
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
14125
14504
  if (!surfaces.length) {
14126
14505
  return ctx.out.log(
@@ -14247,12 +14626,12 @@ async function runbookComment(ctx, slug, body) {
14247
14626
 
14248
14627
  // src/runbook-editor.ts
14249
14628
  import { spawnSync } from "child_process";
14250
- import { mkdtempSync, readFileSync as readFileSync13, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
14629
+ import { mkdtempSync, readFileSync as readFileSync13, rmSync as rmSync6, writeFileSync as writeFileSync5 } from "fs";
14251
14630
  import { tmpdir as tmpdir4 } from "os";
14252
- import { join as join16 } from "path";
14253
- import process17 from "process";
14631
+ import { join as join17 } from "path";
14632
+ import process21 from "process";
14254
14633
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
14255
- function resolveEditor(env = process17.env) {
14634
+ function resolveEditor(env = process21.env) {
14256
14635
  for (const name of EDITOR_ENV) {
14257
14636
  const value2 = env[name];
14258
14637
  if (value2 && value2.trim()) return value2.trim();
@@ -14266,8 +14645,8 @@ function defaultRun(command, path) {
14266
14645
  return result.status ?? 0;
14267
14646
  }
14268
14647
  function editText(initial, slug, deps = {}) {
14269
- const env = deps.env ?? process17.env;
14270
- const interactive = deps.interactive ?? (() => Boolean(process17.stdin.isTTY));
14648
+ const env = deps.env ?? process21.env;
14649
+ const interactive = deps.interactive ?? (() => Boolean(process21.stdin.isTTY));
14271
14650
  const editor = resolveEditor(env);
14272
14651
  if (!editor)
14273
14652
  throw new Error(
@@ -14275,8 +14654,8 @@ function editText(initial, slug, deps = {}) {
14275
14654
  );
14276
14655
  if (!interactive())
14277
14656
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
14278
- const dir = mkdtempSync(join16(tmpdir4(), "odla-runbook-"));
14279
- const file = join16(dir, `${slug}.md`);
14657
+ const dir = mkdtempSync(join17(tmpdir4(), "odla-runbook-"));
14658
+ const file = join17(dir, `${slug}.md`);
14280
14659
  try {
14281
14660
  writeFileSync5(file, initial, { mode: 384 });
14282
14661
  const code = defaultRunOrInjected(deps)(editor, file);
@@ -14284,7 +14663,7 @@ function editText(initial, slug, deps = {}) {
14284
14663
  const edited = readFileSync13(file, "utf8");
14285
14664
  return edited === initial ? null : edited;
14286
14665
  } finally {
14287
- rmSync3(dir, { recursive: true, force: true });
14666
+ rmSync6(dir, { recursive: true, force: true });
14288
14667
  }
14289
14668
  }
14290
14669
  var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
@@ -15196,8 +15575,10 @@ async function dispatchCli(argv, dependencies) {
15196
15575
  return;
15197
15576
  }
15198
15577
  if (command === "help" || command === "--help" || command === "-h") {
15199
- assertArgs(parsed, ["help"], 1);
15200
- printHelp(runtime.stdout);
15578
+ assertArgs(parsed, ["help"], 2);
15579
+ const topic = parsed.positionals[1];
15580
+ if (topic) printCommandHelp(topic, runtime.stdout);
15581
+ else printHelp(runtime.stdout);
15201
15582
  return;
15202
15583
  }
15203
15584
  if (command === "whoami") {
@@ -15411,4 +15792,4 @@ export {
15411
15792
  isTerminalHostedSecurityStatus,
15412
15793
  runCli
15413
15794
  };
15414
- //# sourceMappingURL=chunk-WJAN5CZ2.js.map
15795
+ //# sourceMappingURL=chunk-2IHE5U3M.js.map