client-handover 1.0.4 → 1.0.6

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 CHANGED
@@ -163,7 +163,9 @@ All prompt builders accept an optional `projectInfo` string. If omitted, Claude
163
163
  ## Requirements
164
164
 
165
165
  - Node.js 18+
166
- - An Anthropic API key — [get one free at console.anthropic.com](https://console.anthropic.com)
166
+ - One of the following:
167
+ - [Claude Code](https://marketplace.visualstudio.com/items?itemName=anthropic.claude-code) installed (zero config — credentials detected automatically)
168
+ - Or an Anthropic API key — [get one free at console.anthropic.com](https://console.anthropic.com)
167
169
 
168
170
  ---
169
171
 
@@ -196,6 +198,32 @@ client-handover/
196
198
 
197
199
  ---
198
200
 
201
+ ## Changelog
202
+
203
+ ### 1.0.6
204
+ - Auto-scans your actual project files on every run — reads package.json, config files, env variable keys, deploy configs, folder structure, and more
205
+ - Generated documents are now tailored to your real project, not generic templates
206
+ - Extra notes file still supported as optional additional context
207
+
208
+ ### 1.0.5
209
+ - Interactive API key setup prompt on install — no manual config needed
210
+ - Added `handover key <api-key>` command to save key at any time
211
+ - Key is stored in `~/.handover/config.json` and persists across all sessions
212
+
213
+ ### 1.0.4
214
+ - Auto-detects Claude Code credentials — no API key setup needed if you have Claude Code installed
215
+
216
+ ### 1.0.3
217
+ - Commands now work with or without a leading `/` — fixes Git Bash path conversion issues on Windows
218
+
219
+ ### 1.0.2
220
+ - Added `all` command — generates every section as separate files in a single named folder
221
+
222
+ ### 1.0.1
223
+ - Initial release
224
+
225
+ ---
226
+
199
227
  ## Contributing
200
228
 
201
229
  Pull requests are welcome. For major changes, open an issue first.
package/cli.js CHANGED
@@ -5,7 +5,8 @@ import { deploy } from './deploy.js'
5
5
  import { credentials } from './credentials.js'
6
6
  import { handover } from './handover.js'
7
7
  import { license } from './license.js'
8
- import { generateDoc } from './generator.js'
8
+ import { generateDoc, saveApiKey } from './generator.js'
9
+ import { scanProject } from './scanner.js'
9
10
  import chalk from 'chalk'
10
11
  import fs from 'fs'
11
12
  import path from 'path'
@@ -33,6 +34,7 @@ function printHelp() {
33
34
  console.log(` ${chalk.cyan(cmd.padEnd(16))} ${label}`)
34
35
  })
35
36
  console.log(` ${chalk.cyan('all'.padEnd(16))} All sections in a single folder`)
37
+ console.log(` ${chalk.cyan('key <api-key>'.padEnd(16))} Save your Anthropic API key (one-time setup)`)
36
38
  console.log('\n' + chalk.dim('Examples:'))
37
39
  console.log(' handover handover # Full doc with placeholders')
38
40
  console.log(' handover setup project-info.txt # Setup section using your notes')
@@ -41,7 +43,7 @@ function printHelp() {
41
43
  console.log()
42
44
  }
43
45
 
44
- async function runAll(projectInfo, folderName) {
46
+ async function runAll(projectInfo, folderName, outputName) {
45
47
  const sections = [
46
48
  { key: 'handover', fn: handover, label: 'Full Handover Document' },
47
49
  { key: 'setup', fn: setup, label: 'Project Setup & Dependencies' },
@@ -74,17 +76,34 @@ async function main() {
74
76
 
75
77
  const command = normalizeCommand(rawCommand)
76
78
 
77
- // Read optional project info file
78
- let projectInfo = ''
79
+ if (command === 'key') {
80
+ const key = infoFile // second arg is the key
81
+ if (!key) {
82
+ console.error(chalk.red('\n❌ Usage: handover key <your-api-key>\n'))
83
+ process.exit(1)
84
+ }
85
+ saveApiKey(key)
86
+ console.log(chalk.green('\n✅ API key saved. You\'re all set — run any handover command.\n'))
87
+ process.exit(0)
88
+ }
89
+
90
+ // Scan the current project directory
91
+ console.log(chalk.dim('\n🔍 Scanning project...'))
92
+ const scannedContext = scanProject(process.cwd())
93
+
94
+ // Read optional extra project info file
95
+ let extraInfo = ''
79
96
  if (infoFile) {
80
97
  if (!fs.existsSync(infoFile)) {
81
98
  console.error(chalk.red(`\n❌ File not found: ${infoFile}\n`))
82
99
  process.exit(1)
83
100
  }
84
- projectInfo = fs.readFileSync(infoFile, 'utf-8')
85
- console.log(chalk.green(`\n📄 Loaded project info from: ${infoFile}`))
101
+ extraInfo = fs.readFileSync(infoFile, 'utf-8')
102
+ console.log(chalk.green(`📄 Loaded extra info from: ${infoFile}`))
86
103
  }
87
104
 
105
+ const projectInfo = [scannedContext, extraInfo].filter(Boolean).join('\n\n---\n\n')
106
+
88
107
  if (command === 'all') {
89
108
  const folderName = outputName || 'all'
90
109
  try {
package/credentials.js CHANGED
@@ -4,8 +4,9 @@ You are a technical documentation writer creating a CREDENTIALS section for a fr
4
4
 
5
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
6
 
7
- Using the following project information:
8
- ${projectInfo || '[No project info provided — use placeholder examples]'}
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]'}
9
10
 
10
11
  Generate a CREDENTIALS & ACCESS section that includes:
11
12
 
package/deploy.js CHANGED
@@ -6,8 +6,9 @@ The audience is TWO types of readers:
6
6
  1. A NON-TECHNICAL CLIENT — plain English, no jargon
7
7
  2. A DEVELOPER — exact technical steps
8
8
 
9
- Using the following project information:
10
- ${projectInfo || '[No project info provided — use placeholder examples]'}
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]'}
11
12
 
12
13
  Generate a DEPLOYMENT section that includes:
13
14
 
package/generator.js CHANGED
@@ -7,6 +7,17 @@ import os from 'os'
7
7
  function resolveApiKey() {
8
8
  if (process.env.ANTHROPIC_API_KEY) return process.env.ANTHROPIC_API_KEY
9
9
 
10
+ // Check saved key from `handover key <your-key>`
11
+ const configPath = path.join(os.homedir(), '.handover', 'config.json')
12
+ if (fs.existsSync(configPath)) {
13
+ try {
14
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
15
+ if (config?.apiKey) return config.apiKey
16
+ } catch {
17
+ // ignore malformed config file
18
+ }
19
+ }
20
+
10
21
  const credentialsPath = path.join(os.homedir(), '.claude', '.credentials.json')
11
22
  if (fs.existsSync(credentialsPath)) {
12
23
  try {
@@ -24,6 +35,13 @@ function resolveApiKey() {
24
35
  return null
25
36
  }
26
37
 
38
+ export function saveApiKey(key) {
39
+ const configDir = path.join(os.homedir(), '.handover')
40
+ const configPath = path.join(configDir, 'config.json')
41
+ if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true })
42
+ fs.writeFileSync(configPath, JSON.stringify({ apiKey: key }, null, 2), 'utf-8')
43
+ }
44
+
27
45
  const apiKey = resolveApiKey()
28
46
  if (!apiKey) {
29
47
  console.error('\n❌ No API key found. Set ANTHROPIC_API_KEY or install Claude Code (code.visualstudio.com/download).\n')
package/handover.js CHANGED
@@ -6,8 +6,9 @@ This document is for TWO audiences:
6
6
  1. NON-TECHNICAL CLIENT — plain English, reassuring tone, no jargon. They need to feel confident owning this site.
7
7
  2. DEVELOPER (future maintainer) — precise, technical, complete. They need to be able to pick this up cold.
8
8
 
9
- Using the following project information from the developer:
10
- ${projectInfo || '[No project info provided — use placeholder examples throughout]'}
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]'}
11
12
 
12
13
  Generate a full handover document with ALL of the following sections:
13
14
 
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
- Using the following project information:
6
- ${projectInfo || '[No project info provided — use placeholder examples]'}
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.4",
3
+ "version": "1.0.6",
4
4
  "description": "AI-powered handover document generator for frontend developers handing off client websites",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -16,10 +16,13 @@
16
16
  "deploy.js",
17
17
  "credentials.js",
18
18
  "license.js",
19
+ "postinstall.js",
20
+ "scanner.js",
19
21
  "README.md"
20
22
  ],
21
23
  "scripts": {
22
- "test": "node test.js"
24
+ "test": "node test.js",
25
+ "postinstall": "node postinstall.js"
23
26
  },
24
27
  "keywords": [
25
28
  "handover",
package/postinstall.js ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+
3
+ import readline from 'readline'
4
+ import fs from 'fs'
5
+ import path from 'path'
6
+ import os from 'os'
7
+
8
+ const configDir = path.join(os.homedir(), '.handover')
9
+ const configPath = path.join(configDir, 'config.json')
10
+ const claudeCredsPath = path.join(os.homedir(), '.claude', '.credentials.json')
11
+
12
+ function alreadyConfigured() {
13
+ if (process.env.ANTHROPIC_API_KEY) return true
14
+
15
+ if (fs.existsSync(configPath)) {
16
+ try {
17
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
18
+ if (config?.apiKey) return true
19
+ } catch {}
20
+ }
21
+
22
+ if (fs.existsSync(claudeCredsPath)) {
23
+ try {
24
+ const creds = JSON.parse(fs.readFileSync(claudeCredsPath, 'utf-8'))
25
+ const token = creds?.claudeAiOauth?.accessToken
26
+ const expiresAt = creds?.claudeAiOauth?.expiresAt
27
+ if (token && (!expiresAt || expiresAt > Date.now())) return true
28
+ } catch {}
29
+ }
30
+
31
+ return false
32
+ }
33
+
34
+ function saveKey(key) {
35
+ if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true })
36
+ fs.writeFileSync(configPath, JSON.stringify({ apiKey: key }, null, 2), 'utf-8')
37
+ }
38
+
39
+ // Skip if already set up or not in an interactive terminal
40
+ if (alreadyConfigured() || !process.stdin.isTTY) {
41
+ process.exit(0)
42
+ }
43
+
44
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
45
+
46
+ console.log('\n──────────────────────────────────────────')
47
+ console.log(' client-handover setup')
48
+ console.log('──────────────────────────────────────────')
49
+ console.log(' To generate documents, you need an Anthropic API key.')
50
+ console.log(' Get one free at: https://console.anthropic.com\n')
51
+
52
+ rl.question(' Enter your Anthropic API key (or press Enter to skip): ', (answer) => {
53
+ rl.close()
54
+ const key = answer.trim()
55
+
56
+ if (!key) {
57
+ console.log('\n Skipped. Run "handover key <your-api-key>" at any time to set it.\n')
58
+ process.exit(0)
59
+ }
60
+
61
+ saveKey(key)
62
+ console.log('\n✅ API key saved. Run "handover handover" to get started.\n')
63
+ process.exit(0)
64
+ })
package/scanner.js ADDED
@@ -0,0 +1,193 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+
4
+ const CONFIG_FILES = [
5
+ 'vite.config.js', 'vite.config.ts',
6
+ 'next.config.js', 'next.config.ts', 'next.config.mjs',
7
+ 'nuxt.config.js', 'nuxt.config.ts',
8
+ 'astro.config.js', 'astro.config.ts', 'astro.config.mjs',
9
+ 'svelte.config.js',
10
+ 'remix.config.js',
11
+ 'tailwind.config.js', 'tailwind.config.ts',
12
+ 'postcss.config.js',
13
+ 'netlify.toml',
14
+ 'vercel.json',
15
+ 'render.yaml',
16
+ 'railway.json',
17
+ '.htaccess',
18
+ 'Dockerfile',
19
+ 'docker-compose.yml',
20
+ 'firebase.json',
21
+ ]
22
+
23
+ const DEPLOY_WORKFLOW_DIR = '.github/workflows'
24
+
25
+ function readFileSafe(filePath) {
26
+ try {
27
+ return fs.readFileSync(filePath, 'utf-8')
28
+ } catch {
29
+ return null
30
+ }
31
+ }
32
+
33
+ function getEnvKeys(filePath) {
34
+ const content = readFileSafe(filePath)
35
+ if (!content) return []
36
+ return content
37
+ .split('\n')
38
+ .map(l => l.trim())
39
+ .filter(l => l && !l.startsWith('#') && l.includes('='))
40
+ .map(l => l.split('=')[0].trim())
41
+ }
42
+
43
+ function getFolderStructure(dir, depth = 0, maxDepth = 2) {
44
+ if (depth > maxDepth) return []
45
+ const ignore = new Set(['node_modules', '.git', '.next', '.nuxt', 'dist', 'build', '.cache', 'coverage', '.turbo'])
46
+ let entries = []
47
+ try {
48
+ const items = fs.readdirSync(dir, { withFileTypes: true })
49
+ for (const item of items) {
50
+ if (ignore.has(item.name) || item.name.startsWith('.')) continue
51
+ const indent = ' '.repeat(depth)
52
+ if (item.isDirectory()) {
53
+ entries.push(`${indent}${item.name}/`)
54
+ entries.push(...getFolderStructure(path.join(dir, item.name), depth + 1, maxDepth))
55
+ } else {
56
+ entries.push(`${indent}${item.name}`)
57
+ }
58
+ }
59
+ } catch {}
60
+ return entries
61
+ }
62
+
63
+ function detectFramework(deps = {}, devDeps = {}) {
64
+ const all = { ...deps, ...devDeps }
65
+ if (all['next']) return 'Next.js'
66
+ if (all['nuxt'] || all['nuxt3']) return 'Nuxt'
67
+ if (all['@astrojs/core'] || all['astro']) return 'Astro'
68
+ if (all['@sveltejs/kit']) return 'SvelteKit'
69
+ if (all['svelte']) return 'Svelte'
70
+ if (all['@remix-run/react']) return 'Remix'
71
+ if (all['gatsby']) return 'Gatsby'
72
+ if (all['react']) return 'React'
73
+ if (all['vue']) return 'Vue'
74
+ if (all['angular']) return 'Angular'
75
+ return null
76
+ }
77
+
78
+ export function scanProject(dir = process.cwd()) {
79
+ const sections = []
80
+
81
+ // --- package.json ---
82
+ const pkgPath = path.join(dir, 'package.json')
83
+ let pkg = null
84
+ if (fs.existsSync(pkgPath)) {
85
+ try {
86
+ pkg = JSON.parse(readFileSafe(pkgPath))
87
+ const framework = detectFramework(pkg.dependencies, pkg.devDependencies)
88
+ sections.push(`## package.json`)
89
+ sections.push(`Name: ${pkg.name || 'unknown'}`)
90
+ if (pkg.description) sections.push(`Description: ${pkg.description}`)
91
+ if (pkg.version) sections.push(`Version: ${pkg.version}`)
92
+ if (framework) sections.push(`Detected framework: ${framework}`)
93
+ if (pkg.engines?.node) sections.push(`Node requirement: ${pkg.engines.node}`)
94
+
95
+ if (pkg.scripts && Object.keys(pkg.scripts).length) {
96
+ sections.push(`\nScripts:`)
97
+ for (const [k, v] of Object.entries(pkg.scripts)) {
98
+ sections.push(` ${k}: ${v}`)
99
+ }
100
+ }
101
+
102
+ const deps = Object.keys(pkg.dependencies || {})
103
+ const devDeps = Object.keys(pkg.devDependencies || {})
104
+ if (deps.length) sections.push(`\nDependencies: ${deps.join(', ')}`)
105
+ if (devDeps.length) sections.push(`Dev dependencies: ${devDeps.join(', ')}`)
106
+ } catch {}
107
+ }
108
+
109
+ // --- Package manager ---
110
+ let pm = 'npm'
111
+ if (fs.existsSync(path.join(dir, 'yarn.lock'))) pm = 'yarn'
112
+ else if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))) pm = 'pnpm'
113
+ else if (fs.existsSync(path.join(dir, 'bun.lockb'))) pm = 'bun'
114
+ sections.push(`\nPackage manager: ${pm}`)
115
+
116
+ // --- Environment variables ---
117
+ const envExampleKeys = getEnvKeys(path.join(dir, '.env.example'))
118
+ const envLocalKeys = getEnvKeys(path.join(dir, '.env.local'))
119
+ const envKeys = getEnvKeys(path.join(dir, '.env'))
120
+ const allEnvKeys = [...new Set([...envExampleKeys, ...envLocalKeys, ...envKeys])]
121
+ if (allEnvKeys.length) {
122
+ sections.push(`\n## Environment Variables (keys only)`)
123
+ allEnvKeys.forEach(k => sections.push(` ${k}`))
124
+ }
125
+
126
+ // --- Config files ---
127
+ for (const file of CONFIG_FILES) {
128
+ const filePath = path.join(dir, file)
129
+ if (fs.existsSync(filePath)) {
130
+ const content = readFileSafe(filePath)
131
+ if (content && content.length < 4000) {
132
+ sections.push(`\n## ${file}`)
133
+ sections.push('```')
134
+ sections.push(content.trim())
135
+ sections.push('```')
136
+ } else if (content) {
137
+ sections.push(`\n## ${file} (truncated)`)
138
+ sections.push('```')
139
+ sections.push(content.trim().slice(0, 4000) + '\n...')
140
+ sections.push('```')
141
+ }
142
+ }
143
+ }
144
+
145
+ // --- GitHub Actions workflows ---
146
+ const workflowDir = path.join(dir, DEPLOY_WORKFLOW_DIR)
147
+ if (fs.existsSync(workflowDir)) {
148
+ try {
149
+ const files = fs.readdirSync(workflowDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml'))
150
+ for (const file of files) {
151
+ const content = readFileSafe(path.join(workflowDir, file))
152
+ if (content) {
153
+ sections.push(`\n## .github/workflows/${file}`)
154
+ sections.push('```yaml')
155
+ sections.push(content.trim().slice(0, 3000))
156
+ sections.push('```')
157
+ }
158
+ }
159
+ } catch {}
160
+ }
161
+
162
+ // --- README ---
163
+ const readmePath = path.join(dir, 'README.md')
164
+ if (fs.existsSync(readmePath)) {
165
+ const content = readFileSafe(readmePath)
166
+ if (content) {
167
+ sections.push(`\n## README.md`)
168
+ sections.push(content.trim().slice(0, 3000))
169
+ }
170
+ }
171
+
172
+ // --- License ---
173
+ for (const f of ['LICENSE', 'LICENSE.md', 'LICENSE.txt']) {
174
+ const p = path.join(dir, f)
175
+ if (fs.existsSync(p)) {
176
+ const content = readFileSafe(p)
177
+ if (content) {
178
+ sections.push(`\n## ${f}`)
179
+ sections.push(content.trim().slice(0, 500))
180
+ }
181
+ break
182
+ }
183
+ }
184
+
185
+ // --- Folder structure ---
186
+ const structure = getFolderStructure(dir)
187
+ if (structure.length) {
188
+ sections.push(`\n## Project folder structure`)
189
+ sections.push(structure.join('\n'))
190
+ }
191
+
192
+ return sections.join('\n')
193
+ }
package/setup.js CHANGED
@@ -6,8 +6,9 @@ The audience is TWO types of readers:
6
6
  1. A NON-TECHNICAL CLIENT — who needs plain-English explanations of what everything is and why it matters
7
7
  2. A DEVELOPER — who needs exact commands, file paths, and technical detail
8
8
 
9
- Using the following project information provided by the developer:
10
- ${projectInfo || '[No project info provided — use placeholder examples]'}
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]'}
11
12
 
12
13
  Generate a SETUP section that includes:
13
14