@zshuangmu/agenthub 0.1.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,9 @@
1
+ import path from "node:path";
2
+ import { createServer } from "../server.js";
3
+
4
+ export async function serveCommand(options) {
5
+ const registryDir = path.resolve(options.registry);
6
+ const port = options.port ? Number(options.port) : 3000;
7
+ const host = options.host || "0.0.0.0";
8
+ return createServer({ registryDir, port, host });
9
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Stats Command
3
+ * 查看 Agent 统计信息
4
+ */
5
+
6
+ import path from "node:path";
7
+ import { pathExists, readJson } from "../lib/fs-utils.js";
8
+
9
+ export async function statsCommand(agentSpec, options) {
10
+ const registryDir = path.resolve(options.registry);
11
+ const [slug] = agentSpec.split(":");
12
+
13
+ const indexPath = path.join(registryDir, "index.json");
14
+ if (!(await pathExists(indexPath))) {
15
+ throw new Error(`Agent not found: ${slug}`);
16
+ }
17
+
18
+ const index = await readJson(indexPath);
19
+ const agentEntries = index.agents.filter((entry) => entry.slug === slug);
20
+
21
+ if (agentEntries.length === 0) {
22
+ throw new Error(`Agent not found: ${slug}`);
23
+ }
24
+
25
+ // 获取最新版本的完整信息
26
+ const latestEntry = agentEntries.sort((a, b) =>
27
+ b.version.localeCompare(a.version, undefined, { numeric: true })
28
+ )[0];
29
+
30
+ // 读取完整 MANIFEST
31
+ const manifestPath = path.join(
32
+ registryDir,
33
+ "agents",
34
+ slug,
35
+ latestEntry.version,
36
+ "MANIFEST.json"
37
+ );
38
+ const manifest = await readJson(manifestPath);
39
+
40
+ // 统计信息
41
+ const stats = {
42
+ slug,
43
+ name: manifest.name,
44
+ latestVersion: latestEntry.version,
45
+ totalVersions: agentEntries.length,
46
+ description: manifest.description,
47
+ author: manifest.author,
48
+ runtime: manifest.runtime,
49
+ includes: manifest.includes,
50
+ requirements: manifest.requirements,
51
+ metadata: manifest.metadata,
52
+ // 以下数据在实际系统中会从数据库获取
53
+ downloads: manifest.stats?.installs || 0,
54
+ stars: manifest.stats?.stars || 0,
55
+ rating: manifest.stats?.rating || null,
56
+ };
57
+
58
+ return stats;
59
+ }
60
+
61
+ export function formatStatsOutput(stats) {
62
+ const lines = [];
63
+ lines.push("\n╔═══════════════════════════════════════════════════════════════╗");
64
+ lines.push("║ Agent Statistics ║");
65
+ lines.push("╠═══════════════════════════════════════════════════════════════╣");
66
+ lines.push(`║ Name: ${stats.name.substring(0, 44).padEnd(44)}║`);
67
+ lines.push(`║ Slug: ${stats.slug.padEnd(44)}║`);
68
+ lines.push(`║ Version: ${stats.latestVersion.padEnd(44)}║`);
69
+ lines.push(`║ Author: ${(stats.author || "unknown").padEnd(44)}║`);
70
+ lines.push("╠═══════════════════════════════════════════════════════════════╣");
71
+ lines.push("║ Statistics: ║");
72
+ lines.push(`║ Downloads: ${String(stats.downloads).padEnd(44)}║`);
73
+ lines.push(`║ Stars: ${String(stats.stars).padEnd(44)}║`);
74
+ lines.push(`║ Versions: ${String(stats.totalVersions).padEnd(44)}║`);
75
+ if (stats.rating) {
76
+ lines.push(`║ Rating: ${`${stats.rating}/5.0`.padEnd(44)}║`);
77
+ }
78
+ lines.push("╠═══════════════════════════════════════════════════════════════╣");
79
+ lines.push("║ Content: ║");
80
+ const memory = stats.includes?.memory || {};
81
+ lines.push(`║ Memory: ${`共 ${memory.count || 0} 条 (public: ${memory.public || 0}, portable: ${memory.portable || 0})`.padEnd(42)}║`);
82
+ if (stats.includes?.skills?.length > 0) {
83
+ lines.push(`║ Skills: ${stats.includes.skills.join(", ").substring(0, 42).padEnd(42)}║`);
84
+ }
85
+ lines.push("╠═══════════════════════════════════════════════════════════════╣");
86
+ lines.push(`║ Runtime: ${`${stats.runtime?.type || "openclaw"} ${stats.runtime?.version || ""}`.padEnd(44)}║`);
87
+ lines.push("╚═══════════════════════════════════════════════════════════════╝");
88
+
89
+ return lines.join("\n");
90
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Update Command
3
+ * 更新已安装的 Agent 到最新版本
4
+ */
5
+
6
+ import path from "node:path";
7
+ import { pathExists, readJson, writeJson } from "../lib/fs-utils.js";
8
+ import { installBundle } from "../lib/install.js";
9
+ import { versionsCommand } from "./versions.js";
10
+
11
+ export async function updateCommand(agentSpec, options) {
12
+ const registryDir = path.resolve(options.registry);
13
+ const targetWorkspace = options.targetWorkspace ? path.resolve(options.targetWorkspace) : null;
14
+ const [slug] = agentSpec.split(":");
15
+
16
+ // 获取可用版本
17
+ const versions = await versionsCommand(slug, { registry: registryDir });
18
+ if (versions.length === 0) {
19
+ throw new Error(`Agent not found: ${slug}`);
20
+ }
21
+
22
+ const latestVersion = versions[0].version;
23
+
24
+ // 获取当前安装版本
25
+ let currentVersion = null;
26
+ if (targetWorkspace) {
27
+ const installRecordPath = path.join(targetWorkspace, ".agenthub", "install.json");
28
+ if (await pathExists(installRecordPath)) {
29
+ const record = await readJson(installRecordPath);
30
+ currentVersion = record.version;
31
+ }
32
+ }
33
+
34
+ if (currentVersion === latestVersion) {
35
+ return {
36
+ updated: false,
37
+ message: `已是最新版本: ${latestVersion}`,
38
+ currentVersion,
39
+ latestVersion,
40
+ };
41
+ }
42
+
43
+ // 执行更新
44
+ const result = await installBundle({
45
+ registryDir,
46
+ agentSpec: `${slug}:${latestVersion}`,
47
+ targetWorkspace,
48
+ });
49
+
50
+ // 记录更新
51
+ if (targetWorkspace) {
52
+ const installRecordPath = path.join(targetWorkspace, ".agenthub", "install.json");
53
+ await writeJson(installRecordPath, {
54
+ slug,
55
+ version: latestVersion,
56
+ updatedAt: new Date().toISOString(),
57
+ previousVersion: currentVersion,
58
+ });
59
+ }
60
+
61
+ return {
62
+ updated: true,
63
+ message: `已更新 ${slug} 从 ${currentVersion || "未知"} 到 ${latestVersion}`,
64
+ currentVersion: latestVersion,
65
+ previousVersion: currentVersion,
66
+ manifest: result.manifest,
67
+ };
68
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Versions Command
3
+ * 查看 Agent 的所有可用版本
4
+ */
5
+
6
+ import { pathExists, readJson } from "../lib/fs-utils.js";
7
+ import path from "node:path";
8
+
9
+ export async function versionsCommand(agentSpec, options) {
10
+ const registryDir = path.resolve(options.registry);
11
+ const [slug] = agentSpec.split(":");
12
+
13
+ const indexPath = path.join(registryDir, "index.json");
14
+ if (!(await pathExists(indexPath))) {
15
+ return [];
16
+ }
17
+
18
+ const index = await readJson(indexPath);
19
+ const versions = index.agents
20
+ .filter((entry) => entry.slug === slug)
21
+ .map((entry) => ({
22
+ version: entry.version,
23
+ name: entry.name,
24
+ description: entry.description,
25
+ updatedAt: entry.updatedAt || entry.createdAt,
26
+ }))
27
+ .sort((a, b) => b.version.localeCompare(a.version, undefined, { numeric: true }));
28
+
29
+ return versions;
30
+ }
31
+
32
+ export function formatVersionsOutput(slug, versions, currentVersion = null) {
33
+ const lines = [];
34
+ lines.push(`\n📋 ${slug} 可用版本:\n`);
35
+ lines.push("─".repeat(50));
36
+
37
+ for (const v of versions) {
38
+ const isLatest = v === versions[0];
39
+ const isCurrent = currentVersion && v.version === currentVersion;
40
+ const marker = isCurrent ? "✓" : isLatest ? "*" : " ";
41
+ const latestTag = isLatest ? " (latest)" : "";
42
+ const currentTag = isCurrent ? " (installed)" : "";
43
+
44
+ lines.push(` ${marker} ${v.version}${latestTag}${currentTag}`);
45
+ if (v.description) {
46
+ lines.push(` ${v.description.substring(0, 60)}`);
47
+ }
48
+ }
49
+
50
+ lines.push("─".repeat(50));
51
+
52
+ if (currentVersion) {
53
+ lines.push(`\n当前安装: ${currentVersion}`);
54
+ if (versions.length > 0 && versions[0].version !== currentVersion) {
55
+ lines.push(`可更新到: ${versions[0].version}`);
56
+ lines.push(`\n更新命令: agenthub update ${slug}`);
57
+ }
58
+ } else {
59
+ lines.push(`\n安装命令: agenthub install ${slug}`);
60
+ }
61
+
62
+ return lines.join("\n");
63
+ }
@@ -0,0 +1,8 @@
1
+ import { createWebServer } from "../web-server.js";
2
+
3
+ export async function webCommand(options) {
4
+ const port = parseInt(options.port || "3000", 10);
5
+ const apiBase = options.apiBase || "http://127.0.0.1:3001";
6
+
7
+ return createWebServer({ port, apiBase });
8
+ }
package/src/index.js ADDED
@@ -0,0 +1,14 @@
1
+ export { packCommand } from "./commands/pack.js";
2
+ export { publishCommand } from "./commands/publish.js";
3
+ export { publishRemoteCommand } from "./commands/publish-remote.js";
4
+ export { searchCommand } from "./commands/search.js";
5
+ export { infoCommand } from "./commands/info.js";
6
+ export { installCommand } from "./commands/install.js";
7
+ export { serveCommand } from "./commands/serve.js";
8
+ export { versionsCommand, formatVersionsOutput } from "./commands/versions.js";
9
+ export { updateCommand } from "./commands/update.js";
10
+ export { rollbackCommand } from "./commands/rollback.js";
11
+ export { statsCommand, formatStatsOutput } from "./commands/stats.js";
12
+ export { listCommand, formatListOutput } from "./commands/list.js";
13
+ export { apiCommand } from "./commands/api.js";
14
+ export { webCommand } from "./commands/web.js";
@@ -0,0 +1,58 @@
1
+ import { mkdtemp, readdir, readFile, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+ import { ensureDir } from "./fs-utils.js";
5
+ import { publishBundle } from "./registry.js";
6
+
7
+ async function walk(dirPath, baseDir = dirPath) {
8
+ const entries = await readdir(dirPath, { withFileTypes: true });
9
+ const files = [];
10
+ for (const entry of entries) {
11
+ const fullPath = path.join(dirPath, entry.name);
12
+ if (entry.isDirectory()) {
13
+ files.push(...(await walk(fullPath, baseDir)));
14
+ } else {
15
+ files.push({
16
+ path: path.relative(baseDir, fullPath),
17
+ contentBase64: (await readFile(fullPath)).toString("base64"),
18
+ });
19
+ }
20
+ }
21
+ return files;
22
+ }
23
+
24
+ export async function serializeBundleDir(bundleDir) {
25
+ return {
26
+ bundleName: path.basename(bundleDir),
27
+ files: await walk(bundleDir),
28
+ };
29
+ }
30
+
31
+ export async function materializeBundlePayload(payload) {
32
+ const tempRoot = await mkdtemp(path.join(tmpdir(), "agenthub-upload-"));
33
+ const bundleDir = path.join(tempRoot, payload.bundleName);
34
+ for (const file of payload.files) {
35
+ const destination = path.join(bundleDir, file.path);
36
+ await ensureDir(path.dirname(destination));
37
+ await writeFile(destination, Buffer.from(file.contentBase64, "base64"));
38
+ }
39
+ return bundleDir;
40
+ }
41
+
42
+ export async function publishUploadedBundle({ payload, registryDir }) {
43
+ const bundleDir = await materializeBundlePayload(payload);
44
+ return publishBundle(bundleDir, registryDir);
45
+ }
46
+
47
+ export async function publishRemoteBundle({ bundleDir, serverUrl }) {
48
+ const payload = await serializeBundleDir(bundleDir);
49
+ const response = await fetch(new URL("/api/publish-upload", serverUrl), {
50
+ method: "POST",
51
+ headers: { "content-type": "application/json" },
52
+ body: JSON.stringify(payload),
53
+ });
54
+ if (!response.ok) {
55
+ throw new Error(`Remote publish failed: ${response.status} ${await response.text()}`);
56
+ }
57
+ return response.json();
58
+ }
@@ -0,0 +1,244 @@
1
+ import path from "node:path";
2
+ import { pathExists, ensureDir } from "./fs-utils.js";
3
+ import { readFile, writeFile } from "node:fs/promises";
4
+ import initSqlJs from "sql.js";
5
+
6
+ let db = null;
7
+ let dbPath = null;
8
+
9
+ /**
10
+ * 初始化数据库
11
+ */
12
+ export async function initDatabase(registryDir) {
13
+ if (db) return db;
14
+
15
+ const SQL = await initSqlJs();
16
+ dbPath = path.join(registryDir, "agenthub.db");
17
+
18
+ // 确保目录存在
19
+ await ensureDir(registryDir);
20
+
21
+ // 尝试加载现有数据库
22
+ if (await pathExists(dbPath)) {
23
+ const buffer = await readFile(dbPath);
24
+ db = new SQL.Database(buffer);
25
+ } else {
26
+ db = new SQL.Database();
27
+ }
28
+
29
+ // 创建表
30
+ db.run(`
31
+ CREATE TABLE IF NOT EXISTS download_stats (
32
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
33
+ agent_slug TEXT NOT NULL UNIQUE,
34
+ downloads INTEGER DEFAULT 0,
35
+ last_download_at TEXT,
36
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
37
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
38
+ )
39
+ `);
40
+
41
+ db.run(`
42
+ CREATE TABLE IF NOT EXISTS download_logs (
43
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
44
+ agent_slug TEXT NOT NULL,
45
+ installed_at TEXT DEFAULT CURRENT_TIMESTAMP,
46
+ target_workspace TEXT,
47
+ ip_address TEXT,
48
+ user_agent TEXT
49
+ )
50
+ `);
51
+
52
+ // 创建索引
53
+ db.run(`CREATE INDEX IF NOT EXISTS idx_download_stats_slug ON download_stats(agent_slug)`);
54
+ db.run(`CREATE INDEX IF NOT EXISTS idx_download_logs_slug ON download_logs(agent_slug)`);
55
+
56
+ await saveDatabase();
57
+ return db;
58
+ }
59
+
60
+ /**
61
+ * 保存数据库到文件
62
+ */
63
+ export async function saveDatabase() {
64
+ if (!db || !dbPath) return;
65
+ const data = db.export();
66
+ const buffer = Buffer.from(data);
67
+ await writeFile(dbPath, buffer);
68
+ }
69
+
70
+ /**
71
+ * 关闭数据库连接
72
+ */
73
+ export async function closeDatabase() {
74
+ if (db) {
75
+ await saveDatabase();
76
+ db.close();
77
+ db = null;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * 增加下载次数
83
+ */
84
+ export async function incrementDownloads(registryDir, slug, metadata = {}) {
85
+ await initDatabase(registryDir);
86
+
87
+ // 更新或插入下载统计
88
+ const existing = db.exec(`SELECT downloads FROM download_stats WHERE agent_slug = ?`, [slug]);
89
+
90
+ if (existing.length > 0 && existing[0].values.length > 0) {
91
+ db.run(`
92
+ UPDATE download_stats
93
+ SET downloads = downloads + 1,
94
+ last_download_at = CURRENT_TIMESTAMP,
95
+ updated_at = CURRENT_TIMESTAMP
96
+ WHERE agent_slug = ?
97
+ `, [slug]);
98
+ } else {
99
+ db.run(`
100
+ INSERT INTO download_stats (agent_slug, downloads, last_download_at, created_at, updated_at)
101
+ VALUES (?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
102
+ `, [slug]);
103
+ }
104
+
105
+ // 记录下载日志
106
+ db.run(`
107
+ INSERT INTO download_logs (agent_slug, target_workspace, ip_address, user_agent)
108
+ VALUES (?, ?, ?, ?)
109
+ `, [slug, metadata.targetWorkspace || null, metadata.ip || null, metadata.userAgent || null]);
110
+
111
+ await saveDatabase();
112
+
113
+ return getAgentDownloads(registryDir, slug);
114
+ }
115
+
116
+ /**
117
+ * 获取单个 Agent 的下载次数
118
+ */
119
+ export async function getAgentDownloads(registryDir, slug) {
120
+ await initDatabase(registryDir);
121
+
122
+ const result = db.exec(`SELECT downloads FROM download_stats WHERE agent_slug = ?`, [slug]);
123
+ if (result.length > 0 && result[0].values.length > 0) {
124
+ return result[0].values[0][0];
125
+ }
126
+ return 0;
127
+ }
128
+
129
+ /**
130
+ * 批量获取多个 Agent 的下载次数
131
+ */
132
+ export async function getAgentsDownloads(registryDir, slugs) {
133
+ await initDatabase(registryDir);
134
+
135
+ const result = {};
136
+ for (const slug of slugs) {
137
+ result[slug] = 0;
138
+ }
139
+
140
+ const placeholders = slugs.map(() => '?').join(',');
141
+ const rows = db.exec(`SELECT agent_slug, downloads FROM download_stats WHERE agent_slug IN (${placeholders})`, slugs);
142
+
143
+ if (rows.length > 0) {
144
+ for (const row of rows[0].values) {
145
+ result[row[0]] = row[1];
146
+ }
147
+ }
148
+
149
+ return result;
150
+ }
151
+
152
+ /**
153
+ * 获取所有 Agent 的下载次数
154
+ */
155
+ export async function getAllDownloads(registryDir) {
156
+ await initDatabase(registryDir);
157
+
158
+ const result = {};
159
+ const rows = db.exec(`SELECT agent_slug, downloads FROM download_stats`);
160
+
161
+ if (rows.length > 0) {
162
+ for (const row of rows[0].values) {
163
+ result[row[0]] = row[1];
164
+ }
165
+ }
166
+
167
+ return result;
168
+ }
169
+
170
+ /**
171
+ * 获取总下载次数
172
+ */
173
+ export async function getTotalDownloads(registryDir) {
174
+ await initDatabase(registryDir);
175
+
176
+ const result = db.exec(`SELECT COALESCE(SUM(downloads), 0) as total FROM download_stats`);
177
+ if (result.length > 0 && result[0].values.length > 0) {
178
+ return result[0].values[0][0];
179
+ }
180
+ return 0;
181
+ }
182
+
183
+ /**
184
+ * 获取下载排行
185
+ */
186
+ export async function getDownloadRanking(registryDir, limit = 10) {
187
+ await initDatabase(registryDir);
188
+
189
+ const rows = db.exec(`
190
+ SELECT agent_slug, downloads, last_download_at
191
+ FROM download_stats
192
+ ORDER BY downloads DESC
193
+ LIMIT ?
194
+ `, [limit]);
195
+
196
+ if (rows.length > 0) {
197
+ return rows[0].values.map(row => ({
198
+ slug: row[0],
199
+ downloads: row[1],
200
+ lastDownload: row[2]
201
+ }));
202
+ }
203
+ return [];
204
+ }
205
+
206
+ /**
207
+ * 获取最近的下载日志
208
+ */
209
+ export async function getRecentDownloads(registryDir, limit = 50) {
210
+ await initDatabase(registryDir);
211
+
212
+ const rows = db.exec(`
213
+ SELECT agent_slug, installed_at, target_workspace
214
+ FROM download_logs
215
+ ORDER BY installed_at DESC
216
+ LIMIT ?
217
+ `, [limit]);
218
+
219
+ if (rows.length > 0) {
220
+ return rows[0].values.map(row => ({
221
+ slug: row[0],
222
+ installedAt: row[1],
223
+ targetWorkspace: row[2]
224
+ }));
225
+ }
226
+ return [];
227
+ }
228
+
229
+ /**
230
+ * 获取数据库统计信息
231
+ */
232
+ export async function getDatabaseStats(registryDir) {
233
+ await initDatabase(registryDir);
234
+
235
+ const totalAgents = db.exec(`SELECT COUNT(*) FROM download_stats`)[0]?.values[0][0] || 0;
236
+ const totalDownloads = await getTotalDownloads(registryDir);
237
+ const totalLogs = db.exec(`SELECT COUNT(*) FROM download_logs`)[0]?.values[0][0] || 0;
238
+
239
+ return {
240
+ totalAgents,
241
+ totalDownloads,
242
+ totalLogs
243
+ };
244
+ }
@@ -0,0 +1,77 @@
1
+ import path from "node:path";
2
+ import { pathExists, readJson, writeJson } from "./fs-utils.js";
3
+
4
+ const STATS_FILE = "download-stats.json";
5
+
6
+ /**
7
+ * 获取下载统计
8
+ */
9
+ export async function getDownloadStats(registryDir) {
10
+ const statsPath = path.join(registryDir, STATS_FILE);
11
+ if (await pathExists(statsPath)) {
12
+ return readJson(statsPath);
13
+ }
14
+ return { agents: {} };
15
+ }
16
+
17
+ /**
18
+ * 保存下载统计
19
+ */
20
+ export async function saveDownloadStats(registryDir, stats) {
21
+ const statsPath = path.join(registryDir, STATS_FILE);
22
+ await writeJson(statsPath, stats);
23
+ }
24
+
25
+ /**
26
+ * 增加下载次数
27
+ */
28
+ export async function incrementDownloads(registryDir, slug) {
29
+ const stats = await getDownloadStats(registryDir);
30
+ if (!stats.agents[slug]) {
31
+ stats.agents[slug] = { downloads: 0 };
32
+ }
33
+ stats.agents[slug].downloads += 1;
34
+ stats.agents[slug].lastDownload = new Date().toISOString();
35
+ await saveDownloadStats(registryDir, stats);
36
+ return stats.agents[slug].downloads;
37
+ }
38
+
39
+ /**
40
+ * 获取单个 Agent 的下载次数
41
+ */
42
+ export async function getAgentDownloads(registryDir, slug) {
43
+ const stats = await getDownloadStats(registryDir);
44
+ return stats.agents[slug]?.downloads || 0;
45
+ }
46
+
47
+ /**
48
+ * 批量获取多个 Agent 的下载次数
49
+ */
50
+ export async function getAgentsDownloads(registryDir, slugs) {
51
+ const stats = await getDownloadStats(registryDir);
52
+ const result = {};
53
+ for (const slug of slugs) {
54
+ result[slug] = stats.agents[slug]?.downloads || 0;
55
+ }
56
+ return result;
57
+ }
58
+
59
+ /**
60
+ * 获取所有 Agent 的下载次数
61
+ */
62
+ export async function getAllDownloads(registryDir) {
63
+ const stats = await getDownloadStats(registryDir);
64
+ return stats.agents;
65
+ }
66
+
67
+ /**
68
+ * 获取总下载次数
69
+ */
70
+ export async function getTotalDownloads(registryDir) {
71
+ const stats = await getDownloadStats(registryDir);
72
+ let total = 0;
73
+ for (const agent of Object.values(stats.agents)) {
74
+ total += agent.downloads || 0;
75
+ }
76
+ return total;
77
+ }
@@ -0,0 +1,46 @@
1
+ import { cp, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export async function ensureDir(dirPath) {
5
+ await mkdir(dirPath, { recursive: true });
6
+ }
7
+
8
+ export async function copyDir(source, destination) {
9
+ await ensureDir(path.dirname(destination));
10
+ await cp(source, destination, { recursive: true });
11
+ }
12
+
13
+ export async function readJson(filePath) {
14
+ return JSON.parse(await readFile(filePath, "utf8"));
15
+ }
16
+
17
+ export async function writeJson(filePath, value) {
18
+ await ensureDir(path.dirname(filePath));
19
+ await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
20
+ }
21
+
22
+ export async function pathExists(targetPath) {
23
+ try {
24
+ await stat(targetPath);
25
+ return true;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ export async function countFiles(dirPath) {
32
+ if (!(await pathExists(dirPath))) {
33
+ return 0;
34
+ }
35
+ const entries = await readdir(dirPath, { withFileTypes: true });
36
+ let total = 0;
37
+ for (const entry of entries) {
38
+ const fullPath = path.join(dirPath, entry.name);
39
+ if (entry.isDirectory()) {
40
+ total += await countFiles(fullPath);
41
+ } else {
42
+ total += 1;
43
+ }
44
+ }
45
+ return total;
46
+ }