@talex-touch/utils 1.0.40 → 1.0.44

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 (235) 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 +97 -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 +21 -3
  72. package/market/index.ts +1 -1
  73. package/market/types.ts +20 -5
  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 +82 -8
  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 -0
  87. package/plugin/providers/market-client.ts +218 -0
  88. package/plugin/providers/npm-provider.ts +228 -0
  89. package/plugin/providers/tpex-provider.ts +297 -0
  90. package/plugin/providers/tpex-types.ts +34 -0
  91. package/plugin/sdk/box-items.ts +14 -0
  92. package/plugin/sdk/box-sdk.ts +64 -0
  93. package/plugin/sdk/channel.ts +119 -4
  94. package/plugin/sdk/clipboard.ts +26 -12
  95. package/plugin/sdk/cloud-sync.ts +113 -0
  96. package/plugin/sdk/common.ts +19 -11
  97. package/plugin/sdk/core-box.ts +6 -15
  98. package/plugin/sdk/division-box.ts +160 -65
  99. package/plugin/sdk/examples/storage-onDidChange-example.js +5 -2
  100. package/plugin/sdk/feature-sdk.ts +111 -76
  101. package/plugin/sdk/flow.ts +146 -45
  102. package/plugin/sdk/hooks/bridge.ts +113 -49
  103. package/plugin/sdk/hooks/life-cycle.ts +35 -16
  104. package/plugin/sdk/index.ts +14 -3
  105. package/plugin/sdk/intelligence.ts +87 -0
  106. package/plugin/sdk/meta/README.md +179 -0
  107. package/plugin/sdk/meta-sdk.ts +244 -0
  108. package/plugin/sdk/notification.ts +9 -0
  109. package/plugin/sdk/performance.ts +1 -16
  110. package/plugin/sdk/plugin-info.ts +64 -0
  111. package/plugin/sdk/power.ts +155 -0
  112. package/plugin/sdk/recommend.ts +21 -0
  113. package/plugin/sdk/service/index.ts +12 -8
  114. package/plugin/sdk/sqlite.ts +141 -0
  115. package/plugin/sdk/storage.ts +2 -6
  116. package/plugin/sdk/system.ts +2 -9
  117. package/plugin/sdk/temp-files.ts +41 -0
  118. package/plugin/sdk/touch-sdk.ts +18 -0
  119. package/plugin/sdk/types.ts +44 -4
  120. package/plugin/sdk/window/index.ts +12 -9
  121. package/plugin/sdk-version.ts +231 -0
  122. package/preload/renderer.ts +3 -2
  123. package/renderer/hooks/arg-mapper.ts +34 -6
  124. package/renderer/hooks/index.ts +13 -0
  125. package/renderer/hooks/initialize.ts +2 -1
  126. package/renderer/hooks/use-agent-market-sdk.ts +7 -0
  127. package/renderer/hooks/use-agent-market.ts +106 -0
  128. package/renderer/hooks/use-agents-sdk.ts +7 -0
  129. package/renderer/hooks/use-app-sdk.ts +7 -0
  130. package/renderer/hooks/use-channel.ts +33 -4
  131. package/renderer/hooks/use-download-sdk.ts +21 -0
  132. package/renderer/hooks/use-intelligence-sdk.ts +7 -0
  133. package/renderer/hooks/use-intelligence-stats.ts +290 -0
  134. package/renderer/hooks/use-intelligence.ts +202 -104
  135. package/renderer/hooks/use-market-sdk.ts +16 -0
  136. package/renderer/hooks/use-notification-sdk.ts +7 -0
  137. package/renderer/hooks/use-permission-sdk.ts +7 -0
  138. package/renderer/hooks/use-permission.ts +325 -0
  139. package/renderer/hooks/use-platform-sdk.ts +7 -0
  140. package/renderer/hooks/use-plugin-sdk.ts +16 -0
  141. package/renderer/hooks/use-settings-sdk.ts +7 -0
  142. package/renderer/hooks/use-update-sdk.ts +21 -0
  143. package/renderer/index.ts +1 -0
  144. package/renderer/ref.ts +19 -10
  145. package/renderer/shared/components/SharedPluginDetailContent.vue +84 -0
  146. package/renderer/shared/components/SharedPluginDetailHeader.vue +116 -0
  147. package/renderer/shared/components/SharedPluginDetailMetaList.vue +39 -0
  148. package/renderer/shared/components/SharedPluginDetailReadme.vue +45 -0
  149. package/renderer/shared/components/SharedPluginDetailVersions.vue +98 -0
  150. package/renderer/shared/components/index.ts +5 -0
  151. package/renderer/shared/components/shims-vue.d.ts +5 -0
  152. package/renderer/shared/index.ts +2 -0
  153. package/renderer/shared/plugin-detail.ts +62 -0
  154. package/renderer/storage/app-settings.ts +3 -1
  155. package/renderer/storage/base-storage.ts +508 -82
  156. package/renderer/storage/intelligence-storage.ts +37 -46
  157. package/renderer/storage/openers.ts +3 -1
  158. package/renderer/storage/storage-subscription.ts +126 -42
  159. package/renderer/touch-sdk/env.ts +10 -10
  160. package/renderer/touch-sdk/index.ts +114 -18
  161. package/renderer/touch-sdk/terminal.ts +24 -13
  162. package/search/feature-matcher.ts +279 -0
  163. package/search/fuzzy-match.ts +64 -34
  164. package/search/index.ts +10 -0
  165. package/search/levenshtein-utils.ts +17 -11
  166. package/transport/errors.ts +310 -0
  167. package/transport/event/builder.ts +378 -0
  168. package/transport/event/index.ts +7 -0
  169. package/transport/event/types.ts +292 -0
  170. package/transport/events/index.ts +2670 -0
  171. package/transport/events/meta-overlay.ts +79 -0
  172. package/transport/events/types/agents.ts +177 -0
  173. package/transport/events/types/app-index.ts +9 -0
  174. package/transport/events/types/app.ts +475 -0
  175. package/transport/events/types/box-item.ts +222 -0
  176. package/transport/events/types/clipboard.ts +80 -0
  177. package/transport/events/types/core-box.ts +534 -0
  178. package/transport/events/types/device-idle.ts +7 -0
  179. package/transport/events/types/division-box.ts +99 -0
  180. package/transport/events/types/download.ts +115 -0
  181. package/transport/events/types/file-index.ts +73 -0
  182. package/transport/events/types/flow.ts +149 -0
  183. package/transport/events/types/index.ts +70 -0
  184. package/transport/events/types/market.ts +39 -0
  185. package/transport/events/types/meta-overlay.ts +184 -0
  186. package/transport/events/types/notification.ts +140 -0
  187. package/transport/events/types/permission.ts +90 -0
  188. package/transport/events/types/platform.ts +8 -0
  189. package/transport/events/types/plugin.ts +620 -0
  190. package/transport/events/types/sentry.ts +20 -0
  191. package/transport/events/types/storage.ts +208 -0
  192. package/transport/events/types/transport.ts +60 -0
  193. package/transport/events/types/tray.ts +16 -0
  194. package/transport/events/types/update.ts +78 -0
  195. package/transport/index.ts +139 -0
  196. package/transport/main.ts +2 -0
  197. package/transport/sdk/constants.ts +29 -0
  198. package/transport/sdk/domains/agents-market.ts +47 -0
  199. package/transport/sdk/domains/agents.ts +62 -0
  200. package/transport/sdk/domains/app.ts +48 -0
  201. package/transport/sdk/domains/disposable.ts +35 -0
  202. package/transport/sdk/domains/download.ts +139 -0
  203. package/transport/sdk/domains/index.ts +13 -0
  204. package/transport/sdk/domains/intelligence.ts +616 -0
  205. package/transport/sdk/domains/market.ts +35 -0
  206. package/transport/sdk/domains/notification.ts +62 -0
  207. package/transport/sdk/domains/permission.ts +85 -0
  208. package/transport/sdk/domains/platform.ts +19 -0
  209. package/transport/sdk/domains/plugin.ts +144 -0
  210. package/transport/sdk/domains/settings.ts +92 -0
  211. package/transport/sdk/domains/update.ts +64 -0
  212. package/transport/sdk/index.ts +60 -0
  213. package/transport/sdk/main-transport.ts +710 -0
  214. package/transport/sdk/main.ts +9 -0
  215. package/transport/sdk/plugin-transport.ts +654 -0
  216. package/transport/sdk/port-policy.ts +38 -0
  217. package/transport/sdk/renderer-transport.ts +1165 -0
  218. package/transport/types.ts +605 -0
  219. package/types/agent.ts +399 -0
  220. package/types/cloud-sync.ts +157 -0
  221. package/types/division-box.ts +47 -27
  222. package/types/download.ts +1 -0
  223. package/types/flow.ts +63 -12
  224. package/types/icon.ts +2 -1
  225. package/types/index.ts +5 -0
  226. package/types/intelligence.ts +1492 -81
  227. package/types/modules/base.ts +2 -0
  228. package/types/path-browserify.d.ts +5 -0
  229. package/types/platform.ts +12 -0
  230. package/types/startup-info.ts +32 -0
  231. package/types/touch-app-core.ts +8 -8
  232. package/types/update.ts +94 -1
  233. package/vitest.config.ts +25 -0
  234. package/auth/useClerkConfig.ts +0 -40
  235. package/auth/useClerkProvider.ts +0 -52
@@ -0,0 +1,228 @@
1
+ import type { IManifest } from '..'
2
+ import type {
3
+ PluginInstallRequest,
4
+ PluginInstallResult,
5
+ PluginProvider,
6
+ PluginProviderContext,
7
+ } from './types'
8
+ import { Buffer } from 'node:buffer'
9
+ import process from 'node:process'
10
+ import { PluginProviderType } from './types'
11
+
12
+ const NPM_REGISTRY = 'https://registry.npmjs.org'
13
+ const TUFF_PLUGIN_PREFIX = 'tuff-plugin-'
14
+ const TUFF_PLUGIN_SCOPE = '@tuff/'
15
+
16
+ export interface NpmPackageInfo {
17
+ name: string
18
+ version: string
19
+ description?: string
20
+ author?: string | { name: string, email?: string }
21
+ main?: string
22
+ keywords?: string[]
23
+ dist: {
24
+ tarball: string
25
+ shasum: string
26
+ integrity?: string
27
+ }
28
+ tuff?: {
29
+ icon?: string
30
+ activationKeywords?: string[]
31
+ }
32
+ }
33
+
34
+ export interface NpmPackageVersions {
35
+ 'name': string
36
+ 'dist-tags': {
37
+ latest: string
38
+ [tag: string]: string
39
+ }
40
+ 'versions': Record<string, NpmPackageInfo>
41
+ }
42
+
43
+ /**
44
+ * Parse NPM source string to extract package name and optional version
45
+ * Formats:
46
+ * - "npm:package-name"
47
+ * - "npm:package-name@version"
48
+ * - "npm:@scope/package-name"
49
+ * - "npm:@scope/package-name@version"
50
+ * - "tuff-plugin-xxx" (when hintType is NPM)
51
+ * - "@tuff/xxx" (when hintType is NPM)
52
+ */
53
+ function parseNpmSource(source: string): { packageName: string, version?: string } | null {
54
+ const npmMatch = source.match(/^npm:(@?[a-z0-9][\w\-.]*(?:\/[a-z0-9][\w\-.]*)?)(?:@(.+))?$/i)
55
+ if (npmMatch) {
56
+ const packageName = npmMatch[1]
57
+ if (!packageName)
58
+ return null
59
+ const version = npmMatch[2]
60
+ return version ? { packageName, version } : { packageName }
61
+ }
62
+
63
+ const scopedMatch = source.match(/^(@tuff\/[a-z0-9][\w\-.]*)(?:@(.+))?$/i)
64
+ if (scopedMatch) {
65
+ const packageName = scopedMatch[1]
66
+ if (!packageName)
67
+ return null
68
+ const version = scopedMatch[2]
69
+ return version ? { packageName, version } : { packageName }
70
+ }
71
+
72
+ const prefixMatch = source.match(/^(tuff-plugin-[a-z0-9][\w\-.]*)(?:@(.+))?$/i)
73
+ if (prefixMatch) {
74
+ const packageName = prefixMatch[1]
75
+ if (!packageName)
76
+ return null
77
+ const version = prefixMatch[2]
78
+ return version ? { packageName, version } : { packageName }
79
+ }
80
+
81
+ return null
82
+ }
83
+
84
+ /**
85
+ * Check if a package name looks like a Tuff plugin
86
+ */
87
+ function isTuffPluginPackage(name: string): boolean {
88
+ return name.startsWith(TUFF_PLUGIN_PREFIX) || name.startsWith(TUFF_PLUGIN_SCOPE)
89
+ }
90
+
91
+ export class NpmProvider implements PluginProvider {
92
+ readonly type = PluginProviderType.NPM
93
+ private registry: string
94
+
95
+ constructor(registry: string = NPM_REGISTRY) {
96
+ this.registry = registry.replace(/\/$/, '')
97
+ }
98
+
99
+ canHandle(request: PluginInstallRequest): boolean {
100
+ if (request.hintType === PluginProviderType.NPM) {
101
+ return parseNpmSource(request.source) !== null
102
+ }
103
+
104
+ if (request.source.startsWith('npm:')) {
105
+ return true
106
+ }
107
+
108
+ const parsed = parseNpmSource(request.source)
109
+ return parsed !== null && isTuffPluginPackage(parsed.packageName)
110
+ }
111
+
112
+ async install(
113
+ request: PluginInstallRequest,
114
+ context?: PluginProviderContext,
115
+ ): Promise<PluginInstallResult> {
116
+ const parsed = parseNpmSource(request.source)
117
+ if (!parsed) {
118
+ throw new Error(`Invalid NPM source format: ${request.source}`)
119
+ }
120
+
121
+ const { packageName, version } = parsed
122
+ const packageInfo = await this.getPackageInfo(packageName, version)
123
+
124
+ if (!packageInfo) {
125
+ throw new Error(`Package not found: ${packageName}`)
126
+ }
127
+
128
+ const tarballUrl = packageInfo.dist.tarball
129
+ const downloadRes = await fetch(tarballUrl)
130
+ if (!downloadRes.ok) {
131
+ throw new Error(`Failed to download package: ${downloadRes.statusText}`)
132
+ }
133
+
134
+ const arrayBuffer = await downloadRes.arrayBuffer()
135
+ const tempDir = context?.tempDir ?? '/tmp'
136
+ const safePackageName = packageName.replace(/[@/]/g, '-')
137
+ const fileName = `${safePackageName}-${packageInfo.version}.tgz`
138
+ const filePath = `${tempDir}/${fileName}`
139
+
140
+ if (typeof process !== 'undefined') {
141
+ const fs = await import('node:fs/promises')
142
+ await fs.writeFile(filePath, Buffer.from(arrayBuffer))
143
+ }
144
+
145
+ const authorName = typeof packageInfo.author === 'string'
146
+ ? packageInfo.author
147
+ : packageInfo.author?.name ?? 'Unknown'
148
+
149
+ const manifest: IManifest = {
150
+ id: packageInfo.name,
151
+ name: packageInfo.name.replace(TUFF_PLUGIN_PREFIX, '').replace(TUFF_PLUGIN_SCOPE, ''),
152
+ version: packageInfo.version,
153
+ description: packageInfo.description ?? '',
154
+ author: authorName,
155
+ main: packageInfo.main ?? 'index.js',
156
+ icon: packageInfo.tuff?.icon,
157
+ activationKeywords: packageInfo.tuff?.activationKeywords ?? packageInfo.keywords,
158
+ }
159
+
160
+ return {
161
+ provider: PluginProviderType.NPM,
162
+ filePath,
163
+ official: packageName.startsWith(TUFF_PLUGIN_SCOPE),
164
+ manifest,
165
+ metadata: {
166
+ packageName: packageInfo.name,
167
+ version: packageInfo.version,
168
+ tarball: tarballUrl,
169
+ shasum: packageInfo.dist.shasum,
170
+ integrity: packageInfo.dist.integrity,
171
+ },
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Get package info from npm registry
177
+ */
178
+ async getPackageInfo(packageName: string, version?: string): Promise<NpmPackageInfo | null> {
179
+ const encodedName = encodeURIComponent(packageName).replace('%40', '@')
180
+ const res = await fetch(`${this.registry}/${encodedName}`)
181
+
182
+ if (!res.ok) {
183
+ if (res.status === 404)
184
+ return null
185
+ throw new Error(`Failed to fetch package info: ${res.statusText}`)
186
+ }
187
+
188
+ const data: NpmPackageVersions = await res.json()
189
+ const targetVersion = version ?? data['dist-tags'].latest
190
+
191
+ return data.versions[targetVersion] ?? null
192
+ }
193
+
194
+ /**
195
+ * Search for Tuff plugins on npm
196
+ */
197
+ async searchPlugins(keyword?: string): Promise<NpmPackageInfo[]> {
198
+ const searchTerms = [
199
+ `keywords:tuff-plugin`,
200
+ keyword ? `${keyword}` : '',
201
+ ].filter(Boolean).join('+')
202
+
203
+ const res = await fetch(
204
+ `${this.registry}/-/v1/search?text=${encodeURIComponent(searchTerms)}&size=100`,
205
+ )
206
+
207
+ if (!res.ok) {
208
+ throw new Error(`Failed to search packages: ${res.statusText}`)
209
+ }
210
+
211
+ const data = await res.json() as {
212
+ objects: Array<{ package: NpmPackageInfo }>
213
+ }
214
+
215
+ return data.objects
216
+ .map(obj => obj.package)
217
+ .filter(pkg => isTuffPluginPackage(pkg.name))
218
+ }
219
+
220
+ /**
221
+ * List all available Tuff plugins from npm
222
+ */
223
+ async listPlugins(): Promise<NpmPackageInfo[]> {
224
+ return this.searchPlugins()
225
+ }
226
+ }
227
+
228
+ export const npmProvider = new NpmProvider()
@@ -0,0 +1,297 @@
1
+ import type { IManifest } from '..'
2
+ import type {
3
+ PluginInstallRequest,
4
+ PluginInstallResult,
5
+ PluginProvider,
6
+ PluginProviderContext,
7
+ } from './types'
8
+ import { Buffer } from 'node:buffer'
9
+ import process from 'node:process'
10
+ import { NEXUS_BASE_URL } from '../../env'
11
+ import { PluginProviderType } from './types'
12
+
13
+ const DEFAULT_TPEX_API = NEXUS_BASE_URL
14
+
15
+ /**
16
+ * Check if source is a .tpex file path or URL
17
+ */
18
+ function isTpexFile(source: string): boolean {
19
+ return source.trim().toLowerCase().endsWith('.tpex')
20
+ }
21
+
22
+ /**
23
+ * Check if source is a remote URL
24
+ */
25
+ function isRemoteUrl(source: string): boolean {
26
+ return /^https?:\/\//i.test(source)
27
+ }
28
+
29
+ export interface TpexPluginInfo {
30
+ id: string
31
+ slug: string
32
+ name: string
33
+ summary: string
34
+ category: string
35
+ installs: number
36
+ homepage?: string | null
37
+ isOfficial: boolean
38
+ badges: string[]
39
+ author?: { name: string, avatarColor?: string } | null
40
+ iconUrl?: string | null
41
+ latestVersion?: {
42
+ id: string
43
+ version: string
44
+ channel: string
45
+ packageUrl: string
46
+ packageSize: number
47
+ signature?: string
48
+ manifest?: Record<string, unknown> | null
49
+ changelog?: string | null
50
+ }
51
+ }
52
+
53
+ export interface TpexListResponse {
54
+ plugins: TpexPluginInfo[]
55
+ total: number
56
+ }
57
+
58
+ export interface TpexDetailResponse {
59
+ plugin: TpexPluginInfo & {
60
+ versions?: Array<{
61
+ id: string
62
+ version: string
63
+ channel: string
64
+ packageUrl: string
65
+ packageSize: number
66
+ signature?: string
67
+ manifest?: Record<string, unknown> | null
68
+ changelog?: string | null
69
+ }>
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Parse TPEX source string to extract slug and optional version
75
+ * Formats: "tpex:slug", "tpex:slug@version", "slug" (when hintType is TPEX)
76
+ */
77
+ function parseTpexSource(source: string): { slug: string, version?: string } | null {
78
+ const tpexMatch = source.match(/^tpex:([a-z0-9][\w\-.]{1,62}[a-z0-9])(?:@(.+))?$/i)
79
+ if (tpexMatch) {
80
+ const slug = tpexMatch[1]
81
+ if (!slug)
82
+ return null
83
+ const version = tpexMatch[2]
84
+ return version ? { slug, version } : { slug }
85
+ }
86
+
87
+ const slugMatch = source.match(/^([a-z0-9][\w\-.]{1,62}[a-z0-9])(?:@(.+))?$/i)
88
+ if (slugMatch) {
89
+ const slug = slugMatch[1]
90
+ if (!slug)
91
+ return null
92
+ const version = slugMatch[2]
93
+ return version ? { slug, version } : { slug }
94
+ }
95
+
96
+ return null
97
+ }
98
+
99
+ export class TpexProvider implements PluginProvider {
100
+ readonly type = PluginProviderType.TPEX
101
+ private apiBase: string
102
+
103
+ constructor(apiBase: string = DEFAULT_TPEX_API) {
104
+ this.apiBase = apiBase.replace(/\/$/, '')
105
+ }
106
+
107
+ canHandle(request: PluginInstallRequest): boolean {
108
+ // Handle .tpex file paths (local or remote URL)
109
+ if (isTpexFile(request.source)) {
110
+ return true
111
+ }
112
+
113
+ if (request.hintType === PluginProviderType.TPEX) {
114
+ return parseTpexSource(request.source) !== null
115
+ }
116
+ return request.source.startsWith('tpex:')
117
+ }
118
+
119
+ async install(
120
+ request: PluginInstallRequest,
121
+ context?: PluginProviderContext,
122
+ ): Promise<PluginInstallResult> {
123
+ // Handle .tpex file directly (local path or remote URL)
124
+ if (isTpexFile(request.source)) {
125
+ return this.installFromFile(request, context)
126
+ }
127
+
128
+ // Handle tpex:slug format - fetch from API
129
+ return this.installFromRegistry(request, context)
130
+ }
131
+
132
+ /**
133
+ * Install from a .tpex file (local path or remote URL)
134
+ */
135
+ private async installFromFile(
136
+ request: PluginInstallRequest,
137
+ context?: PluginProviderContext,
138
+ ): Promise<PluginInstallResult> {
139
+ let filePath = request.source
140
+ let arrayBuffer: ArrayBuffer | undefined
141
+
142
+ if (isRemoteUrl(request.source)) {
143
+ // Download remote .tpex file
144
+ const downloadRes = await fetch(request.source)
145
+ if (!downloadRes.ok) {
146
+ throw new Error(`Failed to download TPEX file: ${downloadRes.statusText}`)
147
+ }
148
+
149
+ arrayBuffer = await downloadRes.arrayBuffer()
150
+ const tempDir = context?.tempDir ?? '/tmp'
151
+ const fileName = `tpex-${Date.now()}.tpex`
152
+ filePath = `${tempDir}/${fileName}`
153
+
154
+ if (typeof process !== 'undefined') {
155
+ const fs = await import('node:fs/promises')
156
+ await fs.writeFile(filePath, Buffer.from(arrayBuffer))
157
+ }
158
+ }
159
+
160
+ // For local files, just return the path - manifest extraction happens in core-app
161
+ return {
162
+ provider: PluginProviderType.TPEX,
163
+ filePath,
164
+ official: false,
165
+ metadata: {
166
+ sourceType: 'file',
167
+ originalSource: request.source,
168
+ },
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Install from TPEX registry (tpex:slug format)
174
+ */
175
+ private async installFromRegistry(
176
+ request: PluginInstallRequest,
177
+ context?: PluginProviderContext,
178
+ ): Promise<PluginInstallResult> {
179
+ const parsed = parseTpexSource(request.source)
180
+ if (!parsed) {
181
+ throw new Error(`Invalid TPEX source format: ${request.source}`)
182
+ }
183
+
184
+ const { slug, version } = parsed
185
+
186
+ const detailRes = await fetch(`${this.apiBase}/api/market/plugins/${slug}`)
187
+ if (!detailRes.ok) {
188
+ if (detailRes.status === 404) {
189
+ throw new Error(`Plugin not found: ${slug}`)
190
+ }
191
+ throw new Error(`Failed to fetch plugin details: ${detailRes.statusText}`)
192
+ }
193
+
194
+ const detail: TpexDetailResponse = await detailRes.json()
195
+ const plugin = detail.plugin
196
+
197
+ let targetVersion = plugin.latestVersion
198
+ if (version && plugin.versions) {
199
+ targetVersion = plugin.versions.find(v => v.version === version) ?? targetVersion
200
+ }
201
+
202
+ if (!targetVersion?.packageUrl) {
203
+ throw new Error(`No downloadable version found for plugin: ${slug}`)
204
+ }
205
+
206
+ const downloadUrl = targetVersion.packageUrl.startsWith('http')
207
+ ? targetVersion.packageUrl
208
+ : `${this.apiBase}${targetVersion.packageUrl}`
209
+
210
+ const downloadRes = await fetch(downloadUrl)
211
+ if (!downloadRes.ok) {
212
+ throw new Error(`Failed to download plugin package: ${downloadRes.statusText}`)
213
+ }
214
+
215
+ const arrayBuffer = await downloadRes.arrayBuffer()
216
+ const tempDir = context?.tempDir ?? '/tmp'
217
+ const fileName = `${slug}-${targetVersion.version}.tpex`
218
+ const filePath = `${tempDir}/${fileName}`
219
+
220
+ if (typeof process !== 'undefined') {
221
+ const fs = await import('node:fs/promises')
222
+ await fs.writeFile(filePath, Buffer.from(arrayBuffer))
223
+ }
224
+
225
+ const manifest: IManifest | undefined = targetVersion.manifest
226
+ ? {
227
+ id: plugin.slug,
228
+ name: plugin.name,
229
+ version: targetVersion.version,
230
+ description: plugin.summary,
231
+ author: plugin.author?.name ?? 'Unknown',
232
+ main: (targetVersion.manifest as Record<string, unknown>).main as string ?? 'index.js',
233
+ icon: plugin.iconUrl ?? undefined,
234
+ ...targetVersion.manifest,
235
+ }
236
+ : undefined
237
+
238
+ return {
239
+ provider: PluginProviderType.TPEX,
240
+ filePath,
241
+ official: plugin.isOfficial,
242
+ manifest,
243
+ metadata: {
244
+ sourceType: 'registry',
245
+ slug: plugin.slug,
246
+ version: targetVersion.version,
247
+ channel: targetVersion.channel,
248
+ packageSize: targetVersion.packageSize,
249
+ installs: plugin.installs,
250
+ },
251
+ }
252
+ }
253
+
254
+ /**
255
+ * List all available plugins from TPEX registry
256
+ */
257
+ async listPlugins(): Promise<TpexPluginInfo[]> {
258
+ const res = await fetch(`${this.apiBase}/api/market/plugins`)
259
+ if (!res.ok) {
260
+ throw new Error(`Failed to fetch plugin list: ${res.statusText}`)
261
+ }
262
+
263
+ const data: TpexListResponse = await res.json()
264
+ return data.plugins
265
+ }
266
+
267
+ /**
268
+ * Get plugin details by slug
269
+ */
270
+ async getPlugin(slug: string): Promise<TpexDetailResponse['plugin'] | null> {
271
+ const res = await fetch(`${this.apiBase}/api/market/plugins/${slug}`)
272
+ if (!res.ok) {
273
+ if (res.status === 404)
274
+ return null
275
+ throw new Error(`Failed to fetch plugin: ${res.statusText}`)
276
+ }
277
+
278
+ const data: TpexDetailResponse = await res.json()
279
+ return data.plugin
280
+ }
281
+
282
+ /**
283
+ * Search plugins by keyword
284
+ */
285
+ async searchPlugins(keyword: string): Promise<TpexPluginInfo[]> {
286
+ const plugins = await this.listPlugins()
287
+ const lowerKeyword = keyword.toLowerCase()
288
+
289
+ return plugins.filter(plugin =>
290
+ plugin.name.toLowerCase().includes(lowerKeyword)
291
+ || plugin.slug.toLowerCase().includes(lowerKeyword)
292
+ || plugin.summary.toLowerCase().includes(lowerKeyword),
293
+ )
294
+ }
295
+ }
296
+
297
+ export const tpexProvider = new TpexProvider()
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Metadata extracted from a .tpex package file
3
+ */
4
+ export interface TpexMetadata {
5
+ readmeMarkdown?: string | null
6
+ manifest?: Record<string, unknown> | null
7
+ }
8
+
9
+ /**
10
+ * Result of package preview operation
11
+ */
12
+ export interface TpexPackagePreviewResult {
13
+ manifest: Record<string, unknown> | null
14
+ readmeMarkdown: string | null
15
+ }
16
+
17
+ /**
18
+ * Extracted manifest fields from tpex package
19
+ */
20
+ export interface TpexExtractedManifest {
21
+ id?: string
22
+ name?: string
23
+ description?: string
24
+ version?: string
25
+ homepage?: string
26
+ changelog?: string
27
+ channel?: string
28
+ category?: string
29
+ icon?: {
30
+ type?: string
31
+ value?: string
32
+ }
33
+ [key: string]: unknown
34
+ }
@@ -0,0 +1,14 @@
1
+ import { hasWindow } from '../../env'
2
+
3
+ export type BoxItemsAPI = Record<string, any>
4
+
5
+ const DEFAULT_BOX_ITEMS_ERROR
6
+ = '[Feature SDK] boxItems API not available. Make sure this is called in a plugin context.'
7
+
8
+ export function useBoxItems(errorMessage = DEFAULT_BOX_ITEMS_ERROR): BoxItemsAPI {
9
+ const boxItemsApi = hasWindow() ? (window as any)?.$boxItems as BoxItemsAPI | undefined : undefined
10
+ if (!boxItemsApi) {
11
+ throw new Error(errorMessage)
12
+ }
13
+ return boxItemsApi
14
+ }
@@ -184,6 +184,39 @@ export interface BoxSDK {
184
184
  * ```
185
185
  */
186
186
  allowClipboard: (types: number) => Promise<void>
187
+
188
+ /**
189
+ * Set CoreBox window to a specific height (60-800px)
190
+ *
191
+ * @param height - Target height in pixels (will be clamped to 60-800)
192
+ *
193
+ * @example
194
+ * ```typescript
195
+ * // Set to custom height
196
+ * await plugin.box.setHeight(400)
197
+ * ```
198
+ */
199
+ setHeight: (height: number) => Promise<void>
200
+
201
+ /**
202
+ * Set CoreBox vertical position as percentage from top (0.1-0.9)
203
+ *
204
+ * @param topPercent - Position from top (0.1 = 10%, 0.5 = 50%, etc.)
205
+ *
206
+ * @example
207
+ * ```typescript
208
+ * // Position at 40% from top
209
+ * await plugin.box.setPositionOffset(0.4)
210
+ * ```
211
+ */
212
+ setPositionOffset: (topPercent: number) => Promise<void>
213
+
214
+ /**
215
+ * Get current CoreBox window bounds
216
+ *
217
+ * @returns Window bounds { x, y, width, height }
218
+ */
219
+ getBounds: () => Promise<{ x: number, y: number, width: number, height: number }>
187
220
  }
188
221
 
189
222
  /**
@@ -298,6 +331,37 @@ export function createBoxSDK(channel: ITouchClientChannel): BoxSDK {
298
331
  throw error
299
332
  }
300
333
  },
334
+
335
+ async setHeight(height: number): Promise<void> {
336
+ try {
337
+ await channel.send('core-box:set-height', { height })
338
+ }
339
+ catch (error) {
340
+ console.error('[Box SDK] Failed to set height:', error)
341
+ throw error
342
+ }
343
+ },
344
+
345
+ async setPositionOffset(topPercent: number): Promise<void> {
346
+ try {
347
+ await channel.send('core-box:set-position-offset', { topPercent })
348
+ }
349
+ catch (error) {
350
+ console.error('[Box SDK] Failed to set position offset:', error)
351
+ throw error
352
+ }
353
+ },
354
+
355
+ async getBounds(): Promise<{ x: number, y: number, width: number, height: number }> {
356
+ try {
357
+ const result = await channel.send('core-box:get-bounds')
358
+ return result.bounds
359
+ }
360
+ catch (error) {
361
+ console.error('[Box SDK] Failed to get bounds:', error)
362
+ throw error
363
+ }
364
+ },
301
365
  }
302
366
  }
303
367