Skip to content

Commit

Permalink
Implement new command run-locally.
Browse files Browse the repository at this point in the history
The main advantage is that it supports executing interactive commands,
including runme-in-runme, and pipes (|).

On top of that, there is the new package internal/command. It gives
a better interface to create commands, as well as unifies the idea
of a program config that now is defined in a proto file.
  • Loading branch information
adambabik committed Feb 2, 2024
1 parent 81e29e8 commit efb5d85
Show file tree
Hide file tree
Showing 43 changed files with 6,730 additions and 9 deletions.
5 changes: 3 additions & 2 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,16 @@ exit 1
```sh {"id":"01HF7BT3HD84GWTQB8ZNTNW63E","name":"print-name"}
echo -n "Enter your name: "
read name
echo "\nHi, $name!"
echo ""
echo "Hi, $name!"
```

## JavaScript

It can also execute a snippet of JavaScript code:

```js {"id":"01HF7BT3HD84GWTQB8ZPB8TH53","name":"hello-js"}
console.log("Hello World!")
console.log("Hello World!");
```

## Go
Expand Down
25 changes: 25 additions & 0 deletions examples/goexec/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package main

import (
"log"
"os"
"os/exec"
)

func main() {
cmd := exec.Cmd{
Path: "/usr/local/bin/bash",
Args: []string{"-l", "-c", "python"},
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
}

if err := cmd.Start(); err != nil {
log.Fatalf("failed to start: %v", err)
}

if err := cmd.Wait(); err != nil {
log.Fatalf("failed to wait: %v", err)
}
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ require (
github.com/Masterminds/semver/v3 v3.2.1
github.com/Microsoft/go-winio v0.6.1
github.com/atotto/clipboard v0.1.4
github.com/bufbuild/connect-go v1.10.0
github.com/charmbracelet/bubbletea v0.25.0
github.com/charmbracelet/lipgloss v0.9.1
github.com/cli/cli/v2 v2.42.1
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81
github.com/creack/pty v1.1.21
github.com/fatih/color v1.16.0
github.com/go-git/go-billy/v5 v5.5.0
github.com/gobwas/glob v0.2.3
github.com/golang/mock v1.6.0
github.com/google/go-github/v45 v45.2.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A=
github.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE=
github.com/bufbuild/connect-go v1.10.0 h1:QAJ3G9A1OYQW2Jbk3DeoJbkCxuKArrvZgDt47mjdTbg=
github.com/bufbuild/connect-go v1.10.0/go.mod h1:CAIePUgkDR5pAFaylSMtNK45ANQjp9JvpluG20rhpV8=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/charmbracelet/bubbles v0.17.1 h1:0SIyjOnkrsfDo88YvPgAWvZMwXe26TP6drRvmkjyUu4=
github.com/charmbracelet/bubbles v0.17.1/go.mod h1:9HxZWlkCqz2PRwsCbYl7a3KXvGzFaDHpYbSYMJ+nE3o=
Expand Down Expand Up @@ -77,6 +79,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74=
github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
Expand Down
55 changes: 55 additions & 0 deletions internal/api/runme/runner/v2alpha1/config.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
syntax = "proto3";

package runme.runner.v2alpha1;

option go_package = "github.com/stateful/runme/internal/gen/proto/go/runme/runner/v1;runnerv1";

enum CommandMode {
COMMAND_MODE_UNSPECIFIED = 0;
COMMAND_MODE_INLINE = 1;
COMMAND_MODE_FILE = 2;
}

// ProgramConfig is a configuration for a program to execute.
// From this configuration, any program can be built.
message ProgramConfig {
// program_name is a name of the program to execute.
// If it's not a path (relative or absolute), the runner
// will try to resolve the name.
// For example: "sh", "/bin/bash".
string program_name = 1;

// arguments is a list of arguments passed to the program.
repeated string arguments = 2;

// directory to execute the program in.
string directory = 3;

// env is a list of additional environment variables
// that will be injected to the executed program.
repeated string env = 4;

oneof source {
// commands are commands to be executed by the program.
// The commands are joined and executed as a script.
CommandList commands = 5;

// script is code to be executed by the program.
// Individual lines are joined with the new line character.
string script = 6;
}

// interactive, if true, uses a pseudo-tty to execute the program.
bool interactive = 7;

// TODO(adamb): understand motivation for this. In theory, source
// should tell whether to execute it inline or as a file.
CommandMode mode = 8;

message CommandList {
// commands are commands to be executed by the program.
// The commands are joined and executed as a script.
// For example: ["echo 'Hello, World'", "ls -l /etc"].
repeated string items = 1;
}
}
169 changes: 169 additions & 0 deletions internal/api/runme/runner/v2alpha1/runner.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
syntax = "proto3";

package runme.runner.v2alpha1;

import "google/protobuf/wrappers.proto";
import "runme/runner/v2alpha1/config.proto";

option go_package = "github.com/stateful/runme/internal/gen/proto/go/runme/runner/v1;runnerv1";

message Project {
// root is a root directory of the project.
// The semantic is the same as for the "--project"
// flag in "runme".
string root = 1;

// env_load_order is list of environment files
// to try and load env from.
repeated string env_load_order = 2;
}

message Session {
string id = 1;

// env keeps track of session environment variables.
// They can be modified by executing programs which
// alter them through "export" and "unset" commands.
repeated string env = 2;

// metadata is a map of client specific metadata.
map<string, string> metadata = 3;
}

message CreateSessionRequest {
// metadata is a map of client specific metadata.
map<string, string> metadata = 1;

// env field provides an initial set of environment variables
// for a newly created session.
repeated string env = 2;

// project from which to load environment variables.
// They will be appended to the list from the env field.
// The env field has a higher priority.
optional Project project = 3;
}

message CreateSessionResponse {
Session session = 1;
}

message GetSessionRequest {
string id = 1;
}

message GetSessionResponse {
Session session = 1;
}

message ListSessionsRequest {}

message ListSessionsResponse {
repeated Session sessions = 1;
}

message DeleteSessionRequest {
string id = 1;
}

message DeleteSessionResponse {}

enum ExecuteStop {
EXECUTE_STOP_UNSPECIFIED = 0;
EXECUTE_STOP_INTERRUPT = 1;
EXECUTE_STOP_KILL = 2;
}

// SessionStrategy determines a session selection in
// an initial execute request.
enum SessionStrategy {
// Uses the session_id field to determine the session.
// If none is present, a new session is created.
SESSION_STRATEGY_UNSPECIFIED = 0;
// Uses the most recent session on the server.
// If there is none, a new one is created.
SESSION_STRATEGY_MOST_RECENT = 1;
}

message Winsize {
uint32 rows = 1;
uint32 cols = 2;
uint32 x = 3;
uint32 y = 4;
}

message ExecuteRequest {
runme.runner.v2alpha1.ProgramConfig config = 1;

// input_data is a byte array that will be send as input
// to the program.
bytes input_data = 8;

// stop requests the running process to be stopped.
// It is allowed only in the consecutive calls.
ExecuteStop stop = 9;

// sets pty winsize
// has no effect in non-interactive mode
optional Winsize winsize = 10;

// background, if true, will send process' PID as a first response.
bool background = 11;

// session_id indicates in which Session the program should execute.
// Executing in a Session might provide additional context like
// environment variables.
string session_id = 20;

// session_strategy is a strategy for selecting the session.
SessionStrategy session_strategy = 21;

// project used to load environment variables from .env files.
optional Project project = 22;

// store_last_output, if true, will store the stdout of
// the last ran block in the environment variable `__`.
bool store_last_output = 23;

// language_id indicates a language to exeucute scripts/commands.
string language_id = 25;

// file_extension is associated with the script.
string file_extension = 26;
}

message ProcessPID {
int64 pid = 1;
}

message ExecuteResponse {
// exit_code is sent only in the final message.
google.protobuf.UInt32Value exit_code = 1;

// stdout_data contains bytes from stdout since the last response.
bytes stdout_data = 2;

// stderr_data contains bytes from stderr since the last response.
bytes stderr_data = 3;

// pid contains the process' PID.
// This is only sent once in an initial response
// for background processes.
ProcessPID pid = 4;
}

service RunnerService {
rpc CreateSession(CreateSessionRequest) returns (CreateSessionResponse) {}
rpc GetSession(GetSessionRequest) returns (GetSessionResponse) {}
rpc ListSessions(ListSessionsRequest) returns (ListSessionsResponse) {}
rpc DeleteSession(DeleteSessionRequest) returns (DeleteSessionResponse) {}

// Execute executes a program. Examine "ExecuteRequest" to explore
// configuration options.
//
// It's a bidirectional stream RPC method. It expects the first
// "ExecuteRequest" to contain details of a program to execute.
// Subsequent "ExecuteRequest" should only contain "input_data" as
// other fields will be ignored.
rpc Execute(stream ExecuteRequest) returns (stream ExecuteResponse) {}
}
4 changes: 2 additions & 2 deletions internal/cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,13 +350,13 @@ func promptEnvVars(cmd *cobra.Command, envs []string, tasks ...project.Task) err
varPrompts := getCommandExportExtractMatches(block.Lines())
for _, ev := range varPrompts {
if slices.Contains(keys, ev.Key) {
block.GetBlock().SetLine(ev.LineNumber, "")
block.SetLine(ev.LineNumber, "")

continue
}

newVal, err := promptForEnvVar(cmd, ev)
block.GetBlock().SetLine(ev.LineNumber, replaceVarValue(ev, newVal))
block.SetLine(ev.LineNumber, replaceVarValue(ev, newVal))

if err != nil {
return err
Expand Down
1 change: 1 addition & 0 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ func Root() *cobra.Command {
cmd.AddCommand(printCmd())
cmd.AddCommand(extensionCmd())
cmd.AddCommand(runCmd())
cmd.AddCommand(runLocally())
cmd.AddCommand(serverCmd())
cmd.AddCommand(shellCmd())
cmd.AddCommand(suggestCmd)
Expand Down
Loading

0 comments on commit efb5d85

Please sign in to comment.