diff --git a/cli/cmd/encore/exec.go b/cli/cmd/encore/exec.go index b6f964b586..90db36bd03 100644 --- a/cli/cmd/encore/exec.go +++ b/cli/cmd/encore/exec.go @@ -8,6 +8,7 @@ import ( "os/signal" "github.com/spf13/cobra" + "golang.org/x/term" "encr.dev/cli/cmd/encore/cmdutil" "encr.dev/cli/cmd/encore/root" @@ -66,12 +67,13 @@ func execScript(appRoot, relWD string, args []string) { defer func() { _ = os.RemoveAll(tempDir) }() stream, err := daemon.ExecSpec(ctx, &daemonpb.ExecSpecRequest{ - AppRoot: appRoot, - WorkingDir: relWD, - ScriptArgs: args, - Environ: os.Environ(), - Namespace: nonZeroPtr(nsName), - TempDir: tempDir, + AppRoot: appRoot, + WorkingDir: relWD, + ScriptArgs: args, + Environ: os.Environ(), + Namespace: nonZeroPtr(nsName), + TempDir: tempDir, + NonInteractive: !term.IsTerminal(int(os.Stderr.Fd())), }) if err != nil { fatal(err) @@ -120,12 +122,13 @@ func execScript(appRoot, relWD string, args []string) { // For Go apps, use the streaming ExecScript RPC. stream, err := daemon.ExecScript(ctx, &daemonpb.ExecScriptRequest{ - AppRoot: appRoot, - WorkingDir: relWD, - ScriptArgs: args, - Environ: os.Environ(), - TraceFile: root.TraceFile, - Namespace: nonZeroPtr(nsName), + AppRoot: appRoot, + WorkingDir: relWD, + ScriptArgs: args, + Environ: os.Environ(), + TraceFile: root.TraceFile, + Namespace: nonZeroPtr(nsName), + NonInteractive: !term.IsTerminal(int(os.Stderr.Fd())), }) if err != nil { fatal(err) diff --git a/cli/cmd/encore/run.go b/cli/cmd/encore/run.go index e199a5ac85..3636f5acb1 100644 --- a/cli/cmd/encore/run.go +++ b/cli/cmd/encore/run.go @@ -137,6 +137,7 @@ func runApp(appRoot, wd string) { Browser: browserMode, LogLevel: nonZeroPtr(logLevel.Value), ScrubSensitiveData: scrubSensitiveData, + NonInteractive: !term.IsTerminal(int(os.Stderr.Fd())), }) if err != nil { fatal(err) diff --git a/cli/daemon/exec_script.go b/cli/daemon/exec_script.go index 2f574ad9e3..819f43f3a1 100644 --- a/cli/daemon/exec_script.go +++ b/cli/daemon/exec_script.go @@ -56,7 +56,12 @@ func (s *Server) ExecScript(req *daemonpb.ExecScriptRequest, stream daemonpb.Dae return nil } - ops := optracker.New(stderr, stream) + var ops *optracker.OpTracker + if req.NonInteractive { + ops = optracker.NewLineMode(stderr) + } else { + ops = optracker.New(stderr, stream) + } defer ops.AllDone() // Kill the tracker when we exit this function testResults := make(chan error, 1) @@ -163,7 +168,12 @@ func (s *Server) ExecSpec(req *daemonpb.ExecSpecRequest, stream daemonpb.Daemon_ return nil } - ops := optracker.New(stderr, adapter) + var ops *optracker.OpTracker + if req.NonInteractive { + ops = optracker.NewLineMode(stderr) + } else { + ops = optracker.New(stderr, adapter) + } defer ops.AllDone() defer func() { diff --git a/cli/daemon/run.go b/cli/daemon/run.go index 5ccdbd2594..558a144287 100644 --- a/cli/daemon/run.go +++ b/cli/daemon/run.go @@ -95,7 +95,12 @@ func (s *Server) Run(req *daemonpb.RunRequest, stream daemonpb.Daemon_RunServer) return nil } - ops := optracker.New(stderr, stream) + var ops *optracker.OpTracker + if req.NonInteractive { + ops = optracker.NewLineMode(stderr) + } else { + ops = optracker.New(stderr, stream) + } defer ops.AllDone() // Kill the tracker when we exit this function // Check for available update before we start the proc diff --git a/proto/encore/daemon/daemon.pb.go b/proto/encore/daemon/daemon.pb.go index 1bbc7c56ad..79a92bd864 100644 --- a/proto/encore/daemon/daemon.pb.go +++ b/proto/encore/daemon/daemon.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 -// protoc v6.32.1 +// protoc-gen-go v1.36.11 +// protoc v7.34.1 // source: encore/daemon/daemon.proto package daemon @@ -649,8 +649,12 @@ type RunRequest struct { LogLevel *string `protobuf:"bytes,12,opt,name=log_level,json=logLevel,proto3,oneof" json:"log_level,omitempty"` // scrub_sensitive_data, if true, scrubs sensitive data from local traces. ScrubSensitiveData bool `protobuf:"varint,13,opt,name=scrub_sensitive_data,json=scrubSensitiveData,proto3" json:"scrub_sensitive_data,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // non_interactive, if true, signals that the CLI is not connected to an + // interactive terminal. The build progress UI is rendered as plain + // one-line-per-event output instead of the spinner. + NonInteractive bool `protobuf:"varint,14,opt,name=non_interactive,json=nonInteractive,proto3" json:"non_interactive,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RunRequest) Reset() { @@ -760,6 +764,13 @@ func (x *RunRequest) GetScrubSensitiveData() bool { return false } +func (x *RunRequest) GetNonInteractive() bool { + if x != nil { + return x.NonInteractive + } + return false +} + type RunSpecRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // app_root is the absolute filesystem path to the Encore app root. @@ -1475,9 +1486,12 @@ type ExecScriptRequest struct { TraceFile *string `protobuf:"bytes,6,opt,name=trace_file,json=traceFile,proto3,oneof" json:"trace_file,omitempty"` // namespace is the infrastructure namespace to use. // If empty the active namespace is used. - Namespace *string `protobuf:"bytes,7,opt,name=namespace,proto3,oneof" json:"namespace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Namespace *string `protobuf:"bytes,7,opt,name=namespace,proto3,oneof" json:"namespace,omitempty"` + // non_interactive, if true, signals that the CLI is not connected to an + // interactive terminal. See RunRequest.non_interactive for details. + NonInteractive bool `protobuf:"varint,8,opt,name=non_interactive,json=nonInteractive,proto3" json:"non_interactive,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ExecScriptRequest) Reset() { @@ -1552,6 +1566,13 @@ func (x *ExecScriptRequest) GetNamespace() string { return "" } +func (x *ExecScriptRequest) GetNonInteractive() bool { + if x != nil { + return x.NonInteractive + } + return false +} + type ExecSpecRequest struct { state protoimpl.MessageState `protogen:"open.v1"` AppRoot string `protobuf:"bytes,1,opt,name=app_root,json=appRoot,proto3" json:"app_root,omitempty"` @@ -1565,9 +1586,12 @@ type ExecSpecRequest struct { Namespace *string `protobuf:"bytes,7,opt,name=namespace,proto3,oneof" json:"namespace,omitempty"` // temp_dir is a temp dir that will be cleaned up by the CLI after the command // has been executed, to write things like app meta and runtime config etc. - TempDir string `protobuf:"bytes,8,opt,name=temp_dir,json=tempDir,proto3" json:"temp_dir,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + TempDir string `protobuf:"bytes,8,opt,name=temp_dir,json=tempDir,proto3" json:"temp_dir,omitempty"` + // non_interactive, if true, signals that the CLI is not connected to an + // interactive terminal. See RunRequest.non_interactive for details. + NonInteractive bool `protobuf:"varint,9,opt,name=non_interactive,json=nonInteractive,proto3" json:"non_interactive,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ExecSpecRequest) Reset() { @@ -1642,6 +1666,13 @@ func (x *ExecSpecRequest) GetTempDir() string { return "" } +func (x *ExecSpecRequest) GetNonInteractive() bool { + if x != nil { + return x.NonInteractive + } + return false +} + type ExecSpecMessage struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Msg: @@ -4495,7 +4526,7 @@ const file_encore_daemon_daemon_proto_rawDesc = "" + "\btemplate\x18\x02 \x01(\tR\btemplate\x12\x1a\n" + "\btutorial\x18\x03 \x01(\bR\btutorial\"*\n" + "\x11CreateAppResponse\x12\x15\n" + - "\x06app_id\x18\x01 \x01(\tR\x05appId\"\xf1\x04\n" + + "\x06app_id\x18\x01 \x01(\tR\x05appId\"\x9a\x05\n" + "\n" + "RunRequest\x12\x19\n" + "\bapp_root\x18\x01 \x01(\tR\aappRoot\x12\x1f\n" + @@ -4513,7 +4544,8 @@ const file_encore_daemon_daemon_proto_rawDesc = "" + "\n" + "debug_mode\x18\v \x01(\x0e2#.encore.daemon.RunRequest.DebugModeR\tdebugMode\x12 \n" + "\tlog_level\x18\f \x01(\tH\x02R\blogLevel\x88\x01\x01\x120\n" + - "\x14scrub_sensitive_data\x18\r \x01(\bR\x12scrubSensitiveData\"F\n" + + "\x14scrub_sensitive_data\x18\r \x01(\bR\x12scrubSensitiveData\x12'\n" + + "\x0fnon_interactive\x18\x0e \x01(\bR\x0enonInteractive\"F\n" + "\vBrowserMode\x12\x10\n" + "\fBROWSER_AUTO\x10\x00\x12\x11\n" + "\rBROWSER_NEVER\x10\x01\x12\x12\n" + @@ -4580,7 +4612,7 @@ const file_encore_daemon_daemon_proto_rawDesc = "" + "\x10TestSpecResponse\x12\x18\n" + "\acommand\x18\x01 \x01(\tR\acommand\x12\x12\n" + "\x04args\x18\x02 \x03(\tR\x04args\x12\x18\n" + - "\aenviron\x18\x03 \x03(\tR\aenviron\"\xee\x01\n" + + "\aenviron\x18\x03 \x03(\tR\aenviron\"\x97\x02\n" + "\x11ExecScriptRequest\x12\x19\n" + "\bapp_root\x18\x01 \x01(\tR\aappRoot\x12\x1f\n" + "\vworking_dir\x18\x02 \x01(\tR\n" + @@ -4590,10 +4622,11 @@ const file_encore_daemon_daemon_proto_rawDesc = "" + "\aenviron\x18\x05 \x03(\tR\aenviron\x12\"\n" + "\n" + "trace_file\x18\x06 \x01(\tH\x00R\ttraceFile\x88\x01\x01\x12!\n" + - "\tnamespace\x18\a \x01(\tH\x01R\tnamespace\x88\x01\x01B\r\n" + + "\tnamespace\x18\a \x01(\tH\x01R\tnamespace\x88\x01\x01\x12'\n" + + "\x0fnon_interactive\x18\b \x01(\bR\x0enonInteractiveB\r\n" + "\v_trace_fileB\f\n" + "\n" + - "_namespace\"\xd4\x01\n" + + "_namespace\"\xfd\x01\n" + "\x0fExecSpecRequest\x12\x19\n" + "\bapp_root\x18\x01 \x01(\tR\aappRoot\x12\x1f\n" + "\vworking_dir\x18\x02 \x01(\tR\n" + @@ -4602,7 +4635,8 @@ const file_encore_daemon_daemon_proto_rawDesc = "" + "scriptArgs\x12\x18\n" + "\aenviron\x18\x05 \x03(\tR\aenviron\x12!\n" + "\tnamespace\x18\a \x01(\tH\x00R\tnamespace\x88\x01\x01\x12\x19\n" + - "\btemp_dir\x18\b \x01(\tR\atempDirB\f\n" + + "\btemp_dir\x18\b \x01(\tR\atempDir\x12'\n" + + "\x0fnon_interactive\x18\t \x01(\bR\x0enonInteractiveB\f\n" + "\n" + "_namespace\"\x87\x01\n" + "\x0fExecSpecMessage\x126\n" + diff --git a/proto/encore/daemon/daemon.proto b/proto/encore/daemon/daemon.proto index cda0221af8..2d61e9653b 100644 --- a/proto/encore/daemon/daemon.proto +++ b/proto/encore/daemon/daemon.proto @@ -128,6 +128,11 @@ message RunRequest { // scrub_sensitive_data, if true, scrubs sensitive data from local traces. bool scrub_sensitive_data = 13; + // non_interactive, if true, signals that the CLI is not connected to an + // interactive terminal. The build progress UI is rendered as plain + // one-line-per-event output instead of the spinner. + bool non_interactive = 14; + enum BrowserMode { BROWSER_AUTO = 0; BROWSER_NEVER = 1; @@ -262,6 +267,10 @@ message ExecScriptRequest { // namespace is the infrastructure namespace to use. // If empty the active namespace is used. optional string namespace = 7; + + // non_interactive, if true, signals that the CLI is not connected to an + // interactive terminal. See RunRequest.non_interactive for details. + bool non_interactive = 8; } message ExecSpecRequest { @@ -280,6 +289,10 @@ message ExecSpecRequest { // temp_dir is a temp dir that will be cleaned up by the CLI after the command // has been executed, to write things like app meta and runtime config etc. string temp_dir = 8; + + // non_interactive, if true, signals that the CLI is not connected to an + // interactive terminal. See RunRequest.non_interactive for details. + bool non_interactive = 9; } message ExecSpecMessage {