@aigne/doc-smith 0.8.15-beta.6 โ†’ 0.8.15-beta.7

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,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.15-beta.7](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.8.15-beta.6...v0.8.15-beta.7) (2025-10-31)
4
+
5
+
6
+ ### Features
7
+
8
+ * add web-smith powered web-pages ([#229](https://github.com/AIGNE-io/aigne-doc-smith/issues/229)) ([c3c00c1](https://github.com/AIGNE-io/aigne-doc-smith/commit/c3c00c12f092b125b6adb1a13ed5ff9720fbdab7))
9
+ * support cleaning specific documents ([#231](https://github.com/AIGNE-io/aigne-doc-smith/issues/231)) ([67607c9](https://github.com/AIGNE-io/aigne-doc-smith/commit/67607c9ff3852cc81a29e5a11b2151d26879b000))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * tune custom component prompt and batch update history ([#228](https://github.com/AIGNE-io/aigne-doc-smith/issues/228)) ([ab13b97](https://github.com/AIGNE-io/aigne-doc-smith/commit/ab13b9737e5f111d0939f9c39ee76e13c0692a68))
15
+
3
16
  ## [0.8.15-beta.6](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.8.15-beta.5...v0.8.15-beta.6) (2025-10-30)
4
17
 
5
18
 
@@ -10,7 +10,7 @@ const TARGET_METADATA = {
10
10
  generatedDocs: {
11
11
  label: "Generated Documents",
12
12
  description: ({ docsDir }) =>
13
- `Delete all generated documents in './${toDisplayPath(docsDir)}'. The documentation structure will be preserved.`,
13
+ `Select and delete specific generated documents in './${toDisplayPath(docsDir)}'. The documentation structure will be preserved.`,
14
14
  agent: "clearGeneratedDocs",
15
15
  },
16
16
  documentStructure: {
@@ -150,9 +150,9 @@ export default async function chooseContents(input = {}, options = {}) {
150
150
  }
151
151
 
152
152
  const header = hasError
153
- ? "Cleanup finished with some issues."
154
- : "Cleanup completed successfully!";
155
- const detailLines = results.map((item) => `- ${item.message}`).join("\n");
153
+ ? "๐Ÿงน Cleanup finished with some issues.\n"
154
+ : "๐Ÿงน Cleanup completed successfully!\n";
155
+ const detailLines = results.map((item) => `${item.message}`).join("\n\n");
156
156
 
157
157
  const suggestions = [];
158
158
  results.forEach((result) => {
@@ -9,7 +9,7 @@ export default async function clearAuthTokens(_input = {}, options = {}) {
9
9
  // Check if the file exists
10
10
  if (!existsSync(DOC_SMITH_ENV_FILE)) {
11
11
  return {
12
- message: "No site authorizations found to clear",
12
+ message: "๐Ÿ”‘ No site authorizations found to clear",
13
13
  };
14
14
  }
15
15
 
@@ -23,7 +23,7 @@ export default async function clearAuthTokens(_input = {}, options = {}) {
23
23
 
24
24
  if (siteHostnames.length === 0) {
25
25
  return {
26
- message: "No site authorizations found to clear",
26
+ message: "๐Ÿ”‘ No site authorizations found to clear",
27
27
  };
28
28
  }
29
29
 
@@ -58,7 +58,7 @@ export default async function clearAuthTokens(_input = {}, options = {}) {
58
58
 
59
59
  if (selectedSites.length === 0) {
60
60
  return {
61
- message: "No sites selected for clearing authorization",
61
+ message: "๐Ÿ”‘ No sites selected for clearing authorization",
62
62
  };
63
63
  }
64
64
 
@@ -68,7 +68,7 @@ export default async function clearAuthTokens(_input = {}, options = {}) {
68
68
  if (selectedSites.includes("__ALL__")) {
69
69
  // Clear all site authorizations
70
70
  await writeFile(DOC_SMITH_ENV_FILE, stringify({}));
71
- results.push(`Cleared site authorization for all sites (${siteHostnames.length} sites)`);
71
+ results.push(`โœ” Cleared site authorization for all sites (${siteHostnames.length} sites)`);
72
72
  clearedCount = siteHostnames.length;
73
73
  } else {
74
74
  // Clear site authorizations for selected sites
@@ -79,7 +79,7 @@ export default async function clearAuthTokens(_input = {}, options = {}) {
79
79
  // Remove the entire site object
80
80
  delete updatedEnvs[hostname];
81
81
 
82
- results.push(`Cleared site authorization for ${chalk.cyan(hostname)}`);
82
+ results.push(`โœ” Cleared site authorization for ${chalk.cyan(hostname)}`);
83
83
  clearedCount++;
84
84
  }
85
85
  }
@@ -87,8 +87,8 @@ export default async function clearAuthTokens(_input = {}, options = {}) {
87
87
  await writeFile(DOC_SMITH_ENV_FILE, stringify(updatedEnvs));
88
88
  }
89
89
 
90
- const header = `Successfully cleared site authorizations!`;
91
- const detailLines = results.join("\n");
90
+ const header = `๐Ÿ”‘ Successfully cleared site authorizations!`;
91
+ const detailLines = results.map((item) => ` ${item}`).join("\n");
92
92
 
93
93
  const message = [header, "", detailLines, ""].filter(Boolean).join("\n");
94
94
 
@@ -99,7 +99,7 @@ export default async function clearAuthTokens(_input = {}, options = {}) {
99
99
  };
100
100
  } catch (error) {
101
101
  return {
102
- message: `Failed to clear site authorizations: ${error.message}`,
102
+ message: `โš ๏ธ Failed to clear site authorizations: ${error.message}`,
103
103
  error: true,
104
104
  };
105
105
  }
@@ -36,12 +36,12 @@ export default async function clearDeploymentConfig(input = {}) {
36
36
  }
37
37
 
38
38
  return {
39
- message: `๐Ÿงน Cleared appUrl from config file (${displayPath})`,
39
+ message: `๐Ÿ“ฆ Cleared appUrl from config file (${displayPath})`,
40
40
  };
41
41
  } catch (error) {
42
42
  return {
43
43
  error: true,
44
- message: `โŒ Failed to clear deployment config: ${error.message}`,
44
+ message: `โš ๏ธ Failed to clear deployment config: ${error.message}`,
45
45
  };
46
46
  }
47
47
  }
@@ -10,8 +10,8 @@ export default async function clearDocumentConfig({ workDir }) {
10
10
  await rm(documentConfigPath, { recursive: true, force: true });
11
11
 
12
12
  const message = existed
13
- ? `Cleared document configuration (${displayPath})`
14
- : `Document configuration already empty (${displayPath})`;
13
+ ? `โš™๏ธ Cleared document configuration (${displayPath})`
14
+ : `โš™๏ธ Document configuration already empty (${displayPath})`;
15
15
 
16
16
  const suggestions = existed
17
17
  ? ["Run `aigne doc init` to generate a fresh configuration file."]
@@ -25,7 +25,7 @@ export default async function clearDocumentConfig({ workDir }) {
25
25
  };
26
26
  } catch (error) {
27
27
  return {
28
- message: `Failed to clear document configuration: ${error.message}`,
28
+ message: `โš ๏ธ Failed to clear document configuration: ${error.message}`,
29
29
  error: true,
30
30
  path: displayPath,
31
31
  };
@@ -16,8 +16,8 @@ export default async function clearDocumentStructure(input = {}, _options = {})
16
16
 
17
17
  const structureDisplayPath = toDisplayPath(structurePlanPath);
18
18
  const structureMessage = structureExists
19
- ? `Cleared documentation structure (${structureDisplayPath})`
20
- : `Documentation structure already empty (${structureDisplayPath})`;
19
+ ? `โœ” Cleared documentation structure (${structureDisplayPath})`
20
+ : `โ€ข Documentation structure already empty (${structureDisplayPath})`;
21
21
 
22
22
  results.push({
23
23
  type: "structure",
@@ -29,7 +29,7 @@ export default async function clearDocumentStructure(input = {}, _options = {})
29
29
  results.push({
30
30
  type: "structure",
31
31
  error: true,
32
- message: `Failed to clear documentation structure: ${error.message}`,
32
+ message: `โœ— Failed to clear documentation structure: ${error.message}`,
33
33
  });
34
34
  }
35
35
 
@@ -41,8 +41,8 @@ export default async function clearDocumentStructure(input = {}, _options = {})
41
41
 
42
42
  const docsDisplayPath = toDisplayPath(docsDir);
43
43
  const docsMessage = docsExists
44
- ? `Cleared documents directory (${docsDisplayPath})`
45
- : `Documents directory already empty (${docsDisplayPath})`;
44
+ ? `โœ” Cleared documents directory (${docsDisplayPath})`
45
+ : `โ€ข Documents directory already empty (${docsDisplayPath})`;
46
46
 
47
47
  results.push({
48
48
  type: "documents",
@@ -54,7 +54,7 @@ export default async function clearDocumentStructure(input = {}, _options = {})
54
54
  results.push({
55
55
  type: "documents",
56
56
  error: true,
57
- message: `Failed to clear documents directory: ${error.message}`,
57
+ message: `โœ— Failed to clear documents directory: ${error.message}`,
58
58
  });
59
59
  }
60
60
  }
@@ -65,14 +65,14 @@ export default async function clearDocumentStructure(input = {}, _options = {})
65
65
 
66
66
  let header;
67
67
  if (errorItems > 0) {
68
- header = "Documentation Structure cleanup finished with some issues.";
68
+ header = "โš ๏ธ Documentation Structure cleanup finished with some issues.";
69
69
  } else if (clearedItems > 0) {
70
- header = "Documentation Structure cleared successfully!";
70
+ header = "๐Ÿ“– Documentation Structure cleared successfully!";
71
71
  } else {
72
- header = "Documentation Structure already empty.";
72
+ header = "๐Ÿ“– Documentation Structure already empty.";
73
73
  }
74
74
 
75
- const detailLines = results.map((item) => `- ${item.message}`).join("\n");
75
+ const detailLines = results.map((item) => ` ${item.message}`).join("\n");
76
76
  const message = [header, "", detailLines].filter(Boolean).join("\n");
77
77
 
78
78
  return {
@@ -1,12 +1,19 @@
1
1
  import { rm } from "node:fs/promises";
2
+ import { join } from "node:path";
2
3
  import { pathExists, resolveToAbsolute, toDisplayPath } from "../../utils/file-utils.mjs";
4
+ import {
5
+ pathToFlatName,
6
+ generateFileName,
7
+ loadDocumentStructure,
8
+ } from "../../utils/docs-finder-utils.mjs";
9
+ import chooseDocs from "../utils/choose-docs.mjs";
3
10
 
4
- export default async function clearGeneratedDocs(input = {}, _options = {}) {
5
- const { docsDir } = input;
11
+ export default async function clearGeneratedDocs(input = {}, options = {}) {
12
+ const { docsDir, outputDir, locale, translateLanguages } = input;
6
13
 
7
14
  if (!docsDir) {
8
15
  return {
9
- message: "No generated documents directory specified",
16
+ message: "๐Ÿ“ No generated documents directory specified",
10
17
  };
11
18
  }
12
19
 
@@ -14,23 +21,104 @@ export default async function clearGeneratedDocs(input = {}, _options = {}) {
14
21
  const displayPath = toDisplayPath(generatedDocsPath);
15
22
 
16
23
  try {
17
- const existed = await pathExists(generatedDocsPath);
18
- await rm(generatedDocsPath, { recursive: true, force: true });
24
+ const dirExists = await pathExists(generatedDocsPath);
25
+ if (!dirExists) {
26
+ return {
27
+ message: `๐Ÿ“ Generated documents directory does not exist (${displayPath})`,
28
+ cleared: false,
29
+ };
30
+ }
19
31
 
20
- const message = existed
21
- ? `Cleared generated documents (${displayPath})`
22
- : `Generated documents already empty (${displayPath})`;
32
+ const documentExecutionStructure = (await loadDocumentStructure(outputDir)) || [];
33
+ // select documents interactively
34
+ const chooseResult = await chooseDocs(
35
+ {
36
+ docs: [], // Empty to trigger interactive selection
37
+ documentExecutionStructure,
38
+ docsDir: generatedDocsPath,
39
+ locale: locale || "en",
40
+ isTranslate: false,
41
+ title: "Select documents to delete:",
42
+ feedback: "Skip feedback",
43
+ requiredFeedback: false,
44
+ },
45
+ options,
46
+ );
47
+
48
+ if (!chooseResult?.selectedDocs || chooseResult.selectedDocs.length === 0) {
49
+ return {
50
+ message: "๐Ÿ“ No documents selected for deletion",
51
+ cleared: false,
52
+ path: displayPath,
53
+ };
54
+ }
55
+
56
+ // Extract file names
57
+ const filesToDelete = new Set();
58
+ const allLanguages = [locale || "en", ...(translateLanguages || [])];
59
+
60
+ for (const selectedDoc of chooseResult.selectedDocs) {
61
+ // Convert path to flat filename format using utility function
62
+ const flatName = pathToFlatName(selectedDoc.path);
63
+
64
+ // Generate file names for all languages
65
+ for (const lang of allLanguages) {
66
+ const fileName = generateFileName(flatName, lang);
67
+ filesToDelete.add(fileName);
68
+ }
69
+ }
70
+
71
+ if (filesToDelete.size === 0) {
72
+ return {
73
+ message: "๐Ÿ“ No documents were deleted.",
74
+ cleared: false,
75
+ };
76
+ }
77
+
78
+ // Delete selected files (including all language versions)
79
+ const deletedFiles = [];
80
+ const failedFiles = [];
81
+ let hasError = false;
82
+
83
+ for (const file of filesToDelete) {
84
+ try {
85
+ const filePath = join(generatedDocsPath, file);
86
+ await rm(filePath, { force: true });
87
+ deletedFiles.push(file);
88
+ } catch (error) {
89
+ hasError = true;
90
+ failedFiles.push({ file, error: error.message });
91
+ }
92
+ }
93
+
94
+ // Build result message
95
+ const deletedCount = deletedFiles.length;
96
+ const failedCount = failedFiles.length;
97
+
98
+ let message = "";
99
+ if (deletedCount > 0) {
100
+ const lastIndex = deletedFiles.length - 1;
101
+ message = `๐Ÿ“ Deleted ${deletedCount} document(s) in "${displayPath}":\n${deletedFiles
102
+ .map((f, i) => ` ${i === lastIndex ? "โ””โ”€" : "โ”œโ”€"} ${f}`)
103
+ .join("\n")}`;
104
+ }
105
+
106
+ if (failedCount > 0) {
107
+ const lastIndex = failedFiles.length - 1;
108
+ message = `โš ๏ธ Failed to delete ${failedCount} document(s) in "${displayPath}":\n${failedFiles
109
+ .map((f, i) => ` ${i === lastIndex ? "โ””โ”€" : "โ”œโ”€"} ${f.file}: ${f.error}`)
110
+ .join("\n")}`;
111
+ }
23
112
 
24
113
  return {
25
114
  message,
26
- cleared: existed,
27
- path: displayPath,
115
+ cleared: deletedCount > 0,
116
+ error: hasError,
28
117
  };
29
118
  } catch (error) {
30
119
  return {
31
- message: `Failed to clear generated documents: ${error.message}`,
120
+ message: `โš ๏ธ Failed to clear generated documents: ${error.message}`,
32
121
  error: true,
33
- path: displayPath,
34
122
  };
35
123
  }
36
124
  }
@@ -46,5 +134,6 @@ clearGeneratedDocs.input_schema = {
46
134
  required: ["docsDir"],
47
135
  };
48
136
 
49
- clearGeneratedDocs.taskTitle = "Clear all generated documents";
50
- clearGeneratedDocs.description = "Clear the generated documents directory";
137
+ clearGeneratedDocs.taskTitle = "Clear generated documents";
138
+ clearGeneratedDocs.description =
139
+ "Select and delete specific generated documents from the docs directory";
@@ -11,7 +11,7 @@ export default async function clearMediaDescription(_input = {}, options = {}) {
11
11
  // Check if the cache file exists
12
12
  if (!existsSync(cacheFilePath)) {
13
13
  return {
14
- message: "No media descriptions found to clear",
14
+ message: "๐Ÿ–ผ๏ธ No media descriptions found to clear",
15
15
  };
16
16
  }
17
17
 
@@ -26,7 +26,7 @@ export default async function clearMediaDescription(_input = {}, options = {}) {
26
26
 
27
27
  if (mediaHashes.length === 0) {
28
28
  return {
29
- message: "No media descriptions found to clear",
29
+ message: "๐Ÿ–ผ๏ธ No media descriptions found to clear",
30
30
  };
31
31
  }
32
32
 
@@ -68,7 +68,7 @@ export default async function clearMediaDescription(_input = {}, options = {}) {
68
68
 
69
69
  if (selectedHashes.length === 0) {
70
70
  return {
71
- message: "No media files selected for clearing descriptions",
71
+ message: "๐Ÿ–ผ๏ธ No media files selected for clearing descriptions",
72
72
  };
73
73
  }
74
74
 
@@ -84,7 +84,7 @@ export default async function clearMediaDescription(_input = {}, options = {}) {
84
84
  lastUpdated: new Date().toISOString(),
85
85
  }),
86
86
  );
87
- results.push(`Cleared descriptions for all media files (${mediaHashes.length} files)`);
87
+ results.push(`โœ” Cleared descriptions for all media files (${mediaHashes.length} files)`);
88
88
  clearedCount = mediaHashes.length;
89
89
  } else {
90
90
  // Clear descriptions for selected files
@@ -94,7 +94,7 @@ export default async function clearMediaDescription(_input = {}, options = {}) {
94
94
  if (updatedCache[hash]) {
95
95
  const filename = path.basename(updatedCache[hash].path);
96
96
  delete updatedCache[hash];
97
- results.push(`Cleared description for ${chalk.cyan(filename)}`);
97
+ results.push(`โœ” Cleared description for ${chalk.cyan(filename)}`);
98
98
  clearedCount++;
99
99
  }
100
100
  }
@@ -108,8 +108,8 @@ export default async function clearMediaDescription(_input = {}, options = {}) {
108
108
  );
109
109
  }
110
110
 
111
- const header = `โœจ Successfully cleared media descriptions`;
112
- const detailLines = results.join("\n");
111
+ const header = `๐Ÿ–ผ๏ธ Successfully cleared media descriptions`;
112
+ const detailLines = results.map((item) => ` ${item}`).join("\n");
113
113
 
114
114
  const message = [header, "", detailLines, ""].filter(Boolean).join("\n");
115
115
 
@@ -33,12 +33,12 @@ skills:
33
33
  name: batchTranslateDocument
34
34
  skills:
35
35
  - ../translate/translate-multilingual.yaml
36
- - url: ./record-translation-history.mjs
37
36
  iterate_on: selectedDocs
38
37
  concurrency: 10
39
38
  - url: ../utils/check-feedback-refiner.mjs
40
39
  default_input:
41
40
  stage: translation_refine
41
+ - url: ./record-translation-history.mjs
42
42
  - url: ../utils/action-success.mjs
43
43
  default_input:
44
44
  action: 'โœ… Translation completed'
@@ -1,16 +1,20 @@
1
1
  import { recordUpdate } from "../../utils/history-utils.mjs";
2
2
 
3
- export default function recordTranslationHistory({ feedback, path }) {
3
+ export default function recordTranslationHistory({ selectedPaths, feedback }) {
4
4
  // Skip if no feedback provided
5
5
  if (!feedback?.trim()) {
6
6
  return {};
7
7
  }
8
8
 
9
+ if (!Array.isArray(selectedPaths) || selectedPaths.length === 0) {
10
+ return {};
11
+ }
12
+
9
13
  // Record translation history for this document
10
14
  recordUpdate({
11
15
  operation: "translation_update",
12
16
  feedback: feedback.trim(),
13
- documentPath: path,
17
+ docPaths: selectedPaths,
14
18
  });
15
19
 
16
20
  return {};
@@ -1,4 +1,5 @@
1
1
  import pMap from "p-map";
2
+ import { recordUpdate } from "../../utils/history-utils.mjs";
2
3
 
3
4
  export default async function saveAndTranslateDocument(input, options) {
4
5
  const { selectedDocs, docsDir, translateLanguages, locale } = input;
@@ -7,6 +8,16 @@ export default async function saveAndTranslateDocument(input, options) {
7
8
  return {};
8
9
  }
9
10
 
11
+ // Record history if feedback is provided
12
+ const doc = selectedDocs[0];
13
+ if (doc.feedback?.trim()) {
14
+ recordUpdate({
15
+ operation: "document_update",
16
+ feedback: doc.feedback.trim(),
17
+ docPaths: selectedDocs.map((v) => v.path),
18
+ });
19
+ }
20
+
10
21
  // Only prompt user if translation is actually needed
11
22
  let shouldTranslate = false;
12
23
  if (
@@ -18,6 +18,7 @@ export default async function chooseDocs(
18
18
  locale,
19
19
  reset = false,
20
20
  requiredFeedback = true,
21
+ title,
21
22
  },
22
23
  options,
23
24
  ) {
@@ -65,7 +66,7 @@ export default async function chooseDocs(
65
66
 
66
67
  // Let user select multiple files
67
68
  selectedFiles = await options.prompts.checkbox({
68
- message: getActionText(isTranslate, "Select documents to {action}:"),
69
+ message: title || getActionText(isTranslate, "Select documents to {action}:"),
69
70
  source: (term) => {
70
71
  if (!term) return choices;
71
72
 
@@ -21,6 +21,7 @@ import {
21
21
  DEFAULT_INCLUDE_PATTERNS,
22
22
  } from "../../utils/constants/index.mjs";
23
23
  import { isOpenAPISpecFile } from "../../utils/openapi/index.mjs";
24
+ import { loadDocumentStructure } from "../../utils/docs-finder-utils.mjs";
24
25
 
25
26
  export default async function loadSources(
26
27
  {
@@ -231,31 +232,7 @@ export default async function loadSources(
231
232
  const allFilesPaths = sourceFiles.map((x) => `- ${toRelativePath(x.sourceId)}`).join("\n");
232
233
 
233
234
  // Get the last documentation structure
234
- let originalDocumentStructure;
235
- if (outputDir) {
236
- const documentStructurePath = path.join(outputDir, "structure-plan.json");
237
- try {
238
- const documentExecutionStructure = await readFile(documentStructurePath, "utf8");
239
- if (documentExecutionStructure?.trim()) {
240
- try {
241
- // Validate that the content looks like JSON before parsing
242
- const trimmedContent = documentExecutionStructure.trim();
243
- if (trimmedContent.startsWith("{") || trimmedContent.startsWith("[")) {
244
- originalDocumentStructure = JSON.parse(documentExecutionStructure);
245
- } else {
246
- console.warn(`structure-plan.json contains non-JSON content, skipping parse`);
247
- }
248
- } catch (err) {
249
- console.error(`Failed to parse structure-plan.json: ${err.message}`);
250
- }
251
- }
252
- } catch (err) {
253
- if (err.code !== "ENOENT") {
254
- console.warn(`Error reading structure-plan.json: ${err.message}`);
255
- }
256
- // The file does not exist or is not readable, originalDocumentStructure remains undefined
257
- }
258
- }
235
+ const originalDocumentStructure = await loadDocumentStructure(outputDir);
259
236
 
260
237
  // Get the last output result of the specified path
261
238
  let content;
@@ -1,4 +1,3 @@
1
- import { recordUpdate } from "../../utils/history-utils.mjs";
2
1
  import { shutdownMermaidWorkerPool } from "../../utils/mermaid-worker-pool.mjs";
3
2
  import { saveDoc as _saveDoc } from "../../utils/utils.mjs";
4
3
 
@@ -20,14 +19,6 @@ export default async function saveDoc({
20
19
  locale,
21
20
  });
22
21
 
23
- if (feedback?.trim()) {
24
- recordUpdate({
25
- operation: "document_update",
26
- feedback: feedback.trim(),
27
- documentPath: path,
28
- });
29
- }
30
-
31
22
  if (isShowMessage) {
32
23
  // Shutdown mermaid worker pool to ensure clean exit
33
24
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/doc-smith",
3
- "version": "0.8.15-beta.6",
3
+ "version": "0.8.15-beta.7",
4
4
  "description": "AI-driven documentation generation tool built on the AIGNE Framework",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -72,12 +72,11 @@ Suitable for displaying multiple links using a card list format, providing a ric
72
72
 
73
73
  ### Attributes
74
74
 
75
- - data-columns (optional): Number of columns, integer (e.g., 2, 3). Default is 2.
76
- - Must contain multiple <x-card> elements internally.
75
+ - data-columns (optional): Must be an **integer โ‰ฅ 2**. Values below 2 are disallowed. Default is 2.
77
76
 
78
77
  ### Children
79
78
 
80
- - Must contain multiple <x-card> elements internally.
79
+ - Must contain multiple `<x-card>` elements internally.
81
80
 
82
81
  ### Usage Rules
83
82
 
@@ -107,6 +106,25 @@ Example 2: Two-column cards with images
107
106
  </x-cards>
108
107
  ```
109
108
 
109
+ ### Bad Examples
110
+
111
+ Example 1: Using a single-column layout (`data-columns="1"`) is not allowed
112
+
113
+ ```md
114
+ <x-cards data-columns="1">
115
+ <x-card data-title="Feature 1" data-icon="lucide:rocket">Description of Feature 1.</x-card>
116
+ <x-card data-title="Feature 2" data-icon="lucide:bolt">Description of Feature 2.</x-card>
117
+ </x-cards>
118
+ ```
119
+
120
+ Example 2: Contains only one `<x-card>` (must include multiple cards)
121
+
122
+ ```md
123
+ <x-cards data-columns="2">
124
+ <x-card data-title="Card A" data-image="https://picsum.photos/id/10/300/300">Content A</x-card>
125
+ </x-cards>
126
+ ```
127
+
110
128
  ## XField: Structured data field
111
129
 
112
130
  Suitable for displaying API parameters, return values, context data, and any structured data with metadata in a clean, organized format. Supports nested structures for complex data types.
@@ -414,6 +432,7 @@ Used to group multiple related `<x-field>` elements at the top level, indicating
414
432
 
415
433
  - **Top-Level Only**: Used only at the top level for grouping related `<x-field>` elements. Cannot be nested inside other `<x-field>` or `<x-field-group>` elements
416
434
  - **Structured Data Only**: Use `<x-field-group>` for fields **other than simple types** (`string`, `number`, `boolean`, `symbol`), e.g., Properties, Context, Parameters, Return values. For simple-type fields, use plain Markdown text.
435
+ - **Spacing Around**: Always insert a blank line before and after `<x-field-group>` when itโ€™s adjacent to Markdown content.
417
436
 
418
437
  ### Good Examples
419
438
 
@@ -482,4 +501,20 @@ Example 5: Using x-field-group for simple-type (violates "Structured Data Only"
482
501
  </x-field-group>
483
502
  ```
484
503
 
504
+ Example 6: Missing blank line before x-field-group (violates "Spacing Around" rule)
505
+
506
+ ```md
507
+ **Parameters**
508
+ <x-field-group>
509
+ <x-field data-name="initialState" data-type="any" data-required="false">
510
+ <x-field-desc markdown>The initial state value.</x-field-desc>
511
+ </x-field>
512
+ </x-field-group>
513
+
514
+ `useReducer` returns an array with two items:
515
+ <x-field-group>
516
+ <x-field data-name="dispatch" data-type="function" data-desc="A function that you can call with an action to update the state."></x-field>
517
+ </x-field-group>
518
+ ```
519
+
485
520
  </custom_components_usage>
@@ -2,13 +2,23 @@
2
2
  The following formats are considered Code Blocks:
3
3
 
4
4
  - Wrapped with ```
5
- - Supports configurations: language, title, icon, where title and icon are optional
5
+ - Supports configurations: language, optional title, optional icon (icon uses key=value)
6
+ - title is free text placed after the language (not as title=xxx), may contain spaces, and **must NEVER be wrapped in quotes**
6
7
  - content can be code, command line examples, text or any other content
7
8
 
8
9
  <code_block_sample>
9
10
 
10
- ```{language} [{title}] [icon={icon}]
11
- {content}
11
+ - `language`: javascript
12
+ - `title`: Modern: Using createRoot()
13
+ - `icon`: logos:javascript
14
+
15
+ ```javascript Modern: Using createRoot() icon=logos:javascript
16
+ import { createRoot } from 'react-dom/client'
17
+
18
+ const container = document.getElementById('root')
19
+ const root = createRoot(container)
20
+
21
+ root.unmount()
12
22
  ```
13
23
 
14
24
  </code_block_sample>
@@ -1,5 +1,6 @@
1
1
  import { access, readdir, readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { pathExists } from "./file-utils.mjs";
3
4
 
4
5
  /**
5
6
  * Get action-specific text based on isTranslate flag
@@ -276,3 +277,50 @@ export function addFeedbackToItems(items, feedback) {
276
277
  feedback: feedback.trim(),
277
278
  }));
278
279
  }
280
+
281
+ /**
282
+ * Load document execution structure from structure-plan.json
283
+ * @param {string} outputDir - Output directory containing structure-plan.json
284
+ * @returns {Promise<Array|null>} Document execution structure array or null if not found/failed
285
+ */
286
+ export async function loadDocumentStructure(outputDir) {
287
+ if (!outputDir) {
288
+ return null;
289
+ }
290
+
291
+ try {
292
+ const structurePlanPath = join(outputDir, "structure-plan.json");
293
+ const structureExists = await pathExists(structurePlanPath);
294
+
295
+ if (!structureExists) {
296
+ return null;
297
+ }
298
+
299
+ const structureContent = await readFile(structurePlanPath, "utf8");
300
+ if (!structureContent?.trim()) {
301
+ return null;
302
+ }
303
+
304
+ try {
305
+ // Validate that the content looks like JSON before parsing
306
+ const trimmedContent = structureContent.trim();
307
+ if (!trimmedContent.startsWith("[") && !trimmedContent.startsWith("{")) {
308
+ console.warn("structure-plan.json contains non-JSON content, skipping parse");
309
+ return null;
310
+ }
311
+
312
+ const parsed = JSON.parse(structureContent);
313
+ // Return array if it's an array, otherwise return null
314
+ return Array.isArray(parsed) ? parsed : null;
315
+ } catch (parseError) {
316
+ console.error(`Failed to parse structure-plan.json: ${parseError.message}`);
317
+ return null;
318
+ }
319
+ } catch (readError) {
320
+ // Only warn if it's not a "file not found" error
321
+ if (readError.code !== "ENOENT") {
322
+ console.warn(`Error reading structure-plan.json: ${readError.message}`);
323
+ }
324
+ return null;
325
+ }
326
+ }
@@ -99,7 +99,7 @@ function recordUpdateGit({ feedback }) {
99
99
  /**
100
100
  * Records an update in the YAML file.
101
101
  */
102
- function recordUpdateYaml({ operation, feedback, documentPath = null }) {
102
+ function recordUpdateYaml({ operation, feedback, docPaths = null }) {
103
103
  try {
104
104
  const docSmithDir = join(process.cwd(), DOC_SMITH_DIR);
105
105
  if (!existsSync(docSmithDir)) {
@@ -125,9 +125,9 @@ function recordUpdateYaml({ operation, feedback, documentPath = null }) {
125
125
  feedback,
126
126
  };
127
127
 
128
- // Add document path if provided
129
- if (documentPath) {
130
- entry.documentPath = documentPath;
128
+ // Add document paths if provided
129
+ if (Array.isArray(docPaths) && docPaths.length > 0) {
130
+ entry.docPaths = docPaths;
131
131
  }
132
132
 
133
133
  // Add to beginning (newest first)
@@ -153,14 +153,14 @@ function recordUpdateYaml({ operation, feedback, documentPath = null }) {
153
153
  * @param {Object} params
154
154
  * @param {string} params.operation - The type of operation (e.g., 'document_update', 'structure_update', 'translation_update').
155
155
  * @param {string} params.feedback - The user's feedback text.
156
- * @param {string} params.documentPath - The document path.
156
+ * @param {string[]} [params.docPaths] - Document path list for updates.
157
157
  */
158
- export function recordUpdate({ operation, feedback, documentPath = null }) {
158
+ export function recordUpdate({ operation, feedback, docPaths = null }) {
159
159
  // Skip if no feedback
160
160
  if (!feedback?.trim()) return;
161
161
 
162
162
  // Always record in YAML
163
- recordUpdateYaml({ operation, feedback, documentPath });
163
+ recordUpdateYaml({ operation, feedback, docPaths });
164
164
 
165
165
  // Also record in git if git is available and not in a git repository
166
166
  if (isGitAvailable() && !isInGitRepository(process.cwd())) {
@@ -183,7 +183,19 @@ export function getHistory() {
183
183
 
184
184
  try {
185
185
  const content = readFileSync(historyPath, "utf8");
186
- return parse(content) || { entries: [] };
186
+ const parsed = parse(content) || { entries: [] };
187
+ const entries = Array.isArray(parsed.entries) ? parsed.entries : [];
188
+
189
+ const normalized = entries.map((entry) => {
190
+ if (!entry) return entry;
191
+ // Normalize legacy entries: documentPath -> docPaths: [documentPath]
192
+ if (!entry.docPaths && entry.documentPath) {
193
+ return { ...entry, docPaths: [entry.documentPath] };
194
+ }
195
+ return entry;
196
+ });
197
+
198
+ return { ...parsed, entries: normalized };
187
199
  } catch (error) {
188
200
  console.warn("Could not read the history:", error.message);
189
201
  return { entries: [] };