@aigne/doc-smith 0.1.4 → 0.2.1

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.
@@ -1,5 +1,15 @@
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
+ 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";
10
+
11
+ // UI constants
12
+ const PRESS_ENTER_TO_FINISH = "Press Enter to finish";
3
13
 
4
14
  /**
5
15
  * Guide users through multi-turn dialogue to collect information and generate YAML configuration
@@ -9,48 +19,175 @@ import { join, dirname } from "node:path";
9
19
  * @returns {Promise<Object>}
10
20
  */
11
21
  export default async function init(
12
- { outputPath = "./doc-smith", fileName = "config.yaml" },
22
+ {
23
+ outputPath = "./doc-smith",
24
+ fileName = "config.yaml",
25
+ skipIfExists = false,
26
+ },
13
27
  options
14
28
  ) {
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
- );
29
+ if (skipIfExists) {
30
+ const filePath = join(outputPath, fileName);
31
+ if (await readFile(filePath, "utf8").catch(() => null)) {
32
+ return {};
33
+ }
34
+ }
35
+
36
+ console.log("šŸš€ Welcome to AIGNE DocSmith!");
37
+ console.log("Let's create your documentation configuration.\n");
19
38
 
20
39
  // Collect user information
21
40
  const input = {};
22
41
 
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:",
42
+ // 1. Document generation rules with style selection
43
+ console.log("šŸ“ Step 1/6: Document Generation Rules");
44
+
45
+ // Let user select a document style
46
+ const styleChoice = await options.prompts.select({
47
+ message: "Choose your documentation style:",
48
+ choices: Object.entries(DOCUMENT_STYLES).map(([key, style]) => ({
49
+ name: `${style.name} - ${style.rules}`,
50
+ value: key,
51
+ })),
27
52
  });
28
- input.rules = rulesInput.trim();
29
53
 
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'):",
54
+ let rules;
55
+ if (styleChoice === "custom") {
56
+ // User wants to input custom rules
57
+ rules = await options.prompts.input({
58
+ message: "Enter your custom documentation rules:",
59
+ });
60
+ } else {
61
+ // Use predefined style directly
62
+ rules = DOCUMENT_STYLES[styleChoice].rules;
63
+ }
64
+
65
+ input.rules = rules.trim();
66
+
67
+ // 2. Target audience selection
68
+ console.log("\nšŸ‘„ Step 2/6: Target Audience");
69
+
70
+ // Let user select target audience
71
+ const audienceChoice = await options.prompts.select({
72
+ message: "Who is your target audience?",
73
+ choices: Object.entries(TARGET_AUDIENCES).map(([key, audience]) => ({
74
+ name: audience,
75
+ value: key,
76
+ })),
35
77
  });
36
- input.targetAudience = targetAudienceInput.trim() || "developers";
78
+
79
+ let targetAudience;
80
+ if (audienceChoice === "custom") {
81
+ // User wants to input custom audience
82
+ targetAudience = await options.prompts.input({
83
+ message: "Enter your custom target audience:",
84
+ });
85
+ } else {
86
+ // Use predefined audience directly
87
+ targetAudience = TARGET_AUDIENCES[audienceChoice];
88
+ }
89
+
90
+ input.targetAudience = targetAudience.trim();
37
91
 
38
92
  // 3. Language settings
39
- console.log("\n=== Language Settings ===");
40
- const localeInput = await options.prompts.input({
41
- message: "Primary language (e.g., en, zh, press Enter for default 'en'):",
93
+ console.log("\n🌐 Step 3/6: Primary Language");
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
+ })),
42
102
  });
43
- input.locale = localeInput.trim() || "en";
103
+
104
+ input.locale = primaryLanguageChoice;
44
105
 
45
106
  // 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):",
107
+ console.log("\nšŸ”„ Step 4/6: Translation Languages");
108
+
109
+ // Filter out the primary language from available choices
110
+ const availableTranslationLanguages = SUPPORTED_LANGUAGES.filter(
111
+ (lang) => lang.code !== primaryLanguageChoice
112
+ );
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;
123
+
124
+ // 5. Documentation directory
125
+ console.log("\nšŸ“ Step 5/6: Output Directory");
126
+ const docsDirInput = await options.prompts.input({
127
+ message: `Where to save generated docs:`,
128
+ default: `${outputPath}/docs`,
50
129
  });
51
- input.translateLanguages = translateInput.trim()
52
- ? translateInput.split(",").map((lang) => lang.trim())
53
- : [];
130
+ input.docsDir = docsDirInput.trim() || `${outputPath}/docs`;
131
+
132
+ // 6. Source code paths
133
+ console.log("\nšŸ” Step 6/6: Source Code Paths");
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");
136
+
137
+ const sourcePaths = [];
138
+ while (true) {
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
+ },
159
+ });
160
+
161
+ // Check if user chose to exit
162
+ if (
163
+ !selectedPath ||
164
+ selectedPath.trim() === "" ||
165
+ selectedPath === "Press Enter to finish"
166
+ ) {
167
+ break;
168
+ }
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);
187
+ }
188
+
189
+ // If no paths entered, use default
190
+ input.sourcesPath = sourcePaths.length > 0 ? sourcePaths : ["./"];
54
191
 
55
192
  // Generate YAML content
56
193
  const yamlContent = generateYAML(input, outputPath);
@@ -64,13 +201,17 @@ export default async function init(
64
201
  await mkdir(dirPath, { recursive: true });
65
202
 
66
203
  await writeFile(filePath, yamlContent, "utf8");
67
- console.log(`\nāœ… Configuration file saved to: ${filePath}`);
204
+ console.log(`\nšŸŽ‰ Configuration saved to: ${chalk.cyan(filePath)}`);
205
+ console.log(
206
+ "šŸ’” You can edit the configuration file anytime to modify settings."
207
+ );
208
+ console.log(
209
+ `šŸš€ Run ${chalk.cyan(
210
+ "'aigne doc generate'"
211
+ )} to start documentation generation!`
212
+ );
68
213
 
69
- return {
70
- inputGeneratorStatus: true,
71
- inputGeneratorPath: filePath,
72
- inputGeneratorContent: yamlContent,
73
- };
214
+ return {};
74
215
  } catch (error) {
75
216
  console.error(`āŒ Failed to save configuration file: ${error.message}`);
76
217
  return {
@@ -121,11 +262,13 @@ function generateYAML(input, outputPath) {
121
262
  yaml += `# - en # Example: English translation\n`;
122
263
  }
123
264
 
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`;
265
+ // Add directory and source path configurations
266
+ yaml += `docsDir: ${input.docsDir} # Directory to save generated documentation\n`;
267
+ // yaml += `outputDir: ${outputPath}/output # Directory to save output files\n`;
127
268
  yaml += `sourcesPath: # Source code paths to analyze\n`;
128
- yaml += ` - ./ # Current directory\n`;
269
+ input.sourcesPath.forEach((path) => {
270
+ yaml += ` - ${path}\n`;
271
+ });
129
272
 
130
273
  return yaml;
131
274
  }
@@ -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
  };