xuanwu-cli 1.0.0 → 2.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/README.md +138 -88
- package/bin/xuanwu +0 -0
- package/dist/api/client.d.ts +25 -1
- package/dist/api/client.js +92 -0
- package/dist/commands/app.d.ts +5 -0
- package/dist/commands/app.js +241 -0
- package/dist/commands/auth/login.d.ts +2 -0
- package/dist/commands/auth/login.js +130 -0
- package/dist/commands/auth/logout.d.ts +2 -0
- package/dist/commands/auth/logout.js +33 -0
- package/dist/commands/auth/tokens.d.ts +2 -0
- package/dist/commands/auth/tokens.js +70 -0
- package/dist/commands/auth/whoami.d.ts +2 -0
- package/dist/commands/auth/whoami.js +32 -0
- package/dist/commands/build.js +74 -17
- package/dist/commands/svc.d.ts +1 -1
- package/dist/commands/svc.js +219 -24
- package/dist/config/types.d.ts +92 -0
- package/dist/index.js +13 -116
- package/dist/lib/auth.d.ts +6 -0
- package/dist/lib/auth.js +29 -0
- package/dist/lib/session.d.ts +18 -0
- package/dist/lib/session.js +72 -0
- package/package.json +11 -9
- package/src/api/client.ts +111 -1
- package/src/commands/app.ts +269 -0
- package/src/commands/auth/login.ts +165 -0
- package/src/commands/auth/logout.ts +34 -0
- package/src/commands/auth/tokens.ts +85 -0
- package/src/commands/auth/whoami.ts +35 -0
- package/src/commands/build.ts +78 -18
- package/src/commands/svc.ts +266 -25
- package/src/config/types.ts +105 -0
- package/src/index.ts +13 -97
- package/src/lib/auth.ts +41 -0
- package/src/lib/session.ts +50 -0
- package/test/REPORT.md +78 -0
- package/test/integration.js +431 -0
package/src/config/types.ts
CHANGED
|
@@ -97,3 +97,108 @@ export interface CLIResult<T = any> {
|
|
|
97
97
|
duration?: number
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
+
|
|
101
|
+
// ===================
|
|
102
|
+
// Application Types
|
|
103
|
+
// ===================
|
|
104
|
+
|
|
105
|
+
export interface Application {
|
|
106
|
+
id: string
|
|
107
|
+
code: string
|
|
108
|
+
name: string
|
|
109
|
+
gitRepo: string
|
|
110
|
+
gitBranch: string
|
|
111
|
+
buildPath: string
|
|
112
|
+
dockerfile: string
|
|
113
|
+
buildArgs?: Record<string, string>
|
|
114
|
+
imageName: string
|
|
115
|
+
jenkinsConfigId?: string
|
|
116
|
+
jenkinsJobName?: string
|
|
117
|
+
createdAt: string
|
|
118
|
+
updatedAt: string
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface CreateApplicationDto {
|
|
122
|
+
name: string
|
|
123
|
+
code?: string
|
|
124
|
+
gitRepo: string
|
|
125
|
+
gitBranch?: string
|
|
126
|
+
imageName?: string
|
|
127
|
+
buildPath?: string
|
|
128
|
+
dockerfile?: string
|
|
129
|
+
buildArgs?: Record<string, string>
|
|
130
|
+
jenkinsConfigId?: string
|
|
131
|
+
jenkinsJobName?: string
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface UpdateApplicationDto {
|
|
135
|
+
name?: string
|
|
136
|
+
gitRepo?: string
|
|
137
|
+
gitBranch?: string
|
|
138
|
+
imageName?: string
|
|
139
|
+
buildPath?: string
|
|
140
|
+
dockerfile?: string
|
|
141
|
+
buildArgs?: Record<string, string>
|
|
142
|
+
jenkinsConfigId?: string
|
|
143
|
+
jenkinsJobName?: string
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ===================
|
|
147
|
+
// Build Types
|
|
148
|
+
// ===================
|
|
149
|
+
|
|
150
|
+
export interface Build {
|
|
151
|
+
id: string
|
|
152
|
+
buildNumber: number
|
|
153
|
+
status: string
|
|
154
|
+
imageTag: string
|
|
155
|
+
commitSha?: string
|
|
156
|
+
commitMessage?: string
|
|
157
|
+
commitAuthor?: string
|
|
158
|
+
createdAt: string
|
|
159
|
+
completedAt?: string
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface TriggerBuildOptions {
|
|
163
|
+
gitBranch?: string
|
|
164
|
+
commitSha?: string
|
|
165
|
+
commitMessage?: string
|
|
166
|
+
commitAuthor?: string
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ===================
|
|
170
|
+
// Service Types (K8s)
|
|
171
|
+
// ===================
|
|
172
|
+
|
|
173
|
+
export interface K8sService {
|
|
174
|
+
name: string
|
|
175
|
+
namespace: string
|
|
176
|
+
type: string
|
|
177
|
+
clusterIP: string
|
|
178
|
+
externalIP?: string
|
|
179
|
+
ports: { port: number; targetPort: number; protocol: string }[]
|
|
180
|
+
age: string
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface K8sDeployment {
|
|
184
|
+
name: string
|
|
185
|
+
namespace: string
|
|
186
|
+
replicas: number
|
|
187
|
+
readyReplicas: number
|
|
188
|
+
availableReplicas: number
|
|
189
|
+
image: string
|
|
190
|
+
labels?: Record<string, string>
|
|
191
|
+
createdAt: string
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface K8sPod {
|
|
195
|
+
name: string
|
|
196
|
+
namespace: string
|
|
197
|
+
status: string
|
|
198
|
+
ready: string
|
|
199
|
+
restarts: number
|
|
200
|
+
age: string
|
|
201
|
+
ip: string
|
|
202
|
+
node: string
|
|
203
|
+
containers: { name: string; image: string }[]
|
|
204
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,121 +7,37 @@ import { configStore } from './config/store'
|
|
|
7
7
|
import { makeConnectCommand } from './commands/connect'
|
|
8
8
|
import { makeEnvCommand } from './commands/env'
|
|
9
9
|
import { makeDeployCommand } from './commands/deploy'
|
|
10
|
+
import { makeAppCommand } from './commands/app'
|
|
10
11
|
import { makeSvcCommand } from './commands/svc'
|
|
11
12
|
import { makeBuildCommand } from './commands/build'
|
|
12
13
|
import { makeScaleCommand } from './commands/scale'
|
|
13
14
|
import { makeLogsCommand } from './commands/logs'
|
|
14
15
|
import { makePodsCommand } from './commands/pods'
|
|
16
|
+
import { makeLoginCommand } from './commands/auth/login'
|
|
17
|
+
import { makeLogoutCommand } from './commands/auth/logout'
|
|
18
|
+
import { makeWhoamiCommand } from './commands/auth/whoami'
|
|
19
|
+
import { makeTokensCommand } from './commands/auth/tokens'
|
|
15
20
|
|
|
16
21
|
const program = new Command()
|
|
17
22
|
|
|
18
23
|
program
|
|
19
|
-
.name('
|
|
24
|
+
.name('xw')
|
|
20
25
|
.description('玄武工厂平台 CLI 工具')
|
|
21
|
-
.version('
|
|
26
|
+
.version('2.0.0')
|
|
22
27
|
.option('-o, --output <format>', 'Output format (human|json)', 'human')
|
|
23
28
|
|
|
24
|
-
// 添加子命令
|
|
25
29
|
program.addCommand(makeConnectCommand())
|
|
26
30
|
program.addCommand(makeEnvCommand())
|
|
27
|
-
program.addCommand(
|
|
31
|
+
program.addCommand(makeAppCommand())
|
|
28
32
|
program.addCommand(makeSvcCommand())
|
|
29
33
|
program.addCommand(makeBuildCommand())
|
|
34
|
+
program.addCommand(makeDeployCommand())
|
|
30
35
|
program.addCommand(makeScaleCommand())
|
|
31
36
|
program.addCommand(makeLogsCommand())
|
|
32
37
|
program.addCommand(makePodsCommand())
|
|
38
|
+
program.addCommand(makeLoginCommand())
|
|
39
|
+
program.addCommand(makeLogoutCommand())
|
|
40
|
+
program.addCommand(makeWhoamiCommand())
|
|
41
|
+
program.addCommand(makeTokensCommand())
|
|
33
42
|
|
|
34
|
-
// 快捷命令:直接 deploy 也支持
|
|
35
|
-
program
|
|
36
|
-
.command('up <namespace> <service-name>')
|
|
37
|
-
.description('Build and deploy a service')
|
|
38
|
-
.action(async (namespace, serviceName) => {
|
|
39
|
-
const { makeBuildCommand } = await import('./commands/build')
|
|
40
|
-
const buildCmd = makeBuildCommand()
|
|
41
|
-
// 简化:只调用部署
|
|
42
|
-
console.log('Use: xuanwu build <namespace> <service-name> && xuanwu deploy <namespace> <service-name>')
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
// restart 命令
|
|
46
|
-
program
|
|
47
|
-
.command('restart <namespace> <service-name>')
|
|
48
|
-
.description('Restart a service')
|
|
49
|
-
.action(async (namespace, serviceName) => {
|
|
50
|
-
console.log('Restarting service... (not implemented)')
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
// exec 命令
|
|
54
|
-
program
|
|
55
|
-
.command('exec <namespace> <service-name>')
|
|
56
|
-
.description('Execute command in container')
|
|
57
|
-
.option('-c, --command <cmd>', 'Command to execute', 'ls -la')
|
|
58
|
-
.option('-p, --pod <pod-name>', 'Specific pod name')
|
|
59
|
-
.action(async (namespace, serviceName, options) => {
|
|
60
|
-
const { configStore } = await import('./config/store')
|
|
61
|
-
const { createClient } = await import('./api/client')
|
|
62
|
-
const { OutputFormatter } = await import('./output/formatter')
|
|
63
|
-
|
|
64
|
-
const conn = configStore.getDefaultConnection()
|
|
65
|
-
if (!conn) {
|
|
66
|
-
OutputFormatter.error('No connection configured')
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const client = createClient(conn)
|
|
71
|
-
const result = await client.exec(namespace, serviceName, options.command, options.pod)
|
|
72
|
-
|
|
73
|
-
if (!result.success) {
|
|
74
|
-
OutputFormatter.error(result.error!.message)
|
|
75
|
-
return
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const data = result.data
|
|
79
|
-
if (data.stdout) console.log(data.stdout)
|
|
80
|
-
if (data.stderr) console.error(data.stderr)
|
|
81
|
-
if (data.exitCode !== 0) {
|
|
82
|
-
process.exit(data.exitCode)
|
|
83
|
-
}
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
// top 命令
|
|
87
|
-
program
|
|
88
|
-
.command('top <namespace> <service-name>')
|
|
89
|
-
.description('Show resource usage')
|
|
90
|
-
.option('-p, --pod <pod-name>', 'Specific pod name')
|
|
91
|
-
.action(async (namespace, serviceName, options) => {
|
|
92
|
-
const { configStore } = await import('./config/store')
|
|
93
|
-
const { createClient } = await import('./api/client')
|
|
94
|
-
const { OutputFormatter } = await import('./output/formatter')
|
|
95
|
-
|
|
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.getMetrics(namespace, serviceName, options.pod)
|
|
104
|
-
|
|
105
|
-
if (!result.success) {
|
|
106
|
-
OutputFormatter.error(result.error!.message)
|
|
107
|
-
return
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const pods = result.data?.pods || []
|
|
111
|
-
if (pods.length === 0) {
|
|
112
|
-
OutputFormatter.info('No pod metrics found')
|
|
113
|
-
return
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
OutputFormatter.table(
|
|
117
|
-
['Pod Name', 'CPU', 'Memory'],
|
|
118
|
-
pods.map((p: any) => [
|
|
119
|
-
p.name,
|
|
120
|
-
p.containers?.[0]?.cpuRaw || 'N/A',
|
|
121
|
-
p.containers?.[0]?.memoryRaw || 'N/A'
|
|
122
|
-
])
|
|
123
|
-
)
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
// 解析参数
|
|
127
43
|
program.parse(process.argv)
|
package/src/lib/auth.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { SessionManager } from './session'
|
|
2
|
+
|
|
3
|
+
export interface ApiOptions {
|
|
4
|
+
method?: string
|
|
5
|
+
body?: any
|
|
6
|
+
headers?: Record<string, string>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function authenticatedFetch(
|
|
10
|
+
url: string,
|
|
11
|
+
options: ApiOptions = {}
|
|
12
|
+
): Promise<Response> {
|
|
13
|
+
const sessionManager = new SessionManager()
|
|
14
|
+
const session = await sessionManager.loadSession()
|
|
15
|
+
|
|
16
|
+
if (!session) {
|
|
17
|
+
throw new Error('Not logged in. Please run: xw login')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (sessionManager.isExpired(session)) {
|
|
21
|
+
await sessionManager.clearSession()
|
|
22
|
+
throw new Error('Session expired. Please run: xw login')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const response = await fetch(url, {
|
|
26
|
+
method: options.method || 'GET',
|
|
27
|
+
headers: {
|
|
28
|
+
'Authorization': `Bearer ${session.token}`,
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
...options.headers
|
|
31
|
+
},
|
|
32
|
+
body: options.body ? JSON.stringify(options.body) : undefined
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
if (response.status === 401) {
|
|
36
|
+
await sessionManager.clearSession()
|
|
37
|
+
throw new Error('Authentication failed. Please run: xw login')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return response
|
|
41
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as fs from 'fs/promises'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
import * as os from 'os'
|
|
4
|
+
|
|
5
|
+
export interface Session {
|
|
6
|
+
token: string
|
|
7
|
+
userId: string
|
|
8
|
+
userName: string
|
|
9
|
+
userEmail: string
|
|
10
|
+
userRole: string
|
|
11
|
+
deviceId: string
|
|
12
|
+
expiresAt: string | null
|
|
13
|
+
apiUrl: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class SessionManager {
|
|
17
|
+
private sessionPath: string
|
|
18
|
+
|
|
19
|
+
constructor() {
|
|
20
|
+
this.sessionPath = path.join(os.homedir(), '.xw', 'session.json')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async saveSession(session: Session): Promise<void> {
|
|
24
|
+
const dir = path.dirname(this.sessionPath)
|
|
25
|
+
await fs.mkdir(dir, { recursive: true })
|
|
26
|
+
await fs.writeFile(this.sessionPath, JSON.stringify(session, null, 2))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async loadSession(): Promise<Session | null> {
|
|
30
|
+
try {
|
|
31
|
+
const content = await fs.readFile(this.sessionPath, 'utf-8')
|
|
32
|
+
return JSON.parse(content)
|
|
33
|
+
} catch {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async clearSession(): Promise<void> {
|
|
39
|
+
try {
|
|
40
|
+
await fs.unlink(this.sessionPath)
|
|
41
|
+
} catch {
|
|
42
|
+
// Ignore errors
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
isExpired(session: Session): boolean {
|
|
47
|
+
if (!session.expiresAt) return false
|
|
48
|
+
return new Date(session.expiresAt) < new Date()
|
|
49
|
+
}
|
|
50
|
+
}
|
package/test/REPORT.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# 集成测试报告 - CLI API v2.0
|
|
2
|
+
|
|
3
|
+
> 测试时间: 2026-03-11 06:13:57
|
|
4
|
+
> 测试环境: localhost:3000
|
|
5
|
+
> 测试用户: test@cli.local (ADMIN)
|
|
6
|
+
|
|
7
|
+
## 执行摘要
|
|
8
|
+
|
|
9
|
+
| 指标 | 数值 |
|
|
10
|
+
|------|------|
|
|
11
|
+
| 总测试数 | 17 |
|
|
12
|
+
| 通过 | 17 ✅ |
|
|
13
|
+
| 失败 | 0 ❌ |
|
|
14
|
+
| **通过率** | **100.0%** |
|
|
15
|
+
|
|
16
|
+
## 测试结果详情
|
|
17
|
+
|
|
18
|
+
### Application API (使用 code 标识)
|
|
19
|
+
|
|
20
|
+
| # | 测试项 | 状态 | 说明 |
|
|
21
|
+
|---|--------|------|------|
|
|
22
|
+
| 1 | listApplications | ✅ PASS | 列出所有应用 |
|
|
23
|
+
| 2 | createApplication | ✅ PASS | 创建应用 (code: xw-cli-test-app) |
|
|
24
|
+
| 3 | getApplication | ✅ PASS | 获取应用详情 |
|
|
25
|
+
| 4 | updateApplication | ✅ PASS | 更新应用 |
|
|
26
|
+
| 5 | triggerApplicationBuild | ✅ PASS | 触发构建 (buildNumber: 1, status: QUEUED) |
|
|
27
|
+
| 6 | listApplicationBuilds | ✅ PASS | 列出应用构建 |
|
|
28
|
+
| 7 | listBuilds | ✅ PASS | 列出所有构建 |
|
|
29
|
+
|
|
30
|
+
### Build API
|
|
31
|
+
|
|
32
|
+
| # | 测试项 | 状态 | 说明 |
|
|
33
|
+
|---|--------|------|------|
|
|
34
|
+
| 8 | getBuild | ✅ PASS | 获取构建详情 |
|
|
35
|
+
|
|
36
|
+
### Service API (使用 ns/name 路径)
|
|
37
|
+
|
|
38
|
+
| # | 测试项 | 状态 | 说明 |
|
|
39
|
+
|---|--------|------|------|
|
|
40
|
+
| 9 | listK8sServices | ✅ PASS | 列出所有 K8s 服务 |
|
|
41
|
+
| 10 | listK8sServices (namespace) | ✅ PASS | 列出指定 namespace 服务 |
|
|
42
|
+
| 11 | getK8sService | ✅ PASS | 获取服务详情 (cli-test-fix/nginx) |
|
|
43
|
+
| 12 | getK8sServiceStatus | ✅ PASS | 获取部署状态 |
|
|
44
|
+
| 13 | getK8sServiceLogs | ✅ PASS | 获取服务日志 |
|
|
45
|
+
| 14 | listK8sServicePods | ✅ PASS | 列出 Pods |
|
|
46
|
+
| 15 | scaleK8sService | ✅ PASS | 扩缩容 (扩到 2 副本) |
|
|
47
|
+
| 16 | restartK8sService | ✅ PASS | 重启服务 |
|
|
48
|
+
| 17 | scaleK8sService (restore) | ✅ PASS | 恢复扩缩容 (缩到 1 副本) |
|
|
49
|
+
|
|
50
|
+
## 测试数据
|
|
51
|
+
|
|
52
|
+
- **应用**: xw-cli-test-app
|
|
53
|
+
- **命名空间**: cli-test-fix
|
|
54
|
+
- **服务**: nginx (image: nginx:latest)
|
|
55
|
+
|
|
56
|
+
## API 路由验证
|
|
57
|
+
|
|
58
|
+
所有设计文档中定义的 CLI API 路由均已实现并正常工作:
|
|
59
|
+
|
|
60
|
+
- `GET /api/cli/apps` ✅
|
|
61
|
+
- `POST /api/applications` ✅
|
|
62
|
+
- `GET /api/cli/apps/:code` ✅
|
|
63
|
+
- `PUT /api/cli/apps/:code` ✅
|
|
64
|
+
- `POST /api/cli/apps/:code/build` ✅
|
|
65
|
+
- `GET /api/cli/apps/:code/builds` ✅
|
|
66
|
+
- `GET /api/cli/builds` ✅
|
|
67
|
+
- `GET /api/cli/builds/:id` ✅
|
|
68
|
+
- `GET /api/cli/services` ✅
|
|
69
|
+
- `GET /api/cli/services/:ns/:name` ✅
|
|
70
|
+
- `GET /api/cli/services/:ns/:name/status` ✅
|
|
71
|
+
- `GET /api/cli/services/:ns/:name/logs` ✅
|
|
72
|
+
- `POST /api/cli/services/:ns/:name/restart` ✅
|
|
73
|
+
- `POST /api/cli/services/:ns/:name/scale` ✅
|
|
74
|
+
- `GET /api/cli/services/:ns/:name/pods` ✅
|
|
75
|
+
|
|
76
|
+
## 结论
|
|
77
|
+
|
|
78
|
+
CLI v2.0 重构成功!所有 17 个集成测试全部通过,API 功能完整可用。
|