numux 2.5.1 → 2.6.1

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.1",
39
+ version: "2.6.1",
40
40
  description: "Terminal multiplexer with dependency orchestration",
41
41
  type: "module",
42
42
  license: "MIT",
@@ -713,29 +713,47 @@ function splitPatternArgs(raw) {
713
713
  return { glob: raw, extraArgs: "" };
714
714
  return { glob: raw.slice(0, i), extraArgs: raw.slice(i) };
715
715
  }
716
+ function expandScriptCommand(raw, pm) {
717
+ const { glob: script, extraArgs } = splitPatternArgs(raw);
718
+ if (extraArgs) {
719
+ return `${pm} run ${script} --${extraArgs}`;
720
+ }
721
+ return `${pm} run ${script}`;
722
+ }
716
723
  function expandScriptPatterns(config, cwd) {
717
724
  const entries = Object.entries(config.processes);
725
+ const cmd = (v) => typeof v === "string" ? v : v?.command;
718
726
  const hasScriptRef = entries.some(([name, value]) => isScriptReference(name, value));
719
- if (!hasScriptRef)
727
+ const hasNpmCommand = entries.some(([, v]) => {
728
+ const c = cmd(v);
729
+ return typeof c === "string" && c.startsWith("npm:");
730
+ });
731
+ if (!(hasScriptRef || hasNpmCommand))
720
732
  return config;
721
733
  const dir = config.cwd ?? cwd ?? process.cwd();
722
734
  const pkgPath = resolve(dir, "package.json");
723
- if (!existsSync(pkgPath)) {
735
+ if (!existsSync(pkgPath) && hasScriptRef) {
724
736
  throw new Error(`Wildcard patterns require a package.json (looked in ${dir})`);
725
737
  }
726
- const pkgJson = JSON.parse(readFileSync(pkgPath, "utf-8"));
738
+ const pkgJson = existsSync(pkgPath) ? JSON.parse(readFileSync(pkgPath, "utf-8")) : {};
727
739
  const scripts = pkgJson.scripts;
728
- if (!scripts || typeof scripts !== "object") {
729
- throw new Error('package.json has no "scripts" field');
730
- }
731
- const scriptNames = Object.keys(scripts);
740
+ const scriptNames = scripts && typeof scripts === "object" ? Object.keys(scripts) : [];
732
741
  const pm = detectPackageManager(pkgJson, dir);
733
742
  const expanded = {};
734
743
  for (const [name, value] of entries) {
735
744
  if (!isScriptReference(name, value)) {
736
- expanded[name] = value;
745
+ let proc = value;
746
+ const c = cmd(proc);
747
+ if (typeof c === "string" && c.startsWith("npm:")) {
748
+ const expandedCmd = expandScriptCommand(c.slice(4), pm);
749
+ proc = typeof proc === "string" ? expandedCmd : { ...proc, command: expandedCmd };
750
+ }
751
+ expanded[name] = proc;
737
752
  continue;
738
753
  }
754
+ if (!scripts || typeof scripts !== "object") {
755
+ throw new Error('package.json has no "scripts" field');
756
+ }
739
757
  const rawPattern = name.startsWith("npm:") ? name.slice(4) : name;
740
758
  const { glob: globPattern, extraArgs } = splitPatternArgs(rawPattern);
741
759
  const template = value ?? {};
@@ -762,7 +780,7 @@ function expandScriptPatterns(config, cwd) {
762
780
  const { color: _color, ...rest } = template;
763
781
  expanded[displayName] = {
764
782
  ...rest,
765
- command: `${pm} run ${scriptName}${extraArgs}`,
783
+ command: expandScriptCommand(`${scriptName}${extraArgs}`, pm),
766
784
  ...color ? { color } : {}
767
785
  };
768
786
  }
@@ -1278,13 +1296,12 @@ function validateStopSignal(value) {
1278
1296
  // src/config/workspaces.ts
1279
1297
  import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1280
1298
  import { basename, resolve as resolve4 } from "path";
1281
- function resolveWorkspaceProcesses(script, cwd) {
1299
+ function discoverWorkspaces(cwd) {
1282
1300
  const pkgPath = resolve4(cwd, "package.json");
1283
1301
  if (!existsSync4(pkgPath)) {
1284
1302
  throw new Error(`No package.json found in ${cwd}`);
1285
1303
  }
1286
1304
  const pkgJson = JSON.parse(readFileSync3(pkgPath, "utf-8"));
1287
- const pm = detectPackageManager(pkgJson, cwd);
1288
1305
  const raw = pkgJson.workspaces;
1289
1306
  let patterns;
1290
1307
  if (Array.isArray(raw)) {
@@ -1294,31 +1311,113 @@ function resolveWorkspaceProcesses(script, cwd) {
1294
1311
  } else {
1295
1312
  throw new Error('No "workspaces" field found in package.json');
1296
1313
  }
1297
- const dirs = [];
1314
+ const workspaces = [];
1298
1315
  for (const pattern of patterns) {
1299
1316
  const glob = new Bun.Glob(pattern);
1300
1317
  for (const match of glob.scanSync({ cwd, onlyFiles: false })) {
1301
1318
  const abs = resolve4(cwd, match);
1302
1319
  const wsPkgPath = resolve4(abs, "package.json");
1303
- if (existsSync4(wsPkgPath)) {
1304
- dirs.push(abs);
1320
+ if (!existsSync4(wsPkgPath))
1321
+ continue;
1322
+ const wsPkg = JSON.parse(readFileSync3(wsPkgPath, "utf-8"));
1323
+ const pkgName = typeof wsPkg.name === "string" && wsPkg.name ? wsPkg.name : undefined;
1324
+ const name = pkgName ? pkgName.replace(/^@[^/]+\//, "") : basename(abs);
1325
+ const scripts = wsPkg.scripts ?? {};
1326
+ workspaces.push({ dir: abs, name, pkgName, scripts });
1327
+ }
1328
+ }
1329
+ return workspaces.sort((a, b) => a.name.localeCompare(b.name));
1330
+ }
1331
+ function findWorkspace(nameOrPath, workspaces, cwd) {
1332
+ const byPkgName = workspaces.find((ws) => ws.pkgName === nameOrPath);
1333
+ if (byPkgName)
1334
+ return byPkgName;
1335
+ const byName = workspaces.find((ws) => ws.name === nameOrPath);
1336
+ if (byName)
1337
+ return byName;
1338
+ const absPath = resolve4(cwd, nameOrPath);
1339
+ return workspaces.find((ws) => ws.dir === absPath);
1340
+ }
1341
+ function extractScriptFromCommand(command) {
1342
+ const match = command.match(/^(?:npm|yarn|pnpm|bun)\s+run\s+(\S+)/);
1343
+ return match ? match[1] : null;
1344
+ }
1345
+ function expandWorkspaces(config) {
1346
+ const cwd = config.cwd ? resolve4(config.cwd) : process.cwd();
1347
+ const newProcesses = {};
1348
+ let discoveredWorkspaces = null;
1349
+ for (const [name, entry] of Object.entries(config.processes)) {
1350
+ if (typeof entry === "string" || !entry.workspaces) {
1351
+ newProcesses[name] = entry;
1352
+ continue;
1353
+ }
1354
+ const proc = entry;
1355
+ const wsField = proc.workspaces;
1356
+ if (!proc.command) {
1357
+ throw new Error(`Process "${name}": workspaces requires a "command"`);
1358
+ }
1359
+ if (proc.cwd) {
1360
+ throw new Error(`Process "${name}": cannot set both "workspaces" and "cwd"`);
1361
+ }
1362
+ if (!discoveredWorkspaces) {
1363
+ discoveredWorkspaces = discoverWorkspaces(cwd);
1364
+ }
1365
+ if (discoveredWorkspaces.length === 0) {
1366
+ throw new Error(`Process "${name}": no workspaces found`);
1367
+ }
1368
+ const { workspaces: _, ...template } = proc;
1369
+ let targets;
1370
+ if (wsField === true) {
1371
+ const script = extractScriptFromCommand(proc.command);
1372
+ if (script) {
1373
+ targets = discoveredWorkspaces.filter((ws) => ws.scripts[script]);
1374
+ if (targets.length === 0) {
1375
+ throw new Error(`Process "${name}": no workspaces have a "${script}" script`);
1376
+ }
1377
+ } else {
1378
+ targets = discoveredWorkspaces;
1305
1379
  }
1380
+ } else {
1381
+ const names = Array.isArray(wsField) ? wsField : [wsField];
1382
+ targets = [];
1383
+ for (const wsName of names) {
1384
+ const ws = findWorkspace(wsName, discoveredWorkspaces, cwd);
1385
+ if (!ws) {
1386
+ const available = discoveredWorkspaces.map((w) => w.name).join(", ");
1387
+ throw new Error(`Process "${name}": workspace "${wsName}" not found. Available: ${available}`);
1388
+ }
1389
+ targets.push(ws);
1390
+ }
1391
+ }
1392
+ const usedNames = new Set(Object.keys(newProcesses));
1393
+ for (const ws of targets) {
1394
+ let wsKey = `${name}:${ws.name}`;
1395
+ if (usedNames.has(wsKey)) {
1396
+ let suffix = 1;
1397
+ while (usedNames.has(`${wsKey}-${suffix}`))
1398
+ suffix++;
1399
+ wsKey = `${wsKey}-${suffix}`;
1400
+ }
1401
+ usedNames.add(wsKey);
1402
+ newProcesses[wsKey] = { ...template, cwd: ws.dir };
1306
1403
  }
1307
1404
  }
1405
+ return { ...config, processes: newProcesses };
1406
+ }
1407
+ function resolveWorkspaceProcesses(script, cwd) {
1408
+ const pkgPath = resolve4(cwd, "package.json");
1409
+ if (!existsSync4(pkgPath)) {
1410
+ throw new Error(`No package.json found in ${cwd}`);
1411
+ }
1412
+ const pkgJson = JSON.parse(readFileSync3(pkgPath, "utf-8"));
1413
+ const pm = detectPackageManager(pkgJson, cwd);
1414
+ const workspaces = discoverWorkspaces(cwd);
1308
1415
  const processes = {};
1309
1416
  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])
1417
+ for (const ws of workspaces) {
1418
+ if (!ws.scripts[script])
1315
1419
  continue;
1316
- let name;
1317
- if (typeof wsPkg.name === "string" && wsPkg.name) {
1318
- name = wsPkg.name.replace(/^@[^/]+\//, "");
1319
- } else {
1320
- name = basename(dir);
1321
- }
1420
+ let name = ws.name;
1322
1421
  if (usedNames.has(name)) {
1323
1422
  let suffix = 1;
1324
1423
  while (usedNames.has(`${name}-${suffix}`))
@@ -1328,7 +1427,7 @@ function resolveWorkspaceProcesses(script, cwd) {
1328
1427
  usedNames.add(name);
1329
1428
  processes[name] = {
1330
1429
  command: `${pm} run ${script}`,
1331
- cwd: dir
1430
+ cwd: ws.dir
1332
1431
  };
1333
1432
  }
1334
1433
  if (Object.keys(processes).length === 0) {
@@ -3851,7 +3950,7 @@ async function main() {
3851
3950
  process.exit(0);
3852
3951
  }
3853
3952
  if (parsed.validate) {
3854
- const raw = expandScriptPatterns(await loadConfig(parsed.configPath));
3953
+ const raw = expandWorkspaces(expandScriptPatterns(await loadConfig(parsed.configPath)));
3855
3954
  const warnings2 = [];
3856
3955
  let config2 = validateConfig(raw, warnings2);
3857
3956
  config2 = filterByPlatform(config2);
@@ -3894,7 +3993,7 @@ async function main() {
3894
3993
  process.exit(0);
3895
3994
  }
3896
3995
  if (parsed.exec) {
3897
- const raw = expandScriptPatterns(await loadConfig(parsed.configPath));
3996
+ const raw = expandWorkspaces(expandScriptPatterns(await loadConfig(parsed.configPath)));
3898
3997
  const config2 = validateConfig(raw);
3899
3998
  const proc = config2.processes[parsed.execName];
3900
3999
  if (!proc) {
@@ -3966,7 +4065,7 @@ async function main() {
3966
4065
  }
3967
4066
  }
3968
4067
  } else {
3969
- const raw = expandScriptPatterns(await loadConfig(parsed.configPath));
4068
+ const raw = expandWorkspaces(expandScriptPatterns(await loadConfig(parsed.configPath)));
3970
4069
  config = validateConfig(raw, warnings);
3971
4070
  config = filterByPlatform(config);
3972
4071
  }
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.1",
3
+ "version": "2.6.1",
4
4
  "description": "Terminal multiplexer with dependency orchestration",
5
5
  "type": "module",
6
6
  "license": "MIT",