@vibedx/vibekit 0.6.2 → 0.6.4
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/README.md +33 -1
- package/assets/team.yml +18 -12
- package/index.js +1 -1
- package/package.json +1 -1
- package/skills/vibekit/SKILL.md +23 -1
- package/src/commands/close/index.js +34 -5
- package/src/commands/list/index.js +6 -6
- package/src/commands/skills/index.js +65 -0
- package/src/commands/start/index.js +126 -63
- package/src/utils/git.js +112 -1
- package/src/utils/index.js +26 -5
package/README.md
CHANGED
|
@@ -63,6 +63,7 @@ The skill teaches agents the ticket-driven workflow — they'll create focused t
|
|
|
63
63
|
- **🚀 AI-native ticket management** - Purpose-built for AI-assisted development workflows
|
|
64
64
|
- **⚡ Generate tickets using Claude Code and Codex power** - Leverage cutting-edge AI for planning
|
|
65
65
|
- **🔄 Seamless git workflow** - Automatic branch creation and status tracking
|
|
66
|
+
- **🌿 Parallel worktrees** - Work on multiple tickets simultaneously with `--worktree`
|
|
66
67
|
- **📝 Living documentation** - Your tickets in git become your project's development story
|
|
67
68
|
|
|
68
69
|
## ✨ Features
|
|
@@ -74,7 +75,8 @@ The skill teaches agents the ticket-driven workflow — they'll create focused t
|
|
|
74
75
|
```
|
|
75
76
|
|
|
76
77
|
- **🎯 Smart Tickets**: Create, manage, and track development tickets with unique IDs
|
|
77
|
-
- **🔗 Git Integration**: Automatic branch creation and workflow management
|
|
78
|
+
- **🔗 Git Integration**: Automatic branch creation and workflow management
|
|
79
|
+
- **🌿 Worktree Support**: Work on multiple tickets in parallel without switching branches
|
|
78
80
|
- **🤖 AI Enhancement**: Claude Code integration for ticket refinement and content improvement
|
|
79
81
|
- **📋 Interactive CLI**: Beautiful terminal interface with arrow navigation
|
|
80
82
|
- **📝 Templates**: Customizable ticket templates for consistent workflows
|
|
@@ -110,6 +112,10 @@ vibe close TKT-001
|
|
|
110
112
|
# Start working on a ticket (creates git branch)
|
|
111
113
|
vibe start TKT-001
|
|
112
114
|
vibe start TKT-001 --base main --update-status
|
|
115
|
+
|
|
116
|
+
# Start in a separate worktree (parallel work without switching branches)
|
|
117
|
+
vibe start TKT-001 --worktree
|
|
118
|
+
vibe start TKT-001 -w
|
|
113
119
|
```
|
|
114
120
|
|
|
115
121
|
### 👥 Team Management
|
|
@@ -229,6 +235,31 @@ Fix responsive layout issues in `src/components/Layout.jsx`
|
|
|
229
235
|
- Test on devices: iPhone SE, iPad, desktop (1920px+)
|
|
230
236
|
```
|
|
231
237
|
|
|
238
|
+
### Working with Worktrees (Parallel Development)
|
|
239
|
+
```bash
|
|
240
|
+
# Start a ticket in its own worktree
|
|
241
|
+
$ vibe start TKT-005 --worktree
|
|
242
|
+
🌿 Created worktree at ~/.vibekit/worktrees/myproject/feature--TKT-005-add-api-cache/
|
|
243
|
+
📝 Updated ticket status to in_progress
|
|
244
|
+
💡 cd ~/.vibekit/worktrees/myproject/feature--TKT-005-add-api-cache/
|
|
245
|
+
💡 Run npm install in the worktree if needed
|
|
246
|
+
|
|
247
|
+
# Your main branch stays untouched — work on TKT-005 in the worktree
|
|
248
|
+
# List tickets shows worktree indicator
|
|
249
|
+
$ vibe list
|
|
250
|
+
┌─────────┬──────────────┬─────────────────────────────┬──────────┐
|
|
251
|
+
│ ID │ Status │ Title │ Worktree │
|
|
252
|
+
├─────────┼──────────────┼─────────────────────────────┼──────────┤
|
|
253
|
+
│ TKT-005 │ in_progress │ Add API cache │ 🌿 │
|
|
254
|
+
│ TKT-004 │ in_progress │ Add dark mode toggle │ │
|
|
255
|
+
└─────────┴──────────────┴─────────────────────────────┴──────────┘
|
|
256
|
+
|
|
257
|
+
# Close removes the worktree automatically
|
|
258
|
+
$ vibe close TKT-005
|
|
259
|
+
🗑️ Removed worktree at ~/.vibekit/worktrees/myproject/feature--TKT-005-add-api-cache/
|
|
260
|
+
✅ Closed TKT-005
|
|
261
|
+
```
|
|
262
|
+
|
|
232
263
|
### Quality Control with Lint
|
|
233
264
|
```bash
|
|
234
265
|
# Check all tickets for formatting issues
|
|
@@ -359,6 +390,7 @@ ai:
|
|
|
359
390
|
git:
|
|
360
391
|
branch_prefix: feature/
|
|
361
392
|
default_base: main
|
|
393
|
+
worktrees_path: ~/.vibekit/worktrees # Where worktrees are created
|
|
362
394
|
|
|
363
395
|
# Hooks
|
|
364
396
|
hooks:
|
package/assets/team.yml
CHANGED
|
@@ -6,17 +6,23 @@
|
|
|
6
6
|
# vibe team add <id> --name "Name" ... - Add a member
|
|
7
7
|
# vibe team remove <id> - Remove a member
|
|
8
8
|
# vibe new "task" --assignee <id> - Assign ticket to member
|
|
9
|
+
#
|
|
10
|
+
# No need to edit this file manually — use the CLI commands above.
|
|
11
|
+
# But if you prefer to edit directly, just add entries under `members:`
|
|
12
|
+
# like this:
|
|
13
|
+
#
|
|
14
|
+
# members:
|
|
15
|
+
# jane-doe:
|
|
16
|
+
# name: Jane Doe
|
|
17
|
+
# github: jane-doe
|
|
18
|
+
# slack: U04ABC12DEF
|
|
19
|
+
# x: janedoe
|
|
20
|
+
# role: Founder
|
|
21
|
+
# john-smith:
|
|
22
|
+
# name: John Smith
|
|
23
|
+
# github: john-smith
|
|
24
|
+
# slack: U05XYZ34GHI
|
|
25
|
+
# x: johnsmith
|
|
26
|
+
# role: Engineer
|
|
9
27
|
|
|
10
28
|
members: {}
|
|
11
|
-
# Example:
|
|
12
|
-
# mani-yadv:
|
|
13
|
-
# name: Mani
|
|
14
|
-
# github: mani-yadv
|
|
15
|
-
# slack: U0XXXXXXXX
|
|
16
|
-
# x: vernon1943
|
|
17
|
-
# role: Founder
|
|
18
|
-
# opusaku:
|
|
19
|
-
# name: Opus
|
|
20
|
-
# github: opusaku
|
|
21
|
-
# slack: U0XXXXXXXX
|
|
22
|
-
# role: Senior Engineer
|
package/index.js
CHANGED
|
@@ -23,7 +23,7 @@ const __dirname = dirname(__filename);
|
|
|
23
23
|
// Available commands in VibeKit
|
|
24
24
|
const AVAILABLE_COMMANDS = [
|
|
25
25
|
'init', 'new', 'close', 'list', 'get-started',
|
|
26
|
-
'start', 'link', 'unlink', 'refine', 'lint', 'review', 'team'
|
|
26
|
+
'start', 'link', 'unlink', 'refine', 'lint', 'review', 'team', 'skills'
|
|
27
27
|
];
|
|
28
28
|
|
|
29
29
|
/**
|
package/package.json
CHANGED
package/skills/vibekit/SKILL.md
CHANGED
|
@@ -59,7 +59,8 @@ vibe init # Creates .vibe/ directory with config, team, templates
|
|
|
59
59
|
| `vibe new "title"` | Create a ticket |
|
|
60
60
|
| `vibe list` | List all tickets |
|
|
61
61
|
| `vibe start <id>` | Start work (creates git branch) |
|
|
62
|
-
| `vibe
|
|
62
|
+
| `vibe start <id> --worktree` | Start work in a separate worktree |
|
|
63
|
+
| `vibe close <id>` | Mark ticket done (cleans up worktree if any) |
|
|
63
64
|
| `vibe lint` | Validate ticket format |
|
|
64
65
|
| `vibe lint --fix` | Auto-fix missing sections |
|
|
65
66
|
| `vibe refine <id>` | AI-enhance ticket details |
|
|
@@ -154,6 +155,27 @@ Apply vibekit workflow when:
|
|
|
154
155
|
1. Updates ticket status to `done`
|
|
155
156
|
2. Leaves the branch for manual merge/PR
|
|
156
157
|
|
|
158
|
+
### Worktree Support (Parallel Development)
|
|
159
|
+
|
|
160
|
+
Use `--worktree` / `-w` to work on multiple tickets simultaneously without switching branches:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
# Start ticket in a dedicated worktree
|
|
164
|
+
vibe start TKT-002 --worktree
|
|
165
|
+
|
|
166
|
+
# The worktree is created at ~/.vibekit/worktrees/<repo>/<branch>/
|
|
167
|
+
# Your main working directory stays on its current branch
|
|
168
|
+
# cd into the worktree path to work on the ticket
|
|
169
|
+
|
|
170
|
+
# Close automatically removes the worktree
|
|
171
|
+
vibe close TKT-002
|
|
172
|
+
|
|
173
|
+
# If the worktree has uncommitted changes, use --force
|
|
174
|
+
vibe close TKT-002 --force
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`vibe list` shows a 🌿 indicator next to tickets with active worktrees.
|
|
178
|
+
|
|
157
179
|
## Automation Pattern
|
|
158
180
|
|
|
159
181
|
For bots and automated agents working through tickets:
|
|
@@ -2,6 +2,8 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import yaml from 'js-yaml';
|
|
4
4
|
import { getTicketsDir } from '../../utils/index.js';
|
|
5
|
+
import { removeWorktree, getGitStatus } from '../../utils/git.js';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Mark a ticket as done
|
|
@@ -9,6 +11,7 @@ import { getTicketsDir } from '../../utils/index.js';
|
|
|
9
11
|
*/
|
|
10
12
|
function closeCommand(args) {
|
|
11
13
|
const ticketArg = args[0];
|
|
14
|
+
const forceFlag = args.includes('--force') || args.includes('-f');
|
|
12
15
|
|
|
13
16
|
if (!ticketArg) {
|
|
14
17
|
console.error("❌ Please provide a ticket ID or number.");
|
|
@@ -16,12 +19,12 @@ function closeCommand(args) {
|
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
const ticketFolder = getTicketsDir();
|
|
19
|
-
|
|
22
|
+
|
|
20
23
|
if (!fs.existsSync(ticketFolder)) {
|
|
21
24
|
console.error(`❌ Tickets directory not found: ${ticketFolder}`);
|
|
22
25
|
process.exit(1);
|
|
23
26
|
}
|
|
24
|
-
|
|
27
|
+
|
|
25
28
|
const files = fs.readdirSync(ticketFolder);
|
|
26
29
|
const normalizedInput = ticketArg.startsWith("TKT-")
|
|
27
30
|
? ticketArg
|
|
@@ -31,12 +34,11 @@ function closeCommand(args) {
|
|
|
31
34
|
|
|
32
35
|
for (const file of files) {
|
|
33
36
|
const fullPath = path.join(ticketFolder, file);
|
|
34
|
-
|
|
35
|
-
// Skip directories
|
|
37
|
+
|
|
36
38
|
if (fs.statSync(fullPath).isDirectory()) {
|
|
37
39
|
continue;
|
|
38
40
|
}
|
|
39
|
-
|
|
41
|
+
|
|
40
42
|
const content = fs.readFileSync(fullPath, "utf-8");
|
|
41
43
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
42
44
|
|
|
@@ -46,7 +48,34 @@ function closeCommand(args) {
|
|
|
46
48
|
frontmatter.id === normalizedInput ||
|
|
47
49
|
file.includes(normalizedInput)
|
|
48
50
|
) {
|
|
51
|
+
// Clean up worktree if one exists
|
|
52
|
+
if (frontmatter.worktree_path) {
|
|
53
|
+
const wtPath = frontmatter.worktree_path;
|
|
54
|
+
if (fs.existsSync(wtPath)) {
|
|
55
|
+
try {
|
|
56
|
+
// Check for uncommitted changes in the worktree
|
|
57
|
+
const wtStatus = execSync(`git -C "${wtPath}" status --porcelain`, { encoding: 'utf-8' }).trim();
|
|
58
|
+
if (wtStatus && !forceFlag) {
|
|
59
|
+
console.error(`❌ Worktree at ${wtPath} has uncommitted changes.`);
|
|
60
|
+
console.error(` Use --force to remove it anyway, or commit/stash changes first.`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
removeWorktree(wtPath, forceFlag);
|
|
64
|
+
console.log(`🗑️ Removed worktree: ${wtPath}`);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.warn(`⚠️ Could not remove worktree: ${error.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Prune stale worktree entries
|
|
70
|
+
try {
|
|
71
|
+
execSync('git worktree prune', { stdio: 'ignore' });
|
|
72
|
+
} catch (error) {
|
|
73
|
+
// Ignore prune errors
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
49
77
|
frontmatter.status = "done";
|
|
78
|
+
delete frontmatter.worktree_path;
|
|
50
79
|
|
|
51
80
|
const updated = `---\n${yaml.dump(frontmatter)}---${content.split("---").slice(2).join("---")}`;
|
|
52
81
|
fs.writeFileSync(fullPath, updated, "utf-8");
|
|
@@ -54,6 +54,7 @@ function listCommand(args) {
|
|
|
54
54
|
priority: frontmatter.priority || "medium",
|
|
55
55
|
assignee: frontmatter.assignee || frontmatter.owner || "",
|
|
56
56
|
author: frontmatter.author || "",
|
|
57
|
+
worktree_path: frontmatter.worktree_path || "",
|
|
57
58
|
file
|
|
58
59
|
});
|
|
59
60
|
}
|
|
@@ -114,8 +115,7 @@ function listCommand(args) {
|
|
|
114
115
|
|
|
115
116
|
for (const ticket of filteredTickets) {
|
|
116
117
|
let statusColor = "";
|
|
117
|
-
|
|
118
|
-
// Add color based on status
|
|
118
|
+
|
|
119
119
|
switch (ticket.status) {
|
|
120
120
|
case "done":
|
|
121
121
|
statusColor = "\x1b[32m"; // Green
|
|
@@ -129,8 +129,8 @@ function listCommand(args) {
|
|
|
129
129
|
default:
|
|
130
130
|
statusColor = "\x1b[0m"; // Default
|
|
131
131
|
}
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
|
|
133
|
+
const wtIndicator = ticket.worktree_path ? " 🌿" : "";
|
|
134
134
|
const truncatedTitle = ticket.title.length > titleWidth - 3
|
|
135
135
|
? ticket.title.substring(0, titleWidth - 3) + "..."
|
|
136
136
|
: ticket.title;
|
|
@@ -141,13 +141,13 @@ function listCommand(args) {
|
|
|
141
141
|
statusColor + ticket.status.padEnd(statusWidth - 1) + "\x1b[0m"
|
|
142
142
|
}${"|"} ${
|
|
143
143
|
(ticket.assignee || "").padEnd(assigneeWidth - 1)
|
|
144
|
-
}${"|"} ${truncatedTitle}`
|
|
144
|
+
}${"|"} ${truncatedTitle}${wtIndicator}`
|
|
145
145
|
);
|
|
146
146
|
} else {
|
|
147
147
|
console.log(
|
|
148
148
|
`${ticket.id.padEnd(idWidth)}${"|"} ${
|
|
149
149
|
statusColor + ticket.status.padEnd(statusWidth - 1) + "\x1b[0m"
|
|
150
|
-
}${"|"} ${truncatedTitle}`
|
|
150
|
+
}${"|"} ${truncatedTitle}${wtIndicator}`
|
|
151
151
|
);
|
|
152
152
|
}
|
|
153
153
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
const SKILL_PACKAGE = 'vibedx/vibekit';
|
|
6
|
+
|
|
7
|
+
function runVibeLink() {
|
|
8
|
+
const hasVibeDir = fs.existsSync(path.join(process.cwd(), '.vibe'));
|
|
9
|
+
if (!hasVibeDir) {
|
|
10
|
+
console.log('\n💡 No .vibe/ directory found. Run "vibe init" then "vibe link" to complete setup.');
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
console.log('\n🔗 Running post-install: vibe link\n');
|
|
15
|
+
try {
|
|
16
|
+
execSync('vibe link', { stdio: 'inherit' });
|
|
17
|
+
} catch {
|
|
18
|
+
try {
|
|
19
|
+
execSync('npx @vibedx/vibekit link', { stdio: 'inherit' });
|
|
20
|
+
} catch {
|
|
21
|
+
console.log('\n💡 To complete setup, run: vibe link');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default function skillsCommand(args) {
|
|
27
|
+
const subcommand = args[0] || 'add';
|
|
28
|
+
|
|
29
|
+
if (subcommand === 'add' || subcommand === 'install') {
|
|
30
|
+
const target = args[1] || SKILL_PACKAGE;
|
|
31
|
+
console.log(`📦 Installing skill: ${target}\n`);
|
|
32
|
+
try {
|
|
33
|
+
execSync(`npx skills add ${target}`, { stdio: 'inherit' });
|
|
34
|
+
} catch {
|
|
35
|
+
console.error('\n❌ Failed to install skill. Make sure npx is available.');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (target === SKILL_PACKAGE) {
|
|
40
|
+
runVibeLink();
|
|
41
|
+
}
|
|
42
|
+
} else if (subcommand === 'remove' || subcommand === 'uninstall') {
|
|
43
|
+
const target = args[1] || SKILL_PACKAGE;
|
|
44
|
+
console.log(`🗑️ Removing skill: ${target}\n`);
|
|
45
|
+
try {
|
|
46
|
+
execSync(`npx skills remove ${target}`, { stdio: 'inherit' });
|
|
47
|
+
} catch {
|
|
48
|
+
console.error('\n❌ Failed to remove skill.');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
} else if (subcommand === 'list' || subcommand === 'ls') {
|
|
52
|
+
try {
|
|
53
|
+
execSync('npx skills list', { stdio: 'inherit' });
|
|
54
|
+
} catch {
|
|
55
|
+
console.error('❌ Failed to list skills.');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
console.log('Usage: vibe skills [command]\n');
|
|
60
|
+
console.log('Commands:');
|
|
61
|
+
console.log(' add [package] Install a skill (default: vibedx/vibekit)');
|
|
62
|
+
console.log(' remove [package] Remove a skill');
|
|
63
|
+
console.log(' list List installed skills');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -2,14 +2,18 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import yaml from 'js-yaml';
|
|
4
4
|
import { getTicketsDir, getConfig, createSlug } from '../../utils/index.js';
|
|
5
|
-
import {
|
|
6
|
-
isGitRepository,
|
|
7
|
-
getCurrentBranch,
|
|
8
|
-
branchExistsLocally,
|
|
5
|
+
import {
|
|
6
|
+
isGitRepository,
|
|
7
|
+
getCurrentBranch,
|
|
8
|
+
branchExistsLocally,
|
|
9
9
|
branchExistsRemotely,
|
|
10
10
|
createAndCheckoutBranch,
|
|
11
11
|
checkoutBranch,
|
|
12
|
-
getGitStatus
|
|
12
|
+
getGitStatus,
|
|
13
|
+
getRepoName,
|
|
14
|
+
getWorktreePath,
|
|
15
|
+
createWorktree,
|
|
16
|
+
createWorktreeExistingBranch
|
|
13
17
|
} from '../../utils/git.js';
|
|
14
18
|
|
|
15
19
|
/**
|
|
@@ -33,14 +37,17 @@ function startCommand(args) {
|
|
|
33
37
|
let ticketId = args[0];
|
|
34
38
|
let baseBranch = null;
|
|
35
39
|
let updateStatus = true;
|
|
36
|
-
|
|
40
|
+
let useWorktree = false;
|
|
41
|
+
|
|
37
42
|
// Process additional arguments
|
|
38
43
|
for (let i = 1; i < args.length; i++) {
|
|
39
44
|
if (args[i] === '--base' && i + 1 < args.length) {
|
|
40
45
|
baseBranch = args[i + 1];
|
|
41
|
-
i++;
|
|
46
|
+
i++;
|
|
42
47
|
} else if (args[i] === '--update-status' || args[i] === '-u') {
|
|
43
48
|
updateStatus = true;
|
|
49
|
+
} else if (args[i] === '--worktree' || args[i] === '-w') {
|
|
50
|
+
useWorktree = true;
|
|
44
51
|
}
|
|
45
52
|
}
|
|
46
53
|
|
|
@@ -111,70 +118,126 @@ function startCommand(args) {
|
|
|
111
118
|
? `${branchPrefix}${slug}`
|
|
112
119
|
: `${branchPrefix}${ticketId}-${slug}`;
|
|
113
120
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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}`);
|
|
121
|
+
if (useWorktree) {
|
|
122
|
+
const repoName = getRepoName();
|
|
123
|
+
const worktreePath = getWorktreePath(repoName, branchName);
|
|
124
|
+
|
|
125
|
+
if (fs.existsSync(worktreePath)) {
|
|
126
|
+
console.log(`🔍 Worktree already exists at: ${worktreePath}`);
|
|
127
|
+
console.log(`✅ Ready to work in: ${worktreePath}`);
|
|
141
128
|
} else {
|
|
142
|
-
|
|
143
|
-
|
|
129
|
+
const branchExistsLocal = branchExistsLocally(branchName);
|
|
130
|
+
const branchExistsRemote = branchExistsRemotely(branchName);
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
if (branchExistsLocal || branchExistsRemote) {
|
|
134
|
+
console.log(`🔍 Creating worktree for existing branch: ${branchName}`);
|
|
135
|
+
createWorktreeExistingBranch(worktreePath, branchName);
|
|
136
|
+
} else {
|
|
137
|
+
console.log(`🔍 Creating worktree with new branch: ${branchName}`);
|
|
138
|
+
createWorktree(worktreePath, branchName, baseBranch);
|
|
139
|
+
}
|
|
140
|
+
console.log(`✅ Worktree created at: ${worktreePath}`);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error(`❌ Failed to create worktree: ${error.message}`);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
144
145
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// Update ticket status if requested
|
|
148
|
-
if (updateStatus) {
|
|
146
|
+
|
|
147
|
+
// Store worktree_path in ticket frontmatter
|
|
149
148
|
try {
|
|
150
|
-
|
|
151
|
-
const ticketContent = fs.readFileSync(ticketPath, 'utf-8');
|
|
152
|
-
|
|
153
|
-
// Get current timestamp in ISO format
|
|
149
|
+
const currentContent = fs.readFileSync(ticketPath, 'utf-8');
|
|
154
150
|
const now = new Date().toISOString();
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
.replace(/^
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
151
|
+
let updatedContent = currentContent;
|
|
152
|
+
|
|
153
|
+
if (updatedContent.match(/^worktree_path: .+$/m)) {
|
|
154
|
+
updatedContent = updatedContent.replace(/^worktree_path: .+$/m, `worktree_path: "${worktreePath}"`);
|
|
155
|
+
} else {
|
|
156
|
+
updatedContent = updatedContent.replace(/^(updated_at: .+)$/m, `$1\nworktree_path: "${worktreePath}"`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (updateStatus) {
|
|
160
|
+
updatedContent = updatedContent
|
|
161
|
+
.replace(/^status: (.+)$/m, 'status: in_progress')
|
|
162
|
+
.replace(/^updated_at: (.+)$/m, `updated_at: "${now}"`);
|
|
163
|
+
}
|
|
164
|
+
|
|
162
165
|
fs.writeFileSync(ticketPath, updatedContent, 'utf-8');
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
+
if (updateStatus) {
|
|
167
|
+
console.log(`✅ Updated ticket status to: in_progress`);
|
|
168
|
+
}
|
|
166
169
|
} catch (error) {
|
|
167
|
-
console.error(`❌ Failed to update ticket
|
|
170
|
+
console.error(`❌ Failed to update ticket: ${error.message}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
console.log('');
|
|
174
|
+
console.log(`🎯 Now working on: ${ticketId} - ${title}`);
|
|
175
|
+
console.log(`🌿 Branch: ${branchName}`);
|
|
176
|
+
console.log(`📂 Worktree: ${worktreePath}`);
|
|
177
|
+
console.log('');
|
|
178
|
+
console.log('To start working in the worktree:');
|
|
179
|
+
console.log(` cd ${worktreePath}`);
|
|
180
|
+
console.log('');
|
|
181
|
+
console.log('💡 Run `npm install` in the worktree to install dependencies.');
|
|
182
|
+
} else {
|
|
183
|
+
// Check if there are uncommitted changes
|
|
184
|
+
const gitStatus = getGitStatus();
|
|
185
|
+
if (gitStatus) {
|
|
186
|
+
console.warn('⚠️ You have uncommitted changes. Stash or commit them before switching branches.');
|
|
187
|
+
console.log('');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check if the branch already exists
|
|
191
|
+
const branchExistsLocal = branchExistsLocally(branchName);
|
|
192
|
+
const branchExistsRemote = branchExistsRemotely(branchName);
|
|
193
|
+
|
|
194
|
+
if (branchExistsLocal || branchExistsRemote) {
|
|
195
|
+
console.log(`🔍 Branch ${branchName} already exists.`);
|
|
196
|
+
|
|
197
|
+
if (checkoutBranch(branchName)) {
|
|
198
|
+
console.log(`✅ Switched to branch: ${branchName}`);
|
|
199
|
+
} else {
|
|
200
|
+
console.error(`❌ Failed to switch to branch: ${branchName}`);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
console.log(`🔍 Creating new branch: ${branchName}`);
|
|
205
|
+
|
|
206
|
+
if (createAndCheckoutBranch(branchName, baseBranch)) {
|
|
207
|
+
console.log(`✅ Created and switched to branch: ${branchName}`);
|
|
208
|
+
} else {
|
|
209
|
+
console.error(`❌ Failed to create branch: ${branchName}`);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Update ticket status if requested
|
|
215
|
+
if (updateStatus) {
|
|
216
|
+
try {
|
|
217
|
+
const currentContent = fs.readFileSync(ticketPath, 'utf-8');
|
|
218
|
+
const now = new Date().toISOString();
|
|
219
|
+
|
|
220
|
+
let updatedContent = currentContent
|
|
221
|
+
.replace(/^status: (.+)$/m, 'status: in_progress')
|
|
222
|
+
.replace(/^updated_at: (.+)$/m, `updated_at: ${now}`);
|
|
223
|
+
|
|
224
|
+
fs.writeFileSync(ticketPath, updatedContent, 'utf-8');
|
|
225
|
+
|
|
226
|
+
console.log(`✅ Updated ticket status to: in_progress`);
|
|
227
|
+
console.log(`✅ Updated timestamp to: ${now}`);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.error(`❌ Failed to update ticket status: ${error.message}`);
|
|
230
|
+
}
|
|
168
231
|
}
|
|
232
|
+
|
|
233
|
+
// Summary
|
|
234
|
+
console.log('');
|
|
235
|
+
console.log(`🎯 Now working on: ${ticketId} - ${title}`);
|
|
236
|
+
console.log(`🌿 Branch: ${branchName}`);
|
|
237
|
+
console.log('');
|
|
238
|
+
console.log('To push this branch to remote:');
|
|
239
|
+
console.log(` git push -u origin ${branchName}`);
|
|
169
240
|
}
|
|
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
241
|
}
|
|
179
242
|
|
|
180
243
|
export default startCommand;
|
package/src/utils/git.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execSync } from 'child_process';
|
|
2
2
|
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
3
4
|
import path from 'path';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -134,6 +135,106 @@ function getGitStatus() {
|
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
function getRepoRoot() {
|
|
139
|
+
try {
|
|
140
|
+
return execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function getMainWorktreeRoot() {
|
|
147
|
+
try {
|
|
148
|
+
const worktrees = execSync('git worktree list --porcelain', { encoding: 'utf-8' });
|
|
149
|
+
const firstWorktreeLine = worktrees.split('\n').find(line => line.startsWith('worktree '));
|
|
150
|
+
if (firstWorktreeLine) {
|
|
151
|
+
return firstWorktreeLine.replace('worktree ', '');
|
|
152
|
+
}
|
|
153
|
+
return getRepoRoot();
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return getRepoRoot();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getRepoName() {
|
|
160
|
+
try {
|
|
161
|
+
const remoteUrl = execSync('git remote get-url origin', { encoding: 'utf-8' }).trim();
|
|
162
|
+
// Handle git@github.com:org/repo.git
|
|
163
|
+
let match = remoteUrl.match(/[:/]([^/]+?)(?:\.git)?$/);
|
|
164
|
+
if (match) return match[1];
|
|
165
|
+
} catch (error) {
|
|
166
|
+
// No remote — fall through
|
|
167
|
+
}
|
|
168
|
+
const root = getRepoRoot();
|
|
169
|
+
return root ? path.basename(root) : 'unknown-repo';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function getWorktreesDir(repoName) {
|
|
173
|
+
const name = repoName || getRepoName();
|
|
174
|
+
return path.join(os.homedir(), '.vibekit', 'worktrees', name);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function sanitizeBranchForPath(branch) {
|
|
178
|
+
return branch.replace(/\//g, '--');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function getWorktreePath(repoName, branch) {
|
|
182
|
+
return path.join(getWorktreesDir(repoName), sanitizeBranchForPath(branch));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function createWorktree(worktreePath, branch, baseBranch) {
|
|
186
|
+
const base = baseBranch || getDefaultBaseBranch();
|
|
187
|
+
try {
|
|
188
|
+
execSync(`git fetch origin ${base}`, { stdio: 'ignore' });
|
|
189
|
+
} catch (error) {
|
|
190
|
+
// Ignore fetch errors
|
|
191
|
+
}
|
|
192
|
+
fs.mkdirSync(path.dirname(worktreePath), { recursive: true });
|
|
193
|
+
try {
|
|
194
|
+
execSync(`git worktree add "${worktreePath}" -b ${branch} origin/${base}`, { stdio: 'pipe' });
|
|
195
|
+
} catch (error) {
|
|
196
|
+
execSync(`git worktree add "${worktreePath}" -b ${branch} ${base}`, { stdio: 'pipe' });
|
|
197
|
+
}
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function createWorktreeExistingBranch(worktreePath, branch) {
|
|
202
|
+
fs.mkdirSync(path.dirname(worktreePath), { recursive: true });
|
|
203
|
+
execSync(`git worktree add "${worktreePath}" ${branch}`, { stdio: 'pipe' });
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function removeWorktree(worktreePath, force = false) {
|
|
208
|
+
const forceFlag = force ? ' --force' : '';
|
|
209
|
+
execSync(`git worktree remove "${worktreePath}"${forceFlag}`, { stdio: 'pipe' });
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function listWorktrees() {
|
|
214
|
+
try {
|
|
215
|
+
const output = execSync('git worktree list --porcelain', { encoding: 'utf-8' });
|
|
216
|
+
const worktrees = [];
|
|
217
|
+
let current = {};
|
|
218
|
+
for (const line of output.split('\n')) {
|
|
219
|
+
if (line.startsWith('worktree ')) {
|
|
220
|
+
if (current.path) worktrees.push(current);
|
|
221
|
+
current = { path: line.replace('worktree ', '') };
|
|
222
|
+
} else if (line.startsWith('branch refs/heads/')) {
|
|
223
|
+
current.branch = line.replace('branch refs/heads/', '');
|
|
224
|
+
} else if (line === 'bare') {
|
|
225
|
+
current.bare = true;
|
|
226
|
+
} else if (line === '') {
|
|
227
|
+
if (current.path) worktrees.push(current);
|
|
228
|
+
current = {};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (current.path) worktrees.push(current);
|
|
232
|
+
return worktrees;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
137
238
|
export {
|
|
138
239
|
isGitRepository,
|
|
139
240
|
getCurrentBranch,
|
|
@@ -142,5 +243,15 @@ export {
|
|
|
142
243
|
getDefaultBaseBranch,
|
|
143
244
|
createAndCheckoutBranch,
|
|
144
245
|
checkoutBranch,
|
|
145
|
-
getGitStatus
|
|
246
|
+
getGitStatus,
|
|
247
|
+
getRepoRoot,
|
|
248
|
+
getMainWorktreeRoot,
|
|
249
|
+
getRepoName,
|
|
250
|
+
getWorktreesDir,
|
|
251
|
+
getWorktreePath,
|
|
252
|
+
sanitizeBranchForPath,
|
|
253
|
+
createWorktree,
|
|
254
|
+
createWorktreeExistingBranch,
|
|
255
|
+
removeWorktree,
|
|
256
|
+
listWorktrees
|
|
146
257
|
};
|
package/src/utils/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import yaml from 'js-yaml';
|
|
4
|
+
import { getRepoRoot, getMainWorktreeRoot } from './git.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Get the path to the tickets directory from config or use default
|
|
@@ -9,9 +10,26 @@ import yaml from 'js-yaml';
|
|
|
9
10
|
* @returns {string} Absolute path to the tickets directory
|
|
10
11
|
* @throws {Error} Logs error but doesn't throw - returns default path
|
|
11
12
|
*/
|
|
13
|
+
function getProjectRoot() {
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
if (fs.existsSync(path.join(cwd, '.vibe'))) {
|
|
16
|
+
return cwd;
|
|
17
|
+
}
|
|
18
|
+
// Only resolve to main worktree when cwd IS a worktree root (not a subdirectory)
|
|
19
|
+
const repoRoot = getRepoRoot();
|
|
20
|
+
if (repoRoot && repoRoot === cwd) {
|
|
21
|
+
const mainRoot = getMainWorktreeRoot();
|
|
22
|
+
if (mainRoot && mainRoot !== cwd && fs.existsSync(path.join(mainRoot, '.vibe'))) {
|
|
23
|
+
return mainRoot;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return cwd;
|
|
27
|
+
}
|
|
28
|
+
|
|
12
29
|
function getTicketsDir() {
|
|
13
|
-
const
|
|
14
|
-
|
|
30
|
+
const root = getProjectRoot();
|
|
31
|
+
const configPath = path.join(root, '.vibe', 'config.yml');
|
|
32
|
+
let ticketDir = path.join(root, '.vibe', 'tickets');
|
|
15
33
|
|
|
16
34
|
try {
|
|
17
35
|
if (fs.existsSync(configPath)) {
|
|
@@ -19,7 +37,7 @@ function getTicketsDir() {
|
|
|
19
37
|
const config = yaml.load(configContent) || {};
|
|
20
38
|
|
|
21
39
|
if (config.tickets?.path && typeof config.tickets.path === 'string') {
|
|
22
|
-
const customPath = path.resolve(
|
|
40
|
+
const customPath = path.resolve(root, config.tickets.path);
|
|
23
41
|
ticketDir = customPath;
|
|
24
42
|
}
|
|
25
43
|
}
|
|
@@ -40,7 +58,8 @@ function getTicketsDir() {
|
|
|
40
58
|
* console.log(config.tickets?.path); // Access tickets path
|
|
41
59
|
*/
|
|
42
60
|
function getConfig() {
|
|
43
|
-
const
|
|
61
|
+
const root = getProjectRoot();
|
|
62
|
+
const configPath = path.join(root, '.vibe', 'config.yml');
|
|
44
63
|
let config = {};
|
|
45
64
|
|
|
46
65
|
try {
|
|
@@ -180,10 +199,12 @@ function createFullSlug(ticketId, slugText) {
|
|
|
180
199
|
* @returns {string} Absolute path to the config.yml file
|
|
181
200
|
*/
|
|
182
201
|
function getConfigPath() {
|
|
183
|
-
|
|
202
|
+
const root = getProjectRoot();
|
|
203
|
+
return path.join(root, '.vibe', 'config.yml');
|
|
184
204
|
}
|
|
185
205
|
|
|
186
206
|
export {
|
|
207
|
+
getProjectRoot,
|
|
187
208
|
getTicketsDir,
|
|
188
209
|
getConfig,
|
|
189
210
|
getConfigPath,
|