@longzai-intelligence-issues/cli 0.0.1
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 +16 -0
- package/README.md +33 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/rolldown-runtime-DCIJsbUY.js +2 -0
- package/lzi-builder.config.ts +12 -0
- package/lzi-bun.config.ts +6 -0
- package/oxlint.config.ts +3 -0
- package/package.json +46 -0
- package/src/commands/close.commands.ts +257 -0
- package/src/commands/context.utils.ts +108 -0
- package/src/commands/gate.commands.ts +259 -0
- package/src/commands/index.ts +29 -0
- package/src/commands/issue.commands.ts +252 -0
- package/src/commands/scaffold.commands.ts +140 -0
- package/src/index.ts +31 -0
- package/tsconfig/.cache/build.tsbuildinfo +1 -0
- package/tsconfig/.cache/node.tsbuildinfo +1 -0
- package/tsconfig/.cache/test.tsbuildinfo +1 -0
- package/tsconfig/app.json +13 -0
- package/tsconfig/build.json +15 -0
- package/tsconfig/node.json +12 -0
- package/tsconfig/test.json +15 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import type { Command } from '@longzai-intelligence-cli/core';
|
|
2
|
+
|
|
3
|
+
import { defineCommand } from '@longzai-intelligence-cli/core';
|
|
4
|
+
import {
|
|
5
|
+
runDocsCheck,
|
|
6
|
+
runHealth,
|
|
7
|
+
runLint,
|
|
8
|
+
runNumberingAudit,
|
|
9
|
+
runNumberingCheck,
|
|
10
|
+
runNumberingNext,
|
|
11
|
+
runTriage,
|
|
12
|
+
runWriteDocsBaseline,
|
|
13
|
+
runWriteLintBaseline,
|
|
14
|
+
} from '@longzai-intelligence-issues/ledger';
|
|
15
|
+
import { writeFileSync } from 'node:fs';
|
|
16
|
+
import * as z from 'zod';
|
|
17
|
+
|
|
18
|
+
import { fail, info, loadConfigOrExit, ok, repoRoot, violation } from './context.utils.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 门禁与观测命令组:lint / numbering / docs / health / triage
|
|
22
|
+
*
|
|
23
|
+
* 判据口径:exit 0 过 / exit 1 违规(报文逐条输出后退出)。
|
|
24
|
+
*/
|
|
25
|
+
export function registerGateCommands(program: Command): void {
|
|
26
|
+
defineCommand(program, {
|
|
27
|
+
name: 'lint',
|
|
28
|
+
description: '注册表 lint(12 核心规则 + 棘轮基线三态)',
|
|
29
|
+
options: [
|
|
30
|
+
{ flags: '--dir <dir>', description: '限定注册表目录(可重复)' },
|
|
31
|
+
{ flags: '--strict-legacy', description: '旧格式状态行一并计违规' },
|
|
32
|
+
{ flags: '--write-baseline', description: '再生棘轮基线(须配 --issue)' },
|
|
33
|
+
{ flags: '--issue <issue>', description: '收编指针(write-baseline 必填)' },
|
|
34
|
+
{ flags: '--config <path>', description: '显式配置路径' },
|
|
35
|
+
],
|
|
36
|
+
schema: z
|
|
37
|
+
.object({
|
|
38
|
+
dir: z.union([z.string(), z.array(z.string())]).optional(),
|
|
39
|
+
strictLegacy: z.boolean().optional(),
|
|
40
|
+
writeBaseline: z.boolean().optional(),
|
|
41
|
+
issue: z.string().optional(),
|
|
42
|
+
config: z.string().optional(),
|
|
43
|
+
})
|
|
44
|
+
.passthrough(),
|
|
45
|
+
action: async (opts) => {
|
|
46
|
+
const root = repoRoot();
|
|
47
|
+
const { config, configFilePath } = await loadConfigOrExit(opts.config);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 限定目录归一
|
|
51
|
+
*/
|
|
52
|
+
const dirRels = opts.dir === undefined ? undefined : Array.isArray(opts.dir) ? opts.dir : [opts.dir];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* strictLegacy 覆盖(命令级旗标优先)
|
|
56
|
+
*/
|
|
57
|
+
const effective = opts.strictLegacy === true ? { ...config, lint: { ...config.lint, strictLegacy: true } } : config;
|
|
58
|
+
|
|
59
|
+
if (opts.writeBaseline === true) {
|
|
60
|
+
const regen = runWriteLintBaseline({ root, config: effective, issuePointer: opts.issue ?? '' });
|
|
61
|
+
|
|
62
|
+
if (!regen.ok) {
|
|
63
|
+
fail(regen.error);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
writeFileSync(configFilePath, regen.content);
|
|
67
|
+
ok(`基线已再生(${regen.entryCount} 条,收编指针 ${opts.issue})——棘轮只许缩短`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const result = runLint({ root, config: effective, dirRels });
|
|
72
|
+
|
|
73
|
+
for (const v of result.violations) {
|
|
74
|
+
violation(`${v.file}:${v.line} [${v.rule}] ${v.message}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (result.violations.length > 0) {
|
|
78
|
+
process.stderr.write(`错误: issues:lint ${result.violations.length} 项违规|存量豁免 ${result.suppressed}|基线 ${result.baselineCount} 条\n`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
ok(`issues:lint 通过(0 新增违规|存量豁免 ${result.suppressed}|基线 ${result.baselineCount} 条)`);
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* numbering 三子命令
|
|
88
|
+
*/
|
|
89
|
+
const numberingSchema = z
|
|
90
|
+
.object({
|
|
91
|
+
scope: z.string().optional(),
|
|
92
|
+
dir: z.string().optional(),
|
|
93
|
+
config: z.string().optional(),
|
|
94
|
+
})
|
|
95
|
+
.passthrough();
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* numbering 父命令(子命令 next/check/audit)
|
|
99
|
+
*/
|
|
100
|
+
const numbering = program.command('numbering').description('编号门禁(next 取号 / check 判定 / audit 覆盖面审计)');
|
|
101
|
+
|
|
102
|
+
defineCommand(numbering, {
|
|
103
|
+
name: 'next',
|
|
104
|
+
description: '取号(--scope <key> 或 --dir <目录> 互斥;只读不写)',
|
|
105
|
+
options: [
|
|
106
|
+
{ flags: '--scope <scope>', description: '目标 scope key' },
|
|
107
|
+
{ flags: '--dir <dir>', description: '目标注册表目录' },
|
|
108
|
+
{ flags: '--config <path>', description: '显式配置路径' },
|
|
109
|
+
],
|
|
110
|
+
schema: numberingSchema,
|
|
111
|
+
action: async (opts) => {
|
|
112
|
+
const root = repoRoot();
|
|
113
|
+
const { config } = await loadConfigOrExit(opts.config);
|
|
114
|
+
|
|
115
|
+
const result = runNumberingNext({ root, config, scopeKey: opts.scope, dirRel: opts.dir });
|
|
116
|
+
|
|
117
|
+
if (!result.ok) {
|
|
118
|
+
fail(result.error);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
info(`${result.target} 下一可用编号:${result.number}(已跳过保留号与已占用号)`);
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
defineCommand(numbering, {
|
|
126
|
+
name: 'check',
|
|
127
|
+
description: '编号门禁判定(缺省全量;--dir 目录增量)',
|
|
128
|
+
options: [
|
|
129
|
+
{ flags: '--dir <dir>', description: '目录增量档' },
|
|
130
|
+
{ flags: '--config <path>', description: '显式配置路径' },
|
|
131
|
+
],
|
|
132
|
+
schema: numberingSchema,
|
|
133
|
+
action: async (opts) => {
|
|
134
|
+
const root = repoRoot();
|
|
135
|
+
const { config } = await loadConfigOrExit(opts.config);
|
|
136
|
+
|
|
137
|
+
const result = runNumberingCheck({ root, config, dirRel: opts.dir });
|
|
138
|
+
|
|
139
|
+
for (const f of result.findings) {
|
|
140
|
+
violation(`[${f.kind}] ${f.message}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (result.findings.length > 0) {
|
|
144
|
+
process.stderr.write(`错误: numbering:check ${result.findings.length} 项违规(scope ${result.scopeCount} 个 / 扫描面 ${result.scannedFiles} 文件)\n`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
ok(`numbering:check 通过(scope ${result.scopeCount} 个 / 扫描面 ${result.scannedFiles} 文件 / 零违规)`);
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
defineCommand(numbering, {
|
|
153
|
+
name: 'audit',
|
|
154
|
+
description: '覆盖面审计(未登记编号区 / pending 失效)',
|
|
155
|
+
options: [{ flags: '--config <path>', description: '显式配置路径' }],
|
|
156
|
+
schema: numberingSchema,
|
|
157
|
+
action: async (opts) => {
|
|
158
|
+
const root = repoRoot();
|
|
159
|
+
const { config } = await loadConfigOrExit(opts.config);
|
|
160
|
+
|
|
161
|
+
const result = runNumberingAudit({ root, config });
|
|
162
|
+
|
|
163
|
+
for (const f of result.findings) {
|
|
164
|
+
violation(`[${f.kind}] ${f.message}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (result.findings.length > 0) {
|
|
168
|
+
process.stderr.write(`错误: numbering:audit ${result.findings.length} 项违规(扫描面 ${result.scannedFiles} 文件)\n`);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
ok(`numbering:audit 通过(扫描面 ${result.scannedFiles} 文件 / 零未登记编号区)`);
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* docs 两子命令
|
|
178
|
+
*/
|
|
179
|
+
/**
|
|
180
|
+
* docs 父命令(子命令 check / fix)
|
|
181
|
+
*/
|
|
182
|
+
const docs = program.command('docs').description('文档 issue 引用检查与机械修复');
|
|
183
|
+
|
|
184
|
+
defineCommand(docs, {
|
|
185
|
+
name: 'check',
|
|
186
|
+
description: '文档 issue 引用检查(不完整 slug 前缀唯一解析 / 棘轮基线)',
|
|
187
|
+
options: [
|
|
188
|
+
{ flags: '--write-baseline', description: '再生 docs 基线(须配 --issue)' },
|
|
189
|
+
{ flags: '--issue <issue>', description: '收编指针' },
|
|
190
|
+
{ flags: '--config <path>', description: '显式配置路径' },
|
|
191
|
+
],
|
|
192
|
+
schema: z.object({ writeBaseline: z.boolean().optional(), issue: z.string().optional(), config: z.string().optional() }).passthrough(),
|
|
193
|
+
action: async (opts) => {
|
|
194
|
+
const root = repoRoot();
|
|
195
|
+
const { config, configFilePath } = await loadConfigOrExit(opts.config);
|
|
196
|
+
|
|
197
|
+
if (opts.writeBaseline === true) {
|
|
198
|
+
try {
|
|
199
|
+
const regen = runWriteDocsBaseline({ root, config, issuePointer: opts.issue ?? '' });
|
|
200
|
+
|
|
201
|
+
if (!regen.ok) {
|
|
202
|
+
fail(regen.error);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
writeFileSync(configFilePath, regen.content);
|
|
206
|
+
ok(`docs 基线已再生(${regen.entryCount} 条)`);
|
|
207
|
+
return;
|
|
208
|
+
} catch (error) {
|
|
209
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const result = runDocsCheck({ root, config });
|
|
215
|
+
|
|
216
|
+
for (const v of result.violations) {
|
|
217
|
+
violation(`${v.file}:${v.line} [${v.rule}] ${v.message}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (result.violations.length > 0) {
|
|
221
|
+
process.stderr.write(`错误: docs:check ${result.violations.length} 项违规(扫描面 ${result.scannedFiles} 文件 / 豁免 ${result.suppressed})\n`);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
ok(`docs:check 通过(扫描面 ${result.scannedFiles} 文件 / 零违规)`);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
defineCommand(program, {
|
|
233
|
+
name: 'health',
|
|
234
|
+
description: '黄灯巡检(状态分布 / 最老账龄 / A-B-C 分类,JSON)',
|
|
235
|
+
options: [{ flags: '--config <path>', description: '显式配置路径' }],
|
|
236
|
+
schema: z.object({ config: z.string().optional() }).passthrough(),
|
|
237
|
+
action: async (opts) => {
|
|
238
|
+
const root = repoRoot();
|
|
239
|
+
const { config } = await loadConfigOrExit(opts.config);
|
|
240
|
+
|
|
241
|
+
process.stdout.write(`${JSON.stringify(runHealth({ root, config }), null, 2)}\n`);
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
defineCommand(program, {
|
|
246
|
+
name: 'triage',
|
|
247
|
+
description: '黄灯分诊台账(append-only 落 .lzi/issues/runs/ledger/)',
|
|
248
|
+
options: [{ flags: '--config <path>', description: '显式配置路径' }],
|
|
249
|
+
schema: z.object({ config: z.string().optional() }).passthrough(),
|
|
250
|
+
action: async (opts) => {
|
|
251
|
+
const root = repoRoot();
|
|
252
|
+
const { config } = await loadConfigOrExit(opts.config);
|
|
253
|
+
|
|
254
|
+
const { ledgerPath, ledger } = runTriage({ root, config });
|
|
255
|
+
|
|
256
|
+
ok(`issues:triage 台账已落盘: ${ledgerPath}(黄灯 ${ledger.items.length} 项)`);
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 命令注册入口
|
|
3
|
+
*
|
|
4
|
+
* 四组:脚手架(init/template)、议题(create/list/show)、门禁与观测
|
|
5
|
+
* (lint/numbering/docs/health/triage)、收口与冻结(verify-close/freeze/
|
|
6
|
+
* unfreeze/normalize-header/prompts)。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Command } from '@longzai-intelligence-cli/core';
|
|
10
|
+
|
|
11
|
+
import { registerCloseCommands } from './close.commands.js';
|
|
12
|
+
import { registerGateCommands } from './gate.commands.js';
|
|
13
|
+
import { registerIssueCommands } from './issue.commands.js';
|
|
14
|
+
import { registerScaffoldCommands } from './scaffold.commands.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 注册全部命令
|
|
18
|
+
*
|
|
19
|
+
* @param program - CLI 程序实例
|
|
20
|
+
*/
|
|
21
|
+
export function registerCommands(program: Command): void {
|
|
22
|
+
registerScaffoldCommands(program);
|
|
23
|
+
|
|
24
|
+
registerIssueCommands(program);
|
|
25
|
+
|
|
26
|
+
registerGateCommands(program);
|
|
27
|
+
|
|
28
|
+
registerCloseCommands(program);
|
|
29
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import type { Command } from '@longzai-intelligence-cli/core';
|
|
2
|
+
|
|
3
|
+
import { defineCommand } from '@longzai-intelligence-cli/core';
|
|
4
|
+
import { renderIssueTemplate } from '@longzai-intelligence-issues/ledger';
|
|
5
|
+
import { runNumberingNext, scanRegistryDir } from '@longzai-intelligence-issues/ledger';
|
|
6
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join, relative } from 'node:path';
|
|
8
|
+
import * as z from 'zod';
|
|
9
|
+
|
|
10
|
+
import { fail, info, loadConfigOrExit, ok, repoRoot, resolveScopeFromCwd } from './context.utils.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* create / list / show 命令组:建档与查询
|
|
14
|
+
*/
|
|
15
|
+
export function registerIssueCommands(program: Command): void {
|
|
16
|
+
defineCommand(program, {
|
|
17
|
+
name: 'create',
|
|
18
|
+
description: '建档(门禁取号 → 标准模板落盘 → 编号自检)',
|
|
19
|
+
options: [
|
|
20
|
+
{ flags: '--title <title>', description: '一句话标题(必填)' },
|
|
21
|
+
{ flags: '--slug <slug>', description: 'slug(缺省由标题派生)' },
|
|
22
|
+
{ flags: '--priority <priority>', description: 'P1 | P2 | P3(缺省 P2)' },
|
|
23
|
+
{ flags: '--type <type>', description: '议题类型(缺省 task)' },
|
|
24
|
+
{ flags: '--scope <scope>', description: 'scope key(缺省按 cwd 解析)' },
|
|
25
|
+
{ flags: '--source <source>', description: '来源说明' },
|
|
26
|
+
{ flags: '--dry-run', description: '只呈现不落盘' },
|
|
27
|
+
{ flags: '--config <path>', description: '显式配置路径' },
|
|
28
|
+
],
|
|
29
|
+
schema: z
|
|
30
|
+
.object({
|
|
31
|
+
title: z.string(),
|
|
32
|
+
slug: z.string().optional(),
|
|
33
|
+
priority: z.string().optional(),
|
|
34
|
+
type: z.string().optional(),
|
|
35
|
+
scope: z.string().optional(),
|
|
36
|
+
source: z.string().optional(),
|
|
37
|
+
dryRun: z.boolean().optional(),
|
|
38
|
+
config: z.string().optional(),
|
|
39
|
+
})
|
|
40
|
+
.passthrough(),
|
|
41
|
+
action: async (opts) => {
|
|
42
|
+
/**
|
|
43
|
+
* 仓库根与配置
|
|
44
|
+
*/
|
|
45
|
+
const root = repoRoot();
|
|
46
|
+
const { config } = await loadConfigOrExit(opts.config);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* scope key(显式 > cwd 最长前缀 > 全部列出退出)
|
|
50
|
+
*/
|
|
51
|
+
const cwdRel = relative(root, process.cwd());
|
|
52
|
+
const scopeKey =
|
|
53
|
+
opts.scope ??
|
|
54
|
+
resolveScopeFromCwd(config, cwdRel) ??
|
|
55
|
+
fail(`cwd「${cwdRel}」未命中任何 scope 覆盖面——候选:${config.scopes.map((s) => s.key).join('、')}(或显式 --scope)`);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 目标 scope
|
|
59
|
+
*/
|
|
60
|
+
const scope = config.scopes.find((s) => s.key === scopeKey) ?? fail(`scope「${scopeKey}」未登记`);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 写入时刻取号(落盘前现算最新值)
|
|
64
|
+
*/
|
|
65
|
+
const next = runNumberingNext({ root, config, scopeKey });
|
|
66
|
+
|
|
67
|
+
if (!next.ok) {
|
|
68
|
+
fail(next.error);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* slug(缺省标题派生 kebab)
|
|
73
|
+
*/
|
|
74
|
+
/**
|
|
75
|
+
* slug(缺省由标题派生 ASCII kebab——中文标题无法派生,须显式 --slug)
|
|
76
|
+
*/
|
|
77
|
+
const derivedSlug =
|
|
78
|
+
opts.slug ??
|
|
79
|
+
opts.title
|
|
80
|
+
.toLowerCase()
|
|
81
|
+
.replace(/[\s_]+/g, '-')
|
|
82
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
83
|
+
.replace(/-+/g, '-')
|
|
84
|
+
.replace(/^-|-$/g, '');
|
|
85
|
+
|
|
86
|
+
const slug = /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(derivedSlug)
|
|
87
|
+
? derivedSlug
|
|
88
|
+
: fail('slug 须为纯 ASCII kebab(小写字母/数字/连字符,禁中文)——中文标题请显式提供 --slug <slug>');
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 标准模板落盘(instance 渲染)
|
|
92
|
+
*/
|
|
93
|
+
const content = renderIssueTemplate({
|
|
94
|
+
number: next.ok ? next.number : '0001',
|
|
95
|
+
slug,
|
|
96
|
+
title: opts.title,
|
|
97
|
+
priority: (opts.priority === 'P1' || opts.priority === 'P3' ? opts.priority : 'P2') as 'P1' | 'P2' | 'P3',
|
|
98
|
+
scopeTitle: scope.title,
|
|
99
|
+
date: new Date().toISOString().slice(0, 10),
|
|
100
|
+
source: opts.source ?? 'CLI 建档',
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 文件路径(注册表目录缺席自创建——init 的轻量等价)
|
|
105
|
+
*/
|
|
106
|
+
const registryAbs = join(root, scope.registryDir);
|
|
107
|
+
const filePath = join(registryAbs, `${next.ok ? next.number : '0001'}-${slug}.md`);
|
|
108
|
+
|
|
109
|
+
if (opts.dryRun === true) {
|
|
110
|
+
info(`(dry-run)将落盘:${relative(root, filePath)}`);
|
|
111
|
+
process.stdout.write(content);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
mkdirSync(registryAbs, { recursive: true });
|
|
116
|
+
|
|
117
|
+
if (existsSync(filePath)) {
|
|
118
|
+
fail(`文件已存在:${filePath}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
writeFileSync(filePath, content);
|
|
122
|
+
ok(`已建档:${relative(root, filePath)}`);
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 落盘后目录增量自检
|
|
126
|
+
*/
|
|
127
|
+
const check = runNumberingNext({ root, config, scopeKey });
|
|
128
|
+
|
|
129
|
+
info(`编号自检:下一可用编号 ${check.ok ? check.number : '—'}(${scope.registryDir} 目录口径)`);
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 编号撞号即让号提示
|
|
133
|
+
*/
|
|
134
|
+
const duplicates = scanRegistryDir(registryAbs).length;
|
|
135
|
+
void duplicates;
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
defineCommand(program, {
|
|
140
|
+
name: 'list',
|
|
141
|
+
description: '列出注册表 issue(按 scope 分组)',
|
|
142
|
+
options: [
|
|
143
|
+
{ flags: '--scope <scope>', description: '限定 scope' },
|
|
144
|
+
{ flags: '--status <status>', description: '限定状态灯位(red/yellow/green/frozen/canceled)' },
|
|
145
|
+
{ flags: '--config <path>', description: '显式配置路径' },
|
|
146
|
+
],
|
|
147
|
+
schema: z.object({ scope: z.string().optional(), status: z.string().optional(), config: z.string().optional() }).passthrough(),
|
|
148
|
+
action: async (opts) => {
|
|
149
|
+
const root = repoRoot();
|
|
150
|
+
const { config } = await loadConfigOrExit(opts.config);
|
|
151
|
+
|
|
152
|
+
for (const scope of config.scopes) {
|
|
153
|
+
if (opts.scope !== undefined && scope.key !== opts.scope) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 注册表记录
|
|
159
|
+
*/
|
|
160
|
+
const records = scanRegistryDir(join(root, scope.registryDir));
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 状态过滤
|
|
164
|
+
*/
|
|
165
|
+
const filtered = opts.status === undefined ? records : records.filter((r) => r.status === opts.status);
|
|
166
|
+
|
|
167
|
+
process.stdout.write(`# ${scope.key}(${scope.title})— ${filtered.length} 项\n`);
|
|
168
|
+
|
|
169
|
+
for (const record of filtered) {
|
|
170
|
+
process.stdout.write(`${record.id} ${record.status.padEnd(7)} ${record.title}\n`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
defineCommand(program, {
|
|
177
|
+
name: 'show',
|
|
178
|
+
description: '展示单份 issue(ref 支持 编号 / 编号-slug / 不完整前缀;多义列候选)',
|
|
179
|
+
arguments: [{ signature: '<ref>', description: 'issue 引用' }],
|
|
180
|
+
options: [{ flags: '--config <path>', description: '显式配置路径' }],
|
|
181
|
+
schema: z.object({ ref: z.string() }).passthrough(),
|
|
182
|
+
action: async (rawOpts) => {
|
|
183
|
+
/**
|
|
184
|
+
* 位置参数(defineCommand 位置参数注入 opts)
|
|
185
|
+
*/
|
|
186
|
+
const opts = rawOpts as unknown as { ref?: string; config?: string; _: string[] };
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* ref 取值(位置参数或 ref 字段)
|
|
190
|
+
*/
|
|
191
|
+
const ref = opts.ref ?? opts._?.[0] ?? fail('缺少 issue 引用(show <ref>)');
|
|
192
|
+
|
|
193
|
+
const root = repoRoot();
|
|
194
|
+
const { config } = await loadConfigOrExit(opts.config);
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* ref 解析(编号 / 编号-slug)
|
|
198
|
+
*/
|
|
199
|
+
const match = /^(\d{3,4})(?:-([a-z0-9-]+))?$/.exec(ref);
|
|
200
|
+
|
|
201
|
+
if (match === null) {
|
|
202
|
+
fail(`引用「${ref}」不合法(编号或 编号-slug 形态)`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 全 scope 候选收集
|
|
207
|
+
*/
|
|
208
|
+
const candidates: Array<{ scopeKey: string; file: string; slug: string; title: string; record: ReturnType<typeof scanRegistryDir>[number] }> = [];
|
|
209
|
+
|
|
210
|
+
for (const scope of config.scopes) {
|
|
211
|
+
for (const record of scanRegistryDir(join(root, scope.registryDir))) {
|
|
212
|
+
if (record.id === match[1] && (match[2] === undefined || record.slug === match[2])) {
|
|
213
|
+
candidates.push({ scopeKey: scope.key, file: `${scope.registryDir}/${record.file}`, slug: record.slug, title: record.title, record });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (candidates.length === 0) {
|
|
219
|
+
/**
|
|
220
|
+
* 前缀兜底(不完整 slug 唯一命中)
|
|
221
|
+
*/
|
|
222
|
+
for (const scope of config.scopes) {
|
|
223
|
+
for (const record of scanRegistryDir(join(root, scope.registryDir))) {
|
|
224
|
+
if (record.id === match[1] && match[2] !== undefined && record.slug.startsWith(match[2])) {
|
|
225
|
+
candidates.push({ scopeKey: scope.key, file: `${scope.registryDir}/${record.file}`, slug: record.slug, title: record.title, record });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (candidates.length === 0) {
|
|
232
|
+
fail(`引用「${ref}」无命中`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (candidates.length > 1) {
|
|
236
|
+
process.stderr.write(`错误: 引用「${ref}」命中不唯一——候选:\n`);
|
|
237
|
+
for (const c of candidates) {
|
|
238
|
+
process.stderr.write(` ${c.scopeKey}/${c.record.id}-${c.slug}(${c.file})\n`);
|
|
239
|
+
}
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* 唯一命中输出全文
|
|
245
|
+
*/
|
|
246
|
+
const hit = candidates[0]!;
|
|
247
|
+
|
|
248
|
+
info(`命中:${hit.scopeKey} ${hit.file}`);
|
|
249
|
+
process.stdout.write(hit.record.rawContent);
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { Command } from '@longzai-intelligence-cli/core';
|
|
2
|
+
|
|
3
|
+
import { defineCommand } from '@longzai-intelligence-cli/core';
|
|
4
|
+
import { appendScopeToConfigText, renderConfigScaffoldFile } from '@longzai-intelligence-issues/config';
|
|
5
|
+
import { TEMPLATE_FIELDS, renderCriteriaBlockExample, renderIssueTemplateSkeleton, renderRegistryReadmeStub } from '@longzai-intelligence-issues/ledger';
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import * as z from 'zod';
|
|
9
|
+
|
|
10
|
+
import { fail, info, loadConfigOrExit, ok, repoRoot } from './context.utils.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* init / template 命令组:标准格式的「提供 + 初始化」入口
|
|
14
|
+
*/
|
|
15
|
+
export function registerScaffoldCommands(program: Command): void {
|
|
16
|
+
defineCommand(program, {
|
|
17
|
+
name: 'init',
|
|
18
|
+
description: '初始化 lzi-issues:生成/追加 lzi-issues.config.ts、创建注册表目录并落格式存根',
|
|
19
|
+
options: [
|
|
20
|
+
{ flags: '--key <key>', description: 'scope key(缺省按 package.json 名推导)' },
|
|
21
|
+
{ flags: '--title <title>', description: 'scope 人类可读名' },
|
|
22
|
+
{ flags: '--registry-dir <dir>', description: '注册表目录(缺省 docs/issues)' },
|
|
23
|
+
{ flags: '--paths <paths...>', description: '覆盖路径 glob(缺省 .)' },
|
|
24
|
+
],
|
|
25
|
+
schema: z
|
|
26
|
+
.object({
|
|
27
|
+
key: z.string().optional(),
|
|
28
|
+
title: z.string().optional(),
|
|
29
|
+
registryDir: z.string().optional(),
|
|
30
|
+
paths: z.array(z.string()).optional(),
|
|
31
|
+
})
|
|
32
|
+
.passthrough(),
|
|
33
|
+
action: async (opts) => {
|
|
34
|
+
/**
|
|
35
|
+
* 仓库根
|
|
36
|
+
*/
|
|
37
|
+
const root = repoRoot();
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 配置文件路径
|
|
41
|
+
*/
|
|
42
|
+
const configPath = join(root, 'lzi-issues.config.ts');
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* scope key 推导(显式 > package.json 名 > 目录名)
|
|
46
|
+
*/
|
|
47
|
+
const key =
|
|
48
|
+
opts.key ??
|
|
49
|
+
(existsSync(join(root, 'package.json'))
|
|
50
|
+
? (() => {
|
|
51
|
+
try {
|
|
52
|
+
/**
|
|
53
|
+
* package.json 名
|
|
54
|
+
*/
|
|
55
|
+
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { name?: string };
|
|
56
|
+
|
|
57
|
+
return (pkg.name ?? 'root').replace(/^@[^/]+\//, '').replace(/[^a-z0-9-]/g, '-');
|
|
58
|
+
} catch {
|
|
59
|
+
return 'root';
|
|
60
|
+
}
|
|
61
|
+
})()
|
|
62
|
+
: 'root');
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* scope 入参
|
|
66
|
+
*/
|
|
67
|
+
const scope = {
|
|
68
|
+
key,
|
|
69
|
+
title: opts.title ?? `${key} 域`,
|
|
70
|
+
paths: opts.paths ?? ['.'],
|
|
71
|
+
registryDir: opts.registryDir ?? 'docs/issues',
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (existsSync(configPath)) {
|
|
75
|
+
/**
|
|
76
|
+
* 追加模式(不动其他节)
|
|
77
|
+
*/
|
|
78
|
+
try {
|
|
79
|
+
const appended = appendScopeToConfigText(readFileSync(configPath, 'utf8'), scope);
|
|
80
|
+
writeFileSync(configPath, appended);
|
|
81
|
+
info(`已追加 scope「${key}」到 ${configPath}`);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
writeFileSync(configPath, renderConfigScaffoldFile(scope));
|
|
87
|
+
ok(`已生成 ${configPath}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 注册表目录 + README 存根
|
|
92
|
+
*/
|
|
93
|
+
const registryAbs = join(root, scope.registryDir);
|
|
94
|
+
const readmePath = join(registryAbs, 'README.md');
|
|
95
|
+
|
|
96
|
+
mkdirSync(registryAbs, { recursive: true });
|
|
97
|
+
|
|
98
|
+
if (!existsSync(readmePath)) {
|
|
99
|
+
writeFileSync(readmePath, renderRegistryReadmeStub(scope.title, '../../docs/guides/issue-file-format.md'));
|
|
100
|
+
ok(`已创建注册表目录 ${scope.registryDir} 并落格式存根 README.md`);
|
|
101
|
+
} else {
|
|
102
|
+
info(`注册表 README 已存在,跳过(幂等不覆盖)`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
ok('后续:lzi-issues template 查看标准模板;lzi-issues create --title <标题> 建档');
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
defineCommand(program, {
|
|
110
|
+
name: 'template',
|
|
111
|
+
description: '输出 issue 标准格式模板(--with-criteria 附判据块示例)',
|
|
112
|
+
options: [
|
|
113
|
+
{ flags: '--with-criteria', description: '附 criteria 判据块示例' },
|
|
114
|
+
{ flags: '--format <format>', description: 'text | json(json 返回字段契约)' },
|
|
115
|
+
],
|
|
116
|
+
schema: z.object({ withCriteria: z.boolean().optional(), format: z.string().optional() }).passthrough(),
|
|
117
|
+
action: async (opts) => {
|
|
118
|
+
/**
|
|
119
|
+
* 骨架全文(渲染真源产物)
|
|
120
|
+
*/
|
|
121
|
+
const skeleton = renderIssueTemplateSkeleton();
|
|
122
|
+
|
|
123
|
+
if (opts.format === 'json') {
|
|
124
|
+
process.stdout.write(`${JSON.stringify({ template: skeleton, fields: TEMPLATE_FIELDS }, null, 2)}\n`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
process.stdout.write(skeleton);
|
|
129
|
+
|
|
130
|
+
if (opts.withCriteria === true) {
|
|
131
|
+
process.stdout.write(`\n${renderCriteriaBlockExample()}`);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 保留 context.utils 的导入占位使用(loadConfigOrExit 在 init 不需要配置——生成方)
|
|
138
|
+
*/
|
|
139
|
+
void loadConfigOrExit;
|
|
140
|
+
}
|