hive-lite 0.1.0
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/README.md +443 -0
- package/bin/hive.js +6 -0
- package/docs/cli-semantics.md +386 -0
- package/docs/skills/hive-lite-finish/SKILL.md +282 -0
- package/docs/skills/hive-lite-finish/agents/openai.yaml +4 -0
- package/docs/skills/hive-lite-finish/references/safety.md +95 -0
- package/docs/skills/hive-lite-finish/references/verdicts.md +123 -0
- package/docs/skills/hive-lite-map-maintainer/SKILL.md +203 -0
- package/docs/skills/hive-lite-map-maintainer/agents/openai.yaml +7 -0
- package/docs/skills/hive-lite-map-maintainer/references/lifecycle.md +114 -0
- package/docs/skills/hive-lite-map-maintainer/references/repair-rules.md +201 -0
- package/docs/skills/hive-lite-start-prompt/SKILL.md +283 -0
- package/docs/skills/hive-lite-start-prompt/agents/openai.yaml +4 -0
- package/docs/skills/hive-lite-start-prompt/references/input-calibration.md +82 -0
- package/docs/skills/hive-lite-start-prompt/references/preflight.md +116 -0
- package/package.json +40 -0
- package/src/cli.js +910 -0
- package/src/lib/change.js +642 -0
- package/src/lib/context.js +1104 -0
- package/src/lib/evidence.js +230 -0
- package/src/lib/fsx.js +54 -0
- package/src/lib/git.js +128 -0
- package/src/lib/glob.js +47 -0
- package/src/lib/health.js +1012 -0
- package/src/lib/id.js +13 -0
- package/src/lib/map.js +713 -0
- package/src/lib/next.js +341 -0
- package/src/lib/risk.js +122 -0
- package/src/lib/roles.js +109 -0
- package/src/lib/scope.js +168 -0
- package/src/lib/skills.js +349 -0
- package/src/lib/status.js +344 -0
- package/src/lib/yaml.js +223 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
const { validationStatus } = require('./risk');
|
|
2
|
+
const {
|
|
3
|
+
inferRoleFromPath,
|
|
4
|
+
isInternalBehaviorRole,
|
|
5
|
+
isUiManualRole,
|
|
6
|
+
normalizeRole,
|
|
7
|
+
} = require('./roles');
|
|
8
|
+
|
|
9
|
+
function areaForChange(map, change) {
|
|
10
|
+
const areaId = change.source && change.source.contextPacketId && change.scope && change.scope.matchedAreas
|
|
11
|
+
? change.scope.matchedAreas[0]
|
|
12
|
+
: null;
|
|
13
|
+
if (areaId) return map.areas.find((area) => area.id === areaId) || null;
|
|
14
|
+
const ids = (change.scope && change.scope.matchedAreas) || [];
|
|
15
|
+
return ids.length ? map.areas.find((area) => area.id === ids[0]) || null : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function roleForFile(area, file) {
|
|
19
|
+
const entry = area && Array.isArray(area.entrypoints)
|
|
20
|
+
? area.entrypoints.find((item) => item.path === file)
|
|
21
|
+
: null;
|
|
22
|
+
if (entry && entry.role) return normalizeRole(entry.role) || 'unknown';
|
|
23
|
+
return inferRoleFromPath(file);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function changedRoles(change, area) {
|
|
27
|
+
const seen = new Map();
|
|
28
|
+
for (const file of change.diff.changedFiles || []) {
|
|
29
|
+
const filePath = file.path || file;
|
|
30
|
+
const role = roleForFile(area, filePath);
|
|
31
|
+
if (!seen.has(role)) seen.set(role, []);
|
|
32
|
+
seen.get(role).push(filePath);
|
|
33
|
+
}
|
|
34
|
+
return [...seen.entries()].map(([role, files]) => ({ role, files }));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function validationResultForProfile(change, profile) {
|
|
38
|
+
return ((change.validation && change.validation.results) || [])
|
|
39
|
+
.find((result) => result.profile === profile && result.status === 'passed');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function manualEvidence(change) {
|
|
43
|
+
return ((change.validation && change.validation.results) || [])
|
|
44
|
+
.filter((result) => result.type === 'manual' && result.status === 'passed');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function commandEvidence(change) {
|
|
48
|
+
return ((change.validation && change.validation.results) || [])
|
|
49
|
+
.filter((result) => result.type === 'command' && result.status === 'passed');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function profileMeta(map, id) {
|
|
53
|
+
return (map.validationProfiles || []).find((profile) => profile.id === id) || {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function commandLooksFocusedTest(command, profileId, meta) {
|
|
57
|
+
const text = [command, profileId, meta.evidence_type, meta.evidenceType, meta.description].filter(Boolean).join(' ').toLowerCase();
|
|
58
|
+
return /\b(test|tests|spec|vitest|jest|playwright|e2e|smoke|golden)\b/.test(text)
|
|
59
|
+
|| text.includes('focused_test')
|
|
60
|
+
|| text.includes('existing_tests');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hasFocusedTestEvidence(change, map) {
|
|
64
|
+
return commandEvidence(change).some((result) => commandLooksFocusedTest(result.command, result.profile, profileMeta(map, result.profile)));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function requiredValidationMissing(change) {
|
|
68
|
+
const plan = (change.validation && change.validation.plan) || [];
|
|
69
|
+
if (!plan.some((item) => item.required !== false && item.type !== 'manual' && item.command)) return false;
|
|
70
|
+
return validationStatus(change) === 'not_run';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function hasManualEvidenceFor(change, profiles) {
|
|
74
|
+
if (!profiles.length) return manualEvidence(change).length > 0;
|
|
75
|
+
return profiles.some((profile) => validationResultForProfile(change, profile));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeList(values) {
|
|
79
|
+
return Array.isArray(values) ? values.filter(Boolean) : [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function reviewReasonAllowed(verification) {
|
|
83
|
+
return !verification || verification.review_with_reason_allowed !== false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function evaluateEvidencePolicy(change, map) {
|
|
87
|
+
const area = areaForChange(map, change);
|
|
88
|
+
const verification = (area && area.verification) || {};
|
|
89
|
+
const roles = changedRoles(change, area);
|
|
90
|
+
const roleNames = roles.map((item) => item.role);
|
|
91
|
+
const reasons = [];
|
|
92
|
+
const required = ['scope_clean'];
|
|
93
|
+
const recommended = [];
|
|
94
|
+
const missing = [];
|
|
95
|
+
const nextActions = [];
|
|
96
|
+
const manualProfiles = normalizeList(verification.manual_profiles);
|
|
97
|
+
const focusedRecommended = new Set([
|
|
98
|
+
...normalizeList(verification.focused_test_recommended_for_roles),
|
|
99
|
+
...normalizeList(verification.focusedTestRecommendedForRoles),
|
|
100
|
+
].map((role) => normalizeRole(role)).filter(Boolean));
|
|
101
|
+
const focusedRequired = new Set([
|
|
102
|
+
...normalizeList(verification.focused_test_required_for_roles),
|
|
103
|
+
...normalizeList(verification.focusedTestRequiredForRoles),
|
|
104
|
+
].map((role) => normalizeRole(role)).filter(Boolean));
|
|
105
|
+
|
|
106
|
+
let changeClass = 'unknown';
|
|
107
|
+
let verdict = 'acceptable';
|
|
108
|
+
|
|
109
|
+
if (change.scope.status === 'violation') {
|
|
110
|
+
verdict = 'blocked';
|
|
111
|
+
missing.push('scope_clean');
|
|
112
|
+
reasons.push('Scope violation blocks acceptance.');
|
|
113
|
+
} else if (change.scope.status === 'needs_review' || change.scope.status === 'unknown') {
|
|
114
|
+
verdict = 'needs_review_reason';
|
|
115
|
+
missing.push('review_reason');
|
|
116
|
+
reasons.push(`Scope is ${change.scope.status}.`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (change.risk && change.risk.verdict === 'blocked') {
|
|
120
|
+
verdict = 'blocked';
|
|
121
|
+
reasons.push(...(change.risk.blockingReasons || []));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const validation = validationStatus(change);
|
|
125
|
+
if (validation === 'failed') {
|
|
126
|
+
verdict = 'blocked';
|
|
127
|
+
missing.push('validation_passed');
|
|
128
|
+
reasons.push('Validation failed.');
|
|
129
|
+
} else if (requiredValidationMissing(change)) {
|
|
130
|
+
verdict = verdict === 'blocked' ? verdict : 'needs_validation';
|
|
131
|
+
missing.push('required_validation');
|
|
132
|
+
reasons.push('Required validation has not run.');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const manualLike = roleNames.some((role) => isUiManualRole(role));
|
|
136
|
+
const internalLike = roleNames.some((role) => isInternalBehaviorRole(role))
|
|
137
|
+
|| roleNames.some((role) => focusedRecommended.has(role) || focusedRequired.has(role));
|
|
138
|
+
|
|
139
|
+
if (manualLike && verification.direct_manual_allowed !== false) {
|
|
140
|
+
changeClass = 'direct_manual_verifiable';
|
|
141
|
+
const profiles = manualProfiles.length ? manualProfiles : ['manual-verification'];
|
|
142
|
+
required.push(`manual:${profiles.join('|')}`);
|
|
143
|
+
if (validation !== 'not_run' && validation !== 'failed' && verdict === 'acceptable' && !hasManualEvidenceFor(change, manualProfiles)) {
|
|
144
|
+
verdict = 'needs_manual_verification';
|
|
145
|
+
missing.push('manual_verification');
|
|
146
|
+
reasons.push('This change is directly manually verifiable, but no manual evidence was recorded.');
|
|
147
|
+
nextActions.push({
|
|
148
|
+
kind: 'manual_validation',
|
|
149
|
+
command: `hive-lite validate ${change.id} --manual ${profiles[0]} --result passed --note "<what you verified>"`,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
} else if (internalLike) {
|
|
153
|
+
changeClass = 'internal_logic';
|
|
154
|
+
const requiresFocused = roleNames.some((role) => focusedRequired.has(role));
|
|
155
|
+
if (requiresFocused) required.push('focused_test');
|
|
156
|
+
else recommended.push('focused_test');
|
|
157
|
+
if (validation !== 'not_run' && validation !== 'failed' && verdict === 'acceptable' && !hasFocusedTestEvidence(change, map)) {
|
|
158
|
+
verdict = requiresFocused ? 'evidence_insufficient' : 'needs_review_reason';
|
|
159
|
+
missing.push(requiresFocused ? 'focused_test' : 'focused_test_or_review_reason');
|
|
160
|
+
reasons.push('This change touches internal behavior; build/typecheck alone may not prove behavior.');
|
|
161
|
+
nextActions.push({
|
|
162
|
+
kind: 'focused_test',
|
|
163
|
+
message: 'Ask the coding agent to add or update a focused automated verification when practical.',
|
|
164
|
+
});
|
|
165
|
+
if (reviewReasonAllowed(verification)) {
|
|
166
|
+
nextActions.push({
|
|
167
|
+
kind: 'accept_reviewed',
|
|
168
|
+
command: `hive-lite accept ${change.id} --reviewed --reason "<why the evidence is sufficient>"`,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} else if (!area || !area.verification) {
|
|
173
|
+
changeClass = 'unknown_policy';
|
|
174
|
+
if (validation !== 'not_run' && validation !== 'failed' && verdict === 'acceptable') {
|
|
175
|
+
verdict = 'needs_review_reason';
|
|
176
|
+
missing.push('review_reason');
|
|
177
|
+
reasons.push('No verification policy is configured for this area.');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (change.risk && change.risk.reviewReasons && change.risk.reviewReasons.length > 0 && verdict === 'acceptable') {
|
|
182
|
+
verdict = 'needs_review_reason';
|
|
183
|
+
missing.push('review_reason');
|
|
184
|
+
reasons.push(...change.risk.reviewReasons);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (verdict === 'needs_validation') {
|
|
188
|
+
nextActions.push({ kind: 'validate', command: `hive-lite validate ${change.id}` });
|
|
189
|
+
}
|
|
190
|
+
if ((verdict === 'needs_review_reason' || verdict === 'evidence_insufficient') && reviewReasonAllowed(verification)) {
|
|
191
|
+
nextActions.push({
|
|
192
|
+
kind: 'accept_reviewed',
|
|
193
|
+
command: `hive-lite accept ${change.id} --reviewed --reason "<why the evidence is sufficient>"`,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (verdict === 'acceptable') {
|
|
197
|
+
nextActions.push({ kind: 'accept', command: `hive-lite accept ${change.id}` });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
version: 1,
|
|
202
|
+
class: changeClass,
|
|
203
|
+
verdict,
|
|
204
|
+
areaId: area ? area.id : null,
|
|
205
|
+
changedRoles: roles,
|
|
206
|
+
required: [...new Set(required)],
|
|
207
|
+
recommended: [...new Set(recommended)],
|
|
208
|
+
missing: [...new Set(missing)],
|
|
209
|
+
manualVerification: {
|
|
210
|
+
allowed: Boolean(manualLike && verification.direct_manual_allowed !== false),
|
|
211
|
+
profiles: manualProfiles.length ? manualProfiles : (manualLike ? ['manual-verification'] : []),
|
|
212
|
+
recorded: manualEvidence(change).map((item) => ({
|
|
213
|
+
profile: item.profile,
|
|
214
|
+
note: item.note || '',
|
|
215
|
+
})),
|
|
216
|
+
},
|
|
217
|
+
focusedTest: {
|
|
218
|
+
recommended: recommended.includes('focused_test'),
|
|
219
|
+
present: hasFocusedTestEvidence(change, map),
|
|
220
|
+
},
|
|
221
|
+
reviewWithReasonAllowed: reviewReasonAllowed(verification),
|
|
222
|
+
reasons: [...new Set(reasons)],
|
|
223
|
+
nextActions,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = {
|
|
228
|
+
evaluateEvidencePolicy,
|
|
229
|
+
inferRole: inferRoleFromPath,
|
|
230
|
+
};
|
package/src/lib/fsx.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function ensureDir(dir) {
|
|
5
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function readText(file) {
|
|
9
|
+
return fs.readFileSync(file, 'utf8');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function writeText(file, value) {
|
|
13
|
+
ensureDir(path.dirname(file));
|
|
14
|
+
fs.writeFileSync(file, value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readJson(file) {
|
|
18
|
+
return JSON.parse(readText(file));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function writeJson(file, value) {
|
|
22
|
+
writeText(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function exists(file) {
|
|
26
|
+
return fs.existsSync(file);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function listDirs(dir) {
|
|
30
|
+
if (!exists(dir)) return [];
|
|
31
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
32
|
+
.filter((entry) => entry.isDirectory())
|
|
33
|
+
.map((entry) => entry.name);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function appendIfMissing(file, lines) {
|
|
37
|
+
const existing = exists(file) ? readText(file) : '';
|
|
38
|
+
const additions = lines.filter((line) => !existing.split(/\r?\n/).includes(line));
|
|
39
|
+
if (additions.length === 0) return false;
|
|
40
|
+
const prefix = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
|
|
41
|
+
writeText(file, `${existing}${prefix}${additions.join('\n')}\n`);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = {
|
|
46
|
+
appendIfMissing,
|
|
47
|
+
ensureDir,
|
|
48
|
+
exists,
|
|
49
|
+
listDirs,
|
|
50
|
+
readJson,
|
|
51
|
+
readText,
|
|
52
|
+
writeJson,
|
|
53
|
+
writeText,
|
|
54
|
+
};
|
package/src/lib/git.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const cp = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function runGit(args, options = {}) {
|
|
5
|
+
return cp.execFileSync('git', args, {
|
|
6
|
+
cwd: options.cwd || process.cwd(),
|
|
7
|
+
encoding: 'utf8',
|
|
8
|
+
stdio: options.stdio || ['ignore', 'pipe', 'pipe'],
|
|
9
|
+
}).trim();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function runGitRaw(args, options = {}) {
|
|
13
|
+
return cp.execFileSync('git', args, {
|
|
14
|
+
cwd: options.cwd || process.cwd(),
|
|
15
|
+
encoding: 'utf8',
|
|
16
|
+
stdio: options.stdio || ['ignore', 'pipe', 'pipe'],
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function tryGit(args, options = {}) {
|
|
21
|
+
try {
|
|
22
|
+
return runGit(args, options);
|
|
23
|
+
} catch {
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function repoRoot(cwd = process.cwd()) {
|
|
29
|
+
const root = tryGit(['rev-parse', '--show-toplevel'], { cwd });
|
|
30
|
+
return root || cwd;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isGitRepo(cwd = process.cwd()) {
|
|
34
|
+
return tryGit(['rev-parse', '--is-inside-work-tree'], { cwd }) === 'true';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function requireGitRepo(cwd = process.cwd(), label = 'Hive Lite') {
|
|
38
|
+
if (!isGitRepo(cwd)) {
|
|
39
|
+
throw new Error(`${label} requires a git repository. Switch to the correct repo root, or initialize and commit the project manually before using Hive Lite.`);
|
|
40
|
+
}
|
|
41
|
+
return repoRoot(cwd);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function currentBranch(root) {
|
|
45
|
+
return tryGit(['branch', '--show-current'], { cwd: root }) || null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function currentHead(root) {
|
|
49
|
+
return tryGit(['rev-parse', 'HEAD'], { cwd: root }) || null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function statusLines(root) {
|
|
53
|
+
let output = '';
|
|
54
|
+
try {
|
|
55
|
+
output = runGitRaw(['status', '--porcelain'], { cwd: root });
|
|
56
|
+
} catch {
|
|
57
|
+
output = '';
|
|
58
|
+
}
|
|
59
|
+
return output ? output.split(/\r?\n/).filter(Boolean) : [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseStatusLine(line) {
|
|
63
|
+
const status = line.slice(0, 2);
|
|
64
|
+
let file = line.slice(3).trim();
|
|
65
|
+
if (file.includes(' -> ')) file = file.split(' -> ').pop().trim();
|
|
66
|
+
return { status, path: file.replace(/^"|"$/g, '') };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function changedFiles(root) {
|
|
70
|
+
const seen = new Map();
|
|
71
|
+
for (const line of statusLines(root)) {
|
|
72
|
+
const parsed = parseStatusLine(line);
|
|
73
|
+
seen.set(parsed.path, {
|
|
74
|
+
path: parsed.path,
|
|
75
|
+
status: parsed.status.includes('?') ? 'untracked' : 'modified',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return [...seen.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function diffFromHead(root) {
|
|
82
|
+
return runGitRaw(['diff', '--binary', 'HEAD', '--'], { cwd: root });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function grep(root, term, max = 20) {
|
|
86
|
+
if (!term || term.length < 2) return [];
|
|
87
|
+
const output = tryGit(['grep', '-n', '-i', '--', term], { cwd: root });
|
|
88
|
+
if (!output) return [];
|
|
89
|
+
return output.split(/\r?\n/)
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
.slice(0, max)
|
|
92
|
+
.map((line) => {
|
|
93
|
+
const first = line.indexOf(':');
|
|
94
|
+
const second = line.indexOf(':', first + 1);
|
|
95
|
+
if (first === -1 || second === -1) return { path: line, line: null, text: '' };
|
|
96
|
+
return {
|
|
97
|
+
path: line.slice(0, first),
|
|
98
|
+
line: Number(line.slice(first + 1, second)),
|
|
99
|
+
text: line.slice(second + 1).trim(),
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function commitAll(root, message) {
|
|
105
|
+
runGit(['add', '-A'], { cwd: root });
|
|
106
|
+
runGit(['commit', '-m', message], { cwd: root, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
107
|
+
return currentHead(root);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function displayPath(root, file) {
|
|
111
|
+
return path.relative(root, file).replace(/\\/g, '/');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
changedFiles,
|
|
116
|
+
commitAll,
|
|
117
|
+
currentBranch,
|
|
118
|
+
currentHead,
|
|
119
|
+
diffFromHead,
|
|
120
|
+
displayPath,
|
|
121
|
+
grep,
|
|
122
|
+
isGitRepo,
|
|
123
|
+
repoRoot,
|
|
124
|
+
requireGitRepo,
|
|
125
|
+
runGit,
|
|
126
|
+
statusLines,
|
|
127
|
+
tryGit,
|
|
128
|
+
};
|
package/src/lib/glob.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
function escapeRegex(value) {
|
|
2
|
+
return value.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function globToRegExp(pattern) {
|
|
6
|
+
const normalized = pattern.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
7
|
+
let source = '';
|
|
8
|
+
for (let i = 0; i < normalized.length; i += 1) {
|
|
9
|
+
const ch = normalized[i];
|
|
10
|
+
const next = normalized[i + 1];
|
|
11
|
+
if (ch === '*' && next === '*') {
|
|
12
|
+
const after = normalized[i + 2];
|
|
13
|
+
if (after === '/') {
|
|
14
|
+
source += '(?:.*\\/)?';
|
|
15
|
+
i += 2;
|
|
16
|
+
} else {
|
|
17
|
+
source += '.*';
|
|
18
|
+
i += 1;
|
|
19
|
+
}
|
|
20
|
+
} else if (ch === '*') {
|
|
21
|
+
source += '[^/]*';
|
|
22
|
+
} else {
|
|
23
|
+
source += escapeRegex(ch);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return new RegExp(`^${source}$`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function matchesPattern(file, pattern) {
|
|
30
|
+
const normalized = file.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
31
|
+
const cleanPattern = String(pattern || '').trim().replace(/^\.\//, '');
|
|
32
|
+
if (!cleanPattern) return false;
|
|
33
|
+
if (!cleanPattern.includes('*')) {
|
|
34
|
+
const prefix = cleanPattern.replace(/\/+$/, '');
|
|
35
|
+
return normalized === prefix || normalized.startsWith(`${prefix}/`);
|
|
36
|
+
}
|
|
37
|
+
return globToRegExp(cleanPattern).test(normalized);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function firstPatternMatch(file, patterns) {
|
|
41
|
+
return (patterns || []).find((pattern) => matchesPattern(file, typeof pattern === 'string' ? pattern : pattern.pattern));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
firstPatternMatch,
|
|
46
|
+
matchesPattern,
|
|
47
|
+
};
|