@haltcase/run 3.0.1 → 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.
Files changed (51) hide show
  1. package/dist/cli/bin.d.mts +1 -0
  2. package/dist/cli/bin.mjs +7 -0
  3. package/dist/cli/handler.mjs +110 -0
  4. package/dist/cli/main.mjs +56 -0
  5. package/dist/cli/parseOptions.d.mts +7 -0
  6. package/dist/cli/parseOptions.mjs +36 -0
  7. package/dist/cli/spinner.mjs +128 -0
  8. package/dist/config.d.mts +18 -0
  9. package/dist/config.mjs +19 -0
  10. package/dist/index.d.mts +5 -0
  11. package/dist/index.mjs +2 -0
  12. package/dist/tasks/executeTask.mjs +83 -0
  13. package/dist/tasks/guards.mjs +4 -0
  14. package/dist/tasks/task.d.mts +10 -0
  15. package/dist/tasks/task.mjs +40 -0
  16. package/dist/tasks/types.d.mts +60 -0
  17. package/dist/util/getSchemaProperties.mjs +34 -0
  18. package/dist/util/loadTaskFile.mjs +31 -0
  19. package/dist/util/resolveTaskFile.mjs +13 -0
  20. package/package.json +51 -42
  21. package/dist/cli/bin.d.ts +0 -2
  22. package/dist/cli/bin.js +0 -7
  23. package/dist/cli/handler.d.ts +0 -28
  24. package/dist/cli/handler.js +0 -105
  25. package/dist/cli/main.d.ts +0 -21
  26. package/dist/cli/main.js +0 -64
  27. package/dist/cli/parseOptions.d.ts +0 -13
  28. package/dist/cli/parseOptions.js +0 -48
  29. package/dist/config.d.ts +0 -16
  30. package/dist/config.js +0 -16
  31. package/dist/index.d.ts +0 -4
  32. package/dist/index.js +0 -1
  33. package/dist/tasks/executeTask.d.ts +0 -12
  34. package/dist/tasks/executeTask.js +0 -45
  35. package/dist/tasks/guards.d.ts +0 -3
  36. package/dist/tasks/guards.js +0 -2
  37. package/dist/tasks/task.d.ts +0 -14
  38. package/dist/tasks/task.js +0 -55
  39. package/dist/tasks/types.d.ts +0 -58
  40. package/dist/tasks/types.js +0 -1
  41. package/dist/tsconfig.build.tsbuildinfo +0 -1
  42. package/dist/util/getSchemaProperties.d.ts +0 -2
  43. package/dist/util/getSchemaProperties.js +0 -40
  44. package/dist/util/loadTaskFile.d.ts +0 -8
  45. package/dist/util/loadTaskFile.js +0 -30
  46. package/dist/util/resolveTaskFile.d.ts +0 -3
  47. package/dist/util/resolveTaskFile.js +0 -17
  48. package/dist/util/result.d.ts +0 -9
  49. package/dist/util/result.js +0 -1
  50. /package/{license → LICENSE} +0 -0
  51. /package/{readme.md → README.md} +0 -0
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { createHandler } from "./handler.mjs";
3
+ import { main } from "./main.mjs";
4
+ //#region src/cli/bin.ts
5
+ main({ handler: createHandler() });
6
+ //#endregion
7
+ export {};
@@ -0,0 +1,110 @@
1
+ import { isBrandedTask } from "../tasks/guards.mjs";
2
+ import { getSchemaProperties } from "../util/getSchemaProperties.mjs";
3
+ import { extensions } from "../util/resolveTaskFile.mjs";
4
+ import { Spinner } from "./spinner.mjs";
5
+ import { styleText } from "node:util";
6
+ import { readdirSync } from "node:fs";
7
+ import { extname } from "node:path";
8
+ import cliui from "cliui";
9
+ //#region src/cli/handler.ts
10
+ const failWith = (spinner, message) => {
11
+ let text;
12
+ if (Array.isArray(message)) text = message.map(String).join("\n");
13
+ if (text == null || typeof text !== "string") text = String(message);
14
+ spinner.error({ text });
15
+ process.exit(1);
16
+ };
17
+ const write = (message) => {
18
+ process.stdout.write(`${message}\n`);
19
+ };
20
+ const usage = `Usage: ${styleText("yellow", "hr")} <taskFile> [task]`;
21
+ const commandHandler = (_context) => {
22
+ return usage;
23
+ };
24
+ const taskFileListHandler = (context) => {
25
+ return `${[
26
+ usage,
27
+ "Available task files:",
28
+ readdirSync(context.config.taskDirectory).filter((file) => extensions.includes(extname(file))).map((it) => ` ${it}`).join("\n")
29
+ ].join("\n\n")}\n`;
30
+ };
31
+ const taskListHandler = (context) => {
32
+ const ui = cliui({
33
+ width: Math.max(0, process.stdout.columns - 4),
34
+ wrap: true
35
+ });
36
+ const { config } = context.taskFile.data;
37
+ const baseUsage = usage.replace("<taskFile>", context.taskFile.name);
38
+ ui.div(`${baseUsage} <parameter> [...options]`);
39
+ ui.div({
40
+ text: "Available tasks:",
41
+ padding: [
42
+ 1,
43
+ 0,
44
+ 1,
45
+ 0
46
+ ]
47
+ });
48
+ const taskListPaddingX = 2;
49
+ const nameColumnWidth = Object.keys(config).reduce((previous, current) => Math.max(previous, current.length), 1) + 2 + taskListPaddingX * 2;
50
+ for (const [name, value] of Object.entries(config)) {
51
+ if (!value) continue;
52
+ const formattedName = styleText("bold", name);
53
+ if (isBrandedTask(value)) {
54
+ if (value.kind === "strictTask") {
55
+ const properties = getSchemaProperties(value.schema) || styleText("gray", "not available");
56
+ ui.div({
57
+ text: formattedName,
58
+ width: nameColumnWidth,
59
+ padding: [
60
+ 0,
61
+ taskListPaddingX,
62
+ 0,
63
+ taskListPaddingX
64
+ ]
65
+ }, properties);
66
+ continue;
67
+ }
68
+ ui.div({
69
+ text: formattedName,
70
+ width: nameColumnWidth,
71
+ padding: [
72
+ 0,
73
+ taskListPaddingX,
74
+ 0,
75
+ taskListPaddingX
76
+ ]
77
+ });
78
+ continue;
79
+ }
80
+ ui.div({
81
+ text: formattedName,
82
+ width: nameColumnWidth,
83
+ padding: [
84
+ 0,
85
+ taskListPaddingX,
86
+ 0,
87
+ taskListPaddingX
88
+ ]
89
+ });
90
+ }
91
+ ui.div("");
92
+ return ui.toString();
93
+ };
94
+ const help = (context) => {
95
+ if ("command" in context) return commandHandler(context);
96
+ if ("taskFileList" in context) return taskFileListHandler(context);
97
+ if ("taskList" in context) return taskListHandler(context);
98
+ return "";
99
+ };
100
+ const createHandler = () => {
101
+ const spinner = new Spinner();
102
+ return {
103
+ spinner,
104
+ help,
105
+ write,
106
+ failWith: (message) => failWith(spinner, message)
107
+ };
108
+ };
109
+ //#endregion
110
+ export { createHandler };
@@ -0,0 +1,56 @@
1
+ import { parseOptions } from "./parseOptions.mjs";
2
+ import { resolveTaskFile } from "../util/resolveTaskFile.mjs";
3
+ import { getAppConfig } from "../config.mjs";
4
+ import { executeTask } from "../tasks/executeTask.mjs";
5
+ import { loadTaskFile } from "../util/loadTaskFile.mjs";
6
+ import { parse, resolve } from "node:path";
7
+ //#region src/cli/main.ts
8
+ const main = async (props) => {
9
+ const { config } = await getAppConfig();
10
+ const inputFileName = process.argv[2];
11
+ const taskName = process.argv[3] ?? "";
12
+ if (!inputFileName) {
13
+ props.handler.write(props.handler.help({
14
+ taskFileList: true,
15
+ config
16
+ }));
17
+ props.handler.failWith("Task file name is required");
18
+ }
19
+ const fullFilePath = resolve(config.taskDirectory, inputFileName);
20
+ const context = {
21
+ ...props,
22
+ config,
23
+ taskFile: {
24
+ ...parse(fullFilePath),
25
+ path: fullFilePath
26
+ },
27
+ taskName
28
+ };
29
+ const resolutions = resolveTaskFile(context);
30
+ if (Array.isArray(resolutions)) props.handler.failWith([
31
+ `Found multiple task files with the name '${context.taskFile.name}'`,
32
+ `Rename the ambiguous files or specify an extension and try again`,
33
+ resolutions.map((it) => ` ${it}`).join("\n")
34
+ ]);
35
+ context.taskFile = {
36
+ ...parse(resolutions),
37
+ path: resolutions
38
+ };
39
+ const taskFileResult = await loadTaskFile(context);
40
+ if (!taskFileResult.ok) props.handler.failWith(taskFileResult.error);
41
+ const contextWithData = {
42
+ ...context,
43
+ taskFile: {
44
+ ...context.taskFile,
45
+ data: taskFileResult.value
46
+ }
47
+ };
48
+ try {
49
+ await executeTask(contextWithData, parseOptions(process.argv.slice(4)));
50
+ if (!config.quiet) props.handler.spinner.success({ text: "Success" });
51
+ } catch (error) {
52
+ props.handler.failWith([`Failed to execute task`, String(error)]);
53
+ }
54
+ };
55
+ //#endregion
56
+ export { main };
@@ -0,0 +1,7 @@
1
+ //#region src/cli/parseOptions.d.ts
2
+ interface ParsedOptions {
3
+ _: string[];
4
+ [key: string]: unknown;
5
+ }
6
+ //#endregion
7
+ export { ParsedOptions };
@@ -0,0 +1,36 @@
1
+ import { parseArgs } from "node:util";
2
+ //#region src/cli/parseOptions.ts
3
+ const reservedNames = new Set(["_", "env"]);
4
+ /**
5
+ * Wrapper around Node's {@link parseArgs} that treats options as strings
6
+ * by default, instead of boolean flags.
7
+ *
8
+ * @param args - argv-like string
9
+ * @returns
10
+ */
11
+ const parseOptions = (args) => {
12
+ const { tokens } = parseArgs({
13
+ args,
14
+ allowPositionals: true,
15
+ strict: false,
16
+ tokens: true
17
+ });
18
+ const options = { _: [] };
19
+ for (let index = 0; index < tokens.length; index++) {
20
+ const token = tokens[index];
21
+ if (token.kind === "option-terminator") continue;
22
+ if (token.kind === "option") {
23
+ if (reservedNames.has(token.name)) throw new Error(`Reserved name '${token.name}' cannot be used as option`);
24
+ if (index + 1 < tokens.length) {
25
+ const next = tokens[index + 1];
26
+ if (next.kind !== "positional") throw new Error(`Expected value for option ${token.rawName}`);
27
+ options[token.name] = next.value;
28
+ index++;
29
+ } else throw new Error(`Expected option ${token.rawName} to be followed by a value`);
30
+ }
31
+ if (token.kind === "positional") options._.push(token.value);
32
+ }
33
+ return options;
34
+ };
35
+ //#endregion
36
+ export { parseOptions, reservedNames };
@@ -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 };
@@ -0,0 +1,18 @@
1
+ //#region src/config.d.ts
2
+ interface AppConfig {
3
+ /**
4
+ * Directory containing task files. Defaults to the `scripts` folder within
5
+ * the current working directory.
6
+ *
7
+ * @default `<cwd>/scripts`
8
+ */
9
+ taskDirectory: string;
10
+ /**
11
+ * Print no output unless errors occur.
12
+ *
13
+ * @default false
14
+ */
15
+ quiet: boolean;
16
+ }
17
+ //#endregion
18
+ export { AppConfig };
@@ -0,0 +1,19 @@
1
+ import { join } from "node:path";
2
+ import { loadConfig } from "c12";
3
+ import { cwd } from "node:process";
4
+ //#region src/config.ts
5
+ const getAppConfig = async () => {
6
+ const configName = "haltcase.run";
7
+ return loadConfig({
8
+ name: configName,
9
+ configFile: configName,
10
+ packageJson: configName,
11
+ rcFile: configName,
12
+ defaults: {
13
+ taskDirectory: join(cwd(), "scripts"),
14
+ quiet: false
15
+ }
16
+ });
17
+ };
18
+ //#endregion
19
+ export { getAppConfig };
@@ -0,0 +1,5 @@
1
+ import { ParsedOptions } from "./cli/parseOptions.mjs";
2
+ import { AppConfig } from "./config.mjs";
3
+ import { Task } from "./tasks/types.mjs";
4
+ import { task } from "./tasks/task.mjs";
5
+ export { type AppConfig as HaltcaseRunConfig, type ParsedOptions, type Task, task };
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { task } from "./tasks/task.mjs";
2
+ export { task };
@@ -0,0 +1,83 @@
1
+ import { $, execa } from "execa";
2
+ const taskUtilities = {
3
+ command: execa,
4
+ $,
5
+ exec: execa({
6
+ stdin: "inherit",
7
+ stdout: "inherit",
8
+ stderr: "inherit"
9
+ })
10
+ };
11
+ const executeTask = async (context, options) => {
12
+ const taskFunction = context.taskFile.data.config[context.taskName];
13
+ const specifier = `${context.taskFile.name}::${context.taskName}`;
14
+ if (taskFunction == null) {
15
+ context.handler.write(context.handler.help({
16
+ taskList: true,
17
+ ...context
18
+ }));
19
+ if (!context.taskName) context.handler.failWith(`Task name is required.`);
20
+ context.handler.failWith([`Task file '${context.taskFile.name}' does not export '${context.taskName}'`, `Resolved to: ${context.taskFile.path}`]);
21
+ }
22
+ try {
23
+ const optionsWithEnvironment = {
24
+ ...options,
25
+ env: process.env
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
+ }
76
+ } catch (error) {
77
+ const message = error instanceof Error ? error.message : String(error);
78
+ if (message.includes("is not a function")) context.handler.failWith([`Failed to execute ${specifier}`, `Exported value '${context.taskName}' is not a function`]);
79
+ context.handler.failWith([`Failed to execute ${specifier}`, message]);
80
+ }
81
+ };
82
+ //#endregion
83
+ export { executeTask };
@@ -0,0 +1,4 @@
1
+ //#region src/tasks/guards.ts
2
+ const isBrandedTask = (task) => "kind" in task;
3
+ //#endregion
4
+ export { isBrandedTask };
@@ -0,0 +1,10 @@
1
+ import { BrandedTask, BrandedTaskStrict, DefaultOptionsInput, Task } from "./types.mjs";
2
+ import { Type, type } from "arktype";
3
+
4
+ //#region src/tasks/task.d.ts
5
+ declare const task: {
6
+ <T = DefaultOptionsInput>(fn: Task<T>): BrandedTask<T>;
7
+ strict<const TShape>(shape: type.validate<TShape>, fn: Task<NoInfer<Type<type.infer<TShape>>["infer"]>>): BrandedTaskStrict<Type<type.infer<TShape>>["inferIn"]>;
8
+ };
9
+ //#endregion
10
+ export { task };
@@ -0,0 +1,40 @@
1
+ import { styleText } from "node:util";
2
+ import { type } from "arktype";
3
+ //#region src/tasks/task.ts
4
+ const formatValidationIssues = (errors) => errors.map((error) => {
5
+ const propertyName = String(error.path[0]);
6
+ const dottedPath = error.path.join(".");
7
+ if (!propertyName) return error.message;
8
+ const messageWithoutProperty = error.message.startsWith(dottedPath) ? error.message.slice(dottedPath.length + 1) : error.message;
9
+ if (propertyName === "env") return `${styleText("red", `Environment variable '${String(error.path[1])}'`)}: ${messageWithoutProperty}`;
10
+ const optionText = propertyName === "_" ? "Positionals" : `--${propertyName}`;
11
+ if (error.code === "predicate" && error.expected === "removed") return `${styleText("red", optionText)}: unknown option`;
12
+ return `${styleText("red", optionText)}: ${messageWithoutProperty}`;
13
+ }).join("\n");
14
+ const task = (fn) => {
15
+ fn.kind = "task";
16
+ return fn;
17
+ };
18
+ /**
19
+ * Default schema for the options received by a {@link task}.
20
+ */
21
+ const defaultOptionsInput = type({
22
+ _: "string[]",
23
+ env: "Record<string, string | undefined>"
24
+ });
25
+ /**
26
+ * Create a task that validates its input against a schema.
27
+ */
28
+ task.strict = (shape, fn) => {
29
+ const schema = defaultOptionsInput.merge(type.raw(shape)).onUndeclaredKey("reject");
30
+ const taskFunction = (options, utilities) => {
31
+ const validationResult = schema(options);
32
+ if (validationResult instanceof type.errors) throw new TypeError(formatValidationIssues(validationResult), { cause: validationResult });
33
+ return fn(validationResult, utilities);
34
+ };
35
+ taskFunction.kind = "strictTask";
36
+ taskFunction.schema = schema;
37
+ return taskFunction;
38
+ };
39
+ //#endregion
40
+ export { task };
@@ -0,0 +1,60 @@
1
+ import { ParsedOptions } from "../cli/parseOptions.mjs";
2
+ import { Type } from "arktype";
3
+ import { ExecaMethod, ExecaScriptMethod } from "execa";
4
+
5
+ //#region src/tasks/types.d.ts
6
+ interface TaskUtilities {
7
+ /**
8
+ * Run a command using Execa's
9
+ * {@link https://github.com/sindresorhus/execa/blob/main/docs/scripts.md|script mode}.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const { stdout } = await $`echo ${"hello"}`;
14
+ * console.log(`stdout = ${stdout}`);
15
+ * ```
16
+ *
17
+ * @see {@link https://github.com/sindresorhus/execa/blob/main/docs/execution.md#%EF%B8%8F-basic-execution|Execa docs}
18
+ */
19
+ $: ExecaScriptMethod;
20
+ /**
21
+ * Run a command using {@link https://github.com/sindresorhus/execa|Execa}
22
+ * (e.g., shell command or script).
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const { stdout } = await execa`echo 'hello'`;
27
+ * console.log(stdout);
28
+ * ```
29
+ *
30
+ * @see {@link https://github.com/sindresorhus/execa/blob/main/docs/execution.md#%EF%B8%8F-basic-execution|Execa docs}
31
+ */
32
+ command: ExecaMethod;
33
+ /**
34
+ * Same as `command`, but inherits the parent process' stdio streams by
35
+ * default, i.e., logs and errors will be sent directly to the terminal.
36
+ * Use `command` or `$` if you want to capture the output instead.
37
+ */
38
+ exec: ExecaMethod<{
39
+ stdin: "inherit";
40
+ stdout: "inherit";
41
+ stderr: "inherit";
42
+ }>;
43
+ }
44
+ type DefaultOptionsInput = ParsedOptions & {
45
+ env: Record<string, string | undefined>;
46
+ };
47
+ type MergePositionals<TOptions> = "_" extends keyof TOptions ? TOptions : TOptions & ParsedOptions;
48
+ type MergeEnvironment<TOptions> = "env" extends keyof TOptions ? TOptions : TOptions & Pick<DefaultOptionsInput, "env">;
49
+ type Task<TOptions = unknown> = (options: MergePositionals<MergeEnvironment<TOptions>>, utilities: TaskUtilities) => unknown;
50
+ type BrandedTaskStrict<TShape = DefaultOptionsInput> = Task<TShape> & {
51
+ kind: "strictTask";
52
+ schema: Type<TShape>;
53
+ };
54
+ type BrandedTaskLoose<TOptions = unknown> = Task<TOptions> & {
55
+ kind: "task";
56
+ schema: never;
57
+ };
58
+ type BrandedTask<TOptions = unknown> = BrandedTaskLoose<TOptions> | BrandedTaskStrict<TOptions>;
59
+ //#endregion
60
+ export { BrandedTask, BrandedTaskStrict, DefaultOptionsInput, Task };
@@ -0,0 +1,34 @@
1
+ import { reservedNames } from "../cli/parseOptions.mjs";
2
+ import { styleText } from "node:util";
3
+ import { type } from "arktype";
4
+ //#region src/util/getSchemaProperties.ts
5
+ const propertyEntry = type({
6
+ key: "string",
7
+ "+": "delete"
8
+ });
9
+ const arktypeJson = type({
10
+ domain: "string",
11
+ required: propertyEntry.array().default(() => []),
12
+ optional: propertyEntry.array().default(() => [])
13
+ });
14
+ const getSchemaProperties = (schema) => {
15
+ const definition = arktypeJson.assert(schema.json);
16
+ if (definition instanceof type.errors || definition.domain !== "object") return "";
17
+ const { required, optional } = definition;
18
+ const requiredProperties = required.map(({ key }) => ({
19
+ key,
20
+ isOptional: false
21
+ }));
22
+ const optionalProperties = optional.map(({ key }) => ({
23
+ key,
24
+ isOptional: true
25
+ }));
26
+ const properties = [...requiredProperties, ...optionalProperties];
27
+ if (properties.length === 0) return "";
28
+ return `{ ${properties.filter(({ key }) => !reservedNames.has(key)).map(({ key, isOptional }) => {
29
+ if (isOptional) return styleText("dim", `[${key}]`);
30
+ return key;
31
+ }).join(", ")} }`;
32
+ };
33
+ //#endregion
34
+ export { getSchemaProperties };
@@ -0,0 +1,31 @@
1
+ import { resolve } from "node:path";
2
+ import { loadConfig } from "c12";
3
+ //#region src/util/loadTaskFile.ts
4
+ const loadTaskFile = async (context) => {
5
+ try {
6
+ const collection = await loadConfig({
7
+ cwd: context.taskFile.dir,
8
+ name: context.taskFile.name,
9
+ configFile: context.taskFile.base,
10
+ dotenv: false,
11
+ rcFile: false,
12
+ packageJson: false,
13
+ jitiOptions: { tsconfigPaths: true }
14
+ });
15
+ if (!collection.configFile || collection.layers?.length === 0) return {
16
+ ok: false,
17
+ error: /* @__PURE__ */ new Error(`Failed to load task file at path ${resolve(context.taskFile.dir, context.taskFile.base)}`)
18
+ };
19
+ return {
20
+ ok: true,
21
+ value: collection
22
+ };
23
+ } catch (error) {
24
+ return {
25
+ ok: false,
26
+ error: error instanceof Error ? error : new Error(String(error))
27
+ };
28
+ }
29
+ };
30
+ //#endregion
31
+ export { loadTaskFile };