ctxman 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ctxman/custom-profiles.example.json +20 -0
- package/.ctxman/llm-profiles.json +131 -0
- package/.ctxman/logs/ctxman-2026-07-02.log +156 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
- package/.ctxman/wizard-profiles/code-review/.contextignore +41 -0
- package/.ctxman/wizard-profiles/code-review/.contextinclude +30 -0
- package/.ctxman/wizard-profiles/code-review/.methodignore +26 -0
- package/.ctxman/wizard-profiles/code-review/.methodinclude +47 -0
- package/.ctxman/wizard-profiles/code-review/profile.json +26 -0
- package/.ctxman/wizard-profiles/documentation/.contextignore +21 -0
- package/.ctxman/wizard-profiles/documentation/.contextinclude +24 -0
- package/.ctxman/wizard-profiles/documentation/.methodignore +19 -0
- package/.ctxman/wizard-profiles/documentation/.methodinclude +44 -0
- package/.ctxman/wizard-profiles/documentation/profile.json +27 -0
- package/.ctxman/wizard-profiles/full/.contextignore +23 -0
- package/.ctxman/wizard-profiles/full/.contextinclude +38 -0
- package/.ctxman/wizard-profiles/full/.methodignore +5 -0
- package/.ctxman/wizard-profiles/full/.methodinclude +5 -0
- package/.ctxman/wizard-profiles/full/profile.json +28 -0
- package/.ctxman/wizard-profiles/llm-explain/.contextignore +25 -0
- package/.ctxman/wizard-profiles/llm-explain/.contextinclude +33 -0
- package/.ctxman/wizard-profiles/llm-explain/.methodignore +19 -0
- package/.ctxman/wizard-profiles/llm-explain/.methodinclude +48 -0
- package/.ctxman/wizard-profiles/llm-explain/profile.json +27 -0
- package/.ctxman/wizard-profiles/minimal/.contextignore +27 -0
- package/.ctxman/wizard-profiles/minimal/.contextinclude +14 -0
- package/.ctxman/wizard-profiles/minimal/.methodignore +22 -0
- package/.ctxman/wizard-profiles/minimal/.methodinclude +18 -0
- package/.ctxman/wizard-profiles/minimal/profile.json +26 -0
- package/.ctxman/wizard-profiles/security-audit/.contextignore +36 -0
- package/.ctxman/wizard-profiles/security-audit/.contextinclude +37 -0
- package/.ctxman/wizard-profiles/security-audit/.methodignore +29 -0
- package/.ctxman/wizard-profiles/security-audit/.methodinclude +60 -0
- package/.ctxman/wizard-profiles/security-audit/profile.json +34 -0
- package/README.md +1167 -0
- package/bin/cli.js +650 -0
- package/bin/cm-ask.js +95 -0
- package/bin/cm-gitingest.js +134 -0
- package/bin/cm-update.js +175 -0
- package/bin/mcp-server.js +31 -0
- package/ctxman.js +236 -0
- package/examples/README.md +298 -0
- package/examples/custom-llm-profiles.example.json +19 -0
- package/examples/wizard-profiles/code-review/.contextignore +41 -0
- package/examples/wizard-profiles/code-review/.contextinclude +30 -0
- package/examples/wizard-profiles/code-review/.methodignore +26 -0
- package/examples/wizard-profiles/code-review/.methodinclude +47 -0
- package/examples/wizard-profiles/code-review/profile.json +26 -0
- package/examples/wizard-profiles/documentation/.contextignore +21 -0
- package/examples/wizard-profiles/documentation/.contextinclude +24 -0
- package/examples/wizard-profiles/documentation/.methodignore +19 -0
- package/examples/wizard-profiles/documentation/.methodinclude +44 -0
- package/examples/wizard-profiles/documentation/profile.json +27 -0
- package/examples/wizard-profiles/full/.contextignore +23 -0
- package/examples/wizard-profiles/full/.contextinclude +38 -0
- package/examples/wizard-profiles/full/.methodignore +5 -0
- package/examples/wizard-profiles/full/.methodinclude +5 -0
- package/examples/wizard-profiles/full/profile.json +28 -0
- package/examples/wizard-profiles/llm-explain/.contextignore +25 -0
- package/examples/wizard-profiles/llm-explain/.contextinclude +33 -0
- package/examples/wizard-profiles/llm-explain/.methodignore +19 -0
- package/examples/wizard-profiles/llm-explain/.methodinclude +48 -0
- package/examples/wizard-profiles/llm-explain/profile.json +27 -0
- package/examples/wizard-profiles/minimal/.contextignore +27 -0
- package/examples/wizard-profiles/minimal/.contextinclude +14 -0
- package/examples/wizard-profiles/minimal/.methodignore +22 -0
- package/examples/wizard-profiles/minimal/.methodinclude +18 -0
- package/examples/wizard-profiles/minimal/profile.json +26 -0
- package/examples/wizard-profiles/security-audit/.contextignore +36 -0
- package/examples/wizard-profiles/security-audit/.contextinclude +37 -0
- package/examples/wizard-profiles/security-audit/.methodignore +29 -0
- package/examples/wizard-profiles/security-audit/.methodinclude +60 -0
- package/examples/wizard-profiles/security-audit/profile.json +34 -0
- package/index.js +68 -0
- package/lib/agents/pr-failure-agent.js +201 -0
- package/lib/analyzers/go-method-analyzer.js +126 -0
- package/lib/analyzers/method-analyzer.js +457 -0
- package/lib/analyzers/token-calculator.js +577 -0
- package/lib/api/mcp/prompts.js +319 -0
- package/lib/api/mcp/resources.js +269 -0
- package/lib/api/mcp/server.js +339 -0
- package/lib/api/mcp/transports/HttpSseTransport.js +63 -0
- package/lib/api/rest/server.js +405 -0
- package/lib/cache/CacheManager.js +388 -0
- package/lib/core/Analyzer.js +180 -0
- package/lib/core/ContextBuilder.js +325 -0
- package/lib/core/Reporter.js +270 -0
- package/lib/core/Scanner.js +129 -0
- package/lib/formatters/format-registry.js +419 -0
- package/lib/formatters/gitingest-formatter.js +746 -0
- package/lib/formatters/toon-formatter-v1.3.js +731 -0
- package/lib/formatters/toon-formatter.js +291 -0
- package/lib/integrations/git/BlameTracker.js +142 -0
- package/lib/integrations/git/DiffAnalyzer.js +260 -0
- package/lib/integrations/git/GitClient.js +336 -0
- package/lib/parsers/gitignore-parser.js +127 -0
- package/lib/parsers/method-filter-parser.js +50 -0
- package/lib/plugins/ExporterPlugin.js +102 -0
- package/lib/plugins/LanguagePlugin.js +100 -0
- package/lib/plugins/PluginManager.js +302 -0
- package/lib/rag/DataSourcePlugin.js +31 -0
- package/lib/rag/EmbeddingProvider.js +75 -0
- package/lib/rag/Indexer.js +83 -0
- package/lib/rag/VectorStoreFactory.js +24 -0
- package/lib/rag/sources/FileDataSource.js +70 -0
- package/lib/rag/stores/BaseVectorStore.js +43 -0
- package/lib/rag/stores/ChromaDBStore.js +85 -0
- package/lib/rag/stores/LanceDBStore.js +109 -0
- package/lib/rag/stores/PineconeStore.js +74 -0
- package/lib/ui/dashboard.js +109 -0
- package/lib/ui/index.js +15 -0
- package/lib/ui/progress-bar.js +110 -0
- package/lib/ui/select-input.js +51 -0
- package/lib/ui/wizard.js +359 -0
- package/lib/utils/clipboard-utils.js +66 -0
- package/lib/utils/config-utils.js +94 -0
- package/lib/utils/error-handler.js +145 -0
- package/lib/utils/file-utils.js +89 -0
- package/lib/utils/format-converter.js +325 -0
- package/lib/utils/git-utils.js +337 -0
- package/lib/utils/llm-detector.js +409 -0
- package/lib/utils/logger.js +278 -0
- package/lib/utils/token-utils.js +200 -0
- package/lib/utils/tokenizer-adapter.js +454 -0
- package/lib/utils/updater.js +565 -0
- package/lib/watch/FileWatcher.js +208 -0
- package/lib/watch/IncrementalAnalyzer.js +206 -0
- package/package.json +118 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitClient - Git Operations Module
|
|
3
|
+
* v3.0.0 - Complete Git integration
|
|
4
|
+
*
|
|
5
|
+
* Responsibilities:
|
|
6
|
+
* - Execute git commands
|
|
7
|
+
* - Parse git output
|
|
8
|
+
* - Provide high-level git operations
|
|
9
|
+
* - Cache git metadata
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { execSync } from 'child_process';
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { getLogger } from '../../utils/logger.js';
|
|
16
|
+
|
|
17
|
+
const logger = getLogger('GitClient');
|
|
18
|
+
|
|
19
|
+
export class GitClient {
|
|
20
|
+
constructor(repositoryPath) {
|
|
21
|
+
this.repoPath = repositoryPath;
|
|
22
|
+
this.isGitRepo = this.checkIsGitRepository();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if directory is a git repository
|
|
27
|
+
* @returns {boolean}
|
|
28
|
+
*/
|
|
29
|
+
checkIsGitRepository() {
|
|
30
|
+
try {
|
|
31
|
+
const gitDir = path.join(this.repoPath, '.git');
|
|
32
|
+
return fs.existsSync(gitDir);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validate git reference to prevent command injection
|
|
40
|
+
* @param {string} ref
|
|
41
|
+
* @returns {boolean}
|
|
42
|
+
*/
|
|
43
|
+
validateGitRef(ref) {
|
|
44
|
+
if (!ref) return true; // null/undefined is safe (handled logic elsewhere)
|
|
45
|
+
// Allow alphanumeric, /, -, _, ., @, ^, ~
|
|
46
|
+
// Disallow ; | & $ > < ` \ !
|
|
47
|
+
return /^[a-zA-Z0-9/\-_.\@\^\~]+$/.test(ref);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Execute git command
|
|
52
|
+
* @param {string} command - Git command (without 'git' prefix)
|
|
53
|
+
* @returns {string} Command output
|
|
54
|
+
*/
|
|
55
|
+
exec(command) {
|
|
56
|
+
if (!this.isGitRepo) {
|
|
57
|
+
throw new Error('Not a git repository');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Basic sanity check against shell injection if the command string was constructed with user input
|
|
61
|
+
// This is a last line of defense; specific methods should validate their inputs.
|
|
62
|
+
if (command.includes(';') || command.includes('&')) {
|
|
63
|
+
logger.warn(`Potential injection detected in command: ${command}`);
|
|
64
|
+
throw new Error('Invalid characters in git command');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const fullCommand = `git ${command}`;
|
|
69
|
+
const output = execSync(fullCommand, {
|
|
70
|
+
cwd: this.repoPath,
|
|
71
|
+
encoding: 'utf-8',
|
|
72
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return output.trim();
|
|
76
|
+
} catch (error) {
|
|
77
|
+
logger.error(`Git command failed: ${command}`);
|
|
78
|
+
throw new Error(`Git command failed: ${error.message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get current branch name
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
getCurrentBranch() {
|
|
87
|
+
return this.exec('rev-parse --abbrev-ref HEAD');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get default branch name
|
|
92
|
+
* @returns {string}
|
|
93
|
+
*/
|
|
94
|
+
getDefaultBranch() {
|
|
95
|
+
try {
|
|
96
|
+
return this.exec('symbolic-ref refs/remotes/origin/HEAD --short').split('/').pop();
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return 'main'; // Fallback
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get changed files
|
|
104
|
+
* @param {string} since - Commit/branch reference (e.g., 'main', 'HEAD~5')
|
|
105
|
+
* @returns {Array<string>}
|
|
106
|
+
*/
|
|
107
|
+
getChangedFiles(since = null) {
|
|
108
|
+
let command;
|
|
109
|
+
|
|
110
|
+
if (since) {
|
|
111
|
+
// Files changed between commits/branches
|
|
112
|
+
command = `diff --name-only ${since}`;
|
|
113
|
+
} else {
|
|
114
|
+
// Files with uncommitted changes
|
|
115
|
+
command = 'diff --name-only';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const output = this.exec(command);
|
|
119
|
+
return output ? output.split('\n').filter(Boolean) : [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get staged files
|
|
124
|
+
* @returns {Array<string>}
|
|
125
|
+
*/
|
|
126
|
+
getStagedFiles() {
|
|
127
|
+
const output = this.exec('diff --cached --name-only');
|
|
128
|
+
return output ? output.split('\n').filter(Boolean) : [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get unstaged files
|
|
133
|
+
* @returns {Array<string>}
|
|
134
|
+
*/
|
|
135
|
+
getUnstagedFiles() {
|
|
136
|
+
const output = this.exec('diff --name-only');
|
|
137
|
+
return output ? output.split('\n').filter(Boolean) : [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get untracked files
|
|
142
|
+
* @returns {Array<string>}
|
|
143
|
+
*/
|
|
144
|
+
getUntrackedFiles() {
|
|
145
|
+
const output = this.exec('ls-files --others --exclude-standard');
|
|
146
|
+
return output ? output.split('\n').filter(Boolean) : [];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get all modified files (staged + unstaged + untracked)
|
|
151
|
+
* @returns {Array<string>}
|
|
152
|
+
*/
|
|
153
|
+
getAllModifiedFiles() {
|
|
154
|
+
const staged = this.getStagedFiles();
|
|
155
|
+
const unstaged = this.getUnstagedFiles();
|
|
156
|
+
const untracked = this.getUntrackedFiles();
|
|
157
|
+
|
|
158
|
+
// Deduplicate
|
|
159
|
+
return [...new Set([...staged, ...unstaged, ...untracked])];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get file history
|
|
164
|
+
* @param {string} filePath - File path
|
|
165
|
+
* @param {number} limit - Number of commits
|
|
166
|
+
* @returns {Array<Commit>}
|
|
167
|
+
*/
|
|
168
|
+
getFileHistory(filePath, limit = 10) {
|
|
169
|
+
const command = `log --follow --pretty=format:"%H|%an|%ae|%at|%s" -n ${limit} -- "${filePath}"`;
|
|
170
|
+
const output = this.exec(command);
|
|
171
|
+
|
|
172
|
+
if (!output) return [];
|
|
173
|
+
|
|
174
|
+
return output.split('\n').map(line => {
|
|
175
|
+
const [hash, author, email, timestamp, subject] = line.split('|');
|
|
176
|
+
return {
|
|
177
|
+
hash,
|
|
178
|
+
author,
|
|
179
|
+
email,
|
|
180
|
+
timestamp: parseInt(timestamp, 10),
|
|
181
|
+
date: new Date(parseInt(timestamp, 10) * 1000),
|
|
182
|
+
subject
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get blame information for file
|
|
189
|
+
* @param {string} filePath - File path
|
|
190
|
+
* @returns {Array<BlameLine>}
|
|
191
|
+
*/
|
|
192
|
+
getBlame(filePath) {
|
|
193
|
+
try {
|
|
194
|
+
const command = `blame --line-porcelain "${filePath}"`;
|
|
195
|
+
const output = this.exec(command);
|
|
196
|
+
|
|
197
|
+
return this.parseBlameOutput(output);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
logger.warn(`Blame failed for ${filePath}: ${error.message}`);
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Parse git blame porcelain output
|
|
206
|
+
* @param {string} output
|
|
207
|
+
* @returns {Array<BlameLine>}
|
|
208
|
+
*/
|
|
209
|
+
parseBlameOutput(output) {
|
|
210
|
+
const lines = output.split('\n');
|
|
211
|
+
const blameInfo = [];
|
|
212
|
+
let currentCommit = null;
|
|
213
|
+
let currentAuthor = null;
|
|
214
|
+
let currentTimestamp = null;
|
|
215
|
+
|
|
216
|
+
for (const line of lines) {
|
|
217
|
+
if (line.match(/^[0-9a-f]{40}/)) {
|
|
218
|
+
// Commit hash line
|
|
219
|
+
currentCommit = line.split(' ')[0];
|
|
220
|
+
} else if (line.startsWith('author ')) {
|
|
221
|
+
currentAuthor = line.substring(7);
|
|
222
|
+
} else if (line.startsWith('author-time ')) {
|
|
223
|
+
currentTimestamp = parseInt(line.substring(12), 10);
|
|
224
|
+
} else if (line.startsWith('\t')) {
|
|
225
|
+
// Code line
|
|
226
|
+
blameInfo.push({
|
|
227
|
+
commit: currentCommit,
|
|
228
|
+
author: currentAuthor,
|
|
229
|
+
timestamp: currentTimestamp,
|
|
230
|
+
date: new Date(currentTimestamp * 1000),
|
|
231
|
+
code: line.substring(1)
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return blameInfo;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Get commit count for file
|
|
241
|
+
* @param {string} filePath
|
|
242
|
+
* @returns {number}
|
|
243
|
+
*/
|
|
244
|
+
getCommitCount(filePath) {
|
|
245
|
+
try {
|
|
246
|
+
const output = this.exec(`log --follow --oneline "${filePath}"`);
|
|
247
|
+
return output ? output.split('\n').length : 0;
|
|
248
|
+
} catch (error) {
|
|
249
|
+
return 0;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get file authors
|
|
255
|
+
* @param {string} filePath
|
|
256
|
+
* @returns {Array<Author>}
|
|
257
|
+
*/
|
|
258
|
+
getFileAuthors(filePath) {
|
|
259
|
+
try {
|
|
260
|
+
const command = `log --follow --pretty=format:"%an|%ae" -- "${filePath}"`;
|
|
261
|
+
const output = this.exec(command);
|
|
262
|
+
|
|
263
|
+
if (!output) return [];
|
|
264
|
+
|
|
265
|
+
const authorsMap = new Map();
|
|
266
|
+
|
|
267
|
+
output.split('\n').forEach(line => {
|
|
268
|
+
const [name, email] = line.split('|');
|
|
269
|
+
if (!authorsMap.has(email)) {
|
|
270
|
+
authorsMap.set(email, { name, email, commits: 0 });
|
|
271
|
+
}
|
|
272
|
+
authorsMap.get(email).commits++;
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return Array.from(authorsMap.values())
|
|
276
|
+
.sort((a, b) => b.commits - a.commits);
|
|
277
|
+
|
|
278
|
+
} catch (error) {
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Get repository statistics
|
|
285
|
+
* @returns {object}
|
|
286
|
+
*/
|
|
287
|
+
getRepoStats() {
|
|
288
|
+
if (!this.isGitRepo) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
const totalCommits = this.exec('rev-list --count HEAD');
|
|
294
|
+
const totalFiles = this.exec('ls-files').split('\n').length;
|
|
295
|
+
const contributors = this.exec('shortlog -sn --all').split('\n').length;
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
totalCommits: parseInt(totalCommits, 10),
|
|
299
|
+
totalFiles,
|
|
300
|
+
contributors,
|
|
301
|
+
currentBranch: this.getCurrentBranch()
|
|
302
|
+
};
|
|
303
|
+
} catch (error) {
|
|
304
|
+
logger.warn(`Failed to get repo stats: ${error.message}`);
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Get last commit info for file
|
|
311
|
+
* @param {string} filePath
|
|
312
|
+
* @returns {Commit|null}
|
|
313
|
+
*/
|
|
314
|
+
getLastCommit(filePath) {
|
|
315
|
+
try {
|
|
316
|
+
const command = `log -1 --pretty=format:"%H|%an|%ae|%at|%s" -- "${filePath}"`;
|
|
317
|
+
const output = this.exec(command);
|
|
318
|
+
|
|
319
|
+
if (!output) return null;
|
|
320
|
+
|
|
321
|
+
const [hash, author, email, timestamp, subject] = output.split('|');
|
|
322
|
+
return {
|
|
323
|
+
hash,
|
|
324
|
+
author,
|
|
325
|
+
email,
|
|
326
|
+
timestamp: parseInt(timestamp, 10),
|
|
327
|
+
date: new Date(parseInt(timestamp, 10) * 1000),
|
|
328
|
+
subject
|
|
329
|
+
};
|
|
330
|
+
} catch (error) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export default GitClient;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitIgnore Parser
|
|
3
|
+
* Parses .gitignore, .contextignore, and .contextinclude files
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
|
|
8
|
+
class GitIgnoreParser {
|
|
9
|
+
constructor(gitignorePath, contextIgnorePath, contextIncludePath) {
|
|
10
|
+
this.patterns = [];
|
|
11
|
+
this.contextPatterns = [];
|
|
12
|
+
this.hasIncludeFile = false;
|
|
13
|
+
this._lastIgnoreReason = null;
|
|
14
|
+
|
|
15
|
+
this.loadPatterns(gitignorePath, contextIgnorePath, contextIncludePath);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
loadPatterns(gitignorePath, contextIgnorePath, contextIncludePath) {
|
|
19
|
+
// Load .gitignore
|
|
20
|
+
if (fs.existsSync(gitignorePath)) {
|
|
21
|
+
this.patterns = this.parsePatternFile(gitignorePath);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Load context patterns (include takes priority)
|
|
25
|
+
if (contextIncludePath && fs.existsSync(contextIncludePath)) {
|
|
26
|
+
this.contextPatterns = this.parsePatternFile(contextIncludePath);
|
|
27
|
+
this.hasIncludeFile = true;
|
|
28
|
+
console.log(`📅 Context include rules loaded: ${this.contextPatterns.length} patterns`);
|
|
29
|
+
} else if (contextIgnorePath && fs.existsSync(contextIgnorePath)) {
|
|
30
|
+
this.contextPatterns = this.parsePatternFile(contextIgnorePath);
|
|
31
|
+
console.log(`📋 Context ignore rules loaded: ${this.contextPatterns.length} patterns`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
parsePatternFile(filePath) {
|
|
36
|
+
try {
|
|
37
|
+
// Check if path is actually a file (not a directory)
|
|
38
|
+
const stats = fs.statSync(filePath);
|
|
39
|
+
if (stats.isDirectory()) {
|
|
40
|
+
console.warn(`Warning: ${filePath} is a directory, expected a file`);
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return fs.readFileSync(filePath, 'utf8')
|
|
45
|
+
.split('\n')
|
|
46
|
+
.map(line => line.trim())
|
|
47
|
+
.filter(line => line && !line.startsWith('#'))
|
|
48
|
+
.map(pattern => this.convertToRegex(pattern));
|
|
49
|
+
} catch (error) {
|
|
50
|
+
// File doesn't exist or can't be read
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
convertToRegex(pattern) {
|
|
56
|
+
const isDirectory = pattern.endsWith('/');
|
|
57
|
+
const isNegation = pattern.startsWith('!');
|
|
58
|
+
const original = pattern;
|
|
59
|
+
|
|
60
|
+
pattern = pattern.replace(/^[!/]/, '').replace(/\/$/, '');
|
|
61
|
+
|
|
62
|
+
let regexPattern = pattern
|
|
63
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
64
|
+
.replace(/\*\*/g, '.*')
|
|
65
|
+
.replace(/\*/g, '[^/]*')
|
|
66
|
+
.replace(/\?/g, '[^/]');
|
|
67
|
+
|
|
68
|
+
regexPattern = original.startsWith('/')
|
|
69
|
+
? `^${regexPattern}`
|
|
70
|
+
: `(^|/)${regexPattern}`;
|
|
71
|
+
|
|
72
|
+
regexPattern += isDirectory ? '(/.*)?$' : '$';
|
|
73
|
+
|
|
74
|
+
return { regex: new RegExp(regexPattern), isNegation, original, isDirectory };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
isIgnored(filePath, relativePath) {
|
|
78
|
+
let ignored = this.testPatterns(this.patterns, relativePath, 'gitignore');
|
|
79
|
+
|
|
80
|
+
if (this.hasIncludeFile) {
|
|
81
|
+
if (ignored) return true; // Keep gitignore exclusions
|
|
82
|
+
|
|
83
|
+
const stat = fs.statSync(filePath);
|
|
84
|
+
if (stat.isDirectory()) {
|
|
85
|
+
const shouldTraverse = this.contextPatterns.some(p =>
|
|
86
|
+
p.original.startsWith(relativePath) ||
|
|
87
|
+
p.original.includes('**') ||
|
|
88
|
+
relativePath === ''
|
|
89
|
+
);
|
|
90
|
+
ignored = !shouldTraverse;
|
|
91
|
+
} else {
|
|
92
|
+
const included = this.testPatternsWithNegation(this.contextPatterns, relativePath);
|
|
93
|
+
ignored = !included;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (ignored) this._lastIgnoreReason = 'context-include';
|
|
97
|
+
} else if (!ignored) {
|
|
98
|
+
ignored = this.testPatterns(this.contextPatterns, relativePath, 'context');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return ignored;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
testPatterns(patterns, relativePath, reason) {
|
|
105
|
+
let ignored = false;
|
|
106
|
+
for (const pattern of patterns) {
|
|
107
|
+
if (pattern.regex.test(relativePath)) {
|
|
108
|
+
ignored = !pattern.isNegation;
|
|
109
|
+
if (ignored) this._lastIgnoreReason = reason;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return ignored;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
testPatternsWithNegation(patterns, relativePath) {
|
|
116
|
+
let included = false;
|
|
117
|
+
for (const pattern of patterns) {
|
|
118
|
+
if (pattern.regex.test(relativePath)) {
|
|
119
|
+
included = pattern.isNegation ? false : true;
|
|
120
|
+
if (pattern.isNegation) break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return included;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export default GitIgnoreParser;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Method Filter Parser
|
|
3
|
+
* Parses .methodinclude and .methodignore files for method-level filtering
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
|
|
8
|
+
class MethodFilterParser {
|
|
9
|
+
constructor(methodIncludePath, methodIgnorePath) {
|
|
10
|
+
this.includePatterns = [];
|
|
11
|
+
this.ignorePatterns = [];
|
|
12
|
+
this.hasIncludeFile = false;
|
|
13
|
+
|
|
14
|
+
if (methodIncludePath && fs.existsSync(methodIncludePath)) {
|
|
15
|
+
this.includePatterns = this.parseMethodFile(methodIncludePath);
|
|
16
|
+
this.hasIncludeFile = true;
|
|
17
|
+
console.log(`🔧 Method include rules loaded: ${this.includePatterns.length} patterns`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (methodIgnorePath && fs.existsSync(methodIgnorePath)) {
|
|
21
|
+
this.ignorePatterns = this.parseMethodFile(methodIgnorePath);
|
|
22
|
+
console.log(`🚫 Method ignore rules loaded: ${this.ignorePatterns.length} patterns`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
parseMethodFile(filePath) {
|
|
27
|
+
return fs.readFileSync(filePath, 'utf8')
|
|
28
|
+
.split('\n')
|
|
29
|
+
.map(line => line.trim())
|
|
30
|
+
.filter(line => line && !line.startsWith('#'))
|
|
31
|
+
.map(pattern => ({
|
|
32
|
+
pattern: pattern,
|
|
33
|
+
regex: new RegExp(pattern.replace(/\*/g, '.*'), 'i')
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
shouldIncludeMethod(methodName, fileName) {
|
|
38
|
+
if (this.hasIncludeFile) {
|
|
39
|
+
return this.includePatterns.some(p =>
|
|
40
|
+
p.regex.test(methodName) || p.regex.test(`${fileName}.${methodName}`)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return !this.ignorePatterns.some(p =>
|
|
45
|
+
p.regex.test(methodName) || p.regex.test(`${fileName}.${methodName}`)
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default MethodFilterParser;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExporterPlugin - Base Class for Format Exporter Plugins
|
|
3
|
+
* v3.0.0 - Plugin architecture
|
|
4
|
+
*
|
|
5
|
+
* All format exporters should extend this class
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class ExporterPlugin {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.name = 'base';
|
|
11
|
+
this.extension = '.txt';
|
|
12
|
+
this.mimeType = 'text/plain';
|
|
13
|
+
this.version = '1.0.0';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get plugin metadata
|
|
18
|
+
* @returns {object}
|
|
19
|
+
*/
|
|
20
|
+
getMetadata() {
|
|
21
|
+
return {
|
|
22
|
+
name: this.name,
|
|
23
|
+
version: this.version,
|
|
24
|
+
extension: this.extension,
|
|
25
|
+
mimeType: this.mimeType,
|
|
26
|
+
supportsChunking: this.supportsChunking(),
|
|
27
|
+
supportsCompression: this.supportsCompression()
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if this exporter supports chunking
|
|
33
|
+
* @returns {boolean}
|
|
34
|
+
*/
|
|
35
|
+
supportsChunking() {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if this exporter supports compression
|
|
41
|
+
* @returns {boolean}
|
|
42
|
+
*/
|
|
43
|
+
supportsCompression() {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Encode context to this format
|
|
49
|
+
* @param {Context} context
|
|
50
|
+
* @param {object} options
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
encode(context, options = {}) {
|
|
54
|
+
throw new Error('encode() must be implemented by subclass');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Decode this format to context
|
|
59
|
+
* @param {string} content
|
|
60
|
+
* @returns {Context}
|
|
61
|
+
*/
|
|
62
|
+
decode(content) {
|
|
63
|
+
throw new Error('decode() not supported for this format');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Validate encoded output
|
|
68
|
+
* @param {string} content
|
|
69
|
+
* @returns {boolean}
|
|
70
|
+
*/
|
|
71
|
+
validate(content) {
|
|
72
|
+
return typeof content === 'string' && content.length > 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Estimate token count of encoded output
|
|
77
|
+
* @param {Context} context
|
|
78
|
+
* @returns {number}
|
|
79
|
+
*/
|
|
80
|
+
estimateTokens(context) {
|
|
81
|
+
const encoded = this.encode(context);
|
|
82
|
+
return Math.ceil(encoded.length / 4); // Rough estimate
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Compress output (optional)
|
|
87
|
+
* @param {string} content
|
|
88
|
+
* @returns {string}
|
|
89
|
+
*/
|
|
90
|
+
compress(content) {
|
|
91
|
+
return content; // Override if compression supported
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Cleanup resources
|
|
96
|
+
*/
|
|
97
|
+
cleanup() {
|
|
98
|
+
// Override if needed
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export default ExporterPlugin;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LanguagePlugin - Base Class for Language Plugins
|
|
3
|
+
* v3.0.0 - Plugin architecture
|
|
4
|
+
*
|
|
5
|
+
* All language analyzers should extend this class
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class LanguagePlugin {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.name = 'base';
|
|
11
|
+
this.extensions = [];
|
|
12
|
+
this.version = '1.0.0';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get plugin metadata
|
|
17
|
+
* @returns {object}
|
|
18
|
+
*/
|
|
19
|
+
getMetadata() {
|
|
20
|
+
return {
|
|
21
|
+
name: this.name,
|
|
22
|
+
version: this.version,
|
|
23
|
+
extensions: this.extensions,
|
|
24
|
+
supportsAST: this.supportsAST(),
|
|
25
|
+
supportsMethodExtraction: this.supportsMethodExtraction(),
|
|
26
|
+
supportsFrameworkDetection: this.supportsFrameworkDetection()
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if this plugin supports AST parsing
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
supportsAST() {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if this plugin supports method extraction
|
|
40
|
+
* @returns {boolean}
|
|
41
|
+
*/
|
|
42
|
+
supportsMethodExtraction() {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if this plugin supports framework detection
|
|
48
|
+
* @returns {boolean}
|
|
49
|
+
*/
|
|
50
|
+
supportsFrameworkDetection() {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Extract methods from code
|
|
56
|
+
* @param {string} content - File content
|
|
57
|
+
* @param {string} filePath - File path
|
|
58
|
+
* @returns {Array<Method>}
|
|
59
|
+
*/
|
|
60
|
+
extractMethods(content, filePath) {
|
|
61
|
+
throw new Error('extractMethods() must be implemented by subclass');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Detect framework (optional)
|
|
66
|
+
* @param {string} content - File content
|
|
67
|
+
* @returns {string|null} Framework name or null
|
|
68
|
+
*/
|
|
69
|
+
detectFramework(content) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Analyze code complexity (optional)
|
|
75
|
+
* @param {string} content - Method content
|
|
76
|
+
* @returns {number} Complexity score
|
|
77
|
+
*/
|
|
78
|
+
analyzeComplexity(content) {
|
|
79
|
+
// Default: Simple line count
|
|
80
|
+
return content.split('\n').length;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Validate file content
|
|
85
|
+
* @param {string} content - File content
|
|
86
|
+
* @returns {boolean}
|
|
87
|
+
*/
|
|
88
|
+
validate(content) {
|
|
89
|
+
return typeof content === 'string' && content.length > 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Cleanup resources
|
|
94
|
+
*/
|
|
95
|
+
cleanup() {
|
|
96
|
+
// Override if needed
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default LanguagePlugin;
|