@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.
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +20 -0
- package/agents/clear/index.yaml +1 -0
- package/agents/evaluate/index.yaml +1 -0
- package/agents/init/index.mjs +24 -1
- package/agents/publish/index.yaml +1 -0
- package/agents/translate/index.yaml +1 -0
- package/agents/update/index.yaml +1 -0
- package/agents/update/update-single-document.yaml +1 -0
- package/agents/utils/choose-docs.mjs +22 -11
- package/aigne.yaml +5 -0
- package/package.json +1 -1
- package/prompts/detail/update-document.md +1 -1
- package/prompts/translate/code-block.md +16 -0
- package/prompts/translate/translate-document.md +116 -20
- package/tests/agents/init/init.test.mjs +122 -0
- package/tests/agents/utils/choose-docs.test.mjs +2 -9
- package/tests/utils/docs-finder-utils.test.mjs +12 -0
- package/utils/docs-finder-utils.mjs +12 -1
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
|
|
package/agents/clear/index.yaml
CHANGED
package/agents/init/index.mjs
CHANGED
|
@@ -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
|
-
{
|
|
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);
|
package/agents/update/index.yaml
CHANGED
|
@@ -34,18 +34,33 @@ export default async function chooseDocs(
|
|
|
34
34
|
);
|
|
35
35
|
|
|
36
36
|
if (mainLanguageFiles.length === 0) {
|
|
37
|
-
throw new Error(
|
|
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
|
@@ -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
|
-
|
|
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
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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)
|