Repository: puppetlabs/wash
Pull request #258 (2019-05-16 17:50:13)

Add ability to follow Wash history
Adds `-f,--follow` option to the history command so you can watch for new entries added to history or follow updates to a journal for an in-progress command. This is particularly useful when a command has been running for awhile and you want to see a more detailed look at what it's doing.

Signed-off-by: Michael Smith <michael.smith@puppet.com>


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 21a42cb..b7f220f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -32,6 +32,10 @@ Versioning for the kubernetes and docker projects don't work well with Go module
 
 It should always be safe to run `go get -u=patch` to pickup patches.
 
+### Modifying APIs
+
+When making changes to Wash's APIs, remember to update the inline swagger documentation. Instructions for regenerating the API docs are [here](./website/README.md#regenerate-swagger-docs).
+
 ## Submitting Changes
 Fork the repo, make changes, file a Pull Request.
 
diff --git a/activity/journal.go b/activity/journal.go
index 1fc765d..a66b58c 100644
--- a/activity/journal.go
+++ b/activity/journal.go
@@ -7,6 +7,7 @@ import (
 	"sync"
 	"time"
 
+	"github.com/hpcloud/tail"
 	log "github.com/sirupsen/logrus"
 )
 
@@ -99,6 +100,18 @@ func (j Journal) Open() (io.ReadCloser, error) {
 	return os.Open(j.filepath())
 }
 
+var endOfFileLocation = tail.SeekInfo{Offset: -5, Whence: 2}
+
+// Tail streams updates to the journal.
+func (j Journal) Tail() (*tail.Tail, error) {
+	return tail.TailFile(j.filepath(), tail.Config{
+		Follow:    true,
+		MustExist: true,
+		Location:  &endOfFileLocation,
+		Logger:    tail.DiscardingLogger,
+	})
+}
+
 func (j Journal) filepath() string {
 	return filepath.Join(Dir(), j.ID+".log")
 }
diff --git a/api/cache.go b/api/cache.go
index 5b4e823..b528954 100644
--- a/api/cache.go
+++ b/api/cache.go
@@ -35,7 +35,6 @@ var cacheHandler handler = func(w http.ResponseWriter, r *http.Request) *errorRe
 	}
 	activity.Record(r.Context(), "API: Cache DELETE %v %+v", path, deleted)
 
-	w.WriteHeader(http.StatusOK)
 	jsonEncoder := json.NewEncoder(w)
 	if err = jsonEncoder.Encode(deleted); err != nil {
 		return unknownErrorResponse(fmt.Errorf("Could not marshal deleted keys for %v: %v", path, err))
diff --git a/api/client/client.go b/api/client/client.go
index edace42..f54adc3 100644
--- a/api/client/client.go
+++ b/api/client/client.go
@@ -32,8 +32,8 @@ type Client interface {
 	Metadata(path string) (map[string]interface{}, error)
 	Stream(path string) (io.ReadCloser, error)
 	Exec(path string, command string, args []string, opts apitypes.ExecOptions) (<-chan apitypes.ExecPacket, error)
-	History() ([]apitypes.Activity, error)
-	ActivityJournal(index int) (io.ReadCloser, error)
+	History(bool) (chan apitypes.Activity, error)
+	ActivityJournal(index int, follow bool) (io.ReadCloser, error)
 	Clear(path string) ([]string, error)
 }
 
@@ -197,10 +197,15 @@ func (c *domainSocketClient) Exec(path string, command string, args []string, op
 	return events, nil
 }
 
-// History returns the command history for the current wash server session.
-func (c *domainSocketClient) History() ([]apitypes.Activity, error) {
+// History returns a command history channel for the current wash server session.
+// If follow is false, it closes when all current activity has been delivered.
+func (c *domainSocketClient) History(follow bool) (chan apitypes.Activity, error) {
 	// Intentionally skip journaling activity associated with history because that would modify it.
-	resp, err := c.Get(domainSocketBaseURL + "/history")
+	url := domainSocketBaseURL + "/history"
+	if follow {
+		url += "?follow=true"
+	}
+	resp, err := c.Get(url)
 	if err != nil {
 		return nil, err
 	}
@@ -209,24 +214,34 @@ func (c *domainSocketClient) History() ([]apitypes.Activity, error) {
 		return nil, unmarshalErrorResp(resp)
 	}
 
-	defer func() { errz.Log(resp.Body.Close()) }()
-	body, err := ioutil.ReadAll(resp.Body)
-	if err != nil {
-		return nil, err
-	}
-
-	var result apitypes.HistoryResponse
-	if err := json.Unmarshal(body, &result.Activities); err != nil {
-		return nil, fmt.Errorf("Non-JSON body at %v: %v", "/history", string(body))
-	}
+	acts := make(chan apitypes.Activity)
+	go func() {
+		defer func() { errz.Log(resp.Body.Close()) }()
+		dec := json.NewDecoder(resp.Body)
+		for dec.More() {
+			var act apitypes.Activity
+			if err := dec.Decode(&act); err != nil {
+				log.Println(err)
+				close(acts)
+				return
+			}
+			acts <- act
+		}
+		close(acts)
+	}()
 
-	return result.Activities, nil
+	return acts, nil
 }
 
 // ActivityJournal returns a reader for the journal associated with a particular command in history.
-func (c *domainSocketClient) ActivityJournal(index int) (io.ReadCloser, error) {
+// If follow is true, it streams new updates instead of returning the whole journal.
+func (c *domainSocketClient) ActivityJournal(index int, follow bool) (io.ReadCloser, error) {
 	// Intentionally skip journaling activity associated with history because that would modify it.
-	resp, err := c.Get(domainSocketBaseURL + "/history/" + strconv.Itoa(index))
+	url := domainSocketBaseURL + "/history/" + strconv.Itoa(index)
+	if follow {
+		url += "?follow=true"
+	}
+	resp, err := c.Get(url)
 	if err != nil {
 		return nil, err
 	}
diff --git a/api/error.go b/api/error.go
index 67dda24..f7e094e 100644
--- a/api/error.go
+++ b/api/error.go
@@ -139,6 +139,14 @@ func outOfBoundsRequest(size int, reason string) *errorResponse {
 	)}
 }
 
+func invalidBoolParam(name, value string) *errorResponse {
+	return &errorResponse{http.StatusBadRequest, newErrorObj(
+		apitypes.InvalidBool,
+		fmt.Sprintf("Invalid boolean value '%v' given for %v parameter", value, name),
+		apitypes.ErrorFields{"value": value},
+	)}
+}
+
 func invalidPathsResponse(paths []string) *errorResponse {
 	return &errorResponse{http.StatusBadRequest, newErrorObj(
 		apitypes.InvalidPaths,
diff --git a/api/exec.go b/api/exec.go
index 008e709..d9c9890 100644
--- a/api/exec.go
+++ b/api/exec.go
@@ -81,6 +81,7 @@ var execHandler handler = func(w http.ResponseWriter, r *http.Request) *errorRes
 		return erroredActionResponse(path, plugin.ExecAction(), err.Error())
 	}
 
+	// Ensure every write is a flush, and do an initial flush to send the header.
 	w.WriteHeader(http.StatusOK)
 	fw.Flush()
 
diff --git a/api/helpers.go b/api/helpers.go
index fd52276..0b70f2d 100644
--- a/api/helpers.go
+++ b/api/helpers.go
@@ -4,8 +4,10 @@ import (
 	"context"
 	"fmt"
 	"net/http"
+	"net/url"
 	"os"
 	"path/filepath"
+	"strconv"
 	"strings"
 
 	apifs "github.com/puppetlabs/wash/api/fs"
@@ -159,3 +161,24 @@ func getEntryFromRequest(r *http.Request) (plugin.Entry, string, *errorResponse)
 	entry, err := findEntry(ctx, root, segments)
 	return entry, path, err
 }
+
+func getScalarParam(u *url.URL, key string) string {
+	vals := u.Query()[key]
+	if len(vals) > 0 {
+		// Take last value
+		return vals[len(vals)-1]
+	}
+	return ""
+}
+
+func getBoolParam(u *url.URL, key string) (bool, *errorResponse) {
+	val := getScalarParam(u, key)
+	if val != "" {
+		b, err := strconv.ParseBool(val)
+		if err != nil {
+			return false, invalidBoolParam(key, val)
+		}
+		return b, nil
+	}
+	return false, nil
+}
diff --git a/api/history.go b/api/history.go
index 0c5e1b7..0f84bd9 100644
--- a/api/history.go
+++ b/api/history.go
@@ -1,17 +1,28 @@
 package api
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 	"io"
 	"net/http"
 	"strconv"
+	"time"
 
 	"github.com/gorilla/mux"
 	"github.com/puppetlabs/wash/activity"
 	apitypes "github.com/puppetlabs/wash/api/types"
 )
 
+// swagger:parameters retrieveHistory getJournal
+//nolint:deadcode,unused
+type historyParams struct {
+	// stream updates when true
+	//
+	// in: query
+	Follow bool
+}
+
 // swagger:route GET /history history retrieveHistory
 //
 // Get command history
@@ -29,18 +40,59 @@ import (
 //       404: errorResp
 //       500: errorResp
 var historyHandler handler = func(w http.ResponseWriter, r *http.Request) *errorResponse {
+	follow, err := getBoolParam(r.URL, "follow")
+	if err != nil {
+		return err
+	}
+
+	var enc *json.Encoder
+	if follow {
+		// Ensure every write is a flush.
+		f, ok := w.(flushableWriter)
+		if !ok {
+			return unknownErrorResponse(fmt.Errorf("Cannot stream history, response handler does not support flushing"))
+		}
+		enc = json.NewEncoder(&streamableResponseWriter{f})
+	} else {
+		enc = json.NewEncoder(w)
+	}
+
 	history := activity.History()
+	if err := writeHistory(r.Context(), enc, history); err != nil {
+		return err
+	}
 
-	w.WriteHeader(http.StatusOK)
+	if follow {
+		last := len(history)
+		for {
+			// Continue sending updates
+			select {
+			case <-r.Context().Done():
+				return nil
+			case <-time.After(1 * time.Second):
+				// Retry
+			}
 
-	commands := make([]apitypes.Activity, len(history))
-	for i, item := range history {
-		commands[i].Description = item.Description
-		commands[i].Start = item.Start()
+			history = activity.History()
+			if len(history) > last {
+				if err := writeHistory(r.Context(), enc, history[last:]); err != nil {
+					return err
+				}
+				last = len(history)
+			}
+		}
 	}
-	jsonEncoder := json.NewEncoder(w)
-	if err := jsonEncoder.Encode(&commands); err != nil {
-		return unknownErrorResponse(fmt.Errorf("Could not marshal %v: %v", history, err))
+	return nil
+}
+
+func writeHistory(ctx context.Context, enc *json.Encoder, history []activity.Journal) *errorResponse {
+	var act apitypes.Activity
+	for _, item := range history {
+		act.Description = item.Description
+		act.Start = item.Start()
+		if err := enc.Encode(&act); err != nil {
+			return unknownErrorResponse(fmt.Errorf("Could not marshal %v: %v", history, err))
+		}
 	}
 	return nil
 }
@@ -75,20 +127,56 @@ var historyEntryHandler handler = func(w http.ResponseWriter, r *http.Request) *
 		return outOfBoundsRequest(len(history), err.Error())
 	}
 
-	journal := history[idx]
-	rdr, err := journal.Open()
-	if err != nil {
-		return journalUnavailableResponse(journal.String(), err.Error())
+	follow, errResp := getBoolParam(r.URL, "follow")
+	if errResp != nil {
+		return errResp
 	}
-	defer func() {
-		if err := rdr.Close(); err != nil {
-			activity.Record(r.Context(), "Failed to close journal %v: %v", journal, err)
+
+	journal := history[idx]
+
+	if follow {
+		// Ensure every write is a flush.
+		f, ok := w.(flushableWriter)
+		if !ok {
+			return unknownErrorResponse(fmt.Errorf("Cannot stream history, response handler does not support flushing"))
+		}
+
+		rdr, err := journal.Tail()
+		if err != nil {
+			return journalUnavailableResponse(journal.String(), err.Error())
 		}
-	}()
 
-	w.WriteHeader(http.StatusOK)
-	if _, err := io.Copy(w, rdr); err != nil {
-		return unknownErrorResponse(fmt.Errorf("Could not read journal %v: %v", journal, err))
+		// Ensure the reader is closed when context stops.
+		streamCleanup(r.Context(), "Journal "+journal.String(), func() error {
+			rdr.Cleanup()
+			return rdr.Stop()
+		})
+
+		// Do an initial flush to send the header.
+		w.WriteHeader(http.StatusOK)
+		f.Flush()
+
+		for line := range rdr.Lines {
+			if line.Err != nil {
+				return unknownErrorResponse(line.Err)
+			}
+			if _, err := fmt.Fprintln(f, line.Text); err != nil {
+				return unknownErrorResponse(err)
+			}
+			// Flush every line to the client.
+			f.Flush()
+		}
+	} else {
+		rdr, err := journal.Open()
+		if err != nil {
+			return journalUnavailableResponse(journal.String(), err.Error())
+		}
+
+		streamCleanup(r.Context(), "Journal "+journal.String(), rdr.Close)
+
+		if _, err := io.Copy(w, rdr); err != nil {
+			return unknownErrorResponse(fmt.Errorf("Could not read journal %v: %v", journal, err))
+		}
 	}
 	return nil
 }
diff --git a/api/info.go b/api/info.go
index 15db378..93c9624 100644
--- a/api/info.go
+++ b/api/info.go
@@ -28,7 +28,6 @@ var infoHandler handler = func(w http.ResponseWriter, r *http.Request) *errorRes
 		return errResp
 	}
 
-	w.WriteHeader(http.StatusOK)
 	jsonEncoder := json.NewEncoder(w)
 	// TODO: Include the entry's full metadata?
 	apiEntry := toAPIEntry(entry)
diff --git a/api/list.go b/api/list.go
index ff9177a..65e7ab5 100644
--- a/api/list.go
+++ b/api/list.go
@@ -62,7 +62,6 @@ var listHandler handler = func(w http.ResponseWriter, r *http.Request) *errorRes
 	}
 	activity.Record(ctx, "API: List %v %+v", path, result)
 
-	w.WriteHeader(http.StatusOK)
 	jsonEncoder := json.NewEncoder(w)
 	if err = jsonEncoder.Encode(result); err != nil {
 		return unknownErrorResponse(fmt.Errorf("Could not marshal list results for %v: %v", path, err))
diff --git a/api/metadata.go b/api/metadata.go
index d7047c0..ddbfa30 100644
--- a/api/metadata.go
+++ b/api/metadata.go
@@ -44,7 +44,6 @@ var metadataHandler handler = func(w http.ResponseWriter, r *http.Request) *erro
 	}
 	activity.Record(ctx, "API: Metadata %v %+v", path, metadata)
 
-	w.WriteHeader(http.StatusOK)
 	jsonEncoder := json.NewEncoder(w)
 	if err = jsonEncoder.Encode(metadata); err != nil {
 		return unknownErrorResponse(fmt.Errorf("Could not marshal metadata for %v: %v", path, err))
diff --git a/api/read.go b/api/read.go
index 17e07ef..86db562 100644
--- a/api/read.go
+++ b/api/read.go
@@ -42,7 +42,6 @@ var readHandler handler = func(w http.ResponseWriter, r *http.Request) *errorRes
 	}
 	activity.Record(ctx, "API: Reading %v", path)
 
-	w.WriteHeader(http.StatusOK)
 	n, err := io.Copy(w, io.NewSectionReader(content, 0, content.Size()))
 	if n != content.Size() {
 		activity.Record(ctx, "API: Reading %v incomplete: %v/%v", path, n, content.Size())
diff --git a/api/stream.go b/api/stream.go
index ad53136..36c1fce 100644
--- a/api/stream.go
+++ b/api/stream.go
@@ -26,7 +26,6 @@ import (
 //       404: errorResp
 //       500: errorResp
 var streamHandler handler = func(w http.ResponseWriter, r *http.Request) *errorResponse {
-	ctx := r.Context()
 	entry, path, errResp := getEntryFromRequest(r)
 	if errResp != nil {
 		return errResp
@@ -41,6 +40,7 @@ var streamHandler handler = func(w http.ResponseWriter, r *http.Request) *errorR
 		return unknownErrorResponse(fmt.Errorf("Cannot stream %v, response handler does not support flushing", path))
 	}
 
+	ctx := r.Context()
 	rdr, err := entry.(plugin.Streamable).Stream(ctx)
 
 	if err != nil {
@@ -48,18 +48,15 @@ var streamHandler handler = func(w http.ResponseWriter, r *http.Request) *errorR
 	}
 	activity.Record(ctx, "API: Streaming %v", path)
 
+	// Do an initial flush to send the header.
 	w.WriteHeader(http.StatusOK)
-	// Ensure every write is a flush, and do an initial flush to send the header.
-	wf := &streamableResponseWriter{f}
 	f.Flush()
 
 	// Ensure it's closed when the context is cancelled.
-	go func() {
-		<-r.Context().Done()
-		activity.Record(ctx, "API: Stream %v closed by completed context: %v", path, rdr.Close())
-	}()
+	streamCleanup(ctx, "Stream "+path, rdr.Close)
 
-	if _, err := io.Copy(wf, rdr); err != nil {
+	// Ensure every write is a flush with streamableResponseWriter.
+	if _, err := io.Copy(&streamableResponseWriter{f}, rdr); err != nil {
 		// Common for copy to error when the caller closes the connection.
 		activity.Record(ctx, "API: Streaming %v errored: %v", path, err)
 	}
diff --git a/api/streamableResponseWriter.go b/api/streamableResponseWriter.go
index 3c53c04..c3d0fbf 100644
--- a/api/streamableResponseWriter.go
+++ b/api/streamableResponseWriter.go
@@ -1,8 +1,11 @@
 package api
 
 import (
+	"context"
 	"io"
 	"net/http"
+
+	"github.com/puppetlabs/wash/activity"
 )
 
 // Inspired by Docker's WriteFlusher: https://github.com/moby/moby/blob/17.05.x/pkg/ioutils/writeflusher.go
@@ -20,3 +23,12 @@ func (wf *streamableResponseWriter) Write(b []byte) (n int, err error) {
 	wf.Flush()
 	return n, err
 }
+
+type cleanupFunc = func() error
+
+func streamCleanup(ctx context.Context, desc string, cleanup cleanupFunc) {
+	go func() {
+		<-ctx.Done()
+		activity.Record(ctx, "API: %v closed by completed context: %v", desc, cleanup())
+	}()
+}
diff --git a/api/types/error.go b/api/types/error.go
index 2ccf013..439ece7 100644
--- a/api/types/error.go
+++ b/api/types/error.go
@@ -40,4 +40,5 @@ const (
 	InvalidPaths       = "puppetlabs.wash/invalid-paths"
 	OutOfBounds        = "puppetlabs.wash/out-of-bounds"
 	NonWashPath        = "puppetlabs.wash/non-wash-path"
+	InvalidBool        = "puppetlabs.wash/invalid-bool"
 )
diff --git a/cmd/history.go b/cmd/history.go
index 9c89716..cbf1d00 100644
--- a/cmd/history.go
+++ b/cmd/history.go
@@ -11,21 +11,27 @@ import (
 	cmdutil "github.com/puppetlabs/wash/cmd/util"
 	"github.com/puppetlabs/wash/config"
 	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
 )
 
 func historyCommand() *cobra.Command {
 	historyCmd := &cobra.Command{
-		Use:   "history [<id>]",
+		Use:   "history [-f] [<id>]",
 		Short: "Prints the wash command history, or journal of a particular item",
 		Args:  cobra.MaximumNArgs(1),
 	}
 
+	historyCmd.Flags().BoolP("follow", "f", false, "Follow new updates")
+	if err := viper.BindPFlag("follow", historyCmd.Flags().Lookup("follow")); err != nil {
+		cmdutil.ErrPrintf("%v\n", err)
+	}
+
 	historyCmd.RunE = toRunE(historyMain)
 
 	return historyCmd
 }
 
-func printJournalEntry(index string) error {
+func printJournalEntry(index string, follow bool) error {
 	idx, err := strconv.Atoi(index)
 	if err != nil {
 		return err
@@ -33,7 +39,7 @@ func printJournalEntry(index string) error {
 
 	conn := client.ForUNIXSocket(config.Socket)
 	// Translate from 1-indexing for history entries
-	rdr, err := conn.ActivityJournal(idx - 1)
+	rdr, err := conn.ActivityJournal(idx-1, follow)
 	if err != nil {
 		return err
 	}
@@ -45,9 +51,9 @@ func printJournalEntry(index string) error {
 	return err
 }
 
-func printHistory() error {
+func printHistory(follow bool) error {
 	conn := client.ForUNIXSocket(config.Socket)
-	history, err := conn.History()
+	history, err := conn.History(follow)
 	if err != nil {
 		return err
 	}
@@ -55,18 +61,22 @@ func printHistory() error {
 	// Use 1-indexing for history entries
 	indexColumnLength := len(strconv.Itoa(len(history)))
 	formatStr := "%" + strconv.Itoa(indexColumnLength) + "d  %s  %s\n"
-	for i, item := range history {
+	i := 0
+	for item := range history {
 		fmt.Printf(formatStr, i+1, item.Start.Format("2006-01-02 15:04"), item.Description)
+		i++
 	}
 	return nil
 }
 
 func historyMain(cmd *cobra.Command, args []string) exitCode {
+	follow := viper.GetBool("follow")
+
 	var err error
 	if len(args) > 0 {
-		err = printJournalEntry(args[0])
+		err = printJournalEntry(args[0], follow)
 	} else {
-		err = printHistory()
+		err = printHistory(follow)
 	}
 
 	if err != nil {
diff --git a/docs/docs/api/api.json b/docs/docs/api/api.json
index 321d90c..d998979 100644
--- a/docs/docs/api/api.json
+++ b/docs/docs/api/api.json
@@ -49,6 +49,12 @@
         "summary": "Execute a command on a remote system",
         "operationId": "executeCommand",
         "parameters": [
+          {
+            "type": "string",
+            "description": "uniquely identifies an entry",
+            "name": "Path",
+            "in": "query"
+          },
           {
             "x-go-name": "Cmd",
             "description": "Name of the executable to invoke",
@@ -78,12 +84,6 @@
             "schema": {
               "$ref": "#/definitions/ExecOptions"
             }
-          },
-          {
-            "type": "string",
-            "description": "uniquely identifies an entry",
-            "name": "Path",
-            "in": "query"
           }
         ],
         "responses": {
@@ -299,6 +299,14 @@
         ],
         "summary": "Get command history",
         "operationId": "retrieveHistory",
+        "parameters": [
+          {
+            "type": "boolean",
+            "description": "stream updates when true",
+            "name": "Follow",
+            "in": "query"
+          }
+        ],
         "responses": {
           "200": {
             "$ref": "#/responses/HistoryResponse"
@@ -330,6 +338,14 @@
         ],
         "summary": "Get logs for a particular entry in history",
         "operationId": "getJournal",
+        "parameters": [
+          {
+            "type": "boolean",
+            "description": "stream updates when true",
+            "name": "Follow",
+            "in": "query"
+          }
+        ],
         "responses": {
           "200": {
             "$ref": "#/responses/octetResponse"
diff --git a/docs/docs/api/index.html b/docs/docs/api/index.html
index 4e327c1..a2c7f91 100644
--- a/docs/docs/api/index.html
+++ b/docs/docs/api/index.html
@@ -278,12 +278,14 @@ If TypeField is Exitcode, Data will be an int (or float64 if deserialized from J
 </div></div></div></td></tr></tbody></table></div><div><h3 class="zr04gj-0 jKmdQj"> Responses </h3><div><div class="sc-1kbihq-0 kXBEML"><svg class="sc-1g5rdgs-0 fgqOex" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>200<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>404<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>500<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div></div></div><div class="sc-6itmo6-2 sc-6itmo6-3 iJUwRU"><div class="nrhuz6-0 kmORFN"><div class="nrhuz6-2 jmkkdP"><span class="nrhuz6-3 http-verb get klEXcZ" type="get"> <!-- -->get</span> <span class="nrhuz6-1 LxZmB">/fs/read</span><svg class="sc-1g5rdgs-0 gtRsjv" style="margin-right:-25px" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></div><div class="nrhuz6-4 hKgcyu"><div class="nrhuz6-5 kjYYZz"><div class="sc-107yc6q-0 sc-1m0b31p-0 eabDir"></div><div><div class="nrhuz6-6 iuqUuR"><span></span>/fs/read</div></div></div></div></div></div></div></div><div data-section-id="tag/stream" id="tag/stream" class="sc-6itmo6-1 dykNiJ"><div class="sc-6itmo6-4 bRFgPh"><div class="sc-6itmo6-0 diqfaT"><h1 class="shv3r-0 eNRwGZ"><a class="eb0njo-0 kvqXYL" href="#tag/stream"></a>stream</h1></div></div></div><div data-section-id="operation/streamUpdates" id="operation/streamUpdates" class="sc-6itmo6-1 csNTMr"><div class="sc-6itmo6-4 bf7xj3-0 IavJd"><div class="sc-6itmo6-0 diqfaT"><h2 class="shv3r-1 csolXY"><a class="eb0njo-0 kvqXYL" href="#operation/streamUpdates"></a>Stream updates<!-- --> </h2><div class="bf7xj3-1 cbOXAc"><div class="sc-107yc6q-0 sc-1m0b31p-0 eleWFd"><p>Get a stream of new updates to the specified entry.</p>
 </div></div><div><h5 class="shv3r-4 gLzrjW">query<!-- --> Parameters</h5><table class="dz44d2-7 iYGBSY"><tbody><tr class="last undefined"><td class="dz44d2-1 dz44d2-3 ehjTDN" kind="field" title="Path"><span class="dz44d2-5 jwaijW"></span>Path</td><td class="dz44d2-4 iWnyBt"><div><div><span class="sc-1noysbl-1 sc-1noysbl-2 hYZGfb"></span><span class="sc-1noysbl-1 sc-1noysbl-3 Hidzy">string</span></div> <div><div class="sc-107yc6q-0 sc-1m0b31p-0 eabDir"><p>uniquely identifies an entry</p>
 </div></div></div></td></tr></tbody></table></div><div><h3 class="zr04gj-0 jKmdQj"> Responses </h3><div><div class="sc-1kbihq-0 kXBEML"><svg class="sc-1g5rdgs-0 fgqOex" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>200<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>404<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>500<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div></div></div><div class="sc-6itmo6-2 sc-6itmo6-3 iJUwRU"><div class="nrhuz6-0 kmORFN"><div class="nrhuz6-2 jmkkdP"><span class="nrhuz6-3 http-verb get klEXcZ" type="get"> <!-- -->get</span> <span class="nrhuz6-1 LxZmB">/fs/stream</span><svg class="sc-1g5rdgs-0 gtRsjv" style="margin-right:-25px" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></div><div class="nrhuz6-4 hKgcyu"><div class="nrhuz6-5 kjYYZz"><div class="sc-107yc6q-0 sc-1m0b31p-0 eabDir"></div><div><div class="nrhuz6-6 iuqUuR"><span></span>/fs/stream</div></div></div></div></div></div></div></div><div data-section-id="tag/history" id="tag/history" class="sc-6itmo6-1 dykNiJ"><div class="sc-6itmo6-4 bRFgPh"><div class="sc-6itmo6-0 diqfaT"><h1 class="shv3r-0 eNRwGZ"><a class="eb0njo-0 kvqXYL" href="#tag/history"></a>history</h1></div></div></div><div data-section-id="operation/retrieveHistory" id="operation/retrieveHistory" class="sc-6itmo6-1 csNTMr"><div class="sc-6itmo6-4 bf7xj3-0 IavJd"><div class="sc-6itmo6-0 diqfaT"><h2 class="shv3r-1 csolXY"><a class="eb0njo-0 kvqXYL" href="#operation/retrieveHistory"></a>Get command history<!-- --> </h2><div class="bf7xj3-1 cbOXAc"><div class="sc-107yc6q-0 sc-1m0b31p-0 eleWFd"><p>Get a list of commands that have been run via &#39;wash&#39; and when they were run.</p>
-</div></div><div><h3 class="zr04gj-0 jKmdQj"> Responses </h3><div><div class="sc-1kbihq-0 kXBEML"><svg class="sc-1g5rdgs-0 fgqOex" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>200<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"><p>HistoryResponse describes the result returned by the <code>/history</code> endpoint.</p>
+</div></div><div><h5 class="shv3r-4 gLzrjW">query<!-- --> Parameters</h5><table class="dz44d2-7 iYGBSY"><tbody><tr class="last undefined"><td class="dz44d2-1 dz44d2-3 ehjTDN" kind="field" title="Follow"><span class="dz44d2-5 jwaijW"></span>Follow</td><td class="dz44d2-4 iWnyBt"><div><div><span class="sc-1noysbl-1 sc-1noysbl-2 hYZGfb"></span><span class="sc-1noysbl-1 sc-1noysbl-3 Hidzy">boolean</span></div> <div><div class="sc-107yc6q-0 sc-1m0b31p-0 eabDir"><p>stream updates when true</p>
+</div></div></div></td></tr></tbody></table></div><div><h3 class="zr04gj-0 jKmdQj"> Responses </h3><div><div class="sc-1kbihq-0 kXBEML"><svg class="sc-1g5rdgs-0 fgqOex" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>200<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"><p>HistoryResponse describes the result returned by the <code>/history</code> endpoint.</p>
 </span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>400<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>404<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>500<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div></div></div><div class="sc-6itmo6-2 sc-6itmo6-3 iJUwRU"><div class="nrhuz6-0 kmORFN"><div class="nrhuz6-2 jmkkdP"><span class="nrhuz6-3 http-verb get klEXcZ" type="get"> <!-- -->get</span> <span class="nrhuz6-1 LxZmB">/history</span><svg class="sc-1g5rdgs-0 gtRsjv" style="margin-right:-25px" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></div><div class="nrhuz6-4 hKgcyu"><div class="nrhuz6-5 kjYYZz"><div class="sc-107yc6q-0 sc-1m0b31p-0 eabDir"></div><div><div class="nrhuz6-6 iuqUuR"><span></span>/history</div></div></div></div></div></div></div></div><div data-section-id="tag/journal" id="tag/journal" class="sc-6itmo6-1 dykNiJ"><div class="sc-6itmo6-4 bRFgPh"><div class="sc-6itmo6-0 diqfaT"><h1 class="shv3r-0 eNRwGZ"><a class="eb0njo-0 kvqXYL" href="#tag/journal"></a>journal</h1></div></div></div><div data-section-id="operation/getJournal" id="operation/getJournal" class="sc-6itmo6-1 csNTMr"><div class="sc-6itmo6-4 bf7xj3-0 IavJd"><div class="sc-6itmo6-0 diqfaT"><h2 class="shv3r-1 csolXY"><a class="eb0njo-0 kvqXYL" href="#operation/getJournal"></a>Get logs for a particular entry in history<!-- --> </h2><div class="bf7xj3-1 cbOXAc"><div class="sc-107yc6q-0 sc-1m0b31p-0 eleWFd"><p>Get the logs related to a particular command run via &#39;wash&#39;, requested by
 index within its activity history.</p>
-</div></div><div><h3 class="zr04gj-0 jKmdQj"> Responses </h3><div><div class="sc-1kbihq-0 kXBEML"><svg class="sc-1g5rdgs-0 fgqOex" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>200<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>400<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>404<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>500<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div></div></div><div class="sc-6itmo6-2 sc-6itmo6-3 iJUwRU"><div class="nrhuz6-0 kmORFN"><div class="nrhuz6-2 jmkkdP"><span class="nrhuz6-3 http-verb get klEXcZ" type="get"> <!-- -->get</span> <span class="nrhuz6-1 LxZmB">/history/{id}</span><svg class="sc-1g5rdgs-0 gtRsjv" style="margin-right:-25px" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></div><div class="nrhuz6-4 hKgcyu"><div class="nrhuz6-5 kjYYZz"><div class="sc-107yc6q-0 sc-1m0b31p-0 eabDir"></div><div><div class="nrhuz6-6 iuqUuR"><span></span>/history/{id}</div></div></div></div></div></div></div></div></div><div class="sc-13p9gjn-2 egwcPy"></div></div></div>
+</div></div><div><h5 class="shv3r-4 gLzrjW">query<!-- --> Parameters</h5><table class="dz44d2-7 iYGBSY"><tbody><tr class="last undefined"><td class="dz44d2-1 dz44d2-3 ehjTDN" kind="field" title="Follow"><span class="dz44d2-5 jwaijW"></span>Follow</td><td class="dz44d2-4 iWnyBt"><div><div><span class="sc-1noysbl-1 sc-1noysbl-2 hYZGfb"></span><span class="sc-1noysbl-1 sc-1noysbl-3 Hidzy">boolean</span></div> <div><div class="sc-107yc6q-0 sc-1m0b31p-0 eabDir"><p>stream updates when true</p>
+</div></div></div></td></tr></tbody></table></div><div><h3 class="zr04gj-0 jKmdQj"> Responses </h3><div><div class="sc-1kbihq-0 kXBEML"><svg class="sc-1g5rdgs-0 fgqOex" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>200<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>400<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>404<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>500<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div></div></div><div class="sc-6itmo6-2 sc-6itmo6-3 iJUwRU"><div class="nrhuz6-0 kmORFN"><div class="nrhuz6-2 jmkkdP"><span class="nrhuz6-3 http-verb get klEXcZ" type="get"> <!-- -->get</span> <span class="nrhuz6-1 LxZmB">/history/{id}</span><svg class="sc-1g5rdgs-0 gtRsjv" style="margin-right:-25px" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></div><div class="nrhuz6-4 hKgcyu"><div class="nrhuz6-5 kjYYZz"><div class="sc-107yc6q-0 sc-1m0b31p-0 eabDir"></div><div><div class="nrhuz6-6 iuqUuR"><span></span>/history/{id}</div></div></div></div></div></div></div></div></div><div class="sc-13p9gjn-2 egwcPy"></div></div></div>
     <script>
-    const __redoc_state = {"menu":{"activeItemIdx":-1},"spec":{"data":{"openapi":"3.0.0","paths":{"/cache":{"delete":{"description":"Removes the specified entry and its children from the cache.","tags":["cache"],"summary":"Remove items from the cache","operationId":"cacheDelete","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/exec":{"post":{"description":"Executes a command on the remote system described by the supplied path.","tags":["exec"],"summary":"Execute a command on a remote system","operationId":"executeCommand","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Name of the executable to invoke","x-go-name":"Cmd","x-s2o-overloaded":true},"responses":{"200":{"$ref":"#/components/responses/ExecPacket"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"x-s2o-warning":"Operation executeCommand has multiple requestBodies","servers":[]}},"/fs/info":{"get":{"description":"Returns an Entry object describing the given path.","tags":["info"],"summary":"Info about entry at path","operationId":"entryInfo","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/Entry"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/list":{"get":{"description":"Returns a list of Entry objects describing children of the given path.","tags":["list"],"summary":"Lists children of a path","operationId":"listEntries","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/entryList"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/metadata":{"get":{"description":"Get metadata about the specified entry.","tags":["metadata"],"summary":"Get metadata","operationId":"getMetadata","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/entryMetadata"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/read":{"get":{"description":"Read content from the specified entry.","tags":["read"],"summary":"Read content","operationId":"readContent","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/stream":{"get":{"description":"Get a stream of new updates to the specified entry.","tags":["stream"],"summary":"Stream updates","operationId":"streamUpdates","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/history":{"get":{"description":"Get a list of commands that have been run via 'wash' and when they were run.","tags":["history"],"summary":"Get command history","operationId":"retrieveHistory","responses":{"200":{"$ref":"#/components/responses/HistoryResponse"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/history/{id}":{"get":{"description":"Get the logs related to a particular command run via 'wash', requested by\nindex within its activity history.","tags":["journal"],"summary":"Get logs for a particular entry in history","operationId":"getJournal","responses":{"200":{"$ref":"#/components/responses/octetResponse"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}}},"info":{"version":"","title":""},"components":{"responses":{"Entry":{"description":"Entry represents a Wash entry as interpreted by the API.","headers":{"actions":{"schema":{"type":"array","items":{"type":"string"}}},"attributes":{},"cname":{"schema":{"type":"string"}},"name":{"schema":{"type":"string"}},"path":{"schema":{"type":"string"}}},"content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntryAttributes"}}}},"ExecPacket":{"description":"ExecPacket is a single packet of results from an exec.\nIf TypeField is Stdout or Stderr, Data will be a string.\nIf TypeField is Exitcode, Data will be an int (or float64 if deserialized from JSON).","headers":{"data":{},"error":{},"timestamp":{"schema":{"type":"string","format":"date-time"}},"type":{}},"content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorObj"}}}},"HistoryResponse":{"description":"HistoryResponse describes the result returned by the `/history` endpoint.","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Activity"}}}}},"entryList":{"description":"","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Entry"}}}}},"entryMetadata":{"headers":{"EntryMetadata":{}},"description":"","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntryMetadata"}}}},"errorResp":{"headers":{"Body":{}},"description":"","content":{"*/*":{"schema":{"type":"object","properties":{"fields":{"$ref":"#/components/schemas/ErrorFields"},"kind":{"description":"Identifies the kind of error.","type":"string","x-go-name":"Kind"},"msg":{"description":"A description of what failed.","type":"string","x-go-name":"Msg"}}}}}},"octetResponse":{"description":"","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Reader"}}}}},"schemas":{"Activity":{"type":"object","title":"Activity describes an activity from wash's `activity.History`.","properties":{"description":{"type":"string","x-go-name":"Description"},"start":{"type":"string","format":"date-time","x-go-name":"Start"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"Entry":{"type":"object","title":"Entry represents a Wash entry as interpreted by the API.","properties":{"actions":{"type":"array","items":{"type":"string"},"x-go-name":"Actions"},"attributes":{"$ref":"#/components/schemas/EntryAttributes"},"cname":{"type":"string","x-go-name":"CName"},"name":{"type":"string","x-go-name":"Name"},"path":{"type":"string","x-go-name":"Path"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"EntryAttributes":{"description":"Each of the setters supports the builder pattern, which enables you\nto do something like\n\nattr := plugin.EntryAttributes{}\nattr.\nSetCtime(ctime).\nSetMtime(mtime).\nSetMeta(meta)\nentry.SetAttributes(attr)","type":"object","title":"EntryAttributes represents an entry's attributes. We use a struct\ninstead of a map for efficient memory allocation/deallocation,\nwhich is needed to make Group#List fast.","x-go-package":"github.com/puppetlabs/wash/plugin"},"EntryMetadata":{"description":"EntryMetadata represents the entry's metadata\nas returned by Entry#Metadata.","type":"object","additionalProperties":{"type":"object"},"x-go-package":"github.com/puppetlabs/wash/plugin"},"ErrorFields":{"description":"ErrorFields represents the fields of an ErrorObj","type":"object","additionalProperties":{"type":"object"},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ErrorObj":{"description":"ErrorObj represents an API error object","type":"object","properties":{"fields":{"$ref":"#/components/schemas/ErrorFields"},"kind":{"description":"Identifies the kind of error.","type":"string","x-go-name":"Kind"},"msg":{"description":"A description of what failed.","type":"string","x-go-name":"Msg"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ExecOptions":{"description":"These are not identical to plugin.ExecOptions because initially the API only\nsupports receiving a string of input, not a reader.","type":"object","title":"ExecOptions are options that can be passed as part of an Exec call.","properties":{"input":{"description":"Input to pass on stdin when executing the command","type":"string","x-go-name":"Input"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ExecPacketType":{"type":"string","title":"ExecPacketType identifies the packet type.","x-go-package":"github.com/puppetlabs/wash/plugin"},"Reader":{"description":"Read reads up to len(p) bytes into p. It returns the number of bytes\nread (0 <= n <= len(p)) and any error encountered. Even if Read\nreturns n < len(p), it may use all of p as scratch space during the call.\nIf some data is available but not len(p) bytes, Read conventionally\nreturns what is available instead of waiting for more.\n\nWhen Read encounters an error or end-of-file condition after\nsuccessfully reading n > 0 bytes, it returns the number of\nbytes read. It may return the (non-nil) error from the same call\nor return the error (and n == 0) from a subsequent call.\nAn instance of this general case is that a Reader returning\na non-zero number of bytes at the end of the input stream may\nreturn either err == EOF or err == nil. The next Read should\nreturn 0, EOF.\n\nCallers should always process the n > 0 bytes returned before\nconsidering the error err. Doing so correctly handles I/O errors\nthat happen after reading some bytes and also both of the\nallowed EOF behaviors.\n\nImplementations of Read are discouraged from returning a\nzero byte count with a nil error, except when len(p) == 0.\nCallers should treat a return of 0 and nil as indicating that\nnothing happened; in particular it does not indicate EOF.\n\nImplementations must not retain p.","type":"object","title":"Reader is the interface that wraps the basic Read method.","x-go-package":"io"}}}}},"searchIndex":{"store":["tag/cache","operation/cacheDelete","tag/exec","operation/executeCommand","tag/info","operation/entryInfo","tag/list","operation/listEntries","tag/metadata","operation/getMetadata","tag/read","operation/readContent","tag/stream","operation/streamUpdates","tag/history","operation/retrieveHistory","tag/journal","operation/getJournal"],"index":{"version":"2.3.6","fields":["title","description"],"fieldVectors":[["title/0",[0,2.078]],["description/0",[]],["title/1",[0,1.342,1,1.609,2,2.015]],["description/1",[0,1.355,1,1.625,3,1.154,4,0.644,5,1.355]],["title/2",[6,3.119]],["description/2",[]],["title/3",[7,1.367,8,0.836,9,1.367,10,1.367]],["description/3",[7,1.342,8,0.82,9,1.342,10,1.342,11,1.119,12,1.68,13,0.82]],["title/4",[14,2.491]],["description/4",[]],["title/5",[4,0.638,13,0.984,14,1.609]],["description/5",[4,0.583,11,1.226,13,0.898,15,1.47,16,1.47,17,1.47]],["title/6",[18,1.769]],["description/6",[]],["title/7",[5,1.342,13,0.984,18,1.143]],["description/7",[4,0.49,5,1.03,11,1.03,13,0.755,15,1.235,16,1.235,17,1.235,18,0.877]],["title/8",[19,2.078]],["description/8",[]],["title/9",[19,2.078]],["description/9",[3,1.462,4,0.816,19,1.717]],["title/10",[20,2.078]],["description/10",[]],["title/11",[20,1.631,21,1.955]],["description/11",[3,1.29,4,0.72,20,1.515,21,1.816]],["title/12",[22,2.078]],["description/12",[]],["title/13",[22,1.631,23,1.955]],["description/13",[3,1.154,4,0.644,22,1.355,23,1.625,24,2.034]],["title/14",[25,1.769]],["description/14",[]],["title/15",[8,1.195,25,1.389]],["description/15",[8,0.898,18,1.044,26,2.211,27,1.47,28,1.47]],["title/16",[29,3.119]],["description/16",[]],["title/17",[4,0.542,25,0.971,30,1.367,31,1.367]],["description/17",[8,0.572,25,0.664,26,0.935,27,0.935,28,0.935,30,0.935,31,0.935,32,1.171,33,1.171,34,1.171,35,1.171,36,1.171]]],"invertedIndex":[["activ",{"_index":36,"title":{},"description":{"17":{}}}],["cach",{"_index":0,"title":{"0":{},"1":{}},"description":{"1":{}}}],["children",{"_index":5,"title":{"7":{}},"description":{"1":{},"7":{}}}],["command",{"_index":8,"title":{"3":{},"15":{}},"description":{"3":{},"15":{},"17":{}}}],["content",{"_index":21,"title":{"11":{}},"description":{"11":{}}}],["describ",{"_index":11,"title":{},"description":{"3":{},"5":{},"7":{}}}],["entri",{"_index":4,"title":{"5":{},"17":{}},"description":{"1":{},"5":{},"7":{},"9":{},"11":{},"13":{}}}],["exec",{"_index":6,"title":{"2":{}},"description":{}}],["execut",{"_index":7,"title":{"3":{}},"description":{"3":{}}}],["given",{"_index":17,"title":{},"description":{"5":{},"7":{}}}],["histori",{"_index":25,"title":{"14":{},"15":{},"17":{}},"description":{"17":{}}}],["index",{"_index":34,"title":{},"description":{"17":{}}}],["info",{"_index":14,"title":{"4":{},"5":{}},"description":{}}],["item",{"_index":2,"title":{"1":{}},"description":{}}],["journal",{"_index":29,"title":{"16":{}},"description":{}}],["list",{"_index":18,"title":{"6":{},"7":{}},"description":{"7":{},"15":{}}}],["log",{"_index":30,"title":{"17":{}},"description":{"17":{}}}],["metadata",{"_index":19,"title":{"8":{},"9":{}},"description":{"9":{}}}],["new",{"_index":24,"title":{},"description":{"13":{}}}],["object",{"_index":16,"title":{},"description":{"5":{},"7":{}}}],["particular",{"_index":31,"title":{"17":{}},"description":{"17":{}}}],["path",{"_index":13,"title":{"5":{},"7":{}},"description":{"3":{},"5":{},"7":{}}}],["read",{"_index":20,"title":{"10":{},"11":{}},"description":{"11":{}}}],["relat",{"_index":32,"title":{},"description":{"17":{}}}],["remot",{"_index":9,"title":{"3":{}},"description":{"3":{}}}],["remov",{"_index":1,"title":{"1":{}},"description":{"1":{}}}],["request",{"_index":33,"title":{},"description":{"17":{}}}],["return",{"_index":15,"title":{},"description":{"5":{},"7":{}}}],["run",{"_index":26,"title":{},"description":{"15":{},"17":{}}}],["specifi",{"_index":3,"title":{},"description":{"1":{},"9":{},"11":{},"13":{}}}],["stream",{"_index":22,"title":{"12":{},"13":{}},"description":{"13":{}}}],["suppli",{"_index":12,"title":{},"description":{"3":{}}}],["system",{"_index":10,"title":{"3":{}},"description":{"3":{}}}],["updat",{"_index":23,"title":{"13":{}},"description":{"13":{}}}],["via",{"_index":27,"title":{},"description":{"15":{},"17":{}}}],["wash",{"_index":28,"title":{},"description":{"15":{},"17":{}}}],["within",{"_index":35,"title":{},"description":{"17":{}}}]],"pipeline":[]}},"options":{"nativeScrollbars":true}};
+    const __redoc_state = {"menu":{"activeItemIdx":-1},"spec":{"data":{"openapi":"3.0.0","paths":{"/cache":{"delete":{"description":"Removes the specified entry and its children from the cache.","tags":["cache"],"summary":"Remove items from the cache","operationId":"cacheDelete","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/exec":{"post":{"description":"Executes a command on the remote system described by the supplied path.","tags":["exec"],"summary":"Execute a command on a remote system","operationId":"executeCommand","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Name of the executable to invoke","x-go-name":"Cmd","x-s2o-overloaded":true},"responses":{"200":{"$ref":"#/components/responses/ExecPacket"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"x-s2o-warning":"Operation executeCommand has multiple requestBodies","servers":[]}},"/fs/info":{"get":{"description":"Returns an Entry object describing the given path.","tags":["info"],"summary":"Info about entry at path","operationId":"entryInfo","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/Entry"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/list":{"get":{"description":"Returns a list of Entry objects describing children of the given path.","tags":["list"],"summary":"Lists children of a path","operationId":"listEntries","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/entryList"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/metadata":{"get":{"description":"Get metadata about the specified entry.","tags":["metadata"],"summary":"Get metadata","operationId":"getMetadata","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/entryMetadata"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/read":{"get":{"description":"Read content from the specified entry.","tags":["read"],"summary":"Read content","operationId":"readContent","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/stream":{"get":{"description":"Get a stream of new updates to the specified entry.","tags":["stream"],"summary":"Stream updates","operationId":"streamUpdates","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/history":{"get":{"description":"Get a list of commands that have been run via 'wash' and when they were run.","tags":["history"],"summary":"Get command history","operationId":"retrieveHistory","parameters":[{"description":"stream updates when true","name":"Follow","in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"$ref":"#/components/responses/HistoryResponse"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/history/{id}":{"get":{"description":"Get the logs related to a particular command run via 'wash', requested by\nindex within its activity history.","tags":["journal"],"summary":"Get logs for a particular entry in history","operationId":"getJournal","parameters":[{"description":"stream updates when true","name":"Follow","in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}}},"info":{"version":"","title":""},"components":{"responses":{"Entry":{"description":"Entry represents a Wash entry as interpreted by the API.","headers":{"actions":{"schema":{"type":"array","items":{"type":"string"}}},"attributes":{},"cname":{"schema":{"type":"string"}},"name":{"schema":{"type":"string"}},"path":{"schema":{"type":"string"}}},"content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntryAttributes"}}}},"ExecPacket":{"description":"ExecPacket is a single packet of results from an exec.\nIf TypeField is Stdout or Stderr, Data will be a string.\nIf TypeField is Exitcode, Data will be an int (or float64 if deserialized from JSON).","headers":{"data":{},"error":{},"timestamp":{"schema":{"type":"string","format":"date-time"}},"type":{}},"content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorObj"}}}},"HistoryResponse":{"description":"HistoryResponse describes the result returned by the `/history` endpoint.","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Activity"}}}}},"entryList":{"description":"","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Entry"}}}}},"entryMetadata":{"headers":{"EntryMetadata":{}},"description":"","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntryMetadata"}}}},"errorResp":{"headers":{"Body":{}},"description":"","content":{"*/*":{"schema":{"type":"object","properties":{"fields":{"$ref":"#/components/schemas/ErrorFields"},"kind":{"description":"Identifies the kind of error.","type":"string","x-go-name":"Kind"},"msg":{"description":"A description of what failed.","type":"string","x-go-name":"Msg"}}}}}},"octetResponse":{"description":"","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Reader"}}}}},"schemas":{"Activity":{"type":"object","title":"Activity describes an activity from wash's `activity.History`.","properties":{"description":{"type":"string","x-go-name":"Description"},"start":{"type":"string","format":"date-time","x-go-name":"Start"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"Entry":{"type":"object","title":"Entry represents a Wash entry as interpreted by the API.","properties":{"actions":{"type":"array","items":{"type":"string"},"x-go-name":"Actions"},"attributes":{"$ref":"#/components/schemas/EntryAttributes"},"cname":{"type":"string","x-go-name":"CName"},"name":{"type":"string","x-go-name":"Name"},"path":{"type":"string","x-go-name":"Path"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"EntryAttributes":{"description":"Each of the setters supports the builder pattern, which enables you\nto do something like\n\nattr := plugin.EntryAttributes{}\nattr.\nSetCtime(ctime).\nSetMtime(mtime).\nSetMeta(meta)\nentry.SetAttributes(attr)","type":"object","title":"EntryAttributes represents an entry's attributes. We use a struct\ninstead of a map for efficient memory allocation/deallocation,\nwhich is needed to make Group#List fast.","x-go-package":"github.com/puppetlabs/wash/plugin"},"EntryMetadata":{"description":"EntryMetadata represents the entry's metadata\nas returned by Entry#Metadata.","type":"object","additionalProperties":{"type":"object"},"x-go-package":"github.com/puppetlabs/wash/plugin"},"ErrorFields":{"description":"ErrorFields represents the fields of an ErrorObj","type":"object","additionalProperties":{"type":"object"},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ErrorObj":{"description":"ErrorObj represents an API error object","type":"object","properties":{"fields":{"$ref":"#/components/schemas/ErrorFields"},"kind":{"description":"Identifies the kind of error.","type":"string","x-go-name":"Kind"},"msg":{"description":"A description of what failed.","type":"string","x-go-name":"Msg"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ExecOptions":{"description":"These are not identical to plugin.ExecOptions because initially the API only\nsupports receiving a string of input, not a reader.","type":"object","title":"ExecOptions are options that can be passed as part of an Exec call.","properties":{"input":{"description":"Input to pass on stdin when executing the command","type":"string","x-go-name":"Input"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ExecPacketType":{"type":"string","title":"ExecPacketType identifies the packet type.","x-go-package":"github.com/puppetlabs/wash/plugin"},"Reader":{"description":"Read reads up to len(p) bytes into p. It returns the number of bytes\nread (0 <= n <= len(p)) and any error encountered. Even if Read\nreturns n < len(p), it may use all of p as scratch space during the call.\nIf some data is available but not len(p) bytes, Read conventionally\nreturns what is available instead of waiting for more.\n\nWhen Read encounters an error or end-of-file condition after\nsuccessfully reading n > 0 bytes, it returns the number of\nbytes read. It may return the (non-nil) error from the same call\nor return the error (and n == 0) from a subsequent call.\nAn instance of this general case is that a Reader returning\na non-zero number of bytes at the end of the input stream may\nreturn either err == EOF or err == nil. The next Read should\nreturn 0, EOF.\n\nCallers should always process the n > 0 bytes returned before\nconsidering the error err. Doing so correctly handles I/O errors\nthat happen after reading some bytes and also both of the\nallowed EOF behaviors.\n\nImplementations of Read are discouraged from returning a\nzero byte count with a nil error, except when len(p) == 0.\nCallers should treat a return of 0 and nil as indicating that\nnothing happened; in particular it does not indicate EOF.\n\nImplementations must not retain p.","type":"object","title":"Reader is the interface that wraps the basic Read method.","x-go-package":"io"}}}}},"searchIndex":{"store":["tag/cache","operation/cacheDelete","tag/exec","operation/executeCommand","tag/info","operation/entryInfo","tag/list","operation/listEntries","tag/metadata","operation/getMetadata","tag/read","operation/readContent","tag/stream","operation/streamUpdates","tag/history","operation/retrieveHistory","tag/journal","operation/getJournal"],"index":{"version":"2.3.6","fields":["title","description"],"fieldVectors":[["title/0",[0,2.078]],["description/0",[]],["title/1",[0,1.342,1,1.609,2,2.015]],["description/1",[0,1.355,1,1.625,3,1.154,4,0.644,5,1.355]],["title/2",[6,3.119]],["description/2",[]],["title/3",[7,1.367,8,0.836,9,1.367,10,1.367]],["description/3",[7,1.342,8,0.82,9,1.342,10,1.342,11,1.119,12,1.68,13,0.82]],["title/4",[14,2.491]],["description/4",[]],["title/5",[4,0.638,13,0.984,14,1.609]],["description/5",[4,0.583,11,1.226,13,0.898,15,1.47,16,1.47,17,1.47]],["title/6",[18,1.769]],["description/6",[]],["title/7",[5,1.342,13,0.984,18,1.143]],["description/7",[4,0.49,5,1.03,11,1.03,13,0.755,15,1.235,16,1.235,17,1.235,18,0.877]],["title/8",[19,2.078]],["description/8",[]],["title/9",[19,2.078]],["description/9",[3,1.462,4,0.816,19,1.717]],["title/10",[20,2.078]],["description/10",[]],["title/11",[20,1.631,21,1.955]],["description/11",[3,1.29,4,0.72,20,1.515,21,1.816]],["title/12",[22,2.078]],["description/12",[]],["title/13",[22,1.631,23,1.955]],["description/13",[3,1.154,4,0.644,22,1.355,23,1.625,24,2.034]],["title/14",[25,1.769]],["description/14",[]],["title/15",[8,1.195,25,1.389]],["description/15",[8,0.898,18,1.044,26,2.211,27,1.47,28,1.47]],["title/16",[29,3.119]],["description/16",[]],["title/17",[4,0.542,25,0.971,30,1.367,31,1.367]],["description/17",[8,0.572,25,0.664,26,0.935,27,0.935,28,0.935,30,0.935,31,0.935,32,1.171,33,1.171,34,1.171,35,1.171,36,1.171]]],"invertedIndex":[["activ",{"_index":36,"title":{},"description":{"17":{}}}],["cach",{"_index":0,"title":{"0":{},"1":{}},"description":{"1":{}}}],["children",{"_index":5,"title":{"7":{}},"description":{"1":{},"7":{}}}],["command",{"_index":8,"title":{"3":{},"15":{}},"description":{"3":{},"15":{},"17":{}}}],["content",{"_index":21,"title":{"11":{}},"description":{"11":{}}}],["describ",{"_index":11,"title":{},"description":{"3":{},"5":{},"7":{}}}],["entri",{"_index":4,"title":{"5":{},"17":{}},"description":{"1":{},"5":{},"7":{},"9":{},"11":{},"13":{}}}],["exec",{"_index":6,"title":{"2":{}},"description":{}}],["execut",{"_index":7,"title":{"3":{}},"description":{"3":{}}}],["given",{"_index":17,"title":{},"description":{"5":{},"7":{}}}],["histori",{"_index":25,"title":{"14":{},"15":{},"17":{}},"description":{"17":{}}}],["index",{"_index":34,"title":{},"description":{"17":{}}}],["info",{"_index":14,"title":{"4":{},"5":{}},"description":{}}],["item",{"_index":2,"title":{"1":{}},"description":{}}],["journal",{"_index":29,"title":{"16":{}},"description":{}}],["list",{"_index":18,"title":{"6":{},"7":{}},"description":{"7":{},"15":{}}}],["log",{"_index":30,"title":{"17":{}},"description":{"17":{}}}],["metadata",{"_index":19,"title":{"8":{},"9":{}},"description":{"9":{}}}],["new",{"_index":24,"title":{},"description":{"13":{}}}],["object",{"_index":16,"title":{},"description":{"5":{},"7":{}}}],["particular",{"_index":31,"title":{"17":{}},"description":{"17":{}}}],["path",{"_index":13,"title":{"5":{},"7":{}},"description":{"3":{},"5":{},"7":{}}}],["read",{"_index":20,"title":{"10":{},"11":{}},"description":{"11":{}}}],["relat",{"_index":32,"title":{},"description":{"17":{}}}],["remot",{"_index":9,"title":{"3":{}},"description":{"3":{}}}],["remov",{"_index":1,"title":{"1":{}},"description":{"1":{}}}],["request",{"_index":33,"title":{},"description":{"17":{}}}],["return",{"_index":15,"title":{},"description":{"5":{},"7":{}}}],["run",{"_index":26,"title":{},"description":{"15":{},"17":{}}}],["specifi",{"_index":3,"title":{},"description":{"1":{},"9":{},"11":{},"13":{}}}],["stream",{"_index":22,"title":{"12":{},"13":{}},"description":{"13":{}}}],["suppli",{"_index":12,"title":{},"description":{"3":{}}}],["system",{"_index":10,"title":{"3":{}},"description":{"3":{}}}],["updat",{"_index":23,"title":{"13":{}},"description":{"13":{}}}],["via",{"_index":27,"title":{},"description":{"15":{},"17":{}}}],["wash",{"_index":28,"title":{},"description":{"15":{},"17":{}}}],["within",{"_index":35,"title":{},"description":{"17":{}}}]],"pipeline":[]}},"options":{"nativeScrollbars":true}};
 
     var container = document.getElementById('redoc');
     Redoc.hydrate(__redoc_state, container);;
diff --git a/plugin/aws/s3Bucket.go b/plugin/aws/s3Bucket.go
index 82fd44a..fd01c64 100644
--- a/plugin/aws/s3Bucket.go
+++ b/plugin/aws/s3Bucket.go
@@ -225,18 +225,16 @@ func (b *s3Bucket) getRegion(ctx context.Context) (string, error) {
 		}
 
 		// The response will be empty if the bucket is in Amazon's default region (us-east-1)
-		return s3Client.NormalizeBucketLocation(awsSDK.StringValue(resp.LocationConstraint)), nil
+		region := s3Client.NormalizeBucketLocation(awsSDK.StringValue(resp.LocationConstraint))
+		// Update client to be region-specific
+		b.client = s3Client.New(b.session, aws.NewConfig().WithRegion(region))
+
+		return region, nil
 	})
 
 	if err != nil {
 		return "", err
 	}
 
-	region := resp.(string)
-	if awsSDK.StringValue(b.client.Config.Region) != region {
-		// Update client to be region-specific
-		b.client = s3Client.New(b.session, aws.NewConfig().WithRegion(resp.(string)))
-	}
-
-	return region, nil
+	return resp.(string), nil
 }
diff --git a/plugin/externalPluginEntry.go b/plugin/externalPluginEntry.go
index ae37645..18555eb 100644
--- a/plugin/externalPluginEntry.go
+++ b/plugin/externalPluginEntry.go
@@ -233,7 +233,7 @@ func (e *externalPluginEntry) Stream(ctx context.Context) (io.ReadCloser, error)
 	// do this in a separate goroutine?
 	waitForCommandToFinish := func() {
 		activity.Record(ctx, "Waiting for command: %v", cmdStr)
-		err = cmd.Wait()
+		_, err := ExitCodeFromErr(cmd.Wait())
 		if err != nil {
 			activity.Record(ctx, "Failed waiting for command: %v", err)
 		}
@@ -303,8 +303,7 @@ func (e *externalPluginEntry) Exec(ctx context.Context, cmd string, args []strin
 	// TODO: Figure out how to pass-in opts when we have entries
 	// besides Stdin. Could do something like
 	//   <plugin_script> exec <path> <state> <opts> <cmd> <args...>
-	cmdObj := exec.CommandContext(
-		ctx,
+	cmdObj := exec.Command(
 		e.script.Path(),
 		append(
 			[]string{"exec", e.id(), e.state, cmd},
@@ -328,19 +327,15 @@ func (e *externalPluginEntry) Exec(ctx context.Context, cmd string, args []strin
 	if err := cmdObj.Start(); err != nil {
 		return nil, err
 	}
-	// Note that CommandContext handles context-cancellation cleanup for us,
-	// so we don't have to use execCmd.SetStopFunc.
 
 	// Wait for the command to finish
 	go func() {
-		err := cmdObj.Wait()
-		execCmd.CloseStreamsWithError(nil)
-		exitCode := cmdObj.ProcessState.ExitCode()
-		if exitCode < 0 {
-			execCmd.SetExitCodeErr(err)
-		} else {
-			execCmd.SetExitCode(exitCode)
+		ec, err := ExitCodeFromErr(cmdObj.Wait())
+		if err != nil {
+			execCmd.CloseStreamsWithError(err)
+			return
 		}
+		execCmd.SetExitCode(ec)
 	}()
 	
 	return execCmd, nil
diff --git a/plugin/externalPluginScript.go b/plugin/externalPluginScript.go
index b1bc5c5..cc543d9 100644
--- a/plugin/externalPluginScript.go
+++ b/plugin/externalPluginScript.go
@@ -27,12 +27,15 @@ func (s externalPluginScriptImpl) Path() string {
 // InvokeAndWait shells out to the plugin script, passing it the given
 // arguments. It waits for the script to exit, then returns its standard
 // output.
+//
+// TODO: Add a suitable timeout. This could be done w/ CommandContext per
+// https://golang.org/pkg/os/exec/#Cmd.Wait. Could this be specified by
+// plugin authors in the top-level YAML file? Should it be a per-entry
+// thing?
 func (s externalPluginScriptImpl) InvokeAndWait(ctx context.Context, args ...string) ([]byte, error) {
 	activity.Record(ctx, "Running command: %v %v", s.Path(), strings.Join(args, " "))
 
-	// Use CommandContext to ensure that the process is killed when the context
-	// is cancelled
-	cmd := exec.CommandContext(ctx, s.Path(), args...)
+	cmd := exec.Command(s.Path(), args...)
 
 	var stdoutBuf, stderrBuf bytes.Buffer
 	cmd.Stdout = &stdoutBuf
@@ -40,8 +43,8 @@ func (s externalPluginScriptImpl) InvokeAndWait(ctx context.Context, args ...str
 
 	err := cmd.Run()
 
-	exitCode := cmd.ProcessState.ExitCode()
-	if exitCode < 0 {
+	exitCode, err := ExitCodeFromErr(err)
+	if err != nil {
 		return nil, err
 	}
 
diff --git a/plugin/helpers.go b/plugin/helpers.go
index 03cdd59..9cf3021 100644
--- a/plugin/helpers.go
+++ b/plugin/helpers.go
@@ -121,4 +121,20 @@ func CreateCommand(cmd string, args ...string) (*exec.Cmd, io.ReadCloser, io.Rea
 	}
 
 	return cmdObj, stdout, stderr, nil
-}
\ No newline at end of file
+}
+
+// ExitCodeFromErr attempts to get the exit-code from the passed-in
+// error object. If successful, it returns the exit-code. Otherwise,
+// it returns the passed-in error object as the error.
+func ExitCodeFromErr(err error) (int, error) {
+	if err == nil {
+		return 0, nil
+	}
+
+	exitErr, ok := err.(*exec.ExitError)
+	if !ok {
+		return 0, err
+	}
+
+	return exitErr.ExitCode(), nil
+}
diff --git a/website/README.md b/website/README.md
index 989a160..764859d 100644
--- a/website/README.md
+++ b/website/README.md
@@ -11,7 +11,7 @@ $ tree ../docs
 This will output the contents of the site into `../docs`, the directory we've
 configured Github to serve our website from.
 
-### Regenerate swagger docs
+## Regenerate swagger docs
 
 [api docs](./content/api/api.json) are generated using [go-swagger](https://github.com/go-swagger/go-swagger). Install `swagger` with
 ```
@@ -20,8 +20,8 @@ go get -u github.com/go-swagger/go-swagger/cmd/swagger
 
 Then run it and [redoc-cli](https://github.com/Rebilly/ReDoc/blob/master/cli/README.md) from the project root to update the static docs page.
 ```
-swagger generate spec > website/static/api/api.json
-npx redoc-cli bundle website/static/api/api.json -o website/static/api/index.html --options.nativeScrollbars
+swagger generate spec > website/static/docs/api/api.json
+npx redoc-cli bundle website/static/docs/api/api.json -o website/static/docs/api/index.html --options.nativeScrollbars
 ```
 
 > Note that this is somewhat painful to get right with the current state of Go modules. Please ask for help if you have trouble.
diff --git a/website/static/docs/api/api.json b/website/static/docs/api/api.json
index 321d90c..a630df9 100644
--- a/website/static/docs/api/api.json
+++ b/website/static/docs/api/api.json
@@ -299,6 +299,14 @@
         ],
         "summary": "Get command history",
         "operationId": "retrieveHistory",
+        "parameters": [
+          {
+            "type": "boolean",
+            "description": "stream updates when true",
+            "name": "Follow",
+            "in": "query"
+          }
+        ],
         "responses": {
           "200": {
             "$ref": "#/responses/HistoryResponse"
@@ -330,6 +338,14 @@
         ],
         "summary": "Get logs for a particular entry in history",
         "operationId": "getJournal",
+        "parameters": [
+          {
+            "type": "boolean",
+            "description": "stream updates when true",
+            "name": "Follow",
+            "in": "query"
+          }
+        ],
         "responses": {
           "200": {
             "$ref": "#/responses/octetResponse"
diff --git a/website/static/docs/api/index.html b/website/static/docs/api/index.html
index 4e327c1..a2c7f91 100644
--- a/website/static/docs/api/index.html
+++ b/website/static/docs/api/index.html
@@ -278,12 +278,14 @@ If TypeField is Exitcode, Data will be an int (or float64 if deserialized from J
 </div></div></div></td></tr></tbody></table></div><div><h3 class="zr04gj-0 jKmdQj"> Responses </h3><div><div class="sc-1kbihq-0 kXBEML"><svg class="sc-1g5rdgs-0 fgqOex" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>200<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>404<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>500<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div></div></div><div class="sc-6itmo6-2 sc-6itmo6-3 iJUwRU"><div class="nrhuz6-0 kmORFN"><div class="nrhuz6-2 jmkkdP"><span class="nrhuz6-3 http-verb get klEXcZ" type="get"> <!-- -->get</span> <span class="nrhuz6-1 LxZmB">/fs/read</span><svg class="sc-1g5rdgs-0 gtRsjv" style="margin-right:-25px" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></div><div class="nrhuz6-4 hKgcyu"><div class="nrhuz6-5 kjYYZz"><div class="sc-107yc6q-0 sc-1m0b31p-0 eabDir"></div><div><div class="nrhuz6-6 iuqUuR"><span></span>/fs/read</div></div></div></div></div></div></div></div><div data-section-id="tag/stream" id="tag/stream" class="sc-6itmo6-1 dykNiJ"><div class="sc-6itmo6-4 bRFgPh"><div class="sc-6itmo6-0 diqfaT"><h1 class="shv3r-0 eNRwGZ"><a class="eb0njo-0 kvqXYL" href="#tag/stream"></a>stream</h1></div></div></div><div data-section-id="operation/streamUpdates" id="operation/streamUpdates" class="sc-6itmo6-1 csNTMr"><div class="sc-6itmo6-4 bf7xj3-0 IavJd"><div class="sc-6itmo6-0 diqfaT"><h2 class="shv3r-1 csolXY"><a class="eb0njo-0 kvqXYL" href="#operation/streamUpdates"></a>Stream updates<!-- --> </h2><div class="bf7xj3-1 cbOXAc"><div class="sc-107yc6q-0 sc-1m0b31p-0 eleWFd"><p>Get a stream of new updates to the specified entry.</p>
 </div></div><div><h5 class="shv3r-4 gLzrjW">query<!-- --> Parameters</h5><table class="dz44d2-7 iYGBSY"><tbody><tr class="last undefined"><td class="dz44d2-1 dz44d2-3 ehjTDN" kind="field" title="Path"><span class="dz44d2-5 jwaijW"></span>Path</td><td class="dz44d2-4 iWnyBt"><div><div><span class="sc-1noysbl-1 sc-1noysbl-2 hYZGfb"></span><span class="sc-1noysbl-1 sc-1noysbl-3 Hidzy">string</span></div> <div><div class="sc-107yc6q-0 sc-1m0b31p-0 eabDir"><p>uniquely identifies an entry</p>
 </div></div></div></td></tr></tbody></table></div><div><h3 class="zr04gj-0 jKmdQj"> Responses </h3><div><div class="sc-1kbihq-0 kXBEML"><svg class="sc-1g5rdgs-0 fgqOex" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>200<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>404<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>500<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div></div></div><div class="sc-6itmo6-2 sc-6itmo6-3 iJUwRU"><div class="nrhuz6-0 kmORFN"><div class="nrhuz6-2 jmkkdP"><span class="nrhuz6-3 http-verb get klEXcZ" type="get"> <!-- -->get</span> <span class="nrhuz6-1 LxZmB">/fs/stream</span><svg class="sc-1g5rdgs-0 gtRsjv" style="margin-right:-25px" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></div><div class="nrhuz6-4 hKgcyu"><div class="nrhuz6-5 kjYYZz"><div class="sc-107yc6q-0 sc-1m0b31p-0 eabDir"></div><div><div class="nrhuz6-6 iuqUuR"><span></span>/fs/stream</div></div></div></div></div></div></div></div><div data-section-id="tag/history" id="tag/history" class="sc-6itmo6-1 dykNiJ"><div class="sc-6itmo6-4 bRFgPh"><div class="sc-6itmo6-0 diqfaT"><h1 class="shv3r-0 eNRwGZ"><a class="eb0njo-0 kvqXYL" href="#tag/history"></a>history</h1></div></div></div><div data-section-id="operation/retrieveHistory" id="operation/retrieveHistory" class="sc-6itmo6-1 csNTMr"><div class="sc-6itmo6-4 bf7xj3-0 IavJd"><div class="sc-6itmo6-0 diqfaT"><h2 class="shv3r-1 csolXY"><a class="eb0njo-0 kvqXYL" href="#operation/retrieveHistory"></a>Get command history<!-- --> </h2><div class="bf7xj3-1 cbOXAc"><div class="sc-107yc6q-0 sc-1m0b31p-0 eleWFd"><p>Get a list of commands that have been run via &#39;wash&#39; and when they were run.</p>
-</div></div><div><h3 class="zr04gj-0 jKmdQj"> Responses </h3><div><div class="sc-1kbihq-0 kXBEML"><svg class="sc-1g5rdgs-0 fgqOex" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>200<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"><p>HistoryResponse describes the result returned by the <code>/history</code> endpoint.</p>
+</div></div><div><h5 class="shv3r-4 gLzrjW">query<!-- --> Parameters</h5><table class="dz44d2-7 iYGBSY"><tbody><tr class="last undefined"><td class="dz44d2-1 dz44d2-3 ehjTDN" kind="field" title="Follow"><span class="dz44d2-5 jwaijW"></span>Follow</td><td class="dz44d2-4 iWnyBt"><div><div><span class="sc-1noysbl-1 sc-1noysbl-2 hYZGfb"></span><span class="sc-1noysbl-1 sc-1noysbl-3 Hidzy">boolean</span></div> <div><div class="sc-107yc6q-0 sc-1m0b31p-0 eabDir"><p>stream updates when true</p>
+</div></div></div></td></tr></tbody></table></div><div><h3 class="zr04gj-0 jKmdQj"> Responses </h3><div><div class="sc-1kbihq-0 kXBEML"><svg class="sc-1g5rdgs-0 fgqOex" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>200<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"><p>HistoryResponse describes the result returned by the <code>/history</code> endpoint.</p>
 </span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>400<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>404<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>500<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div></div></div><div class="sc-6itmo6-2 sc-6itmo6-3 iJUwRU"><div class="nrhuz6-0 kmORFN"><div class="nrhuz6-2 jmkkdP"><span class="nrhuz6-3 http-verb get klEXcZ" type="get"> <!-- -->get</span> <span class="nrhuz6-1 LxZmB">/history</span><svg class="sc-1g5rdgs-0 gtRsjv" style="margin-right:-25px" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></div><div class="nrhuz6-4 hKgcyu"><div class="nrhuz6-5 kjYYZz"><div class="sc-107yc6q-0 sc-1m0b31p-0 eabDir"></div><div><div class="nrhuz6-6 iuqUuR"><span></span>/history</div></div></div></div></div></div></div></div><div data-section-id="tag/journal" id="tag/journal" class="sc-6itmo6-1 dykNiJ"><div class="sc-6itmo6-4 bRFgPh"><div class="sc-6itmo6-0 diqfaT"><h1 class="shv3r-0 eNRwGZ"><a class="eb0njo-0 kvqXYL" href="#tag/journal"></a>journal</h1></div></div></div><div data-section-id="operation/getJournal" id="operation/getJournal" class="sc-6itmo6-1 csNTMr"><div class="sc-6itmo6-4 bf7xj3-0 IavJd"><div class="sc-6itmo6-0 diqfaT"><h2 class="shv3r-1 csolXY"><a class="eb0njo-0 kvqXYL" href="#operation/getJournal"></a>Get logs for a particular entry in history<!-- --> </h2><div class="bf7xj3-1 cbOXAc"><div class="sc-107yc6q-0 sc-1m0b31p-0 eleWFd"><p>Get the logs related to a particular command run via &#39;wash&#39;, requested by
 index within its activity history.</p>
-</div></div><div><h3 class="zr04gj-0 jKmdQj"> Responses </h3><div><div class="sc-1kbihq-0 kXBEML"><svg class="sc-1g5rdgs-0 fgqOex" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>200<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>400<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>404<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>500<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div></div></div><div class="sc-6itmo6-2 sc-6itmo6-3 iJUwRU"><div class="nrhuz6-0 kmORFN"><div class="nrhuz6-2 jmkkdP"><span class="nrhuz6-3 http-verb get klEXcZ" type="get"> <!-- -->get</span> <span class="nrhuz6-1 LxZmB">/history/{id}</span><svg class="sc-1g5rdgs-0 gtRsjv" style="margin-right:-25px" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></div><div class="nrhuz6-4 hKgcyu"><div class="nrhuz6-5 kjYYZz"><div class="sc-107yc6q-0 sc-1m0b31p-0 eabDir"></div><div><div class="nrhuz6-6 iuqUuR"><span></span>/history/{id}</div></div></div></div></div></div></div></div></div><div class="sc-13p9gjn-2 egwcPy"></div></div></div>
+</div></div><div><h5 class="shv3r-4 gLzrjW">query<!-- --> Parameters</h5><table class="dz44d2-7 iYGBSY"><tbody><tr class="last undefined"><td class="dz44d2-1 dz44d2-3 ehjTDN" kind="field" title="Follow"><span class="dz44d2-5 jwaijW"></span>Follow</td><td class="dz44d2-4 iWnyBt"><div><div><span class="sc-1noysbl-1 sc-1noysbl-2 hYZGfb"></span><span class="sc-1noysbl-1 sc-1noysbl-3 Hidzy">boolean</span></div> <div><div class="sc-107yc6q-0 sc-1m0b31p-0 eabDir"><p>stream updates when true</p>
+</div></div></div></td></tr></tbody></table></div><div><h3 class="zr04gj-0 jKmdQj"> Responses </h3><div><div class="sc-1kbihq-0 kXBEML"><svg class="sc-1g5rdgs-0 fgqOex" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>200<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>400<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>404<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>500<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div></div></div><div class="sc-6itmo6-2 sc-6itmo6-3 iJUwRU"><div class="nrhuz6-0 kmORFN"><div class="nrhuz6-2 jmkkdP"><span class="nrhuz6-3 http-verb get klEXcZ" type="get"> <!-- -->get</span> <span class="nrhuz6-1 LxZmB">/history/{id}</span><svg class="sc-1g5rdgs-0 gtRsjv" style="margin-right:-25px" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></div><div class="nrhuz6-4 hKgcyu"><div class="nrhuz6-5 kjYYZz"><div class="sc-107yc6q-0 sc-1m0b31p-0 eabDir"></div><div><div class="nrhuz6-6 iuqUuR"><span></span>/history/{id}</div></div></div></div></div></div></div></div></div><div class="sc-13p9gjn-2 egwcPy"></div></div></div>
     <script>
-    const __redoc_state = {"menu":{"activeItemIdx":-1},"spec":{"data":{"openapi":"3.0.0","paths":{"/cache":{"delete":{"description":"Removes the specified entry and its children from the cache.","tags":["cache"],"summary":"Remove items from the cache","operationId":"cacheDelete","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/exec":{"post":{"description":"Executes a command on the remote system described by the supplied path.","tags":["exec"],"summary":"Execute a command on a remote system","operationId":"executeCommand","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Name of the executable to invoke","x-go-name":"Cmd","x-s2o-overloaded":true},"responses":{"200":{"$ref":"#/components/responses/ExecPacket"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"x-s2o-warning":"Operation executeCommand has multiple requestBodies","servers":[]}},"/fs/info":{"get":{"description":"Returns an Entry object describing the given path.","tags":["info"],"summary":"Info about entry at path","operationId":"entryInfo","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/Entry"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/list":{"get":{"description":"Returns a list of Entry objects describing children of the given path.","tags":["list"],"summary":"Lists children of a path","operationId":"listEntries","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/entryList"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/metadata":{"get":{"description":"Get metadata about the specified entry.","tags":["metadata"],"summary":"Get metadata","operationId":"getMetadata","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/entryMetadata"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/read":{"get":{"description":"Read content from the specified entry.","tags":["read"],"summary":"Read content","operationId":"readContent","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/stream":{"get":{"description":"Get a stream of new updates to the specified entry.","tags":["stream"],"summary":"Stream updates","operationId":"streamUpdates","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/history":{"get":{"description":"Get a list of commands that have been run via 'wash' and when they were run.","tags":["history"],"summary":"Get command history","operationId":"retrieveHistory","responses":{"200":{"$ref":"#/components/responses/HistoryResponse"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/history/{id}":{"get":{"description":"Get the logs related to a particular command run via 'wash', requested by\nindex within its activity history.","tags":["journal"],"summary":"Get logs for a particular entry in history","operationId":"getJournal","responses":{"200":{"$ref":"#/components/responses/octetResponse"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}}},"info":{"version":"","title":""},"components":{"responses":{"Entry":{"description":"Entry represents a Wash entry as interpreted by the API.","headers":{"actions":{"schema":{"type":"array","items":{"type":"string"}}},"attributes":{},"cname":{"schema":{"type":"string"}},"name":{"schema":{"type":"string"}},"path":{"schema":{"type":"string"}}},"content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntryAttributes"}}}},"ExecPacket":{"description":"ExecPacket is a single packet of results from an exec.\nIf TypeField is Stdout or Stderr, Data will be a string.\nIf TypeField is Exitcode, Data will be an int (or float64 if deserialized from JSON).","headers":{"data":{},"error":{},"timestamp":{"schema":{"type":"string","format":"date-time"}},"type":{}},"content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorObj"}}}},"HistoryResponse":{"description":"HistoryResponse describes the result returned by the `/history` endpoint.","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Activity"}}}}},"entryList":{"description":"","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Entry"}}}}},"entryMetadata":{"headers":{"EntryMetadata":{}},"description":"","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntryMetadata"}}}},"errorResp":{"headers":{"Body":{}},"description":"","content":{"*/*":{"schema":{"type":"object","properties":{"fields":{"$ref":"#/components/schemas/ErrorFields"},"kind":{"description":"Identifies the kind of error.","type":"string","x-go-name":"Kind"},"msg":{"description":"A description of what failed.","type":"string","x-go-name":"Msg"}}}}}},"octetResponse":{"description":"","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Reader"}}}}},"schemas":{"Activity":{"type":"object","title":"Activity describes an activity from wash's `activity.History`.","properties":{"description":{"type":"string","x-go-name":"Description"},"start":{"type":"string","format":"date-time","x-go-name":"Start"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"Entry":{"type":"object","title":"Entry represents a Wash entry as interpreted by the API.","properties":{"actions":{"type":"array","items":{"type":"string"},"x-go-name":"Actions"},"attributes":{"$ref":"#/components/schemas/EntryAttributes"},"cname":{"type":"string","x-go-name":"CName"},"name":{"type":"string","x-go-name":"Name"},"path":{"type":"string","x-go-name":"Path"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"EntryAttributes":{"description":"Each of the setters supports the builder pattern, which enables you\nto do something like\n\nattr := plugin.EntryAttributes{}\nattr.\nSetCtime(ctime).\nSetMtime(mtime).\nSetMeta(meta)\nentry.SetAttributes(attr)","type":"object","title":"EntryAttributes represents an entry's attributes. We use a struct\ninstead of a map for efficient memory allocation/deallocation,\nwhich is needed to make Group#List fast.","x-go-package":"github.com/puppetlabs/wash/plugin"},"EntryMetadata":{"description":"EntryMetadata represents the entry's metadata\nas returned by Entry#Metadata.","type":"object","additionalProperties":{"type":"object"},"x-go-package":"github.com/puppetlabs/wash/plugin"},"ErrorFields":{"description":"ErrorFields represents the fields of an ErrorObj","type":"object","additionalProperties":{"type":"object"},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ErrorObj":{"description":"ErrorObj represents an API error object","type":"object","properties":{"fields":{"$ref":"#/components/schemas/ErrorFields"},"kind":{"description":"Identifies the kind of error.","type":"string","x-go-name":"Kind"},"msg":{"description":"A description of what failed.","type":"string","x-go-name":"Msg"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ExecOptions":{"description":"These are not identical to plugin.ExecOptions because initially the API only\nsupports receiving a string of input, not a reader.","type":"object","title":"ExecOptions are options that can be passed as part of an Exec call.","properties":{"input":{"description":"Input to pass on stdin when executing the command","type":"string","x-go-name":"Input"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ExecPacketType":{"type":"string","title":"ExecPacketType identifies the packet type.","x-go-package":"github.com/puppetlabs/wash/plugin"},"Reader":{"description":"Read reads up to len(p) bytes into p. It returns the number of bytes\nread (0 <= n <= len(p)) and any error encountered. Even if Read\nreturns n < len(p), it may use all of p as scratch space during the call.\nIf some data is available but not len(p) bytes, Read conventionally\nreturns what is available instead of waiting for more.\n\nWhen Read encounters an error or end-of-file condition after\nsuccessfully reading n > 0 bytes, it returns the number of\nbytes read. It may return the (non-nil) error from the same call\nor return the error (and n == 0) from a subsequent call.\nAn instance of this general case is that a Reader returning\na non-zero number of bytes at the end of the input stream may\nreturn either err == EOF or err == nil. The next Read should\nreturn 0, EOF.\n\nCallers should always process the n > 0 bytes returned before\nconsidering the error err. Doing so correctly handles I/O errors\nthat happen after reading some bytes and also both of the\nallowed EOF behaviors.\n\nImplementations of Read are discouraged from returning a\nzero byte count with a nil error, except when len(p) == 0.\nCallers should treat a return of 0 and nil as indicating that\nnothing happened; in particular it does not indicate EOF.\n\nImplementations must not retain p.","type":"object","title":"Reader is the interface that wraps the basic Read method.","x-go-package":"io"}}}}},"searchIndex":{"store":["tag/cache","operation/cacheDelete","tag/exec","operation/executeCommand","tag/info","operation/entryInfo","tag/list","operation/listEntries","tag/metadata","operation/getMetadata","tag/read","operation/readContent","tag/stream","operation/streamUpdates","tag/history","operation/retrieveHistory","tag/journal","operation/getJournal"],"index":{"version":"2.3.6","fields":["title","description"],"fieldVectors":[["title/0",[0,2.078]],["description/0",[]],["title/1",[0,1.342,1,1.609,2,2.015]],["description/1",[0,1.355,1,1.625,3,1.154,4,0.644,5,1.355]],["title/2",[6,3.119]],["description/2",[]],["title/3",[7,1.367,8,0.836,9,1.367,10,1.367]],["description/3",[7,1.342,8,0.82,9,1.342,10,1.342,11,1.119,12,1.68,13,0.82]],["title/4",[14,2.491]],["description/4",[]],["title/5",[4,0.638,13,0.984,14,1.609]],["description/5",[4,0.583,11,1.226,13,0.898,15,1.47,16,1.47,17,1.47]],["title/6",[18,1.769]],["description/6",[]],["title/7",[5,1.342,13,0.984,18,1.143]],["description/7",[4,0.49,5,1.03,11,1.03,13,0.755,15,1.235,16,1.235,17,1.235,18,0.877]],["title/8",[19,2.078]],["description/8",[]],["title/9",[19,2.078]],["description/9",[3,1.462,4,0.816,19,1.717]],["title/10",[20,2.078]],["description/10",[]],["title/11",[20,1.631,21,1.955]],["description/11",[3,1.29,4,0.72,20,1.515,21,1.816]],["title/12",[22,2.078]],["description/12",[]],["title/13",[22,1.631,23,1.955]],["description/13",[3,1.154,4,0.644,22,1.355,23,1.625,24,2.034]],["title/14",[25,1.769]],["description/14",[]],["title/15",[8,1.195,25,1.389]],["description/15",[8,0.898,18,1.044,26,2.211,27,1.47,28,1.47]],["title/16",[29,3.119]],["description/16",[]],["title/17",[4,0.542,25,0.971,30,1.367,31,1.367]],["description/17",[8,0.572,25,0.664,26,0.935,27,0.935,28,0.935,30,0.935,31,0.935,32,1.171,33,1.171,34,1.171,35,1.171,36,1.171]]],"invertedIndex":[["activ",{"_index":36,"title":{},"description":{"17":{}}}],["cach",{"_index":0,"title":{"0":{},"1":{}},"description":{"1":{}}}],["children",{"_index":5,"title":{"7":{}},"description":{"1":{},"7":{}}}],["command",{"_index":8,"title":{"3":{},"15":{}},"description":{"3":{},"15":{},"17":{}}}],["content",{"_index":21,"title":{"11":{}},"description":{"11":{}}}],["describ",{"_index":11,"title":{},"description":{"3":{},"5":{},"7":{}}}],["entri",{"_index":4,"title":{"5":{},"17":{}},"description":{"1":{},"5":{},"7":{},"9":{},"11":{},"13":{}}}],["exec",{"_index":6,"title":{"2":{}},"description":{}}],["execut",{"_index":7,"title":{"3":{}},"description":{"3":{}}}],["given",{"_index":17,"title":{},"description":{"5":{},"7":{}}}],["histori",{"_index":25,"title":{"14":{},"15":{},"17":{}},"description":{"17":{}}}],["index",{"_index":34,"title":{},"description":{"17":{}}}],["info",{"_index":14,"title":{"4":{},"5":{}},"description":{}}],["item",{"_index":2,"title":{"1":{}},"description":{}}],["journal",{"_index":29,"title":{"16":{}},"description":{}}],["list",{"_index":18,"title":{"6":{},"7":{}},"description":{"7":{},"15":{}}}],["log",{"_index":30,"title":{"17":{}},"description":{"17":{}}}],["metadata",{"_index":19,"title":{"8":{},"9":{}},"description":{"9":{}}}],["new",{"_index":24,"title":{},"description":{"13":{}}}],["object",{"_index":16,"title":{},"description":{"5":{},"7":{}}}],["particular",{"_index":31,"title":{"17":{}},"description":{"17":{}}}],["path",{"_index":13,"title":{"5":{},"7":{}},"description":{"3":{},"5":{},"7":{}}}],["read",{"_index":20,"title":{"10":{},"11":{}},"description":{"11":{}}}],["relat",{"_index":32,"title":{},"description":{"17":{}}}],["remot",{"_index":9,"title":{"3":{}},"description":{"3":{}}}],["remov",{"_index":1,"title":{"1":{}},"description":{"1":{}}}],["request",{"_index":33,"title":{},"description":{"17":{}}}],["return",{"_index":15,"title":{},"description":{"5":{},"7":{}}}],["run",{"_index":26,"title":{},"description":{"15":{},"17":{}}}],["specifi",{"_index":3,"title":{},"description":{"1":{},"9":{},"11":{},"13":{}}}],["stream",{"_index":22,"title":{"12":{},"13":{}},"description":{"13":{}}}],["suppli",{"_index":12,"title":{},"description":{"3":{}}}],["system",{"_index":10,"title":{"3":{}},"description":{"3":{}}}],["updat",{"_index":23,"title":{"13":{}},"description":{"13":{}}}],["via",{"_index":27,"title":{},"description":{"15":{},"17":{}}}],["wash",{"_index":28,"title":{},"description":{"15":{},"17":{}}}],["within",{"_index":35,"title":{},"description":{"17":{}}}]],"pipeline":[]}},"options":{"nativeScrollbars":true}};
+    const __redoc_state = {"menu":{"activeItemIdx":-1},"spec":{"data":{"openapi":"3.0.0","paths":{"/cache":{"delete":{"description":"Removes the specified entry and its children from the cache.","tags":["cache"],"summary":"Remove items from the cache","operationId":"cacheDelete","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/exec":{"post":{"description":"Executes a command on the remote system described by the supplied path.","tags":["exec"],"summary":"Execute a command on a remote system","operationId":"executeCommand","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Name of the executable to invoke","x-go-name":"Cmd","x-s2o-overloaded":true},"responses":{"200":{"$ref":"#/components/responses/ExecPacket"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"x-s2o-warning":"Operation executeCommand has multiple requestBodies","servers":[]}},"/fs/info":{"get":{"description":"Returns an Entry object describing the given path.","tags":["info"],"summary":"Info about entry at path","operationId":"entryInfo","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/Entry"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/list":{"get":{"description":"Returns a list of Entry objects describing children of the given path.","tags":["list"],"summary":"Lists children of a path","operationId":"listEntries","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/entryList"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/metadata":{"get":{"description":"Get metadata about the specified entry.","tags":["metadata"],"summary":"Get metadata","operationId":"getMetadata","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/entryMetadata"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/read":{"get":{"description":"Read content from the specified entry.","tags":["read"],"summary":"Read content","operationId":"readContent","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/stream":{"get":{"description":"Get a stream of new updates to the specified entry.","tags":["stream"],"summary":"Stream updates","operationId":"streamUpdates","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/history":{"get":{"description":"Get a list of commands that have been run via 'wash' and when they were run.","tags":["history"],"summary":"Get command history","operationId":"retrieveHistory","parameters":[{"description":"stream updates when true","name":"Follow","in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"$ref":"#/components/responses/HistoryResponse"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/history/{id}":{"get":{"description":"Get the logs related to a particular command run via 'wash', requested by\nindex within its activity history.","tags":["journal"],"summary":"Get logs for a particular entry in history","operationId":"getJournal","parameters":[{"description":"stream updates when true","name":"Follow","in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}}},"info":{"version":"","title":""},"components":{"responses":{"Entry":{"description":"Entry represents a Wash entry as interpreted by the API.","headers":{"actions":{"schema":{"type":"array","items":{"type":"string"}}},"attributes":{},"cname":{"schema":{"type":"string"}},"name":{"schema":{"type":"string"}},"path":{"schema":{"type":"string"}}},"content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntryAttributes"}}}},"ExecPacket":{"description":"ExecPacket is a single packet of results from an exec.\nIf TypeField is Stdout or Stderr, Data will be a string.\nIf TypeField is Exitcode, Data will be an int (or float64 if deserialized from JSON).","headers":{"data":{},"error":{},"timestamp":{"schema":{"type":"string","format":"date-time"}},"type":{}},"content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorObj"}}}},"HistoryResponse":{"description":"HistoryResponse describes the result returned by the `/history` endpoint.","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Activity"}}}}},"entryList":{"description":"","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Entry"}}}}},"entryMetadata":{"headers":{"EntryMetadata":{}},"description":"","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntryMetadata"}}}},"errorResp":{"headers":{"Body":{}},"description":"","content":{"*/*":{"schema":{"type":"object","properties":{"fields":{"$ref":"#/components/schemas/ErrorFields"},"kind":{"description":"Identifies the kind of error.","type":"string","x-go-name":"Kind"},"msg":{"description":"A description of what failed.","type":"string","x-go-name":"Msg"}}}}}},"octetResponse":{"description":"","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Reader"}}}}},"schemas":{"Activity":{"type":"object","title":"Activity describes an activity from wash's `activity.History`.","properties":{"description":{"type":"string","x-go-name":"Description"},"start":{"type":"string","format":"date-time","x-go-name":"Start"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"Entry":{"type":"object","title":"Entry represents a Wash entry as interpreted by the API.","properties":{"actions":{"type":"array","items":{"type":"string"},"x-go-name":"Actions"},"attributes":{"$ref":"#/components/schemas/EntryAttributes"},"cname":{"type":"string","x-go-name":"CName"},"name":{"type":"string","x-go-name":"Name"},"path":{"type":"string","x-go-name":"Path"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"EntryAttributes":{"description":"Each of the setters supports the builder pattern, which enables you\nto do something like\n\nattr := plugin.EntryAttributes{}\nattr.\nSetCtime(ctime).\nSetMtime(mtime).\nSetMeta(meta)\nentry.SetAttributes(attr)","type":"object","title":"EntryAttributes represents an entry's attributes. We use a struct\ninstead of a map for efficient memory allocation/deallocation,\nwhich is needed to make Group#List fast.","x-go-package":"github.com/puppetlabs/wash/plugin"},"EntryMetadata":{"description":"EntryMetadata represents the entry's metadata\nas returned by Entry#Metadata.","type":"object","additionalProperties":{"type":"object"},"x-go-package":"github.com/puppetlabs/wash/plugin"},"ErrorFields":{"description":"ErrorFields represents the fields of an ErrorObj","type":"object","additionalProperties":{"type":"object"},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ErrorObj":{"description":"ErrorObj represents an API error object","type":"object","properties":{"fields":{"$ref":"#/components/schemas/ErrorFields"},"kind":{"description":"Identifies the kind of error.","type":"string","x-go-name":"Kind"},"msg":{"description":"A description of what failed.","type":"string","x-go-name":"Msg"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ExecOptions":{"description":"These are not identical to plugin.ExecOptions because initially the API only\nsupports receiving a string of input, not a reader.","type":"object","title":"ExecOptions are options that can be passed as part of an Exec call.","properties":{"input":{"description":"Input to pass on stdin when executing the command","type":"string","x-go-name":"Input"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ExecPacketType":{"type":"string","title":"ExecPacketType identifies the packet type.","x-go-package":"github.com/puppetlabs/wash/plugin"},"Reader":{"description":"Read reads up to len(p) bytes into p. It returns the number of bytes\nread (0 <= n <= len(p)) and any error encountered. Even if Read\nreturns n < len(p), it may use all of p as scratch space during the call.\nIf some data is available but not len(p) bytes, Read conventionally\nreturns what is available instead of waiting for more.\n\nWhen Read encounters an error or end-of-file condition after\nsuccessfully reading n > 0 bytes, it returns the number of\nbytes read. It may return the (non-nil) error from the same call\nor return the error (and n == 0) from a subsequent call.\nAn instance of this general case is that a Reader returning\na non-zero number of bytes at the end of the input stream may\nreturn either err == EOF or err == nil. The next Read should\nreturn 0, EOF.\n\nCallers should always process the n > 0 bytes returned before\nconsidering the error err. Doing so correctly handles I/O errors\nthat happen after reading some bytes and also both of the\nallowed EOF behaviors.\n\nImplementations of Read are discouraged from returning a\nzero byte count with a nil error, except when len(p) == 0.\nCallers should treat a return of 0 and nil as indicating that\nnothing happened; in particular it does not indicate EOF.\n\nImplementations must not retain p.","type":"object","title":"Reader is the interface that wraps the basic Read method.","x-go-package":"io"}}}}},"searchIndex":{"store":["tag/cache","operation/cacheDelete","tag/exec","operation/executeCommand","tag/info","operation/entryInfo","tag/list","operation/listEntries","tag/metadata","operation/getMetadata","tag/read","operation/readContent","tag/stream","operation/streamUpdates","tag/history","operation/retrieveHistory","tag/journal","operation/getJournal"],"index":{"version":"2.3.6","fields":["title","description"],"fieldVectors":[["title/0",[0,2.078]],["description/0",[]],["title/1",[0,1.342,1,1.609,2,2.015]],["description/1",[0,1.355,1,1.625,3,1.154,4,0.644,5,1.355]],["title/2",[6,3.119]],["description/2",[]],["title/3",[7,1.367,8,0.836,9,1.367,10,1.367]],["description/3",[7,1.342,8,0.82,9,1.342,10,1.342,11,1.119,12,1.68,13,0.82]],["title/4",[14,2.491]],["description/4",[]],["title/5",[4,0.638,13,0.984,14,1.609]],["description/5",[4,0.583,11,1.226,13,0.898,15,1.47,16,1.47,17,1.47]],["title/6",[18,1.769]],["description/6",[]],["title/7",[5,1.342,13,0.984,18,1.143]],["description/7",[4,0.49,5,1.03,11,1.03,13,0.755,15,1.235,16,1.235,17,1.235,18,0.877]],["title/8",[19,2.078]],["description/8",[]],["title/9",[19,2.078]],["description/9",[3,1.462,4,0.816,19,1.717]],["title/10",[20,2.078]],["description/10",[]],["title/11",[20,1.631,21,1.955]],["description/11",[3,1.29,4,0.72,20,1.515,21,1.816]],["title/12",[22,2.078]],["description/12",[]],["title/13",[22,1.631,23,1.955]],["description/13",[3,1.154,4,0.644,22,1.355,23,1.625,24,2.034]],["title/14",[25,1.769]],["description/14",[]],["title/15",[8,1.195,25,1.389]],["description/15",[8,0.898,18,1.044,26,2.211,27,1.47,28,1.47]],["title/16",[29,3.119]],["description/16",[]],["title/17",[4,0.542,25,0.971,30,1.367,31,1.367]],["description/17",[8,0.572,25,0.664,26,0.935,27,0.935,28,0.935,30,0.935,31,0.935,32,1.171,33,1.171,34,1.171,35,1.171,36,1.171]]],"invertedIndex":[["activ",{"_index":36,"title":{},"description":{"17":{}}}],["cach",{"_index":0,"title":{"0":{},"1":{}},"description":{"1":{}}}],["children",{"_index":5,"title":{"7":{}},"description":{"1":{},"7":{}}}],["command",{"_index":8,"title":{"3":{},"15":{}},"description":{"3":{},"15":{},"17":{}}}],["content",{"_index":21,"title":{"11":{}},"description":{"11":{}}}],["describ",{"_index":11,"title":{},"description":{"3":{},"5":{},"7":{}}}],["entri",{"_index":4,"title":{"5":{},"17":{}},"description":{"1":{},"5":{},"7":{},"9":{},"11":{},"13":{}}}],["exec",{"_index":6,"title":{"2":{}},"description":{}}],["execut",{"_index":7,"title":{"3":{}},"description":{"3":{}}}],["given",{"_index":17,"title":{},"description":{"5":{},"7":{}}}],["histori",{"_index":25,"title":{"14":{},"15":{},"17":{}},"description":{"17":{}}}],["index",{"_index":34,"title":{},"description":{"17":{}}}],["info",{"_index":14,"title":{"4":{},"5":{}},"description":{}}],["item",{"_index":2,"title":{"1":{}},"description":{}}],["journal",{"_index":29,"title":{"16":{}},"description":{}}],["list",{"_index":18,"title":{"6":{},"7":{}},"description":{"7":{},"15":{}}}],["log",{"_index":30,"title":{"17":{}},"description":{"17":{}}}],["metadata",{"_index":19,"title":{"8":{},"9":{}},"description":{"9":{}}}],["new",{"_index":24,"title":{},"description":{"13":{}}}],["object",{"_index":16,"title":{},"description":{"5":{},"7":{}}}],["particular",{"_index":31,"title":{"17":{}},"description":{"17":{}}}],["path",{"_index":13,"title":{"5":{},"7":{}},"description":{"3":{},"5":{},"7":{}}}],["read",{"_index":20,"title":{"10":{},"11":{}},"description":{"11":{}}}],["relat",{"_index":32,"title":{},"description":{"17":{}}}],["remot",{"_index":9,"title":{"3":{}},"description":{"3":{}}}],["remov",{"_index":1,"title":{"1":{}},"description":{"1":{}}}],["request",{"_index":33,"title":{},"description":{"17":{}}}],["return",{"_index":15,"title":{},"description":{"5":{},"7":{}}}],["run",{"_index":26,"title":{},"description":{"15":{},"17":{}}}],["specifi",{"_index":3,"title":{},"description":{"1":{},"9":{},"11":{},"13":{}}}],["stream",{"_index":22,"title":{"12":{},"13":{}},"description":{"13":{}}}],["suppli",{"_index":12,"title":{},"description":{"3":{}}}],["system",{"_index":10,"title":{"3":{}},"description":{"3":{}}}],["updat",{"_index":23,"title":{"13":{}},"description":{"13":{}}}],["via",{"_index":27,"title":{},"description":{"15":{},"17":{}}}],["wash",{"_index":28,"title":{},"description":{"15":{},"17":{}}}],["within",{"_index":35,"title":{},"description":{"17":{}}}]],"pipeline":[]}},"options":{"nativeScrollbars":true}};
 
     var container = document.getElementById('redoc');
     Redoc.hydrate(__redoc_state, container);;


Test patch:
diff --git a/api/helpers_test.go b/api/helpers_test.go
index a2ed7a9..0f0f717 100644
--- a/api/helpers_test.go
+++ b/api/helpers_test.go
@@ -164,6 +164,31 @@ func (suite *HelpersTestSuite) TestGetEntryFromPath() {
 	plug.AssertExpectations(suite.T())
 }
 
+func (suite *HelpersTestSuite) TestGetScalarParam() {
+	var u url.URL
+	suite.Empty(getScalarParam(&u, "param"))
+
+	u.RawQuery = "param=hello"
+	suite.Equal("hello", getScalarParam(&u, "param"))
+
+	u.RawQuery = "param=hello&param=goodbye"
+	suite.Equal("goodbye", getScalarParam(&u, "param"))
+}
+
+func (suite *HelpersTestSuite) TestGetBoolParam() {
+	var u url.URL
+	for query, expect := range map[string]bool{"": false, "param=true": true, "param=false": false} {
+		u.RawQuery = query
+		val, err := getBoolParam(&u, "param")
+		suite.Nil(err)
+		suite.Equal(expect, val)
+	}
+
+	u.RawQuery = "param=other"
+	_, err := getBoolParam(&u, "param")
+	suite.Error(err)
+}
+
 func TestHelpers(t *testing.T) {
 	suite.Run(t, new(HelpersTestSuite))
 }
diff --git a/plugin/helpers_test.go b/plugin/helpers_test.go
index 5ad0d6f..3686823 100644
--- a/plugin/helpers_test.go
+++ b/plugin/helpers_test.go
@@ -1,6 +1,9 @@
 package plugin
 
 import (
+	"fmt"
+	"os"
+	"os/exec"
 	"testing"
 	"time"
 
@@ -67,6 +70,32 @@ func (suite *HelpersTestSuite) TestAttributes() {
 	suite.Equal(e.attr, Attributes(e))
 }
 
+func (suite *HelpersTestSuite) TestExitCodeFromErr() {
+	exitCode, err := ExitCodeFromErr(nil)
+	if suite.NoError(err) {
+		suite.Equal(
+			0,
+			exitCode,
+			"ExitCodeFromErr should return an exit code of 0 if no error was passed-in",
+		)
+	}
+
+	arbitraryErr := fmt.Errorf("an arbitrary error")
+	_, err = ExitCodeFromErr(arbitraryErr)
+	suite.EqualError(err, arbitraryErr.Error())
+
+	// The default exit code is 0 for an empty ProcessState object
+	exitErr := &exec.ExitError{ProcessState: &os.ProcessState{}}
+	exitCode, err = ExitCodeFromErr(exitErr)
+	if suite.NoError(err) {
+		suite.Equal(
+			0,
+			exitCode,
+			"ExitCodeFromErr should return the ExitError's exit code",
+		)
+	}
+}
+
 func TestHelpers(t *testing.T) {
 	suite.Run(t, new(HelpersTestSuite))
 }