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.
@@ -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" # 替换成你的 App ID
131
- appSecret: "你的AppSecret" # 替换成你的 App Secret
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" # 替换成你的 Token
137
-
138
- # 要同步的文档列表
139
- docs:
140
- # 飞书文档示例
141
- - name: prd # 给文档起个名字(英文,方便记忆)
142
- type: feishu # 类型:feishu
143
- url: "https://xxx.larksuite.com/wiki/xxxxx" # 文档链接
144
- output: "docs/prd.md" # 保存到哪里
145
-
146
- # Notion 页面示例
147
- - name: design # 给文档起个名字
148
- type: notion # 类型:notion
149
- url: "https://notion.so/xxxxx" # 页面链接
150
- output: "docs/design.md" # 保存到哪里
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
- 同步完成后,文档会保存到你配置的 `output` 路径。
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 prd
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/你的仓库/issues
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 loadConfig() {
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.docs || config.docs.length === 0) {
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
- # \u6587\u6863\u540C\u6B65\u914D\u7F6E
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
- # Notion \u6587\u6863\u793A\u4F8B
65
- - name: notion-prd
66
- type: notion
67
- url: "https://www.notion.so/xxxxx"
68
- output: "docs/product/notion-prd.md"
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/product",
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
- async function syncCommand(docName) {
597
- const config = loadConfig();
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
- let docsToSync = config.docs;
601
- if (docName) {
602
- const doc = config.docs.find((d) => d.name === docName);
603
- if (!doc) {
604
- console.log(chalk2.red(`\u274C \u672A\u627E\u5230\u540D\u4E3A "${docName}" \u7684\u6587\u6863\u914D\u7F6E`));
605
- console.log(chalk2.gray(`\u53EF\u7528\u7684\u6587\u6863: ${config.docs.map((d) => d.name).join(", ")}`));
606
- process.exit(1);
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 docsToSync) {
613
- process.stdout.write(chalk2.gray(` \u23F3 ${doc.name.padEnd(15)} \u2192 ${doc.output}`));
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
- throw new Error(`\u4E0D\u652F\u6301\u7684\u6587\u6863\u7C7B\u578B: ${doc.type}`);
700
+ content = await notionService.getPageAsMarkdown(doc.url);
624
701
  }
625
- const outputPath = path3.join(process.cwd(), doc.output);
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 ${doc.name.padEnd(15)} \u2192 ${doc.output}`));
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 ${doc.name.padEnd(15)} \u2192 ${errorMessage}`));
716
+ console.log(chalk2.red(` \u2717 ${displayName.padEnd(25)} \u2192 ${errorMessage}`));
638
717
  }
639
718
  }
640
719
  console.log("");
641
- if (successCount === docsToSync.length) {
642
- console.log(chalk2.green(`\u2705 \u540C\u6B65\u5B8C\u6210\uFF0C\u5171 ${docsToSync.length} \u4E2A\u6587\u6863`));
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}/${docsToSync.length} \u4E2A\u6587\u6863\u6210\u529F`));
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 = loadConfig();
658
- const cwd = process.cwd();
738
+ const config = loadConfigV2();
659
739
  console.log(chalk3.blue("\u{1F4CB} \u6587\u6863\u5217\u8868\n"));
660
- console.log(chalk3.gray(" \u540D\u79F0".padEnd(18) + "\u7C7B\u578B".padEnd(10) + "\u8F93\u51FA\u8DEF\u5F84".padEnd(30) + "\u72B6\u6001"));
661
- console.log(chalk3.gray(" " + "-".repeat(70)));
662
- for (const doc of config.docs) {
663
- const outputPath = path4.join(cwd, doc.output);
664
- const exists = fs4.existsSync(outputPath);
665
- const status = exists ? chalk3.green("\u5DF2\u540C\u6B65") : chalk3.yellow("\u672A\u540C\u6B65");
666
- const name = ` ${doc.name}`.padEnd(18);
667
- const type = doc.type.padEnd(10);
668
- const output = doc.output.padEnd(30);
669
- console.log(`${name}${chalk3.gray(type)}${output}${status}`);
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
- console.log("");
672
- console.log(chalk3.gray(`\u5171 ${config.docs.length} \u4E2A\u6587\u6863\u914D\u7F6E`));
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("1.0.0");
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 [name]").description("\u540C\u6B65\u6587\u6863\uFF0C\u53EF\u9009\u6307\u5B9A\u6587\u6863\u540D\u79F0").action(syncCommand);
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();