@aigne/doc-smith 0.1.4 → 0.2.0

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.
@@ -0,0 +1,28 @@
1
+ ## Related Issue
2
+
3
+ <!-- Use keywords like fixes, closes, resolves, relates to link the issue. In principle, all PRs should be associated with an issue -->
4
+
5
+ ### Major Changes
6
+
7
+ <!--
8
+ @example:
9
+ 1. Fixed xxx
10
+ 2. Improved xxx
11
+ 3. Adjusted xxx
12
+ -->
13
+
14
+ ### Screenshots
15
+
16
+ <!-- If the changes are related to the UI, whether CLI or WEB, screenshots should be included -->
17
+
18
+ ### Test Plan
19
+
20
+ <!-- If this change is not covered by automated tests, what is your test case collection? Please write it as a to-do list below -->
21
+
22
+ ### Checklist
23
+
24
+ - [ ] This change requires documentation updates, and I have updated the relevant documentation. If the documentation has not been updated, please create a documentation update issue and link it here
25
+ - [ ] The changes are already covered by tests, and I have adjusted the test coverage for the changed parts
26
+ - [ ] The newly added code logic is also covered by tests
27
+ - [ ] This change adds dependencies, and they are placed in dependencies and devDependencies
28
+ - [ ] This change includes adding or updating npm dependencies, and it does not result in multiple versions of the same dependency [check the diff of pnpm-lock.yaml]
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.0](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.1.4...v0.2.0) (2025-08-05)
4
+
5
+
6
+ ### Features
7
+
8
+ * support automatic init configuration when calling agents ([24d29db](https://github.com/AIGNE-io/aigne-doc-smith/commit/24d29db4dd86709750aa22ff649e7dacc4124126))
9
+ * update docs when sources changed ([#9](https://github.com/AIGNE-io/aigne-doc-smith/issues/9)) ([4adcecf](https://github.com/AIGNE-io/aigne-doc-smith/commit/4adcecfb32e72c9e88d0b0bd8ce0a91022847ca7))
10
+
3
11
  ## [0.1.4](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.1.3...v0.1.4) (2025-08-04)
4
12
 
5
13
 
@@ -10,5 +10,9 @@ input_schema:
10
10
  type: string
11
11
  description: 结构规划的上下文,用于辅助结构规划
12
12
  structurePlanResult: ./schema/structure-plan-result.yaml
13
+ modifiedFiles:
14
+ type: array
15
+ items: { type: string }
16
+ description: Array of modified files since last generation
13
17
  iterate_on: structurePlanResult
14
18
  mode: sequential
@@ -3,12 +3,22 @@ import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { TeamAgent } from "@aigne/core";
5
5
  import checkDetailResult from "./check-detail-result.mjs";
6
+ import { hasSourceFilesChanged } from "../utils/utils.mjs";
6
7
 
7
8
  // Get current script directory
8
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
9
10
 
10
11
  export default async function checkDetailGenerated(
11
- { path, docsDir, sourceIds, originalStructurePlan, structurePlan, ...rest },
12
+ {
13
+ path,
14
+ docsDir,
15
+ sourceIds,
16
+ originalStructurePlan,
17
+ structurePlan,
18
+ modifiedFiles,
19
+ lastGitHead,
20
+ ...rest
21
+ },
12
22
  options
13
23
  ) {
14
24
  // Check if the detail file already exists
@@ -61,6 +71,21 @@ export default async function checkDetailGenerated(
61
71
  }
62
72
  }
63
73
 
74
+ // Check if source files have changed since last generation
75
+ let sourceFilesChanged = false;
76
+ if (sourceIds && sourceIds.length > 0 && modifiedFiles) {
77
+ sourceFilesChanged = hasSourceFilesChanged(sourceIds, modifiedFiles);
78
+
79
+ if (sourceFilesChanged) {
80
+ console.log(`Source files changed for ${path}, will regenerate`);
81
+ }
82
+ }
83
+
84
+ // If lastGitHead is not set, regenerate
85
+ if (!lastGitHead) {
86
+ sourceFilesChanged = true;
87
+ }
88
+
64
89
  // If file exists, check content validation
65
90
  let contentValidationFailed = false;
66
91
  if (detailGenerated && fileContent && structurePlan) {
@@ -74,8 +99,13 @@ export default async function checkDetailGenerated(
74
99
  }
75
100
  }
76
101
 
77
- // If file exists, sourceIds haven't changed, and content validation passes, no need to regenerate
78
- if (detailGenerated && !sourceIdsChanged && !contentValidationFailed) {
102
+ // If file exists, sourceIds haven't changed, source files haven't changed, and content validation passes, no need to regenerate
103
+ if (
104
+ detailGenerated &&
105
+ !sourceIdsChanged &&
106
+ !sourceFilesChanged &&
107
+ !contentValidationFailed
108
+ ) {
79
109
  return {
80
110
  path,
81
111
  docsDir,
@@ -51,49 +51,35 @@ export default async function checkDetailResult({
51
51
  }
52
52
  };
53
53
 
54
- const checkTableSeparators = (text, source) => {
55
- // Split text into lines and check each line
54
+ const performAllChecks = (text, source) => {
55
+ // Split text into lines once and perform all checks in a single pass
56
56
  const lines = text.split("\n");
57
- for (let i = 0; i < lines.length; i++) {
58
- const line = lines[i];
59
- if (tableSeparatorRegex.test(line)) {
60
- isApproved = false;
61
- detailFeedback.push(
62
- `Found an incorrect table separator in ${source} at line ${
63
- i + 1
64
- }: ${line.trim()}`
65
- );
66
- }
67
- }
68
- };
69
57
 
70
- const checkSingleLine = (text, source) => {
71
- // Count newline characters to check if content is only on one line
72
- const newlineCount = (text.match(/\n/g) || []).length;
73
- if (newlineCount === 0 && text.trim().length > 0) {
74
- isApproved = false;
75
- detailFeedback.push(
76
- `Found single line content in ${source}: content appears to be on only one line, check for missing line breaks`
77
- );
78
- }
79
- };
80
-
81
- const checkCodeBlockIndentation = (text, source) => {
82
- // Split text into lines and check each line
83
- const lines = text.split("\n");
58
+ // State variables for different checks
84
59
  let inCodeBlock = false;
85
60
  let codeBlockIndentLevel = 0;
86
61
  let codeBlockStartLine = 0;
62
+ let inMermaidBlock = false;
63
+ let mermaidStartLine = 0;
87
64
 
88
65
  for (let i = 0; i < lines.length; i++) {
89
66
  const line = lines[i];
67
+ const lineNumber = i + 1;
90
68
 
91
- // Check if this line is a code block marker
69
+ // Check table separators
70
+ if (tableSeparatorRegex.test(line)) {
71
+ isApproved = false;
72
+ detailFeedback.push(
73
+ `Found an incorrect table separator in ${source} at line ${lineNumber}: ${line.trim()}`
74
+ );
75
+ }
76
+
77
+ // Check code block markers and indentation
92
78
  if (codeBlockRegex.test(line)) {
93
79
  if (!inCodeBlock) {
94
80
  // Starting a new code block
95
81
  inCodeBlock = true;
96
- codeBlockStartLine = i + 1;
82
+ codeBlockStartLine = lineNumber;
97
83
  // Calculate indentation level of the code block marker
98
84
  const match = line.match(/^(\s*)(```)/);
99
85
  codeBlockIndentLevel = match ? match[1].length : 0;
@@ -102,11 +88,8 @@ export default async function checkDetailResult({
102
88
  inCodeBlock = false;
103
89
  codeBlockIndentLevel = 0;
104
90
  }
105
- continue;
106
- }
107
-
108
- // If we're inside a code block, check if content has proper indentation
109
- if (inCodeBlock) {
91
+ } else if (inCodeBlock) {
92
+ // If we're inside a code block, check if content has proper indentation
110
93
  const contentIndentLevel = line.match(/^(\s*)/)[1].length;
111
94
 
112
95
  // If code block marker has indentation, content should have at least the same indentation
@@ -116,23 +99,51 @@ export default async function checkDetailResult({
116
99
  ) {
117
100
  isApproved = false;
118
101
  detailFeedback.push(
119
- `Found code block with inconsistent indentation in ${source} at line ${codeBlockStartLine}: code block marker has ${codeBlockIndentLevel} spaces indentation but content at line ${
120
- i + 1
121
- } has only ${contentIndentLevel} spaces indentation`
102
+ `Found code block with inconsistent indentation in ${source} at line ${codeBlockStartLine}: code block marker has ${codeBlockIndentLevel} spaces indentation but content at line ${lineNumber} has only ${contentIndentLevel} spaces indentation`
122
103
  );
123
104
  // Reset to avoid multiple errors for the same code block
124
105
  inCodeBlock = false;
125
106
  codeBlockIndentLevel = 0;
126
107
  }
127
108
  }
109
+
110
+ // Check mermaid block markers
111
+ if (/^\s*```mermaid\s*$/.test(line)) {
112
+ inMermaidBlock = true;
113
+ mermaidStartLine = lineNumber;
114
+ } else if (inMermaidBlock && /^\s*```\s*$/.test(line)) {
115
+ inMermaidBlock = false;
116
+ } else if (inMermaidBlock) {
117
+ // If we're inside a mermaid block, check for backticks in node labels
118
+ // Check for node definitions with backticks in labels
119
+ // Pattern: A["label with backticks"] or A{"label with backticks"}
120
+ const nodeLabelRegex =
121
+ /[A-Za-z0-9_]+\["([^"]*`[^"]*)"\]|[A-Za-z0-9_]+{"([^}]*`[^}]*)"}/g;
122
+ let match;
123
+
124
+ while ((match = nodeLabelRegex.exec(line)) !== null) {
125
+ const label = match[1] || match[2];
126
+ isApproved = false;
127
+ detailFeedback.push(
128
+ `Found backticks in Mermaid node label in ${source} at line ${lineNumber}: "${label}" - backticks in node labels cause rendering issues in Mermaid diagrams`
129
+ );
130
+ }
131
+ }
132
+ }
133
+
134
+ // Check single line content (this needs to be done after the loop)
135
+ const newlineCount = (text.match(/\n/g) || []).length;
136
+ if (newlineCount === 0 && text.trim().length > 0) {
137
+ isApproved = false;
138
+ detailFeedback.push(
139
+ `Found single line content in ${source}: content appears to be on only one line, check for missing line breaks`
140
+ );
128
141
  }
129
142
  };
130
143
 
131
144
  // Check content
132
145
  checkLinks(reviewContent, "result");
133
- checkTableSeparators(reviewContent, "result");
134
- checkSingleLine(reviewContent, "result");
135
- checkCodeBlockIndentation(reviewContent, "result");
146
+ performAllChecks(reviewContent, "result");
136
147
 
137
148
  return {
138
149
  isApproved,
@@ -1,15 +1,49 @@
1
- import { dirname } from "node:path";
2
- import { fileURLToPath } from "node:url";
3
-
4
- // Get current script directory
5
- const __dirname = dirname(fileURLToPath(import.meta.url));
1
+ import {
2
+ getCurrentGitHead,
3
+ hasFileChangesBetweenCommits,
4
+ } from "../utils/utils.mjs";
6
5
 
7
6
  export default async function checkStructurePlanning(
8
- { originalStructurePlan, feedback, ...rest },
7
+ { originalStructurePlan, feedback, lastGitHead, ...rest },
9
8
  options
10
9
  ) {
11
- // If originalStructurePlan exists, return directly
12
- if (originalStructurePlan && !feedback) {
10
+ // Check if we need to regenerate structure plan
11
+ let shouldRegenerate = false;
12
+ let finalFeedback = feedback;
13
+
14
+ // If no feedback and originalStructurePlan exists, check for git changes
15
+ if (originalStructurePlan) {
16
+ // If no lastGitHead, regenerate by default
17
+ if (!lastGitHead) {
18
+ shouldRegenerate = true;
19
+ } else {
20
+ // Check if there are relevant file changes since last generation
21
+ const currentGitHead = getCurrentGitHead();
22
+ if (currentGitHead && currentGitHead !== lastGitHead) {
23
+ const hasChanges = hasFileChangesBetweenCommits(
24
+ lastGitHead,
25
+ currentGitHead
26
+ );
27
+ if (hasChanges) {
28
+ shouldRegenerate = true;
29
+ }
30
+ }
31
+ }
32
+
33
+ if (shouldRegenerate) {
34
+ finalFeedback = `
35
+ ${finalFeedback || ""}
36
+
37
+ 根据最新的 DataSources 更新结构规划:
38
+ 1. 对于新增的内容,可以根据需要新增节点,或补充到原有节点展示
39
+ 2. 谨慎删除节点,除非节点关联 sourceIds 都被删除了
40
+ 3. 不能修改原有节点的 path
41
+ `;
42
+ }
43
+ }
44
+
45
+ // If no regeneration needed, return original structure plan
46
+ if (originalStructurePlan && !feedback && !shouldRegenerate) {
13
47
  return {
14
48
  structurePlan: originalStructurePlan,
15
49
  };
@@ -18,7 +52,7 @@ export default async function checkStructurePlanning(
18
52
  const panningAgent = options.context.agents["reflective-structure-planner"];
19
53
 
20
54
  const result = await options.context.invoke(panningAgent, {
21
- feedback: feedback || "",
55
+ feedback: finalFeedback || "",
22
56
  originalStructurePlan,
23
57
  ...rest,
24
58
  });
@@ -4,6 +4,9 @@ alias:
4
4
  - up
5
5
  description: Optimize and regenerate individual document content and translations
6
6
  skills:
7
+ - url: ./input-generator.mjs
8
+ default_input:
9
+ skipIfExists: true
7
10
  - ./load-config.mjs
8
11
  - ./load-sources.mjs
9
12
  - type: transform
@@ -5,6 +5,9 @@ alias:
5
5
  - g
6
6
  description: Automatically generates comprehensive project documentation
7
7
  skills:
8
+ - url: ./input-generator.mjs
9
+ default_input:
10
+ skipIfExists: true
8
11
  - ./load-config.mjs
9
12
  - ./load-sources.mjs
10
13
  - ./check-structure-planning.mjs
@@ -1,6 +1,38 @@
1
- import { writeFile, mkdir } from "node:fs/promises";
1
+ import { writeFile, mkdir, readFile } from "node:fs/promises";
2
2
  import { join, dirname } from "node:path";
3
3
 
4
+ // Predefined document generation styles
5
+ const DOCUMENT_STYLES = {
6
+ actionFirst: {
7
+ name: "Action-First Style",
8
+ rules:
9
+ "Action-first and task-oriented; steps first, copyable examples, minimal context; second person, active voice, short sentences",
10
+ },
11
+ conceptFirst: {
12
+ name: "Concept-First Style",
13
+ rules:
14
+ "Why/What before How; precise and restrained, provide trade-offs and comparisons; support with architecture/flow/sequence diagrams",
15
+ },
16
+ specReference: {
17
+ name: "Spec-Reference Style",
18
+ rules:
19
+ "Objective and precise, no rhetoric; tables/Schema focused, authoritative fields and defaults; clear error codes and multi-language examples",
20
+ },
21
+ custom: {
22
+ name: "Custom Rules",
23
+ rules: "Enter your own documentation generation rules",
24
+ },
25
+ };
26
+
27
+ // Predefined target audiences
28
+ const TARGET_AUDIENCES = {
29
+ actionFirst: "Developers, Implementation Engineers, DevOps",
30
+ conceptFirst:
31
+ "Architects, Technical Leads, Developers interested in principles",
32
+ generalUsers: "General Users",
33
+ custom: "Enter your own target audience",
34
+ };
35
+
4
36
  /**
5
37
  * Guide users through multi-turn dialogue to collect information and generate YAML configuration
6
38
  * @param {Object} params
@@ -9,48 +41,125 @@ import { join, dirname } from "node:path";
9
41
  * @returns {Promise<Object>}
10
42
  */
11
43
  export default async function init(
12
- { outputPath = "./doc-smith", fileName = "config.yaml" },
44
+ { outputPath = "./doc-smith", fileName = "config.yaml", skipIfExists = false },
13
45
  options
14
46
  ) {
15
- console.log("Welcome to AIGNE Doc Smith!");
16
- console.log(
17
- "I will help you generate a configuration file through several questions.\n"
18
- );
47
+ if (skipIfExists) {
48
+ const filePath = join(outputPath, fileName);
49
+ if (await readFile(filePath, "utf8").catch(() => null)) {
50
+ return {}
51
+ }
52
+ }
53
+
54
+ console.log("🚀 Welcome to AIGNE Doc Smith!");
55
+ console.log("Let's create your documentation configuration.\n");
19
56
 
20
57
  // Collect user information
21
58
  const input = {};
22
59
 
23
- // 1. Document generation rules
24
- console.log("=== Document Generation Rules ===");
25
- const rulesInput = await options.prompts.input({
26
- message: "Please describe the document generation rules and requirements:",
60
+ // 1. Document generation rules with style selection
61
+ console.log("📝 Step 1/6: Document Generation Rules");
62
+
63
+ // Let user select a document style
64
+ const styleChoice = await options.prompts.select({
65
+ message: "Choose your documentation style:",
66
+ choices: Object.entries(DOCUMENT_STYLES).map(([key, style]) => ({
67
+ name: `${style.name} - ${style.rules}`,
68
+ value: key,
69
+ })),
27
70
  });
28
- input.rules = rulesInput.trim();
29
71
 
30
- // 2. Target audience
31
- console.log("\n=== Target Audience ===");
32
- const targetAudienceInput = await options.prompts.input({
33
- message:
34
- "What is the target audience? (e.g., developers, users, press Enter for default 'developers'):",
72
+ let rules;
73
+ if (styleChoice === "custom") {
74
+ // User wants to input custom rules
75
+ rules = await options.prompts.input({
76
+ message: "Enter your custom documentation rules:",
77
+ });
78
+ } else {
79
+ // Use predefined style directly
80
+ rules = DOCUMENT_STYLES[styleChoice].rules;
81
+ console.log(`✅ Selected: ${DOCUMENT_STYLES[styleChoice].name}`);
82
+ }
83
+
84
+ input.rules = rules.trim();
85
+
86
+ // 2. Target audience selection
87
+ console.log("\n👥 Step 2/6: Target Audience");
88
+
89
+ // Let user select target audience
90
+ const audienceChoice = await options.prompts.select({
91
+ message: "Who is your target audience?",
92
+ choices: Object.entries(TARGET_AUDIENCES).map(([key, audience]) => ({
93
+ name: audience,
94
+ value: key,
95
+ })),
35
96
  });
36
- input.targetAudience = targetAudienceInput.trim() || "developers";
97
+
98
+ let targetAudience;
99
+ if (audienceChoice === "custom") {
100
+ // User wants to input custom audience
101
+ targetAudience = await options.prompts.input({
102
+ message: "Enter your custom target audience:",
103
+ });
104
+ } else {
105
+ // Use predefined audience directly
106
+ targetAudience = TARGET_AUDIENCES[audienceChoice];
107
+ console.log(`✅ Selected: ${TARGET_AUDIENCES[audienceChoice]}`);
108
+ }
109
+
110
+ input.targetAudience = targetAudience.trim();
37
111
 
38
112
  // 3. Language settings
39
- console.log("\n=== Language Settings ===");
113
+ console.log("\n🌐 Step 3/6: Primary Language");
40
114
  const localeInput = await options.prompts.input({
41
- message: "Primary language (e.g., en, zh, press Enter for default 'en'):",
115
+ message:
116
+ "Primary documentation language (e.g., en, zh, press Enter for 'en'):",
42
117
  });
43
118
  input.locale = localeInput.trim() || "en";
44
119
 
45
120
  // 4. Translation languages
46
- console.log("\n=== Translation Settings ===");
47
- const translateInput = await options.prompts.input({
48
- message:
49
- "Translation language list (comma-separated, e.g., zh,en, press Enter to skip):",
121
+ console.log("\n🔄 Step 4/6: Translation Languages");
122
+ console.log(
123
+ "Enter additional languages for translation (press Enter to skip):"
124
+ );
125
+ const translateLanguages = [];
126
+ while (true) {
127
+ const langInput = await options.prompts.input({
128
+ message: `Language ${translateLanguages.length + 1} (e.g., zh, ja, fr):`,
129
+ });
130
+ if (!langInput.trim()) {
131
+ break;
132
+ }
133
+ translateLanguages.push(langInput.trim());
134
+ }
135
+ input.translateLanguages = translateLanguages;
136
+
137
+ // 5. Documentation directory
138
+ console.log("\n📁 Step 5/6: Output Directory");
139
+ const docsDirInput = await options.prompts.input({
140
+ message: `Where to save generated docs (press Enter for '${outputPath}/docs'):`,
50
141
  });
51
- input.translateLanguages = translateInput.trim()
52
- ? translateInput.split(",").map((lang) => lang.trim())
53
- : [];
142
+ input.docsDir = docsDirInput.trim() || `${outputPath}/docs`;
143
+
144
+ // 6. Source code paths
145
+ console.log("\n🔍 Step 6/6: Source Code Paths");
146
+ console.log(
147
+ "Enter paths to analyze for documentation (press Enter to use './'):"
148
+ );
149
+
150
+ const sourcePaths = [];
151
+ while (true) {
152
+ const pathInput = await options.prompts.input({
153
+ message: `Path ${sourcePaths.length + 1} (e.g., ./src, ./lib):`,
154
+ });
155
+ if (!pathInput.trim()) {
156
+ break;
157
+ }
158
+ sourcePaths.push(pathInput.trim());
159
+ }
160
+
161
+ // If no paths entered, use default
162
+ input.sourcesPath = sourcePaths.length > 0 ? sourcePaths : ["./"];
54
163
 
55
164
  // Generate YAML content
56
165
  const yamlContent = generateYAML(input, outputPath);
@@ -64,12 +173,17 @@ export default async function init(
64
173
  await mkdir(dirPath, { recursive: true });
65
174
 
66
175
  await writeFile(filePath, yamlContent, "utf8");
67
- console.log(`\n Configuration file saved to: ${filePath}`);
176
+ console.log(`\n🎉 Configuration saved to: ${filePath}`);
177
+ console.log(
178
+ "💡 You can edit the configuration file anytime to modify settings."
179
+ );
180
+ console.log(
181
+ "🚀 Run 'aigne doc generate' to start documentation generation!"
182
+ );
68
183
 
69
184
  return {
70
185
  inputGeneratorStatus: true,
71
186
  inputGeneratorPath: filePath,
72
- inputGeneratorContent: yamlContent,
73
187
  };
74
188
  } catch (error) {
75
189
  console.error(`❌ Failed to save configuration file: ${error.message}`);
@@ -121,11 +235,13 @@ function generateYAML(input, outputPath) {
121
235
  yaml += `# - en # Example: English translation\n`;
122
236
  }
123
237
 
124
- // Add default directory and source path configurations
125
- yaml += `docsDir: ${outputPath}/docs # Directory to save generated documentation\n`;
126
- yaml += `outputDir: ${outputPath}/output # Directory to save output files\n`;
238
+ // Add directory and source path configurations
239
+ yaml += `docsDir: ${input.docsDir} # Directory to save generated documentation\n`;
240
+ // yaml += `outputDir: ${outputPath}/output # Directory to save output files\n`;
127
241
  yaml += `sourcesPath: # Source code paths to analyze\n`;
128
- yaml += ` - ./ # Current directory\n`;
242
+ input.sourcesPath.forEach((path) => {
243
+ yaml += ` - ${path}\n`;
244
+ });
129
245
 
130
246
  return yaml;
131
247
  }
@@ -24,6 +24,7 @@ export default async function loadConfig({ config }) {
24
24
  sourcesPath: ["./"],
25
25
  docDir: "./doc-smith/docs",
26
26
  outputDir: "./doc-smith/output",
27
+ lastGitHead: parsedConfig.lastGitHead || "",
27
28
  ...parsedConfig,
28
29
  };
29
30
  } catch (error) {
@@ -1,67 +1,14 @@
1
1
  import { access, readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { glob } from "glob";
4
-
5
- // Default file patterns for inclusion and exclusion
6
- const DEFAULT_INCLUDE_PATTERNS = [
7
- "*.py",
8
- "*.js",
9
- "*.jsx",
10
- "*.ts",
11
- "*.tsx",
12
- "*.go",
13
- "*.java",
14
- "*.pyi",
15
- "*.pyx",
16
- "*.c",
17
- "*.cc",
18
- "*.cpp",
19
- "*.h",
20
- "*.md",
21
- "*.rst",
22
- "*.json",
23
- "*Dockerfile",
24
- "*Makefile",
25
- "*.yaml",
26
- "*.yml",
27
- ];
28
-
29
- const DEFAULT_EXCLUDE_PATTERNS = [
30
- "aigne-docs/**",
31
- "doc-smith/**",
32
- "assets/**",
33
- "data/**",
34
- "images/**",
35
- "public/**",
36
- "static/**",
37
- "**/vendor/**",
38
- "temp/**",
39
- "**/*docs/**",
40
- "**/*doc/**",
41
- "**/*venv/**",
42
- "*.venv/**",
43
- "*test*",
44
- "**/*test/**",
45
- "**/*tests/**",
46
- "**/*examples/**",
47
- "**/playgrounds/**",
48
- "v1/**",
49
- "**/dist/**",
50
- "**/*build/**",
51
- "**/*experimental/**",
52
- "**/*deprecated/**",
53
- "**/*misc/**",
54
- "**/*legacy/**",
55
- ".git/**",
56
- ".github/**",
57
- ".next/**",
58
- ".vscode/**",
59
- "**/*obj/**",
60
- "**/*bin/**",
61
- "**/*node_modules/**",
62
- "*.log",
63
- "**/*test.*",
64
- ];
4
+ import {
5
+ getCurrentGitHead,
6
+ getModifiedFilesBetweenCommits,
7
+ } from "../utils/utils.mjs";
8
+ import {
9
+ DEFAULT_INCLUDE_PATTERNS,
10
+ DEFAULT_EXCLUDE_PATTERNS,
11
+ } from "../utils/constants.mjs";
65
12
 
66
13
  /**
67
14
  * Load .gitignore patterns from a directory
@@ -158,6 +105,7 @@ export default async function loadSources({
158
105
  "doc-path": docPath,
159
106
  boardId,
160
107
  useDefaultPatterns = true,
108
+ lastGitHead,
161
109
  } = {}) {
162
110
  let files = Array.isArray(sources) ? [...sources] : [];
163
111
 
@@ -224,9 +172,11 @@ export default async function loadSources({
224
172
  const sourceFiles = await Promise.all(
225
173
  files.map(async (file) => {
226
174
  const content = await readFile(file, "utf8");
227
- allSources += `// sourceId: ${file}\n${content}\n`;
175
+ // Convert absolute path to relative path from project root
176
+ const relativePath = path.relative(process.cwd(), file);
177
+ allSources += `// sourceId: ${relativePath}\n${content}\n`;
228
178
  return {
229
- sourceId: file,
179
+ sourceId: relativePath,
230
180
  content,
231
181
  };
232
182
  })
@@ -280,12 +230,34 @@ export default async function loadSources({
280
230
  }
281
231
  }
282
232
 
233
+ // Get git change detection data
234
+ let modifiedFiles = [];
235
+ let currentGitHead = null;
236
+
237
+ if (lastGitHead) {
238
+ try {
239
+ currentGitHead = getCurrentGitHead();
240
+ if (currentGitHead && currentGitHead !== lastGitHead) {
241
+ modifiedFiles = getModifiedFilesBetweenCommits(
242
+ lastGitHead,
243
+ currentGitHead
244
+ );
245
+ console.log(
246
+ `Detected ${modifiedFiles.length} modified files since last generation`
247
+ );
248
+ }
249
+ } catch (error) {
250
+ console.warn("Failed to detect git changes:", error.message);
251
+ }
252
+ }
253
+
283
254
  return {
284
255
  datasourcesList: sourceFiles,
285
256
  datasources: allSources,
286
257
  content,
287
258
  originalStructurePlan,
288
259
  files,
260
+ modifiedFiles,
289
261
  };
290
262
  }
291
263
 
@@ -324,6 +296,10 @@ loadSources.input_schema = {
324
296
  type: "string",
325
297
  description: "The board ID for boardId-flattenedPath format matching",
326
298
  },
299
+ lastGitHead: {
300
+ type: "string",
301
+ description: "The git HEAD from last generation for change detection",
302
+ },
327
303
  },
328
304
  required: [],
329
305
  };
@@ -349,5 +325,10 @@ loadSources.output_schema = {
349
325
  items: { type: "string" },
350
326
  description: "Array of file paths that were loaded",
351
327
  },
328
+ modifiedFiles: {
329
+ type: "array",
330
+ items: { type: "string" },
331
+ description: "Array of modified files since last generation",
332
+ },
352
333
  },
353
334
  };
@@ -9,8 +9,10 @@ import { homedir } from "node:os";
9
9
  import { parse, stringify } from "yaml";
10
10
  import { execSync } from "node:child_process";
11
11
  import { basename } from "node:path";
12
+ import { loadConfigFromFile, saveValueToConfig } from "../utils/utils.mjs";
12
13
 
13
14
  const WELLKNOWN_SERVICE_PATH_PREFIX = "/.well-known/service";
15
+ const DEFAULT_APP_URL = "https://docsmith.aigne.io";
14
16
 
15
17
  /**
16
18
  * Get project name from git repository or current directory
@@ -116,51 +118,50 @@ async function getAccessToken(appUrl) {
116
118
  return accessToken;
117
119
  }
118
120
 
119
- /**
120
- * Save boardId to config.yaml file if it was auto-created
121
- * @param {string} boardId - The original boardId (may be empty)
122
- * @param {string} newBoardId - The boardId returned from publishDocsFn
123
- */
124
- async function saveBoardIdToInput(boardId, newBoardId) {
125
- // Only save if boardId was auto-created
126
- if (!boardId && newBoardId) {
127
- try {
128
- const docSmithDir = join(process.cwd(), "doc-smith");
129
- if (!existsSync(docSmithDir)) {
130
- mkdirSync(docSmithDir, { recursive: true });
131
- }
132
-
133
- const inputFilePath = join(docSmithDir, "config.yaml");
134
- let fileContent = "";
135
-
136
- // Read existing file content if it exists
137
- if (existsSync(inputFilePath)) {
138
- fileContent = await readFile(inputFilePath, "utf8");
139
- }
140
-
141
- // Check if boardId already exists in the file
142
- const boardIdRegex = /^boardId:\s*.*$/m;
143
- const newBoardIdLine = `boardId: ${newBoardId}`;
144
-
145
- if (boardIdRegex.test(fileContent)) {
146
- // Replace existing boardId line
147
- fileContent = fileContent.replace(boardIdRegex, newBoardIdLine);
148
- } else {
149
- // Add boardId to the end of file
150
- if (fileContent && !fileContent.endsWith("\n")) {
151
- fileContent += "\n";
152
- }
153
- fileContent += newBoardIdLine + "\n";
154
- }
121
+ export default async function publishDocs(
122
+ { docsDir, appUrl, boardId },
123
+ options
124
+ ) {
125
+ // Check if appUrl is default and not saved in config
126
+ const config = await loadConfigFromFile();
127
+ const isDefaultAppUrl = appUrl === DEFAULT_APP_URL;
128
+ const hasAppUrlInConfig = config && config.appUrl;
129
+
130
+ if (isDefaultAppUrl && !hasAppUrlInConfig) {
131
+ console.log("\n=== Document Publishing Platform Selection ===");
132
+ console.log(
133
+ "Please select the platform where you want to publish your documents:"
134
+ );
155
135
 
156
- await writeFile(inputFilePath, fileContent);
157
- } catch (error) {
158
- console.warn("Failed to save board ID to config.yaml:", error.message);
136
+ const choice = await options.prompts.select({
137
+ message: "Select publishing platform:",
138
+ choices: [
139
+ {
140
+ name: "Use official platform (docsmith.aigne.io) - Documents will be publicly accessible, suitable for open source projects",
141
+ value: "default",
142
+ },
143
+ {
144
+ name: "Use private platform - Deploy your own Discuss Kit instance, suitable for internal documentation",
145
+ value: "custom",
146
+ },
147
+ ],
148
+ });
149
+
150
+ if (choice === "custom") {
151
+ appUrl = await options.prompts.input({
152
+ message: "Please enter your Discuss Kit platform URL:",
153
+ validate: (input) => {
154
+ try {
155
+ new URL(input);
156
+ return true;
157
+ } catch {
158
+ return "Please enter a valid URL";
159
+ }
160
+ },
161
+ });
159
162
  }
160
163
  }
161
- }
162
164
 
163
- export default async function publishDocs({ docsDir, appUrl, boardId }) {
164
165
  const accessToken = await getAccessToken(appUrl);
165
166
 
166
167
  process.env.DOC_ROOT_DIR = docsDir;
@@ -179,8 +180,16 @@ export default async function publishDocs({ docsDir, appUrl, boardId }) {
179
180
  autoCreateBoard: !boardId,
180
181
  });
181
182
 
182
- // Save boardId to config.yaml if it was auto-created
183
- await saveBoardIdToInput(boardId, newBoardId);
183
+ // Save values to config.yaml if publish was successful
184
+ if (success) {
185
+ // Save appUrl to config
186
+ await saveValueToConfig("appUrl", appUrl);
187
+
188
+ // Save boardId to config if it was auto-created
189
+ if (!boardId && newBoardId) {
190
+ await saveValueToConfig("boardId", newBoardId);
191
+ }
192
+ }
184
193
 
185
194
  return {
186
195
  publishResult: {
@@ -199,9 +208,7 @@ publishDocs.input_schema = {
199
208
  appUrl: {
200
209
  type: "string",
201
210
  description: "The url of the app",
202
- default:
203
- // "https://bbqawfllzdt3pahkdsrsone6p3wpxcwp62vlabtawfu.did.abtnet.io",
204
- "https://www.staging.arcblock.io",
211
+ default: DEFAULT_APP_URL,
205
212
  },
206
213
  boardId: {
207
214
  type: "string",
@@ -1,5 +1,6 @@
1
1
  import { writeFile, readdir, unlink } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { getCurrentGitHead, saveGitHeadToConfig } from "../utils/utils.mjs";
3
4
 
4
5
  /**
5
6
  * @param {Object} params
@@ -16,6 +17,14 @@ export default async function saveDocs({
16
17
  }) {
17
18
  const results = [];
18
19
 
20
+ // Save current git HEAD to config.yaml for change detection
21
+ try {
22
+ const gitHead = getCurrentGitHead();
23
+ await saveGitHeadToConfig(gitHead);
24
+ } catch (err) {
25
+ console.warn("Failed to save git HEAD:", err.message);
26
+ }
27
+
19
28
  // Generate _sidebar.md
20
29
  try {
21
30
  const sidebar = generateSidebar(structurePlan);
@@ -5,6 +5,9 @@ alias:
5
5
  - p
6
6
  description: Publish the documentation to Discuss Kit
7
7
  skills:
8
+ - url: ./input-generator.mjs
9
+ default_input:
10
+ skipIfExists: true
8
11
  - load-config.mjs
9
12
  - publish-docs.mjs
10
13
  input_schema:
@@ -1,14 +1,28 @@
1
+ import { normalizePath, toRelativePath } from "../utils/utils.mjs";
2
+
1
3
  export default function transformDetailDatasources({
2
4
  sourceIds,
3
5
  datasourcesList,
4
6
  }) {
5
- // Build a map for fast lookup
7
+ // Build a map for fast lookup, with path normalization for compatibility
6
8
  const dsMap = Object.fromEntries(
7
- (datasourcesList || []).map((ds) => [ds.sourceId, ds.content])
9
+ (datasourcesList || []).map((ds) => {
10
+ const normalizedSourceId = normalizePath(ds.sourceId);
11
+ return [normalizedSourceId, ds.content];
12
+ })
8
13
  );
9
- // Collect formatted contents in order
14
+
15
+ // Collect formatted contents in order, with path normalization
10
16
  const contents = (sourceIds || [])
11
- .filter((id) => dsMap[id])
12
- .map((id) => `// sourceId: ${id}\n${dsMap[id]}\n`);
17
+ .filter((id) => {
18
+ const normalizedId = normalizePath(id);
19
+ return dsMap[normalizedId];
20
+ })
21
+ .map((id) => {
22
+ const normalizedId = normalizePath(id);
23
+ const relativeId = toRelativePath(id);
24
+ return `// sourceId: ${relativeId}\n${dsMap[normalizedId]}\n`;
25
+ });
26
+
13
27
  return { detailDataSources: contents.join("") };
14
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/doc-smith",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -17,7 +17,7 @@
17
17
  ```
18
18
  {{ feedback }}
19
19
 
20
- 根据最新的 Data Sources 按需要更新节点的 sourceIds ,更新至状态。
20
+ 根据最新的 Data Sources 按需要更新节点的 sourceIds
21
21
  ```
22
22
  </context>
23
23
 
@@ -30,7 +30,7 @@
30
30
  <structure_plan_feedback>
31
31
  {{ feedback }}
32
32
 
33
- 根据最新的 Data Sources 按需要更新节点的 sourceIds ,更新至状态。
33
+ 根据最新的 Data Sources 按需要更新节点的 sourceIds
34
34
  </structure_plan_feedback>
35
35
 
36
36
  <review_structure_plan>
@@ -0,0 +1,60 @@
1
+ // Default file patterns for inclusion and exclusion
2
+ export const DEFAULT_INCLUDE_PATTERNS = [
3
+ "*.py",
4
+ "*.js",
5
+ "*.jsx",
6
+ "*.ts",
7
+ "*.tsx",
8
+ "*.go",
9
+ "*.java",
10
+ "*.pyi",
11
+ "*.pyx",
12
+ "*.c",
13
+ "*.cc",
14
+ "*.cpp",
15
+ "*.h",
16
+ "*.md",
17
+ "*.rst",
18
+ "*.json",
19
+ "*Dockerfile",
20
+ "*Makefile",
21
+ "*.yaml",
22
+ "*.yml",
23
+ ];
24
+
25
+ export const DEFAULT_EXCLUDE_PATTERNS = [
26
+ "aigne-docs/**",
27
+ "doc-smith/**",
28
+ "assets/**",
29
+ "data/**",
30
+ "images/**",
31
+ "public/**",
32
+ "static/**",
33
+ "**/vendor/**",
34
+ "temp/**",
35
+ "**/*docs/**",
36
+ "**/*doc/**",
37
+ "**/*venv/**",
38
+ "*.venv/**",
39
+ "*test*",
40
+ "**/*test/**",
41
+ "**/*tests/**",
42
+ "**/*examples/**",
43
+ "**/playgrounds/**",
44
+ "v1/**",
45
+ "**/dist/**",
46
+ "**/*build/**",
47
+ "**/*experimental/**",
48
+ "**/*deprecated/**",
49
+ "**/*misc/**",
50
+ "**/*legacy/**",
51
+ ".git/**",
52
+ ".github/**",
53
+ ".next/**",
54
+ ".vscode/**",
55
+ "**/*obj/**",
56
+ "**/*bin/**",
57
+ "**/*node_modules/**",
58
+ "*.log",
59
+ "**/*test.*",
60
+ ];
package/utils/utils.mjs CHANGED
@@ -1,5 +1,34 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { execSync } from "node:child_process";
4
+ import { existsSync, mkdirSync } from "node:fs";
5
+ import { parse } from "yaml";
6
+ import {
7
+ DEFAULT_INCLUDE_PATTERNS,
8
+ DEFAULT_EXCLUDE_PATTERNS,
9
+ } from "./constants.mjs";
10
+
11
+ /**
12
+ * Normalize path to absolute path for consistent comparison
13
+ * @param {string} filePath - The path to normalize
14
+ * @returns {string} - Absolute path
15
+ */
16
+ export function normalizePath(filePath) {
17
+ return path.isAbsolute(filePath)
18
+ ? filePath
19
+ : path.resolve(process.cwd(), filePath);
20
+ }
21
+
22
+ /**
23
+ * Convert path to relative path from current working directory
24
+ * @param {string} filePath - The path to convert
25
+ * @returns {string} - Relative path
26
+ */
27
+ export function toRelativePath(filePath) {
28
+ return path.isAbsolute(filePath)
29
+ ? path.relative(process.cwd(), filePath)
30
+ : filePath;
31
+ }
3
32
 
4
33
  export function processContent({ content }) {
5
34
  // Match markdown regular links [text](link), exclude images ![text](link)
@@ -95,3 +124,273 @@ export async function saveDocWithTranslations({
95
124
  }
96
125
  return results;
97
126
  }
127
+
128
+ /**
129
+ * Get current git HEAD commit hash
130
+ * @returns {string} - The current git HEAD commit hash
131
+ */
132
+ export function getCurrentGitHead() {
133
+ try {
134
+ return execSync("git rev-parse HEAD", {
135
+ encoding: "utf8",
136
+ stdio: ["pipe", "pipe", "ignore"],
137
+ }).trim();
138
+ } catch (error) {
139
+ // Not in git repository or git command failed
140
+ console.warn("Failed to get git HEAD:", error.message);
141
+ return null;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Save git HEAD to config.yaml file
147
+ * @param {string} gitHead - The current git HEAD commit hash
148
+ */
149
+ export async function saveGitHeadToConfig(gitHead) {
150
+ if (!gitHead) {
151
+ return; // Skip if no git HEAD available
152
+ }
153
+
154
+ try {
155
+ const docSmithDir = path.join(process.cwd(), "doc-smith");
156
+ if (!existsSync(docSmithDir)) {
157
+ mkdirSync(docSmithDir, { recursive: true });
158
+ }
159
+
160
+ const inputFilePath = path.join(docSmithDir, "config.yaml");
161
+ let fileContent = "";
162
+
163
+ // Read existing file content if it exists
164
+ if (existsSync(inputFilePath)) {
165
+ fileContent = await fs.readFile(inputFilePath, "utf8");
166
+ }
167
+
168
+ // Check if lastGitHead already exists in the file
169
+ const lastGitHeadRegex = /^lastGitHead:\s*.*$/m;
170
+ const newLastGitHeadLine = `lastGitHead: ${gitHead}`;
171
+
172
+ if (lastGitHeadRegex.test(fileContent)) {
173
+ // Replace existing lastGitHead line
174
+ fileContent = fileContent.replace(lastGitHeadRegex, newLastGitHeadLine);
175
+ } else {
176
+ // Add lastGitHead to the end of file
177
+ if (fileContent && !fileContent.endsWith("\n")) {
178
+ fileContent += "\n";
179
+ }
180
+ fileContent += newLastGitHeadLine + "\n";
181
+ }
182
+
183
+ await fs.writeFile(inputFilePath, fileContent);
184
+ } catch (error) {
185
+ console.warn("Failed to save git HEAD to config.yaml:", error.message);
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Check if files have been modified between two git commits
191
+ * @param {string} fromCommit - Starting commit hash
192
+ * @param {string} toCommit - Ending commit hash (defaults to HEAD)
193
+ * @param {Array<string>} filePaths - Array of file paths to check
194
+ * @returns {Array<string>} - Array of modified file paths
195
+ */
196
+ export function getModifiedFilesBetweenCommits(
197
+ fromCommit,
198
+ toCommit = "HEAD",
199
+ filePaths = []
200
+ ) {
201
+ try {
202
+ // Get all modified files between commits
203
+ const modifiedFiles = execSync(
204
+ `git diff --name-only ${fromCommit}..${toCommit}`,
205
+ {
206
+ encoding: "utf8",
207
+ stdio: ["pipe", "pipe", "ignore"],
208
+ }
209
+ )
210
+ .trim()
211
+ .split("\n")
212
+ .filter(Boolean);
213
+
214
+ // Filter to only include files we care about
215
+ if (filePaths.length === 0) {
216
+ return modifiedFiles;
217
+ }
218
+
219
+ return modifiedFiles.filter((file) =>
220
+ filePaths.some((targetPath) => {
221
+ const absoluteFile = normalizePath(file);
222
+ const absoluteTarget = normalizePath(targetPath);
223
+ return absoluteFile === absoluteTarget;
224
+ })
225
+ );
226
+ } catch (error) {
227
+ console.warn(
228
+ `Failed to get modified files between ${fromCommit} and ${toCommit}:`,
229
+ error.message
230
+ );
231
+ return [];
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Check if any source files have changed based on modified files list
237
+ * @param {Array<string>} sourceIds - Source file paths
238
+ * @param {Array<string>} modifiedFiles - List of modified files between commits
239
+ * @returns {boolean} - True if any source files have changed
240
+ */
241
+ export function hasSourceFilesChanged(sourceIds, modifiedFiles) {
242
+ if (!sourceIds || sourceIds.length === 0 || !modifiedFiles) {
243
+ return false; // No source files or no modified files
244
+ }
245
+
246
+ return modifiedFiles.some((modifiedFile) =>
247
+ sourceIds.some((sourceId) => {
248
+ const absoluteModifiedFile = normalizePath(modifiedFile);
249
+ const absoluteSourceId = normalizePath(sourceId);
250
+ return absoluteModifiedFile === absoluteSourceId;
251
+ })
252
+ );
253
+ }
254
+
255
+ /**
256
+ * Check if there are any added or deleted files between two git commits that match the include/exclude patterns
257
+ * @param {string} fromCommit - Starting commit hash
258
+ * @param {string} toCommit - Ending commit hash (defaults to HEAD)
259
+ * @param {Array<string>} includePatterns - Include patterns to match files
260
+ * @param {Array<string>} excludePatterns - Exclude patterns to filter files
261
+ * @returns {boolean} - True if there are relevant added/deleted files
262
+ */
263
+ export function hasFileChangesBetweenCommits(
264
+ fromCommit,
265
+ toCommit = "HEAD",
266
+ includePatterns = DEFAULT_INCLUDE_PATTERNS,
267
+ excludePatterns = DEFAULT_EXCLUDE_PATTERNS
268
+ ) {
269
+ try {
270
+ // Get file changes with status (A=added, D=deleted, M=modified)
271
+ const changes = execSync(
272
+ `git diff --name-status ${fromCommit}..${toCommit}`,
273
+ {
274
+ encoding: "utf8",
275
+ stdio: ["pipe", "pipe", "ignore"],
276
+ }
277
+ )
278
+ .trim()
279
+ .split("\n")
280
+ .filter(Boolean);
281
+
282
+ // Only check for added (A) and deleted (D) files
283
+ const addedOrDeletedFiles = changes
284
+ .filter((line) => {
285
+ const [status, filePath] = line.split(/\s+/);
286
+ return (status === "A" || status === "D") && filePath;
287
+ })
288
+ .map((line) => line.split(/\s+/)[1]);
289
+
290
+ if (addedOrDeletedFiles.length === 0) {
291
+ return false;
292
+ }
293
+
294
+ // Check if any of the added/deleted files match the include patterns and don't match exclude patterns
295
+ return addedOrDeletedFiles.some((filePath) => {
296
+ // Check if file matches any include pattern
297
+ const matchesInclude = includePatterns.some((pattern) => {
298
+ // Convert glob pattern to regex for matching
299
+ const regexPattern = pattern
300
+ .replace(/\./g, "\\.")
301
+ .replace(/\*/g, ".*")
302
+ .replace(/\?/g, ".");
303
+ const regex = new RegExp(regexPattern);
304
+ return regex.test(filePath);
305
+ });
306
+
307
+ if (!matchesInclude) {
308
+ return false;
309
+ }
310
+
311
+ // Check if file matches any exclude pattern
312
+ const matchesExclude = excludePatterns.some((pattern) => {
313
+ // Convert glob pattern to regex for matching
314
+ const regexPattern = pattern
315
+ .replace(/\./g, "\\.")
316
+ .replace(/\*/g, ".*")
317
+ .replace(/\?/g, ".");
318
+ const regex = new RegExp(regexPattern);
319
+ return regex.test(filePath);
320
+ });
321
+
322
+ return !matchesExclude;
323
+ });
324
+ } catch (error) {
325
+ console.warn(
326
+ `Failed to check file changes between ${fromCommit} and ${toCommit}:`,
327
+ error.message
328
+ );
329
+ return false;
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Load config from config.yaml file
335
+ * @returns {Promise<Object|null>} - The config object or null if file doesn't exist
336
+ */
337
+ export async function loadConfigFromFile() {
338
+ const configPath = path.join(process.cwd(), "doc-smith", "config.yaml");
339
+
340
+ try {
341
+ if (!existsSync(configPath)) {
342
+ return null;
343
+ }
344
+
345
+ const configContent = await fs.readFile(configPath, "utf8");
346
+ return parse(configContent);
347
+ } catch (error) {
348
+ console.warn("Failed to read config file:", error.message);
349
+ return null;
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Save value to config.yaml file
355
+ * @param {string} key - The config key to save
356
+ * @param {string} value - The value to save
357
+ */
358
+ export async function saveValueToConfig(key, value) {
359
+ if (!value) {
360
+ return; // Skip if no value provided
361
+ }
362
+
363
+ try {
364
+ const docSmithDir = path.join(process.cwd(), "doc-smith");
365
+ if (!existsSync(docSmithDir)) {
366
+ mkdirSync(docSmithDir, { recursive: true });
367
+ }
368
+
369
+ const configPath = path.join(docSmithDir, "config.yaml");
370
+ let fileContent = "";
371
+
372
+ // Read existing file content if it exists
373
+ if (existsSync(configPath)) {
374
+ fileContent = await fs.readFile(configPath, "utf8");
375
+ }
376
+
377
+ // Check if key already exists in the file
378
+ const keyRegex = new RegExp(`^${key}:\\s*.*$`, "m");
379
+ const newKeyLine = `${key}: ${value}`;
380
+
381
+ if (keyRegex.test(fileContent)) {
382
+ // Replace existing key line
383
+ fileContent = fileContent.replace(keyRegex, newKeyLine);
384
+ } else {
385
+ // Add key to the end of file
386
+ if (fileContent && !fileContent.endsWith("\n")) {
387
+ fileContent += "\n";
388
+ }
389
+ fileContent += newKeyLine + "\n";
390
+ }
391
+
392
+ await fs.writeFile(configPath, fileContent);
393
+ } catch (error) {
394
+ console.warn(`Failed to save ${key} to config.yaml:`, error.message);
395
+ }
396
+ }