@ngockhoale/ukit 1.1.6 → 1.1.7
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 +11 -0
- package/README.md +5 -3
- package/manifests/platform.full.yaml +13 -0
- package/package.json +1 -1
- package/src/core/runtimeConfig.js +1 -1
- package/src/index/buildIndex.js +88 -2
- package/src/index/impactCatalog.js +126 -0
- package/src/index/impactContext.js +232 -0
- package/src/index/paths.js +1 -0
- package/src/index/resolveContext.js +1 -0
- package/src/index/taskRouting.js +10 -3
- package/src/index/verificationPlan.js +18 -1
- package/templates/.claude/hooks/skill-router.sh +1 -1
- package/templates/.claude/hooks/verification-guard.sh +150 -12
- package/templates/.claude/ukit/index/impact-context.mjs +122 -0
- package/templates/.claude/ukit/index/lib/index-core.mjs +352 -2
- package/templates/.claude/ukit/index/route-task.mjs +29 -3
- package/templates/.codex/README.md +1 -1
- package/templates/AGENTS.md +1 -1
- package/templates/CLAUDE.md +1 -1
- package/templates/ukit/README.md +1 -1
- package/templates/ukit/storage/config.json +1 -1
|
@@ -23,6 +23,7 @@ export async function deriveVerificationPlan({
|
|
|
23
23
|
targetFile = null,
|
|
24
24
|
taskType = null,
|
|
25
25
|
contextResult = null,
|
|
26
|
+
impactContext = null,
|
|
26
27
|
skillIds = [],
|
|
27
28
|
} = {}) {
|
|
28
29
|
const absoluteRoot = path.resolve(rootDir);
|
|
@@ -72,7 +73,15 @@ export async function deriveVerificationPlan({
|
|
|
72
73
|
const notes = [];
|
|
73
74
|
const docsOnly = primaryTargets.length > 0
|
|
74
75
|
&& primaryTargets.every((filePath) => filePath.startsWith('docs/'));
|
|
76
|
+
const impactTests = unique(impactContext?.recommendedTestFiles ?? []);
|
|
77
|
+
const impactRisk = Boolean(
|
|
78
|
+
impactContext?.requiresImpactCheck
|
|
79
|
+
|| (impactContext?.riskLabels ?? []).length > 0,
|
|
80
|
+
);
|
|
81
|
+
const sharedSimple = effectiveTaskType === 'shared-simple' || taskType === 'shared-simple';
|
|
75
82
|
const risky = effectiveTaskType === 'non-trivial'
|
|
83
|
+
|| sharedSimple
|
|
84
|
+
|| impactRisk
|
|
76
85
|
|| skillIds.some((skillId) => RISKY_SKILL_IDS.has(skillId));
|
|
77
86
|
|
|
78
87
|
let mode = 'minimal';
|
|
@@ -98,6 +107,14 @@ export async function deriveVerificationPlan({
|
|
|
98
107
|
}
|
|
99
108
|
}
|
|
100
109
|
|
|
110
|
+
if (impactTests.length > 0 && scripts.test) {
|
|
111
|
+
mode = relatedTests.length > 0 ? mode : 'impact-tests-first';
|
|
112
|
+
reasons.push('impactTests');
|
|
113
|
+
for (const testFile of impactTests.slice(0, 2)) {
|
|
114
|
+
commands.push(buildScriptCommand(packageManager, 'test', [testFile]));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
101
118
|
if (risky) {
|
|
102
119
|
reasons.push('riskySkillOrTask');
|
|
103
120
|
if (scripts.test) {
|
|
@@ -345,7 +362,7 @@ function deriveExecutionPolicy({
|
|
|
345
362
|
|| reasons.includes('riskySkillOrTask')
|
|
346
363
|
|| reasons.includes('highRiskFallback');
|
|
347
364
|
const hasPrimaryCommands = commands.length > 0;
|
|
348
|
-
const hasTargetedPrimaryCommands = mode === 'targeted-tests-first' && hasPrimaryCommands;
|
|
365
|
+
const hasTargetedPrimaryCommands = (mode === 'targeted-tests-first' || mode === 'impact-tests-first') && hasPrimaryCommands;
|
|
349
366
|
const sharedScope = sharedAbstractions.length > 0;
|
|
350
367
|
const noRelatedTests = relatedTests.length === 0;
|
|
351
368
|
const broadOnlyPrimary = hasPrimaryCommands && !hasTargetedPrimaryCommands && noRelatedTests;
|
|
@@ -386,7 +386,7 @@ const { pathToFileURL } = require('url');
|
|
|
386
386
|
return '';
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
-
function inferTaskType({ promptText, commandText, selectedIds, buildRouteSignalText }) {
|
|
389
|
+
function inferTaskType({ promptText, commandText, selectedIds, buildRouteSignalText, targetFile = null }) {
|
|
390
390
|
const lower = buildRouteSignalText(promptText, commandText);
|
|
391
391
|
|
|
392
392
|
if (
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
INPUT=$(cat)
|
|
6
6
|
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
7
7
|
STATE_FILE="$PROJECT_ROOT/.claude/ukit/skill-router-state.json"
|
|
8
|
+
ROUTE_CACHE_FILE="$PROJECT_ROOT/.claude/ukit/route-cache.json"
|
|
8
9
|
PROGRESS_FILE="$PROJECT_ROOT/.claude/ukit/verification-progress.json"
|
|
9
10
|
|
|
10
|
-
INPUT="$INPUT" STATE_FILE="$STATE_FILE" PROGRESS_FILE="$PROGRESS_FILE" node <<'NODE'
|
|
11
|
+
INPUT="$INPUT" STATE_FILE="$STATE_FILE" ROUTE_CACHE_FILE="$ROUTE_CACHE_FILE" PROGRESS_FILE="$PROGRESS_FILE" node <<'NODE'
|
|
11
12
|
const fs = require('fs');
|
|
12
13
|
const path = require('path');
|
|
13
14
|
|
|
@@ -125,12 +126,122 @@ function isExplicitBroadVerificationRequest(promptText) {
|
|
|
125
126
|
/chạy full test suite/,
|
|
126
127
|
/verify toàn bộ/,
|
|
127
128
|
/kiểm tra toàn bộ/,
|
|
129
|
+
// Plan-driven execution prompts often authorize the plan's final broad
|
|
130
|
+
// verification without spelling it as "full test suite".
|
|
131
|
+
/(?:run|execute|implement|do)\b.*\bfull\b.*\b(?:prd\s+)?plan\b/,
|
|
132
|
+
/\bfull\b.*\b(?:prd\s+)?plan\b.*\b(?:verify|verification|test|tests)\b/,
|
|
133
|
+
/(?:chạy|lam|làm|thực hiện|triển khai)\b.*\bfull\b.*\b(?:prd|plan|ke hoach|kế hoạch)\b/,
|
|
134
|
+
/\bfull\b.*\b(?:prd|plan|ke hoach|kế hoạch)\b.*\b(?:verify|verification|test|tests|kiểm tra)\b/,
|
|
128
135
|
].some((pattern) => pattern.test(text));
|
|
129
136
|
}
|
|
130
137
|
|
|
138
|
+
function collectRecentPromptTexts(state, routeCache, maxAgeMs) {
|
|
139
|
+
const prompts = [
|
|
140
|
+
state?.routingContext?.lastExplicitUserPromptText,
|
|
141
|
+
state?.routingContext?.promptText,
|
|
142
|
+
];
|
|
143
|
+
const now = Date.now();
|
|
144
|
+
|
|
145
|
+
for (const entry of routeCache?.entries ?? []) {
|
|
146
|
+
const updatedAt = Number(entry?.updatedAt ?? entry?.ts ?? 0);
|
|
147
|
+
if (Number.isFinite(updatedAt) && updatedAt > 0 && now - updatedAt > maxAgeMs) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
prompts.push(
|
|
151
|
+
entry?.routingContext?.lastExplicitUserPromptText,
|
|
152
|
+
entry?.routingContext?.promptText,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return unique(prompts.map((prompt) => String(prompt || '').trim()).filter(Boolean));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function hasRecentExplicitBroadVerificationRequest(state, routeCache, maxAgeMs) {
|
|
160
|
+
return collectRecentPromptTexts(state, routeCache, maxAgeMs)
|
|
161
|
+
.some((promptText) => isExplicitBroadVerificationRequest(promptText));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function collectRecentRouteSummaries(routeCache, maxAgeMs) {
|
|
165
|
+
const now = Date.now();
|
|
166
|
+
const summaries = [];
|
|
167
|
+
|
|
168
|
+
for (const entry of routeCache?.entries ?? []) {
|
|
169
|
+
const updatedAt = Number(entry?.updatedAt ?? entry?.ts ?? 0);
|
|
170
|
+
if (Number.isFinite(updatedAt) && updatedAt > 0 && now - updatedAt > maxAgeMs) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (entry?.routeSummary && typeof entry.routeSummary === 'object') {
|
|
174
|
+
summaries.push(entry.routeSummary);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return summaries;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function collectAttemptedCommands(progress, {
|
|
182
|
+
currentFingerprint = null,
|
|
183
|
+
maxAgeMs,
|
|
184
|
+
} = {}) {
|
|
185
|
+
const now = Date.now();
|
|
186
|
+
const attempted = [];
|
|
187
|
+
const progressUpdatedAt = Number(progress?.updatedAt ?? 0);
|
|
188
|
+
const legacyProgressIsFresh = Number.isFinite(progressUpdatedAt)
|
|
189
|
+
&& progressUpdatedAt > 0
|
|
190
|
+
&& now - progressUpdatedAt <= maxAgeMs;
|
|
191
|
+
|
|
192
|
+
if (progress?.fingerprint === currentFingerprint || legacyProgressIsFresh) {
|
|
193
|
+
attempted.push(...(progress?.attemptedCommands ?? []));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (const entry of progress?.recentAttempts ?? []) {
|
|
197
|
+
const ts = Number(entry?.ts ?? 0);
|
|
198
|
+
if (Number.isFinite(ts) && ts > 0 && now - ts <= maxAgeMs) {
|
|
199
|
+
attempted.push(entry.command);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return new Set(attempted.map(normalizeCommand).filter(Boolean));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function hasCompletedRecentTargetedLane({
|
|
207
|
+
routeCache,
|
|
208
|
+
attemptedCommands,
|
|
209
|
+
command,
|
|
210
|
+
maxAgeMs,
|
|
211
|
+
} = {}) {
|
|
212
|
+
const normalizedCommand = normalizeCommand(command);
|
|
213
|
+
if (!isBroadTestCommand(normalizedCommand)) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return collectRecentRouteSummaries(routeCache, maxAgeMs).some((summary) => {
|
|
218
|
+
const mode = summary?.policyMode;
|
|
219
|
+
if (mode !== 'auto-run-targeted' && mode !== 'auto-run-targeted-then-fallback') {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const recentPrimary = unique((summary?.primaryCommands ?? []).map(normalizeCommand));
|
|
224
|
+
if (recentPrimary.length === 0 || !recentPrimary.every((item) => attemptedCommands.has(item))) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const recentFallbacks = unique((summary?.fallbackCommands ?? []).map(normalizeCommand));
|
|
229
|
+
const recentPreferred = unique((summary?.preferredOrder ?? []).map(normalizeCommand));
|
|
230
|
+
return recentFallbacks.includes(normalizedCommand)
|
|
231
|
+
|| recentPreferred.includes(normalizedCommand)
|
|
232
|
+
|| recentPrimary.includes(normalizedCommand);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
131
236
|
function deny(message) {
|
|
132
|
-
|
|
133
|
-
process.
|
|
237
|
+
const advisory = message.replace(/^BLOCKED:/, 'ADVISORY:');
|
|
238
|
+
if (process.env.UKIT_VERIFICATION_GUARD_ENFORCE === '1') {
|
|
239
|
+
fs.writeSync(2, `${message}\n`);
|
|
240
|
+
process.exit(2);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
fs.writeSync(1, `${advisory}\n`);
|
|
244
|
+
process.exit(0);
|
|
134
245
|
}
|
|
135
246
|
|
|
136
247
|
const payload = (() => {
|
|
@@ -148,6 +259,7 @@ if (!command) {
|
|
|
148
259
|
const statePath = process.env.STATE_FILE;
|
|
149
260
|
const progressPath = process.env.PROGRESS_FILE;
|
|
150
261
|
const state = readJson(statePath, null);
|
|
262
|
+
const routeCache = readJson(process.env.ROUTE_CACHE_FILE, null);
|
|
151
263
|
const recommendation = state?.verificationRecommendation ?? null;
|
|
152
264
|
const routeSummary = state?.routeSummary ?? null;
|
|
153
265
|
const helpers = state?.helpers ?? null;
|
|
@@ -195,24 +307,39 @@ if (!policyMode && primaryCommands.length === 0 && fallbackCommands.length === 0
|
|
|
195
307
|
}
|
|
196
308
|
|
|
197
309
|
const progress = readJson(progressPath, {});
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
:
|
|
201
|
-
|
|
310
|
+
const attemptedCommands = collectAttemptedCommands(progress, {
|
|
311
|
+
currentFingerprint: state?.fingerprint || null,
|
|
312
|
+
maxAgeMs: STATE_FRESH_MS,
|
|
313
|
+
});
|
|
202
314
|
|
|
203
315
|
function persistAttempt(commandText) {
|
|
204
|
-
|
|
316
|
+
const normalizedCommandText = normalizeCommand(commandText);
|
|
317
|
+
attemptedCommands.add(normalizedCommandText);
|
|
318
|
+
const now = Date.now();
|
|
319
|
+
const recentAttempts = [
|
|
320
|
+
...(progress?.recentAttempts ?? []),
|
|
321
|
+
{ command: normalizedCommandText, ts: now },
|
|
322
|
+
]
|
|
323
|
+
.map((entry) => ({
|
|
324
|
+
command: normalizeCommand(entry?.command),
|
|
325
|
+
ts: Number(entry?.ts ?? now),
|
|
326
|
+
}))
|
|
327
|
+
.filter((entry) => entry.command && Number.isFinite(entry.ts) && now - entry.ts <= STATE_FRESH_MS)
|
|
328
|
+
.filter((entry, index, list) => (
|
|
329
|
+
list.findLastIndex((candidate) => candidate.command === entry.command) === index
|
|
330
|
+
));
|
|
205
331
|
writeJson(progressPath, {
|
|
206
332
|
fingerprint: state?.fingerprint || null,
|
|
333
|
+
updatedAt: now,
|
|
207
334
|
attemptedCommands: [...attemptedCommands],
|
|
335
|
+
recentAttempts,
|
|
208
336
|
});
|
|
209
337
|
}
|
|
210
338
|
|
|
211
339
|
const isPrimaryCommand = primaryCommands.includes(command);
|
|
212
|
-
const explicitBroadRequested =
|
|
213
|
-
state?.routingContext?.lastExplicitUserPromptText || state?.routingContext?.promptText || '',
|
|
214
|
-
);
|
|
340
|
+
const explicitBroadRequested = hasRecentExplicitBroadVerificationRequest(state, routeCache, STATE_FRESH_MS);
|
|
215
341
|
const isFallbackCommand = fallbackCommands.includes(command);
|
|
342
|
+
const isTargetedVerification = looksTargetedTestCommand(command);
|
|
216
343
|
if (!isPrimaryCommand && !isFallbackCommand && !isVerificationCommand(command)) {
|
|
217
344
|
process.exit(0);
|
|
218
345
|
}
|
|
@@ -223,10 +350,21 @@ const helperSource = routeSummary?.helperHint
|
|
|
223
350
|
|| '';
|
|
224
351
|
const helperCommand = helperSource ? ` Helper: ${helperSource}` : '';
|
|
225
352
|
const missingPrimary = primaryCommands.filter((item) => !attemptedCommands.has(item));
|
|
353
|
+
const recentTargetedLaneCompleted = hasCompletedRecentTargetedLane({
|
|
354
|
+
routeCache,
|
|
355
|
+
attemptedCommands,
|
|
356
|
+
command,
|
|
357
|
+
maxAgeMs: STATE_FRESH_MS,
|
|
358
|
+
});
|
|
226
359
|
const preferredText = formatCompactCommandList(preferredOrder, { separator: ' -> ' });
|
|
227
360
|
const missingPrimaryText = formatCompactCommandList(missingPrimary);
|
|
228
361
|
|
|
229
|
-
if (
|
|
362
|
+
if (isTargetedVerification) {
|
|
363
|
+
persistAttempt(command);
|
|
364
|
+
process.exit(0);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (policyMode === 'confirm-then-broad' && isBroadTestCommand(command) && !explicitBroadRequested && !recentTargetedLaneCompleted) {
|
|
230
368
|
deny(
|
|
231
369
|
`BLOCKED: Ask the user before broad verification. Routed policy is confirm-then-broad because the index could not localize related tests confidently.${helperCommand}`,
|
|
232
370
|
);
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import * as indexCore from './lib/index-core.mjs';
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
buildCodeIndex,
|
|
7
|
+
DEFAULT_INDEX_CACHE_MAX_AGE_MS,
|
|
8
|
+
getIndexArtifactGeneratedAt,
|
|
9
|
+
isIndexStale,
|
|
10
|
+
} = indexCore;
|
|
11
|
+
|
|
12
|
+
const resolveImpactContext = typeof indexCore.resolveImpactContext === 'function'
|
|
13
|
+
? indexCore.resolveImpactContext
|
|
14
|
+
: null;
|
|
15
|
+
const formatImpactConfidenceSummary = typeof indexCore.formatImpactConfidenceSummary === 'function'
|
|
16
|
+
? indexCore.formatImpactConfidenceSummary
|
|
17
|
+
: null;
|
|
18
|
+
|
|
19
|
+
const CALLEE_CAP = 5;
|
|
20
|
+
const CALLER_CAP = 5;
|
|
21
|
+
const MIRROR_CAP = 3;
|
|
22
|
+
const TEST_CAP = 4;
|
|
23
|
+
const VERIFY_CAP = 3;
|
|
24
|
+
|
|
25
|
+
async function main() {
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
const rootDir = getRootDir(args);
|
|
28
|
+
const changedFiles = parseCsv(readFlagValue(args, '--changed'));
|
|
29
|
+
const changedSymbols = parseCsv(readFlagValue(args, '--symbol'));
|
|
30
|
+
|
|
31
|
+
if (changedFiles.length === 0) {
|
|
32
|
+
console.error('Usage: node .claude/ukit/index/impact-context.mjs --changed <file[,file]> [--symbol <name[,name]>] [--root <dir>]');
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await ensureFreshIndex(rootDir);
|
|
38
|
+
|
|
39
|
+
const impact = resolveImpactContext
|
|
40
|
+
? await resolveImpactContext({ rootDir, changedFiles, changedSymbols })
|
|
41
|
+
: {
|
|
42
|
+
changedFiles,
|
|
43
|
+
changedSymbols,
|
|
44
|
+
riskLabels: [],
|
|
45
|
+
callees: [],
|
|
46
|
+
callers: [],
|
|
47
|
+
importers: [],
|
|
48
|
+
mirrorCounterparts: [],
|
|
49
|
+
relatedTests: [],
|
|
50
|
+
recommendedTestFiles: [],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
printCompact(impact);
|
|
54
|
+
|
|
55
|
+
if (formatImpactConfidenceSummary) {
|
|
56
|
+
console.log('');
|
|
57
|
+
console.log(formatImpactConfidenceSummary(impact));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function ensureFreshIndex(rootDir) {
|
|
62
|
+
const generatedAtMs = await getIndexArtifactGeneratedAt({ rootDir });
|
|
63
|
+
const stale = generatedAtMs === null
|
|
64
|
+
? true
|
|
65
|
+
: await isIndexStale({
|
|
66
|
+
rootDir,
|
|
67
|
+
maxAgeMs: DEFAULT_INDEX_CACHE_MAX_AGE_MS,
|
|
68
|
+
now: Date.now(),
|
|
69
|
+
generatedAtMs,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (stale) {
|
|
73
|
+
await buildCodeIndex({ rootDir });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function printCompact(impact) {
|
|
78
|
+
const changed = (impact.changedFiles ?? []).slice(0, 5).join(', ');
|
|
79
|
+
const symbols = (impact.changedSymbols ?? []).slice(0, 5).join(', ');
|
|
80
|
+
const risk = (impact.riskLabels ?? []).slice(0, 5).join(', ');
|
|
81
|
+
const callees = unique((impact.callees ?? []).map((entry) => entry?.symbol).filter(Boolean)).slice(0, CALLEE_CAP).join(', ');
|
|
82
|
+
const callers = unique((impact.callers ?? []).map((entry) => entry?.filePath).filter(Boolean)).slice(0, CALLER_CAP).join(', ');
|
|
83
|
+
const mirrors = unique((impact.mirrorCounterparts ?? []).map((entry) => entry?.filePath).filter(Boolean)).slice(0, MIRROR_CAP).join(', ');
|
|
84
|
+
const tests = (impact.recommendedTestFiles ?? impact.relatedTests ?? []).slice(0, TEST_CAP);
|
|
85
|
+
const verify = tests.slice(0, VERIFY_CAP).map((testFile) => `yarn test ${testFile}`).join(' | ');
|
|
86
|
+
|
|
87
|
+
console.log('[ukit:impact]');
|
|
88
|
+
console.log(`changed: ${changed || '(none)'}`);
|
|
89
|
+
if (symbols) console.log(`symbols: ${symbols}`);
|
|
90
|
+
if (risk) console.log(`risk: ${risk}`);
|
|
91
|
+
if (callees) console.log(`callees: ${callees}`);
|
|
92
|
+
if (callers) console.log(`callers: ${callers}`);
|
|
93
|
+
if (mirrors) console.log(`mirrors: ${mirrors}`);
|
|
94
|
+
if (tests.length > 0) console.log(`related-tests: ${tests.join(', ')}`);
|
|
95
|
+
if (verify) console.log(`verify: ${verify}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseCsv(value) {
|
|
99
|
+
return unique(
|
|
100
|
+
String(value ?? '')
|
|
101
|
+
.split(',')
|
|
102
|
+
.map((entry) => entry.trim().replace(/^\.\//, ''))
|
|
103
|
+
.filter(Boolean),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function unique(values = []) {
|
|
108
|
+
return [...new Set(values)];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function readFlagValue(args, flag) {
|
|
112
|
+
const index = args.indexOf(flag);
|
|
113
|
+
if (index === -1) return null;
|
|
114
|
+
return args[index + 1] ?? null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getRootDir(args) {
|
|
118
|
+
const explicitRoot = readFlagValue(args, '--root');
|
|
119
|
+
return path.resolve(explicitRoot || process.cwd());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await main();
|