client-handover 1.0.6 → 1.1.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 +6 -0
- package/cli.js +86 -96
- package/generator.js +289 -104
- package/package.json +5 -9
- package/postinstall.js +63 -32
- package/prompts.js +185 -0
- package/scanner.js +57 -0
- package/credentials.js +0 -51
- package/deploy.js +0 -35
- package/handover.js +0 -97
- package/setup.js +0 -32
package/README.md
CHANGED
|
@@ -200,6 +200,12 @@ client-handover/
|
|
|
200
200
|
|
|
201
201
|
## Changelog
|
|
202
202
|
|
|
203
|
+
### 1.1.0
|
|
204
|
+
- Complete CLI redesign: all section commands replaced with a single `handover /create` command
|
|
205
|
+
- Now generates two separate documents per run — a technical handover (for the incoming developer) and a non-technical client guide (for the business owner)
|
|
206
|
+
- First-run developer info prompt — saves your name, company, email, and phone to config so every document is personalised
|
|
207
|
+
- CSS colour detection — automatically extracts hex colours and CSS custom properties from `.css`, `.scss`, `.less` files and Tailwind config
|
|
208
|
+
|
|
203
209
|
### 1.0.6
|
|
204
210
|
- Auto-scans your actual project files on every run — reads package.json, config files, env variable keys, deploy configs, folder structure, and more
|
|
205
211
|
- Generated documents are now tailored to your real project, not generic templates
|
package/cli.js
CHANGED
|
@@ -1,73 +1,103 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { credentials } from './credentials.js'
|
|
6
|
-
import { handover } from './handover.js'
|
|
7
|
-
import { license } from './license.js'
|
|
8
|
-
import { generateDoc, saveApiKey } from './generator.js'
|
|
3
|
+
import { generateDoc, loadDeveloperInfo, saveApiKey } from './generator.js'
|
|
4
|
+
import { technicalHandoverPrompt, nonTechnicalHandoverPrompt } from './prompts.js'
|
|
9
5
|
import { scanProject } from './scanner.js'
|
|
10
6
|
import chalk from 'chalk'
|
|
7
|
+
import readline from 'readline'
|
|
11
8
|
import fs from 'fs'
|
|
12
9
|
import path from 'path'
|
|
13
|
-
|
|
14
|
-
const COMMANDS = {
|
|
15
|
-
'setup': { fn: setup, label: 'Project Setup & Dependencies' },
|
|
16
|
-
'deploy': { fn: deploy, label: 'Deployment & Hosting' },
|
|
17
|
-
'credentials': { fn: credentials, label: 'Credentials & Access' },
|
|
18
|
-
'handover': { fn: handover, label: 'Full Handover Document' },
|
|
19
|
-
'license': { fn: license, label: 'Licensing & Attribution' },
|
|
20
|
-
}
|
|
10
|
+
import os from 'os'
|
|
21
11
|
|
|
22
12
|
function normalizeCommand(cmd) {
|
|
23
|
-
// Git Bash converts /handover → C:\Program Files\Git\handover before Node sees it
|
|
24
|
-
// path.basename extracts just the last segment regardless of what form it arrives in
|
|
25
13
|
return path.basename(cmd).replace(/^\/+/, '')
|
|
26
14
|
}
|
|
27
15
|
|
|
28
16
|
function printHelp() {
|
|
29
|
-
console.log(chalk.bold('\n
|
|
17
|
+
console.log(chalk.bold('\n handover — Website Handover Document Generator\n'))
|
|
30
18
|
console.log(chalk.dim('Usage:'))
|
|
31
|
-
console.log(' handover
|
|
32
|
-
console.log(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
19
|
+
console.log(' handover /create Generate technical & non-technical handover documents')
|
|
20
|
+
console.log(' handover key <key> Save your Anthropic API key\n')
|
|
21
|
+
console.log(chalk.dim('Example:'))
|
|
22
|
+
console.log(' cd my-client-project')
|
|
23
|
+
console.log(' handover /create\n')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ask(rl, question) {
|
|
27
|
+
return new Promise(resolve => rl.question(question, answer => resolve(answer.trim())))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function ensureDeveloperInfo() {
|
|
31
|
+
const configDir = path.join(os.homedir(), '.handover')
|
|
32
|
+
const configPath = path.join(configDir, 'config.json')
|
|
33
|
+
|
|
34
|
+
let config = {}
|
|
35
|
+
try {
|
|
36
|
+
if (fs.existsSync(configPath)) config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
|
37
|
+
} catch {}
|
|
38
|
+
|
|
39
|
+
if (config.developerName) {
|
|
40
|
+
return {
|
|
41
|
+
name: config.developerName,
|
|
42
|
+
company: config.developerCompany || '',
|
|
43
|
+
email: config.developerEmail || '',
|
|
44
|
+
phone: config.developerPhone || '',
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log(chalk.bold('\n Your developer details will appear in the handover documents.\n'))
|
|
49
|
+
|
|
50
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
51
|
+
const name = await ask(rl, ' Your full name: ')
|
|
52
|
+
const company = await ask(rl, ' Your company name (or press Enter if not applicable): ')
|
|
53
|
+
const email = await ask(rl, ' Your email address: ')
|
|
54
|
+
const phone = await ask(rl, ' Your phone number (optional): ')
|
|
55
|
+
rl.close()
|
|
56
|
+
|
|
57
|
+
if (name) config.developerName = name
|
|
58
|
+
if (company) config.developerCompany = company
|
|
59
|
+
if (email) config.developerEmail = email
|
|
60
|
+
if (phone) config.developerPhone = phone
|
|
61
|
+
|
|
62
|
+
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true })
|
|
63
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8')
|
|
43
64
|
console.log()
|
|
65
|
+
|
|
66
|
+
return { name: name || '', company: company || '', email: email || '', phone: phone || '' }
|
|
44
67
|
}
|
|
45
68
|
|
|
46
|
-
async function
|
|
47
|
-
const
|
|
48
|
-
{ key: 'handover', fn: handover, label: 'Full Handover Document' },
|
|
49
|
-
{ key: 'setup', fn: setup, label: 'Project Setup & Dependencies' },
|
|
50
|
-
{ key: 'deploy', fn: deploy, label: 'Deployment & Hosting' },
|
|
51
|
-
{ key: 'credentials', fn: credentials, label: 'Credentials & Access' },
|
|
52
|
-
{ key: 'license', fn: license, label: 'Licensing & Attribution' },
|
|
53
|
-
]
|
|
69
|
+
async function runCreate() {
|
|
70
|
+
const developerInfo = await ensureDeveloperInfo()
|
|
54
71
|
|
|
55
|
-
|
|
72
|
+
console.log(chalk.dim(' Scanning project files...'))
|
|
73
|
+
const projectInfo = scanProject(process.cwd())
|
|
56
74
|
|
|
57
|
-
|
|
75
|
+
const techDir = './technical-handover'
|
|
76
|
+
const clientDir = './handover'
|
|
58
77
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
78
|
+
console.log(chalk.bold('\n Generating Technical Handover Document...'))
|
|
79
|
+
const techPrompt = technicalHandoverPrompt(projectInfo, developerInfo)
|
|
80
|
+
await generateDoc(techPrompt, 'technical-handover', techDir)
|
|
81
|
+
|
|
82
|
+
console.log(chalk.bold('\n Generating Client Handover Document...'))
|
|
83
|
+
const nonTechPrompt = nonTechnicalHandoverPrompt(projectInfo, developerInfo)
|
|
84
|
+
await generateDoc(nonTechPrompt, 'handover', clientDir)
|
|
65
85
|
|
|
66
|
-
console.log(chalk.green.bold(
|
|
86
|
+
console.log(chalk.green.bold('\n ✅ Handover documents created successfully!\n'))
|
|
87
|
+
console.log(` ${chalk.cyan('./technical-handover/')}`)
|
|
88
|
+
console.log(` technical-handover.md`)
|
|
89
|
+
console.log(` technical-handover.txt`)
|
|
90
|
+
console.log(` technical-handover.docx`)
|
|
91
|
+
console.log()
|
|
92
|
+
console.log(` ${chalk.cyan('./handover/')}`)
|
|
93
|
+
console.log(` handover.md`)
|
|
94
|
+
console.log(` handover.txt`)
|
|
95
|
+
console.log(` handover.docx`)
|
|
96
|
+
console.log()
|
|
67
97
|
}
|
|
68
98
|
|
|
69
99
|
async function main() {
|
|
70
|
-
const [,, rawCommand,
|
|
100
|
+
const [,, rawCommand, secondArg] = process.argv
|
|
71
101
|
|
|
72
102
|
if (!rawCommand || rawCommand === '--help' || rawCommand === '-h') {
|
|
73
103
|
printHelp()
|
|
@@ -77,68 +107,28 @@ async function main() {
|
|
|
77
107
|
const command = normalizeCommand(rawCommand)
|
|
78
108
|
|
|
79
109
|
if (command === 'key') {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
console.error(chalk.red('\n❌ Usage: handover key <your-api-key>\n'))
|
|
110
|
+
if (!secondArg) {
|
|
111
|
+
console.error(chalk.red('\n ❌ Usage: handover key <your-api-key>\n'))
|
|
83
112
|
process.exit(1)
|
|
84
113
|
}
|
|
85
|
-
saveApiKey(
|
|
86
|
-
console.log(chalk.green('\n✅ API key saved.
|
|
114
|
+
saveApiKey(secondArg)
|
|
115
|
+
console.log(chalk.green('\n ✅ API key saved. Run "handover /create" to get started.\n'))
|
|
87
116
|
process.exit(0)
|
|
88
117
|
}
|
|
89
118
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const scannedContext = scanProject(process.cwd())
|
|
93
|
-
|
|
94
|
-
// Read optional extra project info file
|
|
95
|
-
let extraInfo = ''
|
|
96
|
-
if (infoFile) {
|
|
97
|
-
if (!fs.existsSync(infoFile)) {
|
|
98
|
-
console.error(chalk.red(`\n❌ File not found: ${infoFile}\n`))
|
|
99
|
-
process.exit(1)
|
|
100
|
-
}
|
|
101
|
-
extraInfo = fs.readFileSync(infoFile, 'utf-8')
|
|
102
|
-
console.log(chalk.green(`📄 Loaded extra info from: ${infoFile}`))
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const projectInfo = [scannedContext, extraInfo].filter(Boolean).join('\n\n---\n\n')
|
|
106
|
-
|
|
107
|
-
if (command === 'all') {
|
|
108
|
-
const folderName = outputName || 'all'
|
|
109
|
-
try {
|
|
110
|
-
await runAll(projectInfo, folderName)
|
|
111
|
-
} catch (err) {
|
|
112
|
-
if (err.status === 401) {
|
|
113
|
-
console.error(chalk.red('\n❌ Authentication failed. Set ANTHROPIC_API_KEY or install Claude Code.\n'))
|
|
114
|
-
} else {
|
|
115
|
-
console.error(chalk.red(`\n❌ Error: ${err.message}\n`))
|
|
116
|
-
}
|
|
117
|
-
process.exit(1)
|
|
118
|
-
}
|
|
119
|
-
return
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const entry = COMMANDS[command]
|
|
123
|
-
if (!entry) {
|
|
124
|
-
console.error(chalk.red(`\n❌ Unknown command: ${rawCommand}\n`))
|
|
119
|
+
if (command !== 'create') {
|
|
120
|
+
console.error(chalk.red(`\n ❌ Unknown command: ${rawCommand}\n`))
|
|
125
121
|
printHelp()
|
|
126
122
|
process.exit(1)
|
|
127
123
|
}
|
|
128
124
|
|
|
129
|
-
const docName = outputName || command
|
|
130
|
-
const prompt = entry.fn(projectInfo)
|
|
131
|
-
|
|
132
|
-
console.log(chalk.bold(`\n📝 Generating: ${entry.label}`))
|
|
133
|
-
console.log(chalk.dim(` Output: ./output/${docName}.{md,txt,html}\n`))
|
|
134
|
-
|
|
135
125
|
try {
|
|
136
|
-
await
|
|
126
|
+
await runCreate()
|
|
137
127
|
} catch (err) {
|
|
138
128
|
if (err.status === 401) {
|
|
139
|
-
console.error(chalk.red('\n❌ Authentication failed.
|
|
129
|
+
console.error(chalk.red('\n ❌ Authentication failed. Run "handover key <your-api-key>" to set your API key.\n'))
|
|
140
130
|
} else {
|
|
141
|
-
console.error(chalk.red(`\n❌ Error: ${err.message}\n`))
|
|
131
|
+
console.error(chalk.red(`\n ❌ Error: ${err.message}\n`))
|
|
142
132
|
}
|
|
143
133
|
process.exit(1)
|
|
144
134
|
}
|
package/generator.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Document, Packer, Paragraph, TextRun, HeadingLevel,
|
|
4
|
+
AlignmentType, BorderStyle, TableRow, TableCell, Table,
|
|
5
|
+
WidthType, ShadingType
|
|
6
|
+
} from 'docx'
|
|
3
7
|
import fs from 'fs'
|
|
4
8
|
import path from 'path'
|
|
5
9
|
import os from 'os'
|
|
@@ -7,15 +11,12 @@ import os from 'os'
|
|
|
7
11
|
function resolveApiKey() {
|
|
8
12
|
if (process.env.ANTHROPIC_API_KEY) return process.env.ANTHROPIC_API_KEY
|
|
9
13
|
|
|
10
|
-
// Check saved key from `handover key <your-key>`
|
|
11
14
|
const configPath = path.join(os.homedir(), '.handover', 'config.json')
|
|
12
15
|
if (fs.existsSync(configPath)) {
|
|
13
16
|
try {
|
|
14
17
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
|
15
18
|
if (config?.apiKey) return config.apiKey
|
|
16
|
-
} catch {
|
|
17
|
-
// ignore malformed config file
|
|
18
|
-
}
|
|
19
|
+
} catch {}
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
const credentialsPath = path.join(os.homedir(), '.claude', '.credentials.json')
|
|
@@ -24,42 +25,293 @@ function resolveApiKey() {
|
|
|
24
25
|
const creds = JSON.parse(fs.readFileSync(credentialsPath, 'utf-8'))
|
|
25
26
|
const token = creds?.claudeAiOauth?.accessToken
|
|
26
27
|
const expiresAt = creds?.claudeAiOauth?.expiresAt
|
|
27
|
-
if (token && (!expiresAt || expiresAt > Date.now()))
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
} catch {
|
|
31
|
-
// ignore malformed credentials file
|
|
32
|
-
}
|
|
28
|
+
if (token && (!expiresAt || expiresAt > Date.now())) return token
|
|
29
|
+
} catch {}
|
|
33
30
|
}
|
|
34
31
|
|
|
35
32
|
return null
|
|
36
33
|
}
|
|
37
34
|
|
|
35
|
+
export function loadDeveloperInfo() {
|
|
36
|
+
const configPath = path.join(os.homedir(), '.handover', 'config.json')
|
|
37
|
+
try {
|
|
38
|
+
if (fs.existsSync(configPath)) {
|
|
39
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
|
40
|
+
return {
|
|
41
|
+
name: config.developerName || '',
|
|
42
|
+
company: config.developerCompany || '',
|
|
43
|
+
email: config.developerEmail || '',
|
|
44
|
+
phone: config.developerPhone || '',
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch {}
|
|
48
|
+
return { name: '', company: '', email: '', phone: '' }
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
export function saveApiKey(key) {
|
|
39
52
|
const configDir = path.join(os.homedir(), '.handover')
|
|
40
53
|
const configPath = path.join(configDir, 'config.json')
|
|
41
54
|
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true })
|
|
42
|
-
|
|
55
|
+
let config = {}
|
|
56
|
+
try { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) } catch {}
|
|
57
|
+
config.apiKey = key
|
|
58
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8')
|
|
43
59
|
}
|
|
44
60
|
|
|
45
61
|
const apiKey = resolveApiKey()
|
|
46
62
|
if (!apiKey) {
|
|
47
|
-
console.error('\n❌ No API key found.
|
|
63
|
+
console.error('\n❌ No API key found. Run "handover key <your-api-key>" or install Claude Code.\n')
|
|
48
64
|
process.exit(1)
|
|
49
65
|
}
|
|
50
66
|
|
|
51
67
|
const client = new Anthropic({ apiKey })
|
|
52
68
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
*
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Inline text parser — splits a line into TextRun segments handling **bold**,
|
|
71
|
+
// *italic*, `code`, and [link text](url) → just the text
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
function parseInline(text) {
|
|
74
|
+
const runs = []
|
|
75
|
+
// Strip markdown links to plain text
|
|
76
|
+
text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
77
|
+
|
|
78
|
+
const pattern = /(\*\*(.+?)\*\*|\*(.+?)\*|`(.+?)`)/g
|
|
79
|
+
let lastIndex = 0
|
|
80
|
+
let match
|
|
81
|
+
|
|
82
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
83
|
+
if (match.index > lastIndex) {
|
|
84
|
+
runs.push(new TextRun({ text: text.slice(lastIndex, match.index) }))
|
|
85
|
+
}
|
|
86
|
+
if (match[2] !== undefined) {
|
|
87
|
+
runs.push(new TextRun({ text: match[2], bold: true }))
|
|
88
|
+
} else if (match[3] !== undefined) {
|
|
89
|
+
runs.push(new TextRun({ text: match[3], italics: true }))
|
|
90
|
+
} else if (match[4] !== undefined) {
|
|
91
|
+
runs.push(new TextRun({
|
|
92
|
+
text: match[4],
|
|
93
|
+
font: 'Courier New',
|
|
94
|
+
size: 18,
|
|
95
|
+
shading: { type: ShadingType.CLEAR, fill: 'F4F4F4' },
|
|
96
|
+
}))
|
|
97
|
+
}
|
|
98
|
+
lastIndex = match.index + match[0].length
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (lastIndex < text.length) {
|
|
102
|
+
runs.push(new TextRun({ text: text.slice(lastIndex) }))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return runs.length ? runs : [new TextRun({ text })]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Convert markdown string to an array of docx Paragraph/Table objects
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
function markdownToDocxChildren(markdown) {
|
|
112
|
+
const children = []
|
|
113
|
+
const lines = markdown.split('\n')
|
|
114
|
+
let i = 0
|
|
115
|
+
|
|
116
|
+
while (i < lines.length) {
|
|
117
|
+
const line = lines[i]
|
|
118
|
+
|
|
119
|
+
// Fenced code block
|
|
120
|
+
if (line.trim().startsWith('```')) {
|
|
121
|
+
const codeLines = []
|
|
122
|
+
i++
|
|
123
|
+
while (i < lines.length && !lines[i].trim().startsWith('```')) {
|
|
124
|
+
codeLines.push(lines[i])
|
|
125
|
+
i++
|
|
126
|
+
}
|
|
127
|
+
for (const codeLine of codeLines) {
|
|
128
|
+
children.push(new Paragraph({
|
|
129
|
+
children: [new TextRun({ text: codeLine || ' ', font: 'Courier New', size: 18 })],
|
|
130
|
+
shading: { type: ShadingType.CLEAR, fill: 'F4F4F4' },
|
|
131
|
+
spacing: { before: 0, after: 0 },
|
|
132
|
+
indent: { left: 360 },
|
|
133
|
+
}))
|
|
134
|
+
}
|
|
135
|
+
// Add small gap after code block
|
|
136
|
+
children.push(new Paragraph({ text: '' }))
|
|
137
|
+
i++
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Horizontal rule
|
|
142
|
+
if (/^---+$/.test(line.trim())) {
|
|
143
|
+
children.push(new Paragraph({
|
|
144
|
+
text: '',
|
|
145
|
+
border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: 'CCCCCC' } },
|
|
146
|
+
spacing: { before: 200, after: 200 },
|
|
147
|
+
}))
|
|
148
|
+
i++
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Headings
|
|
153
|
+
const h1 = line.match(/^# (.+)/)
|
|
154
|
+
const h2 = line.match(/^## (.+)/)
|
|
155
|
+
const h3 = line.match(/^### (.+)/)
|
|
156
|
+
const h4 = line.match(/^#{4,6} (.+)/)
|
|
157
|
+
|
|
158
|
+
if (h1) {
|
|
159
|
+
children.push(new Paragraph({
|
|
160
|
+
children: parseInline(h1[1]),
|
|
161
|
+
heading: HeadingLevel.HEADING_1,
|
|
162
|
+
spacing: { before: 400, after: 160 },
|
|
163
|
+
}))
|
|
164
|
+
i++; continue
|
|
165
|
+
}
|
|
166
|
+
if (h2) {
|
|
167
|
+
children.push(new Paragraph({
|
|
168
|
+
children: parseInline(h2[1]),
|
|
169
|
+
heading: HeadingLevel.HEADING_2,
|
|
170
|
+
spacing: { before: 320, after: 120 },
|
|
171
|
+
}))
|
|
172
|
+
i++; continue
|
|
173
|
+
}
|
|
174
|
+
if (h3) {
|
|
175
|
+
children.push(new Paragraph({
|
|
176
|
+
children: parseInline(h3[1]),
|
|
177
|
+
heading: HeadingLevel.HEADING_3,
|
|
178
|
+
spacing: { before: 240, after: 80 },
|
|
179
|
+
}))
|
|
180
|
+
i++; continue
|
|
181
|
+
}
|
|
182
|
+
if (h4) {
|
|
183
|
+
children.push(new Paragraph({
|
|
184
|
+
children: [new TextRun({ text: h4[1], bold: true })],
|
|
185
|
+
spacing: { before: 160, after: 80 },
|
|
186
|
+
}))
|
|
187
|
+
i++; continue
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Checkbox list items - [ ] or - [x]
|
|
191
|
+
const checkbox = line.match(/^(\s*)- \[(x| )\] (.+)/i)
|
|
192
|
+
if (checkbox) {
|
|
193
|
+
const checked = checkbox[2].toLowerCase() === 'x'
|
|
194
|
+
children.push(new Paragraph({
|
|
195
|
+
children: [
|
|
196
|
+
new TextRun({ text: checked ? '☑ ' : '☐ ' }),
|
|
197
|
+
...parseInline(checkbox[3]),
|
|
198
|
+
],
|
|
199
|
+
indent: { left: 360 },
|
|
200
|
+
spacing: { before: 40, after: 40 },
|
|
201
|
+
}))
|
|
202
|
+
i++; continue
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Bullet list items - or *
|
|
206
|
+
const bullet = line.match(/^(\s*)[-*+] (.+)/)
|
|
207
|
+
if (bullet) {
|
|
208
|
+
const indent = bullet[1].length > 0 ? 720 : 360
|
|
209
|
+
children.push(new Paragraph({
|
|
210
|
+
children: [new TextRun({ text: '• ' }), ...parseInline(bullet[2])],
|
|
211
|
+
indent: { left: indent },
|
|
212
|
+
spacing: { before: 40, after: 40 },
|
|
213
|
+
}))
|
|
214
|
+
i++; continue
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Numbered list items
|
|
218
|
+
const numbered = line.match(/^\d+\. (.+)/)
|
|
219
|
+
if (numbered) {
|
|
220
|
+
children.push(new Paragraph({
|
|
221
|
+
children: parseInline(numbered[1]),
|
|
222
|
+
numbering: { reference: 'default-numbering', level: 0 },
|
|
223
|
+
indent: { left: 360 },
|
|
224
|
+
spacing: { before: 40, after: 40 },
|
|
225
|
+
}))
|
|
226
|
+
i++; continue
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Bold-only line (often used as a label/key line like **Key:** value)
|
|
230
|
+
// Handled by parseInline already — fall through to normal paragraph
|
|
231
|
+
|
|
232
|
+
// Empty line
|
|
233
|
+
if (line.trim() === '') {
|
|
234
|
+
children.push(new Paragraph({ text: '', spacing: { before: 0, after: 80 } }))
|
|
235
|
+
i++; continue
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Normal paragraph
|
|
239
|
+
children.push(new Paragraph({
|
|
240
|
+
children: parseInline(line),
|
|
241
|
+
spacing: { before: 0, after: 80 },
|
|
242
|
+
}))
|
|
243
|
+
i++
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return children
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// Build a docx Document from markdown
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
function buildDocx(markdown) {
|
|
253
|
+
const children = markdownToDocxChildren(markdown)
|
|
254
|
+
|
|
255
|
+
return new Document({
|
|
256
|
+
numbering: {
|
|
257
|
+
config: [{
|
|
258
|
+
reference: 'default-numbering',
|
|
259
|
+
levels: [{
|
|
260
|
+
level: 0,
|
|
261
|
+
format: 'decimal',
|
|
262
|
+
text: '%1.',
|
|
263
|
+
alignment: AlignmentType.LEFT,
|
|
264
|
+
style: { paragraph: { indent: { left: 360, hanging: 260 } } },
|
|
265
|
+
}],
|
|
266
|
+
}],
|
|
267
|
+
},
|
|
268
|
+
styles: {
|
|
269
|
+
default: {
|
|
270
|
+
document: {
|
|
271
|
+
run: { font: 'Calibri', size: 22 },
|
|
272
|
+
paragraph: { spacing: { line: 276 } },
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
paragraphStyles: [
|
|
276
|
+
{
|
|
277
|
+
id: 'Heading1',
|
|
278
|
+
name: 'Heading 1',
|
|
279
|
+
basedOn: 'Normal',
|
|
280
|
+
next: 'Normal',
|
|
281
|
+
run: { bold: true, size: 36, color: '1a1a1a' },
|
|
282
|
+
paragraph: { spacing: { before: 400, after: 160 } },
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
id: 'Heading2',
|
|
286
|
+
name: 'Heading 2',
|
|
287
|
+
basedOn: 'Normal',
|
|
288
|
+
next: 'Normal',
|
|
289
|
+
run: { bold: true, size: 28, color: '2c2c2c' },
|
|
290
|
+
paragraph: {
|
|
291
|
+
spacing: { before: 320, after: 120 },
|
|
292
|
+
border: { bottom: { style: BorderStyle.SINGLE, size: 4, color: 'E0E0E0' } },
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
id: 'Heading3',
|
|
297
|
+
name: 'Heading 3',
|
|
298
|
+
basedOn: 'Normal',
|
|
299
|
+
next: 'Normal',
|
|
300
|
+
run: { bold: true, size: 24, color: '444444' },
|
|
301
|
+
paragraph: { spacing: { before: 240, after: 80 } },
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
},
|
|
305
|
+
sections: [{ properties: {}, children }],
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
// Main export
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
59
312
|
export async function generateDoc(prompt, outputName = 'handover', outputDir = './output') {
|
|
60
313
|
console.log('⏳ Generating document with Claude...\n')
|
|
61
314
|
|
|
62
|
-
// Call Claude API
|
|
63
315
|
const message = await client.messages.create({
|
|
64
316
|
model: 'claude-sonnet-4-20250514',
|
|
65
317
|
max_tokens: 4096,
|
|
@@ -68,105 +320,38 @@ export async function generateDoc(prompt, outputName = 'handover', outputDir = '
|
|
|
68
320
|
|
|
69
321
|
const markdown = message.content[0].text
|
|
70
322
|
|
|
71
|
-
// Ensure output directory exists
|
|
72
323
|
if (!fs.existsSync(outputDir)) {
|
|
73
324
|
fs.mkdirSync(outputDir, { recursive: true })
|
|
74
325
|
}
|
|
75
326
|
|
|
76
327
|
const basePath = path.join(outputDir, outputName)
|
|
77
328
|
|
|
78
|
-
//
|
|
329
|
+
// Save as Markdown
|
|
79
330
|
const mdPath = `${basePath}.md`
|
|
80
331
|
fs.writeFileSync(mdPath, markdown, 'utf-8')
|
|
81
|
-
console.log(`✅ Markdown saved:
|
|
332
|
+
console.log(`✅ Markdown saved: ${mdPath}`)
|
|
82
333
|
|
|
83
|
-
//
|
|
334
|
+
// Save as plain text
|
|
84
335
|
const plainText = markdown
|
|
85
|
-
.replace(/#{1,6}\s+/g, '')
|
|
86
|
-
.replace(/\*\*(.*?)\*\*/g, '$1')
|
|
87
|
-
.replace(/\*(.*?)\*/g, '$1')
|
|
88
|
-
.replace(/`{1,3}[^`]*`{1,3}/g,
|
|
89
|
-
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
90
|
-
.replace(/[-*+] /g, '• ')
|
|
91
|
-
.replace(/\n{3,}/g, '\n\n')
|
|
336
|
+
.replace(/#{1,6}\s+/g, '')
|
|
337
|
+
.replace(/\*\*(.*?)\*\*/g, '$1')
|
|
338
|
+
.replace(/\*(.*?)\*/g, '$1')
|
|
339
|
+
.replace(/`{1,3}[^`]*`{1,3}/g, m => m.replace(/`/g, ''))
|
|
340
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
341
|
+
.replace(/[-*+] /g, '• ')
|
|
342
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
92
343
|
.trim()
|
|
93
344
|
|
|
94
345
|
const txtPath = `${basePath}.txt`
|
|
95
346
|
fs.writeFileSync(txtPath, plainText, 'utf-8')
|
|
96
347
|
console.log(`✅ Plain text saved: ${txtPath}`)
|
|
97
348
|
|
|
98
|
-
//
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
105
|
-
<title>Handover Document</title>
|
|
106
|
-
<style>
|
|
107
|
-
body {
|
|
108
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
109
|
-
max-width: 860px;
|
|
110
|
-
margin: 0 auto;
|
|
111
|
-
padding: 2rem;
|
|
112
|
-
color: #1a1a1a;
|
|
113
|
-
line-height: 1.7;
|
|
114
|
-
}
|
|
115
|
-
h1 { border-bottom: 2px solid #e0e0e0; padding-bottom: 0.5rem; }
|
|
116
|
-
h2 { margin-top: 2.5rem; color: #2c2c2c; }
|
|
117
|
-
h3 { color: #444; }
|
|
118
|
-
code {
|
|
119
|
-
background: #f4f4f4;
|
|
120
|
-
padding: 0.2em 0.4em;
|
|
121
|
-
border-radius: 4px;
|
|
122
|
-
font-size: 0.9em;
|
|
123
|
-
}
|
|
124
|
-
pre {
|
|
125
|
-
background: #f4f4f4;
|
|
126
|
-
padding: 1rem;
|
|
127
|
-
border-radius: 6px;
|
|
128
|
-
overflow-x: auto;
|
|
129
|
-
}
|
|
130
|
-
pre code { background: none; padding: 0; }
|
|
131
|
-
table {
|
|
132
|
-
width: 100%;
|
|
133
|
-
border-collapse: collapse;
|
|
134
|
-
margin: 1rem 0;
|
|
135
|
-
}
|
|
136
|
-
th, td {
|
|
137
|
-
border: 1px solid #ddd;
|
|
138
|
-
padding: 0.6rem 0.8rem;
|
|
139
|
-
text-align: left;
|
|
140
|
-
}
|
|
141
|
-
th { background: #f0f0f0; font-weight: 600; }
|
|
142
|
-
blockquote {
|
|
143
|
-
border-left: 4px solid #ccc;
|
|
144
|
-
margin: 0;
|
|
145
|
-
padding-left: 1rem;
|
|
146
|
-
color: #555;
|
|
147
|
-
}
|
|
148
|
-
.badge {
|
|
149
|
-
display: inline-block;
|
|
150
|
-
background: #e8f4fd;
|
|
151
|
-
color: #0969da;
|
|
152
|
-
padding: 0.2em 0.6em;
|
|
153
|
-
border-radius: 4px;
|
|
154
|
-
font-size: 0.8em;
|
|
155
|
-
font-weight: 600;
|
|
156
|
-
}
|
|
157
|
-
input[type="checkbox"] { margin-right: 0.4rem; }
|
|
158
|
-
</style>
|
|
159
|
-
</head>
|
|
160
|
-
<body>
|
|
161
|
-
${htmlBody}
|
|
162
|
-
</body>
|
|
163
|
-
</html>`
|
|
164
|
-
|
|
165
|
-
const htmlPath = `${basePath}.html`
|
|
166
|
-
fs.writeFileSync(htmlPath, html, 'utf-8')
|
|
167
|
-
console.log(`✅ HTML saved: ${htmlPath}`)
|
|
168
|
-
|
|
169
|
-
console.log('\n🎉 All formats generated successfully!\n')
|
|
349
|
+
// Save as Word document
|
|
350
|
+
const doc = buildDocx(markdown)
|
|
351
|
+
const docxBuffer = await Packer.toBuffer(doc)
|
|
352
|
+
const docxPath = `${basePath}.docx`
|
|
353
|
+
fs.writeFileSync(docxPath, docxBuffer)
|
|
354
|
+
console.log(`✅ Word doc saved: ${docxPath}`)
|
|
170
355
|
|
|
171
|
-
return { markdown, plainText
|
|
356
|
+
return { markdown, plainText }
|
|
172
357
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "client-handover",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "AI-powered handover document generator for frontend developers handing off client websites",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -11,13 +11,9 @@
|
|
|
11
11
|
"cli.js",
|
|
12
12
|
"index.js",
|
|
13
13
|
"generator.js",
|
|
14
|
-
"
|
|
15
|
-
"setup.js",
|
|
16
|
-
"deploy.js",
|
|
17
|
-
"credentials.js",
|
|
18
|
-
"license.js",
|
|
19
|
-
"postinstall.js",
|
|
14
|
+
"prompts.js",
|
|
20
15
|
"scanner.js",
|
|
16
|
+
"postinstall.js",
|
|
21
17
|
"README.md"
|
|
22
18
|
],
|
|
23
19
|
"scripts": {
|
|
@@ -41,7 +37,7 @@
|
|
|
41
37
|
"dependencies": {
|
|
42
38
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
43
39
|
"chalk": "^5.3.0",
|
|
44
|
-
"client-handover": "^1.0.
|
|
45
|
-
"
|
|
40
|
+
"client-handover": "^1.0.6",
|
|
41
|
+
"docx": "^8.5.0"
|
|
46
42
|
}
|
|
47
43
|
}
|
package/postinstall.js
CHANGED
|
@@ -9,56 +9,87 @@ const configDir = path.join(os.homedir(), '.handover')
|
|
|
9
9
|
const configPath = path.join(configDir, 'config.json')
|
|
10
10
|
const claudeCredsPath = path.join(os.homedir(), '.claude', '.credentials.json')
|
|
11
11
|
|
|
12
|
-
function
|
|
13
|
-
|
|
12
|
+
function loadConfig() {
|
|
13
|
+
try {
|
|
14
|
+
if (fs.existsSync(configPath)) {
|
|
15
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
|
16
|
+
}
|
|
17
|
+
} catch {}
|
|
18
|
+
return {}
|
|
19
|
+
}
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} catch {}
|
|
20
|
-
}
|
|
21
|
+
function saveConfig(config) {
|
|
22
|
+
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true })
|
|
23
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8')
|
|
24
|
+
}
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
function hasApiKey(config) {
|
|
27
|
+
if (process.env.ANTHROPIC_API_KEY) return true
|
|
28
|
+
if (config?.apiKey) return true
|
|
29
|
+
try {
|
|
30
|
+
if (fs.existsSync(claudeCredsPath)) {
|
|
24
31
|
const creds = JSON.parse(fs.readFileSync(claudeCredsPath, 'utf-8'))
|
|
25
32
|
const token = creds?.claudeAiOauth?.accessToken
|
|
26
33
|
const expiresAt = creds?.claudeAiOauth?.expiresAt
|
|
27
34
|
if (token && (!expiresAt || expiresAt > Date.now())) return true
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
35
|
+
}
|
|
36
|
+
} catch {}
|
|
31
37
|
return false
|
|
32
38
|
}
|
|
33
39
|
|
|
34
|
-
|
|
35
|
-
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true })
|
|
36
|
-
fs.writeFileSync(configPath, JSON.stringify({ apiKey: key }, null, 2), 'utf-8')
|
|
37
|
-
}
|
|
40
|
+
if (!process.stdin.isTTY) process.exit(0)
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
const config = loadConfig()
|
|
43
|
+
const needsApiKey = !hasApiKey(config)
|
|
44
|
+
const needsDeveloperInfo = !config.developerName
|
|
45
|
+
|
|
46
|
+
if (!needsApiKey && !needsDeveloperInfo) process.exit(0)
|
|
43
47
|
|
|
44
48
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
45
49
|
|
|
50
|
+
function ask(question) {
|
|
51
|
+
return new Promise(resolve => rl.question(question, answer => resolve(answer.trim())))
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
console.log('\n──────────────────────────────────────────')
|
|
47
55
|
console.log(' client-handover setup')
|
|
48
56
|
console.log('──────────────────────────────────────────')
|
|
49
|
-
console.log('
|
|
50
|
-
console.log(' Get one free at: https://console.anthropic.com\n')
|
|
57
|
+
console.log(' This tool generates professional handover documents for client websites.\n')
|
|
51
58
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
async function run() {
|
|
60
|
+
if (needsApiKey) {
|
|
61
|
+
console.log(' To generate documents you need an Anthropic API key.')
|
|
62
|
+
console.log(' Get one free at: https://console.anthropic.com\n')
|
|
63
|
+
const key = await ask(' Enter your Anthropic API key (or press Enter to skip): ')
|
|
64
|
+
if (key) {
|
|
65
|
+
config.apiKey = key
|
|
66
|
+
console.log(' ✓ API key saved.\n')
|
|
67
|
+
} else {
|
|
68
|
+
console.log(' Skipped. Run "handover key <your-api-key>" at any time.\n')
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (needsDeveloperInfo) {
|
|
73
|
+
console.log(' Developer details will appear in all generated handover documents.\n')
|
|
74
|
+
const name = await ask(' Your full name (developer): ')
|
|
75
|
+
if (name) config.developerName = name
|
|
55
76
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
77
|
+
const company = await ask(' Your company name (or press Enter if not applicable): ')
|
|
78
|
+
if (company) config.developerCompany = company
|
|
79
|
+
|
|
80
|
+
const email = await ask(' Your email address: ')
|
|
81
|
+
if (email) config.developerEmail = email
|
|
82
|
+
|
|
83
|
+
const phone = await ask(' Your phone number (optional, press Enter to skip): ')
|
|
84
|
+
if (phone) config.developerPhone = phone
|
|
85
|
+
|
|
86
|
+
console.log()
|
|
59
87
|
}
|
|
60
88
|
|
|
61
|
-
|
|
62
|
-
|
|
89
|
+
rl.close()
|
|
90
|
+
saveConfig(config)
|
|
91
|
+
console.log(' ✅ Setup complete. Run "handover /create" in any project folder to generate handover documents.\n')
|
|
63
92
|
process.exit(0)
|
|
64
|
-
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
run()
|
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
CHANGED
|
@@ -182,6 +182,13 @@ export function scanProject(dir = process.cwd()) {
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
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
|
+
|
|
185
192
|
// --- Folder structure ---
|
|
186
193
|
const structure = getFolderStructure(dir)
|
|
187
194
|
if (structure.length) {
|
|
@@ -191,3 +198,53 @@ export function scanProject(dir = process.cwd()) {
|
|
|
191
198
|
|
|
192
199
|
return sections.join('\n')
|
|
193
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
|
+
}
|
package/credentials.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
export function credentials(projectInfo = '') {
|
|
2
|
-
return `
|
|
3
|
-
You are a technical documentation writer creating a CREDENTIALS section for a frontend website handover document.
|
|
4
|
-
|
|
5
|
-
⚠️ IMPORTANT: Never include real passwords, keys, or secrets. All sensitive values must use placeholder format: YOUR_VALUE_HERE or [REPLACE_WITH_ACTUAL_VALUE]
|
|
6
|
-
|
|
7
|
-
The following context has been automatically scanned from the developer's actual project files. Use the real environment variable keys and detected services to build the credentials table for THIS specific project. Do not invent services that are not present. Mark values as "[to be filled in]".
|
|
8
|
-
|
|
9
|
-
${projectInfo || '[No project data available — use placeholder examples]'}
|
|
10
|
-
|
|
11
|
-
Generate a CREDENTIALS & ACCESS section that includes:
|
|
12
|
-
|
|
13
|
-
## Credentials & Access
|
|
14
|
-
|
|
15
|
-
### For the Client (Plain English)
|
|
16
|
-
- What accounts exist and what each one is for
|
|
17
|
-
- Who currently has admin access
|
|
18
|
-
- How to reset a password if they get locked out
|
|
19
|
-
- Reminder to store credentials in a password manager (suggest 1Password, Bitwarden, etc.)
|
|
20
|
-
- What NOT to share over email
|
|
21
|
-
|
|
22
|
-
### For the Developer (Technical)
|
|
23
|
-
Generate a credentials table template with these categories (use placeholders for all values):
|
|
24
|
-
|
|
25
|
-
| Service | Purpose | URL | Username/Email | Notes |
|
|
26
|
-
|---------|---------|-----|----------------|-------|
|
|
27
|
-
|
|
28
|
-
Include rows for:
|
|
29
|
-
- Hosting provider (e.g. Vercel, Netlify, cPanel)
|
|
30
|
-
- Domain registrar
|
|
31
|
-
- CMS or admin panel (if applicable)
|
|
32
|
-
- Database access (if applicable)
|
|
33
|
-
- Third-party APIs (list each one)
|
|
34
|
-
- Email/SMTP service
|
|
35
|
-
- Analytics (e.g. Google Analytics)
|
|
36
|
-
- Version control (GitHub/GitLab repo URL)
|
|
37
|
-
- Any other relevant service
|
|
38
|
-
|
|
39
|
-
### API Keys & Environment Variables
|
|
40
|
-
List all .env variables used with placeholder values and a description of each.
|
|
41
|
-
|
|
42
|
-
### Access Transfer Checklist
|
|
43
|
-
- [ ] Client has been added as admin to hosting
|
|
44
|
-
- [ ] Client has been added to domain registrar
|
|
45
|
-
- [ ] Developer has been removed or downgraded after handover
|
|
46
|
-
- [ ] 2FA has been set up for client accounts
|
|
47
|
-
- [ ] All credentials have been shared via secure method (not email)
|
|
48
|
-
|
|
49
|
-
Format as clean Markdown with tables.
|
|
50
|
-
`.trim()
|
|
51
|
-
}
|
package/deploy.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
export function deploy(projectInfo = '') {
|
|
2
|
-
return `
|
|
3
|
-
You are a technical documentation writer creating a DEPLOYMENT section for a frontend website handover document.
|
|
4
|
-
|
|
5
|
-
The audience is TWO types of readers:
|
|
6
|
-
1. A NON-TECHNICAL CLIENT — plain English, no jargon
|
|
7
|
-
2. A DEVELOPER — exact technical steps
|
|
8
|
-
|
|
9
|
-
The following context has been automatically scanned from the developer's actual project files (deploy configs, package.json scripts, workflow files, etc.). Use it to generate deployment documentation specific to THIS project — do not use generic placeholders. Mark any missing details as "[to be filled in]".
|
|
10
|
-
|
|
11
|
-
${projectInfo || '[No project data available — use placeholder examples]'}
|
|
12
|
-
|
|
13
|
-
Generate a DEPLOYMENT section that includes:
|
|
14
|
-
|
|
15
|
-
## Deployment & Hosting
|
|
16
|
-
|
|
17
|
-
### For the Client (Plain English)
|
|
18
|
-
- Where the website is hosted and what that means
|
|
19
|
-
- How updates get published (automatic or manual?)
|
|
20
|
-
- What happens if the site goes down — who to call, what to check
|
|
21
|
-
- Estimated monthly/yearly hosting costs if known
|
|
22
|
-
|
|
23
|
-
### For the Developer (Technical)
|
|
24
|
-
- Hosting provider and account details (use placeholders for sensitive info)
|
|
25
|
-
- Deployment method (e.g. Vercel push-to-deploy, FTP, CI/CD pipeline)
|
|
26
|
-
- Step-by-step deploy process with exact commands
|
|
27
|
-
- Domain/DNS setup and where it's managed
|
|
28
|
-
- SSL certificate info
|
|
29
|
-
- Build command and output directory
|
|
30
|
-
- Any environment-specific configs (staging vs production)
|
|
31
|
-
- Rollback procedure if a deployment fails
|
|
32
|
-
|
|
33
|
-
Format as clean Markdown. Use code blocks for terminal commands.
|
|
34
|
-
`.trim()
|
|
35
|
-
}
|
package/handover.js
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
export function handover(projectInfo = '') {
|
|
2
|
-
return `
|
|
3
|
-
You are an expert technical writer generating a COMPLETE CLIENT WEBSITE HANDOVER DOCUMENT.
|
|
4
|
-
|
|
5
|
-
This document is for TWO audiences:
|
|
6
|
-
1. NON-TECHNICAL CLIENT — plain English, reassuring tone, no jargon. They need to feel confident owning this site.
|
|
7
|
-
2. DEVELOPER (future maintainer) — precise, technical, complete. They need to be able to pick this up cold.
|
|
8
|
-
|
|
9
|
-
The following context has been automatically scanned from the developer's actual project files (package.json, config files, folder structure, environment variable keys, deploy configs, README, etc.). Use this to generate a document specific to THIS project — do not invent or use generic placeholder examples. If a specific detail is not available in the scanned data, note it as "[to be filled in]" rather than guessing.
|
|
10
|
-
|
|
11
|
-
${projectInfo || '[No project data available — use placeholder examples throughout]'}
|
|
12
|
-
|
|
13
|
-
Generate a full handover document with ALL of the following sections:
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
# [Project Name] — Website Handover Document
|
|
18
|
-
**Prepared by:** [Developer Name]
|
|
19
|
-
**Handover Date:** [Date]
|
|
20
|
-
**Project URL:** [Live URL]
|
|
21
|
-
**Repository:** [Repo URL]
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
## 1. Project Overview
|
|
26
|
-
- What the site does (1 paragraph, plain English)
|
|
27
|
-
- Tech stack summary (with one-line plain English explanation of each)
|
|
28
|
-
- Key contacts (developer, hosting support, etc.)
|
|
29
|
-
|
|
30
|
-
## 2. Project Setup & Dependencies
|
|
31
|
-
*(Follow the dual-audience format: client section + developer section)*
|
|
32
|
-
- Prerequisites, install steps, local dev commands, environment variables
|
|
33
|
-
|
|
34
|
-
## 3. Deployment & Hosting
|
|
35
|
-
*(Dual-audience format)*
|
|
36
|
-
- Hosting provider, deploy method, domain/DNS, SSL, build config, rollback steps
|
|
37
|
-
|
|
38
|
-
## 4. Credentials & Access
|
|
39
|
-
*(Dual-audience format)*
|
|
40
|
-
- All accounts, credentials table with placeholders, .env variables, access transfer checklist
|
|
41
|
-
|
|
42
|
-
## 5. Site Structure & Content Management
|
|
43
|
-
### For the Client
|
|
44
|
-
- How to update content (CMS, direct file edits, or contact developer)
|
|
45
|
-
- What they should NEVER touch without developer help
|
|
46
|
-
- How to add new pages or blog posts (if applicable)
|
|
47
|
-
|
|
48
|
-
### For the Developer
|
|
49
|
-
- Folder structure overview
|
|
50
|
-
- Where key config files live
|
|
51
|
-
- Component/template structure
|
|
52
|
-
- Any custom hooks, stores, or utilities worth knowing about
|
|
53
|
-
|
|
54
|
-
## 6. Maintenance & Updates
|
|
55
|
-
### For the Client
|
|
56
|
-
- What needs renewing and when (domain, hosting, licences)
|
|
57
|
-
- How to know if something is broken
|
|
58
|
-
- Recommended monthly/yearly maintenance tasks
|
|
59
|
-
|
|
60
|
-
### For the Developer
|
|
61
|
-
- How to update npm dependencies safely
|
|
62
|
-
- Any known technical debt or TODO items
|
|
63
|
-
- Performance and SEO considerations
|
|
64
|
-
|
|
65
|
-
## 7. Licensing & Attribution
|
|
66
|
-
*(Dual-audience format)*
|
|
67
|
-
- Ownership summary, third-party licences table, attribution requirements, portfolio rights
|
|
68
|
-
|
|
69
|
-
## 8. Handover Checklist
|
|
70
|
-
A final sign-off checklist for both parties:
|
|
71
|
-
|
|
72
|
-
### Developer Checklist (before handover)
|
|
73
|
-
- [ ] All credentials transferred securely
|
|
74
|
-
- [ ] Client added to hosting & domain accounts
|
|
75
|
-
- [ ] Repository access transferred or shared
|
|
76
|
-
- [ ] Live site tested across browsers and devices
|
|
77
|
-
- [ ] Analytics verified working
|
|
78
|
-
- [ ] All placeholder content replaced
|
|
79
|
-
- [ ] .env.example file committed to repo
|
|
80
|
-
- [ ] README updated
|
|
81
|
-
- [ ] Handover document reviewed with client
|
|
82
|
-
|
|
83
|
-
### Client Acknowledgement
|
|
84
|
-
- [ ] I have received all login credentials
|
|
85
|
-
- [ ] I understand how to update content
|
|
86
|
-
- [ ] I know who to contact for technical support
|
|
87
|
-
- [ ] I have been shown the live site and approve it
|
|
88
|
-
|
|
89
|
-
---
|
|
90
|
-
|
|
91
|
-
Format the entire document in clean, well-structured Markdown.
|
|
92
|
-
Use code blocks for all terminal commands.
|
|
93
|
-
Use tables where appropriate.
|
|
94
|
-
Keep client sections warm, clear, and jargon-free.
|
|
95
|
-
Keep developer sections precise and thorough.
|
|
96
|
-
`.trim()
|
|
97
|
-
}
|
package/setup.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
export function setup(projectInfo = '') {
|
|
2
|
-
return `
|
|
3
|
-
You are a technical documentation writer creating a SETUP section for a frontend website handover document.
|
|
4
|
-
|
|
5
|
-
The audience is TWO types of readers:
|
|
6
|
-
1. A NON-TECHNICAL CLIENT — who needs plain-English explanations of what everything is and why it matters
|
|
7
|
-
2. A DEVELOPER — who needs exact commands, file paths, and technical detail
|
|
8
|
-
|
|
9
|
-
The following context has been automatically scanned from the developer's actual project files. Use it to generate documentation specific to THIS project — do not use generic placeholders. If something is unclear from the scanned data, mark it as "[to be filled in]".
|
|
10
|
-
|
|
11
|
-
${projectInfo || '[No project data available — use placeholder examples]'}
|
|
12
|
-
|
|
13
|
-
Generate a SETUP section that includes:
|
|
14
|
-
|
|
15
|
-
## Project Setup & Dependencies
|
|
16
|
-
|
|
17
|
-
### For the Client (Plain English)
|
|
18
|
-
- What tech stack is used and a simple one-line explanation of each part
|
|
19
|
-
- What they'll need to pay for or maintain (hosting, domain, licences)
|
|
20
|
-
- Who to contact if something breaks
|
|
21
|
-
|
|
22
|
-
### For the Developer (Technical)
|
|
23
|
-
- Prerequisites (Node version, package manager, etc.)
|
|
24
|
-
- Step-by-step install instructions with exact terminal commands
|
|
25
|
-
- Environment variables needed (use placeholder values like YOUR_VALUE_HERE)
|
|
26
|
-
- How to run locally (dev server command, expected URL)
|
|
27
|
-
- Any known gotchas or first-time setup issues
|
|
28
|
-
|
|
29
|
-
Format this as clean, well-structured Markdown with clear headings.
|
|
30
|
-
Keep the client section jargon-free. Keep the developer section precise.
|
|
31
|
-
`.trim()
|
|
32
|
-
}
|