@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,153 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
|
2
|
+
import {
|
|
3
|
+
createTempDir,
|
|
4
|
+
cleanupTempDir,
|
|
5
|
+
mockConsole,
|
|
6
|
+
mockProcessCwd,
|
|
7
|
+
mockProcessExit,
|
|
8
|
+
createMockVibeProject
|
|
9
|
+
} from '../../utils/test-helpers.js';
|
|
10
|
+
import listCommand from './index.js';
|
|
11
|
+
|
|
12
|
+
describe('list command', () => {
|
|
13
|
+
let tempDir;
|
|
14
|
+
let consoleMock;
|
|
15
|
+
let restoreCwd;
|
|
16
|
+
let exitMock;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
tempDir = createTempDir('list-test');
|
|
20
|
+
consoleMock = mockConsole();
|
|
21
|
+
restoreCwd = mockProcessCwd(tempDir);
|
|
22
|
+
exitMock = mockProcessExit();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
consoleMock.restore();
|
|
27
|
+
restoreCwd();
|
|
28
|
+
exitMock.restore();
|
|
29
|
+
cleanupTempDir(tempDir);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('basic listing', () => {
|
|
33
|
+
it('should show message when no tickets exist', () => {
|
|
34
|
+
// Arrange
|
|
35
|
+
createMockVibeProject(tempDir);
|
|
36
|
+
|
|
37
|
+
// Act
|
|
38
|
+
expect(() => listCommand([])).toThrow('process.exit(0)');
|
|
39
|
+
|
|
40
|
+
// Assert
|
|
41
|
+
expect(exitMock.exitCalls).toContain(0);
|
|
42
|
+
expect(consoleMock.logs.log.some(log =>
|
|
43
|
+
log.includes('No tickets found')
|
|
44
|
+
)).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should list all tickets with their details', () => {
|
|
48
|
+
// Arrange
|
|
49
|
+
createMockVibeProject(tempDir, {
|
|
50
|
+
withTickets: [
|
|
51
|
+
{
|
|
52
|
+
id: 'TKT-001',
|
|
53
|
+
title: 'First ticket',
|
|
54
|
+
status: 'open',
|
|
55
|
+
priority: 'high',
|
|
56
|
+
slug: 'first-ticket'
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'TKT-002',
|
|
60
|
+
title: 'Second ticket',
|
|
61
|
+
status: 'in_progress',
|
|
62
|
+
priority: 'medium',
|
|
63
|
+
slug: 'second-ticket'
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Act
|
|
69
|
+
listCommand([]);
|
|
70
|
+
|
|
71
|
+
// Assert
|
|
72
|
+
const output = consoleMock.logs.log.join(' ');
|
|
73
|
+
expect(output).toContain('TKT-001');
|
|
74
|
+
expect(output).toContain('First ticket');
|
|
75
|
+
expect(output).toContain('TKT-002');
|
|
76
|
+
expect(output).toContain('Second ticket');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should show ticket status and priority', () => {
|
|
80
|
+
// Arrange
|
|
81
|
+
createMockVibeProject(tempDir, {
|
|
82
|
+
withTickets: [
|
|
83
|
+
{
|
|
84
|
+
id: 'TKT-001',
|
|
85
|
+
title: 'Test ticket',
|
|
86
|
+
status: 'done',
|
|
87
|
+
priority: 'urgent',
|
|
88
|
+
slug: 'test-ticket'
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Act
|
|
94
|
+
listCommand([]);
|
|
95
|
+
|
|
96
|
+
// Assert
|
|
97
|
+
const output = consoleMock.logs.log.join(' ');
|
|
98
|
+
expect(output).toContain('done');
|
|
99
|
+
expect(output).toContain('TKT-001');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('error handling', () => {
|
|
104
|
+
it('should handle missing vibe directory gracefully', () => {
|
|
105
|
+
// Act - no vibe project created
|
|
106
|
+
expect(() => listCommand([])).toThrow('process.exit(1)');
|
|
107
|
+
|
|
108
|
+
// Assert
|
|
109
|
+
expect(exitMock.exitCalls).toContain(1);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should handle corrupted ticket files', async () => {
|
|
113
|
+
// Arrange
|
|
114
|
+
const vibeProject = createMockVibeProject(tempDir);
|
|
115
|
+
|
|
116
|
+
// Create a corrupted ticket file
|
|
117
|
+
const fs = await import('fs');
|
|
118
|
+
fs.writeFileSync(
|
|
119
|
+
`${vibeProject.ticketsDir}/TKT-001-corrupted.md`,
|
|
120
|
+
'invalid yaml content without frontmatter',
|
|
121
|
+
'utf-8'
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// Act
|
|
125
|
+
expect(() => listCommand([])).toThrow('process.exit(0)');
|
|
126
|
+
|
|
127
|
+
// Assert - should handle gracefully and continue
|
|
128
|
+
expect(exitMock.exitCalls).toContain(0);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('filtering and sorting', () => {
|
|
133
|
+
it('should handle multiple tickets correctly', () => {
|
|
134
|
+
// Arrange
|
|
135
|
+
createMockVibeProject(tempDir, {
|
|
136
|
+
withTickets: [
|
|
137
|
+
{ id: 'TKT-003', title: 'Third', status: 'open' },
|
|
138
|
+
{ id: 'TKT-001', title: 'First', status: 'done' },
|
|
139
|
+
{ id: 'TKT-002', title: 'Second', status: 'in_progress' }
|
|
140
|
+
]
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Act
|
|
144
|
+
listCommand([]);
|
|
145
|
+
|
|
146
|
+
// Assert
|
|
147
|
+
const output = consoleMock.logs.log.join(' ');
|
|
148
|
+
expect(output).toContain('TKT-001');
|
|
149
|
+
expect(output).toContain('TKT-002');
|
|
150
|
+
expect(output).toContain('TKT-003');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import { getTicketsDir, getConfig, getNextTicketId, createSlug } from '../../utils/index.js';
|
|
5
|
+
import { confirmPrompt } from '../../utils/prompts.js';
|
|
6
|
+
import { logger } from '../../utils/cli.js';
|
|
7
|
+
|
|
8
|
+
// Configuration constants
|
|
9
|
+
const DEFAULT_PRIORITY = 'medium';
|
|
10
|
+
const DEFAULT_STATUS = 'open';
|
|
11
|
+
const GIT_STATUS_CHECK_TIMEOUT = 5000;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if AI is enabled in config
|
|
15
|
+
* @param {Object} config - Configuration object
|
|
16
|
+
* @returns {boolean} True if AI is enabled
|
|
17
|
+
*/
|
|
18
|
+
function checkAiEnabled(config) {
|
|
19
|
+
return config && config.ai && config.ai.enabled && config.ai.provider !== 'none';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse command arguments into structured data
|
|
24
|
+
* @param {Array} args - Command line arguments
|
|
25
|
+
* @returns {Object} Parsed arguments with title, priority, and status
|
|
26
|
+
*/
|
|
27
|
+
function parseArguments(args) {
|
|
28
|
+
if (!Array.isArray(args)) {
|
|
29
|
+
throw new Error('Arguments must be an array');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let titleParts = [];
|
|
33
|
+
let priority = DEFAULT_PRIORITY;
|
|
34
|
+
let status = DEFAULT_STATUS;
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < args.length; i++) {
|
|
37
|
+
const arg = args[i];
|
|
38
|
+
|
|
39
|
+
if (arg === '--priority' && i + 1 < args.length) {
|
|
40
|
+
priority = args[i + 1];
|
|
41
|
+
i++; // Skip the next argument as it's the priority value
|
|
42
|
+
} else if (arg === '--status' && i + 1 < args.length) {
|
|
43
|
+
status = args[i + 1];
|
|
44
|
+
i++; // Skip the next argument as it's the status value
|
|
45
|
+
} else if (!arg.startsWith('--')) {
|
|
46
|
+
titleParts.push(arg);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const title = titleParts.join(' ').trim();
|
|
51
|
+
|
|
52
|
+
if (!title) {
|
|
53
|
+
throw new Error('Please provide a title for the new ticket.');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { title, priority, status };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Validate priority and status against config options
|
|
61
|
+
* @param {string} priority - Priority value
|
|
62
|
+
* @param {string} status - Status value
|
|
63
|
+
* @param {Object} config - Configuration object
|
|
64
|
+
* @returns {Object} Validated priority and status
|
|
65
|
+
*/
|
|
66
|
+
function validateOptions(priority, status, config) {
|
|
67
|
+
let validatedPriority = priority;
|
|
68
|
+
let validatedStatus = status;
|
|
69
|
+
|
|
70
|
+
// Validate priority
|
|
71
|
+
if (config.tickets?.priority_options && !config.tickets.priority_options.includes(priority)) {
|
|
72
|
+
logger.warning(`Priority '${priority}' not in config options. Using default.`);
|
|
73
|
+
validatedPriority = DEFAULT_PRIORITY;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Validate status
|
|
77
|
+
if (config.tickets?.status_options && !config.tickets.status_options.includes(status)) {
|
|
78
|
+
logger.warning(`Status '${status}' not in config options. Using default.`);
|
|
79
|
+
validatedStatus = DEFAULT_STATUS;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { priority: validatedPriority, status: validatedStatus };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create ticket content from template
|
|
87
|
+
* @param {string} template - Template content
|
|
88
|
+
* @param {Object} ticketData - Ticket data
|
|
89
|
+
* @returns {string} Generated ticket content
|
|
90
|
+
*/
|
|
91
|
+
function createTicketContent(template, ticketData) {
|
|
92
|
+
if (typeof template !== 'string') {
|
|
93
|
+
throw new Error('Template must be a string');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const { ticketId, title, slug, priority, status, timestamp } = ticketData;
|
|
97
|
+
const paddedId = ticketId.replace('TKT-', '');
|
|
98
|
+
|
|
99
|
+
return template
|
|
100
|
+
.replace(/{id}/g, paddedId)
|
|
101
|
+
.replace(/{title}/g, title)
|
|
102
|
+
.replace(/{slug}/g, slug)
|
|
103
|
+
.replace(/{date}/g, timestamp)
|
|
104
|
+
.replace(/^priority: .*$/m, `priority: ${priority}`)
|
|
105
|
+
.replace(/^status: .*$/m, `status: ${status}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if file is untracked in git
|
|
110
|
+
* @param {string} filename - File to check
|
|
111
|
+
* @returns {Promise<boolean>} True if file is untracked
|
|
112
|
+
*/
|
|
113
|
+
async function isFileUntracked(filename) {
|
|
114
|
+
try {
|
|
115
|
+
const { spawn } = await import('child_process');
|
|
116
|
+
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
const child = spawn('git', ['status', '--porcelain', filename], {
|
|
119
|
+
cwd: process.cwd(),
|
|
120
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
121
|
+
timeout: GIT_STATUS_CHECK_TIMEOUT
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
let output = '';
|
|
125
|
+
child.stdout.on('data', (data) => {
|
|
126
|
+
output += data.toString();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
child.on('close', (code) => {
|
|
130
|
+
// If output starts with '??', file is untracked
|
|
131
|
+
resolve(output.trim().startsWith('??'));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
child.on('error', () => {
|
|
135
|
+
resolve(false); // Assume tracked if git check fails
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Timeout handler
|
|
139
|
+
setTimeout(() => {
|
|
140
|
+
try {
|
|
141
|
+
child.kill('SIGTERM');
|
|
142
|
+
} catch (killError) {
|
|
143
|
+
// Ignore kill errors
|
|
144
|
+
}
|
|
145
|
+
resolve(false);
|
|
146
|
+
}, GIT_STATUS_CHECK_TIMEOUT);
|
|
147
|
+
});
|
|
148
|
+
} catch (error) {
|
|
149
|
+
return false; // Assume tracked if anything fails
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Handle file renaming based on AI-generated slug
|
|
155
|
+
* @param {string} originalPath - Original file path
|
|
156
|
+
* @param {string} ticketDir - Tickets directory
|
|
157
|
+
* @returns {Promise<void>}
|
|
158
|
+
*/
|
|
159
|
+
async function handleFileRename(originalPath, ticketDir) {
|
|
160
|
+
try {
|
|
161
|
+
const filename = path.basename(originalPath);
|
|
162
|
+
const isUntracked = await isFileUntracked(filename);
|
|
163
|
+
|
|
164
|
+
if (!isUntracked) {
|
|
165
|
+
return; // Don't rename tracked files
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const { parseTicket } = await import('../../utils/ticket.js');
|
|
169
|
+
const ticketData = parseTicket(originalPath);
|
|
170
|
+
|
|
171
|
+
if (!ticketData.metadata.slug) {
|
|
172
|
+
return; // No slug to use for renaming
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const newFilename = `${ticketData.metadata.slug}.md`;
|
|
176
|
+
const newPath = path.join(ticketDir, newFilename);
|
|
177
|
+
|
|
178
|
+
if (newPath !== originalPath && !fs.existsSync(newPath)) {
|
|
179
|
+
fs.renameSync(originalPath, newPath);
|
|
180
|
+
logger.info(`Renamed ticket file to: ${newFilename}`);
|
|
181
|
+
}
|
|
182
|
+
} catch (error) {
|
|
183
|
+
logger.debug(`File rename skipped - ${error.message}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Create a new ticket
|
|
189
|
+
* @param {Array} args - Command arguments
|
|
190
|
+
* @returns {Promise<void>}
|
|
191
|
+
* @throws {Error} If ticket creation fails
|
|
192
|
+
*/
|
|
193
|
+
async function newCommand(args) {
|
|
194
|
+
try {
|
|
195
|
+
// Parse and validate arguments
|
|
196
|
+
const { title, priority, status } = parseArguments(args);
|
|
197
|
+
|
|
198
|
+
// Check required files and paths
|
|
199
|
+
const configPath = path.join(process.cwd(), '.vibe', 'config.yml');
|
|
200
|
+
const templatePath = path.join(process.cwd(), '.vibe', '.templates', 'default.md');
|
|
201
|
+
|
|
202
|
+
if (!fs.existsSync(configPath)) {
|
|
203
|
+
throw new Error('Missing config.yml. Run "vibe init" first.');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!fs.existsSync(templatePath)) {
|
|
207
|
+
throw new Error('Missing default.md template. Run "vibe init" first.');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Load configuration and template
|
|
211
|
+
let config, template;
|
|
212
|
+
try {
|
|
213
|
+
config = yaml.load(fs.readFileSync(configPath, 'utf-8'));
|
|
214
|
+
template = fs.readFileSync(templatePath, 'utf-8');
|
|
215
|
+
} catch (readError) {
|
|
216
|
+
throw new Error(`Failed to read configuration or template: ${readError.message}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!config) {
|
|
220
|
+
throw new Error('Configuration file is empty or invalid');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Validate options against config
|
|
224
|
+
const validatedOptions = validateOptions(priority, status, config);
|
|
225
|
+
|
|
226
|
+
// Generate ticket data
|
|
227
|
+
const ticketId = getNextTicketId();
|
|
228
|
+
const simpleSlug = createSlug(title, config);
|
|
229
|
+
const ticketSlug = `${ticketId}-${simpleSlug}`;
|
|
230
|
+
const filename = `${ticketId}-${simpleSlug}.md`;
|
|
231
|
+
const timestamp = new Date().toISOString();
|
|
232
|
+
|
|
233
|
+
const ticketData = {
|
|
234
|
+
ticketId,
|
|
235
|
+
title,
|
|
236
|
+
slug: ticketSlug,
|
|
237
|
+
priority: validatedOptions.priority,
|
|
238
|
+
status: validatedOptions.status,
|
|
239
|
+
timestamp
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// Create ticket content
|
|
243
|
+
const content = createTicketContent(template, ticketData);
|
|
244
|
+
|
|
245
|
+
// Write ticket file
|
|
246
|
+
const ticketDir = path.join(process.cwd(), config.tickets?.path || '.vibe/tickets');
|
|
247
|
+
const outputPath = path.join(ticketDir, filename);
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
fs.writeFileSync(outputPath, content, 'utf-8');
|
|
251
|
+
} catch (writeError) {
|
|
252
|
+
throw new Error(`Failed to create ticket file: ${writeError.message}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
logger.success(`Created ticket: ${filename} (priority: ${ticketData.priority}, status: ${ticketData.status})`);
|
|
256
|
+
|
|
257
|
+
// Offer AI enhancement if available
|
|
258
|
+
if (checkAiEnabled(config)) {
|
|
259
|
+
await offerAiEnhancement(ticketId, outputPath, ticketDir);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
} catch (error) {
|
|
263
|
+
logger.error(error.message);
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Offer AI enhancement for newly created ticket
|
|
270
|
+
* @param {string} ticketId - The ticket ID
|
|
271
|
+
* @param {string} outputPath - Path to the ticket file
|
|
272
|
+
* @param {string} ticketDir - Tickets directory
|
|
273
|
+
* @returns {Promise<void>}
|
|
274
|
+
*/
|
|
275
|
+
async function offerAiEnhancement(ticketId, outputPath, ticketDir) {
|
|
276
|
+
logger.ai('AI enhancement is available for this ticket.');
|
|
277
|
+
|
|
278
|
+
const shouldRefine = await confirmPrompt('🚀', 'Do you want to refine this ticket automatically based on your codebase?', true);
|
|
279
|
+
|
|
280
|
+
if (!shouldRefine) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
logger.step('Starting AI enhancement...');
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
// Import and run refine command
|
|
288
|
+
const refineModule = await import('../refine/index.js');
|
|
289
|
+
const refineCommand = refineModule.default;
|
|
290
|
+
|
|
291
|
+
// Extract ticket number for refine command
|
|
292
|
+
const ticketNumber = ticketId.replace('TKT-', '');
|
|
293
|
+
await refineCommand([ticketNumber], { fromNewCommand: true });
|
|
294
|
+
|
|
295
|
+
// Handle potential file renaming based on AI-generated slug
|
|
296
|
+
await handleFileRename(outputPath, ticketDir);
|
|
297
|
+
|
|
298
|
+
} catch (error) {
|
|
299
|
+
logger.error(`Failed to enhance ticket: ${error.message}`);
|
|
300
|
+
const ticketNumber = ticketId.replace('TKT-', '');
|
|
301
|
+
logger.tip(`You can manually enhance it later with: vibe refine ${ticketNumber}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export default newCommand;
|