bun-workspaces 1.10.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.
- package/README.md +3 -1
- package/package.json +1 -1
- package/src/2392.mjs +10 -1
- package/src/5166.mjs +1 -0
- package/src/affected/gitAffectedFiles.mjs +26 -1
- package/src/ai/mcp/bwMcpServer.mjs +5 -1
- package/src/ai/mcp/serverState.mjs +10 -1
- package/src/ai/mcp/tools.mjs +1 -1
- package/src/cli/commands/commandHandlerUtils.mjs +11 -10
- package/src/cli/commands/commands.mjs +1 -1
- package/src/cli/commands/handleSimpleCommands.mjs +7 -6
- package/src/cli/commands/listAffected.mjs +17 -13
- package/src/cli/commands/mcp.mjs +11 -1
- package/src/cli/commands/runScript/output/renderGroupedOutput.mjs +3 -2
- package/src/cli/commands/runScript/output/renderPlainOutput.mjs +4 -1
- package/src/cli/commands/runScript/scriptRunFlow.mjs +8 -3
- package/src/cli/createCli.mjs +8 -2
- package/src/cli/globalOptions/globalOptions.mjs +5 -4
- package/src/cli/index.d.ts +35 -2
- package/src/config/rootConfig/loadRootConfig.mjs +2 -1
- package/src/config/userEnvVars/userEnvVars.mjs +12 -1
- package/src/config/util/loadConfig.mjs +23 -2
- package/src/config/workspaceConfig/loadWorkspaceConfig.mjs +2 -1
- package/src/index.d.ts +11 -0
- package/src/internal/core/language/string/index.mjs +1 -0
- package/src/internal/core/language/string/sanitizeOutput.mjs +15 -0
- package/src/internal/core/runtime/tempFile.mjs +20 -2
- package/src/project/implementations/fileSystemProject/fileSystemProject.mjs +39 -14
- package/src/runScript/scriptExecution.mjs +1 -1
- package/src/runScript/workspaceScriptMetadata.mjs +24 -1
- package/src/workspaces/findWorkspaces.mjs +2 -0
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<a href="https://bunworkspaces.com">
|
|
2
|
-
<img src="./workspaces/web/documentation-website/src/pages/public/images/png/bwunster-bg-
|
|
2
|
+
<img src="./workspaces/web/documentation-website/src/pages/public/images/png/bwunster-bg-rect-title-wide_3000x900.png" alt="bun-workspaces" width="100%" />
|
|
3
3
|
</a>
|
|
4
4
|
|
|
5
5
|
<br/>
|
|
@@ -8,6 +8,8 @@ Full Documentation: [https://bunworkspaces.com](https://bunworkspaces.com)
|
|
|
8
8
|
|
|
9
9
|
Changelog: [GitHub Releases](https://github.com/bun-workspaces/bun-workspaces/releases)
|
|
10
10
|
|
|
11
|
+
Example Projects: [Repository](https://github.com/bun-workspaces/bun-workspaces-examples)
|
|
12
|
+
|
|
11
13
|
# bun-workspaces
|
|
12
14
|
|
|
13
15
|
A [monorepo](http://sonarsource.com/resources/library/monorepo/) tool that enhances native [Bun workspaces](https://bun.sh/docs/install/workspaces).
|
package/package.json
CHANGED
package/src/2392.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { OUTPUT_STYLE_VALUES, SCRIPT_SHELL_OPTIONS } from "./4427.mjs";
|
|
2
2
|
import { LOG_LEVELS } from "./8126.mjs";
|
|
3
|
+
import { getUserEnvVarName } from "./5166.mjs";
|
|
3
4
|
|
|
4
5
|
const JSON_FLAGS = ["-j", "--json"];
|
|
5
6
|
const PRETTY_FLAGS = ["-p", "--pretty"];
|
|
@@ -142,7 +143,7 @@ const CLI_COMMANDS_CONFIG = {
|
|
|
142
143
|
isGlobal: true,
|
|
143
144
|
aliases: [],
|
|
144
145
|
description:
|
|
145
|
-
"Start the bun-workspaces MCP (Model Context Protocol) server over stdio",
|
|
146
|
+
"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.",
|
|
146
147
|
options: {},
|
|
147
148
|
},
|
|
148
149
|
listAffected: {
|
|
@@ -431,6 +432,14 @@ const CLI_GLOBAL_OPTIONS_CONFIG = {
|
|
|
431
432
|
values: null,
|
|
432
433
|
param: "",
|
|
433
434
|
},
|
|
435
|
+
disableExecutableConfigs: {
|
|
436
|
+
mainOption: "--disable-executable-configs",
|
|
437
|
+
shortOption: "",
|
|
438
|
+
description: `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 ${getUserEnvVarName("disableExecutableConfigsDefault")}=true.`,
|
|
439
|
+
defaultValue: "",
|
|
440
|
+
values: null,
|
|
441
|
+
param: "",
|
|
442
|
+
},
|
|
434
443
|
};
|
|
435
444
|
const getCliGlobalOptionConfig = (optionName) =>
|
|
436
445
|
CLI_GLOBAL_OPTIONS_CONFIG[optionName];
|
package/src/5166.mjs
CHANGED
|
@@ -3,6 +3,7 @@ const USER_ENV_VARS = {
|
|
|
3
3
|
scriptShellDefault: "BW_SHELL_DEFAULT",
|
|
4
4
|
includeRootWorkspaceDefault: "BW_INCLUDE_ROOT_WORKSPACE_DEFAULT",
|
|
5
5
|
affectedBaseRefDefault: "BW_AFFECTED_BASE_REF_DEFAULT",
|
|
6
|
+
disableExecutableConfigsDefault: "BW_DISABLE_EXECUTABLE_CONFIGS_DEFAULT",
|
|
6
7
|
};
|
|
7
8
|
const getUserEnvVarName = (key) => USER_ENV_VARS[key];
|
|
8
9
|
|
|
@@ -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(
|
|
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 {
|
|
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 {
|
|
25
|
+
export {
|
|
26
|
+
getServerProject,
|
|
27
|
+
setServerEnableExecutableConfigs,
|
|
28
|
+
setServerWorkingDirectory,
|
|
29
|
+
};
|
package/src/ai/mcp/tools.mjs
CHANGED
|
@@ -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
|
-
|
|
23
|
-
|
|
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
|
|
158
|
+
affectedResults
|
|
159
|
+
.map(({ workspace }) => sanitizeOutput(workspace.name))
|
|
160
|
+
.join("\n"),
|
|
157
161
|
);
|
|
158
162
|
} else {
|
|
159
163
|
logger.info("No affected workspaces");
|
package/src/cli/commands/mcp.mjs
CHANGED
|
@@ -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: ") +
|
|
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
|
|
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 {
|
|
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
|
-
`➖ ${
|
|
140
|
+
`➖ ${safeWorkspaceName}: ${safeScriptName} (skipped due to dependency failure)`,
|
|
136
141
|
);
|
|
137
142
|
} else {
|
|
138
143
|
logger.info(
|
|
139
|
-
`${success ? "✅" : "❌"} ${
|
|
144
|
+
`${success ? "✅" : "❌"} ${safeWorkspaceName}: ${safeScriptName}${exitCode ? ` (exited with code ${exitCode})` : ""}`,
|
|
140
145
|
);
|
|
141
146
|
}
|
|
142
147
|
},
|
package/src/cli/createCli.mjs
CHANGED
|
@@ -82,8 +82,12 @@ const createCli = ({ defaultCwd = process.cwd(), defaultMiddleware } = {}) => {
|
|
|
82
82
|
process.exit(1);
|
|
83
83
|
return;
|
|
84
84
|
}
|
|
85
|
-
const {
|
|
86
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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) => {
|
package/src/cli/index.d.ts
CHANGED
|
@@ -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,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
|
|
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(
|
|
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
|
}
|
|
@@ -5,13 +5,14 @@ import {
|
|
|
5
5
|
} from "./workspaceConfig.mjs";
|
|
6
6
|
import { WORKSPACE_CONFIG_FILE_NAME } from "../../8529.mjs";
|
|
7
7
|
|
|
8
|
-
const loadWorkspaceConfig = (workspacePath) => {
|
|
8
|
+
const loadWorkspaceConfig = (workspacePath, loadOptions = {}) => {
|
|
9
9
|
const config = loadConfig(
|
|
10
10
|
"workspace",
|
|
11
11
|
workspacePath,
|
|
12
12
|
WORKSPACE_CONFIG_FILE_NAME,
|
|
13
13
|
/* inlined export .WORKSPACE_CONFIG_PACKAGE_JSON_KEY */ "bw",
|
|
14
14
|
(content) => resolveWorkspaceConfig(content),
|
|
15
|
+
loadOptions,
|
|
15
16
|
);
|
|
16
17
|
return config ?? createDefaultWorkspaceConfig();
|
|
17
18
|
};
|
package/src/index.d.ts
CHANGED
|
@@ -600,6 +600,17 @@ export type CreateFileSystemProjectOptions = {
|
|
|
600
600
|
name?: string;
|
|
601
601
|
/** Whether to include the root workspace as a normal workspace. This overrides any config or env var settings. */
|
|
602
602
|
includeRootWorkspace?: boolean;
|
|
603
|
+
/**
|
|
604
|
+
* When true, skip discovery of `.ts`/`.js` config files (`bw.root.{ts,js}`,
|
|
605
|
+
* `bw.workspace.{ts,js}`) so no executable code is loaded from the project,
|
|
606
|
+
* for untrusted contexts.
|
|
607
|
+
*
|
|
608
|
+
* `.jsonc`/`.json` configs and the `package.json` `bw` key still resolve.
|
|
609
|
+
*
|
|
610
|
+
* When omitted, the `BW_DISABLE_EXECUTABLE_CONFIGS_DEFAULT` user env var is
|
|
611
|
+
* consulted (`"true"` or `"false"`). If neither is set, defaults to false.
|
|
612
|
+
*/
|
|
613
|
+
disableExecutableConfigs?: boolean;
|
|
603
614
|
};
|
|
604
615
|
export type InlineScriptOptions = {
|
|
605
616
|
/** A name to act as a label for the inline script */
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip ANSI escape sequences and other terminal-disruptive control
|
|
3
|
+
* characters from a string before rendering it. Use this for values
|
|
4
|
+
* sourced from package.json, config files, or other untrusted inputs to
|
|
5
|
+
* prevent terminal-escape injection in bw's CLI output (e.g., a workspace
|
|
6
|
+
* name containing `\x1b[2J` clearing the user's screen during `bw info`).
|
|
7
|
+
*
|
|
8
|
+
* Preserves `\n` and `\t`. Strips ANSI sequences plus C0/C1 controls
|
|
9
|
+
* other than newline and tab.
|
|
10
|
+
*/ // eslint-disable-next-line no-control-regex
|
|
11
|
+
const DISRUPTIVE_CONTROLS_REGEX = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g;
|
|
12
|
+
const sanitizeOutput = (value) =>
|
|
13
|
+
Bun.stripANSI(value).replace(DISRUPTIVE_CONTROLS_REGEX, "");
|
|
14
|
+
|
|
15
|
+
export { sanitizeOutput };
|
|
@@ -7,7 +7,21 @@ import { BUN_WORKSPACES_VERSION } from "../../version.mjs";
|
|
|
7
7
|
import { createShortId } from "../language/string/id.mjs";
|
|
8
8
|
import { runOnExit } from "./onExit.mjs";
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Per-user suffix on the temp base dir prevents a different local user from
|
|
12
|
+
* pre-creating or symlink-squatting `/tmp/bun-workspaces` to steer our writes.
|
|
13
|
+
* On platforms without a numeric uid (Windows), `os.tmpdir()` is already
|
|
14
|
+
* per-user so the suffix becomes inert.
|
|
15
|
+
*/ const getUserSuffix = () => {
|
|
16
|
+
try {
|
|
17
|
+
const { uid } = os.userInfo();
|
|
18
|
+
return uid >= 0 ? `-${uid}` : "";
|
|
19
|
+
} catch {
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const getTempBasePackageDir = () =>
|
|
24
|
+
path.join(os.tmpdir(), `bun-workspaces${getUserSuffix()}`);
|
|
11
25
|
const getTempParentDir = () =>
|
|
12
26
|
path.join(getTempBasePackageDir(), BUN_WORKSPACES_VERSION);
|
|
13
27
|
class TempDir {
|
|
@@ -18,10 +32,14 @@ class TempDir {
|
|
|
18
32
|
}
|
|
19
33
|
initialize(clean = false) {
|
|
20
34
|
if (fs.existsSync(this.dir)) return;
|
|
35
|
+
// Pass mode at creation time so the dir is never briefly readable by
|
|
36
|
+
// other local users between mkdir and a subsequent chmod (closes the
|
|
37
|
+
// TOCTOU window where another process could enter the dir before the
|
|
38
|
+
// mode is tightened).
|
|
21
39
|
fs.mkdirSync(this.dir, {
|
|
22
40
|
recursive: true,
|
|
41
|
+
mode: 448,
|
|
23
42
|
});
|
|
24
|
-
fs.chmodSync(this.dir, 448);
|
|
25
43
|
if (clean) {
|
|
26
44
|
for (const dir of fs.readdirSync(path.resolve(getTempBasePackageDir()))) {
|
|
27
45
|
if (dir !== BUN_WORKSPACES_VERSION) {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { loadRootConfig } from "../../../config/index.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getUserBoolEnvVar,
|
|
6
|
+
getUserEnvVar,
|
|
7
|
+
} from "../../../config/userEnvVars/index.mjs";
|
|
5
8
|
import { parse, quote } from "../../../internal/bundledDeps/shellQuote.mjs";
|
|
6
9
|
import {
|
|
7
10
|
DEFAULT_TEMP_DIR,
|
|
@@ -70,17 +73,22 @@ const serializeArgs = (args, metadata, shell) => {
|
|
|
70
73
|
return args
|
|
71
74
|
.map((arg) =>
|
|
72
75
|
quoteArg(
|
|
73
|
-
interpolateWorkspaceScriptMetadata(
|
|
76
|
+
interpolateWorkspaceScriptMetadata({
|
|
77
|
+
text: arg,
|
|
78
|
+
metadata,
|
|
79
|
+
shell,
|
|
80
|
+
}),
|
|
74
81
|
shell,
|
|
75
82
|
),
|
|
76
83
|
)
|
|
77
84
|
.join(" ");
|
|
78
85
|
}
|
|
79
|
-
const interpolated = interpolateWorkspaceScriptMetadata(
|
|
80
|
-
args,
|
|
86
|
+
const interpolated = interpolateWorkspaceScriptMetadata({
|
|
87
|
+
text: args,
|
|
81
88
|
metadata,
|
|
82
89
|
shell,
|
|
83
|
-
|
|
90
|
+
quoteValues: true,
|
|
91
|
+
});
|
|
84
92
|
// Escape backslashes in interpolated values before POSIX parse on Windows,
|
|
85
93
|
// so that path separators survive parse's escape processing (\\→\)
|
|
86
94
|
const parseInput =
|
|
@@ -128,6 +136,11 @@ class _FileSystemProject extends ProjectBase {
|
|
|
128
136
|
typeofName: "boolean",
|
|
129
137
|
optional: true,
|
|
130
138
|
},
|
|
139
|
+
"disableExecutableConfigs option": {
|
|
140
|
+
value: options.disableExecutableConfigs,
|
|
141
|
+
typeofName: "boolean",
|
|
142
|
+
optional: true,
|
|
143
|
+
},
|
|
131
144
|
},
|
|
132
145
|
{
|
|
133
146
|
throw: true,
|
|
@@ -141,7 +154,16 @@ class _FileSystemProject extends ProjectBase {
|
|
|
141
154
|
process.cwd(),
|
|
142
155
|
expandHomePath(options.rootDirectory ?? ""),
|
|
143
156
|
);
|
|
144
|
-
|
|
157
|
+
// Root config can't supply a default for this — the config file itself
|
|
158
|
+
// is what we're deciding whether to evaluate. Precedence is therefore
|
|
159
|
+
// option > BW_DISABLE_EXECUTABLE_CONFIGS_DEFAULT env var > false.
|
|
160
|
+
const loadConfigOptions = {
|
|
161
|
+
disableExecutableConfigs:
|
|
162
|
+
options.disableExecutableConfigs ??
|
|
163
|
+
getUserBoolEnvVar("disableExecutableConfigsDefault") ??
|
|
164
|
+
false,
|
|
165
|
+
};
|
|
166
|
+
const rootConfig = loadRootConfig(this.rootDirectory, loadConfigOptions);
|
|
145
167
|
const { workspaces, workspaceMap, rootWorkspace } = findWorkspaces({
|
|
146
168
|
rootDirectory: this.rootDirectory,
|
|
147
169
|
includeRootWorkspace:
|
|
@@ -149,6 +171,7 @@ class _FileSystemProject extends ProjectBase {
|
|
|
149
171
|
rootConfig.defaults.includeRootWorkspace ??
|
|
150
172
|
getUserEnvVar("includeRootWorkspaceDefault") === "true",
|
|
151
173
|
workspacePatternConfigs: rootConfig.workspacePatternConfigs,
|
|
174
|
+
loadConfigOptions,
|
|
152
175
|
});
|
|
153
176
|
this.rootWorkspace = rootWorkspace;
|
|
154
177
|
this.workspaces = workspaces;
|
|
@@ -262,11 +285,12 @@ class _FileSystemProject extends ProjectBase {
|
|
|
262
285
|
};
|
|
263
286
|
const args = serializeArgs(options.args, workspaceScriptMetadata, shell);
|
|
264
287
|
const script = options.inline
|
|
265
|
-
? interpolateWorkspaceScriptMetadata(
|
|
266
|
-
options.script,
|
|
267
|
-
workspaceScriptMetadata,
|
|
288
|
+
? interpolateWorkspaceScriptMetadata({
|
|
289
|
+
text: options.script,
|
|
290
|
+
metadata: workspaceScriptMetadata,
|
|
268
291
|
shell,
|
|
269
|
-
|
|
292
|
+
quoteValues: true,
|
|
293
|
+
}) + (args ? " " + args : "")
|
|
270
294
|
: options.script;
|
|
271
295
|
if (!options.inline && checkIsRecursiveScript(workspace.name, script)) {
|
|
272
296
|
throw new PROJECT_ERRORS.RecursiveWorkspaceScript(
|
|
@@ -473,11 +497,12 @@ class _FileSystemProject extends ProjectBase {
|
|
|
473
497
|
shell,
|
|
474
498
|
);
|
|
475
499
|
const script = options.inline
|
|
476
|
-
? interpolateWorkspaceScriptMetadata(
|
|
477
|
-
options.script,
|
|
478
|
-
workspaceScriptMetadata,
|
|
500
|
+
? interpolateWorkspaceScriptMetadata({
|
|
501
|
+
text: options.script,
|
|
502
|
+
metadata: workspaceScriptMetadata,
|
|
479
503
|
shell,
|
|
480
|
-
|
|
504
|
+
quoteValues: true,
|
|
505
|
+
}) + (args ? " " + args : "")
|
|
481
506
|
: options.script;
|
|
482
507
|
const scriptCommand = options.inline
|
|
483
508
|
? {
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
+
import { quote } from "../internal/bundledDeps/shellQuote.mjs";
|
|
1
2
|
import { BunWorkspacesError, IS_WINDOWS } from "../internal/core/index.mjs";
|
|
2
3
|
import { getWorkspaceScriptMetadataConfig } from "../3725.mjs";
|
|
3
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Wrap a value so that, when it is concatenated into a shell command for
|
|
7
|
+
* `shell`, the receiving shell parses it as a single literal token. Used
|
|
8
|
+
* when substituted metadata values land in a shell-interpretable context
|
|
9
|
+
* (inline command bodies, string-form `--args` before POSIX parse).
|
|
10
|
+
*/ const quoteShellValue = (value, shell) =>
|
|
11
|
+
IS_WINDOWS && shell === "system"
|
|
12
|
+
? `"${value.replace(/"/g, '""')}"`
|
|
13
|
+
: quote([value]);
|
|
4
14
|
const createScriptRuntimeEnvVars = (metadata) => {
|
|
5
15
|
const keys = [
|
|
6
16
|
"projectPath",
|
|
@@ -16,7 +26,12 @@ const createScriptRuntimeEnvVars = (metadata) => {
|
|
|
16
26
|
return acc;
|
|
17
27
|
}, {});
|
|
18
28
|
};
|
|
19
|
-
const interpolateWorkspaceScriptMetadata = (
|
|
29
|
+
const interpolateWorkspaceScriptMetadata = ({
|
|
30
|
+
text,
|
|
31
|
+
metadata,
|
|
32
|
+
shell,
|
|
33
|
+
quoteValues = false,
|
|
34
|
+
}) => {
|
|
20
35
|
const keys = [
|
|
21
36
|
"projectPath",
|
|
22
37
|
"projectName",
|
|
@@ -33,6 +48,13 @@ const interpolateWorkspaceScriptMetadata = (text, metadata, shell) => {
|
|
|
33
48
|
(k) => getWorkspaceScriptMetadataConfig(k).inlineName === match,
|
|
34
49
|
);
|
|
35
50
|
const value = metadata[key];
|
|
51
|
+
if (quoteValues) {
|
|
52
|
+
// Preserve "empty substitution is invisible" — quoting an empty value
|
|
53
|
+
// would inject a literal `''` shell token (an empty positional arg),
|
|
54
|
+
// which changes argv length for commands like `echo <scriptName>`
|
|
55
|
+
// when no inline scriptName is set.
|
|
56
|
+
return value === "" ? "" : quoteShellValue(value, shell);
|
|
57
|
+
}
|
|
36
58
|
if (IS_WINDOWS && shell === "bun") {
|
|
37
59
|
return value.replace(/\\/g, "\\\\");
|
|
38
60
|
}
|
|
@@ -57,4 +79,5 @@ export {
|
|
|
57
79
|
createScriptRuntimeEnvVars,
|
|
58
80
|
getWorkspaceScriptMetadata,
|
|
59
81
|
interpolateWorkspaceScriptMetadata,
|
|
82
|
+
quoteShellValue,
|
|
60
83
|
};
|
|
@@ -67,6 +67,7 @@ const findWorkspaces = ({
|
|
|
67
67
|
workspaceGlobs: _workspaceGlobs,
|
|
68
68
|
includeRootWorkspace = false,
|
|
69
69
|
workspacePatternConfigs,
|
|
70
|
+
loadConfigOptions,
|
|
70
71
|
}) => {
|
|
71
72
|
rootDirectory = path.resolve(rootDirectory);
|
|
72
73
|
logger.debug(`Finding workspaces in ${rootDirectory}`);
|
|
@@ -109,6 +110,7 @@ const findWorkspaces = ({
|
|
|
109
110
|
);
|
|
110
111
|
const workspaceConfig = loadWorkspaceConfig(
|
|
111
112
|
path.dirname(packageJsonPath),
|
|
113
|
+
loadConfigOptions,
|
|
112
114
|
);
|
|
113
115
|
if (workspaceConfig) {
|
|
114
116
|
for (const alias of workspaceConfig.aliases) {
|