runset 0.0.2 → 0.0.4
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 +12 -3
- package/dist/index.d.mts +28 -26
- package/dist/index.mjs +1450 -1414
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,31 +1,107 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
+
import fs from "node:fs";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { WriteStream } from "node:tty";
|
|
6
|
-
import fs from "node:fs";
|
|
7
|
-
import { inspect, styleText } from "node:util";
|
|
8
7
|
import os from "node:os";
|
|
8
|
+
import { inspect, styleText } from "node:util";
|
|
9
9
|
import cp from "node:child_process";
|
|
10
|
-
//#region src/
|
|
10
|
+
//#region src/config/std.ts
|
|
11
|
+
const TIMINGS = /* @__PURE__ */ new Set(["realtime", "grouped"]);
|
|
12
|
+
/**
|
|
13
|
+
* Parses `<timing>+<destination>` (either order, either optional) into the
|
|
14
|
+
* axes it names. Unnamed axes are left out so settings can be layered.
|
|
15
|
+
*/
|
|
16
|
+
function parsePartialStd(value) {
|
|
17
|
+
const result = {};
|
|
18
|
+
for (const part of value.split("+")) {
|
|
19
|
+
const token = part.trim();
|
|
20
|
+
if (token === "") continue;
|
|
21
|
+
if (TIMINGS.has(token)) result.timing = token;
|
|
22
|
+
else result.destination = token;
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
function toPartialStd(value) {
|
|
27
|
+
return typeof value === "string" ? parsePartialStd(value) : value;
|
|
28
|
+
}
|
|
29
|
+
/** Layers a stream setting, in either form, over `base` one axis at a time. */
|
|
30
|
+
function mergeStd(base, value) {
|
|
31
|
+
return value === void 0 ? base : {
|
|
32
|
+
...base,
|
|
33
|
+
...toPartialStd(value)
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function defaultStd(destination) {
|
|
37
|
+
return {
|
|
38
|
+
destination,
|
|
39
|
+
timing: "realtime"
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Layers bags' stream settings over `base`, later bags over earlier, one axis
|
|
44
|
+
* at a time. `output` names both streams; a stream's own setting in the same
|
|
45
|
+
* bag outranks it.
|
|
46
|
+
*/
|
|
47
|
+
function layerStreams(base, bags) {
|
|
48
|
+
const result = { ...base };
|
|
49
|
+
for (const bag of bags) for (const key of ["stderr", "stdout"]) result[key] = mergeStd(mergeStd(result[key], bag.output), bag[key]);
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/utils/object.ts
|
|
54
|
+
function isPlainObject(value) {
|
|
55
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/config/validate.ts
|
|
11
59
|
const EXIT_ACTIONS = /* @__PURE__ */ new Set([
|
|
12
60
|
"continue",
|
|
13
61
|
"restart",
|
|
14
62
|
"stop"
|
|
15
63
|
]);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return !("command" in definition);
|
|
64
|
+
function formatValue(value) {
|
|
65
|
+
return typeof value === "string" ? `"${value}"` : String(value);
|
|
19
66
|
}
|
|
20
|
-
function
|
|
21
|
-
|
|
67
|
+
function numberError(name, value, minimum) {
|
|
68
|
+
if (typeof value !== "number" || Number.isNaN(value) || value < minimum) return `${name} must be a number of ${minimum} or more, got ${formatValue(value)}.`;
|
|
69
|
+
}
|
|
70
|
+
function booleanError(name, value) {
|
|
71
|
+
return typeof value === "boolean" ? void 0 : `${name} must be true or false, got ${formatValue(value)}.`;
|
|
72
|
+
}
|
|
73
|
+
function stringError(name, value) {
|
|
74
|
+
return typeof value === "string" ? void 0 : `${name} must be a string, got ${formatValue(value)}.`;
|
|
75
|
+
}
|
|
76
|
+
function envError(name, value) {
|
|
77
|
+
if (!isPlainObject(value)) return `${name} must be an object.`;
|
|
78
|
+
const bad = Object.entries(value).find(([, entry]) => typeof entry !== "string");
|
|
79
|
+
return bad === void 0 ? void 0 : `${name}.${bad[0]} must be a string, got ${formatValue(bad[1])}.`;
|
|
80
|
+
}
|
|
81
|
+
function oneOfError(name, value, allowed) {
|
|
82
|
+
return allowed.has(value) ? void 0 : `${name} is one of ${[...allowed].join(", ")}, got ${formatValue(value)}.`;
|
|
83
|
+
}
|
|
84
|
+
function actionError(name, value) {
|
|
85
|
+
return oneOfError(name, value, EXIT_ACTIONS);
|
|
86
|
+
}
|
|
87
|
+
function stdError(name, value) {
|
|
88
|
+
if (typeof value !== "object" || value === null) return `${name} must be a stream setting.`;
|
|
89
|
+
if (!TIMINGS.has(value.timing)) return `Invalid timing ${formatValue(value.timing)} for ${name}; one of ${[...TIMINGS].join(", ")}.`;
|
|
90
|
+
if (typeof value.destination !== "string" || value.destination === "") return `${name} needs a destination to write to.`;
|
|
91
|
+
}
|
|
92
|
+
/** Throws `message` as an `ErrorClass`, if there is one. */
|
|
93
|
+
function check(message, ErrorClass) {
|
|
94
|
+
if (message !== void 0) throw new ErrorClass(message);
|
|
22
95
|
}
|
|
23
96
|
//#endregion
|
|
24
97
|
//#region src/utils/errors.ts
|
|
25
98
|
/** Any failure runset reports itself; `exitCode` is what the CLI exits with. */
|
|
26
99
|
var RunsetError = class extends Error {
|
|
27
100
|
exitCode;
|
|
28
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* What the CLI prints in place of `message`; empty when it already said so.
|
|
103
|
+
* @internal
|
|
104
|
+
*/
|
|
29
105
|
report;
|
|
30
106
|
constructor(message, exitCode = 1, report) {
|
|
31
107
|
super(message);
|
|
@@ -41,107 +117,44 @@ var ConfigError = class extends RunsetError {};
|
|
|
41
117
|
/** A command that cannot be resolved. */
|
|
42
118
|
var NormalizeError = class extends RunsetError {};
|
|
43
119
|
//#endregion
|
|
44
|
-
//#region src/utils/
|
|
45
|
-
|
|
46
|
-
*
|
|
47
|
-
* take no room, wide characters (CJK, emoji) take two columns.
|
|
48
|
-
*/
|
|
49
|
-
const ESCAPE = /\u001B(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007\u001B]*(?:\u0007|\u001B\\)?|[@-Z\\-_])/g;
|
|
50
|
-
const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
51
|
-
function isTerminal(stream) {
|
|
52
|
-
return stream.isTTY === true;
|
|
53
|
-
}
|
|
54
|
-
/** The width a terminal stream reports; `undefined` for anything else. */
|
|
55
|
-
function terminalColumns(stream) {
|
|
56
|
-
const { columns } = stream;
|
|
57
|
-
return isTerminal(stream) && columns > 0 ? columns : void 0;
|
|
120
|
+
//#region src/utils/glob.ts
|
|
121
|
+
function isGlob(pattern) {
|
|
122
|
+
return pattern.includes("*");
|
|
58
123
|
}
|
|
59
|
-
/** `
|
|
60
|
-
function
|
|
61
|
-
const
|
|
62
|
-
return
|
|
124
|
+
/** The `names` a glob matches, in declared order; none is not an error. */
|
|
125
|
+
function matchGlob(pattern, names) {
|
|
126
|
+
const regexp = toRegExp(pattern);
|
|
127
|
+
return names.filter((name) => regexp.test(name));
|
|
63
128
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
let width = 0;
|
|
67
|
-
for (const { segment } of segmenter.segment(text.replace(ESCAPE, ""))) width += columnWidth(segment, width);
|
|
68
|
-
return width;
|
|
129
|
+
function escape(literal) {
|
|
130
|
+
return literal.replaceAll(/[$()+.?[\\\]^{|}]/g, String.raw`\$&`);
|
|
69
131
|
}
|
|
70
132
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* reset at the end of the piece and re-applied at the start of the next, so a
|
|
74
|
-
* label never inherits it. `offset` is the column the line starts at, for tab
|
|
75
|
-
* stops. A line that moves the cursor — `\r`, or any escape but a color — is
|
|
76
|
-
* left whole: where it lands is not something runset can know.
|
|
133
|
+
* `*` matches within one `:`-separated segment, `**` spans one or more.
|
|
134
|
+
* `build:**:*` means `build:**`.
|
|
77
135
|
*/
|
|
78
|
-
function
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
for (const token of tokenize(line)) {
|
|
86
|
-
if (token.escape) {
|
|
87
|
-
if (token.text.startsWith("\x1B[") && !isSgr(token.text)) return [line];
|
|
88
|
-
current += token.text;
|
|
89
|
-
style = applySgr(style, token.text);
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
for (const { segment } of segmenter.segment(token.text)) {
|
|
93
|
-
let size = columnWidth(segment, offset + column);
|
|
94
|
-
if (column > 0 && column + size > width) {
|
|
95
|
-
pieces.push(style.length > 0 ? `${current}\u001B[0m` : current);
|
|
96
|
-
current = style.join("");
|
|
97
|
-
column = 0;
|
|
98
|
-
size = columnWidth(segment, offset);
|
|
99
|
-
}
|
|
100
|
-
current += segment;
|
|
101
|
-
column += size;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
pieces.push(current);
|
|
105
|
-
return pieces;
|
|
106
|
-
}
|
|
107
|
-
function tokenize(text) {
|
|
108
|
-
const tokens = [];
|
|
109
|
-
let last = 0;
|
|
110
|
-
for (const match of text.matchAll(ESCAPE)) {
|
|
111
|
-
if (match.index > last) tokens.push({
|
|
112
|
-
escape: false,
|
|
113
|
-
text: text.slice(last, match.index)
|
|
114
|
-
});
|
|
115
|
-
tokens.push({
|
|
116
|
-
escape: true,
|
|
117
|
-
text: match[0]
|
|
118
|
-
});
|
|
119
|
-
last = match.index + match[0].length;
|
|
136
|
+
function toRegExp(pattern) {
|
|
137
|
+
const segments = pattern.split(":");
|
|
138
|
+
const source = [];
|
|
139
|
+
for (const [index, segment] of segments.entries()) {
|
|
140
|
+
if (segment === "*" && segments[index - 1] === "**") continue;
|
|
141
|
+
if (segment === "**") source.push(String.raw`[^:]+(?::[^:]+)*`);
|
|
142
|
+
else source.push(segment.split("*").map((part) => escape(part)).join("[^:]*"));
|
|
120
143
|
}
|
|
121
|
-
|
|
122
|
-
escape: false,
|
|
123
|
-
text: text.slice(last)
|
|
124
|
-
});
|
|
125
|
-
return tokens;
|
|
144
|
+
return new RegExp(`^${source.join(":")}$`);
|
|
126
145
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/utils/string.ts
|
|
148
|
+
function toCamelCase(name) {
|
|
149
|
+
return name.replaceAll(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
130
150
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (!isSgr(sequence)) return style;
|
|
134
|
-
const params = sequence.slice(2, -1);
|
|
135
|
-
if (params === "" || params === "0") return [];
|
|
136
|
-
return params.startsWith("0;") ? [sequence] : [...style, sequence];
|
|
151
|
+
function toKebabCase(name) {
|
|
152
|
+
return name.replaceAll(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
137
153
|
}
|
|
138
|
-
/**
|
|
139
|
-
function
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (/^[\p{Cc}\p{Cf}\p{Mn}\p{Me}]+$/u.test(grapheme)) return 0;
|
|
143
|
-
if (/\p{Extended_Pictographic}|\p{Regional_Indicator}/u.test(grapheme) && /\p{Emoji_Presentation}|\uFE0F/u.test(grapheme)) return 2;
|
|
144
|
-
return /^[\u1100-\u115F\u2E80-\u303E\u3041-\u33FF\u3400-\u4DBF\u4E00-\u9FFF\uA000-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFF60\uFFE0-\uFFE6\u{20000}-\u{3FFFD}]/u.test(grapheme) ? 2 : 1;
|
|
154
|
+
/** `850ms` under a second, `2.1s` from one up. */
|
|
155
|
+
function formatDuration(ms) {
|
|
156
|
+
const rounded = Math.round(ms);
|
|
157
|
+
return rounded < 1e3 ? `${rounded}ms` : `${(ms / 1e3).toFixed(1)}s`;
|
|
145
158
|
}
|
|
146
159
|
//#endregion
|
|
147
160
|
//#region src/utils/fs.ts
|
|
@@ -150,13 +163,26 @@ const require = createRequire(import.meta.url);
|
|
|
150
163
|
function importSync(modulePath) {
|
|
151
164
|
return require(modulePath);
|
|
152
165
|
}
|
|
166
|
+
/** `dir` and every directory above it, up to the root. */
|
|
167
|
+
function ancestors(dir) {
|
|
168
|
+
const dirs = [path.resolve(dir)];
|
|
169
|
+
for (;;) {
|
|
170
|
+
const current = dirs.at(-1);
|
|
171
|
+
const parent = path.dirname(current);
|
|
172
|
+
if (parent === current) return dirs;
|
|
173
|
+
dirs.push(parent);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/** A parsed JSON file, or `undefined` when there is none. */
|
|
177
|
+
function readJson(filePath) {
|
|
178
|
+
return fs.existsSync(filePath) ? JSON.parse(fs.readFileSync(filePath, "utf8")) : void 0;
|
|
179
|
+
}
|
|
153
180
|
/** The nearest `package.json` at or above `cwd`. */
|
|
154
181
|
function readPackageJson(cwd) {
|
|
155
182
|
for (const dir of ancestors(cwd)) {
|
|
156
183
|
const filePath = path.join(dir, "package.json");
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return {
|
|
184
|
+
const raw = readJson(filePath);
|
|
185
|
+
if (raw !== void 0) return {
|
|
160
186
|
filePath,
|
|
161
187
|
name: raw.name ?? "",
|
|
162
188
|
scripts: raw.scripts ?? {},
|
|
@@ -171,1351 +197,1324 @@ function readPackageJson(cwd) {
|
|
|
171
197
|
};
|
|
172
198
|
}
|
|
173
199
|
//#endregion
|
|
174
|
-
//#region src/
|
|
200
|
+
//#region src/utils/workspace.ts
|
|
175
201
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
* and unwraps a default export or a factory function.
|
|
202
|
+
* The packages of the workspace at or above `cwd`, from `pnpm-workspace.yaml`
|
|
203
|
+
* or `package.json` `workspaces`, sorted by path; the root is not one of them.
|
|
179
204
|
*/
|
|
180
|
-
function
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
function unwrapConfigJs(source, context) {
|
|
192
|
-
const unwrapped = source.default ?? source;
|
|
193
|
-
return typeof unwrapped === "function" ? unwrapped(context) : unwrapped;
|
|
194
|
-
}
|
|
195
|
-
/** `dir` and every directory above it, up to the root. */
|
|
196
|
-
function ancestors(dir) {
|
|
197
|
-
const dirs = [path.resolve(dir)];
|
|
198
|
-
for (;;) {
|
|
199
|
-
const current = dirs.at(-1);
|
|
200
|
-
const parent = path.dirname(current);
|
|
201
|
-
if (parent === current) return dirs;
|
|
202
|
-
dirs.push(parent);
|
|
205
|
+
function readWorkspacePackages(cwd) {
|
|
206
|
+
for (const dir of ancestors(cwd)) {
|
|
207
|
+
const patterns = readWorkspacePatterns(dir);
|
|
208
|
+
if (patterns === void 0) continue;
|
|
209
|
+
const include = patterns.filter((pattern) => !pattern.startsWith("!"));
|
|
210
|
+
const exclude = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1));
|
|
211
|
+
return fs.globSync(include, {
|
|
212
|
+
cwd: dir,
|
|
213
|
+
exclude: [...exclude, "**/node_modules"]
|
|
214
|
+
}).map((match) => path.join(dir, match, "package.json")).filter((filePath) => fs.existsSync(filePath)).toSorted().map((filePath) => readPackageJson(path.dirname(filePath)));
|
|
203
215
|
}
|
|
216
|
+
return [];
|
|
204
217
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
for (const dir of ancestors(cwd)) {
|
|
211
|
-
for (const extension of [
|
|
212
|
-
"ts",
|
|
213
|
-
"js",
|
|
214
|
-
"mjs",
|
|
215
|
-
"cjs",
|
|
216
|
-
"json"
|
|
217
|
-
]) {
|
|
218
|
-
const candidate = path.join(dir, `runset.config.${extension}`);
|
|
219
|
-
if (fs.existsSync(candidate)) return importSync(candidate);
|
|
220
|
-
}
|
|
221
|
-
const section = readPackageJsonSection(path.join(dir, "package.json"));
|
|
222
|
-
if (section !== void 0) return section;
|
|
223
|
-
}
|
|
218
|
+
function readWorkspacePatterns(dir) {
|
|
219
|
+
const yamlPath = path.join(dir, "pnpm-workspace.yaml");
|
|
220
|
+
if (fs.existsSync(yamlPath)) return parsePnpmPackages(fs.readFileSync(yamlPath, "utf8"));
|
|
221
|
+
const { workspaces } = readJson(path.join(dir, "package.json")) ?? {};
|
|
222
|
+
return Array.isArray(workspaces) ? workspaces : workspaces?.packages;
|
|
224
223
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
224
|
+
/** The `packages:` list of a `pnpm-workspace.yaml`, read without a YAML parser. */
|
|
225
|
+
function parsePnpmPackages(text) {
|
|
226
|
+
const patterns = [];
|
|
227
|
+
let inPackages = false;
|
|
228
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
229
|
+
const line = raw.replace(/\s#.*$/, "").trimEnd();
|
|
230
|
+
if (/^\S/.test(line)) inPackages = /^packages\s*:/.test(line);
|
|
231
|
+
else if (inPackages && /^\s+-/.test(line)) patterns.push(line.replace(/^\s+-\s*/, "").replaceAll(/^['"]|['"]$/g, ""));
|
|
232
|
+
}
|
|
233
|
+
return patterns;
|
|
231
234
|
}
|
|
232
235
|
//#endregion
|
|
233
|
-
//#region src/utils/
|
|
234
|
-
|
|
235
|
-
|
|
236
|
+
//#region src/utils/os.ts
|
|
237
|
+
/** 128 + the signal's number, the way a shell reports it: 130, 143, … */
|
|
238
|
+
function signalExitCode(signal) {
|
|
239
|
+
return 128 + (os.constants.signals[signal] ?? 15);
|
|
236
240
|
}
|
|
237
|
-
|
|
238
|
-
|
|
241
|
+
/**
|
|
242
|
+
* Quotes a value so the platform's shell passes it on as literal text —
|
|
243
|
+
* double quotes are not enough, since `$(…)` still runs inside them.
|
|
244
|
+
*/
|
|
245
|
+
function quoteForShell(value, platform = process.platform) {
|
|
246
|
+
return platform === "win32" ? quoteForCmd(value) : quoteForPosix(value);
|
|
239
247
|
}
|
|
240
|
-
/**
|
|
241
|
-
function
|
|
242
|
-
|
|
243
|
-
|
|
248
|
+
/** Single quotes are fully literal; each `'` inside closes, escapes, reopens. */
|
|
249
|
+
function quoteForPosix(value) {
|
|
250
|
+
return `'${value.replaceAll("'", String.raw`'\''`)}'`;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Quoted for the C runtime first, then every `cmd.exe` metacharacter
|
|
254
|
+
* `^`-escaped (see <https://qntm.org/cmd>). Newlines cannot survive `cmd.exe`,
|
|
255
|
+
* so they become spaces.
|
|
256
|
+
*/
|
|
257
|
+
function quoteForCmd(value) {
|
|
258
|
+
return `"${value.replaceAll("\r\n", " ").replaceAll(/[\n\r]/g, " ").replaceAll(/(\\*)"/g, String.raw`$1$1\"`).replace(/(\\*)$/, "$1$1")}"`.replaceAll(/["%&()<>^|]/g, (character) => `^${character}`);
|
|
244
259
|
}
|
|
245
260
|
//#endregion
|
|
246
|
-
//#region src/
|
|
261
|
+
//#region src/plan/placeholders.ts
|
|
247
262
|
/**
|
|
248
|
-
*
|
|
249
|
-
*
|
|
263
|
+
* Fills `{1}`, `{@}`, `{*}` and `{name}` from the arguments after `--`.
|
|
264
|
+
*
|
|
265
|
+
* In a command line values are shell-quoted and unknown names left as written
|
|
266
|
+
* (`${HOME}`); in an option (`forOption`) values are raw and unknown names
|
|
267
|
+
* are empty, so `disabled={noTest}` is false when nothing was passed.
|
|
250
268
|
*/
|
|
251
|
-
function
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
continue;
|
|
269
|
+
function substitutePlaceholders(text, args, forOption = false) {
|
|
270
|
+
const PLACEHOLDER = /\{([^\s{}]*)\}/g;
|
|
271
|
+
const wrap = forOption ? (value) => value : quoteForShell;
|
|
272
|
+
return text.replaceAll(PLACEHOLDER, (match, key) => {
|
|
273
|
+
if (key === "@") return args.map((arg) => wrap(arg)).join(" ");
|
|
274
|
+
if (key === "*") return args.length === 0 ? "" : wrap(args.join(" "));
|
|
275
|
+
if (/^[1-9]\d*$/.test(key)) {
|
|
276
|
+
const value = args[Number(key) - 1];
|
|
277
|
+
return value === void 0 ? "" : wrap(value);
|
|
261
278
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
279
|
+
const named = namedValue(key, args);
|
|
280
|
+
if (named !== void 0) return wrap(named);
|
|
281
|
+
return forOption ? "" : match;
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* The value of `--name=value` or `--name value` (camel or kebab); a bare
|
|
286
|
+
* `--name` is `'true'`.
|
|
287
|
+
*/
|
|
288
|
+
function namedValue(key, args) {
|
|
289
|
+
for (const [index, token] of args.entries()) {
|
|
290
|
+
if (!token.startsWith("--") || token.length === 2) continue;
|
|
291
|
+
const equals = token.indexOf("=");
|
|
292
|
+
if (toCamelCase(equals === -1 ? token.slice(2) : token.slice(2, equals)) !== key) continue;
|
|
293
|
+
if (equals !== -1) return token.slice(equals + 1);
|
|
294
|
+
const next = args[index + 1];
|
|
295
|
+
return next !== void 0 && !next.startsWith("-") ? next : "true";
|
|
267
296
|
}
|
|
268
|
-
if (open !== void 0) throw new CliError(`Unterminated quote in "'${open.join(" ")}".`);
|
|
269
|
-
return result;
|
|
270
297
|
}
|
|
271
298
|
//#endregion
|
|
272
|
-
//#region src/
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
},
|
|
284
|
-
{
|
|
285
|
-
name: "recursive",
|
|
286
|
-
short: "r",
|
|
287
|
-
takesValue: false
|
|
288
|
-
},
|
|
289
|
-
{
|
|
290
|
-
name: "jobs",
|
|
291
|
-
short: "j",
|
|
292
|
-
takesValue: true
|
|
293
|
-
},
|
|
294
|
-
{
|
|
295
|
-
name: "killTimeout",
|
|
296
|
-
takesValue: true
|
|
297
|
-
},
|
|
298
|
-
{
|
|
299
|
-
name: "onSuccess",
|
|
300
|
-
takesValue: true
|
|
301
|
-
},
|
|
302
|
-
{
|
|
303
|
-
name: "onFailure",
|
|
304
|
-
takesValue: true
|
|
305
|
-
},
|
|
306
|
-
{
|
|
307
|
-
name: "config",
|
|
308
|
-
short: "c",
|
|
309
|
-
takesValue: true
|
|
310
|
-
},
|
|
311
|
-
{
|
|
312
|
-
name: "cwd",
|
|
313
|
-
takesValue: true
|
|
314
|
-
},
|
|
315
|
-
{
|
|
316
|
-
name: "env",
|
|
317
|
-
short: "e",
|
|
318
|
-
takesValue: true
|
|
319
|
-
},
|
|
320
|
-
{
|
|
321
|
-
name: "color",
|
|
322
|
-
negated: "none",
|
|
323
|
-
takesValue: true
|
|
324
|
-
},
|
|
325
|
-
{
|
|
326
|
-
name: "logLevel",
|
|
327
|
-
takesValue: true
|
|
328
|
-
},
|
|
329
|
-
{
|
|
330
|
-
name: "dryRun",
|
|
331
|
-
takesValue: false
|
|
332
|
-
},
|
|
333
|
-
{
|
|
334
|
-
name: "stdout",
|
|
335
|
-
takesValue: true
|
|
336
|
-
},
|
|
337
|
-
{
|
|
338
|
-
name: "stderr",
|
|
339
|
-
takesValue: true
|
|
340
|
-
},
|
|
341
|
-
{
|
|
342
|
-
name: "output",
|
|
343
|
-
short: "o",
|
|
344
|
-
takesValue: true
|
|
345
|
-
},
|
|
346
|
-
{
|
|
347
|
-
name: "labels",
|
|
348
|
-
takesValue: true
|
|
349
|
-
},
|
|
350
|
-
{
|
|
351
|
-
name: "showCommand",
|
|
352
|
-
takesValue: false
|
|
353
|
-
},
|
|
354
|
-
{
|
|
355
|
-
name: "showExitCode",
|
|
356
|
-
takesValue: false
|
|
357
|
-
},
|
|
358
|
-
{
|
|
359
|
-
name: "wrap",
|
|
360
|
-
short: "w",
|
|
361
|
-
takesValue: false
|
|
362
|
-
},
|
|
363
|
-
{
|
|
364
|
-
name: "help",
|
|
365
|
-
short: "h",
|
|
366
|
-
takesValue: false
|
|
367
|
-
},
|
|
368
|
-
{
|
|
369
|
-
name: "version",
|
|
370
|
-
short: "v",
|
|
371
|
-
takesValue: false
|
|
372
|
-
}
|
|
373
|
-
];
|
|
299
|
+
//#region src/plan/stages.ts
|
|
300
|
+
const EMPTY_SEGMENT = {
|
|
301
|
+
commands: [],
|
|
302
|
+
parallel: false
|
|
303
|
+
};
|
|
304
|
+
/** A `-p`/`-s`: what follows starts a new group past everything before it. */
|
|
305
|
+
const BOUNDARY = {
|
|
306
|
+
boundary: true,
|
|
307
|
+
commands: [],
|
|
308
|
+
parallel: false
|
|
309
|
+
};
|
|
374
310
|
/**
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
311
|
+
* Lays segments out into stages. A parallel segment joins the open stage;
|
|
312
|
+
* anything else starts past every stage in use, and so does what follows it.
|
|
313
|
+
* A segment's commands keep their relative stages, shifted onto its own.
|
|
378
314
|
*/
|
|
379
|
-
function
|
|
380
|
-
const argv = regroupQuoted(rawArgv);
|
|
381
|
-
const options = {};
|
|
315
|
+
function link(segments) {
|
|
382
316
|
const commands = [];
|
|
383
|
-
|
|
384
|
-
let
|
|
385
|
-
|
|
386
|
-
let
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const text = String(raw);
|
|
392
|
-
const eq = text.indexOf("=");
|
|
393
|
-
if (eq < 1) throw new CliError(`Option "--env" takes NAME=value, not "${text}".`);
|
|
394
|
-
(options.env ??= {})[text.slice(0, eq)] = text.slice(eq + 1);
|
|
395
|
-
break;
|
|
396
|
-
}
|
|
397
|
-
case "help":
|
|
398
|
-
help = true;
|
|
399
|
-
break;
|
|
400
|
-
case "jobs":
|
|
401
|
-
case "killTimeout":
|
|
402
|
-
options[spec.name] = Number(raw);
|
|
403
|
-
break;
|
|
404
|
-
case "parallel":
|
|
405
|
-
case "serial":
|
|
406
|
-
commands.push({ parallel: spec.name === "parallel" });
|
|
407
|
-
break;
|
|
408
|
-
case "version":
|
|
409
|
-
version = true;
|
|
410
|
-
break;
|
|
411
|
-
default: if (typeof raw === "boolean") options[spec.name] = raw;
|
|
412
|
-
else options[spec.name] = raw;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
function takeValue(label) {
|
|
416
|
-
const value = argv[cursor + 1];
|
|
417
|
-
if (value === void 0) throw new CliError(`Option "${label}" needs a value.`);
|
|
418
|
-
cursor += 1;
|
|
419
|
-
return value;
|
|
420
|
-
}
|
|
421
|
-
function readLong(body) {
|
|
422
|
-
const eq = body.indexOf("=");
|
|
423
|
-
const name = eq === -1 ? body : body.slice(0, eq);
|
|
424
|
-
const inlineValue = eq === -1 ? void 0 : body.slice(eq + 1);
|
|
425
|
-
const flag = findLongFlag(name);
|
|
426
|
-
if (flag === void 0 && /^no-?[A-Za-z]/.test(name)) {
|
|
427
|
-
const negated = findLongFlag(name.replace(/^no-?/, ""));
|
|
428
|
-
if (negated && (!negated.takesValue || negated.negated !== void 0)) {
|
|
429
|
-
setOption(negated, negated.negated ?? false);
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
317
|
+
/** The stage a parallel segment joins. */
|
|
318
|
+
let open = 0;
|
|
319
|
+
/** The last stage in use; -1 while none is. */
|
|
320
|
+
let used = -1;
|
|
321
|
+
for (const segment of segments) {
|
|
322
|
+
if (segment.boundary) {
|
|
323
|
+
open = used + 1;
|
|
324
|
+
continue;
|
|
432
325
|
}
|
|
433
|
-
if (
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const letter = body[position];
|
|
440
|
-
const flag = findShortFlag(letter);
|
|
441
|
-
if (flag === void 0) throw new CliError(`Unknown option "-${letter}".`);
|
|
442
|
-
if (!flag.takesValue) {
|
|
443
|
-
setOption(flag, true);
|
|
444
|
-
continue;
|
|
445
|
-
}
|
|
446
|
-
const rest = body.slice(position + 1);
|
|
447
|
-
setOption(flag, rest === "" ? takeValue(`-${letter}`) : rest);
|
|
448
|
-
return;
|
|
326
|
+
if (segment.commands.length === 0) continue;
|
|
327
|
+
const base = segment.parallel ? open : used + 1;
|
|
328
|
+
for (const command of segment.commands) {
|
|
329
|
+
command.stage += base;
|
|
330
|
+
used = Math.max(used, command.stage);
|
|
331
|
+
commands.push(command);
|
|
449
332
|
}
|
|
333
|
+
open = segment.parallel ? open : used + 1;
|
|
450
334
|
}
|
|
451
|
-
|
|
452
|
-
const token = argv[cursor];
|
|
453
|
-
if (afterDoubleDash) positional.push(token);
|
|
454
|
-
else if (token === "--") afterDoubleDash = true;
|
|
455
|
-
else if (token.startsWith("--") && token.length > 2) readLong(token.slice(2));
|
|
456
|
-
else if (token.startsWith("-") && token.length > 1) readShortCluster(token.slice(1));
|
|
457
|
-
else commands.push(token);
|
|
458
|
-
}
|
|
459
|
-
return {
|
|
460
|
-
argv,
|
|
461
|
-
commands,
|
|
462
|
-
help,
|
|
463
|
-
options,
|
|
464
|
-
positional,
|
|
465
|
-
version
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
function findLongFlag(name) {
|
|
469
|
-
const camel = toCamelCase(name);
|
|
470
|
-
return FLAGS.find((flag) => flag.name === camel);
|
|
335
|
+
return commands;
|
|
471
336
|
}
|
|
472
|
-
|
|
473
|
-
|
|
337
|
+
/** Splits a stage-sorted list into its stages, in order. */
|
|
338
|
+
function groupByStage(items, stageOf) {
|
|
339
|
+
return [...Map.groupBy(items, stageOf).values()];
|
|
474
340
|
}
|
|
475
341
|
//#endregion
|
|
476
|
-
//#region src/
|
|
477
|
-
const
|
|
342
|
+
//#region src/plan/token.ts
|
|
343
|
+
const BOOLEAN_OPTIONS = [
|
|
344
|
+
"disabled",
|
|
345
|
+
"parallel",
|
|
346
|
+
"recursive"
|
|
347
|
+
];
|
|
348
|
+
const ACTION_OPTIONS = ["onFailure", "onSuccess"];
|
|
349
|
+
/** Keys a `::` suffix may set. */
|
|
350
|
+
const INLINE_OPTIONS = /* @__PURE__ */ new Set([
|
|
351
|
+
"bgColor",
|
|
352
|
+
"color",
|
|
353
|
+
"cwd",
|
|
354
|
+
"label",
|
|
355
|
+
"output",
|
|
356
|
+
"stderr",
|
|
357
|
+
"stdout",
|
|
358
|
+
...BOOLEAN_OPTIONS,
|
|
359
|
+
...ACTION_OPTIONS
|
|
360
|
+
]);
|
|
478
361
|
/**
|
|
479
|
-
*
|
|
480
|
-
*
|
|
362
|
+
* Splits `"name args::options"` at the last `::`. The options run to the end
|
|
363
|
+
* of the string, so a trailing space stays part of the last value.
|
|
481
364
|
*/
|
|
482
|
-
function
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
if (TIMINGS.has(token)) result.timing = token;
|
|
488
|
-
else result.destination = token;
|
|
489
|
-
}
|
|
490
|
-
return result;
|
|
491
|
-
}
|
|
492
|
-
function toPartialStd(value) {
|
|
493
|
-
return typeof value === "string" ? parsePartialStd(value) : value;
|
|
494
|
-
}
|
|
495
|
-
/** Layers a stream setting, in either form, over `base` one axis at a time. */
|
|
496
|
-
function mergeStd(base, value) {
|
|
497
|
-
return value === void 0 ? base : {
|
|
498
|
-
...base,
|
|
499
|
-
...toPartialStd(value)
|
|
500
|
-
};
|
|
501
|
-
}
|
|
502
|
-
function defaultStd(destination) {
|
|
365
|
+
function splitToken(raw) {
|
|
366
|
+
const trimmed = raw.trimStart();
|
|
367
|
+
const marker = trimmed.lastIndexOf("::");
|
|
368
|
+
const command = (marker === -1 ? trimmed : trimmed.slice(0, marker)).trim();
|
|
369
|
+
const space = command.search(/\s/);
|
|
503
370
|
return {
|
|
504
|
-
|
|
505
|
-
|
|
371
|
+
args: space === -1 ? "" : command.slice(space + 1).trim(),
|
|
372
|
+
name: space === -1 ? command : command.slice(0, space),
|
|
373
|
+
options: marker === -1 ? "" : trimmed.slice(marker + 2)
|
|
506
374
|
};
|
|
507
375
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
return
|
|
529
|
-
}
|
|
530
|
-
function actionError(name, value) {
|
|
531
|
-
return oneOfError(name, value, EXIT_ACTIONS);
|
|
532
|
-
}
|
|
533
|
-
function stdError(name, value) {
|
|
534
|
-
if (typeof value !== "object" || value === null) return `${name} must be a stream setting.`;
|
|
535
|
-
if (!TIMINGS.has(value.timing)) return `Invalid timing ${formatValue(value.timing)} for ${name}; one of ${[...TIMINGS].join(", ")}.`;
|
|
536
|
-
if (typeof value.destination !== "string" || value.destination === "") return `${name} needs a destination to write to.`;
|
|
537
|
-
}
|
|
538
|
-
/** Throws `message` as an `ErrorClass`, if there is one. */
|
|
539
|
-
function check(message, ErrorClass) {
|
|
540
|
-
if (message !== void 0) throw new ErrorClass(message);
|
|
376
|
+
/**
|
|
377
|
+
* Parses `key=value,flag`. Split before placeholders are filled, so a `,` or
|
|
378
|
+
* `=` arriving in a value cannot inject another option.
|
|
379
|
+
*/
|
|
380
|
+
function parseInlineOptions(text, args) {
|
|
381
|
+
const options = {};
|
|
382
|
+
for (const entry of text.split(",")) {
|
|
383
|
+
if (entry.trim() === "") continue;
|
|
384
|
+
const eq = entry.indexOf("=");
|
|
385
|
+
const key = toCamelCase((eq === -1 ? entry : entry.slice(0, eq)).trim());
|
|
386
|
+
const written = eq === -1 ? "true" : entry.slice(eq + 1);
|
|
387
|
+
const value = substitutePlaceholders(key === "label" ? written : written.trim(), args, true);
|
|
388
|
+
if (!INLINE_OPTIONS.has(key)) throw new NormalizeError(`Unknown command option "${toKebabCase(key)}" in "::${text.trim()}".`);
|
|
389
|
+
if (key === "output" || key === "stderr" || key === "stdout") options[key] = parsePartialStd(value);
|
|
390
|
+
else if (BOOLEAN_OPTIONS.includes(key)) options[key] = value !== "false" && value !== "";
|
|
391
|
+
else if (ACTION_OPTIONS.includes(key)) {
|
|
392
|
+
check(actionError(`"${toKebabCase(key)}"`, value), NormalizeError);
|
|
393
|
+
options[key] = value;
|
|
394
|
+
} else options[key] = value;
|
|
395
|
+
}
|
|
396
|
+
return options;
|
|
541
397
|
}
|
|
542
398
|
//#endregion
|
|
543
|
-
//#region src/
|
|
544
|
-
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
"warn",
|
|
548
|
-
"info",
|
|
549
|
-
"debug"
|
|
550
|
-
]);
|
|
551
|
-
const LABEL_MODES = /* @__PURE__ */ new Set([
|
|
552
|
-
"none",
|
|
553
|
-
"auto",
|
|
554
|
-
"custom",
|
|
555
|
-
"all"
|
|
556
|
-
]);
|
|
557
|
-
const COLOR_MODES = /* @__PURE__ */ new Set([
|
|
558
|
-
"auto",
|
|
559
|
-
"none",
|
|
560
|
-
"basic",
|
|
561
|
-
"soft",
|
|
562
|
-
"all"
|
|
563
|
-
]);
|
|
564
|
-
const KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
565
|
-
"color",
|
|
566
|
-
"scripts",
|
|
567
|
-
"commands",
|
|
568
|
-
"cwd",
|
|
569
|
-
"dryRun",
|
|
399
|
+
//#region src/plan/normalize.ts
|
|
400
|
+
/** Keys a settings entry may set. */
|
|
401
|
+
const SETTINGS_OPTIONS = /* @__PURE__ */ new Set([
|
|
402
|
+
...INLINE_OPTIONS,
|
|
570
403
|
"env",
|
|
571
|
-
"
|
|
572
|
-
"jobs",
|
|
573
|
-
"killTimeout",
|
|
574
|
-
"labels",
|
|
575
|
-
"logLevel",
|
|
576
|
-
"onFailure",
|
|
577
|
-
"onSuccess",
|
|
578
|
-
"output",
|
|
579
|
-
"parallel",
|
|
580
|
-
"recursive",
|
|
581
|
-
"showCommand",
|
|
582
|
-
"showExitCode",
|
|
583
|
-
"stderr",
|
|
584
|
-
"stdout",
|
|
585
|
-
"wrap"
|
|
404
|
+
"serial"
|
|
586
405
|
]);
|
|
587
|
-
/**
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
showCommand;
|
|
615
|
-
showExitCode;
|
|
616
|
-
wrap;
|
|
617
|
-
/** `COLUMNS` as runset was started with it: the width where no TTY says. */
|
|
618
|
-
envColumns;
|
|
619
|
-
labels;
|
|
620
|
-
logLevel;
|
|
621
|
-
dryRun;
|
|
622
|
-
formatLabel;
|
|
623
|
-
stdout;
|
|
624
|
-
stderr;
|
|
625
|
-
destinations;
|
|
626
|
-
/** Printed by the run once it is built. */
|
|
627
|
-
warnings = [];
|
|
628
|
-
constructor({ cli, configJs, cwd, destinations, env }) {
|
|
629
|
-
const { options } = cli;
|
|
630
|
-
this.cwd = path.resolve(cwd, options.cwd ?? ".");
|
|
631
|
-
this.destinations = destinations;
|
|
632
|
-
const context = {
|
|
633
|
-
argv: cli.argv,
|
|
634
|
-
cwd: this.cwd,
|
|
635
|
-
env
|
|
406
|
+
/**
|
|
407
|
+
* Turns `config.commands` into the run's commands: flat, validated, numbered
|
|
408
|
+
* by stage and sorted by it.
|
|
409
|
+
*/
|
|
410
|
+
function normalize(config, packageInfo) {
|
|
411
|
+
let workspacePackages;
|
|
412
|
+
const defaults = {
|
|
413
|
+
bgColor: "",
|
|
414
|
+
color: "",
|
|
415
|
+
command: "",
|
|
416
|
+
cwd: config.cwd,
|
|
417
|
+
env: {},
|
|
418
|
+
label: "",
|
|
419
|
+
line: "",
|
|
420
|
+
onFailure: config.onFailure,
|
|
421
|
+
onSuccess: config.onSuccess,
|
|
422
|
+
stage: 0,
|
|
423
|
+
stderr: config.stderr,
|
|
424
|
+
stdout: config.stdout,
|
|
425
|
+
type: "shell"
|
|
426
|
+
};
|
|
427
|
+
function makeLeaf(token, rawOptions, script, pkg = packageInfo) {
|
|
428
|
+
const { disabled, parallel, recursive, ...own } = rawOptions;
|
|
429
|
+
const options = {
|
|
430
|
+
...own,
|
|
431
|
+
stderr: mergeStd(defaults.stderr, own.stderr),
|
|
432
|
+
stdout: mergeStd(defaults.stdout, own.stdout)
|
|
636
433
|
};
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
...
|
|
434
|
+
const args = substitutePlaceholders(token.args, config.args);
|
|
435
|
+
const command = [token.name, args].filter((part) => part !== "").join(" ");
|
|
436
|
+
const line = script === void 0 ? command : [pkg.scripts[script], args].filter((part) => part !== "").join(" ");
|
|
437
|
+
const leaf = {
|
|
438
|
+
...defaults,
|
|
439
|
+
...options,
|
|
440
|
+
command,
|
|
441
|
+
line,
|
|
442
|
+
stage: 0,
|
|
443
|
+
type: script === void 0 ? "shell" : "npm",
|
|
444
|
+
...script === void 0 ? {} : { scriptName: script },
|
|
445
|
+
...pkg === packageInfo ? {} : { packageName: packageNameOf(pkg) }
|
|
648
446
|
};
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
this.formatLabel = file.formatLabel;
|
|
667
|
-
const std = (stream) => {
|
|
668
|
-
let result = defaultStd(stream);
|
|
669
|
-
for (const value of [
|
|
670
|
-
file.output,
|
|
671
|
-
file[stream],
|
|
672
|
-
options.output,
|
|
673
|
-
options[stream]
|
|
674
|
-
]) result = mergeStd(result, value);
|
|
675
|
-
return result;
|
|
447
|
+
if (script !== void 0 && options.cwd === void 0) leaf.cwd = pkg.filePath === "" ? config.cwd : path.dirname(pkg.filePath);
|
|
448
|
+
validateCommand(leaf, {
|
|
449
|
+
disabled,
|
|
450
|
+
parallel,
|
|
451
|
+
recursive
|
|
452
|
+
});
|
|
453
|
+
return leaf;
|
|
454
|
+
}
|
|
455
|
+
/** Links one token's commands (e.g. glob matches) by their `parallel`. */
|
|
456
|
+
function toSegment(leaves, settled) {
|
|
457
|
+
const parallel = settled.parallel ?? config.parallel;
|
|
458
|
+
return {
|
|
459
|
+
commands: settled.disabled ? [] : link(leaves.map((leaf) => ({
|
|
460
|
+
commands: [leaf],
|
|
461
|
+
parallel
|
|
462
|
+
}))),
|
|
463
|
+
parallel
|
|
676
464
|
};
|
|
677
|
-
this.stdout = std("stdout");
|
|
678
|
-
this.stderr = std("stderr");
|
|
679
465
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
466
|
+
function resolveToken(raw, inherited, overrides, seen) {
|
|
467
|
+
const token = splitToken(raw);
|
|
468
|
+
const inline = resolveCwd(parseInlineOptions(token.options, config.args), inherited.cwd ?? config.cwd);
|
|
469
|
+
if (Object.hasOwn(config.scripts, token.name)) {
|
|
470
|
+
if (seen.has(token.name)) throw new NormalizeError(`Command "${token.name}" refers to itself.`);
|
|
471
|
+
const resolved = resolveEntry(config.scripts[token.name], inherited, layer(inline, overrides), /* @__PURE__ */ new Set([...seen, token.name]));
|
|
472
|
+
return token.args === "" ? resolved : appendArgs(resolved, token);
|
|
473
|
+
}
|
|
474
|
+
const settled = layer(inherited, inline, overrides);
|
|
475
|
+
if (settled.recursive ?? config.recursive) {
|
|
476
|
+
workspacePackages ??= readWorkspacePackages(config.cwd);
|
|
477
|
+
const leaves = workspacePackages.flatMap((pkg) => matchScripts(token.name, pkg).map((name) => makeLeaf({
|
|
478
|
+
...token,
|
|
479
|
+
name
|
|
480
|
+
}, settled, name, pkg)));
|
|
481
|
+
if (leaves.length > 0) return toSegment(leaves, settled);
|
|
482
|
+
}
|
|
483
|
+
const names = matchScripts(token.name, packageInfo);
|
|
484
|
+
return toSegment(names.length > 0 || isGlob(token.name) ? names.map((name) => makeLeaf({
|
|
485
|
+
...token,
|
|
486
|
+
name
|
|
487
|
+
}, settled, name)) : [makeLeaf(token, settled, void 0)], settled);
|
|
701
488
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
489
|
+
function appendArgs(segment, token) {
|
|
490
|
+
const [leaf] = segment.commands;
|
|
491
|
+
if (segment.commands.length !== 1 || leaf === void 0) throw new NormalizeError(`Cannot pass arguments to "${token.name}": it expands to more than one command.`);
|
|
492
|
+
const args = substitutePlaceholders(token.args, config.args);
|
|
493
|
+
const extended = {
|
|
494
|
+
...leaf,
|
|
495
|
+
command: `${leaf.command} ${args}`,
|
|
496
|
+
line: `${leaf.line} ${args}`
|
|
497
|
+
};
|
|
498
|
+
return {
|
|
499
|
+
...segment,
|
|
500
|
+
commands: [extended]
|
|
501
|
+
};
|
|
705
502
|
}
|
|
706
|
-
|
|
503
|
+
/** One list entry other than a settings entry. */
|
|
504
|
+
function resolve(definition, inherited, overrides, seen) {
|
|
505
|
+
if (!definition) return EMPTY_SEGMENT;
|
|
506
|
+
if (typeof definition === "string") return resolveToken(definition, inherited, overrides, seen);
|
|
507
|
+
if (Array.isArray(definition)) throw new NormalizeError("A list of commands belongs in \"commands\" or in a \"scripts\" entry, not inside one.");
|
|
508
|
+
if (isSettingsEntry(definition)) throw new NormalizeError("A command definition needs a \"command\" field.");
|
|
509
|
+
const entry = definition;
|
|
510
|
+
if (typeof entry.command !== "string") throw new NormalizeError("A command's \"command\" must be a string.");
|
|
511
|
+
const { command, ...raw } = entry;
|
|
512
|
+
return resolveToken(command, layer(inherited, resolveCwd(raw, inherited.cwd ?? config.cwd)), overrides, seen);
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* `commands`, or a `scripts` entry that is a list. The list's own
|
|
516
|
+
* `parallel` places the list as a whole and is not passed to its members.
|
|
517
|
+
*/
|
|
518
|
+
function resolveList(definitions, inherited, overrides, seen) {
|
|
519
|
+
const { parallel: inheritedParallel, ...passedDown } = inherited;
|
|
520
|
+
const { parallel: overriddenParallel, ...passedOver } = overrides;
|
|
521
|
+
const parallel = overriddenParallel ?? inheritedParallel ?? config.parallel;
|
|
522
|
+
check(booleanError("\"parallel\" on a list of commands", parallel), NormalizeError);
|
|
523
|
+
const segments = [];
|
|
524
|
+
let settled = passedDown;
|
|
525
|
+
for (const definition of definitions) {
|
|
526
|
+
if (isSettingsEntry(definition)) {
|
|
527
|
+
const written = readSettings(definition, settled.cwd ?? config.cwd);
|
|
528
|
+
settled = layer(settled, written);
|
|
529
|
+
if (written.parallel !== void 0) segments.push(BOUNDARY);
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
segments.push(resolve(definition, settled, passedOver, seen));
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
commands: link(segments),
|
|
536
|
+
parallel
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function resolveEntry(entry, inherited, overrides, seen) {
|
|
540
|
+
return Array.isArray(entry) ? resolveList(entry, inherited, overrides, seen) : resolve(entry, inherited, overrides, seen);
|
|
541
|
+
}
|
|
542
|
+
const { commands } = resolveList(config.commands, {}, {}, /* @__PURE__ */ new Set());
|
|
543
|
+
commands.sort((first, second) => first.stage - second.stage);
|
|
544
|
+
return commands;
|
|
545
|
+
}
|
|
546
|
+
/** True for an object written without a `command` — a settings entry. */
|
|
547
|
+
function isSettingsEntry(definition) {
|
|
548
|
+
return typeof definition === "object" && definition !== null && !Array.isArray(definition) && !("command" in definition);
|
|
549
|
+
}
|
|
550
|
+
/** The scripts of `pkg` a token name matches, glob or not. */
|
|
551
|
+
function matchScripts(name, pkg) {
|
|
552
|
+
if (isGlob(name)) return matchGlob(name, Object.keys(pkg.scripts));
|
|
553
|
+
return Object.hasOwn(pkg.scripts, name) ? [name] : [];
|
|
554
|
+
}
|
|
555
|
+
function packageNameOf(pkg) {
|
|
556
|
+
return pkg.name || path.basename(path.dirname(pkg.filePath));
|
|
557
|
+
}
|
|
558
|
+
/** Resolves a relative `cwd` against the inherited one, so directories nest. */
|
|
559
|
+
function resolveCwd(options, base) {
|
|
560
|
+
return options.cwd === void 0 ? options : {
|
|
561
|
+
...options,
|
|
562
|
+
cwd: path.resolve(base, options.cwd)
|
|
563
|
+
};
|
|
564
|
+
}
|
|
707
565
|
/**
|
|
708
|
-
*
|
|
709
|
-
*
|
|
710
|
-
* left for `validate` to refuse.
|
|
566
|
+
* Reads a settings entry into the options the rest of its list inherits.
|
|
567
|
+
* Keys are checked strictly: `{ paralel: true }` would otherwise do nothing.
|
|
711
568
|
*/
|
|
712
|
-
function
|
|
713
|
-
|
|
714
|
-
if (
|
|
715
|
-
|
|
569
|
+
function readSettings(entry, base) {
|
|
570
|
+
const keys = Object.keys(entry);
|
|
571
|
+
if (keys.length === 0) throw new NormalizeError("A settings entry must set at least one option.");
|
|
572
|
+
for (const key of keys) if (!SETTINGS_OPTIONS.has(key)) throw new NormalizeError(`Unknown option "${toKebabCase(key)}" in a settings entry.`);
|
|
573
|
+
const { serial, ...options } = entry;
|
|
574
|
+
if (serial !== void 0) {
|
|
575
|
+
check(booleanError("\"serial\"", serial), NormalizeError);
|
|
576
|
+
if (options.parallel !== void 0) throw new NormalizeError("A settings entry takes \"parallel\" or \"serial\", not both.");
|
|
577
|
+
options.parallel = !serial;
|
|
578
|
+
}
|
|
579
|
+
return resolveCwd(options, base);
|
|
716
580
|
}
|
|
717
|
-
/**
|
|
718
|
-
function
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
return
|
|
581
|
+
/** Layers option bags, later over earlier; stream settings merge per axis. */
|
|
582
|
+
function layer(...bags) {
|
|
583
|
+
const result = {};
|
|
584
|
+
for (const { output, stderr, stdout, ...rest } of bags) Object.assign(result, rest);
|
|
585
|
+
return {
|
|
586
|
+
...result,
|
|
587
|
+
...layerStreams({
|
|
588
|
+
stderr: {},
|
|
589
|
+
stdout: {}
|
|
590
|
+
}, bags)
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Checks what a config file could have written by hand: the leaf, and the
|
|
595
|
+
* options that were read while laying it out.
|
|
596
|
+
*/
|
|
597
|
+
function validateCommand(command, read) {
|
|
598
|
+
const where = (key) => `"${toKebabCase(key)}" on "${command.command}"`;
|
|
599
|
+
check([
|
|
600
|
+
...ACTION_OPTIONS.map((key) => actionError(where(key), command[key])),
|
|
601
|
+
...BOOLEAN_OPTIONS.map((key) => read[key] === void 0 ? void 0 : booleanError(where(key), read[key])),
|
|
602
|
+
...[
|
|
603
|
+
"bgColor",
|
|
604
|
+
"color",
|
|
605
|
+
"cwd",
|
|
606
|
+
"label"
|
|
607
|
+
].map((key) => stringError(where(key), command[key])),
|
|
608
|
+
...["stderr", "stdout"].map((key) => stdError(where(key), command[key])),
|
|
609
|
+
envError(where("env"), command.env)
|
|
610
|
+
].find((error) => error !== void 0), NormalizeError);
|
|
722
611
|
}
|
|
723
612
|
//#endregion
|
|
724
|
-
//#region src/utils/
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
},
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
613
|
+
//#region src/utils/terminal.ts
|
|
614
|
+
/**
|
|
615
|
+
* Measuring and wrapping text the way a terminal lays it out: escape sequences
|
|
616
|
+
* take no room, wide characters (CJK, emoji) take two columns.
|
|
617
|
+
*/
|
|
618
|
+
const ESCAPE = /\u001B(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007\u001B]*(?:\u0007|\u001B\\)?|[@-Z\\-_])/g;
|
|
619
|
+
const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
620
|
+
/** Printable ASCII only: one column per character, nothing to segment. */
|
|
621
|
+
const PLAIN = /^[ -~]*$/;
|
|
622
|
+
function isTerminal(stream) {
|
|
623
|
+
return stream.isTTY === true;
|
|
624
|
+
}
|
|
625
|
+
/** The width a terminal stream reports; `undefined` for anything else. */
|
|
626
|
+
function terminalColumns(stream) {
|
|
627
|
+
const { columns } = stream;
|
|
628
|
+
return isTerminal(stream) && columns > 0 ? columns : void 0;
|
|
629
|
+
}
|
|
630
|
+
/** `COLUMNS` as a positive integer, or `undefined`. */
|
|
631
|
+
function parseColumns(value) {
|
|
632
|
+
const columns = Number(value);
|
|
633
|
+
return Number.isInteger(columns) && columns > 0 ? columns : void 0;
|
|
634
|
+
}
|
|
635
|
+
/** The columns `text` takes on screen. */
|
|
636
|
+
function visibleWidth(text) {
|
|
637
|
+
if (PLAIN.test(text)) return text.length;
|
|
638
|
+
let width = 0;
|
|
639
|
+
for (const { segment } of segmenter.segment(text.replace(ESCAPE, ""))) width += columnWidth(segment, width);
|
|
640
|
+
return width;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Hard-wraps one line to `width` columns, the way the terminal would have, so
|
|
644
|
+
* each piece can carry a label of its own. A style still open at a break is
|
|
645
|
+
* reset at the end of the piece and re-applied at the start of the next, so a
|
|
646
|
+
* label never inherits it. `offset` is the column the line starts at, for tab
|
|
647
|
+
* stops. A line that moves the cursor — `\r`, or any escape but a color — is
|
|
648
|
+
* left whole: where it lands is not something runset can know.
|
|
649
|
+
*/
|
|
650
|
+
function wrapLine(line, width, offset = 0) {
|
|
651
|
+
const fits = line.length * 2 <= width && !line.includes(" ");
|
|
652
|
+
if (width < 1 || fits || line.includes("\r")) return [line];
|
|
653
|
+
if (PLAIN.test(line)) {
|
|
654
|
+
const pieces = [];
|
|
655
|
+
for (let start = 0; start < line.length; start += width) pieces.push(line.slice(start, start + width));
|
|
656
|
+
return pieces;
|
|
657
|
+
}
|
|
658
|
+
const pieces = [];
|
|
659
|
+
let current = "";
|
|
660
|
+
let column = 0;
|
|
661
|
+
let style = [];
|
|
662
|
+
for (const token of tokenize(line)) {
|
|
663
|
+
if (token.escape) {
|
|
664
|
+
if (token.text.startsWith("\x1B[") && !isSgr(token.text)) return [line];
|
|
665
|
+
current += token.text;
|
|
666
|
+
style = applySgr(style, token.text);
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
for (const { segment } of segmenter.segment(token.text)) {
|
|
670
|
+
let size = columnWidth(segment, offset + column);
|
|
671
|
+
if (column > 0 && column + size > width) {
|
|
672
|
+
pieces.push(style.length > 0 ? `${current}\u001B[0m` : current);
|
|
673
|
+
current = style.join("");
|
|
674
|
+
column = 0;
|
|
675
|
+
size = columnWidth(segment, offset);
|
|
676
|
+
}
|
|
677
|
+
current += segment;
|
|
678
|
+
column += size;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
pieces.push(current);
|
|
682
|
+
return pieces;
|
|
683
|
+
}
|
|
684
|
+
function tokenize(text) {
|
|
685
|
+
const tokens = [];
|
|
686
|
+
let last = 0;
|
|
687
|
+
for (const match of text.matchAll(ESCAPE)) {
|
|
688
|
+
if (match.index > last) tokens.push({
|
|
689
|
+
escape: false,
|
|
690
|
+
text: text.slice(last, match.index)
|
|
691
|
+
});
|
|
692
|
+
tokens.push({
|
|
693
|
+
escape: true,
|
|
694
|
+
text: match[0]
|
|
695
|
+
});
|
|
696
|
+
last = match.index + match[0].length;
|
|
697
|
+
}
|
|
698
|
+
if (last < text.length) tokens.push({
|
|
699
|
+
escape: false,
|
|
700
|
+
text: text.slice(last)
|
|
701
|
+
});
|
|
702
|
+
return tokens;
|
|
703
|
+
}
|
|
704
|
+
/** A color or style (`ESC [ … m`); any other CSI moves the cursor. */
|
|
705
|
+
function isSgr(sequence) {
|
|
706
|
+
return sequence.startsWith("\x1B[") && sequence.endsWith("m");
|
|
707
|
+
}
|
|
708
|
+
/** The SGR sequences in force after `sequence`; a reset clears them. */
|
|
709
|
+
function applySgr(style, sequence) {
|
|
710
|
+
if (!isSgr(sequence)) return style;
|
|
711
|
+
const params = sequence.slice(2, -1);
|
|
712
|
+
if (params === "" || params === "0") return [];
|
|
713
|
+
return params.startsWith("0;") ? [sequence] : [...style, sequence];
|
|
714
|
+
}
|
|
715
|
+
/** The columns one grapheme takes when it starts at `column`. */
|
|
716
|
+
function columnWidth(grapheme, column) {
|
|
717
|
+
if (grapheme === " ") return 8 - column % 8;
|
|
718
|
+
if (/^[ -~]$/.test(grapheme)) return 1;
|
|
719
|
+
if (/^[\p{Cc}\p{Cf}\p{Mn}\p{Me}]+$/u.test(grapheme)) return 0;
|
|
720
|
+
if (/\p{Extended_Pictographic}|\p{Regional_Indicator}/u.test(grapheme) && /\p{Emoji_Presentation}|\uFE0F/u.test(grapheme)) return 2;
|
|
721
|
+
return /^[\u1100-\u115F\u2E80-\u303E\u3041-\u33FF\u3400-\u4DBF\u4E00-\u9FFF\uA000-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFF60\uFFE0-\uFFE6\u{20000}-\u{3FFFD}]/u.test(grapheme) ? 2 : 1;
|
|
722
|
+
}
|
|
723
|
+
//#endregion
|
|
724
|
+
//#region src/config/loadConfig.ts
|
|
725
|
+
/**
|
|
726
|
+
* Loads `runset.config.*` — the `explicitPath`, or the first one found walking
|
|
727
|
+
* up from `cwd`, where a `package.json` `runset` section counts as one too —
|
|
728
|
+
* and unwraps a default export or a factory function.
|
|
729
|
+
*/
|
|
730
|
+
function loadConfigJs(context, explicitPath) {
|
|
731
|
+
if (explicitPath === void 0) return unwrapConfigJs(findConfigJs(context.cwd) ?? {}, context);
|
|
732
|
+
const filePath = path.resolve(context.cwd, explicitPath);
|
|
733
|
+
if (!fs.existsSync(filePath)) throw new ConfigError(`Config file not found: ${explicitPath}`);
|
|
734
|
+
return unwrapConfigJs(importSync(filePath), context);
|
|
735
|
+
}
|
|
736
|
+
function unwrapConfigJs(source, context) {
|
|
737
|
+
const unwrapped = source.default ?? source;
|
|
738
|
+
return typeof unwrapped === "function" ? unwrapped(context) : unwrapped;
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* The nearest config walking up from `cwd`. Within one directory a
|
|
742
|
+
* `runset.config.*` file wins over the `package.json` `runset` section.
|
|
743
|
+
*/
|
|
744
|
+
function findConfigJs(cwd) {
|
|
745
|
+
for (const dir of ancestors(cwd)) {
|
|
746
|
+
for (const extension of [
|
|
747
|
+
"ts",
|
|
748
|
+
"js",
|
|
749
|
+
"mjs",
|
|
750
|
+
"cjs",
|
|
751
|
+
"json"
|
|
752
|
+
]) {
|
|
753
|
+
const candidate = path.join(dir, `runset.config.${extension}`);
|
|
754
|
+
if (fs.existsSync(candidate)) return importSync(candidate);
|
|
755
|
+
}
|
|
756
|
+
const section = readPackageJsonSection(path.join(dir, "package.json"));
|
|
757
|
+
if (section !== void 0) return section;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
function readPackageJsonSection(filePath) {
|
|
761
|
+
const runset = readJson(filePath)?.runset;
|
|
762
|
+
if (runset === void 0) return;
|
|
763
|
+
if (!isPlainObject(runset)) throw new ConfigError(`${filePath}: "runset" must be an object`);
|
|
764
|
+
return runset;
|
|
765
|
+
}
|
|
766
|
+
//#endregion
|
|
767
|
+
//#region src/config/winArgv.ts
|
|
768
|
+
/**
|
|
769
|
+
* Windows only: rejoins `'single quoted'` tokens that `cmd.exe` leaves split,
|
|
770
|
+
* joined by single spaces. The identity everywhere else.
|
|
771
|
+
*/
|
|
772
|
+
function regroupQuoted(argv, platform = process.platform) {
|
|
773
|
+
if (platform !== "win32") return argv;
|
|
774
|
+
const result = [];
|
|
775
|
+
let open;
|
|
776
|
+
for (const token of argv) {
|
|
777
|
+
if (open === void 0) {
|
|
778
|
+
if (token.startsWith("'") && token.endsWith("'") && token.length > 1) result.push(token.slice(1, -1));
|
|
779
|
+
else if (token.startsWith("'")) open = [token.slice(1)];
|
|
780
|
+
else result.push(token);
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
if (token.endsWith("'")) {
|
|
784
|
+
open.push(token.slice(0, -1));
|
|
785
|
+
result.push(open.join(" "));
|
|
786
|
+
open = void 0;
|
|
787
|
+
} else open.push(token);
|
|
788
|
+
}
|
|
789
|
+
if (open !== void 0) throw new CliError(`Unterminated quote in "'${open.join(" ")}".`);
|
|
790
|
+
return result;
|
|
791
|
+
}
|
|
792
|
+
//#endregion
|
|
793
|
+
//#region src/config/parseCli.ts
|
|
794
|
+
const FLAGS = [
|
|
795
|
+
{
|
|
796
|
+
name: "parallel",
|
|
797
|
+
short: "p",
|
|
798
|
+
takesValue: false
|
|
758
799
|
},
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
800
|
+
{
|
|
801
|
+
name: "serial",
|
|
802
|
+
short: "s",
|
|
803
|
+
takesValue: false
|
|
762
804
|
},
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
805
|
+
{
|
|
806
|
+
name: "recursive",
|
|
807
|
+
short: "r",
|
|
808
|
+
takesValue: false
|
|
766
809
|
},
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
810
|
+
{
|
|
811
|
+
name: "jobs",
|
|
812
|
+
short: "j",
|
|
813
|
+
takesValue: true
|
|
770
814
|
},
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
815
|
+
{
|
|
816
|
+
name: "killTimeout",
|
|
817
|
+
takesValue: true
|
|
774
818
|
},
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
819
|
+
{
|
|
820
|
+
name: "onSuccess",
|
|
821
|
+
takesValue: true
|
|
778
822
|
},
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
823
|
+
{
|
|
824
|
+
name: "onFailure",
|
|
825
|
+
takesValue: true
|
|
782
826
|
},
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
827
|
+
{
|
|
828
|
+
name: "config",
|
|
829
|
+
short: "c",
|
|
830
|
+
takesValue: true
|
|
786
831
|
},
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
832
|
+
{
|
|
833
|
+
name: "cwd",
|
|
834
|
+
takesValue: true
|
|
790
835
|
},
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
836
|
+
{
|
|
837
|
+
name: "env",
|
|
838
|
+
short: "e",
|
|
839
|
+
takesValue: true
|
|
794
840
|
},
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
841
|
+
{
|
|
842
|
+
name: "color",
|
|
843
|
+
negated: "none",
|
|
844
|
+
takesValue: true
|
|
798
845
|
},
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
846
|
+
{
|
|
847
|
+
name: "logLevel",
|
|
848
|
+
takesValue: true
|
|
802
849
|
},
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
},
|
|
807
|
-
iris: {
|
|
808
|
-
index: 141,
|
|
809
|
-
fallback: "blueBright"
|
|
810
|
-
},
|
|
811
|
-
ice: {
|
|
812
|
-
index: 117,
|
|
813
|
-
fallback: "cyanBright"
|
|
814
|
-
},
|
|
815
|
-
salmon: {
|
|
816
|
-
index: 174,
|
|
817
|
-
fallback: "redBright"
|
|
818
|
-
},
|
|
819
|
-
olive: {
|
|
820
|
-
index: 150,
|
|
821
|
-
fallback: "greenBright"
|
|
822
|
-
},
|
|
823
|
-
navy: {
|
|
824
|
-
index: 24,
|
|
825
|
-
fallback: "blue",
|
|
826
|
-
foreground: "paper"
|
|
827
|
-
},
|
|
828
|
-
plum: {
|
|
829
|
-
index: 96,
|
|
830
|
-
fallback: "magenta",
|
|
831
|
-
foreground: "paper"
|
|
832
|
-
},
|
|
833
|
-
forest: {
|
|
834
|
-
index: 22,
|
|
835
|
-
fallback: "green",
|
|
836
|
-
foreground: "paper"
|
|
837
|
-
},
|
|
838
|
-
wine: {
|
|
839
|
-
index: 89,
|
|
840
|
-
fallback: "red",
|
|
841
|
-
foreground: "paper"
|
|
842
|
-
},
|
|
843
|
-
ocean: {
|
|
844
|
-
index: 25,
|
|
845
|
-
fallback: "blue",
|
|
846
|
-
foreground: "paper"
|
|
847
|
-
},
|
|
848
|
-
indigo: {
|
|
849
|
-
index: 61,
|
|
850
|
-
fallback: "blue",
|
|
851
|
-
foreground: "paper"
|
|
852
|
-
},
|
|
853
|
-
copper: {
|
|
854
|
-
index: 94,
|
|
855
|
-
fallback: "red",
|
|
856
|
-
foreground: "paper"
|
|
857
|
-
},
|
|
858
|
-
pine: {
|
|
859
|
-
index: 23,
|
|
860
|
-
fallback: "cyan",
|
|
861
|
-
foreground: "paper"
|
|
862
|
-
},
|
|
863
|
-
violet: {
|
|
864
|
-
index: 55,
|
|
865
|
-
fallback: "magenta",
|
|
866
|
-
foreground: "paper"
|
|
867
|
-
},
|
|
868
|
-
brick: {
|
|
869
|
-
index: 88,
|
|
870
|
-
fallback: "red",
|
|
871
|
-
foreground: "paper"
|
|
872
|
-
},
|
|
873
|
-
slate: {
|
|
874
|
-
index: 60,
|
|
875
|
-
fallback: "blue",
|
|
876
|
-
foreground: "paper"
|
|
877
|
-
},
|
|
878
|
-
cocoa: {
|
|
879
|
-
index: 95,
|
|
880
|
-
fallback: "red",
|
|
881
|
-
foreground: "paper"
|
|
882
|
-
},
|
|
883
|
-
ink: {
|
|
884
|
-
index: 235,
|
|
885
|
-
fallback: "black"
|
|
886
|
-
},
|
|
887
|
-
paper: {
|
|
888
|
-
index: 255,
|
|
889
|
-
fallback: "whiteBright"
|
|
890
|
-
}
|
|
891
|
-
};
|
|
892
|
-
/** Every shade by its foreground and its background name, e.g. `mint`, `bgMint`. */
|
|
893
|
-
const SHADE_NAMES = new Map(Object.entries(SHADES).flatMap(([name, custom]) => [[name, custom], [toCamelCase(`bg-${name}`), custom]]));
|
|
894
|
-
const BASIC_PALETTE = [
|
|
895
|
-
{
|
|
896
|
-
bgColor: "bgGreen",
|
|
897
|
-
color: "black"
|
|
898
|
-
},
|
|
899
|
-
{
|
|
900
|
-
bgColor: "bgBlue",
|
|
901
|
-
color: "white"
|
|
850
|
+
{
|
|
851
|
+
name: "dryRun",
|
|
852
|
+
takesValue: false
|
|
902
853
|
},
|
|
903
854
|
{
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
},
|
|
907
|
-
{
|
|
908
|
-
bgColor: "bgCyan",
|
|
909
|
-
color: "black"
|
|
855
|
+
name: "stdout",
|
|
856
|
+
takesValue: true
|
|
910
857
|
},
|
|
911
858
|
{
|
|
912
|
-
|
|
913
|
-
|
|
859
|
+
name: "stderr",
|
|
860
|
+
takesValue: true
|
|
914
861
|
},
|
|
915
862
|
{
|
|
916
|
-
|
|
917
|
-
|
|
863
|
+
name: "output",
|
|
864
|
+
short: "o",
|
|
865
|
+
takesValue: true
|
|
918
866
|
},
|
|
919
867
|
{
|
|
920
|
-
|
|
921
|
-
|
|
868
|
+
name: "labels",
|
|
869
|
+
takesValue: true
|
|
922
870
|
},
|
|
923
871
|
{
|
|
924
|
-
|
|
925
|
-
|
|
872
|
+
name: "showCommand",
|
|
873
|
+
takesValue: false
|
|
926
874
|
},
|
|
927
875
|
{
|
|
928
|
-
|
|
929
|
-
|
|
876
|
+
name: "showExitCode",
|
|
877
|
+
takesValue: false
|
|
930
878
|
},
|
|
931
879
|
{
|
|
932
|
-
|
|
933
|
-
|
|
880
|
+
name: "wrap",
|
|
881
|
+
takesValue: false
|
|
934
882
|
},
|
|
935
883
|
{
|
|
936
|
-
|
|
937
|
-
|
|
884
|
+
name: "help",
|
|
885
|
+
short: "h",
|
|
886
|
+
takesValue: false
|
|
938
887
|
},
|
|
939
888
|
{
|
|
940
|
-
|
|
941
|
-
|
|
889
|
+
name: "version",
|
|
890
|
+
short: "v",
|
|
891
|
+
takesValue: false
|
|
942
892
|
}
|
|
943
893
|
];
|
|
944
|
-
/** Interleave hue families so adjacent assignments remain easy to tell apart. */
|
|
945
|
-
const PALETTE = Object.entries(SHADES).filter(([name]) => name !== "ink" && name !== "paper").map(([name, { foreground }]) => ({
|
|
946
|
-
bgColor: toCamelCase(`bg-${name}`),
|
|
947
|
-
color: foreground ?? "ink"
|
|
948
|
-
}));
|
|
949
|
-
/** What `colorLabels` hands out under each mode; `none` still picks, unseen. */
|
|
950
|
-
const PALETTES = {
|
|
951
|
-
none: BASIC_PALETTE,
|
|
952
|
-
basic: BASIC_PALETTE,
|
|
953
|
-
soft: PALETTE.filter((pair) => pair.color === "ink"),
|
|
954
|
-
all: PALETTE
|
|
955
|
-
};
|
|
956
|
-
/** A built-in style or one of runset's foreground/background shades. */
|
|
957
|
-
function isColorName(name) {
|
|
958
|
-
return KNOWN.has(name) || shade(name) !== void 0;
|
|
959
|
-
}
|
|
960
|
-
/** Styles text; a shade is painted from the 256-color table. */
|
|
961
|
-
function paint(text, colors) {
|
|
962
|
-
let result = text;
|
|
963
|
-
for (const name of colors.toReversed()) {
|
|
964
|
-
const custom = shade(name);
|
|
965
|
-
if (custom !== void 0) {
|
|
966
|
-
const background = name.startsWith("bg");
|
|
967
|
-
const open = `\u001B[${background ? 48 : 38};5;${custom.index}m`;
|
|
968
|
-
const close = `\u001B[${background ? 49 : 39}m`;
|
|
969
|
-
result = open + result.replaceAll(close, open) + close;
|
|
970
|
-
} else if (KNOWN.has(name)) result = styleText(name, result, { validateStream: false });
|
|
971
|
-
}
|
|
972
|
-
return result;
|
|
973
|
-
}
|
|
974
|
-
/** The theme color used for a shade on a basic terminal. */
|
|
975
|
-
function fallbackColor(name) {
|
|
976
|
-
const custom = shade(name);
|
|
977
|
-
if (custom === void 0) return name;
|
|
978
|
-
return name.startsWith("bg") ? toCamelCase(`bg-${custom.fallback}`) : custom.fallback;
|
|
979
|
-
}
|
|
980
|
-
/** A stable palette index for a label. */
|
|
981
|
-
function paletteIndex(label, palette = PALETTE) {
|
|
982
|
-
/** The largest modulus that keeps `hash * 31 + code` an exact integer. */
|
|
983
|
-
const HASH_MODULUS = 2147483647;
|
|
984
|
-
let hash = 0;
|
|
985
|
-
for (const character of label) hash = (hash * 31 + (character.codePointAt(0) ?? 0)) % HASH_MODULUS;
|
|
986
|
-
return hash % palette.length;
|
|
987
|
-
}
|
|
988
|
-
function shade(name) {
|
|
989
|
-
return SHADE_NAMES.get(name);
|
|
990
|
-
}
|
|
991
|
-
//#endregion
|
|
992
|
-
//#region src/plan/stages.ts
|
|
993
|
-
const EMPTY_SEGMENT = {
|
|
994
|
-
commands: [],
|
|
995
|
-
parallel: false
|
|
996
|
-
};
|
|
997
|
-
/** A `-p`/`-s`: what follows starts a new group past everything before it. */
|
|
998
|
-
const BOUNDARY = {
|
|
999
|
-
boundary: true,
|
|
1000
|
-
commands: [],
|
|
1001
|
-
parallel: false
|
|
1002
|
-
};
|
|
1003
894
|
/**
|
|
1004
|
-
*
|
|
1005
|
-
*
|
|
1006
|
-
*
|
|
895
|
+
* Splits argv into options, commands, and the literal arguments after `--`.
|
|
896
|
+
* Each `-p`/`-s` becomes a settings entry where it was typed, the same object
|
|
897
|
+
* a config file would write.
|
|
1007
898
|
*/
|
|
1008
|
-
function
|
|
899
|
+
function parseCli(rawArgv) {
|
|
900
|
+
const argv = regroupQuoted(rawArgv);
|
|
901
|
+
const options = {};
|
|
1009
902
|
const commands = [];
|
|
1010
|
-
|
|
1011
|
-
let
|
|
1012
|
-
|
|
1013
|
-
let
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
current = [];
|
|
1039
|
-
stage = own;
|
|
1040
|
-
stages.push(current);
|
|
903
|
+
let positional = [];
|
|
904
|
+
let help = false;
|
|
905
|
+
let version = false;
|
|
906
|
+
let cursor = 0;
|
|
907
|
+
function setOption(spec, raw) {
|
|
908
|
+
switch (spec.name) {
|
|
909
|
+
case "env": {
|
|
910
|
+
const text = String(raw);
|
|
911
|
+
const eq = text.indexOf("=");
|
|
912
|
+
if (eq < 1) throw new CliError(`Option "--env" takes NAME=value, not "${text}".`);
|
|
913
|
+
(options.env ??= {})[text.slice(0, eq)] = text.slice(eq + 1);
|
|
914
|
+
break;
|
|
915
|
+
}
|
|
916
|
+
case "help":
|
|
917
|
+
help = true;
|
|
918
|
+
break;
|
|
919
|
+
case "jobs":
|
|
920
|
+
case "killTimeout":
|
|
921
|
+
options[spec.name] = Number(raw);
|
|
922
|
+
break;
|
|
923
|
+
case "parallel":
|
|
924
|
+
case "serial":
|
|
925
|
+
commands.push({ parallel: spec.name === "parallel" });
|
|
926
|
+
break;
|
|
927
|
+
case "version":
|
|
928
|
+
version = true;
|
|
929
|
+
break;
|
|
930
|
+
default: options[spec.name] = raw;
|
|
1041
931
|
}
|
|
1042
|
-
current.push(item);
|
|
1043
932
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
* Settles who is labelled, per `config.labels`. Under `auto`, a command
|
|
1050
|
-
* sharing a stage is named after itself, whether or not someone in its stage
|
|
1051
|
-
* has a label of their own. Under `custom` it gets a blank label instead,
|
|
1052
|
-
* holding the column when someone in its stage has one.
|
|
1053
|
-
*/
|
|
1054
|
-
function assignAutoLabels({ commands, config }) {
|
|
1055
|
-
const mode = config.labels;
|
|
1056
|
-
if (mode === "none") {
|
|
1057
|
-
for (const command of commands) command.label = "";
|
|
1058
|
-
return;
|
|
933
|
+
function takeValue(label) {
|
|
934
|
+
const value = argv[cursor + 1];
|
|
935
|
+
if (value === void 0) throw new CliError(`Option "${label}" needs a value.`);
|
|
936
|
+
cursor += 1;
|
|
937
|
+
return value;
|
|
1059
938
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
const
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
939
|
+
function readLong(body) {
|
|
940
|
+
const eq = body.indexOf("=");
|
|
941
|
+
const name = eq === -1 ? body : body.slice(0, eq);
|
|
942
|
+
const inlineValue = eq === -1 ? void 0 : body.slice(eq + 1);
|
|
943
|
+
const flag = findLongFlag(name);
|
|
944
|
+
if (flag === void 0 && /^no-?[A-Za-z]/.test(name)) {
|
|
945
|
+
const negated = findLongFlag(name.replace(/^no-?/, ""));
|
|
946
|
+
if (negated && (!negated.takesValue || negated.negated !== void 0)) {
|
|
947
|
+
setOption(negated, negated.negated ?? false);
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
1070
950
|
}
|
|
951
|
+
if (flag === void 0) throw new CliError(`Unknown option "--${name}".`);
|
|
952
|
+
if (flag.takesValue) setOption(flag, inlineValue ?? takeValue(`--${name}`));
|
|
953
|
+
else setOption(flag, true);
|
|
1071
954
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
955
|
+
function readShortCluster(body) {
|
|
956
|
+
for (let position = 0; position < body.length; position += 1) {
|
|
957
|
+
const letter = body[position];
|
|
958
|
+
const flag = findShortFlag(letter);
|
|
959
|
+
if (flag === void 0) throw new CliError(`Unknown option "-${letter}".`);
|
|
960
|
+
if (!flag.takesValue) {
|
|
961
|
+
setOption(flag, true);
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
const rest = body.slice(position + 1);
|
|
965
|
+
setOption(flag, rest === "" ? takeValue(`-${letter}`) : rest);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
1085
968
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
969
|
+
for (; cursor < argv.length; cursor += 1) {
|
|
970
|
+
const token = argv[cursor];
|
|
971
|
+
if (token === "--") {
|
|
972
|
+
positional = argv.slice(cursor + 1);
|
|
973
|
+
break;
|
|
974
|
+
}
|
|
975
|
+
if (token.startsWith("--") && token.length > 2) readLong(token.slice(2));
|
|
976
|
+
else if (token.startsWith("-") && token.length > 1) readShortCluster(token.slice(1));
|
|
977
|
+
else commands.push(token);
|
|
1095
978
|
}
|
|
979
|
+
return {
|
|
980
|
+
argv,
|
|
981
|
+
commands,
|
|
982
|
+
help,
|
|
983
|
+
options,
|
|
984
|
+
positional,
|
|
985
|
+
version
|
|
986
|
+
};
|
|
1096
987
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
const width = Math.max(0, ...labeled.map((command) => command.label.length));
|
|
1101
|
-
for (const command of labeled) command.label = command.label.padEnd(width);
|
|
1102
|
-
}
|
|
1103
|
-
/** The script name, or the program a shell command runs. */
|
|
1104
|
-
function nameOf(command) {
|
|
1105
|
-
return command.packageName ?? command.scriptName ?? command.command.split(/\s/, 1)[0] ?? "";
|
|
1106
|
-
}
|
|
1107
|
-
/** `serve`, `serve` → `serve#1`, `serve#2`. */
|
|
1108
|
-
function numberRepeats(named) {
|
|
1109
|
-
const totals = /* @__PURE__ */ new Map();
|
|
1110
|
-
for (const command of named) totals.set(command.label, (totals.get(command.label) ?? 0) + 1);
|
|
1111
|
-
const counters = /* @__PURE__ */ new Map();
|
|
1112
|
-
for (const command of named) {
|
|
1113
|
-
if ((totals.get(command.label) ?? 0) < 2) continue;
|
|
1114
|
-
const index = (counters.get(command.label) ?? 0) + 1;
|
|
1115
|
-
counters.set(command.label, index);
|
|
1116
|
-
command.label = `${command.label}#${index}`;
|
|
1117
|
-
}
|
|
988
|
+
function findLongFlag(name) {
|
|
989
|
+
const camel = toCamelCase(name);
|
|
990
|
+
return FLAGS.find((flag) => flag.name === camel);
|
|
1118
991
|
}
|
|
1119
|
-
function
|
|
1120
|
-
|
|
1121
|
-
let index = preferred;
|
|
1122
|
-
while (taken.has(index)) index = (index + 1) % size;
|
|
1123
|
-
return index;
|
|
992
|
+
function findShortFlag(letter) {
|
|
993
|
+
return FLAGS.find((flag) => flag.short === letter);
|
|
1124
994
|
}
|
|
1125
995
|
//#endregion
|
|
1126
|
-
//#region src/
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
996
|
+
//#region src/config/Config.ts
|
|
997
|
+
const DEFAULT_KILL_TIMEOUT = 5e3;
|
|
998
|
+
const LOG_LEVELS = /* @__PURE__ */ new Set([
|
|
999
|
+
"error",
|
|
1000
|
+
"warn",
|
|
1001
|
+
"info",
|
|
1002
|
+
"debug"
|
|
1003
|
+
]);
|
|
1004
|
+
const LABEL_MODES = /* @__PURE__ */ new Set([
|
|
1005
|
+
"none",
|
|
1006
|
+
"auto",
|
|
1007
|
+
"custom",
|
|
1008
|
+
"all"
|
|
1009
|
+
]);
|
|
1010
|
+
const COLOR_MODES = /* @__PURE__ */ new Set([
|
|
1011
|
+
"auto",
|
|
1012
|
+
"none",
|
|
1013
|
+
"basic",
|
|
1014
|
+
"soft",
|
|
1015
|
+
"all"
|
|
1016
|
+
]);
|
|
1017
|
+
/** Every key a config may set; a record, so a new one cannot be missed. */
|
|
1018
|
+
const KNOWN_KEYS = {
|
|
1019
|
+
color: true,
|
|
1020
|
+
commands: true,
|
|
1021
|
+
cwd: true,
|
|
1022
|
+
dryRun: true,
|
|
1023
|
+
env: true,
|
|
1024
|
+
formatLabel: true,
|
|
1025
|
+
jobs: true,
|
|
1026
|
+
killTimeout: true,
|
|
1027
|
+
labels: true,
|
|
1028
|
+
logLevel: true,
|
|
1029
|
+
onFailure: true,
|
|
1030
|
+
onSuccess: true,
|
|
1031
|
+
output: true,
|
|
1032
|
+
parallel: true,
|
|
1033
|
+
recursive: true,
|
|
1034
|
+
scripts: true,
|
|
1035
|
+
showCommand: true,
|
|
1036
|
+
showExitCode: true,
|
|
1037
|
+
stderr: true,
|
|
1038
|
+
stdout: true,
|
|
1039
|
+
wrap: true
|
|
1040
|
+
};
|
|
1041
|
+
/** Parses, loads and validates; anything left out comes from the process. */
|
|
1042
|
+
function createConfig({ cli = parseCli(process.argv.slice(2)), configJs, cwd = process.cwd(), destinations = process, env = process.env } = {}) {
|
|
1043
|
+
const config = new Config({
|
|
1044
|
+
cli,
|
|
1045
|
+
configJs,
|
|
1046
|
+
cwd,
|
|
1047
|
+
destinations,
|
|
1048
|
+
env
|
|
1049
|
+
});
|
|
1050
|
+
config.validate();
|
|
1051
|
+
return config;
|
|
1137
1052
|
}
|
|
1053
|
+
/** The resolved run-wide configuration: CLI flag → config file → default. */
|
|
1054
|
+
var Config = class {
|
|
1055
|
+
commands;
|
|
1056
|
+
scripts;
|
|
1057
|
+
/** Everything after `--`, for placeholders. */
|
|
1058
|
+
args;
|
|
1059
|
+
cwd;
|
|
1060
|
+
env;
|
|
1061
|
+
jobs;
|
|
1062
|
+
killTimeout;
|
|
1063
|
+
onSuccess;
|
|
1064
|
+
onFailure;
|
|
1065
|
+
color;
|
|
1066
|
+
parallel;
|
|
1067
|
+
recursive;
|
|
1068
|
+
showCommand;
|
|
1069
|
+
showExitCode;
|
|
1070
|
+
wrap;
|
|
1071
|
+
/** `COLUMNS` as runset was started with it: the width where no TTY says. */
|
|
1072
|
+
envColumns;
|
|
1073
|
+
labels;
|
|
1074
|
+
logLevel;
|
|
1075
|
+
dryRun;
|
|
1076
|
+
formatLabel;
|
|
1077
|
+
stdout;
|
|
1078
|
+
stderr;
|
|
1079
|
+
destinations;
|
|
1080
|
+
/** Printed by the run once it is built. */
|
|
1081
|
+
warnings = [];
|
|
1082
|
+
constructor({ cli, configJs, cwd, destinations, env }) {
|
|
1083
|
+
const { options } = cli;
|
|
1084
|
+
this.cwd = path.resolve(cwd, options.cwd ?? ".");
|
|
1085
|
+
this.destinations = destinations;
|
|
1086
|
+
const context = {
|
|
1087
|
+
argv: cli.argv,
|
|
1088
|
+
cwd: this.cwd,
|
|
1089
|
+
env
|
|
1090
|
+
};
|
|
1091
|
+
const file = configJs === void 0 ? loadConfigJs(context, options.config) : unwrapConfigJs(configJs, context);
|
|
1092
|
+
for (const key of Object.keys(file)) if (!Object.hasOwn(KNOWN_KEYS, key)) this.warnings.push(`Unknown config key "${key}" was ignored.`);
|
|
1093
|
+
if (file.cwd !== void 0 && options.cwd === void 0) this.cwd = path.resolve(cwd, file.cwd);
|
|
1094
|
+
check(envError("env", file.env ?? {}), ConfigError);
|
|
1095
|
+
this.env = {
|
|
1096
|
+
...env,
|
|
1097
|
+
...file.env,
|
|
1098
|
+
...options.env
|
|
1099
|
+
};
|
|
1100
|
+
this.commands = [...file.commands ?? [], ...cli.commands].filter(Boolean);
|
|
1101
|
+
this.scripts = file.scripts ?? {};
|
|
1102
|
+
this.args = cli.positional;
|
|
1103
|
+
this.jobs = options.jobs ?? file.jobs ?? Number.POSITIVE_INFINITY;
|
|
1104
|
+
this.killTimeout = options.killTimeout ?? file.killTimeout ?? DEFAULT_KILL_TIMEOUT;
|
|
1105
|
+
this.parallel = file.parallel ?? false;
|
|
1106
|
+
this.recursive = options.recursive ?? file.recursive ?? false;
|
|
1107
|
+
this.showCommand = options.showCommand ?? file.showCommand ?? false;
|
|
1108
|
+
this.showExitCode = options.showExitCode ?? file.showExitCode ?? false;
|
|
1109
|
+
this.wrap = options.wrap ?? file.wrap ?? true;
|
|
1110
|
+
this.envColumns = parseColumns(env.COLUMNS);
|
|
1111
|
+
this.labels = options.labels ?? file.labels ?? "auto";
|
|
1112
|
+
this.onSuccess = options.onSuccess ?? file.onSuccess ?? "continue";
|
|
1113
|
+
this.onFailure = options.onFailure ?? file.onFailure ?? "stop";
|
|
1114
|
+
this.dryRun = options.dryRun ?? file.dryRun ?? false;
|
|
1115
|
+
this.logLevel = options.logLevel ?? file.logLevel ?? "info";
|
|
1116
|
+
this.color = resolveColor(options.color ?? file.color, env, destinations);
|
|
1117
|
+
this.formatLabel = file.formatLabel;
|
|
1118
|
+
const streams = layerStreams({
|
|
1119
|
+
stderr: defaultStd("stderr"),
|
|
1120
|
+
stdout: defaultStd("stdout")
|
|
1121
|
+
}, [file, options]);
|
|
1122
|
+
this.stdout = streams.stdout;
|
|
1123
|
+
this.stderr = streams.stderr;
|
|
1124
|
+
}
|
|
1125
|
+
/** Throws on the first invalid value; types too, as config files are arbitrary JS. */
|
|
1126
|
+
validate() {
|
|
1127
|
+
check([
|
|
1128
|
+
oneOfError("logLevel", this.logLevel, LOG_LEVELS),
|
|
1129
|
+
numberError("jobs", this.jobs, 1),
|
|
1130
|
+
numberError("killTimeout", this.killTimeout, 0),
|
|
1131
|
+
oneOfError("color", this.color, COLOR_MODES),
|
|
1132
|
+
booleanError("parallel", this.parallel),
|
|
1133
|
+
booleanError("recursive", this.recursive),
|
|
1134
|
+
booleanError("showCommand", this.showCommand),
|
|
1135
|
+
booleanError("showExitCode", this.showExitCode),
|
|
1136
|
+
booleanError("wrap", this.wrap),
|
|
1137
|
+
booleanError("dryRun", this.dryRun),
|
|
1138
|
+
oneOfError("labels", this.labels, LABEL_MODES),
|
|
1139
|
+
stdError("stdout", this.stdout),
|
|
1140
|
+
stdError("stderr", this.stderr),
|
|
1141
|
+
this.formatLabel !== void 0 && typeof this.formatLabel !== "function" ? "formatLabel must be a function." : void 0,
|
|
1142
|
+
isPlainObject(this.scripts) ? void 0 : "scripts must be an object.",
|
|
1143
|
+
actionError("onSuccess", this.onSuccess),
|
|
1144
|
+
actionError("onFailure", this.onFailure)
|
|
1145
|
+
].find((error) => error !== void 0), ConfigError);
|
|
1146
|
+
}
|
|
1147
|
+
/** True when `commands` holds something to run, not just settings entries. */
|
|
1148
|
+
hasCommands() {
|
|
1149
|
+
return this.commands.some((command) => Boolean(command) && !isSettingsEntry(command));
|
|
1150
|
+
}
|
|
1151
|
+
};
|
|
1138
1152
|
/**
|
|
1139
|
-
*
|
|
1140
|
-
* `
|
|
1153
|
+
* A mode as given; under `auto`, whether to color at all, then `soft` where
|
|
1154
|
+
* the terminal shows 256 colors. `all` is only ever asked for. Anything else is
|
|
1155
|
+
* left for `validate` to refuse.
|
|
1141
1156
|
*/
|
|
1142
|
-
function
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
if (segment === "*" && segments[index - 1] === "**") continue;
|
|
1147
|
-
if (segment === "**") source.push(String.raw`[^:]+(?::[^:]+)*`);
|
|
1148
|
-
else source.push(segment.split("*").map((part) => escape(part)).join("[^:]*"));
|
|
1149
|
-
}
|
|
1150
|
-
return new RegExp(`^${source.join(":")}$`);
|
|
1151
|
-
}
|
|
1152
|
-
//#endregion
|
|
1153
|
-
//#region src/utils/workspace.ts
|
|
1154
|
-
/**
|
|
1155
|
-
* The packages of the workspace at or above `cwd`, from `pnpm-workspace.yaml`
|
|
1156
|
-
* or `package.json` `workspaces`, sorted by path; the root is not one of them.
|
|
1157
|
-
*/
|
|
1158
|
-
function readWorkspacePackages(cwd) {
|
|
1159
|
-
for (const dir of ancestors(cwd)) {
|
|
1160
|
-
const patterns = readWorkspacePatterns(dir);
|
|
1161
|
-
if (patterns === void 0) continue;
|
|
1162
|
-
const include = patterns.filter((pattern) => !pattern.startsWith("!"));
|
|
1163
|
-
const exclude = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1));
|
|
1164
|
-
return fs.globSync(include, {
|
|
1165
|
-
cwd: dir,
|
|
1166
|
-
exclude: [...exclude, "**/node_modules"]
|
|
1167
|
-
}).map((match) => path.join(dir, match, "package.json")).filter((filePath) => fs.existsSync(filePath)).toSorted().map((filePath) => readPackageJson(path.dirname(filePath)));
|
|
1168
|
-
}
|
|
1169
|
-
return [];
|
|
1170
|
-
}
|
|
1171
|
-
function readWorkspacePatterns(dir) {
|
|
1172
|
-
const yamlPath = path.join(dir, "pnpm-workspace.yaml");
|
|
1173
|
-
if (fs.existsSync(yamlPath)) return parsePnpmPackages(fs.readFileSync(yamlPath, "utf8"));
|
|
1174
|
-
const jsonPath = path.join(dir, "package.json");
|
|
1175
|
-
if (!fs.existsSync(jsonPath)) return;
|
|
1176
|
-
const { workspaces } = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
|
1177
|
-
return Array.isArray(workspaces) ? workspaces : workspaces?.packages;
|
|
1178
|
-
}
|
|
1179
|
-
/** The `packages:` list of a `pnpm-workspace.yaml`, read without a YAML parser. */
|
|
1180
|
-
function parsePnpmPackages(text) {
|
|
1181
|
-
const patterns = [];
|
|
1182
|
-
let inPackages = false;
|
|
1183
|
-
for (const raw of text.split(/\r?\n/)) {
|
|
1184
|
-
const line = raw.replace(/\s#.*$/, "").trimEnd();
|
|
1185
|
-
if (/^\S/.test(line)) inPackages = /^packages\s*:/.test(line);
|
|
1186
|
-
else if (inPackages && /^\s+-/.test(line)) patterns.push(line.replace(/^\s+-\s*/, "").replaceAll(/^['"]|['"]$/g, ""));
|
|
1187
|
-
}
|
|
1188
|
-
return patterns;
|
|
1189
|
-
}
|
|
1190
|
-
//#endregion
|
|
1191
|
-
//#region src/utils/os.ts
|
|
1192
|
-
/** 128 + the signal's number, the way a shell reports it: 130, 143, … */
|
|
1193
|
-
function signalExitCode(signal) {
|
|
1194
|
-
return 128 + (os.constants.signals[signal] ?? 15);
|
|
1195
|
-
}
|
|
1196
|
-
/**
|
|
1197
|
-
* Quotes a value so the platform's shell passes it on as literal text —
|
|
1198
|
-
* double quotes are not enough, since `$(…)` still runs inside them.
|
|
1199
|
-
*/
|
|
1200
|
-
function quoteForShell(value, platform = process.platform) {
|
|
1201
|
-
return platform === "win32" ? quoteForCmd(value) : quoteForPosix(value);
|
|
1202
|
-
}
|
|
1203
|
-
/** Single quotes are fully literal; each `'` inside closes, escapes, reopens. */
|
|
1204
|
-
function quoteForPosix(value) {
|
|
1205
|
-
return `'${value.replaceAll("'", String.raw`'\''`)}'`;
|
|
1206
|
-
}
|
|
1207
|
-
/**
|
|
1208
|
-
* Quoted for the C runtime first, then every `cmd.exe` metacharacter
|
|
1209
|
-
* `^`-escaped (see <https://qntm.org/cmd>). Newlines cannot survive `cmd.exe`,
|
|
1210
|
-
* so they become spaces.
|
|
1211
|
-
*/
|
|
1212
|
-
function quoteForCmd(value) {
|
|
1213
|
-
return `"${value.replaceAll("\r\n", " ").replaceAll(/[\n\r]/g, " ").replaceAll(/(\\*)"/g, String.raw`$1$1\"`).replace(/(\\*)$/, "$1$1")}"`.replaceAll(/["%&()<>^|]/g, (character) => `^${character}`);
|
|
1214
|
-
}
|
|
1215
|
-
//#endregion
|
|
1216
|
-
//#region src/plan/placeholders.ts
|
|
1217
|
-
/**
|
|
1218
|
-
* Fills `{1}`, `{@}`, `{*}` and `{name}` from the arguments after `--`.
|
|
1219
|
-
*
|
|
1220
|
-
* In a command line values are shell-quoted and unknown names left as written
|
|
1221
|
-
* (`${HOME}`); in an option (`forOption`) values are raw and unknown names
|
|
1222
|
-
* are empty, so `disabled={noTest}` is false when nothing was passed.
|
|
1223
|
-
*/
|
|
1224
|
-
function substitutePlaceholders(text, args, forOption = false) {
|
|
1225
|
-
const PLACEHOLDER = /\{([^\s{}]*)\}/g;
|
|
1226
|
-
const wrap = forOption ? (value) => value : quoteForShell;
|
|
1227
|
-
return text.replaceAll(PLACEHOLDER, (match, key) => {
|
|
1228
|
-
if (key === "@") return args.map((arg) => wrap(arg)).join(" ");
|
|
1229
|
-
if (key === "*") return args.length === 0 ? "" : wrap(args.join(" "));
|
|
1230
|
-
if (/^[1-9]\d*$/.test(key)) {
|
|
1231
|
-
const value = args[Number(key) - 1];
|
|
1232
|
-
return value === void 0 ? "" : wrap(value);
|
|
1233
|
-
}
|
|
1234
|
-
const named = namedValue(key, args);
|
|
1235
|
-
if (named !== void 0) return wrap(named);
|
|
1236
|
-
return forOption ? "" : match;
|
|
1237
|
-
});
|
|
1238
|
-
}
|
|
1239
|
-
/**
|
|
1240
|
-
* The value of `--name=value` or `--name value` (camel or kebab); a bare
|
|
1241
|
-
* `--name` is `'true'`.
|
|
1242
|
-
*/
|
|
1243
|
-
function namedValue(key, args) {
|
|
1244
|
-
for (const [index, token] of args.entries()) {
|
|
1245
|
-
if (!token.startsWith("--") || token.length === 2) continue;
|
|
1246
|
-
const equals = token.indexOf("=");
|
|
1247
|
-
if (toCamelCase(equals === -1 ? token.slice(2) : token.slice(2, equals)) !== key) continue;
|
|
1248
|
-
if (equals !== -1) return token.slice(equals + 1);
|
|
1249
|
-
const next = args[index + 1];
|
|
1250
|
-
return next !== void 0 && !next.startsWith("-") ? next : "true";
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
//#endregion
|
|
1254
|
-
//#region src/plan/token.ts
|
|
1255
|
-
const BOOLEAN_OPTIONS = /* @__PURE__ */ new Set([
|
|
1256
|
-
"disabled",
|
|
1257
|
-
"parallel",
|
|
1258
|
-
"recursive"
|
|
1259
|
-
]);
|
|
1260
|
-
const ACTION_OPTIONS = /* @__PURE__ */ new Set(["onFailure", "onSuccess"]);
|
|
1261
|
-
/** Keys a `::` suffix may set. */
|
|
1262
|
-
const INLINE_OPTIONS = /* @__PURE__ */ new Set([
|
|
1263
|
-
"bgColor",
|
|
1264
|
-
"color",
|
|
1265
|
-
"cwd",
|
|
1266
|
-
"label",
|
|
1267
|
-
"output",
|
|
1268
|
-
"stderr",
|
|
1269
|
-
"stdout",
|
|
1270
|
-
...BOOLEAN_OPTIONS,
|
|
1271
|
-
...ACTION_OPTIONS
|
|
1272
|
-
]);
|
|
1273
|
-
/**
|
|
1274
|
-
* Splits `"name args::options"` at the last `::`. The options run to the end
|
|
1275
|
-
* of the string, so a trailing space stays part of the last value.
|
|
1276
|
-
*/
|
|
1277
|
-
function splitToken(raw) {
|
|
1278
|
-
const trimmed = raw.trimStart();
|
|
1279
|
-
const marker = trimmed.lastIndexOf("::");
|
|
1280
|
-
const command = (marker === -1 ? trimmed : trimmed.slice(0, marker)).trim();
|
|
1281
|
-
const space = command.search(/\s/);
|
|
1282
|
-
return {
|
|
1283
|
-
args: space === -1 ? "" : command.slice(space + 1).trim(),
|
|
1284
|
-
name: space === -1 ? command : command.slice(0, space),
|
|
1285
|
-
options: marker === -1 ? "" : trimmed.slice(marker + 2)
|
|
1286
|
-
};
|
|
1157
|
+
function resolveColor(mode, env, destinations) {
|
|
1158
|
+
if (mode !== void 0 && mode !== "auto") return mode;
|
|
1159
|
+
if (!detectColor(env, destinations)) return "none";
|
|
1160
|
+
return WriteStream.prototype.getColorDepth(env) >= 8 ? "soft" : "basic";
|
|
1287
1161
|
}
|
|
1288
|
-
/**
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
const options = {};
|
|
1294
|
-
for (const entry of text.split(",")) {
|
|
1295
|
-
if (entry.trim() === "") continue;
|
|
1296
|
-
const eq = entry.indexOf("=");
|
|
1297
|
-
const key = toCamelCase((eq === -1 ? entry : entry.slice(0, eq)).trim());
|
|
1298
|
-
const written = eq === -1 ? "true" : entry.slice(eq + 1);
|
|
1299
|
-
const value = substitutePlaceholders(key === "label" ? written : written.trim(), args, true);
|
|
1300
|
-
if (!INLINE_OPTIONS.has(key)) throw new NormalizeError(`Unknown command option "${toKebabCase(key)}" in "::${text.trim()}".`);
|
|
1301
|
-
if (key === "output" || key === "stderr" || key === "stdout") options[key] = parsePartialStd(value);
|
|
1302
|
-
else if (BOOLEAN_OPTIONS.has(key)) options[key] = value !== "false" && value !== "";
|
|
1303
|
-
else if (ACTION_OPTIONS.has(key)) {
|
|
1304
|
-
check(actionError(`"${toKebabCase(key)}"`, value), NormalizeError);
|
|
1305
|
-
options[key] = value;
|
|
1306
|
-
} else options[key] = value;
|
|
1307
|
-
}
|
|
1308
|
-
return options;
|
|
1162
|
+
/** `NO_COLOR`/`FORCE_COLOR`, then whether both streams are TTYs. */
|
|
1163
|
+
function detectColor(env, destinations) {
|
|
1164
|
+
if (env.NO_COLOR !== void 0 && env.NO_COLOR !== "") return false;
|
|
1165
|
+
if (env.FORCE_COLOR !== void 0 && env.FORCE_COLOR !== "" && env.FORCE_COLOR !== "0") return true;
|
|
1166
|
+
return isTerminal(destinations.stdout) && isTerminal(destinations.stderr);
|
|
1309
1167
|
}
|
|
1310
1168
|
//#endregion
|
|
1311
|
-
//#region src/
|
|
1312
|
-
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1169
|
+
//#region src/utils/colors.ts
|
|
1170
|
+
const KNOWN = new Set(Object.keys(inspect.colors));
|
|
1171
|
+
const SHADES = {
|
|
1172
|
+
mint: {
|
|
1173
|
+
index: 115,
|
|
1174
|
+
fallback: "greenBright"
|
|
1175
|
+
},
|
|
1176
|
+
sky: {
|
|
1177
|
+
index: 111,
|
|
1178
|
+
fallback: "blueBright"
|
|
1179
|
+
},
|
|
1180
|
+
rose: {
|
|
1181
|
+
index: 211,
|
|
1182
|
+
fallback: "magentaBright"
|
|
1183
|
+
},
|
|
1184
|
+
amber: {
|
|
1185
|
+
index: 179,
|
|
1186
|
+
fallback: "yellowBright"
|
|
1187
|
+
},
|
|
1188
|
+
lavender: {
|
|
1189
|
+
index: 183,
|
|
1190
|
+
fallback: "magentaBright"
|
|
1191
|
+
},
|
|
1192
|
+
aqua: {
|
|
1193
|
+
index: 80,
|
|
1194
|
+
fallback: "cyanBright"
|
|
1195
|
+
},
|
|
1196
|
+
coral: {
|
|
1197
|
+
index: 210,
|
|
1198
|
+
fallback: "redBright"
|
|
1199
|
+
},
|
|
1200
|
+
sage: {
|
|
1201
|
+
index: 151,
|
|
1202
|
+
fallback: "greenBright"
|
|
1203
|
+
},
|
|
1204
|
+
periwinkle: {
|
|
1205
|
+
index: 147,
|
|
1206
|
+
fallback: "blueBright"
|
|
1207
|
+
},
|
|
1208
|
+
peach: {
|
|
1209
|
+
index: 216,
|
|
1210
|
+
fallback: "yellowBright"
|
|
1211
|
+
},
|
|
1212
|
+
teal: {
|
|
1213
|
+
index: 79,
|
|
1214
|
+
fallback: "cyanBright"
|
|
1215
|
+
},
|
|
1216
|
+
lilac: {
|
|
1217
|
+
index: 177,
|
|
1218
|
+
fallback: "magentaBright"
|
|
1219
|
+
},
|
|
1220
|
+
lime: {
|
|
1221
|
+
index: 149,
|
|
1222
|
+
fallback: "greenBright"
|
|
1223
|
+
},
|
|
1224
|
+
steel: {
|
|
1225
|
+
index: 110,
|
|
1226
|
+
fallback: "blueBright"
|
|
1227
|
+
},
|
|
1228
|
+
pink: {
|
|
1229
|
+
index: 218,
|
|
1230
|
+
fallback: "magentaBright"
|
|
1231
|
+
},
|
|
1232
|
+
seafoam: {
|
|
1233
|
+
index: 121,
|
|
1234
|
+
fallback: "cyanBright"
|
|
1235
|
+
},
|
|
1236
|
+
apricot: {
|
|
1237
|
+
index: 215,
|
|
1238
|
+
fallback: "yellowBright"
|
|
1239
|
+
},
|
|
1240
|
+
mauve: {
|
|
1241
|
+
index: 139,
|
|
1242
|
+
fallback: "magentaBright"
|
|
1243
|
+
},
|
|
1244
|
+
jade: {
|
|
1245
|
+
index: 114,
|
|
1246
|
+
fallback: "greenBright"
|
|
1247
|
+
},
|
|
1248
|
+
sand: {
|
|
1249
|
+
index: 180,
|
|
1250
|
+
fallback: "yellowBright"
|
|
1251
|
+
},
|
|
1252
|
+
iris: {
|
|
1253
|
+
index: 141,
|
|
1254
|
+
fallback: "blueBright"
|
|
1255
|
+
},
|
|
1256
|
+
ice: {
|
|
1257
|
+
index: 117,
|
|
1258
|
+
fallback: "cyanBright"
|
|
1259
|
+
},
|
|
1260
|
+
salmon: {
|
|
1261
|
+
index: 174,
|
|
1262
|
+
fallback: "redBright"
|
|
1263
|
+
},
|
|
1264
|
+
olive: {
|
|
1265
|
+
index: 150,
|
|
1266
|
+
fallback: "greenBright"
|
|
1267
|
+
},
|
|
1268
|
+
navy: {
|
|
1269
|
+
index: 24,
|
|
1270
|
+
fallback: "blue",
|
|
1271
|
+
foreground: "paper"
|
|
1272
|
+
},
|
|
1273
|
+
plum: {
|
|
1274
|
+
index: 96,
|
|
1275
|
+
fallback: "magenta",
|
|
1276
|
+
foreground: "paper"
|
|
1277
|
+
},
|
|
1278
|
+
forest: {
|
|
1279
|
+
index: 22,
|
|
1280
|
+
fallback: "green",
|
|
1281
|
+
foreground: "paper"
|
|
1282
|
+
},
|
|
1283
|
+
wine: {
|
|
1284
|
+
index: 89,
|
|
1285
|
+
fallback: "red",
|
|
1286
|
+
foreground: "paper"
|
|
1287
|
+
},
|
|
1288
|
+
ocean: {
|
|
1289
|
+
index: 25,
|
|
1290
|
+
fallback: "blue",
|
|
1291
|
+
foreground: "paper"
|
|
1292
|
+
},
|
|
1293
|
+
indigo: {
|
|
1294
|
+
index: 61,
|
|
1295
|
+
fallback: "blue",
|
|
1296
|
+
foreground: "paper"
|
|
1297
|
+
},
|
|
1298
|
+
copper: {
|
|
1299
|
+
index: 94,
|
|
1300
|
+
fallback: "red",
|
|
1301
|
+
foreground: "paper"
|
|
1302
|
+
},
|
|
1303
|
+
pine: {
|
|
1304
|
+
index: 23,
|
|
1305
|
+
fallback: "cyan",
|
|
1306
|
+
foreground: "paper"
|
|
1307
|
+
},
|
|
1308
|
+
violet: {
|
|
1309
|
+
index: 55,
|
|
1310
|
+
fallback: "magenta",
|
|
1311
|
+
foreground: "paper"
|
|
1312
|
+
},
|
|
1313
|
+
brick: {
|
|
1314
|
+
index: 88,
|
|
1315
|
+
fallback: "red",
|
|
1316
|
+
foreground: "paper"
|
|
1317
|
+
},
|
|
1318
|
+
slate: {
|
|
1319
|
+
index: 60,
|
|
1320
|
+
fallback: "blue",
|
|
1321
|
+
foreground: "paper"
|
|
1322
|
+
},
|
|
1323
|
+
cocoa: {
|
|
1324
|
+
index: 95,
|
|
1325
|
+
fallback: "red",
|
|
1326
|
+
foreground: "paper"
|
|
1327
|
+
},
|
|
1328
|
+
ink: {
|
|
1329
|
+
index: 235,
|
|
1330
|
+
fallback: "black"
|
|
1331
|
+
},
|
|
1332
|
+
paper: {
|
|
1333
|
+
index: 255,
|
|
1334
|
+
fallback: "whiteBright"
|
|
1448
1335
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1336
|
+
};
|
|
1337
|
+
/** Every shade by its foreground and its background name, e.g. `mint`, `bgMint`. */
|
|
1338
|
+
const SHADE_NAMES = new Map(Object.entries(SHADES).flatMap(([name, custom]) => [[name, custom], [toCamelCase(`bg-${name}`), custom]]));
|
|
1339
|
+
const BASIC_PALETTE = [
|
|
1340
|
+
{
|
|
1341
|
+
bgColor: "bgGreen",
|
|
1342
|
+
color: "black"
|
|
1343
|
+
},
|
|
1344
|
+
{
|
|
1345
|
+
bgColor: "bgBlue",
|
|
1346
|
+
color: "white"
|
|
1347
|
+
},
|
|
1348
|
+
{
|
|
1349
|
+
bgColor: "bgMagenta",
|
|
1350
|
+
color: "white"
|
|
1351
|
+
},
|
|
1352
|
+
{
|
|
1353
|
+
bgColor: "bgCyan",
|
|
1354
|
+
color: "black"
|
|
1355
|
+
},
|
|
1356
|
+
{
|
|
1357
|
+
bgColor: "bgYellow",
|
|
1358
|
+
color: "black"
|
|
1359
|
+
},
|
|
1360
|
+
{
|
|
1361
|
+
bgColor: "bgRed",
|
|
1362
|
+
color: "white"
|
|
1363
|
+
},
|
|
1364
|
+
{
|
|
1365
|
+
bgColor: "bgGray",
|
|
1366
|
+
color: "white"
|
|
1367
|
+
},
|
|
1368
|
+
{
|
|
1369
|
+
bgColor: "bgGreenBright",
|
|
1370
|
+
color: "black"
|
|
1371
|
+
},
|
|
1372
|
+
{
|
|
1373
|
+
bgColor: "bgBlueBright",
|
|
1374
|
+
color: "black"
|
|
1375
|
+
},
|
|
1376
|
+
{
|
|
1377
|
+
bgColor: "bgMagentaBright",
|
|
1378
|
+
color: "black"
|
|
1379
|
+
},
|
|
1380
|
+
{
|
|
1381
|
+
bgColor: "bgCyanBright",
|
|
1382
|
+
color: "black"
|
|
1383
|
+
},
|
|
1384
|
+
{
|
|
1385
|
+
bgColor: "bgYellowBright",
|
|
1386
|
+
color: "black"
|
|
1451
1387
|
}
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1388
|
+
];
|
|
1389
|
+
/** Interleave hue families so adjacent assignments remain easy to tell apart. */
|
|
1390
|
+
const PALETTE = Object.entries(SHADES).filter(([name]) => name !== "ink" && name !== "paper").map(([name, { foreground }]) => ({
|
|
1391
|
+
bgColor: toCamelCase(`bg-${name}`),
|
|
1392
|
+
color: foreground ?? "ink"
|
|
1393
|
+
}));
|
|
1394
|
+
/** What `colorLabels` hands out under each mode; `none` still picks, unseen. */
|
|
1395
|
+
const PALETTES = {
|
|
1396
|
+
none: BASIC_PALETTE,
|
|
1397
|
+
basic: BASIC_PALETTE,
|
|
1398
|
+
soft: PALETTE.filter((pair) => pair.color === "ink"),
|
|
1399
|
+
all: PALETTE
|
|
1400
|
+
};
|
|
1401
|
+
/** A built-in style or one of runset's foreground/background shades. */
|
|
1402
|
+
function isColorName(name) {
|
|
1403
|
+
return KNOWN.has(name) || shade(name) !== void 0;
|
|
1455
1404
|
}
|
|
1456
|
-
/**
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1405
|
+
/**
|
|
1406
|
+
* Styles text; a shade is painted from the 256-color table. Plain text when
|
|
1407
|
+
* not `enabled`.
|
|
1408
|
+
*/
|
|
1409
|
+
function paint(text, colors, enabled = true) {
|
|
1410
|
+
if (!enabled) return text;
|
|
1411
|
+
let result = text;
|
|
1412
|
+
for (const name of colors.toReversed()) {
|
|
1413
|
+
const custom = shade(name);
|
|
1414
|
+
if (custom !== void 0) {
|
|
1415
|
+
const background = name.startsWith("bg");
|
|
1416
|
+
const open = `\u001B[${background ? 48 : 38};5;${custom.index}m`;
|
|
1417
|
+
const close = `\u001B[${background ? 49 : 39}m`;
|
|
1418
|
+
result = open + result.replaceAll(close, open) + close;
|
|
1419
|
+
} else if (KNOWN.has(name)) result = styleText(name, result, { validateStream: false });
|
|
1420
|
+
}
|
|
1421
|
+
return result;
|
|
1461
1422
|
}
|
|
1462
|
-
|
|
1463
|
-
|
|
1423
|
+
/** The theme color used for a shade on a basic terminal. */
|
|
1424
|
+
function fallbackColor(name) {
|
|
1425
|
+
const custom = shade(name);
|
|
1426
|
+
if (custom === void 0) return name;
|
|
1427
|
+
return name.startsWith("bg") ? toCamelCase(`bg-${custom.fallback}`) : custom.fallback;
|
|
1464
1428
|
}
|
|
1465
|
-
|
|
1466
|
-
|
|
1429
|
+
/** A stable palette index for a label. */
|
|
1430
|
+
function paletteIndex(label, palette = PALETTE) {
|
|
1431
|
+
/** The largest modulus that keeps `hash * 31 + code` an exact integer. */
|
|
1432
|
+
const HASH_MODULUS = 2147483647;
|
|
1433
|
+
let hash = 0;
|
|
1434
|
+
for (const character of label) hash = (hash * 31 + (character.codePointAt(0) ?? 0)) % HASH_MODULUS;
|
|
1435
|
+
return hash % palette.length;
|
|
1467
1436
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
return options.cwd === void 0 ? options : {
|
|
1471
|
-
...options,
|
|
1472
|
-
cwd: path.resolve(base, options.cwd)
|
|
1473
|
-
};
|
|
1437
|
+
function shade(name) {
|
|
1438
|
+
return SHADE_NAMES.get(name);
|
|
1474
1439
|
}
|
|
1440
|
+
//#endregion
|
|
1441
|
+
//#region src/plan/labels.ts
|
|
1475
1442
|
/**
|
|
1476
|
-
*
|
|
1477
|
-
*
|
|
1443
|
+
* Settles who is labelled, per `config.labels`. Under `auto`, a command
|
|
1444
|
+
* sharing a stage is named after itself, whether or not someone in its stage
|
|
1445
|
+
* has a label of their own. Under `custom` it gets a blank label instead,
|
|
1446
|
+
* holding the column when someone in its stage has one.
|
|
1478
1447
|
*/
|
|
1479
|
-
function
|
|
1480
|
-
const
|
|
1481
|
-
if (
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
if (serial !== void 0) {
|
|
1485
|
-
check(booleanError("\"serial\"", serial), NormalizeError);
|
|
1486
|
-
if (options.parallel !== void 0) throw new NormalizeError("A settings entry takes \"parallel\" or \"serial\", not both.");
|
|
1487
|
-
options.parallel = !serial;
|
|
1448
|
+
function assignAutoLabels({ commands, config }) {
|
|
1449
|
+
const mode = config.labels;
|
|
1450
|
+
if (mode === "none") {
|
|
1451
|
+
for (const command of commands) command.label = "";
|
|
1452
|
+
return;
|
|
1488
1453
|
}
|
|
1489
|
-
|
|
1454
|
+
const named = [];
|
|
1455
|
+
for (const stage of groupByStage(commands, (command) => command.stage)) {
|
|
1456
|
+
const shared = stage.length > 1;
|
|
1457
|
+
const labeled = stage.some((command) => command.label !== "");
|
|
1458
|
+
for (const command of stage) {
|
|
1459
|
+
if (command.label !== "") continue;
|
|
1460
|
+
if (mode === "all" || mode === "auto" && shared) {
|
|
1461
|
+
command.label = nameOf(command);
|
|
1462
|
+
named.push(command);
|
|
1463
|
+
} else if (shared && labeled) command.label = " ";
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
numberRepeats(named);
|
|
1490
1467
|
}
|
|
1491
|
-
/**
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1468
|
+
/**
|
|
1469
|
+
* Gives each labelled command without colors of its own a palette pair: the
|
|
1470
|
+
* one its label hashes to, or the next free when that is taken, from the
|
|
1471
|
+
* palette of `config.color`. Without shades every one is first swapped for its
|
|
1472
|
+
* theme color, so nothing past the plan has to know how many colors there are.
|
|
1473
|
+
*/
|
|
1474
|
+
function colorLabels({ commands, config }) {
|
|
1475
|
+
const palette = PALETTES[config.color];
|
|
1476
|
+
if (config.color === "none" || config.color === "basic") for (const command of commands) {
|
|
1477
|
+
command.bgColor = fallbackColor(command.bgColor);
|
|
1478
|
+
command.color = fallbackColor(command.color);
|
|
1479
|
+
}
|
|
1480
|
+
const labeled = commands.filter((command) => command.label !== "");
|
|
1481
|
+
const taken = new Set(labeled.map((command) => palette.findIndex((pair) => pair.bgColor === command.bgColor)).filter((index) => index !== -1));
|
|
1482
|
+
for (const command of labeled) {
|
|
1483
|
+
if (command.color !== "" || command.bgColor !== "" || command.label.trim() === "") continue;
|
|
1484
|
+
const index = claim(paletteIndex(command.label, palette), taken, palette.length);
|
|
1485
|
+
taken.add(index);
|
|
1486
|
+
const pair = palette[index];
|
|
1487
|
+
command.bgColor = pair.bgColor;
|
|
1488
|
+
command.color = pair.color;
|
|
1497
1489
|
}
|
|
1498
|
-
return result;
|
|
1499
1490
|
}
|
|
1500
|
-
/**
|
|
1501
|
-
function
|
|
1502
|
-
const
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1491
|
+
/** Pads every label to the widest one. */
|
|
1492
|
+
function alignLabels({ commands }) {
|
|
1493
|
+
const labeled = commands.filter((command) => command.label !== "");
|
|
1494
|
+
const width = Math.max(0, ...labeled.map((command) => command.label.length));
|
|
1495
|
+
for (const command of labeled) command.label = command.label.padEnd(width);
|
|
1496
|
+
}
|
|
1497
|
+
/** The script name, or the program a shell command runs. */
|
|
1498
|
+
function nameOf(command) {
|
|
1499
|
+
return command.packageName ?? command.scriptName ?? command.command.split(/\s/, 1)[0] ?? "";
|
|
1500
|
+
}
|
|
1501
|
+
/** `serve`, `serve` → `serve#1`, `serve#2`. */
|
|
1502
|
+
function numberRepeats(named) {
|
|
1503
|
+
const totals = /* @__PURE__ */ new Map();
|
|
1504
|
+
for (const command of named) totals.set(command.label, (totals.get(command.label) ?? 0) + 1);
|
|
1505
|
+
const counters = /* @__PURE__ */ new Map();
|
|
1506
|
+
for (const command of named) {
|
|
1507
|
+
if ((totals.get(command.label) ?? 0) < 2) continue;
|
|
1508
|
+
const index = (counters.get(command.label) ?? 0) + 1;
|
|
1509
|
+
counters.set(command.label, index);
|
|
1510
|
+
command.label = `${command.label}#${index}`;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
function claim(preferred, taken, size) {
|
|
1514
|
+
if (taken.size >= size) return preferred;
|
|
1515
|
+
let index = preferred;
|
|
1516
|
+
while (taken.has(index)) index = (index + 1) % size;
|
|
1517
|
+
return index;
|
|
1519
1518
|
}
|
|
1520
1519
|
//#endregion
|
|
1521
1520
|
//#region src/plan/plan.ts
|
|
@@ -1602,13 +1601,11 @@ var FileRegistry = class {
|
|
|
1602
1601
|
error;
|
|
1603
1602
|
streams = /* @__PURE__ */ new Map();
|
|
1604
1603
|
cwd;
|
|
1605
|
-
|
|
1606
|
-
|
|
1604
|
+
onFailure;
|
|
1605
|
+
/** `onFailure` is called once, with the first failure. */
|
|
1606
|
+
constructor(cwd, onFailure) {
|
|
1607
1607
|
this.cwd = cwd;
|
|
1608
|
-
|
|
1609
|
-
/** Called once, with the first failure. */
|
|
1610
|
-
onFailure(handler) {
|
|
1611
|
-
this.handler = handler;
|
|
1608
|
+
this.onFailure = onFailure;
|
|
1612
1609
|
}
|
|
1613
1610
|
open(destination) {
|
|
1614
1611
|
const filePath = path.resolve(this.cwd, destination);
|
|
@@ -1626,12 +1623,12 @@ var FileRegistry = class {
|
|
|
1626
1623
|
async closeAll() {
|
|
1627
1624
|
const streams = [...this.streams.values()];
|
|
1628
1625
|
this.streams.clear();
|
|
1629
|
-
await Promise.all(streams.map(
|
|
1626
|
+
await Promise.all(streams.map((stream) => closed(stream)));
|
|
1630
1627
|
}
|
|
1631
1628
|
fail(destination, error) {
|
|
1632
1629
|
if (this.error !== void 0) return;
|
|
1633
1630
|
this.error = /* @__PURE__ */ new Error(`cannot write to "${destination}": ${error.message}`);
|
|
1634
|
-
this.
|
|
1631
|
+
this.onFailure(this.error);
|
|
1635
1632
|
}
|
|
1636
1633
|
};
|
|
1637
1634
|
function closed(stream) {
|
|
@@ -1679,8 +1676,7 @@ var Logger = class {
|
|
|
1679
1676
|
}
|
|
1680
1677
|
write(level, message) {
|
|
1681
1678
|
if (RANK[level] > RANK[this.level]) return;
|
|
1682
|
-
|
|
1683
|
-
this.stream.write(`${text}\n`);
|
|
1679
|
+
this.stream.write(`${paint(message, COLORS[level], this.color)}\n`);
|
|
1684
1680
|
}
|
|
1685
1681
|
};
|
|
1686
1682
|
//#endregion
|
|
@@ -1721,17 +1717,28 @@ function createEnv({ base, cwd, packageInfo, scriptName }) {
|
|
|
1721
1717
|
const PATH_KEY = process.platform === "win32" ? "Path" : "PATH";
|
|
1722
1718
|
const pathKey = Object.keys(base).find((key) => key.toUpperCase() === "PATH") ?? PATH_KEY;
|
|
1723
1719
|
const binDirs = ancestors(cwd).map((dir) => path.join(dir, "node_modules", ".bin"));
|
|
1724
|
-
const
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
};
|
|
1728
|
-
if (packageInfo.name !== "") env.npm_package_name = packageInfo.name;
|
|
1729
|
-
if (packageInfo.version !== "") env.npm_package_version = packageInfo.version;
|
|
1720
|
+
const own = {};
|
|
1721
|
+
if (packageInfo.name !== "") own.npm_package_name = packageInfo.name;
|
|
1722
|
+
if (packageInfo.version !== "") own.npm_package_version = packageInfo.version;
|
|
1730
1723
|
if (scriptName !== void 0) {
|
|
1731
|
-
|
|
1732
|
-
|
|
1724
|
+
own.npm_lifecycle_event = scriptName;
|
|
1725
|
+
own.npm_lifecycle_script = packageInfo.scripts[scriptName] ?? "";
|
|
1733
1726
|
}
|
|
1734
|
-
return
|
|
1727
|
+
return {
|
|
1728
|
+
...withoutSpellingsOf(base, Object.keys(own)),
|
|
1729
|
+
...own,
|
|
1730
|
+
[pathKey]: [...binDirs, base[pathKey]].filter(Boolean).join(path.delimiter)
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* Windows names are case-insensitive, and of two spellings a child is given
|
|
1735
|
+
* the one that sorts first — an outer npm's `NPM_…` over ours — so on Windows
|
|
1736
|
+
* every other spelling of a name about to be set is dropped.
|
|
1737
|
+
*/
|
|
1738
|
+
function withoutSpellingsOf(env, names) {
|
|
1739
|
+
if (process.platform !== "win32") return env;
|
|
1740
|
+
const replaced = new Set(names.map((name) => name.toUpperCase()));
|
|
1741
|
+
return Object.fromEntries(Object.entries(env).filter(([key]) => !replaced.has(key.toUpperCase())));
|
|
1735
1742
|
}
|
|
1736
1743
|
//#endregion
|
|
1737
1744
|
//#region src/run/OutputSink.ts
|
|
@@ -1740,31 +1747,26 @@ function createEnv({ base, cwd, packageInfo, scriptName }) {
|
|
|
1740
1747
|
* `grouped` holds everything until {@link flush}, `none` discards.
|
|
1741
1748
|
*/
|
|
1742
1749
|
var OutputSink = class {
|
|
1750
|
+
/** A grouped stream's output, or a prefixed stream's unfinished line. */
|
|
1743
1751
|
buffered = "";
|
|
1744
|
-
/** A prefixed stream's unfinished line, held until its newline. */
|
|
1745
|
-
pending = "";
|
|
1746
1752
|
opened;
|
|
1747
1753
|
/** Whether everything written so far ended with a newline. */
|
|
1748
1754
|
atLineStart = true;
|
|
1749
1755
|
std;
|
|
1750
1756
|
prefix;
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
envColumns;
|
|
1756
|
-
constructor(std, prefix, destinations, files, wrap = false, envColumns) {
|
|
1757
|
+
/** The columns a fixed prefix takes; measured per line otherwise. */
|
|
1758
|
+
prefixWidth;
|
|
1759
|
+
context;
|
|
1760
|
+
constructor(std, prefix, context) {
|
|
1757
1761
|
this.std = std;
|
|
1758
1762
|
this.prefix = prefix;
|
|
1759
|
-
this.
|
|
1760
|
-
this.
|
|
1761
|
-
this.wrap = wrap;
|
|
1762
|
-
this.envColumns = envColumns;
|
|
1763
|
+
this.prefixWidth = typeof prefix === "string" ? visibleWidth(prefix) : void 0;
|
|
1764
|
+
this.context = context;
|
|
1763
1765
|
}
|
|
1764
1766
|
/** Opened on first use, so a command that never writes truncates nothing. */
|
|
1765
1767
|
get target() {
|
|
1766
|
-
if (isStreamDestination(this.std.destination)) return this.destinations[this.std.destination];
|
|
1767
|
-
this.opened ??= this.files.open(this.std.destination);
|
|
1768
|
+
if (isStreamDestination(this.std.destination)) return this.context.config.destinations[this.std.destination];
|
|
1769
|
+
this.opened ??= this.context.files.open(this.std.destination);
|
|
1768
1770
|
return this.opened;
|
|
1769
1771
|
}
|
|
1770
1772
|
/**
|
|
@@ -1772,8 +1774,9 @@ var OutputSink = class {
|
|
|
1772
1774
|
* else `COLUMNS`. A file has none, and neither has a run without `wrap`.
|
|
1773
1775
|
*/
|
|
1774
1776
|
columns() {
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
+
const { envColumns, wrap } = this.context.config;
|
|
1778
|
+
if (!wrap || !isStreamDestination(this.std.destination)) return;
|
|
1779
|
+
return terminalColumns(this.target) ?? envColumns;
|
|
1777
1780
|
}
|
|
1778
1781
|
write(chunk) {
|
|
1779
1782
|
if (this.std.destination === "none" || chunk === "") return;
|
|
@@ -1786,11 +1789,13 @@ var OutputSink = class {
|
|
|
1786
1789
|
this.target.write(chunk);
|
|
1787
1790
|
return;
|
|
1788
1791
|
}
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1792
|
+
const end = chunk.lastIndexOf("\n");
|
|
1793
|
+
if (end === -1) {
|
|
1794
|
+
this.buffered += chunk;
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
const complete = this.buffered + chunk.slice(0, end + 1);
|
|
1798
|
+
this.buffered = chunk.slice(end + 1);
|
|
1794
1799
|
this.target.write(this.applyPrefix(complete));
|
|
1795
1800
|
}
|
|
1796
1801
|
/** A line of runset's own, on a line of its own: after any unfinished one. */
|
|
@@ -1799,27 +1804,32 @@ var OutputSink = class {
|
|
|
1799
1804
|
}
|
|
1800
1805
|
/** Emits what is held back: a `grouped` stream, or a partial line. */
|
|
1801
1806
|
flush() {
|
|
1802
|
-
const held =
|
|
1803
|
-
this.pending = "";
|
|
1807
|
+
const held = this.buffered;
|
|
1804
1808
|
this.buffered = "";
|
|
1805
|
-
|
|
1809
|
+
if (held !== "") this.target.write(this.applyPrefix(held));
|
|
1806
1810
|
}
|
|
1807
1811
|
applyPrefix(chunk) {
|
|
1808
1812
|
const { prefix } = this;
|
|
1809
1813
|
if (prefix === "") return chunk;
|
|
1810
1814
|
const endsWithNewline = chunk.endsWith("\n");
|
|
1811
1815
|
const body = endsWithNewline ? chunk.slice(0, -1) : chunk;
|
|
1816
|
+
const end = endsWithNewline ? "\n" : "";
|
|
1812
1817
|
const columns = this.columns();
|
|
1813
|
-
|
|
1818
|
+
if (typeof prefix === "string" && columns === void 0) return prefix + body.replaceAll("\n", `\n${prefix}`) + end;
|
|
1819
|
+
return body.split("\n").map((line) => this.label(line, prefix, columns)).join("\n") + end;
|
|
1814
1820
|
}
|
|
1815
1821
|
/** One line, prefixed; wrapped first when it would overflow the terminal. */
|
|
1816
1822
|
label(line, prefix, columns) {
|
|
1817
1823
|
const text = typeof prefix === "string" ? prefix : prefix();
|
|
1818
1824
|
if (columns === void 0) return text + line;
|
|
1819
|
-
const
|
|
1820
|
-
|
|
1825
|
+
const cr = line.endsWith("\r") ? "\r" : "";
|
|
1826
|
+
const width = this.prefixWidth ?? visibleWidth(text);
|
|
1827
|
+
return wrapLine(line.slice(0, line.length - cr.length), columns - width, width).map((piece) => text + piece).join("\n") + cr;
|
|
1821
1828
|
}
|
|
1822
1829
|
};
|
|
1830
|
+
function isStreamDestination(destination) {
|
|
1831
|
+
return destination === "stdout" || destination === "stderr";
|
|
1832
|
+
}
|
|
1823
1833
|
//#endregion
|
|
1824
1834
|
//#region src/run/Process.ts
|
|
1825
1835
|
const USE_PROCESS_GROUPS = process.platform !== "win32";
|
|
@@ -1842,9 +1852,9 @@ var Process = class {
|
|
|
1842
1852
|
constructor(command, context) {
|
|
1843
1853
|
this.command = command;
|
|
1844
1854
|
this.context = context;
|
|
1845
|
-
const {
|
|
1846
|
-
const
|
|
1847
|
-
const sink = (stream) => new OutputSink(command[stream], makePrefix(command,
|
|
1855
|
+
const { color, formatLabel } = context.config;
|
|
1856
|
+
const colored = color !== "none";
|
|
1857
|
+
const sink = (stream) => new OutputSink(command[stream], makePrefix(command, colored, formatLabel, stream), context);
|
|
1848
1858
|
this.stdout = sink("stdout");
|
|
1849
1859
|
this.stderr = sink("stderr");
|
|
1850
1860
|
}
|
|
@@ -1862,12 +1872,13 @@ var Process = class {
|
|
|
1862
1872
|
let stopTheRun = false;
|
|
1863
1873
|
for (;;) {
|
|
1864
1874
|
this.started = true;
|
|
1865
|
-
|
|
1866
|
-
|
|
1875
|
+
const { color, showCommand, showExitCode } = this.context.config;
|
|
1876
|
+
if (showCommand) {
|
|
1877
|
+
const run = paint("run", ["blue"], color !== "none");
|
|
1867
1878
|
this.stdout.writeLine(`${run} ${this.command.command}`);
|
|
1868
1879
|
}
|
|
1869
1880
|
await this.spawn();
|
|
1870
|
-
if (
|
|
1881
|
+
if (showExitCode) this.stdout.writeLine(this.exitLine());
|
|
1871
1882
|
this.stdout.flush();
|
|
1872
1883
|
this.stderr.flush();
|
|
1873
1884
|
this.context.logger.debug(`< ${this.command.line} ${this.describeExit()}`);
|
|
@@ -1895,12 +1906,12 @@ var Process = class {
|
|
|
1895
1906
|
* in gray, so it can be found without its label.
|
|
1896
1907
|
*/
|
|
1897
1908
|
exitLine() {
|
|
1898
|
-
const
|
|
1909
|
+
const colored = this.context.config.color !== "none";
|
|
1899
1910
|
const duration = formatDuration(performance.now() - this.attemptStart);
|
|
1900
|
-
if (this.exitCode === 0 && this.signal === void 0 && !this.terminated) return `${
|
|
1911
|
+
if (this.exitCode === 0 && this.signal === void 0 && !this.terminated) return `${paint("✓", ["green"], colored)} ${duration}`;
|
|
1901
1912
|
const [status, tint] = this.terminated ? ["– stopped", "yellow"] : [`✗ ${this.shortExit()}`, "red"];
|
|
1902
|
-
const
|
|
1903
|
-
return
|
|
1913
|
+
const command = paint(this.command.command, ["gray"], colored);
|
|
1914
|
+
return `${paint(status, [tint], colored)} · ${duration} ${command}`;
|
|
1904
1915
|
}
|
|
1905
1916
|
/** `code N`, the signal's name, or `stopped`. */
|
|
1906
1917
|
shortExit() {
|
|
@@ -2017,6 +2028,7 @@ async function schedule(processes, stopped, jobs) {
|
|
|
2017
2028
|
//#region src/run/Run.ts
|
|
2018
2029
|
/** One run: the processes built from a plan, and what stops them. */
|
|
2019
2030
|
var Run = class Run {
|
|
2031
|
+
/** @internal */
|
|
2020
2032
|
config;
|
|
2021
2033
|
/** @internal */
|
|
2022
2034
|
processes;
|
|
@@ -2025,9 +2037,8 @@ var Run = class Run {
|
|
|
2025
2037
|
stopping;
|
|
2026
2038
|
constructor({ commands, config, packageInfo }) {
|
|
2027
2039
|
this.config = config;
|
|
2028
|
-
this.files = new FileRegistry(config.cwd);
|
|
2040
|
+
this.files = new FileRegistry(config.cwd, () => this.stopEverything({ reason: "policy" }));
|
|
2029
2041
|
this.logger = new Logger(config.logLevel, config.destinations.stderr, config.color !== "none");
|
|
2030
|
-
for (const warning of config.warnings) this.logger.warn(`runset: ${warning}`);
|
|
2031
2042
|
const context = {
|
|
2032
2043
|
config,
|
|
2033
2044
|
files: this.files,
|
|
@@ -2036,7 +2047,6 @@ var Run = class Run {
|
|
|
2036
2047
|
requestStop: () => this.stopEverything({ reason: "policy" })
|
|
2037
2048
|
};
|
|
2038
2049
|
this.processes = commands.map((command) => new Process(command, context));
|
|
2039
|
-
this.files.onFailure(() => this.stopEverything({ reason: "policy" }));
|
|
2040
2050
|
}
|
|
2041
2051
|
/** A library caller's config object, through the same pipeline as the CLI. */
|
|
2042
2052
|
static fromConfigJs(configJs) {
|
|
@@ -2058,6 +2068,7 @@ var Run = class Run {
|
|
|
2058
2068
|
}
|
|
2059
2069
|
/** Rejects with a {@link RunsetError} when the run did not succeed. */
|
|
2060
2070
|
async start() {
|
|
2071
|
+
for (const warning of this.config.warnings) this.logger.warn(`runset: ${warning}`);
|
|
2061
2072
|
if (this.config.dryRun) {
|
|
2062
2073
|
this.config.destinations.stdout.write(`${this.describe()}\n`);
|
|
2063
2074
|
return this;
|
|
@@ -2075,43 +2086,45 @@ var Run = class Run {
|
|
|
2075
2086
|
await this.files.closeAll();
|
|
2076
2087
|
}
|
|
2077
2088
|
const failed = this.failed();
|
|
2078
|
-
const exitCode =
|
|
2089
|
+
const exitCode = this.getExitCode();
|
|
2079
2090
|
const { stopping } = this;
|
|
2080
|
-
if (this.files.error) throw new RunsetError(this.files.error.message, exitCode
|
|
2091
|
+
if (this.files.error) throw new RunsetError(this.files.error.message, exitCode || 1);
|
|
2081
2092
|
if (stopping?.signal) throw new RunsetError(`the run was stopped by ${stopping.signal}`, signalExitCode(stopping.signal), "");
|
|
2082
2093
|
if (stopping?.reason === "terminate") throw new RunsetError("the run was terminated", signalExitCode("SIGTERM"));
|
|
2083
|
-
if (
|
|
2094
|
+
if (failed.length > 0) {
|
|
2095
|
+
const tally = this.tally(failed);
|
|
2096
|
+
throw new RunsetError(`${tally}: ${failed.map((process) => describeFailure(process)).join(", ")}`, exitCode, this.failureReport(tally, failed));
|
|
2097
|
+
}
|
|
2084
2098
|
return this;
|
|
2085
2099
|
}
|
|
2086
2100
|
terminate(options = {}) {
|
|
2087
2101
|
this.stopEverything({ reason: "terminate" }, options);
|
|
2088
2102
|
}
|
|
2089
2103
|
/** `1 of 5 commands failed, 2 stopped, 1 not started`. */
|
|
2090
|
-
tally() {
|
|
2104
|
+
tally(failed) {
|
|
2091
2105
|
const { processes } = this;
|
|
2092
2106
|
const total = processes.length;
|
|
2093
2107
|
const stopped = processes.filter((process) => process.started && process.terminated).length;
|
|
2094
2108
|
const notStarted = processes.filter((process) => !process.started).length;
|
|
2095
2109
|
return [
|
|
2096
|
-
`${
|
|
2110
|
+
`${failed.length} of ${total} command${total === 1 ? "" : "s"} failed`,
|
|
2097
2111
|
stopped > 0 && `${stopped} stopped`,
|
|
2098
2112
|
notStarted > 0 && `${notStarted} not started`
|
|
2099
2113
|
].filter(Boolean).join(", ");
|
|
2100
2114
|
}
|
|
2101
2115
|
/** The tally, then a row per failure, labelled as its output was. */
|
|
2102
|
-
failureReport() {
|
|
2103
|
-
const
|
|
2104
|
-
const red = (text) =>
|
|
2105
|
-
|
|
2116
|
+
failureReport(tally, failed) {
|
|
2117
|
+
const colored = this.config.color !== "none";
|
|
2118
|
+
const red = (text) => paint(text, ["red"], colored);
|
|
2119
|
+
return [tally, ...failed.map((process) => {
|
|
2106
2120
|
const { command } = process;
|
|
2107
2121
|
const label = command.label.trim();
|
|
2108
|
-
const prefix =
|
|
2122
|
+
const prefix = label === "" ? "" : defaultLabel({
|
|
2109
2123
|
...command,
|
|
2110
2124
|
label
|
|
2111
|
-
},
|
|
2125
|
+
}, colored);
|
|
2112
2126
|
return ` ${red("✖")} ${prefix}${command.command} ${red(process.describeExit())}`;
|
|
2113
|
-
});
|
|
2114
|
-
return [this.tally(), ...rows].join("\n");
|
|
2127
|
+
})].join("\n");
|
|
2115
2128
|
}
|
|
2116
2129
|
failed() {
|
|
2117
2130
|
return this.processes.filter((process) => process.isFailed());
|
|
@@ -2190,7 +2203,7 @@ Output:
|
|
|
2190
2203
|
none | auto | custom | all (default: auto)
|
|
2191
2204
|
--show-command print each command as it starts
|
|
2192
2205
|
--show-exit-code mark each exit and its duration: ✓ 2.1s, ✗ code 1 · 2.1s
|
|
2193
|
-
|
|
2206
|
+
--no-wrap leave long labelled lines whole instead of wrapping
|
|
2194
2207
|
|
|
2195
2208
|
General:
|
|
2196
2209
|
-c, --config <path> load a config file, skipping the default lookup
|
|
@@ -2250,7 +2263,17 @@ function version() {
|
|
|
2250
2263
|
async function runset(first, second) {
|
|
2251
2264
|
return Run.fromConfigJs(toConfigJs(first, second)).start();
|
|
2252
2265
|
}
|
|
2253
|
-
if (import.meta.main
|
|
2266
|
+
if (import.meta.main || isEntryPoint()) main(process.argv.slice(2)).then((code) => {
|
|
2267
|
+
process.exitCode = code;
|
|
2268
|
+
}).catch((error) => {
|
|
2269
|
+
console.error(error);
|
|
2270
|
+
process.exitCode = 1;
|
|
2271
|
+
}).finally(() => {
|
|
2272
|
+
setTimeout(() => {
|
|
2273
|
+
console.error("runset: forced exit, a handle kept the process alive");
|
|
2274
|
+
process.exit();
|
|
2275
|
+
}, 500).unref();
|
|
2276
|
+
});
|
|
2254
2277
|
/** A lone object carrying `commands` is a config; anything else is commands. */
|
|
2255
2278
|
function toConfigJs(first, second) {
|
|
2256
2279
|
if (second === void 0 && isConfigObject(first)) return first;
|
|
@@ -2261,7 +2284,20 @@ function toConfigJs(first, second) {
|
|
|
2261
2284
|
};
|
|
2262
2285
|
}
|
|
2263
2286
|
function isConfigObject(value) {
|
|
2264
|
-
return
|
|
2287
|
+
return isPlainObject(value) && Array.isArray(value.commands);
|
|
2288
|
+
}
|
|
2289
|
+
/**
|
|
2290
|
+
* Node 24.2 reports `import.meta.main` as false for a `.ts` entry point, which
|
|
2291
|
+
* is how the tests run this file; 24.3 fixed it. The module URL is a real
|
|
2292
|
+
* path, so the argument is resolved to one too — macOS's temp dir is a link.
|
|
2293
|
+
*/
|
|
2294
|
+
function isEntryPoint() {
|
|
2295
|
+
const entry = process.argv[1];
|
|
2296
|
+
try {
|
|
2297
|
+
return entry !== void 0 && fs.realpathSync(entry) === fileURLToPath(import.meta.url);
|
|
2298
|
+
} catch {
|
|
2299
|
+
return false;
|
|
2300
|
+
}
|
|
2265
2301
|
}
|
|
2266
2302
|
//#endregion
|
|
2267
|
-
export { CliError, ConfigError,
|
|
2303
|
+
export { CliError, ConfigError, NormalizeError, Run, RunsetError, runset as default, runset };
|