@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.
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-WJSQEBDO.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
  {
@@ -6121,7 +6145,7 @@ async function stageWorkspacePair(baselineSource, workspaceSource, options = {})
6121
6145
  }
6122
6146
  }
6123
6147
 
6124
- // ../harness/dist/chunk-UVGZHNLW.js
6148
+ // ../harness/dist/chunk-L2T3LPEF.js
6125
6149
  import { createHash as createHash3 } from "crypto";
6126
6150
  import { readFile as readFile2, readdir as readdir2 } from "fs/promises";
6127
6151
  import { relative as relative4, resolve as resolve10 } from "path";
@@ -6458,7 +6482,7 @@ function validateSnapshot(snapshot, limits) {
6458
6482
  }
6459
6483
  }
6460
6484
 
6461
- // ../harness/dist/chunk-UVGZHNLW.js
6485
+ // ../harness/dist/chunk-L2T3LPEF.js
6462
6486
  import { spawn as spawn4 } from "child_process";
6463
6487
  import { lstat as lstat2 } from "fs/promises";
6464
6488
  import { resolve as resolve23, sep as sep3 } from "path";
@@ -6763,7 +6787,7 @@ function looksLikeDestination(value2) {
6763
6787
  return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text3) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text3);
6764
6788
  }
6765
6789
 
6766
- // ../harness/dist/chunk-UVGZHNLW.js
6790
+ // ../harness/dist/chunk-L2T3LPEF.js
6767
6791
  import { readFile as readFile4, stat as stat2 } from "fs/promises";
6768
6792
  import { readFile as readFile3 } from "fs/promises";
6769
6793
  import { join as join33 } from "path";
@@ -7031,7 +7055,7 @@ async function buildCodeGraph(input) {
7031
7055
  return builder.build();
7032
7056
  }
7033
7057
 
7034
- // ../harness/dist/chunk-UVGZHNLW.js
7058
+ // ../harness/dist/chunk-L2T3LPEF.js
7035
7059
  import { createHash as createHash32 } from "crypto";
7036
7060
  async function digestStagedWorkspace(root, limits) {
7037
7061
  const files = [];
@@ -7290,6 +7314,7 @@ function parseSnapshot(value2) {
7290
7314
  return { host, bindings, commands };
7291
7315
  }
7292
7316
  async function parseSource(value2) {
7317
+ const repositoryLimits = { maximumFiles: 1e5, maximumBytes: 80 * 1024 * 1024 };
7293
7318
  const snapshot = record5(record5(value2)?.snapshot);
7294
7319
  if (!snapshot || typeof snapshot.repository !== "string" || typeof snapshot.commitSha !== "string" || typeof snapshot.treeDigest !== "string" || !Array.isArray(snapshot.files)) throw invalid("source");
7295
7320
  const files = snapshot.files.map((value22) => {
@@ -7311,12 +7336,12 @@ async function parseSource(value2) {
7311
7336
  return { path: file.path, content: file.content };
7312
7337
  });
7313
7338
  const source2 = { repository: reference.repository, commitSha: reference.commitSha, files: referenceFiles };
7314
- const referenceDigest = await digestCodeRepositorySnapshot(source2, { maximumFiles: 1e4, maximumBytes: 16 * 1024 * 1024 });
7339
+ const referenceDigest = await digestCodeRepositorySnapshot(source2, repositoryLimits);
7315
7340
  if (referenceDigest !== reference.treeDigest) throw invalid("reference source digest");
7316
7341
  references.push({ alias: reference.alias, ...source2, treeDigest: referenceDigest });
7317
7342
  }
7318
7343
  const source = { repository: snapshot.repository, commitSha: snapshot.commitSha, files };
7319
- const digest = await digestCodeRepositorySnapshot(source, { maximumFiles: 1e4, maximumBytes: 16 * 1024 * 1024 });
7344
+ const digest = await digestCodeRepositorySnapshot(source, repositoryLimits);
7320
7345
  if (digest !== snapshot.treeDigest) throw invalid("source digest");
7321
7346
  return { ...source, treeDigest: digest, ...references.length ? { references } : {} };
7322
7347
  }
@@ -7968,8 +7993,11 @@ async function prepareRuntimeLocalSource(input) {
7968
7993
  }
7969
7994
  var RESERVED2 = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_modules", "dist", "coverage"]);
7970
7995
  var SECRET2 = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
7996
+ var SOURCE_MAX_FILES = 1e5;
7997
+ var SOURCE_MAX_BYTES = 80 * 1024 * 1024;
7998
+ var SOURCE_SET_MAX_BYTES = 480 * 1024 * 1024;
7971
7999
  async function materializeCodeRuntimeSource(snapshot, tempRoot = tmpdir3()) {
7972
- if (!snapshot.files.length || snapshot.files.length > 1e4) throw new TypeError("Code source file count is invalid");
8000
+ if (!snapshot.files.length || snapshot.files.length > SOURCE_MAX_FILES) throw new TypeError("Code source file count is invalid");
7973
8001
  const root = await mkdtemp3(join23(tempRoot, "odla-code-source-"));
7974
8002
  const sourceDir = join23(root, "source");
7975
8003
  await mkdir3(sourceDir);
@@ -7981,7 +8009,7 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = tmpdir3()) {
7981
8009
  if (seen.has(file.path)) throw new TypeError("Code source repeats a path");
7982
8010
  seen.add(file.path);
7983
8011
  bytes += Buffer.byteLength(file.path) + Buffer.byteLength(file.content);
7984
- if (bytes > 16 * 1024 * 1024) throw new TypeError("Code source exceeds its byte bound");
8012
+ if (bytes > SOURCE_MAX_BYTES) throw new TypeError("Code source exceeds its byte bound");
7985
8013
  const target = resolve32(sourceDir, file.path);
7986
8014
  if (!target.startsWith(`${resolve32(sourceDir)}${sep23}`)) throw new TypeError("Code source path escapes its root");
7987
8015
  await mkdir3(dirname10(target), { recursive: true });
@@ -7989,14 +8017,14 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = tmpdir3()) {
7989
8017
  }
7990
8018
  for (const reference of snapshot.references ?? []) {
7991
8019
  validateAlias(reference.alias);
7992
- if (!reference.files.length || reference.files.length > 1e4) throw new TypeError("Code reference file count is invalid");
8020
+ if (!reference.files.length || reference.files.length > SOURCE_MAX_FILES) throw new TypeError("Code reference file count is invalid");
7993
8021
  for (const file of reference.files) {
7994
8022
  validatePath(file.path);
7995
8023
  const path = `.odla-references/${reference.alias}/${file.path}`;
7996
8024
  if (seen.has(path)) throw new TypeError("Code reference repeats a path");
7997
8025
  seen.add(path);
7998
8026
  bytes += Buffer.byteLength(path) + Buffer.byteLength(file.content);
7999
- if (bytes > 80 * 1024 * 1024) throw new TypeError("Code source set exceeds its byte bound");
8027
+ if (bytes > SOURCE_SET_MAX_BYTES) throw new TypeError("Code source set exceeds its byte bound");
8000
8028
  const target = resolve32(sourceDir, path);
8001
8029
  if (!target.startsWith(`${resolve32(sourceDir)}${sep23}`)) throw new TypeError("Code reference path escapes its root");
8002
8030
  await mkdir3(dirname10(target), { recursive: true });
@@ -8022,7 +8050,7 @@ async function attachCodeRuntimeReferences(workspace, references) {
8022
8050
  validatePath(file.path);
8023
8051
  const path = `.odla-references/${reference.alias}/${file.path}`;
8024
8052
  bytes += Buffer.byteLength(path) + Buffer.byteLength(file.content);
8025
- if (bytes > 64 * 1024 * 1024) throw new TypeError("Code reference set exceeds its byte bound");
8053
+ if (bytes > SOURCE_SET_MAX_BYTES - SOURCE_MAX_BYTES) throw new TypeError("Code reference set exceeds its byte bound");
8026
8054
  for (const root of [workspace.baselineDir, workspace.workspaceDir]) {
8027
8055
  const target = resolve32(root, path);
8028
8056
  if (!target.startsWith(`${resolve32(root)}${sep23}`)) throw new TypeError("Code reference path escapes its root");
@@ -8072,7 +8100,10 @@ async function materializeCommandWorkspace(input) {
8072
8100
  trustedBaseDir: materialized.sourceDir,
8073
8101
  trustedBaseCommitSha: source.commitSha,
8074
8102
  checkpoint: codeCheckpointPayload(command.payload)
8075
- })).workspace : await stageWorkspace(materialized.sourceDir);
8103
+ })).workspace : await stageWorkspace(materialized.sourceDir, {
8104
+ maxFiles: SOURCE_MAX_FILES,
8105
+ maxBytes: SOURCE_SET_MAX_BYTES
8106
+ });
8076
8107
  return { workspace, sourceDigest: source.treeDigest, requestedLocal: null };
8077
8108
  } finally {
8078
8109
  await materialized.cleanup();
@@ -10446,13 +10477,16 @@ async function credentialCommand(parsed, deps = {}) {
10446
10477
  // src/help-usage.ts
10447
10478
  var AUTH_SECTION = `
10448
10479
  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.
10480
+ npx odla-ai device enroll --no-open --wait 600
10481
+ One browser approval, and no flags to remember: this covers every app you
10482
+ own \u2014 including apps you create later \u2014 with every capability that
10483
+ approval can carry. Afterwards every worktree on this machine mints its
10484
+ own short-lived credentials with nobody's attention. The window rolls
10485
+ forward each time you use it, so continuous work never interrupts anyone;
10486
+ only a real gap does. Give the human the printed /studio?code= URL, keep
10487
+ the process alive, and wait on it.
10488
+ Narrow it deliberately with --app <id> or --capability <c>; the CLI then
10489
+ says what that gave up.
10456
10490
 
10457
10491
  npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600
10458
10492
  The same thing across all of odla, for weeks. Needs a platform
@@ -10595,7 +10629,7 @@ Usage:
10595
10629
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
10596
10630
  odla-ai security run [target] --self --ack-redacted-source
10597
10631
  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]
10632
+ odla-ai device enroll [--app <id>[,<id>...]|--all-apps] [--capability all|<c>[,<c>...]] [--platform-wide]
10599
10633
  [--name <label>] [--device-ttl <30d|6w|2y|forever>] [--email <odla-account>] [--no-open] [--wait <seconds>] [--json]
10600
10634
  odla-ai device list [--email <odla-account>] [--json]
10601
10635
  odla-ai device revoke <device-id> [--email <odla-account>] [--json]
@@ -10721,11 +10755,13 @@ Commands:
10721
10755
  enrollment in the browser; from then on EVERY worktree on this
10722
10756
  machine mints its own short-lived credentials with nobody's
10723
10757
  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.
10758
+ With no flags it covers every app you own, now and later, with
10759
+ every capability that approval can carry \u2014 so creating an app
10760
+ costs no new approval, and a capability nobody thought to name is
10761
+ not a 403 next week. "--app" or "--capability" narrow it
10762
+ deliberately, and the CLI says what that gave up.
10763
+ "--platform-wide" is the administrator's version, across all of
10764
+ odla.
10729
10765
  The expiry is a GAP, not a clock: each use rolls it forward, so
10730
10766
  only going quiet brings a human back into the loop \u2014 which is
10731
10767
  where anything that changed can be explained.
@@ -13765,10 +13801,17 @@ function advisoryCollectingFetch(inner, sink) {
13765
13801
  return response2;
13766
13802
  });
13767
13803
  }
13804
+ var superseded = /* @__PURE__ */ new Set();
13805
+ function supersedeAdvisory(code) {
13806
+ superseded.add(code);
13807
+ }
13768
13808
  function renderAdvisories(out, advisories, env = process.env) {
13809
+ const retracted = new Set(superseded);
13810
+ superseded.clear();
13769
13811
  if (env.ODLA_NO_ADVISORIES) return;
13770
13812
  const seen = /* @__PURE__ */ new Set();
13771
13813
  for (const advisory of advisories) {
13814
+ if (retracted.has(advisory.code)) continue;
13772
13815
  const key = `${advisory.code}:${advisory.message}`;
13773
13816
  if (seen.has(key)) continue;
13774
13817
  seen.add(key);
@@ -13836,11 +13879,18 @@ async function deviceCommand(parsed, deps) {
13836
13879
  async function enroll(parsed, deps, cfg, doFetch, out, json) {
13837
13880
  const name = stringOpt(parsed.options.name) ?? defaultDeviceName();
13838
13881
  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);
13882
+ const narrowed = stringOpt(parsed.options.app) !== void 0 || stringOpt(parsed.options.capability) !== void 0;
13883
+ 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
13884
  if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026], or --all-apps");
13841
13885
  const deviceTtlMs = parseDeviceTtl(parsed.options["device-ttl"]);
13842
13886
  const extended = platformWide || deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
13843
- const { capabilities, scopes } = requestedEnvelope(parsed, platformWide);
13887
+ const { capabilities, scopes } = requestedEnvelope(parsed, platformWide, narrowed);
13888
+ const narrowNotice = narrowEnrollmentNotice({
13889
+ appIds: apps,
13890
+ capabilities: capabilities ?? [],
13891
+ scopes: scopes ?? []
13892
+ });
13893
+ if (narrowNotice) out.error(narrowNotice);
13844
13894
  const token = await scopedToken2(
13845
13895
  parsed,
13846
13896
  deps,
@@ -13876,8 +13926,10 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
13876
13926
  }, null, 2));
13877
13927
  chmodSync3(path, 384);
13878
13928
  rememberMachineIdentity(cfg.platformUrl.replace(/\/$/, ""), stringOpt(parsed.options.email));
13929
+ supersedeAdvisory("credential.expiring");
13879
13930
  const reach = body.device.appIds.includes(ALL_OWNED_APPS) ? "every app you own, now and later" : body.device.appIds.join(", ");
13880
13931
  out.error(`device: enrolled "${name}" for ${reach}; credential written to ${path}`);
13932
+ if (narrowNotice) out.error(narrowNotice);
13881
13933
  out.error(
13882
13934
  "device: every worktree on this machine mints its own credentials from now on \u2014 no further approvals,"
13883
13935
  );
@@ -13896,9 +13948,9 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
13896
13948
  }, null, 2));
13897
13949
  }
13898
13950
  }
13899
- function requestedEnvelope(parsed, platformWide) {
13951
+ function requestedEnvelope(parsed, platformWide, narrowed) {
13900
13952
  const raw = stringOpt(parsed.options.capability);
13901
- const everything = platformWide || raw?.trim().toLowerCase() === "all";
13953
+ const everything = platformWide || !narrowed || raw?.trim().toLowerCase() === "all";
13902
13954
  if (everything) {
13903
13955
  return {
13904
13956
  capabilities: [...OPTIONAL_AGENT_PROJECT_CAPABILITIES],
@@ -15747,4 +15799,4 @@ export {
15747
15799
  isTerminalHostedSecurityStatus,
15748
15800
  runCli
15749
15801
  };
15750
- //# sourceMappingURL=chunk-PJ3RATDD.js.map
15802
+ //# sourceMappingURL=chunk-XUTMQMXZ.js.map