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