runset 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -12
- package/dist/index.d.mts +30 -28
- package/dist/index.mjs +1450 -1413
- package/package.json +2 -2
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,1325 @@ 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
|
-
"commandDictionary",
|
|
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
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|
|
762
799
|
},
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
800
|
+
{
|
|
801
|
+
name: "serial",
|
|
802
|
+
short: "s",
|
|
803
|
+
takesValue: false
|
|
766
804
|
},
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
805
|
+
{
|
|
806
|
+
name: "recursive",
|
|
807
|
+
short: "r",
|
|
808
|
+
takesValue: false
|
|
770
809
|
},
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
810
|
+
{
|
|
811
|
+
name: "jobs",
|
|
812
|
+
short: "j",
|
|
813
|
+
takesValue: true
|
|
774
814
|
},
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
815
|
+
{
|
|
816
|
+
name: "killTimeout",
|
|
817
|
+
takesValue: true
|
|
778
818
|
},
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
819
|
+
{
|
|
820
|
+
name: "onSuccess",
|
|
821
|
+
takesValue: true
|
|
782
822
|
},
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
823
|
+
{
|
|
824
|
+
name: "onFailure",
|
|
825
|
+
takesValue: true
|
|
786
826
|
},
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
827
|
+
{
|
|
828
|
+
name: "config",
|
|
829
|
+
short: "c",
|
|
830
|
+
takesValue: true
|
|
790
831
|
},
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
832
|
+
{
|
|
833
|
+
name: "cwd",
|
|
834
|
+
takesValue: true
|
|
794
835
|
},
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
836
|
+
{
|
|
837
|
+
name: "env",
|
|
838
|
+
short: "e",
|
|
839
|
+
takesValue: true
|
|
798
840
|
},
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
841
|
+
{
|
|
842
|
+
name: "color",
|
|
843
|
+
negated: "none",
|
|
844
|
+
takesValue: true
|
|
802
845
|
},
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
846
|
+
{
|
|
847
|
+
name: "logLevel",
|
|
848
|
+
takesValue: true
|
|
806
849
|
},
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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"
|
|
902
|
-
},
|
|
903
|
-
{
|
|
904
|
-
bgColor: "bgMagenta",
|
|
905
|
-
color: "white"
|
|
850
|
+
{
|
|
851
|
+
name: "dryRun",
|
|
852
|
+
takesValue: false
|
|
906
853
|
},
|
|
907
854
|
{
|
|
908
|
-
|
|
909
|
-
|
|
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
|
+
short: "w",
|
|
882
|
+
takesValue: false
|
|
934
883
|
},
|
|
935
884
|
{
|
|
936
|
-
|
|
937
|
-
|
|
885
|
+
name: "help",
|
|
886
|
+
short: "h",
|
|
887
|
+
takesValue: false
|
|
938
888
|
},
|
|
939
889
|
{
|
|
940
|
-
|
|
941
|
-
|
|
890
|
+
name: "version",
|
|
891
|
+
short: "v",
|
|
892
|
+
takesValue: false
|
|
942
893
|
}
|
|
943
894
|
];
|
|
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
895
|
/**
|
|
1004
|
-
*
|
|
1005
|
-
*
|
|
1006
|
-
*
|
|
896
|
+
* Splits argv into options, commands, and the literal arguments after `--`.
|
|
897
|
+
* Each `-p`/`-s` becomes a settings entry where it was typed, the same object
|
|
898
|
+
* a config file would write.
|
|
1007
899
|
*/
|
|
1008
|
-
function
|
|
900
|
+
function parseCli(rawArgv) {
|
|
901
|
+
const argv = regroupQuoted(rawArgv);
|
|
902
|
+
const options = {};
|
|
1009
903
|
const commands = [];
|
|
1010
|
-
|
|
1011
|
-
let
|
|
1012
|
-
|
|
1013
|
-
let
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
904
|
+
let positional = [];
|
|
905
|
+
let help = false;
|
|
906
|
+
let version = false;
|
|
907
|
+
let cursor = 0;
|
|
908
|
+
function setOption(spec, raw) {
|
|
909
|
+
switch (spec.name) {
|
|
910
|
+
case "env": {
|
|
911
|
+
const text = String(raw);
|
|
912
|
+
const eq = text.indexOf("=");
|
|
913
|
+
if (eq < 1) throw new CliError(`Option "--env" takes NAME=value, not "${text}".`);
|
|
914
|
+
(options.env ??= {})[text.slice(0, eq)] = text.slice(eq + 1);
|
|
915
|
+
break;
|
|
916
|
+
}
|
|
917
|
+
case "help":
|
|
918
|
+
help = true;
|
|
919
|
+
break;
|
|
920
|
+
case "jobs":
|
|
921
|
+
case "killTimeout":
|
|
922
|
+
options[spec.name] = Number(raw);
|
|
923
|
+
break;
|
|
924
|
+
case "parallel":
|
|
925
|
+
case "serial":
|
|
926
|
+
commands.push({ parallel: spec.name === "parallel" });
|
|
927
|
+
break;
|
|
928
|
+
case "version":
|
|
929
|
+
version = true;
|
|
930
|
+
break;
|
|
931
|
+
default: options[spec.name] = raw;
|
|
1025
932
|
}
|
|
1026
|
-
open = segment.parallel ? open : used + 1;
|
|
1027
933
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
let current;
|
|
1034
|
-
let stage;
|
|
1035
|
-
for (const item of items) {
|
|
1036
|
-
const own = stageOf(item);
|
|
1037
|
-
if (current === void 0 || own !== stage) {
|
|
1038
|
-
current = [];
|
|
1039
|
-
stage = own;
|
|
1040
|
-
stages.push(current);
|
|
1041
|
-
}
|
|
1042
|
-
current.push(item);
|
|
934
|
+
function takeValue(label) {
|
|
935
|
+
const value = argv[cursor + 1];
|
|
936
|
+
if (value === void 0) throw new CliError(`Option "${label}" needs a value.`);
|
|
937
|
+
cursor += 1;
|
|
938
|
+
return value;
|
|
1043
939
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
const mode = config.labels;
|
|
1056
|
-
if (mode === "none") {
|
|
1057
|
-
for (const command of commands) command.label = "";
|
|
1058
|
-
return;
|
|
1059
|
-
}
|
|
1060
|
-
const named = [];
|
|
1061
|
-
for (const stage of groupByStage(commands, (command) => command.stage)) {
|
|
1062
|
-
const shared = stage.length > 1;
|
|
1063
|
-
const labeled = stage.some((command) => command.label !== "");
|
|
1064
|
-
for (const command of stage) {
|
|
1065
|
-
if (command.label !== "") continue;
|
|
1066
|
-
if (mode === "all" || mode === "auto" && shared) {
|
|
1067
|
-
command.label = nameOf(command);
|
|
1068
|
-
named.push(command);
|
|
1069
|
-
} else if (shared && labeled) command.label = " ";
|
|
940
|
+
function readLong(body) {
|
|
941
|
+
const eq = body.indexOf("=");
|
|
942
|
+
const name = eq === -1 ? body : body.slice(0, eq);
|
|
943
|
+
const inlineValue = eq === -1 ? void 0 : body.slice(eq + 1);
|
|
944
|
+
const flag = findLongFlag(name);
|
|
945
|
+
if (flag === void 0 && /^no-?[A-Za-z]/.test(name)) {
|
|
946
|
+
const negated = findLongFlag(name.replace(/^no-?/, ""));
|
|
947
|
+
if (negated && (!negated.takesValue || negated.negated !== void 0)) {
|
|
948
|
+
setOption(negated, negated.negated ?? false);
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
1070
951
|
}
|
|
952
|
+
if (flag === void 0) throw new CliError(`Unknown option "--${name}".`);
|
|
953
|
+
if (flag.takesValue) setOption(flag, inlineValue ?? takeValue(`--${name}`));
|
|
954
|
+
else setOption(flag, true);
|
|
1071
955
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
956
|
+
function readShortCluster(body) {
|
|
957
|
+
for (let position = 0; position < body.length; position += 1) {
|
|
958
|
+
const letter = body[position];
|
|
959
|
+
const flag = findShortFlag(letter);
|
|
960
|
+
if (flag === void 0) throw new CliError(`Unknown option "-${letter}".`);
|
|
961
|
+
if (!flag.takesValue) {
|
|
962
|
+
setOption(flag, true);
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
const rest = body.slice(position + 1);
|
|
966
|
+
setOption(flag, rest === "" ? takeValue(`-${letter}`) : rest);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
1085
969
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
970
|
+
for (; cursor < argv.length; cursor += 1) {
|
|
971
|
+
const token = argv[cursor];
|
|
972
|
+
if (token === "--") {
|
|
973
|
+
positional = argv.slice(cursor + 1);
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
if (token.startsWith("--") && token.length > 2) readLong(token.slice(2));
|
|
977
|
+
else if (token.startsWith("-") && token.length > 1) readShortCluster(token.slice(1));
|
|
978
|
+
else commands.push(token);
|
|
1095
979
|
}
|
|
980
|
+
return {
|
|
981
|
+
argv,
|
|
982
|
+
commands,
|
|
983
|
+
help,
|
|
984
|
+
options,
|
|
985
|
+
positional,
|
|
986
|
+
version
|
|
987
|
+
};
|
|
1096
988
|
}
|
|
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
|
-
}
|
|
989
|
+
function findLongFlag(name) {
|
|
990
|
+
const camel = toCamelCase(name);
|
|
991
|
+
return FLAGS.find((flag) => flag.name === camel);
|
|
1118
992
|
}
|
|
1119
|
-
function
|
|
1120
|
-
|
|
1121
|
-
let index = preferred;
|
|
1122
|
-
while (taken.has(index)) index = (index + 1) % size;
|
|
1123
|
-
return index;
|
|
993
|
+
function findShortFlag(letter) {
|
|
994
|
+
return FLAGS.find((flag) => flag.short === letter);
|
|
1124
995
|
}
|
|
1125
996
|
//#endregion
|
|
1126
|
-
//#region src/
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
997
|
+
//#region src/config/Config.ts
|
|
998
|
+
const DEFAULT_KILL_TIMEOUT = 5e3;
|
|
999
|
+
const LOG_LEVELS = /* @__PURE__ */ new Set([
|
|
1000
|
+
"error",
|
|
1001
|
+
"warn",
|
|
1002
|
+
"info",
|
|
1003
|
+
"debug"
|
|
1004
|
+
]);
|
|
1005
|
+
const LABEL_MODES = /* @__PURE__ */ new Set([
|
|
1006
|
+
"none",
|
|
1007
|
+
"auto",
|
|
1008
|
+
"custom",
|
|
1009
|
+
"all"
|
|
1010
|
+
]);
|
|
1011
|
+
const COLOR_MODES = /* @__PURE__ */ new Set([
|
|
1012
|
+
"auto",
|
|
1013
|
+
"none",
|
|
1014
|
+
"basic",
|
|
1015
|
+
"soft",
|
|
1016
|
+
"all"
|
|
1017
|
+
]);
|
|
1018
|
+
/** Every key a config may set; a record, so a new one cannot be missed. */
|
|
1019
|
+
const KNOWN_KEYS = {
|
|
1020
|
+
color: true,
|
|
1021
|
+
commands: true,
|
|
1022
|
+
cwd: true,
|
|
1023
|
+
dryRun: true,
|
|
1024
|
+
env: true,
|
|
1025
|
+
formatLabel: true,
|
|
1026
|
+
jobs: true,
|
|
1027
|
+
killTimeout: true,
|
|
1028
|
+
labels: true,
|
|
1029
|
+
logLevel: true,
|
|
1030
|
+
onFailure: true,
|
|
1031
|
+
onSuccess: true,
|
|
1032
|
+
output: true,
|
|
1033
|
+
parallel: true,
|
|
1034
|
+
recursive: true,
|
|
1035
|
+
scripts: true,
|
|
1036
|
+
showCommand: true,
|
|
1037
|
+
showExitCode: true,
|
|
1038
|
+
stderr: true,
|
|
1039
|
+
stdout: true,
|
|
1040
|
+
wrap: true
|
|
1041
|
+
};
|
|
1042
|
+
/** Parses, loads and validates; anything left out comes from the process. */
|
|
1043
|
+
function createConfig({ cli = parseCli(process.argv.slice(2)), configJs, cwd = process.cwd(), destinations = process, env = process.env } = {}) {
|
|
1044
|
+
const config = new Config({
|
|
1045
|
+
cli,
|
|
1046
|
+
configJs,
|
|
1047
|
+
cwd,
|
|
1048
|
+
destinations,
|
|
1049
|
+
env
|
|
1050
|
+
});
|
|
1051
|
+
config.validate();
|
|
1052
|
+
return config;
|
|
1137
1053
|
}
|
|
1054
|
+
/** The resolved run-wide configuration: CLI flag → config file → default. */
|
|
1055
|
+
var Config = class {
|
|
1056
|
+
commands;
|
|
1057
|
+
scripts;
|
|
1058
|
+
/** Everything after `--`, for placeholders. */
|
|
1059
|
+
args;
|
|
1060
|
+
cwd;
|
|
1061
|
+
env;
|
|
1062
|
+
jobs;
|
|
1063
|
+
killTimeout;
|
|
1064
|
+
onSuccess;
|
|
1065
|
+
onFailure;
|
|
1066
|
+
color;
|
|
1067
|
+
parallel;
|
|
1068
|
+
recursive;
|
|
1069
|
+
showCommand;
|
|
1070
|
+
showExitCode;
|
|
1071
|
+
wrap;
|
|
1072
|
+
/** `COLUMNS` as runset was started with it: the width where no TTY says. */
|
|
1073
|
+
envColumns;
|
|
1074
|
+
labels;
|
|
1075
|
+
logLevel;
|
|
1076
|
+
dryRun;
|
|
1077
|
+
formatLabel;
|
|
1078
|
+
stdout;
|
|
1079
|
+
stderr;
|
|
1080
|
+
destinations;
|
|
1081
|
+
/** Printed by the run once it is built. */
|
|
1082
|
+
warnings = [];
|
|
1083
|
+
constructor({ cli, configJs, cwd, destinations, env }) {
|
|
1084
|
+
const { options } = cli;
|
|
1085
|
+
this.cwd = path.resolve(cwd, options.cwd ?? ".");
|
|
1086
|
+
this.destinations = destinations;
|
|
1087
|
+
const context = {
|
|
1088
|
+
argv: cli.argv,
|
|
1089
|
+
cwd: this.cwd,
|
|
1090
|
+
env
|
|
1091
|
+
};
|
|
1092
|
+
const file = configJs === void 0 ? loadConfigJs(context, options.config) : unwrapConfigJs(configJs, context);
|
|
1093
|
+
for (const key of Object.keys(file)) if (!Object.hasOwn(KNOWN_KEYS, key)) this.warnings.push(`Unknown config key "${key}" was ignored.`);
|
|
1094
|
+
if (file.cwd !== void 0 && options.cwd === void 0) this.cwd = path.resolve(cwd, file.cwd);
|
|
1095
|
+
check(envError("env", file.env ?? {}), ConfigError);
|
|
1096
|
+
this.env = {
|
|
1097
|
+
...env,
|
|
1098
|
+
...file.env,
|
|
1099
|
+
...options.env
|
|
1100
|
+
};
|
|
1101
|
+
this.commands = [...file.commands ?? [], ...cli.commands].filter(Boolean);
|
|
1102
|
+
this.scripts = file.scripts ?? {};
|
|
1103
|
+
this.args = cli.positional;
|
|
1104
|
+
this.jobs = options.jobs ?? file.jobs ?? Number.POSITIVE_INFINITY;
|
|
1105
|
+
this.killTimeout = options.killTimeout ?? file.killTimeout ?? DEFAULT_KILL_TIMEOUT;
|
|
1106
|
+
this.parallel = file.parallel ?? false;
|
|
1107
|
+
this.recursive = options.recursive ?? file.recursive ?? false;
|
|
1108
|
+
this.showCommand = options.showCommand ?? file.showCommand ?? false;
|
|
1109
|
+
this.showExitCode = options.showExitCode ?? file.showExitCode ?? false;
|
|
1110
|
+
this.wrap = options.wrap ?? file.wrap ?? false;
|
|
1111
|
+
this.envColumns = parseColumns(env.COLUMNS);
|
|
1112
|
+
this.labels = options.labels ?? file.labels ?? "auto";
|
|
1113
|
+
this.onSuccess = options.onSuccess ?? file.onSuccess ?? "continue";
|
|
1114
|
+
this.onFailure = options.onFailure ?? file.onFailure ?? "stop";
|
|
1115
|
+
this.dryRun = options.dryRun ?? file.dryRun ?? false;
|
|
1116
|
+
this.logLevel = options.logLevel ?? file.logLevel ?? "info";
|
|
1117
|
+
this.color = resolveColor(options.color ?? file.color, env, destinations);
|
|
1118
|
+
this.formatLabel = file.formatLabel;
|
|
1119
|
+
const streams = layerStreams({
|
|
1120
|
+
stderr: defaultStd("stderr"),
|
|
1121
|
+
stdout: defaultStd("stdout")
|
|
1122
|
+
}, [file, options]);
|
|
1123
|
+
this.stdout = streams.stdout;
|
|
1124
|
+
this.stderr = streams.stderr;
|
|
1125
|
+
}
|
|
1126
|
+
/** Throws on the first invalid value; types too, as config files are arbitrary JS. */
|
|
1127
|
+
validate() {
|
|
1128
|
+
check([
|
|
1129
|
+
oneOfError("logLevel", this.logLevel, LOG_LEVELS),
|
|
1130
|
+
numberError("jobs", this.jobs, 1),
|
|
1131
|
+
numberError("killTimeout", this.killTimeout, 0),
|
|
1132
|
+
oneOfError("color", this.color, COLOR_MODES),
|
|
1133
|
+
booleanError("parallel", this.parallel),
|
|
1134
|
+
booleanError("recursive", this.recursive),
|
|
1135
|
+
booleanError("showCommand", this.showCommand),
|
|
1136
|
+
booleanError("showExitCode", this.showExitCode),
|
|
1137
|
+
booleanError("wrap", this.wrap),
|
|
1138
|
+
booleanError("dryRun", this.dryRun),
|
|
1139
|
+
oneOfError("labels", this.labels, LABEL_MODES),
|
|
1140
|
+
stdError("stdout", this.stdout),
|
|
1141
|
+
stdError("stderr", this.stderr),
|
|
1142
|
+
this.formatLabel !== void 0 && typeof this.formatLabel !== "function" ? "formatLabel must be a function." : void 0,
|
|
1143
|
+
isPlainObject(this.scripts) ? void 0 : "scripts must be an object.",
|
|
1144
|
+
actionError("onSuccess", this.onSuccess),
|
|
1145
|
+
actionError("onFailure", this.onFailure)
|
|
1146
|
+
].find((error) => error !== void 0), ConfigError);
|
|
1147
|
+
}
|
|
1148
|
+
/** True when `commands` holds something to run, not just settings entries. */
|
|
1149
|
+
hasCommands() {
|
|
1150
|
+
return this.commands.some((command) => Boolean(command) && !isSettingsEntry(command));
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1138
1153
|
/**
|
|
1139
|
-
*
|
|
1140
|
-
* `
|
|
1154
|
+
* A mode as given; under `auto`, whether to color at all, then `soft` where
|
|
1155
|
+
* the terminal shows 256 colors. `all` is only ever asked for. Anything else is
|
|
1156
|
+
* left for `validate` to refuse.
|
|
1141
1157
|
*/
|
|
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
|
-
};
|
|
1158
|
+
function resolveColor(mode, env, destinations) {
|
|
1159
|
+
if (mode !== void 0 && mode !== "auto") return mode;
|
|
1160
|
+
if (!detectColor(env, destinations)) return "none";
|
|
1161
|
+
return WriteStream.prototype.getColorDepth(env) >= 8 ? "soft" : "basic";
|
|
1287
1162
|
}
|
|
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;
|
|
1163
|
+
/** `NO_COLOR`/`FORCE_COLOR`, then whether both streams are TTYs. */
|
|
1164
|
+
function detectColor(env, destinations) {
|
|
1165
|
+
if (env.NO_COLOR !== void 0 && env.NO_COLOR !== "") return false;
|
|
1166
|
+
if (env.FORCE_COLOR !== void 0 && env.FORCE_COLOR !== "" && env.FORCE_COLOR !== "0") return true;
|
|
1167
|
+
return isTerminal(destinations.stdout) && isTerminal(destinations.stderr);
|
|
1309
1168
|
}
|
|
1310
1169
|
//#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
|
-
|
|
1170
|
+
//#region src/utils/colors.ts
|
|
1171
|
+
const KNOWN = new Set(Object.keys(inspect.colors));
|
|
1172
|
+
const SHADES = {
|
|
1173
|
+
mint: {
|
|
1174
|
+
index: 115,
|
|
1175
|
+
fallback: "greenBright"
|
|
1176
|
+
},
|
|
1177
|
+
sky: {
|
|
1178
|
+
index: 111,
|
|
1179
|
+
fallback: "blueBright"
|
|
1180
|
+
},
|
|
1181
|
+
rose: {
|
|
1182
|
+
index: 211,
|
|
1183
|
+
fallback: "magentaBright"
|
|
1184
|
+
},
|
|
1185
|
+
amber: {
|
|
1186
|
+
index: 179,
|
|
1187
|
+
fallback: "yellowBright"
|
|
1188
|
+
},
|
|
1189
|
+
lavender: {
|
|
1190
|
+
index: 183,
|
|
1191
|
+
fallback: "magentaBright"
|
|
1192
|
+
},
|
|
1193
|
+
aqua: {
|
|
1194
|
+
index: 80,
|
|
1195
|
+
fallback: "cyanBright"
|
|
1196
|
+
},
|
|
1197
|
+
coral: {
|
|
1198
|
+
index: 210,
|
|
1199
|
+
fallback: "redBright"
|
|
1200
|
+
},
|
|
1201
|
+
sage: {
|
|
1202
|
+
index: 151,
|
|
1203
|
+
fallback: "greenBright"
|
|
1204
|
+
},
|
|
1205
|
+
periwinkle: {
|
|
1206
|
+
index: 147,
|
|
1207
|
+
fallback: "blueBright"
|
|
1208
|
+
},
|
|
1209
|
+
peach: {
|
|
1210
|
+
index: 216,
|
|
1211
|
+
fallback: "yellowBright"
|
|
1212
|
+
},
|
|
1213
|
+
teal: {
|
|
1214
|
+
index: 79,
|
|
1215
|
+
fallback: "cyanBright"
|
|
1216
|
+
},
|
|
1217
|
+
lilac: {
|
|
1218
|
+
index: 177,
|
|
1219
|
+
fallback: "magentaBright"
|
|
1220
|
+
},
|
|
1221
|
+
lime: {
|
|
1222
|
+
index: 149,
|
|
1223
|
+
fallback: "greenBright"
|
|
1224
|
+
},
|
|
1225
|
+
steel: {
|
|
1226
|
+
index: 110,
|
|
1227
|
+
fallback: "blueBright"
|
|
1228
|
+
},
|
|
1229
|
+
pink: {
|
|
1230
|
+
index: 218,
|
|
1231
|
+
fallback: "magentaBright"
|
|
1232
|
+
},
|
|
1233
|
+
seafoam: {
|
|
1234
|
+
index: 121,
|
|
1235
|
+
fallback: "cyanBright"
|
|
1236
|
+
},
|
|
1237
|
+
apricot: {
|
|
1238
|
+
index: 215,
|
|
1239
|
+
fallback: "yellowBright"
|
|
1240
|
+
},
|
|
1241
|
+
mauve: {
|
|
1242
|
+
index: 139,
|
|
1243
|
+
fallback: "magentaBright"
|
|
1244
|
+
},
|
|
1245
|
+
jade: {
|
|
1246
|
+
index: 114,
|
|
1247
|
+
fallback: "greenBright"
|
|
1248
|
+
},
|
|
1249
|
+
sand: {
|
|
1250
|
+
index: 180,
|
|
1251
|
+
fallback: "yellowBright"
|
|
1252
|
+
},
|
|
1253
|
+
iris: {
|
|
1254
|
+
index: 141,
|
|
1255
|
+
fallback: "blueBright"
|
|
1256
|
+
},
|
|
1257
|
+
ice: {
|
|
1258
|
+
index: 117,
|
|
1259
|
+
fallback: "cyanBright"
|
|
1260
|
+
},
|
|
1261
|
+
salmon: {
|
|
1262
|
+
index: 174,
|
|
1263
|
+
fallback: "redBright"
|
|
1264
|
+
},
|
|
1265
|
+
olive: {
|
|
1266
|
+
index: 150,
|
|
1267
|
+
fallback: "greenBright"
|
|
1268
|
+
},
|
|
1269
|
+
navy: {
|
|
1270
|
+
index: 24,
|
|
1271
|
+
fallback: "blue",
|
|
1272
|
+
foreground: "paper"
|
|
1273
|
+
},
|
|
1274
|
+
plum: {
|
|
1275
|
+
index: 96,
|
|
1276
|
+
fallback: "magenta",
|
|
1277
|
+
foreground: "paper"
|
|
1278
|
+
},
|
|
1279
|
+
forest: {
|
|
1280
|
+
index: 22,
|
|
1281
|
+
fallback: "green",
|
|
1282
|
+
foreground: "paper"
|
|
1283
|
+
},
|
|
1284
|
+
wine: {
|
|
1285
|
+
index: 89,
|
|
1286
|
+
fallback: "red",
|
|
1287
|
+
foreground: "paper"
|
|
1288
|
+
},
|
|
1289
|
+
ocean: {
|
|
1290
|
+
index: 25,
|
|
1291
|
+
fallback: "blue",
|
|
1292
|
+
foreground: "paper"
|
|
1293
|
+
},
|
|
1294
|
+
indigo: {
|
|
1295
|
+
index: 61,
|
|
1296
|
+
fallback: "blue",
|
|
1297
|
+
foreground: "paper"
|
|
1298
|
+
},
|
|
1299
|
+
copper: {
|
|
1300
|
+
index: 94,
|
|
1301
|
+
fallback: "red",
|
|
1302
|
+
foreground: "paper"
|
|
1303
|
+
},
|
|
1304
|
+
pine: {
|
|
1305
|
+
index: 23,
|
|
1306
|
+
fallback: "cyan",
|
|
1307
|
+
foreground: "paper"
|
|
1308
|
+
},
|
|
1309
|
+
violet: {
|
|
1310
|
+
index: 55,
|
|
1311
|
+
fallback: "magenta",
|
|
1312
|
+
foreground: "paper"
|
|
1313
|
+
},
|
|
1314
|
+
brick: {
|
|
1315
|
+
index: 88,
|
|
1316
|
+
fallback: "red",
|
|
1317
|
+
foreground: "paper"
|
|
1318
|
+
},
|
|
1319
|
+
slate: {
|
|
1320
|
+
index: 60,
|
|
1321
|
+
fallback: "blue",
|
|
1322
|
+
foreground: "paper"
|
|
1323
|
+
},
|
|
1324
|
+
cocoa: {
|
|
1325
|
+
index: 95,
|
|
1326
|
+
fallback: "red",
|
|
1327
|
+
foreground: "paper"
|
|
1328
|
+
},
|
|
1329
|
+
ink: {
|
|
1330
|
+
index: 235,
|
|
1331
|
+
fallback: "black"
|
|
1332
|
+
},
|
|
1333
|
+
paper: {
|
|
1334
|
+
index: 255,
|
|
1335
|
+
fallback: "whiteBright"
|
|
1448
1336
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1337
|
+
};
|
|
1338
|
+
/** Every shade by its foreground and its background name, e.g. `mint`, `bgMint`. */
|
|
1339
|
+
const SHADE_NAMES = new Map(Object.entries(SHADES).flatMap(([name, custom]) => [[name, custom], [toCamelCase(`bg-${name}`), custom]]));
|
|
1340
|
+
const BASIC_PALETTE = [
|
|
1341
|
+
{
|
|
1342
|
+
bgColor: "bgGreen",
|
|
1343
|
+
color: "black"
|
|
1344
|
+
},
|
|
1345
|
+
{
|
|
1346
|
+
bgColor: "bgBlue",
|
|
1347
|
+
color: "white"
|
|
1348
|
+
},
|
|
1349
|
+
{
|
|
1350
|
+
bgColor: "bgMagenta",
|
|
1351
|
+
color: "white"
|
|
1352
|
+
},
|
|
1353
|
+
{
|
|
1354
|
+
bgColor: "bgCyan",
|
|
1355
|
+
color: "black"
|
|
1356
|
+
},
|
|
1357
|
+
{
|
|
1358
|
+
bgColor: "bgYellow",
|
|
1359
|
+
color: "black"
|
|
1360
|
+
},
|
|
1361
|
+
{
|
|
1362
|
+
bgColor: "bgRed",
|
|
1363
|
+
color: "white"
|
|
1364
|
+
},
|
|
1365
|
+
{
|
|
1366
|
+
bgColor: "bgGray",
|
|
1367
|
+
color: "white"
|
|
1368
|
+
},
|
|
1369
|
+
{
|
|
1370
|
+
bgColor: "bgGreenBright",
|
|
1371
|
+
color: "black"
|
|
1372
|
+
},
|
|
1373
|
+
{
|
|
1374
|
+
bgColor: "bgBlueBright",
|
|
1375
|
+
color: "black"
|
|
1376
|
+
},
|
|
1377
|
+
{
|
|
1378
|
+
bgColor: "bgMagentaBright",
|
|
1379
|
+
color: "black"
|
|
1380
|
+
},
|
|
1381
|
+
{
|
|
1382
|
+
bgColor: "bgCyanBright",
|
|
1383
|
+
color: "black"
|
|
1384
|
+
},
|
|
1385
|
+
{
|
|
1386
|
+
bgColor: "bgYellowBright",
|
|
1387
|
+
color: "black"
|
|
1451
1388
|
}
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1389
|
+
];
|
|
1390
|
+
/** Interleave hue families so adjacent assignments remain easy to tell apart. */
|
|
1391
|
+
const PALETTE = Object.entries(SHADES).filter(([name]) => name !== "ink" && name !== "paper").map(([name, { foreground }]) => ({
|
|
1392
|
+
bgColor: toCamelCase(`bg-${name}`),
|
|
1393
|
+
color: foreground ?? "ink"
|
|
1394
|
+
}));
|
|
1395
|
+
/** What `colorLabels` hands out under each mode; `none` still picks, unseen. */
|
|
1396
|
+
const PALETTES = {
|
|
1397
|
+
none: BASIC_PALETTE,
|
|
1398
|
+
basic: BASIC_PALETTE,
|
|
1399
|
+
soft: PALETTE.filter((pair) => pair.color === "ink"),
|
|
1400
|
+
all: PALETTE
|
|
1401
|
+
};
|
|
1402
|
+
/** A built-in style or one of runset's foreground/background shades. */
|
|
1403
|
+
function isColorName(name) {
|
|
1404
|
+
return KNOWN.has(name) || shade(name) !== void 0;
|
|
1455
1405
|
}
|
|
1456
|
-
/**
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1406
|
+
/**
|
|
1407
|
+
* Styles text; a shade is painted from the 256-color table. Plain text when
|
|
1408
|
+
* not `enabled`.
|
|
1409
|
+
*/
|
|
1410
|
+
function paint(text, colors, enabled = true) {
|
|
1411
|
+
if (!enabled) return text;
|
|
1412
|
+
let result = text;
|
|
1413
|
+
for (const name of colors.toReversed()) {
|
|
1414
|
+
const custom = shade(name);
|
|
1415
|
+
if (custom !== void 0) {
|
|
1416
|
+
const background = name.startsWith("bg");
|
|
1417
|
+
const open = `\u001B[${background ? 48 : 38};5;${custom.index}m`;
|
|
1418
|
+
const close = `\u001B[${background ? 49 : 39}m`;
|
|
1419
|
+
result = open + result.replaceAll(close, open) + close;
|
|
1420
|
+
} else if (KNOWN.has(name)) result = styleText(name, result, { validateStream: false });
|
|
1421
|
+
}
|
|
1422
|
+
return result;
|
|
1461
1423
|
}
|
|
1462
|
-
|
|
1463
|
-
|
|
1424
|
+
/** The theme color used for a shade on a basic terminal. */
|
|
1425
|
+
function fallbackColor(name) {
|
|
1426
|
+
const custom = shade(name);
|
|
1427
|
+
if (custom === void 0) return name;
|
|
1428
|
+
return name.startsWith("bg") ? toCamelCase(`bg-${custom.fallback}`) : custom.fallback;
|
|
1464
1429
|
}
|
|
1465
|
-
|
|
1466
|
-
|
|
1430
|
+
/** A stable palette index for a label. */
|
|
1431
|
+
function paletteIndex(label, palette = PALETTE) {
|
|
1432
|
+
/** The largest modulus that keeps `hash * 31 + code` an exact integer. */
|
|
1433
|
+
const HASH_MODULUS = 2147483647;
|
|
1434
|
+
let hash = 0;
|
|
1435
|
+
for (const character of label) hash = (hash * 31 + (character.codePointAt(0) ?? 0)) % HASH_MODULUS;
|
|
1436
|
+
return hash % palette.length;
|
|
1467
1437
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
return options.cwd === void 0 ? options : {
|
|
1471
|
-
...options,
|
|
1472
|
-
cwd: path.resolve(base, options.cwd)
|
|
1473
|
-
};
|
|
1438
|
+
function shade(name) {
|
|
1439
|
+
return SHADE_NAMES.get(name);
|
|
1474
1440
|
}
|
|
1441
|
+
//#endregion
|
|
1442
|
+
//#region src/plan/labels.ts
|
|
1475
1443
|
/**
|
|
1476
|
-
*
|
|
1477
|
-
*
|
|
1444
|
+
* Settles who is labelled, per `config.labels`. Under `auto`, a command
|
|
1445
|
+
* sharing a stage is named after itself, whether or not someone in its stage
|
|
1446
|
+
* has a label of their own. Under `custom` it gets a blank label instead,
|
|
1447
|
+
* holding the column when someone in its stage has one.
|
|
1478
1448
|
*/
|
|
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;
|
|
1449
|
+
function assignAutoLabels({ commands, config }) {
|
|
1450
|
+
const mode = config.labels;
|
|
1451
|
+
if (mode === "none") {
|
|
1452
|
+
for (const command of commands) command.label = "";
|
|
1453
|
+
return;
|
|
1488
1454
|
}
|
|
1489
|
-
|
|
1455
|
+
const named = [];
|
|
1456
|
+
for (const stage of groupByStage(commands, (command) => command.stage)) {
|
|
1457
|
+
const shared = stage.length > 1;
|
|
1458
|
+
const labeled = stage.some((command) => command.label !== "");
|
|
1459
|
+
for (const command of stage) {
|
|
1460
|
+
if (command.label !== "") continue;
|
|
1461
|
+
if (mode === "all" || mode === "auto" && shared) {
|
|
1462
|
+
command.label = nameOf(command);
|
|
1463
|
+
named.push(command);
|
|
1464
|
+
} else if (shared && labeled) command.label = " ";
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
numberRepeats(named);
|
|
1490
1468
|
}
|
|
1491
|
-
/**
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1469
|
+
/**
|
|
1470
|
+
* Gives each labelled command without colors of its own a palette pair: the
|
|
1471
|
+
* one its label hashes to, or the next free when that is taken, from the
|
|
1472
|
+
* palette of `config.color`. Without shades every one is first swapped for its
|
|
1473
|
+
* theme color, so nothing past the plan has to know how many colors there are.
|
|
1474
|
+
*/
|
|
1475
|
+
function colorLabels({ commands, config }) {
|
|
1476
|
+
const palette = PALETTES[config.color];
|
|
1477
|
+
if (config.color === "none" || config.color === "basic") for (const command of commands) {
|
|
1478
|
+
command.bgColor = fallbackColor(command.bgColor);
|
|
1479
|
+
command.color = fallbackColor(command.color);
|
|
1480
|
+
}
|
|
1481
|
+
const labeled = commands.filter((command) => command.label !== "");
|
|
1482
|
+
const taken = new Set(labeled.map((command) => palette.findIndex((pair) => pair.bgColor === command.bgColor)).filter((index) => index !== -1));
|
|
1483
|
+
for (const command of labeled) {
|
|
1484
|
+
if (command.color !== "" || command.bgColor !== "" || command.label.trim() === "") continue;
|
|
1485
|
+
const index = claim(paletteIndex(command.label, palette), taken, palette.length);
|
|
1486
|
+
taken.add(index);
|
|
1487
|
+
const pair = palette[index];
|
|
1488
|
+
command.bgColor = pair.bgColor;
|
|
1489
|
+
command.color = pair.color;
|
|
1497
1490
|
}
|
|
1498
|
-
return result;
|
|
1499
1491
|
}
|
|
1500
|
-
/**
|
|
1501
|
-
function
|
|
1502
|
-
const
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1492
|
+
/** Pads every label to the widest one. */
|
|
1493
|
+
function alignLabels({ commands }) {
|
|
1494
|
+
const labeled = commands.filter((command) => command.label !== "");
|
|
1495
|
+
const width = Math.max(0, ...labeled.map((command) => command.label.length));
|
|
1496
|
+
for (const command of labeled) command.label = command.label.padEnd(width);
|
|
1497
|
+
}
|
|
1498
|
+
/** The script name, or the program a shell command runs. */
|
|
1499
|
+
function nameOf(command) {
|
|
1500
|
+
return command.packageName ?? command.scriptName ?? command.command.split(/\s/, 1)[0] ?? "";
|
|
1501
|
+
}
|
|
1502
|
+
/** `serve`, `serve` → `serve#1`, `serve#2`. */
|
|
1503
|
+
function numberRepeats(named) {
|
|
1504
|
+
const totals = /* @__PURE__ */ new Map();
|
|
1505
|
+
for (const command of named) totals.set(command.label, (totals.get(command.label) ?? 0) + 1);
|
|
1506
|
+
const counters = /* @__PURE__ */ new Map();
|
|
1507
|
+
for (const command of named) {
|
|
1508
|
+
if ((totals.get(command.label) ?? 0) < 2) continue;
|
|
1509
|
+
const index = (counters.get(command.label) ?? 0) + 1;
|
|
1510
|
+
counters.set(command.label, index);
|
|
1511
|
+
command.label = `${command.label}#${index}`;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
function claim(preferred, taken, size) {
|
|
1515
|
+
if (taken.size >= size) return preferred;
|
|
1516
|
+
let index = preferred;
|
|
1517
|
+
while (taken.has(index)) index = (index + 1) % size;
|
|
1518
|
+
return index;
|
|
1519
1519
|
}
|
|
1520
1520
|
//#endregion
|
|
1521
1521
|
//#region src/plan/plan.ts
|
|
@@ -1602,13 +1602,11 @@ var FileRegistry = class {
|
|
|
1602
1602
|
error;
|
|
1603
1603
|
streams = /* @__PURE__ */ new Map();
|
|
1604
1604
|
cwd;
|
|
1605
|
-
|
|
1606
|
-
|
|
1605
|
+
onFailure;
|
|
1606
|
+
/** `onFailure` is called once, with the first failure. */
|
|
1607
|
+
constructor(cwd, onFailure) {
|
|
1607
1608
|
this.cwd = cwd;
|
|
1608
|
-
|
|
1609
|
-
/** Called once, with the first failure. */
|
|
1610
|
-
onFailure(handler) {
|
|
1611
|
-
this.handler = handler;
|
|
1609
|
+
this.onFailure = onFailure;
|
|
1612
1610
|
}
|
|
1613
1611
|
open(destination) {
|
|
1614
1612
|
const filePath = path.resolve(this.cwd, destination);
|
|
@@ -1626,12 +1624,12 @@ var FileRegistry = class {
|
|
|
1626
1624
|
async closeAll() {
|
|
1627
1625
|
const streams = [...this.streams.values()];
|
|
1628
1626
|
this.streams.clear();
|
|
1629
|
-
await Promise.all(streams.map(
|
|
1627
|
+
await Promise.all(streams.map((stream) => closed(stream)));
|
|
1630
1628
|
}
|
|
1631
1629
|
fail(destination, error) {
|
|
1632
1630
|
if (this.error !== void 0) return;
|
|
1633
1631
|
this.error = /* @__PURE__ */ new Error(`cannot write to "${destination}": ${error.message}`);
|
|
1634
|
-
this.
|
|
1632
|
+
this.onFailure(this.error);
|
|
1635
1633
|
}
|
|
1636
1634
|
};
|
|
1637
1635
|
function closed(stream) {
|
|
@@ -1679,8 +1677,7 @@ var Logger = class {
|
|
|
1679
1677
|
}
|
|
1680
1678
|
write(level, message) {
|
|
1681
1679
|
if (RANK[level] > RANK[this.level]) return;
|
|
1682
|
-
|
|
1683
|
-
this.stream.write(`${text}\n`);
|
|
1680
|
+
this.stream.write(`${paint(message, COLORS[level], this.color)}\n`);
|
|
1684
1681
|
}
|
|
1685
1682
|
};
|
|
1686
1683
|
//#endregion
|
|
@@ -1721,17 +1718,28 @@ function createEnv({ base, cwd, packageInfo, scriptName }) {
|
|
|
1721
1718
|
const PATH_KEY = process.platform === "win32" ? "Path" : "PATH";
|
|
1722
1719
|
const pathKey = Object.keys(base).find((key) => key.toUpperCase() === "PATH") ?? PATH_KEY;
|
|
1723
1720
|
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;
|
|
1721
|
+
const own = {};
|
|
1722
|
+
if (packageInfo.name !== "") own.npm_package_name = packageInfo.name;
|
|
1723
|
+
if (packageInfo.version !== "") own.npm_package_version = packageInfo.version;
|
|
1730
1724
|
if (scriptName !== void 0) {
|
|
1731
|
-
|
|
1732
|
-
|
|
1725
|
+
own.npm_lifecycle_event = scriptName;
|
|
1726
|
+
own.npm_lifecycle_script = packageInfo.scripts[scriptName] ?? "";
|
|
1733
1727
|
}
|
|
1734
|
-
return
|
|
1728
|
+
return {
|
|
1729
|
+
...withoutSpellingsOf(base, Object.keys(own)),
|
|
1730
|
+
...own,
|
|
1731
|
+
[pathKey]: [...binDirs, base[pathKey]].filter(Boolean).join(path.delimiter)
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* Windows names are case-insensitive, and of two spellings a child is given
|
|
1736
|
+
* the one that sorts first — an outer npm's `NPM_…` over ours — so on Windows
|
|
1737
|
+
* every other spelling of a name about to be set is dropped.
|
|
1738
|
+
*/
|
|
1739
|
+
function withoutSpellingsOf(env, names) {
|
|
1740
|
+
if (process.platform !== "win32") return env;
|
|
1741
|
+
const replaced = new Set(names.map((name) => name.toUpperCase()));
|
|
1742
|
+
return Object.fromEntries(Object.entries(env).filter(([key]) => !replaced.has(key.toUpperCase())));
|
|
1735
1743
|
}
|
|
1736
1744
|
//#endregion
|
|
1737
1745
|
//#region src/run/OutputSink.ts
|
|
@@ -1740,31 +1748,26 @@ function createEnv({ base, cwd, packageInfo, scriptName }) {
|
|
|
1740
1748
|
* `grouped` holds everything until {@link flush}, `none` discards.
|
|
1741
1749
|
*/
|
|
1742
1750
|
var OutputSink = class {
|
|
1751
|
+
/** A grouped stream's output, or a prefixed stream's unfinished line. */
|
|
1743
1752
|
buffered = "";
|
|
1744
|
-
/** A prefixed stream's unfinished line, held until its newline. */
|
|
1745
|
-
pending = "";
|
|
1746
1753
|
opened;
|
|
1747
1754
|
/** Whether everything written so far ended with a newline. */
|
|
1748
1755
|
atLineStart = true;
|
|
1749
1756
|
std;
|
|
1750
1757
|
prefix;
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
envColumns;
|
|
1756
|
-
constructor(std, prefix, destinations, files, wrap = false, envColumns) {
|
|
1758
|
+
/** The columns a fixed prefix takes; measured per line otherwise. */
|
|
1759
|
+
prefixWidth;
|
|
1760
|
+
context;
|
|
1761
|
+
constructor(std, prefix, context) {
|
|
1757
1762
|
this.std = std;
|
|
1758
1763
|
this.prefix = prefix;
|
|
1759
|
-
this.
|
|
1760
|
-
this.
|
|
1761
|
-
this.wrap = wrap;
|
|
1762
|
-
this.envColumns = envColumns;
|
|
1764
|
+
this.prefixWidth = typeof prefix === "string" ? visibleWidth(prefix) : void 0;
|
|
1765
|
+
this.context = context;
|
|
1763
1766
|
}
|
|
1764
1767
|
/** Opened on first use, so a command that never writes truncates nothing. */
|
|
1765
1768
|
get target() {
|
|
1766
|
-
if (isStreamDestination(this.std.destination)) return this.destinations[this.std.destination];
|
|
1767
|
-
this.opened ??= this.files.open(this.std.destination);
|
|
1769
|
+
if (isStreamDestination(this.std.destination)) return this.context.config.destinations[this.std.destination];
|
|
1770
|
+
this.opened ??= this.context.files.open(this.std.destination);
|
|
1768
1771
|
return this.opened;
|
|
1769
1772
|
}
|
|
1770
1773
|
/**
|
|
@@ -1772,8 +1775,9 @@ var OutputSink = class {
|
|
|
1772
1775
|
* else `COLUMNS`. A file has none, and neither has a run without `wrap`.
|
|
1773
1776
|
*/
|
|
1774
1777
|
columns() {
|
|
1775
|
-
|
|
1776
|
-
|
|
1778
|
+
const { envColumns, wrap } = this.context.config;
|
|
1779
|
+
if (!wrap || !isStreamDestination(this.std.destination)) return;
|
|
1780
|
+
return terminalColumns(this.target) ?? envColumns;
|
|
1777
1781
|
}
|
|
1778
1782
|
write(chunk) {
|
|
1779
1783
|
if (this.std.destination === "none" || chunk === "") return;
|
|
@@ -1786,11 +1790,13 @@ var OutputSink = class {
|
|
|
1786
1790
|
this.target.write(chunk);
|
|
1787
1791
|
return;
|
|
1788
1792
|
}
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1793
|
+
const end = chunk.lastIndexOf("\n");
|
|
1794
|
+
if (end === -1) {
|
|
1795
|
+
this.buffered += chunk;
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
const complete = this.buffered + chunk.slice(0, end + 1);
|
|
1799
|
+
this.buffered = chunk.slice(end + 1);
|
|
1794
1800
|
this.target.write(this.applyPrefix(complete));
|
|
1795
1801
|
}
|
|
1796
1802
|
/** A line of runset's own, on a line of its own: after any unfinished one. */
|
|
@@ -1799,27 +1805,32 @@ var OutputSink = class {
|
|
|
1799
1805
|
}
|
|
1800
1806
|
/** Emits what is held back: a `grouped` stream, or a partial line. */
|
|
1801
1807
|
flush() {
|
|
1802
|
-
const held =
|
|
1803
|
-
this.pending = "";
|
|
1808
|
+
const held = this.buffered;
|
|
1804
1809
|
this.buffered = "";
|
|
1805
|
-
|
|
1810
|
+
if (held !== "") this.target.write(this.applyPrefix(held));
|
|
1806
1811
|
}
|
|
1807
1812
|
applyPrefix(chunk) {
|
|
1808
1813
|
const { prefix } = this;
|
|
1809
1814
|
if (prefix === "") return chunk;
|
|
1810
1815
|
const endsWithNewline = chunk.endsWith("\n");
|
|
1811
1816
|
const body = endsWithNewline ? chunk.slice(0, -1) : chunk;
|
|
1817
|
+
const end = endsWithNewline ? "\n" : "";
|
|
1812
1818
|
const columns = this.columns();
|
|
1813
|
-
|
|
1819
|
+
if (typeof prefix === "string" && columns === void 0) return prefix + body.replaceAll("\n", `\n${prefix}`) + end;
|
|
1820
|
+
return body.split("\n").map((line) => this.label(line, prefix, columns)).join("\n") + end;
|
|
1814
1821
|
}
|
|
1815
1822
|
/** One line, prefixed; wrapped first when it would overflow the terminal. */
|
|
1816
1823
|
label(line, prefix, columns) {
|
|
1817
1824
|
const text = typeof prefix === "string" ? prefix : prefix();
|
|
1818
1825
|
if (columns === void 0) return text + line;
|
|
1819
|
-
const
|
|
1820
|
-
|
|
1826
|
+
const cr = line.endsWith("\r") ? "\r" : "";
|
|
1827
|
+
const width = this.prefixWidth ?? visibleWidth(text);
|
|
1828
|
+
return wrapLine(line.slice(0, line.length - cr.length), columns - width, width).map((piece) => text + piece).join("\n") + cr;
|
|
1821
1829
|
}
|
|
1822
1830
|
};
|
|
1831
|
+
function isStreamDestination(destination) {
|
|
1832
|
+
return destination === "stdout" || destination === "stderr";
|
|
1833
|
+
}
|
|
1823
1834
|
//#endregion
|
|
1824
1835
|
//#region src/run/Process.ts
|
|
1825
1836
|
const USE_PROCESS_GROUPS = process.platform !== "win32";
|
|
@@ -1842,9 +1853,9 @@ var Process = class {
|
|
|
1842
1853
|
constructor(command, context) {
|
|
1843
1854
|
this.command = command;
|
|
1844
1855
|
this.context = context;
|
|
1845
|
-
const {
|
|
1846
|
-
const
|
|
1847
|
-
const sink = (stream) => new OutputSink(command[stream], makePrefix(command,
|
|
1856
|
+
const { color, formatLabel } = context.config;
|
|
1857
|
+
const colored = color !== "none";
|
|
1858
|
+
const sink = (stream) => new OutputSink(command[stream], makePrefix(command, colored, formatLabel, stream), context);
|
|
1848
1859
|
this.stdout = sink("stdout");
|
|
1849
1860
|
this.stderr = sink("stderr");
|
|
1850
1861
|
}
|
|
@@ -1862,12 +1873,13 @@ var Process = class {
|
|
|
1862
1873
|
let stopTheRun = false;
|
|
1863
1874
|
for (;;) {
|
|
1864
1875
|
this.started = true;
|
|
1865
|
-
|
|
1866
|
-
|
|
1876
|
+
const { color, showCommand, showExitCode } = this.context.config;
|
|
1877
|
+
if (showCommand) {
|
|
1878
|
+
const run = paint("run", ["blue"], color !== "none");
|
|
1867
1879
|
this.stdout.writeLine(`${run} ${this.command.command}`);
|
|
1868
1880
|
}
|
|
1869
1881
|
await this.spawn();
|
|
1870
|
-
if (
|
|
1882
|
+
if (showExitCode) this.stdout.writeLine(this.exitLine());
|
|
1871
1883
|
this.stdout.flush();
|
|
1872
1884
|
this.stderr.flush();
|
|
1873
1885
|
this.context.logger.debug(`< ${this.command.line} ${this.describeExit()}`);
|
|
@@ -1895,12 +1907,12 @@ var Process = class {
|
|
|
1895
1907
|
* in gray, so it can be found without its label.
|
|
1896
1908
|
*/
|
|
1897
1909
|
exitLine() {
|
|
1898
|
-
const
|
|
1910
|
+
const colored = this.context.config.color !== "none";
|
|
1899
1911
|
const duration = formatDuration(performance.now() - this.attemptStart);
|
|
1900
|
-
if (this.exitCode === 0 && this.signal === void 0 && !this.terminated) return `${
|
|
1912
|
+
if (this.exitCode === 0 && this.signal === void 0 && !this.terminated) return `${paint("✓", ["green"], colored)} ${duration}`;
|
|
1901
1913
|
const [status, tint] = this.terminated ? ["– stopped", "yellow"] : [`✗ ${this.shortExit()}`, "red"];
|
|
1902
|
-
const
|
|
1903
|
-
return
|
|
1914
|
+
const command = paint(this.command.command, ["gray"], colored);
|
|
1915
|
+
return `${paint(status, [tint], colored)} · ${duration} ${command}`;
|
|
1904
1916
|
}
|
|
1905
1917
|
/** `code N`, the signal's name, or `stopped`. */
|
|
1906
1918
|
shortExit() {
|
|
@@ -2017,6 +2029,7 @@ async function schedule(processes, stopped, jobs) {
|
|
|
2017
2029
|
//#region src/run/Run.ts
|
|
2018
2030
|
/** One run: the processes built from a plan, and what stops them. */
|
|
2019
2031
|
var Run = class Run {
|
|
2032
|
+
/** @internal */
|
|
2020
2033
|
config;
|
|
2021
2034
|
/** @internal */
|
|
2022
2035
|
processes;
|
|
@@ -2025,9 +2038,8 @@ var Run = class Run {
|
|
|
2025
2038
|
stopping;
|
|
2026
2039
|
constructor({ commands, config, packageInfo }) {
|
|
2027
2040
|
this.config = config;
|
|
2028
|
-
this.files = new FileRegistry(config.cwd);
|
|
2041
|
+
this.files = new FileRegistry(config.cwd, () => this.stopEverything({ reason: "policy" }));
|
|
2029
2042
|
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
2043
|
const context = {
|
|
2032
2044
|
config,
|
|
2033
2045
|
files: this.files,
|
|
@@ -2036,7 +2048,6 @@ var Run = class Run {
|
|
|
2036
2048
|
requestStop: () => this.stopEverything({ reason: "policy" })
|
|
2037
2049
|
};
|
|
2038
2050
|
this.processes = commands.map((command) => new Process(command, context));
|
|
2039
|
-
this.files.onFailure(() => this.stopEverything({ reason: "policy" }));
|
|
2040
2051
|
}
|
|
2041
2052
|
/** A library caller's config object, through the same pipeline as the CLI. */
|
|
2042
2053
|
static fromConfigJs(configJs) {
|
|
@@ -2058,6 +2069,7 @@ var Run = class Run {
|
|
|
2058
2069
|
}
|
|
2059
2070
|
/** Rejects with a {@link RunsetError} when the run did not succeed. */
|
|
2060
2071
|
async start() {
|
|
2072
|
+
for (const warning of this.config.warnings) this.logger.warn(`runset: ${warning}`);
|
|
2061
2073
|
if (this.config.dryRun) {
|
|
2062
2074
|
this.config.destinations.stdout.write(`${this.describe()}\n`);
|
|
2063
2075
|
return this;
|
|
@@ -2075,43 +2087,45 @@ var Run = class Run {
|
|
|
2075
2087
|
await this.files.closeAll();
|
|
2076
2088
|
}
|
|
2077
2089
|
const failed = this.failed();
|
|
2078
|
-
const exitCode =
|
|
2090
|
+
const exitCode = this.getExitCode();
|
|
2079
2091
|
const { stopping } = this;
|
|
2080
|
-
if (this.files.error) throw new RunsetError(this.files.error.message, exitCode
|
|
2092
|
+
if (this.files.error) throw new RunsetError(this.files.error.message, exitCode || 1);
|
|
2081
2093
|
if (stopping?.signal) throw new RunsetError(`the run was stopped by ${stopping.signal}`, signalExitCode(stopping.signal), "");
|
|
2082
2094
|
if (stopping?.reason === "terminate") throw new RunsetError("the run was terminated", signalExitCode("SIGTERM"));
|
|
2083
|
-
if (
|
|
2095
|
+
if (failed.length > 0) {
|
|
2096
|
+
const tally = this.tally(failed);
|
|
2097
|
+
throw new RunsetError(`${tally}: ${failed.map((process) => describeFailure(process)).join(", ")}`, exitCode, this.failureReport(tally, failed));
|
|
2098
|
+
}
|
|
2084
2099
|
return this;
|
|
2085
2100
|
}
|
|
2086
2101
|
terminate(options = {}) {
|
|
2087
2102
|
this.stopEverything({ reason: "terminate" }, options);
|
|
2088
2103
|
}
|
|
2089
2104
|
/** `1 of 5 commands failed, 2 stopped, 1 not started`. */
|
|
2090
|
-
tally() {
|
|
2105
|
+
tally(failed) {
|
|
2091
2106
|
const { processes } = this;
|
|
2092
2107
|
const total = processes.length;
|
|
2093
2108
|
const stopped = processes.filter((process) => process.started && process.terminated).length;
|
|
2094
2109
|
const notStarted = processes.filter((process) => !process.started).length;
|
|
2095
2110
|
return [
|
|
2096
|
-
`${
|
|
2111
|
+
`${failed.length} of ${total} command${total === 1 ? "" : "s"} failed`,
|
|
2097
2112
|
stopped > 0 && `${stopped} stopped`,
|
|
2098
2113
|
notStarted > 0 && `${notStarted} not started`
|
|
2099
2114
|
].filter(Boolean).join(", ");
|
|
2100
2115
|
}
|
|
2101
2116
|
/** The tally, then a row per failure, labelled as its output was. */
|
|
2102
|
-
failureReport() {
|
|
2103
|
-
const
|
|
2104
|
-
const red = (text) =>
|
|
2105
|
-
|
|
2117
|
+
failureReport(tally, failed) {
|
|
2118
|
+
const colored = this.config.color !== "none";
|
|
2119
|
+
const red = (text) => paint(text, ["red"], colored);
|
|
2120
|
+
return [tally, ...failed.map((process) => {
|
|
2106
2121
|
const { command } = process;
|
|
2107
2122
|
const label = command.label.trim();
|
|
2108
|
-
const prefix =
|
|
2123
|
+
const prefix = label === "" ? "" : defaultLabel({
|
|
2109
2124
|
...command,
|
|
2110
2125
|
label
|
|
2111
|
-
},
|
|
2126
|
+
}, colored);
|
|
2112
2127
|
return ` ${red("✖")} ${prefix}${command.command} ${red(process.describeExit())}`;
|
|
2113
|
-
});
|
|
2114
|
-
return [this.tally(), ...rows].join("\n");
|
|
2128
|
+
})].join("\n");
|
|
2115
2129
|
}
|
|
2116
2130
|
failed() {
|
|
2117
2131
|
return this.processes.filter((process) => process.isFailed());
|
|
@@ -2250,7 +2264,17 @@ function version() {
|
|
|
2250
2264
|
async function runset(first, second) {
|
|
2251
2265
|
return Run.fromConfigJs(toConfigJs(first, second)).start();
|
|
2252
2266
|
}
|
|
2253
|
-
if (import.meta.main
|
|
2267
|
+
if (import.meta.main || isEntryPoint()) main(process.argv.slice(2)).then((code) => {
|
|
2268
|
+
process.exitCode = code;
|
|
2269
|
+
}).catch((error) => {
|
|
2270
|
+
console.error(error);
|
|
2271
|
+
process.exitCode = 1;
|
|
2272
|
+
}).finally(() => {
|
|
2273
|
+
setTimeout(() => {
|
|
2274
|
+
console.error("runset: forced exit, a handle kept the process alive");
|
|
2275
|
+
process.exit();
|
|
2276
|
+
}, 500).unref();
|
|
2277
|
+
});
|
|
2254
2278
|
/** A lone object carrying `commands` is a config; anything else is commands. */
|
|
2255
2279
|
function toConfigJs(first, second) {
|
|
2256
2280
|
if (second === void 0 && isConfigObject(first)) return first;
|
|
@@ -2261,7 +2285,20 @@ function toConfigJs(first, second) {
|
|
|
2261
2285
|
};
|
|
2262
2286
|
}
|
|
2263
2287
|
function isConfigObject(value) {
|
|
2264
|
-
return
|
|
2288
|
+
return isPlainObject(value) && Array.isArray(value.commands);
|
|
2289
|
+
}
|
|
2290
|
+
/**
|
|
2291
|
+
* Node 24.2 reports `import.meta.main` as false for a `.ts` entry point, which
|
|
2292
|
+
* is how the tests run this file; 24.3 fixed it. The module URL is a real
|
|
2293
|
+
* path, so the argument is resolved to one too — macOS's temp dir is a link.
|
|
2294
|
+
*/
|
|
2295
|
+
function isEntryPoint() {
|
|
2296
|
+
const entry = process.argv[1];
|
|
2297
|
+
try {
|
|
2298
|
+
return entry !== void 0 && fs.realpathSync(entry) === fileURLToPath(import.meta.url);
|
|
2299
|
+
} catch {
|
|
2300
|
+
return false;
|
|
2301
|
+
}
|
|
2265
2302
|
}
|
|
2266
2303
|
//#endregion
|
|
2267
|
-
export { CliError, ConfigError,
|
|
2304
|
+
export { CliError, ConfigError, NormalizeError, Run, RunsetError, runset as default, runset };
|