feat(crew): Provision .claude/commands/ for crew and polecat workspaces (gt-jhr85)
When adding a crew member with 'gt crew add' or spawning a polecat, provision the .claude/commands/ directory with standard slash commands like /handoff. This ensures all agents have Gas Town utilities even if the source repo does not have them tracked. Changes: - Add embedded commands templates (internal/templates/commands/) - Add ProvisionCommands() to templates package - Call ProvisionCommands from crew and polecat managers - Add gt doctor commands-provisioned check with --fix support
This commit is contained in:
@@ -5,12 +5,17 @@ import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
//go:embed roles/*.md.tmpl messages/*.md.tmpl
|
||||
var templateFS embed.FS
|
||||
|
||||
//go:embed commands/*.md
|
||||
var commandsFS embed.FS
|
||||
|
||||
// Templates manages role and message templates.
|
||||
type Templates struct {
|
||||
roleTemplates *template.Template
|
||||
@@ -148,3 +153,90 @@ func GetAllRoleTemplates() (map[string][]byte, error) {
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ProvisionCommands creates the .claude/commands/ directory with standard slash commands.
|
||||
// This ensures crew/polecat workspaces have the handoff command and other utilities
|
||||
// even if the source repo doesn't have them tracked.
|
||||
// If a command already exists, it is skipped (no overwrite).
|
||||
func ProvisionCommands(workspacePath string) error {
|
||||
entries, err := commandsFS.ReadDir("commands")
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading commands directory: %w", err)
|
||||
}
|
||||
|
||||
// Create .claude/commands/ directory
|
||||
commandsDir := filepath.Join(workspacePath, ".claude", "commands")
|
||||
if err := os.MkdirAll(commandsDir, 0755); err != nil {
|
||||
return fmt.Errorf("creating commands directory: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
destPath := filepath.Join(commandsDir, entry.Name())
|
||||
|
||||
// Skip if command already exists (don't overwrite user customizations)
|
||||
if _, err := os.Stat(destPath); err == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
content, err := commandsFS.ReadFile("commands/" + entry.Name())
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading %s: %w", entry.Name(), err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(destPath, content, 0644); err != nil {
|
||||
return fmt.Errorf("writing %s: %w", entry.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommandNames returns the list of embedded slash commands.
|
||||
func CommandNames() ([]string, error) {
|
||||
entries, err := commandsFS.ReadDir("commands")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading commands directory: %w", err)
|
||||
}
|
||||
|
||||
var names []string
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
names = append(names, entry.Name())
|
||||
}
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// HasCommands checks if a workspace has the .claude/commands/ directory provisioned.
|
||||
func HasCommands(workspacePath string) bool {
|
||||
commandsDir := filepath.Join(workspacePath, ".claude", "commands")
|
||||
info, err := os.Stat(commandsDir)
|
||||
return err == nil && info.IsDir()
|
||||
}
|
||||
|
||||
// MissingCommands returns the list of embedded commands missing from the workspace.
|
||||
func MissingCommands(workspacePath string) ([]string, error) {
|
||||
entries, err := commandsFS.ReadDir("commands")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading commands directory: %w", err)
|
||||
}
|
||||
|
||||
commandsDir := filepath.Join(workspacePath, ".claude", "commands")
|
||||
var missing []string
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
destPath := filepath.Join(commandsDir, entry.Name())
|
||||
if _, err := os.Stat(destPath); os.IsNotExist(err) {
|
||||
missing = append(missing, entry.Name())
|
||||
}
|
||||
}
|
||||
|
||||
return missing, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user