spindb 0.9.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/spindb.svg)](https://www.npmjs.com/package/spindb)
4
4
  [![License: PolyForm Noncommercial](https://img.shields.io/badge/License-PolyForm%20Noncommercial-blue.svg)](LICENSE)
5
- [![Platform: macOS | Linux](https://img.shields.io/badge/Platform-macOS%20%7C%20Linux-lightgrey.svg)](#supported-platforms)
5
+ [![Platform: macOS | Linux | Windows](https://img.shields.io/badge/Platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey.svg)](#supported-platforms)
6
6
 
7
7
  **Local databases without the Docker baggage.**
8
8
 
@@ -140,9 +140,11 @@ That's it. Your database is running on `localhost:5432`, and your data persists
140
140
  | Versions | 14, 15, 16, 17 |
141
141
  | Default port | 5432 |
142
142
  | Default user | `postgres` |
143
- | Binary source | [zonky.io](https://github.com/zonkyio/embedded-postgres-binaries) |
143
+ | Binary source | [zonky.io](https://github.com/zonkyio/embedded-postgres-binaries) (macOS/Linux), [EDB](https://www.enterprisedb.com/) (Windows) |
144
144
 
145
- SpinDB downloads PostgreSQL server binaries automatically. These are pre-compiled binaries from the zonky.io project, hosted on Maven Central. They're extracted from official PostgreSQL distributions and work on macOS and Linux (x64 and ARM64).
145
+ SpinDB downloads PostgreSQL server binaries automatically:
146
+ - **macOS/Linux:** Pre-compiled binaries from the zonky.io project, hosted on Maven Central
147
+ - **Windows:** Official binaries from EnterpriseDB (EDB)
146
148
 
147
149
  **Why download binaries instead of using system PostgreSQL?** You might want PostgreSQL 14 for one project and 17 for another. SpinDB lets you run different versions side-by-side without conflicts.
148
150
 
@@ -170,6 +172,12 @@ brew install mysql
170
172
  # Ubuntu/Debian
171
173
  sudo apt install mysql-server
172
174
 
175
+ # Windows (Chocolatey)
176
+ choco install mysql
177
+
178
+ # Windows (winget)
179
+ winget install Oracle.MySQL
180
+
173
181
  # Check if SpinDB can find MySQL
174
182
  spindb deps check --engine mysql
175
183
  ```
@@ -561,11 +569,11 @@ When you stop a container:
561
569
 
562
570
  ### Binary Sources
563
571
 
564
- **PostgreSQL:** Server binaries are downloaded from [zonky.io/embedded-postgres-binaries](https://github.com/zonkyio/embedded-postgres-binaries), a project that packages official PostgreSQL releases for embedding in applications. The binaries are hosted on Maven Central and support:
565
- - macOS (Apple Silicon and Intel)
566
- - Linux (x64 and ARM64)
572
+ **PostgreSQL:** Server binaries are downloaded automatically:
573
+ - **macOS/Linux:** From [zonky.io/embedded-postgres-binaries](https://github.com/zonkyio/embedded-postgres-binaries), hosted on Maven Central
574
+ - **Windows:** From [EnterpriseDB (EDB)](https://www.enterprisedb.com/download-postgresql-binaries), official PostgreSQL distributions
567
575
 
568
- **MySQL:** Uses your system's MySQL installation. SpinDB detects binaries from Homebrew, apt, pacman, or custom paths.
576
+ **MySQL:** Uses your system's MySQL installation. SpinDB detects binaries from Homebrew (macOS), apt/pacman (Linux), or Chocolatey/winget/Scoop (Windows).
569
577
 
570
578
  ---
571
579
 
@@ -577,15 +585,14 @@ When you stop a container:
577
585
  | macOS | Intel (x64) | ✅ Supported |
578
586
  | Linux | x64 | ✅ Supported |
579
587
  | Linux | ARM64 | ✅ Supported |
580
- | Windows | Any | Not supported |
588
+ | Windows | x64 | Supported |
581
589
 
582
- **Why no Windows?** The zonky.io project doesn't provide Windows binaries for PostgreSQL. Windows support would require a different binary source and significant testing.
590
+ Windows uses EnterpriseDB (EDB) official binaries for PostgreSQL. MySQL and SQLite require system installations via Chocolatey, winget, or Scoop.
583
591
 
584
592
  ---
585
593
 
586
594
  ## Limitations
587
595
 
588
- - **No Windows support** - zonky.io doesn't provide Windows PostgreSQL binaries
589
596
  - **Client tools required** - `psql` and `mysql` must be installed separately for some operations
590
597
  - **Local only** - Databases bind to `127.0.0.1`; remote connections planned for v1.1
591
598
  - **MySQL requires system install** - Unlike PostgreSQL, we don't download MySQL binaries
@@ -653,6 +660,8 @@ rm -rf ~/.spindb
653
660
 
654
661
  ## Contributing
655
662
 
663
+ Note: This repo currently assumes `pnpm` for running tests. `npm test` will shell out to `pnpm` and fail if `pnpm` isn't installed.
664
+
656
665
  See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, testing, and distribution info.
657
666
 
658
667
  See [ARCHITECTURE.md](ARCHITECTURE.md) for project architecture and comprehensive CLI command examples.
@@ -288,6 +288,7 @@ export const createCommand = new Command('create')
288
288
  console.log()
289
289
 
290
290
  const dbEngine = getEngine(engine)
291
+ const isPostgreSQL = engine === Engine.PostgreSQL
291
292
 
292
293
  // SQLite has a simplified flow (no port, no start/stop)
293
294
  if (engine === Engine.SQLite) {
@@ -322,6 +323,60 @@ export const createCommand = new Command('create')
322
323
  }
323
324
  }
324
325
 
326
+ const portSpinner = createSpinner('Finding available port...')
327
+ portSpinner.start()
328
+
329
+ let port: number
330
+ if (options.port) {
331
+ port = parseInt(options.port, 10)
332
+ const available = await portManager.isPortAvailable(port)
333
+ if (!available) {
334
+ portSpinner.fail(`Port ${port} is already in use`)
335
+ process.exit(1)
336
+ }
337
+ portSpinner.succeed(`Using port ${port}`)
338
+ } else {
339
+ const { port: foundPort, isDefault } =
340
+ await portManager.findAvailablePort({
341
+ preferredPort: engineDefaults.defaultPort,
342
+ portRange: engineDefaults.portRange,
343
+ })
344
+ port = foundPort
345
+ if (isDefault) {
346
+ portSpinner.succeed(`Using default port ${port}`)
347
+ } else {
348
+ portSpinner.warn(
349
+ `Default port ${engineDefaults.defaultPort} is in use, using port ${port}`,
350
+ )
351
+ }
352
+ }
353
+
354
+ // For PostgreSQL, download binaries FIRST - they include client tools (psql, pg_dump, etc.)
355
+ // This avoids requiring a separate system installation of client tools
356
+ if (isPostgreSQL) {
357
+ const binarySpinner = createSpinner(
358
+ `Checking ${dbEngine.displayName} ${version} binaries...`,
359
+ )
360
+ binarySpinner.start()
361
+
362
+ const isInstalled = await dbEngine.isBinaryInstalled(version)
363
+ if (isInstalled) {
364
+ binarySpinner.succeed(
365
+ `${dbEngine.displayName} ${version} binaries ready (cached)`,
366
+ )
367
+ } else {
368
+ binarySpinner.text = `Downloading ${dbEngine.displayName} ${version} binaries...`
369
+ await dbEngine.ensureBinaries(version, ({ message }) => {
370
+ binarySpinner.text = message
371
+ })
372
+ binarySpinner.succeed(
373
+ `${dbEngine.displayName} ${version} binaries downloaded`,
374
+ )
375
+ }
376
+ }
377
+
378
+ // Check dependencies (all engines need this)
379
+ // For PostgreSQL, this runs AFTER binary download so client tools are available
325
380
  const depsSpinner = createSpinner('Checking required tools...')
326
381
  depsSpinner.start()
327
382
 
@@ -356,54 +411,29 @@ export const createCommand = new Command('create')
356
411
  depsSpinner.succeed('Required tools available')
357
412
  }
358
413
 
359
- const portSpinner = createSpinner('Finding available port...')
360
- portSpinner.start()
414
+ // For MySQL (and other non-PostgreSQL server DBs), download binaries after dep check
415
+ if (!isPostgreSQL) {
416
+ const binarySpinner = createSpinner(
417
+ `Checking ${dbEngine.displayName} ${version} binaries...`,
418
+ )
419
+ binarySpinner.start()
361
420
 
362
- let port: number
363
- if (options.port) {
364
- port = parseInt(options.port, 10)
365
- const available = await portManager.isPortAvailable(port)
366
- if (!available) {
367
- portSpinner.fail(`Port ${port} is already in use`)
368
- process.exit(1)
369
- }
370
- portSpinner.succeed(`Using port ${port}`)
371
- } else {
372
- const { port: foundPort, isDefault } =
373
- await portManager.findAvailablePort({
374
- preferredPort: engineDefaults.defaultPort,
375
- portRange: engineDefaults.portRange,
376
- })
377
- port = foundPort
378
- if (isDefault) {
379
- portSpinner.succeed(`Using default port ${port}`)
421
+ const isInstalled = await dbEngine.isBinaryInstalled(version)
422
+ if (isInstalled) {
423
+ binarySpinner.succeed(
424
+ `${dbEngine.displayName} ${version} binaries ready (cached)`,
425
+ )
380
426
  } else {
381
- portSpinner.warn(
382
- `Default port ${engineDefaults.defaultPort} is in use, using port ${port}`,
427
+ binarySpinner.text = `Downloading ${dbEngine.displayName} ${version} binaries...`
428
+ await dbEngine.ensureBinaries(version, ({ message }) => {
429
+ binarySpinner.text = message
430
+ })
431
+ binarySpinner.succeed(
432
+ `${dbEngine.displayName} ${version} binaries downloaded`,
383
433
  )
384
434
  }
385
435
  }
386
436
 
387
- const binarySpinner = createSpinner(
388
- `Checking ${dbEngine.displayName} ${version} binaries...`,
389
- )
390
- binarySpinner.start()
391
-
392
- const isInstalled = await dbEngine.isBinaryInstalled(version)
393
- if (isInstalled) {
394
- binarySpinner.succeed(
395
- `${dbEngine.displayName} ${version} binaries ready (cached)`,
396
- )
397
- } else {
398
- binarySpinner.text = `Downloading ${dbEngine.displayName} ${version} binaries...`
399
- await dbEngine.ensureBinaries(version, ({ message }) => {
400
- binarySpinner.text = message
401
- })
402
- binarySpinner.succeed(
403
- `${dbEngine.displayName} ${version} binaries downloaded`,
404
- )
405
- }
406
-
407
437
  while (await containerManager.exists(containerName)) {
408
438
  console.log(
409
439
  chalk.yellow(` Container "${containerName}" already exists.`),
@@ -3,6 +3,10 @@ import chalk from 'chalk'
3
3
  import { rm } from 'fs/promises'
4
4
  import inquirer from 'inquirer'
5
5
  import { containerManager } from '../../core/container-manager'
6
+ import { getEngine } from '../../engines'
7
+ import { binaryManager } from '../../core/binary-manager'
8
+ import { paths } from '../../config/paths'
9
+ import { platformService } from '../../core/platform-service'
6
10
  import { promptConfirm } from '../ui/prompts'
7
11
  import { createSpinner } from '../ui/spinner'
8
12
  import { uiError, uiWarning, uiInfo, formatBytes } from '../ui/theme'
@@ -14,6 +18,7 @@ import {
14
18
  type InstalledMysqlEngine,
15
19
  type InstalledSqliteEngine,
16
20
  } from '../helpers'
21
+ import { Engine } from '../../types'
17
22
 
18
23
  /**
19
24
  * Pad string to width, accounting for emoji taking 2 display columns
@@ -279,3 +284,59 @@ enginesCommand
279
284
  }
280
285
  },
281
286
  )
287
+
288
+ // Download subcommand
289
+ enginesCommand
290
+ .command('download <engine> <version>')
291
+ .description('Download engine binaries (PostgreSQL only)')
292
+ .action(async (engineName: string, version: string) => {
293
+ try {
294
+ // Validate engine name
295
+ const validEngines = ['postgresql', 'pg', 'postgres']
296
+ if (!validEngines.includes(engineName.toLowerCase())) {
297
+ console.error(
298
+ uiError(
299
+ `Only PostgreSQL binaries can be downloaded. MySQL and SQLite use system installations.`,
300
+ ),
301
+ )
302
+ process.exit(1)
303
+ }
304
+
305
+ const engine = getEngine(Engine.PostgreSQL)
306
+
307
+ // Check if already installed
308
+ const isInstalled = await engine.isBinaryInstalled(version)
309
+ if (isInstalled) {
310
+ console.log(
311
+ uiInfo(`PostgreSQL ${version} binaries are already installed.`),
312
+ )
313
+ return
314
+ }
315
+
316
+ const spinner = createSpinner(
317
+ `Downloading PostgreSQL ${version} binaries...`,
318
+ )
319
+ spinner.start()
320
+
321
+ await engine.ensureBinaries(version, ({ message }) => {
322
+ spinner.text = message
323
+ })
324
+
325
+ spinner.succeed(`PostgreSQL ${version} binaries downloaded`)
326
+
327
+ // Show the path for reference
328
+ const { platform, arch } = platformService.getPlatformInfo()
329
+ const fullVersion = binaryManager.getFullVersion(version)
330
+ const binPath = paths.getBinaryPath({
331
+ engine: 'postgresql',
332
+ version: fullVersion,
333
+ platform,
334
+ arch,
335
+ })
336
+ console.log(chalk.gray(` Location: ${binPath}`))
337
+ } catch (error) {
338
+ const e = error as Error
339
+ console.error(uiError(e.message))
340
+ process.exit(1)
341
+ }
342
+ })
@@ -6,13 +6,7 @@ import { containerManager } from '../../core/container-manager'
6
6
  import { paths } from '../../config/paths'
7
7
  import { promptContainerSelect } from '../ui/prompts'
8
8
  import { uiError, uiWarning, uiInfo } from '../ui/theme'
9
-
10
- function getLastNLines(content: string, n: number): string {
11
- const lines = content.split('\n')
12
- const nonEmptyLines =
13
- lines[lines.length - 1] === '' ? lines.slice(0, -1) : lines
14
- return nonEmptyLines.slice(-n).join('\n')
15
- }
9
+ import { followFile, getLastNLines } from '../utils/file-follower'
16
10
 
17
11
  export const logsCommand = new Command('logs')
18
12
  .description('View container logs')
@@ -84,28 +78,8 @@ export const logsCommand = new Command('logs')
84
78
 
85
79
  if (options.follow) {
86
80
  const lineCount = parseInt(options.lines || '50', 10)
87
- const child = spawn(
88
- 'tail',
89
- ['-n', String(lineCount), '-f', logPath],
90
- {
91
- stdio: 'inherit',
92
- },
93
- )
94
-
95
- // Use named handler so we can remove it to prevent listener leaks
96
- const sigintHandler = () => {
97
- process.removeListener('SIGINT', sigintHandler)
98
- child.kill('SIGTERM')
99
- process.exit(0)
100
- }
101
- process.on('SIGINT', sigintHandler)
102
-
103
- await new Promise<void>((resolve) => {
104
- child.on('close', () => {
105
- process.removeListener('SIGINT', sigintHandler)
106
- resolve()
107
- })
108
- })
81
+ // Use cross-platform file following (works on Windows, macOS, Linux)
82
+ await followFile(logPath, lineCount)
109
83
  return
110
84
  }
111
85
 
@@ -53,8 +53,37 @@ export async function handleCreate(): Promise<void> {
53
53
 
54
54
  const dbEngine = getEngine(engine)
55
55
  const isSQLite = engine === 'sqlite'
56
+ const isPostgreSQL = engine === 'postgresql'
57
+
58
+ // For PostgreSQL, download binaries FIRST - they include client tools (psql, pg_dump, etc.)
59
+ // This avoids requiring a separate system installation of client tools
60
+ let portAvailable = true
61
+ if (isPostgreSQL) {
62
+ portAvailable = await portManager.isPortAvailable(port)
63
+
64
+ const binarySpinner = createSpinner(
65
+ `Checking ${dbEngine.displayName} ${version} binaries...`,
66
+ )
67
+ binarySpinner.start()
68
+
69
+ const isInstalled = await dbEngine.isBinaryInstalled(version)
70
+ if (isInstalled) {
71
+ binarySpinner.succeed(
72
+ `${dbEngine.displayName} ${version} binaries ready (cached)`,
73
+ )
74
+ } else {
75
+ binarySpinner.text = `Downloading ${dbEngine.displayName} ${version} binaries...`
76
+ await dbEngine.ensureBinaries(version, ({ message }) => {
77
+ binarySpinner.text = message
78
+ })
79
+ binarySpinner.succeed(
80
+ `${dbEngine.displayName} ${version} binaries downloaded`,
81
+ )
82
+ }
83
+ }
56
84
 
57
85
  // Check dependencies (all engines need this)
86
+ // For PostgreSQL, this runs AFTER binary download so client tools are available
58
87
  const depsSpinner = createSpinner('Checking required tools...')
59
88
  depsSpinner.start()
60
89
 
@@ -89,9 +118,9 @@ export async function handleCreate(): Promise<void> {
89
118
  depsSpinner.succeed('Required tools available')
90
119
  }
91
120
 
92
- // Server databases: check port and binaries
93
- let portAvailable = true
94
- if (!isSQLite) {
121
+ // Server databases (MySQL): check port and binaries
122
+ // PostgreSQL already handled above
123
+ if (!isSQLite && !isPostgreSQL) {
95
124
  portAvailable = await portManager.isPortAvailable(port)
96
125
 
97
126
  const binarySpinner = createSpinner(
@@ -13,6 +13,7 @@ import {
13
13
  } from '../../ui/prompts'
14
14
  import { uiError, uiWarning, uiInfo, uiSuccess } from '../../ui/theme'
15
15
  import { pressEnterToContinue } from './shared'
16
+ import { followFile, getLastNLines } from '../../utils/file-follower'
16
17
 
17
18
  export async function handleRunSql(containerName: string): Promise<void> {
18
19
  const config = await containerManager.getConfig(containerName)
@@ -171,28 +172,8 @@ export async function handleViewLogs(containerName: string): Promise<void> {
171
172
  if (action === 'follow') {
172
173
  console.log(chalk.gray(' Press Ctrl+C to stop following logs'))
173
174
  console.log()
174
- const child = spawn('tail', ['-n', '50', '-f', logPath], {
175
- stdio: 'inherit',
176
- })
177
- await new Promise<void>((resolve) => {
178
- let settled = false
179
-
180
- const cleanup = () => {
181
- if (!settled) {
182
- settled = true
183
- process.off('SIGINT', handleSigint)
184
- resolve()
185
- }
186
- }
187
-
188
- const handleSigint = () => {
189
- child.kill('SIGTERM')
190
- cleanup()
191
- }
192
-
193
- process.on('SIGINT', handleSigint)
194
- child.on('close', cleanup)
195
- })
175
+ // Use cross-platform file following (works on Windows, macOS, Linux)
176
+ await followFile(logPath, 50)
196
177
  return
197
178
  }
198
179
 
@@ -202,10 +183,7 @@ export async function handleViewLogs(containerName: string): Promise<void> {
202
183
  if (content.trim() === '') {
203
184
  console.log(uiInfo('Log file is empty'))
204
185
  } else {
205
- const lines = content.split('\n')
206
- const nonEmptyLines =
207
- lines[lines.length - 1] === '' ? lines.slice(0, -1) : lines
208
- console.log(nonEmptyLines.slice(-lineCount).join('\n'))
186
+ console.log(getLastNLines(content, lineCount))
209
187
  }
210
188
  console.log()
211
189
  await pressEnterToContinue()
package/cli/helpers.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  import { existsSync } from 'fs'
2
2
  import { readdir, lstat } from 'fs/promises'
3
3
  import { join } from 'path'
4
- import { exec, execFile } from 'child_process'
4
+ import { execFile } from 'child_process'
5
5
  import { promisify } from 'util'
6
6
  import { paths } from '../config/paths'
7
+ import { platformService } from '../core/platform-service'
7
8
  import {
8
9
  getMysqldPath,
9
10
  getMysqlVersion,
10
11
  isMariaDB,
11
12
  } from '../engines/mysql/binary-detection'
12
13
 
13
- const execAsync = promisify(exec)
14
14
  const execFileAsync = promisify(execFile)
15
15
 
16
16
  export type InstalledPostgresEngine = {
@@ -44,7 +44,8 @@ export type InstalledEngine =
44
44
  | InstalledSqliteEngine
45
45
 
46
46
  async function getPostgresVersion(binPath: string): Promise<string | null> {
47
- const postgresPath = join(binPath, 'bin', 'postgres')
47
+ const ext = platformService.getExecutableExtension()
48
+ const postgresPath = join(binPath, 'bin', `postgres${ext}`)
48
49
  if (!existsSync(postgresPath)) {
49
50
  return null
50
51
  }
@@ -140,9 +141,8 @@ async function getInstalledMysqlEngine(): Promise<InstalledMysqlEngine | null> {
140
141
 
141
142
  async function getInstalledSqliteEngine(): Promise<InstalledSqliteEngine | null> {
142
143
  try {
143
- // TODO: Use 'where sqlite3' on Windows when adding Windows support
144
- const { stdout: whichOutput } = await execAsync('which sqlite3')
145
- const sqlitePath = whichOutput.trim()
144
+ // Use platform service for cross-platform binary detection
145
+ const sqlitePath = await platformService.findToolPath('sqlite3')
146
146
  if (!sqlitePath) {
147
147
  return null
148
148
  }
package/cli/index.ts CHANGED
@@ -2,9 +2,6 @@ import { program } from 'commander'
2
2
  import { createRequire } from 'module'
3
3
  import chalk from 'chalk'
4
4
  import { createCommand } from './commands/create'
5
-
6
- const require = createRequire(import.meta.url)
7
- const pkg = require('../package.json') as { version: string }
8
5
  import { listCommand } from './commands/list'
9
6
  import { startCommand } from './commands/start'
10
7
  import { stopCommand } from './commands/stop'
@@ -30,6 +27,9 @@ import { detachCommand } from './commands/detach'
30
27
  import { sqliteCommand } from './commands/sqlite'
31
28
  import { updateManager } from '../core/update-manager'
32
29
 
30
+ const require = createRequire(import.meta.url)
31
+ const pkg = require('../package.json') as { version: string }
32
+
33
33
  /**
34
34
  * Show update notification banner if an update is available (from cached data)
35
35
  * This shows on every run until the user updates or disables checks
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Cross-platform file following utility
3
+ *
4
+ * Replaces Unix `tail -f` with Node.js fs.watch for cross-platform support.
5
+ * Works on Windows, macOS, and Linux.
6
+ */
7
+
8
+ import { watch, createReadStream } from 'fs'
9
+ import { readFile, stat } from 'fs/promises'
10
+ import { createInterface } from 'readline'
11
+
12
+ /**
13
+ * Get the last N lines from a string
14
+ */
15
+ export function getLastNLines(content: string, n: number): string {
16
+ const lines = content.split('\n')
17
+ const nonEmptyLines =
18
+ lines[lines.length - 1] === '' ? lines.slice(0, -1) : lines
19
+ return nonEmptyLines.slice(-n).join('\n')
20
+ }
21
+
22
+ /**
23
+ * Follow a file and stream new content to stdout
24
+ *
25
+ * Uses Node.js fs.watch to monitor file changes and streams new content.
26
+ * Handles SIGINT (Ctrl+C) gracefully and cleans up resources.
27
+ *
28
+ * @param filePath - Path to the file to follow
29
+ * @param initialLines - Number of lines to display initially
30
+ * @returns Promise that resolves when following is stopped (via SIGINT)
31
+ */
32
+ export async function followFile(
33
+ filePath: string,
34
+ initialLines: number,
35
+ ): Promise<void> {
36
+ // Read and display initial content
37
+ const content = await readFile(filePath, 'utf-8')
38
+ const initial = getLastNLines(content, initialLines)
39
+ if (initial) {
40
+ console.log(initial)
41
+ }
42
+
43
+ // Track file position - use byte length of content we already read
44
+ // This eliminates race condition: we start exactly where the initial read ended
45
+ let fileSize = Buffer.byteLength(content, 'utf-8')
46
+
47
+ return new Promise((resolve) => {
48
+ let settled = false
49
+
50
+ // Watch for changes
51
+ const watcher = watch(filePath, async (eventType) => {
52
+ if (eventType === 'change') {
53
+ try {
54
+ const newSize = (await stat(filePath)).size
55
+
56
+ if (newSize > fileSize) {
57
+ // Read only the new content
58
+ const stream = createReadStream(filePath, {
59
+ start: fileSize,
60
+ encoding: 'utf-8',
61
+ })
62
+
63
+ const rl = createInterface({ input: stream })
64
+
65
+ for await (const line of rl) {
66
+ console.log(line)
67
+ }
68
+
69
+ fileSize = newSize
70
+ } else if (newSize < fileSize) {
71
+ // File was truncated (log rotation), reset position
72
+ fileSize = newSize
73
+ }
74
+ } catch {
75
+ // File might be temporarily unavailable, ignore
76
+ }
77
+ }
78
+ })
79
+
80
+ const cleanup = () => {
81
+ if (!settled) {
82
+ settled = true
83
+ watcher.close()
84
+ process.off('SIGINT', handleSigint)
85
+ resolve()
86
+ }
87
+ }
88
+
89
+ const handleSigint = () => {
90
+ cleanup()
91
+ }
92
+
93
+ process.on('SIGINT', handleSigint)
94
+ })
95
+ }
@@ -49,5 +49,8 @@ export const defaults: Defaults = {
49
49
  'darwin-x64': 'darwin-amd64',
50
50
  'linux-arm64': 'linux-arm64v8',
51
51
  'linux-x64': 'linux-amd64',
52
+ // Windows uses EDB binaries instead of zonky.io
53
+ // EDB naming convention: windows-x64
54
+ 'win32-x64': 'windows-x64',
52
55
  },
53
56
  }