@ninjaxtools/slopdex 0.18.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.
Files changed (62) hide show
  1. package/.gitignore +2 -0
  2. package/README.md +136 -48
  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 -62
  8. package/run-slopdex.js +4 -0
  9. package/.agents/skills/slopdex/SKILL.md +0 -241
  10. package/dist/chunk-6LHZCHEB.js +0 -1112
  11. package/dist/chunk-6LHZCHEB.js.map +0 -1
  12. package/dist/chunk-BXWO2KMC.js +0 -20
  13. package/dist/chunk-BXWO2KMC.js.map +0 -1
  14. package/dist/chunk-DKRB2XT5.js +0 -33
  15. package/dist/chunk-DKRB2XT5.js.map +0 -1
  16. package/dist/chunk-DTI7SXGL.js +0 -109
  17. package/dist/chunk-DTI7SXGL.js.map +0 -1
  18. package/dist/chunk-IQU3YVZ3.js +0 -85
  19. package/dist/chunk-IQU3YVZ3.js.map +0 -1
  20. package/dist/chunk-JAVPHCZH.js +0 -24
  21. package/dist/chunk-JAVPHCZH.js.map +0 -1
  22. package/dist/chunk-KQRS5P4U.js +0 -10
  23. package/dist/chunk-KQRS5P4U.js.map +0 -1
  24. package/dist/chunk-KUV6PKKL.js +0 -425
  25. package/dist/chunk-KUV6PKKL.js.map +0 -1
  26. package/dist/chunk-MKUTTXZB.js +0 -74
  27. package/dist/chunk-MKUTTXZB.js.map +0 -1
  28. package/dist/chunk-N6D66ASM.js +0 -2217
  29. package/dist/chunk-N6D66ASM.js.map +0 -1
  30. package/dist/chunk-OIKBO3NJ.js +0 -133
  31. package/dist/chunk-OIKBO3NJ.js.map +0 -1
  32. package/dist/chunk-S225GYCL.js +0 -79
  33. package/dist/chunk-S225GYCL.js.map +0 -1
  34. package/dist/chunk-TSURHRFF.js +0 -240
  35. package/dist/chunk-TSURHRFF.js.map +0 -1
  36. package/dist/chunk-VH5VGRCI.js +0 -103
  37. package/dist/chunk-VH5VGRCI.js.map +0 -1
  38. package/dist/cli.d.ts +0 -1
  39. package/dist/cli.js +0 -1130
  40. package/dist/cli.js.map +0 -1
  41. package/dist/code-index-YEFTNG7E.js +0 -14
  42. package/dist/code-index-YEFTNG7E.js.map +0 -1
  43. package/dist/cross-search-FVKDMGP7.js +0 -10
  44. package/dist/cross-search-FVKDMGP7.js.map +0 -1
  45. package/dist/database-KAPV2YMP.js +0 -14
  46. package/dist/database-KAPV2YMP.js.map +0 -1
  47. package/dist/hosted-GHIUHKOU.js +0 -13
  48. package/dist/hosted-GHIUHKOU.js.map +0 -1
  49. package/dist/index.d.ts +0 -681
  50. package/dist/index.js +0 -102
  51. package/dist/index.js.map +0 -1
  52. package/dist/jina-43RL7C6M.js +0 -12
  53. package/dist/jina-43RL7C6M.js.map +0 -1
  54. package/dist/openai-CYUPNBGB.js +0 -12
  55. package/dist/openai-CYUPNBGB.js.map +0 -1
  56. package/dist/openai-EC6THXKV.js +0 -21
  57. package/dist/openai-EC6THXKV.js.map +0 -1
  58. package/dist/openai-TCRXM66O.js +0 -11
  59. package/dist/openai-TCRXM66O.js.map +0 -1
  60. package/docs/implementation.md +0 -162
  61. package/docs/reference.md +0 -187
  62. package/scripts/install-opencode-skill.mjs +0 -11
package/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ /node_modules
2
+
package/README.md CHANGED
@@ -4,40 +4,61 @@ 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
17
22
 
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.
23
+ `search` does a vector searche of code, and markdown, and when enabled generated code descriptions.
19
24
 
20
- ## Getting started
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
- An embedding-provider API key is required. You can use either OpenAI or Jina.
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
- ```bash
31
+ Install from npm:
32
+
33
+ ```sh
25
34
  npm install -g @ninjaxtools/slopdex
35
+ ```
26
36
 
27
- export OPENAI_API_KEY="your-api-key"
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
- slopdex search "validate an authenticated session"
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 code
57
+ ### Search
37
58
 
38
- By default only vector embeddings of code is used for search.
59
+ By default all available indexes are searched and ranked together.
39
60
 
40
- ```bash
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
- ```bash
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-description "keep the repository index synchronized"
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
- You can list and configure one of OpenCode's models like this:
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
- ```bash
68
- slopdex models opencode-go
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
 
72
- ### Find duplicate-code
121
+ For the best matching files (configured with `--describe-full-file-threshold`, default `0.8`) the full file contents are used.
122
+
123
+ ### Find duplicate code
73
124
 
74
125
  Compare functions across files, exclude short wrappers, and group matches into clusters:
75
126
 
76
- ```bash
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
- Review adjacent bands with threshold ranges:
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
- ```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
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
- ```bash
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
- ```bash
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
- ```bash
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
- 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 by elminated through the use of an abstraction, by ordering matches from farthest to nearest.
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
- ```bash
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 `search` and `search-description`:
190
+ Optionally a second-stage reranker can be enabled for all query-search commands:
135
191
 
136
- ```bash
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
- ```bash
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
- ```bash
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;