numux 2.5.0 → 2.6.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 CHANGED
@@ -244,6 +244,32 @@ Each process accepts:
244
244
  | `interactive` | `boolean` | `false` | When `true`, keyboard input is forwarded to the process |
245
245
  | `errorMatcher` | `boolean \| string` | — | `true` detects ANSI red output, string = regex pattern — shows error indicator on tab |
246
246
  | `showCommand` | `boolean` | `true` | Print the command being run as the first line of output |
247
+ | `workspaces` | `boolean \| string \| string[]` | — | Run command in monorepo workspaces (see below) |
248
+
249
+ ### Workspace expansion
250
+
251
+ Use `workspaces` on a process to expand it into per-workspace processes. Reads the `workspaces` field from your root `package.json`.
252
+
253
+ ```ts
254
+ export default defineConfig({
255
+ processes: {
256
+ // All workspaces — filters by script availability for PM run commands
257
+ lint: { command: 'npm run lint', workspaces: true },
258
+
259
+ // Specific workspace by package name
260
+ validate: { command: 'npm run validate', workspaces: '@repo/image-worker' },
261
+
262
+ // Multiple specific workspaces
263
+ dev: { command: 'npm run dev', workspaces: ['@repo/api', '@repo/web'] },
264
+ },
265
+ })
266
+ ```
267
+
268
+ Each entry expands into `{name}:{wsName}` processes (e.g. `lint:api`, `lint:web`) with `cwd` set to the workspace directory. All other config (env, dependsOn, color, etc.) is inherited from the template.
269
+
270
+ When `workspaces: true` is used with a PM run command (`npm run lint`), only workspaces that have the matching script are included. Raw commands (`eslint .`) run in all workspaces.
271
+
272
+ String values resolve by package name first (with or without scope), then fall back to relative path. Cannot be combined with `cwd`.
247
273
 
248
274
  ### File watching
249
275
 
package/dist/numux.js CHANGED
@@ -36,7 +36,7 @@ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports,
36
36
  var require_package = __commonJS((exports, module) => {
37
37
  module.exports = {
38
38
  name: "numux",
39
- version: "2.5.0",
39
+ version: "2.6.0",
40
40
  description: "Terminal multiplexer with dependency orchestration",
41
41
  type: "module",
42
42
  license: "MIT",
@@ -1278,13 +1278,12 @@ function validateStopSignal(value) {
1278
1278
  // src/config/workspaces.ts
1279
1279
  import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1280
1280
  import { basename, resolve as resolve4 } from "path";
1281
- function resolveWorkspaceProcesses(script, cwd) {
1281
+ function discoverWorkspaces(cwd) {
1282
1282
  const pkgPath = resolve4(cwd, "package.json");
1283
1283
  if (!existsSync4(pkgPath)) {
1284
1284
  throw new Error(`No package.json found in ${cwd}`);
1285
1285
  }
1286
1286
  const pkgJson = JSON.parse(readFileSync3(pkgPath, "utf-8"));
1287
- const pm = detectPackageManager(pkgJson, cwd);
1288
1287
  const raw = pkgJson.workspaces;
1289
1288
  let patterns;
1290
1289
  if (Array.isArray(raw)) {
@@ -1294,31 +1293,113 @@ function resolveWorkspaceProcesses(script, cwd) {
1294
1293
  } else {
1295
1294
  throw new Error('No "workspaces" field found in package.json');
1296
1295
  }
1297
- const dirs = [];
1296
+ const workspaces = [];
1298
1297
  for (const pattern of patterns) {
1299
1298
  const glob = new Bun.Glob(pattern);
1300
1299
  for (const match of glob.scanSync({ cwd, onlyFiles: false })) {
1301
1300
  const abs = resolve4(cwd, match);
1302
1301
  const wsPkgPath = resolve4(abs, "package.json");
1303
- if (existsSync4(wsPkgPath)) {
1304
- dirs.push(abs);
1302
+ if (!existsSync4(wsPkgPath))
1303
+ continue;
1304
+ const wsPkg = JSON.parse(readFileSync3(wsPkgPath, "utf-8"));
1305
+ const pkgName = typeof wsPkg.name === "string" && wsPkg.name ? wsPkg.name : undefined;
1306
+ const name = pkgName ? pkgName.replace(/^@[^/]+\//, "") : basename(abs);
1307
+ const scripts = wsPkg.scripts ?? {};
1308
+ workspaces.push({ dir: abs, name, pkgName, scripts });
1309
+ }
1310
+ }
1311
+ return workspaces.sort((a, b) => a.name.localeCompare(b.name));
1312
+ }
1313
+ function findWorkspace(nameOrPath, workspaces, cwd) {
1314
+ const byPkgName = workspaces.find((ws) => ws.pkgName === nameOrPath);
1315
+ if (byPkgName)
1316
+ return byPkgName;
1317
+ const byName = workspaces.find((ws) => ws.name === nameOrPath);
1318
+ if (byName)
1319
+ return byName;
1320
+ const absPath = resolve4(cwd, nameOrPath);
1321
+ return workspaces.find((ws) => ws.dir === absPath);
1322
+ }
1323
+ function extractScriptFromCommand(command) {
1324
+ const match = command.match(/^(?:npm|yarn|pnpm|bun)\s+run\s+(\S+)/);
1325
+ return match ? match[1] : null;
1326
+ }
1327
+ function expandWorkspaces(config) {
1328
+ const cwd = config.cwd ? resolve4(config.cwd) : process.cwd();
1329
+ const newProcesses = {};
1330
+ let discoveredWorkspaces = null;
1331
+ for (const [name, entry] of Object.entries(config.processes)) {
1332
+ if (typeof entry === "string" || !entry.workspaces) {
1333
+ newProcesses[name] = entry;
1334
+ continue;
1335
+ }
1336
+ const proc = entry;
1337
+ const wsField = proc.workspaces;
1338
+ if (!proc.command) {
1339
+ throw new Error(`Process "${name}": workspaces requires a "command"`);
1340
+ }
1341
+ if (proc.cwd) {
1342
+ throw new Error(`Process "${name}": cannot set both "workspaces" and "cwd"`);
1343
+ }
1344
+ if (!discoveredWorkspaces) {
1345
+ discoveredWorkspaces = discoverWorkspaces(cwd);
1346
+ }
1347
+ if (discoveredWorkspaces.length === 0) {
1348
+ throw new Error(`Process "${name}": no workspaces found`);
1349
+ }
1350
+ const { workspaces: _, ...template } = proc;
1351
+ let targets;
1352
+ if (wsField === true) {
1353
+ const script = extractScriptFromCommand(proc.command);
1354
+ if (script) {
1355
+ targets = discoveredWorkspaces.filter((ws) => ws.scripts[script]);
1356
+ if (targets.length === 0) {
1357
+ throw new Error(`Process "${name}": no workspaces have a "${script}" script`);
1358
+ }
1359
+ } else {
1360
+ targets = discoveredWorkspaces;
1305
1361
  }
1362
+ } else {
1363
+ const names = Array.isArray(wsField) ? wsField : [wsField];
1364
+ targets = [];
1365
+ for (const wsName of names) {
1366
+ const ws = findWorkspace(wsName, discoveredWorkspaces, cwd);
1367
+ if (!ws) {
1368
+ const available = discoveredWorkspaces.map((w) => w.name).join(", ");
1369
+ throw new Error(`Process "${name}": workspace "${wsName}" not found. Available: ${available}`);
1370
+ }
1371
+ targets.push(ws);
1372
+ }
1373
+ }
1374
+ const usedNames = new Set(Object.keys(newProcesses));
1375
+ for (const ws of targets) {
1376
+ let wsKey = `${name}:${ws.name}`;
1377
+ if (usedNames.has(wsKey)) {
1378
+ let suffix = 1;
1379
+ while (usedNames.has(`${wsKey}-${suffix}`))
1380
+ suffix++;
1381
+ wsKey = `${wsKey}-${suffix}`;
1382
+ }
1383
+ usedNames.add(wsKey);
1384
+ newProcesses[wsKey] = { ...template, cwd: ws.dir };
1306
1385
  }
1307
1386
  }
1387
+ return { ...config, processes: newProcesses };
1388
+ }
1389
+ function resolveWorkspaceProcesses(script, cwd) {
1390
+ const pkgPath = resolve4(cwd, "package.json");
1391
+ if (!existsSync4(pkgPath)) {
1392
+ throw new Error(`No package.json found in ${cwd}`);
1393
+ }
1394
+ const pkgJson = JSON.parse(readFileSync3(pkgPath, "utf-8"));
1395
+ const pm = detectPackageManager(pkgJson, cwd);
1396
+ const workspaces = discoverWorkspaces(cwd);
1308
1397
  const processes = {};
1309
1398
  const usedNames = new Set;
1310
- for (const dir of dirs) {
1311
- const wsPkgPath = resolve4(dir, "package.json");
1312
- const wsPkg = JSON.parse(readFileSync3(wsPkgPath, "utf-8"));
1313
- const scripts = wsPkg.scripts;
1314
- if (!scripts?.[script])
1399
+ for (const ws of workspaces) {
1400
+ if (!ws.scripts[script])
1315
1401
  continue;
1316
- let name;
1317
- if (typeof wsPkg.name === "string" && wsPkg.name) {
1318
- name = wsPkg.name.replace(/^@[^/]+\//, "");
1319
- } else {
1320
- name = basename(dir);
1321
- }
1402
+ let name = ws.name;
1322
1403
  if (usedNames.has(name)) {
1323
1404
  let suffix = 1;
1324
1405
  while (usedNames.has(`${name}-${suffix}`))
@@ -1328,7 +1409,7 @@ function resolveWorkspaceProcesses(script, cwd) {
1328
1409
  usedNames.add(name);
1329
1410
  processes[name] = {
1330
1411
  command: `${pm} run ${script}`,
1331
- cwd: dir
1412
+ cwd: ws.dir
1332
1413
  };
1333
1414
  }
1334
1415
  if (Object.keys(processes).length === 0) {
@@ -2053,7 +2134,7 @@ class ProcessManager {
2053
2134
  }
2054
2135
  this.restartAttempts.set(name, 0);
2055
2136
  state.exitCode = null;
2056
- state.restartCount++;
2137
+ state.restartCount = 0;
2057
2138
  this.startTimes.set(name, Date.now());
2058
2139
  const { command, env } = this.expandDependencyCaptures(name);
2059
2140
  runner.restart(cols, rows, command, env);
@@ -2092,7 +2173,7 @@ class ProcessManager {
2092
2173
  }
2093
2174
  this.restartAttempts.set(name, 0);
2094
2175
  state.exitCode = null;
2095
- state.restartCount++;
2176
+ state.restartCount = 0;
2096
2177
  this.startTimes.set(name, Date.now());
2097
2178
  const { command, env } = this.expandDependencyCaptures(name);
2098
2179
  this.runners.get(name)?.restart(cols, rows, command, env);
@@ -3851,7 +3932,7 @@ async function main() {
3851
3932
  process.exit(0);
3852
3933
  }
3853
3934
  if (parsed.validate) {
3854
- const raw = expandScriptPatterns(await loadConfig(parsed.configPath));
3935
+ const raw = expandWorkspaces(expandScriptPatterns(await loadConfig(parsed.configPath)));
3855
3936
  const warnings2 = [];
3856
3937
  let config2 = validateConfig(raw, warnings2);
3857
3938
  config2 = filterByPlatform(config2);
@@ -3894,7 +3975,7 @@ async function main() {
3894
3975
  process.exit(0);
3895
3976
  }
3896
3977
  if (parsed.exec) {
3897
- const raw = expandScriptPatterns(await loadConfig(parsed.configPath));
3978
+ const raw = expandWorkspaces(expandScriptPatterns(await loadConfig(parsed.configPath)));
3898
3979
  const config2 = validateConfig(raw);
3899
3980
  const proc = config2.processes[parsed.execName];
3900
3981
  if (!proc) {
@@ -3966,7 +4047,7 @@ async function main() {
3966
4047
  }
3967
4048
  }
3968
4049
  } else {
3969
- const raw = expandScriptPatterns(await loadConfig(parsed.configPath));
4050
+ const raw = expandWorkspaces(expandScriptPatterns(await loadConfig(parsed.configPath)));
3970
4051
  config = validateConfig(raw, warnings);
3971
4052
  config = filterByPlatform(config);
3972
4053
  }
package/dist/types.d.ts CHANGED
@@ -45,6 +45,11 @@ export interface NumuxProcessConfig<K extends string = string> {
45
45
  interactive?: boolean;
46
46
  /** `true` = detect ANSI red output, string = regex pattern */
47
47
  errorMatcher?: boolean | string;
48
+ /**
49
+ * Run command in monorepo workspaces.
50
+ * `true` = all workspaces, string = specific workspace by name/path, string[] = multiple workspaces
51
+ */
52
+ workspaces?: boolean | string | string[];
48
53
  /**
49
54
  * Print the command being run as the first line of output
50
55
  * @default true
@@ -118,7 +123,7 @@ export interface NumuxConfig<K extends string = string> {
118
123
  }
119
124
  export type SortOrder = 'config' | 'alphabetical' | 'topological';
120
125
  /** Process config after validation — dependsOn is always normalized to an array */
121
- export interface ResolvedProcessConfig extends Omit<NumuxProcessConfig, 'dependsOn'> {
126
+ export interface ResolvedProcessConfig extends Omit<NumuxProcessConfig, 'dependsOn' | 'workspaces'> {
122
127
  dependsOn?: string[];
123
128
  }
124
129
  /** Validated config with all shorthand expanded to full objects */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "numux",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "Terminal multiplexer with dependency orchestration",
5
5
  "type": "module",
6
6
  "license": "MIT",