From a30f769ad63954ba9061202e6c6e41c599a68621 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Mon, 28 Apr 2025 12:46:14 +0800
Subject: [PATCH 01/18] chore(image): add git to container image

---
 docker/Dockerfile                           | 22 +++++++--------------
 docker/{ => rootfs}/entrypoint.sh           | 10 ++++++++--
 docker/rootfs/etc/bash_completion.d/drycc   |  3 +++
 docker/rootfs/etc/bash_completion.d/ps1     |  3 +++
 docker/rootfs/etc/bash_completion.d/timeout |  5 +++++
 internal/utils/utils_test.go                |  6 ++++++
 pkg/git/git.go                              |  3 +++
 7 files changed, 35 insertions(+), 17 deletions(-)
 rename docker/{ => rootfs}/entrypoint.sh (66%)
 create mode 100755 docker/rootfs/etc/bash_completion.d/drycc
 create mode 100755 docker/rootfs/etc/bash_completion.d/ps1
 create mode 100755 docker/rootfs/etc/bash_completion.d/timeout

diff --git a/docker/Dockerfile b/docker/Dockerfile
index 45a442d31..7ac212f11 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -16,26 +16,18 @@ ENV DRYCC_UID=1001 \
 
 COPY --from=build /usr/local/bin/drycc /usr/local/bin/drycc
 
-ADD docker/entrypoint.sh /entrypoint.sh
+ADD docker/rootfs /
 
-RUN install-packages bash-completion \
+RUN install-packages bash-completion git vim \
   && groupadd drycc --gid ${DRYCC_GID} \
   && useradd drycc -u ${DRYCC_UID} -g ${DRYCC_GID} -s /bin/bash -m -d ${DRYCC_HOME_DIR} \
-  && mkdir -p /etc/bash_completion.d \
-  && drycc completion bash > /etc/bash_completion.d/drycc \
-  && chown ${DRYCC_GID}:${DRYCC_UID} /usr/local/bin \
+  && 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 \
-  && ln -s /usr/bin/ls /usr/local/bin/ls \
-  && ln -s /usr/bin/cat /usr/local/bin/cat \
-  && ln -s /usr/bin/sed /usr/local/bin/sed \
-  && ln -s /usr/bin/find /usr/local/bin/find \
-  && ln -s /usr/bin/bash /usr/local/bin/bash \
-  && ln -s /usr/bin/mkdir /usr/local/bin/mkdir \
-  && ln -s /usr/bin/sleep /usr/local/bin/sleep \
-  && ln -s /usr/bin/dircolors /usr/local/bin/dircolors \
-  && ln -s /usr/bin/tini /usr/local/bin/tini \
-  && ln -s /usr/bin/init-stack /usr/local/bin/init-stack
+  && ln /usr/bin/{ls,git,date,vim,cat,sed,clear,find,flock,bash,mkdir,sleep,dircolors} /usr/local/bin/ \
+  && apt autoremove python3 -yq \
+  && rm -rf /var/lib/apt/lists/* /sbin /usr/sbin /usr/bin
 
 ENTRYPOINT [ "/entrypoint.sh" ] 
 
diff --git a/docker/entrypoint.sh b/docker/rootfs/entrypoint.sh
similarity index 66%
rename from docker/entrypoint.sh
rename to docker/rootfs/entrypoint.sh
index 94503e6ca..1af748750 100755
--- a/docker/entrypoint.sh
+++ b/docker/rootfs/entrypoint.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/local/bin/bash
 
 if [ -n "$DRYCC_USER" ] && [ -n "$DRYCC_TOKEN" ] && [ -n "$DRYCC_ENDPOINT" ]; then
     mkdir -p ~/.drycc
@@ -19,9 +19,15 @@ _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 "$@"
\ No newline at end of file
+_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/internal/utils/utils_test.go b/internal/utils/utils_test.go
index a77a4f025..b8e2b8419 100644
--- a/internal/utils/utils_test.go
+++ b/internal/utils/utils_test.go
@@ -35,6 +35,12 @@ func TestLoadProject(t *testing.T) {
 
 	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")
diff --git a/pkg/git/git.go b/pkg/git/git.go
index 08add8d7d..a921052d2 100644
--- a/pkg/git/git.go
+++ b/pkg/git/git.go
@@ -108,6 +108,9 @@ func DetectAppName(cmd Cmd, host string) (string, error) {
 
 	// 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
 	}

From a1ac6b86122c47ec247447017124a484018462b6 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Mon, 19 May 2025 13:54:25 +0800
Subject: [PATCH 02/18] chore(woodpecker): use new plugin-release

---
 .woodpecker/build-binary.yml | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/.woodpecker/build-binary.yml b/.woodpecker/build-binary.yml
index 0aede2358..1ab037ced 100644
--- a/.woodpecker/build-binary.yml
+++ b/.woodpecker/build-binary.yml
@@ -28,18 +28,19 @@ steps:
   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 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 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
+      docker.io/woodpeckerci/plugin-release
   environment:
     GITHUB_TOKEN:
       from_secret: github_token

From 485fb7a47230f793c16d6fa26ba2e7626fd6c32b Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Thu, 22 May 2025 16:22:37 +0800
Subject: [PATCH 03/18] docs(workflow-cli): add builds create args

---
 internal/parser/builds.go                     |  11 +++++++++--
 .../drycc/en_US/LC_MESSAGES/cli.po            |   4 ++++
 pkg/i18n/translations/drycc/template.pot      |   4 ++++
 .../drycc/zh_CN/LC_MESSAGES/cli.mo            | Bin 42725 -> 42786 bytes
 .../drycc/zh_CN/LC_MESSAGES/cli.po            |   4 ++++
 5 files changed, 21 insertions(+), 2 deletions(-)

diff --git a/internal/parser/builds.go b/internal/parser/builds.go
index 8a267300b..154290809 100644
--- a/internal/parser/builds.go
+++ b/internal/parser/builds.go
@@ -3,6 +3,7 @@ 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"
 )
@@ -39,8 +40,14 @@ func buildsCreate(cmdr *commands.DryccCmd) *cobra.Command {
 		confirm   string
 	}
 	cmd := &cobra.Command{
-		Use:   `create <image>`,
-		Args:  cobra.ExactArgs(1),
+		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,
diff --git a/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.po b/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.po
index 1e9692777..d393e8239 100644
--- a/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.po
+++ b/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.po
@@ -1677,6 +1677,10 @@ msgstr ""
 msgid "Volume mounts in format 'volume:path'"
 msgstr ""
 
+#: internal/parser/builds.go:42
+msgid "default container image"
+msgstr ""
+
 #: internal/parser/builds.go:44
 msgid "imports an image and deploys as a new release"
 msgstr ""
diff --git a/pkg/i18n/translations/drycc/template.pot b/pkg/i18n/translations/drycc/template.pot
index ae5ac06b6..ab77aacdd 100644
--- a/pkg/i18n/translations/drycc/template.pot
+++ b/pkg/i18n/translations/drycc/template.pot
@@ -1670,6 +1670,10 @@ msgstr ""
 msgid "Volume mounts in format 'volume:path'"
 msgstr ""
 
+#: internal/parser/builds.go:42
+msgid "default container image"
+msgstr ""
+
 #: internal/parser/builds.go:44
 msgid "imports an image and deploys as a new release"
 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
index 09c3dd08923ac37ef588f0c2ca505ae4326e3105..f89f42030677f0a77562548658c661ef6b008d2b 100644
GIT binary patch
diff --git a/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po b/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po
index dff88ba14..9f89f224b 100644
--- a/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po
+++ b/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po
@@ -1750,6 +1750,10 @@ msgstr "查看应用信息"
 msgid "Volume mounts in format 'volume:path'"
 msgstr "卷挂载格式 '卷名:挂载路径'"
 
+#: internal/parser/builds.go:42
+msgid "default container image"
+msgstr "默认的容器镜像"
+
 #: internal/parser/builds.go:44
 msgid "imports an image and deploys as a new release"
 msgstr "导入镜像并部署为新版本"

From b431724ce561407f457a784383aeebe74fb3ff5b Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Thu, 26 Jun 2025 16:30:48 +0800
Subject: [PATCH 04/18] chore(workflow-cli): remove apps logs

---
 go.mod                    |  2 +-
 go.sum                    |  4 ++--
 internal/commands/apps.go | 37 -------------------------------------
 internal/parser/apps.go   | 29 -----------------------------
 4 files changed, 3 insertions(+), 69 deletions(-)

diff --git a/go.mod b/go.mod
index 154056f34..4f6a2510f 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.24
 require (
 	github.com/chai2010/gettext-go v1.0.3
 	github.com/containerd/console v1.0.4
-	github.com/drycc/controller-sdk-go v0.0.0-20250326034724-1dafccaadf6b
+	github.com/drycc/controller-sdk-go v0.0.0-20250625050826-8be5432737be
 	github.com/drycc/pkg v0.0.0-20241223062248-04810c5faa4b
 	github.com/minio/selfupdate v0.6.0
 	github.com/olekukonko/tablewriter v0.0.5
diff --git a/go.sum b/go.sum
index aed5f3f90..14621474b 100644
--- a/go.sum
+++ b/go.sum
@@ -11,8 +11,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
 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-20250326034724-1dafccaadf6b h1:IA4sT1t/x/JtUm6F6T7j66ETAXwikYaUuNoQMmVp5ew=
-github.com/drycc/controller-sdk-go v0.0.0-20250326034724-1dafccaadf6b/go.mod h1:m4nhGeLJvR9bWuF12XhJ1c+hvkVY27OPN48F3l+AvAI=
+github.com/drycc/controller-sdk-go v0.0.0-20250625050826-8be5432737be h1:aIdgIpvYa6SuPQIV3C3UKNItMaTK3YDxP0sRlfrYSUE=
+github.com/drycc/controller-sdk-go v0.0.0-20250625050826-8be5432737be/go.mod h1:MbFxGlgRQUaVlThbn3DmB06EJsf+e3/O+qgbWmdvtoE=
 github.com/drycc/pkg v0.0.0-20241223062248-04810c5faa4b h1:wSIZheOVyeakCpI1i6xDta9e5YLZgNCqw7JfxC6R5MA=
 github.com/drycc/pkg v0.0.0-20241223062248-04810c5faa4b/go.mod h1:upGcZF/RCEqidGjSnWHh/txRC1I4GPsRGvF8/+bDfwU=
 github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
diff --git a/internal/commands/apps.go b/internal/commands/apps.go
index 29a8ce85b..31c19f8f8 100644
--- a/internal/commands/apps.go
+++ b/internal/commands/apps.go
@@ -2,24 +2,18 @@ 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/utils"
 	"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"
-	"golang.org/x/net/websocket"
 )
 
 // AppCreate creates an app.
@@ -205,37 +199,6 @@ func (d *DryccCmd) AppOpen(appID string) error {
 	return webbrowser.Webbrowser(u)
 }
 
-// AppLogs returns the logs from an app.
-func (d *DryccCmd) AppLogs(appID string, lines int, follow bool, timeout int) error {
-	appID, s, err := utils.LoadAppSettings(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 {
 	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
diff --git a/internal/parser/apps.go b/internal/parser/apps.go
index ef0484679..a14547dd1 100644
--- a/internal/parser/apps.go
+++ b/internal/parser/apps.go
@@ -25,7 +25,6 @@ func NewAppsCommand(cmdr *commands.DryccCmd) *cobra.Command {
 	cmd.AddCommand(appsList(cmdr))
 	cmd.AddCommand(appsInfo(cmdr))
 	cmd.AddCommand(appsOpen(cmdr))
-	cmd.AddCommand(appsLogs(cmdr))
 	cmd.AddCommand(appsRun(cmdr))
 	cmd.AddCommand(appsDestroy(cmdr))
 	cmd.AddCommand(appsTransfer(cmdr))
@@ -111,34 +110,6 @@ func appsOpen(cmdr *commands.DryccCmd) *cobra.Command {
 	return cmd
 }
 
-// AppsLogs creates the apps logs command
-func appsLogs(cmdr *commands.DryccCmd) *cobra.Command {
-	var flags struct {
-		app     string
-		lines   int
-		follow  bool
-		timeout int
-	}
-
-	cmd := &cobra.Command{
-		Use:   "logs",
-		Short: i18n.T("Retrieve application log events"),
-		Long:  i18n.T("Retrieves the most recent log events"),
-		RunE: func(_ *cobra.Command, _ []string) error {
-			return cmdr.AppLogs(app, flags.lines, flags.follow, flags.timeout)
-		},
-	}
-
-	cmd.Flags().StringVarP(&app, "app", "a", "", i18n.T("The uniquely identifiable name for the application"))
-	cmd.Flags().IntVarP(&flags.lines, "lines", "n", 300, i18n.T("The number of lines to display"))
-	cmd.Flags().BoolVarP(&flags.follow, "follow", "f", false, i18n.T("Specify if the logs should be streamed"))
-	cmd.Flags().IntVarP(&flags.timeout, "timeout", "t", 300, i18n.T("The max seconds of followz the log stream"))
-
-	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 {

From 079572723055332175427a46ddec3a189eddfd2a Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Fri, 12 Sep 2025 10:14:33 +0800
Subject: [PATCH 05/18] chore(go): bump go version to 1.25

---
 docker/Dockerfile           | 3 ++-
 docker/rootfs/entrypoint.sh | 5 ++---
 go.mod                      | 2 +-
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index 7ac212f11..69ed56a87 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -29,9 +29,10 @@ RUN install-packages bash-completion git vim \
   && apt autoremove python3 -yq \
   && rm -rf /var/lib/apt/lists/* /sbin /usr/sbin /usr/bin
 
-ENTRYPOINT [ "/entrypoint.sh" ] 
+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
index 1af748750..a88e8a1a5 100755
--- a/docker/rootfs/entrypoint.sh
+++ b/docker/rootfs/entrypoint.sh
@@ -1,9 +1,8 @@
 #!/usr/local/bin/bash
 
 if [ -n "$DRYCC_USER" ] && [ -n "$DRYCC_TOKEN" ] && [ -n "$DRYCC_ENDPOINT" ]; then
-    mkdir -p ~/.drycc
-    if [ ! -f ~/.drycc/client.json ]; then
-        cat > ~/.drycc/client.json <<EOF
+    if [ ! -f "$DRYCC_PROFILE" ]; then
+        cat > "$DRYCC_PROFILE" <<EOF
 {
     "username": "${DRYCC_USER}",
     "ssl_verify": ${DRYCC_SSL_VERIFY:-true},
diff --git a/go.mod b/go.mod
index 4f6a2510f..2ae4f28f5 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/drycc/workflow-cli
 
-go 1.24
+go 1.25
 
 require (
 	github.com/chai2010/gettext-go v1.0.3

From 8eb78c83f1aa86090061d13507c402114175d61a Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Wed, 17 Sep 2025 17:01:52 +0800
Subject: [PATCH 06/18] chore(workflow-cli): format go code

---
 cmd/root.go                                   |   9 +-
 go.mod                                        |   4 +-
 go.sum                                        |   8 +-
 internal/commands/apps.go                     |  24 +--
 internal/commands/apps_test.go                |  61 +++---
 internal/commands/auth.go                     |   2 -
 internal/commands/auth_test.go                |   2 +-
 internal/commands/autodeploy.go               |  11 +-
 internal/commands/autodeploy_test.go          |   4 +-
 internal/commands/autorollback.go             |  11 +-
 internal/commands/autorollback_test.go        |   4 +-
 internal/commands/autoscale.go                |  11 +-
 internal/commands/builds.go                   |  31 ++-
 internal/commands/builds_test.go              |  36 ++--
 internal/commands/certs.go                    |  18 +-
 internal/commands/certs_test.go               |   1 -
 internal/commands/commands.go                 |  30 +--
 internal/commands/config.go                   |  43 ++--
 internal/commands/config_test.go              |   2 +-
 internal/commands/domains.go                  |  11 +-
 internal/commands/gateways.go                 |  11 +-
 internal/commands/gateways_test.go            |   3 +-
 internal/commands/git.go                      |  17 +-
 internal/commands/healthchecks.go             |  10 +-
 internal/commands/keys.go                     |   9 +-
 internal/commands/keys_test.go                |  11 +-
 internal/commands/labels.go                   |  18 +-
 internal/commands/labels_test.go              |  12 +-
 internal/commands/limits.go                   |  32 ++-
 internal/commands/limits_test.go              |  91 +++++----
 internal/commands/perms.go                    |  10 +-
 internal/commands/ps.go                       |  21 +-
 internal/commands/pts.go                      |  15 +-
 internal/commands/registry.go                 |  20 +-
 internal/commands/registry_test.go            |  18 +-
 internal/commands/releases.go                 |  15 +-
 internal/commands/resources.go                |  44 ++--
 internal/commands/resources_test.go           |  55 +++--
 internal/commands/routes.go                   |  26 +--
 internal/commands/routes_test.go              |   1 +
 internal/commands/routing.go                  |  11 +-
 internal/commands/services.go                 |  11 +-
 internal/commands/tags.go                     |  17 +-
 internal/commands/tags_test.go                |  28 +--
 internal/commands/timeouts.go                 |  18 +-
 internal/commands/timeouts_test.go            |  69 +++----
 internal/commands/tls.go                      |  20 +-
 internal/commands/token.go                    |   8 +-
 internal/commands/token_test.go               |   3 +-
 internal/commands/users.go                    |   3 -
 internal/commands/utils.go                    |  12 +-
 internal/commands/version.go                  |   1 -
 internal/commands/volumes.go                  |  42 ++--
 internal/commands/volumes_test.go             |  50 ++---
 internal/completion/completion.go             | 193 ++++++++++++------
 internal/{utils/utils.go => loader/loader.go} |   5 +-
 .../utils_test.go => loader/loader_test.go}   |   3 +-
 internal/parser/apps.go                       |   4 +-
 internal/parser/autodeploy.go                 |   1 -
 internal/parser/builds.go                     |   1 +
 internal/parser/certs.go                      |   1 -
 internal/parser/flags.go                      |   8 +-
 internal/parser/git.go                        |   1 +
 internal/parser/healthchecks.go               |   2 +-
 internal/parser/keys.go                       |   1 +
 internal/parser/labels.go                     |   1 +
 internal/parser/limits.go                     |   1 +
 internal/parser/perms.go                      |   2 +-
 internal/parser/ps.go                         |   2 +-
 internal/parser/pts.go                        |   1 +
 internal/parser/registry.go                   |   1 +
 internal/parser/releases.go                   |   2 -
 internal/parser/resources.go                  |   1 +
 internal/parser/routes.go                     |   3 +-
 internal/parser/routing.go                    |   1 +
 internal/parser/services.go                   |   2 +-
 internal/parser/shortcuts.go                  |  20 +-
 internal/parser/tls.go                        |  10 +-
 internal/parser/update.go                     |   3 +-
 internal/parser/version.go                    |   2 +-
 internal/parser/volumes.go                    |  11 +-
 internal/template/template.go                 |   2 +
 pkg/git/git.go                                |   7 +-
 pkg/i18n/i18n.go                              |   5 +-
 pkg/settings/home_unix.go                     |   1 +
 pkg/settings/settings.go                      |  11 +-
 pkg/settings/settings_test.go                 |  11 +-
 pkg/settings/utils.go                         |  17 +-
 pkg/settings/utils_test.go                    |   4 +-
 pkg/ssh/ssh.go                                |   4 +-
 pkg/ssh/ssh_test.go                           |   1 -
 pkg/testutil/testutil.go                      |  26 ++-
 pkg/testutil/testutil_test.go                 | 101 ++++++++-
 pkg/webbrowser/webbrowser_linux.go            |   1 +
 version/version.go                            |   1 +
 95 files changed, 806 insertions(+), 724 deletions(-)
 rename internal/{utils/utils.go => loader/loader.go} (81%)
 rename internal/{utils/utils_test.go => loader/loader_test.go} (92%)

diff --git a/cmd/root.go b/cmd/root.go
index 29f0ca97f..09f7beeb4 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -1,3 +1,4 @@
+// Package cmd provides the root command for the Drycc CLI.
 /*
 Copyright © 2025 NAME HERE <EMAIL ADDRESS>
 */
@@ -13,6 +14,7 @@ import (
 	"github.com/spf13/cobra"
 )
 
+// NewDryccCommand creates the root command for the Drycc CLI.
 func NewDryccCommand() *cobra.Command {
 	var flags struct {
 		config  string
@@ -29,8 +31,11 @@ func NewDryccCommand() *cobra.Command {
 			cmdr = commands.DryccCmd{ConfigFile: flags.config, WOut: os.Stdout, WErr: os.Stderr, WIn: os.Stdin, Location: time.Local}
 		},
 	}
-
-	rootCmd.PersistentFlags().StringVarP(&flags.config, "config", "c", "~/.drycc/client.json", i18n.T("Path to configuration file"))
+	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))
diff --git a/go.mod b/go.mod
index 2ae4f28f5..d3a641215 100644
--- a/go.mod
+++ b/go.mod
@@ -5,8 +5,8 @@ go 1.25
 require (
 	github.com/chai2010/gettext-go v1.0.3
 	github.com/containerd/console v1.0.4
-	github.com/drycc/controller-sdk-go v0.0.0-20250625050826-8be5432737be
-	github.com/drycc/pkg v0.0.0-20241223062248-04810c5faa4b
+	github.com/drycc/controller-sdk-go v0.0.0-20250917070349-910603c54927
+	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/schollz/progressbar/v3 v3.18.0
diff --git a/go.sum b/go.sum
index 14621474b..1bd67dc1c 100644
--- a/go.sum
+++ b/go.sum
@@ -11,10 +11,10 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
 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-20250625050826-8be5432737be h1:aIdgIpvYa6SuPQIV3C3UKNItMaTK3YDxP0sRlfrYSUE=
-github.com/drycc/controller-sdk-go v0.0.0-20250625050826-8be5432737be/go.mod h1:MbFxGlgRQUaVlThbn3DmB06EJsf+e3/O+qgbWmdvtoE=
-github.com/drycc/pkg v0.0.0-20241223062248-04810c5faa4b h1:wSIZheOVyeakCpI1i6xDta9e5YLZgNCqw7JfxC6R5MA=
-github.com/drycc/pkg v0.0.0-20241223062248-04810c5faa4b/go.mod h1:upGcZF/RCEqidGjSnWHh/txRC1I4GPsRGvF8/+bDfwU=
+github.com/drycc/controller-sdk-go v0.0.0-20250917070349-910603c54927 h1:c1n5N5urQztpcBy93U+oBEWyRXXaa0k3PwCg9pzXqe0=
+github.com/drycc/controller-sdk-go v0.0.0-20250917070349-910603c54927/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=
diff --git a/internal/commands/apps.go b/internal/commands/apps.go
index 31c19f8f8..393b07f81 100644
--- a/internal/commands/apps.go
+++ b/internal/commands/apps.go
@@ -1,3 +1,4 @@
+// Package commands provides implementations for Drycc CLI commands.
 package commands
 
 import (
@@ -10,7 +11,7 @@ import (
 	"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/utils"
+	"github.com/drycc/workflow-cli/internal/loader"
 	"github.com/drycc/workflow-cli/pkg/git"
 	"github.com/drycc/workflow-cli/pkg/settings"
 	"github.com/drycc/workflow-cli/pkg/webbrowser"
@@ -59,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
 	}
@@ -91,8 +91,7 @@ func (d *DryccCmd) AppsList(results int) error {
 
 // AppInfo prints info about app.
 func (d *DryccCmd) AppInfo(appID string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -177,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 {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -192,7 +190,7 @@ 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
 	}
 
@@ -201,8 +199,7 @@ func (d *DryccCmd) AppOpen(appID string) error {
 
 // AppRun runs a one time command in the app.
 func (d *DryccCmd) AppRun(appID, command string, volumeVars []string, timeout, expires uint32) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -219,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 {
@@ -239,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
 		}
@@ -286,8 +281,7 @@ func (d *DryccCmd) AppDestroy(appID, confirm string) error {
 
 // AppTransfer transfers app ownership to another user.
 func (d *DryccCmd) AppTransfer(appID, username string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
diff --git a/internal/commands/apps_test.go b/internal/commands/apps_test.go
index dbddcd83f..3569e1cc0 100644
--- a/internal/commands/apps_test.go
+++ b/internal/commands/apps_test.go
@@ -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/stretchr/testify/assert"
 )
 
 type expandURLCases struct {
@@ -62,10 +61,10 @@ func TestAppsList(t *testing.T) {
 
 	err = cmdr.AppsList(-1)
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `ID             OWNER             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    
-`, "output")
+	testutil.AssertOutput(t, b.String(), `ID             OWNER             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) {
@@ -101,9 +100,9 @@ func TestAppsListLimit(t *testing.T) {
 
 	err = cmdr.AppsList(1)
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `ID             OWNER             CREATED                 UPDATED              
-lorem-ipsum    dolar-sit-amet    2016-08-22T17:40:16Z    2016-08-22T17:40:16Z    
-`, "output")
+	testutil.AssertOutput(t, b.String(), `ID             OWNER             CREATED                 UPDATED
+lorem-ipsum    dolar-sit-amet    2016-08-22T17:40:16Z    2016-08-22T17:40:16Z
+`)
 }
 
 func TestAppsInfo(t *testing.T) {
@@ -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                                  
-              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                            
-`, "output")
+	testutil.AssertOutput(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
+              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) {
@@ -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")
 }
@@ -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")
 }
 
diff --git a/internal/commands/auth.go b/internal/commands/auth.go
index 82d14d676..cc24705fa 100644
--- a/internal/commands/auth.go
+++ b/internal/commands/auth.go
@@ -9,7 +9,6 @@ import (
 // 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/internal/commands/auth_test.go b/internal/commands/auth_test.go
index 489aee12a..42e24feb1 100644
--- a/internal/commands/auth_test.go
+++ b/internal/commands/auth_test.go
@@ -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
index 21b47782e..4194f7aed 100644
--- a/internal/commands/autodeploy.go
+++ b/internal/commands/autodeploy.go
@@ -3,13 +3,12 @@ package commands
 import (
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/appsettings"
-	"github.com/drycc/workflow-cli/internal/utils"
+	"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 := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -29,8 +28,7 @@ func (d *DryccCmd) AutodeployInfo(appID string) error {
 
 // AutodeployEnable enables an app when deploy failed
 func (d *DryccCmd) AutodeployEnable(appID string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -53,8 +51,7 @@ func (d *DryccCmd) AutodeployEnable(appID string) error {
 
 // AutodeployDisable disables an app when deploy failed
 func (d *DryccCmd) AutodeployDisable(appID string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
diff --git a/internal/commands/autodeploy_test.go b/internal/commands/autodeploy_test.go
index 63bfeb5c1..a159787a2 100644
--- a/internal/commands/autodeploy_test.go
+++ b/internal/commands/autodeploy_test.go
@@ -92,7 +92,7 @@ func TestAutodeployEnable(t *testing.T) {
 
 	err = cmdr.AutodeployEnable("lothlorien")
 	assert.NoError(t, err)
-	assert.Equal(t, testutil.StripProgress(b.String()), "Enabling autodeploy for lothlorien... done\n", "output")
+	testutil.AssertOutput(t, testutil.StripProgress(b.String()), "Enabling autodeploy for lothlorien... done\n")
 }
 
 func TestAutodeployDisable(t *testing.T) {
@@ -114,5 +114,5 @@ func TestAutodeployDisable(t *testing.T) {
 
 	err = cmdr.AutodeployDisable("bree")
 	assert.NoError(t, err)
-	assert.Equal(t, testutil.StripProgress(b.String()), "Disabling autodeploy for bree... done\n", "output")
+	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
index 258fee35c..8b4b7c412 100644
--- a/internal/commands/autorollback.go
+++ b/internal/commands/autorollback.go
@@ -3,13 +3,12 @@ package commands
 import (
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/appsettings"
-	"github.com/drycc/workflow-cli/internal/utils"
+	"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 := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -29,8 +28,7 @@ func (d *DryccCmd) AutorollbackInfo(appID string) error {
 
 // AutorollbackEnable enables an app when deploy failed
 func (d *DryccCmd) AutorollbackEnable(appID string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -53,8 +51,7 @@ func (d *DryccCmd) AutorollbackEnable(appID string) error {
 
 // AutorollbackDisable disables an app when deploy failed
 func (d *DryccCmd) AutorollbackDisable(appID string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
diff --git a/internal/commands/autorollback_test.go b/internal/commands/autorollback_test.go
index 4349b4dd2..0407bf30b 100644
--- a/internal/commands/autorollback_test.go
+++ b/internal/commands/autorollback_test.go
@@ -92,7 +92,7 @@ func TestAutorollbackEnable(t *testing.T) {
 
 	err = cmdr.AutorollbackEnable("lothlorien")
 	assert.NoError(t, err)
-	assert.Equal(t, testutil.StripProgress(b.String()), "Enabling autorollback for lothlorien... done\n", "output")
+	testutil.AssertOutput(t, testutil.StripProgress(b.String()), "Enabling autorollback for lothlorien... done\n")
 }
 
 func TestAutorollbackDisable(t *testing.T) {
@@ -114,5 +114,5 @@ func TestAutorollbackDisable(t *testing.T) {
 
 	err = cmdr.AutorollbackDisable("bree")
 	assert.NoError(t, err)
-	assert.Equal(t, testutil.StripProgress(b.String()), "Disabling autorollback for bree... done\n", "output")
+	testutil.AssertOutput(t, testutil.StripProgress(b.String()), "Disabling autorollback for bree... done\n")
 }
diff --git a/internal/commands/autoscale.go b/internal/commands/autoscale.go
index 46dbdea1c..6322be1dd 100644
--- a/internal/commands/autoscale.go
+++ b/internal/commands/autoscale.go
@@ -5,13 +5,12 @@ import (
 
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/appsettings"
-	"github.com/drycc/workflow-cli/internal/utils"
+	"github.com/drycc/workflow-cli/internal/loader"
 )
 
 // AutoscaleList tells the informations about app's autoscale status
 func (d *DryccCmd) AutoscaleList(appID string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -40,8 +39,7 @@ func (d *DryccCmd) AutoscaleList(appID string) error {
 
 // AutoscaleSet sets autoscale options for the app.
 func (d *DryccCmd) AutoscaleSet(appID string, ptype string, minCPU int, maxCPU int, CPUPercent int) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -71,8 +69,7 @@ func (d *DryccCmd) AutoscaleSet(appID string, ptype string, minCPU int, maxCPU i
 
 // AutoscaleUnset removes autoscale for the app.
 func (d *DryccCmd) AutoscaleUnset(appID string, ptype string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
diff --git a/internal/commands/builds.go b/internal/commands/builds.go
index 73d544228..57da91b5c 100644
--- a/internal/commands/builds.go
+++ b/internal/commands/builds.go
@@ -8,13 +8,13 @@ import (
 
 	drycc "github.com/drycc/controller-sdk-go"
 	"github.com/drycc/controller-sdk-go/builds"
-	"github.com/drycc/workflow-cli/internal/utils"
+	"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 := utils.LoadAppSettings(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -39,8 +39,7 @@ func (d *DryccCmd) BuildsInfo(appID string, version int) error {
 
 // BuildsCreate creates a build for an app.
 func (d *DryccCmd) BuildsCreate(appID, image, stack, procfile, dryccpath, confirm string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -57,7 +56,7 @@ func (d *DryccCmd) BuildsCreate(appID, image, stack, procfile, dryccpath, confir
 		}
 	}
 
-	dryccfileMap := make(map[string]interface{})
+	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
@@ -82,8 +81,9 @@ func (d *DryccCmd) BuildsCreate(appID, image, stack, procfile, dryccpath, confir
 	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 := utils.LoadAppSettings(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -120,7 +120,7 @@ func (d *DryccCmd) BuildsFetch(appID string, version int, procfile, dryccpath, c
 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)), 0664)
+		err := os.WriteFile(procfile, []byte(d.toYamlString(Procinfo, 2)), 0o664)
 		return err
 	}
 	d.Println("---\n# Source:", procfile)
@@ -128,7 +128,7 @@ func writeProcfileToPath(d *DryccCmd, procfile string, Procinfo map[string]strin
 	return nil
 }
 
-func writeDryccfileToPath(d *DryccCmd, dryccpath string, dryccfile map[string]interface{}, save bool) error {
+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)
@@ -140,7 +140,7 @@ func writeDryccfileToPath(d *DryccCmd, dryccpath string, dryccfile map[string]in
 	}
 
 	// Write config section
-	if config, ok := dryccfile["config"].(map[string]interface{}); ok {
+	if config, ok := dryccfile["config"].(map[string]any); ok {
 		configDir := filepath.Join(dryccpath, "config")
 		if save {
 			err := os.MkdirAll(configDir, os.ModePerm)
@@ -152,13 +152,13 @@ func writeDryccfileToPath(d *DryccCmd, dryccpath string, dryccfile map[string]in
 		for key, values := range config {
 			envFilePath := filepath.Join(configDir, key)
 			var content string
-			for k, v := range values.(map[string]interface{}) {
+			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), 0664)
+				err := os.WriteFile(envFilePath, []byte(content), 0o664)
 				if err != nil {
 					return fmt.Errorf("failed to write env file: %w", err)
 				}
@@ -170,7 +170,7 @@ func writeDryccfileToPath(d *DryccCmd, dryccpath string, dryccfile map[string]in
 	}
 
 	// Write pipeline section
-	if pipeline, ok := dryccfile["pipeline"].(map[string]interface{}); ok {
+	if pipeline, ok := dryccfile["pipeline"].(map[string]any); ok {
 		for fileName, pipelineConfig := range pipeline {
 			filePath := filepath.Join(dryccpath, fileName)
 			var buf bytes.Buffer
@@ -181,7 +181,7 @@ func writeDryccfileToPath(d *DryccCmd, dryccpath string, dryccfile map[string]in
 			}
 			yamlContent := buf.Bytes()
 			if save {
-				err := os.WriteFile(filePath, yamlContent, 0664)
+				err := os.WriteFile(filePath, yamlContent, 0o664)
 				if err != nil {
 					return fmt.Errorf("failed to write pipeline file: %w", err)
 				}
@@ -201,8 +201,8 @@ func parseProcfile(procfile []byte) (map[string]string, error) {
 }
 
 func buildConfirmAction(c *drycc.Client, appID string, procfileMap map[string]string,
-	dryccfileMap map[string]interface{}, confirm string) error {
-
+	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") {
@@ -222,7 +222,6 @@ func buildConfirmAction(c *drycc.Client, appID string, procfileMap map[string]st
 }
 
 func buildFetchConfirmAction(confirm, procfile, dryccpath string, save bool) error {
-
 	if save && (confirm == "" || confirm != "yes") {
 		// hint
 		msg := fmt.Sprintf(" !    WARNING: Potentially Build Fetch Action\n"+
diff --git a/internal/commands/builds_test.go b/internal/commands/builds_test.go
index 389203ead..d57b626b7 100644
--- a/internal/commands/builds_test.go
+++ b/internal/commands/builds_test.go
@@ -6,9 +6,8 @@ import (
 	"net/http"
 	"os"
 	"path/filepath"
-	"testing"
-
 	"reflect"
+	"testing"
 
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/workflow-cli/pkg/testutil"
@@ -59,15 +58,15 @@ func TestBuildsInfo(t *testing.T) {
 
 	err = cmdr.BuildsInfo("foo", -1)
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `App:                                                
-Sha:                                                
-UUID:       de1bf5b5-4a72-4f94-a10c-d2a3741cdf75    
-Owner:                                              
-Image:                                              
-Stack:                                              
-Created:    2014-01-01T00:00:00UTC                  
-Updated:                                            
-`, "output")
+	testutil.AssertOutput(t, b.String(), `App:
+Sha:
+UUID:       de1bf5b5-4a72-4f94-a10c-d2a3741cdf75
+Owner:
+Image:
+Stack:
+Created:    2014-01-01T00:00:00UTC
+Updated:
+`)
 }
 
 func TestBuildsCreate(t *testing.T) {
@@ -126,7 +125,7 @@ func TestBuildsCreate(t *testing.T) {
 	}
 	data := `web: ./drive
 warp: ./warp 8`
-	if err := os.WriteFile(tmpDir+"/Procfile", []byte(data), 0644); err != nil {
+	if err := os.WriteFile(tmpDir+"/Procfile", []byte(data), 0o644); err != nil {
 		t.Fatalf("error creating %s/Procfile (%s)", tmpDir, err)
 	}
 	defer func() {
@@ -149,12 +148,12 @@ warp: ./warp 8`
 					"web":  "./drive",
 					"warp": "./warp 8",
 				},
-				Dryccfile: map[string]interface{}{
-					"pipeline": map[string]interface{}{
-						"web.yaml": map[string]interface{}{
+				Dryccfile: map[string]any{
+					"pipeline": map[string]any{
+						"web.yaml": map[string]any{
 							"kind":  "pipeline",
 							"ptype": "web",
-							"deploy": map[string]interface{}{
+							"deploy": map[string]any{
 								"command": []string{
 									"bash",
 									"-c",
@@ -180,7 +179,7 @@ warp: ./warp 8
 	assert.NoError(t, err)
 
 	dryccpath := ".drycc"
-	os.MkdirAll(dryccpath, 0700)
+	os.MkdirAll(dryccpath, 0o700)
 	defer os.RemoveAll(dryccpath)
 	err = os.WriteFile(filepath.Join(dryccpath, "web.yaml"), []byte(`
 kind: pipeline
@@ -197,7 +196,6 @@ deploy:
 	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) {
@@ -239,7 +237,7 @@ func TestBuildsFetch(t *testing.T) {
 
 	// Helper function to compare YAML content ignoring field order
 	isEqualYAML := func(actual, expected string) bool {
-		var actualMap, expectedMap map[string]interface{}
+		var actualMap, expectedMap map[string]any
 		err := yaml.Unmarshal([]byte(actual), &actualMap)
 		if err != nil {
 			return false
diff --git a/internal/commands/certs.go b/internal/commands/certs.go
index 4abc1a0a3..bf7b67c9f 100644
--- a/internal/commands/certs.go
+++ b/internal/commands/certs.go
@@ -8,15 +8,14 @@ import (
 	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/internal/utils"
+	"github.com/drycc/workflow-cli/internal/loader"
 )
 
 const dateFormat = "2 Jan 2006"
 
 // CertsList lists certs registered with the controller.
 func (d *DryccCmd) CertsList(appID string, results int) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -56,8 +55,7 @@ func (d *DryccCmd) CertsList(appID string, results int) error {
 
 // CertAdd adds a cert to the controller.
 func (d *DryccCmd) CertAdd(appID string, cert string, key string, name string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -93,7 +91,7 @@ func (d *DryccCmd) doCertAdd(c *drycc.Client, appID string, cert string, key str
 
 // CertRemove deletes a cert from the controller.
 func (d *DryccCmd) CertRemove(appID string, name string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if d.checkAPICompatibility(s.Client, err) != nil {
 		return err
 	}
@@ -114,7 +112,7 @@ func (d *DryccCmd) CertRemove(appID string, name string) error {
 
 // CertInfo gets info about certficiate
 func (d *DryccCmd) CertInfo(appID string, name string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -143,8 +141,7 @@ func (d *DryccCmd) CertInfo(appID string, name string) error {
 
 // CertAttach attaches a certificate to a domain
 func (d *DryccCmd) CertAttach(appID string, name, domain string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -164,8 +161,7 @@ func (d *DryccCmd) CertAttach(appID string, name, domain string) error {
 
 // CertDetach detaches a certificate from a domain
 func (d *DryccCmd) CertDetach(appID, name, domain string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
diff --git a/internal/commands/certs_test.go b/internal/commands/certs_test.go
index 41b552683..f1c7fc98e 100644
--- a/internal/commands/certs_test.go
+++ b/internal/commands/certs_test.go
@@ -151,7 +151,6 @@ func TestCertsListLimit(t *testing.T) {
 	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) {
diff --git a/internal/commands/commands.go b/internal/commands/commands.go
index 6959e8723..4d5b42bcc 100644
--- a/internal/commands/commands.go
+++ b/internal/commands/commands.go
@@ -9,6 +9,8 @@ import (
 	"github.com/drycc/controller-sdk-go/api"
 )
 
+// 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
@@ -123,14 +125,14 @@ type Commander interface {
 	UsersList(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)
+	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
@@ -149,6 +151,8 @@ type Commander interface {
 	ResourceUnbind(string, string) error
 }
 
+// DryccCmd is a struct that implements the Commander interface and provides
+// methods for executing Drycc CLI commands.
 type DryccCmd struct {
 	ConfigFile string
 	Warned     bool
@@ -159,31 +163,31 @@ type DryccCmd struct {
 }
 
 // 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
index 7020bb374..7f12138c0 100644
--- a/internal/commands/config.go
+++ b/internal/commands/config.go
@@ -13,12 +13,12 @@ import (
 	"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/utils"
+	"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 := utils.LoadAppSettings(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -117,8 +117,7 @@ func (d *DryccCmd) ConfigInfo(appID string, ptype string, group string, version
 
 // ConfigSet sets an app's config variables.
 func (d *DryccCmd) ConfigSet(appID string, ptype string, group string, configVars []string, confirm string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -151,8 +150,7 @@ func (d *DryccCmd) ConfigSet(appID string, ptype string, group string, configVar
 
 // 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 := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -196,8 +194,7 @@ func (d *DryccCmd) ConfigUnset(appID string, ptype string, group string, configV
 
 // 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 := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -215,7 +212,6 @@ func (d *DryccCmd) ConfigPull(appID, ptype, group, fileName string, interactive
 	}
 
 	stat, err := os.Stdout.Stat()
-
 	if err != nil {
 		return err
 	}
@@ -261,19 +257,18 @@ func (d *DryccCmd) ConfigPull(appID, ptype, group, fileName string, interactive
 				configMap[value.Name] = value.Value
 			}
 		}
-		return os.WriteFile(fileName, []byte(formatEnv(configMap)), 0664)
+		return os.WriteFile(fileName, []byte(formatEnv(configMap)), 0o664)
 	}
-	return os.WriteFile(fileName, []byte(formatConfig(configValues)), 0664)
+	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, confirm string) error {
 	stat, err := os.Stdin.Stat()
-
 	if err != nil {
 		return err
 	}
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -298,7 +293,6 @@ func (d *DryccCmd) ConfigPush(appID, ptype string, group string, fileName string
 		}
 
 		contents, err = os.ReadFile(fileName)
-
 		if err != nil {
 			return err
 		}
@@ -318,9 +312,9 @@ func (d *DryccCmd) ConfigPush(appID, ptype string, group string, fileName string
 	return d.ConfigSet(appID, ptype, group, config, "yes")
 }
 
+// ConfigAttach attaches config groups to a process type.
 func (d *DryccCmd) ConfigAttach(appID string, ptype string, groups string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -345,9 +339,9 @@ func (d *DryccCmd) ConfigAttach(appID string, ptype string, groups string) error
 	return nil
 }
 
+// ConfigDetach detaches config groups from a process type.
 func (d *DryccCmd) ConfigDetach(appID string, ptype string, groups string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -402,7 +396,7 @@ func parseConfig(ptype, group string, configVars []string) ([]api.ConfigValue, e
 	return configMap, nil
 }
 
-func formatEnv(configVars map[string]interface{}) string {
+func formatEnv(configVars map[string]any) string {
 	var formattedConfig string
 
 	keys := *sortKeys(configVars)
@@ -425,7 +419,6 @@ func formatConfig(configVars []api.ConfigValue) string {
 }
 
 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
@@ -434,10 +427,7 @@ func configConfirmAction(s *drycc.Client, appID string, ptype string, group stri
 	}
 
 	appSettings, _ := appsettings.List(s, appID)
-	autodeploy := true
-	if appSettings.Autodeploy != nil && !*appSettings.Autodeploy {
-		autodeploy = false
-	}
+	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
@@ -472,10 +462,7 @@ func configConfirmActionStdin(s *drycc.Client, appID string, ptype string, group
 	}
 
 	appSettings, _ := appsettings.List(s, appID)
-	autodeploy := true
-	if appSettings.Autodeploy != nil && !*appSettings.Autodeploy {
-		autodeploy = false
-	}
+	autodeploy := appSettings.Autodeploy == nil || *appSettings.Autodeploy
 
 	if ptype == "" && group == "" && (confirm == "" || confirm != "yes") && autodeploy {
 		fmt.Printf(` !    WARNING: Potentially Config Action
diff --git a/internal/commands/config_test.go b/internal/commands/config_test.go
index 13185a977..e496d3bd5 100644
--- a/internal/commands/config_test.go
+++ b/internal/commands/config_test.go
@@ -70,7 +70,7 @@ TRUE=false
 }
 
 func TestSortKeys(t *testing.T) {
-	test := map[string]interface{}{
+	test := map[string]any{
 		"d": nil,
 		"b": nil,
 		"c": nil,
diff --git a/internal/commands/domains.go b/internal/commands/domains.go
index 17808b2d1..46c15d81f 100644
--- a/internal/commands/domains.go
+++ b/internal/commands/domains.go
@@ -4,13 +4,12 @@ import (
 	"fmt"
 
 	"github.com/drycc/controller-sdk-go/domains"
-	"github.com/drycc/workflow-cli/internal/utils"
+	"github.com/drycc/workflow-cli/internal/loader"
 )
 
 // DomainsList lists domains registered with an app.
 func (d *DryccCmd) DomainsList(appID string, results int) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -44,8 +43,7 @@ func (d *DryccCmd) DomainsList(appID string, results int) error {
 
 // DomainsAdd adds a domain to an app.
 func (d *DryccCmd) DomainsAdd(appID, domain, ptype string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -66,8 +64,7 @@ func (d *DryccCmd) DomainsAdd(appID, domain, ptype string) error {
 
 // DomainsRemove removes a domain registered with an app.
 func (d *DryccCmd) DomainsRemove(appID, domain string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
diff --git a/internal/commands/gateways.go b/internal/commands/gateways.go
index 58076908b..432439a90 100644
--- a/internal/commands/gateways.go
+++ b/internal/commands/gateways.go
@@ -6,13 +6,12 @@ import (
 
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/gateways"
-	"github.com/drycc/workflow-cli/internal/utils"
+	"github.com/drycc/workflow-cli/internal/loader"
 )
 
 // GatewaysList lists gateways for the app
 func (d *DryccCmd) GatewaysList(appID string, results int) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -41,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 {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -62,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 {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
diff --git a/internal/commands/gateways_test.go b/internal/commands/gateways_test.go
index f18eece25..463c8d41d 100644
--- a/internal/commands/gateways_test.go
+++ b/internal/commands/gateways_test.go
@@ -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/internal/commands/git.go b/internal/commands/git.go
index fcfd07fd6..6b0f69a01 100644
--- a/internal/commands/git.go
+++ b/internal/commands/git.go
@@ -3,24 +3,25 @@ package commands
 import (
 	"fmt"
 
-	"github.com/drycc/workflow-cli/internal/utils"
+	"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 {
-	appID, s, err := utils.LoadAppSettings(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 {
@@ -63,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 {
-	appID, s, err := utils.LoadAppSettings(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
index 04a241b30..425adbb4d 100644
--- a/internal/commands/healthchecks.go
+++ b/internal/commands/healthchecks.go
@@ -6,7 +6,7 @@ import (
 
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/config"
-	"github.com/drycc/workflow-cli/internal/utils"
+	"github.com/drycc/workflow-cli/internal/loader"
 )
 
 func getHealthcheckString(ptype, probeType string, healthcheck *api.Healthcheck) string {
@@ -47,13 +47,12 @@ func getHealthchecksStrings(ptype string, healthchecks *api.Healthchecks) []stri
 
 // HealthchecksList lists an app's healthchecks.
 func (d *DryccCmd) HealthchecksList(appID, ptype string, version int) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
+	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
 	}
@@ -107,8 +106,7 @@ func (d *DryccCmd) HealthchecksList(appID, ptype string, version int) error {
 
 // HealthchecksSet sets an app's healthchecks.
 func (d *DryccCmd) HealthchecksSet(appID, healthcheckType, ptype string, probe *api.Healthcheck) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -138,7 +136,7 @@ func (d *DryccCmd) HealthchecksSet(appID, healthcheckType, ptype string, probe *
 
 // HealthchecksUnset removes an app's healthchecks.
 func (d *DryccCmd) HealthchecksUnset(appID, ptype string, healthchecks []string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
diff --git a/internal/commands/keys.go b/internal/commands/keys.go
index b283887ac..6698b2690 100644
--- a/internal/commands/keys.go
+++ b/internal/commands/keys.go
@@ -17,7 +17,6 @@ import (
 // KeysList lists a user's keys.
 func (d *DryccCmd) KeysList(results int) error {
 	s, err := settings.Load(d.ConfigFile)
-
 	if err != nil {
 		return err
 	}
@@ -50,7 +49,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 +67,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 +116,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 +132,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 +155,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 +178,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/internal/commands/keys_test.go b/internal/commands/keys_test.go
index c694214be..3b9b235b0 100644
--- a/internal/commands/keys_test.go
+++ b/internal/commands/keys_test.go
@@ -88,7 +88,7 @@ func TestListKeys(t *testing.T) {
 
 	folder := filepath.Join(name, ".ssh")
 
-	err = os.Mkdir(folder, 0600)
+	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 {
@@ -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, 0600)
+	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, 0600)
+	err = os.Mkdir(folder, 0o600)
 	assert.NoError(t, err)
 
 	cf, server, err := testutil.NewTestServerAndClient()
diff --git a/internal/commands/labels.go b/internal/commands/labels.go
index 1d7b7f470..df96cfb6e 100644
--- a/internal/commands/labels.go
+++ b/internal/commands/labels.go
@@ -6,13 +6,12 @@ import (
 
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/appsettings"
-	"github.com/drycc/workflow-cli/internal/utils"
+	"github.com/drycc/workflow-cli/internal/loader"
 )
 
 // LabelsList list app's labels
 func (d *DryccCmd) LabelsList(appID string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -41,8 +40,7 @@ func (d *DryccCmd) LabelsList(appID string) error {
 
 // LabelsSet sets labels for app
 func (d *DryccCmd) LabelsSet(appID string, labels []string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -71,13 +69,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 {
-	appID, s, err := utils.LoadAppSettings(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 +97,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/internal/commands/labels_test.go b/internal/commands/labels_test.go
index cc81b5706..2bf362536 100644
--- a/internal/commands/labels_test.go
+++ b/internal/commands/labels_test.go
@@ -35,10 +35,10 @@ func TestLabelsList(t *testing.T) {
 
 	err = cmdr.LabelsList("rivendell")
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `OWNER    KEY         VALUE                                      
-jim      git_repo    https://github.com/drycc/controller-sdk-go    
-jim      team        drycc                                         
-`, "output")
+	testutil.AssertOutput(t, b.String(), `OWNER    KEY         VALUE
+jim      git_repo    https://github.com/drycc/controller-sdk-go
+jim      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/limits.go b/internal/commands/limits.go
index 30329933e..e021de6c6 100644
--- a/internal/commands/limits.go
+++ b/internal/commands/limits.go
@@ -7,14 +7,13 @@ 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/internal/utils"
+	"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, version int) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -38,8 +37,8 @@ func (d *DryccCmd) LimitsList(appID string, version int) 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,
@@ -58,8 +57,7 @@ func (d *DryccCmd) LimitsList(appID string, version int) error {
 
 // LimitsSet sets an app's limits.
 func (d *DryccCmd) LimitsSet(appID string, limits []string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -91,8 +89,7 @@ func (d *DryccCmd) LimitsSet(appID string, limits []string) error {
 
 // LimitsUnset removes an app's limits.
 func (d *DryccCmd) LimitsUnset(appID string, limits []string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -103,7 +100,7 @@ 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
 		}
@@ -125,7 +122,6 @@ func (d *DryccCmd) LimitsUnset(appID string, limits []string) error {
 // LimitsSpecs list limit spec
 func (d *DryccCmd) LimitsSpecs(keywords string, results int) error {
 	s, err := settings.Load(d.ConfigFile)
-
 	if err != nil {
 		return err
 	}
@@ -142,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"]),
@@ -163,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
 	}
@@ -180,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,
@@ -196,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/internal/commands/limits_test.go b/internal/commands/limits_test.go
index 567e450c8..e98e17ae2 100644
--- a/internal/commands/limits_test.go
+++ b/internal/commands/limits_test.go
@@ -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`},
 	}
@@ -299,11 +300,11 @@ func TestLimitsList(t *testing.T) {
 
 	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)
@@ -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)
@@ -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)
@@ -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",
@@ -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",
@@ -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)
@@ -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)
@@ -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/internal/commands/perms.go b/internal/commands/perms.go
index 7da480f54..790db0f83 100644
--- a/internal/commands/perms.go
+++ b/internal/commands/perms.go
@@ -4,12 +4,12 @@ import (
 	"strings"
 
 	"github.com/drycc/controller-sdk-go/perms"
-	"github.com/drycc/workflow-cli/internal/utils"
+	"github.com/drycc/workflow-cli/internal/loader"
 )
 
 // PermList prints which users have permissions.
 func (d *DryccCmd) PermList(appID string, results int) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -28,7 +28,7 @@ func (d *DryccCmd) PermList(appID string, results int) error {
 
 // PermCreate create user perms.
 func (d *DryccCmd) PermCreate(appID, username, permissions string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -48,7 +48,7 @@ func (d *DryccCmd) PermCreate(appID, username, permissions string) error {
 
 // PermUpdate update user perms.
 func (d *DryccCmd) PermUpdate(appID, username, permissions string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -68,7 +68,7 @@ func (d *DryccCmd) PermUpdate(appID, username, permissions string) error {
 
 // PermDelete removes a user from an app.
 func (d *DryccCmd) PermDelete(appID, username string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
diff --git a/internal/commands/ps.go b/internal/commands/ps.go
index e70e5e216..c2a77b8b7 100644
--- a/internal/commands/ps.go
+++ b/internal/commands/ps.go
@@ -15,7 +15,7 @@ import (
 	"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/utils"
+	"github.com/drycc/workflow-cli/internal/loader"
 	"github.com/drycc/workflow-cli/pkg/logging"
 	"golang.org/x/net/websocket"
 	yaml "gopkg.in/yaml.v3"
@@ -31,7 +31,7 @@ const (
 
 // PsList lists an app's processes.
 func (d *DryccCmd) PsList(appID string, results int) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -50,10 +50,9 @@ func (d *DryccCmd) PsList(appID string, results int) error {
 	return nil
 }
 
-// PodLogs returns the logs from an pod.
+// 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 := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -82,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 {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -108,7 +107,7 @@ func (d *DryccCmd) PsExec(appID, podID string, tty, stdin bool, command []string
 
 // PsDescribe describe an app's processes.
 func (d *DryccCmd) PsDescribe(appID, podID string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -200,8 +199,7 @@ func (d *DryccCmd) PsDescribe(appID, podID string) error {
 
 // PsDelete delete an pod.
 func (d *DryccCmd) PsDelete(appID string, podIDs []string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -349,7 +347,6 @@ 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
 			}
@@ -364,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/internal/commands/pts.go b/internal/commands/pts.go
index e2a5d7e51..78ce5f0dc 100644
--- a/internal/commands/pts.go
+++ b/internal/commands/pts.go
@@ -10,12 +10,12 @@ import (
 	"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/utils"
+	"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 := utils.LoadAppSettings(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -36,7 +36,7 @@ func (d *DryccCmd) PtsList(appID string, results int) error {
 
 // PtsDescribe describe an app's processes.
 func (d *DryccCmd) PtsDescribe(appID, ptype string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -55,7 +55,7 @@ func (d *DryccCmd) PtsDescribe(appID, ptype string) error {
 
 // PtsScale scales an app's processes.
 func (d *DryccCmd) PtsScale(appID string, targets []string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -83,7 +83,7 @@ func (d *DryccCmd) PtsScale(appID string, targets []string) error {
 
 // PtsRestart restarts an app's processes.
 func (d *DryccCmd) PtsRestart(appID string, targets []string, confirm string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -117,9 +117,9 @@ func (d *DryccCmd) PtsRestart(appID string, targets []string, confirm string) er
 	return nil
 }
 
-// PtsRestart restarts an app's processes.
+// PtsClean cleans process types that are not used.
 func (d *DryccCmd) PtsClean(appID string, targets []string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -240,7 +240,6 @@ func parsePtsTargets(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
 			}
diff --git a/internal/commands/registry.go b/internal/commands/registry.go
index 3c8b562a0..a509c535f 100644
--- a/internal/commands/registry.go
+++ b/internal/commands/registry.go
@@ -5,13 +5,12 @@ import (
 
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/config"
-	"github.com/drycc/workflow-cli/internal/utils"
+	"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 := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -27,7 +26,6 @@ func (d *DryccCmd) RegistryList(appID, ptype string, version int) error {
 	ptypes := []string{}
 	if ptype != "" {
 		ptypes = append(ptypes, ptype)
-
 	} else {
 		for ptype := range config.Registry {
 			ptypes = append(ptypes, ptype)
@@ -52,8 +50,7 @@ func (d *DryccCmd) RegistryList(appID, ptype string, version int) error {
 
 // RegistrySet sets an app's registry information.
 func (d *DryccCmd) RegistrySet(appID, ptype, username, password string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -63,8 +60,8 @@ func (d *DryccCmd) RegistrySet(appID, ptype, username, password string) error {
 	quit := progress(d.WOut)
 
 	configObj := api.Config{}
-	registry := make(map[string]map[string]interface{})
-	registry[ptype] = map[string]interface{}{
+	registry := make(map[string]map[string]any)
+	registry[ptype] = map[string]any{
 		"username": username,
 		"password": password,
 	}
@@ -84,8 +81,7 @@ func (d *DryccCmd) RegistrySet(appID, ptype, username, password string) error {
 
 // RegistryUnset removes an app's registry information.
 func (d *DryccCmd) RegistryUnset(appID, ptype string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -95,8 +91,8 @@ func (d *DryccCmd) RegistryUnset(appID, ptype string) error {
 	quit := progress(d.WOut)
 
 	configObj := api.Config{}
-	registry := make(map[string]map[string]interface{})
-	registry[ptype] = map[string]interface{}{
+	registry := make(map[string]map[string]any)
+	registry[ptype] = map[string]any{
 		"username": nil,
 		"password": nil,
 	}
diff --git a/internal/commands/registry_test.go b/internal/commands/registry_test.go
index f1f1ac3e4..0e8474ee6 100644
--- a/internal/commands/registry_test.go
+++ b/internal/commands/registry_test.go
@@ -44,9 +44,9 @@ func TestRegistryList(t *testing.T) {
 
 	err = cmdr.RegistryList("enterprise", "web", -1)
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `PTYPE    USERNAME    PASSWORD 
-web      jkirk       ncc1701     
-`, "output")
+	testutil.AssertOutput(t, b.String(), `PTYPE    USERNAME    PASSWORD
+web      jkirk       ncc1701
+`)
 }
 
 func TestRegistrySet(t *testing.T) {
@@ -61,7 +61,7 @@ func TestRegistrySet(t *testing.T) {
 		testutil.SetHeaders(w)
 		if r.Method == "POST" {
 			testutil.AssertBody(t, api.Config{
-				Registry: map[string]map[string]interface{}{
+				Registry: map[string]map[string]any{
 					"web": {
 						"username": "jkirk",
 						"password": "ncc1701",
@@ -95,11 +95,11 @@ func TestRegistrySet(t *testing.T) {
 	err = cmdr.RegistrySet("foo", "web", "jkirk", "ncc1701")
 	assert.NoError(t, err)
 
-	assert.Equal(t, testutil.StripProgress(b.String()), `Applying registry information... done
+	testutil.AssertOutput(t, testutil.StripProgress(b.String()), `Applying registry information... done
 
-PTYPE    USERNAME    PASSWORD 
-web      jkirk       ncc1701     
-`, "output")
+PTYPE    USERNAME    PASSWORD
+web      jkirk       ncc1701
+`)
 }
 
 func TestRegistryUnset(t *testing.T) {
@@ -114,7 +114,7 @@ func TestRegistryUnset(t *testing.T) {
 		testutil.SetHeaders(w)
 		if r.Method == "POST" {
 			testutil.AssertBody(t, api.Config{
-				Registry: map[string]map[string]interface{}{
+				Registry: map[string]map[string]any{
 					"web": {
 						"username": nil,
 						"password": nil,
diff --git a/internal/commands/releases.go b/internal/commands/releases.go
index 8f1d9d1bf..286e1d401 100644
--- a/internal/commands/releases.go
+++ b/internal/commands/releases.go
@@ -6,13 +6,12 @@ import (
 	"time"
 
 	"github.com/drycc/controller-sdk-go/releases"
-	"github.com/drycc/workflow-cli/internal/utils"
+	"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 := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -43,8 +42,7 @@ func (d *DryccCmd) ReleasesList(appID string, targets []string, results int) err
 
 // ReleasesInfo prints info about a specific release.
 func (d *DryccCmd) ReleasesInfo(appID string, version int) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -95,7 +93,7 @@ func (d *DryccCmd) ReleasesInfo(appID string, version int) error {
 
 // ReleasesDeploy force deploy lastest release.
 func (d *DryccCmd) ReleasesDeploy(appID string, targets []string, force bool, confirm string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -115,7 +113,7 @@ func (d *DryccCmd) ReleasesDeploy(appID string, targets []string, force bool, co
 	startTime := time.Now()
 	quit := progress(d.WOut)
 	ptypes := strings.Join(targets, ",")
-	targetMap := map[string]interface{}{
+	targetMap := map[string]any{
 		"ptypes": ptypes,
 		"force":  force,
 	}
@@ -132,8 +130,7 @@ func (d *DryccCmd) ReleasesDeploy(appID string, targets []string, force bool, co
 
 // ReleasesRollback rolls an app back to a previous release.
 func (d *DryccCmd) ReleasesRollback(appID string, targets []string, version int) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
diff --git a/internal/commands/resources.go b/internal/commands/resources.go
index dec457c10..55a37b42e 100644
--- a/internal/commands/resources.go
+++ b/internal/commands/resources.go
@@ -7,18 +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/internal/utils"
+	"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
 	}
@@ -47,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
 	}
@@ -81,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 {
-	appID, s, err := utils.LoadAppSettings(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 {
@@ -99,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
@@ -133,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 {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -166,13 +161,12 @@ func (d *DryccCmd) ResourcesList(appID string, results int) error {
 
 // ResourceGet describe a resource from the application
 func (d *DryccCmd) ResourceGet(appID, name string, details bool) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	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 {
@@ -205,8 +199,7 @@ func (d *DryccCmd) ResourceGet(appID, name string, details bool) error {
 
 // ResourceDelete delete a resource from the application
 func (d *DryccCmd) ResourceDelete(appID, name, confirm string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -241,12 +234,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 {
-	appID, s, err := utils.LoadAppSettings(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 {
@@ -259,7 +251,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
@@ -292,8 +284,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 {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -316,8 +307,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 {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -339,7 +329,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/internal/commands/resources_test.go b/internal/commands/resources_test.go
index 16b5be4ab..fe76bffb3 100644
--- a/internal/commands/resources_test.go
+++ b/internal/commands/resources_test.go
@@ -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(), `NAME     OWNER    PLAN         UPDATED                
-mysql    test     mysql:5.6    2020-09-08T00:00:00UTC    
-`, "output")
+	testutil.AssertOutput(t, b.String(), `NAME     OWNER    PLAN         UPDATED
+mysql    test     mysql:5.6    2020-09-08T00:00:00UTC
+`)
 }
 
 func TestResourceGet(t *testing.T) {
@@ -170,23 +169,23 @@ func TestResourceGet(t *testing.T) {
 	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
+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
+`)
 }
 
 func TestResourceDelete(t *testing.T) {
@@ -221,7 +220,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/internal/commands/routes.go b/internal/commands/routes.go
index b51754a1b..bfb54a60c 100644
--- a/internal/commands/routes.go
+++ b/internal/commands/routes.go
@@ -7,14 +7,13 @@ import (
 
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/routes"
-	"github.com/drycc/workflow-cli/internal/utils"
+	"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, kind string, backendRefs ...api.BackendRefRequest) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -34,8 +33,7 @@ func (d *DryccCmd) RoutesCreate(appID, name string, kind string, backendRefs ...
 
 // RoutesList lists routes for the app
 func (d *DryccCmd) RoutesList(appID string, results int) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -54,9 +52,9 @@ func (d *DryccCmd) RoutesList(appID string, results int) error {
 		for _, route := range routes {
 			var services []string
 			for _, rule := range route.Rules {
-				if backends, ok := rule["backendRefs"].([]interface{}); ok {
+				if backends, ok := rule["backendRefs"].([]any); ok {
 					for _, backend := range backends {
-						if service, ok := backend.(map[string]interface{}); ok {
+						if service, ok := backend.(map[string]any); ok {
 							services = append(services, fmt.Sprintf("%v:%v", service["name"], service["port"]))
 						}
 					}
@@ -83,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 {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -104,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 {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -125,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 {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -147,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 {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -178,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 {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
diff --git a/internal/commands/routes_test.go b/internal/commands/routes_test.go
index d692bd3e0..82165faca 100644
--- a/internal/commands/routes_test.go
+++ b/internal/commands/routes_test.go
@@ -131,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()
diff --git a/internal/commands/routing.go b/internal/commands/routing.go
index 9dfa5d5bc..3c21f1701 100644
--- a/internal/commands/routing.go
+++ b/internal/commands/routing.go
@@ -3,13 +3,12 @@ package commands
 import (
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/appsettings"
-	"github.com/drycc/workflow-cli/internal/utils"
+	"github.com/drycc/workflow-cli/internal/loader"
 )
 
 // RoutingInfo provides information about the status of app routing.
 func (d *DryccCmd) RoutingInfo(appID string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -29,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 {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -54,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 {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
diff --git a/internal/commands/services.go b/internal/commands/services.go
index 2fca6ded1..87e811b44 100644
--- a/internal/commands/services.go
+++ b/internal/commands/services.go
@@ -6,13 +6,12 @@ import (
 	"strconv"
 
 	"github.com/drycc/controller-sdk-go/services"
-	"github.com/drycc/workflow-cli/internal/utils"
+	"github.com/drycc/workflow-cli/internal/loader"
 )
 
 // ServicesList lists extra services for the app
 func (d *DryccCmd) ServicesList(appID string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -44,8 +43,7 @@ func (d *DryccCmd) ServicesList(appID string) error {
 
 // ServicesAdd adds a service to an app.
 func (d *DryccCmd) ServicesAdd(appID, ptype string, ports string, protocol string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -69,8 +67,7 @@ func (d *DryccCmd) ServicesAdd(appID, ptype string, ports string, protocol strin
 
 // 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 := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
diff --git a/internal/commands/tags.go b/internal/commands/tags.go
index a4119bc29..0157368c8 100644
--- a/internal/commands/tags.go
+++ b/internal/commands/tags.go
@@ -6,13 +6,12 @@ import (
 
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/config"
-	"github.com/drycc/workflow-cli/internal/utils"
+	"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 := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -29,7 +28,6 @@ func (d *DryccCmd) TagsList(appID, ptype string, version int) error {
 	ptypes := []string{}
 	if ptype != "" {
 		ptypes = append(ptypes, ptype)
-
 	} else {
 		for ptype := range config.Tags {
 			ptypes = append(ptypes, ptype)
@@ -55,8 +53,7 @@ func (d *DryccCmd) TagsList(appID, ptype string, version int) error {
 
 // TagsSet sets an app's tags.
 func (d *DryccCmd) TagsSet(appID, ptype string, tags []string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -86,8 +83,7 @@ func (d *DryccCmd) TagsSet(appID, ptype string, tags []string) error {
 
 // TagsUnset removes an app's tags.
 func (d *DryccCmd) TagsUnset(appID, ptype string, tags []string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -115,12 +111,11 @@ func (d *DryccCmd) TagsUnset(appID, ptype string, tags []string) error {
 	return d.TagsList(appID, ptype, -1)
 }
 
-func parseTags(tags []string) (map[string]interface{}, error) {
-	tagMap := make(map[string]interface{})
+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
 		}
diff --git a/internal/commands/tags_test.go b/internal/commands/tags_test.go
index 3256fad14..748d2b147 100644
--- a/internal/commands/tags_test.go
+++ b/internal/commands/tags_test.go
@@ -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`},
 	}
@@ -108,10 +108,10 @@ func TestTagsList(t *testing.T) {
 
 	err = cmdr.TagsList("enterprise", "web", -1)
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `PTYPE    KEY     VALUE 
-web      ncc     1701     
-web      warp    8        
-`, "output")
+	testutil.AssertOutput(t, b.String(), `PTYPE    KEY     VALUE
+web      ncc     1701
+web      warp    8
+`)
 }
 
 func TestTagsSet(t *testing.T) {
@@ -156,11 +156,11 @@ func TestTagsSet(t *testing.T) {
 	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    KEY     VALUE 
-web      true    false    
-`, "output")
+PTYPE    KEY     VALUE
+web      true    false
+`)
 }
 
 func TestTagsUnset(t *testing.T) {
@@ -205,9 +205,9 @@ func TestTagsUnset(t *testing.T) {
 	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    KEY     VALUE 
-web      warp    8        
-`, "output")
+PTYPE    KEY     VALUE
+web      warp    8
+`)
 }
diff --git a/internal/commands/timeouts.go b/internal/commands/timeouts.go
index 93570ffd6..ecbde8510 100644
--- a/internal/commands/timeouts.go
+++ b/internal/commands/timeouts.go
@@ -6,13 +6,12 @@ import (
 
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/config"
-	"github.com/drycc/workflow-cli/internal/utils"
+	"github.com/drycc/workflow-cli/internal/loader"
 )
 
 // TimeoutsList lists an app's timeouts.
 func (d *DryccCmd) TimeoutsList(appID string, version int) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -40,8 +39,7 @@ func (d *DryccCmd) TimeoutsList(appID string, version int) error {
 
 // TimeoutsSet sets an app's timeouts.
 func (d *DryccCmd) TimeoutsSet(appID string, timeouts []string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -72,8 +70,7 @@ func (d *DryccCmd) TimeoutsSet(appID string, timeouts []string) error {
 
 // TimeoutsUnset removes an app's timeouts.
 func (d *DryccCmd) TimeoutsUnset(appID string, timeouts []string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -84,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
@@ -104,12 +101,11 @@ func (d *DryccCmd) TimeoutsUnset(appID string, timeouts []string) error {
 	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/internal/commands/timeouts_test.go b/internal/commands/timeouts_test.go
index 3d27febc3..e86ce730e 100644
--- a/internal/commands/timeouts_test.go
+++ b/internal/commands/timeouts_test.go
@@ -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`},
 	}
@@ -105,10 +105,10 @@ func TestTimeoutsList(t *testing.T) {
 
 	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)
@@ -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)
@@ -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)
@@ -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",
@@ -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)
@@ -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)
@@ -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/internal/commands/tls.go b/internal/commands/tls.go
index 6b32213d8..9dd51d604 100644
--- a/internal/commands/tls.go
+++ b/internal/commands/tls.go
@@ -4,13 +4,12 @@ import (
 	"fmt"
 
 	"github.com/drycc/controller-sdk-go/tls"
-	"github.com/drycc/workflow-cli/internal/utils"
+	"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 {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -47,8 +46,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 {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -69,8 +67,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 {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -91,8 +88,7 @@ func (d *DryccCmd) TLSForceDisable(appID string) error {
 
 // TLSAutoEnable enables certs-auto requests to the application.
 func (d *DryccCmd) TLSAutoEnable(appID string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -113,8 +109,7 @@ func (d *DryccCmd) TLSAutoEnable(appID string) error {
 
 // TLSAutoDisable disables certs-auto requests to the application.
 func (d *DryccCmd) TLSAutoDisable(appID string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -135,8 +130,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 {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
diff --git a/internal/commands/token.go b/internal/commands/token.go
index 8c8c81cd5..db8b408d3 100644
--- a/internal/commands/token.go
+++ b/internal/commands/token.go
@@ -15,9 +15,9 @@ import (
 	"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
 	}
@@ -45,10 +45,10 @@ func (d *DryccCmd) TokensList(results int) error {
 	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/internal/commands/token_test.go b/internal/commands/token_test.go
index 1e6d66451..ab38c853e 100644
--- a/internal/commands/token_test.go
+++ b/internal/commands/token_test.go
@@ -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) {
diff --git a/internal/commands/users.go b/internal/commands/users.go
index a60634651..b6dc6fbda 100644
--- a/internal/commands/users.go
+++ b/internal/commands/users.go
@@ -10,7 +10,6 @@ import (
 // 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
 	}
@@ -41,7 +40,6 @@ func (d *DryccCmd) UsersList(results int) error {
 // UsersEnable enable user with the controller.
 func (d *DryccCmd) UsersEnable(username string) error {
 	s, err := settings.Load(d.ConfigFile)
-
 	if err != nil {
 		return err
 	}
@@ -59,7 +57,6 @@ func (d *DryccCmd) UsersEnable(username string) error {
 // UsersDisable disable user with the controller.
 func (d *DryccCmd) UsersDisable(username string) error {
 	s, err := settings.Load(d.ConfigFile)
-
 	if err != nil {
 		return err
 	}
diff --git a/internal/commands/utils.go b/internal/commands/utils.go
index 0a8f88eba..2e5537413 100644
--- a/internal/commands/utils.go
+++ b/internal/commands/utils.go
@@ -16,8 +16,10 @@ import (
 	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"}
@@ -142,7 +144,7 @@ func (d *DryccCmd) indentString(s string, indent int) string {
 }
 
 // toYamlString convert object to yaml string
-func (d *DryccCmd) toYamlString(v interface{}, indent int) string {
+func (d *DryccCmd) toYamlString(v any, indent int) string {
 	buf := bytes.Buffer{}
 	encode := yaml.NewEncoder(&buf)
 	encode.SetIndent(indent)
@@ -150,7 +152,7 @@ func (d *DryccCmd) toYamlString(v interface{}, indent int) string {
 	return buf.String()
 }
 
-func sortKeys(data map[string]interface{}) *[]string {
+func sortKeys(data map[string]any) *[]string {
 	keys := make([]string, 0, len(data))
 	for k := range data {
 		keys = append(keys, k)
@@ -186,6 +188,8 @@ func safeGetString(data string) string {
 	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
diff --git a/internal/commands/version.go b/internal/commands/version.go
index fd27162b5..6f7a570a4 100644
--- a/internal/commands/version.go
+++ b/internal/commands/version.go
@@ -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/internal/commands/volumes.go b/internal/commands/volumes.go
index 3197c3e7c..0fa5e20bc 100644
--- a/internal/commands/volumes.go
+++ b/internal/commands/volumes.go
@@ -14,15 +14,14 @@ import (
 	drycc "github.com/drycc/controller-sdk-go"
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/volumes"
-	"github.com/drycc/workflow-cli/internal/utils"
+	"github.com/drycc/workflow-cli/internal/loader"
 	"github.com/schollz/progressbar/v3"
 	"sigs.k8s.io/yaml"
 )
 
 // VolumesList list volumes in the application
 func (d *DryccCmd) VolumesList(appID string, results int) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -45,8 +44,7 @@ func (d *DryccCmd) VolumesList(appID string, results int) error {
 
 // VolumesInfo get volume in the application
 func (d *DryccCmd) VolumesInfo(appID, name string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -81,9 +79,8 @@ func (d *DryccCmd) VolumesInfo(appID, name string) error {
 }
 
 // VolumesCreate create a volume for the application
-func (d *DryccCmd) VolumesCreate(appID, name, vType, size string, parameters map[string]interface{}) error {
-	appID, s, err := utils.LoadAppSettings(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
 	}
@@ -115,8 +112,7 @@ Examples: 2G 2g`, size)
 
 // VolumesExpand create a volume for the application
 func (d *DryccCmd) VolumesExpand(appID, name, size string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -146,8 +142,7 @@ Examples: 2G 2g`, size)
 
 // VolumesDelete delete a volume from the application
 func (d *DryccCmd) VolumesDelete(appID, name string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -182,8 +177,7 @@ func (d *DryccCmd) VolumesClient(appID, cmd string, args ...string) error {
 
 // VolumesMount mount a volume to process of the application
 func (d *DryccCmd) VolumesMount(appID string, name string, volumeVars []string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
-
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -212,13 +206,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 {
-	appID, s, err := utils.LoadAppSettings(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
 	}
@@ -242,8 +235,7 @@ func (d *DryccCmd) VolumesUnmount(appID string, name string, volumeVars []string
 
 // volumesClientLs get all directory entries sorted by filename.
 func (d *DryccCmd) volumesClientLs(appID, vol string) error {
-
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -284,7 +276,7 @@ func (d *DryccCmd) volumesClientGetAll(client *drycc.Client, appID, volumeID, vo
 			if err != nil {
 				return err
 			}
-			w, err := os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
+			w, err := os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
 			if err != nil {
 				return err
 			}
@@ -317,7 +309,7 @@ func (d *DryccCmd) volumesClientPostAll(client *drycc.Client, appID, volumeID, v
 		if err != nil {
 			return err
 		}
-		if stat.Size() > 0 { //ignore empty file
+		if stat.Size() > 0 { // ignore empty file
 			reader := progressbar.NewReader(file, d.newProgressbar(stat.Size(), "↑", localPath))
 			if _, err := volumes.PostFile(client, appID, volumeID, volumePath, file.Name(), stat.Size(), &reader); err != nil {
 				return err
@@ -347,7 +339,7 @@ func (d *DryccCmd) volumesClientPostAll(client *drycc.Client, appID, volumeID, v
 
 // volumesClientCp copy files between volume and local file
 func (d *DryccCmd) volumesClientCp(appID, src, dst string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -388,7 +380,7 @@ func (d *DryccCmd) volumesClientCp(appID, src, dst string) error {
 
 // volumesClientRm delete a file from volume
 func (d *DryccCmd) volumesClientRm(appID, vol string) error {
-	appID, s, err := utils.LoadAppSettings(d.ConfigFile, appID)
+	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
@@ -407,8 +399,8 @@ func (d *DryccCmd) volumesClientRm(appID, vol string) error {
 	return nil
 }
 
-func parseVolume(volumeVars []string) (map[string]interface{}, error) {
-	volumeMap := make(map[string]interface{})
+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) {
diff --git a/internal/commands/volumes_test.go b/internal/commands/volumes_test.go
index 4934a7ae3..cb8506047 100644
--- a/internal/commands/volumes_test.go
+++ b/internal/commands/volumes_test.go
@@ -47,10 +47,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        OWNER    TYPE    PTYPE     PATH            SIZE
+myvolume    test     csi     cmd       /data/cmd1      500G
+myvolume    test     csi     cmd123    /data/cmd123    500G
+`)
 }
 
 func TestVolumesCreate(t *testing.T) {
@@ -71,9 +71,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")
@@ -115,23 +115,23 @@ 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
+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
+`)
 }
 
 func TestVolumesClientLs(t *testing.T) {
@@ -268,7 +268,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)
@@ -310,7 +310,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/completion/completion.go b/internal/completion/completion.go
index 78f6c6a5c..b0b1abec9 100644
--- a/internal/completion/completion.go
+++ b/internal/completion/completion.go
@@ -1,12 +1,12 @@
+// Package completion provides types and functions for command completion in the CLI
 package completion
 
 import (
 	"fmt"
+	"slices"
 	"strconv"
 	"strings"
 
-	"slices"
-
 	"github.com/drycc/controller-sdk-go/apps"
 	"github.com/drycc/controller-sdk-go/appsettings"
 	"github.com/drycc/controller-sdk-go/certs"
@@ -24,20 +24,23 @@ import (
 	"github.com/drycc/controller-sdk-go/services"
 	"github.com/drycc/controller-sdk-go/tokens"
 	"github.com/drycc/controller-sdk-go/volumes"
-	"github.com/drycc/workflow-cli/internal/utils"
+	"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 {
@@ -53,14 +56,16 @@ func (c *AppCompletion) CompletionFunc(_ *cobra.Command, args []string, toComple
 	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 := utils.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+	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 {
@@ -74,11 +79,13 @@ func (c *CertCompletion) CompletionFunc(_ *cobra.Command, args []string, toCompl
 	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}
@@ -88,18 +95,20 @@ func (c *CertDomainTachCompletion) CompletionFunc(cmd *cobra.Command, args []str
 	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 := utils.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil {
+	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 {
@@ -115,15 +124,16 @@ func (c *ConfigPtsGroupArgsCompletion) CompletionFunc(cmd *cobra.Command, args [
 	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 := utils.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+	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 {
@@ -138,14 +148,16 @@ func (c *ConfigGroupCompletion) CompletionFunc(_ *cobra.Command, args []string,
 	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 := utils.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+	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 {
@@ -159,11 +171,13 @@ func (c *DomainCompletion) CompletionFunc(_ *cobra.Command, args []string, toCom
 	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 {
@@ -180,15 +194,17 @@ func (c *GatewayProtocolCompletion) CompletionFunc(_ *cobra.Command, args []stri
 	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 := utils.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+	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) {
@@ -200,11 +216,13 @@ func (c *GatewayNameCompletion) CompletionFunc(_ *cobra.Command, args []string,
 	return results, cobra.ShellCompDirectiveNoFileComp
 }
 
+// HealthTypeCompletion provides completion for health check types
 type HealthTypeCompletion struct {
 	ArgsLen    int
 	ConfigFile *string
 }
 
+// CompletionFunc returns a list of health check types for completion
 func (c *HealthTypeCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 	healthTypes := []string{"startupProbe", "livenessProbe", "readinessProbe"}
 	if c.ArgsLen < 0 || len(args) == c.ArgsLen {
@@ -221,11 +239,13 @@ func (c *HealthTypeCompletion) CompletionFunc(_ *cobra.Command, args []string, t
 	return nil, cobra.ShellCompDirectiveNoFileComp
 }
 
+// ProbeTypeCompletion provides completion for probe types
 type ProbeTypeCompletion struct {
 	ArgsLen    int
 	ConfigFile *string
 }
 
+// CompletionFunc returns a list of probe types for completion
 func (c *ProbeTypeCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 	probeTypes := []string{"httpGet", "exec", "tcpSocket"}
 	if c.ArgsLen < 0 || len(args) == c.ArgsLen {
@@ -242,11 +262,13 @@ func (c *ProbeTypeCompletion) CompletionFunc(_ *cobra.Command, args []string, to
 	return nil, cobra.ShellCompDirectiveNoFileComp
 }
 
+// HealthChecksCompletion provides completion for health checks
 type HealthChecksCompletion struct {
 	ArgsLen    int
 	ConfigFile *string
 }
 
+// CompletionFunc returns a list of health checks for completion
 func (c *HealthChecksCompletion) CompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 	if len(args) == 0 {
 		healthTypeCompletion := HealthTypeCompletion{ArgsLen: 0, ConfigFile: c.ConfigFile}
@@ -256,11 +278,13 @@ func (c *HealthChecksCompletion) CompletionFunc(cmd *cobra.Command, args []strin
 	return probeTypeCompletion.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 {
@@ -277,11 +301,13 @@ func (c *ServiceProtocolCompletion) CompletionFunc(_ *cobra.Command, args []stri
 	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 {
@@ -297,14 +323,38 @@ func (c *KeyCompletion) CompletionFunc(_ *cobra.Command, args []string, toComple
 	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 := utils.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+	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 {
@@ -320,11 +370,13 @@ func (c *LabelCompletion) CompletionFunc(_ *cobra.Command, args []string, toComp
 	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 {
@@ -338,12 +390,14 @@ func (c *LimitSpecCompletion) CompletionFunc(_ *cobra.Command, args []string, to
 	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
@@ -363,14 +417,15 @@ func (c *LimitSetPlanCompletion) CompletionFunc(cmd *cobra.Command, args []strin
 		PtsCompletion: &PtsCompletion{AppID: c.AppID, ArgsLen: -1, ConfigFile: c.ConfigFile},
 	}
 	return ptsSetArgsCompletion.CompletionFunc(cmd, args, toComplete)
-
 }
 
+// UserPermsCompletion provides completion for user permissions
 type UserPermsCompletion struct {
 	ArgsLen    int
 	ConfigFile *string
 }
 
+// CompletionFunc returns a list of user permissions for completion
 func (c *UserPermsCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 	perms := []string{"view", "change", "delete"}
 	if len(args) > 0 && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
@@ -387,10 +442,12 @@ func (c *UserPermsCompletion) CompletionFunc(_ *cobra.Command, args []string, to
 	return nil, cobra.ShellCompDirectiveNoFileComp
 }
 
+// UserPermsArgsCompletion provides completion for user permission arguments.
 type UserPermsArgsCompletion struct {
 	*UserPermsCompletion
 }
 
+// CompletionFunc returns a list of user permission arguments for completion
 func (c *UserPermsArgsCompletion) CompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 	c.ArgsLen = -1
 	var results []string
@@ -403,15 +460,17 @@ func (c *UserPermsArgsCompletion) CompletionFunc(cmd *cobra.Command, args []stri
 	return results, cobra.ShellCompDirectiveNoFileComp
 }
 
+// PermUsernameCompletion provides completion for permission usernames
 type PermUsernameCompletion struct {
 	AppID      *string
 	ArgsLen    int
 	ConfigFile *string
 }
 
+// CompletionFunc returns a list of permission usernames for completion
 func (c *PermUsernameCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 	if len(args) == 0 {
-		if appID, s, err := utils.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+		if appID, s, err := loader.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
 			if perms, _, err := perms.List(s.Client, appID, -1); err == nil {
 				var results []string
 				for _, perm := range perms {
@@ -426,12 +485,14 @@ func (c *PermUsernameCompletion) CompletionFunc(_ *cobra.Command, args []string,
 	return nil, cobra.ShellCompDirectiveNoFileComp
 }
 
+// PermUpdateCompletion provides completion for permission updates
 type PermUpdateCompletion struct {
 	AppID      *string
 	ArgsLen    int
 	ConfigFile *string
 }
 
+// CompletionFunc returns a list of permission updates for completion
 func (c *PermUpdateCompletion) CompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 	if len(args) == 0 {
 		permUsernameCompletion := PermUsernameCompletion{AppID: c.AppID, ArgsLen: 0, ConfigFile: c.ConfigFile}
@@ -443,14 +504,16 @@ func (c *PermUpdateCompletion) CompletionFunc(cmd *cobra.Command, args []string,
 	return userPermsArgsCompletion.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 := utils.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+	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 {
@@ -464,15 +527,17 @@ func (c *PsCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplet
 	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 := utils.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+	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) {
@@ -486,10 +551,12 @@ func (c *PtsCompletion) CompletionFunc(_ *cobra.Command, args []string, toComple
 	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)
@@ -501,10 +568,12 @@ func (c *PtsArgsCompletion) CompletionFunc(cmd *cobra.Command, args []string, to
 	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}
@@ -525,11 +594,13 @@ func (c *PtsSetArgsCompletion) CompletionFunc(cmd *cobra.Command, args []string,
 	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 {
@@ -545,12 +616,14 @@ func (c *ResourceServiceCompletion) CompletionFunc(_ *cobra.Command, args []stri
 	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
@@ -564,30 +637,32 @@ func (c *ResourcePlanCompletion) CompletionFunc(_ *cobra.Command, args []string,
 	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) == 1 {
-		resourceServiceCompletion := ResourceServiceCompletion{ArgsLen: -1, ConfigFile: c.ConfigFile}
+	if len(args) == 0 {
+		resourceServiceCompletion := ResourceServiceCompletion{ArgsLen: 0, 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
+	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 := utils.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+	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 {
@@ -601,12 +676,14 @@ func (c *ResourceCompletion) CompletionFunc(_ *cobra.Command, args []string, toC
 	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}
@@ -621,14 +698,16 @@ func (c *ResourceUpdateCompletion) CompletionFunc(cmd *cobra.Command, args []str
 	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 := utils.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+	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")
@@ -644,11 +723,13 @@ func (c *ReleaseCompletion) CompletionFunc(_ *cobra.Command, args []string, toCo
 	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 {
@@ -665,20 +746,20 @@ func (c *RouteKindCompletion) CompletionFunc(_ *cobra.Command, args []string, to
 	return nil, cobra.ShellCompDirectiveNoFileComp
 }
 
+// RouteCompletion provides completion for routes
 type RouteCompletion struct {
 	AppID      *string
 	ArgsLen    int
 	ConfigFile *string
 }
 
-func (c *RouteCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
-	if appID, s, err := utils.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+// 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 {
-				if strings.HasPrefix(route.Name, toComplete) {
-					results = append(results, route.Name)
-				}
+				results = append(results, route.Name)
 			}
 			return results, cobra.ShellCompDirectiveNoFileComp
 		}
@@ -686,14 +767,16 @@ func (c *RouteCompletion) CompletionFunc(_ *cobra.Command, args []string, toComp
 	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 := utils.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+	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 {
@@ -707,6 +790,7 @@ func (c *ServiceCompletion) CompletionFunc(_ *cobra.Command, args []string, toCo
 	return nil, cobra.ShellCompDirectiveNoFileComp
 }
 
+// TagCompletion provides completion for tags
 type TagCompletion struct {
 	AppID      *string
 	Ptype      *string
@@ -714,8 +798,9 @@ type TagCompletion struct {
 	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 := utils.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+	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] {
@@ -731,15 +816,16 @@ func (c *TagCompletion) CompletionFunc(_ *cobra.Command, args []string, toComple
 	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{"enable", "disable"}
+	actions := []string{"redirect", "passthrough"}
 	if c.ArgsLen < 0 || len(args) == c.ArgsLen {
-
 		var results []string
 		for _, action := range actions {
 			if strings.HasPrefix(action, toComplete) {
@@ -747,39 +833,20 @@ func (c *TLSActionCompletion) CompletionFunc(_ *cobra.Command, args []string, to
 			}
 		}
 		return results, cobra.ShellCompDirectiveNoFileComp
-
-	}
-	return nil, cobra.ShellCompDirectiveNoFileComp
-}
-
-type TokenCompletion struct {
-	ArgsLen    int
-	ConfigFile *string
-}
-
-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
 }
 
+// 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 := utils.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
+	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 {
@@ -793,50 +860,54 @@ func (c *VolumeCompletion) CompletionFunc(_ *cobra.Command, args []string, toCom
 	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) {
-	vts := []string{"csi", "nfs", "oss"}
+	types := []string{"static", "dynamic"}
 	if c.ArgsLen < 0 || len(args) == c.ArgsLen {
-
 		var results []string
-		for _, vt := range vts {
-			if strings.HasPrefix(vt, toComplete) {
-				results = append(results, vt)
+		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)
 	}
-	ptsSetArgsCompletion := PtsSetArgsCompletion{
+	ptsArgCompletion := PtsArgsCompletion{
 		PtsCompletion: &PtsCompletion{AppID: c.AppID, ArgsLen: -1, ConfigFile: c.ConfigFile},
 	}
-	return ptsSetArgsCompletion.CompletionFunc(cmd, args, toComplete)
+	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}
diff --git a/internal/utils/utils.go b/internal/loader/loader.go
similarity index 81%
rename from internal/utils/utils.go
rename to internal/loader/loader.go
index d0a9b1c68..4972038e7 100644
--- a/internal/utils/utils.go
+++ b/internal/loader/loader.go
@@ -1,4 +1,5 @@
-package utils
+// Package loader provides common utility functions and helper methods for the workflow CLI.
+package loader
 
 import (
 	"github.com/drycc/workflow-cli/pkg/git"
@@ -8,14 +9,12 @@ import (
 // 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
 		}
diff --git a/internal/utils/utils_test.go b/internal/loader/loader_test.go
similarity index 92%
rename from internal/utils/utils_test.go
rename to internal/loader/loader_test.go
index b8e2b8419..2a0ddc63a 100644
--- a/internal/utils/utils_test.go
+++ b/internal/loader/loader_test.go
@@ -1,4 +1,5 @@
-package utils
+// Package loader provides common utility functions and helper methods for the workflow CLI.
+package loader
 
 import (
 	"os"
diff --git a/internal/parser/apps.go b/internal/parser/apps.go
index a14547dd1..2848dd88f 100644
--- a/internal/parser/apps.go
+++ b/internal/parser/apps.go
@@ -1,3 +1,4 @@
+// Package parser provides command parsers for the Drycc CLI.
 package parser
 
 import (
@@ -58,7 +59,6 @@ func appsCreate(cmdr *commands.DryccCmd) *cobra.Command {
 
 // AppsList creates the apps list command
 func appsList(cmdr *commands.DryccCmd) *cobra.Command {
-
 	cmd := &cobra.Command{
 		Use:   "list",
 		Short: i18n.T("List accessible applications"),
@@ -74,7 +74,6 @@ func appsList(cmdr *commands.DryccCmd) *cobra.Command {
 
 // 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"),
@@ -93,7 +92,6 @@ func appsInfo(cmdr *commands.DryccCmd) *cobra.Command {
 
 // 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"),
diff --git a/internal/parser/autodeploy.go b/internal/parser/autodeploy.go
index 3d2774c4b..97164a865 100644
--- a/internal/parser/autodeploy.go
+++ b/internal/parser/autodeploy.go
@@ -9,7 +9,6 @@ import (
 
 // 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"),
diff --git a/internal/parser/builds.go b/internal/parser/builds.go
index 154290809..f936cc6dc 100644
--- a/internal/parser/builds.go
+++ b/internal/parser/builds.go
@@ -8,6 +8,7 @@ import (
 	"github.com/spf13/cobra"
 )
 
+// NewBuildsCommand creates a command for managing builds.
 func NewBuildsCommand(cmdr *commands.DryccCmd) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "builds",
diff --git a/internal/parser/certs.go b/internal/parser/certs.go
index 103ece957..de1c02b09 100644
--- a/internal/parser/certs.go
+++ b/internal/parser/certs.go
@@ -75,7 +75,6 @@ func certAddCommand(cmdr *commands.DryccCmd) *cobra.Command {
 }
 
 func certRemoveCommand(cmdr *commands.DryccCmd) *cobra.Command {
-
 	certCompletion := completion.CertCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
 	cmd := &cobra.Command{
 		Use: "remove <name>",
diff --git a/internal/parser/flags.go b/internal/parser/flags.go
index af43df267..1aa3c4d4d 100644
--- a/internal/parser/flags.go
+++ b/internal/parser/flags.go
@@ -1,5 +1,7 @@
 package parser
 
-var app string
-var version int
-var limit int
+var (
+	app     string
+	version int
+	limit   int
+)
diff --git a/internal/parser/git.go b/internal/parser/git.go
index 3c5fdbc46..a0698bd01 100644
--- a/internal/parser/git.go
+++ b/internal/parser/git.go
@@ -7,6 +7,7 @@ import (
 	"github.com/spf13/cobra"
 )
 
+// NewGitCommand creates a command for managing git repositories.
 func NewGitCommand(cmdr *commands.DryccCmd) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "git",
diff --git a/internal/parser/healthchecks.go b/internal/parser/healthchecks.go
index 16882a03a..f03e3ff17 100644
--- a/internal/parser/healthchecks.go
+++ b/internal/parser/healthchecks.go
@@ -17,6 +17,7 @@ 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",
@@ -44,7 +45,6 @@ func NewHealthchecksCommand(cmdr *commands.DryccCmd) *cobra.Command {
 }
 
 func healthchecksList(cmdr *commands.DryccCmd) *cobra.Command {
-
 	cmd := &cobra.Command{
 		Use:   "list",
 		Short: i18n.T("List healthchecks for an application"),
diff --git a/internal/parser/keys.go b/internal/parser/keys.go
index b72bc6de0..51dd36826 100644
--- a/internal/parser/keys.go
+++ b/internal/parser/keys.go
@@ -8,6 +8,7 @@ import (
 	"github.com/spf13/cobra"
 )
 
+// NewKeysCommand creates a command for managing SSH keys.
 func NewKeysCommand(cmdr *commands.DryccCmd) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "keys",
diff --git a/internal/parser/labels.go b/internal/parser/labels.go
index c21c8fd09..e4c54c069 100644
--- a/internal/parser/labels.go
+++ b/internal/parser/labels.go
@@ -8,6 +8,7 @@ import (
 	"github.com/spf13/cobra"
 )
 
+// NewLabelsCommand creates a command for managing application labels.
 func NewLabelsCommand(cmdr *commands.DryccCmd) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "labels",
diff --git a/internal/parser/limits.go b/internal/parser/limits.go
index 0eba6dcfc..58badbed9 100644
--- a/internal/parser/limits.go
+++ b/internal/parser/limits.go
@@ -8,6 +8,7 @@ import (
 	"github.com/spf13/cobra"
 )
 
+// NewLimitsCommand creates a command for managing resource limits.
 func NewLimitsCommand(cmdr *commands.DryccCmd) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "limits",
diff --git a/internal/parser/perms.go b/internal/parser/perms.go
index 2181ce309..5da85fb60 100644
--- a/internal/parser/perms.go
+++ b/internal/parser/perms.go
@@ -10,6 +10,7 @@ import (
 	"github.com/spf13/cobra"
 )
 
+// NewPermsCommand creates a command for managing permissions.
 func NewPermsCommand(cmdr *commands.DryccCmd) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "perms",
@@ -128,7 +129,6 @@ func permRemoveCommand(cmdr *commands.DryccCmd) *cobra.Command {
 		Short:             i18n.T("Remove a user permissions"),
 		ValidArgsFunction: permUsernameCompletion.CompletionFunc,
 		RunE: func(_ *cobra.Command, args []string) error {
-
 			flags.username = args[0]
 			return cmdr.PermDelete(app, flags.username)
 		},
diff --git a/internal/parser/ps.go b/internal/parser/ps.go
index a1dc900ff..738b87d71 100644
--- a/internal/parser/ps.go
+++ b/internal/parser/ps.go
@@ -8,6 +8,7 @@ import (
 	"github.com/spf13/cobra"
 )
 
+// NewPsCommand creates a command for managing processes.
 func NewPsCommand(cmdr *commands.DryccCmd) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "ps",
@@ -31,7 +32,6 @@ func NewPsCommand(cmdr *commands.DryccCmd) *cobra.Command {
 }
 
 func psListCommand(cmdr *commands.DryccCmd) *cobra.Command {
-
 	cmd := &cobra.Command{
 		Use:   "list",
 		Short: i18n.T("List application pods"),
diff --git a/internal/parser/pts.go b/internal/parser/pts.go
index c91af53c4..cac96bbe7 100644
--- a/internal/parser/pts.go
+++ b/internal/parser/pts.go
@@ -8,6 +8,7 @@ import (
 	"github.com/spf13/cobra"
 )
 
+// NewPtsCommand creates a command for managing process types.
 func NewPtsCommand(cmdr *commands.DryccCmd) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "pts",
diff --git a/internal/parser/registry.go b/internal/parser/registry.go
index c8cb40925..a77c32156 100644
--- a/internal/parser/registry.go
+++ b/internal/parser/registry.go
@@ -13,6 +13,7 @@ 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",
diff --git a/internal/parser/releases.go b/internal/parser/releases.go
index 01e04d703..e8291b35b 100644
--- a/internal/parser/releases.go
+++ b/internal/parser/releases.go
@@ -66,7 +66,6 @@ func releasesListCommand(cmdr *commands.DryccCmd) *cobra.Command {
 }
 
 func releasesInfoCommand(cmdr *commands.DryccCmd) *cobra.Command {
-
 	releaseCompletion := completion.ReleaseCompletion{AppID: &app, ConfigFile: &cmdr.ConfigFile}
 	cmd := &cobra.Command{
 		Use:  "info <version>",
@@ -146,7 +145,6 @@ func releasesRollbackCommand(cmdr *commands.DryccCmd) *cobra.Command {
 				versionStr = args[len(args)-1]
 			}
 			version, err := versionFromString(versionStr)
-
 			if err != nil {
 				return err
 			}
diff --git a/internal/parser/resources.go b/internal/parser/resources.go
index 89937d5dd..76ec8036a 100644
--- a/internal/parser/resources.go
+++ b/internal/parser/resources.go
@@ -10,6 +10,7 @@ import (
 	"github.com/spf13/cobra"
 )
 
+// NewResourcesCommand creates a command for managing resources.
 func NewResourcesCommand(cmdr *commands.DryccCmd) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "resources",
diff --git a/internal/parser/routes.go b/internal/parser/routes.go
index ba4895360..e75e86dc2 100644
--- a/internal/parser/routes.go
+++ b/internal/parser/routes.go
@@ -13,6 +13,7 @@ import (
 	"github.com/spf13/cobra"
 )
 
+// NewRoutesCommand creates a command for managing application routes.
 func NewRoutesCommand(cmdr *commands.DryccCmd) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "routes",
@@ -40,7 +41,6 @@ func NewRoutesCommand(cmdr *commands.DryccCmd) *cobra.Command {
 }
 
 func routesAddCommand(cmdr *commands.DryccCmd) *cobra.Command {
-
 	routeKindCompletion := completion.RouteKindCompletion{ArgsLen: 1}
 	cmd := &cobra.Command{
 		Use:  "add <name> <kind> <backend>...",
@@ -234,7 +234,6 @@ func routesDetachCommand(cmdr *commands.DryccCmd) *cobra.Command {
 }
 
 func routesRemoveCommand(cmdr *commands.DryccCmd) *cobra.Command {
-
 	routeCompletion := completion.RouteCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
 	cmd := &cobra.Command{
 		Use:  "remove <name>",
diff --git a/internal/parser/routing.go b/internal/parser/routing.go
index 86b643c8d..67db35bc3 100644
--- a/internal/parser/routing.go
+++ b/internal/parser/routing.go
@@ -7,6 +7,7 @@ import (
 	"github.com/spf13/cobra"
 )
 
+// NewRoutingCommand creates a command for managing application routing.
 func NewRoutingCommand(cmdr *commands.DryccCmd) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "routing",
diff --git a/internal/parser/services.go b/internal/parser/services.go
index 0a2c4ab9f..acb26d5ba 100644
--- a/internal/parser/services.go
+++ b/internal/parser/services.go
@@ -6,11 +6,11 @@ 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"
 )
 
+// NewServicesCommand creates a command for managing application services.
 func NewServicesCommand(cmdr *commands.DryccCmd) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "services",
diff --git a/internal/parser/shortcuts.go b/internal/parser/shortcuts.go
index dd6856545..08580813c 100644
--- a/internal/parser/shortcuts.go
+++ b/internal/parser/shortcuts.go
@@ -7,16 +7,26 @@ import (
 	"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/"
@@ -30,8 +40,7 @@ func (a *AuthShortcuts) Create(cmdr *commands.DryccCmd) []*cobra.Command {
 	return []*cobra.Command{login, logout, whoami}
 }
 
-type AppsShortcuts struct{}
-
+// 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>"
@@ -48,8 +57,7 @@ func (a *AppsShortcuts) Create(cmdr *commands.DryccCmd) []*cobra.Command {
 	return []*cobra.Command{appsCreate(cmdr), destroy, appsInfo(cmdr), appsOpen(cmdr), run}
 }
 
-type PsShortcuts struct{}
-
+// 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(
@@ -64,16 +72,20 @@ func (p *PsShortcuts) Create(cmdr *commands.DryccCmd) []*cobra.Command {
 	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(
diff --git a/internal/parser/tls.go b/internal/parser/tls.go
index 401033f33..431c8cf7b 100644
--- a/internal/parser/tls.go
+++ b/internal/parser/tls.go
@@ -61,9 +61,10 @@ func tlsForceCommand(cmdr *commands.DryccCmd) *cobra.Command {
 		ValidArgsFunction: TLSActionCompletion.CompletionFunc,
 		RunE: func(_ *cobra.Command, args []string) error {
 			action := args[0]
-			if action == "enable" {
+			switch action {
+			case "enable":
 				return cmdr.TLSForceEnable(app)
-			} else if action == "disable" {
+			case "disable":
 				return cmdr.TLSForceDisable(app)
 			}
 			return fmt.Errorf("invalid action: %s, please use 'enable' or 'disable'", action)
@@ -89,9 +90,10 @@ func tlsAutoCommand(cmdr *commands.DryccCmd) *cobra.Command {
 		ValidArgsFunction: TLSActionCompletion.CompletionFunc,
 		RunE: func(_ *cobra.Command, args []string) error {
 			action := args[0]
-			if action == "enable" {
+			switch action {
+			case "enable":
 				return cmdr.TLSAutoEnable(app)
-			} else if action == "disable" {
+			case "disable":
 				return cmdr.TLSAutoDisable(app)
 			}
 			return fmt.Errorf("invalid action: %s, please use 'enable' or 'disable'", action)
diff --git a/internal/parser/update.go b/internal/parser/update.go
index 380ab9d2f..78cb47d05 100644
--- a/internal/parser/update.go
+++ b/internal/parser/update.go
@@ -6,9 +6,10 @@ import (
 	"github.com/spf13/cobra"
 )
 
+// NewUpdateCommand creates a command for updating the workflow CLI.
 func NewUpdateCommand(cmdr *commands.DryccCmd) *cobra.Command {
 	var dryRun bool
-	var cmd = &cobra.Command{
+	cmd := &cobra.Command{
 		Use:   "update",
 		Args:  cobra.NoArgs,
 		Short: i18n.T("Update workflow cli to latest release"),
diff --git a/internal/parser/version.go b/internal/parser/version.go
index 5654f891c..c0ed0ef0d 100644
--- a/internal/parser/version.go
+++ b/internal/parser/version.go
@@ -6,7 +6,7 @@ import (
 	"github.com/spf13/cobra"
 )
 
-// Build an image from source code
+// NewVersionCommand creates a command for displaying the workflow CLI version.
 func NewVersionCommand(cmdr *commands.DryccCmd) *cobra.Command {
 	var flags struct {
 		all bool
diff --git a/internal/parser/volumes.go b/internal/parser/volumes.go
index efee15dba..7ab78aa7f 100644
--- a/internal/parser/volumes.go
+++ b/internal/parser/volumes.go
@@ -63,14 +63,15 @@ func volumesAddCommand(cmdr *commands.DryccCmd) *cobra.Command {
 		RunE: func(_ *cobra.Command, args []string) error {
 			flags.name = args[0]
 			flags.size = args[1]
-			parameters := make(map[string]interface{})
-			if flags.vtype == "nfs" {
-				parameters["nfs"] = map[string]interface{}{
+			parameters := make(map[string]any)
+			switch flags.vtype {
+			case "nfs":
+				parameters["nfs"] = map[string]any{
 					"server": flags.nfsServer,
 					"path":   flags.nfsPath,
 				}
-			} else if flags.vtype == "oss" {
-				parameters["oss"] = map[string]interface{}{
+			case "oss":
+				parameters["oss"] = map[string]any{
 					"server":     flags.ossServer,
 					"bucket":     flags.ossBucket,
 					"access_key": flags.ossAccessKey,
diff --git a/internal/template/template.go b/internal/template/template.go
index 8b99aca83..6e4ca0845 100644
--- a/internal/template/template.go
+++ b/internal/template/template.go
@@ -1,3 +1,4 @@
+// Package template provides custom template functions for CLI examples
 package template
 
 import (
@@ -6,6 +7,7 @@ import (
 	"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 {
diff --git a/pkg/git/git.go b/pkg/git/git.go
index a921052d2..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,7 +105,6 @@ 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 {
diff --git a/pkg/i18n/i18n.go b/pkg/i18n/i18n.go
index 4dedd9363..0d871dc11 100644
--- a/pkg/i18n/i18n.go
+++ b/pkg/i18n/i18n.go
@@ -14,6 +14,7 @@ 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 (
@@ -27,7 +28,6 @@ import (
 	"sync"
 
 	"github.com/chai2010/gettext-go"
-
 	"k8s.io/klog/v2"
 )
 
@@ -51,7 +51,8 @@ var knownTranslations = map[string][]string{
 
 var (
 	lazyLoadTranslationsOnce sync.Once
-	LoadTranslationsFunc     = func() error {
+	// LoadTranslationsFunc is the function called to lazy load translations.
+	LoadTranslationsFunc = func() error {
 		return LoadTranslations("drycc", nil)
 	}
 	translationsLoaded bool
diff --git a/pkg/settings/home_unix.go b/pkg/settings/home_unix.go
index 3e45a8421..54b2abb32 100644
--- a/pkg/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/pkg/settings/settings.go b/pkg/settings/settings.go
index 031add317..1d5d8c653 100644
--- a/pkg/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,8 +79,10 @@ 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 {
@@ -90,7 +91,7 @@ func (s *Settings) Save(cf string) (string, error) {
 
 	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.
@@ -98,7 +99,7 @@ func DryccHome() string {
 	dryccHome := filepath.Join(FindHome(), "/.drycc/")
 	_, err := os.Stat(dryccHome)
 	if os.IsNotExist(err) {
-		if err := os.MkdirAll(dryccHome, 0700); err != nil {
+		if err := os.MkdirAll(dryccHome, 0o700); err != nil {
 			panic(err)
 		}
 		return dryccHome
diff --git a/pkg/settings/settings_test.go b/pkg/settings/settings_test.go
index a708ede89..49a616e9f 100644
--- a/pkg/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
index 1a9104fa9..bbbcae4c7 100644
--- a/pkg/settings/utils.go
+++ b/pkg/settings/utils.go
@@ -17,22 +17,9 @@ func locateSettingsFile(cf string) string {
 		}
 		cf = homeDir + cf[1:]
 	}
-
-	cf = os.ExpandEnv(cf)
-
-	if cf == "" {
-		if v, ok := os.LookupEnv("DRYCC_PROFILE"); ok {
-			cf = v
-		} else {
-			cf = "client"
-		}
-	}
-
-	cf = filepath.Clean(cf)
-
+	cf = filepath.Clean(os.ExpandEnv(cf))
 	if filepathRegex.MatchString(cf) {
 		return cf
 	}
-
-	return filepath.Join(DryccHome(), cf+".json")
+	return filepath.Join(DryccHome(), "client.json")
 }
diff --git a/pkg/settings/utils_test.go b/pkg/settings/utils_test.go
index 1146a3a28..b6542c4db 100644
--- a/pkg/settings/utils_test.go
+++ b/pkg/settings/utils_test.go
@@ -17,7 +17,7 @@ func TestSelectSettings(t *testing.T) {
 	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", filepath.Join(FindHome(), "test.json")},
 		{"/opt/test.json", "/opt/test.json"},
@@ -30,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 a1367dff1..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 (
@@ -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/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.

From 716fb9a99bd2b69adbe9d77395e9866be3ce103e Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Thu, 18 Sep 2025 15:58:37 +0800
Subject: [PATCH 07/18] chore(build): remove 386 arch support

---
 scripts/build | 41 +++++++++++++++++++++++++++++------------
 1 file changed, 29 insertions(+), 12 deletions(-)

diff --git a/scripts/build b/scripts/build
index b465661df..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,20 +27,17 @@ go-build(){
     go build \
     -o _dist/drycc-$1-$GOOS-$GOARCH \
     drycc.go
-  if [[ "$GOOS" != "darwin" ]]; then
+  # 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

From 505eaa0926a2aeeea114ddb18884f6b4e190ae7e Mon Sep 17 00:00:00 2001
From: lijianguo <lijianguo1991@outlook.com>
Date: Thu, 25 Sep 2025 16:38:46 +0800
Subject: [PATCH 08/18] chore(workflow-cli): config push add merge param (#84)

---
 go.mod                                               |  2 +-
 go.sum                                               |  4 ++--
 internal/commands/commands.go                        |  4 ++--
 internal/commands/config.go                          | 12 ++++++------
 internal/commands/config_test.go                     |  2 +-
 internal/commands/healthchecks.go                    |  4 ++--
 internal/commands/limits.go                          |  4 ++--
 internal/commands/registry.go                        |  4 ++--
 internal/commands/tags.go                            |  4 ++--
 internal/commands/timeouts.go                        |  4 ++--
 internal/parser/config.go                            |  6 ++++--
 internal/parser/healthchecks.go                      |  1 +
 pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po |  4 ++++
 13 files changed, 31 insertions(+), 24 deletions(-)

diff --git a/go.mod b/go.mod
index d3a641215..4c4c5eade 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.25
 require (
 	github.com/chai2010/gettext-go v1.0.3
 	github.com/containerd/console v1.0.4
-	github.com/drycc/controller-sdk-go v0.0.0-20250917070349-910603c54927
+	github.com/drycc/controller-sdk-go v0.0.0-20250925082016-6b35f24ddb99
 	github.com/drycc/pkg v0.0.0-20250917064731-345368da3dbf
 	github.com/minio/selfupdate v0.6.0
 	github.com/olekukonko/tablewriter v0.0.5
diff --git a/go.sum b/go.sum
index 1bd67dc1c..d5ecba129 100644
--- a/go.sum
+++ b/go.sum
@@ -11,8 +11,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
 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-20250917070349-910603c54927 h1:c1n5N5urQztpcBy93U+oBEWyRXXaa0k3PwCg9pzXqe0=
-github.com/drycc/controller-sdk-go v0.0.0-20250917070349-910603c54927/go.mod h1:eHcmYwg81ASlP55/U587xnBZnZoeZnPHXGeQ8nYWnsg=
+github.com/drycc/controller-sdk-go v0.0.0-20250925082016-6b35f24ddb99 h1:rl63cNXjiTKi/gPbk7Gw0KL0sKJe64w7a9ol/J1DAS0=
+github.com/drycc/controller-sdk-go v0.0.0-20250925082016-6b35f24ddb99/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=
diff --git a/internal/commands/commands.go b/internal/commands/commands.go
index 4d5b42bcc..9189f153e 100644
--- a/internal/commands/commands.go
+++ b/internal/commands/commands.go
@@ -46,10 +46,10 @@ type Commander interface {
 	CertAttach(string, string, string) error
 	CertDetach(string, string, string) error
 	ConfigInfo(string, string, string, int) error
-	ConfigSet(string, string, string, []string, string) 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, string) error
+	ConfigPush(string, string, string, string, bool, string) error
 	ConfigAttach(string, string, string) error
 	ConfigDetach(string, string, string) error
 	DomainsList(string, int) error
diff --git a/internal/commands/config.go b/internal/commands/config.go
index 7f12138c0..bc892b21e 100644
--- a/internal/commands/config.go
+++ b/internal/commands/config.go
@@ -116,7 +116,7 @@ func (d *DryccCmd) ConfigInfo(appID string, ptype string, group string, version
 }
 
 // ConfigSet sets an app's config variables.
-func (d *DryccCmd) ConfigSet(appID string, ptype string, group string, configVars []string, confirm string) error {
+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
@@ -138,7 +138,7 @@ func (d *DryccCmd) ConfigSet(appID string, ptype string, group string, configVar
 
 	quit := progress(d.WOut)
 	configObj := api.Config{Values: configMap}
-	_, err = config.Set(s.Client, appID, configObj)
+	_, err = config.Set(s.Client, appID, configObj, merge)
 	quit <- true
 	<-quit
 	if d.checkAPICompatibility(s.Client, err) != nil {
@@ -180,7 +180,7 @@ func (d *DryccCmd) ConfigUnset(appID string, ptype string, group string, configV
 	}
 
 	configObj := api.Config{Values: valuesMaps}
-	_, err = config.Set(s.Client, appID, configObj)
+	_, err = config.Set(s.Client, appID, configObj, true)
 
 	quit <- true
 	<-quit
@@ -263,7 +263,7 @@ func (d *DryccCmd) ConfigPull(appID, ptype, group, fileName string, interactive
 }
 
 // ConfigPush pushes an app's config from a file.
-func (d *DryccCmd) ConfigPush(appID, ptype string, group string, fileName string, confirm string) error {
+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
@@ -309,7 +309,7 @@ func (d *DryccCmd) ConfigPush(appID, ptype string, group string, fileName string
 		}
 	}
 
-	return d.ConfigSet(appID, ptype, group, config, "yes")
+	return d.ConfigSet(appID, ptype, group, config, merge, "yes")
 }
 
 // ConfigAttach attaches config groups to a process type.
@@ -327,7 +327,7 @@ func (d *DryccCmd) ConfigAttach(appID string, ptype string, groups string) error
 		ptype: gs,
 	}
 	configObj := api.Config{ValuesRefs: refs}
-	_, err = config.Set(s.Client, appID, configObj)
+	_, err = config.Set(s.Client, appID, configObj, true)
 
 	quit <- true
 	<-quit
diff --git a/internal/commands/config_test.go b/internal/commands/config_test.go
index e496d3bd5..46e6614e0 100644
--- a/internal/commands/config_test.go
+++ b/internal/commands/config_test.go
@@ -139,7 +139,7 @@ func TestConfigSet(t *testing.T) {
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
-	err = cmdr.ConfigSet("foo", "web", "", []string{"TRUE=false", "DEBUG=true"}, "yes")
+	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
diff --git a/internal/commands/healthchecks.go b/internal/commands/healthchecks.go
index 425adbb4d..dc6896149 100644
--- a/internal/commands/healthchecks.go
+++ b/internal/commands/healthchecks.go
@@ -120,7 +120,7 @@ func (d *DryccCmd) HealthchecksSet(appID, healthcheckType, ptype string, probe *
 	configObj := api.Config{Healthcheck: make(map[string]*api.Healthchecks)}
 	configObj.Healthcheck[ptype] = &healthcheckMap
 
-	_, err = config.Set(s.Client, appID, configObj)
+	_, err = config.Set(s.Client, appID, configObj, true)
 
 	quit <- true
 	<-quit
@@ -157,7 +157,7 @@ func (d *DryccCmd) HealthchecksUnset(appID, ptype string, healthchecks []string)
 
 	configObj.Healthcheck = healthchecksMap
 
-	_, err = config.Set(s.Client, appID, configObj)
+	_, err = config.Set(s.Client, appID, configObj, true)
 
 	quit <- true
 	<-quit
diff --git a/internal/commands/limits.go b/internal/commands/limits.go
index e021de6c6..29b32f788 100644
--- a/internal/commands/limits.go
+++ b/internal/commands/limits.go
@@ -75,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 {
@@ -107,7 +107,7 @@ func (d *DryccCmd) LimitsUnset(appID string, limits []string) error {
 		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 {
diff --git a/internal/commands/registry.go b/internal/commands/registry.go
index a509c535f..831b231d8 100644
--- a/internal/commands/registry.go
+++ b/internal/commands/registry.go
@@ -67,7 +67,7 @@ func (d *DryccCmd) RegistrySet(appID, ptype, username, password string) error {
 	}
 	configObj.Registry = registry
 
-	_, 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 {
@@ -97,7 +97,7 @@ func (d *DryccCmd) RegistryUnset(appID, ptype string) error {
 		"password": nil,
 	}
 	configObj.Registry = registry
-	_, 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 {
diff --git a/internal/commands/tags.go b/internal/commands/tags.go
index 0157368c8..e0a009b28 100644
--- a/internal/commands/tags.go
+++ b/internal/commands/tags.go
@@ -69,7 +69,7 @@ func (d *DryccCmd) TagsSet(appID, ptype string, tags []string) error {
 	configObj := api.Config{Tags: make(map[string]api.ConfigTags)}
 	configObj.Tags[ptype] = tagsMap
 
-	_, 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 {
@@ -99,7 +99,7 @@ func (d *DryccCmd) TagsUnset(appID, ptype string, tags []string) error {
 	}
 	configObj.Tags[ptype] = configTags
 
-	_, 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 {
diff --git a/internal/commands/timeouts.go b/internal/commands/timeouts.go
index ecbde8510..9fb9b81df 100644
--- a/internal/commands/timeouts.go
+++ b/internal/commands/timeouts.go
@@ -56,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 {
@@ -89,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 {
diff --git a/internal/parser/config.go b/internal/parser/config.go
index 0da44bd81..6ad64553c 100644
--- a/internal/parser/config.go
+++ b/internal/parser/config.go
@@ -85,7 +85,7 @@ func configSetCommand(cmdr *commands.DryccCmd) *cobra.Command {
 		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, flags.confirm)
+			return cmdr.ConfigSet(app, configFlags.ptype, configFlags.group, args, true, flags.confirm)
 		},
 	}
 
@@ -174,6 +174,7 @@ read by foreman to load the local environment for your app.`),
 func configPushCommand(cmdr *commands.DryccCmd) *cobra.Command {
 	var flags struct {
 		path    string
+		merge   bool
 		confirm string
 	}
 
@@ -186,13 +187,14 @@ 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.confirm)
+			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")
diff --git a/internal/parser/healthchecks.go b/internal/parser/healthchecks.go
index f03e3ff17..2e0185ec0 100644
--- a/internal/parser/healthchecks.go
+++ b/internal/parser/healthchecks.go
@@ -178,6 +178,7 @@ port number to perform the socket connection on the Container.
 	}
 
 	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)
 
diff --git a/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po b/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po
index 9f89f224b..3d5bb985c 100644
--- a/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po
+++ b/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po
@@ -29,6 +29,10 @@ msgstr "用于推送应用程序代码的SSH公钥的本地文件路径"
 msgid "A path leading to an environment file"
 msgstr "指向环境文件的路径"
 
+#: internal/parser/config.go:179
+msgid "Merge config values"
+msgstr "合并配置"
+
 #: internal/parser/tls.go:121
 msgid "ACME account email"
 msgstr "ACME账户邮箱"

From ed7bc0d460051fd30cdc9921a5e9160c95f74481 Mon Sep 17 00:00:00 2001
From: lijianguo <lijianguo1991@outlook.com>
Date: Sun, 28 Sep 2025 17:04:27 +0800
Subject: [PATCH 09/18] chore(go): bump controller-sdk-go version (#85)

---
 go.mod | 2 +-
 go.sum | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/go.mod b/go.mod
index 4c4c5eade..5633be426 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.25
 require (
 	github.com/chai2010/gettext-go v1.0.3
 	github.com/containerd/console v1.0.4
-	github.com/drycc/controller-sdk-go v0.0.0-20250925082016-6b35f24ddb99
+	github.com/drycc/controller-sdk-go v0.0.0-20250930075415-578495a55414
 	github.com/drycc/pkg v0.0.0-20250917064731-345368da3dbf
 	github.com/minio/selfupdate v0.6.0
 	github.com/olekukonko/tablewriter v0.0.5
diff --git a/go.sum b/go.sum
index d5ecba129..116f7b3c4 100644
--- a/go.sum
+++ b/go.sum
@@ -11,8 +11,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
 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-20250925082016-6b35f24ddb99 h1:rl63cNXjiTKi/gPbk7Gw0KL0sKJe64w7a9ol/J1DAS0=
-github.com/drycc/controller-sdk-go v0.0.0-20250925082016-6b35f24ddb99/go.mod h1:eHcmYwg81ASlP55/U587xnBZnZoeZnPHXGeQ8nYWnsg=
+github.com/drycc/controller-sdk-go v0.0.0-20250930075415-578495a55414 h1:wiVKgwQsSEBbrQj92Lor6YknaAwPUVp1s/VmAmS3/lI=
+github.com/drycc/controller-sdk-go v0.0.0-20250930075415-578495a55414/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=

From 52ace1273d012038117308f02e94e5b538dafc6f Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Sat, 11 Oct 2025 13:59:24 +0800
Subject: [PATCH 10/18] chore(workflow-cli): use vim-tiny replace vim

---
 docker/Dockerfile | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index 69ed56a87..8bf84ecab 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -18,14 +18,16 @@ COPY --from=build /usr/local/bin/drycc /usr/local/bin/drycc
 
 ADD docker/rootfs /
 
-RUN install-packages bash-completion git vim \
+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 \
-  && ln /usr/bin/{ls,git,date,vim,cat,sed,clear,find,flock,bash,mkdir,sleep,dircolors} /usr/local/bin/ \
+  && 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
 

From 4973687bee22ba5dd6ae179265986a532f4523c1 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Wed, 15 Oct 2025 16:37:15 +0800
Subject: [PATCH 11/18] chore(volumes): add path_style for oss

---
 internal/parser/volumes.go                           | 5 ++++-
 pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.po | 8 ++++++--
 pkg/i18n/translations/drycc/template.pot             | 8 ++++++--
 pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po | 8 ++++++--
 4 files changed, 22 insertions(+), 7 deletions(-)

diff --git a/internal/parser/volumes.go b/internal/parser/volumes.go
index 7ab78aa7f..72e89e790 100644
--- a/internal/parser/volumes.go
+++ b/internal/parser/volumes.go
@@ -45,6 +45,7 @@ func volumesAddCommand(cmdr *commands.DryccCmd) *cobra.Command {
 		nfsPath      string
 		ossServer    string
 		ossBucket    string
+		ossPathStyle bool
 		ossAccessKey string
 		ossSecretKey string
 	}
@@ -76,6 +77,7 @@ func volumesAddCommand(cmdr *commands.DryccCmd) *cobra.Command {
 					"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)
@@ -89,7 +91,8 @@ func volumesAddCommand(cmdr *commands.DryccCmd) *cobra.Command {
 	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().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")
diff --git a/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.po b/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.po
index d393e8239..031f93385 100644
--- a/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.po
+++ b/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.po
@@ -1704,8 +1704,12 @@ msgid ""
 "             only single extra service per Porcfile type could be created."
 msgstr ""
 
-#: internal/parser/volumes.go:91
-msgid "secret access key for authentication"
+#: internal/parser/volumes.go:95
+msgid "Secret access key for authentication"
+msgstr ""
+
+#: internal/parser/volumes.go:97
+msgid "Force a path-style endpoint to be used"
 msgstr ""
 
 #: internal/parser/resources.go:224
diff --git a/pkg/i18n/translations/drycc/template.pot b/pkg/i18n/translations/drycc/template.pot
index ab77aacdd..582546283 100644
--- a/pkg/i18n/translations/drycc/template.pot
+++ b/pkg/i18n/translations/drycc/template.pot
@@ -1697,8 +1697,12 @@ msgid ""
 "             only single extra service per Porcfile type could be created."
 msgstr ""
 
-#: internal/parser/volumes.go:91
-msgid "secret access key for authentication"
+#: internal/parser/volumes.go:95
+msgid "Secret access key for authentication"
+msgstr ""
+
+#: internal/parser/volumes.go:97
+msgid "Force a path-style endpoint to be used"
 msgstr ""
 
 #: internal/parser/resources.go:224
diff --git a/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po b/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po
index 3d5bb985c..993385273 100644
--- a/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po
+++ b/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po
@@ -1783,10 +1783,14 @@ msgstr ""
 "要处理请求的Procfile类型（如webhooks），需绑定到PORT端口\n"
 "             每个Procfile类型只能创建一个附加服务"
 
-#: internal/parser/volumes.go:91
-msgid "secret access key for authentication"
+#: internal/parser/volumes.go:95
+msgid "Secret access key for authentication"
 msgstr "认证密钥（Secret Access Key）"
 
+#: internal/parser/volumes.go:97
+msgid "Force a path-style endpoint to be used"
+msgstr "强制使用路径风格的端点"
+
 #: internal/parser/resources.go:224
 msgid ""
 "skips the prompt for the resource name. <resource> is the uniquely "

From d2a2e22bbed6c1200bb1abf9978e69361eafd999 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Wed, 3 Dec 2025 13:21:57 +0800
Subject: [PATCH 12/18] feat(volumes): add volumes serve

---
 go.mod                                        |   2 +-
 go.sum                                        |   4 +-
 internal/commands/volumes.go                  | 219 +++---------------
 internal/commands/volumes_test.go             |  92 +++-----
 internal/parser/volumes.go                    |  60 ++---
 .../drycc/en_US/LC_MESSAGES/cli.po            |  26 +--
 pkg/i18n/translations/drycc/template.pot      |  26 +--
 .../drycc/zh_CN/LC_MESSAGES/cli.po            |  28 +--
 8 files changed, 94 insertions(+), 363 deletions(-)

diff --git a/go.mod b/go.mod
index 5633be426..9b150614f 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.25
 require (
 	github.com/chai2010/gettext-go v1.0.3
 	github.com/containerd/console v1.0.4
-	github.com/drycc/controller-sdk-go v0.0.0-20250930075415-578495a55414
+	github.com/drycc/controller-sdk-go v0.0.0-20251203071902-cc535b118251
 	github.com/drycc/pkg v0.0.0-20250917064731-345368da3dbf
 	github.com/minio/selfupdate v0.6.0
 	github.com/olekukonko/tablewriter v0.0.5
diff --git a/go.sum b/go.sum
index 116f7b3c4..5c6124b0c 100644
--- a/go.sum
+++ b/go.sum
@@ -11,8 +11,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
 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-20250930075415-578495a55414 h1:wiVKgwQsSEBbrQj92Lor6YknaAwPUVp1s/VmAmS3/lI=
-github.com/drycc/controller-sdk-go v0.0.0-20250930075415-578495a55414/go.mod h1:eHcmYwg81ASlP55/U587xnBZnZoeZnPHXGeQ8nYWnsg=
+github.com/drycc/controller-sdk-go v0.0.0-20251203071902-cc535b118251 h1:6bhcFnQn478sys8IXaeZW4OnurK64gTNMsu2NqKoCIw=
+github.com/drycc/controller-sdk-go v0.0.0-20251203071902-cc535b118251/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=
diff --git a/internal/commands/volumes.go b/internal/commands/volumes.go
index 0fa5e20bc..d00e39b8c 100644
--- a/internal/commands/volumes.go
+++ b/internal/commands/volumes.go
@@ -1,17 +1,16 @@
 package commands
 
 import (
+	"context"
 	"fmt"
-	"io"
-	"net/http"
 	"net/url"
 	"os"
-	"path"
+	"os/signal"
 	"regexp"
 	"strings"
+	"syscall"
 	"time"
 
-	drycc "github.com/drycc/controller-sdk-go"
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/volumes"
 	"github.com/drycc/workflow-cli/internal/loader"
@@ -161,17 +160,41 @@ func (d *DryccCmd) VolumesDelete(appID, name string) error {
 	return nil
 }
 
-// VolumesClient a client for manage volume file
-func (d *DryccCmd) VolumesClient(appID, cmd string, args ...string) error {
-	switch cmd {
-	case "ls":
-		return d.volumesClientLs(appID, args[0])
-	case "cp":
-		return d.volumesClientCp(appID, args[0], args[1])
-	case "rm":
-		return d.volumesClientRm(appID, args[0])
-	default:
-		return fmt.Errorf("unknown command %s", cmd)
+// 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()
+	quit := progress(d.WOut)
+	ctx, filer, err := volumes.Serve(parent, s.Client, appID, name)
+	quit <- true
+	<-quit
+	if err != nil {
+		return err
+	}
+
+	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, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP, syscall.SIGUSR1, syscall.SIGUSR2)
+	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)
+		}
 	}
 }
 
@@ -233,172 +256,6 @@ func (d *DryccCmd) VolumesUnmount(appID string, name string, volumeVars []string
 	return nil
 }
 
-// volumesClientLs get all directory entries sorted by filename.
-func (d *DryccCmd) volumesClientLs(appID, vol string) error {
-	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
-	if err != nil {
-		return err
-	}
-
-	name, path, err := parseVol(vol)
-	if err != nil {
-		return err
-	}
-	dirs, _, err := volumes.ListDir(s.Client, appID, name, path, 3000)
-	if err != nil {
-		return err
-	}
-
-	table := d.getDefaultFormatTable([]string{})
-	for _, dir := range dirs {
-		if dir.Type == "dir" {
-			dir.Name = fmt.Sprintf("%s/", dir.Name)
-		}
-		table.Append([]string{fmt.Sprintf("[%s]", d.formatTime(dir.Timestamp)), dir.Size, dir.Name})
-	}
-	table.Render()
-	return nil
-}
-
-func (d *DryccCmd) volumesClientGetAll(client *drycc.Client, appID, volumeID, volumePath, localPath string) error {
-	if _, err := os.Stat(localPath); err != nil && os.IsNotExist(err) {
-		os.MkdirAll(localPath, os.ModePerm)
-	}
-	dirs, _, err := volumes.ListDir(client, appID, volumeID, volumePath, 3000)
-	if err != nil {
-		return err
-	}
-	for _, dir := range dirs {
-		_, subpath := path.Split(dir.Path)
-		filepath := path.Join(localPath, subpath)
-		if dir.Type == "file" {
-			res, err := volumes.GetFile(client, appID, volumeID, dir.Path)
-			if err != nil {
-				return err
-			}
-			w, err := os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
-			if err != nil {
-				return err
-			}
-			bar := d.newProgressbar(res.ContentLength, "↓", filepath)
-			defer w.Close()
-			if _, err = io.Copy(io.MultiWriter(w, bar), res.Body); err != nil {
-				return err
-			}
-		} else {
-			os.MkdirAll(filepath, os.ModePerm)
-			if err := d.volumesClientGetAll(client, appID, volumeID, dir.Path, filepath); err != nil {
-				return err
-			}
-		}
-	}
-	return nil
-}
-
-func (d *DryccCmd) volumesClientPostAll(client *drycc.Client, appID, volumeID, volumePath, localPath string) error {
-	if file, err := os.Stat(localPath); err != nil {
-		return err
-	} else if !file.IsDir() {
-		file, err := os.Open(localPath)
-		if err != nil {
-			return err
-		}
-		defer file.Close()
-
-		stat, err := file.Stat()
-		if err != nil {
-			return err
-		}
-		if stat.Size() > 0 { // ignore empty file
-			reader := progressbar.NewReader(file, d.newProgressbar(stat.Size(), "↑", localPath))
-			if _, err := volumes.PostFile(client, appID, volumeID, volumePath, file.Name(), stat.Size(), &reader); err != nil {
-				return err
-			}
-		} else {
-			d.newProgressbar(1, "?", localPath).Finish()
-		}
-		return nil
-	}
-	if entries, err := os.ReadDir(localPath); err == nil {
-		for _, entry := range entries {
-			var dstFilepath string
-			if entry.IsDir() {
-				dstFilepath = path.Join(volumePath, entry.Name())
-			} else {
-				dstFilepath = volumePath
-			}
-			if err := d.volumesClientPostAll(client, appID, volumeID, dstFilepath, path.Join(localPath, entry.Name())); err != nil {
-				return err
-			}
-		}
-	} else {
-		return err
-	}
-	return nil
-}
-
-// volumesClientCp copy files between volume and local file
-func (d *DryccCmd) volumesClientCp(appID, src, dst string) error {
-	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
-	if err != nil {
-		return err
-	}
-	if strings.HasPrefix(src, "vol://") {
-		f, err := os.Stat(dst)
-		if err != nil {
-			return err
-		}
-		if !f.IsDir() {
-			return fmt.Errorf("the local path must be an existing dir")
-		}
-		volumeID, volumePath, err := parseVol(src)
-		if err != nil {
-			return err
-		}
-		if dirs, _, err := volumes.ListDir(s.Client, appID, volumeID, volumePath, 3000); err == nil && (len(dirs) != 1 || dirs[0].Type != "file") {
-			dst = mergeDestDir(dst, volumePath)
-		}
-		return d.volumesClientGetAll(s.Client, appID, volumeID, volumePath, dst)
-	} else if strings.HasPrefix(dst, "vol://") {
-		volumeID, volumePath, err := parseVol(dst)
-		if err != nil {
-			return err
-		}
-		if dirs, _, err := volumes.ListDir(s.Client, appID, volumeID, volumePath, 3000); err == nil {
-			names := strings.Split(strings.Trim(src, "/"), "/")
-			if len(dirs) == 1 && dirs[0].Type == "file" && strings.HasSuffix(strings.Trim(volumePath, "/"), names[len(names)-1]) {
-				return fmt.Errorf("the volume path cannot be an existing file")
-			}
-		}
-		if file, err := os.Stat(src); err == nil && file.IsDir() {
-			volumePath = mergeDestDir(volumePath, src)
-		}
-		return d.volumesClientPostAll(s.Client, appID, volumeID, volumePath, src)
-	}
-	return nil
-}
-
-// volumesClientRm delete a file from volume
-func (d *DryccCmd) volumesClientRm(appID, vol string) error {
-	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
-	if err != nil {
-		return err
-	}
-	host, path, err := parseVol(vol)
-	if err != nil {
-		return err
-	}
-	res, err := volumes.DeleteFile(s.Client, appID, host, path)
-	if err != nil {
-		return err
-	}
-	if res.StatusCode != http.StatusOK {
-		return fmt.Errorf("incorrect http status code %d", res.StatusCode)
-	}
-
-	return nil
-}
-
 func parseVolume(volumeVars []string) (map[string]any, error) {
 	volumeMap := make(map[string]any)
 	regex := regexp.MustCompile(`^([a-z0-9]+(?:-[a-z0-9]+)*)=(\/([\w]+[\w-]*\/?)+)$`)
diff --git a/internal/commands/volumes_test.go b/internal/commands/volumes_test.go
index cb8506047..30c29c3da 100644
--- a/internal/commands/volumes_test.go
+++ b/internal/commands/volumes_test.go
@@ -4,8 +4,9 @@ import (
 	"bytes"
 	"fmt"
 	"net/http"
-	"os"
+	"syscall"
 	"testing"
+	"time"
 
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/workflow-cli/pkg/testutil"
@@ -134,7 +135,7 @@ Updated:       2020-08-26T00:00:00UTC
 `)
 }
 
-func TestVolumesClientLs(t *testing.T) {
+func TestVolumesServe(t *testing.T) {
 	t.Parallel()
 	cf, server, err := testutil.NewTestServerAndClient()
 	if err != nil {
@@ -143,68 +144,43 @@ func TestVolumesClientLs(t *testing.T) {
 	defer server.Close()
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-	server.Mux.HandleFunc("/v2/apps/example-go/volumes/myvolume/client/", func(w http.ResponseWriter, _ *http.Request) {
+	server.Mux.HandleFunc("/v2/apps/example-go/volumes/myvolume/filer/_/ping", func(w http.ResponseWriter, _ *http.Request) {
 		testutil.SetHeaders(w)
-		fmt.Fprintf(w, `{"results": [
-  {"name":"handler.go","size":"4159","timestamp":"2024-06-25T22:55:16+08:00","type":"file","path":"/handler.go"},
-  {"name":"handler_test.go","size":"2310","timestamp":"2024-06-04T15:29:45+08:00","type":"file","path":"/handler_test.go"}
-], "count": 2}`)
+		fmt.Fprintf(w, `pong`)
 	})
-
-	err = cmdr.VolumesClient("example-go", "ls", "vol://myvolume")
-	assert.NoError(t, err)
-	assert.Contains(t, b.String(), "handler_test.go")
-}
-
-func TestVolumesClientCp(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	server.Mux.HandleFunc("/v2/apps/example-go/volumes/myvolume/client/", func(w http.ResponseWriter, r *http.Request) {
-		testutil.SetHeaders(w)
-		if r.URL.RawQuery == "path=etc" {
-			fmt.Fprintf(w, `{"results":[],"count":0}`)
-		} else if r.Method == http.MethodGet {
-			fmt.Fprintf(w, `{"results":[{"name":"hello.txt","size":"4159","timestamp":"2024-06-25T22:55:16+08:00","type":"file","path":"/hello.txt"}], "count": 1}`)
-		}
-	})
-	server.Mux.HandleFunc("/v2/apps/example-go/volumes/myvolume/client/hello.txt", func(w http.ResponseWriter, _ *http.Request) {
+	server.Mux.HandleFunc("/v2/apps/example-go/volumes/myvolume/filer/_/bind", func(w http.ResponseWriter, _ *http.Request) {
 		testutil.SetHeaders(w)
-		fmt.Fprintf(w, `hello word`)
+		fmt.Fprintf(w, `{"endpoint": "/v2/apps/example-go/volumes/myvolume/filer/webdav", "username": "user", "password": "pass"}`)
 	})
-	defer server.Close()
 
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-	// test download file
-	err = cmdr.VolumesClient("example-go", "cp", "vol://myvolume/hello.txt", "/tmp")
-	assert.NoError(t, err)
-	result, err := os.ReadFile("/tmp/hello.txt")
-	assert.NoError(t, err)
-	assert.Equal(t, string(result), `hello word`, "output")
-	// test upload file
-	err = cmdr.VolumesClient("example-go", "cp", "/tmp/hello.txt", "vol://myvolume/etc")
-	assert.NoError(t, err)
-}
-
-func TestVolumesClientRm(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
+	// 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")
 	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-	// test rm file
-	server.Mux.HandleFunc("/v2/apps/example-go/volumes/myvolume/client/etc/hello.txt", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-		w.WriteHeader(http.StatusOK)
-	})
-	err = cmdr.VolumesClient("example-go", "rm", "vol://myvolume/etc/hello.txt")
-	assert.NoError(t, err)
+
+	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) {
diff --git a/internal/parser/volumes.go b/internal/parser/volumes.go
index 72e89e790..1818a8156 100644
--- a/internal/parser/volumes.go
+++ b/internal/parser/volumes.go
@@ -30,7 +30,7 @@ func NewVolumesCommand(cmdr *commands.DryccCmd) *cobra.Command {
 	cmd.AddCommand(volumesExpandCommand(cmdr))
 	cmd.AddCommand(volumesInfoCommand(cmdr))
 	cmd.AddCommand(volumesRemoveCommand(cmdr))
-	cmd.AddCommand(volumesClientCommand(cmdr))
+	cmd.AddCommand(volumesServeCommand(cmdr))
 	cmd.AddCommand(volumesMountCommand(cmdr))
 	cmd.AddCommand(volumesUnmountCommand(cmdr))
 	return cmd
@@ -199,57 +199,27 @@ func volumesRemoveCommand(cmdr *commands.DryccCmd) *cobra.Command {
 	return cmd
 }
 
-func volumesClientCommand(cmdr *commands.DryccCmd) *cobra.Command {
-	cmd := &cobra.Command{
-		Use:   "client",
-		Short: i18n.T("The client used to manage volume files"),
+func volumesServeCommand(cmdr *commands.DryccCmd) *cobra.Command {
+	var flags struct {
+		name string
 	}
-	cmd.AddCommand(&cobra.Command{
-		Use: "ls <target>",
-		Example: template.CustomExample(
-			"drycc volumes client ls vol://myvolume/tmp",
-			map[string]string{
-				"<target>": i18n.T("The target path of volume"),
-			},
-		),
-		Short: i18n.T("List volume objects"),
-		Args:  cobra.ExactArgs(1),
-		RunE: func(_ *cobra.Command, args []string) error {
-			return cmdr.VolumesClient(app, "ls", args...)
-		},
-	})
-
-	cmd.AddCommand(&cobra.Command{
-		Use: "cp <source> <target>",
-		Example: template.CustomExample(
-			"drycc volumes client cp vol://myvolume/tmp /tmp",
-			map[string]string{
-				"<source>": i18n.T("The volume or local source path"),
-				"<target>": i18n.T("The volume or local target path"),
-			},
-		),
-		Short: i18n.T("Copy volume objects"),
-		Args:  cobra.ExactArgs(2),
-		RunE: func(_ *cobra.Command, args []string) error {
-			return cmdr.VolumesClient(app, "cp", args...)
-		},
-	})
-
-	cmd.AddCommand(&cobra.Command{
-		Use: "rm <target>",
+	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 client rm vol://myvolume/tmp",
+			"drycc volumes serve myvolume",
 			map[string]string{
-				"<target>": i18n.T("The target path of volume"),
+				"<name>": i18n.T("The volume name"),
 			},
 		),
-		Short: i18n.T("Remove volume objects"),
-		Args:  cobra.ExactArgs(1),
+		Args:              cobra.ExactArgs(1),
+		ValidArgsFunction: volumeCompletion.CompletionFunc,
 		RunE: func(_ *cobra.Command, args []string) error {
-			return cmdr.VolumesClient(app, "rm", args...)
+			flags.name = args[0]
+			return cmdr.VolumesServe(app, flags.name)
 		},
-	})
-
+	}
 	return cmd
 }
 
diff --git a/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.po b/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.po
index 031f93385..d7014b6c6 100644
--- a/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.po
+++ b/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.po
@@ -150,10 +150,6 @@ msgstr ""
 msgid "Configure automatic certificate management issuer details"
 msgstr ""
 
-#: internal/parser/volumes.go:227
-msgid "Copy volume objects"
-msgstr "Copy volume objects"
-
 #: internal/parser/apps.go:44
 msgid "Create a new application"
 msgstr ""
@@ -491,10 +487,6 @@ msgstr ""
 msgid "List tags for an application"
 msgstr ""
 
-#: internal/parser/volumes.go:211
-msgid "List volume objects"
-msgstr "List volume objects"
-
 #: internal/parser/volumes.go:134
 msgid "List volumes in the application"
 msgstr ""
@@ -884,10 +876,6 @@ msgstr ""
 msgid "Remove service from an application"
 msgstr ""
 
-#: internal/parser/volumes.go:242
-msgid "Remove volume objects"
-msgstr "Remove volume objects"
-
 #: internal/parser/keys.go:81
 msgid "Removes an SSH key for the logged in user"
 msgstr ""
@@ -1167,7 +1155,7 @@ msgid ""
 msgstr ""
 
 #: internal/parser/volumes.go:201
-msgid "The client used to manage volume files"
+msgid "Serve a WebDAV service for the volume"
 msgstr ""
 
 #: internal/parser/volumes.go:261 internal/parser/apps.go:158
@@ -1435,10 +1423,6 @@ msgstr ""
 msgid "The tag value"
 msgstr ""
 
-#: internal/parser/volumes.go:208 internal/parser/volumes.go:239
-msgid "The target path of volume"
-msgstr "The target path of volume"
-
 #: internal/parser/routes.go:187 internal/parser/routes.go:223
 msgid "The unique name of the gaetway"
 msgstr ""
@@ -1543,14 +1527,6 @@ msgstr ""
 msgid "The volume name to be removed"
 msgstr ""
 
-#: internal/parser/volumes.go:223
-msgid "The volume or local source path"
-msgstr "The volume or local source path"
-
-#: internal/parser/volumes.go:224
-msgid "The volume or local target path"
-msgstr "The volume or local target path"
-
 #: internal/parser/volumes.go:58 internal/parser/volumes.go:115
 msgid "The volume size, such as '500G'"
 msgstr ""
diff --git a/pkg/i18n/translations/drycc/template.pot b/pkg/i18n/translations/drycc/template.pot
index 582546283..0929c186a 100644
--- a/pkg/i18n/translations/drycc/template.pot
+++ b/pkg/i18n/translations/drycc/template.pot
@@ -150,10 +150,6 @@ msgstr ""
 msgid "Configure automatic certificate management issuer details"
 msgstr ""
 
-#: internal/parser/volumes.go:227
-msgid "Copy volume objects"
-msgstr ""
-
 #: internal/parser/apps.go:44
 msgid "Create a new application"
 msgstr ""
@@ -484,10 +480,6 @@ msgstr ""
 msgid "List tags for an application"
 msgstr ""
 
-#: internal/parser/volumes.go:211
-msgid "List volume objects"
-msgstr ""
-
 #: internal/parser/volumes.go:134
 msgid "List volumes in the application"
 msgstr ""
@@ -877,10 +869,6 @@ msgstr ""
 msgid "Remove service from an application"
 msgstr ""
 
-#: internal/parser/volumes.go:242
-msgid "Remove volume objects"
-msgstr ""
-
 #: internal/parser/keys.go:81
 msgid "Removes an SSH key for the logged in user"
 msgstr ""
@@ -1155,7 +1143,7 @@ msgid ""
 msgstr ""
 
 #: internal/parser/volumes.go:201
-msgid "The client used to manage volume files"
+msgid "Serve a WebDAV service for the volume"
 msgstr ""
 
 #: internal/parser/volumes.go:261 internal/parser/apps.go:158
@@ -1428,10 +1416,6 @@ msgstr ""
 msgid "The tag value"
 msgstr ""
 
-#: internal/parser/volumes.go:208 internal/parser/volumes.go:239
-msgid "The target path of volume"
-msgstr ""
-
 #: internal/parser/routes.go:187 internal/parser/routes.go:223
 msgid "The unique name of the gaetway"
 msgstr ""
@@ -1536,14 +1520,6 @@ msgstr ""
 msgid "The volume name to be removed"
 msgstr ""
 
-#: internal/parser/volumes.go:223
-msgid "The volume or local source path"
-msgstr ""
-
-#: internal/parser/volumes.go:224
-msgid "The volume or local target path"
-msgstr ""
-
 #: internal/parser/volumes.go:58 internal/parser/volumes.go:115
 msgid "The volume size, such as '500G'"
 msgstr ""
diff --git a/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po b/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po
index 993385273..0ec35cd79 100644
--- a/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po
+++ b/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po
@@ -155,10 +155,6 @@ msgstr "要解绑的域名通用名称"
 msgid "Configure automatic certificate management issuer details"
 msgstr "配置自动证书管理颁发者详情"
 
-#: internal/parser/volumes.go:227
-msgid "Copy volume objects"
-msgstr "复制卷对象"
-
 #: internal/parser/apps.go:44
 msgid "Create a new application"
 msgstr "创建新应用"
@@ -501,10 +497,6 @@ msgstr "列出服务器规格信息"
 msgid "List tags for an application"
 msgstr "列出应用标签"
 
-#: internal/parser/volumes.go:211
-msgid "List volume objects"
-msgstr "列出卷对象"
-
 #: internal/parser/volumes.go:134
 msgid "List volumes in the application"
 msgstr "列出应用卷"
@@ -894,10 +886,6 @@ msgstr "从应用移除网关"
 msgid "Remove service from an application"
 msgstr "从应用移除服务"
 
-#: internal/parser/volumes.go:242
-msgid "Remove volume objects"
-msgstr "删除卷对象"
-
 #: internal/parser/keys.go:81
 msgid "Removes an SSH key for the logged in user"
 msgstr "移除登录用户的SSH密钥"
@@ -1244,8 +1232,8 @@ msgstr ""
 "                  exec探测接受参数列表，httpGet和tcpSocket探测需指定端口号"
 
 #: internal/parser/volumes.go:201
-msgid "The client used to manage volume files"
-msgstr "用于管理卷文件的客户端"
+msgid "Serve a WebDAV service for the volume"
+msgstr "为卷提供WebDAV服务"
 
 #: internal/parser/volumes.go:261 internal/parser/apps.go:158
 #: internal/parser/shortcuts.go:44
@@ -1512,10 +1500,6 @@ msgstr "要清除的标签键"
 msgid "The tag value"
 msgstr "标签值"
 
-#: internal/parser/volumes.go:208 internal/parser/volumes.go:239
-msgid "The target path of volume"
-msgstr "卷的目标路径"
-
 #: internal/parser/routes.go:187 internal/parser/routes.go:223
 msgid "The unique name of the gaetway"
 msgstr "网关唯一名称"
@@ -1620,14 +1604,6 @@ msgstr "要查询信息的卷名称"
 msgid "The volume name to be removed"
 msgstr "要移除的卷名称"
 
-#: internal/parser/volumes.go:223
-msgid "The volume or local source path"
-msgstr "The volume or local source path"
-
-#: internal/parser/volumes.go:224
-msgid "The volume or local target path"
-msgstr "卷或本地目标路径"
-
 #: internal/parser/volumes.go:58 internal/parser/volumes.go:115
 msgid "The volume size, such as '500G'"
 msgstr "卷容量（如 '500G'）"

From dd5e5ca091ab8a18d6e59f353c91a6114b4bff09 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 3 Dec 2025 21:29:29 +0800
Subject: [PATCH 13/18] chore(deps): bump golang.org/x/crypto from 0.36.0 to
 0.45.0 (#86)

Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.36.0 to 0.45.0.
- [Commits](https://github.com/golang/crypto/compare/v0.36.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.45.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 go.mod |  8 ++++----
 go.sum | 16 ++++++++--------
 2 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/go.mod b/go.mod
index 9b150614f..5730fe4df 100644
--- a/go.mod
+++ b/go.mod
@@ -12,7 +12,7 @@ require (
 	github.com/schollz/progressbar/v3 v3.18.0
 	github.com/spf13/cobra v1.9.1
 	github.com/stretchr/testify v1.10.0
-	golang.org/x/net v0.38.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
@@ -30,7 +30,7 @@ require (
 	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.36.0 // indirect
-	golang.org/x/sys v0.31.0 // indirect
-	golang.org/x/term v0.30.0 // indirect
+	golang.org/x/crypto v0.45.0 // indirect
+	golang.org/x/sys v0.38.0 // indirect
+	golang.org/x/term v0.37.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 5c6124b0c..a2b182178 100644
--- a/go.sum
+++ b/go.sum
@@ -55,12 +55,12 @@ github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf
 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.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
-golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
+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.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
-golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+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=
@@ -68,12 +68,12 @@ golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7w
 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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
-golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+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/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
-golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
+golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
+golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
 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=

From 60c0d49dff4f0d3be6cde51556e8aa966dc35a83 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Thu, 4 Dec 2025 01:33:22 +0000
Subject: [PATCH 14/18] chore(volumes): optimize the output format of volume
 serve

---
 internal/commands/volumes.go | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/internal/commands/volumes.go b/internal/commands/volumes.go
index d00e39b8c..ddb0270e4 100644
--- a/internal/commands/volumes.go
+++ b/internal/commands/volumes.go
@@ -168,6 +168,8 @@ func (d *DryccCmd) VolumesServe(appID, name string) error {
 	}
 	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
@@ -176,6 +178,7 @@ func (d *DryccCmd) VolumesServe(appID, name string) error {
 		return err
 	}
 
+	d.Print("\n\n")
 	table := d.getDefaultFormatTable([]string{})
 	table.Append([]string{"Endpoint:", filer["endpoint"]})
 	table.Append([]string{"Username:", filer["username"]})

From a00e8f8758d9bb1cecc4a8ed35df6ffe29565482 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Mon, 8 Dec 2025 14:06:20 +0800
Subject: [PATCH 15/18] feat(lifecycle): add lifecycle support

---
 cmd/root.go                                   |   1 +
 go.mod                                        |   5 +-
 go.sum                                        |  12 +-
 internal/commands/apps_test.go                |   8 +-
 internal/commands/commands.go                 |   2 +-
 internal/commands/healthchecks.go             |  89 +-
 internal/commands/healthchecks_test.go        |   2 +-
 internal/commands/lifecycles.go               | 137 +++
 internal/commands/lifecycles_test.go          | 354 ++++++++
 internal/commands/pts.go                      |   6 +-
 internal/commands/volumes.go                  |  49 --
 internal/completion/completion.go             |  91 +-
 internal/parser/healthchecks.go               |  51 +-
 internal/parser/lifecycles.go                 | 235 ++++++
 internal/parser/parse.go                      |  31 +
 .../drycc/en_US/LC_MESSAGES/cli.mo            | Bin 3077 -> 8219 bytes
 .../drycc/en_US/LC_MESSAGES/cli.po            | 726 +++++++++-------
 pkg/i18n/translations/drycc/template.pot      | 668 ++++++++-------
 .../drycc/zh_CN/LC_MESSAGES/cli.mo            | Bin 42786 -> 46795 bytes
 .../drycc/zh_CN/LC_MESSAGES/cli.po            | 780 ++++++++++--------
 20 files changed, 2169 insertions(+), 1078 deletions(-)
 create mode 100644 internal/commands/lifecycles.go
 create mode 100644 internal/commands/lifecycles_test.go
 create mode 100644 internal/parser/lifecycles.go
 create mode 100644 internal/parser/parse.go

diff --git a/cmd/root.go b/cmd/root.go
index 09f7beeb4..7ff6bb002 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -49,6 +49,7 @@ func NewDryccCommand() *cobra.Command {
 	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))
diff --git a/go.mod b/go.mod
index 5730fe4df..1fa25e09f 100644
--- a/go.mod
+++ b/go.mod
@@ -5,11 +5,10 @@ go 1.25
 require (
 	github.com/chai2010/gettext-go v1.0.3
 	github.com/containerd/console v1.0.4
-	github.com/drycc/controller-sdk-go v0.0.0-20251203071902-cc535b118251
+	github.com/drycc/controller-sdk-go v0.0.0-20251206164108-e9369b636dd8
 	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/schollz/progressbar/v3 v3.18.0
 	github.com/spf13/cobra v1.9.1
 	github.com/stretchr/testify v1.10.0
 	golang.org/x/net v0.47.0
@@ -26,11 +25,9 @@ require (
 	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/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // 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
-	golang.org/x/term v0.37.0 // indirect
 )
diff --git a/go.sum b/go.sum
index a2b182178..db07c1bf1 100644
--- a/go.sum
+++ b/go.sum
@@ -3,16 +3,14 @@ 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/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
-github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
 github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
 github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
 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-20251203071902-cc535b118251 h1:6bhcFnQn478sys8IXaeZW4OnurK64gTNMsu2NqKoCIw=
-github.com/drycc/controller-sdk-go v0.0.0-20251203071902-cc535b118251/go.mod h1:eHcmYwg81ASlP55/U587xnBZnZoeZnPHXGeQ8nYWnsg=
+github.com/drycc/controller-sdk-go v0.0.0-20251206164108-e9369b636dd8 h1:EgzCbOPGeDmeClpCjPPwuCJ8viMWEMn7FiUyY1mJvoM=
+github.com/drycc/controller-sdk-go v0.0.0-20251206164108-e9369b636dd8/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=
@@ -31,8 +29,6 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
 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/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
-github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
 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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@@ -44,8 +40,6 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
 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/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA=
-github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
 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=
@@ -72,8 +66,6 @@ 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/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
-golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
 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=
diff --git a/internal/commands/apps_test.go b/internal/commands/apps_test.go
index 3569e1cc0..7077be753 100644
--- a/internal/commands/apps_test.go
+++ b/internal/commands/apps_test.go
@@ -326,7 +326,9 @@ func TestRemoteExists(t *testing.T) {
 
 	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/internal/commands/commands.go b/internal/commands/commands.go
index 9189f153e..c28859867 100644
--- a/internal/commands/commands.go
+++ b/internal/commands/commands.go
@@ -71,7 +71,7 @@ type Commander interface {
 	GitRemote(string, string, bool) error
 	GitRemove(string) error
 	HealthchecksList(string, string, int) error
-	HealthchecksSet(string, string, string, *api.Healthcheck) error
+	HealthchecksSet(string, string, string, *api.ContainerProbe) error
 	HealthchecksUnset(string, string, []string) error
 	KeysList(int) error
 	KeyRemove(string) error
diff --git a/internal/commands/healthchecks.go b/internal/commands/healthchecks.go
index dc6896149..ad7b3badf 100644
--- a/internal/commands/healthchecks.go
+++ b/internal/commands/healthchecks.go
@@ -9,40 +9,44 @@ import (
 	"github.com/drycc/workflow-cli/internal/loader"
 )
 
-func getHealthcheckString(ptype, probeType string, healthcheck *api.Healthcheck) string {
+func getContainerProbeString(ptype, probeType string, containerProbe *api.ContainerProbe) string {
 	params := fmt.Sprintf(
 		"delay=%ds timeout=%ds period=%ds #success=%d #failure=%d",
-		healthcheck.InitialDelaySeconds,
-		healthcheck.TimeoutSeconds,
-		healthcheck.PeriodSeconds,
-		healthcheck.SuccessThreshold,
-		healthcheck.FailureThreshold,
+		containerProbe.InitialDelaySeconds,
+		containerProbe.TimeoutSeconds,
+		containerProbe.PeriodSeconds,
+		containerProbe.SuccessThreshold,
+		containerProbe.FailureThreshold,
 	)
 
-	if healthcheck.Exec != nil {
-		return fmt.Sprintf("%s %s exec %v %s", probeType, ptype, healthcheck.Exec.Command, params)
-	} else if healthcheck.TCPSocket != nil {
-		return fmt.Sprintf("%s %s tcp-socket port=%v %s", probeType, ptype, healthcheck.TCPSocket.Port, params)
-	} else if healthcheck.HTTPGet != nil {
+	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,
-			healthcheck.HTTPGet.HTTPHeaders,
-			healthcheck.HTTPGet.Path,
-			healthcheck.HTTPGet.Port,
+			containerProbe.HTTPGet.HTTPHeaders,
+			containerProbe.HTTPGet.Path,
+			containerProbe.HTTPGet.Port,
 			params,
 		)
 	}
 	return ""
 }
 
-func getHealthchecksStrings(ptype string, healthchecks *api.Healthchecks) []string {
-	var probes []string
-	for key := range *healthchecks {
-		probes = append(probes, getHealthcheckString(ptype, key, (*healthchecks)[key]))
+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 probes
+	return containerProbes
 }
 
 // HealthchecksList lists an app's healthchecks.
@@ -105,7 +109,7 @@ func (d *DryccCmd) HealthchecksList(appID, ptype string, version int) error {
 }
 
 // HealthchecksSet sets an app's healthchecks.
-func (d *DryccCmd) HealthchecksSet(appID, healthcheckType, ptype string, probe *api.Healthcheck) error {
+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
@@ -114,11 +118,19 @@ func (d *DryccCmd) HealthchecksSet(appID, healthcheckType, ptype string, probe *
 	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[ptype] = &healthcheckMap
+	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)
 
@@ -135,7 +147,7 @@ func (d *DryccCmd) HealthchecksSet(appID, healthcheckType, ptype string, probe *
 }
 
 // HealthchecksUnset removes an app's healthchecks.
-func (d *DryccCmd) HealthchecksUnset(appID, ptype string, healthchecks []string) error {
+func (d *DryccCmd) HealthchecksUnset(appID, ptype string, containerProbeTypes []string) error {
 	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
 	if err != nil {
 		return err
@@ -147,16 +159,23 @@ func (d *DryccCmd) HealthchecksUnset(appID, ptype string, healthchecks []string)
 
 	configObj := api.Config{}
 
-	healthchecksMap := make(map[string]*api.Healthchecks)
-	healthcheckMap := make(api.Healthchecks)
-
-	for _, healthcheck := range healthchecks {
-		healthcheckMap[healthcheck] = nil
+	healthcheckMap := make(map[string]*api.Healthcheck)
+	var nullContainerProbe *api.ContainerProbe = nil
+	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
 	}
-	healthchecksMap[ptype] = &healthcheckMap
-
-	configObj.Healthcheck = healthchecksMap
-
+	configObj.Healthcheck = healthcheckMap
 	_, err = config.Set(s.Client, appID, configObj, true)
 
 	quit <- true
diff --git a/internal/commands/healthchecks_test.go b/internal/commands/healthchecks_test.go
index 67ba9b99d..7399ead6e 100644
--- a/internal/commands/healthchecks_test.go
+++ b/internal/commands/healthchecks_test.go
@@ -208,7 +208,7 @@ 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
 
diff --git a/internal/commands/lifecycles.go b/internal/commands/lifecycles.go
new file mode 100644
index 000000000..0f8f66ea6
--- /dev/null
+++ b/internal/commands/lifecycles.go
@@ -0,0 +1,137 @@
+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{"Owner:", config.Owner})
+	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 {
+			table.Append([]string{"", fmt.Sprintf("stopSignal=%s", lifecycle.StopSignal)})
+			table.Append([]string{"", getLifecycleHandlerString(ptype, "postStart", lifecycle.StopSignal, *lifecycle.PostStart)})
+			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 = nil
+	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..475a64e0d
--- /dev/null
+++ b/internal/commands/lifecycles_test.go
@@ -0,0 +1,354 @@
+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, "Owner:        bar")
+	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, "Owner:        bar")
+	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, "Owner:        bar")
+	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/internal/commands/pts.go b/internal/commands/pts.go
index 78ce5f0dc..eb21073bb 100644
--- a/internal/commands/pts.go
+++ b/internal/commands/pts.go
@@ -193,15 +193,15 @@ func printProcessTypeDetail(d *DryccCmd, ptypeStates api.PtypeStates, events api
 				tpt.Append([]string{"", fmt.Sprintf("%s from %s", mount.MountPath, mount.Name)})
 			}
 		}
-		sp := getHealthcheckString("", "", &ptypeState.StartupProbe)
+		sp := getContainerProbeString("", "", &ptypeState.StartupProbe)
 		if sp != "" {
 			tpt.Append([]string{"Startup:", strings.TrimSpace(sp)})
 		}
-		lp := getHealthcheckString("", "", &ptypeState.LivenessProbe)
+		lp := getContainerProbeString("", "", &ptypeState.LivenessProbe)
 		if lp != "" {
 			tpt.Append([]string{"Liveness:", strings.TrimSpace(lp)})
 		}
-		rp := getHealthcheckString("", "", &ptypeState.ReadinessProbe)
+		rp := getContainerProbeString("", "", &ptypeState.ReadinessProbe)
 		if rp != "" {
 			tpt.Append([]string{"Readiness:", strings.TrimSpace(rp)})
 		}
diff --git a/internal/commands/volumes.go b/internal/commands/volumes.go
index ddb0270e4..a70432de2 100644
--- a/internal/commands/volumes.go
+++ b/internal/commands/volumes.go
@@ -3,18 +3,15 @@ package commands
 import (
 	"context"
 	"fmt"
-	"net/url"
 	"os"
 	"os/signal"
 	"regexp"
-	"strings"
 	"syscall"
 	"time"
 
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/volumes"
 	"github.com/drycc/workflow-cli/internal/loader"
-	"github.com/schollz/progressbar/v3"
 	"sigs.k8s.io/yaml"
 )
 
@@ -274,27 +271,6 @@ func parseVolume(volumeVars []string) (map[string]any, error) {
 	return volumeMap, nil
 }
 
-// parseVol format volume url
-func parseVol(vol string) (string, string, error) {
-	u, err := url.Parse(vol)
-	if err != nil {
-		return "", "", err
-	}
-	if u.Scheme != "vol" || u.Host == "" {
-		return "", "", fmt.Errorf("vol %s format err", vol)
-	}
-	return u.Host, strings.TrimPrefix(u.Path, "/"), nil
-}
-
-// mergeDestDir merge dest dir
-func mergeDestDir(prefix, dir string) string {
-	if !strings.HasSuffix(dir, "/") {
-		names := strings.Split(dir, "/")
-		return strings.Join([]string{prefix, names[len(names)-1]}, "/")
-	}
-	return prefix
-}
-
 // printVolumes format volume data
 func printVolumes(d *DryccCmd, volumes api.Volumes) {
 	table := d.getDefaultFormatTable([]string{"NAME", "OWNER", "TYPE", "PTYPE", "PATH", "SIZE"})
@@ -309,28 +285,3 @@ func printVolumes(d *DryccCmd, volumes api.Volumes) {
 	}
 	table.Render()
 }
-
-func (d *DryccCmd) newProgressbar(maxBytes int64, icon, description string) *progressbar.ProgressBar {
-	return progressbar.NewOptions64(
-		maxBytes,
-		progressbar.OptionSetDescription(description),
-		progressbar.OptionSetWriter(os.Stderr),
-		progressbar.OptionShowBytes(true),
-		progressbar.OptionEnableColorCodes(true),
-		progressbar.OptionSetWidth(10),
-		progressbar.OptionThrottle(65*time.Millisecond),
-		progressbar.OptionShowCount(),
-		progressbar.OptionOnCompletion(func() { fmt.Fprint(os.Stderr, "\n") }),
-		progressbar.OptionSpinnerType(14),
-		progressbar.OptionFullWidth(),
-		progressbar.OptionSetRenderBlankState(true),
-		progressbar.OptionSetDescription(fmt.Sprintf("[cyan][%s][reset] %s", icon, d.fixateString(description, 32))),
-		progressbar.OptionSetTheme(progressbar.Theme{
-			Saucer:        "[green]=[reset]",
-			SaucerHead:    "[green]>[reset]",
-			SaucerPadding: " ",
-			BarStart:      "[",
-			BarEnd:        "]",
-		}),
-	)
-}
diff --git a/internal/completion/completion.go b/internal/completion/completion.go
index b0b1abec9..6be57bc90 100644
--- a/internal/completion/completion.go
+++ b/internal/completion/completion.go
@@ -216,17 +216,16 @@ func (c *GatewayNameCompletion) CompletionFunc(_ *cobra.Command, args []string,
 	return results, cobra.ShellCompDirectiveNoFileComp
 }
 
-// HealthTypeCompletion provides completion for health check types
-type HealthTypeCompletion struct {
+// LifecycleHandlerCompletion provides completion for lifecycle handler types
+type LifecycleHandlerCompletion struct {
 	ArgsLen    int
 	ConfigFile *string
 }
 
-// CompletionFunc returns a list of health check types for completion
-func (c *HealthTypeCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
-	healthTypes := []string{"startupProbe", "livenessProbe", "readinessProbe"}
+// 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) {
@@ -239,14 +238,76 @@ func (c *HealthTypeCompletion) CompletionFunc(_ *cobra.Command, args []string, t
 	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)
+}
+
 // ProbeTypeCompletion provides completion for probe types
-type ProbeTypeCompletion struct {
+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 probe types for completion
-func (c *ProbeTypeCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+// 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 {
 
@@ -263,19 +324,19 @@ func (c *ProbeTypeCompletion) CompletionFunc(_ *cobra.Command, args []string, to
 }
 
 // HealthChecksCompletion provides completion for health checks
-type HealthChecksCompletion struct {
+type HealthCheckCompletion struct {
 	ArgsLen    int
 	ConfigFile *string
 }
 
 // CompletionFunc returns a list of health checks for completion
-func (c *HealthChecksCompletion) CompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+func (c *HealthCheckCompletion) CompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 	if len(args) == 0 {
-		healthTypeCompletion := HealthTypeCompletion{ArgsLen: 0, ConfigFile: c.ConfigFile}
-		return healthTypeCompletion.CompletionFunc(cmd, args, toComplete)
+		healthcheckProbeTypeCompletion := HealthcheckProbeTypeCompletion{ArgsLen: 0, ConfigFile: c.ConfigFile}
+		return healthcheckProbeTypeCompletion.CompletionFunc(cmd, args, toComplete)
 	}
-	probeTypeCompletion := ProbeTypeCompletion{ArgsLen: 1, ConfigFile: c.ConfigFile}
-	return probeTypeCompletion.CompletionFunc(cmd, args, toComplete)
+	healthcheckProbeActionTypeCompletion := HealthcheckProbeActionTypeCompletion{ArgsLen: 1, ConfigFile: c.ConfigFile}
+	return healthcheckProbeActionTypeCompletion.CompletionFunc(cmd, args, toComplete)
 }
 
 // ServiceProtocolCompletion provides completion for service protocols
diff --git a/internal/parser/healthchecks.go b/internal/parser/healthchecks.go
index 2e0185ec0..7510d937c 100644
--- a/internal/parser/healthchecks.go
+++ b/internal/parser/healthchecks.go
@@ -63,8 +63,6 @@ func healthchecksList(cmdr *commands.DryccCmd) *cobra.Command {
 func healthchecksSet(cmdr *commands.DryccCmd) *cobra.Command {
 	var flags struct {
 		ptype         string
-		healthType    string
-		probeType     string
 		path          string
 		port          int
 		headers       string
@@ -76,15 +74,15 @@ func healthchecksSet(cmdr *commands.DryccCmd) *cobra.Command {
 		commandArgs   []string
 	}
 
-	healthChecksCompletion := completion.HealthChecksCompletion{ConfigFile: &cmdr.ConfigFile}
+	healthCheckCompletion := completion.HealthCheckCompletion{ConfigFile: &cmdr.ConfigFile}
 	cmd := &cobra.Command{
-		Use:  "set <health-type> <probe-type> [flags] [--] <args>...",
+		Use:  "set <probe> <action> [flags] [--] <args>...",
 		Args: cobra.MinimumNArgs(2),
 		Example: template.CustomExample(
 			"drycc healthchecks set readinessProbe httpGet --path=/health -- 8000",
 			map[string]string{
-				"<health-type>": i18n.T("The healthcheck type, such as 'startupProbe' 'livenessProbe' or 'readinessProbe'"),
-				"<probe-type>":  i18n.T("the healthcheck probe type, such as 'httpGet', 'exec' or 'tcpSocket'"),
+				"<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.`),
 			},
@@ -122,12 +120,12 @@ probes accept a string of arguments to be run inside the Container.
 considered healthy if the check can establish a connection. 'tcpSocket' probes accept a
 port number to perform the socket connection on the Container.
 `),
-		ValidArgsFunction: healthChecksCompletion.CompletionFunc,
+		ValidArgsFunction: healthCheckCompletion.CompletionFunc,
 		RunE: func(_ *cobra.Command, args []string) error {
 			healthType := args[0]
 			probeType := args[1]
 
-			probe := &api.Healthcheck{
+			probe := &api.ContainerProbe{
 				InitialDelaySeconds: flags.initialDelay,
 				TimeoutSeconds:      flags.timeout,
 				PeriodSeconds:       flags.period,
@@ -150,14 +148,14 @@ port number to perform the socket connection on the Container.
 				if err != nil {
 					return fmt.Errorf("could not parse port: %s", err)
 				}
-				probe.HTTPGet = &api.HTTPGetProbe{
+				probe.HTTPGet = &api.HTTPGetAction{
 					Path:        flags.path,
 					Port:        port,
 					HTTPHeaders: parsedHeaders,
 				}
 			case "exec":
 				probeArgs := args[2:]
-				probe.Exec = &api.ExecProbe{
+				probe.Exec = &api.ExecAction{
 					Command: probeArgs,
 				}
 			case "tcpSocket":
@@ -166,7 +164,7 @@ port number to perform the socket connection on the Container.
 					return fmt.Errorf("could not parse port: %s", err)
 				}
 
-				probe.TCPSocket = &api.TCPSocketProbe{
+				probe.TCPSocket = &api.TCPSocketAction{
 					Port: port,
 				}
 			default:
@@ -201,18 +199,18 @@ func healthchecksUnset(cmdr *commands.DryccCmd) *cobra.Command {
 		healths []string
 	}
 
-	healthTypeCompletion := completion.HealthTypeCompletion{ConfigFile: &cmdr.ConfigFile}
+	healthcheckProbeTypeCompletion := completion.HealthcheckProbeTypeCompletion{ConfigFile: &cmdr.ConfigFile}
 	cmd := &cobra.Command{
-		Use:  "unset <health-type>...",
+		Use:  "unset <probe>...",
 		Args: cobra.MinimumNArgs(1),
 		Example: template.CustomExample(
 			"drycc healthchecks unset startupProbe",
 			map[string]string{
-				"<health-type>": i18n.T("the healthcheck type, such as 'startupProbe' 'livenessProbe' or 'readinessProbe'"),
+				"<probe>": i18n.T("the healthcheck probe, such as 'startupProbe' 'livenessProbe' or 'readinessProbe'"),
 			},
 		),
 		Short:             i18n.T("Unset healthchecks for an application"),
-		ValidArgsFunction: healthTypeCompletion.CompletionFunc,
+		ValidArgsFunction: healthcheckProbeTypeCompletion.CompletionFunc,
 		RunE: func(_ *cobra.Command, args []string) error {
 			flags.healths = args
 			for healthcheck := range flags.healths {
@@ -230,29 +228,6 @@ func healthchecksUnset(cmdr *commands.DryccCmd) *cobra.Command {
 	return cmd
 }
 
-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{
diff --git a/internal/parser/lifecycles.go b/internal/parser/lifecycles.go
new file mode 100644
index 000000000..46cff7a33
--- /dev/null
+++ b/internal/parser/lifecycles.go
@@ -0,0 +1,235 @@
+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
+}
+
+// NewLifecycleCommand 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(2),
+		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"))
+	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/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/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.mo b/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.mo
index 2bee5684ce0df937645b5db31958bf2e765b7fa7..7362b2067b1a710ffc17b16f93aa4957add9ccc8 100644
GIT binary patch
diff --git a/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.po b/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.po
index d7014b6c6..5fed8e04d 100644
--- a/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.po
+++ b/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: \n"
 "Report-Msgid-Bugs-To: EMAIL\n"
-"POT-Creation-Date: 2025-04-14 15:39+0800\n"
+"POT-Creation-Date: 2025-12-08 16:06+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"
@@ -17,31 +17,31 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: internal/parser/builds.go:58 internal/parser/builds.go:105
+#: 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:52
+#: 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:195
+#: internal/parser/config.go:158 internal/parser/config.go:196
 msgid "A path leading to an environment file"
 msgstr ""
 
-#: internal/parser/tls.go:121
+#: internal/parser/tls.go:123
 msgid "ACME account email"
 msgstr ""
 
-#: internal/parser/tls.go:122
+#: internal/parser/tls.go:124
 msgid "ACME server URL"
 msgstr ""
 
-#: internal/parser/volumes.go:90
+#: internal/parser/volumes.go:93
 msgid "Access key id for authentication"
 msgstr ""
 
-#: internal/parser/tls.go:55 internal/parser/tls.go:83
+#: internal/parser/tls.go:55 internal/parser/tls.go:84
 msgid "Action to perform, 'enable' or 'disable'"
 msgstr ""
 
@@ -49,11 +49,11 @@ msgstr ""
 msgid "Add a token for controller authentication"
 msgstr ""
 
-#: internal/parser/perms.go:71
+#: internal/parser/perms.go:72
 msgid "Add a user permissions"
 msgstr ""
 
-#: internal/parser/keys.go:56
+#: internal/parser/keys.go:57
 msgid "Add an SSH key"
 msgstr ""
 
@@ -61,15 +61,15 @@ msgstr ""
 msgid "Add an SSL certificate to an app"
 msgstr ""
 
-#: internal/parser/labels.go:59
+#: internal/parser/labels.go:60
 msgid "Add new application's label"
 msgstr ""
 
-#: internal/parser/keys.go:57
+#: internal/parser/keys.go:58
 msgid "Adds SSH keys for the logged in user"
 msgstr ""
 
-#: internal/parser/git.go:35
+#: internal/parser/git.go:36
 msgid "Adds git remote of application to repository"
 msgstr ""
 
@@ -85,11 +85,11 @@ msgstr ""
 msgid "Application name"
 msgstr ""
 
-#: internal/parser/certs.go:136
+#: internal/parser/certs.go:135
 msgid "Attach a certificate to a domain"
 msgstr ""
 
-#: internal/parser/certs.go:135
+#: internal/parser/certs.go:134
 msgid "Attach an SSL certificate to a domain"
 msgstr ""
 
@@ -101,7 +101,7 @@ msgstr ""
 msgid "Authenticate against a controller"
 msgstr ""
 
-#: internal/parser/tls.go:88
+#: internal/parser/tls.go:89
 msgid "Automatic certificate management"
 msgstr ""
 
@@ -109,23 +109,23 @@ msgstr ""
 msgid "Bind a domain to an application"
 msgstr ""
 
-#: internal/parser/resources.go:241
+#: internal/parser/resources.go:242
 msgid "Bind a resource for an application"
 msgstr ""
 
-#: internal/parser/volumes.go:89
+#: internal/parser/volumes.go:92
 msgid "Bucket name in object storage"
 msgstr ""
 
-#: internal/parser/tls.go:123
+#: internal/parser/tls.go:125
 msgid "CA key ID"
 msgstr ""
 
-#: internal/parser/tls.go:124
+#: internal/parser/tls.go:126
 msgid "CA key secret"
 msgstr ""
 
-#: internal/parser/pts.go:156
+#: internal/parser/pts.go:157
 msgid "Clean process types of not used"
 msgstr ""
 
@@ -133,20 +133,20 @@ msgstr ""
 msgid "Clear the current user session"
 msgstr ""
 
-#: internal/parser/apps.go:172
+#: internal/parser/apps.go:141
 msgid "Command execution timeout in seconds"
 msgstr ""
 
-#: internal/parser/certs.go:131
+#: 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:156
+#: internal/parser/certs.go:155
 msgid "Common name of the domain to detach from"
 msgstr ""
 
-#: internal/parser/tls.go:115
+#: internal/parser/tls.go:117
 msgid "Configure automatic certificate management issuer details"
 msgstr ""
 
@@ -154,7 +154,7 @@ msgstr ""
 msgid "Create a new application"
 msgstr ""
 
-#: internal/parser/resources.go:97
+#: internal/parser/resources.go:98
 msgid "Create a resource for the application"
 msgstr ""
 
@@ -162,7 +162,7 @@ msgstr ""
 msgid "Create a route for an application"
 msgstr ""
 
-#: internal/parser/volumes.go:61
+#: internal/parser/volumes.go:62
 msgid "Create a volume for the application"
 msgstr ""
 
@@ -180,7 +180,7 @@ msgid ""
 "automatically"
 msgstr ""
 
-#: internal/parser/builds.go:45
+#: internal/parser/builds.go:53
 msgid ""
 "Creates a new build of an application. Imports an <image> and deploys it to "
 "Drycc\n"
@@ -205,11 +205,11 @@ msgstr ""
 msgid "Creates routes for an application, provides a way to route requests"
 msgstr ""
 
-#: internal/parser/resources.go:216
+#: internal/parser/resources.go:217
 msgid "Delete a resource from the application"
 msgstr ""
 
-#: internal/parser/volumes.go:186
+#: internal/parser/volumes.go:190
 msgid "Delete a volume from the application"
 msgstr ""
 
@@ -221,23 +221,23 @@ msgstr ""
 msgid "Deletes specific gateway for application"
 msgstr ""
 
-#: internal/parser/releases.go:109
+#: internal/parser/releases.go:108
 msgid "Deploy the latest release by process types"
 msgstr ""
 
-#: internal/parser/pts.go:64
+#: internal/parser/pts.go:65
 msgid "Describe a process type"
 msgstr ""
 
-#: internal/parser/apps.go:190
+#: internal/parser/apps.go:159
 msgid "Destroy an application"
 msgstr ""
 
-#: internal/parser/certs.go:160
+#: internal/parser/certs.go:159
 msgid "Detach an SSL certificate from a domain"
 msgstr ""
 
-#: internal/parser/certs.go:161
+#: internal/parser/certs.go:160
 msgid "Detach certificate from a domain"
 msgstr ""
 
@@ -259,11 +259,11 @@ msgstr ""
 msgid "Disable autorollback for an application"
 msgstr ""
 
-#: internal/parser/routing.go:57
+#: internal/parser/routing.go:58
 msgid "Disable routing for an application"
 msgstr ""
 
-#: internal/parser/autodeploy.go:59
+#: internal/parser/autodeploy.go:58
 msgid "Disables autodeploy for an app"
 msgstr ""
 
@@ -271,7 +271,7 @@ msgstr ""
 msgid "Display client version"
 msgstr ""
 
-#: cmd/root.go:34
+#: cmd/root.go:39
 msgid "Display help information"
 msgstr "Display help information"
 
@@ -291,7 +291,7 @@ msgstr ""
 msgid "Do not create a 'drycc' git remote"
 msgstr ""
 
-#: internal/parser/builds.go:59 internal/parser/builds.go:106
+#: internal/parser/builds.go:67 internal/parser/builds.go:114
 msgid "Drycc config path to the application"
 msgstr ""
 
@@ -309,11 +309,11 @@ msgstr ""
 msgid "Enable autorollback for an application"
 msgstr ""
 
-#: internal/parser/routing.go:45
+#: internal/parser/routing.go:46
 msgid "Enable routing for an application"
 msgstr ""
 
-#: internal/parser/autodeploy.go:47
+#: internal/parser/autodeploy.go:46
 msgid "Enables autodeploy for an app"
 msgstr ""
 
@@ -321,7 +321,7 @@ msgstr ""
 msgid "Enables or disables SSL certificate verification for API requests"
 msgstr ""
 
-#: internal/parser/volumes.go:88
+#: internal/parser/volumes.go:91
 msgid "Endpoint url for object storage service"
 msgstr ""
 
@@ -329,7 +329,7 @@ msgstr ""
 msgid "Execute a command in a container"
 msgstr ""
 
-#: internal/parser/volumes.go:118
+#: internal/parser/volumes.go:122
 msgid "Expand a volume capacity for the application"
 msgstr ""
 
@@ -355,7 +355,7 @@ msgstr ""
 msgid "Fetch detailed user information"
 msgstr ""
 
-#: internal/parser/certs.go:112
+#: internal/parser/certs.go:111
 msgid "Fetch more detailed information about a certificate"
 msgstr ""
 
@@ -363,15 +363,19 @@ msgstr ""
 msgid "Force TLS settings to HTTPS-only redirection"
 msgstr ""
 
-#: internal/parser/releases.go:117
+#: 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:147
+#: internal/parser/resources.go:148
 msgid "Get a resource's detail in the application"
 msgstr ""
 
-#: internal/parser/certs.go:111
+#: internal/parser/certs.go:110
 msgid "Get detailed informaton about the certificate"
 msgstr ""
 
@@ -383,11 +387,11 @@ msgstr ""
 msgid "Get route rules for an application"
 msgstr ""
 
-#: internal/parser/healthchecks.go:188
+#: internal/parser/healthchecks.go:187
 msgid "How often (in seconds) to perform the probe"
 msgstr ""
 
-#: internal/parser/keys.go:29
+#: internal/parser/keys.go:30
 msgid "List SSH keys for the logged in user"
 msgstr ""
 
@@ -395,23 +399,23 @@ msgstr ""
 msgid "List SSL certificates for an application"
 msgstr ""
 
-#: internal/parser/apps.go:65
+#: internal/parser/apps.go:64
 msgid "List accessible applications"
 msgstr ""
 
-#: internal/parser/limits.go:140
+#: internal/parser/limits.go:141
 msgid "List all available limit plans"
 msgstr ""
 
-#: internal/parser/limits.go:118
+#: internal/parser/limits.go:119
 msgid "List all available limit specs"
 msgstr ""
 
-#: internal/parser/resources.go:65
+#: internal/parser/resources.go:66
 msgid "List all available plans for a resource service"
 msgstr ""
 
-#: internal/parser/resources.go:44
+#: internal/parser/resources.go:45
 msgid "List all available resource services"
 msgstr ""
 
@@ -419,7 +423,7 @@ msgstr ""
 msgid "List all registered users"
 msgstr ""
 
-#: internal/parser/perms.go:39
+#: internal/parser/perms.go:40
 msgid "List all user permissions"
 msgstr ""
 
@@ -435,7 +439,7 @@ msgstr ""
 msgid "List application pods"
 msgstr ""
 
-#: internal/parser/pts.go:37
+#: internal/parser/pts.go:38
 msgid "List application process types"
 msgstr ""
 
@@ -447,7 +451,7 @@ msgstr ""
 msgid "List application services"
 msgstr ""
 
-#: internal/parser/labels.go:33
+#: internal/parser/labels.go:34
 msgid "List application's labels"
 msgstr ""
 
@@ -463,11 +467,15 @@ msgstr ""
 msgid "List healthchecks for an application"
 msgstr ""
 
-#: internal/parser/registry.go:43
+#: 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:40
+#: internal/parser/limits.go:41
 msgid "List resource limits for an app"
 msgstr ""
 
@@ -475,11 +483,11 @@ msgstr ""
 msgid "List resource timeouts for an app"
 msgstr ""
 
-#: internal/parser/resources.go:119
+#: internal/parser/resources.go:120
 msgid "List resources in the application"
 msgstr ""
 
-#: internal/parser/limits.go:117
+#: internal/parser/limits.go:118
 msgid "List specification information of the server"
 msgstr ""
 
@@ -487,11 +495,11 @@ msgstr ""
 msgid "List tags for an application"
 msgstr ""
 
-#: internal/parser/volumes.go:134
+#: internal/parser/volumes.go:138
 msgid "List volumes in the application"
 msgstr ""
 
-#: internal/parser/apps.go:66
+#: internal/parser/apps.go:65
 msgid "Lists applications visible to the current user"
 msgstr ""
 
@@ -499,7 +507,7 @@ msgstr ""
 msgid "Lists gateways for an application"
 msgstr ""
 
-#: internal/parser/pts.go:38
+#: internal/parser/pts.go:39
 msgid "Lists process types servicing an application"
 msgstr ""
 
@@ -507,7 +515,7 @@ msgstr ""
 msgid "Lists processes servicing an application"
 msgstr ""
 
-#: internal/parser/registry.go:44
+#: internal/parser/registry.go:45
 msgid "Lists registry information for an application"
 msgstr ""
 
@@ -539,11 +547,15 @@ msgstr ""
 msgid "Manage and assign domain names to your applications"
 msgstr ""
 
-#: internal/parser/healthchecks.go:23
+#: internal/parser/healthchecks.go:24
 msgid "Manage application healthchecks"
 msgstr ""
 
-#: internal/parser/apps.go:17
+#: 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 ""
 
@@ -551,7 +563,7 @@ msgstr ""
 msgid "Manage authentication"
 msgstr ""
 
-#: internal/parser/autodeploy.go:15
+#: internal/parser/autodeploy.go:14
 msgid "Manage autodeploy if or not for applications"
 msgstr ""
 
@@ -563,7 +575,7 @@ msgstr ""
 msgid "Manage autoscale for applications"
 msgstr ""
 
-#: internal/parser/builds.go:13
+#: internal/parser/builds.go:15
 msgid "Manage builds created using 'git push'"
 msgstr ""
 
@@ -575,15 +587,15 @@ msgstr ""
 msgid "Manage gateways for your applications"
 msgstr ""
 
-#: internal/parser/git.go:13
+#: internal/parser/git.go:14
 msgid "Manage git for applications"
 msgstr ""
 
-#: internal/parser/labels.go:14
+#: internal/parser/labels.go:15
 msgid "Manage labels of application"
 msgstr ""
 
-#: internal/parser/perms.go:16
+#: internal/parser/perms.go:17
 msgid "Manage permissions for applications"
 msgstr ""
 
@@ -591,15 +603,15 @@ msgstr ""
 msgid "Manage pods termination grace period"
 msgstr ""
 
-#: internal/parser/registry.go:19
+#: internal/parser/registry.go:20
 msgid "Manage private registry information for your application"
 msgstr ""
 
-#: internal/parser/pts.go:14
+#: internal/parser/pts.go:15
 msgid "Manage process types inside an app"
 msgstr ""
 
-#: internal/parser/ps.go:14
+#: internal/parser/ps.go:15
 msgid "Manage processes inside an app"
 msgstr ""
 
@@ -611,19 +623,19 @@ msgstr ""
 msgid "Manage releases of an application"
 msgstr ""
 
-#: internal/parser/limits.go:14
+#: internal/parser/limits.go:15
 msgid "Manage resource limits for your application"
 msgstr ""
 
-#: internal/parser/resources.go:16
+#: internal/parser/resources.go:17
 msgid "Manage resources for your applications"
 msgstr ""
 
-#: internal/parser/routing.go:13
+#: internal/parser/routing.go:14
 msgid "Manage routability of an application"
 msgstr ""
 
-#: internal/parser/routes.go:19
+#: internal/parser/routes.go:20
 msgid "Manage routes for your applications"
 msgstr ""
 
@@ -631,7 +643,7 @@ msgstr ""
 msgid "Manage services for your applications"
 msgstr ""
 
-#: internal/parser/keys.go:14
+#: internal/parser/keys.go:15
 msgid "Manage ssh keys used for 'git push' deployments"
 msgstr ""
 
@@ -651,39 +663,43 @@ msgstr ""
 msgid "Max replicas to scale up to"
 msgstr ""
 
-#: internal/parser/apps.go:22 internal/parser/apps.go:72
+#: internal/parser/apps.go:23 internal/parser/apps.go:71
 msgid "Maximum number of results to display"
 msgstr ""
 
-#: internal/parser/healthchecks.go:190
+#: 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:189
+#: internal/parser/healthchecks.go:188
 msgid ""
 "Minimum consecutive successes for the probe to be considered successful "
 "after having failed"
 msgstr ""
 
-#: internal/parser/volumes.go:264
+#: internal/parser/volumes.go:238
 msgid "Mount a volume to process of the application"
 msgstr ""
 
-#: internal/parser/apps.go:55 internal/parser/git.go:41
+#: internal/parser/apps.go:55 internal/parser/git.go:42
 msgid "Name of remote to create"
 msgstr ""
 
-#: internal/parser/keys.go:51
+#: internal/parser/keys.go:52
 msgid "Name of the SSH key"
 msgstr ""
 
-#: internal/parser/certs.go:130
+#: internal/parser/certs.go:129
 msgid "Name of the certificate to attach domain to"
 msgstr ""
 
-#: internal/parser/certs.go:155
+#: internal/parser/certs.go:154
 msgid "Name of the certificate to deatch from a domain"
 msgstr ""
 
@@ -696,15 +712,15 @@ msgid ""
 "Number or name of the port to access on the pods targeted by the service"
 msgstr ""
 
-#: internal/parser/apps.go:100
+#: internal/parser/apps.go:97
 msgid "Open the application in a browser"
 msgstr ""
 
-#: internal/parser/apps.go:101
+#: internal/parser/apps.go:98
 msgid "Opens a URL to the application in the default browser"
 msgstr ""
 
-#: internal/parser/git.go:42
+#: internal/parser/git.go:43
 msgid "Overwrite remote of the given name if it already exists"
 msgstr ""
 
@@ -712,11 +728,11 @@ msgstr ""
 msgid "Pass stdin to the container"
 msgstr ""
 
-#: internal/parser/volumes.go:86
+#: internal/parser/volumes.go:89
 msgid "Path that is exported by the nfs server"
 msgstr ""
 
-#: cmd/root.go:33
+#: cmd/root.go:38
 msgid "Path to configuration file"
 msgstr "Path to configuration file"
 
@@ -736,23 +752,23 @@ msgstr "Port is the network port, the listener received"
 msgid "Print a detailed description of the selected process"
 msgstr ""
 
-#: internal/parser/pts.go:65
+#: internal/parser/pts.go:66
 msgid "Print a detailed description of the selected process type"
 msgstr ""
 
-#: internal/parser/builds.go:74
+#: internal/parser/builds.go:82
 msgid "Print information about a specific build"
 msgstr ""
 
-#: internal/parser/releases.go:80
+#: internal/parser/releases.go:79
 msgid "Print information about a specific release"
 msgstr ""
 
-#: internal/parser/volumes.go:160
+#: internal/parser/volumes.go:164
 msgid "Print information about a volume"
 msgstr ""
 
-#: internal/parser/builds.go:99
+#: internal/parser/builds.go:107
 msgid "Print process info about a specific build"
 msgstr ""
 
@@ -778,19 +794,19 @@ msgstr ""
 msgid "Prints a list of autoscale options for the application"
 msgstr ""
 
-#: internal/parser/labels.go:34
+#: internal/parser/labels.go:35
 msgid "Prints a list of labels of the application"
 msgstr ""
 
-#: internal/parser/releases.go:81
+#: internal/parser/releases.go:80
 msgid "Prints info about a particular release"
 msgstr ""
 
-#: internal/parser/apps.go:82
+#: internal/parser/apps.go:80
 msgid "Prints info about the current application"
 msgstr ""
 
-#: internal/parser/autodeploy.go:35
+#: internal/parser/autodeploy.go:34
 msgid "Prints info about the current application's autodeploy if or not"
 msgstr ""
 
@@ -828,23 +844,23 @@ msgstr ""
 msgid "Pull environment variables to the path"
 msgstr ""
 
-#: internal/parser/config.go:182
+#: internal/parser/config.go:183
 msgid "Push environment variables from the path"
 msgstr ""
 
-#: internal/parser/limits.go:148
+#: internal/parser/limits.go:149
 msgid "Query plans that meet the specified memory capacity, unit GiB"
 msgstr ""
 
-#: internal/parser/limits.go:147
+#: internal/parser/limits.go:148
 msgid "Query plans that meet the specified number of cpu cores"
 msgstr ""
 
-#: internal/parser/limits.go:149
+#: internal/parser/limits.go:150
 msgid "Query plans that meet the specified spec id, see [specs] subcommand."
 msgstr ""
 
-#: internal/parser/routes.go:248
+#: internal/parser/routes.go:247
 msgid "Remove a route from an application"
 msgstr ""
 
@@ -852,19 +868,19 @@ msgstr ""
 msgid "Remove a token for controller authentication"
 msgstr ""
 
-#: internal/parser/perms.go:128
+#: internal/parser/perms.go:129
 msgid "Remove a user permissions"
 msgstr ""
 
-#: internal/parser/keys.go:80
+#: internal/parser/keys.go:81
 msgid "Remove an SSH key"
 msgstr ""
 
-#: internal/parser/certs.go:89
+#: internal/parser/certs.go:88
 msgid "Remove an SSL certificate from an app"
 msgstr ""
 
-#: internal/parser/labels.go:89
+#: internal/parser/labels.go:90
 msgid "Remove application's label"
 msgstr ""
 
@@ -876,39 +892,31 @@ msgstr ""
 msgid "Remove service from an application"
 msgstr ""
 
-#: internal/parser/keys.go:81
+#: internal/parser/keys.go:82
 msgid "Removes an SSH key for the logged in user"
 msgstr ""
 
-#: internal/parser/git.go:51
+#: internal/parser/git.go:52
 msgid "Removes git remote of application from repository"
 msgstr ""
 
-#: internal/parser/pts.go:90
+#: internal/parser/pts.go:91
 msgid "Restart an application or process types"
 msgstr ""
 
-#: internal/parser/pts.go:89
+#: internal/parser/pts.go:90
 msgid "Restart application process types"
 msgstr ""
 
-#: internal/parser/apps.go:173
+#: internal/parser/apps.go:142
 msgid "Retention time of records in seconds"
 msgstr ""
 
-#: internal/parser/apps.go:125
-msgid "Retrieve application log events"
-msgstr ""
-
-#: internal/parser/apps.go:126
-msgid "Retrieves the most recent log events"
-msgstr ""
-
-#: internal/parser/releases.go:134
+#: internal/parser/releases.go:133
 msgid "Return to a previous release"
 msgstr ""
 
-#: internal/parser/releases.go:135
+#: internal/parser/releases.go:134
 msgid "Rolls back to a previous application release"
 msgstr ""
 
@@ -916,40 +924,48 @@ msgstr ""
 msgid "Rules-file is the file name of rule to apply"
 msgstr ""
 
-#: internal/parser/apps.go:162
+#: internal/parser/apps.go:131
 msgid "Run a command in an ephemeral app container"
 msgstr ""
 
-#: internal/parser/apps.go:163
+#: internal/parser/apps.go:132
 msgid "Runs a command inside an ephemeral app container"
 msgstr ""
 
-#: internal/parser/builds.go:108
+#: internal/parser/builds.go:116
 msgid "Save process info to the local"
 msgstr ""
 
-#: internal/parser/pts.go:123
+#: internal/parser/pts.go:124
 msgid "Scale process types of replicas"
 msgstr ""
 
-#: internal/parser/pts.go:124
+#: internal/parser/pts.go:125
 msgid "Scales an application's processes by type"
 msgstr ""
 
-#: internal/parser/limits.go:125
+#: internal/parser/limits.go:126
 msgid ""
 "Search keywords separated by commas, matching must satisfy all of the "
 "specified."
 msgstr ""
 
-#: internal/parser/config.go:223
+#: 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:248
+#: 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 ""
@@ -958,15 +974,19 @@ msgstr ""
 msgid "Set environment variables for an app"
 msgstr ""
 
-#: internal/parser/healthchecks.go:92
+#: internal/parser/healthchecks.go:90
 msgid "Set healthchecks for an application"
 msgstr ""
 
-#: internal/parser/registry.go:75
+#: 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:69
+#: internal/parser/limits.go:70
 msgid "Set resource limits for an app"
 msgstr ""
 
@@ -990,7 +1010,7 @@ msgstr ""
 msgid "Sets environment variables for an application or config group"
 msgstr ""
 
-#: internal/parser/config.go:183
+#: internal/parser/config.go:184
 msgid ""
 "Sets environment variables for an application or config group.\n"
 "\n"
@@ -1004,7 +1024,7 @@ msgstr ""
 "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:93
+#: internal/parser/healthchecks.go:91
 msgid ""
 "Sets healthchecks for an application.\n"
 "\n"
@@ -1057,7 +1077,7 @@ msgid ""
 "port number to perform the socket connection on the Container.\n"
 msgstr ""
 
-#: internal/parser/labels.go:60
+#: internal/parser/labels.go:61
 msgid ""
 "Sets labels for an application.\n"
 "\n"
@@ -1067,14 +1087,100 @@ msgid ""
 "application. This information isn't send to scheduler."
 msgstr ""
 
-#: internal/parser/registry.go:76
+#: 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:70
+#: internal/parser/limits.go:71
 msgid ""
 "Sets resource limits for an application.\n"
 "\n"
@@ -1097,7 +1203,7 @@ msgstr ""
 msgid "Sets termination grace period for an application"
 msgstr ""
 
-#: cmd/root.go:69
+#: cmd/root.go:75
 msgid "Shortcut Commands:"
 msgstr "Shortcut Commands:"
 
@@ -1105,25 +1211,25 @@ msgstr "Shortcut Commands:"
 msgid "Show certificate information for an SSL application"
 msgstr ""
 
-#: internal/parser/resources.go:155
+#: internal/parser/resources.go:156
 msgid "Show extra details provided to resource"
 msgstr ""
 
-#: internal/parser/update.go:20
+#: internal/parser/update.go:21
 msgid "Simulate an update, only print the version info."
 msgstr ""
 
-#: internal/parser/apps.go:197
+#: internal/parser/apps.go:166
 msgid ""
 "Skips the prompt for the application name. \n"
 "<app> is the uniquely identifiable name for the application"
 msgstr ""
 
-#: internal/parser/apps.go:134 internal/parser/ps.go:76
+#: internal/parser/ps.go:76
 msgid "Specify if the logs should be streamed"
 msgstr ""
 
-#: internal/parser/resources.go:111 internal/parser/resources.go:195
+#: internal/parser/resources.go:112 internal/parser/resources.go:196
 msgid "Specify values in a YAML file. If set, params will be discard"
 msgstr ""
 
@@ -1135,11 +1241,15 @@ msgstr ""
 msgid "Target CPU utilization percentage"
 msgstr ""
 
-#: cmd/root.go:27
+#: cmd/root.go:29
 msgid "The Drycc command-line client issues API calls to a Drycc controller"
 msgstr "Drycc命令行客户端向Drycc控制器发出API调用"
 
-#: internal/parser/healthchecks.go:185
+#: 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 ""
 
@@ -1147,19 +1257,27 @@ msgstr ""
 msgid "The IP protocol for this port. Supports TCP, UDP, and SCTP"
 msgstr ""
 
-#: internal/parser/healthchecks.go:88
+#: 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/volumes.go:201
-msgid "Serve a WebDAV service for the volume"
+#: 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/volumes.go:261 internal/parser/apps.go:158
-#: internal/parser/shortcuts.go:44
+#: internal/parser/apps.go:127 internal/parser/shortcuts.go:53
+#: internal/parser/volumes.go:235
 msgid "The filesystem path"
 msgstr ""
 
@@ -1171,7 +1289,7 @@ msgstr ""
 msgid "The group for which the config needs to be pull"
 msgstr ""
 
-#: internal/parser/config.go:194
+#: internal/parser/config.go:195
 msgid "The group for which the config needs to be push"
 msgstr ""
 
@@ -1183,21 +1301,23 @@ msgstr ""
 msgid "The group for which the config needs to be unset"
 msgstr ""
 
-#: internal/parser/config.go:218
+#: internal/parser/config.go:220
 msgid "The group that requires attach configurations"
 msgstr ""
 
-#: internal/parser/config.go:245
+#: internal/parser/config.go:247
 msgid "The group that requires detach configurations"
 msgstr "The group that requires detach configurations"
 
-#: internal/parser/healthchecks.go:86
+#: internal/parser/healthchecks.go:84
 msgid ""
-"The healthcheck type, such as 'startupProbe' 'livenessProbe' or "
+"The healthcheck probe, such as 'startupProbe' 'livenessProbe' or "
 "'readinessProbe'"
 msgstr ""
+"The healthcheck probe, such as 'startupProbe' 'livenessProbe' or "
+"'readinessProbe'"
 
-#: internal/parser/volumes.go:85
+#: internal/parser/volumes.go:88
 msgid "The hostname or ip address of the nfs server"
 msgstr ""
 
@@ -1205,67 +1325,63 @@ msgstr ""
 msgid "The id of the token for controller authentication"
 msgstr ""
 
-#: internal/parser/healthchecks.go:186
+#: internal/parser/healthchecks.go:185
 msgid "The initial delay timeout for the probe"
 msgstr ""
 
-#: internal/parser/limits.go:66
-msgid "The limit plan id to apply to the process type"
-msgstr ""
+#: internal/parser/lifecycles.go:78
+msgid "The lifecycle handler, such as 'postStart' 'preStop'"
+msgstr "The lifecycle handler, such as 'postStart' 'preStop'"
 
-#: internal/parser/apps.go:135
-msgid "The max seconds of followz the log stream"
+#: internal/parser/limits.go:67
+msgid "The limit plan id to apply to the process type"
 msgstr ""
 
-#: internal/parser/resources.go:24 internal/parser/resources.go:126
-#: internal/parser/certs.go:23 internal/parser/certs.go:48
-#: internal/parser/volumes.go:23 internal/parser/volumes.go:141
-#: internal/parser/routes.go:27 internal/parser/routes.go:107
-#: internal/parser/domains.go:22 internal/parser/domains.go:43
 #: internal/parser/users.go:21 internal/parser/users.go:39
 #: internal/parser/releases.go:33 internal/parser/releases.go:62
-#: internal/parser/tokens.go:22 internal/parser/tokens.go:40
+#: internal/parser/limits.go:127 internal/parser/limits.go:151
+#: internal/parser/certs.go:23 internal/parser/certs.go:48
+#: internal/parser/routes.go:28 internal/parser/routes.go:107
 #: internal/parser/gateways.go:22 internal/parser/gateways.go:44
-#: internal/parser/limits.go:126 internal/parser/limits.go:150
-#: internal/parser/perms.go:24 internal/parser/perms.go:46
+#: internal/parser/volumes.go:23 internal/parser/volumes.go:145
+#: internal/parser/perms.go:25 internal/parser/perms.go:47
+#: internal/parser/tokens.go:22 internal/parser/tokens.go:40
+#: internal/parser/domains.go:22 internal/parser/domains.go:43
+#: internal/parser/resources.go:25 internal/parser/resources.go:127
 msgid "The maximum number of results to display"
 msgstr ""
 
-#: internal/parser/certs.go:107
+#: internal/parser/certs.go:106
 msgid "The name of the cert to get information from"
 msgstr ""
 
-#: internal/parser/certs.go:85
+#: internal/parser/certs.go:84
 msgid "The name of the cert to remove from the app"
 msgstr ""
 
-#: internal/parser/perms.go:67 internal/parser/perms.go:97
-#: internal/parser/perms.go:125
+#: internal/parser/perms.go:68 internal/parser/perms.go:98
+#: internal/parser/perms.go:126
 msgid "The name of the user"
 msgstr ""
 
-#: internal/parser/apps.go:133
-msgid "The number of lines to display"
-msgstr ""
-
 #: internal/parser/ps.go:75
 msgid "The number of lines to display, -1 showing all log lines"
 msgstr ""
 
-#: internal/parser/shortcuts.go:83 internal/parser/pts.go:120
+#: internal/parser/shortcuts.go:95 internal/parser/pts.go:121
 msgid "The number of processes"
 msgstr ""
 
-#: internal/parser/healthchecks.go:187
+#: internal/parser/healthchecks.go:186
 msgid "The number of seconds after which the probe times out"
 msgstr ""
 
-#: internal/parser/registry.go:72
+#: internal/parser/registry.go:73
 msgid "The password of the registry"
 msgstr ""
 
-#: internal/parser/shortcuts.go:58 internal/parser/ps.go:99
-#: internal/parser/ps.go:130 internal/parser/ps.go:152
+#: 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 ""
 
@@ -1281,15 +1397,15 @@ msgstr ""
 msgid "The private key of the SSL certificate"
 msgstr ""
 
-#: internal/parser/volumes.go:260 internal/parser/volumes.go:285
-#: internal/parser/shortcuts.go:82 internal/parser/pts.go:61
-#: internal/parser/pts.go:119 internal/parser/pts.go:153
+#: internal/parser/shortcuts.go:94 internal/parser/volumes.go:234
+#: internal/parser/volumes.go:259 internal/parser/pts.go:62
+#: internal/parser/pts.go:120 internal/parser/pts.go:154
 #: internal/parser/tags.go:26 internal/parser/tags.go:60
 #: internal/parser/tags.go:100 internal/parser/tags.go:131
 msgid "The process name as defined in your Procfile"
 msgstr ""
 
-#: internal/parser/releases.go:130
+#: internal/parser/releases.go:129
 msgid "The process name as defined in your Procfile, such as 'web'"
 msgstr ""
 
@@ -1297,7 +1413,7 @@ msgstr ""
 msgid "The process type as defined in your Procfile"
 msgstr ""
 
-#: internal/parser/limits.go:65 internal/parser/limits.go:95
+#: 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 ""
 
@@ -1313,8 +1429,8 @@ msgstr ""
 msgid "The ptype for domain"
 msgstr ""
 
-#: internal/parser/registry.go:26 internal/parser/registry.go:50
-#: internal/parser/registry.go:85 internal/parser/registry.go:107
+#: 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 ""
 
@@ -1326,7 +1442,7 @@ msgstr ""
 msgid "The ptype for which the config needs to be pull"
 msgstr ""
 
-#: internal/parser/config.go:193
+#: internal/parser/config.go:194
 msgid "The ptype for which the config needs to be push"
 msgstr ""
 
@@ -1338,23 +1454,35 @@ msgstr ""
 msgid "The ptype for which the config needs to be unset"
 msgstr ""
 
-#: internal/parser/healthchecks.go:180
+#: 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:30
+#: 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:225
+#: internal/parser/healthchecks.go:224
 msgid "The ptype for which the health check needs to be removed"
 msgstr ""
 
-#: internal/parser/config.go:217
+#: 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:244
+#: internal/parser/config.go:246
 msgid "The ptype that requires detach configurations"
 msgstr ""
 
@@ -1362,28 +1490,32 @@ msgstr ""
 msgid "The public key of the SSL certificate"
 msgstr ""
 
-#: internal/parser/healthchecks.go:184
+#: 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:77 internal/parser/releases.go:131
+#: internal/parser/releases.go:76 internal/parser/releases.go:130
 msgid "The release of the application, such as '1'"
 msgstr ""
 
-#: internal/parser/resources.go:213 internal/parser/resources.go:238
-#: internal/parser/resources.go:260
+#: 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:93 internal/parser/resources.go:176
+#: internal/parser/resources.go:94 internal/parser/resources.go:177
 msgid "The resource instance parameters key"
 msgstr ""
 
-#: internal/parser/resources.go:94 internal/parser/resources.go:177
+#: internal/parser/resources.go:95 internal/parser/resources.go:178
 msgid "The resource instance parameters value"
 msgstr ""
 
-#: internal/parser/resources.go:91 internal/parser/resources.go:174
+#: internal/parser/resources.go:92 internal/parser/resources.go:175
 msgid "The resource's service"
 msgstr ""
 
@@ -1395,22 +1527,26 @@ msgstr ""
 msgid "The route's backend, pattern: <service>:<port>,<weight>"
 msgstr ""
 
-#: internal/parser/resources.go:62
+#: internal/parser/resources.go:63
 msgid "The service name for plans"
 msgstr ""
 
-#: internal/parser/resources.go:92 internal/parser/resources.go:175
+#: internal/parser/resources.go:93 internal/parser/resources.go:176
 msgid "The service's plan"
 msgstr ""
 
-#: internal/parser/apps.go:159 internal/parser/shortcuts.go:45
+#: internal/parser/apps.go:128 internal/parser/shortcuts.go:54
 msgid "The shell command to run inside the container"
 msgstr ""
 
-#: internal/parser/builds.go:57
+#: 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 ""
@@ -1429,42 +1565,42 @@ 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:245
+#: internal/parser/routes.go:212 internal/parser/routes.go:244
 msgid "The unique name of the route"
 msgstr ""
 
-#: internal/parser/resources.go:23 internal/parser/certs.go:22
-#: internal/parser/autodeploy.go:21 internal/parser/volumes.go:22
-#: internal/parser/routes.go:26 internal/parser/services.go:23
-#: internal/parser/autoscale.go:21 internal/parser/apps.go:88
-#: internal/parser/apps.go:107 internal/parser/apps.go:132
-#: internal/parser/apps.go:170 internal/parser/apps.go:196
-#: internal/parser/apps.go:217 internal/parser/domains.go:21
-#: internal/parser/releases.go:32 internal/parser/registry.go:25
-#: internal/parser/labels.go:20 internal/parser/routing.go:19
-#: internal/parser/gateways.go:21 internal/parser/builds.go:19
-#: internal/parser/builds.go:56 internal/parser/autorollback.go:19
-#: internal/parser/limits.go:20 internal/parser/git.go:17
-#: internal/parser/timeouts.go:21 internal/parser/config.go:29
-#: internal/parser/pts.go:20 internal/parser/pts.go:133
-#: internal/parser/ps.go:20 internal/parser/ps.go:112
-#: internal/parser/tags.go:25 internal/parser/perms.go:23
+#: internal/parser/builds.go:21 internal/parser/builds.go:64
+#: internal/parser/apps.go:86 internal/parser/apps.go:104
+#: internal/parser/apps.go:139 internal/parser/apps.go:165
+#: internal/parser/apps.go:186 internal/parser/autorollback.go:19
+#: internal/parser/timeouts.go:21 internal/parser/releases.go:32
+#: internal/parser/limits.go:21 internal/parser/autoscale.go:21
+#: internal/parser/ps.go:21 internal/parser/ps.go:112
+#: internal/parser/services.go:23 internal/parser/certs.go:22
+#: internal/parser/config.go:29 internal/parser/routes.go:27
+#: internal/parser/routing.go:20 internal/parser/labels.go:21
+#: internal/parser/gateways.go:21 internal/parser/volumes.go:22
+#: internal/parser/autodeploy.go:20 internal/parser/git.go:18
+#: internal/parser/perms.go:24 internal/parser/pts.go:21
+#: internal/parser/pts.go:134 internal/parser/tags.go:25
+#: internal/parser/registry.go:26 internal/parser/domains.go:21
+#: internal/parser/resources.go:24
 msgid "The uniquely identifiable name for the application"
 msgstr ""
 
-#: internal/parser/healthchecks.go:29
+#: internal/parser/healthchecks.go:30 internal/parser/lifecycles.go:30
 msgid "The uniquely identifiable name of the application"
 msgstr ""
 
-#: internal/parser/perms.go:68
+#: internal/parser/perms.go:69
 msgid "The user permissions (view,change,delete)"
 msgstr ""
 
-#: internal/parser/perms.go:98
+#: internal/parser/perms.go:99
 msgid "The user's permissions (view,change,delete)"
 msgstr ""
 
-#: internal/parser/registry.go:71
+#: internal/parser/registry.go:72
 msgid "The username of the registry"
 msgstr ""
 
@@ -1480,8 +1616,8 @@ msgstr ""
 msgid "The value to apply to the process type in seconds"
 msgstr ""
 
-#: internal/parser/builds.go:20 internal/parser/builds.go:79
-#: internal/parser/builds.go:104
+#: 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 ""
 
@@ -1493,15 +1629,19 @@ msgstr ""
 msgid "The version for which the config needs to be pull"
 msgstr ""
 
-#: internal/parser/healthchecks.go:31 internal/parser/healthchecks.go:56
+#: 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/limits.go:21 internal/parser/limits.go:46
+#: 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:27 internal/parser/registry.go:51
+#: internal/parser/registry.go:28 internal/parser/registry.go:52
 msgid "The version for which the registry needs to be listed"
 msgstr ""
 
@@ -1513,49 +1653,50 @@ msgstr ""
 msgid "The version for which the timeout needs to be listed"
 msgstr ""
 
-#: internal/parser/volumes.go:57 internal/parser/volumes.go:114
-#: internal/parser/volumes.go:259 internal/parser/volumes.go:284
-#: internal/parser/apps.go:157 internal/parser/shortcuts.go:43
+#: internal/parser/apps.go:126 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
 msgid "The volume name"
 msgstr ""
 
-#: internal/parser/volumes.go:157
+#: internal/parser/volumes.go:161
 msgid "The volume name to be info"
 msgstr ""
 
-#: internal/parser/volumes.go:183
+#: internal/parser/volumes.go:187
 msgid "The volume name to be removed"
 msgstr ""
 
-#: internal/parser/volumes.go:58 internal/parser/volumes.go:115
+#: internal/parser/volumes.go:59 internal/parser/volumes.go:119
 msgid "The volume size, such as '500G'"
 msgstr ""
 
-#: internal/parser/volumes.go:84
+#: internal/parser/volumes.go:87
 msgid "The volume type, such as csi, nfs, oss"
 msgstr ""
 
-#: internal/parser/resources.go:90 internal/parser/resources.go:144
-#: internal/parser/resources.go:173
+#: internal/parser/resources.go:91 internal/parser/resources.go:145
+#: internal/parser/resources.go:174
 msgid "This resource instance alias"
 msgstr ""
 
-#: internal/parser/releases.go:118 internal/parser/tokens.go:103
-#: internal/parser/builds.go:60 internal/parser/builds.go:107
-#: internal/parser/pts.go:98
+#: internal/parser/builds.go:68 internal/parser/builds.go:115
+#: internal/parser/releases.go:117 internal/parser/tokens.go:103
+#: internal/parser/pts.go:99
 msgid "To proceed, type \"yes\""
 msgstr ""
 
 #: internal/parser/config.go:94 internal/parser/config.go:123
-#: internal/parser/config.go:196
+#: internal/parser/config.go:198
 msgid "To proceed, type 'yes'"
 msgstr ""
 
-#: internal/parser/apps.go:209
+#: internal/parser/apps.go:178
 msgid "Transfer app ownership to another user"
 msgstr ""
 
-#: internal/parser/apps.go:210
+#: internal/parser/apps.go:179
 msgid "Transfer application ownership to another user."
 msgstr ""
 
@@ -1571,7 +1712,7 @@ msgstr ""
 msgid "Unbinds a domain for an application"
 msgstr ""
 
-#: internal/parser/volumes.go:288
+#: internal/parser/volumes.go:262
 msgid "Unmount a volume from process of the application"
 msgstr ""
 
@@ -1583,15 +1724,19 @@ msgstr ""
 msgid "Unset environment variables for an app"
 msgstr ""
 
-#: internal/parser/healthchecks.go:213
+#: internal/parser/healthchecks.go:212
 msgid "Unset healthchecks for an application"
 msgstr ""
 
-#: internal/parser/registry.go:100
+#: 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:98
+#: internal/parser/limits.go:99
 msgid "Unset resource limits for an app"
 msgstr ""
 
@@ -1607,11 +1752,11 @@ msgstr ""
 msgid "Unsets an environment variable for an application or config group"
 msgstr ""
 
-#: internal/parser/labels.go:90
+#: internal/parser/labels.go:91
 msgid "Unsets labels for an application"
 msgstr ""
 
-#: internal/parser/registry.go:101
+#: internal/parser/registry.go:102
 msgid "Unsets registry information for an application"
 msgstr ""
 
@@ -1621,15 +1766,15 @@ msgid ""
 "controller"
 msgstr ""
 
-#: internal/parser/resources.go:180
+#: internal/parser/resources.go:181
 msgid "Update a resource from the application"
 msgstr ""
 
-#: internal/parser/perms.go:101
+#: internal/parser/perms.go:102
 msgid "Update a user permission"
 msgstr ""
 
-#: internal/parser/update.go:14 internal/parser/update.go:15
+#: internal/parser/update.go:15 internal/parser/update.go:16
 msgid "Update workflow cli to latest release"
 msgstr ""
 
@@ -1637,7 +1782,7 @@ msgstr ""
 msgid "View TLS settings information"
 msgstr ""
 
-#: internal/parser/routing.go:33
+#: internal/parser/routing.go:34
 msgid "View application's routability information"
 msgstr ""
 
@@ -1645,19 +1790,19 @@ msgstr ""
 msgid "View autorollback info for an application"
 msgstr ""
 
-#: internal/parser/apps.go:81
+#: internal/parser/apps.go:79
 msgid "View info about an application"
 msgstr ""
 
-#: internal/parser/apps.go:171
+#: internal/parser/apps.go:140
 msgid "Volume mounts in format 'volume:path'"
 msgstr ""
 
-#: internal/parser/builds.go:42
+#: internal/parser/builds.go:49
 msgid "default container image"
 msgstr ""
 
-#: internal/parser/builds.go:44
+#: internal/parser/builds.go:52
 msgid "imports an image and deploys as a new release"
 msgstr ""
 
@@ -1680,58 +1825,49 @@ msgid ""
 "             only single extra service per Porcfile type could be created."
 msgstr ""
 
-#: internal/parser/volumes.go:95
-msgid "Secret access key for authentication"
-msgstr ""
-
-#: internal/parser/volumes.go:97
-msgid "Force a path-style endpoint to be used"
-msgstr ""
-
-#: internal/parser/resources.go:224
+#: 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:87
+#: internal/parser/healthchecks.go:85
 msgid "the healthcheck probe type, such as 'httpGet', 'exec' or 'tcpSocket'"
 msgstr ""
 
-#: internal/parser/healthchecks.go:210
+#: internal/parser/healthchecks.go:209
 msgid ""
-"the healthcheck type, such as 'startupProbe' 'livenessProbe' or "
+"the healthcheck probe, such as 'startupProbe' 'livenessProbe' or "
 "'readinessProbe'"
 msgstr ""
+"the healthcheck probe, such as 'startupProbe' 'livenessProbe' or "
+"'readinessProbe'"
 
-#: internal/parser/labels.go:84
+#: internal/parser/labels.go:85
 msgid "the label key to unset, for example: \"git_repo\" or \"team\""
 msgstr ""
 
-#: internal/parser/labels.go:55
+#: internal/parser/labels.go:56
 msgid "the label key, for example: \"git_repo\" or \"team\""
 msgstr ""
 
-#: internal/parser/labels.go:56
+#: internal/parser/labels.go:57
 msgid ""
 "the label value, for example: \"https://github.com/drycc/workflow\" or "
 "\"frontend\""
 msgstr ""
 
-#: internal/parser/resources.go:263
-msgid "unbind a resources for an application"
+#: 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'"
 
-#~ msgid "Path to configuration file."
-#~ msgstr "Path to configuration file."
-
-#~ msgid "Shortcut commands, use 'drycc shortcuts' to see all:"
-#~ msgstr "Shortcut commands, use 'drycc shortcuts' to see all:"
+#: 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'"
 
-#~ msgid ""
-#~ "procfile type which should handle the request, e.g. webhooks (should be "
-#~ "bind to the port PORT)"
-#~ msgstr ""
-#~ "procfile type which should handle the request, e.g. webhooks (should be "
-#~ "bind to the port PORT)"
+#: 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
index 0929c186a..9db9cc791 100644
--- a/pkg/i18n/translations/drycc/template.pot
+++ b/pkg/i18n/translations/drycc/template.pot
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: \n"
 "Report-Msgid-Bugs-To: EMAIL\n"
-"POT-Creation-Date: 2025-04-14 15:39+0800\n"
+"POT-Creation-Date: 2025-12-08 16:06+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"
@@ -17,31 +17,31 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: internal/parser/builds.go:58 internal/parser/builds.go:105
+#: 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:52
+#: 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:195
+#: internal/parser/config.go:158 internal/parser/config.go:196
 msgid "A path leading to an environment file"
 msgstr ""
 
-#: internal/parser/tls.go:121
+#: internal/parser/tls.go:123
 msgid "ACME account email"
 msgstr ""
 
-#: internal/parser/tls.go:122
+#: internal/parser/tls.go:124
 msgid "ACME server URL"
 msgstr ""
 
-#: internal/parser/volumes.go:90
+#: internal/parser/volumes.go:93
 msgid "Access key id for authentication"
 msgstr ""
 
-#: internal/parser/tls.go:55 internal/parser/tls.go:83
+#: internal/parser/tls.go:55 internal/parser/tls.go:84
 msgid "Action to perform, 'enable' or 'disable'"
 msgstr ""
 
@@ -49,11 +49,11 @@ msgstr ""
 msgid "Add a token for controller authentication"
 msgstr ""
 
-#: internal/parser/perms.go:71
+#: internal/parser/perms.go:72
 msgid "Add a user permissions"
 msgstr ""
 
-#: internal/parser/keys.go:56
+#: internal/parser/keys.go:57
 msgid "Add an SSH key"
 msgstr ""
 
@@ -61,15 +61,15 @@ msgstr ""
 msgid "Add an SSL certificate to an app"
 msgstr ""
 
-#: internal/parser/labels.go:59
+#: internal/parser/labels.go:60
 msgid "Add new application's label"
 msgstr ""
 
-#: internal/parser/keys.go:57
+#: internal/parser/keys.go:58
 msgid "Adds SSH keys for the logged in user"
 msgstr ""
 
-#: internal/parser/git.go:35
+#: internal/parser/git.go:36
 msgid "Adds git remote of application to repository"
 msgstr ""
 
@@ -85,11 +85,11 @@ msgstr ""
 msgid "Application name"
 msgstr ""
 
-#: internal/parser/certs.go:136
+#: internal/parser/certs.go:135
 msgid "Attach a certificate to a domain"
 msgstr ""
 
-#: internal/parser/certs.go:135
+#: internal/parser/certs.go:134
 msgid "Attach an SSL certificate to a domain"
 msgstr ""
 
@@ -101,7 +101,7 @@ msgstr ""
 msgid "Authenticate against a controller"
 msgstr ""
 
-#: internal/parser/tls.go:88
+#: internal/parser/tls.go:89
 msgid "Automatic certificate management"
 msgstr ""
 
@@ -109,23 +109,23 @@ msgstr ""
 msgid "Bind a domain to an application"
 msgstr ""
 
-#: internal/parser/resources.go:241
+#: internal/parser/resources.go:242
 msgid "Bind a resource for an application"
 msgstr ""
 
-#: internal/parser/volumes.go:89
+#: internal/parser/volumes.go:92
 msgid "Bucket name in object storage"
 msgstr ""
 
-#: internal/parser/tls.go:123
+#: internal/parser/tls.go:125
 msgid "CA key ID"
 msgstr ""
 
-#: internal/parser/tls.go:124
+#: internal/parser/tls.go:126
 msgid "CA key secret"
 msgstr ""
 
-#: internal/parser/pts.go:156
+#: internal/parser/pts.go:157
 msgid "Clean process types of not used"
 msgstr ""
 
@@ -133,20 +133,20 @@ msgstr ""
 msgid "Clear the current user session"
 msgstr ""
 
-#: internal/parser/apps.go:172
+#: internal/parser/apps.go:141
 msgid "Command execution timeout in seconds"
 msgstr ""
 
-#: internal/parser/certs.go:131
+#: 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:156
+#: internal/parser/certs.go:155
 msgid "Common name of the domain to detach from"
 msgstr ""
 
-#: internal/parser/tls.go:115
+#: internal/parser/tls.go:117
 msgid "Configure automatic certificate management issuer details"
 msgstr ""
 
@@ -154,7 +154,7 @@ msgstr ""
 msgid "Create a new application"
 msgstr ""
 
-#: internal/parser/resources.go:97
+#: internal/parser/resources.go:98
 msgid "Create a resource for the application"
 msgstr ""
 
@@ -162,7 +162,7 @@ msgstr ""
 msgid "Create a route for an application"
 msgstr ""
 
-#: internal/parser/volumes.go:61
+#: internal/parser/volumes.go:62
 msgid "Create a volume for the application"
 msgstr ""
 
@@ -180,7 +180,7 @@ msgid ""
 "automatically"
 msgstr ""
 
-#: internal/parser/builds.go:45
+#: internal/parser/builds.go:53
 msgid ""
 "Creates a new build of an application. Imports an <image> and deploys it to "
 "Drycc\n"
@@ -205,11 +205,11 @@ msgstr ""
 msgid "Creates routes for an application, provides a way to route requests"
 msgstr ""
 
-#: internal/parser/resources.go:216
+#: internal/parser/resources.go:217
 msgid "Delete a resource from the application"
 msgstr ""
 
-#: internal/parser/volumes.go:186
+#: internal/parser/volumes.go:190
 msgid "Delete a volume from the application"
 msgstr ""
 
@@ -221,23 +221,23 @@ msgstr ""
 msgid "Deletes specific gateway for application"
 msgstr ""
 
-#: internal/parser/releases.go:109
+#: internal/parser/releases.go:108
 msgid "Deploy the latest release by process types"
 msgstr ""
 
-#: internal/parser/pts.go:64
+#: internal/parser/pts.go:65
 msgid "Describe a process type"
 msgstr ""
 
-#: internal/parser/apps.go:190
+#: internal/parser/apps.go:159
 msgid "Destroy an application"
 msgstr ""
 
-#: internal/parser/certs.go:160
+#: internal/parser/certs.go:159
 msgid "Detach an SSL certificate from a domain"
 msgstr ""
 
-#: internal/parser/certs.go:161
+#: internal/parser/certs.go:160
 msgid "Detach certificate from a domain"
 msgstr ""
 
@@ -259,11 +259,11 @@ msgstr ""
 msgid "Disable autorollback for an application"
 msgstr ""
 
-#: internal/parser/routing.go:57
+#: internal/parser/routing.go:58
 msgid "Disable routing for an application"
 msgstr ""
 
-#: internal/parser/autodeploy.go:59
+#: internal/parser/autodeploy.go:58
 msgid "Disables autodeploy for an app"
 msgstr ""
 
@@ -271,7 +271,7 @@ msgstr ""
 msgid "Display client version"
 msgstr ""
 
-#: cmd/root.go:34
+#: cmd/root.go:39
 msgid "Display help information"
 msgstr ""
 
@@ -291,7 +291,7 @@ msgstr ""
 msgid "Do not create a 'drycc' git remote"
 msgstr ""
 
-#: internal/parser/builds.go:59 internal/parser/builds.go:106
+#: internal/parser/builds.go:67 internal/parser/builds.go:114
 msgid "Drycc config path to the application"
 msgstr ""
 
@@ -309,11 +309,11 @@ msgstr ""
 msgid "Enable autorollback for an application"
 msgstr ""
 
-#: internal/parser/routing.go:45
+#: internal/parser/routing.go:46
 msgid "Enable routing for an application"
 msgstr ""
 
-#: internal/parser/autodeploy.go:47
+#: internal/parser/autodeploy.go:46
 msgid "Enables autodeploy for an app"
 msgstr ""
 
@@ -321,7 +321,7 @@ msgstr ""
 msgid "Enables or disables SSL certificate verification for API requests"
 msgstr ""
 
-#: internal/parser/volumes.go:88
+#: internal/parser/volumes.go:91
 msgid "Endpoint url for object storage service"
 msgstr ""
 
@@ -329,7 +329,7 @@ msgstr ""
 msgid "Execute a command in a container"
 msgstr ""
 
-#: internal/parser/volumes.go:118
+#: internal/parser/volumes.go:122
 msgid "Expand a volume capacity for the application"
 msgstr ""
 
@@ -348,7 +348,7 @@ msgstr ""
 msgid "Fetch detailed user information"
 msgstr ""
 
-#: internal/parser/certs.go:112
+#: internal/parser/certs.go:111
 msgid "Fetch more detailed information about a certificate"
 msgstr ""
 
@@ -356,15 +356,19 @@ msgstr ""
 msgid "Force TLS settings to HTTPS-only redirection"
 msgstr ""
 
-#: internal/parser/releases.go:117
+#: 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:147
+#: internal/parser/resources.go:148
 msgid "Get a resource's detail in the application"
 msgstr ""
 
-#: internal/parser/certs.go:111
+#: internal/parser/certs.go:110
 msgid "Get detailed informaton about the certificate"
 msgstr ""
 
@@ -376,11 +380,11 @@ msgstr ""
 msgid "Get route rules for an application"
 msgstr ""
 
-#: internal/parser/healthchecks.go:188
+#: internal/parser/healthchecks.go:187
 msgid "How often (in seconds) to perform the probe"
 msgstr ""
 
-#: internal/parser/keys.go:29
+#: internal/parser/keys.go:30
 msgid "List SSH keys for the logged in user"
 msgstr ""
 
@@ -388,23 +392,23 @@ msgstr ""
 msgid "List SSL certificates for an application"
 msgstr ""
 
-#: internal/parser/apps.go:65
+#: internal/parser/apps.go:64
 msgid "List accessible applications"
 msgstr ""
 
-#: internal/parser/limits.go:140
+#: internal/parser/limits.go:141
 msgid "List all available limit plans"
 msgstr ""
 
-#: internal/parser/limits.go:118
+#: internal/parser/limits.go:119
 msgid "List all available limit specs"
 msgstr ""
 
-#: internal/parser/resources.go:65
+#: internal/parser/resources.go:66
 msgid "List all available plans for a resource service"
 msgstr ""
 
-#: internal/parser/resources.go:44
+#: internal/parser/resources.go:45
 msgid "List all available resource services"
 msgstr ""
 
@@ -412,7 +416,7 @@ msgstr ""
 msgid "List all registered users"
 msgstr ""
 
-#: internal/parser/perms.go:39
+#: internal/parser/perms.go:40
 msgid "List all user permissions"
 msgstr ""
 
@@ -428,7 +432,7 @@ msgstr ""
 msgid "List application pods"
 msgstr ""
 
-#: internal/parser/pts.go:37
+#: internal/parser/pts.go:38
 msgid "List application process types"
 msgstr ""
 
@@ -440,7 +444,7 @@ msgstr ""
 msgid "List application services"
 msgstr ""
 
-#: internal/parser/labels.go:33
+#: internal/parser/labels.go:34
 msgid "List application's labels"
 msgstr ""
 
@@ -456,11 +460,15 @@ msgstr ""
 msgid "List healthchecks for an application"
 msgstr ""
 
-#: internal/parser/registry.go:43
+#: 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:40
+#: internal/parser/limits.go:41
 msgid "List resource limits for an app"
 msgstr ""
 
@@ -468,11 +476,11 @@ msgstr ""
 msgid "List resource timeouts for an app"
 msgstr ""
 
-#: internal/parser/resources.go:119
+#: internal/parser/resources.go:120
 msgid "List resources in the application"
 msgstr ""
 
-#: internal/parser/limits.go:117
+#: internal/parser/limits.go:118
 msgid "List specification information of the server"
 msgstr ""
 
@@ -480,11 +488,11 @@ msgstr ""
 msgid "List tags for an application"
 msgstr ""
 
-#: internal/parser/volumes.go:134
+#: internal/parser/volumes.go:138
 msgid "List volumes in the application"
 msgstr ""
 
-#: internal/parser/apps.go:66
+#: internal/parser/apps.go:65
 msgid "Lists applications visible to the current user"
 msgstr ""
 
@@ -492,7 +500,7 @@ msgstr ""
 msgid "Lists gateways for an application"
 msgstr ""
 
-#: internal/parser/pts.go:38
+#: internal/parser/pts.go:39
 msgid "Lists process types servicing an application"
 msgstr ""
 
@@ -500,7 +508,7 @@ msgstr ""
 msgid "Lists processes servicing an application"
 msgstr ""
 
-#: internal/parser/registry.go:44
+#: internal/parser/registry.go:45
 msgid "Lists registry information for an application"
 msgstr ""
 
@@ -532,11 +540,15 @@ msgstr ""
 msgid "Manage and assign domain names to your applications"
 msgstr ""
 
-#: internal/parser/healthchecks.go:23
+#: internal/parser/healthchecks.go:24
 msgid "Manage application healthchecks"
 msgstr ""
 
-#: internal/parser/apps.go:17
+#: internal/parser/lifecycles.go:24
+msgid "Manage application lifecycles"
+msgstr ""
+
+#: internal/parser/apps.go:18
 msgid "Manage applications used to provide services"
 msgstr ""
 
@@ -544,7 +556,7 @@ msgstr ""
 msgid "Manage authentication"
 msgstr ""
 
-#: internal/parser/autodeploy.go:15
+#: internal/parser/autodeploy.go:14
 msgid "Manage autodeploy if or not for applications"
 msgstr ""
 
@@ -556,7 +568,7 @@ msgstr ""
 msgid "Manage autoscale for applications"
 msgstr ""
 
-#: internal/parser/builds.go:13
+#: internal/parser/builds.go:15
 msgid "Manage builds created using 'git push'"
 msgstr ""
 
@@ -568,15 +580,15 @@ msgstr ""
 msgid "Manage gateways for your applications"
 msgstr ""
 
-#: internal/parser/git.go:13
+#: internal/parser/git.go:14
 msgid "Manage git for applications"
 msgstr ""
 
-#: internal/parser/labels.go:14
+#: internal/parser/labels.go:15
 msgid "Manage labels of application"
 msgstr ""
 
-#: internal/parser/perms.go:16
+#: internal/parser/perms.go:17
 msgid "Manage permissions for applications"
 msgstr ""
 
@@ -584,15 +596,15 @@ msgstr ""
 msgid "Manage pods termination grace period"
 msgstr ""
 
-#: internal/parser/registry.go:19
+#: internal/parser/registry.go:20
 msgid "Manage private registry information for your application"
 msgstr ""
 
-#: internal/parser/pts.go:14
+#: internal/parser/pts.go:15
 msgid "Manage process types inside an app"
 msgstr ""
 
-#: internal/parser/ps.go:14
+#: internal/parser/ps.go:15
 msgid "Manage processes inside an app"
 msgstr ""
 
@@ -604,19 +616,19 @@ msgstr ""
 msgid "Manage releases of an application"
 msgstr ""
 
-#: internal/parser/limits.go:14
+#: internal/parser/limits.go:15
 msgid "Manage resource limits for your application"
 msgstr ""
 
-#: internal/parser/resources.go:16
+#: internal/parser/resources.go:17
 msgid "Manage resources for your applications"
 msgstr ""
 
-#: internal/parser/routing.go:13
+#: internal/parser/routing.go:14
 msgid "Manage routability of an application"
 msgstr ""
 
-#: internal/parser/routes.go:19
+#: internal/parser/routes.go:20
 msgid "Manage routes for your applications"
 msgstr ""
 
@@ -624,7 +636,7 @@ msgstr ""
 msgid "Manage services for your applications"
 msgstr ""
 
-#: internal/parser/keys.go:14
+#: internal/parser/keys.go:15
 msgid "Manage ssh keys used for 'git push' deployments"
 msgstr ""
 
@@ -644,39 +656,43 @@ msgstr ""
 msgid "Max replicas to scale up to"
 msgstr ""
 
-#: internal/parser/apps.go:22 internal/parser/apps.go:72
+#: internal/parser/apps.go:23 internal/parser/apps.go:71
 msgid "Maximum number of results to display"
 msgstr ""
 
-#: internal/parser/healthchecks.go:190
+#: 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:189
+#: internal/parser/healthchecks.go:188
 msgid ""
 "Minimum consecutive successes for the probe to be considered successful "
 "after having failed"
 msgstr ""
 
-#: internal/parser/volumes.go:264
+#: internal/parser/volumes.go:238
 msgid "Mount a volume to process of the application"
 msgstr ""
 
-#: internal/parser/apps.go:55 internal/parser/git.go:41
+#: internal/parser/apps.go:55 internal/parser/git.go:42
 msgid "Name of remote to create"
 msgstr ""
 
-#: internal/parser/keys.go:51
+#: internal/parser/keys.go:52
 msgid "Name of the SSH key"
 msgstr ""
 
-#: internal/parser/certs.go:130
+#: internal/parser/certs.go:129
 msgid "Name of the certificate to attach domain to"
 msgstr ""
 
-#: internal/parser/certs.go:155
+#: internal/parser/certs.go:154
 msgid "Name of the certificate to deatch from a domain"
 msgstr ""
 
@@ -689,15 +705,15 @@ msgid ""
 "Number or name of the port to access on the pods targeted by the service"
 msgstr ""
 
-#: internal/parser/apps.go:100
+#: internal/parser/apps.go:97
 msgid "Open the application in a browser"
 msgstr ""
 
-#: internal/parser/apps.go:101
+#: internal/parser/apps.go:98
 msgid "Opens a URL to the application in the default browser"
 msgstr ""
 
-#: internal/parser/git.go:42
+#: internal/parser/git.go:43
 msgid "Overwrite remote of the given name if it already exists"
 msgstr ""
 
@@ -705,11 +721,11 @@ msgstr ""
 msgid "Pass stdin to the container"
 msgstr ""
 
-#: internal/parser/volumes.go:86
+#: internal/parser/volumes.go:89
 msgid "Path that is exported by the nfs server"
 msgstr ""
 
-#: cmd/root.go:33
+#: cmd/root.go:38
 msgid "Path to configuration file"
 msgstr ""
 
@@ -729,23 +745,23 @@ msgstr ""
 msgid "Print a detailed description of the selected process"
 msgstr ""
 
-#: internal/parser/pts.go:65
+#: internal/parser/pts.go:66
 msgid "Print a detailed description of the selected process type"
 msgstr ""
 
-#: internal/parser/builds.go:74
+#: internal/parser/builds.go:82
 msgid "Print information about a specific build"
 msgstr ""
 
-#: internal/parser/releases.go:80
+#: internal/parser/releases.go:79
 msgid "Print information about a specific release"
 msgstr ""
 
-#: internal/parser/volumes.go:160
+#: internal/parser/volumes.go:164
 msgid "Print information about a volume"
 msgstr ""
 
-#: internal/parser/builds.go:99
+#: internal/parser/builds.go:107
 msgid "Print process info about a specific build"
 msgstr ""
 
@@ -771,19 +787,19 @@ msgstr ""
 msgid "Prints a list of autoscale options for the application"
 msgstr ""
 
-#: internal/parser/labels.go:34
+#: internal/parser/labels.go:35
 msgid "Prints a list of labels of the application"
 msgstr ""
 
-#: internal/parser/releases.go:81
+#: internal/parser/releases.go:80
 msgid "Prints info about a particular release"
 msgstr ""
 
-#: internal/parser/apps.go:82
+#: internal/parser/apps.go:80
 msgid "Prints info about the current application"
 msgstr ""
 
-#: internal/parser/autodeploy.go:35
+#: internal/parser/autodeploy.go:34
 msgid "Prints info about the current application's autodeploy if or not"
 msgstr ""
 
@@ -821,23 +837,23 @@ msgstr ""
 msgid "Pull environment variables to the path"
 msgstr ""
 
-#: internal/parser/config.go:182
+#: internal/parser/config.go:183
 msgid "Push environment variables from the path"
 msgstr ""
 
-#: internal/parser/limits.go:148
+#: internal/parser/limits.go:149
 msgid "Query plans that meet the specified memory capacity, unit GiB"
 msgstr ""
 
-#: internal/parser/limits.go:147
+#: internal/parser/limits.go:148
 msgid "Query plans that meet the specified number of cpu cores"
 msgstr ""
 
-#: internal/parser/limits.go:149
+#: internal/parser/limits.go:150
 msgid "Query plans that meet the specified spec id, see [specs] subcommand."
 msgstr ""
 
-#: internal/parser/routes.go:248
+#: internal/parser/routes.go:247
 msgid "Remove a route from an application"
 msgstr ""
 
@@ -845,19 +861,19 @@ msgstr ""
 msgid "Remove a token for controller authentication"
 msgstr ""
 
-#: internal/parser/perms.go:128
+#: internal/parser/perms.go:129
 msgid "Remove a user permissions"
 msgstr ""
 
-#: internal/parser/keys.go:80
+#: internal/parser/keys.go:81
 msgid "Remove an SSH key"
 msgstr ""
 
-#: internal/parser/certs.go:89
+#: internal/parser/certs.go:88
 msgid "Remove an SSL certificate from an app"
 msgstr ""
 
-#: internal/parser/labels.go:89
+#: internal/parser/labels.go:90
 msgid "Remove application's label"
 msgstr ""
 
@@ -869,39 +885,31 @@ msgstr ""
 msgid "Remove service from an application"
 msgstr ""
 
-#: internal/parser/keys.go:81
+#: internal/parser/keys.go:82
 msgid "Removes an SSH key for the logged in user"
 msgstr ""
 
-#: internal/parser/git.go:51
+#: internal/parser/git.go:52
 msgid "Removes git remote of application from repository"
 msgstr ""
 
-#: internal/parser/pts.go:90
+#: internal/parser/pts.go:91
 msgid "Restart an application or process types"
 msgstr ""
 
-#: internal/parser/pts.go:89
+#: internal/parser/pts.go:90
 msgid "Restart application process types"
 msgstr ""
 
-#: internal/parser/apps.go:173
+#: internal/parser/apps.go:142
 msgid "Retention time of records in seconds"
 msgstr ""
 
-#: internal/parser/apps.go:125
-msgid "Retrieve application log events"
-msgstr ""
-
-#: internal/parser/apps.go:126
-msgid "Retrieves the most recent log events"
-msgstr ""
-
-#: internal/parser/releases.go:134
+#: internal/parser/releases.go:133
 msgid "Return to a previous release"
 msgstr ""
 
-#: internal/parser/releases.go:135
+#: internal/parser/releases.go:134
 msgid "Rolls back to a previous application release"
 msgstr ""
 
@@ -909,40 +917,48 @@ msgstr ""
 msgid "Rules-file is the file name of rule to apply"
 msgstr ""
 
-#: internal/parser/apps.go:162
+#: internal/parser/apps.go:131
 msgid "Run a command in an ephemeral app container"
 msgstr ""
 
-#: internal/parser/apps.go:163
+#: internal/parser/apps.go:132
 msgid "Runs a command inside an ephemeral app container"
 msgstr ""
 
-#: internal/parser/builds.go:108
+#: internal/parser/builds.go:116
 msgid "Save process info to the local"
 msgstr ""
 
-#: internal/parser/pts.go:123
+#: internal/parser/pts.go:124
 msgid "Scale process types of replicas"
 msgstr ""
 
-#: internal/parser/pts.go:124
+#: internal/parser/pts.go:125
 msgid "Scales an application's processes by type"
 msgstr ""
 
-#: internal/parser/limits.go:125
+#: internal/parser/limits.go:126
 msgid ""
 "Search keywords separated by commas, matching must satisfy all of the "
 "specified."
 msgstr ""
 
-#: internal/parser/config.go:223
+#: 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:248
+#: 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 ""
@@ -951,15 +967,19 @@ msgstr ""
 msgid "Set environment variables for an app"
 msgstr ""
 
-#: internal/parser/healthchecks.go:92
+#: internal/parser/healthchecks.go:90
 msgid "Set healthchecks for an application"
 msgstr ""
 
-#: internal/parser/registry.go:75
+#: 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:69
+#: internal/parser/limits.go:70
 msgid "Set resource limits for an app"
 msgstr ""
 
@@ -983,7 +1003,7 @@ msgstr ""
 msgid "Sets environment variables for an application or config group"
 msgstr ""
 
-#: internal/parser/config.go:183
+#: internal/parser/config.go:184
 msgid ""
 "Sets environment variables for an application or config group.\n"
 "\n"
@@ -992,7 +1012,7 @@ msgid ""
 "stdin, 'drycc config push < .env', or using the --path option."
 msgstr ""
 
-#: internal/parser/healthchecks.go:93
+#: internal/parser/healthchecks.go:91
 msgid ""
 "Sets healthchecks for an application.\n"
 "\n"
@@ -1045,7 +1065,7 @@ msgid ""
 "port number to perform the socket connection on the Container.\n"
 msgstr ""
 
-#: internal/parser/labels.go:60
+#: internal/parser/labels.go:61
 msgid ""
 "Sets labels for an application.\n"
 "\n"
@@ -1055,14 +1075,59 @@ msgid ""
 "application. This information isn't send to scheduler."
 msgstr ""
 
-#: internal/parser/registry.go:76
+#: 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:70
+#: internal/parser/limits.go:71
 msgid ""
 "Sets resource limits for an application.\n"
 "\n"
@@ -1085,7 +1150,7 @@ msgstr ""
 msgid "Sets termination grace period for an application"
 msgstr ""
 
-#: cmd/root.go:69
+#: cmd/root.go:75
 msgid "Shortcut Commands:"
 msgstr ""
 
@@ -1093,25 +1158,25 @@ msgstr ""
 msgid "Show certificate information for an SSL application"
 msgstr ""
 
-#: internal/parser/resources.go:155
+#: internal/parser/resources.go:156
 msgid "Show extra details provided to resource"
 msgstr ""
 
-#: internal/parser/update.go:20
+#: internal/parser/update.go:21
 msgid "Simulate an update, only print the version info."
 msgstr ""
 
-#: internal/parser/apps.go:197
+#: internal/parser/apps.go:166
 msgid ""
 "Skips the prompt for the application name. \n"
 "<app> is the uniquely identifiable name for the application"
 msgstr ""
 
-#: internal/parser/apps.go:134 internal/parser/ps.go:76
+#: internal/parser/ps.go:76
 msgid "Specify if the logs should be streamed"
 msgstr ""
 
-#: internal/parser/resources.go:111 internal/parser/resources.go:195
+#: internal/parser/resources.go:112 internal/parser/resources.go:196
 msgid "Specify values in a YAML file. If set, params will be discard"
 msgstr ""
 
@@ -1123,11 +1188,15 @@ msgstr ""
 msgid "Target CPU utilization percentage"
 msgstr ""
 
-#: cmd/root.go:27
+#: cmd/root.go:29
 msgid "The Drycc command-line client issues API calls to a Drycc controller"
 msgstr ""
 
-#: internal/parser/healthchecks.go:185
+#: 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 ""
 
@@ -1135,19 +1204,23 @@ msgstr ""
 msgid "The IP protocol for this port. Supports TCP, UDP, and SCTP"
 msgstr ""
 
-#: internal/parser/healthchecks.go:88
+#: 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/volumes.go:201
-msgid "Serve a WebDAV service for the volume"
+#: 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/volumes.go:261 internal/parser/apps.go:158
-#: internal/parser/shortcuts.go:44
+#: internal/parser/apps.go:127 internal/parser/shortcuts.go:53
+#: internal/parser/volumes.go:235
 msgid "The filesystem path"
 msgstr ""
 
@@ -1159,7 +1232,7 @@ msgstr ""
 msgid "The group for which the config needs to be pull"
 msgstr ""
 
-#: internal/parser/config.go:194
+#: internal/parser/config.go:195
 msgid "The group for which the config needs to be push"
 msgstr ""
 
@@ -1171,21 +1244,21 @@ msgstr ""
 msgid "The group for which the config needs to be unset"
 msgstr ""
 
-#: internal/parser/config.go:218
+#: internal/parser/config.go:220
 msgid "The group that requires attach configurations"
 msgstr ""
 
-#: internal/parser/config.go:245
+#: internal/parser/config.go:247
 msgid "The group that requires detach configurations"
 msgstr ""
 
-#: internal/parser/healthchecks.go:86
+#: internal/parser/healthchecks.go:84
 msgid ""
-"The healthcheck type, such as 'startupProbe' 'livenessProbe' or "
+"The healthcheck probe, such as 'startupProbe' 'livenessProbe' or "
 "'readinessProbe'"
 msgstr ""
 
-#: internal/parser/volumes.go:85
+#: internal/parser/volumes.go:88
 msgid "The hostname or ip address of the nfs server"
 msgstr ""
 
@@ -1193,67 +1266,63 @@ msgstr ""
 msgid "The id of the token for controller authentication"
 msgstr ""
 
-#: internal/parser/healthchecks.go:186
+#: internal/parser/healthchecks.go:185
 msgid "The initial delay timeout for the probe"
 msgstr ""
 
-#: internal/parser/limits.go:66
-msgid "The limit plan id to apply to the process type"
+#: internal/parser/lifecycles.go:78
+msgid "The lifecycle handler, such as 'postStart' 'preStop'"
 msgstr ""
 
-#: internal/parser/apps.go:135
-msgid "The max seconds of followz the log stream"
+#: internal/parser/limits.go:67
+msgid "The limit plan id to apply to the process type"
 msgstr ""
 
-#: internal/parser/resources.go:24 internal/parser/resources.go:126
-#: internal/parser/certs.go:23 internal/parser/certs.go:48
-#: internal/parser/volumes.go:23 internal/parser/volumes.go:141
-#: internal/parser/routes.go:27 internal/parser/routes.go:107
-#: internal/parser/domains.go:22 internal/parser/domains.go:43
 #: internal/parser/users.go:21 internal/parser/users.go:39
 #: internal/parser/releases.go:33 internal/parser/releases.go:62
-#: internal/parser/tokens.go:22 internal/parser/tokens.go:40
+#: internal/parser/limits.go:127 internal/parser/limits.go:151
+#: internal/parser/certs.go:23 internal/parser/certs.go:48
+#: internal/parser/routes.go:28 internal/parser/routes.go:107
 #: internal/parser/gateways.go:22 internal/parser/gateways.go:44
-#: internal/parser/limits.go:126 internal/parser/limits.go:150
-#: internal/parser/perms.go:24 internal/parser/perms.go:46
+#: internal/parser/volumes.go:23 internal/parser/volumes.go:145
+#: internal/parser/perms.go:25 internal/parser/perms.go:47
+#: internal/parser/tokens.go:22 internal/parser/tokens.go:40
+#: internal/parser/domains.go:22 internal/parser/domains.go:43
+#: internal/parser/resources.go:25 internal/parser/resources.go:127
 msgid "The maximum number of results to display"
 msgstr ""
 
-#: internal/parser/certs.go:107
+#: internal/parser/certs.go:106
 msgid "The name of the cert to get information from"
 msgstr ""
 
-#: internal/parser/certs.go:85
+#: internal/parser/certs.go:84
 msgid "The name of the cert to remove from the app"
 msgstr ""
 
-#: internal/parser/perms.go:67 internal/parser/perms.go:97
-#: internal/parser/perms.go:125
+#: internal/parser/perms.go:68 internal/parser/perms.go:98
+#: internal/parser/perms.go:126
 msgid "The name of the user"
 msgstr ""
 
-#: internal/parser/apps.go:133
-msgid "The number of lines to display"
-msgstr ""
-
 #: internal/parser/ps.go:75
 msgid "The number of lines to display, -1 showing all log lines"
 msgstr ""
 
-#: internal/parser/shortcuts.go:83 internal/parser/pts.go:120
+#: internal/parser/shortcuts.go:95 internal/parser/pts.go:121
 msgid "The number of processes"
 msgstr ""
 
-#: internal/parser/healthchecks.go:187
+#: internal/parser/healthchecks.go:186
 msgid "The number of seconds after which the probe times out"
 msgstr ""
 
-#: internal/parser/registry.go:72
+#: internal/parser/registry.go:73
 msgid "The password of the registry"
 msgstr ""
 
-#: internal/parser/shortcuts.go:58 internal/parser/ps.go:99
-#: internal/parser/ps.go:130 internal/parser/ps.go:152
+#: 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 ""
 
@@ -1269,15 +1338,15 @@ msgstr ""
 msgid "The private key of the SSL certificate"
 msgstr ""
 
-#: internal/parser/volumes.go:260 internal/parser/volumes.go:285
-#: internal/parser/shortcuts.go:82 internal/parser/pts.go:61
-#: internal/parser/pts.go:119 internal/parser/pts.go:153
+#: internal/parser/shortcuts.go:94 internal/parser/volumes.go:234
+#: internal/parser/volumes.go:259 internal/parser/pts.go:62
+#: internal/parser/pts.go:120 internal/parser/pts.go:154
 #: internal/parser/tags.go:26 internal/parser/tags.go:60
 #: internal/parser/tags.go:100 internal/parser/tags.go:131
 msgid "The process name as defined in your Procfile"
 msgstr ""
 
-#: internal/parser/releases.go:130
+#: internal/parser/releases.go:129
 msgid "The process name as defined in your Procfile, such as 'web'"
 msgstr ""
 
@@ -1285,15 +1354,10 @@ msgstr ""
 msgid "The process type as defined in your Procfile"
 msgstr ""
 
-#: internal/parser/limits.go:95
+#: 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/limits.go:65
-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 ""
@@ -1306,8 +1370,8 @@ msgstr ""
 msgid "The ptype for domain"
 msgstr ""
 
-#: internal/parser/registry.go:26 internal/parser/registry.go:50
-#: internal/parser/registry.go:85 internal/parser/registry.go:107
+#: 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 ""
 
@@ -1319,7 +1383,7 @@ msgstr ""
 msgid "The ptype for which the config needs to be pull"
 msgstr ""
 
-#: internal/parser/config.go:193
+#: internal/parser/config.go:194
 msgid "The ptype for which the config needs to be push"
 msgstr ""
 
@@ -1331,23 +1395,35 @@ msgstr ""
 msgid "The ptype for which the config needs to be unset"
 msgstr ""
 
-#: internal/parser/healthchecks.go:180
+#: internal/parser/healthchecks.go:178
 msgid "The ptype for which the health check needs to be applied"
 msgstr ""
 
-#: internal/parser/healthchecks.go:30
+#: internal/parser/healthchecks.go:31
 msgid "The ptype for which the health check needs to be listed"
 msgstr ""
 
-#: internal/parser/healthchecks.go:225
+#: internal/parser/healthchecks.go:224
 msgid "The ptype for which the health check needs to be removed"
 msgstr ""
 
-#: internal/parser/config.go:217
+#: 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:244
+#: internal/parser/config.go:246
 msgid "The ptype that requires detach configurations"
 msgstr ""
 
@@ -1355,28 +1431,32 @@ msgstr ""
 msgid "The public key of the SSL certificate"
 msgstr ""
 
-#: internal/parser/healthchecks.go:184
+#: 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:77 internal/parser/releases.go:131
+#: internal/parser/releases.go:76 internal/parser/releases.go:130
 msgid "The release of the application, such as '1'"
 msgstr ""
 
-#: internal/parser/resources.go:213 internal/parser/resources.go:238
-#: internal/parser/resources.go:260
+#: 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:93 internal/parser/resources.go:176
+#: internal/parser/resources.go:94 internal/parser/resources.go:177
 msgid "The resource instance parameters key"
 msgstr ""
 
-#: internal/parser/resources.go:94 internal/parser/resources.go:177
+#: internal/parser/resources.go:95 internal/parser/resources.go:178
 msgid "The resource instance parameters value"
 msgstr ""
 
-#: internal/parser/resources.go:91 internal/parser/resources.go:174
+#: internal/parser/resources.go:92 internal/parser/resources.go:175
 msgid "The resource's service"
 msgstr ""
 
@@ -1388,22 +1468,26 @@ msgstr ""
 msgid "The route's backend, pattern: <service>:<port>,<weight>"
 msgstr ""
 
-#: internal/parser/resources.go:62
+#: internal/parser/resources.go:63
 msgid "The service name for plans"
 msgstr ""
 
-#: internal/parser/resources.go:92 internal/parser/resources.go:175
+#: internal/parser/resources.go:93 internal/parser/resources.go:176
 msgid "The service's plan"
 msgstr ""
 
-#: internal/parser/apps.go:159 internal/parser/shortcuts.go:45
+#: internal/parser/apps.go:128 internal/parser/shortcuts.go:54
 msgid "The shell command to run inside the container"
 msgstr ""
 
-#: internal/parser/builds.go:57
+#: 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 ""
@@ -1422,42 +1506,42 @@ 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:245
+#: internal/parser/routes.go:212 internal/parser/routes.go:244
 msgid "The unique name of the route"
 msgstr ""
 
-#: internal/parser/resources.go:23 internal/parser/certs.go:22
-#: internal/parser/autodeploy.go:21 internal/parser/volumes.go:22
-#: internal/parser/routes.go:26 internal/parser/services.go:23
-#: internal/parser/autoscale.go:21 internal/parser/apps.go:88
-#: internal/parser/apps.go:107 internal/parser/apps.go:132
-#: internal/parser/apps.go:170 internal/parser/apps.go:196
-#: internal/parser/apps.go:217 internal/parser/domains.go:21
-#: internal/parser/releases.go:32 internal/parser/registry.go:25
-#: internal/parser/labels.go:20 internal/parser/routing.go:19
-#: internal/parser/gateways.go:21 internal/parser/builds.go:19
-#: internal/parser/builds.go:56 internal/parser/autorollback.go:19
-#: internal/parser/limits.go:20 internal/parser/git.go:17
-#: internal/parser/timeouts.go:21 internal/parser/config.go:29
-#: internal/parser/pts.go:20 internal/parser/pts.go:133
-#: internal/parser/ps.go:20 internal/parser/ps.go:112
-#: internal/parser/tags.go:25 internal/parser/perms.go:23
+#: internal/parser/builds.go:21 internal/parser/builds.go:64
+#: internal/parser/apps.go:86 internal/parser/apps.go:104
+#: internal/parser/apps.go:139 internal/parser/apps.go:165
+#: internal/parser/apps.go:186 internal/parser/autorollback.go:19
+#: internal/parser/timeouts.go:21 internal/parser/releases.go:32
+#: internal/parser/limits.go:21 internal/parser/autoscale.go:21
+#: internal/parser/ps.go:21 internal/parser/ps.go:112
+#: internal/parser/services.go:23 internal/parser/certs.go:22
+#: internal/parser/config.go:29 internal/parser/routes.go:27
+#: internal/parser/routing.go:20 internal/parser/labels.go:21
+#: internal/parser/gateways.go:21 internal/parser/volumes.go:22
+#: internal/parser/autodeploy.go:20 internal/parser/git.go:18
+#: internal/parser/perms.go:24 internal/parser/pts.go:21
+#: internal/parser/pts.go:134 internal/parser/tags.go:25
+#: internal/parser/registry.go:26 internal/parser/domains.go:21
+#: internal/parser/resources.go:24
 msgid "The uniquely identifiable name for the application"
 msgstr ""
 
-#: internal/parser/healthchecks.go:29
+#: internal/parser/healthchecks.go:30 internal/parser/lifecycles.go:30
 msgid "The uniquely identifiable name of the application"
 msgstr ""
 
-#: internal/parser/perms.go:68
+#: internal/parser/perms.go:69
 msgid "The user permissions (view,change,delete)"
 msgstr ""
 
-#: internal/parser/perms.go:98
+#: internal/parser/perms.go:99
 msgid "The user's permissions (view,change,delete)"
 msgstr ""
 
-#: internal/parser/registry.go:71
+#: internal/parser/registry.go:72
 msgid "The username of the registry"
 msgstr ""
 
@@ -1473,8 +1557,8 @@ msgstr ""
 msgid "The value to apply to the process type in seconds"
 msgstr ""
 
-#: internal/parser/builds.go:20 internal/parser/builds.go:79
-#: internal/parser/builds.go:104
+#: 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 ""
 
@@ -1486,15 +1570,19 @@ msgstr ""
 msgid "The version for which the config needs to be pull"
 msgstr ""
 
-#: internal/parser/healthchecks.go:31 internal/parser/healthchecks.go:56
+#: 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/limits.go:21 internal/parser/limits.go:46
+#: 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:27 internal/parser/registry.go:51
+#: internal/parser/registry.go:28 internal/parser/registry.go:52
 msgid "The version for which the registry needs to be listed"
 msgstr ""
 
@@ -1506,49 +1594,50 @@ msgstr ""
 msgid "The version for which the timeout needs to be listed"
 msgstr ""
 
-#: internal/parser/volumes.go:57 internal/parser/volumes.go:114
-#: internal/parser/volumes.go:259 internal/parser/volumes.go:284
-#: internal/parser/apps.go:157 internal/parser/shortcuts.go:43
+#: internal/parser/apps.go:126 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
 msgid "The volume name"
 msgstr ""
 
-#: internal/parser/volumes.go:157
+#: internal/parser/volumes.go:161
 msgid "The volume name to be info"
 msgstr ""
 
-#: internal/parser/volumes.go:183
+#: internal/parser/volumes.go:187
 msgid "The volume name to be removed"
 msgstr ""
 
-#: internal/parser/volumes.go:58 internal/parser/volumes.go:115
+#: internal/parser/volumes.go:59 internal/parser/volumes.go:119
 msgid "The volume size, such as '500G'"
 msgstr ""
 
-#: internal/parser/volumes.go:84
+#: internal/parser/volumes.go:87
 msgid "The volume type, such as csi, nfs, oss"
 msgstr ""
 
-#: internal/parser/resources.go:90 internal/parser/resources.go:144
-#: internal/parser/resources.go:173
+#: internal/parser/resources.go:91 internal/parser/resources.go:145
+#: internal/parser/resources.go:174
 msgid "This resource instance alias"
 msgstr ""
 
-#: internal/parser/releases.go:118 internal/parser/tokens.go:103
-#: internal/parser/builds.go:60 internal/parser/builds.go:107
-#: internal/parser/pts.go:98
+#: internal/parser/builds.go:68 internal/parser/builds.go:115
+#: internal/parser/releases.go:117 internal/parser/tokens.go:103
+#: internal/parser/pts.go:99
 msgid "To proceed, type \"yes\""
 msgstr ""
 
 #: internal/parser/config.go:94 internal/parser/config.go:123
-#: internal/parser/config.go:196
+#: internal/parser/config.go:198
 msgid "To proceed, type 'yes'"
 msgstr ""
 
-#: internal/parser/apps.go:209
+#: internal/parser/apps.go:178
 msgid "Transfer app ownership to another user"
 msgstr ""
 
-#: internal/parser/apps.go:210
+#: internal/parser/apps.go:179
 msgid "Transfer application ownership to another user."
 msgstr ""
 
@@ -1564,7 +1653,7 @@ msgstr ""
 msgid "Unbinds a domain for an application"
 msgstr ""
 
-#: internal/parser/volumes.go:288
+#: internal/parser/volumes.go:262
 msgid "Unmount a volume from process of the application"
 msgstr ""
 
@@ -1576,15 +1665,19 @@ msgstr ""
 msgid "Unset environment variables for an app"
 msgstr ""
 
-#: internal/parser/healthchecks.go:213
+#: internal/parser/healthchecks.go:212
 msgid "Unset healthchecks for an application"
 msgstr ""
 
-#: internal/parser/registry.go:100
+#: 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:98
+#: internal/parser/limits.go:99
 msgid "Unset resource limits for an app"
 msgstr ""
 
@@ -1600,11 +1693,11 @@ msgstr ""
 msgid "Unsets an environment variable for an application or config group"
 msgstr ""
 
-#: internal/parser/labels.go:90
+#: internal/parser/labels.go:91
 msgid "Unsets labels for an application"
 msgstr ""
 
-#: internal/parser/registry.go:101
+#: internal/parser/registry.go:102
 msgid "Unsets registry information for an application"
 msgstr ""
 
@@ -1614,15 +1707,15 @@ msgid ""
 "controller"
 msgstr ""
 
-#: internal/parser/resources.go:180
+#: internal/parser/resources.go:181
 msgid "Update a resource from the application"
 msgstr ""
 
-#: internal/parser/perms.go:101
+#: internal/parser/perms.go:102
 msgid "Update a user permission"
 msgstr ""
 
-#: internal/parser/update.go:14 internal/parser/update.go:15
+#: internal/parser/update.go:15 internal/parser/update.go:16
 msgid "Update workflow cli to latest release"
 msgstr ""
 
@@ -1630,7 +1723,7 @@ msgstr ""
 msgid "View TLS settings information"
 msgstr ""
 
-#: internal/parser/routing.go:33
+#: internal/parser/routing.go:34
 msgid "View application's routability information"
 msgstr ""
 
@@ -1638,19 +1731,19 @@ msgstr ""
 msgid "View autorollback info for an application"
 msgstr ""
 
-#: internal/parser/apps.go:81
+#: internal/parser/apps.go:79
 msgid "View info about an application"
 msgstr ""
 
-#: internal/parser/apps.go:171
+#: internal/parser/apps.go:140
 msgid "Volume mounts in format 'volume:path'"
 msgstr ""
 
-#: internal/parser/builds.go:42
+#: internal/parser/builds.go:49
 msgid "default container image"
 msgstr ""
 
-#: internal/parser/builds.go:44
+#: internal/parser/builds.go:52
 msgid "imports an image and deploys as a new release"
 msgstr ""
 
@@ -1673,45 +1766,46 @@ msgid ""
 "             only single extra service per Porcfile type could be created."
 msgstr ""
 
-#: internal/parser/volumes.go:95
-msgid "Secret access key for authentication"
-msgstr ""
-
-#: internal/parser/volumes.go:97
-msgid "Force a path-style endpoint to be used"
-msgstr ""
-
-#: internal/parser/resources.go:224
+#: 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:87
+#: internal/parser/healthchecks.go:85
 msgid "the healthcheck probe type, such as 'httpGet', 'exec' or 'tcpSocket'"
 msgstr ""
 
-#: internal/parser/healthchecks.go:210
+#: internal/parser/healthchecks.go:209
 msgid ""
-"the healthcheck type, such as 'startupProbe' 'livenessProbe' or "
+"the healthcheck probe, such as 'startupProbe' 'livenessProbe' or "
 "'readinessProbe'"
 msgstr ""
 
-#: internal/parser/labels.go:84
+#: internal/parser/labels.go:85
 msgid "the label key to unset, for example: \"git_repo\" or \"team\""
 msgstr ""
 
-#: internal/parser/labels.go:55
+#: internal/parser/labels.go:56
 msgid "the label key, for example: \"git_repo\" or \"team\""
 msgstr ""
 
-#: internal/parser/labels.go:56
+#: internal/parser/labels.go:57
 msgid ""
 "the label value, for example: \"https://github.com/drycc/workflow\" or "
 "\"frontend\""
 msgstr ""
 
-#: internal/parser/resources.go:263
+#: 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
index f89f42030677f0a77562548658c661ef6b008d2b..279198927fb6591dc07ee12d9736443a255ae989 100644
GIT binary patch
diff --git a/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po b/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po
index 0ec35cd79..35435ae03 100644
--- a/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po
+++ b/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: EMAIL\n"
-"POT-Creation-Date: 2025-04-14 15:39+0800\n"
+"POT-Creation-Date: 2025-12-08 16:06+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"
@@ -17,35 +17,31 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: internal/parser/builds.go:58 internal/parser/builds.go:105
+#: 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:52
+#: 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:195
+#: internal/parser/config.go:158 internal/parser/config.go:196
 msgid "A path leading to an environment file"
 msgstr "指向环境文件的路径"
 
-#: internal/parser/config.go:179
-msgid "Merge config values"
-msgstr "合并配置"
-
-#: internal/parser/tls.go:121
+#: internal/parser/tls.go:123
 msgid "ACME account email"
 msgstr "ACME账户邮箱"
 
-#: internal/parser/tls.go:122
+#: internal/parser/tls.go:124
 msgid "ACME server URL"
 msgstr "ACME服务器URL"
 
-#: internal/parser/volumes.go:90
+#: internal/parser/volumes.go:93
 msgid "Access key id for authentication"
 msgstr "用于身份验证的访问密钥ID"
 
-#: internal/parser/tls.go:55 internal/parser/tls.go:83
+#: internal/parser/tls.go:55 internal/parser/tls.go:84
 msgid "Action to perform, 'enable' or 'disable'"
 msgstr "要执行的操作，'enable' 或 'disable'"
 
@@ -53,11 +49,11 @@ msgstr "要执行的操作，'enable' 或 'disable'"
 msgid "Add a token for controller authentication"
 msgstr "添加用于控制器身份验证的令牌"
 
-#: internal/parser/perms.go:71
+#: internal/parser/perms.go:72
 msgid "Add a user permissions"
 msgstr "添加用户权限"
 
-#: internal/parser/keys.go:56
+#: internal/parser/keys.go:57
 msgid "Add an SSH key"
 msgstr "添加SSH密钥"
 
@@ -65,15 +61,15 @@ msgstr "添加SSH密钥"
 msgid "Add an SSL certificate to an app"
 msgstr "向应用添加SSL证书"
 
-#: internal/parser/labels.go:59
+#: internal/parser/labels.go:60
 msgid "Add new application's label"
 msgstr "添加新的应用程序标签"
 
-#: internal/parser/keys.go:57
+#: internal/parser/keys.go:58
 msgid "Adds SSH keys for the logged in user"
 msgstr "为登录用户添加SSH密钥"
 
-#: internal/parser/git.go:35
+#: internal/parser/git.go:36
 msgid "Adds git remote of application to repository"
 msgstr "将应用程序的Git远程仓库添加到代码库"
 
@@ -90,11 +86,11 @@ msgstr "应用程序配置信息"
 msgid "Application name"
 msgstr "应用程序名称"
 
-#: internal/parser/certs.go:136
+#: internal/parser/certs.go:135
 msgid "Attach a certificate to a domain"
 msgstr "将证书附加到域名"
 
-#: internal/parser/certs.go:135
+#: internal/parser/certs.go:134
 msgid "Attach an SSL certificate to a domain"
 msgstr "将SSL证书附加到域名"
 
@@ -106,7 +102,7 @@ msgstr "附加到网关"
 msgid "Authenticate against a controller"
 msgstr "对控制器进行身份验证"
 
-#: internal/parser/tls.go:88
+#: internal/parser/tls.go:89
 msgid "Automatic certificate management"
 msgstr "自动证书管理"
 
@@ -114,23 +110,23 @@ msgstr "自动证书管理"
 msgid "Bind a domain to an application"
 msgstr "将域名绑定到应用程序"
 
-#: internal/parser/resources.go:241
+#: internal/parser/resources.go:242
 msgid "Bind a resource for an application"
 msgstr "为应用程序绑定资源"
 
-#: internal/parser/volumes.go:89
+#: internal/parser/volumes.go:92
 msgid "Bucket name in object storage"
 msgstr "对象存储中的存储桶名称"
 
-#: internal/parser/tls.go:123
+#: internal/parser/tls.go:125
 msgid "CA key ID"
 msgstr "CA密钥ID"
 
-#: internal/parser/tls.go:124
+#: internal/parser/tls.go:126
 msgid "CA key secret"
 msgstr "CA密钥机密"
 
-#: internal/parser/pts.go:156
+#: internal/parser/pts.go:157
 msgid "Clean process types of not used"
 msgstr "清理未使用的进程类型"
 
@@ -138,20 +134,20 @@ msgstr "清理未使用的进程类型"
 msgid "Clear the current user session"
 msgstr "清除当前用户会话"
 
-#: internal/parser/apps.go:172
+#: internal/parser/apps.go:141
 msgid "Command execution timeout in seconds"
 msgstr "命令执行超时时间（秒）"
 
-#: internal/parser/certs.go:131
+#: 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:156
+#: internal/parser/certs.go:155
 msgid "Common name of the domain to detach from"
 msgstr "要解绑的域名通用名称"
 
-#: internal/parser/tls.go:115
+#: internal/parser/tls.go:117
 msgid "Configure automatic certificate management issuer details"
 msgstr "配置自动证书管理颁发者详情"
 
@@ -159,7 +155,7 @@ msgstr "配置自动证书管理颁发者详情"
 msgid "Create a new application"
 msgstr "创建新应用"
 
-#: internal/parser/resources.go:97
+#: internal/parser/resources.go:98
 msgid "Create a resource for the application"
 msgstr "为应用创建资源"
 
@@ -167,7 +163,7 @@ msgstr "为应用创建资源"
 msgid "Create a route for an application"
 msgstr "创建应用路由"
 
-#: internal/parser/volumes.go:61
+#: internal/parser/volumes.go:62
 msgid "Create a volume for the application"
 msgstr "创建应用卷"
 
@@ -185,7 +181,7 @@ msgid ""
 "automatically"
 msgstr "创建新应用。若未指定<id>，则自动生成一个唯一ID"
 
-#: internal/parser/builds.go:45
+#: internal/parser/builds.go:53
 msgid ""
 "Creates a new build of an application. Imports an <image> and deploys it to "
 "Drycc\n"
@@ -213,11 +209,11 @@ msgstr "为应用创建网关并绑定以监听主域名流量"
 msgid "Creates routes for an application, provides a way to route requests"
 msgstr "创建应用路由规则，实现请求分发"
 
-#: internal/parser/resources.go:216
+#: internal/parser/resources.go:217
 msgid "Delete a resource from the application"
 msgstr "从应用中删除资源"
 
-#: internal/parser/volumes.go:186
+#: internal/parser/volumes.go:190
 msgid "Delete a volume from the application"
 msgstr "从应用中删除卷"
 
@@ -229,23 +225,23 @@ msgstr "删除选定进程"
 msgid "Deletes specific gateway for application"
 msgstr "删除应用指定网关"
 
-#: internal/parser/releases.go:109
+#: internal/parser/releases.go:108
 msgid "Deploy the latest release by process types"
 msgstr "按进程类型部署最新版本"
 
-#: internal/parser/pts.go:64
+#: internal/parser/pts.go:65
 msgid "Describe a process type"
 msgstr "描述进程类型定义"
 
-#: internal/parser/apps.go:190
+#: internal/parser/apps.go:159
 msgid "Destroy an application"
 msgstr "销毁应用"
 
-#: internal/parser/certs.go:160
+#: internal/parser/certs.go:159
 msgid "Detach an SSL certificate from a domain"
 msgstr "从域名解绑SSL证书"
 
-#: internal/parser/certs.go:161
+#: internal/parser/certs.go:160
 msgid "Detach certificate from a domain"
 msgstr "从域名解绑证书"
 
@@ -269,11 +265,11 @@ msgstr ""
 msgid "Disable autorollback for an application"
 msgstr "禁用应用自动回滚"
 
-#: internal/parser/routing.go:57
+#: internal/parser/routing.go:58
 msgid "Disable routing for an application"
 msgstr "禁用应用路由功能"
 
-#: internal/parser/autodeploy.go:59
+#: internal/parser/autodeploy.go:58
 msgid "Disables autodeploy for an app"
 msgstr "禁用应用自动部署"
 
@@ -281,7 +277,7 @@ msgstr "禁用应用自动部署"
 msgid "Display client version"
 msgstr "显示客户端版本"
 
-#: cmd/root.go:34
+#: cmd/root.go:39
 msgid "Display help information"
 msgstr "显示帮助信息"
 
@@ -301,7 +297,7 @@ msgstr "显示当前登录用户信息"
 msgid "Do not create a 'drycc' git remote"
 msgstr "不创建drycc Git远程仓库"
 
-#: internal/parser/builds.go:59 internal/parser/builds.go:106
+#: internal/parser/builds.go:67 internal/parser/builds.go:114
 msgid "Drycc config path to the application"
 msgstr "Drycc应用配置路径"
 
@@ -321,11 +317,11 @@ msgstr ""
 msgid "Enable autorollback for an application"
 msgstr "启用应用自动回滚"
 
-#: internal/parser/routing.go:45
+#: internal/parser/routing.go:46
 msgid "Enable routing for an application"
 msgstr "启用应用路由功能"
 
-#: internal/parser/autodeploy.go:47
+#: internal/parser/autodeploy.go:46
 msgid "Enables autodeploy for an app"
 msgstr "启用应用自动部署"
 
@@ -333,7 +329,7 @@ msgstr "启用应用自动部署"
 msgid "Enables or disables SSL certificate verification for API requests"
 msgstr "启用/禁用API请求的SSL证书验证"
 
-#: internal/parser/volumes.go:88
+#: internal/parser/volumes.go:91
 msgid "Endpoint url for object storage service"
 msgstr "对象存储服务端点URL"
 
@@ -341,7 +337,7 @@ msgstr "对象存储服务端点URL"
 msgid "Execute a command in a container"
 msgstr "在容器中执行命令"
 
-#: internal/parser/volumes.go:118
+#: internal/parser/volumes.go:122
 msgid "Expand a volume capacity for the application"
 msgstr "扩展应用卷容量"
 
@@ -365,7 +361,7 @@ msgstr ""
 msgid "Fetch detailed user information"
 msgstr "获取用户详细信息"
 
-#: internal/parser/certs.go:112
+#: internal/parser/certs.go:111
 msgid "Fetch more detailed information about a certificate"
 msgstr "获取证书详细信息"
 
@@ -373,15 +369,19 @@ msgstr "获取证书详细信息"
 msgid "Force TLS settings to HTTPS-only redirection"
 msgstr "强制TLS设置为HTTPS重定向"
 
-#: internal/parser/releases.go:117
+#: 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:147
+#: internal/parser/resources.go:148
 msgid "Get a resource's detail in the application"
 msgstr "获取应用资源详情"
 
-#: internal/parser/certs.go:111
+#: internal/parser/certs.go:110
 msgid "Get detailed informaton about the certificate"
 msgstr "获取证书详细信息"
 
@@ -393,11 +393,11 @@ msgstr "获取路由规则"
 msgid "Get route rules for an application"
 msgstr "获取应用路由规则"
 
-#: internal/parser/healthchecks.go:188
+#: internal/parser/healthchecks.go:187
 msgid "How often (in seconds) to perform the probe"
 msgstr "探测执行频率（秒）"
 
-#: internal/parser/keys.go:29
+#: internal/parser/keys.go:30
 msgid "List SSH keys for the logged in user"
 msgstr "列出登录用户的SSH密钥"
 
@@ -405,23 +405,23 @@ msgstr "列出登录用户的SSH密钥"
 msgid "List SSL certificates for an application"
 msgstr "列出应用SSL证书"
 
-#: internal/parser/apps.go:65
+#: internal/parser/apps.go:64
 msgid "List accessible applications"
 msgstr "列出可访问应用"
 
-#: internal/parser/limits.go:140
+#: internal/parser/limits.go:141
 msgid "List all available limit plans"
 msgstr "列出所有可用配额方案"
 
-#: internal/parser/limits.go:118
+#: internal/parser/limits.go:119
 msgid "List all available limit specs"
 msgstr "列出所有可用配额规格"
 
-#: internal/parser/resources.go:65
+#: internal/parser/resources.go:66
 msgid "List all available plans for a resource service"
 msgstr "列出资源服务所有可用方案"
 
-#: internal/parser/resources.go:44
+#: internal/parser/resources.go:45
 msgid "List all available resource services"
 msgstr "列出所有资源服务"
 
@@ -429,7 +429,7 @@ msgstr "列出所有资源服务"
 msgid "List all registered users"
 msgstr "列出所有注册用户"
 
-#: internal/parser/perms.go:39
+#: internal/parser/perms.go:40
 msgid "List all user permissions"
 msgstr "列出所有用户权限"
 
@@ -445,7 +445,7 @@ msgstr "列出应用网关"
 msgid "List application pods"
 msgstr "列出应用Pod"
 
-#: internal/parser/pts.go:37
+#: internal/parser/pts.go:38
 msgid "List application process types"
 msgstr "列出应用进程类型"
 
@@ -457,7 +457,7 @@ msgstr "列出应用路由"
 msgid "List application services"
 msgstr "列出应用服务"
 
-#: internal/parser/labels.go:33
+#: internal/parser/labels.go:34
 msgid "List application's labels"
 msgstr "列出应用标签"
 
@@ -473,11 +473,15 @@ msgstr "列出绑定到应用的域名"
 msgid "List healthchecks for an application"
 msgstr "列出应用健康检查配置"
 
-#: internal/parser/registry.go:43
+#: 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:40
+#: internal/parser/limits.go:41
 msgid "List resource limits for an app"
 msgstr "列出应用资源配额"
 
@@ -485,11 +489,11 @@ msgstr "列出应用资源配额"
 msgid "List resource timeouts for an app"
 msgstr "列出应用资源超时设置"
 
-#: internal/parser/resources.go:119
+#: internal/parser/resources.go:120
 msgid "List resources in the application"
 msgstr "列出应用资源"
 
-#: internal/parser/limits.go:117
+#: internal/parser/limits.go:118
 msgid "List specification information of the server"
 msgstr "列出服务器规格信息"
 
@@ -497,11 +501,11 @@ msgstr "列出服务器规格信息"
 msgid "List tags for an application"
 msgstr "列出应用标签"
 
-#: internal/parser/volumes.go:134
+#: internal/parser/volumes.go:138
 msgid "List volumes in the application"
 msgstr "列出应用卷"
 
-#: internal/parser/apps.go:66
+#: internal/parser/apps.go:65
 msgid "Lists applications visible to the current user"
 msgstr "列出当前用户可见的应用"
 
@@ -509,7 +513,7 @@ msgstr "列出当前用户可见的应用"
 msgid "Lists gateways for an application"
 msgstr "列出应用网关"
 
-#: internal/parser/pts.go:38
+#: internal/parser/pts.go:39
 msgid "Lists process types servicing an application"
 msgstr "列出应用进程类型"
 
@@ -517,7 +521,7 @@ msgstr "列出应用进程类型"
 msgid "Lists processes servicing an application"
 msgstr "列出应用运行进程"
 
-#: internal/parser/registry.go:44
+#: internal/parser/registry.go:45
 msgid "Lists registry information for an application"
 msgstr "列出应用镜像仓库信息"
 
@@ -549,11 +553,15 @@ msgstr "管理应用TLS/SSL设置"
 msgid "Manage and assign domain names to your applications"
 msgstr "管理并分配域名到应用"
 
-#: internal/parser/healthchecks.go:23
+#: internal/parser/healthchecks.go:24
 msgid "Manage application healthchecks"
 msgstr "管理应用健康检查"
 
-#: internal/parser/apps.go:17
+#: internal/parser/lifecycles.go:24
+msgid "Manage application lifecycles"
+msgstr "管理应用生命周期"
+
+#: internal/parser/apps.go:18
 msgid "Manage applications used to provide services"
 msgstr "管理服务型应用"
 
@@ -561,7 +569,7 @@ msgstr "管理服务型应用"
 msgid "Manage authentication"
 msgstr "管理身份验证"
 
-#: internal/parser/autodeploy.go:15
+#: internal/parser/autodeploy.go:14
 msgid "Manage autodeploy if or not for applications"
 msgstr "管理应用自动部署开关"
 
@@ -573,7 +581,7 @@ msgstr "管理应用自动回滚开关"
 msgid "Manage autoscale for applications"
 msgstr "管理应用自动扩缩容"
 
-#: internal/parser/builds.go:13
+#: internal/parser/builds.go:15
 msgid "Manage builds created using 'git push'"
 msgstr "管理通过git push创建的构建版本"
 
@@ -585,15 +593,15 @@ msgstr "管理定义应用配置的环境变量"
 msgid "Manage gateways for your applications"
 msgstr "管理应用网关"
 
-#: internal/parser/git.go:13
+#: internal/parser/git.go:14
 msgid "Manage git for applications"
 msgstr "管理应用Git配置"
 
-#: internal/parser/labels.go:14
+#: internal/parser/labels.go:15
 msgid "Manage labels of application"
 msgstr "管理应用标签"
 
-#: internal/parser/perms.go:16
+#: internal/parser/perms.go:17
 msgid "Manage permissions for applications"
 msgstr "管理应用权限"
 
@@ -601,15 +609,15 @@ msgstr "管理应用权限"
 msgid "Manage pods termination grace period"
 msgstr "管理Pod终止宽限期"
 
-#: internal/parser/registry.go:19
+#: internal/parser/registry.go:20
 msgid "Manage private registry information for your application"
 msgstr "管理应用私有镜像仓库信息"
 
-#: internal/parser/pts.go:14
+#: internal/parser/pts.go:15
 msgid "Manage process types inside an app"
 msgstr "管理应用进程类型"
 
-#: internal/parser/ps.go:14
+#: internal/parser/ps.go:15
 msgid "Manage processes inside an app"
 msgstr "管理应用进程"
 
@@ -621,19 +629,19 @@ msgstr "管理注册用户"
 msgid "Manage releases of an application"
 msgstr "管理应用版本"
 
-#: internal/parser/limits.go:14
+#: internal/parser/limits.go:15
 msgid "Manage resource limits for your application"
 msgstr "管理应用资源配额"
 
-#: internal/parser/resources.go:16
+#: internal/parser/resources.go:17
 msgid "Manage resources for your applications"
 msgstr "管理应用资源"
 
-#: internal/parser/routing.go:13
+#: internal/parser/routing.go:14
 msgid "Manage routability of an application"
 msgstr "管理应用路由能力"
 
-#: internal/parser/routes.go:19
+#: internal/parser/routes.go:20
 msgid "Manage routes for your applications"
 msgstr "管理应用路由规则"
 
@@ -641,7 +649,7 @@ msgstr "管理应用路由规则"
 msgid "Manage services for your applications"
 msgstr "管理应用服务"
 
-#: internal/parser/keys.go:14
+#: internal/parser/keys.go:15
 msgid "Manage ssh keys used for 'git push' deployments"
 msgstr "管理用于git push部署的SSH密钥"
 
@@ -661,39 +669,43 @@ msgstr "管理应用卷"
 msgid "Max replicas to scale up to"
 msgstr "可扩展的最大副本数"
 
-#: internal/parser/apps.go:22 internal/parser/apps.go:72
+#: internal/parser/apps.go:23 internal/parser/apps.go:71
 msgid "Maximum number of results to display"
 msgstr "最多显示结果数量"
 
-#: internal/parser/healthchecks.go:190
+#: 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:189
+#: internal/parser/healthchecks.go:188
 msgid ""
 "Minimum consecutive successes for the probe to be considered successful "
 "after having failed"
 msgstr "探测失败后需连续成功的最小次数（判定为成功）"
 
-#: internal/parser/volumes.go:264
+#: internal/parser/volumes.go:238
 msgid "Mount a volume to process of the application"
 msgstr "将卷挂载到应用进程"
 
-#: internal/parser/apps.go:55 internal/parser/git.go:41
+#: internal/parser/apps.go:55 internal/parser/git.go:42
 msgid "Name of remote to create"
 msgstr "要创建的远程名称"
 
-#: internal/parser/keys.go:51
+#: internal/parser/keys.go:52
 msgid "Name of the SSH key"
 msgstr "SSH密钥名称"
 
-#: internal/parser/certs.go:130
+#: internal/parser/certs.go:129
 msgid "Name of the certificate to attach domain to"
 msgstr "要附加域名的证书名称"
 
-#: internal/parser/certs.go:155
+#: internal/parser/certs.go:154
 msgid "Name of the certificate to deatch from a domain"
 msgstr "要解绑域名的证书名称"
 
@@ -706,15 +718,15 @@ msgid ""
 "Number or name of the port to access on the pods targeted by the service"
 msgstr "服务目标Pod上要访问的端口号或名称"
 
-#: internal/parser/apps.go:100
+#: internal/parser/apps.go:97
 msgid "Open the application in a browser"
 msgstr "在浏览器中打开应用"
 
-#: internal/parser/apps.go:101
+#: internal/parser/apps.go:98
 msgid "Opens a URL to the application in the default browser"
 msgstr "通过默认浏览器打开应用URL"
 
-#: internal/parser/git.go:42
+#: internal/parser/git.go:43
 msgid "Overwrite remote of the given name if it already exists"
 msgstr "若远程名称已存在则覆盖"
 
@@ -722,11 +734,11 @@ msgstr "若远程名称已存在则覆盖"
 msgid "Pass stdin to the container"
 msgstr "将标准输入传递给容器"
 
-#: internal/parser/volumes.go:86
+#: internal/parser/volumes.go:89
 msgid "Path that is exported by the nfs server"
 msgstr "NFS服务器导出的路径"
 
-#: cmd/root.go:33
+#: cmd/root.go:38
 msgid "Path to configuration file"
 msgstr "配置文件的路径"
 
@@ -746,23 +758,23 @@ msgstr "网关监听器接收的网络端口"
 msgid "Print a detailed description of the selected process"
 msgstr "显示所选进程的详细信息"
 
-#: internal/parser/pts.go:65
+#: internal/parser/pts.go:66
 msgid "Print a detailed description of the selected process type"
 msgstr "显示所选进程类型的详细信息"
 
-#: internal/parser/builds.go:74
+#: internal/parser/builds.go:82
 msgid "Print information about a specific build"
 msgstr "显示特定构建版本信息"
 
-#: internal/parser/releases.go:80
+#: internal/parser/releases.go:79
 msgid "Print information about a specific release"
 msgstr "显示特定版本信息"
 
-#: internal/parser/volumes.go:160
+#: internal/parser/volumes.go:164
 msgid "Print information about a volume"
 msgstr "显示卷信息"
 
-#: internal/parser/builds.go:99
+#: internal/parser/builds.go:107
 msgid "Print process info about a specific build"
 msgstr "显示特定构建版本的进程信息"
 
@@ -788,19 +800,19 @@ msgstr "显示该容器的日志"
 msgid "Prints a list of autoscale options for the application"
 msgstr "显示应用自动扩缩容选项列表"
 
-#: internal/parser/labels.go:34
+#: internal/parser/labels.go:35
 msgid "Prints a list of labels of the application"
 msgstr "显示应用标签列表"
 
-#: internal/parser/releases.go:81
+#: internal/parser/releases.go:80
 msgid "Prints info about a particular release"
 msgstr "显示特定版本信息"
 
-#: internal/parser/apps.go:82
+#: internal/parser/apps.go:80
 msgid "Prints info about the current application"
 msgstr "显示当前应用信息"
 
-#: internal/parser/autodeploy.go:35
+#: internal/parser/autodeploy.go:34
 msgid "Prints info about the current application's autodeploy if or not"
 msgstr "显示当前应用是否启用自动部署"
 
@@ -838,23 +850,23 @@ msgstr "提供账户用户名"
 msgid "Pull environment variables to the path"
 msgstr "将环境变量拉取到指定路径"
 
-#: internal/parser/config.go:182
+#: internal/parser/config.go:183
 msgid "Push environment variables from the path"
 msgstr "从指定路径推送环境变量"
 
-#: internal/parser/limits.go:148
+#: internal/parser/limits.go:149
 msgid "Query plans that meet the specified memory capacity, unit GiB"
 msgstr "查询符合指定内存容量（单位GiB）的配额方案"
 
-#: internal/parser/limits.go:147
+#: internal/parser/limits.go:148
 msgid "Query plans that meet the specified number of cpu cores"
 msgstr "查询符合指定CPU核心数的配额方案"
 
-#: internal/parser/limits.go:149
+#: internal/parser/limits.go:150
 msgid "Query plans that meet the specified spec id, see [specs] subcommand."
 msgstr "查询符合指定规格ID的配额方案，使用[specs]子命令查看可用规格"
 
-#: internal/parser/routes.go:248
+#: internal/parser/routes.go:247
 msgid "Remove a route from an application"
 msgstr "从应用移除路由"
 
@@ -862,19 +874,19 @@ msgstr "从应用移除路由"
 msgid "Remove a token for controller authentication"
 msgstr "移除控制器认证令牌"
 
-#: internal/parser/perms.go:128
+#: internal/parser/perms.go:129
 msgid "Remove a user permissions"
 msgstr "移除用户权限"
 
-#: internal/parser/keys.go:80
+#: internal/parser/keys.go:81
 msgid "Remove an SSH key"
 msgstr "移除SSH密钥"
 
-#: internal/parser/certs.go:89
+#: internal/parser/certs.go:88
 msgid "Remove an SSL certificate from an app"
 msgstr "从应用移除SSL证书"
 
-#: internal/parser/labels.go:89
+#: internal/parser/labels.go:90
 msgid "Remove application's label"
 msgstr "移除应用标签"
 
@@ -886,39 +898,31 @@ msgstr "从应用移除网关"
 msgid "Remove service from an application"
 msgstr "从应用移除服务"
 
-#: internal/parser/keys.go:81
+#: internal/parser/keys.go:82
 msgid "Removes an SSH key for the logged in user"
 msgstr "移除登录用户的SSH密钥"
 
-#: internal/parser/git.go:51
+#: internal/parser/git.go:52
 msgid "Removes git remote of application from repository"
 msgstr "从代码库移除应用的Git远程仓库"
 
-#: internal/parser/pts.go:90
+#: internal/parser/pts.go:91
 msgid "Restart an application or process types"
 msgstr "重启应用或进程类型"
 
-#: internal/parser/pts.go:89
+#: internal/parser/pts.go:90
 msgid "Restart application process types"
 msgstr "重启应用进程类型"
 
-#: internal/parser/apps.go:173
+#: internal/parser/apps.go:142
 msgid "Retention time of records in seconds"
 msgstr "日志记录保留时间（秒）"
 
-#: internal/parser/apps.go:125
-msgid "Retrieve application log events"
-msgstr "获取应用日志事件"
-
-#: internal/parser/apps.go:126
-msgid "Retrieves the most recent log events"
-msgstr "获取最近日志事件"
-
-#: internal/parser/releases.go:134
+#: internal/parser/releases.go:133
 msgid "Return to a previous release"
 msgstr "回滚到之前版本"
 
-#: internal/parser/releases.go:135
+#: internal/parser/releases.go:134
 msgid "Rolls back to a previous application release"
 msgstr "回滚到应用之前的版本"
 
@@ -926,40 +930,48 @@ msgstr "回滚到应用之前的版本"
 msgid "Rules-file is the file name of rule to apply"
 msgstr "要应用的规则文件名"
 
-#: internal/parser/apps.go:162
+#: internal/parser/apps.go:131
 msgid "Run a command in an ephemeral app container"
 msgstr "在临时应用容器中运行命令"
 
-#: internal/parser/apps.go:163
+#: internal/parser/apps.go:132
 msgid "Runs a command inside an ephemeral app container"
 msgstr "在临时应用容器内部运行命令"
 
-#: internal/parser/builds.go:108
+#: internal/parser/builds.go:116
 msgid "Save process info to the local"
 msgstr "将进程信息保存到本地"
 
-#: internal/parser/pts.go:123
+#: internal/parser/pts.go:124
 msgid "Scale process types of replicas"
 msgstr "缩放进程类型的副本数"
 
-#: internal/parser/pts.go:124
+#: internal/parser/pts.go:125
 msgid "Scales an application's processes by type"
 msgstr "按类型缩放应用进程"
 
-#: internal/parser/limits.go:125
+#: internal/parser/limits.go:126
 msgid ""
 "Search keywords separated by commas, matching must satisfy all of the "
 "specified."
 msgstr "以逗号分隔的搜索关键词，需满足所有指定条件"
 
-#: internal/parser/config.go:223
+#: 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:248
+#: 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 "设置应用各进程类型的自动扩缩容选项"
@@ -968,15 +980,19 @@ msgstr "设置应用各进程类型的自动扩缩容选项"
 msgid "Set environment variables for an app"
 msgstr "设置应用环境变量"
 
-#: internal/parser/healthchecks.go:92
+#: internal/parser/healthchecks.go:90
 msgid "Set healthchecks for an application"
 msgstr "设置应用健康检查配置"
 
-#: internal/parser/registry.go:75
+#: 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:69
+#: internal/parser/limits.go:70
 msgid "Set resource limits for an app"
 msgstr "设置应用资源配额"
 
@@ -1000,13 +1016,13 @@ msgstr "设置应用标签"
 msgid "Sets environment variables for an application or config group"
 msgstr "设置应用或配置组的环境变量"
 
-#: internal/parser/config.go:183
+#: 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."
+"stdin, 'drycc config push < .env', or using the --path option."
 msgstr ""
 "为应用程序或配置组设置环境变量。\n"
 "\n"
@@ -1014,7 +1030,7 @@ msgstr ""
 "以加载应用程序的本地环境。文件应通过\n"
 "标准输入（'drycc config push < .env'）或使用 --path 选项进行管道传输。"
 
-#: internal/parser/healthchecks.go:93
+#: internal/parser/healthchecks.go:91
 msgid ""
 "Sets healthchecks for an application.\n"
 "\n"
@@ -1066,57 +1082,40 @@ msgid ""
 "probes accept a\n"
 "port number to perform the socket connection on the Container.\n"
 msgstr ""
-"Sets healthchecks for an application.\n"
+"设置应用健康检查配置。\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"
+"默认情况下，Workflow 仅检查应用程序是否在其容器中启动。可以通过为应用程序配"
+"置\n"
+"健康检查探针来添加健康检查。健康检查通过 Kubernetes 容器探针实现。可以配置\n"
+"'startupProbe'、'livenessProbe' 和 'readinessProbe'，每种探针可以是 "
+"'httpGet'、\n"
+"'exec' 或 'tcpSocket' 类型，具体取决于容器所需的探针类型。\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"
+"'startupProbe' 指示容器内的应用程序是否已启动。如果提供了启动探针，则所有其"
+"他\n"
+"探针将被禁用，直到启动探针成功为止。如果启动探针失败，容器将根据其重启策略\n"
+"进行处理。\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"
+"'livenessProbe' 对于长时间运行的应用程序很有用，这些应用程序最终会进入故障状"
+"态，\n"
+"除非重启否则无法恢复。\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"
+"其他时候，当容器只是暂时无法服务并会自行恢复时，'readinessProbe' 很有用。\n"
+"在这种情况下，如果容器未能通过其 'readinessProbe'，容器不会被关闭，而是会\n"
+"停止接收传入请求。\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"
+"'httpGet' 探针如其名：它在容器上执行 HTTP GET 操作。200-399 范围内的响应代"
+"码\n"
+"被视为通过。'httpGet' 探针接受一个端口号以在容器上执行 HTTP GET 操作。\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"
+"'exec' 探针在容器内运行命令以确定其健康状况。退出代码为零被视为通过，而非零\n"
+"状态代码被视为失败。'exec' 探针接受要在容器内运行的参数字符串。\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"
+"'tcpSocket' 探针尝试在容器中打开套接字。仅当检查可以建立连接时，容器才被视"
+"为\n"
+"健康。'tcpSocket' 探针接受一个端口号以在容器上执行套接字连接。\n"
 
-#: internal/parser/labels.go:60
+#: internal/parser/labels.go:61
 msgid ""
 "Sets labels for an application.\n"
 "\n"
@@ -1130,17 +1129,86 @@ msgstr ""
 "标签是键值对形式的元数据，用于标识应用。这些标签属于Drycc用户通用信息，主要用"
 "于管理/维护备注，不会发送给调度器。"
 
-#: internal/parser/registry.go:76
+#: internal/parser/lifecycles.go:85
 msgid ""
-"Sets registry information for an application. These credentials are the same "
-"as those used for\n"
-"'podmain login' to the private registry."
+"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:70
+#: internal/parser/limits.go:71
 msgid ""
 "Sets resource limits for an application.\n"
 "\n"
@@ -1170,23 +1238,23 @@ msgstr ""
 msgid "Sets termination grace period for an application"
 msgstr "设置应用终止宽限期"
 
-#: cmd/root.go:69
+#: cmd/root.go:75
 msgid "Shortcut Commands:"
-msgstr "Shortcut Commands:"
+msgstr "快捷命令："
 
 #: internal/parser/certs.go:41
 msgid "Show certificate information for an SSL application"
 msgstr "显示SSL应用证书信息"
 
-#: internal/parser/resources.go:155
+#: internal/parser/resources.go:156
 msgid "Show extra details provided to resource"
 msgstr "显示资源附加详情"
 
-#: internal/parser/update.go:20
+#: internal/parser/update.go:21
 msgid "Simulate an update, only print the version info."
 msgstr "模拟更新，仅显示版本信息"
 
-#: internal/parser/apps.go:197
+#: internal/parser/apps.go:166
 msgid ""
 "Skips the prompt for the application name. \n"
 "<app> is the uniquely identifiable name for the application"
@@ -1194,11 +1262,11 @@ msgstr ""
 "跳过应用名称提示，直接使用指定名称。\n"
 "<app> 是应用的唯一标识名称"
 
-#: internal/parser/apps.go:134 internal/parser/ps.go:76
+#: internal/parser/ps.go:76
 msgid "Specify if the logs should be streamed"
 msgstr "指定是否实时流式输出日志"
 
-#: internal/parser/resources.go:111 internal/parser/resources.go:195
+#: 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文件指定参数值，若设置则忽略命令行参数"
 
@@ -1210,11 +1278,15 @@ msgstr "标准输入为TTY终端"
 msgid "Target CPU utilization percentage"
 msgstr "目标CPU使用率百分比"
 
-#: cmd/root.go:27
+#: cmd/root.go:29
 msgid "The Drycc command-line client issues API calls to a Drycc controller"
 msgstr "Drycc命令行客户端向Drycc控制器发出API调用"
 
-#: internal/parser/healthchecks.go:185
+#: 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头部字段（逗号分隔）"
 
@@ -1222,7 +1294,7 @@ msgstr "HTTP探测时发送的HTTP头部字段（逗号分隔）"
 msgid "The IP protocol for this port. Supports TCP, UDP, and SCTP"
 msgstr "该端口的IP协议（支持TCP/UDP/SCTP）"
 
-#: internal/parser/healthchecks.go:88
+#: internal/parser/healthchecks.go:86
 msgid ""
 "The arguments required for the healthcheck probe. 'exec', accepts a list of "
 "arguments;\n"
@@ -1231,12 +1303,19 @@ msgstr ""
 "健康检查探测所需的参数：\n"
 "                  exec探测接受参数列表，httpGet和tcpSocket探测需指定端口号"
 
-#: internal/parser/volumes.go:201
-msgid "Serve a WebDAV service for the volume"
-msgstr "为卷提供WebDAV服务"
+#: 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/volumes.go:261 internal/parser/apps.go:158
-#: internal/parser/shortcuts.go:44
+#: internal/parser/apps.go:127 internal/parser/shortcuts.go:53
+#: internal/parser/volumes.go:235
 msgid "The filesystem path"
 msgstr "文件系统路径"
 
@@ -1248,7 +1327,7 @@ msgstr "要列出配置的组"
 msgid "The group for which the config needs to be pull"
 msgstr "要拉取配置的组"
 
-#: internal/parser/config.go:194
+#: internal/parser/config.go:195
 msgid "The group for which the config needs to be push"
 msgstr "要推送配置的组"
 
@@ -1260,21 +1339,22 @@ msgstr "要设置配置的组"
 msgid "The group for which the config needs to be unset"
 msgstr "要清除配置的组"
 
-#: internal/parser/config.go:218
+#: internal/parser/config.go:220
 msgid "The group that requires attach configurations"
 msgstr "需附加配置的组"
 
-#: internal/parser/config.go:245
+#: internal/parser/config.go:247
 msgid "The group that requires detach configurations"
 msgstr "需分离配置的组"
 
-#: internal/parser/healthchecks.go:86
+#: internal/parser/healthchecks.go:84
 msgid ""
-"The healthcheck type, such as 'startupProbe' 'livenessProbe' or "
+"The healthcheck probe, such as 'startupProbe' 'livenessProbe' or "
 "'readinessProbe'"
-msgstr "健康检查类型，如 'startupProbe'、'livenessProbe' 或 'readinessProbe'"
+msgstr ""
+"健康检查探测类型，如 'startupProbe'、'livenessProbe' 或 'readinessProbe'"
 
-#: internal/parser/volumes.go:85
+#: internal/parser/volumes.go:88
 msgid "The hostname or ip address of the nfs server"
 msgstr "NFS服务器的主机名或IP地址"
 
@@ -1282,67 +1362,63 @@ msgstr "NFS服务器的主机名或IP地址"
 msgid "The id of the token for controller authentication"
 msgstr "控制器认证的令牌ID"
 
-#: internal/parser/healthchecks.go:186
+#: internal/parser/healthchecks.go:185
 msgid "The initial delay timeout for the probe"
 msgstr "探测初始延迟超时时间"
 
-#: internal/parser/limits.go:66
+#: 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/apps.go:135
-msgid "The max seconds of followz the log stream"
-msgstr "日志流实时追踪的最大秒数"
-
-#: internal/parser/resources.go:24 internal/parser/resources.go:126
-#: internal/parser/certs.go:23 internal/parser/certs.go:48
-#: internal/parser/volumes.go:23 internal/parser/volumes.go:141
-#: internal/parser/routes.go:27 internal/parser/routes.go:107
-#: internal/parser/domains.go:22 internal/parser/domains.go:43
 #: internal/parser/users.go:21 internal/parser/users.go:39
 #: internal/parser/releases.go:33 internal/parser/releases.go:62
-#: internal/parser/tokens.go:22 internal/parser/tokens.go:40
+#: internal/parser/limits.go:127 internal/parser/limits.go:151
+#: internal/parser/certs.go:23 internal/parser/certs.go:48
+#: internal/parser/routes.go:28 internal/parser/routes.go:107
 #: internal/parser/gateways.go:22 internal/parser/gateways.go:44
-#: internal/parser/limits.go:126 internal/parser/limits.go:150
-#: internal/parser/perms.go:24 internal/parser/perms.go:46
+#: internal/parser/volumes.go:23 internal/parser/volumes.go:145
+#: internal/parser/perms.go:25 internal/parser/perms.go:47
+#: internal/parser/tokens.go:22 internal/parser/tokens.go:40
+#: internal/parser/domains.go:22 internal/parser/domains.go:43
+#: internal/parser/resources.go:25 internal/parser/resources.go:127
 msgid "The maximum number of results to display"
 msgstr "最多显示结果数量"
 
-#: internal/parser/certs.go:107
+#: internal/parser/certs.go:106
 msgid "The name of the cert to get information from"
 msgstr "要查询信息的证书名称"
 
-#: internal/parser/certs.go:85
+#: internal/parser/certs.go:84
 msgid "The name of the cert to remove from the app"
 msgstr "要从应用移除的证书名称"
 
-#: internal/parser/perms.go:67 internal/parser/perms.go:97
-#: internal/parser/perms.go:125
+#: internal/parser/perms.go:68 internal/parser/perms.go:98
+#: internal/parser/perms.go:126
 msgid "The name of the user"
 msgstr "用户名称"
 
-#: internal/parser/apps.go:133
-msgid "The number of lines to display"
-msgstr "显示行数"
-
 #: internal/parser/ps.go:75
 msgid "The number of lines to display, -1 showing all log lines"
 msgstr "要显示的日志行数（-1 表示显示全部）"
 
-#: internal/parser/shortcuts.go:83 internal/parser/pts.go:120
+#: internal/parser/shortcuts.go:95 internal/parser/pts.go:121
 msgid "The number of processes"
 msgstr "进程数量"
 
-#: internal/parser/healthchecks.go:187
+#: internal/parser/healthchecks.go:186
 msgid "The number of seconds after which the probe times out"
 msgstr "探测超时秒数"
 
-#: internal/parser/registry.go:72
+#: internal/parser/registry.go:73
 msgid "The password of the registry"
 msgstr "镜像仓库密码"
 
-#: internal/parser/shortcuts.go:58 internal/parser/ps.go:99
-#: internal/parser/ps.go:130 internal/parser/ps.go:152
+#: 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名称"
 
@@ -1358,15 +1434,15 @@ msgstr "服务将暴露的端口"
 msgid "The private key of the SSL certificate"
 msgstr "SSL证书私钥"
 
-#: internal/parser/volumes.go:260 internal/parser/volumes.go:285
-#: internal/parser/shortcuts.go:82 internal/parser/pts.go:61
-#: internal/parser/pts.go:119 internal/parser/pts.go:153
+#: internal/parser/shortcuts.go:94 internal/parser/volumes.go:234
+#: internal/parser/volumes.go:259 internal/parser/pts.go:62
+#: internal/parser/pts.go:120 internal/parser/pts.go:154
 #: internal/parser/tags.go:26 internal/parser/tags.go:60
 #: internal/parser/tags.go:100 internal/parser/tags.go:131
 msgid "The process name as defined in your Procfile"
 msgstr "Procfile中定义的进程名称"
 
-#: internal/parser/releases.go:130
+#: internal/parser/releases.go:129
 msgid "The process name as defined in your Procfile, such as 'web'"
 msgstr "Procfile中定义的进程名称（如 'web'）"
 
@@ -1374,7 +1450,7 @@ msgstr "Procfile中定义的进程名称（如 'web'）"
 msgid "The process type as defined in your Procfile"
 msgstr "Procfile中定义的进程类型"
 
-#: internal/parser/limits.go:65 internal/parser/limits.go:95
+#: 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'）"
 
@@ -1390,8 +1466,8 @@ msgstr "要从应用自动扩缩容设置移除的进程类型"
 msgid "The ptype for domain"
 msgstr "域名对应的配置进程类型"
 
-#: internal/parser/registry.go:26 internal/parser/registry.go:50
-#: internal/parser/registry.go:85 internal/parser/registry.go:107
+#: 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 "镜像仓库对应的配置进程类型"
 
@@ -1403,7 +1479,7 @@ msgstr "要列出配置的进程类型"
 msgid "The ptype for which the config needs to be pull"
 msgstr "要拉取配置的进程类型"
 
-#: internal/parser/config.go:193
+#: internal/parser/config.go:194
 msgid "The ptype for which the config needs to be push"
 msgstr "要推送配置的进程类型"
 
@@ -1415,23 +1491,35 @@ msgstr "要设置配置的进程类型"
 msgid "The ptype for which the config needs to be unset"
 msgstr "要清除配置的进程类型"
 
-#: internal/parser/healthchecks.go:180
+#: internal/parser/healthchecks.go:178
 msgid "The ptype for which the health check needs to be applied"
 msgstr "要应用健康检查的进程类型"
 
-#: internal/parser/healthchecks.go:30
+#: internal/parser/healthchecks.go:31
 msgid "The ptype for which the health check needs to be listed"
 msgstr "要列出健康检查的进程类型"
 
-#: internal/parser/healthchecks.go:225
+#: internal/parser/healthchecks.go:224
 msgid "The ptype for which the health check needs to be removed"
 msgstr "要移除健康检查的进程类型"
 
-#: internal/parser/config.go:217
+#: 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:244
+#: internal/parser/config.go:246
 msgid "The ptype that requires detach configurations"
 msgstr "需分离配置的进程类型"
 
@@ -1439,28 +1527,32 @@ msgstr "需分离配置的进程类型"
 msgid "The public key of the SSL certificate"
 msgstr "SSL证书公钥"
 
-#: internal/parser/healthchecks.go:184
+#: 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:77 internal/parser/releases.go:131
+#: internal/parser/releases.go:76 internal/parser/releases.go:130
 msgid "The release of the application, such as '1'"
 msgstr "应用版本（如 '1'）"
 
-#: internal/parser/resources.go:213 internal/parser/resources.go:238
-#: internal/parser/resources.go:260
+#: 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:93 internal/parser/resources.go:176
+#: internal/parser/resources.go:94 internal/parser/resources.go:177
 msgid "The resource instance parameters key"
 msgstr "资源实例参数键"
 
-#: internal/parser/resources.go:94 internal/parser/resources.go:177
+#: internal/parser/resources.go:95 internal/parser/resources.go:178
 msgid "The resource instance parameters value"
 msgstr "资源实例参数值"
 
-#: internal/parser/resources.go:91 internal/parser/resources.go:174
+#: internal/parser/resources.go:92 internal/parser/resources.go:175
 msgid "The resource's service"
 msgstr "资源所属服务"
 
@@ -1472,22 +1564,26 @@ msgstr "路由类型（可选：HTTPRoute、TCPRoute、UDPRoute、GRPCRoute、TL
 msgid "The route's backend, pattern: <service>:<port>,<weight>"
 msgstr "路由后端配置，格式：<service>:<port>,<weight>"
 
-#: internal/parser/resources.go:62
+#: internal/parser/resources.go:63
 msgid "The service name for plans"
 msgstr "服务名称"
 
-#: internal/parser/resources.go:92 internal/parser/resources.go:175
+#: internal/parser/resources.go:93 internal/parser/resources.go:176
 msgid "The service's plan"
 msgstr "服务配额方案"
 
-#: internal/parser/apps.go:159 internal/parser/shortcuts.go:45
+#: internal/parser/apps.go:128 internal/parser/shortcuts.go:54
 msgid "The shell command to run inside the container"
 msgstr "容器内要执行的Shell命令"
 
-#: internal/parser/builds.go:57
+#: 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 "标签键"
@@ -1506,42 +1602,42 @@ 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:245
+#: internal/parser/routes.go:212 internal/parser/routes.go:244
 msgid "The unique name of the route"
 msgstr "路由唯一名称"
 
-#: internal/parser/resources.go:23 internal/parser/certs.go:22
-#: internal/parser/autodeploy.go:21 internal/parser/volumes.go:22
-#: internal/parser/routes.go:26 internal/parser/services.go:23
-#: internal/parser/autoscale.go:21 internal/parser/apps.go:88
-#: internal/parser/apps.go:107 internal/parser/apps.go:132
-#: internal/parser/apps.go:170 internal/parser/apps.go:196
-#: internal/parser/apps.go:217 internal/parser/domains.go:21
-#: internal/parser/releases.go:32 internal/parser/registry.go:25
-#: internal/parser/labels.go:20 internal/parser/routing.go:19
-#: internal/parser/gateways.go:21 internal/parser/builds.go:19
-#: internal/parser/builds.go:56 internal/parser/autorollback.go:19
-#: internal/parser/limits.go:20 internal/parser/git.go:17
-#: internal/parser/timeouts.go:21 internal/parser/config.go:29
-#: internal/parser/pts.go:20 internal/parser/pts.go:133
-#: internal/parser/ps.go:20 internal/parser/ps.go:112
-#: internal/parser/tags.go:25 internal/parser/perms.go:23
+#: internal/parser/builds.go:21 internal/parser/builds.go:64
+#: internal/parser/apps.go:86 internal/parser/apps.go:104
+#: internal/parser/apps.go:139 internal/parser/apps.go:165
+#: internal/parser/apps.go:186 internal/parser/autorollback.go:19
+#: internal/parser/timeouts.go:21 internal/parser/releases.go:32
+#: internal/parser/limits.go:21 internal/parser/autoscale.go:21
+#: internal/parser/ps.go:21 internal/parser/ps.go:112
+#: internal/parser/services.go:23 internal/parser/certs.go:22
+#: internal/parser/config.go:29 internal/parser/routes.go:27
+#: internal/parser/routing.go:20 internal/parser/labels.go:21
+#: internal/parser/gateways.go:21 internal/parser/volumes.go:22
+#: internal/parser/autodeploy.go:20 internal/parser/git.go:18
+#: internal/parser/perms.go:24 internal/parser/pts.go:21
+#: internal/parser/pts.go:134 internal/parser/tags.go:25
+#: internal/parser/registry.go:26 internal/parser/domains.go:21
+#: internal/parser/resources.go:24
 msgid "The uniquely identifiable name for the application"
 msgstr "应用唯一标识名称"
 
-#: internal/parser/healthchecks.go:29
+#: internal/parser/healthchecks.go:30 internal/parser/lifecycles.go:30
 msgid "The uniquely identifiable name of the application"
 msgstr "应用唯一名称"
 
-#: internal/parser/perms.go:68
+#: internal/parser/perms.go:69
 msgid "The user permissions (view,change,delete)"
 msgstr "用户权限（view、change、delete）"
 
-#: internal/parser/perms.go:98
+#: internal/parser/perms.go:99
 msgid "The user's permissions (view,change,delete)"
 msgstr "用户的权限（view、change、delete）"
 
-#: internal/parser/registry.go:71
+#: internal/parser/registry.go:72
 msgid "The username of the registry"
 msgstr "镜像仓库用户名"
 
@@ -1557,8 +1653,8 @@ msgstr "要启用的用户名"
 msgid "The value to apply to the process type in seconds"
 msgstr "要应用到进程类型的秒数值"
 
-#: internal/parser/builds.go:20 internal/parser/builds.go:79
-#: internal/parser/builds.go:104
+#: 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 "要查询对应版本的构建信息"
 
@@ -1570,15 +1666,19 @@ msgstr "要查询对应版本的配置"
 msgid "The version for which the config needs to be pull"
 msgstr "要拉取对应版本的配置"
 
-#: internal/parser/healthchecks.go:31 internal/parser/healthchecks.go:56
+#: 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/limits.go:21 internal/parser/limits.go:46
+#: 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:27 internal/parser/registry.go:51
+#: internal/parser/registry.go:28 internal/parser/registry.go:52
 msgid "The version for which the registry needs to be listed"
 msgstr "要查询对应版本的镜像仓库信息"
 
@@ -1590,49 +1690,50 @@ msgstr "要查询对应版本的标签"
 msgid "The version for which the timeout needs to be listed"
 msgstr "要查询对应版本的超时设置"
 
-#: internal/parser/volumes.go:57 internal/parser/volumes.go:114
-#: internal/parser/volumes.go:259 internal/parser/volumes.go:284
-#: internal/parser/apps.go:157 internal/parser/shortcuts.go:43
+#: internal/parser/apps.go:126 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
 msgid "The volume name"
 msgstr "卷名称"
 
-#: internal/parser/volumes.go:157
+#: internal/parser/volumes.go:161
 msgid "The volume name to be info"
 msgstr "要查询信息的卷名称"
 
-#: internal/parser/volumes.go:183
+#: internal/parser/volumes.go:187
 msgid "The volume name to be removed"
 msgstr "要移除的卷名称"
 
-#: internal/parser/volumes.go:58 internal/parser/volumes.go:115
+#: internal/parser/volumes.go:59 internal/parser/volumes.go:119
 msgid "The volume size, such as '500G'"
 msgstr "卷容量（如 '500G'）"
 
-#: internal/parser/volumes.go:84
+#: internal/parser/volumes.go:87
 msgid "The volume type, such as csi, nfs, oss"
 msgstr "卷类型（如 csi, nfs, oss）"
 
-#: internal/parser/resources.go:90 internal/parser/resources.go:144
-#: internal/parser/resources.go:173
+#: internal/parser/resources.go:91 internal/parser/resources.go:145
+#: internal/parser/resources.go:174
 msgid "This resource instance alias"
 msgstr "该资源实例别名"
 
-#: internal/parser/releases.go:118 internal/parser/tokens.go:103
-#: internal/parser/builds.go:60 internal/parser/builds.go:107
-#: internal/parser/pts.go:98
+#: internal/parser/builds.go:68 internal/parser/builds.go:115
+#: internal/parser/releases.go:117 internal/parser/tokens.go:103
+#: internal/parser/pts.go:99
 msgid "To proceed, type \"yes\""
 msgstr "继续操作请输入 \"yes\""
 
 #: internal/parser/config.go:94 internal/parser/config.go:123
-#: internal/parser/config.go:196
+#: internal/parser/config.go:198
 msgid "To proceed, type 'yes'"
 msgstr "继续操作请输入 'yes'"
 
-#: internal/parser/apps.go:209
+#: internal/parser/apps.go:178
 msgid "Transfer app ownership to another user"
 msgstr "转移应用所有权给其他用户"
 
-#: internal/parser/apps.go:210
+#: internal/parser/apps.go:179
 msgid "Transfer application ownership to another user."
 msgstr "转移应用所有权给其他用户"
 
@@ -1648,7 +1749,7 @@ msgstr "开启应用自动扩缩容"
 msgid "Unbinds a domain for an application"
 msgstr "解除应用与域名的绑定"
 
-#: internal/parser/volumes.go:288
+#: internal/parser/volumes.go:262
 msgid "Unmount a volume from process of the application"
 msgstr "从应用进程卸载卷"
 
@@ -1660,15 +1761,19 @@ msgstr "清除应用各进程类型的自动扩缩容设置"
 msgid "Unset environment variables for an app"
 msgstr "清除应用环境变量"
 
-#: internal/parser/healthchecks.go:213
+#: internal/parser/healthchecks.go:212
 msgid "Unset healthchecks for an application"
 msgstr "清除应用健康检查配置"
 
-#: internal/parser/registry.go:100
+#: 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:98
+#: internal/parser/limits.go:99
 msgid "Unset resource limits for an app"
 msgstr "清除应用资源配额"
 
@@ -1684,11 +1789,11 @@ msgstr "清除应用标签"
 msgid "Unsets an environment variable for an application or config group"
 msgstr "清除应用或配置组的环境变量"
 
-#: internal/parser/labels.go:90
+#: internal/parser/labels.go:91
 msgid "Unsets labels for an application"
 msgstr "清除应用标签"
 
-#: internal/parser/registry.go:101
+#: internal/parser/registry.go:102
 msgid "Unsets registry information for an application"
 msgstr "清除应用镜像仓库信息"
 
@@ -1698,15 +1803,15 @@ msgid ""
 "controller"
 msgstr "清除应用超时设置（将恢复默认值30秒或控制器设定值）"
 
-#: internal/parser/resources.go:180
+#: internal/parser/resources.go:181
 msgid "Update a resource from the application"
 msgstr "更新应用资源"
 
-#: internal/parser/perms.go:101
+#: internal/parser/perms.go:102
 msgid "Update a user permission"
 msgstr "更新用户权限"
 
-#: internal/parser/update.go:14 internal/parser/update.go:15
+#: internal/parser/update.go:15 internal/parser/update.go:16
 msgid "Update workflow cli to latest release"
 msgstr "更新Workflow CLI到最新版本"
 
@@ -1714,7 +1819,7 @@ msgstr "更新Workflow CLI到最新版本"
 msgid "View TLS settings information"
 msgstr "查看TLS设置信息"
 
-#: internal/parser/routing.go:33
+#: internal/parser/routing.go:34
 msgid "View application's routability information"
 msgstr "查看应用路由能力信息"
 
@@ -1722,19 +1827,19 @@ msgstr "查看应用路由能力信息"
 msgid "View autorollback info for an application"
 msgstr "查看应用自动回滚信息"
 
-#: internal/parser/apps.go:81
+#: internal/parser/apps.go:79
 msgid "View info about an application"
 msgstr "查看应用信息"
 
-#: internal/parser/apps.go:171
+#: internal/parser/apps.go:140
 msgid "Volume mounts in format 'volume:path'"
 msgstr "卷挂载格式 '卷名:挂载路径'"
 
-#: internal/parser/builds.go:42
+#: internal/parser/builds.go:49
 msgid "default container image"
 msgstr "默认的容器镜像"
 
-#: internal/parser/builds.go:44
+#: internal/parser/builds.go:52
 msgid "imports an image and deploys as a new release"
 msgstr "导入镜像并部署为新版本"
 
@@ -1759,45 +1864,46 @@ msgstr ""
 "要处理请求的Procfile类型（如webhooks），需绑定到PORT端口\n"
 "             每个Procfile类型只能创建一个附加服务"
 
-#: internal/parser/volumes.go:95
-msgid "Secret access key for authentication"
-msgstr "认证密钥（Secret Access Key）"
-
-#: internal/parser/volumes.go:97
-msgid "Force a path-style endpoint to be used"
-msgstr "强制使用路径风格的端点"
-
-#: internal/parser/resources.go:224
+#: 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:87
+#: internal/parser/healthchecks.go:85
 msgid "the healthcheck probe type, such as 'httpGet', 'exec' or 'tcpSocket'"
 msgstr "健康检查探测类型（如 'httpGet'、'exec' 或 'tcpSocket'）"
 
-#: internal/parser/healthchecks.go:210
+#: internal/parser/healthchecks.go:209
 msgid ""
-"the healthcheck type, such as 'startupProbe' 'livenessProbe' or "
+"the healthcheck probe, such as 'startupProbe' 'livenessProbe' or "
 "'readinessProbe'"
-msgstr "健康检查类型，如 'startupProbe'、'livenessProbe' 或 'readinessProbe'"
+msgstr "健康检查探测，如 'startupProbe'、'livenessProbe' 或 'readinessProbe'"
 
-#: internal/parser/labels.go:84
+#: 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:55
+#: internal/parser/labels.go:56
 msgid "the label key, for example: \"git_repo\" or \"team\""
 msgstr "标签键（例如：\"git_repo\" 或 \"team\"）"
 
-#: internal/parser/labels.go:56
+#: 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/resources.go:263
+#: 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 "解除应用与资源的绑定"

From d59654276a6c69507b1823cb2cc86a197723ac30 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Thu, 11 Dec 2025 11:15:46 +0800
Subject: [PATCH 16/18] fix(workflow-cli): args validation errors for
 lifecycles and healthchecks

---
 internal/parser/healthchecks.go | 2 +-
 internal/parser/lifecycles.go   | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/internal/parser/healthchecks.go b/internal/parser/healthchecks.go
index 7510d937c..a73ee42e2 100644
--- a/internal/parser/healthchecks.go
+++ b/internal/parser/healthchecks.go
@@ -77,7 +77,7 @@ func healthchecksSet(cmdr *commands.DryccCmd) *cobra.Command {
 	healthCheckCompletion := completion.HealthCheckCompletion{ConfigFile: &cmdr.ConfigFile}
 	cmd := &cobra.Command{
 		Use:  "set <probe> <action> [flags] [--] <args>...",
-		Args: cobra.MinimumNArgs(2),
+		Args: cobra.MinimumNArgs(3),
 		Example: template.CustomExample(
 			"drycc healthchecks set readinessProbe httpGet --path=/health -- 8000",
 			map[string]string{
diff --git a/internal/parser/lifecycles.go b/internal/parser/lifecycles.go
index 46cff7a33..2e1c4a612 100644
--- a/internal/parser/lifecycles.go
+++ b/internal/parser/lifecycles.go
@@ -71,7 +71,7 @@ func lifecyclesSet(cmdr *commands.DryccCmd) *cobra.Command {
 	lifecycleCompletion := completion.LifecycleCompletion{ConfigFile: &cmdr.ConfigFile}
 	cmd := &cobra.Command{
 		Use:  "set <handler> <action> [flags] [--] <args>...",
-		Args: cobra.MinimumNArgs(2),
+		Args: cobra.MinimumNArgs(3),
 		Example: template.CustomExample(
 			"drycc lifecycles set postStart httpGet --path=/health -- 8000",
 			map[string]string{

From 7a1e9126e52ec62d7ce76cad49a82172aa05de52 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Thu, 11 Dec 2025 13:00:04 +0800
Subject: [PATCH 17/18] fix(lifecycle): invalid memory address or nil pointer
 dereference

---
 go.mod                          |  2 +-
 go.sum                          |  4 ++--
 internal/commands/lifecycles.go | 12 +++++++++---
 3 files changed, 12 insertions(+), 6 deletions(-)

diff --git a/go.mod b/go.mod
index 1fa25e09f..4e87cf4ab 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.25
 require (
 	github.com/chai2010/gettext-go v1.0.3
 	github.com/containerd/console v1.0.4
-	github.com/drycc/controller-sdk-go v0.0.0-20251206164108-e9369b636dd8
+	github.com/drycc/controller-sdk-go v0.0.0-20251211045545-b196e6964a1c
 	github.com/drycc/pkg v0.0.0-20250917064731-345368da3dbf
 	github.com/minio/selfupdate v0.6.0
 	github.com/olekukonko/tablewriter v0.0.5
diff --git a/go.sum b/go.sum
index db07c1bf1..73743e0b8 100644
--- a/go.sum
+++ b/go.sum
@@ -9,8 +9,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
 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-20251206164108-e9369b636dd8 h1:EgzCbOPGeDmeClpCjPPwuCJ8viMWEMn7FiUyY1mJvoM=
-github.com/drycc/controller-sdk-go v0.0.0-20251206164108-e9369b636dd8/go.mod h1:eHcmYwg81ASlP55/U587xnBZnZoeZnPHXGeQ8nYWnsg=
+github.com/drycc/controller-sdk-go v0.0.0-20251211045545-b196e6964a1c h1:v7dQdercvt0o+/dOeCpQSEYedvo2lTY8eCwHBxx48hg=
+github.com/drycc/controller-sdk-go v0.0.0-20251211045545-b196e6964a1c/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=
diff --git a/internal/commands/lifecycles.go b/internal/commands/lifecycles.go
index 0f8f66ea6..fe860a0dd 100644
--- a/internal/commands/lifecycles.go
+++ b/internal/commands/lifecycles.go
@@ -63,9 +63,15 @@ func (d *DryccCmd) LifecyclesList(appID, ptype string, version int) error {
 	table.Append([]string{"Lifecycle:"})
 	for _, ptype := range sortPtypes(ptypes) {
 		if lifecycle, ok := config.Lifecycle[ptype]; ok {
-			table.Append([]string{"", fmt.Sprintf("stopSignal=%s", lifecycle.StopSignal)})
-			table.Append([]string{"", getLifecycleHandlerString(ptype, "postStart", lifecycle.StopSignal, *lifecycle.PostStart)})
-			table.Append([]string{"", getLifecycleHandlerString(ptype, "preStop", lifecycle.StopSignal, *lifecycle.PreStop)})
+			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()

From 70120b173896a164c5a0ca6c04564407fa52cb31 Mon Sep 17 00:00:00 2001
From: duanhongyi <duanhongyi@doopai.com>
Date: Thu, 11 Dec 2025 13:54:47 +0800
Subject: [PATCH 18/18] feat(perms): migrate owner-based to workspace

---
 cmd/root.go                                   |   3 +-
 go.mod                                        |   2 +-
 go.sum                                        |   4 +-
 internal/commands/apps.go                     |  18 +-
 internal/commands/apps_test.go                |  22 +-
 internal/commands/builds.go                   |   1 -
 internal/commands/builds_test.go              |   1 -
 internal/commands/certs.go                    |   1 -
 internal/commands/certs_test.go               |   2 -
 internal/commands/commands.go                 |  16 +-
 internal/commands/domains.go                  |   3 +-
 internal/commands/domains_test.go             |  10 +-
 internal/commands/healthchecks.go             |   4 +-
 internal/commands/healthchecks_test.go        |   3 -
 internal/commands/keys.go                     |   3 +-
 internal/commands/keys_test.go                |  10 +-
 internal/commands/labels.go                   |   3 +-
 internal/commands/labels_test.go              |   6 +-
 internal/commands/lifecycles.go               |   3 +-
 internal/commands/lifecycles_test.go          |   3 -
 internal/commands/perms.go                    |  85 ------
 internal/commands/perms_test.go               | 176 -----------
 internal/commands/releases.go                 |   5 +-
 internal/commands/releases_test.go            |  11 +-
 internal/commands/resources.go                |   4 +-
 internal/commands/resources_test.go           |   5 +-
 internal/commands/routes.go                   |   4 +-
 internal/commands/routes_test.go              |   4 +-
 internal/commands/tls.go                      |   1 -
 internal/commands/tls_test.go                 |   1 -
 internal/commands/users.go                    |  73 -----
 internal/commands/users_test.go               | 157 ----------
 internal/commands/utils.go                    |  14 -
 internal/commands/volumes.go                  |  10 +-
 internal/commands/volumes_test.go             |   7 +-
 internal/commands/workspaces.go               | 202 +++++++++++++
 internal/completion/completion.go             | 134 +++++----
 internal/parser/apps.go                       |  26 +-
 internal/parser/healthchecks.go               |   1 +
 internal/parser/lifecycles.go                 |   3 +-
 internal/parser/perms.go                      | 138 ---------
 internal/parser/pts.go                        |   6 +-
 internal/parser/releases.go                   |  12 +-
 internal/parser/users.go                      |  91 ------
 internal/parser/workspaces.go                 | 204 +++++++++++++
 .../drycc/en_US/LC_MESSAGES/cli.mo            | Bin 8219 -> 9931 bytes
 .../drycc/en_US/LC_MESSAGES/cli.po            | 266 ++++++++---------
 pkg/i18n/translations/drycc/template.pot      | 254 ++++++++--------
 .../drycc/zh_CN/LC_MESSAGES/cli.mo            | Bin 46795 -> 46844 bytes
 .../drycc/zh_CN/LC_MESSAGES/cli.po            | 278 +++++++++---------
 50 files changed, 986 insertions(+), 1304 deletions(-)
 delete mode 100644 internal/commands/perms.go
 delete mode 100644 internal/commands/perms_test.go
 delete mode 100644 internal/commands/users.go
 delete mode 100644 internal/commands/users_test.go
 create mode 100644 internal/commands/workspaces.go
 delete mode 100644 internal/parser/perms.go
 delete mode 100644 internal/parser/users.go
 create mode 100644 internal/parser/workspaces.go

diff --git a/cmd/root.go b/cmd/root.go
index 7ff6bb002..e5b2201f2 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -54,7 +54,7 @@ func NewDryccCommand() *cobra.Command {
 	rootCmd.AddCommand(parser.NewKeysCommand(&cmdr))
 	rootCmd.AddCommand(parser.NewLabelsCommand(&cmdr))
 	rootCmd.AddCommand(parser.NewLimitsCommand(&cmdr))
-	rootCmd.AddCommand(parser.NewPermsCommand(&cmdr))
+	rootCmd.AddCommand(parser.NewWorkspacesCommand(&cmdr))
 	rootCmd.AddCommand(parser.NewPsCommand(&cmdr))
 	rootCmd.AddCommand(parser.NewPtsCommand(&cmdr))
 	rootCmd.AddCommand(parser.NewRegistryCommand(&cmdr))
@@ -68,7 +68,6 @@ func NewDryccCommand() *cobra.Command {
 	rootCmd.AddCommand(parser.NewTLSCommand(&cmdr))
 	rootCmd.AddCommand(parser.NewTokensCommand(&cmdr))
 	rootCmd.AddCommand(parser.NewUpdateCommand(&cmdr))
-	rootCmd.AddCommand(parser.NewUsersCommand(&cmdr))
 	rootCmd.AddCommand(parser.NewVolumesCommand(&cmdr))
 	rootCmd.AddCommand(parser.NewVersionCommand(&cmdr))
 	// shortcuts
diff --git a/go.mod b/go.mod
index 4e87cf4ab..e6e4710df 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.25
 require (
 	github.com/chai2010/gettext-go v1.0.3
 	github.com/containerd/console v1.0.4
-	github.com/drycc/controller-sdk-go v0.0.0-20251211045545-b196e6964a1c
+	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
diff --git a/go.sum b/go.sum
index 73743e0b8..5b91e5cea 100644
--- a/go.sum
+++ b/go.sum
@@ -9,8 +9,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
 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-20251211045545-b196e6964a1c h1:v7dQdercvt0o+/dOeCpQSEYedvo2lTY8eCwHBxx48hg=
-github.com/drycc/controller-sdk-go v0.0.0-20251211045545-b196e6964a1c/go.mod h1:eHcmYwg81ASlP55/U587xnBZnZoeZnPHXGeQ8nYWnsg=
+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=
diff --git a/internal/commands/apps.go b/internal/commands/apps.go
index 393b07f81..3c828870c 100644
--- a/internal/commands/apps.go
+++ b/internal/commands/apps.go
@@ -18,7 +18,7 @@ import (
 )
 
 // 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
@@ -26,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
@@ -73,11 +73,11 @@ func (d *DryccCmd) AppsList(results int) error {
 		return err
 	}
 	if count > 0 {
-		table := d.getDefaultFormatTable([]string{"ID", "OWNER", "CREATED", "UPDATED"})
+		table := d.getDefaultFormatTable([]string{"ID", "WORKSPACE", "CREATED", "UPDATED"})
 		for _, app := range apps {
 			table.Append([]string{
 				app.ID,
-				app.Owner,
+				app.Workspace,
 				d.formatTime(app.Created),
 				d.formatTime(app.Updated),
 			})
@@ -110,7 +110,7 @@ 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{"Workspace:", app.Workspace})
 	table.Append([]string{"Created:", d.formatTime(app.Created)})
 	table.Append([]string{"Updated:", d.formatTime(app.Updated)})
 
@@ -279,16 +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 {
+// 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/internal/commands/apps_test.go b/internal/commands/apps_test.go
index 7077be753..ab5e430fd 100644
--- a/internal/commands/apps_test.go
+++ b/internal/commands/apps_test.go
@@ -38,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": {
@@ -48,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": {
@@ -61,7 +61,7 @@ func TestAppsList(t *testing.T) {
 
 	err = cmdr.AppsList(-1)
 	assert.NoError(t, err)
-	testutil.AssertOutput(t, b.String(), `ID             OWNER             CREATED                 UPDATED
+	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
 `)
@@ -87,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": {
@@ -100,7 +100,7 @@ func TestAppsListLimit(t *testing.T) {
 
 	err = cmdr.AppsList(1)
 	assert.NoError(t, err)
-	testutil.AssertOutput(t, b.String(), `ID             OWNER             CREATED                 UPDATED
+	testutil.AssertOutput(t, b.String(), `ID             WORKSPACE         CREATED                 UPDATED
 lorem-ipsum    dolar-sit-amet    2016-08-22T17:40:16Z    2016-08-22T17:40:16Z
 `)
 }
@@ -120,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
     },
@@ -189,7 +189,7 @@ func TestAppsInfo(t *testing.T) {
 	testutil.AssertOutput(t, b.String(), `App:          lorem-ipsum
 URL:          `+url+`
 UUID:         c4aed81c-d1ca-4ff1-ab89-d2151264e1a3
-Owner:        dolar-sit-amet
+Workspace:    dolar-sit-amet
 Created:      2016-08-22T17:40:16Z
 Updated:      2016-08-22T17:40:16Z
 Processes:
@@ -223,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
     },
@@ -256,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
     },
@@ -324,11 +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)
 
 	// 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", 
+	assert.Contains(t, err.Error(), "drycc",
 		"error message should contain the remote name 'drycc', got: %s", err.Error())
 }
diff --git a/internal/commands/builds.go b/internal/commands/builds.go
index 57da91b5c..74ce5d12b 100644
--- a/internal/commands/builds.go
+++ b/internal/commands/builds.go
@@ -27,7 +27,6 @@ func (d *DryccCmd) BuildsInfo(appID string, version int) error {
 	table.Append([]string{"App:", build.App})
 	table.Append([]string{"Sha:", build.Sha})
 	table.Append([]string{"UUID:", build.UUID})
-	table.Append([]string{"Owner:", build.Owner})
 	table.Append([]string{"Image:", build.Image})
 	table.Append([]string{"Stack:", build.Stack})
 	table.Append([]string{"Created:", build.Created})
diff --git a/internal/commands/builds_test.go b/internal/commands/builds_test.go
index d57b626b7..bd9eb384d 100644
--- a/internal/commands/builds_test.go
+++ b/internal/commands/builds_test.go
@@ -61,7 +61,6 @@ func TestBuildsInfo(t *testing.T) {
 	testutil.AssertOutput(t, b.String(), `App:
 Sha:
 UUID:       de1bf5b5-4a72-4f94-a10c-d2a3741cdf75
-Owner:
 Image:
 Stack:
 Created:    2014-01-01T00:00:00UTC
diff --git a/internal/commands/certs.go b/internal/commands/certs.go
index bf7b67c9f..13596fd79 100644
--- a/internal/commands/certs.go
+++ b/internal/commands/certs.go
@@ -132,7 +132,6 @@ func (d *DryccCmd) CertInfo(appID string, 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:", d.formatTime(cert.Created)})
 	table.Append([]string{"Updated:", d.formatTime(cert.Updated)})
 	table.Render()
diff --git a/internal/commands/certs_test.go b/internal/commands/certs_test.go
index f1c7fc98e..d41f28a38 100644
--- a/internal/commands/certs_test.go
+++ b/internal/commands/certs_test.go
@@ -200,7 +200,6 @@ 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")
@@ -225,7 +224,6 @@ Issuer:               <none>
 Subject:              <none>            
                       
 Connected Domains:    <none>            
-Owner:                <none>            
 Created:                                
 Updated:                                
 `, "output")
diff --git a/internal/commands/commands.go b/internal/commands/commands.go
index c28859867..c374948cf 100644
--- a/internal/commands/commands.go
+++ b/internal/commands/commands.go
@@ -13,7 +13,7 @@ import (
 //
 //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
@@ -87,10 +87,13 @@ type Commander interface {
 	TimeoutsList(string, int) error
 	TimeoutsSet(string, []string) error
 	TimeoutsUnset(string, []string) error
-	PermList(string, int) error
-	PermCreate(string, string, string) error
-	PermUpdate(string, string, string) error
-	PermDelete(string, string) 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, bool) error
 	PsExec(string, string, bool, bool, []string) error
@@ -122,9 +125,6 @@ type Commander interface {
 	TLSAutoDisable(string) error
 	TLSAutoIssuer(string, string, string, string, string) error
 	Update(bool) error
-	UsersList(int) error
-	UsersEnable(string) error
-	UsersDisable(string) error
 	Println(...any) (int, error)
 	Print(...any) (int, error)
 	Printf(string, ...any) (int, error)
diff --git a/internal/commands/domains.go b/internal/commands/domains.go
index 46c15d81f..66044981f 100644
--- a/internal/commands/domains.go
+++ b/internal/commands/domains.go
@@ -23,11 +23,10 @@ func (d *DryccCmd) DomainsList(appID string, results int) error {
 		return err
 	}
 	if count > 0 {
-		table := d.getDefaultFormatTable([]string{"APP", "OWNER", "PTYPE", "CREATED", "UPDATED", "DOMAIN"})
+		table := d.getDefaultFormatTable([]string{"APP", "PTYPE", "CREATED", "UPDATED", "DOMAIN"})
 		for _, domain := range domains {
 			table.Append([]string{
 				domain.App,
-				domain.Owner,
 				domain.Ptype,
 				d.formatTime(domain.Created),
 				d.formatTime(domain.Updated),
diff --git a/internal/commands/domains_test.go b/internal/commands/domains_test.go
index f9cc5c590..cdbdc337c 100644
--- a/internal/commands/domains_test.go
+++ b/internal/commands/domains_test.go
@@ -51,9 +51,9 @@ func TestDomainsList(t *testing.T) {
 	err = cmdr.DomainsList("foo", -1)
 	assert.NoError(t, err)
 
-	assert.Equal(t, b.String(), `APP    OWNER    PTYPE    CREATED                   UPDATED                   DOMAIN              
-foo    test     web      2014-01-01T00:00:00UTC    2014-01-01T00:00:00UTC    example.example.com    
-foo    test     web      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")
 }
 
@@ -89,8 +89,8 @@ func TestDomainsListLimit(t *testing.T) {
 	err = cmdr.DomainsList("foo", 1)
 	assert.NoError(t, err)
 
-	assert.Equal(t, b.String(), `APP    OWNER    PTYPE    CREATED                   UPDATED                   DOMAIN              
-foo    test     web      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")
 }
 
diff --git a/internal/commands/healthchecks.go b/internal/commands/healthchecks.go
index ad7b3badf..215196512 100644
--- a/internal/commands/healthchecks.go
+++ b/internal/commands/healthchecks.go
@@ -73,7 +73,6 @@ func (d *DryccCmd) HealthchecksList(appID, ptype string, version int) error {
 			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:", d.formatTime(config.Created)})
 			table.Append([]string{"Updated:", d.formatTime(config.Updated)})
 			table.Append([]string{"Healthchecks:"})
@@ -91,7 +90,6 @@ func (d *DryccCmd) HealthchecksList(appID, ptype string, version int) error {
 			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:", d.formatTime(config.Created)})
 			table.Append([]string{"Updated:", d.formatTime(config.Updated)})
 			table.Append([]string{"Healthchecks:"})
@@ -160,7 +158,7 @@ func (d *DryccCmd) HealthchecksUnset(appID, ptype string, containerProbeTypes []
 	configObj := api.Config{}
 
 	healthcheckMap := make(map[string]*api.Healthcheck)
-	var nullContainerProbe *api.ContainerProbe = nil
+	var nullContainerProbe *api.ContainerProbe
 	for _, containerProbeType := range containerProbeTypes {
 		healthcheck := &api.Healthcheck{}
 		switch containerProbeType {
diff --git a/internal/commands/healthchecks_test.go b/internal/commands/healthchecks_test.go
index 7399ead6e..1abbcd690 100644
--- a/internal/commands/healthchecks_test.go
+++ b/internal/commands/healthchecks_test.go
@@ -57,7 +57,6 @@ func TestHealthchecksList(t *testing.T) {
 
 	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:    
@@ -158,7 +157,6 @@ func TestHealthchecksListAllHealthChecks(t *testing.T) {
 
 	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:    
@@ -214,7 +212,6 @@ func TestHealthchecksSet(t *testing.T) {
 
 App:             foo                                                                                                            
 UUID:            c039a380-6068-4511-b35a-535a73b86ef5                                                                           
-Owner:           bar                                                                                                            
 Created:         2016-09-12T22:20:14Z                                                                                           
 Updated:         2016-09-12T22:20:14Z                                                                                           
 Healthchecks:    
diff --git a/internal/commands/keys.go b/internal/commands/keys.go
index 6698b2690..cc2476a8c 100644
--- a/internal/commands/keys.go
+++ b/internal/commands/keys.go
@@ -31,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:]),
 			})
 		}
diff --git a/internal/commands/keys_test.go b/internal/commands/keys_test.go
index 3b9b235b0..620fc5e67 100644
--- a/internal/commands/keys_test.go
+++ b/internal/commands/keys_test.go
@@ -233,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")
 }
 
@@ -270,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")
 }
 
diff --git a/internal/commands/labels.go b/internal/commands/labels.go
index df96cfb6e..a5ed57f17 100644
--- a/internal/commands/labels.go
+++ b/internal/commands/labels.go
@@ -24,10 +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{"OWNER", "KEY", "VALUE"})
+		table := d.getDefaultFormatTable([]string{"KEY", "VALUE"})
 		for _, key := range *sortKeys(appSettings.Label) {
 			table.Append([]string{
-				appSettings.Owner,
 				key,
 				fmt.Sprintf("%v", appSettings.Label[key]),
 			})
diff --git a/internal/commands/labels_test.go b/internal/commands/labels_test.go
index 2bf362536..9635d011c 100644
--- a/internal/commands/labels_test.go
+++ b/internal/commands/labels_test.go
@@ -35,9 +35,9 @@ func TestLabelsList(t *testing.T) {
 
 	err = cmdr.LabelsList("rivendell")
 	assert.NoError(t, err)
-	testutil.AssertOutput(t, b.String(), `OWNER    KEY         VALUE
-jim      git_repo    https://github.com/drycc/controller-sdk-go
-jim      team        drycc
+	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) {
diff --git a/internal/commands/lifecycles.go b/internal/commands/lifecycles.go
index fe860a0dd..1d3bc2d09 100644
--- a/internal/commands/lifecycles.go
+++ b/internal/commands/lifecycles.go
@@ -57,7 +57,6 @@ func (d *DryccCmd) LifecyclesList(appID, ptype string, version int) error {
 	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:", d.formatTime(config.Created)})
 	table.Append([]string{"Updated:", d.formatTime(config.Updated)})
 	table.Append([]string{"Lifecycle:"})
@@ -117,7 +116,7 @@ func (d *DryccCmd) LifecyclesUnset(appID, ptype string, handlers []string) error
 
 	configObj := api.Config{Lifecycle: make(map[string]*api.Lifecycle)}
 	lifecycle := &api.Lifecycle{}
-	var nullLifecycleHandler *api.LifecycleHandler = nil
+	var nullLifecycleHandler *api.LifecycleHandler
 	for _, handler := range handlers {
 		switch handler {
 		case "postStart":
diff --git a/internal/commands/lifecycles_test.go b/internal/commands/lifecycles_test.go
index 475a64e0d..dfb66400e 100644
--- a/internal/commands/lifecycles_test.go
+++ b/internal/commands/lifecycles_test.go
@@ -64,7 +64,6 @@ func TestLifecyclesList(t *testing.T) {
 	// 使用assert.Contains来检查关键部分，而不是完全匹配
 	assert.Contains(t, actual, "App:          foo")
 	assert.Contains(t, actual, "UUID:         c039a380-6068-4511-b35a-535a73b86ef5")
-	assert.Contains(t, actual, "Owner:        bar")
 	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")
@@ -169,7 +168,6 @@ func TestLifecyclesListAllLifecycles(t *testing.T) {
 	// 检查关键部分
 	assert.Contains(t, actual, "App:          foo")
 	assert.Contains(t, actual, "UUID:         c039a380-6068-4511-b35a-535a73b86ef5")
-	assert.Contains(t, actual, "Owner:        bar")
 	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")
@@ -249,7 +247,6 @@ func TestLifecyclesSet(t *testing.T) {
 	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, "Owner:        bar")
 	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")
diff --git a/internal/commands/perms.go b/internal/commands/perms.go
deleted file mode 100644
index 790db0f83..000000000
--- a/internal/commands/perms.go
+++ /dev/null
@@ -1,85 +0,0 @@
-package commands
-
-import (
-	"strings"
-
-	"github.com/drycc/controller-sdk-go/perms"
-	"github.com/drycc/workflow-cli/internal/loader"
-)
-
-// PermList prints which users have permissions.
-func (d *DryccCmd) PermList(appID string, results int) error {
-	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
-	if err != nil {
-		return err
-	}
-	perms, _, err := perms.List(s.Client, appID, results)
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-	table := d.getDefaultFormatTable([]string{"USERNAME", "PERMISSIONS"})
-	for _, perm := range perms {
-		p := strings.Join(perm.Permissions, ",")
-		table.Append([]string{perm.Username, p})
-	}
-	table.Render()
-	return nil
-}
-
-// PermCreate create user perms.
-func (d *DryccCmd) PermCreate(appID, username, permissions string) error {
-	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
-	if err != nil {
-		return err
-	}
-	d.Printf("Adding user %s as a collaborator for %s... ", username, permissions)
-	quit := progress(d.WOut)
-	err = perms.Create(s.Client, appID, username, permissions)
-	quit <- true
-	<-quit
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-
-	d.Println("done")
-
-	return nil
-}
-
-// PermUpdate update user perms.
-func (d *DryccCmd) PermUpdate(appID, username, permissions string) error {
-	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
-	if err != nil {
-		return err
-	}
-	d.Printf("Updating user %s as a collaborator for %s... ", username, permissions)
-	quit := progress(d.WOut)
-	err = perms.Update(s.Client, appID, username, permissions)
-	quit <- true
-	<-quit
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-
-	d.Println("done")
-
-	return nil
-}
-
-// PermDelete removes a user from an app.
-func (d *DryccCmd) PermDelete(appID, username string) error {
-	appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
-	if err != nil {
-		return err
-	}
-	d.Printf("Removing user permission... ")
-	quit := progress(d.WOut)
-	err = perms.Delete(s.Client, appID, username)
-	quit <- true
-	<-quit
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-	d.Println("done")
-	return nil
-}
diff --git a/internal/commands/perms_test.go b/internal/commands/perms_test.go
deleted file mode 100644
index a3869af23..000000000
--- a/internal/commands/perms_test.go
+++ /dev/null
@@ -1,176 +0,0 @@
-package commands
-
-import (
-	"bytes"
-	"fmt"
-	"net/http"
-	"testing"
-
-	"github.com/drycc/workflow-cli/pkg/testutil"
-	"github.com/stretchr/testify/assert"
-)
-
-func TestListUserPerm(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-
-	server.Mux.HandleFunc("/v2/apps/lorem-ipsum/perms/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-		fmt.Fprintf(w, `{
-	"results": [
-		{"app": "lorem-ipsum", "username": "foo", "permissions": ["view"]},
-		{"app": "lorem-ipsum", "username": "foo", "permissions": ["view"]}
-	],
-	"count": 2
-}`)
-	})
-
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	err = cmdr.PermList("lorem-ipsum", -1)
-	assert.NoError(t, err)
-
-	assert.Equal(t, testutil.StripProgress(b.String()), `USERNAME    PERMISSIONS 
-foo         view           
-foo         view           
-`, "output")
-}
-
-func TestListUserPermLimit(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-
-	server.Mux.HandleFunc("/v2/apps/lorem-ipsum/perms/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-		fmt.Fprintf(w, `{
-	"results": [
-		{"app": "lorem-ipsum", "username": "foo", "permissions": ["view"]}
-	],
-	"count": 1
-}`)
-	})
-
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	err = cmdr.PermList("lorem-ipsum", 1)
-	assert.NoError(t, err)
-
-	assert.Equal(t, testutil.StripProgress(b.String()), `USERNAME    PERMISSIONS 
-foo         view           
-`, "output")
-}
-
-func TestCreateUserPerm(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", "view")
-	assert.NoError(t, err)
-	assert.Equal(t,
-		testutil.StripProgress(b.String()),
-		"Adding user test-user as a collaborator for view... done\n", "output")
-}
-
-func TestUpdateUserPerm(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.PermUpdate("lorem-ipsum", "test-user", "view")
-	assert.NoError(t, err)
-	assert.Equal(t,
-		testutil.StripProgress(b.String()),
-		"Updating user test-user as a collaborator for view... done\n", "output")
-}
-
-func TestDeleteUserPerm(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/test-user/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-	})
-
-	err = cmdr.PermDelete("lorem-ipsum", "test-user")
-	assert.NoError(t, err)
-	assert.Equal(t,
-		testutil.StripProgress(b.String()),
-		"Removing user permission... done\n", "output")
-}
diff --git a/internal/commands/releases.go b/internal/commands/releases.go
index 286e1d401..121ee8069 100644
--- a/internal/commands/releases.go
+++ b/internal/commands/releases.go
@@ -27,13 +27,13 @@ func (d *DryccCmd) ReleasesList(appID string, targets []string, results int) err
 	if count == 0 {
 		d.Println(fmt.Sprintf("No releases found in %s app.", appID))
 	} else {
-		table := d.getDefaultFormatTable([]string{"OWNER", "STATE", "VERSION", "CREATED", "SUMMARY"})
+		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.Owner, r.State, fmt.Sprintf("v%d", r.Version), d.formatTime(r.Created), summary})
+			table.Append([]string{r.State, fmt.Sprintf("v%d", r.Version), d.formatTime(r.Created), summary})
 		}
 		table.Render()
 	}
@@ -55,7 +55,6 @@ func (d *DryccCmd) ReleasesInfo(appID string, version int) error {
 	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:", d.formatTime(r.Created)})
diff --git a/internal/commands/releases_test.go b/internal/commands/releases_test.go
index 695a1a7a1..27081b593 100644
--- a/internal/commands/releases_test.go
+++ b/internal/commands/releases_test.go
@@ -59,9 +59,9 @@ func TestReleasesList(t *testing.T) {
 
 	err = cmdr.ReleasesList("numenor", []string{}, -1)
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `OWNER     STATE      VERSION    CREATED                   SUMMARY                        
-nazgul    succeed    v2         2016-08-22T17:40:16Z      khamul added ANGMAR               
-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")
 }
 
@@ -100,8 +100,8 @@ func TestReleasesListLimit(t *testing.T) {
 
 	err = cmdr.ReleasesList("numenor", []string{}, 1)
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `OWNER     STATE      VERSION    CREATED                 SUMMARY             
-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")
 }
 
@@ -144,7 +144,6 @@ 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                    
diff --git a/internal/commands/resources.go b/internal/commands/resources.go
index 55a37b42e..64640de9e 100644
--- a/internal/commands/resources.go
+++ b/internal/commands/resources.go
@@ -145,11 +145,10 @@ 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{"NAME", "OWNER", "PLAN", "UPDATED"})
+		table := d.getDefaultFormatTable([]string{"NAME", "PLAN", "UPDATED"})
 		for _, resource := range resources {
 			table.Append([]string{
 				resource.Name,
-				resource.Owner,
 				resource.Plan,
 				d.formatTime(resource.Updated),
 			})
@@ -177,7 +176,6 @@ func (d *DryccCmd) ResourceGet(appID, name string, details bool) 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:"})
diff --git a/internal/commands/resources_test.go b/internal/commands/resources_test.go
index fe76bffb3..e6711c673 100644
--- a/internal/commands/resources_test.go
+++ b/internal/commands/resources_test.go
@@ -134,8 +134,8 @@ func TestResourcesList(t *testing.T) {
 	err = cmdr.ResourcesList("example-go", -1)
 	assert.NoError(t, err)
 
-	testutil.AssertOutput(t, b.String(), `NAME     OWNER    PLAN         UPDATED
-mysql    test     mysql:5.6    2020-09-08T00:00:00UTC
+	testutil.AssertOutput(t, b.String(), `NAME     PLAN         UPDATED
+mysql    mysql:5.6    2020-09-08T00:00:00UTC
 `)
 }
 
@@ -173,7 +173,6 @@ func TestResourceGet(t *testing.T) {
 UUID:       de1bf5b5-4a72-4f94-a10c-d2a3741cdf75
 Name:       mysql
 Plan:       mysql:5.6
-Owner:      test
 Status:     Ready
 Binding:    Ready
 Data:
diff --git a/internal/commands/routes.go b/internal/commands/routes.go
index bfb54a60c..5123b57b2 100644
--- a/internal/commands/routes.go
+++ b/internal/commands/routes.go
@@ -48,7 +48,7 @@ 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", "KIND", "GATEWAYS", "SERVICES"})
+		table := d.getDefaultFormatTable([]string{"NAME", "KIND", "GATEWAYS", "SERVICES"})
 		for _, route := range routes {
 			var services []string
 			for _, rule := range route.Rules {
@@ -72,7 +72,7 @@ func (d *DryccCmd) RoutesList(appID string, results int) error {
 			if err != nil {
 				return err
 			}
-			table.Append([]string{route.Name, route.Owner, route.Kind, string(gatewaysBytes), string(servicesBytes)})
+			table.Append([]string{route.Name, route.Kind, string(gatewaysBytes), string(servicesBytes)})
 		}
 		table.Render()
 	}
diff --git a/internal/commands/routes_test.go b/internal/commands/routes_test.go
index 82165faca..fc25d46ac 100644
--- a/internal/commands/routes_test.go
+++ b/internal/commands/routes_test.go
@@ -103,8 +103,8 @@ func TestRoutesList(t *testing.T) {
 	err = cmdr.RoutesList("foo", -1)
 	assert.NoError(t, err)
 
-	assert.Equal(t, b.String(), `NAME          OWNER    KIND         GATEWAYS                               SERVICES                                    
-example-go    test     HTTPRoute    ["example-go:80","example-go:8080"]    ["yygl-nextcloud:80","yygl-nextcloud:8080"]    
+	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")
 }
 
diff --git a/internal/commands/tls.go b/internal/commands/tls.go
index 9dd51d604..cc08a2988 100644
--- a/internal/commands/tls.go
+++ b/internal/commands/tls.go
@@ -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
diff --git a/internal/commands/tls_test.go b/internal/commands/tls_test.go
index 071e448b1..5a20c3e34 100644
--- a/internal/commands/tls_test.go
+++ b/internal/commands/tls_test.go
@@ -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/internal/commands/users.go b/internal/commands/users.go
deleted file mode 100644
index b6dc6fbda..000000000
--- a/internal/commands/users.go
+++ /dev/null
@@ -1,73 +0,0 @@
-package commands
-
-import (
-	"fmt"
-
-	"github.com/drycc/controller-sdk-go/users"
-	"github.com/drycc/workflow-cli/pkg/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/internal/commands/users_test.go b/internal/commands/users_test.go
deleted file mode 100644
index bb0eb36e2..000000000
--- a/internal/commands/users_test.go
+++ /dev/null
@@ -1,157 +0,0 @@
-package commands
-
-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/internal/commands/utils.go b/internal/commands/utils.go
index 2e5537413..9b1f088f0 100644
--- a/internal/commands/utils.go
+++ b/internal/commands/utils.go
@@ -118,20 +118,6 @@ func (d *DryccCmd) wrapString(s string) string {
 	return strings.Join(sa, "\n")
 }
 
-// fixateString  fix s width.
-func (d *DryccCmd) fixateString(s string, width int) string {
-	switch {
-	case len(s) > width:
-		trimSize := len(s) - width + 3
-		if trimSize < len(s) {
-			s = "..." + s[trimSize:]
-		}
-	case len(s) < width:
-		s += strings.Repeat(" ", width-len(s))
-	}
-	return s
-}
-
 // 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
diff --git a/internal/commands/volumes.go b/internal/commands/volumes.go
index a70432de2..08bbb58bb 100644
--- a/internal/commands/volumes.go
+++ b/internal/commands/volumes.go
@@ -6,7 +6,6 @@ import (
 	"os"
 	"os/signal"
 	"regexp"
-	"syscall"
 	"time"
 
 	"github.com/drycc/controller-sdk-go/api"
@@ -52,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:"})
@@ -184,7 +182,7 @@ func (d *DryccCmd) VolumesServe(appID, name string) error {
 	d.Print("\n")
 
 	signalChan := make(chan os.Signal, 1)
-	signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP, syscall.SIGUSR1, syscall.SIGUSR2)
+	signal.Notify(signalChan, os.Interrupt)
 	d.Printf("WebDAV service for volume %s is running. Press Ctrl+C to stop.\n", name)
 	for {
 		select {
@@ -273,14 +271,14 @@ func parseVolume(volumeVars []string) (map[string]any, 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/internal/commands/volumes_test.go b/internal/commands/volumes_test.go
index 30c29c3da..46652e587 100644
--- a/internal/commands/volumes_test.go
+++ b/internal/commands/volumes_test.go
@@ -48,9 +48,9 @@ func TestVolumesList(t *testing.T) {
 
 	err = cmdr.VolumesList("example-go", -1)
 	assert.NoError(t, err)
-	testutil.AssertOutput(t, b.String(), `NAME        OWNER    TYPE    PTYPE     PATH            SIZE
-myvolume    test     csi     cmd       /data/cmd1      500G
-myvolume    test     csi     cmd123    /data/cmd123    500G
+	testutil.AssertOutput(t, b.String(), `NAME        TYPE    PTYPE     PATH            SIZE
+myvolume    csi     cmd       /data/cmd1      500G
+myvolume    csi     cmd123    /data/cmd123    500G
 `)
 }
 
@@ -118,7 +118,6 @@ func TestVolumesInfo(t *testing.T) {
 	assert.NoError(t, err)
 	testutil.AssertOutput(t, b.String(), `UUID:          de1bf5b5-4a72-4f94-a10c-d2a3741cdf75
 Name:          myvolume
-Owner:         test
 Type:          nfs
 Path:
                cmd: /data/cmd1
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
index 6be57bc90..b915ddf62 100644
--- a/internal/completion/completion.go
+++ b/internal/completion/completion.go
@@ -15,7 +15,6 @@ import (
 	"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/perms"
 	"github.com/drycc/controller-sdk-go/ps"
 	"github.com/drycc/controller-sdk-go/pts"
 	"github.com/drycc/controller-sdk-go/releases"
@@ -24,6 +23,8 @@ import (
 	"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"
@@ -277,7 +278,7 @@ func (c *LifecycleCompletion) CompletionFunc(cmd *cobra.Command, args []string,
 	return lifecycleActionTypeCompletion.CompletionFunc(cmd, args, toComplete)
 }
 
-// ProbeTypeCompletion provides completion for probe types
+// HealthcheckProbeTypeCompletion provides completion for probe types
 type HealthcheckProbeTypeCompletion struct {
 	ArgsLen    int
 	ConfigFile *string
@@ -323,7 +324,7 @@ func (c *HealthcheckProbeActionTypeCompletion) CompletionFunc(_ *cobra.Command,
 	return nil, cobra.ShellCompDirectiveNoFileComp
 }
 
-// HealthChecksCompletion provides completion for health checks
+// HealthCheckCompletion provides completion for health checks
 type HealthCheckCompletion struct {
 	ArgsLen    int
 	ConfigFile *string
@@ -480,89 +481,112 @@ func (c *LimitSetPlanCompletion) CompletionFunc(cmd *cobra.Command, args []strin
 	return ptsSetArgsCompletion.CompletionFunc(cmd, args, toComplete)
 }
 
-// UserPermsCompletion provides completion for user permissions
-type UserPermsCompletion struct {
+// WorkspaceCompletion provides completion for workspace names
+type WorkspaceCompletion struct {
 	ArgsLen    int
 	ConfigFile *string
 }
 
-// CompletionFunc returns a list of user permissions for completion
-func (c *UserPermsCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
-	perms := []string{"view", "change", "delete"}
-	if len(args) > 0 && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
-
-		var results []string
-		for _, perm := range perms {
-			if strings.HasPrefix(perm, toComplete) {
-				results = append(results, perm)
+// 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 results, cobra.ShellCompDirectiveNoFileComp
-
 	}
 	return nil, cobra.ShellCompDirectiveNoFileComp
 }
 
-// UserPermsArgsCompletion provides completion for user permission arguments.
-type UserPermsArgsCompletion struct {
-	*UserPermsCompletion
+// WorkspaceMemberCompletion provides completion for workspace member usernames
+type WorkspaceMemberCompletion struct {
+	Workspace  *string
+	ArgsLen    int
+	ConfigFile *string
 }
 
-// CompletionFunc returns a list of user permission arguments for completion
-func (c *UserPermsArgsCompletion) CompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
-	c.ArgsLen = -1
-	var results []string
-	completes, _ := c.UserPermsCompletion.CompletionFunc(cmd, args, toComplete)
-	for _, complete := range completes {
-		if !slices.Contains(args, complete) {
-			results = append(results, complete)
+// 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 results, cobra.ShellCompDirectiveNoFileComp
+	return nil, cobra.ShellCompDirectiveNoFileComp
 }
 
-// PermUsernameCompletion provides completion for permission usernames
-type PermUsernameCompletion struct {
-	AppID      *string
+// WorkspaceRoleCompletion provides completion for workspace member roles
+type WorkspaceRoleCompletion struct {
 	ArgsLen    int
 	ConfigFile *string
 }
 
-// CompletionFunc returns a list of permission usernames for completion
-func (c *PermUsernameCompletion) CompletionFunc(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
-	if len(args) == 0 {
-		if appID, s, err := loader.LoadAppSettings(*c.ConfigFile, *c.AppID); err == nil && (c.ArgsLen < 0 || len(args) == c.ArgsLen) {
-			if perms, _, err := perms.List(s.Client, appID, -1); err == nil {
-				var results []string
-				for _, perm := range perms {
-					if strings.HasPrefix(perm.Username, toComplete) {
-						results = append(results, perm.Username)
-					}
-				}
-				return results, cobra.ShellCompDirectiveNoFileComp
+// 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
 }
 
-// PermUpdateCompletion provides completion for permission updates
-type PermUpdateCompletion struct {
-	AppID      *string
-	ArgsLen    int
+// WorkspaceUpdateCompletion provides completion for workspace update command
+type WorkspaceUpdateCompletion struct {
 	ConfigFile *string
 }
 
-// CompletionFunc returns a list of permission updates for completion
-func (c *PermUpdateCompletion) CompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+// 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 {
-		permUsernameCompletion := PermUsernameCompletion{AppID: c.AppID, ArgsLen: 0, ConfigFile: c.ConfigFile}
-		return permUsernameCompletion.CompletionFunc(cmd, args, toComplete)
+		// First arg: workspace name
+		workspaceCompletion := WorkspaceCompletion{ArgsLen: 0, ConfigFile: c.ConfigFile}
+		return workspaceCompletion.CompletionFunc(cmd, args, toComplete)
 	}
-	userPermsArgsCompletion := UserPermsArgsCompletion{
-		UserPermsCompletion: &UserPermsCompletion{ConfigFile: c.ConfigFile},
-	}
-	return userPermsArgsCompletion.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
diff --git a/internal/parser/apps.go b/internal/parser/apps.go
index 2848dd88f..0685cfbb3 100644
--- a/internal/parser/apps.go
+++ b/internal/parser/apps.go
@@ -35,8 +35,9 @@ func NewAppsCommand(cmdr *commands.DryccCmd) *cobra.Command {
 // AppsCreate creates the apps create command
 func appsCreate(cmdr *commands.DryccCmd) *cobra.Command {
 	var flags struct {
-		remote   string
-		noRemote bool
+		workspace string
+		remote    string
+		noRemote  bool
 	}
 	cmd := &cobra.Command{
 		Use:   "create [<id>]",
@@ -48,12 +49,17 @@ func appsCreate(cmdr *commands.DryccCmd) *cobra.Command {
 			if len(args) > 0 {
 				id = args[0]
 			}
-			return cmdr.AppCreate(id, flags.remote, flags.noRemote)
+			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
 }
 
@@ -172,14 +178,16 @@ func appsDestroy(cmdr *commands.DryccCmd) *cobra.Command {
 
 // 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 <username>",
-		Args:  cobra.ExactArgs(1),
-		Short: i18n.T("Transfer app ownership to another user"),
-		Long:  i18n.T("Transfer application ownership to another user."),
+		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 {
-			user := args[0]
-			return cmdr.AppTransfer(app, user)
+			workspace := args[0]
+			return cmdr.AppTransfer(app, workspace)
 		},
 	}
 
diff --git a/internal/parser/healthchecks.go b/internal/parser/healthchecks.go
index a73ee42e2..104fd9e46 100644
--- a/internal/parser/healthchecks.go
+++ b/internal/parser/healthchecks.go
@@ -222,6 +222,7 @@ func healthchecksUnset(cmdr *commands.DryccCmd) *cobra.Command {
 		},
 	}
 	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)
 
diff --git a/internal/parser/lifecycles.go b/internal/parser/lifecycles.go
index 2e1c4a612..4af203023 100644
--- a/internal/parser/lifecycles.go
+++ b/internal/parser/lifecycles.go
@@ -17,7 +17,7 @@ var lifecycleFlags struct {
 	ptype string
 }
 
-// NewLifecycleCommand creates a command for managing application lifecycle.
+// NewLifecyclesCommand creates a command for managing application lifecycle.
 func NewLifecyclesCommand(cmdr *commands.DryccCmd) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "lifecycles",
@@ -219,6 +219,7 @@ func lifecyclesUnset(cmdr *commands.DryccCmd) *cobra.Command {
 		},
 	}
 	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)
 
diff --git a/internal/parser/perms.go b/internal/parser/perms.go
deleted file mode 100644
index 5da85fb60..000000000
--- a/internal/parser/perms.go
+++ /dev/null
@@ -1,138 +0,0 @@
-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"
-)
-
-// NewPermsCommand creates a command for managing permissions.
-func NewPermsCommand(cmdr *commands.DryccCmd) *cobra.Command {
-	cmd := &cobra.Command{
-		Use:   "perms",
-		Short: i18n.T("Manage permissions for applications"),
-		RunE: func(_ *cobra.Command, _ []string) error {
-			results, _ := commands.ResponseLimit(limit)
-			return cmdr.PermList(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(permsListCommand(cmdr))
-	cmd.AddCommand(permAddCommand(cmdr))
-	cmd.AddCommand(permUpdateCommand(cmdr))
-	cmd.AddCommand(permRemoveCommand(cmdr))
-	return cmd
-}
-
-func permsListCommand(cmdr *commands.DryccCmd) *cobra.Command {
-	cmd := &cobra.Command{
-		Use:   "list",
-		Short: i18n.T("List all user permissions"),
-		RunE: func(_ *cobra.Command, _ []string) error {
-			results, _ := commands.ResponseLimit(limit)
-			return cmdr.PermList(app, results)
-		},
-	}
-
-	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("The maximum number of results to display"))
-
-	return cmd
-}
-
-func permAddCommand(cmdr *commands.DryccCmd) *cobra.Command {
-	var flags struct {
-		app         string
-		username    string
-		permissions string
-	}
-
-	userPermsArgsCompletion := completion.UserPermsArgsCompletion{
-		UserPermsCompletion: &completion.UserPermsCompletion{ConfigFile: &cmdr.ConfigFile},
-	}
-	cmd := &cobra.Command{
-		Use:  "add <username> <permission>...",
-		Args: cobra.MinimumNArgs(2),
-		Example: template.CustomExample(
-			"drycc add username view change delete",
-			map[string]string{
-				"<username>":   i18n.T("The name of the user"),
-				"<permission>": i18n.T("The user permissions (view,change,delete)"),
-			},
-		),
-		Short:             i18n.T("Add a user permissions"),
-		ValidArgsFunction: userPermsArgsCompletion.CompletionFunc,
-		RunE: func(_ *cobra.Command, args []string) error {
-			flags.username = args[0]
-			flags.permissions = strings.Join(args[1:], ",")
-			return cmdr.PermCreate(app, flags.username, flags.permissions)
-		},
-	}
-
-	return cmd
-}
-
-func permUpdateCommand(cmdr *commands.DryccCmd) *cobra.Command {
-	var flags struct {
-		app         string
-		username    string
-		permissions string
-	}
-
-	permUpdateCompletion := completion.PermUpdateCompletion{AppID: &app, ArgsLen: -1, ConfigFile: &cmdr.ConfigFile}
-	cmd := &cobra.Command{
-		Use:  "update <username> <permission>... ",
-		Args: cobra.MinimumNArgs(2),
-		Example: template.CustomExample(
-			"drycc update username view,change",
-			map[string]string{
-				"<username>":   i18n.T("The name of the user"),
-				"<permission>": i18n.T("The user's permissions (view,change,delete)"),
-			},
-		),
-		Short:             i18n.T("Update a user permission"),
-		ValidArgsFunction: permUpdateCompletion.CompletionFunc,
-		RunE: func(_ *cobra.Command, args []string) error {
-			flags.username = args[0]
-			flags.permissions = strings.Join(args[1:], ",")
-			return cmdr.PermUpdate(app, flags.username, flags.permissions)
-		},
-	}
-
-	return cmd
-}
-
-func permRemoveCommand(cmdr *commands.DryccCmd) *cobra.Command {
-	var flags struct {
-		app      string
-		username string
-	}
-	permUsernameCompletion := completion.PermUsernameCompletion{AppID: &app, ArgsLen: 0, ConfigFile: &cmdr.ConfigFile}
-	cmd := &cobra.Command{
-		Use:  "remove <username>",
-		Args: cobra.MinimumNArgs(1),
-		Example: template.CustomExample(
-			"drycc perms remove username",
-			map[string]string{
-				"<username>": i18n.T("The name of the user"),
-			},
-		),
-		Short:             i18n.T("Remove a user permissions"),
-		ValidArgsFunction: permUsernameCompletion.CompletionFunc,
-		RunE: func(_ *cobra.Command, args []string) error {
-			flags.username = args[0]
-			return cmdr.PermDelete(app, flags.username)
-		},
-	}
-
-	return cmd
-}
diff --git a/internal/parser/pts.go b/internal/parser/pts.go
index cac96bbe7..863f1143e 100644
--- a/internal/parser/pts.go
+++ b/internal/parser/pts.go
@@ -145,6 +145,9 @@ func ptsCleanCommand(cmdr *commands.DryccCmd) *cobra.Command {
 		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),
@@ -154,7 +157,8 @@ func ptsCleanCommand(cmdr *commands.DryccCmd) *cobra.Command {
 				"<ptype>": i18n.T("The process name as defined in your Procfile"),
 			},
 		),
-		Short: i18n.T("Clean process types of not used"),
+		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)
diff --git a/internal/parser/releases.go b/internal/parser/releases.go
index e8291b35b..ccf8c442e 100644
--- a/internal/parser/releases.go
+++ b/internal/parser/releases.go
@@ -121,6 +121,9 @@ func releasesDeployCommand(cmdr *commands.DryccCmd) *cobra.Command {
 }
 
 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(
@@ -130,16 +133,17 @@ func releasesRollbackCommand(cmdr *commands.DryccCmd) *cobra.Command {
 				"<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),
+		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[1]
+				versionStr = args[0]
 			} else {
 				ptypes = args[:len(args)-1]
 				versionStr = args[len(args)-1]
diff --git a/internal/parser/users.go b/internal/parser/users.go
deleted file mode 100644
index 7058660c1..000000000
--- a/internal/parser/users.go
+++ /dev/null
@@ -1,91 +0,0 @@
-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"
-)
-
-// NewUsersCommand creates the users command
-func NewUsersCommand(cmdr *commands.DryccCmd) *cobra.Command {
-	cmd := &cobra.Command{
-		Use:   "users",
-		Short: i18n.T("Manage registered users"),
-		RunE: func(_ *cobra.Command, _ []string) error {
-			results, _ := commands.ResponseLimit(limit)
-			return cmdr.UsersList(results)
-		},
-	}
-
-	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("The maximum number of results to display"))
-
-	cmd.AddCommand(usersListCommand(cmdr))
-	cmd.AddCommand(usersEnableCommand(cmdr))
-	cmd.AddCommand(usersDisableCommand(cmdr))
-	return cmd
-}
-
-func usersListCommand(cmdr *commands.DryccCmd) *cobra.Command {
-	cmd := &cobra.Command{
-		Use:   "list",
-		Short: i18n.T("List all registered users"),
-		RunE: func(_ *cobra.Command, _ []string) error {
-			results, _ := commands.ResponseLimit(limit)
-			return cmdr.UsersList(results)
-		},
-	}
-
-	cmd.Flags().IntVarP(&limit, "limit", "l", 0, i18n.T("The maximum number of results to display"))
-	return cmd
-}
-
-func usersEnableCommand(cmdr *commands.DryccCmd) *cobra.Command {
-	var flags struct {
-		username string
-	}
-
-	cmd := &cobra.Command{
-		Use: "enable <username>",
-		Example: template.CustomExample(
-			"drycc users enable john_doe",
-			map[string]string{
-				"<username>": i18n.T("The username you want to enable"),
-			},
-		),
-		Short: i18n.T("Enable a user"),
-		Long: i18n.T(`Enable a user when his status is disabled.
-Requires admin privileges.`),
-		Args: cobra.ExactArgs(1),
-		RunE: func(_ *cobra.Command, args []string) error {
-			flags.username = args[0]
-			return cmdr.UsersEnable(flags.username)
-		},
-	}
-	return cmd
-}
-
-func usersDisableCommand(cmdr *commands.DryccCmd) *cobra.Command {
-	var flags struct {
-		username string
-	}
-
-	cmd := &cobra.Command{
-		Use: "disable <username>",
-		Example: template.CustomExample(
-			"drycc users disable john_doe",
-			map[string]string{
-				"<username>": i18n.T("The username you want to disable"),
-			},
-		),
-		Short: i18n.T("Disable a user"),
-		Long: i18n.T(`Disable a user when his status is disabled.
-Requires admin privileges.`),
-		Args: cobra.ExactArgs(1),
-		RunE: func(_ *cobra.Command, args []string) error {
-			flags.username = args[0]
-			return cmdr.UsersDisable(flags.username)
-		},
-	}
-	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/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.mo b/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.mo
index 7362b2067b1a710ffc17b16f93aa4957add9ccc8..7e4b6d47c95a4e2ad3337bdb3e7277b4741f35da 100644
GIT binary patch
diff --git a/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.po b/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.po
index 5fed8e04d..158924d35 100644
--- a/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.po
+++ b/pkg/i18n/translations/drycc/en_US/LC_MESSAGES/cli.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: \n"
 "Report-Msgid-Bugs-To: EMAIL\n"
-"POT-Creation-Date: 2025-12-08 16:06+0800\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"
@@ -49,10 +49,6 @@ msgstr ""
 msgid "Add a token for controller authentication"
 msgstr ""
 
-#: internal/parser/perms.go:72
-msgid "Add a user permissions"
-msgstr ""
-
 #: internal/parser/keys.go:57
 msgid "Add an SSH key"
 msgstr ""
@@ -125,7 +121,7 @@ msgstr ""
 msgid "CA key secret"
 msgstr ""
 
-#: internal/parser/pts.go:157
+#: internal/parser/pts.go:160
 msgid "Clean process types of not used"
 msgstr ""
 
@@ -133,7 +129,7 @@ msgstr ""
 msgid "Clear the current user session"
 msgstr ""
 
-#: internal/parser/apps.go:141
+#: internal/parser/apps.go:146
 msgid "Command execution timeout in seconds"
 msgstr ""
 
@@ -150,7 +146,7 @@ msgstr ""
 msgid "Configure automatic certificate management issuer details"
 msgstr ""
 
-#: internal/parser/apps.go:44
+#: internal/parser/apps.go:45
 msgid "Create a new application"
 msgstr ""
 
@@ -166,6 +162,10 @@ msgstr ""
 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 ""
@@ -174,7 +174,7 @@ msgstr ""
 msgid "Create service for an application"
 msgstr ""
 
-#: internal/parser/apps.go:45
+#: internal/parser/apps.go:46
 msgid ""
 "Creates a new application. If no <id> is provided, one will be generated "
 "automatically"
@@ -213,6 +213,10 @@ msgstr ""
 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 ""
@@ -229,7 +233,7 @@ msgstr ""
 msgid "Describe a process type"
 msgstr ""
 
-#: internal/parser/apps.go:159
+#: internal/parser/apps.go:164
 msgid "Destroy an application"
 msgstr ""
 
@@ -245,16 +249,6 @@ msgstr ""
 msgid "Dettach to gateway"
 msgstr ""
 
-#: internal/parser/users.go:81
-msgid "Disable a user"
-msgstr ""
-
-#: internal/parser/users.go:82
-msgid ""
-"Disable a user when his status is disabled.\n"
-"Requires admin privileges."
-msgstr ""
-
 #: internal/parser/autorollback.go:58
 msgid "Disable autorollback for an application"
 msgstr ""
@@ -287,7 +281,7 @@ msgstr ""
 msgid "Displays the currently logged in user"
 msgstr ""
 
-#: internal/parser/apps.go:56
+#: internal/parser/apps.go:58
 msgid "Do not create a 'drycc' git remote"
 msgstr ""
 
@@ -295,20 +289,14 @@ msgstr ""
 msgid "Drycc config path to the application"
 msgstr ""
 
-#: internal/parser/users.go:56
-msgid "Enable a user"
-msgstr ""
-
-#: internal/parser/users.go:57
-msgid ""
-"Enable a user when his status is disabled.\n"
-"Requires admin privileges."
-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 ""
@@ -391,6 +379,10 @@ msgstr ""
 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 ""
@@ -399,7 +391,7 @@ msgstr ""
 msgid "List SSL certificates for an application"
 msgstr ""
 
-#: internal/parser/apps.go:64
+#: internal/parser/apps.go:69
 msgid "List accessible applications"
 msgstr ""
 
@@ -419,14 +411,6 @@ msgstr ""
 msgid "List all available resource services"
 msgstr ""
 
-#: internal/parser/users.go:32
-msgid "List all registered users"
-msgstr ""
-
-#: internal/parser/perms.go:40
-msgid "List all user permissions"
-msgstr ""
-
 #: internal/parser/releases.go:53
 msgid "List an application's release history"
 msgstr ""
@@ -499,7 +483,7 @@ msgstr ""
 msgid "List volumes in the application"
 msgstr ""
 
-#: internal/parser/apps.go:65
+#: internal/parser/apps.go:70
 msgid "Lists applications visible to the current user"
 msgstr ""
 
@@ -527,6 +511,10 @@ msgstr ""
 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 ""
@@ -595,10 +583,6 @@ msgstr ""
 msgid "Manage labels of application"
 msgstr ""
 
-#: internal/parser/perms.go:17
-msgid "Manage permissions for applications"
-msgstr ""
-
 #: internal/parser/timeouts.go:15
 msgid "Manage pods termination grace period"
 msgstr ""
@@ -615,10 +599,6 @@ msgstr ""
 msgid "Manage processes inside an app"
 msgstr ""
 
-#: internal/parser/users.go:14
-msgid "Manage registered users"
-msgstr ""
-
 #: internal/parser/releases.go:24
 msgid "Manage releases of an application"
 msgstr ""
@@ -659,11 +639,15 @@ msgstr ""
 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:71
+#: internal/parser/apps.go:23 internal/parser/apps.go:76
 msgid "Maximum number of results to display"
 msgstr ""
 
@@ -687,7 +671,7 @@ msgstr ""
 msgid "Mount a volume to process of the application"
 msgstr ""
 
-#: internal/parser/apps.go:55 internal/parser/git.go:42
+#: internal/parser/git.go:42 internal/parser/apps.go:57
 msgid "Name of remote to create"
 msgstr ""
 
@@ -712,11 +696,11 @@ msgid ""
 "Number or name of the port to access on the pods targeted by the service"
 msgstr ""
 
-#: internal/parser/apps.go:97
+#: internal/parser/apps.go:102
 msgid "Open the application in a browser"
 msgstr ""
 
-#: internal/parser/apps.go:98
+#: internal/parser/apps.go:103
 msgid "Opens a URL to the application in the default browser"
 msgstr ""
 
@@ -768,6 +752,10 @@ msgstr ""
 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 ""
@@ -802,7 +790,7 @@ msgstr ""
 msgid "Prints info about a particular release"
 msgstr ""
 
-#: internal/parser/apps.go:80
+#: internal/parser/apps.go:85
 msgid "Prints info about the current application"
 msgstr ""
 
@@ -868,9 +856,9 @@ msgstr ""
 msgid "Remove a token for controller authentication"
 msgstr ""
 
-#: internal/parser/perms.go:129
-msgid "Remove a user permissions"
-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"
@@ -908,15 +896,15 @@ msgstr ""
 msgid "Restart application process types"
 msgstr ""
 
-#: internal/parser/apps.go:142
+#: internal/parser/apps.go:147
 msgid "Retention time of records in seconds"
 msgstr ""
 
-#: internal/parser/releases.go:133
+#: internal/parser/releases.go:136
 msgid "Return to a previous release"
 msgstr ""
 
-#: internal/parser/releases.go:134
+#: internal/parser/releases.go:137
 msgid "Rolls back to a previous application release"
 msgstr ""
 
@@ -924,11 +912,11 @@ msgstr ""
 msgid "Rules-file is the file name of rule to apply"
 msgstr ""
 
-#: internal/parser/apps.go:131
+#: internal/parser/apps.go:136
 msgid "Run a command in an ephemeral app container"
 msgstr ""
 
-#: internal/parser/apps.go:132
+#: internal/parser/apps.go:137
 msgid "Runs a command inside an ephemeral app container"
 msgstr ""
 
@@ -1203,7 +1191,7 @@ msgstr ""
 msgid "Sets termination grace period for an application"
 msgstr ""
 
-#: cmd/root.go:75
+#: cmd/root.go:74
 msgid "Shortcut Commands:"
 msgstr "Shortcut Commands:"
 
@@ -1219,12 +1207,16 @@ msgstr ""
 msgid "Simulate an update, only print the version info."
 msgstr ""
 
-#: internal/parser/apps.go:166
+#: 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 ""
@@ -1276,8 +1268,16 @@ msgstr ""
 "                  'sleep' accepts duration in seconds; 'httpGet' and "
 "'tcpSocket' accept a port number."
 
-#: internal/parser/apps.go:127 internal/parser/shortcuts.go:53
-#: internal/parser/volumes.go:235
+#: 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 ""
 
@@ -1337,17 +1337,17 @@ msgstr "The lifecycle handler, such as 'postStart' 'preStop'"
 msgid "The limit plan id to apply to the process type"
 msgstr ""
 
-#: internal/parser/users.go:21 internal/parser/users.go:39
-#: internal/parser/releases.go:33 internal/parser/releases.go:62
-#: internal/parser/limits.go:127 internal/parser/limits.go:151
 #: internal/parser/certs.go:23 internal/parser/certs.go:48
-#: internal/parser/routes.go:28 internal/parser/routes.go:107
-#: internal/parser/gateways.go:22 internal/parser/gateways.go:44
-#: internal/parser/volumes.go:23 internal/parser/volumes.go:145
-#: internal/parser/perms.go:25 internal/parser/perms.go:47
-#: internal/parser/tokens.go:22 internal/parser/tokens.go:40
 #: 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 ""
 
@@ -1359,16 +1359,17 @@ msgstr ""
 msgid "The name of the cert to remove from the app"
 msgstr ""
 
-#: internal/parser/perms.go:68 internal/parser/perms.go:98
-#: internal/parser/perms.go:126
-msgid "The name of the user"
-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/shortcuts.go:95 internal/parser/pts.go:121
+#: internal/parser/pts.go:121 internal/parser/shortcuts.go:95
 msgid "The number of processes"
 msgstr ""
 
@@ -1397,15 +1398,15 @@ msgstr ""
 msgid "The private key of the SSL certificate"
 msgstr ""
 
-#: internal/parser/shortcuts.go:94 internal/parser/volumes.go:234
-#: internal/parser/volumes.go:259 internal/parser/pts.go:62
-#: internal/parser/pts.go:120 internal/parser/pts.go:154
+#: 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:129
+#: internal/parser/releases.go:132
 msgid "The process name as defined in your Procfile, such as 'web'"
 msgstr ""
 
@@ -1498,7 +1499,7 @@ msgstr "The relative URL path for 'httpGet' actions"
 msgid "The relative URL path for 'httpGet' probes"
 msgstr ""
 
-#: internal/parser/releases.go:76 internal/parser/releases.go:130
+#: internal/parser/releases.go:76 internal/parser/releases.go:133
 msgid "The release of the application, such as '1'"
 msgstr ""
 
@@ -1519,6 +1520,10 @@ msgstr ""
 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 ""
@@ -1535,7 +1540,7 @@ msgstr ""
 msgid "The service's plan"
 msgstr ""
 
-#: internal/parser/apps.go:128 internal/parser/shortcuts.go:54
+#: internal/parser/shortcuts.go:54 internal/parser/apps.go:133
 msgid "The shell command to run inside the container"
 msgstr ""
 
@@ -1569,22 +1574,21 @@ msgstr ""
 msgid "The unique name of the route"
 msgstr ""
 
-#: internal/parser/builds.go:21 internal/parser/builds.go:64
-#: internal/parser/apps.go:86 internal/parser/apps.go:104
-#: internal/parser/apps.go:139 internal/parser/apps.go:165
-#: internal/parser/apps.go:186 internal/parser/autorollback.go:19
-#: internal/parser/timeouts.go:21 internal/parser/releases.go:32
-#: internal/parser/limits.go:21 internal/parser/autoscale.go:21
-#: internal/parser/ps.go:21 internal/parser/ps.go:112
-#: internal/parser/services.go:23 internal/parser/certs.go:22
-#: internal/parser/config.go:29 internal/parser/routes.go:27
-#: internal/parser/routing.go:20 internal/parser/labels.go:21
-#: internal/parser/gateways.go:21 internal/parser/volumes.go:22
-#: internal/parser/autodeploy.go:20 internal/parser/git.go:18
-#: internal/parser/perms.go:24 internal/parser/pts.go:21
-#: internal/parser/pts.go:134 internal/parser/tags.go:25
-#: internal/parser/registry.go:26 internal/parser/domains.go:21
-#: internal/parser/resources.go:24
+#: 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 ""
 
@@ -1592,25 +1596,17 @@ msgstr ""
 msgid "The uniquely identifiable name of the application"
 msgstr ""
 
-#: internal/parser/perms.go:69
-msgid "The user permissions (view,change,delete)"
-msgstr ""
-
-#: internal/parser/perms.go:99
-msgid "The user's permissions (view,change,delete)"
-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/users.go:78
-msgid "The username you want to disable"
-msgstr ""
-
-#: internal/parser/users.go:53
-msgid "The username you want to enable"
-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"
@@ -1653,10 +1649,10 @@ msgstr ""
 msgid "The version for which the timeout needs to be listed"
 msgstr ""
 
-#: internal/parser/apps.go:126 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/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 ""
 
@@ -1676,14 +1672,18 @@ msgstr ""
 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/releases.go:117 internal/parser/tokens.go:103
-#: internal/parser/pts.go:99
+#: internal/parser/pts.go:99 internal/parser/releases.go:117
+#: internal/parser/tokens.go:103
 msgid "To proceed, type \"yes\""
 msgstr ""
 
@@ -1692,13 +1692,13 @@ msgstr ""
 msgid "To proceed, type 'yes'"
 msgstr ""
 
-#: internal/parser/apps.go:178
-msgid "Transfer app ownership to another user"
-msgstr ""
+#: internal/parser/apps.go:184
+msgid "Transfer app to another workspace"
+msgstr "Transfer app to another workspace"
 
-#: internal/parser/apps.go:179
-msgid "Transfer application ownership to another user."
-msgstr ""
+#: 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"
@@ -1766,14 +1766,14 @@ msgid ""
 "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/perms.go:102
-msgid "Update a user permission"
-msgstr ""
-
 #: internal/parser/update.go:15 internal/parser/update.go:16
 msgid "Update workflow cli to latest release"
 msgstr ""
@@ -1790,11 +1790,11 @@ msgstr ""
 msgid "View autorollback info for an application"
 msgstr ""
 
-#: internal/parser/apps.go:79
+#: internal/parser/apps.go:84
 msgid "View info about an application"
 msgstr ""
 
-#: internal/parser/apps.go:140
+#: internal/parser/apps.go:145
 msgid "Volume mounts in format 'volume:path'"
 msgstr ""
 
diff --git a/pkg/i18n/translations/drycc/template.pot b/pkg/i18n/translations/drycc/template.pot
index 9db9cc791..0d644f033 100644
--- a/pkg/i18n/translations/drycc/template.pot
+++ b/pkg/i18n/translations/drycc/template.pot
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: \n"
 "Report-Msgid-Bugs-To: EMAIL\n"
-"POT-Creation-Date: 2025-12-08 16:06+0800\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"
@@ -49,10 +49,6 @@ msgstr ""
 msgid "Add a token for controller authentication"
 msgstr ""
 
-#: internal/parser/perms.go:72
-msgid "Add a user permissions"
-msgstr ""
-
 #: internal/parser/keys.go:57
 msgid "Add an SSH key"
 msgstr ""
@@ -125,7 +121,7 @@ msgstr ""
 msgid "CA key secret"
 msgstr ""
 
-#: internal/parser/pts.go:157
+#: internal/parser/pts.go:160
 msgid "Clean process types of not used"
 msgstr ""
 
@@ -133,7 +129,7 @@ msgstr ""
 msgid "Clear the current user session"
 msgstr ""
 
-#: internal/parser/apps.go:141
+#: internal/parser/apps.go:146
 msgid "Command execution timeout in seconds"
 msgstr ""
 
@@ -150,7 +146,7 @@ msgstr ""
 msgid "Configure automatic certificate management issuer details"
 msgstr ""
 
-#: internal/parser/apps.go:44
+#: internal/parser/apps.go:45
 msgid "Create a new application"
 msgstr ""
 
@@ -166,6 +162,10 @@ msgstr ""
 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 ""
@@ -174,7 +174,7 @@ msgstr ""
 msgid "Create service for an application"
 msgstr ""
 
-#: internal/parser/apps.go:45
+#: internal/parser/apps.go:46
 msgid ""
 "Creates a new application. If no <id> is provided, one will be generated "
 "automatically"
@@ -213,6 +213,10 @@ msgstr ""
 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 ""
@@ -229,7 +233,7 @@ msgstr ""
 msgid "Describe a process type"
 msgstr ""
 
-#: internal/parser/apps.go:159
+#: internal/parser/apps.go:164
 msgid "Destroy an application"
 msgstr ""
 
@@ -245,16 +249,6 @@ msgstr ""
 msgid "Dettach to gateway"
 msgstr ""
 
-#: internal/parser/users.go:81
-msgid "Disable a user"
-msgstr ""
-
-#: internal/parser/users.go:82
-msgid ""
-"Disable a user when his status is disabled.\n"
-"Requires admin privileges."
-msgstr ""
-
 #: internal/parser/autorollback.go:58
 msgid "Disable autorollback for an application"
 msgstr ""
@@ -287,7 +281,7 @@ msgstr ""
 msgid "Displays the currently logged in user"
 msgstr ""
 
-#: internal/parser/apps.go:56
+#: internal/parser/apps.go:58
 msgid "Do not create a 'drycc' git remote"
 msgstr ""
 
@@ -295,20 +289,14 @@ msgstr ""
 msgid "Drycc config path to the application"
 msgstr ""
 
-#: internal/parser/users.go:56
-msgid "Enable a user"
-msgstr ""
-
-#: internal/parser/users.go:57
-msgid ""
-"Enable a user when his status is disabled.\n"
-"Requires admin privileges."
-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 ""
@@ -384,6 +372,10 @@ msgstr ""
 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 ""
@@ -392,7 +384,7 @@ msgstr ""
 msgid "List SSL certificates for an application"
 msgstr ""
 
-#: internal/parser/apps.go:64
+#: internal/parser/apps.go:69
 msgid "List accessible applications"
 msgstr ""
 
@@ -412,14 +404,6 @@ msgstr ""
 msgid "List all available resource services"
 msgstr ""
 
-#: internal/parser/users.go:32
-msgid "List all registered users"
-msgstr ""
-
-#: internal/parser/perms.go:40
-msgid "List all user permissions"
-msgstr ""
-
 #: internal/parser/releases.go:53
 msgid "List an application's release history"
 msgstr ""
@@ -492,7 +476,7 @@ msgstr ""
 msgid "List volumes in the application"
 msgstr ""
 
-#: internal/parser/apps.go:65
+#: internal/parser/apps.go:70
 msgid "Lists applications visible to the current user"
 msgstr ""
 
@@ -520,6 +504,10 @@ msgstr ""
 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 ""
@@ -588,10 +576,6 @@ msgstr ""
 msgid "Manage labels of application"
 msgstr ""
 
-#: internal/parser/perms.go:17
-msgid "Manage permissions for applications"
-msgstr ""
-
 #: internal/parser/timeouts.go:15
 msgid "Manage pods termination grace period"
 msgstr ""
@@ -608,10 +592,6 @@ msgstr ""
 msgid "Manage processes inside an app"
 msgstr ""
 
-#: internal/parser/users.go:14
-msgid "Manage registered users"
-msgstr ""
-
 #: internal/parser/releases.go:24
 msgid "Manage releases of an application"
 msgstr ""
@@ -652,11 +632,15 @@ msgstr ""
 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:71
+#: internal/parser/apps.go:23 internal/parser/apps.go:76
 msgid "Maximum number of results to display"
 msgstr ""
 
@@ -680,7 +664,7 @@ msgstr ""
 msgid "Mount a volume to process of the application"
 msgstr ""
 
-#: internal/parser/apps.go:55 internal/parser/git.go:42
+#: internal/parser/git.go:42 internal/parser/apps.go:57
 msgid "Name of remote to create"
 msgstr ""
 
@@ -705,11 +689,11 @@ msgid ""
 "Number or name of the port to access on the pods targeted by the service"
 msgstr ""
 
-#: internal/parser/apps.go:97
+#: internal/parser/apps.go:102
 msgid "Open the application in a browser"
 msgstr ""
 
-#: internal/parser/apps.go:98
+#: internal/parser/apps.go:103
 msgid "Opens a URL to the application in the default browser"
 msgstr ""
 
@@ -761,6 +745,10 @@ msgstr ""
 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 ""
@@ -795,7 +783,7 @@ msgstr ""
 msgid "Prints info about a particular release"
 msgstr ""
 
-#: internal/parser/apps.go:80
+#: internal/parser/apps.go:85
 msgid "Prints info about the current application"
 msgstr ""
 
@@ -861,8 +849,8 @@ msgstr ""
 msgid "Remove a token for controller authentication"
 msgstr ""
 
-#: internal/parser/perms.go:129
-msgid "Remove a user permissions"
+#: internal/parser/workspaces.go:157
+msgid "Remove a user from a workspace"
 msgstr ""
 
 #: internal/parser/keys.go:81
@@ -901,15 +889,15 @@ msgstr ""
 msgid "Restart application process types"
 msgstr ""
 
-#: internal/parser/apps.go:142
+#: internal/parser/apps.go:147
 msgid "Retention time of records in seconds"
 msgstr ""
 
-#: internal/parser/releases.go:133
+#: internal/parser/releases.go:136
 msgid "Return to a previous release"
 msgstr ""
 
-#: internal/parser/releases.go:134
+#: internal/parser/releases.go:137
 msgid "Rolls back to a previous application release"
 msgstr ""
 
@@ -917,11 +905,11 @@ msgstr ""
 msgid "Rules-file is the file name of rule to apply"
 msgstr ""
 
-#: internal/parser/apps.go:131
+#: internal/parser/apps.go:136
 msgid "Run a command in an ephemeral app container"
 msgstr ""
 
-#: internal/parser/apps.go:132
+#: internal/parser/apps.go:137
 msgid "Runs a command inside an ephemeral app container"
 msgstr ""
 
@@ -1150,7 +1138,7 @@ msgstr ""
 msgid "Sets termination grace period for an application"
 msgstr ""
 
-#: cmd/root.go:75
+#: cmd/root.go:74
 msgid "Shortcut Commands:"
 msgstr ""
 
@@ -1166,12 +1154,16 @@ msgstr ""
 msgid "Simulate an update, only print the version info."
 msgstr ""
 
-#: internal/parser/apps.go:166
+#: 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 ""
@@ -1219,8 +1211,16 @@ msgid ""
 "'tcpSocket' accept a port number."
 msgstr ""
 
-#: internal/parser/apps.go:127 internal/parser/shortcuts.go:53
-#: internal/parser/volumes.go:235
+#: 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 ""
 
@@ -1278,17 +1278,17 @@ msgstr ""
 msgid "The limit plan id to apply to the process type"
 msgstr ""
 
-#: internal/parser/users.go:21 internal/parser/users.go:39
-#: internal/parser/releases.go:33 internal/parser/releases.go:62
-#: internal/parser/limits.go:127 internal/parser/limits.go:151
 #: internal/parser/certs.go:23 internal/parser/certs.go:48
-#: internal/parser/routes.go:28 internal/parser/routes.go:107
-#: internal/parser/gateways.go:22 internal/parser/gateways.go:44
-#: internal/parser/volumes.go:23 internal/parser/volumes.go:145
-#: internal/parser/perms.go:25 internal/parser/perms.go:47
-#: internal/parser/tokens.go:22 internal/parser/tokens.go:40
 #: 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 ""
 
@@ -1300,16 +1300,17 @@ msgstr ""
 msgid "The name of the cert to remove from the app"
 msgstr ""
 
-#: internal/parser/perms.go:68 internal/parser/perms.go:98
-#: internal/parser/perms.go:126
-msgid "The name of the user"
+#: 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/shortcuts.go:95 internal/parser/pts.go:121
+#: internal/parser/pts.go:121 internal/parser/shortcuts.go:95
 msgid "The number of processes"
 msgstr ""
 
@@ -1338,15 +1339,15 @@ msgstr ""
 msgid "The private key of the SSL certificate"
 msgstr ""
 
-#: internal/parser/shortcuts.go:94 internal/parser/volumes.go:234
-#: internal/parser/volumes.go:259 internal/parser/pts.go:62
-#: internal/parser/pts.go:120 internal/parser/pts.go:154
+#: 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:129
+#: internal/parser/releases.go:132
 msgid "The process name as defined in your Procfile, such as 'web'"
 msgstr ""
 
@@ -1439,7 +1440,7 @@ msgstr ""
 msgid "The relative URL path for 'httpGet' probes"
 msgstr ""
 
-#: internal/parser/releases.go:76 internal/parser/releases.go:130
+#: internal/parser/releases.go:76 internal/parser/releases.go:133
 msgid "The release of the application, such as '1'"
 msgstr ""
 
@@ -1460,6 +1461,10 @@ msgstr ""
 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 ""
@@ -1476,7 +1481,7 @@ msgstr ""
 msgid "The service's plan"
 msgstr ""
 
-#: internal/parser/apps.go:128 internal/parser/shortcuts.go:54
+#: internal/parser/shortcuts.go:54 internal/parser/apps.go:133
 msgid "The shell command to run inside the container"
 msgstr ""
 
@@ -1510,22 +1515,21 @@ msgstr ""
 msgid "The unique name of the route"
 msgstr ""
 
-#: internal/parser/builds.go:21 internal/parser/builds.go:64
-#: internal/parser/apps.go:86 internal/parser/apps.go:104
-#: internal/parser/apps.go:139 internal/parser/apps.go:165
-#: internal/parser/apps.go:186 internal/parser/autorollback.go:19
-#: internal/parser/timeouts.go:21 internal/parser/releases.go:32
-#: internal/parser/limits.go:21 internal/parser/autoscale.go:21
-#: internal/parser/ps.go:21 internal/parser/ps.go:112
-#: internal/parser/services.go:23 internal/parser/certs.go:22
-#: internal/parser/config.go:29 internal/parser/routes.go:27
-#: internal/parser/routing.go:20 internal/parser/labels.go:21
-#: internal/parser/gateways.go:21 internal/parser/volumes.go:22
-#: internal/parser/autodeploy.go:20 internal/parser/git.go:18
-#: internal/parser/perms.go:24 internal/parser/pts.go:21
-#: internal/parser/pts.go:134 internal/parser/tags.go:25
-#: internal/parser/registry.go:26 internal/parser/domains.go:21
-#: internal/parser/resources.go:24
+#: 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 ""
 
@@ -1533,24 +1537,16 @@ msgstr ""
 msgid "The uniquely identifiable name of the application"
 msgstr ""
 
-#: internal/parser/perms.go:69
-msgid "The user permissions (view,change,delete)"
-msgstr ""
-
-#: internal/parser/perms.go:99
-msgid "The user's permissions (view,change,delete)"
+#: 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/users.go:78
-msgid "The username you want to disable"
-msgstr ""
-
-#: internal/parser/users.go:53
-msgid "The username you want to enable"
+#: internal/parser/workspaces.go:154
+msgid "The username to remove from the workspace"
 msgstr ""
 
 #: internal/parser/timeouts.go:61
@@ -1594,10 +1590,10 @@ msgstr ""
 msgid "The version for which the timeout needs to be listed"
 msgstr ""
 
-#: internal/parser/apps.go:126 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/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 ""
 
@@ -1617,14 +1613,18 @@ msgstr ""
 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/releases.go:117 internal/parser/tokens.go:103
-#: internal/parser/pts.go:99
+#: internal/parser/pts.go:99 internal/parser/releases.go:117
+#: internal/parser/tokens.go:103
 msgid "To proceed, type \"yes\""
 msgstr ""
 
@@ -1633,12 +1633,12 @@ msgstr ""
 msgid "To proceed, type 'yes'"
 msgstr ""
 
-#: internal/parser/apps.go:178
-msgid "Transfer app ownership to another user"
+#: internal/parser/apps.go:184
+msgid "Transfer app to another workspace"
 msgstr ""
 
-#: internal/parser/apps.go:179
-msgid "Transfer application ownership to another user."
+#: internal/parser/apps.go:185
+msgid "Transfer application to another workspace."
 msgstr ""
 
 #: internal/parser/autoscale.go:100
@@ -1707,12 +1707,12 @@ msgid ""
 "controller"
 msgstr ""
 
-#: internal/parser/resources.go:181
-msgid "Update a resource from the application"
+#: internal/parser/workspaces.go:185
+msgid "Update a member's role and/or alerts in a workspace"
 msgstr ""
 
-#: internal/parser/perms.go:102
-msgid "Update a user permission"
+#: internal/parser/resources.go:181
+msgid "Update a resource from the application"
 msgstr ""
 
 #: internal/parser/update.go:15 internal/parser/update.go:16
@@ -1731,11 +1731,11 @@ msgstr ""
 msgid "View autorollback info for an application"
 msgstr ""
 
-#: internal/parser/apps.go:79
+#: internal/parser/apps.go:84
 msgid "View info about an application"
 msgstr ""
 
-#: internal/parser/apps.go:140
+#: internal/parser/apps.go:145
 msgid "Volume mounts in format 'volume:path'"
 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
index 279198927fb6591dc07ee12d9736443a255ae989..0e67e0eb7c71c89abb01bde815b9196851604bc4 100644
GIT binary patch
diff --git a/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po b/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po
index 35435ae03..0f476316a 100644
--- a/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po
+++ b/pkg/i18n/translations/drycc/zh_CN/LC_MESSAGES/cli.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: EMAIL\n"
-"POT-Creation-Date: 2025-12-08 16:06+0800\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"
@@ -49,10 +49,6 @@ msgstr "要执行的操作，'enable' 或 'disable'"
 msgid "Add a token for controller authentication"
 msgstr "添加用于控制器身份验证的令牌"
 
-#: internal/parser/perms.go:72
-msgid "Add a user permissions"
-msgstr "添加用户权限"
-
 #: internal/parser/keys.go:57
 msgid "Add an SSH key"
 msgstr "添加SSH密钥"
@@ -126,7 +122,7 @@ msgstr "CA密钥ID"
 msgid "CA key secret"
 msgstr "CA密钥机密"
 
-#: internal/parser/pts.go:157
+#: internal/parser/pts.go:160
 msgid "Clean process types of not used"
 msgstr "清理未使用的进程类型"
 
@@ -134,7 +130,7 @@ msgstr "清理未使用的进程类型"
 msgid "Clear the current user session"
 msgstr "清除当前用户会话"
 
-#: internal/parser/apps.go:141
+#: internal/parser/apps.go:146
 msgid "Command execution timeout in seconds"
 msgstr "命令执行超时时间（秒）"
 
@@ -151,7 +147,7 @@ msgstr "要解绑的域名通用名称"
 msgid "Configure automatic certificate management issuer details"
 msgstr "配置自动证书管理颁发者详情"
 
-#: internal/parser/apps.go:44
+#: internal/parser/apps.go:45
 msgid "Create a new application"
 msgstr "创建新应用"
 
@@ -167,6 +163,10 @@ msgstr "创建应用路由"
 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 "创建应用网关"
@@ -175,7 +175,7 @@ msgstr "创建应用网关"
 msgid "Create service for an application"
 msgstr "创建应用服务"
 
-#: internal/parser/apps.go:45
+#: internal/parser/apps.go:46
 msgid ""
 "Creates a new application. If no <id> is provided, one will be generated "
 "automatically"
@@ -217,6 +217,10 @@ msgstr "从应用中删除资源"
 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 "删除选定进程"
@@ -233,7 +237,7 @@ msgstr "按进程类型部署最新版本"
 msgid "Describe a process type"
 msgstr "描述进程类型定义"
 
-#: internal/parser/apps.go:159
+#: internal/parser/apps.go:164
 msgid "Destroy an application"
 msgstr "销毁应用"
 
@@ -249,18 +253,6 @@ msgstr "从域名解绑证书"
 msgid "Dettach to gateway"
 msgstr "从网关解绑"
 
-#: internal/parser/users.go:81
-msgid "Disable a user"
-msgstr "禁用用户"
-
-#: internal/parser/users.go:82
-msgid ""
-"Disable a user when his status is disabled.\n"
-"Requires admin privileges."
-msgstr ""
-"禁用用户账户（需管理员权限）\n"
-"仅当用户状态为启用时生效"
-
 #: internal/parser/autorollback.go:58
 msgid "Disable autorollback for an application"
 msgstr "禁用应用自动回滚"
@@ -293,7 +285,7 @@ msgstr "显示客户端版本信息"
 msgid "Displays the currently logged in user"
 msgstr "显示当前登录用户信息"
 
-#: internal/parser/apps.go:56
+#: internal/parser/apps.go:58
 msgid "Do not create a 'drycc' git remote"
 msgstr "不创建drycc Git远程仓库"
 
@@ -301,22 +293,14 @@ msgstr "不创建drycc Git远程仓库"
 msgid "Drycc config path to the application"
 msgstr "Drycc应用配置路径"
 
-#: internal/parser/users.go:56
-msgid "Enable a user"
-msgstr "启用用户"
-
-#: internal/parser/users.go:57
-msgid ""
-"Enable a user when his status is disabled.\n"
-"Requires admin privileges."
-msgstr ""
-"启用已禁用用户（需管理员权限）\n"
-"仅当用户状态为禁用时生效"
-
 #: 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 "启用应用路由功能"
@@ -397,6 +381,10 @@ msgstr "获取应用路由规则"
 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密钥"
@@ -405,7 +393,7 @@ msgstr "列出登录用户的SSH密钥"
 msgid "List SSL certificates for an application"
 msgstr "列出应用SSL证书"
 
-#: internal/parser/apps.go:64
+#: internal/parser/apps.go:69
 msgid "List accessible applications"
 msgstr "列出可访问应用"
 
@@ -425,14 +413,6 @@ msgstr "列出资源服务所有可用方案"
 msgid "List all available resource services"
 msgstr "列出所有资源服务"
 
-#: internal/parser/users.go:32
-msgid "List all registered users"
-msgstr "列出所有注册用户"
-
-#: internal/parser/perms.go:40
-msgid "List all user permissions"
-msgstr "列出所有用户权限"
-
 #: internal/parser/releases.go:53
 msgid "List an application's release history"
 msgstr "列出应用版本历史"
@@ -505,7 +485,7 @@ msgstr "列出应用标签"
 msgid "List volumes in the application"
 msgstr "列出应用卷"
 
-#: internal/parser/apps.go:65
+#: internal/parser/apps.go:70
 msgid "Lists applications visible to the current user"
 msgstr "列出当前用户可见的应用"
 
@@ -533,6 +513,10 @@ msgstr "列出应用版本历史记录"
 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 "通过控制器身份验证登录"
@@ -601,10 +585,6 @@ msgstr "管理应用Git配置"
 msgid "Manage labels of application"
 msgstr "管理应用标签"
 
-#: internal/parser/perms.go:17
-msgid "Manage permissions for applications"
-msgstr "管理应用权限"
-
 #: internal/parser/timeouts.go:15
 msgid "Manage pods termination grace period"
 msgstr "管理Pod终止宽限期"
@@ -621,10 +601,6 @@ msgstr "管理应用进程类型"
 msgid "Manage processes inside an app"
 msgstr "管理应用进程"
 
-#: internal/parser/users.go:14
-msgid "Manage registered users"
-msgstr "管理注册用户"
-
 #: internal/parser/releases.go:24
 msgid "Manage releases of an application"
 msgstr "管理应用版本"
@@ -665,11 +641,15 @@ msgstr "管理用户令牌"
 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:71
+#: internal/parser/apps.go:23 internal/parser/apps.go:76
 msgid "Maximum number of results to display"
 msgstr "最多显示结果数量"
 
@@ -693,7 +673,7 @@ msgstr "探测失败后需连续成功的最小次数（判定为成功）"
 msgid "Mount a volume to process of the application"
 msgstr "将卷挂载到应用进程"
 
-#: internal/parser/apps.go:55 internal/parser/git.go:42
+#: internal/parser/git.go:42 internal/parser/apps.go:57
 msgid "Name of remote to create"
 msgstr "要创建的远程名称"
 
@@ -718,11 +698,11 @@ msgid ""
 "Number or name of the port to access on the pods targeted by the service"
 msgstr "服务目标Pod上要访问的端口号或名称"
 
-#: internal/parser/apps.go:97
+#: internal/parser/apps.go:102
 msgid "Open the application in a browser"
 msgstr "在浏览器中打开应用"
 
-#: internal/parser/apps.go:98
+#: internal/parser/apps.go:103
 msgid "Opens a URL to the application in the default browser"
 msgstr "通过默认浏览器打开应用URL"
 
@@ -774,6 +754,10 @@ msgstr "显示特定版本信息"
 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 "显示特定构建版本的进程信息"
@@ -808,7 +792,7 @@ msgstr "显示应用标签列表"
 msgid "Prints info about a particular release"
 msgstr "显示特定版本信息"
 
-#: internal/parser/apps.go:80
+#: internal/parser/apps.go:85
 msgid "Prints info about the current application"
 msgstr "显示当前应用信息"
 
@@ -874,9 +858,9 @@ msgstr "从应用移除路由"
 msgid "Remove a token for controller authentication"
 msgstr "移除控制器认证令牌"
 
-#: internal/parser/perms.go:129
-msgid "Remove a user permissions"
-msgstr "移除用户权限"
+#: internal/parser/workspaces.go:157
+msgid "Remove a user from a workspace"
+msgstr "从工作空间移除用户"
 
 #: internal/parser/keys.go:81
 msgid "Remove an SSH key"
@@ -914,15 +898,15 @@ msgstr "重启应用或进程类型"
 msgid "Restart application process types"
 msgstr "重启应用进程类型"
 
-#: internal/parser/apps.go:142
+#: internal/parser/apps.go:147
 msgid "Retention time of records in seconds"
 msgstr "日志记录保留时间（秒）"
 
-#: internal/parser/releases.go:133
+#: internal/parser/releases.go:136
 msgid "Return to a previous release"
 msgstr "回滚到之前版本"
 
-#: internal/parser/releases.go:134
+#: internal/parser/releases.go:137
 msgid "Rolls back to a previous application release"
 msgstr "回滚到应用之前的版本"
 
@@ -930,11 +914,11 @@ msgstr "回滚到应用之前的版本"
 msgid "Rules-file is the file name of rule to apply"
 msgstr "要应用的规则文件名"
 
-#: internal/parser/apps.go:131
+#: internal/parser/apps.go:136
 msgid "Run a command in an ephemeral app container"
 msgstr "在临时应用容器中运行命令"
 
-#: internal/parser/apps.go:132
+#: internal/parser/apps.go:137
 msgid "Runs a command inside an ephemeral app container"
 msgstr "在临时应用容器内部运行命令"
 
@@ -1238,7 +1222,7 @@ msgstr ""
 msgid "Sets termination grace period for an application"
 msgstr "设置应用终止宽限期"
 
-#: cmd/root.go:75
+#: cmd/root.go:74
 msgid "Shortcut Commands:"
 msgstr "快捷命令："
 
@@ -1254,7 +1238,7 @@ msgstr "显示资源附加详情"
 msgid "Simulate an update, only print the version info."
 msgstr "模拟更新，仅显示版本信息"
 
-#: internal/parser/apps.go:166
+#: internal/parser/apps.go:171
 msgid ""
 "Skips the prompt for the application name. \n"
 "<app> is the uniquely identifiable name for the application"
@@ -1262,6 +1246,10 @@ 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 "指定是否实时流式输出日志"
@@ -1314,8 +1302,16 @@ msgstr ""
 "                  'exec' 接受参数列表；'sleep' 接受以秒为单位的时长；\n"
 "                  'httpGet' 和 'tcpSocket' 需要指定端口号。"
 
-#: internal/parser/apps.go:127 internal/parser/shortcuts.go:53
-#: internal/parser/volumes.go:235
+#: 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 "文件系统路径"
 
@@ -1374,17 +1370,17 @@ msgstr "生命周期处理器，如 'postStart' 或 'preStop'"
 msgid "The limit plan id to apply to the process type"
 msgstr "要应用到进程类型的配额方案ID"
 
-#: internal/parser/users.go:21 internal/parser/users.go:39
-#: internal/parser/releases.go:33 internal/parser/releases.go:62
-#: internal/parser/limits.go:127 internal/parser/limits.go:151
 #: internal/parser/certs.go:23 internal/parser/certs.go:48
-#: internal/parser/routes.go:28 internal/parser/routes.go:107
-#: internal/parser/gateways.go:22 internal/parser/gateways.go:44
-#: internal/parser/volumes.go:23 internal/parser/volumes.go:145
-#: internal/parser/perms.go:25 internal/parser/perms.go:47
-#: internal/parser/tokens.go:22 internal/parser/tokens.go:40
 #: 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 "最多显示结果数量"
 
@@ -1396,16 +1392,17 @@ msgstr "要查询信息的证书名称"
 msgid "The name of the cert to remove from the app"
 msgstr "要从应用移除的证书名称"
 
-#: internal/parser/perms.go:68 internal/parser/perms.go:98
-#: internal/parser/perms.go:126
-msgid "The name of the user"
-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/shortcuts.go:95 internal/parser/pts.go:121
+#: internal/parser/pts.go:121 internal/parser/shortcuts.go:95
 msgid "The number of processes"
 msgstr "进程数量"
 
@@ -1434,15 +1431,15 @@ msgstr "服务将暴露的端口"
 msgid "The private key of the SSL certificate"
 msgstr "SSL证书私钥"
 
-#: internal/parser/shortcuts.go:94 internal/parser/volumes.go:234
-#: internal/parser/volumes.go:259 internal/parser/pts.go:62
-#: internal/parser/pts.go:120 internal/parser/pts.go:154
+#: 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:129
+#: internal/parser/releases.go:132
 msgid "The process name as defined in your Procfile, such as 'web'"
 msgstr "Procfile中定义的进程名称（如 'web'）"
 
@@ -1505,15 +1502,15 @@ 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"
+msgstr "要应用生命周期处理器的进程类型"
 
 #: 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"
+msgstr "要清除生命周期处理器的进程类型"
 
 #: 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"
+msgstr "要列出生命周期的进程类型"
 
 #: internal/parser/config.go:219
 msgid "The ptype that requires attach configurations"
@@ -1535,7 +1532,7 @@ msgstr "'httpGet' 操作的相对URL路径"
 msgid "The relative URL path for 'httpGet' probes"
 msgstr "HTTP探测的相对URL路径"
 
-#: internal/parser/releases.go:76 internal/parser/releases.go:130
+#: internal/parser/releases.go:76 internal/parser/releases.go:133
 msgid "The release of the application, such as '1'"
 msgstr "应用版本（如 '1'）"
 
@@ -1556,6 +1553,10 @@ msgstr "资源实例参数值"
 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）"
@@ -1572,7 +1573,7 @@ msgstr "服务名称"
 msgid "The service's plan"
 msgstr "服务配额方案"
 
-#: internal/parser/apps.go:128 internal/parser/shortcuts.go:54
+#: internal/parser/shortcuts.go:54 internal/parser/apps.go:133
 msgid "The shell command to run inside the container"
 msgstr "容器内要执行的Shell命令"
 
@@ -1606,22 +1607,21 @@ msgstr "网关唯一名称"
 msgid "The unique name of the route"
 msgstr "路由唯一名称"
 
-#: internal/parser/builds.go:21 internal/parser/builds.go:64
-#: internal/parser/apps.go:86 internal/parser/apps.go:104
-#: internal/parser/apps.go:139 internal/parser/apps.go:165
-#: internal/parser/apps.go:186 internal/parser/autorollback.go:19
-#: internal/parser/timeouts.go:21 internal/parser/releases.go:32
-#: internal/parser/limits.go:21 internal/parser/autoscale.go:21
-#: internal/parser/ps.go:21 internal/parser/ps.go:112
-#: internal/parser/services.go:23 internal/parser/certs.go:22
-#: internal/parser/config.go:29 internal/parser/routes.go:27
-#: internal/parser/routing.go:20 internal/parser/labels.go:21
-#: internal/parser/gateways.go:21 internal/parser/volumes.go:22
-#: internal/parser/autodeploy.go:20 internal/parser/git.go:18
-#: internal/parser/perms.go:24 internal/parser/pts.go:21
-#: internal/parser/pts.go:134 internal/parser/tags.go:25
-#: internal/parser/registry.go:26 internal/parser/domains.go:21
-#: internal/parser/resources.go:24
+#: 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 "应用唯一标识名称"
 
@@ -1629,25 +1629,17 @@ msgstr "应用唯一标识名称"
 msgid "The uniquely identifiable name of the application"
 msgstr "应用唯一名称"
 
-#: internal/parser/perms.go:69
-msgid "The user permissions (view,change,delete)"
-msgstr "用户权限（view、change、delete）"
-
-#: internal/parser/perms.go:99
-msgid "The user's permissions (view,change,delete)"
-msgstr "用户的权限（view、change、delete）"
+#: 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/users.go:78
-msgid "The username you want to disable"
-msgstr "要禁用的用户名"
-
-#: internal/parser/users.go:53
-msgid "The username you want to enable"
-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"
@@ -1672,7 +1664,7 @@ 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"
+msgstr "要列出生命周期的版本"
 
 #: internal/parser/limits.go:22 internal/parser/limits.go:47
 msgid "The version for which the limit needs to be listed"
@@ -1690,10 +1682,10 @@ msgstr "要查询对应版本的标签"
 msgid "The version for which the timeout needs to be listed"
 msgstr "要查询对应版本的超时设置"
 
-#: internal/parser/apps.go:126 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/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 "卷名称"
 
@@ -1713,14 +1705,18 @@ msgstr "卷容量（如 '500G'）"
 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/releases.go:117 internal/parser/tokens.go:103
-#: internal/parser/pts.go:99
+#: internal/parser/pts.go:99 internal/parser/releases.go:117
+#: internal/parser/tokens.go:103
 msgid "To proceed, type \"yes\""
 msgstr "继续操作请输入 \"yes\""
 
@@ -1729,13 +1725,13 @@ msgstr "继续操作请输入 \"yes\""
 msgid "To proceed, type 'yes'"
 msgstr "继续操作请输入 'yes'"
 
-#: internal/parser/apps.go:178
-msgid "Transfer app ownership to another user"
-msgstr "转移应用所有权给其他用户"
+#: internal/parser/apps.go:184
+msgid "Transfer app to another workspace"
+msgstr "将应用转移到另一个工作空间"
 
-#: internal/parser/apps.go:179
-msgid "Transfer application ownership to another user."
-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"
@@ -1803,14 +1799,14 @@ msgid ""
 "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/perms.go:102
-msgid "Update a user permission"
-msgstr "更新用户权限"
-
 #: internal/parser/update.go:15 internal/parser/update.go:16
 msgid "Update workflow cli to latest release"
 msgstr "更新Workflow CLI到最新版本"
@@ -1827,11 +1823,11 @@ msgstr "查看应用路由能力信息"
 msgid "View autorollback info for an application"
 msgstr "查看应用自动回滚信息"
 
-#: internal/parser/apps.go:79
+#: internal/parser/apps.go:84
 msgid "View info about an application"
 msgstr "查看应用信息"
 
-#: internal/parser/apps.go:140
+#: internal/parser/apps.go:145
 msgid "Volume mounts in format 'volume:path'"
 msgstr "卷挂载格式 '卷名:挂载路径'"
 
