bun-workspaces 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/AGENTS.md +537 -0
  2. package/README.md +4 -2
  3. package/package.json +1 -1
  4. package/src/2392.mjs +10 -1
  5. package/src/5166.mjs +1 -0
  6. package/src/affected/fileAffectedWorkspaces.mjs +7 -1
  7. package/src/affected/gitAffectedFiles.mjs +26 -1
  8. package/src/ai/mcp/bwMcpServer.mjs +5 -1
  9. package/src/ai/mcp/serverState.mjs +10 -1
  10. package/src/ai/mcp/tools.mjs +1 -1
  11. package/src/cli/commands/commandHandlerUtils.mjs +11 -10
  12. package/src/cli/commands/commands.mjs +1 -1
  13. package/src/cli/commands/handleSimpleCommands.mjs +7 -6
  14. package/src/cli/commands/listAffected.mjs +17 -13
  15. package/src/cli/commands/mcp.mjs +11 -1
  16. package/src/cli/commands/runScript/output/renderGroupedOutput.mjs +3 -2
  17. package/src/cli/commands/runScript/output/renderPlainOutput.mjs +4 -1
  18. package/src/cli/commands/runScript/scriptRunFlow.mjs +8 -3
  19. package/src/cli/createCli.mjs +8 -2
  20. package/src/cli/globalOptions/globalOptions.mjs +5 -4
  21. package/src/cli/index.d.ts +35 -2
  22. package/src/config/rootConfig/loadRootConfig.mjs +2 -1
  23. package/src/config/rootConfig/rootConfig.mjs +5 -9
  24. package/src/config/userEnvVars/userEnvVars.mjs +12 -1
  25. package/src/config/util/loadConfig.mjs +23 -2
  26. package/src/config/workspaceConfig/loadWorkspaceConfig.mjs +2 -1
  27. package/src/index.d.ts +11 -0
  28. package/src/internal/core/language/string/index.mjs +1 -0
  29. package/src/internal/core/language/string/sanitizeOutput.mjs +15 -0
  30. package/src/internal/core/runtime/tempFile.mjs +20 -2
  31. package/src/internal/generated/aiDocs/docs.mjs +19 -8
  32. package/src/project/implementations/fileSystemProject/affectedWorkspaces.mjs +2 -0
  33. package/src/project/implementations/fileSystemProject/fileSystemProject.mjs +39 -14
  34. package/src/project/implementations/projectBase.mjs +11 -17
  35. package/src/runScript/scriptExecution.mjs +1 -1
  36. package/src/runScript/workspaceScriptMetadata.mjs +24 -1
  37. package/src/workspaces/applyWorkspacePatternConfigs.mjs +6 -1
  38. package/src/workspaces/dependencyGraph/validateDependencyRules.mjs +14 -7
  39. package/src/workspaces/findWorkspaces.mjs +4 -0
  40. package/src/workspaces/workspacePattern.mjs +134 -46
@@ -3,7 +3,29 @@ import path from "path";
3
3
  import { defineErrors } from "../internal/core/index.mjs";
4
4
  import { createSubprocess } from "../runScript/subprocesses.mjs";
5
5
 
6
- const GIT_AFFECTED_ERRORS = defineErrors("NoGitRepository", "GitCommandFailed");
6
+ const GIT_AFFECTED_ERRORS = defineErrors(
7
+ "NoGitRepository",
8
+ "GitCommandFailed",
9
+ "InvalidGitRef",
10
+ );
11
+ /**
12
+ * Reject ref values that look like CLI options to avoid argument injection
13
+ * (e.g. a ref of `--upload-pack=...` being interpreted as a git option when
14
+ * passed as a positional argument). Git itself forbids refs starting with
15
+ * `-` via `git check-ref-format`, so this rejects only inputs that could
16
+ * never be a real ref.
17
+ */ const assertValidGitRef = (ref, label) => {
18
+ if (typeof ref !== "string" || ref.length === 0) {
19
+ throw new GIT_AFFECTED_ERRORS.InvalidGitRef(
20
+ `${label} must be a non-empty string`,
21
+ );
22
+ }
23
+ if (ref.startsWith("-")) {
24
+ throw new GIT_AFFECTED_ERRORS.InvalidGitRef(
25
+ `${label} cannot start with "-" (got ${JSON.stringify(ref)})`,
26
+ );
27
+ }
28
+ };
7
29
  const GIT_AFFECTED_FILE_REASONS = ["diff", "staged", "unstaged", "untracked"];
8
30
  const runGit = async (args, cwd) => {
9
31
  const proc = createSubprocess(["git", ...args], {
@@ -61,6 +83,7 @@ const resolveGitRoot = async (rootDirectory) => {
61
83
  ref,
62
84
  projectRelativePath,
63
85
  }) => {
86
+ assertValidGitRef(ref, "ref");
64
87
  const gitRoot = fs.realpathSync.native(
65
88
  path.resolve(await resolveGitRoot(rootDirectory)),
66
89
  );
@@ -106,6 +129,8 @@ const getGitAffectedFiles = async (options) => {
106
129
  ignoreUnstaged,
107
130
  ignoreUncommitted,
108
131
  } = options;
132
+ assertValidGitRef(baseRef, "baseRef");
133
+ assertValidGitRef(headRef, "headRef");
109
134
  const gitRoot = fs.realpathSync.native(
110
135
  path.resolve(await resolveGitRoot(rootDirectory)),
111
136
  );
@@ -1,7 +1,10 @@
1
1
  import package_0 from "../../../package.json";
2
2
  import { createMcpServer } from "./core/index.mjs";
3
3
  import { registerBwResources } from "./resources.mjs";
4
- import { setServerWorkingDirectory } from "./serverState.mjs";
4
+ import {
5
+ setServerEnableExecutableConfigs,
6
+ setServerWorkingDirectory,
7
+ } from "./serverState.mjs";
5
8
  import { registerBwTools } from "./tools.mjs";
6
9
 
7
10
  const SERVER_INSTRUCTIONS = `
@@ -32,6 +35,7 @@ $ bw run lint "alias:my-alias-*" "path:packages/**/*" "not:path:my-path/*" # use
32
35
  (end bun-workspaces MCP instructions)
33
36
  `.trim();
34
37
  const startBwMcpServer = async (options) => {
38
+ setServerEnableExecutableConfigs(options.enableExecutableConfigs ?? false);
35
39
  setServerWorkingDirectory(options.initialWorkingDirectory);
36
40
  const server = createMcpServer({
37
41
  name: "bun-workspaces",
@@ -2,19 +2,28 @@ import { createFileSystemProject } from "../../project/implementations/fileSyste
2
2
 
3
3
  const SERVER_STATE = {
4
4
  workingDirectory: null,
5
+ enableExecutableConfigs: false,
5
6
  };
6
7
  const setServerWorkingDirectory = (directory) => {
7
8
  SERVER_STATE.workingDirectory = directory;
8
9
  };
10
+ const setServerEnableExecutableConfigs = (enabled) => {
11
+ SERVER_STATE.enableExecutableConfigs = enabled;
12
+ };
9
13
  const getServerProject = () => {
10
14
  if (!SERVER_STATE.workingDirectory) return null;
11
15
  try {
12
16
  return createFileSystemProject({
13
17
  rootDirectory: SERVER_STATE.workingDirectory,
18
+ disableExecutableConfigs: !SERVER_STATE.enableExecutableConfigs,
14
19
  });
15
20
  } catch {
16
21
  return null;
17
22
  }
18
23
  };
19
24
 
20
- export { getServerProject, setServerWorkingDirectory };
25
+ export {
26
+ getServerProject,
27
+ setServerEnableExecutableConfigs,
28
+ setServerWorkingDirectory,
29
+ };
@@ -202,7 +202,7 @@ const registerBwTools = (server) => {
202
202
  {
203
203
  name: "set_working_directory",
204
204
  description:
205
- "Set the working directory used by this MCP server. All subsequent project queries will reflect the new directory.",
205
+ "Set the working directory used by this MCP server. All subsequent project queries will reflect the new directory. By default the server skips executable config files (bw.root.{ts,js}, bw.workspace.{ts,js}) in the target directory so that pointing the server at an unfamiliar project does not evaluate its code. Only bw.root.{jsonc,json}, bw.workspace.{jsonc,json}, and the package.json bw key are read. Start the server with --no-disable-executable-configs to allow executable configs.",
206
206
  inputSchema: {
207
207
  type: "object",
208
208
  properties: {
@@ -1,4 +1,5 @@
1
1
  import { Option } from "../../internal/bundledDeps/commander.mjs";
2
+ import { sanitizeOutput } from "../../internal/core/index.mjs";
2
3
  import { BunWorkspacesError } from "../../internal/core/error/index.mjs";
3
4
  import { createLogger, logger } from "../../internal/logger/index.mjs";
4
5
  import { getCliCommandConfig } from "../../2392.mjs";
@@ -15,18 +16,18 @@ import { getCliCommandConfig } from "../../2392.mjs";
15
16
  .filter(Boolean)
16
17
  .map((value) => value.replace(/\\\s/g, " "));
17
18
  const createWorkspaceInfoLines = (workspace) => [
18
- `Workspace: ${workspace.name}${workspace.isRoot ? " (root)" : ""}`,
19
- ` - Aliases: ${workspace.aliases.join(", ")}`,
20
- ` - Path: ${workspace.path}`,
21
- ` - Glob Match: ${workspace.matchPattern}`,
22
- ` - Scripts: ${workspace.scripts.join(", ")}`,
23
- ` - Tags: ${workspace.tags.join(", ")}`,
24
- ` - Dependencies: ${workspace.dependencies.join(", ")}`,
25
- ` - Dependents: ${workspace.dependents.join(", ")}`,
19
+ `Workspace: ${sanitizeOutput(workspace.name)}${workspace.isRoot ? " (root)" : ""}`,
20
+ ` - Aliases: ${workspace.aliases.map(sanitizeOutput).join(", ")}`,
21
+ ` - Path: ${sanitizeOutput(workspace.path)}`,
22
+ ` - Glob Match: ${sanitizeOutput(workspace.matchPattern)}`,
23
+ ` - Scripts: ${workspace.scripts.map(sanitizeOutput).join(", ")}`,
24
+ ` - Tags: ${workspace.tags.map(sanitizeOutput).join(", ")}`,
25
+ ` - Dependencies: ${workspace.dependencies.map(sanitizeOutput).join(", ")}`,
26
+ ` - Dependents: ${workspace.dependents.map(sanitizeOutput).join(", ")}`,
26
27
  ];
27
28
  const createScriptInfoLines = (script, workspaces) => [
28
- `Script: ${script}`,
29
- ...workspaces.map((workspace) => ` - ${workspace.name}`),
29
+ `Script: ${sanitizeOutput(script)}`,
30
+ ...workspaces.map((workspace) => ` - ${sanitizeOutput(workspace.name)}`),
30
31
  ];
31
32
  const createJsonLines = (data, options) =>
32
33
  JSON.stringify(data, null, options.pretty ? 2 : undefined).split("\n");
@@ -21,10 +21,10 @@ const defineProjectCommands = (context) => {
21
21
  scriptInfo(context);
22
22
  listTags(context);
23
23
  tagInfo(context);
24
- mcpServer(context);
25
24
  runScript(context);
26
25
  listAffected(context);
27
26
  runAffected(context);
27
+ mcpServer(context);
28
28
  };
29
29
 
30
30
  export { defineGlobalCommands, defineProjectCommands };
@@ -1,4 +1,5 @@
1
1
  import { getDoctorInfo } from "../../doctor/index.mjs";
2
+ import { sanitizeOutput } from "../../internal/core/index.mjs";
2
3
  import { logger } from "../../internal/logger/index.mjs";
3
4
  import {
4
5
  commandOutputLogger,
@@ -65,7 +66,7 @@ const listWorkspaces = handleProjectCommand(
65
66
  } else {
66
67
  workspaces.forEach((workspace) => {
67
68
  if (options.nameOnly) {
68
- lines.push(workspace.name);
69
+ lines.push(sanitizeOutput(workspace.name));
69
70
  } else {
70
71
  lines.push(...createWorkspaceInfoLines(workspace));
71
72
  }
@@ -108,7 +109,7 @@ const listScripts = handleProjectCommand(
108
109
  .sort(({ name: nameA }, { name: nameB }) => nameA.localeCompare(nameB))
109
110
  .forEach(({ name, workspaces }) => {
110
111
  if (options.nameOnly) {
111
- lines.push(name);
112
+ lines.push(sanitizeOutput(name));
112
113
  } else {
113
114
  lines.push(...createScriptInfoLines(name, workspaces));
114
115
  }
@@ -161,7 +162,7 @@ const scriptInfo = handleProjectCommand(
161
162
  options,
162
163
  )
163
164
  : options.workspacesOnly
164
- ? scriptMetadata.workspaces.map(({ name }) => name)
165
+ ? scriptMetadata.workspaces.map(({ name }) => sanitizeOutput(name))
165
166
  : createScriptInfoLines(script, scriptMetadata.workspaces)
166
167
  ).join("\n"),
167
168
  );
@@ -189,10 +190,10 @@ const listTags = handleProjectCommand("listTags", ({ project }, options) => {
189
190
  }
190
191
  tags.forEach(({ tag, workspaces }) => {
191
192
  if (options.nameOnly) {
192
- lines.push(tag);
193
+ lines.push(sanitizeOutput(tag));
193
194
  } else {
194
195
  lines.push(
195
- `Tag: ${tag}\n${workspaces.map((name) => ` - ${name}`).join("\n")}`,
196
+ `Tag: ${sanitizeOutput(tag)}\n${workspaces.map((name) => ` - ${sanitizeOutput(name)}`).join("\n")}`,
196
197
  );
197
198
  }
198
199
  });
@@ -217,7 +218,7 @@ const handleSimpleCommands_tagInfo = handleProjectCommand(
217
218
  commandOutputLogger.info(
218
219
  options.json
219
220
  ? createJsonLines(tagInfo, options).join("\n")
220
- : `Tag: ${tagInfo.name}\n${tagInfo.workspaces.map((name) => ` - ${name}`).join("\n")}`,
221
+ : `Tag: ${sanitizeOutput(tagInfo.name)}\n${tagInfo.workspaces.map((name) => ` - ${sanitizeOutput(name)}`).join("\n")}`,
221
222
  );
222
223
  },
223
224
  );
@@ -1,4 +1,5 @@
1
1
  import path from "path";
2
+ import { sanitizeOutput } from "../../internal/core/index.mjs";
2
3
  import { logger } from "../../internal/logger/index.mjs";
3
4
  import {
4
5
  commandOutputLogger,
@@ -19,34 +20,35 @@ const formatGitHeader = (metadata) => {
19
20
  };
20
21
  const formatDependencyChain = (dependency) => {
21
22
  const segments = dependency.chain.map((entry, index) => {
22
- if (index === 0 || !entry.edgeSource) return entry.workspaceName;
23
- return `\x1b[90m--[${entry.edgeSource}]->\x1b[0m ${entry.workspaceName}`;
23
+ const safeName = sanitizeOutput(entry.workspaceName);
24
+ if (index === 0 || !entry.edgeSource) return safeName;
25
+ return `\x1b[90m--[${entry.edgeSource}]->\x1b[0m ${safeName}`;
24
26
  });
25
27
  return segments.join(" ");
26
28
  };
27
29
  const formatSourceMarker = (change) =>
28
30
  change.source === "devDependencies" ? " (dev)" : "";
29
31
  const formatExternalDepEntryShort = (change) =>
30
- `${change.name}${formatSourceMarker(change)}`;
32
+ `${sanitizeOutput(change.name)}${formatSourceMarker(change)}`;
31
33
  const formatExternalDepEntryDetailed = (change) => {
32
34
  const versions =
33
35
  change.baseVersion === null && change.headVersion === null
34
36
  ? "lockfile changed; precise diff unavailable"
35
- : `${change.baseVersion ?? "(absent)"} -> ${change.headVersion ?? "(absent)"}`;
36
- return `${change.name}${formatSourceMarker(change)} \x1b[90m[${versions}]\x1b[0m`;
37
+ : `${sanitizeOutput(change.baseVersion ?? "(absent)")} -> ${sanitizeOutput(change.headVersion ?? "(absent)")}`;
38
+ return `${sanitizeOutput(change.name)}${formatSourceMarker(change)} \x1b[90m[${versions}]\x1b[0m`;
37
39
  };
38
40
  const createWorkspaceSummaryLines = (result) => {
39
41
  const { workspace, affectedReasons } = result;
40
42
  const lines = [
41
- `\x1b[1mWorkspace: ${workspace.name}\x1b[0m`,
42
- `Path: ${workspace.path}`,
43
+ `\x1b[1mWorkspace: ${sanitizeOutput(workspace.name)}\x1b[0m`,
44
+ `Path: ${sanitizeOutput(workspace.path)}`,
43
45
  ];
44
46
  lines.push(
45
47
  `\x1b[96mChanged input files:\x1b[0m ${affectedReasons.changedFiles.length}`,
46
48
  );
47
49
  if (affectedReasons.dependencies.length) {
48
50
  lines.push(
49
- `\x1b[96mAffected dependencies:\x1b[0m ${affectedReasons.dependencies.map(({ dependencyName }) => dependencyName).join(", ")}`,
51
+ `\x1b[96mAffected dependencies:\x1b[0m ${affectedReasons.dependencies.map(({ dependencyName }) => sanitizeOutput(dependencyName)).join(", ")}`,
50
52
  );
51
53
  } else {
52
54
  lines.push(`\x1b[96mAffected dependencies:\x1b[0m (none)`);
@@ -63,8 +65,8 @@ const createWorkspaceSummaryLines = (result) => {
63
65
  const createWorkspaceDetailedLines = (result) => {
64
66
  const { workspace, affectedReasons } = result;
65
67
  const lines = [
66
- `\x1b[1mWorkspace: ${workspace.name}\x1b[0m`,
67
- `Path: ${workspace.path}`,
68
+ `\x1b[1mWorkspace: ${sanitizeOutput(workspace.name)}\x1b[0m`,
69
+ `Path: ${sanitizeOutput(workspace.path)}`,
68
70
  ];
69
71
  if (affectedReasons.changedFiles.length) {
70
72
  lines.push("\x1b[96mChanged input files:\x1b[0m");
@@ -73,7 +75,7 @@ const createWorkspaceDetailedLines = (result) => {
73
75
  ?.filter((reason) => reason !== "diff")
74
76
  .join(", ");
75
77
  lines.push(
76
- ` - ${path.relative(workspace.path, file.projectFilePath)} \x1b[90m(input: ${JSON.stringify(file.inputMatch)})${reasons ? ` [${reasons}]` : ""}\x1b[0m`,
78
+ ` - ${sanitizeOutput(path.relative(workspace.path, file.projectFilePath))} \x1b[90m(input: ${JSON.stringify(file.inputMatch)})${reasons ? ` [${reasons}]` : ""}\x1b[0m`,
77
79
  );
78
80
  }
79
81
  } else {
@@ -82,7 +84,7 @@ const createWorkspaceDetailedLines = (result) => {
82
84
  if (affectedReasons.dependencies.length) {
83
85
  lines.push("\x1b[96mAffected dependencies:\x1b[0m");
84
86
  for (const dependency of affectedReasons.dependencies) {
85
- lines.push(` - ${dependency.dependencyName}`);
87
+ lines.push(` - ${sanitizeOutput(dependency.dependencyName)}`);
86
88
  lines.push(` chain: ${formatDependencyChain(dependency)}`);
87
89
  }
88
90
  } else {
@@ -153,7 +155,9 @@ const listAffected = handleProjectCommand(
153
155
  if (!options.explain) {
154
156
  if (affectedResults.length) {
155
157
  commandOutputLogger.info(
156
- affectedResults.map(({ workspace }) => workspace.name).join("\n"),
158
+ affectedResults
159
+ .map(({ workspace }) => sanitizeOutput(workspace.name))
160
+ .join("\n"),
157
161
  );
158
162
  } else {
159
163
  logger.info("No affected workspaces");
@@ -1,13 +1,23 @@
1
1
  import { startBwMcpServer } from "../../ai/mcp/index.mjs";
2
+ import { getUserBoolEnvVar } from "../../config/userEnvVars/index.mjs";
2
3
  import { logger } from "../../internal/logger/index.mjs";
3
4
  import { handleGlobalCommand } from "./commandHandlerUtils.mjs";
4
5
 
5
6
  const mcpServer = handleGlobalCommand(
6
7
  "mcpServer",
7
- async ({ workingDirectory }) => {
8
+ async ({ workingDirectory, disableExecutableConfigs }) => {
8
9
  logger.printLevel = "silent";
10
+ // mcp-server is the only command that defaults to *disabled* when
11
+ // neither the global flag nor the env var is set, because the server
12
+ // can be redirected to arbitrary directories at runtime via the
13
+ // set_working_directory tool. Precedence: CLI flag > env var > true.
14
+ const effectiveDisable =
15
+ disableExecutableConfigs ??
16
+ getUserBoolEnvVar("disableExecutableConfigsDefault") ??
17
+ true;
9
18
  await startBwMcpServer({
10
19
  initialWorkingDirectory: workingDirectory,
20
+ enableExecutableConfigs: !effectiveDisable,
11
21
  });
12
22
  },
13
23
  );
@@ -1,4 +1,4 @@
1
- import { runOnExit } from "../../../../internal/core/index.mjs";
1
+ import { runOnExit, sanitizeOutput } from "../../../../internal/core/index.mjs";
2
2
  import {
3
3
  TypedEventTarget,
4
4
  createTypedEventFactory,
@@ -158,7 +158,8 @@ const renderGroupedOutput = async (
158
158
  statusText += ` (signal: ${state.signal})`;
159
159
  }
160
160
  const workspaceLine =
161
- textOps[BORDER_COLOR]("Workspace: ") + textOps.bold(workspace.name);
161
+ textOps[BORDER_COLOR]("Workspace: ") +
162
+ textOps.bold(sanitizeOutput(workspace.name));
162
163
  const statusLine =
163
164
  textOps[BORDER_COLOR](" Status: ") +
164
165
  textOps[STATUS_COLORS[state.status]](statusText);
@@ -1,3 +1,4 @@
1
+ import { sanitizeOutput } from "../../../../internal/core/index.mjs";
1
2
  import { sanitizeChunk } from "./sanitizeChunk.mjs";
2
3
 
3
4
  async function* generatePlainOutputLines(
@@ -6,7 +7,9 @@ async function* generatePlainOutputLines(
6
7
  ) {
7
8
  const workspaceLineBuffers = {};
8
9
  const formatLine = (line, workspaceName) => {
9
- const prefixedLine = prefix ? `[${workspaceName}] ${line}` : line;
10
+ const prefixedLine = prefix
11
+ ? `[${sanitizeOutput(workspaceName)}] ${line}`
12
+ : line;
10
13
  return `\x1b[0m${prefixedLine}`;
11
14
  };
12
15
  for await (const { metadata, chunk } of output.text()) {
@@ -1,6 +1,9 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import { expandHomePath } from "../../../internal/core/index.mjs";
3
+ import {
4
+ expandHomePath,
5
+ sanitizeOutput,
6
+ } from "../../../internal/core/index.mjs";
4
7
  import { logger } from "../../../internal/logger/index.mjs";
5
8
  import {
6
9
  getDefaultOutputStyle,
@@ -130,13 +133,15 @@ const handleScriptRunFlow = async ({
130
133
  exitResults.scriptResults.forEach(
131
134
  ({ success, metadata: { workspace }, exitCode }) => {
132
135
  const isSkipped = exitCode === -1;
136
+ const safeWorkspaceName = sanitizeOutput(workspace.name);
137
+ const safeScriptName = sanitizeOutput(scriptName ?? "");
133
138
  if (isSkipped) {
134
139
  logger.info(
135
- `➖ ${workspace.name}: ${scriptName} (skipped due to dependency failure)`,
140
+ `➖ ${safeWorkspaceName}: ${safeScriptName} (skipped due to dependency failure)`,
136
141
  );
137
142
  } else {
138
143
  logger.info(
139
- `${success ? "✅" : "❌"} ${workspace.name}: ${scriptName}${exitCode ? ` (exited with code ${exitCode})` : ""}`,
144
+ `${success ? "✅" : "❌"} ${safeWorkspaceName}: ${safeScriptName}${exitCode ? ` (exited with code ${exitCode})` : ""}`,
140
145
  );
141
146
  }
142
147
  },
@@ -82,8 +82,12 @@ const createCli = ({ defaultCwd = process.cwd(), defaultMiddleware } = {}) => {
82
82
  process.exit(1);
83
83
  return;
84
84
  }
85
- const { project, projectError, workingDirectory } =
86
- initializeWithGlobalOptions(program, args, middleware);
85
+ const {
86
+ project,
87
+ projectError,
88
+ workingDirectory,
89
+ disableExecutableConfigs,
90
+ } = initializeWithGlobalOptions(program, args, middleware);
87
91
  middleware.findProject({
88
92
  ...defaultContext,
89
93
  project,
@@ -103,6 +107,7 @@ const createCli = ({ defaultCwd = process.cwd(), defaultMiddleware } = {}) => {
103
107
  terminalWidth,
104
108
  terminalHeight,
105
109
  workingDirectory,
110
+ disableExecutableConfigs,
106
111
  });
107
112
  defineGlobalCommands({
108
113
  program,
@@ -112,6 +117,7 @@ const createCli = ({ defaultCwd = process.cwd(), defaultMiddleware } = {}) => {
112
117
  terminalWidth,
113
118
  terminalHeight,
114
119
  workingDirectory,
120
+ disableExecutableConfigs,
115
121
  });
116
122
  logger.debug(`Commands initialized. Parsing args...`);
117
123
  middleware.preParse({
@@ -18,10 +18,8 @@ const ERRORS = defineErrors(
18
18
  const addGlobalOption = (program, optionName, defaultOverride) => {
19
19
  const { mainOption, shortOption, description, param, values, defaultValue } =
20
20
  getCliGlobalOptionConfig(optionName);
21
- let option = new Option(
22
- `${shortOption} ${mainOption}${param ? ` <${param}>` : ""}`,
23
- description,
24
- );
21
+ const flagsString = `${shortOption ? `${shortOption} ` : ""}${mainOption}${param ? ` <${param}>` : ""}`;
22
+ let option = new Option(flagsString, description);
25
23
  const effectiveDefaultValue = defaultOverride ?? defaultValue;
26
24
  if (effectiveDefaultValue) {
27
25
  option = option.default(effectiveDefaultValue);
@@ -106,6 +104,7 @@ const defineGlobalOptions = (program, args, middleware) => {
106
104
  }
107
105
  addGlobalOption(program, "logLevel");
108
106
  addGlobalOption(program, "includeRoot");
107
+ addGlobalOption(program, "disableExecutableConfigs");
109
108
  return {
110
109
  cwd,
111
110
  };
@@ -119,6 +118,7 @@ const applyGlobalOptions = (options) => {
119
118
  project = createFileSystemProject({
120
119
  rootDirectory: options.cwd,
121
120
  includeRootWorkspace: options.includeRoot,
121
+ disableExecutableConfigs: options.disableExecutableConfigs,
122
122
  });
123
123
  logger.debug(
124
124
  `Project: ${JSON.stringify(project.name)} (${project.workspaces.length} workspace${project.workspaces.length === 1 ? "" : "s"})`,
@@ -134,6 +134,7 @@ const applyGlobalOptions = (options) => {
134
134
  project,
135
135
  projectError: error,
136
136
  workingDirectory: options.cwd,
137
+ disableExecutableConfigs: options.disableExecutableConfigs,
137
138
  };
138
139
  };
139
140
  const initializeWithGlobalOptions = (program, args, middleware) => {
@@ -553,7 +553,7 @@ export declare const CLI_COMMANDS_CONFIG: {
553
553
  readonly command: "mcp-server";
554
554
  readonly isGlobal: true;
555
555
  readonly aliases: [];
556
- readonly description: "Start the bun-workspaces MCP (Model Context Protocol) server over stdio";
556
+ readonly description: "Start the bun-workspaces MCP (Model Context Protocol) server over stdio. Defaults to skipping executable config files (bw.root.{ts,js}, bw.workspace.{ts,js}) for agent security. Pass the global --no-disable-executable-configs flag to allow them.";
557
557
  readonly options: {};
558
558
  };
559
559
  readonly listAffected: {
@@ -916,7 +916,7 @@ export declare const getCliCommandConfig: (commandName: CliCommandName) =>
916
916
  readonly command: "mcp-server";
917
917
  readonly isGlobal: true;
918
918
  readonly aliases: [];
919
- readonly description: "Start the bun-workspaces MCP (Model Context Protocol) server over stdio";
919
+ readonly description: "Start the bun-workspaces MCP (Model Context Protocol) server over stdio. Defaults to skipping executable config files (bw.root.{ts,js}, bw.workspace.{ts,js}) for agent security. Pass the global --no-disable-executable-configs flag to allow them.";
920
920
  readonly options: {};
921
921
  }
922
922
  | {
@@ -1150,6 +1150,7 @@ export interface CliGlobalOptions {
1150
1150
  cwd: string;
1151
1151
  includeRoot: boolean;
1152
1152
  workspaceRoot: boolean;
1153
+ disableExecutableConfigs: boolean;
1153
1154
  }
1154
1155
  export interface CliGlobalOptionConfig {
1155
1156
  mainOption: string;
@@ -1194,6 +1195,19 @@ export declare const getCliGlobalOptionConfig: (
1194
1195
  readonly defaultValue: "";
1195
1196
  readonly values: null;
1196
1197
  readonly param: "";
1198
+ }
1199
+ | {
1200
+ readonly mainOption: "--disable-executable-configs";
1201
+ readonly shortOption: "";
1202
+ readonly description:
1203
+ | "Skip evaluating executable config files (bw.root.{ts,js}, bw.workspace.{ts,js}). Only jsonc/json/package.json configs are read, for untrusted contexts. Can also be set via env var BW_PARALLEL_MAX_DEFAULT=true."
1204
+ | "Skip evaluating executable config files (bw.root.{ts,js}, bw.workspace.{ts,js}). Only jsonc/json/package.json configs are read, for untrusted contexts. Can also be set via env var BW_SHELL_DEFAULT=true."
1205
+ | "Skip evaluating executable config files (bw.root.{ts,js}, bw.workspace.{ts,js}). Only jsonc/json/package.json configs are read, for untrusted contexts. Can also be set via env var BW_INCLUDE_ROOT_WORKSPACE_DEFAULT=true."
1206
+ | "Skip evaluating executable config files (bw.root.{ts,js}, bw.workspace.{ts,js}). Only jsonc/json/package.json configs are read, for untrusted contexts. Can also be set via env var BW_AFFECTED_BASE_REF_DEFAULT=true."
1207
+ | "Skip evaluating executable config files (bw.root.{ts,js}, bw.workspace.{ts,js}). Only jsonc/json/package.json configs are read, for untrusted contexts. Can also be set via env var BW_DISABLE_EXECUTABLE_CONFIGS_DEFAULT=true.";
1208
+ readonly defaultValue: "";
1209
+ readonly values: null;
1210
+ readonly param: "";
1197
1211
  };
1198
1212
  export declare const getCliGlobalOptionNames: () => CliGlobalOptionName[];
1199
1213
  /**
@@ -1375,6 +1389,17 @@ export type CreateFileSystemProjectOptions = {
1375
1389
  name?: string;
1376
1390
  /** Whether to include the root workspace as a normal workspace. This overrides any config or env var settings. */
1377
1391
  includeRootWorkspace?: boolean;
1392
+ /**
1393
+ * When true, skip discovery of `.ts`/`.js` config files (`bw.root.{ts,js}`,
1394
+ * `bw.workspace.{ts,js}`) so no executable code is loaded from the project,
1395
+ * for untrusted contexts.
1396
+ *
1397
+ * `.jsonc`/`.json` configs and the `package.json` `bw` key still resolve.
1398
+ *
1399
+ * When omitted, the `BW_DISABLE_EXECUTABLE_CONFIGS_DEFAULT` user env var is
1400
+ * consulted (`"true"` or `"false"`). If neither is set, defaults to false.
1401
+ */
1402
+ disableExecutableConfigs?: boolean;
1378
1403
  };
1379
1404
  export type InlineScriptOptions = {
1380
1405
  /** A name to act as a label for the inline script */
@@ -1525,6 +1550,13 @@ export type GlobalCommandContext = {
1525
1550
  terminalWidth: number;
1526
1551
  terminalHeight: number;
1527
1552
  workingDirectory: string;
1553
+ /**
1554
+ * Value of the global `--disable-executable-configs` flag.
1555
+ * `undefined` means the flag was not passed; commands are free to
1556
+ * apply their own default (notably the mcp-server command defaults
1557
+ * to disabled for security).
1558
+ */
1559
+ disableExecutableConfigs: boolean | undefined;
1528
1560
  };
1529
1561
  export type ProjectCommandContext = GlobalCommandContext & {
1530
1562
  project: FileSystemProject;
@@ -1697,6 +1729,7 @@ export declare const initializeWithGlobalOptions: (
1697
1729
  }>;
1698
1730
  projectError: Error | null;
1699
1731
  workingDirectory: string;
1732
+ disableExecutableConfigs: boolean;
1700
1733
  };
1701
1734
 
1702
1735
  export {};
@@ -5,13 +5,14 @@ import {
5
5
  ROOT_CONFIG_PACKAGE_JSON_KEY,
6
6
  } from "../../8529.mjs";
7
7
 
8
- const loadRootConfig = (rootDirectory) => {
8
+ const loadRootConfig = (rootDirectory, loadOptions = {}) => {
9
9
  const config = loadConfig(
10
10
  "root",
11
11
  rootDirectory,
12
12
  ROOT_CONFIG_FILE_NAME,
13
13
  ROOT_CONFIG_PACKAGE_JSON_KEY,
14
14
  (content) => resolveRootConfig(content),
15
+ loadOptions,
15
16
  );
16
17
  return config ?? createDefaultRootConfig();
17
18
  };
@@ -1,5 +1,5 @@
1
1
  import { resolveDefaultAffectedBaseRef } from "../../affected/affectedBaseRef.mjs";
2
- import validateRootConfig from "../../internal/generated/ajv/validateRootConfig.mjs";
2
+ import { validate } from "../../internal/generated/ajv/validateRootConfig.mjs";
3
3
  import {
4
4
  determineParallelMax,
5
5
  resolveScriptShell,
@@ -9,16 +9,16 @@ import { executeValidator } from "../util/validateConfig.mjs";
9
9
  import { validateWorkspaceConfig } from "../workspaceConfig/workspaceConfig.mjs";
10
10
  import { ROOT_CONFIG_ERRORS } from "./errors.mjs";
11
11
 
12
- const rootConfig_validateRootConfig = (config) =>
12
+ const validateRootConfig = (config) =>
13
13
  executeValidator(
14
- validateRootConfig,
14
+ validate,
15
15
  "RootConfig",
16
16
  config,
17
17
  ROOT_CONFIG_ERRORS.InvalidRootConfig,
18
18
  );
19
19
  const createDefaultRootConfig = () => resolveRootConfig({});
20
20
  const resolveRootConfig = (config) => {
21
- rootConfig_validateRootConfig(config);
21
+ validateRootConfig(config);
22
22
  for (const entry of config.workspacePatternConfigs ?? []) {
23
23
  if (typeof entry.config !== "function") {
24
24
  validateWorkspaceConfig(entry.config);
@@ -42,8 +42,4 @@ const resolveRootConfig = (config) => {
42
42
  };
43
43
  };
44
44
 
45
- export {
46
- createDefaultRootConfig,
47
- resolveRootConfig,
48
- rootConfig_validateRootConfig as validateRootConfig,
49
- };
45
+ export { createDefaultRootConfig, resolveRootConfig, validateRootConfig };
@@ -1,5 +1,16 @@
1
1
  import { USER_ENV_VARS } from "../../5166.mjs";
2
2
 
3
3
  const getUserEnvVar = (key) => process.env[USER_ENV_VARS[key]];
4
+ /**
5
+ * Parse a user env var into a strict boolean tri-state. Returns `true` for
6
+ * "true", `false` for "false", and `undefined` for any other value (including
7
+ * unset) — so callers can distinguish "user explicitly set false" from
8
+ * "user did not set anything."
9
+ */ const getUserBoolEnvVar = (key) => {
10
+ const value = getUserEnvVar(key);
11
+ if (value === "true") return true;
12
+ if (value === "false") return false;
13
+ return undefined;
14
+ };
4
15
 
5
- export { getUserEnvVar };
16
+ export { getUserBoolEnvVar, getUserEnvVar };
@@ -111,9 +111,23 @@ const LOCATION_FINDERS = {
111
111
  return null;
112
112
  },
113
113
  };
114
- const getConfigLocation = (name, directory, fileName, packageJsonKey) => {
114
+ const isExecutableLocationType = (locationType) =>
115
+ locationType === "tsFile" || locationType === "jsFile";
116
+ const getConfigLocation = (
117
+ name,
118
+ directory,
119
+ fileName,
120
+ packageJsonKey,
121
+ loadOptions = {},
122
+ ) => {
115
123
  const locations = [];
116
124
  for (const locationType of CONFIG_LOCATION_TYPES) {
125
+ if (
126
+ loadOptions.disableExecutableConfigs &&
127
+ isExecutableLocationType(locationType)
128
+ ) {
129
+ continue;
130
+ }
117
131
  const location = LOCATION_FINDERS[locationType](
118
132
  directory,
119
133
  fileName,
@@ -136,8 +150,15 @@ const loadConfig = (
136
150
  fileName,
137
151
  packageJsonKey,
138
152
  processContent,
153
+ loadOptions = {},
139
154
  ) => {
140
- const location = getConfigLocation(name, directory, fileName, packageJsonKey);
155
+ const location = getConfigLocation(
156
+ name,
157
+ directory,
158
+ fileName,
159
+ packageJsonKey,
160
+ loadOptions,
161
+ );
141
162
  if (!location) {
142
163
  return null;
143
164
  }