@odla-ai/cli 0.39.0 → 0.40.1

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-XUTMQMXZ.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-WJSQEBDO.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
  {
@@ -6251,7 +6275,7 @@ async function stageWorkspacePair(baselineSource, workspaceSource, options = {})
6251
6275
  }
6252
6276
  }
6253
6277
 
6254
- // ../harness/dist/chunk-UVGZHNLW.js
6278
+ // ../harness/dist/chunk-L2T3LPEF.js
6255
6279
  var import_crypto = require("crypto");
6256
6280
  var import_promises5 = require("fs/promises");
6257
6281
  var import_path5 = require("path");
@@ -6588,7 +6612,7 @@ function validateSnapshot(snapshot, limits) {
6588
6612
  }
6589
6613
  }
6590
6614
 
6591
- // ../harness/dist/chunk-UVGZHNLW.js
6615
+ // ../harness/dist/chunk-L2T3LPEF.js
6592
6616
  var import_child_process4 = require("child_process");
6593
6617
  var import_promises6 = require("fs/promises");
6594
6618
  var import_path6 = require("path");
@@ -6890,7 +6914,7 @@ function looksLikeDestination(value2) {
6890
6914
  return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text3) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text3);
6891
6915
  }
6892
6916
 
6893
- // ../harness/dist/chunk-UVGZHNLW.js
6917
+ // ../harness/dist/chunk-L2T3LPEF.js
6894
6918
  var import_promises10 = require("fs/promises");
6895
6919
  var import_promises11 = require("fs/promises");
6896
6920
  var import_path10 = require("path");
@@ -7158,7 +7182,7 @@ async function buildCodeGraph(input) {
7158
7182
  return builder.build();
7159
7183
  }
7160
7184
 
7161
- // ../harness/dist/chunk-UVGZHNLW.js
7185
+ // ../harness/dist/chunk-L2T3LPEF.js
7162
7186
  var import_crypto4 = require("crypto");
7163
7187
  async function digestStagedWorkspace(root, limits) {
7164
7188
  const files = [];
@@ -7417,6 +7441,7 @@ function parseSnapshot(value2) {
7417
7441
  return { host, bindings, commands };
7418
7442
  }
7419
7443
  async function parseSource(value2) {
7444
+ const repositoryLimits = { maximumFiles: 1e5, maximumBytes: 80 * 1024 * 1024 };
7420
7445
  const snapshot = record5(record5(value2)?.snapshot);
7421
7446
  if (!snapshot || typeof snapshot.repository !== "string" || typeof snapshot.commitSha !== "string" || typeof snapshot.treeDigest !== "string" || !Array.isArray(snapshot.files)) throw invalid("source");
7422
7447
  const files = snapshot.files.map((value22) => {
@@ -7438,12 +7463,12 @@ async function parseSource(value2) {
7438
7463
  return { path: file.path, content: file.content };
7439
7464
  });
7440
7465
  const source2 = { repository: reference.repository, commitSha: reference.commitSha, files: referenceFiles };
7441
- const referenceDigest = await digestCodeRepositorySnapshot(source2, { maximumFiles: 1e4, maximumBytes: 16 * 1024 * 1024 });
7466
+ const referenceDigest = await digestCodeRepositorySnapshot(source2, repositoryLimits);
7442
7467
  if (referenceDigest !== reference.treeDigest) throw invalid("reference source digest");
7443
7468
  references.push({ alias: reference.alias, ...source2, treeDigest: referenceDigest });
7444
7469
  }
7445
7470
  const source = { repository: snapshot.repository, commitSha: snapshot.commitSha, files };
7446
- const digest = await digestCodeRepositorySnapshot(source, { maximumFiles: 1e4, maximumBytes: 16 * 1024 * 1024 });
7471
+ const digest = await digestCodeRepositorySnapshot(source, repositoryLimits);
7447
7472
  if (digest !== snapshot.treeDigest) throw invalid("source digest");
7448
7473
  return { ...source, treeDigest: digest, ...references.length ? { references } : {} };
7449
7474
  }
@@ -8095,8 +8120,11 @@ async function prepareRuntimeLocalSource(input) {
8095
8120
  }
8096
8121
  var RESERVED2 = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_modules", "dist", "coverage"]);
8097
8122
  var SECRET2 = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
8123
+ var SOURCE_MAX_FILES = 1e5;
8124
+ var SOURCE_MAX_BYTES = 80 * 1024 * 1024;
8125
+ var SOURCE_SET_MAX_BYTES = 480 * 1024 * 1024;
8098
8126
  async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_os3.tmpdir)()) {
8099
- if (!snapshot.files.length || snapshot.files.length > 1e4) throw new TypeError("Code source file count is invalid");
8127
+ if (!snapshot.files.length || snapshot.files.length > SOURCE_MAX_FILES) throw new TypeError("Code source file count is invalid");
8100
8128
  const root = await (0, import_promises8.mkdtemp)((0, import_path8.join)(tempRoot, "odla-code-source-"));
8101
8129
  const sourceDir = (0, import_path8.join)(root, "source");
8102
8130
  await (0, import_promises8.mkdir)(sourceDir);
@@ -8108,7 +8136,7 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_os3.
8108
8136
  if (seen.has(file.path)) throw new TypeError("Code source repeats a path");
8109
8137
  seen.add(file.path);
8110
8138
  bytes += Buffer.byteLength(file.path) + Buffer.byteLength(file.content);
8111
- if (bytes > 16 * 1024 * 1024) throw new TypeError("Code source exceeds its byte bound");
8139
+ if (bytes > SOURCE_MAX_BYTES) throw new TypeError("Code source exceeds its byte bound");
8112
8140
  const target = (0, import_path8.resolve)(sourceDir, file.path);
8113
8141
  if (!target.startsWith(`${(0, import_path8.resolve)(sourceDir)}${import_path8.sep}`)) throw new TypeError("Code source path escapes its root");
8114
8142
  await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
@@ -8116,14 +8144,14 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_os3.
8116
8144
  }
8117
8145
  for (const reference of snapshot.references ?? []) {
8118
8146
  validateAlias(reference.alias);
8119
- if (!reference.files.length || reference.files.length > 1e4) throw new TypeError("Code reference file count is invalid");
8147
+ if (!reference.files.length || reference.files.length > SOURCE_MAX_FILES) throw new TypeError("Code reference file count is invalid");
8120
8148
  for (const file of reference.files) {
8121
8149
  validatePath(file.path);
8122
8150
  const path = `.odla-references/${reference.alias}/${file.path}`;
8123
8151
  if (seen.has(path)) throw new TypeError("Code reference repeats a path");
8124
8152
  seen.add(path);
8125
8153
  bytes += Buffer.byteLength(path) + Buffer.byteLength(file.content);
8126
- if (bytes > 80 * 1024 * 1024) throw new TypeError("Code source set exceeds its byte bound");
8154
+ if (bytes > SOURCE_SET_MAX_BYTES) throw new TypeError("Code source set exceeds its byte bound");
8127
8155
  const target = (0, import_path8.resolve)(sourceDir, path);
8128
8156
  if (!target.startsWith(`${(0, import_path8.resolve)(sourceDir)}${import_path8.sep}`)) throw new TypeError("Code reference path escapes its root");
8129
8157
  await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
@@ -8149,7 +8177,7 @@ async function attachCodeRuntimeReferences(workspace, references) {
8149
8177
  validatePath(file.path);
8150
8178
  const path = `.odla-references/${reference.alias}/${file.path}`;
8151
8179
  bytes += Buffer.byteLength(path) + Buffer.byteLength(file.content);
8152
- if (bytes > 64 * 1024 * 1024) throw new TypeError("Code reference set exceeds its byte bound");
8180
+ if (bytes > SOURCE_SET_MAX_BYTES - SOURCE_MAX_BYTES) throw new TypeError("Code reference set exceeds its byte bound");
8153
8181
  for (const root of [workspace.baselineDir, workspace.workspaceDir]) {
8154
8182
  const target = (0, import_path8.resolve)(root, path);
8155
8183
  if (!target.startsWith(`${(0, import_path8.resolve)(root)}${import_path8.sep}`)) throw new TypeError("Code reference path escapes its root");
@@ -8199,7 +8227,10 @@ async function materializeCommandWorkspace(input) {
8199
8227
  trustedBaseDir: materialized.sourceDir,
8200
8228
  trustedBaseCommitSha: source.commitSha,
8201
8229
  checkpoint: codeCheckpointPayload(command.payload)
8202
- })).workspace : await stageWorkspace(materialized.sourceDir);
8230
+ })).workspace : await stageWorkspace(materialized.sourceDir, {
8231
+ maxFiles: SOURCE_MAX_FILES,
8232
+ maxBytes: SOURCE_SET_MAX_BYTES
8233
+ });
8203
8234
  return { workspace, sourceDigest: source.treeDigest, requestedLocal: null };
8204
8235
  } finally {
8205
8236
  await materialized.cleanup();
@@ -10573,13 +10604,16 @@ async function credentialCommand(parsed, deps = {}) {
10573
10604
  // src/help-usage.ts
10574
10605
  var AUTH_SECTION = `
10575
10606
  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.
10607
+ npx odla-ai device enroll --no-open --wait 600
10608
+ One browser approval, and no flags to remember: this covers every app you
10609
+ own \u2014 including apps you create later \u2014 with every capability that
10610
+ approval can carry. Afterwards every worktree on this machine mints its
10611
+ own short-lived credentials with nobody's attention. The window rolls
10612
+ forward each time you use it, so continuous work never interrupts anyone;
10613
+ only a real gap does. Give the human the printed /studio?code= URL, keep
10614
+ the process alive, and wait on it.
10615
+ Narrow it deliberately with --app <id> or --capability <c>; the CLI then
10616
+ says what that gave up.
10583
10617
 
10584
10618
  npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600
10585
10619
  The same thing across all of odla, for weeks. Needs a platform
@@ -10722,7 +10756,7 @@ Usage:
10722
10756
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
10723
10757
  odla-ai security run [target] --self --ack-redacted-source
10724
10758
  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]
10759
+ odla-ai device enroll [--app <id>[,<id>...]|--all-apps] [--capability all|<c>[,<c>...]] [--platform-wide]
10726
10760
  [--name <label>] [--device-ttl <30d|6w|2y|forever>] [--email <odla-account>] [--no-open] [--wait <seconds>] [--json]
10727
10761
  odla-ai device list [--email <odla-account>] [--json]
10728
10762
  odla-ai device revoke <device-id> [--email <odla-account>] [--json]
@@ -10848,11 +10882,13 @@ Commands:
10848
10882
  enrollment in the browser; from then on EVERY worktree on this
10849
10883
  machine mints its own short-lived credentials with nobody's
10850
10884
  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.
10885
+ With no flags it covers every app you own, now and later, with
10886
+ every capability that approval can carry \u2014 so creating an app
10887
+ costs no new approval, and a capability nobody thought to name is
10888
+ not a 403 next week. "--app" or "--capability" narrow it
10889
+ deliberately, and the CLI says what that gave up.
10890
+ "--platform-wide" is the administrator's version, across all of
10891
+ odla.
10856
10892
  The expiry is a GAP, not a clock: each use rolls it forward, so
10857
10893
  only going quiet brings a human back into the loop \u2014 which is
10858
10894
  where anything that changed can be explained.
@@ -13892,10 +13928,17 @@ function advisoryCollectingFetch(inner, sink) {
13892
13928
  return response2;
13893
13929
  });
13894
13930
  }
13931
+ var superseded = /* @__PURE__ */ new Set();
13932
+ function supersedeAdvisory(code) {
13933
+ superseded.add(code);
13934
+ }
13895
13935
  function renderAdvisories(out, advisories, env = process.env) {
13936
+ const retracted = new Set(superseded);
13937
+ superseded.clear();
13896
13938
  if (env.ODLA_NO_ADVISORIES) return;
13897
13939
  const seen = /* @__PURE__ */ new Set();
13898
13940
  for (const advisory of advisories) {
13941
+ if (retracted.has(advisory.code)) continue;
13899
13942
  const key = `${advisory.code}:${advisory.message}`;
13900
13943
  if (seen.has(key)) continue;
13901
13944
  seen.add(key);
@@ -13958,11 +14001,18 @@ async function deviceCommand(parsed, deps) {
13958
14001
  async function enroll(parsed, deps, cfg, doFetch, out, json) {
13959
14002
  const name = stringOpt(parsed.options.name) ?? defaultDeviceName();
13960
14003
  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);
14004
+ const narrowed = stringOpt(parsed.options.app) !== void 0 || stringOpt(parsed.options.capability) !== void 0;
14005
+ 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
14006
  if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026], or --all-apps");
13963
14007
  const deviceTtlMs = parseDeviceTtl(parsed.options["device-ttl"]);
13964
14008
  const extended = platformWide || deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
13965
- const { capabilities, scopes } = requestedEnvelope(parsed, platformWide);
14009
+ const { capabilities, scopes } = requestedEnvelope(parsed, platformWide, narrowed);
14010
+ const narrowNotice = narrowEnrollmentNotice({
14011
+ appIds: apps,
14012
+ capabilities: capabilities ?? [],
14013
+ scopes: scopes ?? []
14014
+ });
14015
+ if (narrowNotice) out.error(narrowNotice);
13966
14016
  const token = await scopedToken2(
13967
14017
  parsed,
13968
14018
  deps,
@@ -13998,8 +14048,10 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
13998
14048
  }, null, 2));
13999
14049
  (0, import_node_fs19.chmodSync)(path, 384);
14000
14050
  rememberMachineIdentity(cfg.platformUrl.replace(/\/$/, ""), stringOpt(parsed.options.email));
14051
+ supersedeAdvisory("credential.expiring");
14001
14052
  const reach = body.device.appIds.includes(import_db4.ALL_OWNED_APPS) ? "every app you own, now and later" : body.device.appIds.join(", ");
14002
14053
  out.error(`device: enrolled "${name}" for ${reach}; credential written to ${path}`);
14054
+ if (narrowNotice) out.error(narrowNotice);
14003
14055
  out.error(
14004
14056
  "device: every worktree on this machine mints its own credentials from now on \u2014 no further approvals,"
14005
14057
  );
@@ -14018,9 +14070,9 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
14018
14070
  }, null, 2));
14019
14071
  }
14020
14072
  }
14021
- function requestedEnvelope(parsed, platformWide) {
14073
+ function requestedEnvelope(parsed, platformWide, narrowed) {
14022
14074
  const raw = stringOpt(parsed.options.capability);
14023
- const everything = platformWide || raw?.trim().toLowerCase() === "all";
14075
+ const everything = platformWide || !narrowed || raw?.trim().toLowerCase() === "all";
14024
14076
  if (everything) {
14025
14077
  return {
14026
14078
  capabilities: [...import_db4.OPTIONAL_AGENT_PROJECT_CAPABILITIES],