chore(dev): add prepare worktree command to mage

This commit is contained in:
kolaente
2026-01-24 18:32:23 +01:00
parent 4df8da549e
commit 5050cd7162
3 changed files with 167 additions and 85 deletions

View File

@@ -1,82 +0,0 @@
---
name: prepare-workspace-for-plan
description: Use when you have a plan file ready and need to create an isolated git worktree for implementation - creates worktree in parent directory following project conventions and moves the plan file
---
# Prepare Workspace for Plan
Use this skill when you have created or refined a plan and need to set up an isolated workspace for implementation.
## When to Use
- After creating/finalizing a plan in the `plans/` directory
- Before starting implementation of a multi-phase plan
- When you need an isolated branch for a feature or fix
## Prerequisites
- A plan file exists in the current workspace's `plans/` directory
- You are in a git repository that supports worktrees
- The parent directory is the standard location for worktrees (e.g., `/path/to/vikunja/`)
## Steps
### 1. Determine Workspace Name
Choose a name following the project convention:
- `fix-<description>` for bug fixes
- `feat-<description>` for new features
The name should be kebab-case and descriptive but concise.
### 2. Create the Git Worktree
```bash
# From the current workspace (e.g., main/)
git worktree add ../<workspace-name> -b <branch-name>
```
The branch name should match the workspace name.
### 3. Create Plans Directory and Move Plan
```bash
mkdir -p ../<workspace-name>/plans
mv plans/<plan-file>.md ../<workspace-name>/plans/
```
### 4. Verify Structure
```bash
ls -la ../<workspace-name>/plans/
```
## Example
```bash
# Create worktree for position healing fix
git worktree add ../fix-position-healing -b fix-position-healing
# Move the plan
mkdir -p ../fix-position-healing/plans
mv plans/positioning-fixes-detection.md ../fix-position-healing/plans/
```
## Result
After completion, you'll have:
```
parent-directory/
├── main/ # Original workspace
├── <new-workspace>/ # New worktree
│ └── plans/
│ └── <plan-file>.md # Your plan
└── ... # Other existing worktrees
```
## Notes
- The new worktree shares git history with main but has its own working directory
- Changes in the new worktree won't affect main until merged
- Plans are not committed to git (see `.gitignore`)
- Remember to switch to the new workspace directory to begin implementation

View File

@@ -11,13 +11,49 @@ The project consists of:
- `desktop/` Electron wrapper application - `desktop/` Electron wrapper application
- `docs/` Documentation website - `docs/` Documentation website
## Plans ## Plans and Worktrees
When the user asks you to create a plan to fix or implement something: When the user asks you to create a plan to fix or implement something:
- ALWAYS write that plan to the plans/ directory on the root of the repo. - ALWAYS write that plan to the plans/ directory on the root of the repo.
- NEVER commit plans to git - NEVER commit plans to git
- Give the plan a descriptive name - Give the plan a descriptive name using kebab-case (e.g., `fix-position-healing.md`, `feat-new-feature.md`)
### Preparing a Worktree for Implementation
When the user tells you to prepare a worktree for a plan, use the mage command to set up an isolated workspace:
```bash
mage dev:prepare-worktree <name> <plan-path>
```
**Arguments:**
- `<name>` - Required. Becomes both the folder name and branch name. Use conventions like `fix-<description>` for bug fixes or `feat-<description>` for new features.
- `<plan-path>` - Required. Path to a plan file (relative to repo root) that will be copied to the new worktree's `plans/` directory. Pass `""` to skip copying a plan.
This will initialize a new worktree in the parent directory and copy some files over.
**Example:**
```bash
# Create worktree for a bug fix with a plan
mage dev:prepare-worktree fix-position-healing plans/fix-position-healing.md
# Create worktree for a new feature without a plan
mage dev:prepare-worktree feat-dark-mode ""
```
**Result:**
```
parent-directory/
├── main/ # Original workspace
├── fix-position-healing/ # New worktree
│ ├── config.yml # With updated rootpath
│ └── plans/
│ └── fix-position-healing.md
└── ...
```
After creation, tell the user where they can find the new worktree.
## Development Commands ## Development Commands
@@ -37,8 +73,9 @@ When the user asks you to create a plan to fix or implement something:
-Development helpers under the `dev` namespace: -Development helpers under the `dev` namespace:
- **Migration**: `mage dev:make-migration <StructName>` - Creates new database migration. If you omit `<StructName>`, the command will prompt for it. - **Migration**: `mage dev:make-migration <StructName>` - Creates new database migration. If you omit `<StructName>`, the command will prompt for it.
- **Event**: `mage dev:make-event` - Create an event type - **Event**: `mage dev:make-event` - Create an event type
- **Listener**: `mage dev:make-listener` - Create an event listener - **Listener**: `mage dev:make-listener` - Create an event listener
- **Notification**: `mage dev:make-notification` - Create a notification skeleton - **Notification**: `mage dev:make-notification` - Create a notification skeleton
- **Prepare Worktree**: `mage dev:prepare-worktree <name> <plan-path>` - Creates a new git worktree in `../` with the given name as folder and branch. Copies a plan file if provided (pass `""` to skip). Copies `config.yml` with updated rootpath and initializes the frontend.
### Frontend (Vue.js) ### Frontend (Vue.js)
Navigate to `frontend/` directory: Navigate to `frontend/` directory:

View File

@@ -72,6 +72,7 @@ var (
"dev:make-event": Dev.MakeEvent, "dev:make-event": Dev.MakeEvent,
"dev:make-listener": Dev.MakeListener, "dev:make-listener": Dev.MakeListener,
"dev:make-notification": Dev.MakeNotification, "dev:make-notification": Dev.MakeNotification,
"dev:prepare-worktree": Dev.PrepareWorktree,
"plugins:build": Plugins.Build, "plugins:build": Plugins.Build,
"lint": Check.Golangci, "lint": Check.Golangci,
"lint:fix": Check.GolangciFix, "lint:fix": Check.GolangciFix,
@@ -1394,6 +1395,132 @@ func (Generate) ConfigYAML(commented bool) {
generateConfigYAMLFromJSON(DefaultConfigYAMLSamplePath, commented) generateConfigYAMLFromJSON(DefaultConfigYAMLSamplePath, commented)
} }
// PrepareWorktree creates a new git worktree for development.
// The first argument is the name, which becomes both the folder name and branch name.
// The second argument is a path to a plan file that will be copied to the new worktree (pass "" to skip).
// The worktree is created in the parent directory (../).
// It also copies the current config.yml with an updated rootpath, and initializes the frontend.
func (Dev) PrepareWorktree(name string, planPath string) error {
if name == "" {
return fmt.Errorf("name is required: mage dev:prepare-worktree <name> <plan-path>")
}
// Get the parent directory path
parentDir := filepath.Dir(RootPath)
worktreePath := filepath.Join(parentDir, name)
fmt.Printf("Creating worktree at %s with branch %s...\n", worktreePath, name)
// Create the git worktree
cmd := exec.Command("git", "worktree", "add", worktreePath, "-b", name)
cmd.Dir = RootPath
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to create worktree: %w", err)
}
printSuccess("Worktree created successfully!")
// Copy and modify config.yml
configSrc := filepath.Join(RootPath, "config.yml")
configDst := filepath.Join(worktreePath, "config.yml")
if _, err := os.Stat(configSrc); err == nil {
configContent, err := os.ReadFile(configSrc)
if err != nil {
return fmt.Errorf("failed to read config.yml: %w", err)
}
// Replace the rootpath value
re := regexp.MustCompile(`(?m)^(\s*rootpath:\s*)"[^"]*"`)
newConfig := re.ReplaceAllString(string(configContent), `${1}"`+worktreePath+`"`)
// Also handle unquoted rootpath values
re2 := regexp.MustCompile(`(?m)^(\s*rootpath:\s*)(/[^\s\n]+)`)
newConfig = re2.ReplaceAllString(newConfig, `${1}"`+worktreePath+`"`)
if err := os.WriteFile(configDst, []byte(newConfig), 0644); err != nil {
return fmt.Errorf("failed to write config.yml: %w", err)
}
printSuccess("Config copied with updated rootpath!")
} else {
fmt.Println("Warning: config.yml not found, skipping config copy")
}
// Copy .claude/settings.local.json if it exists
claudeSettingsSrc := filepath.Join(RootPath, ".claude", "settings.local.json")
if _, err := os.Stat(claudeSettingsSrc); err == nil {
claudeDir := filepath.Join(worktreePath, ".claude")
if err := os.MkdirAll(claudeDir, 0755); err != nil {
return fmt.Errorf("failed to create .claude directory: %w", err)
}
claudeSettingsDst := filepath.Join(claudeDir, "settings.local.json")
if err := copyFile(claudeSettingsSrc, claudeSettingsDst); err != nil {
return fmt.Errorf("failed to copy .claude/settings.local.json: %w", err)
}
printSuccess("Claude settings copied!")
}
// Copy plan file if provided
if planPath != "" {
planPath = strings.TrimSpace(planPath)
if planPath != "" {
// Create plans directory in the new worktree
plansDir := filepath.Join(worktreePath, "plans")
if err := os.MkdirAll(plansDir, 0755); err != nil {
return fmt.Errorf("failed to create plans directory: %w", err)
}
// Determine source path (relative to RootPath or absolute)
srcPlanPath := planPath
if !filepath.IsAbs(planPath) {
srcPlanPath = filepath.Join(RootPath, planPath)
}
if _, err := os.Stat(srcPlanPath); err != nil {
return fmt.Errorf("plan file not found: %s", srcPlanPath)
}
dstPlanPath := filepath.Join(plansDir, filepath.Base(planPath))
if err := copyFile(srcPlanPath, dstPlanPath); err != nil {
return fmt.Errorf("failed to copy plan file: %w", err)
}
printSuccess("Plan file copied to %s!", dstPlanPath)
}
}
// Initialize frontend
fmt.Println("Initializing frontend...")
frontendDir := filepath.Join(worktreePath, "frontend")
// Run pnpm install
pnpmCmd := exec.Command("pnpm", "i")
pnpmCmd.Dir = frontendDir
pnpmCmd.Stdout = os.Stdout
pnpmCmd.Stderr = os.Stderr
if err := pnpmCmd.Run(); err != nil {
return fmt.Errorf("failed to run pnpm install: %w", err)
}
// Run patch-sass-embedded (shell alias from devenv)
patchCmd := exec.Command("bash", "-ic", "patch-sass-embedded")
patchCmd.Dir = frontendDir
patchCmd.Stdout = os.Stdout
patchCmd.Stderr = os.Stderr
if err := patchCmd.Run(); err != nil {
// patch-sass-embedded might not be critical, just warn
fmt.Printf("Warning: patch-sass-embedded failed: %v\n", err)
}
printSuccess("Frontend initialized!")
printSuccess("\nWorktree ready at: %s", worktreePath)
printSuccess("Branch: %s", name)
fmt.Println("\nTo start working:")
fmt.Printf(" cd %s\n", worktreePath)
return nil
}
type Plugins mg.Namespace type Plugins mg.Namespace
// Build compiles a Go plugin at the provided path. // Build compiles a Go plugin at the provided path.