@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,193 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import {
5
+ createTempDir,
6
+ cleanupTempDir,
7
+ mockConsole,
8
+ mockProcessCwd,
9
+ mockProcessExit,
10
+ createMockVibeProject
11
+ } from '../../utils/test-helpers.js';
12
+ import reviewCommand from './index.js';
13
+
14
+ /** Creates a minimal valid ticket file in the tickets directory */
15
+ function createMockTicket(ticketsDir, id = 'TKT-001', status = 'in_progress') {
16
+ const content = `---
17
+ id: ${id}
18
+ title: Test Ticket
19
+ slug: ${id.toLowerCase()}-test-ticket
20
+ status: ${status}
21
+ priority: medium
22
+ created_at: 2026-01-01T00:00:00.000Z
23
+ updated_at: 2026-01-01T00:00:00.000Z
24
+ ---
25
+
26
+ ## Description
27
+
28
+ Test ticket description.
29
+
30
+ ## Acceptance Criteria
31
+
32
+ - [ ] Criterion one
33
+
34
+ ## Implementation Notes
35
+
36
+ Some notes.
37
+
38
+ ## Testing & Test Cases
39
+
40
+ Some tests.
41
+ `;
42
+ fs.writeFileSync(path.join(ticketsDir, `${id}-test-ticket.md`), content, 'utf-8');
43
+ }
44
+
45
+ /** Runs a review command and swallows the process.exit() throw */
46
+ async function runReview(args) {
47
+ try {
48
+ await reviewCommand(args);
49
+ } catch (err) {
50
+ if (!err.message.startsWith('process.exit(')) throw err;
51
+ }
52
+ }
53
+
54
+ describe('review command', () => {
55
+ let tempDir;
56
+ let consoleMock;
57
+ let restoreCwd;
58
+ let exitMock;
59
+ let ticketsDir;
60
+
61
+ beforeEach(() => {
62
+ tempDir = createTempDir('review-test');
63
+ consoleMock = mockConsole();
64
+ restoreCwd = mockProcessCwd(tempDir);
65
+ exitMock = mockProcessExit();
66
+
67
+ const { ticketsDir: td } = createMockVibeProject(tempDir);
68
+ ticketsDir = td;
69
+ createMockTicket(ticketsDir);
70
+ });
71
+
72
+ afterEach(() => {
73
+ consoleMock.restore();
74
+ restoreCwd();
75
+ exitMock.restore();
76
+ cleanupTempDir(tempDir);
77
+ });
78
+
79
+ describe('help', () => {
80
+ it('should display help with --help flag', async () => {
81
+ await reviewCommand(['--help']);
82
+
83
+ expect(consoleMock.logs.log.some(m => m.includes('vibe review'))).toBe(true);
84
+ expect(consoleMock.logs.log.some(m => m.includes('Usage:'))).toBe(true);
85
+ });
86
+
87
+ it('should display help with -h flag', async () => {
88
+ await reviewCommand(['-h']);
89
+
90
+ expect(consoleMock.logs.log.some(m => m.includes('vibe review'))).toBe(true);
91
+ });
92
+ });
93
+
94
+ describe('clean subcommand', () => {
95
+ it('should report no cache files when cache is empty', async () => {
96
+ await reviewCommand(['clean']);
97
+
98
+ expect(consoleMock.logs.log.some(m => m.includes('No review cache files to clean'))).toBe(true);
99
+ });
100
+
101
+ it('should report no cache files for a specific ticket when base cache dir is missing', async () => {
102
+ await reviewCommand(['clean', 'TKT-001']);
103
+
104
+ expect(consoleMock.logs.log.some(m => m.includes('No review cache files to clean'))).toBe(true);
105
+ });
106
+
107
+ it('should report no cache files for specific ticket when base cache exists but ticket dir missing', async () => {
108
+ // Create the base cache dir but not the TKT-001 subdirectory
109
+ fs.mkdirSync(path.join(tempDir, '.vibe/.cache/review/logs'), { recursive: true });
110
+
111
+ await reviewCommand(['clean', 'TKT-001']);
112
+
113
+ expect(consoleMock.logs.log.some(m => m.includes('No cache files found for ticket TKT-001'))).toBe(true);
114
+ });
115
+
116
+ it('should clean existing cache files for a specific ticket', async () => {
117
+ const cacheDir = path.join(tempDir, '.vibe/.cache/review/logs/TKT-001');
118
+ fs.mkdirSync(cacheDir, { recursive: true });
119
+ fs.writeFileSync(path.join(cacheDir, 'review-2026-01-01T00-00-00.txt'), 'test content');
120
+
121
+ await reviewCommand(['clean', 'TKT-001']);
122
+
123
+ expect(consoleMock.logs.log.some(m => m.includes('Cleaned'))).toBe(true);
124
+ expect(fs.existsSync(cacheDir)).toBe(false);
125
+ });
126
+
127
+ it('should normalise a numeric ticket ID for clean', async () => {
128
+ fs.mkdirSync(path.join(tempDir, '.vibe/.cache/review/logs'), { recursive: true });
129
+
130
+ await reviewCommand(['clean', '1']);
131
+
132
+ expect(consoleMock.logs.log.some(m => m.includes('No cache files found for ticket TKT-001'))).toBe(true);
133
+ });
134
+ });
135
+
136
+ describe('ticket ID validation', () => {
137
+ it('should reject an invalid ticket ID format', async () => {
138
+ await runReview(['invalid-id']);
139
+
140
+ expect(consoleMock.logs.error.some(m => m.includes('Invalid ticket ID format'))).toBe(true);
141
+ expect(exitMock.exitCalls).toContain(1);
142
+ });
143
+
144
+ it('should reject a ticket that does not exist', async () => {
145
+ await runReview(['TKT-999']);
146
+
147
+ expect(consoleMock.logs.error.some(m => m.includes('Ticket not found: TKT-999'))).toBe(true);
148
+ expect(exitMock.exitCalls).toContain(1);
149
+ });
150
+
151
+ it('should pass validation for an existing TKT-001', async () => {
152
+ await runReview(['TKT-001']);
153
+
154
+ const errors = consoleMock.logs.error.join(' ');
155
+ expect(errors).not.toContain('Invalid ticket ID format');
156
+ expect(errors).not.toContain('Ticket not found');
157
+ });
158
+
159
+ it('should accept a numeric shorthand and normalise it', async () => {
160
+ await runReview(['1']);
161
+
162
+ const errors = consoleMock.logs.error.join(' ');
163
+ expect(errors).not.toContain('Invalid ticket ID format');
164
+ expect(errors).not.toContain('Ticket not found');
165
+ });
166
+ });
167
+
168
+ describe('ticket ID normalisation logic', () => {
169
+ const normalize = (input) => {
170
+ if (!input || typeof input !== 'string') return null;
171
+ const s = input.trim().toUpperCase();
172
+ if (!s || s.length > 20) return null;
173
+ if (/^\d+$/.test(s)) {
174
+ const n = parseInt(s, 10);
175
+ if (n < 1 || n > 999) return null;
176
+ return `TKT-${s.padStart(3, '0')}`;
177
+ }
178
+ if (/^TKT-\d{3}$/.test(s)) {
179
+ const n = parseInt(s.substring(4), 10);
180
+ return n >= 1 && n <= 999 ? s : null;
181
+ }
182
+ return null;
183
+ };
184
+
185
+ it('normalises single digit to TKT-001', () => expect(normalize('1')).toBe('TKT-001'));
186
+ it('normalises two digits to TKT-011', () => expect(normalize('11')).toBe('TKT-011'));
187
+ it('normalises three digits to TKT-111', () => expect(normalize('111')).toBe('TKT-111'));
188
+ it('accepts already-valid TKT-XXX', () => expect(normalize('TKT-001')).toBe('TKT-001'));
189
+ it('rejects invalid strings', () => expect(normalize('invalid')).toBeNull());
190
+ it('rejects null input', () => expect(normalize(null)).toBeNull());
191
+ it('rejects out-of-range zero', () => expect(normalize('0')).toBeNull());
192
+ });
193
+ });
@@ -0,0 +1,180 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import yaml from 'js-yaml';
4
+ import { getTicketsDir, getConfig, createSlug } from '../../utils/index.js';
5
+ import {
6
+ isGitRepository,
7
+ getCurrentBranch,
8
+ branchExistsLocally,
9
+ branchExistsRemotely,
10
+ createAndCheckoutBranch,
11
+ checkoutBranch,
12
+ getGitStatus
13
+ } from '../../utils/git.js';
14
+
15
+ /**
16
+ * Start working on a ticket by checking out its branch
17
+ * @param {string[]} args Command arguments
18
+ */
19
+ function startCommand(args) {
20
+ // Check if we're in a git repository
21
+ if (!isGitRepository()) {
22
+ console.error('āŒ Not in a git repository. Please run this command from within a git repository.');
23
+ process.exit(1);
24
+ }
25
+
26
+ // Parse arguments
27
+ if (args.length === 0) {
28
+ console.error('āŒ Please provide a ticket ID (e.g., vibe start TKT-006)');
29
+ process.exit(1);
30
+ }
31
+
32
+ // Extract ticket ID and options
33
+ let ticketId = args[0];
34
+ let baseBranch = null;
35
+ let updateStatus = true;
36
+
37
+ // Process additional arguments
38
+ for (let i = 1; i < args.length; i++) {
39
+ if (args[i] === '--base' && i + 1 < args.length) {
40
+ baseBranch = args[i + 1];
41
+ i++; // Skip the next argument as it's the base branch
42
+ } else if (args[i] === '--update-status' || args[i] === '-u') {
43
+ updateStatus = true;
44
+ }
45
+ }
46
+
47
+ // Normalize ticket ID format (add TKT- prefix if not present)
48
+ if (!ticketId.startsWith('TKT-')) {
49
+ // Check if it's just a number
50
+ if (/^\d+$/.test(ticketId)) {
51
+ ticketId = `TKT-${ticketId.padStart(3, '0')}`;
52
+ } else {
53
+ console.error('āŒ Invalid ticket ID format. Expected TKT-XXX or just the number.');
54
+ process.exit(1);
55
+ }
56
+ }
57
+
58
+ // Get configuration
59
+ const config = getConfig();
60
+ const ticketsDir = getTicketsDir();
61
+
62
+ // Check if the ticket exists
63
+ const ticketFiles = fs.readdirSync(ticketsDir).filter(file => file.startsWith(`${ticketId}-`));
64
+
65
+ if (ticketFiles.length === 0) {
66
+ console.error(`āŒ Ticket ${ticketId} not found.`);
67
+ process.exit(1);
68
+ }
69
+
70
+ const ticketFile = ticketFiles[0];
71
+ const ticketPath = path.join(ticketsDir, ticketFile);
72
+
73
+ // Read the ticket content to get the title and slug
74
+ const ticketContent = fs.readFileSync(ticketPath, 'utf-8');
75
+ const titleMatch = ticketContent.match(/title: (.+)/);
76
+ const title = titleMatch ? titleMatch[1].trim() : '';
77
+
78
+ // Look for the slug in the frontmatter
79
+ const slugMatch = ticketContent.match(/slug: (.+)/);
80
+ let slug;
81
+
82
+ if (slugMatch && slugMatch[1].trim()) {
83
+ // Use the slug from the ticket file
84
+ slug = slugMatch[1].trim();
85
+ } else {
86
+ // Generate a slug from the title as fallback
87
+ slug = `${ticketId}-${createSlug(title)}`;
88
+ console.log(`āš ļø No slug found in ticket. Generated slug: ${slug}`);
89
+
90
+ // Update the ticket with the generated slug
91
+ try {
92
+ let updatedContent = ticketContent;
93
+ if (updatedContent.includes('slug:')) {
94
+ updatedContent = updatedContent.replace(/slug:.*/, `slug: ${slug}`);
95
+ } else {
96
+ updatedContent = updatedContent.replace(/---/, `---\nslug: ${slug}`);
97
+ }
98
+
99
+ fs.writeFileSync(ticketPath, updatedContent, 'utf-8');
100
+ console.log(`āœ… Updated ticket with slug: ${slug}`);
101
+ } catch (error) {
102
+ console.error(`āŒ Failed to update ticket with slug: ${error.message}`);
103
+ }
104
+ }
105
+
106
+ // Get branch prefix from config or use default (empty)
107
+ const branchPrefix = config.git?.branch_prefix || '';
108
+
109
+ // Create branch name - if slug already contains the ticket ID, don't add it again
110
+ const branchName = slug.includes(ticketId)
111
+ ? `${branchPrefix}${slug}`
112
+ : `${branchPrefix}${ticketId}-${slug}`;
113
+
114
+ // Check if there are uncommitted changes
115
+ const gitStatus = getGitStatus();
116
+ if (gitStatus) {
117
+ console.warn('āš ļø You have uncommitted changes. Stash or commit them before switching branches.');
118
+ console.log('');
119
+ }
120
+
121
+ // Check if the branch already exists
122
+ const branchExistsLocal = branchExistsLocally(branchName);
123
+ const branchExistsRemote = branchExistsRemotely(branchName);
124
+
125
+ if (branchExistsLocal || branchExistsRemote) {
126
+ console.log(`šŸ” Branch ${branchName} already exists.`);
127
+
128
+ // Checkout the existing branch
129
+ if (checkoutBranch(branchName)) {
130
+ console.log(`āœ… Switched to branch: ${branchName}`);
131
+ } else {
132
+ console.error(`āŒ Failed to switch to branch: ${branchName}`);
133
+ process.exit(1);
134
+ }
135
+ } else {
136
+ console.log(`šŸ” Creating new branch: ${branchName}`);
137
+
138
+ // Create and checkout the new branch
139
+ if (createAndCheckoutBranch(branchName, baseBranch)) {
140
+ console.log(`āœ… Created and switched to branch: ${branchName}`);
141
+ } else {
142
+ console.error(`āŒ Failed to create branch: ${branchName}`);
143
+ process.exit(1);
144
+ }
145
+ }
146
+
147
+ // Update ticket status if requested
148
+ if (updateStatus) {
149
+ try {
150
+ // Read the ticket file
151
+ const ticketContent = fs.readFileSync(ticketPath, 'utf-8');
152
+
153
+ // Get current timestamp in ISO format
154
+ const now = new Date().toISOString();
155
+
156
+ // Update the status to in_progress and update the timestamp
157
+ let updatedContent = ticketContent
158
+ .replace(/^status: (.+)$/m, 'status: in_progress')
159
+ .replace(/^updated_at: (.+)$/m, `updated_at: ${now}`);
160
+
161
+ // Write the updated content back to the file
162
+ fs.writeFileSync(ticketPath, updatedContent, 'utf-8');
163
+
164
+ console.log(`āœ… Updated ticket status to: in_progress`);
165
+ console.log(`āœ… Updated timestamp to: ${now}`);
166
+ } catch (error) {
167
+ console.error(`āŒ Failed to update ticket status: ${error.message}`);
168
+ }
169
+ }
170
+
171
+ // Summary
172
+ console.log('');
173
+ console.log(`šŸŽÆ Now working on: ${ticketId} - ${title}`);
174
+ console.log(`🌿 Branch: ${branchName}`);
175
+ console.log('');
176
+ console.log('To push this branch to remote:');
177
+ console.log(` git push -u origin ${branchName}`);
178
+ }
179
+
180
+ export default startCommand;
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import {
5
+ createTempDir,
6
+ cleanupTempDir,
7
+ mockConsole,
8
+ mockProcessCwd,
9
+ mockProcessExit,
10
+ createMockVibeProject
11
+ } from '../../utils/test-helpers.js';
12
+ import startCommand from './index.js';
13
+
14
+ describe('start command', () => {
15
+ let tempDir;
16
+ let consoleMock;
17
+ let restoreCwd;
18
+ let exitMock;
19
+
20
+ beforeEach(() => {
21
+ tempDir = createTempDir('start-test');
22
+ consoleMock = mockConsole();
23
+ restoreCwd = mockProcessCwd(tempDir);
24
+ exitMock = mockProcessExit();
25
+ });
26
+
27
+ afterEach(() => {
28
+ consoleMock.restore();
29
+ restoreCwd();
30
+ exitMock.restore();
31
+ cleanupTempDir(tempDir);
32
+ });
33
+
34
+ describe('basic validation', () => {
35
+ it('should validate that start command exists and is callable', () => {
36
+ expect(typeof startCommand).toBe('function');
37
+ });
38
+
39
+ it('should accept arguments parameter', () => {
40
+ expect(startCommand.length).toBe(1); // Takes one parameter (args array)
41
+ });
42
+
43
+ it('should show error when no ticket ID provided', () => {
44
+ // Arrange
45
+ createMockVibeProject(tempDir);
46
+
47
+ // Act
48
+ expect(() => startCommand([])).toThrow('process.exit(1)');
49
+
50
+ // Assert
51
+ expect(exitMock.exitCalls).toContain(1);
52
+ expect(consoleMock.logs.error).toContain('āŒ Please provide a ticket ID (e.g., vibe start TKT-006)');
53
+ });
54
+
55
+ it('should show error for invalid ticket ID format', () => {
56
+ // Arrange
57
+ createMockVibeProject(tempDir);
58
+
59
+ // Act
60
+ expect(() => startCommand(['invalid-format'])).toThrow('process.exit(1)');
61
+
62
+ // Assert
63
+ expect(exitMock.exitCalls).toContain(1);
64
+ expect(consoleMock.logs.error).toContain('āŒ Invalid ticket ID format. Expected TKT-XXX or just the number.');
65
+ });
66
+ });
67
+
68
+ describe('ticket validation', () => {
69
+ it('should show error when ticket does not exist', () => {
70
+ // Arrange
71
+ createMockVibeProject(tempDir); // No tickets
72
+
73
+ // Act
74
+ expect(() => startCommand(['TKT-001'])).toThrow('process.exit(1)');
75
+
76
+ // Assert
77
+ expect(exitMock.exitCalls).toContain(1);
78
+ expect(consoleMock.logs.error).toContain('āŒ Ticket TKT-001 not found.');
79
+ });
80
+ });
81
+
82
+ // Note: Full git integration testing would require:
83
+ // - Proper git repository setup
84
+ // - Mocking all git utility functions
85
+ // - Testing branch creation and checkout
86
+ // - Testing ticket status updates
87
+ // This is better handled in integration tests with proper git mocking
88
+ });
@@ -0,0 +1,123 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { createInterface } from 'readline';
4
+ import yaml from 'js-yaml';
5
+
6
+ /**
7
+ * Create readline interface for user input
8
+ */
9
+ function createReadlineInterface() {
10
+ return createInterface({
11
+ input: process.stdin,
12
+ output: process.stdout
13
+ });
14
+ }
15
+
16
+ /**
17
+ * Prompt user for input with question
18
+ */
19
+ function askQuestion(rl, question) {
20
+ return new Promise((resolve) => {
21
+ rl.question(question, (answer) => {
22
+ resolve(answer.trim());
23
+ });
24
+ });
25
+ }
26
+
27
+ /**
28
+ * Load existing config.yml
29
+ */
30
+ function loadConfig() {
31
+ const configPath = path.join(process.cwd(), '.vibe', 'config.yml');
32
+
33
+ if (!fs.existsSync(configPath)) {
34
+ console.error('āŒ No .vibe/config.yml found. Run "vibe init" first.');
35
+ process.exit(1);
36
+ }
37
+
38
+ try {
39
+ const configContent = fs.readFileSync(configPath, 'utf8');
40
+ return yaml.load(configContent);
41
+ } catch (error) {
42
+ console.error('āŒ Error reading config.yml:', error.message);
43
+ process.exit(1);
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Save updated config.yml
49
+ */
50
+ function saveConfig(config) {
51
+ const configPath = path.join(process.cwd(), '.vibe', 'config.yml');
52
+
53
+ try {
54
+ const yamlContent = yaml.dump(config, {
55
+ indent: 2,
56
+ lineWidth: -1,
57
+ noRefs: true
58
+ });
59
+ fs.writeFileSync(configPath, yamlContent, 'utf8');
60
+ return true;
61
+ } catch (error) {
62
+ console.error('āŒ Error saving config.yml:', error.message);
63
+ return false;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Main unlink command implementation
69
+ */
70
+ async function unlinkCommand() {
71
+ console.log('šŸ”“ VibeKit AI Provider Removal\n');
72
+
73
+ const config = loadConfig();
74
+ const rl = createReadlineInterface();
75
+
76
+ try {
77
+ // Check if AI is currently configured
78
+ if (!config.ai || !config.ai.enabled) {
79
+ console.log('ā„¹ļø No AI provider is currently configured.');
80
+ rl.close();
81
+ return;
82
+ }
83
+
84
+ // Show current configuration
85
+ console.log('šŸ“ Current AI configuration:');
86
+ console.log(` Provider: ${config.ai.provider === 'claude-code' ? 'Claude Code (Anthropic API)' : config.ai.provider}`);
87
+ console.log(` Model: ${config.ai.model || 'Not specified'}`);
88
+ console.log(` Status: ${config.ai.enabled ? 'Enabled' : 'Disabled'}`);
89
+ console.log();
90
+
91
+ // Confirm removal
92
+ const confirmRemoval = await askQuestion(rl, '? Are you sure you want to disable AI features? (y/n): ');
93
+
94
+ if (confirmRemoval.toLowerCase() !== 'y' && confirmRemoval.toLowerCase() !== 'yes') {
95
+ console.log('🚫 Cancelled. AI configuration unchanged.');
96
+ rl.close();
97
+ return;
98
+ }
99
+
100
+ // Simply disable AI in config (no credential storage to remove)
101
+ config.ai = {
102
+ ...config.ai,
103
+ enabled: false,
104
+ provider: 'none'
105
+ };
106
+
107
+ if (saveConfig(config)) {
108
+ console.log('āœ… AI features disabled successfully!');
109
+ console.log('šŸ”“ AI provider has been unlinked.');
110
+ console.log('\nšŸ’” Your API keys in environment variables or .env files remain unchanged.');
111
+ console.log('šŸ’” Run "vibe link" anytime to re-enable AI features.');
112
+ } else {
113
+ console.log('āŒ Failed to save configuration changes.');
114
+ }
115
+
116
+ } catch (error) {
117
+ console.error('āŒ Error during removal:', error.message);
118
+ } finally {
119
+ rl.close();
120
+ }
121
+ }
122
+
123
+ export default unlinkCommand;
@@ -0,0 +1,22 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import unlinkCommand from './index.js';
3
+
4
+ describe('unlink command', () => {
5
+ describe('basic validation', () => {
6
+ it('should validate that unlink command exists and is callable', () => {
7
+ // This test validates the command structure without executing interactive parts
8
+ expect(typeof unlinkCommand).toBe('function');
9
+ });
10
+
11
+ it('should be an async function', () => {
12
+ // Validates the command is properly structured as async
13
+ expect(unlinkCommand.constructor.name).toBe('AsyncFunction');
14
+ });
15
+ });
16
+
17
+ // Note: The unlink command is interactive and async, requiring:
18
+ // - Mocking readline.createInterface()
19
+ // - Mocking user input responses
20
+ // - Testing actual config file modifications
21
+ // Full testing would require complex async/interactive mocking
22
+ });