@haltcase/run 3.0.1 → 4.0.0
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/bin.d.mts +1 -0
- package/dist/cli/bin.mjs +7 -0
- package/dist/cli/handler.mjs +110 -0
- package/dist/cli/main.mjs +56 -0
- package/dist/cli/parseOptions.d.mts +7 -0
- package/dist/cli/parseOptions.mjs +36 -0
- package/dist/config.d.mts +18 -0
- package/dist/config.mjs +19 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +2 -0
- package/dist/tasks/executeTask.mjs +34 -0
- package/dist/tasks/guards.mjs +4 -0
- package/dist/tasks/task.d.mts +10 -0
- package/dist/tasks/task.mjs +40 -0
- package/dist/tasks/types.d.mts +60 -0
- package/dist/util/getSchemaProperties.mjs +34 -0
- package/dist/util/loadTaskFile.mjs +30 -0
- package/dist/util/resolveTaskFile.mjs +13 -0
- package/package.json +41 -39
- package/dist/cli/bin.d.ts +0 -2
- package/dist/cli/bin.js +0 -7
- package/dist/cli/handler.d.ts +0 -28
- package/dist/cli/handler.js +0 -105
- package/dist/cli/main.d.ts +0 -21
- package/dist/cli/main.js +0 -64
- package/dist/cli/parseOptions.d.ts +0 -13
- package/dist/cli/parseOptions.js +0 -48
- package/dist/config.d.ts +0 -16
- package/dist/config.js +0 -16
- package/dist/index.d.ts +0 -4
- package/dist/index.js +0 -1
- package/dist/tasks/executeTask.d.ts +0 -12
- package/dist/tasks/executeTask.js +0 -45
- package/dist/tasks/guards.d.ts +0 -3
- package/dist/tasks/guards.js +0 -2
- package/dist/tasks/task.d.ts +0 -14
- package/dist/tasks/task.js +0 -55
- package/dist/tasks/types.d.ts +0 -58
- package/dist/tasks/types.js +0 -1
- package/dist/tsconfig.build.tsbuildinfo +0 -1
- package/dist/util/getSchemaProperties.d.ts +0 -2
- package/dist/util/getSchemaProperties.js +0 -40
- package/dist/util/loadTaskFile.d.ts +0 -8
- package/dist/util/loadTaskFile.js +0 -30
- package/dist/util/resolveTaskFile.d.ts +0 -3
- package/dist/util/resolveTaskFile.js +0 -17
- package/dist/util/result.d.ts +0 -9
- package/dist/util/result.js +0 -1
- /package/{license → LICENSE} +0 -0
- /package/{readme.md → README.md} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/cli/bin.mjs
ADDED
|
@@ -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 { bold, gray, yellow } from "colorette";
|
|
5
|
+
import { readdirSync } from "node:fs";
|
|
6
|
+
import { extname } from "node:path";
|
|
7
|
+
import { Spinner } from "@favware/colorette-spinner";
|
|
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: ${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 = bold(name);
|
|
53
|
+
if (isBrandedTask(value)) {
|
|
54
|
+
if (value.kind === "strictTask") {
|
|
55
|
+
const properties = getSchemaProperties(value.schema) || 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,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,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 };
|
package/dist/config.mjs
ADDED
|
@@ -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 };
|
package/dist/index.d.mts
ADDED
|
@@ -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,34 @@
|
|
|
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
|
+
await taskFunction({
|
|
24
|
+
...options,
|
|
25
|
+
env: process.env
|
|
26
|
+
}, taskUtilities);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
29
|
+
if (message.includes("is not a function")) context.handler.failWith([`Failed to execute ${specifier}`, `Exported value '${context.taskName}' is not a function`]);
|
|
30
|
+
context.handler.failWith([`Failed to execute ${specifier}`, message]);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { executeTask };
|
|
@@ -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 { type } from "arktype";
|
|
2
|
+
import { red } from "colorette";
|
|
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 `${red(`Environment variable '${String(error.path[1])}'`)}: ${messageWithoutProperty}`;
|
|
10
|
+
const optionText = propertyName === "_" ? "Positionals" : `--${propertyName}`;
|
|
11
|
+
if (error.code === "predicate" && error.expected === "removed") return `${red(optionText)}: unknown option`;
|
|
12
|
+
return `${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 { type } from "arktype";
|
|
3
|
+
import { dim } from "colorette";
|
|
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 dim(`[${key}]`);
|
|
30
|
+
return key;
|
|
31
|
+
}).join(", ")} }`;
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { getSchemaProperties };
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
});
|
|
14
|
+
if (!collection.configFile || collection.layers?.length === 0) return {
|
|
15
|
+
ok: false,
|
|
16
|
+
error: /* @__PURE__ */ new Error(`Failed to load task file at path ${resolve(context.taskFile.dir, context.taskFile.base)}`)
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
ok: true,
|
|
20
|
+
value: collection
|
|
21
|
+
};
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
//#endregion
|
|
30
|
+
export { loadTaskFile };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { SUPPORTED_EXTENSIONS } from "c12";
|
|
4
|
+
//#region src/util/resolveTaskFile.ts
|
|
5
|
+
const extensions = SUPPORTED_EXTENSIONS.filter((extension) => extension.endsWith("js") || extension.endsWith("ts"));
|
|
6
|
+
const resolveTaskFile = (context) => {
|
|
7
|
+
if (context.taskFile.ext) return resolve(context.taskFile.dir, context.taskFile.name);
|
|
8
|
+
const foundConfigs = extensions.map((it) => join(context.taskFile.dir, `${context.taskFile.name}${it}`)).filter((path) => existsSync(path));
|
|
9
|
+
if (foundConfigs.length === 1) return foundConfigs[0];
|
|
10
|
+
return foundConfigs;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { extensions, resolveTaskFile };
|
package/package.json
CHANGED
|
@@ -1,77 +1,79 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haltcase/run",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Flexible, function-based task runner where command line options are props",
|
|
5
5
|
"keywords": [
|
|
6
|
-
"
|
|
7
|
-
"runner",
|
|
6
|
+
"arktype",
|
|
8
7
|
"command",
|
|
9
|
-
"function",
|
|
10
|
-
"typescript",
|
|
11
8
|
"execa",
|
|
12
|
-
"
|
|
9
|
+
"function",
|
|
10
|
+
"runner",
|
|
11
|
+
"task",
|
|
12
|
+
"typescript"
|
|
13
13
|
],
|
|
14
14
|
"homepage": "https://github.com/haltcase/run#readme",
|
|
15
15
|
"bugs": "https://github.com/haltcase/run/issues",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Bo Lingen <bo@haltcase.dev> (https://haltcase.dev)",
|
|
16
18
|
"repository": {
|
|
17
19
|
"type": "git",
|
|
18
20
|
"url": "https://github.com/haltcase/run.git"
|
|
19
21
|
},
|
|
20
|
-
"license": "MIT",
|
|
21
|
-
"author": "Bo Lingen <bo@haltcase.dev> (https://haltcase.dev)",
|
|
22
|
-
"type": "module",
|
|
23
|
-
"exports": {
|
|
24
|
-
".": "./dist/index.js"
|
|
25
|
-
},
|
|
26
|
-
"main": "./dist/index.js",
|
|
27
|
-
"types": "./dist/index.d.ts",
|
|
28
22
|
"bin": {
|
|
29
|
-
"hr": "./dist/cli/bin.
|
|
23
|
+
"hr": "./dist/cli/bin.mjs"
|
|
30
24
|
},
|
|
31
25
|
"files": [
|
|
32
26
|
"dist"
|
|
33
27
|
],
|
|
34
|
-
"
|
|
35
|
-
|
|
28
|
+
"type": "module",
|
|
29
|
+
"main": "./dist/index.mjs",
|
|
30
|
+
"types": "./dist/index.d.mts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.mts",
|
|
34
|
+
"default": "./dist/index.mjs"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
36
39
|
},
|
|
37
|
-
"prettier": "@haltcase/style/prettier",
|
|
38
40
|
"dependencies": {
|
|
39
41
|
"@favware/colorette-spinner": "^1.0.1",
|
|
40
|
-
"arktype": "^2.
|
|
41
|
-
"c12": "^3.
|
|
42
|
+
"arktype": "^2.2.0",
|
|
43
|
+
"c12": "^3.3.4",
|
|
42
44
|
"cliui": "^9.0.1",
|
|
43
45
|
"colorette": "^2.0.20",
|
|
44
|
-
"execa": "^9.
|
|
46
|
+
"execa": "^9.6.1"
|
|
45
47
|
},
|
|
46
48
|
"devDependencies": {
|
|
49
|
+
"@arethetypeswrong/core": "^0.18.3",
|
|
47
50
|
"@haltcase/style": "^7.3.1",
|
|
48
|
-
"@types/node": "^
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
51
|
+
"@types/node": "^24.13.1",
|
|
52
|
+
"@typescript/native-preview": "7.0.0-dev.20260608.1",
|
|
53
|
+
"eslint": "^9.39.4",
|
|
54
|
+
"oxlint-tsgolint": "^0.23.0",
|
|
55
|
+
"tsx": "^4.22.4",
|
|
56
|
+
"typescript": "^6.0.3",
|
|
57
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@0.1.24",
|
|
58
|
+
"vite-plus": "0.1.24",
|
|
59
|
+
"vitest": "npm:@voidzero-dev/vite-plus-test@0.1.24"
|
|
56
60
|
},
|
|
57
61
|
"engines": {
|
|
58
|
-
"node": ">=
|
|
59
|
-
},
|
|
60
|
-
"publishConfig": {
|
|
61
|
-
"access": "public"
|
|
62
|
+
"node": ">=22"
|
|
62
63
|
},
|
|
63
64
|
"haltcase.run": {
|
|
64
65
|
"taskDirectory": "./scripts",
|
|
65
66
|
"quiet": true
|
|
66
67
|
},
|
|
67
68
|
"scripts": {
|
|
68
|
-
"build": "
|
|
69
|
-
"build:watch": "
|
|
69
|
+
"build": "vp pack",
|
|
70
|
+
"build:watch": "vp pack --watch",
|
|
71
|
+
"check": "pnpm eslint:check && vp check --no-lint",
|
|
70
72
|
"eslint:check": "eslint src",
|
|
71
|
-
"format": "eslint --fix src &&
|
|
73
|
+
"format": "eslint --fix src && vp check --fix --no-lint",
|
|
72
74
|
"hr": "tsx ./src/cli/bin.ts",
|
|
73
|
-
"
|
|
74
|
-
"test": "
|
|
75
|
-
"
|
|
75
|
+
"test": "vp test watch",
|
|
76
|
+
"test:run": "vp test",
|
|
77
|
+
"types": "vp check --no-fmt --no-lint"
|
|
76
78
|
}
|
|
77
79
|
}
|
package/dist/cli/bin.d.ts
DELETED
package/dist/cli/bin.js
DELETED
package/dist/cli/handler.d.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { Spinner } from "@favware/colorette-spinner";
|
|
2
|
-
import type { AppConfig } from "../config.js";
|
|
3
|
-
import type { MainContextWithData } from "./main.js";
|
|
4
|
-
export declare const failWith: (spinner: Spinner, message: unknown) => never;
|
|
5
|
-
export declare const write: (message: string) => void;
|
|
6
|
-
interface HelpContextCommand {
|
|
7
|
-
command: string;
|
|
8
|
-
}
|
|
9
|
-
interface HelpContextFile {
|
|
10
|
-
taskFileList: true;
|
|
11
|
-
config: AppConfig;
|
|
12
|
-
}
|
|
13
|
-
interface HelpContextScript extends MainContextWithData {
|
|
14
|
-
taskList: true;
|
|
15
|
-
}
|
|
16
|
-
type HelpContext = HelpContextCommand | HelpContextFile | HelpContextScript;
|
|
17
|
-
export declare const commandHandler: (_context: HelpContextCommand) => string;
|
|
18
|
-
export declare const taskFileListHandler: (context: HelpContextFile) => string;
|
|
19
|
-
export declare const taskListHandler: (context: HelpContextScript) => string;
|
|
20
|
-
export declare const help: (context: HelpContext) => string;
|
|
21
|
-
export interface Handler {
|
|
22
|
-
spinner: Spinner;
|
|
23
|
-
help: (context: HelpContext) => string;
|
|
24
|
-
write: (message: string) => void;
|
|
25
|
-
failWith: (message: unknown) => never;
|
|
26
|
-
}
|
|
27
|
-
export declare const createHandler: () => Handler;
|
|
28
|
-
export {};
|