@geekjourneyx/findo 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/package.json +34 -0
- package/scripts/install.js +274 -0
- package/scripts/run.js +26 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## v1.0.0
|
|
4
|
+
|
|
5
|
+
- Initial stable CLI contract.
|
|
6
|
+
- Add Bocha web search, Volcengine web-grounded answer, and Zhihu search/hotlist adapters.
|
|
7
|
+
- Add JSON envelope, source status, stable error codes, and release gates.
|
|
8
|
+
- Add GitHub Actions CI and tag-triggered cross-platform release builds.
|
|
9
|
+
- Add npm global installer package for the matching GitHub Release binary.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Findo contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# Findo
|
|
4
|
+
|
|
5
|
+
**Search Chinese web sources from one Go CLI.**
|
|
6
|
+
|
|
7
|
+
<img src="assets/banner.webp" alt="Findo - Chinese web sources from one Go CLI" width="100%">
|
|
8
|
+
|
|
9
|
+
[](./LICENSE)
|
|
10
|
+
[](./go.mod)
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
Findo queries Bocha, Volcengine Ark, and Zhihu through provider APIs, then returns normalized terminal output or automation-safe JSON. It is built for developers, AI agents, and research workflows that need Chinese internet retrieval without scraping, browser sessions, or hidden side effects.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
Recommended:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g @geekjourneyx/findo
|
|
22
|
+
findo version
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The npm package installs the matching GitHub Release binary for your platform and verifies it against `SHA256SUMS`.
|
|
26
|
+
|
|
27
|
+
Alternative Go install:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
go install github.com/geekjourneyx/findo/cmd/findo@v1.0.0
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Prebuilt binaries and checksums are available on the [GitHub Releases](https://github.com/geekjourneyx/findo/releases) page.
|
|
34
|
+
|
|
35
|
+
From a local checkout:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
make build
|
|
39
|
+
./findo version
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Configure
|
|
43
|
+
|
|
44
|
+
Set the credentials for the providers you want to use:
|
|
45
|
+
|
|
46
|
+
| Provider | Environment variables |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| Bocha | `BOCHA_API_KEY` |
|
|
49
|
+
| Volcengine Ark | `ARK_API_KEY` or `VOLCENGINE_API_KEY` |
|
|
50
|
+
| Zhihu | `ZHIHU_ACCESS_SECRET` or `ZHIHU_API_KEY` |
|
|
51
|
+
|
|
52
|
+
Configuration precedence is:
|
|
53
|
+
|
|
54
|
+
1. CLI flags
|
|
55
|
+
2. Environment variables
|
|
56
|
+
3. Config file
|
|
57
|
+
4. Built-in defaults
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
findo version
|
|
63
|
+
findo sources --json
|
|
64
|
+
|
|
65
|
+
BOCHA_API_KEY=... findo bocha "AI Agent 商业化" --json
|
|
66
|
+
ARK_API_KEY=... findo volc "瑞幸咖啡 2026 门店数是否可信" --json
|
|
67
|
+
ZHIHU_ACCESS_SECRET=... findo zhihu "AI 搜索" --json
|
|
68
|
+
ZHIHU_ACCESS_SECRET=... findo zhihu web "ChatGPT 桌面版" --json
|
|
69
|
+
ZHIHU_ACCESS_SECRET=... findo hot zhihu --json
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Human output defaults to a table. Use `--json` for scripts, agents, CI, and downstream tools.
|
|
73
|
+
|
|
74
|
+
## Output
|
|
75
|
+
|
|
76
|
+
Retrieval commands return a stable envelope. A successful JSON response looks like this:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"version": "1.0.0",
|
|
81
|
+
"query": {
|
|
82
|
+
"text": "AI Agent 商业化",
|
|
83
|
+
"mode": "search",
|
|
84
|
+
"sources": ["bocha_web"],
|
|
85
|
+
"limit": 10
|
|
86
|
+
},
|
|
87
|
+
"status": "ok",
|
|
88
|
+
"results": [
|
|
89
|
+
{
|
|
90
|
+
"source": "bocha_web",
|
|
91
|
+
"title": "Example result title",
|
|
92
|
+
"url": "https://example.com/article",
|
|
93
|
+
"snippet": "A normalized summary from the provider response."
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
"source_status": [
|
|
97
|
+
{
|
|
98
|
+
"source": "bocha_web",
|
|
99
|
+
"status": "ok",
|
|
100
|
+
"results": 1,
|
|
101
|
+
"effective_limit": 10,
|
|
102
|
+
"duration_ms": 842,
|
|
103
|
+
"error": null
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
"errors": []
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Exit codes are part of the public contract:
|
|
111
|
+
|
|
112
|
+
| Code | Meaning |
|
|
113
|
+
| --- | --- |
|
|
114
|
+
| `0` | Success |
|
|
115
|
+
| `1` | Partial success |
|
|
116
|
+
| `2` | Invalid argument |
|
|
117
|
+
| `3` | Config error |
|
|
118
|
+
| `4` | Credential missing |
|
|
119
|
+
| `5` | Source error |
|
|
120
|
+
| `6` | Timeout |
|
|
121
|
+
| `7` | No results |
|
|
122
|
+
| `9` | Internal error |
|
|
123
|
+
|
|
124
|
+
## Sources
|
|
125
|
+
|
|
126
|
+
| Source ID | Command | Provider | Capability |
|
|
127
|
+
| --- | --- | --- | --- |
|
|
128
|
+
| `bocha_web` | `findo bocha` | Bocha | Web search |
|
|
129
|
+
| `volcengine_answer` | `findo volc` | Volcengine Ark | Web-grounded answer |
|
|
130
|
+
| `zhihu_search` | `findo zhihu` | Zhihu | In-site search |
|
|
131
|
+
| `zhihu_web` | `findo zhihu web` | Zhihu | Global web search |
|
|
132
|
+
| `zhihu_hot` | `findo hot zhihu` | Zhihu | Hotlist |
|
|
133
|
+
|
|
134
|
+
Zhihu global search supports provider-specific filters:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
findo zhihu web "ChatGPT 桌面版" \
|
|
138
|
+
--filter 'host=="example.com"' \
|
|
139
|
+
--search-db realtime \
|
|
140
|
+
--json
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`--filter` and `--search-db` are only valid for `findo zhihu web`.
|
|
144
|
+
|
|
145
|
+
## Automation Contract
|
|
146
|
+
|
|
147
|
+
Findo keeps the automation contract narrow and predictable:
|
|
148
|
+
|
|
149
|
+
- stdout is reserved for results.
|
|
150
|
+
- stderr is reserved for diagnostics.
|
|
151
|
+
- JSON output keeps source IDs, source status, error codes, and exit codes stable.
|
|
152
|
+
- Provider behavior stays behind typed Go adapters.
|
|
153
|
+
- Built-in timeout defaults to `45s`; override it with `--timeout` when a workflow needs a different budget.
|
|
154
|
+
|
|
155
|
+
## Development
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
make build
|
|
159
|
+
make test
|
|
160
|
+
make lint
|
|
161
|
+
make release-check
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The normal test suite does not require real provider credentials. Real API smoke checks are separate:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
make smoke-bocha
|
|
168
|
+
make smoke-volcengine
|
|
169
|
+
make smoke-zhihu
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Non-Goals
|
|
173
|
+
|
|
174
|
+
Findo v1.0.0 intentionally does not implement browser scraping, cache, reranking, plugin runtime, MCP, stdin query input, Bocha image search, or Zhihu direct answer. Those boundaries keep the CLI small, testable, and stable.
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
[MIT](./LICENSE)
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@geekjourneyx/findo",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Install the findo CLI from the matching GitHub Release bundle.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/geekjourneyx/findo.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/geekjourneyx/findo#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/geekjourneyx/findo/issues"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"findo": "scripts/run.js"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"pack:check": "npm pack --json --dry-run",
|
|
19
|
+
"postinstall": "node scripts/install.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"scripts/install.js",
|
|
23
|
+
"scripts/run.js",
|
|
24
|
+
"README.md",
|
|
25
|
+
"CHANGELOG.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const http = require("http");
|
|
4
|
+
const https = require("https");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { execFileSync } = require("child_process");
|
|
8
|
+
const { fileURLToPath } = require("url");
|
|
9
|
+
const zlib = require("zlib");
|
|
10
|
+
|
|
11
|
+
const pkg = require("../package.json");
|
|
12
|
+
|
|
13
|
+
const VERSION = pkg.version;
|
|
14
|
+
const REPO = "geekjourneyx/findo";
|
|
15
|
+
const PACKAGE_NAME = pkg.name;
|
|
16
|
+
|
|
17
|
+
const TARGETS = {
|
|
18
|
+
darwin: {
|
|
19
|
+
x64: { goos: "darwin", goarch: "amd64", archive: "tar.gz" },
|
|
20
|
+
arm64: { goos: "darwin", goarch: "arm64", archive: "tar.gz" },
|
|
21
|
+
},
|
|
22
|
+
linux: {
|
|
23
|
+
x64: { goos: "linux", goarch: "amd64", archive: "tar.gz" },
|
|
24
|
+
arm64: { goos: "linux", goarch: "arm64", archive: "tar.gz" },
|
|
25
|
+
},
|
|
26
|
+
win32: {
|
|
27
|
+
x64: { goos: "windows", goarch: "amd64", archive: "zip" },
|
|
28
|
+
arm64: { goos: "windows", goarch: "arm64", archive: "zip" },
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const target = TARGETS[process.platform]?.[process.arch];
|
|
33
|
+
const releaseBaseUrl =
|
|
34
|
+
process.env.FINDO_RELEASE_BASE_URL ||
|
|
35
|
+
`https://github.com/${REPO}/releases/download/v${VERSION}`;
|
|
36
|
+
const binaryName = process.platform === "win32" ? "findo.exe" : "findo";
|
|
37
|
+
const binDir = path.join(__dirname, "..", "bin");
|
|
38
|
+
const destination = path.join(binDir, binaryName);
|
|
39
|
+
|
|
40
|
+
if (!target) {
|
|
41
|
+
console.error(
|
|
42
|
+
[
|
|
43
|
+
`Unsupported platform for ${PACKAGE_NAME}: ${process.platform}-${process.arch}`,
|
|
44
|
+
"Supported npm install targets are:",
|
|
45
|
+
" - darwin-x64",
|
|
46
|
+
" - darwin-arm64",
|
|
47
|
+
" - linux-x64",
|
|
48
|
+
" - linux-arm64",
|
|
49
|
+
" - win32-x64",
|
|
50
|
+
" - win32-arm64",
|
|
51
|
+
].join("\n")
|
|
52
|
+
);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const archiveName = `findo_${VERSION}_${target.goos}_${target.goarch}.${target.archive}`;
|
|
57
|
+
|
|
58
|
+
function hasScheme(value) {
|
|
59
|
+
return (
|
|
60
|
+
/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(value) && !path.win32.isAbsolute(value)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resolveAssetLocation(base, name) {
|
|
65
|
+
if (!hasScheme(base)) {
|
|
66
|
+
return path.join(base, name);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return base.endsWith("/") ? `${base}${name}` : `${base}/${name}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function downloadToFile(source, destinationPath) {
|
|
73
|
+
if (!hasScheme(source)) {
|
|
74
|
+
fs.copyFileSync(source, destinationPath);
|
|
75
|
+
return Promise.resolve();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (source.startsWith("file://")) {
|
|
79
|
+
fs.copyFileSync(fileURLToPath(source), destinationPath);
|
|
80
|
+
return Promise.resolve();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const client = source.startsWith("https:") ? https : http;
|
|
85
|
+
|
|
86
|
+
client
|
|
87
|
+
.get(source, (response) => {
|
|
88
|
+
if (
|
|
89
|
+
(response.statusCode === 301 ||
|
|
90
|
+
response.statusCode === 302 ||
|
|
91
|
+
response.statusCode === 307 ||
|
|
92
|
+
response.statusCode === 308) &&
|
|
93
|
+
response.headers.location
|
|
94
|
+
) {
|
|
95
|
+
response.resume();
|
|
96
|
+
downloadToFile(response.headers.location, destinationPath).then(
|
|
97
|
+
resolve,
|
|
98
|
+
reject
|
|
99
|
+
);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (response.statusCode !== 200) {
|
|
104
|
+
response.resume();
|
|
105
|
+
reject(
|
|
106
|
+
new Error(
|
|
107
|
+
`download failed with status ${response.statusCode}: ${source}`
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const file = fs.createWriteStream(destinationPath);
|
|
114
|
+
response.pipe(file);
|
|
115
|
+
file.on("finish", () => file.close(resolve));
|
|
116
|
+
file.on("error", reject);
|
|
117
|
+
})
|
|
118
|
+
.on("error", reject);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function sha256(filePath) {
|
|
123
|
+
const hash = crypto.createHash("sha256");
|
|
124
|
+
hash.update(fs.readFileSync(filePath));
|
|
125
|
+
return hash.digest("hex");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function expectedChecksum(checksumsPath, filename) {
|
|
129
|
+
const line = fs
|
|
130
|
+
.readFileSync(checksumsPath, "utf8")
|
|
131
|
+
.split(/\r?\n/)
|
|
132
|
+
.find((entry) => entry.trim().endsWith(` ${filename}`));
|
|
133
|
+
|
|
134
|
+
if (!line) {
|
|
135
|
+
throw new Error(`SHA256SUMS does not contain an entry for ${filename}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return line.trim().split(/\s+/)[0].toLowerCase();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function extractArchive(archivePath, extractDir) {
|
|
142
|
+
if (archiveName.endsWith(".tar.gz")) {
|
|
143
|
+
extractTarGz(archivePath, extractDir);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (archiveName.endsWith(".zip") && process.platform === "win32") {
|
|
148
|
+
const archiveLiteral = powershellSingleQuoted(archivePath);
|
|
149
|
+
const extractLiteral = powershellSingleQuoted(extractDir);
|
|
150
|
+
execFileSync("powershell.exe", [
|
|
151
|
+
"-NoProfile",
|
|
152
|
+
"-Command",
|
|
153
|
+
`Expand-Archive -LiteralPath ${archiveLiteral} -DestinationPath ${extractLiteral} -Force`,
|
|
154
|
+
]);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
throw new Error(`unsupported archive format for this platform: ${archiveName}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function powershellSingleQuoted(value) {
|
|
162
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function readTarString(buffer, start, length) {
|
|
166
|
+
return buffer
|
|
167
|
+
.subarray(start, start + length)
|
|
168
|
+
.toString("utf8")
|
|
169
|
+
.replace(/\0.*$/, "");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function extractTarGz(archivePath, extractDir) {
|
|
173
|
+
const data = zlib.gunzipSync(fs.readFileSync(archivePath));
|
|
174
|
+
let offset = 0;
|
|
175
|
+
|
|
176
|
+
while (offset + 512 <= data.length) {
|
|
177
|
+
const header = data.subarray(offset, offset + 512);
|
|
178
|
+
if (header.every((byte) => byte === 0)) {
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const name = readTarString(header, 0, 100);
|
|
183
|
+
const prefix = readTarString(header, 345, 155);
|
|
184
|
+
const fullName = prefix ? `${prefix}/${name}` : name;
|
|
185
|
+
const sizeText = readTarString(header, 124, 12).trim();
|
|
186
|
+
const size = sizeText ? parseInt(sizeText, 8) : 0;
|
|
187
|
+
const typeflag = readTarString(header, 156, 1) || "0";
|
|
188
|
+
const bodyOffset = offset + 512;
|
|
189
|
+
const nextOffset = bodyOffset + Math.ceil(size / 512) * 512;
|
|
190
|
+
|
|
191
|
+
const destinationPath = path.resolve(extractDir, fullName);
|
|
192
|
+
const extractRoot = path.resolve(extractDir);
|
|
193
|
+
if (
|
|
194
|
+
destinationPath !== extractRoot &&
|
|
195
|
+
!destinationPath.startsWith(`${extractRoot}${path.sep}`)
|
|
196
|
+
) {
|
|
197
|
+
throw new Error(`refusing to extract path outside target directory: ${fullName}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (typeflag === "5") {
|
|
201
|
+
fs.mkdirSync(destinationPath, { recursive: true });
|
|
202
|
+
} else if (typeflag === "0") {
|
|
203
|
+
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
204
|
+
fs.writeFileSync(destinationPath, data.subarray(bodyOffset, bodyOffset + size));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
offset = nextOffset;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function findBinary(root) {
|
|
212
|
+
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
213
|
+
for (const entry of entries) {
|
|
214
|
+
const fullPath = path.join(root, entry.name);
|
|
215
|
+
if (entry.isDirectory()) {
|
|
216
|
+
const found = findBinary(fullPath);
|
|
217
|
+
if (found) {
|
|
218
|
+
return found;
|
|
219
|
+
}
|
|
220
|
+
} else if (entry.isFile() && entry.name === binaryName) {
|
|
221
|
+
return fullPath;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return "";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function install() {
|
|
228
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "findo-npm-"));
|
|
229
|
+
const archivePath = path.join(tmpDir, archiveName);
|
|
230
|
+
const checksumsPath = path.join(tmpDir, "SHA256SUMS");
|
|
231
|
+
const extractDir = path.join(tmpDir, "extract");
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
235
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
236
|
+
|
|
237
|
+
await downloadToFile(
|
|
238
|
+
resolveAssetLocation(releaseBaseUrl, archiveName),
|
|
239
|
+
archivePath
|
|
240
|
+
);
|
|
241
|
+
await downloadToFile(
|
|
242
|
+
resolveAssetLocation(releaseBaseUrl, "SHA256SUMS"),
|
|
243
|
+
checksumsPath
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const expected = expectedChecksum(checksumsPath, archiveName);
|
|
247
|
+
const actual = sha256(archivePath);
|
|
248
|
+
if (expected !== actual) {
|
|
249
|
+
throw new Error(`checksum mismatch for ${archiveName}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
extractArchive(archivePath, extractDir);
|
|
253
|
+
const extractedBinary = findBinary(extractDir);
|
|
254
|
+
if (!extractedBinary) {
|
|
255
|
+
throw new Error(`archive does not contain ${binaryName}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
fs.copyFileSync(extractedBinary, destination);
|
|
259
|
+
if (process.platform !== "win32") {
|
|
260
|
+
fs.chmodSync(destination, 0o755);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
console.log(
|
|
264
|
+
`Installed findo ${VERSION} from ${resolveAssetLocation(releaseBaseUrl, archiveName)}`
|
|
265
|
+
);
|
|
266
|
+
} finally {
|
|
267
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
install().catch((error) => {
|
|
272
|
+
console.error(`Failed to install ${PACKAGE_NAME}: ${error.message}`);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
});
|
package/scripts/run.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { execFileSync } = require("child_process");
|
|
6
|
+
|
|
7
|
+
const ext = process.platform === "win32" ? ".exe" : "";
|
|
8
|
+
const binaryPath = path.join(__dirname, "..", "bin", `findo${ext}`);
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(binaryPath)) {
|
|
11
|
+
console.error(
|
|
12
|
+
"findo binary is missing. Reinstall with `npm install -g @geekjourneyx/findo`."
|
|
13
|
+
);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
execFileSync(binaryPath, process.argv.slice(2), { stdio: "inherit" });
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (typeof error.status === "number") {
|
|
21
|
+
process.exit(error.status);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.error(`Failed to launch findo: ${error.message}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|