@dimensional-innovations/tool-config 2.0.0 → 3.0.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
@@ -39,7 +39,9 @@ npm install --save-dev @dimensional-innovations/tool-config
39
39
  - 📦 **All-In-One** - All plugins and parsers included as dependencies
40
40
  - 🔧 **Customizable** - Override any setting while keeping smart defaults
41
41
  - 🚀 **Modern** - ESLint 9+ flat config, TypeScript with tsgo support (10x faster)
42
- - **Battle-Tested** - 535 tests with 100% coverage
42
+ - 🔄 **Uninstall Support** - Clean removal with safety checks for modified files
43
+ - 🎬 **Smart Scripts** - Auto-generated `check-all` script runs tools in optimal order
44
+ - ✅ **Battle-Tested** - 556 tests with 100% coverage
43
45
 
44
46
  ## Quick Start
45
47
 
@@ -121,11 +123,14 @@ That's it! The configs will automatically detect your framework and TypeScript s
121
123
  "style": "stylelint '**/*.css'",
122
124
  "style:fix": "stylelint '**/*.css' --fix",
123
125
  "typecheck": "tsc --noEmit",
124
- "typecheck:watch": "tsc --noEmit --watch"
126
+ "typecheck:watch": "tsc --noEmit --watch",
127
+ "check-all": "npm run prettier && npm run style && npm run lint && npm run typecheck"
125
128
  }
126
129
  }
127
130
  ```
128
131
 
132
+ **Tip:** The CLI automatically adds a `check-all` script when you setup multiple tools. This runs all validation tools in the optimal order: formatting → styles → linting → type checking.
133
+
129
134
  ## Supported Frameworks
130
135
 
131
136
  | Framework | ESLint | Prettier | Stylelint | TypeScript | Auto-Detect |
@@ -419,22 +424,68 @@ npx @dimensional-innovations/tool-config
419
424
  - Choose which tools to configure
420
425
  - Auto-detects framework and TypeScript
421
426
  - Creates config files automatically
422
- - Adds npm scripts to package.json
423
- - **NEW:** Automated CI/CD pipeline setup
427
+ - Adds npm scripts to package.json (including `check-all` for multiple tools)
428
+ - Automated CI/CD pipeline setup
429
+ - **Uninstall support** - Clean removal of configs and scripts
424
430
  - Supports dry-run mode
425
431
 
426
- **Options:**
432
+ **Setup Options:**
427
433
 
428
434
  ```bash
435
+ # Interactive mode
436
+ npx @dimensional-innovations/tool-config # Choose tools interactively
437
+
438
+ # Single tool setup
429
439
  npx @dimensional-innovations/tool-config eslint # Setup ESLint only
430
- npx @dimensional-innovations/tool-config --all # Setup all tools
431
- npx @dimensional-innovations/tool-config --dry-run # Preview without creating files
440
+ npx @dimensional-innovations/tool-config prettier # Setup Prettier only
441
+ npx @dimensional-innovations/tool-config stylelint # Setup Stylelint only
442
+
443
+ # Setup all tools
444
+ npx @dimensional-innovations/tool-config --all # Setup all tools at once
445
+
446
+ # CI/CD setup
432
447
  npx @dimensional-innovations/tool-config --ci gitlab # Setup GitLab CI/CD
433
448
  npx @dimensional-innovations/tool-config --ci github # Setup GitHub Actions
434
449
  npx @dimensional-innovations/tool-config --setup-ci # Interactive CI setup
450
+
451
+ # Preview mode
452
+ npx @dimensional-innovations/tool-config --dry-run # Preview without creating files
453
+
454
+ # Help
435
455
  npx @dimensional-innovations/tool-config --help # Show all options
436
456
  ```
437
457
 
458
+ **Uninstall Options:**
459
+
460
+ ```bash
461
+ # Interactive uninstall
462
+ npx @dimensional-innovations/tool-config --uninstall # Choose which tools to remove
463
+
464
+ # Uninstall specific tool
465
+ npx @dimensional-innovations/tool-config --uninstall eslint
466
+
467
+ # Uninstall all detected tools
468
+ npx @dimensional-innovations/tool-config --uninstall --all
469
+
470
+ # Remove CI/CD configuration
471
+ npx @dimensional-innovations/tool-config --uninstall --ci
472
+
473
+ # Preview uninstall
474
+ npx @dimensional-innovations/tool-config --uninstall --dry-run
475
+ ```
476
+
477
+ **Safety Features:**
478
+
479
+ The uninstall command includes safety features to protect your customizations:
480
+
481
+ - Only removes files that match the original generated templates
482
+ - Skips modified configuration files (warns you instead)
483
+ - Only removes auto-generated package.json scripts
484
+ - Preserves custom scripts with the same names
485
+ - Supports dry-run to preview what will be removed
486
+ - Cleans up empty directories (e.g., `.github/workflows`)
487
+ - Idempotent - safe to run multiple times
488
+
438
489
  **CI/CD Integration:**
439
490
 
440
491
  When you select `semantic-release` in interactive mode, the CLI will automatically prompt you to setup CI/CD for automated releases. Or use the `--ci` flag to setup CI/CD directly:
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Formatting utilities for CLI output
3
+ * Standardized box drawing with consistent width
4
+ */
5
+
6
+ import pc from 'picocolors'
7
+
8
+ const BOX_WIDTH = 70 // Standard width for all boxes
9
+
10
+ export const BOX = {
11
+ topLeft: '┌',
12
+ topRight: '┐',
13
+ bottomLeft: '└',
14
+ bottomRight: '┘',
15
+ vertical: '│',
16
+ horizontal: '─',
17
+ separator: '━',
18
+ width: BOX_WIDTH
19
+ }
20
+
21
+ /**
22
+ * Create a box with standard width (70 chars)
23
+ * @param {string[]} lines - Array of content lines
24
+ * @param {string} title - Optional title for top border
25
+ * @returns {string} Formatted box
26
+ */
27
+ export function createBox(lines, title = '') {
28
+ const width = BOX.width
29
+ const contentWidth = width - 4 // Remove: │ + space + space + │
30
+
31
+ // Create top border
32
+ let top
33
+ if (title) {
34
+ const titlePart = `─ ${title} `
35
+ const remaining = width - 2 - titlePart.length // -2 for corners
36
+ top = BOX.topLeft + titlePart + BOX.horizontal.repeat(remaining) + BOX.topRight
37
+ } else {
38
+ top = BOX.topLeft + BOX.horizontal.repeat(width - 2) + BOX.topRight
39
+ }
40
+
41
+ // Create content lines - pad each to exact width
42
+ const content = lines.map(line => {
43
+ const padded = line.slice(0, contentWidth).padEnd(contentWidth)
44
+ return `${BOX.vertical} ${padded} ${BOX.vertical}`
45
+ })
46
+
47
+ // Create bottom border
48
+ const bottom = BOX.bottomLeft + BOX.horizontal.repeat(width - 2) + BOX.bottomRight
49
+
50
+ return [top, ...content, bottom].join('\n')
51
+ }
52
+
53
+ /**
54
+ * Create a separator line
55
+ * @returns {string} Separator line
56
+ */
57
+ export function createSeparator() {
58
+ return BOX.separator.repeat(BOX.width)
59
+ }
60
+
61
+ /**
62
+ * Create a simple indented list
63
+ * @param {string[]} items - Array of items
64
+ * @param {string} bullet - Bullet character (default: '•')
65
+ * @returns {string} Formatted list
66
+ */
67
+ export function createList(items, bullet = '•') {
68
+ return items.map(item => ` ${bullet} ${item}`).join('\n')
69
+ }
70
+
71
+ /**
72
+ * Create a progress indicator
73
+ * @param {number} current - Current step
74
+ * @param {number} total - Total steps
75
+ * @param {string} label - Label for this step
76
+ * @returns {string} Progress line
77
+ */
78
+ export function createProgress(current, total, label) {
79
+ return ` [${current}/${total}] ${label}`
80
+ }
81
+
82
+ /**
83
+ * Color symbols for CLI output
84
+ */
85
+ export const SYMBOLS = {
86
+ success: pc.green('✓'),
87
+ warning: pc.yellow('⚠'),
88
+ error: pc.red('✗'),
89
+ info: pc.blue('ℹ')
90
+ }
91
+
92
+ /**
93
+ * Color helper functions
94
+ */
95
+ export const colors = {
96
+ success: pc.green,
97
+ warning: pc.yellow,
98
+ error: pc.red,
99
+ info: pc.blue,
100
+ dim: pc.dim,
101
+ bold: pc.bold,
102
+ cyan: pc.cyan
103
+ }
@@ -112,3 +112,90 @@ export function updatePackageJsonScripts(tools, getScriptsFn, context) {
112
112
  console.error(' ❌ Failed to update package.json:', error.message)
113
113
  }
114
114
  }
115
+
116
+ /**
117
+ * Remove scripts from package.json for tools
118
+ * @param {string[]} tools - Array of tool names to remove scripts for
119
+ * @param {Function} getScriptsFn - Function that returns scripts for a tool: (tool, detected) => object
120
+ * @param {Object} context - Context object with {detected, cwd, dryRun}
121
+ */
122
+ export function removePackageJsonScripts(tools, getScriptsFn, context) {
123
+ const { detected, cwd, dryRun = false } = context
124
+ const packageJsonPath = join(cwd, 'package.json')
125
+
126
+ if (!existsSync(packageJsonPath)) {
127
+ console.log(' ℹ️ No package.json found - skipping script removal')
128
+ return
129
+ }
130
+
131
+ try {
132
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
133
+
134
+ if (!packageJson.scripts) {
135
+ console.log(' ℹ️ No scripts section in package.json')
136
+ return
137
+ }
138
+
139
+ const scriptsToRemove = []
140
+
141
+ // Collect script names to remove based on tools
142
+ for (const tool of tools) {
143
+ const toolScripts = getScriptsFn(tool, detected)
144
+ for (const scriptName of Object.keys(toolScripts)) {
145
+ const expectedCommand = toolScripts[scriptName]
146
+ const actualCommand = packageJson.scripts[scriptName]
147
+
148
+ // Only remove if the script exists and matches our expected command
149
+ if (actualCommand === expectedCommand) {
150
+ scriptsToRemove.push(scriptName)
151
+ } else if (actualCommand) {
152
+ console.log(` ⚠️ Script "${scriptName}" has been modified - skipping for safety`)
153
+ }
154
+ }
155
+ }
156
+
157
+ // Handle check-all script separately
158
+ const checkAllScript = packageJson.scripts['check-all']
159
+ if (checkAllScript) {
160
+ // Only remove if it's auto-generated (follows npm run X && npm run Y pattern)
161
+ const isAutoGenerated =
162
+ checkAllScript.startsWith('npm run') && checkAllScript.includes(' && npm run')
163
+
164
+ if (isAutoGenerated) {
165
+ scriptsToRemove.push('check-all')
166
+ }
167
+ }
168
+
169
+ if (scriptsToRemove.length === 0) {
170
+ console.log(' ℹ️ No scripts to remove from package.json')
171
+ return
172
+ }
173
+
174
+ if (dryRun) {
175
+ console.log(' 📝 Would remove scripts from package.json:')
176
+ for (const name of scriptsToRemove) {
177
+ console.log(` - ${name}`)
178
+ }
179
+ return
180
+ }
181
+
182
+ // Remove scripts
183
+ for (const scriptName of scriptsToRemove) {
184
+ delete packageJson.scripts[scriptName]
185
+ }
186
+
187
+ // Remove scripts section if empty
188
+ if (Object.keys(packageJson.scripts).length === 0) {
189
+ delete packageJson.scripts
190
+ }
191
+
192
+ writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8')
193
+
194
+ console.log(' ✅ Removed scripts from package.json:')
195
+ for (const name of scriptsToRemove) {
196
+ console.log(` - ${name}`)
197
+ }
198
+ } catch (error) {
199
+ console.error(' ❌ Failed to update package.json:', error.message)
200
+ }
201
+ }
package/bin/lib/ui.js CHANGED
@@ -3,6 +3,8 @@
3
3
  * Handles help text, banners, and output formatting
4
4
  */
5
5
 
6
+ import { createBox, createSeparator, SYMBOLS, colors } from './formatting.js'
7
+
6
8
  const VERSION = '0.0.0-development'
7
9
 
8
10
  /**
@@ -26,11 +28,12 @@ Options:
26
28
  --all, -a Setup all tools (non-interactive)
27
29
  --ci [provider] Setup CI/CD pipeline (gitlab, github, bitbucket)
28
30
  --setup-ci Interactive CI/CD setup
31
+ --uninstall Remove tool configuration and scripts
29
32
  --dry-run, -d Preview changes without writing files
30
33
  --help, -h Show this help message
31
34
  --version, -v Show version number
32
35
 
33
- Examples:
36
+ Examples (Install):
34
37
  setup-tool-config # Interactive mode
35
38
  setup-tool-config eslint # Setup ESLint only
36
39
  setup-tool-config --all # Setup all tools
@@ -40,6 +43,13 @@ Examples:
40
43
  setup-tool-config --setup-ci # Interactive CI setup
41
44
  setup-tool-config semantic-release --ci # Setup release + CI
42
45
 
46
+ Examples (Uninstall):
47
+ setup-tool-config --uninstall # Interactive uninstall
48
+ setup-tool-config --uninstall eslint # Remove ESLint only
49
+ setup-tool-config --uninstall --all # Remove all detected tools
50
+ setup-tool-config --uninstall --ci # Remove CI/CD configuration
51
+ setup-tool-config --uninstall --dry-run # Preview uninstall
52
+
43
53
  For more information, visit:
44
54
  https://gitlab.com/dimensional-innovations/tool-config
45
55
  `)
@@ -53,19 +63,40 @@ export function showVersion() {
53
63
  }
54
64
 
55
65
  /**
56
- * Show setup banner with detected configuration
66
+ * Show welcome and setup banner with detected configuration
57
67
  */
58
68
  export function showBanner(detected) {
59
69
  console.log('')
60
- console.log('📦 @dimensional-innovations/tool-config Setup')
61
- console.log('━'.repeat(50))
70
+
71
+ // Welcome box
72
+ const welcome = [
73
+ '🚀 Welcome to @dimensional-innovations/tool-config',
74
+ '',
75
+ 'Zero-config setup for ESLint, Prettier, Stylelint,',
76
+ 'TypeScript, and semantic-release.',
77
+ '',
78
+ '💡 Navigation: ↑↓ to move, Space to select, Enter to continue'
79
+ ]
80
+ console.log(createBox(welcome))
81
+
82
+ console.log('')
83
+
84
+ // Condensed detection info
85
+ const detectedParts = []
86
+ if (detected.framework !== 'vanilla') {
87
+ detectedParts.push(detected.framework.charAt(0).toUpperCase() + detected.framework.slice(1))
88
+ }
89
+ if (detected.typescript) {
90
+ detectedParts.push('TypeScript')
91
+ }
92
+ if (detected.environment) {
93
+ detectedParts.push(detected.environment.charAt(0).toUpperCase() + detected.environment.slice(1))
94
+ }
95
+
96
+ const detectedStr = detectedParts.length > 0 ? detectedParts.join(' • ') : 'JavaScript project'
97
+ console.log(`🔍 Auto-detected: ${detectedStr}`)
62
98
  console.log('')
63
- console.log('🔍 Detected project configuration:')
64
- console.log(` Framework: ${detected.framework}`)
65
- console.log(` Environment: ${detected.environment}`)
66
- console.log(` TypeScript: ${detected.typescript ? 'Yes' : 'No'}`)
67
- console.log(` Electron: ${detected.electron ? 'Yes' : 'No'}`)
68
- console.log(` Git Provider: ${detected.gitProvider || 'Unknown'}`)
99
+ console.log('💡 Recommended tools are pre-selected based on your project')
69
100
  console.log('')
70
101
  }
71
102
 
@@ -79,21 +110,70 @@ export function showSection(title) {
79
110
  }
80
111
 
81
112
  /**
82
- * Show completion message
113
+ * Show completion message with detailed summary
114
+ * @param {boolean} dryRun - Whether this was a dry run
115
+ * @param {string[]} createdFiles - Files that were created
116
+ * @param {string[]} scripts - Scripts that were added
83
117
  */
84
- export function showCompletion(dryRun) {
118
+ export function showCompletion(dryRun, createdFiles = [], scripts = []) {
85
119
  console.log('')
120
+
86
121
  if (dryRun) {
87
- console.log('ℹ️ Dry run mode - no files were modified')
88
- console.log('Run without --dry-run to apply changes')
122
+ console.log(createSeparator())
123
+ console.log('')
124
+ const dryRunMsg = [
125
+ `${SYMBOLS.info} DRY RUN MODE - Preview only, no files created`,
126
+ '',
127
+ 'Run without --dry-run to create these files'
128
+ ]
129
+ console.log(createBox(dryRunMsg))
89
130
  } else {
90
- console.log('✅ Setup complete!')
131
+ console.log(createSeparator())
91
132
  console.log('')
92
- console.log('Next steps:')
93
- console.log(' 1. Review the generated config files')
94
- console.log(' 2. Install peer dependencies if needed')
95
- console.log(' 3. Run: npm run lint (or other added scripts)')
133
+
134
+ const completion = [colors.bold(colors.cyan('🎉 All done! Your project is ready.'))]
135
+
136
+ if (createdFiles.length > 0) {
137
+ completion.push('')
138
+ completion.push(
139
+ `📁 Created ${createdFiles.length} config file${createdFiles.length > 1 ? 's' : ''}:`
140
+ )
141
+ createdFiles.forEach(file => {
142
+ completion.push(` • ${file}`)
143
+ })
144
+ }
145
+
146
+ if (scripts.length > 0) {
147
+ completion.push('')
148
+ completion.push(`📦 Added ${scripts.length} npm script${scripts.length > 1 ? 's' : ''}:`)
149
+
150
+ // Highlight check-all if present
151
+ const checkAllIndex = scripts.indexOf('check-all')
152
+ if (checkAllIndex !== -1) {
153
+ completion.push(` • check-all ← Run this to validate everything`)
154
+ scripts
155
+ .filter(s => s !== 'check-all')
156
+ .forEach(script => {
157
+ completion.push(` • ${script}`)
158
+ })
159
+ } else {
160
+ scripts.forEach(script => {
161
+ completion.push(` • ${script}`)
162
+ })
163
+ }
164
+ }
165
+
166
+ completion.push('')
167
+ completion.push('🚀 Quick start:')
168
+ if (scripts.includes('check-all')) {
169
+ completion.push(' npm run check-all')
170
+ } else if (scripts.length > 0) {
171
+ completion.push(` npm run ${scripts[0]}`)
172
+ }
173
+
174
+ console.log(createBox(completion))
96
175
  }
176
+
97
177
  console.log('')
98
178
  }
99
179
 
@@ -103,9 +183,57 @@ export function showCompletion(dryRun) {
103
183
  export function showCICompletion(dryRun) {
104
184
  console.log('')
105
185
  if (dryRun) {
106
- console.log('ℹ️ Dry run mode - no files were modified')
186
+ console.log(`${SYMBOLS.info} Dry run mode - no files were modified`)
187
+ } else {
188
+ console.log(`${SYMBOLS.success} CI/CD setup complete!`)
189
+ }
190
+ console.log('')
191
+ }
192
+
193
+ /**
194
+ * Show uninstall banner
195
+ */
196
+ export function showUninstallBanner(installedTools, installedCI) {
197
+ console.log('')
198
+ console.log('🗑️ @dimensional-innovations/tool-config Uninstall')
199
+ console.log('━'.repeat(50))
200
+ console.log('')
201
+ console.log('⚠️ WARNING: This will remove configuration files and scripts')
202
+ console.log(' Make sure you have committed or backed up your changes first!')
203
+ console.log('')
204
+
205
+ if (installedTools.length > 0) {
206
+ console.log('📦 Detected tools:')
207
+ for (const tool of installedTools) {
208
+ console.log(` - ${tool}`)
209
+ }
210
+ console.log('')
211
+ }
212
+
213
+ if (installedCI) {
214
+ console.log(`🚀 Detected CI/CD: ${installedCI}`)
215
+ console.log('')
216
+ }
217
+
218
+ if (installedTools.length === 0 && !installedCI) {
219
+ console.log('ℹ️ No tools or CI configuration detected')
220
+ console.log('')
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Show uninstall completion message
226
+ */
227
+ export function showUninstallCompletion(dryRun, removedCount) {
228
+ console.log('')
229
+ if (dryRun) {
230
+ console.log(`${SYMBOLS.info} Dry run mode - no files were modified`)
231
+ console.log(' Run without --dry-run to apply changes')
232
+ } else if (removedCount > 0) {
233
+ console.log(`${SYMBOLS.success} Uninstall complete!`)
234
+ console.log(` Removed ${removedCount} file(s) and their associated scripts`)
107
235
  } else {
108
- console.log('✅ CI/CD setup complete!')
236
+ console.log(`${SYMBOLS.info} Nothing was removed`)
109
237
  }
110
238
  console.log('')
111
239
  }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Uninstall utilities for setup-tool-config CLI
3
+ * Handles safe removal of config files, CI templates, and scripts
4
+ */
5
+
6
+ import { existsSync, readdirSync, readFileSync, rmSync } from 'fs'
7
+ import { join } from 'path'
8
+
9
+ /**
10
+ * Detect which tools are currently installed
11
+ * @param {string} cwd - Current working directory
12
+ * @returns {string[]} Array of installed tool names
13
+ */
14
+ export function detectInstalledTools(cwd) {
15
+ const toolFiles = {
16
+ eslint: 'eslint.config.js',
17
+ prettier: 'prettier.config.js',
18
+ stylelint: 'stylelint.config.js',
19
+ typescript: 'tsconfig.json',
20
+ 'semantic-release': 'release.config.js'
21
+ }
22
+
23
+ const installed = []
24
+ for (const [tool, filename] of Object.entries(toolFiles)) {
25
+ if (existsSync(join(cwd, filename))) {
26
+ installed.push(tool)
27
+ }
28
+ }
29
+
30
+ return installed
31
+ }
32
+
33
+ /**
34
+ * Detect which CI provider is installed
35
+ * @param {string} cwd - Current working directory
36
+ * @returns {string|null} Provider name or null
37
+ */
38
+ export function detectInstalledCI(cwd) {
39
+ const ciFiles = {
40
+ gitlab: '.gitlab-ci.yml',
41
+ github: '.github/workflows/ci.yml',
42
+ bitbucket: 'bitbucket-pipelines.yml'
43
+ }
44
+
45
+ for (const [provider, filepath] of Object.entries(ciFiles)) {
46
+ if (existsSync(join(cwd, filepath))) {
47
+ return provider
48
+ }
49
+ }
50
+
51
+ return null
52
+ }
53
+
54
+ /**
55
+ * Check if a file's content matches the expected template
56
+ * @param {string} filepath - Path to file
57
+ * @param {string} expectedContent - Expected content
58
+ * @returns {boolean} True if content matches
59
+ */
60
+ function contentMatches(filepath, expectedContent) {
61
+ if (!existsSync(filepath)) {
62
+ return false
63
+ }
64
+
65
+ try {
66
+ const actualContent = readFileSync(filepath, 'utf8')
67
+ return actualContent.trim() === expectedContent.trim()
68
+ } catch {
69
+ return false
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Uninstall a tool by removing its config file
75
+ * @param {string} _tool - Tool name
76
+ * @param {Object} handler - Tool handler with getConfigFilename and generateConfigContent
77
+ * @param {string} cwd - Current working directory
78
+ * @param {boolean} dryRun - Preview mode
79
+ * @returns {boolean} True if uninstalled successfully
80
+ */
81
+ export function uninstallTool(_tool, handler, cwd, dryRun = false) {
82
+ const filename = handler.getConfigFilename()
83
+ const filepath = join(cwd, filename)
84
+
85
+ if (!existsSync(filepath)) {
86
+ console.log(` ℹ️ ${filename} not found - already uninstalled`)
87
+ return false
88
+ }
89
+
90
+ // Check if file was modified by user
91
+ const expectedContent = handler.generateConfigContent()
92
+ const isUnmodified = contentMatches(filepath, expectedContent)
93
+
94
+ if (!isUnmodified) {
95
+ console.log(` ⚠️ ${filename} has been modified - skipping for safety`)
96
+ console.log(' Remove manually if you want to delete it')
97
+ return false
98
+ }
99
+
100
+ if (dryRun) {
101
+ console.log(` 🗑️ Would remove: ${filename}`)
102
+ return true
103
+ }
104
+
105
+ try {
106
+ rmSync(filepath)
107
+ console.log(` ✅ Removed: ${filename}`)
108
+ return true
109
+ } catch (error) {
110
+ console.error(` ❌ Failed to remove ${filename}:`, error.message)
111
+ return false
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Uninstall CI/CD configuration
117
+ * @param {string} provider - CI provider (gitlab, github, bitbucket)
118
+ * @param {string} cwd - Current working directory
119
+ * @param {boolean} dryRun - Preview mode
120
+ * @returns {boolean} True if uninstalled successfully
121
+ */
122
+ export function uninstallCIConfig(provider, cwd, dryRun = false) {
123
+ const ciPaths = {
124
+ gitlab: '.gitlab-ci.yml',
125
+ github: '.github/workflows/ci.yml',
126
+ bitbucket: 'bitbucket-pipelines.yml'
127
+ }
128
+
129
+ const ciPath = ciPaths[provider]
130
+ if (!ciPath) {
131
+ console.log(` ⚠️ Unknown provider: ${provider}`)
132
+ return false
133
+ }
134
+
135
+ const filepath = join(cwd, ciPath)
136
+
137
+ if (!existsSync(filepath)) {
138
+ console.log(` ℹ️ ${ciPath} not found - already uninstalled`)
139
+ return false
140
+ }
141
+
142
+ if (dryRun) {
143
+ console.log(` 🗑️ Would remove: ${ciPath}`)
144
+ if (provider === 'github') {
145
+ console.log(` 🗑️ Would clean up empty directories if needed`)
146
+ }
147
+ return true
148
+ }
149
+
150
+ try {
151
+ rmSync(filepath)
152
+ console.log(` ✅ Removed: ${ciPath}`)
153
+
154
+ // Clean up empty directories for GitHub
155
+ if (provider === 'github') {
156
+ cleanupGitHubDirectories(cwd)
157
+ }
158
+
159
+ return true
160
+ } catch (error) {
161
+ console.error(` ❌ Failed to remove ${ciPath}:`, error.message)
162
+ return false
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Clean up empty GitHub directories
168
+ * @param {string} cwd - Current working directory
169
+ */
170
+ function cleanupGitHubDirectories(cwd) {
171
+ const workflowDir = join(cwd, '.github/workflows')
172
+ const githubDir = join(cwd, '.github')
173
+
174
+ // Remove .github/workflows if empty
175
+ if (existsSync(workflowDir)) {
176
+ try {
177
+ const files = readdirSync(workflowDir)
178
+ if (files.length === 0) {
179
+ rmSync(workflowDir, { recursive: true })
180
+ console.log(` ✅ Removed empty directory: .github/workflows`)
181
+ }
182
+ } catch {
183
+ // Ignore cleanup errors
184
+ }
185
+ }
186
+
187
+ // Remove .github if empty
188
+ if (existsSync(githubDir)) {
189
+ try {
190
+ const files = readdirSync(githubDir)
191
+ if (files.length === 0) {
192
+ rmSync(githubDir, { recursive: true })
193
+ console.log(` ✅ Removed empty directory: .github`)
194
+ }
195
+ } catch {
196
+ // Ignore cleanup errors
197
+ }
198
+ }
199
+ }
@@ -16,15 +16,23 @@ import * as prettierHandler from './lib/handlers/prettier.js'
16
16
  import * as semanticReleaseHandler from './lib/handlers/semantic-release.js'
17
17
  import * as stylelintHandler from './lib/handlers/stylelint.js'
18
18
  import * as typescriptHandler from './lib/handlers/typescript.js'
19
- import { updatePackageJsonScripts } from './lib/package-manager.js'
19
+ import { removePackageJsonScripts, updatePackageJsonScripts } from './lib/package-manager.js'
20
20
  import {
21
21
  showBanner,
22
22
  showCICompletion,
23
23
  showCompletion,
24
24
  showHelp,
25
25
  showSection,
26
+ showUninstallBanner,
27
+ showUninstallCompletion,
26
28
  showVersion
27
29
  } from './lib/ui.js'
30
+ import {
31
+ detectInstalledCI,
32
+ detectInstalledTools,
33
+ uninstallCIConfig,
34
+ uninstallTool
35
+ } from './lib/uninstall.js'
28
36
  import { VALID_TOOLS, validateProvider, validateTool } from './lib/validators.js'
29
37
 
30
38
  // Tool handler registry
@@ -46,27 +54,59 @@ function getToolScripts(tool, detected) {
46
54
 
47
55
  /**
48
56
  * Setup tools
57
+ * @returns {Object} Summary of created files and scripts
49
58
  */
50
59
  async function setupTools(tools, detected, cwd, dryRun = false) {
51
- showSection('📝 Generating configuration files...')
60
+ const { createProgress, SYMBOLS, colors } = await import('./lib/formatting.js')
61
+
62
+ console.log('')
63
+ console.log('Creating configuration files...')
64
+ console.log('')
52
65
 
53
66
  // Track which tools successfully created configs
54
67
  const successfulTools = []
68
+ const createdFiles = []
55
69
 
70
+ let current = 0
56
71
  for (const tool of tools) {
57
- console.log(`${tool}:`)
72
+ current++
58
73
  const handler = TOOL_HANDLERS[tool]
74
+ const filename = handler.getConfigFilename()
75
+
76
+ const progressLine = colors.dim(createProgress(current, tools.length, filename))
77
+ process.stdout.write(progressLine)
78
+
59
79
  const success = await handler.writeConfig(cwd, detected, dryRun)
80
+
60
81
  if (success) {
61
82
  successfulTools.push(tool)
83
+ createdFiles.push(filename)
84
+ console.log(` ${SYMBOLS.success}`)
85
+ } else {
86
+ console.log(` ${SYMBOLS.warning}`)
62
87
  }
63
88
  }
64
89
 
65
90
  // Only add scripts for tools that created configs
91
+ const addedScripts = []
66
92
  if (successfulTools.length > 0) {
67
- showSection('📦 Updating package.json scripts...')
93
+ console.log('')
94
+ console.log('Adding npm scripts...')
95
+ console.log('')
96
+
97
+ // Collect all scripts that will be added
98
+ for (const tool of successfulTools) {
99
+ const scripts = getToolScripts(tool, detected)
100
+ addedScripts.push(...Object.keys(scripts))
101
+ }
102
+
68
103
  updatePackageJsonScripts(successfulTools, getToolScripts, { detected, cwd, dryRun })
69
104
  }
105
+
106
+ return {
107
+ files: createdFiles,
108
+ scripts: addedScripts
109
+ }
70
110
  }
71
111
 
72
112
  /**
@@ -97,9 +137,10 @@ async function setupCI(detected, cwd, dryRun, ciProvider = null) {
97
137
 
98
138
  // If provider not detected or unknown, prompt for it
99
139
  if (!provider || provider === 'unknown') {
140
+ const { SYMBOLS } = await import('./lib/formatting.js')
100
141
  provider = await promptForProvider()
101
142
  if (!provider) {
102
- console.log('⚠️ Skipping CI/CD setup.')
143
+ console.log(`${SYMBOLS.warning} Skipping CI/CD setup.`)
103
144
  console.log('')
104
145
  return
105
146
  }
@@ -111,6 +152,32 @@ async function setupCI(detected, cwd, dryRun, ciProvider = null) {
111
152
  showCICompletion(dryRun)
112
153
  }
113
154
 
155
+ /**
156
+ * Uninstall tools
157
+ */
158
+ function uninstallTools(tools, detected, cwd, dryRun = false) {
159
+ showSection('🗑️ Removing configuration files...')
160
+
161
+ let removedCount = 0
162
+
163
+ for (const tool of tools) {
164
+ console.log(`${tool}:`)
165
+ const handler = TOOL_HANDLERS[tool]
166
+ const success = uninstallTool(tool, handler, cwd, dryRun)
167
+ if (success) {
168
+ removedCount++
169
+ }
170
+ }
171
+
172
+ // Remove scripts for successfully uninstalled tools
173
+ if (tools.length > 0) {
174
+ showSection('📦 Removing package.json scripts...')
175
+ removePackageJsonScripts(tools, getToolScripts, { detected, cwd, dryRun })
176
+ }
177
+
178
+ return removedCount
179
+ }
180
+
114
181
  /**
115
182
  * Main CLI function
116
183
  */
@@ -122,7 +189,8 @@ async function main() {
122
189
  version: args.includes('--version') || args.includes('-v'),
123
190
  dryRun: args.includes('--dry-run') || args.includes('-d'),
124
191
  all: args.includes('--all') || args.includes('-a'),
125
- setupCI: args.includes('--setup-ci')
192
+ setupCI: args.includes('--setup-ci'),
193
+ uninstall: args.includes('--uninstall')
126
194
  }
127
195
 
128
196
  // Check for --ci flag with optional provider
@@ -149,6 +217,102 @@ async function main() {
149
217
 
150
218
  const cwd = process.cwd()
151
219
 
220
+ // Handle uninstall mode
221
+ if (flags.uninstall) {
222
+ const detected = autoDetect(cwd)
223
+ const installedTools = detectInstalledTools(cwd)
224
+ const installedCI = detectInstalledCI(cwd)
225
+
226
+ showUninstallBanner(installedTools, installedCI)
227
+
228
+ // No tools or CI detected
229
+ if (installedTools.length === 0 && !installedCI) {
230
+ console.log('Nothing to uninstall.')
231
+ process.exit(0)
232
+ }
233
+
234
+ // Handle --uninstall --ci flag
235
+ if (hasCI) {
236
+ if (!installedCI) {
237
+ console.log('⚠️ No CI/CD configuration detected')
238
+ process.exit(0)
239
+ }
240
+
241
+ showSection('🗑️ Removing CI/CD configuration...')
242
+ const success = uninstallCIConfig(installedCI, cwd, flags.dryRun)
243
+ showUninstallCompletion(flags.dryRun, success ? 1 : 0)
244
+ process.exit(0)
245
+ }
246
+
247
+ // Handle --uninstall --all flag
248
+ if (flags.all) {
249
+ if (installedTools.length === 0) {
250
+ console.log('⚠️ No tools detected to uninstall')
251
+ process.exit(0)
252
+ }
253
+
254
+ const removedCount = uninstallTools(installedTools, detected, cwd, flags.dryRun)
255
+ showUninstallCompletion(flags.dryRun, removedCount)
256
+ process.exit(0)
257
+ }
258
+
259
+ // Handle --uninstall <tool> flag
260
+ if (singleTool) {
261
+ if (!installedTools.includes(singleTool)) {
262
+ console.log(`⚠️ ${singleTool} is not installed`)
263
+ process.exit(0)
264
+ }
265
+
266
+ const removedCount = uninstallTools([singleTool], detected, cwd, flags.dryRun)
267
+ showUninstallCompletion(flags.dryRun, removedCount)
268
+ process.exit(0)
269
+ }
270
+
271
+ // Interactive uninstall mode
272
+ if (installedTools.length === 0) {
273
+ console.log('⚠️ No tools detected to uninstall')
274
+ process.exit(0)
275
+ }
276
+
277
+ const response = await prompts({
278
+ type: 'multiselect',
279
+ name: 'tools',
280
+ message: 'Which tools would you like to uninstall?',
281
+ choices: installedTools.map(tool => ({
282
+ title: tool,
283
+ value: tool,
284
+ selected: false
285
+ })),
286
+ hint: '- Space to select. Return to submit'
287
+ })
288
+
289
+ if (!response.tools || response.tools.length === 0) {
290
+ console.log('')
291
+ console.log('❌ No tools selected. Exiting.')
292
+ process.exit(0)
293
+ }
294
+
295
+ const removedCount = uninstallTools(response.tools, detected, cwd, flags.dryRun)
296
+
297
+ // Ask about CI removal if detected
298
+ if (installedCI) {
299
+ const ciResponse = await prompts({
300
+ type: 'confirm',
301
+ name: 'removeCI',
302
+ message: `Remove ${installedCI} CI/CD configuration?`,
303
+ initial: false
304
+ })
305
+
306
+ if (ciResponse.removeCI) {
307
+ showSection('🗑️ Removing CI/CD configuration...')
308
+ uninstallCIConfig(installedCI, cwd, flags.dryRun)
309
+ }
310
+ }
311
+
312
+ showUninstallCompletion(flags.dryRun, removedCount)
313
+ process.exit(0)
314
+ }
315
+
152
316
  // Show banner and auto-detect
153
317
  const detected = autoDetect(cwd)
154
318
  showBanner(detected)
@@ -168,22 +332,22 @@ async function main() {
168
332
 
169
333
  // Handle --all flag
170
334
  if (flags.all) {
171
- await setupTools(VALID_TOOLS, detected, cwd, flags.dryRun)
172
- showCompletion(flags.dryRun)
335
+ const summary = await setupTools(VALID_TOOLS, detected, cwd, flags.dryRun)
336
+ showCompletion(flags.dryRun, summary.files, summary.scripts)
173
337
  process.exit(0)
174
338
  }
175
339
 
176
340
  // Handle single tool mode
177
341
  if (singleTool) {
178
342
  validateTool(singleTool)
179
- await setupTools([singleTool], detected, cwd, flags.dryRun)
343
+ const summary = await setupTools([singleTool], detected, cwd, flags.dryRun)
180
344
 
181
345
  // If semantic-release + --ci flag, setup CI
182
346
  if (singleTool === 'semantic-release' && hasCI) {
183
347
  await setupCI(detected, cwd, flags.dryRun, ciProvider)
184
348
  }
185
349
 
186
- showCompletion(flags.dryRun)
350
+ showCompletion(flags.dryRun, summary.files, summary.scripts)
187
351
  process.exit(0)
188
352
  }
189
353
 
@@ -192,23 +356,35 @@ async function main() {
192
356
  {
193
357
  type: 'multiselect',
194
358
  name: 'tools',
195
- message: 'Which tools would you like to setup?',
359
+ message: 'Select development tools to configure:',
196
360
  choices: [
197
- { title: 'ESLint (JavaScript/TypeScript linting)', value: 'eslint', selected: true },
198
- { title: 'Prettier (Code formatting)', value: 'prettier', selected: true },
199
- { title: 'Stylelint (CSS/style linting)', value: 'stylelint', selected: false },
200
361
  {
201
- title: 'TypeScript (Type checking)',
362
+ title: 'ESLint Catch bugs and enforce standards',
363
+ value: 'eslint',
364
+ selected: true
365
+ },
366
+ {
367
+ title: 'Prettier Auto-format code consistently',
368
+ value: 'prettier',
369
+ selected: true
370
+ },
371
+ {
372
+ title: 'Stylelint Lint CSS, SCSS, and style files',
373
+ value: 'stylelint',
374
+ selected: false
375
+ },
376
+ {
377
+ title: 'TypeScript Type safety and better tooling',
202
378
  value: 'typescript',
203
379
  selected: detected.typescript
204
380
  },
205
381
  {
206
- title: 'semantic-release (Automated versioning)',
382
+ title: 'semantic-release Automated versioning & releases',
207
383
  value: 'semantic-release',
208
384
  selected: false
209
385
  }
210
386
  ],
211
- hint: '- Space to select. Return to submit'
387
+ hint: ' (Recommended tools are pre-selected)'
212
388
  }
213
389
  ])
214
390
 
@@ -218,16 +394,18 @@ async function main() {
218
394
  process.exit(0)
219
395
  }
220
396
 
221
- await setupTools(response.tools, detected, cwd, flags.dryRun)
397
+ const summary = await setupTools(response.tools, detected, cwd, flags.dryRun)
222
398
 
223
399
  // If semantic-release selected, prompt for CI setup
224
400
  if (response.tools.includes('semantic-release')) {
225
- showSection('🚀 CI/CD Pipeline Setup')
401
+ console.log('')
402
+ console.log('🚀 semantic-release needs CI/CD to automate releases')
403
+ console.log('')
226
404
 
227
405
  const ciResponse = await prompts({
228
406
  type: 'confirm',
229
407
  name: 'setupCI',
230
- message: 'Setup CI/CD pipeline for automated releases?',
408
+ message: 'Setup CI/CD pipeline now?',
231
409
  initial: true
232
410
  })
233
411
 
@@ -252,7 +430,7 @@ async function main() {
252
430
  }
253
431
  }
254
432
 
255
- showCompletion(flags.dryRun)
433
+ showCompletion(flags.dryRun, summary.files, summary.scripts)
256
434
  }
257
435
 
258
436
  // Run CLI
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dimensional-innovations/tool-config",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "Universal configuration package for ESLint, Prettier, Stylelint, TypeScript, and semantic-release with auto-detection for React, Vue, Svelte, Solid, Astro, Angular, and more",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -81,6 +81,7 @@
81
81
  "eslint-plugin-svelte": "^3.12.4",
82
82
  "eslint-plugin-vue": "^10.5.0",
83
83
  "globals": "^16.4.0",
84
+ "picocolors": "^1.1.1",
84
85
  "postcss": "^8.4.49",
85
86
  "postcss-html": "^1.8.0",
86
87
  "postcss-scss": "^4.0.9",