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,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code units of a source file: methods/functions (from the existing method analyzers) and
|
|
3
|
+
* class-like declarations, each with the line range of its source.
|
|
4
|
+
*
|
|
5
|
+
* Shared by the ranked search (lib/search/bm25.js) and symbol lookup (lib/search/symbols.js).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import MethodAnalyzer from '../analyzers/method-analyzer.js';
|
|
10
|
+
|
|
11
|
+
/** Extensions the method analyzers understand; other files have no code units */
|
|
12
|
+
export const CODE_EXTENSIONS = new Set([
|
|
13
|
+
'.js',
|
|
14
|
+
'.jsx',
|
|
15
|
+
'.mjs',
|
|
16
|
+
'.cjs',
|
|
17
|
+
'.ts',
|
|
18
|
+
'.tsx',
|
|
19
|
+
'.mts',
|
|
20
|
+
'.cts',
|
|
21
|
+
'.rs',
|
|
22
|
+
'.go',
|
|
23
|
+
'.java',
|
|
24
|
+
'.cs',
|
|
25
|
+
'.py',
|
|
26
|
+
'.php',
|
|
27
|
+
'.rb',
|
|
28
|
+
'.kt',
|
|
29
|
+
'.kts',
|
|
30
|
+
'.swift',
|
|
31
|
+
'.c',
|
|
32
|
+
'.cpp',
|
|
33
|
+
'.cc',
|
|
34
|
+
'.h',
|
|
35
|
+
'.hpp',
|
|
36
|
+
'.scala',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const MAX_AVERAGE_LINE_LENGTH = 250;
|
|
40
|
+
|
|
41
|
+
// Modifiers that may precede a class-like keyword in the brace languages
|
|
42
|
+
const MODIFIERS =
|
|
43
|
+
'(?:(?:export|default|declare|public|private|protected|internal|static|abstract|final|sealed|' +
|
|
44
|
+
'partial|open|data|inner|value|annotation|readonly|override|case|implicit|lazy|unsafe|' +
|
|
45
|
+
'pub(?:\\([^)]*\\))?|fileprivate)\\s+)*';
|
|
46
|
+
|
|
47
|
+
const CLASS_PATTERNS = {
|
|
48
|
+
brace: new RegExp(
|
|
49
|
+
`^[ \\t]*${MODIFIERS}(class|interface|enum(?:\\s+(?:class|struct))?|struct|record|trait|object|protocol|extension|union)\\s+([A-Za-z_$][\\w$]*)`,
|
|
50
|
+
'gm'
|
|
51
|
+
),
|
|
52
|
+
rustImpl:
|
|
53
|
+
/^[ \t]*(?:unsafe\s+)?impl(?:\s*<[^>]*>)?\s+(?:[\w:]+(?:<[^>]*>)?\s+for\s+)?([A-Za-z_]\w*)/gm,
|
|
54
|
+
goType: /^type\s+([A-Za-z_]\w*)\s+(struct|interface)\b/gm,
|
|
55
|
+
python: /^[ \t]*class\s+([A-Za-z_]\w*)/gm,
|
|
56
|
+
ruby: /^[ \t]*(class|module)\s+([A-Z]\w*(?:::\w+)*)/gm,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const analyzer = new MethodAnalyzer();
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Function mapping a string index to its 1-based line number
|
|
63
|
+
* @param {string} content
|
|
64
|
+
* @returns {(index: number) => number}
|
|
65
|
+
*/
|
|
66
|
+
function lineLocator(content) {
|
|
67
|
+
const breaks = [];
|
|
68
|
+
for (let i = content.indexOf('\n'); i !== -1; i = content.indexOf('\n', i + 1)) breaks.push(i);
|
|
69
|
+
return (index) => {
|
|
70
|
+
// Number of line breaks before `index`
|
|
71
|
+
let low = 0;
|
|
72
|
+
let high = breaks.length;
|
|
73
|
+
while (low < high) {
|
|
74
|
+
const mid = (low + high) >> 1;
|
|
75
|
+
if (breaks[mid] < index) low = mid + 1;
|
|
76
|
+
else high = mid;
|
|
77
|
+
}
|
|
78
|
+
return low + 1;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Class-like declarations (classes, interfaces, structs, enums, traits, modules, impl blocks)
|
|
84
|
+
* @param {string} content
|
|
85
|
+
* @param {string} ext - Lower-case file extension
|
|
86
|
+
* @returns {Array<{name: string, kind: string, line: number}>}
|
|
87
|
+
*/
|
|
88
|
+
function findClassDeclarations(content, ext) {
|
|
89
|
+
const found = [];
|
|
90
|
+
const lineAt = lineLocator(content);
|
|
91
|
+
const collect = (regex, pick) => {
|
|
92
|
+
for (const match of content.matchAll(regex)) {
|
|
93
|
+
const { name, kind } = pick(match);
|
|
94
|
+
found.push({ name, kind, line: lineAt(match.index + match[0].lastIndexOf(name)) });
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (ext === '.py') {
|
|
99
|
+
collect(CLASS_PATTERNS.python, (m) => ({ name: m[1], kind: 'class' }));
|
|
100
|
+
} else if (ext === '.rb') {
|
|
101
|
+
collect(CLASS_PATTERNS.ruby, (m) => ({ name: m[2], kind: m[1] }));
|
|
102
|
+
} else if (ext === '.go') {
|
|
103
|
+
collect(CLASS_PATTERNS.goType, (m) => ({ name: m[1], kind: m[2] }));
|
|
104
|
+
} else {
|
|
105
|
+
collect(CLASS_PATTERNS.brace, (m) => ({ name: m[2], kind: m[1].replace(/\s+/g, ' ') }));
|
|
106
|
+
if (ext === '.rs') {
|
|
107
|
+
collect(CLASS_PATTERNS.rustImpl, (m) => ({ name: m[1], kind: 'impl' }));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return found;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Methods, functions and class-like declarations of a file with their line ranges.
|
|
115
|
+
*
|
|
116
|
+
* Method ranges come from MethodAnalyzer.extractMethodSources(); class ranges use the same
|
|
117
|
+
* block rules (braces, Python indentation, Ruby `end`). Units are sorted by start line.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} content - File content
|
|
120
|
+
* @param {string} filePath - Used for the language (extension)
|
|
121
|
+
* @returns {Array<{name: string, kind: string, startLine: number, endLine: number,
|
|
122
|
+
* container?: boolean, parent?: string}>} `container` marks class-like declarations;
|
|
123
|
+
* `parent` names the class-like declaration a method lies in
|
|
124
|
+
*/
|
|
125
|
+
export function extractCodeUnits(content, filePath) {
|
|
126
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
127
|
+
if (!CODE_EXTENSIONS.has(ext)) return [];
|
|
128
|
+
|
|
129
|
+
const lines = content.split('\n');
|
|
130
|
+
// Minified or generated code: the regex analyzers crawl over very long lines and find
|
|
131
|
+
// nothing useful, so such files are searched by line windows only
|
|
132
|
+
if (content.length / lines.length > MAX_AVERAGE_LINE_LENGTH) return [];
|
|
133
|
+
let methods = [];
|
|
134
|
+
try {
|
|
135
|
+
methods = analyzer.extractMethods(content, filePath);
|
|
136
|
+
} catch (_error) {
|
|
137
|
+
// A regex analyzer failing on odd input leaves the file to the line windows
|
|
138
|
+
}
|
|
139
|
+
const methodSources = analyzer.extractMethodSources(content, methods, filePath);
|
|
140
|
+
const units = methods.map((method, i) => ({
|
|
141
|
+
name: method.name,
|
|
142
|
+
kind: method.type || 'function',
|
|
143
|
+
startLine: method.line,
|
|
144
|
+
endLine: method.line + methodSources[i].split('\n').length - 1,
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
const classes = findClassDeclarations(content, ext);
|
|
148
|
+
const starts = [...new Set([...units, ...classes].map((u) => u.startLine ?? u.line))].sort(
|
|
149
|
+
(a, b) => a - b
|
|
150
|
+
);
|
|
151
|
+
for (const decl of classes) {
|
|
152
|
+
const start = decl.line - 1;
|
|
153
|
+
let end;
|
|
154
|
+
if (ext === '.py') {
|
|
155
|
+
end = analyzer.findIndentedBlockEnd(lines, start);
|
|
156
|
+
} else if (ext === '.rb') {
|
|
157
|
+
end = analyzer.findRubyBlockEnd(lines, start);
|
|
158
|
+
} else if (ext === '.go') {
|
|
159
|
+
end = analyzer.findBraceBlockEnd(lines, start, lines.length);
|
|
160
|
+
} else {
|
|
161
|
+
// A declaration without a body (`class Foo;`, `case class Foo(x: Int)`) must not run
|
|
162
|
+
// into the body of the next unit
|
|
163
|
+
const next = starts.find((line) => line > decl.line);
|
|
164
|
+
end = analyzer.findBraceBlockEnd(lines, start, next ? next - 1 : lines.length);
|
|
165
|
+
}
|
|
166
|
+
units.push({
|
|
167
|
+
name: decl.name,
|
|
168
|
+
kind: decl.kind,
|
|
169
|
+
startLine: decl.line,
|
|
170
|
+
endLine: end + 1,
|
|
171
|
+
container: true,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
units.sort((a, b) => a.startLine - b.startLine || b.endLine - a.endLine);
|
|
176
|
+
|
|
177
|
+
// Go methods belong to their receiver type: `func (s *Server) Start()`
|
|
178
|
+
if (ext === '.go') {
|
|
179
|
+
for (const unit of units) {
|
|
180
|
+
const receiver = /^\s*func\s*\(\s*(?:\w+\s+)?\*?\s*([A-Za-z_]\w*)/.exec(
|
|
181
|
+
lines[unit.startLine - 1] ?? ''
|
|
182
|
+
);
|
|
183
|
+
if (receiver && !unit.container) unit.parent = receiver[1];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Functions inside a class-like block are its methods
|
|
188
|
+
const containers = units.filter((unit) => unit.container);
|
|
189
|
+
for (const unit of units) {
|
|
190
|
+
if (unit.container) continue;
|
|
191
|
+
const parent = containers
|
|
192
|
+
.filter((c) => c.startLine < unit.startLine && c.endLine >= unit.endLine)
|
|
193
|
+
.at(-1);
|
|
194
|
+
if (parent) {
|
|
195
|
+
unit.parent = parent.name;
|
|
196
|
+
if (unit.kind === 'function') unit.kind = 'method';
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return units;
|
|
201
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol lookup: the full source of a named method, function or class.
|
|
3
|
+
*
|
|
4
|
+
* Names are matched exactly first, then ignoring case; when neither finds anything, similar
|
|
5
|
+
* names are suggested. `Class.method` (also `Class::method`, `Class#method`) narrows the
|
|
6
|
+
* lookup to methods of that class.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import TokenUtils from '../utils/token-utils.js';
|
|
11
|
+
import { redactSecrets } from '../utils/secret-redactor.js';
|
|
12
|
+
import { compareStrings, fitToTokenBudget, getProjectIndex, sliceLines } from './bm25.js';
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_SYMBOL_LIMIT = 10;
|
|
15
|
+
export const DEFAULT_SYMBOL_MAX_TOKENS = 20000;
|
|
16
|
+
const MAX_SUGGESTIONS = 10;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Name without an accessor prefix: `get value` -> `value`
|
|
20
|
+
* @param {string} name
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
function bareName(name) {
|
|
24
|
+
return name.replace(/^(?:get|set)\s+/, '');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Edit distance between two strings, or `max + 1` once it is certain to exceed `max`
|
|
29
|
+
* @param {string} a
|
|
30
|
+
* @param {string} b
|
|
31
|
+
* @param {number} [max=Infinity]
|
|
32
|
+
* @returns {number}
|
|
33
|
+
*/
|
|
34
|
+
export function editDistance(a, b, max = Infinity) {
|
|
35
|
+
if (Math.abs(a.length - b.length) > max) return max + 1;
|
|
36
|
+
let previous = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
37
|
+
for (let i = 1; i <= a.length; i++) {
|
|
38
|
+
const current = [i];
|
|
39
|
+
let rowMin = i;
|
|
40
|
+
for (let j = 1; j <= b.length; j++) {
|
|
41
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
42
|
+
current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + cost);
|
|
43
|
+
rowMin = Math.min(rowMin, current[j]);
|
|
44
|
+
}
|
|
45
|
+
if (rowMin > max) return max + 1;
|
|
46
|
+
previous = current;
|
|
47
|
+
}
|
|
48
|
+
return previous[b.length];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Split a qualified name: `Store.get`, `Store::get` and `Store#get` give
|
|
53
|
+
* `{ parent: 'Store', name: 'get' }`
|
|
54
|
+
* @param {string} query
|
|
55
|
+
* @returns {{parent: string|null, name: string}}
|
|
56
|
+
*/
|
|
57
|
+
export function parseSymbolQuery(query) {
|
|
58
|
+
const match = /^(.+?)(?:\.|::|#)([^.:#]+)$/.exec(query);
|
|
59
|
+
return match ? { parent: match[1], name: match[2] } : { parent: null, name: query };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Similar names for a query that matched nothing
|
|
64
|
+
* @private
|
|
65
|
+
*/
|
|
66
|
+
function suggest(candidates, name) {
|
|
67
|
+
const query = name.toLowerCase();
|
|
68
|
+
const threshold = Math.max(2, Math.floor(query.length / 3));
|
|
69
|
+
const best = new Map();
|
|
70
|
+
for (const { entry, unit } of candidates) {
|
|
71
|
+
const candidate = bareName(unit.name);
|
|
72
|
+
const lower = candidate.toLowerCase();
|
|
73
|
+
let distance = editDistance(lower, query, threshold);
|
|
74
|
+
// `user` suggests `getUserName`: containment counts as close
|
|
75
|
+
const shorter = Math.min(lower.length, query.length);
|
|
76
|
+
if (shorter >= 3 && (lower.includes(query) || query.includes(lower))) {
|
|
77
|
+
distance = Math.min(distance, 1);
|
|
78
|
+
}
|
|
79
|
+
if (distance > threshold || best.has(candidate)) continue;
|
|
80
|
+
best.set(candidate, {
|
|
81
|
+
name: unit.name,
|
|
82
|
+
kind: unit.kind,
|
|
83
|
+
...(unit.parent ? { parent: unit.parent } : {}),
|
|
84
|
+
file: entry.relativePath,
|
|
85
|
+
startLine: unit.startLine,
|
|
86
|
+
distance,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return [...best.values()]
|
|
90
|
+
.sort(
|
|
91
|
+
(a, b) =>
|
|
92
|
+
a.distance - b.distance ||
|
|
93
|
+
Math.abs(a.name.length - query.length) - Math.abs(b.name.length - query.length) ||
|
|
94
|
+
compareStrings(a.name, b.name)
|
|
95
|
+
)
|
|
96
|
+
.slice(0, MAX_SUGGESTIONS)
|
|
97
|
+
.map(({ distance: _distance, ...rest }) => rest);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Find the units named by a query
|
|
102
|
+
* @param {Array<{entry: Object, unit: Object}>} candidates - Units with their file entries
|
|
103
|
+
* @param {string} query - `name` or `Parent.name`
|
|
104
|
+
* @returns {{match: 'exact'|'case-insensitive'|'none', found: Array, suggestions: Array}}
|
|
105
|
+
*/
|
|
106
|
+
export function lookupSymbols(candidates, query) {
|
|
107
|
+
const { parent, name } = parseSymbolQuery(query);
|
|
108
|
+
const tiers = [
|
|
109
|
+
['exact', (a, b) => a === b],
|
|
110
|
+
['case-insensitive', (a, b) => a.toLowerCase() === b.toLowerCase()],
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
for (const [match, same] of tiers) {
|
|
114
|
+
const named = (unitName, wanted) => same(unitName, wanted) || same(bareName(unitName), wanted);
|
|
115
|
+
const found = candidates.filter(
|
|
116
|
+
({ unit }) =>
|
|
117
|
+
named(unit.name, query) ||
|
|
118
|
+
(parent !== null && unit.parent && same(unit.parent, parent) && named(unit.name, name))
|
|
119
|
+
);
|
|
120
|
+
if (found.length > 0) return { match, found, suggestions: [] };
|
|
121
|
+
}
|
|
122
|
+
return { match: 'none', found: [], suggestions: suggest(candidates, name) };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Source of the methods, functions or classes with a given name in a project.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} root - Project directory (files found with the Scanner, so the ignore
|
|
129
|
+
* rules apply)
|
|
130
|
+
* @param {string} query - Symbol name, or `Class.method`
|
|
131
|
+
* @param {Object} [options]
|
|
132
|
+
* @param {string} [options.file] - Only this project-relative file
|
|
133
|
+
* @param {number} [options.limit=10] - Maximum number of symbols returned with source
|
|
134
|
+
* @param {number} [options.maxTokens=20000] - Token budget for the sources; sources past it
|
|
135
|
+
* are cut (`truncated: true`)
|
|
136
|
+
* @param {boolean} [options.redact=true] - Redact secrets in the sources
|
|
137
|
+
* @returns {{query: string, match: string, total: number, symbols: Array,
|
|
138
|
+
* suggestions?: Array}}
|
|
139
|
+
* @throws {Error} When the query is empty or `file` is not a project file
|
|
140
|
+
*/
|
|
141
|
+
export function getSymbol(
|
|
142
|
+
root,
|
|
143
|
+
query,
|
|
144
|
+
{ file, limit = DEFAULT_SYMBOL_LIMIT, maxTokens = DEFAULT_SYMBOL_MAX_TOKENS, redact = true } = {}
|
|
145
|
+
) {
|
|
146
|
+
const wanted = typeof query === 'string' ? query.trim() : '';
|
|
147
|
+
if (!wanted) throw new Error('The symbol name is empty');
|
|
148
|
+
|
|
149
|
+
const project = getProjectIndex(root);
|
|
150
|
+
let entries = [...project.files.values()];
|
|
151
|
+
if (file) {
|
|
152
|
+
const target = path.normalize(file);
|
|
153
|
+
entries = entries.filter((entry) => path.normalize(entry.relativePath) === target);
|
|
154
|
+
if (entries.length === 0) throw new Error(`File not found in project: ${file}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const candidates = entries.flatMap((entry) => entry.units.map((unit) => ({ entry, unit })));
|
|
158
|
+
const { match, found, suggestions } = lookupSymbols(candidates, wanted);
|
|
159
|
+
|
|
160
|
+
let used = 0;
|
|
161
|
+
const symbols = found.slice(0, Math.max(0, limit)).map(({ entry, unit }) => {
|
|
162
|
+
const raw = sliceLines(entry.content, unit.startLine, unit.endLine);
|
|
163
|
+
let source = redact ? redactSecrets(raw).text : raw;
|
|
164
|
+
let tokens = TokenUtils.calculate(source, entry.path);
|
|
165
|
+
const symbol = {
|
|
166
|
+
name: unit.name,
|
|
167
|
+
kind: unit.kind,
|
|
168
|
+
...(unit.parent ? { parent: unit.parent } : {}),
|
|
169
|
+
file: entry.relativePath,
|
|
170
|
+
startLine: unit.startLine,
|
|
171
|
+
endLine: unit.endLine,
|
|
172
|
+
};
|
|
173
|
+
if (used + tokens > maxTokens) {
|
|
174
|
+
source = fitToTokenBudget(source, maxTokens - used, entry.path);
|
|
175
|
+
tokens = source ? TokenUtils.calculate(source, entry.path) : 0;
|
|
176
|
+
symbol.truncated = true;
|
|
177
|
+
}
|
|
178
|
+
used += tokens;
|
|
179
|
+
return { ...symbol, tokens, source };
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
query: wanted,
|
|
184
|
+
match,
|
|
185
|
+
total: found.length,
|
|
186
|
+
symbols,
|
|
187
|
+
...(match === 'none' ? { suggestions } : {}),
|
|
188
|
+
};
|
|
189
|
+
}
|
package/lib/ui/dashboard.js
CHANGED
|
@@ -6,104 +6,135 @@ import { Box, Text, useInput, useStdin } from 'ink';
|
|
|
6
6
|
* Real-time statistics and progress display
|
|
7
7
|
*/
|
|
8
8
|
export default function Dashboard({ stats, topFiles = [], status = 'analyzing' }) {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const [shouldRefresh, setShouldRefresh] = useState(false);
|
|
10
|
+
const { isRawModeSupported } = useStdin();
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
// Keyboard controls
|
|
13
|
+
useInput(
|
|
14
|
+
(input, _key) => {
|
|
15
|
+
if (input === 'r' || input === 'R') {
|
|
16
|
+
setShouldRefresh(!shouldRefresh);
|
|
17
|
+
}
|
|
18
|
+
if (input === 'q' || input === 'Q') {
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
{ isActive: isRawModeSupported }
|
|
23
|
+
);
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
const {
|
|
26
|
+
totalFiles = 0,
|
|
27
|
+
totalTokens = 0,
|
|
28
|
+
totalBytes = 0,
|
|
29
|
+
totalLines = 0,
|
|
30
|
+
byExtension = {},
|
|
31
|
+
} = stats;
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
const avgTokensPerFile = totalFiles > 0 ? Math.round(totalTokens / totalFiles) : 0;
|
|
34
|
+
const sizeInMB = (totalBytes / (1024 * 1024)).toFixed(2);
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
: null;
|
|
36
|
+
// Get dominant language
|
|
37
|
+
const extensions = Object.entries(byExtension);
|
|
38
|
+
const dominant =
|
|
39
|
+
extensions.length > 0 ? extensions.sort((a, b) => b[1].count - a[1].count)[0] : null;
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
return React.createElement(
|
|
42
|
+
Box,
|
|
43
|
+
{
|
|
44
|
+
flexDirection: 'column',
|
|
45
|
+
padding: 1,
|
|
46
|
+
borderStyle: 'round',
|
|
47
|
+
borderColor: 'green',
|
|
48
|
+
width: 76, // Max width to prevent scrollbar overlap (reduced from 80)
|
|
49
|
+
},
|
|
50
|
+
React.createElement(
|
|
51
|
+
Box,
|
|
52
|
+
{ marginBottom: 1 },
|
|
53
|
+
React.createElement(Text, { bold: true, color: 'green' }, '📊 Live Analysis Dashboard'),
|
|
54
|
+
React.createElement(Text, { color: 'gray' }, ` (${status})`)
|
|
55
|
+
),
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
// Summary Stats
|
|
58
|
+
React.createElement(
|
|
59
|
+
Box,
|
|
60
|
+
{ flexDirection: 'column', marginBottom: 1 },
|
|
61
|
+
React.createElement(
|
|
62
|
+
Box,
|
|
63
|
+
null,
|
|
64
|
+
React.createElement(Text, null, 'Files: '),
|
|
65
|
+
React.createElement(Text, { bold: true, color: 'cyan' }, totalFiles.toLocaleString()),
|
|
66
|
+
React.createElement(Text, null, ' Methods: '),
|
|
67
|
+
React.createElement(Text, { bold: true, color: 'cyan' }, '-'),
|
|
68
|
+
React.createElement(Text, null, ' Tokens: '),
|
|
69
|
+
React.createElement(Text, { bold: true, color: 'cyan' }, totalTokens.toLocaleString())
|
|
70
|
+
),
|
|
61
71
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
React.createElement(
|
|
73
|
+
Box,
|
|
74
|
+
null,
|
|
75
|
+
React.createElement(Text, null, 'Size: '),
|
|
76
|
+
React.createElement(Text, { bold: true, color: 'cyan' }, `${sizeInMB} MB`),
|
|
77
|
+
React.createElement(Text, null, ' Lines: '),
|
|
78
|
+
React.createElement(Text, { bold: true, color: 'cyan' }, totalLines.toLocaleString()),
|
|
79
|
+
React.createElement(Text, null, ' Avg: '),
|
|
80
|
+
React.createElement(
|
|
81
|
+
Text,
|
|
82
|
+
{ bold: true, color: 'cyan' },
|
|
83
|
+
`${avgTokensPerFile.toLocaleString()} tok/file`
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
),
|
|
71
87
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
88
|
+
// Top Languages
|
|
89
|
+
dominant &&
|
|
90
|
+
React.createElement(
|
|
91
|
+
Box,
|
|
92
|
+
{ flexDirection: 'column', marginBottom: 1 },
|
|
93
|
+
React.createElement(Text, { bold: true }, 'Top Languages:'),
|
|
94
|
+
React.createElement(
|
|
95
|
+
Box,
|
|
96
|
+
{ marginLeft: 2 },
|
|
97
|
+
React.createElement(Text, { color: 'green' }, '▓'.repeat(30)),
|
|
98
|
+
React.createElement(Text, null, ` ${dominant[0]} (${dominant[1].count} files)`)
|
|
99
|
+
)
|
|
100
|
+
),
|
|
80
101
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
102
|
+
// Largest Files
|
|
103
|
+
topFiles.length > 0 &&
|
|
104
|
+
React.createElement(
|
|
105
|
+
Box,
|
|
106
|
+
{ flexDirection: 'column', marginBottom: 1 },
|
|
107
|
+
React.createElement(Text, { bold: true }, 'Largest Files:'),
|
|
108
|
+
...topFiles.slice(0, 3).map((file, index) => {
|
|
109
|
+
const barLength = Math.round((file.tokens / topFiles[0].tokens) * 20);
|
|
110
|
+
const percentage = ((file.tokens / totalTokens) * 100).toFixed(1);
|
|
111
|
+
const fileName = (file.name || file.relativePath || file.path || 'unknown').substring(
|
|
112
|
+
0,
|
|
113
|
+
30
|
|
114
|
+
);
|
|
88
115
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
116
|
+
return React.createElement(
|
|
117
|
+
Box,
|
|
118
|
+
{ key: index, marginLeft: 2 },
|
|
119
|
+
React.createElement(Text, null, '• '),
|
|
120
|
+
React.createElement(Text, { color: 'cyan' }, fileName),
|
|
121
|
+
React.createElement(Text, null, ` ${file.tokens.toLocaleString()} tokens `),
|
|
122
|
+
React.createElement(Text, { color: 'green' }, '▓'.repeat(barLength)),
|
|
123
|
+
React.createElement(Text, { color: 'gray' }, ` (${percentage}%)`)
|
|
124
|
+
);
|
|
125
|
+
})
|
|
126
|
+
),
|
|
98
127
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
128
|
+
// Controls
|
|
129
|
+
React.createElement(
|
|
130
|
+
Box,
|
|
131
|
+
{
|
|
132
|
+
marginTop: 1,
|
|
133
|
+
borderStyle: 'single',
|
|
134
|
+
borderColor: 'gray',
|
|
135
|
+
padding: 1,
|
|
136
|
+
},
|
|
137
|
+
React.createElement(Text, { dimColor: true }, '[R] Refresh [S] Save [E] Export [Q] Quit')
|
|
138
|
+
)
|
|
139
|
+
);
|
|
109
140
|
}
|
package/lib/ui/index.js
CHANGED
|
@@ -7,9 +7,4 @@ import { ProgressBar, SpinnerWithText } from './progress-bar.js';
|
|
|
7
7
|
import Wizard from './wizard.js';
|
|
8
8
|
import Dashboard from './dashboard.js';
|
|
9
9
|
|
|
10
|
-
export {
|
|
11
|
-
ProgressBar,
|
|
12
|
-
SpinnerWithText,
|
|
13
|
-
Wizard,
|
|
14
|
-
Dashboard
|
|
15
|
-
};
|
|
10
|
+
export { ProgressBar, SpinnerWithText, Wizard, Dashboard };
|