@vibedx/vibekit 0.1.0 → 0.2.1
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/package.json +2 -1
- package/src/commands/link/index.js +109 -323
- package/src/commands/refine/index.js +115 -211
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibedx/vibekit",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A powerful CLI tool for managing development tickets and project workflows",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"test:watch": "NODE_OPTIONS='--experimental-vm-modules' jest --watch",
|
|
19
19
|
"test:coverage": "NODE_OPTIONS='--experimental-vm-modules' jest --coverage --detectOpenHandles",
|
|
20
20
|
"test:cleanup": "rm -rf __temp__",
|
|
21
|
+
"test:e2e": "node scripts/e2e.js",
|
|
21
22
|
"prepublishOnly": "echo 'Ready to publish'",
|
|
22
23
|
"version": "git add -A && git commit -m 'chore: version bump'",
|
|
23
24
|
"postversion": "git push && git push --tags"
|
|
@@ -1,96 +1,26 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
4
|
import yaml from 'js-yaml';
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
};
|
|
6
|
+
const CLAUDE_CODE_INSTALL_URL = 'https://docs.anthropic.com/en/docs/claude-code';
|
|
7
|
+
const CLAUDE_DETECT_TIMEOUT = 5000;
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
function createReadlineInterface() {
|
|
14
|
-
return createInterface({
|
|
15
|
-
input: process.stdin,
|
|
16
|
-
output: process.stdout
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Prompt user for input with question
|
|
22
|
-
*/
|
|
23
|
-
function askQuestion(rl, question) {
|
|
24
|
-
return new Promise((resolve) => {
|
|
25
|
-
rl.question(question, (answer) => {
|
|
26
|
-
resolve(answer.trim());
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Prompt user for password/API key (hidden input)
|
|
33
|
-
*/
|
|
34
|
-
function askSecretQuestion(rl, question) {
|
|
35
|
-
return new Promise((resolve) => {
|
|
36
|
-
process.stdout.write(question);
|
|
37
|
-
|
|
38
|
-
// Use readline's built-in password functionality instead of raw mode
|
|
39
|
-
const stdin = process.stdin;
|
|
40
|
-
const originalMode = stdin.isTTY ? stdin.setRawMode : null;
|
|
41
|
-
|
|
42
|
-
if (stdin.isTTY && originalMode) {
|
|
43
|
-
stdin.setRawMode(true);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
let input = '';
|
|
47
|
-
const onData = (buffer) => {
|
|
48
|
-
const char = buffer.toString('utf8');
|
|
49
|
-
const code = char.charCodeAt(0);
|
|
50
|
-
|
|
51
|
-
if (code === 13 || code === 10) { // Enter key (CR or LF)
|
|
52
|
-
if (stdin.isTTY && originalMode) {
|
|
53
|
-
stdin.setRawMode(false);
|
|
54
|
-
}
|
|
55
|
-
stdin.removeListener('data', onData);
|
|
56
|
-
console.log(); // New line
|
|
57
|
-
resolve(input);
|
|
58
|
-
} else if (code === 127 || code === 8) { // Backspace/Delete
|
|
59
|
-
if (input.length > 0) {
|
|
60
|
-
input = input.slice(0, -1);
|
|
61
|
-
process.stdout.write('\b \b');
|
|
62
|
-
}
|
|
63
|
-
} else if (code === 3) { // Ctrl+C
|
|
64
|
-
if (stdin.isTTY && originalMode) {
|
|
65
|
-
stdin.setRawMode(false);
|
|
66
|
-
}
|
|
67
|
-
stdin.removeListener('data', onData);
|
|
68
|
-
console.log('\n❌ Cancelled');
|
|
69
|
-
process.exit(0);
|
|
70
|
-
} else if (code >= 32 && code <= 126) { // Printable ASCII characters
|
|
71
|
-
input += char;
|
|
72
|
-
process.stdout.write('*');
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
stdin.on('data', onData);
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Load existing config.yml
|
|
10
|
+
* Load existing .vibe/config.yml
|
|
11
|
+
* @returns {Object} Parsed config object
|
|
82
12
|
*/
|
|
83
13
|
function loadConfig() {
|
|
84
14
|
const configPath = path.join(process.cwd(), '.vibe', 'config.yml');
|
|
85
|
-
|
|
15
|
+
|
|
86
16
|
if (!fs.existsSync(configPath)) {
|
|
87
17
|
console.error('❌ No .vibe/config.yml found. Run "vibe init" first.');
|
|
88
18
|
process.exit(1);
|
|
89
19
|
}
|
|
90
|
-
|
|
20
|
+
|
|
91
21
|
try {
|
|
92
|
-
const
|
|
93
|
-
return yaml.load(
|
|
22
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
23
|
+
return yaml.load(content);
|
|
94
24
|
} catch (error) {
|
|
95
25
|
console.error('❌ Error reading config.yml:', error.message);
|
|
96
26
|
process.exit(1);
|
|
@@ -98,17 +28,15 @@ function loadConfig() {
|
|
|
98
28
|
}
|
|
99
29
|
|
|
100
30
|
/**
|
|
101
|
-
* Save updated config.yml (
|
|
31
|
+
* Save updated config to .vibe/config.yml (no sensitive data stored)
|
|
32
|
+
* @param {Object} config - Config object to persist
|
|
33
|
+
* @returns {boolean} True on success
|
|
102
34
|
*/
|
|
103
35
|
function saveConfig(config) {
|
|
104
36
|
const configPath = path.join(process.cwd(), '.vibe', 'config.yml');
|
|
105
|
-
|
|
37
|
+
|
|
106
38
|
try {
|
|
107
|
-
const yamlContent = yaml.dump(config, {
|
|
108
|
-
indent: 2,
|
|
109
|
-
lineWidth: -1,
|
|
110
|
-
noRefs: true
|
|
111
|
-
});
|
|
39
|
+
const yamlContent = yaml.dump(config, { indent: 2, lineWidth: -1, noRefs: true });
|
|
112
40
|
fs.writeFileSync(configPath, yamlContent, 'utf8');
|
|
113
41
|
return true;
|
|
114
42
|
} catch (error) {
|
|
@@ -118,278 +46,136 @@ function saveConfig(config) {
|
|
|
118
46
|
}
|
|
119
47
|
|
|
120
48
|
/**
|
|
121
|
-
* Check if
|
|
49
|
+
* Check if the Claude Code CLI is installed and accessible
|
|
50
|
+
* @returns {Promise<{ installed: boolean, version: string | null }>}
|
|
122
51
|
*/
|
|
123
|
-
function
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (!fs.existsSync(envPath)) {
|
|
127
|
-
return { exists: false, hasKey: false };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
try {
|
|
131
|
-
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
132
|
-
const hasKey = envContent.includes('ANTHROPIC_API_KEY=');
|
|
133
|
-
return { exists: true, hasKey };
|
|
134
|
-
} catch (error) {
|
|
135
|
-
return { exists: false, hasKey: false };
|
|
136
|
-
}
|
|
137
|
-
}
|
|
52
|
+
function detectClaudeCode() {
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
let stdout = '';
|
|
138
55
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const apiKey = await askSecretQuestion(rl, '? Enter your Claude API key: ');
|
|
157
|
-
|
|
158
|
-
if (!apiKey) {
|
|
159
|
-
console.log('❌ API key is required.');
|
|
160
|
-
return false;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Validate API key first
|
|
164
|
-
console.log('🔍 Validating API key...');
|
|
165
|
-
const validation = validateClaudeApiKey(apiKey);
|
|
166
|
-
|
|
167
|
-
if (!validation.valid) {
|
|
168
|
-
console.log(`❌ ${validation.error}`);
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
let envContent = '';
|
|
174
|
-
|
|
175
|
-
if (envInfo.exists) {
|
|
176
|
-
// Read existing .env and update/add ANTHROPIC_API_KEY
|
|
177
|
-
envContent = fs.readFileSync(envPath, 'utf8');
|
|
178
|
-
|
|
179
|
-
if (envInfo.hasKey) {
|
|
180
|
-
// Replace existing key
|
|
181
|
-
envContent = envContent.replace(/ANTHROPIC_API_KEY=.*$/m, `ANTHROPIC_API_KEY=${apiKey}`);
|
|
56
|
+
const child = spawn('claude', ['--version'], {
|
|
57
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
child.stdout.on('data', (data) => {
|
|
61
|
+
stdout += data.toString();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const timer = setTimeout(() => {
|
|
65
|
+
child.kill('SIGTERM');
|
|
66
|
+
resolve({ installed: false, version: null });
|
|
67
|
+
}, CLAUDE_DETECT_TIMEOUT);
|
|
68
|
+
|
|
69
|
+
child.on('close', (code) => {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
if (code === 0) {
|
|
72
|
+
resolve({ installed: true, version: stdout.trim() || null });
|
|
182
73
|
} else {
|
|
183
|
-
|
|
184
|
-
envContent += envContent.endsWith('\n') ? '' : '\n';
|
|
185
|
-
envContent += `ANTHROPIC_API_KEY=${apiKey}\n`;
|
|
74
|
+
resolve({ installed: false, version: null });
|
|
186
75
|
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
fs.writeFileSync(envPath, envContent, 'utf8');
|
|
195
|
-
console.log('✅ API key saved to .env file');
|
|
196
|
-
console.log('🔒 Make sure .env is in your .gitignore');
|
|
197
|
-
|
|
198
|
-
// Update the current process environment
|
|
199
|
-
process.env.ANTHROPIC_API_KEY = apiKey;
|
|
200
|
-
|
|
201
|
-
return true;
|
|
202
|
-
} catch (error) {
|
|
203
|
-
console.error('❌ Error creating .env file:', error.message);
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
child.on('error', () => {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
resolve({ installed: false, version: null });
|
|
81
|
+
});
|
|
82
|
+
});
|
|
206
83
|
}
|
|
207
84
|
|
|
208
85
|
/**
|
|
209
|
-
* Create AI instructions
|
|
86
|
+
* Create AI workflow instructions from the assets template
|
|
87
|
+
* @returns {Promise<boolean>} True on success
|
|
210
88
|
*/
|
|
211
89
|
async function createAiInstructions() {
|
|
212
90
|
try {
|
|
213
|
-
// Create .context/instructions directory
|
|
214
91
|
const instructionsDir = path.join(process.cwd(), '.context', 'instructions');
|
|
92
|
+
|
|
215
93
|
if (!fs.existsSync(instructionsDir)) {
|
|
216
94
|
fs.mkdirSync(instructionsDir, { recursive: true });
|
|
217
95
|
}
|
|
218
|
-
|
|
219
|
-
// Copy claude instructions from assets template
|
|
96
|
+
|
|
220
97
|
const templatePath = path.join(process.cwd(), 'assets', 'instructions', 'claude.md');
|
|
221
|
-
const
|
|
222
|
-
|
|
98
|
+
const outputPath = path.join(instructionsDir, 'claude.md');
|
|
99
|
+
|
|
223
100
|
if (fs.existsSync(templatePath)) {
|
|
224
|
-
|
|
225
|
-
fs.writeFileSync(claudeInstructionsPath, templateContent, 'utf8');
|
|
226
|
-
console.log('📄 Created .context/instructions/claude.md from template');
|
|
101
|
+
fs.writeFileSync(outputPath, fs.readFileSync(templatePath, 'utf8'), 'utf8');
|
|
227
102
|
} else {
|
|
228
|
-
|
|
229
|
-
|
|
103
|
+
fs.writeFileSync(outputPath, buildFallbackInstructions(), 'utf8');
|
|
104
|
+
}
|
|
230
105
|
|
|
231
|
-
|
|
106
|
+
console.log('📄 Created .context/instructions/claude.md');
|
|
107
|
+
return true;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.warn('⚠️ Could not create AI instructions:', error.message);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Fallback instructions if the assets template is missing
|
|
116
|
+
* @returns {string} Markdown instruction content
|
|
117
|
+
*/
|
|
118
|
+
function buildFallbackInstructions() {
|
|
119
|
+
return `# VibeKit Instructions for Claude
|
|
120
|
+
|
|
121
|
+
This project uses VibeKit for organised, ticket-driven development.
|
|
232
122
|
|
|
233
123
|
## Primary Rule: Always Work Through Tickets
|
|
234
|
-
1. Create ticket first: \`vibe new\`
|
|
124
|
+
1. Create a ticket first: \`vibe new\`
|
|
235
125
|
2. Start working: \`vibe start <ticket-id>\`
|
|
236
126
|
3. Close when done: \`vibe close <ticket-id>\`
|
|
237
127
|
|
|
238
128
|
For detailed instructions, see the VibeKit documentation.
|
|
239
129
|
`;
|
|
240
|
-
fs.writeFileSync(claudeInstructionsPath, basicContent, 'utf8');
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Create a README for the .context/instructions folder
|
|
244
|
-
const readmePath = path.join(instructionsDir, 'README.md');
|
|
245
|
-
const readmeContent = `# AI Instructions
|
|
246
|
-
|
|
247
|
-
This folder contains instructions for different AI coding assistants.
|
|
248
|
-
|
|
249
|
-
## Files
|
|
250
|
-
- \`claude.md\` - Instructions for Claude Code (Anthropic)
|
|
251
|
-
- \`codex.md\` - Instructions for OpenAI Codex (coming soon)
|
|
252
|
-
|
|
253
|
-
## Important Notes
|
|
254
|
-
- These files are automatically read by AI assistants
|
|
255
|
-
- Only modify if you understand how AI instructions work
|
|
256
|
-
- Changes affect how AI assistants interact with this project
|
|
257
|
-
- Maintained by VibeKit for consistent development workflow
|
|
258
|
-
|
|
259
|
-
## Current Status
|
|
260
|
-
- ✅ Claude Code - Active and configured
|
|
261
|
-
- 🚧 OpenAI Codex - Coming soon
|
|
262
|
-
`;
|
|
263
|
-
|
|
264
|
-
fs.writeFileSync(readmePath, readmeContent, 'utf8');
|
|
265
|
-
console.log('📄 Created .context/instructions/README.md with folder documentation');
|
|
266
|
-
|
|
267
|
-
return true;
|
|
268
|
-
} catch (error) {
|
|
269
|
-
console.warn('⚠️ Could not create AI instructions:', error.message);
|
|
270
|
-
return false;
|
|
271
|
-
}
|
|
272
130
|
}
|
|
273
131
|
|
|
274
132
|
/**
|
|
275
|
-
*
|
|
133
|
+
* Print install instructions for Claude Code CLI
|
|
276
134
|
*/
|
|
277
|
-
function
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (!apiKey.startsWith('sk-ant-api')) {
|
|
283
|
-
return { valid: false, error: 'Invalid Claude API key format' };
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return { valid: true };
|
|
135
|
+
function printInstallInstructions() {
|
|
136
|
+
console.log('\n📦 Install Claude Code to continue:\n');
|
|
137
|
+
console.log(' npm install -g @anthropic-ai/claude-code');
|
|
138
|
+
console.log(`\n📖 Documentation: ${CLAUDE_CODE_INSTALL_URL}`);
|
|
139
|
+
console.log('\nOnce installed, run "vibe link" again.\n');
|
|
287
140
|
}
|
|
288
141
|
|
|
289
142
|
/**
|
|
290
|
-
* Main link command
|
|
143
|
+
* Main link command — detects Claude Code CLI and configures the project
|
|
291
144
|
*/
|
|
292
145
|
async function linkCommand() {
|
|
293
|
-
console.log('🔗 VibeKit
|
|
294
|
-
|
|
146
|
+
console.log('🔗 Linking VibeKit to Claude Code\n');
|
|
147
|
+
|
|
295
148
|
const config = loadConfig();
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
console.log('✅ Environment API key validated!');
|
|
319
|
-
console.log('🔗 Ready for ticket refinement!');
|
|
320
|
-
console.log(`\n📝 Configuration updated:`);
|
|
321
|
-
console.log(` Provider: ${SUPPORTED_PROVIDERS['claude-code']}`);
|
|
322
|
-
console.log(' Source: Environment variable (ANTHROPIC_API_KEY)');
|
|
323
|
-
console.log('\n💡 You can now use AI features like "vibe refine"');
|
|
324
|
-
|
|
325
|
-
// Create AI instructions documentation
|
|
326
|
-
await createAiInstructions();
|
|
327
|
-
} else {
|
|
328
|
-
console.log('❌ Failed to save configuration.');
|
|
329
|
-
}
|
|
330
|
-
rl.close();
|
|
331
|
-
return;
|
|
332
|
-
} else {
|
|
333
|
-
console.log(`❌ Environment API key validation failed: ${validation.error}`);
|
|
334
|
-
console.log('Continuing with setup...\n');
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// No environment variable found or user chose not to use it
|
|
340
|
-
console.log('\n🔑 API Key Setup Required');
|
|
341
|
-
console.log('To use Claude Code, you need to set up your API key.');
|
|
342
|
-
console.log('\nChoose your preferred method:');
|
|
343
|
-
console.log('1. Export environment variable (recommended)');
|
|
344
|
-
console.log('2. Create .env file');
|
|
345
|
-
console.log();
|
|
346
|
-
|
|
347
|
-
const methodChoice = await askQuestion(rl, '? Choose setup method (1-2): ');
|
|
348
|
-
|
|
349
|
-
if (methodChoice === '1') {
|
|
350
|
-
// Guide user to export environment variable
|
|
351
|
-
console.log('\n📋 To set up environment variable:');
|
|
352
|
-
console.log('\n🔹 For current session:');
|
|
353
|
-
console.log(' export ANTHROPIC_API_KEY="your-api-key-here"');
|
|
354
|
-
console.log('\n🔹 For permanent setup (add to ~/.bashrc or ~/.zshrc):');
|
|
355
|
-
console.log(' echo \'export ANTHROPIC_API_KEY="your-api-key-here"\' >> ~/.bashrc');
|
|
356
|
-
console.log('\n💡 After setting the environment variable, run "vibe link" again.');
|
|
357
|
-
rl.close();
|
|
358
|
-
return;
|
|
359
|
-
} else if (methodChoice === '2') {
|
|
360
|
-
// Create .env file
|
|
361
|
-
const envCreated = await createEnvFile(rl);
|
|
362
|
-
|
|
363
|
-
if (envCreated) {
|
|
364
|
-
// Update config
|
|
365
|
-
config.ai = {
|
|
366
|
-
...config.ai,
|
|
367
|
-
provider: 'claude-code',
|
|
368
|
-
enabled: true
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
if (saveConfig(config)) {
|
|
372
|
-
console.log('🔗 Ready for ticket refinement!');
|
|
373
|
-
console.log(`\n📝 Configuration updated:`);
|
|
374
|
-
console.log(` Provider: ${SUPPORTED_PROVIDERS['claude-code']}`);
|
|
375
|
-
console.log(' Source: .env file');
|
|
376
|
-
console.log('\n💡 You can now use AI features like "vibe refine"');
|
|
377
|
-
|
|
378
|
-
// Create AI instructions documentation
|
|
379
|
-
await createAiInstructions();
|
|
380
|
-
} else {
|
|
381
|
-
console.log('❌ Failed to save configuration.');
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
} else {
|
|
385
|
-
console.log('❌ Invalid choice. Please run the command again.');
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
} catch (error) {
|
|
389
|
-
console.error('❌ Error during setup:', error.message);
|
|
390
|
-
} finally {
|
|
391
|
-
rl.close();
|
|
149
|
+
|
|
150
|
+
console.log('🔍 Detecting Claude Code CLI...');
|
|
151
|
+
const { installed, version } = await detectClaudeCode();
|
|
152
|
+
|
|
153
|
+
if (!installed) {
|
|
154
|
+
console.error('❌ Claude Code CLI not found.');
|
|
155
|
+
printInstallInstructions();
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const versionLabel = version ? ` (${version})` : '';
|
|
160
|
+
console.log(`✅ Claude Code detected${versionLabel}`);
|
|
161
|
+
|
|
162
|
+
config.ai = {
|
|
163
|
+
...config.ai,
|
|
164
|
+
enabled: true,
|
|
165
|
+
provider: 'claude-code',
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
if (!saveConfig(config)) {
|
|
169
|
+
console.error('❌ Failed to save configuration.');
|
|
170
|
+
process.exit(1);
|
|
392
171
|
}
|
|
172
|
+
|
|
173
|
+
console.log('✅ Configuration updated');
|
|
174
|
+
|
|
175
|
+
await createAiInstructions();
|
|
176
|
+
|
|
177
|
+
console.log('\n🎉 Claude Code linked successfully!');
|
|
178
|
+
console.log(' You can now use AI features like "vibe refine"\n');
|
|
393
179
|
}
|
|
394
180
|
|
|
395
|
-
export default linkCommand;
|
|
181
|
+
export default linkCommand;
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
3
4
|
import yaml from 'js-yaml';
|
|
4
5
|
import { resolveTicketId, parseTicket, updateTicket } from '../../utils/ticket.js';
|
|
5
6
|
import { select, spinner, input, logger } from '../../utils/cli.js';
|
|
6
7
|
import { arrowSelect } from '../../utils/arrow-select.js';
|
|
7
8
|
|
|
8
|
-
// Configuration constants
|
|
9
|
-
const CLAUDE_SDK_TIMEOUT = 30000;
|
|
10
|
-
const ENHANCEMENT_MODEL = 'claude-3-5-sonnet-latest';
|
|
11
|
-
|
|
12
9
|
/**
|
|
13
10
|
* Load VibeKit configuration
|
|
14
11
|
* @returns {Object} Configuration object
|
|
@@ -36,92 +33,20 @@ function loadConfig() {
|
|
|
36
33
|
}
|
|
37
34
|
|
|
38
35
|
/**
|
|
39
|
-
* Check if
|
|
40
|
-
* @returns {
|
|
36
|
+
* Check if AI is configured in .vibe/config.yml
|
|
37
|
+
* @returns {Object} AI configuration status
|
|
41
38
|
*/
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const { spawn } = await import('child_process');
|
|
45
|
-
|
|
46
|
-
return new Promise((resolve) => {
|
|
47
|
-
const child = spawn('claude', ['--version'], {
|
|
48
|
-
stdio: 'pipe',
|
|
49
|
-
timeout: 5000
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
child.on('close', (code) => {
|
|
53
|
-
resolve(code === 0);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
child.on('error', () => {
|
|
57
|
-
resolve(false);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
// Timeout fallback
|
|
61
|
-
const timeout = setTimeout(() => {
|
|
62
|
-
try {
|
|
63
|
-
child.kill('SIGTERM');
|
|
64
|
-
} catch (killError) {
|
|
65
|
-
// Ignore kill errors
|
|
66
|
-
}
|
|
67
|
-
resolve(false);
|
|
68
|
-
}, 5000);
|
|
69
|
-
|
|
70
|
-
child.on('exit', () => {
|
|
71
|
-
clearTimeout(timeout);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
} catch (error) {
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
39
|
+
function checkAiConfiguration() {
|
|
40
|
+
const config = loadConfig();
|
|
78
41
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
*/
|
|
84
|
-
async function checkAiConfiguration() {
|
|
85
|
-
try {
|
|
86
|
-
const config = loadConfig();
|
|
87
|
-
|
|
88
|
-
// Check if AI is enabled in config
|
|
89
|
-
if (!config.ai || !config.ai.enabled || config.ai.provider === 'none') {
|
|
90
|
-
return {
|
|
91
|
-
configured: false,
|
|
92
|
-
needsSetup: false,
|
|
93
|
-
reason: 'AI is not enabled in configuration'
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Check for Claude Code SDK availability
|
|
98
|
-
const sdkAvailable = await checkClaudeCodeSDK();
|
|
99
|
-
if (sdkAvailable) {
|
|
100
|
-
return { configured: true };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// SDK not available - needs installation
|
|
104
|
-
return {
|
|
105
|
-
configured: false,
|
|
106
|
-
needsSetup: true,
|
|
107
|
-
reason: 'Claude Code SDK not found'
|
|
42
|
+
if (!config.ai?.enabled || config.ai?.provider === 'none') {
|
|
43
|
+
return {
|
|
44
|
+
configured: false,
|
|
45
|
+
reason: 'AI is not enabled. Run "vibe link" first.'
|
|
108
46
|
};
|
|
109
|
-
} catch (error) {
|
|
110
|
-
throw new Error(`Failed to check AI configuration: ${error.message}`);
|
|
111
47
|
}
|
|
112
|
-
}
|
|
113
48
|
|
|
114
|
-
|
|
115
|
-
* Show Claude Code SDK installation information
|
|
116
|
-
* @returns {void}
|
|
117
|
-
*/
|
|
118
|
-
function showClaudeCodeInstallation() {
|
|
119
|
-
logger.error('Claude Code SDK not found.');
|
|
120
|
-
logger.info('VibeKit refine requires Claude Code SDK to enhance tickets.');
|
|
121
|
-
console.log('\nTo install Claude Code SDK, run:');
|
|
122
|
-
console.log(' npm install -g @anthropic-ai/claude-code');
|
|
123
|
-
console.log('\nOr visit: https://docs.anthropic.com/en/docs/claude-code');
|
|
124
|
-
logger.tip('After installation, run this command again.');
|
|
49
|
+
return { configured: true };
|
|
125
50
|
}
|
|
126
51
|
|
|
127
52
|
|
|
@@ -225,125 +150,118 @@ Response must be valid JSON only.`;
|
|
|
225
150
|
}
|
|
226
151
|
|
|
227
152
|
/**
|
|
228
|
-
*
|
|
153
|
+
* Extract a JSON object from Claude's raw text response.
|
|
154
|
+
* Handles plain JSON, markdown code blocks, and mixed content.
|
|
155
|
+
* @param {string} text - Raw text from Claude
|
|
156
|
+
* @returns {string} Validated JSON string
|
|
157
|
+
* @throws {Error} If no valid JSON object can be found
|
|
158
|
+
*/
|
|
159
|
+
function extractJsonFromResponse(text) {
|
|
160
|
+
const cleaned = text.trim();
|
|
161
|
+
|
|
162
|
+
// 1. Direct parse — Claude responded with pure JSON
|
|
163
|
+
try {
|
|
164
|
+
JSON.parse(cleaned);
|
|
165
|
+
return cleaned;
|
|
166
|
+
} catch { /* fall through */ }
|
|
167
|
+
|
|
168
|
+
// 2. Markdown code block — ```json ... ``` or ``` ... ```
|
|
169
|
+
const codeBlockMatch = cleaned.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
170
|
+
if (codeBlockMatch) {
|
|
171
|
+
const inner = codeBlockMatch[1].trim();
|
|
172
|
+
try {
|
|
173
|
+
JSON.parse(inner);
|
|
174
|
+
return inner;
|
|
175
|
+
} catch { /* fall through */ }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 3. Loose extraction — grab first {...} block
|
|
179
|
+
const objMatch = cleaned.match(/\{[\s\S]*\}/);
|
|
180
|
+
if (objMatch) {
|
|
181
|
+
try {
|
|
182
|
+
JSON.parse(objMatch[0]);
|
|
183
|
+
return objMatch[0];
|
|
184
|
+
} catch { /* fall through */ }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
throw new Error('No valid JSON object found in Claude response');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Parse the JSON envelope returned by `claude --print --output-format json`.
|
|
192
|
+
* Throws if the response signals an error or contains no result text.
|
|
193
|
+
* @param {string} raw - Raw stdout from the claude subprocess
|
|
194
|
+
* @returns {string} Claude's text result
|
|
195
|
+
* @throws {Error} If the envelope signals an error or cannot be parsed
|
|
196
|
+
*/
|
|
197
|
+
function parseClaudeEnvelope(raw) {
|
|
198
|
+
let envelope;
|
|
199
|
+
try {
|
|
200
|
+
envelope = JSON.parse(raw.trim());
|
|
201
|
+
} catch {
|
|
202
|
+
throw new Error('Claude returned non-JSON output');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (envelope.is_error) {
|
|
206
|
+
throw new Error(envelope.result || 'Claude reported an error');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const text = envelope.result ?? '';
|
|
210
|
+
if (!text.trim()) {
|
|
211
|
+
throw new Error('Claude returned an empty result');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return text;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Send a prompt to Claude via `claude --print` and return the JSON response string.
|
|
219
|
+
* Uses the native Claude Code CLI — no API key management required.
|
|
229
220
|
* @param {string} prompt - The prompt to send to Claude
|
|
230
|
-
* @returns {Promise<string>} Claude's
|
|
231
|
-
* @throws {Error} If
|
|
221
|
+
* @returns {Promise<string>} Validated JSON string from Claude's result
|
|
222
|
+
* @throws {Error} If the subprocess fails or response contains no valid JSON
|
|
232
223
|
*/
|
|
233
224
|
async function executeClaudeCommand(prompt) {
|
|
234
225
|
if (typeof prompt !== 'string' || !prompt.trim()) {
|
|
235
226
|
throw new Error('Prompt must be a non-empty string');
|
|
236
227
|
}
|
|
237
|
-
|
|
238
|
-
const { writeFileSync, unlinkSync } = await import('fs');
|
|
239
|
-
const { exec } = await import('child_process');
|
|
240
|
-
const { tmpdir } = await import('os');
|
|
241
|
-
const { join } = await import('path');
|
|
242
|
-
|
|
228
|
+
|
|
243
229
|
return new Promise((resolve, reject) => {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
try {
|
|
267
|
-
// Write prompt to temporary file
|
|
268
|
-
writeFileSync(tempFile, prompt, 'utf8');
|
|
269
|
-
} catch (writeError) {
|
|
270
|
-
reject(new Error(`Failed to write prompt file: ${writeError.message}`));
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const command = `cat "${tempFile}" | claude --print --output-format json --model ${ENHANCEMENT_MODEL}`;
|
|
275
|
-
|
|
276
|
-
childProcess = exec(command, {
|
|
277
|
-
timeout: CLAUDE_SDK_TIMEOUT,
|
|
278
|
-
maxBuffer: 2 * 1024 * 1024, // 2MB buffer
|
|
279
|
-
killSignal: 'SIGTERM'
|
|
280
|
-
}, (error, stdout, stderr) => {
|
|
281
|
-
cleanup();
|
|
282
|
-
|
|
283
|
-
if (error) {
|
|
284
|
-
// Handle specific error types
|
|
285
|
-
if (error.code === 'ENOENT') {
|
|
286
|
-
reject(new Error('Claude Code SDK not found. Please install it first.'));
|
|
287
|
-
} else if (error.code === 'EACCES') {
|
|
288
|
-
reject(new Error('Permission denied accessing Claude Code SDK.'));
|
|
289
|
-
} else if (error.signal === 'SIGTERM') {
|
|
290
|
-
reject(new Error('Claude SDK operation timed out.'));
|
|
291
|
-
} else {
|
|
292
|
-
reject(new Error(`Claude Code SDK failed: ${error.message}`));
|
|
293
|
-
}
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Check for stderr output
|
|
298
|
-
if (stderr && stderr.trim()) {
|
|
299
|
-
console.warn(`⚠️ Claude SDK warning: ${stderr.trim()}`);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Validate stdout
|
|
303
|
-
if (!stdout || stdout.trim() === '') {
|
|
304
|
-
reject(new Error('Claude SDK returned empty response'));
|
|
230
|
+
// Create environment without API key — use native Claude Code authentication only
|
|
231
|
+
const env = { ...process.env };
|
|
232
|
+
delete env.ANTHROPIC_API_KEY;
|
|
233
|
+
|
|
234
|
+
const child = spawn('claude', ['--print', '--output-format', 'json'], {
|
|
235
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
236
|
+
env
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
let stdout = '';
|
|
240
|
+
let stderr = '';
|
|
241
|
+
|
|
242
|
+
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
243
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
244
|
+
|
|
245
|
+
child.on('error', (err) => {
|
|
246
|
+
reject(new Error(`Failed to spawn Claude: ${err.message}`));
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
child.on('close', (code) => {
|
|
250
|
+
if (!stdout.trim()) {
|
|
251
|
+
reject(new Error(stderr.trim() || `Claude exited with code ${code}`));
|
|
305
252
|
return;
|
|
306
253
|
}
|
|
307
|
-
|
|
254
|
+
|
|
308
255
|
try {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if (typeof result === 'string') {
|
|
314
|
-
resolve(result);
|
|
315
|
-
} else {
|
|
316
|
-
resolve(JSON.stringify(result));
|
|
317
|
-
}
|
|
318
|
-
} catch (parseError) {
|
|
319
|
-
// If JSON parsing fails, try to extract JSON from response
|
|
320
|
-
const jsonMatch = stdout.match(/{[\s\S]*}/);
|
|
321
|
-
if (jsonMatch) {
|
|
322
|
-
try {
|
|
323
|
-
JSON.parse(jsonMatch[0]); // Validate JSON
|
|
324
|
-
resolve(jsonMatch[0]);
|
|
325
|
-
} catch (secondParseError) {
|
|
326
|
-
reject(new Error(`Failed to parse Claude SDK response as JSON: ${secondParseError.message}`));
|
|
327
|
-
}
|
|
328
|
-
} else {
|
|
329
|
-
reject(new Error('Claude SDK response is not valid JSON'));
|
|
330
|
-
}
|
|
256
|
+
const text = parseClaudeEnvelope(stdout);
|
|
257
|
+
resolve(extractJsonFromResponse(text));
|
|
258
|
+
} catch (err) {
|
|
259
|
+
reject(err);
|
|
331
260
|
}
|
|
332
261
|
});
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
cleanup();
|
|
337
|
-
reject(new Error(`Failed to run Claude Code SDK: ${error.message}`));
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
// Set up timeout handler
|
|
341
|
-
setTimeout(() => {
|
|
342
|
-
if (childProcess && !childProcess.killed) {
|
|
343
|
-
cleanup();
|
|
344
|
-
reject(new Error('Claude SDK operation timed out'));
|
|
345
|
-
}
|
|
346
|
-
}, CLAUDE_SDK_TIMEOUT + 1000);
|
|
262
|
+
|
|
263
|
+
child.stdin.write(prompt, 'utf8');
|
|
264
|
+
child.stdin.end();
|
|
347
265
|
});
|
|
348
266
|
}
|
|
349
267
|
|
|
@@ -646,14 +564,10 @@ async function refineCommand(args, options = {}) {
|
|
|
646
564
|
logger.step(`Analyzing ticket ${ticketInput}...`);
|
|
647
565
|
|
|
648
566
|
// Check AI configuration
|
|
649
|
-
const aiStatus =
|
|
567
|
+
const aiStatus = checkAiConfiguration();
|
|
650
568
|
if (!aiStatus.configured) {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
} else {
|
|
654
|
-
logger.error('AI is not enabled in config. Run "vibe link" first.');
|
|
655
|
-
}
|
|
656
|
-
throw new Error(aiStatus.reason || 'AI configuration check failed');
|
|
569
|
+
logger.error(aiStatus.reason);
|
|
570
|
+
return;
|
|
657
571
|
}
|
|
658
572
|
|
|
659
573
|
// Resolve ticket ID
|
|
@@ -686,18 +600,8 @@ async function refineCommand(args, options = {}) {
|
|
|
686
600
|
|
|
687
601
|
} catch (error) {
|
|
688
602
|
loadingSpinner.fail('Enhancement failed');
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
logger.error('AI enhancement service is unavailable.');
|
|
692
|
-
logger.info('This could be due to:');
|
|
693
|
-
console.log(' • Claude Code SDK not installed or configured');
|
|
694
|
-
console.log(' • Network connectivity issues');
|
|
695
|
-
console.log(' • API rate limits or authentication problems');
|
|
696
|
-
logger.tip('You can still view and edit the ticket manually.');
|
|
697
|
-
} else {
|
|
698
|
-
logger.error('Failed to enhance ticket.');
|
|
699
|
-
logger.tip('Please check your Claude Code SDK installation and try again.');
|
|
700
|
-
}
|
|
603
|
+
logger.error(error.message);
|
|
604
|
+
logger.tip('You can still view and edit the ticket manually.');
|
|
701
605
|
return;
|
|
702
606
|
}
|
|
703
607
|
|