@kubb/studio 5.12.2 → 5.12.4

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/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as InstalledExtensions, L as PublishedSnapshot, a as AgentConnectInfo, c as AgentPermissions, d as ConfigEditOutcome, q as SnapshotUploadMetadata, s as AgentPermission, u as ConfigEdit } from "./protocol-DhNrwhqp.cjs";
1
+ import { Y as SnapshotUploadMetadata, a as AgentConnectInfo, c as AgentPermissions, f as ConfigEditOutcome, j as InstalledExtensions, s as AgentPermission, u as ConfigEdit, z as PublishedSnapshot } from "./protocol-C4loKcxw.cjs";
2
2
  import { Config, Hookable, KubbHooks } from "@kubb/core";
3
3
  //#region src/connect.d.ts
4
4
  /**
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as InstalledExtensions, L as PublishedSnapshot, a as AgentConnectInfo, c as AgentPermissions, d as ConfigEditOutcome, q as SnapshotUploadMetadata, s as AgentPermission, u as ConfigEdit } from "./protocol-DhNrwhqp.js";
1
+ import { Y as SnapshotUploadMetadata, a as AgentConnectInfo, c as AgentPermissions, f as ConfigEditOutcome, j as InstalledExtensions, s as AgentPermission, u as ConfigEdit, z as PublishedSnapshot } from "./protocol-C4loKcxw.js";
2
2
  import { Config, Hookable, KubbHooks } from "@kubb/core";
3
3
  //#region src/connect.d.ts
4
4
  /**
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
- import { AGENT_INSTANCE_HEADER, AGENT_SOCKET_PATH, AgentCloseCode, AgentMessage, AgentPermission, MAX_SNAPSHOT_BYTES, generationEventTypes } from "./protocol.js";
1
+ import { AGENT_INSTANCE_HEADER, AGENT_SOCKET_PATH, AgentCloseCode, AgentMessage, AgentPermission, ConfigEditOperation, MAX_SNAPSHOT_BYTES, generationEventTypes } from "./protocol.js";
2
2
  import { createRequire } from "node:module";
3
3
  import { createHash, hash, randomUUID } from "node:crypto";
4
4
  import { existsSync } from "node:fs";
5
+ import { mkdir, mkdir as mkdir$1, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
5
6
  import { tmpdir } from "node:os";
6
7
  import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
8
  import process$1 from "node:process";
8
9
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
9
10
  import { x } from "tinyexec";
10
- import { mkdir, mkdir as mkdir$1, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
11
11
  import { Diagnostics, Hookable, cacheStorage, checkoutGitInput, createKubb, fsStorage, getInputKind, isPackageName, isPublicAddress, readInstalledPackages, resolveCacheDir, resolveMeta } from "@kubb/core";
12
12
  import { RpcTarget, newWebSocketRpcSession } from "capnweb";
13
13
  import { detectCodeFormat, parseExpression, parseModule } from "magicast";
@@ -202,7 +202,7 @@ function getElapsedMs(hrStart) {
202
202
  }
203
203
  //#endregion
204
204
  //#region package.json
205
- var version = "5.12.2";
205
+ var version = "5.12.4";
206
206
  //#endregion
207
207
  //#region src/resolveConfig.ts
208
208
  /**
@@ -558,6 +558,44 @@ async function rebuildExtension({ disk, studioOptions, kind, extensions }) {
558
558
  });
559
559
  }))(mergeOptions(disk.options ?? {}, studioOptions));
560
560
  }
561
+ /** The root `output` members Studio may set, with the values each takes. The rest, `path` and `postGenerate` among them, stay with the config file. */
562
+ const OUTPUT_VALUES = {
563
+ barrel: (value) => value === false || isPlainObject(value) && Object.keys(value).length === 1 && (value.type === "named" || value.type === "all"),
564
+ format: (value) => value === false || [
565
+ "auto",
566
+ "prettier",
567
+ "biome",
568
+ "oxfmt"
569
+ ].includes(value),
570
+ lint: (value) => value === false || [
571
+ "auto",
572
+ "eslint",
573
+ "biome",
574
+ "oxlint"
575
+ ].includes(value),
576
+ clean: (value) => typeof value === "boolean",
577
+ defaultBanner: (value) => value === false || value === "simple" || value === "full"
578
+ };
579
+ /** Whether Studio may set the root `output` member `key`. */
580
+ function isOutputKey(key) {
581
+ return Object.hasOwn(OUTPUT_VALUES, key);
582
+ }
583
+ /** Whether Studio may set the root `output` member `key` to `value`. */
584
+ function isOutputValue({ key, value }) {
585
+ return isOutputKey(key) && OUTPUT_VALUES[key](value);
586
+ }
587
+ /** The members of `output` Studio may see and set. */
588
+ function pickOutput(output) {
589
+ return Object.fromEntries(Object.entries(output).filter(([key]) => isOutputKey(key)));
590
+ }
591
+ /** Merges Studio's valid `output` overrides, with `null` unsetting a member. */
592
+ function mergeOutput({ output, studio }) {
593
+ if (!isPlainObject(studio)) return output;
594
+ return mergeOptions(output, Object.fromEntries(Object.entries(studio).filter(([key, value]) => value === null ? isOutputKey(key) : isOutputValue({
595
+ key,
596
+ value
597
+ }))));
598
+ }
561
599
  /**
562
600
  * Merges Studio-provided adapter option overrides into the disk config's adapter, see {@link rebuildExtension}.
563
601
  */
@@ -600,13 +638,7 @@ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
600
638
  * line count, so `enable-plugin` restores exactly those lines and not whatever comments follow.
601
639
  */
602
640
  const DISABLED_MARKER = /^\/\/ kubb:disabled (.+)\s+(\d+)$/;
603
- const OPERATIONS = /* @__PURE__ */ new Set([
604
- "set",
605
- "remove",
606
- "add-plugin",
607
- "disable-plugin",
608
- "enable-plugin"
609
- ]);
641
+ const OPERATIONS = new Set(Object.values(ConfigEditOperation));
610
642
  const KINDS = /* @__PURE__ */ new Set([
611
643
  "plugin",
612
644
  "adapter",
@@ -617,17 +649,20 @@ const KINDS = /* @__PURE__ */ new Set([
617
649
  * The edit crossed the wire, so its declared type is a promise the sender may not have kept.
618
650
  */
619
651
  function invalidEdit(edit) {
620
- const { operation, plugin, config, path, kind } = typeof edit === "object" && edit !== null ? edit : {};
652
+ const { operation, plugin, config, path, kind, input } = typeof edit === "object" && edit !== null ? edit : {};
621
653
  if (typeof operation !== "string" || !OPERATIONS.has(operation)) return `unknown operation ${JSON.stringify(operation)}`;
622
- if (typeof plugin !== "string" || !plugin) return "the edit names no plugin";
623
- if (kind !== void 0 && (typeof kind !== "string" || !KINDS.has(kind))) return `unknown kind ${JSON.stringify(kind)}`;
624
- if (kind !== void 0 && operation !== "set" && operation !== "remove") return `${operation} edits the plugins array, so it takes no kind`;
625
654
  if (config !== void 0 && typeof config !== "string" && !Number.isSafeInteger(config)) return "config must name an entry or give its index";
626
- if (operation === "set" || operation === "remove") {
655
+ if (operation === ConfigEditOperation.SET_INPUT) return typeof input === "string" && input.trim() ? void 0 : "the edit names no input";
656
+ const isOutput = operation === ConfigEditOperation.SET_OUTPUT || operation === ConfigEditOperation.REMOVE_OUTPUT;
657
+ if (!isOutput && (typeof plugin !== "string" || !plugin)) return "the edit names no plugin";
658
+ if (kind !== void 0 && (typeof kind !== "string" || !KINDS.has(kind))) return `unknown kind ${JSON.stringify(kind)}`;
659
+ if (kind !== void 0 && operation !== ConfigEditOperation.SET && operation !== ConfigEditOperation.REMOVE) return `${operation} edits the plugins array, so it takes no kind`;
660
+ if (isOutput || operation === ConfigEditOperation.SET || operation === ConfigEditOperation.REMOVE) {
627
661
  if (!Array.isArray(path) || !path.every((key) => typeof key === "string" && key)) return "path must be a list of option names";
628
662
  if (!path.length) return "no option path given";
629
663
  const unsafe = path.find((key) => !isSafeKey(key));
630
664
  if (unsafe !== void 0) return `${unsafe} cannot be used as an option name`;
665
+ if (isOutput && !isOutputKey(path[0])) return `output.${path[0]} can only be changed in the config file`;
631
666
  }
632
667
  }
633
668
  /** Where a node sits in the source text. */
@@ -928,9 +963,12 @@ function readPartial(node, prefix = []) {
928
963
  }
929
964
  /** The top-level options of a plugin call, each flagged with whether Studio may write it. */
930
965
  function readOptions(call) {
931
- const entries = {};
932
966
  const options = call.arguments[0];
933
- if (options?.type !== "ObjectExpression") return entries;
967
+ return options?.type === "ObjectExpression" ? readObject(options) : {};
968
+ }
969
+ /** The members of an object literal, each flagged with whether Studio may write it. */
970
+ function readObject(options) {
971
+ const entries = {};
934
972
  for (const [index, entry] of options.properties.entries()) {
935
973
  const key = memberKey(entry);
936
974
  if (key === void 0 || !isSafeKey(key)) continue;
@@ -1003,11 +1041,13 @@ function readConfig(source) {
1003
1041
  });
1004
1042
  const [adapter] = factoryCalls(mod, config, "adapter").map(toView);
1005
1043
  const parsers = factoryCalls(mod, config, "parser").map(toView);
1044
+ const output = outputObject(config);
1006
1045
  return {
1007
1046
  name: configName(config),
1008
1047
  plugins,
1009
1048
  ...adapter && { adapter },
1010
- ...parsers.length ? { parsers } : {}
1049
+ ...parsers.length ? { parsers } : {},
1050
+ ...!("reason" in output) && { output: pickOutput(readObject(output.object)) }
1011
1051
  };
1012
1052
  })
1013
1053
  };
@@ -1056,6 +1096,16 @@ function applySet(source, call, path, value, quote) {
1056
1096
  return { source: splice(source, closing, closing, printValue(nest(path, value), quote)) };
1057
1097
  }
1058
1098
  if (options.type !== "ObjectExpression") return { reason: "the factory was not called with an object literal" };
1099
+ return setIn({
1100
+ source,
1101
+ options,
1102
+ path,
1103
+ value,
1104
+ quote
1105
+ });
1106
+ }
1107
+ /** Writes the literal `value` at `path` inside `options`, adding any intermediate object it needs. */
1108
+ function setIn({ source, options, path, value, quote }) {
1059
1109
  const target = walk(options, path);
1060
1110
  if ("reason" in target) return target;
1061
1111
  if ("missing" in target) {
@@ -1064,6 +1114,43 @@ function applySet(source, call, path, value, quote) {
1064
1114
  }
1065
1115
  return { source: splice(source, rangeOf(target.value).start, rangeOf(target.value).end, printValue(value, quote)) };
1066
1116
  }
1117
+ /** Writes the config's top-level `input` as a string literal, replacing what it held. */
1118
+ function applySetInput(source, config, input, quote) {
1119
+ const value = printValue(input, quote);
1120
+ const found = findMember(config, "input");
1121
+ if (!found) return { source: insertItem(source, config, `input: ${value}`) };
1122
+ if (found.member.type !== "ObjectProperty") return { reason: "input is not a plain property" };
1123
+ if (isShadowed(config, found.index)) return { reason: "a spread after input could override it" };
1124
+ return { source: splice(source, rangeOf(found.member.value).start, rangeOf(found.member.value).end, value) };
1125
+ }
1126
+ /** The config's root `output` object literal, or why Studio cannot edit inside it. */
1127
+ function outputObject(config) {
1128
+ const found = findMember(config, "output");
1129
+ if (found?.member.type !== "ObjectProperty" || found.member.value.type !== "ObjectExpression") return { reason: "output is not an object literal" };
1130
+ if (isShadowed(config, found.index)) return { reason: "a spread after output could override it" };
1131
+ return { object: found.member.value };
1132
+ }
1133
+ /** Writes or drops a member of the config's root `output` object. */
1134
+ function applyOutputEdit({ source, config, edit, quote }) {
1135
+ const output = outputObject(config);
1136
+ if ("reason" in output) return output;
1137
+ if (edit.operation === ConfigEditOperation.REMOVE_OUTPUT) return removeIn({
1138
+ source,
1139
+ options: output.object,
1140
+ path: edit.path
1141
+ });
1142
+ if (!isOptionValue(edit.value) || !isOutputValue({
1143
+ key: edit.path[0],
1144
+ value: nest(edit.path.slice(1), edit.value)
1145
+ })) return { reason: `the value is not one output.${edit.path[0]} takes` };
1146
+ return setIn({
1147
+ source,
1148
+ options: output.object,
1149
+ path: edit.path,
1150
+ value: edit.value,
1151
+ quote
1152
+ });
1153
+ }
1067
1154
  /**
1068
1155
  * Deletes the option at `path`, falling the plugin back to its default for it. An option the file
1069
1156
  * does not set already has its default, so removing it changes nothing.
@@ -1072,6 +1159,14 @@ function applyRemove(source, call, path) {
1072
1159
  const options = call.arguments[0];
1073
1160
  if (!options) return { source };
1074
1161
  if (options.type !== "ObjectExpression") return { reason: "the factory was not called with an object literal" };
1162
+ return removeIn({
1163
+ source,
1164
+ options,
1165
+ path
1166
+ });
1167
+ }
1168
+ /** Deletes the member at `path` inside `options`. A path that is not there already holds its default. */
1169
+ function removeIn({ source, options, path }) {
1075
1170
  const target = walk(options, path);
1076
1171
  if ("reason" in target) return target;
1077
1172
  if ("missing" in target) return { source };
@@ -1160,7 +1255,14 @@ function applyEdit({ source, edit, quote, extensions }) {
1160
1255
  const findConfig = (entries) => typeof ref === "string" ? entries.configs.find((entry) => configName(entry) === ref) : entries.configs[ref ?? 0];
1161
1256
  const config = findConfig(parsed);
1162
1257
  if (!config) return { reason: `no config entry found for ${JSON.stringify(ref)}` };
1163
- if (edit.operation === "disable-plugin") {
1258
+ if (edit.operation === ConfigEditOperation.SET_INPUT) return applySetInput(source, config, edit.input, quote);
1259
+ if (edit.operation === ConfigEditOperation.SET_OUTPUT || edit.operation === ConfigEditOperation.REMOVE_OUTPUT) return applyOutputEdit({
1260
+ source,
1261
+ config,
1262
+ edit,
1263
+ quote
1264
+ });
1265
+ if (edit.operation === ConfigEditOperation.DISABLE_PLUGIN) {
1164
1266
  const result = disablePlugin(source, parsed.mod, config, edit.plugin);
1165
1267
  if (!("reason" in result) || !result.reason.includes("shares a line")) return result;
1166
1268
  const expanded = expandPluginsArray(source, config);
@@ -1168,8 +1270,8 @@ function applyEdit({ source, edit, quote, extensions }) {
1168
1270
  const reparsedConfig = reparsed && !("reason" in reparsed) ? findConfig(reparsed) : void 0;
1169
1271
  return expanded !== void 0 && reparsed && !("reason" in reparsed) && reparsedConfig ? disablePlugin(expanded, reparsed.mod, reparsedConfig, edit.plugin) : result;
1170
1272
  }
1171
- if (edit.operation === "enable-plugin") return enablePlugin(source, config, edit.plugin);
1172
- if (edit.operation === "add-plugin") return applyAddPlugin({
1273
+ if (edit.operation === ConfigEditOperation.ENABLE_PLUGIN) return enablePlugin(source, config, edit.plugin);
1274
+ if (edit.operation === ConfigEditOperation.ADD_PLUGIN) return applyAddPlugin({
1173
1275
  source,
1174
1276
  parsed,
1175
1277
  config,
@@ -1185,7 +1287,7 @@ function applyEdit({ source, edit, quote, extensions }) {
1185
1287
  if (kind === "adapter") return { reason: `${edit.plugin} is not the config's adapter` };
1186
1288
  return { reason: kind === "plugin" && disabledBlocks(source, config).some((block) => block.packageName === edit.plugin) ? `${edit.plugin} is disabled, enable it first` : `${edit.plugin} is not in the ${kind}s array` };
1187
1289
  }
1188
- return edit.operation === "set" ? applySet(source, call, edit.path, edit.value, quote) : applyRemove(source, call, edit.path);
1290
+ return edit.operation === ConfigEditOperation.SET ? applySet(source, call, edit.path, edit.value, quote) : applyRemove(source, call, edit.path);
1189
1291
  }
1190
1292
  /**
1191
1293
  * Applies edits to a `kubb.config.ts` in place. Each edit rewrites only the text it targets, so
@@ -2123,6 +2225,7 @@ var Agent = class extends RpcTarget {
2123
2225
  allowConfigEdit: false,
2124
2226
  allowInstall: false,
2125
2227
  allowUpdate: false,
2228
+ allowSaveInput: false,
2126
2229
  allowRead: true,
2127
2230
  allowReadAst: true
2128
2231
  } : permissions;
@@ -2148,13 +2251,13 @@ var Agent = class extends RpcTarget {
2148
2251
  permission
2149
2252
  });
2150
2253
  }
2151
- async #refuse({ message, permission }) {
2254
+ async #refuse({ message, permission, code = Diagnostics.code.unknown }) {
2152
2255
  await this.#warn({
2153
2256
  message,
2154
2257
  permission
2155
2258
  });
2156
2259
  throw new Diagnostics.Error({
2157
- code: Diagnostics.code.unknown,
2260
+ code,
2158
2261
  severity: "error",
2159
2262
  message
2160
2263
  });
@@ -2346,12 +2449,22 @@ var Agent = class extends RpcTarget {
2346
2449
  });
2347
2450
  const activePlugins = plugins ?? config.plugins;
2348
2451
  if (patch?.input && !this.#sandbox && !this.#managed) await emit(this.#options.hooks, "kubb:warn", { message: "Ignored the spec from Studio: generating from a Studio spec is only available to a sandbox or managed agent" });
2349
- const checkout = this.#managed && studioInput !== void 0 && getInputKind(studioInput) === "git" ? await checkoutGitInput(studioInput, signal, { publicOnly: true }).catch((error) => this.#refuse({ message: getErrorMessage(error) })) : void 0;
2452
+ const checkout = this.#managed && studioInput !== void 0 && getInputKind(studioInput) === "git" ? await checkoutGitInput(studioInput, signal, {
2453
+ publicOnly: true,
2454
+ authorization: input.gitAuth?.authorization
2455
+ }).catch((error) => this.#refuse({
2456
+ message: getErrorMessage(error),
2457
+ code: Diagnostics.code.gitInputFailed
2458
+ })) : void 0;
2350
2459
  const run = () => generate({
2351
2460
  config: {
2352
2461
  ...config,
2353
2462
  root,
2354
2463
  input: checkout?.path ?? studioInput ?? config.input,
2464
+ output: mergeOutput({
2465
+ output: config.output,
2466
+ studio: patch?.output
2467
+ }),
2355
2468
  storage: outputRoot ? stagingStorage({
2356
2469
  root,
2357
2470
  directory: outputRoot
@@ -2420,11 +2533,29 @@ var Agent = class extends RpcTarget {
2420
2533
  if (busy) return refuse(busy);
2421
2534
  this.#session.saving = true;
2422
2535
  try {
2423
- const { outcomes, changed } = await writeConfigEdits({
2536
+ const kept = [];
2537
+ for (const edit of edits) kept.push(await this.#keepInput(edit));
2538
+ const written = await writeConfigEdits({
2424
2539
  filePath: configFile,
2425
- edits,
2540
+ edits: kept.flatMap((edit) => "reason" in edit ? [] : [edit]),
2426
2541
  extensions: await this.#installedExtensions()
2427
2542
  });
2543
+ let next = 0;
2544
+ const outcomes = kept.map((edit, index) => "reason" in edit ? {
2545
+ edit: edits[index],
2546
+ applied: false,
2547
+ reason: edit.reason
2548
+ } : {
2549
+ ...written.outcomes[next++],
2550
+ edit: edits[index]
2551
+ });
2552
+ let wroteDocument = false;
2553
+ for (const [index, edit] of kept.entries()) {
2554
+ if (!("document" in edit) || !edit.document || !outcomes[index].applied) continue;
2555
+ await writeFile(path.join(this.#options.root, edit.document.fileName), edit.document.text, "utf-8");
2556
+ wroteDocument = true;
2557
+ }
2558
+ const changed = written.changed || wroteDocument;
2428
2559
  const applied = outcomes.filter((outcome) => outcome.applied).length;
2429
2560
  await this.#logCommand("saveConfig", `applied ${applied}/${outcomes.length} edits to ${configPath}`, input.requestedBy);
2430
2561
  return {
@@ -2439,6 +2570,33 @@ var Agent = class extends RpcTarget {
2439
2570
  }
2440
2571
  }
2441
2572
  /**
2573
+ * Readies a `set-input` edit for the config file: a document is pointed at `openapi.yaml` (`openapi.json`
2574
+ * for JSON) in the project, one file per config entry, for `saveConfig` to write once the edit applies.
2575
+ * A managed agent refuses a file path, as in a generation.
2576
+ */
2577
+ async #keepInput(edit) {
2578
+ if (edit?.operation !== ConfigEditOperation.SET_INPUT || typeof edit.input !== "string" || !edit.input.trim()) return edit;
2579
+ if (!this.#permissions.allowSaveInput) {
2580
+ await this.#warn({
2581
+ message: "Ignored the spec to save: saving the input was not granted",
2582
+ permission: AgentPermission.SAVE_INPUT
2583
+ });
2584
+ return { reason: "the agent was not granted permission to save the input" };
2585
+ }
2586
+ const kind = getInputKind(edit.input);
2587
+ if (this.#managed && !MANAGED_INPUT_KINDS.has(kind)) return { reason: AgentMessage.MANAGED_INPUT_KIND };
2588
+ if (kind !== "inline") return edit;
2589
+ const fileName = `openapi${edit.config === void 0 || edit.config === 0 ? "" : `-${String(edit.config).replace(/[^\w.-]/g, "-")}`}.${isJson(edit.input) ? "json" : "yaml"}`;
2590
+ return {
2591
+ ...edit,
2592
+ input: `./${fileName}`,
2593
+ document: {
2594
+ fileName,
2595
+ text: edit.input
2596
+ }
2597
+ };
2598
+ }
2599
+ /**
2442
2600
  * Installs a registry extension into the project with `kubb add <id> --yes`, which also adds a
2443
2601
  * plugin to `kubb.config.ts`. Studio calls `connect()` again afterwards to see the result.
2444
2602
  */
@@ -2739,6 +2897,15 @@ function pickConfig(configs, ref) {
2739
2897
  if (ref !== void 0 && !Number.isSafeInteger(ref)) return void 0;
2740
2898
  return configs[ref ?? 0];
2741
2899
  }
2900
+ /** Whether `text` parses as JSON, so a pasted document is saved under the matching extension. */
2901
+ function isJson(text) {
2902
+ try {
2903
+ JSON.parse(text);
2904
+ return true;
2905
+ } catch {
2906
+ return false;
2907
+ }
2908
+ }
2742
2909
  /** The input path or URL Studio may show: no inline document, and no credentials from a URL. */
2743
2910
  function reportedInput(input) {
2744
2911
  if (typeof input !== "string") return void 0;
@@ -2757,6 +2924,7 @@ function toEntryView(config, extensions) {
2757
2924
  ...config.name !== void 0 && { name: config.name },
2758
2925
  ...input !== void 0 && { input },
2759
2926
  output: config.output.path,
2927
+ outputOptions: toJSONOptions(pickOutput(config.output)),
2760
2928
  plugins: config.plugins.map((plugin) => ({
2761
2929
  name: toStudioName({
2762
2930
  name: resolveMeta(plugin).name,
@@ -2794,6 +2962,7 @@ async function runAgent({ options, openRpc }) {
2794
2962
  allowConfigEdit: false,
2795
2963
  allowInstall: false,
2796
2964
  allowUpdate: false,
2965
+ allowSaveInput: false,
2797
2966
  allowExec: false,
2798
2967
  allowRead: false,
2799
2968
  allowReadAst: false,