@talex-touch/utils 1.0.42 → 1.0.45

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.
Files changed (234) hide show
  1. package/.eslintcache +1 -0
  2. package/__tests__/cloud-sync-sdk.test.ts +442 -0
  3. package/__tests__/icons/icons.test.ts +84 -0
  4. package/__tests__/plugin-sdk-lifecycle.test.ts +130 -0
  5. package/__tests__/power-sdk.test.ts +143 -0
  6. package/__tests__/preset-export-types.test.ts +108 -0
  7. package/__tests__/search/fuzzy-match.test.ts +137 -0
  8. package/__tests__/transport/port-policy.test.ts +44 -0
  9. package/__tests__/transport-domain-sdks.test.ts +152 -0
  10. package/__tests__/types/update.test.ts +67 -0
  11. package/account/account-sdk.ts +915 -0
  12. package/account/index.ts +2 -0
  13. package/account/types.ts +321 -0
  14. package/analytics/client.ts +136 -0
  15. package/analytics/index.ts +2 -0
  16. package/analytics/types.ts +156 -0
  17. package/animation/auto-resize.ts +322 -0
  18. package/animation/window-node.ts +26 -19
  19. package/auth/clerk-types.ts +12 -30
  20. package/auth/index.ts +0 -2
  21. package/auth/useAuthState.ts +6 -14
  22. package/base/index.ts +2 -0
  23. package/base/log-level.ts +105 -0
  24. package/channel/index.ts +170 -69
  25. package/cloud-sync/cloud-sync-sdk.ts +450 -0
  26. package/cloud-sync/index.ts +1 -0
  27. package/common/file-scan-utils.ts +17 -9
  28. package/common/index.ts +4 -0
  29. package/common/logger/index.ts +46 -0
  30. package/common/logger/logger-manager.ts +303 -0
  31. package/common/logger/module-logger.ts +270 -0
  32. package/common/logger/transport-logger.ts +234 -0
  33. package/common/logger/types.ts +93 -0
  34. package/common/search/gather.ts +48 -6
  35. package/common/search/index.ts +8 -0
  36. package/common/storage/constants.ts +13 -0
  37. package/common/storage/entity/app-settings.ts +245 -0
  38. package/common/storage/entity/index.ts +3 -0
  39. package/common/storage/entity/layout-atom-types.ts +147 -0
  40. package/common/storage/entity/openers.ts +1 -0
  41. package/common/storage/entity/preset-cloud-api.ts +132 -0
  42. package/common/storage/entity/preset-export-types.ts +256 -0
  43. package/common/storage/entity/shortcut-settings.ts +1 -0
  44. package/common/storage/shortcut-storage.ts +11 -0
  45. package/common/utils/clone-diagnostics.ts +105 -0
  46. package/common/utils/file.ts +16 -8
  47. package/common/utils/index.ts +6 -2
  48. package/common/utils/payload-preview.ts +173 -0
  49. package/common/utils/polling.ts +167 -13
  50. package/common/utils/safe-path.ts +103 -0
  51. package/common/utils/safe-shell.ts +115 -0
  52. package/common/utils/task-queue.ts +4 -1
  53. package/core-box/builder/tuff-builder.ts +0 -1
  54. package/core-box/index.ts +1 -1
  55. package/core-box/recommendation.ts +38 -1
  56. package/core-box/tuff/tuff-dsl.ts +32 -0
  57. package/electron/download-manager.ts +10 -7
  58. package/electron/env-tool.ts +42 -40
  59. package/electron/index.ts +0 -1
  60. package/env/index.ts +156 -0
  61. package/eslint.config.js +55 -0
  62. package/i18n/index.ts +62 -0
  63. package/i18n/locales/en.json +226 -0
  64. package/i18n/locales/zh.json +226 -0
  65. package/i18n/message-keys.ts +236 -0
  66. package/i18n/resolver.ts +181 -0
  67. package/icons/index.ts +257 -0
  68. package/icons/svg.ts +69 -0
  69. package/index.ts +9 -1
  70. package/intelligence/client.ts +72 -42
  71. package/market/constants.ts +9 -5
  72. package/market/index.ts +1 -1
  73. package/market/types.ts +19 -4
  74. package/package.json +15 -5
  75. package/permission/index.ts +143 -46
  76. package/permission/legacy.ts +26 -0
  77. package/permission/registry.ts +304 -0
  78. package/permission/types.ts +164 -0
  79. package/plugin/channel.ts +68 -39
  80. package/plugin/index.ts +80 -7
  81. package/plugin/install.ts +3 -0
  82. package/plugin/log/types.ts +22 -5
  83. package/plugin/node/logger-manager.ts +11 -3
  84. package/plugin/node/logger.ts +24 -17
  85. package/plugin/preload.ts +25 -2
  86. package/plugin/providers/index.ts +4 -4
  87. package/plugin/providers/market-client.ts +6 -3
  88. package/plugin/providers/npm-provider.ts +22 -7
  89. package/plugin/providers/tpex-provider.ts +22 -8
  90. package/plugin/sdk/box-items.ts +14 -0
  91. package/plugin/sdk/box-sdk.ts +64 -0
  92. package/plugin/sdk/channel.ts +119 -4
  93. package/plugin/sdk/clipboard.ts +26 -12
  94. package/plugin/sdk/cloud-sync.ts +113 -0
  95. package/plugin/sdk/common.ts +19 -11
  96. package/plugin/sdk/core-box.ts +6 -15
  97. package/plugin/sdk/division-box.ts +160 -65
  98. package/plugin/sdk/examples/storage-onDidChange-example.js +5 -2
  99. package/plugin/sdk/feature-sdk.ts +111 -76
  100. package/plugin/sdk/flow.ts +146 -45
  101. package/plugin/sdk/hooks/bridge.ts +13 -6
  102. package/plugin/sdk/hooks/life-cycle.ts +35 -16
  103. package/plugin/sdk/index.ts +14 -3
  104. package/plugin/sdk/intelligence.ts +87 -0
  105. package/plugin/sdk/meta/README.md +179 -0
  106. package/plugin/sdk/meta-sdk.ts +244 -0
  107. package/plugin/sdk/notification.ts +9 -0
  108. package/plugin/sdk/plugin-info.ts +64 -0
  109. package/plugin/sdk/power.ts +155 -0
  110. package/plugin/sdk/recommend.ts +21 -0
  111. package/plugin/sdk/service/index.ts +12 -8
  112. package/plugin/sdk/sqlite.ts +141 -0
  113. package/plugin/sdk/storage.ts +2 -6
  114. package/plugin/sdk/system.ts +2 -9
  115. package/plugin/sdk/temp-files.ts +41 -0
  116. package/plugin/sdk/touch-sdk.ts +18 -0
  117. package/plugin/sdk/types.ts +44 -4
  118. package/plugin/sdk/window/index.ts +12 -9
  119. package/plugin/sdk-version.ts +231 -0
  120. package/preload/renderer.ts +3 -2
  121. package/renderer/hooks/arg-mapper.ts +16 -2
  122. package/renderer/hooks/index.ts +13 -0
  123. package/renderer/hooks/initialize.ts +2 -1
  124. package/renderer/hooks/use-agent-market-sdk.ts +7 -0
  125. package/renderer/hooks/use-agent-market.ts +106 -0
  126. package/renderer/hooks/use-agents-sdk.ts +7 -0
  127. package/renderer/hooks/use-app-sdk.ts +7 -0
  128. package/renderer/hooks/use-channel.ts +33 -4
  129. package/renderer/hooks/use-download-sdk.ts +21 -0
  130. package/renderer/hooks/use-intelligence-sdk.ts +7 -0
  131. package/renderer/hooks/use-intelligence-stats.ts +290 -0
  132. package/renderer/hooks/use-intelligence.ts +55 -214
  133. package/renderer/hooks/use-market-sdk.ts +16 -0
  134. package/renderer/hooks/use-notification-sdk.ts +7 -0
  135. package/renderer/hooks/use-permission-sdk.ts +7 -0
  136. package/renderer/hooks/use-permission.ts +325 -0
  137. package/renderer/hooks/use-platform-sdk.ts +7 -0
  138. package/renderer/hooks/use-plugin-sdk.ts +16 -0
  139. package/renderer/hooks/use-settings-sdk.ts +7 -0
  140. package/renderer/hooks/use-update-sdk.ts +21 -0
  141. package/renderer/index.ts +1 -0
  142. package/renderer/ref.ts +19 -10
  143. package/renderer/shared/components/SharedPluginDetailContent.vue +84 -0
  144. package/renderer/shared/components/SharedPluginDetailHeader.vue +116 -0
  145. package/renderer/shared/components/SharedPluginDetailMetaList.vue +39 -0
  146. package/renderer/shared/components/SharedPluginDetailReadme.vue +45 -0
  147. package/renderer/shared/components/SharedPluginDetailVersions.vue +98 -0
  148. package/renderer/shared/components/index.ts +5 -0
  149. package/renderer/shared/components/shims-vue.d.ts +5 -0
  150. package/renderer/shared/index.ts +2 -0
  151. package/renderer/shared/plugin-detail.ts +62 -0
  152. package/renderer/storage/app-settings.ts +3 -1
  153. package/renderer/storage/base-storage.ts +508 -82
  154. package/renderer/storage/intelligence-storage.ts +31 -40
  155. package/renderer/storage/openers.ts +3 -1
  156. package/renderer/storage/storage-subscription.ts +126 -42
  157. package/renderer/touch-sdk/env.ts +10 -10
  158. package/renderer/touch-sdk/index.ts +114 -18
  159. package/renderer/touch-sdk/terminal.ts +24 -13
  160. package/search/feature-matcher.ts +279 -0
  161. package/search/fuzzy-match.ts +64 -34
  162. package/search/index.ts +10 -0
  163. package/search/levenshtein-utils.ts +17 -11
  164. package/transport/errors.ts +310 -0
  165. package/transport/event/builder.ts +378 -0
  166. package/transport/event/index.ts +7 -0
  167. package/transport/event/types.ts +292 -0
  168. package/transport/events/index.ts +2690 -0
  169. package/transport/events/meta-overlay.ts +79 -0
  170. package/transport/events/types/agents.ts +177 -0
  171. package/transport/events/types/app-index.ts +20 -0
  172. package/transport/events/types/app.ts +475 -0
  173. package/transport/events/types/box-item.ts +222 -0
  174. package/transport/events/types/clipboard.ts +80 -0
  175. package/transport/events/types/core-box.ts +534 -0
  176. package/transport/events/types/device-idle.ts +7 -0
  177. package/transport/events/types/division-box.ts +99 -0
  178. package/transport/events/types/download.ts +115 -0
  179. package/transport/events/types/file-index.ts +84 -0
  180. package/transport/events/types/flow.ts +149 -0
  181. package/transport/events/types/index.ts +70 -0
  182. package/transport/events/types/market.ts +39 -0
  183. package/transport/events/types/meta-overlay.ts +184 -0
  184. package/transport/events/types/notification.ts +140 -0
  185. package/transport/events/types/permission.ts +90 -0
  186. package/transport/events/types/platform.ts +8 -0
  187. package/transport/events/types/plugin.ts +631 -0
  188. package/transport/events/types/sentry.ts +20 -0
  189. package/transport/events/types/storage.ts +208 -0
  190. package/transport/events/types/transport.ts +60 -0
  191. package/transport/events/types/tray.ts +16 -0
  192. package/transport/events/types/update.ts +78 -0
  193. package/transport/index.ts +141 -0
  194. package/transport/main.ts +2 -0
  195. package/transport/prelude.ts +208 -0
  196. package/transport/sdk/constants.ts +29 -0
  197. package/transport/sdk/domains/agents-market.ts +47 -0
  198. package/transport/sdk/domains/agents.ts +62 -0
  199. package/transport/sdk/domains/app.ts +48 -0
  200. package/transport/sdk/domains/disposable.ts +35 -0
  201. package/transport/sdk/domains/download.ts +139 -0
  202. package/transport/sdk/domains/index.ts +13 -0
  203. package/transport/sdk/domains/intelligence.ts +616 -0
  204. package/transport/sdk/domains/market.ts +35 -0
  205. package/transport/sdk/domains/notification.ts +62 -0
  206. package/transport/sdk/domains/permission.ts +85 -0
  207. package/transport/sdk/domains/platform.ts +19 -0
  208. package/transport/sdk/domains/plugin.ts +144 -0
  209. package/transport/sdk/domains/settings.ts +102 -0
  210. package/transport/sdk/domains/update.ts +64 -0
  211. package/transport/sdk/index.ts +60 -0
  212. package/transport/sdk/main-transport.ts +710 -0
  213. package/transport/sdk/main.ts +9 -0
  214. package/transport/sdk/plugin-transport.ts +654 -0
  215. package/transport/sdk/port-policy.ts +38 -0
  216. package/transport/sdk/renderer-transport.ts +1165 -0
  217. package/transport/types.ts +605 -0
  218. package/types/agent.ts +399 -0
  219. package/types/cloud-sync.ts +157 -0
  220. package/types/division-box.ts +31 -31
  221. package/types/download.ts +1 -0
  222. package/types/flow.ts +63 -12
  223. package/types/icon.ts +2 -1
  224. package/types/index.ts +5 -0
  225. package/types/intelligence.ts +166 -173
  226. package/types/modules/base.ts +2 -0
  227. package/types/path-browserify.d.ts +5 -0
  228. package/types/platform.ts +12 -0
  229. package/types/startup-info.ts +32 -0
  230. package/types/touch-app-core.ts +8 -8
  231. package/types/update.ts +94 -1
  232. package/vitest.config.ts +25 -0
  233. package/auth/useClerkConfig.ts +0 -40
  234. package/auth/useClerkProvider.ts +0 -52
@@ -1,4 +1,15 @@
1
- import type { ITouchClientChannel } from '@talex-touch/utils/channel'
1
+ import type { ITuffTransport } from '@talex-touch/utils/transport'
2
+ import { defineRawEvent } from '@talex-touch/utils/transport/event/builder'
3
+
4
+ const terminalCreateEvent = defineRawEvent<{ command: string, args?: string[] }, { id: string }>(
5
+ 'terminal:create',
6
+ )
7
+ const terminalWriteEvent = defineRawEvent<{ id: string, data: string }, void>('terminal:write')
8
+ const terminalKillEvent = defineRawEvent<{ id: string }, void>('terminal:kill')
9
+ const terminalDataEvent = defineRawEvent<{ id: string, data: string }, void>('terminal:data')
10
+ const terminalExitEvent = defineRawEvent<{ id: string, exitCode: number | null }, void>(
11
+ 'terminal:exit',
12
+ )
2
13
 
3
14
  type DataCallback = (data: string) => void
4
15
  type ExitCallback = (exitCode: number | null) => void
@@ -7,10 +18,10 @@ export class Terminal {
7
18
  private id: string | null = null
8
19
  private onDataCallback: DataCallback | null = null
9
20
  private onExitCallback: ExitCallback | null = null
10
- private channel: ITouchClientChannel
21
+ private transport: ITuffTransport
11
22
 
12
- constructor(channel: ITouchClientChannel) {
13
- this.channel = channel
23
+ constructor(transport: ITuffTransport) {
24
+ this.transport = transport
14
25
  }
15
26
 
16
27
  /**
@@ -26,19 +37,19 @@ export class Terminal {
26
37
  // However, for simplicity in this refactor, we'll assume exec is called for a new, independent command.
27
38
  // A more robust implementation might track multiple concurrent processes.
28
39
 
29
- const { id } = await this.channel.send('terminal:create', { command, args })
40
+ const { id } = await this.transport.send(terminalCreateEvent, { command, args })
30
41
  this.id = id
31
42
 
32
43
  // Re-register listeners for the new process ID
33
- this.channel.regChannel('terminal:data', (channelData) => {
34
- if (this.id === channelData.data.id && this.onDataCallback) {
35
- this.onDataCallback(channelData.data.data)
44
+ this.transport.on(terminalDataEvent, (payload) => {
45
+ if (this.id === payload.id && this.onDataCallback) {
46
+ this.onDataCallback(payload.data)
36
47
  }
37
48
  })
38
49
 
39
- this.channel.regChannel('terminal:exit', (channelData) => {
40
- if (this.id === channelData.data.id && this.onExitCallback) {
41
- this.onExitCallback(channelData.data.exitCode)
50
+ this.transport.on(terminalExitEvent, (payload) => {
51
+ if (this.id === payload.id && this.onExitCallback) {
52
+ this.onExitCallback(payload.exitCode)
42
53
  this.id = null
43
54
  }
44
55
  })
@@ -53,7 +64,7 @@ export class Terminal {
53
64
  */
54
65
  public write(data: string): void {
55
66
  if (this.id) {
56
- this.channel.send('terminal:write', { id: this.id, data })
67
+ void this.transport.send(terminalWriteEvent, { id: this.id, data })
57
68
  }
58
69
  }
59
70
 
@@ -62,7 +73,7 @@ export class Terminal {
62
73
  */
63
74
  public kill(): void {
64
75
  if (this.id) {
65
- this.channel.send('terminal:kill', { id: this.id })
76
+ void this.transport.send(terminalKillEvent, { id: this.id })
66
77
  this.id = null
67
78
  }
68
79
  }
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Feature Matching Utilities
3
+ *
4
+ * Provides enhanced search matching for plugin features with:
5
+ * - Pinyin/English token matching
6
+ * - Fuzzy match support
7
+ * - Match range generation for UI highlighting
8
+ */
9
+
10
+ import { fuzzyMatch, indicesToRanges } from './fuzzy-match'
11
+
12
+ /**
13
+ * Match range for highlighting
14
+ */
15
+ export interface MatchRange {
16
+ start: number
17
+ end: number
18
+ }
19
+
20
+ /**
21
+ * Feature match result with score and highlight ranges
22
+ */
23
+ export interface FeatureMatchResult {
24
+ /** Whether the feature matches the query */
25
+ matched: boolean
26
+ /** Match score (0-1000, higher is better) */
27
+ score: number
28
+ /** Match type for debugging */
29
+ matchType: 'exact' | 'token' | 'prefix' | 'contains' | 'fuzzy' | 'none'
30
+ /** Match ranges for highlighting in title */
31
+ matchRanges: MatchRange[]
32
+ /** Which token matched (for debugging) */
33
+ matchedToken?: string
34
+ }
35
+
36
+ /**
37
+ * Options for feature matching
38
+ */
39
+ export interface FeatureMatchOptions {
40
+ /** Feature title/name */
41
+ title: string
42
+ /** Feature description */
43
+ desc?: string
44
+ /** Pre-computed search tokens (pinyin, initials, etc.) */
45
+ searchTokens?: string[]
46
+ /** Search query */
47
+ query: string
48
+ /** Enable fuzzy matching (default: true) */
49
+ enableFuzzy?: boolean
50
+ /** Maximum fuzzy errors (default: 2) */
51
+ maxFuzzyErrors?: number
52
+ }
53
+
54
+ /**
55
+ * Find substring match and return range
56
+ */
57
+ function findSubstringMatch(text: string, query: string): MatchRange | null {
58
+ const lowerText = text.toLowerCase()
59
+ const lowerQuery = query.toLowerCase()
60
+ const index = lowerText.indexOf(lowerQuery)
61
+
62
+ if (index === -1)
63
+ return null
64
+
65
+ return { start: index, end: index + query.length }
66
+ }
67
+
68
+ /**
69
+ * Match query against a single token and return match info
70
+ */
71
+ function matchToken(
72
+ token: string,
73
+ query: string,
74
+ ): { matched: boolean, score: number, type: 'exact' | 'prefix' | 'contains' | 'none' } {
75
+ const lowerToken = token.toLowerCase()
76
+ const lowerQuery = query.toLowerCase()
77
+
78
+ // Exact match
79
+ if (lowerToken === lowerQuery) {
80
+ return { matched: true, score: 1000, type: 'exact' }
81
+ }
82
+
83
+ // Prefix match (token starts with query)
84
+ if (lowerToken.startsWith(lowerQuery)) {
85
+ // Score based on how much of the token is matched
86
+ const coverage = lowerQuery.length / lowerToken.length
87
+ return { matched: true, score: 800 + Math.round(coverage * 100), type: 'prefix' }
88
+ }
89
+
90
+ // Contains match
91
+ if (lowerToken.includes(lowerQuery)) {
92
+ const coverage = lowerQuery.length / lowerToken.length
93
+ return { matched: true, score: 600 + Math.round(coverage * 50), type: 'contains' }
94
+ }
95
+
96
+ return { matched: false, score: 0, type: 'none' }
97
+ }
98
+
99
+ /**
100
+ * Match feature against search query
101
+ *
102
+ * This is the main matching function that:
103
+ * 1. Tries exact/prefix/contains match against title
104
+ * 2. Tries token matching (pinyin, initials, keywords)
105
+ * 3. Falls back to fuzzy matching
106
+ *
107
+ * @returns FeatureMatchResult with score and highlight ranges
108
+ */
109
+ export function matchFeature(options: FeatureMatchOptions): FeatureMatchResult {
110
+ const {
111
+ title,
112
+ desc,
113
+ searchTokens = [],
114
+ query,
115
+ enableFuzzy = true,
116
+ maxFuzzyErrors = 2,
117
+ } = options
118
+
119
+ const trimmedQuery = query.trim()
120
+
121
+ // Empty query - no match
122
+ if (!trimmedQuery) {
123
+ return { matched: false, score: 0, matchType: 'none', matchRanges: [] }
124
+ }
125
+
126
+ const lowerQuery = trimmedQuery.toLowerCase()
127
+ const lowerTitle = title.toLowerCase()
128
+
129
+ // 1. Exact title match
130
+ if (lowerTitle === lowerQuery) {
131
+ return {
132
+ matched: true,
133
+ score: 1000,
134
+ matchType: 'exact',
135
+ matchRanges: [{ start: 0, end: title.length }],
136
+ }
137
+ }
138
+
139
+ // 2. Title prefix match
140
+ if (lowerTitle.startsWith(lowerQuery)) {
141
+ return {
142
+ matched: true,
143
+ score: 900,
144
+ matchType: 'prefix',
145
+ matchRanges: [{ start: 0, end: trimmedQuery.length }],
146
+ }
147
+ }
148
+
149
+ // 3. Title contains match
150
+ const titleMatch = findSubstringMatch(title, trimmedQuery)
151
+ if (titleMatch) {
152
+ return {
153
+ matched: true,
154
+ score: 700,
155
+ matchType: 'contains',
156
+ matchRanges: [titleMatch],
157
+ }
158
+ }
159
+
160
+ // 4. Search tokens matching (pinyin, initials, keywords)
161
+ // This enables searching "fanyi" to match "翻译"
162
+ if (searchTokens.length > 0) {
163
+ let bestTokenMatch: {
164
+ score: number
165
+ type: 'exact' | 'prefix' | 'contains'
166
+ token: string
167
+ } | null = null
168
+
169
+ for (const token of searchTokens) {
170
+ if (!token)
171
+ continue
172
+
173
+ const result = matchToken(token, trimmedQuery)
174
+ if (result.matched && (!bestTokenMatch || result.score > bestTokenMatch.score)) {
175
+ bestTokenMatch = {
176
+ score: result.score,
177
+ type: result.type as 'exact' | 'prefix' | 'contains',
178
+ token,
179
+ }
180
+ }
181
+ }
182
+
183
+ if (bestTokenMatch) {
184
+ // Token matched - highlight entire title since we can't map token back to characters
185
+ // For pinyin matches, the full Chinese title is relevant
186
+ return {
187
+ matched: true,
188
+ score: bestTokenMatch.score - 50, // Slightly lower than direct title match
189
+ matchType: 'token',
190
+ matchRanges: [{ start: 0, end: title.length }],
191
+ matchedToken: bestTokenMatch.token,
192
+ }
193
+ }
194
+ }
195
+
196
+ // 5. Description matching (lower priority)
197
+ if (desc) {
198
+ const descMatch = findSubstringMatch(desc, trimmedQuery)
199
+ if (descMatch) {
200
+ return {
201
+ matched: true,
202
+ score: 400,
203
+ matchType: 'contains',
204
+ matchRanges: [], // No title highlight for desc matches
205
+ }
206
+ }
207
+ }
208
+
209
+ // 6. Fuzzy matching on title
210
+ if (enableFuzzy) {
211
+ const fuzzyResult = fuzzyMatch(title, trimmedQuery, maxFuzzyErrors)
212
+ if (fuzzyResult.matched && fuzzyResult.score > 0.5) {
213
+ return {
214
+ matched: true,
215
+ score: Math.round(fuzzyResult.score * 500), // Scale to 0-500 range
216
+ matchType: 'fuzzy',
217
+ matchRanges: indicesToRanges(fuzzyResult.matchedIndices),
218
+ }
219
+ }
220
+
221
+ // Try fuzzy on tokens
222
+ for (const token of searchTokens) {
223
+ if (!token || token.length < 2)
224
+ continue
225
+
226
+ const tokenFuzzy = fuzzyMatch(token, trimmedQuery, maxFuzzyErrors)
227
+ if (tokenFuzzy.matched && tokenFuzzy.score > 0.6) {
228
+ return {
229
+ matched: true,
230
+ score: Math.round(tokenFuzzy.score * 400),
231
+ matchType: 'fuzzy',
232
+ matchRanges: [{ start: 0, end: title.length }],
233
+ matchedToken: token,
234
+ }
235
+ }
236
+ }
237
+ }
238
+
239
+ return { matched: false, score: 0, matchType: 'none', matchRanges: [] }
240
+ }
241
+
242
+ /**
243
+ * Batch match multiple features and return sorted results
244
+ *
245
+ * @param features Array of features with their search metadata
246
+ * @param query Search query
247
+ * @returns Features sorted by match score (highest first), with match metadata
248
+ */
249
+ export function matchFeatures<T extends { searchTokens?: string[] }>(
250
+ features: Array<{
251
+ feature: T
252
+ title: string
253
+ desc?: string
254
+ }>,
255
+ query: string,
256
+ ): Array<{
257
+ feature: T
258
+ result: FeatureMatchResult
259
+ }> {
260
+ const results: Array<{ feature: T, result: FeatureMatchResult }> = []
261
+
262
+ for (const { feature, title, desc } of features) {
263
+ const result = matchFeature({
264
+ title,
265
+ desc,
266
+ searchTokens: feature.searchTokens,
267
+ query,
268
+ })
269
+
270
+ if (result.matched) {
271
+ results.push({ feature, result })
272
+ }
273
+ }
274
+
275
+ // Sort by score descending
276
+ results.sort((a, b) => b.result.score - a.result.score)
277
+
278
+ return results
279
+ }
@@ -24,7 +24,7 @@ export interface FuzzyMatchResult {
24
24
  export function fuzzyMatch(
25
25
  target: string,
26
26
  query: string,
27
- maxErrors = 2
27
+ maxErrors = 2,
28
28
  ): FuzzyMatchResult {
29
29
  if (!query || !target) {
30
30
  return { matched: false, score: 0, matchedIndices: [] }
@@ -38,7 +38,7 @@ export function fuzzyMatch(
38
38
  return {
39
39
  matched: true,
40
40
  score: 1,
41
- matchedIndices: Array.from({ length: target.length }, (_, i) => i)
41
+ matchedIndices: Array.from({ length: target.length }, (_, i) => i),
42
42
  }
43
43
  }
44
44
 
@@ -48,7 +48,7 @@ export function fuzzyMatch(
48
48
  return {
49
49
  matched: true,
50
50
  score: 0.95,
51
- matchedIndices: Array.from({ length: query.length }, (_, i) => substringIndex + i)
51
+ matchedIndices: Array.from({ length: query.length }, (_, i) => substringIndex + i),
52
52
  }
53
53
  }
54
54
 
@@ -58,7 +58,7 @@ export function fuzzyMatch(
58
58
  return {
59
59
  matched: true,
60
60
  score: 0.8 + (subsequenceResult.matchedIndices.length / target.length) * 0.1,
61
- matchedIndices: subsequenceResult.matchedIndices
61
+ matchedIndices: subsequenceResult.matchedIndices,
62
62
  }
63
63
  }
64
64
 
@@ -77,8 +77,8 @@ export function fuzzyMatch(
77
77
  */
78
78
  function subsequenceMatch(
79
79
  target: string,
80
- query: string
81
- ): { matched: boolean; matchedIndices: number[] } {
80
+ query: string,
81
+ ): { matched: boolean, matchedIndices: number[] } {
82
82
  const matchedIndices: number[] = []
83
83
  let queryIdx = 0
84
84
 
@@ -91,7 +91,7 @@ function subsequenceMatch(
91
91
 
92
92
  return {
93
93
  matched: queryIdx === query.length,
94
- matchedIndices
94
+ matchedIndices,
95
95
  }
96
96
  }
97
97
 
@@ -102,13 +102,15 @@ function subsequenceMatch(
102
102
  function fuzzyMatchWithErrors(
103
103
  target: string,
104
104
  query: string,
105
- maxErrors: number
105
+ maxErrors: number,
106
106
  ): FuzzyMatchResult {
107
107
  const m = query.length
108
108
  const n = target.length
109
109
 
110
- if (m === 0) return { matched: true, score: 1, matchedIndices: [] }
111
- if (n === 0) return { matched: false, score: 0, matchedIndices: [] }
110
+ if (m === 0)
111
+ return { matched: true, score: 1, matchedIndices: [] }
112
+ if (n === 0)
113
+ return { matched: false, score: 0, matchedIndices: [] }
112
114
 
113
115
  // Allow more errors for longer queries
114
116
  const allowedErrors = Math.min(maxErrors, Math.floor(m / 3) + 1)
@@ -135,7 +137,7 @@ function fuzzyMatchWithErrors(
135
137
  bestScore = score
136
138
  bestStart = start
137
139
  // Adjust indices to be relative to the full target string
138
- bestMatchedIndices = matchedIndices.map((i) => start + i)
140
+ bestMatchedIndices = matchedIndices.map(i => start + i)
139
141
  }
140
142
  }
141
143
  }
@@ -145,7 +147,7 @@ function fuzzyMatchWithErrors(
145
147
  return {
146
148
  matched: true,
147
149
  score: bestScore,
148
- matchedIndices: bestMatchedIndices
150
+ matchedIndices: bestMatchedIndices,
149
151
  }
150
152
  }
151
153
 
@@ -157,25 +159,41 @@ function fuzzyMatchWithErrors(
157
159
  */
158
160
  function editDistanceWithPath(
159
161
  s1: string,
160
- s2: string
161
- ): { distance: number; matchedIndices: number[] } {
162
+ s2: string,
163
+ ): { distance: number, matchedIndices: number[] } {
162
164
  const m = s1.length
163
165
  const n = s2.length
164
166
 
165
167
  // DP table
166
- const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))
168
+ const dp: number[][] = Array.from({ length: m + 1 }, () => Array.from({ length: n + 1 }, () => 0))
167
169
 
168
170
  // Initialize
169
- for (let i = 0; i <= m; i++) dp[i][0] = i
170
- for (let j = 0; j <= n; j++) dp[0][j] = j
171
+ for (let i = 0; i <= m; i++) {
172
+ const row = dp[i]
173
+ if (row)
174
+ row[0] = i
175
+ }
176
+ const firstRow = dp[0]
177
+ if (firstRow) {
178
+ for (let j = 0; j <= n; j++)
179
+ firstRow[j] = j
180
+ }
171
181
 
172
182
  // Fill DP table
173
183
  for (let i = 1; i <= m; i++) {
174
184
  for (let j = 1; j <= n; j++) {
185
+ const row = dp[i]
186
+ const prevRow = dp[i - 1]
187
+ if (!row || !prevRow)
188
+ continue
175
189
  if (s1[i - 1] === s2[j - 1]) {
176
- dp[i][j] = dp[i - 1][j - 1]
177
- } else {
178
- dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
190
+ row[j] = prevRow[j - 1] ?? 0
191
+ }
192
+ else {
193
+ const up = prevRow[j] ?? 0
194
+ const left = row[j - 1] ?? 0
195
+ const diag = prevRow[j - 1] ?? 0
196
+ row[j] = 1 + Math.min(up, left, diag)
179
197
  }
180
198
  }
181
199
  }
@@ -190,20 +208,24 @@ function editDistanceWithPath(
190
208
  matchedIndices.unshift(i - 1)
191
209
  i--
192
210
  j--
193
- } else if (dp[i - 1][j - 1] <= dp[i - 1][j] && dp[i - 1][j - 1] <= dp[i][j - 1]) {
211
+ }
212
+ else if ((dp[i - 1]?.[j - 1] ?? 0) <= (dp[i - 1]?.[j] ?? 0)
213
+ && (dp[i - 1]?.[j - 1] ?? 0) <= (dp[i]?.[j - 1] ?? 0)) {
194
214
  // Substitution
195
215
  i--
196
216
  j--
197
- } else if (dp[i - 1][j] <= dp[i][j - 1]) {
217
+ }
218
+ else if ((dp[i - 1]?.[j] ?? 0) <= (dp[i]?.[j - 1] ?? 0)) {
198
219
  // Deletion from s1
199
220
  i--
200
- } else {
221
+ }
222
+ else {
201
223
  // Insertion into s1
202
224
  j--
203
225
  }
204
226
  }
205
227
 
206
- return { distance: dp[m][n], matchedIndices }
228
+ return { distance: dp[m]?.[n] ?? 0, matchedIndices }
207
229
  }
208
230
 
209
231
  /**
@@ -213,7 +235,7 @@ function calculateFuzzyScore(
213
235
  editDistance: number,
214
236
  queryLength: number,
215
237
  matchStart: number,
216
- targetLength: number
238
+ targetLength: number,
217
239
  ): number {
218
240
  // Base score from edit distance (0.5 - 0.7 range for fuzzy matches)
219
241
  const distanceScore = Math.max(0, 1 - editDistance / queryLength) * 0.3 + 0.4
@@ -230,22 +252,30 @@ function calculateFuzzyScore(
230
252
  /**
231
253
  * Convert matched indices to Range array for highlighting
232
254
  */
233
- export function indicesToRanges(indices: number[]): Array<{ start: number; end: number }> {
234
- if (!indices.length) return []
255
+ export function indicesToRanges(indices: number[]): Array<{ start: number, end: number }> {
256
+ if (!indices.length)
257
+ return []
235
258
 
236
259
  const sorted = Array.from(new Set(indices)).sort((a, b) => a - b)
237
- const ranges: Array<{ start: number; end: number }> = []
260
+ const ranges: Array<{ start: number, end: number }> = []
238
261
 
239
- let start = sorted[0]
240
- let end = sorted[0] + 1
262
+ const first = sorted[0]
263
+ if (first === undefined)
264
+ return []
265
+ let start = first
266
+ let end = first + 1
241
267
 
242
268
  for (let i = 1; i < sorted.length; i++) {
243
- if (sorted[i] === end) {
269
+ const current = sorted[i]
270
+ if (current === undefined)
271
+ continue
272
+ if (current === end) {
244
273
  end++
245
- } else {
274
+ }
275
+ else {
246
276
  ranges.push({ start, end })
247
- start = sorted[i]
248
- end = sorted[i] + 1
277
+ start = current
278
+ end = current + 1
249
279
  }
250
280
  }
251
281
  ranges.push({ start, end })
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Search utilities and types
3
+ *
4
+ * @module search
5
+ */
6
+
7
+ export * from './feature-matcher'
8
+ export * from './fuzzy-match'
9
+ export * from './levenshtein-utils'
10
+ export * from './types'
@@ -11,29 +11,35 @@ export function levenshteinDistance(s1: string, s2: string): number {
11
11
  const n = s2.length
12
12
 
13
13
  // Create a 2D array (m+1)x(n+1) to store distances
14
- const dp: number[][] = new Array(m + 1)
15
- .fill(0)
16
- .map(() => new Array(n + 1).fill(0))
14
+ const dp: number[][] = Array.from({ length: m + 1 }, () => Array.from({ length: n + 1 }, () => 0))
17
15
 
18
16
  // Initialize the DP table
19
17
  for (let i = 0; i <= m; i++) {
20
- dp[i][0] = i
18
+ const row = dp[i]
19
+ if (row)
20
+ row[0] = i
21
21
  }
22
- for (let j = 0; j <= n; j++) {
23
- dp[0][j] = j
22
+ const firstRow = dp[0]
23
+ if (firstRow) {
24
+ for (let j = 0; j <= n; j++)
25
+ firstRow[j] = j
24
26
  }
25
27
 
26
28
  // Fill the DP table
27
29
  for (let i = 1; i <= m; i++) {
28
30
  for (let j = 1; j <= n; j++) {
29
31
  const cost = s1[i - 1] === s2[j - 1] ? 0 : 1
30
- dp[i][j] = Math.min(
31
- dp[i - 1][j] + 1, // Deletion
32
- dp[i][j - 1] + 1, // Insertion
33
- dp[i - 1][j - 1] + cost, // Substitution
32
+ const row = dp[i]
33
+ const prevRow = dp[i - 1]
34
+ if (!row || !prevRow)
35
+ continue
36
+ row[j] = Math.min(
37
+ (prevRow[j] ?? 0) + 1, // Deletion
38
+ (row[j - 1] ?? 0) + 1, // Insertion
39
+ (prevRow[j - 1] ?? 0) + cost, // Substitution
34
40
  )
35
41
  }
36
42
  }
37
43
 
38
- return dp[m][n]
44
+ return dp[m]?.[n] ?? 0
39
45
  }