@aigne/doc-smith 0.9.8-alpha.3 → 0.9.8-alpha.4
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/CLAUDE.md +43 -0
- package/README.md +94 -250
- package/aigne.yaml +2 -149
- package/doc-smith/SKILL.md +117 -0
- package/doc-smith/references/changeset_schema.md +118 -0
- package/doc-smith/references/document_structure_schema.md +139 -0
- package/doc-smith/references/document_update_guide.md +193 -0
- package/doc-smith/references/structure_confirmation_guide.md +133 -0
- package/doc-smith/references/structure_planning_guide.md +146 -0
- package/doc-smith/references/user_intent_guide.md +172 -0
- package/doc-smith.yaml +114 -0
- package/main-system-prompt.md +56 -0
- package/package.json +3 -69
- package/scripts/README.md +90 -0
- package/scripts/install.sh +86 -0
- package/scripts/uninstall.sh +52 -0
- package/CHANGELOG.md +0 -994
- package/LICENSE +0 -93
- package/agentic-agents/common/base-info.md +0 -53
- package/agentic-agents/common/completer.md +0 -54
- package/agentic-agents/common/planner.md +0 -168
- package/agentic-agents/common/worker.md +0 -93
- package/agentic-agents/create/index.yaml +0 -129
- package/agentic-agents/create/objective.md +0 -44
- package/agentic-agents/create/set-custom-prompt.mjs +0 -27
- package/agentic-agents/detail/index.yaml +0 -95
- package/agentic-agents/detail/objective.md +0 -9
- package/agentic-agents/detail/set-custom-prompt.mjs +0 -88
- package/agentic-agents/predict-resources/index.yaml +0 -44
- package/agentic-agents/predict-resources/instructions.md +0 -61
- package/agentic-agents/structure/design-rules.md +0 -39
- package/agentic-agents/structure/index.yaml +0 -86
- package/agentic-agents/structure/objective.md +0 -14
- package/agentic-agents/structure/review-criteria.md +0 -55
- package/agentic-agents/structure/set-custom-prompt.mjs +0 -78
- package/agentic-agents/utils/init-workspace-cache.mjs +0 -171
- package/agentic-agents/utils/load-base-sources.mjs +0 -20
- package/agentic-agents/workspace-cache-sharing-design.md +0 -671
- package/agents/chat/chat-system.md +0 -38
- package/agents/chat/index.mjs +0 -59
- package/agents/chat/skills/generate-document.yaml +0 -15
- package/agents/chat/skills/list-documents.mjs +0 -15
- package/agents/chat/skills/update-document.yaml +0 -24
- package/agents/clear/choose-contents.mjs +0 -192
- package/agents/clear/clear-auth-tokens.mjs +0 -88
- package/agents/clear/clear-deployment-config.mjs +0 -49
- package/agents/clear/clear-document-config.mjs +0 -36
- package/agents/clear/clear-document-structure.mjs +0 -102
- package/agents/clear/clear-generated-docs.mjs +0 -142
- package/agents/clear/clear-media-description.mjs +0 -129
- package/agents/clear/index.yaml +0 -26
- package/agents/create/analyze-diagram-type-llm.yaml +0 -160
- package/agents/create/analyze-diagram-type.mjs +0 -297
- package/agents/create/check-document-structure.yaml +0 -30
- package/agents/create/check-need-generate-structure.mjs +0 -105
- package/agents/create/document-structure-tools/add-document.mjs +0 -85
- package/agents/create/document-structure-tools/delete-document.mjs +0 -116
- package/agents/create/document-structure-tools/move-document.mjs +0 -109
- package/agents/create/document-structure-tools/update-document.mjs +0 -84
- package/agents/create/generate-diagram-image.yaml +0 -60
- package/agents/create/generate-structure.yaml +0 -117
- package/agents/create/index.yaml +0 -49
- package/agents/create/refine-document-structure.yaml +0 -12
- package/agents/create/replace-d2-with-image.mjs +0 -625
- package/agents/create/update-document-structure.yaml +0 -54
- package/agents/create/user-add-document/add-documents-to-structure.mjs +0 -90
- package/agents/create/user-add-document/find-documents-to-add-links.yaml +0 -47
- package/agents/create/user-add-document/index.yaml +0 -46
- package/agents/create/user-add-document/prepare-documents-to-translate.mjs +0 -22
- package/agents/create/user-add-document/print-add-document-summary.mjs +0 -63
- package/agents/create/user-add-document/review-documents-with-new-links.mjs +0 -110
- package/agents/create/user-remove-document/find-documents-with-invalid-links.mjs +0 -78
- package/agents/create/user-remove-document/index.yaml +0 -40
- package/agents/create/user-remove-document/prepare-documents-to-translate.mjs +0 -22
- package/agents/create/user-remove-document/print-remove-document-summary.mjs +0 -53
- package/agents/create/user-remove-document/remove-documents-from-structure.mjs +0 -99
- package/agents/create/user-remove-document/review-documents-with-invalid-links.mjs +0 -115
- package/agents/create/user-review-document-structure.mjs +0 -140
- package/agents/create/utils/init-current-content.mjs +0 -34
- package/agents/create/utils/merge-document-structures.mjs +0 -30
- package/agents/evaluate/code-snippet.mjs +0 -97
- package/agents/evaluate/document-structure.yaml +0 -67
- package/agents/evaluate/document.yaml +0 -82
- package/agents/evaluate/generate-report.mjs +0 -85
- package/agents/evaluate/index.yaml +0 -46
- package/agents/history/index.yaml +0 -6
- package/agents/history/view.mjs +0 -78
- package/agents/init/check.mjs +0 -16
- package/agents/init/index.mjs +0 -275
- package/agents/init/validate.mjs +0 -16
- package/agents/localize/choose-language.mjs +0 -107
- package/agents/localize/index.yaml +0 -58
- package/agents/localize/record-translation-history.mjs +0 -23
- package/agents/localize/translate-document.yaml +0 -24
- package/agents/localize/translate-multilingual.yaml +0 -51
- package/agents/media/batch-generate-media-description.yaml +0 -46
- package/agents/media/generate-media-description.yaml +0 -50
- package/agents/media/load-media-description.mjs +0 -256
- package/agents/prefs/index.mjs +0 -203
- package/agents/publish/index.yaml +0 -26
- package/agents/publish/publish-docs.mjs +0 -356
- package/agents/publish/translate-meta.mjs +0 -103
- package/agents/schema/document-structure-item.yaml +0 -26
- package/agents/schema/document-structure-refine-item.yaml +0 -23
- package/agents/schema/document-structure.yaml +0 -29
- package/agents/update/batch-generate-document.yaml +0 -27
- package/agents/update/batch-update-document.yaml +0 -7
- package/agents/update/check-diagram-flag.mjs +0 -116
- package/agents/update/check-document.mjs +0 -162
- package/agents/update/check-generate-diagram.mjs +0 -106
- package/agents/update/check-sync-image-flag.mjs +0 -55
- package/agents/update/check-update-is-single.mjs +0 -53
- package/agents/update/document-tools/update-document-content.mjs +0 -303
- package/agents/update/generate-diagram.yaml +0 -63
- package/agents/update/generate-document.yaml +0 -70
- package/agents/update/handle-document-update.yaml +0 -103
- package/agents/update/index.yaml +0 -79
- package/agents/update/pre-check-generate-diagram.yaml +0 -44
- package/agents/update/save-and-translate-document.mjs +0 -76
- package/agents/update/sync-images-and-exit.mjs +0 -148
- package/agents/update/update-document-detail.yaml +0 -71
- package/agents/update/update-single/update-single-document-detail.mjs +0 -280
- package/agents/update/update-single-document.yaml +0 -7
- package/agents/update/user-review-document.mjs +0 -272
- package/agents/utils/action-success.mjs +0 -16
- package/agents/utils/analyze-document-feedback-intent.yaml +0 -32
- package/agents/utils/analyze-feedback-intent.mjs +0 -136
- package/agents/utils/analyze-structure-feedback-intent.yaml +0 -29
- package/agents/utils/check-detail-result.mjs +0 -38
- package/agents/utils/check-feedback-refiner.mjs +0 -81
- package/agents/utils/choose-docs.mjs +0 -293
- package/agents/utils/document-icon-generate.yaml +0 -52
- package/agents/utils/document-title-streamline.yaml +0 -48
- package/agents/utils/ensure-document-icons.mjs +0 -129
- package/agents/utils/exit.mjs +0 -6
- package/agents/utils/feedback-refiner.yaml +0 -50
- package/agents/utils/find-item-by-path.mjs +0 -114
- package/agents/utils/find-user-preferences-by-path.mjs +0 -37
- package/agents/utils/format-document-structure.mjs +0 -35
- package/agents/utils/generate-document-or-skip.mjs +0 -41
- package/agents/utils/handle-diagram-operations.mjs +0 -263
- package/agents/utils/load-all-document-content.mjs +0 -30
- package/agents/utils/load-document-all-content.mjs +0 -84
- package/agents/utils/load-sources.mjs +0 -405
- package/agents/utils/map-reasoning-effort-level.mjs +0 -15
- package/agents/utils/post-generate.mjs +0 -144
- package/agents/utils/read-current-document-content.mjs +0 -46
- package/agents/utils/save-doc-translation.mjs +0 -61
- package/agents/utils/save-doc.mjs +0 -88
- package/agents/utils/save-output.mjs +0 -26
- package/agents/utils/save-sidebar.mjs +0 -51
- package/agents/utils/skip-if-content-exists.mjs +0 -27
- package/agents/utils/streamline-document-titles-if-needed.mjs +0 -88
- package/agents/utils/transform-detail-data-sources.mjs +0 -45
- package/agents/utils/update-branding.mjs +0 -84
- package/assets/report-template/report.html +0 -198
- package/docs-mcp/analyze-content-relevance.yaml +0 -50
- package/docs-mcp/analyze-docs-relevance.yaml +0 -59
- package/docs-mcp/docs-search.yaml +0 -42
- package/docs-mcp/get-docs-detail.mjs +0 -41
- package/docs-mcp/get-docs-structure.mjs +0 -16
- package/docs-mcp/read-doc-content.mjs +0 -119
- package/prompts/common/document/content-rules-core.md +0 -20
- package/prompts/common/document/markdown-syntax-rules.md +0 -65
- package/prompts/common/document/media-file-list-usage-rules.md +0 -18
- package/prompts/common/document/openapi-usage-rules.md +0 -189
- package/prompts/common/document/role-and-personality.md +0 -16
- package/prompts/common/document/user-preferences.md +0 -9
- package/prompts/common/document-structure/conflict-resolution-guidance.md +0 -16
- package/prompts/common/document-structure/document-icon-generate.md +0 -116
- package/prompts/common/document-structure/document-structure-rules.md +0 -43
- package/prompts/common/document-structure/document-title-streamline.md +0 -86
- package/prompts/common/document-structure/glossary.md +0 -7
- package/prompts/common/document-structure/intj-traits.md +0 -5
- package/prompts/common/document-structure/openapi-usage-rules.md +0 -28
- package/prompts/common/document-structure/output-constraints.md +0 -18
- package/prompts/common/document-structure/user-locale-rules.md +0 -10
- package/prompts/common/document-structure/user-preferences.md +0 -9
- package/prompts/detail/custom/admonition-usage-rules.md +0 -94
- package/prompts/detail/custom/code-block-usage-rules.md +0 -163
- package/prompts/detail/custom/custom-components/x-card-usage-rules.md +0 -63
- package/prompts/detail/custom/custom-components/x-cards-usage-rules.md +0 -83
- package/prompts/detail/custom/custom-components/x-field-desc-usage-rules.md +0 -120
- package/prompts/detail/custom/custom-components/x-field-group-usage-rules.md +0 -80
- package/prompts/detail/custom/custom-components/x-field-usage-rules.md +0 -189
- package/prompts/detail/custom/custom-components-usage-rules.md +0 -18
- package/prompts/detail/diagram/generate-image-system.md +0 -135
- package/prompts/detail/diagram/generate-image-user.md +0 -32
- package/prompts/detail/diagram/guide.md +0 -29
- package/prompts/detail/diagram/official-examples.md +0 -712
- package/prompts/detail/diagram/pre-check.md +0 -23
- package/prompts/detail/diagram/role-and-personality.md +0 -2
- package/prompts/detail/diagram/rules.md +0 -46
- package/prompts/detail/diagram/system-prompt.md +0 -1139
- package/prompts/detail/diagram/user-prompt.md +0 -43
- package/prompts/detail/generate/detail-example.md +0 -457
- package/prompts/detail/generate/document-rules.md +0 -45
- package/prompts/detail/generate/system-prompt.md +0 -61
- package/prompts/detail/generate/user-prompt.md +0 -99
- package/prompts/detail/jsx/rules.md +0 -6
- package/prompts/detail/update/system-prompt.md +0 -121
- package/prompts/detail/update/user-prompt.md +0 -41
- package/prompts/evaluate/document-structure.md +0 -93
- package/prompts/evaluate/document.md +0 -149
- package/prompts/media/media-description/system-prompt.md +0 -43
- package/prompts/media/media-description/user-prompt.md +0 -17
- package/prompts/structure/check-document-structure.md +0 -93
- package/prompts/structure/document-rules.md +0 -21
- package/prompts/structure/find-documents-to-add-links.md +0 -52
- package/prompts/structure/generate/system-prompt.md +0 -13
- package/prompts/structure/generate/user-prompt.md +0 -137
- package/prompts/structure/review/structure-review-system.md +0 -81
- package/prompts/structure/structure-example.md +0 -89
- package/prompts/structure/structure-getting-started.md +0 -10
- package/prompts/structure/update/system-prompt.md +0 -93
- package/prompts/structure/update/user-prompt.md +0 -43
- package/prompts/translate/admonition.md +0 -20
- package/prompts/translate/code-block.md +0 -33
- package/prompts/translate/glossary.md +0 -6
- package/prompts/translate/translate-document.md +0 -305
- package/prompts/utils/analyze-document-feedback-intent.md +0 -54
- package/prompts/utils/analyze-structure-feedback-intent.md +0 -43
- package/prompts/utils/feedback-refiner.md +0 -105
- package/types/document-schema.mjs +0 -55
- package/types/document-structure-schema.mjs +0 -261
- package/utils/auth-utils.mjs +0 -275
- package/utils/blocklet.mjs +0 -104
- package/utils/check-document-has-diagram.mjs +0 -95
- package/utils/conflict-detector.mjs +0 -149
- package/utils/constants/index.mjs +0 -620
- package/utils/constants/linter.mjs +0 -102
- package/utils/d2-utils.mjs +0 -198
- package/utils/debug.mjs +0 -3
- package/utils/delete-diagram-images.mjs +0 -99
- package/utils/deploy.mjs +0 -86
- package/utils/docs-finder-utils.mjs +0 -623
- package/utils/evaluate/report-utils.mjs +0 -132
- package/utils/extract-api.mjs +0 -32
- package/utils/file-utils.mjs +0 -960
- package/utils/history-utils.mjs +0 -203
- package/utils/icon-map.mjs +0 -26
- package/utils/image-compress.mjs +0 -75
- package/utils/kroki-utils.mjs +0 -173
- package/utils/linter/index.mjs +0 -50
- package/utils/load-config.mjs +0 -107
- package/utils/markdown/index.mjs +0 -26
- package/utils/markdown-checker.mjs +0 -694
- package/utils/mermaid-validator.mjs +0 -140
- package/utils/mermaid-worker-pool.mjs +0 -250
- package/utils/mermaid-worker.mjs +0 -233
- package/utils/openapi/index.mjs +0 -28
- package/utils/preferences-utils.mjs +0 -175
- package/utils/request.mjs +0 -10
- package/utils/store/index.mjs +0 -45
- package/utils/sync-diagram-to-translations.mjs +0 -262
- package/utils/upload-files.mjs +0 -231
- package/utils/utils.mjs +0 -1354
package/utils/file-utils.mjs
DELETED
|
@@ -1,960 +0,0 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
|
-
import { randomBytes } from "node:crypto";
|
|
3
|
-
import { access, readFile, stat } from "node:fs/promises";
|
|
4
|
-
import path, { join } from "node:path";
|
|
5
|
-
import { glob } from "glob";
|
|
6
|
-
import fs from "fs-extra";
|
|
7
|
-
import { isBinaryFile } from "isbinaryfile";
|
|
8
|
-
import { encode } from "gpt-tokenizer";
|
|
9
|
-
import { fileTypeFromBuffer } from "file-type";
|
|
10
|
-
import { gunzipSync } from "node:zlib";
|
|
11
|
-
|
|
12
|
-
import { debug } from "./debug.mjs";
|
|
13
|
-
import { isGlobPattern } from "./utils.mjs";
|
|
14
|
-
import { uploadFiles } from "./upload-files.mjs";
|
|
15
|
-
import { extractApi } from "./extract-api.mjs";
|
|
16
|
-
import { minimatch } from "minimatch";
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Check if a directory is inside a git repository using git command
|
|
20
|
-
* @param {string} dir - Directory path to check
|
|
21
|
-
* @returns {boolean} True if inside a git repository
|
|
22
|
-
*/
|
|
23
|
-
export function isInGitRepository(dir) {
|
|
24
|
-
try {
|
|
25
|
-
execSync("git rev-parse --is-inside-work-tree", {
|
|
26
|
-
cwd: dir,
|
|
27
|
-
stdio: "pipe",
|
|
28
|
-
encoding: "utf8",
|
|
29
|
-
});
|
|
30
|
-
return true;
|
|
31
|
-
} catch {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Find git repository root directory using git command
|
|
38
|
-
* @param {string} startDir - Starting directory path
|
|
39
|
-
* @returns {string|null} Git repository root path or null if not found
|
|
40
|
-
*/
|
|
41
|
-
function findGitRoot(startDir) {
|
|
42
|
-
try {
|
|
43
|
-
const gitRoot = execSync("git rev-parse --show-toplevel", {
|
|
44
|
-
cwd: startDir,
|
|
45
|
-
stdio: "pipe",
|
|
46
|
-
encoding: "utf8",
|
|
47
|
-
}).trim();
|
|
48
|
-
return gitRoot;
|
|
49
|
-
} catch {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Convert gitignore patterns to glob-compatible patterns
|
|
56
|
-
* @param {string} pattern - A single gitignore pattern
|
|
57
|
-
* @returns {string[]} Array of glob patterns that match gitignore behavior
|
|
58
|
-
*/
|
|
59
|
-
function gitignoreToGlobPatterns(pattern) {
|
|
60
|
-
const patterns = [];
|
|
61
|
-
|
|
62
|
-
// Remove leading slash (already handled by gitignore parsing)
|
|
63
|
-
const cleanPattern = pattern.replace(/^\//, "");
|
|
64
|
-
|
|
65
|
-
// If pattern doesn't contain wildcards and doesn't end with /
|
|
66
|
-
// it could match both files and directories
|
|
67
|
-
if (!cleanPattern.includes("*") && !cleanPattern.includes("?") && !cleanPattern.endsWith("/")) {
|
|
68
|
-
// Add patterns to match both file and directory
|
|
69
|
-
patterns.push(cleanPattern); // Exact match
|
|
70
|
-
patterns.push(`${cleanPattern}/**`); // Directory contents
|
|
71
|
-
patterns.push(`**/${cleanPattern}`); // Nested exact match
|
|
72
|
-
patterns.push(`**/${cleanPattern}/**`); // Nested directory contents
|
|
73
|
-
} else if (cleanPattern.endsWith("/")) {
|
|
74
|
-
// Directory-only pattern
|
|
75
|
-
const dirPattern = cleanPattern.slice(0, -1);
|
|
76
|
-
patterns.push(`${dirPattern}/**`);
|
|
77
|
-
patterns.push(`**/${dirPattern}/**`);
|
|
78
|
-
} else {
|
|
79
|
-
// Pattern with wildcards or specific file
|
|
80
|
-
patterns.push(cleanPattern);
|
|
81
|
-
if (!cleanPattern.startsWith("**/")) {
|
|
82
|
-
patterns.push(`**/${cleanPattern}`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return patterns;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Parse .gitignore content into patterns
|
|
91
|
-
* @param {string} content - .gitignore file content
|
|
92
|
-
* @returns {string[]} Array of ignore patterns converted to glob format
|
|
93
|
-
*/
|
|
94
|
-
function parseGitignoreContent(content) {
|
|
95
|
-
const lines = content
|
|
96
|
-
.split("\n")
|
|
97
|
-
.map((line) => line.trim())
|
|
98
|
-
.filter((line) => line && !line.startsWith("#"))
|
|
99
|
-
.map((line) => line.replace(/^\//, "")); // Remove leading slash
|
|
100
|
-
|
|
101
|
-
// Convert each gitignore pattern to glob patterns
|
|
102
|
-
const allPatterns = [];
|
|
103
|
-
for (const line of lines) {
|
|
104
|
-
allPatterns.push(...gitignoreToGlobPatterns(line));
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return [...new Set(allPatterns)]; // Remove duplicates
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Load .gitignore patterns from multiple directories (current + all parent directories up to git root)
|
|
112
|
-
* @param {string} dir - Directory path (will search up to find all .gitignore files)
|
|
113
|
-
* @returns {string[]|null} Array of merged ignore patterns or null if no .gitignore found
|
|
114
|
-
*/
|
|
115
|
-
export async function loadGitignore(dir) {
|
|
116
|
-
// First, check if we're in a git repository
|
|
117
|
-
const inGitRepo = isInGitRepository(dir);
|
|
118
|
-
if (!inGitRepo) {
|
|
119
|
-
// Not in a git repository, just check the current directory
|
|
120
|
-
const gitignorePath = path.join(dir, ".gitignore");
|
|
121
|
-
try {
|
|
122
|
-
await access(gitignorePath);
|
|
123
|
-
const gitignoreContent = await readFile(gitignorePath, "utf8");
|
|
124
|
-
const ignorePatterns = parseGitignoreContent(gitignoreContent);
|
|
125
|
-
return ignorePatterns.length > 0 ? ignorePatterns : null;
|
|
126
|
-
} catch {
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// We're in a git repository, collect all .gitignore files from current dir to git root
|
|
132
|
-
const gitRoot = findGitRoot(dir);
|
|
133
|
-
if (!gitRoot) {
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const allPatterns = [];
|
|
138
|
-
let currentDir = path.resolve(dir);
|
|
139
|
-
|
|
140
|
-
// Collect .gitignore patterns from current directory up to git root
|
|
141
|
-
while (currentDir.startsWith(gitRoot)) {
|
|
142
|
-
const gitignorePath = path.join(currentDir, ".gitignore");
|
|
143
|
-
try {
|
|
144
|
-
await access(gitignorePath);
|
|
145
|
-
const gitignoreContent = await readFile(gitignorePath, "utf8");
|
|
146
|
-
const patterns = parseGitignoreContent(gitignoreContent);
|
|
147
|
-
|
|
148
|
-
// Add patterns with context of which directory they came from
|
|
149
|
-
// Patterns from deeper directories take precedence
|
|
150
|
-
allPatterns.unshift(...patterns);
|
|
151
|
-
} catch {
|
|
152
|
-
// .gitignore doesn't exist in this directory, continue
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Move up one directory
|
|
156
|
-
if (currentDir === gitRoot) {
|
|
157
|
-
break;
|
|
158
|
-
}
|
|
159
|
-
currentDir = path.dirname(currentDir);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return allPatterns.length > 0 ? [...new Set(allPatterns)] : null;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Get files using glob patterns
|
|
167
|
-
* @param {string} dir - Directory to search
|
|
168
|
-
* @param {string[]} includePatterns - Include patterns
|
|
169
|
-
* @param {string[]} excludePatterns - Exclude patterns
|
|
170
|
-
* @param {string[]} gitignorePatterns - .gitignore patterns
|
|
171
|
-
* @returns {Promise<string[]>} Array of file paths
|
|
172
|
-
*/
|
|
173
|
-
export async function getFilesWithGlob(dir, includePatterns, excludePatterns, gitignorePatterns) {
|
|
174
|
-
if (!includePatterns || includePatterns.length === 0) {
|
|
175
|
-
console.warn("No include patterns provided");
|
|
176
|
-
return [];
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Prepare all ignore patterns
|
|
180
|
-
const allIgnorePatterns = [];
|
|
181
|
-
|
|
182
|
-
if (excludePatterns) {
|
|
183
|
-
allIgnorePatterns.push(...excludePatterns);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (gitignorePatterns) {
|
|
187
|
-
allIgnorePatterns.push(...gitignorePatterns);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Add default exclusions if not already present
|
|
191
|
-
const defaultExclusions = ["node_modules/**", "temp/**"];
|
|
192
|
-
for (const exclusion of defaultExclusions) {
|
|
193
|
-
if (!allIgnorePatterns.includes(exclusion)) {
|
|
194
|
-
allIgnorePatterns.push(exclusion);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Convert patterns to be relative to the directory
|
|
199
|
-
const patterns = includePatterns.map((pattern) => {
|
|
200
|
-
// If pattern doesn't start with / or **, make it relative to dir
|
|
201
|
-
if (!pattern.startsWith("/") && !pattern.startsWith("**")) {
|
|
202
|
-
return `**/${pattern}`; // Use ** to search recursively
|
|
203
|
-
}
|
|
204
|
-
return pattern;
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
try {
|
|
208
|
-
const files = await glob(patterns, {
|
|
209
|
-
cwd: dir,
|
|
210
|
-
ignore: allIgnorePatterns.length > 0 ? allIgnorePatterns : undefined,
|
|
211
|
-
absolute: true,
|
|
212
|
-
nodir: true, // Only return files, not directories
|
|
213
|
-
dot: false, // Don't include dot files by default
|
|
214
|
-
gitignore: true, // Enable .gitignore support
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
return files;
|
|
218
|
-
} catch (error) {
|
|
219
|
-
console.warn(`Warning: Error during glob search in ${dir}: ${error.message}`);
|
|
220
|
-
return [];
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Check if a path exists
|
|
226
|
-
* @param {string} targetPath - Path to check
|
|
227
|
-
* @returns {Promise<boolean>} True if path exists
|
|
228
|
-
*/
|
|
229
|
-
export async function pathExists(targetPath) {
|
|
230
|
-
try {
|
|
231
|
-
await stat(targetPath);
|
|
232
|
-
return true;
|
|
233
|
-
} catch (error) {
|
|
234
|
-
if (error.code === "ENOENT") return false;
|
|
235
|
-
throw error;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Convert absolute path to display path relative to current working directory
|
|
241
|
-
* @param {string} targetPath - Absolute path to convert
|
|
242
|
-
* @returns {string} Display path (relative or absolute)
|
|
243
|
-
*/
|
|
244
|
-
export function toDisplayPath(targetPath) {
|
|
245
|
-
const rel = path.relative(process.cwd(), targetPath);
|
|
246
|
-
return rel.startsWith("..") ? targetPath : rel || ".";
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Resolve path to absolute path
|
|
251
|
-
* @param {string} value - Path to resolve
|
|
252
|
-
* @returns {string|undefined} Absolute path or undefined if no value provided
|
|
253
|
-
*/
|
|
254
|
-
export function resolveToAbsolute(value) {
|
|
255
|
-
if (!value) return undefined;
|
|
256
|
-
return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Load files from sourcesPath array
|
|
261
|
-
* Supports file paths, directory paths, and glob patterns
|
|
262
|
-
* @param {string|string[]} sourcesPath - Single path or array of paths
|
|
263
|
-
* @param {object} options - Options for file loading
|
|
264
|
-
* @param {string|string[]} options.includePatterns - Include patterns for directories
|
|
265
|
-
* @param {string|string[]} options.excludePatterns - Exclude patterns for directories
|
|
266
|
-
* @param {boolean} options.useDefaultPatterns - Whether to use default patterns (default: true)
|
|
267
|
-
* @param {string[]} options.defaultIncludePatterns - Default include patterns
|
|
268
|
-
* @param {string[]} options.defaultExcludePatterns - Default exclude patterns
|
|
269
|
-
* @returns {Promise<string[]>} Array of absolute file paths
|
|
270
|
-
*/
|
|
271
|
-
export async function loadFilesFromPaths(sourcesPath, options = {}) {
|
|
272
|
-
const {
|
|
273
|
-
includePatterns,
|
|
274
|
-
excludePatterns,
|
|
275
|
-
useDefaultPatterns = true,
|
|
276
|
-
defaultIncludePatterns = [],
|
|
277
|
-
defaultExcludePatterns = [],
|
|
278
|
-
} = options;
|
|
279
|
-
|
|
280
|
-
const paths = Array.isArray(sourcesPath) ? sourcesPath : [sourcesPath];
|
|
281
|
-
let allFiles = [];
|
|
282
|
-
|
|
283
|
-
for (const dir of paths) {
|
|
284
|
-
try {
|
|
285
|
-
if (typeof dir !== "string") {
|
|
286
|
-
console.warn(`Invalid source path: ${dir}`);
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (isRemoteFile(dir)) {
|
|
291
|
-
allFiles.push(dir);
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// First try to access as a file or directory
|
|
296
|
-
const stats = await stat(dir);
|
|
297
|
-
|
|
298
|
-
if (stats.isFile()) {
|
|
299
|
-
// If it's a file, add it directly without filtering
|
|
300
|
-
allFiles.push(dir);
|
|
301
|
-
} else if (stats.isDirectory()) {
|
|
302
|
-
// If it's a directory, use the existing glob logic
|
|
303
|
-
// Load .gitignore for this directory
|
|
304
|
-
const gitignorePatterns = await loadGitignore(dir);
|
|
305
|
-
|
|
306
|
-
// Prepare patterns
|
|
307
|
-
let finalIncludePatterns = null;
|
|
308
|
-
let finalExcludePatterns = null;
|
|
309
|
-
|
|
310
|
-
if (useDefaultPatterns) {
|
|
311
|
-
// Merge with default patterns
|
|
312
|
-
const userInclude = includePatterns
|
|
313
|
-
? Array.isArray(includePatterns)
|
|
314
|
-
? includePatterns
|
|
315
|
-
: [includePatterns]
|
|
316
|
-
: [];
|
|
317
|
-
const userExclude = excludePatterns
|
|
318
|
-
? Array.isArray(excludePatterns)
|
|
319
|
-
? excludePatterns
|
|
320
|
-
: [excludePatterns]
|
|
321
|
-
: [];
|
|
322
|
-
|
|
323
|
-
finalIncludePatterns = [...defaultIncludePatterns, ...userInclude];
|
|
324
|
-
finalExcludePatterns = [
|
|
325
|
-
...defaultExcludePatterns,
|
|
326
|
-
...userExclude.map((x) => {
|
|
327
|
-
const prefix = `${dir}/`;
|
|
328
|
-
return x.startsWith(prefix) ? x.slice(prefix.length) : x;
|
|
329
|
-
}),
|
|
330
|
-
];
|
|
331
|
-
} else {
|
|
332
|
-
// Use only user patterns
|
|
333
|
-
if (includePatterns) {
|
|
334
|
-
finalIncludePatterns = Array.isArray(includePatterns)
|
|
335
|
-
? includePatterns
|
|
336
|
-
: [includePatterns];
|
|
337
|
-
}
|
|
338
|
-
if (excludePatterns) {
|
|
339
|
-
finalExcludePatterns = Array.isArray(excludePatterns)
|
|
340
|
-
? excludePatterns
|
|
341
|
-
: [excludePatterns];
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Get files using glob
|
|
346
|
-
const filesInDir = await getFilesWithGlob(
|
|
347
|
-
dir,
|
|
348
|
-
finalIncludePatterns,
|
|
349
|
-
finalExcludePatterns,
|
|
350
|
-
gitignorePatterns,
|
|
351
|
-
);
|
|
352
|
-
allFiles = allFiles.concat(filesInDir);
|
|
353
|
-
}
|
|
354
|
-
} catch (err) {
|
|
355
|
-
if (err.code === "ENOENT") {
|
|
356
|
-
// Path doesn't exist as file or directory, try as glob pattern
|
|
357
|
-
try {
|
|
358
|
-
// Check if it looks like a glob pattern
|
|
359
|
-
const isGlobPatternResult = isGlobPattern(dir);
|
|
360
|
-
|
|
361
|
-
if (isGlobPatternResult) {
|
|
362
|
-
// Use glob to find matching files from current working directory
|
|
363
|
-
const matchedFiles = await glob(dir, {
|
|
364
|
-
absolute: true,
|
|
365
|
-
nodir: true, // Only files, not directories
|
|
366
|
-
dot: false, // Don't include hidden files
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
if (matchedFiles.length > 0) {
|
|
370
|
-
allFiles = allFiles.concat(matchedFiles);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
} catch (globErr) {
|
|
374
|
-
console.warn(`Failed to process glob pattern "${dir}": ${globErr.message}`);
|
|
375
|
-
}
|
|
376
|
-
} else {
|
|
377
|
-
throw err;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
return allFiles;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* Check if a file is likely a text file by checking if it's binary
|
|
387
|
-
* @param {string} filePath - File path to check
|
|
388
|
-
* @returns {Promise<boolean>} True if file appears to be a text file
|
|
389
|
-
*/
|
|
390
|
-
async function isTextFile(filePath) {
|
|
391
|
-
if (isRemoteFile(filePath)) {
|
|
392
|
-
return isRemoteTextFile(filePath);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
try {
|
|
396
|
-
const isBinary = await isBinaryFile(filePath);
|
|
397
|
-
return !isBinary;
|
|
398
|
-
} catch (_error) {
|
|
399
|
-
// If we can't read the file, assume it might be binary to be safe
|
|
400
|
-
return false;
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Check if a string is an HTTP/HTTPS URL
|
|
406
|
-
* @param {string} fileUrl - The string to check
|
|
407
|
-
* @returns {boolean} - True if the string starts with http:// or https://
|
|
408
|
-
*/
|
|
409
|
-
export function isRemoteFile(fileUrl) {
|
|
410
|
-
if (typeof fileUrl !== "string") return false;
|
|
411
|
-
|
|
412
|
-
try {
|
|
413
|
-
const url = new URL(fileUrl);
|
|
414
|
-
// Only accept http and https url
|
|
415
|
-
if (["http:", "https:"].includes(url.protocol)) {
|
|
416
|
-
return true;
|
|
417
|
-
}
|
|
418
|
-
// other protocol will be treated as bad url
|
|
419
|
-
return false;
|
|
420
|
-
} catch {
|
|
421
|
-
return false;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
export async function isRemoteFileAvailable(fileUrl) {
|
|
426
|
-
if (!isRemoteFile(fileUrl)) return false;
|
|
427
|
-
|
|
428
|
-
try {
|
|
429
|
-
const res = await fetch(fileUrl, {
|
|
430
|
-
method: "HEAD",
|
|
431
|
-
});
|
|
432
|
-
return res.ok;
|
|
433
|
-
} catch (error) {
|
|
434
|
-
debug(`Failed to check HTTP file availability: ${fileUrl} - ${error.message}`);
|
|
435
|
-
return false;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
export async function isRemoteTextFile(fileUrl) {
|
|
440
|
-
try {
|
|
441
|
-
const res = await fetch(fileUrl, {
|
|
442
|
-
method: "HEAD",
|
|
443
|
-
});
|
|
444
|
-
const contentType = res.headers.get("content-type") || "";
|
|
445
|
-
const textMimeTypes = [
|
|
446
|
-
"application/json",
|
|
447
|
-
"application/ld+json",
|
|
448
|
-
"application/graphql+json",
|
|
449
|
-
"application/xml",
|
|
450
|
-
"application/xhtml+xml",
|
|
451
|
-
"application/javascript",
|
|
452
|
-
"application/ecmascript",
|
|
453
|
-
"application/x-www-form-urlencoded",
|
|
454
|
-
"application/rss+xml",
|
|
455
|
-
"application/atom+xml",
|
|
456
|
-
];
|
|
457
|
-
if (contentType.startsWith("text/") || textMimeTypes.includes(contentType)) {
|
|
458
|
-
return true;
|
|
459
|
-
}
|
|
460
|
-
return false;
|
|
461
|
-
} catch (error) {
|
|
462
|
-
debug(`Failed to check HTTP file content type: ${fileUrl} - ${error.message}`);
|
|
463
|
-
return null;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
export async function getRemoteFileContent(fileUrl) {
|
|
468
|
-
if (!fileUrl) return null;
|
|
469
|
-
try {
|
|
470
|
-
const res = await fetch(fileUrl);
|
|
471
|
-
const text = await res.text();
|
|
472
|
-
return text;
|
|
473
|
-
} catch (error) {
|
|
474
|
-
debug(`Failed to fetch HTTP file content: ${fileUrl} - ${error.message}`);
|
|
475
|
-
return null;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* Read and parse file contents from an array of file paths
|
|
481
|
-
* @param {string[]} files - Array of file paths to read
|
|
482
|
-
* @param {string} baseDir - Base directory for calculating relative paths (defaults to cwd)
|
|
483
|
-
* @param {object} options - Options for reading files
|
|
484
|
-
* @param {boolean} options.skipBinaryFiles - Whether to skip binary files (default: true)
|
|
485
|
-
* @returns {Promise<{sourceId: string, content: string}[]>} Array of file objects with sourceId and content
|
|
486
|
-
*/
|
|
487
|
-
export async function readFileContents(files, baseDir = process.cwd(), options = {}) {
|
|
488
|
-
const { skipBinaryFiles = true } = options;
|
|
489
|
-
|
|
490
|
-
const results = await Promise.all(
|
|
491
|
-
files.map(async (file) => {
|
|
492
|
-
// Skip binary files if enabled
|
|
493
|
-
if (skipBinaryFiles) {
|
|
494
|
-
const isText = await isTextFile(file);
|
|
495
|
-
if (!isText) {
|
|
496
|
-
return null;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
try {
|
|
501
|
-
if (isRemoteFile(file)) {
|
|
502
|
-
const content = await getRemoteFileContent(file);
|
|
503
|
-
if (content) {
|
|
504
|
-
return {
|
|
505
|
-
sourceId: file,
|
|
506
|
-
content,
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
return null;
|
|
511
|
-
} else {
|
|
512
|
-
const content = await extractApi(file);
|
|
513
|
-
if (!content) return null;
|
|
514
|
-
|
|
515
|
-
const relativePath = path.relative(baseDir, file);
|
|
516
|
-
return {
|
|
517
|
-
sourceId: relativePath,
|
|
518
|
-
content,
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
} catch (error) {
|
|
522
|
-
// If reading as text fails (e.g., binary file), skip it
|
|
523
|
-
console.warn(`Failed to read file as text: ${file} - ${error.message}`);
|
|
524
|
-
return null;
|
|
525
|
-
}
|
|
526
|
-
}),
|
|
527
|
-
);
|
|
528
|
-
|
|
529
|
-
// Filter out null results
|
|
530
|
-
return results.filter((result) => result !== null);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
export function calculateTokens(text) {
|
|
534
|
-
const tokens = encode(text);
|
|
535
|
-
return tokens.length;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/**
|
|
539
|
-
* Calculate total lines and tokens from file contents
|
|
540
|
-
* @param {Array<{content: string}>} sourceFiles - Array of objects containing content property
|
|
541
|
-
* @returns {{totalTokens: number, totalLines: number}} Object with totalTokens and totalLines
|
|
542
|
-
*/
|
|
543
|
-
export function calculateFileStats(sourceFiles) {
|
|
544
|
-
let totalTokens = 0;
|
|
545
|
-
let totalLines = 0;
|
|
546
|
-
|
|
547
|
-
for (const source of sourceFiles) {
|
|
548
|
-
const { content } = source;
|
|
549
|
-
if (content) {
|
|
550
|
-
// Count tokens using gpt-tokenizer
|
|
551
|
-
const tokens = encode(content);
|
|
552
|
-
totalTokens += tokens.length;
|
|
553
|
-
|
|
554
|
-
// Count lines (excluding empty lines)
|
|
555
|
-
totalLines += content.split("\n").filter((line) => line.trim() !== "").length;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
return { totalTokens, totalLines };
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* Build sources content string
|
|
564
|
-
* @param {Array<{sourceId: string, content: string}>} sourceFiles - Array of source file objects
|
|
565
|
-
* @returns {string} Concatenated sources content with sourceId comments
|
|
566
|
-
*/
|
|
567
|
-
export function buildSourcesContent(sourceFiles) {
|
|
568
|
-
// Build sources string
|
|
569
|
-
let allSources = "";
|
|
570
|
-
|
|
571
|
-
// Include all files for normal contexts
|
|
572
|
-
for (const source of sourceFiles) {
|
|
573
|
-
allSources += `\n// sourceId: ${source.sourceId}\n${source.content}\n`;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
return allSources;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/**
|
|
580
|
-
* Get doc-smith configuration file path
|
|
581
|
-
* @param {string} workDir - Working directory (defaults to current directory)
|
|
582
|
-
* @returns {string} Absolute path to config.yaml
|
|
583
|
-
*/
|
|
584
|
-
export function getConfigFilePath(workDir) {
|
|
585
|
-
const cwd = workDir || process.cwd();
|
|
586
|
-
return path.join(cwd, ".aigne", "doc-smith", "config.yaml");
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
/**
|
|
590
|
-
* Get doc-smith structure plan file path
|
|
591
|
-
* @param {string} workDir - Working directory (defaults to current directory)
|
|
592
|
-
* @returns {string} Absolute path to structure-plan.json
|
|
593
|
-
*/
|
|
594
|
-
export function getStructurePlanPath(workDir) {
|
|
595
|
-
const cwd = workDir || process.cwd();
|
|
596
|
-
return path.join(cwd, ".aigne", "doc-smith", "output", "structure-plan.json");
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// Shared extension → MIME type mapping table
|
|
600
|
-
const EXT_TO_MIME = {
|
|
601
|
-
".jpg": "image/jpeg",
|
|
602
|
-
".jpeg": "image/jpeg",
|
|
603
|
-
".png": "image/png",
|
|
604
|
-
".gif": "image/gif",
|
|
605
|
-
".bmp": "image/bmp",
|
|
606
|
-
".webp": "image/webp",
|
|
607
|
-
".svg": "image/svg+xml",
|
|
608
|
-
".heic": "image/heic",
|
|
609
|
-
".heif": "image/heif",
|
|
610
|
-
".mp4": "video/mp4",
|
|
611
|
-
".mpeg": "video/mpeg",
|
|
612
|
-
".mpg": "video/mpg",
|
|
613
|
-
".mov": "video/mov",
|
|
614
|
-
".avi": "video/avi",
|
|
615
|
-
".flv": "video/x-flv",
|
|
616
|
-
".mkv": "video/x-matroska",
|
|
617
|
-
".webm": "video/webm",
|
|
618
|
-
".wmv": "video/wmv",
|
|
619
|
-
".m4v": "video/x-m4v",
|
|
620
|
-
".3gpp": "video/3gpp",
|
|
621
|
-
".mp3": "audio/mpeg",
|
|
622
|
-
".wav": "audio/wav",
|
|
623
|
-
".pdf": "application/pdf",
|
|
624
|
-
".doc": "application/msword",
|
|
625
|
-
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
626
|
-
".xls": "application/vnd.ms-excel",
|
|
627
|
-
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
628
|
-
".ppt": "application/vnd.ms-powerpoint",
|
|
629
|
-
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
630
|
-
".txt": "text/plain",
|
|
631
|
-
".json": "application/json",
|
|
632
|
-
".xml": "application/xml",
|
|
633
|
-
".html": "text/html",
|
|
634
|
-
".css": "text/css",
|
|
635
|
-
".js": "application/javascript",
|
|
636
|
-
".zip": "application/zip",
|
|
637
|
-
".rar": "application/x-rar-compressed",
|
|
638
|
-
".7z": "application/x-7z-compressed",
|
|
639
|
-
};
|
|
640
|
-
|
|
641
|
-
// Build reverse mapping: MIME → extensions
|
|
642
|
-
const MIME_TO_EXTS = Object.entries(EXT_TO_MIME).reduce((acc, [ext, mime]) => {
|
|
643
|
-
const key = mime.toLowerCase();
|
|
644
|
-
if (!acc[key]) {
|
|
645
|
-
acc[key] = [];
|
|
646
|
-
}
|
|
647
|
-
acc[key].push(ext);
|
|
648
|
-
return acc;
|
|
649
|
-
}, {});
|
|
650
|
-
|
|
651
|
-
/**
|
|
652
|
-
* Get MIME type from file path based on extension
|
|
653
|
-
* @param {string} filePath - File path
|
|
654
|
-
* @returns {string} MIME type
|
|
655
|
-
*/
|
|
656
|
-
export function getMimeType(filePath) {
|
|
657
|
-
const ext = path.extname(filePath || "").toLowerCase();
|
|
658
|
-
return EXT_TO_MIME[ext] || "application/octet-stream";
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
/**
|
|
662
|
-
* Get file extension (without dot) from content type
|
|
663
|
-
* Handles content types with parameters (e.g., "image/jpeg; charset=utf-8")
|
|
664
|
-
* @param {string} contentType - Content type string
|
|
665
|
-
* @returns {string} File extension without dot
|
|
666
|
-
*/
|
|
667
|
-
export function getExtnameFromContentType(contentType) {
|
|
668
|
-
if (!contentType) return "";
|
|
669
|
-
const base = String(contentType).split(";")[0].trim().toLowerCase();
|
|
670
|
-
const exts = MIME_TO_EXTS[base];
|
|
671
|
-
if (exts?.length) return exts[0].slice(1); // Remove leading dot
|
|
672
|
-
const parts = base.split("/");
|
|
673
|
-
return parts[1] || "";
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
/**
|
|
677
|
-
* Get media description cache file path
|
|
678
|
-
* @returns {string} Absolute path to media-description.yaml
|
|
679
|
-
*/
|
|
680
|
-
export function getMediaDescriptionCachePath() {
|
|
681
|
-
const cwd = process.cwd();
|
|
682
|
-
return path.join(cwd, ".aigne", "doc-smith", "media-description.yaml");
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
* Detect file type from buffer with comprehensive fallback strategy
|
|
687
|
-
* @param {Buffer} buffer - File buffer
|
|
688
|
-
* @param {string} contentType - HTTP Content-Type header
|
|
689
|
-
* @param {string} url - Original URL (for fallback)
|
|
690
|
-
* @returns {Promise<{ext: string, mime: string}>} File extension and MIME type
|
|
691
|
-
*/
|
|
692
|
-
export async function detectFileType(buffer, contentType, url = "") {
|
|
693
|
-
// 1. Try file-type for binary images (PNG, JPG, WebP, GIF, etc.)
|
|
694
|
-
try {
|
|
695
|
-
const fileType = await fileTypeFromBuffer(buffer);
|
|
696
|
-
if (fileType) {
|
|
697
|
-
return {
|
|
698
|
-
ext: fileType.ext,
|
|
699
|
-
mime: fileType.mime,
|
|
700
|
-
};
|
|
701
|
-
}
|
|
702
|
-
} catch (error) {
|
|
703
|
-
console.debug("file-type detection failed:", error.message);
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
// 2. Check for SVG/SVGZ
|
|
707
|
-
const svgResult = await detectSvgType(buffer, contentType);
|
|
708
|
-
if (svgResult) {
|
|
709
|
-
return svgResult;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// 3. Fallback to Content-Type
|
|
713
|
-
if (contentType) {
|
|
714
|
-
const ext = getExtnameFromContentType(contentType);
|
|
715
|
-
if (ext) {
|
|
716
|
-
return {
|
|
717
|
-
ext,
|
|
718
|
-
mime: contentType.split(";")[0].trim(),
|
|
719
|
-
};
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
// 4. Fallback to URL extension
|
|
724
|
-
if (url) {
|
|
725
|
-
const urlExt = path.extname(url).toLowerCase();
|
|
726
|
-
if (urlExt) {
|
|
727
|
-
return {
|
|
728
|
-
ext: urlExt.slice(1), // Remove leading dot
|
|
729
|
-
mime: getMimeType(url),
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// 5. Default fallback
|
|
735
|
-
return {
|
|
736
|
-
ext: "bin",
|
|
737
|
-
mime: "application/octet-stream",
|
|
738
|
-
};
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
/**
|
|
742
|
-
* Detect SVG/SVGZ file type
|
|
743
|
-
* @param {Buffer} buffer - File buffer
|
|
744
|
-
* @param {string} contentType - HTTP Content-Type header
|
|
745
|
-
* @returns {Promise<{ext: string, mime: string} | null>} SVG info or null
|
|
746
|
-
*/
|
|
747
|
-
async function detectSvgType(buffer, contentType) {
|
|
748
|
-
// Check Content-Type first
|
|
749
|
-
if (contentType?.includes("image/svg+xml")) {
|
|
750
|
-
return {
|
|
751
|
-
ext: "svg",
|
|
752
|
-
mime: "image/svg+xml",
|
|
753
|
-
};
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
try {
|
|
757
|
-
let text = "";
|
|
758
|
-
|
|
759
|
-
// Check if it's gzipped (SVGZ)
|
|
760
|
-
if (buffer.length >= 2 && buffer[0] === 0x1f && buffer[1] === 0x8b) {
|
|
761
|
-
try {
|
|
762
|
-
const decompressed = gunzipSync(buffer);
|
|
763
|
-
text = decompressed.toString("utf8");
|
|
764
|
-
if (isSvgContent(text)) {
|
|
765
|
-
return {
|
|
766
|
-
ext: "svgz",
|
|
767
|
-
mime: "image/svg+xml",
|
|
768
|
-
};
|
|
769
|
-
}
|
|
770
|
-
} catch {
|
|
771
|
-
// Not gzipped, continue with regular text detection
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
// Try as regular text
|
|
776
|
-
if (!text) {
|
|
777
|
-
text = buffer.toString("utf8");
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
if (isSvgContent(text)) {
|
|
781
|
-
return {
|
|
782
|
-
ext: "svg",
|
|
783
|
-
mime: "image/svg+xml",
|
|
784
|
-
};
|
|
785
|
-
}
|
|
786
|
-
} catch (error) {
|
|
787
|
-
console.debug("SVG detection failed:", error.message);
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
return null;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
/**
|
|
794
|
-
* Check if text content is SVG
|
|
795
|
-
* @param {string} text - Text content
|
|
796
|
-
* @returns {boolean} True if SVG
|
|
797
|
-
*/
|
|
798
|
-
function isSvgContent(text) {
|
|
799
|
-
if (!text || typeof text !== "string") return false;
|
|
800
|
-
|
|
801
|
-
// Remove BOM and trim
|
|
802
|
-
const cleanText = text.replace(/^\uFEFF/, "").trim();
|
|
803
|
-
|
|
804
|
-
// Check for SVG root element
|
|
805
|
-
return /^<\?xml\s+[^>]*>\s*<svg/i.test(cleanText) || /^<svg/i.test(cleanText);
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
/**
|
|
809
|
-
* Download and upload a remote image URL
|
|
810
|
-
* @param {string} imageUrl - The remote image URL
|
|
811
|
-
* @param {string} docsDir - Directory to save temporary files
|
|
812
|
-
* @param {string} appUrl - Application URL for upload
|
|
813
|
-
* @param {string} accessToken - Access token for upload
|
|
814
|
-
* @returns {Promise<{url: string, downloadFinalPath: string | null}>} The uploaded image URL and final path if failed
|
|
815
|
-
*/
|
|
816
|
-
export async function downloadAndUploadImage(imageUrl, docsDir, appUrl, accessToken) {
|
|
817
|
-
let downloadFinalPath = null;
|
|
818
|
-
|
|
819
|
-
try {
|
|
820
|
-
// 1. Download with timeout control
|
|
821
|
-
const controller = new AbortController();
|
|
822
|
-
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout
|
|
823
|
-
|
|
824
|
-
const response = await fetch(imageUrl, {
|
|
825
|
-
signal: controller.signal,
|
|
826
|
-
});
|
|
827
|
-
clearTimeout(timeoutId);
|
|
828
|
-
|
|
829
|
-
if (!response.ok) {
|
|
830
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
// 2. Convert to Buffer for file type detection
|
|
834
|
-
const blob = await response.blob();
|
|
835
|
-
const arrayBuffer = await blob.arrayBuffer();
|
|
836
|
-
const buffer = Buffer.from(arrayBuffer);
|
|
837
|
-
|
|
838
|
-
// 3. Detect file type with comprehensive fallback
|
|
839
|
-
const contentType = response.headers.get("content-type");
|
|
840
|
-
const { ext } = await detectFileType(buffer, contentType, imageUrl);
|
|
841
|
-
|
|
842
|
-
// 4. Generate random filename and save file
|
|
843
|
-
const randomId = randomBytes(16).toString("hex");
|
|
844
|
-
const tempFilePath = join(docsDir, `temp-logo-${randomId}`);
|
|
845
|
-
downloadFinalPath = ext ? `${tempFilePath}.${ext}` : tempFilePath;
|
|
846
|
-
fs.writeFileSync(downloadFinalPath, buffer);
|
|
847
|
-
|
|
848
|
-
// 5. Upload and get URL
|
|
849
|
-
const { results: uploadResults } = await uploadFiles({
|
|
850
|
-
appUrl,
|
|
851
|
-
filePaths: [downloadFinalPath],
|
|
852
|
-
accessToken,
|
|
853
|
-
concurrency: 1,
|
|
854
|
-
});
|
|
855
|
-
|
|
856
|
-
// 6. Return uploaded URL
|
|
857
|
-
return { url: uploadResults?.[0]?.url || imageUrl, downloadFinalPath };
|
|
858
|
-
} catch (error) {
|
|
859
|
-
console.warn(`Failed to download and upload image from ${imageUrl}: ${error.message}`);
|
|
860
|
-
return { url: imageUrl, downloadFinalPath: null };
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
/**
|
|
865
|
-
* Extract the path prefix from a glob pattern until the first glob character
|
|
866
|
-
*/
|
|
867
|
-
export function getPathPrefix(pattern) {
|
|
868
|
-
const segments = pattern.split("/");
|
|
869
|
-
const result = [];
|
|
870
|
-
|
|
871
|
-
for (const segment of segments) {
|
|
872
|
-
if (isGlobPattern(segment)) {
|
|
873
|
-
break;
|
|
874
|
-
}
|
|
875
|
-
result.push(segment);
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
return result.join("/") || ".";
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
/**
|
|
882
|
-
* Check if a dir matches any exclude pattern
|
|
883
|
-
*/
|
|
884
|
-
export function isDirExcluded(dir, excludePatterns) {
|
|
885
|
-
if (!dir || typeof dir !== "string") {
|
|
886
|
-
return false;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
let normalizedDir = dir.replace(/\\/g, "/").replace(/^\.\/+/, "");
|
|
890
|
-
normalizedDir = normalizedDir.endsWith("/") ? normalizedDir : `${normalizedDir}/`;
|
|
891
|
-
|
|
892
|
-
for (const excludePattern of excludePatterns) {
|
|
893
|
-
if (minimatch(normalizedDir, excludePattern, { dot: true })) {
|
|
894
|
-
return true;
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
return false;
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
/**
|
|
902
|
-
* Return source paths that would be excluded by exclude patterns (files are skipped, directories use minimatch, glob patterns use path prefix heuristic)
|
|
903
|
-
* @returns {{excluded: string[], notFound: string[]}} Object with excluded and notFound arrays
|
|
904
|
-
*/
|
|
905
|
-
export async function findInvalidSourcePaths(sourcePaths, excludePatterns) {
|
|
906
|
-
if (!Array.isArray(sourcePaths) || sourcePaths.length === 0) {
|
|
907
|
-
return { excluded: [], notFound: [] };
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
if (!Array.isArray(excludePatterns) || excludePatterns.length === 0) {
|
|
911
|
-
return { excluded: [], notFound: [] };
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
const excluded = [];
|
|
915
|
-
const notFound = [];
|
|
916
|
-
|
|
917
|
-
for (const sourcePath of sourcePaths) {
|
|
918
|
-
if (typeof sourcePath !== "string" || !sourcePath) {
|
|
919
|
-
continue;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
// Skip paths starting with "!" (exclusion patterns)
|
|
923
|
-
if (sourcePath.startsWith("!")) {
|
|
924
|
-
continue;
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
// Skip remote URLs
|
|
928
|
-
if (isRemoteFile(sourcePath)) {
|
|
929
|
-
continue;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
// Check glob pattern: use heuristic algorithm
|
|
933
|
-
if (isGlobPattern(sourcePath)) {
|
|
934
|
-
const representativePath = getPathPrefix(sourcePath);
|
|
935
|
-
if (isDirExcluded(representativePath, excludePatterns)) {
|
|
936
|
-
excluded.push(sourcePath);
|
|
937
|
-
}
|
|
938
|
-
continue;
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
try {
|
|
942
|
-
const stats = await stat(sourcePath);
|
|
943
|
-
// Skip file
|
|
944
|
-
if (stats.isFile()) {
|
|
945
|
-
continue;
|
|
946
|
-
}
|
|
947
|
-
// Check dir with minimatch
|
|
948
|
-
if (stats.isDirectory()) {
|
|
949
|
-
if (isDirExcluded(sourcePath, excludePatterns)) {
|
|
950
|
-
excluded.push(sourcePath);
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
} catch {
|
|
954
|
-
// Path doesn't exist
|
|
955
|
-
notFound.push(sourcePath);
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
return { excluded, notFound };
|
|
960
|
-
}
|