@vibedx/vibekit 0.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/LICENSE +21 -0
- package/README.md +368 -0
- package/assets/config.yml +35 -0
- package/assets/default.md +47 -0
- package/assets/instructions/README.md +46 -0
- package/assets/instructions/claude.md +83 -0
- package/assets/instructions/codex.md +19 -0
- package/index.js +106 -0
- package/package.json +90 -0
- package/src/commands/close/index.js +66 -0
- package/src/commands/close/index.test.js +235 -0
- package/src/commands/get-started/index.js +138 -0
- package/src/commands/get-started/index.test.js +246 -0
- package/src/commands/init/index.js +51 -0
- package/src/commands/init/index.test.js +159 -0
- package/src/commands/link/index.js +395 -0
- package/src/commands/link/index.test.js +28 -0
- package/src/commands/lint/index.js +657 -0
- package/src/commands/lint/index.test.js +569 -0
- package/src/commands/list/index.js +131 -0
- package/src/commands/list/index.test.js +153 -0
- package/src/commands/new/index.js +305 -0
- package/src/commands/new/index.test.js +256 -0
- package/src/commands/refine/index.js +741 -0
- package/src/commands/refine/index.test.js +28 -0
- package/src/commands/review/index.js +957 -0
- package/src/commands/review/index.test.js +193 -0
- package/src/commands/start/index.js +180 -0
- package/src/commands/start/index.test.js +88 -0
- package/src/commands/unlink/index.js +123 -0
- package/src/commands/unlink/index.test.js +22 -0
- package/src/utils/arrow-select.js +233 -0
- package/src/utils/cli.js +489 -0
- package/src/utils/cli.test.js +9 -0
- package/src/utils/git.js +146 -0
- package/src/utils/git.test.js +330 -0
- package/src/utils/index.js +193 -0
- package/src/utils/index.test.js +375 -0
- package/src/utils/prompts.js +47 -0
- package/src/utils/prompts.test.js +165 -0
- package/src/utils/test-helpers.js +492 -0
- package/src/utils/ticket.js +423 -0
- package/src/utils/ticket.test.js +190 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { createInterface } from 'readline';
|
|
4
|
+
import yaml from 'js-yaml';
|
|
5
|
+
|
|
6
|
+
const SUPPORTED_PROVIDERS = {
|
|
7
|
+
'claude-code': 'Claude Code (Anthropic)'
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create readline interface for user input
|
|
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
|
|
82
|
+
*/
|
|
83
|
+
function loadConfig() {
|
|
84
|
+
const configPath = path.join(process.cwd(), '.vibe', 'config.yml');
|
|
85
|
+
|
|
86
|
+
if (!fs.existsSync(configPath)) {
|
|
87
|
+
console.error('ā No .vibe/config.yml found. Run "vibe init" first.');
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
93
|
+
return yaml.load(configContent);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error('ā Error reading config.yml:', error.message);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Save updated config.yml (without sensitive data)
|
|
102
|
+
*/
|
|
103
|
+
function saveConfig(config) {
|
|
104
|
+
const configPath = path.join(process.cwd(), '.vibe', 'config.yml');
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const yamlContent = yaml.dump(config, {
|
|
108
|
+
indent: 2,
|
|
109
|
+
lineWidth: -1,
|
|
110
|
+
noRefs: true
|
|
111
|
+
});
|
|
112
|
+
fs.writeFileSync(configPath, yamlContent, 'utf8');
|
|
113
|
+
return true;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error('ā Error saving config.yml:', error.message);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if .env file exists and contains ANTHROPIC_API_KEY
|
|
122
|
+
*/
|
|
123
|
+
function checkEnvFile() {
|
|
124
|
+
const envPath = path.join(process.cwd(), '.env');
|
|
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
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Create or update .env file with API key
|
|
141
|
+
*/
|
|
142
|
+
async function createEnvFile(rl) {
|
|
143
|
+
const envPath = path.join(process.cwd(), '.env');
|
|
144
|
+
const envInfo = checkEnvFile();
|
|
145
|
+
|
|
146
|
+
console.log('\nš Setting up .env file for secure API key storage');
|
|
147
|
+
|
|
148
|
+
if (envInfo.exists && envInfo.hasKey) {
|
|
149
|
+
console.log('ā ļø .env file already contains ANTHROPIC_API_KEY');
|
|
150
|
+
const overwrite = await askQuestion(rl, '? Update the existing API key? (y/n): ');
|
|
151
|
+
if (overwrite.toLowerCase() !== 'y' && overwrite.toLowerCase() !== 'yes') {
|
|
152
|
+
return false;
|
|
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}`);
|
|
182
|
+
} else {
|
|
183
|
+
// Add new key
|
|
184
|
+
envContent += envContent.endsWith('\n') ? '' : '\n';
|
|
185
|
+
envContent += `ANTHROPIC_API_KEY=${apiKey}\n`;
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
// Create new .env file
|
|
189
|
+
envContent = `# Environment variables for VibeKit
|
|
190
|
+
ANTHROPIC_API_KEY=${apiKey}
|
|
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
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Create AI instructions for Claude Code from template
|
|
210
|
+
*/
|
|
211
|
+
async function createAiInstructions() {
|
|
212
|
+
try {
|
|
213
|
+
// Create .context/instructions directory
|
|
214
|
+
const instructionsDir = path.join(process.cwd(), '.context', 'instructions');
|
|
215
|
+
if (!fs.existsSync(instructionsDir)) {
|
|
216
|
+
fs.mkdirSync(instructionsDir, { recursive: true });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Copy claude instructions from assets template
|
|
220
|
+
const templatePath = path.join(process.cwd(), 'assets', 'instructions', 'claude.md');
|
|
221
|
+
const claudeInstructionsPath = path.join(instructionsDir, 'claude.md');
|
|
222
|
+
|
|
223
|
+
if (fs.existsSync(templatePath)) {
|
|
224
|
+
const templateContent = fs.readFileSync(templatePath, 'utf8');
|
|
225
|
+
fs.writeFileSync(claudeInstructionsPath, templateContent, 'utf8');
|
|
226
|
+
console.log('š Created .context/instructions/claude.md from template');
|
|
227
|
+
} else {
|
|
228
|
+
console.warn('ā ļø Template not found, creating basic instructions');
|
|
229
|
+
const basicContent = `# VibeKit Instructions for Claude
|
|
230
|
+
|
|
231
|
+
This project uses VibeKit for organized development.
|
|
232
|
+
|
|
233
|
+
## Primary Rule: Always Work Through Tickets
|
|
234
|
+
1. Create ticket first: \`vibe new\`
|
|
235
|
+
2. Start working: \`vibe start <ticket-id>\`
|
|
236
|
+
3. Close when done: \`vibe close <ticket-id>\`
|
|
237
|
+
|
|
238
|
+
For detailed instructions, see the VibeKit documentation.
|
|
239
|
+
`;
|
|
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
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Validate Claude API key format
|
|
276
|
+
*/
|
|
277
|
+
function validateClaudeApiKey(apiKey) {
|
|
278
|
+
if (!apiKey || typeof apiKey !== 'string') {
|
|
279
|
+
return { valid: false, error: 'API key is required' };
|
|
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 };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Main link command implementation
|
|
291
|
+
*/
|
|
292
|
+
async function linkCommand() {
|
|
293
|
+
console.log('š VibeKit AI Provider Setup\n');
|
|
294
|
+
|
|
295
|
+
const config = loadConfig();
|
|
296
|
+
const rl = createReadlineInterface();
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
// Check for environment variable first
|
|
300
|
+
const envApiKey = process.env.ANTHROPIC_API_KEY;
|
|
301
|
+
if (envApiKey) {
|
|
302
|
+
console.log('š Found ANTHROPIC_API_KEY in environment variables');
|
|
303
|
+
const useEnvKey = await askQuestion(rl, '? Use this API key? (y/n): ');
|
|
304
|
+
|
|
305
|
+
if (useEnvKey.toLowerCase() === 'y' || useEnvKey.toLowerCase() === 'yes') {
|
|
306
|
+
console.log('š Validating API key...');
|
|
307
|
+
const validation = validateClaudeApiKey(envApiKey);
|
|
308
|
+
|
|
309
|
+
if (validation.valid) {
|
|
310
|
+
// Update config (never store API key in config)
|
|
311
|
+
config.ai = {
|
|
312
|
+
...config.ai,
|
|
313
|
+
provider: 'claude-code',
|
|
314
|
+
enabled: true
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
if (saveConfig(config)) {
|
|
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();
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export default linkCommand;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import linkCommand from './index.js';
|
|
3
|
+
|
|
4
|
+
describe('link command', () => {
|
|
5
|
+
describe('basic validation', () => {
|
|
6
|
+
it('should validate that link command exists and is callable', () => {
|
|
7
|
+
// This test validates the command structure without executing interactive parts
|
|
8
|
+
expect(typeof linkCommand).toBe('function');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should be an async function', () => {
|
|
12
|
+
// Validates the command is properly structured as async
|
|
13
|
+
expect(linkCommand.constructor.name).toBe('AsyncFunction');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should have correct function signature', () => {
|
|
17
|
+
// Validates the command signature
|
|
18
|
+
expect(linkCommand.length).toBe(0); // Async function with no required parameters
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Note: The link command is interactive and async, requiring:
|
|
23
|
+
// - Mocking readline.createInterface()
|
|
24
|
+
// - Mocking user input responses
|
|
25
|
+
// - Testing actual config file modifications
|
|
26
|
+
// - API key handling and external service validation
|
|
27
|
+
// Full testing would require complex async/interactive mocking
|
|
28
|
+
});
|