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