@odla-ai/cli 0.39.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -174,7 +174,7 @@ function absoluteEntryPath(entryPath) {
174
174
 
175
175
  // src/bin.ts
176
176
  var argv = process.argv.slice(2);
177
- requireCurrentCliForProvision(argv).then(() => requireCoherentProvisionRuntime(argv)).then(async () => (await import("./cli-ALS5OZPB.js")).runCli()).catch((err) => {
177
+ requireCurrentCliForProvision(argv).then(() => requireCoherentProvisionRuntime(argv)).then(async () => (await import("./cli-LRJBESE6.js")).runCli()).catch((err) => {
178
178
  console.error(redactSecrets(`odla-ai: ${err instanceof Error ? err.message : String(err)}`));
179
179
  process.exitCode = exitCodeFor(err);
180
180
  });
@@ -208,6 +208,11 @@ import { homedir as homedir2 } from "os";
208
208
  import { dirname as dirname2, join as join3 } from "path";
209
209
  import process6 from "process";
210
210
  function odlaHome(env = process6.env) {
211
+ if (env.VITEST && !env.ODLA_HOME) {
212
+ throw new Error(
213
+ "ODLA_HOME must be set under test \u2014 resolving the real ~/.odla would write to the developer's machine"
214
+ );
215
+ }
211
216
  return env.ODLA_HOME ?? join3(env.HOME ?? homedir2(), ".odla");
212
217
  }
213
218
  function odlaHomePath(segments, env = process6.env) {
@@ -233,15 +238,15 @@ function pmContextFile(env) {
233
238
  }
234
239
  function adoptRepoLocalCache(legacyPath, machinePath, out) {
235
240
  if (!existsSync2(legacyPath) || legacyPath === machinePath) return false;
236
- const superseded = existsSync2(machinePath);
237
- if (!superseded) {
241
+ const superseded2 = existsSync2(machinePath);
242
+ if (!superseded2) {
238
243
  mkdirSync(dirname2(machinePath), { recursive: true });
239
244
  copyFileSync(legacyPath, machinePath);
240
245
  chmodSync(machinePath, 384);
241
246
  }
242
247
  rmSync2(legacyPath, { force: true });
243
248
  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`
249
+ superseded2 ? `auth: removed superseded ${legacyPath}; this machine's credentials live in ${odlaHome()}` : `auth: moved ${legacyPath} into ${machinePath}; credentials are per machine now, not per worktree`
245
250
  );
246
251
  return true;
247
252
  }
@@ -378,7 +383,7 @@ function displayPath(path, rootDir = process.cwd()) {
378
383
  }
379
384
 
380
385
  // src/auth-guidance.ts
381
- var ENROL_EVERYTHING = "npx odla-ai device enroll --all-apps --capability all --no-open --wait 600";
386
+ var ENROL_EVERYTHING = "npx odla-ai device enroll --no-open --wait 600";
382
387
  var ENROL_PLATFORM_WIDE = "npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600";
383
388
  function machineAuthState(audience, env = process7.env) {
384
389
  const device = readDeviceCredential(audience, env);
@@ -404,6 +409,20 @@ function scopeInterruptionNotice(scope, state2) {
404
409
  platformScope ? " A platform scope needs an administrator's approval; an app owner's cannot carry it." : " One approval, every app you own, and it rolls forward while you keep working."
405
410
  ].join("\n");
406
411
  }
412
+ function narrowEnrollmentNotice(envelope) {
413
+ const everyApp = envelope.appIds.includes("*");
414
+ if (everyApp && envelope.capabilities.length > 0 && envelope.scopes.length > 0) return null;
415
+ const missing = [
416
+ everyApp ? null : `only ${envelope.appIds.length} app${envelope.appIds.length === 1 ? "" : "s"} \u2014 a new one will need a new approval`,
417
+ envelope.capabilities.length ? null : "no optional capabilities \u2014 app.manage, crm.read and code.session are not included",
418
+ envelope.scopes.length ? null : "no platform scopes \u2014 runbook edits, config plans and host connect will each ask again"
419
+ ].filter(Boolean);
420
+ return [
421
+ `odla: this is a NARROW enrollment: ${missing.join("; ")}.`,
422
+ " That is a fine choice if you meant it. If you did not, drop the flags:",
423
+ ` ${ENROL_EVERYTHING}`
424
+ ].join("\n");
425
+ }
407
426
  function lapseNotice(state2, now = Date.now()) {
408
427
  if (!state2.enrolled || !state2.lapsesAt) return null;
409
428
  const days = Math.floor((state2.lapsesAt - now) / (24 * 60 * 60 * 1e3));
@@ -2281,6 +2300,11 @@ async function authCommand(parsed, deps = {}) {
2281
2300
  "auth login requires --email <odla-account> or ODLA_USER_EMAIL; confirm the signed-in odla email instead of using git or GitHub identity"
2282
2301
  );
2283
2302
  }
2303
+ if (!machineAuthState(platformAudience(cfg.platformUrl)).enrolled) {
2304
+ out.error("odla: this machine is not enrolled, so this approval buys one project until it lapses.");
2305
+ out.error(` For one approval that covers every app you own, with no repeats:
2306
+ ${ENROL_EVERYTHING}`);
2307
+ }
2284
2308
  const token = await getDeveloperToken(
2285
2309
  cfg,
2286
2310
  {
@@ -10446,13 +10470,16 @@ async function credentialCommand(parsed, deps = {}) {
10446
10470
  // src/help-usage.ts
10447
10471
  var AUTH_SECTION = `
10448
10472
  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.
10473
+ npx odla-ai device enroll --no-open --wait 600
10474
+ One browser approval, and no flags to remember: this covers every app you
10475
+ own \u2014 including apps you create later \u2014 with every capability that
10476
+ approval can carry. Afterwards every worktree on this machine mints its
10477
+ own short-lived credentials with nobody's attention. The window rolls
10478
+ forward each time you use it, so continuous work never interrupts anyone;
10479
+ only a real gap does. Give the human the printed /studio?code= URL, keep
10480
+ the process alive, and wait on it.
10481
+ Narrow it deliberately with --app <id> or --capability <c>; the CLI then
10482
+ says what that gave up.
10456
10483
 
10457
10484
  npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600
10458
10485
  The same thing across all of odla, for weeks. Needs a platform
@@ -10595,7 +10622,7 @@ Usage:
10595
10622
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
10596
10623
  odla-ai security run [target] --self --ack-redacted-source
10597
10624
  odla-ai provision [--live] [--config odla.config.mjs] [--email <odla-account>] [--request-grant] [--no-open] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
10598
- odla-ai device enroll [--all-apps|--app <id>[,<id>...]] [--capability all|<c>[,<c>...]] [--platform-wide]
10625
+ odla-ai device enroll [--app <id>[,<id>...]|--all-apps] [--capability all|<c>[,<c>...]] [--platform-wide]
10599
10626
  [--name <label>] [--device-ttl <30d|6w|2y|forever>] [--email <odla-account>] [--no-open] [--wait <seconds>] [--json]
10600
10627
  odla-ai device list [--email <odla-account>] [--json]
10601
10628
  odla-ai device revoke <device-id> [--email <odla-account>] [--json]
@@ -10721,11 +10748,13 @@ Commands:
10721
10748
  enrollment in the browser; from then on EVERY worktree on this
10722
10749
  machine mints its own short-lived credentials with nobody's
10723
10750
  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.
10751
+ With no flags it covers every app you own, now and later, with
10752
+ every capability that approval can carry \u2014 so creating an app
10753
+ costs no new approval, and a capability nobody thought to name is
10754
+ not a 403 next week. "--app" or "--capability" narrow it
10755
+ deliberately, and the CLI says what that gave up.
10756
+ "--platform-wide" is the administrator's version, across all of
10757
+ odla.
10729
10758
  The expiry is a GAP, not a clock: each use rolls it forward, so
10730
10759
  only going quiet brings a human back into the loop \u2014 which is
10731
10760
  where anything that changed can be explained.
@@ -13765,10 +13794,17 @@ function advisoryCollectingFetch(inner, sink) {
13765
13794
  return response2;
13766
13795
  });
13767
13796
  }
13797
+ var superseded = /* @__PURE__ */ new Set();
13798
+ function supersedeAdvisory(code) {
13799
+ superseded.add(code);
13800
+ }
13768
13801
  function renderAdvisories(out, advisories, env = process.env) {
13802
+ const retracted = new Set(superseded);
13803
+ superseded.clear();
13769
13804
  if (env.ODLA_NO_ADVISORIES) return;
13770
13805
  const seen = /* @__PURE__ */ new Set();
13771
13806
  for (const advisory of advisories) {
13807
+ if (retracted.has(advisory.code)) continue;
13772
13808
  const key = `${advisory.code}:${advisory.message}`;
13773
13809
  if (seen.has(key)) continue;
13774
13810
  seen.add(key);
@@ -13836,11 +13872,18 @@ async function deviceCommand(parsed, deps) {
13836
13872
  async function enroll(parsed, deps, cfg, doFetch, out, json) {
13837
13873
  const name = stringOpt(parsed.options.name) ?? defaultDeviceName();
13838
13874
  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);
13875
+ const narrowed = stringOpt(parsed.options.app) !== void 0 || stringOpt(parsed.options.capability) !== void 0;
13876
+ const apps = platformWide || parsed.options["all-apps"] === true || !narrowed ? [ALL_OWNED_APPS] : (stringOpt(parsed.options.app) ?? cfg.app.id).split(",").map((id2) => id2.trim()).filter(Boolean);
13840
13877
  if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026], or --all-apps");
13841
13878
  const deviceTtlMs = parseDeviceTtl(parsed.options["device-ttl"]);
13842
13879
  const extended = platformWide || deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
13843
- const { capabilities, scopes } = requestedEnvelope(parsed, platformWide);
13880
+ const { capabilities, scopes } = requestedEnvelope(parsed, platformWide, narrowed);
13881
+ const narrowNotice = narrowEnrollmentNotice({
13882
+ appIds: apps,
13883
+ capabilities: capabilities ?? [],
13884
+ scopes: scopes ?? []
13885
+ });
13886
+ if (narrowNotice) out.error(narrowNotice);
13844
13887
  const token = await scopedToken2(
13845
13888
  parsed,
13846
13889
  deps,
@@ -13876,8 +13919,10 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
13876
13919
  }, null, 2));
13877
13920
  chmodSync3(path, 384);
13878
13921
  rememberMachineIdentity(cfg.platformUrl.replace(/\/$/, ""), stringOpt(parsed.options.email));
13922
+ supersedeAdvisory("credential.expiring");
13879
13923
  const reach = body.device.appIds.includes(ALL_OWNED_APPS) ? "every app you own, now and later" : body.device.appIds.join(", ");
13880
13924
  out.error(`device: enrolled "${name}" for ${reach}; credential written to ${path}`);
13925
+ if (narrowNotice) out.error(narrowNotice);
13881
13926
  out.error(
13882
13927
  "device: every worktree on this machine mints its own credentials from now on \u2014 no further approvals,"
13883
13928
  );
@@ -13896,9 +13941,9 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
13896
13941
  }, null, 2));
13897
13942
  }
13898
13943
  }
13899
- function requestedEnvelope(parsed, platformWide) {
13944
+ function requestedEnvelope(parsed, platformWide, narrowed) {
13900
13945
  const raw = stringOpt(parsed.options.capability);
13901
- const everything = platformWide || raw?.trim().toLowerCase() === "all";
13946
+ const everything = platformWide || !narrowed || raw?.trim().toLowerCase() === "all";
13902
13947
  if (everything) {
13903
13948
  return {
13904
13949
  capabilities: [...OPTIONAL_AGENT_PROJECT_CAPABILITIES],
@@ -15747,4 +15792,4 @@ export {
15747
15792
  isTerminalHostedSecurityStatus,
15748
15793
  runCli
15749
15794
  };
15750
- //# sourceMappingURL=chunk-PJ3RATDD.js.map
15795
+ //# sourceMappingURL=chunk-2IHE5U3M.js.map