@nathievzm/lumi 1.0.1 → 1.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/.oxfmtrc.json CHANGED
@@ -1,39 +1,39 @@
1
- {
2
- "$schema": "./node_modules/oxfmt/configuration_schema.json",
3
- "arrowParens": "avoid",
4
- "bracketSameLine": true,
5
- "bracketSpacing": true,
6
- "embeddedLanguageFormatting": "auto",
7
- "endOfLine": "lf",
8
- "insertFinalNewline": true,
9
- "jsdoc": {
10
- "bracketSpacing": true,
11
- "lineWrappingStyle": "balance",
12
- "separateReturnsFromParam": true,
13
- "capitalizeDescriptions": true,
14
- "descriptionWithDot": false,
15
- "commentLineStrategy": "multiline"
16
- },
17
- "objectWrap": "preserve",
18
- "printWidth": 120,
19
- "proseWrap": "always",
20
- "quoteProps": "consistent",
21
- "semi": false,
22
- "singleQuote": true,
23
- "sortImports": {
24
- "newlinesBetween": true
25
- },
26
- "sortPackageJson": {
27
- "sortScripts": true
28
- },
29
- "tabWidth": 4,
30
- "trailingComma": "none",
31
- "overrides": [
32
- {
33
- "files": ["*.yml", "*.yaml"],
34
- "options": {
35
- "tabWidth": 2
36
- }
37
- }
38
- ]
39
- }
1
+ {
2
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
3
+ "arrowParens": "avoid",
4
+ "bracketSameLine": true,
5
+ "bracketSpacing": true,
6
+ "embeddedLanguageFormatting": "auto",
7
+ "endOfLine": "lf",
8
+ "insertFinalNewline": true,
9
+ "jsdoc": {
10
+ "bracketSpacing": true,
11
+ "lineWrappingStyle": "balance",
12
+ "separateReturnsFromParam": true,
13
+ "capitalizeDescriptions": true,
14
+ "descriptionWithDot": false,
15
+ "commentLineStrategy": "multiline"
16
+ },
17
+ "objectWrap": "preserve",
18
+ "printWidth": 120,
19
+ "proseWrap": "always",
20
+ "quoteProps": "consistent",
21
+ "semi": false,
22
+ "singleQuote": true,
23
+ "sortImports": {
24
+ "newlinesBetween": true
25
+ },
26
+ "sortPackageJson": {
27
+ "sortScripts": true
28
+ },
29
+ "tabWidth": 4,
30
+ "trailingComma": "none",
31
+ "overrides": [
32
+ {
33
+ "files": ["*.yml", "*.yaml"],
34
+ "options": {
35
+ "tabWidth": 2
36
+ }
37
+ }
38
+ ]
39
+ }
package/.oxlintrc.json CHANGED
@@ -1,35 +1,36 @@
1
- {
2
- "$schema": "./node_modules/oxlint/configuration_schema.json",
3
- "plugins": ["typescript", "unicorn", "oxc", "eslint", "import", "jsdoc", "node", "promise"],
4
- "categories": {
5
- "correctness": "error",
6
- "suspicious": "error",
7
- "pedantic": "warn",
8
- "perf": "warn",
9
- "style": "warn",
10
- "restriction": "off",
11
- "nursery": "off"
12
- },
13
- "rules": {
14
- "no-nodejs-modules": "off",
15
- "no-magic-numbers": "off",
16
- "no-ternary": "off",
17
- "sort-imports": ["warn", { "ignoreDeclarationSort": true }],
18
- "consistent-type-specifier-style": ["warn", "prefer-inline"],
19
- "prefer-default-export": "off",
20
- "no-named-export": "off",
21
- "prefer-readonly-parameter-types": ["warn", { "ignoreInferredTypes": true }],
22
- "group-exports": "off",
23
- "unicorn/no-useless-undefined": "off",
24
- "no-continue": "off",
25
- "require-param-type": "off",
26
- "require-returns-type": "off"
27
- },
28
- "env": {
29
- "builtin": true
30
- },
31
- "options": {
32
- "typeAware": true,
33
- "typeCheck": true
34
- }
35
- }
1
+ {
2
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
3
+ "plugins": ["typescript", "unicorn", "oxc", "eslint", "import", "jsdoc", "node", "promise"],
4
+ "categories": {
5
+ "correctness": "error",
6
+ "suspicious": "error",
7
+ "pedantic": "warn",
8
+ "perf": "warn",
9
+ "style": "warn",
10
+ "restriction": "off",
11
+ "nursery": "off"
12
+ },
13
+ "rules": {
14
+ "no-nodejs-modules": "off",
15
+ "no-magic-numbers": "off",
16
+ "no-ternary": "off",
17
+ "sort-imports": ["warn", { "ignoreDeclarationSort": true }],
18
+ "consistent-type-specifier-style": ["warn", "prefer-inline"],
19
+ "prefer-default-export": "off",
20
+ "no-named-export": "off",
21
+ "prefer-readonly-parameter-types": ["warn", { "ignoreInferredTypes": true }],
22
+ "group-exports": "off",
23
+ "unicorn/no-useless-undefined": "off",
24
+ "no-continue": "off",
25
+ "require-param-type": "off",
26
+ "require-returns-type": "off",
27
+ "max-dependencies": ["warn", { "max": 12 }]
28
+ },
29
+ "env": {
30
+ "builtin": true
31
+ },
32
+ "options": {
33
+ "typeAware": true,
34
+ "typeCheck": true
35
+ }
36
+ }
@@ -1,36 +1,36 @@
1
- {
2
- // Use IntelliSense to learn about possible attributes.
3
- // Hover to view descriptions of existing attributes.
4
- // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5
- "version": "0.2.0",
6
- "configurations": [
7
- {
8
- "type": "bun",
9
- "internalConsoleOptions": "neverOpen",
10
- "request": "launch",
11
- "name": "Debug File",
12
- "program": "${file}",
13
- "cwd": "${workspaceFolder}",
14
- "stopOnEntry": false,
15
- "watchMode": false
16
- },
17
- {
18
- "type": "bun",
19
- "internalConsoleOptions": "neverOpen",
20
- "request": "launch",
21
- "name": "Run File",
22
- "program": "${file}",
23
- "cwd": "${workspaceFolder}",
24
- "noDebug": true,
25
- "watchMode": false
26
- },
27
- {
28
- "type": "bun",
29
- "internalConsoleOptions": "neverOpen",
30
- "request": "attach",
31
- "name": "Attach Bun",
32
- "url": "ws://localhost:6499/",
33
- "stopOnEntry": false
34
- }
35
- ]
36
- }
1
+ {
2
+ // Use IntelliSense to learn about possible attributes.
3
+ // Hover to view descriptions of existing attributes.
4
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5
+ "version": "0.2.0",
6
+ "configurations": [
7
+ {
8
+ "type": "bun",
9
+ "internalConsoleOptions": "neverOpen",
10
+ "request": "launch",
11
+ "name": "Debug File",
12
+ "program": "${file}",
13
+ "cwd": "${workspaceFolder}",
14
+ "stopOnEntry": false,
15
+ "watchMode": false
16
+ },
17
+ {
18
+ "type": "bun",
19
+ "internalConsoleOptions": "neverOpen",
20
+ "request": "launch",
21
+ "name": "Run File",
22
+ "program": "${file}",
23
+ "cwd": "${workspaceFolder}",
24
+ "noDebug": true,
25
+ "watchMode": false
26
+ },
27
+ {
28
+ "type": "bun",
29
+ "internalConsoleOptions": "neverOpen",
30
+ "request": "attach",
31
+ "name": "Attach Bun",
32
+ "url": "ws://localhost:6499/",
33
+ "stopOnEntry": false
34
+ }
35
+ ]
36
+ }
package/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ # [1.1.0](https://github.com/nathievzm/lumi/compare/v1.0.1...v1.1.0) (2026-05-12)
2
+
3
+ ### Features
4
+
5
+ - add colors to logs ([#44](https://github.com/nathievzm/lumi/issues/44))
6
+ ([2dcca2e](https://github.com/nathievzm/lumi/commit/2dcca2e3df40ac577ca5fdaeddbaedecd67d3256))
7
+ - add recursive arg to toggle the recursiveness when reading the input folder
8
+ ([#25](https://github.com/nathievzm/lumi/issues/25))
9
+ ([3644d34](https://github.com/nathievzm/lumi/commit/3644d346569aa40a2d6b7007ece64be6b787dc42))
10
+ - clear console on start and add a cute banner at the beginning
11
+ ([1ad4ade](https://github.com/nathievzm/lumi/commit/1ad4adef1090224915b724374f0e537f371a93a7))
12
+ - **image, index:** make getImages function pure by removing any side effect
13
+ ([addea55](https://github.com/nathievzm/lumi/commit/addea5542f6b923c36d093797d0b9387c199cfc3))
14
+ - **index:** replace spinnies with clack spinner for cleaner cli ([#42](https://github.com/nathievzm/lumi/issues/42))
15
+ ([6c59c5a](https://github.com/nathievzm/lumi/commit/6c59c5a62af9d3ac13c94997f063842591f9a799))
16
+ - show the seconds that took to process the files in the result log ([#43](https://github.com/nathievzm/lumi/issues/43))
17
+ ([439482e](https://github.com/nathievzm/lumi/commit/439482e075af4aaba70da2967743250456b35b20))
18
+
19
+ ### Performance Improvements
20
+
21
+ - optimize path parsing by using extname and basename instead of parse
22
+ ([962299f](https://github.com/nathievzm/lumi/commit/962299ffc519826d1c023275290525d92b7d4db2))
23
+
1
24
  ## [1.0.1](https://github.com/nathievzm/lumi/compare/v1.0.0...v1.0.1) (2026-05-10)
2
25
 
3
26
  ### Bug Fixes
package/README.md CHANGED
@@ -7,6 +7,7 @@ A fast, interactive CLI tool for batch image processing. Resize, convert, and op
7
7
 
8
8
  - **Batch Processing:** Process hundreds of images in seconds with high concurrency.
9
9
  - **Interactive UI:** User-friendly prompts for missing configurations using `@clack/prompts`.
10
+ - **Recursive Processing:** Automatically finds all images in the input folder and its subdirectories.
10
11
  - **Progress Tracking:** Real-time feedback with `spinnies` progress indicators.
11
12
  - **Smart Resizing:** Automatically fits images while maintaining aspect ratio (`contain` fit).
12
13
  - **Multi-Format Support:** Convert between all formats supported by Sharp (WebP, PNG, JPEG, GIF, AVIF, etc.).
@@ -38,9 +39,14 @@ lumi [options]
38
39
 
39
40
  ### Interactive Mode
40
41
 
41
- If you run **lumi** without providing required flags (like input/output folders or dimensions), it will guide you
42
- through the configuration using cute, interactive prompts. You can even choose specific output formats for each unique
43
- extension found!
42
+ If you run **lumi** without providing required flags (like dimensions or output formats), it will guide you through the
43
+ configuration using cute, interactive prompts.
44
+
45
+ - **Dimensions:** You can enter a single number for square images (e.g., `1080`) or two numbers for custom dimensions
46
+ (e.g., `1920 1080`).
47
+ - **Formats:** Choose specific output formats for each unique extension found in your input!
48
+ - **Folders:** By default, **lumi** uses your current directory as the input and creates an `output` folder for the
49
+ results.
44
50
 
45
51
  ### CLI Options
46
52
 
@@ -50,15 +56,15 @@ You can bypass the prompts by providing the flags directly:
50
56
  bunx @nathievzm/lumi -i ./my-vacation-pics -s 1080 -f .webp
51
57
  ```
52
58
 
53
- | Flag | Shortcut | Description | Default |
54
- | :--------- | :------- | :---------------------------- | :------------------ |
55
- | `--input` | `-i` | Input directory path | `INPUT_FOLDER` env |
56
- | `--output` | `-o` | Output directory path | `OUTPUT_FOLDER` env |
57
- | `--width` | `-w` | Target width in pixels | `WIDTH` env |
58
- | `--height` | `-h` | Target height in pixels | `HEIGHT` env |
59
- | `--size` | `-s` | Sets both width and height | - |
60
- | `--format` | `-f` | Output format (e.g., `.webp`) | `FORMAT` env |
61
- | `--limit` | `-l` | Max concurrent operations | `LIMIT` env |
59
+ | Flag | Shortcut | Description | Default / Env |
60
+ | :--------- | :------- | :---------------------------- | :--------------------------- |
61
+ | `--input` | `-i` | Input directory path | `.` / `INPUT_FOLDER` |
62
+ | `--output` | `-o` | Output directory path | `./output` / `OUTPUT_FOLDER` |
63
+ | `--width` | `-w` | Target width in pixels | Prompt / `WIDTH` |
64
+ | `--height` | `-h` | Target height in pixels | Prompt / `HEIGHT` |
65
+ | `--size` | `-s` | Sets both width and height | - |
66
+ | `--format` | `-f` | Output format (e.g., `.webp`) | Prompt / `FORMAT` |
67
+ | `--limit` | `-l` | Max concurrent operations | `10` / `LIMIT` |
62
68
 
63
69
  ### 🌍 Environment Variables
64
70
 
@@ -97,11 +103,12 @@ bun install
97
103
 
98
104
  - `bun start`: Run the application.
99
105
  - `bun dev`: Start with hot reloading.
100
- - `bun lint`: Check for code quality issues.
106
+ - `bun lint`: Check for code quality issues using `oxlint`.
101
107
  - `bun lint:fix`: Fix linting issues automatically.
102
- - `bun fmt`: Format the codebase.
108
+ - `bun fmt`: Format the codebase using `oxfmt`.
103
109
  - `bun fmt:check`: Check for formatting issues.
104
- - `bun prepare`: Setup husky hooks.
110
+ - `bun changelog`: Update the changelog.
111
+ - `bun release`: Release a new version with `bumpp` and update the changelog.
105
112
 
106
113
  ## 📄 License
107
114
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathievzm/lumi",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "a concurrent cli tool to resize and convert images using bun and sharp",
5
5
  "keywords": [
6
6
  "bun",
@@ -51,21 +51,21 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@clack/prompts": "^1.3.0",
54
+ "@js-temporal/polyfill": "^0.5.1",
54
55
  "image-extensions": "^1.1.0",
55
56
  "p-limit": "^7.3.0",
56
- "sharp": "^0.34.5",
57
- "spinnies": "^0.5.1"
57
+ "picocolors": "^1.1.1",
58
+ "sharp": "^0.34.5"
58
59
  },
59
60
  "devDependencies": {
60
61
  "@commitlint/cli": "^20.5.3",
61
62
  "@commitlint/config-conventional": "^20.5.3",
62
63
  "@types/bun": "latest",
63
- "@types/spinnies": "^0.5.3",
64
64
  "bumpp": "^11.1.0",
65
65
  "conventional-changelog-cli": "^5.0.0",
66
66
  "lefthook": "^2.1.6",
67
67
  "oxfmt": "^0.47.0",
68
- "oxlint": "^1.62.0",
68
+ "oxlint": "^1.64.0",
69
69
  "oxlint-tsgolint": "^0.22.1",
70
70
  "typescript": "^6.0.3"
71
71
  }
package/src/env.d.ts CHANGED
@@ -1,28 +1,32 @@
1
- declare module 'bun' {
2
- interface Env {
3
- /**
4
- * Default target width for resized images.
5
- */
6
- WIDTH?: string
7
- /**
8
- * Default target height for resized images.
9
- */
10
- HEIGHT?: string
11
- /**
12
- * Default path to the input folder containing images.
13
- */
14
- INPUT_FOLDER?: string
15
- /**
16
- * Default path where processed images will be saved.
17
- */
18
- OUTPUT_FOLDER?: string
19
- /**
20
- * Default global output format (e.g., '.webp', '.png').
21
- */
22
- FORMAT?: string
23
- /**
24
- * Default concurrent processing limit.
25
- */
26
- LIMIT?: string
27
- }
28
- }
1
+ declare module 'bun' {
2
+ interface Env {
3
+ /**
4
+ * Default target width for resized images.
5
+ */
6
+ WIDTH?: string
7
+ /**
8
+ * Default target height for resized images.
9
+ */
10
+ HEIGHT?: string
11
+ /**
12
+ * Default path to the input folder containing images.
13
+ */
14
+ INPUT_FOLDER?: string
15
+ /**
16
+ * Default path where processed images will be saved.
17
+ */
18
+ OUTPUT_FOLDER?: string
19
+ /**
20
+ * Default global output format (e.g., '.webp', '.png').
21
+ */
22
+ FORMAT?: string
23
+ /**
24
+ * Default concurrent processing limit.
25
+ */
26
+ LIMIT?: string
27
+ /**
28
+ * Whether to process the input directory recursively by default.
29
+ */
30
+ RECURSIVE?: string
31
+ }
32
+ }
package/src/index.ts CHANGED
@@ -1,73 +1,92 @@
1
- #!/usr/bin/env bun
2
-
3
- import { parse } from 'node:path'
4
- import { exit } from 'node:process'
5
-
6
- import { intro, log, note, outro } from '@clack/prompts'
7
- import pLimit from 'p-limit'
8
- import Spinnies from 'spinnies'
9
-
10
- import { cli } from '@/args'
11
- import { getMessage } from '@/error'
12
- import { ensureOutputExists, getInputPath, getOutputPath } from '@/folder'
13
- import { getExtensions, getValidImages, getWidthAndHeight, resize } from '@/image'
14
-
15
- intro('✨ welcome to lumi ✨')
16
-
17
- const input = getInputPath(cli.input)
18
- const images = await getValidImages(input)
19
-
20
- if (images.length === 0) {
21
- log.error('yikes! no valid images found in the input folder 😭')
22
- outro('please add some images to the input folder and try again 👋')
23
- exit(1)
24
- }
25
-
26
- note(`found ${images.length} images to process! 🚀`)
27
-
28
- const output = getOutputPath(cli.output)
29
- await ensureOutputExists(output)
30
-
31
- const { width, height } = await getWidthAndHeight(cli.width, cli.height)
32
-
33
- const spinnies = new Spinnies({
34
- failColor: 'red',
35
- spinnerColor: 'magenta',
36
- succeedColor: 'green'
37
- })
38
-
39
- const extensions = await getExtensions(images, cli.format)
40
- const limit = pLimit({ concurrency: cli.limit || 10, rejectOnClear: true })
41
-
42
- const promises = images.map(image =>
43
- limit(async () => {
44
- spinnies.add(image, { text: `processing: ${image}` })
45
-
46
- const { name, ext } = parse(image)
47
- const extension = extensions['default'] ?? extensions[ext] ?? '.png'
48
-
49
- try {
50
- const result = await resize({ extension, height, image, input, name, output, width })
51
- spinnies.succeed(image, { text: result })
52
- } catch (error: unknown) {
53
- const message = getMessage(error)
54
- spinnies.fail(image, { text: message })
55
- throw error
56
- }
57
- })
58
- )
59
-
60
- log.message()
61
-
62
- const result = await Promise.allSettled(promises)
63
- let outroMessage = ''
64
-
65
- if (result.some(pr => pr.status === 'rejected')) {
66
- log.error('yikes, some images failed to process! 😢')
67
- outroMessage = 'please check the error messages above and try again 🛠️'
68
- } else {
69
- log.success('yay! all images processed and saved successfully! 💖')
70
- outroMessage = 'bye 👋'
71
- }
72
-
73
- outro(outroMessage)
1
+ #!/usr/bin/env bun
2
+
3
+ import { readdir } from 'node:fs/promises'
4
+ import { basename, extname } from 'node:path'
5
+ import { exit } from 'node:process'
6
+
7
+ import { intro, log, note, outro, spinner } from '@clack/prompts'
8
+ import { Temporal } from '@js-temporal/polyfill'
9
+ import pLimit from 'p-limit'
10
+ import color from 'picocolors'
11
+
12
+ import { cli } from '@/args'
13
+ import { ensureOutputExists, getInputPath, getOutputPath } from '@/folder'
14
+ import { getExtensions, getImages, getWidthAndHeight, resize } from '@/image'
15
+
16
+ import pkg from '../package.json'
17
+
18
+ console.clear()
19
+
20
+ const banner = `
21
+ ${color.magenta('╭──────────────────────────────╮')}
22
+ ${color.magenta('')} ${color.magenta('│')}
23
+ ${color.magenta('│')} 🩷 ${color.magenta('lumi')} 🩷 ${color.magenta('│')}
24
+ ${color.magenta('│')} ${color.magenta('│')}
25
+ ${color.magenta('╰──────────────────────────────╯')}
26
+ `
27
+
28
+ console.log(banner)
29
+
30
+ intro(color.magenta(`welcome to lumi v${pkg.version} 🩷`))
31
+
32
+ const input = getInputPath(cli.input)
33
+
34
+ const allFiles = await readdir(input, { recursive: cli.recursive })
35
+ const images = getImages(allFiles)
36
+
37
+ if (images.length === 0) {
38
+ log.error('yikes! no valid images found in the input folder 😭')
39
+ outro('please add some images to the input folder and try again 👋')
40
+ exit(1)
41
+ }
42
+
43
+ note(`found ${color.magenta(images.length)} images to process! 🚀`)
44
+
45
+ const output = getOutputPath(cli.output)
46
+ await ensureOutputExists(output)
47
+
48
+ const { width, height } = await getWidthAndHeight(cli.width, cli.height)
49
+
50
+ const extensions = await getExtensions(images, cli.format)
51
+ const limit = pLimit({ concurrency: cli.limit || 10, rejectOnClear: true })
52
+
53
+ const spin = spinner()
54
+ spin.start(`processing: ${color.magenta(0)}/${color.magenta(images.length)} images 🔃`)
55
+
56
+ let processed = 0
57
+
58
+ const startTime = Temporal.Now.instant()
59
+
60
+ const promises = images.map(image =>
61
+ limit(async () => {
62
+ const ext = extname(image)
63
+ const name = basename(image, ext)
64
+ const extension = extensions['default'] ?? extensions[ext] ?? '.png'
65
+
66
+ await resize({ extension, height, image, input, name, output, width })
67
+
68
+ processed++
69
+ spin.message(`processing: ${color.green(processed)}/${color.magenta(images.length)} images 🔃`)
70
+ })
71
+ )
72
+
73
+ log.message()
74
+
75
+ const result = await Promise.allSettled(promises)
76
+
77
+ const endTime = Temporal.Now.instant()
78
+ const duration = startTime.until(endTime).total('seconds').toFixed(2)
79
+
80
+ let outroMessage = ''
81
+
82
+ if (result.some(pr => pr.status === 'rejected')) {
83
+ spin.error(
84
+ `yikes! finished with errors. processed ${color.red(processed)}/${color.red(images.length)} images in ${color.yellow(duration)} seconds 😢`
85
+ )
86
+ outroMessage = 'please check your input files and try again 🛠️'
87
+ } else {
88
+ spin.stop(`yay! ${color.green(images.length)} images processed in ${color.green(duration)} seconds! 💡`)
89
+ outroMessage = 'bye 👋'
90
+ }
91
+
92
+ outro(color.magenta(outroMessage))