xuanwu-cli 1.0.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/README.md +171 -0
- package/bin/xuanwu +20 -0
- package/dist/api/client.d.ts +30 -0
- package/dist/api/client.js +332 -0
- package/dist/commands/build.d.ts +5 -0
- package/dist/commands/build.js +57 -0
- package/dist/commands/connect.d.ts +5 -0
- package/dist/commands/connect.js +67 -0
- package/dist/commands/deploy.d.ts +5 -0
- package/dist/commands/deploy.js +85 -0
- package/dist/commands/env.d.ts +5 -0
- package/dist/commands/env.js +119 -0
- package/dist/commands/logs.d.ts +5 -0
- package/dist/commands/logs.js +39 -0
- package/dist/commands/pods.d.ts +5 -0
- package/dist/commands/pods.js +56 -0
- package/dist/commands/scale.d.ts +5 -0
- package/dist/commands/scale.js +32 -0
- package/dist/commands/svc.d.ts +5 -0
- package/dist/commands/svc.js +100 -0
- package/dist/config/store.d.ts +18 -0
- package/dist/config/store.js +108 -0
- package/dist/config/types.d.ts +86 -0
- package/dist/config/types.js +5 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +142 -0
- package/dist/output/formatter.d.ts +15 -0
- package/dist/output/formatter.js +95 -0
- package/docs/DESIGN.md +363 -0
- package/docs//345/276/205/344/274/230/345/214/226.md +89 -0
- package/package.json +31 -0
- package/src/api/client.ts +380 -0
- package/src/commands/build.ts +67 -0
- package/src/commands/connect.ts +75 -0
- package/src/commands/deploy.ts +90 -0
- package/src/commands/env.ts +144 -0
- package/src/commands/logs.ts +47 -0
- package/src/commands/pods.ts +60 -0
- package/src/commands/scale.ts +35 -0
- package/src/commands/svc.ts +114 -0
- package/src/config/store.ts +86 -0
- package/src/config/types.ts +99 -0
- package/src/index.ts +127 -0
- package/src/output/formatter.ts +112 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 部署命令
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander'
|
|
6
|
+
import { configStore } from '../config/store'
|
|
7
|
+
import { createClient } from '../api/client'
|
|
8
|
+
import { OutputFormatter } from '../output/formatter'
|
|
9
|
+
import { DeployOptions } from '../config/types'
|
|
10
|
+
|
|
11
|
+
export function makeDeployCommand(): Command {
|
|
12
|
+
const cmd = new Command('deploy')
|
|
13
|
+
.description('Deploy services to environment')
|
|
14
|
+
.argument('<namespace>', 'Target namespace')
|
|
15
|
+
.argument('<service-name>', 'Service name')
|
|
16
|
+
.option('-t, --type <type>', 'Service type (application|database|image)', 'image')
|
|
17
|
+
.option('--git <url>', 'Git repository URL')
|
|
18
|
+
.option('--git-branch <branch>', 'Git branch', 'main')
|
|
19
|
+
.option('--build-type <type>', 'Build type (template|dockerfile)', 'template')
|
|
20
|
+
.option('--language <lang>', 'Language (java-springboot|nodejs|python|golang)')
|
|
21
|
+
.option('--dockerfile-path <path>', 'Dockerfile path')
|
|
22
|
+
.option('--db-type <type>', 'Database type (mysql|redis|postgres|elasticsearch)')
|
|
23
|
+
.option('--db-version <version>', 'Database version')
|
|
24
|
+
.option('--root-password <password>', 'Root password')
|
|
25
|
+
.option('--password <password>', 'Password')
|
|
26
|
+
.option('--user <user>', 'Username')
|
|
27
|
+
.option('--database <name>', 'Database name')
|
|
28
|
+
.option('--image <image>', 'Container image')
|
|
29
|
+
.option('-p, --port <port>', 'Container port')
|
|
30
|
+
.option('-r, --replicas <num>', 'Number of replicas', '1')
|
|
31
|
+
.option('--cpu <value>', 'CPU limit')
|
|
32
|
+
.option('--memory <value>', 'Memory limit')
|
|
33
|
+
.option('--domain <prefix>', 'Domain prefix')
|
|
34
|
+
.option('-e, --env <key=value>', 'Environment variables', (val, memo: Record<string, string> = {}) => {
|
|
35
|
+
const [key, value] = val.split('=')
|
|
36
|
+
memo[key] = value
|
|
37
|
+
return memo
|
|
38
|
+
}, {})
|
|
39
|
+
.action(async (namespace, serviceName, options) => {
|
|
40
|
+
const conn = configStore.getDefaultConnection()
|
|
41
|
+
if (!conn) {
|
|
42
|
+
OutputFormatter.error('No connection configured')
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const type = options.type as 'application' | 'database' | 'image'
|
|
47
|
+
|
|
48
|
+
const deployOptions: DeployOptions = {
|
|
49
|
+
namespace,
|
|
50
|
+
serviceName,
|
|
51
|
+
type,
|
|
52
|
+
port: options.port ? parseInt(options.port) : undefined,
|
|
53
|
+
replicas: options.replicas ? parseInt(options.replicas) : undefined,
|
|
54
|
+
cpu: options.cpu,
|
|
55
|
+
memory: options.memory,
|
|
56
|
+
domain: options.domain,
|
|
57
|
+
envVars: options.env
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 根据类型添加配置
|
|
61
|
+
if (type === 'application') {
|
|
62
|
+
deployOptions.git = options.git
|
|
63
|
+
deployOptions.gitBranch = options.gitBranch
|
|
64
|
+
deployOptions.buildType = options.buildType
|
|
65
|
+
deployOptions.language = options.language
|
|
66
|
+
deployOptions.dockerfilePath = options.dockerfilePath
|
|
67
|
+
} else if (type === 'database') {
|
|
68
|
+
deployOptions.dbType = options.dbType
|
|
69
|
+
deployOptions.dbVersion = options.dbVersion
|
|
70
|
+
deployOptions.rootPassword = options.rootPassword
|
|
71
|
+
deployOptions.password = options.password
|
|
72
|
+
deployOptions.user = options.user
|
|
73
|
+
deployOptions.database = options.database
|
|
74
|
+
} else if (type === 'image') {
|
|
75
|
+
deployOptions.image = options.image
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const client = createClient(conn)
|
|
79
|
+
const result = await client.deploy(deployOptions)
|
|
80
|
+
|
|
81
|
+
if (!result.success) {
|
|
82
|
+
OutputFormatter.error(result.error!.message)
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
OutputFormatter.success(`Service "${serviceName}" deployed to ${namespace}`)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
return cmd
|
|
90
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 环境管理命令
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander'
|
|
6
|
+
import { configStore } from '../config/store'
|
|
7
|
+
import { createClient } from '../api/client'
|
|
8
|
+
import { OutputFormatter } from '../output/formatter'
|
|
9
|
+
|
|
10
|
+
export function makeEnvCommand(): Command {
|
|
11
|
+
const cmd = new Command('env')
|
|
12
|
+
.description('Manage environments (K8s namespaces)')
|
|
13
|
+
|
|
14
|
+
cmd
|
|
15
|
+
.command('ls')
|
|
16
|
+
.description('List accessible environments')
|
|
17
|
+
.action(async () => {
|
|
18
|
+
const conn = configStore.getDefaultConnection()
|
|
19
|
+
if (!conn) {
|
|
20
|
+
OutputFormatter.error('No connection configured. Run: xuanwu connect add <name> -e <endpoint> -t <token>')
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const client = createClient(conn)
|
|
25
|
+
const result = await client.listNamespaces()
|
|
26
|
+
|
|
27
|
+
if (!result.success) {
|
|
28
|
+
OutputFormatter.error(result.error!.message)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const spaces = result.data || []
|
|
33
|
+
if (spaces.length === 0) {
|
|
34
|
+
OutputFormatter.info('No environments found')
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
OutputFormatter.table(
|
|
39
|
+
['Name', 'Identifier', 'Environment', 'Status'],
|
|
40
|
+
spaces.map((s: any) => [
|
|
41
|
+
s.name || s.identifier,
|
|
42
|
+
s.identifier,
|
|
43
|
+
s.environment,
|
|
44
|
+
s.status || 'active'
|
|
45
|
+
])
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
cmd
|
|
50
|
+
.command('create <namespace>')
|
|
51
|
+
.description('Create a new environment (K8s namespace)')
|
|
52
|
+
.option('-p, --project-id <id>', 'Project ID')
|
|
53
|
+
.option('-e, --environment <env>', 'Environment type (development|staging|production)', 'development')
|
|
54
|
+
.action(async (namespace, options) => {
|
|
55
|
+
const conn = configStore.getDefaultConnection()
|
|
56
|
+
if (!conn) {
|
|
57
|
+
OutputFormatter.error('No connection configured')
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const client = createClient(conn)
|
|
62
|
+
|
|
63
|
+
let projectId = options.projectId
|
|
64
|
+
|
|
65
|
+
// 如果没有指定 project-id,尝试获取第一个项目
|
|
66
|
+
if (!projectId) {
|
|
67
|
+
const projectsResult = await client.getProjects()
|
|
68
|
+
if (!projectsResult.success || !projectsResult.data || projectsResult.data.length === 0) {
|
|
69
|
+
OutputFormatter.error('No projects found. Please specify --project-id')
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
projectId = projectsResult.data[0].id
|
|
73
|
+
OutputFormatter.info(`Using project: ${projectsResult.data[0].name}`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const result = await client.createNamespaceWithProject(namespace, projectId, options.environment)
|
|
77
|
+
|
|
78
|
+
if (!result.success) {
|
|
79
|
+
OutputFormatter.error(result.error!.message)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
OutputFormatter.success(`Environment "${namespace}" created`)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
cmd
|
|
87
|
+
.command('rm <namespace>')
|
|
88
|
+
.description('Delete an environment')
|
|
89
|
+
.action(async (namespace) => {
|
|
90
|
+
const conn = configStore.getDefaultConnection()
|
|
91
|
+
if (!conn) {
|
|
92
|
+
OutputFormatter.error('No connection configured')
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const client = createClient(conn)
|
|
97
|
+
|
|
98
|
+
// 先获取 namespace ID
|
|
99
|
+
const infoResult = await client.getNamespaceInfo(namespace)
|
|
100
|
+
if (!infoResult.success || !infoResult.data) {
|
|
101
|
+
OutputFormatter.error(`Environment "${namespace}" not found`)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const envData = Array.isArray(infoResult.data) ? infoResult.data[0] : infoResult.data
|
|
106
|
+
const id = envData.id
|
|
107
|
+
const result = await client.deleteNamespace(id)
|
|
108
|
+
|
|
109
|
+
if (!result.success) {
|
|
110
|
+
OutputFormatter.error(result.error!.message)
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
OutputFormatter.success(`Environment "${namespace}" deleted`)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
cmd
|
|
118
|
+
.command('info <namespace>')
|
|
119
|
+
.description('Show environment details')
|
|
120
|
+
.action(async (namespace) => {
|
|
121
|
+
const conn = configStore.getDefaultConnection()
|
|
122
|
+
if (!conn) {
|
|
123
|
+
OutputFormatter.error('No connection configured')
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const client = createClient(conn)
|
|
128
|
+
const result = await client.getNamespaceInfo(namespace)
|
|
129
|
+
|
|
130
|
+
if (!result.success) {
|
|
131
|
+
OutputFormatter.error(result.error!.message)
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const info = result.data
|
|
136
|
+
OutputFormatter.info(`Name: ${info.name}`)
|
|
137
|
+
OutputFormatter.info(`Identifier: ${info.identifier}`)
|
|
138
|
+
OutputFormatter.info(`Namespace: ${info.namespace}`)
|
|
139
|
+
OutputFormatter.info(`Environment: ${info.environment}`)
|
|
140
|
+
OutputFormatter.info(`Status: ${info.status}`)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
return cmd
|
|
144
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 日志命令
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander'
|
|
6
|
+
import { configStore } from '../config/store'
|
|
7
|
+
import { createClient, APIClient } from '../api/client'
|
|
8
|
+
import { OutputFormatter } from '../output/formatter'
|
|
9
|
+
|
|
10
|
+
export function makeLogsCommand(): Command {
|
|
11
|
+
const cmd = new Command('logs')
|
|
12
|
+
.description('View service logs')
|
|
13
|
+
.argument('<namespace>', 'Target namespace')
|
|
14
|
+
.argument('<service-name>', 'Service name')
|
|
15
|
+
.option('-n, --lines <num>', 'Number of lines', '100')
|
|
16
|
+
.option('-f, --follow', 'Follow logs in real-time (SSE)')
|
|
17
|
+
.action(async (namespace, serviceName, options) => {
|
|
18
|
+
const conn = configStore.getDefaultConnection()
|
|
19
|
+
if (!conn) {
|
|
20
|
+
OutputFormatter.error('No connection configured')
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const client = createClient(conn)
|
|
25
|
+
|
|
26
|
+
if (options.follow) {
|
|
27
|
+
// 使用 SSE 实时日志
|
|
28
|
+
await client.streamLogs(namespace, serviceName)
|
|
29
|
+
} else {
|
|
30
|
+
const result = await client.getLogs(
|
|
31
|
+
namespace,
|
|
32
|
+
serviceName,
|
|
33
|
+
parseInt(options.lines),
|
|
34
|
+
false
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if (!result.success) {
|
|
38
|
+
OutputFormatter.error(result.error!.message)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(result.data?.logs || result.data || '')
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
return cmd
|
|
47
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pods 命令
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander'
|
|
6
|
+
import { configStore } from '../config/store'
|
|
7
|
+
import { createClient } from '../api/client'
|
|
8
|
+
import { OutputFormatter } from '../output/formatter'
|
|
9
|
+
|
|
10
|
+
function getAge(timestamp?: string): string {
|
|
11
|
+
if (!timestamp) return '-'
|
|
12
|
+
const created = new Date(timestamp)
|
|
13
|
+
const now = new Date()
|
|
14
|
+
const diff = Math.floor((now.getTime() - created.getTime()) / 1000)
|
|
15
|
+
if (diff < 60) return `${diff}s`
|
|
16
|
+
if (diff < 3600) return `${Math.floor(diff / 60)}m`
|
|
17
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)}h`
|
|
18
|
+
return `${Math.floor(diff / 86400)}d`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function makePodsCommand(): Command {
|
|
22
|
+
const cmd = new Command('pods')
|
|
23
|
+
.description('List pods')
|
|
24
|
+
.argument('<namespace>', 'Target namespace')
|
|
25
|
+
.argument('<service-name>', 'Service name')
|
|
26
|
+
.action(async (namespace, serviceName) => {
|
|
27
|
+
const conn = configStore.getDefaultConnection()
|
|
28
|
+
if (!conn) {
|
|
29
|
+
OutputFormatter.error('No connection configured')
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const client = createClient(conn)
|
|
34
|
+
const result = await client.listPods(namespace, serviceName)
|
|
35
|
+
|
|
36
|
+
if (!result.success) {
|
|
37
|
+
OutputFormatter.error(result.error!.message)
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const pods = Array.isArray(result.data) ? result.data : (result.data?.items || [])
|
|
42
|
+
if (pods.length === 0) {
|
|
43
|
+
OutputFormatter.info('No pods found')
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
OutputFormatter.table(
|
|
48
|
+
['Name', 'Status', 'Ready', 'Restarts', 'Age'],
|
|
49
|
+
pods.map((p: any) => [
|
|
50
|
+
p.metadata?.name || 'unknown',
|
|
51
|
+
p.status?.phase || 'Unknown',
|
|
52
|
+
`${p.status?.containerStatuses?.filter((c: any) => c.ready).length || 0}/${p.spec?.containers?.length || 0}`,
|
|
53
|
+
p.status?.containerStatuses?.[0]?.restartCount?.toString() || '0',
|
|
54
|
+
getAge(p.metadata?.creationTimestamp)
|
|
55
|
+
])
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
return cmd
|
|
60
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 扩缩容命令
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander'
|
|
6
|
+
import { configStore } from '../config/store'
|
|
7
|
+
import { createClient } from '../api/client'
|
|
8
|
+
import { OutputFormatter } from '../output/formatter'
|
|
9
|
+
|
|
10
|
+
export function makeScaleCommand(): Command {
|
|
11
|
+
const cmd = new Command('scale')
|
|
12
|
+
.description('Scale services')
|
|
13
|
+
.argument('<namespace>', 'Target namespace')
|
|
14
|
+
.argument('<service-name>', 'Service name')
|
|
15
|
+
.requiredOption('-r, --replicas <num>', 'Number of replicas')
|
|
16
|
+
.action(async (namespace, serviceName, options) => {
|
|
17
|
+
const conn = configStore.getDefaultConnection()
|
|
18
|
+
if (!conn) {
|
|
19
|
+
OutputFormatter.error('No connection configured')
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const client = createClient(conn)
|
|
24
|
+
const result = await client.scale(namespace, serviceName, parseInt(options.replicas))
|
|
25
|
+
|
|
26
|
+
if (!result.success) {
|
|
27
|
+
OutputFormatter.error(result.error!.message)
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
OutputFormatter.success(`Scaled to ${options.replicas} replicas`)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
return cmd
|
|
35
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 服务管理命令
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander'
|
|
6
|
+
import { configStore } from '../config/store'
|
|
7
|
+
import { createClient } from '../api/client'
|
|
8
|
+
import { OutputFormatter } from '../output/formatter'
|
|
9
|
+
|
|
10
|
+
function getAge(timestamp?: string): string {
|
|
11
|
+
if (!timestamp) return '-'
|
|
12
|
+
const created = new Date(timestamp)
|
|
13
|
+
const now = new Date()
|
|
14
|
+
const diff = Math.floor((now.getTime() - created.getTime()) / 1000)
|
|
15
|
+
if (diff < 60) return `${diff}s`
|
|
16
|
+
if (diff < 3600) return `${Math.floor(diff / 60)}m`
|
|
17
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)}h`
|
|
18
|
+
return `${Math.floor(diff / 86400)}d`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function makeSvcCommand(): Command {
|
|
22
|
+
const cmd = new Command('svc')
|
|
23
|
+
.description('Manage services')
|
|
24
|
+
|
|
25
|
+
cmd
|
|
26
|
+
.command('ls <namespace>')
|
|
27
|
+
.description('List services in namespace')
|
|
28
|
+
.action(async (namespace) => {
|
|
29
|
+
const conn = configStore.getDefaultConnection()
|
|
30
|
+
if (!conn) {
|
|
31
|
+
OutputFormatter.error('No connection configured')
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const client = createClient(conn)
|
|
36
|
+
const result = await client.listServices(namespace)
|
|
37
|
+
|
|
38
|
+
if (!result.success) {
|
|
39
|
+
OutputFormatter.error(result.error!.message)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const services = result.data || []
|
|
44
|
+
if (services.length === 0) {
|
|
45
|
+
OutputFormatter.info(`No services in ${namespace}`)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
OutputFormatter.table(
|
|
50
|
+
['Name', 'Ready', 'Up-to-date', 'Available', 'Age'],
|
|
51
|
+
services.map((s: any) => [
|
|
52
|
+
s.metadata?.name || 'unknown',
|
|
53
|
+
`${s.status?.readyReplicas || 0}/${s.status?.replicas || 0}`,
|
|
54
|
+
s.status?.updatedReplicas || 0,
|
|
55
|
+
s.status?.availableReplicas || 0,
|
|
56
|
+
getAge(s.metadata?.creationTimestamp)
|
|
57
|
+
])
|
|
58
|
+
)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
cmd
|
|
62
|
+
.command('status <namespace> <service-name>')
|
|
63
|
+
.description('Get service status')
|
|
64
|
+
.action(async (namespace, serviceName) => {
|
|
65
|
+
const conn = configStore.getDefaultConnection()
|
|
66
|
+
if (!conn) {
|
|
67
|
+
OutputFormatter.error('No connection configured')
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const client = createClient(conn)
|
|
72
|
+
const result = await client.getServiceStatus(namespace, serviceName)
|
|
73
|
+
|
|
74
|
+
if (!result.success) {
|
|
75
|
+
OutputFormatter.error(result.error!.message)
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const info = result.data
|
|
80
|
+
OutputFormatter.info(`Service: ${info.name}`)
|
|
81
|
+
OutputFormatter.info(`Namespace: ${info.namespace}`)
|
|
82
|
+
OutputFormatter.info(`Replicas: ${info.replicas}`)
|
|
83
|
+
OutputFormatter.info(`Ready Replicas: ${info.readyReplicas}`)
|
|
84
|
+
OutputFormatter.info(`Available Replicas: ${info.availableReplicas}`)
|
|
85
|
+
OutputFormatter.info(`Image: ${info.image}`)
|
|
86
|
+
|
|
87
|
+
if (info.labels) {
|
|
88
|
+
OutputFormatter.info(`Labels: ${JSON.stringify(info.labels)}`)
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
cmd
|
|
93
|
+
.command('rm <namespace> <service-name>')
|
|
94
|
+
.description('Delete a service')
|
|
95
|
+
.action(async (namespace, serviceName) => {
|
|
96
|
+
const conn = configStore.getDefaultConnection()
|
|
97
|
+
if (!conn) {
|
|
98
|
+
OutputFormatter.error('No connection configured')
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const client = createClient(conn)
|
|
103
|
+
const result = await client.deleteService(namespace, serviceName)
|
|
104
|
+
|
|
105
|
+
if (!result.success) {
|
|
106
|
+
OutputFormatter.error(result.error!.message)
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
OutputFormatter.success(`Service "${serviceName}" deleted`)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
return cmd
|
|
114
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置存储
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'fs'
|
|
6
|
+
import * as path from 'path'
|
|
7
|
+
import * as os from 'os'
|
|
8
|
+
import { Config, Connection } from './types'
|
|
9
|
+
|
|
10
|
+
const CONFIG_DIR = path.join(os.homedir(), '.xuanwu')
|
|
11
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
|
|
12
|
+
|
|
13
|
+
const DEFAULT_CONFIG: Config = {
|
|
14
|
+
version: '1.0',
|
|
15
|
+
connections: []
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ConfigStore {
|
|
19
|
+
private config: Config
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
this.config = this.load()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private load(): Config {
|
|
26
|
+
try {
|
|
27
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
28
|
+
const data = fs.readFileSync(CONFIG_FILE, 'utf-8')
|
|
29
|
+
return JSON.parse(data)
|
|
30
|
+
}
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('Failed to load config:', error)
|
|
33
|
+
}
|
|
34
|
+
return { ...DEFAULT_CONFIG }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private save(): void {
|
|
38
|
+
try {
|
|
39
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
40
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
|
41
|
+
}
|
|
42
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(this.config, null, 2))
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Failed to save config:', error)
|
|
45
|
+
throw error
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getConnections(): Connection[] {
|
|
50
|
+
return this.config.connections
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getConnection(name: string): Connection | undefined {
|
|
54
|
+
return this.config.connections.find(c => c.name === name)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getDefaultConnection(): Connection | undefined {
|
|
58
|
+
return this.config.connections.find(c => c.isDefault)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
addConnection(connection: Connection): void {
|
|
62
|
+
// Remove existing with same name
|
|
63
|
+
this.config.connections = this.config.connections.filter(c => c.name !== connection.name)
|
|
64
|
+
this.config.connections.push(connection)
|
|
65
|
+
this.save()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
removeConnection(name: string): void {
|
|
69
|
+
this.config.connections = this.config.connections.filter(c => c.name !== name)
|
|
70
|
+
this.save()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
setDefaultConnection(name: string): void {
|
|
74
|
+
this.config.connections = this.config.connections.map(c => ({
|
|
75
|
+
...c,
|
|
76
|
+
isDefault: c.name === name
|
|
77
|
+
}))
|
|
78
|
+
this.save()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getConfig(): Config {
|
|
82
|
+
return this.config
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const configStore = new ConfigStore()
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* xuanwu-cli 类型定义
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface Connection {
|
|
6
|
+
name: string
|
|
7
|
+
endpoint: string
|
|
8
|
+
token: string
|
|
9
|
+
isDefault: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Config {
|
|
13
|
+
version: string
|
|
14
|
+
connections: Connection[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ServiceType {
|
|
18
|
+
type: 'application' | 'database' | 'image'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ApplicationOptions extends ServiceType {
|
|
22
|
+
git?: string
|
|
23
|
+
gitBranch?: string
|
|
24
|
+
buildType?: 'template' | 'dockerfile'
|
|
25
|
+
language?: string
|
|
26
|
+
dockerfilePath?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DatabaseOptions extends ServiceType {
|
|
30
|
+
dbType: 'mysql' | 'redis' | 'postgres' | 'elasticsearch'
|
|
31
|
+
dbVersion: string
|
|
32
|
+
rootPassword?: string
|
|
33
|
+
password?: string
|
|
34
|
+
user?: string
|
|
35
|
+
database?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ImageOptions extends ServiceType {
|
|
39
|
+
image: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface DeployOptions {
|
|
43
|
+
namespace: string
|
|
44
|
+
serviceName: string
|
|
45
|
+
type: 'application' | 'database' | 'image'
|
|
46
|
+
port?: number
|
|
47
|
+
replicas?: number
|
|
48
|
+
cpu?: string
|
|
49
|
+
memory?: string
|
|
50
|
+
domain?: string
|
|
51
|
+
envVars?: Record<string, string>
|
|
52
|
+
volume?: string
|
|
53
|
+
// application
|
|
54
|
+
git?: string
|
|
55
|
+
gitBranch?: string
|
|
56
|
+
buildType?: 'template' | 'dockerfile'
|
|
57
|
+
language?: string
|
|
58
|
+
dockerfilePath?: string
|
|
59
|
+
// database
|
|
60
|
+
dbType?: 'mysql' | 'redis' | 'postgres' | 'elasticsearch'
|
|
61
|
+
dbVersion?: string
|
|
62
|
+
rootPassword?: string
|
|
63
|
+
password?: string
|
|
64
|
+
user?: string
|
|
65
|
+
database?: string
|
|
66
|
+
// image
|
|
67
|
+
image?: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ServiceInfo {
|
|
71
|
+
name: string
|
|
72
|
+
type: string
|
|
73
|
+
status: string
|
|
74
|
+
ready: string
|
|
75
|
+
image: string
|
|
76
|
+
age: string
|
|
77
|
+
ports: number[]
|
|
78
|
+
endpoint?: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface NamespaceInfo {
|
|
82
|
+
name: string
|
|
83
|
+
status: string
|
|
84
|
+
age: string
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface CLIResult<T = any> {
|
|
88
|
+
success: boolean
|
|
89
|
+
data?: T
|
|
90
|
+
error?: {
|
|
91
|
+
code: string
|
|
92
|
+
message: string
|
|
93
|
+
details?: any
|
|
94
|
+
}
|
|
95
|
+
meta?: {
|
|
96
|
+
timestamp: string
|
|
97
|
+
duration?: number
|
|
98
|
+
}
|
|
99
|
+
}
|