@@ -2,11 +2,14 @@ package cli
22
33import (
44 "fmt"
5+ "io"
6+ "os"
57 "strings"
68 "text/tabwriter"
79
810 "github.com/spf13/cobra"
911
12+ "github.com/open-source-cloud/devstack/internal/config"
1013 "github.com/open-source-cloud/devstack/internal/lock"
1114 "github.com/open-source-cloud/devstack/internal/version"
1215 "github.com/open-source-cloud/devstack/internal/workspace"
@@ -76,14 +79,40 @@ func renderContextHeader(cmd *cobra.Command, mgr *workspace.Manager, g *GlobalOp
7679 fmt .Fprintf (cmd .OutOrStdout (), "devstack · %s\n \n " , strings .Join (parts , " · " ))
7780}
7881
82+ // renderPromptSegment prints a terse "workspace" or "workspace:project" segment
83+ // for a shell prompt (spec 30). It is deliberately cheap: it discovers the
84+ // workspace from config only (no Docker client, no ledger) and reads the project
85+ // from DEVSTACK_PROJECT (set by the `use` shell hook). Outside a workspace it
86+ // prints nothing, so the prompt segment simply disappears.
87+ func renderPromptSegment (cmd * cobra.Command ) error {
88+ cwd , err := os .Getwd ()
89+ if err != nil {
90+ return nil
91+ }
92+ m , err := config .Load (cwd )
93+ if err != nil {
94+ return nil
95+ }
96+ seg := m .Workspace .Name
97+ if p := os .Getenv ("DEVSTACK_PROJECT" ); p != "" {
98+ seg += ":" + p
99+ }
100+ fmt .Fprintln (cmd .OutOrStdout (), seg )
101+ return nil
102+ }
103+
79104// newContextCmd wires the read-only `context` command: print the resolved active
80105// workspace/project/role/docker-context/version. Lock-free.
81106func newContextCmd (g * GlobalOpts ) * cobra.Command {
82- return & cobra.Command {
107+ var promptMode bool
108+ cmd := & cobra.Command {
83109 Use : "context" ,
84110 Short : "Show the active workspace, project, role and Docker context" ,
85111 Args : cobra .NoArgs ,
86112 RunE : func (cmd * cobra.Command , _ []string ) error {
113+ if promptMode {
114+ return renderPromptSegment (cmd )
115+ }
87116 mgr , closeFn , err := buildManager (cmd )
88117 if err != nil {
89118 return err
@@ -110,6 +139,8 @@ func newContextCmd(g *GlobalOpts) *cobra.Command {
110139 return tw .Flush ()
111140 },
112141 }
142+ cmd .Flags ().BoolVar (& promptMode , "prompt" , false , "terse single-line output for a shell prompt segment (cheap; no Docker/ledger)" )
143+ return cmd
113144}
114145
115146// newUseCmd wires `use [name]`: set the active project (or switch to a registered
@@ -119,6 +150,7 @@ func newContextCmd(g *GlobalOpts) *cobra.Command {
119150func newUseCmd (g * GlobalOpts ) * cobra.Command {
120151 var project string
121152 var printScript bool
153+ var shell string
122154 cmd := & cobra.Command {
123155 Use : "use [name]" ,
124156 Short : "Set the active project (or switch workspace); persists across terminals" ,
@@ -162,9 +194,13 @@ func newUseCmd(g *GlobalOpts) *cobra.Command {
162194 targetRoot = root
163195 }
164196 default :
165- // Bare `use`: report current context + candidates (the fuzzy picker
166- // TUI lands in the shell-integration phase).
167- return printUseHint (cmd , mgr , projects )
197+ // Bare `use`: report current context + candidates. Under --print
198+ // the hint goes to stderr so stdout stays an eval-safe (empty) script.
199+ out := cmd .OutOrStdout ()
200+ if printScript {
201+ out = cmd .ErrOrStderr ()
202+ }
203+ return printUseHint (out , mgr , projects )
168204 }
169205
170206 if err := lock .WithLock (cmd .Context (), mgr .LockPath , func () error {
@@ -174,7 +210,7 @@ func newUseCmd(g *GlobalOpts) *cobra.Command {
174210 }
175211
176212 if printScript {
177- emitUseScript (cmd , targetRoot , targetProject )
213+ emitUseScript (cmd , targetRoot , targetProject , shell )
178214 return nil
179215 }
180216 if g .JSON {
@@ -194,6 +230,8 @@ func newUseCmd(g *GlobalOpts) *cobra.Command {
194230 }
195231 cmd .Flags ().StringVar (& project , "project" , "" , "force-select a project in the current workspace" )
196232 cmd .Flags ().BoolVar (& printScript , "print" , false , "emit an eval-able shell script (cd + export) instead of persisting only" )
233+ cmd .Flags ().StringVar (& shell , "shell" , "" , "syntax for --print output: fish (else POSIX sh/zsh/bash)" )
234+ _ = cmd .Flags ().MarkHidden ("shell" )
197235 return cmd
198236}
199237
@@ -211,11 +249,21 @@ func lookupWorkspaceRoot(mgr *workspace.Manager, name string) (string, bool, err
211249 return "" , false , nil
212250}
213251
214- // emitUseScript writes the POSIX eval script the shell wrapper runs. Fish support
215- // is handled by the `shell-init` wrapper (it re-emits in fish syntax).
216- func emitUseScript (cmd * cobra.Command , root , project string ) {
252+ // emitUseScript writes the eval script the shell wrapper runs: POSIX (sh/zsh/bash)
253+ // by default, fish syntax when shell=="fish". Single-quoted values are valid in
254+ // both. The `devstack` wrapper from `shell-init` eval's this to mutate the shell.
255+ func emitUseScript (cmd * cobra.Command , root , project , shell string ) {
217256 w := cmd .OutOrStdout ()
218257 fmt .Fprintf (w , "cd %s\n " , shellQuote (root ))
258+ if shell == "fish" {
259+ fmt .Fprintf (w , "set -gx DEVSTACK_WORKSPACE %s\n " , shellQuote (root ))
260+ if project != "" {
261+ fmt .Fprintf (w , "set -gx DEVSTACK_PROJECT %s\n " , shellQuote (project ))
262+ } else {
263+ fmt .Fprintln (w , "set -e DEVSTACK_PROJECT" )
264+ }
265+ return
266+ }
219267 fmt .Fprintf (w , "export DEVSTACK_WORKSPACE=%s\n " , shellQuote (root ))
220268 if project != "" {
221269 fmt .Fprintf (w , "export DEVSTACK_PROJECT=%s\n " , shellQuote (project ))
@@ -226,8 +274,7 @@ func emitUseScript(cmd *cobra.Command, root, project string) {
226274
227275// printUseHint reports the current active context and the selectable projects when
228276// `use` is invoked with no target.
229- func printUseHint (cmd * cobra.Command , mgr * workspace.Manager , projects []string ) error {
230- w := cmd .OutOrStdout ()
277+ func printUseHint (w io.Writer , mgr * workspace.Manager , projects []string ) error {
231278 active := resolveActiveProject (mgr .Model , mgr .DB )
232279 if active != "" {
233280 fmt .Fprintf (w , "active project: %s\n " , active )
0 commit comments