growork 1.0.1 → 1.1.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/.claude/commands/review.md +62 -0
- package/README.md +92 -21
- package/claude.md +72 -1
- package/dist/index.js +185 -71
- package/docs/product/prd-1.0.0.md +86 -0
- package/docs/product/prd-1.1.0.md +215 -0
- package/docs/product/prd-template.md +65 -0
- package/growork.config.yaml +31 -33
- package/package.json +1 -1
- package/src/commands/init.ts +1 -5
- package/src/commands/list.ts +52 -19
- package/src/commands/sync.ts +40 -26
- package/src/index.ts +13 -4
- package/src/utils/config.ts +156 -40
- package/tests/config.test.ts +372 -3
- package/{docs/test → tests}/test-cases.md +76 -26
- package/docs/product/prd-v1.0.md +0 -418
- package/test/backend-ai.md +0 -539
- package/test/backend-api.md +0 -1236
- package/test/prd-5spread.md +0 -74
- package/test/push-prd-notion.md +0 -126
- package/test/push.md +0 -119
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
allowed-tools: Bash, Read, Grep, Glob
|
|
3
|
+
description: 代码审查,检查变更是否符合项目规范
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## 代码审查
|
|
7
|
+
|
|
8
|
+
审查当前变更,核心原则:**用最少的代码实现功能**。
|
|
9
|
+
|
|
10
|
+
### 当前变更
|
|
11
|
+
|
|
12
|
+
!`git diff --stat HEAD`
|
|
13
|
+
|
|
14
|
+
### 审查清单
|
|
15
|
+
|
|
16
|
+
读取变更内容 `git diff HEAD`,逐条检查:
|
|
17
|
+
|
|
18
|
+
#### 1. 删除未使用的代码
|
|
19
|
+
- [ ] 未使用的函数、变量、导入
|
|
20
|
+
- [ ] 注释掉的代码
|
|
21
|
+
- [ ] 未使用的 TODO 注释
|
|
22
|
+
|
|
23
|
+
#### 2. 不要提前抽象
|
|
24
|
+
- [ ] 只用一次的代码不需要封装成函数
|
|
25
|
+
- [ ] 不要写 Factory、Manager、Helper 等过度抽象
|
|
26
|
+
- [ ] 三行重复代码比一个过早抽象更好
|
|
27
|
+
|
|
28
|
+
#### 3. 不要写"可能用到"的代码
|
|
29
|
+
- [ ] 只实现当前需求
|
|
30
|
+
- [ ] 不要预留"将来可能需要"的接口
|
|
31
|
+
- [ ] 不要添加未使用的配置项
|
|
32
|
+
|
|
33
|
+
#### 4. 错误处理要简洁
|
|
34
|
+
- [ ] 不要过度防御(检查不可能发生的情况)
|
|
35
|
+
- [ ] 让错误自然抛出,顶层统一处理
|
|
36
|
+
- [ ] 不要给每种错误类型都写 catch 分支
|
|
37
|
+
|
|
38
|
+
#### 5. 避免过度配置化
|
|
39
|
+
- [ ] 不要添加不会用到的选项
|
|
40
|
+
- [ ] 硬编码比过度灵活更好
|
|
41
|
+
- [ ] 一个功能只需要一种实现方式
|
|
42
|
+
|
|
43
|
+
#### 6. 依赖和结构
|
|
44
|
+
- [ ] 能用标准库就不引入新依赖
|
|
45
|
+
- [ ] 目录结构保持扁平
|
|
46
|
+
- [ ] 一个函数只做一件事
|
|
47
|
+
|
|
48
|
+
### 输出格式
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
## 审查结果
|
|
52
|
+
|
|
53
|
+
✅ 通过 / ⚠️ 需要修改
|
|
54
|
+
|
|
55
|
+
### 问题列表(如有)
|
|
56
|
+
|
|
57
|
+
1. `文件:行号` - 违反了什么原则 - 如何简化
|
|
58
|
+
|
|
59
|
+
### 可删除的代码
|
|
60
|
+
|
|
61
|
+
- `文件:行号` - 原因
|
|
62
|
+
```
|
package/README.md
CHANGED
|
@@ -10,11 +10,18 @@
|
|
|
10
10
|
|
|
11
11
|
✅ 自动下载飞书文档,转成 Markdown 格式
|
|
12
12
|
✅ 自动下载 Notion 页面,转成 Markdown 格式
|
|
13
|
+
✅ 按**版本 → 功能 → 文档类型**组织文档,AI 更容易理解
|
|
13
14
|
✅ 一条命令同步所有文档
|
|
14
15
|
✅ 文档更新后,重新运行命令即可获取最新内容
|
|
15
16
|
|
|
16
17
|
---
|
|
17
18
|
|
|
19
|
+
## 版本说明
|
|
20
|
+
|
|
21
|
+
⚠️ **每个版本都是破坏性更新**,不保证向后兼容。升级前请查看更新日志,必要时重新初始化配置文件。
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
18
25
|
## 安装步骤
|
|
19
26
|
|
|
20
27
|
### 第一步:安装 Node.js
|
|
@@ -127,29 +134,52 @@ growork init
|
|
|
127
134
|
```yaml
|
|
128
135
|
# 飞书配置(如果不用飞书,可以删除这部分)
|
|
129
136
|
feishu:
|
|
130
|
-
appId: "cli_你的AppID"
|
|
131
|
-
appSecret: "你的AppSecret"
|
|
137
|
+
appId: "cli_你的AppID"
|
|
138
|
+
appSecret: "你的AppSecret"
|
|
132
139
|
domain: "lark" # 国际版用 lark,国内版用 feishu
|
|
133
140
|
|
|
134
141
|
# Notion 配置(如果不用 Notion,可以删除这部分)
|
|
135
142
|
notion:
|
|
136
|
-
token: "ntn_你的Token"
|
|
137
|
-
|
|
138
|
-
#
|
|
139
|
-
docs
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
143
|
+
token: "ntn_你的Token"
|
|
144
|
+
|
|
145
|
+
# 输出目录(默认是 docs)
|
|
146
|
+
outputDir: "docs"
|
|
147
|
+
|
|
148
|
+
# 全局文档(不跟版本走的公共文档)
|
|
149
|
+
custom:
|
|
150
|
+
- "https://xxx.feishu.cn/docx/xxxxx" # 直接写链接
|
|
151
|
+
- url: "https://www.notion.so/xxxxx"
|
|
152
|
+
name: "技术架构" # 可选:自定义名称
|
|
153
|
+
|
|
154
|
+
# 版本化文档(按功能模块组织)
|
|
155
|
+
versions:
|
|
156
|
+
v1.0:
|
|
157
|
+
用户登录: # 功能名称
|
|
158
|
+
prd: # PRD 文档
|
|
159
|
+
- "https://xxx.feishu.cn/docx/xxxxx"
|
|
160
|
+
design: # 设计文档
|
|
161
|
+
- "https://xxx.feishu.cn/docx/yyyyy"
|
|
162
|
+
api: # 接口文档
|
|
163
|
+
- "https://xxx.feishu.cn/docx/zzzzz"
|
|
164
|
+
test: # 测试用例
|
|
165
|
+
- "https://www.notion.so/aaaaa"
|
|
166
|
+
|
|
167
|
+
# 简单功能可以不分类
|
|
168
|
+
小优化:
|
|
169
|
+
- "https://xxx.feishu.cn/docx/bbbbb"
|
|
151
170
|
```
|
|
152
171
|
|
|
172
|
+
**配置说明:**
|
|
173
|
+
|
|
174
|
+
| 区域 | 说明 |
|
|
175
|
+
| --- | --- |
|
|
176
|
+
| `custom` | 全局文档,不属于任何版本(如技术架构、编码规范) |
|
|
177
|
+
| `versions` | 按版本和功能组织的文档 |
|
|
178
|
+
| `prd` | 产品需求文档 |
|
|
179
|
+
| `design` | 设计稿/交互文档 |
|
|
180
|
+
| `api` | 接口/技术方案 |
|
|
181
|
+
| `test` | 测试用例 |
|
|
182
|
+
|
|
153
183
|
**如何获取文档链接?**
|
|
154
184
|
|
|
155
185
|
- **飞书**:打开文档,直接复制浏览器地址栏的链接
|
|
@@ -165,12 +195,31 @@ docs:
|
|
|
165
195
|
growork sync
|
|
166
196
|
```
|
|
167
197
|
|
|
168
|
-
|
|
198
|
+
同步完成后,文档会按以下结构保存:
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
docs/
|
|
202
|
+
├── custom/ # 全局文档
|
|
203
|
+
│ ├── 技术架构.md
|
|
204
|
+
│ └── 编码规范.md
|
|
205
|
+
└── v1.0/ # 版本目录
|
|
206
|
+
├── 用户登录/ # 功能目录
|
|
207
|
+
│ ├── prd/
|
|
208
|
+
│ │ └── 用户登录PRD.md
|
|
209
|
+
│ ├── design/
|
|
210
|
+
│ │ └── 登录页设计.md
|
|
211
|
+
│ └── api/
|
|
212
|
+
│ └── 登录接口文档.md
|
|
213
|
+
└── 小优化/
|
|
214
|
+
└── 优化说明.md
|
|
215
|
+
```
|
|
169
216
|
|
|
170
|
-
|
|
217
|
+
**只同步特定内容:**
|
|
171
218
|
|
|
172
219
|
```bash
|
|
173
|
-
growork sync
|
|
220
|
+
growork sync -c # 只同步全局文档
|
|
221
|
+
growork sync --ver v1.0 # 只同步 v1.0 版本
|
|
222
|
+
growork sync -f 用户登录 # 只同步「用户登录」功能
|
|
174
223
|
```
|
|
175
224
|
|
|
176
225
|
**查看所有已配置的文档:**
|
|
@@ -221,6 +270,28 @@ npx growork sync
|
|
|
221
270
|
|
|
222
271
|
---
|
|
223
272
|
|
|
273
|
+
### Q: 文件名是怎么生成的?
|
|
274
|
+
|
|
275
|
+
文件名会自动从文档标题获取,并处理特殊字符:
|
|
276
|
+
- `用户登录 / 注册流程 (v1.0)` → `用户登录-注册流程-v1.0.md`
|
|
277
|
+
|
|
278
|
+
你也可以在配置中用 `name` 字段自定义名称。
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## 命令速查
|
|
283
|
+
|
|
284
|
+
| 命令 | 说明 |
|
|
285
|
+
| --- | --- |
|
|
286
|
+
| `growork init` | 创建配置文件 |
|
|
287
|
+
| `growork sync` | 同步所有文档 |
|
|
288
|
+
| `growork sync -c` | 只同步全局文档 |
|
|
289
|
+
| `growork sync --ver v1.0` | 只同步指定版本 |
|
|
290
|
+
| `growork sync -f 功能名` | 只同步指定功能 |
|
|
291
|
+
| `growork list` | 查看文档列表 |
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
224
295
|
## 支持的文档类型
|
|
225
296
|
|
|
226
297
|
| 平台 | 支持的内容 |
|
|
@@ -234,7 +305,7 @@ npx growork sync
|
|
|
234
305
|
|
|
235
306
|
如果遇到问题,可以:
|
|
236
307
|
|
|
237
|
-
1. 在 GitHub 上提 Issue:https://github.com
|
|
308
|
+
1. 在 GitHub 上提 Issue:https://github.com/anthropics/growork/issues
|
|
238
309
|
2. 检查上面的「常见问题」部分
|
|
239
310
|
|
|
240
311
|
---
|
package/claude.md
CHANGED
|
@@ -1,4 +1,75 @@
|
|
|
1
|
-
# GroWork
|
|
1
|
+
# GroWork
|
|
2
|
+
|
|
3
|
+
无需关注test_export下的内容
|
|
4
|
+
|
|
5
|
+
## 项目简介
|
|
6
|
+
|
|
7
|
+
GroWork 是一个文档同步 CLI 工具,将飞书文档和 Notion 页面自动下载到本地,转为 Markdown 格式,便于 AI 工具(Claude、Cursor 等)读取。
|
|
8
|
+
|
|
9
|
+
## 项目结构
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
src/
|
|
13
|
+
├── index.ts # CLI 入口,使用 commander 定义命令
|
|
14
|
+
├── commands/
|
|
15
|
+
│ ├── init.ts # growork init - 初始化配置文件
|
|
16
|
+
│ ├── sync.ts # growork sync [name] - 同步文档
|
|
17
|
+
│ └── list.ts # growork list - 列出所有文档
|
|
18
|
+
├── services/
|
|
19
|
+
│ ├── feishu.ts # 飞书 API,转换飞书 block 为 Markdown
|
|
20
|
+
│ └── notion.ts # Notion API,使用 notion-to-md 库
|
|
21
|
+
└── utils/
|
|
22
|
+
└── config.ts # 配置文件解析和类型定义
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 技术栈
|
|
26
|
+
|
|
27
|
+
- **Runtime**: Node.js >= 18, ESM 模块
|
|
28
|
+
- **构建**: tsup
|
|
29
|
+
- **依赖**: commander (CLI), @larksuiteoapi/node-sdk (飞书), @notionhq/client + notion-to-md (Notion), yaml (配置), chalk (终端颜色)
|
|
30
|
+
|
|
31
|
+
## 配置文件
|
|
32
|
+
|
|
33
|
+
`growork.config.yaml` 结构:
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
feishu:
|
|
37
|
+
appId: "cli_xxx"
|
|
38
|
+
appSecret: "xxx"
|
|
39
|
+
domain: "lark" # lark(国际版) 或 feishu(国内版)
|
|
40
|
+
|
|
41
|
+
notion:
|
|
42
|
+
token: "ntn_xxx"
|
|
43
|
+
|
|
44
|
+
docs:
|
|
45
|
+
- name: prd # 唯一标识
|
|
46
|
+
type: feishu # feishu | notion | notion-database
|
|
47
|
+
url: "https://..."
|
|
48
|
+
output: "docs/prd.md"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 常用命令
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm run build # 构建
|
|
55
|
+
npm run dev # 开发模式
|
|
56
|
+
node dist/index.js sync [name] # 同步文档
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 关键类型
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
interface DocConfig {
|
|
63
|
+
name: string;
|
|
64
|
+
type: 'feishu' | 'notion' | 'notion-database';
|
|
65
|
+
url: string;
|
|
66
|
+
output: string;
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
# 架构原则
|
|
2
73
|
|
|
3
74
|
## 核心原则:用最少的代码实现功能
|
|
4
75
|
|
package/dist/index.js
CHANGED
|
@@ -19,7 +19,80 @@ function getConfigPath() {
|
|
|
19
19
|
function configExists() {
|
|
20
20
|
return fs.existsSync(getConfigPath());
|
|
21
21
|
}
|
|
22
|
-
function
|
|
22
|
+
function inferDocType(url) {
|
|
23
|
+
if (url.includes("feishu.cn") || url.includes("larksuite.com")) {
|
|
24
|
+
return "feishu";
|
|
25
|
+
}
|
|
26
|
+
if (url.includes("notion.so") || url.includes("notion.site")) {
|
|
27
|
+
return "notion";
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`\u65E0\u6CD5\u4ECE URL \u63A8\u65AD\u6587\u6863\u7C7B\u578B: ${url}`);
|
|
30
|
+
}
|
|
31
|
+
function sanitizeFileName(title) {
|
|
32
|
+
return title.replace(/[\/\\:*?"<>|]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
33
|
+
}
|
|
34
|
+
function parseDocInput(input) {
|
|
35
|
+
if (typeof input === "string") {
|
|
36
|
+
return { url: input };
|
|
37
|
+
}
|
|
38
|
+
return input;
|
|
39
|
+
}
|
|
40
|
+
function isTypedFeature(value) {
|
|
41
|
+
return !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
function normalizeConfig(config, options = {}) {
|
|
44
|
+
const docs = [];
|
|
45
|
+
const outputDir = config.outputDir || "docs";
|
|
46
|
+
if (config.custom && (options.custom || !options.version && !options.feature)) {
|
|
47
|
+
for (const input of config.custom) {
|
|
48
|
+
const { url, name } = parseDocInput(input);
|
|
49
|
+
docs.push({
|
|
50
|
+
url,
|
|
51
|
+
name,
|
|
52
|
+
type: inferDocType(url),
|
|
53
|
+
outputPath: `${outputDir}/custom/{title}.md`
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (options.custom) {
|
|
58
|
+
return docs;
|
|
59
|
+
}
|
|
60
|
+
if (config.versions) {
|
|
61
|
+
for (const [version, features] of Object.entries(config.versions)) {
|
|
62
|
+
if (options.version && options.version !== version) continue;
|
|
63
|
+
for (const [feature, value] of Object.entries(features)) {
|
|
64
|
+
if (options.feature && options.feature !== feature) continue;
|
|
65
|
+
if (isTypedFeature(value)) {
|
|
66
|
+
for (const docType of ["prd", "design", "api", "test"]) {
|
|
67
|
+
const docInputs = value[docType];
|
|
68
|
+
if (!docInputs) continue;
|
|
69
|
+
for (const input of docInputs) {
|
|
70
|
+
const { url, name } = parseDocInput(input);
|
|
71
|
+
docs.push({
|
|
72
|
+
url,
|
|
73
|
+
name,
|
|
74
|
+
type: inferDocType(url),
|
|
75
|
+
outputPath: `${outputDir}/${version}/${feature}/${docType}/{title}.md`
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
for (const input of value) {
|
|
81
|
+
const { url, name } = parseDocInput(input);
|
|
82
|
+
docs.push({
|
|
83
|
+
url,
|
|
84
|
+
name,
|
|
85
|
+
type: inferDocType(url),
|
|
86
|
+
outputPath: `${outputDir}/${version}/${feature}/{title}.md`
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return docs;
|
|
94
|
+
}
|
|
95
|
+
function loadConfigV2() {
|
|
23
96
|
const configPath = getConfigPath();
|
|
24
97
|
if (!fs.existsSync(configPath)) {
|
|
25
98
|
throw new Error(`\u914D\u7F6E\u6587\u4EF6\u4E0D\u5B58\u5728: ${configPath}
|
|
@@ -27,21 +100,13 @@ function loadConfig() {
|
|
|
27
100
|
}
|
|
28
101
|
const content = fs.readFileSync(configPath, "utf-8");
|
|
29
102
|
const config = yaml.parse(content);
|
|
30
|
-
if (!config.
|
|
31
|
-
throw new Error("\u914D\u7F6E\u6587\u4EF6\u4E2D\u6CA1\u6709\u914D\u7F6E\u4EFB\u4F55\u6587\u6863");
|
|
32
|
-
}
|
|
33
|
-
const hasFeishuDocs = config.docs.some((d) => d.type === "feishu");
|
|
34
|
-
const hasNotionDocs = config.docs.some((d) => d.type === "notion");
|
|
35
|
-
if (hasFeishuDocs && (!config.feishu?.appId || !config.feishu?.appSecret)) {
|
|
36
|
-
throw new Error("\u914D\u7F6E\u6587\u4EF6\u7F3A\u5C11\u98DE\u4E66\u51ED\u8BC1 (feishu.appId, feishu.appSecret)");
|
|
37
|
-
}
|
|
38
|
-
if (hasNotionDocs && !config.notion?.token) {
|
|
39
|
-
throw new Error("\u914D\u7F6E\u6587\u4EF6\u7F3A\u5C11 Notion \u51ED\u8BC1 (notion.token)");
|
|
103
|
+
if (!config.custom && !config.versions) {
|
|
104
|
+
throw new Error("\u914D\u7F6E\u6587\u4EF6\u4E2D\u6CA1\u6709\u914D\u7F6E\u4EFB\u4F55\u6587\u6863\uFF08custom \u6216 versions\uFF09");
|
|
40
105
|
}
|
|
41
106
|
return config;
|
|
42
107
|
}
|
|
43
108
|
function getDefaultConfig() {
|
|
44
|
-
return `# Growork \u914D\u7F6E\u6587\u4EF6
|
|
109
|
+
return `# Growork v2.0 \u914D\u7F6E\u6587\u4EF6
|
|
45
110
|
|
|
46
111
|
# \u98DE\u4E66\u5E94\u7528\u51ED\u8BC1 (\u4F7F\u7528\u98DE\u4E66\u6587\u6863\u65F6\u9700\u8981)
|
|
47
112
|
feishu:
|
|
@@ -53,29 +118,37 @@ feishu:
|
|
|
53
118
|
notion:
|
|
54
119
|
token: "ntn_xxxx" # Notion Integration Token
|
|
55
120
|
|
|
56
|
-
# \
|
|
57
|
-
docs
|
|
58
|
-
# \u98DE\u4E66\u6587\u6863\u793A\u4F8B
|
|
59
|
-
- name: prd
|
|
60
|
-
type: feishu
|
|
61
|
-
url: "https://xxx.feishu.cn/docx/xxxxx"
|
|
62
|
-
output: "docs/product/prd.md"
|
|
121
|
+
# \u8F93\u51FA\u6839\u76EE\u5F55\uFF08\u9ED8\u8BA4 "docs"\uFF09
|
|
122
|
+
outputDir: "docs"
|
|
63
123
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
124
|
+
# \u5168\u5C40\u6587\u6863\uFF08\u4E0D\u8DDF\u7248\u672C\uFF09
|
|
125
|
+
custom:
|
|
126
|
+
- "https://xxx.feishu.cn/docx/xxxxx" # \u6700\u7B80\u5199\u6CD5
|
|
127
|
+
# - url: "https://www.notion.so/xxxxx"
|
|
128
|
+
# name: "\u6280\u672F\u67B6\u6784" # \u53EF\u9009\uFF1A\u81EA\u5B9A\u4E49\u540D\u79F0
|
|
129
|
+
|
|
130
|
+
# \u7248\u672C\u5316\u6587\u6863
|
|
131
|
+
versions:
|
|
132
|
+
v1.0:
|
|
133
|
+
\u7528\u6237\u767B\u5F55:
|
|
134
|
+
prd:
|
|
135
|
+
- "https://xxx.feishu.cn/docx/xxxxx"
|
|
136
|
+
# design:
|
|
137
|
+
# - "https://xxx.feishu.cn/docx/yyyyy"
|
|
138
|
+
# api:
|
|
139
|
+
# - "https://xxx.feishu.cn/docx/zzzzz"
|
|
140
|
+
# test:
|
|
141
|
+
# - "https://xxx.feishu.cn/docx/aaaaa"
|
|
142
|
+
|
|
143
|
+
# \u7B80\u5355 feature \u53EF\u4E0D\u5206\u7C7B
|
|
144
|
+
# \u5C0F\u4F18\u5316:
|
|
145
|
+
# - "https://xxx.feishu.cn/docx/bbbbb"
|
|
69
146
|
`;
|
|
70
147
|
}
|
|
71
148
|
|
|
72
149
|
// src/commands/init.ts
|
|
73
150
|
var DIRS_TO_CREATE = [
|
|
74
|
-
"docs/
|
|
75
|
-
"docs/design",
|
|
76
|
-
"docs/api",
|
|
77
|
-
"docs/tech",
|
|
78
|
-
"docs/test"
|
|
151
|
+
"docs/custom"
|
|
79
152
|
];
|
|
80
153
|
async function initCommand() {
|
|
81
154
|
console.log(chalk.blue("\u{1F4C1} \u521D\u59CB\u5316 Growork \u9879\u76EE...\n"));
|
|
@@ -593,89 +666,130 @@ function clearLine() {
|
|
|
593
666
|
console.log("");
|
|
594
667
|
}
|
|
595
668
|
}
|
|
596
|
-
|
|
597
|
-
const
|
|
669
|
+
function extractTitleFromMarkdown(content) {
|
|
670
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
671
|
+
return match ? match[1].trim() : "\u672A\u547D\u540D\u6587\u6863";
|
|
672
|
+
}
|
|
673
|
+
async function syncCommand(options = {}) {
|
|
674
|
+
const config = loadConfigV2();
|
|
675
|
+
const docs = normalizeConfig(config, options);
|
|
676
|
+
if (docs.length === 0) {
|
|
677
|
+
console.log(chalk2.yellow("\u26A0\uFE0F \u6CA1\u6709\u627E\u5230\u5339\u914D\u7684\u6587\u6863"));
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
598
680
|
const feishuService = config.feishu ? new FeishuService(config.feishu) : null;
|
|
599
681
|
const notionService = config.notion ? new NotionService(config.notion) : null;
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
}
|
|
608
|
-
docsToSync = [doc];
|
|
682
|
+
const hasFeishuDocs = docs.some((d) => d.type === "feishu");
|
|
683
|
+
const hasNotionDocs = docs.some((d) => d.type === "notion");
|
|
684
|
+
if (hasFeishuDocs && !feishuService) {
|
|
685
|
+
throw new Error("\u914D\u7F6E\u6587\u4EF6\u7F3A\u5C11\u98DE\u4E66\u51ED\u8BC1 (feishu.appId, feishu.appSecret)");
|
|
686
|
+
}
|
|
687
|
+
if (hasNotionDocs && !notionService) {
|
|
688
|
+
throw new Error("\u914D\u7F6E\u6587\u4EF6\u7F3A\u5C11 Notion \u51ED\u8BC1 (notion.token)");
|
|
609
689
|
}
|
|
610
690
|
console.log(chalk2.blue("\u{1F4C4} \u5F00\u59CB\u540C\u6B65\u6587\u6863...\n"));
|
|
611
691
|
let successCount = 0;
|
|
612
|
-
for (const doc of
|
|
613
|
-
|
|
692
|
+
for (const doc of docs) {
|
|
693
|
+
const displayName = doc.name || doc.url.slice(-20);
|
|
694
|
+
process.stdout.write(chalk2.gray(` \u23F3 ${displayName}`));
|
|
614
695
|
try {
|
|
615
696
|
let content;
|
|
616
697
|
if (doc.type === "feishu") {
|
|
617
|
-
if (!feishuService) throw new Error("\u98DE\u4E66\u670D\u52A1\u672A\u914D\u7F6E");
|
|
618
698
|
content = await feishuService.getDocumentAsMarkdown(doc.url);
|
|
619
|
-
} else if (doc.type === "notion") {
|
|
620
|
-
if (!notionService) throw new Error("Notion \u670D\u52A1\u672A\u914D\u7F6E");
|
|
621
|
-
content = await notionService.getPageAsMarkdown(doc.url);
|
|
622
699
|
} else {
|
|
623
|
-
|
|
700
|
+
content = await notionService.getPageAsMarkdown(doc.url);
|
|
624
701
|
}
|
|
625
|
-
const
|
|
702
|
+
const title = doc.name || extractTitleFromMarkdown(content);
|
|
703
|
+
const safeTitle = sanitizeFileName(title);
|
|
704
|
+
const outputPath = path3.join(process.cwd(), doc.outputPath.replace("{title}", safeTitle));
|
|
626
705
|
const outputDir = path3.dirname(outputPath);
|
|
627
706
|
if (!fs3.existsSync(outputDir)) {
|
|
628
707
|
fs3.mkdirSync(outputDir, { recursive: true });
|
|
629
708
|
}
|
|
630
709
|
fs3.writeFileSync(outputPath, content, "utf-8");
|
|
631
710
|
clearLine();
|
|
632
|
-
console.log(chalk2.green(` \u2713 ${
|
|
711
|
+
console.log(chalk2.green(` \u2713 ${safeTitle.padEnd(25)} \u2192 ${path3.relative(process.cwd(), outputPath)}`));
|
|
633
712
|
successCount++;
|
|
634
713
|
} catch (error) {
|
|
635
714
|
clearLine();
|
|
636
715
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
637
|
-
console.log(chalk2.red(` \u2717 ${
|
|
716
|
+
console.log(chalk2.red(` \u2717 ${displayName.padEnd(25)} \u2192 ${errorMessage}`));
|
|
638
717
|
}
|
|
639
718
|
}
|
|
640
719
|
console.log("");
|
|
641
|
-
if (successCount ===
|
|
642
|
-
console.log(chalk2.green(`\u2705 \u540C\u6B65\u5B8C\u6210\uFF0C\u5171 ${
|
|
720
|
+
if (successCount === docs.length) {
|
|
721
|
+
console.log(chalk2.green(`\u2705 \u540C\u6B65\u5B8C\u6210\uFF0C\u5171 ${docs.length} \u4E2A\u6587\u6863`));
|
|
643
722
|
} else {
|
|
644
|
-
console.log(chalk2.yellow(`\u26A0\uFE0F \u540C\u6B65\u5B8C\u6210: ${successCount}/${
|
|
723
|
+
console.log(chalk2.yellow(`\u26A0\uFE0F \u540C\u6B65\u5B8C\u6210: ${successCount}/${docs.length} \u4E2A\u6587\u6863\u6210\u529F`));
|
|
645
724
|
}
|
|
646
725
|
}
|
|
647
726
|
|
|
648
727
|
// src/commands/list.ts
|
|
649
|
-
import * as fs4 from "fs";
|
|
650
|
-
import * as path4 from "path";
|
|
651
728
|
import chalk3 from "chalk";
|
|
729
|
+
function shortenUrl(url, maxLen = 40) {
|
|
730
|
+
if (url.length <= maxLen) return url;
|
|
731
|
+
return url.slice(0, maxLen - 3) + "...";
|
|
732
|
+
}
|
|
652
733
|
async function listCommand() {
|
|
653
734
|
if (!configExists()) {
|
|
654
735
|
console.log(chalk3.red("\u274C \u914D\u7F6E\u6587\u4EF6\u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u8FD0\u884C growork init"));
|
|
655
736
|
process.exit(1);
|
|
656
737
|
}
|
|
657
|
-
const config =
|
|
658
|
-
const cwd = process.cwd();
|
|
738
|
+
const config = loadConfigV2();
|
|
659
739
|
console.log(chalk3.blue("\u{1F4CB} \u6587\u6863\u5217\u8868\n"));
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
console.log(
|
|
740
|
+
let totalCount = 0;
|
|
741
|
+
if (config.custom && config.custom.length > 0) {
|
|
742
|
+
console.log(chalk3.cyan("\u{1F4C1} custom (\u5168\u5C40\u6587\u6863)"));
|
|
743
|
+
for (const input of config.custom) {
|
|
744
|
+
const { url, name } = parseDocInput(input);
|
|
745
|
+
const displayName = name || shortenUrl(url);
|
|
746
|
+
console.log(chalk3.gray(` - ${displayName}`));
|
|
747
|
+
totalCount++;
|
|
748
|
+
}
|
|
749
|
+
console.log("");
|
|
670
750
|
}
|
|
671
|
-
|
|
672
|
-
|
|
751
|
+
if (config.versions) {
|
|
752
|
+
for (const [version, features] of Object.entries(config.versions)) {
|
|
753
|
+
console.log(chalk3.cyan(`\u{1F4C1} ${version}`));
|
|
754
|
+
for (const [feature, value] of Object.entries(features)) {
|
|
755
|
+
if (Array.isArray(value)) {
|
|
756
|
+
console.log(chalk3.white(` \u2514\u2500 ${feature}`));
|
|
757
|
+
for (const input of value) {
|
|
758
|
+
const { url, name } = parseDocInput(input);
|
|
759
|
+
const displayName = name || shortenUrl(url, 30);
|
|
760
|
+
console.log(chalk3.gray(` - ${displayName}`));
|
|
761
|
+
totalCount++;
|
|
762
|
+
}
|
|
763
|
+
} else {
|
|
764
|
+
console.log(chalk3.white(` \u2514\u2500 ${feature}`));
|
|
765
|
+
for (const docType of ["prd", "design", "api", "test"]) {
|
|
766
|
+
const docInputs = value[docType];
|
|
767
|
+
if (!docInputs || docInputs.length === 0) continue;
|
|
768
|
+
console.log(chalk3.yellow(` ${docType}:`));
|
|
769
|
+
for (const input of docInputs) {
|
|
770
|
+
const { url, name } = parseDocInput(input);
|
|
771
|
+
const displayName = name || shortenUrl(url, 25);
|
|
772
|
+
console.log(chalk3.gray(` - ${displayName}`));
|
|
773
|
+
totalCount++;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
console.log("");
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
console.log(chalk3.gray(`\u5171 ${totalCount} \u4E2A\u6587\u6863\u914D\u7F6E`));
|
|
673
782
|
}
|
|
674
783
|
|
|
675
784
|
// src/index.ts
|
|
676
785
|
var program = new Command();
|
|
677
|
-
program.name("growork").description("\u5C06\u98DE\u4E66\u6587\u6863\u540C\u6B65\u5230\u672C\u5730\uFF0C\u4E3A AI Agent \u63D0\u4F9B\u5B8C\u6574\u4E0A\u4E0B\u6587").version("
|
|
786
|
+
program.name("growork").description("\u5C06\u98DE\u4E66\u6587\u6863\u540C\u6B65\u5230\u672C\u5730\uFF0C\u4E3A AI Agent \u63D0\u4F9B\u5B8C\u6574\u4E0A\u4E0B\u6587").version("2.0.0");
|
|
678
787
|
program.command("init").description("\u521D\u59CB\u5316 Growork \u914D\u7F6E\u548C\u76EE\u5F55\u7ED3\u6784").action(initCommand);
|
|
679
|
-
program.command("sync
|
|
788
|
+
program.command("sync").description("\u540C\u6B65\u6587\u6863").option("--ver <version>", "\u53EA\u540C\u6B65\u6307\u5B9A\u7248\u672C").option("-f, --feature <feature>", "\u53EA\u540C\u6B65\u6307\u5B9A feature").option("-c, --custom", "\u53EA\u540C\u6B65\u5168\u5C40\u6587\u6863").action((options) => {
|
|
789
|
+
if (options.ver) {
|
|
790
|
+
options.version = options.ver;
|
|
791
|
+
}
|
|
792
|
+
syncCommand(options);
|
|
793
|
+
});
|
|
680
794
|
program.command("list").description("\u5217\u51FA\u6240\u6709\u914D\u7F6E\u7684\u6587\u6863").action(listCommand);
|
|
681
795
|
program.parse();
|