detached 03a3f23

feat(cli): add gordon preview command scaffold

Adds `gordon preview` with create/list/delete/extend subcommands,
auto-detecting git branch for the default preview name via SanitizeBranchName.
commit
03a3f236619f4bf1aac0c52a6ebe01f86de1d951
author
brice · · 2026-03-19 05:03 UTC
committer
brice · · 2026-03-19 05:13 UTC
parent
2a743d6
signature
unsigned
2 files · +127 -0
A internal/adapters/in/cli/preview.go +123 -0
@@ -0,0 +1,123 @@
1+package cli
2+
3+import (
4+ "fmt"
5+ "os/exec"
6+ "strings"
7+
8+ "github.com/bnema/gordon/internal/usecase/auto/preview"
9+ "github.com/spf13/cobra"
10+)
11+
12+func newPreviewCmd() *cobra.Command {
13+ cmd := &cobra.Command{
14+ Use: "preview [name]",
15+ Short: "Create or manage preview environments",
16+ Long: "Build, push, and deploy an ephemeral preview environment. Defaults to current git branch name.",
17+ }
18+
19+ createCmd := newPreviewCreateCmd()
20+
21+ cmd.AddCommand(
22+ createCmd,
23+ newPreviewListCmd(),
24+ newPreviewDeleteCmd(),
25+ newPreviewExtendCmd(),
26+ )
27+
28+ // Default action (no subcommand) = create
29+ cmd.RunE = createCmd.RunE
30+ cmd.Args = cobra.MaximumNArgs(1)
31+ cmd.Flags().AddFlagSet(createCmd.Flags())
32+
33+ return cmd
34+}
35+
36+func newPreviewCreateCmd() *cobra.Command {
37+ var ttl string
38+ var noBuild bool
39+ var noData bool
40+
41+ cmd := &cobra.Command{
42+ Use: "create [name]",
43+ Short: "Create a preview environment",
44+ Args: cobra.MaximumNArgs(1),
45+ RunE: func(cmd *cobra.Command, args []string) error {
46+ name := ""
47+ if len(args) > 0 {
48+ name = args[0]
49+ } else {
50+ branch, err := detectGitBranch()
51+ if err != nil {
52+ return fmt.Errorf("no preview name provided and could not detect git branch: %w", err)
53+ }
54+ name = preview.SanitizeBranchName(branch)
55+ }
56+
57+ fmt.Printf("Creating preview: %s\n", name)
58+ // TODO: Reuse push mechanics (build, tag with preview-{name}, push)
59+ // TODO: Wire to remote API or local service
60+ _ = ttl
61+ _ = noBuild
62+ _ = noData
63+ return nil
64+ },
65+ }
66+
67+ cmd.Flags().StringVar(&ttl, "ttl", "", "Override TTL (e.g., 72h)")
68+ cmd.Flags().BoolVar(&noBuild, "no-build", false, "Skip image build")
69+ cmd.Flags().BoolVar(&noData, "no-data", false, "Skip volume cloning")
70+ return cmd
71+}
72+
73+func newPreviewListCmd() *cobra.Command {
74+ var jsonOutput bool
75+ cmd := &cobra.Command{
76+ Use: "list",
77+ Short: "List active preview environments",
78+ RunE: func(cmd *cobra.Command, args []string) error {
79+ // TODO: Wire to local service or admin API
80+ fmt.Println("No active previews")
81+ return nil
82+ },
83+ }
84+ cmd.Flags().BoolVar(&jsonOutput, "json", false, "Output as JSON")
85+ return cmd
86+}
87+
88+func newPreviewDeleteCmd() *cobra.Command {
89+ return &cobra.Command{
90+ Use: "delete <name>",
91+ Short: "Delete a preview environment",
92+ Args: cobra.ExactArgs(1),
93+ RunE: func(cmd *cobra.Command, args []string) error {
94+ // TODO: Wire to local service or admin API
95+ fmt.Printf("Deleting preview: %s\n", args[0])
96+ return nil
97+ },
98+ }
99+}
100+
101+func newPreviewExtendCmd() *cobra.Command {
102+ var ttl string
103+ cmd := &cobra.Command{
104+ Use: "extend <name>",
105+ Short: "Extend a preview environment's TTL",
106+ Args: cobra.ExactArgs(1),
107+ RunE: func(cmd *cobra.Command, args []string) error {
108+ // TODO: Wire to local service or admin API
109+ fmt.Printf("Extending preview %s by %s\n", args[0], ttl)
110+ return nil
111+ },
112+ }
113+ cmd.Flags().StringVar(&ttl, "ttl", "24h", "Additional TTL duration")
114+ return cmd
115+}
116+
117+func detectGitBranch() (string, error) {
118+ out, err := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD").Output() // #nosec G204
119+ if err != nil {
120+ return "", err
121+ }
122+ return strings.TrimSpace(string(out)), nil
123+}
@@ -0,0 +1,123 @@
1+package cli
2+
3+import (
4+ "fmt"
5+ "os/exec"
6+ "strings"
7+
8+ "github.com/bnema/gordon/internal/usecase/auto/preview"
9+ "github.com/spf13/cobra"
10+)
11+
12+func newPreviewCmd() *cobra.Command {
13+ cmd := &cobra.Command{
14+ Use: "preview [name]",
15+ Short: "Create or manage preview environments",
16+ Long: "Build, push, and deploy an ephemeral preview environment. Defaults to current git branch name.",
17+ }
18+
19+ createCmd := newPreviewCreateCmd()
20+
21+ cmd.AddCommand(
22+ createCmd,
23+ newPreviewListCmd(),
24+ newPreviewDeleteCmd(),
25+ newPreviewExtendCmd(),
26+ )
27+
28+ // Default action (no subcommand) = create
29+ cmd.RunE = createCmd.RunE
30+ cmd.Args = cobra.MaximumNArgs(1)
31+ cmd.Flags().AddFlagSet(createCmd.Flags())
32+
33+ return cmd
34+}
35+
36+func newPreviewCreateCmd() *cobra.Command {
37+ var ttl string
38+ var noBuild bool
39+ var noData bool
40+
41+ cmd := &cobra.Command{
42+ Use: "create [name]",
43+ Short: "Create a preview environment",
44+ Args: cobra.MaximumNArgs(1),
45+ RunE: func(cmd *cobra.Command, args []string) error {
46+ name := ""
47+ if len(args) > 0 {
48+ name = args[0]
49+ } else {
50+ branch, err := detectGitBranch()
51+ if err != nil {
52+ return fmt.Errorf("no preview name provided and could not detect git branch: %w", err)
53+ }
54+ name = preview.SanitizeBranchName(branch)
55+ }
56+
57+ fmt.Printf("Creating preview: %s\n", name)
58+ // TODO: Reuse push mechanics (build, tag with preview-{name}, push)
59+ // TODO: Wire to remote API or local service
60+ _ = ttl
61+ _ = noBuild
62+ _ = noData
63+ return nil
64+ },
65+ }
66+
67+ cmd.Flags().StringVar(&ttl, "ttl", "", "Override TTL (e.g., 72h)")
68+ cmd.Flags().BoolVar(&noBuild, "no-build", false, "Skip image build")
69+ cmd.Flags().BoolVar(&noData, "no-data", false, "Skip volume cloning")
70+ return cmd
71+}
72+
73+func newPreviewListCmd() *cobra.Command {
74+ var jsonOutput bool
75+ cmd := &cobra.Command{
76+ Use: "list",
77+ Short: "List active preview environments",
78+ RunE: func(cmd *cobra.Command, args []string) error {
79+ // TODO: Wire to local service or admin API
80+ fmt.Println("No active previews")
81+ return nil
82+ },
83+ }
84+ cmd.Flags().BoolVar(&jsonOutput, "json", false, "Output as JSON")
85+ return cmd
86+}
87+
88+func newPreviewDeleteCmd() *cobra.Command {
89+ return &cobra.Command{
90+ Use: "delete <name>",
91+ Short: "Delete a preview environment",
92+ Args: cobra.ExactArgs(1),
93+ RunE: func(cmd *cobra.Command, args []string) error {
94+ // TODO: Wire to local service or admin API
95+ fmt.Printf("Deleting preview: %s\n", args[0])
96+ return nil
97+ },
98+ }
99+}
100+
101+func newPreviewExtendCmd() *cobra.Command {
102+ var ttl string
103+ cmd := &cobra.Command{
104+ Use: "extend <name>",
105+ Short: "Extend a preview environment's TTL",
106+ Args: cobra.ExactArgs(1),
107+ RunE: func(cmd *cobra.Command, args []string) error {
108+ // TODO: Wire to local service or admin API
109+ fmt.Printf("Extending preview %s by %s\n", args[0], ttl)
110+ return nil
111+ },
112+ }
113+ cmd.Flags().StringVar(&ttl, "ttl", "24h", "Additional TTL duration")
114+ return cmd
115+}
116+
117+func detectGitBranch() (string, error) {
118+ out, err := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD").Output() // #nosec G204
119+ if err != nil {
120+ return "", err
121+ }
122+ return strings.TrimSpace(string(out)), nil
123+}
M internal/adapters/in/cli/root.go +4 -0
@@ -145,6 +145,10 @@ Commands are organized by where they run:
145145 autorouteCmd.GroupID = groupManage
146146 rootCmd.AddCommand(autorouteCmd)
147147
148+ previewCmd := newPreviewCmd()
149+ previewCmd.GroupID = groupManage
150+ rootCmd.AddCommand(previewCmd)
151+
148152 // Client-only commands (no server needed)
149153 remotesCmd := newRemotesCmd()
150154 remotesCmd.GroupID = groupClient
@@ -145,6 +145,10 @@ Commands are organized by where they run:
145 autorouteCmd.GroupID = groupManage145 autorouteCmd.GroupID = groupManage
146 rootCmd.AddCommand(autorouteCmd)146 rootCmd.AddCommand(autorouteCmd)
147 147
148+ previewCmd := newPreviewCmd()
149+ previewCmd.GroupID = groupManage
150+ rootCmd.AddCommand(previewCmd)
151+
148 // Client-only commands (no server needed)152 // Client-only commands (no server needed)
149 remotesCmd := newRemotesCmd()153 remotesCmd := newRemotesCmd()
150 remotesCmd.GroupID = groupClient154 remotesCmd.GroupID = groupClient

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close