opencode-manager 0.3.1 → 0.4.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.
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Fuzzy search utilities using fast-fuzzy.
3
+ * Extracted from TUI for reuse in CLI.
4
+ */
5
+ import { Searcher, MatchData, FullOptions } from "fast-fuzzy"
6
+
7
+ /**
8
+ * A search candidate with an item and searchable text.
9
+ */
10
+ export type SearchCandidate<T> = {
11
+ item: T
12
+ searchText: string
13
+ }
14
+
15
+ /**
16
+ * A search result with the matched item and score.
17
+ */
18
+ export type SearchResult<T> = {
19
+ item: T
20
+ score: number
21
+ }
22
+
23
+ /**
24
+ * Options for fuzzy search.
25
+ */
26
+ export type FuzzySearchOptions = {
27
+ /** Maximum number of results to return (default: 200) */
28
+ limit?: number
29
+ }
30
+
31
+ // Type for the searcher options with keySelector
32
+ type SearcherOptions<T> = FullOptions<SearchCandidate<T>> & {
33
+ keySelector: (c: SearchCandidate<T>) => string
34
+ }
35
+
36
+ /**
37
+ * Creates a fuzzy searcher for items with searchable text.
38
+ *
39
+ * @param candidates - Array of search candidates with items and search text
40
+ * @returns A Searcher instance configured for the candidates
41
+ */
42
+ export function createSearcher<T>(
43
+ candidates: SearchCandidate<T>[]
44
+ ): Searcher<SearchCandidate<T>, SearcherOptions<T>> {
45
+ return new Searcher(candidates, {
46
+ keySelector: (c: SearchCandidate<T>) => c.searchText,
47
+ })
48
+ }
49
+
50
+ /**
51
+ * Performs a fuzzy search on the given candidates.
52
+ *
53
+ * @param candidates - Array of search candidates
54
+ * @param query - The search query string
55
+ * @param options - Optional search options
56
+ * @returns Array of search results sorted by score (descending)
57
+ */
58
+ export function fuzzySearch<T>(
59
+ candidates: SearchCandidate<T>[],
60
+ query: string,
61
+ options?: FuzzySearchOptions
62
+ ): SearchResult<T>[] {
63
+ const q = query.trim()
64
+ if (!q) {
65
+ // No query - return all items with score 1
66
+ return candidates.map((c) => ({ item: c.item, score: 1 }))
67
+ }
68
+
69
+ const searcher = createSearcher(candidates)
70
+ const results = searcher.search(q, { returnMatchData: true }) as MatchData<SearchCandidate<T>>[]
71
+
72
+ const mapped: SearchResult<T>[] = results.map((match) => ({
73
+ item: match.item.item,
74
+ score: match.score,
75
+ }))
76
+
77
+ // Sort by score descending
78
+ mapped.sort((a, b) => b.score - a.score)
79
+
80
+ // Apply limit
81
+ const limit = options?.limit ?? 200
82
+ if (mapped.length > limit) {
83
+ return mapped.slice(0, limit)
84
+ }
85
+
86
+ return mapped
87
+ }
88
+
89
+ /**
90
+ * Performs fuzzy search and returns only the matched items (not scores).
91
+ *
92
+ * @param candidates - Array of search candidates
93
+ * @param query - The search query string
94
+ * @param options - Optional search options
95
+ * @returns Array of matched items sorted by score (descending)
96
+ */
97
+ export function fuzzySearchItems<T>(
98
+ candidates: SearchCandidate<T>[],
99
+ query: string,
100
+ options?: FuzzySearchOptions
101
+ ): T[] {
102
+ return fuzzySearch(candidates, query, options).map((r) => r.item)
103
+ }
104
+
105
+ /**
106
+ * Builds a search text string from multiple fields.
107
+ * Joins all fields with spaces and normalizes whitespace.
108
+ *
109
+ * @param fields - Array of string fields to combine
110
+ * @returns A normalized search text string
111
+ */
112
+ export function buildSearchText(...fields: (string | null | undefined)[]): string {
113
+ return fields
114
+ .filter((f): f is string => f != null && f !== "")
115
+ .join(" ")
116
+ .replace(/\s+/g, " ")
117
+ .trim()
118
+ }
119
+
120
+ /**
121
+ * Options for tokenized search.
122
+ */
123
+ export type TokenizedSearchOptions = {
124
+ /** Maximum number of results to return (default: 200) */
125
+ limit?: number
126
+ }
127
+
128
+ /**
129
+ * Performs tokenized substring search on items.
130
+ * Matches TUI project search semantics:
131
+ * - Query is split on whitespace into tokens
132
+ * - Each token must be found in at least one of the searchable fields
133
+ * - Matching is case-insensitive substring matching
134
+ *
135
+ * @param items - Array of items to search
136
+ * @param query - The search query string
137
+ * @param getFields - Function to extract searchable fields from an item
138
+ * @param options - Optional search options
139
+ * @returns Array of items that match all tokens
140
+ */
141
+ export function tokenizedSearch<T>(
142
+ items: T[],
143
+ query: string,
144
+ getFields: (item: T) => (string | null | undefined)[],
145
+ options?: TokenizedSearchOptions
146
+ ): T[] {
147
+ const q = query?.trim().toLowerCase() ?? ""
148
+ if (!q) {
149
+ const limit = options?.limit ?? 200
150
+ return items.slice(0, limit)
151
+ }
152
+
153
+ const tokens = q.split(/\s+/).filter(Boolean)
154
+ if (tokens.length === 0) {
155
+ const limit = options?.limit ?? 200
156
+ return items.slice(0, limit)
157
+ }
158
+
159
+ const matched = items.filter((item) => {
160
+ const fields = getFields(item).map((f) => (f || "").toLowerCase())
161
+ return tokens.every((tok) => fields.some((field) => field.includes(tok)))
162
+ })
163
+
164
+ const limit = options?.limit ?? 200
165
+ if (matched.length > limit) {
166
+ return matched.slice(0, limit)
167
+ }
168
+
169
+ return matched
170
+ }