@ngockhoale/ukit 1.3.1 → 1.4.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/CHANGELOG.md +29 -1
- package/package.json +1 -1
- package/src/bug/triageBug.js +1 -33
- package/src/cli/commands/install.js +5 -10
- package/src/context/detectProjectContext.js +3 -24
- package/src/core/compact/index.js +19 -27
- package/src/core/ensureGitignore.js +1 -1
- package/src/core/fileOps.js +41 -2
- package/src/core/memory/hygiene.js +17 -1
- package/src/core/memory/store.js +14 -36
- package/src/core/metadata.js +5 -5
- package/src/core/output/index.js +20 -20
- package/src/core/packageManager.js +51 -0
- package/src/core/router/router.js +22 -6
- package/src/core/runInstallPipeline.js +1 -36
- package/src/core/runtimeConfig.js +27 -6
- package/src/core/token/index.js +21 -1
- package/src/core/uninstall.js +15 -38
- package/src/index/buildIndex.js +217 -49
- package/src/index/gitHooks.js +32 -7
- package/src/index/impactContext.js +16 -6
- package/src/index/importResolution.js +105 -28
- package/src/index/paths.js +29 -0
- package/src/index/queryIndex.js +20 -35
- package/src/index/relatedTests.js +15 -2
- package/src/index/routeCatalog.js +1 -1
- package/src/index/taskRouting.js +96 -17
- package/src/index/verificationPlan.js +12 -38
- package/templates/.claude/hooks/skill-router.sh +5 -2
- package/templates/.claude/ukit/index/route-catalog.mjs +1 -1
- package/templates/.claude/ukit/index/route-task.mjs +41 -5
- package/templates/.claude/ukit/index/verify-context.mjs +10 -2
- package/templates/.codex/README.md +1 -1
- package/templates/CLAUDE.md +9 -1
- package/templates/ukit/README.md +1 -1
- package/templates/ukit/storage/config.json +23 -5
|
@@ -12,7 +12,7 @@ import { summarizeDiff, toDiffRows } from './report.js';
|
|
|
12
12
|
import { writeInstallMetadata } from './metadata.js';
|
|
13
13
|
import { cleanupLegacyPaths, migrateLegacyRuntimeRoot } from './migrateLegacy.js';
|
|
14
14
|
import { ensureGitignore } from './ensureGitignore.js';
|
|
15
|
-
import { readJsonIfExists, removeFileOrLinkOnly, resolveProjectRelativePath } from './fileOps.js';
|
|
15
|
+
import { cleanupEmptyParents, readJsonIfExists, removeFileOrLinkOnly, resolveProjectRelativePath } from './fileOps.js';
|
|
16
16
|
|
|
17
17
|
const AUTO_PRUNE_OBSOLETE_PREFIXES = [
|
|
18
18
|
'.claude/skills/',
|
|
@@ -57,41 +57,6 @@ function shouldAutoPruneObsoletePath(relativePath) {
|
|
|
57
57
|
|| AUTO_PRUNE_OBSOLETE_PREFIXES.some((prefix) => relativePath.startsWith(prefix));
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
async function isDirEmpty(dirPath) {
|
|
61
|
-
try {
|
|
62
|
-
const entries = await fs.readdir(dirPath);
|
|
63
|
-
return entries.length === 0;
|
|
64
|
-
} catch {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function cleanupEmptyParents(targetPath, projectRoot) {
|
|
70
|
-
let currentDir = path.dirname(targetPath);
|
|
71
|
-
const stopDir = path.resolve(projectRoot);
|
|
72
|
-
|
|
73
|
-
while (currentDir.startsWith(stopDir) && currentDir !== stopDir) {
|
|
74
|
-
let stat;
|
|
75
|
-
try {
|
|
76
|
-
stat = await fs.lstat(currentDir);
|
|
77
|
-
} catch {
|
|
78
|
-
currentDir = path.dirname(currentDir);
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (!stat.isDirectory()) {
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (!(await isDirEmpty(currentDir))) {
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
await fs.rmdir(currentDir);
|
|
91
|
-
currentDir = path.dirname(currentDir);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
60
|
async function isTrackedManagedDirectoryTree({
|
|
96
61
|
dirPath,
|
|
97
62
|
projectRoot,
|
|
@@ -49,8 +49,13 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
|
|
|
49
49
|
const safeOverrides = isPlainObject(overrides) ? overrides : {};
|
|
50
50
|
|
|
51
51
|
return mergeObjects({
|
|
52
|
-
version: '1.
|
|
52
|
+
version: '1.4.1',
|
|
53
53
|
agent: 'claude-code',
|
|
54
|
+
autonomy: {
|
|
55
|
+
level: 'balanced',
|
|
56
|
+
affectVerification: true,
|
|
57
|
+
affectDelegation: true,
|
|
58
|
+
},
|
|
54
59
|
compact: {
|
|
55
60
|
enabled: true,
|
|
56
61
|
tokenThreshold: 100_000,
|
|
@@ -174,9 +179,10 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
|
|
|
174
179
|
'summarize-docs-or-keep-detail',
|
|
175
180
|
],
|
|
176
181
|
stepBudgets: {
|
|
177
|
-
trivial: { maxSteps: 1, verification: 'skip-unless-risky' },
|
|
178
|
-
simple: { maxSteps: 2, verification: 'targeted-if-covered' },
|
|
179
|
-
|
|
182
|
+
trivial: { maxSteps: 1, verification: 'skip-unless-risky', label: 'LITE' },
|
|
183
|
+
simple: { maxSteps: 2, verification: 'targeted-if-covered', label: 'LITE' },
|
|
184
|
+
sharedSimple: { maxSteps: 3, verification: 'targeted-then-widen-on-risk', label: 'FULL' },
|
|
185
|
+
nonTrivial: { maxSteps: 4, verification: 'targeted-then-widen-on-risk', label: 'FULL' },
|
|
180
186
|
},
|
|
181
187
|
},
|
|
182
188
|
},
|
|
@@ -194,6 +200,17 @@ export function validateRuntimeConfig(config) {
|
|
|
194
200
|
errors.push('version must be a non-empty string.');
|
|
195
201
|
}
|
|
196
202
|
|
|
203
|
+
const VALID_AUTONOMY_LEVELS = new Set(['conservative', 'balanced', 'free-run']);
|
|
204
|
+
if (!isPlainObject(config.autonomy)) {
|
|
205
|
+
errors.push('autonomy must be an object.');
|
|
206
|
+
} else {
|
|
207
|
+
if (!VALID_AUTONOMY_LEVELS.has(config.autonomy.level)) {
|
|
208
|
+
errors.push(`autonomy.level must be one of: ${[...VALID_AUTONOMY_LEVELS].join(', ')}.`);
|
|
209
|
+
}
|
|
210
|
+
pushBooleanError(errors, config.autonomy.affectVerification, 'autonomy.affectVerification');
|
|
211
|
+
pushBooleanError(errors, config.autonomy.affectDelegation, 'autonomy.affectDelegation');
|
|
212
|
+
}
|
|
213
|
+
|
|
197
214
|
if (!VALID_AGENTS.has(config.agent)) {
|
|
198
215
|
errors.push(`agent must be one of: ${[...VALID_AGENTS].join(', ')}.`);
|
|
199
216
|
}
|
|
@@ -251,10 +268,10 @@ export function validateRuntimeConfig(config) {
|
|
|
251
268
|
errors.push('router must be an object.');
|
|
252
269
|
} else {
|
|
253
270
|
pushBooleanError(errors, config.router.enabled, 'router.enabled');
|
|
254
|
-
if (typeof config.router.defaultModel !== 'string' || config.router.defaultModel.trim()
|
|
271
|
+
if (typeof config.router.defaultModel !== 'string' || config.router.defaultModel.trim() === '') {
|
|
255
272
|
errors.push('router.defaultModel must be a non-empty string.');
|
|
256
273
|
}
|
|
257
|
-
if (typeof config.router.advisorModel !== 'string' || config.router.advisorModel.trim()
|
|
274
|
+
if (typeof config.router.advisorModel !== 'string' || config.router.advisorModel.trim() === '') {
|
|
258
275
|
errors.push('router.advisorModel must be a non-empty string.');
|
|
259
276
|
}
|
|
260
277
|
pushBooleanError(errors, config.router.advisorEnabled, 'router.advisorEnabled');
|
|
@@ -355,5 +372,9 @@ export async function inspectRuntimeConfig(projectRoot) {
|
|
|
355
372
|
|
|
356
373
|
export async function loadRuntimeConfig(projectRoot) {
|
|
357
374
|
const inspection = await inspectRuntimeConfig(projectRoot);
|
|
375
|
+
if (inspection.exists && !inspection.valid) {
|
|
376
|
+
console.warn(`[UKit] Invalid UKit runtime config: ${inspection.errors.join('; ')}`);
|
|
377
|
+
return buildDefaultRuntimeConfig();
|
|
378
|
+
}
|
|
358
379
|
return inspection.config;
|
|
359
380
|
}
|
package/src/core/token/index.js
CHANGED
|
@@ -30,6 +30,25 @@ export function estimateTokenCount(value) {
|
|
|
30
30
|
return Math.max(1, Math.ceil(text.length / 4));
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export function normalizeLineForDedupe(line) {
|
|
34
|
+
return String(line ?? '').trim().replace(/\s+/g, ' ').toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function uniqueLines(lines) {
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
const unique = [];
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
const trimmed = String(line ?? '').trim();
|
|
42
|
+
const normalized = normalizeLineForDedupe(trimmed);
|
|
43
|
+
if (!normalized || seen.has(normalized)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
seen.add(normalized);
|
|
47
|
+
unique.push(trimmed);
|
|
48
|
+
}
|
|
49
|
+
return unique;
|
|
50
|
+
}
|
|
51
|
+
|
|
33
52
|
export function buildCompactMachineKey(prefix, payload = {}) {
|
|
34
53
|
return `${prefix}:${crypto.createHash('sha256').update(JSON.stringify(payload)).digest('base64url')}`;
|
|
35
54
|
}
|
|
@@ -47,7 +66,8 @@ function isStructuredLine(line) {
|
|
|
47
66
|
function normalizeWhitespace(value) {
|
|
48
67
|
return value
|
|
49
68
|
.replace(/\s+\|\s+/g, ' | ')
|
|
50
|
-
.replace(/\s
|
|
69
|
+
.replace(/\s*:\s*/g, ': ')
|
|
70
|
+
.replace(/\s*;\s*/g, '; ')
|
|
51
71
|
.replace(/\s*,\s*/g, ', ')
|
|
52
72
|
.replace(/\s+/g, ' ')
|
|
53
73
|
.trim();
|
package/src/core/uninstall.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import {
|
|
4
|
+
cleanupEmptyParents,
|
|
4
5
|
readJsonIfExists,
|
|
5
6
|
removeLinkOrDir,
|
|
6
7
|
removeLinkOnly,
|
|
@@ -8,41 +9,6 @@ import {
|
|
|
8
9
|
} from './fileOps.js';
|
|
9
10
|
import { removeGitignoreBlock } from './ensureGitignore.js';
|
|
10
11
|
|
|
11
|
-
async function isDirEmpty(dirPath) {
|
|
12
|
-
try {
|
|
13
|
-
const entries = await fs.readdir(dirPath);
|
|
14
|
-
return entries.length === 0;
|
|
15
|
-
} catch {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async function cleanupEmptyParents(targetPath, projectRoot) {
|
|
21
|
-
let currentDir = path.dirname(targetPath);
|
|
22
|
-
const stopDir = path.resolve(projectRoot);
|
|
23
|
-
|
|
24
|
-
while (currentDir.startsWith(stopDir) && currentDir !== stopDir) {
|
|
25
|
-
let stat;
|
|
26
|
-
try {
|
|
27
|
-
stat = await fs.lstat(currentDir);
|
|
28
|
-
} catch {
|
|
29
|
-
currentDir = path.dirname(currentDir);
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (!stat.isDirectory()) {
|
|
34
|
-
break;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (!(await isDirEmpty(currentDir))) {
|
|
38
|
-
break;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
await fs.rmdir(currentDir);
|
|
42
|
-
currentDir = path.dirname(currentDir);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
12
|
// Use lstat (not access) so broken symlinks are also detected as existing.
|
|
47
13
|
async function pathExistsLstat(targetPath) {
|
|
48
14
|
try {
|
|
@@ -134,7 +100,12 @@ export async function uninstallUkit({ projectRoot, dryRun = false }) {
|
|
|
134
100
|
// An attacker (or confused user) could forge the file to trigger uninstall of
|
|
135
101
|
// the hardcoded managed paths. Requiring 'tool: ukit' ensures the file was
|
|
136
102
|
// written by the UKit installer, not created manually.
|
|
137
|
-
|
|
103
|
+
let installData;
|
|
104
|
+
try {
|
|
105
|
+
installData = await readJsonIfExists(installMetaPath);
|
|
106
|
+
} catch {
|
|
107
|
+
return { removed: 0, attempted: 0, wasInstalled: false };
|
|
108
|
+
}
|
|
138
109
|
if (!installData || installData.tool !== 'ukit') {
|
|
139
110
|
return { removed: 0, attempted: 0, wasInstalled: false };
|
|
140
111
|
}
|
|
@@ -233,14 +204,20 @@ export async function uninstallUkit({ projectRoot, dryRun = false }) {
|
|
|
233
204
|
// Clean up .gitignore block added during install
|
|
234
205
|
await removeGitignoreBlock(projectRoot);
|
|
235
206
|
|
|
236
|
-
// Cleanup empty parent directories sequentially (order matters)
|
|
237
207
|
let removed = 0;
|
|
208
|
+
const removedPaths = [];
|
|
238
209
|
for (const { abs, didRemove } of results) {
|
|
239
210
|
if (didRemove) {
|
|
240
211
|
removed += 1;
|
|
241
|
-
|
|
212
|
+
removedPaths.push(abs);
|
|
242
213
|
}
|
|
243
214
|
}
|
|
244
215
|
|
|
216
|
+
const deepestRemovedPaths = [...new Set(removedPaths)]
|
|
217
|
+
.sort((left, right) => right.length - left.length);
|
|
218
|
+
for (const removedPath of deepestRemovedPaths) {
|
|
219
|
+
await cleanupEmptyParents(removedPath, projectRoot);
|
|
220
|
+
}
|
|
221
|
+
|
|
245
222
|
return { removed, attempted: allEntries.length, wasInstalled: true };
|
|
246
223
|
}
|
package/src/index/buildIndex.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
4
|
|
|
5
|
-
import { getArtifactPath, getIndexDir, INDEX_ARTIFACTS, INDEX_SCHEMA_VERSION, normalizeRelative } from './paths.js';
|
|
5
|
+
import { getArtifactPath, getIndexDir, INDEX_ARTIFACTS, INDEX_SCHEMA_VERSION, isLikelyTestFilePath, normalizeRelative } from './paths.js';
|
|
6
6
|
import { importsNeedAliasContext, loadImportAliasContextState, resolveImportSpecifier } from './importResolution.js';
|
|
7
7
|
import { clearIndexArtifactCache } from './queryIndex.js';
|
|
8
8
|
import { clearRelatedTestArtifactCache } from './relatedTests.js';
|
|
@@ -466,16 +466,163 @@ async function collectFiles(scanRoots) {
|
|
|
466
466
|
return result;
|
|
467
467
|
}
|
|
468
468
|
|
|
469
|
+
|
|
470
|
+
function stripCommentsStringAware(content) {
|
|
471
|
+
let result = '';
|
|
472
|
+
let state = 'normal';
|
|
473
|
+
let escaped = false;
|
|
474
|
+
let templateExpressionDepth = 0;
|
|
475
|
+
let regexCharClass = false;
|
|
476
|
+
|
|
477
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
478
|
+
const char = content[index];
|
|
479
|
+
const next = content[index + 1];
|
|
480
|
+
|
|
481
|
+
if (state === 'line-comment') {
|
|
482
|
+
if (char === '\n') {
|
|
483
|
+
result += char;
|
|
484
|
+
state = 'normal';
|
|
485
|
+
}
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (state === 'block-comment') {
|
|
490
|
+
if (char === '*' && next === '/') {
|
|
491
|
+
index += 1;
|
|
492
|
+
state = 'normal';
|
|
493
|
+
}
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
result += char;
|
|
498
|
+
|
|
499
|
+
if (state === 'single' || state === 'double' || state === 'template' || state === 'regex') {
|
|
500
|
+
if (escaped) {
|
|
501
|
+
escaped = false;
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (char === '\\') {
|
|
505
|
+
escaped = true;
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (state === 'single' && char === "'") state = 'normal';
|
|
509
|
+
if (state === 'double' && char === '"') state = 'normal';
|
|
510
|
+
if (state === 'template') {
|
|
511
|
+
if (char === '`' && templateExpressionDepth === 0) state = 'normal';
|
|
512
|
+
else if (char === '{' && content[index - 1] === '$') templateExpressionDepth += 1;
|
|
513
|
+
else if (char === '}' && templateExpressionDepth > 0) templateExpressionDepth -= 1;
|
|
514
|
+
}
|
|
515
|
+
if (state === 'regex') {
|
|
516
|
+
if (char === '[') regexCharClass = true;
|
|
517
|
+
else if (char === ']') regexCharClass = false;
|
|
518
|
+
else if (char === '/' && !regexCharClass) state = 'normal';
|
|
519
|
+
}
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (char === '/' && next === '/') {
|
|
524
|
+
result = result.slice(0, -1);
|
|
525
|
+
state = 'line-comment';
|
|
526
|
+
index += 1;
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (char === '/' && next === '*') {
|
|
530
|
+
result = result.slice(0, -1);
|
|
531
|
+
state = 'block-comment';
|
|
532
|
+
index += 1;
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (char === "'") state = 'single';
|
|
536
|
+
else if (char === '"') state = 'double';
|
|
537
|
+
else if (char === '`') state = 'template';
|
|
538
|
+
else if (char === '/' && isRegexLiteralStart(result.slice(0, -1))) {
|
|
539
|
+
state = 'regex';
|
|
540
|
+
regexCharClass = false;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return result;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function isRegexLiteralStart(prefix) {
|
|
548
|
+
const trimmed = prefix.trimEnd();
|
|
549
|
+
return !trimmed || /(?:[=(:,!&|?{};\[\n]|\b(?:return|throw|case|delete|typeof|void|new|in|of|yield|await)\s*)$/.test(trimmed);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function findScriptClose(content, startIndex) {
|
|
553
|
+
const lower = content.toLowerCase();
|
|
554
|
+
let state = 'normal';
|
|
555
|
+
let escaped = false;
|
|
556
|
+
let templateExpressionDepth = 0;
|
|
557
|
+
|
|
558
|
+
for (let index = startIndex; index < content.length; index += 1) {
|
|
559
|
+
const char = content[index];
|
|
560
|
+
const next = content[index + 1];
|
|
561
|
+
|
|
562
|
+
if (state === 'line-comment') {
|
|
563
|
+
if (char === '\n') state = 'normal';
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (state === 'block-comment') {
|
|
567
|
+
if (char === '*' && next === '/') {
|
|
568
|
+
index += 1;
|
|
569
|
+
state = 'normal';
|
|
570
|
+
}
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
if (state === 'single' || state === 'double' || state === 'template') {
|
|
574
|
+
if (escaped) {
|
|
575
|
+
escaped = false;
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (char === '\\') {
|
|
579
|
+
escaped = true;
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
if (state === 'single' && char === "'") state = 'normal';
|
|
583
|
+
else if (state === 'double' && char === '"') state = 'normal';
|
|
584
|
+
else if (state === 'template') {
|
|
585
|
+
if (char === '`' && templateExpressionDepth === 0) state = 'normal';
|
|
586
|
+
else if (char === '{' && content[index - 1] === '$') templateExpressionDepth += 1;
|
|
587
|
+
else if (char === '}' && templateExpressionDepth > 0) templateExpressionDepth -= 1;
|
|
588
|
+
}
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (lower.startsWith('</script>', index)) return index;
|
|
593
|
+
if (char === '/' && next === '/') {
|
|
594
|
+
state = 'line-comment';
|
|
595
|
+
index += 1;
|
|
596
|
+
} else if (char === '/' && next === '*') {
|
|
597
|
+
state = 'block-comment';
|
|
598
|
+
index += 1;
|
|
599
|
+
} else if (char === "'") state = 'single';
|
|
600
|
+
else if (char === '"') state = 'double';
|
|
601
|
+
else if (char === '`') state = 'template';
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return -1;
|
|
605
|
+
}
|
|
606
|
+
|
|
469
607
|
function extractScriptContent(filePath, content) {
|
|
470
608
|
if (!filePath.endsWith('.vue')) {
|
|
471
609
|
return content;
|
|
472
610
|
}
|
|
473
611
|
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
612
|
+
const scripts = [];
|
|
613
|
+
const startTagPattern = /<script[^>]*>/gi;
|
|
614
|
+
for (const match of content.matchAll(startTagPattern)) {
|
|
615
|
+
const bodyStart = match.index + match[0].length;
|
|
616
|
+
const bodyEnd = findScriptClose(content, bodyStart);
|
|
617
|
+
if (bodyEnd !== -1) {
|
|
618
|
+
scripts.push(content.slice(bodyStart, bodyEnd));
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (scripts.length === 0) return '';
|
|
622
|
+
return scripts.join('\n');
|
|
477
623
|
}
|
|
478
624
|
|
|
625
|
+
|
|
479
626
|
function extractSymbols(filePath, content) {
|
|
480
627
|
const symbols = [];
|
|
481
628
|
const addSymbol = (name, type) => {
|
|
@@ -529,13 +676,10 @@ function extractSymbols(filePath, content) {
|
|
|
529
676
|
}
|
|
530
677
|
|
|
531
678
|
function extractImports(filePath, content) {
|
|
532
|
-
|
|
533
|
-
const stripped = content
|
|
534
|
-
.replace(/\/\*[\s\S]*?\*\//g, '') // block comments
|
|
535
|
-
.replace(/\/\/.*$/gm, ''); // line comments
|
|
679
|
+
const stripped = stripCommentsStringAware(content);
|
|
536
680
|
const imports = [];
|
|
537
681
|
|
|
538
|
-
for (const match of stripped.matchAll(/import\s+(?:[^'";]+?\s+from\s+)?['"]([^'"]+)['"]/g)) {
|
|
682
|
+
for (const match of stripped.matchAll(/(?:^|[;\n])\s*import\s+(?:[^'";]+?\s+from\s+)?['"]([^'"]+)['"]/g)) {
|
|
539
683
|
imports.push({
|
|
540
684
|
from: filePath,
|
|
541
685
|
to: match[1],
|
|
@@ -559,7 +703,7 @@ function extractImports(filePath, content) {
|
|
|
559
703
|
});
|
|
560
704
|
}
|
|
561
705
|
|
|
562
|
-
for (const match of stripped.matchAll(/export\s+(?:type\s+)?\{[^}]+\}\s+from\s+['"]([^'"]+)['"]/g)) {
|
|
706
|
+
for (const match of stripped.matchAll(/(?:^|[;\n])\s*export\s+(?:type\s+)?\{[^}]+\}\s+from\s+['"]([^'"]+)['"]/g)) {
|
|
563
707
|
imports.push({
|
|
564
708
|
from: filePath,
|
|
565
709
|
to: match[1],
|
|
@@ -567,7 +711,7 @@ function extractImports(filePath, content) {
|
|
|
567
711
|
});
|
|
568
712
|
}
|
|
569
713
|
|
|
570
|
-
for (const match of stripped.matchAll(/export\s+\*(?:\s+as\s+[A-Za-z_$][A-Za-z0-9_$]*)?\s+from\s+['"]([^'"]+)['"]/g)) {
|
|
714
|
+
for (const match of stripped.matchAll(/(?:^|[;\n])\s*export\s+\*(?:\s+as\s+[A-Za-z_$][A-Za-z0-9_$]*)?\s+from\s+['"]([^'"]+)['"]/g)) {
|
|
571
715
|
imports.push({
|
|
572
716
|
from: filePath,
|
|
573
717
|
to: match[1],
|
|
@@ -629,18 +773,71 @@ function extractFunctionBlocks(content) {
|
|
|
629
773
|
|
|
630
774
|
function sliceBalancedBlock(content, startIndex) {
|
|
631
775
|
let depth = 1;
|
|
776
|
+
let state = 'normal';
|
|
777
|
+
let escaped = false;
|
|
778
|
+
let templateExpressionDepth = 0;
|
|
779
|
+
|
|
632
780
|
for (let index = startIndex; index < content.length; index += 1) {
|
|
633
781
|
const char = content[index];
|
|
634
|
-
|
|
635
|
-
|
|
782
|
+
const next = content[index + 1];
|
|
783
|
+
|
|
784
|
+
if (state === 'line-comment') {
|
|
785
|
+
if (char === '\n') state = 'normal';
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
if (state === 'block-comment') {
|
|
789
|
+
if (char === '*' && next === '/') {
|
|
790
|
+
index += 1;
|
|
791
|
+
state = 'normal';
|
|
792
|
+
}
|
|
793
|
+
continue;
|
|
636
794
|
}
|
|
637
|
-
if (
|
|
638
|
-
|
|
795
|
+
if (state === 'single' || state === 'double' || state === 'template') {
|
|
796
|
+
if (escaped) {
|
|
797
|
+
escaped = false;
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (char === '\\') {
|
|
801
|
+
escaped = true;
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
if (state === 'single' && char === "'") state = 'normal';
|
|
805
|
+
else if (state === 'double' && char === '"') state = 'normal';
|
|
806
|
+
else if (state === 'template') {
|
|
807
|
+
if (char === '`' && templateExpressionDepth === 0) state = 'normal';
|
|
808
|
+
else if (char === '{' && content[index - 1] === '$') templateExpressionDepth += 1;
|
|
809
|
+
else if (char === '}' && templateExpressionDepth > 0) templateExpressionDepth -= 1;
|
|
810
|
+
}
|
|
811
|
+
continue;
|
|
639
812
|
}
|
|
640
|
-
|
|
641
|
-
|
|
813
|
+
|
|
814
|
+
if (char === '/' && next === '/') {
|
|
815
|
+
state = 'line-comment';
|
|
816
|
+
index += 1;
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
if (char === '/' && next === '*') {
|
|
820
|
+
state = 'block-comment';
|
|
821
|
+
index += 1;
|
|
822
|
+
continue;
|
|
642
823
|
}
|
|
824
|
+
if (char === "'") {
|
|
825
|
+
state = 'single';
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
if (char === '"') {
|
|
829
|
+
state = 'double';
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
if (char === '`') {
|
|
833
|
+
state = 'template';
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
if (char === '{') depth += 1;
|
|
837
|
+
if (char === '}') depth -= 1;
|
|
838
|
+
if (depth === 0) return content.slice(startIndex, index);
|
|
643
839
|
}
|
|
840
|
+
|
|
644
841
|
return '';
|
|
645
842
|
}
|
|
646
843
|
|
|
@@ -666,8 +863,8 @@ function extractFunctionCalls(filePath, content) {
|
|
|
666
863
|
}
|
|
667
864
|
|
|
668
865
|
function buildTestsMap(fileRecords) {
|
|
669
|
-
const testFiles = fileRecords.filter((file) =>
|
|
670
|
-
const sourceFiles = fileRecords.filter((file) => CODE_EXTENSIONS.has(file.ext) && !
|
|
866
|
+
const testFiles = fileRecords.filter((file) => isLikelyTestFilePath(file.filePath));
|
|
867
|
+
const sourceFiles = fileRecords.filter((file) => CODE_EXTENSIONS.has(file.ext) && !isLikelyTestFilePath(file.filePath));
|
|
671
868
|
|
|
672
869
|
return sourceFiles.map((sourceFile) => {
|
|
673
870
|
const matches = testFiles
|
|
@@ -771,35 +968,6 @@ function shouldSkipDirectory(name) {
|
|
|
771
968
|
return name.startsWith('.') && name !== '.claude' && name !== '.codex' && name !== '.antigravity';
|
|
772
969
|
}
|
|
773
970
|
|
|
774
|
-
function isLikelyTestFile(filePath) {
|
|
775
|
-
const lower = filePath.toLowerCase();
|
|
776
|
-
return (
|
|
777
|
-
lower.startsWith('__tests__/')
|
|
778
|
-
|| lower.startsWith('test/')
|
|
779
|
-
|| lower.startsWith('tests/')
|
|
780
|
-
|| lower.startsWith('spec/')
|
|
781
|
-
|| lower.startsWith('specs/')
|
|
782
|
-
|| lower.includes('/__tests__/')
|
|
783
|
-
|| lower.includes('/test/')
|
|
784
|
-
|| lower.includes('/spec/')
|
|
785
|
-
|| lower.includes('/specs/')
|
|
786
|
-
|| lower.endsWith('.test.js')
|
|
787
|
-
|| lower.endsWith('.test.ts')
|
|
788
|
-
|| lower.endsWith('.test.vue')
|
|
789
|
-
|| lower.endsWith('.test.tsx')
|
|
790
|
-
|| lower.endsWith('.test.jsx')
|
|
791
|
-
|| lower.endsWith('.test.mjs')
|
|
792
|
-
|| lower.endsWith('.test.cjs')
|
|
793
|
-
|| lower.endsWith('.spec.js')
|
|
794
|
-
|| lower.endsWith('.spec.ts')
|
|
795
|
-
|| lower.endsWith('.spec.vue')
|
|
796
|
-
|| lower.endsWith('.spec.tsx')
|
|
797
|
-
|| lower.endsWith('.spec.jsx')
|
|
798
|
-
|| lower.endsWith('.spec.mjs')
|
|
799
|
-
|| lower.endsWith('.spec.cjs')
|
|
800
|
-
);
|
|
801
|
-
}
|
|
802
|
-
|
|
803
971
|
function createSourceFingerprint(rootDir, discoveredFiles) {
|
|
804
972
|
const hash = crypto.createHash('sha256');
|
|
805
973
|
const normalizedEntries = discoveredFiles
|
|
@@ -985,7 +1153,7 @@ function classifyArchetypes(fileRecords, symbols) {
|
|
|
985
1153
|
return fileRecords
|
|
986
1154
|
.filter((f) => CODE_EXTENSIONS.has(f.ext))
|
|
987
1155
|
.map((file) => {
|
|
988
|
-
if (
|
|
1156
|
+
if (isLikelyTestFilePath(file.filePath)) {
|
|
989
1157
|
return {
|
|
990
1158
|
filePath: file.filePath,
|
|
991
1159
|
archetype: 'test',
|
package/src/index/gitHooks.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
+
import { escapeRegExp } from '../core/fileOps.js';
|
|
5
|
+
|
|
4
6
|
const HOOK_NAMES = ['post-commit', 'post-merge', 'post-checkout'];
|
|
5
7
|
const BEGIN = '# UKit index auto-refresh (begin)';
|
|
6
8
|
const END = '# UKit index auto-refresh (end)';
|
|
@@ -17,12 +19,13 @@ export async function installIndexRefreshHooks({ projectRoot }) {
|
|
|
17
19
|
const existing = await readTextOrEmpty(hookPath);
|
|
18
20
|
const base = existing || '#!/bin/sh\n';
|
|
19
21
|
|
|
20
|
-
if (
|
|
22
|
+
if (hasCompleteManagedBlock(base)) {
|
|
21
23
|
unchanged += 1;
|
|
22
24
|
continue;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
const
|
|
27
|
+
const cleanedBase = removeManagedBlockFragments(base);
|
|
28
|
+
const next = `${cleanedBase.trimEnd()}\n\n${buildManagedBlock()}\n`;
|
|
26
29
|
await fs.writeFile(hookPath, next, { mode: 0o755 });
|
|
27
30
|
await fs.chmod(hookPath, 0o755);
|
|
28
31
|
installed += 1;
|
|
@@ -45,7 +48,7 @@ export async function removeIndexRefreshHooks({ projectRoot }) {
|
|
|
45
48
|
for (const hookName of HOOK_NAMES) {
|
|
46
49
|
const hookPath = path.join(hooksDir, hookName);
|
|
47
50
|
const existing = await readTextOrEmpty(hookPath);
|
|
48
|
-
if (!existing || !
|
|
51
|
+
if (!existing || !hasCompleteManagedBlock(existing)) {
|
|
49
52
|
unchanged += 1;
|
|
50
53
|
continue;
|
|
51
54
|
}
|
|
@@ -75,8 +78,10 @@ function buildManagedBlock() {
|
|
|
75
78
|
].join('\n');
|
|
76
79
|
}
|
|
77
80
|
|
|
78
|
-
function
|
|
79
|
-
|
|
81
|
+
function hasCompleteManagedBlock(content) {
|
|
82
|
+
const beginIndex = content.indexOf(BEGIN);
|
|
83
|
+
const endIndex = content.indexOf(END, beginIndex + BEGIN.length);
|
|
84
|
+
return beginIndex >= 0 && endIndex > beginIndex;
|
|
80
85
|
}
|
|
81
86
|
|
|
82
87
|
function removeManagedBlock(content) {
|
|
@@ -84,8 +89,28 @@ function removeManagedBlock(content) {
|
|
|
84
89
|
return content.replace(pattern, '').replace(/\n{3,}/g, '\n\n');
|
|
85
90
|
}
|
|
86
91
|
|
|
87
|
-
function
|
|
88
|
-
|
|
92
|
+
function removeManagedBlockFragments(content) {
|
|
93
|
+
const lines = content.split('\n');
|
|
94
|
+
const kept = [];
|
|
95
|
+
let skipping = false;
|
|
96
|
+
|
|
97
|
+
for (const line of lines) {
|
|
98
|
+
if (line === BEGIN) {
|
|
99
|
+
skipping = true;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (line === END) {
|
|
104
|
+
skipping = false;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!skipping) {
|
|
109
|
+
kept.push(line);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return kept.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
89
114
|
}
|
|
90
115
|
|
|
91
116
|
async function ensureHooksDir(hooksDir) {
|