@vibedx/vibekit 0.6.1 → 0.6.3

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/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,13 +1,14 @@
1
1
  {
2
2
  "name": "@vibedx/vibekit",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "A powerful CLI tool for managing development tickets and project workflows",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "files": [
8
8
  "index.js",
9
9
  "src/",
10
- "assets/"
10
+ "assets/",
11
+ "skills/"
11
12
  ],
12
13
  "bin": {
13
14
  "vibe": "index.js"
@@ -0,0 +1,23 @@
1
+ # VibeKit Skills
2
+
3
+ Agent skills for the [open agent skills ecosystem](https://skills.sh).
4
+
5
+ ## Installing
6
+
7
+ ```bash
8
+ npx skills add vibedx/vibekit
9
+ ```
10
+
11
+ Or install directly:
12
+
13
+ ```bash
14
+ npx skills add https://github.com/vibedx/vibekit/tree/main/skills/vibekit
15
+ ```
16
+
17
+ ## Available Skills
18
+
19
+ ### vibekit
20
+
21
+ Ticket-driven development workflow. Teaches AI coding agents how to use the vibekit CLI to create scoped tickets before writing code, track work through git branches, and maintain living documentation.
22
+
23
+ See [skills/vibekit/SKILL.md](./vibekit/SKILL.md) for full details.
@@ -0,0 +1,199 @@
1
+ ---
2
+ name: vibekit
3
+ description: Ticket-driven development workflow for AI coding agents using VibeKit CLI. Use when the user asks to create a task, feature, bug fix, or ticket; mentions "vibe new", "vibe list", or vibekit commands; or wants structured, scoped work breakdown. Triggers on phrases like "add a ticket", "track this task", "break this down", or "start a new feature". Helps agents create focused tickets with clear acceptance criteria before writing code.
4
+ license: MIT
5
+ ---
6
+
7
+ # VibeKit — Ticket-Driven Development
8
+
9
+ VibeKit is a CLI for managing development tickets and AI-assisted workflows. This skill teaches agents how to use vibekit properly — create tickets before coding, work through them systematically, and keep tickets as living documentation.
10
+
11
+ ## 🔴 MANDATORY RULE: Always Create a Ticket First
12
+
13
+ **Before writing ANY code on a repo that has a `.vibe/` directory, you MUST create a vibekit ticket.** No exceptions for features, bug fixes, refactors, or multi-step tasks.
14
+
15
+ This is not a suggestion — it's the core workflow. If the user asks you to "add X", "fix Y", or "build Z", your first action should be:
16
+
17
+ ```bash
18
+ vibe new "descriptive title" --assignee <your-username> --priority <level> -n
19
+ ```
20
+
21
+ Then read the ticket, plan the work, and `vibe start TKT-XXX` before touching any code.
22
+
23
+ **Only skip the ticket for truly trivial changes**: typo fixes, single-line config tweaks, or answering a quick question.
24
+
25
+ ## Why This Matters
26
+
27
+ Tickets break work into scoped, focused chunks that reduce AI drift and create documentation as a side effect. They also give humans a clear trail of what was done and why — critical for collaboration between humans and agents.
28
+
29
+ ```bash
30
+ # 1. Create a ticket
31
+ vibe new "Add user authentication" --assignee <username> --priority high -n
32
+
33
+ # 2. Start working (creates git branch feature/TKT-XXX-slug)
34
+ vibe start TKT-001
35
+
36
+ # 3. Implement the work, commit with ticket reference
37
+ git commit -m "TKT-001: add login endpoint"
38
+
39
+ # 4. Close when done
40
+ vibe close TKT-001
41
+ ```
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ npm install -g @vibedx/vibekit
47
+ ```
48
+
49
+ Then in any project:
50
+ ```bash
51
+ vibe init # Creates .vibe/ directory with config, team, templates
52
+ ```
53
+
54
+ ## Core Commands
55
+
56
+ | Command | Purpose |
57
+ |---------|---------|
58
+ | `vibe init` | Initialize vibekit in a project |
59
+ | `vibe new "title"` | Create a ticket |
60
+ | `vibe list` | List all tickets |
61
+ | `vibe start <id>` | Start work (creates git branch) |
62
+ | `vibe start <id> --worktree` | Start work in a separate worktree |
63
+ | `vibe close <id>` | Mark ticket done (cleans up worktree if any) |
64
+ | `vibe lint` | Validate ticket format |
65
+ | `vibe lint --fix` | Auto-fix missing sections |
66
+ | `vibe refine <id>` | AI-enhance ticket details |
67
+ | `vibe team` | Manage team members |
68
+
69
+ ## Creating Tickets (for AI agents)
70
+
71
+ **Always use `-n` / `--no-interactive` when creating tickets programmatically.** This skips the AI enhancement prompt which would otherwise block automation.
72
+
73
+ ```bash
74
+ vibe new "Fix login redirect loop" --assignee alice --priority high -n
75
+ vibe new "Add dark mode" --assignee bob --author alice -n
76
+ ```
77
+
78
+ ### Useful flags
79
+
80
+ - `--priority low|medium|high|critical` (default: medium)
81
+ - `--status open|in_progress|review|done` (default: open)
82
+ - `--assignee <username>` — who works on it
83
+ - `--author <username>` — who created it
84
+ - `-n` / `--no-interactive` — skip AI enhancement prompt
85
+
86
+ ## Ticket Structure
87
+
88
+ Tickets live in `.vibe/tickets/` as markdown files with YAML frontmatter:
89
+
90
+ ```markdown
91
+ ---
92
+ id: TKT-001
93
+ title: Add user authentication
94
+ slug: TKT-001-add-user-authentication
95
+ status: open
96
+ priority: high
97
+ assignee: "alice"
98
+ author: "bob"
99
+ created_at: 2026-04-11T10:00:00Z
100
+ updated_at: 2026-04-11T10:00:00Z
101
+ ---
102
+
103
+ ## Description
104
+ What needs to be done and why.
105
+
106
+ ## Acceptance Criteria
107
+ - [ ] Concrete checkbox 1
108
+ - [ ] Concrete checkbox 2
109
+
110
+ ## Implementation Notes
111
+ Technical details, file paths, API references.
112
+
113
+ ## Testing & Test Cases
114
+ Brief test scenarios.
115
+ ```
116
+
117
+ ## Filtering Tickets
118
+
119
+ ```bash
120
+ vibe list # All tickets
121
+ vibe list --status=open # Only open
122
+ vibe list --assignee=alice # Only alice's tickets
123
+ ```
124
+
125
+ ## Team Management
126
+
127
+ Teams are stored in `.vibe/team.yml`. Assignee values in tickets should match team member IDs.
128
+
129
+ ```bash
130
+ vibe team add alice --name "Alice" --github alice --slack U0ABC123 --role Engineer
131
+ vibe team # List members
132
+ vibe team show alice # Show one member
133
+ ```
134
+
135
+ ## When to Use This Skill
136
+
137
+ Apply vibekit workflow when:
138
+
139
+ - User asks to "add a feature", "fix a bug", "implement X", "build Y"
140
+ - Task is non-trivial (more than a one-line fix)
141
+ - Work touches multiple files or requires planning
142
+ - User mentions `vibe`, tickets, tracking, or task management
143
+ - Working in a project that has a `.vibe/` directory
144
+
145
+ **Don't force tickets for trivial changes** — typo fixes, config tweaks, or single-line edits can skip the ticket workflow.
146
+
147
+ ## Integration with Git
148
+
149
+ `vibe start TKT-001` automatically:
150
+ 1. Creates branch `feature/TKT-001-<slug>` (or configured prefix)
151
+ 2. Updates ticket status to `in_progress`
152
+ 3. Switches to the new branch
153
+
154
+ `vibe close TKT-001`:
155
+ 1. Updates ticket status to `done`
156
+ 2. Leaves the branch for manual merge/PR
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
+
179
+ ## Automation Pattern
180
+
181
+ For bots and automated agents working through tickets:
182
+
183
+ ```bash
184
+ # Find open tickets assigned to you
185
+ vibe list --assignee=mybotname --status=open
186
+
187
+ # For each ticket:
188
+ # 1. Read .vibe/tickets/TKT-XXX-*.md for full context
189
+ # 2. Do the work on branch opus/<ticket-id>-<description>
190
+ # 3. Commit changes
191
+ # 4. Close: vibe close TKT-XXX
192
+ # 5. Notify team
193
+ ```
194
+
195
+ ## Links
196
+
197
+ - Repo: https://github.com/vibedx/vibekit
198
+ - npm: https://www.npmjs.com/package/@vibedx/vibekit
199
+ - Issues: https://github.com/vibedx/vibekit/issues
@@ -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,