dev-change-report 0.0.0-stage → 1.1.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 +162 -3
- package/SKILL.md +110 -0
- package/bin/dcr.js +11 -0
- package/bin/postinstall.js +47 -0
- package/lib/build.js +106 -0
- package/lib/changes.js +100 -0
- package/lib/cli.js +107 -0
- package/lib/install.js +84 -0
- package/lib/sea-entry.js +7 -0
- package/package.json +42 -6
package/README.md
CHANGED
|
@@ -1,3 +1,162 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
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
|
+
**一句话:装个依赖就行,skill 会自动注册。**
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm i -D dev-change-report
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`postinstall` 会自动把 `SKILL.md` 写到 `~/.agents/skills/dev-change-report/`,
|
|
19
|
+
**重启编辑器**后对 AI 说「整理一下这次改动」即可触发。不需要额外命令。
|
|
20
|
+
|
|
21
|
+
升级同理,改一下版本号即可,skill 会跟着更新:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm i -D dev-change-report@latest
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 需要时可手动控制
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx dcr install # 重新注册(幂等,内容相同则不写盘)
|
|
31
|
+
npx dcr install --project # 装到 ./.agents/skills/,跟着仓库走
|
|
32
|
+
npx dcr install --force # 强制覆盖
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| 环境变量 | 作用 |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `DCR_SKIP_INSTALL=1` | 跳过 postinstall 自动注册(CI 里常用) |
|
|
38
|
+
| `DCR_SKILLS_DIR=<路径>` | 自定义 skills 根目录,沙箱/测试用 |
|
|
39
|
+
|
|
40
|
+
> 如果公司 npm 配置了 `--ignore-scripts`,postinstall 不会执行,
|
|
41
|
+
> 装完手动跑一次 `npx dcr install` 即可。
|
|
42
|
+
|
|
43
|
+
### 备选:单文件 exe(对方机器没装 Node 时才需要)
|
|
44
|
+
|
|
45
|
+
`npm run exe` 会生成 `dist/dcr.exe`(83 MB),打包后约 33 MB。
|
|
46
|
+
体积来自内嵌的 Node 运行时,SEA 不支持裁剪,无法再小。
|
|
47
|
+
|
|
48
|
+
对方解压 `dcr-win-x64.zip` 后双击 `install.cmd` 即可(自动复制到
|
|
49
|
+
`%LOCALAPPDATA%\Programs\dcr`、加 PATH、注册 skill)。首次运行可能有
|
|
50
|
+
SmartScreen 提示,点「更多信息」→「仍要运行」。
|
|
51
|
+
|
|
52
|
+
> 83 MB 换来的只是「不用装 Node」。如果对方有 Node,用 npm 包,体积差约 3000 倍。
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
## 使用
|
|
56
|
+
|
|
57
|
+
### 1. 收集改动
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx dcr changes --repo . --base origin/release
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
输出分支、HEAD、文件清单与增删行数,并列出工作区未提交改动。
|
|
64
|
+
加 `--json changes.json` 可导出结构化清单。
|
|
65
|
+
|
|
66
|
+
### 2. 写片段
|
|
67
|
+
|
|
68
|
+
新建 `report.fragment.html`,只写标签,不写样式:
|
|
69
|
+
|
|
70
|
+
```html
|
|
71
|
+
<h2>一、选择柜组件优化</h2>
|
|
72
|
+
|
|
73
|
+
<h3>1.1 默认搜索条件与 PC 对齐</h3>
|
|
74
|
+
<p>原问题:默认按实际柜号搜索且发货日期写死当天,柜的发货时间普遍早于当天,必然空结果。</p>
|
|
75
|
+
<pre data-code-file="src/views/qbcrm/blRelease/Form.vue" data-code-range="56-79"></pre>
|
|
76
|
+
<p tip="info">改后效果:打开即有数据,输入提示随字段联动。</p>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
| 写法 | 用途 |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `<h2>` / `<h3>` | 一级章节 / 二级标题 |
|
|
82
|
+
| `<p>` | 正文 |
|
|
83
|
+
| `<p tip="info\|warn\|danger">` | 提示块(绿 / 黄 / 红) |
|
|
84
|
+
| `<table><tr><th>…<td>…` | 表格,不要写 border 或 style |
|
|
85
|
+
| `<ul><li>` | 列表 |
|
|
86
|
+
| `<code>` | 行内代码 |
|
|
87
|
+
| `<pre data-code-file="…" data-code-range="…">` | 从文件按行截取(推荐) |
|
|
88
|
+
| `<pre data-code-lang="text">…</pre>` | 内嵌代码,需转义 `<` `>` `&` |
|
|
89
|
+
|
|
90
|
+
`data-code-range` 是 1-based 闭区间(如 `56-79`),省略则取全文。
|
|
91
|
+
|
|
92
|
+
### 3. 生成
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npx dcr build --input report.fragment.html --repo-root .
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
产物 `change-report/report.html`。
|
|
99
|
+
|
|
100
|
+
### 4. 粘贴
|
|
101
|
+
|
|
102
|
+
打开 `report.html`,全选复制 → 编辑器切到「源码」模式 → 粘贴 → 切回可视化。
|
|
103
|
+
|
|
104
|
+
## 调整样式
|
|
105
|
+
|
|
106
|
+
打开 `lib/build.js`,顶部的 `STYLE` 与 `TIP` 两个字典控制全部观感:
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
const STYLE = {
|
|
110
|
+
h2: 'font-size:19px;font-weight:600;margin:26px 0 12px;...',
|
|
111
|
+
pre: 'background:#f6f8fa;border:1px solid #e5e8eb;...',
|
|
112
|
+
// ...
|
|
113
|
+
};
|
|
114
|
+
const TIP = { info: ['#f2f7f4', '#0a7a50'], warn: [...], danger: [...] };
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## 命令总览
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
dcr install [--project] [--force] 安装 skill 到 .agents/skills/
|
|
121
|
+
dcr build --input <fragment.html> 生成 report.html
|
|
122
|
+
[--outdir ./change-report]
|
|
123
|
+
[--repo-root .]
|
|
124
|
+
dcr changes --repo <路径> [--repo …] 收集 git 改动清单
|
|
125
|
+
[--base origin/release]
|
|
126
|
+
[--json changes.json]
|
|
127
|
+
dcr --version 查看版本
|
|
128
|
+
dcr help 查看帮助
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## 自行构建 exe
|
|
132
|
+
|
|
133
|
+
只在确实需要「无 Node 环境」分发时才构建。需要 Node 20+(含 SEA 支持),
|
|
134
|
+
`tools/` 下的脚本零第三方依赖,只有注入环节需要 `postject`。
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
npm i -D postject
|
|
138
|
+
npm run bundle # lib/ + bin/ 合并成 dist/dcr-bundle.js(21 KB)
|
|
139
|
+
npm run sea:blob # 生成 dist/sea-prep.blob
|
|
140
|
+
node tools/make-exe.js --node "C:\Program Files\nodejs\node.exe"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`--node` 必须指向**真实的 node.exe**(80 MB 左右)。用 Volta / nvm 时 `Get-Command node`
|
|
144
|
+
拿到的是几 MB 的 shim,注入会失败,用 `node -e "console.log(process.execPath)"` 找真实路径。
|
|
145
|
+
|
|
146
|
+
SKILL.md 会被内嵌进 exe,所以对方运行 `dcr.exe install` 时不需要任何外部文件。
|
|
147
|
+
|
|
148
|
+
exe 产物已在 `.gitignore` 中排除,不会进版本库。
|
|
149
|
+
|
|
150
|
+
## 常见问题
|
|
151
|
+
**代码块显示成一行?**
|
|
152
|
+
`<pre>` 依赖 `white-space:pre-wrap`,若编辑器剥离了内联样式,把生成的 `<pre>` 标签内容重新粘贴一次,或换用支持内联样式的编辑器(飞书文档、新版 CKEditor 均可)。
|
|
153
|
+
|
|
154
|
+
**`data-code-file` 报文件不存在?**
|
|
155
|
+
检查 `--repo-root` 是否指向代码仓库根目录。引用其他仓库的文件用相对路径,如 `../another-repo/src/a.ts`。
|
|
156
|
+
|
|
157
|
+
**表格没有边框?**
|
|
158
|
+
片段里不要给 `<table>` 写 `border` 属性,交给脚本注入 `border-collapse` 与单元格边框。
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
`npm i -D dev-change-report`,装完 skill 会自动注册。
|
|
12
|
+
|
|
13
|
+
核心约定:**片段只写语义标签,不写任何 style**,样式由 `dcr build` 统一注入。
|
|
14
|
+
|
|
15
|
+
## 工作流
|
|
16
|
+
|
|
17
|
+
1. **收集改动**
|
|
18
|
+
```bash
|
|
19
|
+
npx dcr changes --repo . --repo ../another-repo --base origin/release
|
|
20
|
+
```
|
|
21
|
+
省略 `--base` 会自动探测(`origin/release` → `master` → `main` → `develop` → `HEAD~10`),
|
|
22
|
+
同时列出工作区未提交改动。
|
|
23
|
+
|
|
24
|
+
2. **读关键 diff**,每个要点写清三件事:**原问题 → 怎么改的 → 改后效果**。
|
|
25
|
+
需要精确行号时用 `git -C <repo> diff --unified=0 <base>...HEAD -- <file>`,
|
|
26
|
+
拿到的 `@@ -a,b +c,d @@` 可直接换算成 `data-code-range`。
|
|
27
|
+
|
|
28
|
+
3. **写片段** `report.fragment.html`(只写标签,不写样式)。
|
|
29
|
+
|
|
30
|
+
4. **生成**
|
|
31
|
+
```bash
|
|
32
|
+
npx dcr build --input report.fragment.html --outdir ./change-report --repo-root .
|
|
33
|
+
```
|
|
34
|
+
产物只有 `report.html` 一个文件。
|
|
35
|
+
|
|
36
|
+
5. **交付**:给出 `report.html` 全文,提示「粘到编辑器源码模式 → 切回可视化」。
|
|
37
|
+
|
|
38
|
+
## 片段写法
|
|
39
|
+
|
|
40
|
+
```html
|
|
41
|
+
<h2>一、模块名</h2>
|
|
42
|
+
|
|
43
|
+
<h3>1.1 要点</h3>
|
|
44
|
+
<p>原问题:……</p>
|
|
45
|
+
<pre data-code-file="src/a.ts" data-code-range="40-60"></pre>
|
|
46
|
+
<p tip="info">改后效果:……</p>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
| 写法 | 用途 |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `<h2>` / `<h3>` | 一级章节 / 二级标题 |
|
|
52
|
+
| `<p>` | 正文 |
|
|
53
|
+
| `<p tip="info">` / `warn` / `danger` | 提示块(绿 / 黄 / 红) |
|
|
54
|
+
| `<table><tr><th>…<td>…` | 表格,**不要**写 border 或 style |
|
|
55
|
+
| `<ul><li>` | 列表 |
|
|
56
|
+
| `<code>` | 行内代码,如 `<code>Form.vue</code>` |
|
|
57
|
+
| `<pre data-code-file="…" data-code-range="…">` | 代码块,从文件按行截取(推荐) |
|
|
58
|
+
| `<pre data-code-lang="text">…</pre>` | 代码块,内嵌(需转义 `<` `>` `&`) |
|
|
59
|
+
|
|
60
|
+
`data-code-file` 相对 `--repo-root`;`data-code-range` 是 1-based 闭区间,省略取全文。
|
|
61
|
+
**优先引用真实文件**,避免手抄出错。只截 8~25 行能说明问题的片段,不要贴整文件。
|
|
62
|
+
|
|
63
|
+
## 报告结构
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
<h2>标题</h2>
|
|
67
|
+
<table> 元信息:需求 / 分支 / 提交 / 影响范围
|
|
68
|
+
<h2>一、<板块 1></h2> 要点 + 代码块
|
|
69
|
+
<h2>二、<板块 2></h2>
|
|
70
|
+
<h2>三、涉及文件清单</h2> 表格,相对路径 + <code>
|
|
71
|
+
<h2>四、验证记录</h2>
|
|
72
|
+
<h2>五、上线说明</h2> 发布顺序、环境变量(用 tip="danger" 提示风险)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
写作要点:
|
|
76
|
+
- 代码块前用一句话说明**为什么**需要这段代码,块后不重复解释代码本身
|
|
77
|
+
- 「两个字段名字一样但含义不同」这类根因,比贴代码更有价值
|
|
78
|
+
- 文件清单写**相对路径**,不写绝对路径
|
|
79
|
+
- 先写正文章节,最后统一补标题编号,避免反复改
|
|
80
|
+
|
|
81
|
+
## 富文本兼容
|
|
82
|
+
|
|
83
|
+
脚本已处理:`<pre>` 等宽与换行、表格边框、行内代码底色、提示块配色、各级标签默认字号间距。
|
|
84
|
+
片段里仍需避免:
|
|
85
|
+
|
|
86
|
+
| 禁止 | 原因 |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `<style>` 块 | 编辑器会剥离 |
|
|
89
|
+
| `<p>` 嵌套 `<p>` | 编辑器会拆结构 |
|
|
90
|
+
| Markdown 语法(`**`、`` ` ``) | 源码模式不转换 |
|
|
91
|
+
| `<pre>` 里手写代码 | 用 `data-code-*` 占位,交给脚本转义 |
|
|
92
|
+
|
|
93
|
+
## 命令参数
|
|
94
|
+
|
|
95
|
+
| 命令 | 参数 | 说明 |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `dcr build` | `--input` | 必填,片段 HTML 路径 |
|
|
98
|
+
| | `--outdir` | 默认 `./change-report` |
|
|
99
|
+
| | `--repo-root` | 默认当前目录,`data-code-file` 的解析根 |
|
|
100
|
+
| `dcr changes` | `--repo` | 可多次传入 |
|
|
101
|
+
| | `--base` | 对比基线,省略自动探测 |
|
|
102
|
+
| | `--json` | 导出结构化清单 |
|
|
103
|
+
| `dcr install` | `--project` | 装到当前项目而非用户目录 |
|
|
104
|
+
| | `--force` | 覆盖已有版本 |
|
|
105
|
+
|
|
106
|
+
## 环境
|
|
107
|
+
|
|
108
|
+
`npx dcr --version` 能输出版本号即可用。包内零第三方依赖,Node 18+,Windows / macOS / Linux 通用。
|
|
109
|
+
|
|
110
|
+
`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));
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* postinstall 钩子:npm i 时自动把 skill 注册到 ~/.agents/skills/。
|
|
4
|
+
*
|
|
5
|
+
* 设计约束:
|
|
6
|
+
* - 永不失败。任何异常都吞掉并退出码 0,不能因为装 skill 失败而破坏 npm i。
|
|
7
|
+
* - 内容一致时完全静默,不给每次 npm i 都刷屏。
|
|
8
|
+
* - 用 DCR_SKIP_INSTALL=1 可跳过(CI、或不想污染用户目录时)。
|
|
9
|
+
* - 只装用户级(~/.agents/skills)。项目级请手动 dcr install --project。
|
|
10
|
+
*/
|
|
11
|
+
let installSkill;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
({ installSkill } = require('../lib/install'));
|
|
15
|
+
} catch (error) {
|
|
16
|
+
// lib 缺失说明包损坏,交给 npm 的完整性校验处理,这里不发声。
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const skip = String(process.env.DCR_SKIP_INSTALL || '') === '1';
|
|
21
|
+
if (skip) process.exit(0);
|
|
22
|
+
|
|
23
|
+
// npm run 时会带上大量 npm_config_*,取 loglevel 判断是否静默
|
|
24
|
+
const silent = /silent|error/.test(String(process.env.npm_config_loglevel || ''));
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const result = installSkill({
|
|
28
|
+
quiet: true,
|
|
29
|
+
force: true,
|
|
30
|
+
log: silent ? () => {} : (line) => process.stdout.write(`[dev-change-report] ${line}\n`)
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (result.written && !silent) {
|
|
34
|
+
process.stdout.write(
|
|
35
|
+
'[dev-change-report] skill 已就绪,重启编辑器后对 AI 说「整理一下这次改动」即可触发。\n'
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
// 无写权限(如只读 HOME、受限沙箱)时静默降级,提示手动安装。
|
|
40
|
+
if (!silent) {
|
|
41
|
+
process.stdout.write(
|
|
42
|
+
'[dev-change-report] 自动注册 skill 失败,请稍后手动执行: npx dcr install\n'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
process.exit(0);
|
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
31
|
+
const decodeEntities = (s) => String(s)
|
|
32
|
+
.replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
33
|
+
.replace(/'/g, "'").replace(/ /g, ' ').replace(/&/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,107 @@
|
|
|
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
|
+
安装后无需任何配置:npm i 时已自动把 skill 注册到 ~/.agents/skills/。
|
|
11
|
+
|
|
12
|
+
用法:
|
|
13
|
+
dcr install [--project] [--force] 手动注册 skill(一般不需要)
|
|
14
|
+
--project 装到当前项目而非用户目录
|
|
15
|
+
--force 覆盖已存在的版本
|
|
16
|
+
|
|
17
|
+
dcr build --input <fragment.html> 生成富文本报告
|
|
18
|
+
[--outdir ./change-report] 输出目录
|
|
19
|
+
[--repo-root .] data-code-file 的解析根目录
|
|
20
|
+
|
|
21
|
+
dcr changes --repo <路径> [--repo …] 收集 git 改动清单
|
|
22
|
+
[--base origin/release] 对比基线(省略自动探测)
|
|
23
|
+
[--json changes.json] 导出 JSON
|
|
24
|
+
|
|
25
|
+
示例:
|
|
26
|
+
dcr changes --repo . --base origin/release
|
|
27
|
+
dcr build --input report.fragment.html --repo-root .`;
|
|
28
|
+
|
|
29
|
+
function parseArgs(argv) {
|
|
30
|
+
const args = { repos: [] };
|
|
31
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
32
|
+
const token = argv[i];
|
|
33
|
+
if (!token.startsWith('--')) continue;
|
|
34
|
+
const key = token.replace(/^--/, '').replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
35
|
+
const value = argv[i + 1];
|
|
36
|
+
if (key === 'repo') {
|
|
37
|
+
if (value && !value.startsWith('--')) { args.repos.push(value); i += 1; }
|
|
38
|
+
} else if (value === undefined || value.startsWith('--')) {
|
|
39
|
+
args[key] = true;
|
|
40
|
+
} else {
|
|
41
|
+
args[key] = value;
|
|
42
|
+
i += 1;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return args;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const version = () => global.__DCR_VERSION__ || require('../package.json').version;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string[]} argv 不含 node 与脚本路径的参数
|
|
52
|
+
* @param {{print?:(s?:string)=>void}} [io]
|
|
53
|
+
* @returns {number} 退出码
|
|
54
|
+
*/
|
|
55
|
+
function run(argv, io = {}) {
|
|
56
|
+
const print = io.print || ((line = '') => process.stdout.write(`${line}\n`));
|
|
57
|
+
const [command, ...rest] = argv;
|
|
58
|
+
const args = parseArgs(rest);
|
|
59
|
+
|
|
60
|
+
if (command === '--version' || command === '-v' || command === 'version') {
|
|
61
|
+
print(version());
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!command || command === 'help' || command === '--help' || args.help) {
|
|
66
|
+
print(HELP);
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
if (command === 'install') {
|
|
72
|
+
installSkill({ project: args.project === true, force: args.force === true, log: print });
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (command === 'build') {
|
|
77
|
+
if (!args.input) {
|
|
78
|
+
print('缺少 --input <fragment.html>,用 dcr help 查看用法');
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
print('处理代码块:');
|
|
82
|
+
const { outFile, bytes } = buildReport({
|
|
83
|
+
input: args.input,
|
|
84
|
+
outdir: args.outdir,
|
|
85
|
+
repoRoot: args.repoRoot,
|
|
86
|
+
log: print
|
|
87
|
+
});
|
|
88
|
+
print(`\n完成 -> ${outFile} (${Math.round(bytes / 1024)} KB)`);
|
|
89
|
+
print('粘贴方式:编辑器切到「源码」模式,粘入 report.html 全文,再切回可视化。');
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (command === 'changes') {
|
|
94
|
+
collectChanges({ repos: args.repos, base: args.base, json: args.json, log: print });
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
print(`未知命令: ${command}\n`);
|
|
99
|
+
print(HELP);
|
|
100
|
+
return 1;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
print(`\n出错: ${error.message}`);
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = { run, version, HELP };
|
package/lib/install.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
* 三种调用场景:
|
|
13
|
+
* - postinstall 自动安装:内容相同则完全静默,不同则覆盖(升级路径)
|
|
14
|
+
* - dcr install:显式执行,输出完整提示
|
|
15
|
+
* - dcr install --force:强制重写
|
|
16
|
+
*/
|
|
17
|
+
const fs = require('node:fs');
|
|
18
|
+
const os = require('node:os');
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
|
|
21
|
+
const SKILL_NAME = 'dev-change-report';
|
|
22
|
+
const PACKAGE_ROOT = path.join(__dirname, '..');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* skills 根目录。
|
|
26
|
+
* DCR_SKILLS_DIR 可整体覆盖,便于在沙箱/CI 里验证而不污染真实用户目录。
|
|
27
|
+
*/
|
|
28
|
+
const targetDir = (useProject) => {
|
|
29
|
+
const override = String(process.env.DCR_SKILLS_DIR || '').trim();
|
|
30
|
+
if (override) return path.join(override, SKILL_NAME);
|
|
31
|
+
return path.join(useProject ? process.cwd() : os.homedir(), '.agents', 'skills', SKILL_NAME);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function readSkillMarkdown(explicit) {
|
|
35
|
+
if (explicit) return explicit;
|
|
36
|
+
if (typeof global.__DCR_SKILL_MD__ === 'string') return global.__DCR_SKILL_MD__;
|
|
37
|
+
const source = path.join(PACKAGE_ROOT, 'SKILL.md');
|
|
38
|
+
if (fs.existsSync(source)) return fs.readFileSync(source, 'utf8');
|
|
39
|
+
throw new Error('找不到 SKILL.md,请确认安装完整');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {object} options
|
|
44
|
+
* @param {boolean} [options.project] true 装到当前项目,false 装到用户目录
|
|
45
|
+
* @param {boolean} [options.force] 已存在且内容不同时也覆盖;内容相同永远跳过写入
|
|
46
|
+
* @param {string} [options.content] 直接指定 SKILL.md 内容
|
|
47
|
+
* @param {boolean} [options.quiet] 内容无变化时不输出(postinstall 用)
|
|
48
|
+
* @param {(s:string)=>void} [options.log]
|
|
49
|
+
* @returns {{dir:string, written:boolean, existed:boolean, skipped:boolean}}
|
|
50
|
+
*/
|
|
51
|
+
function installSkill(options = {}) {
|
|
52
|
+
const log = options.log || (() => {});
|
|
53
|
+
const content = readSkillMarkdown(options.content);
|
|
54
|
+
const dir = targetDir(options.project);
|
|
55
|
+
const target = path.join(dir, 'SKILL.md');
|
|
56
|
+
const existed = fs.existsSync(target);
|
|
57
|
+
const unchanged = existed && fs.readFileSync(target, 'utf8') === content;
|
|
58
|
+
|
|
59
|
+
// 内容一致就没必要写盘,也不打扰用户。
|
|
60
|
+
if (unchanged) {
|
|
61
|
+
if (!options.quiet) {
|
|
62
|
+
log(`已是最新: ${target}`);
|
|
63
|
+
}
|
|
64
|
+
return { dir, written: false, existed: true, skipped: true };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 已存在且内容不同:force 或 quiet(postinstall 升级场景)时覆盖
|
|
68
|
+
if (existed && !options.force && !options.quiet) {
|
|
69
|
+
log(`已存在,未覆盖: ${target}`);
|
|
70
|
+
log('如需更新,加 --force');
|
|
71
|
+
return { dir, written: false, existed: true, skipped: false };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
75
|
+
fs.writeFileSync(target, content, 'utf8');
|
|
76
|
+
log(`${existed ? '已更新' : '已安装'}: ${target}`);
|
|
77
|
+
if (!options.quiet) {
|
|
78
|
+
log('');
|
|
79
|
+
log('下一步:重启编辑器,然后对它说「整理一下这次改动」即可触发。');
|
|
80
|
+
}
|
|
81
|
+
return { dir, written: true, existed, skipped: false };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { installSkill, SKILL_NAME };
|
package/lib/sea-entry.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dev-change-report",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "dev-change-report",
|
|
3
|
+
"version": "1.1.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
|
+
"postinstall": "node bin/postinstall.js",
|
|
29
|
+
"bundle": "node tools/bundle.js",
|
|
30
|
+
"sea:blob": "node --experimental-sea-config packaging/sea-config.json",
|
|
31
|
+
"exe": "npm run bundle && npm run sea:blob && node tools/make-exe.js",
|
|
32
|
+
"pack:win": "npm run exe && node tools/pack-win.js"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"postject": "^1.0.0-alpha.6"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public",
|
|
39
|
+
"registry": "https://registry.npmjs.org/"
|
|
40
|
+
},
|
|
41
|
+
"license": "MIT"
|
|
42
|
+
}
|