@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.cjs CHANGED
@@ -403,6 +403,11 @@ var init_device_session = __esm({
403
403
 
404
404
  // src/odla-home.ts
405
405
  function odlaHome(env = import_node_process5.default.env) {
406
+ if (env.VITEST && !env.ODLA_HOME) {
407
+ throw new Error(
408
+ "ODLA_HOME must be set under test \u2014 resolving the real ~/.odla would write to the developer's machine"
409
+ );
410
+ }
406
411
  return env.ODLA_HOME ?? (0, import_node_path4.join)(env.HOME ?? (0, import_node_os2.homedir)(), ".odla");
407
412
  }
408
413
  function odlaHomePath(segments, env = import_node_process5.default.env) {
@@ -428,15 +433,15 @@ function pmContextFile(env) {
428
433
  }
429
434
  function adoptRepoLocalCache(legacyPath, machinePath, out) {
430
435
  if (!(0, import_node_fs6.existsSync)(legacyPath) || legacyPath === machinePath) return false;
431
- const superseded = (0, import_node_fs6.existsSync)(machinePath);
432
- if (!superseded) {
436
+ const superseded2 = (0, import_node_fs6.existsSync)(machinePath);
437
+ if (!superseded2) {
433
438
  (0, import_node_fs6.mkdirSync)((0, import_node_path4.dirname)(machinePath), { recursive: true });
434
439
  (0, import_node_fs6.copyFileSync)(legacyPath, machinePath);
435
440
  (0, import_node_fs6.chmodSync)(machinePath, 384);
436
441
  }
437
442
  (0, import_node_fs6.rmSync)(legacyPath, { force: true });
438
443
  out?.error(
439
- 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`
444
+ 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`
440
445
  );
441
446
  return true;
442
447
  }
@@ -615,6 +620,20 @@ function scopeInterruptionNotice(scope, state2) {
615
620
  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."
616
621
  ].join("\n");
617
622
  }
623
+ function narrowEnrollmentNotice(envelope) {
624
+ const everyApp = envelope.appIds.includes("*");
625
+ if (everyApp && envelope.capabilities.length > 0 && envelope.scopes.length > 0) return null;
626
+ const missing = [
627
+ everyApp ? null : `only ${envelope.appIds.length} app${envelope.appIds.length === 1 ? "" : "s"} \u2014 a new one will need a new approval`,
628
+ envelope.capabilities.length ? null : "no optional capabilities \u2014 app.manage, crm.read and code.session are not included",
629
+ envelope.scopes.length ? null : "no platform scopes \u2014 runbook edits, config plans and host connect will each ask again"
630
+ ].filter(Boolean);
631
+ return [
632
+ `odla: this is a NARROW enrollment: ${missing.join("; ")}.`,
633
+ " That is a fine choice if you meant it. If you did not, drop the flags:",
634
+ ` ${ENROL_EVERYTHING}`
635
+ ].join("\n");
636
+ }
618
637
  function lapseNotice(state2, now = Date.now()) {
619
638
  if (!state2.enrolled || !state2.lapsesAt) return null;
620
639
  const days = Math.floor((state2.lapsesAt - now) / (24 * 60 * 60 * 1e3));
@@ -630,7 +649,7 @@ var init_auth_guidance = __esm({
630
649
  init_device_session();
631
650
  init_odla_home();
632
651
  init_local();
633
- ENROL_EVERYTHING = "npx odla-ai device enroll --all-apps --capability all --no-open --wait 600";
652
+ ENROL_EVERYTHING = "npx odla-ai device enroll --no-open --wait 600";
634
653
  ENROL_PLATFORM_WIDE = "npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600";
635
654
  }
636
655
  });
@@ -2705,6 +2724,11 @@ async function authCommand(parsed, deps = {}) {
2705
2724
  "auth login requires --email <odla-account> or ODLA_USER_EMAIL; confirm the signed-in odla email instead of using git or GitHub identity"
2706
2725
  );
2707
2726
  }
2727
+ if (!machineAuthState(platformAudience(cfg.platformUrl)).enrolled) {
2728
+ out.error("odla: this machine is not enrolled, so this approval buys one project until it lapses.");
2729
+ out.error(` For one approval that covers every app you own, with no repeats:
2730
+ ${ENROL_EVERYTHING}`);
2731
+ }
2708
2732
  const token = await getDeveloperToken(
2709
2733
  cfg,
2710
2734
  {
@@ -2742,6 +2766,7 @@ var init_auth_command = __esm({
2742
2766
  init_argv();
2743
2767
  init_operator_context();
2744
2768
  init_token();
2769
+ init_auth_guidance();
2745
2770
  init_whoami_command();
2746
2771
  }
2747
2772
  });
@@ -11397,13 +11422,16 @@ var init_help_usage = __esm({
11397
11422
  init_cjs_shims();
11398
11423
  AUTH_SECTION = `
11399
11424
  Enrol this machine once, then stop asking:
11400
- npx odla-ai device enroll --all-apps --capability all --no-open --wait 600
11401
- One browser approval. Afterwards every worktree on this machine mints its
11402
- own short-lived credentials with nobody's attention, for every app you
11403
- own \u2014 including apps you create later. The window rolls forward each time
11404
- you use it, so continuous work never interrupts anyone; only a real gap
11405
- does. Give the human the printed /studio?code= URL, keep the process
11406
- alive, and wait on it.
11425
+ npx odla-ai device enroll --no-open --wait 600
11426
+ One browser approval, and no flags to remember: this covers every app you
11427
+ own \u2014 including apps you create later \u2014 with every capability that
11428
+ approval can carry. Afterwards every worktree on this machine mints its
11429
+ own short-lived credentials with nobody's attention. The window rolls
11430
+ forward each time you use it, so continuous work never interrupts anyone;
11431
+ only a real gap does. Give the human the printed /studio?code= URL, keep
11432
+ the process alive, and wait on it.
11433
+ Narrow it deliberately with --app <id> or --capability <c>; the CLI then
11434
+ says what that gave up.
11407
11435
 
11408
11436
  npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600
11409
11437
  The same thing across all of odla, for weeks. Needs a platform
@@ -11546,7 +11574,7 @@ Usage:
11546
11574
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
11547
11575
  odla-ai security run [target] --self --ack-redacted-source
11548
11576
  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]
11549
- odla-ai device enroll [--all-apps|--app <id>[,<id>...]] [--capability all|<c>[,<c>...]] [--platform-wide]
11577
+ odla-ai device enroll [--app <id>[,<id>...]|--all-apps] [--capability all|<c>[,<c>...]] [--platform-wide]
11550
11578
  [--name <label>] [--device-ttl <30d|6w|2y|forever>] [--email <odla-account>] [--no-open] [--wait <seconds>] [--json]
11551
11579
  odla-ai device list [--email <odla-account>] [--json]
11552
11580
  odla-ai device revoke <device-id> [--email <odla-account>] [--json]
@@ -11674,11 +11702,13 @@ Commands:
11674
11702
  enrollment in the browser; from then on EVERY worktree on this
11675
11703
  machine mints its own short-lived credentials with nobody's
11676
11704
  attention, until the device is revoked or goes unused.
11677
- "--all-apps" covers every app you own, now and later, so creating
11678
- an app costs no new approval. "--capability all" takes everything
11679
- that approval is allowed to carry, so a capability you did not
11680
- think to name is not a 403 next week. "--platform-wide" is the
11681
- administrator's version, across all of odla.
11705
+ With no flags it covers every app you own, now and later, with
11706
+ every capability that approval can carry \u2014 so creating an app
11707
+ costs no new approval, and a capability nobody thought to name is
11708
+ not a 403 next week. "--app" or "--capability" narrow it
11709
+ deliberately, and the CLI says what that gave up.
11710
+ "--platform-wide" is the administrator's version, across all of
11711
+ odla.
11682
11712
  The expiry is a GAP, not a clock: each use rolls it forward, so
11683
11713
  only going quiet brings a human back into the loop \u2014 which is
11684
11714
  where anything that changed can be explained.
@@ -14988,22 +15018,29 @@ function advisoryCollectingFetch(inner, sink) {
14988
15018
  return response2;
14989
15019
  });
14990
15020
  }
15021
+ function supersedeAdvisory(code) {
15022
+ superseded.add(code);
15023
+ }
14991
15024
  function renderAdvisories(out, advisories, env = process.env) {
15025
+ const retracted = new Set(superseded);
15026
+ superseded.clear();
14992
15027
  if (env.ODLA_NO_ADVISORIES) return;
14993
15028
  const seen = /* @__PURE__ */ new Set();
14994
15029
  for (const advisory of advisories) {
15030
+ if (retracted.has(advisory.code)) continue;
14995
15031
  const key = `${advisory.code}:${advisory.message}`;
14996
15032
  if (seen.has(key)) continue;
14997
15033
  seen.add(key);
14998
15034
  out.error((0, import_apps14.formatAdvisory)(advisory));
14999
15035
  }
15000
15036
  }
15001
- var import_apps14;
15037
+ var import_apps14, superseded;
15002
15038
  var init_advisory_output = __esm({
15003
15039
  "src/advisory-output.ts"() {
15004
15040
  "use strict";
15005
15041
  init_cjs_shims();
15006
15042
  import_apps14 = require("@odla-ai/apps");
15043
+ superseded = /* @__PURE__ */ new Set();
15007
15044
  }
15008
15045
  });
15009
15046
 
@@ -15063,11 +15100,18 @@ async function deviceCommand(parsed, deps) {
15063
15100
  async function enroll(parsed, deps, cfg, doFetch, out, json) {
15064
15101
  const name = stringOpt(parsed.options.name) ?? defaultDeviceName();
15065
15102
  const platformWide = parsed.options["platform-wide"] === true;
15066
- 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);
15103
+ const narrowed = stringOpt(parsed.options.app) !== void 0 || stringOpt(parsed.options.capability) !== void 0;
15104
+ 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);
15067
15105
  if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026], or --all-apps");
15068
15106
  const deviceTtlMs = parseDeviceTtl(parsed.options["device-ttl"]);
15069
15107
  const extended = platformWide || deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
15070
- const { capabilities, scopes } = requestedEnvelope(parsed, platformWide);
15108
+ const { capabilities, scopes } = requestedEnvelope(parsed, platformWide, narrowed);
15109
+ const narrowNotice = narrowEnrollmentNotice({
15110
+ appIds: apps,
15111
+ capabilities: capabilities ?? [],
15112
+ scopes: scopes ?? []
15113
+ });
15114
+ if (narrowNotice) out.error(narrowNotice);
15071
15115
  const token = await scopedToken2(
15072
15116
  parsed,
15073
15117
  deps,
@@ -15103,8 +15147,10 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
15103
15147
  }, null, 2));
15104
15148
  (0, import_node_fs21.chmodSync)(path, 384);
15105
15149
  rememberMachineIdentity(cfg.platformUrl.replace(/\/$/, ""), stringOpt(parsed.options.email));
15150
+ supersedeAdvisory("credential.expiring");
15106
15151
  const reach = body.device.appIds.includes(import_db4.ALL_OWNED_APPS) ? "every app you own, now and later" : body.device.appIds.join(", ");
15107
15152
  out.error(`device: enrolled "${name}" for ${reach}; credential written to ${path}`);
15153
+ if (narrowNotice) out.error(narrowNotice);
15108
15154
  out.error(
15109
15155
  "device: every worktree on this machine mints its own credentials from now on \u2014 no further approvals,"
15110
15156
  );
@@ -15123,9 +15169,9 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
15123
15169
  }, null, 2));
15124
15170
  }
15125
15171
  }
15126
- function requestedEnvelope(parsed, platformWide) {
15172
+ function requestedEnvelope(parsed, platformWide, narrowed) {
15127
15173
  const raw = stringOpt(parsed.options.capability);
15128
- const everything = platformWide || raw?.trim().toLowerCase() === "all";
15174
+ const everything = platformWide || !narrowed || raw?.trim().toLowerCase() === "all";
15129
15175
  if (everything) {
15130
15176
  return {
15131
15177
  capabilities: [...import_db4.OPTIONAL_AGENT_PROJECT_CAPABILITIES],
@@ -15209,6 +15255,8 @@ var init_device_command = __esm({
15209
15255
  import_db4 = require("@odla-ai/db");
15210
15256
  init_device_ttl();
15211
15257
  init_machine_identity();
15258
+ init_auth_guidance();
15259
+ init_advisory_output();
15212
15260
  import_node_fs21 = require("fs");
15213
15261
  import_node_path19 = require("path");
15214
15262
  import_node_process19 = __toESM(require("process"), 1);