@odla-ai/cli 0.35.0 → 0.35.3

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-EG23MPUC.js";
4
+ } from "./chunk-PYR73XBD.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-Z5NSTS75.js.map
12
+ //# sourceMappingURL=cli-ZBNA7J4T.js.map
package/dist/index.cjs CHANGED
@@ -236,13 +236,36 @@ function handshakeWaitMs(waitSeconds, interactive = import_node_process3.default
236
236
  return interactive ? void 0 : 9e4;
237
237
  }
238
238
 
239
- // src/local.ts
239
+ // src/cached-credential.ts
240
240
  var import_node_fs2 = require("fs");
241
+ var noted = null;
242
+ function noteCachedCredential(tokenFile) {
243
+ noted = tokenFile;
244
+ }
245
+ function isCredentialRejection(error) {
246
+ const message2 = error instanceof Error ? error.message : String(error ?? "");
247
+ return /\((401|403)\)\s*$/.test(message2.trim());
248
+ }
249
+ function explainRejectedCredential(error) {
250
+ const tokenFile = noted;
251
+ if (!tokenFile || !isCredentialRejection(error)) return null;
252
+ noted = null;
253
+ (0, import_node_fs2.rmSync)(tokenFile, { force: true });
254
+ return [
255
+ "auth: the cached credential was rejected by odla, so it was revoked before its cached expiry.",
256
+ " The usual cause is a newer sign-in for this project: collecting a handshake retires the",
257
+ " principal's other credentials, so a second terminal or worktree supersedes this one.",
258
+ ` Discarded ${tokenFile}; re-run this command to request a fresh approval.`
259
+ ].join("\n");
260
+ }
261
+
262
+ // src/local.ts
263
+ var import_node_fs3 = require("fs");
241
264
  var import_node_path2 = require("path");
242
265
  var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
243
266
  function readJsonFile(path) {
244
267
  try {
245
- return JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
268
+ return JSON.parse((0, import_node_fs3.readFileSync)(path, "utf8"));
246
269
  } catch {
247
270
  return null;
248
271
  }
@@ -252,10 +275,10 @@ function writePrivateJson(path, value2) {
252
275
  `);
253
276
  }
254
277
  function readCredentials(path) {
255
- if (!(0, import_node_fs2.existsSync)(path)) return null;
278
+ if (!(0, import_node_fs3.existsSync)(path)) return null;
256
279
  let value2;
257
280
  try {
258
- value2 = JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
281
+ value2 = JSON.parse((0, import_node_fs3.readFileSync)(path, "utf8"));
259
282
  } catch {
260
283
  throw new Error(`credentials file ${path} is not valid JSON; fix or remove it before provisioning`);
261
284
  }
@@ -286,13 +309,13 @@ function mergeCredential(current, update) {
286
309
  }
287
310
  function ensureGitignore(rootDir, localPaths = []) {
288
311
  const path = (0, import_node_path2.resolve)(rootDir, ".gitignore");
289
- const existing = (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : "";
312
+ const existing = (0, import_node_fs3.existsSync)(path) ? (0, import_node_fs3.readFileSync)(path, "utf8") : "";
290
313
  const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line2) => !!line2);
291
314
  const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
292
315
  const missing = wanted.filter((line2) => !existing.split(/\r?\n/).includes(line2));
293
316
  if (missing.length === 0) return;
294
317
  const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
295
- (0, import_node_fs2.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
318
+ (0, import_node_fs3.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
296
319
  `);
297
320
  }
298
321
  function o11yDevVars(cfg) {
@@ -320,7 +343,7 @@ function writeDevVars(path, credentials, env, o11y) {
320
343
  if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
321
344
  if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
322
345
  }
323
- const existing = (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : "";
346
+ const existing = (0, import_node_fs3.existsSync)(path) ? (0, import_node_fs3.readFileSync)(path, "utf8") : "";
324
347
  const retained = existing.split(/\r?\n/).filter((line2) => !isManagedDevVar(line2));
325
348
  while (retained.at(-1) === "") retained.pop();
326
349
  const prefix = retained.length ? `${retained.join("\n")}
@@ -346,11 +369,11 @@ function isManagedDevVar(line2) {
346
369
  return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
347
370
  }
348
371
  function writePrivateText(path, text3) {
349
- (0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(path), { recursive: true });
372
+ (0, import_node_fs3.mkdirSync)((0, import_node_path2.dirname)(path), { recursive: true });
350
373
  const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
351
- (0, import_node_fs2.writeFileSync)(temporary, text3, { mode: 384 });
352
- (0, import_node_fs2.chmodSync)(temporary, 384);
353
- (0, import_node_fs2.renameSync)(temporary, path);
374
+ (0, import_node_fs3.writeFileSync)(temporary, text3, { mode: 384 });
375
+ (0, import_node_fs3.chmodSync)(temporary, 384);
376
+ (0, import_node_fs3.renameSync)(temporary, path);
354
377
  }
355
378
  function gitignoreEntry(rootDir, path) {
356
379
  const rel = (0, import_node_path2.relative)((0, import_node_path2.resolve)(rootDir), (0, import_node_path2.resolve)(path));
@@ -381,6 +404,7 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
381
404
  }
382
405
  if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4 && cachedGrantCovers(cached, grantIntent)) {
383
406
  out.error(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
407
+ noteCachedCredential(cfg.local.tokenFile);
384
408
  return cached.token;
385
409
  }
386
410
  } else {
@@ -535,7 +559,7 @@ async function readSecretStream(kind, stream = import_node_process5.default.stdi
535
559
  }
536
560
 
537
561
  // src/admin-ai-auth.ts
538
- var import_node_fs3 = require("fs");
562
+ var import_node_fs4 = require("fs");
539
563
  var import_node_path3 = require("path");
540
564
  var import_node_process6 = __toESM(require("process"), 1);
541
565
  var import_db2 = require("@odla-ai/db");
@@ -610,7 +634,7 @@ async function scopedToken(platform, scope, options, doFetch, out) {
610
634
  if (options.cache !== false) {
611
635
  const tokens = cache2?.platform === audience ? { ...cache2.tokens ?? {} } : {};
612
636
  tokens[scope] = { token, expiresAt };
613
- if ((0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, ".git"))) ensureGitignore(rootDir, [tokenFile]);
637
+ if ((0, import_node_fs4.existsSync)((0, import_node_path3.join)(rootDir, ".git"))) ensureGitignore(rootDir, [tokenFile]);
614
638
  writePrivateJson(tokenFile, { platform: audience, email, tokens });
615
639
  out.error(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
616
640
  } else {
@@ -1042,12 +1066,12 @@ function addOption(options, name, value2) {
1042
1066
  }
1043
1067
 
1044
1068
  // src/operator-context.ts
1045
- var import_node_fs6 = require("fs");
1069
+ var import_node_fs7 = require("fs");
1046
1070
  var import_node_path6 = require("path");
1047
1071
  var import_node_process9 = __toESM(require("process"), 1);
1048
1072
 
1049
1073
  // src/config.ts
1050
- var import_node_fs4 = require("fs");
1074
+ var import_node_fs5 = require("fs");
1051
1075
  var import_node_path4 = require("path");
1052
1076
  var import_node_url = require("url");
1053
1077
  var import_apps = require("@odla-ai/apps");
@@ -1454,7 +1478,7 @@ var configImportSerial = 0;
1454
1478
  var GOOGLE_CALENDAR_EVENTS_SCOPE = "https://www.googleapis.com/auth/calendar.events";
1455
1479
  async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1456
1480
  const resolved = (0, import_node_path4.resolve)(configPath);
1457
- if (!(0, import_node_fs4.existsSync)(resolved)) {
1481
+ if (!(0, import_node_fs5.existsSync)(resolved)) {
1458
1482
  throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
1459
1483
  }
1460
1484
  const raw = await loadConfigModule(resolved);
@@ -1489,7 +1513,7 @@ async function resolveDataExport(cfg, value2, names) {
1489
1513
  if (typeof value2 !== "string") return value2;
1490
1514
  const target = (0, import_node_path4.isAbsolute)(value2) ? value2 : (0, import_node_path4.resolve)(cfg.rootDir, value2);
1491
1515
  if (target.endsWith(".json")) {
1492
- return JSON.parse((0, import_node_fs4.readFileSync)(target, "utf8"));
1516
+ return JSON.parse((0, import_node_fs5.readFileSync)(target, "utf8"));
1493
1517
  }
1494
1518
  const mod = await import((0, import_node_url.pathToFileURL)(target).href);
1495
1519
  for (const name of names) {
@@ -1565,7 +1589,7 @@ function validId2(value2) {
1565
1589
  return typeof value2 === "string" && /^[a-z0-9][a-z0-9-]*$/.test(value2);
1566
1590
  }
1567
1591
  async function loadConfigModule(path) {
1568
- if (path.endsWith(".json")) return JSON.parse((0, import_node_fs4.readFileSync)(path, "utf8"));
1592
+ if (path.endsWith(".json")) return JSON.parse((0, import_node_fs5.readFileSync)(path, "utf8"));
1569
1593
  const nonce = `${Date.now()}-${configImportSerial++}`;
1570
1594
  const mod = await import(`${(0, import_node_url.pathToFileURL)(path).href}?reload=${nonce}`);
1571
1595
  const value2 = mod.default ?? mod.config;
@@ -1580,7 +1604,7 @@ function unique3(values) {
1580
1604
  }
1581
1605
 
1582
1606
  // src/operator-profiles.ts
1583
- var import_node_fs5 = require("fs");
1607
+ var import_node_fs6 = require("fs");
1584
1608
  var import_node_os = require("os");
1585
1609
  var import_node_path5 = require("path");
1586
1610
  var import_node_process8 = __toESM(require("process"), 1);
@@ -1645,10 +1669,10 @@ function assertOperatorName(value2, label) {
1645
1669
  }
1646
1670
  }
1647
1671
  function readOperatorProfiles(file) {
1648
- if (!(0, import_node_fs5.existsSync)(file)) return emptyProfiles();
1672
+ if (!(0, import_node_fs6.existsSync)(file)) return emptyProfiles();
1649
1673
  let raw;
1650
1674
  try {
1651
- raw = JSON.parse((0, import_node_fs5.readFileSync)(file, "utf8"));
1675
+ raw = JSON.parse((0, import_node_fs6.readFileSync)(file, "utf8"));
1652
1676
  } catch {
1653
1677
  throw new Error(`operator context file ${file} is not valid JSON`);
1654
1678
  }
@@ -1714,7 +1738,7 @@ async function resolveOperatorContext(parsed, options = {}) {
1714
1738
  const configArgument = stringOpt(parsed.options.config) ?? "odla.config.mjs";
1715
1739
  const configPath = (0, import_node_path6.resolve)(configArgument);
1716
1740
  const explicitConfig = parsed.options.config !== void 0;
1717
- const hasConfig = (0, import_node_fs6.existsSync)(configPath);
1741
+ const hasConfig = (0, import_node_fs7.existsSync)(configPath);
1718
1742
  if (!hasConfig && (!options.allowMissingConfig || explicitConfig)) {
1719
1743
  await loadProjectConfig(configArgument);
1720
1744
  }
@@ -2180,7 +2204,7 @@ async function appExport(options) {
2180
2204
  }
2181
2205
 
2182
2206
  // src/app-import.ts
2183
- var import_node_fs7 = require("fs");
2207
+ var import_node_fs8 = require("fs");
2184
2208
  var import_import = require("@odla-ai/db/import");
2185
2209
  function chooseIdMode(options, rows) {
2186
2210
  const chosen = [options.idField && "field", options.key && "key", options.generateIds && "generate"].filter(Boolean);
@@ -2198,7 +2222,7 @@ async function appImport(options) {
2198
2222
  const out = options.stdout ?? console;
2199
2223
  const say = options.json ? (line2) => out.error(line2) : (line2) => out.log(line2);
2200
2224
  const { tenant } = resolveTenant(cfg, options.env);
2201
- const text3 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs7.readFileSync)(0, "utf8")))() : (0, import_node_fs7.readFileSync)(options.file, "utf8");
2225
+ const text3 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs8.readFileSync)(0, "utf8")))() : (0, import_node_fs8.readFileSync)(options.file, "utf8");
2202
2226
  const { format, sources } = (0, import_import.parseImport)(text3, options.ns);
2203
2227
  if (format === "namespace-map" && options.ns) {
2204
2228
  throw new Error("--ns cannot be combined with a {namespace: rows} file \u2014 the file already names each namespace");
@@ -3114,9 +3138,9 @@ var import_apps6 = require("@odla-ai/apps");
3114
3138
  var import_node_path8 = require("path");
3115
3139
 
3116
3140
  // src/version.ts
3117
- var import_node_fs8 = require("fs");
3141
+ var import_node_fs9 = require("fs");
3118
3142
  function cliVersion() {
3119
- const pkg = JSON.parse((0, import_node_fs8.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
3143
+ const pkg = JSON.parse((0, import_node_fs9.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
3120
3144
  return pkg.version ?? "unknown";
3121
3145
  }
3122
3146
 
@@ -3132,7 +3156,7 @@ var ConfigOperationCommandError = class extends Error {
3132
3156
 
3133
3157
  // src/config-operation-validate.ts
3134
3158
  var import_apps3 = require("@odla-ai/apps");
3135
- var import_node_fs9 = require("fs");
3159
+ var import_node_fs10 = require("fs");
3136
3160
 
3137
3161
  // src/config-reconcile-digest.ts
3138
3162
  var import_node_crypto2 = require("crypto");
@@ -3168,7 +3192,7 @@ var SERVICE = /^[a-z][a-z0-9-]{0,39}$/;
3168
3192
  function readPlan(path) {
3169
3193
  let value2;
3170
3194
  try {
3171
- const raw = (0, import_node_fs9.readFileSync)(path, "utf8");
3195
+ const raw = (0, import_node_fs10.readFileSync)(path, "utf8");
3172
3196
  if (Buffer.byteLength(raw) > 128 * 1024) throw new Error("plan exceeds 128 KiB");
3173
3197
  value2 = JSON.parse(raw);
3174
3198
  } catch (error) {
@@ -4024,12 +4048,12 @@ function quoteArg2(value2) {
4024
4048
 
4025
4049
  // src/doctor-checks.ts
4026
4050
  var import_node_child_process3 = require("child_process");
4027
- var import_node_fs11 = require("fs");
4051
+ var import_node_fs12 = require("fs");
4028
4052
  var import_node_path11 = require("path");
4029
4053
 
4030
4054
  // src/wrangler.ts
4031
4055
  var import_node_child_process2 = require("child_process");
4032
- var import_node_fs10 = require("fs");
4056
+ var import_node_fs11 = require("fs");
4033
4057
  var import_node_path10 = require("path");
4034
4058
  var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
4035
4059
  const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
@@ -4045,14 +4069,14 @@ var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"]
4045
4069
  function findWranglerConfig(rootDir) {
4046
4070
  for (const name of WRANGLER_CONFIG_FILES) {
4047
4071
  const path = (0, import_node_path10.join)(rootDir, name);
4048
- if ((0, import_node_fs10.existsSync)(path)) return path;
4072
+ if ((0, import_node_fs11.existsSync)(path)) return path;
4049
4073
  }
4050
4074
  return null;
4051
4075
  }
4052
4076
  function readWranglerConfig(path) {
4053
4077
  if (path.endsWith(".toml")) return null;
4054
4078
  try {
4055
- return JSON.parse(stripJsonComments((0, import_node_fs10.readFileSync)(path, "utf8")));
4079
+ return JSON.parse(stripJsonComments((0, import_node_fs11.readFileSync)(path, "utf8")));
4056
4080
  } catch {
4057
4081
  return null;
4058
4082
  }
@@ -4205,7 +4229,7 @@ function wranglerWarnings(rootDir) {
4205
4229
  const dir = (0, import_node_path11.resolve)(rootDir, assets.directory);
4206
4230
  if (dir === (0, import_node_path11.resolve)(rootDir)) {
4207
4231
  warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
4208
- } else if ((0, import_node_fs11.existsSync)((0, import_node_path11.join)(dir, "node_modules"))) {
4232
+ } else if ((0, import_node_fs12.existsSync)((0, import_node_path11.join)(dir, "node_modules"))) {
4209
4233
  warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
4210
4234
  }
4211
4235
  }
@@ -4241,12 +4265,12 @@ function o11yProjectWarnings(rootDir) {
4241
4265
  return warnings;
4242
4266
  }
4243
4267
  const main = typeof config.main === "string" ? (0, import_node_path11.resolve)(rootDir, config.main) : null;
4244
- if (!main || !(0, import_node_fs11.existsSync)(main)) {
4268
+ if (!main || !(0, import_node_fs12.existsSync)(main)) {
4245
4269
  warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
4246
4270
  } else {
4247
4271
  let source = "";
4248
4272
  try {
4249
- source = (0, import_node_fs11.readFileSync)(main, "utf8");
4273
+ source = (0, import_node_fs12.readFileSync)(main, "utf8");
4250
4274
  } catch {
4251
4275
  }
4252
4276
  if (!/\bwithObservability\b/.test(source)) {
@@ -4270,7 +4294,7 @@ function calendarProjectWarnings(rootDir) {
4270
4294
  }
4271
4295
  function readPackageJson(rootDir) {
4272
4296
  try {
4273
- return JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path11.join)(rootDir, "package.json"), "utf8"));
4297
+ return JSON.parse((0, import_node_fs12.readFileSync)((0, import_node_path11.join)(rootDir, "package.json"), "utf8"));
4274
4298
  } catch {
4275
4299
  return null;
4276
4300
  }
@@ -4564,14 +4588,14 @@ function harnessOption(value2, flag) {
4564
4588
  }
4565
4589
 
4566
4590
  // src/init.ts
4567
- var import_node_fs12 = require("fs");
4591
+ var import_node_fs13 = require("fs");
4568
4592
  var import_node_path12 = require("path");
4569
4593
  var import_apps9 = require("@odla-ai/apps");
4570
4594
  function initProject(options) {
4571
4595
  const out = options.stdout ?? console;
4572
4596
  const rootDir = (0, import_node_path12.resolve)(options.rootDir ?? process.cwd());
4573
4597
  const configPath = (0, import_node_path12.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
4574
- if ((0, import_node_fs12.existsSync)(configPath) && !options.force) {
4598
+ if ((0, import_node_fs13.existsSync)(configPath) && !options.force) {
4575
4599
  throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
4576
4600
  }
4577
4601
  if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
@@ -4587,10 +4611,10 @@ function initProject(options) {
4587
4611
  }
4588
4612
  }
4589
4613
  const aiProvider = options.aiProvider;
4590
- (0, import_node_fs12.mkdirSync)((0, import_node_path12.dirname)(configPath), { recursive: true });
4591
- (0, import_node_fs12.mkdirSync)((0, import_node_path12.resolve)(rootDir, "src/odla"), { recursive: true });
4592
- (0, import_node_fs12.mkdirSync)((0, import_node_path12.resolve)(rootDir, ".odla"), { recursive: true });
4593
- (0, import_node_fs12.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
4614
+ (0, import_node_fs13.mkdirSync)((0, import_node_path12.dirname)(configPath), { recursive: true });
4615
+ (0, import_node_fs13.mkdirSync)((0, import_node_path12.resolve)(rootDir, "src/odla"), { recursive: true });
4616
+ (0, import_node_fs13.mkdirSync)((0, import_node_path12.resolve)(rootDir, ".odla"), { recursive: true });
4617
+ (0, import_node_fs13.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
4594
4618
  writeIfMissing((0, import_node_path12.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
4595
4619
  writeIfMissing((0, import_node_path12.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
4596
4620
  ensureGitignore(rootDir);
@@ -4599,8 +4623,8 @@ function initProject(options) {
4599
4623
  out.log("updated .gitignore for local odla credentials");
4600
4624
  }
4601
4625
  function writeIfMissing(path, text3) {
4602
- if ((0, import_node_fs12.existsSync)(path)) return;
4603
- (0, import_node_fs12.writeFileSync)(path, text3);
4626
+ if ((0, import_node_fs13.existsSync)(path)) return;
4627
+ (0, import_node_fs13.writeFileSync)(path, text3);
4604
4628
  }
4605
4629
  function configTemplate(input) {
4606
4630
  const calendar = input.services.includes("calendar") ? ` calendar: {
@@ -4896,7 +4920,7 @@ function printReport(report5, out) {
4896
4920
  }
4897
4921
 
4898
4922
  // src/skill.ts
4899
- var import_node_fs13 = require("fs");
4923
+ var import_node_fs14 = require("fs");
4900
4924
  var import_node_os2 = require("os");
4901
4925
  var import_node_path13 = require("path");
4902
4926
  var import_node_url2 = require("url");
@@ -5012,7 +5036,7 @@ function installSkill(options = {}) {
5012
5036
  plans.set(target, { target, content: content2, boundary, managedMerge });
5013
5037
  };
5014
5038
  const planSkillTree = (targetDir2, boundary = root) => {
5015
- for (const rel of files) plan((0, import_node_path13.join)(targetDir2, rel), (0, import_node_fs13.readFileSync)((0, import_node_path13.join)(sourceDir, rel), "utf8"), false, boundary);
5039
+ for (const rel of files) plan((0, import_node_path13.join)(targetDir2, rel), (0, import_node_fs14.readFileSync)((0, import_node_path13.join)(sourceDir, rel), "utf8"), false, boundary);
5016
5040
  };
5017
5041
  let targetDir;
5018
5042
  if (options.global) {
@@ -5032,7 +5056,7 @@ function installSkill(options = {}) {
5032
5056
  for (const harness of harnesses) rememberTarget(harness, sharedRoot);
5033
5057
  if (harnesses.includes("claude")) {
5034
5058
  for (const skill of skillNames(files)) {
5035
- const canonical2 = (0, import_node_fs13.readFileSync)((0, import_node_path13.join)(sourceDir, skill, "SKILL.md"), "utf8");
5059
+ const canonical2 = (0, import_node_fs14.readFileSync)((0, import_node_path13.join)(sourceDir, skill, "SKILL.md"), "utf8");
5036
5060
  plan((0, import_node_path13.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical2));
5037
5061
  }
5038
5062
  rememberTarget("claude", claudeRoot);
@@ -5067,11 +5091,11 @@ function installSkill(options = {}) {
5067
5091
  conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
5068
5092
  continue;
5069
5093
  }
5070
- if (!(0, import_node_fs13.existsSync)(file.target)) {
5094
+ if (!(0, import_node_fs14.existsSync)(file.target)) {
5071
5095
  writtenPaths.add(file.target);
5072
5096
  continue;
5073
5097
  }
5074
- const current = (0, import_node_fs13.readFileSync)(file.target, "utf8");
5098
+ const current = (0, import_node_fs14.readFileSync)(file.target, "utf8");
5075
5099
  if (current === file.content) {
5076
5100
  unchangedPaths.add(file.target);
5077
5101
  } else if (file.managedMerge || options.force) {
@@ -5088,9 +5112,9 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
5088
5112
  );
5089
5113
  }
5090
5114
  for (const file of plans.values()) {
5091
- if (!(0, import_node_fs13.existsSync)(file.target) || (0, import_node_fs13.readFileSync)(file.target, "utf8") !== file.content) {
5092
- (0, import_node_fs13.mkdirSync)((0, import_node_path13.dirname)(file.target), { recursive: true });
5093
- (0, import_node_fs13.writeFileSync)(file.target, file.content);
5115
+ if (!(0, import_node_fs14.existsSync)(file.target) || (0, import_node_fs14.readFileSync)(file.target, "utf8") !== file.content) {
5116
+ (0, import_node_fs14.mkdirSync)((0, import_node_path13.dirname)(file.target), { recursive: true });
5117
+ (0, import_node_fs14.writeFileSync)(file.target, file.content);
5094
5118
  }
5095
5119
  }
5096
5120
  const skills = skillNames(files);
@@ -5131,9 +5155,9 @@ function normalizeHarnesses(values, global) {
5131
5155
  function managedFileContent(path, block, force, boundary) {
5132
5156
  const symlink = symlinkedComponent(boundary, path);
5133
5157
  if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
5134
- if (!(0, import_node_fs13.existsSync)(path)) return `${block}
5158
+ if (!(0, import_node_fs14.existsSync)(path)) return `${block}
5135
5159
  `;
5136
- const current = (0, import_node_fs13.readFileSync)(path, "utf8");
5160
+ const current = (0, import_node_fs14.readFileSync)(path, "utf8");
5137
5161
  const start = "<!-- odla-ai agent setup:start -->";
5138
5162
  const end = "<!-- odla-ai agent setup:end -->";
5139
5163
  const startAt = current.indexOf(start);
@@ -5162,7 +5186,7 @@ function symlinkedComponent(boundary, target) {
5162
5186
  for (const part of rel.split(import_node_path13.sep).filter(Boolean)) {
5163
5187
  current = (0, import_node_path13.join)(current, part);
5164
5188
  try {
5165
- if ((0, import_node_fs13.lstatSync)(current).isSymbolicLink()) return current;
5189
+ if ((0, import_node_fs14.lstatSync)(current).isSymbolicLink()) return current;
5166
5190
  } catch (error) {
5167
5191
  if (error.code !== "ENOENT") throw error;
5168
5192
  }
@@ -5173,10 +5197,10 @@ function skillNames(files) {
5173
5197
  return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
5174
5198
  }
5175
5199
  function listFiles(dir) {
5176
- if (!(0, import_node_fs13.existsSync)(dir)) return [];
5200
+ if (!(0, import_node_fs14.existsSync)(dir)) return [];
5177
5201
  const results = [];
5178
5202
  const walk = (current) => {
5179
- for (const entry of (0, import_node_fs13.readdirSync)(current, { withFileTypes: true })) {
5203
+ for (const entry of (0, import_node_fs14.readdirSync)(current, { withFileTypes: true })) {
5180
5204
  const path = (0, import_node_path13.join)(current, entry.name);
5181
5205
  if (entry.isDirectory()) walk(path);
5182
5206
  else results.push((0, import_node_path13.relative)(dir, path));
@@ -5534,7 +5558,7 @@ async function projectCommand(command, parsed, deps) {
5534
5558
  }
5535
5559
 
5536
5560
  // src/code-connect.ts
5537
- var import_node_fs14 = require("fs");
5561
+ var import_node_fs15 = require("fs");
5538
5562
  var import_node_os3 = require("os");
5539
5563
  var import_node_path14 = require("path");
5540
5564
 
@@ -9584,7 +9608,7 @@ var CODE_BUILD_RECIPES = Object.freeze([{
9584
9608
  async function codeConnect(options) {
9585
9609
  const cwd = options.cwd ?? process.cwd();
9586
9610
  const configPath = (0, import_node_path14.resolve)(cwd, options.configPath);
9587
- const cfg = (0, import_node_fs14.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
9611
+ const cfg = (0, import_node_fs15.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
9588
9612
  const requestedAppId = options.appId?.trim();
9589
9613
  if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
9590
9614
  throw new Error("--app-id must be a valid odla app id");
@@ -13210,7 +13234,7 @@ async function provision(options) {
13210
13234
  }
13211
13235
 
13212
13236
  // src/record.ts
13213
- var import_node_fs15 = require("fs");
13237
+ var import_node_fs16 = require("fs");
13214
13238
  var import_node_process13 = __toESM(require("process"), 1);
13215
13239
 
13216
13240
  // src/surface.ts
@@ -13400,14 +13424,37 @@ function recordInvocation(parsed) {
13400
13424
  options: Object.entries(parsed.options).map(([name, value2]) => value2 === false ? `no-${name}` : name).sort()
13401
13425
  };
13402
13426
  if (!entry.path.length) return;
13403
- (0, import_node_fs15.appendFileSync)(file, `${JSON.stringify(entry)}
13427
+ (0, import_node_fs16.appendFileSync)(file, `${JSON.stringify(entry)}
13404
13428
  `);
13405
13429
  } catch {
13406
13430
  }
13407
13431
  }
13408
13432
 
13433
+ // src/advisory-output.ts
13434
+ var import_apps14 = require("@odla-ai/apps");
13435
+ function advisoryCollectingFetch(inner, sink) {
13436
+ return (async (input, init) => {
13437
+ const response2 = await inner(input, init);
13438
+ try {
13439
+ sink.push(...(0, import_apps14.parseAdvisories)(response2));
13440
+ } catch {
13441
+ }
13442
+ return response2;
13443
+ });
13444
+ }
13445
+ function renderAdvisories(out, advisories, env = process.env) {
13446
+ if (env.ODLA_NO_ADVISORIES) return;
13447
+ const seen = /* @__PURE__ */ new Set();
13448
+ for (const advisory of advisories) {
13449
+ const key = `${advisory.code}:${advisory.message}`;
13450
+ if (seen.has(key)) continue;
13451
+ seen.add(key);
13452
+ out.error((0, import_apps14.formatAdvisory)(advisory));
13453
+ }
13454
+ }
13455
+
13409
13456
  // src/runbook-actions.ts
13410
- var import_node_fs16 = require("fs");
13457
+ var import_node_fs17 = require("fs");
13411
13458
 
13412
13459
  // src/runbook-requires.ts
13413
13460
  var SPEC = /^(@?[\w./-]+?)@(\d+\.\d+\.\d+(?:[\w.-]*)?)$/;
@@ -13485,14 +13532,25 @@ async function bySlug(ctx, slug) {
13485
13532
  "GET",
13486
13533
  `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
13487
13534
  );
13488
- const found = page2.records[0];
13489
- if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
13490
- return found;
13535
+ const filtered = page2.records.find((record11) => record11.slug === slug);
13536
+ if (filtered) return filtered;
13537
+ const limit = 100;
13538
+ for (let offset = 0; ; offset += limit) {
13539
+ const fallback = await call(
13540
+ ctx,
13541
+ "GET",
13542
+ `/runbook?app=${encodeURIComponent(ctx.appId)}&limit=${limit}&offset=${offset}`
13543
+ );
13544
+ const found = fallback.records.find((record11) => record11.slug === slug);
13545
+ if (found) return found;
13546
+ if (!fallback.records.length || offset + fallback.records.length >= fallback.total) break;
13547
+ }
13548
+ throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
13491
13549
  }
13492
13550
  function readBody(file, inline) {
13493
13551
  if (inline !== void 0) return inline;
13494
13552
  if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
13495
- return (0, import_node_fs16.readFileSync)(file === "-" ? 0 : file, "utf8");
13553
+ return (0, import_node_fs17.readFileSync)(file === "-" ? 0 : file, "utf8");
13496
13554
  }
13497
13555
  var stamp = (ms) => ms ? new Date(ms).toISOString().slice(0, 16).replace("T", " ") : "";
13498
13556
  async function runbookList(ctx, all, query) {
@@ -13572,7 +13630,7 @@ async function runbookRevert(ctx, slug, version) {
13572
13630
  ctx,
13573
13631
  "POST",
13574
13632
  `/runbook/${encodeURIComponent(runbook.id)}/revert`,
13575
- { version }
13633
+ { version, expectedVersion: runbook.version }
13576
13634
  );
13577
13635
  if (ctx.json) return ctx.out.log(JSON.stringify(result, null, 2));
13578
13636
  ctx.out.log(`${slug} reverted to v${version}, now v${result.record?.version ?? runbook.version + 1}`);
@@ -13584,7 +13642,7 @@ async function runbookRemove(ctx, slug) {
13584
13642
  }
13585
13643
 
13586
13644
  // src/runbook-import.ts
13587
- var import_node_fs17 = require("fs");
13645
+ var import_node_fs18 = require("fs");
13588
13646
  var import_node_path16 = require("path");
13589
13647
  function parseRunbook(text3, slug) {
13590
13648
  let rest = text3;
@@ -13610,12 +13668,12 @@ function parseRunbook(text3, slug) {
13610
13668
  };
13611
13669
  }
13612
13670
  function readRunbookDir(dir) {
13613
- if (!(0, import_node_fs17.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
13614
- const files = (0, import_node_fs17.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
13671
+ if (!(0, import_node_fs18.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
13672
+ const files = (0, import_node_fs18.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
13615
13673
  if (!files.length) throw new Error(`no .md files in ${dir}`);
13616
13674
  return files.map((file) => {
13617
13675
  const slug = (0, import_node_path16.basename)(file, ".md");
13618
- const parsed = parseRunbook((0, import_node_fs17.readFileSync)((0, import_node_path16.join)(dir, file), "utf8"), slug);
13676
+ const parsed = parseRunbook((0, import_node_fs18.readFileSync)((0, import_node_path16.join)(dir, file), "utf8"), slug);
13619
13677
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
13620
13678
  });
13621
13679
  }
@@ -13688,7 +13746,7 @@ async function upsert(ctx, r, visibility) {
13688
13746
 
13689
13747
  // src/runbook-impact.ts
13690
13748
  var import_node_child_process6 = require("child_process");
13691
- var import_node_fs18 = require("fs");
13749
+ var import_node_fs19 = require("fs");
13692
13750
  var import_node_path17 = require("path");
13693
13751
 
13694
13752
  // src/runbook-impact-scan.ts
@@ -13859,9 +13917,9 @@ ${body.split("\n").map((line2) => `+${line2}`).join("\n")}
13859
13917
  function manifestLabeller(root) {
13860
13918
  return (workspace) => {
13861
13919
  const manifest = (0, import_node_path17.join)(root, workspace, "package.json");
13862
- if (!(0, import_node_fs18.existsSync)(manifest)) return void 0;
13920
+ if (!(0, import_node_fs19.existsSync)(manifest)) return void 0;
13863
13921
  try {
13864
- const name = JSON.parse((0, import_node_fs18.readFileSync)(manifest, "utf8")).name;
13922
+ const name = JSON.parse((0, import_node_fs19.readFileSync)(manifest, "utf8")).name;
13865
13923
  return typeof name === "string" ? name : void 0;
13866
13924
  } catch {
13867
13925
  return void 0;
@@ -13928,7 +13986,7 @@ function report4(ctx, impacts) {
13928
13986
  async function runbookImpact(ctx, options, deps = {}) {
13929
13987
  const cwd = deps.cwd ?? process.cwd();
13930
13988
  const runGit = deps.runGit ?? gitRunner(cwd);
13931
- const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs18.readFileSync)((0, import_node_path17.join)(cwd, path), "utf8"));
13989
+ const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs19.readFileSync)((0, import_node_path17.join)(cwd, path), "utf8"));
13932
13990
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
13933
13991
  if (!surfaces.length) {
13934
13992
  return ctx.out.log(
@@ -14061,7 +14119,7 @@ async function runbookComment(ctx, slug, body) {
14061
14119
 
14062
14120
  // src/runbook-editor.ts
14063
14121
  var import_node_child_process7 = require("child_process");
14064
- var import_node_fs19 = require("fs");
14122
+ var import_node_fs20 = require("fs");
14065
14123
  var import_node_os4 = require("os");
14066
14124
  var import_node_path18 = require("path");
14067
14125
  var import_node_process14 = __toESM(require("process"), 1);
@@ -14089,16 +14147,16 @@ function editText(initial, slug, deps = {}) {
14089
14147
  );
14090
14148
  if (!interactive())
14091
14149
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
14092
- const dir = (0, import_node_fs19.mkdtempSync)((0, import_node_path18.join)((0, import_node_os4.tmpdir)(), "odla-runbook-"));
14150
+ const dir = (0, import_node_fs20.mkdtempSync)((0, import_node_path18.join)((0, import_node_os4.tmpdir)(), "odla-runbook-"));
14093
14151
  const file = (0, import_node_path18.join)(dir, `${slug}.md`);
14094
14152
  try {
14095
- (0, import_node_fs19.writeFileSync)(file, initial, { mode: 384 });
14153
+ (0, import_node_fs20.writeFileSync)(file, initial, { mode: 384 });
14096
14154
  const code = defaultRunOrInjected(deps)(editor, file);
14097
14155
  if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
14098
- const edited = (0, import_node_fs19.readFileSync)(file, "utf8");
14156
+ const edited = (0, import_node_fs20.readFileSync)(file, "utf8");
14099
14157
  return edited === initial ? null : edited;
14100
14158
  } finally {
14101
- (0, import_node_fs19.rmSync)(dir, { recursive: true, force: true });
14159
+ (0, import_node_fs20.rmSync)(dir, { recursive: true, force: true });
14102
14160
  }
14103
14161
  }
14104
14162
  var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
@@ -14990,6 +15048,23 @@ function exitCodeFor(err) {
14990
15048
 
14991
15049
  // src/cli.ts
14992
15050
  async function runCli(argv = process.argv.slice(2), dependencies = {}) {
15051
+ const out = redactingOutput(dependencies.stdout ?? console);
15052
+ const advisories = [];
15053
+ const withAdvisoryReader = {
15054
+ ...dependencies,
15055
+ fetch: advisoryCollectingFetch(dependencies.fetch ?? fetch, advisories)
15056
+ };
15057
+ try {
15058
+ return await dispatchCli(argv, withAdvisoryReader);
15059
+ } catch (error) {
15060
+ const explanation = explainRejectedCredential(error);
15061
+ if (explanation) out.error(explanation);
15062
+ throw error;
15063
+ } finally {
15064
+ renderAdvisories(out, advisories);
15065
+ }
15066
+ }
15067
+ async function dispatchCli(argv, dependencies) {
14993
15068
  const runtime = {
14994
15069
  ...dependencies,
14995
15070
  stdout: redactingOutput(dependencies.stdout ?? console)