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 +53 -3
- package/dist/api/fetch.d.ts +10 -0
- package/dist/api/fetch.js +10 -0
- package/dist/api/search.d.ts +9 -0
- package/dist/api/search.js +9 -0
- package/dist/core/binary.d.ts +47 -0
- package/dist/core/binary.js +203 -18
- package/dist/core/download.d.ts +49 -0
- package/dist/core/download.js +226 -0
- package/dist/core/runner.d.ts +9 -0
- package/dist/core/runner.js +48 -16
- package/dist/index.d.ts +4 -1
- package/dist/index.js +404 -33
- package/package.json +5 -5
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
package/dist/api/fetch.d.ts
CHANGED
|
@@ -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",
|
package/dist/api/search.d.ts
CHANGED
|
@@ -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[]>;
|
package/dist/api/search.js
CHANGED
|
@@ -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",
|
package/dist/core/binary.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/core/binary.js
CHANGED
|
@@ -1,24 +1,209 @@
|
|
|
1
|
-
import { platform, arch } from "node:process";
|
|
2
|
-
import { dirname,
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
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
|
+
}
|
package/dist/core/runner.d.ts
CHANGED
|
@@ -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>;
|
package/dist/core/runner.js
CHANGED
|
@@ -1,15 +1,37 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
79
|
+
reject(new QuackRuntimeError(parsed.error, "CORE_ERROR"));
|
|
48
80
|
return;
|
|
49
81
|
}
|
|
50
82
|
if (!parsed.results && !parsed.fetch) {
|
|
51
|
-
reject(new
|
|
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 {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
|
26
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
436
|
+
reject(new QuackRuntimeError(parsed.error, "CORE_ERROR"));
|
|
74
437
|
return;
|
|
75
438
|
}
|
|
76
439
|
if (!parsed.results && !parsed.fetch) {
|
|
77
|
-
reject(new
|
|
440
|
+
reject(new QuackRuntimeError("Empty response from core", "EMPTY_RESPONSE"));
|
|
78
441
|
return;
|
|
79
442
|
}
|
|
80
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
24
|
-
"quack-search-darwin-x64": "0.0
|
|
25
|
-
"quack-search-linux-x64": "0.0
|
|
26
|
-
"quack-search-windows-x64": "0.0
|
|
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",
|