Building a Task Runner CLI from Scratch
A step-by-step guide to building a task runner CLI in Node.js with dependency resolution, parallel execution, file watching, and configuration from scratch.
Building a Task Runner CLI from Scratch
Every project reaches a point where npm scripts are not enough. You need task dependencies, parallelism, file watching, conditional execution, and shared configuration. Tools like Make, Gulp, and Grunt solve this, but building your own teaches you how task runners actually work — and sometimes a custom runner fits your workflow better than any off-the-shelf tool.
I have built task runners for teams where existing tools did not fit. The core concepts — dependency graphs, topological sorting, and file watching — are straightforward once you see them in code. This guide builds a complete task runner from zero.
Prerequisites
- Node.js installed (v14+)
- Understanding of promises and async patterns
- Familiarity with file system operations
- Basic graph theory concepts (nodes, edges, cycles)
Task Runner Architecture
A task runner has four core components:
- Task Registry — stores task definitions with their dependencies
- Dependency Resolver — determines execution order using topological sort
- Executor — runs tasks sequentially or in parallel
- File Watcher — re-runs tasks when source files change
┌─────────────┐ ┌──────────────┐ ┌──────────┐
│ Taskfile.js │────▶│ Task Registry │────▶│ Resolver │
└─────────────┘ └──────────────┘ └──────────┘
│
▼
┌──────────────┐ ┌──────────┐
│ File Watcher │◀────│ Executor │
└──────────────┘ └──────────┘
Building the Task Registry
The registry stores task definitions — a name, a function to run, dependencies, and metadata.
function createRegistry() {
var tasks = {};
return {
task: function(name, depsOrFn, fn) {
var deps = [];
var action = null;
if (typeof depsOrFn === "function") {
action = depsOrFn;
} else if (Array.isArray(depsOrFn)) {
deps = depsOrFn;
action = fn || null;
}
tasks[name] = {
name: name,
deps: deps,
action: action,
description: "",
watch: null
};
return this;
},
describe: function(name, description) {
if (tasks[name]) {
tasks[name].description = description;
}
return this;
},
watchFiles: function(name, patterns) {
if (tasks[name]) {
tasks[name].watch = patterns;
}
return this;
},
get: function(name) {
return tasks[name] || null;
},
getAll: function() {
return tasks;
},
names: function() {
return Object.keys(tasks);
},
has: function(name) {
return !!tasks[name];
}
};
}
Usage:
var registry = createRegistry();
registry.task("clean", function() {
console.log("Cleaning build directory...");
// rm -rf dist/
});
registry.task("lint", function() {
console.log("Linting source files...");
// eslint src/
});
registry.task("compile", ["lint"], function() {
console.log("Compiling TypeScript...");
// tsc
});
registry.task("test", ["compile"], function() {
console.log("Running tests...");
// jest
});
registry.task("build", ["clean", "compile"], function() {
console.log("Building production bundle...");
// webpack --mode production
});
registry.task("default", ["build", "test"]);
Dependency Resolution with Topological Sort
Tasks must run in dependency order. If build depends on compile which depends on lint, the order is: lint → compile → build. This is a topological sort.
function resolveDependencies(registry, taskName) {
var visited = {};
var visiting = {}; // For cycle detection
var order = [];
function visit(name) {
if (visited[name]) return;
if (visiting[name]) {
throw new Error("Circular dependency detected: " + name);
}
var task = registry.get(name);
if (!task) {
throw new Error("Unknown task: " + name);
}
visiting[name] = true;
for (var i = 0; i < task.deps.length; i++) {
visit(task.deps[i]);
}
delete visiting[name];
visited[name] = true;
order.push(name);
}
visit(taskName);
return order;
}
// Build the full dependency graph for visualization
function buildGraph(registry) {
var tasks = registry.getAll();
var graph = { nodes: [], edges: [] };
var names = Object.keys(tasks);
for (var i = 0; i < names.length; i++) {
var task = tasks[names[i]];
graph.nodes.push({
name: task.name,
hasAction: !!task.action,
deps: task.deps.length
});
for (var j = 0; j < task.deps.length; j++) {
graph.edges.push({
from: task.deps[j],
to: task.name
});
}
}
return graph;
}
// Find which tasks can run in parallel
function findParallelGroups(registry, taskName) {
var order = resolveDependencies(registry, taskName);
var groups = [];
var completed = {};
while (Object.keys(completed).length < order.length) {
var group = [];
for (var i = 0; i < order.length; i++) {
var name = order[i];
if (completed[name]) continue;
var task = registry.get(name);
var depsReady = task.deps.every(function(dep) {
return !!completed[dep];
});
if (depsReady) {
group.push(name);
}
}
for (var j = 0; j < group.length; j++) {
completed[group[j]] = true;
}
groups.push(group);
}
return groups;
}
Example:
var order = resolveDependencies(registry, "build");
console.log(order);
// ["clean", "lint", "compile", "build"]
var groups = findParallelGroups(registry, "default");
console.log(groups);
// [["clean", "lint"], ["compile"], ["test", "build"]]
// Group 1: clean and lint have no deps, run in parallel
// Group 2: compile depends on lint
// Group 3: test and build depend on compile
Building the Executor
The executor runs tasks in the resolved order, handling both sync and async task functions.
function createExecutor(registry, options) {
options = options || {};
var verbose = options.verbose || false;
var parallel = options.parallel !== false;
function formatTime(ms) {
if (ms < 1000) return ms + "ms";
return (ms / 1000).toFixed(2) + "s";
}
function runTask(name) {
var task = registry.get(name);
if (!task) {
return Promise.reject(new Error("Unknown task: " + name));
}
if (!task.action) {
if (verbose) {
console.log("\u001b[90m [skip] " + name + " (no action)\u001b[0m");
}
return Promise.resolve();
}
var startTime = Date.now();
console.log("\u001b[36m ▸\u001b[0m Starting \u001b[1m" + name + "\u001b[0m");
try {
var result = task.action();
// Handle promise or sync return
if (result && typeof result.then === "function") {
return result.then(function() {
var elapsed = Date.now() - startTime;
console.log(
"\u001b[32m ✔\u001b[0m Finished \u001b[1m" + name +
"\u001b[0m \u001b[90m(" + formatTime(elapsed) + ")\u001b[0m"
);
});
}
var elapsed = Date.now() - startTime;
console.log(
"\u001b[32m ✔\u001b[0m Finished \u001b[1m" + name +
"\u001b[0m \u001b[90m(" + formatTime(elapsed) + ")\u001b[0m"
);
return Promise.resolve();
} catch (err) {
var elapsed2 = Date.now() - startTime;
console.log(
"\u001b[31m ✖\u001b[0m Failed \u001b[1m" + name +
"\u001b[0m \u001b[90m(" + formatTime(elapsed2) + ")\u001b[0m"
);
return Promise.reject(err);
}
}
function runSequential(taskNames) {
var chain = Promise.resolve();
for (var i = 0; i < taskNames.length; i++) {
(function(name) {
chain = chain.then(function() {
return runTask(name);
});
})(taskNames[i]);
}
return chain;
}
function runParallel(taskNames) {
var promises = taskNames.map(function(name) {
return runTask(name);
});
return Promise.all(promises);
}
return {
run: function(taskName) {
var totalStart = Date.now();
console.log("");
console.log("\u001b[1mRunning task: " + taskName + "\u001b[0m");
console.log("\u001b[90m" + "─".repeat(40) + "\u001b[0m");
if (!parallel) {
var order = resolveDependencies(registry, taskName);
return runSequential(order).then(function() {
var totalTime = Date.now() - totalStart;
console.log("\u001b[90m" + "─".repeat(40) + "\u001b[0m");
console.log(
"\u001b[32m\u001b[1m✔ Done\u001b[0m \u001b[90m(" +
formatTime(totalTime) + ")\u001b[0m\n"
);
});
}
// Parallel execution by groups
var groups = findParallelGroups(registry, taskName);
var chain = Promise.resolve();
for (var g = 0; g < groups.length; g++) {
(function(group) {
chain = chain.then(function() {
if (group.length === 1) {
return runTask(group[0]);
}
if (verbose) {
console.log(
"\u001b[90m [parallel] " + group.join(", ") + "\u001b[0m"
);
}
return runParallel(group);
});
})(groups[g]);
}
return chain.then(function() {
var totalTime = Date.now() - totalStart;
console.log("\u001b[90m" + "─".repeat(40) + "\u001b[0m");
console.log(
"\u001b[32m\u001b[1m✔ Done\u001b[0m \u001b[90m(" +
formatTime(totalTime) + ")\u001b[0m\n"
);
});
}
};
}
File Watching
Re-run tasks when files change:
var fs = require("fs");
var path = require("path");
function createWatcher(options) {
options = options || {};
var debounceMs = options.debounce || 200;
var watchers = [];
function matchGlob(filePath, pattern) {
// Simple glob matching for *.ext and **/*.ext patterns
if (pattern.indexOf("**") !== -1) {
var ext = pattern.split(".").pop();
return filePath.endsWith("." + ext);
}
if (pattern.indexOf("*") !== -1) {
var ext2 = pattern.split(".").pop();
var dir = path.dirname(pattern);
return filePath.startsWith(dir) && filePath.endsWith("." + ext2);
}
return filePath === pattern;
}
function watchDir(dir, patterns, callback) {
var timer = null;
var changedFiles = [];
function onChange(eventType, filename) {
if (!filename) return;
var fullPath = path.join(dir, filename);
var matches = patterns.some(function(pattern) {
return matchGlob(fullPath, pattern);
});
if (!matches) return;
changedFiles.push(fullPath);
// Debounce: wait for changes to settle
if (timer) clearTimeout(timer);
timer = setTimeout(function() {
var files = changedFiles.slice();
changedFiles = [];
callback(files);
}, debounceMs);
}
try {
var watcher = fs.watch(dir, { recursive: true }, onChange);
watchers.push(watcher);
return watcher;
} catch (err) {
console.error("Watch error on " + dir + ": " + err.message);
return null;
}
}
return {
watch: function(dir, patterns, callback) {
return watchDir(dir, patterns, callback);
},
close: function() {
for (var i = 0; i < watchers.length; i++) {
watchers[i].close();
}
watchers = [];
}
};
}
Shell Command Helpers
Task runners need to execute shell commands easily:
var childProcess = require("child_process");
function exec(command, options) {
options = options || {};
return new Promise(function(resolve, reject) {
var proc = childProcess.exec(command, {
cwd: options.cwd || process.cwd(),
env: Object.assign({}, process.env, options.env || {}),
maxBuffer: 10 * 1024 * 1024
});
var stdout = "";
var stderr = "";
if (options.silent !== true) {
proc.stdout.pipe(process.stdout);
proc.stderr.pipe(process.stderr);
}
proc.stdout.on("data", function(data) { stdout += data; });
proc.stderr.on("data", function(data) { stderr += data; });
proc.on("close", function(code) {
if (code !== 0 && !options.ignoreError) {
var err = new Error("Command failed: " + command + " (exit code " + code + ")");
err.code = code;
err.stdout = stdout;
err.stderr = stderr;
reject(err);
} else {
resolve({ code: code, stdout: stdout, stderr: stderr });
}
});
});
}
function execSync(command, options) {
options = options || {};
return childProcess.execSync(command, {
cwd: options.cwd || process.cwd(),
stdio: options.silent ? "pipe" : "inherit",
env: Object.assign({}, process.env, options.env || {})
});
}
// Utility: run multiple commands in sequence
function series() {
var commands = Array.prototype.slice.call(arguments);
var chain = Promise.resolve();
for (var i = 0; i < commands.length; i++) {
(function(cmd) {
chain = chain.then(function() {
return exec(cmd);
});
})(commands[i]);
}
return chain;
}
// Utility: run multiple commands in parallel
function parallel() {
var commands = Array.prototype.slice.call(arguments);
return Promise.all(commands.map(function(cmd) {
return exec(cmd);
}));
}
The Taskfile
The taskfile is a JavaScript file that users create in their project root. It uses the task runner's API to define tasks:
// Taskfile.js
var runner = require("./runner");
var path = require("path");
var fs = require("fs");
// Clean
runner.task("clean", function() {
return runner.exec("rm -rf dist coverage");
});
runner.describe("clean", "Remove build artifacts");
// Lint
runner.task("lint", function() {
return runner.exec("npx eslint src/ --fix");
});
runner.describe("lint", "Lint and fix source files");
runner.watchFiles("lint", ["src/**/*.js"]);
// Compile
runner.task("compile", ["lint"], function() {
return runner.exec("npx tsc --outDir dist");
});
runner.describe("compile", "Compile TypeScript to JavaScript");
runner.watchFiles("compile", ["src/**/*.ts"]);
// Test
runner.task("test", ["compile"], function() {
return runner.exec("npx jest --coverage");
});
runner.describe("test", "Run test suite with coverage");
// Bundle
runner.task("bundle", ["compile"], function() {
return runner.exec("npx esbuild dist/index.js --bundle --outfile=dist/app.min.js --minify");
});
runner.describe("bundle", "Create production bundle");
// Build (everything)
runner.task("build", ["clean", "bundle", "test"]);
runner.describe("build", "Full build pipeline");
// Dev mode
runner.task("dev", ["compile"], function() {
return runner.exec("node dist/index.js", { silent: false });
});
runner.describe("dev", "Run in development mode");
// Default
runner.task("default", ["build"]);
Complete Working Example: Full Task Runner CLI
#!/usr/bin/env node
var fs = require("fs");
var path = require("path");
var childProcess = require("child_process");
// ---- ANSI Colors ----
var c = {
reset: "\u001b[0m", bold: "\u001b[1m", dim: "\u001b[90m",
green: "\u001b[32m", red: "\u001b[31m", yellow: "\u001b[33m",
cyan: "\u001b[36m", magenta: "\u001b[35m"
};
// ---- Task Registry ----
var tasks = {};
function task(name, depsOrFn, fn) {
var deps = [];
var action = null;
if (typeof depsOrFn === "function") { action = depsOrFn; }
else if (Array.isArray(depsOrFn)) { deps = depsOrFn; action = fn || null; }
tasks[name] = { name: name, deps: deps, action: action, desc: "", watch: null };
}
function describe(name, desc) { if (tasks[name]) tasks[name].desc = desc; }
function watchFiles(name, patterns) { if (tasks[name]) tasks[name].watch = patterns; }
// ---- Shell Execution ----
function exec(command, opts) {
opts = opts || {};
return new Promise(function(resolve, reject) {
var proc = childProcess.exec(command, {
cwd: opts.cwd || process.cwd(),
env: Object.assign({}, process.env, opts.env || {}),
maxBuffer: 10 * 1024 * 1024
});
if (opts.silent !== true) {
proc.stdout.pipe(process.stdout);
proc.stderr.pipe(process.stderr);
}
proc.on("close", function(code) {
if (code !== 0 && !opts.ignoreError) {
reject(new Error("Command failed (exit " + code + "): " + command));
} else {
resolve(code);
}
});
});
}
// ---- Dependency Resolution ----
function resolve(name) {
var visited = {};
var visiting = {};
var order = [];
function visit(n) {
if (visited[n]) return;
if (visiting[n]) throw new Error("Circular dependency: " + n);
if (!tasks[n]) throw new Error("Unknown task: " + n);
visiting[n] = true;
var deps = tasks[n].deps;
for (var i = 0; i < deps.length; i++) visit(deps[i]);
delete visiting[n];
visited[n] = true;
order.push(n);
}
visit(name);
return order;
}
function parallelGroups(name) {
var order = resolve(name);
var groups = [];
var done = {};
while (Object.keys(done).length < order.length) {
var group = [];
for (var i = 0; i < order.length; i++) {
if (done[order[i]]) continue;
var ready = tasks[order[i]].deps.every(function(d) { return !!done[d]; });
if (ready) group.push(order[i]);
}
for (var j = 0; j < group.length; j++) done[group[j]] = true;
groups.push(group);
}
return groups;
}
// ---- Executor ----
function formatMs(ms) {
return ms < 1000 ? ms + "ms" : (ms / 1000).toFixed(2) + "s";
}
function runOne(name) {
var t = tasks[name];
if (!t.action) return Promise.resolve();
var start = Date.now();
process.stdout.write(c.cyan + " ▸" + c.reset + " " + c.bold + name + c.reset + "\n");
try {
var result = t.action();
if (result && typeof result.then === "function") {
return result.then(function() {
process.stdout.write(
c.green + " ✔" + c.reset + " " + name +
" " + c.dim + formatMs(Date.now() - start) + c.reset + "\n"
);
});
}
process.stdout.write(
c.green + " ✔" + c.reset + " " + name +
" " + c.dim + formatMs(Date.now() - start) + c.reset + "\n"
);
return Promise.resolve();
} catch (err) {
process.stdout.write(c.red + " ✖" + c.reset + " " + name + "\n");
return Promise.reject(err);
}
}
function runTask(name, useParallel) {
var totalStart = Date.now();
console.log("");
console.log(c.bold + "Task: " + name + c.reset);
console.log(c.dim + "─".repeat(40) + c.reset);
var chain;
if (useParallel) {
var groups = parallelGroups(name);
chain = Promise.resolve();
for (var g = 0; g < groups.length; g++) {
(function(group) {
chain = chain.then(function() {
if (group.length === 1) return runOne(group[0]);
return Promise.all(group.map(runOne));
});
})(groups[g]);
}
} else {
var order = resolve(name);
chain = Promise.resolve();
for (var i = 0; i < order.length; i++) {
(function(n) {
chain = chain.then(function() { return runOne(n); });
})(order[i]);
}
}
return chain.then(function() {
console.log(c.dim + "─".repeat(40) + c.reset);
console.log(c.green + c.bold + "✔ Done" + c.reset +
" " + c.dim + formatMs(Date.now() - totalStart) + c.reset + "\n");
}).catch(function(err) {
console.log(c.dim + "─".repeat(40) + c.reset);
console.log(c.red + c.bold + "✖ Failed" + c.reset + " " + err.message + "\n");
process.exit(1);
});
}
// ---- Watch Mode ----
function watchMode(name) {
var order = resolve(name);
var watchMap = {};
var debounceTimer = null;
for (var i = 0; i < order.length; i++) {
var t = tasks[order[i]];
if (t.watch) {
for (var j = 0; j < t.watch.length; j++) {
var dir = t.watch[j].split("/")[0] || ".";
if (!watchMap[dir]) watchMap[dir] = [];
watchMap[dir].push({ task: t.name, pattern: t.watch[j] });
}
}
}
var dirs = Object.keys(watchMap);
if (dirs.length === 0) {
console.log(c.yellow + " No watch patterns defined for " + name + c.reset);
return;
}
console.log(c.magenta + "\n Watching for changes..." + c.reset);
for (var d = 0; d < dirs.length; d++) {
console.log(c.dim + " " + dirs[d] + "/" + c.reset);
}
console.log("");
// Initial run
runTask(name, true).then(function() {
// Set up watchers
for (var d2 = 0; d2 < dirs.length; d2++) {
(function(dir) {
try {
fs.watch(dir, { recursive: true }, function(event, filename) {
if (!filename) return;
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(function() {
console.log(c.magenta + " ↻ Change detected: " + filename + c.reset);
runTask(name, true);
}, 250);
});
} catch (e) {
console.log(c.yellow + " Cannot watch: " + dir + c.reset);
}
})(dirs[d2]);
}
});
}
// ---- CLI ----
function showHelp() {
console.log("");
console.log(" " + c.bold + "runner" + c.reset + " — Task runner CLI");
console.log("");
console.log(" " + c.bold + "Usage:" + c.reset);
console.log(" runner [task] [options]");
console.log("");
console.log(" " + c.bold + "Options:" + c.reset);
console.log(" --list, -l List available tasks");
console.log(" --watch, -w Watch mode");
console.log(" --parallel, -p Run independent tasks in parallel");
console.log(" --graph Show dependency graph");
console.log(" --file, -f Taskfile path (default: Taskfile.js)");
console.log(" --help, -h Show help");
console.log("");
}
function showTaskList() {
var names = Object.keys(tasks);
console.log("");
console.log(" " + c.bold + "Available tasks:" + c.reset);
console.log("");
for (var i = 0; i < names.length; i++) {
var t = tasks[names[i]];
var deps = t.deps.length > 0 ? c.dim + " → " + t.deps.join(", ") + c.reset : "";
var desc = t.desc ? c.dim + " — " + t.desc + c.reset : "";
console.log(" " + c.cyan + t.name + c.reset + deps + desc);
}
console.log("");
}
function showGraph(name) {
var order = resolve(name);
var groups = parallelGroups(name);
console.log("");
console.log(" " + c.bold + "Execution plan for: " + name + c.reset);
console.log("");
for (var g = 0; g < groups.length; g++) {
var label = groups[g].length > 1 ? " (parallel)" : "";
console.log(" " + c.dim + "Step " + (g + 1) + label + ":" + c.reset);
for (var i = 0; i < groups[g].length; i++) {
var t = tasks[groups[g][i]];
var hasAction = t.action ? c.green + "●" : c.dim + "○";
console.log(" " + hasAction + c.reset + " " + groups[g][i]);
}
}
console.log("");
}
// ---- Main ----
function main() {
var args = process.argv.slice(2);
var taskName = "default";
var flags = {};
for (var i = 0; i < args.length; i++) {
var arg = args[i];
if (arg === "--help" || arg === "-h") { flags.help = true; }
else if (arg === "--list" || arg === "-l") { flags.list = true; }
else if (arg === "--watch" || arg === "-w") { flags.watch = true; }
else if (arg === "--parallel" || arg === "-p") { flags.parallel = true; }
else if (arg === "--graph") { flags.graph = true; }
else if ((arg === "--file" || arg === "-f") && args[i + 1]) { flags.file = args[++i]; }
else if (arg.charAt(0) !== "-") { taskName = arg; }
}
if (flags.help) { showHelp(); return; }
// Load Taskfile
var taskfilePath = path.resolve(flags.file || "Taskfile.js");
if (!fs.existsSync(taskfilePath)) {
console.error(c.red + "\n Taskfile not found: " + taskfilePath + c.reset);
console.error(c.dim + " Create a Taskfile.js in your project root\n" + c.reset);
process.exit(1);
}
// Expose API globally for the Taskfile
global.task = task;
global.describe = describe;
global.watchFiles = watchFiles;
global.exec = exec;
require(taskfilePath);
if (flags.list) { showTaskList(); return; }
if (flags.graph) { showGraph(taskName); return; }
if (!tasks[taskName]) {
console.error(c.red + "\n Unknown task: " + taskName + c.reset);
showTaskList();
process.exit(1);
}
if (flags.watch) {
watchMode(taskName);
} else {
runTask(taskName, flags.parallel);
}
}
main();
Example Taskfile.js:
// Taskfile.js
task("clean", function() {
return exec("rm -rf dist");
});
describe("clean", "Remove build output");
task("lint", function() {
return exec("npx eslint src/");
});
describe("lint", "Check code style");
watchFiles("lint", ["src/**/*.js"]);
task("compile", ["lint"], function() {
return exec("npx tsc");
});
describe("compile", "Compile TypeScript");
task("test", ["compile"], function() {
return exec("npx jest");
});
describe("test", "Run tests");
task("build", ["clean", "compile", "test"]);
describe("build", "Full build");
task("default", ["build"]);
Running it:
$ runner --list
Available tasks:
clean → — Remove build output
lint → — Check code style
compile → lint — Compile TypeScript
test → compile — Run tests
build → clean, compile, test — Full build
default → build
$ runner build --parallel --graph
Execution plan for: build
Step 1 (parallel):
● clean
● lint
Step 2:
● compile
Step 3:
● test
Step 4:
○ build
$ runner build --parallel
Task: build
────────────────────────────────────────
▸ clean
✔ clean 42ms
▸ lint
✔ lint 1.23s
▸ compile
✔ compile 3.45s
▸ test
✔ test 2.18s
────────────────────────────────────────
✔ Done 6.92s
Common Issues and Troubleshooting
Circular dependency causes infinite loop
Without cycle detection, a circular dep (A → B → A) causes a stack overflow:
RangeError: Maximum call stack size exceeded
Fix: Track "visiting" nodes during traversal. If you visit a node that is already being visited, you have found a cycle. Throw an error naming the tasks involved.
Task runs multiple times in diamond dependencies
If build depends on both compile and test, and test also depends on compile, then compile runs twice:
Fix: Track completed tasks in the executor. Skip any task that has already run in the current execution.
Watch mode triggers infinite rebuilds
If a task writes output to a watched directory, the write triggers another watch event:
Fix: Ignore output directories in watch patterns. Add a debounce timer (200-300ms) to coalesce rapid file changes into a single rebuild.
Shell commands fail on Windows
Unix commands like rm -rf do not exist on Windows:
Fix: Use cross-platform alternatives or Node.js APIs:
var fs = require("fs");
task("clean", function() {
fs.rmSync("dist", { recursive: true, force: true });
});
Best Practices
- Keep the Taskfile simple. Tasks should be thin wrappers around shell commands or Node.js functions. Complex logic belongs in your source code, not your build file.
- Always detect circular dependencies. A clear error message beats a stack overflow.
- Default to parallel execution. Independent tasks should run concurrently. Only serialize when there are real dependencies.
- Make task output quiet by default. Show task names and timing. Let
--verboseexpose command output. - Support both sync and async tasks. Check if the return value has a
.thenmethod and handle accordingly. - Provide a
--listcommand. Users should not need to read the Taskfile to know what tasks are available.