@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.
- package/.github/PULL_REQUEST_TEMPLATE.md +28 -0
- package/CHANGELOG.md +8 -0
- package/agents/batch-docs-detail-generator.yaml +4 -0
- package/agents/check-detail-generated.mjs +33 -3
- package/agents/check-detail-result.mjs +52 -41
- package/agents/check-structure-planning.mjs +43 -9
- package/agents/detail-regenerator.yaml +3 -0
- package/agents/docs-generator.yaml +3 -0
- package/agents/input-generator.mjs +148 -32
- package/agents/load-config.mjs +1 -0
- package/agents/load-sources.mjs +44 -63
- package/agents/publish-docs.mjs +53 -46
- package/agents/save-docs.mjs +9 -0
- package/agents/team-publish-docs.yaml +3 -0
- package/agents/transform-detail-datasources.mjs +19 -5
- package/package.json +1 -1
- package/prompts/check-structure-planning-result.md +1 -1
- package/prompts/structure-planning.md +1 -1
- package/utils/constants.mjs +60 -0
- package/utils/utils.mjs +299 -0
|
@@ -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
|
-
{
|
|
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 (
|
|
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
|
|
55
|
-
// Split text into lines and
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
//
|
|
12
|
-
|
|
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:
|
|
55
|
+
feedback: finalFeedback || "",
|
|
22
56
|
originalStructurePlan,
|
|
23
57
|
...rest,
|
|
24
58
|
});
|
|
@@ -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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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("
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
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
|
-
|
|
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
|
|
113
|
+
console.log("\n🌐 Step 3/6: Primary Language");
|
|
40
114
|
const localeInput = await options.prompts.input({
|
|
41
|
-
message:
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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.
|
|
52
|
-
|
|
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
|
|
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
|
|
125
|
-
yaml += `docsDir: ${
|
|
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
|
-
|
|
242
|
+
input.sourcesPath.forEach((path) => {
|
|
243
|
+
yaml += ` - ${path}\n`;
|
|
244
|
+
});
|
|
129
245
|
|
|
130
246
|
return yaml;
|
|
131
247
|
}
|
package/agents/load-config.mjs
CHANGED
package/agents/load-sources.mjs
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
};
|
package/agents/publish-docs.mjs
CHANGED
|
@@ -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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
183
|
-
|
|
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",
|
package/agents/save-docs.mjs
CHANGED
|
@@ -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);
|
|
@@ -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) =>
|
|
9
|
+
(datasourcesList || []).map((ds) => {
|
|
10
|
+
const normalizedSourceId = normalizePath(ds.sourceId);
|
|
11
|
+
return [normalizedSourceId, ds.content];
|
|
12
|
+
})
|
|
8
13
|
);
|
|
9
|
-
|
|
14
|
+
|
|
15
|
+
// Collect formatted contents in order, with path normalization
|
|
10
16
|
const contents = (sourceIds || [])
|
|
11
|
-
.filter((id) =>
|
|
12
|
-
|
|
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
|
@@ -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 
|
|
@@ -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
|
+
}
|