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
package/README.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# xuanwu-cli
|
|
2
|
+
|
|
3
|
+
玄武工厂平台 CLI 工具
|
|
4
|
+
|
|
5
|
+
## 快速开始
|
|
6
|
+
|
|
7
|
+
### 1. 安装依赖
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### 2. 编译
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm run build
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### 3. 配置连接
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# 添加连接
|
|
23
|
+
node bin/xuanwu connect add prod \
|
|
24
|
+
--endpoint http://localhost:3000 \
|
|
25
|
+
--token <your-api-token>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 4. 运行演示
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
./demo.sh
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 5. 运行验证测试
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
./verify.sh
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 功能验证
|
|
41
|
+
|
|
42
|
+
运行验证脚本以测试所有已实现的功能:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
./verify.sh
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
验证内容包括:
|
|
49
|
+
- ✅ 依赖检查(Node.js, jq, curl)
|
|
50
|
+
- ✅ 后端服务连接
|
|
51
|
+
- ✅ CLI 编译和执行
|
|
52
|
+
- ✅ 用户认证(注册、登录)
|
|
53
|
+
- ✅ 环境管理(列出、创建)
|
|
54
|
+
- ✅ 服务管理(列出、查询)
|
|
55
|
+
- ✅ 所有 CLI 命令
|
|
56
|
+
|
|
57
|
+
## 详细文档
|
|
58
|
+
|
|
59
|
+
- [API 任务清单](../xuanwu-factory-v2/docs/tasks.md)
|
|
60
|
+
- [功能验证报告](../xuanwu-factory-v2/docs/verification-report.md)
|
|
61
|
+
|
|
62
|
+
## 当前状态
|
|
63
|
+
|
|
64
|
+
**阶段 1 (基础功能)**: ✅ 完成
|
|
65
|
+
- 环境管理 API
|
|
66
|
+
- 服务管理 API
|
|
67
|
+
- K8s 查询 API
|
|
68
|
+
- 扩缩容 API
|
|
69
|
+
- 用户认证 API
|
|
70
|
+
|
|
71
|
+
**验证结果**: 19/19 测试通过 ✅
|
|
72
|
+
|
|
73
|
+
## 获取 API Token
|
|
74
|
+
|
|
75
|
+
### 方法1: Web 界面
|
|
76
|
+
|
|
77
|
+
登录平台后,进入 `用户菜单 → API Token` 创建新 Token
|
|
78
|
+
|
|
79
|
+
### 方法2: 命令行
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# 登录获取 JWT Token
|
|
83
|
+
curl -X POST http://localhost:3000/api/auth/login \
|
|
84
|
+
-H "Content-Type: application/json" \
|
|
85
|
+
-d '{"email": "your@email.com", "password": "password"}'
|
|
86
|
+
|
|
87
|
+
# 使用 JWT Token 创建 API Token
|
|
88
|
+
curl -X POST http://localhost:3000/api/users/api-tokens \
|
|
89
|
+
-H "Authorization: Bearer <jwt-token>" \
|
|
90
|
+
-H "Content-Type: application/json" \
|
|
91
|
+
-d '{"name": "cli-token"}'
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 快速开始
|
|
95
|
+
|
|
96
|
+
### 1. 配置连接
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
xuanwu connect add prod --endpoint https://xuanwu.company.com --token your-token
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 2. 环境管理
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# 列出环境
|
|
106
|
+
xuanwu env ls
|
|
107
|
+
|
|
108
|
+
# 创建环境
|
|
109
|
+
xuanwu env create shop-dev
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 3. 部署服务
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# 部署镜像
|
|
116
|
+
xuanwu deploy shop-dev nginx --type image --image nginx:latest
|
|
117
|
+
|
|
118
|
+
# 部署数据库
|
|
119
|
+
xuanwu deploy shop-dev mysql --type database --db-type mysql --db-version 8.0
|
|
120
|
+
|
|
121
|
+
# 部署应用(源码构建)
|
|
122
|
+
xuanwu deploy shop --type application \
|
|
123
|
+
--git https-dev api://gitlab.com/user/repo.git \
|
|
124
|
+
--git-branch main \
|
|
125
|
+
--build-type template \
|
|
126
|
+
--language java-springboot
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 4. 查看服务
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# 列出服务
|
|
133
|
+
xuanwu svc ls shop-dev
|
|
134
|
+
|
|
135
|
+
# 服务状态
|
|
136
|
+
xuanwu svc status shop-dev nginx
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 5. 调试
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# 查看日志
|
|
143
|
+
xuanwu logs shop-dev nginx -n 100
|
|
144
|
+
|
|
145
|
+
# 实时日志
|
|
146
|
+
xuanwu logs shop-dev nginx -f
|
|
147
|
+
|
|
148
|
+
# Pod 列表
|
|
149
|
+
xuanwu pods shop-dev nginx
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## 命令参考
|
|
153
|
+
|
|
154
|
+
| 命令 | 说明 |
|
|
155
|
+
|------|------|
|
|
156
|
+
| `connect` | 管理连接 |
|
|
157
|
+
| `env` | 环境管理 |
|
|
158
|
+
| `deploy` | 部署服务 |
|
|
159
|
+
| `svc` | 服务管理 |
|
|
160
|
+
| `build` | 构建应用 |
|
|
161
|
+
| `scale` | 扩缩容 |
|
|
162
|
+
| `logs` | 查看日志 |
|
|
163
|
+
| `pods` | Pod 列表 |
|
|
164
|
+
|
|
165
|
+
## 输出格式
|
|
166
|
+
|
|
167
|
+
默认输出人类可读格式,使用 `-o json` 切换到 JSON 格式:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
xuanwu env ls -o json
|
|
171
|
+
```
|
package/bin/xuanwu
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* xuanwu-cli 入口脚本
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { spawn } = require('child_process');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const distPath = path.join(__dirname, '..', 'dist', 'index.js');
|
|
12
|
+
|
|
13
|
+
const child = spawn('node', [distPath, ...args], {
|
|
14
|
+
stdio: 'inherit',
|
|
15
|
+
shell: process.platform === 'win32'
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
child.on('exit', (code) => {
|
|
19
|
+
process.exit(code || 0);
|
|
20
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API 客户端
|
|
3
|
+
*/
|
|
4
|
+
import { Connection, CLIResult, ServiceInfo, NamespaceInfo, DeployOptions } from '../config/types';
|
|
5
|
+
export declare class APIClient {
|
|
6
|
+
private client;
|
|
7
|
+
constructor(connection: Connection);
|
|
8
|
+
private request;
|
|
9
|
+
listNamespaces(): Promise<CLIResult<NamespaceInfo[]>>;
|
|
10
|
+
createNamespace(name: string): Promise<CLIResult<any>>;
|
|
11
|
+
deleteNamespace(id: string): Promise<CLIResult<void>>;
|
|
12
|
+
getNamespaceInfo(identifier: string): Promise<CLIResult<any>>;
|
|
13
|
+
getProjects(): Promise<CLIResult<any[]>>;
|
|
14
|
+
createNamespaceWithProject(name: string, projectId: string, environment?: string): Promise<CLIResult<any>>;
|
|
15
|
+
listServices(namespace: string): Promise<CLIResult<ServiceInfo[]>>;
|
|
16
|
+
getServiceStatus(namespace: string, name: string): Promise<CLIResult<any>>;
|
|
17
|
+
deleteService(namespace: string, name: string): Promise<CLIResult<void>>;
|
|
18
|
+
deploy(options: DeployOptions): Promise<CLIResult<any>>;
|
|
19
|
+
private getDefaultPort;
|
|
20
|
+
build(namespace: string, serviceName: string): Promise<CLIResult<any>>;
|
|
21
|
+
getBuildStatus(namespace: string, serviceName: string): Promise<CLIResult<any>>;
|
|
22
|
+
private getServiceByName;
|
|
23
|
+
scale(namespace: string, serviceName: string, replicas: number): Promise<CLIResult<any>>;
|
|
24
|
+
getLogs(namespace: string, serviceName: string, lines?: number, follow?: boolean): Promise<CLIResult<any>>;
|
|
25
|
+
streamLogs(namespace: string, serviceName: string): Promise<void>;
|
|
26
|
+
listPods(namespace: string, serviceName: string): Promise<CLIResult<any>>;
|
|
27
|
+
getMetrics(namespace: string, serviceName: string, podName?: string): Promise<CLIResult<any>>;
|
|
28
|
+
exec(namespace: string, serviceName: string, command: string, podName?: string): Promise<CLIResult<any>>;
|
|
29
|
+
}
|
|
30
|
+
export declare function createClient(connection: Connection): APIClient;
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* API 客户端
|
|
4
|
+
*/
|
|
5
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.APIClient = void 0;
|
|
10
|
+
exports.createClient = createClient;
|
|
11
|
+
const axios_1 = __importDefault(require("axios"));
|
|
12
|
+
const formatter_1 = require("../output/formatter");
|
|
13
|
+
class APIClient {
|
|
14
|
+
constructor(connection) {
|
|
15
|
+
this.client = axios_1.default.create({
|
|
16
|
+
baseURL: connection.endpoint,
|
|
17
|
+
headers: {
|
|
18
|
+
'Authorization': `Bearer ${connection.token}`,
|
|
19
|
+
'Content-Type': 'application/json'
|
|
20
|
+
},
|
|
21
|
+
timeout: 30000
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
async request(method, url, data, config) {
|
|
25
|
+
const startTime = Date.now();
|
|
26
|
+
try {
|
|
27
|
+
const response = await this.client.request({
|
|
28
|
+
method,
|
|
29
|
+
url,
|
|
30
|
+
data,
|
|
31
|
+
...config
|
|
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
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const duration = Date.now() - startTime;
|
|
44
|
+
const message = error.response?.data?.message || error.message;
|
|
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
|
+
// Namespace (环境)
|
|
61
|
+
// ===================
|
|
62
|
+
async listNamespaces() {
|
|
63
|
+
return this.request('GET', '/api/deploy-spaces');
|
|
64
|
+
}
|
|
65
|
+
async createNamespace(name) {
|
|
66
|
+
return this.request('POST', '/api/deploy-spaces', {
|
|
67
|
+
name,
|
|
68
|
+
identifier: name,
|
|
69
|
+
namespace: name,
|
|
70
|
+
environment: 'development'
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async deleteNamespace(id) {
|
|
74
|
+
return this.request('DELETE', `/api/deploy-spaces/${id}`);
|
|
75
|
+
}
|
|
76
|
+
async getNamespaceInfo(identifier) {
|
|
77
|
+
return this.request('GET', `/api/deploy-spaces?identifier=${identifier}`);
|
|
78
|
+
}
|
|
79
|
+
async getProjects() {
|
|
80
|
+
return this.request('GET', '/api/projects');
|
|
81
|
+
}
|
|
82
|
+
async createNamespaceWithProject(name, projectId, environment = 'development') {
|
|
83
|
+
return this.request('POST', '/api/deploy-spaces', {
|
|
84
|
+
name,
|
|
85
|
+
identifier: name,
|
|
86
|
+
namespace: name,
|
|
87
|
+
environment,
|
|
88
|
+
project_id: projectId
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
// ===================
|
|
92
|
+
// Service
|
|
93
|
+
// ===================
|
|
94
|
+
async listServices(namespace) {
|
|
95
|
+
return this.request('GET', `/api/k8s?kind=deployments&namespace=${namespace}`);
|
|
96
|
+
}
|
|
97
|
+
async getServiceStatus(namespace, name) {
|
|
98
|
+
return this.request('GET', `/api/k8s/deployments/${name}?namespace=${namespace}`);
|
|
99
|
+
}
|
|
100
|
+
async deleteService(namespace, name) {
|
|
101
|
+
return this.request('DELETE', `/api/services/by-ns/${namespace}/${name}`);
|
|
102
|
+
}
|
|
103
|
+
// ===================
|
|
104
|
+
// Deploy
|
|
105
|
+
// ===================
|
|
106
|
+
async deploy(options) {
|
|
107
|
+
const { namespace, serviceName, type, ...rest } = options;
|
|
108
|
+
// 获取 namespace 对应的 project_id
|
|
109
|
+
let projectId;
|
|
110
|
+
// 尝试通过 identifier 查询
|
|
111
|
+
const envResult = await this.getNamespaceInfo(namespace);
|
|
112
|
+
if (envResult.success && envResult.data) {
|
|
113
|
+
const envData = Array.isArray(envResult.data) ? envResult.data[0] : envResult.data;
|
|
114
|
+
projectId = envData.project?.id || envData.projectId;
|
|
115
|
+
}
|
|
116
|
+
// 如果没找到,尝试从列表中查找
|
|
117
|
+
if (!projectId) {
|
|
118
|
+
const listResult = await this.listNamespaces();
|
|
119
|
+
if (listResult.success && listResult.data) {
|
|
120
|
+
const env = listResult.data.find((e) => e.namespace === namespace || e.identifier === namespace);
|
|
121
|
+
if (env) {
|
|
122
|
+
projectId = env.project?.id || env.projectId;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// 如果还是没有,使用第一个项目的 ID
|
|
127
|
+
if (!projectId) {
|
|
128
|
+
const projectsResult = await this.getProjects();
|
|
129
|
+
if (projectsResult.success && projectsResult.data && projectsResult.data.length > 0) {
|
|
130
|
+
projectId = projectsResult.data[0].id;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!projectId) {
|
|
134
|
+
return {
|
|
135
|
+
success: false,
|
|
136
|
+
error: { code: 'NOT_FOUND', message: `Cannot find project for namespace "${namespace}"` }
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const payload = {
|
|
140
|
+
project_id: projectId,
|
|
141
|
+
name: serviceName,
|
|
142
|
+
type
|
|
143
|
+
};
|
|
144
|
+
// 根据类型添加配置
|
|
145
|
+
if (type === 'application') {
|
|
146
|
+
payload.git_provider = 'gitlab';
|
|
147
|
+
payload.git_repository = rest.git;
|
|
148
|
+
payload.git_branch = rest.gitBranch || 'main';
|
|
149
|
+
payload.build_type = rest.buildType || 'template';
|
|
150
|
+
payload.port = rest.port || 8080;
|
|
151
|
+
}
|
|
152
|
+
else if (type === 'database') {
|
|
153
|
+
payload.database_type = rest.dbType;
|
|
154
|
+
payload.version = rest.dbVersion || '8.0';
|
|
155
|
+
payload.root_password = rest.rootPassword || 'root';
|
|
156
|
+
payload.port = rest.port || this.getDefaultPort(rest.dbType);
|
|
157
|
+
}
|
|
158
|
+
else if (type === 'image') {
|
|
159
|
+
payload.image = rest.image;
|
|
160
|
+
payload.port = rest.port || 80;
|
|
161
|
+
}
|
|
162
|
+
// 添加通用配置
|
|
163
|
+
payload.replicas = rest.replicas || 1;
|
|
164
|
+
payload.port = rest.port || 8080;
|
|
165
|
+
// 添加资源限制
|
|
166
|
+
if (rest.cpu || rest.memory) {
|
|
167
|
+
payload.resource_limits = {
|
|
168
|
+
cpu: rest.cpu || '500m',
|
|
169
|
+
memory: rest.memory || '512Mi'
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
// 构建网络配置
|
|
173
|
+
payload.network_config = {
|
|
174
|
+
service_type: 'ClusterIP',
|
|
175
|
+
ports: [{
|
|
176
|
+
container_port: payload.port,
|
|
177
|
+
protocol: 'TCP'
|
|
178
|
+
}]
|
|
179
|
+
};
|
|
180
|
+
// 添加域名
|
|
181
|
+
if (rest.domain) {
|
|
182
|
+
payload.network_config.ports[0].domain = {
|
|
183
|
+
enabled: true,
|
|
184
|
+
prefix: rest.domain,
|
|
185
|
+
host: `${rest.domain}.${namespace}.dev.aimstek.cn`
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
// 环境变量
|
|
189
|
+
if (rest.envVars) {
|
|
190
|
+
payload.env_vars = rest.envVars;
|
|
191
|
+
}
|
|
192
|
+
return this.request('POST', '/api/services', payload);
|
|
193
|
+
}
|
|
194
|
+
getDefaultPort(dbType) {
|
|
195
|
+
const ports = {
|
|
196
|
+
mysql: 3306,
|
|
197
|
+
redis: 6379,
|
|
198
|
+
postgres: 5432,
|
|
199
|
+
elasticsearch: 9200
|
|
200
|
+
};
|
|
201
|
+
return ports[dbType] || 3306;
|
|
202
|
+
}
|
|
203
|
+
// ===================
|
|
204
|
+
// Build
|
|
205
|
+
// ===================
|
|
206
|
+
async build(namespace, serviceName) {
|
|
207
|
+
// 先获取服务 ID
|
|
208
|
+
const serviceResult = await this.getServiceByName(namespace, serviceName);
|
|
209
|
+
if (!serviceResult.success || !serviceResult.data) {
|
|
210
|
+
return {
|
|
211
|
+
success: false,
|
|
212
|
+
error: { code: 'NOT_FOUND', message: `Service ${serviceName} not found` }
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const service = serviceResult.data;
|
|
216
|
+
if (!service.application_id) {
|
|
217
|
+
return {
|
|
218
|
+
success: false,
|
|
219
|
+
error: { code: 'NO_APPLICATION', message: 'Service is not an application type' }
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return this.request('POST', `/api/applications/${service.application_id}/build`);
|
|
223
|
+
}
|
|
224
|
+
async getBuildStatus(namespace, serviceName) {
|
|
225
|
+
const serviceResult = await this.getServiceByName(namespace, serviceName);
|
|
226
|
+
if (!serviceResult.success || !serviceResult.data) {
|
|
227
|
+
return { success: false, error: { code: 'NOT_FOUND', message: 'Service not found' } };
|
|
228
|
+
}
|
|
229
|
+
const service = serviceResult.data;
|
|
230
|
+
if (!service.application_id) {
|
|
231
|
+
return { success: false, error: { code: 'NO_APPLICATION', message: 'Not an application' } };
|
|
232
|
+
}
|
|
233
|
+
return this.request('GET', `/api/applications/${service.application_id}/builds`);
|
|
234
|
+
}
|
|
235
|
+
async getServiceByName(namespace, name) {
|
|
236
|
+
return this.request('GET', `/api/services?project_id=${namespace}&name=${name}`);
|
|
237
|
+
}
|
|
238
|
+
// ===================
|
|
239
|
+
// Scale
|
|
240
|
+
// ===================
|
|
241
|
+
async scale(namespace, serviceName, replicas) {
|
|
242
|
+
return this.request('POST', `/api/k8s?namespace=${namespace}&action=scale`, {
|
|
243
|
+
name: serviceName,
|
|
244
|
+
replicas
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
// ===================
|
|
248
|
+
// Logs
|
|
249
|
+
// ===================
|
|
250
|
+
async getLogs(namespace, serviceName, lines = 100, follow = false) {
|
|
251
|
+
return this.request('GET', `/api/k8s/logs?namespace=${namespace}&serviceName=${serviceName}&lines=${lines}&follow=${follow}`);
|
|
252
|
+
}
|
|
253
|
+
async streamLogs(namespace, serviceName) {
|
|
254
|
+
const url = `${this.client.defaults.baseURL}/api/k8s/logs/stream?namespace=${namespace}&serviceName=${serviceName}`;
|
|
255
|
+
try {
|
|
256
|
+
const response = await fetch(url, {
|
|
257
|
+
headers: {
|
|
258
|
+
'Authorization': `Bearer ${this.client.defaults.headers['Authorization']}`
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
if (!response.ok) {
|
|
262
|
+
const error = await response.text();
|
|
263
|
+
formatter_1.OutputFormatter.error(`Failed to stream logs: ${error}`);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const reader = response.body?.getReader();
|
|
267
|
+
if (!reader) {
|
|
268
|
+
formatter_1.OutputFormatter.error('Failed to read stream');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const decoder = new TextDecoder();
|
|
272
|
+
while (true) {
|
|
273
|
+
const { done, value } = await reader.read();
|
|
274
|
+
if (done)
|
|
275
|
+
break;
|
|
276
|
+
const text = decoder.decode(value);
|
|
277
|
+
// 解析 SSE 格式
|
|
278
|
+
const lines = text.split('\n');
|
|
279
|
+
for (const line of lines) {
|
|
280
|
+
if (line.startsWith('data: ')) {
|
|
281
|
+
console.log(line.slice(6));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
formatter_1.OutputFormatter.error(`Stream error: ${error.message}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// ===================
|
|
291
|
+
// Pods
|
|
292
|
+
// ===================
|
|
293
|
+
async listPods(namespace, serviceName) {
|
|
294
|
+
return this.request('GET', `/api/k8s?kind=pods&namespace=${namespace}&labelSelector=app=${serviceName}`);
|
|
295
|
+
}
|
|
296
|
+
// ===================
|
|
297
|
+
// Metrics
|
|
298
|
+
// ===================
|
|
299
|
+
async getMetrics(namespace, serviceName, podName) {
|
|
300
|
+
const url = podName
|
|
301
|
+
? `/api/k8s/metrics?namespace=${namespace}&podName=${podName}`
|
|
302
|
+
: `/api/k8s/metrics?namespace=${namespace}&serviceName=${serviceName}`;
|
|
303
|
+
return this.request('GET', url);
|
|
304
|
+
}
|
|
305
|
+
// ===================
|
|
306
|
+
// Exec
|
|
307
|
+
// ===================
|
|
308
|
+
async exec(namespace, serviceName, command, podName) {
|
|
309
|
+
let targetPod = podName;
|
|
310
|
+
// 如果没有指定 podName,自动获取第一个 pod
|
|
311
|
+
if (!targetPod) {
|
|
312
|
+
const podsResult = await this.listPods(namespace, serviceName);
|
|
313
|
+
if (!podsResult.success || !podsResult.data || podsResult.data.length === 0) {
|
|
314
|
+
return { success: false, error: { code: 'NOT_FOUND', message: 'No pods found' } };
|
|
315
|
+
}
|
|
316
|
+
const pods = Array.isArray(podsResult.data) ? podsResult.data : podsResult.data.items || [];
|
|
317
|
+
if (pods.length === 0) {
|
|
318
|
+
return { success: false, error: { code: 'NOT_FOUND', message: 'No pods found' } };
|
|
319
|
+
}
|
|
320
|
+
targetPod = pods[0].metadata?.name;
|
|
321
|
+
}
|
|
322
|
+
return this.request('POST', '/api/debug/pod-exec', {
|
|
323
|
+
namespace,
|
|
324
|
+
podName: targetPod,
|
|
325
|
+
command
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
exports.APIClient = APIClient;
|
|
330
|
+
function createClient(connection) {
|
|
331
|
+
return new APIClient(connection);
|
|
332
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 构建命令
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.makeBuildCommand = makeBuildCommand;
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const store_1 = require("../config/store");
|
|
9
|
+
const client_1 = require("../api/client");
|
|
10
|
+
const formatter_1 = require("../output/formatter");
|
|
11
|
+
function makeBuildCommand() {
|
|
12
|
+
const cmd = new commander_1.Command('build')
|
|
13
|
+
.description('Build applications');
|
|
14
|
+
cmd
|
|
15
|
+
.command('<namespace> <service-name>')
|
|
16
|
+
.description('Build a service')
|
|
17
|
+
.action(async (namespace, serviceName) => {
|
|
18
|
+
const conn = store_1.configStore.getDefaultConnection();
|
|
19
|
+
if (!conn) {
|
|
20
|
+
formatter_1.OutputFormatter.error('No connection configured');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const client = (0, client_1.createClient)(conn);
|
|
24
|
+
formatter_1.OutputFormatter.info('Starting build...');
|
|
25
|
+
const result = await client.build(namespace, serviceName);
|
|
26
|
+
if (!result.success) {
|
|
27
|
+
formatter_1.OutputFormatter.error(result.error.message);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
formatter_1.OutputFormatter.success('Build started');
|
|
31
|
+
});
|
|
32
|
+
cmd
|
|
33
|
+
.command('status <namespace> <service-name>')
|
|
34
|
+
.description('Get build status')
|
|
35
|
+
.action(async (namespace, serviceName) => {
|
|
36
|
+
const conn = store_1.configStore.getDefaultConnection();
|
|
37
|
+
if (!conn) {
|
|
38
|
+
formatter_1.OutputFormatter.error('No connection configured');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const client = (0, client_1.createClient)(conn);
|
|
42
|
+
const result = await client.getBuildStatus(namespace, serviceName);
|
|
43
|
+
if (!result.success) {
|
|
44
|
+
formatter_1.OutputFormatter.error(result.error.message);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const builds = result.data || [];
|
|
48
|
+
if (builds.length === 0) {
|
|
49
|
+
formatter_1.OutputFormatter.info('No builds found');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const latest = builds[0];
|
|
53
|
+
formatter_1.OutputFormatter.info(`Status: ${latest.status}`);
|
|
54
|
+
formatter_1.OutputFormatter.info(`Created: ${latest.created_at}`);
|
|
55
|
+
});
|
|
56
|
+
return cmd;
|
|
57
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 连接管理命令
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.makeConnectCommand = makeConnectCommand;
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const store_1 = require("../config/store");
|
|
9
|
+
const formatter_1 = require("../output/formatter");
|
|
10
|
+
function makeConnectCommand() {
|
|
11
|
+
const cmd = new commander_1.Command('connect')
|
|
12
|
+
.description('Manage connections to xuanwu platform');
|
|
13
|
+
cmd
|
|
14
|
+
.command('add <name>')
|
|
15
|
+
.description('Add a new connection')
|
|
16
|
+
.requiredOption('-e, --endpoint <url>', 'Platform API endpoint')
|
|
17
|
+
.requiredOption('-t, --token <token>', 'API token')
|
|
18
|
+
.action(async (name, options) => {
|
|
19
|
+
try {
|
|
20
|
+
store_1.configStore.addConnection({
|
|
21
|
+
name,
|
|
22
|
+
endpoint: options.endpoint,
|
|
23
|
+
token: options.token,
|
|
24
|
+
isDefault: store_1.configStore.getConnections().length === 0
|
|
25
|
+
});
|
|
26
|
+
formatter_1.OutputFormatter.success(`Connection "${name}" added`);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
formatter_1.OutputFormatter.error(error.message);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
cmd
|
|
33
|
+
.command('ls')
|
|
34
|
+
.description('List all connections')
|
|
35
|
+
.action(() => {
|
|
36
|
+
const connections = store_1.configStore.getConnections();
|
|
37
|
+
if (connections.length === 0) {
|
|
38
|
+
formatter_1.OutputFormatter.info('No connections configured');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
formatter_1.OutputFormatter.table(['Name', 'Endpoint', 'Default'], connections.map(c => [
|
|
42
|
+
c.name,
|
|
43
|
+
c.endpoint,
|
|
44
|
+
c.isDefault ? '✓' : ''
|
|
45
|
+
]));
|
|
46
|
+
});
|
|
47
|
+
cmd
|
|
48
|
+
.command('use <name>')
|
|
49
|
+
.description('Set default connection')
|
|
50
|
+
.action((name) => {
|
|
51
|
+
const conn = store_1.configStore.getConnection(name);
|
|
52
|
+
if (!conn) {
|
|
53
|
+
formatter_1.OutputFormatter.error(`Connection "${name}" not found`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
store_1.configStore.setDefaultConnection(name);
|
|
57
|
+
formatter_1.OutputFormatter.success(`Using connection "${name}"`);
|
|
58
|
+
});
|
|
59
|
+
cmd
|
|
60
|
+
.command('rm <name>')
|
|
61
|
+
.description('Remove a connection')
|
|
62
|
+
.action((name) => {
|
|
63
|
+
store_1.configStore.removeConnection(name);
|
|
64
|
+
formatter_1.OutputFormatter.success(`Connection "${name}" removed`);
|
|
65
|
+
});
|
|
66
|
+
return cmd;
|
|
67
|
+
}
|