dev-change-report 0.0.0-stage → 1.0.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/README.md CHANGED
@@ -1,3 +1,142 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # dev-change-report
2
+
3
+ 把一次代码改动整理成**可直接粘贴的富文本 HTML**,用于 OA / CKEditor / 飞书 / Word 的开发说明文档。
4
+
5
+ - 片段里只写语义标签,样式由命令统一注入,改观感只改一处
6
+ - 代码块自动从**真实文件按行截取**,不用手抄、不用转义
7
+ - 输出单个 `report.html`,编辑器源码模式粘贴即成型,无需上传截图
8
+ - 零第三方依赖,Node 18+,Windows / macOS / Linux 通用
9
+
10
+ ## 安装
11
+
12
+ **首选 npm 包**(10 KB)。前端同事基本都有 Node,这是最省事的方式。
13
+
14
+ ```bash
15
+ npm i -D dev-change-report # 私有 registry
16
+ npm i -D ./dev-change-report-1.0.0.tgz # 本地 tarball
17
+ npx dcr install # 装到 ~/.agents/skills/,所有项目可用
18
+ npx dcr install --project # 装到 ./.agents/skills/,跟着仓库走
19
+ ```
20
+
21
+ 装完**重启编辑器**,对 AI 说「整理一下这次改动」即可触发。
22
+
23
+ ### 备选:单文件 exe(对方机器没装 Node 时才需要)
24
+
25
+ `npm run exe` 会生成 `dist/dcr.exe`(83 MB),打包后约 33 MB。
26
+ 体积来自内嵌的 Node 运行时,SEA 不支持裁剪,无法再小。
27
+
28
+ 对方解压 `dcr-win-x64.zip` 后双击 `install.cmd` 即可(自动复制到
29
+ `%LOCALAPPDATA%\Programs\dcr`、加 PATH、注册 skill)。首次运行可能有
30
+ SmartScreen 提示,点「更多信息」→「仍要运行」。
31
+
32
+ > 83 MB 换来的只是「不用装 Node」。如果对方有 Node,用 npm 包,体积差约 9000 倍。
33
+
34
+
35
+ ## 使用
36
+
37
+ ### 1. 收集改动
38
+
39
+ ```bash
40
+ npx dcr changes --repo . --base origin/release
41
+ ```
42
+
43
+ 输出分支、HEAD、文件清单与增删行数,并列出工作区未提交改动。
44
+ 加 `--json changes.json` 可导出结构化清单。
45
+
46
+ ### 2. 写片段
47
+
48
+ 新建 `report.fragment.html`,只写标签,不写样式:
49
+
50
+ ```html
51
+ <h2>一、选择柜组件优化</h2>
52
+
53
+ <h3>1.1 默认搜索条件与 PC 对齐</h3>
54
+ <p>原问题:默认按实际柜号搜索且发货日期写死当天,柜的发货时间普遍早于当天,必然空结果。</p>
55
+ <pre data-code-file="src/views/qbcrm/blRelease/Form.vue" data-code-range="56-79"></pre>
56
+ <p tip="info">改后效果:打开即有数据,输入提示随字段联动。</p>
57
+ ```
58
+
59
+ | 写法 | 用途 |
60
+ |---|---|
61
+ | `<h2>` / `<h3>` | 一级章节 / 二级标题 |
62
+ | `<p>` | 正文 |
63
+ | `<p tip="info\|warn\|danger">` | 提示块(绿 / 黄 / 红) |
64
+ | `<table><tr><th>…<td>…` | 表格,不要写 border 或 style |
65
+ | `<ul><li>` | 列表 |
66
+ | `<code>` | 行内代码 |
67
+ | `<pre data-code-file="…" data-code-range="…">` | 从文件按行截取(推荐) |
68
+ | `<pre data-code-lang="text">…</pre>` | 内嵌代码,需转义 `<` `>` `&` |
69
+
70
+ `data-code-range` 是 1-based 闭区间(如 `56-79`),省略则取全文。
71
+
72
+ ### 3. 生成
73
+
74
+ ```bash
75
+ npx dcr build --input report.fragment.html --repo-root .
76
+ ```
77
+
78
+ 产物 `change-report/report.html`。
79
+
80
+ ### 4. 粘贴
81
+
82
+ 打开 `report.html`,全选复制 → 编辑器切到「源码」模式 → 粘贴 → 切回可视化。
83
+
84
+ ## 调整样式
85
+
86
+ 打开 `lib/build.js`,顶部的 `STYLE` 与 `TIP` 两个字典控制全部观感:
87
+
88
+ ```js
89
+ const STYLE = {
90
+ h2: 'font-size:19px;font-weight:600;margin:26px 0 12px;...',
91
+ pre: 'background:#f6f8fa;border:1px solid #e5e8eb;...',
92
+ // ...
93
+ };
94
+ const TIP = { info: ['#f2f7f4', '#0a7a50'], warn: [...], danger: [...] };
95
+ ```
96
+
97
+ ## 命令总览
98
+
99
+ ```
100
+ dcr install [--project] [--force] 安装 skill 到 .agents/skills/
101
+ dcr build --input <fragment.html> 生成 report.html
102
+ [--outdir ./change-report]
103
+ [--repo-root .]
104
+ dcr changes --repo <路径> [--repo …] 收集 git 改动清单
105
+ [--base origin/release]
106
+ [--json changes.json]
107
+ dcr --version 查看版本
108
+ dcr help 查看帮助
109
+ ```
110
+
111
+ ## 自行构建 exe
112
+
113
+ 只在确实需要「无 Node 环境」分发时才构建。需要 Node 20+(含 SEA 支持),
114
+ `tools/` 下的脚本零第三方依赖,只有注入环节需要 `postject`。
115
+
116
+ ```bash
117
+ npm i -D postject
118
+ npm run bundle # lib/ + bin/ 合并成 dist/dcr-bundle.js(21 KB)
119
+ npm run sea:blob # 生成 dist/sea-prep.blob
120
+ node tools/make-exe.js --node "C:\Program Files\nodejs\node.exe"
121
+ ```
122
+
123
+ `--node` 必须指向**真实的 node.exe**(80 MB 左右)。用 Volta / nvm 时 `Get-Command node`
124
+ 拿到的是几 MB 的 shim,注入会失败,用 `node -e "console.log(process.execPath)"` 找真实路径。
125
+
126
+ SKILL.md 会被内嵌进 exe,所以对方运行 `dcr.exe install` 时不需要任何外部文件。
127
+
128
+ exe 产物已在 `.gitignore` 中排除,不会进版本库。
129
+
130
+ ## 常见问题
131
+ **代码块显示成一行?**
132
+ `<pre>` 依赖 `white-space:pre-wrap`,若编辑器剥离了内联样式,把生成的 `<pre>` 标签内容重新粘贴一次,或换用支持内联样式的编辑器(飞书文档、新版 CKEditor 均可)。
133
+
134
+ **`data-code-file` 报文件不存在?**
135
+ 检查 `--repo-root` 是否指向代码仓库根目录。引用其他仓库的文件用相对路径,如 `../another-repo/src/a.ts`。
136
+
137
+ **表格没有边框?**
138
+ 片段里不要给 `<table>` 写 `border` 属性,交给脚本注入 `border-collapse` 与单元格边框。
139
+
140
+ ## License
141
+
142
+ MIT
package/SKILL.md ADDED
@@ -0,0 +1,108 @@
1
+ ---
2
+ name: dev-change-report
3
+ description: 把一次代码改动整理成可直接粘贴到 CKEditor / OA / 飞书 / Word 的开发说明文档。代码块以浅灰底等宽字体呈现,全部内联样式,源码模式粘贴即成型。Use when the user asks to 整理改动内容、输出开发描述、写改动说明、总结本次改动、生成需求交付文档、生成代码说明文档、粘贴到富文本编辑器。
4
+ ---
5
+
6
+ # 开发改动说明文档
7
+
8
+ 把代码改动整理成一份**可直接粘贴的富文本 HTML**。
9
+
10
+ 需要的命令由 npm 包 `dev-change-report` 提供(`npx dcr`),前置条件:项目已安装该包。
11
+ 核心约定:**片段只写语义标签,不写任何 style**,样式由 `dcr build` 统一注入。
12
+
13
+ ## 工作流
14
+
15
+ 1. **收集改动**
16
+ ```bash
17
+ npx dcr changes --repo . --repo ../another-repo --base origin/release
18
+ ```
19
+ 省略 `--base` 会自动探测(`origin/release` → `master` → `main` → `develop` → `HEAD~10`),
20
+ 同时列出工作区未提交改动。
21
+
22
+ 2. **读关键 diff**,每个要点写清三件事:**原问题 → 怎么改的 → 改后效果**。
23
+ 需要精确行号时用 `git -C <repo> diff --unified=0 <base>...HEAD -- <file>`,
24
+ 拿到的 `@@ -a,b +c,d @@` 可直接换算成 `data-code-range`。
25
+
26
+ 3. **写片段** `report.fragment.html`(只写标签,不写样式)。
27
+
28
+ 4. **生成**
29
+ ```bash
30
+ npx dcr build --input report.fragment.html --outdir ./change-report --repo-root .
31
+ ```
32
+ 产物只有 `report.html` 一个文件。
33
+
34
+ 5. **交付**:给出 `report.html` 全文,提示「粘到编辑器源码模式 → 切回可视化」。
35
+
36
+ ## 片段写法
37
+
38
+ ```html
39
+ <h2>一、模块名</h2>
40
+
41
+ <h3>1.1 要点</h3>
42
+ <p>原问题:……</p>
43
+ <pre data-code-file="src/a.ts" data-code-range="40-60"></pre>
44
+ <p tip="info">改后效果:……</p>
45
+ ```
46
+
47
+ | 写法 | 用途 |
48
+ |---|---|
49
+ | `<h2>` / `<h3>` | 一级章节 / 二级标题 |
50
+ | `<p>` | 正文 |
51
+ | `<p tip="info">` / `warn` / `danger` | 提示块(绿 / 黄 / 红) |
52
+ | `<table><tr><th>…<td>…` | 表格,**不要**写 border 或 style |
53
+ | `<ul><li>` | 列表 |
54
+ | `<code>` | 行内代码,如 `<code>Form.vue</code>` |
55
+ | `<pre data-code-file="…" data-code-range="…">` | 代码块,从文件按行截取(推荐) |
56
+ | `<pre data-code-lang="text">…</pre>` | 代码块,内嵌(需转义 `<` `>` `&`) |
57
+
58
+ `data-code-file` 相对 `--repo-root`;`data-code-range` 是 1-based 闭区间,省略取全文。
59
+ **优先引用真实文件**,避免手抄出错。只截 8~25 行能说明问题的片段,不要贴整文件。
60
+
61
+ ## 报告结构
62
+
63
+ ```
64
+ <h2>标题</h2>
65
+ <table> 元信息:需求 / 分支 / 提交 / 影响范围
66
+ <h2>一、<板块 1></h2> 要点 + 代码块
67
+ <h2>二、<板块 2></h2>
68
+ <h2>三、涉及文件清单</h2> 表格,相对路径 + <code>
69
+ <h2>四、验证记录</h2>
70
+ <h2>五、上线说明</h2> 发布顺序、环境变量(用 tip="danger" 提示风险)
71
+ ```
72
+
73
+ 写作要点:
74
+ - 代码块前用一句话说明**为什么**需要这段代码,块后不重复解释代码本身
75
+ - 「两个字段名字一样但含义不同」这类根因,比贴代码更有价值
76
+ - 文件清单写**相对路径**,不写绝对路径
77
+ - 先写正文章节,最后统一补标题编号,避免反复改
78
+
79
+ ## 富文本兼容
80
+
81
+ 脚本已处理:`<pre>` 等宽与换行、表格边框、行内代码底色、提示块配色、各级标签默认字号间距。
82
+ 片段里仍需避免:
83
+
84
+ | 禁止 | 原因 |
85
+ |---|---|
86
+ | `<style>` 块 | 编辑器会剥离 |
87
+ | `<p>` 嵌套 `<p>` | 编辑器会拆结构 |
88
+ | Markdown 语法(`**`、`` ` ``) | 源码模式不转换 |
89
+ | `<pre>` 里手写代码 | 用 `data-code-*` 占位,交给脚本转义 |
90
+
91
+ ## 命令参数
92
+
93
+ | 命令 | 参数 | 说明 |
94
+ |---|---|---|
95
+ | `dcr build` | `--input` | 必填,片段 HTML 路径 |
96
+ | | `--outdir` | 默认 `./change-report` |
97
+ | | `--repo-root` | 默认当前目录,`data-code-file` 的解析根 |
98
+ | `dcr changes` | `--repo` | 可多次传入 |
99
+ | | `--base` | 对比基线,省略自动探测 |
100
+ | | `--json` | 导出结构化清单 |
101
+ | `dcr install` | `--project` | 装到当前项目而非用户目录 |
102
+ | | `--force` | 覆盖已有版本 |
103
+
104
+ ## 环境
105
+
106
+ `npx dcr --version` 能输出版本号即可用。包内零第三方依赖,Node 18+,Windows / macOS / Linux 通用。
107
+
108
+ `change-report/` 属于临时产物,交付后清理,或在 `.gitignore` 中排除。
package/bin/dcr.js ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dcr —— dev-change-report 命令行入口
4
+ *
5
+ * dcr install [--project] [--force] 把 skill 装到 .agents/skills/
6
+ * dcr build --input <fragment.html> 生成 report.html
7
+ * dcr changes --repo <path> [--repo …] 收集 git 改动清单
8
+ */
9
+ const { run } = require('../lib/cli');
10
+
11
+ process.exitCode = run(process.argv.slice(2));
package/lib/build.js ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * 报告生成核心:把带 <pre data-code-*> 占位的 HTML 片段转成富文本 HTML。
3
+ * 片段里不写任何 style,样式由 STYLE / TIP 两个字典统一注入。
4
+ */
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+
8
+ // ---- 样式字典:只改这里,全文观感随之变化 ----
9
+ const BASE = 'font-size:14px;line-height:1.75;color:#2f363d;';
10
+ const STYLE = {
11
+ h2: 'font-size:19px;font-weight:600;margin:26px 0 12px;padding-bottom:6px;border-bottom:1px solid #e5e8eb;color:#1f2328;',
12
+ h3: 'font-size:15px;font-weight:600;margin:18px 0 8px;color:#1f2328;',
13
+ p: `${BASE}margin:10px 0;`,
14
+ ul: `${BASE}margin:10px 0 10px 22px;padding:0;`,
15
+ li: 'margin:4px 0;',
16
+ table: 'border-collapse:collapse;width:100%;font-size:13px;margin:12px 0;',
17
+ th: 'padding:7px 10px;text-align:left;border:1px solid #dfe3e6;background:#f6f8fa;font-weight:600;',
18
+ td: 'padding:7px 10px;border:1px solid #dfe3e6;vertical-align:top;',
19
+ code: 'background:#f2f4f6;padding:1px 5px;border-radius:3px;font-family:Consolas,Monaco,monospace;font-size:13px;color:#b3320f;',
20
+ pre: 'background:#f6f8fa;border:1px solid #e5e8eb;border-radius:3px;padding:10px 12px;margin:10px 0;'
21
+ + 'font-family:Consolas,Monaco,"Courier New",monospace;font-size:13px;line-height:1.55;'
22
+ + 'white-space:pre-wrap;word-break:break-word;color:#24292f;'
23
+ };
24
+ const TIP = {
25
+ info: ['#f2f7f4', '#0a7a50'],
26
+ warn: ['#fdf8e9', '#c99515'],
27
+ danger: ['#fcf0ef', '#c0392b']
28
+ };
29
+
30
+ const escapeHtml = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
31
+ const decodeEntities = (s) => String(s)
32
+ .replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"')
33
+ .replace(/&#39;/g, "'").replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&');
34
+ const attr = (tag, name) => (tag.match(new RegExp(`${name}\\s*=\\s*"([^"]*)"`)) || [])[1] || '';
35
+
36
+ /** 从文件按 1-based 闭区间截取;range 形如 "40-60",省略则取全文。 */
37
+ function readCode(repoRoot, file, range) {
38
+ const full = path.isAbsolute(file) ? file : path.join(repoRoot, file);
39
+ if (!fs.existsSync(full)) throw new Error(`data-code-file 指向的文件不存在: ${full}`);
40
+ const lines = fs.readFileSync(full, 'utf8').replace(/\r\n?/g, '\n').split('\n');
41
+ if (!range) return lines.join('\n');
42
+ const [a, b] = String(range).split('-');
43
+ const from = Math.max(1, parseInt(a, 10) || 1);
44
+ const to = b ? Math.min(lines.length, parseInt(b, 10) || lines.length) : lines.length;
45
+ return lines.slice(from - 1, to).join('\n');
46
+ }
47
+
48
+ /** 占位 -> 浅灰底 <pre>。必须在注入样式之前执行,避免代码里的 <h2> 被误处理。 */
49
+ function renderCodeBlocks(html, repoRoot, log) {
50
+ let seq = 0;
51
+ return html.replace(/<pre\b([^>]*)>([\s\S]*?)<\/pre>/gi, (whole, attrs, inner) => {
52
+ if (!/data-code-(file|lang)/i.test(attrs)) return whole;
53
+ seq += 1;
54
+ const file = attr(attrs, 'data-code-file');
55
+ const range = attr(attrs, 'data-code-range');
56
+ const raw = file ? readCode(repoRoot, file, range) : decodeEntities(inner);
57
+ const code = raw.replace(/^\n+/, '').replace(/\s+$/, '');
58
+ if (!code) { log(` ! #${seq} 空代码块,已跳过`); return ''; }
59
+ const src = file ? `${file}:${range || 'all'}` : 'inline';
60
+ log(` #${String(seq).padStart(2, '0')} ${src} (${code.split('\n').length} 行)`);
61
+ return `<pre style="${STYLE.pre}">${escapeHtml(code)}</pre>`;
62
+ });
63
+ }
64
+
65
+ /** 给没有 style 的标签补默认样式。 */
66
+ const styleTag = (html, tag, style) =>
67
+ html.replace(new RegExp(`<${tag}(\\s[^>]*)?>`, 'gi'), (m, attrs = '') =>
68
+ /\bstyle\s*=/i.test(attrs) ? m : `<${tag}${attrs} style="${style}">`);
69
+
70
+ const applyTips = (html) =>
71
+ html.replace(/<p\s+tip="(\w+)"[^>]*>([\s\S]*?)<\/p>/gi, (m, kind, inner) => {
72
+ const [bg, border] = TIP[kind] || TIP.info;
73
+ return `<p style="${BASE}margin:10px 0;padding:8px 12px;background:${bg};border-left:3px solid ${border};">${inner}</p>`;
74
+ });
75
+
76
+ const TAGS = ['h2', 'h3', 'p', 'ul', 'li', 'table', 'th', 'td', 'code'];
77
+
78
+ /**
79
+ * @param {object} options
80
+ * @param {string} options.input 片段 HTML 路径
81
+ * @param {string} [options.outdir] 输出目录,默认 ./change-report
82
+ * @param {string} [options.repoRoot] data-code-file 的解析根目录
83
+ * @param {(s:string)=>void} [options.log]
84
+ * @returns {{outFile:string, bytes:number}}
85
+ */
86
+ function buildReport(options) {
87
+ if (!options.input) throw new Error('缺少 input');
88
+ const input = path.resolve(options.input);
89
+ if (!fs.existsSync(input)) throw new Error(`输入文件不存在: ${input}`);
90
+ const outdir = path.resolve(options.outdir || './change-report');
91
+ const repoRoot = path.resolve(options.repoRoot || process.cwd());
92
+ const log = options.log || (() => {});
93
+
94
+ let html = fs.readFileSync(input, 'utf8');
95
+ html = renderCodeBlocks(html, repoRoot, log);
96
+ html = applyTips(html);
97
+ for (const tag of TAGS) html = styleTag(html, tag, STYLE[tag]);
98
+
99
+ fs.mkdirSync(outdir, { recursive: true });
100
+ const outFile = path.join(outdir, 'report.html');
101
+ const out = html.trim() + '\n';
102
+ fs.writeFileSync(outFile, out, 'utf8');
103
+ return { outFile, bytes: Buffer.byteLength(out) };
104
+ }
105
+
106
+ module.exports = { buildReport, STYLE, TIP };
package/lib/changes.js ADDED
@@ -0,0 +1,100 @@
1
+ /**
2
+ * 收集 git 改动清单,供撰写「改动说明」时对照。
3
+ * 不指定 base 时自动探测:origin/release → origin/master → origin/main → origin/develop → HEAD~10。
4
+ */
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+ const { execFileSync } = require('node:child_process');
8
+
9
+ const git = (repo, args) => {
10
+ try {
11
+ return execFileSync('git', ['-C', repo, ...args], {
12
+ encoding: 'utf8',
13
+ stdio: ['ignore', 'pipe', 'ignore']
14
+ }).trim();
15
+ } catch {
16
+ return '';
17
+ }
18
+ };
19
+
20
+ const detectBase = (repo) => {
21
+ for (const candidate of ['origin/release', 'origin/master', 'origin/main', 'origin/develop']) {
22
+ if (git(repo, ['rev-parse', '--verify', '--quiet', candidate])) return candidate;
23
+ }
24
+ return 'HEAD~10';
25
+ };
26
+
27
+ const parseNumstat = (text) => text.split('\n')
28
+ .filter((line) => line.trim())
29
+ .map((line) => {
30
+ const [added, deleted, file] = line.split('\t');
31
+ if (!file) return null;
32
+ return {
33
+ path: file,
34
+ added: added === '-' ? null : Number(added),
35
+ deleted: deleted === '-' ? null : Number(deleted)
36
+ };
37
+ })
38
+ .filter(Boolean);
39
+
40
+ const parseStatus = (text) => text.split('\n')
41
+ .filter((line) => line.trim())
42
+ .map((line) => ({ code: line.slice(0, 2).trim(), path: line.slice(3).trim() }));
43
+
44
+ /**
45
+ * @param {object} options
46
+ * @param {string[]} options.repos 仓库路径
47
+ * @param {string} [options.base] 对比基线,省略则自动探测
48
+ * @param {string} [options.json] 导出 JSON 的路径
49
+ * @param {(s:string)=>void} [options.log]
50
+ */
51
+ function collectChanges(options) {
52
+ const repos = (options.repos || []).filter(Boolean);
53
+ if (!repos.length) throw new Error('至少需要一个仓库路径');
54
+ const log = options.log || (() => {});
55
+ const report = { generatedAt: new Date().toISOString(), repos: [] };
56
+
57
+ for (const repoArg of repos) {
58
+ const repo = path.resolve(repoArg);
59
+ if (!fs.existsSync(repo)) {
60
+ log(`跳过不存在的路径: ${repo}`);
61
+ continue;
62
+ }
63
+ const name = path.basename(repo);
64
+ const branch = git(repo, ['rev-parse', '--abbrev-ref', 'HEAD']) || '(unknown)';
65
+ const head = git(repo, ['log', '-1', '--format=%h %s']) || '';
66
+ const base = options.base || detectBase(repo);
67
+ const files = parseNumstat(git(repo, ['diff', '--numstat', `${base}...HEAD`]));
68
+ const uncommitted = parseStatus(git(repo, ['status', '--porcelain', '--untracked-files=all']));
69
+
70
+ report.repos.push({ name, path: repo, branch, head, base, files, uncommitted });
71
+
72
+ log(`\n=== ${name} (${branch}) ===`);
73
+ log(`HEAD: ${head}`);
74
+ log(`BASE: ${base}`);
75
+ if (!files.length) {
76
+ log(' 分支内暂无已提交改动');
77
+ } else {
78
+ const added = files.reduce((sum, f) => sum + (f.added || 0), 0);
79
+ const deleted = files.reduce((sum, f) => sum + (f.deleted || 0), 0);
80
+ log(` 已提交改动 ${files.length} 个文件,+${added} / -${deleted}`);
81
+ for (const file of files) {
82
+ const stat = file.added === null ? 'binary' : `+${file.added}/-${file.deleted}`;
83
+ log(` ${stat.padEnd(12)} ${file.path}`);
84
+ }
85
+ }
86
+ if (uncommitted.length) {
87
+ log(` 工作区未提交 ${uncommitted.length} 项`);
88
+ for (const item of uncommitted) log(` [${item.code}] ${item.path}`);
89
+ }
90
+ }
91
+
92
+ if (options.json) {
93
+ const out = path.resolve(options.json);
94
+ fs.writeFileSync(out, JSON.stringify(report, null, 2), 'utf8');
95
+ log(`\nJSON 已写入: ${out}`);
96
+ }
97
+ return report;
98
+ }
99
+
100
+ module.exports = { collectChanges, detectBase };
package/lib/cli.js ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * CLI 主逻辑(不依赖进程启动方式),供 bin/dcr.js 与 SEA 单文件入口共用。
3
+ */
4
+ const { buildReport } = require('./build');
5
+ const { collectChanges } = require('./changes');
6
+ const { installSkill } = require('./install');
7
+
8
+ const HELP = `dcr —— 代码改动说明文档生成器
9
+
10
+ 用法:
11
+ dcr install [--project] [--force] 安装 skill 到 .agents/skills/
12
+ --project 装到当前项目而非用户目录
13
+ --force 覆盖已存在的版本
14
+
15
+ dcr build --input <fragment.html> 生成富文本报告
16
+ [--outdir ./change-report] 输出目录
17
+ [--repo-root .] data-code-file 的解析根目录
18
+
19
+ dcr changes --repo <路径> [--repo …] 收集 git 改动清单
20
+ [--base origin/release] 对比基线(省略自动探测)
21
+ [--json changes.json] 导出 JSON
22
+
23
+ 示例:
24
+ dcr install
25
+ dcr changes --repo . --base origin/release
26
+ dcr build --input report.fragment.html --repo-root .`;
27
+
28
+ function parseArgs(argv) {
29
+ const args = { repos: [] };
30
+ for (let i = 0; i < argv.length; i += 1) {
31
+ const token = argv[i];
32
+ if (!token.startsWith('--')) continue;
33
+ const key = token.replace(/^--/, '').replace(/-([a-z])/g, (_, c) => c.toUpperCase());
34
+ const value = argv[i + 1];
35
+ if (key === 'repo') {
36
+ if (value && !value.startsWith('--')) { args.repos.push(value); i += 1; }
37
+ } else if (value === undefined || value.startsWith('--')) {
38
+ args[key] = true;
39
+ } else {
40
+ args[key] = value;
41
+ i += 1;
42
+ }
43
+ }
44
+ return args;
45
+ }
46
+
47
+ const version = () => global.__DCR_VERSION__ || require('../package.json').version;
48
+
49
+ /**
50
+ * @param {string[]} argv 不含 node 与脚本路径的参数
51
+ * @param {{print?:(s?:string)=>void}} [io]
52
+ * @returns {number} 退出码
53
+ */
54
+ function run(argv, io = {}) {
55
+ const print = io.print || ((line = '') => process.stdout.write(`${line}\n`));
56
+ const [command, ...rest] = argv;
57
+ const args = parseArgs(rest);
58
+
59
+ if (command === '--version' || command === '-v' || command === 'version') {
60
+ print(version());
61
+ return 0;
62
+ }
63
+
64
+ if (!command || command === 'help' || command === '--help' || args.help) {
65
+ print(HELP);
66
+ return 0;
67
+ }
68
+
69
+ try {
70
+ if (command === 'install') {
71
+ installSkill({ project: args.project === true, force: args.force === true, log: print });
72
+ return 0;
73
+ }
74
+
75
+ if (command === 'build') {
76
+ if (!args.input) {
77
+ print('缺少 --input <fragment.html>,用 dcr help 查看用法');
78
+ return 1;
79
+ }
80
+ print('处理代码块:');
81
+ const { outFile, bytes } = buildReport({
82
+ input: args.input,
83
+ outdir: args.outdir,
84
+ repoRoot: args.repoRoot,
85
+ log: print
86
+ });
87
+ print(`\n完成 -> ${outFile} (${Math.round(bytes / 1024)} KB)`);
88
+ print('粘贴方式:编辑器切到「源码」模式,粘入 report.html 全文,再切回可视化。');
89
+ return 0;
90
+ }
91
+
92
+ if (command === 'changes') {
93
+ collectChanges({ repos: args.repos, base: args.base, json: args.json, log: print });
94
+ return 0;
95
+ }
96
+
97
+ print(`未知命令: ${command}\n`);
98
+ print(HELP);
99
+ return 1;
100
+ } catch (error) {
101
+ print(`\n出错: ${error.message}`);
102
+ return 1;
103
+ }
104
+ }
105
+
106
+ module.exports = { run, version, HELP };
package/lib/install.js ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * 把 SKILL.md 安装到 agent 能识别的 skills 目录。
3
+ *
4
+ * 个人级(默认): <home>/.agents/skills/dev-change-report/SKILL.md —— 所有项目可用
5
+ * 项目级(--project): <cwd>/.agents/skills/dev-change-report/SKILL.md —— 跟着仓库走
6
+ *
7
+ * SKILL.md 的来源优先级:
8
+ * 1. options.content 调用方直接给内容
9
+ * 2. global.__DCR_SKILL_MD__ 打包进 exe 的内嵌副本
10
+ * 3. 包内 SKILL.md npm 安装场景
11
+ */
12
+ const fs = require('node:fs');
13
+ const os = require('node:os');
14
+ const path = require('node:path');
15
+
16
+ const SKILL_NAME = 'dev-change-report';
17
+ const PACKAGE_ROOT = path.join(__dirname, '..');
18
+
19
+ const targetDir = (useProject) =>
20
+ path.join(useProject ? process.cwd() : os.homedir(), '.agents', 'skills', SKILL_NAME);
21
+
22
+ function readSkillMarkdown(explicit) {
23
+ if (explicit) return explicit;
24
+ if (typeof global.__DCR_SKILL_MD__ === 'string') return global.__DCR_SKILL_MD__;
25
+ const source = path.join(PACKAGE_ROOT, 'SKILL.md');
26
+ if (fs.existsSync(source)) return fs.readFileSync(source, 'utf8');
27
+ throw new Error('找不到 SKILL.md,请确认安装完整');
28
+ }
29
+
30
+ /**
31
+ * @param {object} options
32
+ * @param {boolean} [options.project] true 装到当前项目,false 装到用户目录
33
+ * @param {boolean} [options.force] 已存在时覆盖
34
+ * @param {string} [options.content] 直接指定 SKILL.md 内容
35
+ * @param {(s:string)=>void} [options.log]
36
+ */
37
+ function installSkill(options = {}) {
38
+ const log = options.log || (() => {});
39
+ const content = readSkillMarkdown(options.content);
40
+ const dir = targetDir(options.project);
41
+ const target = path.join(dir, 'SKILL.md');
42
+ const existed = fs.existsSync(target);
43
+
44
+ if (existed && !options.force) {
45
+ log(`已存在,未覆盖: ${target}`);
46
+ log('如需更新,加 --force');
47
+ return { dir, written: false, existed: true };
48
+ }
49
+
50
+ fs.mkdirSync(dir, { recursive: true });
51
+ fs.writeFileSync(target, content, 'utf8');
52
+ log(`${existed ? '已更新' : '已安装'}: ${target}`);
53
+ log('');
54
+ log('下一步:重启编辑器,然后对它说「整理一下这次改动」即可触发。');
55
+ return { dir, written: true, existed };
56
+ }
57
+
58
+ module.exports = { installSkill, SKILL_NAME };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * SEA 入口:node 单文件可执行程序从这里启动。
3
+ * 该文件会被 tools/bundle.js 内联,不要 require 任何相对路径以外的资源。
4
+ */
5
+ const { run } = require('./cli');
6
+
7
+ process.exitCode = run(process.argv.slice(2));
package/package.json CHANGED
@@ -1,6 +1,41 @@
1
- {
2
- "name": "dev-change-report",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "dev-change-report",
3
+ "version": "1.0.0",
4
+ "description": "把一次代码改动整理成可直接粘贴到 CKEditor / OA / 飞书 / Word 的富文本开发说明文档",
5
+ "keywords": [
6
+ "copilot",
7
+ "skill",
8
+ "changelog",
9
+ "report",
10
+ "ckeditor",
11
+ "rich-text",
12
+ "code-review",
13
+ "documentation"
14
+ ],
15
+ "bin": {
16
+ "dcr": "bin/dcr.js"
17
+ },
18
+ "files": [
19
+ "bin",
20
+ "lib",
21
+ "SKILL.md",
22
+ "README.md"
23
+ ],
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "scripts": {
28
+ "bundle": "node tools/bundle.js",
29
+ "sea:blob": "node --experimental-sea-config packaging/sea-config.json",
30
+ "exe": "npm run bundle && npm run sea:blob && node tools/make-exe.js",
31
+ "pack:win": "npm run exe && node tools/pack-win.js"
32
+ },
33
+ "devDependencies": {
34
+ "postject": "^1.0.0-alpha.6"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public",
38
+ "registry": "https://registry.npmjs.org/"
39
+ },
40
+ "license": "MIT"
41
+ }