quack-search 0.0.4 → 0.1.1

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  DuckDuckGo web search library for JS runtimes, written in Go
4
4
 
5
- ### Installation
5
+ ## Installation
6
6
 
7
7
  ```bash
8
8
  bun add quack-search
@@ -12,7 +12,7 @@ npm install quack-search
12
12
  yarn add quack-search
13
13
  ```
14
14
 
15
- ### Usage
15
+ ## Usage
16
16
 
17
17
  ```ts
18
18
  import { search, fetchContent } from 'quack-search';
@@ -27,7 +27,56 @@ if (!page.success) {
27
27
  }
28
28
  ```
29
29
 
30
- #### _Open to Contributions_
30
+ ## Supported Platforms
31
+
32
+ - macOS (x64, ARM64)
33
+ - Linux (x64, ARM64)
34
+ - Windows (x64, ARM64)
35
+
36
+ ## Advanced: CI / Docker / Offline Environments
37
+
38
+ For constrained environments where automatic download isn't possible:
39
+
40
+ ### Option 1: Environment Variable
41
+
42
+ Pre-install the binary and point to it:
43
+ ```bash
44
+ export QUACK_BINARY_PATH=/usr/local/bin/quack
45
+ ```
46
+
47
+ ### Option 2: Explicit Download
48
+
49
+ Download at build time (e.g., in a Dockerfile):
50
+ ```ts
51
+ import { downloadBinary } from 'quack-search';
52
+
53
+ await downloadBinary({
54
+ targetDir: '/app/bin',
55
+ version: 'v0.1.0', // or 'latest'
56
+ verbose: true
57
+ });
58
+ ```
59
+
60
+ ### Option 3: Platform Package
61
+
62
+ Install the platform-specific package directly:
63
+ ```bash
64
+ npm install quack-search-linux-x64
65
+ ```
66
+
67
+ ### Diagnostics
68
+
69
+ ```ts
70
+ import { checkBinaryStatus } from 'quack-search';
71
+
72
+ const status = checkBinaryStatus();
73
+ console.log(status);
74
+ // { found: true, path: '/path/to/quack', source: 'node_modules', platform: 'darwin', arch: 'arm64' }
75
+ ```
76
+
77
+ ## Contributing
78
+
79
+ Open to contributions! See the [repository](https://github.com/adistrim/quack).
31
80
 
32
81
  ## License
33
82
  MIT License. See [LICENSE](LICENSE) file for details.
@@ -1 +1,48 @@
1
+ export type SupportedPlatform = "darwin-arm64" | "darwin-x64" | "linux-x64" | "linux-arm64" | "windows-x64" | "windows-arm64";
2
+ export declare function getPlatformTarget(): SupportedPlatform;
3
+ export declare function getBinaryName(): string;
4
+ export declare class QuackBinaryError extends Error {
5
+ readonly code: string;
6
+ readonly hint?: string | undefined;
7
+ constructor(message: string, code: string, hint?: string | undefined);
8
+ toString(): string;
9
+ }
10
+ interface BinaryResolutionResult {
11
+ path: string;
12
+ source: "env" | "node_modules" | "local";
13
+ }
14
+ /**
15
+ * Attempts to resolve the quack binary using multiple strategies:
16
+ * 1. QUACK_BINARY_PATH environment variable (explicit override)
17
+ * 2. Platform-specific npm package in node_modules
18
+ * 3. Local binary in package directory (for development/bundled)
19
+ */
1
20
  export declare function resolveBinaryPath(): string;
21
+ export declare function tryResolveBinary(): BinaryResolutionResult;
22
+ export interface BinaryStatus {
23
+ found: boolean;
24
+ path?: string;
25
+ source?: "env" | "node_modules" | "local";
26
+ error?: string;
27
+ platform: string;
28
+ arch: string;
29
+ }
30
+ /**
31
+ * Check if the binary is available without throwing.
32
+ * Useful for diagnostics and graceful degradation.
33
+ */
34
+ export declare function checkBinaryStatus(): BinaryStatus;
35
+ /**
36
+ * Ensures the binary is available. Throws QuackBinaryError if not.
37
+ * Call this at startup or before critical operations to fail fast.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * import { ensureBinary } from 'quack-search';
42
+ *
43
+ * // Fail fast at app startup
44
+ * ensureBinary();
45
+ * ```
46
+ */
47
+ export declare function ensureBinary(): void;
48
+ export {};
@@ -1,24 +1,211 @@
1
- import { platform, arch } from "node:process";
2
- import { dirname, join } from "node:path";
3
- import { createRequire } from "node:module";
4
- const require = createRequire(import.meta.url);
5
- function resolveBinaryPackage() {
6
- if (platform === "darwin" && arch === "arm64") {
7
- return "quack-search-darwin-arm64";
1
+ import { platform, arch, env } from "node:process";
2
+ import { join, dirname, resolve } from "node:path";
3
+ import { existsSync, accessSync, constants } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+ import { homedir } from "node:os";
6
+ export function getPlatformTarget() {
7
+ const key = `${platform}-${arch}`;
8
+ const supported = {
9
+ "darwin-arm64": "darwin-arm64",
10
+ "darwin-x64": "darwin-x64",
11
+ "linux-x64": "linux-x64",
12
+ "linux-arm64": "linux-arm64",
13
+ "win32-x64": "windows-x64",
14
+ "win32-arm64": "windows-arm64",
15
+ };
16
+ const target = supported[key];
17
+ if (!target) {
18
+ throw new QuackBinaryError(`Unsupported platform: ${platform} ${arch}`, "UNSUPPORTED_PLATFORM", `quack-search supports: darwin-arm64, darwin-x64, linux-x64, linux-arm64, windows-x64, windows-arm64.\n` +
19
+ `You can provide a custom binary via QUACK_BINARY_PATH environment variable.`);
8
20
  }
9
- if (platform === "darwin" && arch === "x64") {
10
- return "quack-search-darwin-x64";
11
- }
12
- if (platform === "linux" && arch === "x64") {
13
- return "quack-search-linux-x64";
21
+ return target;
22
+ }
23
+ export function getBinaryName() {
24
+ return platform === "win32" ? "quack.exe" : "quack";
25
+ }
26
+ // ---------- Error Handling ----------
27
+ export class QuackBinaryError extends Error {
28
+ code;
29
+ hint;
30
+ constructor(message, code, hint) {
31
+ super(message);
32
+ this.code = code;
33
+ this.hint = hint;
34
+ this.name = "QuackBinaryError";
14
35
  }
15
- if (platform === "win32" && arch === "x64") {
16
- return "quack-search-windows-x64";
36
+ toString() {
37
+ let msg = `${this.name}: ${this.message}`;
38
+ if (this.hint) {
39
+ msg += `\n\nHint: ${this.hint}`;
40
+ }
41
+ return msg;
17
42
  }
18
- throw new Error(`unsupported platform: ${platform} ${arch}`);
19
43
  }
44
+ /**
45
+ * Attempts to resolve the quack binary using multiple strategies:
46
+ * 1. QUACK_BINARY_PATH environment variable (explicit override)
47
+ * 2. Platform-specific npm package in node_modules
48
+ * 3. Local binary in package directory (for development/bundled)
49
+ */
20
50
  export function resolveBinaryPath() {
21
- const pkgName = resolveBinaryPackage();
22
- const pkgEntry = require.resolve(pkgName);
23
- return pkgEntry;
51
+ const result = tryResolveBinary();
52
+ return result.path;
53
+ }
54
+ export function tryResolveBinary() {
55
+ // Explicit env var override
56
+ const envPath = env.QUACK_BINARY_PATH;
57
+ if (envPath) {
58
+ if (!existsSync(envPath)) {
59
+ throw new QuackBinaryError(`Binary not found at QUACK_BINARY_PATH: ${envPath}`, "ENV_BINARY_NOT_FOUND", `Ensure the file exists and is executable.`);
60
+ }
61
+ assertExecutable(envPath);
62
+ return { path: envPath, source: "env" };
63
+ }
64
+ // Looking in node_modules for platform-specific package
65
+ const nodeModulesPath = tryResolveFromNodeModules();
66
+ if (nodeModulesPath) {
67
+ return { path: nodeModulesPath, source: "node_modules" };
68
+ }
69
+ // Look for local binary
70
+ const localPath = tryResolveLocalBinary();
71
+ if (localPath) {
72
+ return { path: localPath, source: "local" };
73
+ }
74
+ // all failure state
75
+ const targetPlatform = getPlatformTarget();
76
+ const packageName = `quack-search-${targetPlatform}`;
77
+ throw new QuackBinaryError(`Could not find quack binary for ${platform} ${arch}`, "BINARY_NOT_FOUND", `To fix this, try one of:\n` +
78
+ ` 1. Install the platform package: npm install ${packageName}\n` +
79
+ ` 2. Set QUACK_BINARY_PATH to point to your quack binary\n` +
80
+ ` 3. Download the binary from: https://github.com/adistrim/quack/releases\n\n` +
81
+ `If you're using Docker or a bundler, set QUACK_BINARY_PATH explicitly.`);
82
+ }
83
+ // resolve binary from platform-specific package in node_modules
84
+ function tryResolveFromNodeModules() {
85
+ const targetPlatform = getPlatformTarget();
86
+ const packageName = `quack-search-${targetPlatform}`;
87
+ const binaryName = getBinaryName();
88
+ const thisDir = getModuleDir();
89
+ //. node_modules locations to check
90
+ const searchPaths = [
91
+ // hoisted in monorepo (relative to this package)
92
+ join(thisDir, "..", "..", "..", "node_modules", packageName, "bin", binaryName),
93
+ // direct
94
+ join(thisDir, "..", "..", "node_modules", packageName, "bin", binaryName),
95
+ // peer location
96
+ join(thisDir, "..", "..", packageName, "bin", binaryName),
97
+ // pnpm-style
98
+ join(thisDir, "..", "..", "..", ".pnpm", "node_modules", packageName, "bin", binaryName),
99
+ ];
100
+ // Also check process.cwd() based paths for runtime resolution
101
+ const cwd = process.cwd();
102
+ searchPaths.push(join(cwd, "node_modules", packageName, "bin", binaryName), join(cwd, "..", "node_modules", packageName, "bin", binaryName));
103
+ for (const searchPath of searchPaths) {
104
+ const resolved = resolve(searchPath);
105
+ if (existsSync(resolved)) {
106
+ try {
107
+ assertExecutable(resolved);
108
+ return resolved;
109
+ }
110
+ catch {
111
+ // next
112
+ }
113
+ }
114
+ }
115
+ return null;
116
+ }
117
+ function tryResolveLocalBinary() {
118
+ const binaryName = getBinaryName();
119
+ const thisDir = getModuleDir();
120
+ // Check for local bin directory (useful for development or bundled distributions)
121
+ const localPaths = [
122
+ join(thisDir, "..", "..", "bin", binaryName),
123
+ join(thisDir, "..", "bin", binaryName),
124
+ join(thisDir, "bin", binaryName),
125
+ ];
126
+ // Also check user cache directory (auto-downloaded binaries)
127
+ const cacheDir = getCacheDir();
128
+ localPaths.push(join(cacheDir, binaryName));
129
+ for (const localPath of localPaths) {
130
+ const resolved = resolve(localPath);
131
+ if (existsSync(resolved)) {
132
+ try {
133
+ assertExecutable(resolved);
134
+ return resolved;
135
+ }
136
+ catch {
137
+ // Not executable, try next
138
+ }
139
+ }
140
+ }
141
+ return null;
142
+ }
143
+ function getCacheDir() {
144
+ const home = homedir();
145
+ if (platform === "win32") {
146
+ return join(env.LOCALAPPDATA || join(home, "AppData", "Local"), "quack-search");
147
+ }
148
+ return join(env.XDG_CACHE_HOME || join(home, ".cache"), "quack-search");
149
+ }
150
+ function assertExecutable(filePath) {
151
+ try {
152
+ accessSync(filePath, constants.X_OK);
153
+ }
154
+ catch {
155
+ throw new QuackBinaryError(`Binary exists but is not executable: ${filePath}`, "BINARY_NOT_EXECUTABLE", `Run: chmod +x "${filePath}"`);
156
+ }
157
+ }
158
+ function getModuleDir() {
159
+ // Support both Bun and Node.js
160
+ if (typeof import.meta.dir === "string") {
161
+ return import.meta.dir;
162
+ }
163
+ return dirname(fileURLToPath(import.meta.url));
164
+ }
165
+ /**
166
+ * Check if the binary is available without throwing.
167
+ * Useful for diagnostics and graceful degradation.
168
+ */
169
+ export function checkBinaryStatus() {
170
+ const base = { platform, arch };
171
+ try {
172
+ const result = tryResolveBinary();
173
+ return {
174
+ ...base,
175
+ found: true,
176
+ path: result.path,
177
+ source: result.source,
178
+ };
179
+ }
180
+ catch (err) {
181
+ return {
182
+ ...base,
183
+ found: false,
184
+ error: err instanceof Error ? err.message : String(err),
185
+ };
186
+ }
187
+ }
188
+ /**
189
+ * Ensures the binary is available. Throws QuackBinaryError if not.
190
+ * Call this at startup or before critical operations to fail fast.
191
+ *
192
+ * @example
193
+ * ```ts
194
+ * import { ensureBinary } from 'quack-search';
195
+ *
196
+ * // Fail fast at app startup
197
+ * ensureBinary();
198
+ * ```
199
+ */
200
+ export function ensureBinary() {
201
+ const status = checkBinaryStatus();
202
+ if (!status.found) {
203
+ const targetPlatform = getPlatformTarget();
204
+ const packageName = `quack-search-${targetPlatform}`;
205
+ throw new QuackBinaryError(`Quack binary not available for ${status.platform} ${status.arch}`, "BINARY_NOT_AVAILABLE", `To fix this:\n` +
206
+ ` 1. Install platform package: npm install ${packageName}\n` +
207
+ ` 2. Or set QUACK_BINARY_PATH environment variable\n` +
208
+ ` 3. Or download explicitly: await downloadBinary()\n\n` +
209
+ `For Docker/CI: Set QUACK_BINARY_PATH or call downloadBinary() at build time.`);
210
+ }
24
211
  }
@@ -0,0 +1,49 @@
1
+ import { type SupportedPlatform } from "./binary";
2
+ /**
3
+ * Internal: Ensures binary is ready, downloading automatically if needed.
4
+ * This is called internally before any core execution.
5
+ * Users should never need to call this directly.
6
+ */
7
+ export declare function ensureBinaryReady(): Promise<string>;
8
+ /**
9
+ * Resets the bootstrap state. Useful for testing or forcing re-download.
10
+ * @internal
11
+ */
12
+ export declare function resetBootstrap(): void;
13
+ export interface DownloadOptions {
14
+ /** Target directory for the binary. Defaults to package bin directory. */
15
+ targetDir?: string;
16
+ /** Specific version to download. Defaults to 'latest'. */
17
+ version?: string;
18
+ /** Override platform detection. */
19
+ platform?: SupportedPlatform;
20
+ /** Show progress output. */
21
+ verbose?: boolean;
22
+ }
23
+ export interface DownloadResult {
24
+ binaryPath: string;
25
+ version: string;
26
+ platform: SupportedPlatform;
27
+ }
28
+ /**
29
+ * Downloads the quack binary for the current platform.
30
+ * Use this for explicit binary setup in Docker, CI, or bundled environments.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import { downloadBinary } from 'quack-search';
35
+ *
36
+ * // Download to default location
37
+ * const result = await downloadBinary();
38
+ * console.log(`Binary downloaded to: ${result.binaryPath}`);
39
+ *
40
+ * // Download to custom location
41
+ * await downloadBinary({ targetDir: '/app/bin' });
42
+ * ```
43
+ */
44
+ export declare function downloadBinary(options?: DownloadOptions): Promise<DownloadResult>;
45
+ /**
46
+ * Check if a binary download is needed.
47
+ * Returns true if no binary is currently available.
48
+ */
49
+ export declare function needsDownload(): boolean;
@@ -0,0 +1,228 @@
1
+ import { existsSync, mkdirSync, chmodSync, createWriteStream, unlinkSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { getPlatformTarget, getBinaryName, QuackBinaryError, checkBinaryStatus } from "./binary";
5
+ const GITHUB_RELEASE_BASE = "https://github.com/adistrim/quack/releases/download";
6
+ // Cache directory for auto-downloaded binaries (~/.cache/quack-search/)
7
+ function getCacheDir() {
8
+ const home = homedir();
9
+ if (process.platform === "win32") {
10
+ return join(process.env.LOCALAPPDATA || join(home, "AppData", "Local"), "quack-search");
11
+ }
12
+ return join(process.env.XDG_CACHE_HOME || join(home, ".cache"), "quack-search");
13
+ }
14
+ // ---------- Internal Bootstrap (Prisma-style auto-download) ----------
15
+ let bootstrapPromise = null;
16
+ /**
17
+ * Internal: Ensures binary is ready, downloading automatically if needed.
18
+ * This is called internally before any core execution.
19
+ * Users should never need to call this directly.
20
+ */
21
+ export async function ensureBinaryReady() {
22
+ // Return cached promise if bootstrap is in progress
23
+ if (bootstrapPromise) {
24
+ return bootstrapPromise;
25
+ }
26
+ bootstrapPromise = doBootstrap();
27
+ try {
28
+ return await bootstrapPromise;
29
+ }
30
+ catch (err) {
31
+ // Reset on failure so next call can retry
32
+ bootstrapPromise = null;
33
+ throw err;
34
+ }
35
+ }
36
+ async function doBootstrap() {
37
+ // check binary is already available
38
+ const status = checkBinaryStatus();
39
+ if (status.found && status.path) {
40
+ return status.path;
41
+ }
42
+ // download to cache directory
43
+ const cacheDir = getCacheDir();
44
+ const platform = getPlatformTarget();
45
+ const binaryName = getBinaryName();
46
+ const cachePath = join(cacheDir, binaryName);
47
+ // Check if already in cache (may have been downloaded previously)
48
+ if (existsSync(cachePath)) {
49
+ try {
50
+ const { accessSync, constants } = await import("node:fs");
51
+ accessSync(cachePath, constants.X_OK);
52
+ return cachePath;
53
+ }
54
+ catch {
55
+ // Cache exists but not executable, will re-download
56
+ }
57
+ }
58
+ // Download to cache
59
+ try {
60
+ const result = await downloadBinary({
61
+ targetDir: cacheDir,
62
+ version: "latest",
63
+ platform,
64
+ verbose: false,
65
+ });
66
+ return result.binaryPath;
67
+ }
68
+ catch (downloadErr) {
69
+ // Provide clear, actionable error
70
+ throw new QuackBinaryError(`Could not find or download quack binary for ${process.platform} ${process.arch}`, "BOOTSTRAP_FAILED", `Automatic download failed. To fix this:\n` +
71
+ ` 1. Check your internet connection\n` +
72
+ ` 2. Or install manually: npm install quack-search-${platform}\n` +
73
+ ` 3. Or set QUACK_BINARY_PATH to a pre-downloaded binary\n\n` +
74
+ `Original error: ${downloadErr instanceof Error ? downloadErr.message : String(downloadErr)}`);
75
+ }
76
+ }
77
+ /**
78
+ * Resets the bootstrap state. Useful for testing or forcing re-download.
79
+ * @internal
80
+ */
81
+ export function resetBootstrap() {
82
+ bootstrapPromise = null;
83
+ }
84
+ /**
85
+ * Downloads the quack binary for the current platform.
86
+ * Use this for explicit binary setup in Docker, CI, or bundled environments.
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * import { downloadBinary } from 'quack-search';
91
+ *
92
+ * // Download to default location
93
+ * const result = await downloadBinary();
94
+ * console.log(`Binary downloaded to: ${result.binaryPath}`);
95
+ *
96
+ * // Download to custom location
97
+ * await downloadBinary({ targetDir: '/app/bin' });
98
+ * ```
99
+ */
100
+ export async function downloadBinary(options = {}) {
101
+ const { targetDir = getDefaultBinDir(), version = "latest", platform = getPlatformTarget(), verbose = false, } = options;
102
+ const binaryName = getBinaryName();
103
+ const targetPath = join(targetDir, binaryName);
104
+ // Ensure target directory exists
105
+ if (!existsSync(targetDir)) {
106
+ mkdirSync(targetDir, { recursive: true });
107
+ }
108
+ // Resolve version (could fetch latest tag from GitHub API)
109
+ const resolvedVersion = version === "latest" ? await fetchLatestVersion() : version;
110
+ // Build download URL
111
+ const downloadUrl = buildDownloadUrl(platform, resolvedVersion);
112
+ if (verbose) {
113
+ console.log(`Downloading quack binary...`);
114
+ console.log(` Platform: ${platform}`);
115
+ console.log(` Version: ${resolvedVersion}`);
116
+ console.log(` URL: ${downloadUrl}`);
117
+ console.log(` Target: ${targetPath}`);
118
+ }
119
+ // Download the binary
120
+ await downloadFile(downloadUrl, targetPath);
121
+ // Make executable (non-Windows)
122
+ if (process.platform !== "win32") {
123
+ chmodSync(targetPath, 0o755);
124
+ }
125
+ if (verbose) {
126
+ console.log(`Binary downloaded successfully!`);
127
+ }
128
+ return {
129
+ binaryPath: targetPath,
130
+ version: resolvedVersion,
131
+ platform,
132
+ };
133
+ }
134
+ async function fetchLatestVersion() {
135
+ try {
136
+ const response = await fetch("https://api.github.com/repos/adistrim/quack/releases/latest", {
137
+ headers: {
138
+ Accept: "application/vnd.github.v3+json",
139
+ "User-Agent": "quack-search",
140
+ },
141
+ });
142
+ if (!response.ok) {
143
+ throw new QuackBinaryError(`Failed to fetch latest version: GitHub API returned ${response.status}`, "VERSION_FETCH_FAILED", `Specify an explicit version instead, e.g.: downloadBinary({ version: 'v0.1.0' })`);
144
+ }
145
+ const data = (await response.json());
146
+ if (!data.tag_name) {
147
+ throw new QuackBinaryError("Invalid response from GitHub API: missing tag_name", "VERSION_PARSE_FAILED", `Specify an explicit version instead, e.g.: downloadBinary({ version: 'v0.1.0' })`);
148
+ }
149
+ return data.tag_name;
150
+ }
151
+ catch (err) {
152
+ if (err instanceof QuackBinaryError) {
153
+ throw err;
154
+ }
155
+ throw new QuackBinaryError("Failed to fetch latest version from GitHub", "VERSION_FETCH_FAILED", `Network error or GitHub unavailable. Specify an explicit version, e.g.: downloadBinary({ version: 'v0.1.0' })`);
156
+ }
157
+ }
158
+ function buildDownloadUrl(platform, version) {
159
+ // Map platform to release asset naming convention
160
+ const assetMap = {
161
+ "darwin-arm64": "quack-darwin-arm64",
162
+ "darwin-x64": "quack-darwin-x64",
163
+ "linux-x64": "quack-linux-x64",
164
+ "linux-arm64": "quack-linux-arm64",
165
+ "windows-x64": "quack-windows-x64.exe",
166
+ "windows-arm64": "quack-windows-arm64.exe",
167
+ };
168
+ const assetName = assetMap[platform];
169
+ return `${GITHUB_RELEASE_BASE}/${version}/${assetName}`;
170
+ }
171
+ async function downloadFile(url, targetPath) {
172
+ const response = await fetch(url, {
173
+ headers: {
174
+ "User-Agent": "quack-search",
175
+ },
176
+ });
177
+ if (!response.ok) {
178
+ if (response.status === 404) {
179
+ throw new QuackBinaryError(`Binary not found at ${url}`, "DOWNLOAD_NOT_FOUND", `The release may not exist or the platform is not supported.\n` +
180
+ `Check available releases at: https://github.com/adistrim/quack/releases`);
181
+ }
182
+ throw new QuackBinaryError(`Failed to download binary: HTTP ${response.status}`, "DOWNLOAD_FAILED", `URL: ${url}`);
183
+ }
184
+ const body = response.body;
185
+ if (!body) {
186
+ throw new QuackBinaryError("Empty response body", "DOWNLOAD_EMPTY");
187
+ }
188
+ // Remove existing file if present
189
+ if (existsSync(targetPath)) {
190
+ unlinkSync(targetPath);
191
+ }
192
+ // Stream to file
193
+ const fileStream = createWriteStream(targetPath);
194
+ // Convert web stream to node stream for pipeline
195
+ const reader = body.getReader();
196
+ try {
197
+ while (true) {
198
+ const { done, value } = await reader.read();
199
+ if (done)
200
+ break;
201
+ fileStream.write(value);
202
+ }
203
+ fileStream.end();
204
+ await new Promise((resolve, reject) => {
205
+ fileStream.on("finish", resolve);
206
+ fileStream.on("error", reject);
207
+ });
208
+ }
209
+ catch (err) {
210
+ fileStream.close();
211
+ if (existsSync(targetPath)) {
212
+ unlinkSync(targetPath);
213
+ }
214
+ throw new QuackBinaryError("Failed to write binary file", "DOWNLOAD_WRITE_FAILED", err instanceof Error ? err.message : String(err));
215
+ }
216
+ }
217
+ function getDefaultBinDir() {
218
+ // Default to bin directory relative to this package
219
+ const thisDir = dirname(new URL(import.meta.url).pathname);
220
+ return join(thisDir, "..", "..", "bin");
221
+ }
222
+ /**
223
+ * Check if a binary download is needed.
224
+ * Returns true if no binary is currently available.
225
+ */
226
+ export function needsDownload() {
227
+ return !checkBinaryStatus().found;
228
+ }
@@ -1,2 +1,11 @@
1
1
  import type { CoreRequest, CoreResponse } from "../types/core";
2
+ export declare class QuackRuntimeError extends Error {
3
+ readonly code: string;
4
+ readonly details?: string | undefined;
5
+ constructor(message: string, code: string, details?: string | undefined);
6
+ }
7
+ /**
8
+ * Runs the core binary with auto-bootstrap.
9
+ * Binary is automatically downloaded if not present.
10
+ */
2
11
  export declare function runCore(payload: CoreRequest, timeoutMs?: number): Promise<CoreResponse>;
@@ -1,15 +1,37 @@
1
1
  import { spawn } from "node:child_process";
2
- import { resolveBinaryPath } from "./binary";
3
- export function runCore(payload, timeoutMs = 30_000) {
4
- return new Promise((resolve, reject) => {
5
- let binaryPath;
6
- try {
7
- binaryPath = resolveBinaryPath();
8
- }
9
- catch (err) {
10
- reject(err);
11
- return;
2
+ import { QuackBinaryError } from "./binary";
3
+ import { ensureBinaryReady } from "./download";
4
+ export class QuackRuntimeError extends Error {
5
+ code;
6
+ details;
7
+ constructor(message, code, details) {
8
+ super(message);
9
+ this.code = code;
10
+ this.details = details;
11
+ this.name = "QuackRuntimeError";
12
+ }
13
+ }
14
+ /**
15
+ * Runs the core binary with auto-bootstrap.
16
+ * Binary is automatically downloaded if not present.
17
+ */
18
+ export async function runCore(payload, timeoutMs = 30_000) {
19
+ // Auto-bootstrap: ensures binary exists, downloads if needed
20
+ let binaryPath;
21
+ try {
22
+ binaryPath = await ensureBinaryReady();
23
+ }
24
+ catch (err) {
25
+ if (err instanceof QuackBinaryError) {
26
+ throw err;
12
27
  }
28
+ throw new QuackRuntimeError("Failed to prepare quack binary", "BOOTSTRAP_FAILED", err instanceof Error ? err.message : String(err));
29
+ }
30
+ // Execute the binary
31
+ return executeCore(binaryPath, payload, timeoutMs);
32
+ }
33
+ function executeCore(binaryPath, payload, timeoutMs) {
34
+ return new Promise((resolve, reject) => {
13
35
  const proc = spawn(binaryPath, {
14
36
  stdio: ["pipe", "pipe", "pipe"],
15
37
  });
@@ -17,7 +39,7 @@ export function runCore(payload, timeoutMs = 30_000) {
17
39
  let stderr = "";
18
40
  const timeout = setTimeout(() => {
19
41
  proc.kill();
20
- reject(new Error("core process timed out"));
42
+ reject(new QuackRuntimeError(`Core process timed out after ${timeoutMs}ms`, "TIMEOUT", `Binary: ${binaryPath}`));
21
43
  }, timeoutMs);
22
44
  proc.stdout.on("data", (d) => {
23
45
  stdout += d.toString();
@@ -27,12 +49,22 @@ export function runCore(payload, timeoutMs = 30_000) {
27
49
  });
28
50
  proc.on("error", (err) => {
29
51
  clearTimeout(timeout);
30
- reject(err);
52
+ // Handle common spawn errors with actionable messages
53
+ if (err.code === "ENOENT") {
54
+ reject(new QuackRuntimeError(`Binary not found or not executable: ${binaryPath}`, "ENOENT", `The binary path was resolved but the file doesn't exist or isn't accessible.\n` +
55
+ `Check that the binary exists and has execute permissions.`));
56
+ return;
57
+ }
58
+ if (err.code === "EACCES") {
59
+ reject(new QuackRuntimeError(`Permission denied executing binary: ${binaryPath}`, "EACCES", `Run: chmod +x "${binaryPath}"`));
60
+ return;
61
+ }
62
+ reject(new QuackRuntimeError(`Failed to spawn core process: ${err.message}`, "SPAWN_ERROR", `Binary: ${binaryPath}`));
31
63
  });
32
64
  proc.on("close", (code) => {
33
65
  clearTimeout(timeout);
34
66
  if (code !== 0) {
35
- reject(new Error(stderr || "core process failed"));
67
+ reject(new QuackRuntimeError(stderr || `Core process exited with code ${code}`, "PROCESS_FAILED", `Exit code: ${code}, Binary: ${binaryPath}`));
36
68
  return;
37
69
  }
38
70
  let parsed;
@@ -40,15 +72,15 @@ export function runCore(payload, timeoutMs = 30_000) {
40
72
  parsed = JSON.parse(stdout);
41
73
  }
42
74
  catch {
43
- reject(new Error("invalid JSON returned from core"));
75
+ reject(new QuackRuntimeError("Invalid JSON returned from core", "INVALID_RESPONSE", `Raw output: ${stdout.slice(0, 200)}${stdout.length > 200 ? "..." : ""}`));
44
76
  return;
45
77
  }
46
78
  if (parsed.error) {
47
- reject(new Error(parsed.error));
79
+ reject(new QuackRuntimeError(parsed.error, "CORE_ERROR"));
48
80
  return;
49
81
  }
50
82
  if (!parsed.results && !parsed.fetch) {
51
- reject(new Error("empty response from core"));
83
+ reject(new QuackRuntimeError("Empty response from core", "EMPTY_RESPONSE"));
52
84
  return;
53
85
  }
54
86
  resolve(parsed);
package/dist/index.d.ts CHANGED
@@ -1,3 +1,6 @@
1
1
  export { search } from "./api/search";
2
2
  export { fetchContent } from "./api/fetch";
3
- export type { SearchOptions } from "./types/core";
3
+ export type { SearchOptions, SearchResult, FetchResult } from "./types/core";
4
+ export { checkBinaryStatus, ensureBinary, resolveBinaryPath, QuackBinaryError, getPlatformTarget, type BinaryStatus, type SupportedPlatform, } from "./core/binary";
5
+ export { QuackRuntimeError } from "./core/runner";
6
+ export { downloadBinary, needsDownload, type DownloadOptions, type DownloadResult, } from "./core/download";
package/dist/index.js CHANGED
@@ -1,42 +1,400 @@
1
1
  // @bun
2
+ var __create = Object.create;
3
+ var __getProtoOf = Object.getPrototypeOf;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __toESM = (mod, isNodeMode, target) => {
8
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
9
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
+ for (let key of __getOwnPropNames(mod))
11
+ if (!__hasOwnProp.call(to, key))
12
+ __defProp(to, key, {
13
+ get: () => mod[key],
14
+ enumerable: true
15
+ });
16
+ return to;
17
+ };
18
+ var __require = import.meta.require;
19
+
2
20
  // src/core/runner.ts
3
21
  import { spawn } from "child_process";
4
22
 
5
23
  // src/core/binary.ts
6
- import { platform, arch } from "process";
7
- import { createRequire } from "module";
8
- var require2 = createRequire(import.meta.url);
9
- function resolveBinaryPackage() {
10
- if (platform === "darwin" && arch === "arm64") {
11
- return "quack-search-darwin-arm64";
12
- }
13
- if (platform === "darwin" && arch === "x64") {
14
- return "quack-search-darwin-x64";
24
+ import { platform, arch, env } from "process";
25
+ import { join, dirname, resolve } from "path";
26
+ import { existsSync, accessSync, constants } from "fs";
27
+ import { fileURLToPath } from "url";
28
+ import { homedir } from "os";
29
+ function getPlatformTarget() {
30
+ const key = `${platform}-${arch}`;
31
+ const supported = {
32
+ "darwin-arm64": "darwin-arm64",
33
+ "darwin-x64": "darwin-x64",
34
+ "linux-x64": "linux-x64",
35
+ "linux-arm64": "linux-arm64",
36
+ "win32-x64": "windows-x64",
37
+ "win32-arm64": "windows-arm64"
38
+ };
39
+ const target = supported[key];
40
+ if (!target) {
41
+ throw new QuackBinaryError(`Unsupported platform: ${platform} ${arch}`, "UNSUPPORTED_PLATFORM", `quack-search supports: darwin-arm64, darwin-x64, linux-x64, linux-arm64, windows-x64, windows-arm64.
42
+ ` + `You can provide a custom binary via QUACK_BINARY_PATH environment variable.`);
15
43
  }
16
- if (platform === "linux" && arch === "x64") {
17
- return "quack-search-linux-x64";
44
+ return target;
45
+ }
46
+ function getBinaryName() {
47
+ return platform === "win32" ? "quack.exe" : "quack";
48
+ }
49
+
50
+ class QuackBinaryError extends Error {
51
+ code;
52
+ hint;
53
+ constructor(message, code, hint) {
54
+ super(message);
55
+ this.code = code;
56
+ this.hint = hint;
57
+ this.name = "QuackBinaryError";
18
58
  }
19
- if (platform === "win32" && arch === "x64") {
20
- return "quack-search-windows-x64";
59
+ toString() {
60
+ let msg = `${this.name}: ${this.message}`;
61
+ if (this.hint) {
62
+ msg += `
63
+
64
+ Hint: ${this.hint}`;
65
+ }
66
+ return msg;
21
67
  }
22
- throw new Error(`unsupported platform: ${platform} ${arch}`);
23
68
  }
24
69
  function resolveBinaryPath() {
25
- const pkgName = resolveBinaryPackage();
26
- const pkgEntry = require2.resolve(pkgName);
27
- return pkgEntry;
70
+ const result = tryResolveBinary();
71
+ return result.path;
28
72
  }
73
+ function tryResolveBinary() {
74
+ const envPath = env.QUACK_BINARY_PATH;
75
+ if (envPath) {
76
+ if (!existsSync(envPath)) {
77
+ throw new QuackBinaryError(`Binary not found at QUACK_BINARY_PATH: ${envPath}`, "ENV_BINARY_NOT_FOUND", `Ensure the file exists and is executable.`);
78
+ }
79
+ assertExecutable(envPath);
80
+ return { path: envPath, source: "env" };
81
+ }
82
+ const nodeModulesPath = tryResolveFromNodeModules();
83
+ if (nodeModulesPath) {
84
+ return { path: nodeModulesPath, source: "node_modules" };
85
+ }
86
+ const localPath = tryResolveLocalBinary();
87
+ if (localPath) {
88
+ return { path: localPath, source: "local" };
89
+ }
90
+ const targetPlatform = getPlatformTarget();
91
+ const packageName = `quack-search-${targetPlatform}`;
92
+ throw new QuackBinaryError(`Could not find quack binary for ${platform} ${arch}`, "BINARY_NOT_FOUND", `To fix this, try one of:
93
+ ` + ` 1. Install the platform package: npm install ${packageName}
94
+ ` + ` 2. Set QUACK_BINARY_PATH to point to your quack binary
95
+ ` + ` 3. Download the binary from: https://github.com/adistrim/quack/releases
29
96
 
30
- // src/core/runner.ts
31
- function runCore(payload, timeoutMs = 30000) {
32
- return new Promise((resolve, reject) => {
33
- let binaryPath;
97
+ ` + `If you're using Docker or a bundler, set QUACK_BINARY_PATH explicitly.`);
98
+ }
99
+ function tryResolveFromNodeModules() {
100
+ const targetPlatform = getPlatformTarget();
101
+ const packageName = `quack-search-${targetPlatform}`;
102
+ const binaryName = getBinaryName();
103
+ const thisDir = getModuleDir();
104
+ const searchPaths = [
105
+ join(thisDir, "..", "..", "..", "node_modules", packageName, "bin", binaryName),
106
+ join(thisDir, "..", "..", "node_modules", packageName, "bin", binaryName),
107
+ join(thisDir, "..", "..", packageName, "bin", binaryName),
108
+ join(thisDir, "..", "..", "..", ".pnpm", "node_modules", packageName, "bin", binaryName)
109
+ ];
110
+ const cwd = process.cwd();
111
+ searchPaths.push(join(cwd, "node_modules", packageName, "bin", binaryName), join(cwd, "..", "node_modules", packageName, "bin", binaryName));
112
+ for (const searchPath of searchPaths) {
113
+ const resolved = resolve(searchPath);
114
+ if (existsSync(resolved)) {
115
+ try {
116
+ assertExecutable(resolved);
117
+ return resolved;
118
+ } catch {}
119
+ }
120
+ }
121
+ return null;
122
+ }
123
+ function tryResolveLocalBinary() {
124
+ const binaryName = getBinaryName();
125
+ const thisDir = getModuleDir();
126
+ const localPaths = [
127
+ join(thisDir, "..", "..", "bin", binaryName),
128
+ join(thisDir, "..", "bin", binaryName),
129
+ join(thisDir, "bin", binaryName)
130
+ ];
131
+ const cacheDir = getCacheDir();
132
+ localPaths.push(join(cacheDir, binaryName));
133
+ for (const localPath of localPaths) {
134
+ const resolved = resolve(localPath);
135
+ if (existsSync(resolved)) {
136
+ try {
137
+ assertExecutable(resolved);
138
+ return resolved;
139
+ } catch {}
140
+ }
141
+ }
142
+ return null;
143
+ }
144
+ function getCacheDir() {
145
+ const home = homedir();
146
+ if (platform === "win32") {
147
+ return join(env.LOCALAPPDATA || join(home, "AppData", "Local"), "quack-search");
148
+ }
149
+ return join(env.XDG_CACHE_HOME || join(home, ".cache"), "quack-search");
150
+ }
151
+ function assertExecutable(filePath) {
152
+ try {
153
+ accessSync(filePath, constants.X_OK);
154
+ } catch {
155
+ throw new QuackBinaryError(`Binary exists but is not executable: ${filePath}`, "BINARY_NOT_EXECUTABLE", `Run: chmod +x "${filePath}"`);
156
+ }
157
+ }
158
+ function getModuleDir() {
159
+ if (typeof import.meta.dir === "string") {
160
+ return import.meta.dir;
161
+ }
162
+ return dirname(fileURLToPath(import.meta.url));
163
+ }
164
+ function checkBinaryStatus() {
165
+ const base = { platform, arch };
166
+ try {
167
+ const result = tryResolveBinary();
168
+ return {
169
+ ...base,
170
+ found: true,
171
+ path: result.path,
172
+ source: result.source
173
+ };
174
+ } catch (err) {
175
+ return {
176
+ ...base,
177
+ found: false,
178
+ error: err instanceof Error ? err.message : String(err)
179
+ };
180
+ }
181
+ }
182
+ function ensureBinary() {
183
+ const status = checkBinaryStatus();
184
+ if (!status.found) {
185
+ const targetPlatform = getPlatformTarget();
186
+ const packageName = `quack-search-${targetPlatform}`;
187
+ throw new QuackBinaryError(`Quack binary not available for ${status.platform} ${status.arch}`, "BINARY_NOT_AVAILABLE", `To fix this:
188
+ ` + ` 1. Install platform package: npm install ${packageName}
189
+ ` + ` 2. Or set QUACK_BINARY_PATH environment variable
190
+ ` + ` 3. Or download explicitly: await downloadBinary()
191
+
192
+ ` + `For Docker/CI: Set QUACK_BINARY_PATH or call downloadBinary() at build time.`);
193
+ }
194
+ }
195
+
196
+ // src/core/download.ts
197
+ import { existsSync as existsSync2, mkdirSync, chmodSync, createWriteStream, unlinkSync } from "fs";
198
+ import { join as join2, dirname as dirname2 } from "path";
199
+ import { homedir as homedir2 } from "os";
200
+ var GITHUB_RELEASE_BASE = "https://github.com/adistrim/quack/releases/download";
201
+ function getCacheDir2() {
202
+ const home = homedir2();
203
+ if (process.platform === "win32") {
204
+ return join2(process.env.LOCALAPPDATA || join2(home, "AppData", "Local"), "quack-search");
205
+ }
206
+ return join2(process.env.XDG_CACHE_HOME || join2(home, ".cache"), "quack-search");
207
+ }
208
+ var bootstrapPromise = null;
209
+ async function ensureBinaryReady() {
210
+ if (bootstrapPromise) {
211
+ return bootstrapPromise;
212
+ }
213
+ bootstrapPromise = doBootstrap();
214
+ try {
215
+ return await bootstrapPromise;
216
+ } catch (err) {
217
+ bootstrapPromise = null;
218
+ throw err;
219
+ }
220
+ }
221
+ async function doBootstrap() {
222
+ const status = checkBinaryStatus();
223
+ if (status.found && status.path) {
224
+ return status.path;
225
+ }
226
+ const cacheDir = getCacheDir2();
227
+ const platform2 = getPlatformTarget();
228
+ const binaryName = getBinaryName();
229
+ const cachePath = join2(cacheDir, binaryName);
230
+ if (existsSync2(cachePath)) {
34
231
  try {
35
- binaryPath = resolveBinaryPath();
36
- } catch (err) {
37
- reject(err);
38
- return;
232
+ const { accessSync: accessSync2, constants: constants2 } = await import("fs");
233
+ accessSync2(cachePath, constants2.X_OK);
234
+ return cachePath;
235
+ } catch {}
236
+ }
237
+ try {
238
+ const result = await downloadBinary({
239
+ targetDir: cacheDir,
240
+ version: "latest",
241
+ platform: platform2,
242
+ verbose: false
243
+ });
244
+ return result.binaryPath;
245
+ } catch (downloadErr) {
246
+ throw new QuackBinaryError(`Could not find or download quack binary for ${process.platform} ${process.arch}`, "BOOTSTRAP_FAILED", `Automatic download failed. To fix this:
247
+ 1. Check your internet connection
248
+ 2. Or install manually: npm install quack-search-${platform2}
249
+ 3. Or set QUACK_BINARY_PATH to a pre-downloaded binary
250
+
251
+ Original error: ${downloadErr instanceof Error ? downloadErr.message : String(downloadErr)}`);
252
+ }
253
+ }
254
+ async function downloadBinary(options = {}) {
255
+ const {
256
+ targetDir = getDefaultBinDir(),
257
+ version = "latest",
258
+ platform: platform2 = getPlatformTarget(),
259
+ verbose = false
260
+ } = options;
261
+ const binaryName = getBinaryName();
262
+ const targetPath = join2(targetDir, binaryName);
263
+ if (!existsSync2(targetDir)) {
264
+ mkdirSync(targetDir, { recursive: true });
265
+ }
266
+ const resolvedVersion = version === "latest" ? await fetchLatestVersion() : version;
267
+ const downloadUrl = buildDownloadUrl(platform2, resolvedVersion);
268
+ if (verbose) {
269
+ console.log(`Downloading quack binary...`);
270
+ console.log(` Platform: ${platform2}`);
271
+ console.log(` Version: ${resolvedVersion}`);
272
+ console.log(` URL: ${downloadUrl}`);
273
+ console.log(` Target: ${targetPath}`);
274
+ }
275
+ await downloadFile(downloadUrl, targetPath);
276
+ if (process.platform !== "win32") {
277
+ chmodSync(targetPath, 493);
278
+ }
279
+ if (verbose) {
280
+ console.log(`Binary downloaded successfully!`);
281
+ }
282
+ return {
283
+ binaryPath: targetPath,
284
+ version: resolvedVersion,
285
+ platform: platform2
286
+ };
287
+ }
288
+ async function fetchLatestVersion() {
289
+ try {
290
+ const response = await fetch("https://api.github.com/repos/adistrim/quack/releases/latest", {
291
+ headers: {
292
+ Accept: "application/vnd.github.v3+json",
293
+ "User-Agent": "quack-search"
294
+ }
295
+ });
296
+ if (!response.ok) {
297
+ throw new QuackBinaryError(`Failed to fetch latest version: GitHub API returned ${response.status}`, "VERSION_FETCH_FAILED", `Specify an explicit version instead, e.g.: downloadBinary({ version: 'v0.1.0' })`);
298
+ }
299
+ const data = await response.json();
300
+ if (!data.tag_name) {
301
+ throw new QuackBinaryError("Invalid response from GitHub API: missing tag_name", "VERSION_PARSE_FAILED", `Specify an explicit version instead, e.g.: downloadBinary({ version: 'v0.1.0' })`);
302
+ }
303
+ return data.tag_name;
304
+ } catch (err) {
305
+ if (err instanceof QuackBinaryError) {
306
+ throw err;
307
+ }
308
+ throw new QuackBinaryError("Failed to fetch latest version from GitHub", "VERSION_FETCH_FAILED", `Network error or GitHub unavailable. Specify an explicit version, e.g.: downloadBinary({ version: 'v0.1.0' })`);
309
+ }
310
+ }
311
+ function buildDownloadUrl(platform2, version) {
312
+ const assetMap = {
313
+ "darwin-arm64": "quack-darwin-arm64",
314
+ "darwin-x64": "quack-darwin-x64",
315
+ "linux-x64": "quack-linux-x64",
316
+ "linux-arm64": "quack-linux-arm64",
317
+ "windows-x64": "quack-windows-x64.exe",
318
+ "windows-arm64": "quack-windows-arm64.exe"
319
+ };
320
+ const assetName = assetMap[platform2];
321
+ return `${GITHUB_RELEASE_BASE}/${version}/${assetName}`;
322
+ }
323
+ async function downloadFile(url, targetPath) {
324
+ const response = await fetch(url, {
325
+ headers: {
326
+ "User-Agent": "quack-search"
327
+ }
328
+ });
329
+ if (!response.ok) {
330
+ if (response.status === 404) {
331
+ throw new QuackBinaryError(`Binary not found at ${url}`, "DOWNLOAD_NOT_FOUND", `The release may not exist or the platform is not supported.
332
+ Check available releases at: https://github.com/adistrim/quack/releases`);
333
+ }
334
+ throw new QuackBinaryError(`Failed to download binary: HTTP ${response.status}`, "DOWNLOAD_FAILED", `URL: ${url}`);
335
+ }
336
+ const body = response.body;
337
+ if (!body) {
338
+ throw new QuackBinaryError("Empty response body", "DOWNLOAD_EMPTY");
339
+ }
340
+ if (existsSync2(targetPath)) {
341
+ unlinkSync(targetPath);
342
+ }
343
+ const fileStream = createWriteStream(targetPath);
344
+ const reader = body.getReader();
345
+ try {
346
+ while (true) {
347
+ const { done, value } = await reader.read();
348
+ if (done)
349
+ break;
350
+ fileStream.write(value);
351
+ }
352
+ fileStream.end();
353
+ await new Promise((resolve2, reject) => {
354
+ fileStream.on("finish", resolve2);
355
+ fileStream.on("error", reject);
356
+ });
357
+ } catch (err) {
358
+ fileStream.close();
359
+ if (existsSync2(targetPath)) {
360
+ unlinkSync(targetPath);
361
+ }
362
+ throw new QuackBinaryError("Failed to write binary file", "DOWNLOAD_WRITE_FAILED", err instanceof Error ? err.message : String(err));
363
+ }
364
+ }
365
+ function getDefaultBinDir() {
366
+ const thisDir = dirname2(new URL(import.meta.url).pathname);
367
+ return join2(thisDir, "..", "..", "bin");
368
+ }
369
+ function needsDownload() {
370
+ return !checkBinaryStatus().found;
371
+ }
372
+
373
+ // src/core/runner.ts
374
+ class QuackRuntimeError extends Error {
375
+ code;
376
+ details;
377
+ constructor(message, code, details) {
378
+ super(message);
379
+ this.code = code;
380
+ this.details = details;
381
+ this.name = "QuackRuntimeError";
382
+ }
383
+ }
384
+ async function runCore(payload, timeoutMs = 30000) {
385
+ let binaryPath;
386
+ try {
387
+ binaryPath = await ensureBinaryReady();
388
+ } catch (err) {
389
+ if (err instanceof QuackBinaryError) {
390
+ throw err;
39
391
  }
392
+ throw new QuackRuntimeError("Failed to prepare quack binary", "BOOTSTRAP_FAILED", err instanceof Error ? err.message : String(err));
393
+ }
394
+ return executeCore(binaryPath, payload, timeoutMs);
395
+ }
396
+ function executeCore(binaryPath, payload, timeoutMs) {
397
+ return new Promise((resolve2, reject) => {
40
398
  const proc = spawn(binaryPath, {
41
399
  stdio: ["pipe", "pipe", "pipe"]
42
400
  });
@@ -44,7 +402,7 @@ function runCore(payload, timeoutMs = 30000) {
44
402
  let stderr = "";
45
403
  const timeout = setTimeout(() => {
46
404
  proc.kill();
47
- reject(new Error("core process timed out"));
405
+ reject(new QuackRuntimeError(`Core process timed out after ${timeoutMs}ms`, "TIMEOUT", `Binary: ${binaryPath}`));
48
406
  }, timeoutMs);
49
407
  proc.stdout.on("data", (d) => {
50
408
  stdout += d.toString();
@@ -54,30 +412,39 @@ function runCore(payload, timeoutMs = 30000) {
54
412
  });
55
413
  proc.on("error", (err) => {
56
414
  clearTimeout(timeout);
57
- reject(err);
415
+ if (err.code === "ENOENT") {
416
+ reject(new QuackRuntimeError(`Binary not found or not executable: ${binaryPath}`, "ENOENT", `The binary path was resolved but the file doesn't exist or isn't accessible.
417
+ ` + `Check that the binary exists and has execute permissions.`));
418
+ return;
419
+ }
420
+ if (err.code === "EACCES") {
421
+ reject(new QuackRuntimeError(`Permission denied executing binary: ${binaryPath}`, "EACCES", `Run: chmod +x "${binaryPath}"`));
422
+ return;
423
+ }
424
+ reject(new QuackRuntimeError(`Failed to spawn core process: ${err.message}`, "SPAWN_ERROR", `Binary: ${binaryPath}`));
58
425
  });
59
426
  proc.on("close", (code) => {
60
427
  clearTimeout(timeout);
61
428
  if (code !== 0) {
62
- reject(new Error(stderr || "core process failed"));
429
+ reject(new QuackRuntimeError(stderr || `Core process exited with code ${code}`, "PROCESS_FAILED", `Exit code: ${code}, Binary: ${binaryPath}`));
63
430
  return;
64
431
  }
65
432
  let parsed;
66
433
  try {
67
434
  parsed = JSON.parse(stdout);
68
435
  } catch {
69
- reject(new Error("invalid JSON returned from core"));
436
+ reject(new QuackRuntimeError("Invalid JSON returned from core", "INVALID_RESPONSE", `Raw output: ${stdout.slice(0, 200)}${stdout.length > 200 ? "..." : ""}`));
70
437
  return;
71
438
  }
72
439
  if (parsed.error) {
73
- reject(new Error(parsed.error));
440
+ reject(new QuackRuntimeError(parsed.error, "CORE_ERROR"));
74
441
  return;
75
442
  }
76
443
  if (!parsed.results && !parsed.fetch) {
77
- reject(new Error("empty response from core"));
444
+ reject(new QuackRuntimeError("Empty response from core", "EMPTY_RESPONSE"));
78
445
  return;
79
446
  }
80
- resolve(parsed);
447
+ resolve2(parsed);
81
448
  });
82
449
  proc.stdin.write(JSON.stringify(payload));
83
450
  proc.stdin.end();
@@ -103,5 +470,13 @@ async function fetchContent(url, timeoutMs) {
103
470
  }
104
471
  export {
105
472
  search,
106
- fetchContent
473
+ resolveBinaryPath,
474
+ needsDownload,
475
+ getPlatformTarget,
476
+ fetchContent,
477
+ ensureBinary,
478
+ downloadBinary,
479
+ checkBinaryStatus,
480
+ QuackRuntimeError,
481
+ QuackBinaryError
107
482
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quack-search",
3
- "version": "0.0.4",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -20,10 +20,12 @@
20
20
  "typescript": "^5"
21
21
  },
22
22
  "optionalDependencies": {
23
- "quack-search-darwin-arm64": "0.0.4",
24
- "quack-search-darwin-x64": "0.0.4",
25
- "quack-search-linux-x64": "0.0.4",
26
- "quack-search-windows-x64": "0.0.4"
23
+ "quack-search-darwin-arm64": "0.1.1",
24
+ "quack-search-darwin-x64": "0.1.1",
25
+ "quack-search-linux-x64": "0.1.1",
26
+ "quack-search-linux-arm64": "0.1.1",
27
+ "quack-search-windows-x64": "0.1.1",
28
+ "quack-search-windows-arm64": "0.1.1"
27
29
  },
28
30
  "repository": {
29
31
  "type": "git",