@aigne/doc-smith 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/.aigne/doc-smith/config.yaml +70 -0
  2. package/.aigne/doc-smith/output/structure-plan.json +152 -0
  3. package/.aigne/doc-smith/preferences.yml +31 -0
  4. package/.aigne/doc-smith/upload-cache.yaml +288 -0
  5. package/.github/workflows/ci.yml +46 -0
  6. package/.github/workflows/reviewer.yml +2 -1
  7. package/CHANGELOG.md +17 -0
  8. package/README.md +33 -15
  9. package/agents/chat.yaml +30 -0
  10. package/agents/check-structure-plan.mjs +1 -1
  11. package/agents/docs-fs.yaml +25 -0
  12. package/agents/exit.mjs +6 -0
  13. package/agents/feedback-refiner.yaml +5 -1
  14. package/agents/find-items-by-paths.mjs +10 -4
  15. package/agents/fs.mjs +60 -0
  16. package/agents/input-generator.mjs +150 -91
  17. package/agents/load-config.mjs +0 -5
  18. package/agents/load-sources.mjs +61 -8
  19. package/agents/publish-docs.mjs +27 -12
  20. package/agents/retranslate.yaml +1 -1
  21. package/agents/team-publish-docs.yaml +2 -2
  22. package/aigne.yaml +1 -0
  23. package/docs/_sidebar.md +17 -0
  24. package/docs/advanced-how-it-works.md +104 -0
  25. package/docs/advanced-how-it-works.zh.md +104 -0
  26. package/docs/advanced-quality-assurance.md +64 -0
  27. package/docs/advanced-quality-assurance.zh.md +64 -0
  28. package/docs/advanced.md +28 -0
  29. package/docs/advanced.zh.md +28 -0
  30. package/docs/changelog.md +272 -0
  31. package/docs/changelog.zh.md +272 -0
  32. package/docs/cli-reference.md +185 -0
  33. package/docs/cli-reference.zh.md +185 -0
  34. package/docs/configuration-interactive-setup.md +82 -0
  35. package/docs/configuration-interactive-setup.zh.md +82 -0
  36. package/docs/configuration-language-support.md +64 -0
  37. package/docs/configuration-language-support.zh.md +64 -0
  38. package/docs/configuration-llm-setup.md +90 -0
  39. package/docs/configuration-llm-setup.zh.md +90 -0
  40. package/docs/configuration-preferences.md +122 -0
  41. package/docs/configuration-preferences.zh.md +123 -0
  42. package/docs/configuration.md +173 -0
  43. package/docs/configuration.zh.md +173 -0
  44. package/docs/features-generate-documentation.md +82 -0
  45. package/docs/features-generate-documentation.zh.md +82 -0
  46. package/docs/features-publish-your-docs.md +98 -0
  47. package/docs/features-publish-your-docs.zh.md +98 -0
  48. package/docs/features-translate-documentation.md +83 -0
  49. package/docs/features-translate-documentation.zh.md +83 -0
  50. package/docs/features-update-and-refine.md +86 -0
  51. package/docs/features-update-and-refine.zh.md +86 -0
  52. package/docs/features.md +56 -0
  53. package/docs/features.zh.md +56 -0
  54. package/docs/getting-started.md +74 -0
  55. package/docs/getting-started.zh.md +74 -0
  56. package/docs/overview.md +48 -0
  57. package/docs/overview.zh.md +48 -0
  58. package/media.md +19 -0
  59. package/package.json +13 -10
  60. package/prompts/content-detail-generator.md +7 -3
  61. package/prompts/document/custom-components.md +80 -0
  62. package/prompts/document/d2-chart/diy-examples.md +44 -0
  63. package/prompts/document/d2-chart/official-examples.md +708 -0
  64. package/prompts/document/d2-chart/rules.md +48 -0
  65. package/prompts/document/detail-generator.md +12 -15
  66. package/prompts/document/structure-planning.md +1 -3
  67. package/prompts/feedback-refiner.md +81 -60
  68. package/prompts/structure-planning.md +20 -3
  69. package/tests/check-detail-result.test.mjs +3 -4
  70. package/tests/conflict-resolution.test.mjs +237 -0
  71. package/tests/input-generator.test.mjs +940 -0
  72. package/tests/load-sources.test.mjs +627 -3
  73. package/tests/preferences-utils.test.mjs +94 -0
  74. package/tests/save-value-to-config.test.mjs +182 -5
  75. package/tests/utils.test.mjs +49 -0
  76. package/utils/conflict-detector.mjs +72 -1
  77. package/utils/constants.mjs +125 -124
  78. package/utils/kroki-utils.mjs +162 -0
  79. package/utils/markdown-checker.mjs +98 -70
  80. package/utils/utils.mjs +96 -28
package/utils/utils.mjs CHANGED
@@ -1,8 +1,13 @@
1
1
  import { execSync } from "node:child_process";
2
+ import crypto from "node:crypto";
2
3
  import { accessSync, constants, existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
3
4
  import fs from "node:fs/promises";
4
5
  import path from "node:path";
5
- import { parse } from "yaml";
6
+ import { parse, stringify as yamlStringify } from "yaml";
7
+ import {
8
+ detectResolvableConflicts,
9
+ generateConflictResolutionRules,
10
+ } from "./conflict-detector.mjs";
6
11
  import {
7
12
  DEFAULT_EXCLUDE_PATTERNS,
8
13
  DEFAULT_INCLUDE_PATTERNS,
@@ -32,6 +37,16 @@ export function toRelativePath(filePath) {
32
37
  return path.isAbsolute(filePath) ? path.relative(process.cwd(), filePath) : filePath;
33
38
  }
34
39
 
40
+ /**
41
+ * Check if a string looks like a glob pattern
42
+ * @param {string} pattern - The string to check
43
+ * @returns {boolean} - True if the string contains glob pattern characters
44
+ */
45
+ export function isGlobPattern(pattern) {
46
+ if (pattern == null) return false;
47
+ return /[*?[\]]|(\*\*)/.test(pattern);
48
+ }
49
+
35
50
  export function processContent({ content }) {
36
51
  // Match markdown regular links [text](link), exclude images ![text](link)
37
52
  return content.replace(/(?<!!)\[([^\]]+)\]\(([^)]+)\)/g, (match, text, link) => {
@@ -95,6 +110,7 @@ export async function saveDocWithTranslations({
95
110
 
96
111
  // Add labels front matter if labels are provided
97
112
  let finalContent = processContent({ content });
113
+
98
114
  if (labels && labels.length > 0) {
99
115
  const frontMatter = `---\nlabels: ${JSON.stringify(labels)}\n---\n\n`;
100
116
  finalContent = frontMatter + finalContent;
@@ -113,6 +129,7 @@ export async function saveDocWithTranslations({
113
129
  let finalTranslationContent = processContent({
114
130
  content: translate.translation,
115
131
  });
132
+
116
133
  if (labels && labels.length > 0) {
117
134
  const frontMatter = `---\nlabels: ${JSON.stringify(labels)}\n---\n\n`;
118
135
  finalTranslationContent = frontMatter + finalTranslationContent;
@@ -149,7 +166,7 @@ export function getCurrentGitHead() {
149
166
  * @param {string} gitHead - The current git HEAD commit hash
150
167
  */
151
168
  export async function saveGitHeadToConfig(gitHead) {
152
- if (!gitHead || process.env.NODE_ENV === 'test' || process.env.BUN_TEST) {
169
+ if (!gitHead || process.env.NODE_ENV === "test" || process.env.BUN_TEST) {
153
170
  return; // Skip if no git HEAD available or in test environment
154
171
  }
155
172
 
@@ -169,7 +186,9 @@ export async function saveGitHeadToConfig(gitHead) {
169
186
 
170
187
  // Check if lastGitHead already exists in the file
171
188
  const lastGitHeadRegex = /^lastGitHead:\s*.*$/m;
172
- const newLastGitHeadLine = `lastGitHead: ${gitHead}`;
189
+ // Use yaml library to safely serialize the git head value
190
+ const yamlContent = yamlStringify({ lastGitHead: gitHead }).trim();
191
+ const newLastGitHeadLine = yamlContent;
173
192
 
174
193
  if (lastGitHeadRegex.test(fileContent)) {
175
194
  // Replace existing lastGitHead line
@@ -287,8 +306,10 @@ export function hasFileChangesBetweenCommits(
287
306
  return addedOrDeletedFiles.some((filePath) => {
288
307
  // Check if file matches any include pattern
289
308
  const matchesInclude = includePatterns.some((pattern) => {
290
- // Convert glob pattern to regex for matching
291
- const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".");
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, ".");
292
313
  const regex = new RegExp(regexPattern);
293
314
  return regex.test(filePath);
294
315
  });
@@ -299,8 +320,10 @@ export function hasFileChangesBetweenCommits(
299
320
 
300
321
  // Check if file matches any exclude pattern
301
322
  const matchesExclude = excludePatterns.some((pattern) => {
302
- // Convert glob pattern to regex for matching
303
- const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".");
323
+ // First escape all regex special characters except * and ?
324
+ const escapedPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
325
+ // Then convert glob wildcards to regex
326
+ const regexPattern = escapedPattern.replace(/\*/g, ".*").replace(/\?/g, ".");
304
327
  const regex = new RegExp(regexPattern);
305
328
  return regex.test(filePath);
306
329
  });
@@ -351,9 +374,10 @@ export async function loadConfigFromFile() {
351
374
  * @returns {string} Updated file content
352
375
  */
353
376
  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")}`;
377
+ // Use yaml library to safely serialize the key-value pair
378
+ const yamlObject = { [key]: value };
379
+ const yamlContent = yamlStringify(yamlObject).trim();
380
+ const formattedValue = yamlContent;
357
381
 
358
382
  const lines = fileContent.split("\n");
359
383
 
@@ -435,7 +459,10 @@ function handleArrayValueUpdate(key, value, comment, fileContent) {
435
459
  * @returns {string} Updated file content
436
460
  */
437
461
  function handleStringValueUpdate(key, value, comment, fileContent) {
438
- const formattedValue = `${key}: "${value}"`;
462
+ // Use yaml library to safely serialize the key-value pair
463
+ const yamlObject = { [key]: value };
464
+ const yamlContent = yamlStringify(yamlObject).trim();
465
+ const formattedValue = yamlContent;
439
466
  const lines = fileContent.split("\n");
440
467
 
441
468
  // Handle string values (original logic)
@@ -849,6 +876,29 @@ export function processConfigFields(config) {
849
876
  const processed = {};
850
877
  const allRulesContent = [];
851
878
 
879
+ // Set default values for missing or empty fields
880
+ const defaults = {
881
+ nodeName: "Section",
882
+ locale: "en",
883
+ sourcesPath: ["./"],
884
+ docsDir: "./.aigne/doc-smith/docs",
885
+ outputDir: "./.aigne/doc-smith/output",
886
+ translateLanguages: [],
887
+ rules: "",
888
+ targetAudience: "",
889
+ };
890
+
891
+ // Apply defaults for missing or empty fields
892
+ for (const [key, defaultValue] of Object.entries(defaults)) {
893
+ if (
894
+ !config[key] ||
895
+ (Array.isArray(defaultValue) && (!config[key] || config[key].length === 0)) ||
896
+ (typeof defaultValue === "string" && (!config[key] || config[key].trim() === ""))
897
+ ) {
898
+ processed[key] = defaultValue;
899
+ }
900
+ }
901
+
852
902
  // Check if original rules field has content
853
903
  if (config.rules) {
854
904
  if (typeof config.rules === "string") {
@@ -868,27 +918,35 @@ export function processConfigFields(config) {
868
918
  }
869
919
 
870
920
  // Process document purpose (array)
871
- let purposeContents = "";
872
921
  if (config.documentPurpose && Array.isArray(config.documentPurpose)) {
873
- purposeContents = config.documentPurpose
874
- .map((key) => DOCUMENT_STYLES[key]?.content)
875
- .filter(Boolean)
876
- .join("\n\n");
922
+ const purposeRules = config.documentPurpose
923
+ .map((key) => {
924
+ const style = DOCUMENT_STYLES[key];
925
+ if (!style) return null;
926
+ return `Document Purpose - ${style.name}:\n${style.description}\n${style.content}`;
927
+ })
928
+ .filter(Boolean);
877
929
 
878
- if (purposeContents) {
879
- allRulesContent.push(purposeContents);
930
+ if (purposeRules.length > 0) {
931
+ allRulesContent.push(purposeRules.join("\n\n"));
880
932
  }
881
933
  }
882
934
 
883
935
  // Process target audience types (array)
884
- let audienceContents = "";
885
936
  let audienceNames = "";
886
937
  if (config.targetAudienceTypes && Array.isArray(config.targetAudienceTypes)) {
887
- // Get content for rules
888
- audienceContents = config.targetAudienceTypes
889
- .map((key) => TARGET_AUDIENCES[key]?.content)
890
- .filter(Boolean)
891
- .join("\n\n");
938
+ // Get structured content for rules
939
+ const audienceRules = config.targetAudienceTypes
940
+ .map((key) => {
941
+ const audience = TARGET_AUDIENCES[key];
942
+ if (!audience) return null;
943
+ return `Target Audience - ${audience.name}:\n${audience.description}\n${audience.content}`;
944
+ })
945
+ .filter(Boolean);
946
+
947
+ if (audienceRules.length > 0) {
948
+ allRulesContent.push(audienceRules.join("\n\n"));
949
+ }
892
950
 
893
951
  // Get names for targetAudience field
894
952
  audienceNames = config.targetAudienceTypes
@@ -896,10 +954,6 @@ export function processConfigFields(config) {
896
954
  .filter(Boolean)
897
955
  .join(", ");
898
956
 
899
- if (audienceContents) {
900
- allRulesContent.push(audienceContents);
901
- }
902
-
903
957
  if (audienceNames) {
904
958
  // Check if original targetAudience field has content
905
959
  const existingTargetAudience = config.targetAudience?.trim();
@@ -933,6 +987,16 @@ export function processConfigFields(config) {
933
987
  }
934
988
  }
935
989
 
990
+ // Detect and handle conflicts in user selections
991
+ const conflicts = detectResolvableConflicts(config);
992
+ if (conflicts.length > 0) {
993
+ const conflictResolutionRules = generateConflictResolutionRules(conflicts);
994
+ allRulesContent.push(conflictResolutionRules);
995
+
996
+ // Store conflict information for debugging/logging
997
+ processed.detectedConflicts = conflicts;
998
+ }
999
+
936
1000
  // Combine all content into rules field
937
1001
  if (allRulesContent.length > 0) {
938
1002
  processed.rules = allRulesContent.join("\n\n");
@@ -1087,3 +1151,7 @@ export function detectSystemLanguage() {
1087
1151
  return "en";
1088
1152
  }
1089
1153
  }
1154
+
1155
+ export function getContentHash(str) {
1156
+ return crypto.createHash("sha256").update(str).digest("hex");
1157
+ }