@gesslar/uglier 0.5.0 → 0.6.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
@@ -2,7 +2,10 @@
2
2
 
3
3
  **Opinionated, composable ESLint flat config for people who like their code readable.**
4
4
 
5
- A flexible ESLint configuration system built on [flat config](https://eslint.org/docs/latest/use/configure/configuration-files) that lets you mix and match stylistic rules, JSDoc enforcement, and environment presets.
5
+ A flexible ESLint configuration system built on
6
+ [flat config](https://eslint.org/docs/latest/use/configure/configuration-files)
7
+ that lets you mix and match stylistic rules, JSDoc enforcement, and environment
8
+ presets.
6
9
 
7
10
  ## Monotribe
8
11
 
@@ -30,14 +33,16 @@ kthx
30
33
  ## Quick Start
31
34
 
32
35
  ```bash
33
- # Install and set up in one command
36
+ # Install dependencies and generate config in one command
37
+ npx @gesslar/uglier install
34
38
  npx @gesslar/uglier init node
35
39
 
36
40
  # Or for React projects
41
+ npx @gesslar/uglier install
37
42
  npx @gesslar/uglier init react
38
43
 
39
- # Or just install without config generation
40
- npx @gesslar/uglier
44
+ # Forgot to add React later? No problem!
45
+ npx @gesslar/uglier add react
41
46
  ```
42
47
 
43
48
  This automatically installs `@gesslar/uglier`, `eslint`, and all dependencies.
@@ -156,8 +161,8 @@ Run `npx @gesslar/uglier --help` to see all available configs with descriptions.
156
161
  ## Commands
157
162
 
158
163
  ```bash
159
- # Install dependencies only
160
- npx @gesslar/uglier
164
+ # Install dependencies
165
+ npx @gesslar/uglier install
161
166
 
162
167
  # Generate config for specific targets
163
168
  npx @gesslar/uglier init node
@@ -165,13 +170,22 @@ npx @gesslar/uglier init web
165
170
  npx @gesslar/uglier init react
166
171
  npx @gesslar/uglier init node web # Multiple targets
167
172
 
173
+ # Add config blocks to existing eslint.config.js
174
+ npx @gesslar/uglier add react
175
+ npx @gesslar/uglier add tauri vscode-extension # Multiple targets
176
+
177
+ # Remove config blocks from existing eslint.config.js
178
+ npx @gesslar/uglier remove react
179
+ npx @gesslar/uglier remove web tauri # Multiple targets
180
+ # Note: Also removes any overrides for removed targets
181
+
168
182
  # Show available configs
169
183
  npx @gesslar/uglier --help
170
184
 
171
185
  # Works with any package manager
172
- pnpx @gesslar/uglier init node # pnpm
173
- yarn dlx @gesslar/uglier init node # yarn
174
- bunx @gesslar/uglier init node # bun
186
+ pnpx @gesslar/uglier install # pnpm
187
+ yarn dlx @gesslar/uglier install # yarn
188
+ bunx @gesslar/uglier install # bun
175
189
  ```
176
190
 
177
191
  ## Manual Installation
@@ -194,6 +208,19 @@ bun add -d @gesslar/uglier eslint
194
208
 
195
209
  Note: `@stylistic/eslint-plugin`, `eslint-plugin-jsdoc`, and `globals` are bundled as dependencies.
196
210
 
211
+ ## Development
212
+
213
+ ```bash
214
+ # Run tests
215
+ npm test
216
+
217
+ # Run linter
218
+ npm run lint
219
+
220
+ # Fix linting issues
221
+ npm run lint:fix
222
+ ```
223
+
197
224
  ## Philosophy
198
225
 
199
226
  This config enforces:
package/bin/cli.js ADDED
@@ -0,0 +1,633 @@
1
+ /**
2
+ * @file cli.js - Core CLI functions for @gesslar/uglier
3
+ */
4
+
5
+ import {execSync} from "child_process"
6
+ import {
7
+ FileObject,
8
+ DirectoryObject,
9
+ CappedDirectoryObject,
10
+ Sass
11
+ } from "@gesslar/toolkit"
12
+ import c from "@gesslar/colours"
13
+ import {detectAgent} from "@skarab/detect-package-manager"
14
+
15
+ // Use CappedDirectoryObject to ensure we never accidentally venture outside project
16
+ // Cap at project root - this becomes our sandbox boundary
17
+ const PROJECT_ROOT = CappedDirectoryObject.fromCwd()
18
+ const SRC_DIR = PROJECT_ROOT.getDirectory("src")
19
+ const PACKAGE_NAME = "@gesslar/uglier"
20
+
21
+ // Only peer dependencies need to be installed separately
22
+ // (all other dependencies come bundled with the package)
23
+ const PEER_DEPS = [
24
+ "eslint"
25
+ ]
26
+
27
+ /**
28
+ * Parse targets from config file's with array
29
+ *
30
+ * @param {string} content - File content
31
+ * @returns {string[]} Array of target names
32
+ */
33
+ function parseTargetsFromConfig(content) {
34
+ // Match the entire with array, being careful not to stop at nested brackets
35
+ const withMatch = content.match(/with:\s*\[([\s\S]*?)\n\s*\]/m)
36
+
37
+ if(!withMatch) {
38
+ return []
39
+ }
40
+
41
+ // Extract all quoted strings that appear at the start of lines (config names)
42
+ const targets = []
43
+ const lines = withMatch[1].split("\n")
44
+
45
+ for(const line of lines) {
46
+ // Match either single or double quoted strings at start of line
47
+ const match = line.match(/^\s*(['"])([^'"]+)\1/)
48
+
49
+ if(match) {
50
+ targets.push(match[2])
51
+ }
52
+ }
53
+
54
+ return targets
55
+ }
56
+
57
+ /**
58
+ * Get install command for detected package manager
59
+ *
60
+ * @returns {Promise<{manager: string, installCmd: string}>} Package manager info
61
+ */
62
+ export async function getPackageManagerInfo() {
63
+ const agent = await detectAgent()
64
+ const manager = agent?.name || "npm"
65
+
66
+ const commands = {
67
+ npm: "npm i -D",
68
+ pnpm: "pnpm i -D",
69
+ yarn: "yarn add -D",
70
+ bun: "bun add -d"
71
+ }
72
+
73
+ return {
74
+ manager,
75
+ installCmd: commands[manager] || commands.npm
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Get the appropriate eslint run command for the detected package manager
81
+ *
82
+ * @returns {Promise<string>} ESLint command (e.g., "pnpm eslint .")
83
+ */
84
+ async function getEslintCommand() {
85
+ const {manager} = await getPackageManagerInfo()
86
+
87
+ const commands = {
88
+ npm: "npx eslint .",
89
+ pnpm: "pnpm eslint .",
90
+ yarn: "yarn eslint .",
91
+ bun: "bunx eslint ."
92
+ }
93
+
94
+ return commands[manager] || commands.npm
95
+ }
96
+
97
+ /**
98
+ * Execute a command and return output
99
+ *
100
+ * @param {string} cmd - Command to execute
101
+ * @returns {string} Command output
102
+ * @throws {Sass} Enhanced error with command context
103
+ */
104
+ export function exec(cmd) {
105
+ try {
106
+ return execSync(cmd, {encoding: "utf8", stdio: "pipe"})
107
+ } catch(error) {
108
+ const err = new Sass(`Failed to execute command: ${cmd}`, {cause: error})
109
+ err.addTrace("Command execution failed")
110
+ console.error(err.report())
111
+ process.exit(1)
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Get available configs from the source file
117
+ *
118
+ * @returns {Promise<Array<{name: string, description: string, files: string}>|null>} Available configs
119
+ */
120
+ export async function getAvailableConfigs() {
121
+ try {
122
+ // Try to read from installed package or local source
123
+ const cwd = new CappedDirectoryObject(process.cwd())
124
+ const installedDir = new DirectoryObject(`node_modules/${PACKAGE_NAME}/src`, cwd)
125
+
126
+ const localSource = new FileObject("uglier.js", SRC_DIR)
127
+ const installedSource = new FileObject("uglier.js", installedDir)
128
+
129
+ let uglierFile = null
130
+
131
+ if(await localSource.exists) {
132
+ uglierFile = localSource
133
+ } else if(await installedSource.exists) {
134
+ uglierFile = installedSource
135
+ }
136
+
137
+ if(!uglierFile) {
138
+ return null
139
+ }
140
+
141
+ const source = await uglierFile.read()
142
+
143
+ // Extract config names, descriptions, and default files
144
+ const configs = []
145
+ // Match individual config blocks within CONFIGS object
146
+ const configBlockRegex = /\/\*\*\s*\n\s*\*\s*([^\n@*]+?)\s*\n(?:\s*\*[^\n]*\n)*?\s*\*\/\s*\n\s*["']([^"']+)["']:\s*\([^)]*\)\s*=>\s*\{[^}]*?files\s*=\s*(\[[^\]]+\])/g
147
+ let match
148
+
149
+ while((match = configBlockRegex.exec(source)) !== null) {
150
+ configs.push({
151
+ name: match[2],
152
+ description: match[1].trim(),
153
+ files: match[3]
154
+ })
155
+ }
156
+
157
+ return configs
158
+ } catch {
159
+ return null
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Check if a package is already installed
165
+ *
166
+ * @param {string} packageName - Name of package to check
167
+ * @returns {Promise<boolean>} True if installed
168
+ */
169
+ export async function isInstalled(packageName) {
170
+ try {
171
+ const cwd = new CappedDirectoryObject(process.cwd())
172
+ const packageJsonFile = new FileObject("package.json", cwd)
173
+
174
+ if(!(await packageJsonFile.exists)) {
175
+ console.warn(c`No {<B}package.json{B>} found. Please initialize your project first.`)
176
+ process.exit(1)
177
+ }
178
+
179
+ const packageJson = await packageJsonFile.loadData("json")
180
+ const allDeps = {
181
+ ...packageJson.dependencies,
182
+ ...packageJson.devDependencies,
183
+ ...packageJson.peerDependencies
184
+ }
185
+
186
+ return packageName in allDeps
187
+ } catch {
188
+ return false
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Main installation routine
194
+ */
195
+ export async function install() {
196
+ console.log(c`Installing {<B}${PACKAGE_NAME}{B>}...`)
197
+ console.log()
198
+
199
+ const toInstall = []
200
+
201
+ // Check if main package is already installed
202
+ if(!(await isInstalled(PACKAGE_NAME))) {
203
+ toInstall.push(PACKAGE_NAME)
204
+ } else {
205
+ console.log(c`{F070}✓{/} {<B}${PACKAGE_NAME}{B>} already installed`)
206
+ }
207
+
208
+ // Check peer dependencies
209
+ for(const dep of PEER_DEPS) {
210
+ if(!(await isInstalled(dep))) {
211
+ toInstall.push(dep)
212
+ } else {
213
+ console.log(c`{F070}✓{/} {<B}${dep}{B>} already installed`)
214
+ }
215
+ }
216
+
217
+ // Install missing packages
218
+ if(toInstall.length > 0) {
219
+ console.log(c`\n{F027} Installing:{/} ${toInstall.map(p => c`{F172}${p}{/}`).join(", ")}`)
220
+
221
+ const {manager, installCmd} = await getPackageManagerInfo()
222
+ const fullCmd = `${installCmd} ${toInstall.join(" ")}`
223
+
224
+ console.log(c`{F244}Using package manager: ${manager}{/}`)
225
+ console.log(c`{F244}Running: ${fullCmd}{/}`)
226
+ exec(fullCmd)
227
+
228
+ console.log()
229
+ console.log(c`{F070}✓{/} Installation successful.`)
230
+ }
231
+
232
+ console.log()
233
+ console.log(c`{F039}For detailed setup and configuration options, visit:{/}`)
234
+ console.log(c`https://github.com/gesslar/uglier#readme`)
235
+ }
236
+
237
+ /**
238
+ * Generate eslint.config.js file
239
+ *
240
+ * @param {string[]} targets - Target environments (node, web, react, etc.)
241
+ * @returns {Promise<boolean>} True if successful
242
+ */
243
+ export async function generateConfig(targets = []) {
244
+ const cwd = new CappedDirectoryObject(process.cwd())
245
+ const configFile = new FileObject("eslint.config.js", cwd)
246
+
247
+ if(await configFile.exists) {
248
+ console.log(c`{F214}Error:{/} {<B}eslint.config.js{B>} already exists`)
249
+ console.log(c`Use {<B}npx @gesslar/uglier add <targets>{B>} to add config blocks to it`)
250
+
251
+ return false
252
+ }
253
+
254
+ // Get available configs dynamically
255
+ const configs = await getAvailableConfigs()
256
+ const environmentTargets = configs
257
+ ? configs.filter(c => !c.name.startsWith("lints-") && c.name !== "languageOptions" && !c.name.endsWith("-override"))
258
+ .map(c => c.name)
259
+ : ["node", "web", "react", "tauri", "vscode-extension"]
260
+
261
+ // If no targets specified, show error with available options
262
+ if(targets.length === 0) {
263
+ console.log(c`{F214}Error:{/} No targets specified`)
264
+ console.log()
265
+ console.log(c`Available targets: ${environmentTargets.map(t => c`{F172}${t}{/}`).join(", ")}`)
266
+ console.log()
267
+ console.log(c`{F244}Example: npx @gesslar/uglier init ${environmentTargets[0] || "node"}{/}`)
268
+
269
+ return false
270
+ }
271
+
272
+ // Validate targets
273
+ const validTargets = environmentTargets
274
+ const invalidTargets = targets.filter(t => !validTargets.includes(t))
275
+
276
+ if(invalidTargets.length > 0) {
277
+ console.log(c`{F214}Error:{/} Invalid targets: {F172}${invalidTargets.join(", ")}{/}`)
278
+ console.log(c`Valid targets: {F070}${validTargets.join(", ")}{/}`)
279
+
280
+ return false
281
+ }
282
+
283
+ // Build the config with comments
284
+ const withArray = ["lints-js", "lints-jsdoc", ...targets]
285
+
286
+ // Get file patterns dynamically from source
287
+ const allConfigs = await getAvailableConfigs()
288
+ const filePatterns = {}
289
+
290
+ if(allConfigs) {
291
+ for(const config of allConfigs) {
292
+ filePatterns[config.name] = config.files
293
+ }
294
+ }
295
+
296
+ // Build the with array with comments
297
+ const withLines = withArray.map(target => {
298
+ const pattern = filePatterns[target] || "[]"
299
+
300
+ return ` "${target}", // default files: ${pattern}`
301
+ }).join("\n")
302
+
303
+ const configContent = `import uglify from "@gesslar/uglier"
304
+
305
+ export default [
306
+ ...uglify({
307
+ with: [
308
+ ${withLines}
309
+ ]
310
+ })
311
+ ]
312
+ `
313
+
314
+ await configFile.write(configContent)
315
+
316
+ console.log(c`{F070}✓{/} Created {<B}eslint.config.js{B>}`)
317
+ console.log()
318
+ console.log(c`{F039}Configuration includes:{/}`)
319
+
320
+ for(const target of withArray) {
321
+ console.log(c` {F070}•{/} ${target}`)
322
+ }
323
+
324
+ const eslintCmd = await getEslintCommand()
325
+
326
+ console.log()
327
+ console.log(c`{F244}Run {<B}${eslintCmd}{B>} to lint your project{/}`)
328
+
329
+ return true
330
+ }
331
+
332
+ /**
333
+ * Add config blocks to existing eslint.config.js
334
+ *
335
+ * @param {string[]} targets - Target environments to add (node, web, react, etc.)
336
+ * @returns {Promise<boolean>} True if successful
337
+ */
338
+ export async function addToConfig(targets = []) {
339
+ const cwd = new CappedDirectoryObject(process.cwd())
340
+ const configFile = new FileObject("eslint.config.js", cwd)
341
+
342
+ if(!(await configFile.exists)) {
343
+ console.log(c`{F214}Error:{/} {<B}eslint.config.js{B>} not found`)
344
+ console.log(c`Use {<B}npx @gesslar/uglier init <targets>{B>} to create one first`)
345
+
346
+ return false
347
+ }
348
+
349
+ // Get available configs dynamically
350
+ const configs = await getAvailableConfigs()
351
+ const environmentTargets = configs
352
+ ? configs.filter(c => !c.name.startsWith("lints-") && c.name !== "languageOptions" && !c.name.endsWith("-override"))
353
+ .map(c => c.name)
354
+ : ["node", "web", "react", "tauri", "vscode-extension"]
355
+
356
+ // If no targets specified, show error with available options
357
+ if(targets.length === 0) {
358
+ console.log(c`{F214}Error:{/} No targets specified`)
359
+ console.log()
360
+ console.log(c`Available targets: ${environmentTargets.map(t => c`{F172}${t}{/}`).join(", ")}`)
361
+ console.log()
362
+ console.log(c`{F244}Example: npx @gesslar/uglier add react{/}`)
363
+
364
+ return false
365
+ }
366
+
367
+ // Validate targets
368
+ const validTargets = environmentTargets
369
+ const invalidTargets = targets.filter(t => !validTargets.includes(t))
370
+
371
+ if(invalidTargets.length > 0) {
372
+ console.log(c`{F214}Error:{/} Invalid targets: {F172}${invalidTargets.join(", ")}{/}`)
373
+ console.log(c`Valid targets: {F070}${validTargets.join(", ")}{/}`)
374
+
375
+ return false
376
+ }
377
+
378
+ // Read existing config
379
+ const existingContent = await configFile.read()
380
+
381
+ // Parse the with array from the existing config
382
+ const withMatch = existingContent.match(/with:\s*\[([\s\S]*?)\]/m)
383
+ const existingTargets = parseTargetsFromConfig(existingContent)
384
+
385
+ if(existingTargets.length === 0 || !withMatch) {
386
+ console.log(c`{F214}Error:{/} Could not parse existing config`)
387
+ console.log(c`The config file may have a non-standard format`)
388
+
389
+ return false
390
+ }
391
+
392
+ // Find which targets are new
393
+ const newTargets = targets.filter(t => !existingTargets.includes(t))
394
+
395
+ if(newTargets.length === 0) {
396
+ console.log(c`{F214}Warning:{/} All specified targets already exist in config`)
397
+ console.log()
398
+ console.log(c`Current targets: ${existingTargets.map(t => c`{F070}${t}{/}`).join(", ")}`)
399
+
400
+ return false
401
+ }
402
+
403
+ // Get file patterns for new targets
404
+ const allConfigs = await getAvailableConfigs()
405
+ const filePatterns = {}
406
+
407
+ if(allConfigs) {
408
+ for(const config of allConfigs) {
409
+ filePatterns[config.name] = config.files
410
+ }
411
+ }
412
+
413
+ // Build new lines to add
414
+ const newLines = newTargets.map(target => {
415
+ const pattern = filePatterns[target] || "[]"
416
+
417
+ return ` "${target}", // default files: ${pattern}`
418
+ })
419
+
420
+ // Find the position to insert (before the closing bracket)
421
+ const withArrayContent = withMatch[1]
422
+ const lastCommaPos = withArrayContent.lastIndexOf(",")
423
+
424
+ // Build the replacement with new targets added
425
+ let newWithContent
426
+
427
+ if(lastCommaPos === -1) {
428
+ // No existing targets or only one without trailing comma
429
+ newWithContent = withArrayContent.trim() + ",\n" + newLines.join("\n")
430
+ } else {
431
+ newWithContent = withArrayContent + "\n" + newLines.join("\n")
432
+ }
433
+
434
+ const newContent = existingContent.replace(
435
+ /with:\s*\[([\s\S]*?)\]/m,
436
+ `with: [\n${newWithContent}\n ]`
437
+ )
438
+
439
+ await configFile.write(newContent)
440
+
441
+ console.log(c`{F070}✓{/} Added config blocks to {<B}eslint.config.js{B>}`)
442
+ console.log()
443
+ console.log(c`{F039}Added targets:{/}`)
444
+
445
+ for(const target of newTargets) {
446
+ console.log(c` {F070}•{/} ${target}`)
447
+ }
448
+
449
+ const eslintCmd = await getEslintCommand()
450
+
451
+ console.log()
452
+ console.log(c`{F244}Run {<B}${eslintCmd}{B>} to lint your project{/}`)
453
+
454
+ return true
455
+ }
456
+
457
+ /**
458
+ * Remove config blocks from existing eslint.config.js
459
+ *
460
+ * @param {string[]} targets - Target environments to remove (node, web, react, etc.)
461
+ * @returns {Promise<{success: boolean, removedTargets: string[], removedOverrides: string[]}>} Result info
462
+ */
463
+ export async function removeFromConfig(targets = []) {
464
+ const cwd = new CappedDirectoryObject(process.cwd())
465
+ const configFile = new FileObject("eslint.config.js", cwd)
466
+
467
+ if(!(await configFile.exists)) {
468
+ console.log(c`{F214}Error:{/} {<B}eslint.config.js{B>} not found`)
469
+ console.log(c`Use {<B}npx @gesslar/uglier init <targets>{B>} to create one first`)
470
+
471
+ return {success: false, removedTargets: [], removedOverrides: []}
472
+ }
473
+
474
+ // Get available configs dynamically
475
+ const configs = await getAvailableConfigs()
476
+ const environmentTargets = configs
477
+ ? configs.filter(c => !c.name.startsWith("lints-") && c.name !== "languageOptions" && !c.name.endsWith("-override"))
478
+ .map(c => c.name)
479
+ : ["node", "web", "react", "tauri", "vscode-extension"]
480
+
481
+ // If no targets specified, show error with available options
482
+ if(targets.length === 0) {
483
+ console.log(c`{F214}Error:{/} No targets specified`)
484
+ console.log()
485
+ console.log(c`Available targets: ${environmentTargets.map(t => c`{F172}${t}{/}`).join(", ")}`)
486
+ console.log()
487
+ console.log(c`{F244}Example: npx @gesslar/uglier remove react{/}`)
488
+
489
+ return {success: false, removedTargets: [], removedOverrides: []}
490
+ }
491
+
492
+ // Read existing config
493
+ const existingContent = await configFile.read()
494
+
495
+ // Parse the with array from the existing config
496
+ const existingTargets = parseTargetsFromConfig(existingContent)
497
+
498
+ if(existingTargets.length === 0) {
499
+ console.log(c`{F214}Error:{/} Could not parse existing config`)
500
+ console.log(c`The config file may have a non-standard format`)
501
+
502
+ return {success: false, removedTargets: [], removedOverrides: []}
503
+ }
504
+
505
+ // Find which targets exist and can be removed
506
+ const targetsToRemove = targets.filter(t => existingTargets.includes(t))
507
+ const notFoundTargets = targets.filter(t => !existingTargets.includes(t))
508
+
509
+ if(notFoundTargets.length > 0) {
510
+ console.log(c`{F214}Warning:{/} These targets are not in the config: {F172}${notFoundTargets.join(", ")}{/}`)
511
+ }
512
+
513
+ if(targetsToRemove.length === 0) {
514
+ console.log(c`{F214}Error:{/} None of the specified targets exist in config`)
515
+ console.log()
516
+ console.log(c`Current targets: ${existingTargets.map(t => c`{F070}${t}{/}`).join(", ")}`)
517
+
518
+ return {success: false, removedTargets: [], removedOverrides: []}
519
+ }
520
+
521
+ // Build new with array content without the removed targets
522
+ const remainingTargets = existingTargets.filter(
523
+ t => !targetsToRemove.includes(t)
524
+ )
525
+
526
+ if(remainingTargets.length === 0) {
527
+ console.log(c`{F214}Error:{/} Cannot remove all targets from config`)
528
+ console.log(c`At least one target must remain`)
529
+
530
+ return {success: false, removedTargets: [], removedOverrides: []}
531
+ }
532
+
533
+ // Get file patterns for remaining targets
534
+ const allConfigs = await getAvailableConfigs()
535
+ const filePatterns = {}
536
+
537
+ if(allConfigs) {
538
+ for(const config of allConfigs) {
539
+ filePatterns[config.name] = config.files
540
+ }
541
+ }
542
+
543
+ // Build new lines for remaining targets
544
+ const newLines = remainingTargets.map(target => {
545
+ const pattern = filePatterns[target] || "[]"
546
+
547
+ return ` "${target}", // default files: ${pattern}`
548
+ })
549
+
550
+ let newContent = existingContent.replace(
551
+ /with:\s*\[([\s\S]*?)\n\s*\]/m,
552
+ `with: [\n${newLines.join("\n")}\n ]`
553
+ )
554
+
555
+ // Check for and remove overrides for removed targets
556
+ const removedOverrides = []
557
+ const overridesMatch = existingContent.match(/overrides:\s*\{([\s\S]*?)\n\s*\}/m)
558
+
559
+ if(overridesMatch) {
560
+ for(const target of targetsToRemove) {
561
+ // Match override block for this target
562
+ const overridePattern = new RegExp(`\\s*["']${target}["']:\\s*\\{[^}]*\\},?`, "g")
563
+
564
+ if(overridePattern.test(newContent)) {
565
+ removedOverrides.push(target)
566
+ newContent = newContent.replace(overridePattern, "")
567
+ }
568
+ }
569
+
570
+ // Clean up empty overrides object or trailing commas
571
+ newContent = newContent.replace(/overrides:\s*\{\s*,?\s*\}/m, "")
572
+ newContent = newContent.replace(/,(\s*)\}/g, "$1}")
573
+ }
574
+
575
+ await configFile.write(newContent)
576
+
577
+ console.log(c`{F070}✓{/} Removed config blocks from {<B}eslint.config.js{B>}`)
578
+ console.log()
579
+ console.log(c`{F039}Removed targets:{/}`)
580
+
581
+ for(const target of targetsToRemove) {
582
+ console.log(c` {F070}•{/} ${target}`)
583
+ }
584
+
585
+ if(removedOverrides.length > 0) {
586
+ console.log()
587
+ console.log(c`{F039}Also removed overrides for:{/}`)
588
+
589
+ for(const target of removedOverrides) {
590
+ console.log(c` {F070}•{/} ${target}`)
591
+ }
592
+ }
593
+
594
+ const eslintCmd = await getEslintCommand()
595
+
596
+ console.log()
597
+ console.log(c`{F244}Run {<B}${eslintCmd}{B>} to lint your project{/}`)
598
+
599
+ return {success: true, removedTargets: targetsToRemove, removedOverrides}
600
+ }
601
+
602
+ /**
603
+ * Show help information
604
+ */
605
+ export async function showHelp() {
606
+ console.log(c`{F027}@gesslar/uglier{/} - Composable ESLint flat config`)
607
+
608
+ console.log()
609
+ console.log("Usage:")
610
+ console.log()
611
+ console.log(c` {<B}npx @gesslar/uglier install{B>} Install package and dependencies`)
612
+ console.log(c` {<B}npx @gesslar/uglier init <targets>{B>} Generate eslint.config.js with targets`)
613
+ console.log(c` {<B}npx @gesslar/uglier add <targets>{B>} Add config blocks to existing eslint.config.js`)
614
+ console.log(c` {<B}npx @gesslar/uglier remove <targets>{B>} Remove config blocks from existing eslint.config.js`)
615
+ console.log(c` {<B}npx @gesslar/uglier --help{B>} Show this help`)
616
+ console.log()
617
+
618
+ const configs = await getAvailableConfigs()
619
+
620
+ if(configs && configs.length > 0) {
621
+ console.log(c`Available config blocks:`)
622
+ console.log()
623
+
624
+ for(const {name, description} of configs) {
625
+ console.log(c` {<B}${name.padEnd(20)}{B>} ${description}`)
626
+ }
627
+ } else {
628
+ console.log("Install the package to see available config blocks.\n")
629
+ }
630
+
631
+ console.log()
632
+ console.log(`Documentation at https://github.com/gesslar/uglier.`)
633
+ }
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "email": "bmw@gesslar.dev",
7
7
  "url": "https://gesslar.dev"
8
8
  },
9
- "version": "0.5.0",
9
+ "version": "0.6.0",
10
10
  "repository": {
11
11
  "type": "git",
12
12
  "url": "git+https://github.com/gesslar/uglier.git"
@@ -15,7 +15,7 @@
15
15
  "main": "src/uglier.js",
16
16
  "exports": "./src/uglier.js",
17
17
  "bin": {
18
- "uglier": "./bin/install.js"
18
+ "uglier": "./src/install.js"
19
19
  },
20
20
  "files": [
21
21
  "src",
@@ -42,18 +42,19 @@
42
42
  "node": ">=22"
43
43
  },
44
44
  "dependencies": {
45
- "@gesslar/colours": "^0.5.0",
46
- "@gesslar/toolkit": "^3.5.0",
45
+ "@gesslar/colours": "^0.7.1",
46
+ "@gesslar/toolkit": "^3.9.0",
47
47
  "@skarab/detect-package-manager": "^1.0.0",
48
48
  "@stylistic/eslint-plugin": "^5.6.1",
49
49
  "eslint-plugin-jsdoc": "^61.5.0",
50
- "globals": "^16.5.0"
50
+ "globals": "^17.0.0"
51
51
  },
52
52
  "devDependencies": {
53
- "@gesslar/uglier": "^0.4.0",
53
+ "@gesslar/uglier": "^0.5.1",
54
54
  "eslint": "^9.39.2"
55
55
  },
56
56
  "scripts": {
57
+ "test": "node --test tests/unit/*.test.js",
57
58
  "lint": "eslint",
58
59
  "lint:fix": "eslint --fix",
59
60
  "update": "pnpm up --latest --recursive",
package/src/install.js ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @file install.js - Auto-installer and config generator for @gesslar/uglier
5
+ *
6
+ * @description
7
+ * This script can be run via `npx @gesslar/uglier` to automatically install
8
+ * the package and its peer dependencies to the current project.
9
+ *
10
+ * Commands:
11
+ * - npx @gesslar/uglier install - Install package and dependencies
12
+ * - npx @gesslar/uglier init <targets> - Generate eslint.config.js with targets
13
+ * - npx @gesslar/uglier add <targets> - Add config blocks to existing eslint.config.js
14
+ * - npx @gesslar/uglier remove <targets> - Remove config blocks from existing eslint.config.js
15
+ * - npx @gesslar/uglier --help - Show help
16
+ *
17
+ * Installation does:
18
+ * 1. Install @gesslar/uglier as a dev dependency
19
+ * 2. Install eslint as a peer dependency (if not present)
20
+ *
21
+ * Note: All other dependencies (@stylistic/eslint-plugin, eslint-plugin-jsdoc, globals)
22
+ * are bundled with the package and don't need to be installed separately.
23
+ */
24
+
25
+ import c from "@gesslar/colours"
26
+ import {
27
+ install,
28
+ generateConfig,
29
+ addToConfig,
30
+ removeFromConfig,
31
+ showHelp
32
+ } from "../bin/cli.js"
33
+
34
+ // Parse command line arguments and run
35
+ const args = process.argv.slice(2)
36
+
37
+ if(args.includes("--help") || args.includes("-h")) {
38
+ await showHelp()
39
+ } else if(args[0] === "install") {
40
+ await install()
41
+ } else if(args[0] === "init") {
42
+ const targets = args.slice(1)
43
+
44
+ await generateConfig(targets)
45
+ } else if(args[0] === "add") {
46
+ const targets = args.slice(1)
47
+
48
+ await addToConfig(targets)
49
+ } else if(args[0] === "remove") {
50
+ const targets = args.slice(1)
51
+
52
+ await removeFromConfig(targets)
53
+ } else {
54
+ // No command or unknown command - show help
55
+ console.log(c`{F214}Error:{/} Unknown command or no command specified`)
56
+ console.log()
57
+ await showHelp()
58
+ }
package/bin/install.js DELETED
@@ -1,334 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * @file install.js - Auto-installer and config generator for @gesslar/uglier
5
- *
6
- * @description
7
- * This script can be run via `npx @gesslar/uglier` to automatically install
8
- * the package and its peer dependencies to the current project.
9
- *
10
- * Commands:
11
- * - npx @gesslar/uglier - Install package and dependencies
12
- * - npx @gesslar/uglier init - Generate eslint.config.js with prompts
13
- * - npx @gesslar/uglier init node - Generate eslint.config.js for Node.js
14
- * - npx @gesslar/uglier --help - Show help
15
- *
16
- * Installation does:
17
- * 1. Install @gesslar/uglier as a dev dependency
18
- * 2. Install eslint as a peer dependency (if not present)
19
- *
20
- * Note: All other dependencies (@stylistic/eslint-plugin, eslint-plugin-jsdoc, globals)
21
- * are bundled with the package and don't need to be installed separately.
22
- */
23
-
24
- import {execSync} from "child_process"
25
- import {dirname} from "path"
26
- import {fileURLToPath} from "url"
27
- import {FileObject, DirectoryObject} from "@gesslar/toolkit"
28
- import c from "@gesslar/colours"
29
- import {detectAgent} from "@skarab/detect-package-manager"
30
-
31
- const __filename = fileURLToPath(import.meta.url)
32
- const __dirname = dirname(__filename)
33
-
34
- const PACKAGE_NAME = "@gesslar/uglier"
35
-
36
- // Only peer dependencies need to be installed separately
37
- // (all other dependencies come bundled with the package)
38
- const PEER_DEPS = [
39
- "eslint"
40
- ]
41
-
42
- /**
43
- * Get install command for detected package manager
44
- *
45
- * @returns {Promise<{manager: string, installCmd: string}>} Package manager info
46
- */
47
- async function getPackageManagerInfo() {
48
- const agent = await detectAgent()
49
- const manager = agent?.name || "npm"
50
-
51
- const commands = {
52
- npm: "npm i -D",
53
- pnpm: "pnpm i -D",
54
- yarn: "yarn add -D",
55
- bun: "bun add -d"
56
- }
57
-
58
- return {
59
- manager,
60
- installCmd: commands[manager] || commands.npm
61
- }
62
- }
63
-
64
- /**
65
- * Execute a command and return output
66
- *
67
- * @param {string} cmd - Command to execute
68
- * @returns {string} Command output
69
- */
70
- function exec(cmd) {
71
- try {
72
- return execSync(cmd, {encoding: "utf8", stdio: "pipe"})
73
- } catch(error) {
74
- console.error(`Error executing: ${cmd}`)
75
- console.error(error.message)
76
- process.exit(1)
77
- }
78
- }
79
-
80
- /**
81
- * Get available configs from the source file
82
- *
83
- * @returns {Promise<Array<{name: string, description: string, files: string}>|null>} Available configs
84
- */
85
- async function getAvailableConfigs() {
86
- try {
87
- // Try to read from installed package or local source
88
- const localDir = new DirectoryObject(`${__dirname}/../src`)
89
- const installedDir = new DirectoryObject(`${process.cwd()}/node_modules/${PACKAGE_NAME}/src`)
90
-
91
- const localSource = new FileObject("uglier.js", localDir)
92
- const installedSource = new FileObject("uglier.js", installedDir)
93
-
94
- let uglierFile = null
95
-
96
- if(await localSource.exists) {
97
- uglierFile = localSource
98
- } else if(await installedSource.exists) {
99
- uglierFile = installedSource
100
- }
101
-
102
- if(!uglierFile) {
103
- return null
104
- }
105
-
106
- const source = await uglierFile.read()
107
-
108
- // Extract config names, descriptions, and default files
109
- const configs = []
110
- // Match individual config blocks within CONFIGS object
111
- const configBlockRegex = /\/\*\*\s*\n\s*\*\s*([^\n@*]+?)\s*\n(?:\s*\*[^\n]*\n)*?\s*\*\/\s*\n\s*["']([^"']+)["']:\s*\([^)]*\)\s*=>\s*\{[^}]*?files\s*=\s*(\[[^\]]+\])/g
112
- let match
113
-
114
- while((match = configBlockRegex.exec(source)) !== null) {
115
- configs.push({
116
- name: match[2],
117
- description: match[1].trim(),
118
- files: match[3]
119
- })
120
- }
121
-
122
- return configs
123
- } catch {
124
- return null
125
- }
126
- }
127
-
128
- /**
129
- * Show help information
130
- */
131
- async function showHelp() {
132
- console.log(c`{F027}@gesslar/uglier{/} - Composable ESLint flat config`)
133
-
134
- console.log()
135
- console.log("Usage:")
136
- console.log()
137
- console.log(c` {<B}npx @gesslar/uglier{B>} Install package and dependencies`)
138
- console.log(c` {<B}npx @gesslar/uglier init{B>} Generate eslint.config.js interactively`)
139
- console.log(c` {<B}npx @gesslar/uglier init <targets>{B>} Generate eslint.config.js with targets`)
140
- console.log(c` {<B}npx @gesslar/uglier --help{B>} Show this help`)
141
- console.log()
142
-
143
- const configs = await getAvailableConfigs()
144
-
145
- if(configs && configs.length > 0) {
146
- console.log(c`Available config blocks:`)
147
- console.log()
148
-
149
- for(const {name, description} of configs) {
150
- console.log(c` {<B}${name.padEnd(20)}{B>} ${description}`)
151
- }
152
- } else {
153
- console.log("Install the package to see available config blocks.\n")
154
- }
155
-
156
- console.log()
157
- console.log(`Documentation at https://github.com/gesslar/uglier.`)
158
- }
159
-
160
- /**
161
- * Check if a package is already installed
162
- *
163
- * @param {string} packageName - Name of package to check
164
- * @returns {Promise<boolean>} True if installed
165
- */
166
- async function isInstalled(packageName) {
167
- try {
168
- const packageJsonFile = new FileObject("package.json", process.cwd())
169
-
170
- if(!(await packageJsonFile.exists)) {
171
- console.warn(c`No {<B}package.json{B>} found. Please initialize your project first.`)
172
- process.exit(1)
173
- }
174
-
175
- const packageJson = await packageJsonFile.loadData("json")
176
- const allDeps = {
177
- ...packageJson.dependencies,
178
- ...packageJson.devDependencies,
179
- ...packageJson.peerDependencies
180
- }
181
-
182
- return packageName in allDeps
183
- } catch {
184
- return false
185
- }
186
- }
187
-
188
- /**
189
- * Main installation routine
190
- */
191
- async function install() {
192
- console.log(c`Installing {<B}${PACKAGE_NAME}{B>}...`)
193
- console.log()
194
-
195
- const toInstall = []
196
-
197
- // Check if main package is already installed
198
- if(!(await isInstalled(PACKAGE_NAME))) {
199
- toInstall.push(PACKAGE_NAME)
200
- } else {
201
- console.log(c`{F070}✓{/} {<B}${PACKAGE_NAME}{B>} already installed`)
202
- }
203
-
204
- // Check peer dependencies
205
- for(const dep of PEER_DEPS) {
206
- if(!(await isInstalled(dep))) {
207
- toInstall.push(dep)
208
- } else {
209
- console.log(c`{F070}✓{/} {<B}${dep}{B>} already installed`)
210
- }
211
- }
212
-
213
- // Install missing packages
214
- if(toInstall.length > 0) {
215
- console.log(c`\n{F027} Installing:{/} ${toInstall.map(p => c`{F172}${p}{/}`).join(", ")}`)
216
-
217
- const {manager, installCmd} = await getPackageManagerInfo()
218
- const fullCmd = `${installCmd} ${toInstall.join(" ")}`
219
-
220
- console.log(c`{F244}Using package manager: ${manager}{/}`)
221
- console.log(c`{F244}Running: ${fullCmd}{/}`)
222
- exec(fullCmd)
223
-
224
- console.log()
225
- console.log(c`{F070}✓{/} Installation successful.`)
226
- }
227
-
228
- console.log()
229
- console.log(c`{F039}For detailed setup and configuration options, visit:{/}`)
230
- console.log(c`https://github.com/gesslar/uglier#readme`)
231
- }
232
-
233
- /**
234
- * Generate eslint.config.js file
235
- *
236
- * @param {string[]} targets - Target environments (node, web, react, etc.)
237
- */
238
- async function generateConfig(targets = []) {
239
- const configFile = new FileObject("eslint.config.js", process.cwd())
240
-
241
- if(await configFile.exists) {
242
- console.log(c`{F214}Warning:{/} {<B}eslint.config.js{B>} already exists`)
243
- console.log(c`Delete it first or edit it manually`)
244
-
245
- return
246
- }
247
-
248
- // Get available configs dynamically
249
- const configs = await getAvailableConfigs()
250
- const environmentTargets = configs
251
- ? configs.filter(c => !c.name.startsWith("lints-") && c.name !== "languageOptions" && !c.name.endsWith("-override"))
252
- .map(c => c.name)
253
- : ["node", "web", "react", "tauri", "vscode-extension"]
254
-
255
- // If no targets specified, make it interactive
256
- if(targets.length === 0) {
257
- console.log(c`{F027}Choose your target environments:{/}`)
258
- console.log()
259
- console.log(c`Available targets: ${environmentTargets.map(t => c`{F172}${t}{/}`).join(", ")}`)
260
- console.log()
261
- console.log(c`{F244}Example: npx @gesslar/uglier init ${environmentTargets[0] || "node"}{/}`)
262
-
263
- return
264
- }
265
-
266
- // Validate targets
267
- const validTargets = environmentTargets
268
- const invalidTargets = targets.filter(t => !validTargets.includes(t))
269
-
270
- if(invalidTargets.length > 0) {
271
- console.log(c`{F214}Error:{/} Invalid targets: {F172}${invalidTargets.join(", ")}{/}`)
272
- console.log(c`Valid targets: {F070}${validTargets.join(", ")}{/}`)
273
-
274
- return
275
- }
276
-
277
- // Build the config with comments
278
- const withArray = ["lints-js", "lints-jsdoc", ...targets]
279
-
280
- // Get file patterns dynamically from source
281
- const allConfigs = await getAvailableConfigs()
282
- const filePatterns = {}
283
-
284
- if(allConfigs) {
285
- for(const config of allConfigs) {
286
- filePatterns[config.name] = config.files
287
- }
288
- }
289
-
290
- // Build the with array with comments
291
- const withLines = withArray.map(target => {
292
- const pattern = filePatterns[target] || "[]"
293
-
294
- return ` "${target}", // default files: ${pattern}`
295
- }).join("\n")
296
-
297
- const configContent = `import uglify from "@gesslar/uglier"
298
-
299
- export default [
300
- ...uglify({
301
- with: [
302
- ${withLines}
303
- ]
304
- })
305
- ]
306
- `
307
-
308
- await configFile.write(configContent)
309
-
310
- console.log(c`{F070}✓{/} Created {<B}eslint.config.js{B>}`)
311
- console.log()
312
- console.log(c`{F039}Configuration includes:{/}`)
313
-
314
- for(const target of withArray) {
315
- console.log(c` {F070}•{/} ${target}`)
316
- }
317
-
318
- console.log()
319
- console.log(c`{F244}Run {<B}npx eslint .{B>} to lint your project{/}`)
320
- }
321
-
322
- // Parse command line arguments and run
323
- const args = process.argv.slice(2)
324
-
325
- if(args.includes("--help") || args.includes("-h")) {
326
- await showHelp()
327
- } else if(args[0] === "init") {
328
- const targets = args.slice(1)
329
-
330
- await install()
331
- await generateConfig(targets)
332
- } else {
333
- await install()
334
- }