@42ailab/42plugin 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -0
- package/package.json +58 -0
- package/src/cli.ts +27 -0
- package/src/commands/auth.ts +114 -0
- package/src/commands/install.ts +323 -0
- package/src/commands/list.ts +80 -0
- package/src/commands/search.ts +87 -0
- package/src/commands/uninstall.ts +58 -0
- package/src/commands/version.ts +20 -0
- package/src/config.ts +44 -0
- package/src/db/client.ts +180 -0
- package/src/index.ts +35 -0
- package/src/services/api.ts +128 -0
- package/src/services/auth.ts +46 -0
- package/src/services/cache.ts +101 -0
- package/src/services/download.ts +148 -0
- package/src/services/link.ts +86 -0
- package/src/services/project.ts +179 -0
- package/src/types/api.ts +115 -0
- package/src/types/db.ts +31 -0
- package/src/utils/errors.ts +40 -0
- package/src/utils/platform.ts +6 -0
- package/src/utils/target.ts +114 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { createWriteStream } from 'fs';
|
|
4
|
+
import { pipeline } from 'stream/promises';
|
|
5
|
+
import * as tar from 'tar';
|
|
6
|
+
import { createHash } from 'crypto';
|
|
7
|
+
import { getCacheDir } from '../config';
|
|
8
|
+
|
|
9
|
+
export async function downloadAndExtract(
|
|
10
|
+
url: string,
|
|
11
|
+
expectedChecksum: string,
|
|
12
|
+
fullName: string,
|
|
13
|
+
version: string
|
|
14
|
+
): Promise<string> {
|
|
15
|
+
// Build cache path
|
|
16
|
+
const cachePath = buildCachePath(fullName, version);
|
|
17
|
+
await fs.mkdir(cachePath, { recursive: true });
|
|
18
|
+
|
|
19
|
+
// Download file
|
|
20
|
+
const tempPath = path.join(cachePath, '_download.tar.gz');
|
|
21
|
+
await downloadFile(url, tempPath);
|
|
22
|
+
|
|
23
|
+
// Verify checksum
|
|
24
|
+
const actualChecksum = await computeFileChecksum(tempPath);
|
|
25
|
+
const expectedHash = expectedChecksum.replace('sha256:', '');
|
|
26
|
+
if (actualChecksum !== expectedHash) {
|
|
27
|
+
await fs.rm(tempPath);
|
|
28
|
+
throw new Error('Checksum verification failed');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Extract
|
|
32
|
+
await tar.extract({
|
|
33
|
+
file: tempPath,
|
|
34
|
+
cwd: cachePath,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Delete temp file
|
|
38
|
+
await fs.rm(tempPath);
|
|
39
|
+
|
|
40
|
+
// Check if tarball extracted to a single subdirectory (common pattern)
|
|
41
|
+
// If so, return that subdirectory as the content path
|
|
42
|
+
const entries = await fs.readdir(cachePath);
|
|
43
|
+
if (entries.length === 1) {
|
|
44
|
+
const subPath = path.join(cachePath, entries[0]);
|
|
45
|
+
const stat = await fs.stat(subPath);
|
|
46
|
+
if (stat.isDirectory()) {
|
|
47
|
+
return subPath;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return cachePath;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildCachePath(fullName: string, version: string): string {
|
|
55
|
+
// fullName formats:
|
|
56
|
+
// - owner/repo:plugin:type:name (capability from GitHub)
|
|
57
|
+
// - owner:custom:type:name (custom/curated capability)
|
|
58
|
+
// - owner/repo:plugin (plugin)
|
|
59
|
+
// - owner/plugin (simple)
|
|
60
|
+
|
|
61
|
+
const parts = fullName.split(':');
|
|
62
|
+
const [ownerRepo] = parts;
|
|
63
|
+
const [owner, repo] = ownerRepo.split('/');
|
|
64
|
+
|
|
65
|
+
if (parts.length === 4) {
|
|
66
|
+
const [, pluginName, capType, capName] = parts;
|
|
67
|
+
|
|
68
|
+
// Check if this is a custom capability (no slash in first part)
|
|
69
|
+
if (!repo) {
|
|
70
|
+
// owner:custom:type:name format (custom capability)
|
|
71
|
+
return path.join(
|
|
72
|
+
getCacheDir(),
|
|
73
|
+
owner,
|
|
74
|
+
pluginName, // 'custom'
|
|
75
|
+
capType,
|
|
76
|
+
capName,
|
|
77
|
+
version
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// owner/repo:plugin:type:name format (GitHub capability)
|
|
82
|
+
return path.join(
|
|
83
|
+
getCacheDir(),
|
|
84
|
+
owner,
|
|
85
|
+
repo,
|
|
86
|
+
pluginName,
|
|
87
|
+
capType,
|
|
88
|
+
capName,
|
|
89
|
+
version
|
|
90
|
+
);
|
|
91
|
+
} else if (parts.length === 2) {
|
|
92
|
+
// owner/repo:plugin
|
|
93
|
+
const [, pluginName] = parts;
|
|
94
|
+
return path.join(
|
|
95
|
+
getCacheDir(),
|
|
96
|
+
owner,
|
|
97
|
+
repo || 'default',
|
|
98
|
+
pluginName,
|
|
99
|
+
version
|
|
100
|
+
);
|
|
101
|
+
} else {
|
|
102
|
+
// owner/plugin (simple format)
|
|
103
|
+
return path.join(
|
|
104
|
+
getCacheDir(),
|
|
105
|
+
owner,
|
|
106
|
+
repo || 'default',
|
|
107
|
+
version
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function downloadFile(url: string, destPath: string): Promise<void> {
|
|
113
|
+
const response = await fetch(url);
|
|
114
|
+
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
throw new Error(`Download failed: ${response.status}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!response.body) {
|
|
120
|
+
throw new Error('No response body');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const fileStream = createWriteStream(destPath);
|
|
124
|
+
// Convert Web ReadableStream to Node stream
|
|
125
|
+
const reader = response.body.getReader();
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
while (true) {
|
|
129
|
+
const { done, value } = await reader.read();
|
|
130
|
+
if (done) break;
|
|
131
|
+
fileStream.write(value);
|
|
132
|
+
}
|
|
133
|
+
} finally {
|
|
134
|
+
reader.releaseLock();
|
|
135
|
+
fileStream.end();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Wait for file to be fully written
|
|
139
|
+
await new Promise<void>((resolve, reject) => {
|
|
140
|
+
fileStream.on('finish', resolve);
|
|
141
|
+
fileStream.on('error', reject);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function computeFileChecksum(filePath: string): Promise<string> {
|
|
146
|
+
const content = await fs.readFile(filePath);
|
|
147
|
+
return createHash('sha256').update(content).digest('hex');
|
|
148
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import { platform } from '../utils/platform';
|
|
6
|
+
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
|
|
9
|
+
export async function createLink(source: string, target: string): Promise<void> {
|
|
10
|
+
// Normalize paths and remove trailing slashes (important for symlink to work correctly)
|
|
11
|
+
const normalizedSource = source.replace(/\/+$/, '');
|
|
12
|
+
const normalizedTarget = target.replace(/\/+$/, '');
|
|
13
|
+
|
|
14
|
+
// Ensure target directory exists
|
|
15
|
+
await fs.mkdir(path.dirname(normalizedTarget), { recursive: true });
|
|
16
|
+
|
|
17
|
+
// If target already exists, remove it first
|
|
18
|
+
try {
|
|
19
|
+
await fs.rm(normalizedTarget, { recursive: true, force: true });
|
|
20
|
+
} catch {
|
|
21
|
+
// Ignore
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Choose link strategy based on platform
|
|
25
|
+
if (platform.isWindows) {
|
|
26
|
+
await createWindowsLink(normalizedSource, normalizedTarget);
|
|
27
|
+
} else {
|
|
28
|
+
await createPosixLink(normalizedSource, normalizedTarget);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function createPosixLink(source: string, target: string): Promise<void> {
|
|
33
|
+
try {
|
|
34
|
+
await fs.symlink(source, target, 'dir');
|
|
35
|
+
} catch (error) {
|
|
36
|
+
// symlink failed, fall back to copy
|
|
37
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
38
|
+
console.warn(`Symlink failed (${errorMsg}), falling back to copy`);
|
|
39
|
+
await copyDir(source, target);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function createWindowsLink(source: string, target: string): Promise<void> {
|
|
44
|
+
try {
|
|
45
|
+
// Try to create junction
|
|
46
|
+
await execAsync(`mklink /J "${target}" "${source}"`);
|
|
47
|
+
} catch {
|
|
48
|
+
// junction failed, fall back to copy
|
|
49
|
+
console.warn('Junction failed, falling back to copy');
|
|
50
|
+
await copyDir(source, target);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function copyDir(source: string, target: string): Promise<void> {
|
|
55
|
+
await fs.mkdir(target, { recursive: true });
|
|
56
|
+
|
|
57
|
+
const entries = await fs.readdir(source, { withFileTypes: true });
|
|
58
|
+
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
const srcPath = path.join(source, entry.name);
|
|
61
|
+
const destPath = path.join(target, entry.name);
|
|
62
|
+
|
|
63
|
+
if (entry.isDirectory()) {
|
|
64
|
+
await copyDir(srcPath, destPath);
|
|
65
|
+
} else {
|
|
66
|
+
await fs.copyFile(srcPath, destPath);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function removeLink(linkPath: string): Promise<void> {
|
|
72
|
+
try {
|
|
73
|
+
await fs.rm(linkPath, { recursive: true, force: true });
|
|
74
|
+
} catch {
|
|
75
|
+
// Ignore errors
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function isSymlink(linkPath: string): Promise<boolean> {
|
|
80
|
+
try {
|
|
81
|
+
const stats = await fs.lstat(linkPath);
|
|
82
|
+
return stats.isSymbolicLink();
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { nanoid } from 'nanoid';
|
|
3
|
+
import { getDb } from '../db/client';
|
|
4
|
+
import type { Project, ProjectPlugin } from '../types/db';
|
|
5
|
+
|
|
6
|
+
export async function getProject(projectPath: string): Promise<Project | null> {
|
|
7
|
+
const db = getDb();
|
|
8
|
+
const result = await db.execute({
|
|
9
|
+
sql: 'SELECT * FROM projects WHERE path = ?',
|
|
10
|
+
args: [projectPath],
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
if (result.rows.length === 0) return null;
|
|
14
|
+
return result.rows[0] as unknown as Project;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function getOrCreateProject(projectPath: string): Promise<Project> {
|
|
18
|
+
let project = await getProject(projectPath);
|
|
19
|
+
|
|
20
|
+
if (!project) {
|
|
21
|
+
const db = getDb();
|
|
22
|
+
const id = nanoid();
|
|
23
|
+
const name = path.basename(projectPath);
|
|
24
|
+
const now = new Date().toISOString();
|
|
25
|
+
|
|
26
|
+
await db.execute({
|
|
27
|
+
sql: `INSERT INTO projects (id, path, name, registered_at, last_used_at)
|
|
28
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
29
|
+
args: [id, projectPath, name, now, now],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
project = { id, path: projectPath, name, registered_at: now, last_used_at: now };
|
|
33
|
+
} else {
|
|
34
|
+
// Update last used time
|
|
35
|
+
const db = getDb();
|
|
36
|
+
await db.execute({
|
|
37
|
+
sql: 'UPDATE projects SET last_used_at = ? WHERE id = ?',
|
|
38
|
+
args: [new Date().toISOString(), project.id],
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return project;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function getProjectPlugins(projectId: string): Promise<ProjectPlugin[]> {
|
|
46
|
+
const db = getDb();
|
|
47
|
+
const result = await db.execute({
|
|
48
|
+
sql: 'SELECT * FROM project_plugins WHERE project_id = ?',
|
|
49
|
+
args: [projectId],
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return result.rows as unknown as ProjectPlugin[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function getProjectPlugin(projectId: string, fullName: string): Promise<ProjectPlugin | null> {
|
|
56
|
+
const db = getDb();
|
|
57
|
+
const result = await db.execute({
|
|
58
|
+
sql: 'SELECT * FROM project_plugins WHERE project_id = ? AND full_name = ?',
|
|
59
|
+
args: [projectId, fullName],
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (result.rows.length === 0) return null;
|
|
63
|
+
return result.rows[0] as unknown as ProjectPlugin;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function addProjectPlugin(plugin: {
|
|
67
|
+
project_id: string;
|
|
68
|
+
full_name: string;
|
|
69
|
+
type: string;
|
|
70
|
+
version: string;
|
|
71
|
+
cache_path: string;
|
|
72
|
+
link_path: string;
|
|
73
|
+
source?: string;
|
|
74
|
+
source_list?: string;
|
|
75
|
+
}): Promise<void> {
|
|
76
|
+
const db = getDb();
|
|
77
|
+
const id = nanoid();
|
|
78
|
+
const now = new Date().toISOString();
|
|
79
|
+
|
|
80
|
+
await db.execute({
|
|
81
|
+
sql: `INSERT INTO project_plugins
|
|
82
|
+
(id, project_id, full_name, type, version, cache_path, link_path, installed_at, source, source_list)
|
|
83
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
84
|
+
ON CONFLICT(project_id, full_name) DO UPDATE SET
|
|
85
|
+
id = excluded.id,
|
|
86
|
+
type = excluded.type,
|
|
87
|
+
version = excluded.version,
|
|
88
|
+
cache_path = excluded.cache_path,
|
|
89
|
+
link_path = excluded.link_path,
|
|
90
|
+
installed_at = excluded.installed_at,
|
|
91
|
+
source = excluded.source,
|
|
92
|
+
source_list = excluded.source_list`,
|
|
93
|
+
args: [
|
|
94
|
+
id,
|
|
95
|
+
plugin.project_id,
|
|
96
|
+
plugin.full_name,
|
|
97
|
+
plugin.type,
|
|
98
|
+
plugin.version,
|
|
99
|
+
plugin.cache_path,
|
|
100
|
+
plugin.link_path,
|
|
101
|
+
now,
|
|
102
|
+
plugin.source || 'direct',
|
|
103
|
+
plugin.source_list || null,
|
|
104
|
+
],
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function removeProjectPlugin(
|
|
109
|
+
projectId: string,
|
|
110
|
+
fullName: string
|
|
111
|
+
): Promise<ProjectPlugin | null> {
|
|
112
|
+
const plugin = await findProjectPlugin(projectId, fullName);
|
|
113
|
+
|
|
114
|
+
if (!plugin) return null;
|
|
115
|
+
|
|
116
|
+
const db = getDb();
|
|
117
|
+
await db.execute({
|
|
118
|
+
sql: 'DELETE FROM project_plugins WHERE id = ?',
|
|
119
|
+
args: [plugin.id],
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return plugin;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Find a project plugin by various identifier formats:
|
|
127
|
+
* - Full name: owner:custom:type:name or owner/repo:plugin:type:name
|
|
128
|
+
* - Slug format: author/slug (e.g., 42plugin/wechat-downloader)
|
|
129
|
+
* - Simple name: wechat-downloader
|
|
130
|
+
*/
|
|
131
|
+
export async function findProjectPlugin(
|
|
132
|
+
projectId: string,
|
|
133
|
+
identifier: string
|
|
134
|
+
): Promise<ProjectPlugin | null> {
|
|
135
|
+
const db = getDb();
|
|
136
|
+
|
|
137
|
+
// 1. Try exact full_name match first
|
|
138
|
+
let result = await db.execute({
|
|
139
|
+
sql: 'SELECT * FROM project_plugins WHERE project_id = ? AND full_name = ?',
|
|
140
|
+
args: [projectId, identifier],
|
|
141
|
+
});
|
|
142
|
+
if (result.rows.length > 0) {
|
|
143
|
+
return result.rows[0] as unknown as ProjectPlugin;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 2. Try matching by slug format (author/name -> find where full_name ends with :type:name)
|
|
147
|
+
if (identifier.includes('/') && !identifier.includes(':')) {
|
|
148
|
+
const [author, name] = identifier.split('/');
|
|
149
|
+
// Match pattern: {author}:custom:{type}:{name} or similar
|
|
150
|
+
result = await db.execute({
|
|
151
|
+
sql: `SELECT * FROM project_plugins
|
|
152
|
+
WHERE project_id = ?
|
|
153
|
+
AND (full_name LIKE ? OR full_name LIKE ?)`,
|
|
154
|
+
args: [projectId, `${author}:%:${name}`, `${author}/%:%:${name}`],
|
|
155
|
+
});
|
|
156
|
+
if (result.rows.length > 0) {
|
|
157
|
+
return result.rows[0] as unknown as ProjectPlugin;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 3. Try matching by simple name (last part of full_name)
|
|
162
|
+
result = await db.execute({
|
|
163
|
+
sql: `SELECT * FROM project_plugins
|
|
164
|
+
WHERE project_id = ?
|
|
165
|
+
AND full_name LIKE ?`,
|
|
166
|
+
args: [projectId, `%:${identifier}`],
|
|
167
|
+
});
|
|
168
|
+
if (result.rows.length > 0) {
|
|
169
|
+
return result.rows[0] as unknown as ProjectPlugin;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function getAllProjects(): Promise<Project[]> {
|
|
176
|
+
const db = getDb();
|
|
177
|
+
const result = await db.execute('SELECT * FROM projects ORDER BY last_used_at DESC');
|
|
178
|
+
return result.rows as unknown as Project[];
|
|
179
|
+
}
|
package/src/types/api.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export interface AuthStartResponse {
|
|
2
|
+
code: string;
|
|
3
|
+
auth_url: string;
|
|
4
|
+
expires_at: string;
|
|
5
|
+
poll_interval: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface AuthPollResponse {
|
|
9
|
+
status: 'pending' | 'completed';
|
|
10
|
+
access_token?: string;
|
|
11
|
+
refresh_token?: string;
|
|
12
|
+
expires_in?: number;
|
|
13
|
+
user?: {
|
|
14
|
+
username: string;
|
|
15
|
+
display_name: string | null;
|
|
16
|
+
role: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface UserProfile {
|
|
21
|
+
id: string;
|
|
22
|
+
username: string;
|
|
23
|
+
display_name: string | null;
|
|
24
|
+
email: string | null;
|
|
25
|
+
avatar_url: string | null;
|
|
26
|
+
role: string;
|
|
27
|
+
vip_expires_at: string | null;
|
|
28
|
+
verified: boolean;
|
|
29
|
+
created_at: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CapabilityDownload {
|
|
33
|
+
full_name: string;
|
|
34
|
+
version: string;
|
|
35
|
+
type: string;
|
|
36
|
+
download_url: string;
|
|
37
|
+
checksum: string;
|
|
38
|
+
size_bytes: number;
|
|
39
|
+
is_paid: boolean;
|
|
40
|
+
requires_auth: boolean;
|
|
41
|
+
expires_at?: string;
|
|
42
|
+
install_path: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface PluginDetail {
|
|
46
|
+
full_name: string;
|
|
47
|
+
name: string;
|
|
48
|
+
description: string | null;
|
|
49
|
+
version: string;
|
|
50
|
+
capabilities: Array<{
|
|
51
|
+
full_name: string;
|
|
52
|
+
name: string;
|
|
53
|
+
type: string;
|
|
54
|
+
description: string | null;
|
|
55
|
+
}>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ListDownload {
|
|
59
|
+
list: {
|
|
60
|
+
full_name: string;
|
|
61
|
+
name: string;
|
|
62
|
+
description: string | null;
|
|
63
|
+
price_tier: string | null;
|
|
64
|
+
effective_price_tier: string;
|
|
65
|
+
};
|
|
66
|
+
capabilities: Array<{
|
|
67
|
+
full_name: string;
|
|
68
|
+
name: string;
|
|
69
|
+
type: string;
|
|
70
|
+
version: string;
|
|
71
|
+
required: boolean;
|
|
72
|
+
reason: string | null;
|
|
73
|
+
download_url: string;
|
|
74
|
+
checksum: string;
|
|
75
|
+
size_bytes: number;
|
|
76
|
+
install_path: string;
|
|
77
|
+
}>;
|
|
78
|
+
summary: {
|
|
79
|
+
total_capabilities: number;
|
|
80
|
+
required_count: number;
|
|
81
|
+
optional_count: number;
|
|
82
|
+
total_size_bytes: number;
|
|
83
|
+
expires_at: string;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface SearchCapability {
|
|
88
|
+
full_name: string;
|
|
89
|
+
slug: string | null;
|
|
90
|
+
author_username: string | null;
|
|
91
|
+
short_name: string; // author/slug if available, otherwise full_name
|
|
92
|
+
name: string;
|
|
93
|
+
type: string;
|
|
94
|
+
description: string | null;
|
|
95
|
+
downloads: number;
|
|
96
|
+
relevance: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface SearchList {
|
|
100
|
+
full_name: string;
|
|
101
|
+
name: string;
|
|
102
|
+
capability_count: number;
|
|
103
|
+
downloads: number;
|
|
104
|
+
relevance: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface SearchResult {
|
|
108
|
+
capabilities: SearchCapability[];
|
|
109
|
+
lists: SearchList[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface SearchOptions {
|
|
113
|
+
type?: string;
|
|
114
|
+
per_page?: number;
|
|
115
|
+
}
|
package/src/types/db.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface Project {
|
|
2
|
+
id: string;
|
|
3
|
+
path: string;
|
|
4
|
+
name: string;
|
|
5
|
+
registered_at: string;
|
|
6
|
+
last_used_at: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface CacheEntry {
|
|
10
|
+
id: string;
|
|
11
|
+
full_name: string;
|
|
12
|
+
type: string;
|
|
13
|
+
version: string;
|
|
14
|
+
cache_path: string;
|
|
15
|
+
checksum: string;
|
|
16
|
+
size_bytes: number | null;
|
|
17
|
+
downloaded_at: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ProjectPlugin {
|
|
21
|
+
id: string;
|
|
22
|
+
project_id: string;
|
|
23
|
+
full_name: string;
|
|
24
|
+
type: string;
|
|
25
|
+
version: string;
|
|
26
|
+
cache_path: string;
|
|
27
|
+
link_path: string;
|
|
28
|
+
installed_at: string;
|
|
29
|
+
source: string;
|
|
30
|
+
source_list: string | null;
|
|
31
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export class CliError extends Error {
|
|
2
|
+
constructor(
|
|
3
|
+
message: string,
|
|
4
|
+
public code: string,
|
|
5
|
+
public exitCode: number = 1
|
|
6
|
+
) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'CliError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class AuthError extends CliError {
|
|
13
|
+
constructor(message: string) {
|
|
14
|
+
super(message, 'AUTH_ERROR', 1);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class NetworkError extends CliError {
|
|
19
|
+
constructor(message: string) {
|
|
20
|
+
super(message, 'NETWORK_ERROR', 1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class NotFoundError extends CliError {
|
|
25
|
+
constructor(resource: string) {
|
|
26
|
+
super(`Not found: ${resource}`, 'NOT_FOUND', 1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class PermissionError extends CliError {
|
|
31
|
+
constructor(message: string) {
|
|
32
|
+
super(message, 'PERMISSION_ERROR', 1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class ValidationError extends CliError {
|
|
37
|
+
constructor(message: string) {
|
|
38
|
+
super(message, 'VALIDATION_ERROR', 1);
|
|
39
|
+
}
|
|
40
|
+
}
|