@talex-touch/utils 1.0.40 → 1.0.42
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/core-box/tuff/tuff-dsl.ts +65 -0
- package/intelligence/client.ts +8 -8
- package/market/constants.ts +14 -0
- package/market/types.ts +1 -1
- package/package.json +1 -1
- package/plugin/index.ts +2 -1
- package/plugin/providers/index.ts +4 -0
- package/plugin/providers/market-client.ts +215 -0
- package/plugin/providers/npm-provider.ts +213 -0
- package/plugin/providers/tpex-provider.ts +283 -0
- package/plugin/providers/tpex-types.ts +34 -0
- package/plugin/sdk/hooks/bridge.ts +105 -48
- package/plugin/sdk/performance.ts +1 -16
- package/renderer/hooks/arg-mapper.ts +20 -6
- package/renderer/hooks/use-intelligence.ts +291 -34
- package/renderer/storage/intelligence-storage.ts +9 -9
- package/types/division-box.ts +20 -0
- package/types/intelligence.ts +1496 -78
|
@@ -803,6 +803,65 @@ export interface TuffContext {
|
|
|
803
803
|
tags?: string[]
|
|
804
804
|
}
|
|
805
805
|
|
|
806
|
+
// ==================== Footer Hints 配置 ====================
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Footer hint 单项配置
|
|
810
|
+
*/
|
|
811
|
+
export interface TuffFooterHintItem {
|
|
812
|
+
/** 快捷键显示文本 */
|
|
813
|
+
key: string
|
|
814
|
+
/** 提示标签 */
|
|
815
|
+
label: string
|
|
816
|
+
/** 是否显示此提示 */
|
|
817
|
+
visible?: boolean
|
|
818
|
+
/** 触发的事件名称 */
|
|
819
|
+
event?: string
|
|
820
|
+
/** 事件携带的数据 */
|
|
821
|
+
eventData?: Record<string, unknown>
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Footer hints 配置
|
|
826
|
+
* @description 控制 CoreBox 底部快捷键提示的显示和行为
|
|
827
|
+
*/
|
|
828
|
+
export interface TuffFooterHints {
|
|
829
|
+
/**
|
|
830
|
+
* 主操作提示(回车键)
|
|
831
|
+
* @description 自定义回车键的文案,如 "发送"、"执行"、"打开" 等
|
|
832
|
+
*/
|
|
833
|
+
primary?: {
|
|
834
|
+
/** 自定义标签文案 */
|
|
835
|
+
label?: string
|
|
836
|
+
/** 是否显示 */
|
|
837
|
+
visible?: boolean
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* 辅助操作提示(Meta+K)
|
|
842
|
+
* @description 控制 Meta+K 快捷键的显示和行为
|
|
843
|
+
*/
|
|
844
|
+
secondary?: {
|
|
845
|
+
/** 自定义标签文案 */
|
|
846
|
+
label?: string
|
|
847
|
+
/** 是否显示,默认 false(隐藏) */
|
|
848
|
+
visible?: boolean
|
|
849
|
+
/** 触发的事件名称 */
|
|
850
|
+
event?: string
|
|
851
|
+
/** 事件携带的数据 */
|
|
852
|
+
eventData?: Record<string, unknown>
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* 快速选择提示(Meta+1-0)
|
|
857
|
+
* @description 控制快速选择快捷键的显示
|
|
858
|
+
*/
|
|
859
|
+
quickSelect?: {
|
|
860
|
+
/** 是否显示,默认根据场景自动判断 */
|
|
861
|
+
visible?: boolean
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
806
865
|
// ==================== 扩展元数据 ====================
|
|
807
866
|
|
|
808
867
|
/**
|
|
@@ -1030,6 +1089,12 @@ export interface TuffMeta {
|
|
|
1030
1089
|
* @warning 不建议添加太多关键词(建议 <= 10),过多会影响搜索性能
|
|
1031
1090
|
*/
|
|
1032
1091
|
keywords?: string[]
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* Footer hints 配置
|
|
1095
|
+
* @description 控制 CoreBox 底部快捷键提示的显示和行为
|
|
1096
|
+
*/
|
|
1097
|
+
footerHints?: TuffFooterHints
|
|
1033
1098
|
}
|
|
1034
1099
|
|
|
1035
1100
|
// ==================== 前端展示结构 ====================
|
package/intelligence/client.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { IntelligenceInvokeOptions, IntelligenceInvokeResult, IntelligenceProviderConfig } from '../types/intelligence'
|
|
2
2
|
|
|
3
3
|
export interface IntelligenceClientChannel {
|
|
4
4
|
send: (eventName: string, payload: unknown) => Promise<any>
|
|
@@ -50,10 +50,10 @@ async function assertResponse<T>(promise: Promise<ChannelResponse<T>>): Promise<
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export interface IntelligenceClient {
|
|
53
|
-
invoke: <T = any>(capabilityId: string, payload: any, options?:
|
|
54
|
-
testProvider: (config:
|
|
53
|
+
invoke: <T = any>(capabilityId: string, payload: any, options?: IntelligenceInvokeOptions) => Promise<IntelligenceInvokeResult<T>>
|
|
54
|
+
testProvider: (config: IntelligenceProviderConfig) => Promise<unknown>
|
|
55
55
|
testCapability: (params: Record<string, any>) => Promise<unknown>
|
|
56
|
-
fetchModels: (config:
|
|
56
|
+
fetchModels: (config: IntelligenceProviderConfig) => Promise<{ success: boolean, models?: string[], message?: string }>
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
export function createIntelligenceClient(channel?: IntelligenceClientChannel, resolvers?: IntelligenceChannelResolver[]): IntelligenceClient {
|
|
@@ -63,12 +63,12 @@ export function createIntelligenceClient(channel?: IntelligenceClientChannel, re
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
return {
|
|
66
|
-
invoke<T = any>(capabilityId: string, payload: any, options?:
|
|
67
|
-
return assertResponse<
|
|
66
|
+
invoke<T = any>(capabilityId: string, payload: any, options?: IntelligenceInvokeOptions) {
|
|
67
|
+
return assertResponse<IntelligenceInvokeResult<T>>(
|
|
68
68
|
resolvedChannel.send('intelligence:invoke', { capabilityId, payload, options }),
|
|
69
69
|
)
|
|
70
70
|
},
|
|
71
|
-
testProvider(config:
|
|
71
|
+
testProvider(config: IntelligenceProviderConfig) {
|
|
72
72
|
return assertResponse(
|
|
73
73
|
resolvedChannel.send('intelligence:test-provider', { provider: config }),
|
|
74
74
|
)
|
|
@@ -78,7 +78,7 @@ export function createIntelligenceClient(channel?: IntelligenceClientChannel, re
|
|
|
78
78
|
resolvedChannel.send('intelligence:test-capability', params),
|
|
79
79
|
)
|
|
80
80
|
},
|
|
81
|
-
fetchModels(config:
|
|
81
|
+
fetchModels(config: IntelligenceProviderConfig) {
|
|
82
82
|
return assertResponse<{ success: boolean, models?: string[], message?: string }>(
|
|
83
83
|
resolvedChannel.send('intelligence:fetch-models', { provider: config }),
|
|
84
84
|
)
|
package/market/constants.ts
CHANGED
|
@@ -21,6 +21,20 @@ function defineProvider(
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export const DEFAULT_MARKET_PROVIDERS: MarketProviderDefinition[] = [
|
|
24
|
+
defineProvider({
|
|
25
|
+
id: 'tuff-nexus',
|
|
26
|
+
name: 'Tuff Nexus',
|
|
27
|
+
type: 'tpexApi',
|
|
28
|
+
url: 'https://tuff.tagzxia.com',
|
|
29
|
+
description: 'Tuff 官方插件市场,提供经过审核的插件。',
|
|
30
|
+
enabled: true,
|
|
31
|
+
priority: 110,
|
|
32
|
+
trustLevel: 'official',
|
|
33
|
+
readOnly: true,
|
|
34
|
+
config: {
|
|
35
|
+
apiUrl: 'https://tuff.tagzxia.com/api/market/plugins',
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
24
38
|
defineProvider({
|
|
25
39
|
id: 'talex-official',
|
|
26
40
|
name: 'Talex Official',
|
package/market/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { StorageList } from '../common/storage/constants'
|
|
2
2
|
|
|
3
|
-
export type MarketProviderType = 'repository' | 'nexusStore' | 'npmPackage'
|
|
3
|
+
export type MarketProviderType = 'repository' | 'nexusStore' | 'npmPackage' | 'tpexApi'
|
|
4
4
|
|
|
5
5
|
export type MarketProviderTrustLevel = 'official' | 'verified' | 'unverified'
|
|
6
6
|
|
package/package.json
CHANGED
package/plugin/index.ts
CHANGED
|
@@ -134,7 +134,8 @@ export interface ITouchPlugin extends IPluginBaseInfo {
|
|
|
134
134
|
export interface IFeatureCommand {
|
|
135
135
|
type: 'match' | 'contain' | 'regex' | 'function' | 'over' | 'image' | 'files' | 'directory' | 'window'
|
|
136
136
|
value: string | string[] | RegExp | Function
|
|
137
|
-
|
|
137
|
+
/** Optional trigger callback - not serialized over IPC */
|
|
138
|
+
onTrigger?: () => void
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
export interface IPluginFeature {
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import type { NpmPackageInfo } from './npm-provider'
|
|
2
|
+
import type { TpexPluginInfo } from './tpex-provider'
|
|
3
|
+
import { NpmProvider } from './npm-provider'
|
|
4
|
+
import { TpexProvider } from './tpex-provider'
|
|
5
|
+
import { PluginProviderType } from './types'
|
|
6
|
+
|
|
7
|
+
export type PluginSourceType = 'tpex' | 'npm' | 'all'
|
|
8
|
+
|
|
9
|
+
export interface MarketPluginInfo {
|
|
10
|
+
id: string
|
|
11
|
+
name: string
|
|
12
|
+
slug: string
|
|
13
|
+
version: string
|
|
14
|
+
description: string
|
|
15
|
+
author: string
|
|
16
|
+
icon?: string
|
|
17
|
+
source: PluginSourceType
|
|
18
|
+
isOfficial: boolean
|
|
19
|
+
downloads?: number
|
|
20
|
+
category?: string
|
|
21
|
+
keywords?: string[]
|
|
22
|
+
homepage?: string
|
|
23
|
+
packageUrl?: string
|
|
24
|
+
raw: TpexPluginInfo | NpmPackageInfo
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface MarketSearchOptions {
|
|
28
|
+
keyword?: string
|
|
29
|
+
source?: PluginSourceType
|
|
30
|
+
category?: string
|
|
31
|
+
limit?: number
|
|
32
|
+
offset?: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface MarketSearchResult {
|
|
36
|
+
plugins: MarketPluginInfo[]
|
|
37
|
+
total: number
|
|
38
|
+
sources: {
|
|
39
|
+
tpex: number
|
|
40
|
+
npm: number
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeTpexPlugin(plugin: TpexPluginInfo): MarketPluginInfo {
|
|
45
|
+
return {
|
|
46
|
+
id: plugin.id,
|
|
47
|
+
name: plugin.name,
|
|
48
|
+
slug: plugin.slug,
|
|
49
|
+
version: plugin.latestVersion?.version ?? '0.0.0',
|
|
50
|
+
description: plugin.summary,
|
|
51
|
+
author: plugin.author?.name ?? 'Unknown',
|
|
52
|
+
icon: plugin.iconUrl ?? undefined,
|
|
53
|
+
source: 'tpex',
|
|
54
|
+
isOfficial: plugin.isOfficial,
|
|
55
|
+
downloads: plugin.installs,
|
|
56
|
+
category: plugin.category,
|
|
57
|
+
homepage: plugin.homepage ?? undefined,
|
|
58
|
+
packageUrl: plugin.latestVersion?.packageUrl,
|
|
59
|
+
raw: plugin,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeNpmPlugin(pkg: NpmPackageInfo): MarketPluginInfo {
|
|
64
|
+
const authorName = typeof pkg.author === 'string'
|
|
65
|
+
? pkg.author
|
|
66
|
+
: pkg.author?.name ?? 'Unknown'
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
id: pkg.name,
|
|
70
|
+
name: pkg.name.replace(/^tuff-plugin-/, '').replace(/^@tuff\//, ''),
|
|
71
|
+
slug: pkg.name,
|
|
72
|
+
version: pkg.version,
|
|
73
|
+
description: pkg.description ?? '',
|
|
74
|
+
author: authorName,
|
|
75
|
+
icon: pkg.tuff?.icon,
|
|
76
|
+
source: 'npm',
|
|
77
|
+
isOfficial: pkg.name.startsWith('@tuff/'),
|
|
78
|
+
keywords: pkg.keywords,
|
|
79
|
+
packageUrl: pkg.dist.tarball,
|
|
80
|
+
raw: pkg,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Unified plugin market client supporting multiple sources
|
|
86
|
+
*/
|
|
87
|
+
export class PluginMarketClient {
|
|
88
|
+
private tpexProvider: TpexProvider
|
|
89
|
+
private npmProvider: NpmProvider
|
|
90
|
+
|
|
91
|
+
constructor(options?: {
|
|
92
|
+
tpexApiBase?: string
|
|
93
|
+
npmRegistry?: string
|
|
94
|
+
}) {
|
|
95
|
+
this.tpexProvider = new TpexProvider(options?.tpexApiBase)
|
|
96
|
+
this.npmProvider = new NpmProvider(options?.npmRegistry)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Search plugins from all sources
|
|
101
|
+
*/
|
|
102
|
+
async search(options: MarketSearchOptions = {}): Promise<MarketSearchResult> {
|
|
103
|
+
const { keyword, source = 'all', limit = 50, offset = 0 } = options
|
|
104
|
+
const results: MarketPluginInfo[] = []
|
|
105
|
+
let tpexCount = 0
|
|
106
|
+
let npmCount = 0
|
|
107
|
+
|
|
108
|
+
if (source === 'all' || source === 'tpex') {
|
|
109
|
+
try {
|
|
110
|
+
const tpexPlugins = keyword
|
|
111
|
+
? await this.tpexProvider.searchPlugins(keyword)
|
|
112
|
+
: await this.tpexProvider.listPlugins()
|
|
113
|
+
|
|
114
|
+
const normalized = tpexPlugins.map(normalizeTpexPlugin)
|
|
115
|
+
results.push(...normalized)
|
|
116
|
+
tpexCount = normalized.length
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
console.warn('[MarketClient] TPEX search failed:', error)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (source === 'all' || source === 'npm') {
|
|
124
|
+
try {
|
|
125
|
+
const npmPlugins = await this.npmProvider.searchPlugins(keyword)
|
|
126
|
+
const normalized = npmPlugins.map(normalizeNpmPlugin)
|
|
127
|
+
results.push(...normalized)
|
|
128
|
+
npmCount = normalized.length
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
console.warn('[MarketClient] NPM search failed:', error)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const sorted = results.sort((a, b) => {
|
|
136
|
+
if (a.isOfficial !== b.isOfficial) return a.isOfficial ? -1 : 1
|
|
137
|
+
return (b.downloads ?? 0) - (a.downloads ?? 0)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const paginated = sorted.slice(offset, offset + limit)
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
plugins: paginated,
|
|
144
|
+
total: results.length,
|
|
145
|
+
sources: {
|
|
146
|
+
tpex: tpexCount,
|
|
147
|
+
npm: npmCount,
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get plugin details by identifier
|
|
154
|
+
*/
|
|
155
|
+
async getPlugin(identifier: string, source?: PluginSourceType): Promise<MarketPluginInfo | null> {
|
|
156
|
+
if (source === 'tpex' || (!source && !identifier.includes('/'))) {
|
|
157
|
+
try {
|
|
158
|
+
const plugin = await this.tpexProvider.getPlugin(identifier)
|
|
159
|
+
if (plugin) return normalizeTpexPlugin(plugin)
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// Fall through to npm
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (source === 'npm' || !source) {
|
|
167
|
+
try {
|
|
168
|
+
const pkg = await this.npmProvider.getPackageInfo(identifier)
|
|
169
|
+
if (pkg) return normalizeNpmPlugin(pkg)
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// Not found
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get install source string for a plugin
|
|
181
|
+
*/
|
|
182
|
+
getInstallSource(plugin: MarketPluginInfo): string {
|
|
183
|
+
if (plugin.source === 'tpex') {
|
|
184
|
+
return `tpex:${plugin.slug}`
|
|
185
|
+
}
|
|
186
|
+
return `npm:${plugin.id}`
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get provider type for a plugin
|
|
191
|
+
*/
|
|
192
|
+
getProviderType(plugin: MarketPluginInfo): PluginProviderType {
|
|
193
|
+
return plugin.source === 'tpex'
|
|
194
|
+
? PluginProviderType.TPEX
|
|
195
|
+
: PluginProviderType.NPM
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* List all plugins from official source (TPEX)
|
|
200
|
+
*/
|
|
201
|
+
async listOfficialPlugins(): Promise<MarketPluginInfo[]> {
|
|
202
|
+
const plugins = await this.tpexProvider.listPlugins()
|
|
203
|
+
return plugins.map(normalizeTpexPlugin)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* List all plugins from npm
|
|
208
|
+
*/
|
|
209
|
+
async listNpmPlugins(): Promise<MarketPluginInfo[]> {
|
|
210
|
+
const plugins = await this.npmProvider.listPlugins()
|
|
211
|
+
return plugins.map(normalizeNpmPlugin)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export const defaultMarketClient = new PluginMarketClient()
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type { IManifest } from '..'
|
|
2
|
+
import type {
|
|
3
|
+
PluginInstallRequest,
|
|
4
|
+
PluginInstallResult,
|
|
5
|
+
PluginProvider,
|
|
6
|
+
PluginProviderContext,
|
|
7
|
+
} from './types'
|
|
8
|
+
import { PluginProviderType } from './types'
|
|
9
|
+
|
|
10
|
+
const NPM_REGISTRY = 'https://registry.npmjs.org'
|
|
11
|
+
const TUFF_PLUGIN_PREFIX = 'tuff-plugin-'
|
|
12
|
+
const TUFF_PLUGIN_SCOPE = '@tuff/'
|
|
13
|
+
|
|
14
|
+
export interface NpmPackageInfo {
|
|
15
|
+
name: string
|
|
16
|
+
version: string
|
|
17
|
+
description?: string
|
|
18
|
+
author?: string | { name: string, email?: string }
|
|
19
|
+
main?: string
|
|
20
|
+
keywords?: string[]
|
|
21
|
+
dist: {
|
|
22
|
+
tarball: string
|
|
23
|
+
shasum: string
|
|
24
|
+
integrity?: string
|
|
25
|
+
}
|
|
26
|
+
tuff?: {
|
|
27
|
+
icon?: string
|
|
28
|
+
activationKeywords?: string[]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface NpmPackageVersions {
|
|
33
|
+
name: string
|
|
34
|
+
'dist-tags': {
|
|
35
|
+
latest: string
|
|
36
|
+
[tag: string]: string
|
|
37
|
+
}
|
|
38
|
+
versions: Record<string, NpmPackageInfo>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse NPM source string to extract package name and optional version
|
|
43
|
+
* Formats:
|
|
44
|
+
* - "npm:package-name"
|
|
45
|
+
* - "npm:package-name@version"
|
|
46
|
+
* - "npm:@scope/package-name"
|
|
47
|
+
* - "npm:@scope/package-name@version"
|
|
48
|
+
* - "tuff-plugin-xxx" (when hintType is NPM)
|
|
49
|
+
* - "@tuff/xxx" (when hintType is NPM)
|
|
50
|
+
*/
|
|
51
|
+
function parseNpmSource(source: string): { packageName: string, version?: string } | null {
|
|
52
|
+
const npmMatch = source.match(/^npm:(@?[a-z0-9][\w\-.]*(?:\/[a-z0-9][\w\-.]*)?)(?:@(.+))?$/i)
|
|
53
|
+
if (npmMatch) {
|
|
54
|
+
return { packageName: npmMatch[1], version: npmMatch[2] }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const scopedMatch = source.match(/^(@tuff\/[a-z0-9][\w\-.]*)(?:@(.+))?$/i)
|
|
58
|
+
if (scopedMatch) {
|
|
59
|
+
return { packageName: scopedMatch[1], version: scopedMatch[2] }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const prefixMatch = source.match(/^(tuff-plugin-[a-z0-9][\w\-.]*)(?:@(.+))?$/i)
|
|
63
|
+
if (prefixMatch) {
|
|
64
|
+
return { packageName: prefixMatch[1], version: prefixMatch[2] }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if a package name looks like a Tuff plugin
|
|
72
|
+
*/
|
|
73
|
+
function isTuffPluginPackage(name: string): boolean {
|
|
74
|
+
return name.startsWith(TUFF_PLUGIN_PREFIX) || name.startsWith(TUFF_PLUGIN_SCOPE)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class NpmProvider implements PluginProvider {
|
|
78
|
+
readonly type = PluginProviderType.NPM
|
|
79
|
+
private registry: string
|
|
80
|
+
|
|
81
|
+
constructor(registry: string = NPM_REGISTRY) {
|
|
82
|
+
this.registry = registry.replace(/\/$/, '')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
canHandle(request: PluginInstallRequest): boolean {
|
|
86
|
+
if (request.hintType === PluginProviderType.NPM) {
|
|
87
|
+
return parseNpmSource(request.source) !== null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (request.source.startsWith('npm:')) {
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const parsed = parseNpmSource(request.source)
|
|
95
|
+
return parsed !== null && isTuffPluginPackage(parsed.packageName)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async install(
|
|
99
|
+
request: PluginInstallRequest,
|
|
100
|
+
context?: PluginProviderContext,
|
|
101
|
+
): Promise<PluginInstallResult> {
|
|
102
|
+
const parsed = parseNpmSource(request.source)
|
|
103
|
+
if (!parsed) {
|
|
104
|
+
throw new Error(`Invalid NPM source format: ${request.source}`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const { packageName, version } = parsed
|
|
108
|
+
const packageInfo = await this.getPackageInfo(packageName, version)
|
|
109
|
+
|
|
110
|
+
if (!packageInfo) {
|
|
111
|
+
throw new Error(`Package not found: ${packageName}`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const tarballUrl = packageInfo.dist.tarball
|
|
115
|
+
const downloadRes = await fetch(tarballUrl)
|
|
116
|
+
if (!downloadRes.ok) {
|
|
117
|
+
throw new Error(`Failed to download package: ${downloadRes.statusText}`)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const arrayBuffer = await downloadRes.arrayBuffer()
|
|
121
|
+
const tempDir = context?.tempDir ?? '/tmp'
|
|
122
|
+
const safePackageName = packageName.replace(/[@/]/g, '-')
|
|
123
|
+
const fileName = `${safePackageName}-${packageInfo.version}.tgz`
|
|
124
|
+
const filePath = `${tempDir}/${fileName}`
|
|
125
|
+
|
|
126
|
+
if (typeof globalThis.process !== 'undefined') {
|
|
127
|
+
const fs = await import('node:fs/promises')
|
|
128
|
+
await fs.writeFile(filePath, Buffer.from(arrayBuffer))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const authorName = typeof packageInfo.author === 'string'
|
|
132
|
+
? packageInfo.author
|
|
133
|
+
: packageInfo.author?.name ?? 'Unknown'
|
|
134
|
+
|
|
135
|
+
const manifest: IManifest = {
|
|
136
|
+
id: packageInfo.name,
|
|
137
|
+
name: packageInfo.name.replace(TUFF_PLUGIN_PREFIX, '').replace(TUFF_PLUGIN_SCOPE, ''),
|
|
138
|
+
version: packageInfo.version,
|
|
139
|
+
description: packageInfo.description ?? '',
|
|
140
|
+
author: authorName,
|
|
141
|
+
main: packageInfo.main ?? 'index.js',
|
|
142
|
+
icon: packageInfo.tuff?.icon,
|
|
143
|
+
activationKeywords: packageInfo.tuff?.activationKeywords ?? packageInfo.keywords,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
provider: PluginProviderType.NPM,
|
|
148
|
+
filePath,
|
|
149
|
+
official: packageName.startsWith(TUFF_PLUGIN_SCOPE),
|
|
150
|
+
manifest,
|
|
151
|
+
metadata: {
|
|
152
|
+
packageName: packageInfo.name,
|
|
153
|
+
version: packageInfo.version,
|
|
154
|
+
tarball: tarballUrl,
|
|
155
|
+
shasum: packageInfo.dist.shasum,
|
|
156
|
+
integrity: packageInfo.dist.integrity,
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get package info from npm registry
|
|
163
|
+
*/
|
|
164
|
+
async getPackageInfo(packageName: string, version?: string): Promise<NpmPackageInfo | null> {
|
|
165
|
+
const encodedName = encodeURIComponent(packageName).replace('%40', '@')
|
|
166
|
+
const res = await fetch(`${this.registry}/${encodedName}`)
|
|
167
|
+
|
|
168
|
+
if (!res.ok) {
|
|
169
|
+
if (res.status === 404) return null
|
|
170
|
+
throw new Error(`Failed to fetch package info: ${res.statusText}`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const data: NpmPackageVersions = await res.json()
|
|
174
|
+
const targetVersion = version ?? data['dist-tags'].latest
|
|
175
|
+
|
|
176
|
+
return data.versions[targetVersion] ?? null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Search for Tuff plugins on npm
|
|
181
|
+
*/
|
|
182
|
+
async searchPlugins(keyword?: string): Promise<NpmPackageInfo[]> {
|
|
183
|
+
const searchTerms = [
|
|
184
|
+
`keywords:tuff-plugin`,
|
|
185
|
+
keyword ? `${keyword}` : '',
|
|
186
|
+
].filter(Boolean).join('+')
|
|
187
|
+
|
|
188
|
+
const res = await fetch(
|
|
189
|
+
`${this.registry}/-/v1/search?text=${encodeURIComponent(searchTerms)}&size=100`,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if (!res.ok) {
|
|
193
|
+
throw new Error(`Failed to search packages: ${res.statusText}`)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const data = await res.json() as {
|
|
197
|
+
objects: Array<{ package: NpmPackageInfo }>
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return data.objects
|
|
201
|
+
.map(obj => obj.package)
|
|
202
|
+
.filter(pkg => isTuffPluginPackage(pkg.name))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* List all available Tuff plugins from npm
|
|
207
|
+
*/
|
|
208
|
+
async listPlugins(): Promise<NpmPackageInfo[]> {
|
|
209
|
+
return this.searchPlugins()
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export const npmProvider = new NpmProvider()
|