@aigne/doc-smith 0.9.6-beta.1 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.6-beta.2](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.9.6-beta.1...v0.9.6-beta.2) (2025-11-20)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * enhance the robustness of add/remove document ([#325](https://github.com/AIGNE-io/aigne-doc-smith/issues/325)) ([f78668a](https://github.com/AIGNE-io/aigne-doc-smith/commit/f78668ad93fb1c9913cfd62de466018f177990b3))
9
+ * ignore video media temporary ([#326](https://github.com/AIGNE-io/aigne-doc-smith/issues/326)) ([e65e7fd](https://github.com/AIGNE-io/aigne-doc-smith/commit/e65e7fda49d870c1e60b433fac121193ee72a328))
10
+
3
11
  ## [0.9.6-beta.1](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.9.6-beta...v0.9.6-beta.1) (2025-11-19)
4
12
 
5
13
 
@@ -3,6 +3,7 @@ import {
3
3
  getDeleteDocumentOutputJsonSchema,
4
4
  validateDeleteDocumentInput,
5
5
  } from "../../../types/document-structure-schema.mjs";
6
+ import { userContextAt } from "../../../utils/utils.mjs";
6
7
 
7
8
  export default async function deleteDocument(input, options) {
8
9
  // Validate input using Zod schema
@@ -23,6 +24,21 @@ export default async function deleteDocument(input, options) {
23
24
  documentStructure = input.documentStructure;
24
25
  }
25
26
 
27
+ const deletedPathsContext = userContextAt(options, "deletedPaths");
28
+ const deletedPaths = deletedPathsContext.get() || [];
29
+
30
+ // Check if path has already been deleted
31
+ if (recursive) {
32
+ if (deletedPaths.includes(path)) {
33
+ const message = `Skipping duplicate deletion. Document '${path}' has already been deleted.`;
34
+ return {
35
+ documentStructure,
36
+ message,
37
+ deletedDocuments: [],
38
+ };
39
+ }
40
+ }
41
+
26
42
  // Find the document to delete
27
43
  const documentIndex = documentStructure.findIndex((item) => item.path === path);
28
44
  if (documentIndex === -1) {
@@ -72,6 +88,11 @@ export default async function deleteDocument(input, options) {
72
88
  // Remove all documents from the structure
73
89
  const updatedStructure = documentStructure.filter((item) => !pathsToDelete.has(item.path));
74
90
 
91
+ // Add paths to deleted paths
92
+ if (recursive) {
93
+ deletedPathsContext.set(deletedPaths.concat(Array.from(pathsToDelete)));
94
+ }
95
+
75
96
  // Build success message
76
97
  const successMessage = `deleteDocument executed successfully.
77
98
  Successfully deleted document '${documentToDelete.title}' with path '${path}'${recursive && deletedCount > 0 ? ` along with ${deletedCount} child document(s)` : ""}.
@@ -1,15 +1,24 @@
1
1
  import chalk from "chalk";
2
2
 
3
+ import { recordUpdate } from "../../../utils/history-utils.mjs";
3
4
  /**
4
5
  * Print summary of added documents and documents with new links
5
6
  */
6
7
  export default async function printAddDocumentSummary({
7
8
  newDocuments = [],
8
9
  documentsWithNewLinks = [],
10
+ allFeedback = [],
9
11
  }) {
10
- let message = `\n${"=".repeat(80)}\n`;
11
- message += `${chalk.bold.cyan("šŸ“Š Summary")}\n`;
12
- message += `${"=".repeat(80)}\n\n`;
12
+ let message = `\n---\n`;
13
+ message += `${chalk.bold.cyan("šŸ“Š Summary")}\n\n`;
14
+
15
+ // Record the update
16
+ if (allFeedback.length > 0) {
17
+ recordUpdate({
18
+ operation: "structure_update",
19
+ feedback: allFeedback.join("\n"),
20
+ });
21
+ }
13
22
 
14
23
  // Display added documents
15
24
  if (newDocuments && newDocuments.length > 0) {
@@ -47,8 +56,6 @@ export default async function printAddDocumentSummary({
47
56
  message += `${chalk.gray(" No documents needed to be updated.\n\n")}`;
48
57
  }
49
58
 
50
- message += `${"=".repeat(80)}\n\n`;
51
-
52
59
  return { message };
53
60
  }
54
61
 
@@ -1,24 +1,61 @@
1
+ import { join } from "node:path";
2
+ import chalk from "chalk";
3
+ import pLimit from "p-limit";
4
+ import { generateFileName, pathToFlatName } from "../../../utils/docs-finder-utils.mjs";
5
+ import { pathExists } from "../../../utils/file-utils.mjs";
6
+
1
7
  /**
2
8
  * Review documentsWithNewLinks and let user select which documents should be updated
3
9
  */
4
10
  export default async function reviewDocumentsWithNewLinks(
5
- { documentsWithNewLinks = [], documentExecutionStructure = [] },
11
+ { documentsWithNewLinks = [], documentExecutionStructure = [], locale = "en", docsDir },
6
12
  options,
7
13
  ) {
8
14
  // If no documents to review, return empty array
9
15
  if (!documentsWithNewLinks || documentsWithNewLinks.length === 0) {
10
- return { documentsWithNewLinks: [] };
16
+ return { documentsWithNewLinks: [], documentsToUpdate: [] };
11
17
  }
12
18
 
19
+ // Build choices with file existence check
20
+ const limit = pLimit(50);
21
+ const choices = await Promise.all(
22
+ documentsWithNewLinks.map((document, index) =>
23
+ limit(async () => {
24
+ // Find corresponding document in documentStructure to get title
25
+ const structureDoc = documentExecutionStructure.find((item) => item.path === document.path);
26
+ const title = structureDoc?.title || document.path;
27
+
28
+ // Generate filename from document path
29
+ const flatName = pathToFlatName(document.path);
30
+ const filename = generateFileName(flatName, locale);
31
+
32
+ // Check file existence if docsDir is provided
33
+ let fileExists = true;
34
+ let missingFileText = "";
35
+ if (docsDir) {
36
+ const filePath = join(docsDir, filename);
37
+ fileExists = await pathExists(filePath);
38
+ if (!fileExists) {
39
+ missingFileText = chalk.red(" - file not found");
40
+ }
41
+ }
42
+
43
+ return {
44
+ name: `${title} (${filename})${missingFileText}`,
45
+ value: index,
46
+ checked: fileExists, // Only check if file exists
47
+ disabled: !fileExists, // Disable if file doesn't exist
48
+ description: `New Links: ${document.newLinks.join(", ")}`,
49
+ };
50
+ }),
51
+ ),
52
+ );
53
+
13
54
  // Let user select which documents to update (default: all selected)
14
55
  const selectedDocs = await options.prompts.checkbox({
15
56
  message:
16
57
  "Select documents that need new links added (all selected by default, press Enter to confirm, or unselect all to skip):",
17
- choices: documentsWithNewLinks.map((document, index) => ({
18
- name: `${document.path} → ${document.newLinks.join(", ")}`,
19
- value: index,
20
- checked: true, // Default to all selected
21
- })),
58
+ choices,
22
59
  });
23
60
 
24
61
  // Filter documentsWithNewLinks based on user selection
@@ -42,7 +79,7 @@ export default async function reviewDocumentsWithNewLinks(
42
79
  };
43
80
  }
44
81
 
45
- // Prepare documents: add necessary fields for update (without content)
82
+ // Prepare documents: add necessary fields for update (e.g. feedback)
46
83
  const preparedDocs = [];
47
84
 
48
85
  for (const doc of filteredDocs) {
@@ -1,9 +1,11 @@
1
+ import { join } from "node:path";
1
2
  import {
2
3
  buildAllowedLinksFromStructure,
3
4
  generateFileName,
4
5
  pathToFlatName,
5
6
  readFileContent,
6
7
  } from "../../../utils/docs-finder-utils.mjs";
8
+ import { pathExists } from "../../../utils/file-utils.mjs";
7
9
  import { checkMarkdown, getLinkFromError } from "../../../utils/markdown-checker.mjs";
8
10
 
9
11
  export default async function findDocumentsWithInvalidLinks({
@@ -35,6 +37,15 @@ export default async function findDocumentsWithInvalidLinks({
35
37
  const flatName = pathToFlatName(doc.path);
36
38
  const fileName = generateFileName(flatName, locale);
37
39
 
40
+ // Check if file exists before reading
41
+ const filePath = join(docsDir, fileName);
42
+ const fileExists = await pathExists(filePath);
43
+
44
+ if (!fileExists) {
45
+ // Skip if file doesn't exist
46
+ continue;
47
+ }
48
+
38
49
  // Read document content
39
50
  const content = await readFileContent(docsDir, fileName);
40
51
 
@@ -7,9 +7,8 @@ export default async function printRemoveDocumentSummary({
7
7
  deletedDocuments = [],
8
8
  documentsWithInvalidLinks = [],
9
9
  }) {
10
- let message = `\n${"=".repeat(80)}\n`;
11
- message += `${chalk.bold.cyan("šŸ“Š Summary")}\n`;
12
- message += `${"=".repeat(80)}\n\n`;
10
+ let message = `\n---\n`;
11
+ message += `${chalk.bold.cyan("šŸ“Š Summary")}\n\n`;
13
12
 
14
13
  // Display removed documents
15
14
  if (deletedDocuments && deletedDocuments.length > 0) {
@@ -47,8 +46,6 @@ export default async function printRemoveDocumentSummary({
47
46
  message += `${chalk.gray(" No documents needed to be fixed.\n\n")}`;
48
47
  }
49
48
 
50
- message += `${"=".repeat(80)}\n\n`;
51
-
52
49
  return { message };
53
50
  }
54
51
 
@@ -1,10 +1,8 @@
1
- import chooseDocs from "../../utils/choose-docs.mjs";
2
1
  import deleteDocument from "../document-structure-tools/delete-document.mjs";
3
- import { DOC_ACTION } from "../../../utils/constants/index.mjs";
4
- import addTranslatesToStructure from "../../utils/add-translates-to-structure.mjs";
2
+ import { buildDocumentTree, buildChoicesFromTree } from "../../../utils/docs-finder-utils.mjs";
5
3
 
6
4
  export default async function removeDocumentsFromStructure(input = {}, options = {}) {
7
- const { docsDir, locale = "en", translateLanguages = [], originalDocumentStructure } = input;
5
+ const { originalDocumentStructure, locale = "en", docsDir } = input;
8
6
 
9
7
  if (!Array.isArray(originalDocumentStructure) || originalDocumentStructure.length === 0) {
10
8
  console.warn(
@@ -13,43 +11,43 @@ export default async function removeDocumentsFromStructure(input = {}, options =
13
11
  process.exit(0);
14
12
  }
15
13
 
16
- const { documentExecutionStructure } = addTranslatesToStructure({
17
- originalDocumentStructure,
18
- translateLanguages,
19
- });
20
-
21
14
  // Initialize currentStructure in userContext
22
15
  options.context.userContext.currentStructure = [...originalDocumentStructure];
23
16
 
24
- // Use chooseDocs to select documents to delete
25
- const chooseResult = await chooseDocs(
26
- {
27
- docs: [],
28
- documentExecutionStructure,
29
- docsDir,
30
- locale,
31
- isTranslate: false,
32
- feedback: "no feedback",
33
- requiredFeedback: false,
34
- action: DOC_ACTION.clear,
35
- },
36
- options,
37
- );
17
+ // Build tree structure
18
+ const { rootNodes } = buildDocumentTree(originalDocumentStructure);
19
+
20
+ // Build choices with tree structure visualization
21
+ const choices = await buildChoicesFromTree(rootNodes, "", 0, { locale, docsDir });
22
+
23
+ // Let user select documents to delete
24
+ let selectedPaths = [];
25
+ try {
26
+ selectedPaths = await options.prompts.checkbox({
27
+ message: "Select documents to remove (Press Enter with no selection to finish):",
28
+ choices,
29
+ });
30
+ } catch {
31
+ // User cancelled or no selection made
32
+ console.log("No documents were removed.");
33
+ process.exit(0);
34
+ }
38
35
 
39
- if (!chooseResult?.selectedDocs || chooseResult.selectedDocs.length === 0) {
40
- console.log("No documents selected for removal.");
36
+ // If no documents selected, exit
37
+ if (!selectedPaths || selectedPaths.length === 0) {
38
+ console.log("No documents were removed.");
41
39
  process.exit(0);
42
40
  }
43
41
 
44
- // Delete each selected document
42
+ // Delete each selected document with cascade deletion
45
43
  const deletedDocuments = [];
46
44
  const errors = [];
47
45
 
48
- for (const selectedDoc of chooseResult.selectedDocs) {
46
+ for (const path of selectedPaths) {
49
47
  try {
50
48
  const deleteResult = await deleteDocument(
51
49
  {
52
- path: selectedDoc.path,
50
+ path,
53
51
  recursive: true,
54
52
  },
55
53
  options,
@@ -57,21 +55,21 @@ export default async function removeDocumentsFromStructure(input = {}, options =
57
55
 
58
56
  if (deleteResult.error) {
59
57
  errors.push({
60
- path: selectedDoc.path,
58
+ path,
61
59
  error: deleteResult.error.message,
62
60
  });
63
61
  } else {
64
- // deletedDocuments is now always an array
65
62
  deletedDocuments.push(...deleteResult.deletedDocuments);
66
63
  }
67
64
  } catch (error) {
68
65
  errors.push({
69
- path: selectedDoc.path,
66
+ path,
70
67
  error: error.message,
71
68
  });
72
69
  }
73
70
  }
74
71
 
72
+ // Check if there are errors
75
73
  if (errors.length > 0) {
76
74
  console.warn(
77
75
  `šŸ—‘ļø Remove Documents\n • Failed to remove documents:\n${errors
@@ -81,7 +79,12 @@ export default async function removeDocumentsFromStructure(input = {}, options =
81
79
  process.exit(0);
82
80
  }
83
81
 
84
- // Get updated document structure
82
+ if (deletedDocuments.length === 0) {
83
+ console.log("No documents were removed.");
84
+ process.exit(0);
85
+ }
86
+
87
+ // Get final updated document structure
85
88
  const updatedStructure = options.context.userContext.currentStructure;
86
89
 
87
90
  return {
@@ -1,4 +1,8 @@
1
- import { buildAllowedLinksFromStructure } from "../../../utils/docs-finder-utils.mjs";
1
+ import {
2
+ buildAllowedLinksFromStructure,
3
+ generateFileName,
4
+ pathToFlatName,
5
+ } from "../../../utils/docs-finder-utils.mjs";
2
6
 
3
7
  /**
4
8
  * Generate feedback message for fixing invalid links in a document
@@ -44,27 +48,26 @@ ${allowedLinksList}
44
48
  }
45
49
 
46
50
  export default async function reviewDocumentsWithInvalidLinks(input = {}, options = {}) {
47
- const { documentsWithInvalidLinks = [], documentExecutionStructure = [] } = input;
51
+ const { documentsWithInvalidLinks = [], documentExecutionStructure = [], locale = "en" } = input;
48
52
 
49
53
  // If no documents with invalid links, return empty array
50
54
  if (!Array.isArray(documentsWithInvalidLinks) || documentsWithInvalidLinks.length === 0) {
51
55
  return {
52
56
  documentsWithInvalidLinks: [],
57
+ documentsToUpdate: [],
53
58
  };
54
59
  }
55
60
 
56
61
  // Create choices for user selection, default all checked
57
62
  const choices = documentsWithInvalidLinks.map((doc) => {
58
- const invalidLinksText =
59
- doc.invalidLinks && doc.invalidLinks.length > 0
60
- ? ` (${doc.invalidLinks.length} invalid link${doc.invalidLinks.length > 1 ? "s" : ""})`
61
- : "";
63
+ const flatName = pathToFlatName(doc.path);
64
+ const filename = generateFileName(flatName, locale);
62
65
 
63
66
  return {
64
- name: `${doc.title || doc.path}${invalidLinksText}`,
67
+ name: `${doc.title} (${filename})`,
65
68
  value: doc.path,
66
69
  checked: true, // Default all selected
67
- description: `Invalid Links: ${doc.invalidLinks.join(", ")}`,
70
+ description: `Invalid Links(${doc.invalidLinks?.length || 0}): ${doc.invalidLinks?.join(", ")}`,
68
71
  };
69
72
  });
70
73
 
@@ -14,6 +14,7 @@ import {
14
14
  SUPPORTED_LANGUAGES,
15
15
  TARGET_AUDIENCES,
16
16
  } from "../../utils/constants/index.mjs";
17
+ import { isRemoteFile } from "../../utils/file-utils.mjs";
17
18
  import loadConfig from "../../utils/load-config.mjs";
18
19
  import {
19
20
  detectSystemLanguage,
@@ -22,9 +23,8 @@ import {
22
23
  isGlobPattern,
23
24
  validatePath,
24
25
  } from "../../utils/utils.mjs";
25
- import { isRemoteFile } from "../../utils/file-utils.mjs";
26
- import { validateDocDir } from "./validate.mjs";
27
26
  import mapReasoningEffortLevel from "../utils/map-reasoning-effort-level.mjs";
27
+ import { validateDocDir } from "./validate.mjs";
28
28
 
29
29
  const _PRESS_ENTER_TO_FINISH = "Press Enter to finish";
30
30
 
@@ -362,8 +362,7 @@ async function _init(
362
362
  continue;
363
363
  }
364
364
  sourcePaths.push(trimmedPath);
365
- }
366
- if (isGlobPatternResult) {
365
+ } else if (isGlobPatternResult) {
367
366
  // For glob patterns, just add them without validation
368
367
  if (sourcePaths.includes(trimmedPath)) {
369
368
  console.log(`āš ļø Pattern already exists: ${trimmedPath}`);
@@ -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
@@ -132,7 +132,7 @@ cli:
132
132
  alias: ["add"]
133
133
  url: ./agents/create/user-add-document/index.yaml
134
134
  - name: remove-document
135
- alias: ["rm"]
135
+ alias: ["remove", "rm"]
136
136
  url: ./agents/create/user-remove-document/index.yaml
137
137
  - ./agents/clear/index.yaml
138
138
  mcp_server:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/doc-smith",
3
- "version": "0.9.6-beta.1",
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>
@@ -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
  /**
@@ -393,6 +395,83 @@ export function buildDocumentTree(documentStructure) {
393
395
  return { rootNodes, nodeMap };
394
396
  }
395
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
+
396
475
  /**
397
476
  * Format document structure for printing
398
477
  * @param {Array} structure - Document structure array
@@ -900,17 +900,19 @@ export function isDirExcluded(dir, excludePatterns) {
900
900
 
901
901
  /**
902
902
  * Return source paths that would be excluded by exclude patterns (files are skipped, directories use minimatch, glob patterns use path prefix heuristic)
903
+ * @returns {{excluded: string[], notFound: string[]}} Object with excluded and notFound arrays
903
904
  */
904
905
  export async function findInvalidSourcePaths(sourcePaths, excludePatterns) {
905
906
  if (!Array.isArray(sourcePaths) || sourcePaths.length === 0) {
906
- return [];
907
+ return { excluded: [], notFound: [] };
907
908
  }
908
909
 
909
910
  if (!Array.isArray(excludePatterns) || excludePatterns.length === 0) {
910
- return [];
911
+ return { excluded: [], notFound: [] };
911
912
  }
912
913
 
913
- const invalidPaths = [];
914
+ const excluded = [];
915
+ const notFound = [];
914
916
 
915
917
  for (const sourcePath of sourcePaths) {
916
918
  if (typeof sourcePath !== "string" || !sourcePath) {
@@ -931,7 +933,7 @@ export async function findInvalidSourcePaths(sourcePaths, excludePatterns) {
931
933
  if (isGlobPattern(sourcePath)) {
932
934
  const representativePath = getPathPrefix(sourcePath);
933
935
  if (isDirExcluded(representativePath, excludePatterns)) {
934
- invalidPaths.push(sourcePath);
936
+ excluded.push(sourcePath);
935
937
  }
936
938
  continue;
937
939
  }
@@ -945,14 +947,14 @@ export async function findInvalidSourcePaths(sourcePaths, excludePatterns) {
945
947
  // Check dir with minimatch
946
948
  if (stats.isDirectory()) {
947
949
  if (isDirExcluded(sourcePath, excludePatterns)) {
948
- invalidPaths.push(sourcePath);
950
+ excluded.push(sourcePath);
949
951
  }
950
952
  }
951
953
  } catch {
952
954
  // Path doesn't exist
953
- invalidPaths.push(sourcePath);
955
+ notFound.push(sourcePath);
954
956
  }
955
957
  }
956
958
 
957
- return invalidPaths;
959
+ return { excluded, notFound };
958
960
  }
@@ -41,11 +41,28 @@ export default async function loadConfig({ config, appUrl }) {
41
41
  ...(processedConfig.excludePatterns || parsedConfig.excludePatterns || []),
42
42
  ];
43
43
 
44
- const invalidPaths = await findInvalidSourcePaths(sourcesPath, excludePatterns);
45
- if (invalidPaths.length > 0) {
46
- console.warn(
47
- `āš ļø Some source paths have been excluded and will not be processed:\n${invalidPaths.map((p) => ` - ${chalk.yellow(p)}`).join("\n")}\nšŸ’” Tip: You can remove these paths in ${toDisplayPath(configPath)}\n`,
44
+ const { excluded, notFound } = await findInvalidSourcePaths(sourcesPath, excludePatterns);
45
+
46
+ if (excluded.length > 0 || notFound.length > 0) {
47
+ const warnings = [];
48
+
49
+ if (excluded.length > 0) {
50
+ warnings.push(
51
+ `āš ļø These paths were excluded (ignored by config):\n${excluded.map((p) => ` - ${chalk.yellow(p)}`).join("\n")}`,
52
+ );
53
+ }
54
+
55
+ if (notFound.length > 0) {
56
+ warnings.push(
57
+ `🚫 These paths were skipped because they do not exist:\n${notFound.map((p) => ` - ${chalk.red(p)}`).join("\n")}`,
58
+ );
59
+ }
60
+
61
+ warnings.push(
62
+ `šŸ’” Tip: You can remove these paths in ${chalk.cyan(toDisplayPath(configPath))}`,
48
63
  );
64
+
65
+ console.warn(`${warnings.join("\n\n")}\n`);
49
66
  }
50
67
  }
51
68