@aigne/doc-smith 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/reviewer.yml +53 -0
- package/CHANGELOG.md +7 -0
- package/README.md +2 -2
- package/agents/action-success.mjs +16 -0
- package/agents/check-structure-plan.mjs +12 -1
- package/agents/detail-regenerator.yaml +14 -6
- package/agents/find-item-by-path.mjs +21 -54
- package/agents/find-items-by-paths.mjs +99 -0
- package/agents/input-generator.mjs +11 -6
- package/agents/language-selector.mjs +59 -38
- package/agents/load-config.mjs +5 -2
- package/agents/publish-docs.mjs +2 -2
- package/agents/retranslate.yaml +26 -38
- package/agents/translate.yaml +1 -1
- package/package.json +1 -1
- package/prompts/check-structure-planning-result.md +4 -2
- package/tests/save-value-to-config.test.mjs +370 -0
- package/utils/auth-utils.mjs +2 -2
- package/utils/constants.mjs +3 -0
- package/utils/docs-finder-utils.mjs +258 -0
- package/utils/markdown-checker.mjs +5 -2
- package/utils/utils.mjs +251 -36
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get action-specific text based on isTranslate flag
|
|
6
|
+
* @param {boolean} isTranslate - Whether this is a translation action
|
|
7
|
+
* @param {string} baseText - Base text template with {action} placeholder
|
|
8
|
+
* @returns {string} Text with action replaced
|
|
9
|
+
*/
|
|
10
|
+
export function getActionText(isTranslate, baseText) {
|
|
11
|
+
const action = isTranslate ? "translate" : "update";
|
|
12
|
+
return baseText.replace("{action}", action);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate filename based on flattened path and locale
|
|
17
|
+
* @param {string} flatName - Flattened path name
|
|
18
|
+
* @param {string} locale - Main language locale (e.g., 'en', 'zh', 'fr')
|
|
19
|
+
* @returns {string} Generated filename
|
|
20
|
+
*/
|
|
21
|
+
function generateFileName(flatName, locale) {
|
|
22
|
+
const isEnglish = locale === "en";
|
|
23
|
+
return isEnglish ? `${flatName}.md` : `${flatName}.${locale}.md`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Find a single item by path in structure plan result and read its content
|
|
28
|
+
* @param {Array} structurePlanResult - Array of structure plan items
|
|
29
|
+
* @param {string} docPath - Document path to find (supports .md filenames)
|
|
30
|
+
* @param {string} boardId - Board ID for fallback matching
|
|
31
|
+
* @param {string} docsDir - Docs directory path for reading content
|
|
32
|
+
* @param {string} locale - Main language locale (e.g., 'en', 'zh', 'fr')
|
|
33
|
+
* @returns {Promise<Object|null>} Found item with content or null
|
|
34
|
+
*/
|
|
35
|
+
export async function findItemByPath(
|
|
36
|
+
structurePlanResult,
|
|
37
|
+
docPath,
|
|
38
|
+
boardId,
|
|
39
|
+
docsDir,
|
|
40
|
+
locale = "en",
|
|
41
|
+
) {
|
|
42
|
+
let foundItem = null;
|
|
43
|
+
let fileName = null;
|
|
44
|
+
|
|
45
|
+
// Check if docPath is a .md filename
|
|
46
|
+
if (docPath.endsWith(".md")) {
|
|
47
|
+
fileName = docPath;
|
|
48
|
+
const flatName = fileNameToFlatPath(docPath);
|
|
49
|
+
foundItem = findItemByFlatName(structurePlanResult, flatName);
|
|
50
|
+
} else {
|
|
51
|
+
// First try direct path matching
|
|
52
|
+
foundItem = structurePlanResult.find((item) => item.path === docPath);
|
|
53
|
+
|
|
54
|
+
// If not found and boardId is provided, try boardId-flattenedPath format matching
|
|
55
|
+
if (!foundItem && boardId) {
|
|
56
|
+
// Check if path starts with boardId followed by a dash
|
|
57
|
+
if (docPath.startsWith(`${boardId}-`)) {
|
|
58
|
+
// Extract the flattened path part after boardId-
|
|
59
|
+
const flattenedPath = docPath.substring(boardId.length + 1);
|
|
60
|
+
|
|
61
|
+
// Find item by comparing flattened paths
|
|
62
|
+
foundItem = structurePlanResult.find((item) => {
|
|
63
|
+
// Convert item.path to flattened format (replace / with -)
|
|
64
|
+
const itemFlattenedPath = item.path.replace(/^\//, "").replace(/\//g, "-");
|
|
65
|
+
return itemFlattenedPath === flattenedPath;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Generate filename from found item path
|
|
71
|
+
if (foundItem) {
|
|
72
|
+
const itemFlattenedPath = foundItem.path.replace(/^\//, "").replace(/\//g, "-");
|
|
73
|
+
fileName = generateFileName(itemFlattenedPath, locale);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!foundItem) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Read file content if docsDir is provided
|
|
82
|
+
let content = null;
|
|
83
|
+
if (docsDir && fileName) {
|
|
84
|
+
content = await readFileContent(docsDir, fileName);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Return item with content
|
|
88
|
+
const result = {
|
|
89
|
+
...foundItem,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (content !== null) {
|
|
93
|
+
result.content = content;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Read file content from docs directory
|
|
101
|
+
* @param {string} docsDir - Docs directory path
|
|
102
|
+
* @param {string} fileName - File name to read
|
|
103
|
+
* @returns {Promise<string|null>} File content or null if failed
|
|
104
|
+
*/
|
|
105
|
+
export async function readFileContent(docsDir, fileName) {
|
|
106
|
+
try {
|
|
107
|
+
const filePath = join(docsDir, fileName);
|
|
108
|
+
return await readFile(filePath, "utf-8");
|
|
109
|
+
} catch (readError) {
|
|
110
|
+
console.warn(`⚠️ Could not read content from ${fileName}:`, readError.message);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get main language markdown files from docs directory
|
|
117
|
+
* @param {string} docsDir - Docs directory path
|
|
118
|
+
* @param {string} locale - Main language locale (e.g., 'en', 'zh', 'fr')
|
|
119
|
+
* @param {Array} structurePlanResult - Array of structure plan items to determine file order
|
|
120
|
+
* @returns {Promise<string[]>} Array of main language .md files ordered by structurePlanResult
|
|
121
|
+
*/
|
|
122
|
+
export async function getMainLanguageFiles(docsDir, locale, structurePlanResult = null) {
|
|
123
|
+
const files = await readdir(docsDir);
|
|
124
|
+
|
|
125
|
+
// Filter for main language .md files (exclude _sidebar.md)
|
|
126
|
+
const filteredFiles = files.filter((file) => {
|
|
127
|
+
// Skip non-markdown files and _sidebar.md
|
|
128
|
+
if (!file.endsWith(".md") || file === "_sidebar.md") {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// If main language is English, return files without language suffix
|
|
133
|
+
if (locale === "en") {
|
|
134
|
+
// Return files that don't have language suffixes (e.g., overview.md, not overview.zh.md)
|
|
135
|
+
return !file.match(/\.\w+(-\w+)?\.md$/);
|
|
136
|
+
} else {
|
|
137
|
+
// For non-English main language, return files with the exact locale suffix
|
|
138
|
+
const localePattern = new RegExp(`\\.${locale}\\.md$`);
|
|
139
|
+
return localePattern.test(file);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// If structurePlanResult is provided, sort files according to the order in structurePlanResult
|
|
144
|
+
if (structurePlanResult && Array.isArray(structurePlanResult)) {
|
|
145
|
+
// Create a map from flat file name to structure plan order
|
|
146
|
+
const orderMap = new Map();
|
|
147
|
+
structurePlanResult.forEach((item, index) => {
|
|
148
|
+
const itemFlattenedPath = item.path.replace(/^\//, "").replace(/\//g, "-");
|
|
149
|
+
const expectedFileName = generateFileName(itemFlattenedPath, locale);
|
|
150
|
+
orderMap.set(expectedFileName, index);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Sort filtered files based on their order in structurePlanResult
|
|
154
|
+
return filteredFiles.sort((a, b) => {
|
|
155
|
+
const orderA = orderMap.get(a);
|
|
156
|
+
const orderB = orderMap.get(b);
|
|
157
|
+
|
|
158
|
+
// If both files are in the structure plan, sort by order
|
|
159
|
+
if (orderA !== undefined && orderB !== undefined) {
|
|
160
|
+
return orderA - orderB;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// If only one file is in the structure plan, it comes first
|
|
164
|
+
if (orderA !== undefined) return -1;
|
|
165
|
+
if (orderB !== undefined) return 1;
|
|
166
|
+
|
|
167
|
+
// If neither file is in the structure plan, maintain alphabetical order
|
|
168
|
+
return a.localeCompare(b);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// If no structurePlanResult provided, return files in alphabetical order
|
|
173
|
+
return filteredFiles.sort();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Convert filename to flattened path format
|
|
178
|
+
* @param {string} fileName - File name to convert
|
|
179
|
+
* @returns {string} Flattened path without .md extension and language suffix
|
|
180
|
+
*/
|
|
181
|
+
export function fileNameToFlatPath(fileName) {
|
|
182
|
+
// Remove .md extension first
|
|
183
|
+
let flatName = fileName.replace(/\.md$/, "");
|
|
184
|
+
|
|
185
|
+
// Remove language suffix if present (e.g., .zh, .zh-CN, .fr, etc.)
|
|
186
|
+
flatName = flatName.replace(/\.\w+(-\w+)?$/, "");
|
|
187
|
+
|
|
188
|
+
return flatName;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Find structure plan item by flattened file name
|
|
193
|
+
* @param {Array} structurePlanResult - Array of structure plan items
|
|
194
|
+
* @param {string} flatName - Flattened file name
|
|
195
|
+
* @returns {Object|null} Found item or null
|
|
196
|
+
*/
|
|
197
|
+
export function findItemByFlatName(structurePlanResult, flatName) {
|
|
198
|
+
return structurePlanResult.find((item) => {
|
|
199
|
+
const itemFlattenedPath = item.path.replace(/^\//, "").replace(/\//g, "-");
|
|
200
|
+
return itemFlattenedPath === flatName;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Process selected files and convert to found items with content
|
|
206
|
+
* @param {string[]} selectedFiles - Array of selected file names
|
|
207
|
+
* @param {Array} structurePlanResult - Array of structure plan items
|
|
208
|
+
* @param {string} docsDir - Docs directory path
|
|
209
|
+
* @returns {Promise<Object[]>} Array of found items with content
|
|
210
|
+
*/
|
|
211
|
+
export async function processSelectedFiles(selectedFiles, structurePlanResult, docsDir) {
|
|
212
|
+
const foundItems = [];
|
|
213
|
+
|
|
214
|
+
for (const selectedFile of selectedFiles) {
|
|
215
|
+
// Read the selected .md file content
|
|
216
|
+
const selectedFileContent = await readFileContent(docsDir, selectedFile);
|
|
217
|
+
|
|
218
|
+
// Convert filename back to path
|
|
219
|
+
const flatName = fileNameToFlatPath(selectedFile);
|
|
220
|
+
|
|
221
|
+
// Try to find matching item by comparing flattened paths
|
|
222
|
+
const foundItemByFile = findItemByFlatName(structurePlanResult, flatName);
|
|
223
|
+
|
|
224
|
+
if (foundItemByFile) {
|
|
225
|
+
const result = {
|
|
226
|
+
...foundItemByFile,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Add content if we read it from user selection
|
|
230
|
+
if (selectedFileContent !== null) {
|
|
231
|
+
result.content = selectedFileContent;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
foundItems.push(result);
|
|
235
|
+
} else {
|
|
236
|
+
console.warn(`⚠️ No structure plan item found for file: ${selectedFile}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return foundItems;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Add feedback to all items in the array
|
|
245
|
+
* @param {Object[]} items - Array of items to add feedback to
|
|
246
|
+
* @param {string} feedback - Feedback text to add
|
|
247
|
+
* @returns {Object[]} Items with feedback added
|
|
248
|
+
*/
|
|
249
|
+
export function addFeedbackToItems(items, feedback) {
|
|
250
|
+
if (!feedback?.trim()) {
|
|
251
|
+
return items;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return items.map((item) => ({
|
|
255
|
+
...item,
|
|
256
|
+
feedback: feedback.trim(),
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
@@ -224,10 +224,13 @@ function checkContentStructure(markdown, source, errorMessages) {
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
// Check if content ends with proper punctuation (indicating completeness)
|
|
227
|
+
const validEndingPunctuation = [".", "。", ")", "|"];
|
|
227
228
|
const trimmedText = markdown.trim();
|
|
228
|
-
|
|
229
|
+
const hasValidEnding = validEndingPunctuation.some((punct) => trimmedText.endsWith(punct));
|
|
230
|
+
|
|
231
|
+
if (trimmedText.length > 0 && !hasValidEnding) {
|
|
229
232
|
errorMessages.push(
|
|
230
|
-
`Found incomplete content in ${source}: content does not end with proper punctuation (.
|
|
233
|
+
`Found incomplete content in ${source}: content does not end with proper punctuation (${validEndingPunctuation.join(", ")}). Please return the complete content`,
|
|
231
234
|
);
|
|
232
235
|
}
|
|
233
236
|
}
|
package/utils/utils.mjs
CHANGED
|
@@ -7,10 +7,11 @@ import {
|
|
|
7
7
|
DEFAULT_EXCLUDE_PATTERNS,
|
|
8
8
|
DEFAULT_INCLUDE_PATTERNS,
|
|
9
9
|
DOCUMENT_STYLES,
|
|
10
|
-
TARGET_AUDIENCES,
|
|
11
|
-
READER_KNOWLEDGE_LEVELS,
|
|
12
10
|
DOCUMENTATION_DEPTH,
|
|
11
|
+
SUPPORTED_FILE_EXTENSIONS,
|
|
12
|
+
READER_KNOWLEDGE_LEVELS,
|
|
13
13
|
SUPPORTED_LANGUAGES,
|
|
14
|
+
TARGET_AUDIENCES,
|
|
14
15
|
} from "./constants.mjs";
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -338,9 +339,140 @@ export async function loadConfigFromFile() {
|
|
|
338
339
|
/**
|
|
339
340
|
* Save value to config.yaml file
|
|
340
341
|
* @param {string} key - The config key to save
|
|
341
|
-
* @param {string} value - The value to save
|
|
342
|
+
* @param {string|Array} value - The value to save (can be string or array)
|
|
342
343
|
* @param {string} [comment] - Optional comment to add above the key
|
|
343
344
|
*/
|
|
345
|
+
/**
|
|
346
|
+
* Handle array value formatting and updating in YAML config
|
|
347
|
+
* @param {string} key - The configuration key
|
|
348
|
+
* @param {Array} value - The array value to save
|
|
349
|
+
* @param {string} comment - Optional comment
|
|
350
|
+
* @param {string} fileContent - Current file content
|
|
351
|
+
* @returns {string} Updated file content
|
|
352
|
+
*/
|
|
353
|
+
function handleArrayValueUpdate(key, value, comment, fileContent) {
|
|
354
|
+
// Format array value
|
|
355
|
+
const formattedValue =
|
|
356
|
+
value.length === 0 ? `${key}: []` : `${key}:\n${value.map((item) => ` - ${item}`).join("\n")}`;
|
|
357
|
+
|
|
358
|
+
const lines = fileContent.split("\n");
|
|
359
|
+
|
|
360
|
+
// Find the start line of the key
|
|
361
|
+
const keyStartIndex = lines.findIndex((line) => line.match(new RegExp(`^${key}:\\s*`)));
|
|
362
|
+
|
|
363
|
+
if (keyStartIndex !== -1) {
|
|
364
|
+
// Find the end of the array (next non-indented line or end of file)
|
|
365
|
+
let keyEndIndex = keyStartIndex;
|
|
366
|
+
for (let i = keyStartIndex + 1; i < lines.length; i++) {
|
|
367
|
+
const line = lines[i].trim();
|
|
368
|
+
// If line is empty, starts with comment, or doesn't start with "- ", it's not part of the array
|
|
369
|
+
if (line === "" || line.startsWith("#") || (!line.startsWith("- ") && !line.match(/^\w+:/))) {
|
|
370
|
+
if (!line.startsWith("- ")) {
|
|
371
|
+
keyEndIndex = i - 1;
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
} else if (line.match(/^\w+:/)) {
|
|
375
|
+
// Found another key, stop here
|
|
376
|
+
keyEndIndex = i - 1;
|
|
377
|
+
break;
|
|
378
|
+
} else if (line.startsWith("- ")) {
|
|
379
|
+
keyEndIndex = i;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// If we reached the end of file
|
|
384
|
+
if (keyEndIndex === keyStartIndex) {
|
|
385
|
+
// Check if the value is on the same line
|
|
386
|
+
const keyLine = lines[keyStartIndex];
|
|
387
|
+
if (keyLine.includes("[") || !keyLine.endsWith(":")) {
|
|
388
|
+
keyEndIndex = keyStartIndex;
|
|
389
|
+
} else {
|
|
390
|
+
// Find the actual end of the array
|
|
391
|
+
for (let i = keyStartIndex + 1; i < lines.length; i++) {
|
|
392
|
+
const line = lines[i].trim();
|
|
393
|
+
if (line.startsWith("- ")) {
|
|
394
|
+
keyEndIndex = i;
|
|
395
|
+
} else if (line !== "" && !line.startsWith("#")) {
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Replace the entire array section
|
|
403
|
+
const replacementLines = formattedValue.split("\n");
|
|
404
|
+
lines.splice(keyStartIndex, keyEndIndex - keyStartIndex + 1, ...replacementLines);
|
|
405
|
+
|
|
406
|
+
// Add comment if provided and not already present
|
|
407
|
+
if (comment && keyStartIndex > 0 && !lines[keyStartIndex - 1].trim().startsWith("# ")) {
|
|
408
|
+
lines.splice(keyStartIndex, 0, `# ${comment}`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return lines.join("\n");
|
|
412
|
+
} else {
|
|
413
|
+
// Add new array to end of file
|
|
414
|
+
let updatedContent = fileContent;
|
|
415
|
+
if (updatedContent && !updatedContent.endsWith("\n")) {
|
|
416
|
+
updatedContent += "\n";
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Add comment if provided
|
|
420
|
+
if (comment) {
|
|
421
|
+
updatedContent += `# ${comment}\n`;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
updatedContent += `${formattedValue}\n`;
|
|
425
|
+
return updatedContent;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Handle string value formatting and updating in YAML config
|
|
431
|
+
* @param {string} key - The configuration key
|
|
432
|
+
* @param {string} value - The string value to save
|
|
433
|
+
* @param {string} comment - Optional comment
|
|
434
|
+
* @param {string} fileContent - Current file content
|
|
435
|
+
* @returns {string} Updated file content
|
|
436
|
+
*/
|
|
437
|
+
function handleStringValueUpdate(key, value, comment, fileContent) {
|
|
438
|
+
const formattedValue = `${key}: "${value}"`;
|
|
439
|
+
const lines = fileContent.split("\n");
|
|
440
|
+
|
|
441
|
+
// Handle string values (original logic)
|
|
442
|
+
const keyRegex = new RegExp(`^${key}:\\s*.*$`);
|
|
443
|
+
const keyIndex = lines.findIndex((line) => keyRegex.test(line));
|
|
444
|
+
|
|
445
|
+
if (keyIndex !== -1) {
|
|
446
|
+
// Replace existing key line
|
|
447
|
+
lines[keyIndex] = formattedValue;
|
|
448
|
+
|
|
449
|
+
// Add comment if provided and not already present
|
|
450
|
+
if (comment) {
|
|
451
|
+
const hasCommentAbove = keyIndex > 0 && lines[keyIndex - 1].trim().startsWith("# ");
|
|
452
|
+
if (!hasCommentAbove) {
|
|
453
|
+
// Add comment above the key if it doesn't already have one
|
|
454
|
+
lines.splice(keyIndex, 0, `# ${comment}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return lines.join("\n");
|
|
459
|
+
} else {
|
|
460
|
+
// Add key to the end of file
|
|
461
|
+
let updatedContent = fileContent;
|
|
462
|
+
if (updatedContent && !updatedContent.endsWith("\n")) {
|
|
463
|
+
updatedContent += "\n";
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Add comment if provided
|
|
467
|
+
if (comment) {
|
|
468
|
+
updatedContent += `# ${comment}\n`;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
updatedContent += `${formattedValue}\n`;
|
|
472
|
+
return updatedContent;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
344
476
|
export async function saveValueToConfig(key, value, comment) {
|
|
345
477
|
if (value === undefined) {
|
|
346
478
|
return; // Skip if value is undefined
|
|
@@ -360,39 +492,15 @@ export async function saveValueToConfig(key, value, comment) {
|
|
|
360
492
|
fileContent = await fs.readFile(configPath, "utf8");
|
|
361
493
|
}
|
|
362
494
|
|
|
363
|
-
//
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const keyIndex = lines.findIndex((line) => keyRegex.test(line));
|
|
369
|
-
|
|
370
|
-
if (keyIndex !== -1) {
|
|
371
|
-
// Replace existing key line
|
|
372
|
-
lines[keyIndex] = newKeyLine;
|
|
373
|
-
fileContent = lines.join("\n");
|
|
374
|
-
|
|
375
|
-
// Add comment if provided and not already present
|
|
376
|
-
if (comment && keyIndex > 0 && !lines[keyIndex - 1].trim().startsWith("# ")) {
|
|
377
|
-
// Add comment above the key if it doesn't already have one
|
|
378
|
-
lines.splice(keyIndex, 0, `# ${comment}`);
|
|
379
|
-
fileContent = lines.join("\n");
|
|
380
|
-
}
|
|
495
|
+
// Use extracted helper functions for better maintainability
|
|
496
|
+
let updatedContent;
|
|
497
|
+
if (Array.isArray(value)) {
|
|
498
|
+
updatedContent = handleArrayValueUpdate(key, value, comment, fileContent);
|
|
381
499
|
} else {
|
|
382
|
-
|
|
383
|
-
if (fileContent && !fileContent.endsWith("\n")) {
|
|
384
|
-
fileContent += "\n";
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// Add comment if provided
|
|
388
|
-
if (comment) {
|
|
389
|
-
fileContent += `# ${comment}\n`;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
fileContent += `${newKeyLine}\n`;
|
|
500
|
+
updatedContent = handleStringValueUpdate(key, value, comment, fileContent);
|
|
393
501
|
}
|
|
394
502
|
|
|
395
|
-
await fs.writeFile(configPath,
|
|
503
|
+
await fs.writeFile(configPath, updatedContent);
|
|
396
504
|
} catch (error) {
|
|
397
505
|
console.warn(`Failed to save ${key} to config.yaml:`, error.message);
|
|
398
506
|
}
|
|
@@ -719,9 +827,21 @@ export function processConfigFields(config) {
|
|
|
719
827
|
const allRulesContent = [];
|
|
720
828
|
|
|
721
829
|
// Check if original rules field has content
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
830
|
+
if (config.rules) {
|
|
831
|
+
if (typeof config.rules === "string") {
|
|
832
|
+
const existingRules = config.rules.trim();
|
|
833
|
+
if (existingRules) {
|
|
834
|
+
allRulesContent.push(existingRules);
|
|
835
|
+
}
|
|
836
|
+
} else if (Array.isArray(config.rules)) {
|
|
837
|
+
// Handle array of rules - join them with newlines
|
|
838
|
+
const rulesText = config.rules
|
|
839
|
+
.filter((rule) => typeof rule === "string" && rule.trim())
|
|
840
|
+
.join("\n\n");
|
|
841
|
+
if (rulesText) {
|
|
842
|
+
allRulesContent.push(rulesText);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
725
845
|
}
|
|
726
846
|
|
|
727
847
|
// Process document purpose (array)
|
|
@@ -798,6 +918,101 @@ export function processConfigFields(config) {
|
|
|
798
918
|
return processed;
|
|
799
919
|
}
|
|
800
920
|
|
|
921
|
+
/**
|
|
922
|
+
* Recursively resolves file references in a configuration object.
|
|
923
|
+
*
|
|
924
|
+
* This function traverses the input object, array, or string recursively. Any string value that starts
|
|
925
|
+
* with '@' is treated as a file reference, and the file's content is loaded asynchronously. Supported
|
|
926
|
+
* file formats include .txt, .md, .json, .yaml, and .yml. For .json and .yaml/.yml files, the content
|
|
927
|
+
* is parsed into objects; for .txt and .md, the raw string is returned.
|
|
928
|
+
*
|
|
929
|
+
* If a file cannot be loaded (e.g., does not exist, is of unsupported type, or parsing fails), the
|
|
930
|
+
* original string value (with '@' prefix) is returned in place of the file content.
|
|
931
|
+
*
|
|
932
|
+
* The function processes nested arrays and objects recursively, returning a new structure with file
|
|
933
|
+
* contents loaded in place of references. The input object is not mutated.
|
|
934
|
+
*
|
|
935
|
+
* Examples of supported file reference formats:
|
|
936
|
+
* - "@notes.txt"
|
|
937
|
+
* - "@docs/readme.md"
|
|
938
|
+
* - "@config/settings.json"
|
|
939
|
+
* - "@data.yaml"
|
|
940
|
+
*
|
|
941
|
+
* @param {any} obj - The configuration object, array, or string to process.
|
|
942
|
+
* @param {string} basePath - Base path for resolving relative file paths (defaults to process.cwd()).
|
|
943
|
+
* @returns {Promise<any>} - The processed configuration with file content loaded in place of references.
|
|
944
|
+
*/
|
|
945
|
+
export async function resolveFileReferences(obj, basePath = process.cwd()) {
|
|
946
|
+
if (typeof obj === "string" && obj.startsWith("@")) {
|
|
947
|
+
return await loadFileContent(obj.slice(1), basePath);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (Array.isArray(obj)) {
|
|
951
|
+
return Promise.all(obj.map((item) => resolveFileReferences(item, basePath)));
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (obj && typeof obj === "object") {
|
|
955
|
+
const result = {};
|
|
956
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
957
|
+
result[key] = await resolveFileReferences(value, basePath);
|
|
958
|
+
}
|
|
959
|
+
return result;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return obj;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Load content from a file path
|
|
967
|
+
* @param {string} filePath - The file path to load
|
|
968
|
+
* @param {string} basePath - Base path for resolving relative paths
|
|
969
|
+
* @returns {Promise<any>} - The loaded content or original path if loading fails
|
|
970
|
+
*/
|
|
971
|
+
async function loadFileContent(filePath, basePath) {
|
|
972
|
+
try {
|
|
973
|
+
// Resolve path - if absolute, use as is; if relative, resolve from basePath
|
|
974
|
+
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(basePath, filePath);
|
|
975
|
+
|
|
976
|
+
// Check if file exists
|
|
977
|
+
if (!existsSync(resolvedPath)) {
|
|
978
|
+
return `@${filePath}`; // Return original value if file doesn't exist
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Check file extension
|
|
982
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
983
|
+
|
|
984
|
+
if (!SUPPORTED_FILE_EXTENSIONS.includes(ext)) {
|
|
985
|
+
return `@${filePath}`; // Return original value if unsupported file type
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Read file content
|
|
989
|
+
const content = await fs.readFile(resolvedPath, "utf-8");
|
|
990
|
+
|
|
991
|
+
// Parse JSON/YAML files
|
|
992
|
+
if (ext === ".json") {
|
|
993
|
+
try {
|
|
994
|
+
return JSON.parse(content);
|
|
995
|
+
} catch {
|
|
996
|
+
return content; // Return raw string if JSON parsing fails
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if (ext === ".yaml" || ext === ".yml") {
|
|
1001
|
+
try {
|
|
1002
|
+
return parse(content);
|
|
1003
|
+
} catch {
|
|
1004
|
+
return content; // Return raw string if YAML parsing fails
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// Return raw content for .txt and .md files
|
|
1009
|
+
return content;
|
|
1010
|
+
} catch {
|
|
1011
|
+
// Return original value if any error occurs
|
|
1012
|
+
return `@${filePath}`;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
801
1016
|
/**
|
|
802
1017
|
* Detect system language and map to supported language code
|
|
803
1018
|
* @returns {string} - Supported language code (defaults to 'en' if detection fails or unsupported)
|