@atomservice/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/atom.ts ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env bun
2
+ import { UserError } from "@atomservice/core"
3
+ import { type ArgsDef, type CommandDef, defineCommand, renderUsage, runCommand, runMain } from "citty"
4
+ import pc from "picocolors"
5
+ import { addCmd } from "../src/add/add.cmd.ts"
6
+ import { initCmd } from "../src/init/init.cmd.ts"
7
+ import { runCmd } from "../src/run/index.ts"
8
+ import { buildServiceSubCommands } from "../src/service/index.ts"
9
+
10
+ async function showUsage<T extends ArgsDef>(cmd: CommandDef<T>, parent?: CommandDef<T>) {
11
+ const raw = await renderUsage(cmd, parent)
12
+ const zh = raw
13
+ .replace(/USAGE/g, "用法")
14
+ .replace(/COMMANDS/g, "命令")
15
+ .replace(/ARGUMENTS/g, "参数")
16
+ .replace(/OPTIONS/g, "选项")
17
+ .replace(/Use .+ for more information about a command\./g, "运行 atom <命令> --help 查看命令详情。")
18
+ console.log(`${zh}\n`)
19
+ }
20
+
21
+ const main = defineCommand({
22
+ meta: { name: "atom", version: "0.0.1", description: "原子化自托管服务编排工具" },
23
+ subCommands: async () => ({
24
+ init: initCmd,
25
+ add: addCmd,
26
+ run: runCmd,
27
+ ...(await buildServiceSubCommands()),
28
+ }),
29
+ run: async () => {
30
+ const subcmd = process.argv.slice(2).find((a) => !a.startsWith("-"))
31
+ if (!subcmd) await showUsage(main)
32
+ },
33
+ })
34
+
35
+ const argv = process.argv.slice(2)
36
+ const wantsHelpOrVersion =
37
+ argv.includes("--help") || argv.includes("-h") || (argv.length === 1 && argv[0] === "--version")
38
+
39
+ if (wantsHelpOrVersion) {
40
+ // help / version 由 citty 自带逻辑处理
41
+ await runMain(main, { showUsage })
42
+ } else {
43
+ try {
44
+ await runCommand(main, { rawArgs: argv })
45
+ } catch (err) {
46
+ if (err instanceof UserError) {
47
+ console.error(`\n${pc.red("✗")} ${err.message}`)
48
+ if (err.hint) console.error(pc.dim(` ${err.hint}`))
49
+ } else {
50
+ console.error(`\n${pc.red("✗")} ${err instanceof Error ? err.message : String(err)}`)
51
+ console.error(pc.dim(" 如果这看起来是个 bug,可加 --verbose 查看详细堆栈"))
52
+ if (process.env.ATOMSERVICE_VERBOSE && err instanceof Error && err.stack) {
53
+ console.error(pc.dim(err.stack))
54
+ }
55
+ }
56
+ process.exit(1)
57
+ }
58
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@atomservice/cli",
3
+ "version": "0.1.0",
4
+ "description": "atomservice 命令行工具(atom):init / run / add / service",
5
+ "type": "module",
6
+ "author": "openorson",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/openorson/atomservice.git",
10
+ "directory": "packages/cli"
11
+ },
12
+ "bugs": "https://github.com/openorson/atomservice/issues",
13
+ "homepage": "https://github.com/openorson/atomservice/tree/main/packages/cli#readme",
14
+ "keywords": [
15
+ "atomservice",
16
+ "cli",
17
+ "self-hosted",
18
+ "podman",
19
+ "bun"
20
+ ],
21
+ "engines": {
22
+ "bun": ">=1.0.0"
23
+ },
24
+ "files": [
25
+ "src",
26
+ "bin"
27
+ ],
28
+ "bin": {
29
+ "atom": "./bin/atom.ts"
30
+ },
31
+ "dependencies": {
32
+ "@clack/prompts": "^1.4.0",
33
+ "@atomservice/core": "workspace:*",
34
+ "citty": "^0.2.2",
35
+ "cli-table3": "^0.6.5",
36
+ "log-update": "^8.0.0",
37
+ "ora": "^9.4.0",
38
+ "picocolors": "^1.1.1"
39
+ }
40
+ }
@@ -0,0 +1,67 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+ import { ATOMSERVICE_DIR } from "@atomservice/core"
4
+ import { cancel, confirm, isCancel, multiselect, spinner } from "@clack/prompts"
5
+ import { defineCommand } from "citty"
6
+ import pc from "picocolors"
7
+ import { OFFICIAL_SERVICES } from "../cli.consts.ts"
8
+ import { ensureLocalLink } from "../cli.link.ts"
9
+
10
+ async function installService(name: string) {
11
+ const pkgName = `@atomservice/${name}`
12
+ const symlinkPath = path.join(ATOMSERVICE_DIR, "node_modules", "@atomservice", name)
13
+
14
+ const existing = fs.lstatSync(symlinkPath, { throwIfNoEntry: false })
15
+ if (existing) {
16
+ try {
17
+ const resolved = fs.realpathSync(symlinkPath)
18
+ if (fs.existsSync(path.join(resolved, "package.json"))) return
19
+ } catch {
20
+ fs.rmSync(symlinkPath, { force: true, recursive: true })
21
+ }
22
+ }
23
+
24
+ const s = spinner()
25
+ s.start(`安装 ${pkgName}…`)
26
+
27
+ const installed = await Bun.$`bun add --cwd ${ATOMSERVICE_DIR} ${pkgName}`.quiet().nothrow()
28
+ if (installed.exitCode === 0) {
29
+ s.stop(pc.green(`${pkgName} 已安装`))
30
+ return
31
+ }
32
+
33
+ const linked = ensureLocalLink(name)
34
+ s.stop(linked ? pc.green(`${pkgName} 已就绪(开发模式)`) : pc.red(`${pkgName} 安装失败`))
35
+ }
36
+
37
+ export const addCmd = defineCommand({
38
+ meta: { name: "add", description: "安装原子服务" },
39
+ run: async () => {
40
+ const selected = await multiselect({
41
+ message: "选择要安装的原子服务(空格选择,回车确认)",
42
+ options: OFFICIAL_SERVICES,
43
+ required: true,
44
+ })
45
+
46
+ if (isCancel(selected)) {
47
+ cancel("已取消")
48
+ return
49
+ }
50
+
51
+ const ok = await confirm({ message: `安装 ${(selected as string[]).length} 个原子服务?` })
52
+
53
+ if (isCancel(ok) || !ok) {
54
+ cancel("已取消")
55
+ return
56
+ }
57
+
58
+ for (const name of selected as string[]) {
59
+ await installService(name)
60
+ }
61
+
62
+ console.log("")
63
+ console.log(
64
+ `${pc.green("✔")} 完成,在 ${pc.bold("~/.atomservice/atom.config.ts")} 中引入原子服务后运行 ${pc.cyan("atom run up")}`,
65
+ )
66
+ },
67
+ })
@@ -0,0 +1,14 @@
1
+ import path from "node:path"
2
+ import { ATOMSERVICE_DIR } from "@atomservice/core"
3
+
4
+ export const CONFIG_PATH = path.join(ATOMSERVICE_DIR, "atom.config.ts")
5
+
6
+ export const OFFICIAL_SERVICES: Array<{ value: string; label: string; hint: string }> = [
7
+ { value: "service-redis", label: "service-redis", hint: "Redis 缓存" },
8
+ { value: "service-postgres", label: "service-postgres", hint: "PostgreSQL 数据库" },
9
+ { value: "service-gateway", label: "service-gateway", hint: "反向代理 + HTTPS" },
10
+ { value: "service-gitea", label: "service-gitea", hint: "Gitea Git 服务" },
11
+ { value: "service-config", label: "service-config", hint: "通用配置中心" },
12
+ { value: "service-app", label: "service-app", hint: "容器化后端应用部署" },
13
+ { value: "service-site", label: "service-site", hint: "静态网站部署" },
14
+ ]
@@ -0,0 +1,41 @@
1
+ import fs from "node:fs"
2
+ import os from "node:os"
3
+ import path from "node:path"
4
+
5
+ function findMonorepoRoot(): string | null {
6
+ const scriptDir = import.meta.dirname
7
+ const candidates = [
8
+ path.resolve(scriptDir, "../../.."),
9
+ path.resolve(scriptDir, "../../../.."),
10
+ path.resolve(scriptDir, "../../../../.."),
11
+ ]
12
+ for (const candidate of candidates) {
13
+ if (fs.existsSync(path.join(candidate, "packages", "core", "package.json"))) {
14
+ return candidate
15
+ }
16
+ }
17
+ return null
18
+ }
19
+
20
+ export function ensureLocalLink(packageName: string): boolean {
21
+ const monorepoRoot = findMonorepoRoot()
22
+ if (!monorepoRoot) return false
23
+
24
+ const sourceDir = path.join(monorepoRoot, "packages", packageName)
25
+ if (!fs.existsSync(path.join(sourceDir, "package.json"))) return false
26
+
27
+ const atomserviceDir = path.join(os.homedir(), ".atomservice")
28
+ const targetDir = path.join(atomserviceDir, "node_modules", "@atomservice")
29
+ const symlinkPath = path.join(targetDir, packageName)
30
+
31
+ const existing = fs.lstatSync(symlinkPath, { throwIfNoEntry: false })
32
+ if (existing?.isSymbolicLink() || existing?.isDirectory()) {
33
+ const resolved = fs.realpathSync(symlinkPath)
34
+ if (resolved === sourceDir) return true
35
+ fs.rmSync(symlinkPath, { force: true, recursive: true })
36
+ }
37
+
38
+ fs.mkdirSync(targetDir, { recursive: true })
39
+ fs.symlinkSync(sourceDir, symlinkPath, "dir")
40
+ return true
41
+ }
@@ -0,0 +1,72 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+ import { ATOMSERVICE_DIR } from "@atomservice/core"
4
+ import { spinner } from "@clack/prompts"
5
+ import { defineCommand } from "citty"
6
+ import pc from "picocolors"
7
+ import { CONFIG_PATH } from "../cli.consts.ts"
8
+ import { ensureLocalLink } from "../cli.link.ts"
9
+
10
+ async function ensureDir() {
11
+ fs.mkdirSync(ATOMSERVICE_DIR, { recursive: true })
12
+
13
+ const pkgPath = path.join(ATOMSERVICE_DIR, "package.json")
14
+ if (!fs.existsSync(pkgPath)) {
15
+ await Bun.write(
16
+ pkgPath,
17
+ `${JSON.stringify({ name: "atomservice-config", private: true, type: "module" }, null, 2)}\n`,
18
+ )
19
+ }
20
+ }
21
+
22
+ async function ensureCore() {
23
+ const symlinkPath = path.join(ATOMSERVICE_DIR, "node_modules", "@atomservice", "core")
24
+
25
+ const existing = fs.lstatSync(symlinkPath, { throwIfNoEntry: false })
26
+ if (existing) {
27
+ try {
28
+ const resolved = fs.realpathSync(symlinkPath)
29
+ if (fs.existsSync(path.join(resolved, "package.json"))) return
30
+ } catch {
31
+ fs.rmSync(symlinkPath, { force: true, recursive: true })
32
+ }
33
+ }
34
+
35
+ const s = spinner()
36
+ s.start("安装 @atomservice/core…")
37
+
38
+ const installed = await Bun.$`bun add --cwd ${ATOMSERVICE_DIR} @atomservice/core`.quiet().nothrow()
39
+ if (installed.exitCode === 0) {
40
+ s.stop(pc.green("@atomservice/core 已安装"))
41
+ return
42
+ }
43
+
44
+ const linked = ensureLocalLink("core")
45
+ s.stop(linked ? pc.green("@atomservice/core 已就绪(开发模式)") : pc.red("@atomservice/core 安装失败"))
46
+ }
47
+
48
+ export const initCmd = defineCommand({
49
+ meta: { name: "init", description: `初始化 atomservice 项目(${ATOMSERVICE_DIR})` },
50
+ run: async () => {
51
+ await ensureDir()
52
+ await ensureCore()
53
+
54
+ if (fs.existsSync(CONFIG_PATH)) {
55
+ console.log(pc.dim("已就绪,无需重复初始化"))
56
+ console.log(pc.dim(`配置文件:${CONFIG_PATH}`))
57
+ return
58
+ }
59
+
60
+ const content = `import { defineConfig } from "@atomservice/core"
61
+
62
+ export default defineConfig({
63
+ root: "${ATOMSERVICE_DIR}",
64
+ services: [],
65
+ })
66
+ `
67
+
68
+ await Bun.write(CONFIG_PATH, content)
69
+ console.log(`${pc.green("✔")} 已初始化 ${pc.bold(ATOMSERVICE_DIR)}`)
70
+ console.log(pc.dim(" 编辑 atom.config.ts 添加原子服务后运行 atom run up"))
71
+ },
72
+ })
@@ -0,0 +1,13 @@
1
+ import { defineCommand } from "citty"
2
+ import { downCmd } from "./run.down.ts"
3
+ import { statusCmd } from "./run.status.ts"
4
+ import { upCmd } from "./run.up.ts"
5
+
6
+ export const runCmd = defineCommand({
7
+ meta: { name: "run", description: "管理原子服务生命周期" },
8
+ subCommands: {
9
+ up: upCmd,
10
+ down: downCmd,
11
+ status: statusCmd,
12
+ },
13
+ })
@@ -0,0 +1,36 @@
1
+ import { initForCli, loadConfig, selectServices } from "@atomservice/core"
2
+ import { defineCommand } from "citty"
3
+ import pc from "picocolors"
4
+ import { runAtom } from "./run.utils.ts"
5
+
6
+ export const downCmd = defineCommand({
7
+ meta: { name: "down", description: "反序停止原子服务" },
8
+ args: {
9
+ services: {
10
+ type: "positional",
11
+ required: false,
12
+ description: "要停止的服务名(可多个),不填则停止全部",
13
+ },
14
+ only: { type: "boolean", description: "仅停止指定服务,不自动停止其下游", default: false },
15
+ verbose: { type: "boolean", alias: "v", description: "显示详细命令输出", default: false },
16
+ },
17
+ run: async ({ args }) => {
18
+ if (args.verbose) process.env.ATOMSERVICE_VERBOSE = "1"
19
+ const config = await loadConfig()
20
+ let services = await initForCli(config)
21
+
22
+ if (services.length === 0) {
23
+ console.log(pc.dim("没有配置原子服务,请编辑 ~/.atomservice/atom.config.ts"))
24
+ return
25
+ }
26
+
27
+ const names = (args._ ?? []).filter((a): a is string => typeof a === "string" && a.length > 0)
28
+ if (names.length > 0) {
29
+ services = selectServices(services, names, { direction: "dependents", only: args.only })
30
+ }
31
+
32
+ for (const { serviceId, state, logger } of [...services].reverse()) {
33
+ await runAtom(serviceId, state, logger, (s) => s.downHandlers)
34
+ }
35
+ },
36
+ })
@@ -0,0 +1,70 @@
1
+ import type { HealthStatus } from "@atomservice/core"
2
+ import { initForCli, loadConfig } from "@atomservice/core"
3
+ import { defineCommand } from "citty"
4
+ import Table from "cli-table3"
5
+ import logUpdate from "log-update"
6
+ import pc from "picocolors"
7
+
8
+ function renderTable(results: Map<string, HealthStatus>): string {
9
+ const table = new Table({
10
+ head: [pc.bold("原子服务"), pc.bold("状态"), pc.bold("信息")],
11
+ })
12
+ for (const [id, health] of results) {
13
+ const icon =
14
+ health.status === "healthy"
15
+ ? pc.green("✓ healthy")
16
+ : health.status === "unhealthy"
17
+ ? pc.red("✗ unhealthy")
18
+ : pc.yellow("? unknown")
19
+ table.push([id, icon, health.message ?? ""])
20
+ }
21
+ return table.toString()
22
+ }
23
+
24
+ export const statusCmd = defineCommand({
25
+ meta: { name: "status", description: "查看所有原子服务健康状态" },
26
+ args: {
27
+ watch: {
28
+ type: "boolean",
29
+ description: "持续监控,每 5 秒刷新",
30
+ default: false,
31
+ alias: "w",
32
+ },
33
+ verbose: { type: "boolean", alias: "v", description: "显示详细命令输出", default: false },
34
+ },
35
+ run: async ({ args }) => {
36
+ if (args.verbose) process.env.ATOMSERVICE_VERBOSE = "1"
37
+ const config = await loadConfig()
38
+ const services = await initForCli(config)
39
+
40
+ if (services.length === 0) {
41
+ console.log(pc.dim("没有配置原子服务,请编辑 ~/.atomservice/atom.config.ts"))
42
+ return
43
+ }
44
+
45
+ const refresh = async (): Promise<string> => {
46
+ const results = new Map<string, HealthStatus>()
47
+ await Promise.all(
48
+ services.map(async ({ serviceId, state }) => {
49
+ const status = state.healthHandler ? await state.healthHandler() : ({ status: "unknown" } as const)
50
+ results.set(serviceId, status)
51
+ }),
52
+ )
53
+ return renderTable(results)
54
+ }
55
+
56
+ if (args.watch) {
57
+ logUpdate(await refresh())
58
+ const interval = setInterval(async () => {
59
+ logUpdate(await refresh())
60
+ }, 5000)
61
+ process.on("SIGINT", () => {
62
+ clearInterval(interval)
63
+ logUpdate.done()
64
+ process.exit(0)
65
+ })
66
+ } else {
67
+ console.log(await refresh())
68
+ }
69
+ },
70
+ })
@@ -0,0 +1,38 @@
1
+ import { initForCli, loadConfig, selectServices } from "@atomservice/core"
2
+ import { defineCommand } from "citty"
3
+ import pc from "picocolors"
4
+ import { runAtom } from "./run.utils.ts"
5
+
6
+ export const upCmd = defineCommand({
7
+ meta: { name: "up", description: "按依赖顺序启动原子服务" },
8
+ args: {
9
+ services: {
10
+ type: "positional",
11
+ required: false,
12
+ description: "要启动的服务名(可多个),不填则启动全部",
13
+ },
14
+ only: { type: "boolean", description: "仅启动指定服务,不自动启动其依赖", default: false },
15
+ verbose: { type: "boolean", alias: "v", description: "显示详细命令输出", default: false },
16
+ },
17
+ run: async ({ args }) => {
18
+ if (args.verbose) process.env.ATOMSERVICE_VERBOSE = "1"
19
+ const config = await loadConfig()
20
+ let services = await initForCli(config)
21
+
22
+ if (services.length === 0) {
23
+ console.log(pc.dim("没有配置原子服务,请编辑 ~/.atomservice/atom.config.ts"))
24
+ return
25
+ }
26
+
27
+ const names = (args._ ?? []).filter((a): a is string => typeof a === "string" && a.length > 0)
28
+ if (names.length > 0) {
29
+ services = selectServices(services, names, { direction: "deps", only: args.only })
30
+ }
31
+
32
+ await Bun.$`podman network create atomservice`.quiet().nothrow()
33
+
34
+ for (const { serviceId, state, logger } of services) {
35
+ await runAtom(serviceId, state, logger, (s) => s.upHandlers)
36
+ }
37
+ },
38
+ })
@@ -0,0 +1,76 @@
1
+ import type { Logger, ServiceLifecycle } from "@atomservice/core"
2
+ import { UserError } from "@atomservice/core"
3
+ import { spinner } from "@clack/prompts"
4
+ import pc from "picocolors"
5
+
6
+ export function elapsed(ms: number): string {
7
+ return `${(ms / 1000).toFixed(1)}s`
8
+ }
9
+
10
+ export async function runAtom(
11
+ serviceId: string,
12
+ state: ServiceLifecycle,
13
+ logger: Logger,
14
+ handlers: (lifecycle: ServiceLifecycle) => (() => Bun.MaybePromise<void>)[],
15
+ ): Promise<void> {
16
+ const verbose = !!process.env.ATOMSERVICE_VERBOSE
17
+ const t0 = Date.now()
18
+ let finalMsg = serviceId
19
+
20
+ if (verbose) {
21
+ const RESET = "\x1b[0m"
22
+ const BOLD = "\x1b[1m"
23
+ const cyan = (s: string) => (Bun.color("#22d3ee", "ansi") ?? "") + s + RESET
24
+ const green = (s: string) => (Bun.color("#4ade80", "ansi") ?? "") + s + RESET
25
+ const blue = (s: string) => (Bun.color("#7dd3fc", "ansi") ?? "") + s + RESET
26
+ const yellow = (s: string) => (Bun.color("#fbbf24", "ansi") ?? "") + s + RESET
27
+ const red = (s: string) => (Bun.color("#f87171", "ansi") ?? "") + s + RESET
28
+ const bar = (s: string) => (Bun.color("#52525b", "ansi") ?? "") + s + RESET
29
+ console.log(`${cyan("◇")} ${BOLD}${serviceId}${RESET}`)
30
+ logger.info = (msg) => {
31
+ console.log(`${bar("│")} ${blue("›")} ${msg}`)
32
+ }
33
+ logger.success = (msg) => {
34
+ finalMsg = msg
35
+ }
36
+ logger.warn = (msg) => {
37
+ console.log(`${bar("│")} ${yellow("⚠")} ${msg}`)
38
+ }
39
+ logger.error = (msg) => {
40
+ console.log(`${bar("│")} ${red("✗")} ${msg}`)
41
+ finalMsg = msg
42
+ }
43
+ try {
44
+ for (const fn of handlers(state)) await fn()
45
+ console.log(`${green("└")} ${green(finalMsg)} ${bar(elapsed(Date.now() - t0))}`)
46
+ console.log()
47
+ } catch (err) {
48
+ console.log(`${red("└")} ${red(serviceId)} ${bar(elapsed(Date.now() - t0))}`)
49
+ console.error(red(` ${err instanceof Error ? err.message : String(err)}`))
50
+ if (err instanceof UserError && err.hint) console.error(bar(` ${err.hint}`))
51
+ process.exit(1)
52
+ }
53
+ } else {
54
+ const s = spinner()
55
+ s.start(serviceId)
56
+ logger.info = (msg) => s.message(msg)
57
+ logger.success = (msg) => {
58
+ finalMsg = msg
59
+ s.message(msg)
60
+ }
61
+ logger.warn = (msg) => s.message(pc.yellow(msg))
62
+ logger.error = (msg) => {
63
+ finalMsg = msg
64
+ s.message(pc.red(msg))
65
+ }
66
+ try {
67
+ for (const fn of handlers(state)) await fn()
68
+ s.stop(`${pc.green(finalMsg)} ${pc.dim(elapsed(Date.now() - t0))}`)
69
+ } catch (err) {
70
+ s.stop(`${pc.red(serviceId)} ${pc.dim(elapsed(Date.now() - t0))}`)
71
+ console.error(pc.red(` ${err instanceof Error ? err.message : String(err)}`))
72
+ if (err instanceof UserError && err.hint) console.error(pc.dim(` ${err.hint}`))
73
+ process.exit(1)
74
+ }
75
+ }
76
+ }
@@ -0,0 +1 @@
1
+ export { buildServiceSubCommands } from "./service.cmds.ts"
@@ -0,0 +1,34 @@
1
+ import { initForCli, loadConfig } from "@atomservice/core"
2
+ import { defineCommand } from "citty"
3
+
4
+ export async function buildServiceSubCommands(): Promise<Record<string, ReturnType<typeof defineCommand>>> {
5
+ let config: Awaited<ReturnType<typeof loadConfig>>
6
+ try {
7
+ config = await loadConfig()
8
+ } catch {
9
+ return {}
10
+ }
11
+ const services = await initForCli(config)
12
+ const subCommands: Record<string, ReturnType<typeof defineCommand>> = {}
13
+
14
+ for (const { serviceId, state } of services) {
15
+ if (!state.commands.length) continue
16
+
17
+ const cmds: Record<string, ReturnType<typeof defineCommand>> = {}
18
+ for (const cmd of state.commands) {
19
+ cmds[cmd.name] = defineCommand({
20
+ meta: { name: cmd.name, description: cmd.description },
21
+ run: async ({ args }) => {
22
+ await cmd.handler(args as Record<string, string>)
23
+ },
24
+ })
25
+ }
26
+
27
+ subCommands[serviceId] = defineCommand({
28
+ meta: { name: serviceId, description: `${serviceId} 原子服务命令` },
29
+ subCommands: cmds,
30
+ })
31
+ }
32
+
33
+ return subCommands
34
+ }