bun-workspaces 1.1.0 → 1.1.2

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 CHANGED
@@ -65,9 +65,6 @@ bw run lint my-workspace # Run for a single workspace
65
65
  bw run lint my-workspace-a my-workspace-b # Run for multiple workspaces
66
66
  bw run lint my-alias-a my-alias-b # Run by alias (set by optional config)
67
67
 
68
- bw run lint "my-workspace-*" # Run for matching workspace names
69
- bw run lint "alias:my-alias-pattern-*" "path:my-glob/**/*" # Use matching specifiers
70
-
71
68
  # A workspace's script will wait until any workspaces it depends on have completed
72
69
  # Similar to Bun's --filter behavior
73
70
  bw run lint --dep-order
@@ -75,6 +72,9 @@ bw run lint --dep-order
75
72
  # Continue running scripts even if a dependency fails
76
73
  bw run lint --dep-order --ignore-dep-failure
77
74
 
75
+ bw run lint "my-workspace-*" # Run for matching workspace names
76
+ bw run lint "alias:my-alias-pattern-*" "path:my-glob/**/*" # Use matching specifiers
77
+
78
78
  bw run lint --args="--my-appended-args" # Add args to each script call
79
79
  bw run lint --args="--my-arg=<workspaceName>" # Use the workspace name in args
80
80
 
@@ -90,7 +90,7 @@ bw run lint --parallel=50% # Run in parallel with a max of 50% of the "auto" lim
90
90
  bw run my-script --output-style=grouped
91
91
 
92
92
  # Set the max preview lines for script output in grouped output style
93
- bw run my-script --output-style=grouped --grouped-lines=all
93
+ bw run my-script --output-style=grouped --grouped-lines=auto
94
94
  bw run my-script --output-style=grouped --grouped-lines=10
95
95
 
96
96
  # Use simple script output with workspace prefixes (default when not on a TTY)
@@ -140,14 +140,22 @@ const runSingleScript = async () => {
140
140
  const { output, exit } = project.runWorkspaceScript({
141
141
  workspaceNameOrAlias: "my-workspace",
142
142
  script: "my-script",
143
- args: "--my --appended --args", // optional, arguments to add to the command
143
+
144
+ // Optional. Arguments to add to the command
145
+ // Can be a string or an array of strings
146
+ // If string, the argv will be parsed POSIX-style
147
+ args: ["--my", "--appended", "--args"],
148
+
149
+ // Optional. Whether to ignore all output from the script.
150
+ // This saves memory when you don't need script output.
151
+ ignoreOutput: false,
144
152
  });
145
153
 
146
154
  // Get a stream of the script subprocess's output
147
155
  for await (const { chunk, metadata } of output.text()) {
148
- // console.log(chunk); // the content (string)
149
- // console.log(metadata.streamName); // "stdout" or "stderr"
150
- // console.log(metadata.workspace); // the workspace that the output came from
156
+ // console.log(chunk); // The output chunk's content (string)
157
+ // console.log(metadata.streamName); // The output stream, "stdout" or "stderr"
158
+ // console.log(metadata.workspace); // The target Workspace
151
159
  }
152
160
 
153
161
  // Get data about the script execution after it exits
@@ -173,8 +181,8 @@ const runManyScripts = async () => {
173
181
  // Required. The package.json "scripts" field name to run
174
182
  script: "my-script",
175
183
 
176
- // Optional. Arguments to add to the command
177
- args: "--my --appended --args",
184
+ // Optional. Arguments to add to the command (same as for runWorkspaceScript)
185
+ args: ["--my", "--appended", "--args"],
178
186
 
179
187
  // Optional. Whether to run the scripts in parallel (default: true)
180
188
  parallel: true,
@@ -187,6 +195,10 @@ const runManyScripts = async () => {
187
195
  // continue running scripts even if a dependency fails
188
196
  ignoreDependencyFailure: false,
189
197
 
198
+ // Optional. Whether to ignore all output from the scripts.
199
+ // This saves memory when you don't need script output.
200
+ ignoreOutput: false,
201
+
190
202
  // Optional, callback when script starts, skips, or exits
191
203
  onScriptEvent: (event, { workspace, exitResult }) => {
192
204
  // event: "start", "skip", "exit"
@@ -195,9 +207,9 @@ const runManyScripts = async () => {
195
207
 
196
208
  // Get a stream of script output
197
209
  for await (const { chunk, metadata } of output.text()) {
198
- // console.log(chunk); // the content (string)
210
+ // console.log(chunk); // the output chunk's content (string)
199
211
  // console.log(metadata.streamName); // "stdout" or "stderr"
200
- // console.log(metadata.workspace); // the workspace that the output came from
212
+ // console.log(metadata.workspace); // the Workspace that the output came from
201
213
  }
202
214
 
203
215
  // Get final summary data and script exit details after all scripts have completed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bun-workspaces",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "A monorepo management tool for Bun, with a CLI and API to enhance Bun's native workspaces.",
5
5
  "license": "MIT",
6
6
  "main": "src/index.mjs",
@@ -32,6 +32,7 @@
32
32
  }
33
33
  },
34
34
  "dependencies": {
35
- "commander": "^12.1.0"
35
+ "commander": "^12.1.0",
36
+ "shell-quote": "^1.8.3"
36
37
  }
37
38
  }
@@ -14,6 +14,7 @@ export type GlobalCommandContext = {
14
14
  middleware: CliMiddleware;
15
15
  outputWriters: Required<WriteOutputOptions>;
16
16
  terminalWidth: number;
17
+ terminalHeight: number;
17
18
  };
18
19
  export type ProjectCommandContext = GlobalCommandContext & {
19
20
  project: FileSystemProject;
@@ -26,7 +26,6 @@ export type CliProjectCommandName = Exclude<
26
26
  CliGlobalCommandName
27
27
  >;
28
28
  export declare const JSON_FLAGS: readonly ["-j", "--json"];
29
- export declare const DEFAULT_GROUPED_LINES = 20;
30
29
  export declare const CLI_COMMANDS_CONFIG: {
31
30
  readonly doctor: {
32
31
  readonly command: "doctor";
@@ -153,7 +152,7 @@ export declare const CLI_COMMANDS_CONFIG: {
153
152
  };
154
153
  readonly groupedLines: {
155
154
  readonly flags: ["-L", "--grouped-lines <count>"];
156
- readonly description: 'With "grouped" output, the max preview lines (number or "all", default 20)';
155
+ readonly description: 'With grouped output, the max preview lines (number or "auto", default "auto")';
157
156
  };
158
157
  readonly noPrefix: {
159
158
  readonly flags: ["-N", "--no-prefix"];
@@ -314,7 +313,7 @@ export declare const getCliCommandConfig: (commandName: CliCommandName) =>
314
313
  };
315
314
  readonly groupedLines: {
316
315
  readonly flags: ["-L", "--grouped-lines <count>"];
317
- readonly description: 'With "grouped" output, the max preview lines (number or "all", default 20)';
316
+ readonly description: 'With grouped output, the max preview lines (number or "auto", default "auto")';
318
317
  };
319
318
  readonly noPrefix: {
320
319
  readonly flags: ["-N", "--no-prefix"];
@@ -4,7 +4,6 @@ import { OUTPUT_STYLE_VALUES } from "./runScript/output/outputStyle.mjs"; // CON
4
4
  // CONCATENATED MODULE: ./src/cli/commands/commandsConfig.ts
5
5
 
6
6
  const JSON_FLAGS = ["-j", "--json"];
7
- const DEFAULT_GROUPED_LINES = 20;
8
7
  const CLI_COMMANDS_CONFIG = {
9
8
  doctor: {
10
9
  command: "doctor",
@@ -133,7 +132,7 @@ const CLI_COMMANDS_CONFIG = {
133
132
  },
134
133
  groupedLines: {
135
134
  flags: ["-L", "--grouped-lines <count>"],
136
- description: `With "grouped" output, the max preview lines (number or "all", default ${DEFAULT_GROUPED_LINES})`,
135
+ description: `With grouped output, the max preview lines (number or "auto", default "auto")`,
137
136
  },
138
137
  noPrefix: {
139
138
  flags: ["-N", "--no-prefix"],
@@ -176,7 +175,6 @@ const getCliCommandNames = () => Object.keys(CLI_COMMANDS_CONFIG);
176
175
 
177
176
  export {
178
177
  CLI_COMMANDS_CONFIG,
179
- DEFAULT_GROUPED_LINES,
180
178
  JSON_FLAGS,
181
179
  getCliCommandConfig,
182
180
  getCliCommandNames,
@@ -1,11 +1,11 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
+ import { expandHomePath } from "../../../internal/core/index.mjs";
3
4
  import { logger } from "../../../internal/logger/index.mjs";
4
5
  import {
5
6
  handleProjectCommand,
6
7
  splitWorkspacePatterns,
7
8
  } from "../commandHandlerUtils.mjs";
8
- import { DEFAULT_GROUPED_LINES } from "../commandsConfig.mjs";
9
9
  import {
10
10
  getDefaultOutputStyle,
11
11
  validateOutputStyle,
@@ -17,9 +17,9 @@ import {
17
17
  } from "./output/renderGroupedOutput.mjs";
18
18
  import { renderPlainOutput } from "./output/renderPlainOutput.mjs"; // CONCATENATED MODULE: external "fs"
19
19
  // CONCATENATED MODULE: external "path"
20
+ // CONCATENATED MODULE: external "../../../internal/core/index.mjs"
20
21
  // CONCATENATED MODULE: external "../../../internal/logger/index.mjs"
21
22
  // CONCATENATED MODULE: external "../commandHandlerUtils.mjs"
22
- // CONCATENATED MODULE: external "../commandsConfig.mjs"
23
23
  // CONCATENATED MODULE: external "./output/outputStyle.mjs"
24
24
  // CONCATENATED MODULE: external "./output/renderGroupedOutput.mjs"
25
25
  // CONCATENATED MODULE: external "./output/renderPlainOutput.mjs"
@@ -28,7 +28,13 @@ import { renderPlainOutput } from "./output/renderPlainOutput.mjs"; // CONCATENA
28
28
  const runScript = handleProjectCommand(
29
29
  "runScript",
30
30
  async (
31
- { project, postTerminatorArgs, outputWriters, terminalWidth },
31
+ {
32
+ project,
33
+ postTerminatorArgs,
34
+ outputWriters,
35
+ terminalWidth,
36
+ terminalHeight,
37
+ },
32
38
  positionalScript,
33
39
  positionalWorkspacePatterns,
34
40
  options,
@@ -52,7 +58,7 @@ const runScript = handleProjectCommand(
52
58
  process.exit(1);
53
59
  }
54
60
  const scriptArgs = postTerminatorArgs.length
55
- ? postTerminatorArgs.join(" ")
61
+ ? postTerminatorArgs
56
62
  : options.args;
57
63
  if (positionalWorkspacePatterns.length && options.workspacePatterns) {
58
64
  logger.error(
@@ -114,10 +120,12 @@ const runScript = handleProjectCommand(
114
120
  logger.debug(`Script name: ${scriptName}`);
115
121
  const stripDisruptiveControls = workspaces.length > 1 || !!options.parallel;
116
122
  logger.debug(`Strip disruptive controls: ${stripDisruptiveControls}`);
117
- let groupedLines = DEFAULT_GROUPED_LINES;
123
+ let groupedLines = "auto";
118
124
  if (options.groupedLines) {
119
125
  if (options.groupedLines === "all") {
120
126
  groupedLines = "all";
127
+ } else if (options.groupedLines === "auto") {
128
+ groupedLines = "auto";
121
129
  } else {
122
130
  const parsedGroupedLines = parseInt(options.groupedLines);
123
131
  if (parsedGroupedLines <= 0 || isNaN(parsedGroupedLines)) {
@@ -148,6 +156,7 @@ const runScript = handleProjectCommand(
148
156
  groupedLines,
149
157
  outputWriters,
150
158
  terminalWidth,
159
+ terminalHeight,
151
160
  ),
152
161
  prefixed: () =>
153
162
  renderPlainOutput(output, outputWriters, {
@@ -196,7 +205,7 @@ const runScript = handleProjectCommand(
196
205
  if (options.jsonOutfile) {
197
206
  const fullOutputPath = path.resolve(
198
207
  project.rootDirectory,
199
- options.jsonOutfile,
208
+ expandHomePath(options.jsonOutfile),
200
209
  );
201
210
  // Check if can make directory
202
211
  const jsonOutputDir = path.dirname(fullOutputPath);
@@ -67,8 +67,9 @@ export declare const renderGroupedOutput: (
67
67
  output: RunScriptAcrossWorkspacesOutput,
68
68
  summary: Promise<RunScriptsSummary<RunWorkspaceScriptMetadata>>,
69
69
  scriptEventTarget: ScriptEventTarget,
70
- activeScriptLines: number | "all",
70
+ activeScriptLines: number | "all" | "auto",
71
71
  outputWriters: Required<WriteOutputOptions>,
72
72
  terminalWidth: number,
73
+ terminalHeight: number,
73
74
  ) => Promise<void>;
74
75
  export {};
@@ -55,7 +55,7 @@ const textOps = {
55
55
  };
56
56
  const STATUS_COLORS = {
57
57
  pending: "gray",
58
- running: "intenseCyan",
58
+ running: "intenseMagenta",
59
59
  skipped: "gray",
60
60
  success: "intenseGreen",
61
61
  failure: "intenseRed",
@@ -63,7 +63,8 @@ const STATUS_COLORS = {
63
63
  cancelled: "gray",
64
64
  killed: "intenseRed",
65
65
  };
66
- const BORDER_COLOR = "blue";
66
+ const BORDER_COLOR = "intenseCyan";
67
+ const HEADER_ROWS_PER_WORKSPACE = 2;
67
68
  const renderGroupedOutput = async (
68
69
  workspaces,
69
70
  output,
@@ -72,6 +73,7 @@ const renderGroupedOutput = async (
72
73
  activeScriptLines,
73
74
  outputWriters,
74
75
  terminalWidth,
76
+ terminalHeight,
75
77
  ) => {
76
78
  const workspaceState = workspaces.reduce((acc, workspace) => {
77
79
  acc[workspace.name] = {
@@ -112,7 +114,26 @@ const renderGroupedOutput = async (
112
114
  if (isFinal) {
113
115
  didFinalRender = true;
114
116
  }
115
- const width = Math.max(2, terminalWidth || process.stdout.columns);
117
+ const width = Math.max(2, terminalWidth || process.stdout.columns || 2);
118
+ const height = Math.max(1, terminalHeight || process.stdout.rows || 1);
119
+ // Compute the max script lines to show per workspace based on terminal
120
+ // height, so the live TUI never exceeds the visible viewport (cursor up
121
+ // is clamped and cannot recover from overflow). Each workspace occupies
122
+ // HEADER_ROWS_PER_WORKSPACE rows plus one row for the hidden-lines
123
+ // indicator, with one additional safety row to prevent scroll on the
124
+ // final newline. The user's activeScriptLines acts as a ceiling if lower.
125
+ const availableRows = Math.max(
126
+ 1,
127
+ height - 1 - workspaces.length * (HEADER_ROWS_PER_WORKSPACE + 1),
128
+ );
129
+ const computedScriptLines = Math.max(
130
+ 1,
131
+ Math.floor(availableRows / workspaces.length),
132
+ );
133
+ const effectiveScriptLines =
134
+ activeScriptLines === "all" || activeScriptLines === "auto"
135
+ ? computedScriptLines
136
+ : Math.min(activeScriptLines, computedScriptLines);
116
137
  const linesToWrite = [];
117
138
  const workspaceBoxContents = {};
118
139
  workspaces.forEach((workspace) => {
@@ -134,69 +155,51 @@ const renderGroupedOutput = async (
134
155
  } else if (exitState === "signal") {
135
156
  statusText += ` (signal: ${state.signal})`;
136
157
  }
137
- const workspaceLine = "Workspace: " + textOps.bold(workspace.name);
158
+ const workspaceLine =
159
+ textOps[BORDER_COLOR]("Workspace: ") + textOps.bold(workspace.name);
138
160
  const statusLine =
139
- " Status: " + textOps[STATUS_COLORS[state.status]](statusText);
161
+ textOps[BORDER_COLOR](" Status: ") +
162
+ textOps[STATUS_COLORS[state.status]](statusText);
140
163
  workspaceBoxContents[workspace.name] = {
141
164
  name: workspaceLine,
142
165
  status: statusLine,
143
166
  };
144
167
  });
145
168
  const padding = 4; // left border, spaces, right border
146
- const workspaceBoxWidth = Math.min(
147
- width,
148
- Math.max(
149
- ...Object.values(workspaceBoxContents).map((content) =>
150
- Math.max(
151
- calculateVisibleLength(content.name),
152
- calculateVisibleLength(content.status),
153
- ),
154
- ),
155
- ) + padding,
156
- );
157
169
  workspaces.forEach((workspace) => {
158
170
  const state = workspaceState[workspace.name];
159
171
  const { name: workspaceNameContent, status: statusTextContent } =
160
172
  workspaceBoxContents[workspace.name];
161
- linesToWrite.push({
162
- text: textOps[BORDER_COLOR](
163
- "┌" + "─".repeat(workspaceBoxWidth - 2) + "┐",
164
- ),
165
- type: "border",
166
- });
167
- const borderText = (text) => {
173
+ const borderText = (text, top, headerWidth) => {
168
174
  const visibleLength = calculateVisibleLength(text);
169
175
  const truncated =
170
176
  visibleLength > width - padding
171
177
  ? truncateTerminalString(text, width - padding - 1) + "\x1b[0m…"
172
178
  : text;
173
179
  return (
174
- textOps[BORDER_COLOR](" ") +
180
+ textOps[BORDER_COLOR](top ? " " : "└ ") +
175
181
  truncated +
176
- " ".repeat(Math.max(0, workspaceBoxWidth - visibleLength - padding)) +
177
- textOps[BORDER_COLOR](" ")
182
+ " ".repeat(Math.max(0, headerWidth - visibleLength - padding)) +
183
+ textOps[BORDER_COLOR](top ? " " : " ┘")
178
184
  );
179
185
  };
186
+ const headerWidth = Math.min(
187
+ width,
188
+ Math.max(
189
+ Bun.stripANSI(workspaceNameContent).length,
190
+ Bun.stripANSI(statusTextContent).length,
191
+ ) + padding,
192
+ );
180
193
  linesToWrite.push({
181
- text: borderText(workspaceNameContent),
194
+ text: borderText(workspaceNameContent, true, headerWidth),
182
195
  type: "borderedContent",
183
196
  });
184
197
  linesToWrite.push({
185
- text: borderText(statusTextContent),
198
+ text: borderText(statusTextContent, false, headerWidth),
186
199
  type: "borderedContent",
187
200
  });
188
- linesToWrite.push({
189
- text: textOps[BORDER_COLOR](
190
- "└" + "─".repeat(workspaceBoxWidth - 2) + "┘",
191
- ),
192
- type: "border",
193
- });
194
- if (
195
- activeScriptLines !== "all" &&
196
- state.lines.length > activeScriptLines &&
197
- !isFinal
198
- ) {
199
- const hiddenLines = state.lines.length - activeScriptLines;
201
+ if (state.lines.length > effectiveScriptLines && !isFinal) {
202
+ const hiddenLines = state.lines.length - effectiveScriptLines;
200
203
  linesToWrite.push({
201
204
  text: textOps.gray(
202
205
  `(${hiddenLines} line${hiddenLines === 1 ? "" : "s"} hidden until exit)`,
@@ -206,7 +209,7 @@ const renderGroupedOutput = async (
206
209
  }
207
210
  linesToWrite.push(
208
211
  ...state.lines
209
- .slice(isFinal ? undefined : -activeScriptLines)
212
+ .slice(isFinal ? undefined : -effectiveScriptLines)
210
213
  .map((line) => ({
211
214
  text: line.text,
212
215
  type: "scriptOutput",
@@ -214,6 +217,12 @@ const renderGroupedOutput = async (
214
217
  );
215
218
  return linesToWrite;
216
219
  });
220
+ if (isFinal) {
221
+ linesToWrite.push({
222
+ text: textOps[BORDER_COLOR]("─ Summary ─"),
223
+ type: "borderedContent",
224
+ });
225
+ }
217
226
  if (previousHeight > 0) {
218
227
  // clear previous frame
219
228
  outputWriters.stdout(cursorOps.up(previousHeight));
@@ -10,6 +10,7 @@ export interface RunCliOptions {
10
10
  middleware?: CliMiddlewareOptions;
11
11
  writeOutput?: WriteOutputOptions;
12
12
  terminalWidth?: number;
13
+ terminalHeight?: number;
13
14
  }
14
15
  export interface CLI {
15
16
  run: (options?: RunCliOptions) => Promise<void>;
@@ -30,6 +30,7 @@ const createCli = ({ defaultCwd = process.cwd(), defaultMiddleware } = {}) => {
30
30
  middleware: _runMiddleware,
31
31
  writeOutput,
32
32
  terminalWidth = process.stdout.columns,
33
+ terminalHeight = process.stdout.rows,
33
34
  } = {}) => {
34
35
  const middleware = resolveMiddleware(
35
36
  defaultMiddleware ?? {},
@@ -112,6 +113,7 @@ const createCli = ({ defaultCwd = process.cwd(), defaultMiddleware } = {}) => {
112
113
  middleware,
113
114
  outputWriters,
114
115
  terminalWidth,
116
+ terminalHeight,
115
117
  });
116
118
  defineGlobalCommands({
117
119
  program,
@@ -119,6 +121,7 @@ const createCli = ({ defaultCwd = process.cwd(), defaultMiddleware } = {}) => {
119
121
  middleware,
120
122
  outputWriters,
121
123
  terminalWidth,
124
+ terminalHeight,
122
125
  });
123
126
  logger.debug(`Commands initialized. Parsing args...`);
124
127
  middleware.preParse({
@@ -1,7 +1,7 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import { Option } from "commander";
4
- import { defineErrors } from "../../internal/core/index.mjs";
4
+ import { defineErrors, expandHomePath } from "../../internal/core/index.mjs";
5
5
  import { logger } from "../../internal/logger/index.mjs";
6
6
  import {
7
7
  createFileSystemProject,
@@ -90,8 +90,9 @@ const defineGlobalOptions = (program, args, middleware) => {
90
90
  program,
91
91
  args,
92
92
  );
93
- const cwd =
94
- cwdOption || (workspaceRootOption ? findRootFromCwd() : process.cwd());
93
+ const cwd = expandHomePath(
94
+ cwdOption || (workspaceRootOption ? findRootFromCwd() : process.cwd()),
95
+ );
95
96
  const exists = fs.existsSync(cwd);
96
97
  const isDirectory = exists ? fs.statSync(cwd).isDirectory() : false;
97
98
  middleware.processWorkingDirectory({
@@ -2,3 +2,5 @@ export declare const IS_WINDOWS: boolean;
2
2
  export declare const IS_MACOS: boolean;
3
3
  export declare const IS_LINUX: boolean;
4
4
  export declare const IS_POSIX: boolean;
5
+ /** Expands a leading `~` or `~/` to the user's home directory */
6
+ export declare const expandHomePath: (filePath: string) => string;
@@ -1,7 +1,19 @@
1
+ import os from "os";
2
+ import path from "path"; // CONCATENATED MODULE: external "os"
3
+ // CONCATENATED MODULE: external "path"
1
4
  // CONCATENATED MODULE: ./src/internal/core/runtime/os.ts
5
+
2
6
  const IS_WINDOWS = process.platform === "win32";
3
7
  const IS_MACOS = process.platform === "darwin";
4
8
  const IS_LINUX = process.platform === "linux";
5
9
  const IS_POSIX = IS_MACOS || IS_LINUX;
10
+ /** Expands a leading `~` or `~/` to the user's home directory */ const expandHomePath =
11
+ (filePath) => {
12
+ if (filePath === "~") return os.homedir();
13
+ if (filePath.startsWith("~/") || filePath.startsWith("~\\")) {
14
+ return path.join(os.homedir(), filePath.slice(2));
15
+ }
16
+ return filePath;
17
+ };
6
18
 
7
- export { IS_LINUX, IS_MACOS, IS_POSIX, IS_WINDOWS };
19
+ export { IS_LINUX, IS_MACOS, IS_POSIX, IS_WINDOWS, expandHomePath };
@@ -39,8 +39,8 @@ export type RunWorkspaceScriptOptions = {
39
39
  script: string;
40
40
  /** Whether to run the script as an inline command */
41
41
  inline?: boolean | InlineScriptOptions;
42
- /** The arguments to append to the script command */
43
- args?: string;
42
+ /** The arguments to append to the script command. If passed as a string, the argv will be parsed POSIX-style */
43
+ args?: string | string[];
44
44
  /** Set to `true` to ignore all output from the script. This saves memory when you don't need script output. */
45
45
  ignoreOutput?: boolean;
46
46
  };
@@ -89,8 +89,8 @@ export type RunScriptAcrossWorkspacesOptions = {
89
89
  script: string;
90
90
  /** Whether to run the script as an inline command */
91
91
  inline?: boolean | InlineScriptOptions;
92
- /** The arguments to append to the script command. `<workspaceName>` will be replaced with the workspace name */
93
- args?: string;
92
+ /** The arguments to append to the script command. If passed as a string, the argv will be parsed POSIX-style */
93
+ args?: string | string[];
94
94
  /** Whether to run the scripts in parallel (default: `true`). Pass `false` to run in series. */
95
95
  parallel?: ParallelOption;
96
96
  /** When `true`, run scripts so that dependent workspaces run only after their dependencies */
@@ -1,10 +1,15 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
+ import { parse, quote } from "shell-quote/";
3
4
  import { loadRootConfig } from "../../config/index.mjs";
4
5
  import { getUserEnvVar } from "../../config/userEnvVars/index.mjs";
5
6
  import {
6
7
  DEFAULT_TEMP_DIR,
8
+ IS_WINDOWS,
9
+ InvalidJSTypeError,
10
+ expandHomePath,
7
11
  isPlainObject,
12
+ validateJSArray,
8
13
  validateJSTypes,
9
14
  } from "../../internal/core/index.mjs";
10
15
  import { logger } from "../../internal/logger/index.mjs";
@@ -26,6 +31,7 @@ import {
26
31
  resolveWorkspacePath,
27
32
  } from "./projectBase.mjs"; // CONCATENATED MODULE: external "fs"
28
33
  // CONCATENATED MODULE: external "path"
34
+ // CONCATENATED MODULE: external "shell-quote/"
29
35
  // CONCATENATED MODULE: external "../../config/index.mjs"
30
36
  // CONCATENATED MODULE: external "../../config/userEnvVars/index.mjs"
31
37
  // CONCATENATED MODULE: external "../../internal/core/index.mjs"
@@ -39,6 +45,41 @@ import {
39
45
  // CONCATENATED MODULE: external "./projectBase.mjs"
40
46
  // CONCATENATED MODULE: ./src/project/implementations/fileSystemProject.ts
41
47
 
48
+ const quoteArg = (arg, shell) =>
49
+ IS_WINDOWS && shell === "system"
50
+ ? `"${arg.replace(/"/g, '""')}"`
51
+ : quote([arg]);
52
+ const serializeArgs = (args, metadata, shell) => {
53
+ if (!args || args.length === 0) return "";
54
+ if (Array.isArray(args)) {
55
+ return args
56
+ .map((arg) =>
57
+ quoteArg(interpolateScriptRuntimeMetadata(arg, metadata, shell), shell),
58
+ )
59
+ .join(" ");
60
+ }
61
+ const interpolated = interpolateScriptRuntimeMetadata(args, metadata, shell);
62
+ // Escape backslashes in interpolated values before POSIX parse on Windows,
63
+ // so that path separators survive parse's escape processing (\\→\)
64
+ const parseInput =
65
+ IS_WINDOWS && shell === "system"
66
+ ? interpolated.replace(/\\/g, "\\\\")
67
+ : interpolated;
68
+ return parse(parseInput)
69
+ .flatMap((entry) => {
70
+ if (typeof entry === "string") {
71
+ return [quoteArg(entry, shell)];
72
+ }
73
+ if ("comment" in entry) {
74
+ return [];
75
+ }
76
+ if ("pattern" in entry) {
77
+ return [entry.pattern];
78
+ }
79
+ return [entry.op];
80
+ })
81
+ .join(" ");
82
+ };
42
83
  class _FileSystemProject extends ProjectBase {
43
84
  rootDirectory;
44
85
  workspaces;
@@ -76,7 +117,7 @@ class _FileSystemProject extends ProjectBase {
76
117
  }
77
118
  this.rootDirectory = path.resolve(
78
119
  process.cwd(),
79
- options.rootDirectory ?? "",
120
+ expandHomePath(options.rootDirectory ?? ""),
80
121
  );
81
122
  const rootConfig = loadRootConfig(this.rootDirectory);
82
123
  const { workspaces, workspaceMap, rootWorkspace } = findWorkspaces({
@@ -121,11 +162,6 @@ class _FileSystemProject extends ProjectBase {
121
162
  typeofName: ["boolean", "object"],
122
163
  optional: true,
123
164
  },
124
- "args option": {
125
- value: options.args,
126
- typeofName: "string",
127
- optional: true,
128
- },
129
165
  "ignoreOutput option": {
130
166
  value: options.ignoreOutput,
131
167
  typeofName: "boolean",
@@ -136,6 +172,23 @@ class _FileSystemProject extends ProjectBase {
136
172
  throw: true,
137
173
  },
138
174
  );
175
+ if (options.args !== undefined) {
176
+ if (typeof options.args !== "string" && !Array.isArray(options.args)) {
177
+ throw new InvalidJSTypeError(
178
+ `Type error: args option expects type string | string[], received ${typeof options.args}`,
179
+ );
180
+ }
181
+ if (Array.isArray(options.args)) {
182
+ const argsError = validateJSArray({
183
+ value: options.args,
184
+ valueLabel: "args option",
185
+ itemOptions: {
186
+ typeofName: "string",
187
+ },
188
+ });
189
+ if (argsError) throw argsError;
190
+ }
191
+ }
139
192
  if (isPlainObject(options.inline)) {
140
193
  validateJSTypes(
141
194
  {
@@ -184,11 +237,7 @@ class _FileSystemProject extends ProjectBase {
184
237
  workspaceName: workspace.name,
185
238
  scriptName: options.inline ? inlineScriptName : options.script,
186
239
  };
187
- const args = interpolateScriptRuntimeMetadata(
188
- options.args ?? "",
189
- scriptRuntimeMetadata,
190
- shell,
191
- );
240
+ const args = serializeArgs(options.args, scriptRuntimeMetadata, shell);
192
241
  const script = options.inline
193
242
  ? interpolateScriptRuntimeMetadata(
194
243
  options.script,
@@ -242,11 +291,6 @@ class _FileSystemProject extends ProjectBase {
242
291
  typeofName: ["boolean", "object"],
243
292
  optional: true,
244
293
  },
245
- "args option": {
246
- value: options.args,
247
- typeofName: "string",
248
- optional: true,
249
- },
250
294
  "parallel option": {
251
295
  value: options.parallel,
252
296
  typeofName: ["boolean", "object"],
@@ -296,6 +340,23 @@ class _FileSystemProject extends ProjectBase {
296
340
  },
297
341
  );
298
342
  }
343
+ if (options.args !== undefined) {
344
+ if (typeof options.args !== "string" && !Array.isArray(options.args)) {
345
+ throw new InvalidJSTypeError(
346
+ `Type error: args option expects type string | string[], received ${typeof options.args}`,
347
+ );
348
+ }
349
+ if (Array.isArray(options.args)) {
350
+ const argsError = validateJSArray({
351
+ value: options.args,
352
+ valueLabel: "args option",
353
+ itemOptions: {
354
+ typeofName: "string",
355
+ },
356
+ });
357
+ if (argsError) throw argsError;
358
+ }
359
+ }
299
360
  if (isPlainObject(options.parallel)) {
300
361
  validateJSTypes(
301
362
  {
@@ -383,11 +444,7 @@ class _FileSystemProject extends ProjectBase {
383
444
  workspaceName: workspace.name,
384
445
  scriptName: options.inline ? inlineScriptName : options.script,
385
446
  };
386
- const args = interpolateScriptRuntimeMetadata(
387
- options.args ?? "",
388
- scriptRuntimeMetadata,
389
- shell,
390
- );
447
+ const args = serializeArgs(options.args, scriptRuntimeMetadata, shell);
391
448
  const script = options.inline
392
449
  ? interpolateScriptRuntimeMetadata(
393
450
  options.script,