client-handover 1.0.5 → 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.
Potentially problematic release.
This version of client-handover might be problematic. Click here for more details.
- package/README.md +11 -0
- package/cli.js +87 -90
- package/generator.js +289 -104
- package/license.js +3 -2
- package/package.json +5 -8
- package/postinstall.js +63 -32
- package/prompts.js +185 -0
- package/scanner.js +250 -0
- package/credentials.js +0 -50
- package/deploy.js +0 -34
- package/handover.js +0 -96
- package/setup.js +0 -31
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/license.js
CHANGED
|
@@ -2,8 +2,9 @@ export function license(projectInfo = '') {
|
|
|
2
2
|
return `
|
|
3
3
|
You are a technical documentation writer creating a LICENSING & ATTRIBUTION section for a frontend website handover document.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
The following context has been automatically scanned from the developer's actual project files. Use the real dependencies, license file, and detected libraries to generate licensing documentation specific to THIS project — do not use generic placeholder libraries.
|
|
6
|
+
|
|
7
|
+
${projectInfo || '[No project data available — use placeholder examples]'}
|
|
7
8
|
|
|
8
9
|
Generate a LICENSING & ATTRIBUTION section that includes:
|
|
9
10
|
|
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,11 +11,8 @@
|
|
|
11
11
|
"cli.js",
|
|
12
12
|
"index.js",
|
|
13
13
|
"generator.js",
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"deploy.js",
|
|
17
|
-
"credentials.js",
|
|
18
|
-
"license.js",
|
|
14
|
+
"prompts.js",
|
|
15
|
+
"scanner.js",
|
|
19
16
|
"postinstall.js",
|
|
20
17
|
"README.md"
|
|
21
18
|
],
|
|
@@ -40,7 +37,7 @@
|
|
|
40
37
|
"dependencies": {
|
|
41
38
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
42
39
|
"chalk": "^5.3.0",
|
|
43
|
-
"client-handover": "^1.0.
|
|
44
|
-
"
|
|
40
|
+
"client-handover": "^1.0.6",
|
|
41
|
+
"docx": "^8.5.0"
|
|
45
42
|
}
|
|
46
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()
|