@aigne/doc-smith 0.8.11-beta.2 → 0.8.11-beta.4

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.
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.8.11-beta.2"
2
+ ".": "0.8.11-beta.4"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.11-beta.4](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.8.11-beta.3...v0.8.11-beta.4) (2025-09-30)
4
+
5
+
6
+ ### Features
7
+
8
+ * add check-only option for agents with selection input ([#147](https://github.com/AIGNE-io/aigne-doc-smith/issues/147)) ([3340988](https://github.com/AIGNE-io/aigne-doc-smith/commit/3340988e67ffef7e1087d560930b3cf98c860693))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * improve error handling in choose-docs utility ([#145](https://github.com/AIGNE-io/aigne-doc-smith/issues/145)) ([7052ffc](https://github.com/AIGNE-io/aigne-doc-smith/commit/7052ffc106817144bff815422dced7faa4dfc3e8))
14
+ * translation prompt tuned; translate comments only in code blocks ([#149](https://github.com/AIGNE-io/aigne-doc-smith/issues/149)) ([30ce2c0](https://github.com/AIGNE-io/aigne-doc-smith/commit/30ce2c0e43c01b6588b4ada84aafecfd83c1b23b))
15
+
16
+ ## [0.8.11-beta.3](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.8.11-beta.2...v0.8.11-beta.3) (2025-09-29)
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * add evaluation agents cil entry ([#143](https://github.com/AIGNE-io/aigne-doc-smith/issues/143)) ([016096a](https://github.com/AIGNE-io/aigne-doc-smith/commit/016096af6c0cec0b86fc538e1f0b7b42e928451b))
22
+
3
23
  ## [0.8.11-beta.2](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.8.11-beta.1...v0.8.11-beta.2) (2025-09-27)
4
24
 
5
25
 
@@ -7,6 +7,7 @@ skills:
7
7
  - url: ../init/index.mjs
8
8
  default_input:
9
9
  skipIfExists: true
10
+ checkOnly: true
10
11
  - ./choose-contents.mjs
11
12
  input_schema:
12
13
  type: object
@@ -5,6 +5,7 @@ skills:
5
5
  - url: ../init/index.mjs
6
6
  default_input:
7
7
  skipIfExists: true
8
+ checkOnly: true
8
9
  - ../utils/load-sources.mjs
9
10
  - ../utils/format-document-structure.mjs
10
11
  - type: transform
@@ -32,9 +32,32 @@ const _PRESS_ENTER_TO_FINISH = "Press Enter to finish";
32
32
  * @returns {Promise<Object>}
33
33
  */
34
34
  export default async function init(
35
- { outputPath = ".aigne/doc-smith", fileName = "config.yaml", skipIfExists = false, appUrl },
35
+ {
36
+ outputPath = ".aigne/doc-smith",
37
+ fileName = "config.yaml",
38
+ skipIfExists = false,
39
+ appUrl,
40
+ checkOnly = false,
41
+ },
36
42
  options,
37
43
  ) {
44
+ // Check if we're in checkOnly mode
45
+ if (checkOnly) {
46
+ const filePath = join(outputPath, fileName);
47
+ const configContent = await readFile(filePath, "utf8").catch(() => null);
48
+
49
+ if (!configContent || configContent.trim() === "") {
50
+ console.log(`⚠️ No configuration found.`);
51
+ console.log(
52
+ `🚀 Run ${chalk.cyan("aigne doc init")} to set up your documentation configuration.`,
53
+ );
54
+ process.exit(0);
55
+ }
56
+
57
+ // Config exists, load and return it
58
+ return loadConfig({ config: filePath, appUrl });
59
+ }
60
+
38
61
  if (skipIfExists) {
39
62
  const filePath = join(outputPath, fileName);
40
63
  const configContent = await readFile(filePath, "utf8").catch(() => null);
@@ -8,6 +8,7 @@ skills:
8
8
  - url: ../init/index.mjs
9
9
  default_input:
10
10
  skipIfExists: true
11
+ checkOnly: true
11
12
  - publish-docs.mjs
12
13
  input_schema:
13
14
  type: object
@@ -5,6 +5,7 @@ skills:
5
5
  - url: ../init/index.mjs
6
6
  default_input:
7
7
  skipIfExists: true
8
+ checkOnly: true
8
9
  - ../utils/load-sources.mjs
9
10
  - type: transform
10
11
  task_render_mode: hide
@@ -7,6 +7,7 @@ skills:
7
7
  - url: ../init/index.mjs
8
8
  default_input:
9
9
  skipIfExists: true
10
+ checkOnly: true
10
11
  - ../utils/load-sources.mjs
11
12
  - type: transform
12
13
  task_render_mode: hide
@@ -1,6 +1,7 @@
1
1
  type: team
2
2
  name: updateSingleDocument
3
3
  skills:
4
+ - ../utils/transform-detail-datasources.mjs
4
5
  - ../update/user-review-document.mjs
5
6
  - type: transform
6
7
  task_render_mode: hide
@@ -34,18 +34,33 @@ export default async function chooseDocs(
34
34
  );
35
35
 
36
36
  if (mainLanguageFiles.length === 0) {
37
- throw new Error("No documents found in the docs directory");
37
+ throw new Error(
38
+ "No documents found in the docs directory. Please run `aigne docs generate` to generate the documents",
39
+ );
38
40
  }
39
41
 
42
+ // Convert files to choices with titles
43
+ const choices = mainLanguageFiles.map((file) => {
44
+ // Convert filename to flat path to find corresponding document structure item
45
+ const flatName = file.replace(/\.md$/, "").replace(/\.\w+(-\w+)?$/, "");
46
+ const docItem = documentExecutionStructure.find((item) => {
47
+ const itemFlattenedPath = item.path.replace(/^\//, "").replace(/\//g, "-");
48
+ return itemFlattenedPath === flatName;
49
+ });
50
+
51
+ // Use title if available, otherwise fall back to filename
52
+ const displayName = docItem?.title || file;
53
+
54
+ return {
55
+ name: displayName,
56
+ value: file,
57
+ };
58
+ });
59
+
40
60
  // Let user select multiple files
41
61
  selectedFiles = await options.prompts.checkbox({
42
62
  message: getActionText(isTranslate, "Select documents to {action}:"),
43
63
  source: (term) => {
44
- const choices = mainLanguageFiles.map((file) => ({
45
- name: file,
46
- value: file,
47
- }));
48
-
49
64
  if (!term) return choices;
50
65
 
51
66
  return choices.filter((choice) => choice.name.toLowerCase().includes(term.toLowerCase()));
@@ -65,12 +80,8 @@ export default async function chooseDocs(
65
80
  // Process selected files and convert to found items
66
81
  foundItems = await processSelectedFiles(selectedFiles, documentExecutionStructure, docsDir);
67
82
  } catch (error) {
68
- console.error(error);
69
83
  throw new Error(
70
- getActionText(
71
- isTranslate,
72
- "Please provide a docs parameter to specify which documents to {action}",
73
- ),
84
+ getActionText(isTranslate, `\nFailed to select documents to {action}: ${error.message}`),
74
85
  );
75
86
  }
76
87
  } else {
package/aigne.yaml CHANGED
@@ -79,6 +79,10 @@ agents:
79
79
 
80
80
  # Evaluation
81
81
  - ./agents/evaluate/index.yaml
82
+ - ./agents/evaluate/generate-report.mjs
83
+ - ./agents/evaluate/document-structure.yaml
84
+ - ./agents/evaluate/document.yaml
85
+ - ./agents/evaluate/code-snippet.mjs
82
86
  cli:
83
87
  chat: ./agents/chat/index.yaml
84
88
  agents:
@@ -89,6 +93,7 @@ cli:
89
93
  - ./agents/translate/index.yaml
90
94
  - ./agents/clear/index.yaml
91
95
  - ./agents/prefs/index.mjs
96
+ - ./agents/evaluate/index.yaml
92
97
  mcp_server:
93
98
  agents:
94
99
  - ./docs-mcp/get-docs-structure.mjs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/doc-smith",
3
- "version": "0.8.11-beta.2",
3
+ "version": "0.8.11-beta.4",
4
4
  "description": "AI-driven documentation generation tool built on the AIGNE Framework",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -95,7 +95,7 @@ parentId: {{parentId}}
95
95
  </current_document>
96
96
 
97
97
  <datasources>
98
- {{ datasources }}
98
+ {{ detailDataSources }}
99
99
 
100
100
  {{ additionalInformation }}
101
101
 
@@ -0,0 +1,16 @@
1
+ <code_block_rules>
2
+ The following formats are considered Code Blocks:
3
+ - Wrapped with ```
4
+ - Supports configurations: language, title, icon, where title and icon are optional
5
+ - content can be code, command line examples, text or any other content
6
+
7
+ <code_block_sample>
8
+ ```{language} [{title}] [icon={icon}]
9
+ {content}
10
+ ```
11
+ </code_block_sample>
12
+
13
+ Code Block Translation:
14
+ - For D2 code blocks, only translate labels
15
+ - For other language code blocks, **only translate comments starting with #, keep all other content unchanged without translation**
16
+ </code_block_rules>
@@ -1,26 +1,26 @@
1
1
  <role_and_goal>
2
- You are a professional translator proficient in multiple languages, skilled in accurate and standardized bilingual conversion.
2
+
3
+ You are an **Elite Polyglot Localization and Translation Specialist** with extensive professional experience across multiple domains. Your core mission is to produce translations that are not only **100% accurate** to the source meaning but are also **natively fluent, highly readable, and culturally appropriate** in the target language.
4
+
5
+ Core Mandates:
6
+
7
+ 1. Semantic Fidelity (Accuracy): The translation must perfectly and comprehensively convey the **entire meaning, tone, and nuance** of the source text. **No omission, addition, or distortion of the original content** is permitted.
8
+ 2. Native Fluency and Style: The resulting text must adhere strictly to the target language's **grammar, syntax, and idiomatic expressions**. The translation must **sound like it was originally written by a native speaker**, completely **free of grammatical errors** or "translationese" (literal, stiff, or unnatural phrasing).
9
+ 3. Readability and Flow: The final output must be **smooth, logical, and highly readable**. Sentences must flow naturally, ensuring a pleasant and coherent reading experience for the target audience.
10
+ 4. Localization and Clarity: Where a **literal (word-for-word) translation** of a term, phrase, or idiom would be **uncommon, confusing, or ambiguous** in the target language, you must apply **localization best practices**. This means translating the **concept** into the most **idiomatic, common, and easily understandable expression** in the target language.
11
+ 5. Versatility and Scope: You are proficient in handling **any pair of requested languages** (e.g., Chinese $\leftrightarrow$ English, English $\leftrightarrow$ Japanese) and are adept at translating diverse **document types**, including but not limited to: **Technical Manuals, Business Reports, Marketing Copy/Ads, Legal Documents, Academic Papers, and General Correspondence.**
3
12
  </role_and_goal>
4
13
 
5
14
  <translation_rules>
6
15
  Translation Requirements:
7
16
 
8
- - **Accurate Conveyance**: Accurately convey the facts and context of the original text, ensuring complete coverage.
9
- - **Avoid Exaggeration**: Avoid using emotionally charged or subjective words (for example, "excited" or "shocked").
10
- - **Follow Language Standards**: Ensure correct punctuation and grammar, and express ideas naturally and fluently.
11
- - **Preserve Original Structure**: Translate only the content portions without modifying tags or introducing any extra content or punctuation. Do not add markdown syntax at the outermost level. Ensure the translated structure matches the original, preserving line breaks and blank lines from the source.
12
- - **Strictly Protect Markdown Syntax**: All Markdown syntax characters, including but not limited to `|` and `-` in tables, `*` and `-` in lists, `#` in headings, `` ` `` in code blocks, etc., must be **copied exactly**, with no modification, addition, deletion, or merging. Table separators (e.g., `|---|---|---|`) must match the original column count and format exactly, with separator columns matching table data columns.
13
- - **Follow Translation Process**: Include literal translation, optimization, and omission checking to ensure the final output meets all requirements.
14
- - **Use Terminology Reference**: Ensure accuracy and consistency of professional terminology.
15
- - **Preserve Terms**: Retain specific terms in their original form, avoiding translation.
16
-
17
- Translation Process:
18
-
19
- - **Literal Translation**: Translate the original text word by word and sentence by sentence into the target language, ensuring every word's meaning is accurately conveyed.
20
- - **Optimization**: Based on the literal translation, ensure the text stays faithful to the original meaning while making it more natural and aligned with **{{ language }}** usage.
21
- - **Check for Omissions**: Compare the original with the literal translation to correct any meaning distortions or omissions.
22
- - **Format Check**: Compare the original with the literal translation to ensure the translated content is complete. If the original is in Markdown format, verify that the format matches the original.
23
- - **Final Output**: Output the optimized translation result, ensuring compliance with the above requirements (do not output the literal translation content).
17
+ - Avoid Exaggeration: Avoid using emotionally charged or subjective words (for example, "excited" or "shocked").
18
+ - Preserve Original Structure: Translate only the content portions without modifying tags or introducing any extra content or punctuation. Do not add markdown syntax at the outermost level. Ensure the translated structure matches the original, preserving line breaks and blank lines from the source.
19
+ - Strictly Protect Markdown Syntax: All Markdown syntax characters, including but not limited to `|` and `-` in tables, `*` and `-` in lists, `#` in headings, `` ` `` in code blocks, etc., must be **copied exactly**, with no modification, addition, deletion, or merging. Table separators (e.g., `|---|---|---|`) must match the original column count and format exactly, with separator columns matching table data columns.
20
+ - Use Terminology Reference: Ensure accuracy and consistency of professional terminology.
21
+ - Preserve Terms: Retain specific terms in their original form, avoiding translation.
22
+
23
+ {% include "./code-block.md" %}
24
24
  </translation_rules>
25
25
 
26
26
 
@@ -57,6 +57,9 @@ Terms to preserve (do not translate):
57
57
  </terms>
58
58
 
59
59
  <example>
60
+ <example_item>
61
+ **Special Note**: Keep table separators `|---|---|---|` unchanged from the original
62
+
60
63
  <before_translate>
61
64
  | Name | Type | Description |
62
65
  |---|---|---|
@@ -71,9 +74,10 @@ Terms to preserve (do not translate):
71
74
  | `id` | `string` | 要更新的 Webhook 端点的唯一标识符。 |
72
75
  | `data` | `PartialDeep<ABTNodeClient.WebhookEndpointStateInput>` | 包含要更新的 Webhook 端点字段的对象。 |
73
76
  </after_translate>
77
+ </example_item>
74
78
 
75
- **Special Note**: Keep table separators `|---|---|---|` unchanged from the original
76
-
79
+ <example_item>
80
+ **Special Note**: All x-field component attributes must maintain the original format. Only translate the description content within data-desc attributes or x-field-desc elements
77
81
  <before_translate>
78
82
 
79
83
  <x-field data-name="teamDid" data-type="string" data-required="true" data-desc="The DID of the team or Blocklet managing the webhook."></x-field>
@@ -89,8 +93,100 @@ Terms to preserve (do not translate):
89
93
  <x-field-desc markdown>您的 **API 密钥**,用于身份验证。请从 `设置 > API 密钥` 部分生成一个。</x-field-desc>
90
94
  </x-field>
91
95
  </after_translate>
96
+ </example_item>
97
+
98
+ <example_item>
99
+ **Special Note**: In code blocks, only translate comments while keeping all other code content (variables, functions, syntax) unchanged
100
+
101
+ <before_translate>
102
+ ```xxx
103
+ // Initialize the API client
104
+ const client = new APIClient({
105
+ apiKey: 'your-api-key', // Replace with your actual API key
106
+ baseUrl: 'https://api.example.com'
107
+ });
108
+
109
+ const errorMessage = 'Failed to fetch user data';
110
+ const successMessage = 'User data retrieved successfully';
111
+
112
+ // Send request to get user data
113
+ async function getUserData(userId) {
114
+ console.log('Starting user data fetch for ID:', userId);
115
+
116
+ try {
117
+ // Fetch user information from the API
118
+ const result = await client.get(`/users/${userId}`);
119
+ console.log('API response received');
120
+ console.log(successMessage);
121
+ return result;
122
+ } catch (error) {
123
+ console.error('Error occurred:', error.message);
124
+ throw new Error(errorMessage);
125
+ }
126
+ }
127
+ ```
128
+ </before_translate>
129
+
130
+ <after_translate>
131
+ ```xxx
132
+ // 初始化 API 客户端
133
+ const client = new APIClient({
134
+ apiKey: 'your-api-key', // 替换为您的实际 API 密钥
135
+ baseUrl: 'https://api.example.com'
136
+ });
137
+
138
+ const errorMessage = 'Failed to fetch user data';
139
+ const successMessage = 'User data retrieved successfully';
140
+
141
+ // 发送请求获取用户数据
142
+ async function getUserData(userId) {
143
+ console.log('Starting user data fetch for ID:', userId);
144
+
145
+ try {
146
+ // 从 API 获取用户信息
147
+ const result = await client.get(`/users/${userId}`);
148
+ console.log('API response received');
149
+ console.log(successMessage);
150
+ return result;
151
+ } catch (error) {
152
+ console.error('Error occurred:', error.message);
153
+ throw new Error(errorMessage);
154
+ }
155
+ }
156
+ ```
157
+ </after_translate>
158
+ </example_item>
159
+
160
+ <example_item>
161
+ **Special Note**: **Command execution and log printing** should untranslated
162
+
163
+ <before_translate>
164
+ ```text Timeout Error Message
165
+ Blocklet Server failed to stop within 5 minutes
166
+ You can stop blocklet server with blocklet stop --force
167
+ ```
168
+
169
+ ```bash Success Output
170
+ $ cli log
171
+
172
+ Cache for server cleared: [list of cleared cache keys]
173
+ ```
174
+ </before_translate>
175
+
176
+ <after_translate>
177
+ ```text 超时错误消息
178
+ Blocklet Server failed to stop within 5 minutes
179
+ You can stop blocklet server with blocklet stop --force
180
+ ```
181
+
182
+ ```bash 成功输出
183
+ $ cli log
184
+
185
+ Cache for server cleared: [list of cleared cache keys]
186
+ ```
187
+ </after_translate>
188
+ </example_item>
92
189
 
93
- **Special Note**: All x-field component attributes must maintain the original format. Only translate the description content within data-desc attributes or x-field-desc elements
94
190
  </example>
95
191
 
96
192
  Original text as follows:
@@ -1438,6 +1438,128 @@ describe("init", () => {
1438
1438
  });
1439
1439
  });
1440
1440
 
1441
+ describe("checkOnly parameter", () => {
1442
+ test("should exit silently when checkOnly is true and config exists", async () => {
1443
+ const tempDir = await createTempDir();
1444
+
1445
+ try {
1446
+ // Create existing config file
1447
+ const configPath = join(tempDir, "config.yaml");
1448
+ const existingConfig = `projectName: "Test Project"
1449
+ locale: "en"
1450
+ documentPurpose:
1451
+ - getStarted
1452
+ targetAudienceTypes:
1453
+ - developers`;
1454
+ await fs.writeFile(configPath, existingConfig, "utf8");
1455
+
1456
+ // The function should continue normally when config exists
1457
+ // No mocking needed as it doesn't log or exit in this case
1458
+ const result = await init(
1459
+ {
1460
+ outputPath: tempDir,
1461
+ fileName: "config.yaml",
1462
+ checkOnly: true,
1463
+ },
1464
+ { prompts: {} }, // Options not needed for checkOnly
1465
+ );
1466
+
1467
+ // Should return loaded config
1468
+ expect(result).toBeDefined();
1469
+ expect(result.projectName).toBe("Test Project");
1470
+ expect(result.locale).toBe("en");
1471
+ } finally {
1472
+ await cleanupTempDir(tempDir);
1473
+ }
1474
+ });
1475
+
1476
+ test("should show reminder and exit when checkOnly is true and config doesn't exist", async () => {
1477
+ const tempDir = await createTempDir();
1478
+
1479
+ try {
1480
+ // Mock console.log and process.exit
1481
+ const originalLog = console.log;
1482
+ const originalExit = process.exit;
1483
+ const logMessages = [];
1484
+ let exitCalled = false;
1485
+ let exitCode = null;
1486
+
1487
+ console.log = (...args) => logMessages.push(args.join(" "));
1488
+ process.exit = (code) => {
1489
+ exitCalled = true;
1490
+ exitCode = code;
1491
+ throw new Error("process.exit called"); // Prevent actual exit
1492
+ };
1493
+
1494
+ try {
1495
+ await expect(
1496
+ init(
1497
+ {
1498
+ outputPath: tempDir,
1499
+ fileName: "config.yaml",
1500
+ checkOnly: true,
1501
+ },
1502
+ { prompts: {} },
1503
+ ),
1504
+ ).rejects.toThrow("process.exit called");
1505
+
1506
+ // Should log reminder messages
1507
+ expect(logMessages.some((msg) => msg.includes("No configuration found"))).toBe(true);
1508
+ expect(logMessages.some((msg) => msg.includes("aigne doc init"))).toBe(true);
1509
+
1510
+ // Should call process.exit(0)
1511
+ expect(exitCalled).toBe(true);
1512
+ expect(exitCode).toBe(0);
1513
+ } finally {
1514
+ console.log = originalLog;
1515
+ process.exit = originalExit;
1516
+ }
1517
+ } finally {
1518
+ await cleanupTempDir(tempDir);
1519
+ }
1520
+ });
1521
+
1522
+ test("should not interfere with normal workflow when checkOnly is false", async () => {
1523
+ const tempDir = await createTempDir();
1524
+
1525
+ try {
1526
+ const mockPrompts = createMockPrompts({
1527
+ checkbox_1: ["getStarted"],
1528
+ checkbox_2: ["developers"],
1529
+ select_3: "domainFamiliar",
1530
+ select_4: "balancedCoverage",
1531
+ select_5: "en",
1532
+ checkbox_6: [],
1533
+ input_7: join(tempDir, "docs"),
1534
+ search: "",
1535
+ });
1536
+
1537
+ const options = { prompts: mockPrompts };
1538
+
1539
+ const result = await init(
1540
+ {
1541
+ outputPath: tempDir,
1542
+ fileName: "config.yaml",
1543
+ checkOnly: false, // Explicit false
1544
+ },
1545
+ options,
1546
+ );
1547
+
1548
+ expect(result).toBeDefined();
1549
+
1550
+ // Check that config file was created normally
1551
+ const configPath = join(tempDir, "config.yaml");
1552
+ const configExists = await fs
1553
+ .access(configPath)
1554
+ .then(() => true)
1555
+ .catch(() => false);
1556
+ expect(configExists).toBe(true);
1557
+ } finally {
1558
+ await cleanupTempDir(tempDir);
1559
+ }
1560
+ });
1561
+ });
1562
+
1441
1563
  describe("Advanced source path scenarios", () => {
1442
1564
  test("should handle source path validation and duplicate detection", async () => {
1443
1565
  const tempDir = await createTempDir();
@@ -183,12 +183,7 @@ describe("chooseDocs utility", () => {
183
183
  locale: "en",
184
184
  };
185
185
 
186
- await expect(chooseDocs(input, mockOptions)).rejects.toThrow(
187
- "Please provide a docs parameter to specify which documents to update",
188
- );
189
- expect(consoleErrorSpy).toHaveBeenCalledWith(
190
- new Error("No documents found in the docs directory"),
191
- );
186
+ await expect(chooseDocs(input, mockOptions)).rejects.toThrow();
192
187
  });
193
188
 
194
189
  test("should throw error when no documents selected interactively", async () => {
@@ -202,9 +197,7 @@ describe("chooseDocs utility", () => {
202
197
  locale: "en",
203
198
  };
204
199
 
205
- await expect(chooseDocs(input, mockOptions)).rejects.toThrow(
206
- "Please provide a docs parameter to specify which documents to update",
207
- );
200
+ await expect(chooseDocs(input, mockOptions)).rejects.toThrow();
208
201
  });
209
202
 
210
203
  // CHECKBOX VALIDATION TESTS
@@ -15,6 +15,7 @@ import {
15
15
  describe("docs-finder-utils", () => {
16
16
  let readdirSpy;
17
17
  let readFileSpy;
18
+ let accessSpy;
18
19
  let joinSpy;
19
20
  let consoleWarnSpy;
20
21
 
@@ -22,6 +23,7 @@ describe("docs-finder-utils", () => {
22
23
  // Mock file system operations
23
24
  readdirSpy = spyOn(fs, "readdir").mockResolvedValue([]);
24
25
  readFileSpy = spyOn(fs, "readFile").mockResolvedValue("test content");
26
+ accessSpy = spyOn(fs, "access").mockResolvedValue(undefined);
25
27
  joinSpy = spyOn(path, "join").mockImplementation((...paths) => paths.join("/"));
26
28
 
27
29
  // Mock console methods
@@ -31,6 +33,7 @@ describe("docs-finder-utils", () => {
31
33
  afterEach(() => {
32
34
  readdirSpy?.mockRestore();
33
35
  readFileSpy?.mockRestore();
36
+ accessSpy?.mockRestore();
34
37
  joinSpy?.mockRestore();
35
38
  consoleWarnSpy?.mockRestore();
36
39
  });
@@ -606,11 +609,20 @@ describe("docs-finder-utils", () => {
606
609
  });
607
610
 
608
611
  test("getMainLanguageFiles should handle readdir errors", async () => {
612
+ accessSpy.mockResolvedValue(undefined); // Directory exists
609
613
  readdirSpy.mockRejectedValue(new Error("Permission denied"));
610
614
 
611
615
  await expect(getMainLanguageFiles("/denied", "en")).rejects.toThrow("Permission denied");
612
616
  });
613
617
 
618
+ test("getMainLanguageFiles should throw non-ENOENT access errors", async () => {
619
+ const permissionError = new Error("Permission denied");
620
+ permissionError.code = "EACCES";
621
+ accessSpy.mockRejectedValue(permissionError);
622
+
623
+ await expect(getMainLanguageFiles("/denied", "en")).rejects.toThrow("Permission denied");
624
+ });
625
+
614
626
  test("processSelectedFiles should handle empty document structure", async () => {
615
627
  readFileSpy.mockResolvedValue("content");
616
628
 
@@ -1,4 +1,4 @@
1
- import { readdir, readFile } from "node:fs/promises";
1
+ import { access, readdir, readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
 
4
4
  /**
@@ -120,6 +120,17 @@ export async function readFileContent(docsDir, fileName) {
120
120
  * @returns {Promise<string[]>} Array of main language .md files ordered by documentExecutionStructure
121
121
  */
122
122
  export async function getMainLanguageFiles(docsDir, locale, documentExecutionStructure = null) {
123
+ // Check if docsDir exists
124
+ try {
125
+ await access(docsDir);
126
+ } catch (error) {
127
+ if (error.code === "ENOENT") {
128
+ return [];
129
+ }
130
+
131
+ throw error;
132
+ }
133
+
123
134
  const files = await readdir(docsDir);
124
135
 
125
136
  // Filter for main language .md files (exclude _sidebar.md)