@besile/scm-cli 2026.3.29
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.zh.md +451 -0
- package/bin/scm-cli.js +38 -0
- package/index.d.ts +59 -0
- package/package.json +51 -0
- package/src/cmd/auth/login.js +92 -0
- package/src/cmd/auth/logout.js +39 -0
- package/src/cmd/auth/status.js +29 -0
- package/src/cmd/auth/token.js +118 -0
- package/src/cmd/index.js +40 -0
- package/src/cmd/orders/material-orders.js +123 -0
- package/src/cmd/orders/production-orders.js +143 -0
- package/src/cmd/orders/qc-orders.js +109 -0
- package/src/cmd/root.js +41 -0
- package/src/core/scm-client.js +8 -0
- package/src/core/scm-errors.js +20 -0
- package/src/core/scm-logger.js +18 -0
- package/src/internal/auth/token-store.js +155 -0
- package/src/internal/client/scm-client.js +420 -0
- package/src/internal/config/config.js +62 -0
- package/src/internal/errors/scm-errors.js +140 -0
- package/src/internal/index.js +43 -0
- package/src/internal/output/formatter.js +120 -0
- package/src/internal/output/logger.js +168 -0
- package/src/shortcuts/common/runner.js +145 -0
- package/src/shortcuts/common/types.js +35 -0
- package/src/shortcuts/index.js +23 -0
- package/src/shortcuts/material-orders/index.js +12 -0
- package/src/shortcuts/material-orders/list.js +77 -0
- package/src/shortcuts/production-orders/index.js +13 -0
- package/src/shortcuts/production-orders/list.js +48 -0
- package/src/shortcuts/production-orders/query.js +86 -0
- package/src/shortcuts/qc-orders/index.js +12 -0
- package/src/shortcuts/qc-orders/list.js +79 -0
- package/src/shortcuts/register.js +82 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 WFS
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*
|
|
6
|
+
* SCM 错误类型定义
|
|
7
|
+
*
|
|
8
|
+
* 所有与认证/授权/API 调用相关的错误类型集中在此文件。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// SCM Error Codes
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
export const SCM_ERROR = {
|
|
15
|
+
// 认证错误
|
|
16
|
+
AUTH_FAILED: 401,
|
|
17
|
+
AUTH_TOKEN_EXPIRED: 40101,
|
|
18
|
+
AUTH_TOKEN_INVALID: 40102,
|
|
19
|
+
AUTH_PERMISSION_DENIED: 403,
|
|
20
|
+
|
|
21
|
+
// API 错误
|
|
22
|
+
API_REQUEST_FAILED: 500,
|
|
23
|
+
API_TIMEOUT: 50001,
|
|
24
|
+
API_NETWORK_ERROR: 50002,
|
|
25
|
+
API_PARAM_ERROR: 400,
|
|
26
|
+
|
|
27
|
+
// 业务错误
|
|
28
|
+
USER_NOT_LOGGED_IN: 10001,
|
|
29
|
+
USER_LOGIN_EXPIRED: 10002,
|
|
30
|
+
TOKEN_REFRESH_FAILED: 10003,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Error Classes
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 基础 SCM 错误类
|
|
39
|
+
*/
|
|
40
|
+
export class ScmError extends Error {
|
|
41
|
+
constructor(message, code, details = {}) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = 'ScmError';
|
|
44
|
+
this.code = code;
|
|
45
|
+
this.details = details;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 认证错误 - 登录失败或凭据无效
|
|
51
|
+
*/
|
|
52
|
+
export class ScmAuthError extends ScmError {
|
|
53
|
+
constructor(message, code = SCM_ERROR.AUTH_FAILED, details = {}) {
|
|
54
|
+
super(message, code, details);
|
|
55
|
+
this.name = 'ScmAuthError';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Token 过期错误 - 需要重新登录
|
|
61
|
+
*/
|
|
62
|
+
export class ScmTokenExpiredError extends ScmAuthError {
|
|
63
|
+
constructor(message = 'Token 已过期,需要重新登录', details = {}) {
|
|
64
|
+
super(message, SCM_ERROR.AUTH_TOKEN_EXPIRED, details);
|
|
65
|
+
this.name = 'ScmTokenExpiredError';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Token 无效错误 - Token 格式错误或被撤销
|
|
71
|
+
*/
|
|
72
|
+
export class ScmTokenInvalidError extends ScmAuthError {
|
|
73
|
+
constructor(message = 'Token 无效', details = {}) {
|
|
74
|
+
super(message, SCM_ERROR.AUTH_TOKEN_INVALID, details);
|
|
75
|
+
this.name = 'ScmTokenInvalidError';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 用户未登录错误
|
|
81
|
+
*/
|
|
82
|
+
export class ScmUserNotLoggedInError extends ScmAuthError {
|
|
83
|
+
constructor(openId, details = {}) {
|
|
84
|
+
super(`用户 ${openId} 未登录,请先调用 scm_login 工具`, SCM_ERROR.USER_NOT_LOGGED_IN, {
|
|
85
|
+
openId,
|
|
86
|
+
...details,
|
|
87
|
+
});
|
|
88
|
+
this.name = 'ScmUserNotLoggedInError';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* API 调用错误
|
|
94
|
+
*/
|
|
95
|
+
export class ScmApiError extends ScmError {
|
|
96
|
+
constructor(message, code = SCM_ERROR.API_REQUEST_FAILED, details = {}) {
|
|
97
|
+
super(message, code, details);
|
|
98
|
+
this.name = 'ScmApiError';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* API 超时错误
|
|
104
|
+
*/
|
|
105
|
+
export class ScmTimeoutError extends ScmApiError {
|
|
106
|
+
constructor(message = 'API 请求超时', details = {}) {
|
|
107
|
+
super(message, SCM_ERROR.API_TIMEOUT, details);
|
|
108
|
+
this.name = 'ScmTimeoutError';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 网络错误
|
|
114
|
+
*/
|
|
115
|
+
export class ScmNetworkError extends ScmApiError {
|
|
116
|
+
constructor(message = '网络错误', details = {}) {
|
|
117
|
+
super(message, SCM_ERROR.API_NETWORK_ERROR, details);
|
|
118
|
+
this.name = 'ScmNetworkError';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 参数错误
|
|
124
|
+
*/
|
|
125
|
+
export class ScmParamError extends ScmApiError {
|
|
126
|
+
constructor(message, details = {}) {
|
|
127
|
+
super(message, SCM_ERROR.API_PARAM_ERROR, details);
|
|
128
|
+
this.name = 'ScmParamError';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 权限不足错误
|
|
134
|
+
*/
|
|
135
|
+
export class ScmPermissionDeniedError extends ScmAuthError {
|
|
136
|
+
constructor(message = '权限不足', details = {}) {
|
|
137
|
+
super(message, SCM_ERROR.AUTH_PERMISSION_DENIED, details);
|
|
138
|
+
this.name = 'ScmPermissionDeniedError';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 WFS
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*
|
|
6
|
+
* internal/ barrel export — re-export all internal modules
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Client
|
|
10
|
+
export { scmClient, ScmClient } from './client/scm-client.js';
|
|
11
|
+
|
|
12
|
+
// Errors
|
|
13
|
+
export {
|
|
14
|
+
ScmError,
|
|
15
|
+
ScmAuthError,
|
|
16
|
+
ScmTokenExpiredError,
|
|
17
|
+
ScmTokenInvalidError,
|
|
18
|
+
ScmUserNotLoggedInError,
|
|
19
|
+
ScmApiError,
|
|
20
|
+
ScmTimeoutError,
|
|
21
|
+
ScmNetworkError,
|
|
22
|
+
ScmParamError,
|
|
23
|
+
ScmPermissionDeniedError,
|
|
24
|
+
SCM_ERROR,
|
|
25
|
+
} from './errors/scm-errors.js';
|
|
26
|
+
|
|
27
|
+
// Output
|
|
28
|
+
export { scmLogger, setRuntimeLoggerFactory, setLogLevel, getLogLevel } from './output/logger.js';
|
|
29
|
+
export { coreLogger, tokenLogger, clientLogger, loginLogger, ordersLogger } from './output/logger.js';
|
|
30
|
+
export { formatOutput } from './output/formatter.js';
|
|
31
|
+
|
|
32
|
+
// Config
|
|
33
|
+
export { loadConfig, createLogger } from './config/config.js';
|
|
34
|
+
|
|
35
|
+
// Auth / Token Store
|
|
36
|
+
export {
|
|
37
|
+
getCachedToken,
|
|
38
|
+
checkToken,
|
|
39
|
+
TOKEN_FILE_PATH,
|
|
40
|
+
printLoginHint,
|
|
41
|
+
promptLogin,
|
|
42
|
+
withAuth,
|
|
43
|
+
} from './auth/token-store.js';
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 WFS
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*
|
|
6
|
+
* Output formatter — json, table, pretty, ndjson, csv
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const TAB = ' ';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format data as JSON
|
|
13
|
+
*/
|
|
14
|
+
function formatJson(data) {
|
|
15
|
+
return JSON.stringify(data, null, 2);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Format data as pretty (human-readable)
|
|
20
|
+
*/
|
|
21
|
+
function formatPretty(data) {
|
|
22
|
+
if (typeof data === 'string') return data;
|
|
23
|
+
return JSON.stringify(data, null, 2);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Format data as ndjson (newline-delimited JSON)
|
|
28
|
+
*/
|
|
29
|
+
function formatNdjson(data) {
|
|
30
|
+
if (Array.isArray(data)) {
|
|
31
|
+
return data.map(item => JSON.stringify(item)).join('\n');
|
|
32
|
+
}
|
|
33
|
+
return JSON.stringify(data);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Format data as CSV (arrays of objects only)
|
|
38
|
+
*/
|
|
39
|
+
function formatCsv(data) {
|
|
40
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
41
|
+
if (rows.length === 0) return '';
|
|
42
|
+
|
|
43
|
+
const keys = Object.keys(rows[0]);
|
|
44
|
+
const header = keys.join(',');
|
|
45
|
+
const body = rows.map(row =>
|
|
46
|
+
keys.map(k => {
|
|
47
|
+
const val = String(row[k] ?? '');
|
|
48
|
+
// Escape quotes and wrap if contains comma/quote
|
|
49
|
+
if (val.includes(',') || val.includes('"') || val.includes('\n')) {
|
|
50
|
+
return `"${val.replace(/"/g, '""')}"`;
|
|
51
|
+
}
|
|
52
|
+
return val;
|
|
53
|
+
}).join(',')
|
|
54
|
+
).join('\n');
|
|
55
|
+
|
|
56
|
+
return [header, body].join('\n');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Format orders list as ASCII table
|
|
61
|
+
*/
|
|
62
|
+
function formatTable(data) {
|
|
63
|
+
const rows = Array.isArray(data) ? data : (data?.list || [data]);
|
|
64
|
+
if (!rows || rows.length === 0) {
|
|
65
|
+
return 'No results found.';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const keys = Object.keys(rows[0]);
|
|
69
|
+
const colWidths = {};
|
|
70
|
+
for (const key of keys) {
|
|
71
|
+
colWidths[key] = Math.max(
|
|
72
|
+
key.length,
|
|
73
|
+
...rows.map(r => String(r[key] ?? '').length)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const totalWidth = Object.values(colWidths).reduce((a, b) => a + b, 0) + keys.length * 3 + 1;
|
|
78
|
+
const sep = '─'.repeat(Math.min(totalWidth, 120));
|
|
79
|
+
|
|
80
|
+
const lines = [];
|
|
81
|
+
lines.push('┌' + sep + '┐');
|
|
82
|
+
lines.push('│ ' + keys.map(k => String(k).padEnd(colWidths[k])).join(' │ ') + ' │');
|
|
83
|
+
lines.push('├' + sep + '┤');
|
|
84
|
+
|
|
85
|
+
for (const row of rows) {
|
|
86
|
+
const vals = keys.map(k => String(row[k] ?? '').padEnd(colWidths[k]));
|
|
87
|
+
lines.push('│ ' + vals.join(' │ ') + ' │');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
lines.push('└' + sep + '┘');
|
|
91
|
+
|
|
92
|
+
if (data?.total !== undefined) {
|
|
93
|
+
lines.push(`Total: ${data.total}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return lines.join('\n');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Format output based on format type
|
|
101
|
+
*
|
|
102
|
+
* @param {*} data - Data to format
|
|
103
|
+
* @param {string} format - Format: 'json' | 'table' | 'pretty' | 'ndjson' | 'csv'
|
|
104
|
+
* @returns {string} Formatted output
|
|
105
|
+
*/
|
|
106
|
+
export function formatOutput(data, format = 'table') {
|
|
107
|
+
switch (format) {
|
|
108
|
+
case 'json':
|
|
109
|
+
return formatJson(data);
|
|
110
|
+
case 'pretty':
|
|
111
|
+
return formatPretty(data);
|
|
112
|
+
case 'ndjson':
|
|
113
|
+
return formatNdjson(data);
|
|
114
|
+
case 'csv':
|
|
115
|
+
return formatCsv(data);
|
|
116
|
+
case 'table':
|
|
117
|
+
default:
|
|
118
|
+
return formatTable(data);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 WFS
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*
|
|
6
|
+
* SCM 统一日志模块
|
|
7
|
+
*
|
|
8
|
+
* 基于 openclaw-lark 的 lark-logger 模式实现,
|
|
9
|
+
* 支持 PluginRuntime 日志和 console 回退。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Console fallback (with ANSI colors)
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
const CYAN = '\x1b[36m';
|
|
16
|
+
const YELLOW = '\x1b[33m';
|
|
17
|
+
const RED = '\x1b[31m';
|
|
18
|
+
const GRAY = '\x1b[90m';
|
|
19
|
+
const RESET = '\x1b[0m';
|
|
20
|
+
|
|
21
|
+
function consoleFallback(subsystem) {
|
|
22
|
+
const tag = `scm/${subsystem}`;
|
|
23
|
+
/* eslint-disable no-console -- logger底层实现,console 是最终输出目标 */
|
|
24
|
+
return {
|
|
25
|
+
debug: (msg, meta) => console.debug(`${GRAY}[${tag}]${RESET}`, msg, ...(meta ? [meta] : [])),
|
|
26
|
+
info: (msg, meta) => console.log(`${CYAN}[${tag}]${RESET}`, msg, ...(meta ? [meta] : [])),
|
|
27
|
+
warn: (msg, meta) => console.warn(`${YELLOW}[${tag}]${RESET}`, msg, ...(meta ? [meta] : [])),
|
|
28
|
+
error: (msg, meta) => console.error(`${RED}[${tag}]${RESET}`, msg, ...(meta ? [meta] : [])),
|
|
29
|
+
};
|
|
30
|
+
/* eslint-enable no-console */
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Runtime logger storage
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
let _runtimeLoggerFactory = null;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 设置运行时日志工厂
|
|
40
|
+
* @param {Function} factory - LarkClient.runtime.logging.getChildLogger
|
|
41
|
+
*/
|
|
42
|
+
export function setRuntimeLoggerFactory(factory) {
|
|
43
|
+
_runtimeLoggerFactory = factory;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveRuntimeLogger(subsystem) {
|
|
47
|
+
if (!_runtimeLoggerFactory) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
return _runtimeLoggerFactory({ subsystem: `scm/${subsystem}` });
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Log level control
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
const LOG_LEVELS = {
|
|
61
|
+
debug: 0,
|
|
62
|
+
info: 1,
|
|
63
|
+
warn: 2,
|
|
64
|
+
error: 3,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
let _currentLevel = LOG_LEVELS.info;
|
|
68
|
+
|
|
69
|
+
export function setLogLevel(level) {
|
|
70
|
+
if (LOG_LEVELS[level] !== undefined) {
|
|
71
|
+
_currentLevel = LOG_LEVELS[level];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getLogLevel() {
|
|
76
|
+
return Object.keys(LOG_LEVELS).find(key => LOG_LEVELS[key] === _currentLevel) || 'info';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Logger implementation
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
function createScmLogger(subsystem) {
|
|
83
|
+
let cachedLogger = null;
|
|
84
|
+
let resolved = false;
|
|
85
|
+
|
|
86
|
+
function getLogger() {
|
|
87
|
+
if (!resolved) {
|
|
88
|
+
cachedLogger = resolveRuntimeLogger(subsystem);
|
|
89
|
+
if (cachedLogger) {
|
|
90
|
+
resolved = true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return cachedLogger ?? consoleFallback(subsystem);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function formatMessage(message, meta) {
|
|
97
|
+
if (!meta || Object.keys(meta).length === 0) {
|
|
98
|
+
return `${subsystem}: ${message}`;
|
|
99
|
+
}
|
|
100
|
+
const parts = Object.entries(meta)
|
|
101
|
+
.map(([k, v]) => {
|
|
102
|
+
if (v === undefined || v === null) return null;
|
|
103
|
+
if (typeof v === 'object') return `${k}=${JSON.stringify(v)}`;
|
|
104
|
+
return `${k}=${v}`;
|
|
105
|
+
})
|
|
106
|
+
.filter(Boolean);
|
|
107
|
+
return parts.length > 0
|
|
108
|
+
? `${subsystem}: ${message} (${parts.join(', ')})`
|
|
109
|
+
: `${subsystem}: ${message}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function shouldLog(level) {
|
|
113
|
+
return LOG_LEVELS[level] >= _currentLevel;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
subsystem,
|
|
118
|
+
debug(message, meta) {
|
|
119
|
+
if (shouldLog('debug')) {
|
|
120
|
+
getLogger().debug?.(formatMessage(message, meta), meta);
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
info(message, meta) {
|
|
124
|
+
if (shouldLog('info')) {
|
|
125
|
+
getLogger().info(formatMessage(message, meta), meta);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
warn(message, meta) {
|
|
129
|
+
if (shouldLog('warn')) {
|
|
130
|
+
getLogger().warn(formatMessage(message, meta), meta);
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
error(message, meta) {
|
|
134
|
+
if (shouldLog('error')) {
|
|
135
|
+
getLogger().error(formatMessage(message, meta), meta);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
child(name) {
|
|
139
|
+
return createScmLogger(`${subsystem}/${name}`);
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Public factory
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
/**
|
|
148
|
+
* 创建 SCM 模块日志器
|
|
149
|
+
* @param {string} subsystem - 子系统名称,如 'core/client', 'tools/login'
|
|
150
|
+
* @returns {Object} 日志器对象 { debug, info, warn, error, child }
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* const log = scmLogger('core/client');
|
|
154
|
+
* log.info('Client initialized', { baseUrl: 'http://localhost:48080' });
|
|
155
|
+
* log.error('Request failed', { error: err.message });
|
|
156
|
+
*/
|
|
157
|
+
export function scmLogger(subsystem) {
|
|
158
|
+
return createScmLogger(subsystem);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Pre-configured loggers
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
export const coreLogger = scmLogger('core');
|
|
165
|
+
export const tokenLogger = scmLogger('core/token');
|
|
166
|
+
export const clientLogger = scmLogger('core/client');
|
|
167
|
+
export const loginLogger = scmLogger('cmd/auth');
|
|
168
|
+
export const ordersLogger = scmLogger('cmd/orders');
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 WFS
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*
|
|
6
|
+
* Shortcut runner — execution pipeline
|
|
7
|
+
*
|
|
8
|
+
* Inspired by lark-cli's shortcuts/common/runner.go
|
|
9
|
+
*
|
|
10
|
+
* Responsibilities:
|
|
11
|
+
* - Parse flag values from runtime (supports @file and stdin input)
|
|
12
|
+
* - Inject --format flag and route output
|
|
13
|
+
* - Call shortcut.execute() with context
|
|
14
|
+
* - Handle errors with structured output
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createReadStream } from 'node:fs';
|
|
18
|
+
import { readFile } from 'node:fs/promises';
|
|
19
|
+
import { Readable } from 'node:stream';
|
|
20
|
+
import { formatOutput } from '../../internal/output/formatter.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve flag input — supports plain value, @file path, or stdin '-'
|
|
24
|
+
*
|
|
25
|
+
* @param {string} value - Flag value from CLI
|
|
26
|
+
* @returns {Promise<string>} Resolved value
|
|
27
|
+
*/
|
|
28
|
+
export async function resolveFlagInput(value) {
|
|
29
|
+
if (!value || typeof value !== 'string') {
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const trimmed = value.trim();
|
|
34
|
+
|
|
35
|
+
if (trimmed === '-') {
|
|
36
|
+
// Read from stdin
|
|
37
|
+
const chunks = [];
|
|
38
|
+
for await (const chunk of process.stdin) {
|
|
39
|
+
chunks.push(chunk);
|
|
40
|
+
}
|
|
41
|
+
return Buffer.concat(chunks).toString('utf-8').trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (trimmed.startsWith('@')) {
|
|
45
|
+
// Read from file
|
|
46
|
+
const filePath = trimmed.slice(1);
|
|
47
|
+
return await readFile(filePath, 'utf-8');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build runtime context for shortcut execution
|
|
55
|
+
*
|
|
56
|
+
* @param {Object} shortcut - Shortcut definition
|
|
57
|
+
* @param {Object} flags - Parsed flag values from CLI
|
|
58
|
+
* @param {Object} internalCtx - Internal context (client, config, etc.)
|
|
59
|
+
* @returns {Object} Runtime context
|
|
60
|
+
*/
|
|
61
|
+
export async function buildRuntime(shortcut, flags, internalCtx) {
|
|
62
|
+
const resolvedFlags = {};
|
|
63
|
+
|
|
64
|
+
for (const flag of (shortcut.flags || [])) {
|
|
65
|
+
const rawValue = flags[flag.name] ?? flags[flag.name.replace(/-/g, '')] ?? flag.default;
|
|
66
|
+
if (rawValue !== undefined) {
|
|
67
|
+
resolvedFlags[flag.name] = await resolveFlagInput(String(rawValue));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Handle --format from flags or default to 'table'
|
|
72
|
+
const format = resolvedFlags.format || flags.format || 'table';
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
flags: resolvedFlags,
|
|
76
|
+
format,
|
|
77
|
+
internal: internalCtx,
|
|
78
|
+
log: internalCtx.log || console,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Run a shortcut
|
|
84
|
+
*
|
|
85
|
+
* @param {Object} shortcut - Shortcut definition
|
|
86
|
+
* @param {Object} runtime - Runtime context from buildRuntime
|
|
87
|
+
* @returns {Promise<Object>} Execution result
|
|
88
|
+
*/
|
|
89
|
+
export async function runShortcut(shortcut, runtime) {
|
|
90
|
+
// Validate
|
|
91
|
+
if (shortcut.validate) {
|
|
92
|
+
const err = shortcut.validate(runtime.internal, runtime);
|
|
93
|
+
if (err) {
|
|
94
|
+
return { success: false, error: err.message || String(err) };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Execute
|
|
99
|
+
let result;
|
|
100
|
+
try {
|
|
101
|
+
result = await shortcut.execute(runtime.internal, runtime);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
return { success: false, error: err.message || String(err) };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Format output
|
|
107
|
+
const formatted = formatOutput(result, runtime.format);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
success: true,
|
|
111
|
+
data: result,
|
|
112
|
+
formatted,
|
|
113
|
+
format: runtime.format,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Create CLI action handler for a shortcut
|
|
119
|
+
*
|
|
120
|
+
* @param {Object} shortcut - Shortcut definition
|
|
121
|
+
* @param {Object} internalCtx - Internal context (client, config)
|
|
122
|
+
* @returns {Function} Commander action handler
|
|
123
|
+
*/
|
|
124
|
+
export function shortcutAction(shortcut, internalCtx) {
|
|
125
|
+
return async function (opts) {
|
|
126
|
+
try {
|
|
127
|
+
const runtime = await buildRuntime(shortcut, opts, internalCtx);
|
|
128
|
+
const result = await runShortcut(shortcut, runtime);
|
|
129
|
+
|
|
130
|
+
if (!result.success) {
|
|
131
|
+
console.error(`Error: ${result.error}`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (result.formatted) {
|
|
136
|
+
console.log(result.formatted);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return result;
|
|
140
|
+
} catch (err) {
|
|
141
|
+
console.error(`Error: ${err.message}`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 WFS
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*
|
|
6
|
+
* Shortcut type definitions
|
|
7
|
+
*
|
|
8
|
+
* Inspired by lark-cli's shortcuts/common/types.go
|
|
9
|
+
* Shortcuts are user-friendly commands with `+` prefix.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} Flag
|
|
14
|
+
* @property {string} name - Flag name (e.g., 'open-id', becomes --open-id)
|
|
15
|
+
* @property {string} type - Flag type: 'string', 'number', 'boolean', 'array'
|
|
16
|
+
* @property {boolean} [required] - Whether the flag is required
|
|
17
|
+
* @property {*} [default] - Default value if not provided
|
|
18
|
+
* @property {string} [description] - Help text for the flag
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} Shortcut
|
|
23
|
+
* @property {string} service - Domain: 'production-orders' | 'material-orders' | 'qc-orders' | 'auth'
|
|
24
|
+
* @property {string} command - Command name with + prefix: '+list' | '+query'
|
|
25
|
+
* @property {string} description - Human-readable description
|
|
26
|
+
* @property {string} risk - 'read' | 'write' | 'high-risk-write'
|
|
27
|
+
* @property {string[]} scopes - Required OAuth scopes (for API calls)
|
|
28
|
+
* @property {Flag[]} flags - Declarative flag definitions
|
|
29
|
+
* @property {string[]} [tips] - Help tips shown to user
|
|
30
|
+
* @property {boolean} [hasFormat] - Whether to auto-inject --format flag
|
|
31
|
+
* @property {Function} [validate] - Pre-execution validation function (ctx, runtime) => error
|
|
32
|
+
* @property {Function} execute - Execution logic (ctx, runtime) => result
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
export { };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 WFS
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*
|
|
6
|
+
* All shortcuts — collected map for registration
|
|
7
|
+
*/
|
|
8
|
+
import { Shortcuts as ProductionOrdersShortcuts } from './production-orders/index.js';
|
|
9
|
+
import { Shortcuts as MaterialOrdersShortcuts } from './material-orders/index.js';
|
|
10
|
+
import { Shortcuts as QcOrdersShortcuts } from './qc-orders/index.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get all shortcuts as a domain -> Shortcut[] map
|
|
14
|
+
*/
|
|
15
|
+
export function getShortcutsMap() {
|
|
16
|
+
return {
|
|
17
|
+
'production-orders': ProductionOrdersShortcuts(),
|
|
18
|
+
'material-orders': MaterialOrdersShortcuts(),
|
|
19
|
+
'qc-orders': QcOrdersShortcuts(),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { registerShortcuts } from './register.js';
|