@ninjaxtools/slopdex 0.19.0 → 0.21.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.
Files changed (60) hide show
  1. package/.gitignore +2 -0
  2. package/README.md +154 -46
  3. package/binary-install.js +348 -0
  4. package/binary.js +124 -0
  5. package/install.js +4 -0
  6. package/npm-shrinkwrap.json +52 -0
  7. package/package.json +78 -61
  8. package/run-slopdex.js +4 -0
  9. package/dist/chunk-5BO2LLXC.js +0 -15
  10. package/dist/chunk-5BO2LLXC.js.map +0 -1
  11. package/dist/chunk-BXWO2KMC.js +0 -20
  12. package/dist/chunk-BXWO2KMC.js.map +0 -1
  13. package/dist/chunk-DKRB2XT5.js +0 -33
  14. package/dist/chunk-DKRB2XT5.js.map +0 -1
  15. package/dist/chunk-DTI7SXGL.js +0 -109
  16. package/dist/chunk-DTI7SXGL.js.map +0 -1
  17. package/dist/chunk-GBEJHETS.js +0 -1117
  18. package/dist/chunk-GBEJHETS.js.map +0 -1
  19. package/dist/chunk-HMKVMRLF.js +0 -2221
  20. package/dist/chunk-HMKVMRLF.js.map +0 -1
  21. package/dist/chunk-IQU3YVZ3.js +0 -85
  22. package/dist/chunk-IQU3YVZ3.js.map +0 -1
  23. package/dist/chunk-JAVPHCZH.js +0 -24
  24. package/dist/chunk-JAVPHCZH.js.map +0 -1
  25. package/dist/chunk-MKUTTXZB.js +0 -74
  26. package/dist/chunk-MKUTTXZB.js.map +0 -1
  27. package/dist/chunk-OIKBO3NJ.js +0 -133
  28. package/dist/chunk-OIKBO3NJ.js.map +0 -1
  29. package/dist/chunk-P57ZJREE.js +0 -426
  30. package/dist/chunk-P57ZJREE.js.map +0 -1
  31. package/dist/chunk-S225GYCL.js +0 -79
  32. package/dist/chunk-S225GYCL.js.map +0 -1
  33. package/dist/chunk-TSURHRFF.js +0 -240
  34. package/dist/chunk-TSURHRFF.js.map +0 -1
  35. package/dist/chunk-VH5VGRCI.js +0 -103
  36. package/dist/chunk-VH5VGRCI.js.map +0 -1
  37. package/dist/cli.d.ts +0 -1
  38. package/dist/cli.js +0 -1130
  39. package/dist/cli.js.map +0 -1
  40. package/dist/code-index-MWSJKV3G.js +0 -14
  41. package/dist/code-index-MWSJKV3G.js.map +0 -1
  42. package/dist/cross-search-ERBFC25T.js +0 -10
  43. package/dist/cross-search-ERBFC25T.js.map +0 -1
  44. package/dist/database-P5XWJWQL.js +0 -14
  45. package/dist/database-P5XWJWQL.js.map +0 -1
  46. package/dist/hosted-GHIUHKOU.js +0 -13
  47. package/dist/hosted-GHIUHKOU.js.map +0 -1
  48. package/dist/index.d.ts +0 -681
  49. package/dist/index.js +0 -102
  50. package/dist/index.js.map +0 -1
  51. package/dist/jina-43RL7C6M.js +0 -12
  52. package/dist/jina-43RL7C6M.js.map +0 -1
  53. package/dist/openai-CYUPNBGB.js +0 -12
  54. package/dist/openai-CYUPNBGB.js.map +0 -1
  55. package/dist/openai-EC6THXKV.js +0 -21
  56. package/dist/openai-EC6THXKV.js.map +0 -1
  57. package/dist/openai-TCRXM66O.js +0 -11
  58. package/dist/openai-TCRXM66O.js.map +0 -1
  59. package/docs/implementation.md +0 -162
  60. package/docs/reference.md +0 -187
package/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ /node_modules
2
+
package/README.md CHANGED
@@ -4,40 +4,79 @@ This readme is written by a human.
4
4
 
5
5
  # slopdex
6
6
 
7
- <img src="sloppy-dexter.png" width="280" align="right" alt="Dexter, the sloppy slime" />
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 code.
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/>which finds code similar to the query
17
+ <br/>find code/docs similar to the query
15
18
  - `slopdex cross-search`
16
- <br/>which finds clusters of similar code
19
+ <br/>find clusters of similar code/docs
20
+ - `slopdex describe <query>`
21
+ <br/>explain code relevant to a task
22
+ - `slopdex map [PATH]...`
23
+ <br/>show a map/skeleton of the code structure
17
24
 
18
- `cross-search` helps with finding duplicated code, and with `--cohesion` helps with identifying similar code that is not necessarily duplicated but spread out across the codebase, which could indicate that a refactoring could make it more cohesive.
25
+ `search` does a vector searche of code, and markdown, and when enabled generated code descriptions.
19
26
 
20
- ## Getting started
27
+ `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`).
28
+
29
+ `describe` does a `search` first, then passes the result through the configured LLM model to create a tailored description.
30
+
31
+ `map` shows a map/skeleton of the code, excluding implementation details like function bodies. This can be helpful to get a concise map of the code to allow an LLM to explore the codebase incrementally.
21
32
 
22
- An embedding-provider API key is required. You can use either OpenAI or Jina.
33
+ ## Installation
23
34
 
24
- ```bash
35
+ Install from npm:
36
+
37
+ ```sh
25
38
  npm install -g @ninjaxtools/slopdex
39
+ ```
26
40
 
27
- export OPENAI_API_KEY="your-api-key"
28
- # Or
29
- # export JINA_API_KEY="your-api-key" # and pass --provider jina
41
+ You can also download a binary from one of the releases: https://github.com/ninjaxtools/slopdex/releases
30
42
 
31
- slopdex search "validate an authenticated session"
43
+ Or you can build from source:
44
+
45
+ ```sh
46
+ cargo install --path . --locked
47
+ ```
48
+
49
+ ## Getting started
50
+
51
+ Semantic search requires an embedding-provider API key from OpenAI or Jina. `slopdex map` doesn't
52
+ use vector search and works locally without a provider or API key.
53
+
54
+ ```console
55
+ $ export OPENAI_API_KEY="your-api-key"
56
+ $ slopdex search "validate an authenticated session"
57
+ ...
32
58
  ```
33
59
 
34
60
  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
61
 
36
- ### Search code
62
+ ### Code map/skeleton
63
+
64
+ ```console
65
+ $ slopdex map src docs
66
+ $ slopdex map src -g '*.rs' -k fns -e 'refresh|search'
67
+ $ slopdex map -g '*.md' -e '^Guide\.Setup' -i --format json
68
+ $ slopdex map --no-reindex
69
+ ```
70
+
71
+ The `map` command can be used to generate a source code skeleton that strips most of the code implementation but retains many of the structurally useful information that can act as an index into the source code. The resulting index output is most useful to conserve tokens and improve the agentic programming experience.
72
+
73
+ See the [selector reference](docs/reference.md#shared-selectors).
74
+
75
+ ### Search
37
76
 
38
- By default only vector embeddings of code is used for search.
77
+ By default all available indexes are searched and ranked together.
39
78
 
40
- ```bash
79
+ ```console
41
80
  $ slopdex search "keep the repository index synchronized"
42
81
  0.4284 tests/languages.test.ts :: refresh
43
82
  0.4200 src/cli.ts :: refreshIndex
@@ -45,35 +84,65 @@ $ slopdex search "keep the repository index synchronized"
45
84
  ...
46
85
  ```
47
86
 
87
+ To search code/docs individually:
88
+
89
+ ```console
90
+ $ slopdex search-code "configure the embedding provider"
91
+ $ slopdex search-md "configure the embedding provider"
92
+ ```
93
+
48
94
  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
95
 
50
96
  I use OpenCode Go usually with DeepSeek or Muse Spark, which are fairly good low-cost models. If you
51
97
  sign up for OpenCode Go through [this link](https://opencode.ai/go?ref=RAR3Z744DZ), we both receive
52
98
  $5 in credit.
53
99
 
54
- ```bash
55
- export OPENAI_API_KEY="your-api-key"
100
+ ```console
101
+ $ export OPENAI_API_KEY="your-api-key"
56
102
  # Or
57
103
  # export OPENCODE_API_KEY="your-api-key" # for OpenCode Zen/Go descriptions
58
104
  # Or
59
105
  # opencode auth login # use stored credentials instead of env variables
60
106
 
61
- slopdex descriptions enable
62
- slopdex search-description "keep the repository index synchronized"
107
+ $ slopdex descriptions enable
108
+ $ slopdex search-descriptions "keep the repository index synchronized"
109
+ ```
110
+
111
+ You can list and configure one of OpenCode's models like this (or use the interactive config):
112
+
113
+ ```console
114
+ $ slopdex models opencode-go
115
+ $ slopdex config model opencode-go/gpt-5.6-luna
116
+ $ slopdex config fallback-model opencode-go/muse-spark-1.3-contributor
117
+ ```
118
+
119
+ 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.
120
+
121
+ 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.
122
+
123
+ ### Interactive Config
124
+
125
+ To configure all configurable settings interactively run:
126
+
127
+ ```console
128
+ $ slopdex config
63
129
  ```
64
130
 
65
- You can list and configure one of OpenCode's models like this:
131
+ ### Search and describe
132
+
133
+ 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
134
 
67
- ```bash
68
- slopdex models opencode-go
69
- slopdex config model opencode-go/gpt-5.6-luna
135
+ ```console
136
+ $ slopdex describe "I want to implement a new rpc endpoint"
70
137
  ```
71
138
 
139
+ For the best matching files (configured with `--describe-full-file-threshold`, default `0.8`) the full file contents are used.
140
+
72
141
  ### Find duplicate code
73
142
 
74
143
  Compare functions across files, exclude short wrappers, and group matches into clusters:
75
144
 
76
- ```bash
145
+ ```console
77
146
  $ slopdex cross-search --cross-file-only --min-lines 4 --threshold 0.9
78
147
  Cluster 1 (3 functions, similarity 0.9124-0.9568)
79
148
  src/auth/session.ts:18:1 :: validateSession
@@ -84,66 +153,73 @@ Cluster 1 (3 functions, similarity 0.9124-0.9568)
84
153
 
85
154
  Using `--cross-file-only` is useful to exclude similar code in the same file.
86
155
 
87
- Review adjacent bands with threshold ranges:
156
+ Cross-search defaults to `--threshold 0.8` (query search defaults to `0.3`).
157
+ Clusters join matches transitively, so a low threshold can connect many groups
158
+ into one large cluster. Use `0.9` for stricter duplicate detection, or explicitly
159
+ pass `--threshold 0.3` for the pre-rewrite default.
88
160
 
89
- ```bash
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
161
+ You can use threshold ranges as well:
162
+
163
+ ```console
164
+ $ slopdex cross-search --cross-file-only --min-lines 4 --threshold 0.9
165
+ $ slopdex cross-search --cross-file-only --min-lines 4 --threshold 0.85-0.9
92
166
  ```
93
167
 
94
168
  ### Restrict functions used in the cross-search
95
169
 
96
170
  Only use uncommitted working-tree functions as sources:
97
171
 
98
- ```bash
99
- slopdex cross-search --uncommitted --cross-file-only --min-lines 4 --threshold 0.9
172
+ ```console
173
+ $ slopdex cross-search --uncommitted --cross-file-only --min-lines 4 --threshold 0.9
100
174
  ```
101
175
 
102
176
  Only use functions changed since origin/main as sources:
103
177
 
104
- ```bash
105
- slopdex cross-search --changed-since origin/main --threshold 0.9
178
+ ```console
179
+ $ slopdex cross-search --changed-since origin/main --threshold 0.9
106
180
  ```
107
181
 
108
182
  Only use matching symbols under src/services as sources:
109
183
 
110
- ```bash
111
- slopdex cross-search --source-path src/services -e '^UserService\.' --threshold 0.9
184
+ ```console
185
+ $ slopdex cross-search --source-path src/services -e '^UserService\.' --threshold 0.9
112
186
  ```
113
187
 
114
188
  ### Find related code stored far apart
115
189
 
116
190
  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
191
 
118
- ```bash
119
- slopdex cross-search --cross-file-only --cohesion --threshold 0.8
192
+ ```console
193
+ $ slopdex cross-search --cross-file-only --cohesion --threshold 0.8
120
194
  ```
121
195
  ### Use with agents
122
196
 
123
197
  Just put this in your `AGENTS.md` file, no skill required:
124
198
 
125
199
  ```
200
+ - use `treesitter-index -g <glob> <files or directories...>` early to obtain a compact structural skeleton before reading the full file, and then perform targeted reads for implementation details. Line numbers are indicated in square brackets (e.g [5] means line 5, and [5-10] means lines 5 to 10). To filter for specific symbols, use `-e`. To only include specific kinds of symbols use `-k` with `imports`, `fns`, `consts`, `types`, or `classes`. For example: `treesitter-index -g "*.ts" -i -e "manager|main" src`.
126
201
  - use semantic code search to find code with: `slopdex search "..." --threshold 0.5`
127
202
  - when reviewing uncommitted code avoid introducing duplicates by looking for related matches: `slopdex cross-search --uncommitted --threshold 0.8`
203
+ - don't use `*` as the glob since that circumvents the default ignore rules.
128
204
  ```
129
205
 
130
206
  Any other use, like doing a full `cross-search` is probably better done interactively with the agent, in which case you can just ask the agent to run `slopdex --help` to get usage information.
131
207
 
132
208
  ### Reranking
133
209
 
134
- Optionally a second-stage reranker can be enabled for `search` and `search-description`:
210
+ Optionally a second-stage reranker can be enabled for all query-search commands:
135
211
 
136
- ```bash
137
- export COHERE_API_KEY="your-api-key"
138
- slopdex config reranker cohere
212
+ ```console
213
+ $ export COHERE_API_KEY="your-api-key"
214
+ $ slopdex config reranker cohere
139
215
  # Or: export JINA_API_KEY="your-api-key" && slopdex config reranker jina
140
216
  # Or use an LLM: export OPENAI_API_KEY="your-api-key" && slopdex config reranker openai
141
217
  ```
142
218
 
143
219
  ### Compare repositories
144
220
 
145
- ```bash
146
- slopdex cross-search \
221
+ ```console
222
+ $ slopdex cross-search \
147
223
  --target-root /path/to/other/repo \
148
224
  --target-index /path/to/other/repo/.slopdex/index.sqlite \
149
225
  --threshold 0.9
@@ -153,10 +229,42 @@ slopdex cross-search \
153
229
 
154
230
  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
231
 
156
- ```bash
157
- slopdex status
158
- slopdex index-errors --format summary
159
- slopdex --version
232
+ ```console
233
+ $ slopdex status
234
+ $ slopdex index-errors --format summary
235
+ $ slopdex --version
236
+ ```
237
+
238
+ ## Related Work
239
+
240
+ Code similarity search
241
+
242
+ - [treepeat](https://github.com/dsummersl/treepeat)
243
+
244
+ Structured code queries:
245
+
246
+ - [CodeGraphContext](https://github.com/CodeGraphContext/CodeGraphContext)
247
+ - [CoderLM](https://github.com/JaredStewart/coderlm)
248
+
249
+ Structured code queries and edits:
250
+
251
+ - [srgn](https://github.com/alexpovel/srgn)
252
+ - [comby](https://github.com/comby-tools/comby)
253
+ - [ast-grep](https://ast-grep.github.io/guide/introduction)
254
+ - [gritql](https://github.com/biomejs/gritql)
255
+
256
+ Semantic code search with vector embeddings:
257
+
258
+ - [qmd](https://github.com/tobi/qmd)
259
+ - [grepai](https://github.com/yoanbernabeu/grepai)
260
+ - [cocoindex-code](https://github.com/cocoindex-io/cocoindex-code)
261
+ - [codana](https://github.com/bartolli/codanna)
262
+ - [open-codebase-index](https://github.com/Helweg/open-codebase-index)
263
+
264
+ ## Publishing
265
+
266
+ ```console
267
+ $ cargo release minor --no-publish --execute
160
268
  ```
161
269
 
162
270
  ## 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;