@42ailab/42plugin 0.1.0-beta.1 → 0.1.5

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/src/api.ts ADDED
@@ -0,0 +1,447 @@
1
+ /**
2
+ * API 客户端
3
+ *
4
+ * 单文件封装所有 API 调用
5
+ */
6
+
7
+ import { config } from './config';
8
+ import type {
9
+ PluginDownload,
10
+ KitDownload,
11
+ KitApiResponse,
12
+ SearchResponse,
13
+ SearchApiResponse,
14
+ DeviceCodeResponse,
15
+ DeviceTokenResponse,
16
+ Session,
17
+ PluginStatusResponse,
18
+ UploadUrlResponse,
19
+ PublishConfirmRequest,
20
+ PublishConfirmResponse,
21
+ PluginType,
22
+ } from './types';
23
+
24
+ // ============================================================================
25
+ // 错误类型
26
+ // ============================================================================
27
+
28
+ export class ApiError extends Error {
29
+ constructor(
30
+ public statusCode: number,
31
+ public endpoint: string,
32
+ message: string,
33
+ public code?: string
34
+ ) {
35
+ super(message);
36
+ this.name = 'ApiError';
37
+ }
38
+ }
39
+
40
+ export class QuotaExceededError extends ApiError {
41
+ constructor(
42
+ public quotaType: string,
43
+ public limit: number,
44
+ public role: string,
45
+ message: string
46
+ ) {
47
+ super(429, 'quota', message, 'QUOTA_EXCEEDED');
48
+ this.name = 'QuotaExceededError';
49
+ }
50
+
51
+ isGuestQuota(): boolean {
52
+ return this.role === 'guest';
53
+ }
54
+ }
55
+
56
+ // ============================================================================
57
+ // API 客户端
58
+ // ============================================================================
59
+
60
+ class ApiClient {
61
+ private baseUrl: string;
62
+ private sessionToken: string | null = null;
63
+
64
+ constructor() {
65
+ this.baseUrl = config.apiBaseUrl;
66
+ }
67
+
68
+ setSessionToken(token: string | null): void {
69
+ this.sessionToken = token;
70
+ }
71
+
72
+ isAuthenticated(): boolean {
73
+ return !!this.sessionToken;
74
+ }
75
+
76
+ private async request<T>(
77
+ path: string,
78
+ options: {
79
+ method?: string;
80
+ body?: unknown;
81
+ auth?: boolean;
82
+ } = {}
83
+ ): Promise<T> {
84
+ const { method = 'GET', body, auth = true } = options;
85
+
86
+ const headers: Record<string, string> = {
87
+ 'Content-Type': 'application/json',
88
+ 'User-Agent': '42plugin-cli/1.0.0',
89
+ 'X-Download-Source': 'cli',
90
+ };
91
+
92
+ if (auth && this.sessionToken) {
93
+ headers['Authorization'] = `Bearer ${this.sessionToken}`;
94
+ }
95
+
96
+ const response = await fetch(`${this.baseUrl}${path}`, {
97
+ method,
98
+ headers,
99
+ body: body ? JSON.stringify(body) : undefined,
100
+ });
101
+
102
+ const data = await response.json().catch(() => ({}));
103
+
104
+ if (!response.ok) {
105
+ const error = (data as { error?: { code?: string; message?: string; quotaType?: string; limit?: number; role?: string } }).error;
106
+ const message = error?.message || `HTTP ${response.status}`;
107
+
108
+ if (response.status === 429 && error?.code === 'QUOTA_EXCEEDED') {
109
+ throw new QuotaExceededError(
110
+ error.quotaType || 'unknown',
111
+ error.limit || 0,
112
+ error.role || 'guest',
113
+ message
114
+ );
115
+ }
116
+
117
+ throw new ApiError(response.status, path, message, error?.code);
118
+ }
119
+
120
+ return ((data as { data?: T }).data ?? data) as T;
121
+ }
122
+
123
+ // ==========================================================================
124
+ // 插件 API
125
+ // ==========================================================================
126
+
127
+ async getPluginDownload(author: string, name: string): Promise<PluginDownload> {
128
+ return this.request<PluginDownload>(`/v1/plugins/${author}/${name}/download`);
129
+ }
130
+
131
+ // ==========================================================================
132
+ // 套包 API
133
+ // ==========================================================================
134
+
135
+ async getKitDownload(username: string, slugOrId: string): Promise<KitDownload> {
136
+ const raw = await this.request<KitApiResponse['data']>(
137
+ `/v1/kits/${username}/kit/${slugOrId}/download`
138
+ );
139
+
140
+ // 转换 snake_case -> camelCase
141
+ return {
142
+ kit: {
143
+ fullName: raw.kit.full_name,
144
+ name: raw.kit.name,
145
+ description: raw.kit.description,
146
+ priceTier: raw.kit.price_tier,
147
+ effectivePriceTier: raw.kit.effective_price_tier,
148
+ },
149
+ plugins: raw.plugins.map((p) => ({
150
+ plugin: {
151
+ fullName: p.plugin.full_name,
152
+ name: p.plugin.name,
153
+ title: p.plugin.title,
154
+ author: p.plugin.author,
155
+ type: p.plugin.type,
156
+ },
157
+ download: {
158
+ fullName: p.download.full_name,
159
+ name: p.download.name,
160
+ type: p.download.type,
161
+ version: p.download.version,
162
+ downloadUrl: p.download.download_url,
163
+ checksum: p.download.checksum,
164
+ sizeBytes: p.download.size_bytes,
165
+ installPath: p.download.install_path,
166
+ },
167
+ required: p.required,
168
+ reason: p.reason,
169
+ })),
170
+ summary: {
171
+ totalPlugins: raw.summary.total_plugins,
172
+ requiredPlugins: raw.summary.required_plugins,
173
+ optionalPlugins: raw.summary.optional_plugins,
174
+ totalSizeBytes: raw.summary.total_size_bytes,
175
+ expiresAt: raw.summary.expires_at,
176
+ },
177
+ };
178
+ }
179
+
180
+ // ==========================================================================
181
+ // 搜索 API
182
+ // ==========================================================================
183
+
184
+ async search(params: {
185
+ q: string;
186
+ pluginType?: string;
187
+ page?: number;
188
+ perPage?: number;
189
+ }): Promise<SearchResponse> {
190
+ const searchParams = new URLSearchParams();
191
+ searchParams.set('q', params.q);
192
+ if (params.pluginType) searchParams.set('plugin_type', params.pluginType);
193
+ if (params.page) searchParams.set('page', String(params.page));
194
+ if (params.perPage) searchParams.set('per_page', String(params.perPage));
195
+
196
+ // 搜索 API 直接返回 {data, pagination},不需要解包
197
+ const headers: Record<string, string> = {
198
+ 'Content-Type': 'application/json',
199
+ 'User-Agent': '42plugin-cli/1.0.0',
200
+ 'X-Download-Source': 'cli',
201
+ };
202
+ if (this.sessionToken) {
203
+ headers['Authorization'] = `Bearer ${this.sessionToken}`;
204
+ }
205
+
206
+ const response = await fetch(`${this.baseUrl}/v1/search?${searchParams}`, {
207
+ method: 'GET',
208
+ headers,
209
+ });
210
+
211
+ const raw = await response.json();
212
+
213
+ if (!response.ok) {
214
+ const error = raw?.error;
215
+ const message = error?.message || `HTTP ${response.status}`;
216
+ throw new ApiError(response.status, '/v1/search', message, error?.code);
217
+ }
218
+
219
+ // 转换 API 响应格式为内部格式 (snake_case -> camelCase)
220
+ const apiData = raw as SearchApiResponse;
221
+ const plugins = apiData.data?.plugins || [];
222
+
223
+ return {
224
+ data: plugins.map((p) => ({
225
+ fullName: p.full_name,
226
+ name: p.name,
227
+ title: p.title,
228
+ slogan: p.slogan || null,
229
+ description: p.description,
230
+ type: p.type,
231
+ version: p.version || '0.0.0',
232
+ author: p.author,
233
+ tags: p.tags || [],
234
+ downloads: p.downloads,
235
+ icon: p.icon || null,
236
+ })),
237
+ pagination: {
238
+ page: apiData.pagination?.page || 1,
239
+ perPage: apiData.pagination?.per_page || 20,
240
+ total: apiData.pagination?.total || 0,
241
+ totalPages: Math.ceil((apiData.pagination?.total || 0) / (apiData.pagination?.per_page || 20)),
242
+ },
243
+ };
244
+ }
245
+
246
+ // ==========================================================================
247
+ // 认证 API
248
+ // ==========================================================================
249
+
250
+ async requestDeviceCode(): Promise<DeviceCodeResponse> {
251
+ const response = await fetch(`${this.baseUrl}/auth/device/code`, {
252
+ method: 'POST',
253
+ headers: {
254
+ 'Content-Type': 'application/json',
255
+ 'User-Agent': '42plugin-cli/1.0.0',
256
+ 'X-Download-Source': 'cli',
257
+ },
258
+ body: JSON.stringify({
259
+ client_id: '42plugin-cli',
260
+ scope: 'openid profile email',
261
+ }),
262
+ });
263
+
264
+ const data = await response.json();
265
+
266
+ return {
267
+ deviceCode: data.device_code,
268
+ userCode: data.user_code,
269
+ verificationUri: data.verification_uri,
270
+ verificationUriComplete: data.verification_uri_complete,
271
+ expiresIn: data.expires_in,
272
+ interval: data.interval,
273
+ };
274
+ }
275
+
276
+ async pollDeviceToken(deviceCode: string): Promise<DeviceTokenResponse> {
277
+ const response = await fetch(`${this.baseUrl}/auth/device/token`, {
278
+ method: 'POST',
279
+ headers: {
280
+ 'Content-Type': 'application/json',
281
+ 'User-Agent': '42plugin-cli/1.0.0',
282
+ 'X-Download-Source': 'cli',
283
+ },
284
+ body: JSON.stringify({
285
+ client_id: '42plugin-cli',
286
+ device_code: deviceCode,
287
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
288
+ }),
289
+ });
290
+
291
+ const data = await response.json();
292
+
293
+ return {
294
+ accessToken: data.access_token,
295
+ user: data.user
296
+ ? {
297
+ id: data.user.id,
298
+ name: data.user.name,
299
+ username: data.user.username,
300
+ email: data.user.email,
301
+ }
302
+ : undefined,
303
+ error: data.error,
304
+ errorDescription: data.error_description,
305
+ };
306
+ }
307
+
308
+ async getSession(): Promise<Session> {
309
+ // 开发模式跳过认证 - 返回 mock session
310
+ if (config.devSkipAuth) {
311
+ return {
312
+ user: {
313
+ id: 'dev-user-id',
314
+ name: 'DevUser',
315
+ username: 'devuser',
316
+ email: 'dev@localhost',
317
+ },
318
+ session: {
319
+ id: 'dev-session-id',
320
+ expiresAt: new Date(Date.now() + 86400000).toISOString(),
321
+ },
322
+ };
323
+ }
324
+ return this.request<Session>('/session');
325
+ }
326
+
327
+ // ==========================================================================
328
+ // 用户 API
329
+ // ==========================================================================
330
+
331
+ async recordInstall(pluginFullName: string): Promise<void> {
332
+ await this.request('/user/installed', {
333
+ method: 'POST',
334
+ body: { plugin_full_name: pluginFullName },
335
+ });
336
+ }
337
+
338
+ async removeInstallRecord(author: string, name: string): Promise<void> {
339
+ await this.request(`/user/installed/${author}/${name}`, {
340
+ method: 'DELETE',
341
+ });
342
+ }
343
+
344
+ // ==========================================================================
345
+ // 发布 API
346
+ // ==========================================================================
347
+
348
+ /**
349
+ * 获取插件状态(用于版本决策)
350
+ */
351
+ async getPluginStatus(name: string): Promise<PluginStatusResponse> {
352
+ try {
353
+ const result = await this.request<{
354
+ exists: boolean;
355
+ version?: string;
356
+ content_hash?: string;
357
+ visibility?: 'self' | 'public';
358
+ }>(`/v1/plugins/me/${name}/status`);
359
+
360
+ return {
361
+ exists: result.exists,
362
+ version: result.version,
363
+ contentHash: result.content_hash,
364
+ visibility: result.visibility,
365
+ };
366
+ } catch (error) {
367
+ // 404 表示插件不存在
368
+ if (error instanceof ApiError && error.statusCode === 404) {
369
+ return { exists: false };
370
+ }
371
+ throw error;
372
+ }
373
+ }
374
+
375
+ /**
376
+ * 获取上传签名 URL
377
+ */
378
+ async getUploadUrl(params: {
379
+ name: string;
380
+ type: PluginType;
381
+ version: string;
382
+ contentHash: string;
383
+ sizeBytes: number;
384
+ }): Promise<UploadUrlResponse> {
385
+ const result = await this.request<{
386
+ upload_url: string;
387
+ storage_key: string;
388
+ expires_at: string;
389
+ }>('/v1/plugins/upload-url', {
390
+ method: 'POST',
391
+ body: {
392
+ name: params.name,
393
+ type: params.type,
394
+ version: params.version,
395
+ content_hash: params.contentHash,
396
+ size_bytes: params.sizeBytes,
397
+ },
398
+ });
399
+
400
+ return {
401
+ uploadUrl: result.upload_url,
402
+ storageKey: result.storage_key,
403
+ expiresAt: result.expires_at,
404
+ };
405
+ }
406
+
407
+ /**
408
+ * 确认发布
409
+ */
410
+ async confirmPublish(params: PublishConfirmRequest): Promise<PublishConfirmResponse> {
411
+ const result = await this.request<{
412
+ plugin: {
413
+ id: string;
414
+ full_name: string;
415
+ version: string;
416
+ };
417
+ action: 'created' | 'updated';
418
+ }>('/v1/plugins', {
419
+ method: 'POST',
420
+ body: {
421
+ name: params.name,
422
+ type: params.type,
423
+ version: params.version,
424
+ content_hash: params.contentHash,
425
+ package_hash: params.packageHash,
426
+ storage_key: params.storageKey,
427
+ size_bytes: params.sizeBytes,
428
+ title: params.title,
429
+ description: params.description,
430
+ tags: params.tags,
431
+ visibility: params.visibility,
432
+ },
433
+ });
434
+
435
+ return {
436
+ plugin: {
437
+ id: result.plugin.id,
438
+ fullName: result.plugin.full_name,
439
+ version: result.plugin.version,
440
+ },
441
+ action: result.action,
442
+ };
443
+ }
444
+ }
445
+
446
+ // 单例导出
447
+ export const api = new ApiClient();
package/src/cli.ts CHANGED
@@ -1,27 +1,50 @@
1
+ /**
2
+ * CLI 入口
3
+ */
4
+
1
5
  import { Command } from 'commander';
2
- import { authCommand } from './commands/auth';
3
- import { installCommand } from './commands/install';
4
- import { searchCommand } from './commands/search';
5
- import { listCommand } from './commands/list';
6
- import { uninstallCommand } from './commands/uninstall';
7
- import { versionCommand } from './commands/version';
6
+ import {
7
+ authCommand,
8
+ installCommand,
9
+ searchCommand,
10
+ listCommand,
11
+ uninstallCommand,
12
+ completionCommand,
13
+ setupCommand,
14
+ publishCommand,
15
+ checkCommand,
16
+ } from './commands';
17
+
18
+ // 动态读取版本号
19
+ const packageJson = await Bun.file(
20
+ new URL('../package.json', import.meta.url)
21
+ ).json();
22
+ const version: string = packageJson.version;
8
23
 
9
- export const program = new Command();
24
+ const program = new Command();
10
25
 
11
26
  program
12
27
  .name('42plugin')
13
- .description('活水插件')
14
- .version('0.1.0', '-v, --version', '显示版本号')
15
- .option('--verbose', '详细输出')
16
- .option('--no-color', '禁用颜色输出');
28
+ .description('活水插件 - AI 插件管理工具')
29
+ .version(version);
17
30
 
18
- // Register commands
31
+ // 注册命令
19
32
  program.addCommand(authCommand);
20
- program.addCommand(installCommand);
21
33
  program.addCommand(searchCommand);
34
+ program.addCommand(installCommand);
22
35
  program.addCommand(listCommand);
23
36
  program.addCommand(uninstallCommand);
24
- program.addCommand(versionCommand);
37
+ program.addCommand(publishCommand);
38
+ program.addCommand(checkCommand);
39
+ program.addCommand(completionCommand);
40
+ program.addCommand(setupCommand);
41
+
42
+ // 版本命令
43
+ program
44
+ .command('version')
45
+ .description('显示版本信息')
46
+ .action(() => {
47
+ console.log(`42plugin v${version}`);
48
+ });
25
49
 
26
- // Default help
27
- program.showHelpAfterError();
50
+ export { program };