Repository: digitalocean/doctl
Pull request #1290 (2022-10-20 17:14:59)

Various improvements to 'doctl serverless'
This PR merges `feature/serverless` into `main` as part of a plan to stop using the feature branch to stage everything.   We may resurrect it to stage large multi-PR multi-developer changes in the future, but, for now, we will be opening pull requests directly against `main`.

The changes being merged here are all internal-facing.  There is a substantial reduction in the number of commands that use the serverless plugin and a corresponding increase in the number of commands coded natively as part of doctl and going more directly to the appropriate remote endpoints via https.   There are a couple of hidden flags on the `connect` command that allow it to be used with test backends (not intended for normal customer use).   Otherwise, behavior should be preserved.


Patch:
diff --git a/commands/activations.go b/commands/activations.go
index b8e77edd..2362ad6d 100644
--- a/commands/activations.go
+++ b/commands/activations.go
@@ -14,10 +14,26 @@ limitations under the License.
 package commands
 
 import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"regexp"
+	"time"
+
+	"github.com/apache/openwhisk-client-go/whisk"
 	"github.com/digitalocean/doctl"
+	"github.com/digitalocean/doctl/commands/charm/text"
+	"github.com/digitalocean/doctl/commands/displayers"
 	"github.com/spf13/cobra"
 )
 
+// ShownActivation is what is actually shown as an activation ... it adds a date field which is a human-readable
+// version of the start field.
+type ShownActivation struct {
+	whisk.Activation
+	Date string `json:"date,omitempty"`
+}
+
 // Activations generates the serverless 'activations' subtree for addition to the doctl command
 func Activations() *Command {
 	cmd := &Command{
@@ -43,14 +59,16 @@ logs.`,
 	AddStringFlag(get, "function", "f", "", "Fetch activations for a specific function")
 	AddBoolFlag(get, "quiet", "q", false, "Suppress last activation information header")
 
-	list := CmdBuilder(cmd, RunActivationsList, "list [<activation_name>]", "Lists Activations for which records exist",
+	list := CmdBuilder(cmd, RunActivationsList, "list [<function_name>]", "Lists Activations for which records exist",
 		`Use `+"`"+`doctl serverless activations list`+"`"+` to list the activation records that are present in the cloud for previously
 invoked functions.`,
-		Writer, aliasOpt("ls"))
-	AddStringFlag(list, "limit", "l", "", "only return LIMIT number of activations (default 30, max 200)")
-	AddStringFlag(list, "skip", "s", "", "exclude the first SKIP number of activations from the result")
-	AddStringFlag(list, "since", "", "", "return activations with timestamps later than SINCE; measured in milliseconds since Th, 01, Jan 1970")
-	AddStringFlag(list, "upto", "", "", "return activations with timestamps earlier than UPTO; measured in milliseconds since Th, 01, Jan 1970")
+		Writer,
+		displayerType(&displayers.Activation{}),
+		aliasOpt("ls"))
+	AddIntFlag(list, "limit", "l", 30, "only return LIMIT number of activations (default 30, max 200)")
+	AddIntFlag(list, "skip", "s", 0, "exclude the first SKIP number of activations from the result")
+	AddIntFlag(list, "since", "", 0, "return activations with timestamps later than SINCE; measured in milliseconds since Th, 01, Jan 1970")
+	AddIntFlag(list, "upto", "", 0, "return activations with timestamps earlier than UPTO; measured in milliseconds since Th, 01, Jan 1970")
 	AddBoolFlag(list, "count", "", false, "show only the total number of activations")
 	AddBoolFlag(list, "full", "f", false, "include full activation description")
 
@@ -85,25 +103,186 @@ func RunActivationsGet(c *CmdConfig) error {
 	if argCount > 1 {
 		return doctl.NewTooManyArgsErr(c.NS)
 	}
-	replaceFunctionWithAction(c)
-	output, err := RunServerlessExec(activationGet, c, []string{flagLast, flagLogs, flagResult, flagQuiet}, []string{flagSkip, flagAction})
-	if err != nil {
-		return err
+	var id string
+	if argCount > 0 {
+		id = c.Args[0]
 	}
-	return c.PrintServerlessTextOutput(output)
+	logsFlag, _ := c.Doit.GetBool(c.NS, flagLogs)
+	resultFlag, _ := c.Doit.GetBool(c.NS, flagResult)
+	quietFlag, _ := c.Doit.GetBool(c.NS, flagQuiet)
+	// There is also a 'last' flag, which is historical.  Since it's behavior is the
+	// default, and the past convention was to ignore it if a single id was specified,
+	// (rather than indicating an error), it is completely ignored here but accepted for
+	// backward compatibility.  In the aio implementation (incorporated in nim, previously
+	// incorporated here), the flag had to be set explicitly (rather than just implied) in
+	// order to get a "banner" (additional informational line)  when requesting logs or
+	// result only.  This seems pointless and we will always display the banner for a
+	// single logs or result output unless --quiet is specified.
+	skipFlag, _ := c.Doit.GetInt(c.NS, flagSkip) // 0 if not there
+	functionFlag, _ := c.Doit.GetString(c.NS, flagFunction)
+	sls := c.Serverless()
+	if id == "" {
+		// If there is no id, the convention is to retrieve the last activation, subject to possible
+		// filtering or skipping
+		options := whisk.ActivationListOptions{Limit: 1, Skip: skipFlag}
+		if functionFlag != "" {
+			options.Name = functionFlag
+		}
+		list, err := sls.ListActivations(options)
+		if err != nil {
+			return err
+		}
+		if len(list) == 0 {
+			return fmt.Errorf("no activations were returned")
+		}
+		activation := list[0]
+		id = activation.ActivationID
+		if !quietFlag && (logsFlag || resultFlag) {
+			makeBanner(c.Out, activation)
+		}
+	}
+	if logsFlag {
+		activation, err := sls.GetActivationLogs(id)
+		if err != nil {
+			return err
+		}
+		if len(activation.Logs) == 0 {
+			return fmt.Errorf("no logs available")
+		}
+		printLogs(c.Out, true, activation)
+	} else if resultFlag {
+		response, err := sls.GetActivationResult(id)
+		if err != nil {
+			return err
+		}
+		if response.Result == nil {
+			return fmt.Errorf("no result available")
+		}
+		printResult(c.Out, response.Result)
+	} else {
+		activation, err := sls.GetActivation(id)
+		if err != nil {
+			return err
+		}
+		printActivationRecord(c.Out, activation)
+	}
+	return nil
+}
+
+// makeBanner is a subroutine that prints a single "banner" line summarizing information about an
+// activation.  This is done in conjunction with a request to print only logs or only the result, since,
+// otherwise, it is difficult to know what activation is being talked about.
+func makeBanner(writer io.Writer, activation whisk.Activation) {
+	end := time.UnixMilli(activation.End).Format("01/02 03:04:05")
+	init := text.NewStyled("=== ").Muted()
+	body := fmt.Sprintf("%s %s %s %s:%s", activation.ActivationID, displayers.GetActivationStatus(activation.StatusCode),
+		end, activation.Name, activation.Version)
+	msg := text.NewStyled(body).Highlight()
+	fmt.Fprintln(writer, init.String()+msg.String())
+}
+
+// printLog is a subroutine for printing just the logs of an activation
+func printLogs(writer io.Writer, strip bool, activation whisk.Activation) {
+	for _, log := range activation.Logs {
+		if strip {
+			log = stripLog(log)
+		}
+		fmt.Fprintln(writer, log)
+	}
+}
+
+// dtsRegex is a regular expression that matches the prefix of some activation log entries.
+// It is used by stripLog to remove that prefix
+var dtsRegex = regexp.MustCompile(`\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:.*: `)
+
+// stripLog strips the prefix from log entries
+func stripLog(entry string) string {
+	// `2019-10-11T19:08:57.298Z       stdout: login-success ::  { code: ...`
+	// should become: `login-success ::  { code: ...`
+	found := dtsRegex.FindString(entry)
+	return entry[len(found):]
+}
+
+// printResult is a subroutine for printing just the result of an activation
+func printResult(writer io.Writer, result *whisk.Result) {
+	var msg string
+	bytes, err := json.MarshalIndent(result, "", "  ")
+	if err == nil {
+		msg = string(bytes)
+	} else {
+		msg = "<unable to represent the result as JSON>"
+	}
+	fmt.Fprintln(writer, msg)
+}
+
+// printActivationRecord is a subroutine for printing the entire activation record
+func printActivationRecord(writer io.Writer, activation whisk.Activation) {
+	var msg string
+	date := time.UnixMilli(activation.Start).Format("2006-01-02 03:04:05")
+	toShow := ShownActivation{Activation: activation, Date: date}
+	bytes, err := json.MarshalIndent(toShow, "", "  ")
+	if err == nil {
+		msg = string(bytes)
+	} else {
+		msg = "<unable to represent the activation as JSON>"
+	}
+	fmt.Fprintln(writer, msg)
 }
 
 // RunActivationsList supports the 'activations list' command
 func RunActivationsList(c *CmdConfig) error {
 	argCount := len(c.Args)
+
 	if argCount > 1 {
 		return doctl.NewTooManyArgsErr(c.NS)
 	}
-	output, err := RunServerlessExec(activationList, c, []string{flagCount, flagFull}, []string{flagLimit, flagSkip, flagSince, flagUpto})
+	sls := c.Serverless()
+
+	var name string
+	if argCount > 0 {
+		name = c.Args[0]
+	}
+
+	countFlags, _ := c.Doit.GetBool(c.NS, flagCount)
+	fullFlag, _ := c.Doit.GetBool(c.NS, flagFull)
+	skipFlag, _ := c.Doit.GetInt(c.NS, flagSkip)
+	sinceFlag, _ := c.Doit.GetInt(c.NS, flagSince)
+	upToFlag, _ := c.Doit.GetInt(c.NS, flagUpto)
+	limitFlag, _ := c.Doit.GetInt(c.NS, flagLimit)
+
+	limit := limitFlag
+	if limitFlag > 200 {
+		limit = 200
+	}
+
+	if countFlags {
+		options := whisk.ActivationCountOptions{Since: int64(sinceFlag), Upto: int64(upToFlag), Name: name}
+		count, err := sls.GetActivationCount(options)
+		if err != nil {
+			return err
+		}
+
+		if name != "" {
+			fmt.Fprintf(c.Out, "You have %d activations in this namespace for function %s \n", count.Activations, name)
+		} else {
+			fmt.Fprintf(c.Out, "You have %d activations in this namespace \n", count.Activations)
+		}
+		return nil
+	}
+
+	options := whisk.ActivationListOptions{Limit: limit, Skip: skipFlag, Since: int64(sinceFlag), Upto: int64(upToFlag), Docs: fullFlag, Name: name}
+
+	actv, err := sls.ListActivations(options)
 	if err != nil {
 		return err
 	}
-	return c.PrintServerlessTextOutput(output)
+
+	items := &displayers.Activation{Activations: actv}
+	if fullFlag {
+		return items.JSON(c.Out)
+	}
+
+	return c.Display(items)
 }
 
 // RunActivationsLogs supports the 'activations logs' command
@@ -142,12 +321,53 @@ func RunActivationsResult(c *CmdConfig) error {
 	if argCount > 1 {
 		return doctl.NewTooManyArgsErr(c.NS)
 	}
-	replaceFunctionWithAction(c)
-	output, err := RunServerlessExec(activationResult, c, []string{flagLast, flagQuiet}, []string{flagLimit, flagSkip, flagAction})
-	if err != nil {
-		return err
+	var id string
+	if argCount > 0 {
+		id = c.Args[0]
 	}
-	return c.PrintServerlessTextOutput(output)
+	quietFlag, _ := c.Doit.GetBool(c.NS, flagQuiet)
+	skipFlag, _ := c.Doit.GetInt(c.NS, flagSkip)   // 0 if not there
+	limitFlag, _ := c.Doit.GetInt(c.NS, flagLimit) // 0 if not there
+	functionFlag, _ := c.Doit.GetString(c.NS, flagFunction)
+	limit := 1
+	if limitFlag > 200 {
+		limit = 200
+	} else if limitFlag > 0 {
+		limit = limitFlag
+	}
+	options := whisk.ActivationListOptions{Limit: limit, Skip: skipFlag}
+	sls := c.Serverless()
+	var activations []whisk.Activation
+	if id == "" {
+		if functionFlag != "" {
+			options.Name = functionFlag
+		}
+		actv, err := sls.ListActivations(options)
+		if err != nil {
+			return err
+		}
+		activations = actv
+	} else {
+		activations = []whisk.Activation{
+			{ActivationID: id},
+		}
+	}
+	reversed := make([]whisk.Activation, len(activations))
+	for i, activation := range activations {
+		response, err := sls.GetActivationResult(activation.ActivationID)
+		if err != nil {
+			return err
+		}
+		activation.Result = response.Result
+		reversed[len(activations)-i-1] = activation
+	}
+	for _, activation := range reversed {
+		if !quietFlag && id == "" {
+			makeBanner(c.Out, activation)
+		}
+		printResult(c.Out, activation.Result)
+	}
+	return nil
 }
 
 // replaceFunctionWithAction detects that --function was specified and renames it to --action (which is what nim
diff --git a/commands/displayers/activations.go b/commands/displayers/activations.go
new file mode 100644
index 00000000..f7d30fbf
--- /dev/null
+++ b/commands/displayers/activations.go
@@ -0,0 +1,120 @@
+package displayers
+
+import (
+	"fmt"
+	"io"
+	"strings"
+	"time"
+
+	"github.com/apache/openwhisk-client-go/whisk"
+)
+
+type Activation struct {
+	Activations []whisk.Activation
+}
+
+var _ Displayable = &Activation{}
+
+// ColMap implements Displayable
+func (a *Activation) ColMap() map[string]string {
+	return map[string]string{
+		"Datetime":     "Datetime",
+		"Status":       "Status",
+		"Kind":         "Kind",
+		"Version":      "Version",
+		"ActivationID": "Activation ID",
+		"Start":        "Start",
+		"Wait":         "Wait",
+		"Duration":     "Duration",
+		"Function":     "Function",
+	}
+}
+
+// Cols implements Displayable
+func (a *Activation) Cols() []string {
+	return []string{
+		"Datetime",
+		"Status",
+		"Kind",
+		"Version",
+		"ActivationID",
+		"Start",
+		"Wait",
+		"Duration",
+		"Function",
+	}
+}
+
+// JSON implements Displayable
+func (a *Activation) JSON(out io.Writer) error {
+	return writeJSON(a.Activations, out)
+}
+
+// KV implements Displayable
+func (a *Activation) KV() []map[string]interface{} {
+	out := make([]map[string]interface{}, 0, len(a.Activations))
+
+	for _, actv := range a.Activations {
+		o := map[string]interface{}{
+			"Datetime":     time.UnixMilli(actv.Start).Format("01/02 03:04:05"),
+			"Status":       GetActivationStatus(actv.StatusCode),
+			"Kind":         getActivationAnnotationValue(actv, "kind"),
+			"Version":      actv.Version,
+			"ActivationID": actv.ActivationID,
+			"Start":        getActivationStartType(actv),
+			"Wait":         getActivationAnnotationValue(actv, "waitTime"),
+			"Duration":     fmt.Sprintf("%dms", actv.Duration),
+			"Function":     getActivationFunctionName(actv),
+		}
+		out = append(out, o)
+	}
+	return out
+}
+
+func getActivationStartType(a whisk.Activation) string {
+	if getActivationAnnotationValue(a, "init") == "" {
+		return "cold"
+	}
+	return "warm"
+}
+
+// Gets the full function name for the activation.
+func getActivationFunctionName(a whisk.Activation) string {
+	name := a.Name
+	path := getActivationAnnotationValue(a, "path")
+
+	if path == nil {
+		return name
+	}
+
+	parts := strings.Split(path.(string), "/")
+
+	if len(parts) == 3 {
+		return parts[1] + "/" + name
+	}
+
+	return name
+}
+
+func getActivationAnnotationValue(a whisk.Activation, key string) interface{} {
+	if a.Annotations == nil {
+		return nil
+	}
+	return a.Annotations.GetValue(key)
+}
+
+// converts numeric status codes to typical string
+func GetActivationStatus(statusCode int) string {
+	switch statusCode {
+	case 0:
+		return "success"
+	case 1:
+		return "application error"
+	case 2:
+		return "developer error"
+	case 3:
+		return "system error"
+	default:
+		return "unknown"
+	}
+}
diff --git a/commands/functions.go b/commands/functions.go
index 67244b60..987eef65 100644
--- a/commands/functions.go
+++ b/commands/functions.go
@@ -19,6 +19,7 @@ import (
 	"errors"
 	"fmt"
 	"os"
+	"sort"
 	"strings"
 
 	"github.com/apache/openwhisk-client-go/whisk"
@@ -266,34 +267,48 @@ func RunFunctionsList(c *CmdConfig) error {
 	if argCount > 1 {
 		return doctl.NewTooManyArgsErr(c.NS)
 	}
+	var pkg string
+	if argCount == 1 {
+		pkg = c.Args[0]
+	}
 	// Determine if '--count' is requested since we will use simple text output in that case.
 	// Count is mutually exclusive with the global format flag.
 	count, _ := c.Doit.GetBool(c.NS, flagCount)
 	if count && c.Doit.IsSet("format") {
 		return errors.New("the --count and --format flags are mutually exclusive")
 	}
-	// Add JSON flag so we can control output format
-	if !count {
-		c.Doit.Set(c.NS, flagJSON, true)
-	}
-	output, err := RunServerlessExec(actionList, c, []string{flagCount, flagNameSort, flagNameName, flagJSON}, []string{flagLimit, flagSkip})
+	// Retrieve other flags
+	skip, _ := c.Doit.GetInt(c.NS, flagSkip)
+	limit, _ := c.Doit.GetInt(c.NS, flagLimit)
+	nameSort, _ := c.Doit.GetBool(c.NS, flagNameSort)
+	nameName, _ := c.Doit.GetBool(c.NS, flagNameName)
+	// Get information from backend
+	list, err := c.Serverless().ListFunctions(pkg, skip, limit)
 	if err != nil {
 		return err
 	}
 	if count {
-		return c.PrintServerlessTextOutput(output)
+		plural := "s"
+		are := "are"
+		if len(list) == 1 {
+			plural = ""
+			are = "is"
+		}
+		fmt.Fprintf(c.Out, "There %s %d function%s in this namespace.\n", are, len(list), plural)
+		return nil
 	}
-	// Reparse the output to use a more specific type, which can then be passed to the displayer
-	rawOutput, err := json.Marshal(output.Entity)
-	if err != nil {
-		return err
+	if nameSort || nameName {
+		sortFunctionList(list)
 	}
-	var formatted []whisk.Action
-	err = json.Unmarshal(rawOutput, &formatted)
-	if err != nil {
-		return err
+	return c.Display(&displayers.Functions{Info: list})
+}
+
+// sortFunctionList performs a sort of a function list (by name)
+func sortFunctionList(list []whisk.Action) {
+	isLess := func(i, j int) bool {
+		return list[i].Name < list[j].Name
 	}
-	return c.Display(&displayers.Functions{Info: formatted})
+	sort.Slice(list, isLess)
 }
 
 // consolidateParams accepts parameters from a file, the command line, or both, and consolidates all
diff --git a/commands/serverless.go b/commands/serverless.go
index fae99786..8c69e2de 100644
--- a/commands/serverless.go
+++ b/commands/serverless.go
@@ -83,7 +83,7 @@ The install operation is long-running, and a network connection is required.`,
 	CmdBuilder(cmd, RunServerlessUninstall, "uninstall", "Removes the serverless support", `Removes serverless support from `+"`"+`doctl`+"`",
 		Writer)
 
-	CmdBuilder(cmd, RunServerlessConnect, "connect [<hint>]", "Connects local serverless support to a functions namespace",
+	connect := CmdBuilder(cmd, RunServerlessConnect, "connect [<hint>]", "Connects local serverless support to a functions namespace",
 		`This command connects `+"`"+`doctl serverless`+"`"+` support to a functions namespace of your choice.
 The optional argument should be a (complete or partial) match to a namespace label or id.
 If there is no argument, all namespaces are matched.  If the result is exactly one namespace,
@@ -91,6 +91,12 @@ you are connected to it.  If there are multiple namespaces, you have an opportun
 the one you want from a dialog.  Use `+"`"+`doctl serverless namespaces`+"`"+` to create, delete, and
 list your namespaces.`,
 		Writer)
+	// The apihost and auth flags will always be hidden.  They support testing using doctl on clusters that are not in production
+	// and hence are unknown to the portal.
+	AddStringFlag(connect, "apihost", "", "", "")
+	AddStringFlag(connect, "auth", "", "", "")
+	connect.Flags().MarkHidden("apihost")
+	connect.Flags().MarkHidden("auth")
 
 	status := CmdBuilder(cmd, RunServerlessStatus, "status", "Provide information about serverless support",
 		`This command reports the status of serverless support and some details concerning its connected functions namespace.
@@ -194,13 +200,34 @@ func RunServerlessConnect(c *CmdConfig) error {
 	var (
 		err error
 	)
+	sls := c.Serverless()
+
+	// Support the hidden capability to connect to non-production clusters to support various kinds of testing.
+	// The presence of 'auth' and 'apihost' flags trumps other parts of the syntax, but both must be present.
+	apihost, _ := c.Doit.GetString(c.NS, "apihost")
+	auth, _ := c.Doit.GetString(c.NS, "auth")
+	if len(apihost) > 0 && len(auth) > 0 {
+		namespace, err := sls.GetNamespaceFromCluster(apihost, auth)
+		if err != nil {
+			return err
+		}
+		credential := do.ServerlessCredential{Auth: auth}
+		creds := do.ServerlessCredentials{
+			APIHost:     apihost,
+			Namespace:   namespace,
+			Credentials: map[string]map[string]do.ServerlessCredential{apihost: {namespace: credential}},
+		}
+		return finishConnecting(sls, creds, "", c.Out)
+	}
+	if len(apihost) > 0 || len(auth) > 0 {
+		return fmt.Errorf("If either of 'apihost' or 'auth' is specified then both must be specified")
+	}
+	// Neither 'auth' nor 'apihost' was specified, so continue with other options.
 
 	if len(c.Args) > 1 {
 		return doctl.NewTooManyArgsErr(c.NS)
 	}
 
-	sls := c.Serverless()
-
 	// Non-standard check for the connect command (only): it's ok to not be connected.
 	err = sls.CheckServerlessStatus()
 	if err != nil && err != do.ErrServerlessNotConnected {
@@ -298,7 +325,8 @@ func finishConnecting(sls do.ServerlessService, creds do.ServerlessCredentials,
 
 // RunServerlessStatus gives a report on the status of the serverless (installed, up to date, connected)
 func RunServerlessStatus(c *CmdConfig) error {
-	status := c.Serverless().CheckServerlessStatus()
+	sls := c.Serverless()
+	status := sls.CheckServerlessStatus()
 	if status == do.ErrServerlessNotInstalled {
 		return status
 	}
@@ -321,20 +349,20 @@ func RunServerlessStatus(c *CmdConfig) error {
 	}
 	// Check the connected state more deeply (since this is a status command we want to
 	// be more accurate; the connected check in checkServerlessStatus is lightweight and heuristic).
-	result, err := ServerlessExec(c, "auth/current", "--apihost", "--name")
-	if err != nil || len(result.Error) > 0 {
-		return do.ErrServerlessNotConnected
+	creds, err := sls.ReadCredentials()
+	if err != nil {
+		return nil
 	}
-	if result.Entity == nil {
-		return errors.New("Could not retrieve information about the connected namespace")
+	auth := creds.Credentials[creds.APIHost][creds.Namespace].Auth
+	checkNS, err := sls.GetNamespaceFromCluster(creds.APIHost, auth)
+	if err != nil || checkNS != creds.Namespace {
+		return do.ErrServerlessNotConnected
 	}
-	mapResult := result.Entity.(map[string]interface{})
-	apiHost := mapResult["apihost"].(string)
-	fmt.Fprintf(c.Out, "Connected to functions namespace '%s' on API host '%s'\n", mapResult["name"], apiHost)
+	fmt.Fprintf(c.Out, "Connected to functions namespace '%s' on API host '%s'\n", creds.Namespace, creds.APIHost)
 	fmt.Fprintf(c.Out, "Serverless software version is %s\n\n", do.GetMinServerlessVersion())
 	languages, _ := c.Doit.GetBool(c.NS, "languages")
 	if languages {
-		return showLanguageInfo(c, apiHost)
+		return showLanguageInfo(c, creds.APIHost)
 	}
 	return nil
 }
diff --git a/do/mocks/ServerlessService.go b/do/mocks/ServerlessService.go
index 4c02b965..d56985fb 100644
--- a/do/mocks/ServerlessService.go
+++ b/do/mocks/ServerlessService.go
@@ -124,6 +124,66 @@ func (mr *MockServerlessServiceMockRecorder) Exec(arg0 interface{}) *gomock.Call
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockServerlessService)(nil).Exec), arg0)
 }
 
+// GetActivation mocks base method.
+func (m *MockServerlessService) GetActivation(arg0 string) (whisk.Activation, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetActivation", arg0)
+	ret0, _ := ret[0].(whisk.Activation)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetActivation indicates an expected call of GetActivation.
+func (mr *MockServerlessServiceMockRecorder) GetActivation(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActivation", reflect.TypeOf((*MockServerlessService)(nil).GetActivation), arg0)
+}
+
+// GetActivationCount mocks base method.
+func (m *MockServerlessService) GetActivationCount(arg0 whisk.ActivationCountOptions) (whisk.ActivationCount, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetActivationCount", arg0)
+	ret0, _ := ret[0].(whisk.ActivationCount)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetActivationCount indicates an expected call of GetActivationCount.
+func (mr *MockServerlessServiceMockRecorder) GetActivationCount(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActivationCount", reflect.TypeOf((*MockServerlessService)(nil).GetActivationCount), arg0)
+}
+
+// GetActivationLogs mocks base method.
+func (m *MockServerlessService) GetActivationLogs(arg0 string) (whisk.Activation, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetActivationLogs", arg0)
+	ret0, _ := ret[0].(whisk.Activation)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetActivationLogs indicates an expected call of GetActivationLogs.
+func (mr *MockServerlessServiceMockRecorder) GetActivationLogs(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActivationLogs", reflect.TypeOf((*MockServerlessService)(nil).GetActivationLogs), arg0)
+}
+
+// GetActivationResult mocks base method.
+func (m *MockServerlessService) GetActivationResult(arg0 string) (whisk.Response, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetActivationResult", arg0)
+	ret0, _ := ret[0].(whisk.Response)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetActivationResult indicates an expected call of GetActivationResult.
+func (mr *MockServerlessServiceMockRecorder) GetActivationResult(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActivationResult", reflect.TypeOf((*MockServerlessService)(nil).GetActivationResult), arg0)
+}
+
 // GetConnectedAPIHost mocks base method.
 func (m *MockServerlessService) GetConnectedAPIHost() (string, error) {
 	m.ctrl.T.Helper()
@@ -185,6 +245,21 @@ func (mr *MockServerlessServiceMockRecorder) GetNamespace(arg0, arg1 interface{}
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNamespace", reflect.TypeOf((*MockServerlessService)(nil).GetNamespace), arg0, arg1)
 }
 
+// GetNamespaceFromCluster mocks base method.
+func (m *MockServerlessService) GetNamespaceFromCluster(arg0, arg1 string) (string, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetNamespaceFromCluster", arg0, arg1)
+	ret0, _ := ret[0].(string)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetNamespaceFromCluster indicates an expected call of GetNamespaceFromCluster.
+func (mr *MockServerlessServiceMockRecorder) GetNamespaceFromCluster(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNamespaceFromCluster", reflect.TypeOf((*MockServerlessService)(nil).GetNamespaceFromCluster), arg0, arg1)
+}
+
 // GetServerlessNamespace mocks base method.
 func (m *MockServerlessService) GetServerlessNamespace(arg0 context.Context) (do.ServerlessCredentials, error) {
 	m.ctrl.T.Helper()
@@ -230,10 +305,10 @@ func (mr *MockServerlessServiceMockRecorder) InstallServerless(arg0, arg1 interf
 }
 
 // InvokeFunction mocks base method.
-func (m *MockServerlessService) InvokeFunction(arg0 string, arg1 interface{}, arg2, arg3 bool) (map[string]interface{}, error) {
+func (m *MockServerlessService) InvokeFunction(arg0 string, arg1 interface{}, arg2, arg3 bool) (interface{}, error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "InvokeFunction", arg0, arg1, arg2, arg3)
-	ret0, _ := ret[0].(map[string]interface{})
+	ret0, _ := ret[0].(interface{})
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
@@ -245,7 +320,7 @@ func (mr *MockServerlessServiceMockRecorder) InvokeFunction(arg0, arg1, arg2, ar
 }
 
 // InvokeFunctionViaWeb mocks base method.
-func (m *MockServerlessService) InvokeFunctionViaWeb(arg0 string, arg1 map[string]interface{}) error {
+func (m *MockServerlessService) InvokeFunctionViaWeb(arg0 string, arg1 interface{}) error {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "InvokeFunctionViaWeb", arg0, arg1)
 	ret0, _ := ret[0].(error)
@@ -258,6 +333,36 @@ func (mr *MockServerlessServiceMockRecorder) InvokeFunctionViaWeb(arg0, arg1 int
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InvokeFunctionViaWeb", reflect.TypeOf((*MockServerlessService)(nil).InvokeFunctionViaWeb), arg0, arg1)
 }
 
+// ListActivations mocks base method.
+func (m *MockServerlessService) ListActivations(arg0 whisk.ActivationListOptions) ([]whisk.Activation, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListActivations", arg0)
+	ret0, _ := ret[0].([]whisk.Activation)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ListActivations indicates an expected call of ListActivations.
+func (mr *MockServerlessServiceMockRecorder) ListActivations(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListActivations", reflect.TypeOf((*MockServerlessService)(nil).ListActivations), arg0)
+}
+
+// ListFunctions mocks base method.
+func (m *MockServerlessService) ListFunctions(arg0 string, arg1, arg2 int) ([]whisk.Action, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListFunctions", arg0, arg1, arg2)
+	ret0, _ := ret[0].([]whisk.Action)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ListFunctions indicates an expected call of ListFunctions.
+func (mr *MockServerlessServiceMockRecorder) ListFunctions(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListFunctions", reflect.TypeOf((*MockServerlessService)(nil).ListFunctions), arg0, arg1, arg2)
+}
+
 // ListNamespaces mocks base method.
 func (m *MockServerlessService) ListNamespaces(arg0 context.Context) (do.NamespaceListResponse, error) {
 	m.ctrl.T.Helper()
diff --git a/do/serverless.go b/do/serverless.go
index 1fb8ecb0..fde815f7 100644
--- a/do/serverless.go
+++ b/do/serverless.go
@@ -206,6 +206,7 @@ type ServerlessService interface {
 	GetServerlessNamespace(context.Context) (ServerlessCredentials, error)
 	ListNamespaces(context.Context) (NamespaceListResponse, error)
 	GetNamespace(context.Context, string) (ServerlessCredentials, error)
+	GetNamespaceFromCluster(string, string) (string, error)
 	CreateNamespace(context.Context, string, string) (ServerlessCredentials, error)
 	DeleteNamespace(context.Context, string) error
 	ListTriggers(context.Context, string) ([]ServerlessTrigger, error)
@@ -217,8 +218,14 @@ type ServerlessService interface {
 	CheckServerlessStatus() error
 	InstallServerless(string, bool) error
 	GetFunction(string, bool) (whisk.Action, []FunctionParameter, error)
-	InvokeFunction(string, interface{}, bool, bool) (map[string]interface{}, error)
-	InvokeFunctionViaWeb(string, map[string]interface{}) error
+	ListFunctions(string, int, int) ([]whisk.Action, error)
+	InvokeFunction(string, interface{}, bool, bool) (interface{}, error)
+	InvokeFunctionViaWeb(string, interface{}) error
+	ListActivations(whisk.ActivationListOptions) ([]whisk.Activation, error)
+	GetActivationCount(whisk.ActivationCountOptions) (whisk.ActivationCount, error)
+	GetActivation(string) (whisk.Activation, error)
+	GetActivationLogs(string) (whisk.Activation, error)
+	GetActivationResult(string) (whisk.Response, error)
 	GetConnectedAPIHost() (string, error)
 	ReadProject(*ServerlessProject, []string) (ServerlessOutput, error)
 	WriteProject(ServerlessProject) (string, error)
@@ -343,6 +350,10 @@ func initWhisk(s *serverlessService) error {
 	if s.owClient != nil {
 		return nil
 	}
+	err := s.CheckServerlessStatus()
+	if err != nil {
+		return err
+	}
 	creds, err := s.ReadCredentials()
 	if err != nil {
 		return err
@@ -635,6 +646,27 @@ func (s *serverlessService) GetNamespace(ctx context.Context, name string) (Serv
 	return executeNamespaceRequest(ctx, s, req)
 }
 
+// GetNamespaceFromCluster obtains the namespace that uniquely owns a valid combination of API host and "auth"
+// (uuid:key).  This can be used to connect to clusters not known to the portal (e.g. dev clusters) or simply
+// to check that credentials are valid.
+func (s *serverlessService) GetNamespaceFromCluster(APIhost string, auth string) (string, error) {
+	// We do not use the shared client in serverlessService for this because it uses the stored
+	// credentials, not the passed ones.
+	config := whisk.Config{Host: APIhost, AuthToken: auth}
+	client, err := whisk.NewClient(http.DefaultClient, &config)
+	if err != nil {
+		return "", err
+	}
+	ns, _, err := client.Namespaces.List()
+	if err != nil {
+		return "", err
+	}
+	if len(ns) != 1 {
+		return "", fmt.Errorf("unexpected response when validating apihost and auth")
+	}
+	return ns[0].Name, nil
+}
+
 // CreateNamespace creates a new namespace and returns its credentials, given a label and region
 func (s *serverlessService) CreateNamespace(ctx context.Context, label string, region string) (ServerlessCredentials, error) {
 	reqBody := newNamespaceRequest{Namespace: inputNamespace{Label: label, Region: region}}
@@ -697,8 +729,25 @@ func (s *serverlessService) GetFunction(name string, fetchCode bool) (whisk.Acti
 	return *action, parameters, nil
 }
 
+// ListFunctions lists the functions of the connected namespace
+func (s *serverlessService) ListFunctions(pkg string, skip int, limit int) ([]whisk.Action, error) {
+	err := initWhisk(s)
+	if err != nil {
+		return []whisk.Action{}, err
+	}
+	if limit == 0 {
+		limit = 30
+	}
+	options := &whisk.ActionListOptions{
+		Skip:  skip,
+		Limit: limit,
+	}
+	list, _, err := s.owClient.Actions.List(pkg, options)
+	return list, err
+}
+
 // InvokeFunction invokes a function via POST with authentication
-func (s *serverlessService) InvokeFunction(name string, params interface{}, blocking bool, result bool) (map[string]interface{}, error) {
+func (s *serverlessService) InvokeFunction(name string, params interface{}, blocking bool, result bool) (interface{}, error) {
 	var empty map[string]interface{}
 	err := initWhisk(s)
 	if err != nil {
@@ -709,7 +758,7 @@ func (s *serverlessService) InvokeFunction(name string, params interface{}, bloc
 }
 
 // InvokeFunctionViaWeb invokes a function via GET using its web URL (or error if not a web function)
-func (s *serverlessService) InvokeFunctionViaWeb(name string, params map[string]interface{}) error {
+func (s *serverlessService) InvokeFunctionViaWeb(name string, params interface{}) error {
 	// Get the function so we can use its metadata in formulating the request
 	theFunction, _, err := s.GetFunction(name, false)
 	if err != nil {
@@ -741,7 +790,7 @@ func (s *serverlessService) InvokeFunctionViaWeb(name string, params map[string]
 	// Add params, if any
 	if params != nil {
 		encoded := url.Values{}
-		for key, val := range params {
+		for key, val := range params.(map[string]interface{}) {
 			stringVal, ok := val.(string)
 			if !ok {
 				return fmt.Errorf("the value of '%s' is not a string; web invocation is not possible", key)
@@ -753,6 +802,61 @@ func (s *serverlessService) InvokeFunctionViaWeb(name string, params map[string]
 	return browser.OpenURL(theURL)
 }
 
+// ListActivations drives the OpenWhisk API for listing activations
+func (s *serverlessService) ListActivations(options whisk.ActivationListOptions) ([]whisk.Activation, error) {
+	empty := []whisk.Activation{}
+	err := initWhisk(s)
+	if err != nil {
+		return empty, err
+	}
+	resp, _, err := s.owClient.Activations.List(&options)
+	return resp, err
+}
+
+// GetActivationCount drives the OpenWhisk API for getting the total number of activations in namespace
+func (s *serverlessService) GetActivationCount(options whisk.ActivationCountOptions) (whisk.ActivationCount, error) {
+	err := initWhisk(s)
+	empty := whisk.ActivationCount{}
+	if err != nil {
+		return empty, err
+	}
+	resp, _, err := s.owClient.Activations.Count(&options)
+	return *resp, err
+}
+
+// GetActivation drives the OpenWhisk API getting an activation
+func (s *serverlessService) GetActivation(id string) (whisk.Activation, error) {
+	empty := whisk.Activation{}
+	err := initWhisk(s)
+	if err != nil {
+		return empty, err
+	}
+	resp, _, err := s.owClient.Activations.Get(id)
+	return *resp, err
+}
+
+// GetActivationLogs drives the OpenWhisk API getting the logs of an activation
+func (s *serverlessService) GetActivationLogs(id string) (whisk.Activation, error) {
+	empty := whisk.Activation{}
+	err := initWhisk(s)
+	if err != nil {
+		return empty, err
+	}
+	resp, _, err := s.owClient.Activations.Logs(id)
+	return *resp, err
+}
+
+// GetActivationResult drives the OpenWhisk API getting the result of an activation
+func (s *serverlessService) GetActivationResult(id string) (whisk.Response, error) {
+	empty := whisk.Response{}
+	err := initWhisk(s)
+	if err != nil {
+		return empty, err
+	}
+	resp, _, err := s.owClient.Activations.Result(id)
+	return *resp, err
+}
+
 // GetConnectedAPIHost retrieves the API host to which the service is currently connected
 func (s *serverlessService) GetConnectedAPIHost() (string, error) {
 	err := initWhisk(s)
@@ -1001,8 +1105,8 @@ func (s *serverlessService) ReadCredentials() (ServerlessCredentials, error) {
 	return creds, err
 }
 
-// Determines whether the serverlessUptodate appears to be connected.  The purpose is
-// to fail fast (when feasible) on sandboxes that are clearly not connected.
+// Determines whether the serverless support appears to be connected.  The purpose is
+// to fail fast (when feasible) when it clearly is not connected.
 // However, it is important not to add excessive overhead on each call (e.g.
 // asking the plugin to validate credentials), so the test is not foolproof.
 // It merely tests whether a credentials directory has been created for the
@@ -1015,15 +1119,15 @@ func isServerlessConnected(credsDir string) bool {
 	return err == nil
 }
 
-// serverlessUptodate answers whether the installed version of the serverlessUptodate is at least
+// serverlessUptodate answers whether the installed version of the serverless support is at least
 // what is required by doctl
 func serverlessUptodate(serverlessDir string) bool {
 	return GetCurrentServerlessVersion(serverlessDir) >= GetMinServerlessVersion()
 }
 
-// GetCurrentServerlessVersion gets the version of the current serverless.
-// To be called only when serverless is known to exist.
-// Returns "0" if the installed serverless pre-dates the versioning system
+// GetCurrentServerlessVersion gets the version of the current plugin.
+// To be called only when the plugin is known to exist.
+// Returns "0" if the installed plugin pre-dates the versioning system
 // Otherwise, returns the version string stored in the serverless directory.
 func GetCurrentServerlessVersion(serverlessDir string) string {
 	versionFile := filepath.Join(serverlessDir, "version")
diff --git a/go.mod b/go.mod
index 41a30dfb..c4dea4a3 100644
--- a/go.mod
+++ b/go.mod
@@ -46,7 +46,7 @@ require (
 
 require (
 	github.com/MakeNowJust/heredoc v1.0.0
-	github.com/apache/openwhisk-client-go v0.0.0-20211007130743-38709899040b
+	github.com/apache/openwhisk-client-go v0.0.0-20221014112704-1ca897633f2d
 	github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2
 	github.com/charmbracelet/bubbles v0.13.1-0.20220731172002-8f6516082803
 	github.com/charmbracelet/bubbletea v0.22.0
diff --git a/go.sum b/go.sum
index 3847c1d8..51192a4d 100644
--- a/go.sum
+++ b/go.sum
@@ -93,8 +93,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
-github.com/apache/openwhisk-client-go v0.0.0-20211007130743-38709899040b h1:E0IprB8sveEHNP3WiZB/N/3nWzljyQCIDYFg0N3geGU=
-github.com/apache/openwhisk-client-go v0.0.0-20211007130743-38709899040b/go.mod h1:SAQU4bHGJ0sg6c1vQ8ojmQKXgGaneVnexWX4+2/KMr8=
+github.com/apache/openwhisk-client-go v0.0.0-20221014112704-1ca897633f2d h1:8sh89OGDm1tx/D/nsFwunhX90NjEPTn2k/DDLhOjexs=
+github.com/apache/openwhisk-client-go v0.0.0-20221014112704-1ca897633f2d/go.mod h1:SAQU4bHGJ0sg6c1vQ8ojmQKXgGaneVnexWX4+2/KMr8=
 github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs=
 github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
diff --git a/vendor/github.com/apache/openwhisk-client-go/whisk/action.go b/vendor/github.com/apache/openwhisk-client-go/whisk/action.go
index 53965683..36732360 100644
--- a/vendor/github.com/apache/openwhisk-client-go/whisk/action.go
+++ b/vendor/github.com/apache/openwhisk-client-go/whisk/action.go
@@ -287,8 +287,8 @@ func (s *ActionService) Delete(actionName string) (*http.Response, error) {
 	return resp, nil
 }
 
-func (s *ActionService) Invoke(actionName string, payload interface{}, blocking bool, result bool) (map[string]interface{}, *http.Response, error) {
-	var res map[string]interface{}
+func (s *ActionService) Invoke(actionName string, payload interface{}, blocking bool, result bool) (interface{}, *http.Response, error) {
+	var res interface{}
 
 	// Encode resource name as a path (with no query params) before inserting it into the URI
 	// This way any '?' chars in the name won't be treated as the beginning of the query params
diff --git a/vendor/github.com/apache/openwhisk-client-go/whisk/activation.go b/vendor/github.com/apache/openwhisk-client-go/whisk/activation.go
index fdbe40cd..2082bfa1 100644
--- a/vendor/github.com/apache/openwhisk-client-go/whisk/activation.go
+++ b/vendor/github.com/apache/openwhisk-client-go/whisk/activation.go
@@ -20,11 +20,12 @@ package whisk
 import (
 	"errors"
 	"fmt"
-	"github.com/apache/openwhisk-client-go/wski18n"
 	"net/http"
 	"net/url"
 	"strconv"
 	"time"
+
+	"github.com/apache/openwhisk-client-go/wski18n"
 )
 
 type ActivationService struct {
@@ -48,6 +49,10 @@ type Activation struct {
 	Publish      *bool       `json:"publish,omitempty"`
 }
 
+type ActivationCount struct {
+	Activations int64 `json:"activations"`
+}
+
 type ActivationFilteredRow struct {
 	Row       Activation
 	HeaderFmt string
@@ -61,7 +66,7 @@ type Response struct {
 	Result     *Result `json:"result,omitempty"`
 }
 
-type Result map[string]interface{}
+type Result interface{}
 
 type ActivationListOptions struct {
 	Name  string `url:"name,omitempty"`
@@ -72,7 +77,15 @@ type ActivationListOptions struct {
 	Docs  bool   `url:"docs,omitempty"`
 }
 
-//MWD - This structure may no longer be needed as the log format is now a string and not JSON
+type ActivationCountOptions struct {
+	Name  string `url:"name,omitempty"`
+	Skip  int    `url:"skip"`
+	Since int64  `url:"since,omitempty"`
+	Upto  int64  `url:"upto,omitempty"`
+	Count bool   `url:"count,omitempty"`
+}
+
+// MWD - This structure may no longer be needed as the log format is now a string and not JSON
 type Log struct {
 	Log    string `json:"log,omitempty"`
 	Stream string `json:"stream,omitempty"`
@@ -118,7 +131,9 @@ func TruncateStr(str string, maxlen int) string {
 }
 
 // ToSummaryRowString() returns a compound string of required parameters for printing
-//   from CLI command `wsk activation list`.
+//
+//	from CLI command `wsk activation list`.
+//
 // ***Method of type Sortable***
 func (activation ActivationFilteredRow) ToSummaryRowString() string {
 	s := time.Unix(0, activation.Row.Start*1000000)
@@ -189,6 +204,44 @@ func (s *ActivationService) List(options *ActivationListOptions) ([]Activation,
 	return activations, resp, nil
 }
 
+func (s *ActivationService) Count(options *ActivationCountOptions) (*ActivationCount, *http.Response, error) {
+	// TODO :: for some reason /activations only works with "_" as namespace
+	s.client.Namespace = "_"
+	route := "activations"
+
+	options.Count = true
+	routeUrl, err := addRouteOptions(route, options)
+
+	if err != nil {
+		Debug(DbgError, "addRouteOptions(%s, %#v) error: '%s'\n", route, options, err)
+		errStr := wski18n.T("Unable to append options '{{.options}}' to URL route '{{.route}}': {{.err}}",
+			map[string]interface{}{"options": fmt.Sprintf("%#v", options), "route": route, "err": err})
+		werr := MakeWskErrorFromWskError(errors.New(errStr), err, EXIT_CODE_ERR_GENERAL, DISPLAY_MSG, NO_DISPLAY_USAGE)
+		return nil, nil, werr
+	}
+
+	req, err := s.client.NewRequestUrl("GET", routeUrl, nil, IncludeNamespaceInUrl, AppendOpenWhiskPathPrefix, EncodeBodyAsJson, AuthRequired)
+	if err != nil {
+		Debug(DbgError, "http.NewRequestUrl(GET, %s, nil, IncludeNamespaceInUrl, AppendOpenWhiskPathPrefix, EncodeBodyAsJson, AuthRequired) error: '%s'\n", route, err)
+		errStr := wski18n.T("Unable to create HTTP request for GET '{{.route}}': {{.err}}",
+			map[string]interface{}{"route": route, "err": err})
+		werr := MakeWskErrorFromWskError(errors.New(errStr), err, EXIT_CODE_ERR_GENERAL, DISPLAY_MSG, NO_DISPLAY_USAGE)
+		return nil, nil, werr
+	}
+
+	Debug(DbgInfo, "Sending HTTP request - URL '%s'; req %#v\n", req.URL.String(), req)
+
+	count := new(ActivationCount)
+	resp, err := s.client.Do(req, &count, ExitWithSuccessOnTimeout)
+
+	if err != nil {
+		Debug(DbgError, "s.client.Do() error - HTTP req %s; error '%s'\n", req.URL.String(), err)
+		return nil, resp, err
+	}
+
+	return count, resp, nil
+}
+
 func (s *ActivationService) Get(activationID string) (*Activation, *http.Response, error) {
 	// TODO :: for some reason /activations/:id only works with "_" as namespace
 	s.client.Namespace = "_"
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 30bb0715..96940e8f 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -33,7 +33,7 @@ github.com/Microsoft/hcsshim/internal/vmcompute
 github.com/Microsoft/hcsshim/internal/wclayer
 github.com/Microsoft/hcsshim/internal/winapi
 github.com/Microsoft/hcsshim/osversion
-# github.com/apache/openwhisk-client-go v0.0.0-20211007130743-38709899040b
+# github.com/apache/openwhisk-client-go v0.0.0-20221014112704-1ca897633f2d
 ## explicit; go 1.15
 github.com/apache/openwhisk-client-go/whisk
 github.com/apache/openwhisk-client-go/wski18n


Test patch:
diff --git a/commands/activations_test.go b/commands/activations_test.go
index 8987a5f6..bf570a9f 100644
--- a/commands/activations_test.go
+++ b/commands/activations_test.go
@@ -14,10 +14,15 @@ limitations under the License.
 package commands
 
 import (
+	"bytes"
 	"os/exec"
 	"sort"
+	"strconv"
+	"strings"
 	"testing"
+	"time"
 
+	"github.com/apache/openwhisk-client-go/whisk"
 	"github.com/digitalocean/doctl/do"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -38,55 +43,248 @@ func TestActivationsCommand(t *testing.T) {
 	assert.Equal(t, expected, names)
 }
 
+var hello1Result = whisk.Result(map[string]interface{}{
+	"body": "Hello stranger!",
+})
+
+var hello2Result = whisk.Result(map[string]interface{}{
+	"body": "Hello Archie!",
+})
+
+var hello3Result = whisk.Result(map[string]interface{}{
+	"error": "Missing main/no code to execute.",
+})
+
+// theActivations is the set of activation assumed to be present, used to mock whisk API behavior
+var theActivations = []whisk.Activation{
+	{
+		Namespace:    "my-namespace",
+		Name:         "hello1",
+		Version:      "0.0.1",
+		ActivationID: "activation-1",
+		Start:        1664538810000,
+		End:          1664538820000,
+		Response: whisk.Response{
+			Status:     "success",
+			StatusCode: 0,
+			Success:    true,
+			Result:     &hello1Result,
+		},
+		Logs: []string{
+			"2022-09-30T11:53:50.567914279Z stdout: Hello stranger!",
+		},
+	},
+	{
+		Namespace:    "my-namespace",
+		Name:         "hello2",
+		Version:      "0.0.2",
+		ActivationID: "activation-2",
+		Start:        1664538830000,
+		End:          1664538840000,
+		Response: whisk.Response{
+			Status:     "success",
+			StatusCode: 0,
+			Success:    true,
+			Result:     &hello2Result,
+		},
+		Logs: []string{
+			"2022-09-30T11:53:50.567914279Z stdout: Hello Archie!",
+		},
+	},
+	{
+		Namespace:    "my-namespace",
+		Name:         "hello3",
+		Version:      "0.0.3",
+		ActivationID: "activation-3",
+		Start:        1664538850000,
+		End:          1664538860000,
+		Response: whisk.Response{
+			Result:  &hello3Result,
+			Status:  "developer error",
+			Success: false,
+		},
+	},
+}
+
+var theActivationCount = whisk.ActivationCount{
+	Activations: 1738,
+}
+
+// Timestamps in the activations are converted to dates using local time so, to make this test capable of running
+// in any timezone, we need to abstract things a bit.  Following the conventions in aio, the banner dates are computed
+// from End and the activation record dates from Start.
+var (
+	timestamps  = []int64{1664538810000, 1664538820000, 1664538830000, 1664538840000, 1664538850000, 1664538860000}
+	actvSymbols = []string{"%START1%", "%START2%", "%START3%"}
+	actvDates   = []string{
+		time.UnixMilli(timestamps[0]).Format("2006-01-02 03:04:05"),
+		time.UnixMilli(timestamps[2]).Format("2006-01-02 03:04:05"),
+		time.UnixMilli(timestamps[4]).Format("2006-01-02 03:04:05"),
+	}
+	bannerSymbols = []string{"%END1%", "%END2%", "%END3%"}
+	bannerDates   = []string{
+		time.UnixMilli(timestamps[1]).Format("01/02 03:04:05"),
+		time.UnixMilli(timestamps[3]).Format("01/02 03:04:05"),
+		time.UnixMilli(timestamps[5]).Format("01/02 03:04:05"),
+	}
+)
+
+// convertDates operates on the expected output (containing symbols) to substitute actual dates
+func convertDates(expected string) string {
+	for i, symbol := range actvSymbols {
+		expected = strings.Replace(expected, symbol, actvDates[i], 1)
+	}
+	for i, symbol := range bannerSymbols {
+		expected = strings.Replace(expected, symbol, bannerDates[i], 1)
+	}
+	return expected
+}
+
+// findActivation finds the activation with a given id (in these tests, assumed to be present)
+func findActivation(id string) whisk.Activation {
+	for _, activation := range theActivations {
+		if activation.ActivationID == id {
+			return activation
+		}
+	}
+	// Should not happen
+	panic("could not find " + id)
+}
+
 func TestActivationsGet(t *testing.T) {
 	tests := []struct {
-		name            string
-		doctlArgs       string
-		doctlFlags      map[string]string
-		expectedNimArgs []string
+		name           string
+		doctlArgs      string
+		doctlFlags     map[string]string
+		listOptions    whisk.ActivationListOptions
+		expectedOutput string
 	}{
 		{
-			name:            "no flags with ID",
-			doctlArgs:       "activationid",
-			expectedNimArgs: []string{"activationid"},
+			name:      "no flags with ID",
+			doctlArgs: "activation-2",
+			expectedOutput: `{
+  "namespace": "my-namespace",
+  "name": "hello2",
+  "version": "0.0.2",
+  "subject": "",
+  "activationId": "activation-2",
+  "start": 1664538830000,
+  "end": 1664538840000,
+  "duration": 0,
+  "statusCode": 0,
+  "response": {
+    "status": "success",
+    "statusCode": 0,
+    "success": true,
+    "result": {
+      "body": "Hello Archie!"
+    }
+  },
+  "logs": [
+    "2022-09-30T11:53:50.567914279Z stdout: Hello Archie!"
+  ],
+  "annotations": null,
+  "date": "%START2%"
+}
+`,
 		},
 		{
-			name:            "no flags or args",
-			expectedNimArgs: []string{},
+			name:        "no flags or args",
+			listOptions: whisk.ActivationListOptions{Limit: 1},
+			expectedOutput: `{
+  "namespace": "my-namespace",
+  "name": "hello1",
+  "version": "0.0.1",
+  "subject": "",
+  "activationId": "activation-1",
+  "start": 1664538810000,
+  "end": 1664538820000,
+  "duration": 0,
+  "statusCode": 0,
+  "response": {
+    "status": "success",
+    "statusCode": 0,
+    "success": true,
+    "result": {
+      "body": "Hello stranger!"
+    }
+  },
+  "logs": [
+    "2022-09-30T11:53:50.567914279Z stdout: Hello stranger!"
+  ],
+  "annotations": null,
+  "date": "%START1%"
+}
+`,
 		},
 		{
-			name:            "last flag",
-			doctlArgs:       "activationid",
-			doctlFlags:      map[string]string{"last": ""},
-			expectedNimArgs: []string{"activationid", "--last"},
+			name:        "logs flag",
+			doctlFlags:  map[string]string{"logs": ""},
+			listOptions: whisk.ActivationListOptions{Limit: 1},
+			expectedOutput: `=== activation-1 success %END1% hello1:0.0.1
+Hello stranger!
+`,
 		},
 		{
-			name:            "logs flag",
-			doctlArgs:       "activationid",
-			doctlFlags:      map[string]string{"logs": ""},
-			expectedNimArgs: []string{"activationid", "--logs"},
+			name:        "result flag",
+			doctlFlags:  map[string]string{"result": ""},
+			listOptions: whisk.ActivationListOptions{Limit: 1},
+			expectedOutput: `=== activation-1 success %END1% hello1:0.0.1
+{
+  "body": "Hello stranger!"
+}
+`,
 		},
 		{
-			name:            "skip flag",
-			doctlArgs:       "activationid",
-			doctlFlags:      map[string]string{"skip": "10"},
-			expectedNimArgs: []string{"activationid", "--skip", "10"},
+			name:        "skip flag",
+			doctlFlags:  map[string]string{"skip": "2"},
+			listOptions: whisk.ActivationListOptions{Limit: 1, Skip: 2},
+			expectedOutput: `{
+  "namespace": "my-namespace",
+  "name": "hello3",
+  "version": "0.0.3",
+  "subject": "",
+  "activationId": "activation-3",
+  "start": 1664538850000,
+  "end": 1664538860000,
+  "duration": 0,
+  "statusCode": 0,
+  "response": {
+    "status": "developer error",
+    "statusCode": 0,
+    "success": false,
+    "result": {
+      "error": "Missing main/no code to execute."
+    }
+  },
+  "logs": null,
+  "annotations": null,
+  "date": "%START3%"
+}
+`,
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
-				fakeCmd := &exec.Cmd{
-					Stdout: config.Out,
-				}
+				buf := &bytes.Buffer{}
+				config.Out = buf
 
 				if tt.doctlArgs != "" {
 					config.Args = append(config.Args, tt.doctlArgs)
 				}
 
+				logs := false
+				result := false
 				if tt.doctlFlags != nil {
 					for k, v := range tt.doctlFlags {
+						if k == "logs" {
+							logs = true
+						}
+						if k == "result" {
+							result = true
+						}
 						if v == "" {
 							config.Doit.Set(config.NS, k, true)
 						} else {
@@ -95,12 +293,29 @@ func TestActivationsGet(t *testing.T) {
 					}
 				}
 
-				tm.serverless.EXPECT().CheckServerlessStatus().MinTimes(1).Return(nil)
-				tm.serverless.EXPECT().Cmd("activation/get", tt.expectedNimArgs).Return(fakeCmd, nil)
-				tm.serverless.EXPECT().Exec(fakeCmd).Return(do.ServerlessOutput{}, nil)
+				id := tt.doctlArgs
+				var activation whisk.Activation
+				if id != "" {
+					activation = findActivation(id)
+				}
+				if tt.listOptions.Limit > 0 {
+					fst := tt.listOptions.Skip
+					lnth := tt.listOptions.Limit + fst
+					tm.serverless.EXPECT().ListActivations(tt.listOptions).Return(theActivations[fst:lnth], nil)
+					activation = theActivations[fst]
+					id = activation.ActivationID
+				}
+				if logs {
+					tm.serverless.EXPECT().GetActivationLogs(id).Return(activation, nil)
+				} else if result {
+					tm.serverless.EXPECT().GetActivationResult(id).Return(activation.Response, nil)
+				} else {
+					tm.serverless.EXPECT().GetActivation(id).Return(activation, nil)
+				}
 
 				err := RunActivationsGet(config)
 				require.NoError(t, err)
+				assert.Equal(t, convertDates(tt.expectedOutput), buf.String())
 			})
 		})
 	}
@@ -108,65 +323,68 @@ func TestActivationsGet(t *testing.T) {
 
 func TestActivationsList(t *testing.T) {
 	tests := []struct {
-		name            string
-		doctlArgs       string
-		doctlFlags      map[string]string
-		expectedNimArgs []string
+		name       string
+		doctlArgs  string
+		doctlFlags map[string]string
 	}{
 		{
-			name:            "no flags or args",
-			expectedNimArgs: []string{},
-		},
-		{
-			name:            "full flag",
-			doctlFlags:      map[string]string{"full": ""},
-			expectedNimArgs: []string{"--full"},
-		},
-		{
-			name:            "count flag",
-			doctlFlags:      map[string]string{"count": ""},
-			expectedNimArgs: []string{"--count"},
-		},
-		{
-			name:            "limit flag",
-			doctlFlags:      map[string]string{"limit": "10"},
-			expectedNimArgs: []string{"--limit", "10"},
-		},
-		{
-			name:            "since flag",
-			doctlFlags:      map[string]string{"since": "1644866670085"},
-			expectedNimArgs: []string{"--since", "1644866670085"},
+			name:      "no flags or args",
+			doctlArgs: "",
 		},
 		{
-			name:            "skip flag",
-			doctlFlags:      map[string]string{"skip": "1"},
-			expectedNimArgs: []string{"--skip", "1"},
+			name:      "function name argument",
+			doctlArgs: "my-package/hello4",
 		},
 		{
-			name:            "upto flag",
-			doctlFlags:      map[string]string{"upto": "1644866670085"},
-			expectedNimArgs: []string{"--upto", "1644866670085"},
+			name:       "count flag",
+			doctlArgs:  "",
+			doctlFlags: map[string]string{"count": "true", "limit": "10"},
 		},
 		{
-			name:            "multiple flags",
-			doctlFlags:      map[string]string{"limit": "10", "count": ""},
-			expectedNimArgs: []string{"--count", "--limit", "10"},
+			name:       "multiple flags and arg",
+			doctlArgs:  "",
+			doctlFlags: map[string]string{"limit": "10", "skip": "100", "since": "1664538750000", "upto": "1664538850000"},
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
-				fakeCmd := &exec.Cmd{
-					Stdout: config.Out,
-				}
+				buf := &bytes.Buffer{}
+				config.Out = buf
 
 				if tt.doctlArgs != "" {
 					config.Args = append(config.Args, tt.doctlArgs)
 				}
 
+				count := false
+				var limit interface{}
+				var since interface{}
+				var upto interface{}
+				var skip interface{}
+
 				if tt.doctlFlags != nil {
 					for k, v := range tt.doctlFlags {
+						if k == "count" {
+							count = true
+						}
+
+						if k == "limit" {
+							limit, _ = strconv.ParseInt(v, 0, 64)
+						}
+
+						if k == "since" {
+							since, _ = strconv.ParseInt(v, 0, 64)
+						}
+
+						if k == "upto" {
+							upto, _ = strconv.ParseInt(v, 0, 64)
+						}
+
+						if k == "skip" {
+							skip, _ = strconv.ParseInt(v, 0, 64)
+						}
+
 						if v == "" {
 							config.Doit.Set(config.NS, k, true)
 						} else {
@@ -175,9 +393,39 @@ func TestActivationsList(t *testing.T) {
 					}
 				}
 
-				tm.serverless.EXPECT().CheckServerlessStatus().MinTimes(1).Return(nil)
-				tm.serverless.EXPECT().Cmd("activation/list", tt.expectedNimArgs).Return(fakeCmd, nil)
-				tm.serverless.EXPECT().Exec(fakeCmd).Return(do.ServerlessOutput{}, nil)
+				if count {
+					expectedListOptions := whisk.ActivationCountOptions{}
+					if since != nil {
+						expectedListOptions.Since = since.(int64)
+					}
+
+					if upto != nil {
+						expectedListOptions.Upto = upto.(int64)
+					}
+
+					tm.serverless.EXPECT().GetActivationCount(expectedListOptions).Return(theActivationCount, nil)
+				} else {
+					expectedListOptions := whisk.ActivationListOptions{}
+					if since != nil {
+						expectedListOptions.Since = since.(int64)
+					}
+
+					if upto != nil {
+						expectedListOptions.Upto = upto.(int64)
+					}
+
+					if len(config.Args) == 1 {
+						expectedListOptions.Name = config.Args[0]
+					}
+					if limit != nil {
+						expectedListOptions.Limit = int(limit.(int64))
+					}
+
+					if skip != nil {
+						expectedListOptions.Skip = int(skip.(int64))
+					}
+					tm.serverless.EXPECT().ListActivations(expectedListOptions).Return(theActivations, nil)
+				}
 
 				err := RunActivationsList(config)
 				require.NoError(t, err)
@@ -276,48 +524,73 @@ func TestActivationsLogs(t *testing.T) {
 
 func TestActivationsResult(t *testing.T) {
 	tests := []struct {
-		name            string
-		doctlArgs       string
-		doctlFlags      map[string]string
-		expectedNimArgs []string
+		name           string
+		doctlArgs      string
+		doctlFlags     map[string]string
+		listOptions    whisk.ActivationListOptions
+		expectedOutput string
 	}{
 		{
-			name:            "no flags or args",
-			expectedNimArgs: []string{},
-		},
-		{
-			name:            "no flags with ID",
-			doctlArgs:       "activationid",
-			expectedNimArgs: []string{"activationid"},
+			name:        "no flags or args",
+			listOptions: whisk.ActivationListOptions{Limit: 1},
+			expectedOutput: `=== activation-1 success %END1% hello1:0.0.1
+{
+  "body": "Hello stranger!"
+}
+`,
 		},
 		{
-			name:            "last flag",
-			doctlFlags:      map[string]string{"last": ""},
-			expectedNimArgs: []string{"--last"},
+			name:      "no flags with ID",
+			doctlArgs: "activation-2",
+			expectedOutput: `{
+  "body": "Hello Archie!"
+}
+`,
 		},
 		{
-			name:            "limit flag",
-			doctlFlags:      map[string]string{"limit": "10"},
-			expectedNimArgs: []string{"--limit", "10"},
+			name:        "limit flag",
+			doctlFlags:  map[string]string{"limit": "10"},
+			listOptions: whisk.ActivationListOptions{Limit: 10},
+			expectedOutput: `=== activation-3 success %END3% hello3:0.0.3
+{
+  "error": "Missing main/no code to execute."
+}
+=== activation-2 success %END2% hello2:0.0.2
+{
+  "body": "Hello Archie!"
+}
+=== activation-1 success %END1% hello1:0.0.1
+{
+  "body": "Hello stranger!"
+}
+`,
 		},
 		{
-			name:            "quiet flag",
-			doctlFlags:      map[string]string{"quiet": ""},
-			expectedNimArgs: []string{"--quiet"},
+			name:        "quiet flag",
+			doctlFlags:  map[string]string{"quiet": ""},
+			listOptions: whisk.ActivationListOptions{Limit: 1},
+			expectedOutput: `{
+  "body": "Hello stranger!"
+}
+`,
 		},
 		{
-			name:            "skip flag",
-			doctlFlags:      map[string]string{"skip": "1"},
-			expectedNimArgs: []string{"--skip", "1"},
+			name:        "skip flag",
+			doctlFlags:  map[string]string{"skip": "1"},
+			listOptions: whisk.ActivationListOptions{Limit: 1, Skip: 1},
+			expectedOutput: `=== activation-2 success %END2% hello2:0.0.2
+{
+  "body": "Hello Archie!"
+}
+`,
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
-				fakeCmd := &exec.Cmd{
-					Stdout: config.Out,
-				}
+				buf := &bytes.Buffer{}
+				config.Out = buf
 
 				if tt.doctlArgs != "" {
 					config.Args = append(config.Args, tt.doctlArgs)
@@ -333,12 +606,35 @@ func TestActivationsResult(t *testing.T) {
 					}
 				}
 
-				tm.serverless.EXPECT().CheckServerlessStatus().MinTimes(1).Return(nil)
-				tm.serverless.EXPECT().Cmd("activation/result", tt.expectedNimArgs).Return(fakeCmd, nil)
-				tm.serverless.EXPECT().Exec(fakeCmd).Return(do.ServerlessOutput{}, nil)
-
+				var ids []string
+				var activations []whisk.Activation
+				if tt.doctlArgs != "" {
+					ids = []string{tt.doctlArgs}
+					activations = []whisk.Activation{findActivation(ids[0])}
+				}
+				limit := tt.listOptions.Limit
+				if limit > 0 {
+					if limit > len(theActivations) {
+						limit = len(theActivations)
+					}
+					fst := tt.listOptions.Skip
+					lnth := limit + fst
+					// The command reverses the returned list in asking for the responses
+					chosen := theActivations[fst:lnth]
+					ids = make([]string, len(chosen))
+					activations = make([]whisk.Activation, len(chosen))
+					for i, activation := range chosen {
+						activations[len(chosen)-i-1] = activation
+						ids[len(chosen)-i-1] = activation.ActivationID
+					}
+					tm.serverless.EXPECT().ListActivations(tt.listOptions).Return(chosen, nil)
+				}
+				for i, id := range ids {
+					tm.serverless.EXPECT().GetActivationResult(id).Return(activations[i].Response, nil)
+				}
 				err := RunActivationsResult(config)
 				require.NoError(t, err)
+				assert.Equal(t, convertDates(tt.expectedOutput), buf.String())
 			})
 		})
 	}
diff --git a/commands/functions_test.go b/commands/functions_test.go
index 1d2d98d2..eb86d05f 100644
--- a/commands/functions_test.go
+++ b/commands/functions_test.go
@@ -16,9 +16,10 @@ package commands
 import (
 	"bytes"
 	"os"
-	"os/exec"
 	"sort"
+	"strings"
 	"testing"
+	"time"
 
 	"github.com/apache/openwhisk-client-go/whisk"
 	"github.com/digitalocean/doctl/do"
@@ -250,54 +251,135 @@ func TestFunctionsInvoke(t *testing.T) {
 }
 
 func TestFunctionsList(t *testing.T) {
+	// The displayer for function list is time-zone sensitive so we need to pre-convert the timestamps using the local
+	// time-zone to get exact matches.
+	timestamps := []int64{1664538810000, 1664538820000, 1664538830000}
+	symbols := []string{"%DATE1%", "%DATE2%", "%DATE3%"}
+	dates := []string{
+		time.UnixMilli(timestamps[0]).Format("01/02 03:04:05"),
+		time.UnixMilli(timestamps[1]).Format("01/02 03:04:05"),
+		time.UnixMilli(timestamps[2]).Format("01/02 03:04:05"),
+	}
+
 	tests := []struct {
-		name            string
-		doctlArgs       string
-		doctlFlags      map[string]string
-		expectedNimArgs []string
+		name           string
+		doctlFlags     map[string]string
+		doctlArg       string
+		skip           int
+		limit          int
+		expectedOutput string
 	}{
 		{
-			name:            "no flags or args",
-			expectedNimArgs: []string{"--json"},
+			name:  "no flags or args",
+			skip:  0,
+			limit: 0,
+			expectedOutput: `%DATE1%    0.0.1    nodejs:14    daily/hello
+%DATE2%    0.0.2    nodejs:14    daily/goodbye
+%DATE3%    0.0.3    nodejs:14    sometimes/meAgain
+`,
+		},
+		{
+			name:     "with package arg",
+			doctlArg: "daily",
+			skip:     0,
+			limit:    0,
+			expectedOutput: `%DATE1%    0.0.1    nodejs:14    daily/hello
+%DATE2%    0.0.2    nodejs:14    daily/goodbye
+`,
+		},
+		{
+			name:           "count flag",
+			doctlFlags:     map[string]string{"count": ""},
+			skip:           0,
+			limit:          0,
+			expectedOutput: "There are 3 functions in this namespace.\n",
+		},
+		{
+			name:           "limit flag",
+			doctlFlags:     map[string]string{"limit": "1"},
+			skip:           0,
+			limit:          1,
+			expectedOutput: "%DATE1%    0.0.1    nodejs:14    daily/hello\n",
+		},
+		{
+			name:       "name flag",
+			doctlFlags: map[string]string{"name": ""},
+			skip:       0,
+			limit:      0,
+			expectedOutput: `%DATE2%    0.0.2    nodejs:14    daily/goodbye
+%DATE1%    0.0.1    nodejs:14    daily/hello
+%DATE3%    0.0.3    nodejs:14    sometimes/meAgain
+`,
 		},
 		{
-			name:            "count flag",
-			doctlFlags:      map[string]string{"count": ""},
-			expectedNimArgs: []string{"--count"},
+			name:       "name-sort flag",
+			doctlFlags: map[string]string{"name-sort": ""},
+			skip:       0,
+			limit:      0,
+			expectedOutput: `%DATE2%    0.0.2    nodejs:14    daily/goodbye
+%DATE1%    0.0.1    nodejs:14    daily/hello
+%DATE3%    0.0.3    nodejs:14    sometimes/meAgain
+`,
 		},
 		{
-			name:            "limit flag",
-			doctlFlags:      map[string]string{"limit": "1"},
-			expectedNimArgs: []string{"--json", "--limit", "1"},
+			name:       "skip flag",
+			doctlFlags: map[string]string{"skip": "1"},
+			skip:       1,
+			limit:      0,
+			expectedOutput: `%DATE2%    0.0.2    nodejs:14    daily/goodbye
+%DATE3%    0.0.3    nodejs:14    sometimes/meAgain
+`,
 		},
+	}
+
+	theList := []whisk.Action{
 		{
-			name:            "name flag",
-			doctlFlags:      map[string]string{"name": ""},
-			expectedNimArgs: []string{"--name", "--json"},
+			Name:      "hello",
+			Namespace: "theNamespace/daily",
+			Updated:   timestamps[0],
+			Version:   "0.0.1",
+			Annotations: whisk.KeyValueArr{
+				whisk.KeyValue{
+					Key:   "exec",
+					Value: "nodejs:14",
+				},
+			},
 		},
 		{
-			name:            "name-sort flag",
-			doctlFlags:      map[string]string{"name-sort": ""},
-			expectedNimArgs: []string{"--name-sort", "--json"},
+			Name:      "goodbye",
+			Namespace: "theNamespace/daily",
+			Updated:   timestamps[1],
+			Version:   "0.0.2",
+			Annotations: whisk.KeyValueArr{
+				whisk.KeyValue{
+					Key:   "exec",
+					Value: "nodejs:14",
+				},
+			},
 		},
 		{
-			name:            "skip flag",
-			doctlFlags:      map[string]string{"skip": "1"},
-			expectedNimArgs: []string{"--json", "--skip", "1"},
+			Name:      "meAgain",
+			Namespace: "theNamespace/sometimes",
+			Version:   "0.0.3",
+			Updated:   timestamps[2],
+			Annotations: whisk.KeyValueArr{
+				whisk.KeyValue{
+					Key:   "exec",
+					Value: "nodejs:14",
+				},
+			},
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
-				fakeCmd := &exec.Cmd{
-					Stdout: config.Out,
-				}
+				buf := &bytes.Buffer{}
+				config.Out = buf
 
-				if tt.doctlArgs != "" {
-					config.Args = append(config.Args, tt.doctlArgs)
+				if tt.doctlArg != "" {
+					config.Args = append(config.Args, tt.doctlArg)
 				}
-
 				if tt.doctlFlags != nil {
 					for k, v := range tt.doctlFlags {
 						if v == "" {
@@ -307,14 +389,40 @@ func TestFunctionsList(t *testing.T) {
 						}
 					}
 				}
+				config.Doit.Set(config.NS, "no-header", true)
 
-				tm.serverless.EXPECT().CheckServerlessStatus().MinTimes(1).Return(nil)
-				tm.serverless.EXPECT().Cmd("action/list", tt.expectedNimArgs).Return(fakeCmd, nil)
-				tm.serverless.EXPECT().Exec(fakeCmd).Return(do.ServerlessOutput{}, nil)
+				answer := selectPackage(theList, tt.doctlArg)[tt.skip:]
+				if tt.limit != 0 {
+					answer = answer[0:tt.limit]
+				}
+				tm.serverless.EXPECT().ListFunctions(tt.doctlArg, tt.skip, tt.limit).Return(answer, nil)
 
 				err := RunFunctionsList(config)
 				require.NoError(t, err)
+				expected := tt.expectedOutput
+				for i := range symbols {
+					expected = strings.Replace(expected, symbols[i], dates[i], 1)
+				}
+				assert.Equal(t, expected, buf.String())
 			})
 		})
 	}
 }
+
+// selectPackage is a testing support utility to trim a master list of functions by package membership
+// Also ensures the array is copied, because the logic being tested may sort it in place.
+func selectPackage(masterList []whisk.Action, pkg string) []whisk.Action {
+	if pkg == "" {
+		copiedList := make([]whisk.Action, len(masterList))
+		copy(copiedList, masterList)
+		return copiedList
+	}
+	namespace := "theNamespace/" + pkg
+	answer := []whisk.Action{}
+	for _, action := range masterList {
+		if action.Namespace == namespace {
+			answer = append(answer, action)
+		}
+	}
+	return answer
+}
diff --git a/commands/serverless_test.go b/commands/serverless_test.go
index 80f66412..baa352d4 100644
--- a/commands/serverless_test.go
+++ b/commands/serverless_test.go
@@ -127,18 +127,20 @@ func TestServerlessStatusWhenConnected(t *testing.T) {
 	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
 		buf := &bytes.Buffer{}
 		config.Out = buf
-		fakeCmd := &exec.Cmd{
-			Stdout: config.Out,
-		}
 
 		tm.serverless.EXPECT().CheckServerlessStatus().MinTimes(1).Return(nil)
-		tm.serverless.EXPECT().Cmd("auth/current", []string{"--apihost", "--name"}).Return(fakeCmd, nil)
-		tm.serverless.EXPECT().Exec(fakeCmd).Return(do.ServerlessOutput{
-			Entity: map[string]interface{}{
-				"name":    "hello",
-				"apihost": "https://api.example.com",
+		tm.serverless.EXPECT().ReadCredentials().Return(do.ServerlessCredentials{
+			APIHost:   "https://api.example.com",
+			Namespace: "hello",
+			Credentials: map[string]map[string]do.ServerlessCredential{
+				"https://api.example.com": {
+					"hello": do.ServerlessCredential{
+						Auth: "here-are-some-credentials",
+					},
+				},
 			},
 		}, nil)
+		tm.serverless.EXPECT().GetNamespaceFromCluster("https://api.example.com", "here-are-some-credentials").Return("hello", nil)
 
 		err := RunServerlessStatus(config)
 		require.NoError(t, err)
@@ -151,9 +153,6 @@ func TestServerlessStatusWithLanguages(t *testing.T) {
 		buf := &bytes.Buffer{}
 		config.Out = buf
 		config.Doit.Set(config.NS, "languages", true)
-		fakeCmd := &exec.Cmd{
-			Stdout: config.Out,
-		}
 		fakeHostInfo := do.ServerlessHostInfo{
 			Runtimes: map[string][]do.ServerlessRuntime{
 				"go": {
@@ -184,14 +183,20 @@ func TestServerlessStatusWithLanguages(t *testing.T) {
 `
 
 		tm.serverless.EXPECT().CheckServerlessStatus().MinTimes(1).Return(nil)
-		tm.serverless.EXPECT().Cmd("auth/current", []string{"--apihost", "--name"}).Return(fakeCmd, nil)
-		tm.serverless.EXPECT().Exec(fakeCmd).Return(do.ServerlessOutput{
-			Entity: map[string]interface{}{
-				"name":    "hello",
-				"apihost": "https://api.example.com",
+		tm.serverless.EXPECT().ReadCredentials().Return(do.ServerlessCredentials{
+			APIHost:   "https://api.example.com",
+			Namespace: "hello",
+			Credentials: map[string]map[string]do.ServerlessCredential{
+				"https://api.example.com": {
+					"hello": do.ServerlessCredential{
+						Auth: "here-are-some-credentials",
+					},
+				},
 			},
 		}, nil)
+		tm.serverless.EXPECT().GetNamespaceFromCluster("https://api.example.com", "here-are-some-credentials").Return("hello", nil)
 		tm.serverless.EXPECT().GetHostInfo("https://api.example.com").Return(fakeHostInfo, nil)
+
 		err := RunServerlessStatus(config)
 		require.NoError(t, err)
 		assert.Contains(t, buf.String(), expectedDisplay)
@@ -200,15 +205,20 @@ func TestServerlessStatusWithLanguages(t *testing.T) {
 
 func TestServerlessStatusWhenNotConnected(t *testing.T) {
 	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
-		fakeCmd := &exec.Cmd{
-			Stdout: config.Out,
-		}
 
 		tm.serverless.EXPECT().CheckServerlessStatus().MinTimes(1).Return(nil)
-		tm.serverless.EXPECT().Cmd("auth/current", []string{"--apihost", "--name"}).Return(fakeCmd, nil)
-		tm.serverless.EXPECT().Exec(fakeCmd).Return(do.ServerlessOutput{
-			Error: "403",
+		tm.serverless.EXPECT().ReadCredentials().Return(do.ServerlessCredentials{
+			APIHost:   "https://api.example.com",
+			Namespace: "hello",
+			Credentials: map[string]map[string]do.ServerlessCredential{
+				"https://api.example.com": {
+					"hello": do.ServerlessCredential{
+						Auth: "here-are-some-credentials",
+					},
+				},
+			},
 		}, nil)
+		tm.serverless.EXPECT().GetNamespaceFromCluster("https://api.example.com", "here-are-some-credentials").Return("not-hello", errors.New("an error"))
 
 		err := RunServerlessStatus(config)
 		require.Error(t, err)