@aigne/doc-smith 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +3 -1
- package/agents/batch-docs-detail-generator.yaml +3 -2
- package/agents/batch-translate.yaml +1 -1
- package/agents/{check-detail-generated.mjs → check-detail.mjs} +6 -4
- package/agents/{check-structure-planning.mjs → check-structure-plan.mjs} +9 -4
- package/agents/check-structure-planning-result.yaml +1 -1
- package/agents/content-detail-generator.yaml +1 -1
- package/agents/detail-generator-and-translate.yaml +1 -1
- package/agents/docs-generator.yaml +11 -9
- package/agents/find-item-by-path.mjs +84 -5
- package/agents/input-generator.mjs +94 -67
- package/agents/publish-docs.mjs +133 -29
- package/agents/reflective-structure-planner.yaml +1 -1
- package/agents/save-docs.mjs +4 -7
- package/agents/save-single-doc.mjs +1 -1
- package/agents/structure-planning.yaml +1 -1
- package/aigne.yaml +2 -2
- package/package.json +9 -7
- package/prompts/structure-planning.md +1 -1
- package/utils/constants.mjs +45 -0
- package/utils/utils.mjs +246 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.2](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.2.1...v0.2.2) (2025-08-07)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Miscellaneous Chores
|
|
7
|
+
|
|
8
|
+
* release 0.2.2 ([c3fb52a](https://github.com/AIGNE-io/aigne-doc-smith/commit/c3fb52a78b95676e1c13361b30ebec2914a89fa8))
|
|
9
|
+
|
|
10
|
+
## [0.2.1](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.2.0...v0.2.1) (2025-08-06)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Miscellaneous Chores
|
|
14
|
+
|
|
15
|
+
* release 0.2.1 ([e3a39ae](https://github.com/AIGNE-io/aigne-doc-smith/commit/e3a39aedcee129deae424e96942f9798b9191663))
|
|
16
|
+
|
|
3
17
|
## [0.2.0](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.1.4...v0.2.0) (2025-08-05)
|
|
4
18
|
|
|
5
19
|
|
package/README.md
CHANGED
|
@@ -63,8 +63,10 @@ npx --no doc-smith run --entry-agent init
|
|
|
63
63
|
# 生成命令
|
|
64
64
|
npx --no doc-smith run --entry-agent generate --model gemini:gemini-2.5-flash
|
|
65
65
|
|
|
66
|
+
aigne run --path /Users/lban/arcblock/code/aigne-doc-smith/ --entry-agent generate --model gemini:gemini-2.5-flash --input-forceRegenerate=true
|
|
67
|
+
|
|
66
68
|
# 重新生成单篇
|
|
67
|
-
npx --no doc-smith run --entry-agent update --input-path bitnet-getting-started
|
|
69
|
+
npx --no doc-smith run --entry-agent update --input-doc-path bitnet-getting-started
|
|
68
70
|
|
|
69
71
|
# 结构规划优化
|
|
70
72
|
npx --no doc-smith run --entry-agent generate --input-feedback "补充节点的 sourceIds,确保所有节点 sourceIds 都有值" --model gemini:gemini-2.5-pro
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
type: team
|
|
2
|
-
name:
|
|
2
|
+
name: batchDocsDetailGenerator
|
|
3
3
|
description: 批量生成文档详情
|
|
4
4
|
skills:
|
|
5
|
-
- ./check-detail
|
|
5
|
+
- ./check-detail.mjs
|
|
6
6
|
input_schema:
|
|
7
7
|
type: object
|
|
8
8
|
properties:
|
|
@@ -15,4 +15,5 @@ input_schema:
|
|
|
15
15
|
items: { type: string }
|
|
16
16
|
description: Array of modified files since last generation
|
|
17
17
|
iterate_on: structurePlanResult
|
|
18
|
+
concurrency: 3
|
|
18
19
|
mode: sequential
|
|
@@ -8,7 +8,7 @@ import { hasSourceFilesChanged } from "../utils/utils.mjs";
|
|
|
8
8
|
// Get current script directory
|
|
9
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
|
|
11
|
-
export default async function
|
|
11
|
+
export default async function checkDetail(
|
|
12
12
|
{
|
|
13
13
|
path,
|
|
14
14
|
docsDir,
|
|
@@ -17,6 +17,7 @@ export default async function checkDetailGenerated(
|
|
|
17
17
|
structurePlan,
|
|
18
18
|
modifiedFiles,
|
|
19
19
|
lastGitHead,
|
|
20
|
+
forceRegenerate,
|
|
20
21
|
...rest
|
|
21
22
|
},
|
|
22
23
|
options
|
|
@@ -104,7 +105,8 @@ export default async function checkDetailGenerated(
|
|
|
104
105
|
detailGenerated &&
|
|
105
106
|
!sourceIdsChanged &&
|
|
106
107
|
!sourceFilesChanged &&
|
|
107
|
-
!contentValidationFailed
|
|
108
|
+
!contentValidationFailed &&
|
|
109
|
+
forceRegenerate !== "true"
|
|
108
110
|
) {
|
|
109
111
|
return {
|
|
110
112
|
path,
|
|
@@ -115,8 +117,8 @@ export default async function checkDetailGenerated(
|
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
const teamAgent = TeamAgent.from({
|
|
118
|
-
name: "
|
|
119
|
-
skills: [options.context.agents["
|
|
120
|
+
name: "generateDetail",
|
|
121
|
+
skills: [options.context.agents["detailGeneratorAndTranslate"]],
|
|
120
122
|
});
|
|
121
123
|
|
|
122
124
|
const result = await options.context.invoke(teamAgent, {
|
|
@@ -3,8 +3,8 @@ import {
|
|
|
3
3
|
hasFileChangesBetweenCommits,
|
|
4
4
|
} from "../utils/utils.mjs";
|
|
5
5
|
|
|
6
|
-
export default async function
|
|
7
|
-
{ originalStructurePlan, feedback, lastGitHead, ...rest },
|
|
6
|
+
export default async function checkStructurePlan(
|
|
7
|
+
{ originalStructurePlan, feedback, lastGitHead, forceRegenerate, ...rest },
|
|
8
8
|
options
|
|
9
9
|
) {
|
|
10
10
|
// Check if we need to regenerate structure plan
|
|
@@ -43,13 +43,18 @@ export default async function checkStructurePlanning(
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// If no regeneration needed, return original structure plan
|
|
46
|
-
if (
|
|
46
|
+
if (
|
|
47
|
+
originalStructurePlan &&
|
|
48
|
+
!feedback &&
|
|
49
|
+
!shouldRegenerate &&
|
|
50
|
+
forceRegenerate !== "true"
|
|
51
|
+
) {
|
|
47
52
|
return {
|
|
48
53
|
structurePlan: originalStructurePlan,
|
|
49
54
|
};
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
const panningAgent = options.context.agents["
|
|
57
|
+
const panningAgent = options.context.agents["reflectiveStructurePlanner"];
|
|
53
58
|
|
|
54
59
|
const result = await options.context.invoke(panningAgent, {
|
|
55
60
|
feedback: finalFeedback || "",
|
|
@@ -10,16 +10,15 @@ skills:
|
|
|
10
10
|
skipIfExists: true
|
|
11
11
|
- ./load-config.mjs
|
|
12
12
|
- ./load-sources.mjs
|
|
13
|
-
- ./check-structure-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}])
|
|
21
|
-
- ./save-output.mjs
|
|
13
|
+
- ./check-structure-plan.mjs
|
|
14
|
+
- url: ./save-output.mjs
|
|
15
|
+
default_input:
|
|
16
|
+
saveKey: structurePlan
|
|
17
|
+
savePath:
|
|
18
|
+
$get: outputDir
|
|
19
|
+
fileName: structure-plan.json
|
|
22
20
|
- type: transform
|
|
21
|
+
name: transformData
|
|
23
22
|
jsonata: |
|
|
24
23
|
$merge([
|
|
25
24
|
$,
|
|
@@ -103,6 +102,9 @@ input_schema:
|
|
|
103
102
|
feedback:
|
|
104
103
|
type: string
|
|
105
104
|
description: Feedback for structure planning adjustments
|
|
105
|
+
forceRegenerate:
|
|
106
|
+
type: string
|
|
107
|
+
description: Force regenerate the documentation
|
|
106
108
|
# labels:
|
|
107
109
|
# type: array
|
|
108
110
|
# items:
|
|
@@ -1,10 +1,89 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
}
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export default async function findItemByPath(
|
|
5
|
+
{ "doc-path": docPath, structurePlanResult, boardId, docsDir },
|
|
6
|
+
options
|
|
7
|
+
) {
|
|
6
8
|
let foundItem = null;
|
|
7
9
|
|
|
10
|
+
// If docPath is empty, let user select from available documents
|
|
11
|
+
if (!docPath) {
|
|
12
|
+
try {
|
|
13
|
+
// Get all .md files in docsDir
|
|
14
|
+
const files = await readdir(docsDir);
|
|
15
|
+
|
|
16
|
+
// Filter for main language .md files (exclude _sidebar.md and language-specific files)
|
|
17
|
+
const mainLanguageFiles = files.filter(
|
|
18
|
+
(file) =>
|
|
19
|
+
file.endsWith(".md") &&
|
|
20
|
+
file !== "_sidebar.md" &&
|
|
21
|
+
!file.match(/\.\w+(-\w+)?\.md$/) // Exclude language-specific files like .en.md, .zh-CN.md, etc.
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
if (mainLanguageFiles.length === 0) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
"Please provide a doc-path parameter to specify which document to update"
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Let user select a file
|
|
31
|
+
const selectedFile = await options.prompts.search({
|
|
32
|
+
message: "Select a document to update:",
|
|
33
|
+
source: async (input, { signal }) => {
|
|
34
|
+
if (!input || input.trim() === "") {
|
|
35
|
+
return mainLanguageFiles.map((file) => ({
|
|
36
|
+
name: file,
|
|
37
|
+
value: file,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const searchTerm = input.trim().toLowerCase();
|
|
42
|
+
const filteredFiles = mainLanguageFiles.filter((file) =>
|
|
43
|
+
file.toLowerCase().includes(searchTerm)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return filteredFiles.map((file) => ({
|
|
47
|
+
name: file,
|
|
48
|
+
value: file,
|
|
49
|
+
}));
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!selectedFile) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
"Please provide a doc-path parameter to specify which document to update"
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Convert filename back to path
|
|
60
|
+
// Remove .md extension
|
|
61
|
+
const flatName = selectedFile.replace(/\.md$/, "");
|
|
62
|
+
|
|
63
|
+
// Try to find matching item by comparing flattened paths
|
|
64
|
+
let foundItemByFile = null;
|
|
65
|
+
|
|
66
|
+
// First try without boardId prefix
|
|
67
|
+
foundItemByFile = structurePlanResult.find((item) => {
|
|
68
|
+
const itemFlattenedPath = item.path
|
|
69
|
+
.replace(/^\//, "")
|
|
70
|
+
.replace(/\//g, "-");
|
|
71
|
+
return itemFlattenedPath === flatName;
|
|
72
|
+
});
|
|
73
|
+
if (!foundItemByFile) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
"Please provide a doc-path parameter to specify which document to update"
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
docPath = foundItemByFile.path;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
"Please provide a doc-path parameter to specify which document to update"
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
8
87
|
// First try direct path matching
|
|
9
88
|
foundItem = structurePlanResult.find((item) => item.path === docPath);
|
|
10
89
|
|
|
@@ -1,37 +1,15 @@
|
|
|
1
1
|
import { writeFile, mkdir, readFile } from "node:fs/promises";
|
|
2
2
|
import { join, dirname } from "node:path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { validatePath, getAvailablePaths } from "../utils/utils.mjs";
|
|
5
|
+
import {
|
|
6
|
+
SUPPORTED_LANGUAGES,
|
|
7
|
+
DOCUMENT_STYLES,
|
|
8
|
+
TARGET_AUDIENCES,
|
|
9
|
+
} from "../utils/constants.mjs";
|
|
3
10
|
|
|
4
|
-
//
|
|
5
|
-
const
|
|
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
|
-
};
|
|
11
|
+
// UI constants
|
|
12
|
+
const PRESS_ENTER_TO_FINISH = "Press Enter to finish";
|
|
35
13
|
|
|
36
14
|
/**
|
|
37
15
|
* Guide users through multi-turn dialogue to collect information and generate YAML configuration
|
|
@@ -41,17 +19,21 @@ const TARGET_AUDIENCES = {
|
|
|
41
19
|
* @returns {Promise<Object>}
|
|
42
20
|
*/
|
|
43
21
|
export default async function init(
|
|
44
|
-
{
|
|
22
|
+
{
|
|
23
|
+
outputPath = "./doc-smith",
|
|
24
|
+
fileName = "config.yaml",
|
|
25
|
+
skipIfExists = false,
|
|
26
|
+
},
|
|
45
27
|
options
|
|
46
28
|
) {
|
|
47
29
|
if (skipIfExists) {
|
|
48
30
|
const filePath = join(outputPath, fileName);
|
|
49
31
|
if (await readFile(filePath, "utf8").catch(() => null)) {
|
|
50
|
-
return {}
|
|
32
|
+
return {};
|
|
51
33
|
}
|
|
52
34
|
}
|
|
53
35
|
|
|
54
|
-
console.log("🚀 Welcome to AIGNE
|
|
36
|
+
console.log("🚀 Welcome to AIGNE DocSmith!");
|
|
55
37
|
console.log("Let's create your documentation configuration.\n");
|
|
56
38
|
|
|
57
39
|
// Collect user information
|
|
@@ -78,7 +60,6 @@ export default async function init(
|
|
|
78
60
|
} else {
|
|
79
61
|
// Use predefined style directly
|
|
80
62
|
rules = DOCUMENT_STYLES[styleChoice].rules;
|
|
81
|
-
console.log(`✅ Selected: ${DOCUMENT_STYLES[styleChoice].name}`);
|
|
82
63
|
}
|
|
83
64
|
|
|
84
65
|
input.rules = rules.trim();
|
|
@@ -104,58 +85,105 @@ export default async function init(
|
|
|
104
85
|
} else {
|
|
105
86
|
// Use predefined audience directly
|
|
106
87
|
targetAudience = TARGET_AUDIENCES[audienceChoice];
|
|
107
|
-
console.log(`✅ Selected: ${TARGET_AUDIENCES[audienceChoice]}`);
|
|
108
88
|
}
|
|
109
89
|
|
|
110
90
|
input.targetAudience = targetAudience.trim();
|
|
111
91
|
|
|
112
92
|
// 3. Language settings
|
|
113
93
|
console.log("\n🌐 Step 3/6: Primary Language");
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
94
|
+
|
|
95
|
+
// Let user select primary language from supported list
|
|
96
|
+
const primaryLanguageChoice = await options.prompts.select({
|
|
97
|
+
message: "Choose primary documentation language:",
|
|
98
|
+
choices: SUPPORTED_LANGUAGES.map((lang) => ({
|
|
99
|
+
name: `${lang.label} - ${lang.sample}`,
|
|
100
|
+
value: lang.code,
|
|
101
|
+
})),
|
|
117
102
|
});
|
|
118
|
-
|
|
103
|
+
|
|
104
|
+
input.locale = primaryLanguageChoice;
|
|
119
105
|
|
|
120
106
|
// 4. Translation languages
|
|
121
107
|
console.log("\n🔄 Step 4/6: Translation Languages");
|
|
122
|
-
|
|
123
|
-
|
|
108
|
+
|
|
109
|
+
// Filter out the primary language from available choices
|
|
110
|
+
const availableTranslationLanguages = SUPPORTED_LANGUAGES.filter(
|
|
111
|
+
(lang) => lang.code !== primaryLanguageChoice
|
|
124
112
|
);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
input.translateLanguages = translateLanguages;
|
|
113
|
+
|
|
114
|
+
const translateLanguageChoices = await options.prompts.checkbox({
|
|
115
|
+
message: "Select translation languages:",
|
|
116
|
+
choices: availableTranslationLanguages.map((lang) => ({
|
|
117
|
+
name: `${lang.label} - ${lang.sample}`,
|
|
118
|
+
value: lang.code,
|
|
119
|
+
})),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
input.translateLanguages = translateLanguageChoices;
|
|
136
123
|
|
|
137
124
|
// 5. Documentation directory
|
|
138
125
|
console.log("\n📁 Step 5/6: Output Directory");
|
|
139
126
|
const docsDirInput = await options.prompts.input({
|
|
140
|
-
message: `Where to save generated docs
|
|
127
|
+
message: `Where to save generated docs:`,
|
|
128
|
+
default: `${outputPath}/docs`,
|
|
141
129
|
});
|
|
142
130
|
input.docsDir = docsDirInput.trim() || `${outputPath}/docs`;
|
|
143
131
|
|
|
144
132
|
// 6. Source code paths
|
|
145
133
|
console.log("\n🔍 Step 6/6: Source Code Paths");
|
|
146
|
-
console.log(
|
|
147
|
-
|
|
148
|
-
);
|
|
134
|
+
console.log("Enter paths to analyze for documentation (e.g., ./src, ./lib)");
|
|
135
|
+
console.log("💡 If no paths are configured, './' will be used as default");
|
|
149
136
|
|
|
150
137
|
const sourcePaths = [];
|
|
151
138
|
while (true) {
|
|
152
|
-
const
|
|
153
|
-
message:
|
|
139
|
+
const selectedPath = await options.prompts.search({
|
|
140
|
+
message: "Path:",
|
|
141
|
+
source: async (input, { signal }) => {
|
|
142
|
+
if (!input || input.trim() === "") {
|
|
143
|
+
return [
|
|
144
|
+
{
|
|
145
|
+
name: "Press Enter to finish",
|
|
146
|
+
value: "",
|
|
147
|
+
description: "",
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const searchTerm = input.trim();
|
|
153
|
+
|
|
154
|
+
// Search for matching files and folders in current directory
|
|
155
|
+
const availablePaths = getAvailablePaths(searchTerm);
|
|
156
|
+
|
|
157
|
+
return [...availablePaths];
|
|
158
|
+
},
|
|
154
159
|
});
|
|
155
|
-
|
|
160
|
+
|
|
161
|
+
// Check if user chose to exit
|
|
162
|
+
if (
|
|
163
|
+
!selectedPath ||
|
|
164
|
+
selectedPath.trim() === "" ||
|
|
165
|
+
selectedPath === "Press Enter to finish"
|
|
166
|
+
) {
|
|
156
167
|
break;
|
|
157
168
|
}
|
|
158
|
-
|
|
169
|
+
|
|
170
|
+
const trimmedPath = selectedPath.trim();
|
|
171
|
+
|
|
172
|
+
// Use validatePath to check if path is valid
|
|
173
|
+
const validation = validatePath(trimmedPath);
|
|
174
|
+
|
|
175
|
+
if (!validation.isValid) {
|
|
176
|
+
console.log(`⚠️ ${validation.error}`);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Avoid duplicate paths
|
|
181
|
+
if (sourcePaths.includes(trimmedPath)) {
|
|
182
|
+
console.log(`⚠️ Path already exists: ${trimmedPath}`);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
sourcePaths.push(trimmedPath);
|
|
159
187
|
}
|
|
160
188
|
|
|
161
189
|
// If no paths entered, use default
|
|
@@ -173,18 +201,17 @@ export default async function init(
|
|
|
173
201
|
await mkdir(dirPath, { recursive: true });
|
|
174
202
|
|
|
175
203
|
await writeFile(filePath, yamlContent, "utf8");
|
|
176
|
-
console.log(`\n🎉 Configuration saved to: ${filePath}`);
|
|
204
|
+
console.log(`\n🎉 Configuration saved to: ${chalk.cyan(filePath)}`);
|
|
177
205
|
console.log(
|
|
178
206
|
"💡 You can edit the configuration file anytime to modify settings."
|
|
179
207
|
);
|
|
180
208
|
console.log(
|
|
181
|
-
|
|
209
|
+
`🚀 Run ${chalk.cyan(
|
|
210
|
+
"'aigne doc generate'"
|
|
211
|
+
)} to start documentation generation!`
|
|
182
212
|
);
|
|
183
213
|
|
|
184
|
-
return {
|
|
185
|
-
inputGeneratorStatus: true,
|
|
186
|
-
inputGeneratorPath: filePath,
|
|
187
|
-
};
|
|
214
|
+
return {};
|
|
188
215
|
} catch (error) {
|
|
189
216
|
console.error(`❌ Failed to save configuration file: ${error.message}`);
|
|
190
217
|
return {
|
package/agents/publish-docs.mjs
CHANGED
|
@@ -15,10 +15,47 @@ const WELLKNOWN_SERVICE_PATH_PREFIX = "/.well-known/service";
|
|
|
15
15
|
const DEFAULT_APP_URL = "https://docsmith.aigne.io";
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Get
|
|
19
|
-
* @
|
|
18
|
+
* Get GitHub repository information
|
|
19
|
+
* @param {string} repoUrl - The repository URL
|
|
20
|
+
* @returns {Promise<Object>} - Repository information
|
|
20
21
|
*/
|
|
21
|
-
function
|
|
22
|
+
async function getGitHubRepoInfo(repoUrl) {
|
|
23
|
+
try {
|
|
24
|
+
// Extract owner and repo from GitHub URL
|
|
25
|
+
const match = repoUrl.match(
|
|
26
|
+
/github\.com[\/:]([^\/]+)\/([^\/]+?)(?:\.git)?$/
|
|
27
|
+
);
|
|
28
|
+
if (!match) return null;
|
|
29
|
+
|
|
30
|
+
const [, owner, repo] = match;
|
|
31
|
+
const apiUrl = `https://api.github.com/repos/${owner}/${repo}`;
|
|
32
|
+
|
|
33
|
+
const response = await fetch(apiUrl);
|
|
34
|
+
if (!response.ok) return null;
|
|
35
|
+
|
|
36
|
+
const data = await response.json();
|
|
37
|
+
return {
|
|
38
|
+
name: data.name,
|
|
39
|
+
description: data.description || "",
|
|
40
|
+
icon: data.owner?.avatar_url || "",
|
|
41
|
+
};
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.warn("Failed to fetch GitHub repository info:", error.message);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get project information with user confirmation
|
|
50
|
+
* @param {Object} options - Options object containing prompts
|
|
51
|
+
* @returns {Promise<Object>} - Project information including name, description, and icon
|
|
52
|
+
*/
|
|
53
|
+
async function getProjectInfo(options) {
|
|
54
|
+
let repoInfo = null;
|
|
55
|
+
let defaultName = basename(process.cwd());
|
|
56
|
+
let defaultDescription = "";
|
|
57
|
+
let defaultIcon = "";
|
|
58
|
+
|
|
22
59
|
// Check if we're in a git repository
|
|
23
60
|
try {
|
|
24
61
|
const gitRemote = execSync("git remote get-url origin", {
|
|
@@ -28,11 +65,59 @@ function getProjectName() {
|
|
|
28
65
|
|
|
29
66
|
// Extract repository name from git remote URL
|
|
30
67
|
const repoName = gitRemote.split("/").pop().replace(".git", "");
|
|
31
|
-
|
|
68
|
+
defaultName = repoName;
|
|
69
|
+
|
|
70
|
+
// If it's a GitHub repository, try to get additional info
|
|
71
|
+
if (gitRemote.includes("github.com")) {
|
|
72
|
+
repoInfo = await getGitHubRepoInfo(gitRemote);
|
|
73
|
+
if (repoInfo) {
|
|
74
|
+
defaultDescription = repoInfo.description;
|
|
75
|
+
defaultIcon = repoInfo.icon;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
32
78
|
} catch (error) {
|
|
33
79
|
// Not in git repository or no origin remote, use current directory name
|
|
34
|
-
|
|
80
|
+
console.warn("No git repository found, using current directory name");
|
|
35
81
|
}
|
|
82
|
+
|
|
83
|
+
// Prompt user for project information
|
|
84
|
+
console.log("\n📋 Project Information for Documentation Platform");
|
|
85
|
+
|
|
86
|
+
const projectName = await options.prompts.input({
|
|
87
|
+
message: "Project name:",
|
|
88
|
+
default: defaultName,
|
|
89
|
+
validate: (input) => {
|
|
90
|
+
if (!input || input.trim() === "") {
|
|
91
|
+
return "Project name cannot be empty";
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const projectDescription = await options.prompts.input({
|
|
98
|
+
message: "Project description (optional):",
|
|
99
|
+
default: defaultDescription,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const projectIcon = await options.prompts.input({
|
|
103
|
+
message: "Project icon URL (optional):",
|
|
104
|
+
default: defaultIcon,
|
|
105
|
+
validate: (input) => {
|
|
106
|
+
if (!input || input.trim() === "") return true;
|
|
107
|
+
try {
|
|
108
|
+
new URL(input);
|
|
109
|
+
return true;
|
|
110
|
+
} catch {
|
|
111
|
+
return "Please enter a valid URL";
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
name: projectName.trim(),
|
|
118
|
+
description: projectDescription.trim(),
|
|
119
|
+
icon: projectIcon.trim(),
|
|
120
|
+
};
|
|
36
121
|
}
|
|
37
122
|
|
|
38
123
|
/**
|
|
@@ -79,8 +164,9 @@ async function getAccessToken(appUrl) {
|
|
|
79
164
|
const result = await createConnect({
|
|
80
165
|
connectUrl: connectUrl,
|
|
81
166
|
connectAction: "gen-simple-access-key",
|
|
82
|
-
source:
|
|
167
|
+
source: `AIGNE DocSmith connect to Discuss Kit`,
|
|
83
168
|
closeOnSuccess: true,
|
|
169
|
+
appName: "AIGNE DocSmith",
|
|
84
170
|
openPage: (pageUrl) => open(pageUrl),
|
|
85
171
|
});
|
|
86
172
|
|
|
@@ -119,29 +205,33 @@ async function getAccessToken(appUrl) {
|
|
|
119
205
|
}
|
|
120
206
|
|
|
121
207
|
export default async function publishDocs(
|
|
122
|
-
{ docsDir, appUrl, boardId },
|
|
208
|
+
{ docsDir, appUrl, boardId, boardName, boardDesc, boardCover },
|
|
123
209
|
options
|
|
124
210
|
) {
|
|
125
|
-
// Check if
|
|
211
|
+
// Check if DOC_DISCUSS_KIT_URL is set in environment variables
|
|
212
|
+
const envAppUrl = process.env.DOC_DISCUSS_KIT_URL;
|
|
213
|
+
const useEnvAppUrl = !!envAppUrl;
|
|
214
|
+
|
|
215
|
+
// Use environment variable if available, otherwise use the provided appUrl
|
|
216
|
+
if (useEnvAppUrl) {
|
|
217
|
+
appUrl = envAppUrl;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check if appUrl is default and not saved in config (only when not using env variable)
|
|
126
221
|
const config = await loadConfigFromFile();
|
|
127
222
|
const isDefaultAppUrl = appUrl === DEFAULT_APP_URL;
|
|
128
223
|
const hasAppUrlInConfig = config && config.appUrl;
|
|
129
224
|
|
|
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
|
-
);
|
|
135
|
-
|
|
225
|
+
if (!useEnvAppUrl && isDefaultAppUrl && !hasAppUrlInConfig) {
|
|
136
226
|
const choice = await options.prompts.select({
|
|
137
|
-
message: "Select
|
|
227
|
+
message: "Select platform to publish your documents:",
|
|
138
228
|
choices: [
|
|
139
229
|
{
|
|
140
|
-
name: "
|
|
230
|
+
name: "Publish to docsmith.aigne.io - free, but your documents will be public accessible, recommended for open-source projects",
|
|
141
231
|
value: "default",
|
|
142
232
|
},
|
|
143
233
|
{
|
|
144
|
-
name: "
|
|
234
|
+
name: "Publish to your own website - you will need to run Discuss Kit by your self ",
|
|
145
235
|
value: "custom",
|
|
146
236
|
},
|
|
147
237
|
],
|
|
@@ -168,34 +258,48 @@ export default async function publishDocs(
|
|
|
168
258
|
|
|
169
259
|
const sidebarPath = join(docsDir, "_sidebar.md");
|
|
170
260
|
|
|
171
|
-
|
|
261
|
+
let projectInfo = {
|
|
262
|
+
name: boardName,
|
|
263
|
+
description: boardDesc,
|
|
264
|
+
icon: boardCover,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// Only get project info if we need to create a new board
|
|
268
|
+
if (!boardName) {
|
|
269
|
+
projectInfo = await getProjectInfo(options);
|
|
270
|
+
|
|
271
|
+
// save project info to config
|
|
272
|
+
await saveValueToConfig("boardName", projectInfo.name);
|
|
273
|
+
await saveValueToConfig("boardDesc", projectInfo.description);
|
|
274
|
+
await saveValueToConfig("boardCover", projectInfo.icon);
|
|
275
|
+
}
|
|
172
276
|
|
|
173
277
|
const { success, boardId: newBoardId } = await publishDocsFn({
|
|
174
278
|
sidebarPath,
|
|
175
279
|
accessToken,
|
|
176
280
|
appUrl,
|
|
177
281
|
boardId,
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
282
|
+
autoCreateBoard: true,
|
|
283
|
+
// Pass additional project information if available
|
|
284
|
+
boardName: projectInfo.name,
|
|
285
|
+
boardDesc: projectInfo.description,
|
|
286
|
+
boardCover: projectInfo.icon,
|
|
181
287
|
});
|
|
182
288
|
|
|
183
289
|
// Save values to config.yaml if publish was successful
|
|
184
290
|
if (success) {
|
|
185
|
-
// Save appUrl to config
|
|
186
|
-
|
|
291
|
+
// Save appUrl to config only when not using environment variable
|
|
292
|
+
if (!useEnvAppUrl) {
|
|
293
|
+
await saveValueToConfig("appUrl", appUrl);
|
|
294
|
+
}
|
|
187
295
|
|
|
188
296
|
// Save boardId to config if it was auto-created
|
|
189
|
-
if (
|
|
297
|
+
if (boardId !== newBoardId) {
|
|
190
298
|
await saveValueToConfig("boardId", newBoardId);
|
|
191
299
|
}
|
|
192
300
|
}
|
|
193
301
|
|
|
194
|
-
return {
|
|
195
|
-
publishResult: {
|
|
196
|
-
success,
|
|
197
|
-
},
|
|
198
|
-
};
|
|
302
|
+
return {};
|
|
199
303
|
}
|
|
200
304
|
|
|
201
305
|
publishDocs.input_schema = {
|
package/agents/save-docs.mjs
CHANGED
|
@@ -16,7 +16,6 @@ export default async function saveDocs({
|
|
|
16
16
|
locale,
|
|
17
17
|
}) {
|
|
18
18
|
const results = [];
|
|
19
|
-
|
|
20
19
|
// Save current git HEAD to config.yaml for change detection
|
|
21
20
|
try {
|
|
22
21
|
const gitHead = getCurrentGitHead();
|
|
@@ -30,25 +29,23 @@ export default async function saveDocs({
|
|
|
30
29
|
const sidebar = generateSidebar(structurePlan);
|
|
31
30
|
const sidebarPath = join(docsDir, "_sidebar.md");
|
|
32
31
|
await writeFile(sidebarPath, sidebar, "utf8");
|
|
33
|
-
results.push({ path: sidebarPath, success: true });
|
|
34
32
|
} catch (err) {
|
|
35
|
-
|
|
33
|
+
console.error("Failed to save _sidebar.md:", err.message);
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
// Clean up invalid .md files that are no longer in the structure plan
|
|
39
37
|
try {
|
|
40
|
-
|
|
38
|
+
await cleanupInvalidFiles(
|
|
41
39
|
structurePlan,
|
|
42
40
|
docsDir,
|
|
43
41
|
translateLanguages,
|
|
44
42
|
locale
|
|
45
43
|
);
|
|
46
|
-
results.push(...cleanupResults);
|
|
47
44
|
} catch (err) {
|
|
48
|
-
|
|
45
|
+
console.error("Failed to cleanup invalid .md files:", err.message);
|
|
49
46
|
}
|
|
50
47
|
|
|
51
|
-
return {
|
|
48
|
+
return {};
|
|
52
49
|
}
|
|
53
50
|
|
|
54
51
|
/**
|
package/aigne.yaml
CHANGED
|
@@ -12,12 +12,12 @@ agents:
|
|
|
12
12
|
- ./agents/save-docs.mjs
|
|
13
13
|
- ./agents/translate.yaml
|
|
14
14
|
- ./agents/detail-generator-and-translate.yaml
|
|
15
|
-
- ./agents/check-detail
|
|
15
|
+
- ./agents/check-detail.mjs
|
|
16
16
|
- ./agents/transform-detail-datasources.mjs
|
|
17
17
|
- ./agents/batch-translate.yaml
|
|
18
18
|
- ./agents/save-single-doc.mjs
|
|
19
19
|
- ./agents/save-output.mjs
|
|
20
|
-
- ./agents/check-structure-
|
|
20
|
+
- ./agents/check-structure-plan.mjs
|
|
21
21
|
- ./agents/content-detail-generator.yaml
|
|
22
22
|
- ./agents/reflective-structure-planner.yaml
|
|
23
23
|
- ./agents/check-structure-planning-result.yaml
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aigne/doc-smith",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -12,14 +12,16 @@
|
|
|
12
12
|
"author": "Arcblock <blocklet@arcblock.io> https://github.com/blocklet",
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@aigne/anthropic": "^0.10.
|
|
16
|
-
"@aigne/cli": "^1.
|
|
17
|
-
"@aigne/core": "^1.
|
|
18
|
-
"@aigne/gemini": "^0.8.
|
|
19
|
-
"@aigne/openai": "^0.10.
|
|
20
|
-
"@aigne/publish-docs": "^0.5.
|
|
15
|
+
"@aigne/anthropic": "^0.10.10",
|
|
16
|
+
"@aigne/cli": "^1.31.0",
|
|
17
|
+
"@aigne/core": "^1.46.0",
|
|
18
|
+
"@aigne/gemini": "^0.8.14",
|
|
19
|
+
"@aigne/openai": "^0.10.14",
|
|
20
|
+
"@aigne/publish-docs": "^0.5.3",
|
|
21
|
+
"chalk": "^5.5.0",
|
|
21
22
|
"glob": "^11.0.3",
|
|
22
23
|
"open": "^10.2.0",
|
|
24
|
+
"terminal-link": "^4.0.0",
|
|
23
25
|
"ufo": "^1.6.1",
|
|
24
26
|
"yaml": "^2.8.0"
|
|
25
27
|
},
|
package/utils/constants.mjs
CHANGED
|
@@ -58,3 +58,48 @@ export const DEFAULT_EXCLUDE_PATTERNS = [
|
|
|
58
58
|
"*.log",
|
|
59
59
|
"**/*test.*",
|
|
60
60
|
];
|
|
61
|
+
|
|
62
|
+
// Supported languages for documentation
|
|
63
|
+
export const SUPPORTED_LANGUAGES = [
|
|
64
|
+
{ code: "en", label: "English (en)", sample: "Hello" },
|
|
65
|
+
{ code: "zh-CN", label: "简体中文 (zh-CN)", sample: "你好" },
|
|
66
|
+
{ code: "zh-TW", label: "繁體中文 (zh-TW)", sample: "你好" },
|
|
67
|
+
{ code: "ja", label: "日本語 (ja)", sample: "こんにちは" },
|
|
68
|
+
{ code: "ko", label: "한국어 (ko)", sample: "안녕하세요" },
|
|
69
|
+
{ code: "es", label: "Español (es)", sample: "Hola" },
|
|
70
|
+
{ code: "fr", label: "Français (fr)", sample: "Bonjour" },
|
|
71
|
+
{ code: "de", label: "Deutsch (de)", sample: "Hallo" },
|
|
72
|
+
{ code: "pt-BR", label: "Português (pt-BR)", sample: "Olá" },
|
|
73
|
+
{ code: "ru", label: "Русский (ru)", sample: "Привет" },
|
|
74
|
+
{ code: "it", label: "Italiano (it)", sample: "Ciao" },
|
|
75
|
+
{ code: "ar", label: "العربية (ar)", sample: "مرحبا" },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
// Predefined document generation styles
|
|
79
|
+
export const DOCUMENT_STYLES = {
|
|
80
|
+
developerDocs: {
|
|
81
|
+
name: "Developer Docs",
|
|
82
|
+
rules: "Steps-first; copy-paste examples; minimal context; active 'you'.",
|
|
83
|
+
},
|
|
84
|
+
userGuide: {
|
|
85
|
+
name: "User Guide",
|
|
86
|
+
rules: "Scenario-based; step-by-step; plain language; outcomes & cautions.",
|
|
87
|
+
},
|
|
88
|
+
apiReference: {
|
|
89
|
+
name: "API Reference",
|
|
90
|
+
rules: "Exact & skimmable; schema-first; clear params/errors/examples.",
|
|
91
|
+
},
|
|
92
|
+
custom: {
|
|
93
|
+
name: "Custom Rules",
|
|
94
|
+
rules: "Enter your own documentation generation rules",
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Predefined target audiences
|
|
99
|
+
export const TARGET_AUDIENCES = {
|
|
100
|
+
actionFirst: "Developers, Implementation Engineers, DevOps",
|
|
101
|
+
conceptFirst:
|
|
102
|
+
"Architects, Technical Leads, Developers interested in principles",
|
|
103
|
+
generalUsers: "General Users",
|
|
104
|
+
custom: "Enter your own target audience",
|
|
105
|
+
};
|
package/utils/utils.mjs
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { execSync } from "node:child_process";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
accessSync,
|
|
9
|
+
constants,
|
|
10
|
+
statSync,
|
|
11
|
+
} from "node:fs";
|
|
5
12
|
import { parse } from "yaml";
|
|
13
|
+
import chalk from "chalk";
|
|
6
14
|
import {
|
|
7
15
|
DEFAULT_INCLUDE_PATTERNS,
|
|
8
16
|
DEFAULT_EXCLUDE_PATTERNS,
|
|
@@ -101,6 +109,7 @@ export async function saveDocWithTranslations({
|
|
|
101
109
|
|
|
102
110
|
await fs.writeFile(mainFilePath, finalContent, "utf8");
|
|
103
111
|
results.push({ path: mainFilePath, success: true });
|
|
112
|
+
console.log(chalk.green(`Saved: ${chalk.cyan(mainFilePath)}`));
|
|
104
113
|
|
|
105
114
|
// Process all translations
|
|
106
115
|
for (const translate of translates) {
|
|
@@ -118,6 +127,7 @@ export async function saveDocWithTranslations({
|
|
|
118
127
|
|
|
119
128
|
await fs.writeFile(translatePath, finalTranslationContent, "utf8");
|
|
120
129
|
results.push({ path: translatePath, success: true });
|
|
130
|
+
console.log(chalk.green(`Saved: ${chalk.cyan(translatePath)}`));
|
|
121
131
|
}
|
|
122
132
|
} catch (err) {
|
|
123
133
|
results.push({ path: docPath, success: false, error: err.message });
|
|
@@ -394,3 +404,238 @@ export async function saveValueToConfig(key, value) {
|
|
|
394
404
|
console.warn(`Failed to save ${key} to config.yaml:`, error.message);
|
|
395
405
|
}
|
|
396
406
|
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Validate if a path exists and is accessible
|
|
410
|
+
* @param {string} filePath - The path to validate (can be absolute or relative)
|
|
411
|
+
* @returns {Object} - Validation result with isValid boolean and error message
|
|
412
|
+
*/
|
|
413
|
+
export function validatePath(filePath) {
|
|
414
|
+
try {
|
|
415
|
+
const absolutePath = normalizePath(filePath);
|
|
416
|
+
|
|
417
|
+
// Check if path exists
|
|
418
|
+
if (!existsSync(absolutePath)) {
|
|
419
|
+
return {
|
|
420
|
+
isValid: false,
|
|
421
|
+
error: `Path does not exist: ${filePath}`,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Check if path is accessible (readable)
|
|
426
|
+
try {
|
|
427
|
+
accessSync(absolutePath, constants.R_OK);
|
|
428
|
+
} catch (accessError) {
|
|
429
|
+
return {
|
|
430
|
+
isValid: false,
|
|
431
|
+
error: `Path is not accessible: ${filePath}`,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
isValid: true,
|
|
437
|
+
error: null,
|
|
438
|
+
};
|
|
439
|
+
} catch (error) {
|
|
440
|
+
return {
|
|
441
|
+
isValid: false,
|
|
442
|
+
error: `Invalid path format: ${filePath}`,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Validate multiple paths and return validation results
|
|
449
|
+
* @param {Array<string>} paths - Array of paths to validate
|
|
450
|
+
* @returns {Object} - Validation results with validPaths array and errors array
|
|
451
|
+
*/
|
|
452
|
+
export function validatePaths(paths) {
|
|
453
|
+
const validPaths = [];
|
|
454
|
+
const errors = [];
|
|
455
|
+
|
|
456
|
+
for (const path of paths) {
|
|
457
|
+
const validation = validatePath(path);
|
|
458
|
+
if (validation.isValid) {
|
|
459
|
+
validPaths.push(path);
|
|
460
|
+
} else {
|
|
461
|
+
errors.push({
|
|
462
|
+
path: path,
|
|
463
|
+
error: validation.error,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
validPaths,
|
|
470
|
+
errors,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Check if input is a valid directory and add it to results if so
|
|
476
|
+
* @param {string} searchTerm - The search term to check
|
|
477
|
+
* @param {Array} results - The results array to modify
|
|
478
|
+
*/
|
|
479
|
+
function addExactDirectoryMatch(searchTerm, results) {
|
|
480
|
+
const inputValidation = validatePath(searchTerm);
|
|
481
|
+
if (inputValidation.isValid) {
|
|
482
|
+
const stats = statSync(normalizePath(searchTerm));
|
|
483
|
+
if (stats.isDirectory()) {
|
|
484
|
+
results.unshift({
|
|
485
|
+
name: searchTerm,
|
|
486
|
+
value: searchTerm,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Get available paths for search suggestions based on user input
|
|
494
|
+
* @param {string} userInput - User's input string
|
|
495
|
+
* @returns {Array<Object>} - Array of path objects with name, value, and description
|
|
496
|
+
*/
|
|
497
|
+
export function getAvailablePaths(userInput = "") {
|
|
498
|
+
try {
|
|
499
|
+
const searchTerm = userInput.trim();
|
|
500
|
+
|
|
501
|
+
// If no input, return current directory contents
|
|
502
|
+
if (!searchTerm) {
|
|
503
|
+
return getDirectoryContents("./");
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
let results = [];
|
|
507
|
+
|
|
508
|
+
// Handle absolute paths
|
|
509
|
+
if (searchTerm.startsWith("/")) {
|
|
510
|
+
const dirPath = path.dirname(searchTerm);
|
|
511
|
+
const fileName = path.basename(searchTerm);
|
|
512
|
+
results = getDirectoryContents(dirPath, fileName);
|
|
513
|
+
addExactDirectoryMatch(searchTerm, results);
|
|
514
|
+
}
|
|
515
|
+
// Handle relative paths
|
|
516
|
+
else if (searchTerm.startsWith("./") || searchTerm.startsWith("../")) {
|
|
517
|
+
// Extract directory path and search term
|
|
518
|
+
const lastSlashIndex = searchTerm.lastIndexOf("/");
|
|
519
|
+
if (lastSlashIndex === -1) {
|
|
520
|
+
// No slash found, treat as current directory search
|
|
521
|
+
results = getDirectoryContents("./", searchTerm);
|
|
522
|
+
addExactDirectoryMatch(searchTerm, results);
|
|
523
|
+
} else {
|
|
524
|
+
const dirPath = searchTerm.substring(0, lastSlashIndex + 1);
|
|
525
|
+
const fileName = searchTerm.substring(lastSlashIndex + 1);
|
|
526
|
+
|
|
527
|
+
// Validate directory path
|
|
528
|
+
const validation = validatePath(dirPath);
|
|
529
|
+
if (!validation.isValid) {
|
|
530
|
+
return [
|
|
531
|
+
{
|
|
532
|
+
name: dirPath,
|
|
533
|
+
value: dirPath,
|
|
534
|
+
description: validation.error,
|
|
535
|
+
},
|
|
536
|
+
];
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
results = getDirectoryContents(dirPath, fileName);
|
|
540
|
+
addExactDirectoryMatch(searchTerm, results);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// Handle simple file/directory names (search in current directory)
|
|
544
|
+
else {
|
|
545
|
+
results = getDirectoryContents("./", searchTerm);
|
|
546
|
+
addExactDirectoryMatch(searchTerm, results);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Remove duplicates based on value (path)
|
|
550
|
+
const uniqueResults = [];
|
|
551
|
+
const seenPaths = new Set();
|
|
552
|
+
|
|
553
|
+
for (const item of results) {
|
|
554
|
+
if (!seenPaths.has(item.value)) {
|
|
555
|
+
seenPaths.add(item.value);
|
|
556
|
+
uniqueResults.push(item);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return uniqueResults;
|
|
561
|
+
} catch (error) {
|
|
562
|
+
console.warn(
|
|
563
|
+
`Failed to get available paths for "${userInput}":`,
|
|
564
|
+
error.message
|
|
565
|
+
);
|
|
566
|
+
return [];
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Get directory contents for a specific path
|
|
572
|
+
* @param {string} dirPath - Directory path to search in
|
|
573
|
+
* @param {string} searchTerm - Optional search term to filter results
|
|
574
|
+
* @returns {Array<Object>} - Array of path objects
|
|
575
|
+
*/
|
|
576
|
+
function getDirectoryContents(dirPath, searchTerm = "") {
|
|
577
|
+
try {
|
|
578
|
+
const absoluteDirPath = normalizePath(dirPath);
|
|
579
|
+
|
|
580
|
+
// Check if directory exists
|
|
581
|
+
if (!existsSync(absoluteDirPath)) {
|
|
582
|
+
return [
|
|
583
|
+
{
|
|
584
|
+
name: dirPath,
|
|
585
|
+
value: dirPath,
|
|
586
|
+
description: "Directory does not exist",
|
|
587
|
+
},
|
|
588
|
+
];
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const items = [];
|
|
592
|
+
|
|
593
|
+
// Read directory contents
|
|
594
|
+
const entries = readdirSync(absoluteDirPath, { withFileTypes: true });
|
|
595
|
+
|
|
596
|
+
for (const entry of entries) {
|
|
597
|
+
const entryName = entry.name;
|
|
598
|
+
const relativePath = path.join(dirPath, entryName);
|
|
599
|
+
|
|
600
|
+
// Filter by search term if provided
|
|
601
|
+
if (
|
|
602
|
+
searchTerm &&
|
|
603
|
+
!entryName.toLowerCase().includes(searchTerm.toLowerCase())
|
|
604
|
+
) {
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Skip hidden files and common ignore patterns
|
|
609
|
+
if (
|
|
610
|
+
entryName.startsWith(".") ||
|
|
611
|
+
entryName === "node_modules" ||
|
|
612
|
+
entryName === ".git" ||
|
|
613
|
+
entryName === "dist" ||
|
|
614
|
+
entryName === "build"
|
|
615
|
+
) {
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const isDirectory = entry.isDirectory();
|
|
620
|
+
|
|
621
|
+
// Only include directories, skip files
|
|
622
|
+
if (isDirectory) {
|
|
623
|
+
items.push({
|
|
624
|
+
name: relativePath,
|
|
625
|
+
value: relativePath,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Sort alphabetically (all items are directories now)
|
|
631
|
+
items.sort((a, b) => a.name.localeCompare(b.name));
|
|
632
|
+
|
|
633
|
+
return items;
|
|
634
|
+
} catch (error) {
|
|
635
|
+
console.warn(
|
|
636
|
+
`Failed to get directory contents from ${dirPath}:`,
|
|
637
|
+
error.message
|
|
638
|
+
);
|
|
639
|
+
return [];
|
|
640
|
+
}
|
|
641
|
+
}
|