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.
Files changed (5) hide show
  1. package/README.md +107 -0
  2. package/http.js +439 -0
  3. package/index.js +34 -0
  4. package/launcher.js +1216 -0
  5. package/package.json +56 -0
package/launcher.js ADDED
@@ -0,0 +1,1216 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn, execFileSync } = require('child_process')
4
+ const fs = require('fs')
5
+ const http = require('http')
6
+ const https = require('https')
7
+ const os = require('os')
8
+ const path = require('path')
9
+ const { pipeline } = require('stream/promises')
10
+ const zlib = require('zlib')
11
+
12
+ const tar = require('tar')
13
+
14
+ const { createReleaseHttpClient } = require('./http')
15
+
16
+ function createLauncher(productConfig) {
17
+ const {
18
+ packageName,
19
+ displayName,
20
+ includeTreeSitterWasm = true,
21
+ startupBanner = [],
22
+ telemetryEvent = 'cli.update_savant_failed',
23
+ telemetryProperties = {},
24
+ tempDownloadDirName = `.${packageName}-download-temp`,
25
+ } = productConfig
26
+
27
+ /**
28
+ * Terminal escape sequences to reset terminal state after the child process exits.
29
+ * When the binary is SIGKILL'd, it can't clean up its own terminal state.
30
+ * The wrapper (this process) survives and must reset these modes.
31
+ */
32
+ const EXIT_ALTERNATE_SCREEN_SEQUENCE = '\x1b[?1049l'
33
+ const SAFE_TERMINAL_RESET_SEQUENCES =
34
+ '\x1b[?1000l' + // Disable X10 mouse mode
35
+ '\x1b[?1002l' + // Disable button event mouse mode
36
+ '\x1b[?1003l' + // Disable any-event mouse mode (all motion)
37
+ '\x1b[?1006l' + // Disable SGR extended mouse mode
38
+ '\x1b[?1004l' + // Disable focus reporting
39
+ '\x1b[?2004l' + // Disable bracketed paste mode
40
+ '\x1b[<u' + // Pop kitty keyboard protocol flags
41
+ '\x1b[>4;0m' + // Reset modifyOtherKeys
42
+ '\x1b[?25h' // Show cursor
43
+
44
+ const FULL_TERMINAL_RESET_SEQUENCES =
45
+ EXIT_ALTERNATE_SCREEN_SEQUENCE + SAFE_TERMINAL_RESET_SEQUENCES
46
+
47
+ function resetTerminal(options = {}) {
48
+ const { exitAlternateScreen = false } = options
49
+
50
+ try {
51
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
52
+ process.stdin.setRawMode(false)
53
+ }
54
+ } catch {
55
+ // stdin may be closed
56
+ }
57
+ try {
58
+ if (process.stdout.isTTY) {
59
+ // Exiting the alternate screen is only safe after an interactive child.
60
+ // Plain CLI paths like --help never enter it, and ?1049l can erase output.
61
+ process.stdout.write(
62
+ exitAlternateScreen
63
+ ? FULL_TERMINAL_RESET_SEQUENCES
64
+ : SAFE_TERMINAL_RESET_SEQUENCES,
65
+ )
66
+ }
67
+ } catch {
68
+ // stdout may be closed
69
+ }
70
+ }
71
+
72
+ function getUnsignedExitCode(code) {
73
+ return code != null && code < 0 ? code >>> 0 : code
74
+ }
75
+
76
+ function isWindowsNativeCrashCode(code) {
77
+ const unsignedCode = getUnsignedExitCode(code)
78
+ return (
79
+ process.platform === 'win32' &&
80
+ (unsignedCode === 0xc000001d ||
81
+ unsignedCode === 0xc0000005 ||
82
+ unsignedCode === 0xc0000409)
83
+ )
84
+ }
85
+
86
+ function shouldExitAlternateScreen(code, signal) {
87
+ return Boolean(signal) || isWindowsNativeCrashCode(code)
88
+ }
89
+
90
+ function isIllegalInstructionExit(code, signal) {
91
+ const unsignedCode = getUnsignedExitCode(code)
92
+ return (
93
+ signal === 'SIGILL' ||
94
+ (process.platform === 'win32' && unsignedCode === 0xc000001d)
95
+ )
96
+ }
97
+
98
+ function createConfig(packageName) {
99
+ const homeDir = os.homedir()
100
+ const configDir = path.join(homeDir, '.config', 'savant')
101
+ const binaryName =
102
+ process.platform === 'win32' ? `${packageName}.exe` : packageName
103
+
104
+ return {
105
+ homeDir,
106
+ configDir,
107
+ binaryName,
108
+ binaryPath: path.join(configDir, binaryName),
109
+ metadataPath: path.join(configDir, `${packageName}-metadata.json`),
110
+ tempDownloadDir: path.join(configDir, tempDownloadDirName),
111
+ userAgent: `${packageName}-cli`,
112
+ requestTimeout: 20000,
113
+ downloadRequestTimeout: 120000,
114
+ downloadMaxAttempts: 3,
115
+ }
116
+ }
117
+
118
+ const CONFIG = createConfig(packageName)
119
+ const { downloadFile, httpGet, withRetries } = createReleaseHttpClient({
120
+ env: process.env,
121
+ userAgent: CONFIG.userAgent,
122
+ requestTimeout: CONFIG.requestTimeout,
123
+ })
124
+
125
+ function getPostHogConfig() {
126
+ const apiKey =
127
+ process.env.SAVANT_CODE_POSTHOG_API_KEY ||
128
+ process.env.NEXT_PUBLIC_POSTHOG_API_KEY
129
+ const host =
130
+ process.env.SAVANT_CODE_POSTHOG_HOST ||
131
+ process.env.NEXT_PUBLIC_POSTHOG_HOST_URL
132
+
133
+ if (!apiKey || !host) {
134
+ return null
135
+ }
136
+
137
+ return { apiKey, host }
138
+ }
139
+
140
+ /**
141
+ * Track update failure event to PostHog.
142
+ * Fire-and-forget - errors are silently ignored.
143
+ */
144
+ function trackUpdateFailed(errorMessage, version, context = {}) {
145
+ try {
146
+ const posthogConfig = getPostHogConfig()
147
+ if (!posthogConfig) {
148
+ return
149
+ }
150
+
151
+ const payload = JSON.stringify({
152
+ api_key: posthogConfig.apiKey,
153
+ event: telemetryEvent,
154
+ properties: {
155
+ distinct_id: `anonymous-${CONFIG.homeDir}`,
156
+ error: errorMessage,
157
+ version: version || 'unknown',
158
+ platform: process.platform,
159
+ arch: process.arch,
160
+ ...telemetryProperties,
161
+ ...context,
162
+ },
163
+ timestamp: new Date().toISOString(),
164
+ })
165
+
166
+ const parsedUrl = new URL(`${posthogConfig.host}/capture/`)
167
+ const isHttps = parsedUrl.protocol === 'https:'
168
+ const options = {
169
+ hostname: parsedUrl.hostname,
170
+ port: parsedUrl.port || (isHttps ? 443 : 80),
171
+ path: parsedUrl.pathname + parsedUrl.search,
172
+ method: 'POST',
173
+ headers: {
174
+ 'Content-Type': 'application/json',
175
+ 'Content-Length': Buffer.byteLength(payload),
176
+ },
177
+ }
178
+
179
+ const transport = isHttps ? https : http
180
+ const req = transport.request(options)
181
+ req.on('error', () => {}) // Silently ignore errors
182
+ req.write(payload)
183
+ req.end()
184
+ } catch (e) {
185
+ // Silently ignore any tracking errors
186
+ }
187
+ }
188
+
189
+ const PLATFORM_TARGETS = {
190
+ 'linux-x64': `${packageName}-linux-x64.tar.gz`,
191
+ 'linux-x64-baseline': `${packageName}-linux-x64-baseline.tar.gz`,
192
+ 'linux-arm64': `${packageName}-linux-arm64.tar.gz`,
193
+ 'darwin-x64': `${packageName}-darwin-x64.tar.gz`,
194
+ 'darwin-arm64': `${packageName}-darwin-arm64.tar.gz`,
195
+ 'win32-x64': `${packageName}-win32-x64.tar.gz`,
196
+ 'win32-x64-baseline': `${packageName}-win32-x64-baseline.tar.gz`,
197
+ }
198
+
199
+ const BASELINE_FALLBACK_TARGETS = {
200
+ 'linux-x64': 'linux-x64-baseline',
201
+ 'win32-x64': 'win32-x64-baseline',
202
+ }
203
+
204
+ const term = {
205
+ clearLine: () => {
206
+ if (process.stderr.isTTY) {
207
+ process.stderr.write('\r\x1b[K')
208
+ }
209
+ },
210
+ write: (text) => {
211
+ term.clearLine()
212
+ process.stderr.write(text)
213
+ },
214
+ writeLine: (text) => {
215
+ term.clearLine()
216
+ process.stderr.write(text + '\n')
217
+ },
218
+ }
219
+
220
+ function getPlatformKey() {
221
+ return `${process.platform}-${process.arch}`
222
+ }
223
+
224
+ function getTargetOverride() {
225
+ const envNames = [
226
+ `${packageName.toUpperCase()}_BINARY_TARGET`,
227
+ 'SAVANT_CODE_BINARY_TARGET',
228
+ 'CLI_BINARY_TARGET',
229
+ ]
230
+
231
+ for (const envName of envNames) {
232
+ const target = process.env[envName]
233
+ if (target && PLATFORM_TARGETS[target]) {
234
+ return target
235
+ }
236
+ }
237
+
238
+ return null
239
+ }
240
+
241
+ function linuxCpuHasAvx2() {
242
+ try {
243
+ return /\bavx2\b/i.test(fs.readFileSync('/proc/cpuinfo', 'utf8'))
244
+ } catch {
245
+ return true
246
+ }
247
+ }
248
+
249
+ // Returns true (AVX2 present), false (absent), or null (couldn't determine).
250
+ // Ask the OS directly via IsProcessorFeaturePresent (kernel32), which is
251
+ // backed by CPUID — far more reliable than matching CPU model names, and it
252
+ // works on the stock Windows PowerShell that ships with every supported
253
+ // Windows version. Feature 40 = PF_AVX2_INSTRUCTIONS_AVAILABLE.
254
+ function probeWindowsAvx2() {
255
+ const script =
256
+ '$f = Add-Type -MemberDefinition \'[DllImport("kernel32.dll")] ' +
257
+ "public static extern bool IsProcessorFeaturePresent(uint feature);' " +
258
+ '-Name Cpu -Namespace Win32 -PassThru; $f::IsProcessorFeaturePresent(40)'
259
+ try {
260
+ const out = execFileSync(
261
+ 'powershell.exe',
262
+ ['-NoProfile', '-NonInteractive', '-Command', script],
263
+ {
264
+ encoding: 'utf8',
265
+ timeout: 5000,
266
+ stdio: ['ignore', 'pipe', 'ignore'],
267
+ },
268
+ ).trim()
269
+ if (out === 'True') return true
270
+ if (out === 'False') return false
271
+ return null
272
+ } catch {
273
+ // No PowerShell, locked-down policy, timeout, etc. — inconclusive.
274
+ return null
275
+ }
276
+ }
277
+
278
+ let _hasAvx2Cache
279
+
280
+ function machineHasAvx2() {
281
+ if (_hasAvx2Cache === undefined) {
282
+ _hasAvx2Cache = detectMachineHasAvx2()
283
+ }
284
+ return _hasAvx2Cache
285
+ }
286
+
287
+ function detectMachineHasAvx2() {
288
+ if (process.arch !== 'x64') {
289
+ return true
290
+ }
291
+
292
+ // Linux detection is a cheap file read, so we don't bother persisting it.
293
+ if (process.platform === 'linux') {
294
+ return linuxCpuHasAvx2()
295
+ }
296
+
297
+ if (process.platform !== 'win32') {
298
+ return true
299
+ }
300
+
301
+ // Windows detection shells out to PowerShell. getDefaultTargetKey runs on
302
+ // every launch (via the version check), so cache the result on disk to keep
303
+ // startup fast after the first probe.
304
+ const cached = readCachedAvx2()
305
+ if (cached !== null) {
306
+ return cached
307
+ }
308
+ const detected = probeWindowsAvx2()
309
+ if (detected === null) {
310
+ // Inconclusive probe: assume AVX2 for this launch and rely on the SIGILL
311
+ // fallback, but don't persist it — a transient failure must not lock in a
312
+ // wrong answer for the lifetime of the install. We'll re-probe next launch.
313
+ return true
314
+ }
315
+ writeCachedAvx2(detected)
316
+ return detected
317
+ }
318
+
319
+ function getCpuFeatureCachePath() {
320
+ return path.join(CONFIG.configDir, 'cpu-features.json')
321
+ }
322
+
323
+ function readCachedAvx2() {
324
+ try {
325
+ const cache = JSON.parse(
326
+ fs.readFileSync(getCpuFeatureCachePath(), 'utf8'),
327
+ )
328
+ return typeof cache.avx2 === 'boolean' ? cache.avx2 : null
329
+ } catch {
330
+ return null
331
+ }
332
+ }
333
+
334
+ function writeCachedAvx2(value) {
335
+ try {
336
+ fs.mkdirSync(CONFIG.configDir, { recursive: true })
337
+ fs.writeFileSync(
338
+ getCpuFeatureCachePath(),
339
+ JSON.stringify({ avx2: value }),
340
+ )
341
+ } catch {
342
+ // Best effort; we'll just re-probe next launch.
343
+ }
344
+ }
345
+
346
+ function getDefaultTargetKey() {
347
+ const override = getTargetOverride()
348
+ if (override) {
349
+ return override
350
+ }
351
+
352
+ const platformKey = getPlatformKey()
353
+ // Select the binary up front from explicit CPU feature detection rather than
354
+ // optimistically launching the AVX2 build and waiting for it to crash with
355
+ // an illegal instruction. The crash isn't always a clean immediate failure —
356
+ // it can surface later from a deeper code path — so older CPUs (e.g. an
357
+ // Intel Xeon with AVX but no AVX2) are safer on baseline from the start.
358
+ //
359
+ // This assumes every baseline target is gated on AVX2 specifically, which
360
+ // holds today (only linux-x64 and win32-x64 have baseline builds, both
361
+ // AVX2-gated). If a baseline build is ever added for a different reason, give
362
+ // BASELINE_FALLBACK_TARGETS a per-target capability and check that instead.
363
+ if (BASELINE_FALLBACK_TARGETS[platformKey] && !machineHasAvx2()) {
364
+ return BASELINE_FALLBACK_TARGETS[platformKey]
365
+ }
366
+
367
+ return platformKey
368
+ }
369
+
370
+ function getBaselineFallbackTargetKey() {
371
+ // Runtime safety net: if proactive detection was unavailable or wrong and the
372
+ // optimized binary still dies with SIGILL, fall back to baseline.
373
+ return BASELINE_FALLBACK_TARGETS[getPlatformKey()] || null
374
+ }
375
+
376
+ function isTargetAllowedForThisMachine(target) {
377
+ const override = getTargetOverride()
378
+ if (override) {
379
+ return target === override
380
+ }
381
+ // Check the baseline fallback first: it's always safe on its platform and
382
+ // avoids running CPU detection when a baseline binary is already installed.
383
+ return (
384
+ target === getBaselineFallbackTargetKey() ||
385
+ target === getDefaultTargetKey()
386
+ )
387
+ }
388
+
389
+ function getDownloadTargetKey() {
390
+ const override = getTargetOverride()
391
+ if (override) {
392
+ return override
393
+ }
394
+
395
+ const metadata = getCurrentMetadata()
396
+ if (metadata?.target && isTargetAllowedForThisMachine(metadata.target)) {
397
+ return metadata.target
398
+ }
399
+
400
+ return getDefaultTargetKey()
401
+ }
402
+
403
+ async function getLatestVersion() {
404
+ try {
405
+ const res = await httpGet(
406
+ `https://registry.npmjs.org/${packageName}/latest`,
407
+ )
408
+
409
+ if (res.statusCode !== 200) return null
410
+
411
+ const body = await streamToString(res)
412
+ const packageData = JSON.parse(body)
413
+
414
+ return packageData.version || null
415
+ } catch (error) {
416
+ return null
417
+ }
418
+ }
419
+
420
+ function streamToString(stream) {
421
+ return new Promise((resolve, reject) => {
422
+ let data = ''
423
+ stream.on('data', (chunk) => (data += chunk))
424
+ stream.on('end', () => resolve(data))
425
+ stream.on('error', reject)
426
+ })
427
+ }
428
+
429
+ function getCurrentVersion() {
430
+ try {
431
+ const metadata = getCurrentMetadata()
432
+ if (!metadata) {
433
+ return null
434
+ }
435
+ // Also verify the binary still exists
436
+ if (!fs.existsSync(CONFIG.binaryPath)) {
437
+ return null
438
+ }
439
+ const metadataTarget = metadata.target || getPlatformKey()
440
+ if (!isTargetAllowedForThisMachine(metadataTarget)) {
441
+ return null
442
+ }
443
+ return metadata.version || null
444
+ } catch (error) {
445
+ return null
446
+ }
447
+ }
448
+
449
+ function getCurrentMetadata() {
450
+ try {
451
+ if (!fs.existsSync(CONFIG.metadataPath)) {
452
+ return null
453
+ }
454
+ return JSON.parse(fs.readFileSync(CONFIG.metadataPath, 'utf8'))
455
+ } catch {
456
+ return null
457
+ }
458
+ }
459
+
460
+ function compareVersions(v1, v2) {
461
+ if (!v1 || !v2) return 0
462
+
463
+ // Always update if the current version is not a valid semver
464
+ // e.g. 1.0.420-beta.1
465
+ if (!v1.match(/^\d+(\.\d+)*$/)) {
466
+ return -1
467
+ }
468
+
469
+ const parseVersion = (version) => {
470
+ const parts = version.split('-')
471
+ const mainParts = parts[0].split('.').map(Number)
472
+ const prereleaseParts = parts[1] ? parts[1].split('.') : []
473
+ return { main: mainParts, prerelease: prereleaseParts }
474
+ }
475
+
476
+ const p1 = parseVersion(v1)
477
+ const p2 = parseVersion(v2)
478
+
479
+ for (let i = 0; i < Math.max(p1.main.length, p2.main.length); i++) {
480
+ const n1 = p1.main[i] || 0
481
+ const n2 = p2.main[i] || 0
482
+
483
+ if (n1 < n2) return -1
484
+ if (n1 > n2) return 1
485
+ }
486
+
487
+ if (p1.prerelease.length === 0 && p2.prerelease.length === 0) {
488
+ return 0
489
+ } else if (p1.prerelease.length === 0) {
490
+ return 1
491
+ } else if (p2.prerelease.length === 0) {
492
+ return -1
493
+ } else {
494
+ for (
495
+ let i = 0;
496
+ i < Math.max(p1.prerelease.length, p2.prerelease.length);
497
+ i++
498
+ ) {
499
+ const pr1 = p1.prerelease[i] || ''
500
+ const pr2 = p2.prerelease[i] || ''
501
+
502
+ const isNum1 = !isNaN(parseInt(pr1))
503
+ const isNum2 = !isNaN(parseInt(pr2))
504
+
505
+ if (isNum1 && isNum2) {
506
+ const num1 = parseInt(pr1)
507
+ const num2 = parseInt(pr2)
508
+ if (num1 < num2) return -1
509
+ if (num1 > num2) return 1
510
+ } else if (isNum1 && !isNum2) {
511
+ return 1
512
+ } else if (!isNum1 && isNum2) {
513
+ return -1
514
+ } else if (pr1 < pr2) {
515
+ return -1
516
+ } else if (pr1 > pr2) {
517
+ return 1
518
+ }
519
+ }
520
+ return 0
521
+ }
522
+ }
523
+
524
+ function formatBytes(bytes) {
525
+ if (bytes === 0) return '0 B'
526
+ const k = 1024
527
+ const sizes = ['B', 'KB', 'MB', 'GB']
528
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
529
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
530
+ }
531
+
532
+ function createProgressBar(percentage, width = 30) {
533
+ const filled = Math.round((width * percentage) / 100)
534
+ const empty = width - filled
535
+ return '[' + '█'.repeat(filled) + '░'.repeat(empty) + ']'
536
+ }
537
+
538
+ function isRetryableDownloadError(error) {
539
+ if (error && typeof error.retryable === 'boolean') return error.retryable
540
+ return !['EACCES', 'ENOSPC', 'EPERM', 'EROFS'].includes(error?.code)
541
+ }
542
+
543
+ function getPartialArchivePath(version, targetKey) {
544
+ return path.join(
545
+ CONFIG.configDir,
546
+ `.${packageName}-${version}-${targetKey}.tar.gz.part`,
547
+ )
548
+ }
549
+
550
+ function getFileSize(filePath) {
551
+ try {
552
+ return fs.statSync(filePath).size
553
+ } catch (error) {
554
+ if (error.code === 'ENOENT') return 0
555
+ throw error
556
+ }
557
+ }
558
+
559
+ function removeFileIfPresent(filePath) {
560
+ try {
561
+ fs.unlinkSync(filePath)
562
+ } catch (error) {
563
+ if (error.code !== 'ENOENT') throw error
564
+ }
565
+ }
566
+
567
+ function formatDownloadSource(downloadUrl) {
568
+ try {
569
+ const parsedUrl = new URL(downloadUrl)
570
+ return `${parsedUrl.origin}${parsedUrl.pathname}`
571
+ } catch {
572
+ return downloadUrl
573
+ }
574
+ }
575
+
576
+ function printDownloadFailure(error) {
577
+ const code = error.code ? ` (${error.code})` : ''
578
+ console.error(
579
+ `❌ Failed to download ${packageName}: ${error.message}${code}`,
580
+ )
581
+
582
+ if (error.requestUrl) {
583
+ console.error(
584
+ `Download source: ${formatDownloadSource(error.requestUrl)}`,
585
+ )
586
+ }
587
+ if (error.downloadedBytes > 0) {
588
+ const total = error.totalBytes
589
+ ? ` of ${formatBytes(error.totalBytes)}`
590
+ : ''
591
+ console.error(
592
+ `Saved ${formatBytes(error.downloadedBytes)}${total}; the next run will resume this download.`,
593
+ )
594
+ } else if (error.requestUrl) {
595
+ console.error(
596
+ 'Please retry. The release host may be temporarily unavailable.',
597
+ )
598
+ } else {
599
+ console.error(
600
+ 'The downloaded update could not be installed; the existing binary was preserved when possible.',
601
+ )
602
+ }
603
+ }
604
+
605
+ function prepareTempDownloadDir() {
606
+ if (fs.existsSync(CONFIG.tempDownloadDir)) {
607
+ fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
608
+ }
609
+ fs.mkdirSync(CONFIG.tempDownloadDir, { recursive: true })
610
+ }
611
+
612
+ async function downloadAndExtract(
613
+ downloadUrl,
614
+ version,
615
+ targetKey,
616
+ { quiet = false } = {},
617
+ ) {
618
+ let attempts = 0
619
+ const partialArchivePath = getPartialArchivePath(version, targetKey)
620
+ let totalBytes = null
621
+
622
+ try {
623
+ return await withRetries(
624
+ async (attempt) => {
625
+ attempts = attempt
626
+ prepareTempDownloadDir()
627
+ if (!quiet) term.write('Downloading...')
628
+
629
+ let lastProgressTime = Date.now()
630
+ const result = await downloadFile(downloadUrl, partialArchivePath, {
631
+ timeout: CONFIG.downloadRequestTimeout,
632
+ onProgress: ({ downloadedBytes, totalBytes: progressTotal }) => {
633
+ totalBytes = progressTotal
634
+ if (quiet) return
635
+ const now = Date.now()
636
+ if (
637
+ now - lastProgressTime < 100 &&
638
+ downloadedBytes !== progressTotal
639
+ ) {
640
+ return
641
+ }
642
+ lastProgressTime = now
643
+ if (progressTotal) {
644
+ const pct = Math.round((downloadedBytes / progressTotal) * 100)
645
+ term.write(
646
+ `Downloading... ${createProgressBar(pct)} ${pct}% of ${formatBytes(progressTotal)}`,
647
+ )
648
+ } else {
649
+ term.write(`Downloading... ${formatBytes(downloadedBytes)}`)
650
+ }
651
+ },
652
+ })
653
+ totalBytes = result.totalBytes
654
+
655
+ try {
656
+ await pipeline(
657
+ fs.createReadStream(partialArchivePath),
658
+ zlib.createGunzip(),
659
+ tar.x({ cwd: CONFIG.tempDownloadDir }),
660
+ )
661
+ } catch (error) {
662
+ // A complete archive that cannot be extracted is corrupt. Do not
663
+ // resume it on the next attempt.
664
+ removeFileIfPresent(partialArchivePath)
665
+ throw error
666
+ }
667
+
668
+ const tempBinaryPath = path.join(
669
+ CONFIG.tempDownloadDir,
670
+ CONFIG.binaryName,
671
+ )
672
+ if (!fs.existsSync(tempBinaryPath)) {
673
+ const files = fs.readdirSync(CONFIG.tempDownloadDir)
674
+ removeFileIfPresent(partialArchivePath)
675
+ const error = new Error(
676
+ `Binary not found after extraction. Expected: ${CONFIG.binaryName}, Available files: ${files.join(', ')}`,
677
+ )
678
+ error.retryable = false
679
+ throw error
680
+ }
681
+
682
+ removeFileIfPresent(partialArchivePath)
683
+ return tempBinaryPath
684
+ },
685
+ {
686
+ maxAttempts: CONFIG.downloadMaxAttempts,
687
+ shouldRetry: isRetryableDownloadError,
688
+ onRetry: ({ nextAttempt, delayMs }) => {
689
+ if (quiet) return
690
+ term.writeLine(
691
+ `Download interrupted. Retrying in ${delayMs / 1000}s (${nextAttempt}/${CONFIG.downloadMaxAttempts})...`,
692
+ )
693
+ },
694
+ },
695
+ )
696
+ } catch (error) {
697
+ try {
698
+ fs.rmSync(CONFIG.tempDownloadDir, { recursive: true, force: true })
699
+ } catch {
700
+ // Best effort after a failed download.
701
+ }
702
+
703
+ trackUpdateFailed(error.message, version, {
704
+ stage: 'download',
705
+ errorCode: error.code,
706
+ statusCode: error.statusCode,
707
+ target: targetKey,
708
+ attempts,
709
+ bytesDownloaded: getFileSize(partialArchivePath),
710
+ totalBytes: error.totalBytes || totalBytes,
711
+ })
712
+ error.downloadedBytes = getFileSize(partialArchivePath)
713
+ error.totalBytes ||= totalBytes
714
+ error.requestUrl ||= downloadUrl
715
+ throw error
716
+ }
717
+ }
718
+
719
+ async function stageBinary(
720
+ version,
721
+ targetKey = getDownloadTargetKey(),
722
+ options = {},
723
+ ) {
724
+ const fileName = PLATFORM_TARGETS[targetKey]
725
+
726
+ if (!fileName) {
727
+ const error = new Error(
728
+ `Unsupported platform: ${process.platform} ${process.arch}`,
729
+ )
730
+ trackUpdateFailed(error.message, version, {
731
+ stage: 'platform_check',
732
+ target: targetKey,
733
+ })
734
+ throw error
735
+ }
736
+
737
+ // Default to GitHub releases. The `SAVANT_CODE_RELEASES_URL` env var lets
738
+ // forks or enterprise deployments override the host (useful when a custom
739
+ // release server replaces the public GitHub assets).
740
+ const releaseBaseUrl =
741
+ process.env.SAVANT_CODE_RELEASES_URL ||
742
+ 'https://github.com/savant0x/savant-code/releases/download'
743
+ const downloadUrl = `${releaseBaseUrl}/v${version}/${fileName}`
744
+
745
+ fs.mkdirSync(CONFIG.configDir, { recursive: true })
746
+ const tempBinaryPath = await downloadAndExtract(
747
+ downloadUrl,
748
+ version,
749
+ targetKey,
750
+ options,
751
+ )
752
+
753
+ try {
754
+ if (process.platform !== 'win32') {
755
+ fs.chmodSync(tempBinaryPath, 0o755)
756
+ }
757
+ } catch (error) {
758
+ try {
759
+ fs.rmSync(CONFIG.tempDownloadDir, { recursive: true, force: true })
760
+ } catch {
761
+ // Preserve the original chmod error.
762
+ }
763
+ throw error
764
+ }
765
+
766
+ return { tempBinaryPath, version, targetKey }
767
+ }
768
+
769
+ function replaceFileWithRollback(sourcePath, targetPath, replacements) {
770
+ const backupPath = fs.existsSync(targetPath)
771
+ ? `${targetPath}.old.${Date.now()}.${replacements.length}`
772
+ : null
773
+
774
+ if (backupPath) {
775
+ fs.renameSync(targetPath, backupPath)
776
+ }
777
+
778
+ try {
779
+ fs.renameSync(sourcePath, targetPath)
780
+ } catch (error) {
781
+ if (backupPath && fs.existsSync(backupPath)) {
782
+ fs.renameSync(backupPath, targetPath)
783
+ }
784
+ throw error
785
+ }
786
+
787
+ replacements.push({ backupPath, targetPath })
788
+ }
789
+
790
+ function rollbackReplacements(replacements) {
791
+ for (const { backupPath, targetPath } of replacements.reverse()) {
792
+ removeFileIfPresent(targetPath)
793
+ if (backupPath && fs.existsSync(backupPath)) {
794
+ fs.renameSync(backupPath, targetPath)
795
+ }
796
+ }
797
+ }
798
+
799
+ function commitReplacements(replacements) {
800
+ for (const { backupPath } of replacements) {
801
+ if (!backupPath) continue
802
+ try {
803
+ removeFileIfPresent(backupPath)
804
+ } catch {
805
+ // The replacement is already committed. A stale backup is safer than
806
+ // rolling back a working install because cleanup failed.
807
+ }
808
+ }
809
+ }
810
+
811
+ function installStagedBinary({ tempBinaryPath, version, targetKey }) {
812
+ const replacements = []
813
+ const metadataTempPath = `${CONFIG.metadataPath}.new.${process.pid}`
814
+
815
+ try {
816
+ fs.writeFileSync(
817
+ metadataTempPath,
818
+ JSON.stringify({ version, target: targetKey }, null, 2),
819
+ )
820
+ replaceFileWithRollback(tempBinaryPath, CONFIG.binaryPath, replacements)
821
+
822
+ // Move tree-sitter.wasm next to the binary if the tarball included
823
+ // it. The CLI binary loads this at startup; embedding it inside the
824
+ // binary itself was unreliable on Windows (bun --compile asset
825
+ // bundling silently dropped or unbound it across several attempts),
826
+ // so we ship it as a sibling file instead. Older artifacts that
827
+ // pre-date this change won't have the wasm and will still install —
828
+ // they'll just hit the same crash they had before, which is fine.
829
+ if (includeTreeSitterWasm) {
830
+ const tempWasmPath = path.join(
831
+ CONFIG.tempDownloadDir,
832
+ 'tree-sitter.wasm',
833
+ )
834
+ if (fs.existsSync(tempWasmPath)) {
835
+ const targetWasmPath = path.join(
836
+ path.dirname(CONFIG.binaryPath),
837
+ 'tree-sitter.wasm',
838
+ )
839
+ replaceFileWithRollback(tempWasmPath, targetWasmPath, replacements)
840
+ }
841
+ }
842
+
843
+ replaceFileWithRollback(
844
+ metadataTempPath,
845
+ CONFIG.metadataPath,
846
+ replacements,
847
+ )
848
+ commitReplacements(replacements)
849
+ } catch (error) {
850
+ rollbackReplacements(replacements)
851
+ throw error
852
+ } finally {
853
+ removeFileIfPresent(metadataTempPath)
854
+ if (fs.existsSync(CONFIG.tempDownloadDir)) {
855
+ fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
856
+ }
857
+ }
858
+
859
+ term.clearLine()
860
+ console.log(`Download complete! Starting ${displayName}...`)
861
+ }
862
+
863
+ async function downloadBinary(version, targetKey = getDownloadTargetKey()) {
864
+ const stagedBinary = await stageBinary(version, targetKey)
865
+ installStagedBinary(stagedBinary)
866
+ }
867
+
868
+ async function ensureBinaryExists() {
869
+ const currentVersion = getCurrentVersion()
870
+ if (currentVersion !== null) {
871
+ return
872
+ }
873
+
874
+ const version = await getLatestVersion()
875
+ if (!version) {
876
+ console.error('❌ Failed to determine latest version')
877
+ console.error('Please check your internet connection and try again')
878
+ process.exit(1)
879
+ }
880
+
881
+ try {
882
+ await downloadBinary(version)
883
+ } catch (error) {
884
+ term.clearLine()
885
+ printDownloadFailure(error)
886
+ process.exit(1)
887
+ }
888
+ }
889
+
890
+ function stopRunningProcess(runningProcess) {
891
+ return new Promise((resolve, reject) => {
892
+ let forceKillTimer
893
+ let forceKillTimeout
894
+
895
+ const cleanup = () => {
896
+ clearTimeout(forceKillTimer)
897
+ clearTimeout(forceKillTimeout)
898
+ runningProcess.removeListener('exit', handleExit)
899
+ }
900
+ const handleExit = () => {
901
+ cleanup()
902
+ resolve()
903
+ }
904
+ const fail = (error) => {
905
+ cleanup()
906
+ reject(error)
907
+ }
908
+
909
+ runningProcess.once('exit', handleExit)
910
+ forceKillTimer = setTimeout(() => {
911
+ forceKillTimeout = setTimeout(() => {
912
+ fail(new Error(`${packageName} did not exit after SIGKILL`))
913
+ }, 1000)
914
+ try {
915
+ runningProcess.kill('SIGKILL')
916
+ } catch (error) {
917
+ fail(error)
918
+ }
919
+ }, 5000)
920
+ try {
921
+ runningProcess.kill('SIGTERM')
922
+ } catch (error) {
923
+ fail(error)
924
+ }
925
+ })
926
+ }
927
+
928
+ async function checkForUpdates(runningProcess, exitListener) {
929
+ let stoppedForUpdate = false
930
+
931
+ try {
932
+ const currentVersion = getCurrentVersion()
933
+
934
+ const latestVersion = await getLatestVersion()
935
+ if (!latestVersion) return
936
+
937
+ if (
938
+ // Download new version if current version is unknown or outdated.
939
+ currentVersion === null ||
940
+ compareVersions(currentVersion, latestVersion) < 0
941
+ ) {
942
+ const stagedBinary = await stageBinary(
943
+ latestVersion,
944
+ getDownloadTargetKey(),
945
+ { quiet: true },
946
+ )
947
+
948
+ term.clearLine()
949
+
950
+ runningProcess.removeListener('exit', exitListener)
951
+ try {
952
+ await stopRunningProcess(runningProcess)
953
+ } catch (error) {
954
+ runningProcess.on('exit', exitListener)
955
+ throw error
956
+ }
957
+ stoppedForUpdate = true
958
+
959
+ resetTerminal({ exitAlternateScreen: true })
960
+ console.log(`Update available: ${currentVersion} → ${latestVersion}`)
961
+
962
+ installStagedBinary(stagedBinary)
963
+
964
+ const newChild = spawnInstalledBinary({ detached: false })
965
+ attachExitHandler(newChild)
966
+
967
+ return new Promise(() => {})
968
+ }
969
+ } catch (error) {
970
+ if (stoppedForUpdate && fs.existsSync(CONFIG.binaryPath)) {
971
+ console.error(
972
+ `Update failed; restarting ${packageName} ${getCurrentVersion()}.`,
973
+ )
974
+ const child = spawnInstalledBinary({ detached: false })
975
+ attachExitHandler(child)
976
+ return new Promise(() => {})
977
+ }
978
+ try {
979
+ fs.rmSync(CONFIG.tempDownloadDir, { recursive: true, force: true })
980
+ } catch {
981
+ // Best effort after a failed background update.
982
+ }
983
+ // A staging failure leaves the current process and binary untouched.
984
+ }
985
+ }
986
+
987
+ function printCrashDiagnostics(code, signal) {
988
+ // Windows NTSTATUS codes (unsigned DWORD)
989
+ const unsignedCode = getUnsignedExitCode(code)
990
+ const isIllegalInstruction = isIllegalInstructionExit(code, signal)
991
+ const isAccessViolation =
992
+ signal === 'SIGSEGV' ||
993
+ (process.platform === 'win32' && unsignedCode === 0xc0000005)
994
+ const isBusError = signal === 'SIGBUS'
995
+ const isAbort =
996
+ signal === 'SIGABRT' ||
997
+ (process.platform === 'win32' && unsignedCode === 0xc0000409)
998
+
999
+ if (!isIllegalInstruction && !isAccessViolation && !isBusError && !isAbort)
1000
+ return
1001
+
1002
+ const exitInfo = signal ? `signal ${signal}` : `code ${code}`
1003
+ console.error('')
1004
+ console.error(`❌ ${packageName} exited immediately (${exitInfo})`)
1005
+ console.error('')
1006
+
1007
+ if (isIllegalInstruction) {
1008
+ console.error(
1009
+ 'Your CPU may not support the required instruction set (AVX2).',
1010
+ )
1011
+ console.error('This typically affects CPUs from before 2013.')
1012
+ console.error('')
1013
+ printBaselineOverrideHint()
1014
+ } else if (isAccessViolation) {
1015
+ console.error('The binary crashed with an access violation.')
1016
+ console.error('')
1017
+ } else if (isBusError) {
1018
+ console.error('The binary crashed with a bus error.')
1019
+ console.error('This may indicate a platform compatibility issue.')
1020
+ console.error('')
1021
+ } else if (isAbort) {
1022
+ console.error('The binary crashed with an abort signal.')
1023
+ console.error('')
1024
+ }
1025
+
1026
+ printSystemInfo()
1027
+ console.error('')
1028
+ console.error('Please report this issue at:')
1029
+ console.error(' https://github.com/savant0x/savant-code/issues')
1030
+ console.error('')
1031
+ }
1032
+
1033
+ function printBaselineOverrideHint() {
1034
+ const fallbackTarget = getBaselineFallbackTargetKey()
1035
+ if (!fallbackTarget) return
1036
+ console.error('To force the baseline (non-AVX2) build, set:')
1037
+ console.error(
1038
+ ` ${packageName.toUpperCase()}_BINARY_TARGET=${fallbackTarget}`,
1039
+ )
1040
+ console.error('')
1041
+ }
1042
+
1043
+ function printSystemInfo() {
1044
+ const metadata = getCurrentMetadata()
1045
+ console.error('System info:')
1046
+ console.error(` Platform: ${process.platform} ${process.arch}`)
1047
+ console.error(` Node: ${process.version}`)
1048
+ if (process.arch === 'x64') {
1049
+ console.error(` AVX2: ${machineHasAvx2() ? 'yes' : 'no'}`)
1050
+ }
1051
+ console.error(` Target: ${metadata?.target || getDefaultTargetKey()}`)
1052
+ console.error(` Binary: ${CONFIG.binaryPath}`)
1053
+ }
1054
+
1055
+ function getInstalledBinaryStatus() {
1056
+ try {
1057
+ const stats = fs.statSync(CONFIG.binaryPath)
1058
+ return stats.isFile() ? `yes (${formatBytes(stats.size)})` : 'no'
1059
+ } catch {
1060
+ return 'no'
1061
+ }
1062
+ }
1063
+
1064
+ function printSpawnFailure(err) {
1065
+ resetTerminal()
1066
+ const code = err && err.code ? ` (${err.code})` : ''
1067
+
1068
+ console.error(`Failed to start ${packageName}: ${err.message}${code}`)
1069
+ console.error('')
1070
+ printSystemInfo()
1071
+ console.error(` Exists: ${getInstalledBinaryStatus()}`)
1072
+
1073
+ if (process.platform === 'win32') {
1074
+ console.error('')
1075
+ console.error(
1076
+ 'On Windows, this can happen when Windows Security or antivirus blocks',
1077
+ )
1078
+ console.error(
1079
+ 'or quarantines the downloaded executable, or when the binary requires',
1080
+ )
1081
+ console.error('CPU instructions that are not available on this machine.')
1082
+ console.error('')
1083
+ printBaselineOverrideHint()
1084
+ }
1085
+
1086
+ console.error('')
1087
+ console.error('Try deleting the downloaded files and running again:')
1088
+ console.error(` ${CONFIG.configDir}`)
1089
+ console.error('')
1090
+ }
1091
+
1092
+ function exitOnSpawnFailure(err) {
1093
+ printSpawnFailure(err)
1094
+ process.exit(1)
1095
+ }
1096
+
1097
+ function spawnInstalledBinary(options = {}) {
1098
+ if (!fs.existsSync(CONFIG.binaryPath)) {
1099
+ try {
1100
+ if (fs.existsSync(CONFIG.metadataPath))
1101
+ fs.unlinkSync(CONFIG.metadataPath)
1102
+ } catch {
1103
+ // best effort
1104
+ }
1105
+ const error = new Error(
1106
+ `downloaded binary is missing at ${CONFIG.binaryPath}`,
1107
+ )
1108
+ error.code = 'BINARY_MISSING'
1109
+ exitOnSpawnFailure(error)
1110
+ }
1111
+
1112
+ // spawn() only emits 'error' asynchronously for a few errno values
1113
+ // (EACCES, EAGAIN, EMFILE, ENFILE, ENOENT); everything else — notably
1114
+ // UNKNOWN on Windows when antivirus or Smart App Control blocks the
1115
+ // exe or the download is corrupt — is thrown synchronously.
1116
+ let child
1117
+ try {
1118
+ child = spawn(CONFIG.binaryPath, process.argv.slice(2), {
1119
+ stdio: 'inherit',
1120
+ ...options,
1121
+ })
1122
+ } catch (err) {
1123
+ exitOnSpawnFailure(err)
1124
+ }
1125
+
1126
+ child.on('error', exitOnSpawnFailure)
1127
+
1128
+ return child
1129
+ }
1130
+
1131
+ async function tryFallbackToBaseline(code, signal) {
1132
+ if (!isIllegalInstructionExit(code, signal)) {
1133
+ return false
1134
+ }
1135
+
1136
+ const fallbackTarget = getBaselineFallbackTargetKey()
1137
+ if (!fallbackTarget) {
1138
+ return false
1139
+ }
1140
+
1141
+ const metadata = getCurrentMetadata()
1142
+ const currentTarget = metadata?.target || getDefaultTargetKey()
1143
+ if (currentTarget === fallbackTarget) {
1144
+ return false
1145
+ }
1146
+
1147
+ const version = metadata?.version || (await getLatestVersion())
1148
+ if (!version) {
1149
+ return false
1150
+ }
1151
+
1152
+ resetTerminal({
1153
+ exitAlternateScreen: shouldExitAlternateScreen(code, signal),
1154
+ })
1155
+ console.error('')
1156
+ console.error(
1157
+ `${packageName} is switching to the older-CPU binary for this machine.`,
1158
+ )
1159
+
1160
+ try {
1161
+ await downloadBinary(version, fallbackTarget)
1162
+ } catch (error) {
1163
+ term.clearLine()
1164
+ console.error(`Failed to download ${fallbackTarget}: ${error.message}`)
1165
+ return false
1166
+ }
1167
+
1168
+ const child = spawnInstalledBinary({ detached: false })
1169
+ attachExitHandler(child, false)
1170
+ return true
1171
+ }
1172
+
1173
+ function attachExitHandler(child, allowBaselineFallback = true) {
1174
+ const exitListener = async (code, signal) => {
1175
+ if (
1176
+ allowBaselineFallback &&
1177
+ (await tryFallbackToBaseline(code, signal))
1178
+ ) {
1179
+ return
1180
+ }
1181
+
1182
+ resetTerminal({
1183
+ exitAlternateScreen: shouldExitAlternateScreen(code, signal),
1184
+ })
1185
+ printCrashDiagnostics(code, signal)
1186
+ process.exit(signal ? 1 : code || 0)
1187
+ }
1188
+
1189
+ child.on('exit', exitListener)
1190
+ return exitListener
1191
+ }
1192
+
1193
+ async function main() {
1194
+ for (const line of startupBanner) {
1195
+ console.log(line)
1196
+ }
1197
+ if (startupBanner.length > 0) console.log('')
1198
+
1199
+ await ensureBinaryExists()
1200
+
1201
+ const child = spawnInstalledBinary()
1202
+ const exitListener = attachExitHandler(child)
1203
+
1204
+ setTimeout(() => {
1205
+ checkForUpdates(child, exitListener)
1206
+ }, 100)
1207
+ }
1208
+
1209
+ return {
1210
+ config: productConfig,
1211
+ main,
1212
+ stopRunningProcess,
1213
+ }
1214
+ }
1215
+
1216
+ module.exports = { createLauncher }