@idlesummer/tasker 0.1.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/LICENSE ADDED
@@ -0,0 +1,6 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 idlesummer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation...
package/README.md ADDED
@@ -0,0 +1,220 @@
1
+ # @idlesummer/tasker
2
+
3
+ A simple, lightweight task pipeline runner with CLI spinners and formatters for Node.js build tools.
4
+
5
+ > **⚠️ Learning Project Notice**
6
+ > Hey! Just so you know, this is a learning project I built to understand task pipelines and build tools better. It works and I use it for my own stuff, but there might be bugs or rough edges. Feel free to use it, but maybe don't bet your production deploy on it just yet. Contributions and bug reports are super welcome though!
7
+
8
+ ## What's This?
9
+
10
+ Basically, it's a simple way to run tasks in sequence with nice terminal spinners. I got tired of writing the same boilerplate for build scripts, so I made this. Think of it like a mini task runner - simpler than Gulp but more structured than a bash script.
11
+
12
+ ## Features
13
+
14
+ - **Task Pipeline** - Run tasks one after another, each task gets the results from the previous ones
15
+ - **CLI Spinners** - Those satisfying loading spinners powered by ora
16
+ - **Formatters** - Helper functions to make bytes, durations, and file lists look nice
17
+ - **TypeScript** - Full type safety so you don't shoot yourself in the foot
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install @idlesummer/tasker
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ Here's the simplest example:
28
+
29
+ ```typescript
30
+ import { pipe, type Context } from '@idlesummer/tasker'
31
+
32
+ interface MyContext extends Context {
33
+ count?: number
34
+ }
35
+
36
+ const pipeline = pipe<MyContext>([
37
+ {
38
+ name: 'Initialize',
39
+ run: async () => ({ count: 0 }),
40
+ },
41
+ {
42
+ name: 'Process',
43
+ run: async (ctx) => ({ count: (ctx.count ?? 0) + 10 }),
44
+ onSuccess: (ctx) => `Processed (count: ${ctx.count})`,
45
+ },
46
+ ])
47
+
48
+ const result = await pipeline.run({})
49
+ console.log(`Done in ${result.duration}ms`)
50
+ ```
51
+
52
+ That's it! You get:
53
+ - Spinners while tasks run ✨
54
+ - Type-safe context passing between tasks
55
+ - Timing info automatically
56
+
57
+ ## Why Would I Use This?
58
+
59
+ Good question! Use this if you:
60
+ - Want to build a simple CLI tool or build script
61
+ - Like seeing spinners while stuff happens
62
+ - Need tasks to share data between each other
63
+ - Want something lighter than Gulp but more structured than raw scripts
64
+
65
+ Don't use this if you:
66
+ - Need parallel task execution (tasks run sequentially here)
67
+ - Want a mature, battle-tested solution (this is a learning project!)
68
+ - Need a full-featured task runner (look at Gulp, Grunt, etc.)
69
+
70
+ ## Documentation
71
+
72
+ I wrote pretty detailed docs with a casual tone (because formal docs are boring):
73
+
74
+ - **[API Reference](./docs/API.md)** - All the functions and types explained
75
+ - **[Examples](./docs/EXAMPLES.md)** - Real code you can copy-paste
76
+ - **[Architecture](./docs/ARCHITECTURE.md)** - How it works under the hood
77
+ - **[Contributing](./docs/CONTRIBUTING.md)** - Want to help? Start here
78
+ - **[Troubleshooting](./docs/TROUBLESHOOTING.md)** - Common issues and fixes
79
+
80
+ ## Example Projects
81
+
82
+ The `examples/` folder has working projects you can run:
83
+
84
+ - **[basic-pipeline](./examples/basic-pipeline)** - Super simple example to get started
85
+ - **[build-tool](./examples/build-tool)** - More realistic build tool with file operations
86
+ - **[formatters](./examples/formatters)** - Shows off all the formatting utilities
87
+
88
+ Each example is a standalone npm package. To run one:
89
+
90
+ ```bash
91
+ # Clone and setup
92
+ git clone https://github.com/idlesummer/tasker.git
93
+ cd tasker
94
+ npm install
95
+ npm run build
96
+
97
+ # Try an example
98
+ cd examples/basic-pipeline
99
+ npm install
100
+ npm run build
101
+ npm start
102
+ ```
103
+
104
+ See the [examples README](./examples/README.md) for more info.
105
+
106
+ ## Quick API Overview
107
+
108
+ ### Pipeline
109
+
110
+ Create a pipeline with tasks:
111
+
112
+ ```typescript
113
+ const pipeline = pipe([
114
+ {
115
+ name: 'Task name',
116
+ run: async (ctx) => {
117
+ // Do stuff
118
+ return { key: 'value' } // Updates context
119
+ },
120
+ onSuccess: (ctx, duration) => 'Custom success message',
121
+ onError: (error) => 'Custom error message'
122
+ }
123
+ ])
124
+
125
+ const result = await pipeline.run({})
126
+ // result.context has your final context
127
+ // result.duration is total time in ms
128
+ ```
129
+
130
+ ### Formatters
131
+
132
+ Make numbers pretty:
133
+
134
+ ```typescript
135
+ import { bytes, duration, fileList } from '@idlesummer/tasker'
136
+
137
+ console.log(bytes(1234567)) // "1.23 MB"
138
+ console.log(duration(12345)) // "12.3s"
139
+ console.log(fileList('./dist')) // Formatted file list with sizes
140
+ ```
141
+
142
+ Check [API.md](./docs/API.md) for the full reference.
143
+
144
+ ## A More Real Example
145
+
146
+ Here's what a build script might look like:
147
+
148
+ ```typescript
149
+ import { pipe, fileList, duration } from '@idlesummer/tasker'
150
+ import { exec } from 'child_process'
151
+ import { promisify } from 'util'
152
+
153
+ const execAsync = promisify(exec)
154
+
155
+ const build = pipe([
156
+ {
157
+ name: 'Clean dist folder',
158
+ run: async () => {
159
+ await execAsync('rm -rf dist')
160
+ }
161
+ },
162
+ {
163
+ name: 'Compile TypeScript',
164
+ run: async () => {
165
+ await execAsync('tsc')
166
+ },
167
+ onSuccess: () => 'TypeScript compiled successfully'
168
+ },
169
+ {
170
+ name: 'Show output',
171
+ run: async () => {
172
+ console.log('\nBuild output:')
173
+ console.log(fileList('./dist'))
174
+ }
175
+ }
176
+ ])
177
+
178
+ console.log('🏗️ Building...\n')
179
+ const result = await build.run({})
180
+ console.log(`\n✅ Done in ${duration(result.duration)}`)
181
+ ```
182
+
183
+ You get nice spinners for each step, and a clean output at the end.
184
+
185
+ ## Known Issues / Limitations
186
+
187
+ Since this is a learning project:
188
+ - Tasks run sequentially only (no parallel execution yet)
189
+ - No built-in retry logic
190
+ - Error handling is basic (task fails = pipeline stops)
191
+ - Haven't tested with huge codebases
192
+
193
+ These might get fixed eventually, or they might not. PRs welcome if you want to add features!
194
+
195
+ ## Contributing
196
+
197
+ Found a bug? Want to add a feature? Awesome!
198
+
199
+ 1. Check [CONTRIBUTING.md](./docs/CONTRIBUTING.md) for guidelines
200
+ 2. Open an issue or PR
201
+ 3. Be nice (we're all learning here)
202
+
203
+ Even if you're new to open source, feel free to contribute. I'm learning too!
204
+
205
+ ## License
206
+
207
+ MIT - do whatever you want with this.
208
+
209
+ ## Questions?
210
+
211
+ - Check the [docs](./docs/)
212
+ - Look at the [examples](./examples/)
213
+ - Open an issue
214
+ - Read the [troubleshooting guide](./docs/TROUBLESHOOTING.md)
215
+
216
+ ---
217
+
218
+ Made with ☕ and procrastination by [@idlesummer](https://github.com/idlesummer)
219
+
220
+ If this helps you, cool! If you find bugs, let me know. If you want to improve it, send a PR. Let's learn together!
package/dist/index.cjs ADDED
@@ -0,0 +1,123 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) {
13
+ __defProp(to, key, {
14
+ get: ((k) => from[k]).bind(null, key),
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ }
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
23
+ value: mod,
24
+ enumerable: true
25
+ }) : target, mod));
26
+
27
+ //#endregion
28
+ let fs = require("fs");
29
+ let path = require("path");
30
+ let fdir = require("fdir");
31
+ let picocolors = require("picocolors");
32
+ picocolors = __toESM(picocolors);
33
+ let picomatch = require("picomatch");
34
+ picomatch = __toESM(picomatch);
35
+ let pretty_bytes = require("pretty-bytes");
36
+ pretty_bytes = __toESM(pretty_bytes);
37
+ let pretty_ms = require("pretty-ms");
38
+ pretty_ms = __toESM(pretty_ms);
39
+ let ora = require("ora");
40
+ ora = __toESM(ora);
41
+
42
+ //#region src/format.ts
43
+ /**
44
+ * Formats byte sizes into human-readable strings
45
+ * @param size - The size in bytes
46
+ * @returns Formatted string (e.g., "1.23 kB", "4.56 MB")
47
+ */
48
+ const bytes = pretty_bytes.default;
49
+ /**
50
+ * Formats milliseconds into human-readable durations
51
+ * @param ms - The duration in milliseconds
52
+ * @returns Formatted string (e.g., "42ms", "1.2s", "2m 3.5s")
53
+ */
54
+ const duration = pretty_ms.default;
55
+ /**
56
+ * Display a formatted list of files in a directory with their sizes
57
+ * @param baseDir - The base directory to search
58
+ * @param pattern - Glob pattern to match files
59
+ * @param width - Row width for file paths
60
+ * @returns Formatted file list with sizes and total
61
+ */
62
+ function fileList(baseDir, pattern = "**/*", width = 45) {
63
+ const matcher = (0, picomatch.default)(pattern, { windows: true });
64
+ const files = new fdir.fdir().withRelativePaths().filter((path$1) => matcher(path$1)).crawl(baseDir).sync();
65
+ const stats = files.map((path$1) => {
66
+ const fullPath = (0, path.join)(baseDir, path$1);
67
+ return {
68
+ path: path$1.replace(/\\/g, "/"),
69
+ size: (0, fs.statSync)(fullPath).size
70
+ };
71
+ });
72
+ const total = stats.reduce((sum, stat) => sum + stat.size, 0);
73
+ const lines = stats.map(({ path: path$1, size }) => {
74
+ return ` ${picocolors.default.cyan(path$1.padEnd(width))} ${picocolors.default.dim(bytes(size))}`;
75
+ });
76
+ const footerLabel = `${files.length} files, total:`.padEnd(width);
77
+ const footer = ` ${picocolors.default.dim(footerLabel)} ${picocolors.default.dim(bytes(total))}`;
78
+ return [...lines, footer].join("\n");
79
+ }
80
+
81
+ //#endregion
82
+ //#region src/pipeline.ts
83
+ /**
84
+ * Creates a task pipeline that runs tasks sequentially with spinners
85
+ * @template TContext - The context type for the pipeline
86
+ */
87
+ function pipe(tasks) {
88
+ return { run: async (initialContext) => {
89
+ const startTime = Date.now();
90
+ let context = initialContext;
91
+ for (const task of tasks) {
92
+ const taskStart = Date.now();
93
+ const spinner = (0, ora.default)(task.name).start();
94
+ try {
95
+ const updates = await task.run(context) ?? {};
96
+ const duration$2 = Date.now() - taskStart;
97
+ const message = task.onSuccess?.(context, duration$2) ?? task.name;
98
+ spinner.succeed(message);
99
+ context = {
100
+ ...context,
101
+ ...updates
102
+ };
103
+ } catch (error) {
104
+ const err = error instanceof Error ? error : new Error(String(error));
105
+ const message = task.onError?.(err) ?? task.name;
106
+ spinner.fail(message);
107
+ throw new Error(`Task "${task.name}" failed: ${err.message}`, { cause: err });
108
+ }
109
+ }
110
+ const duration$1 = Date.now() - startTime;
111
+ return {
112
+ context,
113
+ duration: duration$1
114
+ };
115
+ } };
116
+ }
117
+
118
+ //#endregion
119
+ exports.bytes = bytes;
120
+ exports.duration = duration;
121
+ exports.fileList = fileList;
122
+ exports.pipe = pipe;
123
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["prettyBytes","prettyMs","path","pc","duration"],"sources":["../src/format.ts","../src/pipeline.ts"],"sourcesContent":["import { statSync } from 'fs'\r\nimport { join } from 'path'\r\nimport { fdir } from 'fdir'\r\nimport pc from 'picocolors'\r\nimport pm from 'picomatch'\r\nimport prettyBytes from 'pretty-bytes'\r\nimport prettyMs from 'pretty-ms'\r\n\r\n/**\r\n * Formats byte sizes into human-readable strings\r\n * @param size - The size in bytes\r\n * @returns Formatted string (e.g., \"1.23 kB\", \"4.56 MB\")\r\n */\r\nexport const bytes = prettyBytes\r\n\r\n/**\r\n * Formats milliseconds into human-readable durations\r\n * @param ms - The duration in milliseconds\r\n * @returns Formatted string (e.g., \"42ms\", \"1.2s\", \"2m 3.5s\")\r\n */\r\nexport const duration = prettyMs\r\n\r\n/**\r\n * Display a formatted list of files in a directory with their sizes\r\n * @param baseDir - The base directory to search\r\n * @param pattern - Glob pattern to match files\r\n * @param width - Row width for file paths\r\n * @returns Formatted file list with sizes and total\r\n */\r\nexport function fileList(baseDir: string, pattern = '**/*', width = 45) {\r\n // Find all files matching the pattern\r\n // Use windows: true option to handle both / and \\ as path separators\r\n const matcher = pm(pattern, { windows: true })\r\n const files = new fdir()\r\n .withRelativePaths()\r\n .filter(path => matcher(path))\r\n .crawl(baseDir)\r\n .sync()\r\n\r\n const stats = files.map(path => {\r\n const fullPath = join(baseDir, path)\r\n const displayPath = path.replace(/\\\\/g, '/')\r\n return {\r\n path: displayPath,\r\n size: statSync(fullPath).size,\r\n }\r\n })\r\n\r\n const total = stats.reduce((sum, stat) => sum + stat.size, 0)\r\n const lines = stats.map(({ path, size }) => {\r\n const paddedPath = pc.cyan(path.padEnd(width))\r\n const formattedSize = pc.dim(bytes(size))\r\n return ` ${paddedPath} ${formattedSize}`\r\n })\r\n\r\n const footerLabel = `${files.length} files, total:`.padEnd(width)\r\n const footer = ` ${pc.dim(footerLabel)} ${pc.dim(bytes(total))}`\r\n return [...lines, footer].join('\\n')\r\n}\r\n","import ora from 'ora'\r\n\r\n/** Base context type - extend this to add your own properties */\r\nexport type Context = Record<string, unknown>\r\n\r\n/**\r\n * A task that runs in a pipeline\r\n * @template TContext - The context type for this task\r\n * @property name - Task name shown in spinner\r\n * @property run - Task implementation - return updates to merge into context\r\n * @property onSuccess - Optional success message (default: task name)\r\n * @property onError - Optional error message (default: task name)\r\n */\r\nexport interface Task<TContext extends Context> {\r\n name: string\r\n run: (ctx: TContext) => Promise<Partial<TContext> | void>\r\n onSuccess?: (ctx: TContext, duration: number) => string\r\n onError?: (error: Error) => string\r\n}\r\n\r\n/**\r\n * Result after running a pipeline\r\n * @template TContext - The context type\r\n * @property context - Final context after all tasks\r\n * @property duration - Total duration in milliseconds\r\n */\r\nexport type PipeResult<TContext extends Context> = {\r\n context: TContext\r\n duration: number\r\n}\r\n\r\n/**\r\n * Creates a task pipeline that runs tasks sequentially with spinners\r\n * @template TContext - The context type for the pipeline\r\n */\r\nexport function pipe<TContext extends Context>(tasks: Task<TContext>[]) {\r\n return {\r\n run: async (initialContext: TContext) => {\r\n const startTime = Date.now()\r\n let context = initialContext\r\n\r\n for (const task of tasks) {\r\n const taskStart = Date.now()\r\n const spinner = ora(task.name).start()\r\n\r\n try {\r\n const updates = await task.run(context) ?? {}\r\n const duration = Date.now() - taskStart\r\n const message = task.onSuccess?.(context, duration) ?? task.name\r\n spinner.succeed(message)\r\n context = { ...context, ...updates }\r\n }\r\n catch (error) {\r\n const err = error instanceof Error ? error : new Error(String(error))\r\n const message = task.onError?.(err) ?? task.name\r\n spinner.fail(message)\r\n throw new Error(`Task \"${task.name}\" failed: ${err.message}`, { cause: err })\r\n }\r\n }\r\n\r\n const duration = Date.now() - startTime\r\n return { context, duration } as PipeResult<TContext>\r\n },\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAaA,MAAa,QAAQA;;;;;;AAOrB,MAAa,WAAWC;;;;;;;;AASxB,SAAgB,SAAS,SAAiB,UAAU,QAAQ,QAAQ,IAAI;CAGtE,MAAM,iCAAa,SAAS,EAAE,SAAS,MAAM,CAAC;CAC9C,MAAM,QAAQ,IAAI,WAAM,CACrB,mBAAmB,CACnB,QAAO,WAAQ,QAAQC,OAAK,CAAC,CAC7B,MAAM,QAAQ,CACd,MAAM;CAET,MAAM,QAAQ,MAAM,KAAI,WAAQ;EAC9B,MAAM,0BAAgB,SAASA,OAAK;AAEpC,SAAO;GACL,MAFkBA,OAAK,QAAQ,OAAO,IAAI;GAG1C,uBAAe,SAAS,CAAC;GAC1B;GACD;CAEF,MAAM,QAAQ,MAAM,QAAQ,KAAK,SAAS,MAAM,KAAK,MAAM,EAAE;CAC7D,MAAM,QAAQ,MAAM,KAAK,EAAE,cAAM,WAAW;AAG1C,SAAO,KAFYC,mBAAG,KAAKD,OAAK,OAAO,MAAM,CAAC,CAEvB,IADDC,mBAAG,IAAI,MAAM,KAAK,CAAC;GAEzC;CAEF,MAAM,cAAc,GAAG,MAAM,OAAO,gBAAgB,OAAO,MAAM;CACjE,MAAM,SAAS,KAAKA,mBAAG,IAAI,YAAY,CAAC,IAAIA,mBAAG,IAAI,MAAM,MAAM,CAAC;AAChE,QAAO,CAAC,GAAG,OAAO,OAAO,CAAC,KAAK,KAAK;;;;;;;;;ACtBtC,SAAgB,KAA+B,OAAyB;AACtE,QAAO,EACL,KAAK,OAAO,mBAA6B;EACvC,MAAM,YAAY,KAAK,KAAK;EAC5B,IAAI,UAAU;AAEd,OAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,YAAY,KAAK,KAAK;GAC5B,MAAM,2BAAc,KAAK,KAAK,CAAC,OAAO;AAEtC,OAAI;IACF,MAAM,UAAU,MAAM,KAAK,IAAI,QAAQ,IAAI,EAAE;IAC7C,MAAMC,aAAW,KAAK,KAAK,GAAG;IAC9B,MAAM,UAAU,KAAK,YAAY,SAASA,WAAS,IAAI,KAAK;AAC5D,YAAQ,QAAQ,QAAQ;AACxB,cAAU;KAAE,GAAG;KAAS,GAAG;KAAS;YAE/B,OAAO;IACZ,MAAM,MAAM,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC;IACrE,MAAM,UAAU,KAAK,UAAU,IAAI,IAAI,KAAK;AAC5C,YAAQ,KAAK,QAAQ;AACrB,UAAM,IAAI,MAAM,SAAS,KAAK,KAAK,YAAY,IAAI,WAAW,EAAE,OAAO,KAAK,CAAC;;;EAIjF,MAAMA,aAAW,KAAK,KAAK,GAAG;AAC9B,SAAO;GAAE;GAAS;GAAU;IAE/B"}
@@ -0,0 +1,62 @@
1
+ import prettyBytes from "pretty-bytes";
2
+ import prettyMs from "pretty-ms";
3
+
4
+ //#region src/format.d.ts
5
+ /**
6
+ * Formats byte sizes into human-readable strings
7
+ * @param size - The size in bytes
8
+ * @returns Formatted string (e.g., "1.23 kB", "4.56 MB")
9
+ */
10
+ declare const bytes: typeof prettyBytes;
11
+ /**
12
+ * Formats milliseconds into human-readable durations
13
+ * @param ms - The duration in milliseconds
14
+ * @returns Formatted string (e.g., "42ms", "1.2s", "2m 3.5s")
15
+ */
16
+ declare const duration: typeof prettyMs;
17
+ /**
18
+ * Display a formatted list of files in a directory with their sizes
19
+ * @param baseDir - The base directory to search
20
+ * @param pattern - Glob pattern to match files
21
+ * @param width - Row width for file paths
22
+ * @returns Formatted file list with sizes and total
23
+ */
24
+ declare function fileList(baseDir: string, pattern?: string, width?: number): string;
25
+ //#endregion
26
+ //#region src/pipeline.d.ts
27
+ /** Base context type - extend this to add your own properties */
28
+ type Context = Record<string, unknown>;
29
+ /**
30
+ * A task that runs in a pipeline
31
+ * @template TContext - The context type for this task
32
+ * @property name - Task name shown in spinner
33
+ * @property run - Task implementation - return updates to merge into context
34
+ * @property onSuccess - Optional success message (default: task name)
35
+ * @property onError - Optional error message (default: task name)
36
+ */
37
+ interface Task<TContext extends Context> {
38
+ name: string;
39
+ run: (ctx: TContext) => Promise<Partial<TContext> | void>;
40
+ onSuccess?: (ctx: TContext, duration: number) => string;
41
+ onError?: (error: Error) => string;
42
+ }
43
+ /**
44
+ * Result after running a pipeline
45
+ * @template TContext - The context type
46
+ * @property context - Final context after all tasks
47
+ * @property duration - Total duration in milliseconds
48
+ */
49
+ type PipeResult<TContext extends Context> = {
50
+ context: TContext;
51
+ duration: number;
52
+ };
53
+ /**
54
+ * Creates a task pipeline that runs tasks sequentially with spinners
55
+ * @template TContext - The context type for the pipeline
56
+ */
57
+ declare function pipe<TContext extends Context>(tasks: Task<TContext>[]): {
58
+ run: (initialContext: TContext) => Promise<PipeResult<TContext>>;
59
+ };
60
+ //#endregion
61
+ export { Context, PipeResult, Task, bytes, duration, fileList, pipe };
62
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/format.ts","../src/pipeline.ts"],"mappings":";;;;;AAaA;AAOA;AASA;;cAhBa,KAAA,SAAK,WAAA;AAAA;AAOlB;AASA;;;AAhBkB,cAOL,QAAA,SAAQ,QAAA;AAAA;AASrB;;;;AC1BA;AAUA;ADOqB,iBASL,QAAA,CAAA,OAAA,UAAA,OAAA,WAAA,KAAA;;;;KC1BJ,OAAA,GAAU,MAAA;AAAA;AAUtB;;;;;;;AAVsB,UAUL,IAAA,kBAAsB,OAAA;EAAA,IAAA;EAAA,GAAA,GAAA,GAAA,EAE1B,QAAA,KAAa,OAAA,CAAQ,OAAA,CAAQ,QAAA;EAAA,SAAA,IAAA,GAAA,EACtB,QAAA,EAAA,QAAA;EAAA,OAAA,IAAA,KAAA,EACA,KAAA;AAAA;AAAA;;AASpB;AASA;;;AAlBoB,KASR,UAAA,kBAA4B,OAAA;EAAA,OAAA,EAC7B,QAAA;EAAA,QAAA;AAAA;AAAA;AAQX;;;AARW,iBAQK,IAAA,kBAAsB,OAAA,CAAA,CAAA,KAAA,EAAgB,IAAA,CAAK,QAAA;EAAA,GAAA,GAAA,cAAA,EAE3B,QAAA,KAAQ,OAAA,CAAA,UAAA,CAAA,QAAA;AAAA"}
@@ -0,0 +1,62 @@
1
+ import prettyBytes from "pretty-bytes";
2
+ import prettyMs from "pretty-ms";
3
+
4
+ //#region src/format.d.ts
5
+ /**
6
+ * Formats byte sizes into human-readable strings
7
+ * @param size - The size in bytes
8
+ * @returns Formatted string (e.g., "1.23 kB", "4.56 MB")
9
+ */
10
+ declare const bytes: typeof prettyBytes;
11
+ /**
12
+ * Formats milliseconds into human-readable durations
13
+ * @param ms - The duration in milliseconds
14
+ * @returns Formatted string (e.g., "42ms", "1.2s", "2m 3.5s")
15
+ */
16
+ declare const duration: typeof prettyMs;
17
+ /**
18
+ * Display a formatted list of files in a directory with their sizes
19
+ * @param baseDir - The base directory to search
20
+ * @param pattern - Glob pattern to match files
21
+ * @param width - Row width for file paths
22
+ * @returns Formatted file list with sizes and total
23
+ */
24
+ declare function fileList(baseDir: string, pattern?: string, width?: number): string;
25
+ //#endregion
26
+ //#region src/pipeline.d.ts
27
+ /** Base context type - extend this to add your own properties */
28
+ type Context = Record<string, unknown>;
29
+ /**
30
+ * A task that runs in a pipeline
31
+ * @template TContext - The context type for this task
32
+ * @property name - Task name shown in spinner
33
+ * @property run - Task implementation - return updates to merge into context
34
+ * @property onSuccess - Optional success message (default: task name)
35
+ * @property onError - Optional error message (default: task name)
36
+ */
37
+ interface Task<TContext extends Context> {
38
+ name: string;
39
+ run: (ctx: TContext) => Promise<Partial<TContext> | void>;
40
+ onSuccess?: (ctx: TContext, duration: number) => string;
41
+ onError?: (error: Error) => string;
42
+ }
43
+ /**
44
+ * Result after running a pipeline
45
+ * @template TContext - The context type
46
+ * @property context - Final context after all tasks
47
+ * @property duration - Total duration in milliseconds
48
+ */
49
+ type PipeResult<TContext extends Context> = {
50
+ context: TContext;
51
+ duration: number;
52
+ };
53
+ /**
54
+ * Creates a task pipeline that runs tasks sequentially with spinners
55
+ * @template TContext - The context type for the pipeline
56
+ */
57
+ declare function pipe<TContext extends Context>(tasks: Task<TContext>[]): {
58
+ run: (initialContext: TContext) => Promise<PipeResult<TContext>>;
59
+ };
60
+ //#endregion
61
+ export { Context, PipeResult, Task, bytes, duration, fileList, pipe };
62
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/format.ts","../src/pipeline.ts"],"mappings":";;;;;AAaA;AAOA;AASA;;cAhBa,KAAA,SAAK,WAAA;AAAA;AAOlB;AASA;;;AAhBkB,cAOL,QAAA,SAAQ,QAAA;AAAA;AASrB;;;;AC1BA;AAUA;ADOqB,iBASL,QAAA,CAAA,OAAA,UAAA,OAAA,WAAA,KAAA;;;;KC1BJ,OAAA,GAAU,MAAA;AAAA;AAUtB;;;;;;;AAVsB,UAUL,IAAA,kBAAsB,OAAA;EAAA,IAAA;EAAA,GAAA,GAAA,GAAA,EAE1B,QAAA,KAAa,OAAA,CAAQ,OAAA,CAAQ,QAAA;EAAA,SAAA,IAAA,GAAA,EACtB,QAAA,EAAA,QAAA;EAAA,OAAA,IAAA,KAAA,EACA,KAAA;AAAA;AAAA;;AASpB;AASA;;;AAlBoB,KASR,UAAA,kBAA4B,OAAA;EAAA,OAAA,EAC7B,QAAA;EAAA,QAAA;AAAA;AAAA;AAQX;;;AARW,iBAQK,IAAA,kBAAsB,OAAA,CAAA,CAAA,KAAA,EAAgB,IAAA,CAAK,QAAA;EAAA,GAAA,GAAA,cAAA,EAE3B,QAAA,KAAQ,OAAA,CAAA,UAAA,CAAA,QAAA;AAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,88 @@
1
+ import { statSync } from "fs";
2
+ import { join } from "path";
3
+ import { fdir } from "fdir";
4
+ import pc from "picocolors";
5
+ import pm from "picomatch";
6
+ import prettyBytes from "pretty-bytes";
7
+ import prettyMs from "pretty-ms";
8
+ import ora from "ora";
9
+
10
+ //#region src/format.ts
11
+ /**
12
+ * Formats byte sizes into human-readable strings
13
+ * @param size - The size in bytes
14
+ * @returns Formatted string (e.g., "1.23 kB", "4.56 MB")
15
+ */
16
+ const bytes = prettyBytes;
17
+ /**
18
+ * Formats milliseconds into human-readable durations
19
+ * @param ms - The duration in milliseconds
20
+ * @returns Formatted string (e.g., "42ms", "1.2s", "2m 3.5s")
21
+ */
22
+ const duration = prettyMs;
23
+ /**
24
+ * Display a formatted list of files in a directory with their sizes
25
+ * @param baseDir - The base directory to search
26
+ * @param pattern - Glob pattern to match files
27
+ * @param width - Row width for file paths
28
+ * @returns Formatted file list with sizes and total
29
+ */
30
+ function fileList(baseDir, pattern = "**/*", width = 45) {
31
+ const matcher = pm(pattern, { windows: true });
32
+ const files = new fdir().withRelativePaths().filter((path) => matcher(path)).crawl(baseDir).sync();
33
+ const stats = files.map((path) => {
34
+ const fullPath = join(baseDir, path);
35
+ return {
36
+ path: path.replace(/\\/g, "/"),
37
+ size: statSync(fullPath).size
38
+ };
39
+ });
40
+ const total = stats.reduce((sum, stat) => sum + stat.size, 0);
41
+ const lines = stats.map(({ path, size }) => {
42
+ return ` ${pc.cyan(path.padEnd(width))} ${pc.dim(bytes(size))}`;
43
+ });
44
+ const footerLabel = `${files.length} files, total:`.padEnd(width);
45
+ const footer = ` ${pc.dim(footerLabel)} ${pc.dim(bytes(total))}`;
46
+ return [...lines, footer].join("\n");
47
+ }
48
+
49
+ //#endregion
50
+ //#region src/pipeline.ts
51
+ /**
52
+ * Creates a task pipeline that runs tasks sequentially with spinners
53
+ * @template TContext - The context type for the pipeline
54
+ */
55
+ function pipe(tasks) {
56
+ return { run: async (initialContext) => {
57
+ const startTime = Date.now();
58
+ let context = initialContext;
59
+ for (const task of tasks) {
60
+ const taskStart = Date.now();
61
+ const spinner = ora(task.name).start();
62
+ try {
63
+ const updates = await task.run(context) ?? {};
64
+ const duration$2 = Date.now() - taskStart;
65
+ const message = task.onSuccess?.(context, duration$2) ?? task.name;
66
+ spinner.succeed(message);
67
+ context = {
68
+ ...context,
69
+ ...updates
70
+ };
71
+ } catch (error) {
72
+ const err = error instanceof Error ? error : new Error(String(error));
73
+ const message = task.onError?.(err) ?? task.name;
74
+ spinner.fail(message);
75
+ throw new Error(`Task "${task.name}" failed: ${err.message}`, { cause: err });
76
+ }
77
+ }
78
+ const duration$1 = Date.now() - startTime;
79
+ return {
80
+ context,
81
+ duration: duration$1
82
+ };
83
+ } };
84
+ }
85
+
86
+ //#endregion
87
+ export { bytes, duration, fileList, pipe };
88
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["duration"],"sources":["../src/format.ts","../src/pipeline.ts"],"sourcesContent":["import { statSync } from 'fs'\r\nimport { join } from 'path'\r\nimport { fdir } from 'fdir'\r\nimport pc from 'picocolors'\r\nimport pm from 'picomatch'\r\nimport prettyBytes from 'pretty-bytes'\r\nimport prettyMs from 'pretty-ms'\r\n\r\n/**\r\n * Formats byte sizes into human-readable strings\r\n * @param size - The size in bytes\r\n * @returns Formatted string (e.g., \"1.23 kB\", \"4.56 MB\")\r\n */\r\nexport const bytes = prettyBytes\r\n\r\n/**\r\n * Formats milliseconds into human-readable durations\r\n * @param ms - The duration in milliseconds\r\n * @returns Formatted string (e.g., \"42ms\", \"1.2s\", \"2m 3.5s\")\r\n */\r\nexport const duration = prettyMs\r\n\r\n/**\r\n * Display a formatted list of files in a directory with their sizes\r\n * @param baseDir - The base directory to search\r\n * @param pattern - Glob pattern to match files\r\n * @param width - Row width for file paths\r\n * @returns Formatted file list with sizes and total\r\n */\r\nexport function fileList(baseDir: string, pattern = '**/*', width = 45) {\r\n // Find all files matching the pattern\r\n // Use windows: true option to handle both / and \\ as path separators\r\n const matcher = pm(pattern, { windows: true })\r\n const files = new fdir()\r\n .withRelativePaths()\r\n .filter(path => matcher(path))\r\n .crawl(baseDir)\r\n .sync()\r\n\r\n const stats = files.map(path => {\r\n const fullPath = join(baseDir, path)\r\n const displayPath = path.replace(/\\\\/g, '/')\r\n return {\r\n path: displayPath,\r\n size: statSync(fullPath).size,\r\n }\r\n })\r\n\r\n const total = stats.reduce((sum, stat) => sum + stat.size, 0)\r\n const lines = stats.map(({ path, size }) => {\r\n const paddedPath = pc.cyan(path.padEnd(width))\r\n const formattedSize = pc.dim(bytes(size))\r\n return ` ${paddedPath} ${formattedSize}`\r\n })\r\n\r\n const footerLabel = `${files.length} files, total:`.padEnd(width)\r\n const footer = ` ${pc.dim(footerLabel)} ${pc.dim(bytes(total))}`\r\n return [...lines, footer].join('\\n')\r\n}\r\n","import ora from 'ora'\r\n\r\n/** Base context type - extend this to add your own properties */\r\nexport type Context = Record<string, unknown>\r\n\r\n/**\r\n * A task that runs in a pipeline\r\n * @template TContext - The context type for this task\r\n * @property name - Task name shown in spinner\r\n * @property run - Task implementation - return updates to merge into context\r\n * @property onSuccess - Optional success message (default: task name)\r\n * @property onError - Optional error message (default: task name)\r\n */\r\nexport interface Task<TContext extends Context> {\r\n name: string\r\n run: (ctx: TContext) => Promise<Partial<TContext> | void>\r\n onSuccess?: (ctx: TContext, duration: number) => string\r\n onError?: (error: Error) => string\r\n}\r\n\r\n/**\r\n * Result after running a pipeline\r\n * @template TContext - The context type\r\n * @property context - Final context after all tasks\r\n * @property duration - Total duration in milliseconds\r\n */\r\nexport type PipeResult<TContext extends Context> = {\r\n context: TContext\r\n duration: number\r\n}\r\n\r\n/**\r\n * Creates a task pipeline that runs tasks sequentially with spinners\r\n * @template TContext - The context type for the pipeline\r\n */\r\nexport function pipe<TContext extends Context>(tasks: Task<TContext>[]) {\r\n return {\r\n run: async (initialContext: TContext) => {\r\n const startTime = Date.now()\r\n let context = initialContext\r\n\r\n for (const task of tasks) {\r\n const taskStart = Date.now()\r\n const spinner = ora(task.name).start()\r\n\r\n try {\r\n const updates = await task.run(context) ?? {}\r\n const duration = Date.now() - taskStart\r\n const message = task.onSuccess?.(context, duration) ?? task.name\r\n spinner.succeed(message)\r\n context = { ...context, ...updates }\r\n }\r\n catch (error) {\r\n const err = error instanceof Error ? error : new Error(String(error))\r\n const message = task.onError?.(err) ?? task.name\r\n spinner.fail(message)\r\n throw new Error(`Task \"${task.name}\" failed: ${err.message}`, { cause: err })\r\n }\r\n }\r\n\r\n const duration = Date.now() - startTime\r\n return { context, duration } as PipeResult<TContext>\r\n },\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;AAaA,MAAa,QAAQ;;;;;;AAOrB,MAAa,WAAW;;;;;;;;AASxB,SAAgB,SAAS,SAAiB,UAAU,QAAQ,QAAQ,IAAI;CAGtE,MAAM,UAAU,GAAG,SAAS,EAAE,SAAS,MAAM,CAAC;CAC9C,MAAM,QAAQ,IAAI,MAAM,CACrB,mBAAmB,CACnB,QAAO,SAAQ,QAAQ,KAAK,CAAC,CAC7B,MAAM,QAAQ,CACd,MAAM;CAET,MAAM,QAAQ,MAAM,KAAI,SAAQ;EAC9B,MAAM,WAAW,KAAK,SAAS,KAAK;AAEpC,SAAO;GACL,MAFkB,KAAK,QAAQ,OAAO,IAAI;GAG1C,MAAM,SAAS,SAAS,CAAC;GAC1B;GACD;CAEF,MAAM,QAAQ,MAAM,QAAQ,KAAK,SAAS,MAAM,KAAK,MAAM,EAAE;CAC7D,MAAM,QAAQ,MAAM,KAAK,EAAE,MAAM,WAAW;AAG1C,SAAO,KAFY,GAAG,KAAK,KAAK,OAAO,MAAM,CAAC,CAEvB,IADD,GAAG,IAAI,MAAM,KAAK,CAAC;GAEzC;CAEF,MAAM,cAAc,GAAG,MAAM,OAAO,gBAAgB,OAAO,MAAM;CACjE,MAAM,SAAS,KAAK,GAAG,IAAI,YAAY,CAAC,IAAI,GAAG,IAAI,MAAM,MAAM,CAAC;AAChE,QAAO,CAAC,GAAG,OAAO,OAAO,CAAC,KAAK,KAAK;;;;;;;;;ACtBtC,SAAgB,KAA+B,OAAyB;AACtE,QAAO,EACL,KAAK,OAAO,mBAA6B;EACvC,MAAM,YAAY,KAAK,KAAK;EAC5B,IAAI,UAAU;AAEd,OAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,YAAY,KAAK,KAAK;GAC5B,MAAM,UAAU,IAAI,KAAK,KAAK,CAAC,OAAO;AAEtC,OAAI;IACF,MAAM,UAAU,MAAM,KAAK,IAAI,QAAQ,IAAI,EAAE;IAC7C,MAAMA,aAAW,KAAK,KAAK,GAAG;IAC9B,MAAM,UAAU,KAAK,YAAY,SAASA,WAAS,IAAI,KAAK;AAC5D,YAAQ,QAAQ,QAAQ;AACxB,cAAU;KAAE,GAAG;KAAS,GAAG;KAAS;YAE/B,OAAO;IACZ,MAAM,MAAM,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC;IACrE,MAAM,UAAU,KAAK,UAAU,IAAI,IAAI,KAAK;AAC5C,YAAQ,KAAK,QAAQ;AACrB,UAAM,IAAI,MAAM,SAAS,KAAK,KAAK,YAAY,IAAI,WAAW,EAAE,OAAO,KAAK,CAAC;;;EAIjF,MAAMA,aAAW,KAAK,KAAK,GAAG;AAC9B,SAAO;GAAE;GAAS;GAAU;IAE/B"}
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@idlesummer/tasker",
3
+ "version": "0.1.0",
4
+ "description": "A simple task pipeline runner with CLI spinners and formatters",
5
+ "keywords": [
6
+ "task",
7
+ "pipeline",
8
+ "build",
9
+ "runner",
10
+ "spinner",
11
+ "cli"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/idlesummer/tasker.git"
16
+ },
17
+ "license": "MIT",
18
+ "author": "idlesummer <09louisthebob@gmail.com> (https://github.com/idlesummer)",
19
+ "type": "module",
20
+ "exports": {
21
+ ".": {
22
+ "import": {
23
+ "types": "./dist/index.d.mts",
24
+ "default": "./dist/index.mjs"
25
+ },
26
+ "require": {
27
+ "types": "./dist/index.d.cts",
28
+ "default": "./dist/index.cjs"
29
+ }
30
+ }
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "workspaces": [
36
+ "examples/*"
37
+ ],
38
+ "scripts": {
39
+ "build": "tsdown",
40
+ "lint": "eslint",
41
+ "prepare": "npm run build",
42
+ "test": "vitest run",
43
+ "test:coverage": "vitest run --coverage",
44
+ "test:ui": "vitest --ui",
45
+ "test:watch": "vitest"
46
+ },
47
+ "dependencies": {
48
+ "fdir": "^6.4.2",
49
+ "ora": "^8.1.1",
50
+ "picocolors": "^1.1.1",
51
+ "picomatch": "^4.0.2",
52
+ "pretty-bytes": "^6.1.1",
53
+ "pretty-ms": "^9.2.0"
54
+ },
55
+ "devDependencies": {
56
+ "@eslint/js": "^9.39.2",
57
+ "@types/node": "^22.10.5",
58
+ "@types/picomatch": "^3.0.1",
59
+ "@vitest/ui": "^4.0.17",
60
+ "eslint": "^9.39.2",
61
+ "globals": "^17.0.0",
62
+ "tsdown": "^0.20.0-beta.3",
63
+ "tsx": "^4.21.0",
64
+ "typescript": "^5.7.2",
65
+ "typescript-eslint": "^8.53.0",
66
+ "vitest": "^4.0.17"
67
+ },
68
+ "engines": {
69
+ "node": ">=18"
70
+ }
71
+ }