@getmikk/core 1.2.0 → 1.3.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 +431 -0
- package/package.json +6 -2
- package/src/contract/contract-generator.ts +85 -85
- package/src/contract/contract-reader.ts +28 -28
- package/src/contract/contract-writer.ts +114 -114
- package/src/contract/index.ts +12 -12
- package/src/contract/lock-compiler.ts +221 -221
- package/src/contract/lock-reader.ts +34 -34
- package/src/contract/schema.ts +147 -147
- package/src/graph/cluster-detector.ts +312 -312
- package/src/graph/graph-builder.ts +211 -211
- package/src/graph/impact-analyzer.ts +55 -55
- package/src/graph/index.ts +4 -4
- package/src/graph/types.ts +59 -59
- package/src/hash/file-hasher.ts +30 -30
- package/src/hash/hash-store.ts +119 -119
- package/src/hash/index.ts +3 -3
- package/src/hash/tree-hasher.ts +20 -20
- package/src/index.ts +12 -12
- package/src/parser/base-parser.ts +16 -16
- package/src/parser/boundary-checker.ts +211 -211
- package/src/parser/index.ts +46 -46
- package/src/parser/types.ts +90 -90
- package/src/parser/typescript/ts-extractor.ts +543 -543
- package/src/parser/typescript/ts-parser.ts +41 -41
- package/src/parser/typescript/ts-resolver.ts +86 -86
- package/src/utils/errors.ts +42 -42
- package/src/utils/fs.ts +75 -75
- package/src/utils/fuzzy-match.ts +186 -186
- package/src/utils/logger.ts +36 -36
- package/src/utils/minimatch.ts +19 -19
- package/tests/contract.test.ts +134 -134
- package/tests/fixtures/simple-api/package.json +5 -5
- package/tests/fixtures/simple-api/src/auth/middleware.ts +9 -9
- package/tests/fixtures/simple-api/src/auth/verify.ts +6 -6
- package/tests/fixtures/simple-api/src/index.ts +9 -9
- package/tests/fixtures/simple-api/src/utils/jwt.ts +3 -3
- package/tests/fixtures/simple-api/tsconfig.json +8 -8
- package/tests/fuzzy-match.test.ts +142 -142
- package/tests/graph.test.ts +169 -169
- package/tests/hash.test.ts +49 -49
- package/tests/helpers.ts +83 -83
- package/tests/parser.test.ts +218 -218
- package/tsconfig.json +15 -15
package/src/utils/fuzzy-match.ts
CHANGED
|
@@ -1,186 +1,186 @@
|
|
|
1
|
-
import type { MikkLock, MikkLockFunction } from '../contract/schema.js'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* FuzzyMatcher — scores lock file functions against a search term or prompt.
|
|
5
|
-
*
|
|
6
|
-
* Used for:
|
|
7
|
-
* - "Did you mean?" suggestions when a function name isn't found
|
|
8
|
-
* - Ranking functions by relevance to a developer's prompt
|
|
9
|
-
* - Seed selection for context generation
|
|
10
|
-
*
|
|
11
|
-
* Per spec Section 6: scoring uses exact match, keyword overlap,
|
|
12
|
-
* camelCase decomposition, and Levenshtein distance.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
// ── Public API ───────────────────────────────────────────────
|
|
16
|
-
|
|
17
|
-
export interface FuzzyMatch {
|
|
18
|
-
name: string
|
|
19
|
-
file: string
|
|
20
|
-
moduleId: string
|
|
21
|
-
score: number
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Score every function in the lock against a prompt and return
|
|
26
|
-
* the top matches sorted by relevance.
|
|
27
|
-
*/
|
|
28
|
-
export function scoreFunctions(
|
|
29
|
-
prompt: string,
|
|
30
|
-
lock: MikkLock,
|
|
31
|
-
maxResults = 10
|
|
32
|
-
): FuzzyMatch[] {
|
|
33
|
-
const keywords = extractKeywords(prompt)
|
|
34
|
-
const promptLower = prompt.toLowerCase()
|
|
35
|
-
const results: FuzzyMatch[] = []
|
|
36
|
-
|
|
37
|
-
for (const fn of Object.values(lock.functions)) {
|
|
38
|
-
const score = scoreSingleFunction(fn, promptLower, keywords)
|
|
39
|
-
if (score > 0.2) {
|
|
40
|
-
results.push({
|
|
41
|
-
name: fn.name,
|
|
42
|
-
file: fn.file,
|
|
43
|
-
moduleId: fn.moduleId,
|
|
44
|
-
score: Math.min(score, 1.0),
|
|
45
|
-
})
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return results
|
|
50
|
-
.sort((a, b) => b.score - a.score)
|
|
51
|
-
.slice(0, maxResults)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Find functions whose names are similar to `searchTerm` — for
|
|
56
|
-
* "Did you mean?" suggestions when an exact match is not found.
|
|
57
|
-
*/
|
|
58
|
-
export function findFuzzyMatches(
|
|
59
|
-
searchTerm: string,
|
|
60
|
-
lock: MikkLock,
|
|
61
|
-
maxResults = 5
|
|
62
|
-
): string[] {
|
|
63
|
-
const searchLower = searchTerm.toLowerCase()
|
|
64
|
-
const scored: { name: string; score: number }[] = []
|
|
65
|
-
|
|
66
|
-
for (const fn of Object.values(lock.functions)) {
|
|
67
|
-
const nameLower = fn.name.toLowerCase()
|
|
68
|
-
|
|
69
|
-
// Levenshtein distance normalized by length
|
|
70
|
-
const distance = levenshtein(searchLower, nameLower)
|
|
71
|
-
const maxLen = Math.max(searchLower.length, nameLower.length)
|
|
72
|
-
const similarity = 1 - (distance / maxLen)
|
|
73
|
-
|
|
74
|
-
// Substring containment bonus
|
|
75
|
-
const containsScore =
|
|
76
|
-
nameLower.includes(searchLower) || searchLower.includes(nameLower)
|
|
77
|
-
? 0.3
|
|
78
|
-
: 0
|
|
79
|
-
|
|
80
|
-
const totalScore = similarity + containsScore
|
|
81
|
-
|
|
82
|
-
if (totalScore > 0.5) {
|
|
83
|
-
scored.push({ name: fn.name, score: totalScore })
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return scored
|
|
88
|
-
.sort((a, b) => b.score - a.score)
|
|
89
|
-
.slice(0, maxResults)
|
|
90
|
-
.map(s => s.name)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ── Scoring ──────────────────────────────────────────────────
|
|
94
|
-
|
|
95
|
-
function scoreSingleFunction(
|
|
96
|
-
fn: MikkLockFunction,
|
|
97
|
-
promptLower: string,
|
|
98
|
-
keywords: string[]
|
|
99
|
-
): number {
|
|
100
|
-
let score = 0
|
|
101
|
-
const fnNameLower = fn.name.toLowerCase()
|
|
102
|
-
const fileLower = fn.file.toLowerCase()
|
|
103
|
-
|
|
104
|
-
// Exact name match in prompt → very high
|
|
105
|
-
if (promptLower.includes(fnNameLower) && fnNameLower.length > 3) {
|
|
106
|
-
score += 0.9
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Keyword → function name matches
|
|
110
|
-
for (const kw of keywords) {
|
|
111
|
-
if (fnNameLower.includes(kw)) score += 0.3
|
|
112
|
-
if (fileLower.includes(kw)) score += 0.15
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// CamelCase word partial matches
|
|
116
|
-
const fnWords = splitCamelCase(fn.name).map(w => w.toLowerCase())
|
|
117
|
-
for (const kw of keywords) {
|
|
118
|
-
if (fnWords.some(w => w.startsWith(kw) || kw.startsWith(w))) {
|
|
119
|
-
score += 0.2
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Module match — "fix auth bug" → functions in auth module score higher
|
|
124
|
-
for (const kw of keywords) {
|
|
125
|
-
if (fn.moduleId.toLowerCase().includes(kw)) score += 0.25
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return score
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// ── Levenshtein Distance ─────────────────────────────────────
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Standard Levenshtein edit distance. O(n*m) time, O(min(n,m)) space.
|
|
135
|
-
*/
|
|
136
|
-
export function levenshtein(a: string, b: string): number {
|
|
137
|
-
if (a.length === 0) return b.length
|
|
138
|
-
if (b.length === 0) return a.length
|
|
139
|
-
|
|
140
|
-
// Ensure a is the shorter string for space efficiency
|
|
141
|
-
if (a.length > b.length) [a, b] = [b, a]
|
|
142
|
-
|
|
143
|
-
let prev = Array.from({ length: a.length + 1 }, (_, i) => i)
|
|
144
|
-
let curr = new Array<number>(a.length + 1)
|
|
145
|
-
|
|
146
|
-
for (let j = 1; j <= b.length; j++) {
|
|
147
|
-
curr[0] = j
|
|
148
|
-
for (let i = 1; i <= a.length; i++) {
|
|
149
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1
|
|
150
|
-
curr[i] = Math.min(
|
|
151
|
-
prev[i] + 1, // deletion
|
|
152
|
-
curr[i - 1] + 1, // insertion
|
|
153
|
-
prev[i - 1] + cost // substitution
|
|
154
|
-
)
|
|
155
|
-
}
|
|
156
|
-
;[prev, curr] = [curr, prev]
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return prev[a.length]
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// ── Helpers ──────────────────────────────────────────────────
|
|
163
|
-
|
|
164
|
-
export function splitCamelCase(name: string): string[] {
|
|
165
|
-
return name
|
|
166
|
-
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
167
|
-
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
168
|
-
.split(/[\s_-]+/)
|
|
169
|
-
.filter(w => w.length > 0)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export function extractKeywords(text: string): string[] {
|
|
173
|
-
const stopWords = new Set([
|
|
174
|
-
'the', 'a', 'an', 'in', 'on', 'at', 'to', 'for', 'of', 'and', 'or',
|
|
175
|
-
'is', 'it', 'fix', 'add', 'update', 'change', 'modify', 'make', 'create',
|
|
176
|
-
'bug', 'issue', 'error', 'problem', 'feature', 'function', 'file', 'code',
|
|
177
|
-
'new', 'old', 'all', 'this', 'that', 'from', 'with', 'move', 'remove',
|
|
178
|
-
'delete', 'refactor', 'should', 'can', 'will', 'must', 'need', 'want',
|
|
179
|
-
])
|
|
180
|
-
|
|
181
|
-
return text
|
|
182
|
-
.toLowerCase()
|
|
183
|
-
.replace(/[^a-z0-9\s]/g, ' ')
|
|
184
|
-
.split(/\s+/)
|
|
185
|
-
.filter(w => w.length > 2 && !stopWords.has(w))
|
|
186
|
-
}
|
|
1
|
+
import type { MikkLock, MikkLockFunction } from '../contract/schema.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FuzzyMatcher — scores lock file functions against a search term or prompt.
|
|
5
|
+
*
|
|
6
|
+
* Used for:
|
|
7
|
+
* - "Did you mean?" suggestions when a function name isn't found
|
|
8
|
+
* - Ranking functions by relevance to a developer's prompt
|
|
9
|
+
* - Seed selection for context generation
|
|
10
|
+
*
|
|
11
|
+
* Per spec Section 6: scoring uses exact match, keyword overlap,
|
|
12
|
+
* camelCase decomposition, and Levenshtein distance.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ── Public API ───────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export interface FuzzyMatch {
|
|
18
|
+
name: string
|
|
19
|
+
file: string
|
|
20
|
+
moduleId: string
|
|
21
|
+
score: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Score every function in the lock against a prompt and return
|
|
26
|
+
* the top matches sorted by relevance.
|
|
27
|
+
*/
|
|
28
|
+
export function scoreFunctions(
|
|
29
|
+
prompt: string,
|
|
30
|
+
lock: MikkLock,
|
|
31
|
+
maxResults = 10
|
|
32
|
+
): FuzzyMatch[] {
|
|
33
|
+
const keywords = extractKeywords(prompt)
|
|
34
|
+
const promptLower = prompt.toLowerCase()
|
|
35
|
+
const results: FuzzyMatch[] = []
|
|
36
|
+
|
|
37
|
+
for (const fn of Object.values(lock.functions)) {
|
|
38
|
+
const score = scoreSingleFunction(fn, promptLower, keywords)
|
|
39
|
+
if (score > 0.2) {
|
|
40
|
+
results.push({
|
|
41
|
+
name: fn.name,
|
|
42
|
+
file: fn.file,
|
|
43
|
+
moduleId: fn.moduleId,
|
|
44
|
+
score: Math.min(score, 1.0),
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return results
|
|
50
|
+
.sort((a, b) => b.score - a.score)
|
|
51
|
+
.slice(0, maxResults)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Find functions whose names are similar to `searchTerm` — for
|
|
56
|
+
* "Did you mean?" suggestions when an exact match is not found.
|
|
57
|
+
*/
|
|
58
|
+
export function findFuzzyMatches(
|
|
59
|
+
searchTerm: string,
|
|
60
|
+
lock: MikkLock,
|
|
61
|
+
maxResults = 5
|
|
62
|
+
): string[] {
|
|
63
|
+
const searchLower = searchTerm.toLowerCase()
|
|
64
|
+
const scored: { name: string; score: number }[] = []
|
|
65
|
+
|
|
66
|
+
for (const fn of Object.values(lock.functions)) {
|
|
67
|
+
const nameLower = fn.name.toLowerCase()
|
|
68
|
+
|
|
69
|
+
// Levenshtein distance normalized by length
|
|
70
|
+
const distance = levenshtein(searchLower, nameLower)
|
|
71
|
+
const maxLen = Math.max(searchLower.length, nameLower.length)
|
|
72
|
+
const similarity = 1 - (distance / maxLen)
|
|
73
|
+
|
|
74
|
+
// Substring containment bonus
|
|
75
|
+
const containsScore =
|
|
76
|
+
nameLower.includes(searchLower) || searchLower.includes(nameLower)
|
|
77
|
+
? 0.3
|
|
78
|
+
: 0
|
|
79
|
+
|
|
80
|
+
const totalScore = similarity + containsScore
|
|
81
|
+
|
|
82
|
+
if (totalScore > 0.5) {
|
|
83
|
+
scored.push({ name: fn.name, score: totalScore })
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return scored
|
|
88
|
+
.sort((a, b) => b.score - a.score)
|
|
89
|
+
.slice(0, maxResults)
|
|
90
|
+
.map(s => s.name)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Scoring ──────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
function scoreSingleFunction(
|
|
96
|
+
fn: MikkLockFunction,
|
|
97
|
+
promptLower: string,
|
|
98
|
+
keywords: string[]
|
|
99
|
+
): number {
|
|
100
|
+
let score = 0
|
|
101
|
+
const fnNameLower = fn.name.toLowerCase()
|
|
102
|
+
const fileLower = fn.file.toLowerCase()
|
|
103
|
+
|
|
104
|
+
// Exact name match in prompt → very high
|
|
105
|
+
if (promptLower.includes(fnNameLower) && fnNameLower.length > 3) {
|
|
106
|
+
score += 0.9
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Keyword → function name matches
|
|
110
|
+
for (const kw of keywords) {
|
|
111
|
+
if (fnNameLower.includes(kw)) score += 0.3
|
|
112
|
+
if (fileLower.includes(kw)) score += 0.15
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// CamelCase word partial matches
|
|
116
|
+
const fnWords = splitCamelCase(fn.name).map(w => w.toLowerCase())
|
|
117
|
+
for (const kw of keywords) {
|
|
118
|
+
if (fnWords.some(w => w.startsWith(kw) || kw.startsWith(w))) {
|
|
119
|
+
score += 0.2
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Module match — "fix auth bug" → functions in auth module score higher
|
|
124
|
+
for (const kw of keywords) {
|
|
125
|
+
if (fn.moduleId.toLowerCase().includes(kw)) score += 0.25
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return score
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Levenshtein Distance ─────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Standard Levenshtein edit distance. O(n*m) time, O(min(n,m)) space.
|
|
135
|
+
*/
|
|
136
|
+
export function levenshtein(a: string, b: string): number {
|
|
137
|
+
if (a.length === 0) return b.length
|
|
138
|
+
if (b.length === 0) return a.length
|
|
139
|
+
|
|
140
|
+
// Ensure a is the shorter string for space efficiency
|
|
141
|
+
if (a.length > b.length) [a, b] = [b, a]
|
|
142
|
+
|
|
143
|
+
let prev = Array.from({ length: a.length + 1 }, (_, i) => i)
|
|
144
|
+
let curr = new Array<number>(a.length + 1)
|
|
145
|
+
|
|
146
|
+
for (let j = 1; j <= b.length; j++) {
|
|
147
|
+
curr[0] = j
|
|
148
|
+
for (let i = 1; i <= a.length; i++) {
|
|
149
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1
|
|
150
|
+
curr[i] = Math.min(
|
|
151
|
+
prev[i] + 1, // deletion
|
|
152
|
+
curr[i - 1] + 1, // insertion
|
|
153
|
+
prev[i - 1] + cost // substitution
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
;[prev, curr] = [curr, prev]
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return prev[a.length]
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Helpers ──────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
export function splitCamelCase(name: string): string[] {
|
|
165
|
+
return name
|
|
166
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
167
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
168
|
+
.split(/[\s_-]+/)
|
|
169
|
+
.filter(w => w.length > 0)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function extractKeywords(text: string): string[] {
|
|
173
|
+
const stopWords = new Set([
|
|
174
|
+
'the', 'a', 'an', 'in', 'on', 'at', 'to', 'for', 'of', 'and', 'or',
|
|
175
|
+
'is', 'it', 'fix', 'add', 'update', 'change', 'modify', 'make', 'create',
|
|
176
|
+
'bug', 'issue', 'error', 'problem', 'feature', 'function', 'file', 'code',
|
|
177
|
+
'new', 'old', 'all', 'this', 'that', 'from', 'with', 'move', 'remove',
|
|
178
|
+
'delete', 'refactor', 'should', 'can', 'will', 'must', 'need', 'want',
|
|
179
|
+
])
|
|
180
|
+
|
|
181
|
+
return text
|
|
182
|
+
.toLowerCase()
|
|
183
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
184
|
+
.split(/\s+/)
|
|
185
|
+
.filter(w => w.length > 2 && !stopWords.has(w))
|
|
186
|
+
}
|
package/src/utils/logger.ts
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
type LogLevel = 'debug' | 'info' | 'warn' | 'error'
|
|
2
|
-
|
|
3
|
-
let currentLogLevel: LogLevel = 'info'
|
|
4
|
-
const levelOrder: Record<LogLevel, number> = { debug: 0, info: 1, warn: 2, error: 3 }
|
|
5
|
-
|
|
6
|
-
export function setLogLevel(level: LogLevel | 'silent') {
|
|
7
|
-
if (level === 'silent') {
|
|
8
|
-
currentLogLevel = 'error'
|
|
9
|
-
process.env.MIKK_LOG_LEVEL = 'silent'
|
|
10
|
-
} else {
|
|
11
|
-
currentLogLevel = level
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function shouldLog(level: LogLevel): boolean {
|
|
16
|
-
if (process.env.MIKK_LOG_LEVEL === 'silent') return false
|
|
17
|
-
return levelOrder[level] >= levelOrder[currentLogLevel]
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function log(level: LogLevel, message: string, data?: object) {
|
|
21
|
-
if (!shouldLog(level)) return
|
|
22
|
-
const entry = {
|
|
23
|
-
level,
|
|
24
|
-
timestamp: new Date().toISOString(),
|
|
25
|
-
message,
|
|
26
|
-
...data
|
|
27
|
-
}
|
|
28
|
-
console.error(JSON.stringify(entry))
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export const logger = {
|
|
32
|
-
debug: (message: string, data?: object) => log('debug', message, data),
|
|
33
|
-
info: (message: string, data?: object) => log('info', message, data),
|
|
34
|
-
warn: (message: string, data?: object) => log('warn', message, data),
|
|
35
|
-
error: (message: string, data?: object) => log('error', message, data),
|
|
36
|
-
}
|
|
1
|
+
type LogLevel = 'debug' | 'info' | 'warn' | 'error'
|
|
2
|
+
|
|
3
|
+
let currentLogLevel: LogLevel = 'info'
|
|
4
|
+
const levelOrder: Record<LogLevel, number> = { debug: 0, info: 1, warn: 2, error: 3 }
|
|
5
|
+
|
|
6
|
+
export function setLogLevel(level: LogLevel | 'silent') {
|
|
7
|
+
if (level === 'silent') {
|
|
8
|
+
currentLogLevel = 'error'
|
|
9
|
+
process.env.MIKK_LOG_LEVEL = 'silent'
|
|
10
|
+
} else {
|
|
11
|
+
currentLogLevel = level
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function shouldLog(level: LogLevel): boolean {
|
|
16
|
+
if (process.env.MIKK_LOG_LEVEL === 'silent') return false
|
|
17
|
+
return levelOrder[level] >= levelOrder[currentLogLevel]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function log(level: LogLevel, message: string, data?: object) {
|
|
21
|
+
if (!shouldLog(level)) return
|
|
22
|
+
const entry = {
|
|
23
|
+
level,
|
|
24
|
+
timestamp: new Date().toISOString(),
|
|
25
|
+
message,
|
|
26
|
+
...data
|
|
27
|
+
}
|
|
28
|
+
console.error(JSON.stringify(entry))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const logger = {
|
|
32
|
+
debug: (message: string, data?: object) => log('debug', message, data),
|
|
33
|
+
info: (message: string, data?: object) => log('info', message, data),
|
|
34
|
+
warn: (message: string, data?: object) => log('warn', message, data),
|
|
35
|
+
error: (message: string, data?: object) => log('error', message, data),
|
|
36
|
+
}
|
package/src/utils/minimatch.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Simple minimatch-like glob matching utility.
|
|
3
|
-
* Supports ** (any depth directory) and * (wildcard) patterns.
|
|
4
|
-
*/
|
|
5
|
-
export function minimatch(filePath: string, pattern: string): boolean {
|
|
6
|
-
// Normalize both to forward slashes
|
|
7
|
-
const normalizedPath = filePath.replace(/\\/g, '/')
|
|
8
|
-
const normalizedPattern = pattern.replace(/\\/g, '/')
|
|
9
|
-
|
|
10
|
-
// Convert glob pattern to regex
|
|
11
|
-
const regexStr = normalizedPattern
|
|
12
|
-
.replace(/\./g, '\\.')
|
|
13
|
-
.replace(/\*\*\//g, '(?:.+/)?')
|
|
14
|
-
.replace(/\*\*/g, '.*')
|
|
15
|
-
.replace(/\*/g, '[^/]*')
|
|
16
|
-
|
|
17
|
-
const regex = new RegExp(`^${regexStr}$`)
|
|
18
|
-
return regex.test(normalizedPath)
|
|
19
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Simple minimatch-like glob matching utility.
|
|
3
|
+
* Supports ** (any depth directory) and * (wildcard) patterns.
|
|
4
|
+
*/
|
|
5
|
+
export function minimatch(filePath: string, pattern: string): boolean {
|
|
6
|
+
// Normalize both to forward slashes
|
|
7
|
+
const normalizedPath = filePath.replace(/\\/g, '/')
|
|
8
|
+
const normalizedPattern = pattern.replace(/\\/g, '/')
|
|
9
|
+
|
|
10
|
+
// Convert glob pattern to regex
|
|
11
|
+
const regexStr = normalizedPattern
|
|
12
|
+
.replace(/\./g, '\\.')
|
|
13
|
+
.replace(/\*\*\//g, '(?:.+/)?')
|
|
14
|
+
.replace(/\*\*/g, '.*')
|
|
15
|
+
.replace(/\*/g, '[^/]*')
|
|
16
|
+
|
|
17
|
+
const regex = new RegExp(`^${regexStr}$`)
|
|
18
|
+
return regex.test(normalizedPath)
|
|
19
|
+
}
|