client-handover 1.2.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sabrkei
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.
package/README.md ADDED
@@ -0,0 +1,226 @@
1
+ <div align="center">
2
+ <img src="images/claude-client-handover.png" width="120" alt="claude-client-handover" />
3
+ <h1>claude-client-handover</h1>
4
+ <p><em>Scott AK (sabrkei)</em></p>
5
+ </div>
6
+
7
+ AI-powered handover document generator for frontend developers handing off websites to clients.
8
+
9
+ Run one command. Get a professional handover document in Markdown, plain text, and HTML — written for both your client and the next developer.
10
+
11
+ Uses the [Claude API](https://console.anthropic.com) (Anthropic) under the hood.
12
+
13
+ ---
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install -g client-handover
19
+ ```
20
+
21
+ ### API key setup
22
+
23
+ **If you have Claude Code installed**, no setup needed — `client-handover` will automatically use your existing Claude credentials.
24
+
25
+ **Otherwise**, set your Anthropic API key as an environment variable:
26
+
27
+ ```bash
28
+ # macOS / Linux
29
+ export ANTHROPIC_API_KEY=your_key_here
30
+
31
+ # Windows (Command Prompt)
32
+ set ANTHROPIC_API_KEY=your_key_here
33
+
34
+ # Windows (PowerShell)
35
+ $env:ANTHROPIC_API_KEY="your_key_here"
36
+ ```
37
+
38
+ Get a free API key at [console.anthropic.com](https://console.anthropic.com)
39
+
40
+ ---
41
+
42
+ ## Quick start
43
+
44
+ ```bash
45
+ handover handover
46
+ ```
47
+
48
+ This generates a full handover document with placeholder content in an `output/` folder in your current directory.
49
+
50
+ ---
51
+
52
+ ## Commands
53
+
54
+ | Command | Description |
55
+ |---------|-------------|
56
+ | `all` | Every section as separate files in one folder |
57
+ | `handover` | Full handover document (all sections combined) |
58
+ | `setup` | Project setup & dependencies |
59
+ | `deploy` | Deployment & hosting info |
60
+ | `credentials` | Logins & API keys template |
61
+ | `license` | Licensing & attribution |
62
+
63
+ ---
64
+
65
+ ## Usage
66
+
67
+ ### With your own project notes
68
+
69
+ Create a plain text file describing your project:
70
+
71
+ ```
72
+ project-info.txt
73
+ ----------------
74
+ Project: Acme Corp website
75
+ Stack: Vue 3, Vite, Netlify
76
+ Repo: https://github.com/you/acme
77
+ Live URL: https://acmecorp.com
78
+ Hosting: Netlify (free tier)
79
+ Domain: Namecheap, auto-renews Jan 2026
80
+ CMS: Netlify CMS
81
+ Analytics: Google Analytics 4
82
+ APIs: Mailchimp (newsletter), Stripe (payments)
83
+ ```
84
+
85
+ Then run any command with that file as input:
86
+
87
+ ```bash
88
+ # Every section as separate files in one folder
89
+ handover all project-info.txt acme-client
90
+
91
+ # Full combined doc
92
+ handover handover project-info.txt
93
+
94
+ # Full combined doc with a custom output filename
95
+ handover handover project-info.txt acme-handover
96
+
97
+ # Individual sections
98
+ handover setup project-info.txt
99
+ handover deploy project-info.txt
100
+ handover credentials project-info.txt
101
+ handover license project-info.txt
102
+ ```
103
+
104
+ The more detail you put in your project-info file, the more accurate and useful the output will be.
105
+
106
+ ---
107
+
108
+ ## Output
109
+
110
+ All commands generate three files per section inside an `output/` folder.
111
+
112
+ **Single command** (e.g. `handover`):
113
+ ```
114
+ output/
115
+ ├── handover.md ← Paste into Notion, GitHub, or a README
116
+ ├── handover.txt ← Clean plain text for email or printing
117
+ └── handover.html ← Styled HTML ready to send directly to a client
118
+ ```
119
+
120
+ **`all` command** — every section in its own subfolder:
121
+ ```
122
+ output/acme-client/
123
+ ├── handover.md / .txt / .html
124
+ ├── setup.md / .txt / .html
125
+ ├── deploy.md / .txt / .html
126
+ ├── credentials.md / .txt / .html
127
+ └── license.md / .txt / .html
128
+ ```
129
+
130
+ | Format | Best for |
131
+ |--------|----------|
132
+ | `.md` | Notion, GitHub, linear docs |
133
+ | `.txt` | Email attachments, printing |
134
+ | `.html`| Sending directly to a client |
135
+
136
+ ---
137
+
138
+ ## Use as a library
139
+
140
+ You can also import the prompt builders and doc generator directly into your own project:
141
+
142
+ ```js
143
+ import { handover, setup, generateDoc } from 'client-handover'
144
+
145
+ // Build a prompt from your project info string
146
+ const prompt = handover('Vue 3 project hosted on Vercel, domain on Cloudflare...')
147
+
148
+ // Generate and save the document
149
+ await generateDoc(prompt, 'my-client', './docs')
150
+ ```
151
+
152
+ ### Available exports
153
+
154
+ | Export | Type | Description |
155
+ |--------|------|-------------|
156
+ | `generateDoc(prompt, name, dir)` | async function | Calls Claude API and writes `.md`, `.txt`, `.html` |
157
+ | `handover(projectInfo)` | function | Prompt builder for full handover doc |
158
+ | `setup(projectInfo)` | function | Prompt builder for setup section |
159
+ | `deploy(projectInfo)` | function | Prompt builder for deployment section |
160
+ | `credentials(projectInfo)` | function | Prompt builder for credentials section |
161
+ | `license(projectInfo)` | function | Prompt builder for licensing section |
162
+
163
+ All prompt builders accept an optional `projectInfo` string. If omitted, Claude generates placeholder content.
164
+
165
+ ---
166
+
167
+ ## Requirements
168
+
169
+ - Node.js 18+
170
+ - One of the following:
171
+ - [Claude Code](https://marketplace.visualstudio.com/items?itemName=anthropic.claude-code) installed (zero config — credentials detected automatically)
172
+ - Or an Anthropic API key — [get one free at console.anthropic.com](https://console.anthropic.com)
173
+
174
+ ---
175
+
176
+ ## How it works
177
+
178
+ 1. You provide a plain text description of your project (or nothing, for placeholder output)
179
+ 2. The CLI builds a structured prompt for Claude
180
+ 3. Claude generates a professional, dual-audience document (plain English for clients, technical detail for developers)
181
+ 4. The output is saved as `.md`, `.txt`, and `.html`
182
+
183
+ Each document section is written for **two audiences**:
184
+ - **The client** — plain English, reassuring tone, no jargon
185
+ - **The next developer** — precise technical detail, commands, file paths
186
+
187
+ ---
188
+
189
+ ## Project structure
190
+
191
+ ```
192
+ client-handover/
193
+ ├── cli.js # CLI entry point
194
+ ├── index.js # Library exports
195
+ ├── generator.js # Claude API call + file output
196
+ ├── handover.js # Full handover prompt builder
197
+ ├── setup.js # Setup section prompt builder
198
+ ├── deploy.js # Deployment section prompt builder
199
+ ├── credentials.js # Credentials section prompt builder
200
+ └── license.js # Licensing section prompt builder
201
+ ```
202
+
203
+ ---
204
+
205
+ ## Changelog
206
+
207
+ ### 1.0.0
208
+ - Initial release
209
+ - Single command: `handover /create`
210
+ - Generates two documents per run — technical handover (for developers) and client handover (plain English)
211
+ - Auto-scans project files: package.json, config files, env variable keys, deploy configs, folder structure, CSS colours
212
+ - First-run setup prompts for API key, name, company, email, and phone
213
+ - Outputs `.md`, `.txt`, and `.docx` formats
214
+ - Auto-detects Claude Code credentials — no API key setup needed if you have Claude Code installed
215
+
216
+ ---
217
+
218
+ ## Contributing
219
+
220
+ Pull requests are welcome. For major changes, open an issue first.
221
+
222
+ ---
223
+
224
+ ## License
225
+
226
+ MIT
package/cli.js ADDED
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { generateDoc, resolveApiKey, saveApiKey } from './generator.js'
4
+ import { technicalHandoverPrompt, nonTechnicalHandoverPrompt } from './prompts.js'
5
+ import { scanProject } from './scanner.js'
6
+ import chalk from 'chalk'
7
+ import readline from 'readline'
8
+ import fs from 'fs'
9
+ import path from 'path'
10
+ import os from 'os'
11
+
12
+ const { version } = JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf-8'))
13
+
14
+ function normalizeCommand(cmd) {
15
+ return path.basename(cmd).replace(/^\/+/, '')
16
+ }
17
+
18
+ function printBanner() {
19
+ const t = chalk.bold.white
20
+ console.log()
21
+ console.log(' ' + t('█ █ ███ █ █ ████ ███ █ █ █████ ████ '))
22
+ console.log(' ' + t('█ █ █ █ ██ █ █ █ █ █ █ █ █ █ █'))
23
+ console.log(' ' + t('█████ █████ █ █ █ █ █ █ █ █ █ ████ ████ '))
24
+ console.log(' ' + t('█ █ █ █ █ ██ █ █ █ █ █ █ █ █ █ '))
25
+ console.log(' ' + t('█ █ █ █ █ █ ████ ███ █ █████ █ █ '))
26
+ console.log()
27
+ console.log(chalk.dim(' Website handover documents for clients & developers') + chalk.dim(' · v' + version))
28
+ console.log()
29
+ }
30
+
31
+ function printHelp() {
32
+ printBanner()
33
+ console.log(chalk.dim(' Usage:'))
34
+ console.log(' handover /create Generate technical & non-technical handover documents')
35
+ console.log(' handover key <key> Save your Anthropic API key\n')
36
+ console.log(chalk.dim(' Example:'))
37
+ console.log(' cd my-client-project')
38
+ console.log(' handover /create\n')
39
+ }
40
+
41
+ function ask(rl, question) {
42
+ return new Promise(resolve => rl.question(question, answer => resolve(answer.trim())))
43
+ }
44
+
45
+
46
+ async function runSetup() {
47
+ const configDir = path.join(os.homedir(), '.handover')
48
+ const configPath = path.join(configDir, 'config.json')
49
+
50
+ let config = {}
51
+ try {
52
+ if (fs.existsSync(configPath)) config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
53
+ } catch {}
54
+
55
+ const hasApiKey = !!resolveApiKey(config)
56
+ const hasDeveloperInfo = !!config.developerName
57
+
58
+ if (hasApiKey && hasDeveloperInfo) {
59
+ return {
60
+ name: config.developerName,
61
+ company: config.developerCompany || '',
62
+ email: config.developerEmail || '',
63
+ phone: config.developerPhone || '',
64
+ }
65
+ }
66
+
67
+ console.log(chalk.bold('\n──────────────────────────────────────────'))
68
+ console.log(chalk.bold(' client-handover — first time setup'))
69
+ console.log(chalk.bold('──────────────────────────────────────────\n'))
70
+
71
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
72
+
73
+ if (!hasApiKey) {
74
+ console.log(' To generate documents you need an Anthropic API key.')
75
+ console.log(chalk.dim(' Get one free at: https://console.anthropic.com\n'))
76
+ const key = await ask(rl, ' Enter your Anthropic API key (or press Enter to skip): ')
77
+ if (key) {
78
+ config.apiKey = key
79
+ console.log(chalk.green(' ✓ API key saved.\n'))
80
+ } else {
81
+ console.log(chalk.dim(' Skipped. Run "handover key <your-api-key>" at any time.\n'))
82
+ }
83
+ }
84
+
85
+ if (!hasDeveloperInfo) {
86
+ console.log(' Your details will appear in all generated handover documents.\n')
87
+ const name = await ask(rl, ' Your full name: ')
88
+ if (name) config.developerName = name
89
+
90
+ const company = await ask(rl, ' Your company name (press Enter if not applicable): ')
91
+ if (company) config.developerCompany = company
92
+
93
+ const email = await ask(rl, ' Your email address: ')
94
+ if (email) config.developerEmail = email
95
+
96
+ const phone = await ask(rl, ' Your phone number (optional, press Enter to skip): ')
97
+ if (phone) config.developerPhone = phone
98
+
99
+ console.log()
100
+ }
101
+
102
+ rl.close()
103
+
104
+ if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true })
105
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8')
106
+ console.log(chalk.green(' ✓ Setup saved.\n'))
107
+
108
+ return {
109
+ name: config.developerName || '',
110
+ company: config.developerCompany || '',
111
+ email: config.developerEmail || '',
112
+ phone: config.developerPhone || '',
113
+ }
114
+ }
115
+
116
+ async function runCreate() {
117
+ printBanner()
118
+ const developerInfo = await runSetup()
119
+
120
+ console.log(chalk.dim(' Scanning project files...'))
121
+ const projectInfo = scanProject(process.cwd())
122
+
123
+ const techDir = './technical-handover'
124
+ const clientDir = './handover'
125
+
126
+ console.log(chalk.bold('\n Generating Technical Handover Document...'))
127
+ const techPrompt = technicalHandoverPrompt(projectInfo, developerInfo)
128
+ await generateDoc(techPrompt, 'technical-handover', techDir)
129
+
130
+ console.log(chalk.bold('\n Generating Client Handover Document...'))
131
+ const nonTechPrompt = nonTechnicalHandoverPrompt(projectInfo, developerInfo)
132
+ await generateDoc(nonTechPrompt, 'handover', clientDir)
133
+
134
+ console.log(chalk.green.bold('\n ✅ Handover documents created successfully!\n'))
135
+ console.log(` ${chalk.cyan('./technical-handover/')}`)
136
+ console.log(` technical-handover.md`)
137
+ console.log(` technical-handover.txt`)
138
+ console.log(` technical-handover.docx`)
139
+ console.log()
140
+ console.log(` ${chalk.cyan('./handover/')}`)
141
+ console.log(` handover.md`)
142
+ console.log(` handover.txt`)
143
+ console.log(` handover.docx`)
144
+ console.log()
145
+ }
146
+
147
+ async function main() {
148
+ const [,, rawCommand, secondArg] = process.argv
149
+
150
+ if (!rawCommand || rawCommand === '--help' || rawCommand === '-h') {
151
+ printHelp()
152
+ process.exit(0)
153
+ }
154
+
155
+ const command = normalizeCommand(rawCommand)
156
+
157
+ if (command === 'key') {
158
+ if (!secondArg) {
159
+ console.error(chalk.red('\n ❌ Usage: handover key <your-api-key>\n'))
160
+ process.exit(1)
161
+ }
162
+ saveApiKey(secondArg)
163
+ console.log(chalk.green('\n ✅ API key saved. Run "handover /create" to get started.\n'))
164
+ process.exit(0)
165
+ }
166
+
167
+ if (command !== 'create') {
168
+ console.error(chalk.red(`\n ❌ Unknown command: ${rawCommand}\n`))
169
+ printHelp()
170
+ process.exit(1)
171
+ }
172
+
173
+ try {
174
+ await runCreate()
175
+ } catch (err) {
176
+ if (err.status === 401) {
177
+ console.error(chalk.red('\n ❌ Authentication failed. Run "handover key <your-api-key>" to set your API key.\n'))
178
+ } else {
179
+ console.error(chalk.red(`\n ❌ Error: ${err.message}\n`))
180
+ }
181
+ process.exit(1)
182
+ }
183
+ }
184
+
185
+ main()
package/generator.js ADDED
@@ -0,0 +1,343 @@
1
+ import Anthropic from '@anthropic-ai/sdk'
2
+ import {
3
+ Document, Packer, Paragraph, TextRun, HeadingLevel,
4
+ AlignmentType, BorderStyle, TableRow, TableCell, Table,
5
+ WidthType, ShadingType
6
+ } from 'docx'
7
+ import fs from 'fs'
8
+ import path from 'path'
9
+ import os from 'os'
10
+
11
+ export function resolveApiKey(config = null) {
12
+ if (process.env.ANTHROPIC_API_KEY) return process.env.ANTHROPIC_API_KEY
13
+
14
+ const cfg = config || (() => {
15
+ try {
16
+ const p = path.join(os.homedir(), '.handover', 'config.json')
17
+ if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
18
+ } catch {}
19
+ return {}
20
+ })()
21
+
22
+ if (cfg?.apiKey) return cfg.apiKey
23
+
24
+ const credentialsPath = path.join(os.homedir(), '.claude', '.credentials.json')
25
+ try {
26
+ if (fs.existsSync(credentialsPath)) {
27
+ const creds = JSON.parse(fs.readFileSync(credentialsPath, 'utf-8'))
28
+ const token = creds?.claudeAiOauth?.accessToken
29
+ const expiresAt = creds?.claudeAiOauth?.expiresAt
30
+ if (token && (!expiresAt || expiresAt > Date.now())) return token
31
+ }
32
+ } catch {}
33
+
34
+ return null
35
+ }
36
+
37
+ export function saveApiKey(key) {
38
+ const configDir = path.join(os.homedir(), '.handover')
39
+ const configPath = path.join(configDir, 'config.json')
40
+ if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true })
41
+ let config = {}
42
+ try { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) } catch {}
43
+ config.apiKey = key
44
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8')
45
+ }
46
+
47
+ const apiKey = resolveApiKey()
48
+ if (!apiKey) {
49
+ console.error('\n ❌ No API key found. Run "handover key <your-api-key>" or install Claude Code.\n')
50
+ process.exit(1)
51
+ }
52
+
53
+ const client = new Anthropic({ apiKey })
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Inline text parser — splits a line into TextRun segments handling **bold**,
57
+ // *italic*, `code`, and [link text](url) → just the text
58
+ // ---------------------------------------------------------------------------
59
+ function parseInline(text) {
60
+ const runs = []
61
+ // Strip markdown links to plain text
62
+ text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
63
+
64
+ const pattern = /(\*\*(.+?)\*\*|\*(.+?)\*|`(.+?)`)/g
65
+ let lastIndex = 0
66
+ let match
67
+
68
+ while ((match = pattern.exec(text)) !== null) {
69
+ if (match.index > lastIndex) {
70
+ runs.push(new TextRun({ text: text.slice(lastIndex, match.index) }))
71
+ }
72
+ if (match[2] !== undefined) {
73
+ runs.push(new TextRun({ text: match[2], bold: true }))
74
+ } else if (match[3] !== undefined) {
75
+ runs.push(new TextRun({ text: match[3], italics: true }))
76
+ } else if (match[4] !== undefined) {
77
+ runs.push(new TextRun({
78
+ text: match[4],
79
+ font: 'Courier New',
80
+ size: 18,
81
+ shading: { type: ShadingType.CLEAR, fill: 'F4F4F4' },
82
+ }))
83
+ }
84
+ lastIndex = match.index + match[0].length
85
+ }
86
+
87
+ if (lastIndex < text.length) {
88
+ runs.push(new TextRun({ text: text.slice(lastIndex) }))
89
+ }
90
+
91
+ return runs.length ? runs : [new TextRun({ text })]
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Convert markdown string to an array of docx Paragraph/Table objects
96
+ // ---------------------------------------------------------------------------
97
+ function markdownToDocxChildren(markdown) {
98
+ const children = []
99
+ const lines = markdown.split('\n')
100
+ let i = 0
101
+
102
+ while (i < lines.length) {
103
+ const line = lines[i]
104
+
105
+ // Fenced code block
106
+ if (line.trim().startsWith('```')) {
107
+ const codeLines = []
108
+ i++
109
+ while (i < lines.length && !lines[i].trim().startsWith('```')) {
110
+ codeLines.push(lines[i])
111
+ i++
112
+ }
113
+ for (const codeLine of codeLines) {
114
+ children.push(new Paragraph({
115
+ children: [new TextRun({ text: codeLine || ' ', font: 'Courier New', size: 18 })],
116
+ shading: { type: ShadingType.CLEAR, fill: 'F4F4F4' },
117
+ spacing: { before: 0, after: 0 },
118
+ indent: { left: 360 },
119
+ }))
120
+ }
121
+ // Add small gap after code block
122
+ children.push(new Paragraph({ text: '' }))
123
+ i++
124
+ continue
125
+ }
126
+
127
+ // Horizontal rule
128
+ if (/^---+$/.test(line.trim())) {
129
+ children.push(new Paragraph({
130
+ text: '',
131
+ border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: 'CCCCCC' } },
132
+ spacing: { before: 200, after: 200 },
133
+ }))
134
+ i++
135
+ continue
136
+ }
137
+
138
+ // Headings
139
+ const h1 = line.match(/^# (.+)/)
140
+ const h2 = line.match(/^## (.+)/)
141
+ const h3 = line.match(/^### (.+)/)
142
+ const h4 = line.match(/^#{4,6} (.+)/)
143
+
144
+ if (h1) {
145
+ children.push(new Paragraph({
146
+ children: parseInline(h1[1]),
147
+ heading: HeadingLevel.HEADING_1,
148
+ spacing: { before: 400, after: 160 },
149
+ }))
150
+ i++; continue
151
+ }
152
+ if (h2) {
153
+ children.push(new Paragraph({
154
+ children: parseInline(h2[1]),
155
+ heading: HeadingLevel.HEADING_2,
156
+ spacing: { before: 320, after: 120 },
157
+ }))
158
+ i++; continue
159
+ }
160
+ if (h3) {
161
+ children.push(new Paragraph({
162
+ children: parseInline(h3[1]),
163
+ heading: HeadingLevel.HEADING_3,
164
+ spacing: { before: 240, after: 80 },
165
+ }))
166
+ i++; continue
167
+ }
168
+ if (h4) {
169
+ children.push(new Paragraph({
170
+ children: [new TextRun({ text: h4[1], bold: true })],
171
+ spacing: { before: 160, after: 80 },
172
+ }))
173
+ i++; continue
174
+ }
175
+
176
+ // Checkbox list items - [ ] or - [x]
177
+ const checkbox = line.match(/^(\s*)- \[(x| )\] (.+)/i)
178
+ if (checkbox) {
179
+ const checked = checkbox[2].toLowerCase() === 'x'
180
+ children.push(new Paragraph({
181
+ children: [
182
+ new TextRun({ text: checked ? '☑ ' : '☐ ' }),
183
+ ...parseInline(checkbox[3]),
184
+ ],
185
+ indent: { left: 360 },
186
+ spacing: { before: 40, after: 40 },
187
+ }))
188
+ i++; continue
189
+ }
190
+
191
+ // Bullet list items - or *
192
+ const bullet = line.match(/^(\s*)[-*+] (.+)/)
193
+ if (bullet) {
194
+ const indent = bullet[1].length > 0 ? 720 : 360
195
+ children.push(new Paragraph({
196
+ children: [new TextRun({ text: '• ' }), ...parseInline(bullet[2])],
197
+ indent: { left: indent },
198
+ spacing: { before: 40, after: 40 },
199
+ }))
200
+ i++; continue
201
+ }
202
+
203
+ // Numbered list items
204
+ const numbered = line.match(/^\d+\. (.+)/)
205
+ if (numbered) {
206
+ children.push(new Paragraph({
207
+ children: parseInline(numbered[1]),
208
+ numbering: { reference: 'default-numbering', level: 0 },
209
+ indent: { left: 360 },
210
+ spacing: { before: 40, after: 40 },
211
+ }))
212
+ i++; continue
213
+ }
214
+
215
+ // Bold-only line (often used as a label/key line like **Key:** value)
216
+ // Handled by parseInline already — fall through to normal paragraph
217
+
218
+ // Empty line
219
+ if (line.trim() === '') {
220
+ children.push(new Paragraph({ text: '', spacing: { before: 0, after: 80 } }))
221
+ i++; continue
222
+ }
223
+
224
+ // Normal paragraph
225
+ children.push(new Paragraph({
226
+ children: parseInline(line),
227
+ spacing: { before: 0, after: 80 },
228
+ }))
229
+ i++
230
+ }
231
+
232
+ return children
233
+ }
234
+
235
+ // ---------------------------------------------------------------------------
236
+ // Build a docx Document from markdown
237
+ // ---------------------------------------------------------------------------
238
+ function buildDocx(markdown) {
239
+ const children = markdownToDocxChildren(markdown)
240
+
241
+ return new Document({
242
+ numbering: {
243
+ config: [{
244
+ reference: 'default-numbering',
245
+ levels: [{
246
+ level: 0,
247
+ format: 'decimal',
248
+ text: '%1.',
249
+ alignment: AlignmentType.LEFT,
250
+ style: { paragraph: { indent: { left: 360, hanging: 260 } } },
251
+ }],
252
+ }],
253
+ },
254
+ styles: {
255
+ default: {
256
+ document: {
257
+ run: { font: 'Calibri', size: 22 },
258
+ paragraph: { spacing: { line: 276 } },
259
+ },
260
+ },
261
+ paragraphStyles: [
262
+ {
263
+ id: 'Heading1',
264
+ name: 'Heading 1',
265
+ basedOn: 'Normal',
266
+ next: 'Normal',
267
+ run: { bold: true, size: 36, color: '1a1a1a' },
268
+ paragraph: { spacing: { before: 400, after: 160 } },
269
+ },
270
+ {
271
+ id: 'Heading2',
272
+ name: 'Heading 2',
273
+ basedOn: 'Normal',
274
+ next: 'Normal',
275
+ run: { bold: true, size: 28, color: '2c2c2c' },
276
+ paragraph: {
277
+ spacing: { before: 320, after: 120 },
278
+ border: { bottom: { style: BorderStyle.SINGLE, size: 4, color: 'E0E0E0' } },
279
+ },
280
+ },
281
+ {
282
+ id: 'Heading3',
283
+ name: 'Heading 3',
284
+ basedOn: 'Normal',
285
+ next: 'Normal',
286
+ run: { bold: true, size: 24, color: '444444' },
287
+ paragraph: { spacing: { before: 240, after: 80 } },
288
+ },
289
+ ],
290
+ },
291
+ sections: [{ properties: {}, children }],
292
+ })
293
+ }
294
+
295
+ // ---------------------------------------------------------------------------
296
+ // Main export
297
+ // ---------------------------------------------------------------------------
298
+ export async function generateDoc(prompt, outputName = 'handover', outputDir = './output') {
299
+ console.log('⏳ Generating document with Claude...\n')
300
+
301
+ const message = await client.messages.create({
302
+ model: 'claude-sonnet-4-20250514',
303
+ max_tokens: 4096,
304
+ messages: [{ role: 'user', content: prompt }]
305
+ })
306
+
307
+ const markdown = message.content[0].text
308
+
309
+ if (!fs.existsSync(outputDir)) {
310
+ fs.mkdirSync(outputDir, { recursive: true })
311
+ }
312
+
313
+ const basePath = path.join(outputDir, outputName)
314
+
315
+ // Save as Markdown
316
+ const mdPath = `${basePath}.md`
317
+ fs.writeFileSync(mdPath, markdown, 'utf-8')
318
+ console.log(`✅ Markdown saved: ${mdPath}`)
319
+
320
+ // Save as plain text
321
+ const plainText = markdown
322
+ .replace(/#{1,6}\s+/g, '')
323
+ .replace(/\*\*(.*?)\*\*/g, '$1')
324
+ .replace(/\*(.*?)\*/g, '$1')
325
+ .replace(/`{1,3}[^`]*`{1,3}/g, m => m.replace(/`/g, ''))
326
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
327
+ .replace(/[-*+] /g, '• ')
328
+ .replace(/\n{3,}/g, '\n\n')
329
+ .trim()
330
+
331
+ const txtPath = `${basePath}.txt`
332
+ fs.writeFileSync(txtPath, plainText, 'utf-8')
333
+ console.log(`✅ Plain text saved: ${txtPath}`)
334
+
335
+ // Save as Word document
336
+ const doc = buildDocx(markdown)
337
+ const docxBuffer = await Packer.toBuffer(doc)
338
+ const docxPath = `${basePath}.docx`
339
+ fs.writeFileSync(docxPath, docxBuffer)
340
+ console.log(`✅ Word doc saved: ${docxPath}`)
341
+
342
+ return { markdown, plainText }
343
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "client-handover",
3
+ "version": "1.2.0",
4
+ "description": "AI-powered handover document generator for frontend developers handing off client websites",
5
+ "type": "module",
6
+ "bin": {
7
+ "handover": "cli.js"
8
+ },
9
+ "files": [
10
+ "cli.js",
11
+ "generator.js",
12
+ "prompts.js",
13
+ "scanner.js",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "keywords": [
18
+ "handover",
19
+ "frontend",
20
+ "client",
21
+ "documentation",
22
+ "developer",
23
+ "claude",
24
+ "ai"
25
+ ],
26
+ "author": "sabrkei",
27
+ "license": "MIT",
28
+ "engines": {
29
+ "node": ">=18"
30
+ },
31
+ "dependencies": {
32
+ "@anthropic-ai/sdk": "^0.39.0",
33
+ "chalk": "^5.3.0",
34
+ "docx": "^8.5.0"
35
+ }
36
+ }
package/prompts.js ADDED
@@ -0,0 +1,185 @@
1
+ export function technicalHandoverPrompt(projectInfo, developerInfo) {
2
+ const { name, company, email, phone } = developerInfo
3
+ const date = new Date().toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' })
4
+
5
+ return `
6
+ You are an expert technical writer generating a TECHNICAL HANDOVER DOCUMENT for a developer taking over a client website project.
7
+
8
+ This document is written for a DEVELOPER (future maintainer) — be precise, thorough, and technically complete. They need to be able to pick this project up cold with no prior knowledge.
9
+
10
+ Developer who built this project:
11
+ - Name: ${name || '[Developer Name]'}
12
+ ${company ? `- Company: ${company}` : ''}
13
+ - Email: ${email || '[Developer Email]'}
14
+ ${phone ? `- Phone: ${phone}` : ''}
15
+ - Handover date: ${date}
16
+
17
+ The following context has been automatically scanned from the project files (package.json, config files, folder structure, environment variable keys, deploy configs, README, CSS colors, etc.). Use this to generate a document specific to THIS project. Do not invent or use generic placeholders — if a specific detail is not in the scanned data, note it as "[to be confirmed]".
18
+
19
+ ${projectInfo || '[No project data available]'}
20
+
21
+ Generate a complete technical handover document with ALL of the following sections:
22
+
23
+ ---
24
+
25
+ # [Project Name] — Technical Handover Document
26
+
27
+ **Prepared by:** ${name || '[Developer Name]'}${company ? ` · ${company}` : ''}${email ? ` · ${email}` : ''}${phone ? ` · ${phone}` : ''}
28
+ **Handover Date:** ${date}
29
+ **Project URL:** [Live URL — to be confirmed]
30
+ **Repository:** [Repo URL — to be confirmed]
31
+
32
+ ---
33
+
34
+ ## 1. Tech Stack
35
+ - List every technology, framework, library, and tool used
36
+ - Include versions where known
37
+ - Briefly explain the role of each in the project
38
+
39
+ ## 2. Project Structure
40
+ - Full folder/file structure with explanations of what each directory contains
41
+ - Location of key config files and what they control
42
+ - Entry points and routing overview
43
+
44
+ ## 3. Local Development Setup
45
+ - Prerequisites (Node version, package manager, etc.)
46
+ - Step-by-step install and run instructions using exact terminal commands in code blocks
47
+ - All environment variables required (keys only, no values) — what each one does
48
+
49
+ ## 4. Build & Deployment
50
+ - Build command and output directory
51
+ - Hosting provider and deployment method
52
+ - CI/CD pipeline if applicable
53
+ - Domain, DNS, and SSL details
54
+ - How to roll back a broken deployment
55
+
56
+ ## 5. Third-Party Integrations & Services
57
+ - Every external service, API, or plugin used
58
+ - What each one does and where it is configured
59
+ - Which accounts own these services
60
+
61
+ ## 6. Credentials & Access
62
+ - Table of all accounts needed (service, URL, account owner, how to request access)
63
+ - Environment variable names and their purpose
64
+ - Access transfer checklist
65
+
66
+ ## 7. Known Issues & Technical Debt
67
+ - Any bugs, limitations, or workarounds in the current codebase
68
+ - TODO items or deferred improvements
69
+ - Performance or SEO considerations
70
+
71
+ ## 8. Maintenance Guide
72
+ - How to update npm dependencies safely
73
+ - What to check after deploying changes
74
+ - Renewal dates for domain, hosting, or licences if known
75
+
76
+ ## 9. Developer Handover Checklist
77
+ - [ ] Repository access transferred or shared
78
+ - [ ] All environment variables documented
79
+ - [ ] Hosting and domain access transferred
80
+ - [ ] Third-party service accounts handed over
81
+ - [ ] Local dev environment tested and documented
82
+ - [ ] Live site tested across browsers and devices
83
+ - [ ] Analytics verified working
84
+ - [ ] README updated
85
+
86
+ ---
87
+
88
+ Format the entire document in clean, well-structured Markdown.
89
+ Use code blocks for all terminal commands and code snippets.
90
+ Use tables where appropriate (especially for credentials and dependencies).
91
+ Be thorough and precise — this document is the sole reference for the incoming developer.
92
+ `.trim()
93
+ }
94
+
95
+ export function nonTechnicalHandoverPrompt(projectInfo, developerInfo) {
96
+ const { name, company, email, phone } = developerInfo
97
+ const date = new Date().toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' })
98
+
99
+ return `
100
+ You are an expert writer generating a NON-TECHNICAL HANDOVER DOCUMENT for a CLIENT receiving their completed website.
101
+
102
+ This document is written for the BUSINESS OWNER or CLIENT — use plain English, a warm and reassuring tone, and absolutely no technical jargon. The client should feel confident and informed about what they own.
103
+
104
+ Developer contact details:
105
+ - Name: ${name || '[Developer Name]'}
106
+ ${company ? `- Company: ${company}` : ''}
107
+ - Email: ${email || '[Developer Email]'}
108
+ ${phone ? `- Phone: ${phone}` : ''}
109
+ - Handover date: ${date}
110
+
111
+ The following context has been automatically scanned from the project files. Use this to generate a document specific to THIS project. Focus on what the client needs to know — not how it was built. If a specific detail is not available, note it as "[to be confirmed]".
112
+
113
+ ${projectInfo || '[No project data available]'}
114
+
115
+ Generate a complete non-technical handover document with ALL of the following sections:
116
+
117
+ ---
118
+
119
+ # [Project Name] — Your Website Handover Guide
120
+
121
+ **Prepared by:** ${name || '[Developer Name]'}${company ? ` · ${company}` : ''}${email ? ` · ${email}` : ''}${phone ? ` · ${phone}` : ''}
122
+ **Date:** ${date}
123
+
124
+ ---
125
+
126
+ ## 1. About Your Website
127
+ - What your website does and who it is for (1–2 paragraphs, plain English)
128
+ - The main pages or sections and what each one is for
129
+ - Any special features the site has (contact forms, booking, e-commerce, blog, etc.)
130
+
131
+ ## 2. Your Website's Look & Feel
132
+ - The overall design style and visual identity
133
+ - Colours used on the site (mention specific colours by name and hex code if detected in the project)
134
+ - Fonts used
135
+ - Any branding guidelines to keep in mind when making future updates
136
+
137
+ ## 3. What You Own
138
+ - A plain-English summary of everything handed over: the website, domain, hosting, code, and content
139
+ - Who owns what and what that means for you
140
+
141
+ ## 4. Logging Into Your Website
142
+ - How to access and log in to any admin area or CMS (plain steps, no jargon)
143
+ - What you can safely update yourself
144
+ - What you should NOT change without speaking to a developer first
145
+
146
+ ## 5. How to Update Your Content
147
+ - Step-by-step instructions for common tasks (e.g. updating text, adding images, publishing a blog post)
148
+ - Keep instructions simple and numbered
149
+ - Include a note about backing up before making changes
150
+
151
+ ## 6. Your Accounts & Passwords
152
+ - List of all accounts the client now owns (hosting, domain, CMS, email, analytics, etc.)
153
+ - Reminder to store passwords securely and change them after handover
154
+ - Note: actual passwords should be shared separately and securely — not in this document
155
+
156
+ ## 7. Keeping Your Website Healthy
157
+ - What needs renewing and approximately when (domain name, hosting plan, SSL certificate, any paid plugins)
158
+ - How to tell if something on the site is broken
159
+ - Simple monthly and yearly maintenance checklist
160
+
161
+ ## 8. Getting Help
162
+ - When and how to contact your developer: ${name || '[Developer Name]'}${company ? ` (${company})` : ''}${email ? `, ${email}` : ''}${phone ? `, ${phone}` : ''}
163
+ - What kinds of changes require a developer
164
+ - Recommended process for requesting future updates
165
+
166
+ ## 9. Handover Sign-Off
167
+ ### Developer confirms:
168
+ - [ ] All accounts and login details have been transferred
169
+ - [ ] The live site has been reviewed and approved
170
+ - [ ] This document has been discussed with the client
171
+
172
+ ### Client acknowledges:
173
+ - [ ] I have received all login credentials securely
174
+ - [ ] I understand how to update my website content
175
+ - [ ] I know what I should not change without developer help
176
+ - [ ] I know how to contact my developer for support
177
+
178
+ ---
179
+
180
+ Write the entire document in plain, friendly English. Use short sentences and simple words.
181
+ Avoid ALL technical jargon — if a technical term must be used, explain it in brackets immediately after.
182
+ Use numbered lists for any step-by-step instructions.
183
+ The tone should be warm, professional, and reassuring — the client should feel proud of their new website.
184
+ `.trim()
185
+ }
package/scanner.js ADDED
@@ -0,0 +1,250 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+
4
+ const CONFIG_FILES = [
5
+ 'vite.config.js', 'vite.config.ts',
6
+ 'next.config.js', 'next.config.ts', 'next.config.mjs',
7
+ 'nuxt.config.js', 'nuxt.config.ts',
8
+ 'astro.config.js', 'astro.config.ts', 'astro.config.mjs',
9
+ 'svelte.config.js',
10
+ 'remix.config.js',
11
+ 'tailwind.config.js', 'tailwind.config.ts',
12
+ 'postcss.config.js',
13
+ 'netlify.toml',
14
+ 'vercel.json',
15
+ 'render.yaml',
16
+ 'railway.json',
17
+ '.htaccess',
18
+ 'Dockerfile',
19
+ 'docker-compose.yml',
20
+ 'firebase.json',
21
+ ]
22
+
23
+ const DEPLOY_WORKFLOW_DIR = '.github/workflows'
24
+
25
+ function readFileSafe(filePath) {
26
+ try {
27
+ return fs.readFileSync(filePath, 'utf-8')
28
+ } catch {
29
+ return null
30
+ }
31
+ }
32
+
33
+ function getEnvKeys(filePath) {
34
+ const content = readFileSafe(filePath)
35
+ if (!content) return []
36
+ return content
37
+ .split('\n')
38
+ .map(l => l.trim())
39
+ .filter(l => l && !l.startsWith('#') && l.includes('='))
40
+ .map(l => l.split('=')[0].trim())
41
+ }
42
+
43
+ function getFolderStructure(dir, depth = 0, maxDepth = 2) {
44
+ if (depth > maxDepth) return []
45
+ const ignore = new Set(['node_modules', '.git', '.next', '.nuxt', 'dist', 'build', '.cache', 'coverage', '.turbo'])
46
+ let entries = []
47
+ try {
48
+ const items = fs.readdirSync(dir, { withFileTypes: true })
49
+ for (const item of items) {
50
+ if (ignore.has(item.name) || item.name.startsWith('.')) continue
51
+ const indent = ' '.repeat(depth)
52
+ if (item.isDirectory()) {
53
+ entries.push(`${indent}${item.name}/`)
54
+ entries.push(...getFolderStructure(path.join(dir, item.name), depth + 1, maxDepth))
55
+ } else {
56
+ entries.push(`${indent}${item.name}`)
57
+ }
58
+ }
59
+ } catch {}
60
+ return entries
61
+ }
62
+
63
+ function detectFramework(deps = {}, devDeps = {}) {
64
+ const all = { ...deps, ...devDeps }
65
+ if (all['next']) return 'Next.js'
66
+ if (all['nuxt'] || all['nuxt3']) return 'Nuxt'
67
+ if (all['@astrojs/core'] || all['astro']) return 'Astro'
68
+ if (all['@sveltejs/kit']) return 'SvelteKit'
69
+ if (all['svelte']) return 'Svelte'
70
+ if (all['@remix-run/react']) return 'Remix'
71
+ if (all['gatsby']) return 'Gatsby'
72
+ if (all['react']) return 'React'
73
+ if (all['vue']) return 'Vue'
74
+ if (all['angular']) return 'Angular'
75
+ return null
76
+ }
77
+
78
+ export function scanProject(dir = process.cwd()) {
79
+ const sections = []
80
+
81
+ // --- package.json ---
82
+ const pkgPath = path.join(dir, 'package.json')
83
+ let pkg = null
84
+ if (fs.existsSync(pkgPath)) {
85
+ try {
86
+ pkg = JSON.parse(readFileSafe(pkgPath))
87
+ const framework = detectFramework(pkg.dependencies, pkg.devDependencies)
88
+ sections.push(`## package.json`)
89
+ sections.push(`Name: ${pkg.name || 'unknown'}`)
90
+ if (pkg.description) sections.push(`Description: ${pkg.description}`)
91
+ if (pkg.version) sections.push(`Version: ${pkg.version}`)
92
+ if (framework) sections.push(`Detected framework: ${framework}`)
93
+ if (pkg.engines?.node) sections.push(`Node requirement: ${pkg.engines.node}`)
94
+
95
+ if (pkg.scripts && Object.keys(pkg.scripts).length) {
96
+ sections.push(`\nScripts:`)
97
+ for (const [k, v] of Object.entries(pkg.scripts)) {
98
+ sections.push(` ${k}: ${v}`)
99
+ }
100
+ }
101
+
102
+ const deps = Object.keys(pkg.dependencies || {})
103
+ const devDeps = Object.keys(pkg.devDependencies || {})
104
+ if (deps.length) sections.push(`\nDependencies: ${deps.join(', ')}`)
105
+ if (devDeps.length) sections.push(`Dev dependencies: ${devDeps.join(', ')}`)
106
+ } catch {}
107
+ }
108
+
109
+ // --- Package manager ---
110
+ let pm = 'npm'
111
+ if (fs.existsSync(path.join(dir, 'yarn.lock'))) pm = 'yarn'
112
+ else if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))) pm = 'pnpm'
113
+ else if (fs.existsSync(path.join(dir, 'bun.lockb'))) pm = 'bun'
114
+ sections.push(`\nPackage manager: ${pm}`)
115
+
116
+ // --- Environment variables ---
117
+ const envExampleKeys = getEnvKeys(path.join(dir, '.env.example'))
118
+ const envLocalKeys = getEnvKeys(path.join(dir, '.env.local'))
119
+ const envKeys = getEnvKeys(path.join(dir, '.env'))
120
+ const allEnvKeys = [...new Set([...envExampleKeys, ...envLocalKeys, ...envKeys])]
121
+ if (allEnvKeys.length) {
122
+ sections.push(`\n## Environment Variables (keys only)`)
123
+ allEnvKeys.forEach(k => sections.push(` ${k}`))
124
+ }
125
+
126
+ // --- Config files ---
127
+ for (const file of CONFIG_FILES) {
128
+ const filePath = path.join(dir, file)
129
+ if (fs.existsSync(filePath)) {
130
+ const content = readFileSafe(filePath)
131
+ if (content && content.length < 4000) {
132
+ sections.push(`\n## ${file}`)
133
+ sections.push('```')
134
+ sections.push(content.trim())
135
+ sections.push('```')
136
+ } else if (content) {
137
+ sections.push(`\n## ${file} (truncated)`)
138
+ sections.push('```')
139
+ sections.push(content.trim().slice(0, 4000) + '\n...')
140
+ sections.push('```')
141
+ }
142
+ }
143
+ }
144
+
145
+ // --- GitHub Actions workflows ---
146
+ const workflowDir = path.join(dir, DEPLOY_WORKFLOW_DIR)
147
+ if (fs.existsSync(workflowDir)) {
148
+ try {
149
+ const files = fs.readdirSync(workflowDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml'))
150
+ for (const file of files) {
151
+ const content = readFileSafe(path.join(workflowDir, file))
152
+ if (content) {
153
+ sections.push(`\n## .github/workflows/${file}`)
154
+ sections.push('```yaml')
155
+ sections.push(content.trim().slice(0, 3000))
156
+ sections.push('```')
157
+ }
158
+ }
159
+ } catch {}
160
+ }
161
+
162
+ // --- README ---
163
+ const readmePath = path.join(dir, 'README.md')
164
+ if (fs.existsSync(readmePath)) {
165
+ const content = readFileSafe(readmePath)
166
+ if (content) {
167
+ sections.push(`\n## README.md`)
168
+ sections.push(content.trim().slice(0, 3000))
169
+ }
170
+ }
171
+
172
+ // --- License ---
173
+ for (const f of ['LICENSE', 'LICENSE.md', 'LICENSE.txt']) {
174
+ const p = path.join(dir, f)
175
+ if (fs.existsSync(p)) {
176
+ const content = readFileSafe(p)
177
+ if (content) {
178
+ sections.push(`\n## ${f}`)
179
+ sections.push(content.trim().slice(0, 500))
180
+ }
181
+ break
182
+ }
183
+ }
184
+
185
+ // --- CSS color detection ---
186
+ const cssColors = extractColors(dir)
187
+ if (cssColors.length) {
188
+ sections.push(`\n## Detected colours (from CSS/config files)`)
189
+ cssColors.forEach(c => sections.push(` ${c}`))
190
+ }
191
+
192
+ // --- Folder structure ---
193
+ const structure = getFolderStructure(dir)
194
+ if (structure.length) {
195
+ sections.push(`\n## Project folder structure`)
196
+ sections.push(structure.join('\n'))
197
+ }
198
+
199
+ return sections.join('\n')
200
+ }
201
+
202
+ function extractColors(dir) {
203
+ const colors = new Set()
204
+ const hexPattern = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/g
205
+ const cssVarColorPattern = /(--[\w-]*color[\w-]*|--[\w-]*bg[\w-]*|--[\w-]*primary[\w-]*|--[\w-]*secondary[\w-]*):\s*([^;}\n]+)/gi
206
+ const cssExtensions = ['.css', '.scss', '.sass', '.less']
207
+
208
+ function scanFile(filePath) {
209
+ const content = readFileSafe(filePath)
210
+ if (!content) return
211
+ let match
212
+ while ((match = hexPattern.exec(content)) !== null) {
213
+ colors.add(`#${match[1].toUpperCase()}`)
214
+ }
215
+ while ((match = cssVarColorPattern.exec(content)) !== null) {
216
+ colors.add(`${match[1].trim()}: ${match[2].trim()}`)
217
+ }
218
+ }
219
+
220
+ function walkDir(d, depth = 0) {
221
+ if (depth > 3) return
222
+ const ignore = new Set(['node_modules', '.git', '.next', '.nuxt', 'dist', 'build', '.cache'])
223
+ try {
224
+ const items = fs.readdirSync(d, { withFileTypes: true })
225
+ for (const item of items) {
226
+ if (ignore.has(item.name) || item.name.startsWith('.')) continue
227
+ const full = path.join(d, item.name)
228
+ if (item.isDirectory()) {
229
+ walkDir(full, depth + 1)
230
+ } else if (cssExtensions.includes(path.extname(item.name).toLowerCase())) {
231
+ scanFile(full)
232
+ }
233
+ }
234
+ } catch {}
235
+ }
236
+
237
+ walkDir(dir)
238
+
239
+ // Also check tailwind config for color definitions
240
+ for (const twConfig of ['tailwind.config.js', 'tailwind.config.ts']) {
241
+ const content = readFileSafe(path.join(dir, twConfig))
242
+ if (!content) continue
243
+ let match
244
+ while ((match = hexPattern.exec(content)) !== null) {
245
+ colors.add(`#${match[1].toUpperCase()}`)
246
+ }
247
+ }
248
+
249
+ return [...colors].slice(0, 40) // cap at 40 to avoid flooding context
250
+ }