ctxman 3.0.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ctxman/llm-profiles.json +1067 -39
- package/.ctxman/profiles/backend-team.json +13 -0
- package/.ctxman/profiles/default.json +12 -0
- package/.ctxman/profiles/devops-team.json +21 -0
- package/.ctxman/profiles/frontend-team.json +20 -0
- package/.ctxman/templates/bug-fix.json +17 -0
- package/.ctxman/templates/code-review.json +9 -0
- package/.ctxman/templates/documentation.json +9 -0
- package/.ctxman/templates/feature.json +9 -0
- package/.ctxman/templates/full-context.json +9 -0
- package/.ctxman/templates/refactoring.json +18 -0
- package/.ctxman/templates/testing.json +9 -0
- package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
- package/LICENSE +21 -0
- package/README.md +381 -136
- package/bin/cli.js +1880 -529
- package/bin/cm-ask.js +18 -67
- package/bin/cm-audit.js +121 -0
- package/bin/cm-count.js +151 -0
- package/bin/cm-gitingest.js +128 -115
- package/bin/cm-map.js +242 -0
- package/bin/cm-mcp.js +293 -0
- package/bin/cm-pack.js +405 -0
- package/bin/cm-policy.js +170 -0
- package/bin/cm-pr.js +248 -0
- package/bin/cm-search.js +178 -0
- package/bin/cm-update.js +128 -128
- package/bin/cm-why.js +197 -0
- package/bin/mcp-server.js +8 -24
- package/ctxman.js +237 -183
- package/examples/README.md +19 -4
- package/examples/github-actions/ctxman-check.yml +49 -0
- package/examples/wizard-profiles/security-audit/profile.json +7 -1
- package/index.d.ts +1422 -0
- package/index.js +55 -31
- package/lib/agents/pr-failure-agent.js +48 -45
- package/lib/analyzers/ai-suggester.js +451 -0
- package/lib/analyzers/dependency-scanner.js +505 -0
- package/lib/analyzers/duplicate-detector.js +264 -0
- package/lib/analyzers/go-method-analyzer.js +151 -110
- package/lib/analyzers/import-tracker.js +431 -0
- package/lib/analyzers/method-analyzer.js +1055 -434
- package/lib/analyzers/performance-dashboard.js +687 -0
- package/lib/analyzers/token-calculator.js +895 -504
- package/lib/analyzers/unused-exports-detector.js +336 -0
- package/lib/api/mcp/install.js +424 -0
- package/lib/api/mcp/prompts.js +281 -281
- package/lib/api/mcp/resources.js +300 -241
- package/lib/api/mcp/sandbox.js +89 -0
- package/lib/api/mcp/server.js +772 -312
- package/lib/api/mcp/start.js +125 -0
- package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
- package/lib/api/rest/server.js +105 -28
- package/lib/cache/CacheManager.js +9 -17
- package/lib/core/Analyzer.js +16 -10
- package/lib/core/ContextBuilder.js +19 -28
- package/lib/core/Reporter.js +15 -14
- package/lib/core/Scanner.js +81 -12
- package/lib/count/index.js +100 -0
- package/lib/formatters/format-registry.js +394 -372
- package/lib/formatters/gitingest-formatter.js +656 -621
- package/lib/formatters/toon-formatter-v1.3.js +637 -632
- package/lib/formatters/toon-formatter.js +239 -240
- package/lib/integrations/git/BlameTracker.js +4 -4
- package/lib/integrations/git/DiffAnalyzer.js +38 -31
- package/lib/integrations/git/GitClient.js +71 -53
- package/lib/lsp/server.js +311 -0
- package/lib/map/graph.js +299 -0
- package/lib/map/imports.js +233 -0
- package/lib/map/index.js +33 -0
- package/lib/map/rank.js +253 -0
- package/lib/map/render.js +346 -0
- package/lib/map/resolver.js +574 -0
- package/lib/pack/budget.js +111 -0
- package/lib/pack/code-lexer.js +360 -0
- package/lib/pack/compress.js +414 -0
- package/lib/pack/index.js +354 -0
- package/lib/pack/renderers/common.js +134 -0
- package/lib/pack/renderers/index.js +24 -0
- package/lib/pack/renderers/json.js +47 -0
- package/lib/pack/renderers/markdown.js +54 -0
- package/lib/pack/renderers/plain.js +38 -0
- package/lib/pack/renderers/xml.js +85 -0
- package/lib/pack/select.js +177 -0
- package/lib/pack/split.js +182 -0
- package/lib/pack/tree.js +35 -0
- package/lib/parsers/gitignore-parser.js +116 -107
- package/lib/parsers/method-filter-parser.js +50 -33
- package/lib/plugins/ExporterPlugin.js +3 -3
- package/lib/plugins/LanguagePlugin.js +3 -3
- package/lib/plugins/PluginManager.js +7 -13
- package/lib/policy/audit.js +312 -0
- package/lib/policy/enforce.js +125 -0
- package/lib/policy/ignore-files.js +169 -0
- package/lib/policy/index.js +53 -0
- package/lib/policy/policy.js +489 -0
- package/lib/pr/diff-parser.js +267 -0
- package/lib/pr/method-mapper.js +148 -0
- package/lib/pr/pr-context.js +829 -0
- package/lib/pr/pr-git.js +149 -0
- package/lib/pr/pr-renderer.js +644 -0
- package/lib/pr/related-tests.js +296 -0
- package/lib/rag/DataSourcePlugin.js +5 -5
- package/lib/rag/EmbeddingProvider.js +19 -8
- package/lib/rag/Indexer.js +14 -5
- package/lib/rag/VectorStoreFactory.js +3 -3
- package/lib/rag/ask.js +122 -0
- package/lib/rag/sources/FileDataSource.js +12 -9
- package/lib/rag/stores/BaseVectorStore.js +5 -5
- package/lib/rag/stores/ChromaDBStore.js +10 -10
- package/lib/rag/stores/LanceDBStore.js +29 -19
- package/lib/rag/stores/PineconeStore.js +11 -9
- package/lib/search/bm25.js +433 -0
- package/lib/search/code-units.js +201 -0
- package/lib/search/symbols.js +189 -0
- package/lib/ui/dashboard.js +120 -89
- package/lib/ui/index.js +1 -6
- package/lib/ui/init-wizard-ui.js +550 -0
- package/lib/ui/progress-bar.js +94 -88
- package/lib/ui/select-input.js +40 -32
- package/lib/ui/template-selector.js +99 -0
- package/lib/ui/wizard.js +374 -329
- package/lib/utils/claude-token-counter.js +151 -0
- package/lib/utils/cli-output.js +17 -0
- package/lib/utils/clipboard-utils.js +49 -50
- package/lib/utils/config-utils.js +81 -66
- package/lib/utils/context-versioning.js +532 -0
- package/lib/utils/error-handler.js +112 -112
- package/lib/utils/file-utils.js +153 -69
- package/lib/utils/format-converter.js +356 -283
- package/lib/utils/git-utils.js +366 -294
- package/lib/utils/llm-cost-estimator.js +281 -0
- package/lib/utils/llm-detector.js +242 -78
- package/lib/utils/logger.js +246 -237
- package/lib/utils/messages.js +355 -0
- package/lib/utils/multi-repo-manager.js +364 -0
- package/lib/utils/profile-manager.js +380 -0
- package/lib/utils/safe-read.js +61 -0
- package/lib/utils/secret-redactor.js +61 -0
- package/lib/utils/snapshot-manager.js +679 -0
- package/lib/utils/template-manager.js +378 -0
- package/lib/utils/token-utils.js +201 -170
- package/lib/utils/tokenizer-adapter.js +421 -377
- package/lib/utils/updater.js +539 -506
- package/lib/watch/FileWatcher.js +12 -8
- package/lib/watch/IncrementalAnalyzer.js +7 -7
- package/lib/why/coupling.js +81 -0
- package/lib/why/errors.js +23 -0
- package/lib/why/git.js +169 -0
- package/lib/why/history.js +108 -0
- package/lib/why/index.js +720 -0
- package/lib/why/ownership.js +79 -0
- package/lib/why/paths.js +207 -0
- package/lib/why/references.js +259 -0
- package/lib/why/render.js +722 -0
- package/lib/why/search.js +358 -0
- package/lib/why/target.js +389 -0
- package/lib/wizards/config-generator.js +257 -0
- package/lib/wizards/project-detector.js +572 -0
- package/package.json +44 -10
- package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
- 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
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - Generate change summaries
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import GitClient from './GitClient.js';
|
|
12
|
+
import GitClient, { assertGitRef } from './GitClient.js';
|
|
13
13
|
import { getLogger } from '../../utils/logger.js';
|
|
14
14
|
|
|
15
15
|
const logger = getLogger('DiffAnalyzer');
|
|
@@ -28,16 +28,14 @@ export class DiffAnalyzer {
|
|
|
28
28
|
analyzeChanges(since = null) {
|
|
29
29
|
logger.info(`Analyzing changes since: ${since || 'uncommitted'}`);
|
|
30
30
|
|
|
31
|
-
const changedFiles = since
|
|
32
|
-
? this.git.getChangedFiles(since)
|
|
33
|
-
: this.git.getAllModifiedFiles();
|
|
31
|
+
const changedFiles = since ? this.git.getChangedFiles(since) : this.git.getAllModifiedFiles();
|
|
34
32
|
|
|
35
33
|
const analysis = {
|
|
36
34
|
changedFiles: changedFiles,
|
|
37
35
|
totalChangedFiles: changedFiles.length,
|
|
38
36
|
since: since,
|
|
39
37
|
relatedFiles: [],
|
|
40
|
-
impact: this.calculateImpact(changedFiles)
|
|
38
|
+
impact: this.calculateImpact(changedFiles),
|
|
41
39
|
};
|
|
42
40
|
|
|
43
41
|
return analysis;
|
|
@@ -49,15 +47,13 @@ export class DiffAnalyzer {
|
|
|
49
47
|
* @returns {Array<FileChange>}
|
|
50
48
|
*/
|
|
51
49
|
getDetailedChanges(since = null) {
|
|
52
|
-
const changedFiles = since
|
|
53
|
-
? this.git.getChangedFiles(since)
|
|
54
|
-
: this.git.getAllModifiedFiles();
|
|
50
|
+
const changedFiles = since ? this.git.getChangedFiles(since) : this.git.getAllModifiedFiles();
|
|
55
51
|
|
|
56
|
-
return changedFiles.map(filePath => {
|
|
52
|
+
return changedFiles.map((filePath) => {
|
|
57
53
|
const diff = this.getFileDiff(filePath, since);
|
|
58
54
|
return {
|
|
59
55
|
path: filePath,
|
|
60
|
-
...diff
|
|
56
|
+
...diff,
|
|
61
57
|
};
|
|
62
58
|
});
|
|
63
59
|
}
|
|
@@ -70,18 +66,17 @@ export class DiffAnalyzer {
|
|
|
70
66
|
*/
|
|
71
67
|
getFileDiff(filePath, since = null) {
|
|
72
68
|
try {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
: `diff -- "${filePath}"`;
|
|
69
|
+
if (since) assertGitRef(since);
|
|
70
|
+
const args = since ? ['diff', since, '--', filePath] : ['diff', '--', filePath];
|
|
76
71
|
|
|
77
|
-
const diffOutput = this.git.exec(
|
|
72
|
+
const diffOutput = this.git.exec(args);
|
|
78
73
|
const stats = this.parseDiffStats(diffOutput);
|
|
79
74
|
|
|
80
75
|
return {
|
|
81
76
|
added: stats.added,
|
|
82
77
|
deleted: stats.deleted,
|
|
83
78
|
modified: stats.added + stats.deleted,
|
|
84
|
-
diff: diffOutput
|
|
79
|
+
diff: diffOutput,
|
|
85
80
|
};
|
|
86
81
|
} catch (error) {
|
|
87
82
|
logger.warn(`Failed to get diff for ${filePath}: ${error.message}`);
|
|
@@ -123,24 +118,33 @@ export class DiffAnalyzer {
|
|
|
123
118
|
coreFiles: 0,
|
|
124
119
|
configFiles: 0,
|
|
125
120
|
testFiles: 0,
|
|
126
|
-
documentationFiles: 0
|
|
127
|
-
}
|
|
121
|
+
documentationFiles: 0,
|
|
122
|
+
},
|
|
128
123
|
};
|
|
129
124
|
|
|
130
125
|
for (const file of changedFiles) {
|
|
131
126
|
const fileLower = file.toLowerCase();
|
|
132
127
|
|
|
133
128
|
// Core files (high impact)
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
|
|
129
|
+
if (
|
|
130
|
+
fileLower.includes('src/') ||
|
|
131
|
+
fileLower.includes('lib/') ||
|
|
132
|
+
fileLower.includes('index.') ||
|
|
133
|
+
fileLower.includes('main.') ||
|
|
134
|
+
fileLower.includes('app.') ||
|
|
135
|
+
fileLower.includes('server.')
|
|
136
|
+
) {
|
|
137
137
|
impact.details.coreFiles++;
|
|
138
138
|
impact.score += 10;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
// Config files (medium-high impact)
|
|
142
|
-
if (
|
|
143
|
-
|
|
142
|
+
if (
|
|
143
|
+
fileLower.endsWith('.json') ||
|
|
144
|
+
fileLower.endsWith('.yml') ||
|
|
145
|
+
fileLower.endsWith('.yaml') ||
|
|
146
|
+
fileLower.endsWith('.config.js')
|
|
147
|
+
) {
|
|
144
148
|
impact.details.configFiles++;
|
|
145
149
|
impact.score += 7;
|
|
146
150
|
}
|
|
@@ -177,7 +181,7 @@ export class DiffAnalyzer {
|
|
|
177
181
|
* @param {Array<string>} changedFiles
|
|
178
182
|
* @returns {Array<string>}
|
|
179
183
|
*/
|
|
180
|
-
findRelatedFiles(
|
|
184
|
+
findRelatedFiles(_changedFiles) {
|
|
181
185
|
// TODO: Implement import graph analysis
|
|
182
186
|
// For now, return empty array
|
|
183
187
|
logger.debug('Related files detection not yet implemented');
|
|
@@ -201,14 +205,15 @@ export class DiffAnalyzer {
|
|
|
201
205
|
*/
|
|
202
206
|
compareBranches(base, compare) {
|
|
203
207
|
try {
|
|
204
|
-
|
|
205
|
-
|
|
208
|
+
assertGitRef(base);
|
|
209
|
+
assertGitRef(compare);
|
|
210
|
+
const output = this.git.exec(['diff', '--name-status', `${base}...${compare}`]);
|
|
206
211
|
|
|
207
212
|
const changes = {
|
|
208
213
|
added: [],
|
|
209
214
|
modified: [],
|
|
210
215
|
deleted: [],
|
|
211
|
-
renamed: []
|
|
216
|
+
renamed: [],
|
|
212
217
|
};
|
|
213
218
|
|
|
214
219
|
if (!output) return changes;
|
|
@@ -238,20 +243,22 @@ export class DiffAnalyzer {
|
|
|
238
243
|
*/
|
|
239
244
|
getRangeStats(range) {
|
|
240
245
|
try {
|
|
241
|
-
|
|
242
|
-
const output = this.git.exec(
|
|
246
|
+
assertGitRef(range);
|
|
247
|
+
const output = this.git.exec(['diff', '--shortstat', range]);
|
|
243
248
|
|
|
244
249
|
// Parse: "3 files changed, 45 insertions(+), 12 deletions(-)"
|
|
245
|
-
const match = output.match(
|
|
250
|
+
const match = output.match(
|
|
251
|
+
/(\d+) file[s]? changed(?:, (\d+) insertion[s]?\(\+\))?(?:, (\d+) deletion[s]?\(-\))?/
|
|
252
|
+
);
|
|
246
253
|
|
|
247
254
|
if (!match) return null;
|
|
248
255
|
|
|
249
256
|
return {
|
|
250
257
|
filesChanged: parseInt(match[1], 10),
|
|
251
258
|
insertions: parseInt(match[2] || 0, 10),
|
|
252
|
-
deletions: parseInt(match[3] || 0, 10)
|
|
259
|
+
deletions: parseInt(match[3] || 0, 10),
|
|
253
260
|
};
|
|
254
|
-
} catch (
|
|
261
|
+
} catch (_error) {
|
|
255
262
|
return null;
|
|
256
263
|
}
|
|
257
264
|
}
|
|
@@ -9,13 +9,34 @@
|
|
|
9
9
|
* - Cache git metadata
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { execFileSync } from 'child_process';
|
|
13
13
|
import fs from 'fs';
|
|
14
14
|
import path from 'path';
|
|
15
15
|
import { getLogger } from '../../utils/logger.js';
|
|
16
16
|
|
|
17
17
|
const logger = getLogger('GitClient');
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Check that a user-supplied revision (branch, tag, commit, HEAD~2, a..b) is safe to
|
|
21
|
+
* hand to git: allowlisted characters only, and no leading '-' that git would parse
|
|
22
|
+
* as an option (e.g. --output=<file>)
|
|
23
|
+
* @param {string} ref
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*/
|
|
26
|
+
export function isValidGitRef(ref) {
|
|
27
|
+
return typeof ref === 'string' && !ref.startsWith('-') && /^[a-zA-Z0-9/\-_.@^~]+$/.test(ref);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Throw unless isValidGitRef(ref)
|
|
32
|
+
* @param {string} ref
|
|
33
|
+
*/
|
|
34
|
+
export function assertGitRef(ref) {
|
|
35
|
+
if (!isValidGitRef(ref)) {
|
|
36
|
+
throw new Error(`Invalid git reference: ${JSON.stringify(ref)}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
19
40
|
export class GitClient {
|
|
20
41
|
constructor(repositoryPath) {
|
|
21
42
|
this.repoPath = repositoryPath;
|
|
@@ -30,51 +51,45 @@ export class GitClient {
|
|
|
30
51
|
try {
|
|
31
52
|
const gitDir = path.join(this.repoPath, '.git');
|
|
32
53
|
return fs.existsSync(gitDir);
|
|
33
|
-
} catch (
|
|
54
|
+
} catch (_error) {
|
|
34
55
|
return false;
|
|
35
56
|
}
|
|
36
57
|
}
|
|
37
58
|
|
|
38
59
|
/**
|
|
39
60
|
* Validate git reference to prevent command injection
|
|
40
|
-
* @param {string} ref
|
|
61
|
+
* @param {string} ref
|
|
41
62
|
* @returns {boolean}
|
|
42
63
|
*/
|
|
43
64
|
validateGitRef(ref) {
|
|
44
65
|
if (!ref) return true; // null/undefined is safe (handled logic elsewhere)
|
|
45
|
-
|
|
46
|
-
// Disallow ; | & $ > < ` \ !
|
|
47
|
-
return /^[a-zA-Z0-9/\-_.\@\^\~]+$/.test(ref);
|
|
66
|
+
return isValidGitRef(ref);
|
|
48
67
|
}
|
|
49
68
|
|
|
50
69
|
/**
|
|
51
|
-
* Execute git
|
|
52
|
-
*
|
|
70
|
+
* Execute git with an argument list. No shell is involved, so arguments reach git
|
|
71
|
+
* verbatim; refs from users must still go through isValidGitRef()
|
|
72
|
+
* @param {string[]} args - Arguments after `git`
|
|
53
73
|
* @returns {string} Command output
|
|
54
74
|
*/
|
|
55
|
-
exec(
|
|
75
|
+
exec(args) {
|
|
56
76
|
if (!this.isGitRepo) {
|
|
57
77
|
throw new Error('Not a git repository');
|
|
58
78
|
}
|
|
59
|
-
|
|
60
|
-
|
|
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');
|
|
79
|
+
if (!Array.isArray(args)) {
|
|
80
|
+
throw new TypeError('GitClient.exec() expects an array of arguments');
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
try {
|
|
68
|
-
const
|
|
69
|
-
const output = execSync(fullCommand, {
|
|
84
|
+
const output = execFileSync('git', args, {
|
|
70
85
|
cwd: this.repoPath,
|
|
71
86
|
encoding: 'utf-8',
|
|
72
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
87
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
73
88
|
});
|
|
74
89
|
|
|
75
90
|
return output.trim();
|
|
76
91
|
} catch (error) {
|
|
77
|
-
logger.error(`Git command failed: ${
|
|
92
|
+
logger.error(`Git command failed: git ${args.join(' ')}`);
|
|
78
93
|
throw new Error(`Git command failed: ${error.message}`);
|
|
79
94
|
}
|
|
80
95
|
}
|
|
@@ -84,7 +99,7 @@ export class GitClient {
|
|
|
84
99
|
* @returns {string}
|
|
85
100
|
*/
|
|
86
101
|
getCurrentBranch() {
|
|
87
|
-
return this.exec('rev-parse --abbrev-ref HEAD');
|
|
102
|
+
return this.exec(['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
88
103
|
}
|
|
89
104
|
|
|
90
105
|
/**
|
|
@@ -93,8 +108,8 @@ export class GitClient {
|
|
|
93
108
|
*/
|
|
94
109
|
getDefaultBranch() {
|
|
95
110
|
try {
|
|
96
|
-
return this.exec('symbolic-ref refs/remotes/origin/HEAD --short').split('/').pop();
|
|
97
|
-
} catch (
|
|
111
|
+
return this.exec(['symbolic-ref', 'refs/remotes/origin/HEAD', '--short']).split('/').pop();
|
|
112
|
+
} catch (_error) {
|
|
98
113
|
return 'main'; // Fallback
|
|
99
114
|
}
|
|
100
115
|
}
|
|
@@ -105,17 +120,18 @@ export class GitClient {
|
|
|
105
120
|
* @returns {Array<string>}
|
|
106
121
|
*/
|
|
107
122
|
getChangedFiles(since = null) {
|
|
108
|
-
let
|
|
123
|
+
let args;
|
|
109
124
|
|
|
110
125
|
if (since) {
|
|
111
126
|
// Files changed between commits/branches
|
|
112
|
-
|
|
127
|
+
assertGitRef(since);
|
|
128
|
+
args = ['diff', '--name-only', since, '--'];
|
|
113
129
|
} else {
|
|
114
130
|
// Files with uncommitted changes
|
|
115
|
-
|
|
131
|
+
args = ['diff', '--name-only'];
|
|
116
132
|
}
|
|
117
133
|
|
|
118
|
-
const output = this.exec(
|
|
134
|
+
const output = this.exec(args);
|
|
119
135
|
return output ? output.split('\n').filter(Boolean) : [];
|
|
120
136
|
}
|
|
121
137
|
|
|
@@ -124,7 +140,7 @@ export class GitClient {
|
|
|
124
140
|
* @returns {Array<string>}
|
|
125
141
|
*/
|
|
126
142
|
getStagedFiles() {
|
|
127
|
-
const output = this.exec('diff --cached --name-only');
|
|
143
|
+
const output = this.exec(['diff', '--cached', '--name-only']);
|
|
128
144
|
return output ? output.split('\n').filter(Boolean) : [];
|
|
129
145
|
}
|
|
130
146
|
|
|
@@ -133,7 +149,7 @@ export class GitClient {
|
|
|
133
149
|
* @returns {Array<string>}
|
|
134
150
|
*/
|
|
135
151
|
getUnstagedFiles() {
|
|
136
|
-
const output = this.exec('diff --name-only');
|
|
152
|
+
const output = this.exec(['diff', '--name-only']);
|
|
137
153
|
return output ? output.split('\n').filter(Boolean) : [];
|
|
138
154
|
}
|
|
139
155
|
|
|
@@ -142,7 +158,7 @@ export class GitClient {
|
|
|
142
158
|
* @returns {Array<string>}
|
|
143
159
|
*/
|
|
144
160
|
getUntrackedFiles() {
|
|
145
|
-
const output = this.exec('ls-files --others --exclude-standard');
|
|
161
|
+
const output = this.exec(['ls-files', '--others', '--exclude-standard']);
|
|
146
162
|
return output ? output.split('\n').filter(Boolean) : [];
|
|
147
163
|
}
|
|
148
164
|
|
|
@@ -166,12 +182,19 @@ export class GitClient {
|
|
|
166
182
|
* @returns {Array<Commit>}
|
|
167
183
|
*/
|
|
168
184
|
getFileHistory(filePath, limit = 10) {
|
|
169
|
-
const
|
|
170
|
-
|
|
185
|
+
const output = this.exec([
|
|
186
|
+
'log',
|
|
187
|
+
'--follow',
|
|
188
|
+
'--pretty=format:%H|%an|%ae|%at|%s',
|
|
189
|
+
'-n',
|
|
190
|
+
String(limit),
|
|
191
|
+
'--',
|
|
192
|
+
filePath,
|
|
193
|
+
]);
|
|
171
194
|
|
|
172
195
|
if (!output) return [];
|
|
173
196
|
|
|
174
|
-
return output.split('\n').map(line => {
|
|
197
|
+
return output.split('\n').map((line) => {
|
|
175
198
|
const [hash, author, email, timestamp, subject] = line.split('|');
|
|
176
199
|
return {
|
|
177
200
|
hash,
|
|
@@ -179,7 +202,7 @@ export class GitClient {
|
|
|
179
202
|
email,
|
|
180
203
|
timestamp: parseInt(timestamp, 10),
|
|
181
204
|
date: new Date(parseInt(timestamp, 10) * 1000),
|
|
182
|
-
subject
|
|
205
|
+
subject,
|
|
183
206
|
};
|
|
184
207
|
});
|
|
185
208
|
}
|
|
@@ -191,8 +214,7 @@ export class GitClient {
|
|
|
191
214
|
*/
|
|
192
215
|
getBlame(filePath) {
|
|
193
216
|
try {
|
|
194
|
-
const
|
|
195
|
-
const output = this.exec(command);
|
|
217
|
+
const output = this.exec(['blame', '--line-porcelain', '--', filePath]);
|
|
196
218
|
|
|
197
219
|
return this.parseBlameOutput(output);
|
|
198
220
|
} catch (error) {
|
|
@@ -228,7 +250,7 @@ export class GitClient {
|
|
|
228
250
|
author: currentAuthor,
|
|
229
251
|
timestamp: currentTimestamp,
|
|
230
252
|
date: new Date(currentTimestamp * 1000),
|
|
231
|
-
code: line.substring(1)
|
|
253
|
+
code: line.substring(1),
|
|
232
254
|
});
|
|
233
255
|
}
|
|
234
256
|
}
|
|
@@ -243,9 +265,9 @@ export class GitClient {
|
|
|
243
265
|
*/
|
|
244
266
|
getCommitCount(filePath) {
|
|
245
267
|
try {
|
|
246
|
-
const output = this.exec(
|
|
268
|
+
const output = this.exec(['log', '--follow', '--oneline', '--', filePath]);
|
|
247
269
|
return output ? output.split('\n').length : 0;
|
|
248
|
-
} catch (
|
|
270
|
+
} catch (_error) {
|
|
249
271
|
return 0;
|
|
250
272
|
}
|
|
251
273
|
}
|
|
@@ -257,14 +279,13 @@ export class GitClient {
|
|
|
257
279
|
*/
|
|
258
280
|
getFileAuthors(filePath) {
|
|
259
281
|
try {
|
|
260
|
-
const
|
|
261
|
-
const output = this.exec(command);
|
|
282
|
+
const output = this.exec(['log', '--follow', '--pretty=format:%an|%ae', '--', filePath]);
|
|
262
283
|
|
|
263
284
|
if (!output) return [];
|
|
264
285
|
|
|
265
286
|
const authorsMap = new Map();
|
|
266
287
|
|
|
267
|
-
output.split('\n').forEach(line => {
|
|
288
|
+
output.split('\n').forEach((line) => {
|
|
268
289
|
const [name, email] = line.split('|');
|
|
269
290
|
if (!authorsMap.has(email)) {
|
|
270
291
|
authorsMap.set(email, { name, email, commits: 0 });
|
|
@@ -272,10 +293,8 @@ export class GitClient {
|
|
|
272
293
|
authorsMap.get(email).commits++;
|
|
273
294
|
});
|
|
274
295
|
|
|
275
|
-
return Array.from(authorsMap.values())
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
} catch (error) {
|
|
296
|
+
return Array.from(authorsMap.values()).sort((a, b) => b.commits - a.commits);
|
|
297
|
+
} catch (_error) {
|
|
279
298
|
return [];
|
|
280
299
|
}
|
|
281
300
|
}
|
|
@@ -290,15 +309,15 @@ export class GitClient {
|
|
|
290
309
|
}
|
|
291
310
|
|
|
292
311
|
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;
|
|
312
|
+
const totalCommits = this.exec(['rev-list', '--count', 'HEAD']);
|
|
313
|
+
const totalFiles = this.exec(['ls-files']).split('\n').length;
|
|
314
|
+
const contributors = this.exec(['shortlog', '-sn', '--all']).split('\n').length;
|
|
296
315
|
|
|
297
316
|
return {
|
|
298
317
|
totalCommits: parseInt(totalCommits, 10),
|
|
299
318
|
totalFiles,
|
|
300
319
|
contributors,
|
|
301
|
-
currentBranch: this.getCurrentBranch()
|
|
320
|
+
currentBranch: this.getCurrentBranch(),
|
|
302
321
|
};
|
|
303
322
|
} catch (error) {
|
|
304
323
|
logger.warn(`Failed to get repo stats: ${error.message}`);
|
|
@@ -313,8 +332,7 @@ export class GitClient {
|
|
|
313
332
|
*/
|
|
314
333
|
getLastCommit(filePath) {
|
|
315
334
|
try {
|
|
316
|
-
const
|
|
317
|
-
const output = this.exec(command);
|
|
335
|
+
const output = this.exec(['log', '-1', '--pretty=format:%H|%an|%ae|%at|%s', '--', filePath]);
|
|
318
336
|
|
|
319
337
|
if (!output) return null;
|
|
320
338
|
|
|
@@ -325,9 +343,9 @@ export class GitClient {
|
|
|
325
343
|
email,
|
|
326
344
|
timestamp: parseInt(timestamp, 10),
|
|
327
345
|
date: new Date(parseInt(timestamp, 10) * 1000),
|
|
328
|
-
subject
|
|
346
|
+
subject,
|
|
329
347
|
};
|
|
330
|
-
} catch (
|
|
348
|
+
} catch (_error) {
|
|
331
349
|
return null;
|
|
332
350
|
}
|
|
333
351
|
}
|