| Field | Value |
|---|---|
| Feature ID | UNI-001 |
| Phase | 1 - Core Pipeline |
| Priority | P0 |
| Effort | M (3-5 days) |
| Dependencies | None |
| Packages | cmd/, internal/detector/ |
Provide the CLI entry point for unirelease using Cobra, parse all flags, resolve the project directory, and auto-detect the project type from manifest files. This is the first component users interact with and the foundation all other features build on.
main.go
cmd/
root.go
internal/
detector/
detector.go
detector_test.go
version.go
version_test.go
package main
import (
"os"
"unirelease/cmd"
)
func main() {
if err := cmd.Execute(); err != nil {
os.Exit(1)
}
}Cobra root command setup:
package cmd
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/cobra"
)
var (
flagStep string
flagYes bool
flagDryRun bool
flagVersion string
flagType string
)
var rootCmd = &cobra.Command{
Use: "unirelease [path]",
Short: "Unified release pipeline for any project",
Long: "Auto-detects project type (Rust, Node, Bun, Python) and runs a unified release pipeline.",
Args: cobra.MaximumNArgs(1),
RunE: runRelease,
}
func init() {
rootCmd.Flags().StringVar(&flagStep, "step", "", "Run only a specific pipeline step")
rootCmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Non-interactive mode (skip confirmations)")
rootCmd.Flags().BoolVar(&flagDryRun, "dry-run", false, "Preview pipeline without executing")
rootCmd.Flags().StringVarP(&flagVersion, "version", "v", "", "Override detected version")
rootCmd.Flags().StringVar(&flagType, "type", "", "Override auto-detection (rust|node|bun|python)")
}
func Execute() error {
return rootCmd.Execute()
}runRelease function logic:
- Resolve project directory:
- If positional arg provided, use it (resolve to absolute path).
- Otherwise, use
os.Getwd(). - Validate directory exists with
os.Stat().
- Load config (calls into
internal/config/-- stubbed as empty Config in UNI-001, full implementation in UNI-007). - Build
PipelineContextwith all resolved values. - Instantiate pipeline engine, execute pipeline.
- Map returned errors to exit codes:
ErrDetection-> exit 3ErrMissingTool-> exit 4ErrInvalidArgs-> exit 2- All other errors -> exit 1
Flag validation in runRelease:
| Flag | Validation |
|---|---|
--step |
If non-empty, must be one of the 10 valid step names. Print valid names on error. |
--type |
If non-empty, must be one of: rust, node, bun, python. Print valid types on error. |
--version |
If non-empty, must match semver pattern ^\d+\.\d+\.\d+ (no v prefix). Print format hint on error. |
--yes |
Boolean, no validation needed. |
--dry-run |
Boolean, no validation needed. |
| path arg | Must be an existing directory. Print "directory not found: " on error. |
ProjectType enum:
package detector
type ProjectType string
const (
TypeRust ProjectType = "rust"
TypeNode ProjectType = "node"
TypeBun ProjectType = "bun"
TypePython ProjectType = "python"
)
// ValidTypes returns all valid project types for flag validation.
func ValidTypes() []ProjectType {
return []ProjectType{TypeRust, TypeNode, TypeBun, TypePython}
}DetectionResult struct:
type DetectionResult struct {
Type ProjectType
Confidence int // Higher wins when multiple detectors match
Manifest string // Path to the manifest file that triggered detection
}Detect function:
// Detect scans the project directory and returns the detected project type.
// If typeOverride is non-empty, it is used directly (from --type flag).
// Returns ErrNoProject if no supported manifest file is found.
func Detect(projectDir string, typeOverride string) (*DetectionResult, error)Detection logic (step by step):
- If
typeOverrideis non-empty:- Validate it is a known type.
- Return
DetectionResult{Type: typeOverride, Confidence: 1000, Manifest: ""}.
- Initialize
candidates := []DetectionResult{}. - Check
filepath.Join(projectDir, "Cargo.toml")exists:- If yes, append
DetectionResult{Type: TypeRust, Confidence: 100, Manifest: "Cargo.toml"}.
- If yes, append
- Check
filepath.Join(projectDir, "pyproject.toml")exists:- If yes, append
DetectionResult{Type: TypePython, Confidence: 90, Manifest: "pyproject.toml"}.
- If yes, append
- Check
filepath.Join(projectDir, "package.json")exists:- If yes, read the file, parse JSON, check if any value in
.scriptscontains"bun build --compile". - If bun build --compile found: append
DetectionResult{Type: TypeBun, Confidence: 80, Manifest: "package.json"}. - Else: append
DetectionResult{Type: TypeNode, Confidence: 50, Manifest: "package.json"}.
- If yes, read the file, parse JSON, check if any value in
- If
candidatesis empty, returnErrNoProject. - Sort candidates by
Confidencedescending. - Return
candidates[0].
Bun detection detail:
// isBunBinary checks if any script in package.json contains "bun build --compile".
func isBunBinary(packageJSONPath string) (bool, error) {
data, err := os.ReadFile(packageJSONPath)
if err != nil {
return false, err
}
var pkg struct {
Scripts map[string]string `json:"scripts"`
}
if err := json.Unmarshal(data, &pkg); err != nil {
return false, err
}
for _, script := range pkg.Scripts {
if strings.Contains(script, "bun build --compile") {
return true, nil
}
}
return false, nil
}ReadVersion function:
// ReadVersion reads the version string from the project manifest.
// If versionOverride is non-empty, returns it directly (from --version flag).
func ReadVersion(projectDir string, projectType ProjectType, versionOverride string) (string, error)Implementation per type:
Rust (Cargo.toml):
func readCargoVersion(projectDir string) (string, error) {
path := filepath.Join(projectDir, "Cargo.toml")
var cargo struct {
Package struct {
Version string `toml:"version"`
} `toml:"package"`
}
data, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("read Cargo.toml: %w", err)
}
if err := toml.Unmarshal(data, &cargo); err != nil {
return "", fmt.Errorf("parse Cargo.toml: %w", err)
}
if cargo.Package.Version == "" {
return "", fmt.Errorf("no version found in Cargo.toml [package] section")
}
return cargo.Package.Version, nil
}Node/Bun (package.json):
func readPackageJSONVersion(projectDir string) (string, error) {
path := filepath.Join(projectDir, "package.json")
data, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("read package.json: %w", err)
}
var pkg struct {
Version string `json:"version"`
}
if err := json.Unmarshal(data, &pkg); err != nil {
return "", fmt.Errorf("parse package.json: %w", err)
}
if pkg.Version == "" {
return "", fmt.Errorf("no 'version' field in package.json")
}
return pkg.Version, nil
}Python (pyproject.toml):
func readPyprojectVersion(projectDir string) (string, error) {
path := filepath.Join(projectDir, "pyproject.toml")
var pyproject struct {
Project struct {
Version string `toml:"version"`
} `toml:"project"`
}
data, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("read pyproject.toml: %w", err)
}
if err := toml.Unmarshal(data, &pyproject); err != nil {
return "", fmt.Errorf("parse pyproject.toml: %w", err)
}
if pyproject.Project.Version == "" {
return "", fmt.Errorf("no version found in pyproject.toml [project] section")
}
return pyproject.Project.Version, nil
}| Test Name | Setup | Expected Result |
|---|---|---|
| TestDetect_Rust | Temp dir with Cargo.toml | TypeRust, Confidence 100 |
| TestDetect_Python | Temp dir with pyproject.toml | TypePython, Confidence 90 |
| TestDetect_Node | Temp dir with package.json (no bun compile) | TypeNode, Confidence 50 |
| TestDetect_BunBinary | Temp dir with package.json containing bun build --compile in scripts |
TypeBun, Confidence 80 |
| TestDetect_PriorityCargoOverNode | Temp dir with both Cargo.toml and package.json | TypeRust (higher confidence) |
| TestDetect_PriorityPythonOverNode | Temp dir with both pyproject.toml and package.json | TypePython (higher confidence) |
| TestDetect_TypeOverride | Temp dir with Cargo.toml, typeOverride="python" | TypePython (override wins) |
| TestDetect_NoManifest | Empty temp dir | ErrNoProject |
| TestDetect_InvalidTypeOverride | typeOverride="java" | Error: unsupported type |
| Test Name | Setup | Expected Result |
|---|---|---|
| TestReadVersion_Cargo | Cargo.toml with version = "0.3.0" |
"0.3.0" |
| TestReadVersion_PackageJSON | package.json with "version": "1.2.0" |
"1.2.0" |
| TestReadVersion_Pyproject | pyproject.toml with version = "2.0.0" |
"2.0.0" |
| TestReadVersion_Override | Any manifest, versionOverride="9.9.9" | "9.9.9" |
| TestReadVersion_MissingVersion_Cargo | Cargo.toml without version field | Error |
| TestReadVersion_MissingVersion_JSON | package.json without version field | Error |
| TestReadVersion_MalformedTOML | Invalid TOML content | Parse error |
| TestReadVersion_MalformedJSON | Invalid JSON content | Parse error |
| Test Name | Args | Expected Behavior |
|---|---|---|
| TestCLI_NoArgs | unirelease |
Uses cwd, runs detection |
| TestCLI_ExplicitPath | unirelease /tmp/project |
Uses /tmp/project |
| TestCLI_InvalidPath | unirelease /nonexistent |
Exit code 2, "not found" message |
| TestCLI_InvalidStep | unirelease --step bogus |
Exit code 2, lists valid steps |
| TestCLI_InvalidType | unirelease --type java |
Exit code 2, lists valid types |
| TestCLI_InvalidVersion | unirelease --version abc |
Exit code 2, format hint |
| TestCLI_DryRunFlag | unirelease --dry-run |
Sets DryRun=true on context |
| TestCLI_YesFlag | unirelease -y |
Sets Yes=true on context |
-
unireleasein a directory with Cargo.toml prints "Detected: rust" and proceeds. -
unireleasein a directory with package.json (no bun compile) prints "Detected: node". -
unireleasein a directory with package.json containingbun build --compileprints "Detected: bun". -
unireleasein a directory with pyproject.toml prints "Detected: python". -
unireleasein an empty directory prints error with supported types and exits with code 3. -
unirelease --type rustin any directory forces Rust detection. -
unirelease --version 1.2.3overrides the version read from manifest. -
unirelease /path/to/projectresolves to the specified path. - Version is correctly read from Cargo.toml
[package] version, package.jsonversion, pyproject.toml[project] version. - When both Cargo.toml and package.json exist, Cargo.toml wins.