@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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +368 -0
  3. package/assets/config.yml +35 -0
  4. package/assets/default.md +47 -0
  5. package/assets/instructions/README.md +46 -0
  6. package/assets/instructions/claude.md +83 -0
  7. package/assets/instructions/codex.md +19 -0
  8. package/index.js +106 -0
  9. package/package.json +90 -0
  10. package/src/commands/close/index.js +66 -0
  11. package/src/commands/close/index.test.js +235 -0
  12. package/src/commands/get-started/index.js +138 -0
  13. package/src/commands/get-started/index.test.js +246 -0
  14. package/src/commands/init/index.js +51 -0
  15. package/src/commands/init/index.test.js +159 -0
  16. package/src/commands/link/index.js +395 -0
  17. package/src/commands/link/index.test.js +28 -0
  18. package/src/commands/lint/index.js +657 -0
  19. package/src/commands/lint/index.test.js +569 -0
  20. package/src/commands/list/index.js +131 -0
  21. package/src/commands/list/index.test.js +153 -0
  22. package/src/commands/new/index.js +305 -0
  23. package/src/commands/new/index.test.js +256 -0
  24. package/src/commands/refine/index.js +741 -0
  25. package/src/commands/refine/index.test.js +28 -0
  26. package/src/commands/review/index.js +957 -0
  27. package/src/commands/review/index.test.js +193 -0
  28. package/src/commands/start/index.js +180 -0
  29. package/src/commands/start/index.test.js +88 -0
  30. package/src/commands/unlink/index.js +123 -0
  31. package/src/commands/unlink/index.test.js +22 -0
  32. package/src/utils/arrow-select.js +233 -0
  33. package/src/utils/cli.js +489 -0
  34. package/src/utils/cli.test.js +9 -0
  35. package/src/utils/git.js +146 -0
  36. package/src/utils/git.test.js +330 -0
  37. package/src/utils/index.js +193 -0
  38. package/src/utils/index.test.js +375 -0
  39. package/src/utils/prompts.js +47 -0
  40. package/src/utils/prompts.test.js +165 -0
  41. package/src/utils/test-helpers.js +492 -0
  42. package/src/utils/ticket.js +423 -0
  43. 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
+ });