@rolandjlevy/css-ghost 1.0.2 → 2.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/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Roland Levy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+ Standard MIT License text sourced from https://choosealicense.com/licenses/mit/
package/README.md CHANGED
@@ -7,17 +7,41 @@ Find orphaned CSS class selectors in your codebase — defined in style files bu
7
7
  Scans one or more directories for style files, extracts every class selector defined, then cross-references them against usages in your component files. Anything defined but never used is reported as an orphan — with its line number and the actual CSS printed inline.
8
8
 
9
9
  **Supported style files:**
10
+
10
11
  - `.css`, `.module.css`, `.scss`, `.sass`, `.less`
11
- - `.js` / `.ts` files using `StyleSheet.create({})` or named style objects
12
+ - `.js` / `.ts` files using `StyleSheet.create({})`, `makeStyles({})` (MUI), or named style objects
13
+ - `.css.ts` / `.css.js` files using `style()` or `styleVariants()` (vanilla-extract)
12
14
 
13
15
  **Detected usage patterns:**
16
+
14
17
  - `className="foo"`
15
18
  - `className={styles.foo}`
16
19
  - `styles['foo']`
20
+ - `styles[condition ? 'foo' : 'bar']` — both branches extracted
17
21
  - `cx('foo')`, `clsx('foo')`, `classnames('foo')`
22
+ - `cx({ active: isOn, disabled: true })` — object keys extracted
23
+ - `cx(['foo', condition && 'bar'])` — array string literals extracted
24
+ - `classes.foo` — MUI `makeStyles` / `useStyles` convention
25
+ - `import { foo } from './styles.css'` — vanilla-extract named imports
18
26
 
19
27
  **Dynamic class detection:**
20
- Files containing `styles[variable]` or template literal class names are flagged with a ⚠️ warning — results for those files may be incomplete.
28
+ Patterns where the class name is truly unknowable at parse time — `styles[variable]` or template literals containing `${}` — are flagged with a ⚠️ warning. All statically resolvable patterns (string literals, ternaries, object keys) are analysed without warnings.
29
+
30
+ ## To enable AI features
31
+
32
+ css-ghost can generate a plain English summary of your orphaned selectors using a free [OpenRouter](https://openrouter.ai) API key. Run the setup wizard once:
33
+
34
+ ```bash
35
+ css-ghost --setup
36
+ ```
37
+
38
+ It will prompt you to paste your key (free at [openrouter.ai/keys](https://openrouter.ai/keys)) and save it to `~/.css-ghost`. Then pass `--ai` to any scan:
39
+
40
+ ```bash
41
+ css-ghost ./src --ai
42
+ ```
43
+
44
+ The AI summary appears in both the terminal output and the markdown report. The core scan always runs without a key — `--ai` is entirely optional.
21
45
 
22
46
  ## Install
23
47
 
@@ -34,25 +58,183 @@ npx @rolandjlevy/css-ghost ./src
34
58
  ## Usage
35
59
 
36
60
  ```bash
37
- css-ghost <directory> [directory2] ... [options]
61
+ css-ghost [directory] [directory2] ... [options]
38
62
  ```
39
63
 
40
64
  **Options:**
41
65
 
42
- | Flag | Description | Default |
43
- | --- | --- | --- |
44
- | `--output <file>` | Output report file path | `orphan-report.md` |
45
- | `-h, --help` | Show help | — |
66
+ | Flag | Description | Default |
67
+ | ----------------- | ---------------------------------------------------- | ------------------ |
68
+ | `--output <file>` | Output report file path | `orphan-report.md` |
69
+ | `--delete` | Delete all orphaned selectors from source files | off |
70
+ | `--yes` | Skip the deletion confirmation prompt | off |
71
+ | `--strict` | Exit with code 1 when any orphans are found (CI use) | off |
72
+ | `--ghost-trail` | Show the last git commit that touched each orphan | off |
73
+ | `--install-hook` | Write a pre-commit hook to `.git/hooks/pre-commit` | — |
74
+ | `--ai` | Append an AI-generated summary (requires setup) | off |
75
+ | `--setup` | Save your OpenRouter API key for `--ai` | — |
76
+ | `-h, --help` | Show help | — |
77
+
78
+ ## CSS variable orphan detection
79
+
80
+ css-ghost also catches **orphaned CSS custom properties** — variables defined with `--my-token: value` but never consumed anywhere via `var(--my-token)`.
81
+
82
+ Usages are detected across all file types: CSS, SCSS, JS, TS, JSX, TSX, Vue, Svelte, HTML. A variable defined in `variables.css` and used in a React component is correctly seen as _used_.
83
+
84
+ ```
85
+ 🔮 --colour-danger-hover (line 8)
86
+ --colour-danger-hover: #dc2626;
87
+
88
+ 🔮 --spacing-xl (line 26)
89
+ --spacing-xl: 64px;
90
+ ```
91
+
92
+ Orphaned variables appear under a separate **🔮 Orphaned CSS variables** section in both the terminal output and the markdown report.
93
+
94
+ ---
95
+
96
+ ## Ghost trail — git history for orphans
97
+
98
+ Pass `--ghost-trail` to fetch the last git commit that changed each orphan's occurrence count in the codebase. For class-name orphans the search is limited to component file types (`.tsx`, `.jsx`, `.ts`, `.js`, etc.) so the result reflects _usage_ history rather than the style-file definition.
99
+
100
+ ```bash
101
+ css-ghost ./src --ghost-trail
102
+ ```
103
+
104
+ ```
105
+ 👻 .iconRight (line 28)
106
+ 🕵️ b5c6efb · Roland Levy · 3 weeks ago — "Refactor Button component"
107
+ .iconRight {
108
+ margin-left: 6px;
109
+ }
110
+ ```
111
+
112
+ For CSS variables the search looks for `var(--name` across all file types.
113
+
114
+ > **Note:** ghost trail runs one `git log -S` per orphan in parallel. On large histories with many orphans it may take a few seconds.
115
+
116
+ ---
117
+
118
+ ## Pre-commit hook
119
+
120
+ Install a hook that blocks commits when orphaned selectors are present:
121
+
122
+ ```bash
123
+ css-ghost --install-hook ./src
124
+ ```
125
+
126
+ This writes `.git/hooks/pre-commit` (executable, `chmod 755`) containing:
127
+
128
+ ```sh
129
+ #!/bin/sh
130
+ echo "👻 css-ghost — checking for orphaned CSS selectors..."
131
+ npx --yes @rolandjlevy/css-ghost ./src --strict --output /tmp/css-ghost-hook-report.md
132
+
133
+ if [ $? -ne 0 ]; then
134
+ echo "❌ Commit blocked: orphaned CSS selectors found"
135
+ echo " Run 'css-ghost ./src --delete' to remove them"
136
+ echo " Or skip: git commit --no-verify"
137
+ exit 1
138
+ fi
139
+ ```
140
+
141
+ Pass multiple directories to the hook installer the same way you would to a normal scan:
142
+
143
+ ```bash
144
+ css-ghost --install-hook ./src ./components ./pages
145
+ ```
146
+
147
+ To uninstall: `rm .git/hooks/pre-commit`
148
+
149
+ ### `--strict` flag
150
+
151
+ `--strict` makes css-ghost exit with code 1 when any orphans are found — the mechanism the hook depends on, but also useful in CI workflows independently:
152
+
153
+ ```bash
154
+ # In CI: fail the build if orphans exist
155
+ css-ghost ./src --strict
156
+ ```
157
+
158
+ ---
159
+
160
+ ## Interactive folder picker
161
+
162
+ Run `css-ghost` with no arguments and an arrow-key picker opens in your terminal:
163
+
164
+ ```
165
+ 📂 Directory picker — navigate into a folder, then press [a] to add it
166
+
167
+ 📍 /home/user/my-project
168
+
169
+ 📁 components
170
+ ▶ 📁 src
171
+ 📁 styles
172
+
173
+ ✅ Added to scan:
174
+ /home/user/my-project/src
175
+
176
+ [↑↓/jk] navigate [↵/→] enter [←] parent [a] add current dir [d] done [^C] quit
177
+ ```
178
+
179
+ | Key | Action |
180
+ | ---------------------- | --------------------------------------- |
181
+ | `↑` / `↓` or `k` / `j` | Move up and down the folder list |
182
+ | `↵` or `→` | Enter the highlighted folder |
183
+ | `←` or Backspace | Go up to the parent folder |
184
+ | `a` | Add the current folder to the scan list |
185
+ | `d` | Done — start the scan |
186
+ | `Ctrl+C` | Quit without scanning |
187
+
188
+ You can add **multiple folders** before pressing `d` — just navigate to each one and press `a`. If you press `d` without adding anything, the folder you're currently browsing is used.
189
+
190
+ ## Deleting orphans
191
+
192
+ Pass `--delete` after a scan to remove every flagged selector directly from its source file:
193
+
194
+ ```bash
195
+ css-ghost ./src --delete
196
+ ```
197
+
198
+ css-ghost prints the full report first, then lists what will be deleted and asks for confirmation before touching anything. Pass `--yes` to skip the prompt (useful in CI or scripts):
199
+
200
+ ```bash
201
+ css-ghost ./src --delete --yes
202
+ ```
203
+
204
+ Works across all supported file types — `.css`, `.scss`, `.less`, MUI `makeStyles`, and vanilla-extract exports.
205
+
206
+ > **Tip:** commit or stash your work before running `--delete`. There is no undo.
46
207
 
47
208
  ## Examples
48
209
 
49
210
  ```bash
211
+ # Interactive folder picker (no args)
212
+ css-ghost
213
+
50
214
  # Scan your src folder
51
215
  css-ghost ./src
52
216
 
217
+ # Show git history for each orphan
218
+ css-ghost ./src --ghost-trail
219
+
220
+ # Fail with exit code 1 when orphans exist (CI)
221
+ css-ghost ./src --strict
222
+
223
+ # Install a pre-commit hook that scans ./src
224
+ css-ghost --install-hook ./src
225
+
226
+ # Scan and delete all orphans (with confirmation prompt)
227
+ css-ghost ./src --delete
228
+
229
+ # Scan and delete without prompting
230
+ css-ghost ./src --delete --yes
231
+
53
232
  # Scan multiple directories
54
233
  css-ghost ./src ./components ./pages
55
234
 
235
+ # With AI summary
236
+ css-ghost ./src --ai
237
+
56
238
  # Write report to a custom path
57
239
  css-ghost ./src --output ./reports/css-audit.md
58
240
  ```
@@ -87,6 +269,7 @@ Running `css-ghost ./src` prints a live summary to your terminal:
87
269
  ```
88
270
 
89
271
  Each orphan shows:
272
+
90
273
  - the selector name and the **exact line number** where it's defined
91
274
  - the **full CSS rule** so you can see what it does before deciding to delete it
92
275
 
@@ -136,39 +319,65 @@ npm run scan
136
319
  npm run scan -- --output ./reports/audit.md
137
320
  ```
138
321
 
139
- ## No dependencies
322
+ ## Troubleshooting
323
+
324
+ ### `css-ghost: command not found` after global install
325
+
326
+ Your npm global bin directory may not be on your PATH. Run:
327
+
328
+ ```bash
329
+ npm bin -g
330
+ ```
331
+
332
+ Then add that path to your shell config (`~/.zshrc`, `~/.bashrc`, etc.):
140
333
 
141
- Built with Node.js built-ins only (`fs`, `path`). No npm packages required.
334
+ ```bash
335
+ export PATH="$(npm bin -g):$PATH"
336
+ ```
337
+
338
+ Reload with `source ~/.zshrc`. This is a one-time fix — it won't be needed again after that.
339
+
340
+ > This is common when Node was installed via the official installer rather than nvm or Homebrew.
341
+
342
+ ## Dependencies
343
+
344
+ One runtime dependency: [`picocolors`](https://github.com/alexeyraspopov/picocolors) for terminal colours. Everything else uses Node.js built-ins (`fs`, `path`, `readline`).
142
345
 
143
346
  ## Origin — built with OpenRouter free models via Claude Code
144
347
 
145
348
  This tool was created as a test project to explore [using OpenRouter's free model tier with Claude Code](https://www.mindstudio.ai/blog/open-router-free-models-claude-code-cost-reduction) as a way to cut AI development costs.
146
349
 
147
- Claude Code normally calls Anthropic's API directly. By overriding two environment variables you can route it through OpenRouter's free tier instead — meaning you can run agentic coding sessions at no cost using models like Llama 3.3 70B or Deepseek R1.
148
-
149
- ### Setup
350
+ Claude Code normally calls Anthropic's API directly. By overriding two environment variables you can route it through OpenRouter's free tier instead — meaning you can run agentic coding sessions at no cost.
150
351
 
151
- Get a free API key at [openrouter.ai/keys](https://openrouter.ai/keys), then add these aliases to your `~/.zshrc`:
352
+ If you've already run `css-ghost --setup`, you have an OpenRouter key. You can also export it as an environment variable — css-ghost picks it up automatically, so you don't even need `--setup`:
152
353
 
153
354
  ```bash
154
- # Store your key once
155
355
  export OPENROUTER_API_KEY="sk-or-v1-YOUR_KEY"
356
+ ```
357
+
358
+ And you can reuse the same key with Claude Code by adding these aliases to your `~/.zshrc`:
156
359
 
157
- # Llama 3.3 70B — fast, capable, free
360
+ ```bash
158
361
  alias claude-free='ANTHROPIC_BASE_URL="https://openrouter.ai/api/v1" \
159
362
  ANTHROPIC_API_KEY="$OPENROUTER_API_KEY" \
160
- claude --model "meta-llama/llama-3.3-70b-instruct:free"'
363
+ claude --model "nvidia/nemotron-3.5-lightning:free"'
161
364
 
162
- # Deepseek R1 — strongest free option for reasoning and debugging
163
365
  alias claude-r1='ANTHROPIC_BASE_URL="https://openrouter.ai/api/v1" \
164
366
  ANTHROPIC_API_KEY="$OPENROUTER_API_KEY" \
165
- claude --model "deepseek/deepseek-r1:free"'
367
+ claude --model "qwen/qwen3.8-27b:free"'
166
368
  ```
167
369
 
168
370
  Then `source ~/.zshrc`. Use `claude-free` or `claude-r1` in place of `claude` to start a free session.
169
371
 
170
- > **Note:** Free tier models handle Claude Code's tool call format well in practice, but run a quick agentic task first to confirm file read/edit/bash loops work end to end in your environment before committing to it for serious work.
372
+ > **Note:** Free model availability changes frequently. Browse current options at [openrouter.ai/models](https://openrouter.ai/models?q=free) — filter by `:free`.
171
373
 
172
374
  ## License
173
375
 
174
376
  MIT
377
+
378
+ ```bash
379
+ .-.
380
+ (o o)
381
+ | O |
382
+ '~~~'
383
+ ```
package/bin/css-ghost.js CHANGED
@@ -4,54 +4,98 @@ import { resolve } from 'path';
4
4
  import { existsSync as fsExists } from 'fs';
5
5
  import { scan } from '../src/scanner.js';
6
6
  import { generateReport } from '../src/reporter.js';
7
+ import { deleteOrphans } from '../src/deleter.js';
8
+ import { runSetup } from '../src/setup.js';
9
+ import { getAiSummary } from '../src/ai.js';
7
10
 
8
11
  const args = process.argv.slice(2);
9
12
 
10
- if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
13
+ // --setup
14
+ if (args.includes('--setup')) {
15
+ await runSetup();
16
+ process.exit(0);
17
+ }
18
+
19
+ // --install-hook [dirs...]
20
+ if (args.includes('--install-hook')) {
21
+ const { installHook } = await import('../src/hook.js');
22
+ // Any positional args become the directories the hook will scan
23
+ const hookDirs = args.filter(a => !a.startsWith('-'));
24
+ installHook(hookDirs, process.cwd());
25
+ process.exit(0);
26
+ }
27
+
28
+ if (args.includes('--help') || args.includes('-h')) {
11
29
  console.log(`
12
- 👻 css-ghost — Find orphaned CSS class selectors
30
+ 👻 css-ghost — Find orphaned CSS class selectors and custom properties
13
31
 
14
32
  Usage:
15
- css-ghost <directory> [directory2] ... [options]
33
+ css-ghost [directory] [directory2] ... [options]
34
+
35
+ When no directory is given, an interactive folder picker opens.
16
36
 
17
37
  Options:
18
- --output <file> Output report file path (default: orphan-report.md)
19
- -h, --help Show this help message
38
+ --output <file> Output report file path (default: orphan-report.md)
39
+ --delete Delete all orphaned selectors from source files (prompts first)
40
+ --yes Skip the deletion confirmation prompt (use with --delete)
41
+ --strict Exit with code 1 when orphans are found (useful in CI)
42
+ --ghost-trail Show the last git commit that touched each orphan
43
+ --install-hook Write a pre-commit hook to .git/hooks/pre-commit
44
+ --ai Append an AI-generated summary to the report
45
+ --setup Save your OpenRouter API key for use with --ai
46
+ -h, --help Show this help message
20
47
 
21
48
  Examples:
49
+ css-ghost # interactive folder picker
22
50
  css-ghost ./src
23
51
  css-ghost ./src ./components
52
+ css-ghost ./src --ghost-trail # show git history for each orphan
53
+ css-ghost ./src --strict # exit 1 when orphans found (CI mode)
54
+ css-ghost ./src --delete
55
+ css-ghost ./src --delete --yes
56
+ css-ghost --install-hook ./src # install pre-commit hook for ./src
57
+ css-ghost ./src --ai
24
58
  css-ghost ./src --output ./reports/css-audit.md
25
59
  `);
26
60
  process.exit(0);
27
61
  }
28
62
 
29
- // Parse --output flag
30
- const outputIdx = args.indexOf('--output');
31
- const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : 'orphan-report.md';
63
+ // Parse flags
64
+ const useAi = args.includes('--ai');
65
+ const doDelete = args.includes('--delete');
66
+ const skipConfirm = args.includes('--yes') || args.includes('-y');
67
+ const strict = args.includes('--strict');
68
+ const ghostTrail = args.includes('--ghost-trail');
69
+ const outputIdx = args.indexOf('--output');
70
+ const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : 'orphan-report.md';
32
71
 
33
72
  if (outputIdx !== -1 && !outputPath) {
34
73
  console.error('❌ --output requires a file path');
35
74
  process.exit(1);
36
75
  }
37
76
 
38
- // Remaining args are directories
39
- const rawDirs = args.filter((a, i) => !a.startsWith('--') && (outputIdx === -1 || i !== outputIdx + 1));
77
+ // Remaining positional args are directories (strip flags and their values)
78
+ const rawDirs = args.filter((a, i) =>
79
+ !a.startsWith('-') && (outputIdx === -1 || i !== outputIdx + 1)
80
+ );
40
81
 
82
+ // No directories given → launch interactive picker
83
+ let dirs;
41
84
  if (rawDirs.length === 0) {
42
- console.error('❌ At least one directory is required');
43
- process.exit(1);
44
- }
45
-
46
- // Validate directories exist
47
- const dirs = [];
48
- for (const d of rawDirs) {
49
- const resolved = resolve(d);
50
- if (!fsExists(resolved)) {
51
- console.error(`❌ Directory not found: ${d}`);
52
- process.exit(1);
85
+ const { pickDirectories } = await import('../src/picker.js');
86
+ dirs = await pickDirectories();
87
+ console.log('');
88
+ } else {
89
+ // Validate provided directories exist
90
+ dirs = [];
91
+ for (const d of rawDirs) {
92
+ const resolved = resolve(d);
93
+ if (!fsExists(resolved)) {
94
+ console.error(`❌ Directory not found: ${d}`);
95
+ process.exit(1);
96
+ }
97
+ dirs.push(resolved);
53
98
  }
54
- dirs.push(resolved);
55
99
  }
56
100
 
57
101
  console.log(`\n👻 css-ghost scanning ${dirs.length} director${dirs.length === 1 ? 'y' : 'ies'}...`);
@@ -65,4 +109,40 @@ if (results.length === 0) {
65
109
  process.exit(0);
66
110
  }
67
111
 
68
- generateReport(results, outputPath);
112
+ // Ghost trail — fetch git history for each orphan (opt-in, runs before report)
113
+ if (ghostTrail) {
114
+ const { isGitRepo, getGhostTrails } = await import('../src/git.js');
115
+ if (!isGitRepo(process.cwd())) {
116
+ console.warn('⚠️ --ghost-trail: not a git repository — skipping git history.\n');
117
+ } else {
118
+ const totalOrphans = results.reduce(
119
+ (n, r) => n + r.orphaned.length + (r.orphanedVars?.length ?? 0), 0
120
+ );
121
+ if (totalOrphans > 0) {
122
+ process.stdout.write(`🔍 Fetching git history for ${totalOrphans} orphan${totalOrphans !== 1 ? 's' : ''}...`);
123
+ for (const result of results) {
124
+ if (result.orphaned.length > 0) {
125
+ result.trails = await getGhostTrails(result.orphaned, process.cwd());
126
+ }
127
+ if ((result.orphanedVars?.length ?? 0) > 0) {
128
+ result.varTrails = await getGhostTrails(result.orphanedVars, process.cwd(), { isVar: true });
129
+ }
130
+ }
131
+ process.stdout.write(' done\n\n');
132
+ }
133
+ }
134
+ }
135
+
136
+ // Get AI summary before writing report (so it can be included in the .md)
137
+ const aiSummary = useAi ? await getAiSummary(results) : null;
138
+
139
+ const { totalAllOrphans } = generateReport(results, outputPath, aiSummary);
140
+
141
+ if (doDelete) {
142
+ await deleteOrphans(results, { yes: skipConfirm });
143
+ }
144
+
145
+ // --strict: exit 1 if any orphans were found (for CI / pre-commit hooks)
146
+ if (strict && totalAllOrphans > 0) {
147
+ process.exit(1);
148
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rolandjlevy/css-ghost",
3
- "version": "1.0.2",
3
+ "version": "2.0.0",
4
4
  "description": "Find orphaned CSS class selectors in your codebase",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,12 +8,17 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node bin/css-ghost.js",
11
- "scan": "node bin/css-ghost.js ./test"
11
+ "scan": "node bin/css-ghost.js ./test",
12
+ "scan:trail": "node bin/css-ghost.js ./test --ghost-trail",
13
+ "delete": "node bin/css-ghost.js ./test --delete",
14
+ "delete:yes": "node bin/css-ghost.js ./test --delete --yes",
15
+ "setup": "node bin/css-ghost.js --setup"
12
16
  },
13
17
  "files": [
14
18
  "bin",
15
19
  "src",
16
- "README.md"
20
+ "README.md",
21
+ "LICENSE"
17
22
  ],
18
23
  "keywords": [
19
24
  "css",
@@ -36,5 +41,8 @@
36
41
  "homepage": "https://github.com/rolandjlevy/css-ghost#readme",
37
42
  "engines": {
38
43
  "node": ">=18"
44
+ },
45
+ "dependencies": {
46
+ "picocolors": "^1.1.1"
39
47
  }
40
48
  }
package/src/ai.js ADDED
@@ -0,0 +1,94 @@
1
+ import { relative } from 'path';
2
+ import { loadConfig } from './setup.js';
3
+
4
+ const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
5
+ // Fallback chain — tried in order until one returns a response
6
+ const FREE_MODELS = [
7
+ 'nvidia/nemotron-3.5-lightning:free',
8
+ 'google/gemma-4-31b-it:free',
9
+ 'qwen/qwen3.8-27b:free',
10
+ 'thinkingmachines/inkling:free',
11
+ 'nvidia/nemotron-3-super-120b-a12b:free',
12
+ ];
13
+
14
+ function getApiKey() {
15
+ if (process.env.OPENROUTER_API_KEY) return process.env.OPENROUTER_API_KEY;
16
+ const config = loadConfig();
17
+ return config.openrouterApiKey || null;
18
+ }
19
+
20
+ function getModelOverride() {
21
+ if (process.env.OPENROUTER_MODEL) return process.env.OPENROUTER_MODEL;
22
+ const config = loadConfig();
23
+ return config.openrouterModel || null;
24
+ }
25
+
26
+ async function tryModel(model, key, prompt) {
27
+ const response = await fetch(OPENROUTER_URL, {
28
+ method: 'POST',
29
+ headers: {
30
+ 'Authorization': `Bearer ${key}`,
31
+ 'Content-Type': 'application/json',
32
+ 'HTTP-Referer': 'https://github.com/rolandjlevy/css-ghost',
33
+ 'X-Title': 'css-ghost',
34
+ },
35
+ body: JSON.stringify({
36
+ model,
37
+ messages: [{ role: 'user', content: prompt }],
38
+ max_tokens: 300,
39
+ }),
40
+ });
41
+
42
+ if (!response.ok) return null;
43
+ const data = await response.json();
44
+ return data.choices?.[0]?.message?.content?.trim() || null;
45
+ }
46
+
47
+ function buildPrompt(results) {
48
+ const totalOrphaned = results.reduce((n, r) => n + r.orphaned.length, 0);
49
+ const filesWithOrphans = results.filter(r => r.orphaned.length > 0);
50
+
51
+ const details = filesWithOrphans.map(r => {
52
+ const relPath = relative(process.cwd(), r.filePath);
53
+ const names = r.orphaned.map(o => `.${o.name}`).join(', ');
54
+ return `- ${relPath}: ${names}`;
55
+ }).join('\n');
56
+
57
+ return `You are a CSS code quality assistant. A static analysis tool found ${totalOrphaned} orphaned CSS selectors (defined but never referenced in any component file) across ${filesWithOrphans.length} file(s):
58
+
59
+ ${details}
60
+
61
+ Write a short, practical summary (3–5 sentences) for a developer. Cover:
62
+ 1. Whether this looks like a significant problem or minor cleanup
63
+ 2. Which files or selectors are most worth investigating first
64
+ 3. One concrete suggestion for how to approach the cleanup
65
+
66
+ Be direct and specific. No fluff.`;
67
+ }
68
+
69
+ export async function getAiSummary(results) {
70
+ const key = getApiKey();
71
+
72
+ if (!key) {
73
+ console.log('💡 Tip: Run css-ghost --setup to enable AI summaries with --ai\n');
74
+ return null;
75
+ }
76
+
77
+ const override = getModelOverride();
78
+ const models = override ? [override] : FREE_MODELS;
79
+ const prompt = buildPrompt(results);
80
+
81
+ console.log('🤖 Generating AI summary...\n');
82
+
83
+ for (const model of models) {
84
+ try {
85
+ const content = await tryModel(model, key, prompt);
86
+ if (content) return content;
87
+ } catch {
88
+ // try next model
89
+ }
90
+ }
91
+
92
+ console.warn('⚠️ AI summary unavailable — all free models failed or returned empty responses.\n');
93
+ return null;
94
+ }