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 +26 -0
- package/dist/numux.js +129 -30
- package/dist/types.d.ts +6 -1
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: `${
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
1311
|
-
|
|
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 */
|