@aigne/doc-smith 0.2.4 → 0.2.6

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 (37) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -0
  3. package/agents/check-detail-result.mjs +15 -120
  4. package/agents/check-structure-plan.mjs +56 -7
  5. package/agents/detail-generator-and-translate.yaml +7 -1
  6. package/agents/detail-regenerator.yaml +6 -57
  7. package/agents/docs-generator.yaml +5 -61
  8. package/agents/find-item-by-path.mjs +63 -14
  9. package/agents/input-generator.mjs +31 -21
  10. package/agents/language-selector.mjs +101 -0
  11. package/agents/load-config.mjs +3 -3
  12. package/agents/load-sources.mjs +55 -40
  13. package/agents/publish-docs.mjs +44 -153
  14. package/agents/retranslate.yaml +74 -0
  15. package/agents/save-docs.mjs +12 -2
  16. package/agents/save-output.mjs +9 -3
  17. package/agents/save-single-doc.mjs +19 -0
  18. package/agents/structure-planning.yaml +6 -0
  19. package/agents/team-publish-docs.yaml +7 -7
  20. package/agents/translate.yaml +3 -0
  21. package/aigne.yaml +5 -1
  22. package/docs-mcp/docs-search.yaml +1 -1
  23. package/docs-mcp/get-docs-detail.mjs +1 -1
  24. package/docs-mcp/get-docs-structure.mjs +1 -1
  25. package/docs-mcp/read-doc-content.mjs +1 -1
  26. package/package.json +16 -7
  27. package/prompts/check-structure-planning-result.md +4 -7
  28. package/prompts/content-detail-generator.md +1 -2
  29. package/prompts/structure-planning.md +7 -2
  30. package/prompts/translator.md +4 -0
  31. package/tests/test-all-validation-cases.mjs +707 -0
  32. package/utils/constants.mjs +3 -2
  33. package/utils/markdown-checker.mjs +386 -0
  34. package/utils/mermaid-validator.mjs +158 -0
  35. package/utils/mermaid-worker-pool.mjs +254 -0
  36. package/utils/mermaid-worker.mjs +242 -0
  37. package/utils/utils.mjs +155 -44
package/utils/utils.mjs CHANGED
@@ -83,6 +83,7 @@ export async function saveDocWithTranslations({
83
83
  locale,
84
84
  translates = [],
85
85
  labels,
86
+ isTranslate = false,
86
87
  }) {
87
88
  const results = [];
88
89
  try {
@@ -96,20 +97,22 @@ export async function saveDocWithTranslations({
96
97
  return isEnglish ? `${flatName}.md` : `${flatName}.${language}.md`;
97
98
  };
98
99
 
99
- // Save main content with appropriate filename based on locale
100
- const mainFileName = getFileName(locale);
101
- const mainFilePath = path.join(docsDir, mainFileName);
100
+ // Save main content with appropriate filename based on locale (skip if isTranslate is true)
101
+ if (!isTranslate) {
102
+ const mainFileName = getFileName(locale);
103
+ const mainFilePath = path.join(docsDir, mainFileName);
102
104
 
103
- // Add labels front matter if labels are provided
104
- let finalContent = processContent({ content });
105
- if (labels && labels.length > 0) {
106
- const frontMatter = `---\nlabels: ${JSON.stringify(labels)}\n---\n\n`;
107
- finalContent = frontMatter + finalContent;
108
- }
105
+ // Add labels front matter if labels are provided
106
+ let finalContent = processContent({ content });
107
+ if (labels && labels.length > 0) {
108
+ const frontMatter = `---\nlabels: ${JSON.stringify(labels)}\n---\n\n`;
109
+ finalContent = frontMatter + finalContent;
110
+ }
109
111
 
110
- await fs.writeFile(mainFilePath, finalContent, "utf8");
111
- results.push({ path: mainFilePath, success: true });
112
- console.log(chalk.green(`Saved: ${chalk.cyan(mainFilePath)}`));
112
+ await fs.writeFile(mainFilePath, finalContent, "utf8");
113
+ results.push({ path: mainFilePath, success: true });
114
+ console.log(chalk.green(`Saved: ${chalk.cyan(mainFilePath)}`));
115
+ }
113
116
 
114
117
  // Process all translations
115
118
  for (const translate of translates) {
@@ -162,7 +165,7 @@ export async function saveGitHeadToConfig(gitHead) {
162
165
  }
163
166
 
164
167
  try {
165
- const docSmithDir = path.join(process.cwd(), "doc-smith");
168
+ const docSmithDir = path.join(process.cwd(), "./.aigne/doc-smith");
166
169
  if (!existsSync(docSmithDir)) {
167
170
  mkdirSync(docSmithDir, { recursive: true });
168
171
  }
@@ -345,7 +348,11 @@ export function hasFileChangesBetweenCommits(
345
348
  * @returns {Promise<Object|null>} - The config object or null if file doesn't exist
346
349
  */
347
350
  export async function loadConfigFromFile() {
348
- const configPath = path.join(process.cwd(), "doc-smith", "config.yaml");
351
+ const configPath = path.join(
352
+ process.cwd(),
353
+ "./.aigne/doc-smith",
354
+ "config.yaml"
355
+ );
349
356
 
350
357
  try {
351
358
  if (!existsSync(configPath)) {
@@ -364,14 +371,15 @@ export async function loadConfigFromFile() {
364
371
  * Save value to config.yaml file
365
372
  * @param {string} key - The config key to save
366
373
  * @param {string} value - The value to save
374
+ * @param {string} [comment] - Optional comment to add above the key
367
375
  */
368
- export async function saveValueToConfig(key, value) {
369
- if (!value) {
370
- return; // Skip if no value provided
376
+ export async function saveValueToConfig(key, value, comment) {
377
+ if (value === undefined) {
378
+ return; // Skip if value is undefined
371
379
  }
372
380
 
373
381
  try {
374
- const docSmithDir = path.join(process.cwd(), "doc-smith");
382
+ const docSmithDir = path.join(process.cwd(), "./.aigne/doc-smith");
375
383
  if (!existsSync(docSmithDir)) {
376
384
  mkdirSync(docSmithDir, { recursive: true });
377
385
  }
@@ -385,17 +393,38 @@ export async function saveValueToConfig(key, value) {
385
393
  }
386
394
 
387
395
  // Check if key already exists in the file
388
- const keyRegex = new RegExp(`^${key}:\\s*.*$`, "m");
389
- const newKeyLine = `${key}: ${value}`;
396
+ const lines = fileContent.split("\n");
397
+ const keyRegex = new RegExp(`^${key}:\\s*.*$`);
398
+ const newKeyLine = `${key}: "${value}"`;
399
+
400
+ const keyIndex = lines.findIndex((line) => keyRegex.test(line));
390
401
 
391
- if (keyRegex.test(fileContent)) {
402
+ if (keyIndex !== -1) {
392
403
  // Replace existing key line
393
- fileContent = fileContent.replace(keyRegex, newKeyLine);
404
+ lines[keyIndex] = newKeyLine;
405
+ fileContent = lines.join("\n");
406
+
407
+ // Add comment if provided and not already present
408
+ if (
409
+ comment &&
410
+ keyIndex > 0 &&
411
+ !lines[keyIndex - 1].trim().startsWith("# ")
412
+ ) {
413
+ // Add comment above the key if it doesn't already have one
414
+ lines.splice(keyIndex, 0, `# ${comment}`);
415
+ fileContent = lines.join("\n");
416
+ }
394
417
  } else {
395
418
  // Add key to the end of file
396
419
  if (fileContent && !fileContent.endsWith("\n")) {
397
420
  fileContent += "\n";
398
421
  }
422
+
423
+ // Add comment if provided
424
+ if (comment) {
425
+ fileContent += `# ${comment}\n`;
426
+ }
427
+
399
428
  fileContent += newKeyLine + "\n";
400
429
  }
401
430
 
@@ -472,20 +501,20 @@ export function validatePaths(paths) {
472
501
  }
473
502
 
474
503
  /**
475
- * Check if input is a valid directory and add it to results if so
504
+ * Check if input is a valid directory or file and add it to results if so
476
505
  * @param {string} searchTerm - The search term to check
477
506
  * @param {Array} results - The results array to modify
478
507
  */
479
- function addExactDirectoryMatch(searchTerm, results) {
508
+ function addExactPathMatch(searchTerm, results) {
480
509
  const inputValidation = validatePath(searchTerm);
481
510
  if (inputValidation.isValid) {
482
511
  const stats = statSync(normalizePath(searchTerm));
483
- if (stats.isDirectory()) {
484
- results.unshift({
485
- name: searchTerm,
486
- value: searchTerm,
487
- });
488
- }
512
+ const isDirectory = stats.isDirectory();
513
+ results.unshift({
514
+ name: searchTerm,
515
+ value: searchTerm,
516
+ description: isDirectory ? "📁 Directory" : "📄 File",
517
+ });
489
518
  }
490
519
  }
491
520
 
@@ -510,7 +539,7 @@ export function getAvailablePaths(userInput = "") {
510
539
  const dirPath = path.dirname(searchTerm);
511
540
  const fileName = path.basename(searchTerm);
512
541
  results = getDirectoryContents(dirPath, fileName);
513
- addExactDirectoryMatch(searchTerm, results);
542
+ addExactPathMatch(searchTerm, results);
514
543
  }
515
544
  // Handle relative paths
516
545
  else if (searchTerm.startsWith("./") || searchTerm.startsWith("../")) {
@@ -519,7 +548,7 @@ export function getAvailablePaths(userInput = "") {
519
548
  if (lastSlashIndex === -1) {
520
549
  // No slash found, treat as current directory search
521
550
  results = getDirectoryContents("./", searchTerm);
522
- addExactDirectoryMatch(searchTerm, results);
551
+ addExactPathMatch(searchTerm, results);
523
552
  } else {
524
553
  const dirPath = searchTerm.substring(0, lastSlashIndex + 1);
525
554
  const fileName = searchTerm.substring(lastSlashIndex + 1);
@@ -537,13 +566,13 @@ export function getAvailablePaths(userInput = "") {
537
566
  }
538
567
 
539
568
  results = getDirectoryContents(dirPath, fileName);
540
- addExactDirectoryMatch(searchTerm, results);
569
+ addExactPathMatch(searchTerm, results);
541
570
  }
542
571
  }
543
572
  // Handle simple file/directory names (search in current directory)
544
573
  else {
545
574
  results = getDirectoryContents("./", searchTerm);
546
- addExactDirectoryMatch(searchTerm, results);
575
+ addExactPathMatch(searchTerm, results);
547
576
  }
548
577
 
549
578
  // Remove duplicates based on value (path)
@@ -571,7 +600,7 @@ export function getAvailablePaths(userInput = "") {
571
600
  * Get directory contents for a specific path
572
601
  * @param {string} dirPath - Directory path to search in
573
602
  * @param {string} searchTerm - Optional search term to filter results
574
- * @returns {Array<Object>} - Array of path objects
603
+ * @returns {Array<Object>} - Array of path objects (both files and directories)
575
604
  */
576
605
  function getDirectoryContents(dirPath, searchTerm = "") {
577
606
  try {
@@ -623,17 +652,24 @@ function getDirectoryContents(dirPath, searchTerm = "") {
623
652
 
624
653
  const isDirectory = entry.isDirectory();
625
654
 
626
- // Only include directories, skip files
627
- if (isDirectory) {
628
- items.push({
629
- name: relativePath,
630
- value: relativePath,
631
- });
632
- }
655
+ // Include both directories and files
656
+ items.push({
657
+ name: relativePath,
658
+ value: relativePath,
659
+ description: isDirectory ? "📁 Directory" : "📄 File",
660
+ });
633
661
  }
634
662
 
635
- // Sort alphabetically (all items are directories now)
636
- items.sort((a, b) => a.name.localeCompare(b.name));
663
+ // Sort alphabetically (directories first, then files)
664
+ items.sort((a, b) => {
665
+ const aIsDir = a.description === "📁 Directory";
666
+ const bIsDir = b.description === "📁 Directory";
667
+
668
+ if (aIsDir && !bIsDir) return -1;
669
+ if (!aIsDir && bIsDir) return 1;
670
+
671
+ return a.name.localeCompare(b.name);
672
+ });
637
673
 
638
674
  return items;
639
675
  } catch (error) {
@@ -644,3 +680,78 @@ function getDirectoryContents(dirPath, searchTerm = "") {
644
680
  return [];
645
681
  }
646
682
  }
683
+
684
+ /**
685
+ * Get GitHub repository information
686
+ * @param {string} repoUrl - The repository URL
687
+ * @returns {Promise<Object>} - Repository information
688
+ */
689
+ export async function getGitHubRepoInfo(repoUrl) {
690
+ try {
691
+ // Extract owner and repo from GitHub URL
692
+ const match = repoUrl.match(
693
+ /github\.com[\/:]([^\/]+)\/([^\/]+?)(?:\.git)?$/
694
+ );
695
+ if (!match) return null;
696
+
697
+ const [, owner, repo] = match;
698
+ const apiUrl = `https://api.github.com/repos/${owner}/${repo}`;
699
+
700
+ const response = await fetch(apiUrl);
701
+ if (!response.ok) return null;
702
+
703
+ const data = await response.json();
704
+ return {
705
+ name: data.name,
706
+ description: data.description || "",
707
+ icon: data.owner?.avatar_url || "",
708
+ };
709
+ } catch (error) {
710
+ console.warn("Failed to fetch GitHub repository info:", error.message);
711
+ return null;
712
+ }
713
+ }
714
+
715
+ /**
716
+ * Get project information automatically without user confirmation
717
+ * @returns {Promise<Object>} - Project information including name, description, icon, and fromGitHub flag
718
+ */
719
+ export async function getProjectInfo() {
720
+ let repoInfo = null;
721
+ let defaultName = path.basename(process.cwd());
722
+ let defaultDescription = "";
723
+ let defaultIcon = "";
724
+ let fromGitHub = false;
725
+
726
+ // Check if we're in a git repository
727
+ try {
728
+ const gitRemote = execSync("git remote get-url origin", {
729
+ encoding: "utf8",
730
+ stdio: ["pipe", "pipe", "ignore"],
731
+ }).trim();
732
+
733
+ // Extract repository name from git remote URL
734
+ const repoName = gitRemote.split("/").pop().replace(".git", "");
735
+ defaultName = repoName;
736
+
737
+ // If it's a GitHub repository, try to get additional info
738
+ if (gitRemote.includes("github.com")) {
739
+ repoInfo = await getGitHubRepoInfo(gitRemote);
740
+ if (repoInfo) {
741
+ defaultDescription = repoInfo.description;
742
+ defaultIcon = repoInfo.icon;
743
+ fromGitHub = true;
744
+ }
745
+ }
746
+ } catch (error) {
747
+ // Not in git repository or no origin remote, use current directory name
748
+ console.warn("No git repository found, using current directory name");
749
+ }
750
+
751
+ return {
752
+ name: defaultName,
753
+ description: defaultDescription,
754
+ icon: defaultIcon,
755
+ fromGitHub,
756
+ };
757
+ }