diff --git a/cmd/ai/agentic.go b/cmd/ai/agentic.go index 7048380..21ce5ef 100644 --- a/cmd/ai/agentic.go +++ b/cmd/ai/agentic.go @@ -67,22 +67,22 @@ var ( // AddAgenticCommands adds the agentic task management commands to the dev command. func AddAgenticCommands(parent *clir.Command) { - // core dev tasks - list available tasks + // core ai tasks - list available tasks addTasksCommand(parent) - // core dev task - show task details and claim + // core ai task - show task details and claim addTaskCommand(parent) - // core dev task:update - update task + // core ai task:update - update task addTaskUpdateCommand(parent) - // core dev task:complete - mark task complete + // core ai task:complete - mark task complete addTaskCompleteCommand(parent) - // core dev task:commit - auto-commit with task reference + // core ai task:commit - auto-commit with task reference addTaskCommitCommand(parent) - // core dev task:pr - create PR for task + // core ai task:pr - create PR for task addTaskPRCommand(parent) } @@ -100,9 +100,9 @@ func addTasksCommand(parent *clir.Command) { " 2. .env file in current directory\n" + " 3. ~/.core/agentic.yaml\n\n" + "Examples:\n" + - " core dev tasks\n" + - " core dev tasks --status pending --priority high\n" + - " core dev tasks --labels bug,urgent") + " core ai tasks\n" + + " core ai tasks --status pending --priority high\n" + + " core ai tasks --labels bug,urgent") cmd.StringFlag("status", "Filter by status (pending, in_progress, completed, blocked)", &status) cmd.StringFlag("priority", "Filter by priority (critical, high, medium, low)", &priority) @@ -163,10 +163,10 @@ func addTaskCommand(parent *clir.Command) { cmd := parent.NewSubCommand("task", "Show task details or auto-select a task") cmd.LongDescription("Shows details of a specific task or auto-selects the highest priority task.\n\n" + "Examples:\n" + - " core dev task abc123 # Show task details\n" + - " core dev task abc123 --claim # Show and claim the task\n" + - " core dev task abc123 --context # Show task with gathered context\n" + - " core dev task --auto # Auto-select highest priority pending task") + " core ai task abc123 # Show task details\n" + + " core ai task abc123 --claim # Show and claim the task\n" + + " core ai task abc123 --context # Show task with gathered context\n" + + " core ai task --auto # Auto-select highest priority pending task") cmd.BoolFlag("auto", "Auto-select highest priority pending task", &autoSelect) cmd.BoolFlag("claim", "Claim the task after showing details", &claim) @@ -275,8 +275,8 @@ func addTaskUpdateCommand(parent *clir.Command) { cmd := parent.NewSubCommand("task:update", "Update task status or progress") cmd.LongDescription("Updates a task's status, progress, or adds notes.\n\n" + "Examples:\n" + - " core dev task:update abc123 --status in_progress\n" + - " core dev task:update abc123 --progress 50 --notes 'Halfway done'") + " core ai task:update abc123 --status in_progress\n" + + " core ai task:update abc123 --progress 50 --notes 'Halfway done'") cmd.StringFlag("status", "New status (pending, in_progress, completed, blocked)", &status) cmd.IntFlag("progress", "Progress percentage (0-100)", &progress) @@ -336,8 +336,8 @@ func addTaskCompleteCommand(parent *clir.Command) { cmd := parent.NewSubCommand("task:complete", "Mark a task as completed") cmd.LongDescription("Marks a task as completed with optional output and artifacts.\n\n" + "Examples:\n" + - " core dev task:complete abc123 --output 'Feature implemented'\n" + - " core dev task:complete abc123 --failed --error 'Build failed'") + " core ai task:complete abc123 --output 'Feature implemented'\n" + + " core ai task:complete abc123 --failed --error 'Build failed'") cmd.StringFlag("output", "Summary of the completed work", &output) cmd.BoolFlag("failed", "Mark the task as failed", &failed) @@ -407,7 +407,7 @@ func printTaskList(tasks []agentic.Task) { } fmt.Println() - fmt.Printf("%s\n", dimStyle.Render("Use 'core dev task ' to view details")) + fmt.Printf("%s\n", dimStyle.Render("Use 'core ai task ' to view details")) } func printTaskDetails(task *agentic.Task) { @@ -492,9 +492,9 @@ func addTaskCommitCommand(parent *clir.Command) { " Task: #123\n" + " Co-Authored-By: Claude \n\n" + "Examples:\n" + - " core dev task:commit abc123 --message 'add user authentication'\n" + - " core dev task:commit abc123 -m 'fix login bug' --scope auth\n" + - " core dev task:commit abc123 -m 'update docs' --push") + " core ai task:commit abc123 --message 'add user authentication'\n" + + " core ai task:commit abc123 -m 'fix login bug' --scope auth\n" + + " core ai task:commit abc123 -m 'update docs' --push") cmd.StringFlag("message", "Commit message (without task reference)", &message) cmd.StringFlag("m", "Commit message (short form)", &message) @@ -593,10 +593,10 @@ func addTaskPRCommand(parent *clir.Command) { cmd.LongDescription("Creates a GitHub pull request linked to a task.\n\n" + "Requires the GitHub CLI (gh) to be installed and authenticated.\n\n" + "Examples:\n" + - " core dev task:pr abc123\n" + - " core dev task:pr abc123 --title 'Add authentication feature'\n" + - " core dev task:pr abc123 --draft --labels 'enhancement,needs-review'\n" + - " core dev task:pr abc123 --base develop") + " core ai task:pr abc123\n" + + " core ai task:pr abc123 --title 'Add authentication feature'\n" + + " core ai task:pr abc123 --draft --labels 'enhancement,needs-review'\n" + + " core ai task:pr abc123 --base develop") cmd.StringFlag("title", "PR title (defaults to task title)", &title) cmd.BoolFlag("draft", "Create as draft PR", &draft) diff --git a/cmd/ci/ci_release.go b/cmd/ci/ci_release.go index ab824a9..5b07062 100644 --- a/cmd/ci/ci_release.go +++ b/cmd/ci/ci_release.go @@ -40,7 +40,7 @@ func AddCIReleaseCommand(app *clir.Cli) { releaseCmd := app.NewSubCommand("ci", "Publish releases (dry-run by default)") releaseCmd.LongDescription("Publishes pre-built artifacts from dist/ to configured targets.\n" + "Run 'core build' first to create artifacts.\n\n" + - "SAFE BY DEFAULT: Runs in dry-run mode unless --were-go-for-launch is specified.\n\n" + + "SAFE BY DEFAULT: Runs in dry-run mode unless --we-are-go-for-launch is specified.\n\n" + "Configuration: .core/release.yaml") // Flags for the main release command @@ -49,7 +49,7 @@ func AddCIReleaseCommand(app *clir.Cli) { var draft bool var prerelease bool - releaseCmd.BoolFlag("were-go-for-launch", "Actually publish (default is dry-run for safety)", &goForLaunch) + releaseCmd.BoolFlag("we-are-go-for-launch", "Actually publish (default is dry-run for safety)", &goForLaunch) releaseCmd.StringFlag("version", "Version to release (e.g., v1.2.3)", &version) releaseCmd.BoolFlag("draft", "Create release as a draft", &draft) releaseCmd.BoolFlag("prerelease", "Mark release as a prerelease", &prerelease) @@ -122,7 +122,7 @@ func runCIPublish(dryRun bool, version string, draft, prerelease bool) error { // Print header fmt.Printf("%s Publishing release\n", releaseHeaderStyle.Render("CI:")) if dryRun { - fmt.Printf(" %s\n", releaseDimStyle.Render("(dry-run) use --were-go-for-launch to publish")) + fmt.Printf(" %s\n", releaseDimStyle.Render("(dry-run) use --we-are-go-for-launch to publish")) } else { fmt.Printf(" %s\n", releaseSuccessStyle.Render("🚀 GO FOR LAUNCH")) } diff --git a/cmd/setup/commands.go b/cmd/setup/commands.go index 2b87100..2548dbb 100644 --- a/cmd/setup/commands.go +++ b/cmd/setup/commands.go @@ -1,12 +1,24 @@ -// Package setup provides workspace initialisation commands. +// Package setup provides workspace bootstrap and package cloning commands. // -// Clones all repositories defined in repos.yaml into the workspace. -// Skips repos that already exist. Supports filtering by type. +// Two modes of operation: +// +// REGISTRY MODE (repos.yaml exists): +// - Clones all repositories defined in repos.yaml into packages/ +// - Skips repos that already exist +// - Supports filtering by type with --only +// +// BOOTSTRAP MODE (no repos.yaml): +// - Clones core-devops to set up the workspace foundation +// - Presents an interactive wizard to select packages (unless --all) +// - Clones selected packages // // Flags: // - --registry: Path to repos.yaml (auto-detected if not specified) // - --only: Filter by repo type (foundation, module, product) // - --dry-run: Preview what would be cloned +// - --all: Skip wizard, clone all packages (non-interactive) +// - --name: Project directory name for bootstrap mode +// - --build: Run build after cloning // // Uses gh CLI with HTTPS when authenticated, falls back to SSH. package setup diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index 14f1236..0fced43 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -22,48 +22,178 @@ var ( dimStyle = shared.DimStyle ) +// Default organization and devops repo for bootstrap +const ( + defaultOrg = "host-uk" + devopsRepo = "core-devops" + devopsReposYaml = "repos.yaml" +) + // AddSetupCommand adds the 'setup' command to the given parent command. func AddSetupCommand(parent *clir.Cli) { var registryPath string var only string var dryRun bool + var all bool + var name string + var build bool - setupCmd := parent.NewSubCommand("setup", "Clone all repos from registry") - setupCmd.LongDescription("Clones all repositories defined in repos.yaml into packages/.\n" + - "Skips repos that already exist. Use --only to filter by type.") + setupCmd := parent.NewSubCommand("setup", "Bootstrap workspace or clone packages from registry") + setupCmd.LongDescription("Sets up a development workspace.\n\n" + + "REGISTRY MODE (repos.yaml exists):\n" + + " Clones all repositories defined in repos.yaml into packages/.\n" + + " Skips repos that already exist. Use --only to filter by type.\n\n" + + "BOOTSTRAP MODE (no repos.yaml):\n" + + " 1. Clones core-devops to set up the workspace\n" + + " 2. Presents an interactive wizard to select packages\n" + + " 3. Clones selected packages\n\n" + + "Use --all to skip the wizard and clone everything.") setupCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", ®istryPath) setupCmd.StringFlag("only", "Only clone repos of these types (comma-separated: foundation,module,product)", &only) setupCmd.BoolFlag("dry-run", "Show what would be cloned without cloning", &dryRun) + setupCmd.BoolFlag("all", "Skip wizard, clone all packages (non-interactive)", &all) + setupCmd.StringFlag("name", "Project directory name for bootstrap mode", &name) + setupCmd.BoolFlag("build", "Run build after cloning", &build) setupCmd.Action(func() error { - return runSetup(registryPath, only, dryRun) + return runSetupOrchestrator(registryPath, only, dryRun, all, name, build) }) } -func runSetup(registryPath, only string, dryRun bool) error { +// runSetupOrchestrator decides between registry mode and bootstrap mode. +func runSetupOrchestrator(registryPath, only string, dryRun, all bool, projectName string, runBuild bool) error { ctx := context.Background() - // Find registry - var reg *repos.Registry + // Try to find an existing registry + var foundRegistry string var err error if registryPath != "" { - reg, err = repos.LoadRegistry(registryPath) - if err != nil { - return fmt.Errorf("failed to load registry: %w", err) - } + foundRegistry = registryPath } else { - registryPath, err = repos.FindRegistry() - if err != nil { - return fmt.Errorf("no repos.yaml found - run this from a workspace directory") + foundRegistry, err = repos.FindRegistry() + } + + // If registry exists, use registry mode + if err == nil && foundRegistry != "" { + return runRegistrySetup(ctx, foundRegistry, only, dryRun, all, runBuild) + } + + // No registry found - enter bootstrap mode + return runBootstrap(ctx, only, dryRun, all, projectName, runBuild) +} + +// runBootstrap handles the case where no repos.yaml exists. +func runBootstrap(ctx context.Context, only string, dryRun, all bool, projectName string, runBuild bool) error { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working directory: %w", err) + } + + fmt.Printf("%s Bootstrap mode (no repos.yaml found)\n", dimStyle.Render(">>")) + + var targetDir string + + // Check if current directory is empty + empty, err := isDirEmpty(cwd) + if err != nil { + return fmt.Errorf("failed to check directory: %w", err) + } + + if empty { + // Clone into current directory + targetDir = cwd + fmt.Printf("%s Cloning into current directory\n", dimStyle.Render(">>")) + } else { + // Directory has content - check if it's a git repo root + isRepo := isGitRepoRoot(cwd) + + if isRepo && isTerminal() && !all { + // Offer choice: setup working directory or create package + choice, err := promptSetupChoice() + if err != nil { + return fmt.Errorf("failed to get choice: %w", err) + } + + if choice == "setup" { + // Setup this working directory with .core/ config + return runRepoSetup(cwd, dryRun) + } + // Otherwise continue to "create package" flow } - reg, err = repos.LoadRegistry(registryPath) - if err != nil { - return fmt.Errorf("failed to load registry: %w", err) + + // Create package flow - need a project name + if projectName == "" { + if !isTerminal() || all { + projectName = defaultOrg + } else { + projectName, err = promptProjectName(defaultOrg) + if err != nil { + return fmt.Errorf("failed to get project name: %w", err) + } + } + } + + targetDir = filepath.Join(cwd, projectName) + fmt.Printf("%s Creating project directory: %s\n", dimStyle.Render(">>"), projectName) + + if !dryRun { + if err := os.MkdirAll(targetDir, 0755); err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } } } + // Clone core-devops first + devopsPath := filepath.Join(targetDir, devopsRepo) + if _, err := os.Stat(filepath.Join(devopsPath, ".git")); os.IsNotExist(err) { + fmt.Printf("%s Cloning %s...\n", dimStyle.Render(">>"), devopsRepo) + + if !dryRun { + if err := gitClone(ctx, defaultOrg, devopsRepo, devopsPath); err != nil { + return fmt.Errorf("failed to clone %s: %w", devopsRepo, err) + } + fmt.Printf("%s %s cloned\n", successStyle.Render(">>"), devopsRepo) + } else { + fmt.Printf(" Would clone %s/%s to %s\n", defaultOrg, devopsRepo, devopsPath) + } + } else { + fmt.Printf("%s %s already exists\n", dimStyle.Render(">>"), devopsRepo) + } + + // Load the repos.yaml from core-devops + registryPath := filepath.Join(devopsPath, devopsReposYaml) + + if dryRun { + fmt.Printf("\n%s Would load registry from %s and present package wizard\n", dimStyle.Render(">>"), registryPath) + return nil + } + + reg, err := repos.LoadRegistry(registryPath) + if err != nil { + return fmt.Errorf("failed to load registry from %s: %w", devopsRepo, err) + } + + // Override base path to target directory + reg.BasePath = targetDir + + // Now run the regular setup with the loaded registry + return runRegistrySetupWithReg(ctx, reg, registryPath, only, dryRun, all, runBuild) +} + +// runRegistrySetup loads a registry from path and runs setup. +func runRegistrySetup(ctx context.Context, registryPath, only string, dryRun, all, runBuild bool) error { + reg, err := repos.LoadRegistry(registryPath) + if err != nil { + return fmt.Errorf("failed to load registry: %w", err) + } + + return runRegistrySetupWithReg(ctx, reg, registryPath, only, dryRun, all, runBuild) +} + +// runRegistrySetupWithReg runs setup with an already-loaded registry. +func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryPath, only string, dryRun, all, runBuild bool) error { fmt.Printf("%s %s\n", dimStyle.Render("Registry:"), registryPath) fmt.Printf("%s %s\n", dimStyle.Render("Org:"), reg.Org) @@ -85,11 +215,10 @@ func runSetup(registryPath, only string, dryRun bool) error { fmt.Printf("%s %s\n", dimStyle.Render("Target:"), basePath) // Parse type filter - var typeFilter map[string]bool + var typeFilter []string if only != "" { - typeFilter = make(map[string]bool) for _, t := range strings.Split(only, ",") { - typeFilter[strings.TrimSpace(t)] = true + typeFilter = append(typeFilter, strings.TrimSpace(t)) } fmt.Printf("%s %s\n", dimStyle.Render("Filter:"), only) } @@ -101,32 +230,73 @@ func runSetup(registryPath, only string, dryRun bool) error { } } - // Get repos to clone + // Get all available repos allRepos := reg.List() + + // Determine which repos to clone var toClone []*repos.Repo var skipped, exists int - for _, repo := range allRepos { - // Skip if type filter doesn't match - if typeFilter != nil && !typeFilter[repo.Type] { - skipped++ - continue + // Use wizard in interactive mode, unless --all specified + useWizard := isTerminal() && !all && !dryRun + + if useWizard { + selected, err := runPackageWizard(reg, typeFilter) + if err != nil { + return fmt.Errorf("wizard error: %w", err) } - // Skip if clone: false - if repo.Clone != nil && !*repo.Clone { - skipped++ - continue + // Build set of selected repos + selectedSet := make(map[string]bool) + for _, name := range selected { + selectedSet[name] = true } - // Check if already exists - repoPath := filepath.Join(basePath, repo.Name) - if _, err := os.Stat(filepath.Join(repoPath, ".git")); err == nil { - exists++ - continue + // Filter repos based on selection + for _, repo := range allRepos { + if !selectedSet[repo.Name] { + skipped++ + continue + } + + // Check if already exists + repoPath := filepath.Join(basePath, repo.Name) + if _, err := os.Stat(filepath.Join(repoPath, ".git")); err == nil { + exists++ + continue + } + + toClone = append(toClone, repo) + } + } else { + // Non-interactive: filter by type + typeFilterSet := make(map[string]bool) + for _, t := range typeFilter { + typeFilterSet[t] = true } - toClone = append(toClone, repo) + for _, repo := range allRepos { + // Skip if type filter doesn't match (when filter is specified) + if len(typeFilterSet) > 0 && !typeFilterSet[repo.Type] { + skipped++ + continue + } + + // Skip if clone: false + if repo.Clone != nil && !*repo.Clone { + skipped++ + continue + } + + // Check if already exists + repoPath := filepath.Join(basePath, repo.Name) + if _, err := os.Stat(filepath.Join(repoPath, ".git")); err == nil { + exists++ + continue + } + + toClone = append(toClone, repo) + } } // Summary @@ -146,6 +316,18 @@ func runSetup(registryPath, only string, dryRun bool) error { return nil } + // Confirm in interactive mode + if useWizard { + confirmed, err := confirmClone(len(toClone), basePath) + if err != nil { + return err + } + if !confirmed { + fmt.Println("Cancelled.") + return nil + } + } + // Clone repos fmt.Println() var succeeded, failed int @@ -157,10 +339,10 @@ func runSetup(registryPath, only string, dryRun bool) error { err := gitClone(ctx, reg.Org, repo.Name, repoPath) if err != nil { - fmt.Printf("%s\n", errorStyle.Render("✗ "+err.Error())) + fmt.Printf("%s\n", errorStyle.Render("x "+err.Error())) failed++ } else { - fmt.Printf("%s\n", successStyle.Render("✓")) + fmt.Printf("%s\n", successStyle.Render("done")) succeeded++ } } @@ -176,9 +358,322 @@ func runSetup(registryPath, only string, dryRun bool) error { } fmt.Println() + // Run build if requested + if runBuild && succeeded > 0 { + fmt.Println() + fmt.Printf("%s Running build...\n", dimStyle.Render(">>")) + buildCmd := exec.Command("core", "build") + buildCmd.Dir = basePath + buildCmd.Stdout = os.Stdout + buildCmd.Stderr = os.Stderr + if err := buildCmd.Run(); err != nil { + return fmt.Errorf("build failed: %w", err) + } + } + return nil } +// isGitRepoRoot returns true if the directory is a git repository root. +func isGitRepoRoot(path string) bool { + _, err := os.Stat(filepath.Join(path, ".git")) + return err == nil +} + +// runRepoSetup sets up the current repository with .core/ configuration. +func runRepoSetup(repoPath string, dryRun bool) error { + fmt.Printf("%s Setting up repository: %s\n", dimStyle.Render(">>"), repoPath) + + // Detect project type + projectType := detectProjectType(repoPath) + fmt.Printf("%s Detected project type: %s\n", dimStyle.Render(">>"), projectType) + + // Create .core directory + coreDir := filepath.Join(repoPath, ".core") + if !dryRun { + if err := os.MkdirAll(coreDir, 0755); err != nil { + return fmt.Errorf("failed to create .core directory: %w", err) + } + } + + // Generate configs based on project type + name := filepath.Base(repoPath) + configs := map[string]string{ + "build.yaml": generateBuildConfig(repoPath, projectType), + "release.yaml": generateReleaseConfig(name, projectType), + "test.yaml": generateTestConfig(projectType), + } + + if dryRun { + fmt.Printf("\n%s Would create:\n", dimStyle.Render(">>")) + for filename, content := range configs { + fmt.Printf("\n %s:\n", filepath.Join(coreDir, filename)) + // Indent content for display + for _, line := range strings.Split(content, "\n") { + fmt.Printf(" %s\n", line) + } + } + return nil + } + + for filename, content := range configs { + configPath := filepath.Join(coreDir, filename) + if err := os.WriteFile(configPath, []byte(content), 0644); err != nil { + return fmt.Errorf("failed to write %s: %w", filename, err) + } + fmt.Printf("%s Created %s\n", successStyle.Render(">>"), configPath) + } + + return nil +} + +// detectProjectType identifies the project type from files present. +func detectProjectType(path string) string { + // Check in priority order + if _, err := os.Stat(filepath.Join(path, "wails.json")); err == nil { + return "wails" + } + if _, err := os.Stat(filepath.Join(path, "go.mod")); err == nil { + return "go" + } + if _, err := os.Stat(filepath.Join(path, "composer.json")); err == nil { + return "php" + } + if _, err := os.Stat(filepath.Join(path, "package.json")); err == nil { + return "node" + } + return "unknown" +} + +// generateBuildConfig creates a build.yaml configuration based on project type. +func generateBuildConfig(path, projectType string) string { + name := filepath.Base(path) + + switch projectType { + case "go", "wails": + return fmt.Sprintf(`version: 1 +project: + name: %s + description: Go application + main: ./cmd/%s + binary: %s +build: + cgo: false + flags: + - -trimpath + ldflags: + - -s + - -w +targets: + - os: linux + arch: amd64 + - os: linux + arch: arm64 + - os: darwin + arch: amd64 + - os: darwin + arch: arm64 + - os: windows + arch: amd64 +`, name, name, name) + + case "php": + return fmt.Sprintf(`version: 1 +project: + name: %s + description: PHP application + type: php +build: + dockerfile: Dockerfile + image: %s +`, name, name) + + case "node": + return fmt.Sprintf(`version: 1 +project: + name: %s + description: Node.js application + type: node +build: + script: npm run build + output: dist +`, name) + + default: + return fmt.Sprintf(`version: 1 +project: + name: %s + description: Application +`, name) + } +} + +// generateReleaseConfig creates a release.yaml configuration. +func generateReleaseConfig(name, projectType string) string { + // Try to detect GitHub repo from git remote + repo := detectGitHubRepo() + if repo == "" { + repo = "owner/" + name + } + + base := fmt.Sprintf(`version: 1 +project: + name: %s + repository: %s +`, name, repo) + + switch projectType { + case "go", "wails": + return base + ` +changelog: + include: + - feat + - fix + - perf + - refactor + exclude: + - chore + - docs + - style + - test + +publishers: + - type: github + draft: false + prerelease: false +` + case "php": + return base + ` +changelog: + include: + - feat + - fix + - perf + +publishers: + - type: github + draft: false +` + default: + return base + ` +changelog: + include: + - feat + - fix + +publishers: + - type: github +` + } +} + +// generateTestConfig creates a test.yaml configuration. +func generateTestConfig(projectType string) string { + switch projectType { + case "go", "wails": + return `version: 1 + +commands: + - name: unit + run: go test ./... + - name: coverage + run: go test -coverprofile=coverage.out ./... + - name: race + run: go test -race ./... + +env: + CGO_ENABLED: "0" +` + case "php": + return `version: 1 + +commands: + - name: unit + run: vendor/bin/pest --parallel + - name: types + run: vendor/bin/phpstan analyse + - name: lint + run: vendor/bin/pint --test + +env: + APP_ENV: testing + DB_CONNECTION: sqlite +` + case "node": + return `version: 1 + +commands: + - name: unit + run: npm test + - name: lint + run: npm run lint + - name: typecheck + run: npm run typecheck + +env: + NODE_ENV: test +` + default: + return `version: 1 + +commands: + - name: test + run: echo "No tests configured" +` + } +} + +// detectGitHubRepo tries to extract owner/repo from git remote. +func detectGitHubRepo() string { + cmd := exec.Command("git", "remote", "get-url", "origin") + output, err := cmd.Output() + if err != nil { + return "" + } + + url := strings.TrimSpace(string(output)) + + // Handle SSH format: git@github.com:owner/repo.git + if strings.HasPrefix(url, "git@github.com:") { + repo := strings.TrimPrefix(url, "git@github.com:") + repo = strings.TrimSuffix(repo, ".git") + return repo + } + + // Handle HTTPS format: https://github.com/owner/repo.git + if strings.Contains(url, "github.com/") { + parts := strings.Split(url, "github.com/") + if len(parts) == 2 { + repo := strings.TrimSuffix(parts[1], ".git") + return repo + } + } + + return "" +} + +// isDirEmpty returns true if the directory is empty or contains only hidden files. +func isDirEmpty(path string) (bool, error) { + entries, err := os.ReadDir(path) + if err != nil { + return false, err + } + + for _, e := range entries { + name := e.Name() + // Ignore common hidden/metadata files + if name == ".DS_Store" || name == ".git" || name == ".gitignore" { + continue + } + // Any other non-hidden file means directory is not empty + if !strings.HasPrefix(name, ".") { + return false, nil + } + } + + return true, nil +} + func gitClone(ctx context.Context, org, repo, path string) error { // Try gh clone first with HTTPS (works without SSH keys) if ghAuthenticated() { diff --git a/cmd/setup/setup_wizard.go b/cmd/setup/setup_wizard.go new file mode 100644 index 0000000..9c6e14b --- /dev/null +++ b/cmd/setup/setup_wizard.go @@ -0,0 +1,221 @@ +// setup_wizard.go implements the interactive package selection wizard. +// +// Uses charmbracelet/huh for a rich terminal UI with multi-select checkboxes. +// Falls back to non-interactive mode when not in a TTY or --all is specified. + +package setup + +import ( + "fmt" + "os" + "sort" + "strings" + + "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" + "github.com/host-uk/core/pkg/repos" + "golang.org/x/term" +) + +// wizardTheme returns a Dracula-inspired theme matching our CLI styling. +func wizardTheme() *huh.Theme { + t := huh.ThemeDracula() + return t +} + +// isTerminal returns true if stdin is a terminal. +func isTerminal() bool { + return term.IsTerminal(int(os.Stdin.Fd())) +} + +// promptSetupChoice asks the user whether to setup the working directory or create a package. +func promptSetupChoice() (string, error) { + var choice string + + form := huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("This directory is a git repository"). + Description("What would you like to do?"). + Options( + huh.NewOption("Setup Working Directory", "setup").Selected(true), + huh.NewOption("Create Package (clone repos into subdirectory)", "package"), + ). + Value(&choice), + ), + ).WithTheme(wizardTheme()) + + if err := form.Run(); err != nil { + return "", err + } + + return choice, nil +} + +// promptProjectName asks the user for a project directory name. +func promptProjectName(defaultName string) (string, error) { + var name string + + form := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Project directory name"). + Description("Enter the name for your new workspace directory"). + Placeholder(defaultName). + Value(&name), + ), + ).WithTheme(wizardTheme()) + + if err := form.Run(); err != nil { + return "", err + } + + if name == "" { + return defaultName, nil + } + return name, nil +} + +// groupPackagesByType organizes repos by their type for display. +func groupPackagesByType(reposList []*repos.Repo) map[string][]*repos.Repo { + groups := make(map[string][]*repos.Repo) + + for _, repo := range reposList { + repoType := repo.Type + if repoType == "" { + repoType = "other" + } + groups[repoType] = append(groups[repoType], repo) + } + + // Sort within each group + for _, group := range groups { + sort.Slice(group, func(i, j int) bool { + return group[i].Name < group[j].Name + }) + } + + return groups +} + +// packageOption represents a selectable package in the wizard. +type packageOption struct { + repo *repos.Repo + selected bool +} + +// runPackageWizard presents an interactive multi-select UI for package selection. +// Returns the list of selected repo names. +func runPackageWizard(reg *repos.Registry, preselectedTypes []string) ([]string, error) { + allRepos := reg.List() + + // Build preselection set + preselect := make(map[string]bool) + for _, t := range preselectedTypes { + preselect[strings.TrimSpace(t)] = true + } + + // Group repos by type for organized display + groups := groupPackagesByType(allRepos) + + // Build options with preselection + var options []huh.Option[string] + typeOrder := []string{"foundation", "module", "product", "template", "other"} + + for _, typeKey := range typeOrder { + group, ok := groups[typeKey] + if !ok || len(group) == 0 { + continue + } + + // Add type header as a visual separator (empty option) + typeLabel := strings.ToUpper(typeKey) + options = append(options, huh.NewOption[string]( + fmt.Sprintf("── %s ──", typeLabel), + "", + ).Selected(false)) + + for _, repo := range group { + // Skip if clone: false + if repo.Clone != nil && !*repo.Clone { + continue + } + + label := repo.Name + if repo.Description != "" { + label = fmt.Sprintf("%s - %s", repo.Name, truncateDesc(repo.Description, 40)) + } + + // Preselect based on type filter or select all if no filter + selected := len(preselect) == 0 || preselect[repo.Type] + + options = append(options, huh.NewOption[string](label, repo.Name).Selected(selected)) + } + } + + var selected []string + + // Header styling + headerStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#3b82f6")). + MarginBottom(1) + + fmt.Println(headerStyle.Render("Package Selection")) + fmt.Println("Use space to select/deselect, enter to confirm") + fmt.Println() + + form := huh.NewForm( + huh.NewGroup( + huh.NewMultiSelect[string](). + Title("Select packages to clone"). + Options(options...). + Value(&selected). + Filterable(true). + Height(20), + ), + ).WithTheme(wizardTheme()) + + if err := form.Run(); err != nil { + return nil, err + } + + // Filter out empty values (type headers) + var result []string + for _, name := range selected { + if name != "" { + result = append(result, name) + } + } + + return result, nil +} + +// confirmClone asks for confirmation before cloning. +func confirmClone(count int, target string) (bool, error) { + var confirmed bool + + form := huh.NewForm( + huh.NewGroup( + huh.NewConfirm(). + Title(fmt.Sprintf("Clone %d packages to %s?", count, target)). + Affirmative("Yes, clone"). + Negative("Cancel"). + Value(&confirmed), + ), + ).WithTheme(wizardTheme()) + + if err := form.Run(); err != nil { + return false, err + } + + return confirmed, nil +} + +// truncateDesc truncates a description to max length with ellipsis. +func truncateDesc(s string, max int) string { + if len(s) <= max { + return s + } + return s[:max-3] + "..." +} diff --git a/docs/cmd/ci/index.md b/docs/cmd/ci/index.md index e9e7cf4..86339a4 100644 --- a/docs/cmd/ci/index.md +++ b/docs/cmd/ci/index.md @@ -2,7 +2,7 @@ Publish releases to GitHub, Docker, npm, Homebrew, and more. -**Safety:** Dry-run by default. Use `--were-go-for-launch` to actually publish. +**Safety:** Dry-run by default. Use `--we-are-go-for-launch` to actually publish. ## Subcommands @@ -22,7 +22,7 @@ core ci [flags] | Flag | Description | |------|-------------| -| `--were-go-for-launch` | Actually publish (default is dry-run) | +| `--we-are-go-for-launch` | Actually publish (default is dry-run) | | `--version` | Override version | | `--draft` | Create as draft release | | `--prerelease` | Mark as prerelease | @@ -34,13 +34,13 @@ core ci [flags] core ci # Actually publish -core ci --were-go-for-launch +core ci --we-are-go-for-launch # Publish as draft -core ci --were-go-for-launch --draft +core ci --we-are-go-for-launch --draft # Publish as prerelease -core ci --were-go-for-launch --prerelease +core ci --we-are-go-for-launch --prerelease ``` ## Workflow @@ -56,7 +56,7 @@ core build sdk core ci # Step 3: Publish (explicit flag required) -core ci --were-go-for-launch +core ci --we-are-go-for-launch ``` ## Publishers diff --git a/docs/cmd/setup/index.md b/docs/cmd/setup/index.md index 9ecb652..5516feb 100644 --- a/docs/cmd/setup/index.md +++ b/docs/cmd/setup/index.md @@ -8,7 +8,7 @@ The `setup` command operates in three modes: 1. **Registry mode** - When `repos.yaml` exists nearby, clones repositories into packages/ 2. **Bootstrap mode** - When no registry exists, clones `core-devops` first, then presents an interactive wizard to select packages -3. **Repo setup mode** - When run in a git repository root, offers to create `.core/build.yaml` configuration +3. **Repo setup mode** - When run in a git repo root, offers to create `.core/build.yaml` configuration ## Usage @@ -21,15 +21,15 @@ core setup [flags] | Flag | Description | |------|-------------| | `--registry` | Path to repos.yaml (auto-detected if not specified) | -| `--dry-run` | Show what would happen without making changes | +| `--dry-run` | Show what would be cloned without cloning | | `--only` | Only clone repos of these types (comma-separated: foundation,module,product) | | `--all` | Skip wizard, clone all packages (non-interactive) | | `--name` | Project directory name for bootstrap mode | | `--build` | Run build after cloning | -## Modes +--- -### Registry Mode +## Registry Mode When `repos.yaml` is found nearby (current directory or parents), setup clones all defined repositories: @@ -42,9 +42,16 @@ core setup --dry-run # Only clone foundation packages core setup --only foundation + +# Multiple types +core setup --only foundation,module ``` -### Bootstrap Mode +In registry mode with a TTY, an interactive wizard allows you to select which packages to clone. Use `--all` to skip the wizard and clone everything. + +--- + +## Bootstrap Mode When no `repos.yaml` exists, setup enters bootstrap mode: @@ -62,105 +69,104 @@ core setup --all --name ci-test ``` Bootstrap mode: -1. Clones `core-devops` (contains `repos.yaml`) -2. Shows interactive package selection wizard -3. Clones selected packages +1. Detects if current directory is empty +2. If not empty, prompts for project name (or uses `--name`) +3. Clones `core-devops` (contains `repos.yaml`) +4. Loads the registry from core-devops +5. Shows interactive package selection wizard (unless `--all`) +6. Clones selected packages +7. Optionally runs build (with `--build`) -### Repo Setup Mode +--- -When run in a git repository root, offers to set up the repo with `.core/` configuration: +## Repo Setup Mode + +When run in a git repository root (without `repos.yaml`), setup offers two choices: + +1. **Setup Working Directory** - Creates `.core/build.yaml` based on detected project type +2. **Create Package** - Creates a subdirectory and clones packages there ```bash -# In a git repo without .core/ cd ~/Code/my-go-project core setup -# Choose "Setup this repo" when prompted -# Creates .core/build.yaml based on detected project type +# Output: +# >> This directory is a git repository +# > Setup Working Directory +# Create Package (clone repos into subdirectory) ``` -Supported project types: -- **Go** - Detected via `go.mod` -- **Wails** - Detected via `wails.json` -- **Node.js** - Detected via `package.json` -- **PHP** - Detected via `composer.json` +Choosing "Setup Working Directory" detects the project type and generates configuration: + +| Detected File | Project Type | +|---------------|--------------| +| `wails.json` | Wails | +| `go.mod` | Go | +| `composer.json` | PHP | +| `package.json` | Node.js | + +Creates three config files in `.core/`: + +| File | Purpose | +|------|---------| +| `build.yaml` | Build targets, flags, output settings | +| `release.yaml` | Changelog format, GitHub release config | +| `test.yaml` | Test commands, environment variables | + +Also auto-detects GitHub repo from git remote for release config. + +--- + +## Interactive Wizard + +When running in a terminal (TTY), the setup command presents an interactive multi-select wizard: + +- Packages are grouped by type (foundation, module, product, template) +- Use arrow keys to navigate +- Press space to select/deselect packages +- Type to filter the list +- Press enter to confirm selection + +The wizard is skipped when: +- `--all` flag is specified +- Not running in a TTY (e.g., CI pipelines) +- `--dry-run` is specified + +--- ## Examples ### Clone from Registry ```bash -# Clone all repos +# Clone all repos (interactive wizard) core setup +# Clone all repos (non-interactive) +core setup --all + # Preview without cloning core setup --dry-run # Only foundation packages core setup --only foundation - -# Multiple types -core setup --only foundation,module ``` ### Bootstrap New Workspace ```bash -# Interactive bootstrap +# Interactive bootstrap in empty directory mkdir workspace && cd workspace core setup # Non-interactive with all packages core setup --all --name my-project -# Bootstrap in current directory -core setup --name . +# Bootstrap and run build +core setup --all --name my-project --build ``` -### Setup Single Repository - -```bash -# In a Go project -cd my-go-project -core setup --dry-run - -# Output: -# → Setting up repository configuration -# ✓ Detected project type: go -# → Would create: -# /path/to/my-go-project/.core/build.yaml -``` - -## Generated Configuration - -When setting up a repository, `core setup` generates `.core/build.yaml`: - -```yaml -version: 1 -project: - name: my-project - description: Go application - main: ./cmd/my-project - binary: my-project -build: - cgo: false - flags: - - -trimpath - ldflags: - - -s - - -w -targets: - - os: linux - arch: amd64 - - os: linux - arch: arm64 - - os: darwin - arch: amd64 - - os: darwin - arch: arm64 - - os: windows - arch: amd64 -``` +--- ## Registry Format @@ -183,6 +189,8 @@ repos: description: Link-in-bio product ``` +--- + ## Finding Registry Core looks for `repos.yaml` in: @@ -192,6 +200,8 @@ Core looks for `repos.yaml` in: 3. `~/Code/host-uk/repos.yaml` 4. `~/.config/core/repos.yaml` +--- + ## After Setup ```bash @@ -208,8 +218,10 @@ core build core test ``` +--- + ## See Also -- [work command](../dev/work/) - Multi-repo operations -- [build command](../build/) - Build projects -- [doctor command](../doctor/) - Check environment +- [dev work](../dev/work/) - Multi-repo operations +- [build](../build/) - Build projects +- [doctor](../doctor/) - Check environment diff --git a/go.mod b/go.mod index eff6cf0..4bd611d 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.25.5 require ( github.com/Snider/Borg v0.1.0 + github.com/charmbracelet/huh v0.8.0 github.com/charmbracelet/lipgloss v1.1.0 github.com/getkin/kin-openapi v0.133.0 github.com/leaanthony/clir v1.7.0 @@ -17,6 +18,7 @@ require ( golang.org/x/mod v0.31.0 golang.org/x/net v0.49.0 golang.org/x/oauth2 v0.34.0 + golang.org/x/term v0.39.0 golang.org/x/text v0.33.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -28,15 +30,22 @@ require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.3.0 // indirect github.com/TwiN/go-color v1.4.1 // indirect + github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/catppuccin/go v0.3.0 // indirect + github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect + github.com/charmbracelet/bubbletea v1.3.6 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/x/ansi v0.9.3 // indirect github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect github.com/go-git/go-git/v5 v5.16.3 // indirect @@ -51,8 +60,12 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect @@ -75,6 +88,7 @@ require ( github.com/yosida95/uritemplate/v3 v3.0.2 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/go.sum b/go.sum index 69f70da..9037288 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= @@ -17,31 +19,61 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= +github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= +github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= +github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw= +github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= +github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY= +github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= +github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= +github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= @@ -101,14 +133,22 @@ github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDwU= github.com/minio/selfupdate v0.6.0/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/modelcontextprotocol/go-sdk v1.2.0 h1:Y23co09300CEk8iZ/tMxIX1dVmKZkzoSBZOpJwUnc/s= github.com/modelcontextprotocol/go-sdk v1.2.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/oasdiff/oasdiff v1.11.8 h1:3LalSR0yYVM5sAYNInlIG4TVckLCJBkgjcnst2GKWVg= @@ -190,6 +230,8 @@ golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -197,6 +239,7 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=