@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.
@@ -0,0 +1,87 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { api } from '../services/api';
4
+
5
+ interface SearchOptions {
6
+ type?: string;
7
+ limit?: string;
8
+ json?: boolean;
9
+ }
10
+
11
+ export const searchCommand = new Command('search')
12
+ .description('搜索插件')
13
+ .argument('<query>', '搜索关键词')
14
+ .option('--type <type>', '筛选类型: skill | agent | command | hook | list')
15
+ .option('--limit <n>', '结果数量限制', '20')
16
+ .option('--json', 'JSON 格式输出')
17
+ .action(async (query: string, options: SearchOptions) => {
18
+ await search(query, options);
19
+ });
20
+
21
+ async function search(query: string, options: SearchOptions) {
22
+ try {
23
+ const results = await api.search(query, {
24
+ type: options.type,
25
+ per_page: Number(options.limit) || 20,
26
+ });
27
+
28
+ if (options.json) {
29
+ console.log(JSON.stringify(results, null, 2));
30
+ return;
31
+ }
32
+
33
+ // Display capability results
34
+ if (results.capabilities.length > 0) {
35
+ console.log();
36
+ console.log(chalk.bold('能力:'));
37
+ console.log();
38
+
39
+ for (const cap of results.capabilities) {
40
+ const typeColor = getTypeColor(cap.type);
41
+ // Use short_name (author/slug) for display, making it easy to copy for install
42
+ const displayName = cap.short_name;
43
+ console.log(` ${typeColor(cap.type.padEnd(7))} ${chalk.cyan(displayName)}`);
44
+ if (cap.description) {
45
+ console.log(` ${chalk.gray(truncate(cap.description, 60))}`);
46
+ }
47
+ console.log(` ${chalk.gray(`下载: ${cap.downloads}`)} ${chalk.dim(`安装: 42plugin install ${displayName}`)}`);
48
+ console.log();
49
+ }
50
+ }
51
+
52
+ // Display list results
53
+ if (results.lists.length > 0) {
54
+ console.log();
55
+ console.log(chalk.bold('列表:'));
56
+ console.log();
57
+
58
+ for (const list of results.lists) {
59
+ console.log(` ${chalk.magenta('list')} ${chalk.cyan(list.full_name)}`);
60
+ console.log(` ${list.name}`);
61
+ console.log(` ${chalk.gray(`${list.capability_count} 个能力 · 下载: ${list.downloads}`)}`);
62
+ console.log();
63
+ }
64
+ }
65
+
66
+ if (results.capabilities.length === 0 && results.lists.length === 0) {
67
+ console.log(chalk.yellow('未找到结果'));
68
+ }
69
+ } catch (error) {
70
+ console.error(chalk.red(`搜索失败: ${(error as Error).message}`));
71
+ process.exit(1);
72
+ }
73
+ }
74
+
75
+ function getTypeColor(type: string): (text: string) => string {
76
+ const colors: Record<string, (text: string) => string> = {
77
+ skill: chalk.blue,
78
+ agent: chalk.green,
79
+ command: chalk.yellow,
80
+ hook: chalk.magenta,
81
+ };
82
+ return colors[type] || chalk.white;
83
+ }
84
+
85
+ function truncate(str: string, len: number): string {
86
+ return str.length > len ? str.slice(0, len - 3) + '...' : str;
87
+ }
@@ -0,0 +1,58 @@
1
+ import { Command } from 'commander';
2
+ import fs from 'fs/promises';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import { getProject, removeProjectPlugin } from '../services/project';
6
+ import { removeFromCache } from '../services/cache';
7
+
8
+ interface UninstallOptions {
9
+ purge?: boolean;
10
+ }
11
+
12
+ export const uninstallCommand = new Command('uninstall')
13
+ .description('卸载插件')
14
+ .argument('<plugin>', '插件标识符')
15
+ .option('--purge', '同时删除缓存')
16
+ .action(async (plugin: string, options: UninstallOptions) => {
17
+ await uninstall(plugin, options);
18
+ });
19
+
20
+ async function uninstall(plugin: string, options: UninstallOptions) {
21
+ const spinner = ora(`卸载 ${plugin}...`).start();
22
+
23
+ try {
24
+ const projectPath = process.cwd();
25
+ const project = await getProject(projectPath);
26
+
27
+ if (!project) {
28
+ spinner.fail('当前目录不是 42plugin 项目');
29
+ return;
30
+ }
31
+
32
+ // Get install record
33
+ const record = await removeProjectPlugin(project.id, plugin);
34
+
35
+ if (!record) {
36
+ spinner.fail(`未找到已安装的插件: ${plugin}`);
37
+ return;
38
+ }
39
+
40
+ // Remove link (normalize path by removing trailing slash)
41
+ try {
42
+ const linkPath = record.link_path.replace(/\/+$/, '');
43
+ await fs.rm(linkPath, { recursive: true, force: true });
44
+ } catch {
45
+ // Ignore deletion errors
46
+ }
47
+
48
+ // If --purge specified, also remove from cache
49
+ if (options.purge) {
50
+ await removeFromCache(plugin, record.version);
51
+ }
52
+
53
+ spinner.succeed(`${chalk.green('已卸载')} ${plugin}`);
54
+ } catch (error) {
55
+ spinner.fail(`卸载失败: ${(error as Error).message}`);
56
+ process.exit(1);
57
+ }
58
+ }
@@ -0,0 +1,20 @@
1
+ import { Command } from 'commander';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ export const versionCommand = new Command('version')
7
+ .description('显示版本信息')
8
+ .action(() => {
9
+ // Try to read version from package.json
10
+ try {
11
+ // Get the directory of the current module
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+ const pkgPath = path.resolve(__dirname, '../../package.json');
14
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
15
+ console.log(`42plugin CLI v${pkg.version}`);
16
+ } catch {
17
+ // Fallback if package.json is not readable
18
+ console.log('42plugin CLI v0.1.0');
19
+ }
20
+ });
package/src/config.ts ADDED
@@ -0,0 +1,44 @@
1
+ import path from 'path';
2
+ import os from 'os';
3
+ import fs from 'fs';
4
+
5
+ let dataDir: string | null = null;
6
+
7
+ export function getDataDir(): string {
8
+ if (dataDir) return dataDir;
9
+
10
+ if (process.platform === 'win32') {
11
+ dataDir = path.join(process.env.APPDATA || os.homedir(), '42plugin');
12
+ } else {
13
+ dataDir = path.join(os.homedir(), '.42plugin');
14
+ }
15
+
16
+ // Ensure directory exists
17
+ if (!fs.existsSync(dataDir)) {
18
+ fs.mkdirSync(dataDir, { recursive: true });
19
+ }
20
+
21
+ return dataDir;
22
+ }
23
+
24
+ export function getCacheDir(): string {
25
+ const cacheDir = path.join(getDataDir(), 'cache');
26
+ if (!fs.existsSync(cacheDir)) {
27
+ fs.mkdirSync(cacheDir, { recursive: true });
28
+ }
29
+ return cacheDir;
30
+ }
31
+
32
+ export function getDbPath(): string {
33
+ return path.join(getDataDir(), 'local.db');
34
+ }
35
+
36
+ export function getSecretsPath(): string {
37
+ return path.join(getDataDir(), 'secrets.json');
38
+ }
39
+
40
+ export const config = {
41
+ apiBase: process.env.API_BASE || 'https://api.42plugin.com',
42
+ cdnBase: process.env.CDN_BASE || 'https://cdn.42plugin.com',
43
+ debug: process.env.DEBUG === 'true',
44
+ };
@@ -0,0 +1,180 @@
1
+ import { createClient as createLibsqlClient } from '@libsql/client';
2
+ import { Pool } from 'pg';
3
+ import { getDbPath } from '../config';
4
+
5
+ type Query = string | { sql: string; args?: unknown[] };
6
+
7
+ export interface DbClient {
8
+ execute(query: Query): Promise<{ rows: unknown[] }>;
9
+ close(): Promise<void> | void;
10
+ }
11
+
12
+ let client: DbClient | null = null;
13
+
14
+ function normalizeQuery(query: Query): { sql: string; args: unknown[] } {
15
+ if (typeof query === 'string') {
16
+ return { sql: query, args: [] };
17
+ }
18
+ return {
19
+ sql: query.sql,
20
+ args: query.args || [],
21
+ };
22
+ }
23
+
24
+ function convertParamsToPg(sql: string): string {
25
+ let paramIndex = 0;
26
+ return sql.replace(/\?/g, () => {
27
+ paramIndex += 1;
28
+ return `$${paramIndex}`;
29
+ });
30
+ }
31
+
32
+ function createSqliteClient(): DbClient {
33
+ const sqliteClient = createLibsqlClient({
34
+ url: `file:${getDbPath()}`,
35
+ });
36
+
37
+ return {
38
+ execute: (query) => sqliteClient.execute(query as any),
39
+ close: () => sqliteClient.close(),
40
+ };
41
+ }
42
+
43
+ function createPostgresClient(): DbClient {
44
+ const connectionString =
45
+ process.env.CLI_DATABASE_URL ||
46
+ process.env.LOCAL_DATABASE_URL ||
47
+ process.env.DATABASE_URL ||
48
+ process.env.POSTGRES_URL;
49
+
50
+ if (!connectionString) {
51
+ throw new Error(
52
+ 'Postgres connection string not provided. Set CLI_DATABASE_URL, LOCAL_DATABASE_URL, or DATABASE_URL.'
53
+ );
54
+ }
55
+
56
+ const pool = new Pool({ connectionString });
57
+
58
+ return {
59
+ async execute(query) {
60
+ const { sql, args } = normalizeQuery(query);
61
+ const text = convertParamsToPg(sql);
62
+ const res = await pool.query(text, args);
63
+ return { rows: res.rows };
64
+ },
65
+ async close() {
66
+ await pool.end();
67
+ },
68
+ };
69
+ }
70
+
71
+ function createDbClient(): DbClient {
72
+ const driver = (process.env.CLI_DB_DRIVER || process.env.DB_DRIVER || 'sqlite').toLowerCase();
73
+ if (driver === 'postgres' || driver === 'postgresql' || driver === 'pg') {
74
+ return createPostgresClient();
75
+ }
76
+ return createSqliteClient();
77
+ }
78
+
79
+ export function getDb(): DbClient {
80
+ if (!client) {
81
+ client = createDbClient();
82
+ }
83
+ return client;
84
+ }
85
+
86
+ // Future extension: Support remote sync (Turso); fallback to current driver for Postgres
87
+ export function getDbWithSync(authToken: string): DbClient {
88
+ const driver = (process.env.CLI_DB_DRIVER || process.env.DB_DRIVER || 'sqlite').toLowerCase();
89
+
90
+ if (driver === 'postgres' || driver === 'postgresql' || driver === 'pg') {
91
+ return getDb();
92
+ }
93
+
94
+ const sqliteClient = createLibsqlClient({
95
+ url: `file:${getDbPath()}`,
96
+ syncUrl: process.env.TURSO_DATABASE_URL,
97
+ authToken,
98
+ syncInterval: 60, // Sync every 60 seconds
99
+ });
100
+
101
+ return {
102
+ execute: (query) => sqliteClient.execute(query as any),
103
+ close: () => sqliteClient.close(),
104
+ };
105
+ }
106
+
107
+ export async function initDb(): Promise<void> {
108
+ const db = getDb();
109
+
110
+ // Create projects table
111
+ await db.execute(`
112
+ CREATE TABLE IF NOT EXISTS projects (
113
+ id TEXT PRIMARY KEY,
114
+ path TEXT NOT NULL UNIQUE,
115
+ name TEXT,
116
+ registered_at TEXT NOT NULL,
117
+ last_used_at TEXT NOT NULL
118
+ )
119
+ `);
120
+
121
+ await db.execute(`
122
+ CREATE INDEX IF NOT EXISTS idx_projects_path ON projects(path)
123
+ `);
124
+
125
+ // Create plugin_cache table
126
+ await db.execute(`
127
+ CREATE TABLE IF NOT EXISTS plugin_cache (
128
+ id TEXT PRIMARY KEY,
129
+ full_name TEXT NOT NULL,
130
+ type TEXT NOT NULL,
131
+ version TEXT NOT NULL,
132
+ cache_path TEXT NOT NULL,
133
+ checksum TEXT NOT NULL,
134
+ size_bytes INTEGER,
135
+ downloaded_at TEXT NOT NULL,
136
+ UNIQUE(full_name, version)
137
+ )
138
+ `);
139
+
140
+ await db.execute(`
141
+ CREATE INDEX IF NOT EXISTS idx_cache_name ON plugin_cache(full_name)
142
+ `);
143
+
144
+ // Create project_plugins table
145
+ await db.execute(`
146
+ CREATE TABLE IF NOT EXISTS project_plugins (
147
+ id TEXT PRIMARY KEY,
148
+ project_id TEXT NOT NULL,
149
+ full_name TEXT NOT NULL,
150
+ type TEXT NOT NULL,
151
+ version TEXT NOT NULL,
152
+ cache_path TEXT NOT NULL,
153
+ link_path TEXT NOT NULL,
154
+ installed_at TEXT NOT NULL,
155
+ source TEXT DEFAULT 'direct',
156
+ source_list TEXT,
157
+ UNIQUE(project_id, full_name),
158
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
159
+ )
160
+ `);
161
+
162
+ await db.execute(`
163
+ CREATE INDEX IF NOT EXISTS idx_pp_project ON project_plugins(project_id)
164
+ `);
165
+
166
+ // Create config table
167
+ await db.execute(`
168
+ CREATE TABLE IF NOT EXISTS config (
169
+ key TEXT PRIMARY KEY,
170
+ value TEXT NOT NULL
171
+ )
172
+ `);
173
+ }
174
+
175
+ export async function closeDb(): Promise<void> {
176
+ if (client) {
177
+ await client.close();
178
+ client = null;
179
+ }
180
+ }
package/src/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { program } from './cli';
4
+ import { initDb } from './db/client';
5
+ import { api } from './services/api';
6
+ import { CliError } from './utils/errors';
7
+ import chalk from 'chalk';
8
+
9
+ async function main() {
10
+ try {
11
+ // Initialize database
12
+ await initDb();
13
+
14
+ // Initialize API client
15
+ await api.init();
16
+
17
+ // Parse and execute command
18
+ await program.parseAsync(process.argv);
19
+ } catch (error) {
20
+ if (error instanceof CliError) {
21
+ console.error(chalk.red(`错误: ${error.message}`));
22
+ process.exit(error.exitCode);
23
+ }
24
+
25
+ // Unknown error
26
+ if (process.env.DEBUG) {
27
+ console.error(error);
28
+ } else {
29
+ console.error(chalk.red(`发生错误: ${(error as Error).message}`));
30
+ }
31
+ process.exit(1);
32
+ }
33
+ }
34
+
35
+ main();
@@ -0,0 +1,128 @@
1
+ import { loadSecrets } from './auth';
2
+ import { config } from '../config';
3
+ import type {
4
+ AuthStartResponse,
5
+ AuthPollResponse,
6
+ UserProfile,
7
+ CapabilityDownload,
8
+ PluginDetail,
9
+ ListDownload,
10
+ SearchResult,
11
+ SearchOptions,
12
+ } from '../types/api';
13
+
14
+ class ApiClient {
15
+ private accessToken: string | null = null;
16
+ private initialized = false;
17
+
18
+ async init(): Promise<void> {
19
+ if (this.initialized) return;
20
+ const secrets = await loadSecrets();
21
+ this.accessToken = secrets?.access_token || null;
22
+ this.initialized = true;
23
+ }
24
+
25
+ isAuthenticated(): boolean {
26
+ return !!this.accessToken;
27
+ }
28
+
29
+ private async request<T>(
30
+ method: string,
31
+ path: string,
32
+ options: {
33
+ body?: unknown;
34
+ auth?: boolean;
35
+ } = {}
36
+ ): Promise<T> {
37
+ if (!this.initialized) {
38
+ await this.init();
39
+ }
40
+
41
+ const headers: Record<string, string> = {
42
+ 'Content-Type': 'application/json',
43
+ };
44
+
45
+ if (options.auth !== false && this.accessToken) {
46
+ headers['Authorization'] = `Bearer ${this.accessToken}`;
47
+ }
48
+
49
+ const response = await fetch(`${config.apiBase}${path}`, {
50
+ method,
51
+ headers,
52
+ body: method === 'POST' || method === 'PUT' || method === 'PATCH'
53
+ ? JSON.stringify(options.body ?? {})
54
+ : undefined,
55
+ });
56
+
57
+ const data = await response.json() as { data?: T; error?: { message: string } };
58
+
59
+ if (!response.ok) {
60
+ throw new Error(data.error?.message || 'API request failed');
61
+ }
62
+
63
+ return data.data as T;
64
+ }
65
+
66
+ // Auth
67
+ async startAuth(): Promise<AuthStartResponse> {
68
+ return this.request('POST', '/v1/auth/cli/start', { auth: false });
69
+ }
70
+
71
+ async pollAuth(code: string): Promise<AuthPollResponse> {
72
+ return this.request('GET', `/v1/auth/cli/poll?code=${code}`, { auth: false });
73
+ }
74
+
75
+ async getMe(): Promise<UserProfile> {
76
+ return this.request('GET', '/v1/auth/me');
77
+ }
78
+
79
+ // Capabilities
80
+ async getCapabilityDownload(fullName: string): Promise<CapabilityDownload> {
81
+ const encoded = encodeURIComponent(fullName);
82
+ return this.request('GET', `/v1/capabilities/by-name/download?full_name=${encoded}`);
83
+ }
84
+
85
+ async getCapabilityDownloadBySlug(author: string, slug: string): Promise<CapabilityDownload> {
86
+ return this.request('GET', `/v1/capabilities/${author}/${slug}/download`);
87
+ }
88
+
89
+ // Plugins
90
+ async getPluginByName(fullName: string): Promise<PluginDetail> {
91
+ const encoded = encodeURIComponent(fullName);
92
+ return this.request('GET', `/v1/plugins/by-name?full_name=${encoded}`);
93
+ }
94
+
95
+ async getPluginDownload(user: string, plugin: string, version?: string): Promise<CapabilityDownload> {
96
+ let path = `/v1/plugins/${user}/${plugin}/download`;
97
+ if (version) {
98
+ path += `?version=${version}`;
99
+ }
100
+ return this.request('GET', path);
101
+ }
102
+
103
+ // Lists
104
+ async getListDownload(username: string, slugOrId: string): Promise<ListDownload> {
105
+ return this.request('GET', `/v1/lists/${username}/list/${slugOrId}/download`);
106
+ }
107
+
108
+ // Search
109
+ async search(query: string, options?: SearchOptions): Promise<SearchResult> {
110
+ const params = new URLSearchParams({ q: query });
111
+ if (options?.type) params.set('type', options.type);
112
+ if (options?.per_page) params.set('per_page', String(options.per_page));
113
+ return this.request('GET', `/v1/search?${params}`);
114
+ }
115
+
116
+ // Reset token (for logout)
117
+ resetToken(): void {
118
+ this.accessToken = null;
119
+ this.initialized = false;
120
+ }
121
+
122
+ // Set token (for login)
123
+ setToken(token: string): void {
124
+ this.accessToken = token;
125
+ }
126
+ }
127
+
128
+ export const api = new ApiClient();
@@ -0,0 +1,46 @@
1
+ import fs from 'fs/promises';
2
+ import { getSecretsPath } from '../config';
3
+
4
+ const SECRETS_FILE = 'secrets.json';
5
+
6
+ export interface Secrets {
7
+ access_token: string;
8
+ refresh_token: string;
9
+ created_at: string;
10
+ }
11
+
12
+ export async function saveSecrets(secrets: Secrets): Promise<void> {
13
+ const filePath = getSecretsPath();
14
+
15
+ await fs.writeFile(
16
+ filePath,
17
+ JSON.stringify(secrets, null, 2),
18
+ { mode: 0o600 } // Only current user can read/write
19
+ );
20
+ }
21
+
22
+ export async function loadSecrets(): Promise<Secrets | null> {
23
+ const filePath = getSecretsPath();
24
+
25
+ try {
26
+ const content = await fs.readFile(filePath, 'utf-8');
27
+ return JSON.parse(content);
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ export async function clearSecrets(): Promise<void> {
34
+ const filePath = getSecretsPath();
35
+
36
+ try {
37
+ await fs.rm(filePath);
38
+ } catch {
39
+ // File doesn't exist, ignore
40
+ }
41
+ }
42
+
43
+ export async function getAccessToken(): Promise<string | null> {
44
+ const secrets = await loadSecrets();
45
+ return secrets?.access_token || null;
46
+ }
@@ -0,0 +1,101 @@
1
+ import fs from 'fs/promises';
2
+ import { nanoid } from 'nanoid';
3
+ import { getDb } from '../db/client';
4
+ import type { CacheEntry } from '../types/db';
5
+
6
+ export async function getCachedPlugin(fullName: string, version: string): Promise<CacheEntry | null> {
7
+ const db = getDb();
8
+ const result = await db.execute({
9
+ sql: 'SELECT * FROM plugin_cache WHERE full_name = ? AND version = ?',
10
+ args: [fullName, version],
11
+ });
12
+
13
+ if (result.rows.length === 0) return null;
14
+ return result.rows[0] as unknown as CacheEntry;
15
+ }
16
+
17
+ export async function cachePlugin(entry: {
18
+ full_name: string;
19
+ type: string;
20
+ version: string;
21
+ cache_path: string;
22
+ checksum: string;
23
+ }): Promise<void> {
24
+ const db = getDb();
25
+ const id = nanoid();
26
+ const now = new Date().toISOString();
27
+
28
+ // Calculate directory size
29
+ let sizeBytes: number | null = null;
30
+ try {
31
+ sizeBytes = await getDirSize(entry.cache_path);
32
+ } catch {
33
+ // Ignore
34
+ }
35
+
36
+ await db.execute({
37
+ sql: `INSERT INTO plugin_cache
38
+ (id, full_name, type, version, cache_path, checksum, size_bytes, downloaded_at)
39
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
40
+ ON CONFLICT(full_name, version) DO UPDATE SET
41
+ id = excluded.id,
42
+ type = excluded.type,
43
+ cache_path = excluded.cache_path,
44
+ checksum = excluded.checksum,
45
+ size_bytes = excluded.size_bytes,
46
+ downloaded_at = excluded.downloaded_at`,
47
+ args: [
48
+ id,
49
+ entry.full_name,
50
+ entry.type,
51
+ entry.version,
52
+ entry.cache_path,
53
+ entry.checksum,
54
+ sizeBytes,
55
+ now,
56
+ ],
57
+ });
58
+ }
59
+
60
+ export async function removeFromCache(fullName: string, version: string): Promise<void> {
61
+ const entry = await getCachedPlugin(fullName, version);
62
+
63
+ if (entry) {
64
+ // Delete files
65
+ try {
66
+ await fs.rm(entry.cache_path, { recursive: true, force: true });
67
+ } catch {
68
+ // Ignore
69
+ }
70
+
71
+ // Delete record
72
+ const db = getDb();
73
+ await db.execute({
74
+ sql: 'DELETE FROM plugin_cache WHERE id = ?',
75
+ args: [entry.id],
76
+ });
77
+ }
78
+ }
79
+
80
+ export async function getAllCachedPlugins(): Promise<CacheEntry[]> {
81
+ const db = getDb();
82
+ const result = await db.execute('SELECT * FROM plugin_cache ORDER BY downloaded_at DESC');
83
+ return result.rows as unknown as CacheEntry[];
84
+ }
85
+
86
+ async function getDirSize(dirPath: string): Promise<number> {
87
+ let size = 0;
88
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
89
+
90
+ for (const entry of entries) {
91
+ const fullPath = `${dirPath}/${entry.name}`;
92
+ if (entry.isDirectory()) {
93
+ size += await getDirSize(fullPath);
94
+ } else {
95
+ const stats = await fs.stat(fullPath);
96
+ size += stats.size;
97
+ }
98
+ }
99
+
100
+ return size;
101
+ }