ucn 3.7.0 → 3.7.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/.claude/skills/ucn/SKILL.md +3 -3
- package/README.md +7 -4
- package/core/project.js +24 -4
- package/languages/html.js +85 -4
- package/mcp/server.js +1 -1
- package/package.json +1 -1
- package/test/parser.test.js +153 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ucn
|
|
3
|
-
description: "Code relationship analyzer (callers, call trees, impact, dead code) via tree-sitter AST. PREFER over grep+read when you need: who calls a function, what breaks if you change it, or the full call chain of a pipeline. One `ucn about` replaces 3-4 grep+read cycles. One `ucn trace` maps an entire execution flow without reading any files. Works on
|
|
3
|
+
description: "Code relationship analyzer (callers, call trees, impact, dead code) via tree-sitter AST. PREFER over grep+read when you need: who calls a function, what breaks if you change it, or the full call chain of a pipeline. One `ucn about` replaces 3-4 grep+read cycles. One `ucn trace` maps an entire execution flow without reading any files. Works on JS/TS, Python, Go, Rust, Java, HTML. Skip for plain text search or codebases under 500 LOC."
|
|
4
4
|
allowed-tools: Bash(ucn *), Bash(npx ucn *)
|
|
5
5
|
argument-hint: "[command] [symbol-name] [--flags]"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# UCN — Universal Code Navigator
|
|
9
9
|
|
|
10
|
-
Understands code structure via tree-sitter ASTs: who calls what, what breaks if you change something, full call trees, dead code. Works on
|
|
10
|
+
Understands code structure via tree-sitter ASTs: who calls what, what breaks if you change something, full call trees, dead code. Works on JS/TS, Python, Go, Rust, Java. Also parses HTML files (inline scripts and event handlers).
|
|
11
11
|
|
|
12
12
|
## When to Reach for UCN Instead of Grep/Read
|
|
13
13
|
|
|
@@ -23,7 +23,7 @@ Understands code structure via tree-sitter ASTs: who calls what, what breaks if
|
|
|
23
23
|
|
|
24
24
|
- Searching for a string literal, error message, TODO, or config value
|
|
25
25
|
- The codebase is under 500 LOC — just read the files
|
|
26
|
-
- Language not supported (only
|
|
26
|
+
- Language not supported (only JS/TS, Python, Go, Rust, Java, HTML)
|
|
27
27
|
- Finding files by name — use glob
|
|
28
28
|
|
|
29
29
|
## The 5 Commands You'll Use Most
|
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
UCN is designed to work with large files and codebases, helping AI agents ingest exactly the data they need. Its surgical output discourages agents from cutting corners, and without UCN, agents working with large codebases tend to skip parts of the code structure, assuming they have "enough data."
|
|
4
4
|
|
|
5
|
+
Supported languages: JS/TS, Python, Go, Rust, Java. Also parses HTML files (inline scripts and event handlers).
|
|
6
|
+
|
|
5
7
|
---
|
|
6
8
|
|
|
7
9
|
## Three Ways to Use UCN
|
|
@@ -91,9 +93,10 @@ Instead of reading entire files, ask precise questions:
|
|
|
91
93
|
│
|
|
92
94
|
tree-sitter AST
|
|
93
95
|
│
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
┌───────────────────┴─────────────────┐
|
|
97
|
+
│ Supported Languages │
|
|
98
|
+
│ JS/TS, Python, Go, Rust, Java, HTML │
|
|
99
|
+
└─────────────────────────────────────┘
|
|
97
100
|
```
|
|
98
101
|
|
|
99
102
|
No cloud. No API keys. Parses locally, stays local.
|
|
@@ -496,7 +499,7 @@ ucn toc # Project overview
|
|
|
496
499
|
│ Limitation │ What happens │
|
|
497
500
|
├──────────────────────────┼──────────────────────────────────────────┤
|
|
498
501
|
│ │ │
|
|
499
|
-
│
|
|
502
|
+
│ 5 languages + HTML │ JS/TS, Python, Go, Rust, Java. │
|
|
500
503
|
│ (no C, Ruby, PHP, etc.) │ Agents fall back to grep for the rest. │
|
|
501
504
|
│ │ UCN complements, doesn't replace. │
|
|
502
505
|
│ │ │
|
package/core/project.js
CHANGED
|
@@ -2617,9 +2617,11 @@ class ProjectIndex {
|
|
|
2617
2617
|
const htmlParser = getParser('html');
|
|
2618
2618
|
const jsParser = getParser('javascript');
|
|
2619
2619
|
const blocks = htmlModule.extractScriptBlocks(content, htmlParser);
|
|
2620
|
-
if (blocks.length === 0) continue;
|
|
2621
|
-
|
|
2622
|
-
|
|
2620
|
+
if (blocks.length === 0 && !htmlModule.extractEventHandlerCalls) continue;
|
|
2621
|
+
if (blocks.length > 0) {
|
|
2622
|
+
const virtualJS = htmlModule.buildVirtualJSContent(content, blocks);
|
|
2623
|
+
tree = jsParser.parse(virtualJS, undefined, PARSE_OPTIONS);
|
|
2624
|
+
}
|
|
2623
2625
|
} else {
|
|
2624
2626
|
const parser = getParser(language);
|
|
2625
2627
|
if (!parser) continue;
|
|
@@ -2649,7 +2651,25 @@ class ProjectIndex {
|
|
|
2649
2651
|
traverse(node.child(i));
|
|
2650
2652
|
}
|
|
2651
2653
|
};
|
|
2652
|
-
traverse(tree.rootNode);
|
|
2654
|
+
if (tree) traverse(tree.rootNode);
|
|
2655
|
+
|
|
2656
|
+
// For HTML files, also extract identifiers from event handler attributes
|
|
2657
|
+
// (onclick="foo()" etc. — these are in HTML, not in <script> blocks)
|
|
2658
|
+
if (language === 'html') {
|
|
2659
|
+
const htmlModule = getLanguageModule('html');
|
|
2660
|
+
const htmlParser = getParser('html');
|
|
2661
|
+
const handlerCalls = htmlModule.extractEventHandlerCalls(content, htmlParser);
|
|
2662
|
+
for (const call of handlerCalls) {
|
|
2663
|
+
if (!usageIndex.has(call.name)) {
|
|
2664
|
+
usageIndex.set(call.name, []);
|
|
2665
|
+
}
|
|
2666
|
+
usageIndex.get(call.name).push({
|
|
2667
|
+
file: filePath,
|
|
2668
|
+
line: call.line,
|
|
2669
|
+
relativePath: fileEntry.relativePath
|
|
2670
|
+
});
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2653
2673
|
} catch (e) {
|
|
2654
2674
|
// Skip files that can't be processed
|
|
2655
2675
|
}
|
package/languages/html.js
CHANGED
|
@@ -144,6 +144,81 @@ function extractJS(htmlContent, htmlParser) {
|
|
|
144
144
|
return { virtualJS, jsParser, jsModule };
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Extract function calls from HTML event handler attributes (onclick, onchange, etc.).
|
|
149
|
+
* Walks the HTML AST for elements with on* attributes, extracts function names
|
|
150
|
+
* from the attribute values using regex, and returns call objects.
|
|
151
|
+
*
|
|
152
|
+
* @param {string} htmlContent - Raw HTML source
|
|
153
|
+
* @param {object} htmlParser - tree-sitter parser configured for HTML
|
|
154
|
+
* @returns {Array<{name: string, line: number, isMethod: boolean, enclosingFunction: null, uncertain: boolean, isEventHandler: boolean}>}
|
|
155
|
+
*/
|
|
156
|
+
function extractEventHandlerCalls(htmlContent, htmlParser) {
|
|
157
|
+
const { safeParse, getParseOptions } = require('./index');
|
|
158
|
+
const tree = safeParse(htmlParser, htmlContent, undefined, getParseOptions(htmlContent.length));
|
|
159
|
+
const calls = [];
|
|
160
|
+
|
|
161
|
+
const JS_KEYWORDS = new Set([
|
|
162
|
+
'if', 'for', 'while', 'switch', 'catch', 'function', 'return',
|
|
163
|
+
'typeof', 'void', 'delete', 'new', 'throw', 'class', 'const',
|
|
164
|
+
'let', 'var', 'true', 'false', 'null', 'undefined', 'this'
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
const visit = (node) => {
|
|
168
|
+
// Skip script elements — their content is handled separately
|
|
169
|
+
if (node.type === 'script_element') return;
|
|
170
|
+
|
|
171
|
+
if (node.type === 'attribute') {
|
|
172
|
+
const nameNode = node.children.find(c => c.type === 'attribute_name');
|
|
173
|
+
if (!nameNode || !nameNode.text.toLowerCase().startsWith('on')) {
|
|
174
|
+
for (let i = 0; i < node.childCount; i++) visit(node.child(i));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const valueNode = node.children.find(c =>
|
|
179
|
+
c.type === 'quoted_attribute_value' || c.type === 'attribute_value'
|
|
180
|
+
);
|
|
181
|
+
if (!valueNode) return;
|
|
182
|
+
|
|
183
|
+
let valueText;
|
|
184
|
+
if (valueNode.type === 'quoted_attribute_value') {
|
|
185
|
+
const inner = valueNode.children.find(c => c.type === 'attribute_value');
|
|
186
|
+
valueText = inner ? inner.text : '';
|
|
187
|
+
} else {
|
|
188
|
+
valueText = valueNode.text;
|
|
189
|
+
}
|
|
190
|
+
if (!valueText) return;
|
|
191
|
+
|
|
192
|
+
const line = nameNode.startPosition.row + 1; // 1-indexed
|
|
193
|
+
|
|
194
|
+
// Extract standalone function calls (not method calls like obj.method())
|
|
195
|
+
const regex = /([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g;
|
|
196
|
+
let match;
|
|
197
|
+
while ((match = regex.exec(valueText)) !== null) {
|
|
198
|
+
const fnName = match[1];
|
|
199
|
+
if (JS_KEYWORDS.has(fnName)) continue;
|
|
200
|
+
// Skip if preceded by dot (method call on object)
|
|
201
|
+
if (match.index > 0 && valueText[match.index - 1] === '.') continue;
|
|
202
|
+
|
|
203
|
+
calls.push({
|
|
204
|
+
name: fnName,
|
|
205
|
+
line,
|
|
206
|
+
isMethod: false,
|
|
207
|
+
enclosingFunction: null,
|
|
208
|
+
uncertain: false,
|
|
209
|
+
isEventHandler: true
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
for (let i = 0; i < node.childCount; i++) visit(node.child(i));
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
visit(tree.rootNode);
|
|
219
|
+
return calls;
|
|
220
|
+
}
|
|
221
|
+
|
|
147
222
|
// ── Exported language module interface ──────────────────────────────────────
|
|
148
223
|
|
|
149
224
|
function parse(code, parser) {
|
|
@@ -185,9 +260,14 @@ function findStateObjects(code, parser) {
|
|
|
185
260
|
}
|
|
186
261
|
|
|
187
262
|
function findCallsInCode(code, parser) {
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
263
|
+
const scriptCalls = (() => {
|
|
264
|
+
const result = extractJS(code, parser);
|
|
265
|
+
if (!result) return [];
|
|
266
|
+
return result.jsModule.findCallsInCode(result.virtualJS, result.jsParser);
|
|
267
|
+
})();
|
|
268
|
+
const handlerCalls = extractEventHandlerCalls(code, parser);
|
|
269
|
+
if (handlerCalls.length === 0) return scriptCalls;
|
|
270
|
+
return scriptCalls.concat(handlerCalls);
|
|
191
271
|
}
|
|
192
272
|
|
|
193
273
|
function findCallbackUsages(code, name, parser) {
|
|
@@ -233,5 +313,6 @@ module.exports = {
|
|
|
233
313
|
findUsagesInCode,
|
|
234
314
|
// Exported for testing
|
|
235
315
|
extractScriptBlocks,
|
|
236
|
-
buildVirtualJSContent
|
|
316
|
+
buildVirtualJSContent,
|
|
317
|
+
extractEventHandlerCalls
|
|
237
318
|
};
|
package/mcp/server.js
CHANGED
|
@@ -169,7 +169,7 @@ function requireName(name) {
|
|
|
169
169
|
// CONSOLIDATED TOOL REGISTRATION
|
|
170
170
|
// ============================================================================
|
|
171
171
|
|
|
172
|
-
const TOOL_DESCRIPTION = `Universal Code Navigator powered by tree-sitter ASTs. Analyzes code structure — functions, callers, callees, dependencies — across JavaScript/TypeScript, Python, Go, Rust, Java, and HTML (inline scripts). Use instead of grep/read for code relationships.
|
|
172
|
+
const TOOL_DESCRIPTION = `Universal Code Navigator powered by tree-sitter ASTs. Analyzes code structure — functions, callers, callees, dependencies — across JavaScript/TypeScript, Python, Go, Rust, Java, and HTML (inline scripts and event handlers). Use instead of grep/read for code relationships.
|
|
173
173
|
|
|
174
174
|
TOP 5 (covers 90% of tasks): about, impact, trace, find, deadcode
|
|
175
175
|
|
package/package.json
CHANGED
package/test/parser.test.js
CHANGED
|
@@ -12535,6 +12535,159 @@ function main() { return helper(); }
|
|
|
12535
12535
|
|
|
12536
12536
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
12537
12537
|
});
|
|
12538
|
+
|
|
12539
|
+
// ── HTML event handler tests (fix #90) ──────────────────────────────────
|
|
12540
|
+
|
|
12541
|
+
it('extractEventHandlerCalls extracts calls from onclick attributes', () => {
|
|
12542
|
+
const { extractEventHandlerCalls } = require('../languages/html');
|
|
12543
|
+
const parser = getParser('html');
|
|
12544
|
+
const html = '<button onclick="resetGame()">Click</button>';
|
|
12545
|
+
const calls = extractEventHandlerCalls(html, parser);
|
|
12546
|
+
assert.strictEqual(calls.length, 1);
|
|
12547
|
+
assert.strictEqual(calls[0].name, 'resetGame');
|
|
12548
|
+
assert.strictEqual(calls[0].line, 1);
|
|
12549
|
+
assert.strictEqual(calls[0].isMethod, false);
|
|
12550
|
+
assert.strictEqual(calls[0].isEventHandler, true);
|
|
12551
|
+
});
|
|
12552
|
+
|
|
12553
|
+
it('extractEventHandlerCalls handles multiple calls in one handler', () => {
|
|
12554
|
+
const { extractEventHandlerCalls } = require('../languages/html');
|
|
12555
|
+
const parser = getParser('html');
|
|
12556
|
+
const html = '<button onclick="validateForm(); submitData()">Go</button>';
|
|
12557
|
+
const calls = extractEventHandlerCalls(html, parser);
|
|
12558
|
+
assert.strictEqual(calls.length, 2);
|
|
12559
|
+
assert.strictEqual(calls[0].name, 'validateForm');
|
|
12560
|
+
assert.strictEqual(calls[1].name, 'submitData');
|
|
12561
|
+
});
|
|
12562
|
+
|
|
12563
|
+
it('extractEventHandlerCalls skips method calls on objects', () => {
|
|
12564
|
+
const { extractEventHandlerCalls } = require('../languages/html');
|
|
12565
|
+
const parser = getParser('html');
|
|
12566
|
+
const html = '<button onclick="event.stopPropagation(); selectCar(\'abc\')">Buy</button>';
|
|
12567
|
+
const calls = extractEventHandlerCalls(html, parser);
|
|
12568
|
+
const names = calls.map(c => c.name);
|
|
12569
|
+
assert.ok(!names.includes('stopPropagation'), 'should skip event.stopPropagation()');
|
|
12570
|
+
assert.ok(names.includes('selectCar'), 'should detect selectCar()');
|
|
12571
|
+
});
|
|
12572
|
+
|
|
12573
|
+
it('extractEventHandlerCalls skips JS keywords', () => {
|
|
12574
|
+
const { extractEventHandlerCalls } = require('../languages/html');
|
|
12575
|
+
const parser = getParser('html');
|
|
12576
|
+
const html = '<button onclick="if (confirm(\'sure?\')) deleteItem(id)">Del</button>';
|
|
12577
|
+
const calls = extractEventHandlerCalls(html, parser);
|
|
12578
|
+
const names = calls.map(c => c.name);
|
|
12579
|
+
assert.ok(!names.includes('if'), 'should skip keyword if');
|
|
12580
|
+
assert.ok(names.includes('confirm'));
|
|
12581
|
+
assert.ok(names.includes('deleteItem'));
|
|
12582
|
+
});
|
|
12583
|
+
|
|
12584
|
+
it('extractEventHandlerCalls works with various on* attributes', () => {
|
|
12585
|
+
const { extractEventHandlerCalls } = require('../languages/html');
|
|
12586
|
+
const parser = getParser('html');
|
|
12587
|
+
const html = `<input onchange="updateValue()" onfocus="highlight()" onblur="unhighlight()">`;
|
|
12588
|
+
const calls = extractEventHandlerCalls(html, parser);
|
|
12589
|
+
const names = calls.map(c => c.name);
|
|
12590
|
+
assert.ok(names.includes('updateValue'));
|
|
12591
|
+
assert.ok(names.includes('highlight'));
|
|
12592
|
+
assert.ok(names.includes('unhighlight'));
|
|
12593
|
+
});
|
|
12594
|
+
|
|
12595
|
+
it('extractEventHandlerCalls does not extract from script elements', () => {
|
|
12596
|
+
const { extractEventHandlerCalls } = require('../languages/html');
|
|
12597
|
+
const parser = getParser('html');
|
|
12598
|
+
const html = `<script>function foo() { bar(); }</script>
|
|
12599
|
+
<button onclick="foo()">Click</button>`;
|
|
12600
|
+
const calls = extractEventHandlerCalls(html, parser);
|
|
12601
|
+
// Should only find foo from onclick, not bar from <script>
|
|
12602
|
+
assert.strictEqual(calls.length, 1);
|
|
12603
|
+
assert.strictEqual(calls[0].name, 'foo');
|
|
12604
|
+
});
|
|
12605
|
+
|
|
12606
|
+
it('extractEventHandlerCalls reports correct line numbers', () => {
|
|
12607
|
+
const { extractEventHandlerCalls } = require('../languages/html');
|
|
12608
|
+
const parser = getParser('html');
|
|
12609
|
+
const html = `<html>
|
|
12610
|
+
<body>
|
|
12611
|
+
<div>text</div>
|
|
12612
|
+
<button onclick="doA()">A</button>
|
|
12613
|
+
<button onclick="doB()">B</button>
|
|
12614
|
+
</body>
|
|
12615
|
+
</html>`;
|
|
12616
|
+
const calls = extractEventHandlerCalls(html, parser);
|
|
12617
|
+
assert.strictEqual(calls.length, 2);
|
|
12618
|
+
assert.strictEqual(calls[0].name, 'doA');
|
|
12619
|
+
assert.strictEqual(calls[0].line, 4);
|
|
12620
|
+
assert.strictEqual(calls[1].name, 'doB');
|
|
12621
|
+
assert.strictEqual(calls[1].line, 5);
|
|
12622
|
+
});
|
|
12623
|
+
|
|
12624
|
+
it('findCallsInCode includes event handler calls for HTML', () => {
|
|
12625
|
+
const { parser, mod } = getHtmlTools();
|
|
12626
|
+
const html = `<button onclick="handleClick()">Click</button>
|
|
12627
|
+
<script>
|
|
12628
|
+
function handleClick() { doWork(); }
|
|
12629
|
+
function doWork() { return 42; }
|
|
12630
|
+
</script>`;
|
|
12631
|
+
const calls = mod.findCallsInCode(html, parser);
|
|
12632
|
+
const names = calls.map(c => c.name);
|
|
12633
|
+
assert.ok(names.includes('handleClick'), 'should find handleClick from onclick');
|
|
12634
|
+
assert.ok(names.includes('doWork'), 'should find doWork from script');
|
|
12635
|
+
});
|
|
12636
|
+
|
|
12637
|
+
it('deadcode does not report functions called from HTML event handlers', () => {
|
|
12638
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-html-onclick-'));
|
|
12639
|
+
fs.writeFileSync(path.join(tmpDir, 'page.html'), `<html><body>
|
|
12640
|
+
<button onclick="resetGame()">Reset</button>
|
|
12641
|
+
<button onclick="startGame('easy')">Start</button>
|
|
12642
|
+
<script>
|
|
12643
|
+
function resetGame() { init(); }
|
|
12644
|
+
function startGame(mode) { setup(mode); }
|
|
12645
|
+
function init() { return 1; }
|
|
12646
|
+
function setup(m) { return m; }
|
|
12647
|
+
function unusedFn() { return 0; }
|
|
12648
|
+
</script>
|
|
12649
|
+
</body></html>`);
|
|
12650
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name": "test"}');
|
|
12651
|
+
|
|
12652
|
+
const { ProjectIndex } = require('../core/project');
|
|
12653
|
+
const index = new ProjectIndex(tmpDir);
|
|
12654
|
+
index.build();
|
|
12655
|
+
|
|
12656
|
+
const dead = index.deadcode({ includeExported: true });
|
|
12657
|
+
const deadNames = dead.map(d => d.name);
|
|
12658
|
+
// resetGame and startGame are called from onclick — NOT dead
|
|
12659
|
+
assert.ok(!deadNames.includes('resetGame'), 'resetGame should not be dead (called from onclick)');
|
|
12660
|
+
assert.ok(!deadNames.includes('startGame'), 'startGame should not be dead (called from onclick)');
|
|
12661
|
+
// init and setup are called from script — NOT dead
|
|
12662
|
+
assert.ok(!deadNames.includes('init'), 'init should not be dead (called from resetGame)');
|
|
12663
|
+
assert.ok(!deadNames.includes('setup'), 'setup should not be dead (called from startGame)');
|
|
12664
|
+
// unusedFn has no callers — dead
|
|
12665
|
+
assert.ok(deadNames.includes('unusedFn'), 'unusedFn should be dead');
|
|
12666
|
+
|
|
12667
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
12668
|
+
});
|
|
12669
|
+
|
|
12670
|
+
it('findCallers detects callers from HTML event handlers', () => {
|
|
12671
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-html-callers-'));
|
|
12672
|
+
fs.writeFileSync(path.join(tmpDir, 'page.html'), `<html><body>
|
|
12673
|
+
<button onclick="doStuff()">Go</button>
|
|
12674
|
+
<script>
|
|
12675
|
+
function doStuff() { return 42; }
|
|
12676
|
+
</script>
|
|
12677
|
+
</body></html>`);
|
|
12678
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name": "test"}');
|
|
12679
|
+
|
|
12680
|
+
const { ProjectIndex } = require('../core/project');
|
|
12681
|
+
const index = new ProjectIndex(tmpDir);
|
|
12682
|
+
index.build();
|
|
12683
|
+
|
|
12684
|
+
const callers = index.findCallers('doStuff');
|
|
12685
|
+
assert.strictEqual(callers.length, 1);
|
|
12686
|
+
assert.strictEqual(callers[0].line, 2);
|
|
12687
|
+
assert.ok(callers[0].content.includes('onclick="doStuff()"'));
|
|
12688
|
+
|
|
12689
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
12690
|
+
});
|
|
12538
12691
|
});
|
|
12539
12692
|
|
|
12540
12693
|
console.log('UCN v3 Test Suite');
|