Repository navigation
feat(build): guided build lifecycle and anvil build #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
| "os" | ||
| "os/signal" | ||
| "path/filepath" | ||
| "strings" | ||
|
|
||
| "github.com/spf13/cobra" | ||
| "golang.org/x/term" | ||
|
|
||
| "github.com/openforge-oss/anvil/internal/detect" | ||
| "github.com/openforge-oss/anvil/internal/driver" | ||
| "github.com/openforge-oss/anvil/internal/pipeline" | ||
| "github.com/openforge-oss/anvil/internal/tui" | ||
| ) | ||
|
|
||
| var ( | ||
| buildPath string | ||
| buildTarget string | ||
| buildFlavor string | ||
| buildRelease bool | ||
| buildDryRun bool | ||
| buildPlain bool | ||
| ) | ||
|
|
||
| var buildCmd = &cobra.Command{ | ||
| Use: "build", | ||
| Short: "Run a detected project through deps, analyze, test, and build", | ||
| Args: cobra.NoArgs, | ||
| RunE: runBuild, | ||
| } | ||
|
|
||
| func init() { | ||
| buildCmd.Flags().StringVar(&buildPath, "path", ".", "project directory") | ||
| buildCmd.Flags().StringVar(&buildTarget, "target", "", "build target (stack-specific, e.g. apk, appbundle, ios)") | ||
| buildCmd.Flags().StringVar(&buildFlavor, "flavor", "", "build flavor, product flavor, or scheme") | ||
| buildCmd.Flags().BoolVar(&buildRelease, "release", false, "release build where applicable") | ||
| buildCmd.Flags().BoolVar(&buildDryRun, "dry-run", false, "print the steps without running them") | ||
| buildCmd.Flags().BoolVar(&buildPlain, "plain", false, "plain line output instead of the interactive view") | ||
| rootCmd.AddCommand(buildCmd) | ||
| } | ||
|
|
||
| func runBuild(cmd *cobra.Command, _ []string) error { | ||
| root, err := filepath.Abs(buildPath) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| projects, err := detect.Scan(root, detect.DefaultOptions()) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| chosen, err := chooseProject(cmd, projects, root) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| d, ok := driver.For(chosen.Stack, chosen.Path) | ||
| if !ok { | ||
| return fmt.Errorf("anvil build does not support the %q stack yet", chosen.Stack) | ||
| } | ||
|
|
||
| opts := driver.BuildOptions{Target: buildTarget, Flavor: buildFlavor, Release: buildRelease} | ||
|
|
||
| if buildDryRun { | ||
| printPlan(cmd, chosen, d, opts) | ||
| return nil | ||
| } | ||
|
|
||
| ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) | ||
| defer stop() | ||
|
|
||
| events := make(chan pipeline.Event) | ||
| go pipeline.Run(ctx, chosen.Path, d, opts, events) | ||
|
|
||
| var success bool | ||
| if usePlain() { | ||
| success = tui.Plain(events, cmd.OutOrStdout()) | ||
| } else { | ||
| success, err = tui.Run(events) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| if !success { | ||
| return errors.New("build failed") | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func chooseProject(cmd *cobra.Command, projects []detect.Project, root string) (detect.Project, error) { | ||
| switch len(projects) { | ||
| case 0: | ||
| return detect.Project{}, errors.New("no supported project detected; pass --path to a project directory") | ||
| case 1: | ||
| return projects[0], nil | ||
| } | ||
| for _, p := range projects { | ||
| if p.Path == root { | ||
| return p, nil | ||
| } | ||
| } | ||
| out := cmd.ErrOrStderr() | ||
| fmt.Fprintln(out, "Multiple projects detected; pass --path to one of:") | ||
| for _, p := range projects { | ||
| fmt.Fprintf(out, " %s (%s)\n", p.Path, p.Stack) | ||
| } | ||
| return detect.Project{}, errors.New("multiple projects detected") | ||
| } | ||
|
|
||
| func printPlan(cmd *cobra.Command, p detect.Project, d driver.Driver, opts driver.BuildOptions) { | ||
| out := cmd.OutOrStdout() | ||
| fmt.Fprintf(out, "%s (%s) at %s\n\n", d.Name(), p.Subtype, p.Path) | ||
| for _, it := range pipeline.Plan(d, opts) { | ||
| if it.Step == nil { | ||
| fmt.Fprintf(out, "%s: skipped\n", it.Phase) | ||
| continue | ||
| } | ||
| dir := it.Step.Dir | ||
| if dir == "" { | ||
| dir = "." | ||
| } | ||
| fmt.Fprintf(out, "%s: %s\n dir: %s\n cmd: %s\n", it.Phase, it.Name, dir, strings.Join(it.Step.Argv, " ")) | ||
| } | ||
| } | ||
|
|
||
| func usePlain() bool { | ||
| if buildPlain || os.Getenv("CI") != "" { | ||
| return true | ||
| } | ||
| return !term.IsTerminal(int(os.Stdout.Fd())) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,11 +6,14 @@ High-level phases and status, for monitoring. Granular tasks live in | |
| | Phase | What | Status | | ||
| |-------|------|--------| | ||
| | 0. Repo and pipeline | Go skeleton, wrapper scripts, Claude hooks, CI, docs, working rules | done | | ||
| | 1. Detection | Marker-file detectors for Flutter, React Native, Android, iOS; `anvil detect` | in progress | | ||
| | 2. Guided build | Driver lifecycle (deps, analyze, test, build); Flutter first, then RN, Android, iOS; interactive TUI; unified error surfacing | planned | | ||
| | 1. Detection | Marker-file detectors for Flutter, React Native, Android, iOS, Swift, Kotlin; `anvil detect` | done | | ||
| | 2. Guided build | Driver lifecycle (deps, analyze, test, build) for all six stacks; `--flavor`; interactive TUI plus plain fallback; `anvil build` | in progress | | ||
|
Comment on lines
+9
to
+10
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Keep milestone status consistent across the roadmap and progress board.
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
| | 3. Signing | Guided Android keystore and iOS provisioning/signing | planned | | ||
| | 4. Upload | TestFlight, Play, npm upload; GoReleaser distribution (Homebrew, Scoop, curl) | planned | | ||
| | 5. Breadth | More ecosystems (web, Go) via new drivers; `--explain` educational mode | future | | ||
| | 5. Breadth | More ecosystems (web, Go) via new drivers; flavor auto-detection; `--explain` educational mode | future | | ||
|
|
||
| Swift (SPM) and Kotlin/JVM drivers and `--flavor` were pulled forward into | ||
| Milestone 2. | ||
|
|
||
| ## Guiding decisions (locked) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,65 @@ | ||
| 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/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= | ||
| github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= | ||
| github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= | ||
| github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= | ||
| github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= | ||
| github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= | ||
| 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.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= | ||
| github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= | ||
| github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= | ||
| github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= | ||
| github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= | ||
| github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= | ||
| github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA= | ||
| github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA= | ||
| github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= | ||
| github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= | ||
| github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= | ||
| github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= | ||
| github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= | ||
| 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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= | ||
| github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= | ||
| github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= | ||
| github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= | ||
| 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.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= | ||
| github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= | ||
| 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/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= | ||
| github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= | ||
| github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= | ||
| github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= | ||
| github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= | ||
| github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= | ||
| github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= | ||
| github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= | ||
| github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= | ||
| go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= | ||
| golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= | ||
| golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= | ||
| golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
| golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
| golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= | ||
| golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= | ||
| golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= | ||
| golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= | ||
| golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= | ||
| golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= | ||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: openforge-oss/anvil
Length of output: 774
🏁 Script executed:
Repository: openforge-oss/anvil
Length of output: 10782
🏁 Script executed:
Repository: openforge-oss/anvil
Length of output: 5888
Narrow the
--flavordocs or add support in Swift/Kotlin/JVM. The changelog, roadmap, and todo entries overstate flavor coverage across all six stacks:internal/driver/swift.goandinternal/driver/kotlin.gostill ignoreopts.Flavor. Either thread flavor through those drivers too or limit the docs to the stacks that actually support it.📍 Affects 3 files
CHANGELOG.md#L14-L16(this comment)docs/ROADMAP.md#L10-L16tasks/todo.md#L41-L44🤖 Prompt for AI Agents