Skip to content

Commit 82c0d9f

Browse files
add --quiet flag to suppress silo log output
1 parent 0841fa9 commit 82c0d9f

4 files changed

Lines changed: 139 additions & 36 deletions

File tree

‎backend/fly/fly.go‎

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"encoding/json"
77
"fmt"
8+
"io"
89
"net"
910
"net/http"
1011
"os"
@@ -24,10 +25,11 @@ import (
2425
type Client struct {
2526
app string
2627
region string
28+
logw io.Writer
2729
}
2830

2931
// NewClient creates a new Fly.io backend client.
30-
func NewClient(app, region string) (*Client, error) {
32+
func NewClient(app, region string, logw io.Writer) (*Client, error) {
3133
if _, err := exec.LookPath("fly"); err != nil {
3234
return nil, fmt.Errorf("fly CLI not found (install from https://fly.io/docs/flyctl/install/): %w", err)
3335
}
@@ -43,7 +45,16 @@ func NewClient(app, region string) (*Client, error) {
4345
if region == "" {
4446
region = "syd"
4547
}
46-
return &Client{app: app, region: region}, nil
48+
if logw == nil {
49+
logw = os.Stderr
50+
}
51+
return &Client{app: app, region: region, logw: logw}, nil
52+
}
53+
54+
func (c *Client) logf(format string, args ...any) {
55+
if c.logw != nil {
56+
fmt.Fprintf(c.logw, format, args...)
57+
}
4758
}
4859

4960
// Close is a no-op.
@@ -204,40 +215,40 @@ func (c *Client) Run(ctx context.Context, opts backend.RunOptions) error {
204215
imageRef := fmt.Sprintf("registry.fly.io/%s:%s", c.app, opts.Image)
205216

206217
// 1. Create machine with sleep infinity (keeps it running for sync + reconnect)
207-
fmt.Fprintf(os.Stderr, " → Creating machine...\n")
218+
c.logf(" → Creating machine...\n")
208219
machineID, err := c.createMachine(ctx, imageRef, opts)
209220
if err != nil {
210221
return err
211222
}
212-
fmt.Fprintf(os.Stderr, " → Machine created: %s\n", machineID)
223+
c.logf(" → Machine created: %s\n", machineID)
213224

214225
// 2. Wait for machine to start
215-
fmt.Fprintf(os.Stderr, " → Waiting for machine to start...\n")
226+
c.logf(" → Waiting for machine to start...\n")
216227
if err := c.waitForState(ctx, machineID, "started", 5*time.Minute); err != nil {
217228
c.destroyMachine(ctx, machineID)
218229
return err
219230
}
220231

221232
// 3. Wait for SSH to be ready
222-
fmt.Fprintf(os.Stderr, " → Waiting for SSH...\n")
233+
c.logf(" → Waiting for SSH...\n")
223234
if err := c.waitForSSH(ctx, machineID, 60*time.Second); err != nil {
224235
c.destroyMachine(ctx, machineID)
225236
return err
226237
}
227238

228239
// 4. Start file sync (mutagen: initial sync + continuous bidirectional)
229-
fmt.Fprintf(os.Stderr, " → Syncing files...\n")
240+
c.logf(" → Syncing files...\n")
230241
stopSync, err := c.startMutagenSync(ctx, machineID, opts.MountsRO, opts.MountsRW, opts.CleanMountPaths)
231242
if err != nil {
232243
c.destroyMachine(ctx, machineID)
233244
return fmt.Errorf("file sync failed: %w", err)
234245
}
235-
fmt.Fprintf(os.Stderr, " → Files synced\n")
246+
c.logf(" → Files synced\n")
236247

237248
// 5. Run pre-run hooks (skip mount wait since files are already synced)
238249
hooks := filterMountWait(opts.PreRunHooks)
239250
if len(hooks) > 0 {
240-
fmt.Fprintf(os.Stderr, " → Running pre-run hooks...\n")
251+
c.logf(" → Running pre-run hooks...\n")
241252
hookScript := strings.Join(hooks, " && ")
242253
if err := c.sshExec(ctx, machineID, hookScript); err != nil {
243254
stopSync()
@@ -247,7 +258,7 @@ func (c *Client) Run(ctx context.Context, opts backend.RunOptions) error {
247258
}
248259

249260
// 6. Connect interactively with the tool command (via tmux)
250-
fmt.Fprintf(os.Stderr, " → Connecting...\n")
261+
c.logf(" → Connecting...\n")
251262
connectErr := c.connectInteractive(ctx, machineID, opts)
252263

253264
// 7. Check if the tmux session is still running (user detached vs tool exited).
@@ -256,16 +267,16 @@ func (c *Client) Run(ctx context.Context, opts backend.RunOptions) error {
256267
tmuxAlive := c.isTmuxSessionAlive(ctx, machineID)
257268

258269
// 8. Stop continuous sync (flushes final changes)
259-
fmt.Fprintf(os.Stderr, " → Syncing final changes...\n")
270+
c.logf(" → Syncing final changes...\n")
260271
stopSync()
261272

262273
if tmuxAlive {
263-
fmt.Fprintf(os.Stderr, " → Detached. Machine %s still running — use 'silo reconnect %s --backend fly' to reattach.\n", machineID, opts.Name)
274+
c.logf(" → Detached. Machine %s still running — use 'silo reconnect %s --backend fly' to reattach.\n", machineID, opts.Name)
264275
return nil
265276
}
266277

267278
// Tool exited, destroy the machine
268-
fmt.Fprintf(os.Stderr, " → Destroying machine...\n")
279+
c.logf(" → Destroying machine...\n")
269280
destroyCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
270281
defer cancel()
271282
c.destroyMachine(destroyCtx, machineID)
@@ -297,15 +308,15 @@ func (c *Client) Reconnect(ctx context.Context, name string, opts backend.RunOpt
297308
}
298309

299310
// Re-sync files (don't clean mount paths — tool is already running)
300-
fmt.Fprintf(os.Stderr, " → Syncing files...\n")
311+
c.logf(" → Syncing files...\n")
301312
stopSync, err := c.startMutagenSync(ctx, machineID, opts.MountsRO, opts.MountsRW, nil)
302313
if err != nil {
303314
return fmt.Errorf("file sync failed: %w", err)
304315
}
305-
fmt.Fprintf(os.Stderr, " → Files synced\n")
316+
c.logf(" → Files synced\n")
306317

307318
// Reattach to the tmux session
308-
fmt.Fprintf(os.Stderr, " → Reconnecting...\n")
319+
c.logf(" → Reconnecting...\n")
309320
user := currentUsername()
310321
if user == "" {
311322
user = "root"
@@ -317,16 +328,16 @@ func (c *Client) Reconnect(ctx context.Context, name string, opts backend.RunOpt
317328
tmuxAlive := c.isTmuxSessionAlive(ctx, machineID)
318329

319330
// Stop sync (flush final changes)
320-
fmt.Fprintf(os.Stderr, " → Syncing final changes...\n")
331+
c.logf(" → Syncing final changes...\n")
321332
stopSync()
322333

323334
if tmuxAlive {
324-
fmt.Fprintf(os.Stderr, " → Detached. Machine %s still running — use 'silo reconnect %s --backend fly' to reattach.\n", machineID, name)
335+
c.logf(" → Detached. Machine %s still running — use 'silo reconnect %s --backend fly' to reattach.\n", machineID, name)
325336
return nil
326337
}
327338

328339
// Tool exited, destroy the machine
329-
fmt.Fprintf(os.Stderr, " → Destroying machine...\n")
340+
c.logf(" → Destroying machine...\n")
330341
destroyCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
331342
defer cancel()
332343
c.destroyMachine(destroyCtx, machineID)
@@ -759,7 +770,7 @@ func (c *Client) startMutagenSync(ctx context.Context, machineID string, mountsR
759770
return nil, fmt.Errorf("mutagen is required for the fly backend (install from https://mutagen.io): %w", err)
760771
}
761772

762-
progress := newSyncProgress(os.Stderr)
773+
progress := newSyncProgress(c.logw)
763774

764775
// Create SSH tunnel for mutagen (fly proxy + issued certs + ControlMaster)
765776
progress.setPhase("Establishing SSH tunnel...")
@@ -896,7 +907,7 @@ func (c *Client) startMutagenSync(ctx context.Context, machineID string, mountsR
896907
}
897908
localPath = target
898909
} else {
899-
fmt.Fprintf(os.Stderr, " mutagen: warning: cannot resolve %s: %v\n", m.path, err)
910+
c.logf(" mutagen: warning: cannot resolve %s: %v\n", m.path, err)
900911
}
901912
}
902913

‎main.go‎

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,48 @@ func runMain(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
7979
rootCmd.SetErr(stderr)
8080

8181
if err := rootCmd.Execute(); err != nil {
82-
cli.LogErrorTo(stderr, "%v", err)
82+
if !hasQuietArg(args) {
83+
cli.LogErrorTo(stderr, "%v", err)
84+
}
8385
return 1
8486
}
8587
return 0
8688
}
8789

90+
func hasQuietArg(args []string) bool {
91+
for _, arg := range args {
92+
if arg == "--" {
93+
break
94+
}
95+
if arg == "--quiet" || arg == "-q" {
96+
return true
97+
}
98+
if strings.HasPrefix(arg, "-") && !strings.HasPrefix(arg, "--") && strings.Contains(arg[1:], "q") {
99+
return true
100+
}
101+
}
102+
return false
103+
}
104+
105+
func quietFlag(cmd *cobra.Command) bool {
106+
if cmd == nil {
107+
return false
108+
}
109+
if f := cmd.Flags().Lookup("quiet"); f != nil {
110+
if q, err := cmd.Flags().GetBool("quiet"); err == nil && q {
111+
return true
112+
}
113+
}
114+
if root := cmd.Root(); root != nil && root != cmd {
115+
if f := root.Flags().Lookup("quiet"); f != nil {
116+
if q, err := root.Flags().GetBool("quiet"); err == nil && q {
117+
return true
118+
}
119+
}
120+
}
121+
return false
122+
}
123+
88124
func newRootCmd(stdout, stderr io.Writer) *cobra.Command {
89125
rootCmd := &cobra.Command{
90126
Use: "silo",
@@ -135,6 +171,7 @@ Configuration is loaded from (in order, merged):
135171
rootCmd.Flags().Bool("force-build", false, "Force rebuild of container image")
136172
rootCmd.Flags().Bool("no-cache", false, "Disable build cache (implies --force-build)")
137173
rootCmd.Flags().BoolP("verbose", "v", false, "Show detailed output instead of progress bar")
174+
rootCmd.Flags().BoolP("quiet", "q", false, "Suppress silo logs and render only tool output")
138175
rootCmd.Flags().String("tool-version", "", "Pin a specific tool version (forces synchronous build)")
139176
addMountFlags(rootCmd)
140177

@@ -161,6 +198,7 @@ Configuration is loaded from (in order, merged):
161198
toolCmd.Flags().Bool("force-build", false, "Force rebuild of container image")
162199
toolCmd.Flags().Bool("no-cache", false, "Disable build cache (implies --force-build)")
163200
toolCmd.Flags().BoolP("verbose", "v", false, "Show detailed output instead of progress bar")
201+
toolCmd.Flags().BoolP("quiet", "q", false, "Suppress silo logs and render only tool output")
164202
toolCmd.Flags().String("entrypoint", "", "Run a custom command instead of the tool (e.g. /bin/bash)")
165203
toolCmd.Flags().String("tool-version", "", "Pin a specific tool version (forces synchronous build)")
166204
addMountFlags(toolCmd)
@@ -399,6 +437,9 @@ func runSilo(cmd *cobra.Command, args []string, stdout, stderr io.Writer) error
399437
// Get verbose flag
400438
verbose, _ := cmd.Flags().GetBool("verbose")
401439

440+
// Get quiet flag
441+
quiet := quietFlag(cmd)
442+
402443
// Get tool-version flag
403444
toolVersion, _ := cmd.Flags().GetString("tool-version")
404445

@@ -416,6 +457,7 @@ func runSilo(cmd *cobra.Command, args []string, stdout, stderr io.Writer) error
416457
ForceBuild: forceBuild,
417458
NoCache: noCache,
418459
Verbose: verbose,
460+
Quiet: quiet,
419461
ToolVersion: toolVersion,
420462
ExtraMountsRW: extraMountsRW,
421463
ExtraMountsRO: extraMountsRO,
@@ -451,6 +493,9 @@ func runTool(cmd *cobra.Command, toolDef tools.Tool, args []string, stdout, stde
451493
// Get verbose flag
452494
verbose, _ := cmd.Flags().GetBool("verbose")
453495

496+
// Get quiet flag
497+
quiet := quietFlag(cmd)
498+
454499
// Get entrypoint flag
455500
entrypoint, _ := cmd.Flags().GetString("entrypoint")
456501

@@ -472,6 +517,7 @@ func runTool(cmd *cobra.Command, toolDef tools.Tool, args []string, stdout, stde
472517
ForceBuild: forceBuild,
473518
NoCache: noCache,
474519
Verbose: verbose,
520+
Quiet: quiet,
475521
Entrypoint: entrypoint,
476522
ToolVersion: toolVersion,
477523
ExtraMountsRW: extraMountsRW,
@@ -1073,7 +1119,7 @@ func createBackendByType(backendType string, cfg config.Config) (backend.Backend
10731119
case "container":
10741120
return applecontainer.NewClient()
10751121
case "fly":
1076-
return flybackend.NewClient(cfg.Backends.Fly.App, cfg.Backends.Fly.Region)
1122+
return flybackend.NewClient(cfg.Backends.Fly.App, cfg.Backends.Fly.Region, os.Stderr)
10771123
default:
10781124
return nil, fmt.Errorf("unknown backend: %s", backendType)
10791125
}

‎main_test.go‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,31 @@ func TestToolMountFlags(t *testing.T) {
116116
}
117117
}
118118

119+
func TestQuietFlags(t *testing.T) {
120+
rootCmd := newRootCmd(io.Discard, io.Discard)
121+
122+
rootQuiet := rootCmd.Flags().Lookup("quiet")
123+
if rootQuiet == nil {
124+
t.Fatal("expected --quiet flag on root command")
125+
}
126+
if rootQuiet.Shorthand != "q" {
127+
t.Fatalf("expected root --quiet shorthand to be -q, got %q", rootQuiet.Shorthand)
128+
}
129+
130+
claudeCmd, _, err := rootCmd.Find([]string{"claude"})
131+
if err != nil {
132+
t.Fatalf("failed to find claude command: %v", err)
133+
}
134+
135+
toolQuiet := claudeCmd.Flags().Lookup("quiet")
136+
if toolQuiet == nil {
137+
t.Fatal("expected --quiet flag on claude command")
138+
}
139+
if toolQuiet.Shorthand != "q" {
140+
t.Fatalf("expected claude --quiet shorthand to be -q, got %q", toolQuiet.Shorthand)
141+
}
142+
}
143+
119144
func TestConfigShowCommand(t *testing.T) {
120145
exitCode, stdout, _ := testcli.Main(t, []string{"config", "show"}, nil, mainFunc)
121146

@@ -325,6 +350,19 @@ func TestInvalidTool(t *testing.T) {
325350
}
326351
}
327352

353+
func TestQuietSuppressesTopLevelErrors(t *testing.T) {
354+
var stdout, stderr bytes.Buffer
355+
356+
exitCode := runMain([]string{"--quiet", "invalid-tool"}, nil, &stdout, &stderr)
357+
358+
if exitCode == 0 {
359+
t.Fatal("expected failure for invalid tool")
360+
}
361+
if stderr.Len() != 0 {
362+
t.Fatalf("expected no stderr output with --quiet, got: %s", stderr.String())
363+
}
364+
}
365+
328366
func TestCompletionCommand(t *testing.T) {
329367
shells := []string{"bash", "zsh", "fish", "powershell"}
330368

0 commit comments

Comments
 (0)