@meet-im/lxcli 0.0.1
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/LICENSE +15 -0
- package/README.md +3 -0
- package/bin/lxcli.js +2 -0
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +45 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +28 -0
- package/dist/commands/generate-skills.d.ts +2 -0
- package/dist/commands/generate-skills.js +283 -0
- package/dist/commands/index.d.ts +9 -0
- package/dist/commands/index.js +43 -0
- package/dist/commands/schema.d.ts +2 -0
- package/dist/commands/schema.js +37 -0
- package/dist/commands/services/kb/config.d.ts +2 -0
- package/dist/commands/services/kb/config.js +32 -0
- package/dist/commands/services/kb/descriptions.d.ts +4 -0
- package/dist/commands/services/kb/descriptions.js +24 -0
- package/dist/commands/services/kb/handler.d.ts +12 -0
- package/dist/commands/services/kb/handler.js +179 -0
- package/dist/commands/services/kb/index.d.ts +3 -0
- package/dist/commands/services/kb/index.js +119 -0
- package/dist/commands/services/kb/params.d.ts +8 -0
- package/dist/commands/services/kb/params.js +73 -0
- package/dist/commands/services/kb/rpc.d.ts +1 -0
- package/dist/commands/services/kb/rpc.js +35 -0
- package/dist/commands/services/kb/types.d.ts +97 -0
- package/dist/commands/services/kb/types.js +2 -0
- package/dist/commands/services/kb/utils.d.ts +1 -0
- package/dist/commands/services/kb/utils.js +4 -0
- package/dist/commands/services/meet/api.d.ts +13 -0
- package/dist/commands/services/meet/api.js +38 -0
- package/dist/commands/services/meet/config.d.ts +4 -0
- package/dist/commands/services/meet/config.js +16 -0
- package/dist/commands/services/meet/descriptions.d.ts +3 -0
- package/dist/commands/services/meet/descriptions.js +7 -0
- package/dist/commands/services/meet/handler.d.ts +25 -0
- package/dist/commands/services/meet/handler.js +50 -0
- package/dist/commands/services/meet/index.d.ts +3 -0
- package/dist/commands/services/meet/index.js +43 -0
- package/dist/commands/services/meet/types.d.ts +65 -0
- package/dist/commands/services/meet/types.js +2 -0
- package/dist/commands/services/meetbot/api.d.ts +37 -0
- package/dist/commands/services/meetbot/api.js +130 -0
- package/dist/commands/services/meetbot/config.d.ts +4 -0
- package/dist/commands/services/meetbot/config.js +21 -0
- package/dist/commands/services/meetbot/descriptions.d.ts +3 -0
- package/dist/commands/services/meetbot/descriptions.js +10 -0
- package/dist/commands/services/meetbot/handler.d.ts +42 -0
- package/dist/commands/services/meetbot/handler.js +83 -0
- package/dist/commands/services/meetbot/index.d.ts +3 -0
- package/dist/commands/services/meetbot/index.js +74 -0
- package/dist/commands/services/meetbot/types.d.ts +101 -0
- package/dist/commands/services/meetbot/types.js +2 -0
- package/dist/commands/services/meetbot/utils.d.ts +28 -0
- package/dist/commands/services/meetbot/utils.js +78 -0
- package/dist/commands/services.d.ts +2 -0
- package/dist/commands/services.js +50 -0
- package/dist/commands/tenants.d.ts +2 -0
- package/dist/commands/tenants.js +46 -0
- package/dist/commands/upgrade.d.ts +2 -0
- package/dist/commands/upgrade.js +38 -0
- package/dist/commands/users.d.ts +2 -0
- package/dist/commands/users.js +182 -0
- package/dist/core/auth.d.ts +26 -0
- package/dist/core/auth.js +75 -0
- package/dist/core/config.d.ts +76 -0
- package/dist/core/config.js +255 -0
- package/dist/core/error.d.ts +128 -0
- package/dist/core/error.js +85 -0
- package/dist/core/help.d.ts +6 -0
- package/dist/core/help.js +15 -0
- package/dist/core/http.d.ts +28 -0
- package/dist/core/http.js +140 -0
- package/dist/core/logger.d.ts +24 -0
- package/dist/core/logger.js +53 -0
- package/dist/core/reserved.d.ts +13 -0
- package/dist/core/reserved.js +28 -0
- package/dist/core/tenants.d.ts +36 -0
- package/dist/core/tenants.js +50 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +152 -0
- package/dist/types/index.d.ts +59 -0
- package/dist/types/index.js +5 -0
- package/dist/utils/index.d.ts +20 -0
- package/dist/utils/index.js +133 -0
- package/dist/utils/loose-params.d.ts +9 -0
- package/dist/utils/loose-params.js +274 -0
- package/dist/utils/table.d.ts +4 -0
- package/dist/utils/table.js +25 -0
- package/package.json +46 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// src/core/http.ts
|
|
2
|
+
import { logger } from './logger.js';
|
|
3
|
+
import { outputApiError } from './error.js';
|
|
4
|
+
import pkg from '../../package.json' with { type: 'json' };
|
|
5
|
+
// ─── 重试配置 ───────────────────────────────────────
|
|
6
|
+
const MAX_RETRIES = 3;
|
|
7
|
+
const BASE_DELAY_MS = 1000;
|
|
8
|
+
const MAX_DELAY_MS = 10000;
|
|
9
|
+
function parseRetryAfterDelay(retryAfterHeader) {
|
|
10
|
+
if (!retryAfterHeader) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const retryAfterSeconds = Number(retryAfterHeader);
|
|
14
|
+
if (!Number.isNaN(retryAfterSeconds) && retryAfterSeconds >= 0) {
|
|
15
|
+
return retryAfterSeconds * 1000;
|
|
16
|
+
}
|
|
17
|
+
const retryAt = Date.parse(retryAfterHeader);
|
|
18
|
+
if (Number.isNaN(retryAt)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return Math.max(0, retryAt - Date.now());
|
|
22
|
+
}
|
|
23
|
+
function computeRetryDelay(attempt, retryAfterHeader) {
|
|
24
|
+
const retryAfterDelay = parseRetryAfterDelay(retryAfterHeader);
|
|
25
|
+
if (retryAfterDelay !== null) {
|
|
26
|
+
return Math.min(retryAfterDelay, MAX_DELAY_MS);
|
|
27
|
+
}
|
|
28
|
+
// 指数退避: 1s, 2s, 4s
|
|
29
|
+
const delay = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
30
|
+
return Math.min(delay, MAX_DELAY_MS);
|
|
31
|
+
}
|
|
32
|
+
function sleep(ms) {
|
|
33
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
34
|
+
}
|
|
35
|
+
function parseJsonResponse(responseText) {
|
|
36
|
+
return JSON.parse(responseText);
|
|
37
|
+
}
|
|
38
|
+
function joinUrl(gateway, path) {
|
|
39
|
+
const normalizedGateway = gateway.endsWith('/') ? gateway.slice(0, -1) : gateway;
|
|
40
|
+
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
41
|
+
return `${normalizedGateway}${normalizedPath}`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 通用请求函数
|
|
45
|
+
*
|
|
46
|
+
* 支持灵活传递 gateway、路径、方法、参数、UA 等。
|
|
47
|
+
*
|
|
48
|
+
* 支持自动重试:
|
|
49
|
+
* - 429 (rate limit) 响应时重试,尊重 Retry-After header
|
|
50
|
+
* - 网络错误时重试,使用指数退避
|
|
51
|
+
*/
|
|
52
|
+
export async function request(options) {
|
|
53
|
+
const { gateway, path, method = 'GET', query, body, headers = {}, userAgent = `lxcli/${pkg.version}`, maxRetries = MAX_RETRIES, } = options;
|
|
54
|
+
// 构建 URL(gateway + path + query)
|
|
55
|
+
let url = joinUrl(gateway, path);
|
|
56
|
+
if (query && Object.keys(query).length > 0) {
|
|
57
|
+
const searchParams = new URLSearchParams(Object.fromEntries(Object.entries(query).map(([k, v]) => [k, String(v)])));
|
|
58
|
+
url += `?${searchParams.toString()}`;
|
|
59
|
+
}
|
|
60
|
+
// 构建 headers
|
|
61
|
+
const allHeaders = {
|
|
62
|
+
'Content-Type': 'application/json',
|
|
63
|
+
'User-Agent': userAgent,
|
|
64
|
+
...headers,
|
|
65
|
+
};
|
|
66
|
+
const requestBody = body ? JSON.stringify(body) : undefined;
|
|
67
|
+
let lastError = null;
|
|
68
|
+
// 发送请求(带重试)
|
|
69
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
70
|
+
try {
|
|
71
|
+
logger.debug('Request', { method, url, headers: allHeaders, body, attempt });
|
|
72
|
+
const response = await fetch(url, {
|
|
73
|
+
method,
|
|
74
|
+
headers: allHeaders,
|
|
75
|
+
body: requestBody,
|
|
76
|
+
});
|
|
77
|
+
const responseText = await response.text();
|
|
78
|
+
// 429 rate limit - 重试
|
|
79
|
+
if (response.status === 429) {
|
|
80
|
+
let errorResponse;
|
|
81
|
+
if (responseText) {
|
|
82
|
+
try {
|
|
83
|
+
errorResponse = parseJsonResponse(responseText);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
errorResponse = undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
logger.debug('Response', { status: response.status, data: errorResponse ?? responseText, attempt });
|
|
90
|
+
if (attempt < maxRetries - 1) {
|
|
91
|
+
const retryAfter = response.headers.get('Retry-After');
|
|
92
|
+
const delay = computeRetryDelay(attempt, retryAfter ?? undefined);
|
|
93
|
+
logger.info(`Rate limited (429), retrying after ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
|
|
94
|
+
await sleep(delay);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
outputApiError(errorResponse?.error?.code || response.status, errorResponse?.error?.message || 'Request failed', errorResponse?.error?.details, response.status);
|
|
98
|
+
}
|
|
99
|
+
let responseData;
|
|
100
|
+
try {
|
|
101
|
+
responseData = parseJsonResponse(responseText);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
outputApiError(response.status, `Invalid JSON response: ${responseText}`, undefined, response.status);
|
|
105
|
+
}
|
|
106
|
+
logger.debug('Response', { status: response.status, data: responseData, attempt });
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
const errorResponse = responseData;
|
|
109
|
+
outputApiError(errorResponse.error?.code || response.status, errorResponse.error?.message || 'Request failed', errorResponse.error?.details, response.status);
|
|
110
|
+
}
|
|
111
|
+
return responseData;
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
// 网络错误 - 重试
|
|
115
|
+
const isNetworkError = error instanceof TypeError && (error.message.includes('fetch') ||
|
|
116
|
+
error.message.includes('network') ||
|
|
117
|
+
error.message.includes('ECONNREFUSED') ||
|
|
118
|
+
error.message.includes('ETIMEDOUT'));
|
|
119
|
+
if (isNetworkError && attempt < maxRetries - 1) {
|
|
120
|
+
const delay = computeRetryDelay(attempt);
|
|
121
|
+
logger.info(`Network error, retrying after ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
|
|
122
|
+
lastError = error;
|
|
123
|
+
await sleep(delay);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (isNetworkError) {
|
|
127
|
+
logger.error('Request failed', error);
|
|
128
|
+
outputApiError(500, error.message, undefined, 500);
|
|
129
|
+
}
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// 所有重试都失败了
|
|
134
|
+
if (lastError) {
|
|
135
|
+
logger.error('All retries failed', lastError);
|
|
136
|
+
outputApiError(500, lastError.message, undefined, 500);
|
|
137
|
+
}
|
|
138
|
+
// 不应该到达这里,但 TypeScript 需要返回值
|
|
139
|
+
throw new Error('Request failed after all retries');
|
|
140
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { LogLevel } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* 日志管理器
|
|
4
|
+
*/
|
|
5
|
+
export declare class Logger {
|
|
6
|
+
private static instance;
|
|
7
|
+
level: LogLevel;
|
|
8
|
+
private constructor();
|
|
9
|
+
static getInstance(): Logger;
|
|
10
|
+
setLevel(level: LogLevel): void;
|
|
11
|
+
/**
|
|
12
|
+
* 输出调试信息到 stderr
|
|
13
|
+
*/
|
|
14
|
+
debug(message: string, data?: Record<string, unknown>): void;
|
|
15
|
+
/**
|
|
16
|
+
* 输出信息到 stderr
|
|
17
|
+
*/
|
|
18
|
+
info(message: string): void;
|
|
19
|
+
/**
|
|
20
|
+
* 输出错误信息到 stderr
|
|
21
|
+
*/
|
|
22
|
+
error(message: string, err?: Error): void;
|
|
23
|
+
}
|
|
24
|
+
export declare const logger: Logger;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// src/core/logger.ts
|
|
2
|
+
/**
|
|
3
|
+
* 日志管理器
|
|
4
|
+
*/
|
|
5
|
+
export class Logger {
|
|
6
|
+
static instance;
|
|
7
|
+
level = 'silent';
|
|
8
|
+
constructor() { }
|
|
9
|
+
static getInstance() {
|
|
10
|
+
if (!Logger.instance) {
|
|
11
|
+
Logger.instance = new Logger();
|
|
12
|
+
}
|
|
13
|
+
return Logger.instance;
|
|
14
|
+
}
|
|
15
|
+
setLevel(level) {
|
|
16
|
+
this.level = level;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 输出调试信息到 stderr
|
|
20
|
+
*/
|
|
21
|
+
debug(message, data) {
|
|
22
|
+
if (this.level !== 'debug')
|
|
23
|
+
return;
|
|
24
|
+
const prefix = '[DEBUG]';
|
|
25
|
+
if (data) {
|
|
26
|
+
console.error(`${prefix} ${message}`, JSON.stringify(data, null, 2));
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
console.error(`${prefix} ${message}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 输出信息到 stderr
|
|
34
|
+
*/
|
|
35
|
+
info(message) {
|
|
36
|
+
if (this.level === 'silent')
|
|
37
|
+
return;
|
|
38
|
+
console.error(`[INFO] ${message}`);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 输出错误信息到 stderr
|
|
42
|
+
*/
|
|
43
|
+
error(message, err) {
|
|
44
|
+
const prefix = '[ERROR]';
|
|
45
|
+
if (err) {
|
|
46
|
+
console.error(`${prefix} ${message}`, err.message);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
console.error(`${prefix} ${message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export const logger = Logger.getInstance();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 保留命令名(不可用于外部服务名)
|
|
3
|
+
*/
|
|
4
|
+
export declare const RESERVED_NAMES: readonly ["upgrade", "version", "help", "auth", "users", "tenants", "services", "config"];
|
|
5
|
+
export type ReservedName = typeof RESERVED_NAMES[number];
|
|
6
|
+
/**
|
|
7
|
+
* 验证服务名是否为保留名
|
|
8
|
+
*/
|
|
9
|
+
export declare function isReservedName(name: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* 验证服务名
|
|
12
|
+
*/
|
|
13
|
+
export declare function validateServiceName(name: string): void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// src/core/reserved.ts
|
|
2
|
+
/**
|
|
3
|
+
* 保留命令名(不可用于外部服务名)
|
|
4
|
+
*/
|
|
5
|
+
export const RESERVED_NAMES = [
|
|
6
|
+
'upgrade',
|
|
7
|
+
'version',
|
|
8
|
+
'help',
|
|
9
|
+
'auth',
|
|
10
|
+
'users',
|
|
11
|
+
'tenants',
|
|
12
|
+
'services',
|
|
13
|
+
'config',
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* 验证服务名是否为保留名
|
|
17
|
+
*/
|
|
18
|
+
export function isReservedName(name) {
|
|
19
|
+
return RESERVED_NAMES.includes(name);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 验证服务名
|
|
23
|
+
*/
|
|
24
|
+
export function validateServiceName(name) {
|
|
25
|
+
if (isReservedName(name)) {
|
|
26
|
+
throw new Error(`"${name}" is a reserved command name. Choose a different service name.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { TenantConfig, Env } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* 服务配置类型
|
|
4
|
+
* 按企业、按环境配置 gateway
|
|
5
|
+
*/
|
|
6
|
+
export interface ServiceGatewayConfig {
|
|
7
|
+
name: string;
|
|
8
|
+
gateways: Record<string, Record<Env, string>>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 注册服务配置到主体
|
|
12
|
+
*/
|
|
13
|
+
export declare function registerService(serviceConfig: ServiceGatewayConfig): void;
|
|
14
|
+
/**
|
|
15
|
+
* 获取主体配置
|
|
16
|
+
*/
|
|
17
|
+
export declare function getTenant(name: string): TenantConfig | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* 获取所有主体名称
|
|
20
|
+
*/
|
|
21
|
+
export declare function getTenantNames(): string[];
|
|
22
|
+
/**
|
|
23
|
+
* 列出所有主体
|
|
24
|
+
*/
|
|
25
|
+
export declare function listTenants(): TenantConfig[];
|
|
26
|
+
/**
|
|
27
|
+
* 获取网关地址
|
|
28
|
+
* @param tenant 主体(企业)名称
|
|
29
|
+
* @param env 环境
|
|
30
|
+
* @param service 服务名称
|
|
31
|
+
*/
|
|
32
|
+
export declare function getGateway(tenant: string, env: Env, service: string): string | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* 检查主体是否存在
|
|
35
|
+
*/
|
|
36
|
+
export declare function tenantExists(name: string): boolean;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// src/core/tenants.ts
|
|
2
|
+
/** 服务注册表:serviceName -> tenant -> env -> gateway */
|
|
3
|
+
const SERVICE_REGISTRY = {};
|
|
4
|
+
/**
|
|
5
|
+
* 内置主体配置
|
|
6
|
+
*/
|
|
7
|
+
const TENANTS = {
|
|
8
|
+
meet: { name: 'meet' },
|
|
9
|
+
co: { name: 'co' },
|
|
10
|
+
sky: { name: 'sky' },
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* 注册服务配置到主体
|
|
14
|
+
*/
|
|
15
|
+
export function registerService(serviceConfig) {
|
|
16
|
+
SERVICE_REGISTRY[serviceConfig.name] = serviceConfig.gateways;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 获取主体配置
|
|
20
|
+
*/
|
|
21
|
+
export function getTenant(name) {
|
|
22
|
+
return TENANTS[name];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 获取所有主体名称
|
|
26
|
+
*/
|
|
27
|
+
export function getTenantNames() {
|
|
28
|
+
return Object.keys(TENANTS);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 列出所有主体
|
|
32
|
+
*/
|
|
33
|
+
export function listTenants() {
|
|
34
|
+
return Object.values(TENANTS);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 获取网关地址
|
|
38
|
+
* @param tenant 主体(企业)名称
|
|
39
|
+
* @param env 环境
|
|
40
|
+
* @param service 服务名称
|
|
41
|
+
*/
|
|
42
|
+
export function getGateway(tenant, env, service) {
|
|
43
|
+
return SERVICE_REGISTRY[service]?.[tenant]?.[env];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 检查主体是否存在
|
|
47
|
+
*/
|
|
48
|
+
export function tenantExists(name) {
|
|
49
|
+
return name in TENANTS;
|
|
50
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// src/index.ts
|
|
3
|
+
import { Command, CommanderError } from 'commander';
|
|
4
|
+
import { outputCliError } from './core/error.js';
|
|
5
|
+
import { logger } from './core/logger.js';
|
|
6
|
+
import { registerAllCommands } from './commands/index.js';
|
|
7
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
8
|
+
const program = new Command();
|
|
9
|
+
export function normalizeCliArgv(argv) {
|
|
10
|
+
const normalized = [];
|
|
11
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
12
|
+
const arg = argv[i];
|
|
13
|
+
if (arg !== '--params') {
|
|
14
|
+
normalized.push(arg);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
normalized.push(arg);
|
|
18
|
+
const value = argv[i + 1];
|
|
19
|
+
if (!value) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (!value.startsWith("'") || value.endsWith("'")) {
|
|
23
|
+
normalized.push(value);
|
|
24
|
+
i += 1;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const chunks = [value];
|
|
28
|
+
let endIndex = i + 1;
|
|
29
|
+
for (let j = i + 2; j < argv.length; j += 1) {
|
|
30
|
+
const chunk = argv[j];
|
|
31
|
+
if (chunk.startsWith('-') && !chunk.endsWith("'")) {
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
chunks.push(chunk);
|
|
35
|
+
endIndex = j;
|
|
36
|
+
if (chunk.endsWith("'")) {
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
normalized.push(chunks.join(' '));
|
|
41
|
+
i = endIndex;
|
|
42
|
+
}
|
|
43
|
+
return normalized;
|
|
44
|
+
}
|
|
45
|
+
// 设置基本信息
|
|
46
|
+
program
|
|
47
|
+
.name('lxcli')
|
|
48
|
+
.description('连续科技命令行工具')
|
|
49
|
+
.version(pkg.version);
|
|
50
|
+
// 无子命令时显示帮助
|
|
51
|
+
program.action(() => {
|
|
52
|
+
program.outputHelp();
|
|
53
|
+
process.exitCode = 0;
|
|
54
|
+
});
|
|
55
|
+
// 全局选项
|
|
56
|
+
program
|
|
57
|
+
.option('--debug', 'Enable debug mode')
|
|
58
|
+
.option('--verbose', 'Enable verbose output')
|
|
59
|
+
.hook('preAction', (thisCommand) => {
|
|
60
|
+
const options = thisCommand.opts();
|
|
61
|
+
if (options.debug) {
|
|
62
|
+
logger.setLevel('debug');
|
|
63
|
+
}
|
|
64
|
+
else if (options.verbose) {
|
|
65
|
+
logger.setLevel('info');
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
// 注册所有命令
|
|
69
|
+
registerAllCommands(program);
|
|
70
|
+
function getAuthInvocationState() {
|
|
71
|
+
const args = process.argv.slice(2);
|
|
72
|
+
const authIndex = args.indexOf('auth');
|
|
73
|
+
const authArgs = authIndex >= 0 ? args.slice(authIndex + 1) : [];
|
|
74
|
+
const positionals = [];
|
|
75
|
+
let hasTenant = false;
|
|
76
|
+
for (let i = 0; i < authArgs.length; i += 1) {
|
|
77
|
+
const arg = authArgs[i];
|
|
78
|
+
if (arg === '--tenant') {
|
|
79
|
+
hasTenant = true;
|
|
80
|
+
i += 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (arg.startsWith('--tenant=')) {
|
|
84
|
+
hasTenant = true;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (arg === '--gateway' || arg === '--env' || arg === '-e') {
|
|
88
|
+
i += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (!arg.startsWith('-')) {
|
|
92
|
+
positionals.push(arg);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
authArgs,
|
|
97
|
+
positionals,
|
|
98
|
+
hasTenant,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// 捕获 Commander 错误
|
|
102
|
+
const setupErrorHandling = (cmd) => {
|
|
103
|
+
cmd.exitOverride((err) => {
|
|
104
|
+
// help 和 version 正常输出后退出
|
|
105
|
+
if (err.code === 'commander.help' || err.code === 'commander.version' || err.code === 'commander.helpDisplayed') {
|
|
106
|
+
process.exitCode = 0;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (cmd.name() === 'auth') {
|
|
110
|
+
const { authArgs, positionals, hasTenant } = getAuthInvocationState();
|
|
111
|
+
if (authArgs.length === 0) {
|
|
112
|
+
cmd.help({ error: false });
|
|
113
|
+
}
|
|
114
|
+
if (positionals.length === 0) {
|
|
115
|
+
outputCliError('PARAM_INVALID', `Missing required argument: name. Use 'lxcli auth --help' for usage.`);
|
|
116
|
+
}
|
|
117
|
+
if (positionals.length === 1) {
|
|
118
|
+
outputCliError('PARAM_INVALID', `Missing required argument: token. Use 'lxcli auth --help' for usage.`);
|
|
119
|
+
}
|
|
120
|
+
if (!hasTenant) {
|
|
121
|
+
outputCliError('PARAM_INVALID', `Missing required option: --tenant. Use 'lxcli auth --help' for usage.`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const message = err.message.replace(/^error:\s*/i, '').trim();
|
|
125
|
+
outputCliError('PARAM_INVALID', message, {
|
|
126
|
+
source: 'commander',
|
|
127
|
+
commanderCode: err.code,
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
cmd.configureOutput({
|
|
131
|
+
writeErr: () => { },
|
|
132
|
+
// writeOut 保留默认,让 help 正常显示
|
|
133
|
+
});
|
|
134
|
+
cmd.commands.forEach((sub) => setupErrorHandling(sub));
|
|
135
|
+
};
|
|
136
|
+
setupErrorHandling(program);
|
|
137
|
+
// 解析命令行参数
|
|
138
|
+
program.parseAsync(normalizeCliArgv(process.argv)).catch((error) => {
|
|
139
|
+
if (error instanceof Error && error.message === 'Invalid JSON in --params') {
|
|
140
|
+
outputCliError('PARAM_INVALID', error.message);
|
|
141
|
+
}
|
|
142
|
+
else if (error instanceof CommanderError) {
|
|
143
|
+
const message = error.message.replace(/^error:\s*/i, '').trim();
|
|
144
|
+
outputCliError('PARAM_INVALID', message, {
|
|
145
|
+
source: 'commander',
|
|
146
|
+
commanderCode: error.code,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
outputCliError('UNKNOWN', error instanceof Error ? error.message : 'Unknown error');
|
|
151
|
+
}
|
|
152
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
/**
|
|
3
|
+
* 环境类型
|
|
4
|
+
*/
|
|
5
|
+
export declare const ENVIRONMENTS: readonly ["test", "pre", "staging", "beta", "prod"];
|
|
6
|
+
export type Env = typeof ENVIRONMENTS[number];
|
|
7
|
+
/**
|
|
8
|
+
* 主体配置(内置)
|
|
9
|
+
*/
|
|
10
|
+
export interface TenantConfig {
|
|
11
|
+
name: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 用户配置
|
|
15
|
+
*/
|
|
16
|
+
export interface UserConfig {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
token: string;
|
|
20
|
+
tenant: string;
|
|
21
|
+
env: Env;
|
|
22
|
+
companyID?: number;
|
|
23
|
+
isDefault?: boolean;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
createdAt: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 应用配置
|
|
29
|
+
*/
|
|
30
|
+
export interface AppConfig {
|
|
31
|
+
version: string;
|
|
32
|
+
users: UserConfig[];
|
|
33
|
+
currentUser?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 服务命令接口
|
|
37
|
+
*/
|
|
38
|
+
export interface ServiceCommand {
|
|
39
|
+
name: string;
|
|
40
|
+
description: string;
|
|
41
|
+
/** 是否启用,默认 true。设为 false 时服务不注册到 CLI */
|
|
42
|
+
enabled?: boolean;
|
|
43
|
+
register(program: Command): void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 错误响应格式
|
|
47
|
+
*/
|
|
48
|
+
export interface ErrorResponse {
|
|
49
|
+
error: {
|
|
50
|
+
code: number;
|
|
51
|
+
reason?: string;
|
|
52
|
+
message: string;
|
|
53
|
+
details?: Record<string, unknown>;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 日志级别
|
|
58
|
+
*/
|
|
59
|
+
export type LogLevel = 'silent' | 'info' | 'debug';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const LIST_FORMATS: readonly ["json", "table"];
|
|
2
|
+
export type ListFormat = (typeof LIST_FORMATS)[number];
|
|
3
|
+
export declare function parseCliParams(params?: string): Record<string, unknown> | unknown[];
|
|
4
|
+
/**
|
|
5
|
+
* 合并简单参数和 --params
|
|
6
|
+
*/
|
|
7
|
+
export declare function mergeParams(options: Record<string, unknown>): Record<string, unknown>;
|
|
8
|
+
/**
|
|
9
|
+
* 格式化日期
|
|
10
|
+
*/
|
|
11
|
+
export declare function formatDate(dateStr: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* 输出 JSON 结果
|
|
14
|
+
*/
|
|
15
|
+
export declare function outputJson(data: unknown): void;
|
|
16
|
+
export declare function parseListFormat(format: string): ListFormat;
|
|
17
|
+
/**
|
|
18
|
+
* 确认提示
|
|
19
|
+
*/
|
|
20
|
+
export declare function confirm(message: string): Promise<boolean>;
|