@haltcase/run 4.0.0 → 5.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/handler.mjs +5 -5
- package/dist/cli/spinner.mjs +128 -0
- package/dist/tasks/executeTask.mjs +51 -2
- package/dist/tasks/task.mjs +4 -4
- package/dist/util/getSchemaProperties.mjs +2 -2
- package/dist/util/loadTaskFile.mjs +2 -1
- package/dist/util/resolveTaskFile.mjs +2 -2
- package/package.json +12 -5
package/dist/cli/handler.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { isBrandedTask } from "../tasks/guards.mjs";
|
|
2
2
|
import { getSchemaProperties } from "../util/getSchemaProperties.mjs";
|
|
3
3
|
import { extensions } from "../util/resolveTaskFile.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { Spinner } from "./spinner.mjs";
|
|
5
|
+
import { styleText } from "node:util";
|
|
5
6
|
import { readdirSync } from "node:fs";
|
|
6
7
|
import { extname } from "node:path";
|
|
7
|
-
import { Spinner } from "@favware/colorette-spinner";
|
|
8
8
|
import cliui from "cliui";
|
|
9
9
|
//#region src/cli/handler.ts
|
|
10
10
|
const failWith = (spinner, message) => {
|
|
@@ -17,7 +17,7 @@ const failWith = (spinner, message) => {
|
|
|
17
17
|
const write = (message) => {
|
|
18
18
|
process.stdout.write(`${message}\n`);
|
|
19
19
|
};
|
|
20
|
-
const usage = `Usage: ${
|
|
20
|
+
const usage = `Usage: ${styleText("yellow", "hr")} <taskFile> [task]`;
|
|
21
21
|
const commandHandler = (_context) => {
|
|
22
22
|
return usage;
|
|
23
23
|
};
|
|
@@ -49,10 +49,10 @@ const taskListHandler = (context) => {
|
|
|
49
49
|
const nameColumnWidth = Object.keys(config).reduce((previous, current) => Math.max(previous, current.length), 1) + 2 + taskListPaddingX * 2;
|
|
50
50
|
for (const [name, value] of Object.entries(config)) {
|
|
51
51
|
if (!value) continue;
|
|
52
|
-
const formattedName = bold
|
|
52
|
+
const formattedName = styleText("bold", name);
|
|
53
53
|
if (isBrandedTask(value)) {
|
|
54
54
|
if (value.kind === "strictTask") {
|
|
55
|
-
const properties = getSchemaProperties(value.schema) ||
|
|
55
|
+
const properties = getSchemaProperties(value.schema) || styleText("gray", "not available");
|
|
56
56
|
ui.div({
|
|
57
57
|
text: formattedName,
|
|
58
58
|
width: nameColumnWidth,
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { styleText } from "node:util";
|
|
2
|
+
import tty from "node:tty";
|
|
3
|
+
//#region src/cli/spinner.ts
|
|
4
|
+
/**
|
|
5
|
+
* Based on `nanospinner` and `@favware/colorette-spinner` but using
|
|
6
|
+
*
|
|
7
|
+
* @see https://github.com/usmanyunusov/nanospinner (ISC © 2021 Usman Yunusov)
|
|
8
|
+
* @see https://github.com/favware/colorette-spinner (MIT © 2022 Favware)
|
|
9
|
+
*/
|
|
10
|
+
var Spinner = class {
|
|
11
|
+
#isCI = process.env.CI || process.env.WT_SESSION || process.env.ConEmuTask === "{cmd::Cmder}" || process.env.TERM_PROGRAM === "vscode" || process.env.TERM === "xterm-256color" || process.env.TERM === "alacritty";
|
|
12
|
+
#isTTY = tty.isatty(1) && process.env.TERM !== "dumb" && !("CI" in process.env);
|
|
13
|
+
#supportUnicode = process.platform === "win32" ? this.#isCI : process.env.TERM !== "linux";
|
|
14
|
+
#symbols = {
|
|
15
|
+
frames: this.#isTTY ? this.#supportUnicode ? [
|
|
16
|
+
"⠋",
|
|
17
|
+
"⠙",
|
|
18
|
+
"⠹",
|
|
19
|
+
"⠸",
|
|
20
|
+
"⠼",
|
|
21
|
+
"⠴",
|
|
22
|
+
"⠦",
|
|
23
|
+
"⠧",
|
|
24
|
+
"⠇",
|
|
25
|
+
"⠏"
|
|
26
|
+
] : [
|
|
27
|
+
"-",
|
|
28
|
+
"\\",
|
|
29
|
+
"|",
|
|
30
|
+
"/"
|
|
31
|
+
] : ["-"],
|
|
32
|
+
tick: this.#supportUnicode ? "✔" : "√",
|
|
33
|
+
cross: this.#supportUnicode ? "✖" : "×"
|
|
34
|
+
};
|
|
35
|
+
#text = "";
|
|
36
|
+
#current = 0;
|
|
37
|
+
#interval = 50;
|
|
38
|
+
#stream = process.stderr;
|
|
39
|
+
#frames = this.#symbols.frames;
|
|
40
|
+
#color = "greenBright";
|
|
41
|
+
#lines = 0;
|
|
42
|
+
#timer;
|
|
43
|
+
constructor(text, options) {
|
|
44
|
+
this.#text = text ?? this.#text;
|
|
45
|
+
this.#interval = options?.interval ?? this.#interval;
|
|
46
|
+
this.#stream = options?.stream ?? this.#stream;
|
|
47
|
+
if (options?.frames?.length) this.#frames = options.frames;
|
|
48
|
+
this.#color = options?.color ?? this.#color;
|
|
49
|
+
}
|
|
50
|
+
clear() {
|
|
51
|
+
this.write("\x1B[1G");
|
|
52
|
+
for (let index = 0; index < this.#lines; index++) {
|
|
53
|
+
if (index > 0) this.write("\x1B[1A");
|
|
54
|
+
this.write("\x1B[2K\x1B[1G");
|
|
55
|
+
}
|
|
56
|
+
this.#lines = 0;
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
error(...[options = {}]) {
|
|
60
|
+
const mark = styleText("red", this.#symbols.cross);
|
|
61
|
+
return this.stop({
|
|
62
|
+
mark,
|
|
63
|
+
...options
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
reset() {
|
|
67
|
+
this.#current = 0;
|
|
68
|
+
this.#lines = 0;
|
|
69
|
+
if (this.#timer) clearTimeout(this.#timer);
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
spin() {
|
|
73
|
+
this.render();
|
|
74
|
+
this.#current = ++this.#current % this.#frames.length;
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
start(...[options = {}]) {
|
|
78
|
+
if (this.#timer) this.reset();
|
|
79
|
+
return this.update({
|
|
80
|
+
text: options.text ?? this.#text,
|
|
81
|
+
color: options.color ?? this.#color
|
|
82
|
+
}).loop();
|
|
83
|
+
}
|
|
84
|
+
stop(...[options = {}]) {
|
|
85
|
+
if (this.#timer) clearTimeout(this.#timer);
|
|
86
|
+
const mark = styleText(options.color || this.#color, this.#frames[this.#current] || "");
|
|
87
|
+
const optionsMark = options.mark && options.color ? styleText(options.color, options.mark) : options.mark;
|
|
88
|
+
this.write(`${optionsMark || mark} ${options.text || this.#text}\n`, true);
|
|
89
|
+
return this.#isTTY ? this.write(`\u001B[?25h`) : this;
|
|
90
|
+
}
|
|
91
|
+
success(...[options = {}]) {
|
|
92
|
+
const mark = styleText("green", this.#symbols.tick);
|
|
93
|
+
return this.stop({
|
|
94
|
+
mark,
|
|
95
|
+
...options
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
update(...[options = {}]) {
|
|
99
|
+
this.#text = options.text || this.#text;
|
|
100
|
+
this.#interval = options.interval ?? this.#interval;
|
|
101
|
+
this.#stream = options.stream ?? this.#stream;
|
|
102
|
+
if (options.frames?.length) this.#frames = options.frames;
|
|
103
|
+
this.#color = options.color ?? this.#color;
|
|
104
|
+
if (this.#frames.length - 1 < this.#current) this.#current = 0;
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
loop() {
|
|
108
|
+
if (this.#isTTY) this.#timer = setTimeout(() => this.loop(), this.#interval);
|
|
109
|
+
return this.spin();
|
|
110
|
+
}
|
|
111
|
+
write(value, clear = false) {
|
|
112
|
+
if (clear && this.#isTTY) this.clear();
|
|
113
|
+
this.#stream.write(value);
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
render() {
|
|
117
|
+
let value = `${styleText(this.#color, this.#frames[this.#current] || "")} ${this.#text}`;
|
|
118
|
+
if (this.#isTTY) this.write(`\u001B[?25l`);
|
|
119
|
+
else value += "\n";
|
|
120
|
+
this.write(value, true);
|
|
121
|
+
if (this.#isTTY) this.#lines = this.getLines(value, this.#stream.columns);
|
|
122
|
+
}
|
|
123
|
+
getLines(value = "", width = 80) {
|
|
124
|
+
return value.replaceAll(/\u001B[^m]*m/g, "").split("\n").reduce((column, line) => column + Math.max(1, Math.ceil(line.length / width)), 0);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
//#endregion
|
|
128
|
+
export { Spinner };
|
|
@@ -20,10 +20,59 @@ const executeTask = async (context, options) => {
|
|
|
20
20
|
context.handler.failWith([`Task file '${context.taskFile.name}' does not export '${context.taskName}'`, `Resolved to: ${context.taskFile.path}`]);
|
|
21
21
|
}
|
|
22
22
|
try {
|
|
23
|
-
|
|
23
|
+
const optionsWithEnvironment = {
|
|
24
24
|
...options,
|
|
25
25
|
env: process.env
|
|
26
|
-
}
|
|
26
|
+
};
|
|
27
|
+
const isSpinnerEnabled = !context.config.quiet;
|
|
28
|
+
const pinnedSpinnerRenderInterval = 80;
|
|
29
|
+
if (isSpinnerEnabled) {
|
|
30
|
+
context.handler.spinner.start({ text: `Running ${specifier}` });
|
|
31
|
+
context.handler.spinner.update({ interval: pinnedSpinnerRenderInterval });
|
|
32
|
+
}
|
|
33
|
+
const originalStdoutWrite = Reflect.get(process.stdout, "write");
|
|
34
|
+
const originalStderrWrite = Reflect.get(process.stderr, "write");
|
|
35
|
+
const passthroughStdoutWrite = originalStdoutWrite.bind(process.stdout);
|
|
36
|
+
const passthroughStderrWrite = originalStderrWrite.bind(process.stderr);
|
|
37
|
+
let isSpinnerWrite = false;
|
|
38
|
+
let lastForcedRender = 0;
|
|
39
|
+
const clearPinnedSpinner = () => {
|
|
40
|
+
if (!isSpinnerEnabled) return;
|
|
41
|
+
isSpinnerWrite = true;
|
|
42
|
+
context.handler.spinner.clear();
|
|
43
|
+
isSpinnerWrite = false;
|
|
44
|
+
};
|
|
45
|
+
const renderPinnedSpinner = () => {
|
|
46
|
+
if (!isSpinnerEnabled) return;
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
if (now - lastForcedRender < pinnedSpinnerRenderInterval) return;
|
|
49
|
+
lastForcedRender = now;
|
|
50
|
+
isSpinnerWrite = true;
|
|
51
|
+
context.handler.spinner.spin();
|
|
52
|
+
isSpinnerWrite = false;
|
|
53
|
+
};
|
|
54
|
+
const interceptWrite = (passthroughWrite) => {
|
|
55
|
+
return ((chunk, encoding, callback) => {
|
|
56
|
+
const writeArgs = [
|
|
57
|
+
chunk,
|
|
58
|
+
encoding,
|
|
59
|
+
callback
|
|
60
|
+
].filter((value) => value != null);
|
|
61
|
+
if (!isSpinnerEnabled || isSpinnerWrite) return Reflect.apply(passthroughWrite, process.stdout, writeArgs);
|
|
62
|
+
clearPinnedSpinner();
|
|
63
|
+
const didWrite = Reflect.apply(passthroughWrite, process.stdout, writeArgs);
|
|
64
|
+
renderPinnedSpinner();
|
|
65
|
+
return didWrite;
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
process.stdout.write = interceptWrite(passthroughStdoutWrite);
|
|
69
|
+
process.stderr.write = interceptWrite(passthroughStderrWrite);
|
|
70
|
+
try {
|
|
71
|
+
await taskFunction(optionsWithEnvironment, taskUtilities);
|
|
72
|
+
} finally {
|
|
73
|
+
process.stdout.write = originalStdoutWrite;
|
|
74
|
+
process.stderr.write = originalStderrWrite;
|
|
75
|
+
}
|
|
27
76
|
} catch (error) {
|
|
28
77
|
const message = error instanceof Error ? error.message : String(error);
|
|
29
78
|
if (message.includes("is not a function")) context.handler.failWith([`Failed to execute ${specifier}`, `Exported value '${context.taskName}' is not a function`]);
|
package/dist/tasks/task.mjs
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
+
import { styleText } from "node:util";
|
|
1
2
|
import { type } from "arktype";
|
|
2
|
-
import { red } from "colorette";
|
|
3
3
|
//#region src/tasks/task.ts
|
|
4
4
|
const formatValidationIssues = (errors) => errors.map((error) => {
|
|
5
5
|
const propertyName = String(error.path[0]);
|
|
6
6
|
const dottedPath = error.path.join(".");
|
|
7
7
|
if (!propertyName) return error.message;
|
|
8
8
|
const messageWithoutProperty = error.message.startsWith(dottedPath) ? error.message.slice(dottedPath.length + 1) : error.message;
|
|
9
|
-
if (propertyName === "env") return `${red
|
|
9
|
+
if (propertyName === "env") return `${styleText("red", `Environment variable '${String(error.path[1])}'`)}: ${messageWithoutProperty}`;
|
|
10
10
|
const optionText = propertyName === "_" ? "Positionals" : `--${propertyName}`;
|
|
11
|
-
if (error.code === "predicate" && error.expected === "removed") return `${red
|
|
12
|
-
return `${red
|
|
11
|
+
if (error.code === "predicate" && error.expected === "removed") return `${styleText("red", optionText)}: unknown option`;
|
|
12
|
+
return `${styleText("red", optionText)}: ${messageWithoutProperty}`;
|
|
13
13
|
}).join("\n");
|
|
14
14
|
const task = (fn) => {
|
|
15
15
|
fn.kind = "task";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { reservedNames } from "../cli/parseOptions.mjs";
|
|
2
|
+
import { styleText } from "node:util";
|
|
2
3
|
import { type } from "arktype";
|
|
3
|
-
import { dim } from "colorette";
|
|
4
4
|
//#region src/util/getSchemaProperties.ts
|
|
5
5
|
const propertyEntry = type({
|
|
6
6
|
key: "string",
|
|
@@ -26,7 +26,7 @@ const getSchemaProperties = (schema) => {
|
|
|
26
26
|
const properties = [...requiredProperties, ...optionalProperties];
|
|
27
27
|
if (properties.length === 0) return "";
|
|
28
28
|
return `{ ${properties.filter(({ key }) => !reservedNames.has(key)).map(({ key, isOptional }) => {
|
|
29
|
-
if (isOptional) return dim
|
|
29
|
+
if (isOptional) return styleText("dim", `[${key}]`);
|
|
30
30
|
return key;
|
|
31
31
|
}).join(", ")} }`;
|
|
32
32
|
};
|
|
@@ -9,7 +9,8 @@ const loadTaskFile = async (context) => {
|
|
|
9
9
|
configFile: context.taskFile.base,
|
|
10
10
|
dotenv: false,
|
|
11
11
|
rcFile: false,
|
|
12
|
-
packageJson: false
|
|
12
|
+
packageJson: false,
|
|
13
|
+
jitiOptions: { tsconfigPaths: true }
|
|
13
14
|
});
|
|
14
15
|
if (!collection.configFile || collection.layers?.length === 0) return {
|
|
15
16
|
ok: false,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { join
|
|
2
|
+
import { join } from "node:path";
|
|
3
3
|
import { SUPPORTED_EXTENSIONS } from "c12";
|
|
4
4
|
//#region src/util/resolveTaskFile.ts
|
|
5
5
|
const extensions = SUPPORTED_EXTENSIONS.filter((extension) => extension.endsWith("js") || extension.endsWith("ts"));
|
|
6
6
|
const resolveTaskFile = (context) => {
|
|
7
|
-
if (context.taskFile.ext) return
|
|
7
|
+
if (context.taskFile.ext) return context.taskFile.path;
|
|
8
8
|
const foundConfigs = extensions.map((it) => join(context.taskFile.dir, `${context.taskFile.name}${it}`)).filter((path) => existsSync(path));
|
|
9
9
|
if (foundConfigs.length === 1) return foundConfigs[0];
|
|
10
10
|
return foundConfigs;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haltcase/run",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0-beta.1",
|
|
4
4
|
"description": "Flexible, function-based task runner where command line options are props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"arktype",
|
|
@@ -38,11 +38,9 @@
|
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@favware/colorette-spinner": "^1.0.1",
|
|
42
41
|
"arktype": "^2.2.0",
|
|
43
|
-
"c12": "^
|
|
42
|
+
"c12": "^4.0.0-beta.5",
|
|
44
43
|
"cliui": "^9.0.1",
|
|
45
|
-
"colorette": "^2.0.20",
|
|
46
44
|
"execa": "^9.6.1"
|
|
47
45
|
},
|
|
48
46
|
"devDependencies": {
|
|
@@ -51,6 +49,7 @@
|
|
|
51
49
|
"@types/node": "^24.13.1",
|
|
52
50
|
"@typescript/native-preview": "7.0.0-dev.20260608.1",
|
|
53
51
|
"eslint": "^9.39.4",
|
|
52
|
+
"jiti": "^2.7.0",
|
|
54
53
|
"oxlint-tsgolint": "^0.23.0",
|
|
55
54
|
"tsx": "^4.22.4",
|
|
56
55
|
"typescript": "^6.0.3",
|
|
@@ -58,12 +57,20 @@
|
|
|
58
57
|
"vite-plus": "0.1.24",
|
|
59
58
|
"vitest": "npm:@voidzero-dev/vite-plus-test@0.1.24"
|
|
60
59
|
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"jiti": "^2.7.0"
|
|
62
|
+
},
|
|
63
|
+
"peerDependenciesMeta": {
|
|
64
|
+
"jiti": {
|
|
65
|
+
"optional": true
|
|
66
|
+
}
|
|
67
|
+
},
|
|
61
68
|
"engines": {
|
|
62
69
|
"node": ">=22"
|
|
63
70
|
},
|
|
64
71
|
"haltcase.run": {
|
|
65
72
|
"taskDirectory": "./scripts",
|
|
66
|
-
"quiet":
|
|
73
|
+
"quiet": false
|
|
67
74
|
},
|
|
68
75
|
"scripts": {
|
|
69
76
|
"build": "vp pack",
|