@meet-im/lxcli 0.0.4 → 0.0.5

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.
@@ -2,13 +2,14 @@ import { outputCliError } from '../../../core/error.js';
2
2
  import { request } from '../../../core/http.js';
3
3
  import { getNextRequestId } from './utils.js';
4
4
  export async function jsonRpcCall(gateway, method, params, pat) {
5
+ const requestId = getNextRequestId();
5
6
  const requestBody = {
6
7
  jsonrpc: '2.0',
7
8
  method,
8
- id: getNextRequestId(),
9
+ id: requestId,
9
10
  params,
10
11
  };
11
- const response = await request({
12
+ const result = await request({
12
13
  gateway,
13
14
  path: '/jsonrpc.php',
14
15
  method: 'POST',
@@ -16,11 +17,26 @@ export async function jsonRpcCall(gateway, method, params, pat) {
16
17
  headers: {
17
18
  Authorization: `meet ${pat}`,
18
19
  },
20
+ throwOnError: false,
19
21
  });
22
+ // HTTP 层错误
23
+ if (result.error) {
24
+ outputCliError('NETWORK_ERROR', result.error.message, {
25
+ code: result.error.code,
26
+ details: result.error.details,
27
+ requestId,
28
+ });
29
+ return undefined;
30
+ }
31
+ const response = result.data;
32
+ // JSON-RPC 层错误
20
33
  if (response.error) {
21
34
  outputCliError('NETWORK_ERROR', response.error.message, {
22
35
  code: response.error.code,
23
36
  data: response.error.data,
37
+ requestId,
38
+ // 服务端返回的 id(可能为 null)
39
+ responseId: response.id,
24
40
  });
25
41
  return undefined;
26
42
  }
@@ -28,6 +44,7 @@ export async function jsonRpcCall(gateway, method, params, pat) {
28
44
  outputCliError('NETWORK_ERROR', 'No result returned from Kanboard API', {
29
45
  method,
30
46
  params,
47
+ requestId,
31
48
  });
32
49
  return undefined;
33
50
  }
@@ -33,6 +33,7 @@ export interface KanboardTask {
33
33
  date_due?: string;
34
34
  date_started?: string;
35
35
  priority: string;
36
+ priority_name?: string;
36
37
  score: string;
37
38
  reference: string;
38
39
  url: string;
@@ -43,6 +44,17 @@ export interface KanboardTask {
43
44
  };
44
45
  tags?: string[];
45
46
  }
47
+ export interface KanboardSubtask {
48
+ id: string;
49
+ title: string;
50
+ status: string;
51
+ status_name?: string;
52
+ time_estimated: string;
53
+ time_spent: string;
54
+ task_id: string;
55
+ user_id: string;
56
+ position: string;
57
+ }
46
58
  export interface GetTaskParams {
47
59
  task_id: number;
48
60
  }
@@ -1,2 +1,6 @@
1
1
  import { Command } from 'commander';
2
+ import { spawn } from 'child_process';
3
+ export declare const upgradeDeps: {
4
+ spawn: typeof spawn;
5
+ };
2
6
  export declare function registerUpgradeCommand(program: Command): void;
@@ -1,8 +1,12 @@
1
1
  // src/commands/upgrade.ts
2
+ import { spawn } from 'child_process';
2
3
  import { outputCliError } from '../core/error.js';
3
4
  import { outputJson } from '../utils/index.js';
4
5
  const PACKAGE_NAME = '@meet-im/lxcli';
5
6
  const NPM_REGISTRY = 'https://registry.npmjs.org';
7
+ export const upgradeDeps = {
8
+ spawn,
9
+ };
6
10
  async function getLatestVersion() {
7
11
  const res = await fetch(`${NPM_REGISTRY}/${PACKAGE_NAME}/latest`);
8
12
  if (!res.ok) {
@@ -11,22 +15,52 @@ async function getLatestVersion() {
11
15
  const data = await res.json();
12
16
  return data.version;
13
17
  }
18
+ async function runNpmUpgrade() {
19
+ const command = process.platform === 'win32' ? 'cmd.exe' : 'npm';
20
+ const args = process.platform === 'win32'
21
+ ? ['/c', 'npm', 'install', '-g', `${PACKAGE_NAME}@latest`]
22
+ : ['install', '-g', `${PACKAGE_NAME}@latest`];
23
+ await new Promise((resolve, reject) => {
24
+ const child = upgradeDeps.spawn(command, args, {
25
+ stdio: 'inherit',
26
+ });
27
+ child.on('error', reject);
28
+ child.on('exit', (code) => {
29
+ if (code === 0) {
30
+ resolve();
31
+ return;
32
+ }
33
+ reject(new Error(`npm install exited with code ${code ?? 'unknown'}`));
34
+ });
35
+ });
36
+ }
14
37
  export function registerUpgradeCommand(program) {
15
38
  program
16
39
  .command('upgrade')
17
40
  .description('Check for updates and show upgrade instructions')
18
- .action(async () => {
41
+ .option('--check', 'Only check for updates without installing')
42
+ .action(async (options) => {
19
43
  try {
20
44
  const currentVersion = program.version() || '1.0.0';
21
45
  const latestVersion = await getLatestVersion();
22
46
  const isLatest = currentVersion === latestVersion;
47
+ if (options.check || isLatest) {
48
+ outputJson({
49
+ currentVersion,
50
+ latestVersion,
51
+ isLatest,
52
+ message: isLatest
53
+ ? 'Already on the latest version'
54
+ : `Run "npm install -g ${PACKAGE_NAME}@latest" to upgrade`,
55
+ });
56
+ return;
57
+ }
58
+ await runNpmUpgrade();
23
59
  outputJson({
24
- currentVersion,
60
+ previousVersion: currentVersion,
25
61
  latestVersion,
26
- isLatest,
27
- message: isLatest
28
- ? 'Already on the latest version'
29
- : `Run "npm install -g ${PACKAGE_NAME}@latest" to upgrade`,
62
+ upgraded: true,
63
+ message: `Upgraded ${PACKAGE_NAME} to ${latestVersion}`,
30
64
  });
31
65
  }
32
66
  catch (error) {
@@ -1,4 +1,16 @@
1
- export interface RequestOptions {
1
+ export interface RequestResult<T> {
2
+ /** HTTP 状态码 */
3
+ status: number;
4
+ /** 响应数据(成功时) */
5
+ data?: T;
6
+ /** 错误信息(失败时) */
7
+ error?: {
8
+ code: number;
9
+ message: string;
10
+ details?: unknown;
11
+ };
12
+ }
13
+ export interface RequestOptionsWithThrow {
2
14
  /** gateway 地址(必填) */
3
15
  gateway: string;
4
16
  /** 请求路径(不含域名) */
@@ -15,7 +27,30 @@ export interface RequestOptions {
15
27
  userAgent?: string;
16
28
  /** 最大重试次数,默认 3 */
17
29
  maxRetries?: number;
30
+ /** 错误时抛出异常而非直接退出,默认 true */
31
+ throwOnError?: true;
18
32
  }
33
+ export interface RequestOptionsWithoutThrow {
34
+ /** gateway 地址(必填) */
35
+ gateway: string;
36
+ /** 请求路径(不含域名) */
37
+ path: string;
38
+ /** HTTP 方法,默认 GET */
39
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
40
+ /** URL query 参数 */
41
+ query?: Record<string, string | number | boolean>;
42
+ /** 请求体 */
43
+ body?: Record<string, unknown>;
44
+ /** 自定义 headers(会覆盖默认 headers) */
45
+ headers?: Record<string, string>;
46
+ /** User-Agent,默认 lxcli/版本号 */
47
+ userAgent?: string;
48
+ /** 最大重试次数,默认 3 */
49
+ maxRetries?: number;
50
+ /** 错误时返回 RequestResult 而非退出 */
51
+ throwOnError: false;
52
+ }
53
+ export type RequestOptions = RequestOptionsWithThrow | RequestOptionsWithoutThrow;
19
54
  /**
20
55
  * 通用请求函数
21
56
  *
@@ -24,5 +59,8 @@ export interface RequestOptions {
24
59
  * 支持自动重试:
25
60
  * - 429 (rate limit) 响应时重试,尊重 Retry-After header
26
61
  * - 网络错误时重试,使用指数退避
62
+ *
63
+ * 当 throwOnError 为 false 时,返回 RequestResult<T> 而非直接退出。
27
64
  */
28
- export declare function request<T>(options: RequestOptions): Promise<T>;
65
+ export declare function request<T>(options: RequestOptionsWithThrow): Promise<T>;
66
+ export declare function request<T>(options: RequestOptionsWithoutThrow): Promise<RequestResult<T>>;
package/dist/core/http.js CHANGED
@@ -40,17 +40,8 @@ function joinUrl(gateway, path) {
40
40
  const normalizedPath = path.startsWith('/') ? path : `/${path}`;
41
41
  return `${normalizedGateway}${normalizedPath}`;
42
42
  }
43
- /**
44
- * 通用请求函数
45
- *
46
- * 支持灵活传递 gateway、路径、方法、参数、UA 等。
47
- *
48
- * 支持自动重试:
49
- * - 429 (rate limit) 响应时重试,尊重 Retry-After header
50
- * - 网络错误时重试,使用指数退避
51
- */
52
43
  export async function request(options) {
53
- const { gateway, path, method = 'GET', query, body, headers = {}, userAgent = `lxcli/${pkg.version}`, maxRetries = MAX_RETRIES, } = options;
44
+ const { gateway, path, method = 'GET', query, body, headers = {}, userAgent = `lxcli/${pkg.version}`, maxRetries = MAX_RETRIES, throwOnError = true, } = options;
54
45
  // 构建 URL(gateway + path + query)
55
46
  let url = joinUrl(gateway, path);
56
47
  if (query && Object.keys(query).length > 0) {
@@ -65,6 +56,13 @@ export async function request(options) {
65
56
  };
66
57
  const requestBody = body ? JSON.stringify(body) : undefined;
67
58
  let lastError = null;
59
+ // 辅助函数:处理错误响应
60
+ const makeErrorResult = (status, code, message, details) => {
61
+ if (throwOnError) {
62
+ outputApiError(code, message, details, status);
63
+ }
64
+ return { status, error: { code, message, details } };
65
+ };
68
66
  // 发送请求(带重试)
69
67
  for (let attempt = 0; attempt < maxRetries; attempt++) {
70
68
  try {
@@ -94,19 +92,32 @@ export async function request(options) {
94
92
  await sleep(delay);
95
93
  continue;
96
94
  }
97
- outputApiError(errorResponse?.error?.code || response.status, errorResponse?.error?.message || 'Request failed', errorResponse?.error?.details, response.status);
95
+ const result = makeErrorResult(response.status, errorResponse?.error?.code || response.status, errorResponse?.error?.message || 'Request failed', errorResponse?.error?.details);
96
+ if (!throwOnError)
97
+ return result;
98
+ // throwOnError 为 true 时 makeErrorResult 已经调用 outputApiError 退出了
98
99
  }
99
100
  let responseData;
100
101
  try {
101
102
  responseData = parseJsonResponse(responseText);
102
103
  }
103
104
  catch {
104
- outputApiError(response.status, `Invalid JSON response: ${responseText}`, undefined, response.status);
105
+ const result = makeErrorResult(response.status, response.status, `Invalid JSON response: ${responseText}`);
106
+ if (!throwOnError)
107
+ return result;
108
+ // throwOnError 为 true 时 makeErrorResult 已经调用 outputApiError 退出了
105
109
  }
106
110
  logger.debug('Response', { status: response.status, data: responseData, attempt });
107
111
  if (!response.ok) {
108
112
  const errorResponse = responseData;
109
- outputApiError(errorResponse.error?.code || response.status, errorResponse.error?.message || 'Request failed', errorResponse.error?.details, response.status);
113
+ const result = makeErrorResult(response.status, errorResponse?.error?.code || response.status, errorResponse?.error?.message || 'Request failed', errorResponse?.error?.details);
114
+ if (!throwOnError)
115
+ return result;
116
+ // throwOnError 为 true 时 makeErrorResult 已经调用 outputApiError 退出了
117
+ }
118
+ // throwOnError 为 false 时返回完整结果
119
+ if (!throwOnError) {
120
+ return { status: response.status, data: responseData };
110
121
  }
111
122
  return responseData;
112
123
  }
@@ -125,7 +136,9 @@ export async function request(options) {
125
136
  }
126
137
  if (isNetworkError) {
127
138
  logger.error('Request failed', error);
128
- outputApiError(500, error.message, undefined, 500);
139
+ const result = makeErrorResult(500, 500, error.message);
140
+ if (!throwOnError)
141
+ return result;
129
142
  }
130
143
  throw error;
131
144
  }
@@ -133,7 +146,9 @@ export async function request(options) {
133
146
  // 所有重试都失败了
134
147
  if (lastError) {
135
148
  logger.error('All retries failed', lastError);
136
- outputApiError(500, lastError.message, undefined, 500);
149
+ const result = makeErrorResult(500, 500, lastError.message);
150
+ if (!throwOnError)
151
+ return result;
137
152
  }
138
153
  // 不应该到达这里,但 TypeScript 需要返回值
139
154
  throw new Error('Request failed after all retries');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meet-im/lxcli",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "连续科技命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",