@dreamlogic-ai/cli 1.0.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,132 @@
1
+ /**
2
+ * API client — REST-only communication with Skill Server
3
+ * D-04: CLI uses REST exclusively, no MCP SSE
4
+ * R1-05: Request timeouts
5
+ * R1-09: No redirect following
6
+ * R1-11: Download size limit
7
+ * R1-14: Runtime response validation
8
+ */
9
+ import { createHash } from "crypto";
10
+ import { CLI_VERSION } from "../types.js";
11
+ const API_TIMEOUT = 30_000; // 30s for API calls
12
+ const DOWNLOAD_TIMEOUT = 300_000; // 5min for downloads
13
+ const MAX_DOWNLOAD_SIZE = 200 * 1024 * 1024; // 200MB
14
+ export class ApiClient {
15
+ baseUrl;
16
+ apiKey;
17
+ constructor(baseUrl, apiKey) {
18
+ this.baseUrl = baseUrl;
19
+ this.apiKey = apiKey;
20
+ }
21
+ async request(path, opts = {}) {
22
+ const url = `${this.baseUrl}${path}`;
23
+ const headers = {
24
+ Authorization: `Bearer ${this.apiKey}`,
25
+ "User-Agent": `dreamlogic-cli/${CLI_VERSION}`,
26
+ ...(opts.headers || {}),
27
+ };
28
+ const res = await fetch(url, {
29
+ ...opts,
30
+ headers,
31
+ redirect: "error", // R1-09: reject redirects
32
+ signal: AbortSignal.timeout(API_TIMEOUT), // R1-05
33
+ });
34
+ if (!res.ok) {
35
+ let msg = `HTTP ${res.status}`;
36
+ try {
37
+ const body = (await res.json());
38
+ if (body.error)
39
+ msg = body.error;
40
+ }
41
+ catch { /* ignore parse errors */ }
42
+ throw new ApiError(msg, res.status);
43
+ }
44
+ return res.json();
45
+ }
46
+ /** GET /api/me — verify key + get user info */
47
+ async me() {
48
+ const data = await this.request("/api/me");
49
+ // R1-14: Runtime validation
50
+ if (!data || typeof data.id !== "string" || typeof data.name !== "string") {
51
+ throw new ApiError("Invalid server response: missing user info", 502);
52
+ }
53
+ return data;
54
+ }
55
+ /** GET /skills — list available skills with version info */
56
+ async listSkills() {
57
+ const data = await this.request("/skills");
58
+ // R1-14: Runtime validation
59
+ if (!Array.isArray(data?.skills)) {
60
+ throw new ApiError("Invalid server response: missing skills array", 502);
61
+ }
62
+ return data.skills;
63
+ }
64
+ // R2-11: Removed unused getSkill() method
65
+ /** GET /packages/:filename — download package as buffer (R1-11: size limited) */
66
+ async downloadPackage(filename, onProgress) {
67
+ const url = `${this.baseUrl}/packages/${encodeURIComponent(filename)}`;
68
+ const res = await fetch(url, {
69
+ headers: {
70
+ Authorization: `Bearer ${this.apiKey}`,
71
+ "User-Agent": `dreamlogic-cli/${CLI_VERSION}`,
72
+ },
73
+ redirect: "error", // R1-09
74
+ signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT), // R1-05
75
+ });
76
+ if (!res.ok) {
77
+ throw new ApiError(`Download failed: HTTP ${res.status}`, res.status);
78
+ }
79
+ const contentLength = parseInt(res.headers.get("content-length") || "0", 10);
80
+ // R2-14: Guard against NaN/negative Content-Length
81
+ if (!Number.isNaN(contentLength) && contentLength > 0 && contentLength > MAX_DOWNLOAD_SIZE) {
82
+ throw new ApiError(`Package too large: ${contentLength} bytes (max ${MAX_DOWNLOAD_SIZE})`, 413);
83
+ }
84
+ const reader = res.body?.getReader();
85
+ if (!reader)
86
+ throw new ApiError("No response body", 500);
87
+ const chunks = [];
88
+ let downloaded = 0;
89
+ while (true) {
90
+ const { done, value } = await reader.read();
91
+ if (done)
92
+ break;
93
+ chunks.push(value);
94
+ downloaded += value.length;
95
+ // R1-11: Enforce size limit during streaming
96
+ if (downloaded > MAX_DOWNLOAD_SIZE) {
97
+ reader.cancel();
98
+ throw new ApiError("Package too large", 413);
99
+ }
100
+ if (onProgress && contentLength > 0) {
101
+ onProgress(downloaded, contentLength);
102
+ }
103
+ }
104
+ const buffer = Buffer.concat(chunks);
105
+ const sha256 = createHash("sha256").update(buffer).digest("hex");
106
+ return { buffer, sha256 };
107
+ }
108
+ /** GET /health — check server connectivity (R2-09: validated) */
109
+ async health() {
110
+ const url = `${this.baseUrl}/health`;
111
+ const res = await fetch(url, {
112
+ headers: { "User-Agent": `dreamlogic-cli/${CLI_VERSION}` },
113
+ redirect: "error",
114
+ signal: AbortSignal.timeout(5000),
115
+ });
116
+ if (!res.ok)
117
+ throw new ApiError("Server unreachable", res.status);
118
+ const data = await res.json();
119
+ if (typeof data?.status !== "string" || typeof data?.version !== "string") {
120
+ throw new ApiError("Invalid health response", 502);
121
+ }
122
+ return data;
123
+ }
124
+ }
125
+ export class ApiError extends Error {
126
+ status;
127
+ constructor(message, status) {
128
+ super(message);
129
+ this.status = status;
130
+ this.name = "ApiError";
131
+ }
132
+ }
@@ -0,0 +1,17 @@
1
+ import { type CliConfig, type InstalledRegistry } from "../types.js";
2
+ export declare function getDefaultInstallDir(): string;
3
+ export declare function ensureConfigDir(): void;
4
+ export declare function loadConfig(): CliConfig | null;
5
+ export declare function saveConfig(config: CliConfig): void;
6
+ /** R1-06: Validate key format from all sources (env, config file) */
7
+ export declare function getApiKey(): string | null;
8
+ /** R1-07: Enforce HTTPS (localhost/127.0.0.1 exempt for dev) */
9
+ export declare function getServer(): string;
10
+ export declare function getInstallDir(): string;
11
+ export declare function loadInstalled(): InstalledRegistry;
12
+ /** R1-08: Write installed.json with restricted permissions too */
13
+ export declare function saveInstalled(registry: InstalledRegistry): void;
14
+ /** R1-13: Zero-fill before overwrite for defense-in-depth */
15
+ export declare function clearConfig(): void;
16
+ /** Mask API key for display: sk-user-ce40...4d42 */
17
+ export declare function maskKey(key: string): string;
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Configuration manager — reads/writes ~/.dreamlogic/config.json
3
+ * Key stored with file permissions 600 (owner-only)
4
+ */
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, statSync } from "fs";
6
+ import { join } from "path";
7
+ import { homedir } from "os";
8
+ import { CONFIG_DIR_NAME, DEFAULT_SERVER, DEFAULT_INSTALL_DIR_NAME, } from "../types.js";
9
+ // R1-06: Key format validation applied everywhere
10
+ const KEY_RE = /^sk-(admin|user)-[a-f0-9]{16,}$/;
11
+ function getConfigDir() {
12
+ return join(homedir(), CONFIG_DIR_NAME);
13
+ }
14
+ function getConfigPath() {
15
+ return join(getConfigDir(), "config.json");
16
+ }
17
+ function getInstalledPath() {
18
+ return join(getConfigDir(), "installed.json");
19
+ }
20
+ export function getDefaultInstallDir() {
21
+ return join(homedir(), DEFAULT_INSTALL_DIR_NAME);
22
+ }
23
+ export function ensureConfigDir() {
24
+ const dir = getConfigDir();
25
+ if (!existsSync(dir)) {
26
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
27
+ }
28
+ }
29
+ export function loadConfig() {
30
+ const path = getConfigPath();
31
+ if (!existsSync(path))
32
+ return null;
33
+ try {
34
+ const data = JSON.parse(readFileSync(path, "utf-8"));
35
+ // R2-13: Basic shape validation
36
+ if (typeof data !== "object" || data === null)
37
+ return null;
38
+ if (data.api_key && typeof data.api_key !== "string")
39
+ return null;
40
+ return data;
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ export function saveConfig(config) {
47
+ ensureConfigDir();
48
+ const path = getConfigPath();
49
+ writeFileSync(path, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
50
+ try {
51
+ chmodSync(path, 0o600);
52
+ }
53
+ catch { /* Windows fallback */ }
54
+ }
55
+ /** R1-06: Validate key format from all sources (env, config file) */
56
+ export function getApiKey() {
57
+ const key = process.env.DREAMLOGIC_API_KEY || loadConfig()?.api_key || null;
58
+ if (key && !KEY_RE.test(key)) {
59
+ console.error(" ❌ Invalid API key format (expected sk-user-xxx or sk-admin-xxx)");
60
+ return null;
61
+ }
62
+ return key;
63
+ }
64
+ /** R1-07: Enforce HTTPS (localhost/127.0.0.1 exempt for dev) */
65
+ export function getServer() {
66
+ const url = process.env.DREAMLOGIC_SERVER || loadConfig()?.server || DEFAULT_SERVER;
67
+ // R1-07 + R2-03: HTTPS enforcement (localhost/127.0.0.1 exempt with strict boundary)
68
+ if (!/^https:\/\//i.test(url) && !/^http:\/\/(localhost|127\.0\.0\.1)(:[0-9]+)?(\/|$)/i.test(url)) {
69
+ throw new Error("Server URL must use HTTPS (http://localhost allowed for dev)");
70
+ }
71
+ return url;
72
+ }
73
+ export function getInstallDir() {
74
+ if (process.env.DREAMLOGIC_INSTALL_DIR)
75
+ return process.env.DREAMLOGIC_INSTALL_DIR;
76
+ const config = loadConfig();
77
+ return config?.install_dir ?? getDefaultInstallDir();
78
+ }
79
+ export function loadInstalled() {
80
+ const path = getInstalledPath();
81
+ if (!existsSync(path))
82
+ return {};
83
+ try {
84
+ const data = JSON.parse(readFileSync(path, "utf-8"));
85
+ // R2-13: Basic shape validation
86
+ if (typeof data !== "object" || data === null || Array.isArray(data))
87
+ return {};
88
+ return data;
89
+ }
90
+ catch {
91
+ return {};
92
+ }
93
+ }
94
+ /** R1-08: Write installed.json with restricted permissions too */
95
+ export function saveInstalled(registry) {
96
+ ensureConfigDir();
97
+ const path = getInstalledPath();
98
+ writeFileSync(path, JSON.stringify(registry, null, 2) + "\n", { mode: 0o600 });
99
+ try {
100
+ chmodSync(path, 0o600);
101
+ }
102
+ catch { /* Windows fallback */ }
103
+ }
104
+ /** R1-13: Zero-fill before overwrite for defense-in-depth */
105
+ export function clearConfig() {
106
+ const path = getConfigPath();
107
+ if (existsSync(path)) {
108
+ try {
109
+ const size = statSync(path).size;
110
+ writeFileSync(path, Buffer.alloc(size, 0));
111
+ }
112
+ catch { /* best-effort */ }
113
+ writeFileSync(path, "{}\n", { mode: 0o600 });
114
+ }
115
+ }
116
+ /** Mask API key for display: sk-user-ce40...4d42 */
117
+ export function maskKey(key) {
118
+ if (key.length < 12)
119
+ return "***";
120
+ return key.slice(0, 12) + "..." + key.slice(-4);
121
+ }
@@ -0,0 +1,14 @@
1
+ import { type ApiClient } from "./api-client.js";
2
+ /**
3
+ * Download and install a skill package
4
+ */
5
+ export declare function installSkill(client: ApiClient, skillId: string, packageFile: string, expectedSha256: string | undefined, expectedVersion: string, opts?: {
6
+ fromFile?: string;
7
+ }): Promise<{
8
+ path: string;
9
+ version: string;
10
+ }>;
11
+ /**
12
+ * Rollback a skill to its backup (D-13)
13
+ */
14
+ export declare function rollbackSkill(skillId: string): boolean;
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Installer — download, verify, extract, atomic-swap
3
+ * D-03: Uses yauzl (not adm-zip) for safe ZIP extraction
4
+ * D-05: Atomic directory replacement via staging + rename
5
+ * D-14: SHA256 re-verified on every install (no trusted cache)
6
+ * R1-03: Zip bomb protection (total size + entry count limits)
7
+ * R1-10: SIGINT/SIGTERM cleanup handler
8
+ */
9
+ import { createWriteStream, existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "fs";
10
+ import { join, normalize, resolve as pathResolve } from "path";
11
+ import { pipeline } from "stream/promises";
12
+ import yauzl from "yauzl";
13
+ import { loadInstalled, saveInstalled, getInstallDir } from "./config.js";
14
+ import { ui } from "./ui.js";
15
+ // R1-03: Extraction safety limits
16
+ const MAX_TOTAL_EXTRACT_SIZE = 500 * 1024 * 1024; // 500MB
17
+ const MAX_ENTRY_COUNT = 10_000;
18
+ const MAX_SINGLE_FILE_SIZE = 100 * 1024 * 1024; // 100MB
19
+ // R2-10: Safe skill ID pattern
20
+ const SAFE_SKILL_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
21
+ /**
22
+ * Download and install a skill package
23
+ */
24
+ export async function installSkill(client, skillId, packageFile, expectedSha256, expectedVersion, opts = {}) {
25
+ const installDir = getInstallDir();
26
+ mkdirSync(installDir, { recursive: true });
27
+ // R2-10: Validate skill ID to prevent path traversal via malicious server
28
+ if (!SAFE_SKILL_ID.test(skillId)) {
29
+ throw new Error(`Invalid skill ID: ${skillId}`);
30
+ }
31
+ const skillDir = join(installDir, skillId);
32
+ const stagingDir = join(installDir, `.${skillId}-staging-${Date.now()}`);
33
+ const backupDir = join(installDir, `.${skillId}-backup-${Date.now()}`);
34
+ let buffer;
35
+ let actualSha256;
36
+ if (opts.fromFile) {
37
+ // D-10: Offline install from local file
38
+ const { readFileSync } = await import("fs");
39
+ const { createHash } = await import("crypto");
40
+ buffer = readFileSync(opts.fromFile);
41
+ actualSha256 = createHash("sha256").update(buffer).digest("hex");
42
+ ui.info(`Loaded from file: ${opts.fromFile}`);
43
+ }
44
+ else {
45
+ // Download with progress
46
+ const spinner = ui.spinner(`Downloading ${skillId} ${expectedVersion}...`);
47
+ spinner.start();
48
+ try {
49
+ const result = await client.downloadPackage(packageFile, (dl, total) => {
50
+ const pct = Math.round((dl / total) * 100);
51
+ const bar = "█".repeat(Math.round(pct / 4)) + "░".repeat(25 - Math.round(pct / 4));
52
+ spinner.text = ` 📦 ${bar} ${pct}% | ${ui.fileSize(dl)}`;
53
+ });
54
+ buffer = result.buffer;
55
+ actualSha256 = result.sha256;
56
+ spinner.succeed(` Downloaded ${ui.fileSize(buffer.length)}`);
57
+ }
58
+ catch (err) {
59
+ spinner.fail(` Download failed`);
60
+ throw err;
61
+ }
62
+ }
63
+ // SHA256 verification
64
+ if (expectedSha256) {
65
+ if (actualSha256 !== expectedSha256) {
66
+ ui.err("SHA256 mismatch! Package may be corrupted or tampered.");
67
+ ui.line(` Expected: ${expectedSha256}`);
68
+ ui.line(` Actual: ${actualSha256}`);
69
+ throw new Error("SHA256 verification failed");
70
+ }
71
+ ui.ok("SHA256 verified");
72
+ }
73
+ else {
74
+ ui.warning("No SHA256 hash from server — skipping verification");
75
+ }
76
+ // Extract to staging directory (D-05: never extract directly to target)
77
+ // R1-10 + R2-01/R2-02: Signal cleanup with proper listener management
78
+ const cleanupAndExit = (signal) => {
79
+ rmSync(stagingDir, { recursive: true, force: true });
80
+ // R2-02: Re-raise signal so process actually exits
81
+ process.removeListener("SIGINT", cleanupAndExit);
82
+ process.removeListener("SIGTERM", cleanupAndExit);
83
+ process.kill(process.pid, signal);
84
+ };
85
+ process.on("SIGINT", cleanupAndExit);
86
+ process.on("SIGTERM", cleanupAndExit);
87
+ // R2-01: try/finally ensures listeners are always removed
88
+ try {
89
+ const extractSpinner = ui.spinner("Extracting...");
90
+ extractSpinner.start();
91
+ try {
92
+ mkdirSync(stagingDir, { recursive: true });
93
+ await extractZip(buffer, stagingDir);
94
+ extractSpinner.succeed(" Extracted");
95
+ }
96
+ catch (err) {
97
+ extractSpinner.fail(" Extraction failed");
98
+ rmSync(stagingDir, { recursive: true, force: true });
99
+ throw err;
100
+ }
101
+ // Atomic swap (D-05)
102
+ try {
103
+ if (existsSync(skillDir)) {
104
+ renameSync(skillDir, backupDir);
105
+ ui.info(`Backed up previous version`);
106
+ }
107
+ const extractedRoot = findExtractedRoot(stagingDir);
108
+ renameSync(extractedRoot, skillDir);
109
+ if (existsSync(stagingDir)) {
110
+ rmSync(stagingDir, { recursive: true, force: true });
111
+ }
112
+ }
113
+ catch (err) {
114
+ if (existsSync(backupDir) && !existsSync(skillDir)) {
115
+ renameSync(backupDir, skillDir);
116
+ }
117
+ rmSync(stagingDir, { recursive: true, force: true });
118
+ throw err;
119
+ }
120
+ }
121
+ finally {
122
+ // R2-01: Always remove listeners, even on error
123
+ process.removeListener("SIGINT", cleanupAndExit);
124
+ process.removeListener("SIGTERM", cleanupAndExit);
125
+ }
126
+ // Update installed registry
127
+ const installed = loadInstalled();
128
+ const previousVersion = installed[skillId]?.version;
129
+ const entry = {
130
+ version: expectedVersion,
131
+ installed_at: new Date().toISOString(),
132
+ path: skillDir,
133
+ sha256: actualSha256,
134
+ ...(previousVersion ? { previous_version: previousVersion } : {}),
135
+ };
136
+ installed[skillId] = entry;
137
+ saveInstalled(installed);
138
+ // R2-12: Clean up old backups (keep only the latest)
139
+ try {
140
+ const oldBackups = readdirSync(installDir)
141
+ .filter((e) => e.startsWith(`.${skillId}-backup-`))
142
+ .sort()
143
+ .slice(0, -1); // keep newest
144
+ for (const b of oldBackups) {
145
+ rmSync(join(installDir, b), { recursive: true, force: true });
146
+ }
147
+ }
148
+ catch { /* best-effort cleanup */ }
149
+ if (existsSync(backupDir)) {
150
+ ui.info(`Rollback available: dreamlogic rollback ${skillId}`);
151
+ }
152
+ return { path: skillDir, version: expectedVersion };
153
+ }
154
+ /**
155
+ * Rollback a skill to its backup (D-13)
156
+ */
157
+ export function rollbackSkill(skillId) {
158
+ const installDir = getInstallDir();
159
+ const skillDir = join(installDir, skillId);
160
+ let entries;
161
+ try {
162
+ entries = readdirSync(installDir);
163
+ }
164
+ catch {
165
+ return false;
166
+ }
167
+ const backups = entries
168
+ .filter((e) => e.startsWith(`.${skillId}-backup-`))
169
+ .sort()
170
+ .reverse();
171
+ if (backups.length === 0)
172
+ return false;
173
+ const latestBackup = join(installDir, backups[0]);
174
+ if (existsSync(skillDir)) {
175
+ rmSync(skillDir, { recursive: true, force: true });
176
+ }
177
+ renameSync(latestBackup, skillDir);
178
+ // Update installed.json
179
+ const installed = loadInstalled();
180
+ if (installed[skillId]?.previous_version) {
181
+ installed[skillId].version = installed[skillId].previous_version;
182
+ delete installed[skillId].previous_version;
183
+ installed[skillId].installed_at = new Date().toISOString();
184
+ saveInstalled(installed);
185
+ }
186
+ return true;
187
+ }
188
+ /**
189
+ * Find the actual root directory inside extracted ZIP
190
+ * (handles single-directory-wrapper pattern like suno-cover-v5-v20260406/)
191
+ */
192
+ function findExtractedRoot(stagingDir) {
193
+ const entries = readdirSync(stagingDir);
194
+ // If only one directory entry, use that as root
195
+ if (entries.length === 1) {
196
+ const single = join(stagingDir, entries[0]);
197
+ try {
198
+ if (statSync(single).isDirectory())
199
+ return single;
200
+ }
201
+ catch { /* fallthrough */ }
202
+ }
203
+ return stagingDir;
204
+ }
205
+ /**
206
+ * Safe ZIP extraction using yauzl (D-03)
207
+ * Prevents: path traversal, symlinks, oversized files
208
+ * R1-03: Total size limit + entry count limit (zip bomb protection)
209
+ */
210
+ function extractZip(buffer, targetDir) {
211
+ return new Promise((resolve, reject) => {
212
+ yauzl.fromBuffer(buffer, { lazyEntries: true }, (err, zipfile) => {
213
+ if (err || !zipfile) {
214
+ reject(err || new Error("Failed to open ZIP"));
215
+ return;
216
+ }
217
+ let totalExtracted = 0;
218
+ let entryCount = 0;
219
+ zipfile.readEntry();
220
+ zipfile.on("entry", (entry) => {
221
+ // R1-03: Entry count limit
222
+ entryCount++;
223
+ if (entryCount > MAX_ENTRY_COUNT) {
224
+ zipfile.close(); // R2-08: Close before reject
225
+ reject(new Error(`Too many ZIP entries (>${MAX_ENTRY_COUNT}) — possible zip bomb`));
226
+ return;
227
+ }
228
+ const entryPath = normalize(entry.fileName);
229
+ // D-03 / Security: reject path traversal
230
+ if (entryPath.startsWith("..") || entryPath.includes("/../")) {
231
+ zipfile.close(); // R2-08
232
+ reject(new Error(`Unsafe path in ZIP: ${entry.fileName}`));
233
+ return;
234
+ }
235
+ // Reject oversized single files
236
+ if (entry.uncompressedSize > MAX_SINGLE_FILE_SIZE) {
237
+ zipfile.close(); // R2-08
238
+ reject(new Error(`File too large in ZIP: ${entry.fileName} (${entry.uncompressedSize} bytes)`));
239
+ return;
240
+ }
241
+ // R1-03: Cumulative size limit
242
+ totalExtracted += entry.uncompressedSize;
243
+ if (totalExtracted > MAX_TOTAL_EXTRACT_SIZE) {
244
+ zipfile.close(); // R2-08
245
+ reject(new Error(`Total extracted size exceeds ${MAX_TOTAL_EXTRACT_SIZE / 1024 / 1024}MB — possible zip bomb`));
246
+ return;
247
+ }
248
+ const fullPath = join(targetDir, entryPath);
249
+ const resolvedPath = pathResolve(fullPath);
250
+ // R2-04: Trailing separator prevents prefix collision
251
+ const resolvedTarget = pathResolve(targetDir) + "/";
252
+ if (!resolvedPath.startsWith(resolvedTarget) && resolvedPath !== pathResolve(targetDir)) {
253
+ zipfile.close(); // R2-08
254
+ reject(new Error(`Path traversal detected: ${entry.fileName}`));
255
+ return;
256
+ }
257
+ if (entry.fileName.endsWith("/")) {
258
+ // Directory
259
+ mkdirSync(fullPath, { recursive: true });
260
+ zipfile.readEntry();
261
+ }
262
+ else {
263
+ // File
264
+ mkdirSync(join(fullPath, ".."), { recursive: true });
265
+ zipfile.openReadStream(entry, (err, readStream) => {
266
+ if (err || !readStream) {
267
+ zipfile.close(); // R2-08
268
+ reject(err || new Error("Failed to read ZIP entry"));
269
+ return;
270
+ }
271
+ const writeStream = createWriteStream(fullPath);
272
+ pipeline(readStream, writeStream).then(() => zipfile.readEntry()).catch((e) => {
273
+ zipfile.close(); // R2-08
274
+ reject(e);
275
+ });
276
+ });
277
+ }
278
+ });
279
+ zipfile.on("end", () => resolve());
280
+ zipfile.on("error", reject);
281
+ });
282
+ });
283
+ }
@@ -0,0 +1,39 @@
1
+ import { type Ora } from "ora";
2
+ export declare const ui: {
3
+ brand: import("chalk").ChalkInstance;
4
+ accent: import("chalk").ChalkInstance;
5
+ success: import("chalk").ChalkInstance;
6
+ warn: import("chalk").ChalkInstance;
7
+ error: import("chalk").ChalkInstance;
8
+ dim: import("chalk").ChalkInstance;
9
+ /** Print the welcome banner */
10
+ banner(): void;
11
+ /** Print a section header */
12
+ header(text: string): void;
13
+ /** Print success message */
14
+ ok(msg: string): void;
15
+ /** Print warning */
16
+ warning(msg: string): void;
17
+ /** Print error */
18
+ err(msg: string): void;
19
+ /** Print info line */
20
+ info(msg: string): void;
21
+ /** Indented line */
22
+ line(msg: string): void;
23
+ /** Create a spinner */
24
+ spinner(text: string): Ora;
25
+ /** Format file size */
26
+ fileSize(bytes: number): string;
27
+ /** Format a skill for display */
28
+ skillLine(s: {
29
+ id: string;
30
+ name: string;
31
+ version?: string;
32
+ description: string;
33
+ tag?: string;
34
+ }): string;
35
+ /** Print a key-value table */
36
+ table(rows: [string, string][]): void;
37
+ /** Goodbye message */
38
+ goodbye(): void;
39
+ };