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 +19 -10
- package/cli/commands/create.ts +72 -42
- package/cli/commands/engines.ts +61 -0
- package/cli/commands/logs.ts +3 -29
- package/cli/commands/menu/container-handlers.ts +32 -3
- package/cli/commands/menu/sql-handlers.ts +4 -26
- package/cli/helpers.ts +6 -6
- package/cli/index.ts +3 -3
- package/cli/utils/file-follower.ts +95 -0
- package/config/defaults.ts +3 -0
- package/config/os-dependencies.ts +79 -1
- package/core/binary-manager.ts +181 -66
- package/core/config-manager.ts +5 -65
- package/core/dependency-manager.ts +39 -1
- package/core/platform-service.ts +149 -11
- package/core/process-manager.ts +152 -33
- package/engines/base-engine.ts +27 -0
- package/engines/mysql/backup.ts +12 -5
- package/engines/mysql/index.ts +328 -110
- package/engines/mysql/restore.ts +22 -6
- package/engines/postgresql/backup.ts +7 -3
- package/engines/postgresql/binary-manager.ts +47 -31
- package/engines/postgresql/edb-binary-urls.ts +123 -0
- package/engines/postgresql/index.ts +109 -22
- package/engines/postgresql/version-maps.ts +63 -0
- package/engines/sqlite/index.ts +9 -19
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/spindb)
|
|
4
4
|
[](LICENSE)
|
|
5
|
-
[](#supported-platforms)
|
|
5
|
+
[](#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
|
|
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
|
|
565
|
-
- macOS (
|
|
566
|
-
-
|
|
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
|
|
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 |
|
|
588
|
+
| Windows | x64 | ✅ Supported |
|
|
581
589
|
|
|
582
|
-
|
|
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.
|
package/cli/commands/create.ts
CHANGED
|
@@ -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
|
-
|
|
360
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
382
|
-
|
|
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.`),
|
package/cli/commands/engines.ts
CHANGED
|
@@ -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
|
+
})
|
package/cli/commands/logs.ts
CHANGED
|
@@ -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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
//
|
|
144
|
-
const
|
|
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
|
+
}
|
package/config/defaults.ts
CHANGED
|
@@ -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
|
}
|