@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.
@@ -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 { bold, gray, yellow } from "colorette";
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: ${yellow("hr")} <taskFile> [task]`;
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(name);
52
+ const formattedName = styleText("bold", name);
53
53
  if (isBrandedTask(value)) {
54
54
  if (value.kind === "strictTask") {
55
- const properties = getSchemaProperties(value.schema) || gray("not available");
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
- await taskFunction({
23
+ const optionsWithEnvironment = {
24
24
  ...options,
25
25
  env: process.env
26
- }, taskUtilities);
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`]);
@@ -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(`Environment variable '${String(error.path[1])}'`)}: ${messageWithoutProperty}`;
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(optionText)}: unknown option`;
12
- return `${red(optionText)}: ${messageWithoutProperty}`;
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(`[${key}]`);
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, resolve } from "node:path";
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 resolve(context.taskFile.dir, context.taskFile.name);
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": "4.0.0",
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": "^3.3.4",
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": true
73
+ "quiet": false
67
74
  },
68
75
  "scripts": {
69
76
  "build": "vp pack",