Skip to content
Merged
11 changes: 6 additions & 5 deletions demo-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,12 @@ YAML
)"

# Render all workspaces
$FLOW render bold-creek
$FLOW render swift-pine
$FLOW render calm-ridge
$FLOW render dry-fog
$FLOW render iron-vale
# Use --reset=false to skip interactive prompt when branches already exist
$FLOW render bold-creek --reset=false
$FLOW render swift-pine --reset=false
$FLOW render calm-ridge --reset=false
$FLOW render dry-fog --reset=false
$FLOW render iron-vale --reset=false

# --- Set up different statuses via local changes and marker files ---

Expand Down
1 change: 1 addition & 0 deletions docs/commands/flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ A YAML manifest defines which repos/branches belong together, and `flow render`

### SEE ALSO

* [flow archive](flow_archive.md) - Archive a workspace (remove worktrees, keep state)
* [flow delete](flow_delete.md) - Delete one or more workspaces and their worktrees
* [flow edit](flow_edit.md) - Open flow configuration files in editor
* [flow exec](flow_exec.md) - Run a command from the workspace directory
Expand Down
41 changes: 41 additions & 0 deletions docs/commands/flow_archive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
## flow archive

Archive a workspace (remove worktrees, keep state)

### Synopsis

Archive a workspace by removing its worktrees to free up branches,
while preserving the state file. Archived workspaces are hidden from
flow status by default (use --all to see them).

Use --closed to archive all workspaces with "closed" status at once.

```
flow archive [workspace] [flags]
```

### Examples

```
flow archive my-workspace # Archive a single workspace
flow archive --closed # Archive all closed workspaces
```

### Options

```
--closed Archive all workspaces with closed status
-f, --force Skip confirmation prompt
-h, --help help for archive
```

### Options inherited from parent commands

```
-v, --verbose Enable verbose debug output
```

### SEE ALSO

* [flow](flow.md) - Multi-repo workspace manager using git worktrees

5 changes: 4 additions & 1 deletion docs/commands/flow_render.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ flow render <workspace> [flags]

```
flow render calm-delta
flow render calm-delta --reset # Reset existing branches
flow render calm-delta --reset=false # Use existing branches
```

### Options

```
-h, --help help for render
-h, --help help for render
--reset Reset existing branches to fresh state from default branch (default true)
```

### Options inherited from parent commands
Expand Down
3 changes: 3 additions & 0 deletions docs/commands/flow_status.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Show workspace status
Show the resolved status of workspaces.

Without arguments, shows all workspaces with their statuses.
Archived workspaces are hidden by default; use --all to include them.
With a workspace argument, shows a detailed per-repo status breakdown.

```
Expand All @@ -20,12 +21,14 @@ flow status [workspace] [flags]

```
flow status # Show all workspace statuses
flow status --all # Include archived workspaces
flow status vpc-ipv6 # Show per-repo breakdown
```

### Options

```
-a, --all Include archived workspaces
-h, --help help for status
```

Expand Down
186 changes: 186 additions & 0 deletions internal/cmd/archive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package cmd

import (
"context"
"errors"
"fmt"
"sync"

"github.com/milldr/flow/internal/config"
"github.com/milldr/flow/internal/status"
"github.com/milldr/flow/internal/ui"
"github.com/milldr/flow/internal/workspace"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
)

var errWorkspaceArgRequired = errors.New("workspace argument required (or use --closed)")

func newArchiveCmd(svc *workspace.Service, cfg *config.Config) *cobra.Command {
var closed bool
var force bool

cmd := &cobra.Command{
Use: "archive [workspace]",
Short: "Archive a workspace (remove worktrees, keep state)",
Long: `Archive a workspace by removing its worktrees to free up branches,
while preserving the state file. Archived workspaces are hidden from
flow status by default (use --all to see them).

Use --closed to archive all workspaces with "closed" status at once.`,
Args: cobra.MaximumNArgs(1),
Example: " flow archive my-workspace # Archive a single workspace\n flow archive --closed # Archive all closed workspaces",
RunE: func(cmd *cobra.Command, args []string) error {
if closed {
return runArchiveClosed(cmd.Context(), svc, cfg, force)
}
if len(args) == 0 {
return errWorkspaceArgRequired
}
return runArchiveOne(cmd.Context(), svc, args[0], force)
},
}

cmd.Flags().BoolVar(&closed, "closed", false, "Archive all workspaces with closed status")
cmd.Flags().BoolVarP(&force, "force", "f", false, "Skip confirmation prompt")
return cmd
}

func runArchiveOne(ctx context.Context, svc *workspace.Service, idOrName string, force bool) error {
id, st, err := resolveWorkspace(svc, idOrName)
if err != nil {
return err
}

if st.Metadata.Archived {
ui.Print("Workspace is already archived.")
return nil
}

name := workspaceDisplayName(id, st)

if !force {
confirmed, err := ui.Confirm(fmt.Sprintf("Archive workspace %q? This will remove worktrees and free branches.", name))
if err != nil {
return err
}
if !confirmed {
ui.Print("Cancelled.")
return nil
}
}

err = ui.RunWithSpinner("Archiving workspace: "+name, func(_ func(string)) error {
return svc.Archive(ctx, id)
})
if err != nil {
return err
}

ui.Success("Archived workspace: " + name)
return nil
}

func runArchiveClosed(ctx context.Context, svc *workspace.Service, cfg *config.Config, force bool) error {
infos, err := svc.List()
if err != nil {
return err
}

// Filter to non-archived workspaces only.
var candidates []workspace.Info
for _, info := range infos {
if !info.Archived {
candidates = append(candidates, info)
}
}

if len(candidates) == 0 {
ui.Print("No active workspaces to check.")
return nil
}

// Resolve statuses to find closed workspaces.
type closedWS struct {
id string
name string
}
var closedList []closedWS
var mu sync.Mutex

resolver := &status.Resolver{Runner: &status.ShellRunner{}}

err = ui.RunWithSpinner("Resolving workspace statuses...", func(_ func(string)) error {
g, gctx := errgroup.WithContext(ctx)
g.SetLimit(4)

for _, info := range candidates {
g.Go(func() error {
spec, specErr := status.LoadWithFallback(
cfg.WorkspaceStatusSpecPath(info.ID),
cfg.StatusSpecFile,
)
if specErr != nil {
return nil // skip workspaces without status spec
}

st, findErr := svc.Find(info.ID)
if findErr != nil {
return nil
}

repos := stateReposToInfo(st, cfg.WorkspacePath(info.ID))
wsName := info.Name
if wsName == "" {
wsName = info.ID
}
result := resolver.ResolveWorkspace(gctx, spec, repos, info.ID, wsName)
if result.Status == "closed" {
mu.Lock()
closedList = append(closedList, closedWS{id: info.ID, name: wsName})
mu.Unlock()
}
return nil
})
}

return g.Wait()
})
if err != nil {
return err
}

if len(closedList) == 0 {
ui.Print("No closed workspaces to archive.")
return nil
}

if !force {
ui.Printf("Found %d closed workspace(s) to archive:\n", len(closedList))
for _, ws := range closedList {
ui.Printf(" - %s\n", ws.name)
}
confirmed, err := ui.Confirm("Archive all?")
if err != nil {
return err
}
if !confirmed {
ui.Print("Cancelled.")
return nil
}
}

var archiveErrors []error
for _, ws := range closedList {
if err := svc.Archive(ctx, ws.id); err != nil {
archiveErrors = append(archiveErrors, fmt.Errorf("archiving %s: %w", ws.name, err))
continue
}
ui.Success("Archived: " + ws.name)
}

if len(archiveErrors) > 0 {
return errors.Join(archiveErrors...)
}
return nil
}
27 changes: 23 additions & 4 deletions internal/cmd/render.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package cmd

import (
"fmt"

"github.com/milldr/flow/internal/ui"
"github.com/milldr/flow/internal/workspace"
"github.com/spf13/cobra"
)

func newRenderCmd(svc *workspace.Service) *cobra.Command {
return &cobra.Command{
var reset bool

cmd := &cobra.Command{
Use: "render <workspace>",
Short: "Create worktrees from workspace state file",
Args: cobra.ExactArgs(1),
Example: ` flow render calm-delta`,
Example: " flow render calm-delta\n flow render calm-delta --reset # Reset existing branches\n flow render calm-delta --reset=false # Use existing branches",
RunE: func(cmd *cobra.Command, args []string) error {
id, st, err := resolveWorkspace(svc, args[0])
if err != nil {
Expand All @@ -20,8 +24,20 @@ func newRenderCmd(svc *workspace.Service) *cobra.Command {

name := workspaceDisplayName(id, st)

opts := &workspace.RenderOptions{}
if cmd.Flags().Changed("reset") {
if reset {
opts.OnBranchConflict = workspace.BranchConflictReset
} else {
opts.OnBranchConflict = workspace.BranchConflictUseExisting
}
} else {
opts.OnBranchConflict = workspace.BranchConflictPrompt
opts.PromptBranchConflict = ui.ConfirmBranchReset
}

err = ui.RunWithSpinner("Rendering workspace: "+name, func(report func(string)) error {
return svc.Render(cmd.Context(), id, report)
return svc.Render(cmd.Context(), id, report, opts)
})
if err != nil {
return err
Expand All @@ -33,10 +49,13 @@ func newRenderCmd(svc *workspace.Service) *cobra.Command {
if len(svc.Config.FlowConfig.Spec.Agents) > 0 {
ui.Printf(" %s\n", ui.Code("flow exec "+name))
} else {
ui.Printf(" %s\n", ui.Code("flow exec "+name+" -- <command>"))
ui.Printf(" %s\n", ui.Code(fmt.Sprintf("flow exec %s -- <command>", name)))
}

return nil
},
}

cmd.Flags().BoolVar(&reset, "reset", true, "Reset existing branches to fresh state from default branch")
return cmd
}
1 change: 1 addition & 0 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ func newRootCmd() *cobra.Command {
root.AddCommand(newExecCmd(svc))
root.AddCommand(newOpenCmd(svc))
root.AddCommand(newDeleteCmd(svc))
root.AddCommand(newArchiveCmd(svc, cfg))
root.AddCommand(newResetCmd(svc, cfg))
root.AddCommand(newSyncCmd(svc))

Expand Down
Loading
Loading