@ninjaxtools/slopdex 0.19.0 → 0.20.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/.gitignore +2 -0
- package/README.md +134 -46
- package/binary-install.js +348 -0
- package/binary.js +124 -0
- package/install.js +4 -0
- package/npm-shrinkwrap.json +52 -0
- package/package.json +78 -61
- package/run-slopdex.js +4 -0
- package/dist/chunk-5BO2LLXC.js +0 -15
- package/dist/chunk-5BO2LLXC.js.map +0 -1
- package/dist/chunk-BXWO2KMC.js +0 -20
- package/dist/chunk-BXWO2KMC.js.map +0 -1
- package/dist/chunk-DKRB2XT5.js +0 -33
- package/dist/chunk-DKRB2XT5.js.map +0 -1
- package/dist/chunk-DTI7SXGL.js +0 -109
- package/dist/chunk-DTI7SXGL.js.map +0 -1
- package/dist/chunk-GBEJHETS.js +0 -1117
- package/dist/chunk-GBEJHETS.js.map +0 -1
- package/dist/chunk-HMKVMRLF.js +0 -2221
- package/dist/chunk-HMKVMRLF.js.map +0 -1
- package/dist/chunk-IQU3YVZ3.js +0 -85
- package/dist/chunk-IQU3YVZ3.js.map +0 -1
- package/dist/chunk-JAVPHCZH.js +0 -24
- package/dist/chunk-JAVPHCZH.js.map +0 -1
- package/dist/chunk-MKUTTXZB.js +0 -74
- package/dist/chunk-MKUTTXZB.js.map +0 -1
- package/dist/chunk-OIKBO3NJ.js +0 -133
- package/dist/chunk-OIKBO3NJ.js.map +0 -1
- package/dist/chunk-P57ZJREE.js +0 -426
- package/dist/chunk-P57ZJREE.js.map +0 -1
- package/dist/chunk-S225GYCL.js +0 -79
- package/dist/chunk-S225GYCL.js.map +0 -1
- package/dist/chunk-TSURHRFF.js +0 -240
- package/dist/chunk-TSURHRFF.js.map +0 -1
- package/dist/chunk-VH5VGRCI.js +0 -103
- package/dist/chunk-VH5VGRCI.js.map +0 -1
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +0 -1130
- package/dist/cli.js.map +0 -1
- package/dist/code-index-MWSJKV3G.js +0 -14
- package/dist/code-index-MWSJKV3G.js.map +0 -1
- package/dist/cross-search-ERBFC25T.js +0 -10
- package/dist/cross-search-ERBFC25T.js.map +0 -1
- package/dist/database-P5XWJWQL.js +0 -14
- package/dist/database-P5XWJWQL.js.map +0 -1
- package/dist/hosted-GHIUHKOU.js +0 -13
- package/dist/hosted-GHIUHKOU.js.map +0 -1
- package/dist/index.d.ts +0 -681
- package/dist/index.js +0 -102
- package/dist/index.js.map +0 -1
- package/dist/jina-43RL7C6M.js +0 -12
- package/dist/jina-43RL7C6M.js.map +0 -1
- package/dist/openai-CYUPNBGB.js +0 -12
- package/dist/openai-CYUPNBGB.js.map +0 -1
- package/dist/openai-EC6THXKV.js +0 -21
- package/dist/openai-EC6THXKV.js.map +0 -1
- package/dist/openai-TCRXM66O.js +0 -11
- package/dist/openai-TCRXM66O.js.map +0 -1
- package/docs/implementation.md +0 -162
- package/docs/reference.md +0 -187
package/.gitignore
ADDED
package/README.md
CHANGED
|
@@ -4,40 +4,61 @@ This readme is written by a human.
|
|
|
4
4
|
|
|
5
5
|
# slopdex
|
|
6
6
|
|
|
7
|
-
<
|
|
7
|
+
<picture>
|
|
8
|
+
<source media="(max-width: 600px)" srcset="sloppy-dexter.png" width="600" />
|
|
9
|
+
<img src="sloppy-dexter.png" width="280" align="right" alt="Dexter, the sloppy slime" />
|
|
10
|
+
</picture>
|
|
8
11
|
|
|
9
|
-
Slopdex helps with doing analysis on codebases that contain a lot of AI generated
|
|
12
|
+
Slopdex helps with doing analysis on codebases that contain a lot of AI generated ~~slop~~ slime.
|
|
10
13
|
|
|
11
14
|
The main supported functions are
|
|
12
15
|
|
|
13
16
|
- `slopdex search <query>`
|
|
14
|
-
<br/>
|
|
17
|
+
<br/>find code/docs similar to the query
|
|
15
18
|
- `slopdex cross-search`
|
|
16
|
-
<br/>
|
|
19
|
+
<br/>find clusters of similar code/docs
|
|
20
|
+
- `slopdex describe <query>`
|
|
21
|
+
<br/>explain code relevant to a task
|
|
17
22
|
|
|
18
|
-
`
|
|
23
|
+
`search` does a vector searche of code, and markdown, and when enabled generated code descriptions.
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
`cross-search` also does a vector search but compares all functions with each other (scope can be limited with additional options) and helps with finding duplicated code, or code that is not necessarily duplicated but spread out across the codebase (with `--cohesion`).
|
|
21
26
|
|
|
22
|
-
|
|
27
|
+
`describe` does a `search` first, then passes the result through the configured LLM model to create a tailored description.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
23
30
|
|
|
24
|
-
|
|
31
|
+
Install from npm:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
25
34
|
npm install -g @ninjaxtools/slopdex
|
|
35
|
+
```
|
|
26
36
|
|
|
27
|
-
|
|
28
|
-
# Or
|
|
29
|
-
# export JINA_API_KEY="your-api-key" # and pass --provider jina
|
|
37
|
+
You can also download a binary from one of the releases: https://github.com/ninjaxtools/slopdex/releases
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
Or you can build from source:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
cargo install --path . --locked
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Getting started
|
|
46
|
+
|
|
47
|
+
An embedding-provider API key is required. You can use either OpenAI or Jina.
|
|
48
|
+
|
|
49
|
+
```console
|
|
50
|
+
$ export OPENAI_API_KEY="your-api-key"
|
|
51
|
+
$ slopdex search "validate an authenticated session"
|
|
52
|
+
...
|
|
32
53
|
```
|
|
33
54
|
|
|
34
55
|
The index tracks the current git commit and is created or updated on every command and stored in `.slopdex/index.sqlite`. Add `.slopdex/` to your repository's `.gitignore` to prevent it from being committed.
|
|
35
56
|
|
|
36
|
-
### Search
|
|
57
|
+
### Search
|
|
37
58
|
|
|
38
|
-
By default
|
|
59
|
+
By default all available indexes are searched and ranked together.
|
|
39
60
|
|
|
40
|
-
```
|
|
61
|
+
```console
|
|
41
62
|
$ slopdex search "keep the repository index synchronized"
|
|
42
63
|
0.4284 tests/languages.test.ts :: refresh
|
|
43
64
|
0.4200 src/cli.ts :: refreshIndex
|
|
@@ -45,35 +66,65 @@ $ slopdex search "keep the repository index synchronized"
|
|
|
45
66
|
...
|
|
46
67
|
```
|
|
47
68
|
|
|
69
|
+
To search code/docs individually:
|
|
70
|
+
|
|
71
|
+
```console
|
|
72
|
+
$ slopdex search-code "configure the embedding provider"
|
|
73
|
+
$ slopdex search-md "configure the embedding provider"
|
|
74
|
+
```
|
|
75
|
+
|
|
48
76
|
You can also enable optional description generation which will automatically generate file and function descriptions with a configured LLM provider and include them in the search.
|
|
49
77
|
|
|
50
78
|
I use OpenCode Go usually with DeepSeek or Muse Spark, which are fairly good low-cost models. If you
|
|
51
79
|
sign up for OpenCode Go through [this link](https://opencode.ai/go?ref=RAR3Z744DZ), we both receive
|
|
52
80
|
$5 in credit.
|
|
53
81
|
|
|
54
|
-
```
|
|
55
|
-
export OPENAI_API_KEY="your-api-key"
|
|
82
|
+
```console
|
|
83
|
+
$ export OPENAI_API_KEY="your-api-key"
|
|
56
84
|
# Or
|
|
57
85
|
# export OPENCODE_API_KEY="your-api-key" # for OpenCode Zen/Go descriptions
|
|
58
86
|
# Or
|
|
59
87
|
# opencode auth login # use stored credentials instead of env variables
|
|
60
88
|
|
|
61
|
-
slopdex descriptions enable
|
|
62
|
-
slopdex search-
|
|
89
|
+
$ slopdex descriptions enable
|
|
90
|
+
$ slopdex search-descriptions "keep the repository index synchronized"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
You can list and configure one of OpenCode's models like this (or use the interactive config):
|
|
94
|
+
|
|
95
|
+
```console
|
|
96
|
+
$ slopdex models opencode-go
|
|
97
|
+
$ slopdex config model opencode-go/gpt-5.6-luna
|
|
98
|
+
$ slopdex config fallback-model opencode-go/muse-spark-1.3-contributor
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The fallback-model is used if the main model reports an error, and if the fallback-model reports an error the main model is tried again.
|
|
102
|
+
|
|
103
|
+
File and function descriptions are cached. Function descriptions will only be regenerated when functions change. File descriptions need to be regenerated explcitily with the `reindex-files` command.
|
|
104
|
+
|
|
105
|
+
### Interactive Config
|
|
106
|
+
|
|
107
|
+
To configure all configurable settings interactively run:
|
|
108
|
+
|
|
109
|
+
```console
|
|
110
|
+
$ slopdex config
|
|
63
111
|
```
|
|
64
112
|
|
|
65
|
-
|
|
113
|
+
### Search and describe
|
|
114
|
+
|
|
115
|
+
The intention of the `describe` command is to use a low-cost model to summarise vector search findings to provide more relevant pre-processed results to a more powerful calling agent. It first performs a vector search and then uses the results to provide a tailored summary/description of relevant code and pre-generated generated file and function descriptions.
|
|
66
116
|
|
|
67
|
-
```
|
|
68
|
-
slopdex
|
|
69
|
-
slopdex config model opencode-go/gpt-5.6-luna
|
|
117
|
+
```console
|
|
118
|
+
$ slopdex describe "I want to implement a new rpc endpoint"
|
|
70
119
|
```
|
|
71
120
|
|
|
121
|
+
For the best matching files (configured with `--describe-full-file-threshold`, default `0.8`) the full file contents are used.
|
|
122
|
+
|
|
72
123
|
### Find duplicate code
|
|
73
124
|
|
|
74
125
|
Compare functions across files, exclude short wrappers, and group matches into clusters:
|
|
75
126
|
|
|
76
|
-
```
|
|
127
|
+
```console
|
|
77
128
|
$ slopdex cross-search --cross-file-only --min-lines 4 --threshold 0.9
|
|
78
129
|
Cluster 1 (3 functions, similarity 0.9124-0.9568)
|
|
79
130
|
src/auth/session.ts:18:1 :: validateSession
|
|
@@ -84,39 +135,44 @@ Cluster 1 (3 functions, similarity 0.9124-0.9568)
|
|
|
84
135
|
|
|
85
136
|
Using `--cross-file-only` is useful to exclude similar code in the same file.
|
|
86
137
|
|
|
87
|
-
|
|
138
|
+
Cross-search defaults to `--threshold 0.8` (query search defaults to `0.3`).
|
|
139
|
+
Clusters join matches transitively, so a low threshold can connect many groups
|
|
140
|
+
into one large cluster. Use `0.9` for stricter duplicate detection, or explicitly
|
|
141
|
+
pass `--threshold 0.3` for the pre-rewrite default.
|
|
142
|
+
|
|
143
|
+
You can use threshold ranges as well:
|
|
88
144
|
|
|
89
|
-
```
|
|
90
|
-
slopdex cross-search --cross-file-only --min-lines 4 --threshold 0.9
|
|
91
|
-
slopdex cross-search --cross-file-only --min-lines 4 --threshold 0.85-0.9
|
|
145
|
+
```console
|
|
146
|
+
$ slopdex cross-search --cross-file-only --min-lines 4 --threshold 0.9
|
|
147
|
+
$ slopdex cross-search --cross-file-only --min-lines 4 --threshold 0.85-0.9
|
|
92
148
|
```
|
|
93
149
|
|
|
94
150
|
### Restrict functions used in the cross-search
|
|
95
151
|
|
|
96
152
|
Only use uncommitted working-tree functions as sources:
|
|
97
153
|
|
|
98
|
-
```
|
|
99
|
-
slopdex cross-search --uncommitted --cross-file-only --min-lines 4 --threshold 0.9
|
|
154
|
+
```console
|
|
155
|
+
$ slopdex cross-search --uncommitted --cross-file-only --min-lines 4 --threshold 0.9
|
|
100
156
|
```
|
|
101
157
|
|
|
102
158
|
Only use functions changed since origin/main as sources:
|
|
103
159
|
|
|
104
|
-
```
|
|
105
|
-
slopdex cross-search --changed-since origin/main --threshold 0.9
|
|
160
|
+
```console
|
|
161
|
+
$ slopdex cross-search --changed-since origin/main --threshold 0.9
|
|
106
162
|
```
|
|
107
163
|
|
|
108
164
|
Only use matching symbols under src/services as sources:
|
|
109
165
|
|
|
110
|
-
```
|
|
111
|
-
slopdex cross-search --source-path src/services -e '^UserService\.' --threshold 0.9
|
|
166
|
+
```console
|
|
167
|
+
$ slopdex cross-search --source-path src/services -e '^UserService\.' --threshold 0.9
|
|
112
168
|
```
|
|
113
169
|
|
|
114
170
|
### Find related code stored far apart
|
|
115
171
|
|
|
116
172
|
When code is similar but not actually duplicated, then `--cohesion` can help find similar code that exists far apart in the filesystem tree, which could potentially be refactored to make it more cohesive, by ordering matches from farthest to nearest.
|
|
117
173
|
|
|
118
|
-
```
|
|
119
|
-
slopdex cross-search --cross-file-only --cohesion --threshold 0.8
|
|
174
|
+
```console
|
|
175
|
+
$ slopdex cross-search --cross-file-only --cohesion --threshold 0.8
|
|
120
176
|
```
|
|
121
177
|
### Use with agents
|
|
122
178
|
|
|
@@ -131,19 +187,19 @@ Any other use, like doing a full `cross-search` is probably better done interact
|
|
|
131
187
|
|
|
132
188
|
### Reranking
|
|
133
189
|
|
|
134
|
-
Optionally a second-stage reranker can be enabled for
|
|
190
|
+
Optionally a second-stage reranker can be enabled for all query-search commands:
|
|
135
191
|
|
|
136
|
-
```
|
|
137
|
-
export COHERE_API_KEY="your-api-key"
|
|
138
|
-
slopdex config reranker cohere
|
|
192
|
+
```console
|
|
193
|
+
$ export COHERE_API_KEY="your-api-key"
|
|
194
|
+
$ slopdex config reranker cohere
|
|
139
195
|
# Or: export JINA_API_KEY="your-api-key" && slopdex config reranker jina
|
|
140
196
|
# Or use an LLM: export OPENAI_API_KEY="your-api-key" && slopdex config reranker openai
|
|
141
197
|
```
|
|
142
198
|
|
|
143
199
|
### Compare repositories
|
|
144
200
|
|
|
145
|
-
```
|
|
146
|
-
slopdex cross-search \
|
|
201
|
+
```console
|
|
202
|
+
$ slopdex cross-search \
|
|
147
203
|
--target-root /path/to/other/repo \
|
|
148
204
|
--target-index /path/to/other/repo/.slopdex/index.sqlite \
|
|
149
205
|
--threshold 0.9
|
|
@@ -153,10 +209,42 @@ slopdex cross-search \
|
|
|
153
209
|
|
|
154
210
|
If some functions can't be indexed a warning is printed. Index errors can be investigated and fixed with the `index-errors` command to ensure the index is complete.
|
|
155
211
|
|
|
156
|
-
```
|
|
157
|
-
slopdex status
|
|
158
|
-
slopdex index-errors --format summary
|
|
159
|
-
slopdex --version
|
|
212
|
+
```console
|
|
213
|
+
$ slopdex status
|
|
214
|
+
$ slopdex index-errors --format summary
|
|
215
|
+
$ slopdex --version
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Related Work
|
|
219
|
+
|
|
220
|
+
Code similarity search
|
|
221
|
+
|
|
222
|
+
- [treepeat](https://github.com/dsummersl/treepeat)
|
|
223
|
+
|
|
224
|
+
Structured code queries:
|
|
225
|
+
|
|
226
|
+
- [CodeGraphContext](https://github.com/CodeGraphContext/CodeGraphContext)
|
|
227
|
+
- [CoderLM](https://github.com/JaredStewart/coderlm)
|
|
228
|
+
|
|
229
|
+
Structured code queries and edits:
|
|
230
|
+
|
|
231
|
+
- [srgn](https://github.com/alexpovel/srgn)
|
|
232
|
+
- [comby](https://github.com/comby-tools/comby)
|
|
233
|
+
- [ast-grep](https://ast-grep.github.io/guide/introduction)
|
|
234
|
+
- [gritql](https://github.com/biomejs/gritql)
|
|
235
|
+
|
|
236
|
+
Semantic code search with vector embeddings:
|
|
237
|
+
|
|
238
|
+
- [qmd](https://github.com/tobi/qmd)
|
|
239
|
+
- [grepai](https://github.com/yoanbernabeu/grepai)
|
|
240
|
+
- [cocoindex-code](https://github.com/cocoindex-io/cocoindex-code)
|
|
241
|
+
- [codana](https://github.com/bartolli/codanna)
|
|
242
|
+
- [open-codebase-index](https://github.com/Helweg/open-codebase-index)
|
|
243
|
+
|
|
244
|
+
## Publishing
|
|
245
|
+
|
|
246
|
+
```console
|
|
247
|
+
$ cargo release minor --no-publish --execute
|
|
160
248
|
```
|
|
161
249
|
|
|
162
250
|
## Commands
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
const {
|
|
2
|
+
createWriteStream,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
mkdtemp,
|
|
6
|
+
rmSync,
|
|
7
|
+
} = require("fs");
|
|
8
|
+
const { join, sep } = require("path");
|
|
9
|
+
const { spawnSync } = require("child_process");
|
|
10
|
+
const { tmpdir } = require("os");
|
|
11
|
+
|
|
12
|
+
const https = require("node:https");
|
|
13
|
+
const http = require("node:http");
|
|
14
|
+
|
|
15
|
+
const tmpDir = tmpdir();
|
|
16
|
+
|
|
17
|
+
const error = (msg) => {
|
|
18
|
+
console.error(msg);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function getProxyForUrl(urlString) {
|
|
23
|
+
const url = new URL(urlString);
|
|
24
|
+
const isHttps = url.protocol === "https:";
|
|
25
|
+
|
|
26
|
+
const noProxy = process.env.NO_PROXY || process.env.no_proxy || "";
|
|
27
|
+
if (noProxy === "*") return null;
|
|
28
|
+
if (noProxy) {
|
|
29
|
+
const hostname = url.hostname.toLowerCase();
|
|
30
|
+
const noProxyList = noProxy.split(",").map((s) => s.trim().toLowerCase());
|
|
31
|
+
for (const entry of noProxyList) {
|
|
32
|
+
if (hostname === entry || hostname.endsWith("." + entry)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const proxyEnv = isHttps
|
|
39
|
+
? process.env.HTTPS_PROXY || process.env.https_proxy
|
|
40
|
+
: process.env.HTTP_PROXY || process.env.http_proxy;
|
|
41
|
+
|
|
42
|
+
if (!proxyEnv) return null;
|
|
43
|
+
|
|
44
|
+
const proxyUrl = new URL(proxyEnv);
|
|
45
|
+
|
|
46
|
+
let auth = null;
|
|
47
|
+
if (proxyUrl.username || proxyUrl.password) {
|
|
48
|
+
auth = `${proxyUrl.username}:${proxyUrl.password}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
hostname: proxyUrl.hostname,
|
|
53
|
+
port: proxyUrl.port || (proxyUrl.protocol === "https:" ? 443 : 80),
|
|
54
|
+
auth: auth,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function connectThroughProxy(proxy, target) {
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const headers = {};
|
|
61
|
+
if (proxy.auth) {
|
|
62
|
+
headers["Proxy-Authorization"] =
|
|
63
|
+
"Basic " + Buffer.from(proxy.auth).toString("base64");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const connectReq = http.request({
|
|
67
|
+
hostname: proxy.hostname,
|
|
68
|
+
port: proxy.port,
|
|
69
|
+
method: "CONNECT",
|
|
70
|
+
path: `${target.hostname}:${target.port || 443}`,
|
|
71
|
+
headers,
|
|
72
|
+
});
|
|
73
|
+
connectReq.on("connect", (res, socket) => {
|
|
74
|
+
if (res.statusCode === 200) {
|
|
75
|
+
resolve(socket);
|
|
76
|
+
} else {
|
|
77
|
+
reject(new Error(`Proxy CONNECT failed with status ${res.statusCode}`));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
connectReq.on("error", reject);
|
|
81
|
+
connectReq.end();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function download(urlString, maxRedirects) {
|
|
86
|
+
if (maxRedirects === undefined) maxRedirects = 5;
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
if (maxRedirects < 0) {
|
|
89
|
+
return reject(new Error("Too many redirects"));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const parsed = new URL(urlString);
|
|
93
|
+
const isHttps = parsed.protocol === "https:";
|
|
94
|
+
const mod = isHttps ? https : http;
|
|
95
|
+
const proxy = getProxyForUrl(urlString);
|
|
96
|
+
|
|
97
|
+
const doRequest = (extraOptions) => {
|
|
98
|
+
const options = Object.assign(
|
|
99
|
+
{
|
|
100
|
+
hostname: parsed.hostname,
|
|
101
|
+
port: parsed.port || (isHttps ? 443 : 80),
|
|
102
|
+
path: parsed.pathname + parsed.search,
|
|
103
|
+
method: "GET",
|
|
104
|
+
headers: { "User-Agent": "cargo-dist-npm-installer" },
|
|
105
|
+
},
|
|
106
|
+
extraOptions || {},
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (proxy && !isHttps) {
|
|
110
|
+
// HTTP through HTTP proxy: request the full URL via the proxy
|
|
111
|
+
options.hostname = proxy.hostname;
|
|
112
|
+
options.port = proxy.port;
|
|
113
|
+
options.path = urlString;
|
|
114
|
+
if (proxy.auth) {
|
|
115
|
+
options.headers["Proxy-Authorization"] =
|
|
116
|
+
"Basic " + Buffer.from(proxy.auth).toString("base64");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const req = mod.request(options, (res) => {
|
|
121
|
+
if (
|
|
122
|
+
res.statusCode >= 300 &&
|
|
123
|
+
res.statusCode < 400 &&
|
|
124
|
+
res.headers.location
|
|
125
|
+
) {
|
|
126
|
+
res.resume();
|
|
127
|
+
const nextUrl = new URL(res.headers.location, urlString).toString();
|
|
128
|
+
return download(nextUrl, maxRedirects - 1).then(resolve, reject);
|
|
129
|
+
}
|
|
130
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
131
|
+
res.resume();
|
|
132
|
+
return reject(new Error(`HTTP ${res.statusCode} from ${urlString}`));
|
|
133
|
+
}
|
|
134
|
+
resolve(res);
|
|
135
|
+
});
|
|
136
|
+
req.on("error", reject);
|
|
137
|
+
req.end();
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
if (proxy && isHttps) {
|
|
141
|
+
connectThroughProxy(proxy, parsed).then(
|
|
142
|
+
(socket) => doRequest({ socket, agent: false }),
|
|
143
|
+
reject,
|
|
144
|
+
);
|
|
145
|
+
} else {
|
|
146
|
+
doRequest();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
class Package {
|
|
152
|
+
constructor(platform, name, url, filename, zipExt, binaries) {
|
|
153
|
+
let errors = [];
|
|
154
|
+
if (typeof url !== "string") {
|
|
155
|
+
errors.push("url must be a string");
|
|
156
|
+
} else {
|
|
157
|
+
try {
|
|
158
|
+
new URL(url);
|
|
159
|
+
} catch (e) {
|
|
160
|
+
errors.push(e);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (name && typeof name !== "string") {
|
|
164
|
+
errors.push("package name must be a string");
|
|
165
|
+
}
|
|
166
|
+
if (!name) {
|
|
167
|
+
errors.push("You must specify the name of your package");
|
|
168
|
+
}
|
|
169
|
+
if (binaries && typeof binaries !== "object") {
|
|
170
|
+
errors.push("binaries must be a string => string map");
|
|
171
|
+
}
|
|
172
|
+
if (!binaries) {
|
|
173
|
+
errors.push("You must specify the binaries in the package");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (errors.length > 0) {
|
|
177
|
+
let errorMsg =
|
|
178
|
+
"One or more of the parameters you passed to the Binary constructor are invalid:\n";
|
|
179
|
+
errors.forEach((error) => {
|
|
180
|
+
errorMsg += error;
|
|
181
|
+
});
|
|
182
|
+
errorMsg +=
|
|
183
|
+
'\n\nCorrect usage: new Package("my-binary", "https://example.com/binary/download.tar.gz", {"my-binary": "my-binary"})';
|
|
184
|
+
error(errorMsg);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.platform = platform;
|
|
188
|
+
this.url = url;
|
|
189
|
+
this.name = name;
|
|
190
|
+
this.filename = filename;
|
|
191
|
+
this.zipExt = zipExt;
|
|
192
|
+
this.installDirectory = join(__dirname, "node_modules", ".bin_real");
|
|
193
|
+
this.binaries = binaries;
|
|
194
|
+
|
|
195
|
+
if (!existsSync(this.installDirectory)) {
|
|
196
|
+
mkdirSync(this.installDirectory, { recursive: true });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
exists() {
|
|
201
|
+
for (const binaryName in this.binaries) {
|
|
202
|
+
const binRelPath = this.binaries[binaryName];
|
|
203
|
+
const binPath = join(this.installDirectory, binRelPath);
|
|
204
|
+
if (!existsSync(binPath)) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
install(suppressLogs = false) {
|
|
212
|
+
if (this.exists()) {
|
|
213
|
+
if (!suppressLogs) {
|
|
214
|
+
console.error(
|
|
215
|
+
`${this.name} is already installed, skipping installation.`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
return Promise.resolve();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
rmSync(this.installDirectory, { recursive: true, force: true });
|
|
223
|
+
} catch {
|
|
224
|
+
// ignore - directory may not exist
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
mkdirSync(this.installDirectory, { recursive: true });
|
|
228
|
+
|
|
229
|
+
if (!suppressLogs) {
|
|
230
|
+
console.error(`Downloading release from ${this.url}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return download(this.url)
|
|
234
|
+
.then((res) => {
|
|
235
|
+
return new Promise((resolve, reject) => {
|
|
236
|
+
mkdtemp(`${tmpDir}${sep}`, (err, directory) => {
|
|
237
|
+
if (err) return reject(err);
|
|
238
|
+
let tempFile = join(directory, this.filename);
|
|
239
|
+
const sink = res.pipe(createWriteStream(tempFile));
|
|
240
|
+
sink.on("error", (err) => reject(err));
|
|
241
|
+
sink.on("close", () => {
|
|
242
|
+
if (/\.tar\.*/.test(this.zipExt)) {
|
|
243
|
+
const result = spawnSync("tar", [
|
|
244
|
+
"xf",
|
|
245
|
+
tempFile,
|
|
246
|
+
// The tarballs are stored with a leading directory
|
|
247
|
+
// component; we strip one component in the
|
|
248
|
+
// shell installers too.
|
|
249
|
+
"--strip-components",
|
|
250
|
+
"1",
|
|
251
|
+
"-C",
|
|
252
|
+
this.installDirectory,
|
|
253
|
+
]);
|
|
254
|
+
if (result.status == 0) {
|
|
255
|
+
resolve();
|
|
256
|
+
} else if (result.error) {
|
|
257
|
+
reject(result.error);
|
|
258
|
+
} else {
|
|
259
|
+
reject(
|
|
260
|
+
new Error(
|
|
261
|
+
`An error occurred untarring the artifact: stdout: ${result.stdout}; stderr: ${result.stderr}`,
|
|
262
|
+
),
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
} else if (this.zipExt == ".zip") {
|
|
266
|
+
let result;
|
|
267
|
+
if (this.platform.artifactName.includes("windows")) {
|
|
268
|
+
// Windows does not have "unzip" by default on many installations, instead
|
|
269
|
+
// we use Expand-Archive from powershell
|
|
270
|
+
result = spawnSync("powershell.exe", [
|
|
271
|
+
"-NoProfile",
|
|
272
|
+
"-NonInteractive",
|
|
273
|
+
"-Command",
|
|
274
|
+
`& {
|
|
275
|
+
param([string]$LiteralPath, [string]$DestinationPath)
|
|
276
|
+
Expand-Archive -LiteralPath $LiteralPath -DestinationPath $DestinationPath -Force
|
|
277
|
+
}`,
|
|
278
|
+
tempFile,
|
|
279
|
+
this.installDirectory,
|
|
280
|
+
]);
|
|
281
|
+
} else {
|
|
282
|
+
result = spawnSync("unzip", [
|
|
283
|
+
"-q",
|
|
284
|
+
tempFile,
|
|
285
|
+
"-d",
|
|
286
|
+
this.installDirectory,
|
|
287
|
+
]);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (result.status == 0) {
|
|
291
|
+
resolve();
|
|
292
|
+
} else if (result.error) {
|
|
293
|
+
reject(result.error);
|
|
294
|
+
} else {
|
|
295
|
+
reject(
|
|
296
|
+
new Error(
|
|
297
|
+
`An error occurred unzipping the artifact: stdout: ${result.stdout}; stderr: ${result.stderr}`,
|
|
298
|
+
),
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
reject(
|
|
303
|
+
new Error(`Unrecognized file extension: ${this.zipExt}`),
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
})
|
|
310
|
+
.then(() => {
|
|
311
|
+
if (!suppressLogs) {
|
|
312
|
+
console.error(`${this.name} has been installed!`);
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
.catch((e) => {
|
|
316
|
+
error(`Error fetching release: ${e.message}`);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
run(binaryName) {
|
|
321
|
+
const promise = !this.exists() ? this.install(true) : Promise.resolve();
|
|
322
|
+
|
|
323
|
+
promise
|
|
324
|
+
.then(() => {
|
|
325
|
+
const [, , ...args] = process.argv;
|
|
326
|
+
|
|
327
|
+
const options = { cwd: process.cwd(), stdio: "inherit" };
|
|
328
|
+
|
|
329
|
+
const binRelPath = this.binaries[binaryName];
|
|
330
|
+
if (!binRelPath) {
|
|
331
|
+
error(`${binaryName} is not a known binary in ${this.name}`);
|
|
332
|
+
}
|
|
333
|
+
const binPath = join(this.installDirectory, binRelPath);
|
|
334
|
+
const result = spawnSync(binPath, args, options);
|
|
335
|
+
|
|
336
|
+
if (result.error) {
|
|
337
|
+
error(result.error);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
process.exit(result.status);
|
|
341
|
+
})
|
|
342
|
+
.catch((e) => {
|
|
343
|
+
error(e.message);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
module.exports.Package = Package;
|