@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
package/index.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* VibeKit - A developer-focused ticket and task management CLI tool
|
|
5
|
+
*
|
|
6
|
+
* This is the main entry point for the VibeKit CLI application.
|
|
7
|
+
* It handles command routing and provides a consistent interface
|
|
8
|
+
* for all VibeKit operations.
|
|
9
|
+
*
|
|
10
|
+
* @fileoverview Main CLI entry point for VibeKit
|
|
11
|
+
* @author VibeKit Team
|
|
12
|
+
* @version 1.0.0
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
import { dirname } from 'path';
|
|
18
|
+
|
|
19
|
+
// ESM replacement for __dirname
|
|
20
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
21
|
+
const __dirname = dirname(__filename);
|
|
22
|
+
|
|
23
|
+
// Available commands in VibeKit
|
|
24
|
+
const AVAILABLE_COMMANDS = [
|
|
25
|
+
'init', 'new', 'close', 'list', 'get-started',
|
|
26
|
+
'start', 'link', 'unlink', 'refine', 'lint', 'review'
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Display available commands to the user
|
|
31
|
+
*/
|
|
32
|
+
function showAvailableCommands() {
|
|
33
|
+
console.log(`Available commands: ${AVAILABLE_COMMANDS.join(', ')}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Execute a VibeKit command
|
|
38
|
+
* @param {string} command - The command to execute
|
|
39
|
+
* @param {Array} args - Arguments to pass to the command
|
|
40
|
+
*/
|
|
41
|
+
async function executeCommand(command, args) {
|
|
42
|
+
const commandPath = path.join(__dirname, 'src', 'commands', command, 'index.js');
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// Dynamic import for ESM
|
|
46
|
+
const commandModule = await import(commandPath);
|
|
47
|
+
const commandFunction = commandModule.default;
|
|
48
|
+
|
|
49
|
+
if (typeof commandFunction === 'function') {
|
|
50
|
+
await commandFunction(args);
|
|
51
|
+
} else {
|
|
52
|
+
console.error(`❌ Command '${command}' is not executable.`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (err.code === 'ERR_MODULE_NOT_FOUND') {
|
|
57
|
+
showAvailableCommands();
|
|
58
|
+
console.error(`❌ Command '${command}' not found.`);
|
|
59
|
+
} else {
|
|
60
|
+
console.error(`❌ Error executing command '${command}': ${err.message}`);
|
|
61
|
+
|
|
62
|
+
// Only show stack trace in debug mode or development
|
|
63
|
+
if (process.env.NODE_ENV === 'development' || process.env.DEBUG) {
|
|
64
|
+
console.error(err.stack);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Main application entry point
|
|
73
|
+
*/
|
|
74
|
+
async function main() {
|
|
75
|
+
// Parse command line arguments
|
|
76
|
+
const [command, ...commandArgs] = process.argv.slice(2);
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
// Show help if no command provided
|
|
80
|
+
if (!command) {
|
|
81
|
+
console.log('🎆 VibeKit - Developer-focused ticket management\n');
|
|
82
|
+
showAvailableCommands();
|
|
83
|
+
console.log('\nUse "vibe <command>" to get started!');
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Execute the requested command
|
|
88
|
+
await executeCommand(command, commandArgs);
|
|
89
|
+
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error(`❌ Unexpected error: ${err.message}`);
|
|
92
|
+
|
|
93
|
+
// Only show stack trace in debug mode or development
|
|
94
|
+
if (process.env.NODE_ENV === 'development' || process.env.DEBUG) {
|
|
95
|
+
console.error(err.stack);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Run the application
|
|
103
|
+
main().catch((error) => {
|
|
104
|
+
console.error(`❌ Fatal error: ${error.message}`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vibedx/vibekit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A powerful CLI tool for managing development tickets and project workflows",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"index.js",
|
|
9
|
+
"src/",
|
|
10
|
+
"assets/"
|
|
11
|
+
],
|
|
12
|
+
"bin": {
|
|
13
|
+
"vibe": "index.js"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node --watch index.js",
|
|
17
|
+
"test": "npm run test:cleanup && NODE_OPTIONS='--experimental-vm-modules' jest --detectOpenHandles",
|
|
18
|
+
"test:watch": "NODE_OPTIONS='--experimental-vm-modules' jest --watch",
|
|
19
|
+
"test:coverage": "NODE_OPTIONS='--experimental-vm-modules' jest --coverage --detectOpenHandles",
|
|
20
|
+
"test:cleanup": "rm -rf __temp__",
|
|
21
|
+
"prepublishOnly": "echo 'Ready to publish'",
|
|
22
|
+
"version": "git add -A && git commit -m 'chore: version bump'",
|
|
23
|
+
"postversion": "git push && git push --tags"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"cli",
|
|
27
|
+
"tickets",
|
|
28
|
+
"workflow",
|
|
29
|
+
"development",
|
|
30
|
+
"project-management"
|
|
31
|
+
],
|
|
32
|
+
"author": "Ives van Hoorne",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/vibedx/vibekit.git"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/vibedx/vibekit/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/vibedx/vibekit#readme",
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"chalk": "^4.1.2",
|
|
44
|
+
"clipboardy": "^4.0.0",
|
|
45
|
+
"js-yaml": "^4.1.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@jest/globals": "^29.7.0",
|
|
49
|
+
"jest": "^29.7.0"
|
|
50
|
+
},
|
|
51
|
+
"jest": {
|
|
52
|
+
"testEnvironment": "node",
|
|
53
|
+
"globals": {
|
|
54
|
+
"__DEV__": true
|
|
55
|
+
},
|
|
56
|
+
"testMatch": [
|
|
57
|
+
"**/src/**/*.test.js",
|
|
58
|
+
"**/__tests__/**/*.test.js",
|
|
59
|
+
"**/tests/**/*.test.js"
|
|
60
|
+
],
|
|
61
|
+
"collectCoverage": true,
|
|
62
|
+
"coverageDirectory": "coverage",
|
|
63
|
+
"forceExit": true,
|
|
64
|
+
"coverageReporters": [
|
|
65
|
+
"text",
|
|
66
|
+
"lcov",
|
|
67
|
+
"html"
|
|
68
|
+
],
|
|
69
|
+
"collectCoverageFrom": [
|
|
70
|
+
"src/**/*.js",
|
|
71
|
+
"index.js",
|
|
72
|
+
"!src/utils/cli.test.js",
|
|
73
|
+
"!src/**/*.test.js",
|
|
74
|
+
"!**/__tests__/**",
|
|
75
|
+
"!**/__temp__/**",
|
|
76
|
+
"!**/node_modules/**"
|
|
77
|
+
],
|
|
78
|
+
"coverageThreshold": {
|
|
79
|
+
"global": {
|
|
80
|
+
"branches": 13,
|
|
81
|
+
"functions": 20,
|
|
82
|
+
"lines": 14,
|
|
83
|
+
"statements": 14
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"engines": {
|
|
88
|
+
"node": ">=18.0.0"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import { getTicketsDir } from '../../utils/index.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Mark a ticket as done
|
|
8
|
+
* @param {string[]} args Command arguments
|
|
9
|
+
*/
|
|
10
|
+
function closeCommand(args) {
|
|
11
|
+
const ticketArg = args[0];
|
|
12
|
+
|
|
13
|
+
if (!ticketArg) {
|
|
14
|
+
console.error("❌ Please provide a ticket ID or number.");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const ticketFolder = getTicketsDir();
|
|
19
|
+
|
|
20
|
+
if (!fs.existsSync(ticketFolder)) {
|
|
21
|
+
console.error(`❌ Tickets directory not found: ${ticketFolder}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const files = fs.readdirSync(ticketFolder);
|
|
26
|
+
const normalizedInput = ticketArg.startsWith("TKT-")
|
|
27
|
+
? ticketArg
|
|
28
|
+
: `TKT-${ticketArg.padStart(3, "0")}`;
|
|
29
|
+
|
|
30
|
+
let matchFound = false;
|
|
31
|
+
|
|
32
|
+
for (const file of files) {
|
|
33
|
+
const fullPath = path.join(ticketFolder, file);
|
|
34
|
+
|
|
35
|
+
// Skip directories
|
|
36
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
41
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
42
|
+
|
|
43
|
+
if (match) {
|
|
44
|
+
const frontmatter = yaml.load(match[1]);
|
|
45
|
+
if (
|
|
46
|
+
frontmatter.id === normalizedInput ||
|
|
47
|
+
file.includes(normalizedInput)
|
|
48
|
+
) {
|
|
49
|
+
frontmatter.status = "done";
|
|
50
|
+
|
|
51
|
+
const updated = `---\n${yaml.dump(frontmatter)}---${content.split("---").slice(2).join("---")}`;
|
|
52
|
+
fs.writeFileSync(fullPath, updated, "utf-8");
|
|
53
|
+
|
|
54
|
+
console.log(`✅ Ticket ${frontmatter.id} marked as done.`);
|
|
55
|
+
matchFound = true;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!matchFound) {
|
|
62
|
+
console.log(`❌ No ticket matching '${ticketArg}' found.`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default closeCommand;
|
|
@@ -0,0 +1,235 @@
|
|
|
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 closeCommand from './index.js';
|
|
13
|
+
|
|
14
|
+
describe('close command', () => {
|
|
15
|
+
let tempDir;
|
|
16
|
+
let consoleMock;
|
|
17
|
+
let restoreCwd;
|
|
18
|
+
let exitMock;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
tempDir = createTempDir('close-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('argument validation', () => {
|
|
35
|
+
it('should show error when no ticket ID provided', () => {
|
|
36
|
+
// Act
|
|
37
|
+
expect(() => closeCommand([])).toThrow('process.exit(1)');
|
|
38
|
+
|
|
39
|
+
// Assert
|
|
40
|
+
expect(exitMock.exitCalls).toContain(1);
|
|
41
|
+
expect(consoleMock.logs.error).toContain('❌ Please provide a ticket ID or number.');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('tickets directory validation', () => {
|
|
46
|
+
it('should show error when tickets directory does not exist', () => {
|
|
47
|
+
// Act - no vibe project created
|
|
48
|
+
expect(() => closeCommand(['TKT-001'])).toThrow('process.exit(1)');
|
|
49
|
+
|
|
50
|
+
// Assert
|
|
51
|
+
expect(exitMock.exitCalls).toContain(1);
|
|
52
|
+
expect(consoleMock.logs.error.some(log =>
|
|
53
|
+
log.includes('❌ Tickets directory not found:')
|
|
54
|
+
)).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('ticket closing', () => {
|
|
59
|
+
it('should mark ticket as done when found by full ID', () => {
|
|
60
|
+
// Arrange
|
|
61
|
+
createMockVibeProject(tempDir, {
|
|
62
|
+
withTickets: [
|
|
63
|
+
{
|
|
64
|
+
id: 'TKT-001',
|
|
65
|
+
title: 'Test ticket',
|
|
66
|
+
status: 'open',
|
|
67
|
+
slug: 'test-ticket'
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Act
|
|
73
|
+
closeCommand(['TKT-001']);
|
|
74
|
+
|
|
75
|
+
// Assert
|
|
76
|
+
expect(consoleMock.logs.log).toContain('✅ Ticket TKT-001 marked as done.');
|
|
77
|
+
|
|
78
|
+
// Verify file was updated
|
|
79
|
+
const ticketPath = path.join(tempDir, '.vibe', 'tickets', 'TKT-001-test-ticket.md');
|
|
80
|
+
const content = fs.readFileSync(ticketPath, 'utf-8');
|
|
81
|
+
expect(content).toContain('status: done');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should mark ticket as done when found by number only', () => {
|
|
85
|
+
// Arrange
|
|
86
|
+
createMockVibeProject(tempDir, {
|
|
87
|
+
withTickets: [
|
|
88
|
+
{
|
|
89
|
+
id: 'TKT-002',
|
|
90
|
+
title: 'Another ticket',
|
|
91
|
+
status: 'in_progress',
|
|
92
|
+
slug: 'another-ticket'
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Act
|
|
98
|
+
closeCommand(['2']);
|
|
99
|
+
|
|
100
|
+
// Assert
|
|
101
|
+
expect(consoleMock.logs.log).toContain('✅ Ticket TKT-002 marked as done.');
|
|
102
|
+
|
|
103
|
+
// Verify file was updated
|
|
104
|
+
const ticketPath = path.join(tempDir, '.vibe', 'tickets', 'TKT-002-another-ticket.md');
|
|
105
|
+
const content = fs.readFileSync(ticketPath, 'utf-8');
|
|
106
|
+
expect(content).toContain('status: done');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should preserve other frontmatter fields when updating status', () => {
|
|
110
|
+
// Arrange
|
|
111
|
+
createMockVibeProject(tempDir, {
|
|
112
|
+
withTickets: [
|
|
113
|
+
{
|
|
114
|
+
id: 'TKT-003',
|
|
115
|
+
title: 'Ticket with priority',
|
|
116
|
+
status: 'open',
|
|
117
|
+
priority: 'high',
|
|
118
|
+
slug: 'ticket-with-priority'
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Act
|
|
124
|
+
closeCommand(['TKT-003']);
|
|
125
|
+
|
|
126
|
+
// Assert
|
|
127
|
+
const ticketPath = path.join(tempDir, '.vibe', 'tickets', 'TKT-003-ticket-with-priority.md');
|
|
128
|
+
const content = fs.readFileSync(ticketPath, 'utf-8');
|
|
129
|
+
expect(content).toContain('status: done');
|
|
130
|
+
expect(content).toContain('priority: high');
|
|
131
|
+
expect(content).toContain('title: Ticket with priority');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should handle ticket not found', () => {
|
|
135
|
+
// Arrange
|
|
136
|
+
createMockVibeProject(tempDir, {
|
|
137
|
+
withTickets: [
|
|
138
|
+
{
|
|
139
|
+
id: 'TKT-001',
|
|
140
|
+
title: 'Existing ticket',
|
|
141
|
+
status: 'open',
|
|
142
|
+
slug: 'existing-ticket'
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Act
|
|
148
|
+
closeCommand(['TKT-999']);
|
|
149
|
+
|
|
150
|
+
// Assert
|
|
151
|
+
expect(consoleMock.logs.log).toContain("❌ No ticket matching 'TKT-999' found.");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should handle multiple tickets and find correct one', () => {
|
|
155
|
+
// Arrange
|
|
156
|
+
createMockVibeProject(tempDir, {
|
|
157
|
+
withTickets: [
|
|
158
|
+
{
|
|
159
|
+
id: 'TKT-001',
|
|
160
|
+
title: 'First ticket',
|
|
161
|
+
status: 'open',
|
|
162
|
+
slug: 'first-ticket'
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: 'TKT-002',
|
|
166
|
+
title: 'Second ticket',
|
|
167
|
+
status: 'in_progress',
|
|
168
|
+
slug: 'second-ticket'
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: 'TKT-003',
|
|
172
|
+
title: 'Third ticket',
|
|
173
|
+
status: 'review',
|
|
174
|
+
slug: 'third-ticket'
|
|
175
|
+
}
|
|
176
|
+
]
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Act
|
|
180
|
+
closeCommand(['2']);
|
|
181
|
+
|
|
182
|
+
// Assert
|
|
183
|
+
expect(consoleMock.logs.log).toContain('✅ Ticket TKT-002 marked as done.');
|
|
184
|
+
|
|
185
|
+
// Verify only the targeted ticket was updated
|
|
186
|
+
const ticket1Path = path.join(tempDir, '.vibe', 'tickets', 'TKT-001-first-ticket.md');
|
|
187
|
+
const ticket2Path = path.join(tempDir, '.vibe', 'tickets', 'TKT-002-second-ticket.md');
|
|
188
|
+
const ticket3Path = path.join(tempDir, '.vibe', 'tickets', 'TKT-003-third-ticket.md');
|
|
189
|
+
|
|
190
|
+
expect(fs.readFileSync(ticket1Path, 'utf-8')).toContain('status: open');
|
|
191
|
+
expect(fs.readFileSync(ticket2Path, 'utf-8')).toContain('status: done');
|
|
192
|
+
expect(fs.readFileSync(ticket3Path, 'utf-8')).toContain('status: review');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('error handling', () => {
|
|
197
|
+
it('should skip directories in tickets folder', () => {
|
|
198
|
+
// Arrange
|
|
199
|
+
const vibeProject = createMockVibeProject(tempDir);
|
|
200
|
+
|
|
201
|
+
// Create a subdirectory in tickets folder
|
|
202
|
+
fs.mkdirSync(path.join(vibeProject.ticketsDir, 'subdirectory'));
|
|
203
|
+
|
|
204
|
+
// Create a valid ticket
|
|
205
|
+
const ticketContent = `---
|
|
206
|
+
id: TKT-001
|
|
207
|
+
title: Valid ticket
|
|
208
|
+
status: open
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
# Test ticket`;
|
|
212
|
+
fs.writeFileSync(path.join(vibeProject.ticketsDir, 'TKT-001-valid.md'), ticketContent, 'utf-8');
|
|
213
|
+
|
|
214
|
+
// Act
|
|
215
|
+
closeCommand(['TKT-001']);
|
|
216
|
+
|
|
217
|
+
// Assert
|
|
218
|
+
expect(consoleMock.logs.log).toContain('✅ Ticket TKT-001 marked as done.');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should handle files without proper frontmatter', () => {
|
|
222
|
+
// Arrange
|
|
223
|
+
const vibeProject = createMockVibeProject(tempDir);
|
|
224
|
+
|
|
225
|
+
// Create invalid file without frontmatter
|
|
226
|
+
fs.writeFileSync(path.join(vibeProject.ticketsDir, 'invalid.md'), 'No frontmatter here', 'utf-8');
|
|
227
|
+
|
|
228
|
+
// Act
|
|
229
|
+
closeCommand(['TKT-001']);
|
|
230
|
+
|
|
231
|
+
// Assert
|
|
232
|
+
expect(consoleMock.logs.log).toContain("❌ No ticket matching 'TKT-001' found.");
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
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
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Helper function to create sample tickets for the onboarding experience
|
|
8
|
+
* @param {string} title - The title of the ticket
|
|
9
|
+
* @param {string} description - The description of the ticket
|
|
10
|
+
* @param {string} priority - The priority of the ticket (low, medium, high)
|
|
11
|
+
* @param {string} status - The status of the ticket (open, in_progress, review, done)
|
|
12
|
+
*/
|
|
13
|
+
function createSampleTicket(title, description, priority = "medium", status = "open") {
|
|
14
|
+
const configPath = path.join(process.cwd(), ".vibe", "config.yml");
|
|
15
|
+
const templatePath = path.join(process.cwd(), ".vibe", ".templates", "default.md");
|
|
16
|
+
|
|
17
|
+
if (!fs.existsSync(configPath) || !fs.existsSync(templatePath)) {
|
|
18
|
+
console.error("❌ Missing config.yml or default.md template.");
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const config = yaml.load(fs.readFileSync(configPath, "utf-8"));
|
|
23
|
+
const template = fs.readFileSync(templatePath, "utf-8");
|
|
24
|
+
const ticketDir = path.join(process.cwd(), config.tickets?.path || ".vibe/tickets");
|
|
25
|
+
|
|
26
|
+
const files = fs.readdirSync(ticketDir);
|
|
27
|
+
const ticketNumbers = files
|
|
28
|
+
.map(f => f.match(/^TKT-(\\d+)/))
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.map(match => parseInt(match[1], 10));
|
|
31
|
+
const nextId = Math.max(0, ...ticketNumbers) + 1;
|
|
32
|
+
const paddedId = String(nextId).padStart(3, "0");
|
|
33
|
+
const now = new Date().toISOString();
|
|
34
|
+
|
|
35
|
+
const ticketId = `TKT-${paddedId}`;
|
|
36
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
37
|
+
const filename = `${ticketId}-${slug}.md`;
|
|
38
|
+
|
|
39
|
+
// Replace template placeholders with actual values
|
|
40
|
+
let content = template
|
|
41
|
+
.replace(/{id}/g, paddedId)
|
|
42
|
+
.replace(/{title}/g, title)
|
|
43
|
+
.replace(/{date}/g, now);
|
|
44
|
+
|
|
45
|
+
// Replace priority and status in the frontmatter
|
|
46
|
+
content = content.replace(/^priority: .*$/m, `priority: ${priority}`);
|
|
47
|
+
content = content.replace(/^status: .*$/m, `status: ${status}`);
|
|
48
|
+
|
|
49
|
+
// Add description
|
|
50
|
+
content = content.replace(/## Description\\s*\\n\\s*\\n/m, `## Description\\n\\n${description}\\n\\n`);
|
|
51
|
+
|
|
52
|
+
// Add AI prompt for the feature request example
|
|
53
|
+
if (title.includes("AI Prompt")) {
|
|
54
|
+
content = content.replace(/## AI Prompt\\s*\\n\\s*\\n/m,
|
|
55
|
+
`## AI Prompt\\n\\nGenerate ideas for implementing this feature in a Node.js CLI application. Consider user experience, error handling, and performance.\\n\\n`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const outputPath = path.join(ticketDir, filename);
|
|
59
|
+
fs.writeFileSync(outputPath, content, "utf-8");
|
|
60
|
+
|
|
61
|
+
console.log(`✅ Created sample ticket: ${filename}`);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create onboarding materials for VibeKit
|
|
67
|
+
* @param {string[]} args Command arguments
|
|
68
|
+
*/
|
|
69
|
+
function getStartedCommand(args) {
|
|
70
|
+
console.log("✨ Welcome to VibeKit! Setting up your onboarding experience...");
|
|
71
|
+
|
|
72
|
+
// Check if .vibe directory exists
|
|
73
|
+
if (!fs.existsSync(path.join(process.cwd(), ".vibe"))) {
|
|
74
|
+
console.error("❌ VibeKit is not initialized. Please run 'vibe init' first.");
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Create .vibe/README.md with instructions
|
|
79
|
+
const readmePath = path.join(process.cwd(), ".vibe", "README.md");
|
|
80
|
+
const readmeContent = `# Welcome to VibeKit
|
|
81
|
+
|
|
82
|
+
VibeKit is a CLI tool for managing tickets, project context, and AI suggestions inside your repository.
|
|
83
|
+
|
|
84
|
+
## Getting Started
|
|
85
|
+
|
|
86
|
+
Here are the main commands you can use:
|
|
87
|
+
|
|
88
|
+
- \`vibe init\` - Initialize VibeKit in your repository
|
|
89
|
+
- \`vibe new "Ticket title"\` - Create a new ticket
|
|
90
|
+
- \`vibe list\` - List all tickets
|
|
91
|
+
- \`vibe close TKT-XXX\` - Mark a ticket as done
|
|
92
|
+
- \`vibe get-started\` - Create sample tickets and documentation
|
|
93
|
+
|
|
94
|
+
## Ticket Structure
|
|
95
|
+
|
|
96
|
+
Each ticket is a Markdown file with YAML frontmatter containing metadata like:
|
|
97
|
+
- id
|
|
98
|
+
- title
|
|
99
|
+
- status
|
|
100
|
+
- priority
|
|
101
|
+
- created_at
|
|
102
|
+
- updated_at
|
|
103
|
+
|
|
104
|
+
The body of the ticket contains sections for Description, Acceptance Criteria, Notes, and AI prompts.
|
|
105
|
+
|
|
106
|
+
## Next Steps
|
|
107
|
+
|
|
108
|
+
Check out the sample tickets we've created for you to see how VibeKit can be used in your workflow.
|
|
109
|
+
`;
|
|
110
|
+
|
|
111
|
+
fs.writeFileSync(readmePath, readmeContent, "utf-8");
|
|
112
|
+
console.log("✅ Created README.md with getting started instructions");
|
|
113
|
+
|
|
114
|
+
// Create sample tickets
|
|
115
|
+
createSampleTicket(
|
|
116
|
+
"Simple Task Example",
|
|
117
|
+
"This is a simple task ticket example showing the basic structure.",
|
|
118
|
+
"low"
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
createSampleTicket(
|
|
122
|
+
"Bug Report Example",
|
|
123
|
+
"This ticket demonstrates how to report and track bugs using VibeKit.",
|
|
124
|
+
"high",
|
|
125
|
+
"in_progress"
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
createSampleTicket(
|
|
129
|
+
"Feature Request with AI Prompt",
|
|
130
|
+
"This example shows how to use the AI prompt section to get assistance with implementing a feature.",
|
|
131
|
+
"medium"
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
console.log("✅ Created sample tickets to demonstrate VibeKit features");
|
|
135
|
+
console.log("\n✨ You're all set! Try running 'vibe list' to see your tickets.\n");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export default getStartedCommand;
|