nx-kv 0.1.1 → 0.2.0
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/CHANGELOG.md +100 -48
- package/README.md +38 -22
- package/assets/nx-kv/SKILL.md +21 -18
- package/assets/nx-kv/references/agent-workflow.md +9 -7
- package/assets/nx-kv/references/todo-commands.md +44 -20
- package/package.json +1 -1
- package/src/core/paths.js +1 -1
- package/src/modules/todo/index.js +288 -280
- package/src/modules/todo/service.js +61 -47
- package/src/modules/todo/view.jsx +285 -280
- package/src/runtime/cli.js +300 -299
- package/src/runtime/registry.js +11 -0
- package/src/runtime/spec.js +34 -8
- package/src/web/frontend/App.jsx +1 -1
- package/src/web/frontend/index.html +4 -0
- package/src/web/frontend/style.css +2 -1
- package/src/web/public/assets/{CliHints-3H3KOoDZ.js → CliHints-Bc3Me3vO.js} +1 -1
- package/src/web/public/assets/index-CmCYxz0e.css +1 -0
- package/src/web/public/assets/{index-jhTtYCb9.js → index-Dv9g9jmR.js} +2 -2
- package/src/web/public/assets/{view-CrL-DSJu.js → view-B8AnDK0i.js} +1 -1
- package/src/web/public/assets/{view-Du7Wv_-h.js → view-BKTVRs9U.js} +1 -1
- package/src/web/public/assets/view-Cdi5TUV1.js +2 -0
- package/src/web/public/index.html +6 -2
- package/src/web/public/assets/index-BnWex75m.css +0 -1
- package/src/web/public/assets/view-S0N5g6v1.js +0 -2
package/src/runtime/cli.js
CHANGED
|
@@ -1,299 +1,300 @@
|
|
|
1
|
-
// 通用 CLI 运行器:解析 argv → 匹配 action → 校验 → 执行 → 渲染。
|
|
2
|
-
//
|
|
3
|
-
// 改造前这里是一个 400 行的 switch,每条命令要在 BOOL_FLAGS / helpText / case /
|
|
4
|
-
// renderXxx 四处重复登记。现在命令表、help 文本、参数校验全部由 action 声明派生,
|
|
5
|
-
// 新增命令只需在所属模块的 actions 里加一条。
|
|
6
|
-
import { readFileSync } from 'node:fs';
|
|
7
|
-
import { DEFAULT_PORT } from '../core/paths.js';
|
|
8
|
-
import { configPathFromEnv } from '../core/config.js';
|
|
9
|
-
import { toErrorPayload, exitCodeOf, badInput } from '../core/errors.js';
|
|
10
|
-
import { ACTIONS, MODULES } from './registry.js';
|
|
11
|
-
import { cliPathsOf, argSpecsOf, flagSpecsOf, booleanFlagNames, applySpec, usageOf } from './spec.js';
|
|
12
|
-
|
|
13
|
-
const VERSION = JSON.parse(
|
|
14
|
-
readFileSync(new URL('../../package.json', import.meta.url), 'utf8')
|
|
15
|
-
).version;
|
|
16
|
-
|
|
17
|
-
// ---- 平台命令:不属于任何功能域,不参与 action 表 ----
|
|
18
|
-
// 用与 action 相同的形状声明,好让参数解析与 help 生成保持同一套逻辑。
|
|
19
|
-
const BUILTINS = [
|
|
20
|
-
{
|
|
21
|
-
id: 'serve',
|
|
22
|
-
cli: ['serve'],
|
|
23
|
-
summary: '启动 Web 面板',
|
|
24
|
-
flags: { port: { type: 'number', default: DEFAULT_PORT }, 'no-open': { type: 'boolean' } },
|
|
25
|
-
run: (ctx) => cmdServe(ctx),
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
id: 'help',
|
|
29
|
-
cli: ['help'],
|
|
30
|
-
summary: '显示帮助(可跟模块名或命令组,如 nx-kv help todo)',
|
|
31
|
-
args: [{ name: 'topic', required: false }],
|
|
32
|
-
// 走标准 action 形态:--json 时 emit 会序列化返回的条目,
|
|
33
|
-
// 于是 `help --json` 输出的是可解析的命令表而非帮助文本
|
|
34
|
-
run: (ctx) => helpEntries(ctx.topic),
|
|
35
|
-
render: (entries, ctx) => renderHelp(entries, ctx.topic),
|
|
36
|
-
},
|
|
37
|
-
// render 让 `nx-kv version` 输出裸版本号(脚本里可 `V=$(nx-kv version)`),
|
|
38
|
-
// 而 `--json` 仍走序列化,保持机器可读
|
|
39
|
-
{ id: 'version', cli: ['version'], summary: '显示版本', run: () => VERSION, render: (v) => v },
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
export const ALL_COMMANDS = [...BUILTINS, ...ACTIONS];
|
|
43
|
-
|
|
44
|
-
// ---- 参数解析 ----
|
|
45
|
-
|
|
46
|
-
// 全局 flag 先摘掉:它们可以出现在 argv 任意位置,不属于任何 action 的声明。
|
|
47
|
-
function splitGlobals(argv) {
|
|
48
|
-
const rest = [];
|
|
49
|
-
let json = false;
|
|
50
|
-
let cfgPath;
|
|
51
|
-
for (let i = 0; i < argv.length; i++) {
|
|
52
|
-
const a = argv[i];
|
|
53
|
-
if (a === '--json') { json = true; continue; }
|
|
54
|
-
if (a === '--config') { cfgPath = argv[++i]; continue; }
|
|
55
|
-
if (a.startsWith('--config=')) { cfgPath = a.slice('--config='.length); continue; }
|
|
56
|
-
rest.push(a);
|
|
57
|
-
}
|
|
58
|
-
return { rest, json, cfgPath };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// 匹配命令:把声明路径与 argv 前缀逐 token 比较,选最长匹配。
|
|
62
|
-
//
|
|
63
|
-
// 逐 token 比较(而非只取「开头连续的非 flag token」)是必需的——有些命令路径
|
|
64
|
-
// 本身就含 flag,例如 `skill install --list` 是「列出内置包」而非「安装」。
|
|
65
|
-
// 最长优先则让 ['skill','central','add'] 压过 ['skill','central'],子命令与别名因此共存。
|
|
66
|
-
export function resolveCommand(argv) {
|
|
67
|
-
let best = null;
|
|
68
|
-
let bestLen = -1;
|
|
69
|
-
for (const cmd of ALL_COMMANDS) {
|
|
70
|
-
for (const path of cliPathsOf(cmd)) {
|
|
71
|
-
if (path.length > argv.length || path.length <= bestLen) continue;
|
|
72
|
-
if (path.every((seg, i) => seg === argv[i])) {
|
|
73
|
-
best = { cmd, path };
|
|
74
|
-
bestLen = path.length;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return best ? { ...best, rest: argv.slice(bestLen) } : null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// 解析剩余 token。布尔 flag 不吞下一个 token —— 否则 `skill install --force demo`
|
|
82
|
-
// 会把 force 解析成字符串 'demo',而 demo 本该是位置参数。
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
i
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
lines.push(
|
|
206
|
-
lines.push(
|
|
207
|
-
lines.push(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
groups.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
lines.push('');
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
lines.push('
|
|
227
|
-
lines.push(' --
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
lines.push('');
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (
|
|
241
|
-
lines.push('
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const {
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
console.log(
|
|
254
|
-
console.log(
|
|
255
|
-
console.log(
|
|
256
|
-
console.log('
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
process.on('
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
console.error('
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
await
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
1
|
+
// 通用 CLI 运行器:解析 argv → 匹配 action → 校验 → 执行 → 渲染。
|
|
2
|
+
//
|
|
3
|
+
// 改造前这里是一个 400 行的 switch,每条命令要在 BOOL_FLAGS / helpText / case /
|
|
4
|
+
// renderXxx 四处重复登记。现在命令表、help 文本、参数校验全部由 action 声明派生,
|
|
5
|
+
// 新增命令只需在所属模块的 actions 里加一条。
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { DEFAULT_PORT } from '../core/paths.js';
|
|
8
|
+
import { configPathFromEnv } from '../core/config.js';
|
|
9
|
+
import { toErrorPayload, exitCodeOf, badInput } from '../core/errors.js';
|
|
10
|
+
import { ACTIONS, MODULES } from './registry.js';
|
|
11
|
+
import { cliPathsOf, argSpecsOf, flagSpecsOf, booleanFlagNames, applySpec, usageOf } from './spec.js';
|
|
12
|
+
|
|
13
|
+
const VERSION = JSON.parse(
|
|
14
|
+
readFileSync(new URL('../../package.json', import.meta.url), 'utf8')
|
|
15
|
+
).version;
|
|
16
|
+
|
|
17
|
+
// ---- 平台命令:不属于任何功能域,不参与 action 表 ----
|
|
18
|
+
// 用与 action 相同的形状声明,好让参数解析与 help 生成保持同一套逻辑。
|
|
19
|
+
const BUILTINS = [
|
|
20
|
+
{
|
|
21
|
+
id: 'serve',
|
|
22
|
+
cli: ['serve'],
|
|
23
|
+
summary: '启动 Web 面板',
|
|
24
|
+
flags: { port: { type: 'number', default: DEFAULT_PORT }, 'no-open': { type: 'boolean' } },
|
|
25
|
+
run: (ctx) => cmdServe(ctx),
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'help',
|
|
29
|
+
cli: ['help'],
|
|
30
|
+
summary: '显示帮助(可跟模块名或命令组,如 nx-kv help todo)',
|
|
31
|
+
args: [{ name: 'topic', required: false }],
|
|
32
|
+
// 走标准 action 形态:--json 时 emit 会序列化返回的条目,
|
|
33
|
+
// 于是 `help --json` 输出的是可解析的命令表而非帮助文本
|
|
34
|
+
run: (ctx) => helpEntries(ctx.topic),
|
|
35
|
+
render: (entries, ctx) => renderHelp(entries, ctx.topic),
|
|
36
|
+
},
|
|
37
|
+
// render 让 `nx-kv version` 输出裸版本号(脚本里可 `V=$(nx-kv version)`),
|
|
38
|
+
// 而 `--json` 仍走序列化,保持机器可读
|
|
39
|
+
{ id: 'version', cli: ['version'], summary: '显示版本', run: () => VERSION, render: (v) => v },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
export const ALL_COMMANDS = [...BUILTINS, ...ACTIONS];
|
|
43
|
+
|
|
44
|
+
// ---- 参数解析 ----
|
|
45
|
+
|
|
46
|
+
// 全局 flag 先摘掉:它们可以出现在 argv 任意位置,不属于任何 action 的声明。
|
|
47
|
+
function splitGlobals(argv) {
|
|
48
|
+
const rest = [];
|
|
49
|
+
let json = false;
|
|
50
|
+
let cfgPath;
|
|
51
|
+
for (let i = 0; i < argv.length; i++) {
|
|
52
|
+
const a = argv[i];
|
|
53
|
+
if (a === '--json') { json = true; continue; }
|
|
54
|
+
if (a === '--config') { cfgPath = argv[++i]; continue; }
|
|
55
|
+
if (a.startsWith('--config=')) { cfgPath = a.slice('--config='.length); continue; }
|
|
56
|
+
rest.push(a);
|
|
57
|
+
}
|
|
58
|
+
return { rest, json, cfgPath };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 匹配命令:把声明路径与 argv 前缀逐 token 比较,选最长匹配。
|
|
62
|
+
//
|
|
63
|
+
// 逐 token 比较(而非只取「开头连续的非 flag token」)是必需的——有些命令路径
|
|
64
|
+
// 本身就含 flag,例如 `skill install --list` 是「列出内置包」而非「安装」。
|
|
65
|
+
// 最长优先则让 ['skill','central','add'] 压过 ['skill','central'],子命令与别名因此共存。
|
|
66
|
+
export function resolveCommand(argv) {
|
|
67
|
+
let best = null;
|
|
68
|
+
let bestLen = -1;
|
|
69
|
+
for (const cmd of ALL_COMMANDS) {
|
|
70
|
+
for (const path of cliPathsOf(cmd)) {
|
|
71
|
+
if (path.length > argv.length || path.length <= bestLen) continue;
|
|
72
|
+
if (path.every((seg, i) => seg === argv[i])) {
|
|
73
|
+
best = { cmd, path };
|
|
74
|
+
bestLen = path.length;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return best ? { ...best, rest: argv.slice(bestLen) } : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 解析剩余 token。布尔 flag 不吞下一个 token —— 否则 `skill install --force demo`
|
|
82
|
+
// 会把 force 解析成字符串 'demo',而 demo 本该是位置参数。
|
|
83
|
+
// 任务文本带空格时给 `--ref` 加引号(见 spec.js 关于 rest flag 的说明)。
|
|
84
|
+
function parseRest(rest, cmd) {
|
|
85
|
+
const bools = booleanFlagNames(cmd);
|
|
86
|
+
const positionals = [];
|
|
87
|
+
const flags = {};
|
|
88
|
+
for (let i = 0; i < rest.length; i++) {
|
|
89
|
+
const a = rest[i];
|
|
90
|
+
if (!a.startsWith('--')) { positionals.push(a); continue; }
|
|
91
|
+
const eq = a.indexOf('=');
|
|
92
|
+
if (eq > 2) { flags[a.slice(2, eq)] = a.slice(eq + 1); continue; }
|
|
93
|
+
const key = a.slice(2);
|
|
94
|
+
const next = rest[i + 1];
|
|
95
|
+
if (!bools.has(key) && next !== undefined && !next.startsWith('--')) {
|
|
96
|
+
flags[key] = next;
|
|
97
|
+
i++;
|
|
98
|
+
} else {
|
|
99
|
+
flags[key] = true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { positionals, flags };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 位置参数按声明顺序映射成具名输入;末尾声明 { rest: true } 的收集剩余全部
|
|
106
|
+
function mapPositionals(positionals, argSpecs) {
|
|
107
|
+
const out = {};
|
|
108
|
+
let i = 0;
|
|
109
|
+
for (const spec of argSpecs) {
|
|
110
|
+
if (spec.rest) { out[spec.name] = positionals.slice(i); i = positionals.length; break; }
|
|
111
|
+
if (positionals[i] !== undefined) out[spec.name] = positionals[i];
|
|
112
|
+
i++;
|
|
113
|
+
}
|
|
114
|
+
if (i < positionals.length) {
|
|
115
|
+
throw badInput(`多余的参数: ${positionals.slice(i).join(' ')}(用法见 nx-kv help)`);
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 未知 flag 直接报错——`--fiile` 这类拼写错误应该立刻被指出来,而不是被静默忽略
|
|
121
|
+
function assertKnownFlags(flags, cmd) {
|
|
122
|
+
const known = new Set(flagSpecsOf(cmd).map((f) => f.name));
|
|
123
|
+
for (const k of Object.keys(flags)) {
|
|
124
|
+
if (!known.has(k)) throw badInput(`未知参数 --${k}(用法见 nx-kv help)`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---- 输出 ----
|
|
129
|
+
|
|
130
|
+
// render 拿到 (data, ctx),可以是 async——有些渲染需要补一点上下文才能标注
|
|
131
|
+
// (例如候选清单要标出哪一项是「当前」),而 `--json` 必须保持纯数据的旧形状。
|
|
132
|
+
async function emit(cmd, data, json, ctx) {
|
|
133
|
+
// 无返回值的命令(help / serve 这类)不该打印任何东西:落进 JSON.stringify(undefined)
|
|
134
|
+
// 会打出字面量 "undefined",让 `help --json` 的 stdout 不再是合法 JSON。
|
|
135
|
+
if (data === undefined && !cmd.render) return;
|
|
136
|
+
|
|
137
|
+
if (json) {
|
|
138
|
+
if (data === undefined) return;
|
|
139
|
+
console.log(JSON.stringify(data, null, 2));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (cmd.render) {
|
|
144
|
+
const text = await cmd.render(data, ctx);
|
|
145
|
+
if (text !== undefined && text !== null && text !== '') console.log(text);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
console.log(JSON.stringify(data, null, 2));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 失败一律 exit 1;--json 下输出可解析的错误对象。
|
|
152
|
+
// 注意 error 保持字符串(历史契约),code 是新增字段——agent 可据此分支,
|
|
153
|
+
// 老消费方按字符串匹配「用法:」「未设置」「不存在」仍然可用。
|
|
154
|
+
function fail(err, json) {
|
|
155
|
+
const p = toErrorPayload(err);
|
|
156
|
+
if (json) {
|
|
157
|
+
const payload = { ok: false, error: p.message, code: p.code };
|
|
158
|
+
if (p.details !== undefined) payload.details = p.details;
|
|
159
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
160
|
+
} else {
|
|
161
|
+
console.error('错误: ' + p.message);
|
|
162
|
+
}
|
|
163
|
+
process.exitCode = exitCodeOf(p.code);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---- help:完全由命令表生成 ----
|
|
167
|
+
|
|
168
|
+
// 命令表的一行。`routes` 与 `help` 共用它,因此两张表必然同源同长——
|
|
169
|
+
// 面板底部的「CLI 等价」提示用的也是这个形状(经 bootstrap 下发)。
|
|
170
|
+
export function commandEntry(c) {
|
|
171
|
+
return {
|
|
172
|
+
id: c.id,
|
|
173
|
+
module: c.module || 'platform',
|
|
174
|
+
command: 'nx-kv ' + cliPathsOf(c)[0].join(' '),
|
|
175
|
+
usage: usageOf(c),
|
|
176
|
+
http: c.http ? { method: c.http[0], path: c.http[1] } : null,
|
|
177
|
+
summary: c.summary,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 解析帮助主题。既接受模块 id(repos),也接受命令组(repo)——
|
|
182
|
+
// 用户脑子里想的是「repo 相关的东西」,不会去记内部模块名。
|
|
183
|
+
// 这条路径不能报错退出:`nx-kv help <随便什么>` 失败会让最需要帮助的人卡住。
|
|
184
|
+
function helpEntries(topic) {
|
|
185
|
+
const all = ALL_COMMANDS.map(commandEntry);
|
|
186
|
+
if (!topic) return all;
|
|
187
|
+
|
|
188
|
+
const t = String(topic);
|
|
189
|
+
if (MODULES.some((m) => m.id === t)) return all.filter((e) => e.module === t);
|
|
190
|
+
|
|
191
|
+
const byRoot = all.filter((e) => e.command.split(' ')[1] === t);
|
|
192
|
+
if (byRoot.length) return byRoot;
|
|
193
|
+
|
|
194
|
+
// 命令本身的 id 也认(如 `nx-kv help repo.add`)
|
|
195
|
+
const byId = all.filter((e) => e.id === t);
|
|
196
|
+
if (byId.length) return byId;
|
|
197
|
+
|
|
198
|
+
const topics = [...new Set(all.map((e) => e.module))].join(', ');
|
|
199
|
+
throw badInput(`未知帮助主题: ${t}(可用模块: ${topics};或命令组如 repo / skill / gh)`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function renderHelp(entries, topic) {
|
|
203
|
+
const lines = [];
|
|
204
|
+
if (!topic) {
|
|
205
|
+
lines.push(`nx-kv · nx-kv v${VERSION}`);
|
|
206
|
+
lines.push('KV 清单管理中枢:serve 驱动 CLI 与 Web,同源同构。');
|
|
207
|
+
lines.push(`配置: ${configPathFromEnv()} (环境变量 NX_KV_CONFIG 或 --config 覆盖)`);
|
|
208
|
+
lines.push('');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const groups = new Map();
|
|
212
|
+
for (const e of entries) {
|
|
213
|
+
if (!groups.has(e.module)) groups.set(e.module, []);
|
|
214
|
+
groups.get(e.module).push(e);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (const [mod, list] of groups) {
|
|
218
|
+
const title =
|
|
219
|
+
MODULES.find((m) => m.id === mod)?.title || (mod === 'platform' ? '平台命令' : mod);
|
|
220
|
+
lines.push(title + ':');
|
|
221
|
+
const width = Math.max(...list.map((e) => e.usage.length));
|
|
222
|
+
for (const e of list) lines.push(' ' + e.usage.padEnd(width + 2) + e.summary);
|
|
223
|
+
lines.push('');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
lines.push('通用:');
|
|
227
|
+
lines.push(' --json 机器可读输出(agent 模式)');
|
|
228
|
+
lines.push(' --config 本次运行覆盖配置文件路径');
|
|
229
|
+
return lines.join('\n');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// `nx-kv <命令> --help`:只打印这一条,而不是整个模块——
|
|
233
|
+
// 之前这里传的是 cmd.cli[0],对别名 action(cli 是数组的数组)会拼出 "bundled,list"。
|
|
234
|
+
function printCommandHelp(cmd) {
|
|
235
|
+
const lines = [usageOf(cmd)];
|
|
236
|
+
if (cmd.summary) lines.push(' ' + cmd.summary);
|
|
237
|
+
lines.push('');
|
|
238
|
+
|
|
239
|
+
const aliases = cliPathsOf(cmd).slice(1).map((p) => 'nx-kv ' + p.join(' '));
|
|
240
|
+
if (aliases.length) lines.push('别名: ' + aliases.join(' · '));
|
|
241
|
+
if (cmd.http) lines.push('对应路由: ' + cmd.http[0] + ' ' + cmd.http[1]);
|
|
242
|
+
lines.push('所属模块: ' + (cmd.module || 'platform'));
|
|
243
|
+
console.log(lines.join('\n'));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function cmdServe(ctx) {
|
|
247
|
+
const { startServer } = await import('./server.js');
|
|
248
|
+
const { openBrowser } = await import('../core/open.js');
|
|
249
|
+
const port = ctx.port || DEFAULT_PORT;
|
|
250
|
+
const server = await startServer({ port, host: '127.0.0.1' });
|
|
251
|
+
const addr = `http://127.0.0.1:${server.address().port}`;
|
|
252
|
+
|
|
253
|
+
console.log('nx-kv · nx-kv v' + VERSION);
|
|
254
|
+
console.log(`面板: ${addr}`);
|
|
255
|
+
console.log(`配置: ${configPathFromEnv()}`);
|
|
256
|
+
console.log('CLI: nx-kv help(每个按钮都有对应命令,agent 可加 --json)');
|
|
257
|
+
console.log('按 Ctrl+C 停止');
|
|
258
|
+
|
|
259
|
+
if (!ctx['no-open']) openBrowser(addr);
|
|
260
|
+
|
|
261
|
+
const shutdown = () => {
|
|
262
|
+
console.log('\n正在停止...');
|
|
263
|
+
server.close(() => process.exit(0));
|
|
264
|
+
};
|
|
265
|
+
process.on('SIGINT', shutdown);
|
|
266
|
+
process.on('SIGTERM', shutdown);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ---- 主入口 ----
|
|
270
|
+
|
|
271
|
+
export async function runCli(argv) {
|
|
272
|
+
const { rest, json, cfgPath } = splitGlobals(argv);
|
|
273
|
+
if (cfgPath) process.env.NX_KV_CONFIG = cfgPath;
|
|
274
|
+
|
|
275
|
+
const hit = resolveCommand(rest);
|
|
276
|
+
|
|
277
|
+
// 无参数、或只有全局 flag:显示帮助(与历史行为一致,退出码 0)
|
|
278
|
+
if (!hit) {
|
|
279
|
+
if (!rest.length) { console.log(renderHelp(helpEntries(), '')); return; }
|
|
280
|
+
console.error(`未知命令: ${rest.join(' ')}`);
|
|
281
|
+
console.error('运行 nx-kv help 查看用法');
|
|
282
|
+
process.exitCode = 1;
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const { cmd, rest: tail } = hit;
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
if (tail.includes('--help')) { printCommandHelp(cmd); return; }
|
|
290
|
+
|
|
291
|
+
const { positionals, flags } = parseRest(tail, cmd);
|
|
292
|
+
assertKnownFlags(flags, cmd);
|
|
293
|
+
const input = { ...mapPositionals(positionals, argSpecsOf(cmd)), ...flags };
|
|
294
|
+
const ctx = applySpec(cmd, input);
|
|
295
|
+
const data = await cmd.run(ctx, { transport: 'cli' });
|
|
296
|
+
await emit(cmd, data, json, ctx);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
fail(err, json);
|
|
299
|
+
}
|
|
300
|
+
}
|
package/src/runtime/registry.js
CHANGED
|
@@ -53,4 +53,15 @@ for (const a of ACTIONS) {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
if (!a.run) throw new Error(`action 缺少 run: ${a.id}`);
|
|
56
|
+
|
|
57
|
+
// rest flag(吃掉其后全部 token,见 spec.js)必须在声明里排最后:
|
|
58
|
+
// CLI 是「谁的路径最长谁赢」,flag 的先后由声明顺序决定;一旦后面还跟着别的 flag,
|
|
59
|
+
// 它就会被当成值的一部分静默吞掉——命令照跑,参数却没了。
|
|
60
|
+
const flagNames = Object.keys(a.flags || {});
|
|
61
|
+
const restAt = flagNames.findIndex((n) => a.flags[n].rest);
|
|
62
|
+
if (restAt >= 0 && restAt !== flagNames.length - 1) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`rest flag 必须声明在 flags 的最后(其后还有: ${flagNames.slice(restAt + 1).join(', ')}): ${a.id}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
56
67
|
}
|