@meet-im/lxcli 0.0.4 → 0.0.6

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.
@@ -4,7 +4,7 @@ import * as path from 'path';
4
4
  import { outputCliError } from '../core/error.js';
5
5
  import { logger } from '../core/logger.js';
6
6
  import { PARAM_DEFS, METHOD_DESCRIPTIONS } from './services/kb/descriptions.js';
7
- import { METHOD_SKILL_METADATA } from './services/kb/skill-metadata.js';
7
+ import { METHOD_SHORTCUT_PARAMS, METHOD_SKILL_METADATA } from './services/kb/skill-metadata.js';
8
8
  const API_ROOT = path.resolve('api');
9
9
  const DEFAULT_SKILLS_DIR = path.resolve('api/skills');
10
10
  const DEFAULT_OUTPUT = 'api/docs/skills.md';
@@ -155,41 +155,51 @@ async function readPackageVersion() {
155
155
  const packageJson = JSON.parse(await fs.readFile(PACKAGE_JSON_PATH, 'utf-8'));
156
156
  return packageJson.version || '1.0.0';
157
157
  }
158
+ function buildParamsExample(params) {
159
+ const paramsExample = {};
160
+ for (const p of params) {
161
+ if (p.type === 'array<integer>') {
162
+ paramsExample[p.name] = [1, 2];
163
+ }
164
+ else if (p.type === 'array<string>') {
165
+ paramsExample[p.name] = ['id1', 'id2'];
166
+ }
167
+ else if (p.type === 'integer') {
168
+ paramsExample[p.name] = 123;
169
+ }
170
+ else if (p.type === 'object') {
171
+ paramsExample[p.name] = {};
172
+ }
173
+ else {
174
+ paramsExample[p.name] = 'value';
175
+ }
176
+ }
177
+ return paramsExample;
178
+ }
158
179
  function renderSkillMd(method, version) {
159
180
  const description = METHOD_DESCRIPTIONS[method];
160
181
  const params = PARAM_DEFS[method] || [];
161
182
  const metadata = METHOD_SKILL_METADATA[method];
162
183
  const skillName = `lxcli-kb-${method}`;
163
184
  const hasParams = params.length > 0;
164
- // 快捷参数列表
165
- const shortcuts = params.filter(p => p.required).map(p => `--${p.name} <${p.type === 'integer' ? 'ID' : 'VALUE'}>`);
185
+ // 快捷参数列表(显式配置,不从 required 自动推断)
186
+ const shortcutParams = METHOD_SHORTCUT_PARAMS[method] || [];
187
+ const shortcuts = shortcutParams.map((name) => {
188
+ const param = params.find((p) => p.name === name);
189
+ if (!param) {
190
+ return `--${name} <VALUE>`;
191
+ }
192
+ return `--${param.name} <${param.type === 'integer' ? 'ID' : 'VALUE'}>`;
193
+ });
166
194
  const shortcutUsage = shortcuts.length > 0 ? `lxcli kb ${method} ${shortcuts.join(' ')}` : '';
195
+ const paramsUsage = hasParams ? `lxcli kb ${method} --params '${JSON.stringify(buildParamsExample(params))}'` : '';
167
196
  // Usage 部分
168
197
  const usageLines = ['## Usage', '', '```bash'];
169
198
  if (hasParams) {
170
199
  if (shortcutUsage) {
171
200
  usageLines.push(shortcutUsage);
172
201
  }
173
- // 生成 --params 示例
174
- const paramsExample = {};
175
- for (const p of params) {
176
- if (p.type === 'array<integer>') {
177
- paramsExample[p.name] = [1, 2];
178
- }
179
- else if (p.type === 'array<string>') {
180
- paramsExample[p.name] = ['id1', 'id2'];
181
- }
182
- else if (p.type === 'integer') {
183
- paramsExample[p.name] = 123;
184
- }
185
- else if (p.type === 'object') {
186
- paramsExample[p.name] = {};
187
- }
188
- else {
189
- paramsExample[p.name] = 'value';
190
- }
191
- }
192
- usageLines.push(`lxcli kb ${method} --params '${JSON.stringify(paramsExample)}'`);
202
+ usageLines.push(paramsUsage);
193
203
  }
194
204
  else {
195
205
  usageLines.push(`lxcli kb ${method}`);
@@ -224,7 +234,7 @@ metadata:
224
234
  if (hasParams) {
225
235
  lines.push(renderParamsTable(params));
226
236
  }
227
- lines.push('## Examples', '', '```bash', shortcutUsage || `lxcli kb ${method}`, '```', '');
237
+ lines.push('## Examples', '', '```bash', shortcutUsage || paramsUsage || `lxcli kb ${method}`, '```', '');
228
238
  if (metadata?.notes?.length) {
229
239
  lines.push('## Notes', '', ...metadata.notes.map((note) => `- ${note}`), '');
230
240
  }
@@ -17,6 +17,7 @@ export const METHOD_DESCRIPTIONS = {
17
17
  createSubtask: '创建子任务',
18
18
  updateTask: '修改需求',
19
19
  updateSubtask: '更新子任务',
20
+ moveTaskPosition: '移动需求状态列',
20
21
  getAllComments: '获取评论列表',
21
22
  createComment: '对需求增加评论',
22
23
  createProjectFile: '上传附件到项目',
@@ -90,4 +90,11 @@ export const PARAM_DEFS = {
90
90
  downloadTaskFile: [
91
91
  { name: 'file_id', type: 'integer', required: true, description: '需求附件 ID' },
92
92
  ],
93
+ moveTaskPosition: [
94
+ { name: 'project_id', type: 'integer', required: true, description: '项目 ID。必须是任务所属项目。' },
95
+ { name: 'task_id', type: 'integer', required: true, description: '需求/bug ID' },
96
+ { name: 'column_id', type: 'integer', required: false, description: '目标状态列 ID。未传时保留当前状态列' },
97
+ { name: 'position', type: 'integer', required: false, description: '目标列内排序位置,必须 >= 1。未传时保留当前排序值' },
98
+ { name: 'swimlane_id', type: 'integer', required: false, description: '目标泳道 ID。未传或传 0 保留当前泳道' },
99
+ ],
93
100
  };
@@ -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.ok) {
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
  }
@@ -2,4 +2,5 @@ import type { Method } from './descriptions.js';
2
2
  export interface MethodSkillMetadata {
3
3
  notes?: string[];
4
4
  }
5
+ export declare const METHOD_SHORTCUT_PARAMS: Partial<Record<Method, string[]>>;
5
6
  export declare const METHOD_SKILL_METADATA: Partial<Record<Method, MethodSkillMetadata>>;
@@ -1,3 +1,16 @@
1
+ export const METHOD_SHORTCUT_PARAMS = {
2
+ getTask: ['task_id'],
3
+ createTask: ['title', 'project_id', 'owner_id', 'category_id'],
4
+ searchTasks: ['project_id', 'query'],
5
+ getAllActiveIterations: ['project_id'],
6
+ getAllCategories: ['project_id'],
7
+ getAllComments: ['task_id'],
8
+ createComment: ['task_id', 'user_id', 'content'],
9
+ createProjectFile: ['file', 'project_id'],
10
+ downloadProjectFile: ['project_id', 'file_id'],
11
+ createTaskFile: ['file', 'task_id', 'project_id'],
12
+ downloadTaskFile: ['file_id'],
13
+ };
1
14
  export const METHOD_SKILL_METADATA = {
2
15
  downloadProjectFile: {
3
16
  notes: [
@@ -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,19 @@
1
- export interface RequestOptions {
1
+ export interface RequestSuccessResult<T> {
2
+ ok: true;
3
+ status: number;
4
+ data: T;
5
+ }
6
+ export interface RequestErrorResult {
7
+ ok: false;
8
+ status: number;
9
+ error: {
10
+ code: number;
11
+ message: string;
12
+ details?: unknown;
13
+ };
14
+ }
15
+ export type RequestResult<T> = RequestSuccessResult<T> | RequestErrorResult;
16
+ interface RequestOptionsBase {
2
17
  /** gateway 地址(必填) */
3
18
  gateway: string;
4
19
  /** 请求路径(不含域名) */
@@ -16,6 +31,15 @@ export interface RequestOptions {
16
31
  /** 最大重试次数,默认 3 */
17
32
  maxRetries?: number;
18
33
  }
34
+ export type RequestOptionsWithThrow = RequestOptionsBase & {
35
+ /** 错误时调用 outputApiError 退出进程,默认 true */
36
+ throwOnError?: true;
37
+ };
38
+ export type RequestOptionsWithoutThrow = RequestOptionsBase & {
39
+ /** 错误时返回 RequestResult 而非退出 */
40
+ throwOnError: false;
41
+ };
42
+ export type RequestOptions = RequestOptionsWithThrow | RequestOptionsWithoutThrow;
19
43
  /**
20
44
  * 通用请求函数
21
45
  *
@@ -24,5 +48,9 @@ export interface RequestOptions {
24
48
  * 支持自动重试:
25
49
  * - 429 (rate limit) 响应时重试,尊重 Retry-After header
26
50
  * - 网络错误时重试,使用指数退避
51
+ *
52
+ * 当 throwOnError 为 false 时,返回 RequestResult<T> 而非直接退出。
27
53
  */
28
- export declare function request<T>(options: RequestOptions): Promise<T>;
54
+ export declare function request<T>(options: RequestOptionsWithThrow): Promise<T>;
55
+ export declare function request<T>(options: RequestOptionsWithoutThrow): Promise<RequestResult<T>>;
56
+ export {};
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,17 @@ export async function request(options) {
65
56
  };
66
57
  const requestBody = body ? JSON.stringify(body) : undefined;
67
58
  let lastError = null;
59
+ // 辅助函数:构造错误结果
60
+ const errorResult = (status, code, message, details) => {
61
+ return { ok: false, status, error: { code, message, details } };
62
+ };
63
+ // 辅助函数:处理错误(throwOnError 时退出,否则返回错误结果)
64
+ const handleError = (result) => {
65
+ if (throwOnError) {
66
+ outputApiError(result.error.code, result.error.message, result.error.details, result.status);
67
+ }
68
+ return result;
69
+ };
68
70
  // 发送请求(带重试)
69
71
  for (let attempt = 0; attempt < maxRetries; attempt++) {
70
72
  try {
@@ -94,19 +96,22 @@ export async function request(options) {
94
96
  await sleep(delay);
95
97
  continue;
96
98
  }
97
- outputApiError(errorResponse?.error?.code || response.status, errorResponse?.error?.message || 'Request failed', errorResponse?.error?.details, response.status);
99
+ return handleError(errorResult(response.status, errorResponse?.error?.code || response.status, errorResponse?.error?.message || 'Request failed', errorResponse?.error?.details));
98
100
  }
99
101
  let responseData;
100
102
  try {
101
103
  responseData = parseJsonResponse(responseText);
102
104
  }
103
105
  catch {
104
- outputApiError(response.status, `Invalid JSON response: ${responseText}`, undefined, response.status);
106
+ return handleError(errorResult(response.status, response.status, `Invalid JSON response: ${responseText}`));
105
107
  }
106
108
  logger.debug('Response', { status: response.status, data: responseData, attempt });
107
109
  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
+ const errResp = responseData;
111
+ return handleError(errorResult(response.status, errResp?.error?.code || response.status, errResp?.error?.message || 'Request failed', errResp?.error?.details));
112
+ }
113
+ if (!throwOnError) {
114
+ return { ok: true, status: response.status, data: responseData };
110
115
  }
111
116
  return responseData;
112
117
  }
@@ -125,7 +130,7 @@ export async function request(options) {
125
130
  }
126
131
  if (isNetworkError) {
127
132
  logger.error('Request failed', error);
128
- outputApiError(500, error.message, undefined, 500);
133
+ return handleError(errorResult(500, 500, error.message));
129
134
  }
130
135
  throw error;
131
136
  }
@@ -133,7 +138,7 @@ export async function request(options) {
133
138
  // 所有重试都失败了
134
139
  if (lastError) {
135
140
  logger.error('All retries failed', lastError);
136
- outputApiError(500, lastError.message, undefined, 500);
141
+ return handleError(errorResult(500, 500, lastError.message));
137
142
  }
138
143
  // 不应该到达这里,但 TypeScript 需要返回值
139
144
  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.6",
4
4
  "description": "连续科技命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",