@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,9 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+
3
+ describe('cli utilities', () => {
4
+ it('should be a placeholder test file', () => {
5
+ // This is a placeholder test to prevent Jest from failing
6
+ // due to empty test suite
7
+ expect(true).toBe(true);
8
+ });
9
+ });
@@ -0,0 +1,146 @@
1
+ import { execSync } from 'child_process';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+
5
+ /**
6
+ * Check if the current directory is within a git repository
7
+ * @returns {boolean} True if in a git repository, false otherwise
8
+ */
9
+ function isGitRepository() {
10
+ try {
11
+ execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
12
+ return true;
13
+ } catch (error) {
14
+ return false;
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Get the current branch name
20
+ * @returns {string|null} The current branch name or null if not in a git repository
21
+ */
22
+ function getCurrentBranch() {
23
+ try {
24
+ return execSync('git branch --show-current', { encoding: 'utf-8' }).trim();
25
+ } catch (error) {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Check if a branch exists locally
32
+ * @param {string} branchName The branch name to check
33
+ * @returns {boolean} True if the branch exists locally, false otherwise
34
+ */
35
+ function branchExistsLocally(branchName) {
36
+ try {
37
+ const result = execSync(`git show-ref --verify --quiet refs/heads/${branchName}`);
38
+ return true;
39
+ } catch (error) {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Check if a branch exists remotely
46
+ * @param {string} branchName The branch name to check
47
+ * @returns {boolean} True if the branch exists remotely, false otherwise
48
+ */
49
+ function branchExistsRemotely(branchName) {
50
+ try {
51
+ const result = execSync(`git ls-remote --heads origin ${branchName}`, { encoding: 'utf-8' });
52
+ return result.trim() !== '';
53
+ } catch (error) {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Get the default base branch (usually main or master)
60
+ * @returns {string} The default base branch name
61
+ */
62
+ function getDefaultBaseBranch() {
63
+ try {
64
+ // Try to determine if main or master is the default branch
65
+ const branches = execSync('git branch -r', { encoding: 'utf-8' }).trim().split('\n');
66
+
67
+ // Check if origin/main exists
68
+ if (branches.some(branch => branch.trim() === 'origin/main')) {
69
+ return 'main';
70
+ }
71
+
72
+ // Check if origin/master exists
73
+ if (branches.some(branch => branch.trim() === 'origin/master')) {
74
+ return 'master';
75
+ }
76
+
77
+ // Default to main if we can't determine
78
+ return 'main';
79
+ } catch (error) {
80
+ return 'main'; // Default to main if there's an error
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Create and checkout a new branch
86
+ * @param {string} branchName The name of the branch to create
87
+ * @param {string} baseBranch The base branch to create from (default: main)
88
+ * @returns {boolean} True if successful, false otherwise
89
+ */
90
+ function createAndCheckoutBranch(branchName, baseBranch = null) {
91
+ try {
92
+ const base = baseBranch || getDefaultBaseBranch();
93
+
94
+ // Make sure we have the latest from the base branch
95
+ try {
96
+ execSync(`git fetch origin ${base}`, { stdio: 'ignore' });
97
+ } catch (error) {
98
+ // Ignore fetch errors, we'll try to create the branch anyway
99
+ }
100
+
101
+ // Create and checkout the new branch
102
+ execSync(`git checkout -b ${branchName} origin/${base} || git checkout -b ${branchName} ${base}`, { stdio: 'pipe' });
103
+ return true;
104
+ } catch (error) {
105
+ console.error(`❌ Failed to create branch: ${error.message}`);
106
+ return false;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Checkout an existing branch
112
+ * @param {string} branchName The name of the branch to checkout
113
+ * @returns {boolean} True if successful, false otherwise
114
+ */
115
+ function checkoutBranch(branchName) {
116
+ try {
117
+ execSync(`git checkout ${branchName}`, { stdio: 'pipe' });
118
+ return true;
119
+ } catch (error) {
120
+ console.error(`❌ Failed to checkout branch: ${error.message}`);
121
+ return false;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Get the status of the working directory
127
+ * @returns {string} The git status output
128
+ */
129
+ function getGitStatus() {
130
+ try {
131
+ return execSync('git status --porcelain', { encoding: 'utf-8' }).trim();
132
+ } catch (error) {
133
+ return '';
134
+ }
135
+ }
136
+
137
+ export {
138
+ isGitRepository,
139
+ getCurrentBranch,
140
+ branchExistsLocally,
141
+ branchExistsRemotely,
142
+ getDefaultBaseBranch,
143
+ createAndCheckoutBranch,
144
+ checkoutBranch,
145
+ getGitStatus
146
+ };
@@ -0,0 +1,330 @@
1
+ import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
2
+ import {
3
+ createTempDir,
4
+ cleanupTempDir,
5
+ mockConsole,
6
+ createMockGitRepo
7
+ } from './test-helpers.js';
8
+
9
+ // Mock child_process
10
+ const mockExecSync = jest.fn();
11
+ jest.unstable_mockModule('child_process', () => ({
12
+ execSync: mockExecSync
13
+ }));
14
+
15
+ describe('git utilities', () => {
16
+ let tempDir;
17
+ let consoleMock;
18
+ let gitModule;
19
+
20
+ beforeEach(async () => {
21
+ tempDir = createTempDir('git-test');
22
+ consoleMock = mockConsole();
23
+ mockExecSync.mockReset();
24
+
25
+ // Import after mocking
26
+ gitModule = await import('./git.js');
27
+ });
28
+
29
+ afterEach(() => {
30
+ consoleMock.restore();
31
+ cleanupTempDir(tempDir);
32
+ jest.clearAllMocks();
33
+ });
34
+
35
+ describe('isGitRepository', () => {
36
+ it('should return true when in git repository', () => {
37
+ // Arrange
38
+ mockExecSync.mockReturnValue('true');
39
+
40
+ // Act
41
+ const result = gitModule.isGitRepository();
42
+
43
+ // Assert
44
+ expect(result).toBe(true);
45
+ expect(mockExecSync).toHaveBeenCalledWith('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
46
+ });
47
+
48
+ it('should return false when not in git repository', () => {
49
+ // Arrange
50
+ mockExecSync.mockImplementation(() => {
51
+ throw new Error('Not a git repository');
52
+ });
53
+
54
+ // Act
55
+ const result = gitModule.isGitRepository();
56
+
57
+ // Assert
58
+ expect(result).toBe(false);
59
+ });
60
+ });
61
+
62
+ describe('getCurrentBranch', () => {
63
+ it('should return current branch name', () => {
64
+ // Arrange
65
+ mockExecSync.mockReturnValue('feature/test-branch\n');
66
+
67
+ // Act
68
+ const result = gitModule.getCurrentBranch();
69
+
70
+ // Assert
71
+ expect(result).toBe('feature/test-branch');
72
+ expect(mockExecSync).toHaveBeenCalledWith('git branch --show-current', { encoding: 'utf-8' });
73
+ });
74
+
75
+ it('should return null when git command fails', () => {
76
+ // Arrange
77
+ mockExecSync.mockImplementation(() => {
78
+ throw new Error('Not a git repository');
79
+ });
80
+
81
+ // Act
82
+ const result = gitModule.getCurrentBranch();
83
+
84
+ // Assert
85
+ expect(result).toBe(null);
86
+ });
87
+
88
+ it('should handle empty branch name', () => {
89
+ // Arrange
90
+ mockExecSync.mockReturnValue('');
91
+
92
+ // Act
93
+ const result = gitModule.getCurrentBranch();
94
+
95
+ // Assert
96
+ expect(result).toBe('');
97
+ });
98
+ });
99
+
100
+ describe('branchExistsLocally', () => {
101
+ it('should return true when branch exists locally', () => {
102
+ // Arrange
103
+ mockExecSync.mockReturnValue('');
104
+
105
+ // Act
106
+ const result = gitModule.branchExistsLocally('feature/test');
107
+
108
+ // Assert
109
+ expect(result).toBe(true);
110
+ expect(mockExecSync).toHaveBeenCalledWith('git show-ref --verify --quiet refs/heads/feature/test');
111
+ });
112
+
113
+ it('should return false when branch does not exist locally', () => {
114
+ // Arrange
115
+ mockExecSync.mockImplementation(() => {
116
+ throw new Error('Branch not found');
117
+ });
118
+
119
+ // Act
120
+ const result = gitModule.branchExistsLocally('nonexistent');
121
+
122
+ // Assert
123
+ expect(result).toBe(false);
124
+ });
125
+ });
126
+
127
+ describe('branchExistsRemotely', () => {
128
+ it('should return true when branch exists remotely', () => {
129
+ // Arrange
130
+ mockExecSync.mockReturnValue('abc123\trefs/heads/feature/test\n');
131
+
132
+ // Act
133
+ const result = gitModule.branchExistsRemotely('feature/test');
134
+
135
+ // Assert
136
+ expect(result).toBe(true);
137
+ expect(mockExecSync).toHaveBeenCalledWith('git ls-remote --heads origin feature/test', { encoding: 'utf-8' });
138
+ });
139
+
140
+ it('should return false when branch does not exist remotely', () => {
141
+ // Arrange
142
+ mockExecSync.mockReturnValue('');
143
+
144
+ // Act
145
+ const result = gitModule.branchExistsRemotely('nonexistent');
146
+
147
+ // Assert
148
+ expect(result).toBe(false);
149
+ });
150
+
151
+ it('should return false when git command fails', () => {
152
+ // Arrange
153
+ mockExecSync.mockImplementation(() => {
154
+ throw new Error('Network error');
155
+ });
156
+
157
+ // Act
158
+ const result = gitModule.branchExistsRemotely('test');
159
+
160
+ // Assert
161
+ expect(result).toBe(false);
162
+ });
163
+ });
164
+
165
+ describe('getDefaultBaseBranch', () => {
166
+ it('should return main when origin/main exists', () => {
167
+ // Arrange
168
+ mockExecSync.mockReturnValue(' origin/main\n origin/develop\n');
169
+
170
+ // Act
171
+ const result = gitModule.getDefaultBaseBranch();
172
+
173
+ // Assert
174
+ expect(result).toBe('main');
175
+ });
176
+
177
+ it('should return master when origin/master exists but not main', () => {
178
+ // Arrange
179
+ mockExecSync.mockReturnValue(' origin/master\n origin/develop\n');
180
+
181
+ // Act
182
+ const result = gitModule.getDefaultBaseBranch();
183
+
184
+ // Assert
185
+ expect(result).toBe('master');
186
+ });
187
+
188
+ it('should default to main when neither exists', () => {
189
+ // Arrange
190
+ mockExecSync.mockReturnValue(' origin/develop\n origin/feature\n');
191
+
192
+ // Act
193
+ const result = gitModule.getDefaultBaseBranch();
194
+
195
+ // Assert
196
+ expect(result).toBe('main');
197
+ });
198
+
199
+ it('should handle git command failure', () => {
200
+ // Arrange
201
+ mockExecSync.mockImplementation(() => {
202
+ throw new Error('Git error');
203
+ });
204
+
205
+ // Act
206
+ const result = gitModule.getDefaultBaseBranch();
207
+
208
+ // Assert
209
+ expect(result).toBe('main');
210
+ });
211
+ });
212
+
213
+ describe('createAndCheckoutBranch', () => {
214
+ it('should create and checkout new branch successfully', () => {
215
+ // Arrange
216
+ mockExecSync.mockImplementation((command) => {
217
+ if (command.includes('fetch')) return '';
218
+ if (command.includes('checkout -b')) return '';
219
+ return '';
220
+ });
221
+
222
+ // Act
223
+ const result = gitModule.createAndCheckoutBranch('feature/new-branch');
224
+
225
+ // Assert
226
+ expect(result).toBe(true);
227
+ expect(mockExecSync).toHaveBeenCalledWith(
228
+ expect.stringContaining('checkout -b feature/new-branch'),
229
+ { stdio: 'pipe' }
230
+ );
231
+ });
232
+
233
+ it('should use custom base branch when provided', () => {
234
+ // Arrange
235
+ mockExecSync.mockImplementation(() => '');
236
+
237
+ // Act
238
+ const result = gitModule.createAndCheckoutBranch('feature/test', 'develop');
239
+
240
+ // Assert
241
+ expect(result).toBe(true);
242
+ expect(mockExecSync).toHaveBeenCalledWith(
243
+ expect.stringContaining('develop'),
244
+ expect.any(Object)
245
+ );
246
+ });
247
+
248
+ it('should return false when branch creation fails', () => {
249
+ // Arrange
250
+ mockExecSync.mockImplementation((command) => {
251
+ if (command.includes('checkout -b')) {
252
+ throw new Error('Branch creation failed');
253
+ }
254
+ return '';
255
+ });
256
+
257
+ // Act
258
+ const result = gitModule.createAndCheckoutBranch('invalid/branch');
259
+
260
+ // Assert
261
+ expect(result).toBe(false);
262
+ });
263
+ });
264
+
265
+ describe('checkoutBranch', () => {
266
+ it('should checkout existing branch successfully', () => {
267
+ // Arrange
268
+ mockExecSync.mockReturnValue('');
269
+
270
+ // Act
271
+ const result = gitModule.checkoutBranch('existing-branch');
272
+
273
+ // Assert
274
+ expect(result).toBe(true);
275
+ expect(mockExecSync).toHaveBeenCalledWith('git checkout existing-branch', { stdio: 'pipe' });
276
+ });
277
+
278
+ it('should return false when checkout fails', () => {
279
+ // Arrange
280
+ mockExecSync.mockImplementation(() => {
281
+ throw new Error('Branch not found');
282
+ });
283
+
284
+ // Act
285
+ const result = gitModule.checkoutBranch('nonexistent');
286
+
287
+ // Assert
288
+ expect(result).toBe(false);
289
+ });
290
+ });
291
+
292
+ describe('getGitStatus', () => {
293
+ it('should return git status output', () => {
294
+ // Arrange
295
+ const statusOutput = 'M file1.js\nA file2.js\n';
296
+ mockExecSync.mockReturnValue(statusOutput);
297
+
298
+ // Act
299
+ const result = gitModule.getGitStatus();
300
+
301
+ // Assert
302
+ expect(result).toBe('M file1.js\nA file2.js');
303
+ expect(mockExecSync).toHaveBeenCalledWith('git status --porcelain', { encoding: 'utf-8' });
304
+ });
305
+
306
+ it('should return empty string when no changes', () => {
307
+ // Arrange
308
+ mockExecSync.mockReturnValue('');
309
+
310
+ // Act
311
+ const result = gitModule.getGitStatus();
312
+
313
+ // Assert
314
+ expect(result).toBe('');
315
+ });
316
+
317
+ it('should return empty string when git command fails', () => {
318
+ // Arrange
319
+ mockExecSync.mockImplementation(() => {
320
+ throw new Error('Not a git repository');
321
+ });
322
+
323
+ // Act
324
+ const result = gitModule.getGitStatus();
325
+
326
+ // Assert
327
+ expect(result).toBe('');
328
+ });
329
+ });
330
+ });
@@ -0,0 +1,193 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import yaml from 'js-yaml';
4
+
5
+ /**
6
+ * Get the path to the tickets directory from config or use default
7
+ * Reads the configuration file to determine the tickets directory path.
8
+ * Falls back to default path if config is missing or invalid.
9
+ * @returns {string} Absolute path to the tickets directory
10
+ * @throws {Error} Logs error but doesn't throw - returns default path
11
+ */
12
+ function getTicketsDir() {
13
+ const configPath = path.join(process.cwd(), '.vibe', 'config.yml');
14
+ let ticketDir = path.join(process.cwd(), '.vibe', 'tickets');
15
+
16
+ try {
17
+ if (fs.existsSync(configPath)) {
18
+ const configContent = fs.readFileSync(configPath, 'utf-8');
19
+ const config = yaml.load(configContent) || {};
20
+
21
+ if (config.tickets?.path && typeof config.tickets.path === 'string') {
22
+ const customPath = path.resolve(process.cwd(), config.tickets.path);
23
+ ticketDir = customPath;
24
+ }
25
+ }
26
+ } catch (error) {
27
+ console.error(`❌ Error reading config: ${error.message}`);
28
+ }
29
+
30
+ return ticketDir;
31
+ }
32
+
33
+ /**
34
+ * Get the configuration from .vibe/config.yml
35
+ * Loads and parses the YAML configuration file for VibeKit.
36
+ * Returns empty object if file doesn't exist or cannot be parsed.
37
+ * @returns {Object} The configuration object (empty if file missing/invalid)
38
+ * @example
39
+ * const config = getConfig();
40
+ * console.log(config.tickets?.path); // Access tickets path
41
+ */
42
+ function getConfig() {
43
+ const configPath = path.join(process.cwd(), '.vibe', 'config.yml');
44
+ let config = {};
45
+
46
+ try {
47
+ if (fs.existsSync(configPath)) {
48
+ const configContent = fs.readFileSync(configPath, 'utf-8');
49
+ const parsedConfig = yaml.load(configContent);
50
+
51
+ // Ensure we have a valid object
52
+ if (parsedConfig && typeof parsedConfig === 'object') {
53
+ config = parsedConfig;
54
+ }
55
+ }
56
+ } catch (error) {
57
+ console.error(`❌ Error reading config: ${error.message}`);
58
+ }
59
+
60
+ return config;
61
+ }
62
+
63
+ /**
64
+ * Generate the next ticket ID based on existing tickets
65
+ * Scans the tickets directory for existing ticket files and generates
66
+ * the next sequential ID in the format TKT-XXX (zero-padded to 3 digits).
67
+ * @returns {string} The next ticket ID (e.g., "TKT-004")
68
+ * @example
69
+ * const nextId = getNextTicketId();
70
+ * console.log(nextId); // "TKT-005"
71
+ */
72
+ function getNextTicketId() {
73
+ const ticketDir = getTicketsDir();
74
+
75
+ // If tickets directory doesn't exist, start with 001
76
+ if (!fs.existsSync(ticketDir)) {
77
+ return 'TKT-001';
78
+ }
79
+
80
+ try {
81
+ const files = fs.readdirSync(ticketDir);
82
+ const ticketNumbers = files
83
+ .filter(f => f.endsWith('.md')) // Only consider markdown files
84
+ .map(f => f.match(/^TKT-(\d+)/))
85
+ .filter(Boolean)
86
+ .map(match => parseInt(match[1], 10))
87
+ .filter(num => !isNaN(num) && num > 0); // Filter out invalid numbers
88
+
89
+ // Find the highest existing number and add 1
90
+ const nextId = Math.max(0, ...ticketNumbers) + 1;
91
+ const paddedId = String(nextId).padStart(3, '0');
92
+
93
+ return `TKT-${paddedId}`;
94
+ } catch (error) {
95
+ console.error(`❌ Error scanning tickets directory: ${error.message}`);
96
+ return 'TKT-001'; // Fallback to first ticket
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Create a slug from a title based on configuration
102
+ * Converts a human-readable title into a URL-friendly slug following
103
+ * configuration rules for maximum length and word limits.
104
+ * @param {string} title - The title to slugify
105
+ * @param {Object|null} config - Optional configuration object (defaults to getConfig())
106
+ * @returns {string} The slugified title (kebab-case, lowercase)
107
+ * @example
108
+ * createSlug('Fix User Authentication Bug'); // 'fix-user-authentication-bug'
109
+ * createSlug('Very Long Title That Exceeds Limits', { tickets: { slug: { max_length: 10 } } }); // 'very-long'
110
+ */
111
+ function createSlug(title, config = null) {
112
+ if (!title || typeof title !== 'string') {
113
+ return '';
114
+ }
115
+
116
+ // Get configuration with fallback defaults
117
+ const cfg = config || getConfig();
118
+ const maxLength = cfg.tickets?.slug?.max_length || 30;
119
+ const wordLimit = cfg.tickets?.slug?.word_limit || 5;
120
+
121
+ // Validate configuration values
122
+ const safeMaxLength = Math.max(1, Math.min(100, maxLength)); // Clamp between 1-100
123
+ const safeWordLimit = Math.max(1, Math.min(20, wordLimit)); // Clamp between 1-20
124
+
125
+ // Split into words and limit the number of words
126
+ const words = title.trim().split(/\s+/).filter(Boolean);
127
+ const limitedWords = words.slice(0, safeWordLimit).join(' ');
128
+
129
+ // Create the basic slug
130
+ let slug = limitedWords.toLowerCase()
131
+ .replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric chars with hyphens
132
+ .replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
133
+
134
+ // Limit the length and ensure we don't cut in the middle of a word
135
+ if (slug.length > safeMaxLength) {
136
+ slug = slug.substring(0, safeMaxLength);
137
+ // Remove trailing partial words (anything after the last hyphen)
138
+ const lastHyphen = slug.lastIndexOf('-');
139
+ if (lastHyphen > 0) {
140
+ slug = slug.substring(0, lastHyphen);
141
+ }
142
+ }
143
+
144
+ return slug || 'untitled'; // Fallback if slug is empty
145
+ }
146
+
147
+ /**
148
+ * Create a full slug with ticket ID prefix
149
+ * Combines a ticket ID with a descriptive slug to create a complete
150
+ * filename-ready identifier.
151
+ * @param {string} ticketId - The ticket ID (e.g., "TKT-009")
152
+ * @param {string} slugText - The descriptive slug text part
153
+ * @returns {string} Full slug with ticket ID prefix (e.g., "TKT-009-fix-auth-bug")
154
+ * @example
155
+ * createFullSlug('TKT-001', 'user-login-fix'); // 'TKT-001-user-login-fix'
156
+ */
157
+ function createFullSlug(ticketId, slugText) {
158
+ // Validate inputs
159
+ if (!ticketId || typeof ticketId !== 'string') {
160
+ return '';
161
+ }
162
+
163
+ if (!slugText || typeof slugText !== 'string') {
164
+ return ticketId; // Return just the ticket ID if no slug text
165
+ }
166
+
167
+ // Clean inputs
168
+ const cleanTicketId = ticketId.trim();
169
+ const cleanSlugText = slugText.trim();
170
+
171
+ if (!cleanTicketId || !cleanSlugText) {
172
+ return cleanTicketId || '';
173
+ }
174
+
175
+ return `${cleanTicketId}-${cleanSlugText}`;
176
+ }
177
+
178
+ /**
179
+ * Get the path to the config.yml file
180
+ * @returns {string} Absolute path to the config.yml file
181
+ */
182
+ function getConfigPath() {
183
+ return path.join(process.cwd(), '.vibe', 'config.yml');
184
+ }
185
+
186
+ export {
187
+ getTicketsDir,
188
+ getConfig,
189
+ getConfigPath,
190
+ getNextTicketId,
191
+ createSlug,
192
+ createFullSlug
193
+ };