@idlesummer/tasker 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,17 +1,18 @@
1
1
  # @idlesummer/tasker
2
2
 
3
- A simple, lightweight task pipeline runner with CLI spinners and formatters for Node.js build tools.
3
+ A simple, lightweight declarative task pipeline runner with CLI spinners and formatters for Node.js build tools.
4
4
 
5
5
  > **⚠️ Learning Project Notice**
6
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
7
 
8
8
  ## What's This?
9
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.
10
+ Basically, it's a simple way to run tasks in sequence with nice terminal spinners. The standard imperative way of writing build scripts seemed unelegant and I wanted something more functional/declarative, so I made this. Think of it like a mini task runner - simpler and lighter than Listr2 but more structured than a bash script.
11
11
 
12
12
  ## Features
13
13
 
14
14
  - **Task Pipeline** - Run tasks one after another, each task gets the results from the previous ones
15
+ - **Conditional Tasks** - Use boolean expressions to conditionally include/exclude tasks
15
16
  - **CLI Spinners** - Those satisfying loading spinners powered by ora
16
17
  - **Formatters** - Helper functions to make bytes, durations, and file lists look nice
17
18
  - **TypeScript** - Full type safety so you don't shoot yourself in the foot
@@ -56,7 +57,7 @@ That's it! You get:
56
57
 
57
58
  ## Why Would I Use This?
58
59
 
59
- Good question! Use this if you:
60
+ Use this if you:
60
61
  - Want to build a simple CLI tool or build script
61
62
  - Like seeing spinners while stuff happens
62
63
  - Need tasks to share data between each other
@@ -69,8 +70,6 @@ Don't use this if you:
69
70
 
70
71
  ## Documentation
71
72
 
72
- I wrote pretty detailed docs with a casual tone (because formal docs are boring):
73
-
74
73
  - **[API Reference](./docs/API.md)** - All the functions and types explained
75
74
  - **[Examples](./docs/EXAMPLES.md)** - Real code you can copy-paste
76
75
  - **[Architecture](./docs/ARCHITECTURE.md)** - How it works under the hood
@@ -83,6 +82,7 @@ The `examples/` folder has working projects you can run:
83
82
 
84
83
  - **[basic-pipeline](./examples/basic-pipeline)** - Super simple example to get started
85
84
  - **[build-tool](./examples/build-tool)** - More realistic build tool with file operations
85
+ - **[conditional-tasks](./examples/conditional-tasks)** - Demonstrates conditional task execution
86
86
  - **[formatters](./examples/formatters)** - Shows off all the formatting utilities
87
87
 
88
88
  Each example is a standalone npm package. To run one:
@@ -113,12 +113,12 @@ Create a pipeline with tasks:
113
113
  const pipeline = pipe([
114
114
  {
115
115
  name: 'Task name',
116
+ onSuccess: (ctx, duration) => 'Custom success message',
117
+ onError: (error) => 'Custom error message',
116
118
  run: async (ctx) => {
117
119
  // Do stuff
118
120
  return { key: 'value' } // Updates context
119
121
  },
120
- onSuccess: (ctx, duration) => 'Custom success message',
121
- onError: (error) => 'Custom error message'
122
122
  }
123
123
  ])
124
124
 
@@ -127,6 +127,22 @@ const result = await pipeline.run({})
127
127
  // result.duration is total time in ms
128
128
  ```
129
129
 
130
+ **Conditional Tasks:**
131
+
132
+ You can conditionally include tasks using boolean expressions:
133
+
134
+ ```typescript
135
+ const isProduction = process.env.NODE_ENV === 'production'
136
+
137
+ const pipeline = pipe([
138
+ { name: 'Build', run: async () => ({ built: true }) },
139
+ isProduction && { name: 'Minify', run: async () => ({ minified: true }) },
140
+ { name: 'Deploy', run: async () => ({ deployed: true }) },
141
+ ])
142
+ ```
143
+
144
+ When the condition is `false`, the task is automatically skipped. See the [conditional-tasks example](./examples/conditional-tasks) for a complete demo.
145
+
130
146
  ### Formatters
131
147
 
132
148
  Make numbers pretty:
@@ -161,10 +177,10 @@ const build = pipe([
161
177
  },
162
178
  {
163
179
  name: 'Compile TypeScript',
180
+ onSuccess: () => 'TypeScript compiled successfully',
164
181
  run: async () => {
165
182
  await execAsync('tsc')
166
183
  },
167
- onSuccess: () => 'TypeScript compiled successfully'
168
184
  },
169
185
  {
170
186
  name: 'Show output',
@@ -194,7 +210,7 @@ These might get fixed eventually, or they might not. PRs welcome if you want to
194
210
 
195
211
  ## Contributing
196
212
 
197
- Found a bug? Want to add a feature? Awesome!
213
+ Found a bug? Want to add a feature?
198
214
 
199
215
  1. Check [CONTRIBUTING.md](./docs/CONTRIBUTING.md) for guidelines
200
216
  2. Open an issue or PR
@@ -204,7 +220,7 @@ Even if you're new to open source, feel free to contribute. I'm learning too!
204
220
 
205
221
  ## License
206
222
 
207
- MIT - do whatever you want with this.
223
+ MIT - See [LICENSE](LICENSE) file
208
224
 
209
225
  ## Questions?
210
226
 
package/dist/index.cjs CHANGED
@@ -83,12 +83,14 @@ function fileList(baseDir, pattern = "**/*", width = 45) {
83
83
  /**
84
84
  * Creates a task pipeline that runs tasks sequentially with spinners
85
85
  * @template TContext - The context type for the pipeline
86
+ * @param tasks - Array of tasks (supports conditional spreading with falsy values)
86
87
  */
87
88
  function pipe(tasks) {
88
89
  return { run: async (initialContext) => {
89
90
  const startTime = Date.now();
90
91
  let context = initialContext;
91
92
  for (const task of tasks) {
93
+ if (!task) continue;
92
94
  const taskStart = Date.now();
93
95
  const spinner = (0, ora.default)(task.name).start();
94
96
  try {
@@ -1 +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"}
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 * @param tasks - Array of tasks (supports conditional spreading with falsy values)\r\n */\r\nexport function pipe<TContext extends Context>(tasks: (Task<TContext> | false | null | undefined)[]) {\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 // Skip falsy values (supports conditional spreading)\r\n if (!task) continue\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;;;;;;;;;;ACrBtC,SAAgB,KAA+B,OAAsD;AACnG,QAAO,EACL,KAAK,OAAO,mBAA6B;EACvC,MAAM,YAAY,KAAK,KAAK;EAC5B,IAAI,UAAU;AAEd,OAAK,MAAM,QAAQ,OAAO;AAExB,OAAI,CAAC,KAAM;GACX,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"}
package/dist/index.d.cts CHANGED
@@ -53,8 +53,9 @@ type PipeResult<TContext extends Context> = {
53
53
  /**
54
54
  * Creates a task pipeline that runs tasks sequentially with spinners
55
55
  * @template TContext - The context type for the pipeline
56
+ * @param tasks - Array of tasks (supports conditional spreading with falsy values)
56
57
  */
57
- declare function pipe<TContext extends Context>(tasks: Task<TContext>[]): {
58
+ declare function pipe<TContext extends Context>(tasks: (Task<TContext> | false | null | undefined)[]): {
58
59
  run: (initialContext: TContext) => Promise<PipeResult<TContext>>;
59
60
  };
60
61
  //#endregion
@@ -1 +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"}
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;AAUA;;;AAnBoB,KASR,UAAA,kBAA4B,OAAA;EAAA,OAAA,EAC7B,QAAA;EAAA,QAAA;AAAA;AAAA;AASX;;;;AATW,iBASK,IAAA,kBAAsB,OAAA,CAAA,CAAA,KAAA,GAAiB,IAAA,CAAK,QAAA;EAAA,GAAA,GAAA,cAAA,EAE5B,QAAA,KAAQ,OAAA,CAAA,UAAA,CAAA,QAAA;AAAA"}
package/dist/index.d.mts CHANGED
@@ -53,8 +53,9 @@ type PipeResult<TContext extends Context> = {
53
53
  /**
54
54
  * Creates a task pipeline that runs tasks sequentially with spinners
55
55
  * @template TContext - The context type for the pipeline
56
+ * @param tasks - Array of tasks (supports conditional spreading with falsy values)
56
57
  */
57
- declare function pipe<TContext extends Context>(tasks: Task<TContext>[]): {
58
+ declare function pipe<TContext extends Context>(tasks: (Task<TContext> | false | null | undefined)[]): {
58
59
  run: (initialContext: TContext) => Promise<PipeResult<TContext>>;
59
60
  };
60
61
  //#endregion
@@ -1 +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"}
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;AAUA;;;AAnBoB,KASR,UAAA,kBAA4B,OAAA;EAAA,OAAA,EAC7B,QAAA;EAAA,QAAA;AAAA;AAAA;AASX;;;;AATW,iBASK,IAAA,kBAAsB,OAAA,CAAA,CAAA,KAAA,GAAiB,IAAA,CAAK,QAAA;EAAA,GAAA,GAAA,cAAA,EAE5B,QAAA,KAAQ,OAAA,CAAA,UAAA,CAAA,QAAA;AAAA"}
package/dist/index.mjs CHANGED
@@ -51,12 +51,14 @@ function fileList(baseDir, pattern = "**/*", width = 45) {
51
51
  /**
52
52
  * Creates a task pipeline that runs tasks sequentially with spinners
53
53
  * @template TContext - The context type for the pipeline
54
+ * @param tasks - Array of tasks (supports conditional spreading with falsy values)
54
55
  */
55
56
  function pipe(tasks) {
56
57
  return { run: async (initialContext) => {
57
58
  const startTime = Date.now();
58
59
  let context = initialContext;
59
60
  for (const task of tasks) {
61
+ if (!task) continue;
60
62
  const taskStart = Date.now();
61
63
  const spinner = ora(task.name).start();
62
64
  try {
@@ -1 +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"}
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 * @param tasks - Array of tasks (supports conditional spreading with falsy values)\r\n */\r\nexport function pipe<TContext extends Context>(tasks: (Task<TContext> | false | null | undefined)[]) {\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 // Skip falsy values (supports conditional spreading)\r\n if (!task) continue\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;;;;;;;;;;ACrBtC,SAAgB,KAA+B,OAAsD;AACnG,QAAO,EACL,KAAK,OAAO,mBAA6B;EACvC,MAAM,YAAY,KAAK,KAAK;EAC5B,IAAI,UAAU;AAEd,OAAK,MAAM,QAAQ,OAAO;AAExB,OAAI,CAAC,KAAM;GACX,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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idlesummer/tasker",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A simple task pipeline runner with CLI spinners and formatters",
5
5
  "keywords": [
6
6
  "task",