diff --git a/.woodpecker/build-binary.yml b/.woodpecker/build-binary.yml
new file mode 100644
index 000000000..1ab037ced
--- /dev/null
+++ b/.woodpecker/build-binary.yml
@@ -0,0 +1,52 @@
+labels:
+  type: exec
+  platform: linux/amd64
+
+steps:
+- name: build-binary
+  image: bash
+  commands:
+  - sed -i "s#canary#$CI_COMMIT_TAG#g" version/version.go
+  - VERSION=$CI_COMMIT_TAG make build-binary
+  environment:
+    CODENAME:
+      from_secret: codename
+    DEV_REGISTRY:
+      from_secret: dev_registry
+    DRYCC_REGISTRY:
+      from_secret: drycc_registry
+    CONTAINER_USERNAME:
+      from_secret: container_username
+    CONTAINER_PASSWORD:
+      from_secret: container_password
+  when:
+    event:
+    - push
+    - tag
+
+- name: publish-binary
+  image: bash
+  commands:
+  - podman run --rm
+      -e CI_FORGE_TYPE=github
+      -e CI_PIPELINE_EVENT=tag
+      -e CI_REPO_OWNER="$CI_REPO_OWNER"
+      -e CI_REPO_NAME="$CI_REPO_NAME"
+      -e CI_COMMIT_REF="refs/tags/$CI_COMMIT_TAG"
+      -e PLUGIN_API_KEY="$GITHUB_TOKEN"
+      -e PLUGIN_BASE_URL="https://api.github.com/"
+      -e PLUGIN_UPLOAD_URL="https://uploads.github.com/"
+      -e PLUGIN_OVERWRITE="true"
+      -e PLUGIN_FILES="_dist/*"
+      -v $(pwd):$(pwd)
+      -w $(pwd)
+      docker.io/woodpeckerci/plugin-release
+  environment:
+    GITHUB_TOKEN:
+      from_secret: github_token
+  when:
+    event:
+    - tag
+
+depends_on:
+- test-linux
diff --git a/.woodpecker/build-linux.yml b/.woodpecker/build-linux.yml
index 3d8fbc112..ef4ecfda6 100644
--- a/.woodpecker/build-linux.yml
+++ b/.woodpecker/build-linux.yml
@@ -1,44 +1,35 @@
+matrix:
+  platform:
+    - linux/amd64
+    - linux/arm64
+
 labels:
   type: exec
-  platform: linux/amd64
+  platform: ${platform}
 
 steps:
-- name: build-linux
-  image: bash
-  commands:
-  - sed -i "s#canary#$CI_COMMIT_TAG#g" version/version.go
-  - VERSION=$CI_COMMIT_TAG make build
-  secrets:
-  - dev_registry
-  - drycc_registry
-  - container_username
-  - container_password
-  when:
-    event:
-    - push
-    - tag
-
 - name: publish-linux
   image: bash
   commands:
-  - podman run --rm
-      -e DRONE_BUILD_EVENT=tag
-      -e DRONE_REPO_OWNER="$CI_REPO_OWNER"
-      -e DRONE_REPO_NAME="$CI_REPO_NAME"
-      -e PLUGIN_API_KEY="$GITHUB_TOKEN"
-      -e PLUGIN_BASE_URL="https://api.github.com/"
-      -e PLUGIN_UPLOAD_URL="https://uploads.github.com/"
-      -e DRONE_COMMIT_REF="refs/tags/$CI_COMMIT_TAG"
-      -e PLUGIN_OVERWRITE="true"
-      -e PLUGIN_FILES="_dist/*"
-      -v $(pwd):$(pwd)
-      -w $(pwd)
-      docker.io/plugins/github-release
-  secrets:
-  - github_token
+  - export VERSION=$([ -z $CI_COMMIT_TAG ] && echo latest || echo $CI_COMMIT_TAG)-$(sed 's#/#-#g' <<< $CI_SYSTEM_PLATFORM)
+  - echo $CONTAINER_PASSWORD | podman login $DRYCC_REGISTRY --username $CONTAINER_USERNAME --password-stdin > /dev/null 2>&1
+  - make podman-build podman-immutable-push
+  environment:
+    CODENAME:
+      from_secret: codename
+    DEV_REGISTRY:
+      from_secret: dev_registry
+    DRYCC_REGISTRY:
+      from_secret: drycc_registry
+    CONTAINER_USERNAME:
+      from_secret: container_username
+    CONTAINER_PASSWORD:
+      from_secret: container_password
   when:
     event:
+    - push
     - tag
+    - cron
 
 depends_on:
-- test-linux
+- test-linux
\ No newline at end of file
diff --git a/.woodpecker/manifest.tmpl b/.woodpecker/manifest.tmpl
new file mode 100644
index 000000000..d02b12d82
--- /dev/null
+++ b/.woodpecker/manifest.tmpl
@@ -0,0 +1,18 @@
+image: registry.drycc.cc/drycc/{{project}}:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}canary{{/if}}
+{{#if build.tags}}
+tags:
+{{#each build.tags}}
+  - {{this}}
+{{/each}}
+{{/if}}
+manifests:
+  -
+    image: registry.drycc.cc/drycc/{{project}}:{{#if build.tag}}{{build.tag}}-{{else}}latest-{{/if}}linux-amd64
+    platform:
+      architecture: amd64
+      os: linux
+  -
+    image: registry.drycc.cc/drycc/{{project}}:{{#if build.tag}}{{build.tag}}-{{else}}latest-{{/if}}linux-arm64
+    platform:
+      architecture: arm64
+      os: linux
\ No newline at end of file
diff --git a/.woodpecker/manifest.yml b/.woodpecker/manifest.yml
new file mode 100644
index 000000000..9c1f96de1
--- /dev/null
+++ b/.woodpecker/manifest.yml
@@ -0,0 +1,43 @@
+labels:
+  type: exec
+  platform: linux/amd64
+
+steps:
+- name: generate-manifest
+  image: bash
+  commands:
+  - sed -i "s/{{project}}/$${CI_REPO_NAME}/g" .woodpecker/manifest.tmpl
+  - sed -i "s/registry.drycc.cc/$${DRYCC_REGISTRY}/g" .woodpecker/manifest.tmpl
+  environment:
+    DRYCC_REGISTRY:
+      from_secret: drycc_registry
+  when:
+    event:
+    - tag
+    - push
+    - cron
+
+- name: publish-manifest
+  image: bash
+  commands:
+  - podman run --rm
+    -e PLUGIN_SPEC=.woodpecker/manifest.tmpl
+    -e PLUGIN_USERNAME=$CONTAINER_USERNAME
+    -e PLUGIN_PASSWORD=$CONTAINER_PASSWORD
+    -e DRONE_TAG=$CI_COMMIT_TAG
+    -v $(pwd):$(pwd)
+    -w $(pwd)
+    docker.io/plugins/manifest
+  environment:
+    CONTAINER_USERNAME:
+      from_secret: container_username
+    CONTAINER_PASSWORD:
+      from_secret: container_password
+  when:
+    event:
+    - tag
+    - push
+    - cron
+
+depends_on:
+- build-linux
\ No newline at end of file
diff --git a/.woodpecker/test-linux.yml b/.woodpecker/test-linux.yml
index 467664782..edae26804 100644
--- a/.woodpecker/test-linux.yml
+++ b/.woodpecker/test-linux.yml
@@ -12,8 +12,11 @@ steps:
   image: bash
   commands:
   - make test
-  secrets:
-  - dev_registry
+  environment:
+    DEV_REGISTRY:
+      from_secret: dev_registry
+    CODECOV_TOKEN:
+      from_secret: codecov_token
   when:
     event:
     - push
diff --git a/Makefile b/Makefile
index c22cd8be9..d218bc7b1 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,8 @@
-# the filepath to this repository, relative to $GOPATH/src
+SHORT_NAME ?= workflow-cli
+DRYCC_REGISTRY ?= ${DEV_REGISTRY}
+
+include versioning.mk
+
 VERSION ?= canary
 REPO_PATH := github.com/drycc/workflow-cli
 DEV_ENV_IMAGE := ${DEV_REGISTRY}/drycc/go-dev
@@ -6,20 +10,25 @@ DEV_ENV_WORK_DIR := /opt/drycc/go/src/${REPO_PATH}
 
 DIST_DIR ?= _dist
 
-DEV_ENV_CMD := podman run --rm -v ${CURDIR}:${DEV_ENV_WORK_DIR} -w ${DEV_ENV_WORK_DIR} ${DEV_ENV_IMAGE}
+DEV_ENV_CMD := podman run --rm -v ${CURDIR}:${DEV_ENV_WORK_DIR} -w ${DEV_ENV_WORK_DIR} -e CODECOV_TOKEN=${CODECOV_TOKEN} ${DEV_ENV_IMAGE}
 
 bootstrap:
 	${DEV_ENV_CMD} go mod vendor
 
 # This is supposed to be run within a container
-build:
+build-binary:
+	${DEV_ENV_CMD} scripts/update-translations.sh -g
 	${DEV_ENV_CMD} scripts/build ${VERSION}
 
+podman-build:
+	podman build . --build-arg VERSION=${VERSION} --build-arg CODENAME=${CODENAME} -f docker/Dockerfile -t ${IMAGE}
+	podman tag ${IMAGE} ${MUTABLE_IMAGE}
+
 test-style:
 	${DEV_ENV_CMD} lint
 
 test-cover:
 	${DEV_ENV_CMD} test-cover.sh
 
-test: build test-style test-cover
+test: build-binary test-style test-cover
 	${DEV_ENV_CMD} go test -race -cover -coverprofile=coverage.txt -covermode=atomic ./...
diff --git a/README.md b/README.md
index 1023a61b6..b6b0a62a4 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,6 @@
 [![Go Report Card](https://goreportcard.com/badge/github.com/drycc/workflow-cli)](https://goreportcard.com/report/github.com/drycc/workflow-cli)
 [![codebeat badge](https://codebeat.co/badges/b609cb7f-7b42-4214-8787-09298f553176)](https://codebeat.co/projects/github-com-drycc-workflow-cli-main)
 [![codecov](https://codecov.io/gh/drycc/workflow-cli/branch/main/graph/badge.svg)](https://codecov.io/gh/drycc/workflow-cli)
-[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fdrycc%2Fworkflow-cli.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fdrycc%2Fworkflow-cli?ref=badge_shield)
 
 Download Links: https://github.com/drycc/workflow-cli/releases
 
@@ -69,5 +68,3 @@ see [LICENSE](https://github.com/drycc/workflow-cli/blob/main/LICENSE)
 [Drycc website]: http://drycc.com/
 [blog]: https://blog.drycc.info/blog/
 [slack community]: https://slack.drycc.com/
-
-[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fdrycc%2Fworkflow-cli.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fdrycc%2Fworkflow-cli?ref=badge_large)
diff --git a/cli/cli.go b/cli/cli.go
deleted file mode 100644
index 24f59394c..000000000
--- a/cli/cli.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package cli
-
-// Shortcuts is a map of all the shortcuts supported by the CLI
-var Shortcuts = map[string]string{
-	"create":         "apps:create",
-	"destroy":        "apps:destroy",
-	"info":           "apps:info",
-	"login":          "auth:login",
-	"logout":         "auth:logout",
-	"logs":           "apps:logs",
-	"open":           "apps:open",
-	"pull":           "builds:create",
-	"rollback":       "releases:rollback",
-	"run":            "apps:run",
-	"scale":          "ps:scale",
-	"sharing":        "perms:list",
-	"sharing:list":   "perms:list",
-	"sharing:add":    "perms:create",
-	"sharing:remove": "perms:delete",
-	"whoami":         "auth:whoami",
-}
diff --git a/cmd/builds.go b/cmd/builds.go
deleted file mode 100644
index 7a1bc5196..000000000
--- a/cmd/builds.go
+++ /dev/null
@@ -1,99 +0,0 @@
-package cmd
-
-import (
-	"fmt"
-	"os"
-
-	yaml "gopkg.in/yaml.v3"
-
-	"github.com/drycc/controller-sdk-go/builds"
-)
-
-// BuildsList lists an app's builds.
-func (d *DryccCmd) BuildsList(appID string, results int) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
-	if err != nil {
-		return err
-	}
-
-	if results == defaultLimit {
-		results = s.Limit
-	}
-
-	builds, count, err := builds.List(s.Client, appID, results)
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-	if count > 0 {
-		table := d.getDefaultFormatTable([]string{"UUID", "OWNER", "SHA", "CREATED"})
-		for _, build := range builds {
-			table.Append([]string{
-				build.UUID,
-				safeGetString(build.Owner),
-				safeGetString(build.Sha),
-				build.Created,
-			})
-		}
-		table.Render()
-	} else {
-		d.Println(fmt.Sprintf("No builds found in %s app.", appID))
-	}
-	return nil
-}
-
-// BuildsCreate creates a build for an app.
-func (d *DryccCmd) BuildsCreate(appID, image, stack, procfile string, dryccfile string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
-	if err != nil {
-		return err
-	}
-
-	procfileMap := make(map[string]string)
-	if _, err := os.Stat(procfile); err == nil {
-		contents, err := os.ReadFile(procfile)
-		if err != nil {
-			return err
-		}
-
-		if procfileMap, err = parseProcfile(contents); err != nil {
-			return err
-		}
-	}
-
-	dryccfileMap := make(map[string]interface{})
-	if _, err := os.Stat(dryccfile); err == nil {
-		contents, err := os.ReadFile(dryccfile)
-		if err != nil {
-			return err
-		}
-
-		if dryccfileMap, err = parseDryccfile(contents); err != nil {
-			return err
-		}
-	}
-
-	d.Print("Creating build... ")
-	quit := progress(d.WOut)
-	_, err = builds.New(s.Client, appID, image, stack, procfileMap, dryccfileMap)
-	quit <- true
-	<-quit
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-
-	d.Println("done")
-
-	return nil
-}
-
-func parseProcfile(procfile []byte) (map[string]string, error) {
-	procfileMap := make(map[string]string)
-	return procfileMap, yaml.Unmarshal(procfile, &procfileMap)
-}
-
-func parseDryccfile(dryccfile []byte) (map[string]interface{}, error) {
-	dryccfileMap := make(map[string]interface{})
-	return dryccfileMap, yaml.Unmarshal(dryccfile, &dryccfileMap)
-}
diff --git a/cmd/builds_test.go b/cmd/builds_test.go
deleted file mode 100644
index 6dcd3a741..000000000
--- a/cmd/builds_test.go
+++ /dev/null
@@ -1,229 +0,0 @@
-package cmd
-
-import (
-	"bytes"
-	"fmt"
-	"net/http"
-	"os"
-	"testing"
-
-	"github.com/drycc/controller-sdk-go/api"
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-func TestParseProcfile(t *testing.T) {
-	t.Parallel()
-
-	procMap, err := parseProcfile([]byte(`web: ./test
-foo: test --test
-`))
-	assert.NoError(t, err)
-	assert.Equal(t, procMap, map[string]string{"web": "./test", "foo": "test --test"}, "map")
-
-	_, err = parseProcfile([]byte(`web: ./test
-foo
-`))
-	assert.NotEqual(t, err, nil, "yaml")
-}
-
-func TestBuildsList(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	server.Mux.HandleFunc("/v2/apps/foo/builds/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-		fmt.Fprintf(w, `{
-			"count": 2,
-			"next": null,
-			"previous": null,
-			"results": [
-				{
-					"app": "",
-					"created": "2014-01-01T00:00:00UTC",
-					"dockerfile": "",
-					"image": "",
-					"owner": "",
-					"procfile": {},
-					"sha": "",
-					"updated": "",
-					"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
-				},
-				{
-					"app": "",
-					"created": "2014-01-05T00:00:00UTC",
-					"dockerfile": "",
-					"image": "",
-					"owner": "",
-					"procfile": {},
-					"sha": "",
-					"updated": "",
-					"uuid": "c4aed81c-d1ca-4ff1-ab89-d2151264e1a3"
-				}
-			]
-		}`)
-	})
-
-	err = cmdr.BuildsList("foo", -1)
-	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `UUID                                    OWNER     SHA       CREATED                
-de1bf5b5-4a72-4f94-a10c-d2a3741cdf75    <none>    <none>    2014-01-01T00:00:00UTC    
-c4aed81c-d1ca-4ff1-ab89-d2151264e1a3    <none>    <none>    2014-01-05T00:00:00UTC    
-`, "output")
-}
-
-func TestBuildsListLimit(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	server.Mux.HandleFunc("/v2/apps/foo/builds/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-		fmt.Fprintf(w, `{
-            "count": 2,
-            "next": null,
-            "previous": null,
-            "results": [
-                {
-                    "app": "foo",
-                    "created": "2014-01-01T00:00:00UTC",
-                    "dockerfile": "",
-                    "image": "",
-                    "owner": "",
-                    "procfile": {},
-                    "sha": "",
-                    "updated": "",
-                    "uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
-                }
-            ]
-        }`)
-	})
-
-	err = cmdr.BuildsList("foo", 1)
-	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `UUID                                    OWNER     SHA       CREATED                
-de1bf5b5-4a72-4f94-a10c-d2a3741cdf75    <none>    <none>    2014-01-01T00:00:00UTC    
-`, "output")
-}
-
-func TestBuildsCreate(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	// Create a new temporary directory and change to it.
-	name, err := os.MkdirTemp("", "client")
-	assert.NoError(t, err)
-	err = os.Chdir(name)
-	assert.NoError(t, err)
-
-	server.Mux.HandleFunc("/v2/apps/enterprise/builds/", func(w http.ResponseWriter, r *http.Request) {
-		testutil.SetHeaders(w)
-		testutil.AssertBody(t, api.CreateBuildRequest{
-			Image: "ncc/1701:A",
-			Stack: "container",
-		}, r)
-		w.WriteHeader(http.StatusCreated)
-		fmt.Fprintf(w, "{}")
-	})
-
-	err = cmdr.BuildsCreate("enterprise", "ncc/1701:A", "container", "", "")
-	assert.NoError(t, err)
-	assert.Equal(t, testutil.StripProgress(b.String()), "Creating build... done\n", "output")
-
-	server.Mux.HandleFunc("/v2/apps/bradbury/builds/", func(w http.ResponseWriter, r *http.Request) {
-		testutil.SetHeaders(w)
-		testutil.AssertBody(t, api.CreateBuildRequest{
-			Image: "nx/72307:latest",
-			Stack: "container",
-			Procfile: map[string]string{
-				"web":  "./drive",
-				"warp": "./warp 8",
-			},
-		}, r)
-
-		w.WriteHeader(http.StatusCreated)
-		fmt.Fprintf(w, "{}")
-	})
-	b.Reset()
-
-	tmpDir, err := os.MkdirTemp("", "tmpdir")
-	if err != nil {
-		t.Fatalf("error creating temp directory (%s)", err)
-	}
-	data := `web: ./drive
-warp: ./warp 8`
-	if err := os.WriteFile(tmpDir+"/Procfile", []byte(data), 0644); err != nil {
-		t.Fatalf("error creating %s/Procfile (%s)", tmpDir, err)
-	}
-	defer func() {
-		if err := os.RemoveAll(tmpDir); err != nil {
-			t.Fatalf("failed to remove Procfile from %s (%s)", tmpDir, err)
-		}
-	}()
-
-	err = cmdr.BuildsCreate("bradbury", "nx/72307:latest", "container", tmpDir+"/Procfile", "")
-	assert.NoError(t, err)
-	assert.Equal(t, testutil.StripProgress(b.String()), "Creating build... done\n", "output")
-
-	server.Mux.HandleFunc("/v2/apps/franklin/builds/", func(w http.ResponseWriter, r *http.Request) {
-		testutil.SetHeaders(w)
-		testutil.AssertBody(t, api.CreateBuildRequest{
-			Image: "nx/326:latest",
-			Stack: "container",
-			Procfile: map[string]string{
-				"web":  "./drive",
-				"warp": "./warp 8",
-			},
-			Dryccfile: map[string]interface{}{
-				"deploy": map[string]interface{}{
-					"web": map[string]interface{}{
-						"command": []string{"bash", "-c"},
-						"args":    []string{"bundle exec puma -C config/puma.rb"},
-					},
-				},
-			},
-		}, r)
-
-		w.WriteHeader(http.StatusCreated)
-		fmt.Fprintf(w, "{}")
-	})
-	b.Reset()
-
-	err = os.WriteFile("Procfile", []byte(`web: ./drive
-warp: ./warp 8
-`), os.ModePerm)
-	assert.NoError(t, err)
-
-	err = os.WriteFile("drycc.yaml", []byte(`
-deploy:
-  web:
-    command:
-    - bash
-    - -c
-    args:
-    - bundle exec puma -C config/puma.rb
-`), os.ModePerm)
-	assert.NoError(t, err)
-
-	err = cmdr.BuildsCreate("franklin", "nx/326:latest", "container", "Procfile", "drycc.yaml")
-	assert.NoError(t, err)
-	assert.Equal(t, testutil.StripProgress(b.String()), "Creating build... done\n", "output")
-
-}
diff --git a/cmd/canary.go b/cmd/canary.go
deleted file mode 100644
index 1e5f06fda..000000000
--- a/cmd/canary.go
+++ /dev/null
@@ -1,138 +0,0 @@
-package cmd
-
-import (
-	"fmt"
-	"strings"
-
-	"github.com/drycc/controller-sdk-go/api"
-	"github.com/drycc/controller-sdk-go/appsettings"
-)
-
-// CanaryList tells the informations about app's autoscale status
-func (d *DryccCmd) CanaryInfo(appID string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
-	if err != nil {
-		return err
-	}
-
-	appSettings, err := appsettings.List(s.Client, appID)
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-	if len(appSettings.Canaries) > 0 {
-		table := d.getDefaultFormatTable([]string{"UUID", "OWNER", "PTYPE", "CREATED", "UPDATED"})
-		for _, procType := range appSettings.Canaries {
-			table.Append([]string{
-				appSettings.UUID,
-				appSettings.Owner,
-				procType,
-				appSettings.Created,
-				appSettings.Updated,
-			})
-		}
-		table.Render()
-	} else {
-		d.Println(fmt.Sprintf("No canaries found in %s app.", appID))
-	}
-	return nil
-}
-
-// CanaryCreate sets canary options for the app proc type.
-func (d *DryccCmd) CanaryCreate(appID string, processType []string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
-	if err != nil {
-		return err
-	}
-
-	d.Printf("Applying canary settings for process type %s on %s... ", strings.Join(processType, ","), appID)
-
-	quit := progress(d.WOut)
-	_, err = appsettings.Set(s.Client, appID, api.AppSettings{Canaries: processType})
-
-	quit <- true
-	<-quit
-
-	if err != nil {
-		return err
-	}
-
-	d.Println("done")
-	return nil
-}
-
-// CanaryRemove remove canary for the app proc type.
-func (d *DryccCmd) CanaryRemove(appID string, processType []string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
-	if err != nil {
-		return err
-	}
-
-	d.Printf("Removing canary for process type %s on %s... ", strings.Join(processType, ","), appID)
-
-	quit := progress(d.WOut)
-
-	err = appsettings.CanaryRemove(s.Client, appID, api.AppSettings{Canaries: processType})
-
-	quit <- true
-	<-quit
-
-	if err != nil {
-		return err
-	}
-
-	d.Println("done")
-	return nil
-}
-
-// CanaryRelease release canary for the app.
-func (d *DryccCmd) CanaryRelease(appID string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
-	if err != nil {
-		return err
-	}
-
-	d.Printf("Release canary for %s... ", appID)
-
-	quit := progress(d.WOut)
-
-	err = appsettings.CanaryRelease(s.Client, appID)
-
-	quit <- true
-	<-quit
-
-	if err != nil {
-		return err
-	}
-
-	d.Println("done")
-	return nil
-}
-
-// CanaryRollback rollback canary for the app.
-func (d *DryccCmd) CanaryRollback(appID string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
-	if err != nil {
-		return err
-	}
-
-	d.Printf("Rollback canary for %s... ", appID)
-
-	quit := progress(d.WOut)
-
-	err = appsettings.CanaryRollback(s.Client, appID)
-
-	quit <- true
-	<-quit
-
-	if err != nil {
-		return err
-	}
-
-	d.Println("done")
-	return nil
-}
diff --git a/cmd/canary_test.go b/cmd/canary_test.go
deleted file mode 100644
index 8b43c6c4e..000000000
--- a/cmd/canary_test.go
+++ /dev/null
@@ -1,141 +0,0 @@
-package cmd
-
-import (
-	"bytes"
-	"fmt"
-	"net/http"
-	"testing"
-
-	"github.com/drycc/controller-sdk-go/api"
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-func TestCanaryInfo(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	server.Mux.HandleFunc("/v2/apps/rivendell/settings/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-		fmt.Fprintf(w, `{
-			"owner": "elrond",
-			"app": "rivendell",
-			"canaries": ["cmd"],
-			"created": "2014-01-01T00:00:00UTC",
-			"updated": "2014-01-01T00:00:00UTC",
-			"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
-		}`)
-	})
-
-	err = cmdr.CanaryInfo("rivendell")
-	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `UUID                                    OWNER     PTYPE    CREATED                   UPDATED                
-de1bf5b5-4a72-4f94-a10c-d2a3741cdf75    elrond    cmd      2014-01-01T00:00:00UTC    2014-01-01T00:00:00UTC    
-`, "output")
-
-	server.Mux.HandleFunc("/v2/apps/mordor/settings/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-		fmt.Fprintf(w, `{
-			"owner": "sauron",
-			"app": "mordor",
-			"created": "2014-01-01T00:00:00UTC",
-			"updated": "2014-01-01T00:00:00UTC",
-			"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
-		}`)
-	})
-	b.Reset()
-
-	err = cmdr.CanaryInfo("mordor")
-	assert.NoError(t, err)
-	assert.Equal(t, b.String(), "No canaries found in mordor app.\n", "output")
-}
-
-func TestCanaryCreate(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	server.Mux.HandleFunc("/v2/apps/lothlorien/settings/", func(w http.ResponseWriter, r *http.Request) {
-		testutil.SetHeaders(w)
-		data := []string{"cmd"}
-		testutil.AssertBody(t, api.AppSettings{Canaries: data}, r)
-		fmt.Fprintf(w, `{}`)
-	})
-
-	err = cmdr.CanaryCreate("lothlorien", []string{"cmd"})
-	assert.NoError(t, err)
-	assert.Equal(t, testutil.StripProgress(b.String()), "Applying canary settings for process type cmd on lothlorien... done\n", "output")
-}
-
-func TestCanaryRemove(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	server.Mux.HandleFunc("/v2/apps/bree/settings/", func(w http.ResponseWriter, r *http.Request) {
-		testutil.SetHeaders(w)
-		data := []string{"cmd"}
-		testutil.AssertBody(t, api.AppSettings{Canaries: data}, r)
-		w.WriteHeader(http.StatusNoContent)
-	})
-
-	err = cmdr.CanaryRemove("bree", []string{"cmd"})
-	assert.NoError(t, err)
-	assert.Equal(t, testutil.StripProgress(b.String()), "Removing canary for process type cmd on bree... done\n", "output")
-}
-
-func TestCanaryRelease(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	server.Mux.HandleFunc("/v2/apps/bree/canary/release/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-		w.WriteHeader(http.StatusNoContent)
-	})
-
-	err = cmdr.CanaryRelease("bree")
-	assert.NoError(t, err)
-	assert.Equal(t, testutil.StripProgress(b.String()), "Release canary for bree... done\n", "output")
-}
-
-func TestCanaryRollback(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	server.Mux.HandleFunc("/v2/apps/bree/canary/rollback/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-		w.WriteHeader(http.StatusNoContent)
-	})
-
-	err = cmdr.CanaryRollback("bree")
-	assert.NoError(t, err)
-	assert.Equal(t, testutil.StripProgress(b.String()), "Rollback canary for bree... done\n", "output")
-}
diff --git a/cmd/config.go b/cmd/config.go
deleted file mode 100644
index 5781ab0eb..000000000
--- a/cmd/config.go
+++ /dev/null
@@ -1,296 +0,0 @@
-package cmd
-
-import (
-	"bytes"
-	"encoding/base64"
-	"fmt"
-	"os"
-	"regexp"
-	"strings"
-
-	"github.com/drycc/controller-sdk-go/api"
-	"github.com/drycc/controller-sdk-go/config"
-)
-
-// ConfigList lists an app's config.
-func (d *DryccCmd) ConfigList(appID string, procType string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-	if err != nil {
-		return err
-	}
-	config, err := config.List(s.Client, appID)
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-
-	configValues := config.Values
-	if procType != "" {
-		configValues = config.TypedValues[procType]
-	}
-
-	keys := *sortKeys(configValues)
-	table := d.getDefaultFormatTable([]string{"NAME", "VALUE"})
-
-	for _, key := range keys {
-		table.Append([]string{
-			key,
-			fmt.Sprintf("%v", configValues[key]),
-		})
-	}
-	table.Render()
-	return nil
-}
-
-// ConfigSet sets an app's config variables.
-func (d *DryccCmd) ConfigSet(appID string, procType string, configVars []string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
-	if err != nil {
-		return err
-	}
-
-	configMap, err := parseConfig(configVars)
-	if err != nil {
-		return err
-	}
-
-	if value, ok := configMap["SSH_KEY"]; ok {
-		sshKey, err := parseSSHKey(value.(string))
-		if err != nil {
-			return err
-		}
-		configMap["SSH_KEY"] = base64.StdEncoding.EncodeToString([]byte(sshKey))
-	}
-
-	// NOTE(bacongobbler): check if the user is using the old way to set healthchecks. If so,
-	// send them a deprecation notice.
-	for key := range configMap {
-		if strings.Contains(key, "HEALTHCHECK_") {
-			d.Println(`Hey there! We've noticed that you're using 'drycc config:set HEALTHCHECK_URL'
-to set up healthchecks. This functionality has been deprecated. In the future, please use
-'drycc healthchecks' to set up application health checks. Thanks!`)
-		}
-	}
-
-	d.Print("Creating config... ")
-
-	quit := progress(d.WOut)
-	configObj, err := config.Set(s.Client, appID, func() api.Config {
-		if procType != "" {
-			return api.Config{TypedValues: map[string]api.ConfigValues{procType: configMap}}
-		}
-		return api.Config{Values: configMap}
-	}())
-	quit <- true
-	<-quit
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-
-	if release, ok := configObj.Values["WORKFLOW_RELEASE"]; ok {
-		d.Printf("done, %s\n\n", release)
-	} else {
-		d.Print("done\n\n")
-	}
-
-	return d.ConfigList(appID, procType)
-}
-
-// ConfigUnset removes a config variable from an app.
-func (d *DryccCmd) ConfigUnset(appID string, procType string, configVars []string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
-	if err != nil {
-		return err
-	}
-
-	d.Print("Removing config... ")
-
-	quit := progress(d.WOut)
-
-	configObj := api.Config{}
-
-	valuesMap := make(map[string]interface{})
-
-	for _, configVar := range configVars {
-		valuesMap[configVar] = nil
-	}
-
-	configObj.Values = valuesMap
-
-	_, err = config.Set(s.Client, appID, configObj)
-	quit <- true
-	<-quit
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-
-	d.Print("done\n\n")
-
-	return d.ConfigList(appID, procType)
-}
-
-// ConfigPull pulls an app's config to a file.
-func (d *DryccCmd) ConfigPull(appID, procType, fileName string, interactive bool, overwrite bool) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
-	if err != nil {
-		return err
-	}
-
-	configVars, err := config.List(s.Client, appID)
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-
-	stat, err := os.Stdout.Stat()
-
-	if err != nil {
-		return err
-	}
-	configValues := configVars.Values
-	if procType != "" {
-		configValues = configVars.TypedValues[procType]
-	}
-
-	if (stat.Mode() & os.ModeCharDevice) == 0 {
-		d.Print(formatConfig(configValues))
-		return nil
-	}
-
-	if !overwrite {
-		if _, err := os.Stat(fileName); err == nil {
-			return fmt.Errorf("%s already exists, pass -o to overwrite", fileName)
-		}
-	}
-
-	if interactive {
-		contents, err := os.ReadFile(fileName)
-		if err != nil {
-			return err
-		}
-		localConfigVars := strings.Split(string(contents), "\n")
-		configMap, err := parseConfig(localConfigVars[:len(localConfigVars)-1])
-		if err != nil {
-			return err
-		}
-		for key, value := range configValues {
-			localValue, ok := configMap[key]
-			if ok {
-				if value != localValue {
-					var confirm string
-					d.Printf("%s: overwrite %s with %s? (y/N) ", key, localValue, value)
-
-					fmt.Scanln(&confirm)
-
-					if strings.ToLower(confirm) == "y" {
-						configMap[key] = value
-					}
-				}
-			} else {
-				configMap[key] = value
-			}
-		}
-		return os.WriteFile(fileName, []byte(formatConfig(configMap)), 0755)
-	}
-	return os.WriteFile(fileName, []byte(formatConfig(configValues)), 0755)
-}
-
-// ConfigPush pushes an app's config from a file.
-func (d *DryccCmd) ConfigPush(appID, procType string, fileName string) error {
-	stat, err := os.Stdin.Stat()
-
-	if err != nil {
-		return err
-	}
-
-	var contents []byte
-
-	if (stat.Mode() & os.ModeCharDevice) == 0 {
-		buffer := new(bytes.Buffer)
-		buffer.ReadFrom(os.Stdin)
-		contents = buffer.Bytes()
-	} else {
-		contents, err = os.ReadFile(fileName)
-
-		if err != nil {
-			return err
-		}
-	}
-
-	file := strings.Split(string(contents), "\n")
-	config := []string{}
-
-	for _, configVar := range file {
-		// If file has CRLF encoding, the default on windows, strip the CR
-		configVar = strings.Trim(configVar, "\r")
-		if len(configVar) > 0 {
-			config = append(config, configVar)
-		}
-	}
-
-	return d.ConfigSet(appID, procType, config)
-}
-
-func parseConfig(configVars []string) (api.ConfigValues, error) {
-	configMap := make(api.ConfigValues)
-
-	regex := regexp.MustCompile(`^([A-z_]+[A-z0-9_]*)=([\s\S]*)$`)
-	for _, config := range configVars {
-		// Skip config that starts with an comment
-		if config[0] == '#' {
-			continue
-		}
-
-		if regex.MatchString(config) {
-			captures := regex.FindStringSubmatch(config)
-			configMap[captures[1]] = captures[2]
-		} else {
-			return nil, fmt.Errorf("'%s' does not match the pattern 'key=var', ex: MODE=test", config)
-		}
-	}
-
-	return configMap, nil
-}
-
-func parseSSHKey(value string) (string, error) {
-	sshRegex := regexp.MustCompile("^-----BEGIN (DSA|RSA|EC|OPENSSH) PRIVATE KEY-----")
-
-	if sshRegex.MatchString(value) {
-		return value, nil
-	}
-
-	// NOTE(felixbuenemann): check if the current value is already a base64 encoded key.
-	// This is the case if it was fetched using "drycc config:pull".
-	contents, err := base64.StdEncoding.DecodeString(value)
-
-	if err == nil && sshRegex.MatchString(string(contents)) {
-		return string(contents), nil
-	}
-
-	// NOTE(felixbuenemann): check if the value is a path to a private key.
-	if _, err := os.Stat(value); err == nil {
-		contents, err := os.ReadFile(value)
-
-		if err != nil {
-			return "", err
-		}
-
-		if sshRegex.MatchString(string(contents)) {
-			return string(contents), nil
-		}
-	}
-
-	return "", fmt.Errorf("could not parse SSH private key:\n %s", value)
-}
-
-func formatConfig(configVars map[string]interface{}) string {
-	var formattedConfig string
-
-	keys := *sortKeys(configVars)
-	for _, key := range keys {
-		formattedConfig += fmt.Sprintf("%s=%v\n", key, configVars[key])
-	}
-
-	return formattedConfig
-}
diff --git a/cmd/config_test.go b/cmd/config_test.go
deleted file mode 100644
index 1c0c14166..000000000
--- a/cmd/config_test.go
+++ /dev/null
@@ -1,310 +0,0 @@
-package cmd
-
-import (
-	"bytes"
-	"fmt"
-	"net/http"
-	"os"
-	"testing"
-
-	"github.com/drycc/controller-sdk-go/api"
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-func TestParseConfig(t *testing.T) {
-	t.Parallel()
-
-	_, err := parseConfig([]string{"FOO=bar", "CAR star"})
-	assert.NotEqual(t, err, nil, "config")
-
-	actual, err := parseConfig([]string{"FOO=bar"})
-	assert.NoError(t, err)
-	assert.Equal(t, actual, api.ConfigValues{"FOO": "bar"}, "map")
-
-	actual, err = parseConfig([]string{"FOO="})
-	assert.NoError(t, err)
-	assert.Equal(t, actual, api.ConfigValues{"FOO": ""}, "map")
-}
-
-func TestParseSSHKey(t *testing.T) {
-	t.Parallel()
-
-	_, err := parseSSHKey("foobar")
-	assert.NotEqual(t, err, "bogus key")
-
-	validSSHKey := "-----BEGIN OPENSSH PRIVATE KEY-----"
-
-	actual, err := parseSSHKey(validSSHKey)
-	assert.NoError(t, err)
-	assert.Equal(t, actual, validSSHKey, "plain key")
-
-	encodedSSHKey := "LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0="
-
-	actual, err = parseSSHKey(encodedSSHKey)
-	assert.NoError(t, err)
-	assert.Equal(t, actual, validSSHKey, "base64 key")
-
-	keyFile, err := os.CreateTemp("", "drycc-cli-unit-test-sshkey")
-	assert.NoError(t, err)
-	defer os.Remove(keyFile.Name())
-	_, err = keyFile.Write([]byte(validSSHKey))
-	assert.NoError(t, err)
-	keyFile.Close()
-
-	actual, err = parseSSHKey(keyFile.Name())
-	assert.NoError(t, err)
-	assert.Equal(t, actual, validSSHKey, "key path")
-}
-
-func TestFormatConfig(t *testing.T) {
-	t.Parallel()
-
-	testMap := map[string]interface{}{
-		"TEST":  "testing",
-		"NCC":   1701,
-		"TRUE":  false,
-		"FLOAT": 12.34,
-	}
-
-	testOut := formatConfig(testMap)
-	assert.Equal(t, testOut, `FLOAT=12.34
-NCC=1701
-TEST=testing
-TRUE=false
-`, "output")
-}
-
-func TestSortKeys(t *testing.T) {
-	test := map[string]interface{}{
-		"d": nil,
-		"b": nil,
-		"c": nil,
-		"a": nil,
-	}
-
-	assert.Equal(t, *sortKeys(test), []string{"a", "b", "c", "d"}, "map")
-}
-
-func TestConfigList(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-
-	server.Mux.HandleFunc("/v2/apps/foo/config/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-		fmt.Fprintf(w, `{
-    "owner": "jkirk",
-    "app": "foo",
-    "values": {
-        "TEST":  "testing",
-        "NCC":   "1701",
-        "TRUE":  "false",
-        "FLOAT": "12.34"
-    },
-    "typed_values": {
-		"web": {
-            "PORT":  "9000"
-		}
-    },
-    "memory": {},
-    "cpu": {},
-    "tags": {},
-    "registry": {},
-    "created": "2014-01-01T00:00:00UTC",
-    "updated": "2014-01-01T00:00:00UTC",
-    "uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
-}`)
-	})
-
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	err = cmdr.ConfigList("foo", "")
-	assert.NoError(t, err)
-
-	assert.Equal(t, b.String(), `NAME     VALUE   
-FLOAT    12.34      
-NCC      1701       
-TEST     testing    
-TRUE     false      
-`, "output")
-
-	b.Reset()
-	err = cmdr.ConfigList("foo", "web")
-	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `NAME    VALUE 
-PORT    9000     
-`, "output")
-
-}
-
-func TestConfigSet(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-
-	server.Mux.HandleFunc("/v2/apps/foo/config/", func(w http.ResponseWriter, r *http.Request) {
-		testutil.SetHeaders(w)
-		if r.Method == "POST" {
-			testutil.AssertBody(t, api.Config{
-				Values: map[string]interface{}{
-					"TRUE":    "false",
-					"SSH_KEY": "LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0=",
-				},
-			}, r)
-		}
-
-		fmt.Fprintf(w, `{
-	"owner": "jkirk",
-	"app": "foo",
-	"values": {
-			"TEST":  "testing",
-			"NCC":   "1701",
-			"TRUE":  "false",
-			"SSH_KEY": "LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0=",
-			"FLOAT": "12.34"
-	},
-	"memory": {},
-	"cpu": {},
-	"tags": {},
-	"registry": {},
-	"created": "2014-01-01T00:00:00UTC",
-	"updated": "2014-01-01T00:00:00UTC",
-	"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
-}`)
-	})
-
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	err = cmdr.ConfigSet("foo", "", []string{"TRUE=false", "SSH_KEY=-----BEGIN OPENSSH PRIVATE KEY-----"})
-	assert.NoError(t, err)
-
-	assert.Equal(t, testutil.StripProgress(b.String()), `Creating config... done
-
-NAME       VALUE                                            
-FLOAT      12.34                                               
-NCC        1701                                                
-SSH_KEY    LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0=    
-TEST       testing                                             
-TRUE       false                                               
-`, "output")
-}
-
-func TestConfigUnset(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-
-	server.Mux.HandleFunc("/v2/apps/foo/config/", func(w http.ResponseWriter, r *http.Request) {
-		testutil.SetHeaders(w)
-		if r.Method == "POST" {
-			testutil.AssertBody(t, api.Config{
-				Values: map[string]interface{}{
-					"FOO": nil,
-				},
-			}, r)
-		}
-
-		fmt.Fprintf(w, `{
-	"owner": "jkirk",
-	"app": "foo",
-	"values": {
-			"TEST":  "testing",
-			"NCC":   "1701",
-			"TRUE":  "false",
-			"FLOAT": "12.34"
-	},
-	"memory": {},
-	"cpu": {},
-	"tags": {},
-	"registry": {},
-	"created": "2014-01-01T00:00:00UTC",
-	"updated": "2014-01-01T00:00:00UTC",
-	"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
-}`)
-	})
-
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	err = cmdr.ConfigUnset("foo", "", []string{"FOO"})
-	assert.NoError(t, err)
-
-	assert.Equal(t, testutil.StripProgress(b.String()), `Removing config... done
-
-NAME     VALUE   
-FLOAT    12.34      
-NCC      1701       
-TEST     testing    
-TRUE     false      
-`, "output")
-}
-
-func TestConfigUnsetTypedValues(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-
-	server.Mux.HandleFunc("/v2/apps/foo/config/", func(w http.ResponseWriter, r *http.Request) {
-		testutil.SetHeaders(w)
-		if r.Method == "POST" {
-			testutil.AssertBody(t, api.Config{
-				Values: map[string]interface{}{
-					"FOO": nil,
-				},
-			}, r)
-		}
-
-		fmt.Fprintf(w, `{
-	"owner": "jkirk",
-	"app": "foo",
-	"values": {
-		"RELEASE_VERSION": "v1"
-	},
-	"typed_values": {
-		"web": {
-			"TEST":  "testing",
-			"NCC":   "1701",
-			"TRUE":  "false",
-			"FLOAT": "12.34"
-		}
-	},
-	"memory": {},
-	"cpu": {},
-	"tags": {},
-	"registry": {},
-	"created": "2014-01-01T00:00:00UTC",
-	"updated": "2014-01-01T00:00:00UTC",
-	"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
-}`)
-	})
-
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	err = cmdr.ConfigUnset("foo", "web", []string{"FOO"})
-	assert.NoError(t, err)
-
-	assert.Equal(t, testutil.StripProgress(b.String()), `Removing config... done
-
-NAME     VALUE   
-FLOAT    12.34      
-NCC      1701       
-TEST     testing    
-TRUE     false      
-`, "output")
-}
diff --git a/cmd/healthchecks.go b/cmd/healthchecks.go
deleted file mode 100644
index 27911665b..000000000
--- a/cmd/healthchecks.go
+++ /dev/null
@@ -1,173 +0,0 @@
-package cmd
-
-import (
-	"fmt"
-	"sort"
-
-	"github.com/drycc/controller-sdk-go/api"
-	"github.com/drycc/controller-sdk-go/config"
-)
-
-func getHealthcheckString(procType, probeType string, healthcheck *api.Healthcheck) string {
-	params := fmt.Sprintf(
-		"delay=%ds timeout=%ds period=%ds #success=%d #failure=%d",
-		healthcheck.InitialDelaySeconds,
-		healthcheck.TimeoutSeconds,
-		healthcheck.PeriodSeconds,
-		healthcheck.SuccessThreshold,
-		healthcheck.FailureThreshold,
-	)
-
-	if healthcheck.Exec != nil {
-		return fmt.Sprintf("%s %s exec %v %s", probeType, procType, healthcheck.Exec.Command, params)
-	} else if healthcheck.TCPSocket != nil {
-		return fmt.Sprintf("%s %s tcp-socket port=%v %s", probeType, procType, healthcheck.TCPSocket.Port, params)
-	} else if healthcheck.HTTPGet != nil {
-		return fmt.Sprintf(
-			"%s %s http-get headers=%v path=%s port=%d %s",
-			probeType,
-			procType,
-			healthcheck.HTTPGet.HTTPHeaders,
-			healthcheck.HTTPGet.Path,
-			healthcheck.HTTPGet.Port,
-			params,
-		)
-	}
-	return ""
-}
-
-func getHealthchecksStrings(procType string, healthchecks *api.Healthchecks) []string {
-	var probes []string
-	for key := range *healthchecks {
-		probes = append(probes, getHealthcheckString(procType, key, (*healthchecks)[key]))
-	}
-	return probes
-}
-
-// HealthchecksList lists an app's healthchecks.
-func (d *DryccCmd) HealthchecksList(appID, procType string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-	if err != nil {
-		return err
-	}
-
-	config, err := config.List(s.Client, appID)
-
-	if err != nil {
-		return err
-	}
-
-	if procType == "" {
-		if len(config.Healthcheck) == 0 {
-			d.Println("No health checks configured.")
-		} else {
-			var keys []string
-			for k := range config.Healthcheck {
-				keys = append(keys, k)
-			}
-			sort.Strings(keys)
-			table := d.getDefaultFormatTable([]string{})
-			table.Append([]string{"App:", config.App})
-			table.Append([]string{"UUID:", config.UUID})
-			table.Append([]string{"Owner:", config.Owner})
-			table.Append([]string{"Created:", config.Created})
-			table.Append([]string{"Updated:", config.Updated})
-			table.Append([]string{"Healthchecks:"})
-			for _, key := range keys {
-				for _, probe := range getHealthchecksStrings(key, config.Healthcheck[key]) {
-					if probe != "" {
-						table.Append([]string{"", probe})
-					}
-				}
-			}
-			table.Render()
-		}
-	} else {
-		if healthcheck, found := config.Healthcheck[procType]; found {
-			table := d.getDefaultFormatTable([]string{})
-			table.Append([]string{"App:", config.App})
-			table.Append([]string{"UUID:", config.UUID})
-			table.Append([]string{"Owner:", config.Owner})
-			table.Append([]string{"Created:", config.Created})
-			table.Append([]string{"Updated:", config.Updated})
-			table.Append([]string{"Healthchecks:"})
-			for _, probe := range getHealthchecksStrings(procType, healthcheck) {
-				if probe != "" {
-					table.Append([]string{"", probe})
-				}
-			}
-			table.Render()
-		} else {
-			d.Println("No health checks configured.")
-		}
-	}
-	return nil
-}
-
-// HealthchecksSet sets an app's healthchecks.
-func (d *DryccCmd) HealthchecksSet(appID, healthcheckType, procType string, probe *api.Healthcheck) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
-	if err != nil {
-		return err
-	}
-
-	d.Printf("Applying %s healthcheck... ", healthcheckType)
-
-	quit := progress(d.WOut)
-
-	healthcheckMap := make(api.Healthchecks)
-	healthcheckMap[healthcheckType] = probe
-	configObj := api.Config{Healthcheck: make(map[string]*api.Healthchecks)}
-	configObj.Healthcheck[procType] = &healthcheckMap
-
-	_, err = config.Set(s.Client, appID, configObj)
-
-	quit <- true
-	<-quit
-
-	if err != nil {
-		return err
-	}
-
-	d.Print("done\n\n")
-
-	return d.HealthchecksList(appID, procType)
-}
-
-// HealthchecksUnset removes an app's healthchecks.
-func (d *DryccCmd) HealthchecksUnset(appID, procType string, healthchecks []string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-	if err != nil {
-		return err
-	}
-
-	d.Print("Removing healthchecks... ")
-
-	quit := progress(d.WOut)
-
-	configObj := api.Config{}
-
-	healthchecksMap := make(map[string]*api.Healthchecks)
-	healthcheckMap := make(api.Healthchecks)
-
-	for _, healthcheck := range healthchecks {
-		healthcheckMap[healthcheck] = nil
-	}
-	healthchecksMap[procType] = &healthcheckMap
-
-	configObj.Healthcheck = healthchecksMap
-
-	_, err = config.Set(s.Client, appID, configObj)
-
-	quit <- true
-	<-quit
-
-	if err != nil {
-		return err
-	}
-
-	d.Print("done\n\n")
-
-	return d.HealthchecksList(appID, procType)
-}
diff --git a/cmd/perms.go b/cmd/perms.go
deleted file mode 100644
index a9ae659f3..000000000
--- a/cmd/perms.go
+++ /dev/null
@@ -1,110 +0,0 @@
-package cmd
-
-import (
-	"fmt"
-
-	"github.com/drycc/controller-sdk-go/perms"
-	"github.com/drycc/workflow-cli/pkg/git"
-	"github.com/drycc/workflow-cli/settings"
-)
-
-// PermsList prints which users have permissions.
-func (d *DryccCmd) PermsList(appID string, admin bool, results int) error {
-	s, appID, err := permsLoad(d.ConfigFile, appID, admin)
-
-	if err != nil {
-		return err
-	}
-
-	var users []string
-
-	if admin {
-		if results == defaultLimit {
-			results = s.Limit
-		}
-		users, _, err = perms.ListAdmins(s.Client, results)
-	} else {
-		users, err = perms.List(s.Client, appID)
-	}
-
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-
-	table := d.getDefaultFormatTable([]string{"USERNAME", "ADMIN"})
-	for _, user := range users {
-		table.Append([]string{user, fmt.Sprintf("%v", admin)})
-	}
-	table.Render()
-	return nil
-}
-
-// PermCreate adds a user to an app or makes them an administrator.
-func (d *DryccCmd) PermCreate(appID string, username string, admin bool) error {
-
-	s, appID, err := permsLoad(d.ConfigFile, appID, admin)
-
-	if err != nil {
-		return err
-	}
-
-	if admin {
-		d.Printf("Adding %s to system administrators... ", username)
-		err = perms.NewAdmin(s.Client, username)
-	} else {
-		d.Printf("Adding %s to %s collaborators... ", username, appID)
-		err = perms.New(s.Client, appID, username)
-	}
-
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-
-	d.Println("done")
-
-	return nil
-}
-
-// PermDelete removes a user from an app or revokes admin privileges.
-func (d *DryccCmd) PermDelete(appID, username string, admin bool) error {
-
-	s, appID, err := permsLoad(d.ConfigFile, appID, admin)
-
-	if err != nil {
-		return err
-	}
-
-	if admin {
-		d.Printf("Removing %s from system administrators... ", username)
-		err = perms.DeleteAdmin(s.Client, username)
-	} else {
-		d.Printf("Removing %s from %s collaborators... ", username, appID)
-		err = perms.Delete(s.Client, appID, username)
-	}
-
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-
-	d.Println("done")
-
-	return nil
-}
-
-func permsLoad(cf, appID string, admin bool) (*settings.Settings, string, error) {
-	s, err := settings.Load(cf)
-
-	if err != nil {
-		return nil, "", err
-	}
-
-	if !admin && appID == "" {
-		appID, err = git.DetectAppName(git.DefaultCmd, s.Client.ControllerURL.Host)
-
-		if err != nil {
-			return nil, "", err
-		}
-	}
-
-	return s, appID, err
-}
diff --git a/cmd/perms_test.go b/cmd/perms_test.go
deleted file mode 100644
index 6c70fb37f..000000000
--- a/cmd/perms_test.go
+++ /dev/null
@@ -1,254 +0,0 @@
-package cmd
-
-import (
-	"bytes"
-	"fmt"
-	"net/http"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-func TestPermsListUsers(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-
-	server.Mux.HandleFunc("/v2/apps/foo/perms/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-
-		fmt.Fprintf(w, `{
-  "users": [
-    "baz",
-    "bar"
-  ]
-}`)
-	})
-
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	err = cmdr.PermsList("foo", false, -1)
-	assert.NoError(t, err)
-
-	assert.Equal(t, testutil.StripProgress(b.String()), `USERNAME    ADMIN 
-baz         false    
-bar         false    
-`, "output")
-}
-
-func TestPermsListUsersLimit(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-
-	server.Mux.HandleFunc("/v2/apps/foo/perms/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-
-		fmt.Fprintf(w, `{
-  "users": [
-    "baz"
-  ]
-}`)
-	})
-
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	err = cmdr.PermsList("foo", false, 1)
-	assert.NoError(t, err)
-
-	assert.Equal(t, testutil.StripProgress(b.String()), `USERNAME    ADMIN 
-baz         false    
-`, "output")
-}
-
-func TestPermsListAdmins(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-
-	server.Mux.HandleFunc("/v2/admin/perms/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-
-		fmt.Fprintf(w, `{
-  "count": 2,
-  "next": null,
-  "previous": null,
-  "results": [
-    {
-      "username": "fred",
-      "is_superuser": true
-    },
-    {
-      "username": "bob",
-      "is_superuser": true
-    }
-]
-}`)
-	})
-
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	err = cmdr.PermsList("foo", true, -1)
-	assert.NoError(t, err)
-
-	assert.Equal(t, testutil.StripProgress(b.String()), `USERNAME    ADMIN 
-fred        true     
-bob         true     
-`, "output")
-}
-
-func TestPermsListAdminsLimit(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-
-	server.Mux.HandleFunc("/v2/admin/perms/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-
-		fmt.Fprintf(w, `{
-  "count": 2,
-  "next": null,
-  "previous": null,
-  "results": [
-    {
-      "username": "fred",
-      "is_superuser": true
-    }
-  ]
-}`)
-	})
-
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	err = cmdr.PermsList("foo", true, 1)
-	assert.NoError(t, err)
-
-	assert.Equal(t, testutil.StripProgress(b.String()), `USERNAME    ADMIN 
-fred        true     
-`, "output")
-}
-
-func TestPermsCreateUser(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	server.Mux.HandleFunc("/v2/apps/lorem-ipsum/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-		fmt.Fprintf(w, `{
-  "uuid": "c4aed81c-d1ca-4ff1-ab89-d2151264e1a3",
-  "id": "lorem-ipsum",
-  "owner": "dolar-sit-amet",
-  "structure": {
-    "cmd": 1
-  },
-  "created": "2016-08-22T17:40:16Z",
-  "updated": "2016-08-22T17:40:16Z"
-}`)
-	})
-
-	server.Mux.HandleFunc("/v2/apps/lorem-ipsum/perms", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-	})
-
-	err = cmdr.PermCreate("lorem-ipsum", "test-user", false)
-	assert.NoError(t, err)
-	assert.Equal(t, testutil.StripProgress(b.String()), `Adding test-user to lorem-ipsum collaborators... done
-`, "output")
-}
-
-func TestPermsCreateAdmin(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	server.Mux.HandleFunc("/v2/admin/perms/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-	})
-
-	err = cmdr.PermCreate("lorem-ipsum", "test-admin", true)
-	assert.NoError(t, err)
-	assert.Equal(t, testutil.StripProgress(b.String()), `Adding test-admin to system administrators... done
-`, "output")
-}
-
-func TestPermsDeleteUser(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	server.Mux.HandleFunc("/v2/apps/lorem-ipsum/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-		fmt.Fprintf(w, `{
-  "uuid": "c4aed81c-d1ca-4ff1-ab89-d2151264e1a3",
-  "id": "lorem-ipsum",
-  "owner": "dolar-sit-amet",
-  "structure": {
-    "cmd": 1
-  },
-  "created": "2016-08-22T17:40:16Z",
-  "updated": "2016-08-22T17:40:16Z"
-}`)
-	})
-
-	server.Mux.HandleFunc("/v2/apps/lorem-ipsum/perms", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-	})
-
-	err = cmdr.PermDelete("lorem-ipsum", "test-user", false)
-	assert.NoError(t, err)
-	assert.Equal(t, testutil.StripProgress(b.String()), `Removing test-user from lorem-ipsum collaborators... done
-`, "output")
-}
-
-func TestPermsDeleteAdmin(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	server.Mux.HandleFunc("/v2/admin/perms/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-	})
-
-	err = cmdr.PermDelete("lorem-ipsum", "test-admin", true)
-	assert.NoError(t, err)
-	assert.Equal(t, testutil.StripProgress(b.String()), `Removing test-admin from system administrators... done
-`, "output")
-}
diff --git a/cmd/registry.go b/cmd/registry.go
deleted file mode 100644
index 4d7701f67..000000000
--- a/cmd/registry.go
+++ /dev/null
@@ -1,132 +0,0 @@
-package cmd
-
-import (
-	"fmt"
-	"strings"
-
-	"github.com/drycc/controller-sdk-go/api"
-	"github.com/drycc/controller-sdk-go/config"
-)
-
-// RegistryList lists an app's registry information.
-func (d *DryccCmd) RegistryList(appID string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
-	if err != nil {
-		return err
-	}
-
-	config, err := config.List(s.Client, appID)
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-	if len(config.Registry) == 0 {
-		d.Println(fmt.Sprintf("No registrys found in %s app.", appID))
-	} else {
-		table := d.getDefaultFormatTable([]string{"KEY", "VALUE"})
-		for _, key := range *sortKeys(config.Registry) {
-			table.Append([]string{
-				key,
-				fmt.Sprintf("%v", config.Registry[key]),
-			})
-		}
-		table.Render()
-	}
-	return nil
-}
-
-// RegistrySet sets an app's registry information.
-func (d *DryccCmd) RegistrySet(appID string, item []string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
-	if err != nil {
-		return err
-	}
-
-	registryMap, err := parseInfos(item)
-	if err != nil {
-		return err
-	}
-
-	d.Print("Applying registry information... ")
-
-	quit := progress(d.WOut)
-	configObj := api.Config{}
-	configObj.Registry = registryMap
-
-	_, err = config.Set(s.Client, appID, configObj)
-	quit <- true
-	<-quit
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-
-	d.Print("done\n\n")
-
-	return d.RegistryList(appID)
-}
-
-// RegistryUnset removes an app's registry information.
-func (d *DryccCmd) RegistryUnset(appID string, items []string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
-	if err != nil {
-		return err
-	}
-
-	d.Print("Applying registry information... ")
-
-	quit := progress(d.WOut)
-
-	configObj := api.Config{}
-
-	registryMap := make(map[string]interface{})
-
-	for _, key := range items {
-		registryMap[key] = nil
-	}
-
-	configObj.Registry = registryMap
-
-	_, err = config.Set(s.Client, appID, configObj)
-	quit <- true
-	<-quit
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-
-	d.Print("done\n\n")
-
-	return d.RegistryList(appID)
-}
-
-func parseInfos(items []string) (map[string]interface{}, error) {
-	registryMap := make(map[string]interface{})
-
-	for _, item := range items {
-		key, value, err := parseInfo(item)
-
-		if err != nil {
-			return nil, err
-		}
-
-		registryMap[key] = value
-	}
-
-	return registryMap, nil
-}
-
-func parseInfo(item string) (string, string, error) {
-	parts := strings.SplitN(item, "=", 2)
-
-	if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
-		return "", "", fmt.Errorf(`%s is invalid. Must be in format key=value
-Examples: username=bob password=s3cur3pw1`, item)
-	}
-
-	if parts[0] != "username" && parts[0] != "password" {
-		return "", "", fmt.Errorf(`%s is invalid. Valid keys are "username" or "password"`, parts[0])
-	}
-
-	return parts[0], parts[1], nil
-}
diff --git a/cmd/registry_test.go b/cmd/registry_test.go
deleted file mode 100644
index 79d753303..000000000
--- a/cmd/registry_test.go
+++ /dev/null
@@ -1,207 +0,0 @@
-package cmd
-
-import (
-	"bytes"
-	"fmt"
-	"net/http"
-	"testing"
-
-	"github.com/drycc/controller-sdk-go/api"
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-type parseInfoCase struct {
-	Input         string
-	Key           string
-	Value         string
-	ExpectedError bool
-	ExpectedMsg   string
-}
-
-func TestParseInfo(t *testing.T) {
-	t.Parallel()
-
-	cases := []parseInfoCase{
-		{"username=test", "username", "test", false, ""},
-		{"password=test=", "password", "test=", false, ""},
-		{"test=1", "", "", true, `test is invalid. Valid keys are "username" or "password"`},
-		{"test", "", "", true, `test is invalid. Must be in format key=value
-Examples: username=bob password=s3cur3pw1`},
-		{"test=", "", "", true, `test= is invalid. Must be in format key=value
-Examples: username=bob password=s3cur3pw1`},
-		{"=test", "", "", true, `=test is invalid. Must be in format key=value
-Examples: username=bob password=s3cur3pw1`},
-	}
-
-	for _, check := range cases {
-		key, value, err := parseInfo(check.Input)
-		if check.ExpectedError {
-			assert.Equal(t, err.Error(), check.ExpectedMsg, "error")
-		} else {
-			assert.NoError(t, err)
-			assert.Equal(t, key, check.Key, "key")
-			assert.Equal(t, value, check.Value, "value")
-		}
-	}
-}
-
-type parseInfosCase struct {
-	Input         []string
-	ExpectedMap   map[string]interface{}
-	ExpectedError bool
-	ExpectedMsg   string
-}
-
-func TestParseInfos(t *testing.T) {
-	t.Parallel()
-
-	cases := []parseInfosCase{
-		{[]string{"username=test", "password=abc123"}, map[string]interface{}{"username": "test", "password": "abc123"}, false, ""},
-		{[]string{"foo=", "true=false"}, nil, true, `foo= is invalid. Must be in format key=value
-Examples: username=bob password=s3cur3pw1`},
-	}
-
-	for _, check := range cases {
-		actual, err := parseInfos(check.Input)
-		if check.ExpectedError {
-			assert.Equal(t, err.Error(), check.ExpectedMsg, "error")
-		} else {
-			assert.NoError(t, err)
-			assert.Equal(t, actual, check.ExpectedMap, "map")
-		}
-	}
-}
-
-func TestRegistryList(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-
-	server.Mux.HandleFunc("/v2/apps/enterprise/config/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-		fmt.Fprintf(w, `{
-			"owner": "jkirk",
-			"app": "enterprise",
-			"values": {},
-			"memory": {},
-			"cpu": {},
-			"tags": {},
-			"registry": {
-				"username": "jkirk",
-				"password": "ncc1701"
-			},
-			"created": "2014-01-01T00:00:00UTC",
-			"updated": "2014-01-01T00:00:00UTC",
-			"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
-		}`)
-	})
-
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	err = cmdr.RegistryList("enterprise")
-	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `KEY         VALUE   
-password    ncc1701    
-username    jkirk      
-`, "output")
-}
-
-func TestRegistrySet(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-
-	server.Mux.HandleFunc("/v2/apps/foo/config/", func(w http.ResponseWriter, r *http.Request) {
-		testutil.SetHeaders(w)
-		if r.Method == "POST" {
-			testutil.AssertBody(t, api.Config{
-				Registry: map[string]interface{}{
-					"username": "jkirk",
-					"password": "ncc1701",
-				},
-			}, r)
-		}
-
-		fmt.Fprintf(w, `{
-			"owner": "jkirk",
-			"app": "foo",
-			"values": {},
-			"memory": {},
-			"cpu": {},
-			"registry": {
-				"username": "jkirk",
-				"password": "ncc1701"
-			},
-			"registry": {},
-			"created": "2014-01-01T00:00:00UTC",
-			"updated": "2014-01-01T00:00:00UTC",
-			"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
-		}`)
-	})
-
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	err = cmdr.RegistrySet("foo", []string{"username=jkirk", "password=ncc1701"})
-	assert.NoError(t, err)
-
-	assert.Equal(t, testutil.StripProgress(b.String()), `Applying registry information... done
-
-KEY         VALUE   
-password    ncc1701    
-username    jkirk      
-`, "output")
-}
-
-func TestRegistryUnset(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-
-	server.Mux.HandleFunc("/v2/apps/foo/config/", func(w http.ResponseWriter, r *http.Request) {
-		testutil.SetHeaders(w)
-		if r.Method == "POST" {
-			testutil.AssertBody(t, api.Config{
-				Registry: map[string]interface{}{
-					"username": nil,
-					"password": nil,
-				},
-			}, r)
-		}
-
-		fmt.Fprintf(w, `{
-			"owner": "jkirk",
-			"app": "foo",
-			"values": {},
-			"memory": {},
-			"cpu": {},
-			"tags": {},
-			"registry": {},
-			"created": "2014-01-01T00:00:00UTC",
-			"updated": "2014-01-01T00:00:00UTC",
-			"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
-		}`)
-	})
-
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	err = cmdr.RegistryUnset("foo", []string{"username", "password"})
-	assert.NoError(t, err)
-
-	assert.Equal(t, testutil.StripProgress(b.String()), `Applying registry information... done
-
-No registrys found in foo app.
-`, "output")
-}
diff --git a/cmd/releases.go b/cmd/releases.go
deleted file mode 100644
index 55697333d..000000000
--- a/cmd/releases.go
+++ /dev/null
@@ -1,93 +0,0 @@
-package cmd
-
-import (
-	"fmt"
-
-	"github.com/drycc/controller-sdk-go/releases"
-)
-
-// ReleasesList lists an app's releases.
-func (d *DryccCmd) ReleasesList(appID string, results int) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
-	if err != nil {
-		return err
-	}
-
-	if results == defaultLimit {
-		results = s.Limit
-	}
-
-	releases, count, err := releases.List(s.Client, appID, results)
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-	if count == 0 {
-		d.Println(fmt.Sprintf("No releases found in %s app.", appID))
-	} else {
-		table := d.getDefaultFormatTable([]string{"UUID", "OWNER", "STATE", "VERSION", "CREATED", "SUMMARY"})
-		for _, r := range releases {
-			summary := r.Summary
-			if len(summary) > 64 {
-				summary = fmt.Sprintf("%s[...]", summary[:64])
-			}
-			table.Append([]string{r.UUID, r.Owner, r.State, fmt.Sprintf("v%d", r.Version), r.Created, summary})
-		}
-		table.Render()
-	}
-	return nil
-}
-
-// ReleasesInfo prints info about a specific release.
-func (d *DryccCmd) ReleasesInfo(appID string, version int) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
-	if err != nil {
-		return err
-	}
-
-	r, err := releases.Get(s.Client, appID, version)
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-	table := d.getDefaultFormatTable([]string{})
-	table.Append([]string{"App:", r.App})
-	table.Append([]string{"UUID:", r.UUID})
-	table.Append([]string{"State:", r.State})
-	table.Append([]string{"Owner:", r.Owner})
-	table.Append([]string{"Build:", r.Build})
-	table.Append([]string{"Config:", r.Config})
-	table.Append([]string{"Created:", r.Created})
-	table.Append([]string{"Updated:", r.Updated})
-	table.Append([]string{"Summary:", d.wrapString(r.Summary)})
-	table.Append([]string{"Version:", fmt.Sprintf("v%v", r.Version)})
-	table.Render()
-	return nil
-}
-
-// ReleasesRollback rolls an app back to a previous release.
-func (d *DryccCmd) ReleasesRollback(appID string, version int) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
-	if err != nil {
-		return err
-	}
-
-	if version == -1 {
-		d.Print("Rolling back one release... ")
-	} else {
-		d.Printf("Rolling back to v%d... ", version)
-	}
-
-	quit := progress(d.WOut)
-	newVersion, err := releases.Rollback(s.Client, appID, version)
-	quit <- true
-	<-quit
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-
-	d.Printf("done, v%d\n", newVersion)
-
-	return nil
-}
diff --git a/cmd/root.go b/cmd/root.go
new file mode 100644
index 000000000..e5b2201f2
--- /dev/null
+++ b/cmd/root.go
@@ -0,0 +1,84 @@
+// Package cmd provides the root command for the Drycc CLI.
+/*
+Copyright © 2025 NAME HERE <EMAIL ADDRESS>
+*/
+package cmd
+
+import (
+	"os"
+	"time"
+
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/parser"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewDryccCommand creates the root command for the Drycc CLI.
+func NewDryccCommand() *cobra.Command {
+	var flags struct {
+		config  string
+		version bool
+		help    bool
+	}
+
+	var cmdr commands.DryccCmd
+
+	rootCmd := &cobra.Command{
+		Use:   "drycc",
+		Short: i18n.T("The Drycc command-line client issues API calls to a Drycc controller"),
+		PersistentPreRun: func(_ *cobra.Command, _ []string) {
+			cmdr = commands.DryccCmd{ConfigFile: flags.config, WOut: os.Stdout, WErr: os.Stderr, WIn: os.Stdin, Location: time.Local}
+		},
+	}
+	config := "~/.drycc/client.json"
+	if v, ok := os.LookupEnv("DRYCC_PROFILE"); ok {
+		config = v
+	}
+	rootCmd.PersistentFlags().StringVarP(&flags.config, "config", "c", config, i18n.T("Path to configuration file"))
+	rootCmd.PersistentFlags().BoolVarP(&flags.help, "help", "h", false, i18n.T("Display help information"))
+
+	rootCmd.AddCommand(parser.NewAppsCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewAuthCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewAutodeployCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewAutorollbackCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewAutoscaleCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewBuildsCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewCertsCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewConfigCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewDomainsCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewGatewaysCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewGitCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewLifecyclesCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewHealthchecksCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewKeysCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewLabelsCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewLimitsCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewWorkspacesCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewPsCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewPtsCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewRegistryCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewReleasesCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewResourcesCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewRoutesCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewRoutingCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewServicesCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewTagsCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewTimeoutsCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewTLSCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewTokensCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewUpdateCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewVolumesCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewVersionCommand(&cmdr))
+	// shortcuts
+	rootCmd.AddGroup(&cobra.Group{ID: "shortcut", Title: i18n.T("Shortcut Commands:")})
+	for _, shortcuts := range parser.SupportedShortcuts {
+		for _, shortcut := range shortcuts.Create(&cmdr) {
+			shortcut.GroupID = "shortcut"
+			rootCmd.AddCommand(shortcut)
+		}
+	}
+	rootCmd.SilenceUsage = true
+
+	return rootCmd
+}
diff --git a/cmd/shortcuts.go b/cmd/shortcuts.go
deleted file mode 100644
index b63f03a60..000000000
--- a/cmd/shortcuts.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package cmd
-
-import (
-	"fmt"
-	"sort"
-
-	"github.com/drycc/workflow-cli/cli"
-)
-
-// ShortcutsList displays all relevant shortcuts for the CLI.
-func (d *DryccCmd) ShortcutsList() error {
-	d.Println(sortShortcuts())
-
-	return nil
-}
-
-func sortShortcuts() string {
-	var (
-		strBuilder string
-		keys       []string
-	)
-
-	// NOTE(bacongobbler): go does not guarantee an iteration order when iterating over a map,
-	// so to work around this we can sort the keys and iterate using the key array
-	for k := range cli.Shortcuts {
-		keys = append(keys, k)
-	}
-	sort.Strings(keys)
-
-	for _, k := range keys {
-		strBuilder += fmt.Sprintf("%s -> %s\n", k, cli.Shortcuts[k])
-	}
-
-	return strBuilder
-}
diff --git a/cmd/shortcuts_test.go b/cmd/shortcuts_test.go
deleted file mode 100644
index 0ca0fafc7..000000000
--- a/cmd/shortcuts_test.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package cmd
-
-import (
-	"testing"
-)
-
-func TestShortcutsList(t *testing.T) {
-	t.Parallel()
-
-	expected := `create -> apps:create
-destroy -> apps:destroy
-info -> apps:info
-login -> auth:login
-logout -> auth:logout
-logs -> apps:logs
-open -> apps:open
-pull -> builds:create
-rollback -> releases:rollback
-run -> apps:run
-scale -> ps:scale
-sharing -> perms:list
-sharing:add -> perms:create
-sharing:list -> perms:list
-sharing:remove -> perms:delete
-whoami -> auth:whoami
-`
-	actual := sortShortcuts()
-	if actual != expected {
-		t.Errorf("Expected %s, Got %s", expected, actual)
-	}
-
-}
diff --git a/cmd/tags.go b/cmd/tags.go
deleted file mode 100644
index 0fb5dd1af..000000000
--- a/cmd/tags.go
+++ /dev/null
@@ -1,128 +0,0 @@
-package cmd
-
-import (
-	"fmt"
-	"strings"
-
-	"github.com/drycc/controller-sdk-go/api"
-	"github.com/drycc/controller-sdk-go/config"
-)
-
-// TagsList lists an app's tags.
-func (d *DryccCmd) TagsList(appID string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
-	if err != nil {
-		return err
-	}
-
-	config, err := config.List(s.Client, appID)
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-	if len(config.Tags) == 0 {
-		d.Println(fmt.Sprintf("No tags found in %s app.", appID))
-	} else {
-		table := d.getDefaultFormatTable([]string{"PTYPE", "TAG"})
-		for _, key := range *sortKeys(config.Tags) {
-			table.Append([]string{
-				key,
-				fmt.Sprintf("%v", config.Tags[key]),
-			})
-		}
-		table.Render()
-	}
-	return nil
-}
-
-// TagsSet sets an app's tags.
-func (d *DryccCmd) TagsSet(appID string, tags []string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
-	if err != nil {
-		return err
-	}
-
-	tagsMap, err := parseTags(tags)
-	if err != nil {
-		return err
-	}
-
-	d.Print("Applying tags... ")
-
-	quit := progress(d.WOut)
-	configObj := api.Config{}
-	configObj.Tags = tagsMap
-
-	_, err = config.Set(s.Client, appID, configObj)
-	quit <- true
-	<-quit
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-
-	d.Print("done\n\n")
-
-	return d.TagsList(appID)
-}
-
-// TagsUnset removes an app's tags.
-func (d *DryccCmd) TagsUnset(appID string, tags []string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
-	if err != nil {
-		return err
-	}
-
-	d.Print("Applying tags... ")
-
-	quit := progress(d.WOut)
-
-	configObj := api.Config{}
-
-	tagsMap := make(map[string]interface{})
-
-	for _, tag := range tags {
-		tagsMap[tag] = nil
-	}
-
-	configObj.Tags = tagsMap
-
-	_, err = config.Set(s.Client, appID, configObj)
-	quit <- true
-	<-quit
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-
-	d.Print("done\n\n")
-
-	return d.TagsList(appID)
-}
-
-func parseTags(tags []string) (map[string]interface{}, error) {
-	tagMap := make(map[string]interface{})
-
-	for _, tag := range tags {
-		key, value, err := parseTag(tag)
-
-		if err != nil {
-			return nil, err
-		}
-
-		tagMap[key] = value
-	}
-
-	return tagMap, nil
-}
-
-func parseTag(tag string) (string, string, error) {
-	parts := strings.Split(tag, "=")
-
-	if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
-		return "", "", fmt.Errorf(`%s is invalid, Must be in format key=value
-Examples: rack=1 evironment=production`, tag)
-	}
-
-	return parts[0], parts[1], nil
-}
diff --git a/cmd/users.go b/cmd/users.go
deleted file mode 100644
index 1dcecbc0e..000000000
--- a/cmd/users.go
+++ /dev/null
@@ -1,76 +0,0 @@
-package cmd
-
-import (
-	"fmt"
-
-	"github.com/drycc/controller-sdk-go/users"
-	"github.com/drycc/workflow-cli/settings"
-)
-
-// UsersList lists users registered with the controller.
-func (d *DryccCmd) UsersList(results int) error {
-	s, err := settings.Load(d.ConfigFile)
-
-	if err != nil {
-		return err
-	}
-
-	if results == defaultLimit {
-		results = s.Limit
-	}
-
-	users, _, err := users.List(s.Client, results)
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-	table := d.getDefaultFormatTable([]string{"USERNAME", "EMAIL", "ADMIN", "STAFF", "ACTIVE", "DATE-JOIN"})
-	for _, user := range users {
-		table.Append([]string{
-			user.Username,
-			user.Email,
-			fmt.Sprintf("%v", user.IsSuperuser),
-			fmt.Sprintf("%v", user.IsStaff),
-			fmt.Sprintf("%v", user.IsActive),
-			user.DateJoined,
-		})
-	}
-	table.Render()
-	return nil
-}
-
-// UsersEnable enable user with the controller.
-func (d *DryccCmd) UsersEnable(username string) error {
-	s, err := settings.Load(d.ConfigFile)
-
-	if err != nil {
-		return err
-	}
-	d.Printf("Enabling user %s... ", username)
-	err = users.Enable(s.Client, username)
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-
-	d.Println("done")
-	d.Println("This modification is only temporary and will be reverted when the user login again.")
-	return nil
-}
-
-// UsersDisable disable user with the controller.
-func (d *DryccCmd) UsersDisable(username string) error {
-	s, err := settings.Load(d.ConfigFile)
-
-	if err != nil {
-		return err
-	}
-
-	d.Printf("Disabling user %s... ", username)
-	err = users.Disable(s.Client, username)
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-
-	d.Println("done")
-	d.Println("This modification is only temporary and will be reverted when the user login again.")
-	return nil
-}
diff --git a/cmd/users_test.go b/cmd/users_test.go
deleted file mode 100644
index ab0595935..000000000
--- a/cmd/users_test.go
+++ /dev/null
@@ -1,157 +0,0 @@
-package cmd
-
-import (
-	"bytes"
-	"fmt"
-	"net/http"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-func TestUsersList(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	server.Mux.HandleFunc("/v2/users/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-		fmt.Fprintf(w, `{
-			"count": 2,
-			"next": null,
-			"previous": null,
-			"results": [
-				{
-					"id": 2,
-					"last_login": "2014-10-19T22:01:00.601Z",
-					"is_superuser": false,
-					"username": "test",
-					"first_name": "test",
-					"last_name": "testerson",
-					"email": "test@example.com",
-					"is_staff": false,
-					"is_active": true,
-					"date_joined": "2014-10-19T22:01:00.601Z",
-					"groups": [],
-					"user_permissions": []
-				},
-				{
-					"id": 1,
-					"last_login": "2014-10-19T22:01:00.601Z",
-					"is_superuser": true,
-					"username": "jkirk",
-					"first_name": "james",
-					"last_name": "kirk",
-					"email": "jkrik@starfleet.ufp.gov",
-					"is_staff": true,
-					"is_active": true,
-					"date_joined": "2014-10-19T22:01:00.601Z",
-					"groups": [],
-					"user_permissions": []
-				}
-			]
-		}`)
-	})
-
-	err = cmdr.UsersList(-1)
-	assert.NoError(t, err)
-
-	assert.Equal(t, b.String(), `USERNAME    EMAIL                      ADMIN    STAFF    ACTIVE    DATE-JOIN                
-test        test@example.com           false    false    true      2014-10-19T22:01:00.601Z    
-jkirk       jkrik@starfleet.ufp.gov    true     true     true      2014-10-19T22:01:00.601Z    
-`, "output")
-}
-
-func TestUsersListLimit(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	server.Mux.HandleFunc("/v2/users/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-		fmt.Fprintf(w, `{
-			"count": 2,
-			"next": null,
-			"previous": null,
-			"results": [
-				{
-					"id": 2,
-					"last_login": "2014-10-19T22:01:00.601Z",
-					"is_superuser": false,
-					"username": "test",
-					"first_name": "test",
-					"last_name": "testerson",
-					"email": "test@example.com",
-					"is_staff": false,
-					"is_active": true,
-					"date_joined": "2014-10-19T22:01:00.601Z",
-					"groups": [],
-					"user_permissions": []
-				}
-			]
-		}`)
-	})
-
-	err = cmdr.UsersList(1)
-	assert.NoError(t, err)
-
-	assert.Equal(t, b.String(), `USERNAME    EMAIL               ADMIN    STAFF    ACTIVE    DATE-JOIN                
-test        test@example.com    false    false    true      2014-10-19T22:01:00.601Z    
-`, "output")
-}
-
-func TestUsersEnable(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	server.Mux.HandleFunc("/v2/users/test/enable/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-		fmt.Fprintf(w, ``)
-	})
-
-	err = cmdr.UsersEnable("test")
-	assert.NoError(t, err)
-
-	assert.Equal(t, b.String(), `Enabling user test... done
-This modification is only temporary and will be reverted when the user login again.
-`, "output")
-}
-
-func TestUsersDisable(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	server.Mux.HandleFunc("/v2/users/test/disable/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-		fmt.Fprintf(w, ``)
-	})
-
-	err = cmdr.UsersDisable("test")
-	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `Disabling user test... done
-This modification is only temporary and will be reverted when the user login again.
-`, "output")
-}
diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644
index 000000000..8bf84ecab
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,40 @@
+ARG VERSION
+ARG CODENAME
+
+FROM registry.drycc.cc/drycc/go-dev:latest AS build
+ADD . /workspace
+RUN export GO111MODULE=on \
+  && cd /workspace \
+  && sed -i "s#canary#$VERSION#g" version/version.go \
+  && CGO_ENABLED=0 init-stack go build -o /usr/local/bin/drycc drycc.go
+
+FROM registry.drycc.cc/drycc/base:$CODENAME
+
+ENV DRYCC_UID=1001 \
+  DRYCC_GID=1001 \
+  DRYCC_HOME_DIR=/workspace
+
+COPY --from=build /usr/local/bin/drycc /usr/local/bin/drycc
+
+ADD docker/rootfs /
+
+RUN install-packages bash-completion git vim-tiny \
+  && groupadd drycc --gid ${DRYCC_GID} \
+  && useradd drycc -u ${DRYCC_UID} -g ${DRYCC_GID} -s /bin/bash -m -d ${DRYCC_HOME_DIR} \
+  && mkdir -p /etc/wait \
+  && chown ${DRYCC_GID}:${DRYCC_UID} /etc/wait /usr/local/bin \
+  && echo 'alias cd="echo \"cd: restricted\"; false"' > ${DRYCC_HOME_DIR}/.bash_aliases \
+  && echo 'alias unalias="echo \"unalias: restricted\"; false"' >> ${DRYCC_HOME_DIR}/.bash_aliases \
+  && for cmd in ls git date vi cat sed clear find flock bash mkdir sleep dircolors; do \
+       ln -f "$(readlink -f "/usr/bin/$cmd")" "/usr/local/bin/$cmd"; \
+     done \
+  && apt autoremove python3 -yq \
+  && rm -rf /var/lib/apt/lists/* /sbin /usr/sbin /usr/bin
+
+ENTRYPOINT [ "/entrypoint.sh" ]
+
+ENV PATH=/usr/local/bin
+ENV DRYCC_PROFILE=/tmp/drycc.json
+
+USER ${DRYCC_UID}
+WORKDIR ${DRYCC_HOME_DIR}
diff --git a/docker/rootfs/entrypoint.sh b/docker/rootfs/entrypoint.sh
new file mode 100755
index 000000000..a88e8a1a5
--- /dev/null
+++ b/docker/rootfs/entrypoint.sh
@@ -0,0 +1,32 @@
+#!/usr/local/bin/bash
+
+if [ -n "$DRYCC_USER" ] && [ -n "$DRYCC_TOKEN" ] && [ -n "$DRYCC_ENDPOINT" ]; then
+    if [ ! -f "$DRYCC_PROFILE" ]; then
+        cat > "$DRYCC_PROFILE" <<EOF
+{
+    "username": "${DRYCC_USER}",
+    "ssl_verify": ${DRYCC_SSL_VERIFY:-true},
+    "controller": "${DRYCC_ENDPOINT}",
+    "token": "${DRYCC_TOKEN}",
+    "response_limit": ${DRYCC_RESPONSE_LIMIT:-0}
+}
+EOF
+    fi
+fi
+
+_main() {
+    if [ "$1" == 'bash' ]; then
+        shift
+        exec bash "$@"
+    elif [ "$1" == 'wait' ]; then
+        shift
+        /etc/bash_completion.d/timeout
+        while [ "$(date +%s)" -le "$(flock -x -w 10 /tmp/timeout.lock cat /etc/wait/timeout)" ]; do
+            sleep ${TIMEOUT:-30}
+        done
+    else
+        exec drycc "$@"
+    fi
+}
+
+_main "$@"
diff --git a/docker/rootfs/etc/bash_completion.d/drycc b/docker/rootfs/etc/bash_completion.d/drycc
new file mode 100755
index 000000000..21419326a
--- /dev/null
+++ b/docker/rootfs/etc/bash_completion.d/drycc
@@ -0,0 +1,3 @@
+#!/usr/local/bin/bash
+
+source <(drycc completion bash)
diff --git a/docker/rootfs/etc/bash_completion.d/ps1 b/docker/rootfs/etc/bash_completion.d/ps1
new file mode 100755
index 000000000..cba7dbb7e
--- /dev/null
+++ b/docker/rootfs/etc/bash_completion.d/ps1
@@ -0,0 +1,3 @@
+#!/usr/local/bin/bash
+
+export PS1="\e[32m> \e[0m"
diff --git a/docker/rootfs/etc/bash_completion.d/timeout b/docker/rootfs/etc/bash_completion.d/timeout
new file mode 100755
index 000000000..078055e9f
--- /dev/null
+++ b/docker/rootfs/etc/bash_completion.d/timeout
@@ -0,0 +1,5 @@
+#!/usr/local/bin/bash
+
+flock -x -w 10 /tmp/timeout.lock cat << EOF > /etc/wait/timeout
+$(($(date +%s) + ${TIMEOUT:-30}))
+EOF
\ No newline at end of file
diff --git a/drycc.go b/drycc.go
index 1dcebe678..c2392f9b3 100644
--- a/drycc.go
+++ b/drycc.go
@@ -1,295 +1,20 @@
+/*
+Copyright © 2025 NAME HERE <EMAIL ADDRESS>
+*/
 package main
 
 import (
-	"fmt"
-	"io"
+	"context"
 	"os"
-	"os/exec"
-	"strings"
-	"syscall"
 
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cli"
 	"github.com/drycc/workflow-cli/cmd"
-	"github.com/drycc/workflow-cli/parser"
 )
 
-const extensionPrefix = "drycc-"
-
-// main exits with the return value of Command(os.Args[1:]), deferring all logic to
-// a func we can test.
 func main() {
-	os.Exit(Command(os.Args[1:], os.Stdout, os.Stderr, os.Stdin))
-}
-
-// Command routes drycc commands to their proper parser.
-func Command(argv []string, wOut io.Writer, wErr io.Writer, wIn io.Reader) int {
-	usage := `
-The Drycc command-line client issues API calls to a Drycc controller.
-
-Usage: drycc <command> [<args>...]
-
-Options:
-  -h --help
-    display help information
-  -v --version
-    display client version
-  -c --config=<config>
-    path to configuration file. Equivalent to
-    setting $DRYCC_PROFILE. Defaults to ~/.drycc/config.json.
-    If value is not a filepath, will assume location ~/.drycc/client.json
-
-Auth commands, use 'drycc help auth' to learn more:
-
-  register      register a new user with a controller
-  login         login to a controller
-  logout        logout from the current controller
-
-Subcommands, use 'drycc help [subcommand]' to learn more:
-
-  apps          manage applications used to provide services
-  autoscale     manage autoscale for applications
-  builds        manage builds created using 'git push'
-  canary        manage canary deploy for applications
-  certs         manage SSL endpoints for an app
-  config        manage environment variables that define app config
-  domains       manage and assign domain names to your applications
-  git           manage git for applications
-  healthchecks  manage healthchecks for applications
-  keys          manage ssh keys used for 'git push' deployments
-  labels        manage labels of application
-  limits        manage resource limits for your application
-  perms         manage permissions for applications
-  ps            manage processes inside an app container
-  registry      manage private registry information for your application
-  releases      manage releases of an application
-  routing       manage routability of an application
-  tags          manage tags for application containers
-  tls           manage TLS settings for applications
-  users         manage users
-  tokens        manage tokens
-  version       display client version
-  services      manage services for your applications
-  routes        manage routes for your applications
-  gateways      manage gateways for your applications
-  timeouts      manage pods termination grace period
-  volumes       manage volumes for your applications
-  resources     manage resources for your applications
-
-Shortcut commands, use 'drycc shortcuts' to see all:
-
-  create        create a new application
-  destroy       destroy an application
-  info          view information about the current app
-  logs          view aggregated log info for the app
-  open          open a URL to the app in a browser
-  pull          imports an image and deploys as a new release
-  run           run a command in an ephemeral app container
-  scale         scale processes by type (web=2, worker=1)
-
-Use 'git push drycc main' to deploy to an application.
-`
-	// Reorganize some command line flags and commands.
-	command, argv := parseArgs(argv)
-	// Give docopt an optional final false arg so it doesn't call os.Exit().
-	_, err := docopt.ParseArgs(usage, []string{command}, "")
-
-	if err != nil {
-		fmt.Fprintln(wErr, err)
-		return 1
-	}
-
-	if len(argv) == 0 {
-		fmt.Fprintln(wErr, "Usage: drycc <command> [<args>...]")
-		return 1
-	}
-
-	configFlag := getConfigFlag(argv)
-	// Don't pass down config flag to parser because it isn't defined there.
-	argv = removeConfigFlag(argv)
-	cmdr := cmd.DryccCmd{ConfigFile: configFlag, WOut: wOut, WErr: wErr, WIn: wIn}
-
-	// Dispatch the command, passing the argv through so subcommands can
-	// re-parse it according to their usage strings.
-	switch command {
-	case "apps":
-		err = parser.Apps(argv, &cmdr)
-	case "auth":
-		err = parser.Auth(argv, &cmdr)
-	case "autoscale":
-		err = parser.Autoscale(argv, &cmdr)
-	case "builds":
-		err = parser.Builds(argv, &cmdr)
-	case "canary":
-		err = parser.Canary(argv, &cmdr)
-	case "certs":
-		err = parser.Certs(argv, &cmdr)
-	case "config":
-		err = parser.Config(argv, &cmdr)
-	case "domains":
-		err = parser.Domains(argv, &cmdr)
-	case "services":
-		err = parser.Services(argv, &cmdr)
-	case "gateways":
-		err = parser.Gateways(argv, &cmdr)
-	case "routes":
-		err = parser.Routes(argv, &cmdr)
-	case "git":
-		err = parser.Git(argv, &cmdr)
-	case "healthchecks":
-		err = parser.Healthchecks(argv, &cmdr)
-	case "help":
-		fmt.Fprint(os.Stdout, usage)
-		return 0
-	case "keys":
-		err = parser.Keys(argv, &cmdr)
-	case "labels":
-		err = parser.Labels(argv, &cmdr)
-	case "limits":
-		err = parser.Limits(argv, &cmdr)
-	case "timeouts":
-		err = parser.Timeouts(argv, &cmdr)
-	case "perms":
-		err = parser.Perms(argv, &cmdr)
-	case "ps":
-		err = parser.Ps(argv, &cmdr)
-	case "registry":
-		err = parser.Registry(argv, &cmdr)
-	case "releases":
-		err = parser.Releases(argv, &cmdr)
-	case "routing":
-		err = parser.Routing(argv, &cmdr)
-	case "shortcuts":
-		err = parser.Shortcuts(argv, &cmdr)
-	case "tags":
-		err = parser.Tags(argv, &cmdr)
-	case "tls":
-		err = parser.TLS(argv, &cmdr)
-	case "users":
-		err = parser.Users(argv, &cmdr)
-	case "tokens":
-		err = parser.Tokens(argv, &cmdr)
-	case "version":
-		err = parser.Version(argv, &cmdr)
-	case "volumes":
-		err = parser.Volumes(argv, &cmdr)
-	case "resources":
-		err = parser.Resources(argv, &cmdr)
-	default:
-		env := os.Environ()
-
-		binary, err := exec.LookPath(extensionPrefix + command)
-		if err != nil {
-			parser.PrintUsage(&cmdr)
-			return 1
-		}
-
-		cmdArgv := prepareCmdArgs(command, argv)
-
-		err = syscall.Exec(binary, cmdArgv, env)
-		if err != nil {
-			parser.PrintUsage(&cmdr)
-			return 1
-		}
-	}
-	if err != nil {
-		fmt.Fprintf(wErr, "Error: %v\n", err)
-		return 1
-	}
-	return 0
-}
-
-func removeConfigFlag(argv []string) []string {
-	var kept []string
-	for i, arg := range argv {
-		// -- /bin/sh -c --config  condition
-		if arg == "--" {
-			kept = append(kept, argv[i:]...)
-			break
-		}
-
-		if arg == "-c" || strings.HasPrefix(arg, "--config=") {
-			continue
-			// If the previous option is -c, remove the argument as well
-		} else if i != 0 && argv[i-1] == "-c" {
-			continue
-		}
-
-		kept = append(kept, arg)
-	}
-
-	return kept
-}
-
-func getConfigFlag(argv []string) string {
-	for i, arg := range argv {
-		// -- /bin/sh -c/ --config=  condition
-		if arg == "--" {
-			return ""
-		}
-		if strings.HasPrefix(arg, "--config=") {
-			return strings.TrimPrefix(arg, "--config=")
-		} else if i != 0 && argv[i-1] == "-c" {
-			return arg
-		}
-	}
-
-	return ""
-}
-
-// parseArgs returns the provided args with "--help" as the last arg if need be,
-// expands shortcuts and formats commands to be properly routed.
-func parseArgs(argv []string) (string, []string) {
-	if len(argv) == 1 {
-		if argv[0] == "--help" || argv[0] == "-h" {
-			// rearrange "drycc --help" as "drycc help"
-			argv[0] = "help"
-		} else if argv[0] == "--version" || argv[0] == "-v" {
-			// rearrange "drycc --version" as "drycc version"
-			argv[0] = "version"
-		}
-	}
-
-	if len(argv) > 1 {
-		// Rearrange "drycc help <command>" to "drycc <command> --help".
-		if argv[0] == "help" || argv[0] == "--help" || argv[0] == "-h" {
-			argv = append(argv[1:], "--help")
-		}
-	}
-
-	if len(argv) > 0 {
-		argv[0] = replaceShortcut(argv[0])
-
-		index := strings.Index(argv[0], ":")
-
-		if index != -1 {
-			command := argv[0]
-			return command[:index], argv
-		}
-
-		return argv[0], argv
+	rootCmd := cmd.NewDryccCommand()
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+	if err := rootCmd.ExecuteContext(ctx); err != nil {
+		os.Exit(1)
 	}
-
-	return "", argv
-}
-
-// split original command and pass its first element in arguments
-func prepareCmdArgs(command string, argv []string) []string {
-	cmdArgv := []string{extensionPrefix + command}
-	cmdSplit := strings.Split(argv[0], command+":")
-
-	if len(cmdSplit) > 1 {
-		cmdArgv = append(cmdArgv, cmdSplit[1])
-	}
-
-	return append(cmdArgv, argv[1:]...)
-}
-
-func replaceShortcut(command string) string {
-	expandedCommand := cli.Shortcuts[command]
-	if expandedCommand == "" {
-		return command
-	}
-
-	return expandedCommand
 }
diff --git a/drycc_test.go b/drycc_test.go
deleted file mode 100644
index 0e03150e5..000000000
--- a/drycc_test.go
+++ /dev/null
@@ -1,205 +0,0 @@
-package main
-
-import (
-	"reflect"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func TestHelpReformatting(t *testing.T) {
-	t.Parallel()
-
-	tests := []string{"--help", "-h", "help"}
-	expected := "help"
-
-	for _, test := range tests {
-		actual, argv := parseArgs([]string{test})
-
-		if actual != expected {
-			t.Errorf("Expected %s, Got %s", expected, actual)
-		}
-
-		if len(argv) != 1 {
-			t.Errorf("Expected length of 1, Got %d", len(argv))
-		}
-	}
-}
-
-func TestHelpReformattingWithCommand(t *testing.T) {
-	t.Parallel()
-
-	tests := []string{"--help", "-h", "help"}
-	expected := "test"
-	expectedArgv := []string{"test", "--help"}
-
-	for _, test := range tests {
-		actual, argv := parseArgs([]string{test, "test"})
-
-		if actual != expected {
-			t.Errorf("Expected %s, Got %s", expected, actual)
-		}
-
-		if !reflect.DeepEqual(expectedArgv, argv) {
-			t.Errorf("Expected %v, Got %v", expectedArgv, argv)
-		}
-	}
-}
-
-func TestCommandSplitting(t *testing.T) {
-	t.Parallel()
-
-	expected := "apps"
-	expectedArgv := []string{"apps:create", "test", "foo"}
-
-	actual, argv := parseArgs(expectedArgv)
-
-	if actual != expected {
-		t.Errorf("Expected %s, Got %s", expected, actual)
-	}
-
-	if !reflect.DeepEqual(expectedArgv, argv) {
-		t.Errorf("Expected %v, Got %v", expectedArgv, argv)
-	}
-}
-
-func TestTopLevelCommandArgsPreparing(t *testing.T) {
-	t.Parallel()
-
-	command := "ssh"
-	argv := []string{"ssh"}
-	expected := []string{"drycc-ssh"}
-	actual := prepareCmdArgs(command, argv)
-
-	if !reflect.DeepEqual(expected, actual) {
-		t.Errorf("Expected %v, Got %v", expected, actual)
-	}
-}
-
-func TestCommandWithParameterArgsPreparing(t *testing.T) {
-	t.Parallel()
-
-	command := "ssh --help"
-	argv := []string{"ssh --help"}
-	expected := []string{"drycc-ssh --help"}
-	actual := prepareCmdArgs(command, argv)
-
-	if !reflect.DeepEqual(expected, actual) {
-		t.Errorf("Expected %v, Got %v", expected, actual)
-	}
-}
-
-func TestReplaceShortcutRepalce(t *testing.T) {
-	t.Parallel()
-
-	expected := "apps:create"
-	actual := replaceShortcut("create")
-
-	if expected != actual {
-		t.Errorf("Expected %s, Got %s", expected, actual)
-	}
-}
-
-func TestReplaceShortcutUnchanged(t *testing.T) {
-	t.Parallel()
-
-	expected := "users:list"
-	actual := replaceShortcut(expected)
-
-	if expected != actual {
-		t.Errorf("Expected %s, Got %s", expected, actual)
-	}
-}
-
-func TestGetConfigFlag(t *testing.T) {
-	t.Parallel()
-
-	expected := "the-config-flag"
-	argv := []string{
-		"lorem",
-		"ipsum",
-		"--config=" + expected,
-	}
-	actual := getConfigFlag(argv)
-	assert.Equal(t, actual, expected, "config-flag")
-
-	argv = []string{
-		"lorem",
-		"ipsum",
-		"-c",
-		expected,
-	}
-	actual = getConfigFlag(argv)
-	assert.Equal(t, actual, expected, "config-flag")
-
-	expected = ""
-	argv = []string{
-		"--",
-		"ipsum",
-		"--config=config",
-	}
-	actual = getConfigFlag(argv)
-	assert.Equal(t, actual, expected, "config-flag")
-
-	argv = []string{
-		"--",
-		"ipsum",
-		"-c",
-		"config",
-	}
-	actual = getConfigFlag(argv)
-	assert.Equal(t, actual, expected, "config-flag")
-}
-
-func TestRemoveConfigFlag(t *testing.T) {
-	t.Parallel()
-	expected := []string{
-		"lorem",
-		"ipsum",
-	}
-
-	argv := []string{
-		"lorem",
-		"ipsum",
-		"--config=the-config-flag",
-	}
-	actual := removeConfigFlag(argv)
-	assert.Equal(t, actual, expected, "args")
-
-	argv = []string{
-		"lorem",
-		"ipsum",
-		"-c",
-		"the-config-flag",
-	}
-	actual = removeConfigFlag(argv)
-	assert.Equal(t, actual, expected, "args")
-
-	expected = []string{
-		"--",
-		"sh",
-		"-c",
-		"the-config-flag",
-	}
-	argv = []string{
-		"--",
-		"sh",
-		"-c",
-		"the-config-flag",
-	}
-	actual = removeConfigFlag(argv)
-	assert.Equal(t, actual, expected, "args")
-
-	expected = []string{
-		"--",
-		"sh",
-		"--config=the-config-flag",
-	}
-	argv = []string{
-		"--",
-		"sh",
-		"--config=the-config-flag",
-	}
-	actual = removeConfigFlag(argv)
-	assert.Equal(t, actual, expected, "args")
-}
diff --git a/go.mod b/go.mod
index 1b80bff12..e6e4710df 100644
--- a/go.mod
+++ b/go.mod
@@ -1,23 +1,33 @@
 module github.com/drycc/workflow-cli
 
-go 1.22
+go 1.25
 
 require (
+	github.com/chai2010/gettext-go v1.0.3
 	github.com/containerd/console v1.0.4
-	github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
-	github.com/drycc/controller-sdk-go v0.0.0-20240430013837-6be5299a98ee
-	github.com/drycc/pkg v0.0.0-20240225112316-78fc9239f51f
+	github.com/drycc/controller-sdk-go v0.0.0-20260324023744-f5a3de0a532e
+	github.com/drycc/pkg v0.0.0-20250917064731-345368da3dbf
+	github.com/minio/selfupdate v0.6.0
 	github.com/olekukonko/tablewriter v0.0.5
-	github.com/stretchr/testify v1.9.0
-	golang.org/x/exp v0.0.0-20240213143201-ec583247a57a
-	golang.org/x/net v0.23.0
+	github.com/spf13/cobra v1.9.1
+	github.com/stretchr/testify v1.10.0
+	golang.org/x/net v0.47.0
 	gopkg.in/yaml.v3 v3.0.1
+	k8s.io/klog/v2 v2.130.1
 	sigs.k8s.io/yaml v1.4.0
 )
 
 require (
-	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/mattn/go-runewidth v0.0.9 // indirect
-	github.com/pmezard/go-difflib v1.0.0 // indirect
-	golang.org/x/sys v0.18.0 // indirect
+	aead.dev/minisign v0.3.0 // indirect
+	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+	github.com/go-logr/logr v1.4.1 // indirect
+	github.com/google/go-cmp v0.6.0 // indirect
+	github.com/inconshreveable/mousetrap v1.1.0 // indirect
+	github.com/kr/pretty v0.3.1 // indirect
+	github.com/mattn/go-runewidth v0.0.16 // indirect
+	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+	github.com/rivo/uniseg v0.4.7 // indirect
+	github.com/spf13/pflag v1.0.6 // indirect
+	golang.org/x/crypto v0.45.0 // indirect
+	golang.org/x/sys v0.38.0 // indirect
 )
diff --git a/go.sum b/go.sum
index ec4c59c07..5b91e5cea 100644
--- a/go.sum
+++ b/go.sum
@@ -1,33 +1,80 @@
+aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ=
+aead.dev/minisign v0.3.0 h1:8Xafzy5PEVZqYDNP60yJHARlW1eOQtsKNp/Ph2c0vRA=
+aead.dev/minisign v0.3.0/go.mod h1:NLvG3Uoq3skkRMDuc3YHpWUTMTrSExqm+Ij73W13F6Y=
+github.com/chai2010/gettext-go v1.0.3 h1:9liNh8t+u26xl5ddmWLmsOsdNLwkdRTg5AG+JnTiM80=
+github.com/chai2010/gettext-go v1.0.3/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA=
 github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
 github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
-github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
-github.com/drycc/controller-sdk-go v0.0.0-20240430013837-6be5299a98ee h1:GU8eDN2GrkhcQa3MnBMTJZmJrhZXPbo/rtAQArrzpeo=
-github.com/drycc/controller-sdk-go v0.0.0-20240430013837-6be5299a98ee/go.mod h1:n6eQe1irJqjwLo/7t9+Dhdv6faSESQN+ATnZRBP3/Uc=
-github.com/drycc/pkg v0.0.0-20240225112316-78fc9239f51f h1:kgjvUQJeAszDoU1Vo4vTTE92KI8Av3JPb6Qn890niXg=
-github.com/drycc/pkg v0.0.0-20240225112316-78fc9239f51f/go.mod h1:n+QxGif6ha9CEoxVnlipxb9IdmerybcUSzTEDFkvjiA=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/drycc/controller-sdk-go v0.0.0-20260324023744-f5a3de0a532e h1:4GH098Zz4VsTvk8KfLHQQTP5f/O41i7dk+h/GOYiDy8=
+github.com/drycc/controller-sdk-go v0.0.0-20260324023744-f5a3de0a532e/go.mod h1:eHcmYwg81ASlP55/U587xnBZnZoeZnPHXGeQ8nYWnsg=
+github.com/drycc/pkg v0.0.0-20250917064731-345368da3dbf h1:CYy3NoPhfFhkGAbEppTOQfY/HC2s0FJDcBgbtRKeweg=
+github.com/drycc/pkg v0.0.0-20250917064731-345368da3dbf/go.mod h1:BrrNrNskHKm+nJYhXfGuI114w8nupi0AMo8QZHID7CM=
+github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
+github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDwU=
+github.com/minio/selfupdate v0.6.0/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM=
 github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
-golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
-golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
-golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
+github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
+github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
+github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
+golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
+golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
+golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
-golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
+golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
+k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
 sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
 sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/cmd/apps.go b/internal/commands/apps.go
similarity index 75%
rename from cmd/apps.go
rename to internal/commands/apps.go
index f14718202..3c828870c 100644
--- a/cmd/apps.go
+++ b/internal/commands/apps.go
@@ -1,28 +1,24 @@
-package cmd
+// Package commands provides implementations for Drycc CLI commands.
+package commands
 
 import (
 	"fmt"
-	"io"
-	"log"
-	"os"
 	"regexp"
 	"strings"
 	"time"
 
-	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/apps"
 	"github.com/drycc/controller-sdk-go/appsettings"
 	"github.com/drycc/controller-sdk-go/domains"
 	"github.com/drycc/controller-sdk-go/ps"
+	"github.com/drycc/workflow-cli/internal/loader"
 	"github.com/drycc/workflow-cli/pkg/git"
-	"github.com/drycc/workflow-cli/pkg/logging"
+	"github.com/drycc/workflow-cli/pkg/settings"
 	"github.com/drycc/workflow-cli/pkg/webbrowser"
-	"github.com/drycc/workflow-cli/settings"
-	"golang.org/x/net/websocket"
 )
 
 // AppCreate creates an app.
-func (d *DryccCmd) AppCreate(id, remote string, noRemote bool) error {
+func (d *DryccCmd) AppCreate(id, workspace, remote string, noRemote bool) error {
 	s, err := settings.Load(d.ConfigFile)
 	if err != nil {
 		return err
@@ -30,7 +26,7 @@ func (d *DryccCmd) AppCreate(id, remote string, noRemote bool) error {
 
 	d.Print("Creating Application... ")
 	quit := progress(d.WOut)
-	app, err := apps.New(s.Client, id)
+	app, err := apps.New(s.Client, id, workspace)
 
 	quit <- true
 	<-quit
@@ -45,7 +41,7 @@ func (d *DryccCmd) AppCreate(id, remote string, noRemote bool) error {
 		if err = git.CreateRemote(git.DefaultCmd, s.Client.ControllerURL.Host, remote, app.ID); err != nil {
 			if strings.Contains(err.Error(), fmt.Sprintf("error: remote %s already exists.", remote)) {
 				msg := "A git remote with the name %s already exists. To overwrite this remote run:\n"
-				msg += "drycc git:remote --force --remote %s --app %s"
+				msg += "drycc git remote --force --remote %s --app %s"
 				return fmt.Errorf(msg, remote, remote, app.ID)
 			}
 			return err
@@ -55,7 +51,7 @@ func (d *DryccCmd) AppCreate(id, remote string, noRemote bool) error {
 	}
 
 	if noRemote {
-		d.Printf("If you want to add a git remote for this app later, use `drycc git:remote -a %s`\n", app.ID)
+		d.Printf("If you want to add a git remote for this app later, use `drycc git remote -a %s`\n", app.ID)
 	}
 
 	return nil
@@ -64,7 +60,6 @@ func (d *DryccCmd) AppCreate(id, remote string, noRemote bool) error {
 // AppsList lists apps on the Drycc controller.
 func (d *DryccCmd) AppsList(results int) error {
 	s, err := settings.Load(d.ConfigFile)
-
 	if err != nil {
 		return err
 	}
@@ -78,14 +73,13 @@ func (d *DryccCmd) AppsList(results int) error {
 		return err
 	}
 	if count > 0 {
-		table := d.getDefaultFormatTable([]string{"ID", "UUID", "OWNER", "CREATED", "UPDATED"})
+		table := d.getDefaultFormatTable([]string{"ID", "WORKSPACE", "CREATED", "UPDATED"})
 		for _, app := range apps {
 			table.Append([]string{
 				app.ID,
-				app.UUID,
-				app.Owner,
-				app.Created,
-				app.Updated,
+				app.Workspace,
+				d.formatTime(app.Created),
+				d.formatTime(app.Updated),
 			})
 		}
 		table.Render()
@@ -97,8 +91,7 @@ func (d *DryccCmd) AppsList(results int) error {
 
 // AppInfo prints info about app.
 func (d *DryccCmd) AppInfo(appID string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -117,9 +110,9 @@ func (d *DryccCmd) AppInfo(appID string) error {
 	table.Append([]string{"App:", app.ID})
 	table.Append([]string{"URL:", url})
 	table.Append([]string{"UUID:", app.UUID})
-	table.Append([]string{"Owner:", app.Owner})
-	table.Append([]string{"Created:", app.Created})
-	table.Append([]string{"Updated:", app.Updated})
+	table.Append([]string{"Workspace:", app.Workspace})
+	table.Append([]string{"Created:", d.formatTime(app.Created)})
+	table.Append([]string{"Updated:", d.formatTime(app.Updated)})
 
 	// print the app processes
 	processes, _, err := ps.List(s.Client, appID, defaultLimit)
@@ -133,8 +126,8 @@ func (d *DryccCmd) AppInfo(appID string) error {
 			table.Append([]string{"", "Name:", process.Name})
 			table.Append([]string{"", "Release:", process.Release})
 			table.Append([]string{"", "State:", process.State})
-			table.Append([]string{"", "Type:", process.Type})
-			table.Append([]string{"", "Started:", process.Started.Format("2006-01-02T15:04:05MST")})
+			table.Append([]string{"", "Ptype:", process.Type})
+			table.Append([]string{"", "Started:", d.formatTime(process.Started)})
 			if len(processes) > index+1 {
 				table.Append([]string{""})
 			}
@@ -151,8 +144,8 @@ func (d *DryccCmd) AppInfo(appID string) error {
 		table.Append([]string{"Domains:"})
 		for index, domain := range domains {
 			table.Append([]string{"", "Domain:", domain.Domain})
-			table.Append([]string{"", "Created:", domain.Created})
-			table.Append([]string{"", "Updated:", domain.Updated})
+			table.Append([]string{"", "Created:", d.formatTime(domain.Created)})
+			table.Append([]string{"", "Updated:", d.formatTime(domain.Updated)})
 			if len(domains) > index+1 {
 				table.Append([]string{""})
 			}
@@ -183,8 +176,7 @@ func (d *DryccCmd) AppInfo(appID string) error {
 
 // AppOpen opens an app in the default webbrowser.
 func (d *DryccCmd) AppOpen(appID string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -198,48 +190,16 @@ func (d *DryccCmd) AppOpen(appID string) error {
 		return fmt.Errorf(noDomainAssignedMsg, appID)
 	}
 
-	if !(strings.HasPrefix(u, "http://") || strings.HasPrefix(u, "https://")) {
+	if !strings.HasPrefix(u, "http://") && !strings.HasPrefix(u, "https://") {
 		u = "http://" + u
 	}
 
 	return webbrowser.Webbrowser(u)
 }
 
-// AppLogs returns the logs from an app.
-func (d *DryccCmd) AppLogs(appID string, lines int, follow bool, timeout int) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
-	if err != nil {
-		return err
-	}
-	request := api.AppLogsRequest{
-		Lines:   lines,
-		Follow:  follow,
-		Timeout: timeout,
-	}
-	conn, err := apps.Logs(s.Client, appID, request)
-	if err != nil {
-		return err
-	}
-	defer conn.Close()
-	for {
-		var message string
-		err := websocket.Message.Receive(conn, &message)
-		if err != nil {
-			if err != io.EOF {
-				log.Printf("error: %v", err)
-			}
-			break
-		}
-		logging.PrintLog(os.Stdout, strings.TrimRight(string(message), "\n"))
-	}
-	return nil
-}
-
 // AppRun runs a one time command in the app.
 func (d *DryccCmd) AppRun(appID, command string, volumeVars []string, timeout, expires uint32) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -256,8 +216,8 @@ func (d *DryccCmd) AppRun(appID, command string, volumeVars []string, timeout, e
 	return nil
 }
 
-func parseMount(volumeVars []string) (map[string]interface{}, error) {
-	volumeMap := make(map[string]interface{})
+func parseMount(volumeVars []string) (map[string]any, error) {
+	volumeMap := make(map[string]any)
 
 	regex := regexp.MustCompile(`^([A-z_]+[A-z0-9_]*):([\s\S]*)$`)
 	for _, volume := range volumeVars {
@@ -276,14 +236,12 @@ func (d *DryccCmd) AppDestroy(appID, confirm string) error {
 	gitSession := false
 
 	s, err := settings.Load(d.ConfigFile)
-
 	if err != nil {
 		return err
 	}
 
 	if appID == "" {
 		appID, err = git.DetectAppName(git.DefaultCmd, s.Client.ControllerURL.Host)
-
 		if err != nil {
 			return err
 		}
@@ -321,17 +279,16 @@ func (d *DryccCmd) AppDestroy(appID, confirm string) error {
 	return nil
 }
 
-// AppTransfer transfers app ownership to another user.
-func (d *DryccCmd) AppTransfer(appID, username string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+// AppTransfer transfers app to another workspace.
+func (d *DryccCmd) AppTransfer(appID, workspace string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
 
-	d.Printf("Transferring %s to %s... ", appID, username)
+	d.Printf("Transferring %s to %s... ", appID, workspace)
 
-	err = apps.Transfer(s.Client, appID, username)
+	err = apps.Transfer(s.Client, appID, workspace)
 	if d.checkAPICompatibility(s.Client, err) != nil {
 		return err
 	}
diff --git a/cmd/apps_test.go b/internal/commands/apps_test.go
similarity index 74%
rename from cmd/apps_test.go
rename to internal/commands/apps_test.go
index bc41a0c68..ab5e430fd 100644
--- a/cmd/apps_test.go
+++ b/internal/commands/apps_test.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"bytes"
@@ -7,11 +7,10 @@ import (
 	"os"
 	"testing"
 
-	"github.com/stretchr/testify/assert"
-
 	"github.com/drycc/workflow-cli/pkg/git"
+	"github.com/drycc/workflow-cli/pkg/settings"
 	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/drycc/workflow-cli/settings"
+	"github.com/stretchr/testify/assert"
 )
 
 type expandURLCases struct {
@@ -39,7 +38,7 @@ func TestAppsList(t *testing.T) {
 			    {
 					"uuid": "c4aed81c-d1ca-4ff1-ab89-d2151264e1a3",
 					"id": "lorem-ipsum",
-					"owner": "dolar-sit-amet",
+					"workspace": "dolar-sit-amet",
 					"created": "2016-08-22T17:40:16Z",
 					"updated": "2016-08-22T17:40:16Z",
 					"structure": {
@@ -49,7 +48,7 @@ func TestAppsList(t *testing.T) {
 				{
 					"uuid": "c4aed81c-d1ca-4ff1-ab89-d2151264e1a3",
 					"id": "consectetur",
-					"owner": "adipiscing",
+					"workspace": "adipiscing",
 					"created": "2016-08-22T17:40:16Z",
 					"updated": "2016-08-22T17:40:16Z",
 					"structure": {
@@ -62,10 +61,10 @@ func TestAppsList(t *testing.T) {
 
 	err = cmdr.AppsList(-1)
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `ID             UUID                                    OWNER             CREATED                 UPDATED              
-lorem-ipsum    c4aed81c-d1ca-4ff1-ab89-d2151264e1a3    dolar-sit-amet    2016-08-22T17:40:16Z    2016-08-22T17:40:16Z    
-consectetur    c4aed81c-d1ca-4ff1-ab89-d2151264e1a3    adipiscing        2016-08-22T17:40:16Z    2016-08-22T17:40:16Z    
-`, "output")
+	testutil.AssertOutput(t, b.String(), `ID             WORKSPACE         CREATED                 UPDATED
+lorem-ipsum    dolar-sit-amet    2016-08-22T17:40:16Z    2016-08-22T17:40:16Z
+consectetur    adipiscing        2016-08-22T17:40:16Z    2016-08-22T17:40:16Z
+`)
 }
 
 func TestAppsListLimit(t *testing.T) {
@@ -88,7 +87,7 @@ func TestAppsListLimit(t *testing.T) {
 			    {
 					"uuid": "c4aed81c-d1ca-4ff1-ab89-d2151264e1a3",
 					"id": "lorem-ipsum",
-					"owner": "dolar-sit-amet",
+					"workspace": "dolar-sit-amet",
 					"created": "2016-08-22T17:40:16Z",
 					"updated": "2016-08-22T17:40:16Z",
 					"structure": {
@@ -101,9 +100,9 @@ func TestAppsListLimit(t *testing.T) {
 
 	err = cmdr.AppsList(1)
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `ID             UUID                                    OWNER             CREATED                 UPDATED              
-lorem-ipsum    c4aed81c-d1ca-4ff1-ab89-d2151264e1a3    dolar-sit-amet    2016-08-22T17:40:16Z    2016-08-22T17:40:16Z    
-`, "output")
+	testutil.AssertOutput(t, b.String(), `ID             WORKSPACE         CREATED                 UPDATED
+lorem-ipsum    dolar-sit-amet    2016-08-22T17:40:16Z    2016-08-22T17:40:16Z
+`)
 }
 
 func TestAppsInfo(t *testing.T) {
@@ -121,7 +120,7 @@ func TestAppsInfo(t *testing.T) {
 		fmt.Fprintf(w, `{
     "uuid": "c4aed81c-d1ca-4ff1-ab89-d2151264e1a3",
     "id": "lorem-ipsum",
-    "owner": "dolar-sit-amet",
+    "workspace": "dolar-sit-amet",
     "structure": {
       "cmd": 1
     },
@@ -187,26 +186,26 @@ func TestAppsInfo(t *testing.T) {
 	}
 	err = cmdr.AppInfo("lorem-ipsum")
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `App:          lorem-ipsum                             
-URL:          `+url+`                 
-UUID:         c4aed81c-d1ca-4ff1-ab89-d2151264e1a3    
-Owner:        dolar-sit-amet                          
-Created:      2016-08-22T17:40:16Z                    
-Updated:      2016-08-22T17:40:16Z                    
-Processes:    
-              Name:                                   lorem-ipsum-cmd-1911796442-48b58    
-              Release:                                v2                                  
-              State:                                  up                                  
-              Type:                                   cmd                                 
-              Started:                                2016-08-22T17:42:16UTC              
-Domains:      
-              Domain:                                 lorem-ipsum                         
-              Created:                                2016-08-22T17:40:16Z                
-              Updated:                                2016-08-22T17:40:16Z                
-Labels:       
-              Key:                                    team                                
-              Value:                                  frontend                            
-`, "output")
+	testutil.AssertOutput(t, b.String(), `App:          lorem-ipsum
+URL:          `+url+`
+UUID:         c4aed81c-d1ca-4ff1-ab89-d2151264e1a3
+Workspace:    dolar-sit-amet
+Created:      2016-08-22T17:40:16Z
+Updated:      2016-08-22T17:40:16Z
+Processes:
+              Name:                                   lorem-ipsum-cmd-1911796442-48b58
+              Release:                                v2
+              State:                                  up
+              Ptype:                                  cmd
+              Started:                                2016-08-22T17:42:16Z
+Domains:
+              Domain:                                 lorem-ipsum
+              Created:                                2016-08-22T17:40:16Z
+              Updated:                                2016-08-22T17:40:16Z
+Labels:
+              Key:                                    team
+              Value:                                  frontend
+`)
 }
 
 func TestAppDestroy(t *testing.T) {
@@ -224,7 +223,7 @@ func TestAppDestroy(t *testing.T) {
 		fmt.Fprintf(w, `{
     "uuid": "c4aed81c-d1ca-4ff1-ab89-d2151264e1a3",
     "id": "lorem-ipsum",
-    "owner": "dolar-sit-amet",
+    "workspace": "dolar-sit-amet",
     "structure": {
       "cmd": 1
     },
@@ -237,7 +236,7 @@ func TestAppDestroy(t *testing.T) {
 
 	err = cmdr.AppDestroy("lorem-ipsum", "lorem-ipsum")
 	assert.NoError(t, err)
-	assert.Equal(t, testutil.StripProgress(b.String()), `Destroying lorem-ipsum...
+	assert.Equal(t, b.String(), `Destroying lorem-ipsum...
 done in 0s
 `, "output")
 }
@@ -257,7 +256,7 @@ func TestAppTransfer(t *testing.T) {
 		fmt.Fprintf(w, `{
     "uuid": "c4aed81c-d1ca-4ff1-ab89-d2151264e1a3",
     "id": "lorem-ipsum",
-    "owner": "dolar-sit-amet",
+    "workspace": "dolar-sit-amet",
     "structure": {
       "cmd": 1
     },
@@ -268,7 +267,7 @@ func TestAppTransfer(t *testing.T) {
 
 	err = cmdr.AppTransfer("lorem-ipsum", "test-user")
 	assert.NoError(t, err)
-	assert.Equal(t, testutil.StripProgress(b.String()), `Transferring lorem-ipsum to test-user... done
+	assert.Equal(t, b.String(), `Transferring lorem-ipsum to test-user... done
 `, "output")
 }
 
@@ -325,9 +324,11 @@ func TestRemoteExists(t *testing.T) {
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
-	err = cmdr.AppCreate("foo", "drycc", false)
+	err = cmdr.AppCreate("foo", "", "drycc", false)
 
-	assert.Equal(t, err.Error(), `A git remote with the name drycc already exists. To overwrite this remote run:
-drycc git:remote --force --remote drycc --app foo`,
-		"output")
+	// Check that an error occurred and it contains the remote name
+	// This works for any language since the remote name "drycc" is always in the error
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "drycc",
+		"error message should contain the remote name 'drycc', got: %s", err.Error())
 }
diff --git a/cmd/auth.go b/internal/commands/auth.go
similarity index 96%
rename from cmd/auth.go
rename to internal/commands/auth.go
index bba5dfb04..cc24705fa 100644
--- a/cmd/auth.go
+++ b/internal/commands/auth.go
@@ -1,15 +1,14 @@
-package cmd
+package commands
 
 import (
 	drycc "github.com/drycc/controller-sdk-go"
 	"github.com/drycc/controller-sdk-go/auth"
-	"github.com/drycc/workflow-cli/settings"
+	"github.com/drycc/workflow-cli/pkg/settings"
 )
 
 // Login to a Drycc controller.
 func (d *DryccCmd) Login(controller string, sslVerify bool, username, password string) error {
 	c, err := drycc.New(sslVerify, controller, "")
-
 	if err != nil {
 		return err
 	}
@@ -52,7 +51,6 @@ func (d *DryccCmd) Logout() error {
 // more about the user.
 func (d *DryccCmd) Whoami(all bool) error {
 	s, err := settings.Load(d.ConfigFile)
-
 	if err != nil {
 		return err
 	}
diff --git a/cmd/auth_test.go b/internal/commands/auth_test.go
similarity index 97%
rename from cmd/auth_test.go
rename to internal/commands/auth_test.go
index b499c0e17..42e24feb1 100644
--- a/cmd/auth_test.go
+++ b/internal/commands/auth_test.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"bytes"
@@ -43,7 +43,7 @@ func TestLogin(t *testing.T) {
 			w.WriteHeader(http.StatusOK)
 			w.Write(nil)
 		} else {
-			w.Write([]byte(fmt.Sprintf(`{"key": "%s"}`, keyFixture)))
+			fmt.Fprintf(w, `{"key": "%s"}`, keyFixture)
 		}
 	})
 
diff --git a/internal/commands/autodeploy.go b/internal/commands/autodeploy.go
new file mode 100644
index 000000000..4194f7aed
--- /dev/null
+++ b/internal/commands/autodeploy.go
@@ -0,0 +1,74 @@
+package commands
+
+import (
+	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/controller-sdk-go/appsettings"
+	"github.com/drycc/workflow-cli/internal/loader"
+)
+
+// AutodeployInfo provides information about the status of app autodeploy.
+func (d *DryccCmd) AutodeployInfo(appID string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	appSettings, err := appsettings.List(s.Client, appID)
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	if appSettings.Autodeploy == nil || *appSettings.Autodeploy {
+		d.Println("Autodeploy is enabled.")
+	} else {
+		d.Println("Autodeploy is disabled.")
+	}
+	return nil
+}
+
+// AutodeployEnable enables an app when deploy failed
+func (d *DryccCmd) AutodeployEnable(appID string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	d.Printf("Enabling autodeploy for %s... ", appID)
+
+	quit := progress(d.WOut)
+	appSettings := api.AppSettings{Autodeploy: api.NewAutodeploy()}
+	_, err = appsettings.Set(s.Client, appID, appSettings)
+	quit <- true
+	<-quit
+
+	if err != nil {
+		return err
+	}
+
+	d.Println("done")
+	return nil
+}
+
+// AutodeployDisable disables an app when deploy failed
+func (d *DryccCmd) AutodeployDisable(appID string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	d.Printf("Disabling autodeploy for %s... ", appID)
+
+	quit := progress(d.WOut)
+	appSettings := api.AppSettings{Autodeploy: api.NewAutodeploy()}
+	*appSettings.Autodeploy = false
+	_, err = appsettings.Set(s.Client, appID, appSettings)
+	quit <- true
+	<-quit
+
+	if err != nil {
+		return err
+	}
+
+	d.Println("done")
+	return nil
+}
diff --git a/internal/commands/autodeploy_test.go b/internal/commands/autodeploy_test.go
new file mode 100644
index 000000000..a159787a2
--- /dev/null
+++ b/internal/commands/autodeploy_test.go
@@ -0,0 +1,118 @@
+package commands
+
+import (
+	"bytes"
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/workflow-cli/pkg/testutil"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAutodeployInfo(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	server.Mux.HandleFunc("/v2/apps/rivendell/settings/", func(w http.ResponseWriter, _ *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `{
+			"owner": "elrond",
+			"app": "rivendell",
+			"routable": true,
+			"autoerollback": true,
+			"created": "2024-01-01T00:00:00UTC",
+			"updated": "2024-01-01T00:00:00UTC",
+			"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
+		}`)
+	})
+
+	err = cmdr.AutodeployInfo("rivendell")
+	assert.NoError(t, err)
+	assert.Equal(t, b.String(), "Autodeploy is enabled.\n", "output")
+
+	server.Mux.HandleFunc("/v2/apps/mordor/settings/", func(w http.ResponseWriter, _ *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `{
+			"owner": "sauron",
+			"app": "mordor",
+			"routable": false,
+			"autodeploy": false,
+			"created": "2024-01-01T00:00:00UTC",
+			"updated": "2024-01-01T00:00:00UTC",
+			"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
+		}`)
+	})
+	b.Reset()
+
+	err = cmdr.AutodeployInfo("mordor")
+	assert.NoError(t, err)
+	assert.Equal(t, b.String(), "Autodeploy is disabled.\n", "output")
+
+	// test that no autodeploy field doesn't trigger a panic
+	server.Mux.HandleFunc("/v2/apps/gondor/settings/", func(w http.ResponseWriter, _ *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `{
+			"owner": "aragorn",
+			"app": "gondor",
+			"created": "2024-01-01T00:00:00UTC",
+			"updated": "2024-01-01T00:00:00UTC",
+			"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
+		}`)
+	})
+	b.Reset()
+
+	err = cmdr.AutodeployInfo("gondor")
+	assert.NoError(t, err)
+	assert.Equal(t, b.String(), "Autodeploy is enabled.\n", "output")
+}
+
+func TestAutodeployEnable(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	server.Mux.HandleFunc("/v2/apps/lothlorien/settings/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.SetHeaders(w)
+		testutil.AssertBody(t, api.AppSettings{Autodeploy: api.NewAutodeploy()}, r)
+		fmt.Fprintf(w, `{}`)
+	})
+
+	err = cmdr.AutodeployEnable("lothlorien")
+	assert.NoError(t, err)
+	testutil.AssertOutput(t, testutil.StripProgress(b.String()), "Enabling autodeploy for lothlorien... done\n")
+}
+
+func TestAutodeployDisable(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	server.Mux.HandleFunc("/v2/apps/bree/settings/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.SetHeaders(w)
+		autodeploy := false
+		testutil.AssertBody(t, api.AppSettings{Autodeploy: &autodeploy}, r)
+		fmt.Fprintf(w, `{}`)
+	})
+
+	err = cmdr.AutodeployDisable("bree")
+	assert.NoError(t, err)
+	testutil.AssertOutput(t, testutil.StripProgress(b.String()), "Disabling autodeploy for bree... done\n")
+}
diff --git a/internal/commands/autorollback.go b/internal/commands/autorollback.go
new file mode 100644
index 000000000..8b4b7c412
--- /dev/null
+++ b/internal/commands/autorollback.go
@@ -0,0 +1,74 @@
+package commands
+
+import (
+	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/controller-sdk-go/appsettings"
+	"github.com/drycc/workflow-cli/internal/loader"
+)
+
+// AutorollbackInfo provides information about the status of app autorollback.
+func (d *DryccCmd) AutorollbackInfo(appID string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	appSettings, err := appsettings.List(s.Client, appID)
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	if appSettings.Autorollback == nil || *appSettings.Autorollback {
+		d.Println("Autorollback is enabled.")
+	} else {
+		d.Println("Autorollback is disabled.")
+	}
+	return nil
+}
+
+// AutorollbackEnable enables an app when deploy failed
+func (d *DryccCmd) AutorollbackEnable(appID string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	d.Printf("Enabling autorollback for %s... ", appID)
+
+	quit := progress(d.WOut)
+	appSettings := api.AppSettings{Autorollback: api.NewAutorollback()}
+	_, err = appsettings.Set(s.Client, appID, appSettings)
+	quit <- true
+	<-quit
+
+	if err != nil {
+		return err
+	}
+
+	d.Println("done")
+	return nil
+}
+
+// AutorollbackDisable disables an app when deploy failed
+func (d *DryccCmd) AutorollbackDisable(appID string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	d.Printf("Disabling autorollback for %s... ", appID)
+
+	quit := progress(d.WOut)
+	appSettings := api.AppSettings{Autorollback: api.NewAutorollback()}
+	*appSettings.Autorollback = false
+	_, err = appsettings.Set(s.Client, appID, appSettings)
+	quit <- true
+	<-quit
+
+	if err != nil {
+		return err
+	}
+
+	d.Println("done")
+	return nil
+}
diff --git a/internal/commands/autorollback_test.go b/internal/commands/autorollback_test.go
new file mode 100644
index 000000000..0407bf30b
--- /dev/null
+++ b/internal/commands/autorollback_test.go
@@ -0,0 +1,118 @@
+package commands
+
+import (
+	"bytes"
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/workflow-cli/pkg/testutil"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAutorollbackInfo(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	server.Mux.HandleFunc("/v2/apps/rivendell/settings/", func(w http.ResponseWriter, _ *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `{
+			"owner": "elrond",
+			"app": "rivendell",
+			"routable": true,
+			"autoerollback": true,
+			"created": "2024-01-01T00:00:00UTC",
+			"updated": "2024-01-01T00:00:00UTC",
+			"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
+		}`)
+	})
+
+	err = cmdr.AutorollbackInfo("rivendell")
+	assert.NoError(t, err)
+	assert.Equal(t, b.String(), "Autorollback is enabled.\n", "output")
+
+	server.Mux.HandleFunc("/v2/apps/mordor/settings/", func(w http.ResponseWriter, _ *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `{
+			"owner": "sauron",
+			"app": "mordor",
+			"routable": false,
+			"autorollback": false,
+			"created": "2024-01-01T00:00:00UTC",
+			"updated": "2024-01-01T00:00:00UTC",
+			"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
+		}`)
+	})
+	b.Reset()
+
+	err = cmdr.AutorollbackInfo("mordor")
+	assert.NoError(t, err)
+	assert.Equal(t, b.String(), "Autorollback is disabled.\n", "output")
+
+	// test that no autorollback field doesn't trigger a panic
+	server.Mux.HandleFunc("/v2/apps/gondor/settings/", func(w http.ResponseWriter, _ *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `{
+			"owner": "aragorn",
+			"app": "gondor",
+			"created": "2024-01-01T00:00:00UTC",
+			"updated": "2024-01-01T00:00:00UTC",
+			"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
+		}`)
+	})
+	b.Reset()
+
+	err = cmdr.AutorollbackInfo("gondor")
+	assert.NoError(t, err)
+	assert.Equal(t, b.String(), "Autorollback is enabled.\n", "output")
+}
+
+func TestAutorollbackEnable(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	server.Mux.HandleFunc("/v2/apps/lothlorien/settings/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.SetHeaders(w)
+		testutil.AssertBody(t, api.AppSettings{Autorollback: api.NewAutorollback()}, r)
+		fmt.Fprintf(w, `{}`)
+	})
+
+	err = cmdr.AutorollbackEnable("lothlorien")
+	assert.NoError(t, err)
+	testutil.AssertOutput(t, testutil.StripProgress(b.String()), "Enabling autorollback for lothlorien... done\n")
+}
+
+func TestAutorollbackDisable(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	server.Mux.HandleFunc("/v2/apps/bree/settings/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.SetHeaders(w)
+		autorollback := false
+		testutil.AssertBody(t, api.AppSettings{Autorollback: &autorollback}, r)
+		fmt.Fprintf(w, `{}`)
+	})
+
+	err = cmdr.AutorollbackDisable("bree")
+	assert.NoError(t, err)
+	testutil.AssertOutput(t, testutil.StripProgress(b.String()), "Disabling autorollback for bree... done\n")
+}
diff --git a/cmd/autoscale.go b/internal/commands/autoscale.go
similarity index 68%
rename from cmd/autoscale.go
rename to internal/commands/autoscale.go
index 323ccc44b..6322be1dd 100644
--- a/cmd/autoscale.go
+++ b/internal/commands/autoscale.go
@@ -1,16 +1,16 @@
-package cmd
+package commands
 
 import (
 	"fmt"
 
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/appsettings"
+	"github.com/drycc/workflow-cli/internal/loader"
 )
 
 // AutoscaleList tells the informations about app's autoscale status
 func (d *DryccCmd) AutoscaleList(appID string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -22,10 +22,9 @@ func (d *DryccCmd) AutoscaleList(appID string) error {
 	if appSettings.Autoscale == nil {
 		d.Println("No autoscale rules found.")
 	} else {
-		table := d.getDefaultFormatTable([]string{"UUID", "PTYPE", "PERCENT", "MIN", "MAX"})
+		table := d.getDefaultFormatTable([]string{"PTYPE", "PERCENT", "MIN", "MAX"})
 		for process, kv := range appSettings.Autoscale {
 			table.Append([]string{
-				appSettings.UUID,
 				process,
 				fmt.Sprintf("%d", (*kv).CPUPercent),
 				fmt.Sprintf("%d", (*kv).Min),
@@ -39,20 +38,19 @@ func (d *DryccCmd) AutoscaleList(appID string) error {
 }
 
 // AutoscaleSet sets autoscale options for the app.
-func (d *DryccCmd) AutoscaleSet(appID string, processType string, min int, max int, CPUPercent int) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+func (d *DryccCmd) AutoscaleSet(appID string, ptype string, minCPU int, maxCPU int, CPUPercent int) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
 
-	d.Printf("Applying autoscale settings for process type %s on %s... ", processType, appID)
+	d.Printf("Applying autoscale settings for process type %s on %s... ", ptype, appID)
 
 	quit := progress(d.WOut)
 	data := map[string]*api.Autoscale{
-		processType: {
-			Min:        min,
-			Max:        max,
+		ptype: {
+			Min:        minCPU,
+			Max:        maxCPU,
 			CPUPercent: CPUPercent,
 		},
 	}
@@ -70,18 +68,17 @@ func (d *DryccCmd) AutoscaleSet(appID string, processType string, min int, max i
 }
 
 // AutoscaleUnset removes autoscale for the app.
-func (d *DryccCmd) AutoscaleUnset(appID string, processType string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+func (d *DryccCmd) AutoscaleUnset(appID string, ptype string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
 
-	d.Printf("Removing autoscale for process type %s on %s... ", processType, appID)
+	d.Printf("Removing autoscale for process type %s on %s... ", ptype, appID)
 
 	quit := progress(d.WOut)
 	data := map[string]*api.Autoscale{
-		processType: nil,
+		ptype: nil,
 	}
 	_, err = appsettings.Set(s.Client, appID, api.AppSettings{Autoscale: data})
 
diff --git a/cmd/autoscale_test.go b/internal/commands/autoscale_test.go
similarity index 93%
rename from cmd/autoscale_test.go
rename to internal/commands/autoscale_test.go
index 1966f8b24..ee120e1a9 100644
--- a/cmd/autoscale_test.go
+++ b/internal/commands/autoscale_test.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"bytes"
@@ -35,8 +35,8 @@ func TestAutoscaleList(t *testing.T) {
 
 	err = cmdr.AutoscaleList("rivendell")
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `UUID                                    PTYPE    PERCENT    MIN    MAX 
-de1bf5b5-4a72-4f94-a10c-d2a3741cdf75    cmd      40         3      8      
+	assert.Equal(t, b.String(), `PTYPE    PERCENT    MIN    MAX 
+cmd      40         3      8      
 `, "output")
 
 	server.Mux.HandleFunc("/v2/apps/mordor/settings/", func(w http.ResponseWriter, _ *http.Request) {
diff --git a/internal/commands/builds.go b/internal/commands/builds.go
new file mode 100644
index 000000000..74ce5d12b
--- /dev/null
+++ b/internal/commands/builds.go
@@ -0,0 +1,237 @@
+package commands
+
+import (
+	"bytes"
+	"fmt"
+	"os"
+	"path/filepath"
+
+	drycc "github.com/drycc/controller-sdk-go"
+	"github.com/drycc/controller-sdk-go/builds"
+	"github.com/drycc/workflow-cli/internal/loader"
+	yaml "gopkg.in/yaml.v3"
+)
+
+// BuildsInfo lists an app's builds.
+func (d *DryccCmd) BuildsInfo(appID string, version int) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	build, err := builds.Get(s.Client, appID, version)
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+	table := d.getDefaultFormatTable([]string{})
+	table.Append([]string{"App:", build.App})
+	table.Append([]string{"Sha:", build.Sha})
+	table.Append([]string{"UUID:", build.UUID})
+	table.Append([]string{"Image:", build.Image})
+	table.Append([]string{"Stack:", build.Stack})
+	table.Append([]string{"Created:", build.Created})
+	table.Append([]string{"Updated:", build.Updated})
+	table.Render()
+
+	return nil
+}
+
+// BuildsCreate creates a build for an app.
+func (d *DryccCmd) BuildsCreate(appID, image, stack, procfile, dryccpath, confirm string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	procfileMap := make(map[string]string)
+	if _, err := os.Stat(procfile); err == nil {
+		contents, err := os.ReadFile(procfile)
+		if err != nil {
+			return err
+		}
+
+		if procfileMap, err = parseProcfile(contents); err != nil {
+			return err
+		}
+	}
+
+	dryccfileMap := make(map[string]any)
+	if info, err := os.Stat(dryccpath); err == nil && info.IsDir() {
+		if dryccfileMap, err = drycc.ParseDryccfile(dryccpath); err != nil {
+			return err
+		}
+	}
+	// check procfileMap dryccfileMap stack is exist
+	err = buildConfirmAction(s.Client, appID, procfileMap, dryccfileMap, confirm)
+	if err != nil {
+		return err
+	}
+	d.Print("Creating build... ")
+	quit := progress(d.WOut)
+	_, err = builds.New(s.Client, appID, image, stack, procfileMap, dryccfileMap)
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	d.Println("done")
+
+	return nil
+}
+
+// BuildsFetch fetches a build for an app and saves the Procfile and Dryccfile to local files.
+func (d *DryccCmd) BuildsFetch(appID string, version int, procfile, dryccpath, confirm string, save bool) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	build, err := builds.Get(s.Client, appID, version)
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+	// Confirm again
+	err = buildFetchConfirmAction(confirm, procfile, dryccpath, save)
+	if err != nil {
+		return err
+	}
+
+	if len(build.Procfile) != 0 {
+		err := writeProcfileToPath(d, procfile, build.Procfile, save)
+		if err != nil {
+			return fmt.Errorf("failed to write Procfile: %w", err)
+		}
+	}
+
+	if len(build.Dryccfile) != 0 {
+		err := writeDryccfileToPath(d, dryccpath, build.Dryccfile, save)
+		if err != nil {
+			return fmt.Errorf("failed to write Dryccfile: %w", err)
+		}
+	}
+	if save {
+		d.Println("done")
+	}
+	return nil
+}
+
+func writeProcfileToPath(d *DryccCmd, procfile string, Procinfo map[string]string, save bool) error {
+	if save {
+		os.Remove(procfile)
+		err := os.WriteFile(procfile, []byte(d.toYamlString(Procinfo, 2)), 0o664)
+		return err
+	}
+	d.Println("---\n# Source:", procfile)
+	d.Println(d.toYamlString(Procinfo, 2))
+	return nil
+}
+
+func writeDryccfileToPath(d *DryccCmd, dryccpath string, dryccfile map[string]any, save bool) error {
+	// Create the directory if it doesn't exist
+	if save {
+		os.Remove(dryccpath)
+		os.ReadDir(dryccpath)
+		err := os.MkdirAll(dryccpath, os.ModePerm)
+		if err != nil {
+			return fmt.Errorf("failed to create directory: %w", err)
+		}
+	}
+
+	// Write config section
+	if config, ok := dryccfile["config"].(map[string]any); ok {
+		configDir := filepath.Join(dryccpath, "config")
+		if save {
+			err := os.MkdirAll(configDir, os.ModePerm)
+			if err != nil {
+				return fmt.Errorf("failed to create config directory: %w", err)
+			}
+		}
+
+		for key, values := range config {
+			envFilePath := filepath.Join(configDir, key)
+			var content string
+			for k, v := range values.(map[string]any) {
+				// Append each key-value pair with newline
+				content += fmt.Sprintf("%s=%v\n", k, v)
+			}
+			// Write accumulated content once
+			if save {
+				err := os.WriteFile(envFilePath, []byte(content), 0o664)
+				if err != nil {
+					return fmt.Errorf("failed to write env file: %w", err)
+				}
+			} else {
+				d.Println("---\n# Source:", envFilePath)
+				d.Println(content)
+			}
+		}
+	}
+
+	// Write pipeline section
+	if pipeline, ok := dryccfile["pipeline"].(map[string]any); ok {
+		for fileName, pipelineConfig := range pipeline {
+			filePath := filepath.Join(dryccpath, fileName)
+			var buf bytes.Buffer
+			encoder := yaml.NewEncoder(&buf)
+			encoder.SetIndent(2)
+			if err := encoder.Encode(pipelineConfig); err != nil {
+				return fmt.Errorf("failed to marshal pipeline config: %w", err)
+			}
+			yamlContent := buf.Bytes()
+			if save {
+				err := os.WriteFile(filePath, yamlContent, 0o664)
+				if err != nil {
+					return fmt.Errorf("failed to write pipeline file: %w", err)
+				}
+			} else {
+				d.Println("---\n# Source:", filePath)
+				d.Println(string(yamlContent))
+			}
+		}
+	}
+
+	return nil
+}
+
+func parseProcfile(procfile []byte) (map[string]string, error) {
+	procfileMap := make(map[string]string)
+	return procfileMap, yaml.Unmarshal(procfile, &procfileMap)
+}
+
+func buildConfirmAction(c *drycc.Client, appID string, procfileMap map[string]string,
+	dryccfileMap map[string]any, confirm string,
+) error {
+	build, _ := builds.Get(c, appID, 0)
+
+	if ((len(build.Procfile) != 0 && len(procfileMap) == 0) || (len(build.Dryccfile) != 0 && len(dryccfileMap) == 0)) && (confirm == "" || confirm != "yes") {
+		// hint
+		fmt.Printf(` !    WARNING: Potentially Build Create Action
+ !    The Procfile or drycc file is empty, not last time
+ !    To proceed, type "yes" !
+
+> `)
+
+		fmt.Scanln(&confirm)
+		if confirm != "yes" {
+			return fmt.Errorf("cancel the build create action")
+		}
+	}
+	return nil
+}
+
+func buildFetchConfirmAction(confirm, procfile, dryccpath string, save bool) error {
+	if save && (confirm == "" || confirm != "yes") {
+		// hint
+		msg := fmt.Sprintf(" !    WARNING: Potentially Build Fetch Action\n"+
+			" !    This operation will overwrite the current \x1b[1m%s\x1b[0m or \x1b[1m%s\x1b[0m locally\n"+
+			" !    To proceed, type \"yes\" !\n\n> ", procfile, dryccpath)
+
+		fmt.Print(msg)
+		fmt.Scanln(&confirm)
+		if confirm != "yes" {
+			return fmt.Errorf("cancel the build fetch action")
+		}
+	}
+	return nil
+}
diff --git a/internal/commands/builds_test.go b/internal/commands/builds_test.go
new file mode 100644
index 000000000..bd9eb384d
--- /dev/null
+++ b/internal/commands/builds_test.go
@@ -0,0 +1,335 @@
+package commands
+
+import (
+	"bytes"
+	"fmt"
+	"net/http"
+	"os"
+	"path/filepath"
+	"reflect"
+	"testing"
+
+	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/workflow-cli/pkg/testutil"
+	"github.com/stretchr/testify/assert"
+	"gopkg.in/yaml.v3"
+)
+
+func TestParseProcfile(t *testing.T) {
+	t.Parallel()
+
+	procMap, err := parseProcfile([]byte(`web: ./test
+foo: test --test
+`))
+	assert.NoError(t, err)
+	assert.Equal(t, procMap, map[string]string{"web": "./test", "foo": "test --test"}, "map")
+
+	_, err = parseProcfile([]byte(`web: ./test
+foo
+`))
+	assert.NotEqual(t, err, nil, "yaml")
+}
+
+func TestBuildsInfo(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	server.Mux.HandleFunc("/v2/apps/foo/build/", func(w http.ResponseWriter, _ *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `{
+				"app": "",
+				"created": "2014-01-01T00:00:00UTC",
+				"dockerfile": "",
+				"image": "",
+				"owner": "",
+				"procfile": {},
+				"sha": "",
+				"updated": "",
+				"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
+			}
+		`)
+	})
+
+	err = cmdr.BuildsInfo("foo", -1)
+	assert.NoError(t, err)
+	testutil.AssertOutput(t, b.String(), `App:
+Sha:
+UUID:       de1bf5b5-4a72-4f94-a10c-d2a3741cdf75
+Image:
+Stack:
+Created:    2014-01-01T00:00:00UTC
+Updated:
+`)
+}
+
+func TestBuildsCreate(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	// Create a new temporary directory and change to it.
+	name, err := os.MkdirTemp("", "client")
+	assert.NoError(t, err)
+	err = os.Chdir(name)
+	assert.NoError(t, err)
+
+	server.Mux.HandleFunc("/v2/apps/enterprise/build/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.SetHeaders(w)
+		if r.Method == "POST" {
+			testutil.AssertBody(t, api.CreateBuildRequest{
+				Image: "ncc/1701:A",
+				Stack: "container",
+			}, r)
+			w.WriteHeader(http.StatusCreated)
+			fmt.Fprintf(w, "{}")
+		}
+	})
+
+	err = cmdr.BuildsCreate("enterprise", "ncc/1701:A", "container", "", "", "yes")
+	assert.NoError(t, err)
+	assert.Equal(t, testutil.StripProgress(b.String()), "Creating build... done\n", "output")
+
+	server.Mux.HandleFunc("/v2/apps/bradbury/build/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.SetHeaders(w)
+		if r.Method == "POST" {
+			testutil.AssertBody(t, api.CreateBuildRequest{
+				Image: "nx/72307:latest",
+				Stack: "container",
+				Procfile: map[string]string{
+					"web":  "./drive",
+					"warp": "./warp 8",
+				},
+			}, r)
+		}
+
+		w.WriteHeader(http.StatusCreated)
+		fmt.Fprintf(w, "{}")
+	})
+	b.Reset()
+
+	tmpDir, err := os.MkdirTemp("", "tmpdir")
+	if err != nil {
+		t.Fatalf("error creating temp directory (%s)", err)
+	}
+	data := `web: ./drive
+warp: ./warp 8`
+	if err := os.WriteFile(tmpDir+"/Procfile", []byte(data), 0o644); err != nil {
+		t.Fatalf("error creating %s/Procfile (%s)", tmpDir, err)
+	}
+	defer func() {
+		if err := os.RemoveAll(tmpDir); err != nil {
+			t.Fatalf("failed to remove Procfile from %s (%s)", tmpDir, err)
+		}
+	}()
+
+	err = cmdr.BuildsCreate("bradbury", "nx/72307:latest", "container", tmpDir+"/Procfile", "", "yes")
+	assert.NoError(t, err)
+	assert.Equal(t, testutil.StripProgress(b.String()), "Creating build... done\n", "output")
+
+	server.Mux.HandleFunc("/v2/apps/franklin/build/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.SetHeaders(w)
+		if r.Method == "POST" {
+			testutil.AssertBody(t, api.CreateBuildRequest{
+				Image: "nx/326:latest",
+				Stack: "container",
+				Procfile: map[string]string{
+					"web":  "./drive",
+					"warp": "./warp 8",
+				},
+				Dryccfile: map[string]any{
+					"pipeline": map[string]any{
+						"web.yaml": map[string]any{
+							"kind":  "pipeline",
+							"ptype": "web",
+							"deploy": map[string]any{
+								"command": []string{
+									"bash",
+									"-c",
+								},
+								"args": []string{
+									"bundle exec puma -C config/puma.rb",
+								},
+							},
+						},
+					},
+				},
+			}, r)
+		}
+
+		w.WriteHeader(http.StatusCreated)
+		fmt.Fprintf(w, "{}")
+	})
+	b.Reset()
+
+	err = os.WriteFile("Procfile", []byte(`web: ./drive
+warp: ./warp 8
+`), os.ModePerm)
+	assert.NoError(t, err)
+
+	dryccpath := ".drycc"
+	os.MkdirAll(dryccpath, 0o700)
+	defer os.RemoveAll(dryccpath)
+	err = os.WriteFile(filepath.Join(dryccpath, "web.yaml"), []byte(`
+kind: pipeline
+ptype: web
+deploy:
+  command:
+  - bash
+  - -c
+  args:
+  - bundle exec puma -C config/puma.rb
+`), os.ModePerm)
+	assert.NoError(t, err)
+
+	err = cmdr.BuildsCreate("franklin", "nx/326:latest", "container", "Procfile", dryccpath, "yes")
+	assert.NoError(t, err)
+	assert.Equal(t, testutil.StripProgress(b.String()), "Creating build... done\n", "output")
+}
+
+func TestBuildsFetch(t *testing.T) {
+	t.Parallel()
+
+	// Mock server setup
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+
+	cmdr := DryccCmd{WOut: new(bytes.Buffer), ConfigFile: cf}
+
+	// Mock build data
+	server.Mux.HandleFunc("/v2/apps/testapp/build/", func(w http.ResponseWriter, _ *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `{
+			"app": "testapp",
+			"procfile": {"web": "node server.js"},
+			"dryccfile": {
+				"pipeline": {
+					"web.yaml": {
+						"kind": "pipeline",
+						"ptype": "web",
+						"deploy": {
+							"command": ["bash", "-c"],
+							"args": ["echo hello"]
+						}
+					}
+				},
+				"config": {
+					"env1.env": {"KEY1": "VALUE1"},
+					"env2.env": {"KEY2": "VALUE2"}
+				}
+			}
+		}`)
+	})
+
+	// Helper function to compare YAML content ignoring field order
+	isEqualYAML := func(actual, expected string) bool {
+		var actualMap, expectedMap map[string]any
+		err := yaml.Unmarshal([]byte(actual), &actualMap)
+		if err != nil {
+			return false
+		}
+		err = yaml.Unmarshal([]byte(expected), &expectedMap)
+		if err != nil {
+			return false
+		}
+		return reflect.DeepEqual(actualMap, expectedMap)
+	}
+
+	// Test case 1: Successful fetch with valid confirm
+	t.Run("Successful Fetch", func(t *testing.T) {
+		tmpDir := t.TempDir() // Create a unique temporary directory for this test
+		procfilePath := filepath.Join(tmpDir, "Procfile")
+		dryccpath := filepath.Join(tmpDir, ".drycc")
+
+		// Mock user input for confirmation
+		restoreStdin := os.Stdin
+		r, w, _ := os.Pipe()
+		os.Stdin = r
+		go func() {
+			defer w.Close() // Ensure the pipe is closed after writing
+			w.Write([]byte("yes\n"))
+		}()
+
+		err := cmdr.BuildsFetch("testapp", 0, procfilePath, dryccpath, "", true)
+		if err != nil {
+			t.Fatalf("expected no error, got %v", err)
+		}
+
+		// Verify Procfile content
+		content, err := os.ReadFile(procfilePath)
+		if err != nil {
+			t.Fatalf("failed to read Procfile: %v", err)
+		}
+		expectedProcfile := "web: node server.js\n"
+		if string(content) != expectedProcfile {
+			t.Errorf("expected Procfile content %q, got %q", expectedProcfile, string(content))
+		}
+
+		// Verify .drycc/config/env1.env content
+		env1Content, err := os.ReadFile(filepath.Join(dryccpath, "config", "env1.env"))
+		if err != nil {
+			t.Fatalf("failed to read env1.env: %v", err)
+		}
+		expectedEnv1 := "KEY1=VALUE1\n"
+		if string(env1Content) != expectedEnv1 {
+			t.Errorf("expected env1.env content %q, got %q", expectedEnv1, string(env1Content))
+		}
+
+		// Verify .drycc/web.yaml content
+		webYamlContent, err := os.ReadFile(filepath.Join(dryccpath, "web.yaml"))
+		if err != nil {
+			t.Fatalf("failed to read web.yaml: %v", err)
+		}
+		expectedWebYaml := "kind: pipeline\nptype: web\ndeploy:\n  command:\n  - bash\n  - -c\n  args:\n  - echo hello\n"
+		if !isEqualYAML(string(webYamlContent), expectedWebYaml) {
+			t.Errorf("expected web.yaml content %q, got %q", expectedWebYaml, string(webYamlContent))
+		}
+
+		os.Stdin = restoreStdin
+	})
+
+	// Test case 2: User cancels the operation
+	t.Run("User Cancels Operation", func(t *testing.T) {
+		tmpDir := t.TempDir() // Create a unique temporary directory for this test
+		procfilePath := filepath.Join(tmpDir, "Procfile")
+		dryccpath := filepath.Join(tmpDir, ".drycc")
+
+		// Mock user input for cancellation
+		restoreStdin := os.Stdin
+		r, w, _ := os.Pipe()
+		os.Stdin = r
+		go func() {
+			defer w.Close() // Ensure the pipe is closed after writing
+			w.Write([]byte("no\n"))
+		}()
+
+		err := cmdr.BuildsFetch("testapp", 0, procfilePath, dryccpath, "", true)
+		if err == nil || err.Error() != "cancel the build fetch action" {
+			t.Fatalf("expected cancellation error, got %v", err)
+		}
+
+		// Verify files were not created
+		if _, err := os.Stat(procfilePath); !os.IsNotExist(err) {
+			t.Errorf("expected Procfile to not exist, but it does")
+		}
+
+		if _, err := os.Stat(dryccpath); !os.IsNotExist(err) {
+			t.Errorf("expected .drycc directory to not exist, but it does")
+		}
+
+		os.Stdin = restoreStdin
+	})
+}
diff --git a/cmd/certs.go b/internal/commands/certs.go
similarity index 70%
rename from cmd/certs.go
rename to internal/commands/certs.go
index dc18ba26c..13596fd79 100644
--- a/cmd/certs.go
+++ b/internal/commands/certs.go
@@ -1,23 +1,21 @@
-package cmd
+package commands
 
 import (
 	"fmt"
 	"os"
 	"strings"
-	"time"
 
 	drycc "github.com/drycc/controller-sdk-go"
 	"github.com/drycc/controller-sdk-go/certs"
 	dtime "github.com/drycc/controller-sdk-go/pkg/time"
-	"github.com/drycc/workflow-cli/settings"
+	"github.com/drycc/workflow-cli/internal/loader"
 )
 
 const dateFormat = "2 Jan 2006"
 
 // CertsList lists certs registered with the controller.
-func (d *DryccCmd) CertsList(results int) error {
-	s, err := settings.Load(d.ConfigFile)
-
+func (d *DryccCmd) CertsList(appID string, results int) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -26,7 +24,7 @@ func (d *DryccCmd) CertsList(results int) error {
 		results = s.Limit
 	}
 
-	certList, _, err := certs.List(s.Client, results)
+	certList, _, err := certs.List(s.Client, appID, results)
 	if d.checkAPICompatibility(s.Client, err) != nil {
 		return err
 	}
@@ -56,16 +54,15 @@ func (d *DryccCmd) CertsList(results int) error {
 }
 
 // CertAdd adds a cert to the controller.
-func (d *DryccCmd) CertAdd(cert string, key string, name string) error {
-	s, err := settings.Load(d.ConfigFile)
-
+func (d *DryccCmd) CertAdd(appID string, cert string, key string, name string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
 
 	d.Print("Adding SSL endpoint... ")
 	quit := progress(d.WOut)
-	err = d.doCertAdd(s.Client, cert, key, name)
+	err = d.doCertAdd(s.Client, appID, cert, key, name)
 	quit <- true
 	<-quit
 
@@ -77,7 +74,7 @@ func (d *DryccCmd) CertAdd(cert string, key string, name string) error {
 	return nil
 }
 
-func (d *DryccCmd) doCertAdd(c *drycc.Client, cert string, key string, name string) error {
+func (d *DryccCmd) doCertAdd(c *drycc.Client, appID string, cert string, key string, name string) error {
 	certFile, err := os.ReadFile(cert)
 	if err != nil {
 		return err
@@ -88,13 +85,13 @@ func (d *DryccCmd) doCertAdd(c *drycc.Client, cert string, key string, name stri
 		return err
 	}
 
-	_, err = certs.New(c, string(certFile), string(keyFile), name)
+	_, err = certs.New(c, appID, string(certFile), string(keyFile), name)
 	return d.checkAPICompatibility(c, err)
 }
 
 // CertRemove deletes a cert from the controller.
-func (d *DryccCmd) CertRemove(name string) error {
-	s, err := settings.Load(d.ConfigFile)
+func (d *DryccCmd) CertRemove(appID string, name string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if d.checkAPICompatibility(s.Client, err) != nil {
 		return err
 	}
@@ -102,7 +99,7 @@ func (d *DryccCmd) CertRemove(name string) error {
 	d.Printf("Removing %s... ", name)
 	quit := progress(d.WOut)
 
-	err = certs.Delete(s.Client, name)
+	err = certs.Delete(s.Client, appID, name)
 	quit <- true
 	<-quit
 	if d.checkAPICompatibility(s.Client, err) != nil {
@@ -114,12 +111,12 @@ func (d *DryccCmd) CertRemove(name string) error {
 }
 
 // CertInfo gets info about certficiate
-func (d *DryccCmd) CertInfo(name string) error {
-	s, err := settings.Load(d.ConfigFile)
+func (d *DryccCmd) CertInfo(appID string, name string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
-	cert, err := certs.Get(s.Client, name)
+	cert, err := certs.Get(s.Client, appID, name)
 	if d.checkAPICompatibility(s.Client, err) != nil {
 		return err
 	}
@@ -135,17 +132,15 @@ func (d *DryccCmd) CertInfo(name string) error {
 	table.Append([]string{"Subject:", safeGetString(cert.Subject)})
 	table.Append([]string{""})
 	table.Append([]string{"Connected Domains:", safeGetString(strings.Join(cert.Domains[:], ","))})
-	table.Append([]string{"Owner:", safeGetString(cert.Owner)})
-	table.Append([]string{"Created:", safeGetTime(cert.Created, time.RFC3339)})
-	table.Append([]string{"Updated:", safeGetTime(cert.Updated, time.RFC3339)})
+	table.Append([]string{"Created:", d.formatTime(cert.Created)})
+	table.Append([]string{"Updated:", d.formatTime(cert.Updated)})
 	table.Render()
 	return nil
 }
 
 // CertAttach attaches a certificate to a domain
-func (d *DryccCmd) CertAttach(name, domain string) error {
-	s, err := settings.Load(d.ConfigFile)
-
+func (d *DryccCmd) CertAttach(appID string, name, domain string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -153,7 +148,7 @@ func (d *DryccCmd) CertAttach(name, domain string) error {
 	d.Printf("Attaching certificate %s to domain %s... ", name, domain)
 	quit := progress(d.WOut)
 
-	err = certs.Attach(s.Client, name, domain)
+	err = certs.Attach(s.Client, appID, name, domain)
 	quit <- true
 	<-quit
 	if d.checkAPICompatibility(s.Client, err) == nil {
@@ -164,9 +159,8 @@ func (d *DryccCmd) CertAttach(name, domain string) error {
 }
 
 // CertDetach detaches a certificate from a domain
-func (d *DryccCmd) CertDetach(name, domain string) error {
-	s, err := settings.Load(d.ConfigFile)
-
+func (d *DryccCmd) CertDetach(appID, name, domain string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -174,7 +168,7 @@ func (d *DryccCmd) CertDetach(name, domain string) error {
 	d.Printf("Detaching certificate %s from domain %s... ", name, domain)
 	quit := progress(d.WOut)
 
-	err = certs.Detach(s.Client, name, domain)
+	err = certs.Detach(s.Client, appID, name, domain)
 	quit <- true
 	<-quit
 	if d.checkAPICompatibility(s.Client, err) != nil {
diff --git a/cmd/certs_test.go b/internal/commands/certs_test.go
similarity index 81%
rename from cmd/certs_test.go
rename to internal/commands/certs_test.go
index 0616ff912..d41f28a38 100644
--- a/cmd/certs_test.go
+++ b/internal/commands/certs_test.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"bytes"
@@ -22,14 +22,15 @@ func TestCertsList(t *testing.T) {
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
-	server.Mux.HandleFunc("/v2/certs/", func(w http.ResponseWriter, _ *http.Request) {
+	server.Mux.HandleFunc("/v2/apps/foo/certs/", func(w http.ResponseWriter, _ *http.Request) {
 		testutil.SetHeaders(w)
 		fmt.Fprintf(w, `{
 			"count": 4,
 			"next": null,
 			"previous": null,
 			"results": [
-				{
+				{	
+					"app": "foo",
 					"name": "test-example-com",
 					"common_name": "test.example.com",
 					"san": [
@@ -46,6 +47,7 @@ func TestCertsList(t *testing.T) {
 					"fingerprint": "12:34:56:78:90"
 				},
 				{
+					"app": "foo",
 					"name": "test-drycc-com",
 					"common_name": "test.drycc.com",
 					"created": "2016-06-09T00:00:00UTC",
@@ -54,11 +56,13 @@ func TestCertsList(t *testing.T) {
 					"fingerprint": "ab:12:ab:12:ab"
 				},
 				{
+					"app": "foo",
 					"name": "test1",
 					"common_name": "1.test.drycc.com",
 					"expires": "2016-06-11T00:00:00UTC"
 				},
 				{
+					"app": "foo",
 					"name": "test2",
 					"common_name": "2.test.drycc.com",
 					"expires": "2018-01-01T00:00:00UTC"
@@ -67,7 +71,7 @@ func TestCertsList(t *testing.T) {
 		}`)
 	})
 
-	err = cmdr.CertsList(-1)
+	err = cmdr.CertsList("foo", -1)
 	assert.NoError(t, err)
 
 	assert.Equal(t, b.String(), `NAME                COMMON-NAME         EXPIRES        SAN                     DOMAINS              
@@ -86,7 +90,7 @@ test2               2.test.drycc.com    1 Jan 2018     <none>                  <
 	cmdr.ConfigFile = cf
 	b.Reset()
 
-	server.Mux.HandleFunc("/v2/certs/", func(w http.ResponseWriter, _ *http.Request) {
+	server.Mux.HandleFunc("/v2/apps/foo/certs/", func(w http.ResponseWriter, _ *http.Request) {
 		testutil.SetHeaders(w)
 		fmt.Fprintf(w, `{
 			"count": 0,
@@ -96,7 +100,7 @@ test2               2.test.drycc.com    1 Jan 2018     <none>                  <
 		}`)
 	})
 
-	err = cmdr.CertsList(-1)
+	err = cmdr.CertsList("foo", -1)
 	assert.NoError(t, err)
 
 	assert.Equal(t, b.String(), "No certs\n", "output")
@@ -112,7 +116,7 @@ func TestCertsListLimit(t *testing.T) {
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
-	server.Mux.HandleFunc("/v2/certs/", func(w http.ResponseWriter, _ *http.Request) {
+	server.Mux.HandleFunc("/v2/apps/foo/certs/", func(w http.ResponseWriter, _ *http.Request) {
 		testutil.SetHeaders(w)
 		fmt.Fprintf(w, `{
 			"count": 4,
@@ -141,13 +145,12 @@ func TestCertsListLimit(t *testing.T) {
 		}`)
 	})
 
-	err = cmdr.CertsList(1)
+	err = cmdr.CertsList("foo", 1)
 	assert.NoError(t, err)
 
 	assert.Equal(t, b.String(), `NAME                COMMON-NAME         EXPIRES        SAN                               DOMAINS                        
 test-example-com    test.example.com    10 Nov 2014    test.com,drycc.com,example.com    test.com,drycc.com,example.com    
 `, "output")
-
 }
 
 func TestCertsInfo(t *testing.T) {
@@ -160,16 +163,17 @@ func TestCertsInfo(t *testing.T) {
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
-	server.Mux.HandleFunc("/v2/certs/test-example-com", func(w http.ResponseWriter, _ *http.Request) {
+	server.Mux.HandleFunc("/v2/apps/foo/certs/test-example-com", func(w http.ResponseWriter, _ *http.Request) {
 		testutil.SetHeaders(w)
 		fmt.Fprintf(w, `{
+			"app": "foo",
 			"name": "test-example-com",
 			"owner": "admin",
 			"issuer": "testca",
 			"subject": "testing",
 			"common_name": "test.drycc.com",
-			"created": "2016-06-09T00:00:00UTC",
-			"updated": "2016-06-09T00:00:00UTC",
+			"created": "2016-06-09T00:00:00Z",
+			"updated": "2016-06-09T00:00:00Z",
 			"expires": "2016-06-09T00:00:00UTC",
 			"starts": "2016-06-09T00:00:00UTC",
 			"fingerprint": "ab:12:ab:12:ab",
@@ -184,7 +188,7 @@ func TestCertsInfo(t *testing.T) {
 		}`)
 	})
 
-	err = cmdr.CertInfo("test-example-com")
+	err = cmdr.CertInfo("foo", "test-example-com")
 	assert.NoError(t, err)
 	assert.Equal(t, b.String(), `Name:                 test-example-com        
 Common Name(s):       test.drycc.com          
@@ -196,12 +200,11 @@ Issuer:               testca
 Subject:              testing                 
                       
 Connected Domains:    test.com,example.com    
-Owner:                admin                   
 Created:              2016-06-09T00:00:00Z    
 Updated:              2016-06-09T00:00:00Z    
 `, "output")
 
-	server.Mux.HandleFunc("/v2/certs/test-drycc-com", func(w http.ResponseWriter, _ *http.Request) {
+	server.Mux.HandleFunc("/v2/apps/foo/certs/test-drycc-com", func(w http.ResponseWriter, _ *http.Request) {
 		testutil.SetHeaders(w)
 		fmt.Fprintf(w, `{
 			"name": "test-drycc-com"
@@ -209,7 +212,7 @@ Updated:              2016-06-09T00:00:00Z
 	})
 	b.Reset()
 
-	err = cmdr.CertInfo("test-drycc-com")
+	err = cmdr.CertInfo("foo", "test-drycc-com")
 	assert.NoError(t, err)
 	assert.Equal(t, b.String(), `Name:                 test-drycc-com    
 Common Name(s):       <none>            
@@ -221,9 +224,8 @@ Issuer:               <none>
 Subject:              <none>            
                       
 Connected Domains:    <none>            
-Owner:                <none>            
-Created:              <none>            
-Updated:              <none>            
+Created:                                
+Updated:                                
 `, "output")
 }
 
@@ -237,12 +239,12 @@ func TestCertsRemove(t *testing.T) {
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
-	server.Mux.HandleFunc("/v2/certs/test-example-com", func(w http.ResponseWriter, _ *http.Request) {
+	server.Mux.HandleFunc("/v2/apps/foo/certs/test-example-com", func(w http.ResponseWriter, _ *http.Request) {
 		testutil.SetHeaders(w)
 		w.WriteHeader(http.StatusNoContent)
 	})
 
-	err = cmdr.CertRemove("test-example-com")
+	err = cmdr.CertRemove("foo", "test-example-com")
 	assert.NoError(t, err)
 
 	assert.Equal(t, testutil.StripProgress(b.String()), "Removing test-example-com... done\n", "output")
@@ -258,13 +260,13 @@ func TestCertsAttach(t *testing.T) {
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
-	server.Mux.HandleFunc("/v2/certs/test-example-com/domain/", func(w http.ResponseWriter, r *http.Request) {
+	server.Mux.HandleFunc("/v2/apps/foo/certs/test-example-com/domain/", func(w http.ResponseWriter, r *http.Request) {
 		testutil.SetHeaders(w)
 		testutil.AssertBody(t, api.CertAttachRequest{Domain: "drycc.com"}, r)
 		w.WriteHeader(http.StatusCreated)
 	})
 
-	err = cmdr.CertAttach("test-example-com", "drycc.com")
+	err = cmdr.CertAttach("foo", "test-example-com", "drycc.com")
 	assert.NoError(t, err)
 
 	assert.Equal(t, testutil.StripProgress(b.String()), "Attaching certificate test-example-com to domain drycc.com... done\n", "output")
@@ -280,12 +282,12 @@ func TestCertsDetach(t *testing.T) {
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
-	server.Mux.HandleFunc("/v2/certs/test-example-com/domain/drycc.com", func(w http.ResponseWriter, _ *http.Request) {
+	server.Mux.HandleFunc("/v2/apps/foo/certs/test-example-com/domain/drycc.com", func(w http.ResponseWriter, _ *http.Request) {
 		testutil.SetHeaders(w)
 		w.WriteHeader(http.StatusNoContent)
 	})
 
-	err = cmdr.CertDetach("test-example-com", "drycc.com")
+	err = cmdr.CertDetach("foo", "test-example-com", "drycc.com")
 	assert.NoError(t, err)
 
 	assert.Equal(t, testutil.StripProgress(b.String()), "Detaching certificate test-example-com from domain drycc.com... done\n", "output")
@@ -301,7 +303,7 @@ func TestCertsAdd(t *testing.T) {
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
-	server.Mux.HandleFunc("/v2/certs/", func(w http.ResponseWriter, r *http.Request) {
+	server.Mux.HandleFunc("/v2/apps/foo/certs/", func(w http.ResponseWriter, r *http.Request) {
 		testutil.SetHeaders(w)
 		testutil.AssertBody(t, api.CertCreateRequest{Certificate: "cert", Key: "key", Name: "testcert"}, r)
 		w.WriteHeader(http.StatusCreated)
@@ -320,7 +322,7 @@ func TestCertsAdd(t *testing.T) {
 	assert.NoError(t, err)
 	certFile.Close()
 
-	err = cmdr.CertAdd(certFile.Name(), keyFile.Name(), "testcert")
+	err = cmdr.CertAdd("foo", certFile.Name(), keyFile.Name(), "testcert")
 	assert.NoError(t, err)
 
 	assert.Equal(t, testutil.StripProgress(b.String()), "Adding SSL endpoint... done\n", "output")
diff --git a/cmd/cmd.go b/internal/commands/commands.go
similarity index 50%
rename from cmd/cmd.go
rename to internal/commands/commands.go
index cb2c5d1d3..c374948cf 100644
--- a/cmd/cmd.go
+++ b/internal/commands/commands.go
@@ -1,16 +1,19 @@
-package cmd
+package commands
 
 import (
 	"fmt"
 	"io"
+	"time"
 
 	drycc "github.com/drycc/controller-sdk-go"
 	"github.com/drycc/controller-sdk-go/api"
 )
 
-// Commander is interface definition for running commands
+// Commander is the interface that defines all available commands for the Drycc CLI.
+//
+//go:generate mockgen -package testmocks -destination testmocks/mock_pack_client.go github.com/buildpacks/pack/internal/commands PackClient
 type Commander interface {
-	AppCreate(string, string, bool) error
+	AppCreate(string, string, string, bool) error
 	AppsList(int) error
 	AppInfo(string) error
 	AppOpen(string) error
@@ -18,6 +21,12 @@ type Commander interface {
 	AppRun(string, string, []string, uint32, uint32) error
 	AppDestroy(string, string) error
 	AppTransfer(string, string) error
+	AutodeployInfo(string) error
+	AutodeployEnable(string) error
+	AutodeployDisable(string) error
+	AutorollbackInfo(string) error
+	AutorollbackEnable(string) error
+	AutorollbackDisable(string) error
 	AutoscaleList(string) error
 	AutoscaleSet(string, string, int, int, int) error
 	AutoscaleUnset(string, string) error
@@ -27,26 +36,24 @@ type Commander interface {
 	TokensList(int) error
 	TokensAdd(*drycc.Client, string, string, string, string, bool) (*api.AuthTokenResponse, error)
 	TokensRemove(string, string) error
-	BuildsList(string, int) error
-	BuildsCreate(string, string, string, string, string) error
-	CanaryInfo(string) error
-	CanaryCreate(string, []string) error
-	CanaryRemove(string, []string) error
-	CanaryRelease(string) error
-	CanaryRollback(string) error
-	CertsList(int) error
-	CertAdd(string, string, string) error
-	CertRemove(string) error
-	CertInfo(string) error
-	CertAttach(string, string) error
-	CertDetach(string, string) error
-	ConfigList(string, string) error
-	ConfigSet(string, string, []string) error
-	ConfigUnset(string, string, []string) error
-	ConfigPull(string, string, string, bool, bool) error
-	ConfigPush(string, string, string) error
+	BuildsInfo(string, int) error
+	BuildsCreate(string, string, string, string, string, string) error
+	BuildsFetch(string, int, string, string, string, bool) error
+	CertsList(string, int) error
+	CertAdd(string, string, string, string) error
+	CertRemove(string, string) error
+	CertInfo(string, string) error
+	CertAttach(string, string, string) error
+	CertDetach(string, string, string) error
+	ConfigInfo(string, string, string, int) error
+	ConfigSet(string, string, string, []string, bool, string) error
+	ConfigUnset(string, string, string, []string, string) error
+	ConfigPull(string, string, string, string, bool, bool) error
+	ConfigPush(string, string, string, string, bool, string) error
+	ConfigAttach(string, string, string) error
+	ConfigDetach(string, string, string) error
 	DomainsList(string, int) error
-	DomainsAdd(string, string) error
+	DomainsAdd(string, string, string) error
 	DomainsRemove(string, string) error
 	ServicesList(string) error
 	ServicesAdd(string, string, string, string) error
@@ -54,7 +61,7 @@ type Commander interface {
 	GatewaysAdd(string, string, int, string) error
 	GatewaysList(string, int) error
 	GatewaysRemove(string, string, int, string) error
-	RoutesCreate(string, string, string, string, int) error
+	RoutesCreate(string, string, string, ...api.BackendRefRequest) error
 	RoutesList(string, int) error
 	RoutesGet(string, string) error
 	RoutesSet(string, string, string) error
@@ -63,8 +70,8 @@ type Commander interface {
 	RoutesRemove(string, string) error
 	GitRemote(string, string, bool) error
 	GitRemove(string) error
-	HealthchecksList(string, string) error
-	HealthchecksSet(string, string, string, *api.Healthcheck) error
+	HealthchecksList(string, string, int) error
+	HealthchecksSet(string, string, string, *api.ContainerProbe) error
 	HealthchecksUnset(string, string, []string) error
 	KeysList(int) error
 	KeyRemove(string) error
@@ -72,56 +79,65 @@ type Commander interface {
 	LabelsList(string) error
 	LabelsSet(string, []string) error
 	LabelsUnset(string, []string) error
-	LimitsList(string) error
+	LimitsList(string, int) error
 	LimitsSet(string, []string) error
 	LimitsUnset(string, []string) error
 	LimitsSpecs(string, int) error
 	LimitsPlans(string, int, int, int) error
-	TimeoutsList(string) error
+	TimeoutsList(string, int) error
 	TimeoutsSet(string, []string) error
 	TimeoutsUnset(string, []string) error
-	PermsList(string, bool, int) error
-	PermCreate(string, string, bool) error
-	PermDelete(string, string, bool) error
+	WorkspacesList(int) error
+	WorkspacesCreate(string, string) error
+	WorkspacesInfo(string, int) error
+	WorkspacesDelete(string, string) error
+	WorkspacesInvite(string, string) error
+	WorkspacesRemove(string, string) error
+	WorkspacesUpdate(string, string, string, *bool) error
 	PsList(string, int) error
-	PsLogs(string, string, int, bool, string) error
+	PsLogs(string, string, int, bool, string, bool) error
 	PsExec(string, string, bool, bool, []string) error
-	PsScale(string, []string) error
-	PsRestart(string, string) error
-	RegistryList(string) error
-	RegistrySet(string, []string) error
-	RegistryUnset(string, []string) error
-	ReleasesList(string, int) error
+	PsDescribe(string, string) error
+	PsDelete(string, []string) error
+	PtsList(string, int) error
+	PtsDescribe(string, string) error
+	PtsScale(string, []string) error
+	PtsRestart(string, []string, string) error
+	PtsClean(string, []string) error
+	RegistryList(string, string, int) error
+	RegistrySet(string, string, string, string) error
+	RegistryUnset(string, string) error
+	ReleasesList(string, []string, int) error
 	ReleasesInfo(string, int) error
-	ReleasesRollback(string, int) error
+	ReleasesDeploy(string, []string, bool, string) error
+	ReleasesRollback(string, []string, int) error
 	RoutingInfo(string) error
 	RoutingEnable(string) error
 	RoutingDisable(string) error
 	ShortcutsList() error
-	TagsList(string) error
-	TagsSet(string, []string) error
-	TagsUnset(string, []string) error
+	TagsList(string, string, int) error
+	TagsSet(string, string, []string) error
+	TagsUnset(string, string, []string) error
 	TLSInfo(string) error
 	TLSForceEnable(string) error
 	TLSForceDisable(string) error
 	TLSAutoEnable(string) error
 	TLSAutoDisable(string) error
 	TLSAutoIssuer(string, string, string, string, string) error
-	UsersList(results int) error
-	UsersEnable(string) error
-	UsersDisable(string) error
-	Println(...interface{}) (int, error)
-	Print(...interface{}) (int, error)
-	Printf(string, ...interface{}) (int, error)
-	PrintErrln(...interface{}) (int, error)
-	PrintErr(...interface{}) (int, error)
-	PrintErrf(string, ...interface{}) (int, error)
+	Update(bool) error
+	Println(...any) (int, error)
+	Print(...any) (int, error)
+	Printf(string, ...any) (int, error)
+	PrintErrln(...any) (int, error)
+	PrintErr(...any) (int, error)
+	PrintErrf(string, ...any) (int, error)
 	Version(bool) error
-	VolumesCreate(string, string, string, string, map[string]interface{}) error
+	VolumesCreate(string, string, string, string, map[string]any) error
 	VolumesExpand(string, string, string) error
 	VolumesDelete(string, string) error
 	VolumesList(string, int) error
 	VolumesInfo(string, string) error
+	VolumesClient(string, string, ...string) error
 	VolumesMount(string, string, []string) error
 	VolumesUnmount(string, string, []string) error
 	ResourcesServices(int) error
@@ -129,47 +145,49 @@ type Commander interface {
 	ResourcesCreate(string, string, string, []string, string) error
 	ResourcesList(string, int) error
 	ResourceDelete(string, string, string) error
-	ResourceGet(string, string) error
+	ResourceGet(string, string, bool) error
 	ResourcePut(string, string, string, []string, string) error
 	ResourceBind(string, string) error
 	ResourceUnbind(string, string) error
 }
 
-// DryccCmd is an implementation of Commander.
+// DryccCmd is a struct that implements the Commander interface and provides
+// methods for executing Drycc CLI commands.
 type DryccCmd struct {
 	ConfigFile string
 	Warned     bool
 	WOut       io.Writer
 	WErr       io.Writer
 	WIn        io.Reader
+	Location   *time.Location
 }
 
 // Println prints a line to an output writer.
-func (d *DryccCmd) Println(a ...interface{}) (n int, err error) {
+func (d *DryccCmd) Println(a ...any) (n int, err error) {
 	return fmt.Fprintln(d.WOut, a...)
 }
 
 // Print prints a line to an output writer.
-func (d *DryccCmd) Print(a ...interface{}) (n int, err error) {
+func (d *DryccCmd) Print(a ...any) (n int, err error) {
 	return fmt.Fprint(d.WOut, a...)
 }
 
 // Printf prints a line to an error writer.
-func (d *DryccCmd) Printf(s string, a ...interface{}) (n int, err error) {
+func (d *DryccCmd) Printf(s string, a ...any) (n int, err error) {
 	return fmt.Fprintf(d.WOut, s, a...)
 }
 
 // PrintErrln prints a line to an error writer.
-func (d *DryccCmd) PrintErrln(a ...interface{}) (n int, err error) {
+func (d *DryccCmd) PrintErrln(a ...any) (n int, err error) {
 	return fmt.Fprintln(d.WErr, a...)
 }
 
 // PrintErr prints a line to an error writer.
-func (d *DryccCmd) PrintErr(a ...interface{}) (n int, err error) {
+func (d *DryccCmd) PrintErr(a ...any) (n int, err error) {
 	return fmt.Fprint(d.WErr, a...)
 }
 
 // PrintErrf prints a line to an error writer.
-func (d *DryccCmd) PrintErrf(s string, a ...interface{}) (n int, err error) {
+func (d *DryccCmd) PrintErrf(s string, a ...any) (n int, err error) {
 	return fmt.Fprintf(d.WErr, s, a...)
 }
diff --git a/internal/commands/config.go b/internal/commands/config.go
new file mode 100644
index 000000000..bc892b21e
--- /dev/null
+++ b/internal/commands/config.go
@@ -0,0 +1,485 @@
+package commands
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"os"
+	"regexp"
+	"runtime"
+	"strings"
+
+	drycc "github.com/drycc/controller-sdk-go"
+	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/controller-sdk-go/appsettings"
+	"github.com/drycc/controller-sdk-go/config"
+	"github.com/drycc/workflow-cli/internal/loader"
+)
+
+// ConfigInfo for an app
+func (d *DryccCmd) ConfigInfo(appID string, ptype string, group string, version int) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	config, err := config.List(s.Client, appID, version)
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+	// init output struct
+	cv := api.ConfigInfo{
+		Group: make(map[string][]api.ConfigVar),
+		Ptype: make(map[string]api.PtypeValue),
+	}
+	hasGlobalGroup := false
+	for _, value := range sortConfigValues(config.Values) {
+		if value.Group == "global" {
+			hasGlobalGroup = true
+		}
+
+		// display the selected or all
+		if (ptype != "" && value.Ptype == ptype) ||
+			(group != "" && value.Group == group) ||
+			(ptype == "" && group == "") {
+			if value.Group != "" {
+				cv.Group[value.Group] = append(cv.Group[value.Group], api.ConfigVar{Name: value.Name, Value: value.Value})
+			} else if value.Ptype != "" {
+				temp := cv.Ptype[value.Ptype]
+				temp.Env = append(temp.Env, api.ConfigVar{Name: value.Name, Value: value.Value})
+				cv.Ptype[value.Ptype] = temp
+
+				if len(config.ValuesRefs[value.Ptype]) != 0 {
+					temp.Ref = config.ValuesRefs[value.Ptype]
+					cv.Ptype[value.Ptype] = temp
+				}
+			}
+		}
+	}
+
+	for k, v := range config.ValuesRefs {
+		if (ptype != "" && k == ptype) ||
+			(ptype == "" && group == "") {
+			if k != "" {
+				if len(v) != 0 {
+					temp := cv.Ptype[k]
+					temp.Ref = v
+					cv.Ptype[k] = temp
+				}
+			}
+		}
+	}
+
+	if len(cv.Ptype) == 0 && len(cv.Group) == 0 {
+		d.Println()
+		return nil
+	}
+
+	// Format and print the output
+
+	// print group
+	for group, configVars := range cv.Group {
+		d.Println("---\n# Group:", group)
+		var content string
+		for _, configVar := range configVars {
+			content += fmt.Sprintf("%s=%v\n", configVar.Name, configVar.Value)
+		}
+		d.Println(content)
+	}
+	// print ptype
+	for ptype, ptypeValue := range cv.Ptype {
+		d.Printf("---\n# Ptype %s config\n", ptype)
+
+		if len(ptypeValue.Env) > 0 {
+			d.Println("## env")
+			var content string
+			for _, configVar := range ptypeValue.Env {
+				content += fmt.Sprintf("%s=%v\n", configVar.Name, configVar.Value)
+			}
+			d.Println(content)
+		}
+
+		if len(ptypeValue.Ref) > 0 {
+			d.Println("## ref")
+			var content string
+			for _, ref := range ptypeValue.Ref {
+				content += fmt.Sprintf("- %s\n", ref)
+			}
+			if hasGlobalGroup {
+				content += fmt.Sprintf("- %s\n", "global")
+			}
+			d.Println(content)
+		}
+	}
+
+	return nil
+}
+
+// ConfigSet sets an app's config variables.
+func (d *DryccCmd) ConfigSet(appID string, ptype string, group string, configVars []string, merge bool, confirm string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+	if ptype == "" && group == "" {
+		group = "global"
+	}
+	err = configConfirmAction(s.Client, appID, ptype, group, confirm)
+	if err != nil {
+		return err
+	}
+
+	configMap, err := parseConfig(ptype, group, configVars)
+	if err != nil {
+		return err
+	}
+
+	d.Print("Creating config... ")
+
+	quit := progress(d.WOut)
+	configObj := api.Config{Values: configMap}
+	_, err = config.Set(s.Client, appID, configObj, merge)
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+	d.Print("done\n\n")
+	return nil
+}
+
+// ConfigUnset removes a config variable from an app.
+func (d *DryccCmd) ConfigUnset(appID string, ptype string, group string, configVars []string, confirm string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	if ptype == "" && group == "" {
+		group = "global"
+	}
+	err = configConfirmAction(s.Client, appID, ptype, group, confirm)
+	if err != nil {
+		return err
+	}
+
+	d.Print("Removing config... ")
+
+	quit := progress(d.WOut)
+	valuesMaps := []api.ConfigValue{}
+	for _, configVar := range configVars {
+		valuesMap := api.ConfigValue{
+			Ptype: ptype,
+			Group: group,
+			ConfigVar: api.ConfigVar{
+				Name:  configVar,
+				Value: nil,
+			},
+		}
+		valuesMaps = append(valuesMaps, valuesMap)
+	}
+
+	configObj := api.Config{Values: valuesMaps}
+	_, err = config.Set(s.Client, appID, configObj, true)
+
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	d.Print("done\n\n")
+	return nil
+}
+
+// ConfigPull pulls an app's config to a file.
+func (d *DryccCmd) ConfigPull(appID, ptype, group, fileName string, interactive bool, overwrite bool) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	if ptype == "" && group == "" {
+		group = "global"
+	}
+	if ptype != "" && group != "" {
+		d.Println("Only one of ptype and group can be selected.")
+		return nil
+	}
+	configVars, err := config.List(s.Client, appID, -1)
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	stat, err := os.Stdout.Stat()
+	if err != nil {
+		return err
+	}
+	configValues := []api.ConfigValue{}
+
+	for _, value := range configVars.Values {
+		if (ptype != "" && value.Ptype == ptype) ||
+			(group != "" && value.Group == group) {
+			configValues = append(configValues, value)
+		}
+	}
+
+	if (stat.Mode() & os.ModeCharDevice) == 0 {
+		d.Print(formatConfig(configValues))
+		return nil
+	}
+
+	if !overwrite {
+		if _, err := os.Stat(fileName); err == nil {
+			return fmt.Errorf("%s already exists, pass -o to overwrite", fileName)
+		}
+	}
+
+	if interactive {
+		configMap, err := drycc.ParseEnv(fileName)
+		if err != nil {
+			return err
+		}
+		for _, value := range configValues {
+			localValue, ok := configMap[value.Name]
+			if ok {
+				if value.Value != localValue {
+					var confirm string
+					d.Printf("%s: overwrite %s with %s? (y/N) ", value.Name, localValue, value)
+
+					fmt.Scanln(&confirm)
+
+					if strings.ToLower(confirm) == "y" {
+						configMap[value.Name] = value.Value
+					}
+				}
+			} else {
+				configMap[value.Name] = value.Value
+			}
+		}
+		return os.WriteFile(fileName, []byte(formatEnv(configMap)), 0o664)
+	}
+	return os.WriteFile(fileName, []byte(formatConfig(configValues)), 0o664)
+}
+
+// ConfigPush pushes an app's config from a file.
+func (d *DryccCmd) ConfigPush(appID, ptype string, group string, fileName string, merge bool, confirm string) error {
+	stat, err := os.Stdin.Stat()
+	if err != nil {
+		return err
+	}
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+	if ptype == "" && group == "" {
+		group = "global"
+	}
+	var contents []byte
+
+	if (stat.Mode() & os.ModeCharDevice) == 0 {
+
+		err = configConfirmActionStdin(s.Client, appID, ptype, group, confirm)
+		if err != nil {
+			return err
+		}
+		buffer := new(bytes.Buffer)
+		buffer.ReadFrom(os.Stdin)
+		contents = buffer.Bytes()
+	} else {
+		err = configConfirmAction(s.Client, appID, ptype, group, confirm)
+		if err != nil {
+			return err
+		}
+
+		contents, err = os.ReadFile(fileName)
+		if err != nil {
+			return err
+		}
+	}
+
+	file := strings.Split(string(contents), "\n")
+	config := []string{}
+
+	for _, configVar := range file {
+		// If file has CRLF encoding, the default on windows, strip the CR
+		configVar = strings.Trim(configVar, "\r")
+		if len(configVar) > 0 {
+			config = append(config, configVar)
+		}
+	}
+
+	return d.ConfigSet(appID, ptype, group, config, merge, "yes")
+}
+
+// ConfigAttach attaches config groups to a process type.
+func (d *DryccCmd) ConfigAttach(appID string, ptype string, groups string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	d.Print("Attach config... ")
+
+	quit := progress(d.WOut)
+	gs := strings.Split(groups, ",")
+	refs := api.ValuesRefs{
+		ptype: gs,
+	}
+	configObj := api.Config{ValuesRefs: refs}
+	_, err = config.Set(s.Client, appID, configObj, true)
+
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	d.Print("done\n\n")
+	return nil
+}
+
+// ConfigDetach detaches config groups from a process type.
+func (d *DryccCmd) ConfigDetach(appID string, ptype string, groups string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	d.Print("Detach config... ")
+
+	quit := progress(d.WOut)
+	gs := strings.Split(groups, ",")
+	refs := api.ValuesRefs{
+		ptype: gs,
+	}
+	configObj := api.Config{ValuesRefs: refs}
+	err = config.Detach(s.Client, appID, configObj)
+
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	d.Print("done\n\n")
+	return nil
+}
+
+func parseConfig(ptype, group string, configVars []string) ([]api.ConfigValue, error) {
+	configMap := []api.ConfigValue{}
+
+	regex := regexp.MustCompile(`^([A-z0-9_\-\.]+)=([\s\S]*)$`)
+	for _, config := range configVars {
+		// Skip config that starts with an comment
+		if config[0] == '#' {
+			continue
+		}
+
+		if regex.MatchString(config) {
+			captures := regex.FindStringSubmatch(config)
+			value := api.ConfigValue{
+				Ptype: ptype,
+				Group: group,
+				ConfigVar: api.ConfigVar{
+					Name:  captures[1],
+					Value: captures[2],
+				},
+			}
+			configMap = append(configMap, value)
+
+		} else {
+			return nil, fmt.Errorf("'%s' does not match the pattern 'key=var', ex: MODE=test", config)
+		}
+	}
+
+	return configMap, nil
+}
+
+func formatEnv(configVars map[string]any) string {
+	var formattedConfig string
+
+	keys := *sortKeys(configVars)
+	for _, key := range keys {
+		formattedConfig += fmt.Sprintf("%s=%v\n", key, configVars[key])
+	}
+
+	return formattedConfig
+}
+
+func formatConfig(configVars []api.ConfigValue) string {
+	var formattedConfig string
+
+	values := sortConfigValues(configVars)
+	for _, value := range values {
+		formattedConfig += fmt.Sprintf("%s=%v\n", value.Name, value.Value)
+	}
+
+	return formattedConfig
+}
+
+func configConfirmAction(s *drycc.Client, appID string, ptype string, group string, confirm string) error {
+	if ptype != "" && group != "" {
+		fmt.Println("Only one of ptype and group can be selected.")
+		return nil
+	} else if ptype == "" && group == "" {
+		group = "global"
+	}
+
+	appSettings, _ := appsettings.List(s, appID)
+	autodeploy := appSettings.Autodeploy == nil || *appSettings.Autodeploy
+	if ptype == "" && group == "" && (confirm == "" || confirm != "yes") && autodeploy {
+		fmt.Printf(` !    WARNING: Potentially Config Action
+ !    This command will deploy all processes of the application
+ !    To proceed, type "yes" !
+
+> `)
+
+		fmt.Scanln(&confirm)
+		if confirm != "yes" {
+			return fmt.Errorf("cancel the config action")
+		}
+	}
+	return nil
+}
+
+func configConfirmActionStdin(s *drycc.Client, appID string, ptype string, group string, confirm string) error {
+	var reader *bufio.Reader
+	if runtime.GOOS == "windows" {
+		reader = bufio.NewReader(os.Stdin)
+	} else {
+		file, err := os.Open("/dev/tty")
+		if err != nil {
+			return err
+		}
+		defer file.Close()
+		reader = bufio.NewReader(file)
+	}
+
+	if ptype != "" && group != "" {
+		fmt.Println("Only one of ptype and group can be selected.")
+		return nil
+	}
+
+	appSettings, _ := appsettings.List(s, appID)
+	autodeploy := appSettings.Autodeploy == nil || *appSettings.Autodeploy
+
+	if ptype == "" && group == "" && (confirm == "" || confirm != "yes") && autodeploy {
+		fmt.Printf(` !    WARNING: Potentially Config Action
+ !    This command will deploy all processes of the application
+ !    To proceed, type "yes" !
+
+> `)
+
+		confirm, err := reader.ReadString('\n')
+		if err != nil {
+			return err
+		}
+
+		confirm = strings.TrimSpace(confirm)
+		if confirm != "yes" {
+			return fmt.Errorf("cancel the config action")
+		}
+	}
+	return nil
+}
diff --git a/internal/commands/config_test.go b/internal/commands/config_test.go
new file mode 100644
index 000000000..46e6614e0
--- /dev/null
+++ b/internal/commands/config_test.go
@@ -0,0 +1,208 @@
+package commands
+
+import (
+	"bytes"
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/workflow-cli/pkg/testutil"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestParseConfig(t *testing.T) {
+	t.Parallel()
+
+	_, err := parseConfig("", "", []string{"FOO=bar", "CAR star"})
+	assert.NotEqual(t, err, nil, "config")
+
+	actual, err := parseConfig("", "", []string{"FOO=bar"})
+	assert.NoError(t, err)
+	assert.Equal(t, actual, []api.ConfigValue{{ConfigVar: api.ConfigVar{Name: "FOO", Value: "bar"}}}, "map")
+
+	actual, err = parseConfig("", "", []string{"FOO="})
+	assert.NoError(t, err)
+	assert.Equal(t, actual, []api.ConfigValue{{ConfigVar: api.ConfigVar{Name: "FOO", Value: ""}}}, "map")
+}
+
+func TestFormatConfig(t *testing.T) {
+	t.Parallel()
+
+	testMap := []api.ConfigValue{
+		{
+			Ptype: "web",
+			Group: "",
+			ConfigVar: api.ConfigVar{
+				Name:  "TEST",
+				Value: "testing",
+			},
+		}, {
+			Ptype: "web",
+			Group: "",
+			ConfigVar: api.ConfigVar{
+				Name:  "NCC",
+				Value: "1701",
+			},
+		}, {
+			Ptype: "web",
+			Group: "",
+			ConfigVar: api.ConfigVar{
+				Name:  "TRUE",
+				Value: false,
+			},
+		}, {
+			Ptype: "web",
+			Group: "",
+			ConfigVar: api.ConfigVar{
+				Name:  "FLOAT",
+				Value: 12.34,
+			},
+		},
+	}
+
+	testOut := formatConfig(testMap)
+	assert.Equal(t, testOut, `FLOAT=12.34
+NCC=1701
+TEST=testing
+TRUE=false
+`, "output")
+}
+
+func TestSortKeys(t *testing.T) {
+	test := map[string]any{
+		"d": nil,
+		"b": nil,
+		"c": nil,
+		"a": nil,
+	}
+
+	assert.Equal(t, *sortKeys(test), []string{"a", "b", "c", "d"}, "map")
+}
+
+func TestConfigSet(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+
+	server.Mux.HandleFunc("/v2/apps/foo/config/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.SetHeaders(w)
+		if r.Method == "POST" {
+			testutil.AssertBody(t, api.Config{
+				Values: []api.ConfigValue{
+					{
+						Ptype: "web",
+						ConfigVar: api.ConfigVar{
+							Name:  "TRUE",
+							Value: "false",
+						},
+					},
+					{
+						Ptype: "web",
+						ConfigVar: api.ConfigVar{
+							Name:  "DEBUG",
+							Value: "true",
+						},
+					},
+				},
+			}, r)
+		}
+
+		fmt.Fprintf(w, `{
+	"owner": "jkirk",
+	"app": "foo",
+	"values": [
+	  {
+	    "ptype": "web",
+	    "name": "TRUE",
+		"value": "false"
+	  },
+	  {
+	    "ptype": "web",
+	    "name": "DEBUG",
+		"value": "true"
+	  }
+	],
+	"memory": {},
+	"cpu": {},
+	"tags": {},
+	"registry": {},
+	"created": "2014-01-01T00:00:00UTC",
+	"updated": "2014-01-01T00:00:00UTC",
+	"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
+}`)
+	})
+
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	err = cmdr.ConfigSet("foo", "web", "", []string{"TRUE=false", "DEBUG=true"}, true, "yes")
+	assert.NoError(t, err)
+
+	assert.Equal(t, testutil.StripProgress(b.String()), `Creating config... done
+
+`, "output")
+}
+
+func TestConfigUnset(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+
+	server.Mux.HandleFunc("/v2/apps/foo/config/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.SetHeaders(w)
+		if r.Method == "POST" {
+			testutil.AssertBody(t, api.Config{
+				Values: []api.ConfigValue{
+					{
+						Ptype: "web",
+						ConfigVar: api.ConfigVar{
+							Name:  "FOO",
+							Value: nil,
+						},
+					},
+				},
+			}, r)
+		}
+
+		fmt.Fprintf(w, `{
+	"owner": "jkirk",
+	"app": "foo",
+	"values": [
+	  {
+	    "ptype": "web",
+	    "name": "FLOAT",
+		"value": "12.34"
+	  },
+	  {
+	    "ptype": "web",
+	    "name": "NCC",
+		"value": "1701"
+	  }
+	],
+	"memory": {},
+	"cpu": {},
+	"tags": {},
+	"registry": {},
+	"created": "2014-01-01T00:00:00UTC",
+	"updated": "2014-01-01T00:00:00UTC",
+	"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
+}`)
+	})
+
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	err = cmdr.ConfigUnset("foo", "web", "", []string{"FOO"}, "yes")
+	assert.NoError(t, err)
+
+	assert.Equal(t, testutil.StripProgress(b.String()), `Removing config... done
+
+`, "output")
+}
diff --git a/cmd/domains.go b/internal/commands/domains.go
similarity index 71%
rename from cmd/domains.go
rename to internal/commands/domains.go
index 05e402a50..66044981f 100644
--- a/cmd/domains.go
+++ b/internal/commands/domains.go
@@ -1,15 +1,15 @@
-package cmd
+package commands
 
 import (
 	"fmt"
 
 	"github.com/drycc/controller-sdk-go/domains"
+	"github.com/drycc/workflow-cli/internal/loader"
 )
 
 // DomainsList lists domains registered with an app.
 func (d *DryccCmd) DomainsList(appID string, results int) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -23,13 +23,13 @@ func (d *DryccCmd) DomainsList(appID string, results int) error {
 		return err
 	}
 	if count > 0 {
-		table := d.getDefaultFormatTable([]string{"APP", "OWNER", "CREATED", "UPDATED", "DOMAIN"})
+		table := d.getDefaultFormatTable([]string{"APP", "PTYPE", "CREATED", "UPDATED", "DOMAIN"})
 		for _, domain := range domains {
 			table.Append([]string{
 				domain.App,
-				domain.Owner,
-				domain.Created,
-				domain.Updated,
+				domain.Ptype,
+				d.formatTime(domain.Created),
+				d.formatTime(domain.Updated),
 				domain.Domain,
 			})
 		}
@@ -41,9 +41,8 @@ func (d *DryccCmd) DomainsList(appID string, results int) error {
 }
 
 // DomainsAdd adds a domain to an app.
-func (d *DryccCmd) DomainsAdd(appID, domain string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+func (d *DryccCmd) DomainsAdd(appID, domain, ptype string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -51,7 +50,7 @@ func (d *DryccCmd) DomainsAdd(appID, domain string) error {
 	d.Printf("Adding %s to %s... ", domain, appID)
 
 	quit := progress(d.WOut)
-	_, err = domains.New(s.Client, appID, domain)
+	_, err = domains.New(s.Client, appID, domain, ptype)
 	quit <- true
 	<-quit
 	if d.checkAPICompatibility(s.Client, err) != nil {
@@ -64,8 +63,7 @@ func (d *DryccCmd) DomainsAdd(appID, domain string) error {
 
 // DomainsRemove removes a domain registered with an app.
 func (d *DryccCmd) DomainsRemove(appID, domain string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/domains_test.go b/internal/commands/domains_test.go
similarity index 87%
rename from cmd/domains_test.go
rename to internal/commands/domains_test.go
index dc87e56b1..cdbdc337c 100644
--- a/cmd/domains_test.go
+++ b/internal/commands/domains_test.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"bytes"
@@ -32,6 +32,7 @@ func TestDomainsList(t *testing.T) {
             "app": "foo",
             "created": "2014-01-01T00:00:00UTC",
             "domain": "example.example.com",
+			"ptype": "web",
             "owner": "test",
             "updated": "2014-01-01T00:00:00UTC"
         },
@@ -39,6 +40,7 @@ func TestDomainsList(t *testing.T) {
             "app": "foo",
             "created": "2014-01-01T00:00:00UTC",
             "domain": "foo",
+			"ptype": "web",
             "owner": "test",
             "updated": "2014-01-01T00:00:00UTC"
         }
@@ -49,9 +51,9 @@ func TestDomainsList(t *testing.T) {
 	err = cmdr.DomainsList("foo", -1)
 	assert.NoError(t, err)
 
-	assert.Equal(t, b.String(), `APP    OWNER    CREATED                   UPDATED                   DOMAIN              
-foo    test     2014-01-01T00:00:00UTC    2014-01-01T00:00:00UTC    example.example.com    
-foo    test     2014-01-01T00:00:00UTC    2014-01-01T00:00:00UTC    foo                    
+	assert.Equal(t, b.String(), `APP    PTYPE    CREATED                   UPDATED                   DOMAIN              
+foo    web      2014-01-01T00:00:00UTC    2014-01-01T00:00:00UTC    example.example.com    
+foo    web      2014-01-01T00:00:00UTC    2014-01-01T00:00:00UTC    foo                    
 `, "output")
 }
 
@@ -76,6 +78,7 @@ func TestDomainsListLimit(t *testing.T) {
             "app": "foo",
             "created": "2014-01-01T00:00:00UTC",
             "domain": "example.example.com",
+			"ptype": "web",
             "owner": "test",
             "updated": "2014-01-01T00:00:00UTC"
         }
@@ -86,8 +89,8 @@ func TestDomainsListLimit(t *testing.T) {
 	err = cmdr.DomainsList("foo", 1)
 	assert.NoError(t, err)
 
-	assert.Equal(t, b.String(), `APP    OWNER    CREATED                   UPDATED                   DOMAIN              
-foo    test     2014-01-01T00:00:00UTC    2014-01-01T00:00:00UTC    example.example.com    
+	assert.Equal(t, b.String(), `APP    PTYPE    CREATED                   UPDATED                   DOMAIN              
+foo    web      2014-01-01T00:00:00UTC    2014-01-01T00:00:00UTC    example.example.com    
 `, "output")
 }
 
@@ -102,14 +105,14 @@ func TestDomainsAdd(t *testing.T) {
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
 	server.Mux.HandleFunc("/v2/apps/foo/domains/", func(w http.ResponseWriter, r *http.Request) {
-		testutil.AssertBody(t, api.DomainCreateRequest{Domain: "example.example.com"}, r)
+		testutil.AssertBody(t, api.DomainCreateRequest{Domain: "example.example.com", Ptype: "web"}, r)
 		testutil.SetHeaders(w)
 		w.WriteHeader(http.StatusCreated)
 		// Body isn't used by CLI, so it isn't set.
 		w.Write([]byte("{}"))
 	})
 
-	err = cmdr.DomainsAdd("foo", "example.example.com")
+	err = cmdr.DomainsAdd("foo", "example.example.com", "web")
 	assert.NoError(t, err)
 
 	assert.Equal(t, testutil.StripProgress(b.String()), "Adding example.example.com to foo... done\n", "output")
diff --git a/cmd/gateways.go b/internal/commands/gateways.go
similarity index 89%
rename from cmd/gateways.go
rename to internal/commands/gateways.go
index acec14c50..432439a90 100644
--- a/cmd/gateways.go
+++ b/internal/commands/gateways.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"fmt"
@@ -6,12 +6,12 @@ import (
 
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/gateways"
+	"github.com/drycc/workflow-cli/internal/loader"
 )
 
 // GatewaysList lists gateways for the app
 func (d *DryccCmd) GatewaysList(appID string, results int) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -40,8 +40,7 @@ func (d *DryccCmd) GatewaysList(appID string, results int) error {
 
 // GatewaysAdd adds a gateway to an app.
 func (d *DryccCmd) GatewaysAdd(appID, name string, port int, protocol string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -61,8 +60,7 @@ func (d *DryccCmd) GatewaysAdd(appID, name string, port int, protocol string) er
 
 // GatewaysRemove removes a gateway registered with an app.
 func (d *DryccCmd) GatewaysRemove(appID, name string, port int, protocol string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/gateways_test.go b/internal/commands/gateways_test.go
similarity index 99%
rename from cmd/gateways_test.go
rename to internal/commands/gateways_test.go
index 2ffc98d34..463c8d41d 100644
--- a/cmd/gateways_test.go
+++ b/internal/commands/gateways_test.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"bytes"
@@ -7,9 +7,8 @@ import (
 	"testing"
 
 	"github.com/drycc/controller-sdk-go/api"
-	"github.com/stretchr/testify/assert"
-
 	"github.com/drycc/workflow-cli/pkg/testutil"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestGatewaysList(t *testing.T) {
diff --git a/cmd/git.go b/internal/commands/git.go
similarity index 77%
rename from cmd/git.go
rename to internal/commands/git.go
index e76d2b9f2..6b0f69a01 100644
--- a/cmd/git.go
+++ b/internal/commands/git.go
@@ -1,25 +1,27 @@
-package cmd
+package commands
 
 import (
 	"fmt"
 
+	"github.com/drycc/workflow-cli/internal/loader"
 	"github.com/drycc/workflow-cli/pkg/git"
 )
 
-const remoteCreationMsg = "Git remote %s successfully created for app %s.\n"
-const remoteDeletionMsg = "Git remotes for app %s removed.\n"
+const (
+	remoteCreationMsg = "Git remote %s successfully created for app %s.\n"
+	remoteDeletionMsg = "Git remotes for app %s removed.\n"
+)
 
 // GitRemote creates a git remote for a drycc app.
 func (d *DryccCmd) GitRemote(appID, remote string, force bool) error {
-	s, appID, err := load(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
 
 	remoteURL, err := git.RemoteURL(git.DefaultCmd, remote)
-
 	if err != nil {
-		//If git remote doesn't exist, create it without issue
+		// If git remote doesn't exist, create it without issue
 		if err == git.ErrRemoteNotFound {
 			err := git.CreateRemote(git.DefaultCmd, s.Client.ControllerURL.Host, remote, appID)
 			if err != nil {
@@ -53,7 +55,7 @@ func (d *DryccCmd) GitRemote(appID, remote string, force bool) error {
 		return nil
 	}
 
-	msg := "Remote %s already exists, please run 'drycc git:remote -f' to overwrite\n"
+	msg := "Remote %s already exists, please run 'drycc git remote -f' to overwrite\n"
 	msg += "Existing remote URL: %s\n"
 	msg += "When forced, will overwrite with: %s"
 
@@ -62,14 +64,12 @@ func (d *DryccCmd) GitRemote(appID, remote string, force bool) error {
 
 // GitRemove removes a application git remote from a repository
 func (d *DryccCmd) GitRemove(appID string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
 
 	err = git.DeleteAppRemotes(git.DefaultCmd, s.Client.ControllerURL.Host, appID)
-
 	if err != nil {
 		return err
 	}
diff --git a/internal/commands/healthchecks.go b/internal/commands/healthchecks.go
new file mode 100644
index 000000000..215196512
--- /dev/null
+++ b/internal/commands/healthchecks.go
@@ -0,0 +1,189 @@
+package commands
+
+import (
+	"fmt"
+	"sort"
+
+	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/controller-sdk-go/config"
+	"github.com/drycc/workflow-cli/internal/loader"
+)
+
+func getContainerProbeString(ptype, probeType string, containerProbe *api.ContainerProbe) string {
+	params := fmt.Sprintf(
+		"delay=%ds timeout=%ds period=%ds #success=%d #failure=%d",
+		containerProbe.InitialDelaySeconds,
+		containerProbe.TimeoutSeconds,
+		containerProbe.PeriodSeconds,
+		containerProbe.SuccessThreshold,
+		containerProbe.FailureThreshold,
+	)
+
+	if containerProbe.Exec != nil {
+		return fmt.Sprintf("%s %s exec %v %s", probeType, ptype, containerProbe.Exec.Command, params)
+	} else if containerProbe.TCPSocket != nil {
+		return fmt.Sprintf("%s %s tcp-socket port=%v %s", probeType, ptype, containerProbe.TCPSocket.Port, params)
+	} else if containerProbe.HTTPGet != nil {
+		return fmt.Sprintf(
+			"%s %s http-get headers=%v path=%s port=%d %s",
+			probeType,
+			ptype,
+			containerProbe.HTTPGet.HTTPHeaders,
+			containerProbe.HTTPGet.Path,
+			containerProbe.HTTPGet.Port,
+			params,
+		)
+	}
+	return ""
+}
+
+func getHealthchecksStrings(ptype string, healthcheck *api.Healthcheck) []string {
+	var containerProbes []string
+	if healthcheck.StartupProbe != nil {
+		containerProbes = append(containerProbes, getContainerProbeString(ptype, "startupProbe", *healthcheck.StartupProbe))
+	} else if healthcheck.LivenessProbe != nil {
+		containerProbes = append(containerProbes, getContainerProbeString(ptype, "livenessProbe", *healthcheck.LivenessProbe))
+	} else if healthcheck.ReadinessProbe != nil {
+		containerProbes = append(containerProbes, getContainerProbeString(ptype, "readinessProbe", *healthcheck.ReadinessProbe))
+	}
+	return containerProbes
+}
+
+// HealthchecksList lists an app's healthchecks.
+func (d *DryccCmd) HealthchecksList(appID, ptype string, version int) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	config, err := config.List(s.Client, appID, version)
+	if err != nil {
+		return err
+	}
+
+	if ptype == "" {
+		if len(config.Healthcheck) == 0 {
+			d.Println("No health checks configured.")
+		} else {
+			var keys []string
+			for k := range config.Healthcheck {
+				keys = append(keys, k)
+			}
+			sort.Strings(keys)
+			table := d.getDefaultFormatTable([]string{})
+			table.Append([]string{"App:", config.App})
+			table.Append([]string{"UUID:", config.UUID})
+			table.Append([]string{"Created:", d.formatTime(config.Created)})
+			table.Append([]string{"Updated:", d.formatTime(config.Updated)})
+			table.Append([]string{"Healthchecks:"})
+			for _, key := range keys {
+				for _, probe := range getHealthchecksStrings(key, config.Healthcheck[key]) {
+					if probe != "" {
+						table.Append([]string{"", probe})
+					}
+				}
+			}
+			table.Render()
+		}
+	} else {
+		if healthcheck, found := config.Healthcheck[ptype]; found {
+			table := d.getDefaultFormatTable([]string{})
+			table.Append([]string{"App:", config.App})
+			table.Append([]string{"UUID:", config.UUID})
+			table.Append([]string{"Created:", d.formatTime(config.Created)})
+			table.Append([]string{"Updated:", d.formatTime(config.Updated)})
+			table.Append([]string{"Healthchecks:"})
+			for _, probe := range getHealthchecksStrings(ptype, healthcheck) {
+				if probe != "" {
+					table.Append([]string{"", probe})
+				}
+			}
+			table.Render()
+		} else {
+			d.Println("No health checks configured.")
+		}
+	}
+	return nil
+}
+
+// HealthchecksSet sets an app's healthchecks.
+func (d *DryccCmd) HealthchecksSet(appID, healthcheckType, ptype string, probe *api.ContainerProbe) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	d.Printf("Applying %s healthcheck... ", healthcheckType)
+
+	quit := progress(d.WOut)
+	var healthcheck api.Healthcheck
+	switch healthcheckType {
+	case "livenessProbe":
+		healthcheck.LivenessProbe = &probe
+	case "readinessProbe":
+		healthcheck.ReadinessProbe = &probe
+	case "startupProbe":
+		healthcheck.StartupProbe = &probe
+	default:
+		return fmt.Errorf("unknown healthcheck type: %s", healthcheckType)
+	}
+	configObj := api.Config{Healthcheck: make(map[string]*api.Healthcheck)}
+	configObj.Healthcheck[ptype] = &healthcheck
+
+	_, err = config.Set(s.Client, appID, configObj, true)
+
+	quit <- true
+	<-quit
+
+	if err != nil {
+		return err
+	}
+
+	d.Print("done\n\n")
+
+	return d.HealthchecksList(appID, ptype, -1)
+}
+
+// HealthchecksUnset removes an app's healthchecks.
+func (d *DryccCmd) HealthchecksUnset(appID, ptype string, containerProbeTypes []string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	d.Print("Removing healthchecks... ")
+
+	quit := progress(d.WOut)
+
+	configObj := api.Config{}
+
+	healthcheckMap := make(map[string]*api.Healthcheck)
+	var nullContainerProbe *api.ContainerProbe
+	for _, containerProbeType := range containerProbeTypes {
+		healthcheck := &api.Healthcheck{}
+		switch containerProbeType {
+		case "livenessProbe":
+			healthcheck.LivenessProbe = &nullContainerProbe
+		case "readinessProbe":
+			healthcheck.ReadinessProbe = &nullContainerProbe
+		case "startupProbe":
+			healthcheck.StartupProbe = &nullContainerProbe
+		default:
+			return fmt.Errorf("unknown container probe type: %s", containerProbeType)
+		}
+		healthcheckMap[containerProbeType] = healthcheck
+	}
+	configObj.Healthcheck = healthcheckMap
+	_, err = config.Set(s.Client, appID, configObj, true)
+
+	quit <- true
+	<-quit
+
+	if err != nil {
+		return err
+	}
+
+	d.Print("done\n\n")
+
+	return d.HealthchecksList(appID, ptype, -1)
+}
diff --git a/cmd/healthchecks_test.go b/internal/commands/healthchecks_test.go
similarity index 92%
rename from cmd/healthchecks_test.go
rename to internal/commands/healthchecks_test.go
index 174ae3ee2..1abbcd690 100644
--- a/cmd/healthchecks_test.go
+++ b/internal/commands/healthchecks_test.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"bytes"
@@ -27,7 +27,7 @@ func TestHealthchecksList(t *testing.T) {
   "uuid": "c039a380-6068-4511-b35a-535a73b86ef5",
   "app": "foo",
   "owner": "bar",
-  "values": {},
+  "values": [],
   "memory": {},
   "cpu": {},
   "tags": {},
@@ -52,12 +52,11 @@ func TestHealthchecksList(t *testing.T) {
 }`)
 	})
 
-	err = cmdr.HealthchecksList("foo", "web")
+	err = cmdr.HealthchecksList("foo", "web", -1)
 	assert.NoError(t, err)
 
 	assert.Equal(t, b.String(), `App:             foo                                                                                                            
 UUID:            c039a380-6068-4511-b35a-535a73b86ef5                                                                           
-Owner:           bar                                                                                                            
 Created:         2016-09-12T22:20:14Z                                                                                           
 Updated:         2016-09-12T22:20:14Z                                                                                           
 Healthchecks:    
@@ -81,7 +80,7 @@ func TestHealthchecksListNoHealthCheck(t *testing.T) {
   "uuid": "c039a380-6068-4511-b35a-535a73b86ef5",
   "app": "foo",
   "owner": "bar",
-  "values": {},
+  "values": [],
   "memory": {},
   "cpu": {},
   "tags": {},
@@ -92,7 +91,7 @@ func TestHealthchecksListNoHealthCheck(t *testing.T) {
 }`)
 	})
 
-	err = cmdr.HealthchecksList("foo", "")
+	err = cmdr.HealthchecksList("foo", "", -1)
 	assert.NoError(t, err)
 
 	assert.Equal(t, b.String(), `No health checks configured.
@@ -115,7 +114,7 @@ func TestHealthchecksListAllHealthChecks(t *testing.T) {
   "uuid": "c039a380-6068-4511-b35a-535a73b86ef5",
   "app": "foo",
   "owner": "bar",
-  "values": {},
+  "values": [],
   "memory": {},
   "cpu": {},
   "tags": {},
@@ -153,12 +152,11 @@ func TestHealthchecksListAllHealthChecks(t *testing.T) {
 }`)
 	})
 
-	err = cmdr.HealthchecksList("foo", "")
+	err = cmdr.HealthchecksList("foo", "", -1)
 	assert.NoError(t, err)
 
 	assert.Equal(t, b.String(), `App:             foo                                                                                                               
 UUID:            c039a380-6068-4511-b35a-535a73b86ef5                                                                              
-Owner:           bar                                                                                                               
 Created:         2016-09-12T22:20:14Z                                                                                              
 Updated:         2016-09-12T22:20:14Z                                                                                              
 Healthchecks:    
@@ -183,7 +181,7 @@ func TestHealthchecksSet(t *testing.T) {
   "uuid": "c039a380-6068-4511-b35a-535a73b86ef5",
   "app": "foo",
   "owner": "bar",
-  "values": {},
+  "values": [],
   "memory": {},
   "cpu": {},
   "tags": {},
@@ -208,13 +206,12 @@ func TestHealthchecksSet(t *testing.T) {
 }`)
 	})
 
-	err = cmdr.HealthchecksSet("foo", "livenessProbe", "web", &api.Healthcheck{})
+	err = cmdr.HealthchecksSet("foo", "livenessProbe", "web", &api.ContainerProbe{})
 	assert.NoError(t, err)
 	assert.Equal(t, testutil.StripProgress(b.String()), `Applying livenessProbe healthcheck... done
 
 App:             foo                                                                                                            
 UUID:            c039a380-6068-4511-b35a-535a73b86ef5                                                                           
-Owner:           bar                                                                                                            
 Created:         2016-09-12T22:20:14Z                                                                                           
 Updated:         2016-09-12T22:20:14Z                                                                                           
 Healthchecks:    
@@ -238,7 +235,7 @@ func TestHealthchecksUnset(t *testing.T) {
   "uuid": "c039a380-6068-4511-b35a-535a73b86ef5",
   "app": "foo",
   "owner": "bar",
-  "values": {},
+  "values": [],
   "memory": {},
   "cpu": {},
   "tags": {},
diff --git a/cmd/keys.go b/internal/commands/keys.go
similarity index 95%
rename from cmd/keys.go
rename to internal/commands/keys.go
index d0d964ef6..cc2476a8c 100644
--- a/cmd/keys.go
+++ b/internal/commands/keys.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"fmt"
@@ -10,14 +10,13 @@ import (
 
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/keys"
+	"github.com/drycc/workflow-cli/pkg/settings"
 	"github.com/drycc/workflow-cli/pkg/ssh"
-	"github.com/drycc/workflow-cli/settings"
 )
 
 // KeysList lists a user's keys.
 func (d *DryccCmd) KeysList(results int) error {
 	s, err := settings.Load(d.ConfigFile)
-
 	if err != nil {
 		return err
 	}
@@ -32,11 +31,10 @@ func (d *DryccCmd) KeysList(results int) error {
 	}
 
 	if len(keys) > 0 {
-		table := d.getDefaultFormatTable([]string{"ID", "OWNER", "KEY"})
+		table := d.getDefaultFormatTable([]string{"ID", "KEY"})
 		for _, key := range keys {
 			table.Append([]string{
 				key.ID,
-				key.Owner,
 				fmt.Sprintf("%s...%s", key.Public[:16], key.Public[len(key.Public)-10:]),
 			})
 		}
@@ -50,7 +48,6 @@ func (d *DryccCmd) KeysList(results int) error {
 // KeyRemove removes keys.
 func (d *DryccCmd) KeyRemove(keyID string) error {
 	s, err := settings.Load(d.ConfigFile)
-
 	if err != nil {
 		return err
 	}
@@ -69,7 +66,6 @@ func (d *DryccCmd) KeyRemove(keyID string) error {
 // KeyAdd adds keys.
 func (d *DryccCmd) KeyAdd(name string, keyLocation string) error {
 	s, err := settings.Load(d.ConfigFile)
-
 	if err != nil {
 		return err
 	}
@@ -119,7 +115,8 @@ func (d *DryccCmd) KeyAdd(name string, keyLocation string) error {
 }
 
 func chooseKey(keys []api.KeyCreateRequest, input io.Reader,
-	wOut io.Writer) (api.KeyCreateRequest, error) {
+	wOut io.Writer,
+) (api.KeyCreateRequest, error) {
 	fmt.Fprintln(wOut, "Found the following SSH public keys:")
 
 	for i, key := range keys {
@@ -134,7 +131,6 @@ func chooseKey(keys []api.KeyCreateRequest, input io.Reader,
 	fmt.Fscanln(input, &selected)
 
 	numSelected, err := strconv.Atoi(selected)
-
 	if err != nil {
 		return api.KeyCreateRequest{}, fmt.Errorf("%s is not a valid integer", selected)
 	}
@@ -158,7 +154,6 @@ func chooseKey(keys []api.KeyCreateRequest, input io.Reader,
 func listKeys(wOut io.Writer) ([]api.KeyCreateRequest, error) {
 	folder := filepath.Join(settings.FindHome(), ".ssh")
 	files, err := os.ReadDir(folder)
-
 	if err != nil {
 		return nil, err
 	}
@@ -182,7 +177,6 @@ func listKeys(wOut io.Writer) ([]api.KeyCreateRequest, error) {
 
 func getKey(filename string) (api.KeyCreateRequest, error) {
 	keyContents, err := os.ReadFile(filename)
-
 	if err != nil {
 		return api.KeyCreateRequest{}, err
 	}
diff --git a/cmd/keys_test.go b/internal/commands/keys_test.go
similarity index 93%
rename from cmd/keys_test.go
rename to internal/commands/keys_test.go
index de4735cb2..620fc5e67 100644
--- a/cmd/keys_test.go
+++ b/internal/commands/keys_test.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"bytes"
@@ -11,8 +11,8 @@ import (
 	"testing"
 
 	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/workflow-cli/pkg/settings"
 	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/drycc/workflow-cli/settings"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -88,7 +88,7 @@ func TestListKeys(t *testing.T) {
 
 	folder := filepath.Join(name, ".ssh")
 
-	err = os.Mkdir(folder, 0755)
+	err = os.Mkdir(folder, 0o600)
 	assert.NoError(t, err)
 
 	toWrite := []byte("ssh-rsa abc test@example.com")
@@ -108,7 +108,7 @@ func TestListKeys(t *testing.T) {
 	}
 
 	for _, file := range fileNames {
-		os.WriteFile(filepath.Join(folder, file), toWrite, 0775)
+		os.WriteFile(filepath.Join(folder, file), toWrite, 0o775)
 		assert.NoError(t, err)
 	}
 
@@ -120,11 +120,10 @@ func TestListKeys(t *testing.T) {
 	var b bytes.Buffer
 	// Write bad ssh key
 	filename := filepath.Join(folder, "test3.pub")
-	os.WriteFile(filename, []byte("ssh-rsa"), 0775)
+	os.WriteFile(filename, []byte("ssh-rsa"), 0o775)
 	_, err = listKeys(&b)
 	assert.Equal(t, b.String(), filename+" is not a valid ssh key\n", "output")
 	assert.NoError(t, err)
-
 }
 
 type chooseKeyCases struct {
@@ -234,9 +233,9 @@ func TestKeysList(t *testing.T) {
 
 	err = cmdr.KeysList(-1)
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `ID                              OWNER    KEY                           
-cpike@starfleet.ufp             cpike    ssh-rsa abc cpik...rfleet.ufp    
-cpike@1701.ncc.starfleet.ufp    cpike    ssh-rsa 123 cpik...rfleet.ufp    
+	assert.Equal(t, b.String(), `ID                              KEY                           
+cpike@starfleet.ufp             ssh-rsa abc cpik...rfleet.ufp    
+cpike@1701.ncc.starfleet.ufp    ssh-rsa 123 cpik...rfleet.ufp    
 `, "output")
 }
 
@@ -271,8 +270,8 @@ func TestKeysListLimit(t *testing.T) {
 
 	err = cmdr.KeysList(1)
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `ID                     OWNER    KEY                           
-cpike@starfleet.ufp    cpike    ssh-rsa abc cpik...rfleet.ufp    
+	assert.Equal(t, b.String(), `ID                     KEY                           
+cpike@starfleet.ufp    ssh-rsa abc cpik...rfleet.ufp    
 `, "output")
 }
 
@@ -302,7 +301,7 @@ func TestKeyAdd(t *testing.T) {
 	assert.NoError(t, err)
 	settings.SetHome(name)
 	folder := filepath.Join(name, ".ssh")
-	err = os.Mkdir(folder, 0755)
+	err = os.Mkdir(folder, 0o600)
 	assert.NoError(t, err)
 
 	cf, server, err := testutil.NewTestServerAndClient()
@@ -348,7 +347,7 @@ func TestKeyAddName(t *testing.T) {
 	assert.NoError(t, err)
 	settings.SetHome(name)
 	folder := filepath.Join(name, ".ssh")
-	err = os.Mkdir(folder, 0755)
+	err = os.Mkdir(folder, 0o600)
 	assert.NoError(t, err)
 
 	cf, server, err := testutil.NewTestServerAndClient()
diff --git a/cmd/labels.go b/internal/commands/labels.go
similarity index 82%
rename from cmd/labels.go
rename to internal/commands/labels.go
index 9c6fa5d4c..a5ed57f17 100644
--- a/cmd/labels.go
+++ b/internal/commands/labels.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"fmt"
@@ -6,12 +6,12 @@ import (
 
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/appsettings"
+	"github.com/drycc/workflow-cli/internal/loader"
 )
 
 // LabelsList list app's labels
 func (d *DryccCmd) LabelsList(appID string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -24,11 +24,9 @@ func (d *DryccCmd) LabelsList(appID string) error {
 	if len(appSettings.Label) == 0 {
 		d.Println(fmt.Sprintf("No labels found in %s app.", appID))
 	} else {
-		table := d.getDefaultFormatTable([]string{"UUID", "OWNER", "KEY", "VALUE"})
+		table := d.getDefaultFormatTable([]string{"KEY", "VALUE"})
 		for _, key := range *sortKeys(appSettings.Label) {
 			table.Append([]string{
-				appSettings.UUID,
-				appSettings.Owner,
 				key,
 				fmt.Sprintf("%v", appSettings.Label[key]),
 			})
@@ -41,8 +39,7 @@ func (d *DryccCmd) LabelsList(appID string) error {
 
 // LabelsSet sets labels for app
 func (d *DryccCmd) LabelsSet(appID string, labels []string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -71,13 +68,12 @@ func (d *DryccCmd) LabelsSet(appID string, labels []string) error {
 
 // LabelsUnset removes labels for the app.
 func (d *DryccCmd) LabelsUnset(appID string, labels []string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
 
-	labelsMap := make(map[string]interface{})
+	labelsMap := make(map[string]any)
 
 	for _, label := range labels {
 		labelsMap[label] = nil
@@ -100,12 +96,11 @@ func (d *DryccCmd) LabelsUnset(appID string, labels []string) error {
 	return nil
 }
 
-func parseLabels(labels []string) (map[string]interface{}, error) {
-	labelsMap := make(map[string]interface{})
+func parseLabels(labels []string) (map[string]any, error) {
+	labelsMap := make(map[string]any)
 
 	for _, label := range labels {
 		key, value, err := parseLabel(label)
-
 		if err != nil {
 			return nil, err
 		}
diff --git a/cmd/labels_test.go b/internal/commands/labels_test.go
similarity index 85%
rename from cmd/labels_test.go
rename to internal/commands/labels_test.go
index c95ba8070..9635d011c 100644
--- a/cmd/labels_test.go
+++ b/internal/commands/labels_test.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"bytes"
@@ -35,10 +35,10 @@ func TestLabelsList(t *testing.T) {
 
 	err = cmdr.LabelsList("rivendell")
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `UUID                                    OWNER    KEY         VALUE                                      
-de1bf5b5-4a72-4f94-a10c-d2a3741cdf75    jim      git_repo    https://github.com/drycc/controller-sdk-go    
-de1bf5b5-4a72-4f94-a10c-d2a3741cdf75    jim      team        drycc                                         
-`, "output")
+	testutil.AssertOutput(t, b.String(), `KEY         VALUE
+git_repo    https://github.com/drycc/controller-sdk-go
+team        drycc
+`)
 
 	server.Mux.HandleFunc("/v2/apps/mordor/settings/", func(w http.ResponseWriter, _ *http.Request) {
 		testutil.SetHeaders(w)
@@ -69,7 +69,7 @@ func TestListsSet(t *testing.T) {
 
 	server.Mux.HandleFunc("/v2/apps/lothlorien/settings/", func(w http.ResponseWriter, r *http.Request) {
 		testutil.SetHeaders(w)
-		data := map[string]interface{}{
+		data := map[string]any{
 			"git_repo": "https://github.com/drycc/controller-sdk-go",
 			"team":     "drycc",
 		}
@@ -97,7 +97,7 @@ func TestListsUnset(t *testing.T) {
 
 	server.Mux.HandleFunc("/v2/apps/bree/settings/", func(w http.ResponseWriter, r *http.Request) {
 		testutil.SetHeaders(w)
-		testutil.AssertBody(t, api.AppSettings{Label: map[string]interface{}{
+		testutil.AssertBody(t, api.AppSettings{Label: map[string]any{
 			"team":     nil,
 			"git_repo": nil,
 		}}, r)
diff --git a/internal/commands/lifecycles.go b/internal/commands/lifecycles.go
new file mode 100644
index 000000000..1d3bc2d09
--- /dev/null
+++ b/internal/commands/lifecycles.go
@@ -0,0 +1,142 @@
+package commands
+
+import (
+	"fmt"
+
+	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/controller-sdk-go/config"
+	"github.com/drycc/workflow-cli/internal/loader"
+)
+
+func getLifecycleHandlerString(ptype, handler, signal string, lifecycleHandler *api.LifecycleHandler) string {
+	if lifecycleHandler.Exec != nil {
+		return fmt.Sprintf("%s %s exec %v %s", handler, ptype, lifecycleHandler.Exec.Command, signal)
+	} else if lifecycleHandler.Sleep != nil {
+		return fmt.Sprintf("%s %s sleep %v %s", handler, ptype, lifecycleHandler.Sleep, signal)
+	} else if lifecycleHandler.TCPSocket != nil {
+		return fmt.Sprintf("%s %s tcp-socket port=%v %s", handler, ptype, lifecycleHandler.TCPSocket.Port, signal)
+	} else if lifecycleHandler.HTTPGet != nil {
+		return fmt.Sprintf(
+			"%s %s http-get headers=%v path=%s port=%d %s",
+			handler,
+			ptype,
+			lifecycleHandler.HTTPGet.HTTPHeaders,
+			lifecycleHandler.HTTPGet.Path,
+			lifecycleHandler.HTTPGet.Port,
+			signal,
+		)
+	}
+	return ""
+}
+
+// LifecyclesList lists an app's lifecycles.
+func (d *DryccCmd) LifecyclesList(appID, ptype string, version int) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	config, err := config.List(s.Client, appID, version)
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+	if len(config.Lifecycle) == 0 {
+		d.Println(fmt.Sprintf("No lifecycle found in %s app.", appID))
+		return nil
+	}
+
+	ptypes := []string{}
+	if ptype != "" {
+		ptypes = append(ptypes, ptype)
+	} else {
+		for ptype := range config.Lifecycle {
+			ptypes = append(ptypes, ptype)
+		}
+	}
+
+	table := d.getDefaultFormatTable([]string{})
+	table.Append([]string{"App:", config.App})
+	table.Append([]string{"UUID:", config.UUID})
+	table.Append([]string{"Created:", d.formatTime(config.Created)})
+	table.Append([]string{"Updated:", d.formatTime(config.Updated)})
+	table.Append([]string{"Lifecycle:"})
+	for _, ptype := range sortPtypes(ptypes) {
+		if lifecycle, ok := config.Lifecycle[ptype]; ok {
+			if lifecycle.StopSignal != "" {
+				table.Append([]string{"", fmt.Sprintf("stopSignal=%s", lifecycle.StopSignal)})
+			}
+			if lifecycle.PostStart != nil {
+				table.Append([]string{"", getLifecycleHandlerString(ptype, "postStart", lifecycle.StopSignal, *lifecycle.PostStart)})
+			}
+			if lifecycle.PreStop != nil {
+				table.Append([]string{"", getLifecycleHandlerString(ptype, "preStop", lifecycle.StopSignal, *lifecycle.PreStop)})
+			}
+		}
+	}
+	table.Render()
+
+	return nil
+}
+
+// LifecyclesSet sets an app's lifecycle.
+func (d *DryccCmd) LifecyclesSet(appID, ptype string, lifecycle *api.Lifecycle) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	d.Print("Applying lifecycle... ")
+
+	quit := progress(d.WOut)
+	configObj := api.Config{Lifecycle: make(map[string]*api.Lifecycle)}
+	configObj.Lifecycle[ptype] = lifecycle
+
+	_, err = config.Set(s.Client, appID, configObj, true)
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	d.Print("done\n\n")
+
+	return d.LifecyclesList(appID, ptype, -1)
+}
+
+// LifecyclesUnset removes an app's lifecycle.
+func (d *DryccCmd) LifecyclesUnset(appID, ptype string, handlers []string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	d.Print("Applying lifecycle... ")
+
+	quit := progress(d.WOut)
+
+	configObj := api.Config{Lifecycle: make(map[string]*api.Lifecycle)}
+	lifecycle := &api.Lifecycle{}
+	var nullLifecycleHandler *api.LifecycleHandler
+	for _, handler := range handlers {
+		switch handler {
+		case "postStart":
+			lifecycle.PostStart = &nullLifecycleHandler
+		case "preStop":
+			lifecycle.PreStop = &nullLifecycleHandler
+		default:
+			return fmt.Errorf("unknown lifecycle handler: %s", handler)
+		}
+	}
+	configObj.Lifecycle[ptype] = lifecycle
+
+	_, err = config.Set(s.Client, appID, configObj, true)
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	d.Print("done\n\n")
+
+	return d.LifecyclesList(appID, ptype, -1)
+}
diff --git a/internal/commands/lifecycles_test.go b/internal/commands/lifecycles_test.go
new file mode 100644
index 000000000..dfb66400e
--- /dev/null
+++ b/internal/commands/lifecycles_test.go
@@ -0,0 +1,351 @@
+package commands
+
+import (
+	"bytes"
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/workflow-cli/pkg/testutil"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestLifecyclesList(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	server.Mux.HandleFunc("/v2/apps/foo/config/", func(w http.ResponseWriter, _ *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `{
+  "uuid": "c039a380-6068-4511-b35a-535a73b86ef5",
+  "app": "foo",
+  "owner": "bar",
+  "values": [],
+  "memory": {},
+  "cpu": {},
+  "tags": {},
+  "registry": {},
+  "lifecycle": {
+    "web": {
+      "stopSignal": "SIGTERM",
+      "postStart": {
+        "httpGet": {
+          "port": 80,
+          "path": "/health",
+          "httpHeaders": []
+        }
+      },
+      "preStop": {
+        "exec": {
+          "command": ["/bin/sh", "-c", "echo stopping"]
+        }
+      }
+    }
+  },
+  "created": "2016-09-12T22:20:14Z",
+  "updated": "2016-09-12T22:20:14Z"
+}`)
+	})
+
+	err = cmdr.LifecyclesList("foo", "web", -1)
+	assert.NoError(t, err)
+
+	// 获取实际输出并打印，以便我们可以查看正确的格式
+	actual := b.String()
+	t.Logf("Actual output:\n%s", actual)
+
+	// 使用assert.Contains来检查关键部分，而不是完全匹配
+	assert.Contains(t, actual, "App:          foo")
+	assert.Contains(t, actual, "UUID:         c039a380-6068-4511-b35a-535a73b86ef5")
+	assert.Contains(t, actual, "Created:      2016-09-12T22:20:14Z")
+	assert.Contains(t, actual, "Updated:      2016-09-12T22:20:14Z")
+	assert.Contains(t, actual, "stopSignal=SIGTERM")
+	assert.Contains(t, actual, "postStart web http-get headers=[] path=/health port=80 SIGTERM")
+	assert.Contains(t, actual, "preStop web exec [/bin/sh -c echo stopping] SIGTERM")
+}
+
+func TestLifecyclesListNoLifecycle(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	server.Mux.HandleFunc("/v2/apps/foo/config/", func(w http.ResponseWriter, _ *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `{
+  "uuid": "c039a380-6068-4511-b35a-535a73b86ef5",
+  "app": "foo",
+  "owner": "bar",
+  "values": [],
+  "memory": {},
+  "cpu": {},
+  "tags": {},
+  "registry": {},
+  "lifecycle": {},
+  "created": "2016-09-12T22:20:14Z",
+  "updated": "2016-09-12T22:20:14Z"
+}`)
+	})
+
+	err = cmdr.LifecyclesList("foo", "", -1)
+	assert.NoError(t, err)
+	assert.Equal(t, "No lifecycle found in foo app.\n", b.String())
+}
+
+func TestLifecyclesListAllLifecycles(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	server.Mux.HandleFunc("/v2/apps/foo/config/", func(w http.ResponseWriter, _ *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `{
+  "uuid": "c039a380-6068-4511-b35a-535a73b86ef5",
+  "app": "foo",
+  "owner": "bar",
+  "values": [],
+  "memory": {},
+  "cpu": {},
+  "tags": {},
+  "registry": {},
+  "lifecycle": {
+    "web": {
+      "stopSignal": "SIGTERM",
+      "postStart": {
+        "httpGet": {
+          "port": 80,
+          "path": "/health",
+          "httpHeaders": []
+        }
+      },
+      "preStop": {
+        "exec": {
+          "command": ["/bin/sh", "-c", "echo stopping"]
+        }
+      }
+    },
+    "worker": {
+      "stopSignal": "SIGKILL",
+      "postStart": {
+        "tcpSocket": {
+          "port": 8080
+        }
+      },
+      "preStop": {
+        "sleep": {
+          "seconds": 30
+        }
+      }
+    }
+  },
+  "created": "2016-09-12T22:20:14Z",
+  "updated": "2016-09-12T22:20:14Z"
+}`)
+	})
+
+	err = cmdr.LifecyclesList("foo", "", -1)
+	assert.NoError(t, err)
+
+	actual := b.String()
+	t.Logf("Actual output:\n%s", actual)
+
+	// 检查关键部分
+	assert.Contains(t, actual, "App:          foo")
+	assert.Contains(t, actual, "UUID:         c039a380-6068-4511-b35a-535a73b86ef5")
+	assert.Contains(t, actual, "Created:      2016-09-12T22:20:14Z")
+	assert.Contains(t, actual, "Updated:      2016-09-12T22:20:14Z")
+	assert.Contains(t, actual, "stopSignal=SIGTERM")
+	assert.Contains(t, actual, "postStart web http-get headers=[] path=/health port=80 SIGTERM")
+	assert.Contains(t, actual, "preStop web exec [/bin/sh -c echo stopping] SIGTERM")
+	assert.Contains(t, actual, "stopSignal=SIGKILL")
+	assert.Contains(t, actual, "postStart worker tcp-socket port=8080 SIGKILL")
+	assert.Contains(t, actual, "preStop worker sleep Seconds=30 SIGKILL")
+}
+
+func TestLifecyclesSet(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	server.Mux.HandleFunc("/v2/apps/foo/config/", func(w http.ResponseWriter, _ *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `{
+  "uuid": "c039a380-6068-4511-b35a-535a73b86ef5",
+  "app": "foo",
+  "owner": "bar",
+  "values": [],
+  "memory": {},
+  "cpu": {},
+  "tags": {},
+  "registry": {},
+  "lifecycle": {
+    "web": {
+      "stopSignal": "SIGTERM",
+      "postStart": {
+        "httpGet": {
+          "port": 80,
+          "path": "/health",
+          "httpHeaders": []
+        }
+      },
+      "preStop": {
+        "exec": {
+          "command": ["/bin/sh", "-c", "echo stopping"]
+        }
+      }
+    }
+  },
+  "created": "2016-09-12T22:20:14Z",
+  "updated": "2016-09-12T22:20:14Z"
+}`)
+	})
+
+	postStartHandler := &api.LifecycleHandler{
+		HTTPGet: &api.HTTPGetAction{
+			Path: "/health",
+			Port: 80,
+		},
+	}
+	preStopHandler := &api.LifecycleHandler{
+		Exec: &api.ExecAction{
+			Command: []string{"/bin/sh", "-c", "echo stopping"},
+		},
+	}
+	lifecycle := &api.Lifecycle{
+		StopSignal: "SIGTERM",
+		PostStart:  &postStartHandler,
+		PreStop:    &preStopHandler,
+	}
+
+	err = cmdr.LifecyclesSet("foo", "web", lifecycle)
+	assert.NoError(t, err)
+
+	actual := testutil.StripProgress(b.String())
+	t.Logf("Actual output:\n%s", actual)
+
+	assert.Contains(t, actual, "Applying lifecycle... done")
+	assert.Contains(t, actual, "App:          foo")
+	assert.Contains(t, actual, "UUID:         c039a380-6068-4511-b35a-535a73b86ef5")
+	assert.Contains(t, actual, "Created:      2016-09-12T22:20:14Z")
+	assert.Contains(t, actual, "Updated:      2016-09-12T22:20:14Z")
+	assert.Contains(t, actual, "stopSignal=SIGTERM")
+	assert.Contains(t, actual, "postStart web http-get headers=[] path=/health port=80 SIGTERM")
+	assert.Contains(t, actual, "preStop web exec [/bin/sh -c echo stopping] SIGTERM")
+}
+
+func TestLifecyclesUnset(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	server.Mux.HandleFunc("/v2/apps/foo/config/", func(w http.ResponseWriter, _ *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `{
+  "uuid": "c039a380-6068-4511-b35a-535a73b86ef5",
+  "app": "foo",
+  "owner": "bar",
+  "values": [],
+  "memory": {},
+  "cpu": {},
+  "tags": {},
+  "registry": {},
+  "lifecycle": {},
+  "created": "2016-09-12T22:20:14Z",
+  "updated": "2016-09-12T22:20:14Z"
+}`)
+	})
+
+	err = cmdr.LifecyclesUnset("foo", "web", []string{"postStart"})
+	assert.NoError(t, err)
+	assert.Equal(t, "Applying lifecycle... done\n\nNo lifecycle found in foo app.\n", testutil.StripProgress(b.String()))
+}
+
+func TestLifecyclesUnsetMultipleHandlers(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	server.Mux.HandleFunc("/v2/apps/foo/config/", func(w http.ResponseWriter, _ *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `{
+  "uuid": "c039a380-6068-4511-b35a-535a73b86ef5",
+  "app": "foo",
+  "owner": "bar",
+  "values": [],
+  "memory": {},
+  "cpu": {},
+  "tags": {},
+  "registry": {},
+  "lifecycle": {},
+  "created": "2016-09-12T22:20:14Z",
+  "updated": "2016-09-12T22:20:14Z"
+}`)
+	})
+
+	err = cmdr.LifecyclesUnset("foo", "web", []string{"postStart", "preStop"})
+	assert.NoError(t, err)
+	assert.Equal(t, "Applying lifecycle... done\n\nNo lifecycle found in foo app.\n", testutil.StripProgress(b.String()))
+}
+
+func TestLifecyclesUnsetInvalidHandler(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	server.Mux.HandleFunc("/v2/apps/foo/config/", func(w http.ResponseWriter, _ *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `{
+  "uuid": "c039a380-6068-4511-b35a-535a73b86ef5",
+  "app": "foo",
+  "owner": "bar",
+  "values": [],
+  "memory": {},
+  "cpu": {},
+  "tags": {},
+  "registry": {},
+  "lifecycle": {},
+  "created": "2016-09-12T22:20:14Z",
+  "updated": "2016-09-12T22:20:14Z"
+}`)
+	})
+
+	err = cmdr.LifecyclesUnset("foo", "web", []string{"invalidHandler"})
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "unknown lifecycle handler: invalidHandler")
+}
diff --git a/cmd/limits.go b/internal/commands/limits.go
similarity index 78%
rename from cmd/limits.go
rename to internal/commands/limits.go
index 7d62d8ba2..29b32f788 100644
--- a/cmd/limits.go
+++ b/internal/commands/limits.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"fmt"
@@ -7,18 +7,18 @@ import (
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/config"
 	"github.com/drycc/controller-sdk-go/limits"
-	"github.com/drycc/workflow-cli/settings"
+	"github.com/drycc/workflow-cli/internal/loader"
+	"github.com/drycc/workflow-cli/pkg/settings"
 )
 
 // LimitsList lists an app's limits.
-func (d *DryccCmd) LimitsList(appID string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+func (d *DryccCmd) LimitsList(appID string, version int) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
 
-	config, err := config.List(s.Client, appID)
+	config, err := config.List(s.Client, appID, version)
 	if d.checkAPICompatibility(s.Client, err) != nil {
 		return err
 	}
@@ -37,8 +37,8 @@ func (d *DryccCmd) LimitsList(appID string) error {
 			}
 			limitPlan := cached[limitPlanID]
 			gpuCount := limitPlan.Features["gpu"]
-			gpuName := limitPlan.Spec.Features["gpu"].(map[string]interface{})["name"]
-			gpuMemory := limitPlan.Spec.Features["gpu"].(map[string]interface{})["memory"].(map[string]interface{})["size"]
+			gpuName := limitPlan.Spec.Features["gpu"].(map[string]any)["name"]
+			gpuMemory := limitPlan.Spec.Features["gpu"].(map[string]any)["memory"].(map[string]any)["size"]
 			table.Append([]string{
 				ptype,
 				limitPlanID,
@@ -57,8 +57,7 @@ func (d *DryccCmd) LimitsList(appID string) error {
 
 // LimitsSet sets an app's limits.
 func (d *DryccCmd) LimitsSet(appID string, limits []string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -76,7 +75,7 @@ func (d *DryccCmd) LimitsSet(appID string, limits []string) error {
 
 	quit := progress(d.WOut)
 
-	_, err = config.Set(s.Client, appID, configObj)
+	_, err = config.Set(s.Client, appID, configObj, true)
 	quit <- true
 	<-quit
 	if d.checkAPICompatibility(s.Client, err) != nil {
@@ -85,13 +84,12 @@ func (d *DryccCmd) LimitsSet(appID string, limits []string) error {
 
 	d.Print("done\n\n")
 
-	return d.LimitsList(appID)
+	return d.LimitsList(appID, -1)
 }
 
 // LimitsUnset removes an app's limits.
 func (d *DryccCmd) LimitsUnset(appID string, limits []string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -102,14 +100,14 @@ func (d *DryccCmd) LimitsUnset(appID string, limits []string) error {
 
 	configObj := api.Config{}
 	if len(limits) > 0 {
-		limitsMap := make(map[string]interface{})
+		limitsMap := make(map[string]any)
 		for _, limit := range limits {
 			limitsMap[limit] = nil
 		}
 		configObj.Limits = limitsMap
 	}
 
-	_, err = config.Set(s.Client, appID, configObj)
+	_, err = config.Set(s.Client, appID, configObj, true)
 	quit <- true
 	<-quit
 	if d.checkAPICompatibility(s.Client, err) != nil {
@@ -118,13 +116,12 @@ func (d *DryccCmd) LimitsUnset(appID string, limits []string) error {
 
 	d.Print("done\n\n")
 
-	return d.LimitsList(appID)
+	return d.LimitsList(appID, -1)
 }
 
 // LimitsSpecs list limit spec
 func (d *DryccCmd) LimitsSpecs(keywords string, results int) error {
 	s, err := settings.Load(d.ConfigFile)
-
 	if err != nil {
 		return err
 	}
@@ -141,8 +138,8 @@ func (d *DryccCmd) LimitsSpecs(keywords string, results int) error {
 	} else {
 		table := d.getDefaultFormatTable([]string{"ID", "CPU", "CLOCK", "BOOST", "CORES", "THREADS", "NETWORK", "FEATURES"})
 		for _, limitSpec := range limitSpecs {
-			gpuName := limitSpec.Features["gpu"].(map[string]interface{})["name"]
-			gpuMemory := limitSpec.Features["gpu"].(map[string]interface{})["memory"].(map[string]interface{})["size"]
+			gpuName := limitSpec.Features["gpu"].(map[string]any)["name"]
+			gpuMemory := limitSpec.Features["gpu"].(map[string]any)["memory"].(map[string]any)["size"]
 			table.Append([]string{
 				limitSpec.ID,
 				fmt.Sprintf("%v", limitSpec.CPU["name"]),
@@ -162,7 +159,6 @@ func (d *DryccCmd) LimitsSpecs(keywords string, results int) error {
 // LimitsPlans list limit plan
 func (d *DryccCmd) LimitsPlans(specID string, cpu, memory, results int) error {
 	s, err := settings.Load(d.ConfigFile)
-
 	if err != nil {
 		return err
 	}
@@ -179,8 +175,8 @@ func (d *DryccCmd) LimitsPlans(specID string, cpu, memory, results int) error {
 	} else {
 		table := d.getDefaultFormatTable([]string{"ID", "SPEC", "CPU", "VCPUS", "MEMORY", "FEATURES"})
 		for _, limitPlan := range limitPlans {
-			gpuName := limitPlan.Spec.Features["gpu"].(map[string]interface{})["name"]
-			gpuMemory := limitPlan.Spec.Features["gpu"].(map[string]interface{})["memory"].(map[string]interface{})["size"]
+			gpuName := limitPlan.Spec.Features["gpu"].(map[string]any)["name"]
+			gpuMemory := limitPlan.Spec.Features["gpu"].(map[string]any)["memory"].(map[string]any)["size"]
 			table.Append([]string{
 				limitPlan.ID,
 				limitPlan.Spec.ID,
@@ -195,12 +191,11 @@ func (d *DryccCmd) LimitsPlans(specID string, cpu, memory, results int) error {
 	return nil
 }
 
-func parseLimits(limits []string) (map[string]interface{}, error) {
-	limitsMap := make(map[string]interface{})
+func parseLimits(limits []string) (map[string]any, error) {
+	limitsMap := make(map[string]any)
 
 	for _, limit := range limits {
 		key, value, err := parseLimit(limit)
-
 		if err != nil {
 			return nil, err
 		}
diff --git a/cmd/limits_test.go b/internal/commands/limits_test.go
similarity index 84%
rename from cmd/limits_test.go
rename to internal/commands/limits_test.go
index 9ea1c5660..e98e17ae2 100644
--- a/cmd/limits_test.go
+++ b/internal/commands/limits_test.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"bytes"
@@ -57,6 +57,7 @@ const getPlanFixture string = `
 	"disabled": false
 }
 `
+
 const specsFixture string = `
 {
 	"results": [{
@@ -217,7 +218,7 @@ func newTestServer(t *testing.T) (string, *testutil.TestServer) {
 func TestParseLimit(t *testing.T) {
 	t.Parallel()
 
-	var errorHint = ` doesn't fit format type=#unit or type=#
+	errorHint := ` doesn't fit format type=#unit or type=#
 Examples: web=std1.large.c1m1`
 
 	cases := []parseLimitCase{
@@ -245,7 +246,7 @@ Examples: web=std1.large.c1m1`
 
 type parseLimitsCase struct {
 	Input         []string
-	ExpectedMap   map[string]interface{}
+	ExpectedMap   map[string]any
 	ExpectedError bool
 	ExpectedMsg   string
 }
@@ -254,7 +255,7 @@ func TestLimitTags(t *testing.T) {
 	t.Parallel()
 
 	cases := []parseLimitsCase{
-		{[]string{"web=std1.large.c1m1", "worker=std1.large.c1m2"}, map[string]interface{}{"web": "std1.large.c1m1", "worker": "std1.large.c1m2"}, false, ""},
+		{[]string{"web=std1.large.c1m1", "worker=std1.large.c1m2"}, map[string]any{"web": "std1.large.c1m1", "worker": "std1.large.c1m2"}, false, ""},
 		{[]string{"foo=", "web=std1.large.c1m1"}, nil, true, `foo= doesn't fit format type=#unit or type=#
 Examples: web=std1.large.c1m1`},
 	}
@@ -280,7 +281,7 @@ func TestLimitsList(t *testing.T) {
 		fmt.Fprintf(w, `{
 			"owner": "jkirk",
 			"app": "enterprise",
-			"values": {},
+			"values": [],
 			"limits": {
 				"web": "std1.large.c1m1",
 				"worker": "std1.large.c1m1",
@@ -297,13 +298,13 @@ func TestLimitsList(t *testing.T) {
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
-	err := cmdr.LimitsList("enterprise")
+	err := cmdr.LimitsList("enterprise", -1)
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `PTYPE     PLAN               VCPUS    MEMORY    FEATURES                          
-db        std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1    
-web       std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1    
-worker    std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1    
-`, "output")
+	testutil.AssertOutput(t, b.String(), `PTYPE     PLAN               VCPUS    MEMORY    FEATURES
+db        std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1
+web       std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1
+worker    std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1
+`)
 
 	server.Mux.HandleFunc("/v2/apps/franklin/config/", func(w http.ResponseWriter, _ *http.Request) {
 		testutil.SetHeaders(w)
@@ -321,7 +322,7 @@ worker    std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared *
 	})
 	b.Reset()
 
-	err = cmdr.LimitsList("franklin")
+	err = cmdr.LimitsList("franklin", -1)
 	assert.NoError(t, err)
 	assert.Equal(t, b.String(), `No limits found in franklin app.
 `, "output")
@@ -336,7 +337,7 @@ func TestLimitsSet(t *testing.T) {
 		testutil.SetHeaders(w)
 		if r.Method == "POST" {
 			testutil.AssertBody(t, api.Config{
-				Limits: map[string]interface{}{
+				Limits: map[string]any{
 					"web": "std1.large.c1m1",
 				},
 			}, r)
@@ -345,7 +346,7 @@ func TestLimitsSet(t *testing.T) {
 		fmt.Fprintf(w, `{
 			"owner": "jkirk",
 			"app": "foo",
-			"values": {},
+			"values": [],
 			"limits": {"web": "std1.large.c1m1"},
 			"tags": {},
 			"registry": {},
@@ -361,17 +362,17 @@ func TestLimitsSet(t *testing.T) {
 	err := cmdr.LimitsSet("foo", []string{"web=std1.large.c1m1"})
 	assert.NoError(t, err)
 
-	assert.Equal(t, testutil.StripProgress(b.String()), `Applying limits... done
+	testutil.AssertOutput(t, testutil.StripProgress(b.String()), `Applying limits... done
 
-PTYPE    PLAN               VCPUS    MEMORY    FEATURES                          
-web      std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1    
-`, "output")
+PTYPE    PLAN               VCPUS    MEMORY    FEATURES
+web      std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1
+`)
 
 	server.Mux.HandleFunc("/v2/apps/franklin/config/", func(w http.ResponseWriter, r *http.Request) {
 		testutil.SetHeaders(w)
 		if r.Method == "POST" {
 			testutil.AssertBody(t, api.Config{
-				Limits: map[string]interface{}{
+				Limits: map[string]any{
 					"web": "std1.large.c1m1",
 				},
 			}, r)
@@ -380,7 +381,7 @@ web      std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1
 		fmt.Fprintf(w, `{
 			"owner": "bedison",
 			"app": "franklin",
-			"values": {},
+			"values": [],
 			"limits": {
 				"web": "std1.large.c1m1"
 			},
@@ -396,18 +397,18 @@ web      std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1
 	err = cmdr.LimitsSet("franklin", []string{"web=std1.large.c1m1"})
 	assert.NoError(t, err)
 
-	assert.Equal(t, testutil.StripProgress(b.String()), `Applying limits... done
+	testutil.AssertOutput(t, testutil.StripProgress(b.String()), `Applying limits... done
 
-PTYPE    PLAN               VCPUS    MEMORY    FEATURES                          
-web      std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1    
-`, "output")
+PTYPE    PLAN               VCPUS    MEMORY    FEATURES
+web      std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1
+`)
 
 	// with requests/limit parameter
 	server.Mux.HandleFunc("/v2/apps/jim/config/", func(w http.ResponseWriter, r *http.Request) {
 		testutil.SetHeaders(w)
 		if r.Method == "POST" {
 			testutil.AssertBody(t, api.Config{
-				Limits: map[string]interface{}{
+				Limits: map[string]any{
 					"web":    "std1.large.c1m1",
 					"worker": "std1.large.c1m1",
 					"db":     "std1.large.c1m1",
@@ -418,7 +419,7 @@ web      std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1
 		fmt.Fprintf(w, `{
 			"owner": "foo",
 			"app": "jim",
-			"values": {},
+			"values": [],
 			"limits": {
 				"web": "std1.large.c1m1",
 				"worker": "std1.large.c1m1",
@@ -436,20 +437,20 @@ web      std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1
 	err = cmdr.LimitsSet("jim", []string{"web=std1.large.c1m1", "worker=std1.large.c1m1", "db=std1.large.c1m1"})
 	assert.NoError(t, err)
 
-	assert.Equal(t, testutil.StripProgress(b.String()), `Applying limits... done
+	testutil.AssertOutput(t, testutil.StripProgress(b.String()), `Applying limits... done
 
-PTYPE     PLAN               VCPUS    MEMORY    FEATURES                          
-db        std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1    
-web       std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1    
-worker    std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1    
-`, "output")
+PTYPE     PLAN               VCPUS    MEMORY    FEATURES
+db        std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1
+web       std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1
+worker    std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1
+`)
 
 	// with requests/limit parameter
 	server.Mux.HandleFunc("/v2/apps/phew/config/", func(w http.ResponseWriter, r *http.Request) {
 		testutil.SetHeaders(w)
 		if r.Method == "POST" {
 			testutil.AssertBody(t, api.Config{
-				Limits: map[string]interface{}{
+				Limits: map[string]any{
 					"web":    "std1.large.c1m1",
 					"worker": "std1.large.c1m1",
 					"db":     "std1.large.c1m1",
@@ -460,7 +461,7 @@ worker    std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared *
 		fmt.Fprintf(w, `{
 			"owner": "foo",
 			"app": "jim",
-			"values": {},
+			"values": [],
 			"limits": {
 				"web": "std1.large.c1m1",
 				"worker": "std1.large.c1m1",
@@ -478,13 +479,13 @@ worker    std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared *
 	err = cmdr.LimitsSet("phew", []string{"web=std1.large.c1m1", "worker=std1.large.c1m1", "db=std1.large.c1m1"})
 	assert.NoError(t, err)
 
-	assert.Equal(t, testutil.StripProgress(b.String()), `Applying limits... done
+	testutil.AssertOutput(t, testutil.StripProgress(b.String()), `Applying limits... done
 
-PTYPE     PLAN               VCPUS    MEMORY    FEATURES                          
-db        std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1    
-web       std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1    
-worker    std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1    
-`, "output")
+PTYPE     PLAN               VCPUS    MEMORY    FEATURES
+db        std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1
+web       std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1
+worker    std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1
+`)
 }
 
 func TestLimitsUnset(t *testing.T) {
@@ -496,7 +497,7 @@ func TestLimitsUnset(t *testing.T) {
 		testutil.SetHeaders(w)
 		if r.Method == "POST" {
 			testutil.AssertBody(t, api.Config{
-				Limits: map[string]interface{}{
+				Limits: map[string]any{
 					"web": nil,
 				},
 			}, r)
@@ -505,7 +506,7 @@ func TestLimitsUnset(t *testing.T) {
 		fmt.Fprintf(w, `{
 			"owner": "jkirk",
 			"app": "foo",
-			"values": {},
+			"values": [],
 			"limits": {
 				"web": "std1.large.c1m1"
 			},
@@ -523,17 +524,17 @@ func TestLimitsUnset(t *testing.T) {
 	err := cmdr.LimitsUnset("foo", []string{"web"})
 	assert.NoError(t, err)
 
-	assert.Equal(t, testutil.StripProgress(b.String()), `Applying limits... done
+	testutil.AssertOutput(t, testutil.StripProgress(b.String()), `Applying limits... done
 
-PTYPE    PLAN               VCPUS    MEMORY    FEATURES                          
-web      std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1    
-`, "output")
+PTYPE    PLAN               VCPUS    MEMORY    FEATURES
+web      std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1
+`)
 
 	server.Mux.HandleFunc("/v2/apps/franklin/config/", func(w http.ResponseWriter, r *http.Request) {
 		testutil.SetHeaders(w)
 		if r.Method == "POST" {
 			testutil.AssertBody(t, api.Config{
-				Limits: map[string]interface{}{
+				Limits: map[string]any{
 					"web": nil,
 				},
 			}, r)
@@ -542,7 +543,7 @@ web      std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1
 		fmt.Fprintf(w, `{
 			"owner": "bedison",
 			"app": "franklin",
-			"values": {},
+			"values": [],
 			"limits": {},
 			"tags": {},
 			"registry": {},
@@ -572,9 +573,9 @@ func TestLimitsSpecs(t *testing.T) {
 
 	err := cmdr.LimitsSpecs("", 10)
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `ID      CPU            CLOCK      BOOST      CORES    THREADS    NETWORK    FEATURES                      
-std1    Unknown CPU    3100MHZ    3700MHZ    32       64         10G        Unknown Integrated GPU shared    
-`, "output")
+	testutil.AssertOutput(t, b.String(), `ID      CPU            CLOCK      BOOST      CORES    THREADS    NETWORK    FEATURES
+std1    Unknown CPU    3100MHZ    3700MHZ    32       64         10G        Unknown Integrated GPU shared
+`)
 }
 
 func TestLimitsPlans(t *testing.T) {
@@ -587,8 +588,8 @@ func TestLimitsPlans(t *testing.T) {
 
 	err := cmdr.LimitsPlans("", 0, 0, 100)
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `ID                 SPEC    CPU            VCPUS    MEMORY    FEATURES                      
-std1.large.c1m1    std1    Unknown CPU    1        1 GiB     Unknown Integrated GPU shared    
-std1.large.c1m2    std1    Unknown CPU    1        2 GiB     Unknown Integrated GPU shared    
-`, "output")
+	testutil.AssertOutput(t, b.String(), `ID                 SPEC    CPU            VCPUS    MEMORY    FEATURES
+std1.large.c1m1    std1    Unknown CPU    1        1 GiB     Unknown Integrated GPU shared
+std1.large.c1m2    std1    Unknown CPU    1        2 GiB     Unknown Integrated GPU shared
+`)
 }
diff --git a/cmd/ps.go b/internal/commands/ps.go
similarity index 58%
rename from cmd/ps.go
rename to internal/commands/ps.go
index 24619f5dc..c2a77b8b7 100644
--- a/cmd/ps.go
+++ b/internal/commands/ps.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"context"
@@ -13,7 +13,9 @@ import (
 
 	"github.com/containerd/console"
 	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/controller-sdk-go/events"
 	"github.com/drycc/controller-sdk-go/ps"
+	"github.com/drycc/workflow-cli/internal/loader"
 	"github.com/drycc/workflow-cli/pkg/logging"
 	"golang.org/x/net/websocket"
 	yaml "gopkg.in/yaml.v3"
@@ -29,7 +31,7 @@ const (
 
 // PsList lists an app's processes.
 func (d *DryccCmd) PsList(appID string, results int) error {
-	s, appID, err := load(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -48,10 +50,9 @@ func (d *DryccCmd) PsList(appID string, results int) error {
 	return nil
 }
 
-// PodLogs returns the logs from an pod.
-func (d *DryccCmd) PsLogs(appID, podID string, lines int, follow bool, container string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+// PsLogs returns the logs from a pod.
+func (d *DryccCmd) PsLogs(appID, podID string, lines int, follow bool, container string, previous bool) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -59,6 +60,7 @@ func (d *DryccCmd) PsLogs(appID, podID string, lines int, follow bool, container
 		Lines:     lines,
 		Follow:    follow,
 		Container: container,
+		Previous:  previous,
 	}
 	conn, err := ps.Logs(s.Client, appID, podID, request)
 	if err != nil {
@@ -79,9 +81,9 @@ func (d *DryccCmd) PsLogs(appID, podID string, lines int, follow bool, container
 	return nil
 }
 
-// PsList lists an app's processes.
+// PsExec executes a command in a pod.
 func (d *DryccCmd) PsExec(appID, podID string, tty, stdin bool, command []string) error {
-	s, appID, err := load(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -103,59 +105,116 @@ func (d *DryccCmd) PsExec(appID, podID string, tty, stdin bool, command []string
 	return nil
 }
 
-// PsScale scales an app's processes.
-func (d *DryccCmd) PsScale(appID string, targets []string) error {
-	s, appID, err := load(d.ConfigFile, appID)
+// PsDescribe describe an app's processes.
+func (d *DryccCmd) PsDescribe(appID, podID string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
-
-	targetMap, err := parsePsTargets(targets)
-	if err != nil {
-		return err
-	}
-
-	d.Printf("Scaling processes... but first, %s!\n", drinkOfChoice())
-	startTime := time.Now()
-	quit := progress(d.WOut)
-
-	err = ps.Scale(s.Client, appID, targetMap)
-	quit <- true
-	<-quit
+	// The 1000 is fake for now until API understands limits
+	podState, _, err := ps.Describe(s.Client, appID, podID, 1000)
 	if d.checkAPICompatibility(s.Client, err) != nil {
 		return err
 	}
-
-	d.Printf("done in %ds\n\n", int(time.Since(startTime).Seconds()))
-
-	processes, _, err := ps.List(s.Client, appID, s.Limit)
+	table := d.getDefaultFormatTable([]string{})
+	for _, containerState := range podState {
+		table.Append([]string{"Container:", containerState.Container})
+		table.Append([]string{"Image:", containerState.Image})
+		table.Append([]string{"Command:"})
+		for _, command := range containerState.Command {
+			table.Append([]string{"", fmt.Sprintf("- %v", command)})
+		}
+		table.Append([]string{"Args:"})
+		for _, arg := range containerState.Args {
+			table.Append([]string{"", fmt.Sprintf("- %v", arg)})
+		}
+		// Status/Reason/Message
+		if containerState.Status != "" {
+			table.Append([]string{"Status:", containerState.Status})
+		}
+		if containerState.Reason != "" {
+			table.Append([]string{"Reason:", containerState.Reason})
+		}
+		if containerState.Message != "" {
+			table.Append([]string{"Message:", containerState.Message})
+		}
+		// State
+		for key := range containerState.State {
+			table.Append([]string{"State:", key})
+			value := containerState.State[key]
+			for innerKey := range value {
+				innerValue := strconv.Quote(fmt.Sprintf("%v", value[innerKey]))
+				if innerKey == "startedAt" || innerKey == "finishedAt" {
+					if value[innerKey] != nil {
+						innerValue = d.formatTime(fmt.Sprintf("%s", value[innerKey]))
+					} else {
+						innerValue = "<none>"
+					}
+				}
+				table.Append([]string{d.indentString(innerKey, 2), strings.Trim(innerValue, `"`)})
+			}
+		}
+		// LastState
+		for key := range containerState.LastState {
+			table.Append([]string{"Last State:", key})
+			value := containerState.LastState[key]
+			for innerKey := range value {
+				innerValue := strconv.Quote(fmt.Sprintf("%v", value[innerKey]))
+				if innerKey == "startedAt" || innerKey == "finishedAt" {
+					if value[innerKey] != nil {
+						innerValue = d.formatTime(fmt.Sprintf("%s", value[innerKey]))
+					} else {
+						innerValue = "<none>"
+					}
+				}
+				table.Append([]string{d.indentString(innerKey, 2), strings.Trim(innerValue, `"`)})
+			}
+		}
+		table.Append([]string{"Ready:", fmt.Sprintf("%v", containerState.Ready)})
+		table.Append([]string{"Restart Count:", fmt.Sprintf("%v", containerState.RestartCount)})
+		table.Append([]string{})
+	}
+	table.Render()
+	// display events
+	events, _, err := events.ListPodEvents(s.Client, appID, podID, 1000)
 	if err != nil {
 		return err
 	}
-
-	printProcesses(d, appID, processes)
+	if len(events) != 0 {
+		// table event
+		te := d.getDefaultFormatTable([]string{})
+		te.Append([]string{"Events:"})
+		te.Append([]string{d.indentString("REASON", 2), "MESSAGE", "CREATED"})
+		for _, ev := range events {
+			te.Append([]string{
+				d.indentString(ev.Reason, 2),
+				ev.Message,
+				d.formatTime(ev.Created),
+			})
+		}
+		te.Render()
+	}
 	return nil
 }
 
-// PsRestart restarts an app's processes.
-func (d *DryccCmd) PsRestart(appID, target string) error {
-	s, appID, err := load(d.ConfigFile, appID)
+// PsDelete delete an pod.
+func (d *DryccCmd) PsDelete(appID string, podIDs []string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
+	pods := strings.Join(podIDs, ",")
+	d.Printf("Deleting %s from %s... ", pods, appID)
 
-	d.Printf("Restarting processes... but first, %s!\n", drinkOfChoice())
-	startTime := time.Now()
 	quit := progress(d.WOut)
-
-	err = ps.Restart(s.Client, appID, target)
+	err = ps.Delete(s.Client, appID, pods)
 	quit <- true
 	<-quit
-	if err != nil {
+	if d.checkAPICompatibility(s.Client, err) != nil {
 		return err
 	}
 
-	d.Printf("done in %ds\n", int(time.Since(startTime).Seconds()))
+	d.Println("done")
 	return nil
 }
 
@@ -165,7 +224,7 @@ func printProcesses(d *DryccCmd, appID string, input []api.Pods) {
 	if len(processes) == 0 {
 		d.Println(fmt.Sprintf("No processes found in %s app.", appID))
 	} else {
-		table := d.getDefaultFormatTable([]string{"NAME", "RELEASE", "STATE", "PTYPE", "STARTED"})
+		table := d.getDefaultFormatTable([]string{"NAME", "RELEASE", "STATE", "PTYPE", "READY", "RESTARTS", "STARTED"})
 		for _, process := range processes {
 			for _, pod := range process.PodsList {
 				table.Append([]string{
@@ -173,7 +232,9 @@ func printProcesses(d *DryccCmd, appID string, input []api.Pods) {
 					pod.Release,
 					pod.State,
 					pod.Type,
-					pod.Started.Format("2006-01-02T15:04:05MST"),
+					pod.Ready,
+					fmt.Sprintf("%v", pod.Restarts),
+					d.formatTime(pod.Started),
 				})
 			}
 		}
@@ -286,12 +347,11 @@ func parsePsTargets(targets []string) (map[string]int, error) {
 		if regex.MatchString(target) {
 			captures := regex.FindStringSubmatch(target)
 			targetMap[captures[1]], err = strconv.Atoi(captures[2])
-
 			if err != nil {
 				return nil, err
 			}
 		} else {
-			return nil, fmt.Errorf("'%s' does not match the pattern 'type=num', ex: web=2", target)
+			return nil, fmt.Errorf("'%s' does not match the pattern 'ptype=num', ex: web=2", target)
 		}
 	}
 
@@ -301,7 +361,7 @@ func parsePsTargets(targets []string) (map[string]int, error) {
 func parseChannelMessage(data string) (string, error) {
 	channel, message := data[0], data[1:]
 	if string(channel) == errorChannel {
-		data := make(map[string]interface{})
+		data := make(map[string]any)
 		yaml.Unmarshal([]byte(message), data)
 		if value, hasKey := data["message"]; hasKey {
 			if message, ok := value.(string); ok {
diff --git a/cmd/ps_test.go b/internal/commands/ps_test.go
similarity index 61%
rename from cmd/ps_test.go
rename to internal/commands/ps_test.go
index 375628317..45315dff0 100644
--- a/cmd/ps_test.go
+++ b/internal/commands/ps_test.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"bytes"
@@ -6,10 +6,8 @@ import (
 	"io"
 	"net/http"
 	"testing"
-	"time"
 
 	"github.com/drycc/controller-sdk-go/api"
-	dtime "github.com/drycc/controller-sdk-go/pkg/time"
 	"github.com/drycc/workflow-cli/pkg/testutil"
 	"github.com/stretchr/testify/assert"
 	"golang.org/x/net/websocket"
@@ -17,24 +15,24 @@ import (
 
 func TestPrintProcesses(t *testing.T) {
 	var b bytes.Buffer
-	d, err := time.Parse("2006-01-02T15:04:05MST", "2023-11-15T11:55:16CST")
-	if err != nil {
-		t.Fatal(err)
-	}
 	pods := []api.Pods{
 		{
-			Release: "v3",
-			Name:    "benign-quilting-web-4084101150-c871y",
-			Type:    "web",
-			State:   "up",
-			Started: dtime.Time{Time: &d},
+			Release:  "v3",
+			Name:     "benign-quilting-web-4084101150-c871y",
+			Type:     "web",
+			State:    "up",
+			Ready:    "1/1",
+			Restarts: 0,
+			Started:  "2023-11-15T11:55:16CST",
 		},
 		{
-			Release: "v3",
-			Name:    "benign-quilting-worker-4084101150-c871y",
-			Type:    "worker",
-			State:   "up",
-			Started: dtime.Time{Time: &d},
+			Release:  "v3",
+			Name:     "benign-quilting-worker-4084101150-c871y",
+			Type:     "worker",
+			State:    "up",
+			Ready:    "1/1",
+			Restarts: 0,
+			Started:  "2023-11-15T11:55:16CST",
 		},
 	}
 	cf, server, err := testutil.NewTestServerAndClient()
@@ -45,9 +43,9 @@ func TestPrintProcesses(t *testing.T) {
 
 	printProcesses(&DryccCmd{WOut: &b, ConfigFile: cf}, "appname", pods)
 
-	assert.Equal(t, b.String(), `NAME                                       RELEASE    STATE    PTYPE     STARTED                
-benign-quilting-web-4084101150-c871y       v3         up       web       2023-11-15T11:55:16CST    
-benign-quilting-worker-4084101150-c871y    v3         up       worker    2023-11-15T11:55:16CST    
+	assert.Equal(t, b.String(), `NAME                                       RELEASE    STATE    PTYPE     READY    RESTARTS    STARTED                
+benign-quilting-web-4084101150-c871y       v3         up       web       1/1      0           2023-11-15T11:55:16CST    
+benign-quilting-worker-4084101150-c871y    v3         up       worker    1/1      0           2023-11-15T11:55:16CST    
 `, "output")
 }
 
@@ -73,6 +71,8 @@ func TestPsList(t *testing.T) {
 					"type": "web",
 					"name": "foo-web-4084101150-c871y",
 					"state": "up",
+					"ready": "1/1",
+					"restarts": 0,
 					"started": "2016-02-13T00:47:52"
 				}
 			]
@@ -82,8 +82,8 @@ func TestPsList(t *testing.T) {
 	err = cmdr.PsList("foo", -1)
 	assert.NoError(t, err)
 
-	assert.Equal(t, b.String(), `NAME                        RELEASE    STATE    PTYPE    STARTED                
-foo-web-4084101150-c871y    v2         up       web      2016-02-13T00:47:52UTC    
+	assert.Equal(t, b.String(), `NAME                        RELEASE    STATE    PTYPE    READY    RESTARTS    STARTED             
+foo-web-4084101150-c871y    v2         up       web      1/1      0           2016-02-13T00:47:52    
 `, "output")
 }
 
@@ -121,7 +121,7 @@ func TestPsLogs(t *testing.T) {
 			conn.WriteClose(100)
 		}),
 	)
-	err = cmdr.PsLogs("foo", "foo-web-111", 300, true, "runner")
+	err = cmdr.PsLogs("foo", "foo-web-111", 300, true, "runner", false)
 	assert.NoError(t, err)
 }
 
@@ -136,10 +136,10 @@ func TestParsePsTargets(t *testing.T) {
 	t.Parallel()
 
 	cases := []psTargetCases{
-		{[]string{"test"}, true, nil, "'test' does not match the pattern 'type=num', ex: web=2"},
-		{[]string{"test=a"}, true, nil, "'test=a' does not match the pattern 'type=num', ex: web=2"},
-		{[]string{"test="}, true, nil, "'test=' does not match the pattern 'type=num', ex: web=2"},
-		{[]string{"Test=2"}, true, nil, "'Test=2' does not match the pattern 'type=num', ex: web=2"},
+		{[]string{"test"}, true, nil, "'test' does not match the pattern 'ptype=num', ex: web=2"},
+		{[]string{"test=a"}, true, nil, "'test=a' does not match the pattern 'ptype=num', ex: web=2"},
+		{[]string{"test="}, true, nil, "'test=' does not match the pattern 'ptype=num', ex: web=2"},
+		{[]string{"Test=2"}, true, nil, "'Test=2' does not match the pattern 'ptype=num', ex: web=2"},
 		{[]string{"test=2"}, false, map[string]int{"test": 2}, ""},
 		{[]string{"test-proc=2"}, false, map[string]int{"test-proc": 2}, ""},
 		{[]string{"test1=2"}, false, map[string]int{"test1": 2}, ""},
@@ -156,7 +156,7 @@ func TestParsePsTargets(t *testing.T) {
 	}
 }
 
-func TestPsScale(t *testing.T) {
+func TestPsDescribe(t *testing.T) {
 	t.Parallel()
 	cf, server, err := testutil.NewTestServerAndClient()
 	if err != nil {
@@ -165,45 +165,59 @@ func TestPsScale(t *testing.T) {
 	defer server.Close()
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-	err = cmdr.PsScale("foo", []string{"test"})
-	assert.Equal(t, err.Error(), "'test' does not match the pattern 'type=num', ex: web=2", "error")
 
-	server.Mux.HandleFunc("/v2/apps/foo/pods/", func(w http.ResponseWriter, _ *http.Request) {
+	server.Mux.HandleFunc("/v2/apps/foo/pods/foo-web-111/describe/", func(w http.ResponseWriter, _ *http.Request) {
 		testutil.SetHeaders(w)
 		fmt.Fprintf(w, `{
 			"count": 1,
 			"next": null,
 			"previous": null,
-			"results": [
-				{
-					"release": "v2",
-					"type": "web",
-					"name": "foo-web-4084101150-c871y",
-					"state": "up",
-					"started": "2016-02-13T00:47:52"
-				}
-			]
+			"results": [{
+				"container": "web",
+				"image": "registry.drycc.cc/base/base",
+				"command": ["bash", "-c"],
+				"args": ["sleep 3600s"],
+				"state": {
+					"running": {
+					  "startedAt": "2024-05-21T02:27:03+00:00"
+					},
+					"waiting": {
+					  "message": "container create failed: executable file './start.sh' not found in $PATH: No such file or directory\n",
+					  "reason": "CreateContainerError"
+					}
+				},
+				"lastState": {
+					"terminated": {
+					  "containerID": "cri-o://ccfc73b0b4d966af4f93ca871a04fa97460620cd8005c1c36f7734a08ba49ed0",
+					  "exitCode": 1,
+					  "finishedAt": "2024-05-21T02:27:03+00:00",
+					  "reason": "Error",
+					  "startedAt": "2024-05-21T02:26:33+00:00"
+					}
+				},
+				"ready": true,
+				"restartCount": 1
+			}]
 		}`)
 	})
-
-	server.Mux.HandleFunc("/v2/apps/foo/scale/", func(w http.ResponseWriter, r *http.Request) {
-		testutil.AssertBody(t, map[string]int{"web": 1}, r)
+	server.Mux.HandleFunc("/v2/apps/foo/events/", func(w http.ResponseWriter, _ *http.Request) {
 		testutil.SetHeaders(w)
-		w.WriteHeader(http.StatusNoContent)
+		fmt.Fprintf(w, `{
+			"count": 1,
+			"next": null,
+			"previous": null,
+			"results": [{
+				"reason": "Scheduled",
+				"message": "Successfully assigned example-go/example-go-web-6b44dbd6c8-h89cg to node1",
+				"created": "2024-07-03T16:28:00"
+			}]
+		}`)
 	})
-
-	b.Reset()
-	err = cmdr.PsScale("foo", []string{"web=1"})
+	err = cmdr.PsDescribe("foo", "foo-web-111")
 	assert.NoError(t, err)
-	assert.Equal(t, testutil.StripProgress(b.String()), `Scaling processes... but first, coffee!
-done in 0s
-
-NAME                        RELEASE    STATE    PTYPE    STARTED                
-foo-web-4084101150-c871y    v2         up       web      2016-02-13T00:47:52UTC    
-`, "output")
 }
 
-func TestPsRestart(t *testing.T) {
+func TestPsDelete(t *testing.T) {
 	t.Parallel()
 	cf, server, err := testutil.NewTestServerAndClient()
 	if err != nil {
@@ -212,33 +226,10 @@ func TestPsRestart(t *testing.T) {
 	defer server.Close()
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	server.Mux.HandleFunc("/v2/apps/foo/pods/restart/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-		w.WriteHeader(http.StatusNoContent)
-	})
-
-	b.Reset()
-	err = cmdr.PsRestart("foo", "")
-	assert.NoError(t, err)
-
-	server.Mux.HandleFunc("/v2/apps/coolapp/pods/restart/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-		w.WriteHeader(http.StatusNoContent)
-	})
-
-	b.Reset()
-
-	err = cmdr.PsRestart("coolapp", "")
-	assert.NoError(t, err)
-
-	server.Mux.HandleFunc("/v2/apps/testapp/pods/web/restart/", func(w http.ResponseWriter, _ *http.Request) {
+	server.Mux.HandleFunc("/v2/apps/foo/pods/", func(w http.ResponseWriter, _ *http.Request) {
 		testutil.SetHeaders(w)
 		w.WriteHeader(http.StatusNoContent)
 	})
-
-	b.Reset()
-
-	err = cmdr.PsRestart("testapp", "web")
+	err = cmdr.PsDelete("foo", []string{"foo-web-111"})
 	assert.NoError(t, err)
 }
diff --git a/internal/commands/pts.go b/internal/commands/pts.go
new file mode 100644
index 000000000..eb21073bb
--- /dev/null
+++ b/internal/commands/pts.go
@@ -0,0 +1,252 @@
+package commands
+
+import (
+	"fmt"
+	"regexp"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/controller-sdk-go/events"
+	"github.com/drycc/controller-sdk-go/pts"
+	"github.com/drycc/workflow-cli/internal/loader"
+)
+
+// PtsList lists an app's processes.
+func (d *DryccCmd) PtsList(appID string, results int) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	if results == defaultLimit {
+		results = s.Limit
+	}
+
+	ptypes, _, err := pts.List(s.Client, appID, results)
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	printProcessTypes(d, appID, ptypes)
+
+	return nil
+}
+
+// PtsDescribe describe an app's processes.
+func (d *DryccCmd) PtsDescribe(appID, ptype string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	ptypeStates, _, err := pts.Describe(s.Client, appID, ptype, 1000)
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+	events, _, err := events.ListPtypeEvents(s.Client, appID, ptype, 1000)
+	if err != nil {
+		return err
+	}
+	printProcessTypeDetail(d, ptypeStates, events)
+	return nil
+}
+
+// PtsScale scales an app's processes.
+func (d *DryccCmd) PtsScale(appID string, targets []string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	targetMap, err := parsePtsTargets(targets)
+	if err != nil {
+		return err
+	}
+
+	d.Printf("Scaling process types... but first, %s!\n", drinkOfChoice())
+	startTime := time.Now()
+	quit := progress(d.WOut)
+
+	err = pts.Scale(s.Client, appID, targetMap)
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	d.Printf("done in %ds\n", int(time.Since(startTime).Seconds()))
+
+	return nil
+}
+
+// PtsRestart restarts an app's processes.
+func (d *DryccCmd) PtsRestart(appID string, targets []string, confirm string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+	if len(targets) == 0 && (confirm == "" || confirm != "yes") {
+		d.Printf(` !    WARNING: Potentially Restart Action
+ !    This command will restart all processes of the application ptype
+ !    To proceed, type "yes" !
+
+> `)
+
+		fmt.Scanln(&confirm)
+		if confirm != "yes" {
+			return fmt.Errorf("cancel the restart action")
+		}
+	}
+	d.Printf("Restarting process types... but first, %s!\n", drinkOfChoice())
+	startTime := time.Now()
+	quit := progress(d.WOut)
+	ptypes := strings.Join(targets, ",")
+	targetMap := map[string]string{
+		"ptypes": ptypes,
+	}
+	err = pts.Restart(s.Client, appID, targetMap)
+	quit <- true
+	<-quit
+	if err != nil {
+		return err
+	}
+
+	d.Printf("done in %ds\n", int(time.Since(startTime).Seconds()))
+	return nil
+}
+
+// PtsClean cleans process types that are not used.
+func (d *DryccCmd) PtsClean(appID string, targets []string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+	d.Printf("Cleaning process types... but first, %s!\n", drinkOfChoice())
+	startTime := time.Now()
+	quit := progress(d.WOut)
+	ptypes := strings.Join(targets, ",")
+	targetMap := map[string]string{
+		"ptypes": ptypes,
+	}
+	err = pts.Clean(s.Client, appID, targetMap)
+	quit <- true
+	<-quit
+	if err != nil {
+		return err
+	}
+
+	d.Printf("done in %ds\n", int(time.Since(startTime).Seconds()))
+	return nil
+}
+
+func printProcessTypes(d *DryccCmd, appID string, ptypes api.Ptypes) {
+	pts := pts.ByType(ptypes)
+
+	if len(pts) == 0 {
+		d.Println(fmt.Sprintf("No processes found in %s app.", appID))
+	} else {
+		table := d.getDefaultFormatTable([]string{"NAME", "RELEASE", "READY", "UP-TO-DATE", "AVAILABLE", "STARTED", "GARBAGE"})
+		for _, pt := range pts {
+			table.Append([]string{
+				pt.Name,
+				pt.Release,
+				pt.Ready,
+				fmt.Sprintf("%v", pt.UpToDate),
+				fmt.Sprintf("%v", pt.AvailableReplicas),
+				d.formatTime(pt.Started),
+				fmt.Sprintf("%t", pt.Garbage),
+			})
+		}
+		table.Render()
+	}
+}
+
+func printProcessTypeDetail(d *DryccCmd, ptypeStates api.PtypeStates, events api.AppEvents) {
+	// table process type
+	tpt := d.getDefaultFormatTable([]string{})
+	for _, ptypeState := range ptypeStates {
+		tpt.Append([]string{"Container:", ptypeState.Container})
+		tpt.Append([]string{"Image:", ptypeState.Image})
+		if len(ptypeState.Command) != 0 {
+			tpt.Append([]string{"Command:"})
+			for _, command := range ptypeState.Command {
+				tpt.Append([]string{"", fmt.Sprintf("- %v", command)})
+			}
+		}
+		if len(ptypeState.Args) != 0 {
+			tpt.Append([]string{"Args:"})
+			for _, arg := range ptypeState.Args {
+				tpt.Append([]string{"", fmt.Sprintf("- %v", arg)})
+			}
+		}
+		if len(ptypeState.Limits) != 0 {
+			tpt.Append([]string{"Limits:"})
+			for r, q := range ptypeState.Limits {
+				tpt.Append([]string{"", fmt.Sprintf("%s %s", r, q)})
+			}
+		}
+		if len(ptypeState.VolumeMounts) != 0 {
+			tpt.Append([]string{"Mounts:"})
+			for _, mount := range ptypeState.VolumeMounts {
+				tpt.Append([]string{"", fmt.Sprintf("%s from %s", mount.MountPath, mount.Name)})
+			}
+		}
+		sp := getContainerProbeString("", "", &ptypeState.StartupProbe)
+		if sp != "" {
+			tpt.Append([]string{"Startup:", strings.TrimSpace(sp)})
+		}
+		lp := getContainerProbeString("", "", &ptypeState.LivenessProbe)
+		if lp != "" {
+			tpt.Append([]string{"Liveness:", strings.TrimSpace(lp)})
+		}
+		rp := getContainerProbeString("", "", &ptypeState.ReadinessProbe)
+		if rp != "" {
+			tpt.Append([]string{"Readiness:", strings.TrimSpace(rp)})
+		}
+		if len(ptypeState.NodeSelector) != 0 {
+			tpt.Append([]string{"Node-Selectors:"})
+			for k, v := range ptypeState.NodeSelector {
+				tpt.Append([]string{"", fmt.Sprintf("%s=%s", k, v)})
+			}
+		}
+
+	}
+	tpt.Render()
+
+	if len(events) != 0 {
+		// table event
+		te := d.getDefaultFormatTable([]string{})
+		te.Append([]string{"Events:"})
+		te.Append([]string{d.indentString("REASON", 2), "MESSAGE", "CREATED"})
+		for _, ev := range events {
+			te.Append([]string{
+				d.indentString(ev.Reason, 2),
+				ev.Message,
+				d.formatTime(ev.Created),
+			})
+		}
+		te.Render()
+	}
+}
+
+func parsePtsTargets(targets []string) (map[string]int, error) {
+	targetMap := make(map[string]int)
+	regex := regexp.MustCompile(`^([a-z0-9]+(?:-[a-z0-9]+)*)=([0-9]+)$`)
+	var err error
+
+	for _, target := range targets {
+		if regex.MatchString(target) {
+			captures := regex.FindStringSubmatch(target)
+			targetMap[captures[1]], err = strconv.Atoi(captures[2])
+			if err != nil {
+				return nil, err
+			}
+		} else {
+			return nil, fmt.Errorf("'%s' does not match the pattern 'ptype=num', ex: web=2", target)
+		}
+	}
+
+	return targetMap, nil
+}
diff --git a/internal/commands/pts_test.go b/internal/commands/pts_test.go
new file mode 100644
index 000000000..c2771c815
--- /dev/null
+++ b/internal/commands/pts_test.go
@@ -0,0 +1,268 @@
+package commands
+
+import (
+	"bytes"
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/workflow-cli/pkg/testutil"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestPrintProcessTypes(t *testing.T) {
+	var b bytes.Buffer
+	ptypes := api.Ptypes{
+		{
+			Name:              "web",
+			Release:           "v1",
+			Ready:             "1/1",
+			UpToDate:          1,
+			AvailableReplicas: 1,
+			Started:           "2024-07-04T14:33:00CST",
+			Garbage:           false,
+		},
+		{
+			Name:              "worker",
+			Release:           "v1",
+			Ready:             "1/1",
+			UpToDate:          1,
+			AvailableReplicas: 1,
+			Started:           "2024-07-04T14:33:00CST",
+			Garbage:           false,
+		},
+	}
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+
+	printProcessTypes(&DryccCmd{WOut: &b, ConfigFile: cf}, "appname", ptypes)
+
+	assert.Equal(t, b.String(), `NAME      RELEASE    READY    UP-TO-DATE    AVAILABLE    STARTED                   GARBAGE 
+web       v1         1/1      1             1            2024-07-04T14:33:00CST    false      
+worker    v1         1/1      1             1            2024-07-04T14:33:00CST    false      
+`, "output")
+}
+
+func TestPtsList(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	server.Mux.HandleFunc("/v2/apps/foo/ptypes/", func(w http.ResponseWriter, _ *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `{
+			"count": 1,
+			"next": null,
+			"previous": null,
+			"results": [
+				{
+					"name": "web",
+					"release": "v1",
+					"ready": "1/1",
+					"up_to_date": 1,
+					"available_replicas": 1,
+					"started": "2016-02-13T00:47:52",
+					"garbage": false
+				}
+			]
+		}`)
+	})
+
+	err = cmdr.PtsList("foo", -1)
+	assert.NoError(t, err)
+
+	assert.Equal(t, b.String(), `NAME    RELEASE    READY    UP-TO-DATE    AVAILABLE    STARTED                GARBAGE 
+web     v1         1/1      1             1            2016-02-13T00:47:52    false      
+`, "output")
+}
+
+type ptsTargetCases struct {
+	Targets       []string
+	ExpectedError bool
+	ExpectedMap   map[string]int
+	ExpectedMsg   string
+}
+
+func TestParsePtsTargets(t *testing.T) {
+	t.Parallel()
+
+	cases := []ptsTargetCases{
+		{[]string{"test"}, true, nil, "'test' does not match the pattern 'ptype=num', ex: web=2"},
+		{[]string{"test=a"}, true, nil, "'test=a' does not match the pattern 'ptype=num', ex: web=2"},
+		{[]string{"test="}, true, nil, "'test=' does not match the pattern 'ptype=num', ex: web=2"},
+		{[]string{"Test=2"}, true, nil, "'Test=2' does not match the pattern 'ptype=num', ex: web=2"},
+		{[]string{"test=2"}, false, map[string]int{"test": 2}, ""},
+		{[]string{"test-proc=2"}, false, map[string]int{"test-proc": 2}, ""},
+		{[]string{"test1=2"}, false, map[string]int{"test1": 2}, ""},
+	}
+
+	for _, check := range cases {
+		actual, err := parsePtsTargets(check.Targets)
+		if check.ExpectedError {
+			assert.Equal(t, err.Error(), check.ExpectedMsg, "error")
+		} else {
+			assert.NoError(t, err)
+			assert.Equal(t, actual, check.ExpectedMap, "error")
+		}
+	}
+}
+
+func TestPtsScale(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+	err = cmdr.PtsScale("foo", []string{"test"})
+	assert.Equal(t, err.Error(), "'test' does not match the pattern 'ptype=num', ex: web=2", "error")
+
+	server.Mux.HandleFunc("/v2/apps/foo/ptypes/scale/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.AssertBody(t, map[string]int{"web": 1}, r)
+		testutil.SetHeaders(w)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	b.Reset()
+	err = cmdr.PtsScale("foo", []string{"web=1"})
+	assert.NoError(t, err)
+	assert.Equal(t, testutil.StripProgress(b.String()), `Scaling process types... but first, coffee!
+done in 0s
+`, "output")
+}
+
+func TestPtsRestart(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	server.Mux.HandleFunc("/v2/apps/foo/ptypes/restart/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.AssertBody(t, map[string]string{"ptypes": ""}, r)
+		testutil.SetHeaders(w)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	b.Reset()
+	err = cmdr.PtsRestart("foo", []string{""}, "yes")
+	assert.NoError(t, err)
+
+	server.Mux.HandleFunc("/v2/apps/coolapp/ptypes/restart/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.AssertBody(t, map[string]string{"ptypes": "web"}, r)
+		testutil.SetHeaders(w)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	b.Reset()
+
+	err = cmdr.PtsRestart("coolapp", []string{"web"}, "")
+	assert.NoError(t, err)
+
+	server.Mux.HandleFunc("/v2/apps/testapp/ptypes/restart/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.AssertBody(t, map[string]string{"ptypes": "web,worker"}, r)
+		testutil.SetHeaders(w)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	b.Reset()
+
+	err = cmdr.PtsRestart("testapp", []string{"web", "worker"}, "")
+	assert.NoError(t, err)
+}
+
+func TestPtsDescribe(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+	server.Mux.HandleFunc("/v2/apps/foo/ptypes/foo-web/describe/", func(w http.ResponseWriter, _ *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `{
+            "count": 1,
+            "next": null,
+            "previous": null,
+            "results": [{
+                "container": "web",
+                "image": "registry.drycc.cc/base/base",
+                "command": ["bash", "-c"],
+                "args": ["sleep", "3600s"],
+                "readiness_probe": {
+                    "exec": {
+                        "command": ["ls", "-la"]
+                    },
+                    "failureThreshold": 3,
+                    "initialDelaySeconds": 50,
+                    "periodSeconds": 10,
+                    "successThreshold": 1,
+                    "timeoutSeconds": 50
+                },
+                "limits": {
+                    "cpu": "1",
+                    "memory": "2Gi"
+                },
+                "volume_mounts": [
+                    {
+                        "mountPath": "/data",
+                        "name": "myvolume"
+                    }
+                ]
+            }]
+        }`)
+	})
+	server.Mux.HandleFunc("/v2/apps/foo/events/", func(w http.ResponseWriter, _ *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `{
+            "count": 1,
+            "next": null,
+            "previous": null,
+            "results": [{
+                "reason": "ScalingReplicaSet",
+                "message": "Scaled up replica set example-go-web-6b44dbd6c8 to 2 from 1",
+                "created": "2024-07-03T16:28:00"
+            }]
+        }`)
+	})
+	err = cmdr.PtsDescribe("foo", "web")
+	assert.NoError(t, err)
+}
+
+func TestPtsClean(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	server.Mux.HandleFunc("/v2/apps/testapp/ptypes/clean/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.AssertBody(t, map[string]string{"ptypes": "worker"}, r)
+		testutil.SetHeaders(w)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	b.Reset()
+
+	err = cmdr.PtsClean("testapp", []string{"worker"})
+	assert.NoError(t, err)
+}
diff --git a/internal/commands/registry.go b/internal/commands/registry.go
new file mode 100644
index 000000000..831b231d8
--- /dev/null
+++ b/internal/commands/registry.go
@@ -0,0 +1,110 @@
+package commands
+
+import (
+	"fmt"
+
+	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/controller-sdk-go/config"
+	"github.com/drycc/workflow-cli/internal/loader"
+)
+
+// RegistryList lists an app's registry information.
+func (d *DryccCmd) RegistryList(appID, ptype string, version int) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	config, err := config.List(s.Client, appID, version)
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+	if len(config.Registry) == 0 {
+		d.Println(fmt.Sprintf("No registrys found in %s app.", appID))
+		return nil
+	}
+	ptypes := []string{}
+	if ptype != "" {
+		ptypes = append(ptypes, ptype)
+	} else {
+		for ptype := range config.Registry {
+			ptypes = append(ptypes, ptype)
+		}
+	}
+
+	table := d.getDefaultFormatTable([]string{"PTYPE", "USERNAME", "PASSWORD"})
+	for _, ptype := range sortPtypes(ptypes) {
+		if config.Registry[ptype]["username"] != nil {
+			table.Append([]string{
+				ptype,
+				fmt.Sprintf("%v", config.Registry[ptype]["username"]),
+				fmt.Sprintf("%v", config.Registry[ptype]["password"]),
+			})
+		}
+	}
+
+	table.Render()
+
+	return nil
+}
+
+// RegistrySet sets an app's registry information.
+func (d *DryccCmd) RegistrySet(appID, ptype, username, password string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	d.Print("Applying registry information... ")
+
+	quit := progress(d.WOut)
+
+	configObj := api.Config{}
+	registry := make(map[string]map[string]any)
+	registry[ptype] = map[string]any{
+		"username": username,
+		"password": password,
+	}
+	configObj.Registry = registry
+
+	_, err = config.Set(s.Client, appID, configObj, true)
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	d.Print("done\n\n")
+
+	return d.RegistryList(appID, ptype, -1)
+}
+
+// RegistryUnset removes an app's registry information.
+func (d *DryccCmd) RegistryUnset(appID, ptype string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	d.Print("Applying registry information... ")
+
+	quit := progress(d.WOut)
+
+	configObj := api.Config{}
+	registry := make(map[string]map[string]any)
+	registry[ptype] = map[string]any{
+		"username": nil,
+		"password": nil,
+	}
+	configObj.Registry = registry
+	_, err = config.Set(s.Client, appID, configObj, true)
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	d.Print("done\n\n")
+
+	return nil
+}
diff --git a/internal/commands/registry_test.go b/internal/commands/registry_test.go
new file mode 100644
index 000000000..0e8474ee6
--- /dev/null
+++ b/internal/commands/registry_test.go
@@ -0,0 +1,149 @@
+package commands
+
+import (
+	"bytes"
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/workflow-cli/pkg/testutil"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRegistryList(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+
+	server.Mux.HandleFunc("/v2/apps/enterprise/config/", func(w http.ResponseWriter, _ *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `{
+			"owner": "jkirk",
+			"app": "enterprise",
+			"values": [],
+			"memory": {},
+			"cpu": {},
+			"tags": {},
+			"registry": {
+			  "web": {
+				"username": "jkirk",
+				"password": "ncc1701"
+			}},
+			"created": "2014-01-01T00:00:00UTC",
+			"updated": "2014-01-01T00:00:00UTC",
+			"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
+		}`)
+	})
+
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	err = cmdr.RegistryList("enterprise", "web", -1)
+	assert.NoError(t, err)
+	testutil.AssertOutput(t, b.String(), `PTYPE    USERNAME    PASSWORD
+web      jkirk       ncc1701
+`)
+}
+
+func TestRegistrySet(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+
+	server.Mux.HandleFunc("/v2/apps/foo/config/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.SetHeaders(w)
+		if r.Method == "POST" {
+			testutil.AssertBody(t, api.Config{
+				Registry: map[string]map[string]any{
+					"web": {
+						"username": "jkirk",
+						"password": "ncc1701",
+					},
+				},
+			}, r)
+		}
+
+		fmt.Fprintf(w, `{
+			"owner": "jkirk",
+			"app": "foo",
+			"values": [],
+			"memory": {},
+			"cpu": {},
+			"registry": {
+			  "web": {
+			  	"username": "jkirk",
+				"password": "ncc1701"
+			  }
+			},
+			"registry": {},
+			"created": "2014-01-01T00:00:00UTC",
+			"updated": "2014-01-01T00:00:00UTC",
+			"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
+		}`)
+	})
+
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	err = cmdr.RegistrySet("foo", "web", "jkirk", "ncc1701")
+	assert.NoError(t, err)
+
+	testutil.AssertOutput(t, testutil.StripProgress(b.String()), `Applying registry information... done
+
+PTYPE    USERNAME    PASSWORD
+web      jkirk       ncc1701
+`)
+}
+
+func TestRegistryUnset(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+
+	server.Mux.HandleFunc("/v2/apps/foo/config/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.SetHeaders(w)
+		if r.Method == "POST" {
+			testutil.AssertBody(t, api.Config{
+				Registry: map[string]map[string]any{
+					"web": {
+						"username": nil,
+						"password": nil,
+					},
+				},
+			}, r)
+		}
+
+		fmt.Fprintf(w, `{
+			"owner": "jkirk",
+			"app": "foo",
+			"values": [],
+			"memory": {},
+			"cpu": {},
+			"tags": {},
+			"registry": {},
+			"created": "2014-01-01T00:00:00UTC",
+			"updated": "2014-01-01T00:00:00UTC",
+			"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
+		}`)
+	})
+
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	err = cmdr.RegistryUnset("foo", "web")
+	assert.NoError(t, err)
+
+	assert.Equal(t, testutil.StripProgress(b.String()), `Applying registry information... done
+
+`, "output")
+}
diff --git a/internal/commands/releases.go b/internal/commands/releases.go
new file mode 100644
index 000000000..121ee8069
--- /dev/null
+++ b/internal/commands/releases.go
@@ -0,0 +1,154 @@
+package commands
+
+import (
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/drycc/controller-sdk-go/releases"
+	"github.com/drycc/workflow-cli/internal/loader"
+)
+
+// ReleasesList lists an app's releases.
+func (d *DryccCmd) ReleasesList(appID string, targets []string, results int) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	if results == defaultLimit {
+		results = s.Limit
+	}
+	ptypes := strings.Join(targets, ",")
+	releases, count, err := releases.List(s.Client, appID, ptypes, results)
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+	if count == 0 {
+		d.Println(fmt.Sprintf("No releases found in %s app.", appID))
+	} else {
+		table := d.getDefaultFormatTable([]string{"STATE", "VERSION", "CREATED", "SUMMARY"})
+		for _, r := range releases {
+			summary := r.Summary
+			if len(summary) > 64 {
+				summary = fmt.Sprintf("%s[...]", summary[:64])
+			}
+			table.Append([]string{r.State, fmt.Sprintf("v%d", r.Version), d.formatTime(r.Created), summary})
+		}
+		table.Render()
+	}
+	return nil
+}
+
+// ReleasesInfo prints info about a specific release.
+func (d *DryccCmd) ReleasesInfo(appID string, version int) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	r, err := releases.Get(s.Client, appID, version)
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+	table := d.getDefaultFormatTable([]string{})
+	table.Append([]string{"App:", r.App})
+	table.Append([]string{"UUID:", r.UUID})
+	table.Append([]string{"State:", r.State})
+	table.Append([]string{"Build:", r.Build})
+	table.Append([]string{"Config:", r.Config})
+	table.Append([]string{"Created:", d.formatTime(r.Created)})
+	table.Append([]string{"Updated:", d.formatTime(r.Updated)})
+	table.Append([]string{"Summary:", d.wrapString(r.Summary)})
+	if r.Exception != "" {
+		table.Append([]string{"Exception:", d.wrapString(r.Exception)})
+	}
+	table.Append([]string{"Version:", fmt.Sprintf("v%v", r.Version)})
+	table.Render()
+	// Conditions
+	if len(r.Conditions) != 0 {
+		// table event
+		te := d.getDefaultFormatTable([]string{})
+		te.Append([]string{"Conditions:"})
+		for _, c := range r.Conditions {
+			ptypes := "<none>"
+			if len(c.Ptypes) != 0 {
+				ptypes = strings.Join(c.Ptypes, ",")
+			}
+			exception := "<none>"
+			if c.Exception != "" {
+				exception = c.Exception
+			}
+			te.Append([]string{d.indentString("- created:", 2), d.formatTime(c.Created)})
+			te.Append([]string{d.indentString("state:", 4), c.State})
+			te.Append([]string{d.indentString("action:", 4), c.Action})
+			te.Append([]string{d.indentString("ptypes:", 4), d.wrapString(ptypes)})
+			te.Append([]string{d.indentString("exception:", 4), d.wrapString(exception)})
+		}
+		te.Render()
+	}
+	return nil
+}
+
+// ReleasesDeploy force deploy lastest release.
+func (d *DryccCmd) ReleasesDeploy(appID string, targets []string, force bool, confirm string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+	if len(targets) == 0 && (confirm == "" || confirm != "yes") {
+		d.Printf(` !    WARNING: Potentially Deploy Action
+ !    This command will deploy all processes of the application ptype
+ !    To proceed, type "yes" !
+
+> `)
+
+		fmt.Scanln(&confirm)
+		if confirm != "yes" {
+			return fmt.Errorf("cancel the deploy action")
+		}
+	}
+	d.Printf("Deploying process types... but first, %s!\n", drinkOfChoice())
+	startTime := time.Now()
+	quit := progress(d.WOut)
+	ptypes := strings.Join(targets, ",")
+	targetMap := map[string]any{
+		"ptypes": ptypes,
+		"force":  force,
+	}
+	err = releases.Deploy(s.Client, appID, targetMap)
+	quit <- true
+	<-quit
+	if err != nil {
+		return err
+	}
+
+	d.Printf("done in %ds\n", int(time.Since(startTime).Seconds()))
+	return nil
+}
+
+// ReleasesRollback rolls an app back to a previous release.
+func (d *DryccCmd) ReleasesRollback(appID string, targets []string, version int) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+	ptypes := strings.Join(targets, ",")
+	if version == -1 {
+		d.Print("Rolling back one release... ")
+	} else {
+		d.Printf("Rolling back to v%d... ", version)
+	}
+
+	quit := progress(d.WOut)
+	newVersion, err := releases.Rollback(s.Client, appID, ptypes, version)
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	d.Printf("done, v%d\n", newVersion)
+
+	return nil
+}
diff --git a/cmd/releases_test.go b/internal/commands/releases_test.go
similarity index 69%
rename from cmd/releases_test.go
rename to internal/commands/releases_test.go
index 168d95933..27081b593 100644
--- a/cmd/releases_test.go
+++ b/internal/commands/releases_test.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"bytes"
@@ -57,11 +57,11 @@ func TestReleasesList(t *testing.T) {
 		}`)
 	})
 
-	err = cmdr.ReleasesList("numenor", -1)
+	err = cmdr.ReleasesList("numenor", []string{}, -1)
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `UUID                                    OWNER     STATE      VERSION    CREATED                   SUMMARY                        
-c4aed81c-d1ca-4ff1-ab89-d2151264e1a3    nazgul    succeed    v2         2016-08-22T17:40:16Z      khamul added ANGMAR               
-de1bf5b5-4a72-4f94-a10c-d2a3741cdf75    nazgul    succeed    v1         2014-01-01T00:00:00UTC    nazgul created initial release    
+	assert.Equal(t, b.String(), `STATE      VERSION    CREATED                   SUMMARY                        
+succeed    v2         2016-08-22T17:40:16Z      khamul added ANGMAR               
+succeed    v1         2014-01-01T00:00:00UTC    nazgul created initial release    
 `, "output")
 }
 
@@ -98,10 +98,10 @@ func TestReleasesListLimit(t *testing.T) {
 		}`)
 	})
 
-	err = cmdr.ReleasesList("numenor", 1)
+	err = cmdr.ReleasesList("numenor", []string{}, 1)
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `UUID                                    OWNER     STATE      VERSION    CREATED                 SUMMARY             
-c4aed81c-d1ca-4ff1-ab89-d2151264e1a3    nazgul    succeed    v2         2016-08-22T17:40:16Z    khamul added ANGMAR    
+	assert.Equal(t, b.String(), `STATE      VERSION    CREATED                 SUMMARY             
+succeed    v2         2016-08-22T17:40:16Z    khamul added ANGMAR    
 `, "output")
 }
 
@@ -127,7 +127,15 @@ func TestReleasesInfo(t *testing.T) {
 			"version": 2,
 			"summary": "khamul added ANGMAR",
 			"config": "3bb816b1-4fde-4b06-8afe-acd12f58a266",
-			"build": null
+			"build": null,
+			"exception": null,
+			"conditions": [{
+				"state": "succeed",
+				"action": "pipeline",
+				"ptypes": ["web"],
+				"exception": "Boom!",
+				"created": "2024-08-27T08:31:36Z"
+			}]
 		}`)
 	})
 
@@ -136,16 +144,51 @@ func TestReleasesInfo(t *testing.T) {
 	assert.Equal(t, b.String(), `App:        numenor                                 
 UUID:       c4aed81c-d1ca-4ff1-ab89-d2151264e1a3    
 State:      succeed                                 
-Owner:      nazgul                                  
 Build:                                              
 Config:     3bb816b1-4fde-4b06-8afe-acd12f58a266    
 Created:    2016-08-22T17:40:16Z                    
 Updated:    2016-08-22T17:40:16Z                    
 Summary:    khamul added ANGMAR                     
 Version:    v2                                      
+Conditions:       
+  - created:      2024-08-27T08:31:36Z    
+    state:        succeed                 
+    action:       pipeline                
+    ptypes:       web                     
+    exception:    Boom!                   
 `, "output")
 }
 
+func TestReleasesDeploy(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+	server.Mux.HandleFunc("/v2/apps/example-go/releases/deploy/", func(res http.ResponseWriter, req *http.Request) {
+		testutil.SetHeaders(res)
+		io.ReadAll(req.Body)
+		if err != nil {
+			fmt.Println(err)
+			res.WriteHeader(http.StatusInternalServerError)
+			res.Write(nil)
+		}
+
+		res.WriteHeader(http.StatusCreated)
+	})
+	err = cmdr.ReleasesDeploy("example-go", []string{}, false, "yes")
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = cmdr.ReleasesDeploy("example-go", []string{"web", "task"}, false, "")
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
 func TestReleasesRollback(t *testing.T) {
 	t.Parallel()
 	cf, server, err := testutil.NewTestServerAndClient()
@@ -168,20 +211,20 @@ func TestReleasesRollback(t *testing.T) {
 		fmt.Fprintf(w, `{"version": 5}`)
 	})
 
-	err = cmdr.ReleasesRollback("numenor", -1)
+	err = cmdr.ReleasesRollback("numenor", []string{"web"}, -1)
 	assert.NoError(t, err)
 	assert.Equal(t, testutil.StripProgress(b.String()), "Rolling back one release... done, v5\n", "output")
 
 	server.Mux.HandleFunc("/v2/apps/angmar/releases/rollback/", func(w http.ResponseWriter, r *http.Request) {
 		testutil.SetHeaders(w)
-		testutil.AssertBody(t, api.ReleaseRollback{Version: 3}, r)
+		testutil.AssertBody(t, api.ReleaseRollback{Ptypes: "web", Version: 3}, r)
 		w.WriteHeader(http.StatusCreated)
 		fmt.Fprintf(w, `{"version": 3}`)
 	})
 
 	b.Reset()
 
-	err = cmdr.ReleasesRollback("angmar", 3)
+	err = cmdr.ReleasesRollback("angmar", []string{"web"}, 3)
 	assert.NoError(t, err)
 	assert.Equal(t, testutil.StripProgress(b.String()), "Rolling back to v3... done, v3\n", "output")
 }
diff --git a/cmd/resources.go b/internal/commands/resources.go
similarity index 82%
rename from cmd/resources.go
rename to internal/commands/resources.go
index 6e8e47855..64640de9e 100644
--- a/cmd/resources.go
+++ b/internal/commands/resources.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"encoding/base64"
@@ -7,17 +7,16 @@ import (
 	"regexp"
 	"strconv"
 
-	"sigs.k8s.io/yaml"
-
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/resources"
-	"github.com/drycc/workflow-cli/settings"
+	"github.com/drycc/workflow-cli/internal/loader"
+	"github.com/drycc/workflow-cli/pkg/settings"
+	"sigs.k8s.io/yaml"
 )
 
-// ResourceServices list resource service
+// ResourcesServices lists available resource services.
 func (d *DryccCmd) ResourcesServices(results int) error {
 	s, err := settings.Load(d.ConfigFile)
-
 	if err != nil {
 		return err
 	}
@@ -46,10 +45,9 @@ func (d *DryccCmd) ResourcesServices(results int) error {
 	return nil
 }
 
-// ResourcePlans list resource plans
+// ResourcesPlans lists plans for a resource service.
 func (d *DryccCmd) ResourcesPlans(serviceName string, results int) error {
 	s, err := settings.Load(d.ConfigFile)
-
 	if err != nil {
 		return err
 	}
@@ -80,12 +78,11 @@ func (d *DryccCmd) ResourcesPlans(serviceName string, results int) error {
 
 // ResourcesCreate create a resource for the application
 func (d *DryccCmd) ResourcesCreate(appID, plan string, name string, params []string, values string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
-	paramsMap := make(map[string]interface{})
+	paramsMap := make(map[string]any)
 	if values != "" {
 		valueFile, err := os.Stat(values)
 		if err != nil {
@@ -98,7 +95,7 @@ func (d *DryccCmd) ResourcesCreate(appID, plan string, name string, params []str
 		if err != nil {
 			return err
 		}
-		parsed := make(map[string]interface{})
+		parsed := make(map[string]any)
 		err = yaml.Unmarshal(rawValues, &parsed)
 		if err != nil {
 			return err
@@ -132,8 +129,7 @@ func (d *DryccCmd) ResourcesCreate(appID, plan string, name string, params []str
 
 // ResourcesList list resources in the application
 func (d *DryccCmd) ResourcesList(appID string, results int) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -149,14 +145,12 @@ func (d *DryccCmd) ResourcesList(appID string, results int) error {
 	if count == 0 {
 		d.Println(fmt.Sprintf("No resources found in %s app.", appID))
 	} else {
-		table := d.getDefaultFormatTable([]string{"UUID", "NAME", "OWNER", "PLAN", "UPDATED"})
+		table := d.getDefaultFormatTable([]string{"NAME", "PLAN", "UPDATED"})
 		for _, resource := range resources {
 			table.Append([]string{
-				resource.UUID,
 				resource.Name,
-				resource.Owner,
 				resource.Plan,
-				resource.Updated,
+				d.formatTime(resource.Updated),
 			})
 		}
 		table.Render()
@@ -165,14 +159,13 @@ func (d *DryccCmd) ResourcesList(appID string, results int) error {
 }
 
 // ResourceGet describe a resource from the application
-func (d *DryccCmd) ResourceGet(appID, name string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+func (d *DryccCmd) ResourceGet(appID, name string, details bool) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
 
-	//d.Printf(" %s from %s... ", name, appID)
+	// d.Printf(" %s from %s... ", name, appID)
 
 	resource, err := resources.Get(s.Client, appID, name)
 	if d.checkAPICompatibility(s.Client, err) != nil {
@@ -183,28 +176,28 @@ func (d *DryccCmd) ResourceGet(appID, name string) error {
 	table.Append([]string{"UUID:", resource.UUID})
 	table.Append([]string{"Name:", resource.Name})
 	table.Append([]string{"Plan:", resource.Plan})
-	table.Append([]string{"Owner:", resource.Owner})
 	table.Append([]string{"Status:", resource.Status})
 	table.Append([]string{"Binding:", resource.Binding})
 	table.Append([]string{"Data:"})
 	for _, key := range *sortKeys(resource.Data) {
 		table.Append([]string{"", fmt.Sprintf("%s:", key), fmt.Sprintf("%s", resource.Data[key])})
 	}
-	table.Append([]string{"Options:"})
-	for _, key := range *sortKeys(resource.Options) {
-		table.Append([]string{"", fmt.Sprintf("%s:", key), fmt.Sprintf("%s", resource.Options[key])})
+	if details {
+		table.Append([]string{"Options:"})
+		for _, key := range *sortKeys(resource.Options) {
+			table.Append([]string{"", fmt.Sprintf("%s:", key), fmt.Sprintf("%s", resource.Options[key])})
+		}
+		table.Append([]string{"Message:", d.wrapString(safeGetString(resource.Message))})
 	}
-	table.Append([]string{"Message:", d.wrapString(safeGetString(resource.Message))})
-	table.Append([]string{"Created:", resource.Created})
-	table.Append([]string{"Updated:", resource.Updated})
+	table.Append([]string{"Created:", d.formatTime(resource.Created)})
+	table.Append([]string{"Updated:", d.formatTime(resource.Updated)})
 	table.Render()
 	return nil
 }
 
 // ResourceDelete delete a resource from the application
 func (d *DryccCmd) ResourceDelete(appID, name, confirm string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -239,12 +232,11 @@ func (d *DryccCmd) ResourceDelete(appID, name, confirm string) error {
 
 // ResourcePut update a resource for the application
 func (d *DryccCmd) ResourcePut(appID, plan string, name string, params []string, values string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
-	paramsMap := make(map[string]interface{})
+	paramsMap := make(map[string]any)
 	if values != "" {
 		valueFile, err := os.Stat(values)
 		if err != nil {
@@ -257,7 +249,7 @@ func (d *DryccCmd) ResourcePut(appID, plan string, name string, params []string,
 		if err != nil {
 			return err
 		}
-		parsed := make(map[string]interface{})
+		parsed := make(map[string]any)
 		err = yaml.Unmarshal(rawValues, &parsed)
 		if err != nil {
 			return err
@@ -290,8 +282,7 @@ func (d *DryccCmd) ResourcePut(appID, plan string, name string, params []string,
 
 // ResourceBind mount a resource to process of the application
 func (d *DryccCmd) ResourceBind(appID string, name string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -314,8 +305,7 @@ func (d *DryccCmd) ResourceBind(appID string, name string) error {
 
 // ResourceUnbind resource a resource the application
 func (d *DryccCmd) ResourceUnbind(appID string, name string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -337,7 +327,7 @@ func (d *DryccCmd) ResourceUnbind(appID string, name string) error {
 }
 
 // parseParams transfer params to map
-func parseParams(paramsMap map[string]interface{}, params []string) (map[string]interface{}, error) {
+func parseParams(paramsMap map[string]any, params []string) (map[string]any, error) {
 	regex := regexp.MustCompile(`^([A-z_]+[A-z0-9_]*[\.{1}[A-z0-9_]+]*)=([\s\S]*)$`)
 	for _, param := range params {
 		if regex.MatchString(param) {
diff --git a/cmd/resources_test.go b/internal/commands/resources_test.go
similarity index 82%
rename from cmd/resources_test.go
rename to internal/commands/resources_test.go
index e496fc81d..e6711c673 100644
--- a/cmd/resources_test.go
+++ b/internal/commands/resources_test.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"bytes"
@@ -7,7 +7,6 @@ import (
 	"testing"
 
 	"github.com/drycc/controller-sdk-go/api"
-
 	"github.com/drycc/workflow-cli/pkg/testutil"
 	"github.com/stretchr/testify/assert"
 )
@@ -39,9 +38,9 @@ func TestResourcesServices(t *testing.T) {
 	err = cmdr.ResourcesServices(100)
 	assert.NoError(t, err)
 
-	assert.Equal(t, b.String(), `ID                                      NAME     UPDATEABLE 
-332588e0-6c2c-4f56-a6af-a56fd01ec4b4    mysql    true          
-`, "output")
+	testutil.AssertOutput(t, b.String(), `ID                                      NAME     UPDATEABLE
+332588e0-6c2c-4f56-a6af-a56fd01ec4b4    mysql    true
+`)
 }
 
 func TestResourcesPlans(t *testing.T) {
@@ -69,9 +68,9 @@ func TestResourcesPlans(t *testing.T) {
 	err = cmdr.ResourcesPlans("mysql", 100)
 	assert.NoError(t, err)
 
-	assert.Equal(t, b.String(), `ID                                      NAME             DESCRIPTION                                                   
-4d1dbd33-201b-45bc-9abb-757584ef7ab8    standard-1600    mysql standard-1600 plan which limit persistence size 1600Gi.    
-`, "output")
+	testutil.AssertOutput(t, b.String(), `ID                                      NAME             DESCRIPTION
+4d1dbd33-201b-45bc-9abb-757584ef7ab8    standard-1600    mysql standard-1600 plan which limit persistence size 1600Gi.
+`)
 }
 
 func TestResourcesCreate(t *testing.T) {
@@ -135,9 +134,9 @@ func TestResourcesList(t *testing.T) {
 	err = cmdr.ResourcesList("example-go", -1)
 	assert.NoError(t, err)
 
-	assert.Equal(t, b.String(), `UUID                                    NAME     OWNER    PLAN         UPDATED                
-de1bf5b5-4a72-4f94-a10c-d2a3741cdf75    mysql    test     mysql:5.6    2020-09-08T00:00:00UTC    
-`, "output")
+	testutil.AssertOutput(t, b.String(), `NAME     PLAN         UPDATED
+mysql    mysql:5.6    2020-09-08T00:00:00UTC
+`)
 }
 
 func TestResourceGet(t *testing.T) {
@@ -167,26 +166,25 @@ func TestResourceGet(t *testing.T) {
 }`)
 	})
 
-	err = cmdr.ResourceGet("example-go", "mysql")
+	err = cmdr.ResourceGet("example-go", "mysql", true)
 	assert.NoError(t, err)
 	// todo format data json to yaml
-	assert.Equal(t, b.String(), `App:        example-go                              
-UUID:       de1bf5b5-4a72-4f94-a10c-d2a3741cdf75    
-Name:       mysql                                   
-Plan:       mysql:5.6                               
-Owner:      test                                    
-Status:     Ready                                   
-Binding:    Ready                                   
-Data:       
-            data12:                                 value1    
-            data3:                                  value1    
-Options:    
-            para122:                                value1    
-            para13451:                              value2    
-Message:    <none>                                  
-Created:    2020-09-08T00:00:00UTC                  
-Updated:    2020-09-08T00:00:00UTC                  
-`, "output")
+	testutil.AssertOutput(t, b.String(), `App:        example-go
+UUID:       de1bf5b5-4a72-4f94-a10c-d2a3741cdf75
+Name:       mysql
+Plan:       mysql:5.6
+Status:     Ready
+Binding:    Ready
+Data:
+            data12:                                 value1
+            data3:                                  value1
+Options:
+            para122:                                value1
+            para13451:                              value2
+Message:    <none>
+Created:    2020-09-08T00:00:00UTC
+Updated:    2020-09-08T00:00:00UTC
+`)
 }
 
 func TestResourceDelete(t *testing.T) {
@@ -221,7 +219,7 @@ func TestResourcePut(t *testing.T) {
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
 	server.Mux.HandleFunc("/v2/apps/example-go/resources/mysql/", func(w http.ResponseWriter, r *http.Request) {
-		paras := make(map[string]interface{}, 1)
+		paras := make(map[string]any, 1)
 		paras["para1.para2"] = "v1"
 		testutil.AssertBody(t, api.Resource{Plan: "mysql:5.7", Options: paras}, r)
 		testutil.SetHeaders(w)
diff --git a/cmd/routes.go b/internal/commands/routes.go
similarity index 66%
rename from cmd/routes.go
rename to internal/commands/routes.go
index 3953b4dfb..5123b57b2 100644
--- a/cmd/routes.go
+++ b/internal/commands/routes.go
@@ -1,24 +1,26 @@
-package cmd
+package commands
 
 import (
+	"encoding/json"
 	"fmt"
 	"os"
 
+	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/routes"
+	"github.com/drycc/workflow-cli/internal/loader"
 	"sigs.k8s.io/yaml"
 )
 
 // RoutesCreate create a route to an app.
-func (d *DryccCmd) RoutesCreate(appID, name string, procType string, kind string, port int) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+func (d *DryccCmd) RoutesCreate(appID, name string, kind string, backendRefs ...api.BackendRefRequest) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
 	d.Printf("Adding route %s to %s... ", name, appID)
 
 	quit := progress(d.WOut)
-	err = routes.New(s.Client, appID, name, procType, kind, port)
+	err = routes.New(s.Client, appID, name, kind, backendRefs...)
 	quit <- true
 	<-quit
 	if d.checkAPICompatibility(s.Client, err) != nil {
@@ -31,8 +33,7 @@ func (d *DryccCmd) RoutesCreate(appID, name string, procType string, kind string
 
 // RoutesList lists routes for the app
 func (d *DryccCmd) RoutesList(appID string, results int) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -47,31 +48,31 @@ func (d *DryccCmd) RoutesList(appID string, results int) error {
 	if count == 0 {
 		d.Println(fmt.Sprintf("No routes found in %s app.", appID))
 	} else {
-		table := d.getDefaultFormatTable([]string{"NAME", "OWNER", "PTYPE", "KIND", "SERVICE-PORT", "GATEWAY", "LISTENER-PORT"})
+		table := d.getDefaultFormatTable([]string{"NAME", "KIND", "GATEWAYS", "SERVICES"})
 		for _, route := range routes {
-			if len(route.ParentRefs) > 0 {
-				for _, gateway := range route.ParentRefs {
-					table.Append([]string{
-						route.Name,
-						route.Owner,
-						route.Type,
-						route.Kind,
-						fmt.Sprint(route.Port),
-						gateway.Name,
-						fmt.Sprint(gateway.Port),
-					})
+			var services []string
+			for _, rule := range route.Rules {
+				if backends, ok := rule["backendRefs"].([]any); ok {
+					for _, backend := range backends {
+						if service, ok := backend.(map[string]any); ok {
+							services = append(services, fmt.Sprintf("%v:%v", service["name"], service["port"]))
+						}
+					}
 				}
-			} else {
-				table.Append([]string{
-					route.Name,
-					route.Owner,
-					route.Type,
-					route.Kind,
-					fmt.Sprint(route.Port),
-					"",
-					"",
-				})
 			}
+			var gateways []string
+			for _, gateway := range route.ParentRefs {
+				gateways = append(gateways, fmt.Sprintf("%s:%d", gateway.Name, gateway.Port))
+			}
+			gatewaysBytes, err := json.Marshal(gateways)
+			if err != nil {
+				return err
+			}
+			servicesBytes, err := json.Marshal(services)
+			if err != nil {
+				return err
+			}
+			table.Append([]string{route.Name, route.Kind, string(gatewaysBytes), string(servicesBytes)})
 		}
 		table.Render()
 	}
@@ -80,8 +81,7 @@ func (d *DryccCmd) RoutesList(appID string, results int) error {
 
 // RoutesAttach bind a route to gateway.
 func (d *DryccCmd) RoutesAttach(appID, name string, port int, gateway string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -101,8 +101,7 @@ func (d *DryccCmd) RoutesAttach(appID, name string, port int, gateway string) er
 
 // RoutesDetach bind a route to gateway.
 func (d *DryccCmd) RoutesDetach(appID, name string, port int, gateway string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -122,8 +121,7 @@ func (d *DryccCmd) RoutesDetach(appID, name string, port int, gateway string) er
 
 // RoutesGet get rule of route for the app
 func (d *DryccCmd) RoutesGet(appID string, name string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -144,7 +142,7 @@ func (d *DryccCmd) RoutesGet(appID string, name string) error {
 
 // RoutesSet set rule of route for the app
 func (d *DryccCmd) RoutesSet(appID string, name string, ruleFile string) error {
-	s, appID, err := load(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -175,8 +173,7 @@ func (d *DryccCmd) RoutesSet(appID string, name string, ruleFile string) error {
 
 // RoutesRemove removes a route registered with an app.
 func (d *DryccCmd) RoutesRemove(appID, name string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/routes_test.go b/internal/commands/routes_test.go
similarity index 78%
rename from cmd/routes_test.go
rename to internal/commands/routes_test.go
index eaf775d1a..fc25d46ac 100644
--- a/cmd/routes_test.go
+++ b/internal/commands/routes_test.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"bytes"
@@ -23,14 +23,21 @@ func TestRoutesCreate(t *testing.T) {
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
 	server.Mux.HandleFunc("/v2/apps/foo/routes/", func(w http.ResponseWriter, r *http.Request) {
-		testutil.AssertBody(t, api.RouteCreateRequest{Name: "example-go", Type: "web", Port: 443, Kind: "HTTPRoute"}, r)
+		request := api.RouteCreateRequest{
+			Name: "example-go",
+			Kind: "HTTPRoute",
+			Rules: []api.RequestRouteRule{{
+				BackendRefs: []api.BackendRefRequest{{Name: "example-go", Port: 443}},
+			}},
+		}
+		testutil.AssertBody(t, request, r)
 		testutil.SetHeaders(w)
 		w.WriteHeader(http.StatusCreated)
 		// Body isn't used by CLI, so it isn't set.
 		w.Write([]byte("{}"))
 	})
 
-	err = cmdr.RoutesCreate("foo", "example-go", "web", "HTTPRoute", 443)
+	err = cmdr.RoutesCreate("foo", "example-go", "HTTPRoute", api.BackendRefRequest{Name: "example-go", Port: 443})
 	assert.NoError(t, err)
 
 	assert.Equal(t, testutil.StripProgress(b.String()), "Adding route example-go to foo... done\n", "output")
@@ -59,15 +66,35 @@ func TestRoutesList(t *testing.T) {
             "owner": "test",
             "updated": "2023-04-19T00:00:00UTC",
             "name": "example-go",
-            "procfile_type": "web",
+            "ptype": "web",
             "kind": "HTTPRoute",
             "port": 80,
             "parent_refs": [
                 {
                     "name": "example-go",
                     "port": 80
+                },
+                {
+                    "name": "example-go",
+                    "port": 8080
                 }
-            ]
+            ],
+			"rules": [{
+				"backendRefs": [
+					{
+						"kind": "Service",
+						"name": "yygl-nextcloud",
+						"port": 80,
+						"weight": 100
+					},
+					{
+						"kind": "Service",
+						"name": "yygl-nextcloud",
+						"port": 8080,
+						"weight": 100
+					}
+				]
+			}]
         }
     ]
 }`)
@@ -76,8 +103,8 @@ func TestRoutesList(t *testing.T) {
 	err = cmdr.RoutesList("foo", -1)
 	assert.NoError(t, err)
 
-	assert.Equal(t, b.String(), `NAME          OWNER    PTYPE    KIND         SERVICE-PORT    GATEWAY       LISTENER-PORT 
-example-go    test     web      HTTPRoute    80              example-go    80               
+	assert.Equal(t, b.String(), `NAME          KIND         GATEWAYS                               SERVICES                                    
+example-go    HTTPRoute    ["example-go:80","example-go:8080"]    ["yygl-nextcloud:80","yygl-nextcloud:8080"]    
 `, "output")
 }
 
@@ -92,7 +119,7 @@ func TestRoutesAttach(t *testing.T) {
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
 	server.Mux.HandleFunc("/v2/apps/foo/routes/example-go/attach/", func(w http.ResponseWriter, r *http.Request) {
-		testutil.AssertBody(t, api.RouteAttackRequest{Port: 4443, Gateway: "example-go"}, r)
+		testutil.AssertBody(t, api.RouteAttachRequest{Port: 4443, Gateway: "example-go"}, r)
 		testutil.SetHeaders(w)
 		w.WriteHeader(http.StatusCreated)
 		// Body isn't used by CLI, so it isn't set.
@@ -104,6 +131,7 @@ func TestRoutesAttach(t *testing.T) {
 
 	assert.Equal(t, testutil.StripProgress(b.String()), "Attaching route example-go to gateway example-go... done\n", "output")
 }
+
 func TestRoutesDetach(t *testing.T) {
 	t.Parallel()
 	cf, server, err := testutil.NewTestServerAndClient()
@@ -115,7 +143,7 @@ func TestRoutesDetach(t *testing.T) {
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
 	server.Mux.HandleFunc("/v2/apps/foo/routes/example-go/detach/", func(w http.ResponseWriter, r *http.Request) {
-		testutil.AssertBody(t, api.RouteDetackRequest{Port: 4443, Gateway: "example-go"}, r)
+		testutil.AssertBody(t, api.RouteDetachRequest{Port: 4443, Gateway: "example-go"}, r)
 		testutil.SetHeaders(w)
 		w.WriteHeader(http.StatusCreated)
 		// Body isn't used by CLI, so it isn't set.
@@ -140,36 +168,33 @@ func TestRouteGet(t *testing.T) {
 
 	server.Mux.HandleFunc("/v2/apps/foo/routes/example-go/rules/", func(w http.ResponseWriter, _ *http.Request) {
 		testutil.SetHeaders(w)
-		fmt.Fprintf(w, `{
-  "stable": [
-    {
-      "backendRefs": [
-        {
-          "group": "",
-          "kind": "Service",
-          "name": "example-go",
-          "port": 1234,
-          "weight": 1
-        }
-      ],
-      "matches": [
-        {
-          "path": {
-            "type": "PathPrefix",
-            "value": "/get"
-          }
-        }
-      ]
-    }
-  ]
-}`)
+		fmt.Fprintf(w, `
+[{
+	"backendRefs": [
+		{
+			"group": "",
+			"kind": "Service",
+			"name": "example-go",
+			"port": 1234,
+			"weight": 1
+		}
+		],
+		"matches": [
+		{
+			"path": {
+			"type": "PathPrefix",
+			"value": "/get"
+			}
+		}
+		]
+	}
+]`)
 	})
 
 	err = cmdr.RoutesGet("foo", "example-go")
 	assert.NoError(t, err)
 
-	assert.Equal(t, b.String(), `stable:
-- backendRefs:
+	assert.Equal(t, b.String(), `- backendRefs:
   - group: ""
     kind: Service
     name: example-go
@@ -200,7 +225,6 @@ func TestRoutesSet(t *testing.T) {
 	})
 	ruleFile, err := os.CreateTemp("", "rules.yaml")
 	rules := `
-stable:
 - backendRefs:
   - group: ""
     kind: Service
diff --git a/cmd/routing.go b/internal/commands/routing.go
similarity index 85%
rename from cmd/routing.go
rename to internal/commands/routing.go
index ff8113bf5..3c21f1701 100644
--- a/cmd/routing.go
+++ b/internal/commands/routing.go
@@ -1,14 +1,14 @@
-package cmd
+package commands
 
 import (
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/appsettings"
+	"github.com/drycc/workflow-cli/internal/loader"
 )
 
 // RoutingInfo provides information about the status of app routing.
 func (d *DryccCmd) RoutingInfo(appID string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -28,8 +28,7 @@ func (d *DryccCmd) RoutingInfo(appID string) error {
 
 // RoutingEnable enables an app from being exposed by the router.
 func (d *DryccCmd) RoutingEnable(appID string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -53,8 +52,7 @@ func (d *DryccCmd) RoutingEnable(appID string) error {
 
 // RoutingDisable disables an app from being exposed by the router.
 func (d *DryccCmd) RoutingDisable(appID string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/routing_test.go b/internal/commands/routing_test.go
similarity index 99%
rename from cmd/routing_test.go
rename to internal/commands/routing_test.go
index 0830a125c..073c286cb 100644
--- a/cmd/routing_test.go
+++ b/internal/commands/routing_test.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"bytes"
diff --git a/cmd/services.go b/internal/commands/services.go
similarity index 63%
rename from cmd/services.go
rename to internal/commands/services.go
index 43b2e8c96..87e811b44 100644
--- a/cmd/services.go
+++ b/internal/commands/services.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"fmt"
@@ -6,12 +6,12 @@ import (
 	"strconv"
 
 	"github.com/drycc/controller-sdk-go/services"
+	"github.com/drycc/workflow-cli/internal/loader"
 )
 
 // ServicesList lists extra services for the app
 func (d *DryccCmd) ServicesList(appID string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -21,15 +21,16 @@ func (d *DryccCmd) ServicesList(appID string) error {
 		return err
 	}
 	if len(services) > 0 {
-		table := d.getDefaultFormatTable([]string{"PTYPE", "NAME", "PORT", "PROTOCOL", "TARGET-PORT"})
+		table := d.getDefaultFormatTable([]string{"NAME", "PTYPE", "PORT", "PROTOCOL", "TARGET-PORT", "DOMAIN"})
 		for _, service := range services {
 			for _, port := range service.Ports {
 				table.Append([]string{
-					service.ProcfileType,
-					port.Name,
+					service.Name,
+					service.Ptype,
 					fmt.Sprint(port.Port),
 					port.Protocol,
 					fmt.Sprint(port.TargetPort),
+					service.Domain,
 				})
 			}
 		}
@@ -41,9 +42,8 @@ func (d *DryccCmd) ServicesList(appID string) error {
 }
 
 // ServicesAdd adds a service to an app.
-func (d *DryccCmd) ServicesAdd(appID, procfileType string, ports string, protocol string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+func (d *DryccCmd) ServicesAdd(appID, ptype string, ports string, protocol string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -51,10 +51,10 @@ func (d *DryccCmd) ServicesAdd(appID, procfileType string, ports string, protoco
 	if err != nil {
 		return err
 	}
-	d.Printf("Adding %s (%d) to %s... ", procfileType, portArray[0], appID)
+	d.Printf("Adding %s (%d) to %s... ", ptype, portArray[0], appID)
 
 	quit := progress(d.WOut)
-	err = services.New(s.Client, appID, procfileType, portArray[0], protocol, portArray[1])
+	err = services.New(s.Client, appID, ptype, portArray[0], protocol, portArray[1])
 	quit <- true
 	<-quit
 	if d.checkAPICompatibility(s.Client, err) != nil {
@@ -65,18 +65,17 @@ func (d *DryccCmd) ServicesAdd(appID, procfileType string, ports string, protoco
 	return nil
 }
 
-// ServicesRemove removes a service for procfileType registered with an app.
-func (d *DryccCmd) ServicesRemove(appID, procfileType string, protocol string, port int) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+// ServicesRemove removes a service for Ptype registered with an app.
+func (d *DryccCmd) ServicesRemove(appID, ptype string, protocol string, port int) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
 
-	d.Printf("Removing %s from %s... ", procfileType, appID)
+	d.Printf("Removing %s from %s... ", ptype, appID)
 
 	quit := progress(d.WOut)
-	err = services.Delete(s.Client, appID, procfileType, protocol, port)
+	err = services.Delete(s.Client, appID, ptype, protocol, port)
 	quit <- true
 	<-quit
 	if d.checkAPICompatibility(s.Client, err) != nil {
diff --git a/internal/commands/tags.go b/internal/commands/tags.go
new file mode 100644
index 000000000..e0a009b28
--- /dev/null
+++ b/internal/commands/tags.go
@@ -0,0 +1,138 @@
+package commands
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/controller-sdk-go/config"
+	"github.com/drycc/workflow-cli/internal/loader"
+)
+
+// TagsList lists an app's tags.
+func (d *DryccCmd) TagsList(appID, ptype string, version int) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	config, err := config.List(s.Client, appID, version)
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+	if len(config.Tags) == 0 {
+		d.Println(fmt.Sprintf("No tags found in %s app.", appID))
+		return nil
+	}
+
+	ptypes := []string{}
+	if ptype != "" {
+		ptypes = append(ptypes, ptype)
+	} else {
+		for ptype := range config.Tags {
+			ptypes = append(ptypes, ptype)
+		}
+	}
+
+	table := d.getDefaultFormatTable([]string{"PTYPE", "KEY", "VALUE"})
+	for _, ptype := range sortPtypes(ptypes) {
+		if tags, ok := config.Tags[ptype]; ok {
+			for _, key := range *sortKeys(tags) {
+				table.Append([]string{
+					ptype,
+					key,
+					fmt.Sprintf("%v", config.Tags[ptype][key]),
+				})
+			}
+		}
+	}
+	table.Render()
+
+	return nil
+}
+
+// TagsSet sets an app's tags.
+func (d *DryccCmd) TagsSet(appID, ptype string, tags []string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	tagsMap, err := parseTags(tags)
+	if err != nil {
+		return err
+	}
+
+	d.Print("Applying tags... ")
+
+	quit := progress(d.WOut)
+	configObj := api.Config{Tags: make(map[string]api.ConfigTags)}
+	configObj.Tags[ptype] = tagsMap
+
+	_, err = config.Set(s.Client, appID, configObj, true)
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	d.Print("done\n\n")
+
+	return d.TagsList(appID, ptype, -1)
+}
+
+// TagsUnset removes an app's tags.
+func (d *DryccCmd) TagsUnset(appID, ptype string, tags []string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	d.Print("Applying tags... ")
+
+	quit := progress(d.WOut)
+
+	configObj := api.Config{Tags: make(map[string]api.ConfigTags)}
+	configTags := make(api.ConfigTags)
+	for _, tag := range tags {
+		configTags[tag] = nil
+	}
+	configObj.Tags[ptype] = configTags
+
+	_, err = config.Set(s.Client, appID, configObj, true)
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	d.Print("done\n\n")
+
+	return d.TagsList(appID, ptype, -1)
+}
+
+func parseTags(tags []string) (map[string]any, error) {
+	tagMap := make(map[string]any)
+
+	for _, tag := range tags {
+		key, value, err := parseTag(tag)
+		if err != nil {
+			return nil, err
+		}
+
+		tagMap[key] = value
+	}
+
+	return tagMap, nil
+}
+
+func parseTag(tag string) (string, string, error) {
+	parts := strings.Split(tag, "=")
+
+	if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
+		return "", "", fmt.Errorf(`%s is invalid, Must be in format key=value
+Examples: rack=1 evironment=production`, tag)
+	}
+
+	return parts[0], parts[1], nil
+}
diff --git a/cmd/tags_test.go b/internal/commands/tags_test.go
similarity index 79%
rename from cmd/tags_test.go
rename to internal/commands/tags_test.go
index 541618d9b..748d2b147 100644
--- a/cmd/tags_test.go
+++ b/internal/commands/tags_test.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"bytes"
@@ -49,7 +49,7 @@ Examples: rack=1 evironment=production`},
 
 type parseTagsCase struct {
 	Input         []string
-	ExpectedMap   map[string]interface{}
+	ExpectedMap   map[string]any
 	ExpectedError bool
 	ExpectedMsg   string
 }
@@ -58,7 +58,7 @@ func TestParseTags(t *testing.T) {
 	t.Parallel()
 
 	cases := []parseTagsCase{
-		{[]string{"foo=bar", "true=false"}, map[string]interface{}{"foo": "bar", "true": "false"}, false, ""},
+		{[]string{"foo=bar", "true=false"}, map[string]any{"foo": "bar", "true": "false"}, false, ""},
 		{[]string{"foo=", "true=false"}, nil, true, `foo= is invalid, Must be in format key=value
 Examples: rack=1 evironment=production`},
 	}
@@ -87,12 +87,14 @@ func TestTagsList(t *testing.T) {
 		fmt.Fprintf(w, `{
 			"owner": "jkirk",
 			"app": "enterprise",
-			"values": {},
+			"values": [],
 			"memory": {},
 			"cpu": {},
 			"tags": {
-				"warp": "8",
-				"ncc": "1701"
+				"web": {
+					"warp": "8",
+					"ncc": "1701"
+				}
 			},
 			"registry": {},
 			"created": "2014-01-01T00:00:00UTC",
@@ -104,12 +106,12 @@ func TestTagsList(t *testing.T) {
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
-	err = cmdr.TagsList("enterprise")
+	err = cmdr.TagsList("enterprise", "web", -1)
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `PTYPE    TAG  
-ncc      1701    
-warp     8       
-`, "output")
+	testutil.AssertOutput(t, b.String(), `PTYPE    KEY     VALUE
+web      ncc     1701
+web      warp    8
+`)
 }
 
 func TestTagsSet(t *testing.T) {
@@ -124,8 +126,8 @@ func TestTagsSet(t *testing.T) {
 		testutil.SetHeaders(w)
 		if r.Method == "POST" {
 			testutil.AssertBody(t, api.Config{
-				Tags: map[string]interface{}{
-					"true": "false",
+				Tags: map[string]api.ConfigTags{
+					"web": {"true": "false"},
 				},
 			}, r)
 		}
@@ -133,11 +135,13 @@ func TestTagsSet(t *testing.T) {
 		fmt.Fprintf(w, `{
 			"owner": "jkirk",
 			"app": "foo",
-			"values": {},
+			"values": [],
 			"memory": {},
 			"cpu": {},
 			"tags": {
-				"true": "false"
+				"web": {
+					"true": "false"
+				}
 			},
 			"registry": {},
 			"created": "2014-01-01T00:00:00UTC",
@@ -149,14 +153,14 @@ func TestTagsSet(t *testing.T) {
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
-	err = cmdr.TagsSet("foo", []string{"true=false"})
+	err = cmdr.TagsSet("foo", "web", []string{"true=false"})
 	assert.NoError(t, err)
 
-	assert.Equal(t, testutil.StripProgress(b.String()), `Applying tags... done
+	testutil.AssertOutput(t, testutil.StripProgress(b.String()), `Applying tags... done
 
-PTYPE    TAG   
-true     false    
-`, "output")
+PTYPE    KEY     VALUE
+web      true    false
+`)
 }
 
 func TestTagsUnset(t *testing.T) {
@@ -171,8 +175,8 @@ func TestTagsUnset(t *testing.T) {
 		testutil.SetHeaders(w)
 		if r.Method == "POST" {
 			testutil.AssertBody(t, api.Config{
-				Tags: map[string]interface{}{
-					"ncc": nil,
+				Tags: map[string]api.ConfigTags{
+					"web": {"ncc": nil},
 				},
 			}, r)
 		}
@@ -180,11 +184,13 @@ func TestTagsUnset(t *testing.T) {
 		fmt.Fprintf(w, `{
 			"owner": "jkirk",
 			"app": "foo",
-			"values": {},
+			"values": [],
 			"memory": {},
 			"cpu": {},
 			"tags": {
-				"warp": 8
+				"web": {
+					"warp": 8
+				}
 			},
 			"registry": {},
 			"created": "2014-01-01T00:00:00UTC",
@@ -196,12 +202,12 @@ func TestTagsUnset(t *testing.T) {
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
-	err = cmdr.TagsUnset("foo", []string{"ncc"})
+	err = cmdr.TagsUnset("foo", "web", []string{"ncc"})
 	assert.NoError(t, err)
 
-	assert.Equal(t, testutil.StripProgress(b.String()), `Applying tags... done
+	testutil.AssertOutput(t, testutil.StripProgress(b.String()), `Applying tags... done
 
-PTYPE    TAG 
-warp     8      
-`, "output")
+PTYPE    KEY     VALUE
+web      warp    8
+`)
 }
diff --git a/cmd/timeouts.go b/internal/commands/timeouts.go
similarity index 75%
rename from cmd/timeouts.go
rename to internal/commands/timeouts.go
index 6925c8998..9fb9b81df 100644
--- a/cmd/timeouts.go
+++ b/internal/commands/timeouts.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"fmt"
@@ -6,17 +6,17 @@ import (
 
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/config"
+	"github.com/drycc/workflow-cli/internal/loader"
 )
 
 // TimeoutsList lists an app's timeouts.
-func (d *DryccCmd) TimeoutsList(appID string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+func (d *DryccCmd) TimeoutsList(appID string, version int) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
 
-	config, err := config.List(s.Client, appID)
+	config, err := config.List(s.Client, appID, version)
 	if d.checkAPICompatibility(s.Client, err) != nil {
 		return err
 	}
@@ -39,8 +39,7 @@ func (d *DryccCmd) TimeoutsList(appID string) error {
 
 // TimeoutsSet sets an app's timeouts.
 func (d *DryccCmd) TimeoutsSet(appID string, timeouts []string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -57,7 +56,7 @@ func (d *DryccCmd) TimeoutsSet(appID string, timeouts []string) error {
 
 	configObj.Timeout = timeoutsMap
 
-	_, err = config.Set(s.Client, appID, configObj)
+	_, err = config.Set(s.Client, appID, configObj, true)
 	quit <- true
 	<-quit
 	if d.checkAPICompatibility(s.Client, err) != nil {
@@ -66,13 +65,12 @@ func (d *DryccCmd) TimeoutsSet(appID string, timeouts []string) error {
 
 	d.Print("done\n\n")
 
-	return d.TimeoutsList(appID)
+	return d.TimeoutsList(appID, -1)
 }
 
 // TimeoutsUnset removes an app's timeouts.
 func (d *DryccCmd) TimeoutsUnset(appID string, timeouts []string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -83,7 +81,7 @@ func (d *DryccCmd) TimeoutsUnset(appID string, timeouts []string) error {
 
 	configObj := api.Config{}
 
-	valuesMap := make(map[string]interface{})
+	valuesMap := make(map[string]any)
 
 	for _, timeout := range timeouts {
 		valuesMap[timeout] = nil
@@ -91,7 +89,7 @@ func (d *DryccCmd) TimeoutsUnset(appID string, timeouts []string) error {
 
 	configObj.Timeout = valuesMap
 
-	_, err = config.Set(s.Client, appID, configObj)
+	_, err = config.Set(s.Client, appID, configObj, true)
 	quit <- true
 	<-quit
 	if d.checkAPICompatibility(s.Client, err) != nil {
@@ -100,15 +98,14 @@ func (d *DryccCmd) TimeoutsUnset(appID string, timeouts []string) error {
 
 	d.Print("done\n\n")
 
-	return d.TimeoutsList(appID)
+	return d.TimeoutsList(appID, -1)
 }
 
-func parseTimeouts(timeouts []string) (map[string]interface{}, error) {
-	timeoutsMap := make(map[string]interface{})
+func parseTimeouts(timeouts []string) (map[string]any, error) {
+	timeoutsMap := make(map[string]any)
 
 	for _, timeout := range timeouts {
 		key, value, err := parseTimeout(timeout)
-
 		if err != nil {
 			return nil, err
 		}
diff --git a/cmd/timeouts_test.go b/internal/commands/timeouts_test.go
similarity index 82%
rename from cmd/timeouts_test.go
rename to internal/commands/timeouts_test.go
index f22ba560c..e86ce730e 100644
--- a/cmd/timeouts_test.go
+++ b/internal/commands/timeouts_test.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"bytes"
@@ -22,7 +22,7 @@ type parseTimeoutCase struct {
 func TestParseTimeout(t *testing.T) {
 	t.Parallel()
 
-	var errorHint = ` doesn't fit format type=#
+	errorHint := ` doesn't fit format type=#
 Examples: web=30 worker=300`
 
 	cases := []parseTimeoutCase{
@@ -47,7 +47,7 @@ Examples: web=30 worker=300`
 
 type parseTimeoutsCase struct {
 	Input         []string
-	ExpectedMap   map[string]interface{}
+	ExpectedMap   map[string]any
 	ExpectedError bool
 	ExpectedMsg   string
 }
@@ -56,7 +56,7 @@ func TestTimeoutTags(t *testing.T) {
 	t.Parallel()
 
 	cases := []parseTimeoutsCase{
-		{[]string{"web=10", "worker=20"}, map[string]interface{}{"web": "10", "worker": "20"}, false, ""},
+		{[]string{"web=10", "worker=20"}, map[string]any{"web": "10", "worker": "20"}, false, ""},
 		{[]string{"foo=", "web=10"}, nil, true, `foo= doesn't fit format type=#
 Examples: web=30 worker=300`},
 	}
@@ -85,7 +85,7 @@ func TestTimeoutsList(t *testing.T) {
 		fmt.Fprintf(w, `{
       "owner": "jkirk",
       "app": "enterprise",
-      "values": {},
+      "values": [],
       "memory": {},
       "cpu": {},
       "tags": {},
@@ -103,19 +103,19 @@ func TestTimeoutsList(t *testing.T) {
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
-	err = cmdr.TimeoutsList("enterprise")
+	err = cmdr.TimeoutsList("enterprise", -1)
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `PTYPE     TIMEOUT 
-web       10         
-worker    20         
-`, "output")
+	testutil.AssertOutput(t, b.String(), `PTYPE     TIMEOUT
+web       10
+worker    20
+`)
 
 	server.Mux.HandleFunc("/v2/apps/franklin/config/", func(w http.ResponseWriter, _ *http.Request) {
 		testutil.SetHeaders(w)
 		fmt.Fprintf(w, `{
       "owner": "bedison",
       "app": "franklin",
-      "values": {},
+      "values": [],
       "memory": {},
       "cpu": {},
       "tags": {},
@@ -127,7 +127,7 @@ worker    20
 	})
 	b.Reset()
 
-	err = cmdr.TimeoutsList("franklin")
+	err = cmdr.TimeoutsList("franklin", -1)
 	assert.NoError(t, err)
 	assert.Equal(t, b.String(), `Default (30 sec) or controlled by drycc controller.
 `, "output")
@@ -145,7 +145,7 @@ func TestTimeoutsSet(t *testing.T) {
 		testutil.SetHeaders(w)
 		if r.Method == "POST" {
 			testutil.AssertBody(t, api.Config{
-				Timeout: map[string]interface{}{
+				Timeout: map[string]any{
 					"web": "10",
 				},
 			}, r)
@@ -154,7 +154,7 @@ func TestTimeoutsSet(t *testing.T) {
 		fmt.Fprintf(w, `{
       "owner": "jkirk",
       "app": "foo",
-      "values": {},
+      "values": [],
       "memory": {},
       "cpu": {},
       "termination_grace_period": {
@@ -174,17 +174,17 @@ func TestTimeoutsSet(t *testing.T) {
 	err = cmdr.TimeoutsSet("foo", []string{"web=10"})
 	assert.NoError(t, err)
 
-	assert.Equal(t, testutil.StripProgress(b.String()), `Applying timeouts... done
+	testutil.AssertOutput(t, testutil.StripProgress(b.String()), `Applying timeouts... done
 
-PTYPE    TIMEOUT 
-web      10         
-`, "output")
+PTYPE    TIMEOUT
+web      10
+`)
 
 	server.Mux.HandleFunc("/v2/apps/franklin/config/", func(w http.ResponseWriter, r *http.Request) {
 		testutil.SetHeaders(w)
 		if r.Method == "POST" {
 			testutil.AssertBody(t, api.Config{
-				Timeout: map[string]interface{}{
+				Timeout: map[string]any{
 					"web": "10",
 				},
 			}, r)
@@ -193,7 +193,7 @@ web      10
 		fmt.Fprintf(w, `{
       "owner": "bedison",
       "app": "franklin",
-      "values": {},
+      "values": [],
       "memory": {},
       "cpu": {},
       "termination_grace_period": {
@@ -211,18 +211,18 @@ web      10
 	err = cmdr.TimeoutsSet("franklin", []string{"web=10"})
 	assert.NoError(t, err)
 
-	assert.Equal(t, testutil.StripProgress(b.String()), `Applying timeouts... done
+	testutil.AssertOutput(t, testutil.StripProgress(b.String()), `Applying timeouts... done
 
-PTYPE    TIMEOUT 
-web      10         
-`, "output")
+PTYPE    TIMEOUT
+web      10
+`)
 
 	// with requests/timeout parameter
 	server.Mux.HandleFunc("/v2/apps/jim/config/", func(w http.ResponseWriter, r *http.Request) {
 		testutil.SetHeaders(w)
 		if r.Method == "POST" {
 			testutil.AssertBody(t, api.Config{
-				Timeout: map[string]interface{}{
+				Timeout: map[string]any{
 					"web":    "10",
 					"worker": "100",
 					"db":     "300",
@@ -233,7 +233,7 @@ web      10
 		fmt.Fprintf(w, `{
       "owner": "foo",
       "app": "jim",
-      "values": {},
+      "values": [],
       "memory": {},
       "cpu": {},
       "termination_grace_period": {
@@ -253,14 +253,13 @@ web      10
 	err = cmdr.TimeoutsSet("jim", []string{"web=10", "worker=100", "db=300"})
 	assert.NoError(t, err)
 
-	assert.Equal(t, testutil.StripProgress(b.String()), `Applying timeouts... done
-
-PTYPE     TIMEOUT 
-db        300        
-web       10         
-worker    100        
-`, "output")
+	testutil.AssertOutput(t, testutil.StripProgress(b.String()), `Applying timeouts... done
 
+PTYPE     TIMEOUT
+db        300
+web       10
+worker    100
+`)
 }
 
 func TestTimeoutsUnset(t *testing.T) {
@@ -275,7 +274,7 @@ func TestTimeoutsUnset(t *testing.T) {
 		testutil.SetHeaders(w)
 		if r.Method == "POST" {
 			testutil.AssertBody(t, api.Config{
-				Timeout: map[string]interface{}{
+				Timeout: map[string]any{
 					"web": nil,
 				},
 			}, r)
@@ -284,7 +283,7 @@ func TestTimeoutsUnset(t *testing.T) {
 		fmt.Fprintf(w, `{
       "owner": "jkirk",
       "app": "foo",
-      "values": {},
+      "values": [],
       "memory": {},
       "cpu": {},
       "termination_grace_period": {
@@ -304,17 +303,17 @@ func TestTimeoutsUnset(t *testing.T) {
 	err = cmdr.TimeoutsUnset("foo", []string{"web"})
 	assert.NoError(t, err)
 
-	assert.Equal(t, testutil.StripProgress(b.String()), `Applying timeouts... done
+	testutil.AssertOutput(t, testutil.StripProgress(b.String()), `Applying timeouts... done
 
-PTYPE    TIMEOUT 
-web      10         
-`, "output")
+PTYPE    TIMEOUT
+web      10
+`)
 
 	server.Mux.HandleFunc("/v2/apps/franklin/config/", func(w http.ResponseWriter, r *http.Request) {
 		testutil.SetHeaders(w)
 		if r.Method == "POST" {
 			testutil.AssertBody(t, api.Config{
-				Timeout: map[string]interface{}{
+				Timeout: map[string]any{
 					"web": nil,
 				},
 			}, r)
@@ -323,7 +322,7 @@ web      10
 		fmt.Fprintf(w, `{
       "owner": "bedison",
       "app": "franklin",
-      "values": {},
+      "values": [],
       "memory": {},
       "cpu": {},
       "termination_grace_period": {
@@ -341,9 +340,9 @@ web      10
 	err = cmdr.TimeoutsUnset("franklin", []string{"web"})
 	assert.NoError(t, err)
 
-	assert.Equal(t, testutil.StripProgress(b.String()), `Applying timeouts... done
+	testutil.AssertOutput(t, testutil.StripProgress(b.String()), `Applying timeouts... done
 
-PTYPE    TIMEOUT 
-web      10         
-`, "output")
+PTYPE    TIMEOUT
+web      10
+`)
 }
diff --git a/cmd/tls.go b/internal/commands/tls.go
similarity index 89%
rename from cmd/tls.go
rename to internal/commands/tls.go
index 125be9cd2..cc08a2988 100644
--- a/cmd/tls.go
+++ b/internal/commands/tls.go
@@ -1,15 +1,15 @@
-package cmd
+package commands
 
 import (
 	"fmt"
 
 	"github.com/drycc/controller-sdk-go/tls"
+	"github.com/drycc/workflow-cli/internal/loader"
 )
 
 // TLSInfo prints info about the TLS settings for the given app.
 func (d *DryccCmd) TLSInfo(appID string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -20,7 +20,6 @@ func (d *DryccCmd) TLSInfo(appID string) error {
 	}
 	table := d.getDefaultFormatTable([]string{})
 	table.Append([]string{"UUID:", tls.UUID})
-	table.Append([]string{"Owner:", tls.Owner})
 	table.Append([]string{"CertsAuto:", fmt.Sprintf("%v", tls.CertsAutoEnabled != nil && *(tls.CertsAutoEnabled))})
 	table.Append([]string{"HTTPSEnforced:", fmt.Sprintf("%v", tls.HTTPSEnforced != nil && *(tls.HTTPSEnforced))})
 	// Issuer
@@ -46,8 +45,7 @@ func (d *DryccCmd) TLSInfo(appID string) error {
 
 // TLSForceEnable enables the router to enforce https-only requests to the application.
 func (d *DryccCmd) TLSForceEnable(appID string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -68,8 +66,7 @@ func (d *DryccCmd) TLSForceEnable(appID string) error {
 
 // TLSForceDisable disables the router to enforce https-only requests to the application.
 func (d *DryccCmd) TLSForceDisable(appID string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -90,8 +87,7 @@ func (d *DryccCmd) TLSForceDisable(appID string) error {
 
 // TLSAutoEnable enables certs-auto requests to the application.
 func (d *DryccCmd) TLSAutoEnable(appID string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -112,8 +108,7 @@ func (d *DryccCmd) TLSAutoEnable(appID string) error {
 
 // TLSAutoDisable disables certs-auto requests to the application.
 func (d *DryccCmd) TLSAutoDisable(appID string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -134,8 +129,7 @@ func (d *DryccCmd) TLSAutoDisable(appID string) error {
 
 // TLSAutoIssuer add issuer requests to the application.
 func (d *DryccCmd) TLSAutoIssuer(appID string, email string, server string, keyID string, keySecret string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/tls_test.go b/internal/commands/tls_test.go
similarity index 98%
rename from cmd/tls_test.go
rename to internal/commands/tls_test.go
index eecc393cd..5a20c3e34 100644
--- a/cmd/tls_test.go
+++ b/internal/commands/tls_test.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"bytes"
@@ -44,7 +44,6 @@ func TestTLSInfo(t *testing.T) {
 	err = cmdr.TLSInfo("numenor")
 	assert.NoError(t, err)
 	assert.Equal(t, b.String(), `UUID:             c4aed81c-d1ca-4ff1-ab89-d2151264e1a3    
-Owner:            nazgul                                  
 CertsAuto:        true                                    
 HTTPSEnforced:    true                                    
 Issuer:                                                   
diff --git a/cmd/token.go b/internal/commands/token.go
similarity index 91%
rename from cmd/token.go
rename to internal/commands/token.go
index da115c1f4..db8b408d3 100644
--- a/cmd/token.go
+++ b/internal/commands/token.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"errors"
@@ -12,12 +12,12 @@ import (
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/auth"
 	"github.com/drycc/controller-sdk-go/tokens"
-	"github.com/drycc/workflow-cli/settings"
+	"github.com/drycc/workflow-cli/pkg/settings"
 )
 
+// TokensList lists authentication tokens.
 func (d *DryccCmd) TokensList(results int) error {
 	s, err := settings.Load(d.ConfigFile)
-
 	if err != nil {
 		return err
 	}
@@ -37,18 +37,18 @@ func (d *DryccCmd) TokensList(results int) error {
 			token.Owner,
 			safeGetString(token.Alias),
 			token.Key,
-			token.Created,
-			token.Updated,
+			d.formatTime(token.Created),
+			d.formatTime(token.Updated),
 		})
 	}
 	table.Render()
 	return nil
 }
 
+// TokensAdd creates a new authentication token.
 func (d *DryccCmd) TokensAdd(c *drycc.Client, username, password, alias, confirm string, render bool) (*api.AuthTokenResponse, error) {
 	if c == nil {
 		s, err := settings.Load(d.ConfigFile)
-
 		if err != nil {
 			return nil, err
 		}
@@ -101,9 +101,9 @@ func (d *DryccCmd) TokensAdd(c *drycc.Client, username, password, alias, confirm
 	return token, err
 }
 
+// TokensRemove deletes an authentication token.
 func (d *DryccCmd) TokensRemove(id, confirm string) error {
 	s, err := settings.Load(d.ConfigFile)
-
 	if err != nil {
 		return err
 	}
@@ -132,7 +132,7 @@ func (d *DryccCmd) TokensRemove(id, confirm string) error {
 }
 
 func (d *DryccCmd) openBrower(URL string) error {
-	var commands = map[string]string{
+	commands := map[string]string{
 		"windows": "start",
 		"darwin":  "open",
 		"linux":   "xdg-open",
diff --git a/cmd/token_test.go b/internal/commands/token_test.go
similarity index 96%
rename from cmd/token_test.go
rename to internal/commands/token_test.go
index 71992f60e..ab38c853e 100644
--- a/cmd/token_test.go
+++ b/internal/commands/token_test.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"bytes"
@@ -6,9 +6,8 @@ import (
 	"net/http"
 	"testing"
 
-	"github.com/stretchr/testify/assert"
-
 	"github.com/drycc/workflow-cli/pkg/testutil"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestTokensList(t *testing.T) {
@@ -67,7 +66,7 @@ func TestTokenDelete(t *testing.T) {
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
-	server.Mux.HandleFunc("/v2/tokens/f71e3b18-e702-499e-bd7f-8fb0a66d7b12/", func(w http.ResponseWriter, r *http.Request) {
+	server.Mux.HandleFunc("/v2/tokens/f71e3b18-e702-499e-bd7f-8fb0a66d7b12/", func(w http.ResponseWriter, _ *http.Request) {
 		testutil.SetHeaders(w)
 		w.WriteHeader(204)
 	})
diff --git a/internal/commands/update.go b/internal/commands/update.go
new file mode 100644
index 000000000..adc52b1ac
--- /dev/null
+++ b/internal/commands/update.go
@@ -0,0 +1,71 @@
+package commands
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"runtime"
+	"strings"
+
+	"github.com/drycc/workflow-cli/version"
+	"github.com/minio/selfupdate"
+)
+
+const workflowCliURL = "https://www.drycc.cc/workflow-cli.txt"
+
+func (d *DryccCmd) latestVersion() (string, string, error) {
+	quit := progress(d.WOut)
+	resp, err := http.Get(workflowCliURL)
+	quit <- true
+	<-quit
+	if err != nil {
+		return "", "", err
+	}
+
+	defer resp.Body.Close()
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return "", "", err
+	}
+	prefix := "drycc-"
+	suffix := fmt.Sprintf("-%s-%s", runtime.GOOS, runtime.GOARCH)
+	for _, url := range strings.Split(string(body), "\n") {
+		if strings.HasSuffix(url, suffix) {
+			names := strings.Split(url, "/")
+			version := strings.ReplaceAll(strings.ReplaceAll(names[len(names)-1], suffix, ""), prefix, "")
+			return version, url, nil
+		}
+	}
+	return "", "", fmt.Errorf("unable to obtain version: %s, %s", runtime.GOOS, runtime.GOARCH)
+}
+
+// Update workflow-cli to latest release
+func (d *DryccCmd) Update(dryRun bool) error {
+	latestVersion, downloadURL, err := d.latestVersion()
+	if err != nil {
+		return err
+	}
+	if latestVersion != version.Version {
+		d.Printf("Update workflow cli from %s to %s... ", version.Version, latestVersion)
+		if dryRun {
+			d.Println("skip")
+		} else {
+			resp, err := http.Get(downloadURL)
+			if err != nil {
+				return err
+			}
+			defer resp.Body.Close()
+			quit := progress(d.WOut)
+			err = selfupdate.Apply(resp.Body, selfupdate.Options{})
+			quit <- true
+			<-quit
+			if err != nil {
+				return err
+			}
+			d.Println("done")
+		}
+	} else {
+		d.Println("You are already running the most recent version.")
+	}
+	return nil
+}
diff --git a/cmd/utils.go b/internal/commands/utils.go
similarity index 58%
rename from cmd/utils.go
rename to internal/commands/utils.go
index c5df08c85..9b1f088f0 100644
--- a/cmd/utils.go
+++ b/internal/commands/utils.go
@@ -1,21 +1,25 @@
-package cmd
+package commands
 
 import (
+	"bytes"
 	"fmt"
 	"io"
 	"os"
 	"sort"
+	"strconv"
 	"strings"
 	"time"
 
 	drycc "github.com/drycc/controller-sdk-go"
-	"github.com/drycc/workflow-cli/pkg/git"
-	"github.com/drycc/workflow-cli/settings"
+	"github.com/drycc/controller-sdk-go/api"
 	"github.com/olekukonko/tablewriter"
+	yaml "gopkg.in/yaml.v3"
 )
 
-var defaultLimit = -1
-var defaultLines = 64
+var (
+	defaultLimit = -1
+	defaultLines = 64
+)
 
 func progress(wOut io.Writer) chan bool {
 	frames := []string{"...", "o..", ".o.", "..o"}
@@ -40,25 +44,6 @@ func progress(wOut io.Writer) chan bool {
 	return quit
 }
 
-// load loads settings file and looks up the app name
-func load(cf string, appID string) (*settings.Settings, string, error) {
-	s, err := settings.Load(cf)
-
-	if err != nil {
-		return nil, "", err
-	}
-
-	if appID == "" {
-		appID, err = git.DetectAppName(git.DefaultCmd, s.Client.ControllerURL.Host)
-
-		if err != nil {
-			return nil, "", err
-		}
-	}
-
-	return s, appID, nil
-}
-
 func drinkOfChoice() string {
 	drink := os.Getenv("DRYCC_DRINK_OF_CHOICE")
 
@@ -115,13 +100,45 @@ func (d *DryccCmd) getDefaultFormatTable(headers []string) *tablewriter.Table {
 	return table
 }
 
+// format time string to local time
+func (d *DryccCmd) formatTime(timeStr string) string {
+	t, err := time.Parse(time.RFC3339, timeStr)
+	if err != nil {
+		return timeStr
+	}
+	if d.Location != nil {
+		return t.In(d.Location).Format(time.RFC3339)
+	}
+	return t.In(time.UTC).Format(time.RFC3339)
+}
+
 // wrapString wraps s into a paragraph of lines of length lim, with minimal raggedness.
 func (d *DryccCmd) wrapString(s string) string {
 	sa, _ := tablewriter.WrapString(s, defaultLines)
-	return strings.Join(sa, "\r\n")
+	return strings.Join(sa, "\n")
 }
 
-func sortKeys(data map[string]interface{}) *[]string {
+// indentString indent s into a paragraph of lines of length lim, with minimal raggedness.
+func (d *DryccCmd) indentString(s string, indent int) string {
+	var lines []string
+	for _, line := range strings.Split(s, "\n") {
+		line = strings.TrimRight(line, "\r")
+		padding := indent + len(line)
+		lines = append(lines, fmt.Sprintf("% "+strconv.Itoa(padding)+"s", line))
+	}
+	return strings.Join(lines, "\n")
+}
+
+// toYamlString convert object to yaml string
+func (d *DryccCmd) toYamlString(v any, indent int) string {
+	buf := bytes.Buffer{}
+	encode := yaml.NewEncoder(&buf)
+	encode.SetIndent(indent)
+	encode.Encode(v)
+	return buf.String()
+}
+
+func sortKeys(data map[string]any) *[]string {
 	keys := make([]string, 0, len(data))
 	for k := range data {
 		keys = append(keys, k)
@@ -130,9 +147,38 @@ func sortKeys(data map[string]interface{}) *[]string {
 	return &keys
 }
 
+func sortConfigValues(data []api.ConfigValue) []api.ConfigValue {
+	sort.Slice(data, func(i, j int) bool {
+		return strings.ToLower(data[i].Name) < strings.ToLower(data[j].Name)
+	})
+	return data
+}
+
+func sortPtypes(ptypes []string) []string {
+	sort.Slice(ptypes, func(i, j int) bool {
+		if ptypes[i] == "web" {
+			return true
+		}
+		if ptypes[j] == "web" {
+			return false
+		}
+		return ptypes[i] < ptypes[j]
+	})
+	return ptypes
+}
+
 func safeGetString(data string) string {
 	if data == "" {
 		return "<none>"
 	}
 	return data
 }
+
+// ResponseLimit converts a limit value to the format expected by the API.
+// If limit is 0, it returns -1 to indicate no limit.
+func ResponseLimit(limit int) (int, error) {
+	if limit == 0 {
+		return -1, nil
+	}
+	return limit, nil
+}
diff --git a/cmd/utils_test.go b/internal/commands/utils_test.go
similarity index 56%
rename from cmd/utils_test.go
rename to internal/commands/utils_test.go
index 0a99c97c2..a44c8aa53 100644
--- a/cmd/utils_test.go
+++ b/internal/commands/utils_test.go
@@ -1,53 +1,14 @@
-package cmd
+package commands
 
 import (
 	"bytes"
 	"os"
-	"path/filepath"
 	"testing"
 
 	drycc "github.com/drycc/controller-sdk-go"
-	"github.com/drycc/workflow-cli/pkg/git"
-	"github.com/drycc/workflow-cli/settings"
 	"github.com/stretchr/testify/assert"
 )
 
-func TestLoad(t *testing.T) {
-	name, err := os.MkdirTemp("", "drycc-cli-unit-test-load")
-	assert.NoError(t, err)
-	defer os.RemoveAll(name)
-
-	filename := filepath.Join(name, "test.json")
-	host := "drycc.example.com"
-	client, err := drycc.New(false, host, "")
-	assert.NoError(t, err)
-
-	config := settings.Settings{
-		Username: "test",
-		Client:   client,
-	}
-
-	filename, err = config.Save(filename)
-	assert.NoError(t, err)
-
-	_, appID, err := load(filename, "test")
-	assert.NoError(t, err)
-	assert.Equal(t, appID, "test", "app")
-
-	assert.NoError(t, os.Chdir(name))
-
-	_, appID, err = load(filename, "")
-	assert.NoError(t, err)
-	assert.Equal(t, appID, filepath.Base(name), "app")
-
-	assert.NoError(t, git.Init(git.DefaultCmd))
-	assert.NoError(t, git.CreateRemote(git.DefaultCmd, host, "drycc", "testing"))
-
-	_, appID, err = load(filename, "")
-	assert.NoError(t, err)
-	assert.Equal(t, appID, "testing", "app")
-}
-
 func TestDrinkOfChoice(t *testing.T) {
 	os.Setenv("DRYCC_DRINK_OF_CHOICE", "test")
 	assert.Equal(t, drinkOfChoice(), "test", "output")
diff --git a/cmd/version.go b/internal/commands/version.go
similarity index 91%
rename from cmd/version.go
rename to internal/commands/version.go
index 0727fe7c1..6f7a570a4 100644
--- a/cmd/version.go
+++ b/internal/commands/version.go
@@ -1,8 +1,8 @@
-package cmd
+package commands
 
 import (
 	drycc "github.com/drycc/controller-sdk-go"
-	"github.com/drycc/workflow-cli/settings"
+	"github.com/drycc/workflow-cli/pkg/settings"
 	"github.com/drycc/workflow-cli/version"
 )
 
@@ -17,7 +17,6 @@ func (d *DryccCmd) Version(all bool) error {
 	d.Printf("Workflow CLI API Version:        %s\n", drycc.APIVersion)
 
 	s, err := settings.Load(d.ConfigFile)
-
 	if err != nil {
 		return err
 	}
diff --git a/cmd/version_test.go b/internal/commands/version_test.go
similarity index 98%
rename from cmd/version_test.go
rename to internal/commands/version_test.go
index 35bf13648..7522350b4 100644
--- a/cmd/version_test.go
+++ b/internal/commands/version_test.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
 
 import (
 	"bytes"
diff --git a/cmd/volumes.go b/internal/commands/volumes.go
similarity index 68%
rename from cmd/volumes.go
rename to internal/commands/volumes.go
index 7bbef0dda..08bbb58bb 100644
--- a/cmd/volumes.go
+++ b/internal/commands/volumes.go
@@ -1,18 +1,22 @@
-package cmd
+package commands
 
 import (
+	"context"
 	"fmt"
+	"os"
+	"os/signal"
 	"regexp"
+	"time"
 
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/volumes"
+	"github.com/drycc/workflow-cli/internal/loader"
 	"sigs.k8s.io/yaml"
 )
 
 // VolumesList list volumes in the application
 func (d *DryccCmd) VolumesList(appID string, results int) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -20,7 +24,6 @@ func (d *DryccCmd) VolumesList(appID string, results int) error {
 	if results == defaultLimit {
 		results = s.Limit
 	}
-	//fmt.Println("-----app-----", appID)  # debug
 	volumes, count, err := volumes.List(s.Client, appID, results)
 	if d.checkAPICompatibility(s.Client, err) != nil {
 		return err
@@ -36,8 +39,7 @@ func (d *DryccCmd) VolumesList(appID string, results int) error {
 
 // VolumesInfo get volume in the application
 func (d *DryccCmd) VolumesInfo(appID, name string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -49,7 +51,6 @@ func (d *DryccCmd) VolumesInfo(appID, name string) error {
 	table := d.getDefaultFormatTable([]string{})
 	table.Append([]string{"UUID:", volume.UUID})
 	table.Append([]string{"Name:", volume.Name})
-	table.Append([]string{"Owner:", volume.Owner})
 	table.Append([]string{"Type:", volume.Type})
 	// table append path
 	table.Append([]string{"Path:"})
@@ -65,16 +66,15 @@ func (d *DryccCmd) VolumesInfo(appID, name string) error {
 		return err
 	}
 	table.Append([]string{"", string(parameters)})
-	table.Append([]string{"Created: ", volume.Created})
-	table.Append([]string{"Updated: ", volume.Updated})
+	table.Append([]string{"Created: ", d.formatTime(volume.Created)})
+	table.Append([]string{"Updated: ", d.formatTime(volume.Updated)})
 	table.Render()
 	return nil
 }
 
 // VolumesCreate create a volume for the application
-func (d *DryccCmd) VolumesCreate(appID, name, vType, size string, parameters map[string]interface{}) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+func (d *DryccCmd) VolumesCreate(appID, name, vType, size string, parameters map[string]any) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -106,8 +106,7 @@ Examples: 2G 2g`, size)
 
 // VolumesExpand create a volume for the application
 func (d *DryccCmd) VolumesExpand(appID, name, size string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -137,8 +136,7 @@ Examples: 2G 2g`, size)
 
 // VolumesDelete delete a volume from the application
 func (d *DryccCmd) VolumesDelete(appID, name string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -157,10 +155,50 @@ func (d *DryccCmd) VolumesDelete(appID, name string) error {
 	return nil
 }
 
+// VolumesServe Serve serves an app's volume.
+func (d *DryccCmd) VolumesServe(appID, name string) error {
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+	parent, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	d.Print("Starting WebDAV file access service ")
+	quit := progress(d.WOut)
+	ctx, filer, err := volumes.Serve(parent, s.Client, appID, name)
+	quit <- true
+	<-quit
+	if err != nil {
+		return err
+	}
+
+	d.Print("\n\n")
+	table := d.getDefaultFormatTable([]string{})
+	table.Append([]string{"Endpoint:", filer["endpoint"]})
+	table.Append([]string{"Username:", filer["username"]})
+	table.Append([]string{"Password:", filer["password"]})
+	table.Render()
+	d.Print("\n")
+
+	signalChan := make(chan os.Signal, 1)
+	signal.Notify(signalChan, os.Interrupt)
+	d.Printf("WebDAV service for volume %s is running. Press Ctrl+C to stop.\n", name)
+	for {
+		select {
+		case <-signalChan:
+			return nil
+		case <-ctx.Done():
+			return nil
+		default:
+			time.Sleep(2 * time.Second)
+		}
+	}
+}
+
 // VolumesMount mount a volume to process of the application
 func (d *DryccCmd) VolumesMount(appID string, name string, volumeVars []string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -189,13 +227,12 @@ func (d *DryccCmd) VolumesMount(appID string, name string, volumeVars []string)
 
 // VolumesUnmount unmount a volume from process of the application
 func (d *DryccCmd) VolumesUnmount(appID string, name string, volumeVars []string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
 
-	valuesMap := make(map[string]interface{})
+	valuesMap := make(map[string]any)
 	for _, volumeVar := range volumeVars {
 		valuesMap[volumeVar] = nil
 	}
@@ -217,10 +254,9 @@ func (d *DryccCmd) VolumesUnmount(appID string, name string, volumeVars []string
 	return nil
 }
 
-func parseVolume(volumeVars []string) (map[string]interface{}, error) {
-	volumeMap := make(map[string]interface{})
-
-	regex := regexp.MustCompile(`^([A-z_]+[A-z0-9_]*)=([\s\S]*)$`)
+func parseVolume(volumeVars []string) (map[string]any, error) {
+	volumeMap := make(map[string]any)
+	regex := regexp.MustCompile(`^([a-z0-9]+(?:-[a-z0-9]+)*)=(\/([\w]+[\w-]*\/?)+)$`)
 	for _, volume := range volumeVars {
 		if regex.MatchString(volume) {
 			captures := regex.FindStringSubmatch(volume)
@@ -235,14 +271,14 @@ func parseVolume(volumeVars []string) (map[string]interface{}, error) {
 
 // printVolumes format volume data
 func printVolumes(d *DryccCmd, volumes api.Volumes) {
-	table := d.getDefaultFormatTable([]string{"NAME", "OWNER", "TYPE", "PTYPE", "PATH", "SIZE"})
+	table := d.getDefaultFormatTable([]string{"NAME", "TYPE", "PTYPE", "PATH", "SIZE"})
 	for _, volume := range volumes {
 		if len(volume.Path) > 0 {
 			for _, key := range *sortKeys(volume.Path) {
-				table.Append([]string{volume.Name, volume.Owner, volume.Type, key, fmt.Sprintf("%v", volume.Path[key]), volume.Size})
+				table.Append([]string{volume.Name, volume.Type, key, fmt.Sprintf("%v", volume.Path[key]), volume.Size})
 			}
 		} else {
-			table.Append([]string{volume.Name, volume.Owner, volume.Type, "", "", volume.Size})
+			table.Append([]string{volume.Name, volume.Type, "", "", volume.Size})
 		}
 	}
 	table.Render()
diff --git a/cmd/volumes_test.go b/internal/commands/volumes_test.go
similarity index 73%
rename from cmd/volumes_test.go
rename to internal/commands/volumes_test.go
index b8a78a8ee..46652e587 100644
--- a/cmd/volumes_test.go
+++ b/internal/commands/volumes_test.go
@@ -1,10 +1,12 @@
-package cmd
+package commands
 
 import (
 	"bytes"
 	"fmt"
 	"net/http"
+	"syscall"
 	"testing"
+	"time"
 
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/workflow-cli/pkg/testutil"
@@ -46,10 +48,10 @@ func TestVolumesList(t *testing.T) {
 
 	err = cmdr.VolumesList("example-go", -1)
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `NAME        OWNER    TYPE    PTYPE     PATH            SIZE 
-myvolume    test     csi     cmd       /data/cmd1      500G    
-myvolume    test     csi     cmd123    /data/cmd123    500G    
-`, "output")
+	testutil.AssertOutput(t, b.String(), `NAME        TYPE    PTYPE     PATH            SIZE
+myvolume    csi     cmd       /data/cmd1      500G
+myvolume    csi     cmd123    /data/cmd123    500G
+`)
 }
 
 func TestVolumesCreate(t *testing.T) {
@@ -70,9 +72,9 @@ func TestVolumesCreate(t *testing.T) {
 		w.Write([]byte("{}"))
 	})
 
-	err = cmdr.VolumesCreate("example-go", "myvolume", "csi", "500G", map[string]interface{}{})
+	err = cmdr.VolumesCreate("example-go", "myvolume", "csi", "500G", map[string]any{})
 	assert.NoError(t, err)
-	err = cmdr.VolumesCreate("example-go", "myvolume", "csi", "500K", map[string]interface{}{})
+	err = cmdr.VolumesCreate("example-go", "myvolume", "csi", "500K", map[string]any{})
 	expected := `500K doesn't fit format #unit
 Examples: 2G 2g`
 	assert.Equal(t, err.Error(), expected, "output")
@@ -114,23 +116,70 @@ func TestVolumesInfo(t *testing.T) {
 
 	err = cmdr.VolumesInfo("example-go", "myvolume")
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `UUID:          de1bf5b5-4a72-4f94-a10c-d2a3741cdf75    
-Name:          myvolume                                
-Owner:         test                                    
-Type:          nfs                                     
-Path:          
-               cmd: /data/cmd1                         
-               cmd123: /data/cmd123                    
-                                                       
-Parameters:    
-               nfs:                                    
-                 path: /mnt                            
-                 readOnly: true                        
-                 server: nfs.drycc.cc                  
-                                                       
-Created:       2020-08-26T00:00:00UTC                  
-Updated:       2020-08-26T00:00:00UTC                  
-`, "output")
+	testutil.AssertOutput(t, b.String(), `UUID:          de1bf5b5-4a72-4f94-a10c-d2a3741cdf75
+Name:          myvolume
+Type:          nfs
+Path:
+               cmd: /data/cmd1
+               cmd123: /data/cmd123
+
+Parameters:
+               nfs:
+                 path: /mnt
+                 readOnly: true
+                 server: nfs.drycc.cc
+
+Created:       2020-08-26T00:00:00UTC
+Updated:       2020-08-26T00:00:00UTC
+`)
+}
+
+func TestVolumesServe(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+	server.Mux.HandleFunc("/v2/apps/example-go/volumes/myvolume/filer/_/ping", func(w http.ResponseWriter, _ *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `pong`)
+	})
+	server.Mux.HandleFunc("/v2/apps/example-go/volumes/myvolume/filer/_/bind", func(w http.ResponseWriter, _ *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `{"endpoint": "/v2/apps/example-go/volumes/myvolume/filer/webdav", "username": "user", "password": "pass"}`)
+	})
+
+	// Use a channel to signal when the method has been called
+	done := make(chan error, 1)
+	go func() {
+		done <- cmdr.VolumesServe("example-go", "myvolume")
+	}()
+
+	// Give the goroutine time to start and produce output
+	time.Sleep(500 * time.Millisecond)
+
+	// Send interrupt signal to stop the blocking method
+	go func() {
+		time.Sleep(100 * time.Millisecond)
+		syscall.Kill(syscall.Getpid(), syscall.SIGINT)
+	}()
+
+	// Wait for completion with timeout
+	select {
+	case err := <-done:
+		assert.NoError(t, err)
+	case <-time.After(5 * time.Second):
+		t.Fatal("VolumesServe did not complete within timeout")
+	}
+
+	output := b.String()
+	assert.Contains(t, output, "WebDAV service for volume myvolume is running.")
+	assert.Contains(t, output, "Endpoint:")
+	assert.Contains(t, output, "Username:")
+	assert.Contains(t, output, "Password:")
 }
 
 func TestVolumesExpand(t *testing.T) {
@@ -194,7 +243,7 @@ func TestVolumesMount(t *testing.T) {
 		testutil.SetHeaders(w)
 		if r.Method == "PATCH" {
 			testutil.AssertBody(t, api.Volume{
-				Path: map[string]interface{}{
+				Path: map[string]any{
 					"cmd": "/data/cmd1",
 				},
 			}, r)
@@ -236,7 +285,7 @@ func TestVolumesUnmount(t *testing.T) {
 		testutil.SetHeaders(w)
 		if r.Method == "PATCH" {
 			testutil.AssertBody(t, api.Volume{
-				Path: map[string]interface{}{
+				Path: map[string]any{
 					"cmd": nil,
 				},
 			}, r)
diff --git a/internal/commands/workspaces.go b/internal/commands/workspaces.go
new file mode 100644
index 000000000..78872717c
--- /dev/null
+++ b/internal/commands/workspaces.go
@@ -0,0 +1,202 @@
+package commands
+
+import (
+	"fmt"
+
+	"github.com/drycc/controller-sdk-go/workspaces"
+	"github.com/drycc/controller-sdk-go/workspaces/invitations"
+	"github.com/drycc/controller-sdk-go/workspaces/members"
+	"github.com/drycc/workflow-cli/pkg/settings"
+)
+
+// WorkspacesList lists workspaces visible to the current user.
+func (d *DryccCmd) WorkspacesList(results int) error {
+	s, err := settings.Load(d.ConfigFile)
+	if err != nil {
+		return err
+	}
+
+	if results == defaultLimit {
+		results = s.Limit
+	}
+
+	wkspaces, count, err := workspaces.List(s.Client, results)
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+	if count > 0 {
+		table := d.getDefaultFormatTable([]string{"NAME", "EMAIL", "CREATED", "UPDATED"})
+		for _, ws := range wkspaces {
+			table.Append([]string{
+				ws.Name,
+				ws.Email,
+				d.formatTime(ws.Created),
+				d.formatTime(ws.Updated),
+			})
+		}
+		table.Render()
+	} else {
+		d.Println("No workspaces found.")
+	}
+	return nil
+}
+
+// WorkspacesCreate creates a new workspace.
+func (d *DryccCmd) WorkspacesCreate(name, email string) error {
+	s, err := settings.Load(d.ConfigFile)
+	if err != nil {
+		return err
+	}
+
+	d.Print("Creating Workspace... ")
+	quit := progress(d.WOut)
+	ws, err := workspaces.Create(s.Client, name, email)
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	d.Printf("done, created %s\n", ws.Name)
+	return nil
+}
+
+// WorkspacesInfo shows detailed information about a workspace.
+func (d *DryccCmd) WorkspacesInfo(name string, results int) error {
+	s, err := settings.Load(d.ConfigFile)
+	if err != nil {
+		return err
+	}
+
+	ws, err := workspaces.Get(s.Client, name)
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	table := d.getDefaultFormatTable([]string{})
+	table.Append([]string{"Name:", ws.Name})
+	table.Append([]string{"Email:", ws.Email})
+	table.Append([]string{"Created:", d.formatTime(ws.Created)})
+	table.Append([]string{"Updated:", d.formatTime(ws.Updated)})
+
+	if results == defaultLimit {
+		results = s.Limit
+	}
+
+	// print members
+	mems, _, err := members.List(s.Client, name, results)
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+	if len(mems) > 0 {
+		table.Append([]string{"Members:"})
+		for index, m := range mems {
+			table.Append([]string{"", "User:", m.User})
+			table.Append([]string{"", "Email:", m.Email})
+			table.Append([]string{"", "Role:", m.Role})
+			table.Append([]string{"", "Alerts:", fmt.Sprintf("%v", m.Alerts)})
+			if len(mems) > index+1 {
+				table.Append([]string{""})
+			}
+		}
+	} else {
+		table.Append([]string{"Members:", safeGetString("")})
+	}
+
+	table.Render()
+	return nil
+}
+
+// WorkspacesDelete deletes a workspace.
+func (d *DryccCmd) WorkspacesDelete(name, confirm string) error {
+	s, err := settings.Load(d.ConfigFile)
+	if err != nil {
+		return err
+	}
+
+	if confirm == "" {
+		d.Printf(` !    WARNING: Potentially Destructive Action
+ !    This command will destroy the workspace: %s
+ !    To proceed, type "%s" or re-run this command with --confirm=%s
+
+> `, name, name, name)
+
+		fmt.Scanln(&confirm)
+	}
+
+	if confirm != name {
+		return fmt.Errorf("workspace %s does not match confirm %s, aborting", name, confirm)
+	}
+
+	d.Printf("Destroying %s... ", name)
+	quit := progress(d.WOut)
+	err = workspaces.Delete(s.Client, name)
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	d.Println("done")
+	return nil
+}
+
+// WorkspacesInvite invites a user to a workspace by email.
+func (d *DryccCmd) WorkspacesInvite(workspace, email string) error {
+	s, err := settings.Load(d.ConfigFile)
+	if err != nil {
+		return err
+	}
+
+	d.Printf("Inviting %s to %s... ", email, workspace)
+	quit := progress(d.WOut)
+	_, err = invitations.Create(s.Client, workspace, email)
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	d.Println("done")
+	return nil
+}
+
+// WorkspacesRemove removes a user from a workspace.
+func (d *DryccCmd) WorkspacesRemove(workspace, username string) error {
+	s, err := settings.Load(d.ConfigFile)
+	if err != nil {
+		return err
+	}
+
+	d.Printf("Removing %s from %s... ", username, workspace)
+	quit := progress(d.WOut)
+	err = members.Delete(s.Client, workspace, username)
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	d.Println("done")
+	return nil
+}
+
+// WorkspacesUpdate updates a workspace member's role and/or alerts setting.
+func (d *DryccCmd) WorkspacesUpdate(workspace, username, role string, alerts *bool) error {
+	s, err := settings.Load(d.ConfigFile)
+	if err != nil {
+		return err
+	}
+
+	d.Printf("Updating %s in %s... ", username, workspace)
+	quit := progress(d.WOut)
+	member, err := members.Update(s.Client, workspace, username, role, alerts)
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	d.Printf("done, %s is now role=%s alerts=%v in %s\n", member.User, member.Role, member.Alerts, workspace)
+	return nil
+}
diff --git a/internal/completion/completion.go b/internal/completion/completion.go
new file mode 100644
index 000000000..b915ddf62
--- /dev/null
+++ b/internal/completion/completion.go
@@ -0,0 +1,1005 @@
+// Package completion provides types and functions for command completion in the CLI
+package completion
+
+import (
+	"fmt"
+	"slices"
+	"strconv"
+	"strings"
+
+	"github.com/drycc/controller-sdk-go/apps"
+	"github.com/drycc/controller-sdk-go/appsettings"
+	"github.com/drycc/controller-sdk-go/certs"
+	"github.com/drycc/controller-sdk-go/config"
+	"github.com/drycc/controller-sdk-go/domains"
+	"github.com/drycc/controller-sdk-go/gateways"
+	"github.com/drycc/controller-sdk-go/keys"
+	"github.com/drycc/controller-sdk-go/limits"
+	"github.com/drycc/controller-sdk-go/ps"
+	"github.com/drycc/controller-sdk-go/pts"
+	"github.com/drycc/controller-sdk-go/releases"
+	"github.com/drycc/controller-sdk-go/resources"
+	"github.com/drycc/controller-sdk-go/routes"
+	"github.com/drycc/controller-sdk-go/services"
+	"github.com/drycc/controller-sdk-go/tokens"
+	"github.com/drycc/controller-sdk-go/volumes"
+	"github.com/drycc/controller-sdk-go/workspaces"
+	"github.com/drycc/controller-sdk-go/workspaces/members"
+	"github.com/drycc/workflow-cli/internal/loader"
+	"github.com/drycc/workflow-cli/pkg/settings"
+	"github.com/spf13/cobra"
+)
+
+// Completion is an interface for command completion functions
+type Completion interface {
+	CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)
+}
+
+// AppCompletion provides completion for application names
+type AppCompletion struct {
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of application names for completion
+func (c *AppCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if s, err := settings.Load(*c.ConfigFile); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+		if apps, _, err := apps.List(s.Client, -1); err == nil {
+			var results []string
+			for _, app := range apps {
+				if strings.HasPrefix(app.ID, toComplete) {
+					results = append(results, app.ID)
+				}
+			}
+			return results, cobra.ShellCompDirectiveNoFileComp
+		}
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// CertCompletion provides completion for certificate names
+type CertCompletion struct {
+	AppID      *string
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of certificate names for completion
+func (c *CertCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if appID, s, err := loader.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+		if certs, _, err := certs.List(s.Client, appID, -1); err == nil {
+			var results []string
+			for _, cert := range certs {
+				if strings.HasPrefix(cert.Name, toComplete) {
+					results = append(results, cert.Name)
+				}
+			}
+			return results, cobra.ShellCompDirectiveNoFileComp
+		}
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// CertDomainTachCompletion provides completion for certificate domain names
+type CertDomainTachCompletion struct {
+	AppID      *string
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of certificate domain names for completion
+func (c *CertDomainTachCompletion) CompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if len(args) == 0 {
+		certCompletion := CertCompletion{AppID: c.AppID, ConfigFile: c.ConfigFile}
+		return certCompletion.CompletionFunc(cmd, args, toComplete)
+	}
+	domainCompletion := DomainCompletion{AppID: c.AppID, ArgsLen: 1, ConfigFile: c.ConfigFile}
+	return domainCompletion.CompletionFunc(cmd, args, toComplete)
+}
+
+// ConfigPtsGroupArgsCompletion provides completion for config pts group arguments
+type ConfigPtsGroupArgsCompletion struct {
+	AppID      *string
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of config pts group arguments for completion
+func (c *ConfigPtsGroupArgsCompletion) CompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if len(args) == 0 {
+		ptsCompletion := PtsCompletion{AppID: c.AppID, ConfigFile: c.ConfigFile}
+		return ptsCompletion.CompletionFunc(cmd, args, toComplete)
+	}
+	groups := args[1:]
+	if appID, s, err := loader.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil {
+		if config, err := config.List(s.Client, appID, -1); err == nil {
+			var results []string
+			for _, value := range config.Values {
+				if value.Group != "" && strings.HasPrefix(value.Group, toComplete) {
+					if !slices.Contains(groups, value.Group) && !slices.Contains(results, value.Group) {
+						results = append(results, value.Group)
+					}
+				}
+			}
+			return results, cobra.ShellCompDirectiveNoFileComp
+		}
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// ConfigGroupCompletion provides completion for config group names
+type ConfigGroupCompletion struct {
+	AppID      *string
+	ConfigFile *string
+	ArgsLen    int
+}
+
+// CompletionFunc returns a list of config group names for completion
+func (c *ConfigGroupCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if appID, s, err := loader.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+		if config, err := config.List(s.Client, appID, -1); err == nil {
+			var results []string
+			for _, value := range config.Values {
+				if value.Group != "" && strings.HasPrefix(value.Group, toComplete) {
+					results = append(results, value.Group)
+				}
+			}
+			return results, cobra.ShellCompDirectiveNoFileComp
+		}
+	}
+
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// DomainCompletion provides completion for domain names
+type DomainCompletion struct {
+	AppID      *string
+	ConfigFile *string
+	ArgsLen    int
+}
+
+// CompletionFunc returns a list of domain names for completion
+func (c *DomainCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if appID, s, err := loader.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+		if domains, _, err := domains.List(s.Client, appID, -1); err == nil {
+			var results []string
+			for _, domain := range domains {
+				if strings.HasPrefix(domain.Domain, toComplete) {
+					results = append(results, domain.Domain)
+				}
+			}
+			return results, cobra.ShellCompDirectiveNoFileComp
+		}
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// GatewayProtocolCompletion provides completion for gateway protocols
+type GatewayProtocolCompletion struct {
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of gateway protocols for completion
+func (c *GatewayProtocolCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	protocols := []string{"TCP", "UDP", "TLS", "HTTP", "HTTPS"}
+	if c.ArgsLen < 0 || len(args) == c.ArgsLen {
+
+		var results []string
+		for _, protocol := range protocols {
+			if strings.HasPrefix(protocol, toComplete) {
+				results = append(results, protocol)
+			}
+		}
+		return results, cobra.ShellCompDirectiveNoFileComp
+
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// GatewayNameCompletion provides completion for gateway names
+type GatewayNameCompletion struct {
+	AppID      *string
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of gateway names for completion
+func (c *GatewayNameCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	var results []string
+	if appID, s, err := loader.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+		if gateways, _, err := gateways.List(s.Client, appID, -1); err == nil {
+			for _, gateway := range gateways {
+				if strings.HasPrefix(gateway.Name, toComplete) {
+					results = append(results, gateway.Name)
+				}
+			}
+		}
+	}
+	return results, cobra.ShellCompDirectiveNoFileComp
+}
+
+// LifecycleHandlerCompletion provides completion for lifecycle handler types
+type LifecycleHandlerCompletion struct {
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of lifecycle handler types for completion
+func (c *LifecycleHandlerCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	healthTypes := []string{"postStart", "preStop"}
+	if c.ArgsLen < 0 || len(args) == c.ArgsLen {
+		var results []string
+		for _, healthType := range healthTypes {
+			if strings.HasPrefix(healthType, toComplete) {
+				results = append(results, healthType)
+			}
+		}
+		return results, cobra.ShellCompDirectiveNoFileComp
+
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// LifecycleActionTypeCompletion provides completion for lifecycle action types
+type LifecycleActionTypeCompletion struct {
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of lifecycle action types for completion
+func (c *LifecycleActionTypeCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	lifecycleActionTypes := []string{"exec", "sleep", "httpGet", "tcpSocket"}
+	if c.ArgsLen < 0 || len(args) == c.ArgsLen {
+
+		var results []string
+		for _, lifecycleActionType := range lifecycleActionTypes {
+			if strings.HasPrefix(lifecycleActionType, toComplete) {
+				results = append(results, lifecycleActionType)
+			}
+		}
+		return results, cobra.ShellCompDirectiveNoFileComp
+
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// LifecycleCompletion provides completion for health checks
+type LifecycleCompletion struct {
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of health checks for completion
+func (c *LifecycleCompletion) CompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if len(args) == 0 {
+		lifecycleHandlerCompletion := LifecycleHandlerCompletion{ArgsLen: 0, ConfigFile: c.ConfigFile}
+		return lifecycleHandlerCompletion.CompletionFunc(cmd, args, toComplete)
+	}
+	lifecycleActionTypeCompletion := LifecycleActionTypeCompletion{ArgsLen: 1, ConfigFile: c.ConfigFile}
+	return lifecycleActionTypeCompletion.CompletionFunc(cmd, args, toComplete)
+}
+
+// HealthcheckProbeTypeCompletion provides completion for probe types
+type HealthcheckProbeTypeCompletion struct {
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of health check types for completion
+func (p *HealthcheckProbeTypeCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	probeTypes := []string{"startupProbe", "livenessProbe", "readinessProbe"}
+	if p.ArgsLen < 0 || len(args) == p.ArgsLen {
+
+		var results []string
+		for _, probeType := range probeTypes {
+			if strings.HasPrefix(probeType, toComplete) {
+				results = append(results, probeType)
+			}
+		}
+		return results, cobra.ShellCompDirectiveNoFileComp
+
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// HealthcheckProbeActionTypeCompletion provides completion for health check probe action types
+type HealthcheckProbeActionTypeCompletion struct {
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of health check probe action types for completion
+func (c *HealthcheckProbeActionTypeCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	probeTypes := []string{"httpGet", "exec", "tcpSocket"}
+	if c.ArgsLen < 0 || len(args) == c.ArgsLen {
+
+		var results []string
+		for _, probeType := range probeTypes {
+			if strings.HasPrefix(probeType, toComplete) {
+				results = append(results, probeType)
+			}
+		}
+		return results, cobra.ShellCompDirectiveNoFileComp
+
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// HealthCheckCompletion provides completion for health checks
+type HealthCheckCompletion struct {
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of health checks for completion
+func (c *HealthCheckCompletion) CompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if len(args) == 0 {
+		healthcheckProbeTypeCompletion := HealthcheckProbeTypeCompletion{ArgsLen: 0, ConfigFile: c.ConfigFile}
+		return healthcheckProbeTypeCompletion.CompletionFunc(cmd, args, toComplete)
+	}
+	healthcheckProbeActionTypeCompletion := HealthcheckProbeActionTypeCompletion{ArgsLen: 1, ConfigFile: c.ConfigFile}
+	return healthcheckProbeActionTypeCompletion.CompletionFunc(cmd, args, toComplete)
+}
+
+// ServiceProtocolCompletion provides completion for service protocols
+type ServiceProtocolCompletion struct {
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of service protocols for completion
+func (c *ServiceProtocolCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	protocols := []string{"TCP", "UDP", "TLS", "SCTP"}
+	if c.ArgsLen < 0 || len(args) == c.ArgsLen {
+
+		var results []string
+		for _, protocol := range protocols {
+			if strings.HasPrefix(protocol, toComplete) {
+				results = append(results, protocol)
+			}
+		}
+		return results, cobra.ShellCompDirectiveNoFileComp
+
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// KeyCompletion provides completion for SSH key names
+type KeyCompletion struct {
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of SSH key names for completion
+func (c *KeyCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if s, err := settings.Load(*c.ConfigFile); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+		if keys, _, err := keys.List(s.Client, -1); err == nil {
+			var results []string
+			for _, key := range keys {
+				if strings.HasPrefix(key.ID, toComplete) {
+					results = append(results, key.ID)
+				}
+			}
+			return results, cobra.ShellCompDirectiveNoFileComp
+		}
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// TokenCompletion provides completion for tokens
+type TokenCompletion struct {
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of tokens for completion
+func (c *TokenCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if s, err := settings.Load(*c.ConfigFile); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+		if tokens, _, err := tokens.List(s.Client, -1); err == nil {
+			var results []string
+			for _, token := range tokens {
+				if strings.HasPrefix(token.UUID, toComplete) {
+					results = append(results, token.UUID)
+				}
+			}
+			return results, cobra.ShellCompDirectiveNoFileComp
+		}
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// LabelCompletion provides completion for label names
+type LabelCompletion struct {
+	AppID      *string
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of label names for completion
+func (c *LabelCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if appID, s, err := loader.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+		if appsettings, err := appsettings.List(s.Client, appID); err == nil {
+			var results []string
+			for key := range appsettings.Label {
+				if strings.HasPrefix(key, toComplete) {
+					if !slices.Contains(args, key) {
+						results = append(results, key)
+					}
+				}
+			}
+			return results, cobra.ShellCompDirectiveNoFileComp
+		}
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// LimitSpecCompletion provides completion for limit specifications
+type LimitSpecCompletion struct {
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of limit specifications for completion
+func (c *LimitSpecCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if s, err := settings.Load(*c.ConfigFile); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+		if specs, _, err := limits.Specs(s.Client, toComplete, -1); err == nil {
+			var results []string
+			for _, sepc := range specs {
+				results = append(results, sepc.ID)
+			}
+			return results, cobra.ShellCompDirectiveNoFileComp
+		}
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// LimitSetPlanCompletion provides completion for limit set plans
+type LimitSetPlanCompletion struct {
+	AppID      *string
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of limit set plans for completion
+func (c *LimitSetPlanCompletion) CompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if strings.Contains(toComplete, "=") {
+		var results []string
+		parts := strings.Split(toComplete, "=")
+		if s, err := settings.Load(*c.ConfigFile); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+			if plans, _, err := limits.Plans(s.Client, "", 0, 0, -1); err == nil {
+				for _, plan := range plans {
+					if strings.HasPrefix(plan.ID, parts[1]) {
+						results = append(results, fmt.Sprintf("%s=%s", parts[0], plan.ID))
+					}
+				}
+			}
+		}
+		return results, cobra.ShellCompDirectiveNoFileComp
+	}
+	ptsSetArgsCompletion := PtsSetArgsCompletion{
+		PtsCompletion: &PtsCompletion{AppID: c.AppID, ArgsLen: -1, ConfigFile: c.ConfigFile},
+	}
+	return ptsSetArgsCompletion.CompletionFunc(cmd, args, toComplete)
+}
+
+// WorkspaceCompletion provides completion for workspace names
+type WorkspaceCompletion struct {
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of workspace names for completion
+func (c *WorkspaceCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if s, err := settings.Load(*c.ConfigFile); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+		if wkspaces, _, err := workspaces.List(s.Client, -1); err == nil {
+			var results []string
+			for _, ws := range wkspaces {
+				if strings.HasPrefix(ws.Name, toComplete) {
+					results = append(results, ws.Name)
+				}
+			}
+			return results, cobra.ShellCompDirectiveNoFileComp
+		}
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// WorkspaceMemberCompletion provides completion for workspace member usernames
+type WorkspaceMemberCompletion struct {
+	Workspace  *string
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of workspace member usernames for completion
+func (c *WorkspaceMemberCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	workspace := *c.Workspace
+	if workspace == "" && len(args) > 0 {
+		workspace = args[0]
+	}
+	if workspace != "" {
+		if s, err := settings.Load(*c.ConfigFile); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+			if mems, _, err := members.List(s.Client, workspace, -1); err == nil {
+				var results []string
+				for _, m := range mems {
+					if strings.HasPrefix(m.User, toComplete) {
+						results = append(results, m.User)
+					}
+				}
+				return results, cobra.ShellCompDirectiveNoFileComp
+			}
+		}
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// WorkspaceRoleCompletion provides completion for workspace member roles
+type WorkspaceRoleCompletion struct {
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of workspace roles for completion
+func (c *WorkspaceRoleCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	roles := []string{"admin", "member", "viewer"}
+	if c.ArgsLen < 0 || len(args) == c.ArgsLen {
+		var results []string
+		for _, role := range roles {
+			if strings.HasPrefix(role, toComplete) {
+				results = append(results, role)
+			}
+		}
+		return results, cobra.ShellCompDirectiveNoFileComp
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// WorkspaceUpdateCompletion provides completion for workspace update command
+type WorkspaceUpdateCompletion struct {
+	ConfigFile *string
+}
+
+// CompletionFunc returns completion for workspace update positional args
+func (c *WorkspaceUpdateCompletion) CompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if len(args) == 0 {
+		// First arg: workspace name
+		workspaceCompletion := WorkspaceCompletion{ArgsLen: 0, ConfigFile: c.ConfigFile}
+		return workspaceCompletion.CompletionFunc(cmd, args, toComplete)
+	}
+	// Second arg: member username
+	workspace := args[0]
+	memberCompletion := WorkspaceMemberCompletion{Workspace: &workspace, ArgsLen: 1, ConfigFile: c.ConfigFile}
+	return memberCompletion.CompletionFunc(cmd, args, toComplete)
+}
+
+// WorkspaceRemoveCompletion provides completion for workspace remove command
+type WorkspaceRemoveCompletion struct {
+	ConfigFile *string
+}
+
+// CompletionFunc returns completion for workspace remove positional args
+func (c *WorkspaceRemoveCompletion) CompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if len(args) == 0 {
+		// First arg: workspace name
+		workspaceCompletion := WorkspaceCompletion{ArgsLen: 0, ConfigFile: c.ConfigFile}
+		return workspaceCompletion.CompletionFunc(cmd, args, toComplete)
+	}
+	// Second arg: member username
+	workspace := args[0]
+	memberCompletion := WorkspaceMemberCompletion{Workspace: &workspace, ArgsLen: 1, ConfigFile: c.ConfigFile}
+	return memberCompletion.CompletionFunc(cmd, args, toComplete)
+}
+
+// PsCompletion provides completion for process names
+type PsCompletion struct {
+	AppID      *string
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of process names for completion
+func (c *PsCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if appID, s, err := loader.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+		if pods, _, err := ps.List(s.Client, appID, -1); err == nil {
+			var results []string
+			for _, pod := range pods {
+				if strings.HasPrefix(pod.Name, toComplete) {
+					results = append(results, pod.Name)
+				}
+			}
+			return results, cobra.ShellCompDirectiveNoFileComp
+		}
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// PtsCompletion provides completion for pts names
+type PtsCompletion struct {
+	AppID      *string
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of pts names for completion
+func (c *PtsCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	var results []string
+	if appID, s, err := loader.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+		if ptypes, _, err := pts.List(s.Client, appID, -1); err == nil {
+			for _, ptype := range ptypes {
+				if strings.HasPrefix(ptype.Name, toComplete) {
+					if !slices.Contains(args, ptype.Name) {
+						results = append(results, ptype.Name)
+					}
+				}
+			}
+		}
+	}
+	return results, cobra.ShellCompDirectiveNoFileComp
+}
+
+// PtsArgsCompletion provides completion for process type arguments.
+type PtsArgsCompletion struct {
+	*PtsCompletion
+}
+
+// CompletionFunc returns a list of pts arguments for completion
+func (c *PtsArgsCompletion) CompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	var results []string
+	completes, _ := c.PtsCompletion.CompletionFunc(cmd, args, toComplete)
+	for _, complete := range completes {
+		if !slices.Contains(args, complete) {
+			results = append(results, complete)
+		}
+	}
+	return results, cobra.ShellCompDirectiveNoFileComp
+}
+
+// PtsSetArgsCompletion provides completion for process type scale arguments.
+type PtsSetArgsCompletion struct {
+	*PtsCompletion
+}
+
+// CompletionFunc returns a list of process type scale arguments for completion.
+func (c *PtsSetArgsCompletion) CompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	var results []string
+	completion := PtsCompletion{AppID: c.AppID, ArgsLen: -1, ConfigFile: c.ConfigFile}
+	completes, _ := completion.CompletionFunc(cmd, args, toComplete)
+	for _, complete := range completes {
+		hasArgs := false
+		result := fmt.Sprintf("%s=", complete)
+		for _, arg := range args {
+			if strings.HasPrefix(arg, result) {
+				hasArgs = true
+				break
+			}
+		}
+		if !hasArgs {
+			results = append(results, result)
+		}
+	}
+	return results, cobra.ShellCompDirectiveNoSpace
+}
+
+// ResourceServiceCompletion provides completion for resource services
+type ResourceServiceCompletion struct {
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of resource services for completion
+func (c *ResourceServiceCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if s, err := settings.Load(*c.ConfigFile); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+		if services, _, err := resources.Services(s.Client, -1); err == nil {
+			var results []string
+			for _, service := range services {
+				if strings.HasPrefix(service.Name, toComplete) {
+					results = append(results, service.Name)
+				}
+			}
+			return results, cobra.ShellCompDirectiveNoFileComp
+		}
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// ResourcePlanCompletion provides completion for resource plans
+type ResourcePlanCompletion struct {
+	Service    string
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of resource plans for completion
+func (c *ResourcePlanCompletion) CompletionFunc(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
+	if s, err := settings.Load(*c.ConfigFile); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+		var results []string
+		if services, _, err := resources.Plans(s.Client, c.Service, -1); err == nil {
+			for _, service := range services {
+				results = append(results, service.Name)
+			}
+		}
+		return results, cobra.ShellCompDirectiveNoFileComp
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// ResourceCreateCompletion provides completion for resource creation
+type ResourceCreateCompletion struct {
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of resource creation options for completion
+func (c *ResourceCreateCompletion) CompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if len(args) == 0 {
+		resourceServiceCompletion := ResourceServiceCompletion{ArgsLen: 0, ConfigFile: c.ConfigFile}
+		return resourceServiceCompletion.CompletionFunc(cmd, args, toComplete)
+	}
+	resourcePlanCompletion := ResourcePlanCompletion{Service: args[0], ArgsLen: 1, ConfigFile: c.ConfigFile}
+	return resourcePlanCompletion.CompletionFunc(cmd, args, toComplete)
+}
+
+// ResourceCompletion provides completion for resources
+type ResourceCompletion struct {
+	AppID      *string
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of resources for completion
+func (c *ResourceCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if appID, s, err := loader.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+		if resources, _, err := resources.List(s.Client, appID, -1); err == nil {
+			var results []string
+			for _, resource := range resources {
+				if strings.HasPrefix(resource.Name, toComplete) {
+					results = append(results, resource.Name)
+				}
+			}
+			return results, cobra.ShellCompDirectiveNoFileComp
+		}
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// ResourceUpdateCompletion provides completion for resource update arguments.
+type ResourceUpdateCompletion struct {
+	AppID      *string
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of resource update arguments for completion.
+func (c *ResourceUpdateCompletion) CompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if len(args) == 0 {
+		ResourceCompletion := ResourceCompletion{AppID: c.AppID, ArgsLen: -1, ConfigFile: c.ConfigFile}
+		return ResourceCompletion.CompletionFunc(cmd, args, toComplete)
+	} else if len(args) == 1 {
+		resourceServiceCompletion := ResourceServiceCompletion{ArgsLen: -1, ConfigFile: c.ConfigFile}
+		return resourceServiceCompletion.CompletionFunc(cmd, args, toComplete)
+	} else if len(args) == 2 {
+		resourcePlanCompletion := ResourcePlanCompletion{Service: args[1], ArgsLen: -1, ConfigFile: c.ConfigFile}
+		return resourcePlanCompletion.CompletionFunc(cmd, args, toComplete)
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// ReleaseCompletion provides completion for release versions.
+type ReleaseCompletion struct {
+	AppID      *string
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of release versions for completion.
+func (c *ReleaseCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if appID, s, err := loader.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+		if releases, _, err := releases.List(s.Client, appID, "", -1); err == nil {
+			var results []string
+			toComplete = strings.TrimPrefix(toComplete, "v")
+			for _, release := range releases {
+				version := strconv.Itoa(release.Version)
+				if strings.HasPrefix(version, toComplete) {
+					results = append(results, version)
+				}
+			}
+			return results, cobra.ShellCompDirectiveNoFileComp
+		}
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// RouteKindCompletion provides completion for route kinds.
+type RouteKindCompletion struct {
+	AppID   *string
+	ArgsLen int
+}
+
+// CompletionFunc returns a list of route kinds for completion.
+func (c *RouteKindCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	kinds := []string{"HTTPRoute", "TCPRoute", "UDPRoute", "GRPCRoute", "TLSRoute"}
+	if c.ArgsLen < 0 || len(args) == c.ArgsLen {
+
+		var results []string
+		for _, kind := range kinds {
+			if strings.HasPrefix(kind, toComplete) {
+				results = append(results, kind)
+			}
+		}
+		return results, cobra.ShellCompDirectiveNoFileComp
+
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// RouteCompletion provides completion for routes
+type RouteCompletion struct {
+	AppID      *string
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of routes for completion
+func (c *RouteCompletion) CompletionFunc(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
+	if appID, s, err := loader.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+		if routes, _, err := routes.List(s.Client, appID, -1); err == nil {
+			var results []string
+			for _, route := range routes {
+				results = append(results, route.Name)
+			}
+			return results, cobra.ShellCompDirectiveNoFileComp
+		}
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// ServiceCompletion provides completion for services
+type ServiceCompletion struct {
+	AppID      *string
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of services for completion
+func (c *ServiceCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if appID, s, err := loader.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+		if services, err := services.List(s.Client, appID); err == nil {
+			var results []string
+			for _, service := range services {
+				if strings.HasPrefix(service.Ptype, toComplete) {
+					results = append(results, service.Ptype)
+				}
+			}
+			return results, cobra.ShellCompDirectiveNoFileComp
+		}
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// TagCompletion provides completion for tags
+type TagCompletion struct {
+	AppID      *string
+	Ptype      *string
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of tags for completion
+func (c *TagCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if appID, s, err := loader.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+		if config, err := config.List(s.Client, appID, -1); err == nil {
+			var results []string
+			for tag := range config.Tags[*c.Ptype] {
+				if strings.HasPrefix(tag, toComplete) {
+					if !slices.Contains(args, tag) {
+						results = append(results, tag)
+					}
+				}
+			}
+			return results, cobra.ShellCompDirectiveNoFileComp
+		}
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// TLSActionCompletion provides completion for TLS actions
+type TLSActionCompletion struct {
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of TLS actions for completion
+func (c *TLSActionCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	actions := []string{"redirect", "passthrough"}
+	if c.ArgsLen < 0 || len(args) == c.ArgsLen {
+		var results []string
+		for _, action := range actions {
+			if strings.HasPrefix(action, toComplete) {
+				results = append(results, action)
+			}
+		}
+		return results, cobra.ShellCompDirectiveNoFileComp
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// VolumeCompletion provides completion for volumes
+type VolumeCompletion struct {
+	AppID      *string
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of volumes for completion
+func (c *VolumeCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if appID, s, err := loader.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+		if volumes, _, err := volumes.List(s.Client, appID, -1); err == nil {
+			var results []string
+			for _, volume := range volumes {
+				if strings.HasPrefix(volume.Name, toComplete) {
+					results = append(results, volume.Name)
+				}
+			}
+			return results, cobra.ShellCompDirectiveNoFileComp
+		}
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// VolumeTypeCompletion provides completion for volume types
+type VolumeTypeCompletion struct {
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of volume types for completion
+func (c *VolumeTypeCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	types := []string{"static", "dynamic"}
+	if c.ArgsLen < 0 || len(args) == c.ArgsLen {
+		var results []string
+		for _, t := range types {
+			if strings.HasPrefix(t, toComplete) {
+				results = append(results, t)
+			}
+		}
+		return results, cobra.ShellCompDirectiveNoFileComp
+	}
+	return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+// VolumesMountCompletion provides completion for volume mounts
+type VolumesMountCompletion struct {
+	AppID      *string
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of volume mount options for completion
+func (c *VolumesMountCompletion) CompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if len(args) == 0 {
+		volumeCompletion := VolumeCompletion{AppID: c.AppID, ArgsLen: 0, ConfigFile: c.ConfigFile}
+		return volumeCompletion.CompletionFunc(cmd, args, toComplete)
+	}
+	ptsArgCompletion := PtsArgsCompletion{
+		PtsCompletion: &PtsCompletion{AppID: c.AppID, ArgsLen: -1, ConfigFile: c.ConfigFile},
+	}
+	return ptsArgCompletion.CompletionFunc(cmd, args, toComplete)
+}
+
+// VolumesUnmountCompletion provides completion for volume unmounts
+type VolumesUnmountCompletion struct {
+	AppID      *string
+	ArgsLen    int
+	ConfigFile *string
+}
+
+// CompletionFunc returns a list of volume unmount options for completion
+func (c *VolumesUnmountCompletion) CompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	if len(args) == 0 {
+		volumeCompletion := VolumeCompletion{AppID: c.AppID, ArgsLen: 0, ConfigFile: c.ConfigFile}
+		return volumeCompletion.CompletionFunc(cmd, args, toComplete)
+	}
+	ptsArgCompletion := PtsArgsCompletion{
+		PtsCompletion: &PtsCompletion{AppID: c.AppID, ArgsLen: -1, ConfigFile: c.ConfigFile},
+	}
+	return ptsArgCompletion.CompletionFunc(cmd, args, toComplete)
+}
diff --git a/internal/loader/loader.go b/internal/loader/loader.go
new file mode 100644
index 000000000..4972038e7
--- /dev/null
+++ b/internal/loader/loader.go
@@ -0,0 +1,24 @@
+// Package loader provides common utility functions and helper methods for the workflow CLI.
+package loader
+
+import (
+	"github.com/drycc/workflow-cli/pkg/git"
+	"github.com/drycc/workflow-cli/pkg/settings"
+)
+
+// LoadAppSettings loads settings file and looks up the app name
+func LoadAppSettings(cf string, appID string) (string, *settings.Settings, error) {
+	s, err := settings.Load(cf)
+	if err != nil {
+		return "", nil, err
+	}
+
+	if appID == "" {
+		appID, err = git.DetectAppName(git.DefaultCmd, s.Client.ControllerURL.Host)
+		if err != nil {
+			return "", nil, err
+		}
+	}
+
+	return appID, s, nil
+}
diff --git a/internal/loader/loader_test.go b/internal/loader/loader_test.go
new file mode 100644
index 000000000..2a0ddc63a
--- /dev/null
+++ b/internal/loader/loader_test.go
@@ -0,0 +1,55 @@
+// Package loader provides common utility functions and helper methods for the workflow CLI.
+package loader
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	drycc "github.com/drycc/controller-sdk-go"
+	"github.com/drycc/workflow-cli/pkg/git"
+	"github.com/drycc/workflow-cli/pkg/settings"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestLoadProject(t *testing.T) {
+	name, err := os.MkdirTemp("", "drycc-cli-unit-test-load")
+	assert.NoError(t, err)
+	defer os.RemoveAll(name)
+
+	filename := filepath.Join(name, "test.json")
+	host := "drycc.example.com"
+	client, err := drycc.New(false, host, "")
+	assert.NoError(t, err)
+
+	config := settings.Settings{
+		Username: "test",
+		Client:   client,
+	}
+
+	filename, err = config.Save(filename)
+	assert.NoError(t, err)
+
+	appID, _, err := LoadAppSettings(filename, "test")
+	assert.NoError(t, err)
+	assert.Equal(t, appID, "test", "app")
+
+	assert.NoError(t, os.Chdir(name))
+
+	os.Setenv("DRYCC_APP", "testapp")
+	appID, _, err = LoadAppSettings(filename, "")
+	assert.NoError(t, err)
+	assert.Equal(t, appID, "testapp", "app")
+	os.Unsetenv("DRYCC_APP")
+
+	appID, _, err = LoadAppSettings(filename, "")
+	assert.NoError(t, err)
+	assert.Equal(t, appID, filepath.Base(name), "app")
+
+	assert.NoError(t, git.Init(git.DefaultCmd))
+	assert.NoError(t, git.CreateRemote(git.DefaultCmd, host, "drycc", "testing"))
+
+	appID, _, err = LoadAppSettings(filename, "")
+	assert.NoError(t, err)
+	assert.Equal(t, appID, "testing", "app")
+}
diff --git a/internal/parser/apps.go b/internal/parser/apps.go
new file mode 100644
index 000000000..0685cfbb3
--- /dev/null
+++ b/internal/parser/apps.go
@@ -0,0 +1,199 @@
+// Package parser provides command parsers for the Drycc CLI.
+package parser
+
+import (
+	"strings"
+
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/internal/template"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewAppsCommand creates the apps command
+func NewAppsCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "apps",
+		Short: i18n.T("Manage applications used to provide services"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.AppsList(limit)
+		},
+	}
+	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("Maximum number of results to display"))
+
+	cmd.AddCommand(appsCreate(cmdr))
+	cmd.AddCommand(appsList(cmdr))
+	cmd.AddCommand(appsInfo(cmdr))
+	cmd.AddCommand(appsOpen(cmdr))
+	cmd.AddCommand(appsRun(cmdr))
+	cmd.AddCommand(appsDestroy(cmdr))
+	cmd.AddCommand(appsTransfer(cmdr))
+	return cmd
+}
+
+// AppsCreate creates the apps create command
+func appsCreate(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		workspace string
+		remote    string
+		noRemote  bool
+	}
+	cmd := &cobra.Command{
+		Use:   "create [<id>]",
+		Args:  cobra.MaximumNArgs(1),
+		Short: i18n.T("Create a new application"),
+		Long:  i18n.T(`Creates a new application. If no <id> is provided, one will be generated automatically`),
+		RunE: func(_ *cobra.Command, args []string) error {
+			id := ""
+			if len(args) > 0 {
+				id = args[0]
+			}
+			return cmdr.AppCreate(id, flags.workspace, flags.remote, flags.noRemote)
+		},
+	}
+
+	cmd.Flags().StringVarP(&flags.workspace, "workspace", "w", "", i18n.T("The workspace to create the app in"))
+	cmd.MarkFlagRequired("workspace")
+	cmd.Flags().StringVarP(&flags.remote, "remote", "r", "drycc", i18n.T("Name of remote to create"))
+	cmd.Flags().BoolVar(&flags.noRemote, "no-remote", false, i18n.T("Do not create a 'drycc' git remote"))
+
+	workspaceCompletion := completion.WorkspaceCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("workspace", workspaceCompletion.CompletionFunc)
+	return cmd
+}
+
+// AppsList creates the apps list command
+func appsList(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "list",
+		Short: i18n.T("List accessible applications"),
+		Long:  i18n.T("Lists applications visible to the current user"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.AppsList(limit)
+		},
+	}
+
+	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("Maximum number of results to display"))
+	return cmd
+}
+
+// AppsInfo creates the apps info command
+func appsInfo(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "info",
+		Short: i18n.T("View info about an application"),
+		Long:  i18n.T("Prints info about the current application"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.AppInfo(app)
+		},
+	}
+
+	cmd.Flags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+	return cmd
+}
+
+// AppsOpen creates the apps open command
+func appsOpen(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "open",
+		Short: i18n.T("Open the application in a browser"),
+		Long:  i18n.T("Opens a URL to the application in the default browser"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.AppOpen(app)
+		},
+	}
+
+	cmd.Flags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+	return cmd
+}
+
+// AppsRun creates the apps run command
+func appsRun(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		app     string
+		timeout uint32
+		expires uint32
+		mounts  []string
+	}
+
+	cmd := &cobra.Command{
+		Use:  "run [--mount=<volume>:<path>...] -- <command>...",
+		Args: cobra.MinimumNArgs(1),
+		Example: template.CustomExample(
+			"drycc apps run --mount=myvolume:/data -- 'echo hello'",
+			map[string]string{
+				"<volume>":  i18n.T("The volume name"),
+				"<path>":    i18n.T("The filesystem path"),
+				"<command>": i18n.T("The shell command to run inside the container"),
+			},
+		),
+		Short: i18n.T("Run a command in an ephemeral app container"),
+		Long:  i18n.T("Runs a command inside an ephemeral app container"),
+		RunE: func(_ *cobra.Command, args []string) error {
+			command := strings.Join(args, " ")
+			return cmdr.AppRun(app, command, flags.mounts, flags.timeout, flags.expires)
+		},
+	}
+
+	cmd.Flags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+	cmd.Flags().StringSliceVarP(&flags.mounts, "mount", "m", nil, i18n.T("Volume mounts in format 'volume:path'"))
+	cmd.Flags().Uint32VarP(&flags.timeout, "timeout", "t", 3600, i18n.T("Command execution timeout in seconds"))
+	cmd.Flags().Uint32VarP(&flags.expires, "expires", "e", 3600, i18n.T("Retention time of records in seconds"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+	return cmd
+}
+
+// AppsDestroy creates the apps destroy command
+func appsDestroy(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		app     string
+		confirm string
+	}
+
+	cmd := &cobra.Command{
+		Use:     "destroy",
+		Example: "drycc apps destroy -a <app> --confirm <app>",
+		Short:   i18n.T("Destroy an application"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.AppDestroy(app, flags.confirm)
+		},
+	}
+
+	cmd.Flags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+	cmd.Flags().StringVar(&flags.confirm, "confirm", "", i18n.T("Skips the prompt for the application name. \n<app> is the uniquely identifiable name for the application"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+	return cmd
+}
+
+// AppsTransfer creates the apps transfer command
+func appsTransfer(cmdr *commands.DryccCmd) *cobra.Command {
+	workspaceCompletion := completion.WorkspaceCompletion{ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:               "transfer <workspace>",
+		Args:              cobra.ExactArgs(1),
+		Short:             i18n.T("Transfer app to another workspace"),
+		Long:              i18n.T("Transfer application to another workspace."),
+		ValidArgsFunction: workspaceCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			workspace := args[0]
+			return cmdr.AppTransfer(app, workspace)
+		},
+	}
+
+	cmd.Flags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+	return cmd
+}
diff --git a/internal/parser/auth.go b/internal/parser/auth.go
new file mode 100644
index 000000000..42bc5feb2
--- /dev/null
+++ b/internal/parser/auth.go
@@ -0,0 +1,80 @@
+package parser
+
+import (
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewAuthCommand creates the auth command
+func NewAuthCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "auth",
+		Short: i18n.T("Manage authentication"),
+		RunE:  nil,
+	}
+	cmd.AddCommand(authLogin(cmdr))
+	cmd.AddCommand(authLogout(cmdr))
+	cmd.AddCommand(authWhoami(cmdr))
+	return cmd
+}
+
+// AuthLogin creates the auth:login command
+func authLogin(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		username  string
+		password  string
+		sslVerify bool
+	}
+
+	cmd := &cobra.Command{
+		Use:     "login <controller>",
+		Args:    cobra.ExactArgs(1),
+		Example: "drycc auth login http://drycc.local3.dryccapp.com/",
+		Short:   i18n.T("Authenticate against a controller"),
+		Long:    i18n.T("Logs in by authenticating against a controller"),
+		RunE: func(_ *cobra.Command, args []string) error {
+			controller := args[0]
+			return cmdr.Login(controller, flags.sslVerify, flags.username, flags.password)
+		},
+	}
+	cmd.Flags().StringVarP(&flags.username, "username", "u", "", i18n.T("Provide a username for the account"))
+	cmd.Flags().StringVarP(&flags.password, "password", "p", "", i18n.T("Provide a password for the account"))
+	cmd.Flags().BoolVar(&flags.sslVerify, "ssl-verify", true, i18n.T("Enables or disables SSL certificate verification for API requests"))
+	cmd.Flags().SortFlags = false
+	return cmd
+}
+
+// AuthLogout creates the auth:logout command
+func authLogout(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:     "logout",
+		Example: "drycc auth logout",
+		Short:   i18n.T("Clear the current user session"),
+		Long:    i18n.T("Logs out from a controller and clears the user session"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.Logout()
+		},
+	}
+	return cmd
+}
+
+// AuthWhoami creates the auth:whoami command
+func authWhoami(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		all bool
+	}
+
+	cmd := &cobra.Command{
+		Use:     "whoami",
+		Example: "drycc auth whoami",
+		Short:   i18n.T("Display the current user"),
+		Long:    i18n.T("Displays the currently logged in user"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.Whoami(flags.all)
+		},
+	}
+
+	cmd.Flags().BoolVarP(&flags.all, "all", "a", false, i18n.T("Fetch detailed user information"))
+	return cmd
+}
diff --git a/internal/parser/autodeploy.go b/internal/parser/autodeploy.go
new file mode 100644
index 000000000..97164a865
--- /dev/null
+++ b/internal/parser/autodeploy.go
@@ -0,0 +1,64 @@
+package parser
+
+import (
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewAutodeployCommand creates the autodeploy command
+func NewAutodeployCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "autodeploy",
+		Short: i18n.T("Manage autodeploy if or not for applications"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.AutodeployInfo(app)
+		},
+	}
+
+	cmd.PersistentFlags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+
+	cmd.AddCommand(autodeployInfo(cmdr))
+	cmd.AddCommand(autodeployEnable(cmdr))
+	cmd.AddCommand(autodeployDisable(cmdr))
+	return cmd
+}
+
+// AutodeployInfo Information about the autodeploy
+func autodeployInfo(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "info",
+		Short: i18n.T("Prints info about the current application's autodeploy if or not"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.AutodeployInfo(app)
+		},
+	}
+	return cmd
+}
+
+// AutodeployEnable creates the autodeploy enable command
+func autodeployEnable(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "enable",
+		Short: i18n.T("Enables autodeploy for an app"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.AutodeployEnable(app)
+		},
+	}
+	return cmd
+}
+
+// AutodeployDisable creates the autodeploy disable command
+func autodeployDisable(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "disable",
+		Short: i18n.T("Disables autodeploy for an app"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.AutodeployDisable(app)
+		},
+	}
+	return cmd
+}
diff --git a/internal/parser/autorollback.go b/internal/parser/autorollback.go
new file mode 100644
index 000000000..553950a5a
--- /dev/null
+++ b/internal/parser/autorollback.go
@@ -0,0 +1,64 @@
+package parser
+
+import (
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewAutorollbackCommand creates the autorollback command
+func NewAutorollbackCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "autorollback",
+		Short: i18n.T("Manage autorollback if or not for application"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.AutorollbackInfo(app)
+		},
+	}
+	cmd.PersistentFlags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+
+	cmd.AddCommand(autorollbackInfo(cmdr))
+	cmd.AddCommand(autorollbackEnable(cmdr))
+	cmd.AddCommand(autorollbackDisable(cmdr))
+	return cmd
+}
+
+// AutorollbackInfo creates the autorollback info command
+func autorollbackInfo(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "info",
+		Short: i18n.T("View autorollback info for an application"),
+		Long:  i18n.T("Prints info about the current application's autorollback if or not"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.AutorollbackInfo(app)
+		},
+	}
+	return cmd
+}
+
+// AutorollbackEnable creates the autorollback enable command
+func autorollbackEnable(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "enable",
+		Short: i18n.T("Enable autorollback for an application"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.AutorollbackEnable(app)
+		},
+	}
+	return cmd
+}
+
+// AutorollbackDisable creates the autorollback disable command
+func autorollbackDisable(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "disable",
+		Short: i18n.T("Disable autorollback for an application"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.AutorollbackDisable(app)
+		},
+	}
+	return cmd
+}
diff --git a/internal/parser/autoscale.go b/internal/parser/autoscale.go
new file mode 100644
index 000000000..c65784871
--- /dev/null
+++ b/internal/parser/autoscale.go
@@ -0,0 +1,109 @@
+package parser
+
+import (
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/internal/template"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewAutoscaleCommand creates the autoscale command
+func NewAutoscaleCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "autoscale",
+		Short: i18n.T("Manage autoscale for applications"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.AutoscaleList(app)
+		},
+	}
+
+	cmd.PersistentFlags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+
+	cmd.AddCommand(autoscaleListCommand(cmdr))
+	cmd.AddCommand(autoscaleSetCommand(cmdr))
+	cmd.AddCommand(autoscaleUnsetCommand(cmdr))
+	return cmd
+}
+
+func autoscaleListCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "list",
+		Short: i18n.T("List autoscale options for an application"),
+		Long:  i18n.T("Prints a list of autoscale options for the application"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.AutoscaleList(app)
+		},
+	}
+	return cmd
+}
+
+func autoscaleSetCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		min        int
+		max        int
+		cpuPercent int
+	}
+
+	ptsArgsCompletion := completion.PtsArgsCompletion{
+		PtsCompletion: &completion.PtsCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile},
+	}
+
+	cmd := &cobra.Command{
+		Use: "set <ptype> --min=<min> --max=<max> --cpu-percent=<percent>",
+		Example: template.CustomExample(
+			"drycc autoscale set web --min=1 --max=10 --cpu-percent=50",
+			map[string]string{
+				"<ptype>": i18n.T("The process type to add to the application's autoscale settings"),
+			},
+		),
+		Args:              cobra.ExactArgs(1),
+		Short:             i18n.T("Turn on autoscale for an app"),
+		Long:              i18n.T("Set autoscale option per process type for an app"),
+		ValidArgsFunction: ptsArgsCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			ptype := args[0]
+			return cmdr.AutoscaleSet(app, ptype, flags.min, flags.max, flags.cpuPercent)
+		},
+	}
+
+	cmd.Flags().IntVarP(&flags.min, "min", "", 0, i18n.T("minimum replicas to keep around"))
+	cmd.Flags().IntVarP(&flags.max, "max", "", 0, i18n.T("Max replicas to scale up to"))
+	cmd.Flags().IntVarP(&flags.cpuPercent, "cpu-percent", "", 0, i18n.T("Target CPU utilization percentage"))
+	cmd.Flags().SortFlags = false
+
+	mustFlags := []string{"min", "max", "cpu-percent"}
+	for _, mustFlag := range mustFlags {
+		cmd.MarkFlagRequired(mustFlag)
+	}
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+	return cmd
+}
+
+func autoscaleUnsetCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	ptsArgsCompletion := completion.PtsArgsCompletion{
+		PtsCompletion: &completion.PtsCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile},
+	}
+	cmd := &cobra.Command{
+		Use: "unset <ptype>",
+		Example: template.CustomExample(
+			"drycc autoscale unset web",
+			map[string]string{
+				"<ptype>": i18n.T("The process type to remove from the application's autoscale settings"),
+			},
+		),
+		Args:              cobra.ExactArgs(1),
+		Short:             i18n.T("Turn off autoscale for an app"),
+		Long:              i18n.T("Unset autoscale per process type for an app"),
+		ValidArgsFunction: ptsArgsCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			ptype := args[0]
+			return cmdr.AutoscaleUnset(app, ptype)
+		},
+	}
+	return cmd
+}
diff --git a/internal/parser/builds.go b/internal/parser/builds.go
new file mode 100644
index 000000000..f936cc6dc
--- /dev/null
+++ b/internal/parser/builds.go
@@ -0,0 +1,122 @@
+package parser
+
+import (
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/internal/template"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewBuildsCommand creates a command for managing builds.
+func NewBuildsCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "builds",
+		Short: i18n.T("Manage builds created using 'git push'"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.BuildsInfo(app, version)
+		},
+	}
+
+	cmd.PersistentFlags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+	cmd.Flags().IntVarP(&version, "version", "v", 0, i18n.T("The version for which the build info needs to be displayed"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+	releaseCompletion := completion.ReleaseCompletion{ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("version", releaseCompletion.CompletionFunc)
+
+	cmd.AddCommand(buildsInfo(cmdr))
+	cmd.AddCommand(buildsCreate(cmdr))
+	cmd.AddCommand(buildsFetch(cmdr))
+
+	return cmd
+}
+
+func buildsCreate(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		stack     string
+		procfile  string
+		dryccPath string
+		confirm   string
+	}
+	cmd := &cobra.Command{
+		Use:  `create <image>`,
+		Args: cobra.ExactArgs(1),
+		Example: template.CustomExample(
+			`drycc create docker.io/library/nginx:latest`,
+			map[string]string{
+				"<image>": i18n.T("default container image"),
+			},
+		),
+		Short: i18n.T("imports an image and deploys as a new release"),
+		Long: i18n.T(`Creates a new build of an application. Imports an <image> and deploys it to Drycc
+as a new release. If a Procfile or drycc.yaml is present in the current directory,
+it will be used as the default for this application.`),
+		RunE: func(_ *cobra.Command, args []string) error {
+			image := args[0]
+			err := cmdr.BuildsCreate(app, image, flags.stack, flags.procfile, flags.dryccPath, flags.confirm)
+			return err
+		},
+	}
+
+	// // shortcuts pull has not app flag
+	cmd.Flags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+	cmd.Flags().StringVarP(&flags.stack, "stack", "s", "container", i18n.T("The stack name for the application"))
+	cmd.Flags().StringVarP(&flags.procfile, "procfile", "p", "Procfile", i18n.T("A YAML file used to supply a Procfile to the application"))
+	cmd.Flags().StringVarP(&flags.dryccPath, "dryccpath", "d", ".drycc", i18n.T("Drycc config path to the application"))
+	cmd.Flags().StringVarP(&flags.confirm, "confirm", "", "", i18n.T(`To proceed, type "yes"`))
+	cmd.Flags().SortFlags = false
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+
+	return cmd
+}
+
+// buildsInfo
+func buildsInfo(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:     "info",
+		Example: "drycc builds info",
+		Short:   i18n.T("Print information about a specific build"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.BuildsInfo(app, version)
+		},
+	}
+	cmd.Flags().IntVarP(&version, "version", "v", 0, i18n.T("The version for which the build info needs to be displayed"))
+
+	releaseCompletion := completion.ReleaseCompletion{ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("version", releaseCompletion.CompletionFunc)
+	return cmd
+}
+
+// buildsFetch
+func buildsFetch(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		stack     string
+		procfile  string
+		dryccPath string
+		confirm   string
+		save      bool
+	}
+
+	cmd := &cobra.Command{
+		Use:     "fetch",
+		Example: "drycc builds fetch",
+		Short:   i18n.T("Print process info about a specific build"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.BuildsFetch(app, version, flags.procfile, flags.dryccPath, flags.confirm, flags.save)
+		},
+	}
+	cmd.Flags().IntVarP(&version, "version", "v", 0, i18n.T("The version for which the build info needs to be displayed"))
+	cmd.Flags().StringVarP(&flags.procfile, "procfile", "p", "Procfile", i18n.T("A YAML file used to supply a Procfile to the application"))
+	cmd.Flags().StringVarP(&flags.dryccPath, "dryccpath", "d", ".drycc", i18n.T("Drycc config path to the application"))
+	cmd.Flags().StringVarP(&flags.confirm, "confirm", "", "", i18n.T(`To proceed, type "yes"`))
+	cmd.Flags().BoolVarP(&flags.save, "save", "", false, i18n.T("Save process info to the local"))
+	cmd.Flags().SortFlags = false
+
+	releaseCompletion := completion.ReleaseCompletion{ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("version", releaseCompletion.CompletionFunc)
+	return cmd
+}
diff --git a/internal/parser/certs.go b/internal/parser/certs.go
new file mode 100644
index 000000000..de1c02b09
--- /dev/null
+++ b/internal/parser/certs.go
@@ -0,0 +1,169 @@
+package parser
+
+import (
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/internal/template"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewCertsCommand creates the certs command
+func NewCertsCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "certs",
+		Short: i18n.T("Manage SSL endpoints for an app"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			results, _ := commands.ResponseLimit(limit)
+			return cmdr.CertsList(app, results)
+		},
+	}
+
+	cmd.PersistentFlags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("The maximum number of results to display"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+
+	cmd.AddCommand(certsListCommand(cmdr))
+	cmd.AddCommand(certAddCommand(cmdr))
+	cmd.AddCommand(certRemoveCommand(cmdr))
+	cmd.AddCommand(certInfoCommand(cmdr))
+	cmd.AddCommand(certAttachCommand(cmdr))
+	cmd.AddCommand(certDetachCommand(cmdr))
+	return cmd
+}
+
+func certsListCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "list",
+		Short: i18n.T("List SSL certificates for an application"),
+		Long:  i18n.T("Show certificate information for an SSL application"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			results, _ := commands.ResponseLimit(limit)
+			return cmdr.CertsList(app, results)
+		},
+	}
+
+	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("The maximum number of results to display"))
+
+	return cmd
+}
+
+func certAddCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use: "add <name> <cert> <key>",
+		Example: template.CustomExample(
+			"drycc certs add cert-name default.crt default.key",
+			map[string]string{
+				"<name>": i18n.T("Name of the certificate to reference it by"),
+				"<cert>": i18n.T("The public key of the SSL certificate"),
+				"<key>":  i18n.T("The private key of the SSL certificate"),
+			},
+		),
+		Args:  cobra.ExactArgs(3),
+		Short: i18n.T("Add an SSL certificate to an app"),
+		RunE: func(_ *cobra.Command, args []string) error {
+			name := args[0]
+			cert := args[1]
+			key := args[2]
+			return cmdr.CertAdd(app, cert, key, name)
+		},
+	}
+
+	return cmd
+}
+
+func certRemoveCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	certCompletion := completion.CertCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use: "remove <name>",
+		Example: template.CustomExample(
+			"drycc certs remove cert-name",
+			map[string]string{
+				"<name>": i18n.T("The name of the cert to remove from the app"),
+			},
+		),
+		Args:              cobra.ExactArgs(1),
+		Short:             i18n.T("Remove an SSL certificate from an app"),
+		ValidArgsFunction: certCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			name := args[0]
+			return cmdr.CertRemove(app, name)
+		},
+	}
+
+	return cmd
+}
+
+func certInfoCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	certCompletion := completion.CertCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use: "info <name>",
+		Example: template.CustomExample(
+			"drycc certs info cert-name",
+			map[string]string{
+				"<name>": i18n.T("The name of the cert to get information from"),
+			},
+		),
+		Args:              cobra.ExactArgs(1),
+		Short:             i18n.T("Get detailed informaton about the certificate"),
+		Long:              i18n.T("Fetch more detailed information about a certificate"),
+		ValidArgsFunction: certCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			name := args[0]
+			return cmdr.CertInfo(app, name)
+		},
+	}
+
+	return cmd
+}
+
+func certAttachCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	certTachCompletion := completion.CertDomainTachCompletion{AppID: &app, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use: "attach <name> <domain>",
+		Example: template.CustomExample(
+			"drycc certs attach cert-name id.example.com",
+			map[string]string{
+				"<name>":   i18n.T("Name of the certificate to attach domain to"),
+				"<domain>": i18n.T("Common name of the domain to attach to (needs to already be in the system)"),
+			},
+		),
+		Args:              cobra.ExactArgs(2),
+		Short:             i18n.T("Attach an SSL certificate to a domain"),
+		Long:              i18n.T("Attach a certificate to a domain"),
+		ValidArgsFunction: certTachCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			name := args[0]
+			domain := args[1]
+			return cmdr.CertAttach(app, name, domain)
+		},
+	}
+
+	return cmd
+}
+
+func certDetachCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	certTachCompletion := completion.CertDomainTachCompletion{AppID: &app, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use: "detach <name> <domain>",
+		Example: template.CustomExample(
+			"drycc certs attach cert-name id.example.com",
+			map[string]string{
+				"<name>":   i18n.T("Name of the certificate to deatch from a domain"),
+				"<domain>": i18n.T("Common name of the domain to detach from"),
+			},
+		),
+		Args:              cobra.ExactArgs(2),
+		Short:             i18n.T("Detach an SSL certificate from a domain"),
+		Long:              i18n.T("Detach certificate from a domain"),
+		ValidArgsFunction: certTachCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			name := args[0]
+			domain := args[1]
+			return cmdr.CertDetach(app, name, domain)
+		},
+	}
+	return cmd
+}
diff --git a/internal/parser/config.go b/internal/parser/config.go
new file mode 100644
index 000000000..6ad64553c
--- /dev/null
+++ b/internal/parser/config.go
@@ -0,0 +1,259 @@
+package parser
+
+import (
+	"strings"
+
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/internal/template"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+var configFlags struct {
+	ptype string
+	group string
+}
+
+// NewConfigCommand creates the config command
+func NewConfigCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cobra.EnableCommandSorting = false
+	cmd := &cobra.Command{
+		Use:   "config",
+		Short: i18n.T("Manage environment variables that define app config"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.ConfigInfo(app, configFlags.ptype, configFlags.group, version)
+		},
+	}
+
+	cmd.PersistentFlags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+
+	cmd.Flags().StringVarP(&configFlags.ptype, "ptype", "p", "", i18n.T("The ptype for which the config needs to be listed"))
+	cmd.Flags().StringVarP(&configFlags.group, "group", "g", "", i18n.T("The group for which the config needs to be listed"))
+	cmd.Flags().IntVarP(&version, "version", "v", 0, i18n.T("The version for which the config needs to be listed"))
+
+	ptsCompletion := completion.PtsCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("ptype", ptsCompletion.CompletionFunc)
+	configGroupCompletion := completion.ConfigGroupCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("group", configGroupCompletion.CompletionFunc)
+	releaseCompletion := completion.ReleaseCompletion{ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("version", releaseCompletion.CompletionFunc)
+
+	cmd.AddCommand(configInfoCommand(cmdr))
+	cmd.AddCommand(configSetCommand(cmdr))
+	cmd.AddCommand(configUnsetCommand(cmdr))
+	cmd.AddCommand(configPullCommand(cmdr))
+	cmd.AddCommand(configPushCommand(cmdr))
+	cmd.AddCommand(configAttachCommand(cmdr))
+	cmd.AddCommand(configDetachCommand(cmdr))
+	return cmd
+}
+
+func configInfoCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "info",
+		Short: i18n.T("An app config info"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.ConfigInfo(app, configFlags.ptype, configFlags.group, version)
+		},
+	}
+
+	cmd.Flags().StringVarP(&configFlags.ptype, "ptype", "p", "", i18n.T("The ptype for which the config needs to be listed"))
+	cmd.Flags().StringVarP(&configFlags.group, "group", "g", "", i18n.T("The group for which the config needs to be listed"))
+	cmd.Flags().IntVarP(&version, "version", "v", 0, i18n.T("The version for which the config needs to be listed"))
+
+	ptsCompletion := completion.PtsCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("ptype", ptsCompletion.CompletionFunc)
+	configGroupCompletion := completion.ConfigGroupCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("group", configGroupCompletion.CompletionFunc)
+	releaseCompletion := completion.ReleaseCompletion{ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("version", releaseCompletion.CompletionFunc)
+	return cmd
+}
+
+func configSetCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		group   string
+		confirm string
+	}
+
+	cmd := &cobra.Command{
+		Use:   "set <key>=<value>...",
+		Args:  cobra.MinimumNArgs(1),
+		Short: i18n.T("Set environment variables for an app"),
+		Long:  i18n.T("Sets environment variables for an application or config group"),
+		RunE: func(_ *cobra.Command, args []string) error {
+			return cmdr.ConfigSet(app, configFlags.ptype, configFlags.group, args, true, flags.confirm)
+		},
+	}
+
+	cmd.Flags().StringVarP(&configFlags.ptype, "ptype", "p", "", i18n.T("The ptype for which the config needs to be set"))
+	cmd.Flags().StringVarP(&configFlags.group, "group", "g", "", i18n.T("The group for which the config needs to be set"))
+	cmd.Flags().StringVarP(&flags.confirm, "confirm", "", "", i18n.T("To proceed, type 'yes'"))
+	cmd.Flags().SortFlags = false
+	cmd.MarkFlagsMutuallyExclusive("ptype", "group")
+
+	ptsCompletion := completion.PtsCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("ptype", ptsCompletion.CompletionFunc)
+	configGroupCompletion := completion.ConfigGroupCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("group", configGroupCompletion.CompletionFunc)
+
+	return cmd
+}
+
+func configUnsetCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		confirm string
+	}
+
+	cmd := &cobra.Command{
+		Use:   "unset <key>...",
+		Args:  cobra.MinimumNArgs(1),
+		Short: i18n.T("Unset environment variables for an app"),
+		Long:  i18n.T("Unsets an environment variable for an application or config group"),
+		RunE: func(_ *cobra.Command, args []string) error {
+			return cmdr.ConfigUnset(app, configFlags.ptype, configFlags.group, args, flags.confirm)
+		},
+	}
+
+	cmd.Flags().StringVarP(&configFlags.ptype, "ptype", "p", "", i18n.T("The ptype for which the config needs to be unset"))
+	cmd.Flags().StringVarP(&configFlags.group, "group", "g", "", i18n.T("The group for which the config needs to be unset"))
+	cmd.Flags().StringVarP(&flags.confirm, "confirm", "", "", i18n.T("To proceed, type 'yes'"))
+	cmd.Flags().SortFlags = false
+	cmd.MarkFlagsMutuallyExclusive("ptype", "group")
+
+	ptsCompletion := completion.PtsCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("ptype", ptsCompletion.CompletionFunc)
+	configGroupCompletion := completion.ConfigGroupCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("group", configGroupCompletion.CompletionFunc)
+
+	return cmd
+}
+
+func configPullCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		path        string
+		interactive bool
+		overwrite   bool
+	}
+
+	cmd := &cobra.Command{
+		Use:   "pull",
+		Short: i18n.T("Pull environment variables to the path"),
+		Long: i18n.T(`Extract all environment variables from an application or config group. for local use.
+
+The environmental variables can be piped into a file, 'drycc config pull > file',
+or stored locally in a file named .env. This file can be
+read by foreman to load the local environment for your app.`),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.ConfigPull(app, configFlags.ptype, configFlags.group, flags.path, flags.interactive, flags.overwrite)
+		},
+	}
+
+	cmd.Flags().StringVarP(&configFlags.ptype, "ptype", "p", "", i18n.T("The ptype for which the config needs to be pull"))
+	cmd.Flags().IntVarP(&version, "version", "v", 0, i18n.T("The version for which the config needs to be pull"))
+	cmd.Flags().StringVarP(&configFlags.group, "group", "g", "", i18n.T("The group for which the config needs to be pull"))
+	cmd.Flags().StringVar(&flags.path, "path", ".env", i18n.T("A path leading to an environment file"))
+	cmd.Flags().BoolVarP(&flags.interactive, "interactive", "i", false, i18n.T("Prompts for each value to be overwritten"))
+	cmd.Flags().BoolVarP(&flags.overwrite, "overwrite", "o", false, i18n.T("Allows you to have the pull overwrite keys to the path"))
+	cmd.Flags().SortFlags = false
+	cmd.MarkFlagsMutuallyExclusive("ptype", "group")
+
+	ptsCompletion := completion.PtsCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("ptype", ptsCompletion.CompletionFunc)
+	configGroupCompletion := completion.ConfigGroupCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("group", configGroupCompletion.CompletionFunc)
+	releaseCompletion := completion.ReleaseCompletion{ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("version", releaseCompletion.CompletionFunc)
+
+	return cmd
+}
+
+func configPushCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		path    string
+		merge   bool
+		confirm string
+	}
+
+	cmd := &cobra.Command{
+		Use:   "push",
+		Short: i18n.T("Push environment variables from the path"),
+		Long: i18n.T(`Sets environment variables for an application or config group.
+
+This file can be read by foreman
+to load the local environment for your app. The file should be piped via
+stdin, 'drycc config push < .env', or using the --path option.`),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.ConfigPush(app, configFlags.ptype, configFlags.group, flags.path, flags.merge, flags.confirm)
+		},
+	}
+
+	cmd.Flags().StringVarP(&configFlags.ptype, "ptype", "p", "", i18n.T("The ptype for which the config needs to be push"))
+	cmd.Flags().StringVarP(&configFlags.group, "group", "g", "", i18n.T("The group for which the config needs to be push"))
+	cmd.Flags().StringVar(&flags.path, "path", ".env", i18n.T("A path leading to an environment file"))
+	cmd.Flags().BoolVarP(&flags.merge, "merge", "", false, i18n.T("Merge config values"))
+	cmd.Flags().StringVar(&flags.confirm, "confirm", "", i18n.T("To proceed, type 'yes'"))
+	cmd.Flags().SortFlags = false
+	cmd.MarkFlagsMutuallyExclusive("ptype", "group")
+
+	ptypeCompletion := completion.PtsCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("ptype", ptypeCompletion.CompletionFunc)
+	configGroupCompletion := completion.ConfigGroupCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("group", configGroupCompletion.CompletionFunc)
+	return cmd
+}
+
+func configAttachCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	configPtsGroupArgsCompletion := completion.ConfigPtsGroupArgsCompletion{
+		AppID:      &app,
+		ConfigFile: &cmdr.ConfigFile,
+	}
+	cmd := &cobra.Command{
+		Use: "attach <ptype> <group>...",
+		Example: template.CustomExample(
+			"drycc config attach web group1 group2",
+			map[string]string{
+				"<ptype>": i18n.T("The ptype that requires attach configurations"),
+				"<group>": i18n.T("The group that requires attach configurations"),
+			},
+		),
+		Args:              cobra.MinimumNArgs(2),
+		ValidArgsFunction: configPtsGroupArgsCompletion.CompletionFunc,
+		Short:             i18n.T("Selects environment groups to attach an app ptype"),
+		RunE: func(_ *cobra.Command, args []string) error {
+			configFlags.ptype = args[0]
+			return cmdr.ConfigAttach(app, configFlags.ptype, strings.Join(args[1:], ","))
+		},
+	}
+
+	return cmd
+}
+
+func configDetachCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	configPtsGroupArgsCompletion := completion.ConfigPtsGroupArgsCompletion{
+		AppID:      &app,
+		ConfigFile: &cmdr.ConfigFile,
+	}
+	cmd := &cobra.Command{
+		Use:  "detach <ptype> <group>...",
+		Args: cobra.MinimumNArgs(2),
+		Example: template.CustomExample(
+			"drycc config detach web group1 group2",
+			map[string]string{
+				"<ptype>": i18n.T("The ptype that requires detach configurations"),
+				"<group>": i18n.T("The group that requires detach configurations"),
+			},
+		),
+		Short:             i18n.T("Selects environment groups to detach an app ptype"),
+		ValidArgsFunction: configPtsGroupArgsCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			configFlags.ptype = args[0]
+			return cmdr.ConfigDetach(app, configFlags.ptype, strings.Join(args[1:], ","))
+		},
+	}
+
+	return cmd
+}
diff --git a/internal/parser/domains.go b/internal/parser/domains.go
new file mode 100644
index 000000000..716f6b598
--- /dev/null
+++ b/internal/parser/domains.go
@@ -0,0 +1,84 @@
+package parser
+
+import (
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewDomainsCommand creates the domains command
+func NewDomainsCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "domains",
+		Short: i18n.T("Manage and assign domain names to your applications"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			results, _ := commands.ResponseLimit(limit)
+			return cmdr.DomainsList(app, results)
+		},
+	}
+
+	cmd.PersistentFlags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("The maximum number of results to display"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+
+	cmd.AddCommand(domainsListCommand(cmdr))
+	cmd.AddCommand(domainsAddCommand(cmdr))
+	cmd.AddCommand(domainsRemoveCommand(cmdr))
+	return cmd
+}
+
+func domainsListCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "list",
+		Short: i18n.T("List domains bound to an application"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			results, _ := commands.ResponseLimit(limit)
+			return cmdr.DomainsList(app, results)
+		},
+	}
+
+	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("The maximum number of results to display"))
+
+	return cmd
+}
+
+func domainsAddCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		ptype string
+	}
+	cmd := &cobra.Command{
+		Use:   "add <domain>",
+		Args:  cobra.ExactArgs(1),
+		Short: i18n.T("Bind a domain to an application"),
+		RunE: func(_ *cobra.Command, args []string) error {
+			domain := args[0]
+			return cmdr.DomainsAdd(app, domain, flags.ptype)
+		},
+	}
+
+	cmd.Flags().StringVarP(&flags.ptype, "ptype", "p", "", i18n.T("The ptype for domain"))
+	cmd.MarkFlagRequired("ptype")
+
+	ptypeCompletion := completion.PtsCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("ptype", ptypeCompletion.CompletionFunc)
+	return cmd
+}
+
+func domainsRemoveCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	domainCompletion := completion.DomainCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:               "remove <domain>",
+		Args:              cobra.ExactArgs(1),
+		Short:             i18n.T("Unbinds a domain for an application"),
+		ValidArgsFunction: domainCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			domain := args[0]
+			return cmdr.DomainsRemove(app, domain)
+		},
+	}
+
+	return cmd
+}
diff --git a/internal/parser/flags.go b/internal/parser/flags.go
new file mode 100644
index 000000000..1aa3c4d4d
--- /dev/null
+++ b/internal/parser/flags.go
@@ -0,0 +1,7 @@
+package parser
+
+var (
+	app     string
+	version int
+	limit   int
+)
diff --git a/internal/parser/gateways.go b/internal/parser/gateways.go
new file mode 100644
index 000000000..cc518f04a
--- /dev/null
+++ b/internal/parser/gateways.go
@@ -0,0 +1,111 @@
+package parser
+
+import (
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewGatewaysCommand creates the gateways command
+func NewGatewaysCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "gateways",
+		Short: i18n.T("Manage gateways for your applications"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			results, _ := commands.ResponseLimit(limit)
+			return cmdr.GatewaysList(app, results)
+		},
+	}
+
+	cmd.PersistentFlags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("The maximum number of results to display"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+
+	cmd.AddCommand(gatewaysList(cmdr))
+	cmd.AddCommand(gatewaysAdd(cmdr))
+	cmd.AddCommand(gatewaysRemove(cmdr))
+	return cmd
+}
+
+func gatewaysList(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "list",
+		Short: i18n.T("List application gateways"),
+		Long:  i18n.T("Lists gateways for an application"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			results, _ := commands.ResponseLimit(limit)
+			return cmdr.GatewaysList(app, results)
+		},
+	}
+
+	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("The maximum number of results to display"))
+
+	return cmd
+}
+
+func gatewaysAdd(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		name     string
+		port     int
+		protocol string
+	}
+
+	cmd := &cobra.Command{
+		Use:   "add <name>",
+		Args:  cobra.ExactArgs(1),
+		Short: i18n.T("Create gateways for an application"),
+		Long:  i18n.T("Creates gateways for an application and binds it to allow listener of the main app domain"),
+		RunE: func(_ *cobra.Command, args []string) error {
+			name := args[0]
+			return cmdr.GatewaysAdd(app, name, flags.port, flags.protocol)
+		},
+	}
+
+	cmd.Flags().IntVar(&flags.port, "port", 0, i18n.T("Port is the network port, the listener expects to receive"))
+	cmd.Flags().StringVarP(&flags.protocol, "protocol", "", "", i18n.T("Protocol specifies the network protocol this listener expects to receive. Supports TCP, UDP, TLS, HTTP, and HTTPS"))
+
+	mustFlags := []string{"port", "protocol"}
+	for _, mustFlag := range mustFlags {
+		cmd.MarkFlagRequired(mustFlag)
+	}
+
+	gatewayProtocolCompletion := completion.GatewayProtocolCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("protocol", gatewayProtocolCompletion.CompletionFunc)
+	return cmd
+}
+
+func gatewaysRemove(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		name     string
+		port     int
+		protocol string
+	}
+
+	gatewayNameCompletion := completion.GatewayNameCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:               "remove <name> --port=<port> --protocol=<protocol>",
+		Args:              cobra.ExactArgs(1),
+		Short:             i18n.T("Remove gateways from an application"),
+		Long:              i18n.T("Deletes specific gateway for application"),
+		ValidArgsFunction: gatewayNameCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			name := args[0]
+			return cmdr.GatewaysRemove(app, name, flags.port, flags.protocol)
+		},
+	}
+
+	cmd.Flags().IntVar(&flags.port, "port", 0, i18n.T("Port is the network port, the listener received"))
+	cmd.Flags().StringVarP(&flags.protocol, "protocol", "", "", i18n.T("Protocol specifies the network protocol this listener received"))
+
+	mustFlags := []string{"port", "protocol"}
+	for _, mustFlag := range mustFlags {
+		cmd.MarkFlagRequired(mustFlag)
+	}
+
+	gatewayProtocolCompletion := completion.GatewayProtocolCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("protocol", gatewayProtocolCompletion.CompletionFunc)
+	return cmd
+}
diff --git a/internal/parser/git.go b/internal/parser/git.go
new file mode 100644
index 000000000..a0698bd01
--- /dev/null
+++ b/internal/parser/git.go
@@ -0,0 +1,59 @@
+package parser
+
+import (
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewGitCommand creates a command for managing git repositories.
+func NewGitCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "git",
+		Short: i18n.T("Manage git for applications"),
+		RunE:  nil,
+	}
+
+	cmd.PersistentFlags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+
+	cmd.AddCommand(gitRemote(cmdr))
+	cmd.AddCommand(gitRemove(cmdr))
+	return cmd
+}
+
+func gitRemote(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		app    string
+		remote string
+		force  bool
+	}
+
+	cmd := &cobra.Command{
+		Use:   "remote",
+		Short: i18n.T("Adds git remote of application to repository"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.GitRemote(app, flags.remote, flags.force)
+		},
+	}
+
+	cmd.Flags().StringVarP(&flags.remote, "remote", "r", "drycc", i18n.T("Name of remote to create"))
+	cmd.Flags().BoolVarP(&flags.force, "force", "f", false, i18n.T("Overwrite remote of the given name if it already exists"))
+	cmd.Flags().SortFlags = false
+
+	return cmd
+}
+
+func gitRemove(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "remove",
+		Short: i18n.T("Removes git remote of application from repository"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.GitRemove(app)
+		},
+	}
+
+	return cmd
+}
diff --git a/internal/parser/healthchecks.go b/internal/parser/healthchecks.go
new file mode 100644
index 000000000..104fd9e46
--- /dev/null
+++ b/internal/parser/healthchecks.go
@@ -0,0 +1,248 @@
+package parser
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/internal/template"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+var healthchecksFlags struct {
+	ptype string
+}
+
+// NewHealthchecksCommand creates a command for managing application healthchecks.
+func NewHealthchecksCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "healthchecks",
+		Short: i18n.T("Manage application healthchecks"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.HealthchecksList(app, healthchecksFlags.ptype, version)
+		},
+	}
+
+	cmd.PersistentFlags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name of the application"))
+	cmd.PersistentFlags().StringVarP(&healthchecksFlags.ptype, "ptype", "p", "", i18n.T("The ptype for which the health check needs to be listed"))
+	cmd.Flags().IntVarP(&version, "version", "v", 0, i18n.T("The version for which the health check needs to be listed"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+	ptypeCompletion := completion.PtsCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("ptype", ptypeCompletion.CompletionFunc)
+	releaseCompletion := completion.ReleaseCompletion{ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("version", releaseCompletion.CompletionFunc)
+
+	cmd.AddCommand(healthchecksList(cmdr))
+	cmd.AddCommand(healthchecksSet(cmdr))
+	cmd.AddCommand(healthchecksUnset(cmdr))
+	return cmd
+}
+
+func healthchecksList(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "list",
+		Short: i18n.T("List healthchecks for an application"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.HealthchecksList(app, healthchecksFlags.ptype, version)
+		},
+	}
+
+	cmd.Flags().IntVarP(&version, "version", "v", 0, i18n.T("The version for which the health check needs to be listed"))
+
+	releaseCompletion := completion.ReleaseCompletion{ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("version", releaseCompletion.CompletionFunc)
+	return cmd
+}
+
+func healthchecksSet(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		ptype         string
+		path          string
+		port          int
+		headers       string
+		initialDelay  int
+		timeout       int
+		period        int
+		successThresh int
+		failureThresh int
+		commandArgs   []string
+	}
+
+	healthCheckCompletion := completion.HealthCheckCompletion{ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "set <probe> <action> [flags] [--] <args>...",
+		Args: cobra.MinimumNArgs(3),
+		Example: template.CustomExample(
+			"drycc healthchecks set readinessProbe httpGet --path=/health -- 8000",
+			map[string]string{
+				"<probe>":  i18n.T("The healthcheck probe, such as 'startupProbe' 'livenessProbe' or 'readinessProbe'"),
+				"<action>": i18n.T("the healthcheck probe type, such as 'httpGet', 'exec' or 'tcpSocket'"),
+				"<args>": i18n.T(`The arguments required for the healthcheck probe. 'exec', accepts a list of arguments;
+                  'httpGet' and 'tcpSocket' accept a port number.`),
+			},
+		),
+		Short: i18n.T("Set healthchecks for an application"),
+		Long: i18n.T(`Sets healthchecks for an application.
+
+By default, Workflow only checks that the application starts in their Container. A health
+check may be added by configuring a health check probe for the application. The health
+checks are implemented as Kubernetes Container Probes. A 'startupProbe' 'livenessProbe' 
+and a 'readinessProbe' can be configured, and each probe can be of type 'httpGet', 'exec' 
+or 'tcpSocket' depending on the type of probe the Container requires.
+
+A 'startupProbe' indicates whether the application within the container is started.
+All other probes are disabled if a startup probe is provided, until it succeeds.
+If the startup probe fails, the container is subjected to its restart policy.
+
+A 'livenessProbe' is useful for applications running for long periods of time, eventually
+transitioning to broken states and cannot recover except by restarting them.
+
+Other times, a 'readinessProbe' is useful when the Container is only temporarily unable
+to serve, and will recover on its own. In this case, if a Container fails its 'readinessProbe'
+, the Container will not be shut down, but rather the Container will stop receiving
+incoming requests.
+
+'httpGet' probes are just as it sounds: it performs a HTTP GET operation on the Container.
+A response code inside the 200-399 range is considered a pass. 'httpGet' probes accept a
+port number to perform the HTTP GET operation on the Container.
+
+'exec' probes run a command inside the Container to determine its health. An exit code of
+zero is considered a pass, while a non-zero status code is considered a fail. 'exec'
+probes accept a string of arguments to be run inside the Container.
+
+'tcpSocket' probes attempt to open a socket in the Container. The Container is only
+considered healthy if the check can establish a connection. 'tcpSocket' probes accept a
+port number to perform the socket connection on the Container.
+`),
+		ValidArgsFunction: healthCheckCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			healthType := args[0]
+			probeType := args[1]
+
+			probe := &api.ContainerProbe{
+				InitialDelaySeconds: flags.initialDelay,
+				TimeoutSeconds:      flags.timeout,
+				PeriodSeconds:       flags.period,
+				SuccessThreshold:    flags.successThresh,
+				FailureThreshold:    flags.failureThresh,
+			}
+
+			switch probeType {
+			case "httpGet":
+				headers := []string{}
+				if flags.headers != "" {
+					headers = strings.Split(flags.headers, ",")
+				}
+				parsedHeaders, err := parseHeaders(headers)
+				if err != nil {
+					return fmt.Errorf("could not parse headers: %s", err)
+				}
+
+				port, err := strconv.Atoi(args[2])
+				if err != nil {
+					return fmt.Errorf("could not parse port: %s", err)
+				}
+				probe.HTTPGet = &api.HTTPGetAction{
+					Path:        flags.path,
+					Port:        port,
+					HTTPHeaders: parsedHeaders,
+				}
+			case "exec":
+				probeArgs := args[2:]
+				probe.Exec = &api.ExecAction{
+					Command: probeArgs,
+				}
+			case "tcpSocket":
+				port, err := strconv.Atoi(args[2])
+				if err != nil {
+					return fmt.Errorf("could not parse port: %s", err)
+				}
+
+				probe.TCPSocket = &api.TCPSocketAction{
+					Port: port,
+				}
+			default:
+				return fmt.Errorf("invalid probe type %s, Must be one of: \"httpGet\", \"exec\", \"tcpSocket\"", probeType)
+			}
+
+			return cmdr.HealthchecksSet(app, healthType, healthchecksFlags.ptype, probe)
+		},
+	}
+
+	cmd.Flags().StringVarP(&healthchecksFlags.ptype, "ptype", "p", "", i18n.T("The ptype for which the health check needs to be applied"))
+	cmd.MarkFlagRequired("ptype")
+	ptypeCompletion := completion.PtsCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("ptype", ptypeCompletion.CompletionFunc)
+
+	cmd.Flags().StringVar(&flags.path, "path", "/", i18n.T("The relative URL path for 'httpGet' probes"))
+	cmd.Flags().StringVar(&flags.headers, "headers", "", i18n.T("The HTTP headers to send for 'httpGet' probes, separated by commas"))
+	cmd.Flags().IntVar(&flags.initialDelay, "initial-delay-timeout", 50, i18n.T("The initial delay timeout for the probe"))
+	cmd.Flags().IntVar(&flags.timeout, "timeout-seconds", 50, i18n.T("The number of seconds after which the probe times out"))
+	cmd.Flags().IntVar(&flags.period, "period-seconds", 10, i18n.T("How often (in seconds) to perform the probe"))
+	cmd.Flags().IntVar(&flags.successThresh, "success-threshold", 1, i18n.T("Minimum consecutive successes for the probe to be considered successful after having failed"))
+	cmd.Flags().IntVar(&flags.failureThresh, "failure-threshold", 3, i18n.T("Minimum consecutive successes for the probe to be considered failed after having succeeded"))
+	cmd.Flags().SortFlags = false
+
+	return cmd
+}
+
+func healthchecksUnset(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		app     string
+		ptype   string
+		healths []string
+	}
+
+	healthcheckProbeTypeCompletion := completion.HealthcheckProbeTypeCompletion{ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "unset <probe>...",
+		Args: cobra.MinimumNArgs(1),
+		Example: template.CustomExample(
+			"drycc healthchecks unset startupProbe",
+			map[string]string{
+				"<probe>": i18n.T("the healthcheck probe, such as 'startupProbe' 'livenessProbe' or 'readinessProbe'"),
+			},
+		),
+		Short:             i18n.T("Unset healthchecks for an application"),
+		ValidArgsFunction: healthcheckProbeTypeCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			flags.healths = args
+			for healthcheck := range flags.healths {
+				if err := checkProbeType(flags.healths[healthcheck]); err != nil {
+					return err
+				}
+			}
+			return cmdr.HealthchecksUnset(app, healthchecksFlags.ptype, flags.healths)
+		},
+	}
+	cmd.Flags().StringVarP(&healthchecksFlags.ptype, "ptype", "p", "", i18n.T("The ptype for which the health check needs to be removed"))
+	cmd.MarkFlagRequired("ptype")
+	ptypeCompletion := completion.PtsCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("ptype", ptypeCompletion.CompletionFunc)
+
+	return cmd
+}
+
+func checkProbeType(probe string) error {
+	var found bool
+	probeTypes := []string{
+		"startupProbe",
+		"livenessProbe",
+		"readinessProbe",
+	}
+	for _, probeType := range probeTypes {
+		if probe == probeType {
+			found = true
+		}
+	}
+	if !found {
+		return fmt.Errorf("probe type %s is invalid. Must be one of %s", probe, probeTypes)
+	}
+	return nil
+}
diff --git a/internal/parser/keys.go b/internal/parser/keys.go
new file mode 100644
index 000000000..51dd36826
--- /dev/null
+++ b/internal/parser/keys.go
@@ -0,0 +1,91 @@
+package parser
+
+import (
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/internal/template"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewKeysCommand creates a command for managing SSH keys.
+func NewKeysCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "keys",
+		Short: i18n.T("Manage ssh keys used for 'git push' deployments"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			results, _ := commands.ResponseLimit(limit)
+			return cmdr.KeysList(results)
+		},
+	}
+	cmd.AddCommand(keysListCommand(cmdr))
+	cmd.AddCommand(keyAddCommand(cmdr))
+	cmd.AddCommand(keyRemoveCommand(cmdr))
+	return cmd
+}
+
+func keysListCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "list",
+		Short: i18n.T("List SSH keys for the logged in user"),
+
+		RunE: func(_ *cobra.Command, _ []string) error {
+			results, _ := commands.ResponseLimit(limit)
+			return cmdr.KeysList(results)
+		},
+	}
+
+	return cmd
+}
+
+func keyAddCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		name string
+		key  string
+	}
+
+	cmd := &cobra.Command{
+		Use: "add [<name>] [<key>]",
+		Example: template.CustomExample(
+			"drycc keys add mykey /path/to/public.key",
+			map[string]string{
+				"<name>": i18n.T("Name of the SSH key"),
+				"<key>":  i18n.T("A local file path to an SSH public key used to push application code"),
+			},
+		),
+		Args:  cobra.MaximumNArgs(2),
+		Short: i18n.T("Add an SSH key"),
+		Long:  i18n.T("Adds SSH keys for the logged in user"),
+		RunE: func(_ *cobra.Command, args []string) error {
+			if len(args) == 1 {
+				flags.key = args[0]
+			} else if len(args) == 2 {
+				flags.name = args[0]
+				flags.key = args[1]
+			}
+			return cmdr.KeyAdd(flags.name, flags.key)
+		},
+	}
+
+	return cmd
+}
+
+func keyRemoveCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		key string
+	}
+	keyCompletion := completion.KeyCompletion{ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:               "remove <key>",
+		Args:              cobra.ExactArgs(1),
+		Short:             i18n.T("Remove an SSH key"),
+		Long:              i18n.T("Removes an SSH key for the logged in user"),
+		ValidArgsFunction: keyCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			flags.key = args[0]
+			return cmdr.KeyRemove(flags.key)
+		},
+	}
+
+	return cmd
+}
diff --git a/internal/parser/labels.go b/internal/parser/labels.go
new file mode 100644
index 000000000..e4c54c069
--- /dev/null
+++ b/internal/parser/labels.go
@@ -0,0 +1,100 @@
+package parser
+
+import (
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/internal/template"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewLabelsCommand creates a command for managing application labels.
+func NewLabelsCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "labels",
+		Short: i18n.T("Manage labels of application"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.LabelsList(app)
+		},
+	}
+
+	cmd.PersistentFlags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+
+	cmd.AddCommand(labelsListCommand(cmdr))
+	cmd.AddCommand(labelsSetCommand(cmdr))
+	cmd.AddCommand(labelsUnsetCommand(cmdr))
+	return cmd
+}
+
+func labelsListCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "list",
+		Short: i18n.T("List application's labels"),
+		Long:  i18n.T("Prints a list of labels of the application"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.LabelsList(app)
+		},
+	}
+
+	return cmd
+}
+
+func labelsSetCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		app  string
+		tags []string
+	}
+
+	cmd := &cobra.Command{
+		Use:  "set <key>=<value>...",
+		Args: cobra.MinimumNArgs(1),
+		Example: template.CustomExample(
+			"drycc labels set FOO=bar",
+			map[string]string{
+				"<key>":   i18n.T(`the label key, for example: "git_repo" or "team"`),
+				"<value>": i18n.T(`the label value, for example: "https://github.com/drycc/workflow" or "frontend"`),
+			},
+		),
+		Short: i18n.T("Add new application's label"),
+		Long: i18n.T(`Sets labels for an application.
+
+A label is a key/value pair used to label an application. This label is a general information for drycc user.
+Mostly used for administration/maintenance information, note for application. This information isn't send to scheduler.`),
+		RunE: func(_ *cobra.Command, args []string) error {
+			flags.tags = args
+			return cmdr.LabelsSet(app, flags.tags)
+		},
+	}
+
+	return cmd
+}
+
+func labelsUnsetCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		app  string
+		keys []string
+	}
+	labelCompletion := completion.LabelCompletion{AppID: &app, ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use: "unset <key>...",
+		Example: template.CustomExample(
+			"drycc labels unset FOO",
+			map[string]string{
+				"<key>": i18n.T(`the label key to unset, for example: "git_repo" or "team"`),
+			},
+		),
+		Args: cobra.MinimumNArgs(1),
+
+		Short:             i18n.T("Remove application's label"),
+		Long:              i18n.T("Unsets labels for an application"),
+		ValidArgsFunction: labelCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			flags.keys = args
+			return cmdr.LabelsUnset(app, flags.keys)
+		},
+	}
+
+	return cmd
+}
diff --git a/internal/parser/lifecycles.go b/internal/parser/lifecycles.go
new file mode 100644
index 000000000..4af203023
--- /dev/null
+++ b/internal/parser/lifecycles.go
@@ -0,0 +1,236 @@
+package parser
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/internal/template"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+var lifecycleFlags struct {
+	ptype string
+}
+
+// NewLifecyclesCommand creates a command for managing application lifecycle.
+func NewLifecyclesCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "lifecycles",
+		Short: i18n.T("Manage application lifecycles"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.LifecyclesList(app, lifecycleFlags.ptype, version)
+		},
+	}
+
+	cmd.PersistentFlags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name of the application"))
+	cmd.PersistentFlags().StringVarP(&lifecycleFlags.ptype, "ptype", "p", "", i18n.T("The ptype for which the lifecycle needs to be listed"))
+	cmd.Flags().IntVarP(&version, "version", "v", 0, i18n.T("The version for which the lifecycle needs to be listed"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+	ptypeCompletion := completion.PtsCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("ptype", ptypeCompletion.CompletionFunc)
+	releaseCompletion := completion.ReleaseCompletion{ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("version", releaseCompletion.CompletionFunc)
+
+	cmd.AddCommand(lifecyclesList(cmdr))
+	cmd.AddCommand(lifecyclesSet(cmdr))
+	cmd.AddCommand(lifecyclesUnset(cmdr))
+	return cmd
+}
+
+func lifecyclesList(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "list",
+		Short: i18n.T("List lifecycles for an application"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.LifecyclesList(app, lifecycleFlags.ptype, version)
+		},
+	}
+
+	cmd.Flags().IntVarP(&version, "version", "v", 0, i18n.T("The version for which the lifecycle needs to be listed"))
+	releaseCompletion := completion.ReleaseCompletion{ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("version", releaseCompletion.CompletionFunc)
+	return cmd
+}
+
+func lifecyclesSet(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		ptype      string
+		path       string
+		port       int
+		headers    string
+		stopSignal string
+	}
+
+	lifecycleCompletion := completion.LifecycleCompletion{ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "set <handler> <action> [flags] [--] <args>...",
+		Args: cobra.MinimumNArgs(3),
+		Example: template.CustomExample(
+			"drycc lifecycles set postStart httpGet --path=/health -- 8000",
+			map[string]string{
+				"<handler>": i18n.T("The lifecycle handler, such as 'postStart' 'preStop'"),
+				"<action>":  i18n.T("the lifecycle action type, such as 'httpGet', 'exec', 'sleep' or 'tcpSocket'"),
+				"<args>": i18n.T(`The arguments required for the lifecycle action. 'exec' accepts a list of arguments;
+                  'sleep' accepts duration in seconds; 'httpGet' and 'tcpSocket' accept a port number.`),
+			},
+		),
+		Short: i18n.T("Set lifecycles for an application"),
+		Long: i18n.T(`Sets lifecycle handlers for an application.
+
+Lifecycle handlers allow you to run actions at specific points in a container's lifecycle.
+Two types of lifecycle handlers are supported: 'postStart' and 'preStop'.
+
+A 'postStart' handler runs immediately after a container is created. If the handler fails,
+the container is terminated and restarted according to its restart policy.
+
+A 'preStop' handler runs immediately before a container is terminated. This handler must
+complete before the container termination signal is sent. If the handler fails, the
+container enters a termination grace period and is then forcefully terminated.
+
+Each lifecycle handler can be configured with one of the following action types:
+
+'httpGet': Performs an HTTP GET request to the container. The action is considered
+successful if the response status code is in the 200-399 range. 'httpGet' actions require
+a port number to specify where to send the HTTP request.
+
+'exec': Executes a command inside the container. The action is considered successful if
+the command exits with status code 0. 'exec' actions accept a list of command arguments
+to run inside the container.
+
+'tcpSocket': Attempts to open a TCP socket connection to the container. The action is
+considered successful if a connection can be established. 'tcpSocket' actions require a
+port number to specify where to attempt the connection.
+
+'sleep': Waits for a specified duration before proceeding. 'sleep' actions accept a
+duration in seconds to wait.
+`),
+		ValidArgsFunction: lifecycleCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			action := args[1]
+			lifecycleHandler := &api.LifecycleHandler{}
+
+			switch action {
+			case "httpGet":
+				headers := []string{}
+				if flags.headers != "" {
+					headers = strings.Split(flags.headers, ",")
+				}
+				parsedHeaders, err := parseHeaders(headers)
+				if err != nil {
+					return fmt.Errorf("could not parse headers: %s", err)
+				}
+
+				port, err := strconv.Atoi(args[2])
+				if err != nil {
+					return fmt.Errorf("could not parse port: %s", err)
+				}
+				lifecycleHandler.HTTPGet = &api.HTTPGetAction{
+					Path:        flags.path,
+					Port:        port,
+					HTTPHeaders: parsedHeaders,
+				}
+			case "exec":
+				commandArgs := args[2:]
+				lifecycleHandler.Exec = &api.ExecAction{
+					Command: commandArgs,
+				}
+			case "sleep":
+				seconds, err := strconv.Atoi(args[2])
+				if err != nil {
+					return fmt.Errorf("could not parse sleep duration: %s", err)
+				}
+				lifecycleHandler.Sleep = &api.SleepAction{
+					Seconds: seconds,
+				}
+			case "tcpSocket":
+				port, err := strconv.Atoi(args[2])
+				if err != nil {
+					return fmt.Errorf("could not parse port: %s", err)
+				}
+
+				lifecycleHandler.TCPSocket = &api.TCPSocketAction{
+					Port: port,
+				}
+			default:
+				return fmt.Errorf("invalid action type %s, Must be one of: \"httpGet\", \"sleep\", \"exec\", \"tcpSocket\"", action)
+			}
+			handler := args[0]
+			lifecycle := &api.Lifecycle{StopSignal: flags.stopSignal}
+			switch handler {
+			case "postStart":
+				lifecycle.PostStart = &lifecycleHandler
+			case "preStop":
+				lifecycle.PreStop = &lifecycleHandler
+			default:
+				return fmt.Errorf("lifecycle handler %s is invalid. Must be one of 'postStart' or 'preStop'", handler)
+			}
+
+			return cmdr.LifecyclesSet(app, flags.ptype, lifecycle)
+		},
+	}
+
+	cmd.Flags().StringVarP(&flags.ptype, "ptype", "p", "", i18n.T("The ptype for which the lifecycle handler needs to be applied"))
+	cmd.MarkFlagRequired("ptype")
+	ptypeCompletion := completion.PtsCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("ptype", ptypeCompletion.CompletionFunc)
+
+	cmd.Flags().StringVar(&flags.path, "path", "/", i18n.T("The relative URL path for 'httpGet' actions"))
+	cmd.Flags().StringVar(&flags.headers, "headers", "", i18n.T("The HTTP headers to send for 'httpGet' actions, separated by commas"))
+	cmd.Flags().StringVar(&flags.stopSignal, "stop-signal", "SIGTERM", i18n.T("The stop signal to send to the container"))
+	cmd.Flags().SortFlags = false
+
+	return cmd
+}
+
+func lifecyclesUnset(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		app        string
+		ptype      string
+		lifecycles []string
+	}
+
+	lifecycleHandlerCompletion := completion.LifecycleHandlerCompletion{ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "unset <handler>...",
+		Args: cobra.MinimumNArgs(1),
+		Example: template.CustomExample(
+			"drycc lifecycles unset postStart",
+			map[string]string{
+				"<handler>": i18n.T("the lifecycle handler type, such as 'postStart' or 'preStop'"),
+			},
+		),
+		Short:             i18n.T("Unset lifecycle handler for an application"),
+		ValidArgsFunction: lifecycleHandlerCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			flags.lifecycles = args
+			for lifecycle := range flags.lifecycles {
+				if err := checkLifecycleHandlerType(flags.lifecycles[lifecycle]); err != nil {
+					return err
+				}
+			}
+			return cmdr.LifecyclesUnset(app, flags.ptype, flags.lifecycles)
+		},
+	}
+	cmd.Flags().StringVarP(&flags.ptype, "ptype", "p", "", i18n.T("The ptype for which the lifecycle handler needs to be unset"))
+	cmd.MarkFlagRequired("ptype")
+	ptypeCompletion := completion.PtsCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("ptype", ptypeCompletion.CompletionFunc)
+
+	return cmd
+}
+
+func checkLifecycleHandlerType(lifecycleHandlerType string) error {
+	switch lifecycleHandlerType {
+	case "postStart", "preStop":
+		return nil
+	default:
+		return fmt.Errorf("lifecycle handler type %s is invalid. Must be one of 'postStart' or 'preStop'", lifecycleHandlerType)
+	}
+}
diff --git a/internal/parser/limits.go b/internal/parser/limits.go
new file mode 100644
index 000000000..58badbed9
--- /dev/null
+++ b/internal/parser/limits.go
@@ -0,0 +1,158 @@
+package parser
+
+import (
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/internal/template"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewLimitsCommand creates a command for managing resource limits.
+func NewLimitsCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "limits",
+		Short: i18n.T("Manage resource limits for your application"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.LimitsList(app, version)
+		},
+	}
+
+	cmd.PersistentFlags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+	cmd.Flags().IntVarP(&version, "version", "v", 0, i18n.T("The version for which the limit needs to be listed"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+	releaseCompletion := completion.ReleaseCompletion{ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("version", releaseCompletion.CompletionFunc)
+
+	cmd.AddCommand(limitsListCommand(cmdr))
+	cmd.AddCommand(limitSetCommand(cmdr))
+	cmd.AddCommand(limitUnsetCommand(cmdr))
+	cmd.AddCommand(limitSpecsCommand(cmdr))
+	cmd.AddCommand(limitPlansCommand(cmdr))
+	return cmd
+}
+
+func limitsListCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:     "list",
+		Example: "drycc limits list",
+		Short:   i18n.T("List resource limits for an app"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.LimitsList(app, version)
+		},
+	}
+
+	cmd.Flags().IntVarP(&version, "version", "v", 0, i18n.T("The version for which the limit needs to be listed"))
+
+	releaseCompletion := completion.ReleaseCompletion{ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("version", releaseCompletion.CompletionFunc)
+
+	return cmd
+}
+
+func limitSetCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		limits []string
+	}
+	limitSetPlanCompletion := completion.LimitSetPlanCompletion{AppID: &app, ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "set <ptype>=<value>...",
+		Args: cobra.MinimumNArgs(1),
+		Example: template.CustomExample(
+			"drycc limits set web=std1.large.c1m2",
+			map[string]string{
+				"<ptype>": i18n.T("The process type as defined in your Procfile, such as 'web' or 'worker'"),
+				"<value>": i18n.T("The limit plan id to apply to the process type"),
+			},
+		),
+		Short: i18n.T("Set resource limits for an app"),
+		Long: i18n.T(`Sets resource limits for an application.
+
+A resource limit is a finite resource within a pod which we can apply
+restrictions through Kubernetes.`),
+		ValidArgsFunction: limitSetPlanCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			flags.limits = args
+			return cmdr.LimitsSet(app, flags.limits)
+		},
+	}
+
+	return cmd
+}
+
+func limitUnsetCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		limits []string
+	}
+	ptsCompletion := completion.PtsCompletion{AppID: &app, ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "unset <ptype>...",
+		Args: cobra.MinimumNArgs(1),
+		Example: template.CustomExample(
+			"drycc limits unset web",
+			map[string]string{
+				"<ptype>": i18n.T("The process type as defined in your Procfile, such as 'web' or 'worker'"),
+			},
+		),
+		Short:             i18n.T("Unset resource limits for an app"),
+		ValidArgsFunction: ptsCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			flags.limits = args
+			return cmdr.LimitsUnset(app, flags.limits)
+		},
+	}
+
+	return cmd
+}
+
+func limitSpecsCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		keywords string
+		limit    int
+	}
+
+	cmd := &cobra.Command{
+		Use:   "specs",
+		Short: i18n.T("List specification information of the server"),
+		Long:  i18n.T("List all available limit specs"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			results, _ := commands.ResponseLimit(limit)
+			return cmdr.LimitsSpecs(flags.keywords, results)
+		},
+	}
+
+	cmd.Flags().StringVarP(&flags.keywords, "keywords", "k", "", i18n.T("Search keywords separated by commas, matching must satisfy all of the specified."))
+	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("The maximum number of results to display"))
+	return cmd
+}
+
+func limitPlansCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		specID string
+		cpu    int
+		memory int
+		limit  int
+	}
+
+	cmd := &cobra.Command{
+		Use:   "plans",
+		Short: i18n.T("List all available limit plans"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			results, _ := commands.ResponseLimit(limit)
+			return cmdr.LimitsPlans(flags.specID, flags.cpu, flags.memory, results)
+		},
+	}
+
+	cmd.Flags().IntVar(&flags.cpu, "cpu", 0, i18n.T("Query plans that meet the specified number of cpu cores"))
+	cmd.Flags().IntVarP(&flags.memory, "memory", "m", 0, i18n.T("Query plans that meet the specified memory capacity, unit GiB"))
+	cmd.Flags().StringVar(&flags.specID, "spec-id", "", i18n.T("Query plans that meet the specified spec id, see [specs] subcommand."))
+	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("The maximum number of results to display"))
+	cmd.Flags().SortFlags = false
+
+	completion := completion.LimitSpecCompletion{ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("spec-id", completion.CompletionFunc)
+
+	return cmd
+}
diff --git a/internal/parser/parse.go b/internal/parser/parse.go
new file mode 100644
index 000000000..32f250ec4
--- /dev/null
+++ b/internal/parser/parse.go
@@ -0,0 +1,31 @@
+package parser
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/drycc/controller-sdk-go/api"
+)
+
+func parseHeaders(headers []string) ([]*api.KVPair, error) {
+	var parsedHeaders []*api.KVPair
+	for _, header := range headers {
+		parsedHeader, err := parseHeader(header)
+		if err != nil {
+			return nil, err
+		}
+		parsedHeaders = append(parsedHeaders, parsedHeader)
+	}
+	return parsedHeaders, nil
+}
+
+func parseHeader(header string) (*api.KVPair, error) {
+	headerParts := strings.SplitN(header, ":", 2)
+	if len(headerParts) != 2 {
+		return nil, fmt.Errorf("could not find separator in header (%s)", header)
+	}
+	return &api.KVPair{
+		Name:  strings.TrimSpace(headerParts[0]),
+		Value: strings.TrimSpace(headerParts[1]),
+	}, nil
+}
diff --git a/internal/parser/ps.go b/internal/parser/ps.go
new file mode 100644
index 000000000..738b87d71
--- /dev/null
+++ b/internal/parser/ps.go
@@ -0,0 +1,163 @@
+package parser
+
+import (
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/internal/template"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewPsCommand creates a command for managing processes.
+func NewPsCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "ps",
+		Short: i18n.T("Manage processes inside an app"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.PsList(app, 1000)
+		},
+	}
+
+	cmd.PersistentFlags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+
+	cmd.AddCommand(psListCommand(cmdr))
+	cmd.AddCommand(psLogsCommand(cmdr))
+	cmd.AddCommand(psExecCommand(cmdr))
+	cmd.AddCommand(psDescribeCommand(cmdr))
+	cmd.AddCommand(psDeleteCommand(cmdr))
+	return cmd
+}
+
+func psListCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "list",
+		Short: i18n.T("List application pods"),
+		Long:  i18n.T("Lists processes servicing an application"),
+		Args:  cobra.NoArgs,
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.PsList(app, 1000)
+		},
+	}
+
+	return cmd
+}
+
+func psLogsCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		app       string
+		lines     int
+		follow    bool
+		container string
+		previous  bool
+	}
+
+	psCompletion := completion.PsCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+
+	cmd := &cobra.Command{
+		Use:               "logs <pod>",
+		Args:              cobra.ExactArgs(1),
+		Example:           "drycc ps logs my-pod",
+		Short:             i18n.T("Print the logs for a container"),
+		Long:              i18n.T("Print the logs for a container in a pod or specified resource"),
+		ValidArgsFunction: psCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			podID := args[0]
+			if flags.lines < 0 {
+				flags.lines = -1
+			}
+			return cmdr.PsLogs(app, podID, flags.lines, flags.follow, flags.container, flags.previous)
+		},
+	}
+
+	cmd.Flags().IntVarP(&flags.lines, "lines", "l", 300, i18n.T("The number of lines to display, -1 showing all log lines"))
+	cmd.Flags().BoolVarP(&flags.follow, "follow", "f", false, i18n.T("Specify if the logs should be streamed"))
+	cmd.Flags().StringVar(&flags.container, "container", "", i18n.T("Print the logs of this container"))
+	cmd.Flags().BoolVarP(&flags.previous, "previous", "p", false, i18n.T("Print the logs for the previous instance of the container in a pod if it exists"))
+	cmd.Flags().SortFlags = false
+
+	return cmd
+}
+
+func psExecCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		app     string
+		pod     string
+		command []string
+		tty     bool
+		stdin   bool
+	}
+	psCompletion := completion.PsCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "exec <pod> [flags] -- <command>...",
+		Args: cobra.MinimumNArgs(1),
+		Example: template.CustomExample(
+			"drycc ps exec my-pod -it -- bash",
+			map[string]string{
+				"<pod>": i18n.T("The pod name for the application"),
+			},
+		),
+		Short:             i18n.T("Execute a command in a container"),
+		ValidArgsFunction: psCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			flags.pod = args[0]
+			flags.command = args[1:]
+			return cmdr.PsExec(app, flags.pod, flags.tty, flags.stdin, flags.command)
+		},
+	}
+
+	// shortcuts exec has not app flag
+	cmd.Flags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+	cmd.Flags().BoolVarP(&flags.tty, "tty", "t", false, i18n.T("Stdin is a TTY"))
+	cmd.Flags().BoolVarP(&flags.stdin, "stdin", "i", false, i18n.T("Pass stdin to the container"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+
+	return cmd
+}
+
+func psDescribeCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	psCompletion := completion.PsCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "describe <pod>",
+		Args: cobra.ExactArgs(1),
+		Example: template.CustomExample(
+			"drycc ps describe my-pod",
+			map[string]string{
+				"<pod>": i18n.T("The pod name for the application"),
+			},
+		),
+		Short:             i18n.T("Print a detailed description of the selected process"),
+		ValidArgsFunction: psCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			podID := args[0]
+			return cmdr.PsDescribe(app, podID)
+		},
+	}
+
+	return cmd
+}
+
+func psDeleteCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	psCompletion := completion.PsCompletion{AppID: &app, ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "delete <pod>...",
+		Args: cobra.MinimumNArgs(1),
+		Example: template.CustomExample(
+			"drycc ps delete my-pod another-pod",
+			map[string]string{
+				"<pod>": i18n.T("The pod name for the application"),
+			},
+		),
+		Short:             i18n.T("Delete the selected processes"),
+		ValidArgsFunction: psCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			return cmdr.PsDelete(app, args)
+		},
+	}
+
+	return cmd
+}
diff --git a/internal/parser/pts.go b/internal/parser/pts.go
new file mode 100644
index 000000000..863f1143e
--- /dev/null
+++ b/internal/parser/pts.go
@@ -0,0 +1,169 @@
+package parser
+
+import (
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/internal/template"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewPtsCommand creates a command for managing process types.
+func NewPtsCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "pts",
+		Short: i18n.T("Manage process types inside an app"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.PtsList(app, 1000)
+		},
+	}
+
+	cmd.PersistentFlags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+
+	cmd.AddCommand(ptsListCommand(cmdr))
+	cmd.AddCommand(ptsDescribeCommand(cmdr))
+	cmd.AddCommand(ptsRestartCommand(cmdr))
+	cmd.AddCommand(ptsScaleCommand(cmdr))
+	cmd.AddCommand(ptsCleanCommand(cmdr))
+	return cmd
+}
+
+// ptsListCommand
+func ptsListCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "list",
+		Short: i18n.T("List application process types"),
+		Long:  i18n.T("Lists process types servicing an application"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.PtsList(app, 1000)
+		},
+	}
+
+	return cmd
+}
+
+// ptsDescribeCommand
+func ptsDescribeCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		ptype string
+	}
+	ptsArgsCompletion := completion.PtsArgsCompletion{
+		PtsCompletion: &completion.PtsCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile},
+	}
+	cmd := &cobra.Command{
+		Use:  "describe <ptype>",
+		Args: cobra.ExactArgs(1),
+		Example: template.CustomExample(
+			"drycc pts describe web",
+			map[string]string{
+				"<ptype>": i18n.T("The process name as defined in your Procfile"),
+			},
+		),
+		Short:             i18n.T("Describe a process type"),
+		Long:              i18n.T("Print a detailed description of the selected process type"),
+		ValidArgsFunction: ptsArgsCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			flags.ptype = args[0]
+			return cmdr.PtsDescribe(app, flags.ptype)
+		},
+	}
+
+	return cmd
+}
+
+// ptsRestartCommand
+func ptsRestartCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		ptypes  []string
+		confirm string
+	}
+	ptsArgsCompletion := completion.PtsArgsCompletion{
+		PtsCompletion: &completion.PtsCompletion{AppID: &app, ArgsLen: -1, ConfigFile: &cmdr.ConfigFile},
+	}
+	cmd := &cobra.Command{
+		Use:               "restart [<ptype>...]",
+		Args:              cobra.ArbitraryArgs,
+		Example:           "drycc pts restart web",
+		Short:             i18n.T("Restart application process types"),
+		Long:              i18n.T("Restart an application or process types"),
+		ValidArgsFunction: ptsArgsCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			flags.ptypes = args
+			return cmdr.PtsRestart(app, flags.ptypes, flags.confirm)
+		},
+	}
+
+	cmd.Flags().StringVar(&flags.confirm, "confirm", "", i18n.T(`To proceed, type "yes"`))
+
+	return cmd
+}
+
+// ptsScaleCommand
+func ptsScaleCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		app   string
+		scale []string // format like "web=5"
+	}
+
+	ptsSetArgsCompletion := completion.PtsSetArgsCompletion{
+		PtsCompletion: &completion.PtsCompletion{AppID: &app, ArgsLen: -1, ConfigFile: &cmdr.ConfigFile},
+	}
+	cmd := &cobra.Command{
+		Use:  "scale <ptype>=<num>...",
+		Args: cobra.MinimumNArgs(1),
+		Example: template.CustomExample(
+			"drycc pts scale web=3",
+			map[string]string{
+				"<ptype>": i18n.T("The process name as defined in your Procfile"),
+				"<num>":   i18n.T("The number of processes"),
+			},
+		),
+		Short:             i18n.T("Scale process types of replicas"),
+		Long:              i18n.T("Scales an application's processes by type"),
+		ValidArgsFunction: ptsSetArgsCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			flags.scale = args
+			return cmdr.PtsScale(app, flags.scale)
+		},
+	}
+
+	// shortcuts scale has not app flag
+	cmd.Flags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+
+	return cmd
+}
+
+// ptsCleanCommand
+func ptsCleanCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		ptypes []string
+	}
+
+	ptsArgsCompletion := completion.PtsArgsCompletion{
+		PtsCompletion: &completion.PtsCompletion{AppID: &app, ArgsLen: -1, ConfigFile: &cmdr.ConfigFile},
+	}
+	cmd := &cobra.Command{
+		Use:  "clean <ptype>...",
+		Args: cobra.MinimumNArgs(1),
+		Example: template.CustomExample(
+			"drycc pts clean web",
+			map[string]string{
+				"<ptype>": i18n.T("The process name as defined in your Procfile"),
+			},
+		),
+		Short:             i18n.T("Clean process types of not used"),
+		ValidArgsFunction: ptsArgsCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			flags.ptypes = args
+			return cmdr.PtsClean(app, flags.ptypes)
+		},
+	}
+
+	return cmd
+}
diff --git a/internal/parser/registry.go b/internal/parser/registry.go
new file mode 100644
index 000000000..a77c32156
--- /dev/null
+++ b/internal/parser/registry.go
@@ -0,0 +1,114 @@
+package parser
+
+import (
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/internal/template"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// no default web
+var registryFlags struct {
+	ptype string
+}
+
+// NewRegistryCommand creates a command for managing registry information.
+func NewRegistryCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "registry",
+		Short: i18n.T("Manage private registry information for your application"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.RegistryList(app, registryFlags.ptype, version)
+		},
+	}
+
+	cmd.PersistentFlags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+	cmd.Flags().StringVarP(&registryFlags.ptype, "ptype", "p", "", i18n.T("The ptype for registry"))
+	cmd.Flags().IntVarP(&version, "version", "v", 0, i18n.T("The version for which the registry needs to be listed"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+	ptypeCompletion := completion.PtsCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("ptype", ptypeCompletion.CompletionFunc)
+
+	cmd.AddCommand(registryListCommand(cmdr))
+	cmd.AddCommand(registrySetCommand(cmdr))
+	cmd.AddCommand(registryUnsetCommand(cmdr))
+	return cmd
+}
+
+func registryListCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "list",
+		Short: i18n.T("List registry info for an app"),
+		Long:  i18n.T("Lists registry information for an application"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.RegistryList(app, registryFlags.ptype, version)
+		},
+	}
+
+	cmd.Flags().StringVarP(&registryFlags.ptype, "ptype", "p", "", i18n.T("The ptype for registry"))
+	cmd.Flags().IntVarP(&version, "version", "v", 0, i18n.T("The version for which the registry needs to be listed"))
+
+	ptypeCompletion := completion.PtsCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("ptype", ptypeCompletion.CompletionFunc)
+	releaseCompletion := completion.ReleaseCompletion{ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("version", releaseCompletion.CompletionFunc)
+	return cmd
+}
+
+func registrySetCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	// default web
+	var flags struct {
+		ptype string
+	}
+	cmd := &cobra.Command{
+		Use:  "set <username> <password>",
+		Args: cobra.ExactArgs(2),
+		Example: template.CustomExample(
+			"drycc registry set username password",
+			map[string]string{
+				"<username>": i18n.T("The username of the registry"),
+				"<password>": i18n.T("The password of the registry"),
+			},
+		),
+		Short: i18n.T("Set registry info for an app"),
+		Long: i18n.T(`Sets registry information for an application. These credentials are the same as those used for
+'podmain login' to the private registry.`),
+		RunE: func(_ *cobra.Command, args []string) error {
+			username := args[0]
+			password := args[1]
+			return cmdr.RegistrySet(app, flags.ptype, username, password)
+		},
+	}
+
+	cmd.Flags().StringVarP(&flags.ptype, "ptype", "p", "", i18n.T("The ptype for registry"))
+	cmd.MarkFlagRequired("ptype")
+
+	ptypeCompletion := completion.PtsCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("ptype", ptypeCompletion.CompletionFunc)
+	return cmd
+}
+
+func registryUnsetCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	// default web
+	var flags struct {
+		ptype string
+	}
+	cmd := &cobra.Command{
+		Use:   "unset",
+		Short: i18n.T("Unset registry info for an app"),
+		Long:  i18n.T("Unsets registry information for an application"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.RegistryUnset(app, flags.ptype)
+		},
+	}
+
+	cmd.Flags().StringVarP(&flags.ptype, "ptype", "p", "", i18n.T("The ptype for registry"))
+	cmd.MarkFlagRequired("ptype")
+
+	ptypeCompletion := completion.PtsCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("ptype", ptypeCompletion.CompletionFunc)
+	return cmd
+}
diff --git a/internal/parser/releases.go b/internal/parser/releases.go
new file mode 100644
index 000000000..ccf8c442e
--- /dev/null
+++ b/internal/parser/releases.go
@@ -0,0 +1,172 @@
+package parser
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/internal/template"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewReleasesCommand creates the releases command
+func NewReleasesCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	ptsArgsCompletion := completion.PtsArgsCompletion{
+		PtsCompletion: &completion.PtsCompletion{AppID: &app, ArgsLen: -1, ConfigFile: &cmdr.ConfigFile},
+	}
+
+	cmd := &cobra.Command{
+		Use:               "releases [<ptype>...]",
+		Example:           "drycc releases ptype1 ptype2",
+		Short:             i18n.T("Manage releases of an application"),
+		ValidArgsFunction: ptsArgsCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			results, _ := commands.ResponseLimit(limit)
+			return cmdr.ReleasesList(app, args, results)
+		},
+	}
+
+	cmd.PersistentFlags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("The maximum number of results to display"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+
+	cmd.AddCommand(releasesListCommand(cmdr))
+	cmd.AddCommand(releasesInfoCommand(cmdr))
+	cmd.AddCommand(releasesDeployCommand(cmdr))
+	cmd.AddCommand(releasesRollbackCommand(cmdr))
+	return cmd
+}
+
+func releasesListCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	ptsArgsCompletion := completion.PtsArgsCompletion{
+		PtsCompletion: &completion.PtsCompletion{AppID: &app, ArgsLen: -1, ConfigFile: &cmdr.ConfigFile},
+	}
+
+	cmd := &cobra.Command{
+		Use:               "list [<ptype>...]",
+		Example:           "drycc releases list ptype1 ptype2",
+		Short:             i18n.T("List an application's release history"),
+		Long:              i18n.T("Lists release history for an application"),
+		ValidArgsFunction: ptsArgsCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			results, _ := commands.ResponseLimit(limit)
+			return cmdr.ReleasesList(app, args, results)
+		},
+	}
+
+	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("The maximum number of results to display"))
+	cmd.Flags().SortFlags = false
+
+	return cmd
+}
+
+func releasesInfoCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	releaseCompletion := completion.ReleaseCompletion{AppID: &app, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "info <version>",
+		Args: cobra.ExactArgs(1),
+		Example: template.CustomExample(
+			"drycc releases info 1",
+			map[string]string{
+				"<version>": i18n.T(`The release of the application, such as '1'`),
+			},
+		),
+		Short:             i18n.T("Print information about a specific release"),
+		Long:              i18n.T("Prints info about a particular release"),
+		ValidArgsFunction: releaseCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			versionStr := args[0]
+			version, err := strconv.Atoi(strings.TrimPrefix(versionStr, "v"))
+			if err != nil {
+				return fmt.Errorf("invalid version format: %v", err)
+			}
+			return cmdr.ReleasesInfo(app, version)
+		},
+	}
+
+	return cmd
+}
+
+func releasesDeployCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		app     string
+		force   bool
+		confirm string
+	}
+
+	ptsArgsCompletion := completion.PtsArgsCompletion{
+		PtsCompletion: &completion.PtsCompletion{AppID: &app, ArgsLen: -1, ConfigFile: &cmdr.ConfigFile},
+	}
+
+	cmd := &cobra.Command{
+		Use:               "deploy [<ptype>...]",
+		Short:             i18n.T("Deploy the latest release by process types"),
+		ValidArgsFunction: ptsArgsCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			ptypes := args
+			return cmdr.ReleasesDeploy(app, ptypes, flags.force, flags.confirm)
+		},
+	}
+
+	cmd.Flags().BoolVarP(&flags.force, "force", "f", false, i18n.T("Force deploy"))
+	cmd.Flags().StringVar(&flags.confirm, "confirm", "", i18n.T(`To proceed, type "yes"`))
+	cmd.Flags().SortFlags = false
+
+	return cmd
+}
+
+func releasesRollbackCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	ptsArgsCompletion := completion.PtsArgsCompletion{
+		PtsCompletion: &completion.PtsCompletion{AppID: &app, ArgsLen: -1, ConfigFile: &cmdr.ConfigFile},
+	}
+	cmd := &cobra.Command{
+		Use: "rollback [<ptype>...] [version]",
+		Example: template.CustomExample(
+			"drycc releases rollback web 2",
+			map[string]string{
+				"<ptype>":   i18n.T(`The process name as defined in your Procfile, such as 'web'`),
+				"<version>": i18n.T(`The release of the application, such as '1'`),
+			},
+		),
+		Short:             i18n.T("Return to a previous release"),
+		Long:              i18n.T("Rolls back to a previous application release"),
+		Args:              cobra.MinimumNArgs(1),
+		ValidArgsFunction: ptsArgsCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			// Handle arguments safely
+			var ptypes []string
+			var versionStr string
+
+			if len(args) == 1 {
+				versionStr = args[0]
+			} else {
+				ptypes = args[:len(args)-1]
+				versionStr = args[len(args)-1]
+			}
+			version, err := versionFromString(versionStr)
+			if err != nil {
+				return err
+			}
+			return cmdr.ReleasesRollback(app, ptypes, version)
+		},
+	}
+
+	return cmd
+}
+
+func versionFromString(version string) (int, error) {
+	if version[:1] == "v" {
+		if len(version) < 2 {
+			return -1, fmt.Errorf("%s is not in the form 'v#'", version)
+		}
+
+		return strconv.Atoi(version[1:])
+	}
+
+	return strconv.Atoi(version)
+}
diff --git a/internal/parser/resources.go b/internal/parser/resources.go
new file mode 100644
index 000000000..76ec8036a
--- /dev/null
+++ b/internal/parser/resources.go
@@ -0,0 +1,273 @@
+package parser
+
+import (
+	"fmt"
+
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/internal/template"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewResourcesCommand creates a command for managing resources.
+func NewResourcesCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "resources",
+		Short: i18n.T("Manage resources for your applications"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			results, _ := commands.ResponseLimit(limit)
+			return cmdr.ResourcesList(app, results)
+		},
+	}
+
+	cmd.PersistentFlags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("The maximum number of results to display"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+
+	cmd.AddCommand(resourcesServicesCommand(cmdr))
+	cmd.AddCommand(resourcesPlansCommand(cmdr))
+	cmd.AddCommand(resourcesCreateCommand(cmdr))
+	cmd.AddCommand(resourcesListCommand(cmdr))
+	cmd.AddCommand(resourcesDescribeCommand(cmdr))
+	cmd.AddCommand(resourcesUpdateCommand(cmdr))
+	cmd.AddCommand(resourcesBindCommand(cmdr))
+	cmd.AddCommand(resourcesUnbindCommand(cmdr))
+	cmd.AddCommand(resourcesDestroyCommand(cmdr))
+	return cmd
+}
+
+func resourcesServicesCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "services",
+		Short: i18n.T("List all available resource services"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			results, _ := commands.ResponseLimit(limit)
+			return cmdr.ResourcesServices(results)
+		},
+	}
+
+	return cmd
+}
+
+func resourcesPlansCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	resourceServiceCompletion := completion.ResourceServiceCompletion{ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "plans <service>",
+		Args: cobra.ExactArgs(1),
+		Example: template.CustomExample(
+			"drycc resources plans redis",
+			map[string]string{
+				"<service>": i18n.T("The service name for plans"),
+			},
+		),
+		Short:             i18n.T("List all available plans for a resource service"),
+		ValidArgsFunction: resourceServiceCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			service := args[0]
+			results, _ := commands.ResponseLimit(limit)
+			return cmdr.ResourcesPlans(service, results)
+		},
+	}
+
+	return cmd
+}
+
+func resourcesCreateCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		values  string
+		path    string
+		confirm string
+	}
+	resourceCreateCompletion := completion.ResourceCreateCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "create <name> <service> <plan> [<param>=<value>...]",
+		Args: cobra.MinimumNArgs(3),
+		Example: template.CustomExample(
+			"drycc resources create myredis redis standard-128 -f file.yaml",
+			map[string]string{
+				"<name>":    i18n.T("This resource instance alias"),
+				"<service>": i18n.T("The resource's service"),
+				"<plan>":    i18n.T("The service's plan"),
+				"<param>":   i18n.T("The resource instance parameters key"),
+				"<value>":   i18n.T("The resource instance parameters value"),
+			},
+		),
+		Short:             i18n.T("Create a resource for the application"),
+		ValidArgsFunction: resourceCreateCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			name := args[0]
+			plan := fmt.Sprintf("%s:%s", args[1], args[2])
+			params := args[3:]
+
+			if flags.values != "" {
+				params = nil
+			}
+			return cmdr.ResourcesCreate(app, plan, name, params, flags.values)
+		},
+	}
+
+	cmd.Flags().StringVarP(&flags.values, "values", "f", "", i18n.T("Specify values in a YAML file. If set, params will be discard"))
+
+	return cmd
+}
+
+func resourcesListCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "list",
+		Short: i18n.T("List resources in the application"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			results, _ := commands.ResponseLimit(limit)
+			return cmdr.ResourcesList(app, results)
+		},
+	}
+
+	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("The maximum number of results to display"))
+
+	return cmd
+}
+
+func resourcesDescribeCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		app     string
+		name    string
+		details bool
+	}
+	resourceCompletion := completion.ResourceCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "describe <name>",
+		Args: cobra.ExactArgs(1),
+		Example: template.CustomExample(
+			"drycc resources describe myredis",
+			map[string]string{
+				"<name>": i18n.T("This resource instance alias"),
+			},
+		),
+		Short:             i18n.T("Get a resource's detail in the application"),
+		ValidArgsFunction: resourceCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			name := args[0]
+			return cmdr.ResourceGet(app, name, flags.details)
+		},
+	}
+
+	cmd.Flags().BoolVar(&flags.details, "details", false, i18n.T("Show extra details provided to resource"))
+
+	return cmd
+}
+
+func resourcesUpdateCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		app    string
+		values string
+		path   string
+	}
+	resourceUpdateCompletion := completion.ResourceUpdateCompletion{AppID: &app, ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "update <name> <service> <plan> [<param>=<value>...]",
+		Args: cobra.MinimumNArgs(3),
+		Example: template.CustomExample(
+			"drycc resources update myredis redis standard-128 -f file.yaml",
+			map[string]string{
+				"<name>":    i18n.T("This resource instance alias"),
+				"<service>": i18n.T("The resource's service"),
+				"<plan>":    i18n.T("The service's plan"),
+				"<param>":   i18n.T("The resource instance parameters key"),
+				"<value>":   i18n.T("The resource instance parameters value"),
+			},
+		),
+		Short:             i18n.T("Update a resource from the application"),
+		ValidArgsFunction: resourceUpdateCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			name := args[0]
+			plan := fmt.Sprintf("%s:%s", args[1], args[2])
+			params := args[2:]
+
+			if flags.values != "" {
+				params = nil
+			}
+
+			return cmdr.ResourcePut(app, plan, name, params, flags.values)
+		},
+	}
+
+	cmd.Flags().StringVarP(&flags.values, "values", "f", "", i18n.T("Specify values in a YAML file. If set, params will be discard"))
+
+	return cmd
+}
+
+func resourcesDestroyCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		app     string
+		name    string
+		confirm string
+	}
+	resourceCompletion := completion.ResourceCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "destroy <name> --confirm=<resource>",
+		Args: cobra.ExactArgs(1),
+		Example: template.CustomExample(
+			"drycc resources destroy myredis",
+			map[string]string{
+				"<name>": i18n.T("The resource instance alias name"),
+			},
+		),
+		Short:             i18n.T("Delete a resource from the application"),
+		ValidArgsFunction: resourceCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			name := args[0]
+			return cmdr.ResourceDelete(app, name, flags.confirm)
+		},
+	}
+
+	cmd.Flags().StringVar(&flags.confirm, "confirm", "", i18n.T(`skips the prompt for the resource name. <resource> is the uniquely identifiable
+name for the resource`))
+
+	return cmd
+}
+
+func resourcesBindCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	resourceCompletion := completion.ResourceCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "bind <name>",
+		Args: cobra.ExactArgs(1),
+		Example: template.CustomExample(
+			"drycc resources bind myredis",
+			map[string]string{
+				"<name>": i18n.T("The resource instance alias name"),
+			},
+		),
+		Short:             i18n.T("Bind a resource for an application"),
+		ValidArgsFunction: resourceCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			name := args[0]
+			return cmdr.ResourceBind(app, name)
+		},
+	}
+
+	return cmd
+}
+
+func resourcesUnbindCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	resourceCompletion := completion.ResourceCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "unbind <name>",
+		Args: cobra.ExactArgs(1),
+		Example: template.CustomExample(
+			"drycc resources unbind myredis",
+			map[string]string{
+				"<name>": i18n.T("The resource instance alias name"),
+			},
+		),
+		Short:             i18n.T("unbind a resources for an application"),
+		ValidArgsFunction: resourceCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			name := args[0]
+			return cmdr.ResourceUnbind(app, name)
+		},
+	}
+
+	return cmd
+}
diff --git a/internal/parser/routes.go b/internal/parser/routes.go
new file mode 100644
index 000000000..e75e86dc2
--- /dev/null
+++ b/internal/parser/routes.go
@@ -0,0 +1,256 @@
+package parser
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/internal/template"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewRoutesCommand creates a command for managing application routes.
+func NewRoutesCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "routes",
+		Short: i18n.T("Manage routes for your applications"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			results, _ := commands.ResponseLimit(limit)
+			return cmdr.RoutesList(app, results)
+		},
+	}
+
+	cmd.PersistentFlags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("The maximum number of results to display"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+
+	cmd.AddCommand(routesListCommand(cmdr))
+	cmd.AddCommand(routesAddCommand(cmdr))
+	cmd.AddCommand(routesGetCommand(cmdr))
+	cmd.AddCommand(routesSetCommand(cmdr))
+	cmd.AddCommand(routesAttachCommand(cmdr))
+	cmd.AddCommand(routesDetachCommand(cmdr))
+	cmd.AddCommand(routesRemoveCommand(cmdr))
+	return cmd
+}
+
+func routesAddCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	routeKindCompletion := completion.RouteKindCompletion{ArgsLen: 1}
+	cmd := &cobra.Command{
+		Use:  "add <name> <kind> <backend>...",
+		Args: cobra.MinimumNArgs(3),
+		Example: template.CustomExample(
+			`drycc routes add myroute HTTPRoute svc:8080,100`,
+			map[string]string{
+				"<name>":    i18n.T("The unique name of the route"),
+				"<kind>":    i18n.T("The route kind, range: HTTPRoute,TCPRoute,UDPRoute,GRPCRoute,TLSRoute"),
+				"<backend>": i18n.T(`The route's backend, pattern: <service>:<port>,<weight>`),
+			},
+		),
+		Short:             i18n.T("Create a route for an application"),
+		Long:              i18n.T("Creates routes for an application, provides a way to route requests"),
+		ValidArgsFunction: routeKindCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			name := args[0]
+			kind := args[1]
+			backends := args[2:]
+
+			var backendRefs []api.BackendRefRequest
+			for _, backend := range backends {
+				params := strings.Split(backend, ",")
+				if len(params) != 2 {
+					return fmt.Errorf("invalid backend format %s", backend)
+				}
+				servicePort := strings.Split(params[0], ":")
+				if len(servicePort) != 2 {
+					return fmt.Errorf("backend must be in 'service:port,weight' format")
+				}
+				port, err := strconv.Atoi(servicePort[1])
+				if err != nil {
+					return err
+				}
+				weight, err := strconv.Atoi(params[1])
+				if err != nil {
+					return err
+				}
+				backendRefs = append(backendRefs, api.BackendRefRequest{
+					Kind:   "Service",
+					Name:   servicePort[0],
+					Port:   int32(port),
+					Weight: int32(weight),
+				})
+			}
+
+			return cmdr.RoutesCreate(app, name, kind, backendRefs...)
+		},
+	}
+
+	return cmd
+}
+
+func routesListCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "list",
+		Short: i18n.T("List application routes"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			results, _ := commands.ResponseLimit(limit)
+			return cmdr.RoutesList(app, results)
+		},
+	}
+
+	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("The maximum number of results to display"))
+
+	return cmd
+}
+
+func routesGetCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	routeCompletion := completion.RouteCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "get <name>",
+		Args: cobra.ExactArgs(1),
+		Example: template.CustomExample(
+			"drycc routes get myroute",
+			map[string]string{
+				"<name>": i18n.T("The unique name of the route"),
+			},
+		),
+		Short:             i18n.T("Get route rules"),
+		Long:              i18n.T("Get route rules for an application"),
+		ValidArgsFunction: routeCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			name := args[0]
+			return cmdr.RoutesGet(app, name)
+		},
+	}
+
+	return cmd
+}
+
+func routesSetCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		rulesFile string
+	}
+	routeCompletion := completion.RouteCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "set <name>",
+		Args: cobra.ExactArgs(1),
+		Example: template.CustomExample(
+			"drycc routes set myroute --rules-file=rules.yaml",
+			map[string]string{
+				"<name>": i18n.T("The unique name of the route"),
+			},
+		),
+		Short:             i18n.T("Set route rules"),
+		Long:              i18n.T("Set route rules for an application"),
+		ValidArgsFunction: routeCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			name := args[0]
+			return cmdr.RoutesSet(app, name, flags.rulesFile)
+		},
+	}
+
+	cmd.Flags().StringVar(&flags.rulesFile, "rules-file", "", i18n.T("Rules-file is the file name of rule to apply"))
+	cmd.MarkFlagRequired("rules-file")
+
+	return cmd
+}
+
+func routesAttachCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		port    int
+		gateway string
+	}
+	routeCompletion := completion.RouteCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "attach <name>",
+		Args: cobra.ExactArgs(1),
+		Example: template.CustomExample(
+			"drycc routes attach myroute --gateway=gw1 --port=80",
+			map[string]string{
+				"<name>": i18n.T("The unique name of the route"),
+			},
+		),
+		Short:             i18n.T("Attach to gateway"),
+		ValidArgsFunction: routeCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			name := args[0]
+			return cmdr.RoutesAttach(app, name, flags.port, flags.gateway)
+		},
+	}
+
+	cmd.Flags().StringVar(&flags.gateway, "gateway", "", i18n.T("The unique name of the gaetway"))
+	cmd.Flags().IntVarP(&flags.port, "port", "", 0, i18n.T("Port is the network port, the gateway listener expects to receive"))
+
+	mustFlags := []string{"gateway", "port"}
+	for _, mustFlag := range mustFlags {
+		cmd.MarkFlagRequired(mustFlag)
+	}
+
+	gatewayNameCompletion := completion.GatewayNameCompletion{AppID: &app, ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("gateway", gatewayNameCompletion.CompletionFunc)
+	return cmd
+}
+
+func routesDetachCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		port    int
+		gateway string
+	}
+	routeCompletion := completion.RouteCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "detach <name>",
+		Args: cobra.ExactArgs(1),
+		Example: template.CustomExample(
+			"drycc routes detach myroute --gateway=gw1 --port=80",
+			map[string]string{
+				"<name>": i18n.T("The unique name of the route"),
+			},
+		),
+		Short:             i18n.T("Dettach to gateway"),
+		ValidArgsFunction: routeCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			name := args[0]
+			return cmdr.RoutesDetach(app, name, flags.port, flags.gateway)
+		},
+	}
+
+	cmd.Flags().StringVar(&flags.gateway, "gateway", "", i18n.T("The unique name of the gaetway"))
+	cmd.Flags().IntVarP(&flags.port, "port", "", 0, i18n.T("Port is the network port, the gateway listener expects to receive"))
+
+	mustFlags := []string{"port", "protocol"}
+	for _, mustFlag := range mustFlags {
+		cmd.MarkFlagRequired(mustFlag)
+	}
+
+	gatewayNameCompletion := completion.GatewayNameCompletion{AppID: &app, ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("gateway", gatewayNameCompletion.CompletionFunc)
+	return cmd
+}
+
+func routesRemoveCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	routeCompletion := completion.RouteCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "remove <name>",
+		Args: cobra.ExactArgs(1),
+		Example: template.CustomExample(
+			"drycc routes remove myroute",
+			map[string]string{
+				"<name>": i18n.T("The unique name of the route"),
+			},
+		),
+		Short:             i18n.T("Remove a route from an application"),
+		ValidArgsFunction: routeCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			name := args[0]
+			return cmdr.RoutesRemove(app, name)
+		},
+	}
+
+	return cmd
+}
diff --git a/internal/parser/routing.go b/internal/parser/routing.go
new file mode 100644
index 000000000..67db35bc3
--- /dev/null
+++ b/internal/parser/routing.go
@@ -0,0 +1,65 @@
+package parser
+
+import (
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewRoutingCommand creates a command for managing application routing.
+func NewRoutingCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "routing",
+		Short: i18n.T("Manage routability of an application"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.RoutingInfo(app)
+		},
+	}
+
+	cmd.PersistentFlags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+
+	cmd.AddCommand(routingInfoCommand(cmdr))
+	cmd.AddCommand(routingEnableCommand(cmdr))
+	cmd.AddCommand(routingDisableCommand(cmdr))
+	return cmd
+}
+
+func routingInfoCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "info",
+		Short: i18n.T("View application's routability information"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.RoutingInfo(app)
+		},
+	}
+
+	return cmd
+}
+
+func routingEnableCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "enable",
+		Short: i18n.T("Enable routing for an application"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.RoutingEnable(app)
+		},
+	}
+
+	return cmd
+}
+
+func routingDisableCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "disable",
+		Short: i18n.T("Disable routing for an application"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.RoutingDisable(app)
+		},
+	}
+
+	return cmd
+}
diff --git a/internal/parser/services.go b/internal/parser/services.go
new file mode 100644
index 000000000..acb26d5ba
--- /dev/null
+++ b/internal/parser/services.go
@@ -0,0 +1,121 @@
+package parser
+
+import (
+	"strconv"
+
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/internal/template"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewServicesCommand creates a command for managing application services.
+func NewServicesCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "services",
+		Short: i18n.T("Manage services for your applications"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.ServicesList(app)
+		},
+	}
+
+	cmd.PersistentFlags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+
+	cmd.AddCommand(servicesList(cmdr))
+	cmd.AddCommand(servicesAdd(cmdr))
+	cmd.AddCommand(servicesRemove(cmdr))
+	return cmd
+}
+
+func servicesAdd(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		port     string
+		protocol string
+	}
+
+	ptsArgsCompletion := completion.PtsArgsCompletion{
+		PtsCompletion: &completion.PtsCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile},
+	}
+	cmd := &cobra.Command{
+		Use:  "add <ptype> <port>:<target>",
+		Args: cobra.ExactArgs(2),
+		Example: template.CustomExample(
+			"drycc services add web-new 80:8080",
+			map[string]string{
+				"<ptype>": i18n.T(`procfile type which should handle the request, e.g. webhooks (should be bind to the port PORT)
+             only single extra service per Porcfile type could be created.`),
+				"<port>":   i18n.T("The port that will be exposed by this service"),
+				"<target>": i18n.T("Number or name of the port to access on the pods targeted by the service"),
+			},
+		),
+		Short:             i18n.T("Create service for an application"),
+		Long:              i18n.T("Creates extra service for an application and binds it to specific route of the main app domain"),
+		ValidArgsFunction: ptsArgsCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			ptype := args[0]
+			portTarget := args[1]
+			return cmdr.ServicesAdd(app, ptype, portTarget, flags.protocol)
+		},
+	}
+
+	cmd.Flags().StringVar(&flags.protocol, "protocol", "TCP", i18n.T("The IP protocol for this port. Supports TCP, UDP, and SCTP"))
+
+	serviceProtocolCompletion := completion.ServiceProtocolCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("protocol", serviceProtocolCompletion.CompletionFunc)
+	return cmd
+}
+
+func servicesList(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:     "list",
+		Example: "drycc services list",
+		Short:   i18n.T("List application services"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.ServicesList(app)
+		},
+	}
+
+	return cmd
+}
+
+func servicesRemove(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		app      string
+		port     int
+		protocol string
+	}
+
+	ServiceCompletion := completion.ServiceCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "remove <ptype> <port>",
+		Args: cobra.ExactArgs(2),
+		Example: template.CustomExample(
+			"drycc services remove web-new 80",
+			map[string]string{
+				"<ptype>": i18n.T("procfile type which should handle the request, e.g. webhooks"),
+				"<port>":  i18n.T("The port exposed by this service"),
+			},
+		),
+		Short:             i18n.T("Remove service from an application"),
+		ValidArgsFunction: ServiceCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			ptype := args[0]
+			port, err := strconv.Atoi(args[1])
+			if err != nil {
+				return err
+			}
+			flags.port = port
+			return cmdr.ServicesRemove(app, ptype, flags.protocol, port)
+		},
+	}
+
+	cmd.Flags().StringVar(&flags.protocol, "protocol", "TCP", i18n.T("The IP protocol for this port. Supports TCP, UDP, and SCTP"))
+
+	serviceProtocolCompletion := completion.ServiceProtocolCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("protocol", serviceProtocolCompletion.CompletionFunc)
+	return cmd
+}
diff --git a/internal/parser/shortcuts.go b/internal/parser/shortcuts.go
new file mode 100644
index 000000000..08580813c
--- /dev/null
+++ b/internal/parser/shortcuts.go
@@ -0,0 +1,99 @@
+package parser
+
+import (
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/template"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// Shortcuts defines the interface for creating shortcut commands.
+type Shortcuts interface {
+	Create(cmdr *commands.DryccCmd) []*cobra.Command
+}
+
+// SupportedShortcuts is a list of all supported shortcut types.
+var SupportedShortcuts = []Shortcuts{
+	&AuthShortcuts{}, &AppsShortcuts{}, &PsShortcuts{}, &BuildsShortcuts{}, &PtsShortcuts{},
+}
+
+// AuthShortcuts provides authentication-related shortcut commands.
+type AuthShortcuts struct{}
+
+// AppsShortcuts provides application-related shortcut commands.
+type AppsShortcuts struct{}
+
+// PsShortcuts provides process-related shortcut commands.
+type PsShortcuts struct{}
+
+// Create returns a slice of authentication-related shortcut commands.
+func (a *AuthShortcuts) Create(cmdr *commands.DryccCmd) []*cobra.Command {
+	login := authLogin(cmdr)
+	login.Example = "drycc login http://drycc.local3.dryccapp.com/"
+
+	logout := authLogout(cmdr)
+	logout.Example = "drycc auth logout"
+
+	whoami := authWhoami(cmdr)
+	whoami.Example = "drycc whoami"
+
+	return []*cobra.Command{login, logout, whoami}
+}
+
+// Create returns a slice of application-related shortcut commands.
+func (a *AppsShortcuts) Create(cmdr *commands.DryccCmd) []*cobra.Command {
+	destroy := appsDestroy(cmdr)
+	destroy.Example = "drycc destroy -a <app> --confirm <app>"
+
+	run := appsRun(cmdr)
+	run.Example = template.CustomExample(
+		"drycc run --mount=myvolume:/data -- 'echo hello'",
+		map[string]string{
+			"<volume>":  i18n.T("The volume name"),
+			"<path>":    i18n.T("The filesystem path"),
+			"<command>": i18n.T("The shell command to run inside the container"),
+		},
+	)
+	return []*cobra.Command{appsCreate(cmdr), destroy, appsInfo(cmdr), appsOpen(cmdr), run}
+}
+
+// Create returns a slice of process-related shortcut commands.
+func (p *PsShortcuts) Create(cmdr *commands.DryccCmd) []*cobra.Command {
+	exec := psExecCommand(cmdr)
+	exec.Example = template.CustomExample(
+		"drycc exec my-pod -it -- bash",
+		map[string]string{
+			"<pod>": i18n.T("The pod name for the application"),
+		},
+	)
+
+	logs := psLogsCommand(cmdr)
+	logs.Example = "drycc logs my-pod"
+	return []*cobra.Command{exec, logs}
+}
+
+// BuildsShortcuts provides build-related shortcut commands.
+type BuildsShortcuts struct{}
+
+// Create returns a slice of build-related shortcut commands.
+func (b *BuildsShortcuts) Create(cmdr *commands.DryccCmd) []*cobra.Command {
+	create := buildsCreate(cmdr)
+	create.Use = `pull <image>`
+	return []*cobra.Command{create}
+}
+
+// PtsShortcuts provides process type-related shortcut commands.
+type PtsShortcuts struct{}
+
+// Create returns a slice of process type-related shortcut commands.
+func (p *PtsShortcuts) Create(cmdr *commands.DryccCmd) []*cobra.Command {
+	scale := ptsScaleCommand(cmdr)
+	scale.Example = template.CustomExample(
+		"drycc scale web=3",
+		map[string]string{
+			"<ptype>": i18n.T("The process name as defined in your Procfile"),
+			"<num>":   i18n.T("The number of processes"),
+		},
+	)
+	return []*cobra.Command{scale}
+}
diff --git a/internal/parser/tags.go b/internal/parser/tags.go
new file mode 100644
index 000000000..78ffe34d4
--- /dev/null
+++ b/internal/parser/tags.go
@@ -0,0 +1,138 @@
+package parser
+
+import (
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/internal/template"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewTagsCommand creates the tags command
+func NewTagsCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		ptype string
+	}
+
+	cmd := &cobra.Command{
+		Use:   "tags",
+		Short: i18n.T("Manage tags for application containers"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.TagsList(app, flags.ptype, version)
+		},
+	}
+
+	cmd.PersistentFlags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+	cmd.Flags().StringVarP(&flags.ptype, "ptype", "p", "", i18n.T("The process name as defined in your Procfile"))
+	cmd.Flags().IntVarP(&version, "version", "v", 0, i18n.T("The version for which the tag needs to be listed"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+	ptypeCompletion := completion.PtsCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("ptype", ptypeCompletion.CompletionFunc)
+	releaseCompletion := completion.ReleaseCompletion{ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("version", releaseCompletion.CompletionFunc)
+
+	cmd.AddCommand(tagsListCommand(cmdr))
+	cmd.AddCommand(tagsSetCommand(cmdr))
+	cmd.AddCommand(tagsUnsetCommand(cmdr))
+	return cmd
+}
+
+func tagsListCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		ptype string
+	}
+
+	ptsArgsCompletion := completion.PtsArgsCompletion{
+		PtsCompletion: &completion.PtsCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile},
+	}
+	cmd := &cobra.Command{
+		Use:               "list",
+		Short:             i18n.T("List tags for an application"),
+		Args:              cobra.MaximumNArgs(1),
+		ValidArgsFunction: ptsArgsCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.TagsList(app, flags.ptype, version)
+		},
+	}
+
+	cmd.Flags().StringVarP(&flags.ptype, "ptype", "p", "", i18n.T("The process name as defined in your Procfile"))
+	cmd.Flags().IntVarP(&version, "version", "v", 0, i18n.T("The version for which the tag needs to be listed"))
+
+	ptypeCompletion := completion.PtsCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("ptype", ptypeCompletion.CompletionFunc)
+	releaseCompletion := completion.ReleaseCompletion{ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("version", releaseCompletion.CompletionFunc)
+	return cmd
+}
+
+func tagsSetCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		ptype string
+	}
+	ptsArgsCompletion := completion.PtsArgsCompletion{
+		PtsCompletion: &completion.PtsCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile},
+	}
+	cmd := &cobra.Command{
+		Use: "set <key>=<value>...",
+		Example: template.CustomExample(
+			"drycc tags set environ=prod rack=1",
+			map[string]string{
+				"<key>":   i18n.T("The tag key"),
+				"<value>": i18n.T("The tag value"),
+			},
+		),
+		Short: i18n.T("Set tags for an application"),
+		Long: i18n.T(`Sets tags for an application.
+
+A tag is a key/value pair used to tag an application's containers and is passed to the
+scheduler. This is often used to restrict workloads to specific hosts matching the
+scheduler-configured metadata.`),
+		Args:              cobra.MinimumNArgs(1),
+		ValidArgsFunction: ptsArgsCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			tags := args[0:]
+			return cmdr.TagsSet(app, flags.ptype, tags)
+		},
+	}
+
+	cmd.Flags().StringVarP(&flags.ptype, "ptype", "p", "", i18n.T("The process name as defined in your Procfile"))
+	cmd.MarkFlagRequired("ptype")
+
+	ptypeCompletion := completion.PtsCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("ptype", ptypeCompletion.CompletionFunc)
+
+	return cmd
+}
+
+func tagsUnsetCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		ptype string
+	}
+	tagCompletion := completion.TagCompletion{AppID: &app, Ptype: &flags.ptype, ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use: "unset <key>...",
+		Example: template.CustomExample(
+			"drycc tags unset environ rack",
+			map[string]string{
+				"<key>": i18n.T("The tag key to unset"),
+			},
+		),
+		Short:             i18n.T("Unset tags for an application"),
+		Args:              cobra.MinimumNArgs(1),
+		ValidArgsFunction: tagCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			keys := args[0:]
+			return cmdr.TagsUnset(app, flags.ptype, keys)
+		},
+	}
+
+	cmd.Flags().StringVarP(&flags.ptype, "ptype", "p", "", i18n.T("The process name as defined in your Procfile"))
+	cmd.MarkFlagRequired("ptype")
+
+	ptypeCompletion := completion.PtsCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("ptype", ptypeCompletion.CompletionFunc)
+
+	return cmd
+}
diff --git a/internal/parser/timeouts.go b/internal/parser/timeouts.go
new file mode 100644
index 000000000..11c597dc3
--- /dev/null
+++ b/internal/parser/timeouts.go
@@ -0,0 +1,100 @@
+package parser
+
+import (
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/internal/template"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewTimeoutsCommand creates the timeouts command
+func NewTimeoutsCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "timeouts",
+		Short: i18n.T("Manage pods termination grace period"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.TimeoutsList(app, version)
+		},
+	}
+
+	cmd.PersistentFlags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+	cmd.Flags().IntVarP(&version, "version", "v", 0, i18n.T("The version for which the timeout needs to be listed"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+	releaseCompletion := completion.ReleaseCompletion{ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("version", releaseCompletion.CompletionFunc)
+
+	cmd.AddCommand(timeoutListCommand(cmdr))
+	cmd.AddCommand(timeoutSetCommand(cmdr))
+	cmd.AddCommand(timeoutUnsetCommand(cmdr))
+	return cmd
+}
+
+func timeoutListCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "list",
+		Short: i18n.T("List resource timeouts for an app"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.TimeoutsList(app, version)
+		},
+	}
+
+	cmd.Flags().IntVarP(&version, "version", "v", 0, i18n.T("The version for which the timeout needs to be listed"))
+
+	releaseCompletion := completion.ReleaseCompletion{ConfigFile: &cmdr.ConfigFile, AppID: &app}
+	cmd.RegisterFlagCompletionFunc("version", releaseCompletion.CompletionFunc)
+	return cmd
+}
+
+func timeoutSetCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	ptsSetArgsCompletion := completion.PtsSetArgsCompletion{
+		PtsCompletion: &completion.PtsCompletion{AppID: &app, ArgsLen: -1, ConfigFile: &cmdr.ConfigFile},
+	}
+	cmd := &cobra.Command{
+		Use: "set <ptype>=<value>...",
+		Example: template.CustomExample(
+			"drycc timeouts set web=30 worker=60",
+			map[string]string{
+				"<ptype>": i18n.T("The process type as defined in your Procfile"),
+				"<value>": i18n.T("The value to apply to the process type in seconds"),
+			},
+		),
+		Short:             i18n.T("Set resource timeouts for an app"),
+		Long:              i18n.T("Sets termination grace period for an application"),
+		Args:              cobra.MinimumNArgs(1),
+		ValidArgsFunction: ptsSetArgsCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			timeouts := args
+			return cmdr.TimeoutsSet(app, timeouts)
+		},
+	}
+
+	return cmd
+}
+
+func timeoutUnsetCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	ptsArgsCompletion := completion.PtsArgsCompletion{
+		PtsCompletion: &completion.PtsCompletion{AppID: &app, ArgsLen: -1, ConfigFile: &cmdr.ConfigFile},
+	}
+	cmd := &cobra.Command{
+		Use: "unset <ptype>...",
+		Example: template.CustomExample(
+			"drycc timeouts unset web worker",
+			map[string]string{
+				"<ptype>": i18n.T("The process type as defined in your Procfile"),
+			},
+		),
+		Short:             i18n.T("Unset resource timeouts for an app"),
+		Long:              i18n.T("Unsets timeouts for an application. Default value (30s) or set by drycc controller"),
+		Args:              cobra.MinimumNArgs(1),
+		ValidArgsFunction: ptsArgsCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			ptypes := args
+			return cmdr.TimeoutsUnset(app, ptypes)
+		},
+	}
+
+	return cmd
+}
diff --git a/internal/parser/tls.go b/internal/parser/tls.go
new file mode 100644
index 000000000..431c8cf7b
--- /dev/null
+++ b/internal/parser/tls.go
@@ -0,0 +1,134 @@
+package parser
+
+import (
+	"fmt"
+
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/internal/template"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewTLSCommand creates the TLS command
+func NewTLSCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "tls",
+		Short: i18n.T("Manage TLS/SSL settings for applications"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.TLSInfo(app)
+		},
+	}
+
+	cmd.PersistentFlags().StringVarP(&app, "app", "a", "", i18n.T("Application name"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+
+	cmd.AddCommand(tlsInfoCommand(cmdr))
+	cmd.AddCommand(tlsIssuerCommand(cmdr))
+	cmd.AddCommand(tlsAutoCommand(cmdr))
+	cmd.AddCommand(tlsForceCommand(cmdr))
+	return cmd
+}
+
+func tlsInfoCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:     "info",
+		Example: "drycc tls info",
+		Short:   i18n.T("View TLS settings information"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.TLSInfo(app)
+		},
+	}
+
+	return cmd
+}
+
+func tlsForceCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	TLSActionCompletion := completion.TLSActionCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use: "force <action>",
+		Example: template.CustomExample(
+			"drycc tls force enable",
+			map[string]string{
+				"<action>": i18n.T("Action to perform, 'enable' or 'disable'"),
+			},
+		),
+		Args:              cobra.ExactArgs(1),
+		ValidArgs:         []string{"enable", "disable"},
+		Short:             i18n.T("Force TLS settings to HTTPS-only redirection"),
+		ValidArgsFunction: TLSActionCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			action := args[0]
+			switch action {
+			case "enable":
+				return cmdr.TLSForceEnable(app)
+			case "disable":
+				return cmdr.TLSForceDisable(app)
+			}
+			return fmt.Errorf("invalid action: %s, please use 'enable' or 'disable'", action)
+		},
+	}
+
+	return cmd
+}
+
+func tlsAutoCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	TLSActionCompletion := completion.TLSActionCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use: "auto <action>",
+		Example: template.CustomExample(
+			"drycc tls auto enable",
+			map[string]string{
+				"<action>": i18n.T("Action to perform, 'enable' or 'disable'"),
+			},
+		),
+		Args:              cobra.ExactArgs(1),
+		ValidArgs:         []string{"enable", "disable"},
+		Short:             i18n.T("Automatic certificate management"),
+		ValidArgsFunction: TLSActionCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			action := args[0]
+			switch action {
+			case "enable":
+				return cmdr.TLSAutoEnable(app)
+			case "disable":
+				return cmdr.TLSAutoDisable(app)
+			}
+			return fmt.Errorf("invalid action: %s, please use 'enable' or 'disable'", action)
+		},
+	}
+
+	return cmd
+}
+
+func tlsIssuerCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		app       string
+		email     string
+		server    string
+		keyID     string
+		keySecret string
+	}
+
+	cmd := &cobra.Command{
+		Use:   "issuer",
+		Short: i18n.T("Configure automatic certificate management issuer details"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.TLSAutoIssuer(app, flags.email, flags.server, flags.keyID, flags.keySecret)
+		},
+	}
+
+	cmd.Flags().StringVar(&flags.email, "email", "", i18n.T("ACME account email"))
+	cmd.Flags().StringVar(&flags.server, "server", "", i18n.T("ACME server URL"))
+	cmd.Flags().StringVar(&flags.keyID, "key-id", "", i18n.T("CA key ID"))
+	cmd.Flags().StringVar(&flags.keySecret, "key-secret", "", i18n.T("CA key secret"))
+
+	mustFlags := []string{"email", "server", "key-id", "key-secret"}
+	for _, mustFlag := range mustFlags {
+		cmd.MarkFlagRequired(mustFlag)
+	}
+
+	return cmd
+}
diff --git a/internal/parser/tokens.go b/internal/parser/tokens.go
new file mode 100644
index 000000000..9448123fe
--- /dev/null
+++ b/internal/parser/tokens.go
@@ -0,0 +1,106 @@
+package parser
+
+import (
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/internal/template"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewTokensCommand creates the tokens command
+func NewTokensCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "tokens",
+		Short: i18n.T("Manage user tokens"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			results, _ := commands.ResponseLimit(limit)
+			return cmdr.TokensList(results)
+		},
+	}
+
+	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("The maximum number of results to display"))
+
+	cmd.AddCommand(tokensListCommand(cmdr))
+	cmd.AddCommand(tokensAddCommand(cmdr))
+	cmd.AddCommand(tokensRemoveCommand(cmdr))
+	return cmd
+}
+
+func tokensListCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "list",
+		Short: i18n.T("Lists tokens visible to the current controller"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			results, _ := commands.ResponseLimit(limit)
+			return cmdr.TokensList(results)
+		},
+	}
+
+	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("The maximum number of results to display"))
+	return cmd
+}
+
+func tokensAddCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		username string
+		password string
+	}
+
+	cmd := &cobra.Command{
+		Use: "add <alias>",
+		Example: template.CustomExample(
+			"drycc tokens add mytoken -u uname -p passwd",
+			map[string]string{
+				"<alias>": i18n.T("Provide a alias for controller authentication token"),
+			},
+		),
+		Short: i18n.T("Add a token for controller authentication"),
+		Args:  cobra.ExactArgs(1),
+		RunE: func(_ *cobra.Command, args []string) error {
+			alias := args[0]
+			_, err := cmdr.TokensAdd(nil, flags.username, flags.password, alias, "", true)
+			return err
+		},
+	}
+
+	cmd.Flags().StringVarP(&flags.username, "username", "u", "", i18n.T("Provide a username for the account"))
+	cmd.Flags().StringVarP(&flags.password, "password", "p", "", i18n.T("Provide a password for the account"))
+
+	mustFlags := []string{"username", "password"}
+	for _, mustFlag := range mustFlags {
+		cmd.MarkFlagRequired(mustFlag)
+	}
+
+	return cmd
+}
+
+func tokensRemoveCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		id      string
+		confirm string
+	}
+
+	// coTokenCompletion
+	tokenCompletion := completion.TokenCompletion{ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use: "remove <id>",
+		Example: template.CustomExample(
+			"drycc tokens remove 34103c3b-077e-4e37-a9ad-29ba4324ad8b",
+			map[string]string{
+				"<id>": i18n.T("The id of the token for controller authentication"),
+			},
+		),
+		Short:             i18n.T("Remove a token for controller authentication"),
+		Args:              cobra.ExactArgs(1),
+		ValidArgsFunction: tokenCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			flags.id = args[0]
+			return cmdr.TokensRemove(flags.id, flags.confirm)
+		},
+	}
+
+	cmd.Flags().StringVarP(&flags.confirm, "confirm", "", "", i18n.T(`To proceed, type "yes"`))
+
+	return cmd
+}
diff --git a/internal/parser/update.go b/internal/parser/update.go
new file mode 100644
index 000000000..78cb47d05
--- /dev/null
+++ b/internal/parser/update.go
@@ -0,0 +1,23 @@
+package parser
+
+import (
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewUpdateCommand creates a command for updating the workflow CLI.
+func NewUpdateCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var dryRun bool
+	cmd := &cobra.Command{
+		Use:   "update",
+		Args:  cobra.NoArgs,
+		Short: i18n.T("Update workflow cli to latest release"),
+		Long:  i18n.T("Update workflow cli to latest release"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cmdr.Update(dryRun)
+		},
+	}
+	cmd.Flags().BoolVarP(&dryRun, "dry-run", "d", false, i18n.T("Simulate an update, only print the version info."))
+	return cmd
+}
diff --git a/internal/parser/version.go b/internal/parser/version.go
new file mode 100644
index 000000000..c0ed0ef0d
--- /dev/null
+++ b/internal/parser/version.go
@@ -0,0 +1,24 @@
+package parser
+
+import (
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewVersionCommand creates a command for displaying the workflow CLI version.
+func NewVersionCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		all bool
+	}
+	cmd := &cobra.Command{
+		Use:   "version",
+		Short: i18n.T("Display client version"),
+		Long:  i18n.T("Displays the client version"),
+		Run: func(_ *cobra.Command, _ []string) {
+			cmdr.Version(flags.all)
+		},
+	}
+	cmd.Flags().BoolVarP(&flags.all, "all", "a", false, i18n.T("list api and controller versions."))
+	return cmd
+}
diff --git a/internal/parser/volumes.go b/internal/parser/volumes.go
new file mode 100644
index 000000000..1818a8156
--- /dev/null
+++ b/internal/parser/volumes.go
@@ -0,0 +1,273 @@
+package parser
+
+import (
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/internal/template"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewVolumesCommand creates the volumes command
+func NewVolumesCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "volumes",
+		Short: i18n.T("Manage volumes for your applications"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			results, _ := commands.ResponseLimit(limit)
+			return cmdr.VolumesList(app, results)
+		},
+	}
+
+	cmd.PersistentFlags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
+	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("The maximum number of results to display"))
+
+	appCompletion := completion.AppCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("app", appCompletion.CompletionFunc)
+
+	cmd.AddCommand(volumesListCommand(cmdr))
+	cmd.AddCommand(volumesAddCommand(cmdr))
+	cmd.AddCommand(volumesExpandCommand(cmdr))
+	cmd.AddCommand(volumesInfoCommand(cmdr))
+	cmd.AddCommand(volumesRemoveCommand(cmdr))
+	cmd.AddCommand(volumesServeCommand(cmdr))
+	cmd.AddCommand(volumesMountCommand(cmdr))
+	cmd.AddCommand(volumesUnmountCommand(cmdr))
+	return cmd
+}
+
+func volumesAddCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		vtype        string
+		name         string
+		size         string
+		nfsServer    string
+		nfsPath      string
+		ossServer    string
+		ossBucket    string
+		ossPathStyle bool
+		ossAccessKey string
+		ossSecretKey string
+	}
+
+	cmd := &cobra.Command{
+		Use: "add <name> <size>",
+		Example: template.CustomExample(
+			"drycc volumes add myvolume 1G",
+			map[string]string{
+				"<name>": i18n.T("The volume name"),
+				"<size>": i18n.T("The volume size, such as '500G'"),
+			},
+		),
+		Short: i18n.T("Create a volume for the application"),
+		Args:  cobra.ExactArgs(2),
+		RunE: func(_ *cobra.Command, args []string) error {
+			flags.name = args[0]
+			flags.size = args[1]
+			parameters := make(map[string]any)
+			switch flags.vtype {
+			case "nfs":
+				parameters["nfs"] = map[string]any{
+					"server": flags.nfsServer,
+					"path":   flags.nfsPath,
+				}
+			case "oss":
+				parameters["oss"] = map[string]any{
+					"server":     flags.ossServer,
+					"bucket":     flags.ossBucket,
+					"access_key": flags.ossAccessKey,
+					"secret_key": flags.ossSecretKey,
+					"path_style": flags.ossPathStyle,
+				}
+			}
+			return cmdr.VolumesCreate(app, flags.name, flags.vtype, flags.size, parameters)
+		},
+	}
+
+	cmd.Flags().StringVarP(&flags.vtype, "type", "t", "csi", i18n.T("The volume type, such as csi, nfs, oss"))
+	cmd.Flags().StringVar(&flags.nfsServer, "nfs-server", "", i18n.T("The hostname or ip address of the nfs server"))
+	cmd.Flags().StringVar(&flags.nfsPath, "nfs-path", "", i18n.T("Path that is exported by the nfs server"))
+
+	cmd.Flags().StringVar(&flags.ossServer, "oss-server", "", i18n.T("Endpoint url for object storage service"))
+	cmd.Flags().StringVar(&flags.ossBucket, "oss-bucket", "", i18n.T("Bucket name in object storage"))
+	cmd.Flags().StringVar(&flags.ossAccessKey, "oss-access-key", "", i18n.T("Access key id for authentication"))
+	cmd.Flags().StringVar(&flags.ossSecretKey, "oss-secret-key", "", i18n.T("Secret access key for authentication"))
+	cmd.Flags().BoolVar(&flags.ossPathStyle, "oss-path-style", false, i18n.T("Force a path-style endpoint to be used"))
+
+	cmd.Flags().SortFlags = false
+	cmd.MarkFlagsRequiredTogether("nfs-server", "nfs-path")
+	cmd.MarkFlagsRequiredTogether("oss-server", "oss-bucket", "oss-access-key", "oss-secret-key")
+
+	volumeTypeCompletion := completion.VolumeTypeCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd.RegisterFlagCompletionFunc("type", volumeTypeCompletion.CompletionFunc)
+	return cmd
+}
+
+func volumesExpandCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		name string
+		size string
+	}
+
+	volumeCompletion := completion.VolumeCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use: "expand <name> <size>",
+		Example: template.CustomExample(
+			"drycc volumes expand myvolume 2G",
+			map[string]string{
+				"<name>": i18n.T("The volume name"),
+				"<size>": i18n.T("The volume size, such as '500G'"),
+			},
+		),
+		Short:             i18n.T("Expand a volume capacity for the application"),
+		Args:              cobra.ExactArgs(2),
+		ValidArgsFunction: volumeCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			flags.name = args[0]
+			flags.size = args[1]
+			return cmdr.VolumesExpand(app, flags.name, flags.size)
+		},
+	}
+
+	return cmd
+}
+
+func volumesListCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "list",
+		Short: i18n.T("List volumes in the application"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			results, _ := commands.ResponseLimit(limit)
+			return cmdr.VolumesList(app, results)
+		},
+	}
+
+	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("The maximum number of results to display"))
+
+	return cmd
+}
+
+func volumesInfoCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		name string
+	}
+
+	volumeCompletion := completion.VolumeCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use: "info <name>",
+		Example: template.CustomExample(
+			"drycc volumes info myvolume",
+			map[string]string{
+				"<name>": i18n.T("The volume name to be info"),
+			},
+		),
+		Short:             i18n.T("Print information about a volume"),
+		Args:              cobra.ExactArgs(1),
+		ValidArgsFunction: volumeCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			flags.name = args[0]
+			return cmdr.VolumesInfo(app, flags.name)
+		},
+	}
+
+	return cmd
+}
+
+func volumesRemoveCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		name string
+	}
+
+	volumeCompletion := completion.VolumeCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use: "remove <name>",
+		Example: template.CustomExample(
+			"drycc volumes remove myvolume",
+			map[string]string{
+				"<name>": i18n.T("The volume name to be removed"),
+			},
+		),
+		Short:             i18n.T("Delete a volume from the application"),
+		Args:              cobra.ExactArgs(1),
+		ValidArgsFunction: volumeCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			flags.name = args[0]
+			return cmdr.VolumesDelete(app, flags.name)
+		},
+	}
+
+	return cmd
+}
+
+func volumesServeCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		name string
+	}
+	volumeCompletion := completion.VolumeCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:   "serve <name>",
+		Short: i18n.T("Serve a WebDAV service for the volume"),
+		Example: template.CustomExample(
+			"drycc volumes serve myvolume",
+			map[string]string{
+				"<name>": i18n.T("The volume name"),
+			},
+		),
+		Args:              cobra.ExactArgs(1),
+		ValidArgsFunction: volumeCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			flags.name = args[0]
+			return cmdr.VolumesServe(app, flags.name)
+		},
+	}
+	return cmd
+}
+
+func volumesMountCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	volumesMountCompletion := completion.VolumesMountCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use: "mount <name> <ptype>=<path>...",
+		Example: template.CustomExample(
+			"drycc volumes mount myvolume web=/data",
+			map[string]string{
+				"<name>":  i18n.T("The volume name"),
+				"<ptype>": i18n.T("The process name as defined in your Procfile"),
+				"<path>":  i18n.T("The filesystem path"),
+			},
+		),
+		Short:             i18n.T("Mount a volume to process of the application"),
+		Args:              cobra.MinimumNArgs(2),
+		ValidArgsFunction: volumesMountCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			name := args[0]
+			mountSpecs := args[1:]
+			return cmdr.VolumesMount(app, name, mountSpecs)
+		},
+	}
+
+	return cmd
+}
+
+func volumesUnmountCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	volumesUnmountCompletion := completion.VolumesUnmountCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use: "unmount <name> <ptype>...",
+		Example: template.CustomExample(
+			"drycc volumes unmount myvolume web worker",
+			map[string]string{
+				"<name>":  i18n.T("The volume name"),
+				"<ptype>": i18n.T("The process name as defined in your Procfile"),
+			},
+		),
+		Short:             i18n.T("Unmount a volume from process of the application"),
+		Args:              cobra.MinimumNArgs(2),
+		ValidArgsFunction: volumesUnmountCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			name := args[0]
+			ptypes := args[1:]
+			return cmdr.VolumesUnmount(app, name, ptypes)
+		},
+	}
+
+	return cmd
+}
diff --git a/internal/parser/workspaces.go b/internal/parser/workspaces.go
new file mode 100644
index 000000000..e6877a771
--- /dev/null
+++ b/internal/parser/workspaces.go
@@ -0,0 +1,204 @@
+package parser
+
+import (
+	"fmt"
+
+	"github.com/drycc/workflow-cli/internal/commands"
+	"github.com/drycc/workflow-cli/internal/completion"
+	"github.com/drycc/workflow-cli/internal/template"
+	"github.com/drycc/workflow-cli/pkg/i18n"
+	"github.com/spf13/cobra"
+)
+
+// NewWorkspacesCommand creates the workspaces command.
+func NewWorkspacesCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "workspaces",
+		Short: i18n.T("Manage workspaces"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			results, _ := commands.ResponseLimit(limit)
+			return cmdr.WorkspacesList(results)
+		},
+	}
+
+	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("The maximum number of results to display"))
+
+	cmd.AddCommand(workspacesList(cmdr))
+	cmd.AddCommand(workspacesCreate(cmdr))
+	cmd.AddCommand(workspacesInfo(cmdr))
+	cmd.AddCommand(workspacesDelete(cmdr))
+	cmd.AddCommand(workspacesInvite(cmdr))
+	cmd.AddCommand(workspacesRemove(cmdr))
+	cmd.AddCommand(workspacesUpdate(cmdr))
+	return cmd
+}
+
+func workspacesList(cmdr *commands.DryccCmd) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "list",
+		Short: i18n.T("Lists workspaces for current user"),
+		RunE: func(_ *cobra.Command, _ []string) error {
+			results, _ := commands.ResponseLimit(limit)
+			return cmdr.WorkspacesList(results)
+		},
+	}
+
+	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("The maximum number of results to display"))
+	return cmd
+}
+
+func workspacesCreate(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		email string
+	}
+
+	cmd := &cobra.Command{
+		Use:  "create <name>",
+		Args: cobra.ExactArgs(1),
+		Example: template.CustomExample(
+			"drycc workspaces create my-workspace --email team@example.com",
+			map[string]string{
+				"<name>": i18n.T("The name of the workspace"),
+			},
+		),
+		Short: i18n.T("Create a workspace"),
+		RunE: func(_ *cobra.Command, args []string) error {
+			return cmdr.WorkspacesCreate(args[0], flags.email)
+		},
+	}
+
+	cmd.Flags().StringVar(&flags.email, "email", "", i18n.T("The contact email for the workspace"))
+	return cmd
+}
+
+func workspacesInfo(cmdr *commands.DryccCmd) *cobra.Command {
+	workspaceCompletion := completion.WorkspaceCompletion{ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "info <name>",
+		Args: cobra.ExactArgs(1),
+		Example: template.CustomExample(
+			"drycc workspaces info my-workspace",
+			map[string]string{
+				"<name>": i18n.T("The name of the workspace"),
+			},
+		),
+		Short:             i18n.T("Print information about a workspace"),
+		ValidArgsFunction: workspaceCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			results, _ := commands.ResponseLimit(limit)
+			return cmdr.WorkspacesInfo(args[0], results)
+		},
+	}
+
+	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("The maximum number of results to display"))
+	return cmd
+}
+
+func workspacesDelete(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		confirm string
+	}
+
+	workspaceCompletion := completion.WorkspaceCompletion{ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "delete <name>",
+		Args: cobra.ExactArgs(1),
+		Example: template.CustomExample(
+			"drycc workspaces delete my-workspace",
+			map[string]string{
+				"<name>": i18n.T("The name of the workspace"),
+			},
+		),
+		Short:             i18n.T("Delete a workspace"),
+		ValidArgsFunction: workspaceCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			return cmdr.WorkspacesDelete(args[0], flags.confirm)
+		},
+	}
+
+	cmd.Flags().StringVar(&flags.confirm, "confirm", "", i18n.T("Skips the prompt for the workspace name"))
+	return cmd
+}
+
+func workspacesInvite(cmdr *commands.DryccCmd) *cobra.Command {
+	workspaceCompletion := completion.WorkspaceCompletion{ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "invite <workspace> <email>",
+		Args: cobra.ExactArgs(2),
+		Example: template.CustomExample(
+			"drycc workspaces invite my-workspace user@example.com",
+			map[string]string{
+				"<workspace>": i18n.T("The name of the workspace"),
+				"<email>":     i18n.T("The email address of the user to invite"),
+			},
+		),
+		Short:             i18n.T("Invite a user to a workspace"),
+		ValidArgsFunction: workspaceCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			return cmdr.WorkspacesInvite(args[0], args[1])
+		},
+	}
+
+	return cmd
+}
+
+func workspacesRemove(cmdr *commands.DryccCmd) *cobra.Command {
+	removeCompletion := completion.WorkspaceRemoveCompletion{ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "remove <workspace> <username>",
+		Args: cobra.ExactArgs(2),
+		Example: template.CustomExample(
+			"drycc workspaces remove my-workspace john",
+			map[string]string{
+				"<workspace>": i18n.T("The name of the workspace"),
+				"<username>":  i18n.T("The username to remove from the workspace"),
+			},
+		),
+		Short:             i18n.T("Remove a user from a workspace"),
+		ValidArgsFunction: removeCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			return cmdr.WorkspacesRemove(args[0], args[1])
+		},
+	}
+
+	return cmd
+}
+
+func workspacesUpdate(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		role   string
+		alerts string
+	}
+
+	updateCompletion := completion.WorkspaceUpdateCompletion{ConfigFile: &cmdr.ConfigFile}
+	roleCompletion := completion.WorkspaceRoleCompletion{ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
+	cmd := &cobra.Command{
+		Use:  "update <workspace> <username>",
+		Args: cobra.ExactArgs(2),
+		Example: template.CustomExample(
+			"drycc workspaces update my-workspace john --role=admin --alerts=true",
+			map[string]string{
+				"<workspace>": i18n.T("The name of the workspace"),
+				"<username>":  i18n.T("The username of the member to update"),
+			},
+		),
+		Short:             i18n.T("Update a member's role and/or alerts in a workspace"),
+		ValidArgsFunction: updateCompletion.CompletionFunc,
+		RunE: func(_ *cobra.Command, args []string) error {
+			var alerts *bool
+			if flags.alerts != "" {
+				v := flags.alerts == "true"
+				alerts = &v
+			}
+			if flags.role == "" && alerts == nil {
+				return fmt.Errorf("at least one of --role or --alerts must be specified")
+			}
+			return cmdr.WorkspacesUpdate(args[0], args[1], flags.role, alerts)
+		},
+	}
+
+	cmd.Flags().StringVar(&flags.role, "role", "", i18n.T("The role to assign: admin, member, or viewer"))
+	cmd.Flags().StringVar(&flags.alerts, "alerts", "", i18n.T("Enable or disable alerts: true or false"))
+	cmd.RegisterFlagCompletionFunc("role", roleCompletion.CompletionFunc)
+	return cmd
+}
diff --git a/internal/template/template.go b/internal/template/template.go
new file mode 100644
index 000000000..6e4ca0845
--- /dev/null
+++ b/internal/template/template.go
@@ -0,0 +1,44 @@
+// Package template provides custom template functions for CLI examples
+package template
+
+import (
+	"bytes"
+	"strings"
+	"text/template"
+)
+
+// CustomExample formats a template with the given example and key-value pairs
+func CustomExample(example string, kwargs map[string]string) string {
+	tpl, err := template.New("template").Funcs(template.FuncMap{
+		"rpad": func(str string, length int) string {
+			padding := length - len(str)
+			if padding > 0 {
+				return str + strings.Repeat(" ", padding)
+			}
+			return str
+		},
+	}).Parse(`{{.Example}}
+
+Args:
+  {{- range $key, $value := .Kwargs}}
+  {{rpad $key ($.Padding)}}{{$value}}
+  {{- end}}`)
+	if err != nil {
+		return example
+	}
+	padding, miniPadding := 0, 3
+	for key := range kwargs {
+		if padding < len(key)+miniPadding {
+			padding = len(key) + miniPadding
+		}
+	}
+	var buf bytes.Buffer
+	if err = tpl.Execute(&buf, struct {
+		Example string
+		Kwargs  map[string]string
+		Padding int
+	}{example, kwargs, padding}); err != nil {
+		return example
+	}
+	return buf.String()
+}
diff --git a/parser/apps.go b/parser/apps.go
deleted file mode 100644
index ca7baa6f3..000000000
--- a/parser/apps.go
+++ /dev/null
@@ -1,287 +0,0 @@
-package parser
-
-import (
-	"strings"
-
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Apps routes app commands to their specific function.
-func Apps(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for apps:
-
-apps:create        create a new application
-apps:list          list accessible applications
-apps:info          view info about an application
-apps:open          open the application in a browser
-apps:logs          view aggregated application logs
-apps:run           run a command in an ephemeral app container
-apps:destroy       destroy an application
-apps:transfer      transfer app ownership to another user
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "apps:create":
-		return appCreate(argv, cmdr)
-	case "apps:list":
-		return appsList(argv, cmdr)
-	case "apps:info":
-		return appInfo(argv, cmdr)
-	case "apps:open":
-		return appOpen(argv, cmdr)
-	case "apps:logs":
-		return appLogs(argv, cmdr)
-	case "apps:run":
-		return appRun(argv, cmdr)
-	case "apps:destroy":
-		return appDestroy(argv, cmdr)
-	case "apps:transfer":
-		return appTransfer(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "apps" {
-			argv[0] = "apps:list"
-			return appsList(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func appCreate(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Creates a new application.
-
-- if no <id> is provided, one will be generated automatically.
-
-Usage: drycc apps:create [<id>] [options]
-
-Arguments:
-  <id>
-    a uniquely identifiable name for the application. No other app can already
-    exist with this name.
-
-Options:
-  --no-remote
-    do not create a 'drycc' git remote.
-  -r --remote REMOTE
-    name of remote to create. [default: drycc]
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	id := safeGetString(args, "<id>")
-	remote := safeGetString(args, "--remote")
-	noRemote := safeGetBool(args, "--no-remote")
-
-	return cmdr.AppCreate(id, remote, noRemote)
-}
-
-func appsList(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Lists applications visible to the current user.
-
-Usage: drycc apps:list [options]
-
-Options:
-  -l --limit=<num>
-    the maximum number of results to display, defaults to config setting
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	results, err := responseLimit(safeGetString(args, "--limit"))
-
-	if err != nil {
-		return err
-	}
-
-	return cmdr.AppsList(results)
-}
-
-func appInfo(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Prints info about the current application.
-
-Usage: drycc apps:info [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-
-	return cmdr.AppInfo(app)
-}
-
-func appOpen(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Opens a URL to the application in the default browser.
-
-Usage: drycc apps:open [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-
-	return cmdr.AppOpen(app)
-}
-
-func appLogs(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Retrieves the most recent log events.
-
-Usage: drycc apps:logs [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-  -n --lines=<lines>
-    the number of lines to display.
-  -f --follow
-    specify if the logs should be streamed.
-  -t --timeout=<timeout>
-    the max seconds of follow the log stream.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	lines := safeGetInt(args, "--lines")
-	if lines <= 0 {
-		lines = 300
-	}
-	follow := safeGetBool(args, "--follow")
-	timeout := safeGetInt(args, "--timeout")
-	if timeout <= 0 {
-		timeout = 300
-	}
-
-	return cmdr.AppLogs(app, lines, follow, timeout)
-}
-
-func appRun(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Runs a command inside an ephemeral app container.
-
-Usage: drycc apps:run [--mount=<volume>:<path>...] [options] [--] <command>...
-
-Arguments:
-  <volume>
-    the volume name
-  <path>
-    the filesystem path.
-  <command>
-    the shell command to run inside the container.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-  --timeout=<timeout>
-    the timeout for command run, default to 3600 seconds.
-  --expires=<expires>
-    retention time of running records, default to 3600 seconds.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	timeout := uint32(safeGetInt(args, "--timeout"))
-	expires := uint32(safeGetInt(args, "--expires"))
-	command := strings.Join(args["<command>"].([]string), " ")
-	mounts := args["--mount"].([]string)
-	return cmdr.AppRun(app, command, mounts, timeout, expires)
-}
-
-func appDestroy(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Destroys an application.
-
-Usage: drycc apps:destroy [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-  --confirm=<app>
-    skips the prompt for the application name. <app> is the uniquely identifiable
-    name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	confirm := safeGetString(args, "--confirm")
-
-	return cmdr.AppDestroy(app, confirm)
-}
-
-func appTransfer(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Transfer app ownership to another user.
-
-Usage: drycc apps:transfer <username> [options]
-
-Arguments:
-  <username>
-    the user that the app will be transferred to.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	user := safeGetString(args, "<username>")
-
-	return cmdr.AppTransfer(app, user)
-}
diff --git a/parser/apps_test.go b/parser/apps_test.go
deleted file mode 100644
index a378f80ce..000000000
--- a/parser/apps_test.go
+++ /dev/null
@@ -1,118 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) AppCreate(string, string, bool) error {
-	return errors.New("apps:create")
-}
-
-func (d FakeDryccCmd) AppsList(int) error {
-	return errors.New("apps:list")
-}
-
-func (d FakeDryccCmd) AppInfo(string) error {
-	return errors.New("apps:info")
-}
-
-func (d FakeDryccCmd) AppOpen(string) error {
-	return errors.New("apps:open")
-}
-
-func (d FakeDryccCmd) AppLogs(string, int, bool, int) error {
-	return errors.New("apps:logs")
-}
-
-func (d FakeDryccCmd) AppRun(string, string, []string, uint32, uint32) error {
-	return errors.New("apps:run")
-}
-
-func (d FakeDryccCmd) AppDestroy(string, string) error {
-	return errors.New("apps:destroy")
-}
-
-func (d FakeDryccCmd) AppTransfer(string, string) error {
-	return errors.New("apps:transfer")
-}
-
-func TestApps(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"apps:create"},
-			expected: "",
-		},
-		{
-			args:     []string{"apps:list"},
-			expected: "",
-		},
-		{
-			args:     []string{"apps:info"},
-			expected: "",
-		},
-		{
-			args:     []string{"apps:open"},
-			expected: "",
-		},
-		{
-			args:     []string{"apps:logs"},
-			expected: "",
-		},
-		{
-			args:     []string{"apps:logs", "--lines=1"},
-			expected: "",
-		},
-		{
-			args:     []string{"apps:run", "ls"},
-			expected: "",
-		},
-		{
-			args:     []string{"apps:destroy"},
-			expected: "",
-		},
-		{
-			args:     []string{"apps:transfer", "test-user"},
-			expected: "",
-		},
-		{
-			args:     []string{"apps"},
-			expected: "apps:list",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Apps(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/auth.go b/parser/auth.go
deleted file mode 100644
index 8358b426e..000000000
--- a/parser/auth.go
+++ /dev/null
@@ -1,108 +0,0 @@
-package parser
-
-import (
-	"fmt"
-
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Auth routes auth commands to the specific function.
-func Auth(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for auth:
-
-auth:login             authenticate against a controller
-auth:logout            clear the current user session
-auth:whoami            display the current user
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "auth:login":
-		return authLogin(argv, cmdr)
-	case "auth:logout":
-		return authLogout(argv, cmdr)
-	case "auth:whoami":
-		return authWhoami(argv, cmdr)
-	case "auth":
-		fmt.Print(usage)
-		return nil
-	default:
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func authLogin(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Logs in by authenticating against a controller.
-
-Usage: drycc auth:login <controller> [options]
-
-Arguments:
-  <controller>
-    a fully-qualified controller URI, e.g. "http://drycc.local3.dryccapp.com/".
-
-Options:
-  --username=<username>
-    provide a username for the account.
-  --password=<password>
-    provide a password for the account.
-  --ssl-verify=true
-    enables/disables SSL certificate verification for API requests
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	controller := safeGetString(args, "<controller>")
-	sslVerify := true
-	if args["--ssl-verify"] != nil && args["--ssl-verify"].(string) == "false" {
-		sslVerify = false
-	}
-	username := safeGetString(args, "--username")
-	password := safeGetString(args, "--password")
-
-	return cmdr.Login(controller, sslVerify, username, password)
-}
-
-func authLogout(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Logs out from a controller and clears the user session.
-
-Usage: drycc auth:logout
-
-Options:
-`
-
-	if _, err := docopt.ParseArgs(usage, argv, ""); err != nil {
-		return err
-	}
-
-	return cmdr.Logout()
-}
-
-func authWhoami(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Displays the currently logged in user.
-
-Usage: drycc auth:whoami [options]
-
-Options:
-  --all
-    fetch a more detailed description about the user.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	return cmdr.Whoami(args["--all"].(bool))
-}
diff --git a/parser/auth_test.go b/parser/auth_test.go
deleted file mode 100644
index 333eae547..000000000
--- a/parser/auth_test.go
+++ /dev/null
@@ -1,70 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) Login(string, bool, string, string) error {
-	return errors.New("auth:login")
-}
-
-func (d FakeDryccCmd) Logout() error {
-	return errors.New("auth:logout")
-}
-
-func (d FakeDryccCmd) Whoami(bool) error {
-	return errors.New("auth:whoami")
-}
-
-func TestAuth(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"auth:login", server.Server.URL},
-			expected: "",
-		},
-		{
-			args:     []string{"auth:logout"},
-			expected: "",
-		},
-		{
-			args:     []string{"auth:whoami"},
-			expected: "",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Auth(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/autoscale.go b/parser/autoscale.go
deleted file mode 100644
index 3e62219e6..000000000
--- a/parser/autoscale.go
+++ /dev/null
@@ -1,123 +0,0 @@
-package parser
-
-import (
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Autoscale displays all relevant commands for `drycc autoscale`.
-func Autoscale(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for autoscale:
-
-autoscale:list   list autoscale options of an application
-autoscale:set    turn on autoscale for an app
-autoscale:unset  turn off autoscale for an app
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "autoscale:list":
-		return autoscaleList(argv, cmdr)
-	case "autoscale:set":
-		return autoscaleSet(argv, cmdr)
-	case "autoscale:unset":
-		return autoscaleUnset(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "autoscale" {
-			argv[0] = "autoscale:list"
-			return autoscaleList(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func autoscaleList(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Prints a list of autoscale options for the application.
-
-Usage: drycc autoscale:list [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	return cmdr.AutoscaleList(safeGetString(args, "--app"))
-}
-
-func autoscaleSet(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Set autoscale option per process type for an app.
-
-Usage: drycc autoscale:set <process-type> --min=<min> --max=<max> --cpu-percent=<percent> [options]
-
-Arguments:
-  <process-type>
-    the process type to add to the application's autoscale settings.
-  --min=<min>
-	minimum replicas to keep around
-  --max=<max>
-	max replicas to scale up to
-  --cpu-percent=<cpu-percent>
-	target CPU utilization
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name of the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	processType := args["<process-type>"].(string)
-	app := safeGetString(args, "--app")
-	min := safeGetInt(args, "--min")
-	max := safeGetInt(args, "--max")
-	CPUPercent := safeGetInt(args, "--cpu-percent")
-
-	return cmdr.AutoscaleSet(app, processType, min, max, CPUPercent)
-}
-
-func autoscaleUnset(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Unset autoscale per process type for an app.
-
-Usage: drycc autoscale:unset <process-type> [options]
-
-Arguments:
-  <process-type>
-    the process type to remove from the application's autoscale settings.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name of the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	processType := args["<process-type>"].(string)
-	app := safeGetString(args, "--app")
-
-	return cmdr.AutoscaleUnset(app, processType)
-}
diff --git a/parser/autoscale_test.go b/parser/autoscale_test.go
deleted file mode 100644
index a581b74af..000000000
--- a/parser/autoscale_test.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) AutoscaleList(string) error {
-	return errors.New("autoscale:list")
-}
-
-func (d FakeDryccCmd) AutoscaleSet(string, string, int, int, int) error {
-	return errors.New("autoscale:set")
-}
-
-func (d FakeDryccCmd) AutoscaleUnset(string, string) error {
-	return errors.New("autoscale:unset")
-}
-
-func TestAutoscale(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"autoscale:list"},
-			expected: "",
-		},
-		{
-			args:     []string{"autoscale:set", "web", "--min=1", "--max=3", "--cpu-percent=50"},
-			expected: "",
-		},
-		{
-			args:     []string{"autoscale:unset", "web"},
-			expected: "",
-		},
-		{
-			args:     []string{"autoscale"},
-			expected: "autoscale:list",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Autoscale(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/builds.go b/parser/builds.go
deleted file mode 100644
index 22993d6ae..000000000
--- a/parser/builds.go
+++ /dev/null
@@ -1,108 +0,0 @@
-package parser
-
-import (
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Builds routes build commands to their specific function.
-func Builds(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for builds:
-
-builds:list        list build history for an application
-builds:create      imports an image and deploys as a new release
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "builds:list":
-		return buildsList(argv, cmdr)
-	case "builds:create":
-		return buildsCreate(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "builds" {
-			argv[0] = "builds:list"
-			return buildsList(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func buildsList(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Lists build history for an application.
-
-Usage: drycc builds:list [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-  -l --limit=<num>
-    the maximum number of results to display, defaults to config setting
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	results, err := responseLimit(safeGetString(args, "--limit"))
-
-	if err != nil {
-		return err
-	}
-
-	return cmdr.BuildsList(safeGetString(args, "--app"), results)
-}
-
-func buildsCreate(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Creates a new build of an application. Imports an <image> and deploys it to Drycc
-as a new release. If a Procfile or drycc.yaml is present in the current directory,
-it will be used as the default for this application.
-
-Usage: drycc builds:create <image> [options]
-
-Arguments:
-  <image>
-    A default fully-qualified container image, either from Drycc Registry (e.g. registry.drycc.cc/drycc/example-go:latest)
-    or from an in-house registry (e.g. myregistry.example.com:5000/example-go:latest).
-    This image must include the tag.
-
-Options:
-  -a --app=<app>
-    The uniquely identifiable name for the application.
-  -s --stack=<stack>
-    The stack name for the application, defaults to container.
-  -p --procfile=<procfile>
-    A YAML file used to supply a Procfile to the application.
-  -d --dryccfile=<dryccfile>
-    A YAML file used to supply a drycc.yaml to the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	image := safeGetString(args, "<image>")
-	stack := safeGetString(args, "--stack")
-	if stack == "" {
-		stack = "container"
-	}
-	procfile := safeGetValue(args, "--procfile", "Procfile")
-	dryccfile := safeGetValue(args, "--dryccfile", "drycc.yaml")
-
-	return cmdr.BuildsCreate(app, image, stack, procfile, dryccfile)
-}
diff --git a/parser/builds_test.go b/parser/builds_test.go
deleted file mode 100644
index dd65c2bb3..000000000
--- a/parser/builds_test.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-func (d FakeDryccCmd) BuildsList(string, int) error {
-	return errors.New("builds:list")
-}
-
-func (d FakeDryccCmd) BuildsCreate(string, string, string, string, string) error {
-	return errors.New("builds:create")
-}
-
-func TestBuilds(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"builds:list"},
-			expected: "",
-		},
-		{
-			args:     []string{"builds:create", "drycc/example-go:latest"},
-			expected: "",
-		},
-		{
-			args:     []string{"builds"},
-			expected: "builds:list",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Builds(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/canary.go b/parser/canary.go
deleted file mode 100644
index 9acf9de1e..000000000
--- a/parser/canary.go
+++ /dev/null
@@ -1,157 +0,0 @@
-package parser
-
-import (
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Canary displays all relevant commands for `drycc autoscale`.
-func Canary(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for canary:
-
-canary:info     list canary options of an application
-canary:create   turn on canary for an app procfile type
-canary:remove   turn off canary for an app procfile type
-canary:release  release canary deploy for an app
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "canary:info":
-		return canaryInfo(argv, cmdr)
-	case "canary:create":
-		return canaryCreate(argv, cmdr)
-	case "canary:remove":
-		return canaryRemove(argv, cmdr)
-	case "canary:release":
-		return canaryRelease(argv, cmdr)
-	case "canary:rollback":
-		return canaryRollback(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "canary" {
-			argv[0] = "canary:info"
-			return canaryInfo(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func canaryInfo(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Prints a list of canary options for the application.
-
-Usage: drycc canary:info [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	return cmdr.CanaryInfo(safeGetString(args, "--app"))
-}
-
-func canaryCreate(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Set canary option type for an app.
-
-Usage: drycc canary:create <process-type>... [options]
-
-Arguments:
-  <process-type>
-    the process type to add to the application's canary settings.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name of the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	return cmdr.CanaryCreate(app, args["<process-type>"].([]string))
-}
-
-func canaryRemove(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Remove canary option type for an app.
-
-Usage: drycc canary:remove <process-type>... [options]
-
-Arguments:
-  <process-type>
-    the process type to add to the application's canary settings.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name of the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	return cmdr.CanaryRemove(app, args["<process-type>"].([]string))
-}
-
-func canaryRelease(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Release canary deploy for an app.
-
-Usage: drycc canary:release [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name of the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	return cmdr.CanaryRelease(app)
-}
-
-func canaryRollback(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Rollback canary deploy for an app.
-
-Usage: drycc canary:rollback [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name of the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	return cmdr.CanaryRollback(app)
-}
diff --git a/parser/canary_test.go b/parser/canary_test.go
deleted file mode 100644
index 61dd58d8d..000000000
--- a/parser/canary_test.go
+++ /dev/null
@@ -1,90 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) CanaryInfo(string) error {
-	return errors.New("canary:info")
-}
-
-func (d FakeDryccCmd) CanaryCreate(string, []string) error {
-	return errors.New("canary:create")
-}
-
-func (d FakeDryccCmd) CanaryRemove(string, []string) error {
-	return errors.New("canary:remove")
-}
-
-func (d FakeDryccCmd) CanaryRelease(string) error {
-	return errors.New("canary:release")
-}
-
-func (d FakeDryccCmd) CanaryRollback(string) error {
-	return errors.New("canary:rollback")
-}
-
-func TestCanary(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"canary:info"},
-			expected: "",
-		},
-		{
-			args:     []string{"canary:create", "web cmd"},
-			expected: "",
-		},
-		{
-			args:     []string{"canary:remove", "web cmd"},
-			expected: "",
-		},
-		{
-			args:     []string{"canary:release"},
-			expected: "",
-		},
-		{
-			args:     []string{"canary:rollback"},
-			expected: "",
-		},
-		{
-			args:     []string{"canary"},
-			expected: "canary:info",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Canary(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/certs.go b/parser/certs.go
deleted file mode 100644
index 5273ed515..000000000
--- a/parser/certs.go
+++ /dev/null
@@ -1,194 +0,0 @@
-package parser
-
-import (
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Certs routes certs commands to their specific function.
-func Certs(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for certs:
-
-certs:list            list SSL certificates for an app
-certs:add             add an SSL certificate to an app
-certs:remove          remove an SSL certificate from an app
-certs:info            get detailed informaton about the certificate
-certs:attach          attach an SSL certificate to a domain
-certs:detach          detach an SSL certificate from a domain
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "certs:list":
-		return certsList(argv, cmdr)
-	case "certs:add":
-		return certAdd(argv, cmdr)
-	case "certs:remove":
-		return certRemove(argv, cmdr)
-	case "certs:info":
-		return certInfo(argv, cmdr)
-	case "certs:attach":
-		return certAttach(argv, cmdr)
-	case "certs:detach":
-		return certDetach(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "certs" {
-			argv[0] = "certs:list"
-			return certsList(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func certsList(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Show certificate information for an SSL application.
-
-Usage: drycc certs:list [options]
-
-Options:
-  -l --limit=<num>
-    the maximum number of results to display, defaults to config setting
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-	if err != nil {
-		return err
-	}
-
-	results, err := responseLimit(safeGetString(args, "--limit"))
-	if err != nil {
-		return err
-	}
-
-	return cmdr.CertsList(results)
-}
-
-func certAdd(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Binds a certificate/key pair to an application.
-
-Usage: drycc certs:add <name> <cert> <key> [options]
-
-Arguments:
-  <name>
-    Name of the certificate to reference it by.
-  <cert>
-    The public key of the SSL certificate.
-  <key>
-    The private key of the SSL certificate.
-
-Options:
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-	if err != nil {
-		return err
-	}
-
-	name := args["<name>"].(string)
-	cert := args["<cert>"].(string)
-	key := args["<key>"].(string)
-
-	return cmdr.CertAdd(cert, key, name)
-}
-
-func certRemove(argv []string, cmdr cmd.Commander) error {
-	usage := `
-removes a certificate/key pair from the application.
-
-Usage: drycc certs:remove <name> [options]
-
-Arguments:
-  <name>
-    the name of the cert to remove from the app.
-
-Options:
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-	if err != nil {
-		return err
-	}
-
-	return cmdr.CertRemove(safeGetString(args, "<name>"))
-}
-
-func certInfo(argv []string, cmdr cmd.Commander) error {
-	usage := `
-fetch more detailed information about a certificate
-
-Usage: drycc certs:info <name> [options]
-
-Arguments:
-  <name>
-    the name of the cert to get information from
-
-Options:
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-	if err != nil {
-		return err
-	}
-
-	return cmdr.CertInfo(safeGetString(args, "<name>"))
-}
-
-func certAttach(argv []string, cmdr cmd.Commander) error {
-	usage := `
-attach a certificate to a domain.
-
-Usage: drycc certs:attach <name> <domain> [options]
-
-Arguments:
-  <name>
-    name of the certificate to attach domain to
-  <domain>
-    common name of the domain to attach to (needs to already be in the system)
-
-Options:
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-	if err != nil {
-		return err
-	}
-
-	name := safeGetString(args, "<name>")
-	domain := safeGetString(args, "<domain>")
-	return cmdr.CertAttach(name, domain)
-}
-
-func certDetach(argv []string, cmdr cmd.Commander) error {
-	usage := `
-detach a certificate from a domain.
-
-Usage: drycc certs:detach <name> <domain> [options]
-
-Arguments:
-  <name>
-    name of the certificate to deatch from a domain
-  <domain>
-    common name of the domain to detach from
-
-Options:
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-	if err != nil {
-		return err
-	}
-
-	name := safeGetString(args, "<name>")
-	domain := safeGetString(args, "<domain>")
-	return cmdr.CertDetach(name, domain)
-}
diff --git a/parser/certs_test.go b/parser/certs_test.go
deleted file mode 100644
index 78e8581b4..000000000
--- a/parser/certs_test.go
+++ /dev/null
@@ -1,98 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) CertsList(int) error {
-	return errors.New("certs:list")
-}
-
-func (d FakeDryccCmd) CertAdd(string, string, string) error {
-	return errors.New("certs:add")
-}
-
-func (d FakeDryccCmd) CertRemove(string) error {
-	return errors.New("certs:remove")
-}
-
-func (d FakeDryccCmd) CertInfo(string) error {
-	return errors.New("certs:info")
-}
-
-func (d FakeDryccCmd) CertAttach(string, string) error {
-	return errors.New("certs:attach")
-}
-
-func (d FakeDryccCmd) CertDetach(string, string) error {
-	return errors.New("certs:detach")
-}
-
-func TestCerts(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"certs:list"},
-			expected: "",
-		},
-		{
-			args:     []string{"certs:add", "name", "cert", "key"},
-			expected: "",
-		},
-		{
-			args:     []string{"certs:remove", "name"},
-			expected: "",
-		},
-		{
-			args:     []string{"certs:info", "name"},
-			expected: "",
-		},
-		{
-			args:     []string{"certs:attach", "name", "example.com"},
-			expected: "",
-		},
-		{
-			args:     []string{"certs:detach", "name", "example.com"},
-			expected: "",
-		},
-		{
-			args:     []string{"certs"},
-			expected: "certs:list",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Certs(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/config.go b/parser/config.go
deleted file mode 100644
index 9dcfc7ab9..000000000
--- a/parser/config.go
+++ /dev/null
@@ -1,196 +0,0 @@
-package parser
-
-import (
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Config routes config commands to their specific function.
-func Config(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for config:
-
-config:list        list environment variables for an app
-config:set         set environment variables for an app
-config:unset       unset environment variables for an app
-config:pull        pull environment variables to the path
-config:push        push environment variables from the path
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "config:list":
-		return configList(argv, cmdr)
-	case "config:set":
-		return configSet(argv, cmdr)
-	case "config:unset":
-		return configUnset(argv, cmdr)
-	case "config:pull":
-		return configPull(argv, cmdr)
-	case "config:push":
-		return configPush(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "config" {
-			argv[0] = "config:list"
-			return configList(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func configList(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Lists environment variables for an application.
-
-Usage: drycc config:list [options]
-
-Options:
-  -a --app=<app>
-    the application that you wish to listed.
-  --type=<type>
-    the procType for which the config needs to be listed.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-	app := safeGetString(args, "--app")
-	procType := safeGetString(args, "--type")
-	return cmdr.ConfigList(app, procType)
-}
-
-func configSet(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Sets environment variables for an application.
-
-Usage: drycc config:set <var>=<value> [<var>=<value>...] [options]
-
-Arguments:
-  <var>
-    the uniquely identifiable name for the environment variable.
-  <value>
-    the value of said environment variable.
-
-Options:
-  -a --app=<app>
-    the application that you wish to set.
-  --type=<type>
-    the procType for which the config needs to be set.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	procType := safeGetString(args, "--type")
-
-	return cmdr.ConfigSet(app, procType, args["<var>=<value>"].([]string))
-}
-
-func configUnset(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Unsets an environment variable for an application.
-
-Usage: drycc config:unset <key>... [options]
-
-Arguments:
-  <key>
-    the variable to remove from the application's environment.
-
-Options:
-  -a --app=<app>
-    the application that you wish to unset.
-  --type=<type>
-    the procType for which the config needs to be unset.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-	app := safeGetString(args, "--app")
-	procType := safeGetString(args, "--type")
-	return cmdr.ConfigUnset(app, procType, args["<key>"].([]string))
-}
-
-func configPull(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Extract all environment variables from an application for local use.
-
-The environmental variables can be piped into a file, 'drycc config:pull > file',
-or stored locally in a file named .env. This file can be
-read by foreman to load the local environment for your app.
-
-Usage: drycc config:pull [options]
-
-Options:
-  -a --app=<app>
-    the application that you wish to pull.
-  --type=<type>
-    the procType for which the config needs to be pull.
-  --path=<path>
-    a path leading to an environment file [default: .env]
-  -i --interactive
-    prompts for each value to be overwritten.
-  -o --overwrite
-    allows you to have the pull overwrite keys to the path.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	procType := safeGetString(args, "--type")
-	path := safeGetValue(args, "--path", ".env")
-	interactive := args["--interactive"].(bool)
-	overwrite := args["--overwrite"].(bool)
-
-	return cmdr.ConfigPull(app, procType, path, interactive, overwrite)
-}
-
-func configPush(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Sets environment variables for an application.
-
-This file can be read by foreman
-to load the local environment for your app. The file should be piped via
-stdin, 'drycc config:push < .env', or using the --path option.
-
-Usage: drycc config:push [options]
-
-Options:
-  -a --app=<app>
-    the application that you wish to push.
-  --type=<type>
-    the procType for which the config needs to be push.
-  --path=<path>
-    a path leading to an environment file [default: .env]
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	procType := safeGetString(args, "--type")
-	path := safeGetValue(args, "--path", ".env")
-	return cmdr.ConfigPush(app, procType, path)
-}
diff --git a/parser/config_test.go b/parser/config_test.go
deleted file mode 100644
index a9b9b4651..000000000
--- a/parser/config_test.go
+++ /dev/null
@@ -1,90 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) ConfigList(string, string) error {
-	return errors.New("config:list")
-}
-
-func (d FakeDryccCmd) ConfigSet(string, string, []string) error {
-	return errors.New("config:set")
-}
-
-func (d FakeDryccCmd) ConfigUnset(string, string, []string) error {
-	return errors.New("config:unset")
-}
-
-func (d FakeDryccCmd) ConfigPull(string, string, string, bool, bool) error {
-	return errors.New("config:pull")
-}
-
-func (d FakeDryccCmd) ConfigPush(string, string, string) error {
-	return errors.New("config:push")
-}
-
-func TestConfig(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"config:list"},
-			expected: "",
-		},
-		{
-			args:     []string{"config:set", "var=value"},
-			expected: "",
-		},
-		{
-			args:     []string{"config:unset", "var"},
-			expected: "",
-		},
-		{
-			args:     []string{"config:pull"},
-			expected: "",
-		},
-		{
-			args:     []string{"config:push"},
-			expected: "",
-		},
-		{
-			args:     []string{"config"},
-			expected: "config:list",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Config(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/domains.go b/parser/domains.go
deleted file mode 100644
index e1fbacfe6..000000000
--- a/parser/domains.go
+++ /dev/null
@@ -1,123 +0,0 @@
-package parser
-
-import (
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Domains routes domain commands to their specific function.
-func Domains(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for domains:
-
-domains:add           bind a domain to an application
-domains:list          list domains bound to an application
-domains:remove        unbind a domain from an application
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "domains:add":
-		return domainsAdd(argv, cmdr)
-	case "domains:list":
-		return domainsList(argv, cmdr)
-	case "domains:remove":
-		return domainsRemove(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "domains" {
-			argv[0] = "domains:list"
-			return domainsList(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func domainsAdd(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Binds a domain to an application.
-
-Usage: drycc domains:add <domain> [options]
-
-Arguments:
-  <domain>
-    the domain name to be bound to the application, such as 'domain.dryccapp.com'.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	domain := safeGetString(args, "<domain>")
-
-	return cmdr.DomainsAdd(app, domain)
-}
-
-func domainsList(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Lists domains bound to an application.
-
-Usage: drycc domains:list [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-  -l --limit=<num>
-    the maximum number of results to display, defaults to config setting
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	results, err := responseLimit(safeGetString(args, "--limit"))
-
-	if err != nil {
-		return err
-	}
-	app := safeGetString(args, "--app")
-
-	return cmdr.DomainsList(app, results)
-}
-
-func domainsRemove(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Unbinds a domain for an application.
-
-Usage: drycc domains:remove <domain> [options]
-
-Arguments:
-  <domain>
-    the domain name to be removed from the application.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	domain := safeGetString(args, "<domain>")
-
-	return cmdr.DomainsRemove(app, domain)
-}
diff --git a/parser/domains_test.go b/parser/domains_test.go
deleted file mode 100644
index 11e542710..000000000
--- a/parser/domains_test.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) DomainsList(string, int) error {
-	return errors.New("domains:list")
-}
-
-func (d FakeDryccCmd) DomainsAdd(string, string) error {
-	return errors.New("domains:add")
-}
-
-func (d FakeDryccCmd) DomainsRemove(string, string) error {
-	return errors.New("domains:remove")
-}
-
-func TestDomains(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"domains:add", "example.com"},
-			expected: "",
-		},
-		{
-			args:     []string{"domains:list"},
-			expected: "",
-		},
-		{
-			args:     []string{"domains:remove", "example.com"},
-			expected: "",
-		},
-		{
-			args:     []string{"domains"},
-			expected: "domains:list",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Domains(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/gateways.go b/parser/gateways.go
deleted file mode 100644
index e903066f9..000000000
--- a/parser/gateways.go
+++ /dev/null
@@ -1,134 +0,0 @@
-package parser
-
-import (
-	docopt "github.com/docopt/docopt-go"
-
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Gateways gateways commands to their specific function.
-func Gateways(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for gateways:
-
-gateways:add           create gateways for an application
-gateways:list          list application gateways
-gateways:remove        remove gateways from an application
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "gateways:add":
-		return gatewaysAdd(argv, cmdr)
-	case "gateways:list":
-		return gatewaysList(argv, cmdr)
-	case "gateways:remove":
-		return gatewaysRemove(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "gateways" {
-			argv[0] = "gateways:list"
-			return gatewaysList(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func gatewaysAdd(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Creates gateways for an application and binds it to allow listener of the main app domain
-
-Usage: drycc gateways:add <name> --port=<port> --protocol=<protocol> [options]
-
-Arguments:
-  <name>
-    the gateway name.
-  <port> 
-    port is the network port, the listener expects to receive.
-  <protocol>
-    protocol specifies the network protocol this listener expects to receive. Supports TCP, UDP, TLS, HTTP, and HTTPS.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	name := safeGetString(args, "<name>")
-	port := safeGetInt(args, "--port")
-	protocol := safeGetString(args, "--protocol")
-
-	return cmdr.GatewaysAdd(app, name, port, protocol)
-}
-
-func gatewaysList(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Lists gateways for an application
-
-Usage: drycc gateways:list [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-  -l --limit=<num>
-    the maximum number of results to display, defaults to config setting
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-	results, err := responseLimit(safeGetString(args, "--limit"))
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-
-	return cmdr.GatewaysList(app, results)
-}
-
-func gatewaysRemove(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Deletes specific gateway for application
-
-Usage: drycc gateways:remove <name> --port=<port> --protocol=<protocol> [options]
-
-Arguments:
-  <name>
-    the gateway name.
-  <port> 
-    port is the network port, the listener expects to receive.
-  <protocol>
-    protocol specifies the network protocol this listener expects to receive.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-	app := safeGetString(args, "--app")
-	name := safeGetString(args, "<name>")
-	port := safeGetInt(args, "--port")
-	protocol := safeGetString(args, "--protocol")
-
-	return cmdr.GatewaysRemove(app, name, port, protocol)
-}
diff --git a/parser/gateways_test.go b/parser/gateways_test.go
deleted file mode 100644
index f22733f04..000000000
--- a/parser/gateways_test.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) GatewaysList(string, int) error {
-	return errors.New("gateways:list")
-}
-
-func (d FakeDryccCmd) GatewaysAdd(string, string, int, string) error {
-	return errors.New("gateways:add")
-}
-
-func (d FakeDryccCmd) GatewaysRemove(string, string, int, string) error {
-	return errors.New("gateways:remove")
-}
-
-func TestGateways(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"gateways:add", "example", "--port=80", "--protocol=http"},
-			expected: "",
-		},
-		{
-			args:     []string{"gateways:list"},
-			expected: "",
-		},
-		{
-			args:     []string{"gateways:remove", "example", "--port=80", "--protocol=http"},
-			expected: "",
-		},
-		{
-			args:     []string{"gateways"},
-			expected: "gateways:list",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Gateways(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/git.go b/parser/git.go
deleted file mode 100644
index efe5e3794..000000000
--- a/parser/git.go
+++ /dev/null
@@ -1,81 +0,0 @@
-package parser
-
-import (
-	"fmt"
-
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Git routes git commands to their specific function.
-func Git(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for git:
-
-git:remote          Adds git remote of application to repository
-git:remove          Removes git remote of application from repository
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "git:remote":
-		return gitRemote(argv, cmdr)
-	case "git:remove":
-		return gitRemove(argv, cmdr)
-	case "git":
-		fmt.Print(usage)
-		return nil
-	default:
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func gitRemote(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Adds git remote of application to repository
-
-Usage: drycc git:remote [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-  -r --remote=REMOTE
-    name of remote to create. [default: drycc]
-  -f --force
-    overwrite remote of the given name if it already exists.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	remote := safeGetString(args, "--remote")
-	force := args["--force"].(bool)
-
-	return cmdr.GitRemote(app, remote, force)
-}
-
-func gitRemove(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Removes git remotes of application from repository.
-
-Usage: drycc git:remove [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	return cmdr.GitRemove(safeGetString(args, "--app"))
-}
diff --git a/parser/git_test.go b/parser/git_test.go
deleted file mode 100644
index a42a25076..000000000
--- a/parser/git_test.go
+++ /dev/null
@@ -1,62 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) GitRemote(string, string, bool) error {
-	return errors.New("git:remote")
-}
-
-func (d FakeDryccCmd) GitRemove(string) error {
-	return errors.New("git:remove")
-}
-
-func TestGit(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"git:remote"},
-			expected: "",
-		},
-		{
-			args:     []string{"git:remove"},
-			expected: "",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Git(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/healthchecks.go b/parser/healthchecks.go
deleted file mode 100644
index 163059de9..000000000
--- a/parser/healthchecks.go
+++ /dev/null
@@ -1,291 +0,0 @@
-package parser
-
-import (
-	"fmt"
-	"strconv"
-	"strings"
-
-	"github.com/drycc/workflow-cli/cmd"
-
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/controller-sdk-go/api"
-)
-
-// TODO: This is for supporting backward compatibility and should be removed
-// in future when next major version will be released.
-const (
-	defaultProcType string = "web"
-)
-
-// Healthchecks routes ealthcheck commands to their specific function
-func Healthchecks(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for healthchecks:
-
-healthchecks:list        list healthchecks for an app
-healthchecks:set         set healthchecks for an app
-healthchecks:unset       unset healthchecks for an app
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "healthchecks:list":
-		return healthchecksList(argv, cmdr)
-	case "healthchecks:set":
-		return healthchecksSet(argv, cmdr)
-	case "healthchecks:unset":
-		return healthchecksUnset(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "healthchecks" {
-			argv[0] = "healthchecks:list"
-			return healthchecksList(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func healthchecksList(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Lists healthchecks for an application.
-
-Usage: drycc healthchecks:list [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name of the application.
-  --type=<type>
-    the procType for which the health check needs to be listed.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	procType := safeGetString(args, "--type")
-
-	return cmdr.HealthchecksList(app, procType)
-}
-
-func healthchecksSet(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Sets healthchecks for an application.
-
-By default, Workflow only checks that the application starts in their Container. A health
-check may be added by configuring a health check probe for the application. The health
-checks are implemented as Kubernetes Container Probes. A 'liveness' and a 'readiness'
-probe can be configured, and each probe can be of type 'httpGet', 'exec' or 'tcpSocket'
-depending on the type of probe the Container requires.
-
-A 'liveness' probe is useful for applications running for long periods of time, eventually
-transitioning to broken states and cannot recover except by restarting them.
-
-Other times, a 'readiness' probe is useful when the Container is only temporarily unable
-to serve, and will recover on its own. In this case, if a Container fails its 'readiness'
-probe, the Container will not be shut down, but rather the Container will stop receiving
-incoming requests.
-
-'httpGet' probes are just as it sounds: it performs a HTTP GET operation on the Container.
-A response code inside the 200-399 range is considered a pass. 'httpGet' probes accept a
-port number to perform the HTTP GET operation on the Container.
-
-'exec' probes run a command inside the Container to determine its health. An exit code of
-zero is considered a pass, while a non-zero status code is considered a fail. 'exec'
-probes accept a string of arguments to be run inside the Container.
-
-'tcpSocket' probes attempt to open a socket in the Container. The Container is only
-considered healthy if the check can establish a connection. 'tcpSocket' probes accept a
-port number to perform the socket connection on the Container.
-
-Usage: drycc healthchecks:set <health-type> <probe-type> [options] [--] <args>...
-
-Arguments:
-  <health-type>
-    the healthcheck type, such as 'livenessProbe' or 'readinessProbe'.
-  <probe-type>
-    the healthcheck probe type, such as 'httpGet', 'exec' or 'tcpSocket'.
-  <args>
-    The arguments required for the healthcheck probe. 'exec', accepts a list of arguments;
-    'httpGet' and 'tcpSocket' accept a port number.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-  -p --path=<path>
-    the relative URL path for 'httpGet' probes. [default: /]
-  --type=<type>
-    the procType for which the health check needs to be applied.
-  --headers=<headers>...
-    the HTTP headers to send for 'httpGet' probes, separated by commas.
-  --initial-delay-timeout=<initial-delay-timeout>
-    the initial delay timeout for the probe [default: 50]
-  --timeout-seconds=<timeout-seconds>
-    the number of seconds after which the probe times out [default: 50]
-  --period-seconds=<period-seconds>
-    how often (in seconds) to perform the probe [default: 10]
-  --success-threshold=<success-threshold>
-    minimum consecutive successes for the probe to be considered successful after having failed [default: 1]
-  --failure-threshold=<failure-threshold>
-    minimum consecutive successes for the probe to be considered failed after having succeeded [default: 3]
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	path := safeGetString(args, "--path")
-	procType := safeGetString(args, "--type")
-	initialDelayTimeout := safeGetInt(args, "--initial-delay-timeout")
-	timeoutSeconds := safeGetInt(args, "--timeout-seconds")
-	periodSeconds := safeGetInt(args, "--period-seconds")
-	successThreshold := safeGetInt(args, "--success-threshold")
-	failureThreshold := safeGetInt(args, "--failure-threshold")
-	headers := []string{}
-	if args["--headers"] != nil {
-		headers = strings.Split(args["--headers"].(string), ",")
-	}
-	if procType == "" {
-		procType = defaultProcType
-	}
-
-	healthcheckType := args["<health-type>"].(string)
-	probeType := args["<probe-type>"].(string)
-	probeArgs := args["<args>"].([]string)
-
-	if err := checkProbeType(healthcheckType); err != nil {
-		return err
-	}
-
-	probe := &api.Healthcheck{
-		InitialDelaySeconds: initialDelayTimeout,
-		TimeoutSeconds:      timeoutSeconds,
-		PeriodSeconds:       periodSeconds,
-		SuccessThreshold:    successThreshold,
-		FailureThreshold:    failureThreshold,
-	}
-
-	switch probeType {
-	case "httpGet":
-		parsedHeaders, err := parseHeaders(headers)
-		if err != nil {
-			return fmt.Errorf("could not parse headers: %s", err)
-		}
-		port, err := strconv.Atoi(probeArgs[0])
-		if err != nil {
-			return fmt.Errorf("could not parse port: %s", err)
-		}
-		probe.HTTPGet = &api.HTTPGetProbe{
-			Path:        path,
-			Port:        port,
-			HTTPHeaders: parsedHeaders,
-		}
-	case "exec":
-		probe.Exec = &api.ExecProbe{
-			Command: probeArgs,
-		}
-	case "tcpSocket":
-		port, err := strconv.Atoi(probeArgs[0])
-		if err != nil {
-			return fmt.Errorf("could not parse port: %s", err)
-		}
-		probe.TCPSocket = &api.TCPSocketProbe{
-			Port: port,
-		}
-	default:
-		return fmt.Errorf("invalid probe type. Must be one of: \"httpGet\", \"exec\"")
-	}
-
-	return cmdr.HealthchecksSet(app, healthcheckType, procType, probe)
-}
-
-func healthchecksUnset(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Unsets healthchecks for an application.
-
-Usage: drycc healthchecks:unset [options] <health-type>...
-
-Arguments:
-  <health-type>
-    the healthcheck type, such as 'liveness' or 'readiness'.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-  --type=<type>
-    the procType for which the health check needs to be removed.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	healthchecks := args["<health-type>"].([]string)
-	procType := safeGetString(args, "--type")
-	if procType == "" {
-		procType = defaultProcType
-	}
-
-	for healthcheck := range healthchecks {
-		if err := checkProbeType(healthchecks[healthcheck]); err != nil {
-			return err
-		}
-	}
-
-	return cmdr.HealthchecksUnset(app, procType, healthchecks)
-}
-
-func parseHeaders(headers []string) ([]*api.KVPair, error) {
-	var parsedHeaders []*api.KVPair
-	for _, header := range headers {
-		parsedHeader, err := parseHeader(header)
-		if err != nil {
-			return nil, err
-		}
-		parsedHeaders = append(parsedHeaders, parsedHeader)
-	}
-	return parsedHeaders, nil
-}
-
-func parseHeader(header string) (*api.KVPair, error) {
-	headerParts := strings.SplitN(header, ":", 2)
-	if len(headerParts) != 2 {
-		return nil, fmt.Errorf("could not find separator in header (%s)", header)
-	}
-	return &api.KVPair{
-		Name:  strings.TrimSpace(headerParts[0]),
-		Value: strings.TrimSpace(headerParts[1]),
-	}, nil
-}
-
-func checkProbeType(probe string) error {
-	var found bool
-	probeTypes := []string{
-		"startupProbe",
-		"livenessProbe",
-		"readinessProbe",
-	}
-	for _, ptype := range probeTypes {
-		if probe == ptype {
-			found = true
-		}
-	}
-	if !found {
-		return fmt.Errorf("probe type %s is invalid. Must be one of %s", probe, probeTypes)
-	}
-	return nil
-}
diff --git a/parser/healthchecks_test.go b/parser/healthchecks_test.go
deleted file mode 100644
index 485e1f310..000000000
--- a/parser/healthchecks_test.go
+++ /dev/null
@@ -1,95 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/controller-sdk-go/api"
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) HealthchecksList(string, string) error {
-	return errors.New("healthchecks:list")
-}
-
-func (d FakeDryccCmd) HealthchecksSet(string, string, string, *api.Healthcheck) error {
-	return errors.New("healthchecks:set")
-}
-
-func (d FakeDryccCmd) HealthchecksUnset(string, string, []string) error {
-	return errors.New("healthchecks:unset")
-}
-
-func TestHealthchecks(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"healthchecks:list"},
-			expected: "",
-		},
-		{
-			args:     []string{"healthchecks:set", "liveness", "httpGet", "80"},
-			expected: "",
-		},
-		{
-			args:     []string{"healthchecks:set", "liveness", "httpGet", "80", "--headers=test-header:test-value"},
-			expected: "",
-		},
-		{
-			args:     []string{"healthchecks:set", "liveness", "exec", "ls"},
-			expected: "",
-		},
-		{
-			args:     []string{"healthchecks:set", "liveness", "tcpSocket", "80"},
-			expected: "",
-		},
-		{
-			args:     []string{"healthchecks:unset", "liveness"},
-			expected: "",
-		},
-		{
-			args:     []string{"healthchecks"},
-			expected: "healthchecks:list",
-		},
-		{
-			args:     []string{"healthchecks:set", "alien", "httpGet", "80"},
-			expected: "probe type alien is invalid. Must be one of [liveness readiness]",
-		},
-		{
-			args:     []string{"healthchecks:unset", "alien", "httpGet", "80"},
-			expected: "probe type alien is invalid. Must be one of [liveness readiness]",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Healthchecks(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/keys.go b/parser/keys.go
deleted file mode 100644
index c050d23f4..000000000
--- a/parser/keys.go
+++ /dev/null
@@ -1,108 +0,0 @@
-package parser
-
-import (
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Keys routes key commands to the specific function.
-func Keys(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for SSH keys:
-
-keys:list        list SSH keys for the logged in user
-keys:add         add an SSH key
-keys:remove      remove an SSH key
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "keys:list":
-		return keysList(argv, cmdr)
-	case "keys:add":
-		return keyAdd(argv, cmdr)
-	case "keys:remove":
-		return keyRemove(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "keys" {
-			argv[0] = "keys:list"
-			return keysList(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func keysList(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Lists SSH keys for the logged in user.
-
-Usage: drycc keys:list [options]
-
-Options:
-  -l --limit=<num>
-    the maximum number of results to display, defaults to config setting
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	results, err := responseLimit(safeGetString(args, "--limit"))
-
-	if err != nil {
-		return err
-	}
-
-	return cmdr.KeysList(results)
-}
-
-func keyAdd(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Adds SSH keys for the logged in user.
-
-Usage: drycc keys:add [<name>] [<key>]
-
-<name> and <key> can be used in either order and are both optional
-
-Arguments:
-  <name>
-    name of the SSH key
-  <key>
-    a local file path to an SSH public key used to push application code.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-	if err != nil {
-		return err
-	}
-
-	return cmdr.KeyAdd(safeGetString(args, "<name>"), safeGetString(args, "<key>"))
-}
-
-func keyRemove(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Removes an SSH key for the logged in user.
-
-Usage: drycc keys:remove <key>
-
-Arguments:
-  <key>
-    the SSH public key to revoke source code push access.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-	if err != nil {
-		return err
-	}
-
-	return cmdr.KeyRemove(safeGetString(args, "<key>"))
-}
diff --git a/parser/keys_test.go b/parser/keys_test.go
deleted file mode 100644
index 02f7679e8..000000000
--- a/parser/keys_test.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) KeysList(int) error {
-	return errors.New("keys:list")
-}
-
-func (d FakeDryccCmd) KeyRemove(string) error {
-	return errors.New("keys:remove")
-}
-
-func (d FakeDryccCmd) KeyAdd(string, string) error {
-	return errors.New("keys:add")
-}
-
-func TestKeys(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"keys:list"},
-			expected: "",
-		},
-		{
-			args:     []string{"keys:add", "name", "key"},
-			expected: "",
-		},
-		{
-			args:     []string{"keys:remove", "key"},
-			expected: "",
-		},
-		{
-			args:     []string{"keys"},
-			expected: "keys:list",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Keys(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/labels.go b/parser/labels.go
deleted file mode 100644
index d073ab67c..000000000
--- a/parser/labels.go
+++ /dev/null
@@ -1,114 +0,0 @@
-package parser
-
-import (
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Labels displays all relevant commands for `drycc label`.
-func Labels(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for labels:
-
-labels:list   list application's labels
-labels:set    add new application's label
-labels:unset  remove application's label
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "labels:list":
-		return labelsList(argv, cmdr)
-	case "labels:set":
-		return labelsSet(argv, cmdr)
-	case "labels:unset":
-		return labelsUnset(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "labels" {
-			argv[0] = "labels:list"
-			return labelsList(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func labelsList(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Prints a list of labels of the application.
-
-Usage: drycc labels:list [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	return cmdr.LabelsList(safeGetString(args, "--app"))
-}
-
-func labelsSet(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Sets labels for an application.
-
-A label is a key/value pair used to label an application. This label is a general information for drycc user.
-Mostly used for administration/maintenance information, note for application. This information isn't send to scheduler.
-
-Usage: drycc labels:set [options] <key>=<value>...
-
-Arguments:
-  <key> the label key, for example: "git_repo" or "team"
-  <value> the label value, for example: "https://github.com/drycc/workflow" or "frontend"
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	tags := args["<key>=<value>"].([]string)
-
-	return cmdr.LabelsSet(app, tags)
-}
-
-func labelsUnset(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Unsets labels for an application.
-
-Usage: drycc labels:unset [options] <key>...
-
-Arguments:
-  <key> the label key to unset, for example: "git_repo" or "team"
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	tags := args["<key>"].([]string)
-
-	return cmdr.LabelsUnset(app, tags)
-}
diff --git a/parser/labels_test.go b/parser/labels_test.go
deleted file mode 100644
index 0b75f93b2..000000000
--- a/parser/labels_test.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) LabelsList(string) error {
-	return errors.New("labels:list")
-}
-
-func (d FakeDryccCmd) LabelsSet(string, []string) error {
-	return errors.New("labels:set")
-}
-
-func (d FakeDryccCmd) LabelsUnset(string, []string) error {
-	return errors.New("labels:unset")
-}
-
-func TestLabels(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"labels:list"},
-			expected: "",
-		},
-		{
-			args:     []string{"labels:set", "git_repo=https://github.com/drycc/workflow", "team=drycc"},
-			expected: "",
-		},
-		{
-			args:     []string{"labels:unset", "git_repo", "team"},
-			expected: "",
-		},
-		{
-			args:     []string{"labels"},
-			expected: "labels:list",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Labels(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/limits.go b/parser/limits.go
deleted file mode 100644
index 19d88c810..000000000
--- a/parser/limits.go
+++ /dev/null
@@ -1,188 +0,0 @@
-package parser
-
-import (
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Limits routes limits commands to their specific function
-func Limits(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for limits:
-
-limits:list        list resource limits for an app
-limits:set         set resource limits for an app
-limits:unset       unset resource limits for an app
-limits:specs       unset resource limits for an app
-limits:plans       unset resource limits for an app
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "limits:list":
-		return limitsList(argv, cmdr)
-	case "limits:set":
-		return limitSet(argv, cmdr)
-	case "limits:unset":
-		return limitUnset(argv, cmdr)
-	case "limits:specs":
-		return limitSpecs(argv, cmdr)
-	case "limits:plans":
-		return limitPlans(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "limits" {
-			argv[0] = "limits:list"
-			return limitsList(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func limitsList(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Lists resource limits for an application.
-
-Usage: drycc limits:list [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name of the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	return cmdr.LimitsList(safeGetString(args, "--app"))
-}
-
-func limitSet(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Sets resource limits for an application.
-
-A resource limit is a finite resource within a pod which we can apply
-restrictions through Kubernetes.
-
-Usage: drycc limits:set [options] <type>=<value>...
-
-Arguments:
-  <type>
-    the process type as defined in your Procfile, such as 'web' or 'worker'.
-  <value>
-    The limit plan id to apply to the process type.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-
-Use 'drycc help [command]' to learn more.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	limits := args["<type>=<value>"].([]string)
-	return cmdr.LimitsSet(app, limits)
-}
-
-func limitUnset(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Unsets resource limits for an application.
-
-Usage: drycc limits:unset [options] <type>...
-
-Arguments:
-  <type>
-    the process type as defined in your Procfile, such as 'web' or 'worker'.
-    Note that Dockerfile apps have a default 'cmd' process type.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	limits := args["<type>"].([]string)
-
-	return cmdr.LimitsUnset(app, limits)
-}
-
-func limitSpecs(argv []string, cmdr cmd.Commander) error {
-	usage := `
-List all available limit specs.
-
-Usage: drycc limits:specs [options]
-
-Options:
-  -l --limit=<num>
-    the maximum number of results to display, defaults to config setting.
-  -k --keywords=<keywords>
-    search keywords separated by commas, matching must satisfy all of the specified.
-`
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-	results, err := responseLimit(safeGetString(args, "--limit"))
-
-	if err != nil {
-		return err
-	}
-	keywords := safeGetString(args, "--keywords")
-
-	return cmdr.LimitsSpecs(keywords, results)
-}
-
-func limitPlans(argv []string, cmdr cmd.Commander) error {
-	usage := `
-List all available limit plans.
-
-Usage: drycc limits:plans [options]
-
-Options:
-  --cpu=<cpu>
-    query plans that meet the specified number of cpu cores.
-  --memory=<memory>
-    query plans that meet the specified memory capacity, unit GiB.
-  --spec-id=<spec-id>
-    query plans that meet the specified spec id, see [specs] subcommand.
-  -l --limit=<num>
-    the maximum number of results to display, defaults to config setting.
-`
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-	results, err := responseLimit(safeGetString(args, "--limit"))
-
-	if err != nil {
-		return err
-	}
-
-	specID := safeGetString(args, "--spec-id")
-	cpu := safeGetInt(args, "--cpu")
-	memory := safeGetInt(args, "--memory")
-
-	return cmdr.LimitsPlans(specID, cpu, memory, results)
-}
diff --git a/parser/limits_test.go b/parser/limits_test.go
deleted file mode 100644
index 341298e58..000000000
--- a/parser/limits_test.go
+++ /dev/null
@@ -1,92 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) LimitsList(string) error {
-	return errors.New("limits:list")
-}
-
-func (d FakeDryccCmd) LimitsSet(string, []string) error {
-	return errors.New("limits:set")
-}
-
-func (d FakeDryccCmd) LimitsUnset(string, []string) error {
-	return errors.New("limits:unset")
-}
-func (d FakeDryccCmd) LimitsSpecs(string, int) error {
-	return errors.New("limits:specs")
-}
-func (d FakeDryccCmd) LimitsPlans(string, int, int, int) error {
-	return errors.New("limits:plans")
-}
-
-func TestLimits(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"limits:list"},
-			expected: "",
-		},
-		{
-			args:     []string{"limits:set", "web=std1.large.c1m1"},
-			expected: "",
-		},
-		{
-			args:     []string{"limits:set", "web=std1.large.c1m1 worker=std1.large.c1m1"},
-			expected: "",
-		},
-		{
-			args:     []string{"limits:unset", "web"},
-			expected: "",
-		},
-		{
-			args:     []string{"limits:specs"},
-			expected: "",
-		},
-		{
-			args:     []string{"limits:plans"},
-			expected: "",
-		},
-		{
-			args:     []string{"limits"},
-			expected: "limits:list",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Limits(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/parser_test.go b/parser/parser_test.go
deleted file mode 100644
index 7fa7cef1f..000000000
--- a/parser/parser_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package parser
-
-import "github.com/drycc/workflow-cli/cmd"
-
-// DryccCmd is an implementation of Commander.
-type FakeDryccCmd cmd.DryccCmd
-
-func (d FakeDryccCmd) Println(...interface{}) (int, error) {
-	return 1, nil
-}
-
-func (d FakeDryccCmd) Print(...interface{}) (int, error) {
-	return 1, nil
-}
-
-func (d FakeDryccCmd) Printf(string, ...interface{}) (int, error) {
-	return 1, nil
-}
-
-func (d FakeDryccCmd) PrintErrln(...interface{}) (int, error) {
-	return 1, nil
-}
-
-func (d FakeDryccCmd) PrintErr(...interface{}) (int, error) {
-	return 1, nil
-}
-
-func (d FakeDryccCmd) PrintErrf(string, ...interface{}) (int, error) {
-	return 1, nil
-}
-
-func (d FakeDryccCmd) ServicesAdd(string, string, string, string) error {
-	return nil
-}
-
-func (d FakeDryccCmd) ServicesList(string) error {
-	return nil
-}
-
-func (d FakeDryccCmd) ServicesRemove(string, string, string, int) error {
-	return nil
-}
diff --git a/parser/perms.go b/parser/perms.go
deleted file mode 100644
index 77c2dbad9..000000000
--- a/parser/perms.go
+++ /dev/null
@@ -1,137 +0,0 @@
-package parser
-
-import (
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Perms routes perms commands to their specific function.
-func Perms(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for perms:
-
-perms:list            list permissions granted on an app
-perms:create          create a new permission for a user
-perms:delete          delete a permission for a user
-
-Use 'drycc help perms:[command]' to learn more.
-`
-
-	switch argv[0] {
-	case "perms:list":
-		return permsList(argv, cmdr)
-	case "perms:create":
-		return permCreate(argv, cmdr)
-	case "perms:delete":
-		return permDelete(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "perms" {
-			argv[0] = "perms:list"
-			return permsList(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func permsList(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Lists all users with permission to use an app, or lists all users with system
-administrator privileges.
-
-Usage: drycc perms:list [-a --app=<app>|--admin|--admin --limit=<num>]
-
-Options:
-  -a --app=<app>
-    lists all users with permission to <app>. <app> is the uniquely identifiable name
-    for the application.
-  --admin
-    lists all users with system administrator privileges.
-  -l --limit=<num>
-    the maximum number of results to display, defaults to config setting`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	admin := args["--admin"].(bool)
-
-	results, err := responseLimit(safeGetString(args, "--limit"))
-
-	if err != nil {
-		return err
-	}
-
-	return cmdr.PermsList(app, admin, results)
-}
-
-func permCreate(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Gives another user permission to use an app, or gives another user
-system administrator privileges.
-
-Usage: drycc perms:create <username> [-a --app=<app>|--admin]
-
-Arguments:
-  <username>
-    the name of the new user.
-
-Options:
-  -a --app=<app>
-    grants <username> permission to use <app>. <app> is the uniquely identifiable name
-    for the application.
-  --admin
-    grants <username> system administrator privileges.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	username := args["<username>"].(string)
-	admin := args["--admin"].(bool)
-
-	return cmdr.PermCreate(app, username, admin)
-}
-
-func permDelete(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Revokes another user's permission to use an app, or revokes another user's system
-administrator privileges.
-
-Usage: drycc perms:delete <username> [-a --app=<app>|--admin]
-
-Arguments:
-  <username>
-    the name of the user.
-
-Options:
-  -a --app=<app>
-    revokes <username> permission to use <app>. <app> is the uniquely identifiable name
-    for the application.
-  --admin
-    revokes <username> system administrator privileges.`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	username := args["<username>"].(string)
-	admin := args["--admin"].(bool)
-
-	return cmdr.PermDelete(app, username, admin)
-}
diff --git a/parser/perms_test.go b/parser/perms_test.go
deleted file mode 100644
index 572533c75..000000000
--- a/parser/perms_test.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) PermsList(string, bool, int) error {
-	return errors.New("perms:list")
-}
-
-func (d FakeDryccCmd) PermCreate(string, string, bool) error {
-	return errors.New("perms:create")
-}
-
-func (d FakeDryccCmd) PermDelete(string, string, bool) error {
-	return errors.New("perms:delete")
-}
-
-func TestPerms(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"perms:list"},
-			expected: "",
-		},
-		{
-			args:     []string{"perms:create", "test-user"},
-			expected: "",
-		},
-		{
-			args:     []string{"perms:delete", "test-user"},
-			expected: "",
-		},
-		{
-			args:     []string{"perms"},
-			expected: "perms:list",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Perms(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/ps.go b/parser/ps.go
deleted file mode 100644
index 1556b193c..000000000
--- a/parser/ps.go
+++ /dev/null
@@ -1,186 +0,0 @@
-package parser
-
-import (
-	"os"
-
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-	"golang.org/x/exp/slices"
-)
-
-// Ps routes ps commands to their specific function.
-func Ps(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for processes:
-
-ps:list        list application processes
-ps:logs        print the logs for a container
-ps:exec        execute a command in a container
-ps:restart     restart an application or process type
-ps:scale       scale processes (e.g. web=4 worker=2)
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "ps:list":
-		return psList(argv, cmdr)
-	case "ps:logs":
-		return psLogs(argv, cmdr)
-	case "ps:exec":
-		return psExec(argv, cmdr)
-	case "ps:restart":
-		return psRestart(argv, cmdr)
-	case "ps:scale":
-		return psScale(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "ps" {
-			argv[0] = "ps:list"
-			return psList(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func psList(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Lists processes servicing an application.
-
-Usage: drycc ps:list [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-	if err != nil {
-		return err
-	}
-
-	// The 1000 is fake for now until API understands limits
-	return cmdr.PsList(safeGetString(args, "--app"), 1000)
-}
-
-func psLogs(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Print the logs for a container in a pod or specified resource.
-
-Usage: drycc ps:logs <pod> [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-  -n --lines=<lines>
-    the number of lines to display.
-  -f --follow
-    specify if the logs should be streamed.
-  -t --container=<container>
-    print the logs of this container.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	lines := safeGetInt(args, "--lines")
-	if lines <= 0 {
-		lines = 300
-	}
-	follow := safeGetBool(args, "--follow")
-	podID := safeGetString(args, "<pod>")
-	container := safeGetString(args, "--container")
-
-	return cmdr.PsLogs(app, podID, lines, follow, container)
-}
-
-func psExec(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Execute a command in a container.
-
-Usage: drycc ps:exec <pod> [options] -- <command>...
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-  -t --tty
-    stdin is a TTY.
-  -i --stdin
-    pass stdin to the container.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-	if err != nil {
-		return err
-	}
-	app := safeGetString(args, "--app")
-	pod := safeGetString(args, "<pod>")
-	tty := args["--tty"].(bool)
-	stdin := args["--stdin"].(bool)
-	index := slices.Index(os.Args, "--")
-	command := os.Args[index+1:]
-	return cmdr.PsExec(app, pod, tty, stdin, command)
-}
-
-func psRestart(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Restart an application or process type.
-
-Usage: drycc ps:restart [<type>] [options]
-
-Arguments:
-  <type>
-    the process name as defined in your Procfile, such as 'web' or 'worker'.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	apps := safeGetString(args, "--app")
-	tp := safeGetString(args, "<type>")
-	return cmdr.PsRestart(apps, tp)
-}
-
-func psScale(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Scales an application's processes by type.
-
-Usage: drycc ps:scale <type>=<num>... [options]
-
-Arguments:
-  <type>
-    the process name as defined in your Procfile, such as 'web' or 'worker'.
-    Note that Dockerfile apps have a default 'cmd' process type.
-  <num>
-    the number of processes.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	apps := safeGetString(args, "--app")
-	return cmdr.PsScale(apps, args["<type>=<num>"].([]string))
-}
diff --git a/parser/ps_test.go b/parser/ps_test.go
deleted file mode 100644
index 2cb1c99bf..000000000
--- a/parser/ps_test.go
+++ /dev/null
@@ -1,90 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) PsList(string, int) error {
-	return errors.New("ps:list")
-}
-
-func (d FakeDryccCmd) PsLogs(string, string, int, bool, string) error {
-	return errors.New("ps:logs")
-}
-
-func (d FakeDryccCmd) PsExec(string, string, bool, bool, []string) error {
-	return errors.New("ps:exec")
-}
-
-func (d FakeDryccCmd) PsScale(string, []string) error {
-	return errors.New("ps:scale")
-}
-
-func (d FakeDryccCmd) PsRestart(string, string) error {
-	return errors.New("ps:restart")
-}
-
-func TestPs(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"ps:list"},
-			expected: "",
-		},
-		{
-			args:     []string{"ps:logs", "myapp-web-111"},
-			expected: "",
-		},
-		{
-			args:     []string{"ps:restart", "web"},
-			expected: "",
-		},
-		{
-			args:     []string{"ps:scale", "web", "5"},
-			expected: "",
-		},
-		{
-			args:     []string{"ps:list"},
-			expected: "",
-		},
-		{
-			args:     []string{"ps"},
-			expected: "ps:list",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Ps(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/registry.go b/parser/registry.go
deleted file mode 100644
index fa137c9bf..000000000
--- a/parser/registry.go
+++ /dev/null
@@ -1,116 +0,0 @@
-package parser
-
-import (
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Registry routes registry commands to their specific function
-func Registry(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for registry:
-
-registry:list        list registry info for an app
-registry:set         set registry info for an app
-registry:unset       unset registry info for an app
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "registry:list":
-		return registryList(argv, cmdr)
-	case "registry:set":
-		return registrySet(argv, cmdr)
-	case "registry:unset":
-		return registryUnset(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "registry" {
-			argv[0] = "registry:list"
-			return registryList(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func registryList(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Lists registry information for an application.
-
-Usage: drycc registry:list [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name of the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-	if err != nil {
-		return err
-	}
-
-	return cmdr.RegistryList(safeGetString(args, "--app"))
-}
-
-func registrySet(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Sets registry information for an application. These credentials are the same as those used for
-'podmain login' to the private registry.
-
-Usage: drycc registry:set [options] <key>=<value>...
-
-Arguments:
-  <key>
-    the uniquely identifiable name for logging into the registry. Valid keys are "username" or
-    "password"
-  <value>
-    the value of said environment variable. For example, "bob" or "mysecretpassword"
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	info := args["<key>=<value>"].([]string)
-
-	return cmdr.RegistrySet(app, info)
-}
-
-func registryUnset(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Unsets registry information for an application.
-
-Usage: drycc registry:unset [options] <key>...
-
-Arguments:
-  <key> the registry key to unset, for example: "username" or "password"
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	key := args["<key>"].([]string)
-
-	return cmdr.RegistryUnset(app, key)
-}
diff --git a/parser/registry_test.go b/parser/registry_test.go
deleted file mode 100644
index 868e45557..000000000
--- a/parser/registry_test.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) RegistryList(string) error {
-	return errors.New("registry:list")
-}
-
-func (d FakeDryccCmd) RegistrySet(string, []string) error {
-	return errors.New("registry:set")
-}
-
-func (d FakeDryccCmd) RegistryUnset(string, []string) error {
-	return errors.New("registry:unset")
-}
-
-func TestRegistry(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"registry:list"},
-			expected: "",
-		},
-		{
-			args:     []string{"registry:set", "username", "value"},
-			expected: "",
-		},
-		{
-			args:     []string{"registry:unset", "username"},
-			expected: "",
-		},
-		{
-			args:     []string{"registry"},
-			expected: "registry:list",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Registry(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/releases.go b/parser/releases.go
deleted file mode 100644
index 2bd1ea0d6..000000000
--- a/parser/releases.go
+++ /dev/null
@@ -1,151 +0,0 @@
-package parser
-
-import (
-	"fmt"
-	"strconv"
-
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Releases routes releases commands to their specific function.
-func Releases(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for releases:
-
-releases:list        list an application's release history
-releases:info        print information about a specific release
-releases:rollback    return to a previous release
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "releases:list":
-		return releasesList(argv, cmdr)
-	case "releases:info":
-		return releasesInfo(argv, cmdr)
-	case "releases:rollback":
-		return releasesRollback(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "releases" {
-			argv[0] = "releases:list"
-			return releasesList(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func releasesList(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Lists release history for an application.
-
-Usage: drycc releases:list [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-  -l --limit=<num>
-    the maximum number of results to display, defaults to config setting
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-	if err != nil {
-		return err
-	}
-
-	results, err := responseLimit(safeGetString(args, "--limit"))
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-
-	return cmdr.ReleasesList(app, results)
-}
-
-func releasesInfo(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Prints info about a particular release.
-
-Usage: drycc releases:info <version> [options]
-
-Arguments:
-  <version>
-    the release of the application, such as 'v1'.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-	if err != nil {
-		return err
-	}
-
-	version, err := versionFromString(args["<version>"].(string))
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-
-	return cmdr.ReleasesInfo(app, version)
-}
-
-func releasesRollback(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Rolls back to a previous application release.
-
-Usage: drycc releases:rollback [<version>] [options]
-
-Arguments:
-  <version>
-    the release of the application, such as 'v1'.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name of the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-	if err != nil {
-		return err
-	}
-
-	var version int
-
-	if args["<version>"] == nil {
-		version = -1
-	} else {
-		version, err = versionFromString(args["<version>"].(string))
-
-		if err != nil {
-			return err
-		}
-	}
-
-	app := safeGetString(args, "--app")
-
-	return cmdr.ReleasesRollback(app, version)
-}
-
-func versionFromString(version string) (int, error) {
-	if version[:1] == "v" {
-		if len(version) < 2 {
-			return -1, fmt.Errorf("%s is not in the form 'v#'", version)
-		}
-
-		return strconv.Atoi(version[1:])
-	}
-
-	return strconv.Atoi(version)
-}
diff --git a/parser/releases_test.go b/parser/releases_test.go
deleted file mode 100644
index 28fd31c6d..000000000
--- a/parser/releases_test.go
+++ /dev/null
@@ -1,78 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) ReleasesList(string, int) error {
-	return errors.New("releases:list")
-}
-
-func (d FakeDryccCmd) ReleasesInfo(string, int) error {
-	return errors.New("releases:info")
-}
-
-func (d FakeDryccCmd) ReleasesRollback(string, int) error {
-	return errors.New("releases:rollback")
-}
-
-func TestReleases(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"releases:list"},
-			expected: "",
-		},
-		{
-			args:     []string{"releases:info", "v1"},
-			expected: "",
-		},
-		{
-			args:     []string{"releases:rollback"},
-			expected: "",
-		},
-		{
-			args:     []string{"releases:rollback", "v1"},
-			expected: "",
-		},
-		{
-			args:     []string{"releases"},
-			expected: "releases:list",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Releases(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/resources.go b/parser/resources.go
deleted file mode 100644
index 64e92c965..000000000
--- a/parser/resources.go
+++ /dev/null
@@ -1,334 +0,0 @@
-package parser
-
-import (
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Resources commands to their specific function.
-func Resources(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for resources:
-
-resources:services         list all available resource services
-resources:plans            list all available plans for an resource services
-resources:create           create a resource for the application
-resources:list             list resources in the application
-resources:describe         get a resource detail info in the application
-resources:update           update a resource from the application
-resources:destroy          delete a resource from the applicationa
-resources:bind             bind a resource to servicebroker
-resources:unbind           unbind a resource from servicebroker
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "resources:services":
-		return resourcesServices(argv, cmdr)
-	case "resources:plans":
-		return resourcesPlans(argv, cmdr)
-	case "resources:create":
-		return resourcesCreate(argv, cmdr)
-	case "resources:list":
-		return resourcesList(argv, cmdr)
-	case "resources:describe":
-		return resourceGet(argv, cmdr)
-	case "resources:update":
-		return resourcePut(argv, cmdr)
-	case "resources:destroy":
-		return resourceDelete(argv, cmdr)
-	case "resources:bind":
-		return resourceBind(argv, cmdr)
-	case "resources:unbind":
-		return resourceUnbind(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "resources" {
-			argv[0] = "resources:list"
-			return resourcesList(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func resourcesServices(argv []string, cmdr cmd.Commander) error {
-	usage := `
-List all available resource services.
-
-Usage: drycc resources:services [options]
-
-Options:
-  -l --limit=<num>
-    the maximum number of results to display, defaults to config setting
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	results, err := responseLimit(safeGetString(args, "--limit"))
-
-	if err != nil {
-		return err
-	}
-
-	return cmdr.ResourcesServices(results)
-}
-
-func resourcesPlans(argv []string, cmdr cmd.Commander) error {
-	usage := `
-List all available plans for an resource services.
-
-Usage: drycc resources:plans <service> [options]
-
-Arguments:
-  <service>
-    the service name for plans.
-
-Options:
-  -l --limit=<num>
-    the maximum number of results to display, defaults to config setting
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	service := safeGetString(args, "<service>")
-
-	results, err := responseLimit(safeGetString(args, "--limit"))
-
-	if err != nil {
-		return err
-	}
-
-	return cmdr.ResourcesPlans(service, results)
-}
-
-func resourcesCreate(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Create a resource for the application.
-
-Usage: drycc resources:create <plan> <name> [<param>=<value>...] [options]
-
-Arguments:
-  <plan>
-    the resource's plan, pattern: <service_name>:<plan_name>.
-  <name>
-    this resource instance alias.
-  <param>
-    the resource instance parameters key.
-  <value>
-    the resource instance parameters value.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-  -f --values=<values_file>
-    specify values in a YAML file. If set, params will be discard.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	values := safeGetString(args, "--values")
-	plan := safeGetString(args, "<plan>")
-	name := safeGetString(args, "<name>")
-
-	return cmdr.ResourcesCreate(app, plan, name, args["<param>=<value>"].([]string), values)
-}
-
-func resourcesList(argv []string, cmdr cmd.Commander) error {
-	usage := `
-List resources in the application.
-
-Usage: drycc resources:list [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-  -l --limit=<num>
-    the maximum number of results to display, defaults to config setting
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	results, err := responseLimit(safeGetString(args, "--limit"))
-
-	if err != nil {
-		return err
-	}
-	app := safeGetString(args, "--app")
-
-	return cmdr.ResourcesList(app, results)
-}
-
-func resourceGet(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Get a resource's detail in the application.
-
-Usage: drycc resources:describe <name> [options]
-
-Arguments:
-  <name>
-    this resource instance alias.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-  -l --limit=<num>
-    the maximum number of results to display, defaults to config setting
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	name := safeGetString(args, "<name>")
-
-	return cmdr.ResourceGet(app, name)
-}
-
-func resourcePut(argv []string, cmdr cmd.Commander) error {
-
-	usage := `
-update a resource from the application
-
-Usage: drycc resources:update <plan> <name> [<param>=<value>...] [options]
-
-Arguments:
-  <plan>
-    the resource's plan, pattern: <service_name>:<plan_name>.
-  <name>
-    this resource instance alias.
-  <param>
-    the resource instance parameters key.
-  <value>
-    the resource instance parameters value.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-  -f --values=<values_file>
-	specify values in a YAML file. If set, params will be discard.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	values := safeGetString(args, "--values")
-	plan := safeGetString(args, "<plan>")
-	name := safeGetString(args, "<name>")
-
-	return cmdr.ResourcePut(app, plan, name, args["<param>=<value>"].([]string), values)
-}
-
-func resourceDelete(argv []string, cmdr cmd.Commander) error {
-
-	usage := `
-Delete a resource from the application.
-
-Usage: drycc resources:destroy <name> [options]
-
-Arguments:
-  <name>
-    the resource instance alias name to be removed.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-  --confirm=<resource>
-    skips the prompt for the resource name. <resource> is the uniquely identifiable
-    name for the resource.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	name := safeGetString(args, "<name>")
-	confirm := safeGetString(args, "--confirm")
-
-	return cmdr.ResourceDelete(app, name, confirm)
-}
-
-func resourceBind(argv []string, cmdr cmd.Commander) error {
-	usage := `
-bind a resource for an application.
-
-Usage: drycc resources:bind <name> [options]
-
-Arguments:
-  <name>
-    the resource instance alias name.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	name := safeGetString(args, "<name>")
-
-	return cmdr.ResourceBind(app, name)
-}
-
-func resourceUnbind(argv []string, cmdr cmd.Commander) error {
-	usage := `
-unbind a resources for an application.
-
-Usage: drycc resources:unbind <name> [options]
-
-Arguments:
-  <name>
-    the resource instance alias name.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	name := safeGetString(args, "<name>")
-
-	return cmdr.ResourceUnbind(app, name)
-}
diff --git a/parser/resources_test.go b/parser/resources_test.go
deleted file mode 100644
index 204f8a2e7..000000000
--- a/parser/resources_test.go
+++ /dev/null
@@ -1,122 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) ResourcesServices(int) error {
-	return errors.New("resources:services")
-}
-
-func (d FakeDryccCmd) ResourcesPlans(string, int) error {
-	return errors.New("resources:plans")
-}
-
-func (d FakeDryccCmd) ResourcesCreate(string, string, string, []string, string) error {
-	return errors.New("resources:create")
-}
-
-func (d FakeDryccCmd) ResourcesList(string, int) error {
-	return errors.New("resources:list")
-}
-
-func (d FakeDryccCmd) ResourceGet(string, string) error {
-	return errors.New("resources:describe")
-}
-
-func (d FakeDryccCmd) ResourcePut(string, string, string, []string, string) error {
-	return errors.New("resources:update")
-}
-
-func (d FakeDryccCmd) ResourceDelete(string, string, string) error {
-	return errors.New("resources:destroy")
-}
-
-func (d FakeDryccCmd) ResourceBind(string, string) error {
-	return errors.New("resources:bind")
-}
-
-func (d FakeDryccCmd) ResourceUnbind(string, string) error {
-	return errors.New("resources:unbind")
-}
-
-func TestResources(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"resources:services"},
-			expected: "",
-		},
-		{
-			args:     []string{"resources:plans", "mysql"},
-			expected: "",
-		},
-		{
-			args:     []string{"resources:create", "mysql:5.6", "mysql", "key1=value1"},
-			expected: "",
-		},
-		{
-			args:     []string{"resources:list"},
-			expected: "",
-		},
-		{
-			args:     []string{"resources:describe", "mysql"},
-			expected: "",
-		},
-		{
-			args:     []string{"resources:update", "mysql:5.7", "mysql", "key1=value2"},
-			expected: "",
-		},
-		{
-			args:     []string{"resources:destroy", "mysql"},
-			expected: "",
-		},
-		{
-			args:     []string{"resources:bind", "mysql"},
-			expected: "",
-		},
-		{
-			args:     []string{"resources:unbind", "mysql"},
-			expected: "",
-		},
-		{
-			args:     []string{"resources"},
-			expected: "resources:list",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Resources(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/routes.go b/parser/routes.go
deleted file mode 100644
index c1bab6527..000000000
--- a/parser/routes.go
+++ /dev/null
@@ -1,265 +0,0 @@
-package parser
-
-import (
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Routes routes commands to their specific function.
-func Routes(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for routes:
-
-routes:create        create a route for an application
-routes:list          list application routes
-routes:get           get rule of route
-routes:set           set rule of route
-routes:attach        attach to gateway
-routes:detach        detach to gateway
-routes:remove        remove routes from an application
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "routes:create":
-		return routesCreate(argv, cmdr)
-	case "routes:list":
-		return routesList(argv, cmdr)
-	case "routes:get":
-		return routesGet(argv, cmdr)
-	case "routes:set":
-		return routesSet(argv, cmdr)
-	case "routes:attach":
-		return routesAttach(argv, cmdr)
-	case "routes:detach":
-		return routesDetach(argv, cmdr)
-	case "routes:remove":
-		return routesRemove(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "routes" {
-			argv[0] = "routes:list"
-			return routesList(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func routesCreate(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Creates routes for an application, provides a way to route requests
-
-Usage: drycc routes:create <name> --type=<type> --kind=<kind> --port=<port> [options]
-
-Arguments:
-  <name>
-    the route name.
-  <type>
-    the process type needs to create route.
-  <kind>
-    the route kind. Supports HTTPRoute, TCPRoute, UDPRoute, and TLSRoute.
-  <port>
-    the port that should be exposed by this service.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	name := safeGetString(args, "<name>")
-	procType := safeGetString(args, "--type")
-	kind := safeGetString(args, "--kind")
-	port := safeGetInt(args, "--port")
-
-	return cmdr.RoutesCreate(app, name, procType, kind, port)
-}
-
-func routesList(argv []string, cmdr cmd.Commander) error {
-	usage := `
-List routes for an application
-
-Usage: drycc routes:list [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-  -l --limit=<num>
-    the maximum number of results to display, defaults to config setting
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-	results, err := responseLimit(safeGetString(args, "--limit"))
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-
-	return cmdr.RoutesList(app, results)
-}
-
-func routesGet(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Get a route for an application
-
-Usage: drycc routes:get <name> [options]
-
-Arguments:
-  <name>
-    the route name.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	name := safeGetString(args, "<name>")
-
-	return cmdr.RoutesGet(app, name)
-}
-
-func routesSet(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Set route for an application
-
-Usage: drycc routes:set <name> --rules-file=<rules-file> [options]
-
-Arguments:
-  <name>
-    the route name.
-  <rules-file>
-    rules-file is the file name of rule to apply.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	name := safeGetString(args, "<name>")
-	rulesFile := safeGetString(args, "--rules-file")
-
-	return cmdr.RoutesSet(app, name, rulesFile)
-}
-
-func routesAttach(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Attach a route for an application
-
-Usage: drycc routes:attach <name> --gateway=<gateway> --port=<port> [options]
-
-Arguments:
-  <name>
-    the route name.
-  <gateway>
-    the gateway name.
-  <port>
-    port is the network port, the gateway listener expects to receive.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	name := safeGetString(args, "<name>")
-	port := safeGetInt(args, "--port")
-	gateway := safeGetString(args, "--gateway")
-
-	return cmdr.RoutesAttach(app, name, port, gateway)
-}
-
-func routesDetach(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Detach a route for an application
-
-Usage: drycc routes:detach <name> --gateway=<gateway> --port=<port> [options]
-
-Arguments:
-  <name>
-    the route name.
-  <gateway>
-    the gateway name.
-  <port>
-    port is the network port, the gateway listener expects to receive.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	name := safeGetString(args, "<name>")
-	port := safeGetInt(args, "--port")
-	gateway := safeGetString(args, "--gateway")
-
-	return cmdr.RoutesDetach(app, name, port, gateway)
-}
-
-func routesRemove(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Delete a route from the application.
-
-Usage: drycc routes:remove <name> [options]
-
-Arguments:
-  <name>
-    the route name.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-	app := safeGetString(args, "--app")
-	name := safeGetString(args, "<name>")
-
-	return cmdr.RoutesRemove(app, name)
-}
diff --git a/parser/routes_test.go b/parser/routes_test.go
deleted file mode 100644
index 3a9855a2a..000000000
--- a/parser/routes_test.go
+++ /dev/null
@@ -1,104 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-func (d FakeDryccCmd) RoutesCreate(string, string, string, string, int) error {
-	return errors.New("routes:create")
-}
-
-func (d FakeDryccCmd) RoutesList(string, int) error {
-	return errors.New("routes:list")
-}
-
-func (d FakeDryccCmd) RoutesGet(string, string) error {
-	return errors.New("routes:get")
-}
-
-func (d FakeDryccCmd) RoutesSet(string, string, string) error {
-	return errors.New("routes:Set")
-}
-
-func (d FakeDryccCmd) RoutesAttach(string, string, int, string) error {
-	return errors.New("routes:attach")
-}
-
-func (d FakeDryccCmd) RoutesDetach(string, string, int, string) error {
-	return errors.New("routes:detach")
-}
-
-func (d FakeDryccCmd) RoutesRemove(string, string) error {
-	return errors.New("routes:remove")
-}
-
-func TestRoutes(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"routes:create", "example", "--type=web", "--kind=http", "--port=80"},
-			expected: "",
-		},
-		{
-			args:     []string{"routes:list"},
-			expected: "",
-		},
-		{
-			args:     []string{"routes:get", "example"},
-			expected: "",
-		}, {
-			args:     []string{"routes:set", "example", "--rules-file=rules,json"},
-			expected: "",
-		},
-		{
-			args:     []string{"routes:attach", "example", "--port=80", "--gateway=example"},
-			expected: "",
-		},
-		{
-			args:     []string{"routes:detach", "example", "--port=80", "--gateway=example"},
-			expected: "",
-		},
-		{
-			args:     []string{"routes:remove", "example"},
-			expected: "",
-		},
-		{
-			args:     []string{"routes"},
-			expected: "routes:list",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Routes(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/routing.go b/parser/routing.go
deleted file mode 100644
index a0bba436d..000000000
--- a/parser/routing.go
+++ /dev/null
@@ -1,98 +0,0 @@
-package parser
-
-import (
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Routing displays all relevant commands for `drycc routing`.
-func Routing(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for routing:
-
-routing:info       view routability of an application
-routing:enable     enable routing for an app
-routing:disable    disable routing for an app
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "routing:info":
-		return routingInfo(argv, cmdr)
-	case "routing:enable":
-		return routingEnable(argv, cmdr)
-	case "routing:disable":
-		return routingDisable(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "routing" {
-			argv[0] = "routing:info"
-			return routingInfo(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func routingInfo(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Prints info about the current application's routability.
-
-Usage: drycc routing:info [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-	if err != nil {
-		return err
-	}
-
-	return cmdr.RoutingInfo(safeGetString(args, "--app"))
-}
-
-func routingEnable(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Enables routability for an app.
-
-Usage: drycc routing:enable [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name of the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-	if err != nil {
-		return err
-	}
-
-	return cmdr.RoutingEnable(safeGetString(args, "--app"))
-}
-
-func routingDisable(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Disables routability for an app.
-
-Usage: drycc routing:disable [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name of the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	return cmdr.RoutingDisable(safeGetString(args, "--app"))
-}
diff --git a/parser/routing_test.go b/parser/routing_test.go
deleted file mode 100644
index 2ee3c9914..000000000
--- a/parser/routing_test.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) RoutingInfo(string) error {
-	return errors.New("routing:info")
-}
-
-func (d FakeDryccCmd) RoutingEnable(string) error {
-	return errors.New("routing:enable")
-}
-
-func (d FakeDryccCmd) RoutingDisable(string) error {
-	return errors.New("routing:disable")
-}
-
-func TestRouting(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"routing:info"},
-			expected: "",
-		},
-		{
-			args:     []string{"routing:enable"},
-			expected: "",
-		},
-		{
-			args:     []string{"routing:disable"},
-			expected: "",
-		},
-		{
-			args:     []string{"routing"},
-			expected: "routing:info",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Routing(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/services.go b/parser/services.go
deleted file mode 100644
index 764f72fac..000000000
--- a/parser/services.go
+++ /dev/null
@@ -1,132 +0,0 @@
-package parser
-
-import (
-	docopt "github.com/docopt/docopt-go"
-
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Services routes service commands to their specific function.
-func Services(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for services:
-
-services:add           create service for an application
-services:list          list application services
-services:remove        remove service from an application
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "services:add":
-		return servicesAdd(argv, cmdr)
-	case "services:list":
-		return servicesList(argv, cmdr)
-	case "services:remove":
-		return servicesRemove(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "services" {
-			argv[0] = "services:list"
-			return servicesList(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func servicesAdd(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Creates extra service for an application and binds it to specific route of the main app domain
-
-Usage: drycc services:add --type=<type> --protocol=<protocol> --port=<port>:<target_port> [options]
-
-Arguments:
-  <type>
-    procfile type which should handle the request, e.g. webhooks (should be bind to the port PORT).
-    only single extra service per Porcfile type could be created
-  <protocol>
-    the IP protocol for this port. Supports TCP, UDP, and SCTP. Default is TCP.
-  <port>
-    the port that will be exposed by this service.
-  <targetPort>
-    number or name of the port to access on the pods targeted by the service.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	procfileType := safeGetString(args, "--type")
-	protocol := safeGetString(args, "--protocol")
-	ports := safeGetString(args, "--port")
-	return cmdr.ServicesAdd(app, procfileType, ports, protocol)
-}
-
-func servicesList(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Lists extra services for an application
-
-Usage: drycc services:list [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-
-	return cmdr.ServicesList(app)
-}
-
-func servicesRemove(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Deletes specific extra service for application
-
-Usage: drycc services:remove --type=<type> --protocol=<protocol> --port=<port> [options]
-
-Arguments:
-  <type>
-    procfile type which should handle the request, e.g. webhooks (should be bind to the port PORT).
-    Only single extra service per Porcfile type could be created
-  <protocol>
-    the IP protocol for this port. Supports TCP, UDP, and SCTP. Default is TCP.
-  <port>
-    the port exposed by this service.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	procfileType := safeGetString(args, "--type")
-	protocol := safeGetString(args, "--protocol")
-	port := safeGetInt(args, "--port")
-
-	return cmdr.ServicesRemove(app, procfileType, protocol, port)
-}
diff --git a/parser/shortcuts.go b/parser/shortcuts.go
deleted file mode 100644
index b9d2e3f21..000000000
--- a/parser/shortcuts.go
+++ /dev/null
@@ -1,50 +0,0 @@
-package parser
-
-import (
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Shortcuts displays all relevant shortcuts for the CLI.
-func Shortcuts(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for shortcuts:
-
-shortcuts:list       list all relevant shortcuts for the CLI
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "shortcuts:list":
-		return shortcutsList(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "shortcuts" {
-			argv[0] = "shortcuts:list"
-			return shortcutsList(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func shortcutsList(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Lists all relevant shortcuts for the CLI
-
-Usage: drycc shortcuts:list
-`
-
-	_, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	return cmdr.ShortcutsList()
-}
diff --git a/parser/shortcuts_test.go b/parser/shortcuts_test.go
deleted file mode 100644
index 303d5feba..000000000
--- a/parser/shortcuts_test.go
+++ /dev/null
@@ -1,58 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) ShortcutsList() error {
-	return errors.New("shortcuts:list")
-}
-
-func TestShortcuts(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"shortcuts:list"},
-			expected: "",
-		},
-		{
-			args:     []string{"shortcuts"},
-			expected: "shortcuts:list",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Shortcuts(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/tags.go b/parser/tags.go
deleted file mode 100644
index 3ca0bff71..000000000
--- a/parser/tags.go
+++ /dev/null
@@ -1,115 +0,0 @@
-package parser
-
-import (
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Tags routes tags commands to their specific function
-func Tags(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for tags:
-
-tags:list        list tags for an app
-tags:set         set tags for an app
-tags:unset       unset tags for an app
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "tags:list":
-		return tagsList(argv, cmdr)
-	case "tags:set":
-		return tagsSet(argv, cmdr)
-	case "tags:unset":
-		return tagsUnset(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "tags" {
-			argv[0] = "tags:list"
-			return tagsList(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func tagsList(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Lists tags for an application.
-
-Usage: drycc tags:list [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name of the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	return cmdr.TagsList(safeGetString(args, "--app"))
-}
-
-func tagsSet(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Sets tags for an application.
-
-A tag is a key/value pair used to tag an application's containers and is passed to the
-scheduler. This is often used to restrict workloads to specific hosts matching the
-scheduler-configured metadata.
-
-Usage: drycc tags:set [options] <key>=<value>...
-
-Arguments:
-  <key> the tag key, for example: "environ" or "rack"
-  <value> the tag value, for example: "prod" or "1"
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	tags := args["<key>=<value>"].([]string)
-
-	return cmdr.TagsSet(app, tags)
-}
-
-func tagsUnset(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Unsets tags for an application.
-
-Usage: drycc tags:unset [options] <key>...
-
-Arguments:
-  <key> the tag key to unset, for example: "environ" or "rack"
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	tags := args["<key>"].([]string)
-
-	return cmdr.TagsUnset(app, tags)
-}
diff --git a/parser/tags_test.go b/parser/tags_test.go
deleted file mode 100644
index c254ebe3d..000000000
--- a/parser/tags_test.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) TagsList(string) error {
-	return errors.New("tags:list")
-}
-
-func (d FakeDryccCmd) TagsSet(string, []string) error {
-	return errors.New("tags:set")
-}
-
-func (d FakeDryccCmd) TagsUnset(string, []string) error {
-	return errors.New("tags:unset")
-}
-
-func TestTags(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"tags:list"},
-			expected: "",
-		},
-		{
-			args:     []string{"tags:set", "environ", "prod"},
-			expected: "",
-		},
-		{
-			args:     []string{"tags:unset", "environ"},
-			expected: "",
-		},
-		{
-			args:     []string{"tags"},
-			expected: "tags:list",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Tags(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/timeouts.go b/parser/timeouts.go
deleted file mode 100644
index 5bfde0a40..000000000
--- a/parser/timeouts.go
+++ /dev/null
@@ -1,118 +0,0 @@
-package parser
-
-import (
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Timeouts routes timeouts commands to their specific function
-func Timeouts(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for timeouts:
-
-timeouts:list        list resource timeouts for an app
-timeouts:set         set resource timeouts for an app
-timeouts:unset       unset resource timeouts for an app
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "timeouts:list":
-		return timeoutList(argv, cmdr)
-	case "timeouts:set":
-		return timeoutSet(argv, cmdr)
-	case "timeouts:unset":
-		return timeoutUnset(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "timeouts" {
-			argv[0] = "timeouts:list"
-			return timeoutList(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func timeoutList(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Lists resource timeouts for an application.
-
-Usage: drycc timeouts:list [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name of the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	return cmdr.TimeoutsList(safeGetString(args, "--app"))
-}
-
-func timeoutSet(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Sets termination grace period for an application.
-
-Usage: drycc timeouts:set [options] <type>=<value>...
-
-Arguments:
-  <type>
-    the process type as defined in your Procfile, such as 'web' or 'worker'.
-    Note that Dockerfile apps have a default 'cmd' process type.
-  <value>
-    The value to apply to the process type in seconds.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	timeouts := args["<type>=<value>"].([]string)
-
-	return cmdr.TimeoutsSet(app, timeouts)
-}
-
-func timeoutUnset(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Unsets timeouts for an application. Default value (30s) or set by drycc controller
-
-Usage: drycc timeouts:unset [options] <type>...
-
-Arguments:
-  <type>
-    the process type as defined in your Procfile, such as 'web' or 'worker'.
-    Note that Dockerfile apps have a default 'cmd' process type.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	timeouts := args["<type>"].([]string)
-
-	return cmdr.TimeoutsUnset(app, timeouts)
-}
diff --git a/parser/timeouts_test.go b/parser/timeouts_test.go
deleted file mode 100644
index 51052b882..000000000
--- a/parser/timeouts_test.go
+++ /dev/null
@@ -1,78 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) TimeoutsList(string) error {
-	return errors.New("timeouts:list")
-}
-
-func (d FakeDryccCmd) TimeoutsSet(string, []string) error {
-	return errors.New("timeouts:set")
-}
-
-func (d FakeDryccCmd) TimeoutsUnset(string, []string) error {
-	return errors.New("timeouts:unset")
-}
-
-func TestTimeouts(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"timeouts:list"},
-			expected: "",
-		},
-		{
-			args:     []string{"timeouts:set", "web=100"},
-			expected: "",
-		},
-		{
-			args:     []string{"timeouts:set", "web=100 worker=200"},
-			expected: "",
-		},
-		{
-			args:     []string{"timeouts:unset", "web"},
-			expected: "",
-		},
-		{
-			args:     []string{"timeouts"},
-			expected: "timeouts:list",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Timeouts(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/tls.go b/parser/tls.go
deleted file mode 100644
index 747ea65f2..000000000
--- a/parser/tls.go
+++ /dev/null
@@ -1,183 +0,0 @@
-package parser
-
-import (
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// TLS routes tls commands to their specific function.
-func TLS(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for tls:
-
-tls:info              view info about an application's TLS settings
-tls:force:enable      enables the router to enforce https-only requests to an application
-tls:force:disable     disables the router to enforce https-only requests to an application
-tls:auto:enable       enables the router to automatic generation of certificates to an application
-tls:auto:disable      disables the router to automatic generation of certificates to an application
-tls:auto:issuer       add a issuer to an application
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "tls:info":
-		return tlsInfo(argv, cmdr)
-	case "tls:force:enable":
-		return tlsForceEnable(argv, cmdr)
-	case "tls:force:disable":
-		return tlsForceDisable(argv, cmdr)
-	case "tls:auto:enable":
-		return tlsAutoEnable(argv, cmdr)
-	case "tls:auto:disable":
-		return tlsAutoDisable(argv, cmdr)
-	case "tls:auto:issuer":
-		return tlsAutoIssuer(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "tls" {
-			argv[0] = "tls:info"
-			return tlsInfo(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func tlsInfo(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Prints info about the current application's TLS settings.
-
-Usage: drycc tls:info [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	return cmdr.TLSInfo(safeGetString(args, "--app"))
-}
-
-func tlsForceEnable(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Enable the router to enforce https-only requests to the current application.
-
-Usage: drycc tls:force:enable [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	return cmdr.TLSForceEnable(safeGetString(args, "--app"))
-}
-
-func tlsForceDisable(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Disable the router from enforcing https-only requests to the current application.
-
-Usage: drycc tls:force:disable [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	return cmdr.TLSForceDisable(safeGetString(args, "--app"))
-}
-
-func tlsAutoEnable(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Enable certs-auto requests to current application.
-
-Usage: drycc tls:auto:enable [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	return cmdr.TLSAutoEnable(safeGetString(args, "--app"))
-}
-
-func tlsAutoDisable(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Disable certs-auto requests to current application.
-
-Usage: drycc tls:auto:disable [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	return cmdr.TLSAutoDisable(safeGetString(args, "--app"))
-}
-
-func tlsAutoIssuer(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Disable certs-auto requests to current application.
-
-Usage: drycc tls:auto:issuer --email=<email> --server=<server> --key-id=<key-id> --key-secret=<key-secret> [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-  --email=<email>
-    the email address to be associated with the ACME account.
-  --server=<server>
-    Server is the URL used to access the ACME server's 'directory' endpoint.
-  --key-id=<key-id>
-    keyID is the ID of the CA key that the External Account is bound to.
-  --key-secret=<key-secret>
-    keySecret holds the symmetric MAC key of the External Account Binding.
-
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-	app := safeGetString(args, "--app")
-	email := safeGetString(args, "--email")
-	server := safeGetString(args, "--server")
-	keyID := safeGetString(args, "--key-id")
-	keySecret := safeGetString(args, "--key-secret")
-
-	return cmdr.TLSAutoIssuer(app, email, server, keyID, keySecret)
-}
diff --git a/parser/tls_test.go b/parser/tls_test.go
deleted file mode 100644
index d671a2a38..000000000
--- a/parser/tls_test.go
+++ /dev/null
@@ -1,90 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) TLSInfo(string) error {
-	return errors.New("tls:info")
-}
-
-func (d FakeDryccCmd) TLSForceEnable(string) error {
-	return errors.New("tls:force:enable")
-}
-
-func (d FakeDryccCmd) TLSForceDisable(string) error {
-	return errors.New("tls:force:disable")
-}
-
-func (d FakeDryccCmd) TLSAutoEnable(string) error {
-	return errors.New("tls:auto:disable")
-}
-
-func (d FakeDryccCmd) TLSAutoDisable(string) error {
-	return errors.New("tls:auto:disable")
-}
-
-func (d FakeDryccCmd) TLSAutoIssuer(string, string, string, string, string) error {
-	return errors.New("tls:auto:issuer")
-}
-
-func TestTLS(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"tls:info"},
-			expected: "",
-		},
-		{
-			args:     []string{"tls:force:enable"},
-			expected: "",
-		},
-		{
-			args:     []string{"tls:force:disable"},
-			expected: "",
-		},
-		{
-			args:     []string{"tls:auto:issuer", "--email=drycc@drycc.cc", "--server=https://acme.zerossl.com/v2/DV90", "--key-id=keyID", "--key-secret=keySecret"},
-			expected: "",
-		},
-		{
-			args:     []string{"tls"},
-			expected: "tls:info",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = TLS(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/tokens.go b/parser/tokens.go
deleted file mode 100644
index 81c1bee0f..000000000
--- a/parser/tokens.go
+++ /dev/null
@@ -1,113 +0,0 @@
-package parser
-
-import (
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Tokens commands to the specific function.
-func Tokens(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for tokens:
-
-tokens:list          lists tokens visible to the current controller.
-tokens:add           add a token for controller authentication.
-tokens:remove        remove a token for controller authentication.
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "tokens:list":
-		return tokensList(argv, cmdr)
-	case "tokens:add":
-		return tokensAdd(argv, cmdr)
-	case "tokens:remove":
-		return tokensRemove(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-		if argv[0] == "tokens" {
-			argv[0] = "tokens:list"
-			return tokensList(argv, cmdr)
-		}
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func tokensList(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Lists tokens visible to the current controller.
-
-Usage: drycc tokens:list [options]
-
-Options:
-  -l --limit=<num>
-    the maximum number of results to display, defaults to config setting
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-	results, err := responseLimit(safeGetString(args, "--limit"))
-	if err != nil {
-		return err
-	}
-	return cmdr.TokensList(results)
-}
-
-func tokensAdd(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Add a token for controller authentication.
-
-Usage: drycc tokens:add <alias> [options]
-
-Arguments:
-  <alias>
-  provide a alias for controller authentication token.
-
-Options:
-  --username=<username>
-    provide a username for the account.
-  --password=<password>
-    provide a password for the account.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-	alias := safeGetString(args, "<alias>")
-	username := safeGetString(args, "--username")
-	password := safeGetString(args, "--password")
-	_, err = cmdr.TokensAdd(nil, username, password, alias, "", true)
-	return err
-}
-
-func tokensRemove(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Remove a token for controller authentication.
-
-Usage: drycc tokens:remove <id>
-
-Arguments:
-  <id>
-  the id of the token for controller authentication.
-
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-	if err != nil {
-		return err
-	}
-	return cmdr.TokensRemove(safeGetString(args, "<id>"), "")
-}
diff --git a/parser/tokens_test.go b/parser/tokens_test.go
deleted file mode 100644
index ba6335a38..000000000
--- a/parser/tokens_test.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	drycc "github.com/drycc/controller-sdk-go"
-	"github.com/drycc/controller-sdk-go/api"
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) TokensList(int) error {
-	return errors.New("tokens:list")
-}
-
-func (d FakeDryccCmd) TokensAdd(*drycc.Client, string, string, string, string, bool) (*api.AuthTokenResponse, error) {
-	return nil, errors.New("tokens:add")
-}
-
-func (d FakeDryccCmd) TokensRemove(string, string) error {
-	return errors.New("tokens:remove")
-}
-
-func TestTokens(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"tokens:list"},
-			expected: "",
-		},
-		{
-			args:     []string{"gateways:add", "alias"},
-			expected: "",
-		},
-		{
-			args:     []string{"gateways:remove", "87587952-0b78-4ecb-ac74-1d9c46197efe"},
-			expected: "",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Tokens(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/users.go b/parser/users.go
deleted file mode 100644
index 6aecd8386..000000000
--- a/parser/users.go
+++ /dev/null
@@ -1,108 +0,0 @@
-package parser
-
-import (
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Users routes user commands to the specific function.
-func Users(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for users:
-
-users:list        list all registered users
-users:enable      enable a user
-users:disable     disable a user
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "users:list":
-		return usersList(argv, cmdr)
-	case "users:enable":
-		return usersEnable(argv, cmdr)
-	case "users:disable":
-		return usersDisable(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "users" {
-			argv[0] = "users:list"
-			return usersList(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func usersList(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Lists all registered users. Workflow administrators will be marked with a *.
-Requires admin privileges.
-
-Usage: drycc users:list [options]
-
-Options:
-  -l --limit=<num>
-    the maximum number of results to display, defaults to config setting
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-	if err != nil {
-		return err
-	}
-
-	results, err := responseLimit(safeGetString(args, "--limit"))
-
-	if err != nil {
-		return err
-	}
-
-	return cmdr.UsersList(results)
-}
-
-func usersEnable(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Enable a user when his status is disabled.
-Requires admin privileges.
-
-Usage: drycc users:enable <username>
-
-Arguments:
-  <username>
-  the username you want to enable.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-	if err != nil {
-		return err
-	}
-	username := safeGetString(args, "<username>")
-
-	return cmdr.UsersEnable(username)
-}
-
-func usersDisable(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Disable a user when his status is enabled.
-Requires admin privileges.
-
-Usage: drycc users:disable <username>
-
-Arguments:
-  <username>
-  the username you want to disable.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-	if err != nil {
-		return err
-	}
-	username := safeGetString(args, "<username>")
-
-	return cmdr.UsersDisable(username)
-}
diff --git a/parser/users_test.go b/parser/users_test.go
deleted file mode 100644
index b3a7aa622..000000000
--- a/parser/users_test.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) UsersList(int) error {
-	return errors.New("users:list")
-}
-
-func (d FakeDryccCmd) UsersEnable(string) error {
-	return errors.New("users:enable")
-}
-
-func (d FakeDryccCmd) UsersDisable(string) error {
-	return errors.New("users:disable")
-}
-
-func TestUsers(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"users:list"},
-			expected: "",
-		},
-		{
-			args:     []string{"users"},
-			expected: "users:list",
-		},
-		{
-			args:     []string{"users:enable", "test"},
-			expected: "",
-		},
-		{
-			args:     []string{"users:disable", "test"},
-			expected: "",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Users(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/utils.go b/parser/utils.go
deleted file mode 100644
index f1389f1d7..000000000
--- a/parser/utils.go
+++ /dev/null
@@ -1,60 +0,0 @@
-package parser
-
-import (
-	"fmt"
-	"log"
-	"strconv"
-
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-func safeGetString(args map[string]interface{}, key string) string {
-	return safeGetValue(args, key, "")
-}
-
-func safeGetBool(args map[string]interface{}, key string) bool {
-	return safeGetValue(args, key, false)
-}
-
-func safeGetInt(args map[string]interface{}, key string) int {
-	if args[key] == nil {
-		return 0
-	}
-	retVal, err := strconv.Atoi(args[key].(string))
-	if err != nil {
-		log.Fatalf("could not convert %s to int: %v", args[key], err)
-	}
-	return retVal
-}
-
-func safeGetValue[T any](args map[string]interface{}, key string, defaultValue T) T {
-	if args[key] == nil {
-		return defaultValue
-	}
-	return args[key].(T)
-}
-
-func responseLimit(limit string) (int, error) {
-	if limit == "" {
-		return -1, nil
-	}
-
-	return strconv.Atoi(limit)
-}
-
-// PrintUsage runs if no matching command is found.
-func PrintUsage(cmdr cmd.Commander) {
-	cmdr.PrintErrln("Found no matching command, try 'drycc help'")
-	cmdr.PrintErrln("Usage: drycc <command> [<args>...]")
-}
-
-func printHelp(argv []string, usage string) bool {
-	if len(argv) > 1 {
-		if argv[1] == "--help" || argv[1] == "-h" {
-			fmt.Print(usage)
-			return true
-		}
-	}
-
-	return false
-}
diff --git a/parser/utils_test.go b/parser/utils_test.go
deleted file mode 100644
index dcdf29dd3..000000000
--- a/parser/utils_test.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package parser
-
-import "testing"
-
-func TestSafeGet(t *testing.T) {
-	t.Parallel()
-
-	expected := "foo"
-
-	test := make(map[string]interface{}, 1)
-	test["test"] = "foo"
-
-	actual := safeGetString(test, "test")
-
-	if expected != actual {
-		t.Errorf("Expected %s, Got %s", expected, actual)
-	}
-}
-
-func TestSafeGetNil(t *testing.T) {
-	t.Parallel()
-
-	expected := ""
-
-	test := make(map[string]interface{}, 1)
-	test["test"] = nil
-
-	actual := safeGetString(test, "test")
-
-	if expected != actual {
-		t.Errorf("Expected %s, Got %s", expected, actual)
-	}
-}
-
-func TestSafeGetInt(t *testing.T) {
-	t.Parallel()
-
-	expected := 1
-
-	test := make(map[string]interface{}, 1)
-	test["test"] = "1"
-
-	actual := safeGetInt(test, "test")
-
-	if expected != actual {
-		t.Errorf("Expected %d, Got %d", expected, actual)
-	}
-
-	if actual = safeGetInt(test, "foo"); actual != 0 {
-		t.Errorf("Expected 0, Got %d", actual)
-	}
-}
-
-func TestPrintHelp(t *testing.T) {
-	t.Parallel()
-
-	usage := ""
-
-	if !printHelp([]string{"ps", "--help"}, usage) {
-		t.Error("Expected true")
-	}
-
-	if !printHelp([]string{"ps", "-h"}, usage) {
-		t.Error("Expected true")
-	}
-
-	if printHelp([]string{"ps"}, usage) {
-		t.Error("Expected false")
-	}
-
-	if printHelp([]string{"ps", "--foo"}, usage) {
-		t.Error("Expected false")
-	}
-}
diff --git a/parser/version.go b/parser/version.go
deleted file mode 100644
index f4cc7514a..000000000
--- a/parser/version.go
+++ /dev/null
@@ -1,25 +0,0 @@
-package parser
-
-import (
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Version displays the client version
-func Version(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Displays the client version.
-
-Usage: drycc version [options]
-
-Options:
-  -a --all
-    list api and controller versions
-`
-	args, err := docopt.ParseArgs(usage, argv, "")
-	if err != nil {
-		return err
-	}
-
-	return cmdr.Version(args["--all"].(bool))
-}
diff --git a/parser/version_test.go b/parser/version_test.go
deleted file mode 100644
index 92b43e458..000000000
--- a/parser/version_test.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) Version(bool) error {
-	return errors.New("version")
-}
-
-func TestVersion(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"version"},
-			expected: "",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Version(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/parser/volumes.go b/parser/volumes.go
deleted file mode 100644
index 10671ea5a..000000000
--- a/parser/volumes.go
+++ /dev/null
@@ -1,277 +0,0 @@
-package parser
-
-import (
-	docopt "github.com/docopt/docopt-go"
-	"github.com/drycc/workflow-cli/cmd"
-)
-
-// Volumes commands to their specific function.
-func Volumes(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Valid commands for volumes:
-
-volumes:create           create a volume for the application
-volumes:expand           expand a volume for the application
-volumes:list             list volumes in the application
-volumes:info             print information about a volume
-volumes:delete           delete a volume from the application
-volumes:mount            mount a volume to process of the application
-volumes:unmount          unmount a volume from process of the application
-
-Use 'drycc help [command]' to learn more.
-`
-
-	switch argv[0] {
-	case "volumes:create":
-		return volumesCreate(argv, cmdr)
-	case "volumes:expand":
-		return volumesExpand(argv, cmdr)
-	case "volumes:list":
-		return volumesList(argv, cmdr)
-	case "volumes:info":
-		return volumesInfo(argv, cmdr)
-	case "volumes:delete":
-		return volumesDelete(argv, cmdr)
-	case "volumes:mount":
-		return volumesMount(argv, cmdr)
-	case "volumes:unmount":
-		return volumesUnmount(argv, cmdr)
-	default:
-		if printHelp(argv, usage) {
-			return nil
-		}
-
-		if argv[0] == "volumes" {
-			argv[0] = "volumes:list"
-			return volumesList(argv, cmdr)
-		}
-
-		PrintUsage(cmdr)
-		return nil
-	}
-}
-
-func volumesCreate(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Create a volume for the application.
-
-Usage: drycc volumes:create <name> <size> [options]
-
-Arguments:
-  <name>
-    the volume name.
-  <size>
-    the volume size, such as '500G'.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-  -t --type=<type>
-    the volume type, such as csi, nfs, default is 'csi'.
-  --nfs-server=<nfs-server>
-    the hostname or ip address of the nfs server.
-  --nfs-path=<nfs-path>
-    path that is exported by the nfs server.
-  --nfs-read-only
-    a flag indicating whether the storage will be mounted as read only.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	vType := safeGetValue(args, "--type", "csi")
-	name := safeGetString(args, "<name>")
-	size := safeGetString(args, "<size>")
-	parameters := map[string]interface{}{}
-	if vType == "nfs" {
-		server := safeGetString(args, "--nfs-server")
-		path := safeGetString(args, "--nfs-path")
-		readOnly := safeGetBool(args, "--nfs-read-only")
-		parameters["nfs"] = map[string]interface{}{
-			"server":   server,
-			"path":     path,
-			"readOnly": readOnly,
-		}
-	}
-
-	return cmdr.VolumesCreate(app, name, vType, size, parameters)
-}
-
-func volumesExpand(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Expand a volume for the application.
-
-Usage: drycc volumes:expand <name> <size> [options]
-
-Arguments:
-  <name>
-    the volume name.
-  <size>
-    the volume size, such as '500G'.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	name := safeGetString(args, "<name>")
-	size := safeGetString(args, "<size>")
-
-	return cmdr.VolumesExpand(app, name, size)
-}
-
-func volumesList(argv []string, cmdr cmd.Commander) error {
-	usage := `
-List volumes in the application.
-
-Usage: drycc volumes:list [options]
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-  -l --limit=<num>
-    the maximum number of results to display, defaults to config setting
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	results, err := responseLimit(safeGetString(args, "--limit"))
-
-	if err != nil {
-		return err
-	}
-	app := safeGetString(args, "--app")
-
-	return cmdr.VolumesList(app, results)
-}
-
-func volumesInfo(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Print information about a volume.
-
-Usage: drycc volumes:info <name> [options]
-
-Arguments:
-  <name>
-    the volume name to be info.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	name := safeGetString(args, "<name>")
-
-	return cmdr.VolumesInfo(app, name)
-}
-
-func volumesDelete(argv []string, cmdr cmd.Commander) error {
-
-	usage := `
-Delete a volume from the application.
-
-Usage: drycc volumes:delete <name> [options]
-
-Arguments:
-  <name>
-    the volume name to be removed.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	name := safeGetString(args, "<name>")
-
-	return cmdr.VolumesDelete(app, name)
-}
-
-func volumesMount(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Mount a volume for an application.
-
-Usage: drycc volumes:mount <name> <type>=<path>... [options]
-
-Arguments:
-  <name>
-    the volume name.
-  <type>
-    the process name as defined in your Procfile, such as 'web' or 'worker'.
-    Note that Dockerfile apps have a default 'cmd' process type.
-  <path>
-    the filesystem path.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	name := safeGetString(args, "<name>")
-
-	return cmdr.VolumesMount(app, name, args["<type>=<path>"].([]string))
-}
-
-func volumesUnmount(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Unmount a volume for an application.
-
-Usage: drycc volumes:unmount <name> <type>... [options]
-
-Arguments:
-  <name>
-    the volume name.
-  <type>
-    the process name as defined in your Procfile, such as 'web' or 'worker'.
-    Note that Dockerfile apps have a default 'cmd' process type.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-`
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	app := safeGetString(args, "--app")
-	name := safeGetString(args, "<name>")
-
-	return cmdr.VolumesUnmount(app, name, args["<type>"].([]string))
-}
diff --git a/parser/volumes_test.go b/parser/volumes_test.go
deleted file mode 100644
index 21b941337..000000000
--- a/parser/volumes_test.go
+++ /dev/null
@@ -1,102 +0,0 @@
-package parser
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-// Create fake implementations of each method that return the argument
-// we expect to have called the function (as an error to satisfy the interface).
-
-func (d FakeDryccCmd) VolumesList(string, int) error {
-	return errors.New("volumes:list")
-}
-
-func (d FakeDryccCmd) VolumesInfo(string, string) error {
-	return errors.New("volumes:info")
-}
-
-func (d FakeDryccCmd) VolumesCreate(string, string, string, string, map[string]interface{}) error {
-	return errors.New("volumes:create")
-}
-
-func (d FakeDryccCmd) VolumesExpand(string, string, string) error {
-	return errors.New("volumes:expand")
-}
-
-func (d FakeDryccCmd) VolumesDelete(string, string) error {
-	return errors.New("volumes:delete")
-}
-
-func (d FakeDryccCmd) VolumesMount(string, string, []string) error {
-	return errors.New("volumes:mount")
-}
-
-func (d FakeDryccCmd) VolumesUnmount(string, string, []string) error {
-	return errors.New("volumes:unmount")
-}
-
-func TestVolumes(t *testing.T) {
-	t.Parallel()
-
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
-
-	// cases defines the arguments and expected return of the call.
-	// if expected is "", it defaults to args[0].
-	cases := []struct {
-		args     []string
-		expected string
-	}{
-		{
-			args:     []string{"volumes:create", "myvolume", "500G"},
-			expected: "",
-		},
-		{
-			args:     []string{"volumes:expand", "myvolume", "500G"},
-			expected: "",
-		},
-		{
-			args:     []string{"volumes:list"},
-			expected: "",
-		},
-		{
-			args:     []string{"volumes:delete", "myvolume"},
-			expected: "",
-		},
-		{
-			args:     []string{"volumes:mount", "myvolume", "cmd=data/cmd1"},
-			expected: "",
-		},
-		{
-			args:     []string{"volumes:unmount", "myvolume", "cmd"},
-			expected: "",
-		},
-		{
-			args:     []string{"volumes"},
-			expected: "volumes:list",
-		},
-	}
-
-	// For each case, check that calling the route with the arguments
-	// returns the expected error, which is args[0] if not provided.
-	for _, c := range cases {
-		var expected string
-		if c.expected == "" {
-			expected = c.args[0]
-		} else {
-			expected = c.expected
-		}
-		err = Volumes(c.args, cmdr)
-		assert.Error(t, errors.New(expected), err)
-	}
-}
diff --git a/pkg/git/git.go b/pkg/git/git.go
index 08add8d7d..d19bf079e 100644
--- a/pkg/git/git.go
+++ b/pkg/git/git.go
@@ -1,3 +1,4 @@
+// Package git provides functions for interacting with git repositories.
 package git
 
 import (
@@ -11,9 +12,9 @@ import (
 
 var (
 	// ErrRemoteNotFound is returned when the remote cannot be found in git
-	ErrRemoteNotFound = errors.New("Could not find remote matching app in 'git remote -v'")
+	ErrRemoteNotFound = errors.New("could not find remote matching app in 'git remote -v'")
 	// ErrInvalidRepositoryList is an error returned if git returns unparsible output
-	ErrInvalidRepositoryList = errors.New("Invalid output in 'git remote -v'")
+	ErrInvalidRepositoryList = errors.New("invalid output in 'git remote -v'")
 )
 
 // Cmd is a method the exeutes the given git command and returns the output or the error.
@@ -60,7 +61,6 @@ func Init(cmd Cmd) error {
 // DeleteAppRemotes removes all git remotes corresponding to an app in the repository.
 func DeleteAppRemotes(cmd Cmd, host, appID string) error {
 	names, err := remoteNamesFromAppID(cmd, host, appID)
-
 	if err != nil {
 		return err
 	}
@@ -105,9 +105,11 @@ func remoteNamesFromAppID(cmd Cmd, host, appID string) ([]string, error) {
 // DetectAppName detects if there is drycc remote in git.
 func DetectAppName(cmd Cmd, host string) (string, error) {
 	remote, err := findRemote(cmd, host)
-
 	// Don't return an error if remote can't be found, return directory name instead.
 	if err != nil {
+		if appName, ok := os.LookupEnv("DRYCC_APP"); ok {
+			return appName, nil
+		}
 		dir, err := os.Getwd()
 		return strings.ToLower(filepath.Base(dir)), err
 	}
diff --git a/pkg/i18n/i18n.go b/pkg/i18n/i18n.go
new file mode 100644
index 000000000..0d871dc11
--- /dev/null
+++ b/pkg/i18n/i18n.go
@@ -0,0 +1,210 @@
+/*
+Copyright 2016 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package i18n provides internationalization support for the CLI
+package i18n
+
+import (
+	"archive/zip"
+	"bytes"
+	"embed"
+	"errors"
+	"fmt"
+	"os"
+	"strings"
+	"sync"
+
+	"github.com/chai2010/gettext-go"
+	"k8s.io/klog/v2"
+)
+
+//go:embed translations
+var translations embed.FS
+
+// defaultLanguage is the default language for translations. It is used when no language is specified in the translation key.
+const defaultLanguage = "en_US"
+
+var knownTranslations = map[string][]string{
+	"drycc": {
+		"en_US",
+		"zh_CN",
+	},
+	// only used for unit tests.
+	"test": {
+		"en_US",
+		"zh_CN",
+	},
+}
+
+var (
+	lazyLoadTranslationsOnce sync.Once
+	// LoadTranslationsFunc is the function called to lazy load translations.
+	LoadTranslationsFunc = func() error {
+		return LoadTranslations("drycc", nil)
+	}
+	translationsLoaded bool
+)
+
+// SetLoadTranslationsFunc sets the function called to lazy load translations.
+// It must be called in an init() func that occurs BEFORE any i18n.T() calls are made by any package. You can
+// accomplish this by creating a separate package containing your init() func, and then importing that package BEFORE
+// any other packages that call i18n.T().
+//
+// Example Usage:
+//
+//	package myi18n
+//
+//	import "k8s.io/kubectl/pkg/util/i18n"
+//
+//	func init() {
+//		if err := i18n.SetLoadTranslationsFunc(loadCustomTranslations); err != nil {
+//			panic(err)
+//		}
+//	}
+//
+//	func loadCustomTranslations() error {
+//		// Load your custom translations here...
+//	}
+//
+// And then in your main or root command package, import your custom package like this:
+//
+//	import (
+//		// Other imports that don't need i18n...
+//		_ "example.com/myapp/myi18n"
+//		// Other imports that do need i18n...
+//	)
+func SetLoadTranslationsFunc(f func() error) error {
+	if translationsLoaded {
+		return errors.New("translations have already been loaded")
+	}
+	LoadTranslationsFunc = func() error {
+		if err := f(); err != nil {
+			return err
+		}
+		translationsLoaded = true
+		return nil
+	}
+	return nil
+}
+
+func loadSystemLanguage() string {
+	// Implements the following locale priority order: LC_ALL, LC_MESSAGES, LANG
+	// Similarly to: https://www.gnu.org/software/gettext/manual/html_node/Locale-Environment-Variables.html
+	langStr := os.Getenv("LC_ALL")
+	if langStr == "" {
+		langStr = os.Getenv("LC_MESSAGES")
+	}
+	if langStr == "" {
+		langStr = os.Getenv("LANG")
+	}
+
+	if langStr == "" {
+		klog.V(3).Infof("Couldn't find the LC_ALL, LC_MESSAGES or LANG environment variables, defaulting to %s", defaultLanguage)
+		return defaultLanguage
+	}
+	pieces := strings.Split(langStr, ".")
+	if len(pieces) != 2 {
+		klog.V(3).Infof("Unexpected system language (%s), defaulting to %s", langStr, defaultLanguage)
+		return defaultLanguage
+	}
+	return pieces[0]
+}
+
+func findLanguage(root string, getLanguageFn func() string) string {
+	langStr := getLanguageFn()
+
+	translations := knownTranslations[root]
+	for ix := range translations {
+		if translations[ix] == langStr {
+			return langStr
+		}
+	}
+	klog.V(3).Infof("Couldn't find translations for %s, using %s", langStr, defaultLanguage)
+	return defaultLanguage
+}
+
+// LoadTranslations loads translation files. getLanguageFn should return a language
+// string (e.g. 'en-US'). If getLanguageFn is nil, then the loadSystemLanguage function
+// is used, which uses the 'LANG' environment variable.
+func LoadTranslations(root string, getLanguageFn func() string) error {
+	if getLanguageFn == nil {
+		getLanguageFn = loadSystemLanguage
+	}
+
+	langStr := findLanguage(root, getLanguageFn)
+	translationFiles := []string{
+		fmt.Sprintf("%s/%s/LC_MESSAGES/cli.po", root, langStr),
+		fmt.Sprintf("%s/%s/LC_MESSAGES/cli.mo", root, langStr),
+	}
+	klog.V(3).Infof("Setting language to %s", langStr)
+	// TODO: list the directory and load all files.
+	buf := new(bytes.Buffer)
+	w := zip.NewWriter(buf)
+
+	// Make sure to check the error on Close.
+	for _, file := range translationFiles {
+		filename := "translations/" + file
+		f, err := w.Create(file)
+		if err != nil {
+			return err
+		}
+		data, err := translations.ReadFile(filename)
+		if err != nil {
+			return err
+		}
+		if _, err := f.Write(data); err != nil {
+			return nil
+		}
+	}
+	if err := w.Close(); err != nil {
+		return err
+	}
+	gettext.BindLocale(gettext.New("cli", root+".zip", buf.Bytes()))
+	gettext.SetDomain("cli")
+	gettext.SetLanguage(langStr)
+	translationsLoaded = true
+	return nil
+}
+
+func lazyLoadTranslations() {
+	lazyLoadTranslationsOnce.Do(func() {
+		if translationsLoaded {
+			return
+		}
+		if err := LoadTranslationsFunc(); err != nil {
+			klog.Warning("Failed to load translations")
+		}
+	})
+}
+
+// T translates a string, possibly substituting arguments into it along
+// the way. If len(args) is > 0, args1 is assumed to be the plural value
+// and plural translation is used.
+func T(defaultValue string, args ...int) string {
+	lazyLoadTranslations()
+	if len(args) == 0 {
+		return gettext.PGettext("", defaultValue)
+	}
+	return fmt.Sprintf(gettext.PNGettext("", defaultValue, defaultValue+".plural", args[0]),
+		args[0])
+}
+
+// Errorf produces an error with a translated error string.
+// Substitution is performed via the `T` function above, following
+// the same rules.
+func Errorf(defaultValue string, args ...int) error {
+	return errors.New(T(defaultValue, args...))
+}
diff --git a/pkg/i18n/i18n_test.go b/pkg/i18n/i18n_test.go
new file mode 100644
index 000000000..33c6c9a48
--- /dev/null
+++ b/pkg/i18n/i18n_test.go
@@ -0,0 +1,292 @@
+/*
+Copyright 2016 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package i18n
+
+import (
+	"os"
+	"sync"
+	"testing"
+
+	"github.com/chai2010/gettext-go"
+)
+
+var knownTestLocale = "zh_CN.UTF-8"
+
+func TestTranslation(t *testing.T) {
+	err := LoadTranslations("test", func() string { return defaultLanguage })
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+	}
+
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+	}
+	result := T("test_string")
+	if result != "foo" {
+		t.Errorf("expected: %s, saw: %s", "foo", result)
+	}
+}
+
+func TestTranslationPlural(t *testing.T) {
+	err := LoadTranslations("test", func() string { return defaultLanguage })
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+	}
+
+	result := T("test_plural", 3)
+	if result != "there were 3 items" {
+		t.Errorf("expected: %s, saw: %s", "there were 3 items", result)
+	}
+
+	result = T("test_plural", 1)
+	if result != "there was 1 item" {
+		t.Errorf("expected: %s, saw: %s", "there was 1 item", result)
+	}
+}
+
+func TestTranslationUsingEnvVar(t *testing.T) {
+	// We must backup and restore env vars before setting test values in tests
+	// othervise we are risking to break other tests/test cases
+	// which rely on the same env vars
+	envVarsToBackup := []string{"LC_MESSAGES", "LANG", "LC_ALL"}
+	expectedStrEnUSLocale := "baz"
+	expectedStrFallback := "foo"
+
+	testCases := []struct {
+		name        string
+		setenv      map[string]string
+		expectedStr string
+	}{
+		{
+			name:        "Only LC_ALL is set",
+			setenv:      map[string]string{"LC_ALL": knownTestLocale},
+			expectedStr: expectedStrEnUSLocale,
+		},
+		{
+			name:        "Only LC_MESSAGES is set",
+			setenv:      map[string]string{"LC_MESSAGES": knownTestLocale},
+			expectedStr: expectedStrEnUSLocale,
+		},
+		{
+			name:        "Only LANG",
+			setenv:      map[string]string{"LANG": knownTestLocale},
+			expectedStr: expectedStrEnUSLocale,
+		},
+		{
+			name: "LC_MESSAGES overrides LANG",
+			setenv: map[string]string{
+				"LANG":        "be_BY.UTF-8", // Unknown locale
+				"LC_MESSAGES": knownTestLocale,
+			},
+			expectedStr: expectedStrEnUSLocale,
+		},
+		{
+			name: "LC_ALL overrides LANG",
+			setenv: map[string]string{
+				"LANG":   "be_BY.UTF-8", // Unknown locale
+				"LC_ALL": knownTestLocale,
+			},
+			expectedStr: expectedStrEnUSLocale,
+		},
+		{
+			name: "LC_ALL overrides LC_MESSAGES",
+			setenv: map[string]string{
+				"LC_MESSAGES": "be_BY.UTF-8", // Unknown locale
+				"LC_ALL":      knownTestLocale,
+			},
+			expectedStr: expectedStrEnUSLocale,
+		},
+		{
+			name:        "Unknown locale in LANG",
+			setenv:      map[string]string{"LANG": "be_BY.UTF-8"},
+			expectedStr: expectedStrFallback,
+		},
+		{
+			name:        "Unknown locale in LC_MESSAGES",
+			setenv:      map[string]string{"LC_MESSAGES": "be_BY.UTF-8"},
+			expectedStr: expectedStrFallback,
+		},
+		{
+			name:        "Unknown locale in LC_ALL",
+			setenv:      map[string]string{"LC_ALL": "be_BY.UTF-8"},
+			expectedStr: expectedStrFallback,
+		},
+		{
+			name:        "Invalid env var",
+			setenv:      map[string]string{"LC_MESSAGES": "fake.locale.UTF-8"},
+			expectedStr: expectedStrFallback,
+		},
+		{
+			name:        "No env vars",
+			expectedStr: expectedStrFallback,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			for _, envVar := range envVarsToBackup {
+				if envVarValue := os.Getenv(envVar); envVarValue != "" {
+					envVarValue, envVar := envVarValue, envVar
+					t.Cleanup(func() { os.Setenv(envVar, envVarValue) })
+					os.Unsetenv(envVar)
+				}
+			}
+
+			for envVar, envVarValue := range test.setenv {
+				t.Setenv(envVar, envVarValue)
+			}
+
+			err := LoadTranslations("test", nil)
+			if err != nil {
+				t.Errorf("Unexpected error: %v", err)
+			}
+
+			result := T("test_string")
+			if result != test.expectedStr {
+				t.Errorf("expected: %s, saw: %s", test.expectedStr, result)
+			}
+		})
+	}
+}
+
+// resetLazyLoading allows multiple tests to test translation lazy loading by resetting the state
+func resetLazyLoading() {
+	translationsLoaded = false
+	lazyLoadTranslationsOnce = sync.Once{}
+}
+
+func TestLazyLoadTranslationFuncIsCalled(t *testing.T) {
+	resetLazyLoading()
+
+	timesCalled := 0
+	err := SetLoadTranslationsFunc(func() error {
+		timesCalled++
+		return LoadTranslations("test", func() string { return "zh_CN" })
+	})
+	if err != nil {
+		t.Errorf("unexpected error: %v", err)
+	}
+	if translationsLoaded {
+		t.Errorf("expected translationsLoaded to be false, but it was true")
+	}
+
+	// Translation should succeed and use the lazy loaded translations
+	result := T("test_string")
+	if result != "baz" {
+		t.Errorf("expected: %s, saw: %s", "baz", result)
+	}
+	if timesCalled != 1 {
+		t.Errorf("expected LoadTranslationsFunc to have been called 1 time, but it was called %d times", timesCalled)
+	}
+	if !translationsLoaded {
+		t.Errorf("expected translationsLoaded to be true, but it was false")
+	}
+
+	// Call T() again, and timesCalled should remain 1
+	T("test_string")
+	if timesCalled != 1 {
+		t.Errorf("expected LoadTranslationsFunc to have been called 1 time, but it was called %d times", timesCalled)
+	}
+}
+
+func TestLazyLoadTranslationFuncOnlyCalledIfTranslationsNotLoaded(t *testing.T) {
+	resetLazyLoading()
+
+	// Set a custom translations func
+	timesCalled := 0
+	err := SetLoadTranslationsFunc(func() error {
+		timesCalled++
+		return LoadTranslations("test", func() string { return defaultLanguage })
+	})
+	if err != nil {
+		t.Errorf("unexpected error: %v", err)
+	}
+	if translationsLoaded {
+		t.Errorf("expected translationsLoaded to be false, but it was true")
+	}
+
+	// Explicitly load translations before lazy loading can occur
+	err = LoadTranslations("test", func() string { return defaultLanguage })
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+	}
+	if !translationsLoaded {
+		t.Errorf("expected translationsLoaded to be true, but it was false")
+	}
+
+	// Translation should succeed, and use the explicitly loaded translations, not the lazy loaded ones
+	result := T("test_string")
+	if result != "foo" {
+		t.Errorf("expected: %s, saw: %s", "foo", result)
+	}
+	if timesCalled != 0 {
+		t.Errorf("expected LoadTranslationsFunc to have not been called, but it was called %d times", timesCalled)
+	}
+}
+
+func TestSetCustomLoadTranslationsFunc(t *testing.T) {
+	resetLazyLoading()
+
+	// Set a custom translations func that loads translations from a directory
+	err := SetLoadTranslationsFunc(func() error {
+		gettext.BindLocale(gettext.New("cli", "./translations/test"))
+		gettext.SetDomain("cli")
+		gettext.SetLanguage("zh_CN")
+		return nil
+	})
+	if err != nil {
+		t.Errorf("unexpected error: %v", err)
+	}
+	if translationsLoaded {
+		t.Errorf("expected translationsLoaded to be false, but it was true")
+	}
+
+	// Translation should succeed
+	result := T("test_string")
+	if result != "baz" {
+		t.Errorf("expected: %s, saw: %s", "baz", result)
+	}
+	if !translationsLoaded {
+		t.Errorf("expected translationsLoaded to be true, but it was false")
+	}
+}
+
+func TestSetCustomLoadTranslationsFuncAfterTranslationsLoadedShouldFail(t *testing.T) {
+	resetLazyLoading()
+
+	// Explicitly load translations
+	err := LoadTranslations("test", func() string { return defaultLanguage })
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+	}
+	if !translationsLoaded {
+		t.Errorf("expected translationsLoaded to be true, but it was false")
+	}
+
+	// This should fail because translations have already been loaded, and the custom function should not be called.
+	timesCalled := 0
+	err = SetLoadTranslationsFunc(func() error {
+		timesCalled++
+		return nil
+	})
+	if err == nil {
+		t.Errorf("expected error, but it did not occur")
+	}
+	if timesCalled != 0 {
+		t.Errorf("expected LoadTranslationsFunc to have not been called, but it was called %d times", timesCalled)
+	}
+}
diff --git a/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.mo b/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.mo
new file mode 100644
index 000000000..7e4b6d47c
Binary files /dev/null and b/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.mo differ
diff --git a/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.po b/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.po
new file mode 100644
index 000000000..158924d35
--- /dev/null
+++ b/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.po
@@ -0,0 +1,1873 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"Report-Msgid-Bugs-To: EMAIL\n"
+"POT-Creation-Date: 2026-03-26 14:27+0800\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: en_US\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: internal/parser/builds.go:66 internal/parser/builds.go:113
+msgid "A YAML file used to supply a Procfile to the application"
+msgstr ""
+
+#: internal/parser/keys.go:53
+msgid "A local file path to an SSH public key used to push application code"
+msgstr ""
+
+#: internal/parser/config.go:158 internal/parser/config.go:196
+msgid "A path leading to an environment file"
+msgstr ""
+
+#: internal/parser/tls.go:123
+msgid "ACME account email"
+msgstr ""
+
+#: internal/parser/tls.go:124
+msgid "ACME server URL"
+msgstr ""
+
+#: internal/parser/volumes.go:93
+msgid "Access key id for authentication"
+msgstr ""
+
+#: internal/parser/tls.go:55 internal/parser/tls.go:84
+msgid "Action to perform, 'enable' or 'disable'"
+msgstr ""
+
+#: internal/parser/tokens.go:58
+msgid "Add a token for controller authentication"
+msgstr ""
+
+#: internal/parser/keys.go:57
+msgid "Add an SSH key"
+msgstr ""
+
+#: internal/parser/certs.go:65
+msgid "Add an SSL certificate to an app"
+msgstr ""
+
+#: internal/parser/labels.go:60
+msgid "Add new application's label"
+msgstr ""
+
+#: internal/parser/keys.go:58
+msgid "Adds SSH keys for the logged in user"
+msgstr ""
+
+#: internal/parser/git.go:36
+msgid "Adds git remote of application to repository"
+msgstr ""
+
+#: internal/parser/config.go:160
+msgid "Allows you to have the pull overwrite keys to the path"
+msgstr ""
+
+#: internal/parser/config.go:57
+msgid "An app config info"
+msgstr ""
+
+#: internal/parser/tls.go:23
+msgid "Application name"
+msgstr ""
+
+#: internal/parser/certs.go:135
+msgid "Attach a certificate to a domain"
+msgstr ""
+
+#: internal/parser/certs.go:134
+msgid "Attach an SSL certificate to a domain"
+msgstr ""
+
+#: internal/parser/routes.go:179
+msgid "Attach to gateway"
+msgstr ""
+
+#: internal/parser/auth.go:34
+msgid "Authenticate against a controller"
+msgstr ""
+
+#: internal/parser/tls.go:89
+msgid "Automatic certificate management"
+msgstr ""
+
+#: internal/parser/domains.go:55
+msgid "Bind a domain to an application"
+msgstr ""
+
+#: internal/parser/resources.go:242
+msgid "Bind a resource for an application"
+msgstr ""
+
+#: internal/parser/volumes.go:92
+msgid "Bucket name in object storage"
+msgstr ""
+
+#: internal/parser/tls.go:125
+msgid "CA key ID"
+msgstr ""
+
+#: internal/parser/tls.go:126
+msgid "CA key secret"
+msgstr ""
+
+#: internal/parser/pts.go:160
+msgid "Clean process types of not used"
+msgstr ""
+
+#: internal/parser/auth.go:53
+msgid "Clear the current user session"
+msgstr ""
+
+#: internal/parser/apps.go:146
+msgid "Command execution timeout in seconds"
+msgstr ""
+
+#: internal/parser/certs.go:130
+msgid ""
+"Common name of the domain to attach to (needs to already be in the system)"
+msgstr ""
+
+#: internal/parser/certs.go:155
+msgid "Common name of the domain to detach from"
+msgstr ""
+
+#: internal/parser/tls.go:117
+msgid "Configure automatic certificate management issuer details"
+msgstr ""
+
+#: internal/parser/apps.go:45
+msgid "Create a new application"
+msgstr ""
+
+#: internal/parser/resources.go:98
+msgid "Create a resource for the application"
+msgstr ""
+
+#: internal/parser/routes.go:56
+msgid "Create a route for an application"
+msgstr ""
+
+#: internal/parser/volumes.go:62
+msgid "Create a volume for the application"
+msgstr ""
+
+#: internal/parser/workspaces.go:64
+msgid "Create a workspace"
+msgstr "Create a workspace"
+
+#: internal/parser/gateways.go:59
+msgid "Create gateways for an application"
+msgstr ""
+
+#: internal/parser/services.go:55
+msgid "Create service for an application"
+msgstr ""
+
+#: internal/parser/apps.go:46
+msgid ""
+"Creates a new application. If no <id> is provided, one will be generated "
+"automatically"
+msgstr ""
+
+#: internal/parser/builds.go:53
+msgid ""
+"Creates a new build of an application. Imports an <image> and deploys it to "
+"Drycc\n"
+"as a new release. If a Procfile or drycc.yaml is present in the current "
+"directory,\n"
+"it will be used as the default for this application."
+msgstr ""
+
+#: internal/parser/services.go:56
+msgid ""
+"Creates extra service for an application and binds it to specific route of "
+"the main app domain"
+msgstr ""
+
+#: internal/parser/gateways.go:60
+msgid ""
+"Creates gateways for an application and binds it to allow listener of the "
+"main app domain"
+msgstr ""
+
+#: internal/parser/routes.go:57
+msgid "Creates routes for an application, provides a way to route requests"
+msgstr ""
+
+#: internal/parser/resources.go:217
+msgid "Delete a resource from the application"
+msgstr ""
+
+#: internal/parser/volumes.go:190
+msgid "Delete a volume from the application"
+msgstr ""
+
+#: internal/parser/workspaces.go:112
+msgid "Delete a workspace"
+msgstr "Delete a workspace"
+
+#: internal/parser/ps.go:155
+msgid "Delete the selected processes"
+msgstr ""
+
+#: internal/parser/gateways.go:92
+msgid "Deletes specific gateway for application"
+msgstr ""
+
+#: internal/parser/releases.go:108
+msgid "Deploy the latest release by process types"
+msgstr ""
+
+#: internal/parser/pts.go:65
+msgid "Describe a process type"
+msgstr ""
+
+#: internal/parser/apps.go:164
+msgid "Destroy an application"
+msgstr ""
+
+#: internal/parser/certs.go:159
+msgid "Detach an SSL certificate from a domain"
+msgstr ""
+
+#: internal/parser/certs.go:160
+msgid "Detach certificate from a domain"
+msgstr ""
+
+#: internal/parser/routes.go:215
+msgid "Dettach to gateway"
+msgstr ""
+
+#: internal/parser/autorollback.go:58
+msgid "Disable autorollback for an application"
+msgstr ""
+
+#: internal/parser/routing.go:58
+msgid "Disable routing for an application"
+msgstr ""
+
+#: internal/parser/autodeploy.go:58
+msgid "Disables autodeploy for an app"
+msgstr ""
+
+#: internal/parser/version.go:16
+msgid "Display client version"
+msgstr ""
+
+#: cmd/root.go:39
+msgid "Display help information"
+msgstr "Display help information"
+
+#: internal/parser/auth.go:71
+msgid "Display the current user"
+msgstr ""
+
+#: internal/parser/version.go:17
+msgid "Displays the client version"
+msgstr ""
+
+#: internal/parser/auth.go:72
+msgid "Displays the currently logged in user"
+msgstr ""
+
+#: internal/parser/apps.go:58
+msgid "Do not create a 'drycc' git remote"
+msgstr ""
+
+#: internal/parser/builds.go:67 internal/parser/builds.go:114
+msgid "Drycc config path to the application"
+msgstr ""
+
+#: internal/parser/autorollback.go:46
+msgid "Enable autorollback for an application"
+msgstr ""
+
+#: internal/parser/workspaces.go:201
+msgid "Enable or disable alerts: true or false"
+msgstr "Enable or disable alerts: true or false"
+
+#: internal/parser/routing.go:46
+msgid "Enable routing for an application"
+msgstr ""
+
+#: internal/parser/autodeploy.go:46
+msgid "Enables autodeploy for an app"
+msgstr ""
+
+#: internal/parser/auth.go:43
+msgid "Enables or disables SSL certificate verification for API requests"
+msgstr ""
+
+#: internal/parser/volumes.go:91
+msgid "Endpoint url for object storage service"
+msgstr ""
+
+#: internal/parser/ps.go:102
+msgid "Execute a command in a container"
+msgstr ""
+
+#: internal/parser/volumes.go:122
+msgid "Expand a volume capacity for the application"
+msgstr ""
+
+#: internal/parser/config.go:145
+msgid ""
+"Extract all environment variables from an application or config group. for "
+"local use.\n"
+"\n"
+"The environmental variables can be piped into a file, 'drycc config pull > "
+"file',\n"
+"or stored locally in a file named .env. This file can be\n"
+"read by foreman to load the local environment for your app."
+msgstr ""
+"Extract all environment variables from an application or config group. for "
+"local use.\n"
+"\n"
+"The environmental variables can be piped into a file, 'drycc config pull > "
+"file',\n"
+"or stored locally in a file named .env. This file can be\n"
+"read by foreman to load the local environment for your app."
+
+#: internal/parser/auth.go:78
+msgid "Fetch detailed user information"
+msgstr ""
+
+#: internal/parser/certs.go:111
+msgid "Fetch more detailed information about a certificate"
+msgstr ""
+
+#: internal/parser/tls.go:60
+msgid "Force TLS settings to HTTPS-only redirection"
+msgstr ""
+
+#: internal/parser/volumes.go:95
+msgid "Force a path-style endpoint to be used"
+msgstr ""
+
+#: internal/parser/releases.go:116
+msgid "Force deploy"
+msgstr ""
+
+#: internal/parser/resources.go:148
+msgid "Get a resource's detail in the application"
+msgstr ""
+
+#: internal/parser/certs.go:110
+msgid "Get detailed informaton about the certificate"
+msgstr ""
+
+#: internal/parser/routes.go:123
+msgid "Get route rules"
+msgstr ""
+
+#: internal/parser/routes.go:124
+msgid "Get route rules for an application"
+msgstr ""
+
+#: internal/parser/healthchecks.go:187
+msgid "How often (in seconds) to perform the probe"
+msgstr ""
+
+#: internal/parser/workspaces.go:135
+msgid "Invite a user to a workspace"
+msgstr "Invite a user to a workspace"
+
+#: internal/parser/keys.go:30
+msgid "List SSH keys for the logged in user"
+msgstr ""
+
+#: internal/parser/certs.go:40
+msgid "List SSL certificates for an application"
+msgstr ""
+
+#: internal/parser/apps.go:69
+msgid "List accessible applications"
+msgstr ""
+
+#: internal/parser/limits.go:141
+msgid "List all available limit plans"
+msgstr ""
+
+#: internal/parser/limits.go:119
+msgid "List all available limit specs"
+msgstr ""
+
+#: internal/parser/resources.go:66
+msgid "List all available plans for a resource service"
+msgstr ""
+
+#: internal/parser/resources.go:45
+msgid "List all available resource services"
+msgstr ""
+
+#: internal/parser/releases.go:53
+msgid "List an application's release history"
+msgstr ""
+
+#: internal/parser/gateways.go:36
+msgid "List application gateways"
+msgstr ""
+
+#: internal/parser/ps.go:37
+msgid "List application pods"
+msgstr ""
+
+#: internal/parser/pts.go:38
+msgid "List application process types"
+msgstr ""
+
+#: internal/parser/routes.go:100
+msgid "List application routes"
+msgstr ""
+
+#: internal/parser/services.go:76
+msgid "List application services"
+msgstr ""
+
+#: internal/parser/labels.go:34
+msgid "List application's labels"
+msgstr ""
+
+#: internal/parser/autoscale.go:34
+msgid "List autoscale options for an application"
+msgstr ""
+
+#: internal/parser/domains.go:36
+msgid "List domains bound to an application"
+msgstr ""
+
+#: internal/parser/healthchecks.go:50
+msgid "List healthchecks for an application"
+msgstr ""
+
+#: internal/parser/lifecycles.go:50
+msgid "List lifecycles for an application"
+msgstr "List lifecycles for an application"
+
+#: internal/parser/registry.go:44
+msgid "List registry info for an app"
+msgstr ""
+
+#: internal/parser/limits.go:41
+msgid "List resource limits for an app"
+msgstr ""
+
+#: internal/parser/timeouts.go:38
+msgid "List resource timeouts for an app"
+msgstr ""
+
+#: internal/parser/resources.go:120
+msgid "List resources in the application"
+msgstr ""
+
+#: internal/parser/limits.go:118
+msgid "List specification information of the server"
+msgstr ""
+
+#: internal/parser/tags.go:52
+msgid "List tags for an application"
+msgstr ""
+
+#: internal/parser/volumes.go:138
+msgid "List volumes in the application"
+msgstr ""
+
+#: internal/parser/apps.go:70
+msgid "Lists applications visible to the current user"
+msgstr ""
+
+#: internal/parser/gateways.go:37
+msgid "Lists gateways for an application"
+msgstr ""
+
+#: internal/parser/pts.go:39
+msgid "Lists process types servicing an application"
+msgstr ""
+
+#: internal/parser/ps.go:38
+msgid "Lists processes servicing an application"
+msgstr ""
+
+#: internal/parser/registry.go:45
+msgid "Lists registry information for an application"
+msgstr ""
+
+#: internal/parser/releases.go:54
+msgid "Lists release history for an application"
+msgstr ""
+
+#: internal/parser/tokens.go:33
+msgid "Lists tokens visible to the current controller"
+msgstr ""
+
+#: internal/parser/workspaces.go:39
+msgid "Lists workspaces for current user"
+msgstr "Lists workspaces for current user"
+
+#: internal/parser/auth.go:35
+msgid "Logs in by authenticating against a controller"
+msgstr ""
+
+#: internal/parser/auth.go:54
+msgid "Logs out from a controller and clears the user session"
+msgstr ""
+
+#: internal/parser/certs.go:15
+msgid "Manage SSL endpoints for an app"
+msgstr ""
+
+#: internal/parser/tls.go:17
+msgid "Manage TLS/SSL settings for applications"
+msgstr ""
+
+#: internal/parser/domains.go:14
+msgid "Manage and assign domain names to your applications"
+msgstr ""
+
+#: internal/parser/healthchecks.go:24
+msgid "Manage application healthchecks"
+msgstr ""
+
+#: internal/parser/lifecycles.go:24
+msgid "Manage application lifecycles"
+msgstr "Manage application lifecycles"
+
+#: internal/parser/apps.go:18
+msgid "Manage applications used to provide services"
+msgstr ""
+
+#: internal/parser/auth.go:13
+msgid "Manage authentication"
+msgstr ""
+
+#: internal/parser/autodeploy.go:14
+msgid "Manage autodeploy if or not for applications"
+msgstr ""
+
+#: internal/parser/autorollback.go:14
+msgid "Manage autorollback if or not for application"
+msgstr ""
+
+#: internal/parser/autoscale.go:15
+msgid "Manage autoscale for applications"
+msgstr ""
+
+#: internal/parser/builds.go:15
+msgid "Manage builds created using 'git push'"
+msgstr ""
+
+#: internal/parser/config.go:23
+msgid "Manage environment variables that define app config"
+msgstr ""
+
+#: internal/parser/gateways.go:14
+msgid "Manage gateways for your applications"
+msgstr ""
+
+#: internal/parser/git.go:14
+msgid "Manage git for applications"
+msgstr ""
+
+#: internal/parser/labels.go:15
+msgid "Manage labels of application"
+msgstr ""
+
+#: internal/parser/timeouts.go:15
+msgid "Manage pods termination grace period"
+msgstr ""
+
+#: internal/parser/registry.go:20
+msgid "Manage private registry information for your application"
+msgstr ""
+
+#: internal/parser/pts.go:15
+msgid "Manage process types inside an app"
+msgstr ""
+
+#: internal/parser/ps.go:15
+msgid "Manage processes inside an app"
+msgstr ""
+
+#: internal/parser/releases.go:24
+msgid "Manage releases of an application"
+msgstr ""
+
+#: internal/parser/limits.go:15
+msgid "Manage resource limits for your application"
+msgstr ""
+
+#: internal/parser/resources.go:17
+msgid "Manage resources for your applications"
+msgstr ""
+
+#: internal/parser/routing.go:14
+msgid "Manage routability of an application"
+msgstr ""
+
+#: internal/parser/routes.go:20
+msgid "Manage routes for your applications"
+msgstr ""
+
+#: internal/parser/services.go:17
+msgid "Manage services for your applications"
+msgstr ""
+
+#: internal/parser/keys.go:15
+msgid "Manage ssh keys used for 'git push' deployments"
+msgstr ""
+
+#: internal/parser/tags.go:19
+msgid "Manage tags for application containers"
+msgstr ""
+
+#: internal/parser/tokens.go:15
+msgid "Manage user tokens"
+msgstr ""
+
+#: internal/parser/volumes.go:15
+msgid "Manage volumes for your applications"
+msgstr ""
+
+#: internal/parser/workspaces.go:17
+msgid "Manage workspaces"
+msgstr "Manage workspaces"
+
+#: internal/parser/autoscale.go:73
+msgid "Max replicas to scale up to"
+msgstr ""
+
+#: internal/parser/apps.go:23 internal/parser/apps.go:76
+msgid "Maximum number of results to display"
+msgstr ""
+
+#: internal/parser/config.go:197
+msgid "Merge config values"
+msgstr "Merge config values"
+
+#: internal/parser/healthchecks.go:189
+msgid ""
+"Minimum consecutive successes for the probe to be considered failed after "
+"having succeeded"
+msgstr ""
+
+#: internal/parser/healthchecks.go:188
+msgid ""
+"Minimum consecutive successes for the probe to be considered successful "
+"after having failed"
+msgstr ""
+
+#: internal/parser/volumes.go:238
+msgid "Mount a volume to process of the application"
+msgstr ""
+
+#: internal/parser/git.go:42 internal/parser/apps.go:57
+msgid "Name of remote to create"
+msgstr ""
+
+#: internal/parser/keys.go:52
+msgid "Name of the SSH key"
+msgstr ""
+
+#: internal/parser/certs.go:129
+msgid "Name of the certificate to attach domain to"
+msgstr ""
+
+#: internal/parser/certs.go:154
+msgid "Name of the certificate to deatch from a domain"
+msgstr ""
+
+#: internal/parser/certs.go:59
+msgid "Name of the certificate to reference it by"
+msgstr ""
+
+#: internal/parser/services.go:52
+msgid ""
+"Number or name of the port to access on the pods targeted by the service"
+msgstr ""
+
+#: internal/parser/apps.go:102
+msgid "Open the application in a browser"
+msgstr ""
+
+#: internal/parser/apps.go:103
+msgid "Opens a URL to the application in the default browser"
+msgstr ""
+
+#: internal/parser/git.go:43
+msgid "Overwrite remote of the given name if it already exists"
+msgstr ""
+
+#: internal/parser/ps.go:114
+msgid "Pass stdin to the container"
+msgstr ""
+
+#: internal/parser/volumes.go:89
+msgid "Path that is exported by the nfs server"
+msgstr ""
+
+#: cmd/root.go:38
+msgid "Path to configuration file"
+msgstr "Path to configuration file"
+
+#: internal/parser/routes.go:188 internal/parser/routes.go:224
+msgid "Port is the network port, the gateway listener expects to receive"
+msgstr ""
+
+#: internal/parser/gateways.go:67
+msgid "Port is the network port, the listener expects to receive"
+msgstr ""
+
+#: internal/parser/gateways.go:100
+msgid "Port is the network port, the listener received"
+msgstr "Port is the network port, the listener received"
+
+#: internal/parser/ps.go:133
+msgid "Print a detailed description of the selected process"
+msgstr ""
+
+#: internal/parser/pts.go:66
+msgid "Print a detailed description of the selected process type"
+msgstr ""
+
+#: internal/parser/builds.go:82
+msgid "Print information about a specific build"
+msgstr ""
+
+#: internal/parser/releases.go:79
+msgid "Print information about a specific release"
+msgstr ""
+
+#: internal/parser/volumes.go:164
+msgid "Print information about a volume"
+msgstr ""
+
+#: internal/parser/workspaces.go:85
+msgid "Print information about a workspace"
+msgstr "Print information about a workspace"
+
+#: internal/parser/builds.go:107
+msgid "Print process info about a specific build"
+msgstr ""
+
+#: internal/parser/ps.go:63
+msgid "Print the logs for a container"
+msgstr ""
+
+#: internal/parser/ps.go:64
+msgid "Print the logs for a container in a pod or specified resource"
+msgstr ""
+
+#: internal/parser/ps.go:78
+msgid ""
+"Print the logs for the previous instance of the container in a pod if it "
+"exists"
+msgstr ""
+
+#: internal/parser/ps.go:77
+msgid "Print the logs of this container"
+msgstr ""
+
+#: internal/parser/autoscale.go:35
+msgid "Prints a list of autoscale options for the application"
+msgstr ""
+
+#: internal/parser/labels.go:35
+msgid "Prints a list of labels of the application"
+msgstr ""
+
+#: internal/parser/releases.go:80
+msgid "Prints info about a particular release"
+msgstr ""
+
+#: internal/parser/apps.go:85
+msgid "Prints info about the current application"
+msgstr ""
+
+#: internal/parser/autodeploy.go:34
+msgid "Prints info about the current application's autodeploy if or not"
+msgstr ""
+
+#: internal/parser/autorollback.go:34
+msgid "Prints info about the current application's autorollback if or not"
+msgstr ""
+
+#: internal/parser/config.go:159
+msgid "Prompts for each value to be overwritten"
+msgstr ""
+
+#: internal/parser/gateways.go:68
+msgid ""
+"Protocol specifies the network protocol this listener expects to receive. "
+"Supports TCP, UDP, TLS, HTTP, and HTTPS"
+msgstr ""
+
+#: internal/parser/gateways.go:101
+msgid "Protocol specifies the network protocol this listener received"
+msgstr "Protocol specifies the network protocol this listener received"
+
+#: internal/parser/tokens.go:55
+msgid "Provide a alias for controller authentication token"
+msgstr ""
+
+#: internal/parser/auth.go:42 internal/parser/tokens.go:68
+msgid "Provide a password for the account"
+msgstr ""
+
+#: internal/parser/auth.go:41 internal/parser/tokens.go:67
+msgid "Provide a username for the account"
+msgstr ""
+
+#: internal/parser/config.go:144
+msgid "Pull environment variables to the path"
+msgstr ""
+
+#: internal/parser/config.go:183
+msgid "Push environment variables from the path"
+msgstr ""
+
+#: internal/parser/limits.go:149
+msgid "Query plans that meet the specified memory capacity, unit GiB"
+msgstr ""
+
+#: internal/parser/limits.go:148
+msgid "Query plans that meet the specified number of cpu cores"
+msgstr ""
+
+#: internal/parser/limits.go:150
+msgid "Query plans that meet the specified spec id, see [specs] subcommand."
+msgstr ""
+
+#: internal/parser/routes.go:247
+msgid "Remove a route from an application"
+msgstr ""
+
+#: internal/parser/tokens.go:94
+msgid "Remove a token for controller authentication"
+msgstr ""
+
+#: internal/parser/workspaces.go:157
+msgid "Remove a user from a workspace"
+msgstr "Remove a user from a workspace"
+
+#: internal/parser/keys.go:81
+msgid "Remove an SSH key"
+msgstr ""
+
+#: internal/parser/certs.go:88
+msgid "Remove an SSL certificate from an app"
+msgstr ""
+
+#: internal/parser/labels.go:90
+msgid "Remove application's label"
+msgstr ""
+
+#: internal/parser/gateways.go:91
+msgid "Remove gateways from an application"
+msgstr ""
+
+#: internal/parser/services.go:103
+msgid "Remove service from an application"
+msgstr ""
+
+#: internal/parser/keys.go:82
+msgid "Removes an SSH key for the logged in user"
+msgstr ""
+
+#: internal/parser/git.go:52
+msgid "Removes git remote of application from repository"
+msgstr ""
+
+#: internal/parser/pts.go:91
+msgid "Restart an application or process types"
+msgstr ""
+
+#: internal/parser/pts.go:90
+msgid "Restart application process types"
+msgstr ""
+
+#: internal/parser/apps.go:147
+msgid "Retention time of records in seconds"
+msgstr ""
+
+#: internal/parser/releases.go:136
+msgid "Return to a previous release"
+msgstr ""
+
+#: internal/parser/releases.go:137
+msgid "Rolls back to a previous application release"
+msgstr ""
+
+#: internal/parser/routes.go:158
+msgid "Rules-file is the file name of rule to apply"
+msgstr ""
+
+#: internal/parser/apps.go:136
+msgid "Run a command in an ephemeral app container"
+msgstr ""
+
+#: internal/parser/apps.go:137
+msgid "Runs a command inside an ephemeral app container"
+msgstr ""
+
+#: internal/parser/builds.go:116
+msgid "Save process info to the local"
+msgstr ""
+
+#: internal/parser/pts.go:124
+msgid "Scale process types of replicas"
+msgstr ""
+
+#: internal/parser/pts.go:125
+msgid "Scales an application's processes by type"
+msgstr ""
+
+#: internal/parser/limits.go:126
+msgid ""
+"Search keywords separated by commas, matching must satisfy all of the "
+"specified."
+msgstr ""
+
+#: internal/parser/volumes.go:94
+msgid "Secret access key for authentication"
+msgstr ""
+
+#: internal/parser/config.go:225
+msgid "Selects environment groups to attach an app ptype"
+msgstr ""
+
+#: internal/parser/config.go:250
+msgid "Selects environment groups to detach an app ptype"
+msgstr ""
+
+#: internal/parser/volumes.go:209
+msgid "Serve a WebDAV service for the volume"
+msgstr ""
+
+#: internal/parser/autoscale.go:64
+msgid "Set autoscale option per process type for an app"
+msgstr ""
+
+#: internal/parser/config.go:85
+msgid "Set environment variables for an app"
+msgstr ""
+
+#: internal/parser/healthchecks.go:90
+msgid "Set healthchecks for an application"
+msgstr ""
+
+#: internal/parser/lifecycles.go:84
+msgid "Set lifecycles for an application"
+msgstr "Set lifecycles for an application"
+
+#: internal/parser/registry.go:76
+msgid "Set registry info for an app"
+msgstr ""
+
+#: internal/parser/limits.go:70
+msgid "Set resource limits for an app"
+msgstr ""
+
+#: internal/parser/timeouts.go:64
+msgid "Set resource timeouts for an app"
+msgstr ""
+
+#: internal/parser/routes.go:149
+msgid "Set route rules"
+msgstr ""
+
+#: internal/parser/routes.go:150
+msgid "Set route rules for an application"
+msgstr ""
+
+#: internal/parser/tags.go:86
+msgid "Set tags for an application"
+msgstr ""
+
+#: internal/parser/config.go:86
+msgid "Sets environment variables for an application or config group"
+msgstr ""
+
+#: internal/parser/config.go:184
+msgid ""
+"Sets environment variables for an application or config group.\n"
+"\n"
+"This file can be read by foreman\n"
+"to load the local environment for your app. The file should be piped via\n"
+"stdin, 'drycc config push < .env', or using the --path option."
+msgstr ""
+"Sets environment variables for an application or config group.\n"
+"\n"
+"This file can be read by foreman\n"
+"to load the local environment for your app. The file should be piped via\n"
+"stdin, 'drycc config push < .env', or using the --path option."
+
+#: internal/parser/healthchecks.go:91
+msgid ""
+"Sets healthchecks for an application.\n"
+"\n"
+"By default, Workflow only checks that the application starts in their "
+"Container. A health\n"
+"check may be added by configuring a health check probe for the application. "
+"The health\n"
+"checks are implemented as Kubernetes Container Probes. A 'startupProbe' "
+"'livenessProbe' \n"
+"and a 'readinessProbe' can be configured, and each probe can be of type "
+"'httpGet', 'exec' \n"
+"or 'tcpSocket' depending on the type of probe the Container requires.\n"
+"\n"
+"A 'startupProbe' indicates whether the application within the container is "
+"started.\n"
+"All other probes are disabled if a startup probe is provided, until it "
+"succeeds.\n"
+"If the startup probe fails, the container is subjected to its restart "
+"policy.\n"
+"\n"
+"A 'livenessProbe' is useful for applications running for long periods of "
+"time, eventually\n"
+"transitioning to broken states and cannot recover except by restarting "
+"them.\n"
+"\n"
+"Other times, a 'readinessProbe' is useful when the Container is only "
+"temporarily unable\n"
+"to serve, and will recover on its own. In this case, if a Container fails "
+"its 'readinessProbe'\n"
+", the Container will not be shut down, but rather the Container will stop "
+"receiving\n"
+"incoming requests.\n"
+"\n"
+"'httpGet' probes are just as it sounds: it performs a HTTP GET operation on "
+"the Container.\n"
+"A response code inside the 200-399 range is considered a pass. 'httpGet' "
+"probes accept a\n"
+"port number to perform the HTTP GET operation on the Container.\n"
+"\n"
+"'exec' probes run a command inside the Container to determine its health. An "
+"exit code of\n"
+"zero is considered a pass, while a non-zero status code is considered a "
+"fail. 'exec'\n"
+"probes accept a string of arguments to be run inside the Container.\n"
+"\n"
+"'tcpSocket' probes attempt to open a socket in the Container. The Container "
+"is only\n"
+"considered healthy if the check can establish a connection. 'tcpSocket' "
+"probes accept a\n"
+"port number to perform the socket connection on the Container.\n"
+msgstr ""
+
+#: internal/parser/labels.go:61
+msgid ""
+"Sets labels for an application.\n"
+"\n"
+"A label is a key/value pair used to label an application. This label is a "
+"general information for drycc user.\n"
+"Mostly used for administration/maintenance information, note for "
+"application. This information isn't send to scheduler."
+msgstr ""
+
+#: internal/parser/lifecycles.go:85
+msgid ""
+"Sets lifecycle handlers for an application.\n"
+"\n"
+"Lifecycle handlers allow you to run actions at specific points in a "
+"container's lifecycle.\n"
+"Two types of lifecycle handlers are supported: 'postStart' and 'preStop'.\n"
+"\n"
+"A 'postStart' handler runs immediately after a container is created. If the "
+"handler fails,\n"
+"the container is terminated and restarted according to its restart policy.\n"
+"\n"
+"A 'preStop' handler runs immediately before a container is terminated. This "
+"handler must\n"
+"complete before the container termination signal is sent. If the handler "
+"fails, the\n"
+"container enters a termination grace period and is then forcefully "
+"terminated.\n"
+"\n"
+"Each lifecycle handler can be configured with one of the following action "
+"types:\n"
+"\n"
+"'httpGet': Performs an HTTP GET request to the container. The action is "
+"considered\n"
+"successful if the response status code is in the 200-399 range. 'httpGet' "
+"actions require\n"
+"a port number to specify where to send the HTTP request.\n"
+"\n"
+"'exec': Executes a command inside the container. The action is considered "
+"successful if\n"
+"the command exits with status code 0. 'exec' actions accept a list of "
+"command arguments\n"
+"to run inside the container.\n"
+"\n"
+"'tcpSocket': Attempts to open a TCP socket connection to the container. The "
+"action is\n"
+"considered successful if a connection can be established. 'tcpSocket' "
+"actions require a\n"
+"port number to specify where to attempt the connection.\n"
+"\n"
+"'sleep': Waits for a specified duration before proceeding. 'sleep' actions "
+"accept a\n"
+"duration in seconds to wait.\n"
+msgstr ""
+"Sets lifecycle handlers for an application.\n"
+"\n"
+"Lifecycle handlers allow you to run actions at specific points in a "
+"container's lifecycle.\n"
+"Two types of lifecycle handlers are supported: 'postStart' and 'preStop'.\n"
+"\n"
+"A 'postStart' handler runs immediately after a container is created. If the "
+"handler fails,\n"
+"the container is terminated and restarted according to its restart policy.\n"
+"\n"
+"A 'preStop' handler runs immediately before a container is terminated. This "
+"handler must\n"
+"complete before the container termination signal is sent. If the handler "
+"fails, the\n"
+"container enters a termination grace period and is then forcefully "
+"terminated.\n"
+"\n"
+"Each lifecycle handler can be configured with one of the following action "
+"types:\n"
+"\n"
+"'httpGet': Performs an HTTP GET request to the container. The action is "
+"considered\n"
+"successful if the response status code is in the 200-399 range. 'httpGet' "
+"actions require\n"
+"a port number to specify where to send the HTTP request.\n"
+"\n"
+"'exec': Executes a command inside the container. The action is considered "
+"successful if\n"
+"the command exits with status code 0. 'exec' actions accept a list of "
+"command arguments\n"
+"to run inside the container.\n"
+"\n"
+"'tcpSocket': Attempts to open a TCP socket connection to the container. The "
+"action is\n"
+"considered successful if a connection can be established. 'tcpSocket' "
+"actions require a\n"
+"port number to specify where to attempt the connection.\n"
+"\n"
+"'sleep': Waits for a specified duration before proceeding. 'sleep' actions "
+"accept a\n"
+"duration in seconds to wait.\n"
+
+#: internal/parser/registry.go:77
+msgid ""
+"Sets registry information for an application. These credentials are the same "
+"as those used for\n"
+"'podmain login' to the private registry."
+msgstr ""
+
+#: internal/parser/limits.go:71
+msgid ""
+"Sets resource limits for an application.\n"
+"\n"
+"A resource limit is a finite resource within a pod which we can apply\n"
+"restrictions through Kubernetes."
+msgstr ""
+
+#: internal/parser/tags.go:87
+msgid ""
+"Sets tags for an application.\n"
+"\n"
+"A tag is a key/value pair used to tag an application's containers and is "
+"passed to the\n"
+"scheduler. This is often used to restrict workloads to specific hosts "
+"matching the\n"
+"scheduler-configured metadata."
+msgstr ""
+
+#: internal/parser/timeouts.go:65
+msgid "Sets termination grace period for an application"
+msgstr ""
+
+#: cmd/root.go:74
+msgid "Shortcut Commands:"
+msgstr "Shortcut Commands:"
+
+#: internal/parser/certs.go:41
+msgid "Show certificate information for an SSL application"
+msgstr ""
+
+#: internal/parser/resources.go:156
+msgid "Show extra details provided to resource"
+msgstr ""
+
+#: internal/parser/update.go:21
+msgid "Simulate an update, only print the version info."
+msgstr ""
+
+#: internal/parser/apps.go:171
+msgid ""
+"Skips the prompt for the application name. \n"
+"<app> is the uniquely identifiable name for the application"
+msgstr ""
+
+#: internal/parser/workspaces.go:119
+msgid "Skips the prompt for the workspace name"
+msgstr "Skips the prompt for the workspace name"
+
+#: internal/parser/ps.go:76
+msgid "Specify if the logs should be streamed"
+msgstr ""
+
+#: internal/parser/resources.go:112 internal/parser/resources.go:196
+msgid "Specify values in a YAML file. If set, params will be discard"
+msgstr ""
+
+#: internal/parser/ps.go:113
+msgid "Stdin is a TTY"
+msgstr ""
+
+#: internal/parser/autoscale.go:74
+msgid "Target CPU utilization percentage"
+msgstr ""
+
+#: cmd/root.go:29
+msgid "The Drycc command-line client issues API calls to a Drycc controller"
+msgstr "Drycc命令行客户端向Drycc控制器发出API调用"
+
+#: internal/parser/lifecycles.go:185
+msgid "The HTTP headers to send for 'httpGet' actions, separated by commas"
+msgstr "The HTTP headers to send for 'httpGet' actions, separated by commas"
+
+#: internal/parser/healthchecks.go:184
+msgid "The HTTP headers to send for 'httpGet' probes, separated by commas"
+msgstr ""
+
+#: internal/parser/services.go:65 internal/parser/services.go:116
+msgid "The IP protocol for this port. Supports TCP, UDP, and SCTP"
+msgstr ""
+
+#: internal/parser/healthchecks.go:86
+msgid ""
+"The arguments required for the healthcheck probe. 'exec', accepts a list of "
+"arguments;\n"
+"                  'httpGet' and 'tcpSocket' accept a port number."
+msgstr ""
+
+#: internal/parser/lifecycles.go:80
+msgid ""
+"The arguments required for the lifecycle action. 'exec' accepts a list of "
+"arguments;\n"
+"                  'sleep' accepts duration in seconds; 'httpGet' and "
+"'tcpSocket' accept a port number."
+msgstr ""
+"The arguments required for the lifecycle action. 'exec' accepts a list of "
+"arguments;\n"
+"                  'sleep' accepts duration in seconds; 'httpGet' and "
+"'tcpSocket' accept a port number."
+
+#: internal/parser/workspaces.go:70
+msgid "The contact email for the workspace"
+msgstr "The contact email for the workspace"
+
+#: internal/parser/workspaces.go:132
+msgid "The email address of the user to invite"
+msgstr "The email address of the user to invite"
+
+#: internal/parser/shortcuts.go:53 internal/parser/volumes.go:235
+#: internal/parser/apps.go:132
+msgid "The filesystem path"
+msgstr ""
+
+#: internal/parser/config.go:34 internal/parser/config.go:64
+msgid "The group for which the config needs to be listed"
+msgstr ""
+
+#: internal/parser/config.go:157
+msgid "The group for which the config needs to be pull"
+msgstr ""
+
+#: internal/parser/config.go:195
+msgid "The group for which the config needs to be push"
+msgstr ""
+
+#: internal/parser/config.go:93
+msgid "The group for which the config needs to be set"
+msgstr ""
+
+#: internal/parser/config.go:122
+msgid "The group for which the config needs to be unset"
+msgstr ""
+
+#: internal/parser/config.go:220
+msgid "The group that requires attach configurations"
+msgstr ""
+
+#: internal/parser/config.go:247
+msgid "The group that requires detach configurations"
+msgstr "The group that requires detach configurations"
+
+#: internal/parser/healthchecks.go:84
+msgid ""
+"The healthcheck probe, such as 'startupProbe' 'livenessProbe' or "
+"'readinessProbe'"
+msgstr ""
+"The healthcheck probe, such as 'startupProbe' 'livenessProbe' or "
+"'readinessProbe'"
+
+#: internal/parser/volumes.go:88
+msgid "The hostname or ip address of the nfs server"
+msgstr ""
+
+#: internal/parser/tokens.go:91
+msgid "The id of the token for controller authentication"
+msgstr ""
+
+#: internal/parser/healthchecks.go:185
+msgid "The initial delay timeout for the probe"
+msgstr ""
+
+#: internal/parser/lifecycles.go:78
+msgid "The lifecycle handler, such as 'postStart' 'preStop'"
+msgstr "The lifecycle handler, such as 'postStart' 'preStop'"
+
+#: internal/parser/limits.go:67
+msgid "The limit plan id to apply to the process type"
+msgstr ""
+
+#: internal/parser/certs.go:23 internal/parser/certs.go:48
+#: internal/parser/domains.go:22 internal/parser/domains.go:43
+#: internal/parser/gateways.go:22 internal/parser/gateways.go:44
+#: internal/parser/limits.go:127 internal/parser/limits.go:151
+#: internal/parser/releases.go:33 internal/parser/releases.go:62
+#: internal/parser/resources.go:25 internal/parser/resources.go:127
+#: internal/parser/routes.go:28 internal/parser/routes.go:107
+#: internal/parser/tokens.go:22 internal/parser/tokens.go:40
+#: internal/parser/volumes.go:23 internal/parser/volumes.go:145
+#: internal/parser/workspaces.go:24 internal/parser/workspaces.go:46
+#: internal/parser/workspaces.go:93
+msgid "The maximum number of results to display"
+msgstr ""
+
+#: internal/parser/certs.go:106
+msgid "The name of the cert to get information from"
+msgstr ""
+
+#: internal/parser/certs.go:84
+msgid "The name of the cert to remove from the app"
+msgstr ""
+
+#: internal/parser/workspaces.go:61 internal/parser/workspaces.go:82
+#: internal/parser/workspaces.go:109 internal/parser/workspaces.go:131
+#: internal/parser/workspaces.go:153 internal/parser/workspaces.go:181
+msgid "The name of the workspace"
+msgstr "The name of the workspace"
+
+#: internal/parser/ps.go:75
+msgid "The number of lines to display, -1 showing all log lines"
+msgstr ""
+
+#: internal/parser/pts.go:121 internal/parser/shortcuts.go:95
+msgid "The number of processes"
+msgstr ""
+
+#: internal/parser/healthchecks.go:186
+msgid "The number of seconds after which the probe times out"
+msgstr ""
+
+#: internal/parser/registry.go:73
+msgid "The password of the registry"
+msgstr ""
+
+#: internal/parser/ps.go:99 internal/parser/ps.go:130 internal/parser/ps.go:152
+#: internal/parser/shortcuts.go:66
+msgid "The pod name for the application"
+msgstr ""
+
+#: internal/parser/services.go:100
+msgid "The port exposed by this service"
+msgstr "The port exposed by this service"
+
+#: internal/parser/services.go:51
+msgid "The port that will be exposed by this service"
+msgstr ""
+
+#: internal/parser/certs.go:61
+msgid "The private key of the SSL certificate"
+msgstr ""
+
+#: internal/parser/pts.go:62 internal/parser/pts.go:120
+#: internal/parser/pts.go:157 internal/parser/shortcuts.go:94
+#: internal/parser/tags.go:26 internal/parser/tags.go:60
+#: internal/parser/tags.go:100 internal/parser/tags.go:131
+#: internal/parser/volumes.go:234 internal/parser/volumes.go:259
+msgid "The process name as defined in your Procfile"
+msgstr ""
+
+#: internal/parser/releases.go:132
+msgid "The process name as defined in your Procfile, such as 'web'"
+msgstr ""
+
+#: internal/parser/timeouts.go:60 internal/parser/timeouts.go:86
+msgid "The process type as defined in your Procfile"
+msgstr ""
+
+#: internal/parser/limits.go:66 internal/parser/limits.go:96
+msgid "The process type as defined in your Procfile, such as 'web' or 'worker'"
+msgstr ""
+
+#: internal/parser/autoscale.go:59
+msgid "The process type to add to the application's autoscale settings"
+msgstr ""
+
+#: internal/parser/autoscale.go:96
+msgid "The process type to remove from the application's autoscale settings"
+msgstr ""
+
+#: internal/parser/domains.go:62
+msgid "The ptype for domain"
+msgstr ""
+
+#: internal/parser/registry.go:27 internal/parser/registry.go:51
+#: internal/parser/registry.go:86 internal/parser/registry.go:108
+msgid "The ptype for registry"
+msgstr ""
+
+#: internal/parser/config.go:33 internal/parser/config.go:63
+msgid "The ptype for which the config needs to be listed"
+msgstr ""
+
+#: internal/parser/config.go:155
+msgid "The ptype for which the config needs to be pull"
+msgstr ""
+
+#: internal/parser/config.go:194
+msgid "The ptype for which the config needs to be push"
+msgstr ""
+
+#: internal/parser/config.go:92
+msgid "The ptype for which the config needs to be set"
+msgstr ""
+
+#: internal/parser/config.go:121
+msgid "The ptype for which the config needs to be unset"
+msgstr ""
+
+#: internal/parser/healthchecks.go:178
+msgid "The ptype for which the health check needs to be applied"
+msgstr "The ptype for which the health check needs to be applied"
+
+#: internal/parser/healthchecks.go:31
+msgid "The ptype for which the health check needs to be listed"
+msgstr "The ptype for which the health check needs to be listed"
+
+#: internal/parser/healthchecks.go:224
+msgid "The ptype for which the health check needs to be removed"
+msgstr ""
+
+#: internal/parser/lifecycles.go:179
+msgid "The ptype for which the lifecycle handler needs to be applied"
+msgstr "The ptype for which the lifecycle handler needs to be applied"
+
+#: internal/parser/lifecycles.go:221
+msgid "The ptype for which the lifecycle handler needs to be unset"
+msgstr "The ptype for which the lifecycle handler needs to be unset"
+
+#: internal/parser/lifecycles.go:31
+msgid "The ptype for which the lifecycle needs to be listed"
+msgstr "The ptype for which the lifecycle needs to be listed"
+
+#: internal/parser/config.go:219
+msgid "The ptype that requires attach configurations"
+msgstr ""
+
+#: internal/parser/config.go:246
+msgid "The ptype that requires detach configurations"
+msgstr ""
+
+#: internal/parser/certs.go:60
+msgid "The public key of the SSL certificate"
+msgstr ""
+
+#: internal/parser/lifecycles.go:184
+msgid "The relative URL path for 'httpGet' actions"
+msgstr "The relative URL path for 'httpGet' actions"
+
+#: internal/parser/healthchecks.go:183
+msgid "The relative URL path for 'httpGet' probes"
+msgstr ""
+
+#: internal/parser/releases.go:76 internal/parser/releases.go:133
+msgid "The release of the application, such as '1'"
+msgstr ""
+
+#: internal/parser/resources.go:214 internal/parser/resources.go:239
+#: internal/parser/resources.go:261
+msgid "The resource instance alias name"
+msgstr ""
+
+#: internal/parser/resources.go:94 internal/parser/resources.go:177
+msgid "The resource instance parameters key"
+msgstr ""
+
+#: internal/parser/resources.go:95 internal/parser/resources.go:178
+msgid "The resource instance parameters value"
+msgstr ""
+
+#: internal/parser/resources.go:92 internal/parser/resources.go:175
+msgid "The resource's service"
+msgstr ""
+
+#: internal/parser/workspaces.go:200
+msgid "The role to assign: admin, member, or viewer"
+msgstr "The role to assign: admin, member, or viewer"
+
+#: internal/parser/routes.go:52
+msgid "The route kind, range: HTTPRoute,TCPRoute,UDPRoute,GRPCRoute,TLSRoute"
+msgstr ""
+
+#: internal/parser/routes.go:53
+msgid "The route's backend, pattern: <service>:<port>,<weight>"
+msgstr ""
+
+#: internal/parser/resources.go:63
+msgid "The service name for plans"
+msgstr ""
+
+#: internal/parser/resources.go:93 internal/parser/resources.go:176
+msgid "The service's plan"
+msgstr ""
+
+#: internal/parser/shortcuts.go:54 internal/parser/apps.go:133
+msgid "The shell command to run inside the container"
+msgstr ""
+
+#: internal/parser/builds.go:65
+msgid "The stack name for the application"
+msgstr ""
+
+#: internal/parser/lifecycles.go:186
+msgid "The stop signal to send to the container"
+msgstr "The stop signal to send to the container"
+
+#: internal/parser/tags.go:82
+msgid "The tag key"
+msgstr ""
+
+#: internal/parser/tags.go:119
+msgid "The tag key to unset"
+msgstr ""
+
+#: internal/parser/tags.go:83
+msgid "The tag value"
+msgstr ""
+
+#: internal/parser/routes.go:187 internal/parser/routes.go:223
+msgid "The unique name of the gaetway"
+msgstr ""
+
+#: internal/parser/routes.go:51 internal/parser/routes.go:120
+#: internal/parser/routes.go:146 internal/parser/routes.go:176
+#: internal/parser/routes.go:212 internal/parser/routes.go:244
+msgid "The unique name of the route"
+msgstr ""
+
+#: internal/parser/autodeploy.go:20 internal/parser/autorollback.go:19
+#: internal/parser/autoscale.go:21 internal/parser/builds.go:21
+#: internal/parser/builds.go:64 internal/parser/certs.go:22
+#: internal/parser/config.go:29 internal/parser/domains.go:21
+#: internal/parser/gateways.go:21 internal/parser/git.go:18
+#: internal/parser/labels.go:21 internal/parser/limits.go:21
+#: internal/parser/ps.go:21 internal/parser/ps.go:112 internal/parser/pts.go:21
+#: internal/parser/pts.go:134 internal/parser/registry.go:26
+#: internal/parser/releases.go:32 internal/parser/resources.go:24
+#: internal/parser/routes.go:27 internal/parser/routing.go:20
+#: internal/parser/services.go:23 internal/parser/tags.go:25
+#: internal/parser/timeouts.go:21 internal/parser/volumes.go:22
+#: internal/parser/apps.go:91 internal/parser/apps.go:109
+#: internal/parser/apps.go:144 internal/parser/apps.go:170
+#: internal/parser/apps.go:193
+msgid "The uniquely identifiable name for the application"
+msgstr ""
+
+#: internal/parser/healthchecks.go:30 internal/parser/lifecycles.go:30
+msgid "The uniquely identifiable name of the application"
+msgstr ""
+
+#: internal/parser/workspaces.go:182
+msgid "The username of the member to update"
+msgstr "The username of the member to update"
+
+#: internal/parser/registry.go:72
+msgid "The username of the registry"
+msgstr ""
+
+#: internal/parser/workspaces.go:154
+msgid "The username to remove from the workspace"
+msgstr "The username to remove from the workspace"
+
+#: internal/parser/timeouts.go:61
+msgid "The value to apply to the process type in seconds"
+msgstr ""
+
+#: internal/parser/builds.go:22 internal/parser/builds.go:87
+#: internal/parser/builds.go:112
+msgid "The version for which the build info needs to be displayed"
+msgstr ""
+
+#: internal/parser/config.go:35 internal/parser/config.go:65
+msgid "The version for which the config needs to be listed"
+msgstr ""
+
+#: internal/parser/config.go:156
+msgid "The version for which the config needs to be pull"
+msgstr ""
+
+#: internal/parser/healthchecks.go:32 internal/parser/healthchecks.go:56
+msgid "The version for which the health check needs to be listed"
+msgstr ""
+
+#: internal/parser/lifecycles.go:32 internal/parser/lifecycles.go:56
+msgid "The version for which the lifecycle needs to be listed"
+msgstr "The version for which the lifecycle needs to be listed"
+
+#: internal/parser/limits.go:22 internal/parser/limits.go:47
+msgid "The version for which the limit needs to be listed"
+msgstr ""
+
+#: internal/parser/registry.go:28 internal/parser/registry.go:52
+msgid "The version for which the registry needs to be listed"
+msgstr ""
+
+#: internal/parser/tags.go:27 internal/parser/tags.go:61
+msgid "The version for which the tag needs to be listed"
+msgstr ""
+
+#: internal/parser/timeouts.go:22 internal/parser/timeouts.go:44
+msgid "The version for which the timeout needs to be listed"
+msgstr ""
+
+#: internal/parser/shortcuts.go:52 internal/parser/volumes.go:58
+#: internal/parser/volumes.go:118 internal/parser/volumes.go:213
+#: internal/parser/volumes.go:233 internal/parser/volumes.go:258
+#: internal/parser/apps.go:131
+msgid "The volume name"
+msgstr ""
+
+#: internal/parser/volumes.go:161
+msgid "The volume name to be info"
+msgstr ""
+
+#: internal/parser/volumes.go:187
+msgid "The volume name to be removed"
+msgstr ""
+
+#: internal/parser/volumes.go:59 internal/parser/volumes.go:119
+msgid "The volume size, such as '500G'"
+msgstr ""
+
+#: internal/parser/volumes.go:87
+msgid "The volume type, such as csi, nfs, oss"
+msgstr ""
+
+#: internal/parser/apps.go:56
+msgid "The workspace to create the app in"
+msgstr "The workspace to create the app in"
+
+#: internal/parser/resources.go:91 internal/parser/resources.go:145
+#: internal/parser/resources.go:174
+msgid "This resource instance alias"
+msgstr ""
+
+#: internal/parser/builds.go:68 internal/parser/builds.go:115
+#: internal/parser/pts.go:99 internal/parser/releases.go:117
+#: internal/parser/tokens.go:103
+msgid "To proceed, type \"yes\""
+msgstr ""
+
+#: internal/parser/config.go:94 internal/parser/config.go:123
+#: internal/parser/config.go:198
+msgid "To proceed, type 'yes'"
+msgstr ""
+
+#: internal/parser/apps.go:184
+msgid "Transfer app to another workspace"
+msgstr "Transfer app to another workspace"
+
+#: internal/parser/apps.go:185
+msgid "Transfer application to another workspace."
+msgstr "Transfer application to another workspace."
+
+#: internal/parser/autoscale.go:100
+msgid "Turn off autoscale for an app"
+msgstr ""
+
+#: internal/parser/autoscale.go:63
+msgid "Turn on autoscale for an app"
+msgstr ""
+
+#: internal/parser/domains.go:75
+msgid "Unbinds a domain for an application"
+msgstr ""
+
+#: internal/parser/volumes.go:262
+msgid "Unmount a volume from process of the application"
+msgstr ""
+
+#: internal/parser/autoscale.go:101
+msgid "Unset autoscale per process type for an app"
+msgstr ""
+
+#: internal/parser/config.go:114
+msgid "Unset environment variables for an app"
+msgstr ""
+
+#: internal/parser/healthchecks.go:212
+msgid "Unset healthchecks for an application"
+msgstr ""
+
+#: internal/parser/lifecycles.go:209
+msgid "Unset lifecycle handler for an application"
+msgstr "Unset lifecycle handler for an application"
+
+#: internal/parser/registry.go:101
+msgid "Unset registry info for an app"
+msgstr ""
+
+#: internal/parser/limits.go:99
+msgid "Unset resource limits for an app"
+msgstr ""
+
+#: internal/parser/timeouts.go:89
+msgid "Unset resource timeouts for an app"
+msgstr ""
+
+#: internal/parser/tags.go:122
+msgid "Unset tags for an application"
+msgstr ""
+
+#: internal/parser/config.go:115
+msgid "Unsets an environment variable for an application or config group"
+msgstr ""
+
+#: internal/parser/labels.go:91
+msgid "Unsets labels for an application"
+msgstr ""
+
+#: internal/parser/registry.go:102
+msgid "Unsets registry information for an application"
+msgstr ""
+
+#: internal/parser/timeouts.go:90
+msgid ""
+"Unsets timeouts for an application. Default value (30s) or set by drycc "
+"controller"
+msgstr ""
+
+#: internal/parser/workspaces.go:185
+msgid "Update a member's role and/or alerts in a workspace"
+msgstr "Update a member's role and/or alerts in a workspace"
+
+#: internal/parser/resources.go:181
+msgid "Update a resource from the application"
+msgstr ""
+
+#: internal/parser/update.go:15 internal/parser/update.go:16
+msgid "Update workflow cli to latest release"
+msgstr ""
+
+#: internal/parser/tls.go:39
+msgid "View TLS settings information"
+msgstr ""
+
+#: internal/parser/routing.go:34
+msgid "View application's routability information"
+msgstr ""
+
+#: internal/parser/autorollback.go:33
+msgid "View autorollback info for an application"
+msgstr ""
+
+#: internal/parser/apps.go:84
+msgid "View info about an application"
+msgstr ""
+
+#: internal/parser/apps.go:145
+msgid "Volume mounts in format 'volume:path'"
+msgstr ""
+
+#: internal/parser/builds.go:49
+msgid "default container image"
+msgstr ""
+
+#: internal/parser/builds.go:52
+msgid "imports an image and deploys as a new release"
+msgstr ""
+
+#: internal/parser/version.go:22
+msgid "list api and controller versions."
+msgstr ""
+
+#: internal/parser/autoscale.go:72
+msgid "minimum replicas to keep around"
+msgstr ""
+
+#: internal/parser/services.go:99
+msgid "procfile type which should handle the request, e.g. webhooks"
+msgstr "procfile type which should handle the request, e.g. webhooks"
+
+#: internal/parser/services.go:49
+msgid ""
+"procfile type which should handle the request, e.g. webhooks (should be bind "
+"to the port PORT)\n"
+"             only single extra service per Porcfile type could be created."
+msgstr ""
+
+#: internal/parser/resources.go:225
+msgid ""
+"skips the prompt for the resource name. <resource> is the uniquely "
+"identifiable\n"
+"name for the resource"
+msgstr ""
+
+#: internal/parser/healthchecks.go:85
+msgid "the healthcheck probe type, such as 'httpGet', 'exec' or 'tcpSocket'"
+msgstr ""
+
+#: internal/parser/healthchecks.go:209
+msgid ""
+"the healthcheck probe, such as 'startupProbe' 'livenessProbe' or "
+"'readinessProbe'"
+msgstr ""
+"the healthcheck probe, such as 'startupProbe' 'livenessProbe' or "
+"'readinessProbe'"
+
+#: internal/parser/labels.go:85
+msgid "the label key to unset, for example: \"git_repo\" or \"team\""
+msgstr ""
+
+#: internal/parser/labels.go:56
+msgid "the label key, for example: \"git_repo\" or \"team\""
+msgstr ""
+
+#: internal/parser/labels.go:57
+msgid ""
+"the label value, for example: \"https://github.com/drycc/workflow\" or "
+"\"frontend\""
+msgstr ""
+
+#: internal/parser/lifecycles.go:79
+msgid ""
+"the lifecycle action type, such as 'httpGet', 'exec', 'sleep' or 'tcpSocket'"
+msgstr ""
+"the lifecycle action type, such as 'httpGet', 'exec', 'sleep' or 'tcpSocket'"
+
+#: internal/parser/lifecycles.go:206
+msgid "the lifecycle handler type, such as 'postStart' or 'preStop'"
+msgstr "the lifecycle handler type, such as 'postStart' or 'preStop'"
+
+#: internal/parser/resources.go:264
+msgid "unbind a resources for an application"
+msgstr ""
diff --git a/pkg/i18n/translations/drycc/template.pot b/pkg/i18n/translations/drycc/template.pot
new file mode 100644
index 000000000..0d644f033
--- /dev/null
+++ b/pkg/i18n/translations/drycc/template.pot
@@ -0,0 +1,1811 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"Report-Msgid-Bugs-To: EMAIL\n"
+"POT-Creation-Date: 2026-03-26 14:27+0800\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: internal/parser/builds.go:66 internal/parser/builds.go:113
+msgid "A YAML file used to supply a Procfile to the application"
+msgstr ""
+
+#: internal/parser/keys.go:53
+msgid "A local file path to an SSH public key used to push application code"
+msgstr ""
+
+#: internal/parser/config.go:158 internal/parser/config.go:196
+msgid "A path leading to an environment file"
+msgstr ""
+
+#: internal/parser/tls.go:123
+msgid "ACME account email"
+msgstr ""
+
+#: internal/parser/tls.go:124
+msgid "ACME server URL"
+msgstr ""
+
+#: internal/parser/volumes.go:93
+msgid "Access key id for authentication"
+msgstr ""
+
+#: internal/parser/tls.go:55 internal/parser/tls.go:84
+msgid "Action to perform, 'enable' or 'disable'"
+msgstr ""
+
+#: internal/parser/tokens.go:58
+msgid "Add a token for controller authentication"
+msgstr ""
+
+#: internal/parser/keys.go:57
+msgid "Add an SSH key"
+msgstr ""
+
+#: internal/parser/certs.go:65
+msgid "Add an SSL certificate to an app"
+msgstr ""
+
+#: internal/parser/labels.go:60
+msgid "Add new application's label"
+msgstr ""
+
+#: internal/parser/keys.go:58
+msgid "Adds SSH keys for the logged in user"
+msgstr ""
+
+#: internal/parser/git.go:36
+msgid "Adds git remote of application to repository"
+msgstr ""
+
+#: internal/parser/config.go:160
+msgid "Allows you to have the pull overwrite keys to the path"
+msgstr ""
+
+#: internal/parser/config.go:57
+msgid "An app config info"
+msgstr ""
+
+#: internal/parser/tls.go:23
+msgid "Application name"
+msgstr ""
+
+#: internal/parser/certs.go:135
+msgid "Attach a certificate to a domain"
+msgstr ""
+
+#: internal/parser/certs.go:134
+msgid "Attach an SSL certificate to a domain"
+msgstr ""
+
+#: internal/parser/routes.go:179
+msgid "Attach to gateway"
+msgstr ""
+
+#: internal/parser/auth.go:34
+msgid "Authenticate against a controller"
+msgstr ""
+
+#: internal/parser/tls.go:89
+msgid "Automatic certificate management"
+msgstr ""
+
+#: internal/parser/domains.go:55
+msgid "Bind a domain to an application"
+msgstr ""
+
+#: internal/parser/resources.go:242
+msgid "Bind a resource for an application"
+msgstr ""
+
+#: internal/parser/volumes.go:92
+msgid "Bucket name in object storage"
+msgstr ""
+
+#: internal/parser/tls.go:125
+msgid "CA key ID"
+msgstr ""
+
+#: internal/parser/tls.go:126
+msgid "CA key secret"
+msgstr ""
+
+#: internal/parser/pts.go:160
+msgid "Clean process types of not used"
+msgstr ""
+
+#: internal/parser/auth.go:53
+msgid "Clear the current user session"
+msgstr ""
+
+#: internal/parser/apps.go:146
+msgid "Command execution timeout in seconds"
+msgstr ""
+
+#: internal/parser/certs.go:130
+msgid ""
+"Common name of the domain to attach to (needs to already be in the system)"
+msgstr ""
+
+#: internal/parser/certs.go:155
+msgid "Common name of the domain to detach from"
+msgstr ""
+
+#: internal/parser/tls.go:117
+msgid "Configure automatic certificate management issuer details"
+msgstr ""
+
+#: internal/parser/apps.go:45
+msgid "Create a new application"
+msgstr ""
+
+#: internal/parser/resources.go:98
+msgid "Create a resource for the application"
+msgstr ""
+
+#: internal/parser/routes.go:56
+msgid "Create a route for an application"
+msgstr ""
+
+#: internal/parser/volumes.go:62
+msgid "Create a volume for the application"
+msgstr ""
+
+#: internal/parser/workspaces.go:64
+msgid "Create a workspace"
+msgstr ""
+
+#: internal/parser/gateways.go:59
+msgid "Create gateways for an application"
+msgstr ""
+
+#: internal/parser/services.go:55
+msgid "Create service for an application"
+msgstr ""
+
+#: internal/parser/apps.go:46
+msgid ""
+"Creates a new application. If no <id> is provided, one will be generated "
+"automatically"
+msgstr ""
+
+#: internal/parser/builds.go:53
+msgid ""
+"Creates a new build of an application. Imports an <image> and deploys it to "
+"Drycc\n"
+"as a new release. If a Procfile or drycc.yaml is present in the current "
+"directory,\n"
+"it will be used as the default for this application."
+msgstr ""
+
+#: internal/parser/services.go:56
+msgid ""
+"Creates extra service for an application and binds it to specific route of "
+"the main app domain"
+msgstr ""
+
+#: internal/parser/gateways.go:60
+msgid ""
+"Creates gateways for an application and binds it to allow listener of the "
+"main app domain"
+msgstr ""
+
+#: internal/parser/routes.go:57
+msgid "Creates routes for an application, provides a way to route requests"
+msgstr ""
+
+#: internal/parser/resources.go:217
+msgid "Delete a resource from the application"
+msgstr ""
+
+#: internal/parser/volumes.go:190
+msgid "Delete a volume from the application"
+msgstr ""
+
+#: internal/parser/workspaces.go:112
+msgid "Delete a workspace"
+msgstr ""
+
+#: internal/parser/ps.go:155
+msgid "Delete the selected processes"
+msgstr ""
+
+#: internal/parser/gateways.go:92
+msgid "Deletes specific gateway for application"
+msgstr ""
+
+#: internal/parser/releases.go:108
+msgid "Deploy the latest release by process types"
+msgstr ""
+
+#: internal/parser/pts.go:65
+msgid "Describe a process type"
+msgstr ""
+
+#: internal/parser/apps.go:164
+msgid "Destroy an application"
+msgstr ""
+
+#: internal/parser/certs.go:159
+msgid "Detach an SSL certificate from a domain"
+msgstr ""
+
+#: internal/parser/certs.go:160
+msgid "Detach certificate from a domain"
+msgstr ""
+
+#: internal/parser/routes.go:215
+msgid "Dettach to gateway"
+msgstr ""
+
+#: internal/parser/autorollback.go:58
+msgid "Disable autorollback for an application"
+msgstr ""
+
+#: internal/parser/routing.go:58
+msgid "Disable routing for an application"
+msgstr ""
+
+#: internal/parser/autodeploy.go:58
+msgid "Disables autodeploy for an app"
+msgstr ""
+
+#: internal/parser/version.go:16
+msgid "Display client version"
+msgstr ""
+
+#: cmd/root.go:39
+msgid "Display help information"
+msgstr ""
+
+#: internal/parser/auth.go:71
+msgid "Display the current user"
+msgstr ""
+
+#: internal/parser/version.go:17
+msgid "Displays the client version"
+msgstr ""
+
+#: internal/parser/auth.go:72
+msgid "Displays the currently logged in user"
+msgstr ""
+
+#: internal/parser/apps.go:58
+msgid "Do not create a 'drycc' git remote"
+msgstr ""
+
+#: internal/parser/builds.go:67 internal/parser/builds.go:114
+msgid "Drycc config path to the application"
+msgstr ""
+
+#: internal/parser/autorollback.go:46
+msgid "Enable autorollback for an application"
+msgstr ""
+
+#: internal/parser/workspaces.go:201
+msgid "Enable or disable alerts: true or false"
+msgstr ""
+
+#: internal/parser/routing.go:46
+msgid "Enable routing for an application"
+msgstr ""
+
+#: internal/parser/autodeploy.go:46
+msgid "Enables autodeploy for an app"
+msgstr ""
+
+#: internal/parser/auth.go:43
+msgid "Enables or disables SSL certificate verification for API requests"
+msgstr ""
+
+#: internal/parser/volumes.go:91
+msgid "Endpoint url for object storage service"
+msgstr ""
+
+#: internal/parser/ps.go:102
+msgid "Execute a command in a container"
+msgstr ""
+
+#: internal/parser/volumes.go:122
+msgid "Expand a volume capacity for the application"
+msgstr ""
+
+#: internal/parser/config.go:145
+msgid ""
+"Extract all environment variables from an application or config group. for "
+"local use.\n"
+"\n"
+"The environmental variables can be piped into a file, 'drycc config pull > "
+"file',\n"
+"or stored locally in a file named .env. This file can be\n"
+"read by foreman to load the local environment for your app."
+msgstr ""
+
+#: internal/parser/auth.go:78
+msgid "Fetch detailed user information"
+msgstr ""
+
+#: internal/parser/certs.go:111
+msgid "Fetch more detailed information about a certificate"
+msgstr ""
+
+#: internal/parser/tls.go:60
+msgid "Force TLS settings to HTTPS-only redirection"
+msgstr ""
+
+#: internal/parser/volumes.go:95
+msgid "Force a path-style endpoint to be used"
+msgstr ""
+
+#: internal/parser/releases.go:116
+msgid "Force deploy"
+msgstr ""
+
+#: internal/parser/resources.go:148
+msgid "Get a resource's detail in the application"
+msgstr ""
+
+#: internal/parser/certs.go:110
+msgid "Get detailed informaton about the certificate"
+msgstr ""
+
+#: internal/parser/routes.go:123
+msgid "Get route rules"
+msgstr ""
+
+#: internal/parser/routes.go:124
+msgid "Get route rules for an application"
+msgstr ""
+
+#: internal/parser/healthchecks.go:187
+msgid "How often (in seconds) to perform the probe"
+msgstr ""
+
+#: internal/parser/workspaces.go:135
+msgid "Invite a user to a workspace"
+msgstr ""
+
+#: internal/parser/keys.go:30
+msgid "List SSH keys for the logged in user"
+msgstr ""
+
+#: internal/parser/certs.go:40
+msgid "List SSL certificates for an application"
+msgstr ""
+
+#: internal/parser/apps.go:69
+msgid "List accessible applications"
+msgstr ""
+
+#: internal/parser/limits.go:141
+msgid "List all available limit plans"
+msgstr ""
+
+#: internal/parser/limits.go:119
+msgid "List all available limit specs"
+msgstr ""
+
+#: internal/parser/resources.go:66
+msgid "List all available plans for a resource service"
+msgstr ""
+
+#: internal/parser/resources.go:45
+msgid "List all available resource services"
+msgstr ""
+
+#: internal/parser/releases.go:53
+msgid "List an application's release history"
+msgstr ""
+
+#: internal/parser/gateways.go:36
+msgid "List application gateways"
+msgstr ""
+
+#: internal/parser/ps.go:37
+msgid "List application pods"
+msgstr ""
+
+#: internal/parser/pts.go:38
+msgid "List application process types"
+msgstr ""
+
+#: internal/parser/routes.go:100
+msgid "List application routes"
+msgstr ""
+
+#: internal/parser/services.go:76
+msgid "List application services"
+msgstr ""
+
+#: internal/parser/labels.go:34
+msgid "List application's labels"
+msgstr ""
+
+#: internal/parser/autoscale.go:34
+msgid "List autoscale options for an application"
+msgstr ""
+
+#: internal/parser/domains.go:36
+msgid "List domains bound to an application"
+msgstr ""
+
+#: internal/parser/healthchecks.go:50
+msgid "List healthchecks for an application"
+msgstr ""
+
+#: internal/parser/lifecycles.go:50
+msgid "List lifecycles for an application"
+msgstr ""
+
+#: internal/parser/registry.go:44
+msgid "List registry info for an app"
+msgstr ""
+
+#: internal/parser/limits.go:41
+msgid "List resource limits for an app"
+msgstr ""
+
+#: internal/parser/timeouts.go:38
+msgid "List resource timeouts for an app"
+msgstr ""
+
+#: internal/parser/resources.go:120
+msgid "List resources in the application"
+msgstr ""
+
+#: internal/parser/limits.go:118
+msgid "List specification information of the server"
+msgstr ""
+
+#: internal/parser/tags.go:52
+msgid "List tags for an application"
+msgstr ""
+
+#: internal/parser/volumes.go:138
+msgid "List volumes in the application"
+msgstr ""
+
+#: internal/parser/apps.go:70
+msgid "Lists applications visible to the current user"
+msgstr ""
+
+#: internal/parser/gateways.go:37
+msgid "Lists gateways for an application"
+msgstr ""
+
+#: internal/parser/pts.go:39
+msgid "Lists process types servicing an application"
+msgstr ""
+
+#: internal/parser/ps.go:38
+msgid "Lists processes servicing an application"
+msgstr ""
+
+#: internal/parser/registry.go:45
+msgid "Lists registry information for an application"
+msgstr ""
+
+#: internal/parser/releases.go:54
+msgid "Lists release history for an application"
+msgstr ""
+
+#: internal/parser/tokens.go:33
+msgid "Lists tokens visible to the current controller"
+msgstr ""
+
+#: internal/parser/workspaces.go:39
+msgid "Lists workspaces for current user"
+msgstr ""
+
+#: internal/parser/auth.go:35
+msgid "Logs in by authenticating against a controller"
+msgstr ""
+
+#: internal/parser/auth.go:54
+msgid "Logs out from a controller and clears the user session"
+msgstr ""
+
+#: internal/parser/certs.go:15
+msgid "Manage SSL endpoints for an app"
+msgstr ""
+
+#: internal/parser/tls.go:17
+msgid "Manage TLS/SSL settings for applications"
+msgstr ""
+
+#: internal/parser/domains.go:14
+msgid "Manage and assign domain names to your applications"
+msgstr ""
+
+#: internal/parser/healthchecks.go:24
+msgid "Manage application healthchecks"
+msgstr ""
+
+#: internal/parser/lifecycles.go:24
+msgid "Manage application lifecycles"
+msgstr ""
+
+#: internal/parser/apps.go:18
+msgid "Manage applications used to provide services"
+msgstr ""
+
+#: internal/parser/auth.go:13
+msgid "Manage authentication"
+msgstr ""
+
+#: internal/parser/autodeploy.go:14
+msgid "Manage autodeploy if or not for applications"
+msgstr ""
+
+#: internal/parser/autorollback.go:14
+msgid "Manage autorollback if or not for application"
+msgstr ""
+
+#: internal/parser/autoscale.go:15
+msgid "Manage autoscale for applications"
+msgstr ""
+
+#: internal/parser/builds.go:15
+msgid "Manage builds created using 'git push'"
+msgstr ""
+
+#: internal/parser/config.go:23
+msgid "Manage environment variables that define app config"
+msgstr ""
+
+#: internal/parser/gateways.go:14
+msgid "Manage gateways for your applications"
+msgstr ""
+
+#: internal/parser/git.go:14
+msgid "Manage git for applications"
+msgstr ""
+
+#: internal/parser/labels.go:15
+msgid "Manage labels of application"
+msgstr ""
+
+#: internal/parser/timeouts.go:15
+msgid "Manage pods termination grace period"
+msgstr ""
+
+#: internal/parser/registry.go:20
+msgid "Manage private registry information for your application"
+msgstr ""
+
+#: internal/parser/pts.go:15
+msgid "Manage process types inside an app"
+msgstr ""
+
+#: internal/parser/ps.go:15
+msgid "Manage processes inside an app"
+msgstr ""
+
+#: internal/parser/releases.go:24
+msgid "Manage releases of an application"
+msgstr ""
+
+#: internal/parser/limits.go:15
+msgid "Manage resource limits for your application"
+msgstr ""
+
+#: internal/parser/resources.go:17
+msgid "Manage resources for your applications"
+msgstr ""
+
+#: internal/parser/routing.go:14
+msgid "Manage routability of an application"
+msgstr ""
+
+#: internal/parser/routes.go:20
+msgid "Manage routes for your applications"
+msgstr ""
+
+#: internal/parser/services.go:17
+msgid "Manage services for your applications"
+msgstr ""
+
+#: internal/parser/keys.go:15
+msgid "Manage ssh keys used for 'git push' deployments"
+msgstr ""
+
+#: internal/parser/tags.go:19
+msgid "Manage tags for application containers"
+msgstr ""
+
+#: internal/parser/tokens.go:15
+msgid "Manage user tokens"
+msgstr ""
+
+#: internal/parser/volumes.go:15
+msgid "Manage volumes for your applications"
+msgstr ""
+
+#: internal/parser/workspaces.go:17
+msgid "Manage workspaces"
+msgstr ""
+
+#: internal/parser/autoscale.go:73
+msgid "Max replicas to scale up to"
+msgstr ""
+
+#: internal/parser/apps.go:23 internal/parser/apps.go:76
+msgid "Maximum number of results to display"
+msgstr ""
+
+#: internal/parser/config.go:197
+msgid "Merge config values"
+msgstr ""
+
+#: internal/parser/healthchecks.go:189
+msgid ""
+"Minimum consecutive successes for the probe to be considered failed after "
+"having succeeded"
+msgstr ""
+
+#: internal/parser/healthchecks.go:188
+msgid ""
+"Minimum consecutive successes for the probe to be considered successful "
+"after having failed"
+msgstr ""
+
+#: internal/parser/volumes.go:238
+msgid "Mount a volume to process of the application"
+msgstr ""
+
+#: internal/parser/git.go:42 internal/parser/apps.go:57
+msgid "Name of remote to create"
+msgstr ""
+
+#: internal/parser/keys.go:52
+msgid "Name of the SSH key"
+msgstr ""
+
+#: internal/parser/certs.go:129
+msgid "Name of the certificate to attach domain to"
+msgstr ""
+
+#: internal/parser/certs.go:154
+msgid "Name of the certificate to deatch from a domain"
+msgstr ""
+
+#: internal/parser/certs.go:59
+msgid "Name of the certificate to reference it by"
+msgstr ""
+
+#: internal/parser/services.go:52
+msgid ""
+"Number or name of the port to access on the pods targeted by the service"
+msgstr ""
+
+#: internal/parser/apps.go:102
+msgid "Open the application in a browser"
+msgstr ""
+
+#: internal/parser/apps.go:103
+msgid "Opens a URL to the application in the default browser"
+msgstr ""
+
+#: internal/parser/git.go:43
+msgid "Overwrite remote of the given name if it already exists"
+msgstr ""
+
+#: internal/parser/ps.go:114
+msgid "Pass stdin to the container"
+msgstr ""
+
+#: internal/parser/volumes.go:89
+msgid "Path that is exported by the nfs server"
+msgstr ""
+
+#: cmd/root.go:38
+msgid "Path to configuration file"
+msgstr ""
+
+#: internal/parser/routes.go:188 internal/parser/routes.go:224
+msgid "Port is the network port, the gateway listener expects to receive"
+msgstr ""
+
+#: internal/parser/gateways.go:67
+msgid "Port is the network port, the listener expects to receive"
+msgstr ""
+
+#: internal/parser/gateways.go:100
+msgid "Port is the network port, the listener received"
+msgstr ""
+
+#: internal/parser/ps.go:133
+msgid "Print a detailed description of the selected process"
+msgstr ""
+
+#: internal/parser/pts.go:66
+msgid "Print a detailed description of the selected process type"
+msgstr ""
+
+#: internal/parser/builds.go:82
+msgid "Print information about a specific build"
+msgstr ""
+
+#: internal/parser/releases.go:79
+msgid "Print information about a specific release"
+msgstr ""
+
+#: internal/parser/volumes.go:164
+msgid "Print information about a volume"
+msgstr ""
+
+#: internal/parser/workspaces.go:85
+msgid "Print information about a workspace"
+msgstr ""
+
+#: internal/parser/builds.go:107
+msgid "Print process info about a specific build"
+msgstr ""
+
+#: internal/parser/ps.go:63
+msgid "Print the logs for a container"
+msgstr ""
+
+#: internal/parser/ps.go:64
+msgid "Print the logs for a container in a pod or specified resource"
+msgstr ""
+
+#: internal/parser/ps.go:78
+msgid ""
+"Print the logs for the previous instance of the container in a pod if it "
+"exists"
+msgstr ""
+
+#: internal/parser/ps.go:77
+msgid "Print the logs of this container"
+msgstr ""
+
+#: internal/parser/autoscale.go:35
+msgid "Prints a list of autoscale options for the application"
+msgstr ""
+
+#: internal/parser/labels.go:35
+msgid "Prints a list of labels of the application"
+msgstr ""
+
+#: internal/parser/releases.go:80
+msgid "Prints info about a particular release"
+msgstr ""
+
+#: internal/parser/apps.go:85
+msgid "Prints info about the current application"
+msgstr ""
+
+#: internal/parser/autodeploy.go:34
+msgid "Prints info about the current application's autodeploy if or not"
+msgstr ""
+
+#: internal/parser/autorollback.go:34
+msgid "Prints info about the current application's autorollback if or not"
+msgstr ""
+
+#: internal/parser/config.go:159
+msgid "Prompts for each value to be overwritten"
+msgstr ""
+
+#: internal/parser/gateways.go:68
+msgid ""
+"Protocol specifies the network protocol this listener expects to receive. "
+"Supports TCP, UDP, TLS, HTTP, and HTTPS"
+msgstr ""
+
+#: internal/parser/gateways.go:101
+msgid "Protocol specifies the network protocol this listener received"
+msgstr ""
+
+#: internal/parser/tokens.go:55
+msgid "Provide a alias for controller authentication token"
+msgstr ""
+
+#: internal/parser/auth.go:42 internal/parser/tokens.go:68
+msgid "Provide a password for the account"
+msgstr ""
+
+#: internal/parser/auth.go:41 internal/parser/tokens.go:67
+msgid "Provide a username for the account"
+msgstr ""
+
+#: internal/parser/config.go:144
+msgid "Pull environment variables to the path"
+msgstr ""
+
+#: internal/parser/config.go:183
+msgid "Push environment variables from the path"
+msgstr ""
+
+#: internal/parser/limits.go:149
+msgid "Query plans that meet the specified memory capacity, unit GiB"
+msgstr ""
+
+#: internal/parser/limits.go:148
+msgid "Query plans that meet the specified number of cpu cores"
+msgstr ""
+
+#: internal/parser/limits.go:150
+msgid "Query plans that meet the specified spec id, see [specs] subcommand."
+msgstr ""
+
+#: internal/parser/routes.go:247
+msgid "Remove a route from an application"
+msgstr ""
+
+#: internal/parser/tokens.go:94
+msgid "Remove a token for controller authentication"
+msgstr ""
+
+#: internal/parser/workspaces.go:157
+msgid "Remove a user from a workspace"
+msgstr ""
+
+#: internal/parser/keys.go:81
+msgid "Remove an SSH key"
+msgstr ""
+
+#: internal/parser/certs.go:88
+msgid "Remove an SSL certificate from an app"
+msgstr ""
+
+#: internal/parser/labels.go:90
+msgid "Remove application's label"
+msgstr ""
+
+#: internal/parser/gateways.go:91
+msgid "Remove gateways from an application"
+msgstr ""
+
+#: internal/parser/services.go:103
+msgid "Remove service from an application"
+msgstr ""
+
+#: internal/parser/keys.go:82
+msgid "Removes an SSH key for the logged in user"
+msgstr ""
+
+#: internal/parser/git.go:52
+msgid "Removes git remote of application from repository"
+msgstr ""
+
+#: internal/parser/pts.go:91
+msgid "Restart an application or process types"
+msgstr ""
+
+#: internal/parser/pts.go:90
+msgid "Restart application process types"
+msgstr ""
+
+#: internal/parser/apps.go:147
+msgid "Retention time of records in seconds"
+msgstr ""
+
+#: internal/parser/releases.go:136
+msgid "Return to a previous release"
+msgstr ""
+
+#: internal/parser/releases.go:137
+msgid "Rolls back to a previous application release"
+msgstr ""
+
+#: internal/parser/routes.go:158
+msgid "Rules-file is the file name of rule to apply"
+msgstr ""
+
+#: internal/parser/apps.go:136
+msgid "Run a command in an ephemeral app container"
+msgstr ""
+
+#: internal/parser/apps.go:137
+msgid "Runs a command inside an ephemeral app container"
+msgstr ""
+
+#: internal/parser/builds.go:116
+msgid "Save process info to the local"
+msgstr ""
+
+#: internal/parser/pts.go:124
+msgid "Scale process types of replicas"
+msgstr ""
+
+#: internal/parser/pts.go:125
+msgid "Scales an application's processes by type"
+msgstr ""
+
+#: internal/parser/limits.go:126
+msgid ""
+"Search keywords separated by commas, matching must satisfy all of the "
+"specified."
+msgstr ""
+
+#: internal/parser/volumes.go:94
+msgid "Secret access key for authentication"
+msgstr ""
+
+#: internal/parser/config.go:225
+msgid "Selects environment groups to attach an app ptype"
+msgstr ""
+
+#: internal/parser/config.go:250
+msgid "Selects environment groups to detach an app ptype"
+msgstr ""
+
+#: internal/parser/volumes.go:209
+msgid "Serve a WebDAV service for the volume"
+msgstr ""
+
+#: internal/parser/autoscale.go:64
+msgid "Set autoscale option per process type for an app"
+msgstr ""
+
+#: internal/parser/config.go:85
+msgid "Set environment variables for an app"
+msgstr ""
+
+#: internal/parser/healthchecks.go:90
+msgid "Set healthchecks for an application"
+msgstr ""
+
+#: internal/parser/lifecycles.go:84
+msgid "Set lifecycles for an application"
+msgstr ""
+
+#: internal/parser/registry.go:76
+msgid "Set registry info for an app"
+msgstr ""
+
+#: internal/parser/limits.go:70
+msgid "Set resource limits for an app"
+msgstr ""
+
+#: internal/parser/timeouts.go:64
+msgid "Set resource timeouts for an app"
+msgstr ""
+
+#: internal/parser/routes.go:149
+msgid "Set route rules"
+msgstr ""
+
+#: internal/parser/routes.go:150
+msgid "Set route rules for an application"
+msgstr ""
+
+#: internal/parser/tags.go:86
+msgid "Set tags for an application"
+msgstr ""
+
+#: internal/parser/config.go:86
+msgid "Sets environment variables for an application or config group"
+msgstr ""
+
+#: internal/parser/config.go:184
+msgid ""
+"Sets environment variables for an application or config group.\n"
+"\n"
+"This file can be read by foreman\n"
+"to load the local environment for your app. The file should be piped via\n"
+"stdin, 'drycc config push < .env', or using the --path option."
+msgstr ""
+
+#: internal/parser/healthchecks.go:91
+msgid ""
+"Sets healthchecks for an application.\n"
+"\n"
+"By default, Workflow only checks that the application starts in their "
+"Container. A health\n"
+"check may be added by configuring a health check probe for the application. "
+"The health\n"
+"checks are implemented as Kubernetes Container Probes. A 'startupProbe' "
+"'livenessProbe' \n"
+"and a 'readinessProbe' can be configured, and each probe can be of type "
+"'httpGet', 'exec' \n"
+"or 'tcpSocket' depending on the type of probe the Container requires.\n"
+"\n"
+"A 'startupProbe' indicates whether the application within the container is "
+"started.\n"
+"All other probes are disabled if a startup probe is provided, until it "
+"succeeds.\n"
+"If the startup probe fails, the container is subjected to its restart "
+"policy.\n"
+"\n"
+"A 'livenessProbe' is useful for applications running for long periods of "
+"time, eventually\n"
+"transitioning to broken states and cannot recover except by restarting "
+"them.\n"
+"\n"
+"Other times, a 'readinessProbe' is useful when the Container is only "
+"temporarily unable\n"
+"to serve, and will recover on its own. In this case, if a Container fails "
+"its 'readinessProbe'\n"
+", the Container will not be shut down, but rather the Container will stop "
+"receiving\n"
+"incoming requests.\n"
+"\n"
+"'httpGet' probes are just as it sounds: it performs a HTTP GET operation on "
+"the Container.\n"
+"A response code inside the 200-399 range is considered a pass. 'httpGet' "
+"probes accept a\n"
+"port number to perform the HTTP GET operation on the Container.\n"
+"\n"
+"'exec' probes run a command inside the Container to determine its health. An "
+"exit code of\n"
+"zero is considered a pass, while a non-zero status code is considered a "
+"fail. 'exec'\n"
+"probes accept a string of arguments to be run inside the Container.\n"
+"\n"
+"'tcpSocket' probes attempt to open a socket in the Container. The Container "
+"is only\n"
+"considered healthy if the check can establish a connection. 'tcpSocket' "
+"probes accept a\n"
+"port number to perform the socket connection on the Container.\n"
+msgstr ""
+
+#: internal/parser/labels.go:61
+msgid ""
+"Sets labels for an application.\n"
+"\n"
+"A label is a key/value pair used to label an application. This label is a "
+"general information for drycc user.\n"
+"Mostly used for administration/maintenance information, note for "
+"application. This information isn't send to scheduler."
+msgstr ""
+
+#: internal/parser/lifecycles.go:85
+msgid ""
+"Sets lifecycle handlers for an application.\n"
+"\n"
+"Lifecycle handlers allow you to run actions at specific points in a "
+"container's lifecycle.\n"
+"Two types of lifecycle handlers are supported: 'postStart' and 'preStop'.\n"
+"\n"
+"A 'postStart' handler runs immediately after a container is created. If the "
+"handler fails,\n"
+"the container is terminated and restarted according to its restart policy.\n"
+"\n"
+"A 'preStop' handler runs immediately before a container is terminated. This "
+"handler must\n"
+"complete before the container termination signal is sent. If the handler "
+"fails, the\n"
+"container enters a termination grace period and is then forcefully "
+"terminated.\n"
+"\n"
+"Each lifecycle handler can be configured with one of the following action "
+"types:\n"
+"\n"
+"'httpGet': Performs an HTTP GET request to the container. The action is "
+"considered\n"
+"successful if the response status code is in the 200-399 range. 'httpGet' "
+"actions require\n"
+"a port number to specify where to send the HTTP request.\n"
+"\n"
+"'exec': Executes a command inside the container. The action is considered "
+"successful if\n"
+"the command exits with status code 0. 'exec' actions accept a list of "
+"command arguments\n"
+"to run inside the container.\n"
+"\n"
+"'tcpSocket': Attempts to open a TCP socket connection to the container. The "
+"action is\n"
+"considered successful if a connection can be established. 'tcpSocket' "
+"actions require a\n"
+"port number to specify where to attempt the connection.\n"
+"\n"
+"'sleep': Waits for a specified duration before proceeding. 'sleep' actions "
+"accept a\n"
+"duration in seconds to wait.\n"
+msgstr ""
+
+#: internal/parser/registry.go:77
+msgid ""
+"Sets registry information for an application. These credentials are the same "
+"as those used for\n"
+"'podmain login' to the private registry."
+msgstr ""
+
+#: internal/parser/limits.go:71
+msgid ""
+"Sets resource limits for an application.\n"
+"\n"
+"A resource limit is a finite resource within a pod which we can apply\n"
+"restrictions through Kubernetes."
+msgstr ""
+
+#: internal/parser/tags.go:87
+msgid ""
+"Sets tags for an application.\n"
+"\n"
+"A tag is a key/value pair used to tag an application's containers and is "
+"passed to the\n"
+"scheduler. This is often used to restrict workloads to specific hosts "
+"matching the\n"
+"scheduler-configured metadata."
+msgstr ""
+
+#: internal/parser/timeouts.go:65
+msgid "Sets termination grace period for an application"
+msgstr ""
+
+#: cmd/root.go:74
+msgid "Shortcut Commands:"
+msgstr ""
+
+#: internal/parser/certs.go:41
+msgid "Show certificate information for an SSL application"
+msgstr ""
+
+#: internal/parser/resources.go:156
+msgid "Show extra details provided to resource"
+msgstr ""
+
+#: internal/parser/update.go:21
+msgid "Simulate an update, only print the version info."
+msgstr ""
+
+#: internal/parser/apps.go:171
+msgid ""
+"Skips the prompt for the application name. \n"
+"<app> is the uniquely identifiable name for the application"
+msgstr ""
+
+#: internal/parser/workspaces.go:119
+msgid "Skips the prompt for the workspace name"
+msgstr ""
+
+#: internal/parser/ps.go:76
+msgid "Specify if the logs should be streamed"
+msgstr ""
+
+#: internal/parser/resources.go:112 internal/parser/resources.go:196
+msgid "Specify values in a YAML file. If set, params will be discard"
+msgstr ""
+
+#: internal/parser/ps.go:113
+msgid "Stdin is a TTY"
+msgstr ""
+
+#: internal/parser/autoscale.go:74
+msgid "Target CPU utilization percentage"
+msgstr ""
+
+#: cmd/root.go:29
+msgid "The Drycc command-line client issues API calls to a Drycc controller"
+msgstr ""
+
+#: internal/parser/lifecycles.go:185
+msgid "The HTTP headers to send for 'httpGet' actions, separated by commas"
+msgstr ""
+
+#: internal/parser/healthchecks.go:184
+msgid "The HTTP headers to send for 'httpGet' probes, separated by commas"
+msgstr ""
+
+#: internal/parser/services.go:65 internal/parser/services.go:116
+msgid "The IP protocol for this port. Supports TCP, UDP, and SCTP"
+msgstr ""
+
+#: internal/parser/healthchecks.go:86
+msgid ""
+"The arguments required for the healthcheck probe. 'exec', accepts a list of "
+"arguments;\n"
+"                  'httpGet' and 'tcpSocket' accept a port number."
+msgstr ""
+
+#: internal/parser/lifecycles.go:80
+msgid ""
+"The arguments required for the lifecycle action. 'exec' accepts a list of "
+"arguments;\n"
+"                  'sleep' accepts duration in seconds; 'httpGet' and "
+"'tcpSocket' accept a port number."
+msgstr ""
+
+#: internal/parser/workspaces.go:70
+msgid "The contact email for the workspace"
+msgstr ""
+
+#: internal/parser/workspaces.go:132
+msgid "The email address of the user to invite"
+msgstr ""
+
+#: internal/parser/shortcuts.go:53 internal/parser/volumes.go:235
+#: internal/parser/apps.go:132
+msgid "The filesystem path"
+msgstr ""
+
+#: internal/parser/config.go:34 internal/parser/config.go:64
+msgid "The group for which the config needs to be listed"
+msgstr ""
+
+#: internal/parser/config.go:157
+msgid "The group for which the config needs to be pull"
+msgstr ""
+
+#: internal/parser/config.go:195
+msgid "The group for which the config needs to be push"
+msgstr ""
+
+#: internal/parser/config.go:93
+msgid "The group for which the config needs to be set"
+msgstr ""
+
+#: internal/parser/config.go:122
+msgid "The group for which the config needs to be unset"
+msgstr ""
+
+#: internal/parser/config.go:220
+msgid "The group that requires attach configurations"
+msgstr ""
+
+#: internal/parser/config.go:247
+msgid "The group that requires detach configurations"
+msgstr ""
+
+#: internal/parser/healthchecks.go:84
+msgid ""
+"The healthcheck probe, such as 'startupProbe' 'livenessProbe' or "
+"'readinessProbe'"
+msgstr ""
+
+#: internal/parser/volumes.go:88
+msgid "The hostname or ip address of the nfs server"
+msgstr ""
+
+#: internal/parser/tokens.go:91
+msgid "The id of the token for controller authentication"
+msgstr ""
+
+#: internal/parser/healthchecks.go:185
+msgid "The initial delay timeout for the probe"
+msgstr ""
+
+#: internal/parser/lifecycles.go:78
+msgid "The lifecycle handler, such as 'postStart' 'preStop'"
+msgstr ""
+
+#: internal/parser/limits.go:67
+msgid "The limit plan id to apply to the process type"
+msgstr ""
+
+#: internal/parser/certs.go:23 internal/parser/certs.go:48
+#: internal/parser/domains.go:22 internal/parser/domains.go:43
+#: internal/parser/gateways.go:22 internal/parser/gateways.go:44
+#: internal/parser/limits.go:127 internal/parser/limits.go:151
+#: internal/parser/releases.go:33 internal/parser/releases.go:62
+#: internal/parser/resources.go:25 internal/parser/resources.go:127
+#: internal/parser/routes.go:28 internal/parser/routes.go:107
+#: internal/parser/tokens.go:22 internal/parser/tokens.go:40
+#: internal/parser/volumes.go:23 internal/parser/volumes.go:145
+#: internal/parser/workspaces.go:24 internal/parser/workspaces.go:46
+#: internal/parser/workspaces.go:93
+msgid "The maximum number of results to display"
+msgstr ""
+
+#: internal/parser/certs.go:106
+msgid "The name of the cert to get information from"
+msgstr ""
+
+#: internal/parser/certs.go:84
+msgid "The name of the cert to remove from the app"
+msgstr ""
+
+#: internal/parser/workspaces.go:61 internal/parser/workspaces.go:82
+#: internal/parser/workspaces.go:109 internal/parser/workspaces.go:131
+#: internal/parser/workspaces.go:153 internal/parser/workspaces.go:181
+msgid "The name of the workspace"
+msgstr ""
+
+#: internal/parser/ps.go:75
+msgid "The number of lines to display, -1 showing all log lines"
+msgstr ""
+
+#: internal/parser/pts.go:121 internal/parser/shortcuts.go:95
+msgid "The number of processes"
+msgstr ""
+
+#: internal/parser/healthchecks.go:186
+msgid "The number of seconds after which the probe times out"
+msgstr ""
+
+#: internal/parser/registry.go:73
+msgid "The password of the registry"
+msgstr ""
+
+#: internal/parser/ps.go:99 internal/parser/ps.go:130 internal/parser/ps.go:152
+#: internal/parser/shortcuts.go:66
+msgid "The pod name for the application"
+msgstr ""
+
+#: internal/parser/services.go:100
+msgid "The port exposed by this service"
+msgstr ""
+
+#: internal/parser/services.go:51
+msgid "The port that will be exposed by this service"
+msgstr ""
+
+#: internal/parser/certs.go:61
+msgid "The private key of the SSL certificate"
+msgstr ""
+
+#: internal/parser/pts.go:62 internal/parser/pts.go:120
+#: internal/parser/pts.go:157 internal/parser/shortcuts.go:94
+#: internal/parser/tags.go:26 internal/parser/tags.go:60
+#: internal/parser/tags.go:100 internal/parser/tags.go:131
+#: internal/parser/volumes.go:234 internal/parser/volumes.go:259
+msgid "The process name as defined in your Procfile"
+msgstr ""
+
+#: internal/parser/releases.go:132
+msgid "The process name as defined in your Procfile, such as 'web'"
+msgstr ""
+
+#: internal/parser/timeouts.go:60 internal/parser/timeouts.go:86
+msgid "The process type as defined in your Procfile"
+msgstr ""
+
+#: internal/parser/limits.go:66 internal/parser/limits.go:96
+msgid "The process type as defined in your Procfile, such as 'web' or 'worker'"
+msgstr ""
+
+#: internal/parser/autoscale.go:59
+msgid "The process type to add to the application's autoscale settings"
+msgstr ""
+
+#: internal/parser/autoscale.go:96
+msgid "The process type to remove from the application's autoscale settings"
+msgstr ""
+
+#: internal/parser/domains.go:62
+msgid "The ptype for domain"
+msgstr ""
+
+#: internal/parser/registry.go:27 internal/parser/registry.go:51
+#: internal/parser/registry.go:86 internal/parser/registry.go:108
+msgid "The ptype for registry"
+msgstr ""
+
+#: internal/parser/config.go:33 internal/parser/config.go:63
+msgid "The ptype for which the config needs to be listed"
+msgstr ""
+
+#: internal/parser/config.go:155
+msgid "The ptype for which the config needs to be pull"
+msgstr ""
+
+#: internal/parser/config.go:194
+msgid "The ptype for which the config needs to be push"
+msgstr ""
+
+#: internal/parser/config.go:92
+msgid "The ptype for which the config needs to be set"
+msgstr ""
+
+#: internal/parser/config.go:121
+msgid "The ptype for which the config needs to be unset"
+msgstr ""
+
+#: internal/parser/healthchecks.go:178
+msgid "The ptype for which the health check needs to be applied"
+msgstr ""
+
+#: internal/parser/healthchecks.go:31
+msgid "The ptype for which the health check needs to be listed"
+msgstr ""
+
+#: internal/parser/healthchecks.go:224
+msgid "The ptype for which the health check needs to be removed"
+msgstr ""
+
+#: internal/parser/lifecycles.go:179
+msgid "The ptype for which the lifecycle handler needs to be applied"
+msgstr ""
+
+#: internal/parser/lifecycles.go:221
+msgid "The ptype for which the lifecycle handler needs to be unset"
+msgstr ""
+
+#: internal/parser/lifecycles.go:31
+msgid "The ptype for which the lifecycle needs to be listed"
+msgstr ""
+
+#: internal/parser/config.go:219
+msgid "The ptype that requires attach configurations"
+msgstr ""
+
+#: internal/parser/config.go:246
+msgid "The ptype that requires detach configurations"
+msgstr ""
+
+#: internal/parser/certs.go:60
+msgid "The public key of the SSL certificate"
+msgstr ""
+
+#: internal/parser/lifecycles.go:184
+msgid "The relative URL path for 'httpGet' actions"
+msgstr ""
+
+#: internal/parser/healthchecks.go:183
+msgid "The relative URL path for 'httpGet' probes"
+msgstr ""
+
+#: internal/parser/releases.go:76 internal/parser/releases.go:133
+msgid "The release of the application, such as '1'"
+msgstr ""
+
+#: internal/parser/resources.go:214 internal/parser/resources.go:239
+#: internal/parser/resources.go:261
+msgid "The resource instance alias name"
+msgstr ""
+
+#: internal/parser/resources.go:94 internal/parser/resources.go:177
+msgid "The resource instance parameters key"
+msgstr ""
+
+#: internal/parser/resources.go:95 internal/parser/resources.go:178
+msgid "The resource instance parameters value"
+msgstr ""
+
+#: internal/parser/resources.go:92 internal/parser/resources.go:175
+msgid "The resource's service"
+msgstr ""
+
+#: internal/parser/workspaces.go:200
+msgid "The role to assign: admin, member, or viewer"
+msgstr ""
+
+#: internal/parser/routes.go:52
+msgid "The route kind, range: HTTPRoute,TCPRoute,UDPRoute,GRPCRoute,TLSRoute"
+msgstr ""
+
+#: internal/parser/routes.go:53
+msgid "The route's backend, pattern: <service>:<port>,<weight>"
+msgstr ""
+
+#: internal/parser/resources.go:63
+msgid "The service name for plans"
+msgstr ""
+
+#: internal/parser/resources.go:93 internal/parser/resources.go:176
+msgid "The service's plan"
+msgstr ""
+
+#: internal/parser/shortcuts.go:54 internal/parser/apps.go:133
+msgid "The shell command to run inside the container"
+msgstr ""
+
+#: internal/parser/builds.go:65
+msgid "The stack name for the application"
+msgstr ""
+
+#: internal/parser/lifecycles.go:186
+msgid "The stop signal to send to the container"
+msgstr ""
+
+#: internal/parser/tags.go:82
+msgid "The tag key"
+msgstr ""
+
+#: internal/parser/tags.go:119
+msgid "The tag key to unset"
+msgstr ""
+
+#: internal/parser/tags.go:83
+msgid "The tag value"
+msgstr ""
+
+#: internal/parser/routes.go:187 internal/parser/routes.go:223
+msgid "The unique name of the gaetway"
+msgstr ""
+
+#: internal/parser/routes.go:51 internal/parser/routes.go:120
+#: internal/parser/routes.go:146 internal/parser/routes.go:176
+#: internal/parser/routes.go:212 internal/parser/routes.go:244
+msgid "The unique name of the route"
+msgstr ""
+
+#: internal/parser/autodeploy.go:20 internal/parser/autorollback.go:19
+#: internal/parser/autoscale.go:21 internal/parser/builds.go:21
+#: internal/parser/builds.go:64 internal/parser/certs.go:22
+#: internal/parser/config.go:29 internal/parser/domains.go:21
+#: internal/parser/gateways.go:21 internal/parser/git.go:18
+#: internal/parser/labels.go:21 internal/parser/limits.go:21
+#: internal/parser/ps.go:21 internal/parser/ps.go:112 internal/parser/pts.go:21
+#: internal/parser/pts.go:134 internal/parser/registry.go:26
+#: internal/parser/releases.go:32 internal/parser/resources.go:24
+#: internal/parser/routes.go:27 internal/parser/routing.go:20
+#: internal/parser/services.go:23 internal/parser/tags.go:25
+#: internal/parser/timeouts.go:21 internal/parser/volumes.go:22
+#: internal/parser/apps.go:91 internal/parser/apps.go:109
+#: internal/parser/apps.go:144 internal/parser/apps.go:170
+#: internal/parser/apps.go:193
+msgid "The uniquely identifiable name for the application"
+msgstr ""
+
+#: internal/parser/healthchecks.go:30 internal/parser/lifecycles.go:30
+msgid "The uniquely identifiable name of the application"
+msgstr ""
+
+#: internal/parser/workspaces.go:182
+msgid "The username of the member to update"
+msgstr ""
+
+#: internal/parser/registry.go:72
+msgid "The username of the registry"
+msgstr ""
+
+#: internal/parser/workspaces.go:154
+msgid "The username to remove from the workspace"
+msgstr ""
+
+#: internal/parser/timeouts.go:61
+msgid "The value to apply to the process type in seconds"
+msgstr ""
+
+#: internal/parser/builds.go:22 internal/parser/builds.go:87
+#: internal/parser/builds.go:112
+msgid "The version for which the build info needs to be displayed"
+msgstr ""
+
+#: internal/parser/config.go:35 internal/parser/config.go:65
+msgid "The version for which the config needs to be listed"
+msgstr ""
+
+#: internal/parser/config.go:156
+msgid "The version for which the config needs to be pull"
+msgstr ""
+
+#: internal/parser/healthchecks.go:32 internal/parser/healthchecks.go:56
+msgid "The version for which the health check needs to be listed"
+msgstr ""
+
+#: internal/parser/lifecycles.go:32 internal/parser/lifecycles.go:56
+msgid "The version for which the lifecycle needs to be listed"
+msgstr ""
+
+#: internal/parser/limits.go:22 internal/parser/limits.go:47
+msgid "The version for which the limit needs to be listed"
+msgstr ""
+
+#: internal/parser/registry.go:28 internal/parser/registry.go:52
+msgid "The version for which the registry needs to be listed"
+msgstr ""
+
+#: internal/parser/tags.go:27 internal/parser/tags.go:61
+msgid "The version for which the tag needs to be listed"
+msgstr ""
+
+#: internal/parser/timeouts.go:22 internal/parser/timeouts.go:44
+msgid "The version for which the timeout needs to be listed"
+msgstr ""
+
+#: internal/parser/shortcuts.go:52 internal/parser/volumes.go:58
+#: internal/parser/volumes.go:118 internal/parser/volumes.go:213
+#: internal/parser/volumes.go:233 internal/parser/volumes.go:258
+#: internal/parser/apps.go:131
+msgid "The volume name"
+msgstr ""
+
+#: internal/parser/volumes.go:161
+msgid "The volume name to be info"
+msgstr ""
+
+#: internal/parser/volumes.go:187
+msgid "The volume name to be removed"
+msgstr ""
+
+#: internal/parser/volumes.go:59 internal/parser/volumes.go:119
+msgid "The volume size, such as '500G'"
+msgstr ""
+
+#: internal/parser/volumes.go:87
+msgid "The volume type, such as csi, nfs, oss"
+msgstr ""
+
+#: internal/parser/apps.go:56
+msgid "The workspace to create the app in"
+msgstr ""
+
+#: internal/parser/resources.go:91 internal/parser/resources.go:145
+#: internal/parser/resources.go:174
+msgid "This resource instance alias"
+msgstr ""
+
+#: internal/parser/builds.go:68 internal/parser/builds.go:115
+#: internal/parser/pts.go:99 internal/parser/releases.go:117
+#: internal/parser/tokens.go:103
+msgid "To proceed, type \"yes\""
+msgstr ""
+
+#: internal/parser/config.go:94 internal/parser/config.go:123
+#: internal/parser/config.go:198
+msgid "To proceed, type 'yes'"
+msgstr ""
+
+#: internal/parser/apps.go:184
+msgid "Transfer app to another workspace"
+msgstr ""
+
+#: internal/parser/apps.go:185
+msgid "Transfer application to another workspace."
+msgstr ""
+
+#: internal/parser/autoscale.go:100
+msgid "Turn off autoscale for an app"
+msgstr ""
+
+#: internal/parser/autoscale.go:63
+msgid "Turn on autoscale for an app"
+msgstr ""
+
+#: internal/parser/domains.go:75
+msgid "Unbinds a domain for an application"
+msgstr ""
+
+#: internal/parser/volumes.go:262
+msgid "Unmount a volume from process of the application"
+msgstr ""
+
+#: internal/parser/autoscale.go:101
+msgid "Unset autoscale per process type for an app"
+msgstr ""
+
+#: internal/parser/config.go:114
+msgid "Unset environment variables for an app"
+msgstr ""
+
+#: internal/parser/healthchecks.go:212
+msgid "Unset healthchecks for an application"
+msgstr ""
+
+#: internal/parser/lifecycles.go:209
+msgid "Unset lifecycle handler for an application"
+msgstr ""
+
+#: internal/parser/registry.go:101
+msgid "Unset registry info for an app"
+msgstr ""
+
+#: internal/parser/limits.go:99
+msgid "Unset resource limits for an app"
+msgstr ""
+
+#: internal/parser/timeouts.go:89
+msgid "Unset resource timeouts for an app"
+msgstr ""
+
+#: internal/parser/tags.go:122
+msgid "Unset tags for an application"
+msgstr ""
+
+#: internal/parser/config.go:115
+msgid "Unsets an environment variable for an application or config group"
+msgstr ""
+
+#: internal/parser/labels.go:91
+msgid "Unsets labels for an application"
+msgstr ""
+
+#: internal/parser/registry.go:102
+msgid "Unsets registry information for an application"
+msgstr ""
+
+#: internal/parser/timeouts.go:90
+msgid ""
+"Unsets timeouts for an application. Default value (30s) or set by drycc "
+"controller"
+msgstr ""
+
+#: internal/parser/workspaces.go:185
+msgid "Update a member's role and/or alerts in a workspace"
+msgstr ""
+
+#: internal/parser/resources.go:181
+msgid "Update a resource from the application"
+msgstr ""
+
+#: internal/parser/update.go:15 internal/parser/update.go:16
+msgid "Update workflow cli to latest release"
+msgstr ""
+
+#: internal/parser/tls.go:39
+msgid "View TLS settings information"
+msgstr ""
+
+#: internal/parser/routing.go:34
+msgid "View application's routability information"
+msgstr ""
+
+#: internal/parser/autorollback.go:33
+msgid "View autorollback info for an application"
+msgstr ""
+
+#: internal/parser/apps.go:84
+msgid "View info about an application"
+msgstr ""
+
+#: internal/parser/apps.go:145
+msgid "Volume mounts in format 'volume:path'"
+msgstr ""
+
+#: internal/parser/builds.go:49
+msgid "default container image"
+msgstr ""
+
+#: internal/parser/builds.go:52
+msgid "imports an image and deploys as a new release"
+msgstr ""
+
+#: internal/parser/version.go:22
+msgid "list api and controller versions."
+msgstr ""
+
+#: internal/parser/autoscale.go:72
+msgid "minimum replicas to keep around"
+msgstr ""
+
+#: internal/parser/services.go:99
+msgid "procfile type which should handle the request, e.g. webhooks"
+msgstr ""
+
+#: internal/parser/services.go:49
+msgid ""
+"procfile type which should handle the request, e.g. webhooks (should be bind "
+"to the port PORT)\n"
+"             only single extra service per Porcfile type could be created."
+msgstr ""
+
+#: internal/parser/resources.go:225
+msgid ""
+"skips the prompt for the resource name. <resource> is the uniquely "
+"identifiable\n"
+"name for the resource"
+msgstr ""
+
+#: internal/parser/healthchecks.go:85
+msgid "the healthcheck probe type, such as 'httpGet', 'exec' or 'tcpSocket'"
+msgstr ""
+
+#: internal/parser/healthchecks.go:209
+msgid ""
+"the healthcheck probe, such as 'startupProbe' 'livenessProbe' or "
+"'readinessProbe'"
+msgstr ""
+
+#: internal/parser/labels.go:85
+msgid "the label key to unset, for example: \"git_repo\" or \"team\""
+msgstr ""
+
+#: internal/parser/labels.go:56
+msgid "the label key, for example: \"git_repo\" or \"team\""
+msgstr ""
+
+#: internal/parser/labels.go:57
+msgid ""
+"the label value, for example: \"https://github.com/drycc/workflow\" or "
+"\"frontend\""
+msgstr ""
+
+#: internal/parser/lifecycles.go:79
+msgid ""
+"the lifecycle action type, such as 'httpGet', 'exec', 'sleep' or 'tcpSocket'"
+msgstr ""
+
+#: internal/parser/lifecycles.go:206
+msgid "the lifecycle handler type, such as 'postStart' or 'preStop'"
+msgstr ""
+
+#: internal/parser/resources.go:264
+msgid "unbind a resources for an application"
+msgstr ""
diff --git a/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.mo b/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.mo
new file mode 100644
index 000000000..0e67e0eb7
Binary files /dev/null and b/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.mo differ
diff --git a/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po b/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po
new file mode 100644
index 000000000..0f476316a
--- /dev/null
+++ b/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po
@@ -0,0 +1,1905 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL\n"
+"POT-Creation-Date: 2026-03-26 14:27+0800\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: zh_CN\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: internal/parser/builds.go:66 internal/parser/builds.go:113
+msgid "A YAML file used to supply a Procfile to the application"
+msgstr "用于向应用程序提供Procfile的YAML文件"
+
+#: internal/parser/keys.go:53
+msgid "A local file path to an SSH public key used to push application code"
+msgstr "用于推送应用程序代码的SSH公钥的本地文件路径"
+
+#: internal/parser/config.go:158 internal/parser/config.go:196
+msgid "A path leading to an environment file"
+msgstr "指向环境文件的路径"
+
+#: internal/parser/tls.go:123
+msgid "ACME account email"
+msgstr "ACME账户邮箱"
+
+#: internal/parser/tls.go:124
+msgid "ACME server URL"
+msgstr "ACME服务器URL"
+
+#: internal/parser/volumes.go:93
+msgid "Access key id for authentication"
+msgstr "用于身份验证的访问密钥ID"
+
+#: internal/parser/tls.go:55 internal/parser/tls.go:84
+msgid "Action to perform, 'enable' or 'disable'"
+msgstr "要执行的操作，'enable' 或 'disable'"
+
+#: internal/parser/tokens.go:58
+msgid "Add a token for controller authentication"
+msgstr "添加用于控制器身份验证的令牌"
+
+#: internal/parser/keys.go:57
+msgid "Add an SSH key"
+msgstr "添加SSH密钥"
+
+#: internal/parser/certs.go:65
+msgid "Add an SSL certificate to an app"
+msgstr "向应用添加SSL证书"
+
+#: internal/parser/labels.go:60
+msgid "Add new application's label"
+msgstr "添加新的应用程序标签"
+
+#: internal/parser/keys.go:58
+msgid "Adds SSH keys for the logged in user"
+msgstr "为登录用户添加SSH密钥"
+
+#: internal/parser/git.go:36
+msgid "Adds git remote of application to repository"
+msgstr "将应用程序的Git远程仓库添加到代码库"
+
+#: internal/parser/config.go:160
+msgid "Allows you to have the pull overwrite keys to the path"
+msgstr "允许将拉取的密钥覆盖到指定路径"
+
+# 完整翻译版本：
+#: internal/parser/config.go:57
+msgid "An app config info"
+msgstr "应用程序配置信息"
+
+#: internal/parser/tls.go:23
+msgid "Application name"
+msgstr "应用程序名称"
+
+#: internal/parser/certs.go:135
+msgid "Attach a certificate to a domain"
+msgstr "将证书附加到域名"
+
+#: internal/parser/certs.go:134
+msgid "Attach an SSL certificate to a domain"
+msgstr "将SSL证书附加到域名"
+
+#: internal/parser/routes.go:179
+msgid "Attach to gateway"
+msgstr "附加到网关"
+
+#: internal/parser/auth.go:34
+msgid "Authenticate against a controller"
+msgstr "对控制器进行身份验证"
+
+#: internal/parser/tls.go:89
+msgid "Automatic certificate management"
+msgstr "自动证书管理"
+
+#: internal/parser/domains.go:55
+msgid "Bind a domain to an application"
+msgstr "将域名绑定到应用程序"
+
+#: internal/parser/resources.go:242
+msgid "Bind a resource for an application"
+msgstr "为应用程序绑定资源"
+
+#: internal/parser/volumes.go:92
+msgid "Bucket name in object storage"
+msgstr "对象存储中的存储桶名称"
+
+#: internal/parser/tls.go:125
+msgid "CA key ID"
+msgstr "CA密钥ID"
+
+#: internal/parser/tls.go:126
+msgid "CA key secret"
+msgstr "CA密钥机密"
+
+#: internal/parser/pts.go:160
+msgid "Clean process types of not used"
+msgstr "清理未使用的进程类型"
+
+#: internal/parser/auth.go:53
+msgid "Clear the current user session"
+msgstr "清除当前用户会话"
+
+#: internal/parser/apps.go:146
+msgid "Command execution timeout in seconds"
+msgstr "命令执行超时时间（秒）"
+
+#: internal/parser/certs.go:130
+msgid ""
+"Common name of the domain to attach to (needs to already be in the system)"
+msgstr "要附加到的域名通用名称（需已存在于系统中）"
+
+#: internal/parser/certs.go:155
+msgid "Common name of the domain to detach from"
+msgstr "要解绑的域名通用名称"
+
+#: internal/parser/tls.go:117
+msgid "Configure automatic certificate management issuer details"
+msgstr "配置自动证书管理颁发者详情"
+
+#: internal/parser/apps.go:45
+msgid "Create a new application"
+msgstr "创建新应用"
+
+#: internal/parser/resources.go:98
+msgid "Create a resource for the application"
+msgstr "为应用创建资源"
+
+#: internal/parser/routes.go:56
+msgid "Create a route for an application"
+msgstr "创建应用路由"
+
+#: internal/parser/volumes.go:62
+msgid "Create a volume for the application"
+msgstr "创建应用卷"
+
+#: internal/parser/workspaces.go:64
+msgid "Create a workspace"
+msgstr "创建工作空间"
+
+#: internal/parser/gateways.go:59
+msgid "Create gateways for an application"
+msgstr "创建应用网关"
+
+#: internal/parser/services.go:55
+msgid "Create service for an application"
+msgstr "创建应用服务"
+
+#: internal/parser/apps.go:46
+msgid ""
+"Creates a new application. If no <id> is provided, one will be generated "
+"automatically"
+msgstr "创建新应用。若未指定<id>，则自动生成一个唯一ID"
+
+#: internal/parser/builds.go:53
+msgid ""
+"Creates a new build of an application. Imports an <image> and deploys it to "
+"Drycc\n"
+"as a new release. If a Procfile or drycc.yaml is present in the current "
+"directory,\n"
+"it will be used as the default for this application."
+msgstr ""
+"创建应用新构建版本。导入<image>并部署到Drycc作为新版本\n"
+"若当前目录存在Procfile或drycc.yaml文件，\n"
+"将作为该应用的默认配置文件使用"
+
+#: internal/parser/services.go:56
+msgid ""
+"Creates extra service for an application and binds it to specific route of "
+"the main app domain"
+msgstr "为应用创建附加服务并绑定到主域名的特定路由"
+
+#: internal/parser/gateways.go:60
+msgid ""
+"Creates gateways for an application and binds it to allow listener of the "
+"main app domain"
+msgstr "为应用创建网关并绑定以监听主域名流量"
+
+#: internal/parser/routes.go:57
+msgid "Creates routes for an application, provides a way to route requests"
+msgstr "创建应用路由规则，实现请求分发"
+
+#: internal/parser/resources.go:217
+msgid "Delete a resource from the application"
+msgstr "从应用中删除资源"
+
+#: internal/parser/volumes.go:190
+msgid "Delete a volume from the application"
+msgstr "从应用中删除卷"
+
+#: internal/parser/workspaces.go:112
+msgid "Delete a workspace"
+msgstr "删除工作空间"
+
+#: internal/parser/ps.go:155
+msgid "Delete the selected processes"
+msgstr "删除选定进程"
+
+#: internal/parser/gateways.go:92
+msgid "Deletes specific gateway for application"
+msgstr "删除应用指定网关"
+
+#: internal/parser/releases.go:108
+msgid "Deploy the latest release by process types"
+msgstr "按进程类型部署最新版本"
+
+#: internal/parser/pts.go:65
+msgid "Describe a process type"
+msgstr "描述进程类型定义"
+
+#: internal/parser/apps.go:164
+msgid "Destroy an application"
+msgstr "销毁应用"
+
+#: internal/parser/certs.go:159
+msgid "Detach an SSL certificate from a domain"
+msgstr "从域名解绑SSL证书"
+
+#: internal/parser/certs.go:160
+msgid "Detach certificate from a domain"
+msgstr "从域名解绑证书"
+
+#: internal/parser/routes.go:215
+msgid "Dettach to gateway"
+msgstr "从网关解绑"
+
+#: internal/parser/autorollback.go:58
+msgid "Disable autorollback for an application"
+msgstr "禁用应用自动回滚"
+
+#: internal/parser/routing.go:58
+msgid "Disable routing for an application"
+msgstr "禁用应用路由功能"
+
+#: internal/parser/autodeploy.go:58
+msgid "Disables autodeploy for an app"
+msgstr "禁用应用自动部署"
+
+#: internal/parser/version.go:16
+msgid "Display client version"
+msgstr "显示客户端版本"
+
+#: cmd/root.go:39
+msgid "Display help information"
+msgstr "显示帮助信息"
+
+#: internal/parser/auth.go:71
+msgid "Display the current user"
+msgstr "显示当前登录用户"
+
+#: internal/parser/version.go:17
+msgid "Displays the client version"
+msgstr "显示客户端版本信息"
+
+#: internal/parser/auth.go:72
+msgid "Displays the currently logged in user"
+msgstr "显示当前登录用户信息"
+
+#: internal/parser/apps.go:58
+msgid "Do not create a 'drycc' git remote"
+msgstr "不创建drycc Git远程仓库"
+
+#: internal/parser/builds.go:67 internal/parser/builds.go:114
+msgid "Drycc config path to the application"
+msgstr "Drycc应用配置路径"
+
+#: internal/parser/autorollback.go:46
+msgid "Enable autorollback for an application"
+msgstr "启用应用自动回滚"
+
+#: internal/parser/workspaces.go:201
+msgid "Enable or disable alerts: true or false"
+msgstr "Enable or disable alerts: true or false"
+
+#: internal/parser/routing.go:46
+msgid "Enable routing for an application"
+msgstr "启用应用路由功能"
+
+#: internal/parser/autodeploy.go:46
+msgid "Enables autodeploy for an app"
+msgstr "启用应用自动部署"
+
+#: internal/parser/auth.go:43
+msgid "Enables or disables SSL certificate verification for API requests"
+msgstr "启用/禁用API请求的SSL证书验证"
+
+#: internal/parser/volumes.go:91
+msgid "Endpoint url for object storage service"
+msgstr "对象存储服务端点URL"
+
+#: internal/parser/ps.go:102
+msgid "Execute a command in a container"
+msgstr "在容器中执行命令"
+
+#: internal/parser/volumes.go:122
+msgid "Expand a volume capacity for the application"
+msgstr "扩展应用卷容量"
+
+#: internal/parser/config.go:145
+msgid ""
+"Extract all environment variables from an application or config group. for "
+"local use.\n"
+"\n"
+"The environmental variables can be piped into a file, 'drycc config pull > "
+"file',\n"
+"or stored locally in a file named .env. This file can be\n"
+"read by foreman to load the local environment for your app."
+msgstr ""
+"从应用或配置组提取所有环境变量（用于本地使用）。\n"
+"\n"
+"环境变量可通过管道输出到文件：'drycc config pull > 文件名'，\n"
+"或存储在本地名为.env的文件中。该文件可被foreman读取\n"
+"以加载应用的本地环境。"
+
+#: internal/parser/auth.go:78
+msgid "Fetch detailed user information"
+msgstr "获取用户详细信息"
+
+#: internal/parser/certs.go:111
+msgid "Fetch more detailed information about a certificate"
+msgstr "获取证书详细信息"
+
+#: internal/parser/tls.go:60
+msgid "Force TLS settings to HTTPS-only redirection"
+msgstr "强制TLS设置为HTTPS重定向"
+
+#: internal/parser/volumes.go:95
+msgid "Force a path-style endpoint to be used"
+msgstr "强制使用路径风格的端点"
+
+#: internal/parser/releases.go:116
+msgid "Force deploy"
+msgstr "强制部署"
+
+#: internal/parser/resources.go:148
+msgid "Get a resource's detail in the application"
+msgstr "获取应用资源详情"
+
+#: internal/parser/certs.go:110
+msgid "Get detailed informaton about the certificate"
+msgstr "获取证书详细信息"
+
+#: internal/parser/routes.go:123
+msgid "Get route rules"
+msgstr "获取路由规则"
+
+#: internal/parser/routes.go:124
+msgid "Get route rules for an application"
+msgstr "获取应用路由规则"
+
+#: internal/parser/healthchecks.go:187
+msgid "How often (in seconds) to perform the probe"
+msgstr "探测执行频率（秒）"
+
+#: internal/parser/workspaces.go:135
+msgid "Invite a user to a workspace"
+msgstr "邀请用户到工作空间"
+
+#: internal/parser/keys.go:30
+msgid "List SSH keys for the logged in user"
+msgstr "列出登录用户的SSH密钥"
+
+#: internal/parser/certs.go:40
+msgid "List SSL certificates for an application"
+msgstr "列出应用SSL证书"
+
+#: internal/parser/apps.go:69
+msgid "List accessible applications"
+msgstr "列出可访问应用"
+
+#: internal/parser/limits.go:141
+msgid "List all available limit plans"
+msgstr "列出所有可用配额方案"
+
+#: internal/parser/limits.go:119
+msgid "List all available limit specs"
+msgstr "列出所有可用配额规格"
+
+#: internal/parser/resources.go:66
+msgid "List all available plans for a resource service"
+msgstr "列出资源服务所有可用方案"
+
+#: internal/parser/resources.go:45
+msgid "List all available resource services"
+msgstr "列出所有资源服务"
+
+#: internal/parser/releases.go:53
+msgid "List an application's release history"
+msgstr "列出应用版本历史"
+
+#: internal/parser/gateways.go:36
+msgid "List application gateways"
+msgstr "列出应用网关"
+
+#: internal/parser/ps.go:37
+msgid "List application pods"
+msgstr "列出应用Pod"
+
+#: internal/parser/pts.go:38
+msgid "List application process types"
+msgstr "列出应用进程类型"
+
+#: internal/parser/routes.go:100
+msgid "List application routes"
+msgstr "列出应用路由"
+
+#: internal/parser/services.go:76
+msgid "List application services"
+msgstr "列出应用服务"
+
+#: internal/parser/labels.go:34
+msgid "List application's labels"
+msgstr "列出应用标签"
+
+#: internal/parser/autoscale.go:34
+msgid "List autoscale options for an application"
+msgstr "列出应用自动扩缩容选项"
+
+#: internal/parser/domains.go:36
+msgid "List domains bound to an application"
+msgstr "列出绑定到应用的域名"
+
+#: internal/parser/healthchecks.go:50
+msgid "List healthchecks for an application"
+msgstr "列出应用健康检查配置"
+
+#: internal/parser/lifecycles.go:50
+msgid "List lifecycles for an application"
+msgstr "列出应用生命周期配置"
+
+#: internal/parser/registry.go:44
+msgid "List registry info for an app"
+msgstr "列出应用镜像仓库信息"
+
+#: internal/parser/limits.go:41
+msgid "List resource limits for an app"
+msgstr "列出应用资源配额"
+
+#: internal/parser/timeouts.go:38
+msgid "List resource timeouts for an app"
+msgstr "列出应用资源超时设置"
+
+#: internal/parser/resources.go:120
+msgid "List resources in the application"
+msgstr "列出应用资源"
+
+#: internal/parser/limits.go:118
+msgid "List specification information of the server"
+msgstr "列出服务器规格信息"
+
+#: internal/parser/tags.go:52
+msgid "List tags for an application"
+msgstr "列出应用标签"
+
+#: internal/parser/volumes.go:138
+msgid "List volumes in the application"
+msgstr "列出应用卷"
+
+#: internal/parser/apps.go:70
+msgid "Lists applications visible to the current user"
+msgstr "列出当前用户可见的应用"
+
+#: internal/parser/gateways.go:37
+msgid "Lists gateways for an application"
+msgstr "列出应用网关"
+
+#: internal/parser/pts.go:39
+msgid "Lists process types servicing an application"
+msgstr "列出应用进程类型"
+
+#: internal/parser/ps.go:38
+msgid "Lists processes servicing an application"
+msgstr "列出应用运行进程"
+
+#: internal/parser/registry.go:45
+msgid "Lists registry information for an application"
+msgstr "列出应用镜像仓库信息"
+
+#: internal/parser/releases.go:54
+msgid "Lists release history for an application"
+msgstr "列出应用版本历史记录"
+
+#: internal/parser/tokens.go:33
+msgid "Lists tokens visible to the current controller"
+msgstr "列出控制器可见的令牌"
+
+#: internal/parser/workspaces.go:39
+msgid "Lists workspaces for current user"
+msgstr "列出当前用户的工作空间"
+
+#: internal/parser/auth.go:35
+msgid "Logs in by authenticating against a controller"
+msgstr "通过控制器身份验证登录"
+
+#: internal/parser/auth.go:54
+msgid "Logs out from a controller and clears the user session"
+msgstr "退出控制器并清除用户会话"
+
+#: internal/parser/certs.go:15
+msgid "Manage SSL endpoints for an app"
+msgstr "管理应用SSL端点"
+
+#: internal/parser/tls.go:17
+msgid "Manage TLS/SSL settings for applications"
+msgstr "管理应用TLS/SSL设置"
+
+#: internal/parser/domains.go:14
+msgid "Manage and assign domain names to your applications"
+msgstr "管理并分配域名到应用"
+
+#: internal/parser/healthchecks.go:24
+msgid "Manage application healthchecks"
+msgstr "管理应用健康检查"
+
+#: internal/parser/lifecycles.go:24
+msgid "Manage application lifecycles"
+msgstr "管理应用生命周期"
+
+#: internal/parser/apps.go:18
+msgid "Manage applications used to provide services"
+msgstr "管理服务型应用"
+
+#: internal/parser/auth.go:13
+msgid "Manage authentication"
+msgstr "管理身份验证"
+
+#: internal/parser/autodeploy.go:14
+msgid "Manage autodeploy if or not for applications"
+msgstr "管理应用自动部署开关"
+
+#: internal/parser/autorollback.go:14
+msgid "Manage autorollback if or not for application"
+msgstr "管理应用自动回滚开关"
+
+#: internal/parser/autoscale.go:15
+msgid "Manage autoscale for applications"
+msgstr "管理应用自动扩缩容"
+
+#: internal/parser/builds.go:15
+msgid "Manage builds created using 'git push'"
+msgstr "管理通过git push创建的构建版本"
+
+#: internal/parser/config.go:23
+msgid "Manage environment variables that define app config"
+msgstr "管理定义应用配置的环境变量"
+
+#: internal/parser/gateways.go:14
+msgid "Manage gateways for your applications"
+msgstr "管理应用网关"
+
+#: internal/parser/git.go:14
+msgid "Manage git for applications"
+msgstr "管理应用Git配置"
+
+#: internal/parser/labels.go:15
+msgid "Manage labels of application"
+msgstr "管理应用标签"
+
+#: internal/parser/timeouts.go:15
+msgid "Manage pods termination grace period"
+msgstr "管理Pod终止宽限期"
+
+#: internal/parser/registry.go:20
+msgid "Manage private registry information for your application"
+msgstr "管理应用私有镜像仓库信息"
+
+#: internal/parser/pts.go:15
+msgid "Manage process types inside an app"
+msgstr "管理应用进程类型"
+
+#: internal/parser/ps.go:15
+msgid "Manage processes inside an app"
+msgstr "管理应用进程"
+
+#: internal/parser/releases.go:24
+msgid "Manage releases of an application"
+msgstr "管理应用版本"
+
+#: internal/parser/limits.go:15
+msgid "Manage resource limits for your application"
+msgstr "管理应用资源配额"
+
+#: internal/parser/resources.go:17
+msgid "Manage resources for your applications"
+msgstr "管理应用资源"
+
+#: internal/parser/routing.go:14
+msgid "Manage routability of an application"
+msgstr "管理应用路由能力"
+
+#: internal/parser/routes.go:20
+msgid "Manage routes for your applications"
+msgstr "管理应用路由规则"
+
+#: internal/parser/services.go:17
+msgid "Manage services for your applications"
+msgstr "管理应用服务"
+
+#: internal/parser/keys.go:15
+msgid "Manage ssh keys used for 'git push' deployments"
+msgstr "管理用于git push部署的SSH密钥"
+
+#: internal/parser/tags.go:19
+msgid "Manage tags for application containers"
+msgstr "管理应用容器标签"
+
+#: internal/parser/tokens.go:15
+msgid "Manage user tokens"
+msgstr "管理用户令牌"
+
+#: internal/parser/volumes.go:15
+msgid "Manage volumes for your applications"
+msgstr "管理应用卷"
+
+#: internal/parser/workspaces.go:17
+msgid "Manage workspaces"
+msgstr "管理工作空间"
+
+#: internal/parser/autoscale.go:73
+msgid "Max replicas to scale up to"
+msgstr "可扩展的最大副本数"
+
+#: internal/parser/apps.go:23 internal/parser/apps.go:76
+msgid "Maximum number of results to display"
+msgstr "最多显示结果数量"
+
+#: internal/parser/config.go:197
+msgid "Merge config values"
+msgstr "合并配置"
+
+#: internal/parser/healthchecks.go:189
+msgid ""
+"Minimum consecutive successes for the probe to be considered failed after "
+"having succeeded"
+msgstr "探测成功后需连续失败的最小次数（判定为失败）"
+
+#: internal/parser/healthchecks.go:188
+msgid ""
+"Minimum consecutive successes for the probe to be considered successful "
+"after having failed"
+msgstr "探测失败后需连续成功的最小次数（判定为成功）"
+
+#: internal/parser/volumes.go:238
+msgid "Mount a volume to process of the application"
+msgstr "将卷挂载到应用进程"
+
+#: internal/parser/git.go:42 internal/parser/apps.go:57
+msgid "Name of remote to create"
+msgstr "要创建的远程名称"
+
+#: internal/parser/keys.go:52
+msgid "Name of the SSH key"
+msgstr "SSH密钥名称"
+
+#: internal/parser/certs.go:129
+msgid "Name of the certificate to attach domain to"
+msgstr "要附加域名的证书名称"
+
+#: internal/parser/certs.go:154
+msgid "Name of the certificate to deatch from a domain"
+msgstr "要解绑域名的证书名称"
+
+#: internal/parser/certs.go:59
+msgid "Name of the certificate to reference it by"
+msgstr "证书引用名称"
+
+#: internal/parser/services.go:52
+msgid ""
+"Number or name of the port to access on the pods targeted by the service"
+msgstr "服务目标Pod上要访问的端口号或名称"
+
+#: internal/parser/apps.go:102
+msgid "Open the application in a browser"
+msgstr "在浏览器中打开应用"
+
+#: internal/parser/apps.go:103
+msgid "Opens a URL to the application in the default browser"
+msgstr "通过默认浏览器打开应用URL"
+
+#: internal/parser/git.go:43
+msgid "Overwrite remote of the given name if it already exists"
+msgstr "若远程名称已存在则覆盖"
+
+#: internal/parser/ps.go:114
+msgid "Pass stdin to the container"
+msgstr "将标准输入传递给容器"
+
+#: internal/parser/volumes.go:89
+msgid "Path that is exported by the nfs server"
+msgstr "NFS服务器导出的路径"
+
+#: cmd/root.go:38
+msgid "Path to configuration file"
+msgstr "配置文件的路径"
+
+#: internal/parser/routes.go:188 internal/parser/routes.go:224
+msgid "Port is the network port, the gateway listener expects to receive"
+msgstr "网关监听器期望接收的网络端口"
+
+#: internal/parser/gateways.go:67
+msgid "Port is the network port, the listener expects to receive"
+msgstr "网关监听器期望接收的网络端口"
+
+#: internal/parser/gateways.go:100
+msgid "Port is the network port, the listener received"
+msgstr "网关监听器接收的网络端口"
+
+#: internal/parser/ps.go:133
+msgid "Print a detailed description of the selected process"
+msgstr "显示所选进程的详细信息"
+
+#: internal/parser/pts.go:66
+msgid "Print a detailed description of the selected process type"
+msgstr "显示所选进程类型的详细信息"
+
+#: internal/parser/builds.go:82
+msgid "Print information about a specific build"
+msgstr "显示特定构建版本信息"
+
+#: internal/parser/releases.go:79
+msgid "Print information about a specific release"
+msgstr "显示特定版本信息"
+
+#: internal/parser/volumes.go:164
+msgid "Print information about a volume"
+msgstr "显示卷信息"
+
+#: internal/parser/workspaces.go:85
+msgid "Print information about a workspace"
+msgstr "打印工作区的信息"
+
+#: internal/parser/builds.go:107
+msgid "Print process info about a specific build"
+msgstr "显示特定构建版本的进程信息"
+
+#: internal/parser/ps.go:63
+msgid "Print the logs for a container"
+msgstr "显示容器日志"
+
+#: internal/parser/ps.go:64
+msgid "Print the logs for a container in a pod or specified resource"
+msgstr "显示Pod或指定资源中的容器日志"
+
+#: internal/parser/ps.go:78
+msgid ""
+"Print the logs for the previous instance of the container in a pod if it "
+"exists"
+msgstr "若存在则显示Pod中容器的前一个实例日志"
+
+#: internal/parser/ps.go:77
+msgid "Print the logs of this container"
+msgstr "显示该容器的日志"
+
+#: internal/parser/autoscale.go:35
+msgid "Prints a list of autoscale options for the application"
+msgstr "显示应用自动扩缩容选项列表"
+
+#: internal/parser/labels.go:35
+msgid "Prints a list of labels of the application"
+msgstr "显示应用标签列表"
+
+#: internal/parser/releases.go:80
+msgid "Prints info about a particular release"
+msgstr "显示特定版本信息"
+
+#: internal/parser/apps.go:85
+msgid "Prints info about the current application"
+msgstr "显示当前应用信息"
+
+#: internal/parser/autodeploy.go:34
+msgid "Prints info about the current application's autodeploy if or not"
+msgstr "显示当前应用是否启用自动部署"
+
+#: internal/parser/autorollback.go:34
+msgid "Prints info about the current application's autorollback if or not"
+msgstr "显示当前应用是否启用自动回滚"
+
+#: internal/parser/config.go:159
+msgid "Prompts for each value to be overwritten"
+msgstr "逐项提示覆盖值"
+
+#: internal/parser/gateways.go:68
+msgid ""
+"Protocol specifies the network protocol this listener expects to receive. "
+"Supports TCP, UDP, TLS, HTTP, and HTTPS"
+msgstr "网关监听器期望接收的网络协议（支持TCP/UDP/TLS/HTTP/HTTPS）"
+
+#: internal/parser/gateways.go:101
+msgid "Protocol specifies the network protocol this listener received"
+msgstr "网关监听器接收的网络协议"
+
+#: internal/parser/tokens.go:55
+msgid "Provide a alias for controller authentication token"
+msgstr "提供控制器认证令牌别名"
+
+#: internal/parser/auth.go:42 internal/parser/tokens.go:68
+msgid "Provide a password for the account"
+msgstr "提供账户密码"
+
+#: internal/parser/auth.go:41 internal/parser/tokens.go:67
+msgid "Provide a username for the account"
+msgstr "提供账户用户名"
+
+#: internal/parser/config.go:144
+msgid "Pull environment variables to the path"
+msgstr "将环境变量拉取到指定路径"
+
+#: internal/parser/config.go:183
+msgid "Push environment variables from the path"
+msgstr "从指定路径推送环境变量"
+
+#: internal/parser/limits.go:149
+msgid "Query plans that meet the specified memory capacity, unit GiB"
+msgstr "查询符合指定内存容量（单位GiB）的配额方案"
+
+#: internal/parser/limits.go:148
+msgid "Query plans that meet the specified number of cpu cores"
+msgstr "查询符合指定CPU核心数的配额方案"
+
+#: internal/parser/limits.go:150
+msgid "Query plans that meet the specified spec id, see [specs] subcommand."
+msgstr "查询符合指定规格ID的配额方案，使用[specs]子命令查看可用规格"
+
+#: internal/parser/routes.go:247
+msgid "Remove a route from an application"
+msgstr "从应用移除路由"
+
+#: internal/parser/tokens.go:94
+msgid "Remove a token for controller authentication"
+msgstr "移除控制器认证令牌"
+
+#: internal/parser/workspaces.go:157
+msgid "Remove a user from a workspace"
+msgstr "从工作空间移除用户"
+
+#: internal/parser/keys.go:81
+msgid "Remove an SSH key"
+msgstr "移除SSH密钥"
+
+#: internal/parser/certs.go:88
+msgid "Remove an SSL certificate from an app"
+msgstr "从应用移除SSL证书"
+
+#: internal/parser/labels.go:90
+msgid "Remove application's label"
+msgstr "移除应用标签"
+
+#: internal/parser/gateways.go:91
+msgid "Remove gateways from an application"
+msgstr "从应用移除网关"
+
+#: internal/parser/services.go:103
+msgid "Remove service from an application"
+msgstr "从应用移除服务"
+
+#: internal/parser/keys.go:82
+msgid "Removes an SSH key for the logged in user"
+msgstr "移除登录用户的SSH密钥"
+
+#: internal/parser/git.go:52
+msgid "Removes git remote of application from repository"
+msgstr "从代码库移除应用的Git远程仓库"
+
+#: internal/parser/pts.go:91
+msgid "Restart an application or process types"
+msgstr "重启应用或进程类型"
+
+#: internal/parser/pts.go:90
+msgid "Restart application process types"
+msgstr "重启应用进程类型"
+
+#: internal/parser/apps.go:147
+msgid "Retention time of records in seconds"
+msgstr "日志记录保留时间（秒）"
+
+#: internal/parser/releases.go:136
+msgid "Return to a previous release"
+msgstr "回滚到之前版本"
+
+#: internal/parser/releases.go:137
+msgid "Rolls back to a previous application release"
+msgstr "回滚到应用之前的版本"
+
+#: internal/parser/routes.go:158
+msgid "Rules-file is the file name of rule to apply"
+msgstr "要应用的规则文件名"
+
+#: internal/parser/apps.go:136
+msgid "Run a command in an ephemeral app container"
+msgstr "在临时应用容器中运行命令"
+
+#: internal/parser/apps.go:137
+msgid "Runs a command inside an ephemeral app container"
+msgstr "在临时应用容器内部运行命令"
+
+#: internal/parser/builds.go:116
+msgid "Save process info to the local"
+msgstr "将进程信息保存到本地"
+
+#: internal/parser/pts.go:124
+msgid "Scale process types of replicas"
+msgstr "缩放进程类型的副本数"
+
+#: internal/parser/pts.go:125
+msgid "Scales an application's processes by type"
+msgstr "按类型缩放应用进程"
+
+#: internal/parser/limits.go:126
+msgid ""
+"Search keywords separated by commas, matching must satisfy all of the "
+"specified."
+msgstr "以逗号分隔的搜索关键词，需满足所有指定条件"
+
+#: internal/parser/volumes.go:94
+msgid "Secret access key for authentication"
+msgstr "认证密钥（Secret Access Key）"
+
+#: internal/parser/config.go:225
+msgid "Selects environment groups to attach an app ptype"
+msgstr "选择要附加配置组的环境组"
+
+#: internal/parser/config.go:250
+msgid "Selects environment groups to detach an app ptype"
+msgstr "选择要分离配置组的环境组"
+
+#: internal/parser/volumes.go:209
+msgid "Serve a WebDAV service for the volume"
+msgstr "为卷提供WebDAV服务"
+
+#: internal/parser/autoscale.go:64
+msgid "Set autoscale option per process type for an app"
+msgstr "设置应用各进程类型的自动扩缩容选项"
+
+#: internal/parser/config.go:85
+msgid "Set environment variables for an app"
+msgstr "设置应用环境变量"
+
+#: internal/parser/healthchecks.go:90
+msgid "Set healthchecks for an application"
+msgstr "设置应用健康检查配置"
+
+#: internal/parser/lifecycles.go:84
+msgid "Set lifecycles for an application"
+msgstr "设置应用生命周期"
+
+#: internal/parser/registry.go:76
+msgid "Set registry info for an app"
+msgstr "设置应用镜像仓库信息"
+
+#: internal/parser/limits.go:70
+msgid "Set resource limits for an app"
+msgstr "设置应用资源配额"
+
+#: internal/parser/timeouts.go:64
+msgid "Set resource timeouts for an app"
+msgstr "设置应用资源超时设置"
+
+#: internal/parser/routes.go:149
+msgid "Set route rules"
+msgstr "设置路由规则"
+
+#: internal/parser/routes.go:150
+msgid "Set route rules for an application"
+msgstr "设置应用路由规则"
+
+#: internal/parser/tags.go:86
+msgid "Set tags for an application"
+msgstr "设置应用标签"
+
+#: internal/parser/config.go:86
+msgid "Sets environment variables for an application or config group"
+msgstr "设置应用或配置组的环境变量"
+
+#: internal/parser/config.go:184
+msgid ""
+"Sets environment variables for an application or config group.\n"
+"\n"
+"This file can be read by foreman\n"
+"to load the local environment for your app. The file should be piped via\n"
+"stdin, 'drycc config push < .env', or using the --path option."
+msgstr ""
+"为应用程序或配置组设置环境变量。\n"
+"\n"
+"该文件可由 foreman 读取\n"
+"以加载应用程序的本地环境。文件应通过\n"
+"标准输入（'drycc config push < .env'）或使用 --path 选项进行管道传输。"
+
+#: internal/parser/healthchecks.go:91
+msgid ""
+"Sets healthchecks for an application.\n"
+"\n"
+"By default, Workflow only checks that the application starts in their "
+"Container. A health\n"
+"check may be added by configuring a health check probe for the application. "
+"The health\n"
+"checks are implemented as Kubernetes Container Probes. A 'startupProbe' "
+"'livenessProbe' \n"
+"and a 'readinessProbe' can be configured, and each probe can be of type "
+"'httpGet', 'exec' \n"
+"or 'tcpSocket' depending on the type of probe the Container requires.\n"
+"\n"
+"A 'startupProbe' indicates whether the application within the container is "
+"started.\n"
+"All other probes are disabled if a startup probe is provided, until it "
+"succeeds.\n"
+"If the startup probe fails, the container is subjected to its restart "
+"policy.\n"
+"\n"
+"A 'livenessProbe' is useful for applications running for long periods of "
+"time, eventually\n"
+"transitioning to broken states and cannot recover except by restarting "
+"them.\n"
+"\n"
+"Other times, a 'readinessProbe' is useful when the Container is only "
+"temporarily unable\n"
+"to serve, and will recover on its own. In this case, if a Container fails "
+"its 'readinessProbe'\n"
+", the Container will not be shut down, but rather the Container will stop "
+"receiving\n"
+"incoming requests.\n"
+"\n"
+"'httpGet' probes are just as it sounds: it performs a HTTP GET operation on "
+"the Container.\n"
+"A response code inside the 200-399 range is considered a pass. 'httpGet' "
+"probes accept a\n"
+"port number to perform the HTTP GET operation on the Container.\n"
+"\n"
+"'exec' probes run a command inside the Container to determine its health. An "
+"exit code of\n"
+"zero is considered a pass, while a non-zero status code is considered a "
+"fail. 'exec'\n"
+"probes accept a string of arguments to be run inside the Container.\n"
+"\n"
+"'tcpSocket' probes attempt to open a socket in the Container. The Container "
+"is only\n"
+"considered healthy if the check can establish a connection. 'tcpSocket' "
+"probes accept a\n"
+"port number to perform the socket connection on the Container.\n"
+msgstr ""
+"设置应用健康检查配置。\n"
+"\n"
+"默认情况下，Workflow 仅检查应用程序是否在其容器中启动。可以通过为应用程序配"
+"置\n"
+"健康检查探针来添加健康检查。健康检查通过 Kubernetes 容器探针实现。可以配置\n"
+"'startupProbe'、'livenessProbe' 和 'readinessProbe'，每种探针可以是 "
+"'httpGet'、\n"
+"'exec' 或 'tcpSocket' 类型，具体取决于容器所需的探针类型。\n"
+"\n"
+"'startupProbe' 指示容器内的应用程序是否已启动。如果提供了启动探针，则所有其"
+"他\n"
+"探针将被禁用，直到启动探针成功为止。如果启动探针失败，容器将根据其重启策略\n"
+"进行处理。\n"
+"\n"
+"'livenessProbe' 对于长时间运行的应用程序很有用，这些应用程序最终会进入故障状"
+"态，\n"
+"除非重启否则无法恢复。\n"
+"\n"
+"其他时候，当容器只是暂时无法服务并会自行恢复时，'readinessProbe' 很有用。\n"
+"在这种情况下，如果容器未能通过其 'readinessProbe'，容器不会被关闭，而是会\n"
+"停止接收传入请求。\n"
+"\n"
+"'httpGet' 探针如其名：它在容器上执行 HTTP GET 操作。200-399 范围内的响应代"
+"码\n"
+"被视为通过。'httpGet' 探针接受一个端口号以在容器上执行 HTTP GET 操作。\n"
+"\n"
+"'exec' 探针在容器内运行命令以确定其健康状况。退出代码为零被视为通过，而非零\n"
+"状态代码被视为失败。'exec' 探针接受要在容器内运行的参数字符串。\n"
+"\n"
+"'tcpSocket' 探针尝试在容器中打开套接字。仅当检查可以建立连接时，容器才被视"
+"为\n"
+"健康。'tcpSocket' 探针接受一个端口号以在容器上执行套接字连接。\n"
+
+#: internal/parser/labels.go:61
+msgid ""
+"Sets labels for an application.\n"
+"\n"
+"A label is a key/value pair used to label an application. This label is a "
+"general information for drycc user.\n"
+"Mostly used for administration/maintenance information, note for "
+"application. This information isn't send to scheduler."
+msgstr ""
+"设置应用标签。\n"
+"\n"
+"标签是键值对形式的元数据，用于标识应用。这些标签属于Drycc用户通用信息，主要用"
+"于管理/维护备注，不会发送给调度器。"
+
+#: internal/parser/lifecycles.go:85
+msgid ""
+"Sets lifecycle handlers for an application.\n"
+"\n"
+"Lifecycle handlers allow you to run actions at specific points in a "
+"container's lifecycle.\n"
+"Two types of lifecycle handlers are supported: 'postStart' and 'preStop'.\n"
+"\n"
+"A 'postStart' handler runs immediately after a container is created. If the "
+"handler fails,\n"
+"the container is terminated and restarted according to its restart policy.\n"
+"\n"
+"A 'preStop' handler runs immediately before a container is terminated. This "
+"handler must\n"
+"complete before the container termination signal is sent. If the handler "
+"fails, the\n"
+"container enters a termination grace period and is then forcefully "
+"terminated.\n"
+"\n"
+"Each lifecycle handler can be configured with one of the following action "
+"types:\n"
+"\n"
+"'httpGet': Performs an HTTP GET request to the container. The action is "
+"considered\n"
+"successful if the response status code is in the 200-399 range. 'httpGet' "
+"actions require\n"
+"a port number to specify where to send the HTTP request.\n"
+"\n"
+"'exec': Executes a command inside the container. The action is considered "
+"successful if\n"
+"the command exits with status code 0. 'exec' actions accept a list of "
+"command arguments\n"
+"to run inside the container.\n"
+"\n"
+"'tcpSocket': Attempts to open a TCP socket connection to the container. The "
+"action is\n"
+"considered successful if a connection can be established. 'tcpSocket' "
+"actions require a\n"
+"port number to specify where to attempt the connection.\n"
+"\n"
+"'sleep': Waits for a specified duration before proceeding. 'sleep' actions "
+"accept a\n"
+"duration in seconds to wait.\n"
+msgstr ""
+"设置应用生命周期处理器。\n"
+"\n"
+"生命周期处理器允许您在容器生命周期的特定时间点执行操作。\n"
+"支持两种类型的生命周期处理器：'postStart' 和 'preStop'。\n"
+"\n"
+"'postStart' 处理器在容器创建后立即运行。如果处理器执行失败，\n"
+"容器将根据其重启策略被终止并重启。\n"
+"\n"
+"'preStop' 处理器在容器终止前立即运行。此处理器必须在\n"
+"发送容器终止信号之前完成。如果处理器执行失败，\n"
+"容器将进入终止宽限期，随后被强制终止。\n"
+"\n"
+"每个生命周期处理器可配置为以下操作类型之一：\n"
+"\n"
+"'httpGet'：向容器执行 HTTP GET 请求。当响应状态码在 200-399 范围内时，\n"
+"操作被视为成功。'httpGet' 操作需要指定端口号以确定 HTTP 请求发送位置。\n"
+"\n"
+"'exec'：在容器内部执行命令。当命令退出状态码为 0 时，\n"
+"操作被视为成功。'exec' 操作接受要在容器内运行的命令参数列表。\n"
+"\n"
+"'tcpSocket'：尝试与容器建立 TCP 套接字连接。当连接可以建立时，\n"
+"操作被视为成功。'tcpSocket' 操作需要指定端口号以尝试连接。\n"
+"\n"
+"'sleep'：在继续执行前等待指定时长。'sleep' 操作接受\n"
+"以秒为单位的等待时长。\n"
+
+#: internal/parser/registry.go:77
+msgid ""
+"Sets registry information for an application. These credentials are the same "
+"as those used for\n"
+"'podmain login' to the private registry."
+msgstr ""
+"为应用设置镜像仓库信息。这些凭证与用于\n"
+"'podmain login' 登录私有仓库的凭证相同。"
+
+#: internal/parser/limits.go:71
+msgid ""
+"Sets resource limits for an application.\n"
+"\n"
+"A resource limit is a finite resource within a pod which we can apply\n"
+"restrictions through Kubernetes."
+msgstr ""
+"设置应用资源配额。\n"
+"\n"
+"资源配额是Pod内有限资源的约束，通过Kubernetes进行限制。"
+
+#: internal/parser/tags.go:87
+msgid ""
+"Sets tags for an application.\n"
+"\n"
+"A tag is a key/value pair used to tag an application's containers and is "
+"passed to the\n"
+"scheduler. This is often used to restrict workloads to specific hosts "
+"matching the\n"
+"scheduler-configured metadata."
+msgstr ""
+"设置应用容器标签。\n"
+"\n"
+"标签是键值对形式的元数据，用于标记应用容器并传递给调度器。常用于将工作负载限"
+"制到特定主机（需与调度器元数据匹配）。"
+
+#: internal/parser/timeouts.go:65
+msgid "Sets termination grace period for an application"
+msgstr "设置应用终止宽限期"
+
+#: cmd/root.go:74
+msgid "Shortcut Commands:"
+msgstr "快捷命令："
+
+#: internal/parser/certs.go:41
+msgid "Show certificate information for an SSL application"
+msgstr "显示SSL应用证书信息"
+
+#: internal/parser/resources.go:156
+msgid "Show extra details provided to resource"
+msgstr "显示资源附加详情"
+
+#: internal/parser/update.go:21
+msgid "Simulate an update, only print the version info."
+msgstr "模拟更新，仅显示版本信息"
+
+#: internal/parser/apps.go:171
+msgid ""
+"Skips the prompt for the application name. \n"
+"<app> is the uniquely identifiable name for the application"
+msgstr ""
+"跳过应用名称提示，直接使用指定名称。\n"
+"<app> 是应用的唯一标识名称"
+
+#: internal/parser/workspaces.go:119
+msgid "Skips the prompt for the workspace name"
+msgstr "跳过工作空间名称提示"
+
+#: internal/parser/ps.go:76
+msgid "Specify if the logs should be streamed"
+msgstr "指定是否实时流式输出日志"
+
+#: internal/parser/resources.go:112 internal/parser/resources.go:196
+msgid "Specify values in a YAML file. If set, params will be discard"
+msgstr "通过YAML文件指定参数值，若设置则忽略命令行参数"
+
+#: internal/parser/ps.go:113
+msgid "Stdin is a TTY"
+msgstr "标准输入为TTY终端"
+
+#: internal/parser/autoscale.go:74
+msgid "Target CPU utilization percentage"
+msgstr "目标CPU使用率百分比"
+
+#: cmd/root.go:29
+msgid "The Drycc command-line client issues API calls to a Drycc controller"
+msgstr "Drycc命令行客户端向Drycc控制器发出API调用"
+
+#: internal/parser/lifecycles.go:185
+msgid "The HTTP headers to send for 'httpGet' actions, separated by commas"
+msgstr "'httpGet' 操作发送的HTTP头部字段（逗号分隔）"
+
+#: internal/parser/healthchecks.go:184
+msgid "The HTTP headers to send for 'httpGet' probes, separated by commas"
+msgstr "HTTP探测时发送的HTTP头部字段（逗号分隔）"
+
+#: internal/parser/services.go:65 internal/parser/services.go:116
+msgid "The IP protocol for this port. Supports TCP, UDP, and SCTP"
+msgstr "该端口的IP协议（支持TCP/UDP/SCTP）"
+
+#: internal/parser/healthchecks.go:86
+msgid ""
+"The arguments required for the healthcheck probe. 'exec', accepts a list of "
+"arguments;\n"
+"                  'httpGet' and 'tcpSocket' accept a port number."
+msgstr ""
+"健康检查探测所需的参数：\n"
+"                  exec探测接受参数列表，httpGet和tcpSocket探测需指定端口号"
+
+#: internal/parser/lifecycles.go:80
+msgid ""
+"The arguments required for the lifecycle action. 'exec' accepts a list of "
+"arguments;\n"
+"                  'sleep' accepts duration in seconds; 'httpGet' and "
+"'tcpSocket' accept a port number."
+msgstr ""
+"生命周期操作所需的参数：\n"
+"                  'exec' 接受参数列表；'sleep' 接受以秒为单位的时长；\n"
+"                  'httpGet' 和 'tcpSocket' 需要指定端口号。"
+
+#: internal/parser/workspaces.go:70
+msgid "The contact email for the workspace"
+msgstr "工作空间的联系邮箱"
+
+#: internal/parser/workspaces.go:132
+msgid "The email address of the user to invite"
+msgstr "要邀请用户的邮箱地址"
+
+#: internal/parser/shortcuts.go:53 internal/parser/volumes.go:235
+#: internal/parser/apps.go:132
+msgid "The filesystem path"
+msgstr "文件系统路径"
+
+#: internal/parser/config.go:34 internal/parser/config.go:64
+msgid "The group for which the config needs to be listed"
+msgstr "要列出配置的组"
+
+#: internal/parser/config.go:157
+msgid "The group for which the config needs to be pull"
+msgstr "要拉取配置的组"
+
+#: internal/parser/config.go:195
+msgid "The group for which the config needs to be push"
+msgstr "要推送配置的组"
+
+#: internal/parser/config.go:93
+msgid "The group for which the config needs to be set"
+msgstr "要设置配置的组"
+
+#: internal/parser/config.go:122
+msgid "The group for which the config needs to be unset"
+msgstr "要清除配置的组"
+
+#: internal/parser/config.go:220
+msgid "The group that requires attach configurations"
+msgstr "需附加配置的组"
+
+#: internal/parser/config.go:247
+msgid "The group that requires detach configurations"
+msgstr "需分离配置的组"
+
+#: internal/parser/healthchecks.go:84
+msgid ""
+"The healthcheck probe, such as 'startupProbe' 'livenessProbe' or "
+"'readinessProbe'"
+msgstr ""
+"健康检查探测类型，如 'startupProbe'、'livenessProbe' 或 'readinessProbe'"
+
+#: internal/parser/volumes.go:88
+msgid "The hostname or ip address of the nfs server"
+msgstr "NFS服务器的主机名或IP地址"
+
+#: internal/parser/tokens.go:91
+msgid "The id of the token for controller authentication"
+msgstr "控制器认证的令牌ID"
+
+#: internal/parser/healthchecks.go:185
+msgid "The initial delay timeout for the probe"
+msgstr "探测初始延迟超时时间"
+
+#: internal/parser/lifecycles.go:78
+msgid "The lifecycle handler, such as 'postStart' 'preStop'"
+msgstr "生命周期处理器，如 'postStart' 或 'preStop'"
+
+#: internal/parser/limits.go:67
+msgid "The limit plan id to apply to the process type"
+msgstr "要应用到进程类型的配额方案ID"
+
+#: internal/parser/certs.go:23 internal/parser/certs.go:48
+#: internal/parser/domains.go:22 internal/parser/domains.go:43
+#: internal/parser/gateways.go:22 internal/parser/gateways.go:44
+#: internal/parser/limits.go:127 internal/parser/limits.go:151
+#: internal/parser/releases.go:33 internal/parser/releases.go:62
+#: internal/parser/resources.go:25 internal/parser/resources.go:127
+#: internal/parser/routes.go:28 internal/parser/routes.go:107
+#: internal/parser/tokens.go:22 internal/parser/tokens.go:40
+#: internal/parser/volumes.go:23 internal/parser/volumes.go:145
+#: internal/parser/workspaces.go:24 internal/parser/workspaces.go:46
+#: internal/parser/workspaces.go:93
+msgid "The maximum number of results to display"
+msgstr "最多显示结果数量"
+
+#: internal/parser/certs.go:106
+msgid "The name of the cert to get information from"
+msgstr "要查询信息的证书名称"
+
+#: internal/parser/certs.go:84
+msgid "The name of the cert to remove from the app"
+msgstr "要从应用移除的证书名称"
+
+#: internal/parser/workspaces.go:61 internal/parser/workspaces.go:82
+#: internal/parser/workspaces.go:109 internal/parser/workspaces.go:131
+#: internal/parser/workspaces.go:153 internal/parser/workspaces.go:181
+msgid "The name of the workspace"
+msgstr "工作空间名称"
+
+#: internal/parser/ps.go:75
+msgid "The number of lines to display, -1 showing all log lines"
+msgstr "要显示的日志行数（-1 表示显示全部）"
+
+#: internal/parser/pts.go:121 internal/parser/shortcuts.go:95
+msgid "The number of processes"
+msgstr "进程数量"
+
+#: internal/parser/healthchecks.go:186
+msgid "The number of seconds after which the probe times out"
+msgstr "探测超时秒数"
+
+#: internal/parser/registry.go:73
+msgid "The password of the registry"
+msgstr "镜像仓库密码"
+
+#: internal/parser/ps.go:99 internal/parser/ps.go:130 internal/parser/ps.go:152
+#: internal/parser/shortcuts.go:66
+msgid "The pod name for the application"
+msgstr "应用Pod名称"
+
+#: internal/parser/services.go:100
+msgid "The port exposed by this service"
+msgstr "服务暴露的端口"
+
+#: internal/parser/services.go:51
+msgid "The port that will be exposed by this service"
+msgstr "服务将暴露的端口"
+
+#: internal/parser/certs.go:61
+msgid "The private key of the SSL certificate"
+msgstr "SSL证书私钥"
+
+#: internal/parser/pts.go:62 internal/parser/pts.go:120
+#: internal/parser/pts.go:157 internal/parser/shortcuts.go:94
+#: internal/parser/tags.go:26 internal/parser/tags.go:60
+#: internal/parser/tags.go:100 internal/parser/tags.go:131
+#: internal/parser/volumes.go:234 internal/parser/volumes.go:259
+msgid "The process name as defined in your Procfile"
+msgstr "Procfile中定义的进程名称"
+
+#: internal/parser/releases.go:132
+msgid "The process name as defined in your Procfile, such as 'web'"
+msgstr "Procfile中定义的进程名称（如 'web'）"
+
+#: internal/parser/timeouts.go:60 internal/parser/timeouts.go:86
+msgid "The process type as defined in your Procfile"
+msgstr "Procfile中定义的进程类型"
+
+#: internal/parser/limits.go:66 internal/parser/limits.go:96
+msgid "The process type as defined in your Procfile, such as 'web' or 'worker'"
+msgstr "Procfile中定义的进程类型（如 'web' 或 'worker'）"
+
+#: internal/parser/autoscale.go:59
+msgid "The process type to add to the application's autoscale settings"
+msgstr "要添加到应用自动扩缩容设置的进程类型"
+
+#: internal/parser/autoscale.go:96
+msgid "The process type to remove from the application's autoscale settings"
+msgstr "要从应用自动扩缩容设置移除的进程类型"
+
+#: internal/parser/domains.go:62
+msgid "The ptype for domain"
+msgstr "域名对应的配置进程类型"
+
+#: internal/parser/registry.go:27 internal/parser/registry.go:51
+#: internal/parser/registry.go:86 internal/parser/registry.go:108
+msgid "The ptype for registry"
+msgstr "镜像仓库对应的配置进程类型"
+
+#: internal/parser/config.go:33 internal/parser/config.go:63
+msgid "The ptype for which the config needs to be listed"
+msgstr "要列出配置的进程类型"
+
+#: internal/parser/config.go:155
+msgid "The ptype for which the config needs to be pull"
+msgstr "要拉取配置的进程类型"
+
+#: internal/parser/config.go:194
+msgid "The ptype for which the config needs to be push"
+msgstr "要推送配置的进程类型"
+
+#: internal/parser/config.go:92
+msgid "The ptype for which the config needs to be set"
+msgstr "要设置配置的进程类型"
+
+#: internal/parser/config.go:121
+msgid "The ptype for which the config needs to be unset"
+msgstr "要清除配置的进程类型"
+
+#: internal/parser/healthchecks.go:178
+msgid "The ptype for which the health check needs to be applied"
+msgstr "要应用健康检查的进程类型"
+
+#: internal/parser/healthchecks.go:31
+msgid "The ptype for which the health check needs to be listed"
+msgstr "要列出健康检查的进程类型"
+
+#: internal/parser/healthchecks.go:224
+msgid "The ptype for which the health check needs to be removed"
+msgstr "要移除健康检查的进程类型"
+
+#: internal/parser/lifecycles.go:179
+msgid "The ptype for which the lifecycle handler needs to be applied"
+msgstr "要应用生命周期处理器的进程类型"
+
+#: internal/parser/lifecycles.go:221
+msgid "The ptype for which the lifecycle handler needs to be unset"
+msgstr "要清除生命周期处理器的进程类型"
+
+#: internal/parser/lifecycles.go:31
+msgid "The ptype for which the lifecycle needs to be listed"
+msgstr "要列出生命周期的进程类型"
+
+#: internal/parser/config.go:219
+msgid "The ptype that requires attach configurations"
+msgstr "需附加配置的进程类型"
+
+#: internal/parser/config.go:246
+msgid "The ptype that requires detach configurations"
+msgstr "需分离配置的进程类型"
+
+#: internal/parser/certs.go:60
+msgid "The public key of the SSL certificate"
+msgstr "SSL证书公钥"
+
+#: internal/parser/lifecycles.go:184
+msgid "The relative URL path for 'httpGet' actions"
+msgstr "'httpGet' 操作的相对URL路径"
+
+#: internal/parser/healthchecks.go:183
+msgid "The relative URL path for 'httpGet' probes"
+msgstr "HTTP探测的相对URL路径"
+
+#: internal/parser/releases.go:76 internal/parser/releases.go:133
+msgid "The release of the application, such as '1'"
+msgstr "应用版本（如 '1'）"
+
+#: internal/parser/resources.go:214 internal/parser/resources.go:239
+#: internal/parser/resources.go:261
+msgid "The resource instance alias name"
+msgstr "资源实例别名"
+
+#: internal/parser/resources.go:94 internal/parser/resources.go:177
+msgid "The resource instance parameters key"
+msgstr "资源实例参数键"
+
+#: internal/parser/resources.go:95 internal/parser/resources.go:178
+msgid "The resource instance parameters value"
+msgstr "资源实例参数值"
+
+#: internal/parser/resources.go:92 internal/parser/resources.go:175
+msgid "The resource's service"
+msgstr "资源所属服务"
+
+#: internal/parser/workspaces.go:200
+msgid "The role to assign: admin, member, or viewer"
+msgstr "The role to assign: admin, member, or viewer"
+
+#: internal/parser/routes.go:52
+msgid "The route kind, range: HTTPRoute,TCPRoute,UDPRoute,GRPCRoute,TLSRoute"
+msgstr "路由类型（可选：HTTPRoute、TCPRoute、UDPRoute、GRPCRoute、TLSRoute）"
+
+#: internal/parser/routes.go:53
+msgid "The route's backend, pattern: <service>:<port>,<weight>"
+msgstr "路由后端配置，格式：<service>:<port>,<weight>"
+
+#: internal/parser/resources.go:63
+msgid "The service name for plans"
+msgstr "服务名称"
+
+#: internal/parser/resources.go:93 internal/parser/resources.go:176
+msgid "The service's plan"
+msgstr "服务配额方案"
+
+#: internal/parser/shortcuts.go:54 internal/parser/apps.go:133
+msgid "The shell command to run inside the container"
+msgstr "容器内要执行的Shell命令"
+
+#: internal/parser/builds.go:65
+msgid "The stack name for the application"
+msgstr "应用堆栈名称"
+
+#: internal/parser/lifecycles.go:186
+msgid "The stop signal to send to the container"
+msgstr "发送给容器的停止信号"
+
+#: internal/parser/tags.go:82
+msgid "The tag key"
+msgstr "标签键"
+
+#: internal/parser/tags.go:119
+msgid "The tag key to unset"
+msgstr "要清除的标签键"
+
+#: internal/parser/tags.go:83
+msgid "The tag value"
+msgstr "标签值"
+
+#: internal/parser/routes.go:187 internal/parser/routes.go:223
+msgid "The unique name of the gaetway"
+msgstr "网关唯一名称"
+
+#: internal/parser/routes.go:51 internal/parser/routes.go:120
+#: internal/parser/routes.go:146 internal/parser/routes.go:176
+#: internal/parser/routes.go:212 internal/parser/routes.go:244
+msgid "The unique name of the route"
+msgstr "路由唯一名称"
+
+#: internal/parser/autodeploy.go:20 internal/parser/autorollback.go:19
+#: internal/parser/autoscale.go:21 internal/parser/builds.go:21
+#: internal/parser/builds.go:64 internal/parser/certs.go:22
+#: internal/parser/config.go:29 internal/parser/domains.go:21
+#: internal/parser/gateways.go:21 internal/parser/git.go:18
+#: internal/parser/labels.go:21 internal/parser/limits.go:21
+#: internal/parser/ps.go:21 internal/parser/ps.go:112 internal/parser/pts.go:21
+#: internal/parser/pts.go:134 internal/parser/registry.go:26
+#: internal/parser/releases.go:32 internal/parser/resources.go:24
+#: internal/parser/routes.go:27 internal/parser/routing.go:20
+#: internal/parser/services.go:23 internal/parser/tags.go:25
+#: internal/parser/timeouts.go:21 internal/parser/volumes.go:22
+#: internal/parser/apps.go:91 internal/parser/apps.go:109
+#: internal/parser/apps.go:144 internal/parser/apps.go:170
+#: internal/parser/apps.go:193
+msgid "The uniquely identifiable name for the application"
+msgstr "应用唯一标识名称"
+
+#: internal/parser/healthchecks.go:30 internal/parser/lifecycles.go:30
+msgid "The uniquely identifiable name of the application"
+msgstr "应用唯一名称"
+
+#: internal/parser/workspaces.go:182
+msgid "The username of the member to update"
+msgstr "The username of the member to update"
+
+#: internal/parser/registry.go:72
+msgid "The username of the registry"
+msgstr "镜像仓库用户名"
+
+#: internal/parser/workspaces.go:154
+msgid "The username to remove from the workspace"
+msgstr "要从工作空间移除的用户名"
+
+#: internal/parser/timeouts.go:61
+msgid "The value to apply to the process type in seconds"
+msgstr "要应用到进程类型的秒数值"
+
+#: internal/parser/builds.go:22 internal/parser/builds.go:87
+#: internal/parser/builds.go:112
+msgid "The version for which the build info needs to be displayed"
+msgstr "要查询对应版本的构建信息"
+
+#: internal/parser/config.go:35 internal/parser/config.go:65
+msgid "The version for which the config needs to be listed"
+msgstr "要查询对应版本的配置"
+
+#: internal/parser/config.go:156
+msgid "The version for which the config needs to be pull"
+msgstr "要拉取对应版本的配置"
+
+#: internal/parser/healthchecks.go:32 internal/parser/healthchecks.go:56
+msgid "The version for which the health check needs to be listed"
+msgstr "要查询对应版本的健康检查"
+
+#: internal/parser/lifecycles.go:32 internal/parser/lifecycles.go:56
+msgid "The version for which the lifecycle needs to be listed"
+msgstr "要列出生命周期的版本"
+
+#: internal/parser/limits.go:22 internal/parser/limits.go:47
+msgid "The version for which the limit needs to be listed"
+msgstr "要查询对应版本配额"
+
+#: internal/parser/registry.go:28 internal/parser/registry.go:52
+msgid "The version for which the registry needs to be listed"
+msgstr "要查询对应版本的镜像仓库信息"
+
+#: internal/parser/tags.go:27 internal/parser/tags.go:61
+msgid "The version for which the tag needs to be listed"
+msgstr "要查询对应版本的标签"
+
+#: internal/parser/timeouts.go:22 internal/parser/timeouts.go:44
+msgid "The version for which the timeout needs to be listed"
+msgstr "要查询对应版本的超时设置"
+
+#: internal/parser/shortcuts.go:52 internal/parser/volumes.go:58
+#: internal/parser/volumes.go:118 internal/parser/volumes.go:213
+#: internal/parser/volumes.go:233 internal/parser/volumes.go:258
+#: internal/parser/apps.go:131
+msgid "The volume name"
+msgstr "卷名称"
+
+#: internal/parser/volumes.go:161
+msgid "The volume name to be info"
+msgstr "要查询信息的卷名称"
+
+#: internal/parser/volumes.go:187
+msgid "The volume name to be removed"
+msgstr "要移除的卷名称"
+
+#: internal/parser/volumes.go:59 internal/parser/volumes.go:119
+msgid "The volume size, such as '500G'"
+msgstr "卷容量（如 '500G'）"
+
+#: internal/parser/volumes.go:87
+msgid "The volume type, such as csi, nfs, oss"
+msgstr "卷类型（如 csi, nfs, oss）"
+
+#: internal/parser/apps.go:56
+msgid "The workspace to create the app in"
+msgstr "在其中创建应用的工作空间"
+
+#: internal/parser/resources.go:91 internal/parser/resources.go:145
+#: internal/parser/resources.go:174
+msgid "This resource instance alias"
+msgstr "该资源实例别名"
+
+#: internal/parser/builds.go:68 internal/parser/builds.go:115
+#: internal/parser/pts.go:99 internal/parser/releases.go:117
+#: internal/parser/tokens.go:103
+msgid "To proceed, type \"yes\""
+msgstr "继续操作请输入 \"yes\""
+
+#: internal/parser/config.go:94 internal/parser/config.go:123
+#: internal/parser/config.go:198
+msgid "To proceed, type 'yes'"
+msgstr "继续操作请输入 'yes'"
+
+#: internal/parser/apps.go:184
+msgid "Transfer app to another workspace"
+msgstr "将应用转移到另一个工作空间"
+
+#: internal/parser/apps.go:185
+msgid "Transfer application to another workspace."
+msgstr "将应用转移到另一个工作空间。"
+
+#: internal/parser/autoscale.go:100
+msgid "Turn off autoscale for an app"
+msgstr "关闭应用自动扩缩容"
+
+#: internal/parser/autoscale.go:63
+msgid "Turn on autoscale for an app"
+msgstr "开启应用自动扩缩容"
+
+#: internal/parser/domains.go:75
+msgid "Unbinds a domain for an application"
+msgstr "解除应用与域名的绑定"
+
+#: internal/parser/volumes.go:262
+msgid "Unmount a volume from process of the application"
+msgstr "从应用进程卸载卷"
+
+#: internal/parser/autoscale.go:101
+msgid "Unset autoscale per process type for an app"
+msgstr "清除应用各进程类型的自动扩缩容设置"
+
+#: internal/parser/config.go:114
+msgid "Unset environment variables for an app"
+msgstr "清除应用环境变量"
+
+#: internal/parser/healthchecks.go:212
+msgid "Unset healthchecks for an application"
+msgstr "清除应用健康检查配置"
+
+#: internal/parser/lifecycles.go:209
+msgid "Unset lifecycle handler for an application"
+msgstr "清除应用生命周期处理器"
+
+#: internal/parser/registry.go:101
+msgid "Unset registry info for an app"
+msgstr "清除应用镜像仓库信息"
+
+#: internal/parser/limits.go:99
+msgid "Unset resource limits for an app"
+msgstr "清除应用资源配额"
+
+#: internal/parser/timeouts.go:89
+msgid "Unset resource timeouts for an app"
+msgstr "清除应用资源超时设置"
+
+#: internal/parser/tags.go:122
+msgid "Unset tags for an application"
+msgstr "清除应用标签"
+
+#: internal/parser/config.go:115
+msgid "Unsets an environment variable for an application or config group"
+msgstr "清除应用或配置组的环境变量"
+
+#: internal/parser/labels.go:91
+msgid "Unsets labels for an application"
+msgstr "清除应用标签"
+
+#: internal/parser/registry.go:102
+msgid "Unsets registry information for an application"
+msgstr "清除应用镜像仓库信息"
+
+#: internal/parser/timeouts.go:90
+msgid ""
+"Unsets timeouts for an application. Default value (30s) or set by drycc "
+"controller"
+msgstr "清除应用超时设置（将恢复默认值30秒或控制器设定值）"
+
+#: internal/parser/workspaces.go:185
+msgid "Update a member's role and/or alerts in a workspace"
+msgstr "Update a member's role and/or alerts in a workspace"
+
+#: internal/parser/resources.go:181
+msgid "Update a resource from the application"
+msgstr "更新应用资源"
+
+#: internal/parser/update.go:15 internal/parser/update.go:16
+msgid "Update workflow cli to latest release"
+msgstr "更新Workflow CLI到最新版本"
+
+#: internal/parser/tls.go:39
+msgid "View TLS settings information"
+msgstr "查看TLS设置信息"
+
+#: internal/parser/routing.go:34
+msgid "View application's routability information"
+msgstr "查看应用路由能力信息"
+
+#: internal/parser/autorollback.go:33
+msgid "View autorollback info for an application"
+msgstr "查看应用自动回滚信息"
+
+#: internal/parser/apps.go:84
+msgid "View info about an application"
+msgstr "查看应用信息"
+
+#: internal/parser/apps.go:145
+msgid "Volume mounts in format 'volume:path'"
+msgstr "卷挂载格式 '卷名:挂载路径'"
+
+#: internal/parser/builds.go:49
+msgid "default container image"
+msgstr "默认的容器镜像"
+
+#: internal/parser/builds.go:52
+msgid "imports an image and deploys as a new release"
+msgstr "导入镜像并部署为新版本"
+
+#: internal/parser/version.go:22
+msgid "list api and controller versions."
+msgstr "列出API和控制器版本"
+
+#: internal/parser/autoscale.go:72
+msgid "minimum replicas to keep around"
+msgstr "保留的最小副本数"
+
+#: internal/parser/services.go:99
+msgid "procfile type which should handle the request, e.g. webhooks"
+msgstr "要处理请求的Procfile类型（如webhooks）"
+
+#: internal/parser/services.go:49
+msgid ""
+"procfile type which should handle the request, e.g. webhooks (should be bind "
+"to the port PORT)\n"
+"             only single extra service per Porcfile type could be created."
+msgstr ""
+"要处理请求的Procfile类型（如webhooks），需绑定到PORT端口\n"
+"             每个Procfile类型只能创建一个附加服务"
+
+#: internal/parser/resources.go:225
+msgid ""
+"skips the prompt for the resource name. <resource> is the uniquely "
+"identifiable\n"
+"name for the resource"
+msgstr "跳过资源名称提示，直接使用指定名称。<resource>是资源的唯一标识名称"
+
+#: internal/parser/healthchecks.go:85
+msgid "the healthcheck probe type, such as 'httpGet', 'exec' or 'tcpSocket'"
+msgstr "健康检查探测类型（如 'httpGet'、'exec' 或 'tcpSocket'）"
+
+#: internal/parser/healthchecks.go:209
+msgid ""
+"the healthcheck probe, such as 'startupProbe' 'livenessProbe' or "
+"'readinessProbe'"
+msgstr "健康检查探测，如 'startupProbe'、'livenessProbe' 或 'readinessProbe'"
+
+#: internal/parser/labels.go:85
+msgid "the label key to unset, for example: \"git_repo\" or \"team\""
+msgstr "要清除的标签键（例如：\"git_repo\" 或 \"team\"）"
+
+#: internal/parser/labels.go:56
+msgid "the label key, for example: \"git_repo\" or \"team\""
+msgstr "标签键（例如：\"git_repo\" 或 \"team\"）"
+
+#: internal/parser/labels.go:57
+msgid ""
+"the label value, for example: \"https://github.com/drycc/workflow\" or "
+"\"frontend\""
+msgstr "标签值（例如：\"https://github.com/drycc/workflow\" 或 \"frontend\"）"
+
+#: internal/parser/lifecycles.go:79
+msgid ""
+"the lifecycle action type, such as 'httpGet', 'exec', 'sleep' or 'tcpSocket'"
+msgstr "生命周期操作类型，如 'httpGet'、'exec'、'sleep' 或 'tcpSocket'"
+
+#: internal/parser/lifecycles.go:206
+msgid "the lifecycle handler type, such as 'postStart' or 'preStop'"
+msgstr "生命周期处理器类型，如 'postStart' 或 'preStop'"
+
+#: internal/parser/resources.go:264
+msgid "unbind a resources for an application"
+msgstr "解除应用与资源的绑定"
diff --git a/pkg/i18n/translations/test/en_US/LC_MESSAGES/cli.mo b/pkg/i18n/translations/test/en_US/LC_MESSAGES/cli.mo
new file mode 100644
index 000000000..5487fb8cc
Binary files /dev/null and b/pkg/i18n/translations/test/en_US/LC_MESSAGES/cli.mo differ
diff --git a/pkg/i18n/translations/test/en_US/LC_MESSAGES/cli.po b/pkg/i18n/translations/test/en_US/LC_MESSAGES/cli.po
new file mode 100644
index 000000000..7e77c4300
--- /dev/null
+++ b/pkg/i18n/translations/test/en_US/LC_MESSAGES/cli.po
@@ -0,0 +1,28 @@
+# Test translations for unit tests.
+# Copyright (C) 2016
+# This file is distributed under the same license as the Kubernetes package.
+# FIRST AUTHOR brendan.d.burns@gmail.com, 2016.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: gettext-go-examples-hello\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2013-12-12 20:03+0000\n"
+"PO-Revision-Date: 2016-12-13 21:35-0800\n"
+"Last-Translator: Brendan Burns <brendan.d.burns@gmail.com>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 1.6.10\n"
+"X-Poedit-SourceCharset: UTF-8\n"
+"Language-Team: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Language: en\n"
+
+msgid "test_plural"
+msgid_plural "test_plural"
+msgstr[0] "there was %d item"
+msgstr[1] "there were %d items"
+
+msgid "test_string"
+msgstr "foo"
diff --git a/pkg/i18n/translations/test/zh_CN/LC_MESSAGES/cli.mo b/pkg/i18n/translations/test/zh_CN/LC_MESSAGES/cli.mo
new file mode 100644
index 000000000..44fe7b5d9
Binary files /dev/null and b/pkg/i18n/translations/test/zh_CN/LC_MESSAGES/cli.mo differ
diff --git a/pkg/i18n/translations/test/zh_CN/LC_MESSAGES/cli.po b/pkg/i18n/translations/test/zh_CN/LC_MESSAGES/cli.po
new file mode 100644
index 000000000..9944046ae
--- /dev/null
+++ b/pkg/i18n/translations/test/zh_CN/LC_MESSAGES/cli.po
@@ -0,0 +1,28 @@
+# Test translations for unit tests.
+# Copyright (C) 2016
+# This file is distributed under the same license as the Kubernetes package.
+# FIRST AUTHOR brendan.d.burns@gmail.com, 2016.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: gettext-go-examples-hello\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2013-12-12 20:03+0000\n"
+"PO-Revision-Date: 2016-12-13 22:12-0800\n"
+"Last-Translator: Brendan Burns <brendan.d.burns@gmail.com>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 1.6.10\n"
+"X-Poedit-SourceCharset: UTF-8\n"
+"Language-Team: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Language: en\n"
+
+msgid "test_plural"
+msgid_plural "test_plural"
+msgstr[0] "there was %d item"
+msgstr[1] "there were %d items"
+
+msgid "test_string"
+msgstr "baz"
diff --git a/settings/home_unix.go b/pkg/settings/home_unix.go
similarity index 80%
rename from settings/home_unix.go
rename to pkg/settings/home_unix.go
index 3e45a8421..54b2abb32 100644
--- a/settings/home_unix.go
+++ b/pkg/settings/home_unix.go
@@ -1,6 +1,7 @@
 //go:build linux || darwin
 // +build linux darwin
 
+// Package settings provides functionality for managing Drycc CLI settings.
 package settings
 
 import (
diff --git a/settings/home_unix_test.go b/pkg/settings/home_unix_test.go
similarity index 100%
rename from settings/home_unix_test.go
rename to pkg/settings/home_unix_test.go
diff --git a/settings/home_windows.go b/pkg/settings/home_windows.go
similarity index 100%
rename from settings/home_windows.go
rename to pkg/settings/home_windows.go
diff --git a/settings/home_windows_test.go b/pkg/settings/home_windows_test.go
similarity index 100%
rename from settings/home_windows_test.go
rename to pkg/settings/home_windows_test.go
diff --git a/settings/settings.go b/pkg/settings/settings.go
similarity index 82%
rename from settings/settings.go
rename to pkg/settings/settings.go
index 60ac34eab..1d5d8c653 100644
--- a/settings/settings.go
+++ b/pkg/settings/settings.go
@@ -56,7 +56,6 @@ Are you logged in? Use 'drycc login' or 'drycc register' to get started`, filena
 	}
 
 	c, err := drycc.New(sF.VerifySSL, sF.Controller, sF.Token)
-
 	if err != nil {
 		return nil, err
 	}
@@ -80,22 +79,32 @@ Are you logged in? Use 'drycc login' or 'drycc register' to get started`, filena
 
 // Save settings to a file
 func (s *Settings) Save(cf string) (string, error) {
-	settings := settingsFile{Username: s.Username, VerifySSL: s.Client.VerifySSL,
-		Controller: s.Client.ControllerURL.String(), Token: s.Client.Token, Limit: s.Limit}
+	settings := settingsFile{
+		Username: s.Username, VerifySSL: s.Client.VerifySSL,
+		Controller: s.Client.ControllerURL.String(), Token: s.Client.Token, Limit: s.Limit,
+	}
 
 	settingsContents, err := json.Marshal(settings)
-
 	if err != nil {
 		return "", err
 	}
 
-	if err = os.MkdirAll(filepath.Join(FindHome(), "/.drycc/"), 0700); err != nil {
-		return "", err
-	}
-
 	filename := locateSettingsFile(cf)
 
-	return filename, os.WriteFile(filename, settingsContents, 0600)
+	return filename, os.WriteFile(filename, settingsContents, 0o600)
+}
+
+// DryccHome returns the path to the user's settings path.
+func DryccHome() string {
+	dryccHome := filepath.Join(FindHome(), "/.drycc/")
+	_, err := os.Stat(dryccHome)
+	if os.IsNotExist(err) {
+		if err := os.MkdirAll(dryccHome, 0o700); err != nil {
+			panic(err)
+		}
+		return dryccHome
+	}
+	return dryccHome
 }
 
 // Delete user's settings file.
diff --git a/settings/settings_test.go b/pkg/settings/settings_test.go
similarity index 96%
rename from settings/settings_test.go
rename to pkg/settings/settings_test.go
index a708ede89..49a616e9f 100644
--- a/settings/settings_test.go
+++ b/pkg/settings/settings_test.go
@@ -16,19 +16,18 @@ const sFile string = `{"username":"t","ssl_verify":false,"controller":"http://fo
 
 func createTempProfile(contents string) (string, error) {
 	name, err := os.MkdirTemp("", "client")
-
 	if err != nil {
 		return "", err
 	}
 
 	file := filepath.Join(name, "test.json")
 
-	return file, os.WriteFile(file, []byte(contents), 0775)
+	return file, os.WriteFile(file, []byte(contents), 0o775)
 }
 
 type comparison struct {
-	key      interface{}
-	expected interface{}
+	key      any
+	expected any
 }
 
 func TestLoadSave(t *testing.T) {
@@ -40,7 +39,6 @@ func TestLoadSave(t *testing.T) {
 	}
 
 	s, err := Load(file)
-
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -83,7 +81,6 @@ func TestLoadSave(t *testing.T) {
 	s.Limit = 10
 
 	u, err := url.Parse("http://drycc.test")
-
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -103,7 +100,6 @@ func TestLoadSave(t *testing.T) {
 	}
 
 	s, err = Load(file)
-
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -176,7 +172,6 @@ func TestNotLoggedIn(t *testing.T) {
 	t.Parallel()
 
 	name, err := os.MkdirTemp("", "client")
-
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/pkg/settings/utils.go b/pkg/settings/utils.go
new file mode 100644
index 000000000..bbbcae4c7
--- /dev/null
+++ b/pkg/settings/utils.go
@@ -0,0 +1,25 @@
+package settings
+
+import (
+	"os"
+	"path/filepath"
+	"regexp"
+	"strings"
+)
+
+var filepathRegex = regexp.MustCompile(`^.*[/\\].+\.json$`)
+
+func locateSettingsFile(cf string) string {
+	if strings.HasPrefix(cf, "~") {
+		homeDir, err := os.UserHomeDir()
+		if err != nil {
+			panic(err)
+		}
+		cf = homeDir + cf[1:]
+	}
+	cf = filepath.Clean(os.ExpandEnv(cf))
+	if filepathRegex.MatchString(cf) {
+		return cf
+	}
+	return filepath.Join(DryccHome(), "client.json")
+}
diff --git a/settings/utils_test.go b/pkg/settings/utils_test.go
similarity index 70%
rename from settings/utils_test.go
rename to pkg/settings/utils_test.go
index 93a91abc3..b6542c4db 100644
--- a/settings/utils_test.go
+++ b/pkg/settings/utils_test.go
@@ -14,11 +14,12 @@ type confgCases struct {
 }
 
 func TestSelectSettings(t *testing.T) {
-	t.Parallel()
+	homeDir := "/a/b/c"
+	SetHome(homeDir)
 	cases := []confgCases{
-		{"test", filepath.Join(FindHome(), ".drycc", "test.json")},
+		{"test", filepath.Join(FindHome(), ".drycc", "client.json")},
 		{"", filepath.Join(FindHome(), ".drycc", "client.json")},
-		{"~/test.json", "~/test.json"},
+		{"~/test.json", filepath.Join(FindHome(), "test.json")},
 		{"/opt/test.json", "/opt/test.json"},
 	}
 
@@ -29,5 +30,5 @@ func TestSelectSettings(t *testing.T) {
 	// Check that env variable is used.
 	location := "/test/test.json"
 	os.Setenv("DRYCC_PROFILE", location)
-	assert.Equal(t, locateSettingsFile(""), location, "case")
+	assert.Equal(t, locateSettingsFile(location), location, "case")
 }
diff --git a/pkg/ssh/ssh.go b/pkg/ssh/ssh.go
index 429279a48..16c83f079 100644
--- a/pkg/ssh/ssh.go
+++ b/pkg/ssh/ssh.go
@@ -5,9 +5,7 @@ import (
 	"regexp"
 )
 
-var (
-	pubKeyRegex = regexp.MustCompile("^(ssh-rsa|ssh-dss|ssh-ed25519|ecdsa-[^ ]+) ([^ ]+) ?(.*)")
-)
+var pubKeyRegex = regexp.MustCompile("^(ssh-rsa|ssh-dss|ssh-ed25519|ecdsa-[^ ]+) ([^ ]+) ?(.*)")
 
 // PubKeyInfo contains the information on an SSH public key
 type PubKeyInfo struct {
diff --git a/pkg/ssh/ssh_test.go b/pkg/ssh/ssh_test.go
index 20c063f4c..018467532 100644
--- a/pkg/ssh/ssh_test.go
+++ b/pkg/ssh/ssh_test.go
@@ -36,7 +36,6 @@ func TestParseValidSSHPubKey(t *testing.T) {
 		}
 
 	}
-
 }
 
 func TestParseInvalidSSHPubKey(t *testing.T) {
diff --git a/pkg/testutil/testutil.go b/pkg/testutil/testutil.go
index 41bd6fb79..fee703178 100644
--- a/pkg/testutil/testutil.go
+++ b/pkg/testutil/testutil.go
@@ -1,3 +1,4 @@
+// Package testutil provides utilities for testing the workflow CLI.
 package testutil
 
 import (
@@ -11,7 +12,7 @@ import (
 	"testing"
 
 	drycc "github.com/drycc/controller-sdk-go"
-	"github.com/drycc/workflow-cli/settings"
+	"github.com/drycc/workflow-cli/pkg/settings"
 )
 
 // TestServer represents a test HTTP server along with a path to a config file
@@ -69,7 +70,7 @@ func NewTestServerAndClient() (string, *TestServer, error) {
 }
 
 // AssertBody asserts the value of the body of a request.
-func AssertBody(t *testing.T, expected interface{}, req *http.Request) {
+func AssertBody(t *testing.T, expected any, req *http.Request) {
 	body, err := io.ReadAll(req.Body)
 	if err != nil {
 		t.Fatal(err)
@@ -102,3 +103,26 @@ func StripProgress(input string) string {
 func SetHeaders(w http.ResponseWriter) {
 	w.Header().Add("DRYCC_API_VERSION", drycc.APIVersion)
 }
+
+// AssertOutput compares multi-line text after trimming spaces and tabs from each line
+func AssertOutput(t *testing.T, expected string, actual string) {
+	t.Helper()
+	expectedLines := strings.Split(strings.Trim(expected, "\n"), "\n")
+	actualLines := strings.Split(strings.Trim(actual, "\n"), "\n")
+
+	// Trim spaces and tabs from each line
+	for i, line := range expectedLines {
+		expectedLines[i] = strings.Trim(line, " \t")
+	}
+	for i, line := range actualLines {
+		actualLines[i] = strings.Trim(line, " \t")
+	}
+
+	// Join lines back with newlines
+	expectedProcessed := strings.Join(expectedLines, "\n")
+	actualProcessed := strings.Join(actualLines, "\n")
+
+	if expectedProcessed != actualProcessed {
+		t.Errorf("Expected output:\n%s\n\nActual output:\n%s", expectedProcessed, actualProcessed)
+	}
+}
diff --git a/pkg/testutil/testutil_test.go b/pkg/testutil/testutil_test.go
index d6396986c..5d5430f9b 100644
--- a/pkg/testutil/testutil_test.go
+++ b/pkg/testutil/testutil_test.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"io"
 	"net/http"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -28,15 +29,14 @@ func TestStripProgress(t *testing.T) {
 
 // TestAssertBody ensures AssertBody correctly marshals into the interface.
 func TestAssertBody(t *testing.T) {
-
 	b := nopCloser{bytes.NewBufferString(`{"data":{"lorem":"ipsum"},"dolar":["sit","amet"]}`)}
 
 	sampleRequest := http.Request{
 		Body: b,
 	}
 
-	expected := map[string]interface{}{
-		"data": map[string]interface{}{
+	expected := map[string]any{
+		"data": map[string]any{
 			"lorem": "ipsum",
 		},
 		"dolar": []string{
@@ -47,3 +47,98 @@ func TestAssertBody(t *testing.T) {
 
 	AssertBody(t, expected, &sampleRequest)
 }
+
+// TestAssertOutput tests the AssertOutput method with various scenarios
+func TestAssertOutput(t *testing.T) {
+	tests := []struct {
+		name       string
+		expected   string
+		actual     string
+		shouldPass bool
+	}{
+		{
+			name:       "identical strings",
+			expected:   "line1\nline2\nline3",
+			actual:     "line1\nline2\nline3",
+			shouldPass: true,
+		},
+		{
+			name:       "trailing spaces should be ignored",
+			expected:   "line1\nline2  \nline3",
+			actual:     "line1\nline2\nline3",
+			shouldPass: true,
+		},
+		{
+			name:       "leading spaces should be ignored",
+			expected:   "line1\n  line2\nline3",
+			actual:     "line1\nline2\nline3",
+			shouldPass: true,
+		},
+		{
+			name:       "tabs should be ignored",
+			expected:   "line1\n\tline2\t\nline3",
+			actual:     "line1\nline2\nline3",
+			shouldPass: true,
+		},
+		{
+			name:       "mixed whitespace should be ignored",
+			expected:   "line1\n \t line2 \t \nline3",
+			actual:     "line1\nline2\nline3",
+			shouldPass: true,
+		},
+		{
+			name:       "different content should fail",
+			expected:   "line1\nline2\nline3",
+			actual:     "line1\nline2\nline4",
+			shouldPass: false,
+		},
+		{
+			name:       "different number of lines should fail",
+			expected:   "line1\nline2",
+			actual:     "line1\nline2\nline3",
+			shouldPass: false,
+		},
+		{
+			name:       "empty strings",
+			expected:   "",
+			actual:     "",
+			shouldPass: true,
+		},
+		{
+			name:       "single line with spaces",
+			expected:   "  hello world  ",
+			actual:     "hello world",
+			shouldPass: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// Test by comparing processed results directly
+			expectedLines := strings.Split(tt.expected, "\n")
+			actualLines := strings.Split(tt.actual, "\n")
+
+			// Trim spaces and tabs from each line
+			for i, line := range expectedLines {
+				expectedLines[i] = strings.Trim(line, " \t")
+			}
+			for i, line := range actualLines {
+				actualLines[i] = strings.Trim(line, " \t")
+			}
+
+			// Join lines back with newlines
+			expectedProcessed := strings.Join(expectedLines, "\n")
+			actualProcessed := strings.Join(actualLines, "\n")
+
+			if tt.shouldPass {
+				if expectedProcessed != actualProcessed {
+					t.Errorf("Expected processed output to match, but they don't:\nExpected: %q\nActual: %q", expectedProcessed, actualProcessed)
+				}
+			} else {
+				if expectedProcessed == actualProcessed {
+					t.Errorf("Expected processed output to differ, but they match: %q", expectedProcessed)
+				}
+			}
+		})
+	}
+}
diff --git a/pkg/webbrowser/webbrowser_linux.go b/pkg/webbrowser/webbrowser_linux.go
index bad49b353..856093046 100644
--- a/pkg/webbrowser/webbrowser_linux.go
+++ b/pkg/webbrowser/webbrowser_linux.go
@@ -1,6 +1,7 @@
 //go:build linux
 // +build linux
 
+// Package webbrowser provides functionality to open URLs in the default browser
 package webbrowser
 
 import (
diff --git a/scripts/build b/scripts/build
index 3d1561630..85e279bd9 100755
--- a/scripts/build
+++ b/scripts/build
@@ -1,5 +1,25 @@
 #!/usr/bin/env bash
 
+# Define target OS/arch combinations to build
+BUILD_TARGETS=(
+  "linux/amd64"
+  "linux/arm"
+  "linux/arm64"
+  "linux/riscv64"
+  "windows/amd64"
+  "windows/arm64"
+  "darwin/amd64"
+  "darwin/arm64"
+)
+
+# Define targets that should be compressed with UPX
+COMPRESS_TARGETS=(
+  "linux/amd64"
+  "linux/arm"
+  "linux/arm64"
+  "windows/amd64"
+)
+
 go-build(){
   CGO_ENABLED=0 \
     GOOS=$GOOS \
@@ -7,17 +27,17 @@ go-build(){
     go build \
     -o _dist/drycc-$1-$GOOS-$GOARCH \
     drycc.go
+  # Check if current target is in COMPRESS_TARGETS list
+  target="$GOOS/$GOARCH"
+  if [[ ${COMPRESS_TARGETS[@]/$target/} != ${COMPRESS_TARGETS[@]} ]]; then
+    upx --lzma --best _dist/drycc-$1-$GOOS-$GOARCH
+  fi
 }
 
-GOOS_LIST=("linux" "windows" "darwin")
-GOARCH_LIST=("386" "amd64" "arm" "arm64")
-EXCLUDE_TARGET=("darwin/386", "darwin/arm" "windows/arm64")
-
-for GOOS in ${GOOS_LIST[@]}; do
-  for GOARCH in ${GOARCH_LIST[@]}; do
-    target="$GOOS/$GOARCH"
-    if [[ ${EXCLUDE_TARGET[@]/$target/} == ${EXCLUDE_TARGET[@]} ]]; then
-      go-build "$1"
-    fi
-  done
+# Process each target
+for target in "${BUILD_TARGETS[@]}"; do
+  # Split target into OS and architecture
+  GOOS=${target%/*}
+  GOARCH=${target#*/}
+  go-build "$1"
 done
diff --git a/scripts/update-translations.sh b/scripts/update-translations.sh
new file mode 100755
index 000000000..883740930
--- /dev/null
+++ b/scripts/update-translations.sh
@@ -0,0 +1,132 @@
+#!/usr/bin/env bash
+
+# Copyright 2017 The Kubernetes Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# This script updates `pkg/i18n/translations/drycc/template.pot` and
+# generates/fixes .po and .mo files.
+# Usage: `update-translations.sh`.
+
+
+# Opposite of ensure-temp-dir()
+cleanup-temp-dir() {
+  rm -rf "${WORKFLOW_CLI_TEMP}"
+}
+
+# Create a temp dir that'll be deleted at the end of this bash session.
+#
+# Vars set:
+#   WORKFLOW_CLI_TEMP
+ensure-temp-dir() {
+  if [[ -z ${WORKFLOW_CLI_TEMP-} ]]; then
+    WORKFLOW_CLI_TEMP=$(mktemp -d 2>/dev/null || mktemp -d -t kubernetes.XXXXXX)
+    trap cleanup-temp-dir EXIT
+  fi
+}
+
+
+WORFLOW_CLI_ROOT=$(dirname "${BASH_SOURCE[0]}")/..
+
+TRANSLATIONS="pkg/i18n/translations"
+WORFLOW_CLI_FILES=()
+WORFLOW_CLI_DEFAULT_LOCATIONS=(
+  "cmd"
+  "internal/parser"
+)
+
+generate_pot="false"
+generate_mo="false"
+fix_translations="false"
+
+while getopts "hf:xkg" opt; do
+  case ${opt} in
+    h)
+      echo "$0 [-f files] [-x] [-k] [-g]"
+      echo " -f <file-path>: Files to process"
+      echo " -x extract strings to a POT file"
+      echo " -k fix .po files; deprecate translations by marking them obsolete and supply default messages"
+      echo " -g sort .po files and generate .mo files"
+      exit 0
+      ;;
+    f)
+      WORFLOW_CLI_FILES+=("${OPTARG}")
+      ;;
+    x)
+      generate_pot="true"
+      ;;
+    k)
+      fix_translations="true"
+      ;;
+    g)
+      generate_mo="true"
+      ;;
+    \?)
+      echo "[-f <files>] -x -g" >&2
+      exit 1
+      ;;
+  esac
+done
+
+if [[ ${#WORFLOW_CLI_FILES} -eq 0 ]]; then
+  WORFLOW_CLI_FILES+=("${WORFLOW_CLI_DEFAULT_LOCATIONS[@]}")
+fi
+
+if ! which go-xgettext > /dev/null; then
+  echo 'Can not find go-xgettext, install with:'
+  echo 'go install github.com/gosexy/gettext/go-xgettext@latest'
+  exit 1
+fi
+
+if ! which msgfmt > /dev/null; then
+  echo 'Can not find msgfmt, install with:'
+  echo 'apt-get install gettext'
+  exit 1
+fi
+
+if [[ "${generate_pot}" == "true" ]]; then
+  echo "Extracting strings to POT"
+  # shellcheck disable=SC2046
+  go-xgettext -k=i18n.T $(grep -lr "i18n.T" "${WORFLOW_CLI_FILES[@]}") > tmp.pot
+  perl -pi -e 's/CHARSET/UTF-8/' tmp.pot
+  perl -pi -e 's/\\(?!n"\n|")/\\\\/g' tmp.pot
+  ensure-temp-dir
+  if msgcat -s tmp.pot > "${WORKFLOW_CLI_TEMP}/template.pot"; then
+    mv "${WORKFLOW_CLI_TEMP}/template.pot" "${TRANSLATIONS}/drycc/template.pot"
+    rm tmp.pot
+  else
+    echo "Failed to update template.pot"
+    exit 1
+  fi
+fi
+
+if [[ "${fix_translations}" == "true" ]]; then
+  echo "Fixing .po files"
+  ensure-temp-dir
+  for PO_FILE in "${TRANSLATIONS}"/drycc/*/LC_MESSAGES/cli.po; do
+    TMP="${WORKFLOW_CLI_TEMP}/fix.po"
+    msgen "${TRANSLATIONS}/drycc/template.pot" | \
+      msgmerge --no-fuzzy-matching "${PO_FILE}" - > "${TMP}"
+    mv "${TMP}" "${PO_FILE}"
+  done
+fi
+
+if [[ "${generate_mo}" == "true" ]]; then
+  echo "Generating .po and .mo files"
+  for x in "${TRANSLATIONS}"/*/*/*/*.po; do
+    msgcat -s "${x}" > tmp.po
+    mv tmp.po "${x}"
+    echo "generating .mo file for: ${x}"
+    msgfmt "${x}" -o "$(dirname "${x}")/$(basename "${x}" .po).mo"
+  done
+fi
diff --git a/settings/utils.go b/settings/utils.go
deleted file mode 100644
index a7f4cc44b..000000000
--- a/settings/utils.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package settings
-
-import (
-	"os"
-	"path/filepath"
-	"regexp"
-)
-
-var filepathRegex = regexp.MustCompile(`^.*[/\\].+\.json$`)
-
-func locateSettingsFile(cf string) string {
-	if cf == "" {
-		if v, ok := os.LookupEnv("DRYCC_PROFILE"); ok {
-			cf = v
-		} else {
-			cf = "client"
-		}
-	}
-
-	// if path appears to be a filepath (contains a separator and ends in .json) don't alter the path
-	if filepathRegex.MatchString(cf) {
-		return cf
-	}
-
-	return filepath.Join(FindHome(), ".drycc", cf+".json")
-}
diff --git a/version/version.go b/version/version.go
index 2d56e6cd6..382096b34 100644
--- a/version/version.go
+++ b/version/version.go
@@ -1,3 +1,4 @@
+// Package version provides version information for the Drycc product
 package version
 
 // Version identifies the Drycc product revision.
diff --git a/versioning.mk b/versioning.mk
new file mode 100644
index 000000000..d90ae2529
--- /dev/null
+++ b/versioning.mk
@@ -0,0 +1,23 @@
+MUTABLE_VERSION ?= canary
+VERSION ?= git-$(shell git rev-parse --short HEAD)
+IMAGE_PREFIX ?= drycc
+
+IMAGE := ${DRYCC_REGISTRY}/${IMAGE_PREFIX}/${SHORT_NAME}:${VERSION}
+MUTABLE_IMAGE := ${DRYCC_REGISTRY}/${IMAGE_PREFIX}/${SHORT_NAME}:${MUTABLE_VERSION}
+
+info:
+	@echo "Build tag:       ${VERSION}"
+	@echo "Registry:        ${DRYCC_REGISTRY}"
+	@echo "Immutable tag:   ${IMAGE}"
+	@echo "Mutable tag:     ${MUTABLE_IMAGE}"
+
+.PHONY: podman-push
+podman-push: podman-immutable-push podman-mutable-push
+
+.PHONY: podman-immutable-push
+podman-immutable-push:
+	podman push ${IMAGE}
+
+.PHONY: podman-mutable-push
+podman-mutable-push:
+	podman push ${MUTABLE_IMAGE}
\ No newline at end of file
