@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.
@@ -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';
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 WFS
4
+ * SPDX-License-Identifier: MIT
5
+ *
6
+ * Material Orders shortcuts
7
+ */
8
+ import { MaterialOrdersList } from './list.js';
9
+
10
+ export function Shortcuts() {
11
+ return [MaterialOrdersList];
12
+ }