@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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# 更新日志
|
|
2
|
+
|
|
3
|
+
本项目的所有重要更改都将记录在此文件中。
|
|
4
|
+
|
|
5
|
+
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
|
|
6
|
+
本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
7
|
+
|
|
8
|
+
## [0.0.1] - 2026-09-14
|
|
9
|
+
|
|
10
|
+
### 新增
|
|
11
|
+
|
|
12
|
+
- 新增 apps/cli(bin lzi-issues):init/template/create/list/show/lint/numbering(next|check|audit)/docs check/verify-close/freeze/unfreeze/normalize-header/health/triage/prompts 全命令面(ADR-0049 壳,退出码 0/1/2)
|
|
13
|
+
|
|
14
|
+
### 变更
|
|
15
|
+
|
|
16
|
+
- create 建档 slug 强制 ASCII kebab 校验:派生非法(如纯中文标题)即拒绝并要求显式 --slug
|
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @longzai-intelligence-issues/cli
|
|
2
|
+
|
|
3
|
+
lzi-issues 命令行应用(bin: `lzi-issues`),把 ledger 引擎与配置包的判定能力组装为可直接使用的工具面:初始化接入、标准模板提供、建档取号、门禁检查、收口冻结与巡检观测。
|
|
4
|
+
|
|
5
|
+
## 意图
|
|
6
|
+
|
|
7
|
+
让使用者在任意项目内零服务器零数据库地使用文件化 issue 注册表:一条 `init` 完成接入(配置生成/追加 + 注册表目录 + 格式存根),一条 `create` 完成建档(门禁取号 + 标准模板落盘 + 编号自检),门禁与收口命令的退出码即可作为机器判据接入合入检查。
|
|
8
|
+
|
|
9
|
+
## 定位
|
|
10
|
+
|
|
11
|
+
位于 `longzai-intelligence-issues` 项目 `apps/cli`,是应用层(ADR-0049 CLI 壳:`@longzai-intelligence-cli/core` 的 createCli + defineCommand + zod,禁直引 commander)。向下依赖 `@longzai-intelligence-issues/config`(配置加载)与 `@longzai-intelligence-issues/ledger`(全部判定内核);与 domains/issues 的存储策略装配(createIssuesStorage)互不依赖——CLI 的门禁命令直接面向注册表文件(files 形态),平台管理面走 web/API。
|
|
12
|
+
|
|
13
|
+
## 职能
|
|
14
|
+
|
|
15
|
+
命令面(退出码口径:0 通过 / 1 违规 / 2 门禁拦截零执行;违规与错误走 stderr、过程信息走 stdout):
|
|
16
|
+
|
|
17
|
+
- 接入与格式:`init`(生成或追加 lzi-issues.config.ts、创建注册表目录并落 README 格式存根,幂等)、`template`(输出标准模板全文,`--format json` 返回字段契约,`--with-criteria` 附判据块示例)。
|
|
18
|
+
- 建档与查询:`create`(写入时刻取号 → 模板渲染 → 目录缺席自创建 → 落盘 → numbering check 自检)、`list`(按 scope 分组)、`show <ref>`(编号 / 编号-slug / 不完整前缀三形态,多义列候选)。
|
|
19
|
+
- 门禁与观测:`lint`(13 规则 + 棘轮基线三态,`--write-baseline --issue` 拒绝扩基线再生)、`numbering next|check|audit`、`docs check`(文档引用检查,`--write-baseline` 再生)、`health`(JSON 巡检)、`triage`(append-only 台账)。
|
|
20
|
+
- 收口与冻结:`verify-close`(缺省零执行证据收口;`--rerun` 证书化真跑,`--confirm-heavy` / `--allow-full-criterion` 两级门禁豁免须留痕)、`freeze` / `unfreeze`(裁定语硬门)、`normalize-header`(方言迁移零丢失)、`prompts list|show`(推荐提示词)。
|
|
21
|
+
|
|
22
|
+
快速开始:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bun ./apps/cli/dist/index.js init --key root --title "项目全局"
|
|
26
|
+
bun ./apps/cli/dist/index.js template
|
|
27
|
+
bun ./apps/cli/dist/index.js create --title "<一句话标题>" --scope root
|
|
28
|
+
bun ./apps/cli/dist/index.js lint && bun ./apps/cli/dist/index.js numbering check
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 实现情况
|
|
32
|
+
|
|
33
|
+
已实现全部 15 个命令(init / template / create / list / show / lint / numbering×3 / docs check / verify-close / freeze / unfreeze / normalize-header / health / triage / prompts),构建产物经 smoke 验证(--help、template 输出、错误路径、试点全流程 init→create→双零门禁→list);lint / typecheck / build 门禁通过;建档 slug 强制 ASCII kebab(中文标题须显式 --slug)。命令级自动化测试(含 KNOWN_FLAGS 指南零漂移断言)与格式指南 §七 的旗标清单对齐后补齐。
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import"./rolldown-runtime-DCIJsbUY.js";import{createCli as e,defineCommand as t}from"@longzai-intelligence-cli/core";import{clackInteraction as n,picocolorsColor as r}from"@longzai-intelligence-cli/standard";import{TEMPLATE_FIELDS as i,evidenceCloseFlow as a,freezeIssue as o,listPrompts as s,normalizeHeaderFile as c,renderCriteriaBlockExample as l,renderIssueTemplate as u,renderIssueTemplateSkeleton as d,renderRegistryReadmeStub as f,rerunCloseFlow as p,runDocsCheck as m,runHealth as h,runLint as g,runNumberingAudit as _,runNumberingCheck as v,runNumberingNext as y,runTriage as b,runWriteDocsBaseline as x,runWriteLintBaseline as S,scanRegistryDir as C,showPrompt as w,unfreezeIssue as T}from"@longzai-intelligence-issues/ledger";import*as E from"zod";import{appendScopeToConfigText as D,loadLziIssuesConfig as O,renderConfigScaffoldFile as k}from"@longzai-intelligence-issues/config";import{join as A,relative as j,resolve as M}from"node:path";import{existsSync as N,mkdirSync as P,readFileSync as F,writeFileSync as I}from"node:fs";function L(){return M(process.cwd())}async function R(e){let t=await O(e===void 0?{}:{configPath:e});return t.ok||(process.stderr.write(`错误: ${t.reason}\n`),process.exit(1)),{config:t.config,configFilePath:t.configFilePath}}function z(e){process.stdout.write(`[INFO] ${e}\n`)}function B(e){process.stdout.write(`[SUCCESS] ${e}\n`)}function V(e){process.stdout.write(`${e}\n`)}function H(e){process.stderr.write(`错误: ${e}\n`),process.exit(1)}function U(e,t){let n=null,r=-1;for(let i of e.scopes)for(let e of i.paths)(e===`.`||e===``||t===e||t.startsWith(`${e}/`))&&e.length>r&&(n=i.key,r=e.length);return n}function W(e){t(e,{name:`verify-close`,description:`issue 收口(缺省零执行证据收口;--rerun 证书化真跑)`,options:[{flags:`--issue <issue>`,description:`issue 文件路径(可重复)`},{flags:`--summary <summary>`,description:`收口概要(批量禁用)`},{flags:`--dry-run`,description:`只呈现不落盘/不执行`},{flags:`--rerun`,description:`证书化真跑(重度命令,仅人类明令)`},{flags:`--criterion <criterion>`,description:`显式判据 label|channel|cwd|command(可重复)`},{flags:`--criteria-from-issue`,description:`从各案 criteria 块取判据`},{flags:`--confirm-heavy`,description:`重度命令确认`},{flags:`--allow-full-criterion`,description:`全量判据豁免(须留痕)`},{flags:`--config <path>`,description:`显式配置路径`}],schema:E.object({issue:E.union([E.string(),E.array(E.string())]).optional(),summary:E.string().optional(),dryRun:E.boolean().optional(),rerun:E.boolean().optional(),criterion:E.union([E.string(),E.array(E.string())]).optional(),criteriaFromIssue:E.boolean().optional(),confirmHeavy:E.boolean().optional(),allowFullCriterion:E.boolean().optional(),config:E.string().optional()}).passthrough(),action:async e=>{let t=L(),{config:n}=await R(e.config),r=e.issue===void 0?[]:Array.isArray(e.issue)?e.issue:[e.issue];r.length===0&&H(`缺少 --issue <路径>(可重复指定批量)`);let i=e.criterion===void 0?[]:Array.isArray(e.criterion)?e.criterion:[e.criterion];if(e.rerun===!0){let a=p({root:t,config:n,files:r,explicitCriteria:i,criteriaFromIssue:e.criteriaFromIssue===!0,summaryOverride:r.length===1?e.summary:void 0,dryRun:e.dryRun===!0,allowFullCriterion:e.allowFullCriterion===!0,confirmHeavy:e.confirmHeavy===!0});a.gateBlocked&&(process.stdout.write(`${a.gateMessage}\n`),process.exit(2));for(let e of a.cases)V(`${e.file}: ${e.ok?`通过`:`失败`}——${e.message}`);z(`批次收口汇总:${a.succeeded}/${a.cases.length} 案翻绿`),a.succeeded<a.cases.length&&process.exit(1);return}(i.length>0||e.criteriaFromIssue===!0||e.confirmHeavy===!0||e.allowFullCriterion===!0)&&H(`证据收口(缺省模式)不接受 --criterion/--criteria-from-issue/--confirm-heavy/--allow-full-criterion(属 --rerun 真跑旗标)`);let o=a({root:t,config:n,files:r,summaryOverride:r.length===1?e.summary:void 0,dryRun:e.dryRun===!0});for(let e of o.cases)V(`${e.file}: ${e.ok?`通过`:`失败`}——${e.message}`);z(`批次证据收口汇总:${o.succeeded}/${o.cases.length} 案翻绿(全程零命令执行)`),o.succeeded<o.cases.length&&process.exit(1)}}),t(e,{name:`freeze`,description:`冻结 issue(--reason 须含「用户裁定」)`,options:[{flags:`--issue <issue>`,description:`issue 文件路径`},{flags:`--reason <reason>`,description:`裁定理由(须含「用户裁定」)`},{flags:`--config <path>`,description:`显式配置路径`}],schema:E.object({issue:E.string().optional(),reason:E.string().optional(),config:E.string().optional()}).passthrough(),action:async e=>{let t=L();await R(e.config),(e.issue===void 0||e.reason===void 0)&&H(`freeze 须 --issue <路径> 与 --reason <裁定理由>`);let n=o({root:t,file:e.issue,reason:e.reason});n.ok||H(n.message),B(`${e.issue}: ${n.message}`)}}),t(e,{name:`unfreeze`,description:`解冻 issue(--note 须含「用户裁定」;恢复 🔴)`,options:[{flags:`--issue <issue>`,description:`issue 文件路径`},{flags:`--note <note>`,description:`解冻注记(须含「用户裁定」)`},{flags:`--config <path>`,description:`显式配置路径`}],schema:E.object({issue:E.string().optional(),note:E.string().optional(),config:E.string().optional()}).passthrough(),action:async e=>{let t=L();await R(e.config),(e.issue===void 0||e.note===void 0)&&H(`unfreeze 须 --issue <路径> 与 --note <解冻注记>`);let n=T({root:t,file:e.issue,note:e.note});n.ok||H(n.message),B(`${e.issue}: ${n.message}`)}}),t(e,{name:`normalize-header`,description:`方言头部迁移(字段规范序 / 零丢失守卫 / 幂等)`,options:[{flags:`--issue <issue>`,description:`issue 文件路径(可重复)`},{flags:`--config <path>`,description:`显式配置路径`}],schema:E.object({issue:E.union([E.string(),E.array(E.string())]).optional(),config:E.string().optional()}).passthrough(),action:async e=>{let t=L();await R(e.config);let n=e.issue===void 0?[]:Array.isArray(e.issue)?e.issue:[e.issue];n.length===0&&H(`缺少 --issue <路径>(可重复)`);let r=0;for(let e of n){let n=c({root:t,file:e});V(`${e}: ${n.outcome}(${n.message})`),r+=+(n.outcome===`failed`)}r>0&&process.exit(1)}}),t(e,{name:`prompts`,description:`推荐提示词(list / show <id>)`,arguments:[{signature:`[action]`,description:`list | show`}],options:[{flags:`--id <id>`,description:`提示词 id(show 用)`},{flags:`--config <path>`,description:`显式配置路径`}],schema:E.object({action:E.string().optional(),id:E.string().optional(),config:E.string().optional()}).passthrough(),action:async e=>{let{config:t}=await R(e.config),n={builtin:t.prompts.builtin,extraDir:t.prompts.extraDir===void 0?void 0:`${L()}/${t.prompts.extraDir}`};if((e.action??`list`)===`show`){let t=e.id??H(`show 须 --id <提示词 id>`),r=w(t,n)??H(`提示词「${t}」未命中(lzi-issues prompts list 查看全部)`);process.stdout.write(`${r.content}\n`);return}for(let e of s(n))process.stdout.write(`${e.id}[${e.source}] ${e.description}\n`)}})}function G(e){t(e,{name:`lint`,description:`注册表 lint(12 核心规则 + 棘轮基线三态)`,options:[{flags:`--dir <dir>`,description:`限定注册表目录(可重复)`},{flags:`--strict-legacy`,description:`旧格式状态行一并计违规`},{flags:`--write-baseline`,description:`再生棘轮基线(须配 --issue)`},{flags:`--issue <issue>`,description:`收编指针(write-baseline 必填)`},{flags:`--config <path>`,description:`显式配置路径`}],schema:E.object({dir:E.union([E.string(),E.array(E.string())]).optional(),strictLegacy:E.boolean().optional(),writeBaseline:E.boolean().optional(),issue:E.string().optional(),config:E.string().optional()}).passthrough(),action:async e=>{let t=L(),{config:n,configFilePath:r}=await R(e.config),i=e.dir===void 0?void 0:Array.isArray(e.dir)?e.dir:[e.dir],a=e.strictLegacy===!0?{...n,lint:{...n.lint,strictLegacy:!0}}:n;if(e.writeBaseline===!0){let n=S({root:t,config:a,issuePointer:e.issue??``});n.ok||H(n.error),I(r,n.content),B(`基线已再生(${n.entryCount} 条,收编指针 ${e.issue})——棘轮只许缩短`);return}let o=g({root:t,config:a,dirRels:i});for(let e of o.violations)V(`${e.file}:${e.line} [${e.rule}] ${e.message}`);o.violations.length>0&&(process.stderr.write(`错误: issues:lint ${o.violations.length} 项违规|存量豁免 ${o.suppressed}|基线 ${o.baselineCount} 条\n`),process.exit(1)),B(`issues:lint 通过(0 新增违规|存量豁免 ${o.suppressed}|基线 ${o.baselineCount} 条)`)}});let n=E.object({scope:E.string().optional(),dir:E.string().optional(),config:E.string().optional()}).passthrough(),r=e.command(`numbering`).description(`编号门禁(next 取号 / check 判定 / audit 覆盖面审计)`);t(r,{name:`next`,description:`取号(--scope <key> 或 --dir <目录> 互斥;只读不写)`,options:[{flags:`--scope <scope>`,description:`目标 scope key`},{flags:`--dir <dir>`,description:`目标注册表目录`},{flags:`--config <path>`,description:`显式配置路径`}],schema:n,action:async e=>{let t=L(),{config:n}=await R(e.config),r=y({root:t,config:n,scopeKey:e.scope,dirRel:e.dir});r.ok||H(r.error),z(`${r.target} 下一可用编号:${r.number}(已跳过保留号与已占用号)`)}}),t(r,{name:`check`,description:`编号门禁判定(缺省全量;--dir 目录增量)`,options:[{flags:`--dir <dir>`,description:`目录增量档`},{flags:`--config <path>`,description:`显式配置路径`}],schema:n,action:async e=>{let t=L(),{config:n}=await R(e.config),r=v({root:t,config:n,dirRel:e.dir});for(let e of r.findings)V(`[${e.kind}] ${e.message}`);r.findings.length>0&&(process.stderr.write(`错误: numbering:check ${r.findings.length} 项违规(scope ${r.scopeCount} 个 / 扫描面 ${r.scannedFiles} 文件)\n`),process.exit(1)),B(`numbering:check 通过(scope ${r.scopeCount} 个 / 扫描面 ${r.scannedFiles} 文件 / 零违规)`)}}),t(r,{name:`audit`,description:`覆盖面审计(未登记编号区 / pending 失效)`,options:[{flags:`--config <path>`,description:`显式配置路径`}],schema:n,action:async e=>{let t=L(),{config:n}=await R(e.config),r=_({root:t,config:n});for(let e of r.findings)V(`[${e.kind}] ${e.message}`);r.findings.length>0&&(process.stderr.write(`错误: numbering:audit ${r.findings.length} 项违规(扫描面 ${r.scannedFiles} 文件)\n`),process.exit(1)),B(`numbering:audit 通过(扫描面 ${r.scannedFiles} 文件 / 零未登记编号区)`)}});let i=e.command(`docs`).description(`文档 issue 引用检查与机械修复`);t(i,{name:`check`,description:`文档 issue 引用检查(不完整 slug 前缀唯一解析 / 棘轮基线)`,options:[{flags:`--write-baseline`,description:`再生 docs 基线(须配 --issue)`},{flags:`--issue <issue>`,description:`收编指针`},{flags:`--config <path>`,description:`显式配置路径`}],schema:E.object({writeBaseline:E.boolean().optional(),issue:E.string().optional(),config:E.string().optional()}).passthrough(),action:async e=>{let t=L(),{config:n,configFilePath:r}=await R(e.config);if(e.writeBaseline===!0)try{let i=x({root:t,config:n,issuePointer:e.issue??``});i.ok||H(i.error),I(r,i.content),B(`docs 基线已再生(${i.entryCount} 条)`);return}catch(e){H(e instanceof Error?e.message:String(e))}try{let e=m({root:t,config:n});for(let t of e.violations)V(`${t.file}:${t.line} [${t.rule}] ${t.message}`);e.violations.length>0&&(process.stderr.write(`错误: docs:check ${e.violations.length} 项违规(扫描面 ${e.scannedFiles} 文件 / 豁免 ${e.suppressed})\n`),process.exit(1)),B(`docs:check 通过(扫描面 ${e.scannedFiles} 文件 / 零违规)`)}catch(e){H(e instanceof Error?e.message:String(e))}}}),t(e,{name:`health`,description:`黄灯巡检(状态分布 / 最老账龄 / A-B-C 分类,JSON)`,options:[{flags:`--config <path>`,description:`显式配置路径`}],schema:E.object({config:E.string().optional()}).passthrough(),action:async e=>{let t=L(),{config:n}=await R(e.config);process.stdout.write(`${JSON.stringify(h({root:t,config:n}),null,2)}\n`)}}),t(e,{name:`triage`,description:`黄灯分诊台账(append-only 落 .lzi/issues/runs/ledger/)`,options:[{flags:`--config <path>`,description:`显式配置路径`}],schema:E.object({config:E.string().optional()}).passthrough(),action:async e=>{let t=L(),{config:n}=await R(e.config),{ledgerPath:r,ledger:i}=b({root:t,config:n});B(`issues:triage 台账已落盘: ${r}(黄灯 ${i.items.length} 项)`)}})}function K(e){t(e,{name:`create`,description:`建档(门禁取号 → 标准模板落盘 → 编号自检)`,options:[{flags:`--title <title>`,description:`一句话标题(必填)`},{flags:`--slug <slug>`,description:`slug(缺省由标题派生)`},{flags:`--priority <priority>`,description:`P1 | P2 | P3(缺省 P2)`},{flags:`--type <type>`,description:`议题类型(缺省 task)`},{flags:`--scope <scope>`,description:`scope key(缺省按 cwd 解析)`},{flags:`--source <source>`,description:`来源说明`},{flags:`--dry-run`,description:`只呈现不落盘`},{flags:`--config <path>`,description:`显式配置路径`}],schema:E.object({title:E.string(),slug:E.string().optional(),priority:E.string().optional(),type:E.string().optional(),scope:E.string().optional(),source:E.string().optional(),dryRun:E.boolean().optional(),config:E.string().optional()}).passthrough(),action:async e=>{let t=L(),{config:n}=await R(e.config),r=j(t,process.cwd()),i=e.scope??U(n,r)??H(`cwd「${r}」未命中任何 scope 覆盖面——候选:${n.scopes.map(e=>e.key).join(`、`)}(或显式 --scope)`),a=n.scopes.find(e=>e.key===i)??H(`scope「${i}」未登记`),o=y({root:t,config:n,scopeKey:i});o.ok||H(o.error);let s=e.slug??e.title.toLowerCase().replace(/[\s_]+/g,`-`).replace(/[^a-z0-9-]/g,``).replace(/-+/g,`-`).replace(/^-|-$/g,``),c=/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(s)?s:H(`slug 须为纯 ASCII kebab(小写字母/数字/连字符,禁中文)——中文标题请显式提供 --slug <slug>`),l=u({number:o.ok?o.number:`0001`,slug:c,title:e.title,priority:e.priority===`P1`||e.priority===`P3`?e.priority:`P2`,scopeTitle:a.title,date:new Date().toISOString().slice(0,10),source:e.source??`CLI 建档`}),d=A(t,a.registryDir),f=A(d,`${o.ok?o.number:`0001`}-${c}.md`);if(e.dryRun===!0){z(`(dry-run)将落盘:${j(t,f)}`),process.stdout.write(l);return}P(d,{recursive:!0}),N(f)&&H(`文件已存在:${f}`),I(f,l),B(`已建档:${j(t,f)}`);let p=y({root:t,config:n,scopeKey:i});z(`编号自检:下一可用编号 ${p.ok?p.number:`—`}(${a.registryDir} 目录口径)`),C(d).length}}),t(e,{name:`list`,description:`列出注册表 issue(按 scope 分组)`,options:[{flags:`--scope <scope>`,description:`限定 scope`},{flags:`--status <status>`,description:`限定状态灯位(red/yellow/green/frozen/canceled)`},{flags:`--config <path>`,description:`显式配置路径`}],schema:E.object({scope:E.string().optional(),status:E.string().optional(),config:E.string().optional()}).passthrough(),action:async e=>{let t=L(),{config:n}=await R(e.config);for(let r of n.scopes){if(e.scope!==void 0&&r.key!==e.scope)continue;let n=C(A(t,r.registryDir)),i=e.status===void 0?n:n.filter(t=>t.status===e.status);process.stdout.write(`# ${r.key}(${r.title})— ${i.length} 项\n`);for(let e of i)process.stdout.write(`${e.id} ${e.status.padEnd(7)} ${e.title}\n`)}}}),t(e,{name:`show`,description:`展示单份 issue(ref 支持 编号 / 编号-slug / 不完整前缀;多义列候选)`,arguments:[{signature:`<ref>`,description:`issue 引用`}],options:[{flags:`--config <path>`,description:`显式配置路径`}],schema:E.object({ref:E.string()}).passthrough(),action:async e=>{let t=e,n=t.ref??t._?.[0]??H(`缺少 issue 引用(show <ref>)`),r=L(),{config:i}=await R(t.config),a=/^(\d{3,4})(?:-([a-z0-9-]+))?$/.exec(n);a===null&&H(`引用「${n}」不合法(编号或 编号-slug 形态)`);let o=[];for(let e of i.scopes)for(let t of C(A(r,e.registryDir)))t.id===a[1]&&(a[2]===void 0||t.slug===a[2])&&o.push({scopeKey:e.key,file:`${e.registryDir}/${t.file}`,slug:t.slug,title:t.title,record:t});if(o.length===0)for(let e of i.scopes)for(let t of C(A(r,e.registryDir)))t.id===a[1]&&a[2]!==void 0&&t.slug.startsWith(a[2])&&o.push({scopeKey:e.key,file:`${e.registryDir}/${t.file}`,slug:t.slug,title:t.title,record:t});if(o.length===0&&H(`引用「${n}」无命中`),o.length>1){process.stderr.write(`错误: 引用「${n}」命中不唯一——候选:\n`);for(let e of o)process.stderr.write(` ${e.scopeKey}/${e.record.id}-${e.slug}(${e.file})\n`);process.exit(1)}let s=o[0];z(`命中:${s.scopeKey} ${s.file}`),process.stdout.write(s.record.rawContent)}})}function q(e){t(e,{name:`init`,description:`初始化 lzi-issues:生成/追加 lzi-issues.config.ts、创建注册表目录并落格式存根`,options:[{flags:`--key <key>`,description:`scope key(缺省按 package.json 名推导)`},{flags:`--title <title>`,description:`scope 人类可读名`},{flags:`--registry-dir <dir>`,description:`注册表目录(缺省 docs/issues)`},{flags:`--paths <paths...>`,description:`覆盖路径 glob(缺省 .)`}],schema:E.object({key:E.string().optional(),title:E.string().optional(),registryDir:E.string().optional(),paths:E.array(E.string()).optional()}).passthrough(),action:async e=>{let t=L(),n=A(t,`lzi-issues.config.ts`),r=e.key??(N(A(t,`package.json`))?(()=>{try{return(JSON.parse(F(A(t,`package.json`),`utf8`)).name??`root`).replace(/^@[^/]+\//,``).replace(/[^a-z0-9-]/g,`-`)}catch{return`root`}})():`root`),i={key:r,title:e.title??`${r} 域`,paths:e.paths??[`.`],registryDir:e.registryDir??`docs/issues`};if(N(n))try{let e=D(F(n,`utf8`),i);I(n,e),z(`已追加 scope「${r}」到 ${n}`)}catch(e){H(e instanceof Error?e.message:String(e))}else I(n,k(i)),B(`已生成 ${n}`);let a=A(t,i.registryDir),o=A(a,`README.md`);P(a,{recursive:!0}),N(o)?z(`注册表 README 已存在,跳过(幂等不覆盖)`):(I(o,f(i.title,`../../docs/guides/issue-file-format.md`)),B(`已创建注册表目录 ${i.registryDir} 并落格式存根 README.md`)),B(`后续:lzi-issues template 查看标准模板;lzi-issues create --title <标题> 建档`)}}),t(e,{name:`template`,description:`输出 issue 标准格式模板(--with-criteria 附判据块示例)`,options:[{flags:`--with-criteria`,description:`附 criteria 判据块示例`},{flags:`--format <format>`,description:`text | json(json 返回字段契约)`}],schema:E.object({withCriteria:E.boolean().optional(),format:E.string().optional()}).passthrough(),action:async e=>{let t=d();if(e.format===`json`){process.stdout.write(`${JSON.stringify({template:t,fields:i},null,2)}\n`);return}process.stdout.write(t),e.withCriteria===!0&&process.stdout.write(`\n${l()}`)}})}function J(e){q(e),K(e),G(e),W(e)}async function Y(){let t=e({name:`lzi-issues`,description:`Longzai Intelligence Issues CLI(文件化注册表 · Local First)`,interaction:n,color:r});J(t),await t.parseAsync(process.argv)}await Y();export{};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineConfig } from '@longzai-intelligence-builder/esm';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI 应用构建配置:node 平台入口(bin 由 package.json 声明,shebang 注入)
|
|
5
|
+
*/
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
entry: {
|
|
8
|
+
index: 'src/index.ts',
|
|
9
|
+
},
|
|
10
|
+
platform: 'node',
|
|
11
|
+
banner: '#!/usr/bin/env bun',
|
|
12
|
+
});
|
package/oxlint.config.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@longzai-intelligence-issues/cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "UNLICENSED",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"lzi-issues": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "lzi-builder",
|
|
19
|
+
"build:prod": "NODE_ENV=production bun run build",
|
|
20
|
+
"prepublishOnly": "bun run build:prod",
|
|
21
|
+
"typecheck": "bun run typecheck:app && bun run typecheck:node && bun run typecheck:test",
|
|
22
|
+
"typecheck:app": "lzi-tsgo typecheck tsconfig/app.json",
|
|
23
|
+
"typecheck:node": "lzi-tsgo typecheck tsconfig/node.json",
|
|
24
|
+
"typecheck:test": "lzi-tsgo typecheck tsconfig/test.json",
|
|
25
|
+
"lint": "oxlint && oxfmt --check",
|
|
26
|
+
"lint:fix": "oxlint --fix && oxfmt",
|
|
27
|
+
"test": "lzi-bun-cli test",
|
|
28
|
+
"test:watch": "lzi-bun-cli test --watch",
|
|
29
|
+
"test:coverage": "lzi-bun-cli test --coverage",
|
|
30
|
+
"clean": "lzi-dev-cli clean"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@longzai-intelligence-issues/config": "0.0.1",
|
|
34
|
+
"@longzai-intelligence-issues/ledger": "0.0.1",
|
|
35
|
+
"@longzai-intelligence-cli/core": "^0.0.8",
|
|
36
|
+
"zod": "^4.4.3",
|
|
37
|
+
"@longzai-intelligence-cli/standard": "^0.0.8"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@longzai-intelligence-builder/cli": "^0.2.16",
|
|
41
|
+
"@longzai-intelligence-tsgo/cli": "^0.0.1",
|
|
42
|
+
"@types/bun": "latest",
|
|
43
|
+
"@typescript/native-preview": "beta",
|
|
44
|
+
"oxlint": "^1.77.0"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import type { Command } from '@longzai-intelligence-cli/core';
|
|
2
|
+
|
|
3
|
+
import { defineCommand } from '@longzai-intelligence-cli/core';
|
|
4
|
+
import {
|
|
5
|
+
evidenceCloseFlow,
|
|
6
|
+
freezeIssue,
|
|
7
|
+
listPrompts,
|
|
8
|
+
normalizeHeaderFile,
|
|
9
|
+
rerunCloseFlow,
|
|
10
|
+
showPrompt,
|
|
11
|
+
unfreezeIssue,
|
|
12
|
+
} from '@longzai-intelligence-issues/ledger';
|
|
13
|
+
import * as z from 'zod';
|
|
14
|
+
|
|
15
|
+
import { fail, info, loadConfigOrExit, ok, repoRoot, violation } from './context.utils.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 收口与冻结命令组:verify-close / freeze / unfreeze / normalize-header / prompts
|
|
19
|
+
*
|
|
20
|
+
* 判据口径:exit 0 过 / 1 违规 / 2 门禁拦截零执行。
|
|
21
|
+
*/
|
|
22
|
+
export function registerCloseCommands(program: Command): void {
|
|
23
|
+
defineCommand(program, {
|
|
24
|
+
name: 'verify-close',
|
|
25
|
+
description: 'issue 收口(缺省零执行证据收口;--rerun 证书化真跑)',
|
|
26
|
+
options: [
|
|
27
|
+
{ flags: '--issue <issue>', description: 'issue 文件路径(可重复)' },
|
|
28
|
+
{ flags: '--summary <summary>', description: '收口概要(批量禁用)' },
|
|
29
|
+
{ flags: '--dry-run', description: '只呈现不落盘/不执行' },
|
|
30
|
+
{ flags: '--rerun', description: '证书化真跑(重度命令,仅人类明令)' },
|
|
31
|
+
{ flags: '--criterion <criterion>', description: '显式判据 label|channel|cwd|command(可重复)' },
|
|
32
|
+
{ flags: '--criteria-from-issue', description: '从各案 criteria 块取判据' },
|
|
33
|
+
{ flags: '--confirm-heavy', description: '重度命令确认' },
|
|
34
|
+
{ flags: '--allow-full-criterion', description: '全量判据豁免(须留痕)' },
|
|
35
|
+
{ flags: '--config <path>', description: '显式配置路径' },
|
|
36
|
+
],
|
|
37
|
+
schema: z
|
|
38
|
+
.object({
|
|
39
|
+
issue: z.union([z.string(), z.array(z.string())]).optional(),
|
|
40
|
+
summary: z.string().optional(),
|
|
41
|
+
dryRun: z.boolean().optional(),
|
|
42
|
+
rerun: z.boolean().optional(),
|
|
43
|
+
criterion: z.union([z.string(), z.array(z.string())]).optional(),
|
|
44
|
+
criteriaFromIssue: z.boolean().optional(),
|
|
45
|
+
confirmHeavy: z.boolean().optional(),
|
|
46
|
+
allowFullCriterion: z.boolean().optional(),
|
|
47
|
+
config: z.string().optional(),
|
|
48
|
+
})
|
|
49
|
+
.passthrough(),
|
|
50
|
+
action: async (opts) => {
|
|
51
|
+
const root = repoRoot();
|
|
52
|
+
const { config } = await loadConfigOrExit(opts.config);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 目标文件归一
|
|
56
|
+
*/
|
|
57
|
+
const files = opts.issue === undefined ? [] : Array.isArray(opts.issue) ? opts.issue : [opts.issue];
|
|
58
|
+
|
|
59
|
+
if (files.length === 0) {
|
|
60
|
+
fail('缺少 --issue <路径>(可重复指定批量)');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 显式判据归一
|
|
65
|
+
*/
|
|
66
|
+
const criteria = opts.criterion === undefined ? [] : Array.isArray(opts.criterion) ? opts.criterion : [opts.criterion];
|
|
67
|
+
|
|
68
|
+
if (opts.rerun === true) {
|
|
69
|
+
/**
|
|
70
|
+
* 证书化真跑
|
|
71
|
+
*/
|
|
72
|
+
const result = rerunCloseFlow({
|
|
73
|
+
root,
|
|
74
|
+
config,
|
|
75
|
+
files,
|
|
76
|
+
explicitCriteria: criteria,
|
|
77
|
+
criteriaFromIssue: opts.criteriaFromIssue === true,
|
|
78
|
+
summaryOverride: files.length === 1 ? opts.summary : undefined,
|
|
79
|
+
dryRun: opts.dryRun === true,
|
|
80
|
+
allowFullCriterion: opts.allowFullCriterion === true,
|
|
81
|
+
confirmHeavy: opts.confirmHeavy === true,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (result.gateBlocked) {
|
|
85
|
+
process.stdout.write(`${result.gateMessage}\n`);
|
|
86
|
+
process.exit(2);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const entry of result.cases) {
|
|
90
|
+
violation(`${entry.file}: ${entry.ok ? '通过' : '失败'}——${entry.message}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
info(`批次收口汇总:${result.succeeded}/${result.cases.length} 案翻绿`);
|
|
94
|
+
|
|
95
|
+
if (result.succeeded < result.cases.length) {
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 缺省证据收口(零执行)
|
|
104
|
+
*/
|
|
105
|
+
if (criteria.length > 0 || opts.criteriaFromIssue === true || opts.confirmHeavy === true || opts.allowFullCriterion === true) {
|
|
106
|
+
fail('证据收口(缺省模式)不接受 --criterion/--criteria-from-issue/--confirm-heavy/--allow-full-criterion(属 --rerun 真跑旗标)');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const result = evidenceCloseFlow({
|
|
110
|
+
root,
|
|
111
|
+
config,
|
|
112
|
+
files,
|
|
113
|
+
summaryOverride: files.length === 1 ? opts.summary : undefined,
|
|
114
|
+
dryRun: opts.dryRun === true,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
for (const entry of result.cases) {
|
|
118
|
+
violation(`${entry.file}: ${entry.ok ? '通过' : '失败'}——${entry.message}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
info(`批次证据收口汇总:${result.succeeded}/${result.cases.length} 案翻绿(全程零命令执行)`);
|
|
122
|
+
|
|
123
|
+
if (result.succeeded < result.cases.length) {
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
defineCommand(program, {
|
|
130
|
+
name: 'freeze',
|
|
131
|
+
description: '冻结 issue(--reason 须含「用户裁定」)',
|
|
132
|
+
options: [
|
|
133
|
+
{ flags: '--issue <issue>', description: 'issue 文件路径' },
|
|
134
|
+
{ flags: '--reason <reason>', description: '裁定理由(须含「用户裁定」)' },
|
|
135
|
+
{ flags: '--config <path>', description: '显式配置路径' },
|
|
136
|
+
],
|
|
137
|
+
schema: z.object({ issue: z.string().optional(), reason: z.string().optional(), config: z.string().optional() }).passthrough(),
|
|
138
|
+
action: async (opts) => {
|
|
139
|
+
const root = repoRoot();
|
|
140
|
+
await loadConfigOrExit(opts.config);
|
|
141
|
+
|
|
142
|
+
if (opts.issue === undefined || opts.reason === undefined) {
|
|
143
|
+
fail('freeze 须 --issue <路径> 与 --reason <裁定理由>');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const result = freezeIssue({ root, file: opts.issue, reason: opts.reason });
|
|
147
|
+
|
|
148
|
+
if (!result.ok) {
|
|
149
|
+
fail(result.message);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
ok(`${opts.issue}: ${result.message}`);
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
defineCommand(program, {
|
|
157
|
+
name: 'unfreeze',
|
|
158
|
+
description: '解冻 issue(--note 须含「用户裁定」;恢复 🔴)',
|
|
159
|
+
options: [
|
|
160
|
+
{ flags: '--issue <issue>', description: 'issue 文件路径' },
|
|
161
|
+
{ flags: '--note <note>', description: '解冻注记(须含「用户裁定」)' },
|
|
162
|
+
{ flags: '--config <path>', description: '显式配置路径' },
|
|
163
|
+
],
|
|
164
|
+
schema: z.object({ issue: z.string().optional(), note: z.string().optional(), config: z.string().optional() }).passthrough(),
|
|
165
|
+
action: async (opts) => {
|
|
166
|
+
const root = repoRoot();
|
|
167
|
+
await loadConfigOrExit(opts.config);
|
|
168
|
+
|
|
169
|
+
if (opts.issue === undefined || opts.note === undefined) {
|
|
170
|
+
fail('unfreeze 须 --issue <路径> 与 --note <解冻注记>');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const result = unfreezeIssue({ root, file: opts.issue, note: opts.note });
|
|
174
|
+
|
|
175
|
+
if (!result.ok) {
|
|
176
|
+
fail(result.message);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
ok(`${opts.issue}: ${result.message}`);
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
defineCommand(program, {
|
|
184
|
+
name: 'normalize-header',
|
|
185
|
+
description: '方言头部迁移(字段规范序 / 零丢失守卫 / 幂等)',
|
|
186
|
+
options: [
|
|
187
|
+
{ flags: '--issue <issue>', description: 'issue 文件路径(可重复)' },
|
|
188
|
+
{ flags: '--config <path>', description: '显式配置路径' },
|
|
189
|
+
],
|
|
190
|
+
schema: z.object({ issue: z.union([z.string(), z.array(z.string())]).optional(), config: z.string().optional() }).passthrough(),
|
|
191
|
+
action: async (opts) => {
|
|
192
|
+
const root = repoRoot();
|
|
193
|
+
await loadConfigOrExit(opts.config);
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 目标文件归一
|
|
197
|
+
*/
|
|
198
|
+
const files = opts.issue === undefined ? [] : Array.isArray(opts.issue) ? opts.issue : [opts.issue];
|
|
199
|
+
|
|
200
|
+
if (files.length === 0) {
|
|
201
|
+
fail('缺少 --issue <路径>(可重复)');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 失败计数
|
|
206
|
+
*/
|
|
207
|
+
let failed = 0;
|
|
208
|
+
|
|
209
|
+
for (const file of files) {
|
|
210
|
+
const result = normalizeHeaderFile({ root, file });
|
|
211
|
+
violation(`${file}: ${result.outcome}(${result.message})`);
|
|
212
|
+
failed += result.outcome === 'failed' ? 1 : 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (failed > 0) {
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
defineCommand(program, {
|
|
222
|
+
name: 'prompts',
|
|
223
|
+
description: '推荐提示词(list / show <id>)',
|
|
224
|
+
arguments: [{ signature: '[action]', description: 'list | show' }],
|
|
225
|
+
options: [
|
|
226
|
+
{ flags: '--id <id>', description: '提示词 id(show 用)' },
|
|
227
|
+
{ flags: '--config <path>', description: '显式配置路径' },
|
|
228
|
+
],
|
|
229
|
+
schema: z.object({ action: z.string().optional(), id: z.string().optional(), config: z.string().optional() }).passthrough(),
|
|
230
|
+
action: async (opts) => {
|
|
231
|
+
const { config } = await loadConfigOrExit(opts.config);
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 提示词选项(builtin 开关 + extraDir)
|
|
235
|
+
*/
|
|
236
|
+
const options = {
|
|
237
|
+
builtin: config.prompts.builtin,
|
|
238
|
+
extraDir: config.prompts.extraDir === undefined ? undefined : `${repoRoot()}/${config.prompts.extraDir}`,
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const action = opts.action ?? 'list';
|
|
242
|
+
|
|
243
|
+
if (action === 'show') {
|
|
244
|
+
const id = opts.id ?? fail('show 须 --id <提示词 id>');
|
|
245
|
+
|
|
246
|
+
const entry = showPrompt(id, options) ?? fail(`提示词「${id}」未命中(lzi-issues prompts list 查看全部)`);
|
|
247
|
+
|
|
248
|
+
process.stdout.write(`${entry.content}\n`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const entry of listPrompts(options)) {
|
|
253
|
+
process.stdout.write(`${entry.id}[${entry.source}] ${entry.description}\n`);
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 命令公共上下文:配置加载、根锚定与判据输出
|
|
3
|
+
*
|
|
4
|
+
* 判据口径(apex 迁移):违规与错误走 stderr(`错误: ` 前缀)+ exit 1;过程
|
|
5
|
+
* 信息走 stdout([INFO]/[SUCCESS] 前缀);exit 2 = 门禁拦截零执行。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { loadLziIssuesConfig, type LoadLziIssuesConfigResult, type LziIssuesConfig } from '@longzai-intelligence-issues/config';
|
|
9
|
+
|
|
10
|
+
import { resolve } from 'node:path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 仓库根绝对路径(cwd 锚定;配置查找亦自 cwd 向上)
|
|
14
|
+
*/
|
|
15
|
+
export function repoRoot(): string {
|
|
16
|
+
return resolve(process.cwd());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 加载配置(失败即错误退出——引擎零仓库知识,无配置不可运行)
|
|
21
|
+
*
|
|
22
|
+
* @param configPath - 显式配置路径(--config)
|
|
23
|
+
* @returns 解析后配置与命中路径
|
|
24
|
+
*/
|
|
25
|
+
export async function loadConfigOrExit(configPath?: string): Promise<{ config: LziIssuesConfig; configFilePath: string }> {
|
|
26
|
+
/**
|
|
27
|
+
* 加载结果
|
|
28
|
+
*/
|
|
29
|
+
const result: LoadLziIssuesConfigResult = await loadLziIssuesConfig(
|
|
30
|
+
configPath === undefined ? {} : { configPath },
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
if (!result.ok) {
|
|
34
|
+
process.stderr.write(`错误: ${result.reason}\n`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { config: result.config, configFilePath: result.configFilePath };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 输出 [INFO] 过程信息
|
|
43
|
+
*
|
|
44
|
+
* @param message - 消息
|
|
45
|
+
*/
|
|
46
|
+
export function info(message: string): void {
|
|
47
|
+
process.stdout.write(`[INFO] ${message}\n`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 输出 [SUCCESS] 结果信息
|
|
52
|
+
*
|
|
53
|
+
* @param message - 消息
|
|
54
|
+
*/
|
|
55
|
+
export function ok(message: string): void {
|
|
56
|
+
process.stdout.write(`[SUCCESS] ${message}\n`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 输出违规(`文件:行 [规则] 消息` 或裸消息)
|
|
61
|
+
*
|
|
62
|
+
* @param line - 违规行文本
|
|
63
|
+
*/
|
|
64
|
+
export function violation(line: string): void {
|
|
65
|
+
process.stdout.write(`${line}\n`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 输出错误并 exit 1
|
|
70
|
+
*
|
|
71
|
+
* @param message - 错误消息
|
|
72
|
+
*/
|
|
73
|
+
export function fail(message: string): never {
|
|
74
|
+
process.stderr.write(`错误: ${message}\n`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* cwd → scope 解析(最长 paths 前缀优先;失败列全部 scope)
|
|
80
|
+
*
|
|
81
|
+
* @param config - 解析后配置
|
|
82
|
+
* @param cwdRel - cwd 相对根路径
|
|
83
|
+
* @returns 命中 scope key;未命中为 null
|
|
84
|
+
*/
|
|
85
|
+
export function resolveScopeFromCwd(config: LziIssuesConfig, cwdRel: string): string | null {
|
|
86
|
+
/**
|
|
87
|
+
* 候选(path 命中 cwd 的 scope,取最长 path)
|
|
88
|
+
*/
|
|
89
|
+
let bestKey: string | null = null;
|
|
90
|
+
|
|
91
|
+
let bestLen = -1;
|
|
92
|
+
|
|
93
|
+
for (const scope of config.scopes) {
|
|
94
|
+
for (const path of scope.paths) {
|
|
95
|
+
/**
|
|
96
|
+
* 前缀命中判定('.' 视为全仓命中)
|
|
97
|
+
*/
|
|
98
|
+
const hit = path === '.' || path === '' || cwdRel === path || cwdRel.startsWith(`${path}/`);
|
|
99
|
+
|
|
100
|
+
if (hit && path.length > bestLen) {
|
|
101
|
+
bestKey = scope.key;
|
|
102
|
+
bestLen = path.length;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return bestKey;
|
|
108
|
+
}
|