savant-code 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -0
- package/http.js +439 -0
- package/index.js +34 -0
- package/launcher.js +1216 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# The most powerful coding agent
|
|
2
|
+
|
|
3
|
+
SavantCode is a CLI tool that writes code for you.
|
|
4
|
+
|
|
5
|
+
1. Run `savant-code` from your project directory
|
|
6
|
+
2. Tell it what to do
|
|
7
|
+
3. It will read and write to files and run commands to produce the code you want
|
|
8
|
+
|
|
9
|
+
Note: SavantCode will run commands in your terminal as it deems necessary to fulfill your request.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
To install SavantCode, run:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g savant-code
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
(Use `sudo` if you get a permission error.)
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
After installation, you can start SavantCode by running:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
savant-code [project-directory]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
If no project directory is specified, SavantCode will use the current directory.
|
|
30
|
+
|
|
31
|
+
Once running, simply chat with SavantCode to say what coding task you want done.
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- Understands your whole codebase
|
|
36
|
+
- Creates and edits multiple files based on your request
|
|
37
|
+
- Can run your tests or type checker or linter; can install packages
|
|
38
|
+
- It's powerful: ask SavantCode to keep working until it reaches a condition and it will.
|
|
39
|
+
|
|
40
|
+
Our users regularly use SavantCode to implement new features, write unit tests, refactor code,write scripts, or give advice.
|
|
41
|
+
|
|
42
|
+
## Knowledge Files
|
|
43
|
+
|
|
44
|
+
To unlock the full benefits of modern LLMs, we recommend storing knowledge alongside your code. Add a `knowledge.md` file anywhere in your project to provide helpful context, guidance, and tips for the LLM as it performs tasks for you.
|
|
45
|
+
|
|
46
|
+
SavantCode can fluently read and write files, so it will add knowledge as it goes. You don't need to write knowledge manually!
|
|
47
|
+
|
|
48
|
+
Some have said every change should be paired with a unit test. In 2024, every change should come with a knowledge update!
|
|
49
|
+
|
|
50
|
+
## Tips
|
|
51
|
+
|
|
52
|
+
1. Type '/help' or just '/' to see available commands.
|
|
53
|
+
2. Create a `knowledge.md` file and collect specific points of advice. The assistant will use this knowledge to improve its responses.
|
|
54
|
+
3. Type `undo` or `redo` to revert or reapply file changes from the conversation.
|
|
55
|
+
4. Press `Esc` or `Ctrl+C` while SavantCode is generating a response to stop it.
|
|
56
|
+
|
|
57
|
+
## Troubleshooting
|
|
58
|
+
|
|
59
|
+
### Permission Errors
|
|
60
|
+
|
|
61
|
+
If you are getting permission errors during installation, try using sudo:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
sudo npm install -g savant-code
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
If you still have errors, it's a good idea to [reinstall Node](https://nodejs.org/en/download).
|
|
68
|
+
|
|
69
|
+
### Corporate Proxy / Firewall
|
|
70
|
+
|
|
71
|
+
If you see `Failed to download savant-code: Request timeout` or `Failed to determine latest version`, you may be behind a corporate proxy or firewall.
|
|
72
|
+
|
|
73
|
+
SavantCode respects standard proxy environment variables. Set `HTTPS_PROXY` to route traffic through your proxy:
|
|
74
|
+
|
|
75
|
+
**Linux / macOS (bash/zsh):**
|
|
76
|
+
```bash
|
|
77
|
+
export HTTPS_PROXY=http://your-proxy-server:port
|
|
78
|
+
savant-code
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Windows (PowerShell):**
|
|
82
|
+
```powershell
|
|
83
|
+
$env:HTTPS_PROXY = "http://your-proxy-server:port"
|
|
84
|
+
savant-code
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Windows (CMD):**
|
|
88
|
+
```cmd
|
|
89
|
+
set HTTPS_PROXY=http://your-proxy-server:port
|
|
90
|
+
savant-code
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
To make it permanent, add the `export` or `set` line to your shell profile (e.g. `~/.bashrc`, `~/.zshrc`, or Windows System Environment Variables).
|
|
94
|
+
|
|
95
|
+
**Supported environment variables:**
|
|
96
|
+
|
|
97
|
+
| Variable | Purpose |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `HTTPS_PROXY` / `https_proxy` | Proxy for HTTPS requests (recommended) |
|
|
100
|
+
| `HTTP_PROXY` / `http_proxy` | Fallback proxy for HTTP requests |
|
|
101
|
+
| `NO_PROXY` / `no_proxy` | Comma-separated list of hostnames to bypass the proxy (port suffixes are ignored) |
|
|
102
|
+
|
|
103
|
+
Both `http://` and `https://` proxy URLs are supported. Proxy authentication is supported via URL credentials (e.g. `http://user:password@proxy:port`).
|
|
104
|
+
|
|
105
|
+
## Feedback
|
|
106
|
+
|
|
107
|
+
We value your input! Please email your feedback to `founders@savant-code.com`. Thank you for using SavantCode!
|
package/http.js
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
const http = require('http')
|
|
2
|
+
const https = require('https')
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const { pipeline } = require('stream/promises')
|
|
5
|
+
const tls = require('tls')
|
|
6
|
+
|
|
7
|
+
function createReleaseHttpClient({
|
|
8
|
+
env = process.env,
|
|
9
|
+
userAgent,
|
|
10
|
+
requestTimeout,
|
|
11
|
+
httpModule = http,
|
|
12
|
+
httpsModule = https,
|
|
13
|
+
fsModule = fs,
|
|
14
|
+
pipelineFn = pipeline,
|
|
15
|
+
tlsModule = tls,
|
|
16
|
+
}) {
|
|
17
|
+
function getProxyUrl(protocol = 'https:') {
|
|
18
|
+
if (protocol === 'http:') {
|
|
19
|
+
return (
|
|
20
|
+
env.HTTP_PROXY ||
|
|
21
|
+
env.http_proxy ||
|
|
22
|
+
env.HTTPS_PROXY ||
|
|
23
|
+
env.https_proxy ||
|
|
24
|
+
null
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
return (
|
|
28
|
+
env.HTTPS_PROXY ||
|
|
29
|
+
env.https_proxy ||
|
|
30
|
+
env.HTTP_PROXY ||
|
|
31
|
+
env.http_proxy ||
|
|
32
|
+
null
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function shouldBypassProxy(hostname) {
|
|
37
|
+
const noProxy = env.NO_PROXY || env.no_proxy || ''
|
|
38
|
+
if (!noProxy) return false
|
|
39
|
+
|
|
40
|
+
const domains = noProxy
|
|
41
|
+
.split(',')
|
|
42
|
+
.map((domain) => domain.trim().toLowerCase().replace(/:\d+$/, ''))
|
|
43
|
+
const host = hostname.toLowerCase()
|
|
44
|
+
|
|
45
|
+
return domains.some((domain) => {
|
|
46
|
+
if (domain === '*') return true
|
|
47
|
+
if (domain.startsWith('.')) {
|
|
48
|
+
return host.endsWith(domain) || host === domain.slice(1)
|
|
49
|
+
}
|
|
50
|
+
return host === domain || host.endsWith(`.${domain}`)
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function connectThroughProxy(proxyUrl, targetHost, targetPort) {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const proxy = new URL(proxyUrl)
|
|
57
|
+
const isHttpsProxy = proxy.protocol === 'https:'
|
|
58
|
+
const connectOptions = {
|
|
59
|
+
hostname: proxy.hostname,
|
|
60
|
+
port: proxy.port || (isHttpsProxy ? 443 : 80),
|
|
61
|
+
method: 'CONNECT',
|
|
62
|
+
path: `${targetHost}:${targetPort}`,
|
|
63
|
+
headers: {
|
|
64
|
+
Host: `${targetHost}:${targetPort}`,
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (proxy.username || proxy.password) {
|
|
69
|
+
const auth = Buffer.from(
|
|
70
|
+
`${decodeURIComponent(proxy.username || '')}:${decodeURIComponent(
|
|
71
|
+
proxy.password || '',
|
|
72
|
+
)}`,
|
|
73
|
+
).toString('base64')
|
|
74
|
+
connectOptions.headers['Proxy-Authorization'] = `Basic ${auth}`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const transport = isHttpsProxy ? httpsModule : httpModule
|
|
78
|
+
const req = transport.request(connectOptions)
|
|
79
|
+
|
|
80
|
+
req.on('connect', (res, socket) => {
|
|
81
|
+
if (res.statusCode === 200) {
|
|
82
|
+
resolve(socket)
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
socket.destroy()
|
|
87
|
+
reject(new Error(`Proxy CONNECT failed with status ${res.statusCode}`))
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
req.on('error', (error) => {
|
|
91
|
+
reject(new Error(`Proxy connection failed: ${error.message}`))
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
req.setTimeout(requestTimeout, () => {
|
|
95
|
+
req.destroy()
|
|
96
|
+
reject(new Error('Proxy connection timeout.'))
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
req.end()
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function buildRequest(url, options = {}) {
|
|
104
|
+
const parsedUrl = new URL(url)
|
|
105
|
+
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
|
106
|
+
throw new Error(`Unsupported URL protocol: ${parsedUrl.protocol}`)
|
|
107
|
+
}
|
|
108
|
+
const isHttps = parsedUrl.protocol === 'https:'
|
|
109
|
+
const reqOptions = {
|
|
110
|
+
hostname: parsedUrl.hostname,
|
|
111
|
+
port: parsedUrl.port || (isHttps ? 443 : 80),
|
|
112
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
113
|
+
headers: {
|
|
114
|
+
'User-Agent': userAgent,
|
|
115
|
+
...options.headers,
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const proxyUrl = getProxyUrl(parsedUrl.protocol)
|
|
120
|
+
if (!proxyUrl || shouldBypassProxy(parsedUrl.hostname)) {
|
|
121
|
+
return { transport: isHttps ? httpsModule : httpModule, reqOptions }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const proxy = new URL(proxyUrl)
|
|
125
|
+
if (!['http:', 'https:'].includes(proxy.protocol)) {
|
|
126
|
+
throw new Error(`Unsupported proxy protocol: ${proxy.protocol}`)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!isHttps) {
|
|
130
|
+
reqOptions.hostname = proxy.hostname
|
|
131
|
+
reqOptions.port = proxy.port || (proxy.protocol === 'https:' ? 443 : 80)
|
|
132
|
+
reqOptions.path = parsedUrl.href
|
|
133
|
+
reqOptions.headers.Host = parsedUrl.host
|
|
134
|
+
if (proxy.username || proxy.password) {
|
|
135
|
+
const auth = Buffer.from(
|
|
136
|
+
`${decodeURIComponent(proxy.username || '')}:${decodeURIComponent(proxy.password || '')}`,
|
|
137
|
+
).toString('base64')
|
|
138
|
+
reqOptions.headers['Proxy-Authorization'] = `Basic ${auth}`
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
transport: proxy.protocol === 'https:' ? httpsModule : httpModule,
|
|
142
|
+
reqOptions,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const tunnelSocket = await connectThroughProxy(
|
|
147
|
+
proxyUrl,
|
|
148
|
+
parsedUrl.hostname,
|
|
149
|
+
parsedUrl.port || 443,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
class TunnelAgent extends httpsModule.Agent {
|
|
153
|
+
createConnection(_options, callback) {
|
|
154
|
+
const secureSocket = tlsModule.connect({
|
|
155
|
+
socket: tunnelSocket,
|
|
156
|
+
servername: parsedUrl.hostname,
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
if (typeof callback === 'function') {
|
|
160
|
+
if (typeof secureSocket.once === 'function') {
|
|
161
|
+
let settled = false
|
|
162
|
+
const finish = (error) => {
|
|
163
|
+
if (settled) return
|
|
164
|
+
settled = true
|
|
165
|
+
callback(error || null, error ? undefined : secureSocket)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
secureSocket.once('secureConnect', () => finish(null))
|
|
169
|
+
secureSocket.once('error', (error) => finish(error))
|
|
170
|
+
} else {
|
|
171
|
+
callback(null, secureSocket)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return secureSocket
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
reqOptions.agent = new TunnelAgent({ keepAlive: false })
|
|
180
|
+
return { transport: httpsModule, reqOptions }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function httpGet(url, options = {}) {
|
|
184
|
+
const redirectCount = options.redirectCount || 0
|
|
185
|
+
const { transport, reqOptions } = await buildRequest(url, options)
|
|
186
|
+
|
|
187
|
+
return new Promise((resolve, reject) => {
|
|
188
|
+
const req = transport.get(reqOptions, (res) => {
|
|
189
|
+
if ([301, 302, 303, 307, 308].includes(res.statusCode)) {
|
|
190
|
+
res.resume()
|
|
191
|
+
|
|
192
|
+
if (!res.headers.location) {
|
|
193
|
+
reject(
|
|
194
|
+
new Error(`Redirect ${res.statusCode} missing Location header.`),
|
|
195
|
+
)
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
if (redirectCount >= (options.maxRedirects ?? 10)) {
|
|
199
|
+
reject(new Error('Too many redirects.'))
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
httpGet(new URL(res.headers.location, url).href, {
|
|
204
|
+
...options,
|
|
205
|
+
redirectCount: redirectCount + 1,
|
|
206
|
+
})
|
|
207
|
+
.then(resolve)
|
|
208
|
+
.catch(reject)
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
res.requestUrl = url
|
|
213
|
+
resolve(res)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
req.on('error', (error) => {
|
|
217
|
+
error.requestUrl = url
|
|
218
|
+
reject(error)
|
|
219
|
+
})
|
|
220
|
+
req.setTimeout(options.timeout || requestTimeout, () => {
|
|
221
|
+
req.destroy()
|
|
222
|
+
const error = new Error('Request timeout.')
|
|
223
|
+
error.code = 'ETIMEDOUT'
|
|
224
|
+
error.requestUrl = url
|
|
225
|
+
reject(error)
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function getFileSize(filePath) {
|
|
231
|
+
try {
|
|
232
|
+
return fsModule.statSync(filePath).size
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (error.code === 'ENOENT') return 0
|
|
235
|
+
throw error
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function removeFileIfPresent(filePath) {
|
|
240
|
+
try {
|
|
241
|
+
fsModule.unlinkSync(filePath)
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (error.code !== 'ENOENT') throw error
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function throwResponseError(res, message, requestUrl, retryable) {
|
|
248
|
+
res.resume()
|
|
249
|
+
const error = new Error(message)
|
|
250
|
+
error.statusCode = res.statusCode
|
|
251
|
+
error.retryable = retryable
|
|
252
|
+
error.requestUrl = requestUrl
|
|
253
|
+
throw error
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function parseContentRange(value) {
|
|
257
|
+
if (!value) return null
|
|
258
|
+
|
|
259
|
+
const completeMatch = value.match(/^bytes (\d+)-(\d+)\/(\d+)$/i)
|
|
260
|
+
if (completeMatch) {
|
|
261
|
+
const start = Number(completeMatch[1])
|
|
262
|
+
const end = Number(completeMatch[2])
|
|
263
|
+
const total = Number(completeMatch[3])
|
|
264
|
+
if (start > end || end >= total) return null
|
|
265
|
+
return {
|
|
266
|
+
start,
|
|
267
|
+
end,
|
|
268
|
+
total,
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const unsatisfiedMatch = value.match(/^bytes \*\/(\d+)$/i)
|
|
273
|
+
if (unsatisfiedMatch) {
|
|
274
|
+
return { start: null, end: null, total: Number(unsatisfiedMatch[1]) }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return null
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function isRetryableStatus(statusCode) {
|
|
281
|
+
return (
|
|
282
|
+
statusCode === 408 ||
|
|
283
|
+
statusCode === 425 ||
|
|
284
|
+
statusCode === 429 ||
|
|
285
|
+
statusCode >= 500
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function downloadFile(url, destinationPath, options = {}) {
|
|
290
|
+
let resumedFrom = getFileSize(destinationPath)
|
|
291
|
+
let totalBytes = null
|
|
292
|
+
|
|
293
|
+
const headers = { ...options.headers }
|
|
294
|
+
if (resumedFrom > 0) {
|
|
295
|
+
headers.Range = `bytes=${resumedFrom}-`
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const res = await httpGet(url, { ...options, headers })
|
|
299
|
+
const responseUrl = res.requestUrl || url
|
|
300
|
+
const contentRange = parseContentRange(res.headers['content-range'])
|
|
301
|
+
|
|
302
|
+
if (res.statusCode === 416 && contentRange?.total === resumedFrom) {
|
|
303
|
+
res.resume()
|
|
304
|
+
return {
|
|
305
|
+
downloadedBytes: resumedFrom,
|
|
306
|
+
totalBytes: resumedFrom,
|
|
307
|
+
resumedFrom,
|
|
308
|
+
responseUrl,
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (res.statusCode === 416) {
|
|
313
|
+
removeFileIfPresent(destinationPath)
|
|
314
|
+
throwResponseError(
|
|
315
|
+
res,
|
|
316
|
+
'Saved partial download is no longer valid',
|
|
317
|
+
responseUrl,
|
|
318
|
+
true,
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let writeFlags
|
|
323
|
+
let expectedResponseBytes = null
|
|
324
|
+
if (res.statusCode === 206) {
|
|
325
|
+
if (!contentRange || contentRange.start !== resumedFrom) {
|
|
326
|
+
removeFileIfPresent(destinationPath)
|
|
327
|
+
throwResponseError(
|
|
328
|
+
res,
|
|
329
|
+
`Download resume mismatch: expected byte ${resumedFrom}`,
|
|
330
|
+
responseUrl,
|
|
331
|
+
true,
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
totalBytes = contentRange.total
|
|
335
|
+
expectedResponseBytes = contentRange.end - contentRange.start + 1
|
|
336
|
+
writeFlags = 'a'
|
|
337
|
+
} else if (res.statusCode === 200) {
|
|
338
|
+
// The server may ignore Range. Restart safely instead of appending a
|
|
339
|
+
// complete response to the existing partial archive.
|
|
340
|
+
resumedFrom = 0
|
|
341
|
+
totalBytes = Number(res.headers['content-length']) || null
|
|
342
|
+
writeFlags = 'w'
|
|
343
|
+
} else {
|
|
344
|
+
throwResponseError(
|
|
345
|
+
res,
|
|
346
|
+
`Download failed: HTTP ${res.statusCode}`,
|
|
347
|
+
responseUrl,
|
|
348
|
+
isRetryableStatus(res.statusCode),
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
let downloadedBytes = resumedFrom
|
|
353
|
+
res.on('data', (chunk) => {
|
|
354
|
+
downloadedBytes += chunk.length
|
|
355
|
+
options.onProgress?.({ downloadedBytes, totalBytes, resumedFrom })
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
await pipelineFn(
|
|
360
|
+
res,
|
|
361
|
+
fsModule.createWriteStream(destinationPath, { flags: writeFlags }),
|
|
362
|
+
)
|
|
363
|
+
} catch (error) {
|
|
364
|
+
error.requestUrl ||= responseUrl
|
|
365
|
+
error.downloadedBytes = getFileSize(destinationPath)
|
|
366
|
+
error.totalBytes = totalBytes
|
|
367
|
+
throw error
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
downloadedBytes = getFileSize(destinationPath)
|
|
371
|
+
const responseBytes = downloadedBytes - resumedFrom
|
|
372
|
+
if (
|
|
373
|
+
expectedResponseBytes !== null &&
|
|
374
|
+
responseBytes !== expectedResponseBytes
|
|
375
|
+
) {
|
|
376
|
+
if (responseBytes > expectedResponseBytes) {
|
|
377
|
+
removeFileIfPresent(destinationPath)
|
|
378
|
+
}
|
|
379
|
+
const error = new Error(
|
|
380
|
+
`Download incomplete: response contained ${responseBytes} of ${expectedResponseBytes} bytes`,
|
|
381
|
+
)
|
|
382
|
+
error.code = 'EINCOMPLETE'
|
|
383
|
+
error.retryable = true
|
|
384
|
+
error.requestUrl = responseUrl
|
|
385
|
+
error.downloadedBytes = getFileSize(destinationPath)
|
|
386
|
+
error.totalBytes = totalBytes
|
|
387
|
+
throw error
|
|
388
|
+
}
|
|
389
|
+
if (totalBytes !== null && downloadedBytes !== totalBytes) {
|
|
390
|
+
const error = new Error(
|
|
391
|
+
`Download incomplete: received ${downloadedBytes} of ${totalBytes} bytes`,
|
|
392
|
+
)
|
|
393
|
+
error.code = 'EINCOMPLETE'
|
|
394
|
+
error.retryable = true
|
|
395
|
+
error.requestUrl = responseUrl
|
|
396
|
+
error.downloadedBytes = downloadedBytes
|
|
397
|
+
error.totalBytes = totalBytes
|
|
398
|
+
throw error
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return { downloadedBytes, totalBytes, resumedFrom, responseUrl }
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function withRetries(
|
|
405
|
+
operation,
|
|
406
|
+
{
|
|
407
|
+
maxAttempts = 1,
|
|
408
|
+
baseDelayMs = 1000,
|
|
409
|
+
shouldRetry = () => true,
|
|
410
|
+
onRetry = () => {},
|
|
411
|
+
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
412
|
+
},
|
|
413
|
+
) {
|
|
414
|
+
for (let attempt = 1; ; attempt++) {
|
|
415
|
+
try {
|
|
416
|
+
return await operation(attempt)
|
|
417
|
+
} catch (error) {
|
|
418
|
+
if (attempt >= maxAttempts || !shouldRetry(error)) {
|
|
419
|
+
throw error
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const delayMs = baseDelayMs * 2 ** (attempt - 1)
|
|
423
|
+
await onRetry({ error, attempt, nextAttempt: attempt + 1, delayMs })
|
|
424
|
+
await sleep(delayMs)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
getProxyUrl,
|
|
431
|
+
downloadFile,
|
|
432
|
+
httpGet,
|
|
433
|
+
withRetries,
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
module.exports = {
|
|
438
|
+
createReleaseHttpClient,
|
|
439
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
|
|
6
|
+
const packagedLauncherPath = path.join(__dirname, 'launcher.js')
|
|
7
|
+
const sourceLauncherPath = path.join(
|
|
8
|
+
__dirname,
|
|
9
|
+
'..',
|
|
10
|
+
'release-core',
|
|
11
|
+
'launcher.js',
|
|
12
|
+
)
|
|
13
|
+
// Published packages must not let an unrelated sibling path shadow their
|
|
14
|
+
// bundled launcher. Source checkouts only fall back when that copy is absent.
|
|
15
|
+
const { createLauncher } = require(
|
|
16
|
+
fs.existsSync(packagedLauncherPath)
|
|
17
|
+
? packagedLauncherPath
|
|
18
|
+
: sourceLauncherPath,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const launcher = createLauncher({
|
|
22
|
+
packageName: 'savant-code',
|
|
23
|
+
displayName: 'SavantCode',
|
|
24
|
+
tempDownloadDirName: '.download-temp',
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
module.exports = launcher
|
|
28
|
+
|
|
29
|
+
if (require.main === module) {
|
|
30
|
+
launcher.main().catch((error) => {
|
|
31
|
+
console.error('❌ Unexpected error:', error.message)
|
|
32
|
+
process.exit(1)
|
|
33
|
+
})
|
|
34
|
+
}
|