From 5050cd7162a7c359116a0aa15881526b6481fe75 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 24 Jan 2026 18:32:23 +0100 Subject: [PATCH] chore(dev): add prepare worktree command to mage --- .../prepare-workspace-for-plan/SKILL.md | 82 ----------- AGENTS.md | 43 +++++- magefile.go | 127 ++++++++++++++++++ 3 files changed, 167 insertions(+), 85 deletions(-) delete mode 100644 .claude/skills/prepare-workspace-for-plan/SKILL.md diff --git a/.claude/skills/prepare-workspace-for-plan/SKILL.md b/.claude/skills/prepare-workspace-for-plan/SKILL.md deleted file mode 100644 index 153da51c3..000000000 --- a/.claude/skills/prepare-workspace-for-plan/SKILL.md +++ /dev/null @@ -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-` for bug fixes -- `feat-` 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 ../ -b -``` - -The branch name should match the workspace name. - -### 3. Create Plans Directory and Move Plan - -```bash -mkdir -p ..//plans -mv plans/.md ..//plans/ -``` - -### 4. Verify Structure - -```bash -ls -la ..//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 worktree -│ └── plans/ -│ └── .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 diff --git a/AGENTS.md b/AGENTS.md index 741b2a27a..743eb1b04 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,13 +11,49 @@ The project consists of: - `desktop/` – Electron wrapper application - `docs/` – Documentation website -## Plans +## Plans and Worktrees 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. - 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 +``` + +**Arguments:** +- `` - Required. Becomes both the folder name and branch name. Use conventions like `fix-` for bug fixes or `feat-` for new features. +- `` - 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 @@ -37,8 +73,9 @@ When the user asks you to create a plan to fix or implement something: -Development helpers under the `dev` namespace: - **Migration**: `mage dev:make-migration ` - Creates new database migration. If you omit ``, the command will prompt for it. - **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 +- **Prepare Worktree**: `mage dev:prepare-worktree ` - 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) Navigate to `frontend/` directory: diff --git a/magefile.go b/magefile.go index 7bc0c79aa..3ad654244 100644 --- a/magefile.go +++ b/magefile.go @@ -72,6 +72,7 @@ var ( "dev:make-event": Dev.MakeEvent, "dev:make-listener": Dev.MakeListener, "dev:make-notification": Dev.MakeNotification, + "dev:prepare-worktree": Dev.PrepareWorktree, "plugins:build": Plugins.Build, "lint": Check.Golangci, "lint:fix": Check.GolangciFix, @@ -1394,6 +1395,132 @@ func (Generate) ConfigYAML(commented bool) { 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 ") + } + + // 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 // Build compiles a Go plugin at the provided path.