@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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runCli
4
- } from "./chunk-PJ3RATDD.js";
4
+ } from "./chunk-2IHE5U3M.js";
5
5
  import {
6
6
  exitCodeFor
7
7
  } from "./chunk-UKLSRQ5J.js";
@@ -9,4 +9,4 @@ export {
9
9
  exitCodeFor,
10
10
  runCli
11
11
  };
12
- //# sourceMappingURL=cli-ALS5OZPB.js.map
12
+ //# sourceMappingURL=cli-LRJBESE6.js.map
package/dist/index.cjs CHANGED
@@ -295,6 +295,11 @@ var import_node_os2 = require("os");
295
295
  var import_node_path3 = require("path");
296
296
  var import_node_process5 = __toESM(require("process"), 1);
297
297
  function odlaHome(env = import_node_process5.default.env) {
298
+ if (env.VITEST && !env.ODLA_HOME) {
299
+ throw new Error(
300
+ "ODLA_HOME must be set under test \u2014 resolving the real ~/.odla would write to the developer's machine"
301
+ );
302
+ }
298
303
  return env.ODLA_HOME ?? (0, import_node_path3.join)(env.HOME ?? (0, import_node_os2.homedir)(), ".odla");
299
304
  }
300
305
  function odlaHomePath(segments, env = import_node_process5.default.env) {
@@ -320,15 +325,15 @@ function pmContextFile(env) {
320
325
  }
321
326
  function adoptRepoLocalCache(legacyPath, machinePath, out) {
322
327
  if (!(0, import_node_fs3.existsSync)(legacyPath) || legacyPath === machinePath) return false;
323
- const superseded = (0, import_node_fs3.existsSync)(machinePath);
324
- if (!superseded) {
328
+ const superseded2 = (0, import_node_fs3.existsSync)(machinePath);
329
+ if (!superseded2) {
325
330
  (0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(machinePath), { recursive: true });
326
331
  (0, import_node_fs3.copyFileSync)(legacyPath, machinePath);
327
332
  (0, import_node_fs3.chmodSync)(machinePath, 384);
328
333
  }
329
334
  (0, import_node_fs3.rmSync)(legacyPath, { force: true });
330
335
  out?.error(
331
- 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`
336
+ superseded2 ? `auth: removed superseded ${legacyPath}; this machine's credentials live in ${odlaHome()}` : `auth: moved ${legacyPath} into ${machinePath}; credentials are per machine now, not per worktree`
332
337
  );
333
338
  return true;
334
339
  }
@@ -465,7 +470,7 @@ function displayPath(path, rootDir = process.cwd()) {
465
470
  }
466
471
 
467
472
  // src/auth-guidance.ts
468
- var ENROL_EVERYTHING = "npx odla-ai device enroll --all-apps --capability all --no-open --wait 600";
473
+ var ENROL_EVERYTHING = "npx odla-ai device enroll --no-open --wait 600";
469
474
  var ENROL_PLATFORM_WIDE = "npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600";
470
475
  function machineAuthState(audience, env = import_node_process6.default.env) {
471
476
  const device = readDeviceCredential(audience, env);
@@ -491,6 +496,20 @@ function scopeInterruptionNotice(scope, state2) {
491
496
  platformScope ? " A platform scope needs an administrator's approval; an app owner's cannot carry it." : " One approval, every app you own, and it rolls forward while you keep working."
492
497
  ].join("\n");
493
498
  }
499
+ function narrowEnrollmentNotice(envelope) {
500
+ const everyApp = envelope.appIds.includes("*");
501
+ if (everyApp && envelope.capabilities.length > 0 && envelope.scopes.length > 0) return null;
502
+ const missing = [
503
+ everyApp ? null : `only ${envelope.appIds.length} app${envelope.appIds.length === 1 ? "" : "s"} \u2014 a new one will need a new approval`,
504
+ envelope.capabilities.length ? null : "no optional capabilities \u2014 app.manage, crm.read and code.session are not included",
505
+ envelope.scopes.length ? null : "no platform scopes \u2014 runbook edits, config plans and host connect will each ask again"
506
+ ].filter(Boolean);
507
+ return [
508
+ `odla: this is a NARROW enrollment: ${missing.join("; ")}.`,
509
+ " That is a fine choice if you meant it. If you did not, drop the flags:",
510
+ ` ${ENROL_EVERYTHING}`
511
+ ].join("\n");
512
+ }
494
513
  function lapseNotice(state2, now = Date.now()) {
495
514
  if (!state2.enrolled || !state2.lapsesAt) return null;
496
515
  const days = Math.floor((state2.lapsesAt - now) / (24 * 60 * 60 * 1e3));
@@ -2368,6 +2387,11 @@ async function authCommand(parsed, deps = {}) {
2368
2387
  "auth login requires --email <odla-account> or ODLA_USER_EMAIL; confirm the signed-in odla email instead of using git or GitHub identity"
2369
2388
  );
2370
2389
  }
2390
+ if (!machineAuthState(platformAudience(cfg.platformUrl)).enrolled) {
2391
+ out.error("odla: this machine is not enrolled, so this approval buys one project until it lapses.");
2392
+ out.error(` For one approval that covers every app you own, with no repeats:
2393
+ ${ENROL_EVERYTHING}`);
2394
+ }
2371
2395
  const token = await getDeveloperToken(
2372
2396
  cfg,
2373
2397
  {
@@ -10573,13 +10597,16 @@ async function credentialCommand(parsed, deps = {}) {
10573
10597
  // src/help-usage.ts
10574
10598
  var AUTH_SECTION = `
10575
10599
  Enrol this machine once, then stop asking:
10576
- npx odla-ai device enroll --all-apps --capability all --no-open --wait 600
10577
- One browser approval. Afterwards every worktree on this machine mints its
10578
- own short-lived credentials with nobody's attention, for every app you
10579
- own \u2014 including apps you create later. The window rolls forward each time
10580
- you use it, so continuous work never interrupts anyone; only a real gap
10581
- does. Give the human the printed /studio?code= URL, keep the process
10582
- alive, and wait on it.
10600
+ npx odla-ai device enroll --no-open --wait 600
10601
+ One browser approval, and no flags to remember: this covers every app you
10602
+ own \u2014 including apps you create later \u2014 with every capability that
10603
+ approval can carry. Afterwards every worktree on this machine mints its
10604
+ own short-lived credentials with nobody's attention. The window rolls
10605
+ forward each time you use it, so continuous work never interrupts anyone;
10606
+ only a real gap does. Give the human the printed /studio?code= URL, keep
10607
+ the process alive, and wait on it.
10608
+ Narrow it deliberately with --app <id> or --capability <c>; the CLI then
10609
+ says what that gave up.
10583
10610
 
10584
10611
  npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600
10585
10612
  The same thing across all of odla, for weeks. Needs a platform
@@ -10722,7 +10749,7 @@ Usage:
10722
10749
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
10723
10750
  odla-ai security run [target] --self --ack-redacted-source
10724
10751
  odla-ai provision [--live] [--config odla.config.mjs] [--email <odla-account>] [--request-grant] [--no-open] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
10725
- odla-ai device enroll [--all-apps|--app <id>[,<id>...]] [--capability all|<c>[,<c>...]] [--platform-wide]
10752
+ odla-ai device enroll [--app <id>[,<id>...]|--all-apps] [--capability all|<c>[,<c>...]] [--platform-wide]
10726
10753
  [--name <label>] [--device-ttl <30d|6w|2y|forever>] [--email <odla-account>] [--no-open] [--wait <seconds>] [--json]
10727
10754
  odla-ai device list [--email <odla-account>] [--json]
10728
10755
  odla-ai device revoke <device-id> [--email <odla-account>] [--json]
@@ -10848,11 +10875,13 @@ Commands:
10848
10875
  enrollment in the browser; from then on EVERY worktree on this
10849
10876
  machine mints its own short-lived credentials with nobody's
10850
10877
  attention, until the device is revoked or goes unused.
10851
- "--all-apps" covers every app you own, now and later, so creating
10852
- an app costs no new approval. "--capability all" takes everything
10853
- that approval is allowed to carry, so a capability you did not
10854
- think to name is not a 403 next week. "--platform-wide" is the
10855
- administrator's version, across all of odla.
10878
+ With no flags it covers every app you own, now and later, with
10879
+ every capability that approval can carry \u2014 so creating an app
10880
+ costs no new approval, and a capability nobody thought to name is
10881
+ not a 403 next week. "--app" or "--capability" narrow it
10882
+ deliberately, and the CLI says what that gave up.
10883
+ "--platform-wide" is the administrator's version, across all of
10884
+ odla.
10856
10885
  The expiry is a GAP, not a clock: each use rolls it forward, so
10857
10886
  only going quiet brings a human back into the loop \u2014 which is
10858
10887
  where anything that changed can be explained.
@@ -13892,10 +13921,17 @@ function advisoryCollectingFetch(inner, sink) {
13892
13921
  return response2;
13893
13922
  });
13894
13923
  }
13924
+ var superseded = /* @__PURE__ */ new Set();
13925
+ function supersedeAdvisory(code) {
13926
+ superseded.add(code);
13927
+ }
13895
13928
  function renderAdvisories(out, advisories, env = process.env) {
13929
+ const retracted = new Set(superseded);
13930
+ superseded.clear();
13896
13931
  if (env.ODLA_NO_ADVISORIES) return;
13897
13932
  const seen = /* @__PURE__ */ new Set();
13898
13933
  for (const advisory of advisories) {
13934
+ if (retracted.has(advisory.code)) continue;
13899
13935
  const key = `${advisory.code}:${advisory.message}`;
13900
13936
  if (seen.has(key)) continue;
13901
13937
  seen.add(key);
@@ -13958,11 +13994,18 @@ async function deviceCommand(parsed, deps) {
13958
13994
  async function enroll(parsed, deps, cfg, doFetch, out, json) {
13959
13995
  const name = stringOpt(parsed.options.name) ?? defaultDeviceName();
13960
13996
  const platformWide = parsed.options["platform-wide"] === true;
13961
- const apps = platformWide || parsed.options["all-apps"] === true ? [import_db4.ALL_OWNED_APPS] : (stringOpt(parsed.options.app) ?? cfg.app.id).split(",").map((id2) => id2.trim()).filter(Boolean);
13997
+ const narrowed = stringOpt(parsed.options.app) !== void 0 || stringOpt(parsed.options.capability) !== void 0;
13998
+ const apps = platformWide || parsed.options["all-apps"] === true || !narrowed ? [import_db4.ALL_OWNED_APPS] : (stringOpt(parsed.options.app) ?? cfg.app.id).split(",").map((id2) => id2.trim()).filter(Boolean);
13962
13999
  if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026], or --all-apps");
13963
14000
  const deviceTtlMs = parseDeviceTtl(parsed.options["device-ttl"]);
13964
14001
  const extended = platformWide || deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
13965
- const { capabilities, scopes } = requestedEnvelope(parsed, platformWide);
14002
+ const { capabilities, scopes } = requestedEnvelope(parsed, platformWide, narrowed);
14003
+ const narrowNotice = narrowEnrollmentNotice({
14004
+ appIds: apps,
14005
+ capabilities: capabilities ?? [],
14006
+ scopes: scopes ?? []
14007
+ });
14008
+ if (narrowNotice) out.error(narrowNotice);
13966
14009
  const token = await scopedToken2(
13967
14010
  parsed,
13968
14011
  deps,
@@ -13998,8 +14041,10 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
13998
14041
  }, null, 2));
13999
14042
  (0, import_node_fs19.chmodSync)(path, 384);
14000
14043
  rememberMachineIdentity(cfg.platformUrl.replace(/\/$/, ""), stringOpt(parsed.options.email));
14044
+ supersedeAdvisory("credential.expiring");
14001
14045
  const reach = body.device.appIds.includes(import_db4.ALL_OWNED_APPS) ? "every app you own, now and later" : body.device.appIds.join(", ");
14002
14046
  out.error(`device: enrolled "${name}" for ${reach}; credential written to ${path}`);
14047
+ if (narrowNotice) out.error(narrowNotice);
14003
14048
  out.error(
14004
14049
  "device: every worktree on this machine mints its own credentials from now on \u2014 no further approvals,"
14005
14050
  );
@@ -14018,9 +14063,9 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
14018
14063
  }, null, 2));
14019
14064
  }
14020
14065
  }
14021
- function requestedEnvelope(parsed, platformWide) {
14066
+ function requestedEnvelope(parsed, platformWide, narrowed) {
14022
14067
  const raw = stringOpt(parsed.options.capability);
14023
- const everything = platformWide || raw?.trim().toLowerCase() === "all";
14068
+ const everything = platformWide || !narrowed || raw?.trim().toLowerCase() === "all";
14024
14069
  if (everything) {
14025
14070
  return {
14026
14071
  capabilities: [...import_db4.OPTIONAL_AGENT_PROJECT_CAPABILITIES],