@aigne/doc-smith 0.9.6-beta → 0.9.6-beta.2

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 (41) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/agents/create/document-structure-tools/delete-document.mjs +53 -9
  3. package/agents/create/update-document-structure.yaml +1 -1
  4. package/agents/create/user-add-document/add-documents-to-structure.mjs +96 -0
  5. package/agents/create/user-add-document/find-documents-to-add-links.yaml +47 -0
  6. package/agents/create/user-add-document/index.yaml +46 -0
  7. package/agents/create/user-add-document/prepare-documents-to-translate.mjs +22 -0
  8. package/agents/create/user-add-document/print-add-document-summary.mjs +63 -0
  9. package/agents/create/user-add-document/review-documents-with-new-links.mjs +110 -0
  10. package/agents/create/user-remove-document/find-documents-with-invalid-links.mjs +78 -0
  11. package/agents/create/user-remove-document/index.yaml +41 -0
  12. package/agents/create/user-remove-document/prepare-documents-to-translate.mjs +22 -0
  13. package/agents/create/user-remove-document/print-remove-document-summary.mjs +53 -0
  14. package/agents/create/user-remove-document/remove-documents-from-structure.mjs +99 -0
  15. package/agents/create/user-remove-document/review-documents-with-invalid-links.mjs +119 -0
  16. package/agents/create/user-review-document-structure.mjs +1 -40
  17. package/agents/create/utils/init-current-content.mjs +38 -0
  18. package/agents/init/index.mjs +3 -4
  19. package/agents/update/document-tools/update-document-content.mjs +12 -12
  20. package/agents/update/update-document-detail.yaml +5 -1
  21. package/agents/update/update-single/update-single-document-detail.mjs +21 -6
  22. package/agents/update/user-review-document.mjs +10 -13
  23. package/agents/utils/add-translates-to-structure.mjs +29 -0
  24. package/agents/utils/{analyze-feedback-intent.yaml → analyze-document-feedback-intent.yaml} +5 -2
  25. package/agents/utils/analyze-structure-feedback-intent.yaml +29 -0
  26. package/agents/utils/check-detail-result.mjs +2 -14
  27. package/agents/utils/load-sources.mjs +36 -46
  28. package/aigne.yaml +10 -1
  29. package/package.json +1 -1
  30. package/prompts/detail/custom/custom-components/x-cards-usage-rules.md +18 -10
  31. package/prompts/structure/find-documents-to-add-links.md +52 -0
  32. package/prompts/utils/analyze-document-feedback-intent.md +54 -0
  33. package/prompts/utils/analyze-structure-feedback-intent.md +43 -0
  34. package/types/document-schema.mjs +2 -0
  35. package/types/document-structure-schema.mjs +6 -2
  36. package/utils/docs-finder-utils.mjs +161 -0
  37. package/utils/file-utils.mjs +9 -7
  38. package/utils/load-config.mjs +21 -4
  39. package/utils/markdown-checker.mjs +50 -5
  40. package/utils/utils.mjs +103 -0
  41. package/prompts/utils/analyze-feedback-intent.md +0 -55
@@ -0,0 +1,29 @@
1
+ name: analyzeStructureFeedbackIntent
2
+ description: Analyze user feedback to determine if data sources are needed for structure modifications
3
+ task_render_mode: hide
4
+ instructions:
5
+ url: ../../prompts/utils/analyze-structure-feedback-intent.md
6
+ input_schema:
7
+ type: object
8
+ properties:
9
+ feedback:
10
+ type: string
11
+ description: User feedback for structure modifications
12
+ required:
13
+ - feedback
14
+ output_schema:
15
+ type: object
16
+ properties:
17
+ needDataSources:
18
+ type: boolean
19
+ description: Whether data sources are needed - true for add/edit operations that need context, false for delete/move/reorder operations
20
+ intentType:
21
+ type: string
22
+ description: The primary type of user intention
23
+ reason:
24
+ type: string
25
+ description: Explanation of why data sources are or aren't needed
26
+ required:
27
+ - needDataSources
28
+ - intentType
29
+ - reason
@@ -1,4 +1,5 @@
1
1
  import { checkMarkdown } from "../../utils/markdown-checker.mjs";
2
+ import { buildAllowedLinksFromStructure } from "../../utils/docs-finder-utils.mjs";
2
3
 
3
4
  export default async function checkDetailResult({ documentStructure, reviewContent, docsDir }) {
4
5
  if (!reviewContent || reviewContent.trim() === "") {
@@ -12,20 +13,7 @@ export default async function checkDetailResult({ documentStructure, reviewConte
12
13
  const detailFeedback = [];
13
14
 
14
15
  // Create a set of allowed links, including both original paths and processed .md paths
15
- const allowedLinks = new Set();
16
- documentStructure.forEach((item) => {
17
- // Add original path
18
- allowedLinks.add(item.path);
19
-
20
- // Add processed .md path (same logic as processContent in utils.mjs)
21
- let processedPath = item.path;
22
- if (processedPath.startsWith(".")) {
23
- processedPath = processedPath.replace(/^\./, "");
24
- }
25
- let flatPath = processedPath.replace(/^\//, "").replace(/\//g, "-");
26
- flatPath = `./${flatPath}.md`;
27
- allowedLinks.add(flatPath);
28
- });
16
+ const allowedLinks = buildAllowedLinksFromStructure(documentStructure);
29
17
 
30
18
  // Run comprehensive markdown validation with all checks
31
19
  try {
@@ -1,27 +1,42 @@
1
- import { readFile } from "node:fs/promises";
2
1
  import { statSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import imageSize from "image-size";
5
+ import {
6
+ DEFAULT_EXCLUDE_PATTERNS,
7
+ DEFAULT_INCLUDE_PATTERNS,
8
+ INTELLIGENT_SUGGESTION_TOKEN_THRESHOLD,
9
+ } from "../../utils/constants/index.mjs";
10
+ import { loadDocumentStructure } from "../../utils/docs-finder-utils.mjs";
5
11
  import {
6
12
  buildSourcesContent,
7
- loadFilesFromPaths,
8
- readFileContents,
13
+ calculateTokens,
9
14
  getMimeType,
10
15
  isRemoteFile,
11
- calculateTokens,
16
+ loadFilesFromPaths,
17
+ readFileContents,
12
18
  } from "../../utils/file-utils.mjs";
19
+ import { isOpenAPISpecFile } from "../../utils/openapi/index.mjs";
13
20
  import {
14
21
  getCurrentGitHead,
15
22
  getModifiedFilesBetweenCommits,
16
23
  toRelativePath,
17
24
  } from "../../utils/utils.mjs";
18
- import {
19
- INTELLIGENT_SUGGESTION_TOKEN_THRESHOLD,
20
- DEFAULT_EXCLUDE_PATTERNS,
21
- DEFAULT_INCLUDE_PATTERNS,
22
- } from "../../utils/constants/index.mjs";
23
- import { isOpenAPISpecFile } from "../../utils/openapi/index.mjs";
24
- import { loadDocumentStructure } from "../../utils/docs-finder-utils.mjs";
25
+
26
+ const imageExts = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg", ".heic", ".heif"];
27
+ const videoExts = [
28
+ ".mp4",
29
+ ".mpeg",
30
+ ".mpg",
31
+ ".mov",
32
+ ".avi",
33
+ ".flv",
34
+ ".mkv",
35
+ ".webm",
36
+ ".wmv",
37
+ ".m4v",
38
+ ".3gpp",
39
+ ];
25
40
 
26
41
  export default async function loadSources(
27
42
  {
@@ -77,7 +92,13 @@ export default async function loadSources(
77
92
  .filter(Boolean);
78
93
  const allFiles = await loadFilesFromPaths(pickSourcesPath, {
79
94
  includePatterns,
80
- excludePatterns: [...new Set([...(excludePatterns || []), ...customExcludePatterns])],
95
+ excludePatterns: [
96
+ ...new Set([
97
+ ...(excludePatterns || []),
98
+ ...customExcludePatterns,
99
+ ...videoExts.map((x) => `**/*${x}`),
100
+ ]),
101
+ ],
81
102
  useDefaultPatterns,
82
103
  defaultIncludePatterns: DEFAULT_INCLUDE_PATTERNS,
83
104
  defaultExcludePatterns: DEFAULT_EXCLUDE_PATTERNS,
@@ -90,26 +111,9 @@ export default async function loadSources(
90
111
 
91
112
  // Define media file extensions
92
113
  const mediaExtensions = [
93
- ".jpg",
94
- ".jpeg",
95
- ".png",
96
- ".gif",
97
- ".bmp",
98
- ".webp",
99
- ".svg",
100
- ".heic",
101
- ".heif",
102
- ".mp4",
103
- ".mpeg",
104
- ".mpg",
105
- ".mov",
106
- ".avi",
107
- ".flv",
108
- ".mkv",
109
- ".webm",
110
- ".wmv",
111
- ".m4v",
112
- ".3gpp",
114
+ ...imageExts,
115
+ // ignore video temporary
116
+ // ...videoExts
113
117
  ];
114
118
 
115
119
  // Separate source files from media files
@@ -119,20 +123,6 @@ export default async function loadSources(
119
123
  // Helper function to determine file type from extension
120
124
  const getFileType = (filePath) => {
121
125
  const ext = path.extname(filePath).toLowerCase();
122
- const imageExts = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg", ".heic", ".heif"];
123
- const videoExts = [
124
- ".mp4",
125
- ".mpeg",
126
- ".mpg",
127
- ".mov",
128
- ".avi",
129
- ".flv",
130
- ".mkv",
131
- ".webm",
132
- ".wmv",
133
- ".m4v",
134
- ".3gpp",
135
- ];
136
126
 
137
127
  if (imageExts.includes(ext)) return "image";
138
128
  if (videoExts.includes(ext)) return "video";
package/aigne.yaml CHANGED
@@ -68,6 +68,7 @@ agents:
68
68
  - ./agents/clear/clear-media-description.mjs
69
69
 
70
70
  # Utilities
71
+ - ./agents/utils/add-translates-to-structure.mjs
71
72
  - ./agents/utils/load-sources.mjs
72
73
  - ./agents/utils/post-generate.mjs
73
74
  - ./agents/utils/save-sidebar.mjs
@@ -79,7 +80,8 @@ agents:
79
80
  - ./agents/utils/find-item-by-path.mjs
80
81
  - ./agents/utils/check-feedback-refiner.mjs
81
82
  - ./agents/utils/feedback-refiner.yaml
82
- - ./agents/utils/analyze-feedback-intent.yaml
83
+ - ./agents/utils/analyze-structure-feedback-intent.yaml
84
+ - ./agents/utils/analyze-document-feedback-intent.yaml
83
85
 
84
86
  - ./agents/utils/document-title-streamline.yaml
85
87
  - ./agents/utils/streamline-document-titles-if-needed.mjs
@@ -126,6 +128,13 @@ cli:
126
128
  - url: ./agents/history/view.mjs
127
129
  name: view
128
130
  alias: ["log", "list"]
131
+ - name: add-document
132
+ alias: ["add"]
133
+ url: ./agents/create/user-add-document/index.yaml
134
+ - name: remove-document
135
+ alias: ["remove", "rm"]
136
+ url: ./agents/create/user-remove-document/index.yaml
137
+ - ./agents/clear/index.yaml
129
138
  mcp_server:
130
139
  agents:
131
140
  - ./docs-mcp/get-docs-structure.mjs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/doc-smith",
3
- "version": "0.9.6-beta",
3
+ "version": "0.9.6-beta.2",
4
4
  "description": "AI-driven documentation generation tool built on the AIGNE Framework",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -5,7 +5,7 @@ XCards is multiple `<x-card>` container, suitable for displaying multiple links
5
5
 
6
6
  ### Attributes
7
7
 
8
- - `data-columns` (optional): Must be an **integer ≥ 2**. Values below 2 are disallowed. Default is 2.
8
+ - `data-columns` (required): must be an integer ≥ 2; no upper bound.
9
9
 
10
10
  ### Children
11
11
 
@@ -20,20 +20,21 @@ XCards is multiple `<x-card>` container, suitable for displaying multiple links
20
20
 
21
21
  ### Good Examples
22
22
 
23
- - Example 1: Three-column cards with icons
23
+ - Example 1: Two-column cards with images
24
24
  ```md
25
- <x-cards data-columns="3">
26
- <x-card data-title="Feature 1" data-icon="lucide:rocket">Description of Feature 1.</x-card>
27
- <x-card data-title="Feature 2" data-icon="lucide:bolt">Description of Feature 2.</x-card>
28
- <x-card data-title="Feature 3" data-icon="material-symbols:rocket-outline">Description of Feature 3.</x-card>
25
+ <x-cards data-columns="2">
26
+ <x-card data-title="Card A" data-image="https://picsum.photos/id/10/300/300">Content A</x-card>
27
+ <x-card data-title="Card B" data-image="https://picsum.photos/id/11/300/300">Content B</x-card>
29
28
  </x-cards>
30
29
  ```
31
30
 
32
- - Example 2: Two-column cards with images
31
+ - Example 2: Four-column cards with icons
33
32
  ```md
34
- <x-cards data-columns="2">
35
- <x-card data-title="Card A" data-image="https://picsum.photos/id/10/300/300">Content A</x-card>
36
- <x-card data-title="Card B" data-image="https://picsum.photos/id/11/300/300">Content B</x-card>
33
+ <x-cards data-columns="4">
34
+ <x-card data-title="Feature 1" data-icon="lucide:rocket">Description of Feature 1.</x-card>
35
+ <x-card data-title="Feature 2" data-icon="lucide:bolt">Description of Feature 2.</x-card>
36
+ <x-card data-title="Feature 3" data-icon="material-symbols:rocket-outline">Description of Feature 3.</x-card>
37
+ <x-card data-title="Feature 4" data-icon="lucide:star">Description of Feature 4.</x-card>
37
38
  </x-cards>
38
39
  ```
39
40
 
@@ -72,4 +73,11 @@ XCards is multiple `<x-card>` container, suitable for displaying multiple links
72
73
  - [Using the Blog](./blog.md)
73
74
  - [Using Chat](./chat.md)
74
75
  ```
76
+
77
+ - Example 4: Missing `data-columns` attribute (required)
78
+ <x-cards>
79
+ <x-card data-title="Feature 1" data-icon="lucide:rocket">Description of Feature 1.</x-card>
80
+ <x-card data-title="Feature 2" data-icon="lucide:bolt">Description of Feature 2.</x-card>
81
+ </x-cards>
82
+
75
83
  </x-card-usage-rules>
@@ -0,0 +1,52 @@
1
+ # Task: Find Documents to Add Links
2
+
3
+ Determine which existing documents should link to newly added documents.
4
+
5
+ ## Input
6
+
7
+ <documentStructure>
8
+ {{originalDocumentStructure}}
9
+ </documentStructure>
10
+
11
+ <newDocuments>
12
+ {{newDocuments}}
13
+ </newDocuments>
14
+
15
+ <userFeedback>
16
+ {{allFeedback}}
17
+ </userFeedback>
18
+
19
+ ## Steps
20
+
21
+ 1. **Check <userFeedback> first.**
22
+ - If users explicitly specify linking (e.g., “link FAQ from About”), follow exactly.
23
+ 2. **Analyze <documentStructure>.**
24
+ Identify existing documents that should link to those in <newDocuments> using the rules below.
25
+ 3. For each qualifying document, add a non-empty `newLinks` array containing new document paths.
26
+ 4. Output only these updated documents (subset of <documentStructure>) as `documentsWithNewLinks`.
27
+
28
+ Each item in `documentsWithNewLinks` must:
29
+ - Be an existing document from <documentStructure>
30
+ - Retain all original properties (`path`, `title`, `description`, `parentId`, `icon`, `sourceIds`)
31
+ - Include `newLinks: string[]`
32
+
33
+ ## Linking Rules (in priority order)
34
+
35
+ 1. **User Instructions** — Follow explicit <userFeedback>.
36
+ 2. **Parent–Child** — If a new document’s `parentId` equals a document’s `path`, the parent links to it.
37
+ 3. **Semantic Similarity** — Link thematically related documents (e.g., “About” ↔ “Team”).
38
+ 4. **Navigation Context** — Documents in the same navigation group may link.
39
+ 5. **Hierarchy** — Sibling or section documents may cross-link.
40
+ 6. **Relevance** — Add links only when it improves navigation logically.
41
+
42
+ ## Output Format
43
+
44
+ ```json
45
+ {
46
+ "documentsWithNewLinks": [
47
+ {
48
+ "path": "/existing-document",
49
+ "newLinks": ["/new-document-1", "/new-document-2"]
50
+ }
51
+ ]
52
+ }
@@ -0,0 +1,54 @@
1
+ <role>
2
+ You are a feedback intent analyzer for **document content modifications**. Your task is to determine the intent type of user feedback regarding content-level operations inside a document, and whether external data sources are needed.
3
+ </role>
4
+
5
+ <input>
6
+ feedback: {{feedback}}
7
+ </input>
8
+
9
+ <analysis_rules>
10
+ If the feedback contains any document-level (structure) operations, return an error (document content edits cannot include structure changes).
11
+
12
+ Scope: Only analyze feedback related to document content (e.g. sections, text, images).
13
+
14
+ **Intent types:**
15
+
16
+ 1. add - Adding new sections or content inside a document
17
+ 2. edit - Modifying existing content, titles, descriptions, components
18
+ 3. delete - Removing sections or content
19
+ 4. move - Moving sections to different positions within the document
20
+ 5. reorder - Changing the order of sections at the same level
21
+ 6. mixed - Combination of multiple intent types
22
+
23
+ **Data source rules:**
24
+
25
+ - add/edit -> needDataSources = true
26
+ - delete/move/reorder -> needDataSources = false
27
+ - mixed -> needDataSources = true if any add/edit is included
28
+
29
+ **Decision logic:**
30
+
31
+ - Only consider document content operations.
32
+ - If any add or edit operation exists -> needDataSources = true
33
+ - If only delete, move, or reorder operations exist -> needDataSources = false
34
+ - When uncertain, default to needDataSources = true
35
+ </analysis_rules>
36
+
37
+ <output_rules>
38
+ Normal output:
39
+
40
+ {
41
+ "error": false,
42
+ "needDataSources": boolean,
43
+ "intentType": "add" | "edit" | "delete" | "move" | "reorder" | "mixed",
44
+ "reason": "Explanation of why data sources are or aren't needed based on page content operations."
45
+ }
46
+
47
+ Error output (if document-level operations are detected):
48
+
49
+ {
50
+ "error": true,
51
+ "needDataSources": false,
52
+ "reason": "Feedback mixes document-content edits with document-structure operations. When analyzing document content, structure changes are not allowed. Please split into separate feedback items."
53
+ }
54
+ </output_rules>
@@ -0,0 +1,43 @@
1
+ <role>
2
+ You are a feedback intent analyzer for **document structure modifications**. Your task is to determine the intent type of user feedback regarding document-level operations and whether external data sources are needed.
3
+ </role>
4
+
5
+ <input>
6
+ feedback: {{feedback}}
7
+ </input>
8
+
9
+ <analysis_rules>
10
+ Scope: Only analyze feedback related to document structure. Ignore any content-level operations inside document (e.g. sections, text, images).
11
+
12
+ **intent types:**
13
+
14
+ 1. add - Adding new documents
15
+ 2. edit - Modifying document-level properties (e.g., path, parentId, title of the document itself)
16
+ 3. delete - Removing documents
17
+ 4. move - Moving documents to different positions or parent sections
18
+ 5. reorder - Changing the order of documents
19
+ 6. mixed - Combination of multiple intent types
20
+
21
+ **Data source rules:**
22
+
23
+ - add/edit -> needDataSources = true
24
+ - delete/move/reorder -> needDataSources = false
25
+ - mixed -> needDataSources = true if any add/edit is included
26
+
27
+ **Decision logic:**
28
+
29
+ - Only consider document-level operations in the feedback.
30
+ - If any add or edit operation exists -> needDataSources = true
31
+ - If only delete, move, or reorder operations exist -> needDataSources = false
32
+ - When uncertain, default to needDataSources = true
33
+ </analysis_rules>
34
+
35
+ <output_rules>
36
+ Return a JSON object:
37
+
38
+ {
39
+ "needDataSources": boolean,
40
+ "intentType": "add" | "edit" | "delete" | "move" | "reorder" | "mixed",
41
+ "reason": "Explanation of why data sources are or aren't needed based on document-level operations."
42
+ }
43
+ </output_rules>
@@ -4,6 +4,7 @@ import { zodToJsonSchema } from "zod-to-json-schema";
4
4
  // Update document content schemas
5
5
  export const updateDocumentContentInputSchema = z.object({
6
6
  diffPatch: z.string().min(1, "Diff patch is required"),
7
+ path: z.string().min(1, "Path is required for concurrent document updates"),
7
8
  });
8
9
 
9
10
  export const updateDocumentContentOutputSchema = z.object({
@@ -18,6 +19,7 @@ export const getUpdateDocumentContentInputJsonSchema = () => {
18
19
  const schema = zodToJsonSchema(updateDocumentContentInputSchema);
19
20
  if (schema.properties) {
20
21
  schema.properties.diffPatch.description = "Diff patch string to apply to the original content";
22
+ schema.properties.path.description = "Document path";
21
23
  }
22
24
  return schema;
23
25
  };
@@ -33,12 +33,13 @@ export const addDocumentOutputSchema = z.object({
33
33
  // Delete document schemas
34
34
  export const deleteDocumentInputSchema = z.object({
35
35
  path: z.string().min(1, "Path is required"),
36
+ recursive: z.boolean().optional(),
36
37
  });
37
38
 
38
39
  export const deleteDocumentOutputSchema = z.object({
39
40
  documentStructure: documentStructureSchema,
40
41
  message: z.string().optional(),
41
- deletedDocument: documentItemSchema.optional(),
42
+ deletedDocuments: z.array(documentItemSchema).optional(),
42
43
  error: z.object({ message: z.string() }).optional(),
43
44
  });
44
45
 
@@ -113,6 +114,8 @@ export const getDeleteDocumentInputJsonSchema = () => {
113
114
  const schema = zodToJsonSchema(deleteDocumentInputSchema);
114
115
  if (schema.properties) {
115
116
  schema.properties.path.description = "URL path of the document to delete";
117
+ schema.properties.recursive.description =
118
+ "If true, recursively delete all child documents. If false or not provided, deletion will fail if child documents exist.";
116
119
  }
117
120
  return schema;
118
121
  };
@@ -123,7 +126,8 @@ export const getDeleteDocumentOutputJsonSchema = () => {
123
126
  schema.properties.documentStructure.description =
124
127
  "Updated documentation structure array with the document removed";
125
128
  schema.properties.message.description = "Success message describing the operation result";
126
- schema.properties.deletedDocument.description = "The deleted document object";
129
+ schema.properties.deletedDocuments.description =
130
+ "Array of deleted document objects (includes all recursively deleted child documents if recursive=true)";
127
131
  schema.properties.error.description =
128
132
  "Error object containing error message if operation failed";
129
133
  }
@@ -1,5 +1,7 @@
1
1
  import { access, readdir, readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import chalk from "chalk";
4
+ import pLimit from "p-limit";
3
5
  import { pathExists } from "./file-utils.mjs";
4
6
 
5
7
  /**
@@ -324,6 +326,40 @@ export async function loadDocumentStructure(outputDir) {
324
326
  }
325
327
  }
326
328
 
329
+ /**
330
+ * Build allowed links set from document structure
331
+ * Includes both original paths and processed .md paths for link validation
332
+ * @param {Array} documentStructure - Array of documentation structure items with path property
333
+ * @returns {Set<string>} Set of allowed link paths
334
+ */
335
+ export function buildAllowedLinksFromStructure(documentStructure) {
336
+ const allowedLinks = new Set();
337
+
338
+ if (!Array.isArray(documentStructure)) {
339
+ return allowedLinks;
340
+ }
341
+
342
+ documentStructure.forEach((item) => {
343
+ if (!item?.path) {
344
+ return;
345
+ }
346
+
347
+ // Add original path
348
+ allowedLinks.add(item.path);
349
+
350
+ // Add processed .md path (same logic as processContent in utils.mjs)
351
+ let processedPath = item.path;
352
+ if (processedPath.startsWith(".")) {
353
+ processedPath = processedPath.replace(/^\./, "");
354
+ }
355
+ let flatPath = processedPath.replace(/^\//, "").replace(/\//g, "-");
356
+ flatPath = `./${flatPath}.md`;
357
+ allowedLinks.add(flatPath);
358
+ });
359
+
360
+ return allowedLinks;
361
+ }
362
+
327
363
  /**
328
364
  * Build a tree structure from a flat document structure array using parentId
329
365
  * @param {Array} documentStructure - Flat array of document structure items with path and parentId
@@ -358,3 +394,128 @@ export function buildDocumentTree(documentStructure) {
358
394
 
359
395
  return { rootNodes, nodeMap };
360
396
  }
397
+
398
+ /**
399
+ * Build checkbox choices from tree structure with visual hierarchy
400
+ * @param {Array} nodes - Array of tree nodes
401
+ * @param {string} prefix - Current prefix for indentation
402
+ * @param {number} depth - Current depth level (0 for root)
403
+ * @param {Object} context - Context object containing locale, docsDir, etc.
404
+ * @param {string} context.locale - Main language locale (e.g., 'en', 'zh', 'fr')
405
+ * @param {string} [context.docsDir] - Docs directory path for file existence check
406
+ * @returns {Promise<Array>} Array of choice objects
407
+ */
408
+ export async function buildChoicesFromTree(nodes, prefix = "", depth = 0, context = {}) {
409
+ const { locale = "en", docsDir } = context;
410
+ const choices = [];
411
+
412
+ // Limit concurrent file checks to 50 per level to avoid overwhelming the file system
413
+ const limit = pLimit(50);
414
+
415
+ // Process nodes with controlled concurrency while maintaining order
416
+ const nodePromises = nodes.map((node, i) =>
417
+ limit(async () => {
418
+ const isLastSibling = i === nodes.length - 1;
419
+ const hasChildren = node.children && node.children.length > 0;
420
+
421
+ // Build the tree prefix - top level nodes don't have ├─ or └─
422
+ const treePrefix = depth === 0 ? "" : prefix + (isLastSibling ? "└─ " : "├─ ");
423
+ const flatName = pathToFlatName(node.path);
424
+ const filename = generateFileName(flatName, locale);
425
+
426
+ // Check file existence if docsDir is provided
427
+ let fileExists = true;
428
+ let missingFileText = "";
429
+ if (docsDir) {
430
+ const filePath = join(docsDir, filename);
431
+ fileExists = await pathExists(filePath);
432
+ if (!fileExists) {
433
+ missingFileText = chalk.red(" - file not found");
434
+ }
435
+ }
436
+
437
+ // warningText only shows when file exists, missingFileText has higher priority
438
+ const warningText =
439
+ fileExists && hasChildren ? chalk.yellow(" - will cascade delete all child documents") : "";
440
+
441
+ const displayName = `${treePrefix}${node.title} (${filename})${warningText}${missingFileText}`;
442
+
443
+ const choice = {
444
+ name: displayName,
445
+ value: node.path,
446
+ short: node.title,
447
+ disabled: !fileExists,
448
+ };
449
+
450
+ // Recursively process children
451
+ let childChoices = [];
452
+ if (hasChildren) {
453
+ const childPrefix = depth === 0 ? "" : prefix + (isLastSibling ? " " : "│ ");
454
+ childChoices = await buildChoicesFromTree(node.children, childPrefix, depth + 1, context);
455
+ }
456
+
457
+ return { choice, childChoices };
458
+ }),
459
+ );
460
+
461
+ // Wait for all nodes at this level to complete, maintaining order
462
+ const results = await Promise.all(nodePromises);
463
+
464
+ // Build choices array in order
465
+ for (const { choice, childChoices } of results) {
466
+ choices.push(choice);
467
+ if (childChoices.length > 0) {
468
+ choices.push(...childChoices);
469
+ }
470
+ }
471
+
472
+ return choices;
473
+ }
474
+
475
+ /**
476
+ * Format document structure for printing
477
+ * @param {Array} structure - Document structure array
478
+ * @returns {Object} Object containing rootNodes and printNode function
479
+ */
480
+ function formatDocumentStructure(structure) {
481
+ const { rootNodes } = buildDocumentTree(structure);
482
+
483
+ function printNode(node, depth = 0) {
484
+ const INDENT_SPACES = " ";
485
+ const FOLDER_ICON = " 📁";
486
+ const FILE_ICON = " 📄";
487
+ const indent = INDENT_SPACES.repeat(depth);
488
+ const prefix = depth === 0 ? FOLDER_ICON : FILE_ICON;
489
+
490
+ console.log(`${indent}${prefix} ${node.title}`);
491
+
492
+ if (node.children && node.children.length > 0) {
493
+ node.children.forEach((child) => {
494
+ printNode(child, depth + 1);
495
+ });
496
+ }
497
+ }
498
+
499
+ return { rootNodes, printNode };
500
+ }
501
+
502
+ /**
503
+ * Print document structure in a user-friendly format
504
+ * @param {Array} structure - Document structure array
505
+ */
506
+ export function printDocumentStructure(structure) {
507
+ console.log(`\n ${"-".repeat(50)}`);
508
+ console.log(" Current Documentation Structure");
509
+ console.log(` ${"-".repeat(50)}`);
510
+
511
+ const { rootNodes, printNode } = formatDocumentStructure(structure);
512
+
513
+ if (rootNodes.length === 0) {
514
+ console.log(" No documentation structure found.");
515
+ } else {
516
+ rootNodes.forEach((node) => {
517
+ printNode(node);
518
+ });
519
+ }
520
+ console.log();
521
+ }