cli/internal/cmd/setup/cmd_repo.go
Snider 99514d23e6
feat: Batch implementation of Gemini issues (#176)
* feat(help): Add CLI help command

Fixes #136

* chore: remove binary

* feat(mcp): Add TCP transport

Fixes #126

* feat(io): Migrate pkg/mcp to use Medium abstraction

Fixes #103

* chore(io): Migrate internal/cmd/docs/* to Medium abstraction

Fixes #113

* chore(io): Migrate internal/cmd/dev/* to Medium abstraction

Fixes #114

* chore(io): Migrate internal/cmd/setup/* to Medium abstraction

* chore(io): Complete migration of internal/cmd/dev/* to Medium abstraction

* chore(io): Migrate internal/cmd/sdk, pkgcmd, and workspace to Medium abstraction

* style: fix formatting in internal/variants

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(io): simplify local Medium implementation

Rewrote to match the simpler TypeScript pattern:
- path() sanitizes and returns string directly
- Each method calls path() once
- No complex symlink validation
- Less code, less attack surface

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(mcp): update sandboxing tests for simplified Medium

The simplified io/local.Medium implementation:
- Sanitizes .. to . (no error, path is cleaned)
- Allows absolute paths through (caller validates if needed)
- Follows symlinks (no traversal blocking)

Update tests to match this simplified behavior.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(updater): resolve PkgVersion duplicate declaration

Remove var PkgVersion from updater.go since go generate creates
const PkgVersion in version.go. Track version.go in git to ensure
builds work without running go generate first.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 04:20:18 +00:00

289 lines
5.8 KiB
Go

// cmd_repo.go implements repository setup with .core/ configuration.
//
// When running setup in an existing git repository, this generates
// build.yaml, release.yaml, and test.yaml configurations based on
// detected project type.
package setup
import (
"fmt"
"os/exec"
"path/filepath"
"strings"
"github.com/host-uk/core/pkg/i18n"
coreio "github.com/host-uk/core/pkg/io"
)
// runRepoSetup sets up the current repository with .core/ configuration.
func runRepoSetup(repoPath string, dryRun bool) error {
fmt.Printf("%s %s: %s\n", dimStyle.Render(">>"), i18n.T("cmd.setup.repo.setting_up"), repoPath)
// Detect project type
projectType := detectProjectType(repoPath)
fmt.Printf("%s %s: %s\n", dimStyle.Render(">>"), i18n.T("cmd.setup.repo.detected_type"), projectType)
// Create .core directory
coreDir := filepath.Join(repoPath, ".core")
if !dryRun {
if err := coreio.Local.EnsureDir(coreDir); 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 %s:\n", dimStyle.Render(">>"), i18n.T("cmd.setup.repo.would_create"))
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 := coreio.Local.Write(configPath, content); err != nil {
return fmt.Errorf("failed to write %s: %w", filename, err)
}
fmt.Printf("%s %s %s\n", successStyle.Render(">>"), i18n.T("cmd.setup.repo.created"), configPath)
}
return nil
}
// detectProjectType identifies the project type from files present.
func detectProjectType(path string) string {
// Check in priority order
if coreio.Local.IsFile(filepath.Join(path, "wails.json")) {
return "wails"
}
if coreio.Local.IsFile(filepath.Join(path, "go.mod")) {
return "go"
}
if coreio.Local.IsFile(filepath.Join(path, "composer.json")) {
return "php"
}
if coreio.Local.IsFile(filepath.Join(path, "package.json")) {
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 ""
}