@mpis/run 0.0.4 → 0.0.6

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mpis/run",
3
3
  "type": "module",
4
- "version": "0.0.4",
4
+ "version": "0.0.6",
5
5
  "keywords": [],
6
6
  "bin": {
7
7
  "run": "./loader/bin.js",
@@ -11,22 +11,23 @@
11
11
  "./package.json": "./package.json"
12
12
  },
13
13
  "dependencies": {
14
+ "split-cmd": "^1.1.0",
14
15
  "execa": "^9.6.0",
15
16
  "source-map-support": "^0.5.21",
16
- "@build-script/rushstack-config-loader": "^0.0.22",
17
- "@idlebox/args": "^0.0.11",
18
- "@idlebox/logger": "^0.0.4",
19
- "@idlebox/node": "^1.4.8",
20
- "@idlebox/common": "^1.4.16",
21
- "@mpis/client": "^0.0.4",
22
- "@mpis/shared": "^0.0.4",
23
- "@mpis/server": "^0.0.3"
17
+ "@build-script/rushstack-config-loader": "^0.0.24",
18
+ "@idlebox/args": "^0.0.13",
19
+ "@mpis/client": "^0.0.6",
20
+ "@idlebox/common": "^1.4.18",
21
+ "@idlebox/node": "^1.4.11",
22
+ "@mpis/server": "^0.0.5",
23
+ "@mpis/shared": "^0.0.6",
24
+ "@idlebox/logger": "^0.0.6"
24
25
  },
25
26
  "devDependencies": {
26
27
  "@types/node": "^24.0.14",
27
- "@build-script/single-dog-asset": "^1.0.37",
28
- "@idlebox/esbuild-executer": "^0.0.4",
29
- "@internal/local-rig": "^1.0.1"
28
+ "@idlebox/esbuild-executer": "^0.0.6",
29
+ "@internal/local-rig": "^1.0.1",
30
+ "@build-script/single-dog-asset": "^1.0.38"
30
31
  },
31
32
  "license": "MIT",
32
33
  "author": "GongT <admin@gongt.me>",
package/src/autoindex.ts CHANGED
@@ -3,16 +3,16 @@
3
3
  /* eslint-disable */
4
4
 
5
5
  /* common/args.ts */
6
- // Identifiers
7
- export { printUsage } from "./common/args.js";
6
+ // Identifiers
7
+ export { printUsage } from './common/args.js';
8
8
  /* common/paths.ts */
9
- // Identifiers
10
- export { projectRoot } from "./common/paths.js";
11
- export { selfRoot } from "./common/paths.js";
9
+ // Identifiers
10
+ export { projectRoot } from './common/paths.js';
11
+ export { selfRoot } from './common/paths.js';
12
12
  /* common/config-file.ts */
13
- // Identifiers
14
- export type { ICommand } from "./common/config-file.js";
15
- export type { IConfigFile } from "./common/config-file.js";
16
- export { loadConfigFile } from "./common/config-file.js";
13
+ // Identifiers
14
+ export type { ICommand } from './common/config-file.js';
15
+ export type { IConfigFile } from './common/config-file.js';
16
+ export { loadConfigFile } from './common/config-file.js';
17
17
  /* bin.ts */
18
- // Identifiers
18
+ // Identifiers
package/src/bin.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { humanDate, prettyFormatError, registerGlobalLifecycle, toDisposable } from '@idlebox/common';
2
2
  import { logger } from '@idlebox/logger';
3
- import { registerNodejsExitHandler } from '@idlebox/node';
3
+ import { registerNodejsExitHandler, shutdown } from '@idlebox/node';
4
4
  import { channelClient } from '@mpis/client';
5
5
  import { CompileError, ModeKind, ProcessIPCClient, WorkersManager } from '@mpis/server';
6
6
  import { rmSync } from 'node:fs';
7
+ import { dumpConfig } from './commands/config.js';
7
8
  import { context, parseCliArgs } from './common/args.js';
8
9
  import { loadConfigFile } from './common/config-file.js';
9
10
  import { projectRoot } from './common/paths.js';
@@ -31,7 +32,7 @@ process.title = `MpisRun`;
31
32
 
32
33
  logger.info`Running command "${context.command}" in ${projectRoot}`;
33
34
 
34
- const defaultNoClear = logger.debug.isEnabled;
35
+ const defaultNoClear = logger.debug.isEnabled || !process.stderr.isTTY;
35
36
  let workersManager: WorkersManager;
36
37
 
37
38
  const config = loadConfigFile(context.watchMode);
@@ -43,15 +44,29 @@ switch (context.command) {
43
44
  executeClean();
44
45
  break;
45
46
  case 'build':
47
+ if (context.dumpConfig) {
48
+ dumpConfig(config);
49
+ break;
50
+ }
46
51
  {
47
52
  if (context.withCleanup) executeClean();
48
53
 
49
- await executeBuild();
54
+ await executeBuild().catch((e: Error) => {
55
+ logger.error`failed ${context.command} project: ${e.message}`;
56
+ shutdown(1);
57
+ });
50
58
  }
51
59
  break;
52
60
  case 'watch':
61
+ if (context.dumpConfig) {
62
+ dumpConfig(config);
63
+ break;
64
+ }
53
65
  initializeStdin();
54
- await executeBuild();
66
+ await executeBuild().catch((e: Error) => {
67
+ logger.error`failed ${context.command} project: ${e.message}`;
68
+ shutdown(1);
69
+ });
55
70
  break;
56
71
  }
57
72
 
@@ -61,14 +76,17 @@ async function executeBuild() {
61
76
  workersManager = new WorkersManager(context.watchMode ? ModeKind.Watch : ModeKind.Build);
62
77
 
63
78
  initializeWorkers();
79
+ const graph = workersManager.finalize();
80
+
81
+ workersManager.onTerminate((trans) => {
82
+ const w = trans.worker;
64
83
 
65
- workersManager.onTerminate((w) => {
66
84
  if (!ProcessIPCClient.is(w)) {
67
85
  logger.fatal`worker "${w._id}" is not a ProcessIPCClient, this is a bug.`;
68
86
  return;
69
87
  }
70
88
 
71
- const times = `(+${humanDate.delta(w.time.executeEnd! - w.time.executeStart!)})`;
89
+ const times = `(+${humanDate.delta(w.time.executeStart!, w.time.executeEnd!)})`;
72
90
 
73
91
  if (context.watchMode) {
74
92
  printFailedRunError(w, `unexpected exit in watch mode ${times}`);
@@ -79,9 +97,7 @@ async function executeBuild() {
79
97
  }
80
98
  });
81
99
 
82
- workersManager.finalize();
83
-
84
- if (workersManager.allWorkers.length === 0) {
100
+ if (workersManager.nodeNames.length === 0) {
85
101
  logger.fatal`No workers to execute, check your config file.`;
86
102
  return;
87
103
  }
@@ -94,7 +110,9 @@ async function executeBuild() {
94
110
  return;
95
111
  }
96
112
  logger.verbose`Workers initialized, starting execution...`;
97
- await workersManager.startup();
113
+ await graph.startup();
114
+
115
+ logger.verbose`Startup returned.`;
98
116
 
99
117
  reprintWatchModeError();
100
118
  }
@@ -121,8 +139,7 @@ function initializeWorkers() {
121
139
  worker.pathvar.add(path);
122
140
  }
123
141
 
124
- const cmd0 = typeof cmds.command === 'string' ? cmds.command.split(' ')[0] : cmds.command[0];
125
- worker.displayTitle = `run:${cmd0}`;
142
+ worker.displayTitle = `run:${cmds.command[0]}`;
126
143
 
127
144
  workersManager.addWorker(worker, last ? [last._id] : []);
128
145
 
@@ -147,42 +164,40 @@ function initializeWorkers() {
147
164
  }
148
165
  }
149
166
 
167
+ const cls = /\x1Bc/g;
168
+
150
169
  function printFailedRunError(worker: ProcessIPCClient, message: string) {
151
- if (context.watchMode) process.stderr.write('\x1Bc');
170
+ if (context.watchMode && process.stderr.isTTY) process.stderr.write('\x1Bc');
152
171
 
153
- const text = worker.outputStream.toString().trimEnd();
172
+ const text = worker.outputStream.toString().trimEnd().replace(cls, '');
154
173
 
155
174
  if (text) {
156
- console.error(
157
- '\n\x1B[48;5;1m%s\r \x1B[0;38;5;9;1m %s \x1B[0m',
158
- ' '.repeat(process.stderr.columns || 80),
159
- `below is output of ${worker._id}`,
160
- );
175
+ console.error('\n\x1B[48;5;1m%s\r \x1B[0;38;5;9;1m %s \x1B[0m', ' '.repeat(process.stderr.columns || 80), `[@mpis/run] below is output of ${worker._id}`);
161
176
  console.error(text);
162
177
 
163
- console.error(
164
- '\x1B[48;5;1m%s\r \x1B[0;38;5;9;1m %s \x1B[0m\n',
165
- ' '.repeat(process.stderr.columns || 80),
166
- `ending output of ${worker._id}`,
167
- );
178
+ console.error('\x1B[48;5;1m%s\r \x1B[0;38;5;9;1m %s \x1B[0m\n', ' '.repeat(process.stderr.columns || 80), `[@mpis/run] ending output of ${worker._id}`);
168
179
  } else {
169
- console.error(
170
- '\n\x1B[48;5;1m%s\r \x1B[0;38;5;9;1m %s \x1B[0m',
171
- ' '.repeat(process.stderr.columns || 80),
172
- `no output from ${worker._id}`,
173
- );
180
+ console.error('\n\x1B[48;5;1m%s\r \x1B[0;38;5;9;1m %s \x1B[0m', ' '.repeat(process.stderr.columns || 80), `[@mpis/run] no output from ${worker._id}`);
174
181
  }
175
182
 
176
- console.error(workersManager.formatDebugGraph());
183
+ const graph = workersManager.finalize();
184
+ console.error('%s\n%s', graph.debugFormatGraph(), graph.debugFormatSummary());
177
185
  logger.fatal`"${worker._id}" ${message}`;
178
186
  }
179
187
 
188
+ let printTo: NodeJS.Timeout | undefined;
180
189
  function reprintWatchModeError(noClear?: boolean) {
181
- if (context.watchMode) {
182
- if (!noClear && !defaultNoClear) process.stderr.write('\x1Bc');
183
- }
184
- console.error(workersManager.formatDebugList());
185
- printAllErrors();
190
+ if (printTo) clearTimeout(printTo);
191
+ printTo = setTimeout(() => {
192
+ printTo = undefined;
193
+
194
+ if (context.watchMode) {
195
+ if (!noClear && !defaultNoClear) process.stderr.write('\x1Bc');
196
+ }
197
+ const graph = workersManager.finalize();
198
+ console.error('%s\n%s', graph.debugFormatList(), graph.debugFormatSummary());
199
+ printAllErrors();
200
+ }, 50);
186
201
  }
187
202
 
188
203
  function addDebugCommand() {
@@ -190,7 +205,7 @@ function addDebugCommand() {
190
205
  name: ['continue', 'c'],
191
206
  description: '开始执行',
192
207
  callback: () => {
193
- workersManager.startup();
208
+ workersManager.finalize().startup();
194
209
  },
195
210
  });
196
211
  registerCommand({
@@ -198,7 +213,7 @@ function addDebugCommand() {
198
213
  description: '切换调试模式(仅在启动前有效)',
199
214
  callback: (text: string) => {
200
215
  const [_, index, on_off] = text.split(/\s+/);
201
- const list: ProcessIPCClient[] = workersManager.allWorkers as ProcessIPCClient[];
216
+ const list = workersManager._allWorkers as ProcessIPCClient[];
202
217
  const worker = list[Number(index)];
203
218
  if (!worker) {
204
219
  logger.error`worker index out of range: ${index}`;
@@ -228,10 +243,17 @@ function addDebugCommand() {
228
243
  function sendStatus() {
229
244
  const noError = errors.values().every((e) => !e);
230
245
  if (noError) {
231
- channelClient.success(`All workers completed successfully.`);
246
+ channelClient.success(`all ${workersManager.size()} workers completed successfully.`);
232
247
  } else {
233
- const errorCnt = errors.values().filter((e) => !!e);
234
- channelClient.failed(`${errorCnt} (of ${workersManager.size}) workers error.`, formatAllErrors());
248
+ let errorCnt = 0;
249
+ const arr: string[] = [];
250
+ for (const [client, err] of errors.entries()) {
251
+ if (err) {
252
+ errorCnt++;
253
+ arr.push(client._id);
254
+ }
255
+ }
256
+ channelClient.failed(`mpis-run: ${arr.join(', ')} (${errorCnt} / ${workersManager.size()})`, formatAllErrors());
235
257
  }
236
258
  }
237
259
 
@@ -270,8 +292,8 @@ function printAllErrors() {
270
292
  if (numFailed !== 0) {
271
293
  console.error(formatAllErrors());
272
294
 
273
- logger.error(`💥 ${numFailed} of ${workersManager.size} worker failed (${execTip})`);
295
+ logger.error(`💥 ${numFailed} of ${workersManager.size()} worker failed (${execTip})`);
274
296
  } else {
275
- logger.success(`✅ no error in ${workersManager.size} workers (${execTip})`);
297
+ logger.success(`✅ no error in ${workersManager.size()} workers (${execTip})`);
276
298
  }
277
299
  }
@@ -0,0 +1,78 @@
1
+ import { ProjectConfig } from '@build-script/rushstack-config-loader';
2
+ import { CSI, logger } from '@idlebox/logger';
3
+ import { realpathSync } from 'node:fs';
4
+ import { projectRoot } from '../autoindex.js';
5
+ import type { ICommand, IConfigFile } from '../common/config-file.js';
6
+
7
+ export function dumpConfig(config: IConfigFile) {
8
+ const buildObject: Record<string, ICommand> = {};
9
+ for (const [key, value] of config.build.entries()) {
10
+ buildObject[key] = value;
11
+ }
12
+ const mapRaw = {
13
+ buildTitles: config.buildTitles,
14
+ build: buildObject,
15
+ clean: config.clean,
16
+ additionalPaths: config.additionalPaths,
17
+ };
18
+
19
+ if (!process.stdout.isTTY) {
20
+ console.log(JSON.stringify(mapRaw, null, 2));
21
+ return;
22
+ }
23
+
24
+ console.log('');
25
+ console.log(`🏗️ 构建命令 🏗️`);
26
+
27
+ for (const [key, value] of [...config.build.entries(), ...Object.entries(config.unusedBuild)]) {
28
+ if (config.build.has(key)) {
29
+ console.log(` ${CSI}38;5;10m✓ ${key}${CSI}0m`);
30
+ } else {
31
+ console.log(` ${CSI}38;5;9m✗ ${key}${CSI}0m`);
32
+ }
33
+ const cmdDump = value.command.map((e) => {
34
+ return `${CSI}3m${JSON.stringify(e)}${CSI}0m`;
35
+ });
36
+ console.log(` 命令: ${cmdDump.join(' ')}`);
37
+ console.log(` 工作目录: ${CSI}3m${JSON.stringify(value.cwd)}${CSI}0m`);
38
+ if (Object.keys(value.env).length > 0) {
39
+ console.log(` 额外环境变量:`);
40
+ for (const [key, val] of Object.entries(value.env)) {
41
+ console.log(` ${key}: ${CSI}2m${JSON.stringify(val)}${CSI}0m`);
42
+ }
43
+ } else {
44
+ console.log(` 额外环境变量: 无`);
45
+ }
46
+ console.log('');
47
+ }
48
+
49
+ console.log(`🧹 清理目录 🧹`);
50
+ for (const path of config.clean) {
51
+ console.log(` ${CSI}38;5;10m-${CSI}0m ${path}`);
52
+ }
53
+
54
+ console.log('');
55
+ console.log(`🔍 附加路径 🔍`);
56
+ for (const path of config.additionalPaths) {
57
+ console.log(` ${CSI}38;5;10m-${CSI}0m ${path}`);
58
+ }
59
+
60
+ const cfgObj = new ProjectConfig(projectRoot, undefined, logger);
61
+ const cfgInfo = cfgObj.getJsonConfigInfo('commands');
62
+
63
+ console.log('');
64
+ console.log(`🔧 配置文件信息 🔧`);
65
+ if (cfgInfo.project.exists) {
66
+ console.log(` 项目: ${cfgInfo.project.path}`);
67
+ } else {
68
+ console.log(` 项目: (不存在) ${CSI}2m${cfgInfo.project.path}${CSI}0m`);
69
+ }
70
+ if (cfgInfo.rig.exists) {
71
+ console.log(` 脚手架: ${realpathSync(cfgInfo.rig.path)}`);
72
+ } else {
73
+ console.log(` 脚手架: (不存在) ${CSI}2m${cfgInfo.rig.path}${CSI}0m`);
74
+ }
75
+
76
+ console.log('');
77
+ console.log('');
78
+ }
@@ -5,10 +5,10 @@ export function printUsage() {
5
5
  console.log('Usage: my-cli <command>');
6
6
  console.log();
7
7
  console.log('Commands:');
8
- console.log(' build run build');
9
- console.log(' watch start watch mode');
8
+ console.log(' build run build [--dump]');
9
+ console.log(' watch start watch mode [--dump]');
10
10
  console.log(' clean cleanup the project');
11
- console.log(' init create config/commands.json');
11
+ // console.log(' init create config file if not');
12
12
  }
13
13
 
14
14
  export function parseCliArgs() {
@@ -26,7 +26,7 @@ export function parseCliArgs() {
26
26
 
27
27
  createRootLogger('', level);
28
28
 
29
- const command = argv.command(['build', 'watch', 'clean', 'init']);
29
+ const command = argv.command(['build', 'watch', 'clean', 'config']);
30
30
  if (!command) {
31
31
  printUsage();
32
32
  throw logger.fatal`No command provided. Please specify a command to run.`;
@@ -35,32 +35,53 @@ export function parseCliArgs() {
35
35
  const watchMode = command.value === 'watch';
36
36
  const buildMode = command.value === 'build';
37
37
 
38
+ let dumpConfig = false;
39
+
38
40
  let breakMode = false;
39
41
  if (watchMode) {
42
+ dumpConfig = argv.flag('--dump') > 0;
40
43
  breakMode = argv.flag('--break') > 0;
41
44
  }
42
45
 
43
46
  let withCleanup = false;
44
47
  if (buildMode) {
48
+ dumpConfig = argv.flag('--dump') > 0;
45
49
  withCleanup = argv.flag('--clean') > 0;
46
50
  }
47
51
 
48
- if (argv.unused().length > 0) {
49
- throw logger.fatal`Unknown arguments: ${argv.unused().join(' ')}`;
52
+ let configCommand;
53
+ if (command.value === 'config') {
54
+ const cfg = command.command(['dump']);
55
+ if (!cfg) {
56
+ printUsage();
57
+ throw logger.fatal`No sub command for "config"`;
58
+ }
59
+
60
+ configCommand = cfg.value;
61
+ // will do dump
62
+ } else {
63
+ configCommand = undefined;
50
64
  }
51
65
 
52
66
  const r = {
53
67
  command: command.value,
68
+ configCommand,
54
69
  debugMode,
55
70
  verboseMode,
56
71
  watchMode,
57
72
  buildMode,
58
73
  breakMode,
59
74
  withCleanup,
75
+ dumpConfig,
60
76
  };
61
77
 
62
78
  context = r;
63
79
 
80
+ if (argv.unused().length) {
81
+ printUsage();
82
+ console.error('');
83
+ throw logger.fatal`Unknown arguments: ${argv.unused().join(' ')}`;
84
+ }
64
85
  return r;
65
86
  }
66
87
 
@@ -4,6 +4,7 @@ import { findUpUntilSync } from '@idlebox/node';
4
4
  import { readFileSync } from 'node:fs';
5
5
  import { resolve } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
+ import { split as splitCmd } from 'split-cmd';
7
8
  import { projectRoot, selfRoot } from './paths.js';
8
9
 
9
10
  interface IPackageBinary {
@@ -27,38 +28,30 @@ interface IConfigFileInput {
27
28
 
28
29
  export interface ICommand {
29
30
  title: string;
30
- command: string | readonly string[];
31
+ command: readonly string[];
31
32
  cwd: string;
32
33
  env: Record<string, string>;
33
34
  }
34
35
  export interface IConfigFile {
35
36
  buildTitles: readonly string[];
36
37
  build: ReadonlyMap<string, ICommand>;
38
+ unusedBuild: Record<string, ICommand>;
37
39
  clean: readonly string[];
38
40
  additionalPaths: readonly string[];
39
41
  }
40
42
 
41
- function watchModeCmd(
42
- command: string | readonly string[],
43
- watch?: string | readonly string[] | boolean,
44
- watchMode?: boolean,
45
- ): string | readonly string[] {
43
+ function watchModeCmd(command: string | readonly string[], watch?: string | readonly string[] | boolean, watchMode?: boolean): readonly string[] {
44
+ const cmdArr = typeof command === 'string' ? splitCmd(command) : command;
45
+
46
46
  if (!watchMode) {
47
- return command;
47
+ return cmdArr;
48
48
  }
49
49
  if (typeof watch === 'boolean') {
50
50
  throw new Error(`Invalid watch value: ${watch}. Expected string or array.`);
51
51
  }
52
52
 
53
- if (typeof command === 'string') {
54
- if (!watch) watch = '-w';
55
-
56
- return `${command} ${watch}`;
57
- } else {
58
- if (!watch) watch = ['-w'];
59
-
60
- return [...command, ...watch];
61
- }
53
+ if (!watch) watch = ['-w'];
54
+ return [...cmdArr, ...watch];
62
55
  }
63
56
 
64
57
  export function loadConfigFile(watchMode: boolean): IConfigFile {
@@ -72,6 +65,11 @@ export function loadConfigFile(watchMode: boolean): IConfigFile {
72
65
  array(left, right, keyPath) {
73
66
  switch (keyPath[0]) {
74
67
  case 'build':
68
+ if (Array.isArray(right)) {
69
+ return right;
70
+ } else {
71
+ return left;
72
+ }
75
73
  case 'clean': {
76
74
  if (!Array.isArray(right)) {
77
75
  return left;
@@ -87,13 +85,6 @@ export function loadConfigFile(watchMode: boolean): IConfigFile {
87
85
 
88
86
  const buildMap = new Map<string, ICommand>();
89
87
  const buildTitles: string[] = [];
90
- function set(cmd: ICommand) {
91
- if (buildMap.has(cmd.title)) {
92
- throw new Error(`duplicate command "${cmd.title}", rename it before continue`);
93
- }
94
- buildMap.set(cmd.title, cmd);
95
- buildTitles.push(cmd.title);
96
- }
97
88
 
98
89
  for (let item of input.build) {
99
90
  if (typeof item === 'string') {
@@ -119,31 +110,30 @@ export function loadConfigFile(watchMode: boolean): IConfigFile {
119
110
  continue;
120
111
  }
121
112
 
122
- const cmd = item.command;
123
- if (Array.isArray(cmd)) {
124
- const copy = cmd.slice();
125
- resolveCommandIsFile(config, copy);
126
- set({
127
- title: item.title ?? guessTitle(cmd),
128
- command: watchModeCmd(copy, item.watch, watchMode),
129
- cwd: resolve(projectRoot, item.cwd || '.'),
130
- env: item.env ?? {},
131
- });
132
- } else if (typeof cmd === 'object' && 'package' in cmd) {
133
- const obj = parsePackagedBinary(config, item, watchMode);
134
- set(obj);
135
- } else {
136
- throw TypeError(`Invalid command type: ${typeof cmd}. Expected string or array or object.`);
113
+ const cmd = resolveCommand(config, item, watchMode);
114
+
115
+ if (buildMap.has(cmd.title)) {
116
+ throw new Error(`duplicate command "${cmd.title}", rename it before continue`);
117
+ }
118
+ buildMap.set(cmd.title, cmd);
119
+ buildTitles.push(cmd.title);
120
+ }
121
+
122
+ const unused: Record<string, ICommand> = {};
123
+ for (const [name, item] of Object.entries(input.commands)) {
124
+ const title = item.title ?? name;
125
+ if (buildMap.has(title)) {
126
+ continue;
137
127
  }
128
+
129
+ unused[title] = resolveCommand(config, item, watchMode);
138
130
  }
139
131
 
140
132
  const additionalPaths: string[] = [];
141
133
  if (config.rigConfig.rigFound) {
142
134
  const nmPath = findUpUntilSync({ file: 'node_modules', from: config.rigConfig.getResolvedProfileFolder() });
143
135
  if (!nmPath) {
144
- throw new Error(
145
- `Failed to find "node_modules" folder in rig profile "${config.rigConfig.getResolvedProfileFolder()}".`,
146
- );
136
+ throw new Error(`Failed to find "node_modules" folder in rig profile "${config.rigConfig.getResolvedProfileFolder()}".`);
147
137
  }
148
138
  additionalPaths.push(resolve(nmPath, '.bin'));
149
139
  }
@@ -160,11 +150,31 @@ export function loadConfigFile(watchMode: boolean): IConfigFile {
160
150
  return {
161
151
  buildTitles,
162
152
  build: buildMap,
153
+ unusedBuild: unused,
163
154
  clean,
164
155
  additionalPaths: additionalPaths.toReversed(),
165
156
  };
166
157
  }
167
158
 
159
+ function resolveCommand(config: ProjectConfig, input: ICommandInput, watchMode: boolean): ICommand {
160
+ const cmd = input.command;
161
+ if (Array.isArray(cmd)) {
162
+ const copy = cmd.slice();
163
+ resolveCommandIsFile(config, copy);
164
+ return {
165
+ title: input.title ?? guessTitle(cmd),
166
+ command: watchModeCmd(copy, input.watch, watchMode),
167
+ cwd: resolve(projectRoot, input.cwd || '.'),
168
+ env: input.env ?? {},
169
+ };
170
+ } else if (typeof cmd === 'object' && 'package' in cmd) {
171
+ const obj = parsePackagedBinary(config, input, watchMode);
172
+ return obj;
173
+ } else {
174
+ throw TypeError(`Invalid command type: ${typeof cmd}. Expected string or array or object.`);
175
+ }
176
+ }
177
+
168
178
  function guessTitle(command: string | readonly string[]): string {
169
179
  if (typeof command === 'string') {
170
180
  return command.split(' ')[0];
@@ -12,4 +12,3 @@ if (!self) {
12
12
  throw new Error('Could not find self directory');
13
13
  }
14
14
  export const selfRoot = dirname(self);
15
-