@hackwaly/task 0.2.4 → 0.3.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/bin/task CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env -S node --disable-warning=ExperimentalWarning --experimental-strip-types --import="amaro/strip"
2
- import { cliMain } from "../src/index.ts";
1
+ #!/usr/bin/env -S node
2
+ import { cliMain } from "../src/index.js";
3
3
 
4
4
  await cliMain();
package/package.json CHANGED
@@ -1,16 +1,25 @@
1
1
  {
2
2
  "name": "@hackwaly/task",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": "github:hackwaly/task",
7
7
  "bin": {
8
8
  "task": "bin/task"
9
9
  },
10
- "main": "./src/index.ts",
11
- "pnpm": {},
10
+ "exports": {
11
+ ".": {
12
+ "import": "./src/index.js",
13
+ "types": "./src/index.d.ts"
14
+ }
15
+ },
16
+ "pnpm": {
17
+ "overrides": {}
18
+ },
19
+ "scripts": {
20
+ "prepublish": "tsc --build"
21
+ },
12
22
  "dependencies": {
13
- "amaro": "^1.1.4",
14
23
  "ansi-styles": "^6.2.3",
15
24
  "chokidar": "^4.0.3",
16
25
  "commander": "^14.0.1",
@@ -22,6 +31,7 @@
22
31
  },
23
32
  "devDependencies": {
24
33
  "@types/micromatch": "^4.0.9",
25
- "@types/node": "^24.8.1"
34
+ "@types/node": "^24.8.1",
35
+ "typescript": "^5.9.3"
26
36
  }
27
37
  }
package/src/cli.js ADDED
@@ -0,0 +1,145 @@
1
+ import { Command } from "commander";
2
+ import process from "node:process";
3
+ import { start } from "./scheduler.js";
4
+ import { ReplaySubject } from "rxjs";
5
+ import chokidar from "chokidar";
6
+ import micromatch from "micromatch";
7
+ import NodePath from "node:path";
8
+ export async function cliMain() {
9
+ const program = new Command()
10
+ .name("task")
11
+ .version("0.1.0")
12
+ .description("Just another task runner");
13
+ const runCommand = new Command()
14
+ .name("run")
15
+ .option("-w, --watch", "Watch mode")
16
+ .argument("<tasks...>", "Task(s) to run")
17
+ .action(async (taskNames, options) => {
18
+ const path = process.cwd();
19
+ const allTaskDefs = await import(NodePath.join(path, "taskfile.js"));
20
+ const topTaskSet = new Set();
21
+ for (const name of taskNames) {
22
+ const taskDef = allTaskDefs[name];
23
+ if (!taskDef) {
24
+ throw new Error(`Task "${name}" not found.`);
25
+ }
26
+ topTaskSet.add(taskDef);
27
+ }
28
+ const aborter = new AbortController();
29
+ const taskChan = new ReplaySubject();
30
+ const loop = start(taskChan, {
31
+ abort: aborter.signal,
32
+ });
33
+ if (options.watch) {
34
+ const watchTargets = new Set();
35
+ const watchSet = new Set();
36
+ const addWatchDir = (task) => {
37
+ for (const dep of task.deps) {
38
+ addWatchDir(dep);
39
+ }
40
+ if (!task.meta.persistent || task.meta.interruptible) {
41
+ watchSet.add(task);
42
+ watchTargets.add(task.meta.cwd);
43
+ }
44
+ };
45
+ for (const task of topTaskSet) {
46
+ addWatchDir(task);
47
+ }
48
+ const watcher = chokidar.watch([...watchTargets], {
49
+ ignoreInitial: true,
50
+ ignored: (path) => /\bnode_modules\b/.test(path),
51
+ });
52
+ const dirtySeedSet = new Set();
53
+ const flush = () => {
54
+ const dirtySet = new Set();
55
+ const process = (task, buffer) => {
56
+ if (topTaskSet.has(task)) {
57
+ for (const t of buffer) {
58
+ dirtySet.add(t);
59
+ }
60
+ buffer.length = 0;
61
+ }
62
+ for (const invDep of task.invDeps) {
63
+ process(invDep, !invDep.meta.persistent || invDep.meta.interruptible
64
+ ? [...buffer, invDep]
65
+ : buffer);
66
+ }
67
+ };
68
+ for (const task of dirtySeedSet) {
69
+ process(task, [task]);
70
+ }
71
+ dirtySeedSet.clear();
72
+ if (dirtySet.size > 0) {
73
+ taskChan.next(dirtySet);
74
+ }
75
+ };
76
+ watcher.on("all", (event, path) => {
77
+ for (const task of watchSet) {
78
+ const relPath = NodePath.relative(task.meta.cwd, path);
79
+ if (micromatch.isMatch(relPath, task.meta.inputs)) {
80
+ dirtySeedSet.add(task);
81
+ }
82
+ }
83
+ if (dirtySeedSet.size > 0) {
84
+ flush();
85
+ }
86
+ });
87
+ process.on("SIGINT", async () => {
88
+ aborter.abort();
89
+ watcher.close();
90
+ await loop;
91
+ process.exit(0);
92
+ });
93
+ taskChan.next(topTaskSet);
94
+ }
95
+ else {
96
+ process.on("SIGINT", async () => {
97
+ aborter.abort();
98
+ await loop;
99
+ process.exit(0);
100
+ });
101
+ taskChan.next(topTaskSet);
102
+ taskChan.complete();
103
+ }
104
+ await loop;
105
+ });
106
+ const listCommand = new Command()
107
+ .name("list")
108
+ .alias("ls")
109
+ .description("List all available tasks")
110
+ .action(async () => {
111
+ const path = process.cwd();
112
+ const allTaskDefs = await import(NodePath.join(path, "taskfile.js"));
113
+ // Get all exported tasks
114
+ const tasks = [];
115
+ for (const [exportName, taskDef] of Object.entries(allTaskDefs)) {
116
+ if (exportName !== "default" &&
117
+ taskDef &&
118
+ typeof taskDef === "object" &&
119
+ "meta" in taskDef) {
120
+ const task = taskDef;
121
+ tasks.push({
122
+ name: task.meta.name,
123
+ description: task.meta.description,
124
+ });
125
+ }
126
+ }
127
+ if (tasks.length === 0) {
128
+ process.stdout.write("No tasks found in taskfile.ts\n");
129
+ return;
130
+ }
131
+ // Sort tasks by name
132
+ tasks.sort((a, b) => a.name.localeCompare(b.name));
133
+ // Find the longest task name for formatting
134
+ const maxNameLength = Math.max(...tasks.map((t) => t.name.length));
135
+ process.stdout.write("Available tasks:\n");
136
+ for (const task of tasks) {
137
+ const paddedName = task.name.padEnd(maxNameLength);
138
+ const description = task.description || "No description";
139
+ process.stdout.write(` ${paddedName} ${description}\n`);
140
+ }
141
+ });
142
+ program.addCommand(runCommand);
143
+ program.addCommand(listCommand);
144
+ await program.parseAsync();
145
+ }
package/src/config.js ADDED
@@ -0,0 +1,54 @@
1
+ import NodePath from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { runCommand } from "./run.js";
4
+ import { parseArgsStringToArgv } from "string-argv";
5
+ function normalizeCommand(command) {
6
+ if (typeof command === "string") {
7
+ const argv = parseArgsStringToArgv(command);
8
+ return { program: argv[0], args: argv.slice(1) };
9
+ }
10
+ if (Array.isArray(command)) {
11
+ return { program: command[0], args: command.slice(1) };
12
+ }
13
+ if (command !== undefined) {
14
+ return { program: command.program, args: command.args ?? [] };
15
+ }
16
+ return undefined;
17
+ }
18
+ export function configInit(importMeta) {
19
+ return {
20
+ defineTask: (config) => {
21
+ const command = normalizeCommand(config.command);
22
+ const meta = {
23
+ name: config.name,
24
+ description: config.description,
25
+ cwd: config.cwd ?? NodePath.dirname(fileURLToPath(importMeta.url)),
26
+ env: config.env ?? {},
27
+ inputs: config.inputs ?? ["**/*"],
28
+ outputs: config.outputs ?? ["**/*"],
29
+ persistent: config.persistent ?? false,
30
+ // interactive: config.interactive ?? false,
31
+ interruptible: config.interruptible ?? false,
32
+ };
33
+ const def = {
34
+ run: config.run ??
35
+ (async (ctx) => {
36
+ if (command !== undefined) {
37
+ await runCommand(command, meta, ctx);
38
+ }
39
+ }),
40
+ meta: meta,
41
+ deps: new Set(),
42
+ invDeps: new Set(),
43
+ };
44
+ for (const dep of config.dependsOn ?? []) {
45
+ def.deps.add(dep);
46
+ if (dep.meta.persistent && !def.meta.persistent) {
47
+ throw new Error(`Task "${def.meta.name}" depends on persistent task "${dep.meta.name}", so it must also be marked as persistent.`);
48
+ }
49
+ dep.invDeps.add(def);
50
+ }
51
+ return def;
52
+ },
53
+ };
54
+ }
package/src/errors.js ADDED
@@ -0,0 +1,5 @@
1
+ export class InvariantViolation extends Error {
2
+ get name() {
3
+ return InvariantViolation.name;
4
+ }
5
+ }
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { cliMain } from "./cli.js";
2
+ export { configInit } from "./config.js";
package/src/run.js ADDED
@@ -0,0 +1,28 @@
1
+ import { execa } from "execa";
2
+ import process from "node:process";
3
+ import styles from "ansi-styles";
4
+ import supportsColor from "supports-color";
5
+ const colorSupport = supportsColor.stdout;
6
+ export async function runCommand(command, meta, ctx) {
7
+ const { abort } = ctx;
8
+ const { name, cwd, env } = meta;
9
+ const transform = function* (line) {
10
+ const lastCR = line.lastIndexOf("\r");
11
+ const line2 = lastCR >= 0 ? line.substring(lastCR + 1, line.length) : line;
12
+ const line3 = line2.replace(/\x1bc|\x1b\[2J(?:\x1b\[H)?/g, "");
13
+ yield `\r${name} | ${line3}`;
14
+ };
15
+ process.stdout.write(`▪▪▪▪ ${styles.bold.open}${name}${styles.bold.close}\n`);
16
+ await execa({
17
+ // @ts-expect-error
18
+ cwd: cwd,
19
+ env: colorSupport
20
+ ? { ...(env ?? {}), FORCE_COLOR: `${colorSupport.level}` }
21
+ : env,
22
+ preferLocal: true,
23
+ stdout: [transform, "inherit"],
24
+ stderr: [transform, "inherit"],
25
+ cancelSignal: abort,
26
+ reject: false,
27
+ }) `${command.program} ${command.args}`;
28
+ }
@@ -0,0 +1,128 @@
1
+ import { InvariantViolation } from "./errors.js";
2
+ import { firstValueFrom, Subject } from "rxjs";
3
+ export async function start(taskChan, options) {
4
+ const abort = options.abort;
5
+ // A pending task is dirty, but a dirty task may not be pending
6
+ const dirtySet = new Set();
7
+ const pendingSet = new Set();
8
+ const upToDateSet = new Set();
9
+ const readySignal = new Subject();
10
+ const readySet = new Set();
11
+ const runningSet = new Map();
12
+ const abortedRunningSet = new Map();
13
+ const isReady = (task) => {
14
+ if (!dirtySet.has(task))
15
+ throw new InvariantViolation();
16
+ if (pendingSet.has(task)) {
17
+ return false;
18
+ }
19
+ for (const dep of task.deps) {
20
+ if (!upToDateSet.has(dep)) {
21
+ return false;
22
+ }
23
+ }
24
+ return true;
25
+ };
26
+ const checkReady = (task) => {
27
+ if (isReady(task)) {
28
+ dirtySet.delete(task);
29
+ readySet.add(task);
30
+ readySignal.next();
31
+ // Mark inverse dependencies as pending, so they won't become ready
32
+ for (const invDep of task.invDeps) {
33
+ if (dirtySet.has(invDep)) {
34
+ pendingSet.add(invDep);
35
+ }
36
+ }
37
+ }
38
+ };
39
+ const cancel = (task) => {
40
+ if (!runningSet.has(task))
41
+ throw new InvariantViolation();
42
+ const { promise, aborter } = runningSet.get(task);
43
+ runningSet.delete(task);
44
+ abortedRunningSet.set(task, promise);
45
+ aborter.abort();
46
+ };
47
+ const addDirtyAndCheckReady = (task) => {
48
+ if (runningSet.has(task)) {
49
+ cancel(task);
50
+ runningSet.delete(task);
51
+ }
52
+ else if (readySet.has(task)) {
53
+ readySet.delete(task);
54
+ }
55
+ dirtySet.add(task);
56
+ if (upToDateSet.has(task)) {
57
+ upToDateSet.delete(task);
58
+ }
59
+ else {
60
+ for (const dep of task.deps) {
61
+ addDirtyAndCheckReady(dep);
62
+ }
63
+ }
64
+ checkReady(task);
65
+ };
66
+ const runTask = async (task, abort) => {
67
+ if (runningSet.has(task)) {
68
+ const { promise, aborter } = runningSet.get(task);
69
+ aborter.abort();
70
+ await Promise.allSettled([promise]);
71
+ }
72
+ await task.run({ abort });
73
+ upToDateSet.add(task);
74
+ for (const invDep of task.invDeps) {
75
+ if (pendingSet.has(invDep)) {
76
+ pendingSet.delete(invDep);
77
+ checkReady(invDep);
78
+ }
79
+ }
80
+ };
81
+ let noMoreTasks = false;
82
+ taskChan.subscribe({
83
+ next: (tasks) => {
84
+ for (const task of tasks) {
85
+ addDirtyAndCheckReady(task);
86
+ }
87
+ },
88
+ complete: () => {
89
+ noMoreTasks = true;
90
+ },
91
+ });
92
+ const runLoop = async () => {
93
+ while (!abort.aborted) {
94
+ if (readySet.size === 0) {
95
+ await firstValueFrom(readySignal);
96
+ continue;
97
+ }
98
+ // TODO: limit the concurrency
99
+ for (const task of readySet) {
100
+ const aborter = new AbortController();
101
+ const promise = runTask(task, aborter.signal).finally(() => {
102
+ // Clean up abortedRunningSet if this was the last run
103
+ const wait = abortedRunningSet.get(task);
104
+ if (wait === promise) {
105
+ abortedRunningSet.delete(task);
106
+ }
107
+ const runningEntry = runningSet.get(task);
108
+ if (runningEntry !== undefined && runningEntry.promise === promise) {
109
+ runningSet.delete(task);
110
+ }
111
+ });
112
+ runningSet.set(task, { promise, aborter });
113
+ }
114
+ readySet.clear();
115
+ if (dirtySet.size === 0 && noMoreTasks) {
116
+ break;
117
+ }
118
+ }
119
+ };
120
+ abort.addEventListener("abort", () => {
121
+ for (const task of runningSet.keys()) {
122
+ cancel(task);
123
+ }
124
+ runningSet.clear();
125
+ });
126
+ await runLoop();
127
+ await Promise.allSettled(abortedRunningSet.values());
128
+ }
package/src/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/tsconfig.json CHANGED
@@ -3,13 +3,12 @@
3
3
  "target": "es2024",
4
4
  "module": "nodenext",
5
5
  "moduleResolution": "nodenext",
6
- "allowImportingTsExtensions": true,
7
6
  "skipLibCheck": true,
8
7
  "strict": true,
9
8
  "noUncheckedIndexedAccess": true,
10
9
  "isolatedModules": true,
11
10
  "erasableSyntaxOnly": true,
12
11
  "verbatimModuleSyntax": true,
13
- "noEmit": true
12
+ "declaration": true
14
13
  }
15
14
  }
package/src/cli.ts DELETED
@@ -1,157 +0,0 @@
1
- import { Command } from "commander";
2
- import process from "node:process";
3
- import type { TaskDef } from "./types.ts";
4
- import { start } from "./scheduler.ts";
5
- import { ReplaySubject } from "rxjs";
6
- import chokidar from "chokidar";
7
- import micromatch from "micromatch";
8
- import NodePath from "node:path";
9
-
10
- export async function cliMain(): Promise<void> {
11
- const program = new Command()
12
- .name("task")
13
- .version("0.1.0")
14
- .description("Just another task runner");
15
- const runCommand = new Command()
16
- .name("run")
17
- .option("-w, --watch", "Watch mode")
18
- .argument("<tasks...>", "Task(s) to run")
19
- .action(async (taskNames: string[], options: { watch: boolean }) => {
20
- const path = process.cwd();
21
- const allTaskDefs = await import(NodePath.join(path, "taskfile.ts"));
22
- const topTaskSet = new Set<TaskDef>();
23
- for (const name of taskNames) {
24
- const taskDef = allTaskDefs[name];
25
- if (!taskDef) {
26
- throw new Error(`Task "${name}" not found.`);
27
- }
28
- topTaskSet.add(taskDef);
29
- }
30
- const aborter = new AbortController();
31
- const taskChan = new ReplaySubject<Set<TaskDef>>();
32
- const loop = start(taskChan, {
33
- abort: aborter.signal,
34
- });
35
- if (options.watch) {
36
- const watchTargets = new Set<string>();
37
- const watchSet = new Set<TaskDef>();
38
- const addWatchDir = (task: TaskDef) => {
39
- for (const dep of task.deps) {
40
- addWatchDir(dep);
41
- }
42
- if (!task.meta.persistent || task.meta.interruptible) {
43
- watchSet.add(task);
44
- watchTargets.add(task.meta.cwd);
45
- }
46
- };
47
- for (const task of topTaskSet) {
48
- addWatchDir(task);
49
- }
50
- const watcher = chokidar.watch([...watchTargets], {
51
- ignoreInitial: true,
52
- ignored: (path) => /\bnode_modules\b/.test(path),
53
- });
54
- const dirtySeedSet = new Set<TaskDef>();
55
- const flush = () => {
56
- const dirtySet = new Set<TaskDef>();
57
- const process = (task: TaskDef, buffer: TaskDef[]) => {
58
- if (topTaskSet.has(task)) {
59
- for (const t of buffer) {
60
- dirtySet.add(t);
61
- }
62
- buffer.length = 0;
63
- }
64
- for (const invDep of task.invDeps) {
65
- process(
66
- invDep,
67
- !invDep.meta.persistent || invDep.meta.interruptible
68
- ? [...buffer, invDep]
69
- : buffer
70
- );
71
- }
72
- };
73
- for (const task of dirtySeedSet) {
74
- process(task, [task]);
75
- }
76
- dirtySeedSet.clear();
77
- if (dirtySet.size > 0) {
78
- taskChan.next(dirtySet);
79
- }
80
- };
81
- watcher.on("all", (event, path) => {
82
- for (const task of watchSet) {
83
- const relPath = NodePath.relative(task.meta.cwd, path);
84
- if (micromatch.isMatch(relPath, task.meta.inputs)) {
85
- dirtySeedSet.add(task);
86
- }
87
- }
88
- if (dirtySeedSet.size > 0) {
89
- flush();
90
- }
91
- });
92
- process.on("SIGINT", async () => {
93
- aborter.abort();
94
- watcher.close();
95
- await loop;
96
- process.exit(0);
97
- });
98
- taskChan.next(topTaskSet);
99
- } else {
100
- process.on("SIGINT", async () => {
101
- aborter.abort();
102
- await loop;
103
- process.exit(0);
104
- });
105
- taskChan.next(topTaskSet);
106
- taskChan.complete();
107
- }
108
- await loop;
109
- });
110
- const listCommand = new Command()
111
- .name("list")
112
- .alias("ls")
113
- .description("List all available tasks")
114
- .action(async () => {
115
- const path = process.cwd();
116
- const allTaskDefs = await import(NodePath.join(path, "taskfile.ts"));
117
-
118
- // Get all exported tasks
119
- const tasks: Array<{ name: string; description?: string }> = [];
120
- for (const [exportName, taskDef] of Object.entries(allTaskDefs)) {
121
- if (
122
- exportName !== "default" &&
123
- taskDef &&
124
- typeof taskDef === "object" &&
125
- "meta" in taskDef
126
- ) {
127
- const task = taskDef as TaskDef;
128
- tasks.push({
129
- name: task.meta.name,
130
- description: task.meta.description,
131
- });
132
- }
133
- }
134
-
135
- if (tasks.length === 0) {
136
- process.stdout.write("No tasks found in taskfile.ts\n");
137
- return;
138
- }
139
-
140
- // Sort tasks by name
141
- tasks.sort((a, b) => a.name.localeCompare(b.name));
142
-
143
- // Find the longest task name for formatting
144
- const maxNameLength = Math.max(...tasks.map((t) => t.name.length));
145
-
146
- process.stdout.write("Available tasks:\n");
147
- for (const task of tasks) {
148
- const paddedName = task.name.padEnd(maxNameLength);
149
- const description = task.description || "No description";
150
- process.stdout.write(` ${paddedName} ${description}\n`);
151
- }
152
- });
153
-
154
- program.addCommand(runCommand);
155
- program.addCommand(listCommand);
156
- await program.parseAsync();
157
- }
package/src/config.ts DELETED
@@ -1,82 +0,0 @@
1
- import type { Command, TaskDef, TaskMeta, TaskRunContext } from "./types.ts";
2
- import NodePath from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { runCommand } from "./run.ts";
5
- import { parseArgsStringToArgv } from "string-argv";
6
-
7
- export interface TaskConfig {
8
- name: string;
9
- description?: string;
10
- run?: (ctx: TaskRunContext) => Promise<void>;
11
- command?: string | string[] | { program: string; args?: string[] };
12
- env?: Record<string, string>;
13
- cwd?: string;
14
- inputs?: string[];
15
- outputs?: string[];
16
- persistent?: boolean;
17
- // interactive?: boolean;
18
- interruptible?: boolean;
19
- // cache?: boolean;
20
- dependsOn?: [TaskDef];
21
- }
22
-
23
- export interface ConfigAPI {
24
- defineTask(config: TaskConfig): TaskDef;
25
- }
26
-
27
- function normalizeCommand(
28
- command: string | string[] | { program: string; args?: string[] } | undefined
29
- ): Command | undefined {
30
- if (typeof command === "string") {
31
- const argv = parseArgsStringToArgv(command);
32
- return { program: argv[0]!, args: argv.slice(1) };
33
- }
34
- if (Array.isArray(command)) {
35
- return { program: command[0]!, args: command.slice(1) };
36
- }
37
- if (command !== undefined) {
38
- return { program: command.program, args: command.args ?? [] };
39
- }
40
- return undefined;
41
- }
42
-
43
- export function configInit(importMeta: ImportMeta): ConfigAPI {
44
- return {
45
- defineTask: (config: TaskConfig): TaskDef => {
46
- const command = normalizeCommand(config.command);
47
- const meta: TaskMeta = {
48
- name: config.name,
49
- description: config.description,
50
- cwd: config.cwd ?? NodePath.dirname(fileURLToPath(importMeta.url)),
51
- env: config.env ?? {},
52
- inputs: config.inputs ?? ["**/*"],
53
- outputs: config.outputs ?? ["**/*"],
54
- persistent: config.persistent ?? false,
55
- // interactive: config.interactive ?? false,
56
- interruptible: config.interruptible ?? false,
57
- };
58
- const def: TaskDef = {
59
- run:
60
- config.run ??
61
- (async (ctx: TaskRunContext) => {
62
- if (command !== undefined) {
63
- await runCommand(command, meta, ctx);
64
- }
65
- }),
66
- meta: meta,
67
- deps: new Set(),
68
- invDeps: new Set(),
69
- };
70
- for (const dep of config.dependsOn ?? []) {
71
- def.deps.add(dep);
72
- if (dep.meta.persistent && !def.meta.persistent) {
73
- throw new Error(
74
- `Task "${def.meta.name}" depends on persistent task "${dep.meta.name}", so it must also be marked as persistent.`
75
- );
76
- }
77
- dep.invDeps.add(def);
78
- }
79
- return def;
80
- },
81
- };
82
- }
package/src/errors.ts DELETED
@@ -1,5 +0,0 @@
1
- export class InvariantViolation extends Error {
2
- get name(): string {
3
- return InvariantViolation.name;
4
- }
5
- }
package/src/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export { cliMain } from "./cli.ts";
2
- export { configInit } from "./config.ts";
package/src/run.ts DELETED
@@ -1,37 +0,0 @@
1
- import type { Command, TaskMeta, TaskRunContext } from "./types.ts";
2
- import { execa } from "execa";
3
- import process from "node:process";
4
- import styles from "ansi-styles";
5
- import supportsColor from "supports-color";
6
-
7
- const colorSupport = supportsColor.stdout;
8
-
9
- export async function runCommand(
10
- command: Command,
11
- meta: TaskMeta,
12
- ctx: TaskRunContext
13
- ): Promise<void> {
14
- const { abort } = ctx;
15
- const { name, cwd, env } = meta;
16
-
17
- const transform = function* (line: string) {
18
- const lastCR = line.lastIndexOf("\r");
19
- const line2 = lastCR >= 0 ? line.substring(lastCR + 1, line.length) : line;
20
- const line3 = line2.replace(/\x1bc|\x1b\[2J(?:\x1b\[H)?/g, "");
21
- yield `\r${name} | ${line3}`;
22
- };
23
-
24
- process.stdout.write(`▪▪▪▪ ${styles.bold.open}${name}${styles.bold.close}\n`);
25
- await execa({
26
- // @ts-expect-error
27
- cwd: cwd,
28
- env: colorSupport
29
- ? { ...(env ?? {}), FORCE_COLOR: `${colorSupport.level}` }
30
- : env,
31
- preferLocal: true,
32
- stdout: [transform, "inherit"],
33
- stderr: [transform, "inherit"],
34
- cancelSignal: abort,
35
- reject: false,
36
- })`${command.program} ${command.args}`;
37
- }
package/src/scheduler.ts DELETED
@@ -1,156 +0,0 @@
1
- import type { TaskDef } from "./types.ts";
2
- import { InvariantViolation } from "./errors.ts";
3
- import { firstValueFrom, Subject, type Observable } from "rxjs";
4
-
5
- export async function start(
6
- taskChan: Observable<Set<TaskDef>>,
7
- options: {
8
- abort: AbortSignal;
9
- }
10
- ): Promise<void> {
11
- const abort = options.abort;
12
-
13
- // A pending task is dirty, but a dirty task may not be pending
14
- const dirtySet = new Set<TaskDef>();
15
- const pendingSet = new Set<TaskDef>();
16
- const upToDateSet = new Set<TaskDef>();
17
-
18
- const readySignal = new Subject<void>();
19
- const readySet = new Set<TaskDef>();
20
- const runningSet = new Map<
21
- TaskDef,
22
- {
23
- promise: Promise<void>;
24
- aborter: AbortController;
25
- }
26
- >();
27
- const abortedRunningSet = new Map<TaskDef, Promise<void>>();
28
-
29
- const isReady = (task: TaskDef) => {
30
- if (!dirtySet.has(task)) throw new InvariantViolation();
31
-
32
- if (pendingSet.has(task)) {
33
- return false;
34
- }
35
-
36
- for (const dep of task.deps) {
37
- if (!upToDateSet.has(dep)) {
38
- return false;
39
- }
40
- }
41
- return true;
42
- };
43
-
44
- const checkReady = (task: TaskDef) => {
45
- if (isReady(task)) {
46
- dirtySet.delete(task);
47
- readySet.add(task);
48
- readySignal.next();
49
-
50
- // Mark inverse dependencies as pending, so they won't become ready
51
- for (const invDep of task.invDeps) {
52
- if (dirtySet.has(invDep)) {
53
- pendingSet.add(invDep);
54
- }
55
- }
56
- }
57
- };
58
-
59
- const cancel = (task: TaskDef) => {
60
- if (!runningSet.has(task)) throw new InvariantViolation();
61
-
62
- const { promise, aborter } = runningSet.get(task)!;
63
- runningSet.delete(task);
64
- abortedRunningSet.set(task, promise);
65
- aborter.abort();
66
- };
67
-
68
- const addDirtyAndCheckReady = (task: TaskDef) => {
69
- if (runningSet.has(task)) {
70
- cancel(task);
71
- runningSet.delete(task);
72
- } else if (readySet.has(task)) {
73
- readySet.delete(task);
74
- }
75
-
76
- dirtySet.add(task);
77
-
78
- if (upToDateSet.has(task)) {
79
- upToDateSet.delete(task);
80
- } else {
81
- for (const dep of task.deps) {
82
- addDirtyAndCheckReady(dep);
83
- }
84
- }
85
-
86
- checkReady(task);
87
- };
88
-
89
- const runTask = async (task: TaskDef, abort: AbortSignal) => {
90
- if (runningSet.has(task)) {
91
- const { promise, aborter } = runningSet.get(task)!;
92
- aborter.abort();
93
- await Promise.allSettled([promise]);
94
- }
95
-
96
- await task.run({ abort });
97
- upToDateSet.add(task);
98
- for (const invDep of task.invDeps) {
99
- if (pendingSet.has(invDep)) {
100
- pendingSet.delete(invDep);
101
- checkReady(invDep);
102
- }
103
- }
104
- };
105
-
106
- let noMoreTasks = false;
107
- taskChan.subscribe({
108
- next: (tasks) => {
109
- for (const task of tasks) {
110
- addDirtyAndCheckReady(task);
111
- }
112
- },
113
- complete: () => {
114
- noMoreTasks = true;
115
- },
116
- });
117
-
118
- const runLoop = async () => {
119
- while (!abort.aborted) {
120
- if (readySet.size === 0) {
121
- await firstValueFrom(readySignal);
122
- continue;
123
- }
124
- // TODO: limit the concurrency
125
- for (const task of readySet) {
126
- const aborter = new AbortController();
127
- const promise = runTask(task, aborter.signal).finally(() => {
128
- // Clean up abortedRunningSet if this was the last run
129
- const wait = abortedRunningSet.get(task);
130
- if (wait === promise) {
131
- abortedRunningSet.delete(task);
132
- }
133
- const runningEntry = runningSet.get(task);
134
- if (runningEntry !== undefined && runningEntry.promise === promise) {
135
- runningSet.delete(task);
136
- }
137
- });
138
- runningSet.set(task, { promise, aborter });
139
- }
140
- readySet.clear();
141
- if (dirtySet.size === 0 && noMoreTasks) {
142
- break;
143
- }
144
- }
145
- };
146
-
147
- abort.addEventListener("abort", () => {
148
- for (const task of runningSet.keys()) {
149
- cancel(task);
150
- }
151
- runningSet.clear();
152
- });
153
-
154
- await runLoop();
155
- await Promise.allSettled(abortedRunningSet.values());
156
- }
package/src/types.ts DELETED
@@ -1,27 +0,0 @@
1
- export interface Command {
2
- program: string;
3
- args: string[];
4
- }
5
-
6
- export interface TaskRunContext {
7
- abort: AbortSignal;
8
- }
9
-
10
- export interface TaskMeta {
11
- name: string;
12
- description: string | undefined;
13
- cwd: string;
14
- env: Record<string, string>;
15
- inputs: string[];
16
- outputs: string[];
17
- persistent: boolean;
18
- // interactive: boolean;
19
- interruptible: boolean;
20
- }
21
-
22
- export interface TaskDef {
23
- run: (ctx: TaskRunContext) => Promise<void>;
24
- meta: TaskMeta;
25
- deps: Set<TaskDef>;
26
- invDeps: Set<TaskDef>;
27
- }