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
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Targets of `ctxman why`: `file`, `file:symbol`, `file#L10-L40` (or `file:10-40`),
|
|
3
|
+
* symbol resolution to a line range with the method analyzers, and mapping a
|
|
4
|
+
* working-tree range onto HEAD through the diff hunks between them
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import MethodAnalyzer from '../analyzers/method-analyzer.js';
|
|
9
|
+
import { WhyError } from './errors.js';
|
|
10
|
+
|
|
11
|
+
const RANGE_SUFFIX = /^(.*?)#L(\d+)(?:-L?(\d+))?$/i;
|
|
12
|
+
const LINE_SUFFIX = /^(\d+)(?:-(\d+))?$/;
|
|
13
|
+
const SYMBOL_PATTERN = /^~?[A-Za-z_$][\w$]*(?:(?:\.|::|#)~?[A-Za-z_$][\w$]*)*[?!]?$/;
|
|
14
|
+
const MEMBER_SEPARATOR = /\.|::|#/;
|
|
15
|
+
|
|
16
|
+
// class Foo, interface Foo, struct Foo, impl<T> Trait for Foo, type Foo struct, module Foo, ...
|
|
17
|
+
const CONTAINER_LINE =
|
|
18
|
+
/^\s*(?:template\s*<[^>]*>\s*)?(?:(?:export|default|public|private|protected|internal|abstract|final|sealed|open|data|static|partial|declare|unsafe|case|inline|value|annotation|pub(?:\([^)]*\))?|enum(?=\s+class))\s+)*(class|interface|enum|struct|record|trait|object|protocol|extension|module|namespace|impl|union|type)(?:\s*<[^>]*>)?\s+(?:[\w$:<>,\s]+?\s+for\s+)?([A-Za-z_$][\w$]*)/;
|
|
19
|
+
const NOT_A_NAME = new Set(['extends', 'implements', 'for', 'where', 'of', 'is']);
|
|
20
|
+
|
|
21
|
+
// Comment-only lines, for the doc comments and decorators kept with a definition
|
|
22
|
+
const HASH_COMMENT_EXTENSIONS = new Set(['.py', '.rb', '.sh', '.bash', '.zsh', '.pl', '.r']);
|
|
23
|
+
|
|
24
|
+
let sharedAnalyzer = null;
|
|
25
|
+
const analyzer = () => (sharedAnalyzer ??= new MethodAnalyzer());
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Split a target string into file and symbol or line range. `exists` decides between
|
|
29
|
+
* readings when a path itself contains ':'
|
|
30
|
+
* @param {string} spec
|
|
31
|
+
* @param {(file: string) => boolean} exists
|
|
32
|
+
* @returns {{file: string, symbol?: string, startLine?: number, endLine?: number}}
|
|
33
|
+
*/
|
|
34
|
+
export function parseTargetSpec(spec, exists = () => false) {
|
|
35
|
+
if (typeof spec !== 'string' || spec.trim() === '') {
|
|
36
|
+
throw new WhyError(
|
|
37
|
+
'INVALID_TARGET',
|
|
38
|
+
'A target is required: <file>, <file>:<symbol> or <file>#L10-L40'
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const target = spec.trim();
|
|
42
|
+
|
|
43
|
+
const range = target.match(RANGE_SUFFIX);
|
|
44
|
+
if (range && range[1]) {
|
|
45
|
+
return lineTarget(range[1], range[2], range[3] ?? range[2]);
|
|
46
|
+
}
|
|
47
|
+
if (exists(target)) return { file: target };
|
|
48
|
+
|
|
49
|
+
const readings = [];
|
|
50
|
+
for (let i = target.indexOf(':'); i !== -1; i = target.indexOf(':', i + 1)) {
|
|
51
|
+
// C:\path on Windows: a drive letter, not a separator
|
|
52
|
+
if (i === 1 && /^[A-Za-z]$/.test(target[0])) continue;
|
|
53
|
+
const file = target.slice(0, i);
|
|
54
|
+
const rest = target.slice(i + 1);
|
|
55
|
+
const lines = rest.match(LINE_SUFFIX);
|
|
56
|
+
if (lines) readings.push(lineTarget(file, lines[1], lines[2] ?? lines[1]));
|
|
57
|
+
else if (SYMBOL_PATTERN.test(rest)) readings.push({ file, symbol: rest });
|
|
58
|
+
}
|
|
59
|
+
return readings.find((reading) => exists(reading.file)) ?? readings[0] ?? { file: target };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function lineTarget(file, start, end) {
|
|
63
|
+
return { file, startLine: Number.parseInt(start, 10), endLine: Number.parseInt(end, 10) };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check a symbol name before it reaches a regular expression or git
|
|
68
|
+
* @param {string} symbol
|
|
69
|
+
* @returns {boolean}
|
|
70
|
+
*/
|
|
71
|
+
export function isValidSymbol(symbol) {
|
|
72
|
+
return typeof symbol === 'string' && symbol.length <= 200 && SYMBOL_PATTERN.test(symbol);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Functions, methods and type-like containers (class, struct, impl, ...) in a file, with
|
|
77
|
+
* their line ranges (1-based, inclusive). A range starts at the decorators and doc
|
|
78
|
+
* comment directly above the declaration
|
|
79
|
+
* @param {string} content
|
|
80
|
+
* @param {string} filePath - Picks the language rules
|
|
81
|
+
* @returns {Array<{name: string, kind: string, line: number, startLine: number, endLine: number, container: boolean}>}
|
|
82
|
+
*/
|
|
83
|
+
export function listDefinitions(content, filePath) {
|
|
84
|
+
const methodAnalyzer = analyzer();
|
|
85
|
+
const lines = content.split('\n');
|
|
86
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
87
|
+
|
|
88
|
+
const methods = methodAnalyzer
|
|
89
|
+
.extractMethods(content, filePath)
|
|
90
|
+
.filter((method) => method.line >= 1 && method.line <= lines.length);
|
|
91
|
+
const sources = methodAnalyzer.extractMethodSources(content, methods, filePath);
|
|
92
|
+
|
|
93
|
+
const containers = [];
|
|
94
|
+
lines.forEach((line, index) => {
|
|
95
|
+
const match = line.match(CONTAINER_LINE);
|
|
96
|
+
if (match && !NOT_A_NAME.has(match[2])) {
|
|
97
|
+
containers.push({ name: match[2], kind: match[1], line: index + 1 });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const declarationLines = [...new Set([...methods, ...containers].map((d) => d.line))].sort(
|
|
102
|
+
(a, b) => a - b
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const definitions = methods.map((method, index) => ({
|
|
106
|
+
name: method.name.replace(/^(get|set) /, ''),
|
|
107
|
+
kind:
|
|
108
|
+
method.name.startsWith('get ') || method.name.startsWith('set ') ? 'accessor' : 'function',
|
|
109
|
+
line: method.line,
|
|
110
|
+
endLine: method.line + sources[index].split('\n').length - 1,
|
|
111
|
+
container: false,
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
for (const container of containers) {
|
|
115
|
+
const next = declarationLines.find((line) => line > container.line);
|
|
116
|
+
const end = blockEnd(lines, container.line - 1, ext, next ? next - 1 : lines.length);
|
|
117
|
+
definitions.push({ ...container, endLine: end + 1, container: true });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return definitions
|
|
121
|
+
.map((definition) => ({
|
|
122
|
+
...definition,
|
|
123
|
+
startLine: leadingStart(lines, definition.line - 1, ext) + 1,
|
|
124
|
+
}))
|
|
125
|
+
.sort((a, b) => a.line - b.line || a.name.localeCompare(b.name));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Last line index (0-based) of the block opened at `start`
|
|
130
|
+
*/
|
|
131
|
+
function blockEnd(lines, start, ext, limit) {
|
|
132
|
+
const methodAnalyzer = analyzer();
|
|
133
|
+
if (ext === '.py') return methodAnalyzer.findIndentedBlockEnd(lines, start);
|
|
134
|
+
if (ext === '.rb') return methodAnalyzer.findRubyBlockEnd(lines, start);
|
|
135
|
+
return methodAnalyzer.findBraceBlockEnd(lines, start, limit);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Index of the first line of the decorators / attributes / doc comment directly above
|
|
140
|
+
* line `index` (no blank line in between)
|
|
141
|
+
*/
|
|
142
|
+
function leadingStart(lines, index, ext) {
|
|
143
|
+
const hashComments = HASH_COMMENT_EXTENSIONS.has(ext);
|
|
144
|
+
let first = index;
|
|
145
|
+
for (let i = index - 1; i >= 0 && index - i <= 40; i--) {
|
|
146
|
+
const text = lines[i].trim();
|
|
147
|
+
const isLeading =
|
|
148
|
+
text.startsWith('@') ||
|
|
149
|
+
text.startsWith('#[') ||
|
|
150
|
+
/^\[[A-Z][\w.]*(\(.*\))?\]$/.test(text) ||
|
|
151
|
+
/^(\/\/|\/\*|\*)/.test(text) ||
|
|
152
|
+
(hashComments && text.startsWith('#') && !text.startsWith('#!'));
|
|
153
|
+
if (!text || !isLeading) break;
|
|
154
|
+
first = i;
|
|
155
|
+
}
|
|
156
|
+
return first;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Resolve a symbol (`name`, `Class.method`, `Type::method`, `Class#method`) to a line range
|
|
161
|
+
* @param {string} content
|
|
162
|
+
* @param {string} filePath
|
|
163
|
+
* @param {string} symbol
|
|
164
|
+
* @returns {{name: string, kind: string, line: number, startLine: number, endLine: number,
|
|
165
|
+
* resolvedBy: string, matches: number}|null} null when nothing matches
|
|
166
|
+
*/
|
|
167
|
+
export function resolveSymbol(content, filePath, symbol) {
|
|
168
|
+
const definitions = listDefinitions(content, filePath);
|
|
169
|
+
const lines = content.split('\n');
|
|
170
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
171
|
+
const parts = symbol.split(MEMBER_SEPARATOR);
|
|
172
|
+
const member = parts[parts.length - 1];
|
|
173
|
+
|
|
174
|
+
let matches;
|
|
175
|
+
if (parts.length === 1) {
|
|
176
|
+
matches = definitions.filter((definition) => definition.name === member);
|
|
177
|
+
} else {
|
|
178
|
+
const containerName = parts[parts.length - 2];
|
|
179
|
+
const scopes = definitions.filter((d) => d.name === containerName && d.container);
|
|
180
|
+
const fallbackScope = scopes.length ? null : fallbackDefinition(lines, containerName, ext);
|
|
181
|
+
if (fallbackScope) scopes.push(fallbackScope);
|
|
182
|
+
matches = definitions.filter(
|
|
183
|
+
(d) =>
|
|
184
|
+
d.name === member && scopes.some((scope) => d.line > scope.line && d.line <= scope.endLine)
|
|
185
|
+
);
|
|
186
|
+
if (!matches.length) {
|
|
187
|
+
const qualified = qualifiedDefinition(lines, containerName, member, ext);
|
|
188
|
+
if (qualified) matches = [qualified];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!matches.length && parts.length === 1) {
|
|
193
|
+
const fallback = fallbackDefinition(lines, member, ext);
|
|
194
|
+
if (fallback) matches = [fallback];
|
|
195
|
+
}
|
|
196
|
+
if (!matches.length) return null;
|
|
197
|
+
|
|
198
|
+
const [best] = matches;
|
|
199
|
+
return {
|
|
200
|
+
name: symbol,
|
|
201
|
+
kind: best.kind,
|
|
202
|
+
line: best.line,
|
|
203
|
+
startLine: best.startLine,
|
|
204
|
+
endLine: Math.max(best.endLine, best.line),
|
|
205
|
+
resolvedBy: best.resolvedBy ?? 'analyzer',
|
|
206
|
+
matches: matches.length,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function escapeRegExp(text) {
|
|
211
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* A definition the analyzers do not report: `func name`, `const name =`, `name: ...`,
|
|
216
|
+
* lowercase Go functions, Lua/Elixir/shell functions and so on
|
|
217
|
+
*/
|
|
218
|
+
function fallbackDefinition(lines, name, ext) {
|
|
219
|
+
const n = escapeRegExp(name);
|
|
220
|
+
const patterns = [
|
|
221
|
+
new RegExp(
|
|
222
|
+
`\\b(?:def|defp|defn|defmacro|function|func|fn|fun|sub|proc|class|struct|interface|enum|trait|type|const|let|var|val|macro|module|object|alias)\\s+\\*?\\s*${n}(?![\\w$])`
|
|
223
|
+
),
|
|
224
|
+
new RegExp(`\\bfunc\\s*\\([^)]*\\)\\s*${n}\\s*[(<\\[]`),
|
|
225
|
+
new RegExp(
|
|
226
|
+
`^\\s*(?:(?:export|static|public|private|protected|readonly)\\s+)*(?:[\\w$]+\\.)*${n}\\s*(?::[^=\\n]*)?=(?![=>])`
|
|
227
|
+
),
|
|
228
|
+
new RegExp(`^\\s*['"]?${n}['"]?\\s*:(?!:)`),
|
|
229
|
+
];
|
|
230
|
+
return findDefinitionLine(lines, ext, patterns, 'definition');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* A member defined outside its container's block: Go receivers, C++ `Type::method`,
|
|
235
|
+
* JavaScript `Type.prototype.method =` and `Type.method =`
|
|
236
|
+
*/
|
|
237
|
+
function qualifiedDefinition(lines, container, member, ext) {
|
|
238
|
+
const c = escapeRegExp(container);
|
|
239
|
+
const m = escapeRegExp(member);
|
|
240
|
+
const patterns = [
|
|
241
|
+
new RegExp(`\\bfunc\\s*\\([^)]*\\b${c}\\s*\\)\\s*${m}\\s*[(<\\[]`),
|
|
242
|
+
new RegExp(`\\b${c}::${m}\\s*\\(`),
|
|
243
|
+
new RegExp(`\\b${c}\\.(?:prototype\\.)?${m}\\s*=(?!=)`),
|
|
244
|
+
];
|
|
245
|
+
return findDefinitionLine(lines, ext, patterns, 'qualified');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function findDefinitionLine(lines, ext, patterns, resolvedBy) {
|
|
249
|
+
for (let i = 0; i < lines.length; i++) {
|
|
250
|
+
const text = lines[i].trim();
|
|
251
|
+
if (/^(\/\/|\/\*|\*|#|--)/.test(text) && !/^#\[/.test(text)) continue;
|
|
252
|
+
if (patterns.some((pattern) => pattern.test(lines[i]))) {
|
|
253
|
+
const end = blockEnd(lines, i, ext, lines.length);
|
|
254
|
+
return {
|
|
255
|
+
kind: 'definition',
|
|
256
|
+
line: i + 1,
|
|
257
|
+
startLine: leadingStart(lines, i, ext) + 1,
|
|
258
|
+
endLine: end + 1,
|
|
259
|
+
resolvedBy,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Names in the file closest to a symbol that was not found
|
|
268
|
+
* @param {string} content
|
|
269
|
+
* @param {string} filePath
|
|
270
|
+
* @param {string} symbol
|
|
271
|
+
* @param {number} [limit]
|
|
272
|
+
* @returns {string[]}
|
|
273
|
+
*/
|
|
274
|
+
export function closeMatches(content, filePath, symbol, limit = 5) {
|
|
275
|
+
const definitions = listDefinitions(content, filePath);
|
|
276
|
+
const names = new Set(definitions.map((d) => d.name));
|
|
277
|
+
for (const d of definitions.filter((definition) => !definition.container)) {
|
|
278
|
+
const scope = definitions.find((c) => c.container && d.line > c.line && d.line <= c.endLine);
|
|
279
|
+
if (scope) names.add(`${scope.name}.${d.name}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const wanted = symbol.toLowerCase();
|
|
283
|
+
const wantedMember = wanted.split(MEMBER_SEPARATOR).pop();
|
|
284
|
+
const scored = [];
|
|
285
|
+
for (const name of names) {
|
|
286
|
+
const lower = name.toLowerCase();
|
|
287
|
+
const member = lower.split('.').pop();
|
|
288
|
+
let score;
|
|
289
|
+
if (lower === wanted || member === wantedMember) score = 0;
|
|
290
|
+
else if (lower.includes(wantedMember) || wantedMember.includes(member)) score = 1;
|
|
291
|
+
else {
|
|
292
|
+
const distance = levenshtein(wantedMember, member);
|
|
293
|
+
if (distance <= Math.max(2, Math.floor(wantedMember.length / 3))) score = 1 + distance;
|
|
294
|
+
}
|
|
295
|
+
if (score !== undefined) scored.push({ name, score });
|
|
296
|
+
}
|
|
297
|
+
return scored
|
|
298
|
+
.sort((a, b) => a.score - b.score || a.name.localeCompare(b.name))
|
|
299
|
+
.slice(0, limit)
|
|
300
|
+
.map((entry) => entry.name);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function levenshtein(a, b) {
|
|
304
|
+
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
305
|
+
for (let i = 1; i <= a.length; i++) {
|
|
306
|
+
const current = [i];
|
|
307
|
+
for (let j = 1; j <= b.length; j++) {
|
|
308
|
+
current[j] = Math.min(
|
|
309
|
+
previous[j] + 1,
|
|
310
|
+
current[j - 1] + 1,
|
|
311
|
+
previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
previous = current;
|
|
315
|
+
}
|
|
316
|
+
return previous[b.length];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Hunks of a `git diff -U0` (old = HEAD, new = working tree)
|
|
321
|
+
* @param {string} diff
|
|
322
|
+
* @returns {Array<{oldStart: number, oldCount: number, newStart: number, newCount: number}>}
|
|
323
|
+
*/
|
|
324
|
+
export function parseHunks(diff) {
|
|
325
|
+
const hunks = [];
|
|
326
|
+
for (const match of diff.matchAll(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/gm)) {
|
|
327
|
+
hunks.push({
|
|
328
|
+
oldStart: Number(match[1]),
|
|
329
|
+
oldCount: match[2] === undefined ? 1 : Number(match[2]),
|
|
330
|
+
newStart: Number(match[3]),
|
|
331
|
+
newCount: match[4] === undefined ? 1 : Number(match[4]),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
return hunks;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Map a working-tree line range onto the old (HEAD) side of the diff. Lines inside a
|
|
339
|
+
* changed block widen to the whole old block; a range made only of added lines has no
|
|
340
|
+
* old counterpart
|
|
341
|
+
* @param {ReturnType<typeof parseHunks>} hunks
|
|
342
|
+
* @param {number} start
|
|
343
|
+
* @param {number} end
|
|
344
|
+
* @returns {{startLine: number, endLine: number}|null}
|
|
345
|
+
*/
|
|
346
|
+
export function mapRangeToOld(hunks, start, end) {
|
|
347
|
+
const mapLine = (line, edge) => {
|
|
348
|
+
let offset = 0;
|
|
349
|
+
for (const hunk of hunks) {
|
|
350
|
+
const newEnd = hunk.newStart + hunk.newCount - 1;
|
|
351
|
+
if (hunk.newCount > 0 && line >= hunk.newStart && line <= newEnd) {
|
|
352
|
+
if (hunk.oldCount === 0) return edge === 'start' ? hunk.oldStart + 1 : hunk.oldStart;
|
|
353
|
+
return edge === 'start' ? hunk.oldStart : hunk.oldStart + hunk.oldCount - 1;
|
|
354
|
+
}
|
|
355
|
+
const before = hunk.newCount > 0 ? newEnd < line : hunk.newStart < line;
|
|
356
|
+
if (!before) break;
|
|
357
|
+
offset += hunk.newCount - hunk.oldCount;
|
|
358
|
+
}
|
|
359
|
+
return line - offset;
|
|
360
|
+
};
|
|
361
|
+
const startLine = mapLine(start, 'start');
|
|
362
|
+
const endLine = mapLine(end, 'end');
|
|
363
|
+
return startLine <= endLine ? { startLine, endLine } : null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The range git's default funcname rule gives a name: from the first line that starts
|
|
368
|
+
* with a letter, `_` or `$` and contains the name, up to the next such line (trailing
|
|
369
|
+
* blank lines dropped). Used to show the source when only `git log -L :name:file` found
|
|
370
|
+
* the symbol
|
|
371
|
+
* @param {string[]} lines
|
|
372
|
+
* @param {string} name
|
|
373
|
+
* @returns {{startLine: number, endLine: number}|null}
|
|
374
|
+
*/
|
|
375
|
+
export function funcnameRange(lines, name) {
|
|
376
|
+
const word = new RegExp(`(?<![\\w$])${escapeRegExp(name)}(?![\\w$])`);
|
|
377
|
+
const isFuncname = (line) => /^[A-Za-z_$]/.test(line);
|
|
378
|
+
const start = lines.findIndex((line) => isFuncname(line) && word.test(line));
|
|
379
|
+
if (start === -1) return null;
|
|
380
|
+
let end = lines.length - 1;
|
|
381
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
382
|
+
if (isFuncname(lines[i])) {
|
|
383
|
+
end = i - 1;
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
while (end > start && lines[end].trim() === '') end--;
|
|
388
|
+
return { startLine: start + 1, endLine: end + 1 };
|
|
389
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Generator
|
|
3
|
+
* Generates configuration files for ctxman
|
|
4
|
+
* FEAT-001: Configuration Wizard
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Configuration file names
|
|
12
|
+
*/
|
|
13
|
+
const CONFIG_FILES = {
|
|
14
|
+
contextignore: '.contextignore',
|
|
15
|
+
contextinclude: '.contextinclude',
|
|
16
|
+
methodinclude: '.methodinclude',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Default minimal configuration
|
|
21
|
+
*/
|
|
22
|
+
const MINIMAL_CONFIG = {
|
|
23
|
+
contextignore: ['node_modules/', '.git/', 'dist/', 'build/', 'coverage/'],
|
|
24
|
+
contextinclude: ['src/**', 'lib/**'],
|
|
25
|
+
methodinclude: [],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
class ConfigGenerator {
|
|
29
|
+
/**
|
|
30
|
+
* Create a ConfigGenerator instance
|
|
31
|
+
* @param {string} projectRoot - Project root directory
|
|
32
|
+
*/
|
|
33
|
+
constructor(projectRoot = process.cwd()) {
|
|
34
|
+
this.projectRoot = projectRoot;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if configuration files exist
|
|
39
|
+
* @returns {object} Object with existence status for each config file
|
|
40
|
+
*/
|
|
41
|
+
checkExisting() {
|
|
42
|
+
const existing = {};
|
|
43
|
+
|
|
44
|
+
for (const [type, filename] of Object.entries(CONFIG_FILES)) {
|
|
45
|
+
const filePath = path.join(this.projectRoot, filename);
|
|
46
|
+
existing[type] = fs.existsSync(filePath);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return existing;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Generate configuration files
|
|
54
|
+
* @param {object} config - Configuration object
|
|
55
|
+
* @param {object} options - Generation options
|
|
56
|
+
* @param {boolean} options.force - Overwrite existing files
|
|
57
|
+
* @param {boolean} options.minimal - Use minimal configuration
|
|
58
|
+
* @param {boolean} options.dryRun - Don't write files, just return what would be written
|
|
59
|
+
* @returns {object} Generation result
|
|
60
|
+
*/
|
|
61
|
+
generate(config, options = {}) {
|
|
62
|
+
const { force = false, minimal = false, dryRun = false } = options;
|
|
63
|
+
|
|
64
|
+
// Use minimal config if requested
|
|
65
|
+
const configToUse = minimal ? MINIMAL_CONFIG : config;
|
|
66
|
+
|
|
67
|
+
const result = {
|
|
68
|
+
created: [],
|
|
69
|
+
skipped: [],
|
|
70
|
+
overwritten: [],
|
|
71
|
+
errors: [],
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Generate each config file
|
|
75
|
+
for (const [type, filename] of Object.entries(CONFIG_FILES)) {
|
|
76
|
+
const patterns = configToUse[type] || [];
|
|
77
|
+
|
|
78
|
+
// Skip methodinclude if empty
|
|
79
|
+
if (type === 'methodinclude' && patterns.length === 0) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const filePath = path.join(this.projectRoot, filename);
|
|
84
|
+
const skip = () =>
|
|
85
|
+
result.skipped.push({
|
|
86
|
+
type,
|
|
87
|
+
filename,
|
|
88
|
+
reason: 'File already exists (use --force to overwrite)',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (dryRun) {
|
|
92
|
+
const exists = fs.existsSync(filePath);
|
|
93
|
+
if (exists && !force) {
|
|
94
|
+
skip();
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const content = this.generateFileContent(type, patterns);
|
|
98
|
+
(exists ? result.overwritten : result.created).push({ type, filename, content });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const content = this.generateFileContent(type, patterns);
|
|
103
|
+
// Create exclusively first, so an existing file is found by the write itself rather
|
|
104
|
+
// than by a separate check that could be out of date
|
|
105
|
+
try {
|
|
106
|
+
try {
|
|
107
|
+
fs.writeFileSync(filePath, content, { encoding: 'utf-8', flag: 'wx' });
|
|
108
|
+
result.created.push({ type, filename });
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (error.code !== 'EEXIST') throw error;
|
|
111
|
+
if (!force) {
|
|
112
|
+
skip();
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
116
|
+
result.overwritten.push({ type, filename });
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
result.errors.push({
|
|
120
|
+
type,
|
|
121
|
+
filename,
|
|
122
|
+
error: error.message,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Generate file content for a config type
|
|
132
|
+
* @param {string} type - Config type
|
|
133
|
+
* @param {Array<string>} patterns - Array of patterns
|
|
134
|
+
* @returns {string} File content
|
|
135
|
+
*/
|
|
136
|
+
generateFileContent(type, patterns) {
|
|
137
|
+
const header = this.getFileHeader(type);
|
|
138
|
+
const lines = patterns.filter((p) => p && p.trim());
|
|
139
|
+
|
|
140
|
+
return header + lines.join('\n') + '\n';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get header comment for config file
|
|
145
|
+
* @param {string} type - Config type
|
|
146
|
+
* @returns {string} Header comment
|
|
147
|
+
*/
|
|
148
|
+
getFileHeader(type) {
|
|
149
|
+
const descriptions = {
|
|
150
|
+
contextignore:
|
|
151
|
+
'# Files and patterns to exclude from context generation\n# One pattern per line\n',
|
|
152
|
+
contextinclude:
|
|
153
|
+
'# Files and patterns to include in context generation\n# One pattern per line\n',
|
|
154
|
+
methodinclude:
|
|
155
|
+
'# Method patterns to include in method-level analysis\n# One pattern per line\n',
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
return `# Generated by ctxman init\n# ${descriptions[type] || ''}\n`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Read existing configuration
|
|
163
|
+
* @returns {object} Existing configuration
|
|
164
|
+
*/
|
|
165
|
+
readExisting() {
|
|
166
|
+
const config = {
|
|
167
|
+
contextignore: [],
|
|
168
|
+
contextinclude: [],
|
|
169
|
+
methodinclude: [],
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
for (const [type, filename] of Object.entries(CONFIG_FILES)) {
|
|
173
|
+
const filePath = path.join(this.projectRoot, filename);
|
|
174
|
+
|
|
175
|
+
if (fs.existsSync(filePath)) {
|
|
176
|
+
try {
|
|
177
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
178
|
+
config[type] = content
|
|
179
|
+
.split('\n')
|
|
180
|
+
.map((line) => line.trim())
|
|
181
|
+
.filter((line) => line && !line.startsWith('#'));
|
|
182
|
+
} catch (_error) {
|
|
183
|
+
// Ignore read errors
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return config;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Merge existing config with new config
|
|
193
|
+
* @param {object} existing - Existing configuration
|
|
194
|
+
* @param {object} newConfig - New configuration
|
|
195
|
+
* @returns {object} Merged configuration
|
|
196
|
+
*/
|
|
197
|
+
mergeConfigs(existing, newConfig) {
|
|
198
|
+
const merged = {};
|
|
199
|
+
|
|
200
|
+
for (const type of Object.keys(CONFIG_FILES)) {
|
|
201
|
+
const existingPatterns = existing[type] || [];
|
|
202
|
+
const newPatterns = newConfig[type] || [];
|
|
203
|
+
|
|
204
|
+
// Combine and deduplicate
|
|
205
|
+
merged[type] = [...new Set([...existingPatterns, ...newPatterns])];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return merged;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Format config for display
|
|
213
|
+
* @param {object} config - Configuration object
|
|
214
|
+
* @param {string} type - Config type
|
|
215
|
+
* @returns {string} Formatted string
|
|
216
|
+
*/
|
|
217
|
+
formatForDisplay(config, type) {
|
|
218
|
+
const patterns = config[type] || [];
|
|
219
|
+
if (patterns.length === 0) {
|
|
220
|
+
return ' (empty)';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return patterns.map((p) => ` ${p}`).join('\n');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Preview configuration without writing
|
|
228
|
+
* @param {object} config - Configuration object
|
|
229
|
+
* @returns {string} Preview string
|
|
230
|
+
*/
|
|
231
|
+
preview(config) {
|
|
232
|
+
const lines = [];
|
|
233
|
+
|
|
234
|
+
lines.push('📝 Configuration Preview:');
|
|
235
|
+
lines.push('');
|
|
236
|
+
|
|
237
|
+
for (const [type, filename] of Object.entries(CONFIG_FILES)) {
|
|
238
|
+
const patterns = config[type] || [];
|
|
239
|
+
if (type === 'methodinclude' && patterns.length === 0) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
lines.push(`${filename}:`);
|
|
244
|
+
if (patterns.length === 0) {
|
|
245
|
+
lines.push(' (no patterns)');
|
|
246
|
+
} else {
|
|
247
|
+
patterns.forEach((p) => lines.push(` ${p}`));
|
|
248
|
+
}
|
|
249
|
+
lines.push('');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return lines.join('\n');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export default ConfigGenerator;
|
|
257
|
+
export { CONFIG_FILES, MINIMAL_CONFIG };
|