@aigne/doc-smith 0.1.3 → 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 +15 -0
- package/README.md +3 -3
- 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 +7 -7
- 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
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
|
+
}
|