feat(cli): implement setup wizard, fix ai examples, ci dry-run flag

Setup command now has three modes:
- Registry mode: interactive wizard to select packages
- Bootstrap mode: clones core-devops first, then wizard
- Repo setup mode: generates .core/{build,release,test}.yaml

Changes:
- setup: add interactive package selection with charmbracelet/huh
- setup: detect project type (go/php/node/wails) and generate configs
- setup: auto-detect GitHub repo from git remote
- ai: fix command examples (core dev -> core ai)
- ci: rename flag to --we-are-go-for-launch

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-01-29 19:09:51 +00:00
parent 996fae70f4
commit 9565122fdc
9 changed files with 944 additions and 147 deletions

View file

@ -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 <id> - show task details and claim
// core ai task <id> - show task details and claim
addTaskCommand(parent)
// core dev task:update <id> - update task
// core ai task:update <id> - update task
addTaskUpdateCommand(parent)
// core dev task:complete <id> - mark task complete
// core ai task:complete <id> - mark task complete
addTaskCompleteCommand(parent)
// core dev task:commit <id> - auto-commit with task reference
// core ai task:commit <id> - auto-commit with task reference
addTaskCommitCommand(parent)
// core dev task:pr <id> - create PR for task
// core ai task:pr <id> - 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 <id>' to view details"))
fmt.Printf("%s\n", dimStyle.Render("Use 'core ai task <id>' to view details"))
}
func printTaskDetails(task *agentic.Task) {
@ -492,9 +492,9 @@ func addTaskCommitCommand(parent *clir.Command) {
" Task: #123\n" +
" Co-Authored-By: Claude <noreply@anthropic.com>\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)

View file

@ -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"))
}

View file

@ -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

View file

@ -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)", &registryPath)
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() {

221
cmd/setup/setup_wizard.go Normal file
View file

@ -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] + "..."
}

View file

@ -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

View file

@ -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

14
go.mod
View file

@ -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
)

43
go.sum
View file

@ -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=