ctxman 3.0.0 → 3.1.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/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 +380 -136
- package/bin/cli.js +1880 -529
- package/bin/cm-ask.js +18 -13
- package/bin/cm-audit.js +124 -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 +683 -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 +675 -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 +113 -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 +294 -0
- package/lib/policy/enforce.js +125 -0
- package/lib/policy/ignore-files.js +169 -0
- package/lib/policy/index.js +52 -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/sources/FileDataSource.js +6 -6
- 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 +243 -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 +255 -0
- package/lib/wizards/project-detector.js +571 -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
package/lib/utils/git-utils.js
CHANGED
|
@@ -4,334 +4,406 @@
|
|
|
4
4
|
* v2.3.6+
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { execSync } from 'child_process';
|
|
7
|
+
import { execSync, execFileSync } from 'child_process';
|
|
8
8
|
import fs from 'fs';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import https from 'https';
|
|
11
11
|
import TokenCalculator from '../analyzers/token-calculator.js';
|
|
12
12
|
import GitIngestFormatter from '../formatters/gitingest-formatter.js';
|
|
13
|
+
import { isValidGitRef } from '../integrations/git/GitClient.js';
|
|
14
|
+
import { enforceExport, PolicyViolationError } from '../policy/enforce.js';
|
|
15
|
+
import { recordExport, getGitHead } from '../policy/audit.js';
|
|
13
16
|
|
|
14
17
|
class GitUtils {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this.tempDir = options.tempDir || path.join(process.cwd(), '.ctxman', 'temp');
|
|
20
|
+
this.outputDir = options.outputDir || path.join(process.cwd(), 'docs');
|
|
21
|
+
this.verbose = options.verbose || false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parse GitHub URL and extract repo info
|
|
26
|
+
*/
|
|
27
|
+
parseGitHubURL(url) {
|
|
28
|
+
// Support various GitHub URL formats:
|
|
29
|
+
// - https://github.com/owner/repo
|
|
30
|
+
// - https://github.com/owner/repo.git
|
|
31
|
+
// - git@github.com:owner/repo.git
|
|
32
|
+
// - github.com/owner/repo
|
|
33
|
+
// - owner/repo
|
|
34
|
+
|
|
35
|
+
let cleanUrl = url.trim();
|
|
36
|
+
|
|
37
|
+
// Remove git@ prefix
|
|
38
|
+
if (cleanUrl.startsWith('git@')) {
|
|
39
|
+
cleanUrl = cleanUrl.replace('git@github.com:', 'https://github.com/');
|
|
19
40
|
}
|
|
20
41
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// - owner/repo
|
|
31
|
-
|
|
32
|
-
let cleanUrl = url.trim();
|
|
33
|
-
|
|
34
|
-
// Remove git@ prefix
|
|
35
|
-
if (cleanUrl.startsWith('git@')) {
|
|
36
|
-
cleanUrl = cleanUrl.replace('git@github.com:', 'https://github.com/');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Add https if missing
|
|
40
|
-
if (!cleanUrl.startsWith('http')) {
|
|
41
|
-
if (cleanUrl.includes('/')) {
|
|
42
|
-
cleanUrl = `https://github.com/${cleanUrl}`;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Remove .git suffix
|
|
47
|
-
cleanUrl = cleanUrl.replace(/\.git$/, '');
|
|
48
|
-
|
|
49
|
-
// Parse URL
|
|
50
|
-
try {
|
|
51
|
-
const urlObj = new URL(cleanUrl);
|
|
52
|
-
const pathParts = urlObj.pathname.split('/').filter(p => p);
|
|
53
|
-
|
|
54
|
-
if (pathParts.length < 2) {
|
|
55
|
-
throw new Error('Invalid GitHub URL: must include owner and repo');
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const owner = pathParts[0];
|
|
59
|
-
const repo = pathParts[1];
|
|
60
|
-
const branch = pathParts[3] || 'main'; // If /tree/branch specified
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
owner,
|
|
64
|
-
repo,
|
|
65
|
-
branch,
|
|
66
|
-
fullName: `${owner}/${repo}`,
|
|
67
|
-
cloneUrl: `https://github.com/${owner}/${repo}.git`,
|
|
68
|
-
apiUrl: `https://api.github.com/repos/${owner}/${repo}`,
|
|
69
|
-
rawUrl: `https://raw.githubusercontent.com/${owner}/${repo}/${branch}`,
|
|
70
|
-
originalUrl: url
|
|
71
|
-
};
|
|
72
|
-
} catch (error) {
|
|
73
|
-
throw new Error(`Failed to parse GitHub URL: ${error.message}`);
|
|
74
|
-
}
|
|
42
|
+
// Add https if missing
|
|
43
|
+
if (!cleanUrl.startsWith('http')) {
|
|
44
|
+
// "github.com/owner/repo" only lacks the scheme; prefixing the host again would
|
|
45
|
+
// turn "github.com" into the owner
|
|
46
|
+
if (/^github\.com\//i.test(cleanUrl)) {
|
|
47
|
+
cleanUrl = `https://${cleanUrl}`;
|
|
48
|
+
} else if (cleanUrl.includes('/')) {
|
|
49
|
+
cleanUrl = `https://github.com/${cleanUrl}`;
|
|
50
|
+
}
|
|
75
51
|
}
|
|
76
52
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
53
|
+
// Remove .git suffix
|
|
54
|
+
cleanUrl = cleanUrl.replace(/\.git$/, '');
|
|
55
|
+
|
|
56
|
+
// Parse URL
|
|
57
|
+
try {
|
|
58
|
+
const urlObj = new URL(cleanUrl);
|
|
59
|
+
const pathParts = urlObj.pathname.split('/').filter((p) => p);
|
|
60
|
+
|
|
61
|
+
if (pathParts.length < 2) {
|
|
62
|
+
throw new Error('Invalid GitHub URL: must include owner and repo');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const owner = pathParts[0];
|
|
66
|
+
const repo = pathParts[1];
|
|
67
|
+
const branchFromUrl = Boolean(pathParts[3]); // /owner/repo/tree/<branch>
|
|
68
|
+
const branch = pathParts[3] || 'main';
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
owner,
|
|
72
|
+
repo,
|
|
73
|
+
branch,
|
|
74
|
+
branchFromUrl,
|
|
75
|
+
fullName: `${owner}/${repo}`,
|
|
76
|
+
cloneUrl: `https://github.com/${owner}/${repo}.git`,
|
|
77
|
+
apiUrl: `https://api.github.com/repos/${owner}/${repo}`,
|
|
78
|
+
rawUrl: `https://raw.githubusercontent.com/${owner}/${repo}/${branch}`,
|
|
79
|
+
originalUrl: url,
|
|
80
|
+
};
|
|
81
|
+
} catch (error) {
|
|
82
|
+
throw new Error(`Failed to parse GitHub URL: ${error.message}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if git is installed
|
|
88
|
+
*/
|
|
89
|
+
isGitInstalled() {
|
|
90
|
+
try {
|
|
91
|
+
execSync('git --version', { stdio: 'pipe' });
|
|
92
|
+
return true;
|
|
93
|
+
} catch (_error) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Clone repository to temp directory
|
|
100
|
+
*/
|
|
101
|
+
cloneRepository(repoInfo, options = {}) {
|
|
102
|
+
if (!this.isGitInstalled()) {
|
|
103
|
+
throw new Error('Git is not installed. Please install git to use this feature.');
|
|
87
104
|
}
|
|
88
105
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
*/
|
|
92
|
-
cloneRepository(repoInfo, options = {}) {
|
|
93
|
-
if (!this.isGitInstalled()) {
|
|
94
|
-
throw new Error('Git is not installed. Please install git to use this feature.');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const { shallow = true, depth = 1 } = options;
|
|
98
|
-
const repoPath = path.join(this.tempDir, repoInfo.fullName.replace('/', '-'));
|
|
99
|
-
|
|
100
|
-
// Create temp directory
|
|
101
|
-
if (!fs.existsSync(this.tempDir)) {
|
|
102
|
-
fs.mkdirSync(this.tempDir, { recursive: true });
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Remove existing clone if present
|
|
106
|
-
if (fs.existsSync(repoPath)) {
|
|
107
|
-
if (this.verbose) {
|
|
108
|
-
console.log(`♻️ Removing existing clone: ${repoPath}`);
|
|
109
|
-
}
|
|
110
|
-
fs.rmSync(repoPath, { recursive: true, force: true });
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Clone repository
|
|
114
|
-
if (this.verbose) {
|
|
115
|
-
console.log(`📥 Cloning ${repoInfo.fullName}...`);
|
|
116
|
-
}
|
|
106
|
+
const { shallow = true, depth = 1 } = options;
|
|
107
|
+
const repoPath = path.join(this.tempDir, repoInfo.fullName.replace('/', '-'));
|
|
117
108
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
109
|
+
// Create temp directory
|
|
110
|
+
if (!fs.existsSync(this.tempDir)) {
|
|
111
|
+
fs.mkdirSync(this.tempDir, { recursive: true });
|
|
112
|
+
}
|
|
121
113
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
114
|
+
// Remove existing clone if present
|
|
115
|
+
if (fs.existsSync(repoPath)) {
|
|
116
|
+
if (this.verbose) {
|
|
117
|
+
console.log(`♻️ Removing existing clone: ${repoPath}`);
|
|
118
|
+
}
|
|
119
|
+
fs.rmSync(repoPath, { recursive: true, force: true });
|
|
120
|
+
}
|
|
126
121
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
122
|
+
// Clone repository
|
|
123
|
+
if (this.verbose) {
|
|
124
|
+
console.log(`📥 Cloning ${repoInfo.fullName}...`);
|
|
125
|
+
}
|
|
130
126
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
throw new Error(`Failed to clone repository: ${error.message}`);
|
|
134
|
-
}
|
|
127
|
+
if (repoInfo.branch && !isValidGitRef(repoInfo.branch)) {
|
|
128
|
+
throw new Error(`Invalid branch name: ${JSON.stringify(repoInfo.branch)}`);
|
|
135
129
|
}
|
|
136
130
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
131
|
+
// Argument list, no shell. Option values are attached (`--branch=x`) so none of them
|
|
132
|
+
// is a standalone argument, and `--` ends option parsing before the URL and path
|
|
133
|
+
// Without a branch, git clones the remote's default branch
|
|
134
|
+
const branchArgs = repoInfo.branch ? [`--branch=${repoInfo.branch}`] : [];
|
|
135
|
+
const shallowArgs = shallow
|
|
136
|
+
? [`--depth=${Number.parseInt(depth, 10) || 1}`, '--single-branch', ...branchArgs]
|
|
137
|
+
: branchArgs;
|
|
138
|
+
const cloneArgs = ['clone', ...shallowArgs, '--', repoInfo.cloneUrl, repoPath];
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
execFileSync('git', cloneArgs, {
|
|
142
|
+
stdio: this.verbose ? 'inherit' : 'pipe',
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (this.verbose) {
|
|
146
|
+
console.log(`✅ Repository cloned to: ${repoPath}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return repoPath;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
throw new Error(`Failed to clone repository: ${error.message}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Generate GitIngest digest from GitHub URL
|
|
157
|
+
*/
|
|
158
|
+
async generateFromGitHub(url, options = {}) {
|
|
159
|
+
const { outputFile, cleanup = true, shallow = true, analyzerOptions = {} } = options;
|
|
160
|
+
// Export policy from loadPolicy() (optional): deny rules, limits and audit log
|
|
161
|
+
const policy = options.policy?.active ? options.policy : null;
|
|
162
|
+
|
|
163
|
+
// Parse GitHub URL. An explicit branch wins, then one named in the URL (/tree/<branch>);
|
|
164
|
+
// otherwise the clone uses the remote's default branch
|
|
165
|
+
const repoInfo = this.parseGitHubURL(url);
|
|
166
|
+
repoInfo.branch = options.branch || (repoInfo.branchFromUrl ? repoInfo.branch : null);
|
|
167
|
+
console.log(`\n📦 Processing GitHub Repository: ${repoInfo.fullName}`);
|
|
168
|
+
console.log(` Branch: ${repoInfo.branch || '(default)'}`);
|
|
169
|
+
|
|
170
|
+
// Clone repository
|
|
171
|
+
console.log(`\n📥 Cloning repository...`);
|
|
172
|
+
const repoPath = this.cloneRepository(repoInfo, { shallow });
|
|
173
|
+
|
|
174
|
+
// Generate GitIngest digest
|
|
175
|
+
console.log(`\n🔍 Analyzing repository...`);
|
|
176
|
+
|
|
177
|
+
const analyzer = new TokenCalculator(repoPath, {
|
|
178
|
+
verbose: this.verbose,
|
|
179
|
+
policy,
|
|
180
|
+
...analyzerOptions,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Run analysis
|
|
184
|
+
const results = analyzer.analyze();
|
|
185
|
+
|
|
186
|
+
const audit = {
|
|
187
|
+
command: 'github',
|
|
188
|
+
root: `https://github.com/${repoInfo.fullName}`,
|
|
189
|
+
gitHead: policy ? getGitHead(repoPath) : null,
|
|
190
|
+
format: 'gitingest',
|
|
191
|
+
files: results,
|
|
192
|
+
tokens: analyzer.stats.totalTokens,
|
|
193
|
+
removedByPolicy: analyzer.stats.policyDeniedFiles ?? 0,
|
|
194
|
+
};
|
|
195
|
+
if (policy) {
|
|
196
|
+
if (audit.removedByPolicy > 0) {
|
|
197
|
+
console.log(`🛡️ Removed ${audit.removedByPolicy} file(s) denied by the export policy`);
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
enforceExport(policy, {
|
|
201
|
+
...audit,
|
|
202
|
+
redact: options.formatterOptions?.redactSecrets !== false,
|
|
163
203
|
});
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
// Generate digest
|
|
169
|
-
const formatter = new GitIngestFormatter(
|
|
170
|
-
repoPath,
|
|
171
|
-
analyzer.stats,
|
|
172
|
-
results,
|
|
173
|
-
options.formatterOptions || {}
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
const digest = formatter.generateDigest();
|
|
177
|
-
|
|
178
|
-
// Determine output file
|
|
179
|
-
const outputFileName = outputFile ||
|
|
180
|
-
`${repoInfo.fullName.replace('/', '-')}-gitingest-${Date.now().toString(36)}.txt`;
|
|
181
|
-
|
|
182
|
-
const outputPath = path.isAbsolute(outputFileName)
|
|
183
|
-
? outputFileName
|
|
184
|
-
: path.join(this.outputDir, outputFileName);
|
|
185
|
-
|
|
186
|
-
// Ensure output directory exists
|
|
187
|
-
const outputDirPath = path.dirname(outputPath);
|
|
188
|
-
if (!fs.existsSync(outputDirPath)) {
|
|
189
|
-
fs.mkdirSync(outputDirPath, { recursive: true });
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Save digest
|
|
193
|
-
fs.writeFileSync(outputPath, digest, 'utf8');
|
|
194
|
-
|
|
195
|
-
const stats = {
|
|
196
|
-
repository: repoInfo.fullName,
|
|
197
|
-
branch: repoInfo.branch,
|
|
198
|
-
url: repoInfo.originalUrl,
|
|
199
|
-
localPath: repoPath,
|
|
200
|
-
outputFile: outputPath,
|
|
201
|
-
digestSize: digest.length,
|
|
202
|
-
digestSizeKB: (digest.length / 1024).toFixed(2),
|
|
203
|
-
files: analyzer.stats.totalFiles,
|
|
204
|
-
tokens: analyzer.stats.totalTokens,
|
|
205
|
-
lines: analyzer.stats.totalLines
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
// Cleanup temp directory if requested
|
|
209
|
-
if (cleanup) {
|
|
210
|
-
console.log(`\n🧹 Cleaning up temporary files...`);
|
|
211
|
-
fs.rmSync(repoPath, { recursive: true, force: true });
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error instanceof PolicyViolationError) {
|
|
206
|
+
recordExport(policy, { ...audit, outcome: 'refused', violations: error.violations });
|
|
207
|
+
if (cleanup) fs.rmSync(repoPath, { recursive: true, force: true });
|
|
212
208
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
console.log(` Output: ${outputPath}`);
|
|
216
|
-
console.log(` Size: ${stats.digestSizeKB} KB`);
|
|
217
|
-
console.log(` Files: ${stats.files}`);
|
|
218
|
-
console.log(` Tokens: ${stats.tokens.toLocaleString()}`);
|
|
219
|
-
|
|
220
|
-
return stats;
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
221
211
|
}
|
|
222
212
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
213
|
+
// Generate digest
|
|
214
|
+
const formatter = new GitIngestFormatter(
|
|
215
|
+
repoPath,
|
|
216
|
+
analyzer.stats,
|
|
217
|
+
results,
|
|
218
|
+
options.formatterOptions || {}
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// Determine output file
|
|
222
|
+
const outputFileName =
|
|
223
|
+
outputFile ||
|
|
224
|
+
`${repoInfo.fullName.replace('/', '-')}-gitingest-${Date.now().toString(36)}.txt`;
|
|
225
|
+
|
|
226
|
+
const outputPath = path.isAbsolute(outputFileName)
|
|
227
|
+
? outputFileName
|
|
228
|
+
: path.join(this.outputDir, outputFileName);
|
|
229
|
+
|
|
230
|
+
// Ensure output directory exists
|
|
231
|
+
const outputDirPath = path.dirname(outputPath);
|
|
232
|
+
if (!fs.existsSync(outputDirPath)) {
|
|
233
|
+
fs.mkdirSync(outputDirPath, { recursive: true });
|
|
244
234
|
}
|
|
245
235
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
let data = '';
|
|
259
|
-
|
|
260
|
-
res.on('data', (chunk) => {
|
|
261
|
-
data += chunk;
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
res.on('end', () => {
|
|
265
|
-
try {
|
|
266
|
-
const repo = JSON.parse(data);
|
|
267
|
-
resolve({
|
|
268
|
-
name: repo.name,
|
|
269
|
-
fullName: repo.full_name,
|
|
270
|
-
description: repo.description,
|
|
271
|
-
stars: repo.stargazers_count,
|
|
272
|
-
forks: repo.forks_count,
|
|
273
|
-
defaultBranch: repo.default_branch,
|
|
274
|
-
size: repo.size,
|
|
275
|
-
language: repo.language,
|
|
276
|
-
updatedAt: repo.updated_at
|
|
277
|
-
});
|
|
278
|
-
} catch (error) {
|
|
279
|
-
reject(new Error(`Failed to parse GitHub API response: ${error.message}`));
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
}).on('error', reject);
|
|
283
|
-
});
|
|
236
|
+
// Save digest (a chunked digest is written as one file per chunk)
|
|
237
|
+
const digestLength = formatter.saveToFile(outputPath);
|
|
238
|
+
|
|
239
|
+
if (policy?.audit.enabled) {
|
|
240
|
+
const written = formatter.chunkFiles.length ? formatter.chunkFiles : [outputPath];
|
|
241
|
+
recordExport(policy, {
|
|
242
|
+
...audit,
|
|
243
|
+
destination: outputPath,
|
|
244
|
+
outputFiles: written.length > 1 ? written : undefined,
|
|
245
|
+
redactions: formatter.redactions,
|
|
246
|
+
text: written.map((file) => fs.readFileSync(file)),
|
|
247
|
+
});
|
|
284
248
|
}
|
|
285
249
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
250
|
+
const stats = {
|
|
251
|
+
repository: repoInfo.fullName,
|
|
252
|
+
branch: repoInfo.branch,
|
|
253
|
+
url: repoInfo.originalUrl,
|
|
254
|
+
localPath: repoPath,
|
|
255
|
+
outputFile: formatter.chunkFiles.length ? formatter.chunkFiles[0] : outputPath,
|
|
256
|
+
chunkFiles: formatter.chunkFiles,
|
|
257
|
+
digestSize: digestLength,
|
|
258
|
+
digestSizeKB: (digestLength / 1024).toFixed(2),
|
|
259
|
+
files: analyzer.stats.totalFiles,
|
|
260
|
+
tokens: analyzer.stats.totalTokens,
|
|
261
|
+
lines: analyzer.stats.totalLines,
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Cleanup temp directory if requested
|
|
265
|
+
if (cleanup) {
|
|
266
|
+
console.log(`\n🧹 Cleaning up temporary files...`);
|
|
267
|
+
fs.rmSync(repoPath, { recursive: true, force: true });
|
|
296
268
|
}
|
|
297
269
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
return fs.readdirSync(this.tempDir)
|
|
307
|
-
.filter(name => fs.statSync(path.join(this.tempDir, name)).isDirectory())
|
|
308
|
-
.map(name => ({
|
|
309
|
-
name,
|
|
310
|
-
path: path.join(this.tempDir, name),
|
|
311
|
-
size: this.getDirectorySize(path.join(this.tempDir, name))
|
|
312
|
-
}));
|
|
270
|
+
console.log(`\n✅ GitIngest digest generated!`);
|
|
271
|
+
if (stats.chunkFiles.length) {
|
|
272
|
+
console.log(` Output: ${stats.chunkFiles.length} chunk files`);
|
|
273
|
+
stats.chunkFiles.forEach((file) => console.log(` ${file}`));
|
|
274
|
+
} else {
|
|
275
|
+
console.log(` Output: ${outputPath}`);
|
|
313
276
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
277
|
+
console.log(` Size: ${stats.digestSizeKB} KB`);
|
|
278
|
+
console.log(` Files: ${stats.files}`);
|
|
279
|
+
console.log(` Tokens: ${stats.tokens.toLocaleString()}`);
|
|
280
|
+
|
|
281
|
+
return stats;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Fetch file from GitHub raw URL (without cloning)
|
|
286
|
+
*/
|
|
287
|
+
async fetchFileFromGitHub(url) {
|
|
288
|
+
return new Promise((resolve, reject) => {
|
|
289
|
+
https
|
|
290
|
+
.get(url, (res) => {
|
|
291
|
+
let data = '';
|
|
292
|
+
|
|
293
|
+
res.on('data', (chunk) => {
|
|
294
|
+
data += chunk;
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
res.on('end', () => {
|
|
298
|
+
if (res.statusCode === 200) {
|
|
299
|
+
resolve(data);
|
|
328
300
|
} else {
|
|
329
|
-
|
|
301
|
+
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
|
|
330
302
|
}
|
|
331
|
-
|
|
303
|
+
});
|
|
304
|
+
})
|
|
305
|
+
.on('error', reject);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Get repository info from GitHub API
|
|
311
|
+
*/
|
|
312
|
+
async getRepositoryInfo(repoInfo) {
|
|
313
|
+
return new Promise((resolve, reject) => {
|
|
314
|
+
const options = {
|
|
315
|
+
headers: {
|
|
316
|
+
'User-Agent': 'ctxman-git-utils',
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
https
|
|
321
|
+
.get(repoInfo.apiUrl, options, (res) => {
|
|
322
|
+
let data = '';
|
|
323
|
+
|
|
324
|
+
res.on('data', (chunk) => {
|
|
325
|
+
data += chunk;
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
res.on('end', () => {
|
|
329
|
+
try {
|
|
330
|
+
const repo = JSON.parse(data);
|
|
331
|
+
if (res.statusCode !== 200) {
|
|
332
|
+
reject(
|
|
333
|
+
new Error(`GitHub API returned ${res.statusCode}: ${repo.message || 'error'}`)
|
|
334
|
+
);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
resolve({
|
|
338
|
+
name: repo.name,
|
|
339
|
+
fullName: repo.full_name,
|
|
340
|
+
description: repo.description,
|
|
341
|
+
stars: repo.stargazers_count,
|
|
342
|
+
forks: repo.forks_count,
|
|
343
|
+
defaultBranch: repo.default_branch,
|
|
344
|
+
size: repo.size,
|
|
345
|
+
language: repo.language,
|
|
346
|
+
updatedAt: repo.updated_at,
|
|
347
|
+
});
|
|
348
|
+
} catch (error) {
|
|
349
|
+
reject(new Error(`Failed to parse GitHub API response: ${error.message}`));
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
})
|
|
353
|
+
.on('error', reject);
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Cleanup temp directory
|
|
359
|
+
*/
|
|
360
|
+
cleanupTemp() {
|
|
361
|
+
if (fs.existsSync(this.tempDir)) {
|
|
362
|
+
fs.rmSync(this.tempDir, { recursive: true, force: true });
|
|
363
|
+
if (this.verbose) {
|
|
364
|
+
console.log(`🧹 Cleaned up: ${this.tempDir}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* List cached repositories
|
|
371
|
+
*/
|
|
372
|
+
listCachedRepos() {
|
|
373
|
+
if (!fs.existsSync(this.tempDir)) {
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
332
376
|
|
|
333
|
-
|
|
377
|
+
return fs
|
|
378
|
+
.readdirSync(this.tempDir)
|
|
379
|
+
.filter((name) => fs.statSync(path.join(this.tempDir, name)).isDirectory())
|
|
380
|
+
.map((name) => ({
|
|
381
|
+
name,
|
|
382
|
+
path: path.join(this.tempDir, name),
|
|
383
|
+
size: this.getDirectorySize(path.join(this.tempDir, name)),
|
|
384
|
+
}));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Get directory size recursively
|
|
389
|
+
*/
|
|
390
|
+
getDirectorySize(dir) {
|
|
391
|
+
let size = 0;
|
|
392
|
+
|
|
393
|
+
const files = fs.readdirSync(dir);
|
|
394
|
+
for (const file of files) {
|
|
395
|
+
const filePath = path.join(dir, file);
|
|
396
|
+
const stats = fs.statSync(filePath);
|
|
397
|
+
|
|
398
|
+
if (stats.isDirectory()) {
|
|
399
|
+
size += this.getDirectorySize(filePath);
|
|
400
|
+
} else {
|
|
401
|
+
size += stats.size;
|
|
402
|
+
}
|
|
334
403
|
}
|
|
404
|
+
|
|
405
|
+
return size;
|
|
406
|
+
}
|
|
335
407
|
}
|
|
336
408
|
|
|
337
409
|
export default GitUtils;
|