@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibedx/vibekit",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "A powerful CLI tool for managing development tickets and project workflows",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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 close <id>` | Mark ticket done |
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
- // Format each row
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++; // Skip the next argument as it's the base branch
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
- // Check if there are uncommitted changes
115
- const gitStatus = getGitStatus();
116
- if (gitStatus) {
117
- console.warn('⚠️ You have uncommitted changes. Stash or commit them before switching branches.');
118
- console.log('');
119
- }
120
-
121
- // Check if the branch already exists
122
- const branchExistsLocal = branchExistsLocally(branchName);
123
- const branchExistsRemote = branchExistsRemotely(branchName);
124
-
125
- if (branchExistsLocal || branchExistsRemote) {
126
- console.log(`🔍 Branch ${branchName} already exists.`);
127
-
128
- // Checkout the existing branch
129
- if (checkoutBranch(branchName)) {
130
- console.log(`✅ Switched to branch: ${branchName}`);
131
- } else {
132
- console.error(`❌ Failed to switch to branch: ${branchName}`);
133
- process.exit(1);
134
- }
135
- } else {
136
- console.log(`🔍 Creating new branch: ${branchName}`);
137
-
138
- // Create and checkout the new branch
139
- if (createAndCheckoutBranch(branchName, baseBranch)) {
140
- console.log(`✅ Created and switched to branch: ${branchName}`);
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
- console.error(`❌ Failed to create branch: ${branchName}`);
143
- process.exit(1);
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
- // Read the ticket file
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
- // Update the status to in_progress and update the timestamp
157
- let updatedContent = ticketContent
158
- .replace(/^status: (.+)$/m, 'status: in_progress')
159
- .replace(/^updated_at: (.+)$/m, `updated_at: ${now}`);
160
-
161
- // Write the updated content back to the file
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
- console.log(`✅ Updated ticket status to: in_progress`);
165
- console.log(`✅ Updated timestamp to: ${now}`);
166
+ if (updateStatus) {
167
+ console.log(`✅ Updated ticket status to: in_progress`);
168
+ }
166
169
  } catch (error) {
167
- console.error(`❌ Failed to update ticket status: ${error.message}`);
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
  };
@@ -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 configPath = path.join(process.cwd(), '.vibe', 'config.yml');
14
- let ticketDir = path.join(process.cwd(), '.vibe', 'tickets');
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(process.cwd(), config.tickets.path);
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 configPath = path.join(process.cwd(), '.vibe', 'config.yml');
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
- return path.join(process.cwd(), '.vibe', 'config.yml');
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,