@aigne/doc-smith 0.9.8-alpha.2 → 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/planner.md +0 -168
- package/agentic-agents/common/worker.md +0 -93
- package/agentic-agents/create/index.yaml +0 -118
- 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/utils.mjs
DELETED
|
@@ -1,1354 +0,0 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
|
-
import crypto from "node:crypto";
|
|
3
|
-
import { accessSync, constants, existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
|
|
4
|
-
import fs from "node:fs/promises";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { parse, stringify as yamlStringify } from "yaml";
|
|
7
|
-
import {
|
|
8
|
-
detectResolvableConflicts,
|
|
9
|
-
generateConflictResolutionRules,
|
|
10
|
-
} from "./conflict-detector.mjs";
|
|
11
|
-
import {
|
|
12
|
-
DEFAULT_EXCLUDE_PATTERNS,
|
|
13
|
-
DEFAULT_INCLUDE_PATTERNS,
|
|
14
|
-
DOCUMENT_STYLES,
|
|
15
|
-
DOCUMENTATION_DEPTH,
|
|
16
|
-
READER_KNOWLEDGE_LEVELS,
|
|
17
|
-
SUPPORTED_FILE_EXTENSIONS,
|
|
18
|
-
SUPPORTED_LANGUAGES,
|
|
19
|
-
TARGET_AUDIENCES,
|
|
20
|
-
} from "./constants/index.mjs";
|
|
21
|
-
import { isRemoteFile, getRemoteFileContent } from "./file-utils.mjs";
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Normalize path to absolute path for consistent comparison
|
|
25
|
-
* @param {string} filePath - The path to normalize
|
|
26
|
-
* @returns {string} - Absolute path
|
|
27
|
-
*/
|
|
28
|
-
export function normalizePath(filePath) {
|
|
29
|
-
return path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Convert path to relative path from current working directory
|
|
34
|
-
* @param {string} filePath - The path to convert
|
|
35
|
-
* @returns {string} - Relative path
|
|
36
|
-
*/
|
|
37
|
-
export function toRelativePath(filePath) {
|
|
38
|
-
return path.isAbsolute(filePath) ? path.relative(process.cwd(), filePath) : filePath;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Check if a string looks like a glob pattern
|
|
43
|
-
* @param {string} pattern - The string to check
|
|
44
|
-
* @returns {boolean} - True if the string contains glob pattern characters
|
|
45
|
-
*/
|
|
46
|
-
export function isGlobPattern(pattern) {
|
|
47
|
-
if (pattern == null) return false;
|
|
48
|
-
return /[*?[\]]|(\*\*)/.test(pattern);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function processContent({ content }) {
|
|
52
|
-
// Match markdown regular links [text](link), exclude images 
|
|
53
|
-
return content.replace(/(?<!!)\[([^\]]+)\]\(([^)]+)\)/g, (match, text, link) => {
|
|
54
|
-
const trimLink = link.trim();
|
|
55
|
-
// Exclude external links and mailto
|
|
56
|
-
if (/^(https?:\/\/|mailto:)/.test(trimLink)) return match;
|
|
57
|
-
// Preserve anchors
|
|
58
|
-
const [path, hash] = trimLink.split("#");
|
|
59
|
-
// Skip if already has extension
|
|
60
|
-
if (/\.[a-zA-Z0-9]+$/.test(path)) return match;
|
|
61
|
-
// Only process relative paths or paths starting with /
|
|
62
|
-
if (!path) return match;
|
|
63
|
-
// Flatten to ./xxx-yyy.md
|
|
64
|
-
let finalPath = path;
|
|
65
|
-
if (path.startsWith(".")) {
|
|
66
|
-
finalPath = path.replace(/^\./, "");
|
|
67
|
-
}
|
|
68
|
-
let flatPath = finalPath.replace(/^\//, "").replace(/\//g, "-");
|
|
69
|
-
flatPath = `./${flatPath}.md`;
|
|
70
|
-
const newLink = hash ? `${flatPath}#${hash}` : flatPath;
|
|
71
|
-
return `[${text}](${newLink})`;
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Helper function to generate filename based on language
|
|
76
|
-
export function getFileName(docPath, language) {
|
|
77
|
-
// Flatten path: remove leading /, replace all / with -
|
|
78
|
-
const flatName = docPath.replace(/^\//, "").replace(/\//g, "-");
|
|
79
|
-
const isEnglish = language === "en";
|
|
80
|
-
return isEnglish ? `${flatName}.md` : `${flatName}.${language}.md`;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Save a single document to files
|
|
85
|
-
* @param {Object} params
|
|
86
|
-
* @param {string} params.path - Relative path (without extension)
|
|
87
|
-
* @param {string} params.content - Main document content
|
|
88
|
-
* @param {string} params.docsDir - Root directory
|
|
89
|
-
* @param {string} params.locale - Main content language (e.g., 'en', 'zh', 'fr')
|
|
90
|
-
* @param {Array<string>} [params.labels] - Document labels for front matter
|
|
91
|
-
* @returns {Promise<{ path: string, success: boolean, error?: string }>}
|
|
92
|
-
*/
|
|
93
|
-
export async function saveDoc({ path: docPath, content, docsDir, locale, labels }) {
|
|
94
|
-
try {
|
|
95
|
-
await fs.mkdir(docsDir, { recursive: true });
|
|
96
|
-
const mainFileName = getFileName(docPath, locale);
|
|
97
|
-
const mainFilePath = path.join(docsDir, mainFileName);
|
|
98
|
-
|
|
99
|
-
// Add labels front matter if labels are provided
|
|
100
|
-
let finalContent = processContent({ content });
|
|
101
|
-
|
|
102
|
-
if (labels && labels.length > 0) {
|
|
103
|
-
const frontMatter = `---\nlabels: ${JSON.stringify(labels)}\n---\n\n`;
|
|
104
|
-
finalContent = frontMatter + finalContent;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
await fs.writeFile(mainFilePath, finalContent, "utf8");
|
|
108
|
-
return { path: mainFilePath, success: true };
|
|
109
|
-
} catch (err) {
|
|
110
|
-
return { path: docPath, success: false, error: err.message };
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export async function saveDocTranslation({
|
|
115
|
-
path: docPath,
|
|
116
|
-
docsDir,
|
|
117
|
-
translation,
|
|
118
|
-
language,
|
|
119
|
-
labels,
|
|
120
|
-
}) {
|
|
121
|
-
try {
|
|
122
|
-
await fs.mkdir(docsDir, { recursive: true });
|
|
123
|
-
const translateFileName = getFileName(docPath, language);
|
|
124
|
-
const translatePath = path.join(docsDir, translateFileName);
|
|
125
|
-
|
|
126
|
-
// Add labels front matter to translation content if labels are provided
|
|
127
|
-
let finalTranslationContent = processContent({
|
|
128
|
-
content: translation,
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
if (labels && labels.length > 0) {
|
|
132
|
-
const frontMatter = `---\nlabels: ${JSON.stringify(labels)}\n---\n\n`;
|
|
133
|
-
finalTranslationContent = frontMatter + finalTranslationContent;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
await fs.writeFile(translatePath, finalTranslationContent, "utf8");
|
|
137
|
-
return { path: translatePath, success: true };
|
|
138
|
-
} catch (err) {
|
|
139
|
-
return { path: docPath, success: false, error: err.message };
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Get current git HEAD commit hash
|
|
145
|
-
* @returns {string} - The current git HEAD commit hash
|
|
146
|
-
*/
|
|
147
|
-
export function getCurrentGitHead() {
|
|
148
|
-
try {
|
|
149
|
-
return execSync("git rev-parse HEAD", {
|
|
150
|
-
encoding: "utf8",
|
|
151
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
152
|
-
}).trim();
|
|
153
|
-
} catch (error) {
|
|
154
|
-
// Not in git repository or git command failed
|
|
155
|
-
console.warn("Failed to get git HEAD:", error.message);
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Save git HEAD to config.yaml file
|
|
162
|
-
* @param {string} gitHead - The current git HEAD commit hash
|
|
163
|
-
*/
|
|
164
|
-
export async function saveGitHeadToConfig(gitHead) {
|
|
165
|
-
if (!gitHead || process.env.NODE_ENV === "test" || process.env.BUN_TEST) {
|
|
166
|
-
return; // Skip if no git HEAD available or in test environment
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
const docSmithDir = path.join(process.cwd(), "./.aigne/doc-smith");
|
|
171
|
-
if (!existsSync(docSmithDir)) {
|
|
172
|
-
mkdirSync(docSmithDir, { recursive: true });
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const inputFilePath = path.join(docSmithDir, "config.yaml");
|
|
176
|
-
let fileContent = "";
|
|
177
|
-
|
|
178
|
-
// Read existing file content if it exists
|
|
179
|
-
if (existsSync(inputFilePath)) {
|
|
180
|
-
fileContent = await fs.readFile(inputFilePath, "utf8");
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Check if lastGitHead already exists in the file
|
|
184
|
-
const lastGitHeadRegex = /^lastGitHead:\s*.*$/m;
|
|
185
|
-
// Use yaml library to safely serialize the git head value
|
|
186
|
-
const yamlContent = yamlStringify({ lastGitHead: gitHead }).trim();
|
|
187
|
-
const newLastGitHeadLine = yamlContent;
|
|
188
|
-
|
|
189
|
-
if (lastGitHeadRegex.test(fileContent)) {
|
|
190
|
-
// Replace existing lastGitHead line
|
|
191
|
-
fileContent = fileContent.replace(lastGitHeadRegex, newLastGitHeadLine);
|
|
192
|
-
} else {
|
|
193
|
-
// Add lastGitHead to the end of file
|
|
194
|
-
if (fileContent && !fileContent.endsWith("\n")) {
|
|
195
|
-
fileContent += "\n";
|
|
196
|
-
}
|
|
197
|
-
fileContent += `${newLastGitHeadLine}\n`;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
await fs.writeFile(inputFilePath, fileContent);
|
|
201
|
-
} catch (error) {
|
|
202
|
-
console.warn("Failed to save git HEAD to config.yaml:", error.message);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Check if files have been modified between two git commits
|
|
208
|
-
* @param {string} fromCommit - Starting commit hash
|
|
209
|
-
* @param {string} toCommit - Ending commit hash (defaults to HEAD)
|
|
210
|
-
* @param {Array<string>} filePaths - Array of file paths to check
|
|
211
|
-
* @returns {Array<string>} - Array of modified file paths
|
|
212
|
-
*/
|
|
213
|
-
export function getModifiedFilesBetweenCommits(fromCommit, toCommit = "HEAD", filePaths = []) {
|
|
214
|
-
try {
|
|
215
|
-
// Get all modified files between commits
|
|
216
|
-
const modifiedFiles = execSync(`git diff --name-only ${fromCommit}..${toCommit}`, {
|
|
217
|
-
encoding: "utf8",
|
|
218
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
219
|
-
})
|
|
220
|
-
.trim()
|
|
221
|
-
.split("\n")
|
|
222
|
-
.filter(Boolean);
|
|
223
|
-
|
|
224
|
-
// Filter to only include files we care about
|
|
225
|
-
if (filePaths.length === 0) {
|
|
226
|
-
return modifiedFiles;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return modifiedFiles.filter((file) =>
|
|
230
|
-
filePaths.some((targetPath) => {
|
|
231
|
-
const absoluteFile = normalizePath(file);
|
|
232
|
-
const absoluteTarget = normalizePath(targetPath);
|
|
233
|
-
return absoluteFile === absoluteTarget;
|
|
234
|
-
}),
|
|
235
|
-
);
|
|
236
|
-
} catch (error) {
|
|
237
|
-
console.warn(
|
|
238
|
-
`Failed to get modified files between ${fromCommit} and ${toCommit}:`,
|
|
239
|
-
error.message,
|
|
240
|
-
);
|
|
241
|
-
return [];
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Check if any source files have changed based on modified files list
|
|
247
|
-
* @param {Array<string>} sourceIds - Source file paths
|
|
248
|
-
* @param {Array<string>} modifiedFiles - List of modified files between commits
|
|
249
|
-
* @returns {boolean} - True if any source files have changed
|
|
250
|
-
*/
|
|
251
|
-
export function hasSourceFilesChanged(sourceIds, modifiedFiles) {
|
|
252
|
-
if (!sourceIds || sourceIds.length === 0 || !modifiedFiles) {
|
|
253
|
-
return false; // No source files or no modified files
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return modifiedFiles.some((modifiedFile) =>
|
|
257
|
-
sourceIds.some((sourceId) => {
|
|
258
|
-
const absoluteModifiedFile = normalizePath(modifiedFile);
|
|
259
|
-
const absoluteSourceId = normalizePath(sourceId);
|
|
260
|
-
return absoluteModifiedFile === absoluteSourceId;
|
|
261
|
-
}),
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Check if there are any added or deleted files between two git commits that match the include/exclude patterns
|
|
267
|
-
* @param {string} fromCommit - Starting commit hash
|
|
268
|
-
* @param {string} toCommit - Ending commit hash (defaults to HEAD)
|
|
269
|
-
* @param {Array<string>} includePatterns - Include patterns to match files
|
|
270
|
-
* @param {Array<string>} excludePatterns - Exclude patterns to filter files
|
|
271
|
-
* @returns {boolean} - True if there are relevant added/deleted files
|
|
272
|
-
*/
|
|
273
|
-
export function hasFileChangesBetweenCommits(
|
|
274
|
-
fromCommit,
|
|
275
|
-
toCommit = "HEAD",
|
|
276
|
-
includePatterns = DEFAULT_INCLUDE_PATTERNS,
|
|
277
|
-
excludePatterns = DEFAULT_EXCLUDE_PATTERNS,
|
|
278
|
-
) {
|
|
279
|
-
try {
|
|
280
|
-
// Get file changes with status (A=added, D=deleted, M=modified)
|
|
281
|
-
const changes = execSync(`git diff --name-status ${fromCommit}..${toCommit}`, {
|
|
282
|
-
encoding: "utf8",
|
|
283
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
284
|
-
})
|
|
285
|
-
.trim()
|
|
286
|
-
.split("\n")
|
|
287
|
-
.filter(Boolean);
|
|
288
|
-
|
|
289
|
-
// Only check for added (A) and deleted (D) files
|
|
290
|
-
const addedOrDeletedFiles = changes
|
|
291
|
-
.filter((line) => {
|
|
292
|
-
const [status, filePath] = line.split(/\s+/);
|
|
293
|
-
return (status === "A" || status === "D") && filePath;
|
|
294
|
-
})
|
|
295
|
-
.map((line) => line.split(/\s+/)[1]);
|
|
296
|
-
|
|
297
|
-
if (addedOrDeletedFiles.length === 0) {
|
|
298
|
-
return false;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Check if any of the added/deleted files match the include patterns and don't match exclude patterns
|
|
302
|
-
return addedOrDeletedFiles.some((filePath) => {
|
|
303
|
-
// Check if file matches any include pattern
|
|
304
|
-
const matchesInclude = includePatterns.some((pattern) => {
|
|
305
|
-
// Skip empty patterns
|
|
306
|
-
if (!pattern || !pattern.trim()) {
|
|
307
|
-
return false;
|
|
308
|
-
}
|
|
309
|
-
// First escape all regex special characters except * and ?
|
|
310
|
-
const escapedPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
311
|
-
// Then convert glob wildcards to regex
|
|
312
|
-
const regexPattern = escapedPattern.replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
313
|
-
// Only create regex if pattern is not empty
|
|
314
|
-
if (!regexPattern) {
|
|
315
|
-
return false;
|
|
316
|
-
}
|
|
317
|
-
const regex = new RegExp(regexPattern);
|
|
318
|
-
return regex.test(filePath);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
if (!matchesInclude) {
|
|
322
|
-
return false;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Check if file matches any exclude pattern
|
|
326
|
-
const matchesExclude = excludePatterns.some((pattern) => {
|
|
327
|
-
// Skip empty patterns
|
|
328
|
-
if (!pattern || !pattern.trim()) {
|
|
329
|
-
return false;
|
|
330
|
-
}
|
|
331
|
-
// First escape all regex special characters except * and ?
|
|
332
|
-
const escapedPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
333
|
-
// Then convert glob wildcards to regex
|
|
334
|
-
const regexPattern = escapedPattern.replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
335
|
-
// Only create regex if pattern is not empty
|
|
336
|
-
if (!regexPattern) {
|
|
337
|
-
return false;
|
|
338
|
-
}
|
|
339
|
-
const regex = new RegExp(regexPattern);
|
|
340
|
-
return regex.test(filePath);
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
return !matchesExclude;
|
|
344
|
-
});
|
|
345
|
-
} catch (error) {
|
|
346
|
-
console.warn(
|
|
347
|
-
`Failed to check file changes between ${fromCommit} and ${toCommit}:`,
|
|
348
|
-
error.message,
|
|
349
|
-
);
|
|
350
|
-
return false;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Load config from config.yaml file
|
|
356
|
-
* @returns {Promise<Object|null>} - The config object or null if file doesn't exist
|
|
357
|
-
*/
|
|
358
|
-
export async function loadConfigFromFile() {
|
|
359
|
-
const configPath = path.join(process.cwd(), "./.aigne/doc-smith", "config.yaml");
|
|
360
|
-
|
|
361
|
-
try {
|
|
362
|
-
if (!existsSync(configPath)) {
|
|
363
|
-
return null;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const configContent = await fs.readFile(configPath, "utf8");
|
|
367
|
-
return parse(configContent);
|
|
368
|
-
} catch (error) {
|
|
369
|
-
console.warn("Failed to read config file:", error.message);
|
|
370
|
-
return null;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Save value to config.yaml file
|
|
376
|
-
* @param {string} key - The config key to save
|
|
377
|
-
* @param {string|Array} value - The value to save (can be string or array)
|
|
378
|
-
* @param {string} [comment] - Optional comment to add above the key
|
|
379
|
-
*/
|
|
380
|
-
/**
|
|
381
|
-
* Handle array value formatting and updating in YAML config
|
|
382
|
-
* @param {string} key - The configuration key
|
|
383
|
-
* @param {Array} value - The array value to save
|
|
384
|
-
* @param {string} comment - Optional comment
|
|
385
|
-
* @param {string} fileContent - Current file content
|
|
386
|
-
* @returns {string} Updated file content
|
|
387
|
-
*/
|
|
388
|
-
function handleArrayValueUpdate(key, value, comment, fileContent) {
|
|
389
|
-
// Skip if key is empty to avoid "Empty regular expressions are not allowed" error
|
|
390
|
-
if (!key || !key.trim()) {
|
|
391
|
-
return fileContent;
|
|
392
|
-
}
|
|
393
|
-
// Use yaml library to safely serialize the key-value pair
|
|
394
|
-
const yamlObject = { [key]: value };
|
|
395
|
-
const yamlContent = yamlStringify(yamlObject).trim();
|
|
396
|
-
const formattedValue = yamlContent;
|
|
397
|
-
|
|
398
|
-
const lines = fileContent.split("\n");
|
|
399
|
-
|
|
400
|
-
// Find the start line of the key
|
|
401
|
-
const keyStartIndex = lines.findIndex((line) => line.match(new RegExp(`^${key}:\\s*`)));
|
|
402
|
-
|
|
403
|
-
if (keyStartIndex !== -1) {
|
|
404
|
-
// Find the end of the array (next non-indented line or end of file)
|
|
405
|
-
let keyEndIndex = keyStartIndex;
|
|
406
|
-
for (let i = keyStartIndex + 1; i < lines.length; i++) {
|
|
407
|
-
const line = lines[i].trim();
|
|
408
|
-
// If line is empty, starts with comment, or doesn't start with "- ", it's not part of the array
|
|
409
|
-
if (line === "" || line.startsWith("#") || (!line.startsWith("- ") && !line.match(/^\w+:/))) {
|
|
410
|
-
if (!line.startsWith("- ")) {
|
|
411
|
-
keyEndIndex = i - 1;
|
|
412
|
-
break;
|
|
413
|
-
}
|
|
414
|
-
} else if (line.match(/^\w+:/)) {
|
|
415
|
-
// Found another key, stop here
|
|
416
|
-
keyEndIndex = i - 1;
|
|
417
|
-
break;
|
|
418
|
-
} else if (line.startsWith("- ")) {
|
|
419
|
-
keyEndIndex = i;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// If we reached the end of file
|
|
424
|
-
if (keyEndIndex === keyStartIndex) {
|
|
425
|
-
// Check if the value is on the same line
|
|
426
|
-
const keyLine = lines[keyStartIndex];
|
|
427
|
-
if (keyLine.includes("[") || !keyLine.endsWith(":")) {
|
|
428
|
-
keyEndIndex = keyStartIndex;
|
|
429
|
-
} else {
|
|
430
|
-
// Find the actual end of the array
|
|
431
|
-
for (let i = keyStartIndex + 1; i < lines.length; i++) {
|
|
432
|
-
const line = lines[i].trim();
|
|
433
|
-
if (line.startsWith("- ")) {
|
|
434
|
-
keyEndIndex = i;
|
|
435
|
-
} else if (line !== "" && !line.startsWith("#")) {
|
|
436
|
-
break;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// Replace the entire array section
|
|
443
|
-
const replacementLines = formattedValue.split("\n");
|
|
444
|
-
lines.splice(keyStartIndex, keyEndIndex - keyStartIndex + 1, ...replacementLines);
|
|
445
|
-
|
|
446
|
-
// Add comment if provided and not already present
|
|
447
|
-
if (comment && keyStartIndex > 0 && !lines[keyStartIndex - 1].trim().startsWith("# ")) {
|
|
448
|
-
lines.splice(keyStartIndex, 0, `# ${comment}`);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
return lines.join("\n");
|
|
452
|
-
} else {
|
|
453
|
-
// Add new array to end of file
|
|
454
|
-
let updatedContent = fileContent;
|
|
455
|
-
if (updatedContent && !updatedContent.endsWith("\n")) {
|
|
456
|
-
updatedContent += "\n";
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// Add comment if provided
|
|
460
|
-
if (comment) {
|
|
461
|
-
updatedContent += `# ${comment}\n`;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
updatedContent += `${formattedValue}\n`;
|
|
465
|
-
return updatedContent;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* Handle string value formatting and updating in YAML config
|
|
471
|
-
* @param {string} key - The configuration key
|
|
472
|
-
* @param {string} value - The string value to save
|
|
473
|
-
* @param {string} comment - Optional comment
|
|
474
|
-
* @param {string} fileContent - Current file content
|
|
475
|
-
* @returns {string} Updated file content
|
|
476
|
-
*/
|
|
477
|
-
function handleStringValueUpdate(key, value, comment, fileContent) {
|
|
478
|
-
// Skip if key is empty to avoid "Empty regular expressions are not allowed" error
|
|
479
|
-
if (!key || !key.trim()) {
|
|
480
|
-
return fileContent;
|
|
481
|
-
}
|
|
482
|
-
// Use yaml library to safely serialize the key-value pair
|
|
483
|
-
const yamlObject = { [key]: value };
|
|
484
|
-
const yamlContent = yamlStringify(yamlObject).trim();
|
|
485
|
-
const formattedValue = yamlContent;
|
|
486
|
-
const lines = fileContent.split("\n");
|
|
487
|
-
|
|
488
|
-
// Handle string values (original logic)
|
|
489
|
-
const keyRegex = new RegExp(`^${key}:\\s*.*$`);
|
|
490
|
-
const keyIndex = lines.findIndex((line) => keyRegex.test(line));
|
|
491
|
-
|
|
492
|
-
if (keyIndex !== -1) {
|
|
493
|
-
// Replace existing key line
|
|
494
|
-
lines[keyIndex] = formattedValue;
|
|
495
|
-
|
|
496
|
-
// Add comment if provided and not already present
|
|
497
|
-
if (comment) {
|
|
498
|
-
const hasCommentAbove = keyIndex > 0 && lines[keyIndex - 1].trim().startsWith("# ");
|
|
499
|
-
if (!hasCommentAbove) {
|
|
500
|
-
// Add comment above the key if it doesn't already have one
|
|
501
|
-
lines.splice(keyIndex, 0, `# ${comment}`);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
return lines.join("\n");
|
|
506
|
-
} else {
|
|
507
|
-
// Add key to the end of file
|
|
508
|
-
let updatedContent = fileContent;
|
|
509
|
-
if (updatedContent && !updatedContent.endsWith("\n")) {
|
|
510
|
-
updatedContent += "\n";
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// Add comment if provided
|
|
514
|
-
if (comment) {
|
|
515
|
-
updatedContent += `# ${comment}\n`;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
updatedContent += `${formattedValue}\n`;
|
|
519
|
-
return updatedContent;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
export async function saveValueToConfig(key, value, comment) {
|
|
524
|
-
if (value === undefined) {
|
|
525
|
-
return; // Skip if value is undefined
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
try {
|
|
529
|
-
const docSmithDir = path.join(process.cwd(), "./.aigne/doc-smith");
|
|
530
|
-
if (!existsSync(docSmithDir)) {
|
|
531
|
-
mkdirSync(docSmithDir, { recursive: true });
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
const configPath = path.join(docSmithDir, "config.yaml");
|
|
535
|
-
let fileContent = "";
|
|
536
|
-
|
|
537
|
-
// Read existing file content if it exists
|
|
538
|
-
if (existsSync(configPath)) {
|
|
539
|
-
fileContent = await fs.readFile(configPath, "utf8");
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// Use extracted helper functions for better maintainability
|
|
543
|
-
let updatedContent;
|
|
544
|
-
if (Array.isArray(value)) {
|
|
545
|
-
updatedContent = handleArrayValueUpdate(key, value, comment, fileContent);
|
|
546
|
-
} else {
|
|
547
|
-
updatedContent = handleStringValueUpdate(key, value, comment, fileContent);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
await fs.writeFile(configPath, updatedContent);
|
|
551
|
-
} catch (error) {
|
|
552
|
-
console.warn(`Failed to save ${key} to config.yaml:`, error.message);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
/**
|
|
557
|
-
* Validate if a path exists and is accessible
|
|
558
|
-
* @param {string} filePath - The path to validate (can be absolute or relative)
|
|
559
|
-
* @returns {Object} - Validation result with isValid boolean and error message
|
|
560
|
-
*/
|
|
561
|
-
export function validatePath(filePath) {
|
|
562
|
-
try {
|
|
563
|
-
const absolutePath = normalizePath(filePath);
|
|
564
|
-
|
|
565
|
-
// Check if path exists
|
|
566
|
-
if (!existsSync(absolutePath)) {
|
|
567
|
-
return {
|
|
568
|
-
isValid: false,
|
|
569
|
-
error: `Path does not exist: ${filePath}`,
|
|
570
|
-
};
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// Check if path is accessible (readable)
|
|
574
|
-
try {
|
|
575
|
-
accessSync(absolutePath, constants.R_OK);
|
|
576
|
-
} catch (_accessError) {
|
|
577
|
-
return {
|
|
578
|
-
isValid: false,
|
|
579
|
-
error: `Path is not accessible: ${filePath}`,
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
return {
|
|
584
|
-
isValid: true,
|
|
585
|
-
error: null,
|
|
586
|
-
};
|
|
587
|
-
} catch (_error) {
|
|
588
|
-
return {
|
|
589
|
-
isValid: false,
|
|
590
|
-
error: `Invalid path format: ${filePath}`,
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
/**
|
|
596
|
-
* Validate multiple paths and return validation results
|
|
597
|
-
* @param {Array<string>} paths - Array of paths to validate
|
|
598
|
-
* @returns {Object} - Validation results with validPaths array and errors array
|
|
599
|
-
*/
|
|
600
|
-
export function validatePaths(paths) {
|
|
601
|
-
const validPaths = [];
|
|
602
|
-
const errors = [];
|
|
603
|
-
|
|
604
|
-
for (const path of paths) {
|
|
605
|
-
const validation = validatePath(path);
|
|
606
|
-
if (validation.isValid) {
|
|
607
|
-
validPaths.push(path);
|
|
608
|
-
} else {
|
|
609
|
-
errors.push({
|
|
610
|
-
path: path,
|
|
611
|
-
error: validation.error,
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
return {
|
|
617
|
-
validPaths,
|
|
618
|
-
errors,
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
/**
|
|
623
|
-
* Check if input is a valid directory or file and add it to results if so
|
|
624
|
-
* @param {string} searchTerm - The search term to check
|
|
625
|
-
* @param {Array} results - The results array to modify
|
|
626
|
-
*/
|
|
627
|
-
function addExactPathMatch(searchTerm, results) {
|
|
628
|
-
const inputValidation = validatePath(searchTerm);
|
|
629
|
-
if (inputValidation.isValid) {
|
|
630
|
-
const stats = statSync(normalizePath(searchTerm));
|
|
631
|
-
const isDirectory = stats.isDirectory();
|
|
632
|
-
results.unshift({
|
|
633
|
-
name: searchTerm,
|
|
634
|
-
value: searchTerm,
|
|
635
|
-
description: isDirectory ? "📁 Directory" : "📄 File",
|
|
636
|
-
});
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
/**
|
|
641
|
-
* Get available paths for search suggestions based on user input
|
|
642
|
-
* @param {string} userInput - User's input string
|
|
643
|
-
* @returns {Array<Object>} - Array of path objects with name, value, and description
|
|
644
|
-
*/
|
|
645
|
-
export function getAvailablePaths(userInput = "") {
|
|
646
|
-
try {
|
|
647
|
-
const searchTerm = userInput.trim();
|
|
648
|
-
|
|
649
|
-
// If no input, return current directory contents
|
|
650
|
-
if (!searchTerm) {
|
|
651
|
-
return getDirectoryContents("./");
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
let results = [];
|
|
655
|
-
|
|
656
|
-
// Handle absolute paths
|
|
657
|
-
if (searchTerm.startsWith("/")) {
|
|
658
|
-
const dirPath = path.dirname(searchTerm);
|
|
659
|
-
const fileName = path.basename(searchTerm);
|
|
660
|
-
results = getDirectoryContents(dirPath, fileName);
|
|
661
|
-
addExactPathMatch(searchTerm, results);
|
|
662
|
-
}
|
|
663
|
-
// Handle relative paths
|
|
664
|
-
else if (searchTerm.startsWith("./") || searchTerm.startsWith("../")) {
|
|
665
|
-
// Extract directory path and search term
|
|
666
|
-
const lastSlashIndex = searchTerm.lastIndexOf("/");
|
|
667
|
-
if (lastSlashIndex === -1) {
|
|
668
|
-
// No slash found, treat as current directory search
|
|
669
|
-
results = getDirectoryContents("./", searchTerm);
|
|
670
|
-
addExactPathMatch(searchTerm, results);
|
|
671
|
-
} else {
|
|
672
|
-
const dirPath = searchTerm.substring(0, lastSlashIndex + 1);
|
|
673
|
-
const fileName = searchTerm.substring(lastSlashIndex + 1);
|
|
674
|
-
|
|
675
|
-
// Validate directory path
|
|
676
|
-
const validation = validatePath(dirPath);
|
|
677
|
-
if (!validation.isValid) {
|
|
678
|
-
return [
|
|
679
|
-
{
|
|
680
|
-
name: dirPath,
|
|
681
|
-
value: dirPath,
|
|
682
|
-
description: validation.error,
|
|
683
|
-
},
|
|
684
|
-
];
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
results = getDirectoryContents(dirPath, fileName);
|
|
688
|
-
addExactPathMatch(searchTerm, results);
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
// Handle simple file/directory names (search in current directory)
|
|
692
|
-
else {
|
|
693
|
-
results = getDirectoryContents("./", searchTerm);
|
|
694
|
-
addExactPathMatch(searchTerm, results);
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
// Remove duplicates based on absolute path (real deduplication)
|
|
698
|
-
const uniqueResults = [];
|
|
699
|
-
const seenAbsolutePaths = new Set();
|
|
700
|
-
|
|
701
|
-
for (const item of results) {
|
|
702
|
-
// Normalize to absolute path for proper deduplication
|
|
703
|
-
const absolutePath = normalizePath(item.value);
|
|
704
|
-
if (!seenAbsolutePaths.has(absolutePath)) {
|
|
705
|
-
seenAbsolutePaths.add(absolutePath);
|
|
706
|
-
uniqueResults.push(item);
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
return uniqueResults;
|
|
711
|
-
} catch (error) {
|
|
712
|
-
console.warn(`Failed to get available paths for "${userInput}":`, error.message);
|
|
713
|
-
return [];
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
/**
|
|
718
|
-
* Get directory contents for a specific path
|
|
719
|
-
* @param {string} dirPath - Directory path to search in
|
|
720
|
-
* @param {string} searchTerm - Optional search term to filter results
|
|
721
|
-
* @returns {Array<Object>} - Array of path objects (both files and directories)
|
|
722
|
-
*/
|
|
723
|
-
function getDirectoryContents(dirPath, searchTerm = "") {
|
|
724
|
-
try {
|
|
725
|
-
const absoluteDirPath = normalizePath(dirPath);
|
|
726
|
-
|
|
727
|
-
// Check if directory exists
|
|
728
|
-
if (!existsSync(absoluteDirPath)) {
|
|
729
|
-
return [
|
|
730
|
-
{
|
|
731
|
-
name: dirPath,
|
|
732
|
-
value: dirPath,
|
|
733
|
-
description: "Directory does not exist",
|
|
734
|
-
},
|
|
735
|
-
];
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
const items = [];
|
|
739
|
-
|
|
740
|
-
// Read directory contents
|
|
741
|
-
const entries = readdirSync(absoluteDirPath, { withFileTypes: true });
|
|
742
|
-
|
|
743
|
-
for (const entry of entries) {
|
|
744
|
-
const entryName = entry.name;
|
|
745
|
-
|
|
746
|
-
// Preserve ./ prefix when dirPath is "./"
|
|
747
|
-
let relativePath = path.join(dirPath, entryName);
|
|
748
|
-
if (dirPath?.startsWith("./")) {
|
|
749
|
-
relativePath = `./${relativePath}`;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
// Filter by search term if provided
|
|
753
|
-
if (searchTerm && !entryName.toLowerCase().includes(searchTerm.toLowerCase())) {
|
|
754
|
-
continue;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// Skip hidden files and common ignore patterns
|
|
758
|
-
if (
|
|
759
|
-
entryName.startsWith(".") ||
|
|
760
|
-
entryName === "node_modules" ||
|
|
761
|
-
entryName === ".git" ||
|
|
762
|
-
entryName === "dist" ||
|
|
763
|
-
entryName === "build"
|
|
764
|
-
) {
|
|
765
|
-
continue;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
const isDirectory = entry.isDirectory();
|
|
769
|
-
|
|
770
|
-
// Include both directories and files
|
|
771
|
-
items.push({
|
|
772
|
-
name: relativePath,
|
|
773
|
-
value: relativePath,
|
|
774
|
-
description: isDirectory ? "📁 Directory" : "📄 File",
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
// Sort alphabetically (directories first, then files)
|
|
779
|
-
items.sort((a, b) => {
|
|
780
|
-
const aIsDir = a.description === "📁 Directory";
|
|
781
|
-
const bIsDir = b.description === "📁 Directory";
|
|
782
|
-
|
|
783
|
-
if (aIsDir && !bIsDir) return -1;
|
|
784
|
-
if (!aIsDir && bIsDir) return 1;
|
|
785
|
-
|
|
786
|
-
return a.name.localeCompare(b.name);
|
|
787
|
-
});
|
|
788
|
-
|
|
789
|
-
return items;
|
|
790
|
-
} catch (error) {
|
|
791
|
-
console.warn(`Failed to get directory contents from ${dirPath}:`, error.message);
|
|
792
|
-
return [];
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
/**
|
|
797
|
-
* Get GitHub repository URL from git remote
|
|
798
|
-
* @returns {string} GitHub repository URL or empty string if not a GitHub repo (e.g. git@github.com:xxxx/xxxx.git)
|
|
799
|
-
*/
|
|
800
|
-
export function getGithubRepoUrl() {
|
|
801
|
-
try {
|
|
802
|
-
const gitRemote = execSync("git remote get-url origin", {
|
|
803
|
-
encoding: "utf8",
|
|
804
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
805
|
-
}).trim();
|
|
806
|
-
|
|
807
|
-
// Check if it's a GitHub repository
|
|
808
|
-
if (gitRemote.includes("github.com")) {
|
|
809
|
-
return gitRemote;
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
return "";
|
|
813
|
-
} catch {
|
|
814
|
-
// Not in git repository or no origin remote
|
|
815
|
-
return "";
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
/**
|
|
820
|
-
* Get GitHub repository information
|
|
821
|
-
* @param {string} repoUrl - The repository URL
|
|
822
|
-
* @returns {Promise<Object>} - Repository information
|
|
823
|
-
*/
|
|
824
|
-
export async function getGitHubRepoInfo(repoUrl) {
|
|
825
|
-
try {
|
|
826
|
-
// Extract owner and repo from GitHub URL
|
|
827
|
-
const match = repoUrl.match(/github\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
828
|
-
if (!match) return null;
|
|
829
|
-
|
|
830
|
-
const [, owner, repo] = match;
|
|
831
|
-
const apiUrl = `https://api.github.com/repos/${owner}/${repo}`;
|
|
832
|
-
|
|
833
|
-
const response = await fetch(apiUrl);
|
|
834
|
-
|
|
835
|
-
if (!response.ok) {
|
|
836
|
-
console.warn("Failed to fetch GitHub repository info:", repoUrl, response.statusText);
|
|
837
|
-
return null;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
const data = await response.json();
|
|
841
|
-
return {
|
|
842
|
-
name: data.name,
|
|
843
|
-
description: data.description || "",
|
|
844
|
-
icon: data.owner?.avatar_url || "",
|
|
845
|
-
};
|
|
846
|
-
} catch (error) {
|
|
847
|
-
console.warn("Failed to fetch GitHub repository info:", error.message);
|
|
848
|
-
return null;
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
/**
|
|
853
|
-
* Get project information automatically without user confirmation
|
|
854
|
-
* @returns {Promise<Object>} - Project information including name, description, icon, and fromGitHub flag
|
|
855
|
-
*/
|
|
856
|
-
export async function getProjectInfo() {
|
|
857
|
-
let repoInfo = null;
|
|
858
|
-
let defaultName = path.basename(process.cwd());
|
|
859
|
-
let defaultDescription = "";
|
|
860
|
-
let defaultIcon = "";
|
|
861
|
-
let fromGitHub = false;
|
|
862
|
-
|
|
863
|
-
// Check if we're in a git repository
|
|
864
|
-
try {
|
|
865
|
-
const gitRemote = execSync("git remote get-url origin", {
|
|
866
|
-
encoding: "utf8",
|
|
867
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
868
|
-
}).trim();
|
|
869
|
-
|
|
870
|
-
// Extract repository name from git remote URL
|
|
871
|
-
const repoName = gitRemote.split("/").pop().replace(".git", "");
|
|
872
|
-
defaultName = repoName;
|
|
873
|
-
|
|
874
|
-
// If it's a GitHub repository, try to get additional info
|
|
875
|
-
if (gitRemote.includes("github.com")) {
|
|
876
|
-
repoInfo = await getGitHubRepoInfo(gitRemote);
|
|
877
|
-
if (repoInfo) {
|
|
878
|
-
defaultDescription = repoInfo.description;
|
|
879
|
-
defaultIcon = repoInfo.icon;
|
|
880
|
-
fromGitHub = true;
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
} catch (_error) {
|
|
884
|
-
// Not in git repository or no origin remote, use current directory name
|
|
885
|
-
console.warn("No git repository found, using current directory name");
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
return {
|
|
889
|
-
name: defaultName,
|
|
890
|
-
description: defaultDescription,
|
|
891
|
-
icon: defaultIcon,
|
|
892
|
-
fromGitHub,
|
|
893
|
-
};
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
/**
|
|
897
|
-
* Process document purpose configuration and generate purpose rules
|
|
898
|
-
* @param {Array<string>} documentPurpose - Array of document purpose keys
|
|
899
|
-
* @returns {Object} Object containing purposes string and rules content
|
|
900
|
-
*/
|
|
901
|
-
export function processDocumentPurpose(documentPurpose) {
|
|
902
|
-
if (!documentPurpose || !Array.isArray(documentPurpose)) {
|
|
903
|
-
return { purposes: "" };
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
const purposeRules = documentPurpose
|
|
907
|
-
.map((key) => {
|
|
908
|
-
const style = DOCUMENT_STYLES[key];
|
|
909
|
-
if (!style) return null;
|
|
910
|
-
return `Document Purpose - ${style.name}:\n${style.description}\n${style.content}`;
|
|
911
|
-
})
|
|
912
|
-
.filter(Boolean);
|
|
913
|
-
|
|
914
|
-
if (purposeRules.length === 0) {
|
|
915
|
-
return { purposes: "" };
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
const purposes = purposeRules.join("\n\n");
|
|
919
|
-
return {
|
|
920
|
-
purposes,
|
|
921
|
-
};
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
/**
|
|
925
|
-
* Process target audience configuration and generate audience rules and names
|
|
926
|
-
* @param {Array<string>} targetAudienceTypes - Array of target audience type keys
|
|
927
|
-
* @param {string} existingTargetAudience - Existing target audience content
|
|
928
|
-
* @returns {Object} Object containing audiences string, targetAudience string, and rules content
|
|
929
|
-
*/
|
|
930
|
-
export function processTargetAudience(targetAudienceTypes, existingTargetAudience = "") {
|
|
931
|
-
if (!targetAudienceTypes || !Array.isArray(targetAudienceTypes)) {
|
|
932
|
-
return { audiences: "", targetAudience: existingTargetAudience || "" };
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
// Get structured content for rules
|
|
936
|
-
const audienceRules = targetAudienceTypes
|
|
937
|
-
.map((key) => {
|
|
938
|
-
const audience = TARGET_AUDIENCES[key];
|
|
939
|
-
if (!audience) return null;
|
|
940
|
-
return `Target Audience - ${audience.name}:\n${audience.description}\n${audience.content}`;
|
|
941
|
-
})
|
|
942
|
-
.filter(Boolean);
|
|
943
|
-
|
|
944
|
-
let audiences = "";
|
|
945
|
-
if (audienceRules.length > 0) {
|
|
946
|
-
audiences = audienceRules.join("\n\n");
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
// Get names for targetAudience field
|
|
950
|
-
const audienceNames = targetAudienceTypes
|
|
951
|
-
.map((key) => TARGET_AUDIENCES[key]?.name)
|
|
952
|
-
.filter(Boolean)
|
|
953
|
-
.join(", ");
|
|
954
|
-
|
|
955
|
-
let targetAudience = existingTargetAudience || "";
|
|
956
|
-
if (audienceNames) {
|
|
957
|
-
const existingTargetAudienceTrimmed = existingTargetAudience?.trim();
|
|
958
|
-
if (existingTargetAudienceTrimmed) {
|
|
959
|
-
targetAudience = `${existingTargetAudienceTrimmed}\n\n${audienceNames}`;
|
|
960
|
-
} else {
|
|
961
|
-
targetAudience = audienceNames;
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
return {
|
|
966
|
-
audiences,
|
|
967
|
-
targetAudience,
|
|
968
|
-
};
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
/**
|
|
972
|
-
* Process configuration fields - convert keys to actual content
|
|
973
|
-
* @param {Object} config - Parsed configuration
|
|
974
|
-
* @returns {Object} Processed configuration with content fields
|
|
975
|
-
*/
|
|
976
|
-
export async function processConfigFields(config) {
|
|
977
|
-
const processed = {};
|
|
978
|
-
const allRulesContent = [];
|
|
979
|
-
|
|
980
|
-
// Set default values for missing or empty fields
|
|
981
|
-
const defaults = {
|
|
982
|
-
nodeName: "Section",
|
|
983
|
-
locale: "en",
|
|
984
|
-
sourcesPath: ["./"],
|
|
985
|
-
docsDir: "./.aigne/doc-smith/docs",
|
|
986
|
-
outputDir: "./.aigne/doc-smith/output",
|
|
987
|
-
translateLanguages: [],
|
|
988
|
-
rules: "",
|
|
989
|
-
targetAudience: "",
|
|
990
|
-
};
|
|
991
|
-
|
|
992
|
-
// Apply defaults for missing or empty fields
|
|
993
|
-
for (const [key, defaultValue] of Object.entries(defaults)) {
|
|
994
|
-
if (
|
|
995
|
-
!config[key] ||
|
|
996
|
-
(Array.isArray(defaultValue) && (!config[key] || config[key].length === 0)) ||
|
|
997
|
-
(typeof defaultValue === "string" && (!config[key] || config[key].trim() === ""))
|
|
998
|
-
) {
|
|
999
|
-
processed[key] = defaultValue;
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
// Check if original rules field has content
|
|
1004
|
-
if (config.rules) {
|
|
1005
|
-
if (typeof config.rules === "string") {
|
|
1006
|
-
const existingRules = config.rules.trim();
|
|
1007
|
-
if (existingRules) {
|
|
1008
|
-
// load rules from remote url
|
|
1009
|
-
if (isRemoteFile(existingRules)) {
|
|
1010
|
-
const remoteFileContent = await getRemoteFileContent(existingRules);
|
|
1011
|
-
if (remoteFileContent) {
|
|
1012
|
-
allRulesContent.push(remoteFileContent);
|
|
1013
|
-
}
|
|
1014
|
-
} else {
|
|
1015
|
-
allRulesContent.push(existingRules);
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
} else if (Array.isArray(config.rules)) {
|
|
1019
|
-
// Handle array of rules - join them with newlines
|
|
1020
|
-
const rulesText = config.rules
|
|
1021
|
-
.filter((rule) => typeof rule === "string" && rule.trim())
|
|
1022
|
-
.join("\n\n");
|
|
1023
|
-
if (rulesText) {
|
|
1024
|
-
allRulesContent.push(rulesText);
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
// Process document purpose (array)
|
|
1030
|
-
const documentPurposeResult = processDocumentPurpose(config.documentPurpose);
|
|
1031
|
-
if (documentPurposeResult.purposes) {
|
|
1032
|
-
allRulesContent.push(documentPurposeResult.purposes);
|
|
1033
|
-
processed.purposes = documentPurposeResult.purposes;
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
// Process target audience types (array)
|
|
1037
|
-
const targetAudienceResult = processTargetAudience(
|
|
1038
|
-
config.targetAudienceTypes,
|
|
1039
|
-
config.targetAudience,
|
|
1040
|
-
);
|
|
1041
|
-
if (targetAudienceResult.audiences) {
|
|
1042
|
-
allRulesContent.push(targetAudienceResult.audiences);
|
|
1043
|
-
processed.audiences = targetAudienceResult.audiences;
|
|
1044
|
-
}
|
|
1045
|
-
if (targetAudienceResult.targetAudience) {
|
|
1046
|
-
processed.targetAudience = targetAudienceResult.targetAudience;
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
// Process reader knowledge level (single value)
|
|
1050
|
-
let knowledgeContent = "";
|
|
1051
|
-
if (config.readerKnowledgeLevel) {
|
|
1052
|
-
const knowledgeLevel = READER_KNOWLEDGE_LEVELS[config.readerKnowledgeLevel];
|
|
1053
|
-
if (knowledgeLevel) {
|
|
1054
|
-
knowledgeContent = knowledgeLevel.content;
|
|
1055
|
-
processed.readerKnowledgeContent = knowledgeContent;
|
|
1056
|
-
allRulesContent.push(`Reader Knowledge Level:\n${knowledgeContent}`);
|
|
1057
|
-
|
|
1058
|
-
processed.readerKnowledgeLevel = `Reader Knowledge Level - ${knowledgeLevel.name} : ${knowledgeLevel.description} \n${knowledgeContent}`;
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
// Process documentation depth (single value)
|
|
1063
|
-
let depthContent = "";
|
|
1064
|
-
if (config.documentationDepth) {
|
|
1065
|
-
const depthLevel = DOCUMENTATION_DEPTH[config.documentationDepth];
|
|
1066
|
-
if (depthLevel) {
|
|
1067
|
-
depthContent = depthLevel.content;
|
|
1068
|
-
processed.documentationDepthContent = depthContent;
|
|
1069
|
-
allRulesContent.push(`Documentation Depth:\n${depthContent}`);
|
|
1070
|
-
|
|
1071
|
-
processed.coverageDepth = `Documentation Depth - ${depthLevel.name} : ${depthLevel.description} \n${depthContent}`;
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
if (config.glossary) {
|
|
1076
|
-
if (isRemoteFile(config.glossary)) {
|
|
1077
|
-
processed.glossary = await getRemoteFileContent(config.glossary);
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
// Detect and handle conflicts in user selections
|
|
1082
|
-
const conflicts = detectResolvableConflicts(config);
|
|
1083
|
-
if (conflicts.length > 0) {
|
|
1084
|
-
const conflictResolutionRules = generateConflictResolutionRules(conflicts);
|
|
1085
|
-
allRulesContent.push(conflictResolutionRules);
|
|
1086
|
-
|
|
1087
|
-
// Store conflict information for debugging/logging
|
|
1088
|
-
processed.detectedConflicts = conflicts;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
// Combine all content into rules field
|
|
1092
|
-
if (allRulesContent.length > 0) {
|
|
1093
|
-
processed.rules = allRulesContent.join("\n\n");
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
return processed;
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
/**
|
|
1100
|
-
* Recursively resolves file references in a configuration object.
|
|
1101
|
-
*
|
|
1102
|
-
* This function traverses the input object, array, or string recursively. Any string value that starts
|
|
1103
|
-
* with '@' is treated as a file reference, and the file's content is loaded asynchronously. Supported
|
|
1104
|
-
* file formats include .txt, .md, .json, .yaml, and .yml. For .json and .yaml/.yml files, the content
|
|
1105
|
-
* is parsed into objects; for .txt and .md, the raw string is returned.
|
|
1106
|
-
*
|
|
1107
|
-
* If a file cannot be loaded (e.g., does not exist, is of unsupported type, or parsing fails), the
|
|
1108
|
-
* original string value (with '@' prefix) is returned in place of the file content.
|
|
1109
|
-
*
|
|
1110
|
-
* The function processes nested arrays and objects recursively, returning a new structure with file
|
|
1111
|
-
* contents loaded in place of references. The input object is not mutated.
|
|
1112
|
-
*
|
|
1113
|
-
* Examples of supported file reference formats:
|
|
1114
|
-
* - "@notes.txt"
|
|
1115
|
-
* - "@docs/readme.md"
|
|
1116
|
-
* - "@config/settings.json"
|
|
1117
|
-
* - "@data.yaml"
|
|
1118
|
-
*
|
|
1119
|
-
* @param {any} obj - The configuration object, array, or string to process.
|
|
1120
|
-
* @param {string} basePath - Base path for resolving relative file paths (defaults to process.cwd()).
|
|
1121
|
-
* @returns {Promise<any>} - The processed configuration with file content loaded in place of references.
|
|
1122
|
-
*/
|
|
1123
|
-
export async function resolveFileReferences(obj, basePath = process.cwd()) {
|
|
1124
|
-
if (typeof obj === "string") {
|
|
1125
|
-
if (obj.startsWith("@")) {
|
|
1126
|
-
return await loadFileContent(obj.slice(1), basePath);
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
if (Array.isArray(obj)) {
|
|
1131
|
-
return Promise.all(obj.map((item) => resolveFileReferences(item, basePath)));
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
if (obj && typeof obj === "object") {
|
|
1135
|
-
const result = {};
|
|
1136
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
1137
|
-
result[key] = await resolveFileReferences(value, basePath);
|
|
1138
|
-
}
|
|
1139
|
-
return result;
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
return obj;
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
/**
|
|
1146
|
-
* Load content from a file path
|
|
1147
|
-
* @param {string} filePath - The file path to load
|
|
1148
|
-
* @param {string} basePath - Base path for resolving relative paths
|
|
1149
|
-
* @returns {Promise<any>} - The loaded content or original path if loading fails
|
|
1150
|
-
*/
|
|
1151
|
-
export async function loadFileContent(filePath, basePath) {
|
|
1152
|
-
try {
|
|
1153
|
-
// Resolve path - if absolute, use as is; if relative, resolve from basePath
|
|
1154
|
-
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(basePath, filePath);
|
|
1155
|
-
|
|
1156
|
-
// Check if file exists
|
|
1157
|
-
if (!existsSync(resolvedPath)) {
|
|
1158
|
-
return `@${filePath}`; // Return original value if file doesn't exist
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
// Check file extension
|
|
1162
|
-
const ext = path.extname(resolvedPath).toLowerCase();
|
|
1163
|
-
|
|
1164
|
-
if (!SUPPORTED_FILE_EXTENSIONS.includes(ext)) {
|
|
1165
|
-
return `@${filePath}`; // Return original value if unsupported file type
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
// Read file content
|
|
1169
|
-
const content = await fs.readFile(resolvedPath, "utf-8");
|
|
1170
|
-
|
|
1171
|
-
// Parse JSON/YAML files
|
|
1172
|
-
if (ext === ".json") {
|
|
1173
|
-
try {
|
|
1174
|
-
return JSON.parse(content);
|
|
1175
|
-
} catch {
|
|
1176
|
-
return content; // Return raw string if JSON parsing fails
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
if (ext === ".yaml" || ext === ".yml") {
|
|
1181
|
-
try {
|
|
1182
|
-
return parse(content);
|
|
1183
|
-
} catch {
|
|
1184
|
-
return content; // Return raw string if YAML parsing fails
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
// Return raw content for .txt and .md files
|
|
1189
|
-
return content;
|
|
1190
|
-
} catch {
|
|
1191
|
-
// Return original value if any error occurs
|
|
1192
|
-
return `@${filePath}`;
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
/**
|
|
1197
|
-
* Detect system language and map to supported language code
|
|
1198
|
-
* @returns {string} - Supported language code (defaults to 'en' if detection fails or unsupported)
|
|
1199
|
-
*/
|
|
1200
|
-
export function detectSystemLanguage() {
|
|
1201
|
-
try {
|
|
1202
|
-
// Try multiple methods to detect system language
|
|
1203
|
-
let systemLocale = null;
|
|
1204
|
-
|
|
1205
|
-
// Method 1: Environment variables (most reliable on Unix systems)
|
|
1206
|
-
systemLocale = process.env.LANG || process.env.LANGUAGE || process.env.LC_ALL;
|
|
1207
|
-
|
|
1208
|
-
// Method 2: Node.js Intl API (fallback)
|
|
1209
|
-
if (!systemLocale) {
|
|
1210
|
-
try {
|
|
1211
|
-
systemLocale = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
1212
|
-
} catch (_error) {
|
|
1213
|
-
// Intl API failed, continue to fallback
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
if (!systemLocale) {
|
|
1218
|
-
return "en"; // Default fallback
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
// Extract language code from locale (e.g., 'zh_CN' -> 'zh', 'en_US' -> 'en')
|
|
1222
|
-
const langCode = systemLocale.split(/[-_]/)[0].toLowerCase();
|
|
1223
|
-
|
|
1224
|
-
// Map to supported language codes
|
|
1225
|
-
const supportedLang = SUPPORTED_LANGUAGES.find((lang) => lang.code === langCode);
|
|
1226
|
-
if (supportedLang) {
|
|
1227
|
-
return supportedLang.code;
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
// Handle special cases for Chinese variants
|
|
1231
|
-
if (langCode === "zh") {
|
|
1232
|
-
// Check for Traditional Chinese indicators
|
|
1233
|
-
const fullLocale = systemLocale.toLowerCase();
|
|
1234
|
-
if (fullLocale.includes("tw") || fullLocale.includes("hk") || fullLocale.includes("mo")) {
|
|
1235
|
-
return "zh-TW";
|
|
1236
|
-
}
|
|
1237
|
-
return "zh"; // Default to Simplified Chinese
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
// Return default if no match found
|
|
1241
|
-
return "en";
|
|
1242
|
-
} catch (_error) {
|
|
1243
|
-
// Any error in detection, return default
|
|
1244
|
-
return "en";
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
export function getContentHash(str, { trim = true } = {}) {
|
|
1249
|
-
const input = trim && typeof str === "string" ? str.trim() : str;
|
|
1250
|
-
return crypto.createHash("sha256").update(input).digest("hex");
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
function toPath(path) {
|
|
1254
|
-
if (Array.isArray(path)) return path;
|
|
1255
|
-
|
|
1256
|
-
const result = [];
|
|
1257
|
-
path.replace(/[^.[\]]+|\[(\d+|(["'])(.*?)\2)\]/g, (match, bracketContent, quote, quotedKey) => {
|
|
1258
|
-
if (quote) {
|
|
1259
|
-
// ["key"] or ['key']
|
|
1260
|
-
result.push(quotedKey);
|
|
1261
|
-
} else if (bracketContent !== undefined) {
|
|
1262
|
-
// [123]
|
|
1263
|
-
result.push(bracketContent);
|
|
1264
|
-
} else {
|
|
1265
|
-
// dot notation
|
|
1266
|
-
result.push(match);
|
|
1267
|
-
}
|
|
1268
|
-
});
|
|
1269
|
-
return result;
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
/**
|
|
1273
|
-
* Deeply get the value at a given path from an object, or return a default value if missing.
|
|
1274
|
-
* @param {object} obj - The object to query.
|
|
1275
|
-
* @param {string|Array<string|number>} path - The path to get, as a string or array.
|
|
1276
|
-
* @param {*} defaultValue - The value returned if the resolved value is undefined.
|
|
1277
|
-
* @returns {*} The value at the path or defaultValue.
|
|
1278
|
-
*/
|
|
1279
|
-
export function dget(obj, path, defaultValue) {
|
|
1280
|
-
const parts = toPath(path);
|
|
1281
|
-
|
|
1282
|
-
let current = obj;
|
|
1283
|
-
for (const key of parts) {
|
|
1284
|
-
if (current == null || !(key in current)) return defaultValue;
|
|
1285
|
-
current = current[key];
|
|
1286
|
-
}
|
|
1287
|
-
return current;
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
/**
|
|
1291
|
-
* Deeply set the value at a given path in an object.
|
|
1292
|
-
* @param {object} obj - The object to modify.
|
|
1293
|
-
* @param {string|Array<string|number>} path - The path to set, as a string or array.
|
|
1294
|
-
* @param {*} value - The value to set.
|
|
1295
|
-
* @returns {object} The modified object.
|
|
1296
|
-
*/
|
|
1297
|
-
export function dset(obj, path, value) {
|
|
1298
|
-
const parts = toPath(path);
|
|
1299
|
-
|
|
1300
|
-
let current = obj;
|
|
1301
|
-
for (let i = 0; i < parts.length; i++) {
|
|
1302
|
-
const key = parts[i];
|
|
1303
|
-
|
|
1304
|
-
if (i === parts.length - 1) {
|
|
1305
|
-
current[key] = value;
|
|
1306
|
-
} else {
|
|
1307
|
-
if (current[key] == null || typeof current[key] !== "object") {
|
|
1308
|
-
current[key] = String(parts[i + 1]).match(/^\d+$/) ? [] : {};
|
|
1309
|
-
}
|
|
1310
|
-
current = current[key];
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
return obj;
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
/**
|
|
1317
|
-
* Create a context path manager that provides get/set/clear operations
|
|
1318
|
-
* @param {object} options - The options object containing user context
|
|
1319
|
-
* @param {string} path - The context path (e.g., 'currentPageDetails./about' or 'lastToolInputs./about')
|
|
1320
|
-
* @returns {object} An object with { get, set } methods and a contextPath method for sub-paths
|
|
1321
|
-
*/
|
|
1322
|
-
export function userContextAt(options, path) {
|
|
1323
|
-
const userContext = options?.context?.userContext || null;
|
|
1324
|
-
if (!userContext) {
|
|
1325
|
-
throw new Error("userContext is not available");
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
return {
|
|
1329
|
-
/**
|
|
1330
|
-
* Get a value from the context path
|
|
1331
|
-
* @param {string} [key] - Optional key for nested access (e.g., 'updateMeta' for lastToolInputs)
|
|
1332
|
-
* @returns {*} The value at the path, or undefined if not found
|
|
1333
|
-
*/
|
|
1334
|
-
get(key) {
|
|
1335
|
-
if (key !== undefined) {
|
|
1336
|
-
return dget(userContext, `${path}.${key}`);
|
|
1337
|
-
}
|
|
1338
|
-
return dget(userContext, path);
|
|
1339
|
-
},
|
|
1340
|
-
|
|
1341
|
-
/**
|
|
1342
|
-
* Set a value in the context path
|
|
1343
|
-
* @param {string|*} key - If key is provided, this is the key; otherwise this is the value
|
|
1344
|
-
* @param {*} [value] - The value to set (required if first param is a key)
|
|
1345
|
-
*/
|
|
1346
|
-
set(key, value) {
|
|
1347
|
-
if (value !== undefined) {
|
|
1348
|
-
dset(userContext, `${path}.${key}`, value);
|
|
1349
|
-
} else {
|
|
1350
|
-
dset(userContext, path, key);
|
|
1351
|
-
}
|
|
1352
|
-
},
|
|
1353
|
-
};
|
|
1354
|
-
}
|