@fynjs/run 1.1.8 → 1.1.10

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.
Files changed (3) hide show
  1. package/README.md +29 -15
  2. package/docs/reference.md +1225 -0
  3. package/package.json +15 -14
package/README.md CHANGED
@@ -1,7 +1,16 @@
1
- [![NPM version][npm-image]][npm-url] [![Build Status][ci-shield]][ci-url]
2
-
3
1
  # @fynjs/run
4
2
 
3
+ [![License][license-image]][license-url]
4
+ [![build][build-image]][build-url]
5
+
6
+ [![Downloads][downloads-image]][downloads-url]
7
+
8
+ [![npm badge][npm-badge-png]][package-url]
9
+
10
+ - [API reference](https://fynjs.pages.dev/api/modules/_fynjs_run) - TypeDoc
11
+ - [Markdown reference](docs/reference.md) - the full API and runtime behavior in one file. Also ships in the package.
12
+ - [GitHub](https://github.com/jchip/fynjs/tree/main/packages/xarc-run)
13
+
5
14
  `npm run` enhanced - A powerful task runner and build tool for modern JavaScript projects.
6
15
 
7
16
  - **Compatible** with `npm run` for [npm scripts]
@@ -314,23 +323,23 @@ load({
314
323
  - Serial and concurrent task execution with proper nesting hierarchy
315
324
  - Promise, [node.js stream], or callback support for JavaScript tasks
316
325
  - Run time flow control - return further tasks to execute from JS task functions
317
- - Tasks can have a [_finally_](./REFERENCE.md#finally-hook) hook that always runs after task finish or fail
326
+ - Tasks can have a [_finally_](./docs/reference.md#finally-hook) hook that always runs after task finish or fail
318
327
 
319
328
  - **Developer Experience**
320
329
 
321
330
  - Compatible with and loads npm scripts from `package.json`
322
331
  - Auto completion for [bash] and [zsh]
323
332
  - TypeScript support with automatic tsx/ts-node loading (tsx preferred)
324
- - Advanced CLI with comprehensive options (see [CLI reference](./REFERENCE.md#cli-options))
333
+ - Advanced CLI with comprehensive options (see [CLI reference](./docs/reference.md#cli-options))
325
334
  - Argument parsing with `--` remaining args support
326
335
  - Specify complex task execution patterns from command line
327
336
 
328
337
  - **Extensibility & Organization**
329
338
 
330
339
  - **Provider packages** - reusable task libraries for sharing common workflows
331
- - **[Namespaces](./REFERENCE.md#namespace)** for organizing tasks across modules
340
+ - **[Namespaces](./docs/reference.md#namespace)** for organizing tasks across modules
332
341
  - Define tasks in JavaScript files with full programmatic control
333
- - Support [flexible function tasks](./REFERENCE.md#function) that can return more tasks to run
342
+ - Support [flexible function tasks](./docs/reference.md#function) that can return more tasks to run
334
343
  - Custom task execution reporters
335
344
 
336
345
  - **Advanced Features**
@@ -570,7 +579,7 @@ For complete CLI reference:
570
579
  $ xrun -h
571
580
  ```
572
581
 
573
- See [CLI Options](./REFERENCE.md#cli-options) for full details.
582
+ See [CLI Options](./docs/reference.md#cli-options) for full details.
574
583
 
575
584
  To load [npm scripts] into the `npm` namespace, use the `--npm` option:
576
585
 
@@ -641,7 +650,7 @@ For example:
641
650
 
642
651
  ## Task Definition
643
652
 
644
- A task can be `string`, `array`, `function`, or `object`. See [reference](./REFERENCE.md#task-definition) for details.
653
+ A task can be `string`, `array`, `function`, or `object`. See [reference](./docs/reference.md#task-definition) for details.
645
654
 
646
655
  ## package.json
647
656
 
@@ -706,20 +715,25 @@ const tasks = {
706
715
 
707
716
  ## Detailed Reference
708
717
 
709
- See [reference](./REFERENCE.md) for more detailed information on features such as [load tasks into namespace], and setup [auto complete with namespace] for your shell.
718
+ See the [Markdown reference](./docs/reference.md) for more detailed information on features such as [load tasks into namespace], and setup [auto complete with namespace] for your shell.
710
719
 
711
720
  ## License
712
721
 
713
722
  Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0)
714
723
 
715
- [ci-shield]: https://github.com/jchip/fynjs/actions/workflows/ci.yml/badge.svg
716
- [ci-url]: https://github.com/jchip/fynjs/actions/workflows/ci.yml
717
- [npm-image]: https://badge.fury.io/js/%40fynjs%2Frun.svg
718
- [npm-url]: https://npmjs.org/package/@fynjs/run
719
724
  [npm scripts]: https://docs.npmjs.com/misc/scripts
720
725
  [bash]: https://www.gnu.org/software/bash/
721
726
  [zsh]: http://www.zsh.org/
722
- [load tasks into namespace]: REFERENCE.md#loading-task
723
- [auto complete with namespace]: REFERENCE.md#auto-complete-with-namespace
727
+ [load tasks into namespace]: ./docs/reference.md#loading-tasks
728
+ [auto complete with namespace]: ./docs/reference.md#auto-complete-with-namespace
724
729
  [npm]: https://www.npmjs.com/package/npm
725
730
  [node.js stream]: https://nodejs.org/api/stream.html
731
+
732
+ [license-image]: https://img.shields.io/npm/l/@fynjs/run.svg
733
+ [license-url]: LICENSE
734
+ [build-image]: https://github.com/jchip/fynjs/actions/workflows/ci.yml/badge.svg
735
+ [build-url]: https://github.com/jchip/fynjs/actions/workflows/ci.yml
736
+ [downloads-image]: https://img.shields.io/npm/dm/@fynjs/run.svg
737
+ [downloads-url]: https://npm-stat.com/charts.html?package=%40fynjs%2Frun
738
+ [npm-badge-png]: https://nodei.co/npm/@fynjs/run.png?downloads=true&stars=true
739
+ [package-url]: https://npmjs.com/package/@fynjs/run
@@ -0,0 +1,1225 @@
1
+ # @fynjs/run reference
2
+
3
+ `@fynjs/run` runs npm scripts, shell commands and JavaScript tasks, serially or concurrently, from the `xrun` command or from code. It installs two bins, `xrun` and `qrun` (`xrun` with logs silenced).
4
+
5
+ ## Imports
6
+
7
+ The package root has one export, the shared `xrun` instance. Every API below is a property or method of that instance.
8
+
9
+ ```js
10
+ // ESM
11
+ import xrun from "@fynjs/run";
12
+
13
+ // CommonJS: require() returns the instance itself, not a namespace
14
+ const xrun = require("@fynjs/run");
15
+ ```
16
+
17
+ - There are no named exports. `import { load, serial } from "@fynjs/run"` does not work. Use `xrun.load` and `xrun.serial`.
18
+ - The root default export is typed `any`.
19
+ - The instance also carries the classes `xrun.XRun` (alias `xrun.XClap`), `xrun.XTaskSpec` and `xrun.XReporterConsole`.
20
+ - `@fynjs/run/cli/xrun` exports `xrunMain` as its default, see [`xrunMain`](#xrunmain). `@fynjs/run/package.json` is also exported.
21
+ - The package is ESM (`"type": "module"`). It needs Node `^22.22.2 || ^24.15.0 || >=26.0.0`.
22
+
23
+ ## `xrun` and `qrun` commands
24
+
25
+ ```
26
+ xrun [options] [--] [task1 [task options] task2 [task options] ...]
27
+ ```
28
+
29
+ `qrun` sets `XRUN_QUIET=1` and then behaves exactly like `xrun`.
30
+
31
+ When a project has its own installed `@fynjs/run`, the bin runs that copy rather than the one it lives in. A copy is looked up from the current directory first, then from the bin file.
32
+
33
+ ### CLI Options
34
+
35
+ Every option defined by the CLI is listed here. Booleans accept `--name`, `--name=false` and `--no-name`.
36
+
37
+ | Option | Alias | Type | Default | Behavior |
38
+ | --- | --- | --- | --- | --- |
39
+ | `--cwd <path>` | `-w` | string | process CWD | Set xrun's CWD. Passing it turns off the upward search for a task file. |
40
+ | `--dir <path>` | `-d` | string | CWD | Directory, relative to CWD, to look for the task file. |
41
+ | `--npm [bool]` | `-n` | boolean | `true` | Load `package.json` scripts into namespace `npm`. `--no-npm` disables. |
42
+ | `--nmbin [bool]` | `-b` | boolean | `true` | Add `CWD/node_modules/.bin` to the front of `PATH`, if that directory exists. |
43
+ | `--list [namespaces]` | `-l` | string | all namespaces | List task names from comma separated namespaces, then exit 0. |
44
+ | `--full` | `-f` | count | 0 | With `--list`, print full names: `ns/name`, and `/name` for the root namespace. Repeat (`-ff`) to also put a leading `/` on every name. |
45
+ | `--ns` | `-m` | boolean | `false` | List all namespaces, then exit 0. |
46
+ | `--soe [mode]` | `-e` | `no`, `soft`, `full` | `full` | Stop on error mode, see [Stop on Error Modes](#stop-on-error-modes). `no` means `""`. Any other value fails an assertion. |
47
+ | `--quiet [bool]` | `-q` | boolean | `false` | Suppress xrun's own logs. When not given, the value of env `XRUN_QUIET` (`"1"` means quiet) is used. When given, it sets `XRUN_QUIET=1` for nested runs. |
48
+ | `--serial [bool]` | `-s`, `-x`, `--ser` | boolean | `false` | Run the tasks named on the command line serially. |
49
+ | `--concurrent [bool]` | `-c`, `--conc` | boolean | `true` | Run the tasks named on the command line concurrently. This is the default. |
50
+ | `--env <K=V...>` | none | string array | none | Set env vars in `process.env` before running tasks, ie: `--env NODE_ENV=development FOO=BAR -.`. Only text up to the next `=` is kept as the value. |
51
+ | `--require <modules...>` | `-r` | string array | none | Load these modules for tasks instead of the task file. Resolved from CWD, loaded in the order given. |
52
+ | `--help` | `-h`, `-?` | | | Added by the argument parser. |
53
+ | `--version` | `-V`, `-v` | | | Added by the argument parser. |
54
+ | `--options` | none | | | Only recognized as the sole argument (`xrun --options`). Prints `--name` and `-alias` for each option above, for shell auto completion, then exits 0. |
55
+
56
+ Notes:
57
+
58
+ - `--serial` is applied only when there is more than one task on the command line.
59
+ - With no task on the command line, xrun prints the task list and usage, then exits 1.
60
+ - If no task is found at all, xrun prints a diagnostic and exits 1.
61
+ - The options of the xrun command come first. Options after a task name belong to that task, see [Task Options](#task-options).
62
+
63
+ ### Task Execution
64
+
65
+ Tasks can be executed in various ways:
66
+
67
+ ```bash
68
+ # Run single task
69
+ xrun build
70
+
71
+ # Run multiple tasks concurrently
72
+ xrun lint test
73
+
74
+ # Run multiple tasks serially
75
+ xrun --serial build test deploy
76
+
77
+ # Run namespaced task
78
+ xrun npm/test
79
+ xrun pkg/hello
80
+
81
+ # Optional task execution (won't fail if task not found)
82
+ xrun ?optional-task
83
+ ```
84
+
85
+ Task name rules:
86
+
87
+ - A leading `?` makes the task optional. A missing optional task is skipped.
88
+ - `ns/name` selects a namespace. A leading `/` alone selects the root namespace. See [Namespace](#namespace).
89
+ - When a `--` follows, see [Argument Passing](#argument-passing).
90
+ - A leading `/` on a name that has a second `/` is stripped, so `xrun /ns/name` runs `ns/name`.
91
+
92
+ ### Argument Passing
93
+
94
+ Arguments after `--` are appended to the shell command of the task named last on the command line. This applies only when that task is itself a shell command task. It does not reach shell commands inside an array task.
95
+
96
+ ```bash
97
+ xrun test -- --grep "specific test"
98
+ xrun build -- --watch --verbose
99
+ ```
100
+
101
+ Appended arguments that contain a space are wrapped in double quotes.
102
+
103
+ Options and arguments written after a task name, before `--`, are also appended to the shell command of that task. For a function task they are parsed into the [execution context](#execution-context).
104
+
105
+ ### Array Tasks from Command Line
106
+
107
+ You can specify complex task arrays directly from the command line:
108
+
109
+ ```bash
110
+ # Concurrent execution
111
+ xrun [task1, task2, task3]
112
+
113
+ # Serial execution with --serial
114
+ xrun --serial [task1, task2, task3]
115
+
116
+ # Mixed execution (serial then concurrent)
117
+ xrun --serial [task1, task2, [task3, task4]]
118
+
119
+ # As a quoted string
120
+ xrun "[task1, task2, [task3, task4]]"
121
+ ```
122
+
123
+ If the first argument starts with `[`, all arguments are joined with a space and parsed as an array. A parse failure prints `Parsing array of tasks failed` and the run does not start.
124
+
125
+ ### Task File Discovery
126
+
127
+ Unless `--require` is used, xrun looks for a task file in the search directory, which is CWD joined with `--dir`.
128
+
129
+ 1. A directory entry is a match if its name starts with one of these prefixes, tried in this order: `xrun-tasks`, `xrun`, `xclap.`, `clapfile.`, `clap.`, `gulpfile.`. Any extension matches.
130
+ 2. If the directory has no match, the subdirectories `scripts`, `tools`, `build` and `tasks` are checked the same way.
131
+ 3. If still not found, and `--cwd` was not given, xrun moves to the parent directory and repeats. It stops at the first directory that has a `package.json`, or at the file system root.
132
+ 4. When a file is found, xrun changes its CWD to the directory where it was found.
133
+
134
+ The file is loaded with `import()`, so a task file can use top-level await. Files ending in `.ts`, `.tsx`, `.mts` or `.cts` first start a TypeScript runner, see [TypeScript Support](#typescript-support).
135
+
136
+ What a task file or `--require` module may export:
137
+
138
+ - A function. It is called with the `xrun` instance and should call `xrun.load`.
139
+ - An object. A `default` export is processed the same way. Otherwise a non-empty object is loaded into namespace `xrun`.
140
+ - Nothing. A file that calls `xrun.load` itself needs no export.
141
+
142
+ The task file name `xrun-tasks.js` is the default name used in messages. `--require` modules that cannot be resolved are logged as errors and skipped.
143
+
144
+ ### `xrunMain`
145
+
146
+ ```ts
147
+ xrunMain(argv?: string[], offset?: number, xrunPath?: string, done?: (err?: Error) => void): Promise<unknown>
148
+ ```
149
+
150
+ The default export of `@fynjs/run/cli/xrun`, also what the `xrun` bin calls. It is async. A caller must `await` it to observe the run.
151
+
152
+ - With no `argv`, it parses `process.argv` from index 2. Otherwise it parses `argv` from `offset`.
153
+ - Without `done`, xrun exits the process on listing, help and error conditions. With `done`, it calls `done` with an error that carries `exitCode`, even for exit code 0.
154
+
155
+ ## Provider Packages
156
+
157
+ Provider packages allow you to create reusable task libraries that can be shared across projects.
158
+
159
+ ### Using Provider Packages
160
+
161
+ Provider packages are searched in the dependencies of CWD's `package.json` (`optionalDependencies`, `devDependencies` and `dependencies`) when:
162
+
163
+ 1. No task file or `--require` module was loaded (automatic discovery), or
164
+ 2. The `package.json` has `"loadProviderModules": true` under its `@fynjs/run` key, see [Package.json Configuration](#packagejson-configuration).
165
+
166
+ A dependency is treated as a provider if either is true:
167
+
168
+ - Its `package.json` has an `xrunProvider` field.
169
+ - Its own `dependencies` list `@fynjs/run` (or the older name `@xarc/run`).
170
+
171
+ For each provider, xrun requires the module and calls its `loadTasks` export with the `xrun` instance. A provider without a `loadTasks` function is ignored. The module required is the package name, or `<name>/<module>` if `xrunProvider.module` is set.
172
+
173
+ #### Installation and Configuration
174
+
175
+ 1. Install the provider package:
176
+
177
+ ```bash
178
+ fyn add --dev @my-org/build-tasks
179
+ ```
180
+
181
+ 2. (Optional) Explicitly enable provider module loading in your `package.json`:
182
+
183
+ ```json
184
+ {
185
+ "@fynjs/run": {
186
+ "loadProviderModules": true
187
+ }
188
+ }
189
+ ```
190
+
191
+ 3. Create an `xrun-tasks.js` to use the provider:
192
+
193
+ ```js
194
+ const { loadTasks } = require("@my-org/build-tasks");
195
+ loadTasks();
196
+
197
+ // You can also load your own tasks
198
+ const xrun = require("@fynjs/run");
199
+ xrun.load({
200
+ "my-custom-task": "echo hello"
201
+ });
202
+ ```
203
+
204
+ ### Creating Provider Packages
205
+
206
+ To create a provider package:
207
+
208
+ 1. Create a module that exports a `loadTasks` function:
209
+
210
+ ```js
211
+ // index.js of your provider package
212
+ function loadTasks(xrun) {
213
+ const runner = xrun || require("@fynjs/run");
214
+
215
+ runner.load("build", {
216
+ compile: "babel src -d lib",
217
+ bundle: "webpack --mode production",
218
+ clean: "rimraf lib dist"
219
+ });
220
+
221
+ runner.load("test", {
222
+ unit: "jest",
223
+ e2e: "cypress run",
224
+ coverage: "nyc npm run test:unit"
225
+ });
226
+ }
227
+
228
+ module.exports = { loadTasks };
229
+ ```
230
+
231
+ 2. Mark the package as a provider so xrun finds it. Either list `@fynjs/run` in its `dependencies`, or add an `xrunProvider` field:
232
+
233
+ ```json
234
+ {
235
+ "name": "@my-org/build-tasks",
236
+ "main": "index.js",
237
+ "xrunProvider": {
238
+ "module": "lib/tasks.js"
239
+ }
240
+ }
241
+ ```
242
+
243
+ `xrunProvider.module` is optional. It is a path inside the package.
244
+
245
+ ## TypeScript Support
246
+
247
+ xrun loads TypeScript task files after starting a runner. It tries these in order and uses the first that loads:
248
+
249
+ 1. `@fynjs/ts-resolve/register`. A resolve hook feeding Node's own type stripping. Needs Node >= 22.15.
250
+ 2. `ts-node/register/transpile-only`.
251
+
252
+ Neither is a dependency of `@fynjs/run`. Install one in the project. If neither loads, xrun logs `Unable to load a typescript runner` with each error. `tsx` is not used by xrun.
253
+
254
+ ### Supported Files
255
+
256
+ - `xrun-tasks.ts` - TypeScript task file
257
+ - `xrun-tasks.mts` - TypeScript ES modules
258
+ - `xrun-tasks.cts` - TypeScript CommonJS
259
+ - `xrun-tasks.tsx` - the runner starts, but whether the file loads depends on the runner
260
+
261
+ Node's type stripping cannot transform enums, namespaces or parameter properties. A file using them fails with a message naming the limit. Rewrite them, or run Node with `--experimental-transform-types`.
262
+
263
+ ### Example TypeScript Task File
264
+
265
+ ```typescript
266
+ import xrun from "@fynjs/run";
267
+
268
+ interface BuildOptions {
269
+ production?: boolean;
270
+ watch?: boolean;
271
+ }
272
+
273
+ const buildTask = (options: BuildOptions = {}) => {
274
+ const mode = options.production ? "production" : "development";
275
+ const watchFlag = options.watch ? "--watch" : "";
276
+ return xrun.exec(`webpack --mode ${mode} ${watchFlag}`);
277
+ };
278
+
279
+ xrun.load({
280
+ build: buildTask(),
281
+ "build:prod": buildTask({ production: true }),
282
+ "build:watch": buildTask({ watch: true }),
283
+
284
+ test: xrun.concurrent(xrun.exec("jest"), xrun.exec("eslint src/")),
285
+
286
+ ci: xrun.serial("test", "build:prod")
287
+ });
288
+ ```
289
+
290
+ ## Package.json Configuration
291
+
292
+ xrun reads one config object from the `package.json` in CWD. The key is the first one present of `xclap`, `xrun`, `@fynjs/run`. Only two fields are used. `cwd` is ignored.
293
+
294
+ | Field | Type | Default | Behavior |
295
+ | --- | --- | --- | --- |
296
+ | `tasks` | object | none | Tasks loaded into namespace `pkg`. |
297
+ | `loadProviderModules` | boolean | `false` | Search dependencies for [provider packages](#provider-packages) even when a task file loaded. |
298
+
299
+ Other CLI options, such as `npm` or `soe`, are not read from `package.json`. Set them on the command line.
300
+
301
+ ### Task Definition in Package.json
302
+
303
+ You can define simple tasks directly in `package.json` without JavaScript capability:
304
+
305
+ ```json
306
+ {
307
+ "name": "my-app",
308
+ "@fynjs/run": {
309
+ "tasks": {
310
+ "hello": "echo hello from package.json",
311
+ "build": "webpack --mode production",
312
+ "test": ["lint", "unit-test"],
313
+ "deploy": {
314
+ "desc": "Deploy to production",
315
+ "task": ["build", "upload", "notify"]
316
+ }
317
+ }
318
+ }
319
+ }
320
+ ```
321
+
322
+ These tasks are loaded into the `pkg` namespace and can be invoked as:
323
+
324
+ - `xrun pkg/hello`
325
+ - `xrun hello` (if no conflicts with other namespaces)
326
+
327
+ ### npm Scripts
328
+
329
+ Unless `--no-npm` is given, the `scripts` of the nearest `package.json` (searched upward from CWD) are loaded into namespace `npm`.
330
+
331
+ - Each script runs as a shell command with the `npm` flag, which implies `tty` and `spawn`.
332
+ - The environment gets `npm_lifecycle_event`, `npm_lifecycle_script`, `npm_package_name`, `npm_package_version`, `npm_package_json`, `npm_execpath`, `npm_node_execpath`, `NODE`, `INIT_CWD` and `PWD`.
333
+ - A script whose name does not start with `pre` or `post` runs as a serial array of `pre<name>` (if defined), the script, and `post<name>` (if defined).
334
+ - A script whose name starts with `pre` or `post` is loaded as a plain script, with no hooks of its own.
335
+
336
+ ## Creating Tasks
337
+
338
+ Tasks are defined in an object, for example:
339
+
340
+ ```js
341
+ const tasks = {
342
+ xfoo1: `echo "a direct shell command xfoo1"`,
343
+ xfoo2: `echo "a direct shell command xfoo2"`,
344
+ xfoo3: `echo "a direct shell command xfoo3"`,
345
+ xfoo4: () => console.log("hello, this is xfoo4"),
346
+ foo2: ["xfoo1", "xfoo2", "xfoo3", "xfoo4"],
347
+ foo3: {
348
+ desc: "description for task foo3",
349
+ task: () => {
350
+ console.log("function task for foo3");
351
+ }
352
+ }
353
+ };
354
+ ```
355
+
356
+ ## Loading Tasks
357
+
358
+ Tasks can be loaded with `xrun.load`. You can specify a namespace for the tasks. See [`load`](#loadnamespace-tasks-priority).
359
+
360
+ ```js
361
+ import xrun from "@fynjs/run";
362
+ xrun.load(tasks);
363
+ // or load into a namespace
364
+ xrun.load("myapp", tasks);
365
+ ```
366
+
367
+ ## Task Definition
368
+
369
+ ### Direct Action Task
370
+
371
+ Ultimately, a task would eventually resolve to some kind of runnable action that's either a function, a shell string, or a list of other tasks to run.
372
+
373
+ A task can define its direct action as one of:
374
+
375
+ - [A string](#string) - as a shell command to be spawned, or as a [string that contains an array](#string-array)
376
+ - [An array](#array) - list of tasks to be processed and execute [serially](#serially) or [concurrently](#concurrently).
377
+ - [A function](#function) - to be called, which can return more tasks to execute.
378
+ - [Task Spec](#task-spec) - created using the [exec API](#execspec-options), as a shell command to run.
379
+
380
+ ### A Task Object
381
+
382
+ To allow decorating a task with more information such as name and description, the task definition can be an [object](#object), which should contain a `task` field that defines a [direct action task](#direct-action-task).
383
+
384
+ ### String
385
+
386
+ - A string primarily is executed as a shell command.
387
+ - A string [started with `"~["`](#string-array) is parsed into an [array task](#array).
388
+
389
+ ```js
390
+ {
391
+ foo: "echo hello";
392
+ }
393
+ ```
394
+
395
+ `xrun foo` will cause the shell command `echo hello` to be spawned.
396
+
397
+ Shell commands run through `child_process.exec` unless a [flag](#shell-task-flags) says otherwise. Arguments from the command line or from [inline task options](#inline-task-options) are appended to the command.
398
+
399
+ These environment variables are defined for a shell command, mainly for the [`finally`](#finally-hook) hook:
400
+
401
+ - `XRUN_ERR` - If the task failed, this contains the error message.
402
+ - `XRUN_FAILED` - If any task failed, this is set to `true`.
403
+
404
+ `XCLAP_ERR` and `XCLAP_FAILED` are set to the same values for older scripts.
405
+
406
+ #### String Array
407
+
408
+ If a string task starts with `"~["` then it's parsed as an array with string elements and executed as [array task](#array).
409
+
410
+ For example:
411
+
412
+ ```js
413
+ {
414
+ foo: "~[ foo1, foo2, foo3 ]";
415
+ }
416
+ ```
417
+
418
+ Will be the same as specifying `foo: [ "foo1", "foo2", "foo3" ]` and processed as [array task](#array).
419
+
420
+ ### Array
421
+
422
+ If the task is an array, then it can contain elements that are strings, functions or task specs.
423
+
424
+ - Functions are treated as a [task function](#function) to be called.
425
+ - Strings in a task array are primarily treated as name of another task to look up and execute.
426
+ - String started with `"~$"` or `"~@"` are treated as [anonymous shell commands](#anonymous-string-shell-command) to be executed.
427
+
428
+ The [array serial/concurrent rules](#array-serialconcurrent-rules) will be applied.
429
+
430
+ ```js
431
+ {
432
+ foo: ["foo1", "foo2", "foo3"];
433
+ }
434
+ ```
435
+
436
+ `xrun foo` will cause the three tasks `foo1`, `foo2`, `foo3` to be executed **_serially_**.
437
+
438
+ #### Anonymous String Shell Command
439
+
440
+ If the task name in a task array starts with `"~$"` or `"~@"` then the rest of it is executed as an anonymous shell command directly.
441
+
442
+ For example:
443
+
444
+ ```js
445
+ {
446
+ foo: ["foo1", "~$echo hello"];
447
+ }
448
+ ```
449
+
450
+ Will cause the task `foo1` to be executed and then the shell command `echo hello` to be executed.
451
+
452
+ ##### Shell Task Flags
453
+
454
+ Any string that's to be a shell command can have flags like this:
455
+
456
+ ```js
457
+ {
458
+ foo: `~(tty)$node -e "console.log('isTTY', process.stdout.isTTY)"`;
459
+ }
460
+ ```
461
+
462
+ - The leading part `~(tty)$` is specifying a shell command with flags `(tty)`. The closing can be `)$` or `)@`.
463
+ - Multiple flags can be specified like this: `~(tty,sync)$`.
464
+ - Flag names are case insensitive. An unknown flag fails the task with `Unknown flag`. A missing closing `)$` fails the task with `Missing )$`.
465
+
466
+ These are supported flags:
467
+
468
+ - `tty` - Use [child_process.spawn] to launch the shell command with TTY control. **WARNING** Only one task at a time can take over the TTY.
469
+ - `spawn` - Use [child_process.spawn] API instead of [child_process.exec] to launch the shell process. TTY control is not given.
470
+ - `sync` - If either `tty` or `spawn` flag exist, then use [child_process.spawnSync] API. This will cause concurrent tasks to wait. It has no effect alone.
471
+ - `noenv` - Do not pass `process.env` to child process.
472
+ - `npm` - Implies `tty` and `spawn`. Used for npm scripts.
473
+
474
+ ### Function
475
+
476
+ ```js
477
+ {
478
+ foo: function (context) { return Promise.resolve("hello"); }
479
+ }
480
+ ```
481
+
482
+ `xrun foo` will cause the function to be called.
483
+
484
+ The `this` context for the function will be the xrun [Execution Context](#execution-context), also passed as the first argument. If you don't want to use `this`, then you can use arrow functions for your task.
485
+
486
+ How a function signals completion:
487
+
488
+ - Two parameters: `(context, done)`. The task ends when `done(err, value)` is called.
489
+ - One parameter on a non-async function: if the parameter is named `ctx` or `context`, it receives the context. Any other name receives the `done` callback, and the task ends when `done` is called. xrun decides this by reading the function source text.
490
+ - Otherwise the function gets the context and its return value decides, see below.
491
+
492
+ The function can return:
493
+
494
+ - `Promise` - `xrun` will await for the promise.
495
+ - [node.js stream] - `xrun` will wait for the stream to close or error.
496
+ - `array` - `xrun` will treat the array as a list of tasks to be executed
497
+ - The [array serial/concurrent rules](#array-serialconcurrent-rules) applied to the array.
498
+ - The [anonymous shell command](#anonymous-string-shell-command) rule applied to each string element.
499
+ - `string` - `xrun` will treat the string as a task name, an [anonymous shell command to be executed](#anonymous-string-shell-command), or a `"~["` string array.
500
+ - `function` - `xrun` will call the function as another task function.
501
+ - a [task spec](#task-spec) or the value of [`stop()`](#stop) - run as a task.
502
+ - a child process, or an object with a `child` field that is a child process - the child is tracked so it can be killed on stop.
503
+ - `undefined` or any other value - the task is done as soon as the function returns.
504
+
505
+ An error thrown by the function, or a rejected promise, fails the task. A child process killed with SIGTERM is not treated as a failure.
506
+
507
+ ### Task Spec
508
+
509
+ A direct action task can also be defined as a task spec.
510
+
511
+ Right now the supported spec types are a shell command, created with [`exec`](#execspec-options), and an environment update, created with [`env`](#envspec-options).
512
+
513
+ This is a more systematic approach to declare an [anonymous string shell command](#anonymous-string-shell-command).
514
+
515
+ A task spec shell command can be declared as the task or a task in the array:
516
+
517
+ ```js
518
+ const xrun = require("@fynjs/run");
519
+
520
+ const tasks = {
521
+ hello: xrun.exec("echo hello"),
522
+ foo: [xrun.exec("echo foo"), xrun.exec("echo bar")]
523
+ };
524
+
525
+ xrun.load(tasks);
526
+ ```
527
+
528
+ ### Object
529
+
530
+ You can define your task as an object in order to specify more information.
531
+
532
+ For example:
533
+
534
+ ```js
535
+ {
536
+ task1: {
537
+ desc: "description",
538
+ task: <task-definition>,
539
+ dep: <task-definition>,
540
+ finally: <finally-hook-definition>
541
+ }
542
+ }
543
+ ```
544
+
545
+ Where:
546
+
547
+ | Field | Type | Behavior |
548
+ | --- | --- | --- |
549
+ | `desc` | string | Description, shown in the task list. An object task without `desc` is listed as a task with no description. |
550
+ | `task` | direct action | Defines a [direct action task](#direct-action-task). |
551
+ | `dep` | direct action | Runs before `task`. When it is an array it runs serially. |
552
+ | `finally` | direct action | Always runs after the task finishes or fails, see [finally hook](#finally-hook). |
553
+ | `flags` | object | Shell task flags to merge in, ie: `{ tty: true }`. Applies when `task` is a shell command. |
554
+ | `options` | object | Options for `child_process` (ie: `env`, `cwd`) used when `task` is a shell command. |
555
+ | `argOpts` | object | CLI options this task accepts, see [Task Options](#task-options). |
556
+ | `cliParser` | object | `{ options, subCommands, allowUnknownOption }`. Full option and sub command spec for the task. `options` is used when `argOpts` is absent. |
557
+ | `allowUnknownOption` | boolean | Default `true`. Set `false` to fail on options the task does not define. `cliParser.allowUnknownOption` takes precedence. |
558
+
559
+ #### finally hook
560
+
561
+ When defining task as an object, you can have a `finally` property that defines [direct action task](#direct-action-task) which is always executed after the task completes or fails. Generally for doing clean up chores.
562
+
563
+ Note that the finally hook is processed the same way as a task. Other tasks that are referenced by the `finally` hook will not have their `finally` hook invoked.
564
+
565
+ The task's error is available as `context.err` in a function hook, and as `XRUN_ERR` in a shell hook. Hooks do not run after [`stop()`](#stop) was invoked.
566
+
567
+ If you set `stopOnError` to `full`, then be careful if you have concurrent running tasks, because `full` stop immediately abandon all pending async sub tasks, but since xrun can't reliably cancel them, they could be continuing to run, and therefore could cause concurrent conflict with your finally hook. xrun emits a `warn-finally` event when any loaded task has a `finally` and the mode is `full`.
568
+
569
+ If you have async task, it's best you set [`stopOnError`](#stoponerror) to `soft`.
570
+
571
+ ## Array serial/concurrent rules
572
+
573
+ When you define a task as an array, it should contain a list of task names to be executed serially or concurrently.
574
+
575
+ Generally, the array of tasks is executed concurrently, and only serially when [certain conditions](#serially) are true.
576
+
577
+ An task array can also be explicitly created as concurrent using the [concurrent API](#concurrenttasks--task1-task2-taskn)
578
+
579
+ ### Serially
580
+
581
+ Each task in the array is executed serially if one of the following is true:
582
+
583
+ - The array is defined at the [top level](#top-level).
584
+ - The array is created by the [serial API](#serialtasks--task1-task2-taskn).
585
+ - The first element of the array is [`"."`](#first-element-dot) **DEPRECATED** use the [serial API](#serialtasks--task1-task2-taskn) instead. The first element `"-s"`, `"--serial"` or `"--ser"` has the same effect.
586
+
587
+ #### Using serial API
588
+
589
+ Create an array of serial tasks within another concurrent array:
590
+
591
+ ```js
592
+ const xrun = require("@fynjs/run");
593
+
594
+ const tasks = {
595
+ foo: xrun.concurrent("a", xrun.serial("foo1", "foo2", "foo3"))
596
+ };
597
+
598
+ xrun.load(tasks);
599
+ ```
600
+
601
+ #### top level
602
+
603
+ At top level, an array of task names will be executed serially. Top level means the array is the `task` of a task object or the task definition itself. An array returned by a function, or passed to `run`, is not top level.
604
+
605
+ ```js
606
+ const tasks = {
607
+ foo: ["foo1", "foo2", "foo3"]
608
+ };
609
+
610
+ xrun.load(tasks);
611
+ ```
612
+
613
+ #### First element dot
614
+
615
+ > **DEPRECATED** - Please use the [serial API](#serialtasks--task1-task2-taskn) instead.
616
+
617
+ If the first element of the array is `"."` then the rest of tasks in the array will be executed serially.
618
+
619
+ ```js
620
+ {
621
+ foo: ["bar", [".", "foo1", "foo2", "foo3"]];
622
+ }
623
+ ```
624
+
625
+ #### Concurrently
626
+
627
+ By default, an ordinary array of tasks is executed concurrently, except when it's defined at the [top level](#top-level). A first element of `"--concurrent"`, `"-c"` or `"--conc"` also marks an array concurrent.
628
+
629
+ If you need to have an array of tasks at the top level to execute concurrently, use the [concurrent API](#concurrenttasks--task1-task2-taskn) to create it.
630
+
631
+ ```js
632
+ const xrun = require("@fynjs/run");
633
+
634
+ const tasks = {
635
+ foo: xrun.concurrent("foo1", "foo2", "foo3")
636
+ };
637
+
638
+ xrun.load(tasks);
639
+ ```
640
+
641
+ > `xrun foo` will execute tasks `foo1`, `foo2`, and `foo3` **_concurrently_**.
642
+
643
+ ## Namespace
644
+
645
+ A group of tasks can be assigned a namespace and allows you to have tasks with the same name so you can modify certain tasks without replacing them.
646
+
647
+ For example:
648
+
649
+ ```js
650
+ xrun.load([namespace], tasks);
651
+ ```
652
+
653
+ You refer to the namespaces with `/`, ie: `ns/foo`.
654
+
655
+ Anything that was loaded without a namespace is assigned to the default namespace `/`, which can be accessed with a simple leading `/`, ie: `/foo`.
656
+
657
+ If you run a task without specifying the namespace, then it's searched through all namespaces until it's found. The default namespace is the first one to search. The rest follow their [priority and override order](#namespace-overrides), and namespaces of equal priority are searched in the order they were loaded.
658
+
659
+ > For obvious reasons, this means task names cannot contain `/`.
660
+
661
+ Namespaces created by xrun itself: `npm` (npm scripts), `pkg` (`package.json` tasks), and `xrun` (object exports of a task file).
662
+
663
+ ### Auto Complete with namespace
664
+
665
+ To assist shell auto completion, you may specify all namespaces with a leading `/` when invoking from the command line. It will be stripped before xrun runs them.
666
+
667
+ ie:
668
+
669
+ ```bash
670
+ $ xrun /foo/bar
671
+ ```
672
+
673
+ That way, you can press `tab` after the first `/` to get auto completion with namespaces.
674
+
675
+ ### Namespace Overrides
676
+
677
+ Namespaces can be configured with priority and override relationships. A namespace searched earlier wins when two have a task of the same name.
678
+
679
+ ```js
680
+ // namespace "test" is searched before namespace "build"
681
+ xrun.load({ namespace: "test", overrides: "build" }, tasks);
682
+
683
+ // a higher priority value is searched earlier
684
+ xrun.load("hot", tasks, 5);
685
+ ```
686
+
687
+ - Each namespace starts with its `priority` (default `1`). A higher value is searched earlier.
688
+ - `overrides` is a namespace name or an array of names. A namespace is raised above every namespace it overrides.
689
+ - Namespaces with equal values keep load order.
690
+ - Calling `load` again for the same namespace adds to its `overrides`.
691
+ - Mutual overrides between two namespaces throw `circular namespace override`.
692
+
693
+ ## Execution Context
694
+
695
+ A continuous execution context is maintained from the top whenever you invoke a task with `xrun <n>`.
696
+
697
+ The execution context is passed to any task function as the first parameter and as `this`. It has the following properties:
698
+
699
+ - `run` - a function to run another task, `run(tasks, callback?)`
700
+ - `argv` - arguments to the task (array)
701
+ - `cliCmd` - the CLI command object from argument parser
702
+ - `argp` - parsed arguments metadata (JSON format)
703
+ - `args` - parsed arguments list
704
+ - `argOpts` - parsed CLI options for the task (object)
705
+ - `err` - For the [`finally`](#finally-hook) hook, if task failed, this would be the error
706
+ - `failed` - The first task failure error, or `null`. Further errors are in its `more` array.
707
+
708
+ You can run more tasks under the same context with `this.run` or `context.run`:
709
+
710
+ - run a single task
711
+
712
+ - `this.run("task_name")`
713
+ - `context.run("task_name")`
714
+
715
+ - execute tasks serially.
716
+
717
+ - `this.run(xrun.serial("name1", "name2", "name3"))`
718
+ - `this.run([ ".", "name1", "name2", "name3"])`
719
+
720
+ - execute tasks concurrently
721
+ - `this.run(["name1", "name2", "name3"])`
722
+
723
+ The tasks given to `run` start after the calling function finishes. The optional `callback(err, data)` is called when they complete.
724
+
725
+ For example:
726
+
727
+ ```js
728
+ const tasks = {
729
+ bar: {},
730
+ foo: function(context) {
731
+ console.log("hello from foo");
732
+ context.run("bar");
733
+ }
734
+ };
735
+ ```
736
+
737
+ ### Task Options
738
+
739
+ The execution context also has `argv` which is an array of the task options. The first one is the name as used to invoke the task.
740
+
741
+ Examples:
742
+
743
+ - `xrun foo` - argv: `["foo"]`
744
+ - `xrun foo --bar` - argv: `["foo", "--bar"]`
745
+ - `xrun ?foo --bar --woo` - argv: `["?foo", "--bar", "--woo"]`
746
+ - `xrun ?ns/foo --bar` - argv: `["?ns/foo", "--bar"]`
747
+
748
+ For a shell command task, the options after the task name, minus the task name itself, are appended to the command.
749
+
750
+ For a function task, `context.argOpts` holds the parsed options. Declare the options a task accepts with `argOpts` in the task object:
751
+
752
+ ```js
753
+ const tasks = {
754
+ foo: function(context) {
755
+ console.log(context.argv, context.argOpts);
756
+ },
757
+ boo: {
758
+ desc: "show argv from task options",
759
+ argOpts: {
760
+ level: { alias: "l", args: "<val string>", desc: "log level" },
761
+ verbose: { alias: "v" }
762
+ },
763
+ task: function(context) {
764
+ console.log(context.argv, context.argOpts);
765
+ }
766
+ }
767
+ };
768
+ ```
769
+
770
+ - An option spec uses the `@fynjs/cli-args` format, ie: `{ alias, args, desc, required }`.
771
+ - These older forms are also accepted: `require: true`, `type: "string"` (also `number`, `boolean`, `float`, `int`, `integer`), `type: "array"`, `type: "count"`.
772
+ - Options not in `argOpts` are accepted by default. If `allowUnknownOption` is `false`, they fail the task with `Unknown options for task <name>: ...`.
773
+ - An option parsing error fails the task with `Error parsing options for task <name>: ...`.
774
+
775
+ #### Inline Task Options
776
+
777
+ Task options can be specified inline in the task definition also, not just in command line.
778
+
779
+ Only the first part separated by space `" "` is used as the task name.
780
+
781
+ For example,
782
+
783
+ ```js
784
+ const tasks = {
785
+ foo: function(context) {
786
+ console.log(context.argv);
787
+ },
788
+ zoo: "foo --bar --woo",
789
+ moo: ["?bad", "foo --bar --woo"]
790
+ };
791
+ ```
792
+
793
+ ### Accessing Arguments
794
+
795
+ Options written after a task name on the command line are parsed for that task:
796
+
797
+ ```js
798
+ const tasks = {
799
+ test: function(context) {
800
+ console.log("Parsed options:", context.argOpts);
801
+ // If called with: xrun test --watch --verbose
802
+ // context.argOpts would be: { watch: true, verbose: true }
803
+ }
804
+ };
805
+ ```
806
+
807
+ The `--` remainder is not part of the context. It is only appended to a shell command, see [Argument Passing](#argument-passing).
808
+
809
+ ## Error Handling
810
+
811
+ ### Environment Variables
812
+
813
+ xrun sets these environment variables for shell commands:
814
+
815
+ - `XRUN_ERR` - If task failed, this contains the error message
816
+ - `XRUN_FAILED` - If any task failed, this is set to `true`
817
+
818
+ These are particularly useful in [finally hooks](#finally-hook) for cleanup based on execution results.
819
+
820
+ The `xrun` command sets or reads these variables in `process.env`:
821
+
822
+ | Variable | Behavior |
823
+ | --- | --- |
824
+ | `XRUN_ID` | Set to `1`, or incremented when xrun runs inside another xrun. |
825
+ | `FORCE_COLOR` | Set to `1` if not already set. |
826
+ | `XRUN_QUIET` | `1` means quiet. Set by `qrun` and by `--quiet`. Read when `--quiet` is not given. |
827
+ | `XRUN_CWD` | CWD of the run. |
828
+ | `XRUN_INIT_CWD` | CWD at first invocation. Becomes `INIT_CWD` for npm scripts. |
829
+ | `XRUN_VERSION`, `XRUN_BIN_DIR`, `XRUN_NODE_BIN` | Version, install directory and node binary of the running xrun. Used to skip repeated logs in nested runs. |
830
+ | `XRUN_TASKFILE` | Path of the loaded task file, or `not found`. |
831
+ | `XRUN_PACKAGE_PATH` | Path of the `package.json` whose npm scripts were loaded. |
832
+
833
+ ### Stop on Error Modes
834
+
835
+ xrun supports different error handling modes via the `stopOnError` setting:
836
+
837
+ - `false` or `""` - Continue execution even if tasks fail
838
+ - `"soft"` - Allow existing async tasks to complete, but don't start new tasks
839
+ - `"full"` - Stop immediately and abandon all pending tasks (default)
840
+
841
+ ```js
842
+ // Set globally
843
+ const xrun = require("@fynjs/run");
844
+ xrun.stopOnError = "soft";
845
+
846
+ // Or via CLI
847
+ // xrun --soe=soft task1 task2
848
+ ```
849
+
850
+ When the `xrun` command finishes with a failure and no `done` callback, it prints the errors and exits with code 1, but only if the mode is not `""`. With `--soe no`, the process is not exited with code 1 by xrun.
851
+
852
+ ## APIs
853
+
854
+ All APIs are members of the `xrun` instance. Methods that return `this` can be chained.
855
+
856
+ ### `stopOnError`
857
+
858
+ ```ts
859
+ xrun.stopOnError: "" | "soft" | "full"
860
+ ```
861
+
862
+ Configure `xrun`'s behavior if any task execution failed.
863
+
864
+ Accepted values when setting are:
865
+
866
+ - `false`, `""` - completely turn off, march on if any tasks failed.
867
+ - `"soft"` - Allow existing async tasks to run to completion and invoke `finally` hooks, but no new tasks will be executed.
868
+ - `true`, `"full"` - Stop and exit immediately, don't wait for any pending async tasks, `finally` hooks invocation is unreliable.
869
+
870
+ Any other value throws an assertion error. The getter returns `""`, `"soft"`, `"full"` or `undefined` if never set.
871
+
872
+ > If never set, `run` sets it to `"full"`.
873
+
874
+ Example:
875
+
876
+ ```js
877
+ const xrun = require("@fynjs/run");
878
+
879
+ xrun.stopOnError = "full";
880
+ ```
881
+
882
+ > Note: If user specify this in CLI with option `--soe=<value>` then that will always be used.
883
+
884
+ ### `load([namespace], tasks, [priority])`
885
+
886
+ ```ts
887
+ xrun.load(tasks: object): this
888
+ xrun.load(namespace: string | { namespace: string; overrides?: string | string[] }, tasks: object, priority?: number): this
889
+ ```
890
+
891
+ Load `tasks` into `[namespace]` (optional).
892
+
893
+ If no `namespace`, then tasks are loaded into the root namespace `/`.
894
+
895
+ - `tasks` must be an object, otherwise it throws `Invalid tasks`.
896
+ - Tasks are merged into the namespace with `Object.assign`. A task with the same name replaces the earlier one.
897
+ - `priority` defaults to `1`. See [Namespace Overrides](#namespace-overrides). It has no effect on the root namespace, which is always searched first.
898
+
899
+ ```js
900
+ // Load into default namespace
901
+ xrun.load({
902
+ hello: "echo hello"
903
+ });
904
+
905
+ // Load into specific namespace
906
+ xrun.load("build", {
907
+ compile: "babel src -d lib",
908
+ bundle: "webpack"
909
+ });
910
+
911
+ // Load with namespace options
912
+ xrun.load(
913
+ { namespace: "test", overrides: "build" },
914
+ {
915
+ unit: "jest",
916
+ e2e: "cypress run"
917
+ }
918
+ );
919
+ ```
920
+
921
+ ### `run(tasks, [done])`
922
+
923
+ ```ts
924
+ xrun.run(tasks: string | any[], done?: (err?: Error) => void): this
925
+ ```
926
+
927
+ Run the task specified by `tasks`.
928
+
929
+ - `tasks` - Either a string or an array of names. A string may carry a `?` prefix, a namespace, and inline task options. An array follows the [array rules](#array-serialconcurrent-rules) and is concurrent unless it starts with a serial marker.
930
+ - `done` - Optional callback, called once with the first failure error, or nothing on success. Further failures are in `err.more`. If it's not given, an internal handler prints the failures and exits the process with code 1, see [Stop on Error Modes](#stop-on-error-modes).
931
+
932
+ If `stopOnError` was never set, it is set to `"full"`.
933
+
934
+ ```js
935
+ // Run single task
936
+ xrun.run("build", err => {
937
+ if (err) console.error("Build failed:", err);
938
+ else console.log("Build completed");
939
+ });
940
+
941
+ // Run multiple tasks
942
+ xrun.run(["lint", "test"], done);
943
+ ```
944
+
945
+ ### `asyncRun(tasks)`
946
+
947
+ ```ts
948
+ xrun.asyncRun(tasks: string | any[]): Promise<undefined>
949
+ ```
950
+
951
+ Promise-based version of `run()`. It resolves with `undefined` and rejects with the same error `done` would receive.
952
+
953
+ ```js
954
+ async function buildAndDeploy() {
955
+ try {
956
+ await xrun.asyncRun("build");
957
+ await xrun.asyncRun("deploy");
958
+ console.log("Build and deploy completed");
959
+ } catch (err) {
960
+ console.error("Failed:", err);
961
+ }
962
+ }
963
+ ```
964
+
965
+ ### `stop()`
966
+
967
+ ```ts
968
+ xrun.stop(): symbol
969
+ ```
970
+
971
+ Returns a marker. It does nothing by itself. When xrun executes the marker as a task, it kills all tracked child processes and no further task or `finally` hook starts.
972
+
973
+ Use it as a task, or return it from a function task:
974
+
975
+ ```js
976
+ const tasks = {
977
+ "long-running-server": () => {
978
+ // Start a server
979
+ return new Promise(resolve => {
980
+ // This would run indefinitely
981
+ setInterval(() => {}, 1000);
982
+ });
983
+ },
984
+
985
+ "test-and-stop": xrun.serial(
986
+ "long-running-server",
987
+ () => {
988
+ // Run some tests
989
+ return runTests();
990
+ },
991
+ () => xrun.stop() // Stop everything
992
+ )
993
+ };
994
+ ```
995
+
996
+ ### `waitAllPending(done)`
997
+
998
+ ```ts
999
+ xrun.waitAllPending(done: () => void): this
1000
+ ```
1001
+
1002
+ Wait for all pending concurrent tasks to complete and then call `done` with no arguments. It polls every 10 ms.
1003
+
1004
+ ```js
1005
+ xrun.run("background-task");
1006
+ xrun.waitAllPending(() => {
1007
+ console.log("All tasks completed");
1008
+ });
1009
+ ```
1010
+
1011
+ ### `env(spec, [options])`
1012
+
1013
+ ```ts
1014
+ xrun.env(spec: Record<string, string | null | undefined>, options?: { override?: boolean }): XTaskSpec
1015
+ ```
1016
+
1017
+ Create a task to set environment variables in `process.env`.
1018
+
1019
+ - `spec` - Object of environment variables. A `null` or `undefined` value deletes the variable.
1020
+ - `options.override` - Default `true`. When `false`, variables that already exist are kept.
1021
+
1022
+ Example:
1023
+
1024
+ ```js
1025
+ {
1026
+ someTask: [xrun.env({ FOO: "bar" }), xrun.exec("echo $FOO")];
1027
+ }
1028
+ ```
1029
+
1030
+ > Note that this can be achieved easily with a function task:
1031
+
1032
+ ```js
1033
+ {
1034
+ someTask: [() => Object.assign(process.env, { FOO: "bar" }), xrun.exec("echo $FOO")];
1035
+ }
1036
+ ```
1037
+
1038
+ > However, using `xrun.env` will log out the env variables and values nicely.
1039
+
1040
+ ### `updateEnv(envValues, [options])`
1041
+
1042
+ ```ts
1043
+ xrun.updateEnv(envValues: Record<string, any>, options?: { target?: object; override?: boolean }): object
1044
+ ```
1045
+
1046
+ Immediately apply `envValues` to `options.target` (default `process.env`) and return the target. A `null` or `undefined` value deletes the key. `override` defaults to `true`. When `false`, existing keys are kept. This is the function behind [`env`](#envspec-options).
1047
+
1048
+ ### `concurrent([tasks] | task1, task2, taskN)`
1049
+
1050
+ ```ts
1051
+ xrun.concurrent(...tasks: any[]): any[] | any
1052
+ ```
1053
+
1054
+ Explicitly creates an array of tasks to be executed concurrently.
1055
+
1056
+ - The tasks can be passed in as a single array
1057
+ - Or they can be passed in as a list of variadic arguments
1058
+ - Falsy arguments are dropped. With no arguments it throws `no tasks passed`.
1059
+ - A single argument that is not an array is returned as is.
1060
+
1061
+ Returns an array of tasks that's marked for concurrent execution. `xrun.parallel` is an alias.
1062
+
1063
+ ```js
1064
+ // Using array
1065
+ xrun.concurrent(["task1", "task2", "task3"]);
1066
+
1067
+ // Using variadic arguments
1068
+ xrun.concurrent("task1", "task2", "task3");
1069
+
1070
+ // Nested example
1071
+ const tasks = {
1072
+ "build-all": xrun.concurrent("build-client", "build-server", "build-docs")
1073
+ };
1074
+ ```
1075
+
1076
+ ### `serial([tasks] | task1, task2, taskN)`
1077
+
1078
+ ```ts
1079
+ xrun.serial(...tasks: any[]): any[] | any
1080
+ ```
1081
+
1082
+ Explicitly creates an array of tasks to be executed serially. Argument handling is the same as [`concurrent`](#concurrenttasks--task1-task2-taskn).
1083
+
1084
+ - The tasks can be passed in as a single array
1085
+ - Or they can be passed in as a list of variadic arguments
1086
+
1087
+ Returns an array of tasks that's marked for serial execution.
1088
+
1089
+ ```js
1090
+ // Using array
1091
+ xrun.serial(["build", "test", "deploy"]);
1092
+
1093
+ // Using variadic arguments
1094
+ xrun.serial("build", "test", "deploy");
1095
+
1096
+ // Complex example
1097
+ const tasks = {
1098
+ "ci-pipeline": xrun.serial(
1099
+ "clean",
1100
+ xrun.concurrent("lint", "typecheck"),
1101
+ "build",
1102
+ "test",
1103
+ "deploy"
1104
+ )
1105
+ };
1106
+ ```
1107
+
1108
+ ### `exec(spec, [options])`
1109
+
1110
+ ```ts
1111
+ xrun.exec(spec: string | string[] | ExecSpec, options?: string | string[] | Omit<ExecSpec, "cmd">): XTaskSpec
1112
+
1113
+ interface ExecSpec {
1114
+ cmd: string | string[];
1115
+ flags?: string | string[];
1116
+ execOptions?: object;
1117
+ xrun?: { delayRunMs?: number };
1118
+ env?: Record<string, string>;
1119
+ }
1120
+ ```
1121
+
1122
+ Create a shell command task spec with _optional_ [`flags`](#shell-task-flags) or `options`.
1123
+
1124
+ - `spec` - an object that specifies the following fields:
1125
+
1126
+ | Field | Type | Default | Behavior |
1127
+ | --- | --- | --- | --- |
1128
+ | `cmd` | string, string[] | none | The shell command. An array is joined with a space. `command` is accepted as an alias. |
1129
+ | `flags` | string, string[] | `{}` | [Shell Task Flags](#shell-task-flags), ie: `"tty,sync"` or `["tty", "sync"]`. |
1130
+ | `execOptions` | object | `{}` | Options to pass to [child_process.spawn] or [child_process.exec]. |
1131
+ | `xrun` | object | `{ delayRunMs: 0 }` | xrun execution options. `delayRunMs` is milliseconds to wait before running the command. |
1132
+ | `env` | object | none | `Object.assign`ed into `execOptions.env`. It is added to `process.env` unless the `noenv` flag is set. |
1133
+
1134
+ > Alternatively this can also be called as `exec(cmd, [flags|options])`
1135
+
1136
+ Where:
1137
+
1138
+ - `flags` - string or array as [Shell Task Flags](#shell-task-flags)
1139
+ - `options` - Object to specify: `{ flags, execOptions, xrun, env }`
1140
+
1141
+ Any other `spec` type throws `xrun.exec - unknown spec type`.
1142
+
1143
+ Examples:
1144
+
1145
+ ```js
1146
+ const xrun = require("@fynjs/run");
1147
+
1148
+ const tasks = {
1149
+ cmd1: xrun.exec("echo hello", "tty"),
1150
+ cmd2: [
1151
+ // run `echo foo` with env FOO=bar
1152
+ xrun.exec("echo foo", { env: { FOO: "bar" } }),
1153
+ // run `echo hello world` with tty enabled
1154
+ xrun.exec(["echo", "hello", "world"], "tty"),
1155
+ // with a single spec object
1156
+ xrun.exec({
1157
+ cmd: ["echo", "hello", "world"],
1158
+ flags: "tty",
1159
+ env: { FOO: "bar" }
1160
+ })
1161
+ ]
1162
+ };
1163
+
1164
+ xrun.load(tasks);
1165
+ ```
1166
+
1167
+ ### `printTasks()`
1168
+
1169
+ ```ts
1170
+ xrun.printTasks(): this
1171
+ ```
1172
+
1173
+ Print the loaded tasks and their descriptions to the console.
1174
+
1175
+ ### `countTasks()` and `getNamespaces()`
1176
+
1177
+ ```ts
1178
+ xrun.countTasks(): number
1179
+ xrun.getNamespaces(): string[]
1180
+ ```
1181
+
1182
+ `countTasks` returns the number of loaded tasks across all namespaces. `getNamespaces` returns the namespaces in search order, starting with `/`.
1183
+
1184
+ ### `XRun`
1185
+
1186
+ ```ts
1187
+ new xrun.XRun(namespace?: string, tasks?: object)
1188
+ ```
1189
+
1190
+ The class behind the shared instance. Use it to create an independent runner with its own tasks. It extends `EventEmitter` and has the same methods as `xrun`. Its `load` and `run` are not pre-bound.
1191
+
1192
+ ### Internal members
1193
+
1194
+ These are public on the instance but exist for the CLI and the executor. Do not call them from task code.
1195
+
1196
+ | Member | Behavior |
1197
+ | --- | --- |
1198
+ | `xqTree` | The execution queue tree. The executor creates and looks up queue items in it. |
1199
+ | `addTaskChild(child, sym)`, `removeTaskChild(child, sym)` | Track or untrack a spawned child process under a symbol key. Both return the tracking map. |
1200
+ | `killChildProcess(child)` | Kills one child. Uses `taskkill /T /F` on Windows, `child.kill()` elsewhere. Errors are ignored. Marks the child as stopped. |
1201
+ | `killTaskChildren()` | After a 10 ms timer, kills every tracked child and clears the tracking map. |
1202
+ | `actStop()` | Kills tracked children and marks the run as stopped. |
1203
+ | `exit(code)` | Calls `process.exit(code)` only when `stopOnError` is set. With tracked children it kills them first and exits after 100 ms. Without `stopOnError` it does nothing. |
1204
+ | `setCliContext(cliContext)`, `getCliContext()` | Set or get the CLI context. The CLI sets it before running tasks. `setCliContext` returns the instance. |
1205
+
1206
+ ### Events
1207
+
1208
+ `xrun` is an `EventEmitter`. These events are emitted:
1209
+
1210
+ | Event | Data | When |
1211
+ | --- | --- | --- |
1212
+ | `run` | `{ name }` or `{ tasks }` | `run` was called. |
1213
+ | `execute` | `{ type, qItem, ... }` | A task item executes. `type` is `lookup`, `serial-arr`, `concurrent-arr`, `env`, `shell` or `function`. |
1214
+ | `dep` | `{ qItem }` | A task's `dep` is about to run. |
1215
+ | `search` | `{ qItem, found }` | A task name without a namespace was found by searching namespaces. |
1216
+ | `not-found` | error | An optional task was not found. |
1217
+ | `spawn-async`, `done-async` | none, `{ name }` | A concurrent branch started or finished. |
1218
+ | `done-item` | timing data | A task finished. |
1219
+ | `warn-finally` | none | A `finally` hook exists and `stopOnError` is `"full"`. |
1220
+ | `fail-cancel` | watch object | A pending task was cancelled by a failure. |
1221
+
1222
+ [child_process.spawn]: https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options
1223
+ [child_process.spawnsync]: https://nodejs.org/api/child_process.html#child_process_child_process_spawnsync_command_args_options
1224
+ [child_process.exec]: https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback
1225
+ [node.js stream]: https://nodejs.org/api/stream.html
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fynjs/run",
3
- "version": "1.1.8",
3
+ "version": "1.1.10",
4
4
  "description": "concurrent or serial run npm scripts, javascript tasks, and more",
5
5
  "main": "./dist/lib/index.js",
6
6
  "types": "./dist/lib/index.d.ts",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "repository": {
40
40
  "type": "git",
41
- "url": "https://github.com/jchip/fynjs.git",
41
+ "url": "git+https://github.com/jchip/fynjs.git",
42
42
  "directory": "packages/xarc-run"
43
43
  },
44
44
  "publishConfig": {
@@ -46,7 +46,8 @@
46
46
  },
47
47
  "files": [
48
48
  "dist",
49
- "bin"
49
+ "bin",
50
+ "docs/reference.md"
50
51
  ],
51
52
  "keywords": [
52
53
  "concurrent",
@@ -84,32 +85,32 @@
84
85
  "bugs": {
85
86
  "url": "https://github.com/jchip/fynjs/issues"
86
87
  },
87
- "homepage": "https://github.com/jchip/fynjs/tree/main/packages/xarc-run",
88
+ "homepage": "https://fynjs.pages.dev/api/modules/_fynjs_run",
88
89
  "dependencies": {
89
90
  "chalk": "^6.0.0",
90
- "chalker": "^2.2.0",
91
+ "chalker": "^2.3.0",
91
92
  "insync": "^2.1.1",
92
93
  "jaro-winkler": "^0.2.8",
93
- "@fynjs/cli-args": "^1.1.7",
94
+ "@fynjs/cli-args": "^1.2.0",
94
95
  "optional-require": "^2.1.0",
95
96
  "read-package-up": "^12.0.0",
96
- "string-array": "^2.1.7",
97
- "unwrap-npm-cmd": "^2.1.7",
98
- "xsh": "^1.1.8"
97
+ "string-array": "^2.1.8",
98
+ "unwrap-npm-cmd": "^2.1.8",
99
+ "xsh": "^1.1.9"
99
100
  },
100
101
  "devDependencies": {
101
- "@fynjs/ts-resolve": "^1.0.3",
102
+ "@fynjs/ts-resolve": "^1.0.4",
102
103
  "@types/node": "^26.4.1",
103
104
  "@vitest/coverage-v8": "^5.0.0",
104
- "munchy": "^2.0.7",
105
+ "munchy": "^2.0.8",
105
106
  "prettier": "^3.5.3",
106
- "publish-util": "^3.1.7",
107
- "run-verify": "^2.1.8",
107
+ "publish-util": "^3.1.8",
108
+ "run-verify": "^2.2.0",
108
109
  "strip-ansi": "^7.2.0",
109
110
  "typescript": "^7.0.2",
110
111
  "vite": "^8.2.2",
111
112
  "vitest": "^5.0.0",
112
- "xaa": "^3.1.7",
113
+ "xaa": "^3.1.8",
113
114
  "xstdout": "^0.1.1"
114
115
  },
115
116
  "type": "module"