@aigne/doc-smith 0.9.8-alpha.2 → 0.9.8-alpha.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.
Files changed (256) hide show
  1. package/CLAUDE.md +43 -0
  2. package/README.md +94 -250
  3. package/aigne.yaml +2 -149
  4. package/doc-smith/SKILL.md +117 -0
  5. package/doc-smith/references/changeset_schema.md +118 -0
  6. package/doc-smith/references/document_structure_schema.md +139 -0
  7. package/doc-smith/references/document_update_guide.md +193 -0
  8. package/doc-smith/references/structure_confirmation_guide.md +133 -0
  9. package/doc-smith/references/structure_planning_guide.md +146 -0
  10. package/doc-smith/references/user_intent_guide.md +172 -0
  11. package/doc-smith.yaml +114 -0
  12. package/main-system-prompt.md +56 -0
  13. package/package.json +3 -69
  14. package/scripts/README.md +90 -0
  15. package/scripts/install.sh +86 -0
  16. package/scripts/uninstall.sh +52 -0
  17. package/CHANGELOG.md +0 -994
  18. package/LICENSE +0 -93
  19. package/agentic-agents/common/base-info.md +0 -53
  20. package/agentic-agents/common/planner.md +0 -168
  21. package/agentic-agents/common/worker.md +0 -93
  22. package/agentic-agents/create/index.yaml +0 -118
  23. package/agentic-agents/create/objective.md +0 -44
  24. package/agentic-agents/create/set-custom-prompt.mjs +0 -27
  25. package/agentic-agents/detail/index.yaml +0 -95
  26. package/agentic-agents/detail/objective.md +0 -9
  27. package/agentic-agents/detail/set-custom-prompt.mjs +0 -88
  28. package/agentic-agents/predict-resources/index.yaml +0 -44
  29. package/agentic-agents/predict-resources/instructions.md +0 -61
  30. package/agentic-agents/structure/design-rules.md +0 -39
  31. package/agentic-agents/structure/index.yaml +0 -86
  32. package/agentic-agents/structure/objective.md +0 -14
  33. package/agentic-agents/structure/review-criteria.md +0 -55
  34. package/agentic-agents/structure/set-custom-prompt.mjs +0 -78
  35. package/agentic-agents/utils/init-workspace-cache.mjs +0 -171
  36. package/agentic-agents/utils/load-base-sources.mjs +0 -20
  37. package/agentic-agents/workspace-cache-sharing-design.md +0 -671
  38. package/agents/chat/chat-system.md +0 -38
  39. package/agents/chat/index.mjs +0 -59
  40. package/agents/chat/skills/generate-document.yaml +0 -15
  41. package/agents/chat/skills/list-documents.mjs +0 -15
  42. package/agents/chat/skills/update-document.yaml +0 -24
  43. package/agents/clear/choose-contents.mjs +0 -192
  44. package/agents/clear/clear-auth-tokens.mjs +0 -88
  45. package/agents/clear/clear-deployment-config.mjs +0 -49
  46. package/agents/clear/clear-document-config.mjs +0 -36
  47. package/agents/clear/clear-document-structure.mjs +0 -102
  48. package/agents/clear/clear-generated-docs.mjs +0 -142
  49. package/agents/clear/clear-media-description.mjs +0 -129
  50. package/agents/clear/index.yaml +0 -26
  51. package/agents/create/analyze-diagram-type-llm.yaml +0 -160
  52. package/agents/create/analyze-diagram-type.mjs +0 -297
  53. package/agents/create/check-document-structure.yaml +0 -30
  54. package/agents/create/check-need-generate-structure.mjs +0 -105
  55. package/agents/create/document-structure-tools/add-document.mjs +0 -85
  56. package/agents/create/document-structure-tools/delete-document.mjs +0 -116
  57. package/agents/create/document-structure-tools/move-document.mjs +0 -109
  58. package/agents/create/document-structure-tools/update-document.mjs +0 -84
  59. package/agents/create/generate-diagram-image.yaml +0 -60
  60. package/agents/create/generate-structure.yaml +0 -117
  61. package/agents/create/index.yaml +0 -49
  62. package/agents/create/refine-document-structure.yaml +0 -12
  63. package/agents/create/replace-d2-with-image.mjs +0 -625
  64. package/agents/create/update-document-structure.yaml +0 -54
  65. package/agents/create/user-add-document/add-documents-to-structure.mjs +0 -90
  66. package/agents/create/user-add-document/find-documents-to-add-links.yaml +0 -47
  67. package/agents/create/user-add-document/index.yaml +0 -46
  68. package/agents/create/user-add-document/prepare-documents-to-translate.mjs +0 -22
  69. package/agents/create/user-add-document/print-add-document-summary.mjs +0 -63
  70. package/agents/create/user-add-document/review-documents-with-new-links.mjs +0 -110
  71. package/agents/create/user-remove-document/find-documents-with-invalid-links.mjs +0 -78
  72. package/agents/create/user-remove-document/index.yaml +0 -40
  73. package/agents/create/user-remove-document/prepare-documents-to-translate.mjs +0 -22
  74. package/agents/create/user-remove-document/print-remove-document-summary.mjs +0 -53
  75. package/agents/create/user-remove-document/remove-documents-from-structure.mjs +0 -99
  76. package/agents/create/user-remove-document/review-documents-with-invalid-links.mjs +0 -115
  77. package/agents/create/user-review-document-structure.mjs +0 -140
  78. package/agents/create/utils/init-current-content.mjs +0 -34
  79. package/agents/create/utils/merge-document-structures.mjs +0 -30
  80. package/agents/evaluate/code-snippet.mjs +0 -97
  81. package/agents/evaluate/document-structure.yaml +0 -67
  82. package/agents/evaluate/document.yaml +0 -82
  83. package/agents/evaluate/generate-report.mjs +0 -85
  84. package/agents/evaluate/index.yaml +0 -46
  85. package/agents/history/index.yaml +0 -6
  86. package/agents/history/view.mjs +0 -78
  87. package/agents/init/check.mjs +0 -16
  88. package/agents/init/index.mjs +0 -275
  89. package/agents/init/validate.mjs +0 -16
  90. package/agents/localize/choose-language.mjs +0 -107
  91. package/agents/localize/index.yaml +0 -58
  92. package/agents/localize/record-translation-history.mjs +0 -23
  93. package/agents/localize/translate-document.yaml +0 -24
  94. package/agents/localize/translate-multilingual.yaml +0 -51
  95. package/agents/media/batch-generate-media-description.yaml +0 -46
  96. package/agents/media/generate-media-description.yaml +0 -50
  97. package/agents/media/load-media-description.mjs +0 -256
  98. package/agents/prefs/index.mjs +0 -203
  99. package/agents/publish/index.yaml +0 -26
  100. package/agents/publish/publish-docs.mjs +0 -356
  101. package/agents/publish/translate-meta.mjs +0 -103
  102. package/agents/schema/document-structure-item.yaml +0 -26
  103. package/agents/schema/document-structure-refine-item.yaml +0 -23
  104. package/agents/schema/document-structure.yaml +0 -29
  105. package/agents/update/batch-generate-document.yaml +0 -27
  106. package/agents/update/batch-update-document.yaml +0 -7
  107. package/agents/update/check-diagram-flag.mjs +0 -116
  108. package/agents/update/check-document.mjs +0 -162
  109. package/agents/update/check-generate-diagram.mjs +0 -106
  110. package/agents/update/check-sync-image-flag.mjs +0 -55
  111. package/agents/update/check-update-is-single.mjs +0 -53
  112. package/agents/update/document-tools/update-document-content.mjs +0 -303
  113. package/agents/update/generate-diagram.yaml +0 -63
  114. package/agents/update/generate-document.yaml +0 -70
  115. package/agents/update/handle-document-update.yaml +0 -103
  116. package/agents/update/index.yaml +0 -79
  117. package/agents/update/pre-check-generate-diagram.yaml +0 -44
  118. package/agents/update/save-and-translate-document.mjs +0 -76
  119. package/agents/update/sync-images-and-exit.mjs +0 -148
  120. package/agents/update/update-document-detail.yaml +0 -71
  121. package/agents/update/update-single/update-single-document-detail.mjs +0 -280
  122. package/agents/update/update-single-document.yaml +0 -7
  123. package/agents/update/user-review-document.mjs +0 -272
  124. package/agents/utils/action-success.mjs +0 -16
  125. package/agents/utils/analyze-document-feedback-intent.yaml +0 -32
  126. package/agents/utils/analyze-feedback-intent.mjs +0 -136
  127. package/agents/utils/analyze-structure-feedback-intent.yaml +0 -29
  128. package/agents/utils/check-detail-result.mjs +0 -38
  129. package/agents/utils/check-feedback-refiner.mjs +0 -81
  130. package/agents/utils/choose-docs.mjs +0 -293
  131. package/agents/utils/document-icon-generate.yaml +0 -52
  132. package/agents/utils/document-title-streamline.yaml +0 -48
  133. package/agents/utils/ensure-document-icons.mjs +0 -129
  134. package/agents/utils/exit.mjs +0 -6
  135. package/agents/utils/feedback-refiner.yaml +0 -50
  136. package/agents/utils/find-item-by-path.mjs +0 -114
  137. package/agents/utils/find-user-preferences-by-path.mjs +0 -37
  138. package/agents/utils/format-document-structure.mjs +0 -35
  139. package/agents/utils/generate-document-or-skip.mjs +0 -41
  140. package/agents/utils/handle-diagram-operations.mjs +0 -263
  141. package/agents/utils/load-all-document-content.mjs +0 -30
  142. package/agents/utils/load-document-all-content.mjs +0 -84
  143. package/agents/utils/load-sources.mjs +0 -405
  144. package/agents/utils/map-reasoning-effort-level.mjs +0 -15
  145. package/agents/utils/post-generate.mjs +0 -144
  146. package/agents/utils/read-current-document-content.mjs +0 -46
  147. package/agents/utils/save-doc-translation.mjs +0 -61
  148. package/agents/utils/save-doc.mjs +0 -88
  149. package/agents/utils/save-output.mjs +0 -26
  150. package/agents/utils/save-sidebar.mjs +0 -51
  151. package/agents/utils/skip-if-content-exists.mjs +0 -27
  152. package/agents/utils/streamline-document-titles-if-needed.mjs +0 -88
  153. package/agents/utils/transform-detail-data-sources.mjs +0 -45
  154. package/agents/utils/update-branding.mjs +0 -84
  155. package/assets/report-template/report.html +0 -198
  156. package/docs-mcp/analyze-content-relevance.yaml +0 -50
  157. package/docs-mcp/analyze-docs-relevance.yaml +0 -59
  158. package/docs-mcp/docs-search.yaml +0 -42
  159. package/docs-mcp/get-docs-detail.mjs +0 -41
  160. package/docs-mcp/get-docs-structure.mjs +0 -16
  161. package/docs-mcp/read-doc-content.mjs +0 -119
  162. package/prompts/common/document/content-rules-core.md +0 -20
  163. package/prompts/common/document/markdown-syntax-rules.md +0 -65
  164. package/prompts/common/document/media-file-list-usage-rules.md +0 -18
  165. package/prompts/common/document/openapi-usage-rules.md +0 -189
  166. package/prompts/common/document/role-and-personality.md +0 -16
  167. package/prompts/common/document/user-preferences.md +0 -9
  168. package/prompts/common/document-structure/conflict-resolution-guidance.md +0 -16
  169. package/prompts/common/document-structure/document-icon-generate.md +0 -116
  170. package/prompts/common/document-structure/document-structure-rules.md +0 -43
  171. package/prompts/common/document-structure/document-title-streamline.md +0 -86
  172. package/prompts/common/document-structure/glossary.md +0 -7
  173. package/prompts/common/document-structure/intj-traits.md +0 -5
  174. package/prompts/common/document-structure/openapi-usage-rules.md +0 -28
  175. package/prompts/common/document-structure/output-constraints.md +0 -18
  176. package/prompts/common/document-structure/user-locale-rules.md +0 -10
  177. package/prompts/common/document-structure/user-preferences.md +0 -9
  178. package/prompts/detail/custom/admonition-usage-rules.md +0 -94
  179. package/prompts/detail/custom/code-block-usage-rules.md +0 -163
  180. package/prompts/detail/custom/custom-components/x-card-usage-rules.md +0 -63
  181. package/prompts/detail/custom/custom-components/x-cards-usage-rules.md +0 -83
  182. package/prompts/detail/custom/custom-components/x-field-desc-usage-rules.md +0 -120
  183. package/prompts/detail/custom/custom-components/x-field-group-usage-rules.md +0 -80
  184. package/prompts/detail/custom/custom-components/x-field-usage-rules.md +0 -189
  185. package/prompts/detail/custom/custom-components-usage-rules.md +0 -18
  186. package/prompts/detail/diagram/generate-image-system.md +0 -135
  187. package/prompts/detail/diagram/generate-image-user.md +0 -32
  188. package/prompts/detail/diagram/guide.md +0 -29
  189. package/prompts/detail/diagram/official-examples.md +0 -712
  190. package/prompts/detail/diagram/pre-check.md +0 -23
  191. package/prompts/detail/diagram/role-and-personality.md +0 -2
  192. package/prompts/detail/diagram/rules.md +0 -46
  193. package/prompts/detail/diagram/system-prompt.md +0 -1139
  194. package/prompts/detail/diagram/user-prompt.md +0 -43
  195. package/prompts/detail/generate/detail-example.md +0 -457
  196. package/prompts/detail/generate/document-rules.md +0 -45
  197. package/prompts/detail/generate/system-prompt.md +0 -61
  198. package/prompts/detail/generate/user-prompt.md +0 -99
  199. package/prompts/detail/jsx/rules.md +0 -6
  200. package/prompts/detail/update/system-prompt.md +0 -121
  201. package/prompts/detail/update/user-prompt.md +0 -41
  202. package/prompts/evaluate/document-structure.md +0 -93
  203. package/prompts/evaluate/document.md +0 -149
  204. package/prompts/media/media-description/system-prompt.md +0 -43
  205. package/prompts/media/media-description/user-prompt.md +0 -17
  206. package/prompts/structure/check-document-structure.md +0 -93
  207. package/prompts/structure/document-rules.md +0 -21
  208. package/prompts/structure/find-documents-to-add-links.md +0 -52
  209. package/prompts/structure/generate/system-prompt.md +0 -13
  210. package/prompts/structure/generate/user-prompt.md +0 -137
  211. package/prompts/structure/review/structure-review-system.md +0 -81
  212. package/prompts/structure/structure-example.md +0 -89
  213. package/prompts/structure/structure-getting-started.md +0 -10
  214. package/prompts/structure/update/system-prompt.md +0 -93
  215. package/prompts/structure/update/user-prompt.md +0 -43
  216. package/prompts/translate/admonition.md +0 -20
  217. package/prompts/translate/code-block.md +0 -33
  218. package/prompts/translate/glossary.md +0 -6
  219. package/prompts/translate/translate-document.md +0 -305
  220. package/prompts/utils/analyze-document-feedback-intent.md +0 -54
  221. package/prompts/utils/analyze-structure-feedback-intent.md +0 -43
  222. package/prompts/utils/feedback-refiner.md +0 -105
  223. package/types/document-schema.mjs +0 -55
  224. package/types/document-structure-schema.mjs +0 -261
  225. package/utils/auth-utils.mjs +0 -275
  226. package/utils/blocklet.mjs +0 -104
  227. package/utils/check-document-has-diagram.mjs +0 -95
  228. package/utils/conflict-detector.mjs +0 -149
  229. package/utils/constants/index.mjs +0 -620
  230. package/utils/constants/linter.mjs +0 -102
  231. package/utils/d2-utils.mjs +0 -198
  232. package/utils/debug.mjs +0 -3
  233. package/utils/delete-diagram-images.mjs +0 -99
  234. package/utils/deploy.mjs +0 -86
  235. package/utils/docs-finder-utils.mjs +0 -623
  236. package/utils/evaluate/report-utils.mjs +0 -132
  237. package/utils/extract-api.mjs +0 -32
  238. package/utils/file-utils.mjs +0 -960
  239. package/utils/history-utils.mjs +0 -203
  240. package/utils/icon-map.mjs +0 -26
  241. package/utils/image-compress.mjs +0 -75
  242. package/utils/kroki-utils.mjs +0 -173
  243. package/utils/linter/index.mjs +0 -50
  244. package/utils/load-config.mjs +0 -107
  245. package/utils/markdown/index.mjs +0 -26
  246. package/utils/markdown-checker.mjs +0 -694
  247. package/utils/mermaid-validator.mjs +0 -140
  248. package/utils/mermaid-worker-pool.mjs +0 -250
  249. package/utils/mermaid-worker.mjs +0 -233
  250. package/utils/openapi/index.mjs +0 -28
  251. package/utils/preferences-utils.mjs +0 -175
  252. package/utils/request.mjs +0 -10
  253. package/utils/store/index.mjs +0 -45
  254. package/utils/sync-diagram-to-translations.mjs +0 -262
  255. package/utils/upload-files.mjs +0 -231
  256. package/utils/utils.mjs +0 -1354
package/utils/utils.mjs DELETED
@@ -1,1354 +0,0 @@
1
- import { execSync } from "node:child_process";
2
- import crypto from "node:crypto";
3
- import { accessSync, constants, existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
4
- import fs from "node:fs/promises";
5
- import path from "node:path";
6
- import { parse, stringify as yamlStringify } from "yaml";
7
- import {
8
- detectResolvableConflicts,
9
- generateConflictResolutionRules,
10
- } from "./conflict-detector.mjs";
11
- import {
12
- DEFAULT_EXCLUDE_PATTERNS,
13
- DEFAULT_INCLUDE_PATTERNS,
14
- DOCUMENT_STYLES,
15
- DOCUMENTATION_DEPTH,
16
- READER_KNOWLEDGE_LEVELS,
17
- SUPPORTED_FILE_EXTENSIONS,
18
- SUPPORTED_LANGUAGES,
19
- TARGET_AUDIENCES,
20
- } from "./constants/index.mjs";
21
- import { isRemoteFile, getRemoteFileContent } from "./file-utils.mjs";
22
-
23
- /**
24
- * Normalize path to absolute path for consistent comparison
25
- * @param {string} filePath - The path to normalize
26
- * @returns {string} - Absolute path
27
- */
28
- export function normalizePath(filePath) {
29
- return path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
30
- }
31
-
32
- /**
33
- * Convert path to relative path from current working directory
34
- * @param {string} filePath - The path to convert
35
- * @returns {string} - Relative path
36
- */
37
- export function toRelativePath(filePath) {
38
- return path.isAbsolute(filePath) ? path.relative(process.cwd(), filePath) : filePath;
39
- }
40
-
41
- /**
42
- * Check if a string looks like a glob pattern
43
- * @param {string} pattern - The string to check
44
- * @returns {boolean} - True if the string contains glob pattern characters
45
- */
46
- export function isGlobPattern(pattern) {
47
- if (pattern == null) return false;
48
- return /[*?[\]]|(\*\*)/.test(pattern);
49
- }
50
-
51
- export function processContent({ content }) {
52
- // Match markdown regular links [text](link), exclude images ![text](link)
53
- return content.replace(/(?<!!)\[([^\]]+)\]\(([^)]+)\)/g, (match, text, link) => {
54
- const trimLink = link.trim();
55
- // Exclude external links and mailto
56
- if (/^(https?:\/\/|mailto:)/.test(trimLink)) return match;
57
- // Preserve anchors
58
- const [path, hash] = trimLink.split("#");
59
- // Skip if already has extension
60
- if (/\.[a-zA-Z0-9]+$/.test(path)) return match;
61
- // Only process relative paths or paths starting with /
62
- if (!path) return match;
63
- // Flatten to ./xxx-yyy.md
64
- let finalPath = path;
65
- if (path.startsWith(".")) {
66
- finalPath = path.replace(/^\./, "");
67
- }
68
- let flatPath = finalPath.replace(/^\//, "").replace(/\//g, "-");
69
- flatPath = `./${flatPath}.md`;
70
- const newLink = hash ? `${flatPath}#${hash}` : flatPath;
71
- return `[${text}](${newLink})`;
72
- });
73
- }
74
-
75
- // Helper function to generate filename based on language
76
- export function getFileName(docPath, language) {
77
- // Flatten path: remove leading /, replace all / with -
78
- const flatName = docPath.replace(/^\//, "").replace(/\//g, "-");
79
- const isEnglish = language === "en";
80
- return isEnglish ? `${flatName}.md` : `${flatName}.${language}.md`;
81
- }
82
-
83
- /**
84
- * Save a single document to files
85
- * @param {Object} params
86
- * @param {string} params.path - Relative path (without extension)
87
- * @param {string} params.content - Main document content
88
- * @param {string} params.docsDir - Root directory
89
- * @param {string} params.locale - Main content language (e.g., 'en', 'zh', 'fr')
90
- * @param {Array<string>} [params.labels] - Document labels for front matter
91
- * @returns {Promise<{ path: string, success: boolean, error?: string }>}
92
- */
93
- export async function saveDoc({ path: docPath, content, docsDir, locale, labels }) {
94
- try {
95
- await fs.mkdir(docsDir, { recursive: true });
96
- const mainFileName = getFileName(docPath, locale);
97
- const mainFilePath = path.join(docsDir, mainFileName);
98
-
99
- // Add labels front matter if labels are provided
100
- let finalContent = processContent({ content });
101
-
102
- if (labels && labels.length > 0) {
103
- const frontMatter = `---\nlabels: ${JSON.stringify(labels)}\n---\n\n`;
104
- finalContent = frontMatter + finalContent;
105
- }
106
-
107
- await fs.writeFile(mainFilePath, finalContent, "utf8");
108
- return { path: mainFilePath, success: true };
109
- } catch (err) {
110
- return { path: docPath, success: false, error: err.message };
111
- }
112
- }
113
-
114
- export async function saveDocTranslation({
115
- path: docPath,
116
- docsDir,
117
- translation,
118
- language,
119
- labels,
120
- }) {
121
- try {
122
- await fs.mkdir(docsDir, { recursive: true });
123
- const translateFileName = getFileName(docPath, language);
124
- const translatePath = path.join(docsDir, translateFileName);
125
-
126
- // Add labels front matter to translation content if labels are provided
127
- let finalTranslationContent = processContent({
128
- content: translation,
129
- });
130
-
131
- if (labels && labels.length > 0) {
132
- const frontMatter = `---\nlabels: ${JSON.stringify(labels)}\n---\n\n`;
133
- finalTranslationContent = frontMatter + finalTranslationContent;
134
- }
135
-
136
- await fs.writeFile(translatePath, finalTranslationContent, "utf8");
137
- return { path: translatePath, success: true };
138
- } catch (err) {
139
- return { path: docPath, success: false, error: err.message };
140
- }
141
- }
142
-
143
- /**
144
- * Get current git HEAD commit hash
145
- * @returns {string} - The current git HEAD commit hash
146
- */
147
- export function getCurrentGitHead() {
148
- try {
149
- return execSync("git rev-parse HEAD", {
150
- encoding: "utf8",
151
- stdio: ["pipe", "pipe", "ignore"],
152
- }).trim();
153
- } catch (error) {
154
- // Not in git repository or git command failed
155
- console.warn("Failed to get git HEAD:", error.message);
156
- return null;
157
- }
158
- }
159
-
160
- /**
161
- * Save git HEAD to config.yaml file
162
- * @param {string} gitHead - The current git HEAD commit hash
163
- */
164
- export async function saveGitHeadToConfig(gitHead) {
165
- if (!gitHead || process.env.NODE_ENV === "test" || process.env.BUN_TEST) {
166
- return; // Skip if no git HEAD available or in test environment
167
- }
168
-
169
- try {
170
- const docSmithDir = path.join(process.cwd(), "./.aigne/doc-smith");
171
- if (!existsSync(docSmithDir)) {
172
- mkdirSync(docSmithDir, { recursive: true });
173
- }
174
-
175
- const inputFilePath = path.join(docSmithDir, "config.yaml");
176
- let fileContent = "";
177
-
178
- // Read existing file content if it exists
179
- if (existsSync(inputFilePath)) {
180
- fileContent = await fs.readFile(inputFilePath, "utf8");
181
- }
182
-
183
- // Check if lastGitHead already exists in the file
184
- const lastGitHeadRegex = /^lastGitHead:\s*.*$/m;
185
- // Use yaml library to safely serialize the git head value
186
- const yamlContent = yamlStringify({ lastGitHead: gitHead }).trim();
187
- const newLastGitHeadLine = yamlContent;
188
-
189
- if (lastGitHeadRegex.test(fileContent)) {
190
- // Replace existing lastGitHead line
191
- fileContent = fileContent.replace(lastGitHeadRegex, newLastGitHeadLine);
192
- } else {
193
- // Add lastGitHead to the end of file
194
- if (fileContent && !fileContent.endsWith("\n")) {
195
- fileContent += "\n";
196
- }
197
- fileContent += `${newLastGitHeadLine}\n`;
198
- }
199
-
200
- await fs.writeFile(inputFilePath, fileContent);
201
- } catch (error) {
202
- console.warn("Failed to save git HEAD to config.yaml:", error.message);
203
- }
204
- }
205
-
206
- /**
207
- * Check if files have been modified between two git commits
208
- * @param {string} fromCommit - Starting commit hash
209
- * @param {string} toCommit - Ending commit hash (defaults to HEAD)
210
- * @param {Array<string>} filePaths - Array of file paths to check
211
- * @returns {Array<string>} - Array of modified file paths
212
- */
213
- export function getModifiedFilesBetweenCommits(fromCommit, toCommit = "HEAD", filePaths = []) {
214
- try {
215
- // Get all modified files between commits
216
- const modifiedFiles = execSync(`git diff --name-only ${fromCommit}..${toCommit}`, {
217
- encoding: "utf8",
218
- stdio: ["pipe", "pipe", "ignore"],
219
- })
220
- .trim()
221
- .split("\n")
222
- .filter(Boolean);
223
-
224
- // Filter to only include files we care about
225
- if (filePaths.length === 0) {
226
- return modifiedFiles;
227
- }
228
-
229
- return modifiedFiles.filter((file) =>
230
- filePaths.some((targetPath) => {
231
- const absoluteFile = normalizePath(file);
232
- const absoluteTarget = normalizePath(targetPath);
233
- return absoluteFile === absoluteTarget;
234
- }),
235
- );
236
- } catch (error) {
237
- console.warn(
238
- `Failed to get modified files between ${fromCommit} and ${toCommit}:`,
239
- error.message,
240
- );
241
- return [];
242
- }
243
- }
244
-
245
- /**
246
- * Check if any source files have changed based on modified files list
247
- * @param {Array<string>} sourceIds - Source file paths
248
- * @param {Array<string>} modifiedFiles - List of modified files between commits
249
- * @returns {boolean} - True if any source files have changed
250
- */
251
- export function hasSourceFilesChanged(sourceIds, modifiedFiles) {
252
- if (!sourceIds || sourceIds.length === 0 || !modifiedFiles) {
253
- return false; // No source files or no modified files
254
- }
255
-
256
- return modifiedFiles.some((modifiedFile) =>
257
- sourceIds.some((sourceId) => {
258
- const absoluteModifiedFile = normalizePath(modifiedFile);
259
- const absoluteSourceId = normalizePath(sourceId);
260
- return absoluteModifiedFile === absoluteSourceId;
261
- }),
262
- );
263
- }
264
-
265
- /**
266
- * Check if there are any added or deleted files between two git commits that match the include/exclude patterns
267
- * @param {string} fromCommit - Starting commit hash
268
- * @param {string} toCommit - Ending commit hash (defaults to HEAD)
269
- * @param {Array<string>} includePatterns - Include patterns to match files
270
- * @param {Array<string>} excludePatterns - Exclude patterns to filter files
271
- * @returns {boolean} - True if there are relevant added/deleted files
272
- */
273
- export function hasFileChangesBetweenCommits(
274
- fromCommit,
275
- toCommit = "HEAD",
276
- includePatterns = DEFAULT_INCLUDE_PATTERNS,
277
- excludePatterns = DEFAULT_EXCLUDE_PATTERNS,
278
- ) {
279
- try {
280
- // Get file changes with status (A=added, D=deleted, M=modified)
281
- const changes = execSync(`git diff --name-status ${fromCommit}..${toCommit}`, {
282
- encoding: "utf8",
283
- stdio: ["pipe", "pipe", "ignore"],
284
- })
285
- .trim()
286
- .split("\n")
287
- .filter(Boolean);
288
-
289
- // Only check for added (A) and deleted (D) files
290
- const addedOrDeletedFiles = changes
291
- .filter((line) => {
292
- const [status, filePath] = line.split(/\s+/);
293
- return (status === "A" || status === "D") && filePath;
294
- })
295
- .map((line) => line.split(/\s+/)[1]);
296
-
297
- if (addedOrDeletedFiles.length === 0) {
298
- return false;
299
- }
300
-
301
- // Check if any of the added/deleted files match the include patterns and don't match exclude patterns
302
- return addedOrDeletedFiles.some((filePath) => {
303
- // Check if file matches any include pattern
304
- const matchesInclude = includePatterns.some((pattern) => {
305
- // Skip empty patterns
306
- if (!pattern || !pattern.trim()) {
307
- return false;
308
- }
309
- // First escape all regex special characters except * and ?
310
- const escapedPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
311
- // Then convert glob wildcards to regex
312
- const regexPattern = escapedPattern.replace(/\*/g, ".*").replace(/\?/g, ".");
313
- // Only create regex if pattern is not empty
314
- if (!regexPattern) {
315
- return false;
316
- }
317
- const regex = new RegExp(regexPattern);
318
- return regex.test(filePath);
319
- });
320
-
321
- if (!matchesInclude) {
322
- return false;
323
- }
324
-
325
- // Check if file matches any exclude pattern
326
- const matchesExclude = excludePatterns.some((pattern) => {
327
- // Skip empty patterns
328
- if (!pattern || !pattern.trim()) {
329
- return false;
330
- }
331
- // First escape all regex special characters except * and ?
332
- const escapedPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
333
- // Then convert glob wildcards to regex
334
- const regexPattern = escapedPattern.replace(/\*/g, ".*").replace(/\?/g, ".");
335
- // Only create regex if pattern is not empty
336
- if (!regexPattern) {
337
- return false;
338
- }
339
- const regex = new RegExp(regexPattern);
340
- return regex.test(filePath);
341
- });
342
-
343
- return !matchesExclude;
344
- });
345
- } catch (error) {
346
- console.warn(
347
- `Failed to check file changes between ${fromCommit} and ${toCommit}:`,
348
- error.message,
349
- );
350
- return false;
351
- }
352
- }
353
-
354
- /**
355
- * Load config from config.yaml file
356
- * @returns {Promise<Object|null>} - The config object or null if file doesn't exist
357
- */
358
- export async function loadConfigFromFile() {
359
- const configPath = path.join(process.cwd(), "./.aigne/doc-smith", "config.yaml");
360
-
361
- try {
362
- if (!existsSync(configPath)) {
363
- return null;
364
- }
365
-
366
- const configContent = await fs.readFile(configPath, "utf8");
367
- return parse(configContent);
368
- } catch (error) {
369
- console.warn("Failed to read config file:", error.message);
370
- return null;
371
- }
372
- }
373
-
374
- /**
375
- * Save value to config.yaml file
376
- * @param {string} key - The config key to save
377
- * @param {string|Array} value - The value to save (can be string or array)
378
- * @param {string} [comment] - Optional comment to add above the key
379
- */
380
- /**
381
- * Handle array value formatting and updating in YAML config
382
- * @param {string} key - The configuration key
383
- * @param {Array} value - The array value to save
384
- * @param {string} comment - Optional comment
385
- * @param {string} fileContent - Current file content
386
- * @returns {string} Updated file content
387
- */
388
- function handleArrayValueUpdate(key, value, comment, fileContent) {
389
- // Skip if key is empty to avoid "Empty regular expressions are not allowed" error
390
- if (!key || !key.trim()) {
391
- return fileContent;
392
- }
393
- // Use yaml library to safely serialize the key-value pair
394
- const yamlObject = { [key]: value };
395
- const yamlContent = yamlStringify(yamlObject).trim();
396
- const formattedValue = yamlContent;
397
-
398
- const lines = fileContent.split("\n");
399
-
400
- // Find the start line of the key
401
- const keyStartIndex = lines.findIndex((line) => line.match(new RegExp(`^${key}:\\s*`)));
402
-
403
- if (keyStartIndex !== -1) {
404
- // Find the end of the array (next non-indented line or end of file)
405
- let keyEndIndex = keyStartIndex;
406
- for (let i = keyStartIndex + 1; i < lines.length; i++) {
407
- const line = lines[i].trim();
408
- // If line is empty, starts with comment, or doesn't start with "- ", it's not part of the array
409
- if (line === "" || line.startsWith("#") || (!line.startsWith("- ") && !line.match(/^\w+:/))) {
410
- if (!line.startsWith("- ")) {
411
- keyEndIndex = i - 1;
412
- break;
413
- }
414
- } else if (line.match(/^\w+:/)) {
415
- // Found another key, stop here
416
- keyEndIndex = i - 1;
417
- break;
418
- } else if (line.startsWith("- ")) {
419
- keyEndIndex = i;
420
- }
421
- }
422
-
423
- // If we reached the end of file
424
- if (keyEndIndex === keyStartIndex) {
425
- // Check if the value is on the same line
426
- const keyLine = lines[keyStartIndex];
427
- if (keyLine.includes("[") || !keyLine.endsWith(":")) {
428
- keyEndIndex = keyStartIndex;
429
- } else {
430
- // Find the actual end of the array
431
- for (let i = keyStartIndex + 1; i < lines.length; i++) {
432
- const line = lines[i].trim();
433
- if (line.startsWith("- ")) {
434
- keyEndIndex = i;
435
- } else if (line !== "" && !line.startsWith("#")) {
436
- break;
437
- }
438
- }
439
- }
440
- }
441
-
442
- // Replace the entire array section
443
- const replacementLines = formattedValue.split("\n");
444
- lines.splice(keyStartIndex, keyEndIndex - keyStartIndex + 1, ...replacementLines);
445
-
446
- // Add comment if provided and not already present
447
- if (comment && keyStartIndex > 0 && !lines[keyStartIndex - 1].trim().startsWith("# ")) {
448
- lines.splice(keyStartIndex, 0, `# ${comment}`);
449
- }
450
-
451
- return lines.join("\n");
452
- } else {
453
- // Add new array to end of file
454
- let updatedContent = fileContent;
455
- if (updatedContent && !updatedContent.endsWith("\n")) {
456
- updatedContent += "\n";
457
- }
458
-
459
- // Add comment if provided
460
- if (comment) {
461
- updatedContent += `# ${comment}\n`;
462
- }
463
-
464
- updatedContent += `${formattedValue}\n`;
465
- return updatedContent;
466
- }
467
- }
468
-
469
- /**
470
- * Handle string value formatting and updating in YAML config
471
- * @param {string} key - The configuration key
472
- * @param {string} value - The string value to save
473
- * @param {string} comment - Optional comment
474
- * @param {string} fileContent - Current file content
475
- * @returns {string} Updated file content
476
- */
477
- function handleStringValueUpdate(key, value, comment, fileContent) {
478
- // Skip if key is empty to avoid "Empty regular expressions are not allowed" error
479
- if (!key || !key.trim()) {
480
- return fileContent;
481
- }
482
- // Use yaml library to safely serialize the key-value pair
483
- const yamlObject = { [key]: value };
484
- const yamlContent = yamlStringify(yamlObject).trim();
485
- const formattedValue = yamlContent;
486
- const lines = fileContent.split("\n");
487
-
488
- // Handle string values (original logic)
489
- const keyRegex = new RegExp(`^${key}:\\s*.*$`);
490
- const keyIndex = lines.findIndex((line) => keyRegex.test(line));
491
-
492
- if (keyIndex !== -1) {
493
- // Replace existing key line
494
- lines[keyIndex] = formattedValue;
495
-
496
- // Add comment if provided and not already present
497
- if (comment) {
498
- const hasCommentAbove = keyIndex > 0 && lines[keyIndex - 1].trim().startsWith("# ");
499
- if (!hasCommentAbove) {
500
- // Add comment above the key if it doesn't already have one
501
- lines.splice(keyIndex, 0, `# ${comment}`);
502
- }
503
- }
504
-
505
- return lines.join("\n");
506
- } else {
507
- // Add key to the end of file
508
- let updatedContent = fileContent;
509
- if (updatedContent && !updatedContent.endsWith("\n")) {
510
- updatedContent += "\n";
511
- }
512
-
513
- // Add comment if provided
514
- if (comment) {
515
- updatedContent += `# ${comment}\n`;
516
- }
517
-
518
- updatedContent += `${formattedValue}\n`;
519
- return updatedContent;
520
- }
521
- }
522
-
523
- export async function saveValueToConfig(key, value, comment) {
524
- if (value === undefined) {
525
- return; // Skip if value is undefined
526
- }
527
-
528
- try {
529
- const docSmithDir = path.join(process.cwd(), "./.aigne/doc-smith");
530
- if (!existsSync(docSmithDir)) {
531
- mkdirSync(docSmithDir, { recursive: true });
532
- }
533
-
534
- const configPath = path.join(docSmithDir, "config.yaml");
535
- let fileContent = "";
536
-
537
- // Read existing file content if it exists
538
- if (existsSync(configPath)) {
539
- fileContent = await fs.readFile(configPath, "utf8");
540
- }
541
-
542
- // Use extracted helper functions for better maintainability
543
- let updatedContent;
544
- if (Array.isArray(value)) {
545
- updatedContent = handleArrayValueUpdate(key, value, comment, fileContent);
546
- } else {
547
- updatedContent = handleStringValueUpdate(key, value, comment, fileContent);
548
- }
549
-
550
- await fs.writeFile(configPath, updatedContent);
551
- } catch (error) {
552
- console.warn(`Failed to save ${key} to config.yaml:`, error.message);
553
- }
554
- }
555
-
556
- /**
557
- * Validate if a path exists and is accessible
558
- * @param {string} filePath - The path to validate (can be absolute or relative)
559
- * @returns {Object} - Validation result with isValid boolean and error message
560
- */
561
- export function validatePath(filePath) {
562
- try {
563
- const absolutePath = normalizePath(filePath);
564
-
565
- // Check if path exists
566
- if (!existsSync(absolutePath)) {
567
- return {
568
- isValid: false,
569
- error: `Path does not exist: ${filePath}`,
570
- };
571
- }
572
-
573
- // Check if path is accessible (readable)
574
- try {
575
- accessSync(absolutePath, constants.R_OK);
576
- } catch (_accessError) {
577
- return {
578
- isValid: false,
579
- error: `Path is not accessible: ${filePath}`,
580
- };
581
- }
582
-
583
- return {
584
- isValid: true,
585
- error: null,
586
- };
587
- } catch (_error) {
588
- return {
589
- isValid: false,
590
- error: `Invalid path format: ${filePath}`,
591
- };
592
- }
593
- }
594
-
595
- /**
596
- * Validate multiple paths and return validation results
597
- * @param {Array<string>} paths - Array of paths to validate
598
- * @returns {Object} - Validation results with validPaths array and errors array
599
- */
600
- export function validatePaths(paths) {
601
- const validPaths = [];
602
- const errors = [];
603
-
604
- for (const path of paths) {
605
- const validation = validatePath(path);
606
- if (validation.isValid) {
607
- validPaths.push(path);
608
- } else {
609
- errors.push({
610
- path: path,
611
- error: validation.error,
612
- });
613
- }
614
- }
615
-
616
- return {
617
- validPaths,
618
- errors,
619
- };
620
- }
621
-
622
- /**
623
- * Check if input is a valid directory or file and add it to results if so
624
- * @param {string} searchTerm - The search term to check
625
- * @param {Array} results - The results array to modify
626
- */
627
- function addExactPathMatch(searchTerm, results) {
628
- const inputValidation = validatePath(searchTerm);
629
- if (inputValidation.isValid) {
630
- const stats = statSync(normalizePath(searchTerm));
631
- const isDirectory = stats.isDirectory();
632
- results.unshift({
633
- name: searchTerm,
634
- value: searchTerm,
635
- description: isDirectory ? "📁 Directory" : "📄 File",
636
- });
637
- }
638
- }
639
-
640
- /**
641
- * Get available paths for search suggestions based on user input
642
- * @param {string} userInput - User's input string
643
- * @returns {Array<Object>} - Array of path objects with name, value, and description
644
- */
645
- export function getAvailablePaths(userInput = "") {
646
- try {
647
- const searchTerm = userInput.trim();
648
-
649
- // If no input, return current directory contents
650
- if (!searchTerm) {
651
- return getDirectoryContents("./");
652
- }
653
-
654
- let results = [];
655
-
656
- // Handle absolute paths
657
- if (searchTerm.startsWith("/")) {
658
- const dirPath = path.dirname(searchTerm);
659
- const fileName = path.basename(searchTerm);
660
- results = getDirectoryContents(dirPath, fileName);
661
- addExactPathMatch(searchTerm, results);
662
- }
663
- // Handle relative paths
664
- else if (searchTerm.startsWith("./") || searchTerm.startsWith("../")) {
665
- // Extract directory path and search term
666
- const lastSlashIndex = searchTerm.lastIndexOf("/");
667
- if (lastSlashIndex === -1) {
668
- // No slash found, treat as current directory search
669
- results = getDirectoryContents("./", searchTerm);
670
- addExactPathMatch(searchTerm, results);
671
- } else {
672
- const dirPath = searchTerm.substring(0, lastSlashIndex + 1);
673
- const fileName = searchTerm.substring(lastSlashIndex + 1);
674
-
675
- // Validate directory path
676
- const validation = validatePath(dirPath);
677
- if (!validation.isValid) {
678
- return [
679
- {
680
- name: dirPath,
681
- value: dirPath,
682
- description: validation.error,
683
- },
684
- ];
685
- }
686
-
687
- results = getDirectoryContents(dirPath, fileName);
688
- addExactPathMatch(searchTerm, results);
689
- }
690
- }
691
- // Handle simple file/directory names (search in current directory)
692
- else {
693
- results = getDirectoryContents("./", searchTerm);
694
- addExactPathMatch(searchTerm, results);
695
- }
696
-
697
- // Remove duplicates based on absolute path (real deduplication)
698
- const uniqueResults = [];
699
- const seenAbsolutePaths = new Set();
700
-
701
- for (const item of results) {
702
- // Normalize to absolute path for proper deduplication
703
- const absolutePath = normalizePath(item.value);
704
- if (!seenAbsolutePaths.has(absolutePath)) {
705
- seenAbsolutePaths.add(absolutePath);
706
- uniqueResults.push(item);
707
- }
708
- }
709
-
710
- return uniqueResults;
711
- } catch (error) {
712
- console.warn(`Failed to get available paths for "${userInput}":`, error.message);
713
- return [];
714
- }
715
- }
716
-
717
- /**
718
- * Get directory contents for a specific path
719
- * @param {string} dirPath - Directory path to search in
720
- * @param {string} searchTerm - Optional search term to filter results
721
- * @returns {Array<Object>} - Array of path objects (both files and directories)
722
- */
723
- function getDirectoryContents(dirPath, searchTerm = "") {
724
- try {
725
- const absoluteDirPath = normalizePath(dirPath);
726
-
727
- // Check if directory exists
728
- if (!existsSync(absoluteDirPath)) {
729
- return [
730
- {
731
- name: dirPath,
732
- value: dirPath,
733
- description: "Directory does not exist",
734
- },
735
- ];
736
- }
737
-
738
- const items = [];
739
-
740
- // Read directory contents
741
- const entries = readdirSync(absoluteDirPath, { withFileTypes: true });
742
-
743
- for (const entry of entries) {
744
- const entryName = entry.name;
745
-
746
- // Preserve ./ prefix when dirPath is "./"
747
- let relativePath = path.join(dirPath, entryName);
748
- if (dirPath?.startsWith("./")) {
749
- relativePath = `./${relativePath}`;
750
- }
751
-
752
- // Filter by search term if provided
753
- if (searchTerm && !entryName.toLowerCase().includes(searchTerm.toLowerCase())) {
754
- continue;
755
- }
756
-
757
- // Skip hidden files and common ignore patterns
758
- if (
759
- entryName.startsWith(".") ||
760
- entryName === "node_modules" ||
761
- entryName === ".git" ||
762
- entryName === "dist" ||
763
- entryName === "build"
764
- ) {
765
- continue;
766
- }
767
-
768
- const isDirectory = entry.isDirectory();
769
-
770
- // Include both directories and files
771
- items.push({
772
- name: relativePath,
773
- value: relativePath,
774
- description: isDirectory ? "📁 Directory" : "📄 File",
775
- });
776
- }
777
-
778
- // Sort alphabetically (directories first, then files)
779
- items.sort((a, b) => {
780
- const aIsDir = a.description === "📁 Directory";
781
- const bIsDir = b.description === "📁 Directory";
782
-
783
- if (aIsDir && !bIsDir) return -1;
784
- if (!aIsDir && bIsDir) return 1;
785
-
786
- return a.name.localeCompare(b.name);
787
- });
788
-
789
- return items;
790
- } catch (error) {
791
- console.warn(`Failed to get directory contents from ${dirPath}:`, error.message);
792
- return [];
793
- }
794
- }
795
-
796
- /**
797
- * Get GitHub repository URL from git remote
798
- * @returns {string} GitHub repository URL or empty string if not a GitHub repo (e.g. git@github.com:xxxx/xxxx.git)
799
- */
800
- export function getGithubRepoUrl() {
801
- try {
802
- const gitRemote = execSync("git remote get-url origin", {
803
- encoding: "utf8",
804
- stdio: ["pipe", "pipe", "ignore"],
805
- }).trim();
806
-
807
- // Check if it's a GitHub repository
808
- if (gitRemote.includes("github.com")) {
809
- return gitRemote;
810
- }
811
-
812
- return "";
813
- } catch {
814
- // Not in git repository or no origin remote
815
- return "";
816
- }
817
- }
818
-
819
- /**
820
- * Get GitHub repository information
821
- * @param {string} repoUrl - The repository URL
822
- * @returns {Promise<Object>} - Repository information
823
- */
824
- export async function getGitHubRepoInfo(repoUrl) {
825
- try {
826
- // Extract owner and repo from GitHub URL
827
- const match = repoUrl.match(/github\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?$/);
828
- if (!match) return null;
829
-
830
- const [, owner, repo] = match;
831
- const apiUrl = `https://api.github.com/repos/${owner}/${repo}`;
832
-
833
- const response = await fetch(apiUrl);
834
-
835
- if (!response.ok) {
836
- console.warn("Failed to fetch GitHub repository info:", repoUrl, response.statusText);
837
- return null;
838
- }
839
-
840
- const data = await response.json();
841
- return {
842
- name: data.name,
843
- description: data.description || "",
844
- icon: data.owner?.avatar_url || "",
845
- };
846
- } catch (error) {
847
- console.warn("Failed to fetch GitHub repository info:", error.message);
848
- return null;
849
- }
850
- }
851
-
852
- /**
853
- * Get project information automatically without user confirmation
854
- * @returns {Promise<Object>} - Project information including name, description, icon, and fromGitHub flag
855
- */
856
- export async function getProjectInfo() {
857
- let repoInfo = null;
858
- let defaultName = path.basename(process.cwd());
859
- let defaultDescription = "";
860
- let defaultIcon = "";
861
- let fromGitHub = false;
862
-
863
- // Check if we're in a git repository
864
- try {
865
- const gitRemote = execSync("git remote get-url origin", {
866
- encoding: "utf8",
867
- stdio: ["pipe", "pipe", "ignore"],
868
- }).trim();
869
-
870
- // Extract repository name from git remote URL
871
- const repoName = gitRemote.split("/").pop().replace(".git", "");
872
- defaultName = repoName;
873
-
874
- // If it's a GitHub repository, try to get additional info
875
- if (gitRemote.includes("github.com")) {
876
- repoInfo = await getGitHubRepoInfo(gitRemote);
877
- if (repoInfo) {
878
- defaultDescription = repoInfo.description;
879
- defaultIcon = repoInfo.icon;
880
- fromGitHub = true;
881
- }
882
- }
883
- } catch (_error) {
884
- // Not in git repository or no origin remote, use current directory name
885
- console.warn("No git repository found, using current directory name");
886
- }
887
-
888
- return {
889
- name: defaultName,
890
- description: defaultDescription,
891
- icon: defaultIcon,
892
- fromGitHub,
893
- };
894
- }
895
-
896
- /**
897
- * Process document purpose configuration and generate purpose rules
898
- * @param {Array<string>} documentPurpose - Array of document purpose keys
899
- * @returns {Object} Object containing purposes string and rules content
900
- */
901
- export function processDocumentPurpose(documentPurpose) {
902
- if (!documentPurpose || !Array.isArray(documentPurpose)) {
903
- return { purposes: "" };
904
- }
905
-
906
- const purposeRules = documentPurpose
907
- .map((key) => {
908
- const style = DOCUMENT_STYLES[key];
909
- if (!style) return null;
910
- return `Document Purpose - ${style.name}:\n${style.description}\n${style.content}`;
911
- })
912
- .filter(Boolean);
913
-
914
- if (purposeRules.length === 0) {
915
- return { purposes: "" };
916
- }
917
-
918
- const purposes = purposeRules.join("\n\n");
919
- return {
920
- purposes,
921
- };
922
- }
923
-
924
- /**
925
- * Process target audience configuration and generate audience rules and names
926
- * @param {Array<string>} targetAudienceTypes - Array of target audience type keys
927
- * @param {string} existingTargetAudience - Existing target audience content
928
- * @returns {Object} Object containing audiences string, targetAudience string, and rules content
929
- */
930
- export function processTargetAudience(targetAudienceTypes, existingTargetAudience = "") {
931
- if (!targetAudienceTypes || !Array.isArray(targetAudienceTypes)) {
932
- return { audiences: "", targetAudience: existingTargetAudience || "" };
933
- }
934
-
935
- // Get structured content for rules
936
- const audienceRules = targetAudienceTypes
937
- .map((key) => {
938
- const audience = TARGET_AUDIENCES[key];
939
- if (!audience) return null;
940
- return `Target Audience - ${audience.name}:\n${audience.description}\n${audience.content}`;
941
- })
942
- .filter(Boolean);
943
-
944
- let audiences = "";
945
- if (audienceRules.length > 0) {
946
- audiences = audienceRules.join("\n\n");
947
- }
948
-
949
- // Get names for targetAudience field
950
- const audienceNames = targetAudienceTypes
951
- .map((key) => TARGET_AUDIENCES[key]?.name)
952
- .filter(Boolean)
953
- .join(", ");
954
-
955
- let targetAudience = existingTargetAudience || "";
956
- if (audienceNames) {
957
- const existingTargetAudienceTrimmed = existingTargetAudience?.trim();
958
- if (existingTargetAudienceTrimmed) {
959
- targetAudience = `${existingTargetAudienceTrimmed}\n\n${audienceNames}`;
960
- } else {
961
- targetAudience = audienceNames;
962
- }
963
- }
964
-
965
- return {
966
- audiences,
967
- targetAudience,
968
- };
969
- }
970
-
971
- /**
972
- * Process configuration fields - convert keys to actual content
973
- * @param {Object} config - Parsed configuration
974
- * @returns {Object} Processed configuration with content fields
975
- */
976
- export async function processConfigFields(config) {
977
- const processed = {};
978
- const allRulesContent = [];
979
-
980
- // Set default values for missing or empty fields
981
- const defaults = {
982
- nodeName: "Section",
983
- locale: "en",
984
- sourcesPath: ["./"],
985
- docsDir: "./.aigne/doc-smith/docs",
986
- outputDir: "./.aigne/doc-smith/output",
987
- translateLanguages: [],
988
- rules: "",
989
- targetAudience: "",
990
- };
991
-
992
- // Apply defaults for missing or empty fields
993
- for (const [key, defaultValue] of Object.entries(defaults)) {
994
- if (
995
- !config[key] ||
996
- (Array.isArray(defaultValue) && (!config[key] || config[key].length === 0)) ||
997
- (typeof defaultValue === "string" && (!config[key] || config[key].trim() === ""))
998
- ) {
999
- processed[key] = defaultValue;
1000
- }
1001
- }
1002
-
1003
- // Check if original rules field has content
1004
- if (config.rules) {
1005
- if (typeof config.rules === "string") {
1006
- const existingRules = config.rules.trim();
1007
- if (existingRules) {
1008
- // load rules from remote url
1009
- if (isRemoteFile(existingRules)) {
1010
- const remoteFileContent = await getRemoteFileContent(existingRules);
1011
- if (remoteFileContent) {
1012
- allRulesContent.push(remoteFileContent);
1013
- }
1014
- } else {
1015
- allRulesContent.push(existingRules);
1016
- }
1017
- }
1018
- } else if (Array.isArray(config.rules)) {
1019
- // Handle array of rules - join them with newlines
1020
- const rulesText = config.rules
1021
- .filter((rule) => typeof rule === "string" && rule.trim())
1022
- .join("\n\n");
1023
- if (rulesText) {
1024
- allRulesContent.push(rulesText);
1025
- }
1026
- }
1027
- }
1028
-
1029
- // Process document purpose (array)
1030
- const documentPurposeResult = processDocumentPurpose(config.documentPurpose);
1031
- if (documentPurposeResult.purposes) {
1032
- allRulesContent.push(documentPurposeResult.purposes);
1033
- processed.purposes = documentPurposeResult.purposes;
1034
- }
1035
-
1036
- // Process target audience types (array)
1037
- const targetAudienceResult = processTargetAudience(
1038
- config.targetAudienceTypes,
1039
- config.targetAudience,
1040
- );
1041
- if (targetAudienceResult.audiences) {
1042
- allRulesContent.push(targetAudienceResult.audiences);
1043
- processed.audiences = targetAudienceResult.audiences;
1044
- }
1045
- if (targetAudienceResult.targetAudience) {
1046
- processed.targetAudience = targetAudienceResult.targetAudience;
1047
- }
1048
-
1049
- // Process reader knowledge level (single value)
1050
- let knowledgeContent = "";
1051
- if (config.readerKnowledgeLevel) {
1052
- const knowledgeLevel = READER_KNOWLEDGE_LEVELS[config.readerKnowledgeLevel];
1053
- if (knowledgeLevel) {
1054
- knowledgeContent = knowledgeLevel.content;
1055
- processed.readerKnowledgeContent = knowledgeContent;
1056
- allRulesContent.push(`Reader Knowledge Level:\n${knowledgeContent}`);
1057
-
1058
- processed.readerKnowledgeLevel = `Reader Knowledge Level - ${knowledgeLevel.name} : ${knowledgeLevel.description} \n${knowledgeContent}`;
1059
- }
1060
- }
1061
-
1062
- // Process documentation depth (single value)
1063
- let depthContent = "";
1064
- if (config.documentationDepth) {
1065
- const depthLevel = DOCUMENTATION_DEPTH[config.documentationDepth];
1066
- if (depthLevel) {
1067
- depthContent = depthLevel.content;
1068
- processed.documentationDepthContent = depthContent;
1069
- allRulesContent.push(`Documentation Depth:\n${depthContent}`);
1070
-
1071
- processed.coverageDepth = `Documentation Depth - ${depthLevel.name} : ${depthLevel.description} \n${depthContent}`;
1072
- }
1073
- }
1074
-
1075
- if (config.glossary) {
1076
- if (isRemoteFile(config.glossary)) {
1077
- processed.glossary = await getRemoteFileContent(config.glossary);
1078
- }
1079
- }
1080
-
1081
- // Detect and handle conflicts in user selections
1082
- const conflicts = detectResolvableConflicts(config);
1083
- if (conflicts.length > 0) {
1084
- const conflictResolutionRules = generateConflictResolutionRules(conflicts);
1085
- allRulesContent.push(conflictResolutionRules);
1086
-
1087
- // Store conflict information for debugging/logging
1088
- processed.detectedConflicts = conflicts;
1089
- }
1090
-
1091
- // Combine all content into rules field
1092
- if (allRulesContent.length > 0) {
1093
- processed.rules = allRulesContent.join("\n\n");
1094
- }
1095
-
1096
- return processed;
1097
- }
1098
-
1099
- /**
1100
- * Recursively resolves file references in a configuration object.
1101
- *
1102
- * This function traverses the input object, array, or string recursively. Any string value that starts
1103
- * with '@' is treated as a file reference, and the file's content is loaded asynchronously. Supported
1104
- * file formats include .txt, .md, .json, .yaml, and .yml. For .json and .yaml/.yml files, the content
1105
- * is parsed into objects; for .txt and .md, the raw string is returned.
1106
- *
1107
- * If a file cannot be loaded (e.g., does not exist, is of unsupported type, or parsing fails), the
1108
- * original string value (with '@' prefix) is returned in place of the file content.
1109
- *
1110
- * The function processes nested arrays and objects recursively, returning a new structure with file
1111
- * contents loaded in place of references. The input object is not mutated.
1112
- *
1113
- * Examples of supported file reference formats:
1114
- * - "@notes.txt"
1115
- * - "@docs/readme.md"
1116
- * - "@config/settings.json"
1117
- * - "@data.yaml"
1118
- *
1119
- * @param {any} obj - The configuration object, array, or string to process.
1120
- * @param {string} basePath - Base path for resolving relative file paths (defaults to process.cwd()).
1121
- * @returns {Promise<any>} - The processed configuration with file content loaded in place of references.
1122
- */
1123
- export async function resolveFileReferences(obj, basePath = process.cwd()) {
1124
- if (typeof obj === "string") {
1125
- if (obj.startsWith("@")) {
1126
- return await loadFileContent(obj.slice(1), basePath);
1127
- }
1128
- }
1129
-
1130
- if (Array.isArray(obj)) {
1131
- return Promise.all(obj.map((item) => resolveFileReferences(item, basePath)));
1132
- }
1133
-
1134
- if (obj && typeof obj === "object") {
1135
- const result = {};
1136
- for (const [key, value] of Object.entries(obj)) {
1137
- result[key] = await resolveFileReferences(value, basePath);
1138
- }
1139
- return result;
1140
- }
1141
-
1142
- return obj;
1143
- }
1144
-
1145
- /**
1146
- * Load content from a file path
1147
- * @param {string} filePath - The file path to load
1148
- * @param {string} basePath - Base path for resolving relative paths
1149
- * @returns {Promise<any>} - The loaded content or original path if loading fails
1150
- */
1151
- export async function loadFileContent(filePath, basePath) {
1152
- try {
1153
- // Resolve path - if absolute, use as is; if relative, resolve from basePath
1154
- const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(basePath, filePath);
1155
-
1156
- // Check if file exists
1157
- if (!existsSync(resolvedPath)) {
1158
- return `@${filePath}`; // Return original value if file doesn't exist
1159
- }
1160
-
1161
- // Check file extension
1162
- const ext = path.extname(resolvedPath).toLowerCase();
1163
-
1164
- if (!SUPPORTED_FILE_EXTENSIONS.includes(ext)) {
1165
- return `@${filePath}`; // Return original value if unsupported file type
1166
- }
1167
-
1168
- // Read file content
1169
- const content = await fs.readFile(resolvedPath, "utf-8");
1170
-
1171
- // Parse JSON/YAML files
1172
- if (ext === ".json") {
1173
- try {
1174
- return JSON.parse(content);
1175
- } catch {
1176
- return content; // Return raw string if JSON parsing fails
1177
- }
1178
- }
1179
-
1180
- if (ext === ".yaml" || ext === ".yml") {
1181
- try {
1182
- return parse(content);
1183
- } catch {
1184
- return content; // Return raw string if YAML parsing fails
1185
- }
1186
- }
1187
-
1188
- // Return raw content for .txt and .md files
1189
- return content;
1190
- } catch {
1191
- // Return original value if any error occurs
1192
- return `@${filePath}`;
1193
- }
1194
- }
1195
-
1196
- /**
1197
- * Detect system language and map to supported language code
1198
- * @returns {string} - Supported language code (defaults to 'en' if detection fails or unsupported)
1199
- */
1200
- export function detectSystemLanguage() {
1201
- try {
1202
- // Try multiple methods to detect system language
1203
- let systemLocale = null;
1204
-
1205
- // Method 1: Environment variables (most reliable on Unix systems)
1206
- systemLocale = process.env.LANG || process.env.LANGUAGE || process.env.LC_ALL;
1207
-
1208
- // Method 2: Node.js Intl API (fallback)
1209
- if (!systemLocale) {
1210
- try {
1211
- systemLocale = Intl.DateTimeFormat().resolvedOptions().locale;
1212
- } catch (_error) {
1213
- // Intl API failed, continue to fallback
1214
- }
1215
- }
1216
-
1217
- if (!systemLocale) {
1218
- return "en"; // Default fallback
1219
- }
1220
-
1221
- // Extract language code from locale (e.g., 'zh_CN' -> 'zh', 'en_US' -> 'en')
1222
- const langCode = systemLocale.split(/[-_]/)[0].toLowerCase();
1223
-
1224
- // Map to supported language codes
1225
- const supportedLang = SUPPORTED_LANGUAGES.find((lang) => lang.code === langCode);
1226
- if (supportedLang) {
1227
- return supportedLang.code;
1228
- }
1229
-
1230
- // Handle special cases for Chinese variants
1231
- if (langCode === "zh") {
1232
- // Check for Traditional Chinese indicators
1233
- const fullLocale = systemLocale.toLowerCase();
1234
- if (fullLocale.includes("tw") || fullLocale.includes("hk") || fullLocale.includes("mo")) {
1235
- return "zh-TW";
1236
- }
1237
- return "zh"; // Default to Simplified Chinese
1238
- }
1239
-
1240
- // Return default if no match found
1241
- return "en";
1242
- } catch (_error) {
1243
- // Any error in detection, return default
1244
- return "en";
1245
- }
1246
- }
1247
-
1248
- export function getContentHash(str, { trim = true } = {}) {
1249
- const input = trim && typeof str === "string" ? str.trim() : str;
1250
- return crypto.createHash("sha256").update(input).digest("hex");
1251
- }
1252
-
1253
- function toPath(path) {
1254
- if (Array.isArray(path)) return path;
1255
-
1256
- const result = [];
1257
- path.replace(/[^.[\]]+|\[(\d+|(["'])(.*?)\2)\]/g, (match, bracketContent, quote, quotedKey) => {
1258
- if (quote) {
1259
- // ["key"] or ['key']
1260
- result.push(quotedKey);
1261
- } else if (bracketContent !== undefined) {
1262
- // [123]
1263
- result.push(bracketContent);
1264
- } else {
1265
- // dot notation
1266
- result.push(match);
1267
- }
1268
- });
1269
- return result;
1270
- }
1271
-
1272
- /**
1273
- * Deeply get the value at a given path from an object, or return a default value if missing.
1274
- * @param {object} obj - The object to query.
1275
- * @param {string|Array<string|number>} path - The path to get, as a string or array.
1276
- * @param {*} defaultValue - The value returned if the resolved value is undefined.
1277
- * @returns {*} The value at the path or defaultValue.
1278
- */
1279
- export function dget(obj, path, defaultValue) {
1280
- const parts = toPath(path);
1281
-
1282
- let current = obj;
1283
- for (const key of parts) {
1284
- if (current == null || !(key in current)) return defaultValue;
1285
- current = current[key];
1286
- }
1287
- return current;
1288
- }
1289
-
1290
- /**
1291
- * Deeply set the value at a given path in an object.
1292
- * @param {object} obj - The object to modify.
1293
- * @param {string|Array<string|number>} path - The path to set, as a string or array.
1294
- * @param {*} value - The value to set.
1295
- * @returns {object} The modified object.
1296
- */
1297
- export function dset(obj, path, value) {
1298
- const parts = toPath(path);
1299
-
1300
- let current = obj;
1301
- for (let i = 0; i < parts.length; i++) {
1302
- const key = parts[i];
1303
-
1304
- if (i === parts.length - 1) {
1305
- current[key] = value;
1306
- } else {
1307
- if (current[key] == null || typeof current[key] !== "object") {
1308
- current[key] = String(parts[i + 1]).match(/^\d+$/) ? [] : {};
1309
- }
1310
- current = current[key];
1311
- }
1312
- }
1313
- return obj;
1314
- }
1315
-
1316
- /**
1317
- * Create a context path manager that provides get/set/clear operations
1318
- * @param {object} options - The options object containing user context
1319
- * @param {string} path - The context path (e.g., 'currentPageDetails./about' or 'lastToolInputs./about')
1320
- * @returns {object} An object with { get, set } methods and a contextPath method for sub-paths
1321
- */
1322
- export function userContextAt(options, path) {
1323
- const userContext = options?.context?.userContext || null;
1324
- if (!userContext) {
1325
- throw new Error("userContext is not available");
1326
- }
1327
-
1328
- return {
1329
- /**
1330
- * Get a value from the context path
1331
- * @param {string} [key] - Optional key for nested access (e.g., 'updateMeta' for lastToolInputs)
1332
- * @returns {*} The value at the path, or undefined if not found
1333
- */
1334
- get(key) {
1335
- if (key !== undefined) {
1336
- return dget(userContext, `${path}.${key}`);
1337
- }
1338
- return dget(userContext, path);
1339
- },
1340
-
1341
- /**
1342
- * Set a value in the context path
1343
- * @param {string|*} key - If key is provided, this is the key; otherwise this is the value
1344
- * @param {*} [value] - The value to set (required if first param is a key)
1345
- */
1346
- set(key, value) {
1347
- if (value !== undefined) {
1348
- dset(userContext, `${path}.${key}`, value);
1349
- } else {
1350
- dset(userContext, path, key);
1351
- }
1352
- },
1353
- };
1354
- }