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,89 @@
|
|
|
1
|
+
# CLI 待优化清单
|
|
2
|
+
|
|
3
|
+
**更新日期**: 2026-03-10
|
|
4
|
+
**状态**: 需要修改 API 端点
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 一、需要修改的 API 端点
|
|
9
|
+
|
|
10
|
+
### 服务删除 API 路径变更
|
|
11
|
+
|
|
12
|
+
**原因**: 后端路由冲突,已将服务删除 API 迁移到新路径
|
|
13
|
+
|
|
14
|
+
| 命令 | 旧端点 | 新端点 |
|
|
15
|
+
|------|--------|--------|
|
|
16
|
+
| `xuanwu svc rm <namespace> <name>` | `DELETE /api/services/[ns]/[name]` | `DELETE /api/services/by-ns/[ns]/[name]` |
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 二、CLI 代码修改位置
|
|
21
|
+
|
|
22
|
+
需要修改 CLI 中调用服务删除 API 的代码,将端点从:
|
|
23
|
+
```
|
|
24
|
+
/api/services/${namespace}/${name}
|
|
25
|
+
```
|
|
26
|
+
改为:
|
|
27
|
+
```
|
|
28
|
+
/api/services/by-ns/${namespace}/${name}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 修改示例
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// 旧代码
|
|
35
|
+
const response = await fetch(`${endpoint}/api/services/${namespace}/${name}`, {
|
|
36
|
+
method: 'DELETE',
|
|
37
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// 新代码
|
|
41
|
+
const response = await fetch(`${endpoint}/api/services/by-ns/${namespace}/${name}`, {
|
|
42
|
+
method: 'DELETE',
|
|
43
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
44
|
+
})
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 三、验证命令
|
|
50
|
+
|
|
51
|
+
修改完成后,使用以下命令验证:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# 部署测试服务
|
|
55
|
+
xuanwu deploy test-ns test-delete --image nginx:latest
|
|
56
|
+
|
|
57
|
+
# 删除服务(验证新端点)
|
|
58
|
+
xuanwu svc rm test-ns test-delete
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
预期结果:返回 `{ "success": true }`
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 四、API 端点完整映射(更新后)
|
|
66
|
+
|
|
67
|
+
| CLI 命令 | API 端点 | 状态 |
|
|
68
|
+
|---------|---------|------|
|
|
69
|
+
| env ls | GET /api/deploy-spaces | ✅ |
|
|
70
|
+
| env create | POST /api/deploy-spaces | ✅ |
|
|
71
|
+
| env rm | DELETE /api/deploy-spaces/[id] | ✅ |
|
|
72
|
+
| deploy | POST /api/services | ✅ |
|
|
73
|
+
| svc ls | GET /api/k8s?kind=deployments | ✅ |
|
|
74
|
+
| svc status | GET /api/k8s/deployments/[name] | ✅ |
|
|
75
|
+
| **svc rm** | **DELETE /api/services/by-ns/[ns]/[name]** | **🔄 需修改** |
|
|
76
|
+
| scale | POST /api/k8s?action=scale | ✅ |
|
|
77
|
+
| logs | GET /api/k8s/logs | ✅ |
|
|
78
|
+
| logs -f | GET /api/k8s/logs/stream | ✅ |
|
|
79
|
+
| pods | GET /api/k8s?kind=pods | ✅ |
|
|
80
|
+
| top | GET /api/k8s/metrics | ✅ |
|
|
81
|
+
| exec | POST /api/debug/pod-exec | ✅ |
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 五、后端已完成
|
|
86
|
+
|
|
87
|
+
- [x] 创建新路由 `/api/services/by-ns/[namespace]/[name]`
|
|
88
|
+
- [x] 实现 DELETE 方法
|
|
89
|
+
- [x] 构建验证通过
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "xuanwu-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "玄武工厂平台 CLI 工具",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"xuanwu": "./bin/xuanwu"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"cli",
|
|
11
|
+
"kubernetes",
|
|
12
|
+
"devops"
|
|
13
|
+
],
|
|
14
|
+
"author": "",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"commander": "^11.1.0",
|
|
18
|
+
"inquirer": "^9.2.12",
|
|
19
|
+
"axios": "^1.6.5"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/inquirer": "^9.0.7",
|
|
23
|
+
"@types/node": "^20.11.0",
|
|
24
|
+
"typescript": "^5.3.3"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc",
|
|
28
|
+
"dev": "tsc --watch",
|
|
29
|
+
"start": "node dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API 客户端
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'
|
|
6
|
+
import { Connection, CLIResult, ServiceInfo, NamespaceInfo, DeployOptions } from '../config/types'
|
|
7
|
+
import { OutputFormatter } from '../output/formatter'
|
|
8
|
+
|
|
9
|
+
export class APIClient {
|
|
10
|
+
private client: AxiosInstance
|
|
11
|
+
|
|
12
|
+
constructor(connection: Connection) {
|
|
13
|
+
this.client = axios.create({
|
|
14
|
+
baseURL: connection.endpoint,
|
|
15
|
+
headers: {
|
|
16
|
+
'Authorization': `Bearer ${connection.token}`,
|
|
17
|
+
'Content-Type': 'application/json'
|
|
18
|
+
},
|
|
19
|
+
timeout: 30000
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private async request<T>(method: string, url: string, data?: any, config?: AxiosRequestConfig): Promise<CLIResult<T>> {
|
|
24
|
+
const startTime = Date.now()
|
|
25
|
+
try {
|
|
26
|
+
const response = await this.client.request({
|
|
27
|
+
method,
|
|
28
|
+
url,
|
|
29
|
+
data,
|
|
30
|
+
...config
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
success: true,
|
|
35
|
+
data: response.data,
|
|
36
|
+
meta: {
|
|
37
|
+
timestamp: new Date().toISOString(),
|
|
38
|
+
duration: Date.now() - startTime
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} catch (error: any) {
|
|
42
|
+
const duration = Date.now() - startTime
|
|
43
|
+
const message = error.response?.data?.message || error.message
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
success: false,
|
|
47
|
+
error: {
|
|
48
|
+
code: error.response?.status?.toString() || 'ERROR',
|
|
49
|
+
message,
|
|
50
|
+
details: error.response?.data
|
|
51
|
+
},
|
|
52
|
+
meta: {
|
|
53
|
+
timestamp: new Date().toISOString(),
|
|
54
|
+
duration
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ===================
|
|
61
|
+
// Namespace (环境)
|
|
62
|
+
// ===================
|
|
63
|
+
|
|
64
|
+
async listNamespaces(): Promise<CLIResult<NamespaceInfo[]>> {
|
|
65
|
+
return this.request<NamespaceInfo[]>('GET', '/api/deploy-spaces')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async createNamespace(name: string): Promise<CLIResult<any>> {
|
|
69
|
+
return this.request('POST', '/api/deploy-spaces', {
|
|
70
|
+
name,
|
|
71
|
+
identifier: name,
|
|
72
|
+
namespace: name,
|
|
73
|
+
environment: 'development'
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async deleteNamespace(id: string): Promise<CLIResult<void>> {
|
|
78
|
+
return this.request('DELETE', `/api/deploy-spaces/${id}`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async getNamespaceInfo(identifier: string): Promise<CLIResult<any>> {
|
|
82
|
+
return this.request('GET', `/api/deploy-spaces?identifier=${identifier}`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async getProjects(): Promise<CLIResult<any[]>> {
|
|
86
|
+
return this.request<any[]>('GET', '/api/projects')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async createNamespaceWithProject(name: string, projectId: string, environment: string = 'development'): Promise<CLIResult<any>> {
|
|
90
|
+
return this.request('POST', '/api/deploy-spaces', {
|
|
91
|
+
name,
|
|
92
|
+
identifier: name,
|
|
93
|
+
namespace: name,
|
|
94
|
+
environment,
|
|
95
|
+
project_id: projectId
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ===================
|
|
100
|
+
// Service
|
|
101
|
+
// ===================
|
|
102
|
+
|
|
103
|
+
async listServices(namespace: string): Promise<CLIResult<ServiceInfo[]>> {
|
|
104
|
+
return this.request<any[]>('GET', `/api/k8s?kind=deployments&namespace=${namespace}`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async getServiceStatus(namespace: string, name: string): Promise<CLIResult<any>> {
|
|
108
|
+
return this.request('GET', `/api/k8s/deployments/${name}?namespace=${namespace}`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async deleteService(namespace: string, name: string): Promise<CLIResult<void>> {
|
|
112
|
+
return this.request('DELETE', `/api/services/by-ns/${namespace}/${name}`)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ===================
|
|
116
|
+
// Deploy
|
|
117
|
+
// ===================
|
|
118
|
+
|
|
119
|
+
async deploy(options: DeployOptions): Promise<CLIResult<any>> {
|
|
120
|
+
const { namespace, serviceName, type, ...rest } = options
|
|
121
|
+
|
|
122
|
+
// 获取 namespace 对应的 project_id
|
|
123
|
+
let projectId: string | undefined
|
|
124
|
+
|
|
125
|
+
// 尝试通过 identifier 查询
|
|
126
|
+
const envResult = await this.getNamespaceInfo(namespace)
|
|
127
|
+
if (envResult.success && envResult.data) {
|
|
128
|
+
const envData = Array.isArray(envResult.data) ? envResult.data[0] : envResult.data
|
|
129
|
+
projectId = envData.project?.id || envData.projectId
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 如果没找到,尝试从列表中查找
|
|
133
|
+
if (!projectId) {
|
|
134
|
+
const listResult = await this.listNamespaces()
|
|
135
|
+
if (listResult.success && listResult.data) {
|
|
136
|
+
const env = (listResult.data as any[]).find((e) => e.namespace === namespace || e.identifier === namespace)
|
|
137
|
+
if (env) {
|
|
138
|
+
projectId = env.project?.id || env.projectId
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 如果还是没有,使用第一个项目的 ID
|
|
144
|
+
if (!projectId) {
|
|
145
|
+
const projectsResult = await this.getProjects()
|
|
146
|
+
if (projectsResult.success && projectsResult.data && projectsResult.data.length > 0) {
|
|
147
|
+
projectId = projectsResult.data[0].id
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!projectId) {
|
|
152
|
+
return {
|
|
153
|
+
success: false,
|
|
154
|
+
error: { code: 'NOT_FOUND', message: `Cannot find project for namespace "${namespace}"` }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const payload: any = {
|
|
159
|
+
project_id: projectId,
|
|
160
|
+
name: serviceName,
|
|
161
|
+
type
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 根据类型添加配置
|
|
165
|
+
if (type === 'application') {
|
|
166
|
+
payload.git_provider = 'gitlab'
|
|
167
|
+
payload.git_repository = rest.git
|
|
168
|
+
payload.git_branch = rest.gitBranch || 'main'
|
|
169
|
+
payload.build_type = rest.buildType || 'template'
|
|
170
|
+
payload.port = rest.port || 8080
|
|
171
|
+
} else if (type === 'database') {
|
|
172
|
+
payload.database_type = rest.dbType
|
|
173
|
+
payload.version = rest.dbVersion || '8.0'
|
|
174
|
+
payload.root_password = rest.rootPassword || 'root'
|
|
175
|
+
payload.port = rest.port || this.getDefaultPort(rest.dbType!)
|
|
176
|
+
} else if (type === 'image') {
|
|
177
|
+
payload.image = rest.image
|
|
178
|
+
payload.port = rest.port || 80
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 添加通用配置
|
|
182
|
+
payload.replicas = rest.replicas || 1
|
|
183
|
+
payload.port = rest.port || 8080
|
|
184
|
+
|
|
185
|
+
// 添加资源限制
|
|
186
|
+
if (rest.cpu || rest.memory) {
|
|
187
|
+
payload.resource_limits = {
|
|
188
|
+
cpu: rest.cpu || '500m',
|
|
189
|
+
memory: rest.memory || '512Mi'
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 构建网络配置
|
|
194
|
+
payload.network_config = {
|
|
195
|
+
service_type: 'ClusterIP',
|
|
196
|
+
ports: [{
|
|
197
|
+
container_port: payload.port,
|
|
198
|
+
protocol: 'TCP'
|
|
199
|
+
}]
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 添加域名
|
|
203
|
+
if (rest.domain) {
|
|
204
|
+
payload.network_config.ports[0].domain = {
|
|
205
|
+
enabled: true,
|
|
206
|
+
prefix: rest.domain,
|
|
207
|
+
host: `${rest.domain}.${namespace}.dev.aimstek.cn`
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 环境变量
|
|
212
|
+
if (rest.envVars) {
|
|
213
|
+
payload.env_vars = rest.envVars
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return this.request('POST', '/api/services', payload)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private getDefaultPort(dbType: string): number {
|
|
220
|
+
const ports: Record<string, number> = {
|
|
221
|
+
mysql: 3306,
|
|
222
|
+
redis: 6379,
|
|
223
|
+
postgres: 5432,
|
|
224
|
+
elasticsearch: 9200
|
|
225
|
+
}
|
|
226
|
+
return ports[dbType] || 3306
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ===================
|
|
230
|
+
// Build
|
|
231
|
+
// ===================
|
|
232
|
+
|
|
233
|
+
async build(namespace: string, serviceName: string): Promise<CLIResult<any>> {
|
|
234
|
+
// 先获取服务 ID
|
|
235
|
+
const serviceResult = await this.getServiceByName(namespace, serviceName)
|
|
236
|
+
if (!serviceResult.success || !serviceResult.data) {
|
|
237
|
+
return {
|
|
238
|
+
success: false,
|
|
239
|
+
error: { code: 'NOT_FOUND', message: `Service ${serviceName} not found` }
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const service = serviceResult.data as any
|
|
244
|
+
if (!service.application_id) {
|
|
245
|
+
return {
|
|
246
|
+
success: false,
|
|
247
|
+
error: { code: 'NO_APPLICATION', message: 'Service is not an application type' }
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return this.request('POST', `/api/applications/${service.application_id}/build`)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async getBuildStatus(namespace: string, serviceName: string): Promise<CLIResult<any>> {
|
|
255
|
+
const serviceResult = await this.getServiceByName(namespace, serviceName)
|
|
256
|
+
if (!serviceResult.success || !serviceResult.data) {
|
|
257
|
+
return { success: false, error: { code: 'NOT_FOUND', message: 'Service not found' } }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const service = serviceResult.data as any
|
|
261
|
+
if (!service.application_id) {
|
|
262
|
+
return { success: false, error: { code: 'NO_APPLICATION', message: 'Not an application' } }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return this.request('GET', `/api/applications/${service.application_id}/builds`)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private async getServiceByName(namespace: string, name: string): Promise<CLIResult<any>> {
|
|
269
|
+
return this.request('GET', `/api/services?project_id=${namespace}&name=${name}`)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ===================
|
|
273
|
+
// Scale
|
|
274
|
+
// ===================
|
|
275
|
+
|
|
276
|
+
async scale(namespace: string, serviceName: string, replicas: number): Promise<CLIResult<any>> {
|
|
277
|
+
return this.request('POST', `/api/k8s?namespace=${namespace}&action=scale`, {
|
|
278
|
+
name: serviceName,
|
|
279
|
+
replicas
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ===================
|
|
284
|
+
// Logs
|
|
285
|
+
// ===================
|
|
286
|
+
|
|
287
|
+
async getLogs(namespace: string, serviceName: string, lines: number = 100, follow: boolean = false): Promise<CLIResult<any>> {
|
|
288
|
+
return this.request('GET', `/api/k8s/logs?namespace=${namespace}&serviceName=${serviceName}&lines=${lines}&follow=${follow}`)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async streamLogs(namespace: string, serviceName: string): Promise<void> {
|
|
292
|
+
const url = `${this.client.defaults.baseURL}/api/k8s/logs/stream?namespace=${namespace}&serviceName=${serviceName}`
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const response = await fetch(url, {
|
|
296
|
+
headers: {
|
|
297
|
+
'Authorization': `Bearer ${this.client.defaults.headers['Authorization']}`
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
if (!response.ok) {
|
|
302
|
+
const error = await response.text()
|
|
303
|
+
OutputFormatter.error(`Failed to stream logs: ${error}`)
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const reader = response.body?.getReader()
|
|
308
|
+
if (!reader) {
|
|
309
|
+
OutputFormatter.error('Failed to read stream')
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const decoder = new TextDecoder()
|
|
314
|
+
while (true) {
|
|
315
|
+
const { done, value } = await reader.read()
|
|
316
|
+
if (done) break
|
|
317
|
+
const text = decoder.decode(value)
|
|
318
|
+
// 解析 SSE 格式
|
|
319
|
+
const lines = text.split('\n')
|
|
320
|
+
for (const line of lines) {
|
|
321
|
+
if (line.startsWith('data: ')) {
|
|
322
|
+
console.log(line.slice(6))
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
} catch (error: any) {
|
|
327
|
+
OutputFormatter.error(`Stream error: ${error.message}`)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ===================
|
|
332
|
+
// Pods
|
|
333
|
+
// ===================
|
|
334
|
+
|
|
335
|
+
async listPods(namespace: string, serviceName: string): Promise<CLIResult<any>> {
|
|
336
|
+
return this.request('GET', `/api/k8s?kind=pods&namespace=${namespace}&labelSelector=app=${serviceName}`)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ===================
|
|
340
|
+
// Metrics
|
|
341
|
+
// ===================
|
|
342
|
+
|
|
343
|
+
async getMetrics(namespace: string, serviceName: string, podName?: string): Promise<CLIResult<any>> {
|
|
344
|
+
const url = podName
|
|
345
|
+
? `/api/k8s/metrics?namespace=${namespace}&podName=${podName}`
|
|
346
|
+
: `/api/k8s/metrics?namespace=${namespace}&serviceName=${serviceName}`
|
|
347
|
+
return this.request('GET', url)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ===================
|
|
351
|
+
// Exec
|
|
352
|
+
// ===================
|
|
353
|
+
|
|
354
|
+
async exec(namespace: string, serviceName: string, command: string, podName?: string): Promise<CLIResult<any>> {
|
|
355
|
+
let targetPod = podName
|
|
356
|
+
|
|
357
|
+
// 如果没有指定 podName,自动获取第一个 pod
|
|
358
|
+
if (!targetPod) {
|
|
359
|
+
const podsResult = await this.listPods(namespace, serviceName)
|
|
360
|
+
if (!podsResult.success || !podsResult.data || podsResult.data.length === 0) {
|
|
361
|
+
return { success: false, error: { code: 'NOT_FOUND', message: 'No pods found' } }
|
|
362
|
+
}
|
|
363
|
+
const pods = Array.isArray(podsResult.data) ? podsResult.data : podsResult.data.items || []
|
|
364
|
+
if (pods.length === 0) {
|
|
365
|
+
return { success: false, error: { code: 'NOT_FOUND', message: 'No pods found' } }
|
|
366
|
+
}
|
|
367
|
+
targetPod = pods[0].metadata?.name
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return this.request('POST', '/api/debug/pod-exec', {
|
|
371
|
+
namespace,
|
|
372
|
+
podName: targetPod,
|
|
373
|
+
command
|
|
374
|
+
})
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function createClient(connection: Connection): APIClient {
|
|
379
|
+
return new APIClient(connection)
|
|
380
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
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 makeBuildCommand(): Command {
|
|
11
|
+
const cmd = new Command('build')
|
|
12
|
+
.description('Build applications')
|
|
13
|
+
|
|
14
|
+
cmd
|
|
15
|
+
.command('<namespace> <service-name>')
|
|
16
|
+
.description('Build a service')
|
|
17
|
+
.action(async (namespace, serviceName) => {
|
|
18
|
+
const conn = configStore.getDefaultConnection()
|
|
19
|
+
if (!conn) {
|
|
20
|
+
OutputFormatter.error('No connection configured')
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const client = createClient(conn)
|
|
25
|
+
OutputFormatter.info('Starting build...')
|
|
26
|
+
|
|
27
|
+
const result = await client.build(namespace, serviceName)
|
|
28
|
+
|
|
29
|
+
if (!result.success) {
|
|
30
|
+
OutputFormatter.error(result.error!.message)
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
OutputFormatter.success('Build started')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
cmd
|
|
38
|
+
.command('status <namespace> <service-name>')
|
|
39
|
+
.description('Get build status')
|
|
40
|
+
.action(async (namespace, serviceName) => {
|
|
41
|
+
const conn = configStore.getDefaultConnection()
|
|
42
|
+
if (!conn) {
|
|
43
|
+
OutputFormatter.error('No connection configured')
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const client = createClient(conn)
|
|
48
|
+
const result = await client.getBuildStatus(namespace, serviceName)
|
|
49
|
+
|
|
50
|
+
if (!result.success) {
|
|
51
|
+
OutputFormatter.error(result.error!.message)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const builds = result.data || []
|
|
56
|
+
if (builds.length === 0) {
|
|
57
|
+
OutputFormatter.info('No builds found')
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const latest = builds[0]
|
|
62
|
+
OutputFormatter.info(`Status: ${latest.status}`)
|
|
63
|
+
OutputFormatter.info(`Created: ${latest.created_at}`)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
return cmd
|
|
67
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 连接管理命令
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander'
|
|
6
|
+
import { configStore } from '../config/store'
|
|
7
|
+
import { OutputFormatter } from '../output/formatter'
|
|
8
|
+
import { createClient } from '../api/client'
|
|
9
|
+
|
|
10
|
+
export function makeConnectCommand(): Command {
|
|
11
|
+
const cmd = new Command('connect')
|
|
12
|
+
.description('Manage connections to xuanwu platform')
|
|
13
|
+
|
|
14
|
+
cmd
|
|
15
|
+
.command('add <name>')
|
|
16
|
+
.description('Add a new connection')
|
|
17
|
+
.requiredOption('-e, --endpoint <url>', 'Platform API endpoint')
|
|
18
|
+
.requiredOption('-t, --token <token>', 'API token')
|
|
19
|
+
.action(async (name, options) => {
|
|
20
|
+
try {
|
|
21
|
+
configStore.addConnection({
|
|
22
|
+
name,
|
|
23
|
+
endpoint: options.endpoint,
|
|
24
|
+
token: options.token,
|
|
25
|
+
isDefault: configStore.getConnections().length === 0
|
|
26
|
+
})
|
|
27
|
+
OutputFormatter.success(`Connection "${name}" added`)
|
|
28
|
+
} catch (error: any) {
|
|
29
|
+
OutputFormatter.error(error.message)
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
cmd
|
|
34
|
+
.command('ls')
|
|
35
|
+
.description('List all connections')
|
|
36
|
+
.action(() => {
|
|
37
|
+
const connections = configStore.getConnections()
|
|
38
|
+
if (connections.length === 0) {
|
|
39
|
+
OutputFormatter.info('No connections configured')
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
OutputFormatter.table(
|
|
44
|
+
['Name', 'Endpoint', 'Default'],
|
|
45
|
+
connections.map(c => [
|
|
46
|
+
c.name,
|
|
47
|
+
c.endpoint,
|
|
48
|
+
c.isDefault ? '✓' : ''
|
|
49
|
+
])
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
cmd
|
|
54
|
+
.command('use <name>')
|
|
55
|
+
.description('Set default connection')
|
|
56
|
+
.action((name) => {
|
|
57
|
+
const conn = configStore.getConnection(name)
|
|
58
|
+
if (!conn) {
|
|
59
|
+
OutputFormatter.error(`Connection "${name}" not found`)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
configStore.setDefaultConnection(name)
|
|
63
|
+
OutputFormatter.success(`Using connection "${name}"`)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
cmd
|
|
67
|
+
.command('rm <name>')
|
|
68
|
+
.description('Remove a connection')
|
|
69
|
+
.action((name) => {
|
|
70
|
+
configStore.removeConnection(name)
|
|
71
|
+
OutputFormatter.success(`Connection "${name}" removed`)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
return cmd
|
|
75
|
+
}
|