client-handover 1.4.1 → 1.5.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/cli.js +202 -174
- package/generator.js +52 -8
- package/package.json +2 -2
package/cli.js
CHANGED
|
@@ -1,174 +1,202 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { generateDoc } 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(
|
|
36
|
-
console.log('
|
|
37
|
-
console.log('
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
console.log(chalk.bold('
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
console.log(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (docType === '2' || docType === '3') {
|
|
142
|
-
console.log(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
+
let key = ''
|
|
77
|
+
while (!key) {
|
|
78
|
+
key = await ask(rl, ' Enter your Anthropic API key: ')
|
|
79
|
+
if (!key) console.log(chalk.yellow(' API key is required to continue.\n'))
|
|
80
|
+
}
|
|
81
|
+
config.apiKey = key
|
|
82
|
+
console.log(chalk.green(' ✓ API key saved.\n'))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!hasDeveloperInfo) {
|
|
86
|
+
console.log(' Your details will appear in all generated handover documents.\n')
|
|
87
|
+
const name = await ask(rl, ' Developer name: ')
|
|
88
|
+
if (name) config.developerName = name
|
|
89
|
+
|
|
90
|
+
const company = await ask(rl, ' Company name (press Enter if not applicable): ')
|
|
91
|
+
if (company) config.developerCompany = company
|
|
92
|
+
|
|
93
|
+
const email = await ask(rl, ' Email address: ')
|
|
94
|
+
if (email) config.developerEmail = email
|
|
95
|
+
|
|
96
|
+
const phone = await ask(rl, ' 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
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
121
|
+
let docType = ''
|
|
122
|
+
while (!['1', '2', '3'].includes(docType)) {
|
|
123
|
+
docType = await ask(rl, ' What would you like to generate?\n 1 - Technical handover\n 2 - Non-technical handover\n 3 - Both\n\n Enter 1, 2 or 3: ')
|
|
124
|
+
if (!['1', '2', '3'].includes(docType)) console.log(chalk.yellow('\n Please enter 1, 2 or 3.\n'))
|
|
125
|
+
}
|
|
126
|
+
rl.close()
|
|
127
|
+
console.log()
|
|
128
|
+
|
|
129
|
+
console.log(chalk.dim(' Scanning project files...'))
|
|
130
|
+
const projectInfo = scanProject(process.cwd())
|
|
131
|
+
|
|
132
|
+
const techDir = './technical-handover'
|
|
133
|
+
const clientDir = './client-handover'
|
|
134
|
+
|
|
135
|
+
if (docType === '1' || docType === '3') {
|
|
136
|
+
console.log(chalk.bold('\n Generating Technical Handover Document...'))
|
|
137
|
+
const techPrompt = technicalHandoverPrompt(projectInfo, developerInfo)
|
|
138
|
+
await generateDoc(techPrompt, 'technical-handover', techDir)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (docType === '2' || docType === '3') {
|
|
142
|
+
console.log(chalk.bold('\n Generating Client Handover Document...'))
|
|
143
|
+
const nonTechPrompt = nonTechnicalHandoverPrompt(projectInfo, developerInfo)
|
|
144
|
+
await generateDoc(nonTechPrompt, 'client-handover', clientDir)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(chalk.green.bold('\n ✅ Handover documents created successfully!\n'))
|
|
148
|
+
if (docType === '1' || docType === '3') {
|
|
149
|
+
console.log(` ${chalk.cyan('./technical-handover/')}`)
|
|
150
|
+
console.log(` technical-handover.md`)
|
|
151
|
+
console.log(` technical-handover.txt`)
|
|
152
|
+
console.log(` technical-handover.docx`)
|
|
153
|
+
console.log()
|
|
154
|
+
}
|
|
155
|
+
if (docType === '2' || docType === '3') {
|
|
156
|
+
console.log(` ${chalk.cyan('./client-handover/')}`)
|
|
157
|
+
console.log(` client-handover.md`)
|
|
158
|
+
console.log(` client-handover.txt`)
|
|
159
|
+
console.log(` client-handover.docx`)
|
|
160
|
+
console.log()
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function main() {
|
|
165
|
+
const [,, rawCommand, secondArg] = process.argv
|
|
166
|
+
|
|
167
|
+
if (!rawCommand || rawCommand === '--help' || rawCommand === '-h') {
|
|
168
|
+
printHelp()
|
|
169
|
+
process.exit(0)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const command = normalizeCommand(rawCommand)
|
|
173
|
+
|
|
174
|
+
if (command === 'key') {
|
|
175
|
+
if (!secondArg) {
|
|
176
|
+
console.error(chalk.red('\n ❌ Usage: handover key <your-api-key>\n'))
|
|
177
|
+
process.exit(1)
|
|
178
|
+
}
|
|
179
|
+
saveApiKey(secondArg)
|
|
180
|
+
console.log(chalk.green('\n ✅ API key saved. Run "handover /create" to get started.\n'))
|
|
181
|
+
process.exit(0)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (command !== 'create') {
|
|
185
|
+
console.error(chalk.red(`\n ❌ Unknown command: ${rawCommand}\n`))
|
|
186
|
+
printHelp()
|
|
187
|
+
process.exit(1)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
await runCreate()
|
|
192
|
+
} catch (err) {
|
|
193
|
+
if (err.status === 401) {
|
|
194
|
+
console.error(chalk.red('\n ❌ Authentication failed. Run "handover key <your-api-key>" to set your API key.\n'))
|
|
195
|
+
} else {
|
|
196
|
+
console.error(chalk.red(`\n ❌ Error: ${err.message}\n`))
|
|
197
|
+
}
|
|
198
|
+
process.exit(1)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
main()
|
package/generator.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Anthropic from '@anthropic-ai/sdk'
|
|
2
2
|
import {
|
|
3
3
|
Document, Packer, Paragraph, TextRun, HeadingLevel,
|
|
4
4
|
AlignmentType, BorderStyle, TableRow, TableCell, Table,
|
|
@@ -6,6 +6,51 @@ import {
|
|
|
6
6
|
} from 'docx'
|
|
7
7
|
import fs from 'fs'
|
|
8
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 })
|
|
9
54
|
|
|
10
55
|
// ---------------------------------------------------------------------------
|
|
11
56
|
// Inline text parser — splits a line into TextRun segments handling **bold**,
|
|
@@ -253,14 +298,13 @@ function buildDocx(markdown) {
|
|
|
253
298
|
export async function generateDoc(prompt, outputName = 'handover', outputDir = './output') {
|
|
254
299
|
console.log('⏳ Generating document with Claude...\n')
|
|
255
300
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
301
|
+
const message = await client.messages.create({
|
|
302
|
+
model: 'claude-sonnet-4-6',
|
|
303
|
+
max_tokens: 4096,
|
|
304
|
+
messages: [{ role: 'user', content: prompt }]
|
|
305
|
+
})
|
|
262
306
|
|
|
263
|
-
|
|
307
|
+
const markdown = message.content[0].text
|
|
264
308
|
|
|
265
309
|
if (!fs.existsSync(outputDir)) {
|
|
266
310
|
fs.mkdirSync(outputDir, { recursive: true })
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "client-handover",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "AI-powered handover document generator for frontend developers handing off client websites",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"node": ">=18"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@anthropic-ai/
|
|
32
|
+
"@anthropic-ai/sdk": "^0.39.0",
|
|
33
33
|
"chalk": "^5.3.0",
|
|
34
34
|
"docx": "^8.5.0"
|
|
35
35
|
}
|