@odla-ai/cli 0.38.2 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,71 @@ 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
+ return env.ODLA_HOME ?? join3(env.HOME ?? homedir2(), ".odla");
212
+ }
213
+ function odlaHomePath(segments, env = process6.env) {
214
+ return join3(odlaHome(env), ...segments);
215
+ }
216
+ function identityFile(env) {
217
+ return odlaHomePath(["identity.json"], env);
218
+ }
219
+ function deviceSessionFile(env) {
220
+ return odlaHomePath(["session.json"], env);
221
+ }
222
+ function appTokenFile(appId, env) {
223
+ return odlaHomePath(["apps", safeSegment(appId), "dev-token.json"], env);
224
+ }
225
+ function appCredentialsFile(appId, env) {
226
+ return odlaHomePath(["apps", safeSegment(appId), "credentials.json"], env);
227
+ }
228
+ function scopedTokenFile(env) {
229
+ return odlaHomePath(["admin-token.local.json"], env);
230
+ }
231
+ function pmContextFile(env) {
232
+ return odlaHomePath(["pm-context.json"], env);
233
+ }
234
+ function adoptRepoLocalCache(legacyPath, machinePath, out) {
235
+ if (!existsSync2(legacyPath) || legacyPath === machinePath) return false;
236
+ const superseded = existsSync2(machinePath);
237
+ if (!superseded) {
238
+ mkdirSync(dirname2(machinePath), { recursive: true });
239
+ copyFileSync(legacyPath, machinePath);
240
+ chmodSync(machinePath, 384);
241
+ }
242
+ rmSync2(legacyPath, { force: true });
243
+ out?.error(
244
+ superseded ? `auth: removed superseded ${legacyPath}; this machine's credentials live in ${odlaHome()}` : `auth: moved ${legacyPath} into ${machinePath}; credentials are per machine now, not per worktree`
245
+ );
246
+ return true;
247
+ }
248
+ function safeSegment(value2) {
249
+ const clean4 = value2.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "");
250
+ if (!clean4) throw new Error(`"${value2}" is not a usable app id`);
251
+ return clean4;
210
252
  }
211
253
 
212
254
  // 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";
255
+ import { chmodSync as chmodSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
256
+ import { dirname as dirname3, isAbsolute, relative, resolve } from "path";
215
257
  var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
216
258
  function readJsonFile(path) {
217
259
  try {
@@ -225,7 +267,7 @@ function writePrivateJson(path, value2) {
225
267
  `);
226
268
  }
227
269
  function readCredentials(path) {
228
- if (!existsSync2(path)) return null;
270
+ if (!existsSync3(path)) return null;
229
271
  let value2;
230
272
  try {
231
273
  value2 = JSON.parse(readFileSync2(path, "utf8"));
@@ -259,7 +301,7 @@ function mergeCredential(current, update) {
259
301
  }
260
302
  function ensureGitignore(rootDir, localPaths = []) {
261
303
  const path = resolve(rootDir, ".gitignore");
262
- const existing = existsSync2(path) ? readFileSync2(path, "utf8") : "";
304
+ const existing = existsSync3(path) ? readFileSync2(path, "utf8") : "";
263
305
  const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line2) => !!line2);
264
306
  const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
265
307
  const missing = wanted.filter((line2) => !existing.split(/\r?\n/).includes(line2));
@@ -279,7 +321,7 @@ function o11yDevVars(cfg) {
279
321
  function resolveWriteDevVarsTarget(cfg, requested) {
280
322
  if (!requested) return null;
281
323
  if (requested === true) return cfg.local.devVarsFile;
282
- return resolve(dirname2(cfg.configPath), requested);
324
+ return resolve(dirname3(cfg.configPath), requested);
283
325
  }
284
326
  function writeDevVars(path, credentials, env, o11y) {
285
327
  const entry = credentials.envs[env];
@@ -293,7 +335,7 @@ function writeDevVars(path, credentials, env, o11y) {
293
335
  if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
294
336
  if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
295
337
  }
296
- const existing = existsSync2(path) ? readFileSync2(path, "utf8") : "";
338
+ const existing = existsSync3(path) ? readFileSync2(path, "utf8") : "";
297
339
  const retained = existing.split(/\r?\n/).filter((line2) => !isManagedDevVar(line2));
298
340
  while (retained.at(-1) === "") retained.pop();
299
341
  const prefix = retained.length ? `${retained.join("\n")}
@@ -319,10 +361,10 @@ function isManagedDevVar(line2) {
319
361
  return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
320
362
  }
321
363
  function writePrivateText(path, text3) {
322
- mkdirSync(dirname2(path), { recursive: true });
364
+ mkdirSync2(dirname3(path), { recursive: true });
323
365
  const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
324
366
  writeFileSync(temporary, text3, { mode: 384 });
325
- chmodSync(temporary, 384);
367
+ chmodSync2(temporary, 384);
326
368
  renameSync(temporary, path);
327
369
  }
328
370
  function gitignoreEntry(rootDir, path) {
@@ -335,6 +377,94 @@ function displayPath(path, rootDir = process.cwd()) {
335
377
  return rel && !rel.startsWith("..") ? rel : path;
336
378
  }
337
379
 
380
+ // src/auth-guidance.ts
381
+ var ENROL_EVERYTHING = "npx odla-ai device enroll --all-apps --capability all --no-open --wait 600";
382
+ var ENROL_PLATFORM_WIDE = "npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600";
383
+ function machineAuthState(audience, env = process7.env) {
384
+ const device = readDeviceCredential(audience, env);
385
+ if (!device) return { enrolled: false };
386
+ const session = readJsonFile(deviceSessionFile(env));
387
+ const current = session?.platform === audience && session.deviceId === device.deviceId ? session : void 0;
388
+ return {
389
+ enrolled: true,
390
+ ...device.name ? { deviceName: device.name } : {},
391
+ ...current?.appIds ? { appIds: current.appIds } : {},
392
+ ...current?.capabilities ? { capabilities: current.capabilities } : {},
393
+ ...current?.scopes ? { scopes: current.scopes } : {},
394
+ ...current?.deviceExpiresAt ? { lapsesAt: current.deviceExpiresAt } : {}
395
+ };
396
+ }
397
+ function scopeInterruptionNotice(scope, state2) {
398
+ const platformScope = scope.startsWith("platform:");
399
+ 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}"`;
400
+ return [
401
+ `odla: ${reason}.`,
402
+ ` Approve this one now, then end the interruptions with:`,
403
+ ` ${platformScope ? ENROL_PLATFORM_WIDE : ENROL_EVERYTHING}`,
404
+ 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."
405
+ ].join("\n");
406
+ }
407
+ function lapseNotice(state2, now = Date.now()) {
408
+ if (!state2.enrolled || !state2.lapsesAt) return null;
409
+ const days = Math.floor((state2.lapsesAt - now) / (24 * 60 * 60 * 1e3));
410
+ if (days < 0) return "this machine's enrollment has lapsed; the next command will ask for approval";
411
+ return `idle for ${days} more day${days === 1 ? "" : "s"} before this machine needs approving again (using it resets the clock)`;
412
+ }
413
+
414
+ // src/cached-credential.ts
415
+ var noted = null;
416
+ function noteCachedCredential(tokenFile) {
417
+ noted = tokenFile;
418
+ }
419
+ function isCredentialRejection(error) {
420
+ const message2 = error instanceof Error ? error.message : String(error ?? "");
421
+ return /\((401|403)\)\s*$/.test(message2.trim());
422
+ }
423
+ function explainRejectedCredential(error) {
424
+ const tokenFile = noted;
425
+ if (!tokenFile || !isCredentialRejection(error)) return null;
426
+ noted = null;
427
+ rmSync3(tokenFile, { force: true });
428
+ return [
429
+ "auth: the cached credential was rejected by odla, so it was revoked before its cached expiry.",
430
+ " The usual cause is a newer sign-in for this account: collecting a handshake retires the",
431
+ " principal's other collected credentials, so a second machine supersedes this one.",
432
+ ` Discarded ${tokenFile}; re-run this command to request a fresh approval.`,
433
+ ` To stop needing one: ${ENROL_EVERYTHING}`
434
+ ].join("\n");
435
+ }
436
+
437
+ // src/device-session-cache.ts
438
+ import process8 from "process";
439
+ var SKEW_MS = 6e4;
440
+ async function deviceSessionToken(platformUrl, audience, credential2, doFetch, env = process8.env) {
441
+ const path = deviceSessionFile(env);
442
+ const cached = readJsonFile(path);
443
+ if (cached?.token && cached.platform === audience && cached.deviceId === credential2.deviceId && (cached.expiresAt ?? 0) > Date.now() + SKEW_MS) return cached;
444
+ const minted = await mintDeviceSession(platformUrl, credential2, doFetch);
445
+ const session = {
446
+ ...minted,
447
+ platform: audience,
448
+ ...credential2.deviceId ? { deviceId: credential2.deviceId } : {}
449
+ };
450
+ writePrivateJson(path, session);
451
+ return session;
452
+ }
453
+
454
+ // src/machine-identity.ts
455
+ import process9 from "process";
456
+ function readMachineIdentity(audience, env = process9.env) {
457
+ const stored = readJsonFile(identityFile(env));
458
+ if (!stored || typeof stored.email !== "string" || !stored.email) return null;
459
+ return stored.platform === audience ? { platform: audience, email: stored.email } : null;
460
+ }
461
+ function rememberMachineIdentity(audience, email, env = process9.env) {
462
+ if (!email) return;
463
+ const existing = readMachineIdentity(audience, env);
464
+ if (existing?.email === email) return;
465
+ writePrivateJson(identityFile(env), { platform: audience, email });
466
+ }
467
+
338
468
  // src/token.ts
339
469
  async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {}) {
340
470
  const audience = platformAudience(cfg.platformUrl);
@@ -343,19 +473,19 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
343
473
  const cached = readJsonFile(cfg.local.tokenFile);
344
474
  if (!grantRequest.forceReview && !grantRequest.freshLogin) {
345
475
  if (options.token) return options.token;
346
- if (process6.env.ODLA_DEV_TOKEN) {
347
- const declared = process6.env.ODLA_DEV_TOKEN_AUDIENCE;
476
+ if (process10.env.ODLA_DEV_TOKEN) {
477
+ const declared = process10.env.ODLA_DEV_TOKEN_AUDIENCE;
348
478
  if (declared) {
349
479
  if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
350
480
  } else if (audience !== "https://odla.ai") {
351
481
  throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
352
482
  }
353
- return process6.env.ODLA_DEV_TOKEN;
483
+ return process10.env.ODLA_DEV_TOKEN;
354
484
  }
355
485
  const device = readDeviceCredential(audience);
356
486
  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)})`);
487
+ const session = await deviceSessionToken(cfg.platformUrl, audience, device, doFetch);
488
+ out.error(`auth: session held by this enrolled device (${displayPath(deviceCredentialPath(), cfg.rootDir)})`);
359
489
  return session.token;
360
490
  }
361
491
  if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4 && cachedGrantCovers(cached, grantIntent)) {
@@ -375,7 +505,10 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
375
505
  doFetch,
376
506
  out,
377
507
  audience,
378
- email: handshakeEmail(options.email, cached?.platform === audience ? cached.email : void 0),
508
+ email: handshakeEmail(
509
+ options.email,
510
+ (cached?.platform === audience ? cached.email : void 0) ?? readMachineIdentity(audience)?.email
511
+ ),
379
512
  pendingFile: handshakeFile(cfg),
380
513
  grantIntent
381
514
  };
@@ -392,6 +525,7 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
392
525
  expiresAt
393
526
  });
394
527
  out.error(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
528
+ rememberMachineIdentity(audience, ctx.email);
395
529
  return token;
396
530
  }
397
531
  async function freshHandshake(ctx, waitMs) {
@@ -461,7 +595,7 @@ function stillPending(pending, email) {
461
595
  );
462
596
  }
463
597
  function handshakeEmail(value2, cached) {
464
- const email = (value2 ?? process6.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
598
+ const email = (value2 ?? process10.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
465
599
  if (/@users\.noreply\.github\.com$/i.test(email)) {
466
600
  throw new Error(
467
601
  `"${email}" is a GitHub commit identity, not an odla account email; use --email <signed-in-odla-account> or ODLA_USER_EMAIL`
@@ -492,12 +626,12 @@ function platformAudience(value2) {
492
626
  }
493
627
 
494
628
  // src/secret-input.ts
495
- import process7 from "process";
629
+ import process11 from "process";
496
630
  var MAX_BYTES = 64 * 1024;
497
631
  async function secretInputValue(options, kind = "credential") {
498
632
  if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
499
633
  let value2;
500
- if (options.fromEnv) value2 = process7.env[options.fromEnv];
634
+ if (options.fromEnv) value2 = process11.env[options.fromEnv];
501
635
  else if (options.stdin) value2 = await (options.readStdin ?? (() => readSecretStream(kind)))();
502
636
  else throw new Error(`${kind} input required: use --from-env <NAME> or --stdin; values are never accepted as arguments`);
503
637
  value2 = value2?.replace(/[\r\n]+$/, "");
@@ -505,7 +639,7 @@ async function secretInputValue(options, kind = "credential") {
505
639
  if (new TextEncoder().encode(value2).byteLength > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
506
640
  return value2;
507
641
  }
508
- async function readSecretStream(kind, stream = process7.stdin) {
642
+ async function readSecretStream(kind, stream = process11.stdin) {
509
643
  let value2 = "";
510
644
  for await (const chunk of stream) {
511
645
  value2 += String(chunk);
@@ -515,9 +649,8 @@ async function readSecretStream(kind, stream = process7.stdin) {
515
649
  }
516
650
 
517
651
  // src/admin-ai-auth.ts
518
- import { existsSync as existsSync3 } from "fs";
519
- import { join as join3 } from "path";
520
- import process8 from "process";
652
+ import { join as join4 } from "path";
653
+ import process12 from "process";
521
654
  import { requestToken as requestToken2 } from "@odla-ai/db";
522
655
  async function getScopedPlatformToken(options) {
523
656
  return resolveAdminPlatformToken(options);
@@ -525,7 +658,7 @@ async function getScopedPlatformToken(options) {
525
658
  async function resolveAdminPlatformToken(options) {
526
659
  const audience = platformAudience(options.platform);
527
660
  if (options.token) return options.token;
528
- const fromEnv = process8.env.ODLA_ADMIN_TOKEN;
661
+ const fromEnv = process12.env.ODLA_ADMIN_TOKEN;
529
662
  if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
530
663
  return scopedToken(
531
664
  audience,
@@ -537,7 +670,7 @@ async function resolveAdminPlatformToken(options) {
537
670
  }
538
671
  function audienceBoundEnvToken(token, platform) {
539
672
  const audience = platformAudience(platform);
540
- const declared = process8.env.ODLA_ADMIN_TOKEN_AUDIENCE;
673
+ const declared = process12.env.ODLA_ADMIN_TOKEN_AUDIENCE;
541
674
  if (declared) {
542
675
  if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
543
676
  } else if (audience !== "https://odla.ai") {
@@ -564,15 +697,28 @@ var SCOPE_PURPOSE = {
564
697
  };
565
698
  async function scopedToken(platform, scope, options, doFetch, out) {
566
699
  const audience = platformAudience(platform);
567
- const rootDir = options.rootDir ?? process8.cwd();
568
- const tokenFile = options.tokenFile ?? join3(rootDir, ".odla/admin-token.local.json");
700
+ const rootDir = options.rootDir ?? process12.cwd();
701
+ const tokenFile = options.tokenFile ?? scopedTokenFile();
702
+ adoptRepoLocalCache(join4(rootDir, ".odla/admin-token.local.json"), tokenFile, out);
703
+ const device = readDeviceCredential(audience);
704
+ if (device && options.cache !== false) {
705
+ const session = await deviceSessionToken(platform, audience, device, doFetch);
706
+ if (session.scopes?.includes(scope)) {
707
+ out.error(`auth: ${scope} held by this enrolled device`);
708
+ return session.token;
709
+ }
710
+ }
569
711
  const cache2 = options.cache === false ? null : readJsonFile(tokenFile);
570
712
  const cached = cache2?.platform === audience ? cache2.tokens?.[scope] : void 0;
571
713
  if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
572
714
  out.error(`auth: using cached ${scope} grant (${tokenFile})`);
573
715
  return cached.token;
574
716
  }
575
- const email = handshakeEmail(options.email, cache2?.platform === audience ? cache2.email : void 0);
717
+ out.error(scopeInterruptionNotice(scope, machineAuthState(audience)));
718
+ const email = handshakeEmail(
719
+ options.email,
720
+ (cache2?.platform === audience ? cache2.email : void 0) ?? readMachineIdentity(audience)?.email
721
+ );
576
722
  const { token, expiresAt } = await requestToken2({
577
723
  endpoint: audience,
578
724
  email,
@@ -592,8 +738,8 @@ async function scopedToken(platform, scope, options, doFetch, out) {
592
738
  if (options.cache !== false) {
593
739
  const tokens = cache2?.platform === audience ? { ...cache2.tokens ?? {} } : {};
594
740
  tokens[scope] = { token, expiresAt };
595
- if (existsSync3(join3(rootDir, ".git"))) ensureGitignore(rootDir, [tokenFile]);
596
741
  writePrivateJson(tokenFile, { platform: audience, email, tokens });
742
+ rememberMachineIdentity(audience, email);
597
743
  out.error(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
598
744
  } else {
599
745
  out.error(`auth: ${scope} grant is in memory only; its credential record remains in odla-ai/db`);
@@ -769,7 +915,7 @@ function isRecord2(value2) {
769
915
 
770
916
  // src/admin-ai.ts
771
917
  async function adminAi(options) {
772
- const platform = platformAudience(options.platform ?? process9.env.ODLA_PLATFORM ?? "https://odla.ai");
918
+ const platform = platformAudience(options.platform ?? process13.env.ODLA_PLATFORM ?? "https://odla.ai");
773
919
  const doFetch = options.fetch ?? fetch;
774
920
  const out = options.stdout ?? console;
775
921
  const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
@@ -1085,12 +1231,12 @@ async function adminSpend(parsed, ctx) {
1085
1231
 
1086
1232
  // src/operator-context.ts
1087
1233
  import { existsSync as existsSync6 } from "fs";
1088
- import { join as join5, resolve as resolve4 } from "path";
1089
- import process11 from "process";
1234
+ import { join as join6, resolve as resolve4 } from "path";
1235
+ import process15 from "process";
1090
1236
 
1091
1237
  // 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";
1238
+ import { existsSync as existsSync4, rmSync as rmSync4, readFileSync as readFileSync3 } from "fs";
1239
+ import { dirname as dirname4, isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
1094
1240
  import { pathToFileURL } from "url";
1095
1241
  import { appServiceDefinition, appServiceIds } from "@odla-ai/apps";
1096
1242
 
@@ -1494,13 +1640,17 @@ var DEFAULT_ENVS = ["dev"];
1494
1640
  var DEFAULT_SERVICES = ["db", "ai"];
1495
1641
  var configImportSerial = 0;
1496
1642
  var GOOGLE_CALENDAR_EVENTS_SCOPE = "https://www.googleapis.com/auth/calendar.events";
1643
+ var stderr = { error: (message2) => {
1644
+ process.stderr.write(`${message2}
1645
+ `);
1646
+ } };
1497
1647
  async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1498
1648
  const resolved = resolve2(configPath);
1499
1649
  if (!existsSync4(resolved)) {
1500
1650
  throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
1501
1651
  }
1502
1652
  const raw = await loadConfigModule(resolved);
1503
- const rootDir = dirname3(resolved);
1653
+ const rootDir = dirname4(resolved);
1504
1654
  validateRawConfig(raw, resolved);
1505
1655
  const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
1506
1656
  const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
@@ -1510,11 +1660,14 @@ async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1510
1660
  validateCalendarConfig(raw, unique3([...envs, ...options.additionalEnvs ?? []]), services, resolved);
1511
1661
  validateMonitoringConfig(raw, unique3([...envs, ...options.additionalEnvs ?? []]), services, resolved);
1512
1662
  const local = {
1513
- tokenFile: resolve2(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
1514
- credentialsFile: resolve2(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
1663
+ tokenFile: raw.local?.tokenFile ? resolve2(rootDir, raw.local.tokenFile) : appTokenFile(raw.app.id),
1664
+ credentialsFile: raw.local?.credentialsFile ? resolve2(rootDir, raw.local.credentialsFile) : appCredentialsFile(raw.app.id),
1515
1665
  devVarsFile: resolve2(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
1516
1666
  gitignore: raw.local?.gitignore ?? true
1517
1667
  };
1668
+ adoptRepoLocalCache(resolve2(rootDir, ".odla/dev-token.json"), local.tokenFile, stderr);
1669
+ adoptRepoLocalCache(resolve2(rootDir, ".odla/credentials.local.json"), local.credentialsFile, stderr);
1670
+ rmSync4(resolve2(rootDir, ".odla/handshake.local.json"), { force: true });
1518
1671
  return {
1519
1672
  ...raw,
1520
1673
  configPath: resolved,
@@ -1623,17 +1776,17 @@ function unique3(values) {
1623
1776
 
1624
1777
  // src/operator-profiles.ts
1625
1778
  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";
1779
+ import { homedir as homedir3 } from "os";
1780
+ import { dirname as dirname5, join as join5, resolve as resolve3 } from "path";
1781
+ import process14 from "process";
1629
1782
  function operatorProfileFile() {
1630
1783
  return resolve3(
1631
- clean(process10.env.ODLA_CONTEXT_FILE) ?? join4(homedir2(), ".odla", "contexts.json")
1784
+ clean(process14.env.ODLA_CONTEXT_FILE) ?? join5(homedir3(), ".odla", "contexts.json")
1632
1785
  );
1633
1786
  }
1634
1787
  function resolveOperatorProfile(parsed) {
1635
1788
  const fromFlag = clean(stringOpt(parsed.options.context));
1636
- const fromEnvironment = clean(process10.env.ODLA_CONTEXT);
1789
+ const fromEnvironment = clean(process14.env.ODLA_CONTEXT);
1637
1790
  const name = fromFlag ?? fromEnvironment ?? null;
1638
1791
  const file = operatorProfileFile();
1639
1792
  if (!name) {
@@ -1673,10 +1826,10 @@ function removeOperatorProfile(name, file = operatorProfileFile()) {
1673
1826
  return true;
1674
1827
  }
1675
1828
  function operatorCredentialFiles(selection) {
1676
- const base = selection.name ? join4(dirname4(selection.file), "profiles", selection.name) : join4(homedir2(), ".odla");
1829
+ const base = selection.name ? join5(dirname5(selection.file), "profiles", selection.name) : join5(homedir3(), ".odla");
1677
1830
  return {
1678
- developer: join4(base, "dev-token.json"),
1679
- scoped: join4(base, "admin-token.local.json")
1831
+ developer: join5(base, "dev-token.json"),
1832
+ scoped: join5(base, "admin-token.local.json")
1680
1833
  };
1681
1834
  }
1682
1835
  function assertOperatorName(value2, label) {
@@ -1762,13 +1915,13 @@ async function resolveOperatorContext(parsed, options = {}) {
1762
1915
  }
1763
1916
  const loaded = hasConfig ? await loadProjectConfig(configArgument) : void 0;
1764
1917
  const platformFlag = clean2(stringOpt(parsed.options.platform));
1765
- const platformEnvironment = clean2(process11.env.ODLA_PLATFORM_URL);
1918
+ const platformEnvironment = clean2(process15.env.ODLA_PLATFORM_URL);
1766
1919
  const platformValue = platformAudience(
1767
1920
  platformFlag ?? platformEnvironment ?? profile.value?.platform ?? loaded?.platformUrl ?? DEFAULT_PLATFORM2
1768
1921
  );
1769
1922
  const platformSource = platformFlag ? "flag" : platformEnvironment ? "environment" : profile.value ? "profile" : loaded ? "config" : "default";
1770
1923
  const appFlag = clean2(stringOpt(parsed.options.app));
1771
- const appEnvironment = clean2(process11.env.ODLA_APP_ID);
1924
+ const appEnvironment = clean2(process15.env.ODLA_APP_ID);
1772
1925
  const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
1773
1926
  const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
1774
1927
  if (appValue) {
@@ -1782,16 +1935,16 @@ async function resolveOperatorContext(parsed, options = {}) {
1782
1935
  );
1783
1936
  }
1784
1937
  const envFlag = clean2(stringOpt(parsed.options.env));
1785
- const envEnvironment = clean2(process11.env.ODLA_ENV);
1938
+ const envEnvironment = clean2(process15.env.ODLA_ENV);
1786
1939
  const environmentValue = envFlag ?? envEnvironment ?? profile.value?.environment ?? options.defaultEnvironment ?? null;
1787
1940
  const environmentSource = envFlag ? "flag" : envEnvironment ? "environment" : profile.value?.environment ? "profile" : options.defaultEnvironment ? "default" : "unresolved";
1788
1941
  if (environmentValue) {
1789
1942
  assertOperatorName(environmentValue, "environment");
1790
1943
  }
1791
- const rootDir = loaded?.rootDir ?? process11.cwd();
1944
+ const rootDir = loaded?.rootDir ?? process15.cwd();
1792
1945
  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;
1946
+ 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;
1947
+ 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
1948
  const cfg = loaded ? {
1796
1949
  ...loaded,
1797
1950
  platformUrl: platformValue,
@@ -1813,8 +1966,8 @@ async function resolveOperatorContext(parsed, options = {}) {
1813
1966
  services: [],
1814
1967
  local: {
1815
1968
  tokenFile,
1816
- credentialsFile: join5(rootDir, ".odla", "credentials.local.json"),
1817
- devVarsFile: join5(rootDir, ".dev.vars"),
1969
+ credentialsFile: join6(rootDir, ".odla", "credentials.local.json"),
1970
+ devVarsFile: join6(rootDir, ".dev.vars"),
1818
1971
  gitignore: true
1819
1972
  }
1820
1973
  };
@@ -1838,7 +1991,7 @@ async function resolveOperatorContext(parsed, options = {}) {
1838
1991
  },
1839
1992
  credentials: {
1840
1993
  developerTokenFile: tokenFile,
1841
- scopedTokenFile
1994
+ scopedTokenFile: scopedTokenFile2
1842
1995
  }
1843
1996
  };
1844
1997
  }
@@ -1936,7 +2089,7 @@ async function adminCommand(parsed, deps = {}) {
1936
2089
  }
1937
2090
 
1938
2091
  // src/auth-command.ts
1939
- import process12 from "process";
2092
+ import process16 from "process";
1940
2093
 
1941
2094
  // src/whoami-command.ts
1942
2095
  var text2 = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
@@ -2072,6 +2225,7 @@ async function whoamiCommand(parsed, deps = {}) {
2072
2225
  } else {
2073
2226
  out.log("projects: (none \u2014 every pm and discuss call will be refused)");
2074
2227
  }
2228
+ printMachineBlock(cfg.platformUrl, out);
2075
2229
  if (!identity.admin) {
2076
2230
  if (identity.scopes.includes("platform:runbook:write")) {
2077
2231
  out.log("\nThis exact scope can read and edit all platform runbook content.");
@@ -2082,6 +2236,21 @@ async function whoamiCommand(parsed, deps = {}) {
2082
2236
  }
2083
2237
  }
2084
2238
  }
2239
+ function printMachineBlock(platformUrl, out) {
2240
+ const state2 = machineAuthState(platformAudience(platformUrl));
2241
+ if (!state2.enrolled) {
2242
+ out.log("\nmachine: not enrolled \u2014 every privileged command needs its own browser approval.");
2243
+ out.log(` End that with:
2244
+ ${ENROL_EVERYTHING}`);
2245
+ return;
2246
+ }
2247
+ const reach = state2.appIds?.includes("*") ? "every app you own" : state2.appIds?.join(", ");
2248
+ out.log(`
2249
+ machine: enrolled${state2.deviceName ? ` as "${state2.deviceName}"` : ""}${reach ? ` for ${reach}` : ""}`);
2250
+ if (state2.scopes?.length) out.log(` carrying ${state2.scopes.join(", ")}`);
2251
+ const lapse = lapseNotice(state2);
2252
+ if (lapse) out.log(` ${lapse}`);
2253
+ }
2085
2254
 
2086
2255
  // src/auth-command.ts
2087
2256
  async function authCommand(parsed, deps = {}) {
@@ -2106,7 +2275,7 @@ async function authCommand(parsed, deps = {}) {
2106
2275
  const { cfg } = context;
2107
2276
  const out = deps.stdout ?? console;
2108
2277
  const doFetch = deps.fetch ?? fetch;
2109
- const email = stringOpt(parsed.options.email) ?? process12.env.ODLA_USER_EMAIL?.trim();
2278
+ const email = stringOpt(parsed.options.email) ?? process16.env.ODLA_USER_EMAIL?.trim();
2110
2279
  if (!email) {
2111
2280
  throw new Error(
2112
2281
  "auth login requires --email <odla-account> or ODLA_USER_EMAIL; confirm the signed-in odla email instead of using git or GitHub identity"
@@ -2471,7 +2640,7 @@ async function appCommand(parsed, dependencies = {}) {
2471
2640
 
2472
2641
  // src/brand-command.ts
2473
2642
  import { mkdir, readFile, writeFile } from "fs/promises";
2474
- import { dirname as dirname5, resolve as resolve5 } from "path";
2643
+ import { dirname as dirname6, resolve as resolve5 } from "path";
2475
2644
 
2476
2645
  // src/brand-design-unpack.ts
2477
2646
  import { gunzipSync } from "zlib";
@@ -2586,7 +2755,7 @@ async function readBundle(source, deps) {
2586
2755
  async function writeAll(result, outDir) {
2587
2756
  for (const file of result.files) {
2588
2757
  const target = resolve5(outDir, file.path);
2589
- await mkdir(dirname5(target), { recursive: true });
2758
+ await mkdir(dirname6(target), { recursive: true });
2590
2759
  await writeFile(target, file.bytes);
2591
2760
  }
2592
2761
  }
@@ -3152,7 +3321,7 @@ import {
3152
3321
  AppsError,
3153
3322
  createAppsClient
3154
3323
  } from "@odla-ai/apps";
3155
- import { join as join6 } from "path";
3324
+ import { join as join7 } from "path";
3156
3325
 
3157
3326
  // src/config-operation-error.ts
3158
3327
  var ConfigOperationCommandError = class extends Error {
@@ -3577,7 +3746,7 @@ async function operationClient(cfg, options, purpose) {
3577
3746
  platform: cfg.platformUrl,
3578
3747
  scope: "app:config:write",
3579
3748
  token: options.token,
3580
- tokenFile: join6(cfg.rootDir, ".odla", "admin-token.local.json"),
3749
+ tokenFile: join7(cfg.rootDir, ".odla", "admin-token.local.json"),
3581
3750
  rootDir: cfg.rootDir,
3582
3751
  email: options.email,
3583
3752
  open: options.open,
@@ -3632,7 +3801,7 @@ function record4(value2) {
3632
3801
 
3633
3802
  // src/config-reconcile-command.ts
3634
3803
  import { createAppsClient as createAppsClient2, studioAppSettingsPath } from "@odla-ai/apps";
3635
- import { join as join7 } from "path";
3804
+ import { join as join8 } from "path";
3636
3805
 
3637
3806
  // src/config-reconcile.ts
3638
3807
  import { appServiceIds as appServiceIds2, orderAppServices as orderAppServices2 } from "@odla-ai/apps";
@@ -3928,7 +4097,7 @@ async function inspectConfig(options) {
3928
4097
  platform: cfg.platformUrl,
3929
4098
  scope: "app:config:read",
3930
4099
  token: options.token,
3931
- tokenFile: join7(cfg.rootDir, ".odla", "admin-token.local.json"),
4100
+ tokenFile: join8(cfg.rootDir, ".odla", "admin-token.local.json"),
3932
4101
  rootDir: cfg.rootDir,
3933
4102
  email: options.email,
3934
4103
  open: options.open,
@@ -4061,26 +4230,26 @@ function quoteArg2(value2) {
4061
4230
  // src/doctor-checks.ts
4062
4231
  import { execFileSync } from "child_process";
4063
4232
  import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
4064
- import { join as join9, resolve as resolve6 } from "path";
4233
+ import { join as join10, resolve as resolve6 } from "path";
4065
4234
 
4066
4235
  // src/wrangler.ts
4067
4236
  import { spawn as spawn2 } from "child_process";
4068
4237
  import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
4069
- import { join as join8 } from "path";
4238
+ import { join as join9 } from "path";
4070
4239
  var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
4071
4240
  const child = spawn2(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
4072
4241
  let stdout = "";
4073
- let stderr = "";
4242
+ let stderr2 = "";
4074
4243
  child.stdout.on("data", (chunk) => stdout += chunk.toString());
4075
- child.stderr.on("data", (chunk) => stderr += chunk.toString());
4244
+ child.stderr.on("data", (chunk) => stderr2 += chunk.toString());
4076
4245
  child.on("error", reject);
4077
- child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout, stderr }));
4246
+ child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout, stderr: stderr2 }));
4078
4247
  child.stdin.end(opts?.input ?? "");
4079
4248
  });
4080
4249
  var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
4081
4250
  function findWranglerConfig(rootDir) {
4082
4251
  for (const name of WRANGLER_CONFIG_FILES) {
4083
- const path = join8(rootDir, name);
4252
+ const path = join9(rootDir, name);
4084
4253
  if (existsSync7(path)) return path;
4085
4254
  }
4086
4255
  return null;
@@ -4231,21 +4400,21 @@ function wranglerWarnings(rootDir) {
4231
4400
  const blocks = [{ label: "", block: config }];
4232
4401
  const envs = config.env;
4233
4402
  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 });
4403
+ for (const [name, block2] of Object.entries(envs)) {
4404
+ if (block2 && typeof block2 === "object") blocks.push({ label: `env.${name}.`, block: block2 });
4236
4405
  }
4237
4406
  }
4238
- for (const { label, block } of blocks) {
4239
- const assets = block.assets;
4407
+ for (const { label, block: block2 } of blocks) {
4408
+ const assets = block2.assets;
4240
4409
  if (assets?.directory) {
4241
4410
  const dir = resolve6(rootDir, assets.directory);
4242
4411
  if (dir === resolve6(rootDir)) {
4243
4412
  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"))) {
4413
+ } else if (existsSync8(join10(dir, "node_modules"))) {
4245
4414
  warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
4246
4415
  }
4247
4416
  }
4248
- const vars = block.vars;
4417
+ const vars = block2.vars;
4249
4418
  if (vars && typeof vars === "object") {
4250
4419
  for (const [name, value2] of Object.entries(vars)) {
4251
4420
  if (name === "ODLA_API_KEY" || name === "ODLA_O11Y_TOKEN" || typeof value2 === "string" && looksSecret(value2)) {
@@ -4306,7 +4475,7 @@ function calendarProjectWarnings(rootDir) {
4306
4475
  }
4307
4476
  function readPackageJson(rootDir) {
4308
4477
  try {
4309
- return JSON.parse(readFileSync8(join9(rootDir, "package.json"), "utf8"));
4478
+ return JSON.parse(readFileSync8(join10(rootDir, "package.json"), "utf8"));
4310
4479
  } catch {
4311
4480
  return null;
4312
4481
  }
@@ -4600,8 +4769,8 @@ function harnessOption(value2, flag) {
4600
4769
  }
4601
4770
 
4602
4771
  // 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";
4772
+ import { existsSync as existsSync9, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
4773
+ import { dirname as dirname7, resolve as resolve7 } from "path";
4605
4774
  import { appServiceDefinition as appServiceDefinition3, appServiceIds as appServiceIds3 } from "@odla-ai/apps";
4606
4775
  function initProject(options) {
4607
4776
  const out = options.stdout ?? console;
@@ -4623,9 +4792,9 @@ function initProject(options) {
4623
4792
  }
4624
4793
  }
4625
4794
  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 });
4795
+ mkdirSync3(dirname7(configPath), { recursive: true });
4796
+ mkdirSync3(resolve7(rootDir, "src/odla"), { recursive: true });
4797
+ mkdirSync3(resolve7(rootDir, ".odla"), { recursive: true });
4629
4798
  writeFileSync2(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
4630
4799
  writeIfMissing(resolve7(rootDir, "src/odla/schema.mjs"), schemaTemplate());
4631
4800
  writeIfMissing(resolve7(rootDir, "src/odla/rules.mjs"), rulesTemplate());
@@ -4695,8 +4864,10 @@ ${calendar}
4695
4864
  // prod: "https://example.com",
4696
4865
  },
4697
4866
  local: {
4698
- tokenFile: ".odla/dev-token.json",
4699
- credentialsFile: ".odla/credentials.local.json",
4867
+ // Credentials live in ~/.odla, per machine, so every worktree of this app
4868
+ // shares one approval instead of asking for its own. Pinning tokenFile or
4869
+ // credentialsFile here still works and still overrides that \u2014 it just puts
4870
+ // this checkout back on its own island.
4700
4871
  devVarsFile: ".dev.vars",
4701
4872
  },
4702
4873
  };
@@ -4932,9 +5103,9 @@ function printReport(report5, out) {
4932
5103
  }
4933
5104
 
4934
5105
  // 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";
5106
+ import { existsSync as existsSync10, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync9, readdirSync, writeFileSync as writeFileSync3 } from "fs";
5107
+ import { homedir as homedir4 } from "os";
5108
+ import { dirname as dirname8, isAbsolute as isAbsolute3, join as join11, relative as relative2, resolve as resolve8, sep } from "path";
4938
5109
  import { fileURLToPath } from "url";
4939
5110
 
4940
5111
  // src/skill-adapters.ts
@@ -5034,7 +5205,7 @@ function installSkill(options = {}) {
5034
5205
  if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
5035
5206
  const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
5036
5207
  const root = resolve8(options.dir ?? process.cwd());
5037
- const home = resolve8(options.homeDir ?? homedir3());
5208
+ const home = resolve8(options.homeDir ?? homedir4());
5038
5209
  const plans = /* @__PURE__ */ new Map();
5039
5210
  const targets = /* @__PURE__ */ new Map();
5040
5211
  const rememberTarget = (harness, target) => {
@@ -5048,48 +5219,48 @@ function installSkill(options = {}) {
5048
5219
  plans.set(target, { target, content: content2, boundary, managedMerge });
5049
5220
  };
5050
5221
  const planSkillTree = (targetDir2, boundary = root) => {
5051
- for (const rel of files) plan(join10(targetDir2, rel), readFileSync9(join10(sourceDir, rel), "utf8"), false, boundary);
5222
+ for (const rel of files) plan(join11(targetDir2, rel), readFileSync9(join11(sourceDir, rel), "utf8"), false, boundary);
5052
5223
  };
5053
5224
  let targetDir;
5054
5225
  if (options.global) {
5055
- const claudeRoot = join10(home, ".claude", "skills");
5056
- const codexRoot = resolve8(options.codexHomeDir ?? process.env.CODEX_HOME ?? join10(home, ".codex"), "skills");
5226
+ const claudeRoot = join11(home, ".claude", "skills");
5227
+ const codexRoot = resolve8(options.codexHomeDir ?? process.env.CODEX_HOME ?? join11(home, ".codex"), "skills");
5057
5228
  targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
5058
5229
  for (const harness of harnesses) {
5059
5230
  const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
5060
- planSkillTree(skillRoot, harness === "claude" ? home : dirname7(dirname7(codexRoot)));
5231
+ planSkillTree(skillRoot, harness === "claude" ? home : dirname8(dirname8(codexRoot)));
5061
5232
  rememberTarget(harness, skillRoot);
5062
5233
  }
5063
5234
  } else {
5064
- const sharedRoot = join10(root, ".agents", "skills");
5235
+ const sharedRoot = join11(root, ".agents", "skills");
5065
5236
  planSkillTree(sharedRoot);
5066
- const claudeRoot = join10(root, ".claude", "skills");
5237
+ const claudeRoot = join11(root, ".claude", "skills");
5067
5238
  targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
5068
5239
  for (const harness of harnesses) rememberTarget(harness, sharedRoot);
5069
5240
  if (harnesses.includes("claude")) {
5070
5241
  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));
5242
+ const canonical2 = readFileSync9(join11(sourceDir, skill, "SKILL.md"), "utf8");
5243
+ plan(join11(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical2));
5073
5244
  }
5074
5245
  rememberTarget("claude", claudeRoot);
5075
5246
  }
5076
5247
  if (harnesses.includes("cursor")) {
5077
- const cursorRule = join10(root, ".cursor", "rules", "odla.mdc");
5248
+ const cursorRule = join11(root, ".cursor", "rules", "odla.mdc");
5078
5249
  plan(cursorRule, CURSOR_RULE);
5079
5250
  rememberTarget("cursor", cursorRule);
5080
5251
  }
5081
5252
  if (harnesses.includes("agents")) {
5082
- const agentsFile = join10(root, "AGENTS.md");
5253
+ const agentsFile = join11(root, "AGENTS.md");
5083
5254
  plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
5084
5255
  rememberTarget("agents", agentsFile);
5085
5256
  }
5086
5257
  if (harnesses.includes("copilot")) {
5087
- const copilotFile = join10(root, ".github", "copilot-instructions.md");
5258
+ const copilotFile = join11(root, ".github", "copilot-instructions.md");
5088
5259
  plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
5089
5260
  rememberTarget("copilot", copilotFile);
5090
5261
  }
5091
5262
  if (harnesses.includes("gemini")) {
5092
- const geminiFile = join10(root, "GEMINI.md");
5263
+ const geminiFile = join11(root, "GEMINI.md");
5093
5264
  plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
5094
5265
  rememberTarget("gemini", geminiFile);
5095
5266
  }
@@ -5125,7 +5296,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
5125
5296
  }
5126
5297
  for (const file of plans.values()) {
5127
5298
  if (!existsSync10(file.target) || readFileSync9(file.target, "utf8") !== file.content) {
5128
- mkdirSync3(dirname7(file.target), { recursive: true });
5299
+ mkdirSync4(dirname8(file.target), { recursive: true });
5129
5300
  writeFileSync3(file.target, file.content);
5130
5301
  }
5131
5302
  }
@@ -5164,10 +5335,10 @@ function normalizeHarnesses(values, global) {
5164
5335
  }
5165
5336
  return expanded;
5166
5337
  }
5167
- function managedFileContent(path, block, force, boundary) {
5338
+ function managedFileContent(path, block2, force, boundary) {
5168
5339
  const symlink = symlinkedComponent(boundary, path);
5169
5340
  if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
5170
- if (!existsSync10(path)) return `${block}
5341
+ if (!existsSync10(path)) return `${block2}
5171
5342
  `;
5172
5343
  const current = readFileSync9(path, "utf8");
5173
5344
  const start = "<!-- odla-ai agent setup:start -->";
@@ -5179,15 +5350,15 @@ function managedFileContent(path, block, force, boundary) {
5179
5350
  }
5180
5351
  if (startAt === -1) {
5181
5352
  const separator = current.length === 0 || current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n";
5182
- return `${current}${separator}${block}
5353
+ return `${current}${separator}${block2}
5183
5354
  `;
5184
5355
  }
5185
5356
  const afterEnd = endAt + end.length;
5186
5357
  const existing = current.slice(startAt, afterEnd);
5187
- if (existing !== block && !force) {
5358
+ if (existing !== block2 && !force) {
5188
5359
  throw new Error(`odla-managed section modified locally in ${path}; re-run with --force to replace that section`);
5189
5360
  }
5190
- return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
5361
+ return `${current.slice(0, startAt)}${block2}${current.slice(afterEnd)}`;
5191
5362
  }
5192
5363
  function symlinkedComponent(boundary, target) {
5193
5364
  const rel = relative2(boundary, target);
@@ -5196,7 +5367,7 @@ function symlinkedComponent(boundary, target) {
5196
5367
  }
5197
5368
  let current = boundary;
5198
5369
  for (const part of rel.split(sep).filter(Boolean)) {
5199
- current = join10(current, part);
5370
+ current = join11(current, part);
5200
5371
  try {
5201
5372
  if (lstatSync(current).isSymbolicLink()) return current;
5202
5373
  } catch (error) {
@@ -5213,7 +5384,7 @@ function listFiles(dir) {
5213
5384
  const results = [];
5214
5385
  const walk = (current) => {
5215
5386
  for (const entry of readdirSync(current, { withFileTypes: true })) {
5216
- const path = join10(current, entry.name);
5387
+ const path = join11(current, entry.name);
5217
5388
  if (entry.isDirectory()) walk(path);
5218
5389
  else results.push(relative2(dir, path));
5219
5390
  }
@@ -5581,7 +5752,7 @@ var HARNESS_PROTOCOL_VERSION = 1;
5581
5752
  import { execFile, spawn as spawn3 } from "child_process";
5582
5753
  import { constants } from "fs";
5583
5754
  import { access } from "fs/promises";
5584
- import { delimiter, join as join11 } from "path";
5755
+ import { delimiter, join as join12 } from "path";
5585
5756
  import { getgid, getuid } from "process";
5586
5757
  import { mkdir as mkdir2, mkdtemp, realpath, rm, writeFile as writeFile2 } from "fs/promises";
5587
5758
  import { tmpdir } from "os";
@@ -5599,7 +5770,7 @@ function assertPinnedImage(image) {
5599
5770
  async function commandAvailable(engine) {
5600
5771
  for (const directory of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
5601
5772
  try {
5602
- await access(join11(directory, engine), constants.X_OK);
5773
+ await access(join12(directory, engine), constants.X_OK);
5603
5774
  return true;
5604
5775
  } catch {
5605
5776
  }
@@ -5678,7 +5849,7 @@ function allowedWorkspacePath(relativePath) {
5678
5849
  async function gitOutput(cwd, args, maxBytes) {
5679
5850
  const child = spawn22("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], shell: false });
5680
5851
  const stdout = [];
5681
- const stderr = [];
5852
+ const stderr2 = [];
5682
5853
  let bytes = 0;
5683
5854
  child.stdout.on("data", (chunk) => {
5684
5855
  bytes += chunk.byteLength;
@@ -5686,20 +5857,20 @@ async function gitOutput(cwd, args, maxBytes) {
5686
5857
  else stdout.push(chunk);
5687
5858
  });
5688
5859
  child.stderr.on("data", (chunk) => {
5689
- if (stderr.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr.push(chunk);
5860
+ if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
5690
5861
  });
5691
5862
  const code = await new Promise((accept, reject) => {
5692
5863
  child.once("error", reject);
5693
5864
  child.once("exit", accept);
5694
5865
  });
5695
5866
  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)}`);
5867
+ if (code !== 0) throw new Error(`git command failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
5697
5868
  return Buffer.concat(stdout);
5698
5869
  }
5699
5870
  async function gitBlobs(cwd, entries, maxBytes) {
5700
5871
  const child = spawn22("git", ["cat-file", "--batch"], { cwd, stdio: ["pipe", "pipe", "pipe"], shell: false });
5701
5872
  const stdout = [];
5702
- const stderr = [];
5873
+ const stderr2 = [];
5703
5874
  let bytes = 0;
5704
5875
  child.stdout.on("data", (chunk) => {
5705
5876
  bytes += chunk.byteLength;
@@ -5707,7 +5878,7 @@ async function gitBlobs(cwd, entries, maxBytes) {
5707
5878
  else stdout.push(chunk);
5708
5879
  });
5709
5880
  child.stderr.on("data", (chunk) => {
5710
- if (stderr.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr.push(chunk);
5881
+ if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
5711
5882
  });
5712
5883
  child.stdin.end(`${entries.map((entry) => entry.hash).join("\n")}
5713
5884
  `);
@@ -5716,7 +5887,7 @@ async function gitBlobs(cwd, entries, maxBytes) {
5716
5887
  child.once("exit", accept);
5717
5888
  });
5718
5889
  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)}`);
5890
+ if (code !== 0) throw new Error(`git object read failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
5720
5891
  const output = Buffer.concat(stdout);
5721
5892
  const blobs = [];
5722
5893
  let offset = 0;
@@ -5813,7 +5984,7 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
5813
5984
  shell: false
5814
5985
  });
5815
5986
  const stdout = [];
5816
- const stderr = [];
5987
+ const stderr2 = [];
5817
5988
  let outputBytes = 0;
5818
5989
  child.stdout.on("data", (chunk) => {
5819
5990
  outputBytes += chunk.byteLength;
@@ -5821,14 +5992,14 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
5821
5992
  else stdout.push(chunk);
5822
5993
  });
5823
5994
  child.stderr.on("data", (chunk) => {
5824
- if (stderr.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr.push(chunk);
5995
+ if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
5825
5996
  });
5826
5997
  const code = await new Promise((accept, reject) => {
5827
5998
  child.once("error", reject);
5828
5999
  child.once("exit", accept);
5829
6000
  });
5830
6001
  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)}`);
6002
+ if (code !== 0) throw new Error(`git file inventory failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
5832
6003
  const paths = Buffer.concat(stdout).toString("utf8").split("\0").filter(Boolean).sort();
5833
6004
  if (paths.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
5834
6005
  const root = resolve22(sourceDir);
@@ -5873,7 +6044,7 @@ async function captureGitDiff(root, maxBytes) {
5873
6044
  "workspace"
5874
6045
  ], { cwd: root, stdio: ["ignore", "pipe", "pipe"], shell: false });
5875
6046
  const stdout = [];
5876
- const stderr = [];
6047
+ const stderr2 = [];
5877
6048
  let bytes = 0;
5878
6049
  child.stdout.on("data", (chunk) => {
5879
6050
  bytes += chunk.byteLength;
@@ -5881,7 +6052,7 @@ async function captureGitDiff(root, maxBytes) {
5881
6052
  else stdout.push(chunk);
5882
6053
  });
5883
6054
  child.stderr.on("data", (chunk) => {
5884
- if (stderr.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr.push(chunk);
6055
+ if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
5885
6056
  });
5886
6057
  const code = await new Promise((accept, reject) => {
5887
6058
  child.once("error", reject);
@@ -5889,7 +6060,7 @@ async function captureGitDiff(root, maxBytes) {
5889
6060
  });
5890
6061
  if (bytes > maxBytes) throw new Error(`patch exceeds ${maxBytes} bytes`);
5891
6062
  if (code !== 0 && code !== 1) {
5892
- throw new Error(`git diff failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
6063
+ throw new Error(`git diff failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
5893
6064
  }
5894
6065
  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
6066
  }
@@ -6297,10 +6468,10 @@ import { randomUUID } from "crypto";
6297
6468
  import { createHash as createHash22, randomUUID as randomUUID2 } from "crypto";
6298
6469
  import { createReadStream } from "fs";
6299
6470
  import { lstat as lstat22 } from "fs/promises";
6300
- import { join as join13 } from "path";
6471
+ import { join as join14 } from "path";
6301
6472
  import { mkdir as mkdir3, mkdtemp as mkdtemp3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
6302
6473
  import { tmpdir as tmpdir3 } from "os";
6303
- import { dirname as dirname9, join as join23, resolve as resolve32, sep as sep23 } from "path";
6474
+ import { dirname as dirname10, join as join23, resolve as resolve32, sep as sep23 } from "path";
6304
6475
  import {
6305
6476
  keepRecentExchanges,
6306
6477
  runAgent
@@ -6696,11 +6867,11 @@ function rollup(graph, kind, options = {}) {
6696
6867
  }
6697
6868
 
6698
6869
  // ../graph/dist/code/index.js
6699
- function dirname8(path) {
6870
+ function dirname9(path) {
6700
6871
  const at = path.lastIndexOf("/");
6701
6872
  return at <= 0 ? "." : path.slice(0, at);
6702
6873
  }
6703
- function join12(base, specifier) {
6874
+ function join13(base, specifier) {
6704
6875
  const parts = [];
6705
6876
  const segments = `${base === "." ? "" : `${base}/`}${specifier}`.split("/");
6706
6877
  for (const segment of segments) {
@@ -6724,7 +6895,7 @@ var BARE_IMPORT = /^\s*import\s*["']([^"']+)["']/gm;
6724
6895
  var isSourcePath = (path) => SOURCE.test(path);
6725
6896
  function resolveImport(fromPath, specifier, known) {
6726
6897
  if (!specifier.startsWith(".")) return null;
6727
- const base = join12(dirname8(fromPath), specifier);
6898
+ const base = join13(dirname9(fromPath), specifier);
6728
6899
  const candidates = [
6729
6900
  base,
6730
6901
  base.replace(/\.js$/, ".ts"),
@@ -7255,13 +7426,13 @@ function gitApply(cwd, patch2, check) {
7255
7426
  stdio: ["pipe", "ignore", "pipe"],
7256
7427
  env: { PATH: process.env.PATH ?? "", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null" }
7257
7428
  });
7258
- let stderr = "";
7429
+ let stderr2 = "";
7259
7430
  child.stderr.setEncoding("utf8");
7260
7431
  child.stderr.on("data", (text3) => {
7261
- if (stderr.length < 4e3) stderr += text3.slice(0, 4e3);
7432
+ if (stderr2.length < 4e3) stderr2 += text3.slice(0, 4e3);
7262
7433
  });
7263
7434
  child.once("error", reject);
7264
- child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(describePatchFailure(patch2, stderr.trim().slice(0, 500)))));
7435
+ child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(describePatchFailure(patch2, stderr2.trim().slice(0, 500)))));
7265
7436
  child.stdin.end(patch2);
7266
7437
  });
7267
7438
  }
@@ -7374,7 +7545,7 @@ function execute(engine, args, name, recipe2, signal) {
7374
7545
  const started = Date.now();
7375
7546
  const child = spawn23(engine, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
7376
7547
  const stdout = [];
7377
- const stderr = [];
7548
+ const stderr2 = [];
7378
7549
  let bytes = 0;
7379
7550
  let outputLimitExceeded = false;
7380
7551
  let timedOut = false;
@@ -7395,7 +7566,7 @@ function execute(engine, args, name, recipe2, signal) {
7395
7566
  else target.push(chunk);
7396
7567
  };
7397
7568
  child.stdout.on("data", collect(stdout));
7398
- child.stderr.on("data", collect(stderr));
7569
+ child.stderr.on("data", collect(stderr2));
7399
7570
  const abort = () => stop("abort");
7400
7571
  signal?.addEventListener("abort", abort, { once: true });
7401
7572
  if (signal?.aborted) abort();
@@ -7411,7 +7582,7 @@ function execute(engine, args, name, recipe2, signal) {
7411
7582
  accept({
7412
7583
  exitCode: code ?? 1,
7413
7584
  stdout: Buffer.concat(stdout).toString("utf8"),
7414
- stderr: Buffer.concat(stderr).toString("utf8"),
7585
+ stderr: Buffer.concat(stderr2).toString("utf8"),
7415
7586
  durationMs: Date.now() - started,
7416
7587
  outputLimitExceeded,
7417
7588
  timedOut
@@ -7527,7 +7698,7 @@ async function inspectArtifacts(workspaceDir, recipe2) {
7527
7698
  const receipts = [];
7528
7699
  for (const artifact of recipe2.expectedArtifacts ?? []) {
7529
7700
  try {
7530
- const path = join13(workspaceDir, artifact.path);
7701
+ const path = join14(workspaceDir, artifact.path);
7531
7702
  const info = await lstat22(path);
7532
7703
  if (!info.isFile() || info.isSymbolicLink()) {
7533
7704
  receipts.push({ artifactId: artifact.id, status: "invalid", bytes: null, digest: null });
@@ -7563,11 +7734,11 @@ function checkedResult(result, maximumOutputBytes) {
7563
7734
  }
7564
7735
  function boundedLogs(result, maximum) {
7565
7736
  const stdout = Buffer.from(result.stdout);
7566
- const stderr = Buffer.from(result.stderr);
7737
+ const stderr2 = Buffer.from(result.stderr);
7567
7738
  const first = stdout.subarray(0, maximum);
7568
7739
  return {
7569
7740
  stdout: first.toString("utf8"),
7570
- stderr: stderr.subarray(0, Math.max(0, maximum - first.byteLength)).toString("utf8")
7741
+ stderr: stderr2.subarray(0, Math.max(0, maximum - first.byteLength)).toString("utf8")
7571
7742
  };
7572
7743
  }
7573
7744
  function digestPolicy(policy) {
@@ -7813,7 +7984,7 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = tmpdir3()) {
7813
7984
  if (bytes > 16 * 1024 * 1024) throw new TypeError("Code source exceeds its byte bound");
7814
7985
  const target = resolve32(sourceDir, file.path);
7815
7986
  if (!target.startsWith(`${resolve32(sourceDir)}${sep23}`)) throw new TypeError("Code source path escapes its root");
7816
- await mkdir3(dirname9(target), { recursive: true });
7987
+ await mkdir3(dirname10(target), { recursive: true });
7817
7988
  await writeFile3(target, file.content, { flag: "wx", mode: 420 });
7818
7989
  }
7819
7990
  for (const reference of snapshot.references ?? []) {
@@ -7828,7 +7999,7 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = tmpdir3()) {
7828
7999
  if (bytes > 80 * 1024 * 1024) throw new TypeError("Code source set exceeds its byte bound");
7829
8000
  const target = resolve32(sourceDir, path);
7830
8001
  if (!target.startsWith(`${resolve32(sourceDir)}${sep23}`)) throw new TypeError("Code reference path escapes its root");
7831
- await mkdir3(dirname9(target), { recursive: true });
8002
+ await mkdir3(dirname10(target), { recursive: true });
7832
8003
  await writeFile3(target, file.content, { flag: "wx", mode: 292 });
7833
8004
  }
7834
8005
  }
@@ -7855,7 +8026,7 @@ async function attachCodeRuntimeReferences(workspace, references) {
7855
8026
  for (const root of [workspace.baselineDir, workspace.workspaceDir]) {
7856
8027
  const target = resolve32(root, path);
7857
8028
  if (!target.startsWith(`${resolve32(root)}${sep23}`)) throw new TypeError("Code reference path escapes its root");
7858
- await mkdir3(dirname9(target), { recursive: true });
8029
+ await mkdir3(dirname10(target), { recursive: true });
7859
8030
  await writeFile3(target, file.content, { flag: "wx", mode: 292 });
7860
8031
  }
7861
8032
  }
@@ -9677,7 +9848,8 @@ async function codeConnect(options) {
9677
9848
  );
9678
9849
  try {
9679
9850
  const descriptor2 = localSource.descriptor;
9680
- const approval = await (options.getToken ?? getScopedPlatformToken)({
9851
+ const device = options.getToken ? null : readDeviceCredential(platform);
9852
+ const authorization = device ? (await mintDeviceSession(platform, device, doFetch)).token : await (options.getToken ?? getScopedPlatformToken)({
9681
9853
  platform,
9682
9854
  scope: "app:code:host:connect",
9683
9855
  email: options.email,
@@ -9691,7 +9863,7 @@ async function codeConnect(options) {
9691
9863
  const target = appId ? { appId } : { repository };
9692
9864
  const response2 = await doFetch(`${platform}/registry/code/hosts/connect`, {
9693
9865
  method: "POST",
9694
- headers: { authorization: `Bearer ${approval}`, "content-type": "application/json" },
9866
+ headers: { authorization: `Bearer ${authorization}`, "content-type": "application/json" },
9695
9867
  body: JSON.stringify({ ...target, env: appEnv, name: hostName, platform: hostPlatform, slots }),
9696
9868
  redirect: "error",
9697
9869
  signal: options.signal
@@ -10072,13 +10244,13 @@ async function codeCommand(parsed, dependencies) {
10072
10244
  }
10073
10245
 
10074
10246
  // src/operator-credentials.ts
10075
- import process13 from "process";
10247
+ import process17 from "process";
10076
10248
  function developerTokenStatus(context, parsed, now = Date.now()) {
10077
10249
  const cached = readJsonFile(context.cfg.local.tokenFile);
10078
10250
  const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
10079
10251
  const source = clean3(
10080
10252
  stringOpt(parsed.options.token)
10081
- ) ? "flag" : clean3(process13.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
10253
+ ) ? "flag" : clean3(process17.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
10082
10254
  return {
10083
10255
  source,
10084
10256
  cacheFile: context.cfg.local.tokenFile,
@@ -10272,6 +10444,27 @@ async function credentialCommand(parsed, deps = {}) {
10272
10444
  }
10273
10445
 
10274
10446
  // src/help-usage.ts
10447
+ var AUTH_SECTION = `
10448
+ Enrol this machine once, then stop asking:
10449
+ npx odla-ai device enroll --all-apps --capability all --no-open --wait 600
10450
+ One browser approval. Afterwards every worktree on this machine mints its
10451
+ own short-lived credentials with nobody's attention, for every app you
10452
+ own \u2014 including apps you create later. The window rolls forward each time
10453
+ you use it, so continuous work never interrupts anyone; only a real gap
10454
+ does. Give the human the printed /studio?code= URL, keep the process
10455
+ alive, and wait on it.
10456
+
10457
+ npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600
10458
+ The same thing across all of odla, for weeks. Needs a platform
10459
+ administrator's approval \u2014 an app owner's cannot carry platform scopes.
10460
+
10461
+ npx odla-ai whoami what this machine holds and when it lapses
10462
+ npx odla-ai device list every machine you have enrolled
10463
+
10464
+ Enrollment is the only human decision here. Revoking a machine, purging an app,
10465
+ transferring ownership, and rotating credentials still need a signed-in human in
10466
+ Studio, and no machine credential can do them however wide its approval was.
10467
+ `;
10275
10468
  var USAGE_SECTION = `
10276
10469
  Start here:
10277
10470
  odla-ai runbook ask "<question>" The current procedure, from odla's own
@@ -10310,7 +10503,7 @@ Usage:
10310
10503
  odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]
10311
10504
  odla-ai pm project list [--app <product-id>] [--status <s>] [--json]
10312
10505
  odla-ai pm project add --app <product-id> --name <name> [--description <text>] [--json]
10313
- odla-ai pm project use <project-id> [--json] [saved locally in this worktree]
10506
+ odla-ai pm project use <project-id> [--json] [saved for this app, on this machine]
10314
10507
  odla-ai pm goal list [--app <id>] [--project <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
10315
10508
  odla-ai pm task list [--app <id>] [--column <backlog|ready|doing|review|done>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
10316
10509
  odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
@@ -10402,7 +10595,8 @@ Usage:
10402
10595
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
10403
10596
  odla-ai security run [target] --self --ack-redacted-source
10404
10597
  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]
10405
- odla-ai device enroll [--app <id>[,<id>...]] [--name <label>] [--capability <c>[,<c>...]] [--device-ttl <30d|6w|2y|forever>] [--email <odla-account>] [--no-open] [--json]
10598
+ odla-ai device enroll [--all-apps|--app <id>[,<id>...]] [--capability all|<c>[,<c>...]] [--platform-wide]
10599
+ [--name <label>] [--device-ttl <30d|6w|2y|forever>] [--email <odla-account>] [--no-open] [--wait <seconds>] [--json]
10406
10600
  odla-ai device list [--email <odla-account>] [--json]
10407
10601
  odla-ai device revoke <device-id> [--email <odla-account>] [--json]
10408
10602
  odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
@@ -10416,8 +10610,11 @@ Usage:
10416
10610
 
10417
10611
  // src/help.ts
10418
10612
  function printHelp(output = console) {
10419
- output.log(`odla-ai
10420
- ${USAGE_SECTION}
10613
+ output.log(helpText());
10614
+ }
10615
+ function helpText() {
10616
+ return `odla-ai
10617
+ ${AUTH_SECTION}${USAGE_SECTION}
10421
10618
  Commands:
10422
10619
  auth Start a fresh, exact-project agent authorization for human review.
10423
10620
  The email is the signed-in odla account, never git or GitHub
@@ -10490,8 +10687,9 @@ Commands:
10490
10687
  security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
10491
10688
  pm Project management (via @odla-ai/pm): Products contain Projects;
10492
10689
  projects contain goals, kanban tasks, decisions, and bugs. Use
10493
- "pm project list|add|use" to select worktree-local context, or
10494
- pass --app/--project explicitly. Same device-grant auth as "app".
10690
+ "pm project list|add|use" selects a project for this app on this
10691
+ machine \u2014 every worktree shares it \u2014 or pass --app/--project
10692
+ explicitly. Same device-grant auth as "app".
10495
10693
  Status changes and comments post to each item's @odla-ai/chat
10496
10694
  discussion thread.
10497
10695
  NOTE: "--column ready" is OWNER-ONLY. Creating a task in Ready
@@ -10520,9 +10718,20 @@ Commands:
10520
10718
  platform Read canonical fleet health, releases, provider load/freshness,
10521
10719
  explicit unknowns, and next actions through a read-only grant.
10522
10720
  device Enrol THIS machine once, then stop asking. A human approves the
10523
- enrollment in the browser; from then on this terminal mints its
10524
- own short-lived credentials for the named projects with nobody's
10525
- attention, until the device expires or is revoked.
10721
+ enrollment in the browser; from then on EVERY worktree on this
10722
+ machine mints its own short-lived credentials with nobody's
10723
+ attention, until the device is revoked or goes unused.
10724
+ "--all-apps" covers every app you own, now and later, so creating
10725
+ an app costs no new approval. "--capability all" takes everything
10726
+ that approval is allowed to carry, so a capability you did not
10727
+ think to name is not a 403 next week. "--platform-wide" is the
10728
+ administrator's version, across all of odla.
10729
+ The expiry is a GAP, not a clock: each use rolls it forward, so
10730
+ only going quiet brings a human back into the loop \u2014 which is
10731
+ where anything that changed can be explained.
10732
+ "device list" shows what each machine holds and when it lapses;
10733
+ revoking one takes down every credential it ever minted, and is
10734
+ deliberately a signed-in human's decision in Studio.
10526
10735
  provision Register services, compose integrations, persist credentials, optionally push secrets.
10527
10736
  "provision --live --yes" initializes only the live instance of
10528
10737
  an existing sandbox app and enables every configured service;
@@ -10592,10 +10801,12 @@ Safety:
10592
10801
  release. A confirmed stale client stops with a safe npx rerun command; a
10593
10802
  workspace-linked client also identifies the worktree that must be updated.
10594
10803
  Run Code from a GitHub checkout already connected to an app in Studio; an
10595
- odla.config.mjs may select the app explicitly but is not required. Code host
10596
- approval and credential hashes live in odla-ai/db. The host
10597
- credential is never written under .odla/; it exists only in the foreground
10598
- "code connect" process and is rotated by the next approved connection.
10804
+ odla.config.mjs may select the app explicitly but is not required. With an
10805
+ enrolled code.session device, the Studio repository selection authorizes the
10806
+ host's first connection and reconnects without another human approval. Without
10807
+ an enrolled device, "code connect" falls back to the reviewed handshake. Host
10808
+ credential hashes live in odla-ai/db; the credential itself is never written
10809
+ under .odla/, exists only in the foreground process, and rotates on reconnect.
10599
10810
  "code repository bind" takes owner/name and resolves the two GitHub integers the
10600
10811
  bind route wants across every installation you have, refusing an ambiguous match
10601
10812
  rather than choosing one \u2014 the same repository name under two organizations is
@@ -10616,7 +10827,46 @@ Safety:
10616
10827
  Run security plan first to inspect the admin-selected providers, models,
10617
10828
  per-route bounds, credential readiness, retention, no-execution boundary,
10618
10829
  and digest that binds consent to that exact plan.
10619
- `);
10830
+ `;
10831
+ }
10832
+
10833
+ // src/help-command.ts
10834
+ function printCommandHelp(command, output = console) {
10835
+ const lines = helpText().split("\n");
10836
+ const usage = allBlocks(lines, new RegExp(`^ odla-ai ${escapeRe(command)}(\\s|$)`));
10837
+ const prose = block(lines, (line2) => new RegExp(`^ ${escapeRe(command)}\\s\\s+\\S`).test(line2));
10838
+ if (usage.length === 0 && prose.length === 0) {
10839
+ output.log(`odla-ai: no command "${command}". Run "odla-ai help" for all of them.`);
10840
+ return;
10841
+ }
10842
+ output.log([
10843
+ ...prose.length ? [prose.join("\n"), ""] : [],
10844
+ ...usage.length ? ["Usage:", ...usage, ""] : [],
10845
+ AUTH_SECTION.trimEnd()
10846
+ ].join("\n"));
10847
+ }
10848
+ function allBlocks(lines, pattern) {
10849
+ const out = [];
10850
+ for (let i = 0; i < lines.length; i++) {
10851
+ if (!pattern.test(lines[i])) continue;
10852
+ out.push(...block(lines.slice(i), (line2) => line2 === lines[i]));
10853
+ }
10854
+ return out;
10855
+ }
10856
+ function block(lines, starts) {
10857
+ const first = lines.findIndex(starts);
10858
+ if (first === -1) return [];
10859
+ const indent = lines[first].length - lines[first].trimStart().length;
10860
+ const out = [lines[first]];
10861
+ for (const line2 of lines.slice(first + 1)) {
10862
+ if (!line2.trim()) break;
10863
+ if (line2.length - line2.trimStart().length <= indent) break;
10864
+ out.push(line2);
10865
+ }
10866
+ return out;
10867
+ }
10868
+ function escapeRe(value2) {
10869
+ return value2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10620
10870
  }
10621
10871
 
10622
10872
  // src/discuss-principals.ts
@@ -11773,14 +12023,36 @@ async function pmWatch(ctx, parsed) {
11773
12023
  }
11774
12024
 
11775
12025
  // src/pm-project-context.ts
12026
+ import { existsSync as existsSync12, rmSync as rmSync5 } from "fs";
11776
12027
  import { resolve as resolve12 } from "path";
11777
- var pmProjectContextFile = (rootDir) => resolve12(rootDir, ".odla", "pm-project.local.json");
12028
+ var pmProjectContextFile = () => pmContextFile();
11778
12029
  function readPmProjectContext(rootDir) {
11779
- const value2 = readJsonFile(pmProjectContextFile(rootDir));
11780
- return value2 && typeof value2.appId === "string" && typeof value2.projectId === "string" ? value2 : null;
12030
+ adoptLegacySelection(rootDir);
12031
+ const entries = Object.values(readSelections()).filter(isSelection);
12032
+ return entries.sort((a, b) => b.selectedAt.localeCompare(a.selectedAt))[0] ?? null;
11781
12033
  }
11782
12034
  function writePmProjectContext(rootDir, value2) {
11783
- writePrivateJson(pmProjectContextFile(rootDir), { ...value2, selectedAt: (/* @__PURE__ */ new Date()).toISOString() });
12035
+ adoptLegacySelection(rootDir);
12036
+ writePrivateJson(pmProjectContextFile(), {
12037
+ ...readSelections(),
12038
+ [value2.appId]: { ...value2, selectedAt: (/* @__PURE__ */ new Date()).toISOString() }
12039
+ });
12040
+ }
12041
+ function adoptLegacySelection(rootDir) {
12042
+ const legacy = resolve12(rootDir, ".odla", "pm-project.local.json");
12043
+ if (!existsSync12(legacy)) return;
12044
+ const previous = readJsonFile(legacy);
12045
+ rmSync5(legacy, { force: true });
12046
+ if (!isSelection(previous)) return;
12047
+ const selections = readSelections();
12048
+ if (selections[previous.appId]) return;
12049
+ writePrivateJson(pmProjectContextFile(), { ...selections, [previous.appId]: previous });
12050
+ }
12051
+ function readSelections() {
12052
+ return readJsonFile(pmProjectContextFile()) ?? {};
12053
+ }
12054
+ function isSelection(value2) {
12055
+ return !!value2 && typeof value2.appId === "string" && typeof value2.projectId === "string" && typeof value2.selectedAt === "string";
11784
12056
  }
11785
12057
 
11786
12058
  // src/pm-project-actions.ts
@@ -12805,7 +13077,7 @@ function percent(value2) {
12805
13077
  // src/provision.ts
12806
13078
  import { AppsError as AppsError2, createAppsClient as createAppsClient3, orderAppServices as orderAppServices3, tenantIdFor as tenantIdFor6 } from "@odla-ai/apps";
12807
13079
  import { putSecret as putSecret2 } from "@odla-ai/ai";
12808
- import process14 from "process";
13080
+ import process18 from "process";
12809
13081
 
12810
13082
  // src/integration-provision.ts
12811
13083
  import { uuidv7 } from "@odla-ai/db";
@@ -13243,7 +13515,7 @@ async function provision(options) {
13243
13515
  await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
13244
13516
  }
13245
13517
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
13246
- const key = process14.env[cfg.ai.keyEnv];
13518
+ const key = process18.env[cfg.ai.keyEnv];
13247
13519
  if (key) {
13248
13520
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
13249
13521
  await putSecret2({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -13285,7 +13557,7 @@ async function provision(options) {
13285
13557
 
13286
13558
  // src/record.ts
13287
13559
  import { appendFileSync } from "fs";
13288
- import process15 from "process";
13560
+ import process19 from "process";
13289
13561
 
13290
13562
  // src/surface.ts
13291
13563
  var PM_ACTIONS = {
@@ -13467,7 +13739,7 @@ function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
13467
13739
 
13468
13740
  // src/record.ts
13469
13741
  function recordInvocation(parsed) {
13470
- const file = process15.env.ODLA_CLI_RECORD;
13742
+ const file = process19.env.ODLA_CLI_RECORD;
13471
13743
  if (!file) return;
13472
13744
  try {
13473
13745
  const entry = {
@@ -13504,6 +13776,14 @@ function renderAdvisories(out, advisories, env = process.env) {
13504
13776
  }
13505
13777
  }
13506
13778
 
13779
+ // src/device-command.ts
13780
+ import {
13781
+ ADMIN_DEVICE_SCOPES,
13782
+ ALL_OWNED_APPS,
13783
+ OPTIONAL_AGENT_PROJECT_CAPABILITIES,
13784
+ OWNER_DEVICE_SCOPES
13785
+ } from "@odla-ai/db";
13786
+
13507
13787
  // src/device-ttl.ts
13508
13788
  var OWNER_DEVICE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
13509
13789
  var DAY_MS = 24 * 60 * 60 * 1e3;
@@ -13523,10 +13803,26 @@ function parseDeviceTtl(raw) {
13523
13803
  }
13524
13804
 
13525
13805
  // src/device-command.ts
13526
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
13527
- import { dirname as dirname10 } from "path";
13528
- import process16 from "process";
13806
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "fs";
13807
+ import { dirname as dirname11 } from "path";
13808
+ import process20 from "process";
13529
13809
  async function deviceCommand(parsed, deps) {
13810
+ assertArgs(parsed, [
13811
+ "app",
13812
+ "all-apps",
13813
+ "platform-wide",
13814
+ "name",
13815
+ "capability",
13816
+ "device-ttl",
13817
+ "email",
13818
+ "open",
13819
+ "json",
13820
+ "config",
13821
+ "token",
13822
+ "context",
13823
+ "platform",
13824
+ "wait"
13825
+ ], 3);
13530
13826
  const action2 = parsed.positionals[1] ?? "";
13531
13827
  const out = deps.stdout ?? console;
13532
13828
  const doFetch = deps.fetch ?? fetch;
@@ -13539,10 +13835,12 @@ async function deviceCommand(parsed, deps) {
13539
13835
  }
13540
13836
  async function enroll(parsed, deps, cfg, doFetch, out, json) {
13541
13837
  const name = stringOpt(parsed.options.name) ?? defaultDeviceName();
13542
- const apps = (stringOpt(parsed.options.app) ?? cfg.app.id).split(",").map((id2) => id2.trim()).filter(Boolean);
13543
- if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026]");
13838
+ const platformWide = parsed.options["platform-wide"] === true;
13839
+ const apps = platformWide || parsed.options["all-apps"] === true ? [ALL_OWNED_APPS] : (stringOpt(parsed.options.app) ?? cfg.app.id).split(",").map((id2) => id2.trim()).filter(Boolean);
13840
+ if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026], or --all-apps");
13544
13841
  const deviceTtlMs = parseDeviceTtl(parsed.options["device-ttl"]);
13545
- const extended = deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
13842
+ const extended = platformWide || deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
13843
+ const { capabilities, scopes } = requestedEnvelope(parsed, platformWide);
13546
13844
  const token = await scopedToken2(
13547
13845
  parsed,
13548
13846
  deps,
@@ -13557,9 +13855,10 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
13557
13855
  headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
13558
13856
  body: JSON.stringify({
13559
13857
  name,
13560
- platform: process16.platform,
13858
+ platform: process20.platform,
13561
13859
  appIds: apps,
13562
- ...parsed.options.capability ? { capabilities: String(parsed.options.capability).split(",").map((c) => c.trim()).filter(Boolean) } : {},
13860
+ ...capabilities ? { capabilities } : {},
13861
+ ...scopes ? { scopes } : {},
13563
13862
  ...deviceTtlMs === void 0 ? {} : { deviceTtlMs }
13564
13863
  })
13565
13864
  });
@@ -13568,20 +13867,53 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
13568
13867
  throw new Error(`device enroll failed: ${body.error?.message ?? `registry returned ${response2.status}`} (${response2.status})`);
13569
13868
  }
13570
13869
  const path = deviceCredentialPath();
13571
- mkdirSync4(dirname10(path), { recursive: true });
13870
+ mkdirSync5(dirname11(path), { recursive: true });
13572
13871
  writeFileSync4(path, JSON.stringify({
13573
13872
  token: body.token,
13574
13873
  platform: cfg.platformUrl.replace(/\/$/, ""),
13575
13874
  deviceId: body.device.deviceId,
13576
13875
  name
13577
13876
  }, null, 2));
13578
- chmodSync2(path, 384);
13579
- out.error(`device: enrolled "${name}" for ${body.device.appIds.join(", ")}; credential written to ${path}`);
13580
- out.error("device: this terminal will mint its own credentials from now on \u2014 no further approvals.");
13877
+ chmodSync3(path, 384);
13878
+ rememberMachineIdentity(cfg.platformUrl.replace(/\/$/, ""), stringOpt(parsed.options.email));
13879
+ const reach = body.device.appIds.includes(ALL_OWNED_APPS) ? "every app you own, now and later" : body.device.appIds.join(", ");
13880
+ out.error(`device: enrolled "${name}" for ${reach}; credential written to ${path}`);
13881
+ out.error(
13882
+ "device: every worktree on this machine mints its own credentials from now on \u2014 no further approvals,"
13883
+ );
13884
+ out.error(
13885
+ `device: and the clock resets each time you use it. Going quiet for ${describeWindow(body.device.expiresAt)} is what ends it.`
13886
+ );
13581
13887
  if (json) {
13582
- out.log(JSON.stringify({ deviceId: body.device.deviceId, name, appIds: body.device.appIds, expiresAt: body.device.expiresAt }, null, 2));
13888
+ out.log(JSON.stringify({
13889
+ deviceId: body.device.deviceId,
13890
+ name,
13891
+ appIds: body.device.appIds,
13892
+ capabilities: body.device.capabilities ?? [],
13893
+ scopes: body.device.scopes ?? [],
13894
+ expiresAt: body.device.expiresAt,
13895
+ hardExpiresAt: body.device.hardExpiresAt ?? null
13896
+ }, null, 2));
13583
13897
  }
13584
13898
  }
13899
+ function requestedEnvelope(parsed, platformWide) {
13900
+ const raw = stringOpt(parsed.options.capability);
13901
+ const everything = platformWide || raw?.trim().toLowerCase() === "all";
13902
+ if (everything) {
13903
+ return {
13904
+ capabilities: [...OPTIONAL_AGENT_PROJECT_CAPABILITIES],
13905
+ scopes: platformWide ? [...ADMIN_DEVICE_SCOPES] : [...OWNER_DEVICE_SCOPES]
13906
+ };
13907
+ }
13908
+ const named = raw?.split(",").map((c) => c.trim()).filter(Boolean);
13909
+ return named?.length ? { capabilities: named } : {};
13910
+ }
13911
+ function describeWindow(expiresAt, now = Date.now()) {
13912
+ const days = Math.max(1, Math.round((expiresAt - now) / (24 * 60 * 60 * 1e3)));
13913
+ if (days >= 365) return `${Math.round(days / 365)} year${days >= 730 ? "s" : ""}`;
13914
+ if (days % 7 === 0) return `${days / 7} week${days > 7 ? "s" : ""}`;
13915
+ return `${days} day${days === 1 ? "" : "s"}`;
13916
+ }
13585
13917
  async function list2(parsed, deps, cfg, doFetch, out, json) {
13586
13918
  const token = await scopedToken2(parsed, deps, cfg, doFetch, out, "odla CLI (device list)");
13587
13919
  const response2 = await doFetch(`${cfg.platformUrl}/registry/devices`, {
@@ -13595,12 +13927,17 @@ async function list2(parsed, deps, cfg, doFetch, out, json) {
13595
13927
  if (body.devices.length === 0) return out.log("no enrolled devices");
13596
13928
  for (const device of body.devices) {
13597
13929
  const state2 = device.revokedAt ? "revoked" : device.expiresAt <= Date.now() ? "expired" : "active";
13598
- out.log(`${device.deviceId} ${state2.padEnd(7)} ${device.name} [${device.appIds.join(", ")}]`);
13930
+ const reach = device.appIds.includes("*") ? "every app you own" : device.appIds.join(", ");
13931
+ const gap = state2 === "active" ? ` idle ${describeWindow(device.expiresAt)} left` : "";
13932
+ out.log(`${device.deviceId} ${state2.padEnd(7)} ${device.name} [${reach}]${gap}`);
13599
13933
  }
13600
13934
  }
13601
13935
  async function revoke(parsed, deps, cfg, doFetch, out, json) {
13602
13936
  const deviceId = parsed.positionals[2];
13603
13937
  if (!deviceId) throw new Error("device revoke needs the device id from `odla-ai device list`");
13938
+ out.error(
13939
+ `device: revoking is a signed-in human's decision; if this is refused, open ${cfg.platformUrl}/studio and revoke it there.`
13940
+ );
13604
13941
  const token = await scopedToken2(parsed, deps, cfg, doFetch, out, "odla CLI (device revoke)");
13605
13942
  const response2 = await doFetch(`${cfg.platformUrl}/registry/devices/${encodeURIComponent(deviceId)}/revoke`, {
13606
13943
  method: "POST",
@@ -13619,7 +13956,7 @@ async function scopedToken2(parsed, deps, cfg, doFetch, out, label, scope = "app
13619
13956
  // A device is granted the apps named in ONE approval, so --app is a list here.
13620
13957
  allowAppList: true
13621
13958
  });
13622
- const scopedTokenFile = credentials.scopedTokenFile;
13959
+ const scopedTokenFile2 = credentials.scopedTokenFile;
13623
13960
  return getScopedPlatformToken({
13624
13961
  platform: cfg.platformUrl,
13625
13962
  scope,
@@ -13630,12 +13967,12 @@ async function scopedToken2(parsed, deps, cfg, doFetch, out, label, scope = "app
13630
13967
  open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
13631
13968
  openApprovalUrl: deps.openUrl,
13632
13969
  rootDir: cfg.rootDir,
13633
- tokenFile: scopedTokenFile,
13970
+ tokenFile: scopedTokenFile2,
13634
13971
  ...stringOpt(parsed.options.token) ? { token: stringOpt(parsed.options.token) } : {}
13635
13972
  });
13636
13973
  }
13637
13974
  function defaultDeviceName() {
13638
- return `${process16.env.HOSTNAME ?? process16.env.HOST ?? "machine"}-${process16.platform}`;
13975
+ return `${process20.env.HOSTNAME ?? process20.env.HOST ?? "machine"}-${process20.platform}`;
13639
13976
  }
13640
13977
 
13641
13978
  // src/runbook-actions.ts
@@ -13779,7 +14116,7 @@ async function runbookRemove(ctx, slug) {
13779
14116
 
13780
14117
  // src/runbook-import.ts
13781
14118
  import { readFileSync as readFileSync11, readdirSync as readdirSync2, statSync } from "fs";
13782
- import { basename as basename2, join as join14 } from "path";
14119
+ import { basename as basename2, join as join15 } from "path";
13783
14120
  function parseRunbook(text3, slug) {
13784
14121
  let rest = text3;
13785
14122
  const meta = {};
@@ -13809,7 +14146,7 @@ function readRunbookDir(dir) {
13809
14146
  if (!files.length) throw new Error(`no .md files in ${dir}`);
13810
14147
  return files.map((file) => {
13811
14148
  const slug = basename2(file, ".md");
13812
- const parsed = parseRunbook(readFileSync11(join14(dir, file), "utf8"), slug);
14149
+ const parsed = parseRunbook(readFileSync11(join15(dir, file), "utf8"), slug);
13813
14150
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
13814
14151
  });
13815
14152
  }
@@ -13877,8 +14214,8 @@ async function upsert(ctx, r, visibility) {
13877
14214
 
13878
14215
  // src/runbook-impact.ts
13879
14216
  import { execFileSync as execFileSync2 } from "child_process";
13880
- import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
13881
- import { join as join15 } from "path";
14217
+ import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
14218
+ import { join as join16 } from "path";
13882
14219
 
13883
14220
  // src/runbook-impact-scan.ts
13884
14221
  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$]*)/;
@@ -14047,8 +14384,8 @@ ${body.split("\n").map((line2) => `+${line2}`).join("\n")}
14047
14384
  }
14048
14385
  function manifestLabeller(root) {
14049
14386
  return (workspace) => {
14050
- const manifest = join15(root, workspace, "package.json");
14051
- if (!existsSync12(manifest)) return void 0;
14387
+ const manifest = join16(root, workspace, "package.json");
14388
+ if (!existsSync13(manifest)) return void 0;
14052
14389
  try {
14053
14390
  const name = JSON.parse(readFileSync12(manifest, "utf8")).name;
14054
14391
  return typeof name === "string" ? name : void 0;
@@ -14117,7 +14454,7 @@ function report4(ctx, impacts) {
14117
14454
  async function runbookImpact(ctx, options, deps = {}) {
14118
14455
  const cwd = deps.cwd ?? process.cwd();
14119
14456
  const runGit = deps.runGit ?? gitRunner(cwd);
14120
- const read3 = deps.readRepoFile ?? ((path) => readFileSync12(join15(cwd, path), "utf8"));
14457
+ const read3 = deps.readRepoFile ?? ((path) => readFileSync12(join16(cwd, path), "utf8"));
14121
14458
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
14122
14459
  if (!surfaces.length) {
14123
14460
  return ctx.out.log(
@@ -14244,12 +14581,12 @@ async function runbookComment(ctx, slug, body) {
14244
14581
 
14245
14582
  // src/runbook-editor.ts
14246
14583
  import { spawnSync } from "child_process";
14247
- import { mkdtempSync, readFileSync as readFileSync13, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
14584
+ import { mkdtempSync, readFileSync as readFileSync13, rmSync as rmSync6, writeFileSync as writeFileSync5 } from "fs";
14248
14585
  import { tmpdir as tmpdir4 } from "os";
14249
- import { join as join16 } from "path";
14250
- import process17 from "process";
14586
+ import { join as join17 } from "path";
14587
+ import process21 from "process";
14251
14588
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
14252
- function resolveEditor(env = process17.env) {
14589
+ function resolveEditor(env = process21.env) {
14253
14590
  for (const name of EDITOR_ENV) {
14254
14591
  const value2 = env[name];
14255
14592
  if (value2 && value2.trim()) return value2.trim();
@@ -14263,8 +14600,8 @@ function defaultRun(command, path) {
14263
14600
  return result.status ?? 0;
14264
14601
  }
14265
14602
  function editText(initial, slug, deps = {}) {
14266
- const env = deps.env ?? process17.env;
14267
- const interactive = deps.interactive ?? (() => Boolean(process17.stdin.isTTY));
14603
+ const env = deps.env ?? process21.env;
14604
+ const interactive = deps.interactive ?? (() => Boolean(process21.stdin.isTTY));
14268
14605
  const editor = resolveEditor(env);
14269
14606
  if (!editor)
14270
14607
  throw new Error(
@@ -14272,8 +14609,8 @@ function editText(initial, slug, deps = {}) {
14272
14609
  );
14273
14610
  if (!interactive())
14274
14611
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
14275
- const dir = mkdtempSync(join16(tmpdir4(), "odla-runbook-"));
14276
- const file = join16(dir, `${slug}.md`);
14612
+ const dir = mkdtempSync(join17(tmpdir4(), "odla-runbook-"));
14613
+ const file = join17(dir, `${slug}.md`);
14277
14614
  try {
14278
14615
  writeFileSync5(file, initial, { mode: 384 });
14279
14616
  const code = defaultRunOrInjected(deps)(editor, file);
@@ -14281,7 +14618,7 @@ function editText(initial, slug, deps = {}) {
14281
14618
  const edited = readFileSync13(file, "utf8");
14282
14619
  return edited === initial ? null : edited;
14283
14620
  } finally {
14284
- rmSync3(dir, { recursive: true, force: true });
14621
+ rmSync6(dir, { recursive: true, force: true });
14285
14622
  }
14286
14623
  }
14287
14624
  var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
@@ -15193,8 +15530,10 @@ async function dispatchCli(argv, dependencies) {
15193
15530
  return;
15194
15531
  }
15195
15532
  if (command === "help" || command === "--help" || command === "-h") {
15196
- assertArgs(parsed, ["help"], 1);
15197
- printHelp(runtime.stdout);
15533
+ assertArgs(parsed, ["help"], 2);
15534
+ const topic = parsed.positionals[1];
15535
+ if (topic) printCommandHelp(topic, runtime.stdout);
15536
+ else printHelp(runtime.stdout);
15198
15537
  return;
15199
15538
  }
15200
15539
  if (command === "whoami") {
@@ -15408,4 +15747,4 @@ export {
15408
15747
  isTerminalHostedSecurityStatus,
15409
15748
  runCli
15410
15749
  };
15411
- //# sourceMappingURL=chunk-HETCZVFB.js.map
15750
+ //# sourceMappingURL=chunk-PJ3RATDD.js.map