spindb 0.9.2 → 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 +24 -18
- package/cli/commands/attach.ts +108 -0
- package/cli/commands/create.ts +78 -42
- package/cli/commands/detach.ts +100 -0
- package/cli/commands/doctor.ts +16 -2
- package/cli/commands/edit.ts +12 -2
- package/cli/commands/engines.ts +61 -0
- package/cli/commands/list.ts +96 -2
- package/cli/commands/logs.ts +3 -29
- package/cli/commands/menu/container-handlers.ts +76 -4
- package/cli/commands/menu/sql-handlers.ts +4 -26
- package/cli/commands/sqlite.ts +247 -0
- package/cli/helpers.ts +6 -6
- package/cli/index.ts +9 -3
- package/cli/utils/file-follower.ts +95 -0
- package/config/defaults.ts +3 -0
- package/config/os-dependencies.ts +79 -1
- package/config/paths.ts +0 -8
- package/core/binary-manager.ts +181 -66
- package/core/config-manager.ts +37 -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 +49 -18
- 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 +18 -26
- package/engines/sqlite/registry.ts +64 -33
- package/engines/sqlite/scanner.ts +99 -0
- package/package.json +7 -4
- package/types/index.ts +21 -1
package/core/config-manager.ts
CHANGED
|
@@ -5,11 +5,13 @@ import { promisify } from 'util'
|
|
|
5
5
|
import { dirname } from 'path'
|
|
6
6
|
import { paths } from '../config/paths'
|
|
7
7
|
import { logDebug, logWarning } from './error-handler'
|
|
8
|
+
import { platformService } from './platform-service'
|
|
8
9
|
import type {
|
|
9
10
|
SpinDBConfig,
|
|
10
11
|
BinaryConfig,
|
|
11
12
|
BinaryTool,
|
|
12
13
|
BinarySource,
|
|
14
|
+
SQLiteEngineRegistry,
|
|
13
15
|
} from '../types'
|
|
14
16
|
|
|
15
17
|
const execAsync = promisify(exec)
|
|
@@ -164,73 +166,12 @@ export class ConfigManager {
|
|
|
164
166
|
|
|
165
167
|
/**
|
|
166
168
|
* Detect a binary on the system PATH
|
|
169
|
+
* Uses platformService for cross-platform detection (handles which/where and .exe extension)
|
|
167
170
|
*/
|
|
168
171
|
async detectSystemBinary(tool: BinaryTool): Promise<string | null> {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (path && existsSync(path)) {
|
|
173
|
-
return path
|
|
174
|
-
}
|
|
175
|
-
} catch (error) {
|
|
176
|
-
logDebug('which command failed for binary detection', {
|
|
177
|
-
tool,
|
|
178
|
-
error: error instanceof Error ? error.message : String(error),
|
|
179
|
-
})
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Check common locations
|
|
183
|
-
const commonPaths = this.getCommonBinaryPaths(tool)
|
|
184
|
-
for (const path of commonPaths) {
|
|
185
|
-
if (existsSync(path)) {
|
|
186
|
-
return path
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return null
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Get common installation paths for database tools
|
|
195
|
-
*/
|
|
196
|
-
private getCommonBinaryPaths(tool: BinaryTool): string[] {
|
|
197
|
-
const commonPaths: string[] = []
|
|
198
|
-
|
|
199
|
-
// Homebrew (macOS ARM)
|
|
200
|
-
commonPaths.push(`/opt/homebrew/bin/${tool}`)
|
|
201
|
-
// Homebrew (macOS Intel)
|
|
202
|
-
commonPaths.push(`/usr/local/bin/${tool}`)
|
|
203
|
-
|
|
204
|
-
// PostgreSQL-specific paths
|
|
205
|
-
if (POSTGRESQL_TOOLS.includes(tool) || tool === 'pgcli') {
|
|
206
|
-
commonPaths.push(`/opt/homebrew/opt/libpq/bin/${tool}`)
|
|
207
|
-
commonPaths.push(`/usr/local/opt/libpq/bin/${tool}`)
|
|
208
|
-
// Postgres.app (macOS)
|
|
209
|
-
commonPaths.push(
|
|
210
|
-
`/Applications/Postgres.app/Contents/Versions/latest/bin/${tool}`,
|
|
211
|
-
)
|
|
212
|
-
// Linux PostgreSQL paths
|
|
213
|
-
commonPaths.push(`/usr/lib/postgresql/17/bin/${tool}`)
|
|
214
|
-
commonPaths.push(`/usr/lib/postgresql/16/bin/${tool}`)
|
|
215
|
-
commonPaths.push(`/usr/lib/postgresql/15/bin/${tool}`)
|
|
216
|
-
commonPaths.push(`/usr/lib/postgresql/14/bin/${tool}`)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// MySQL-specific paths
|
|
220
|
-
if (MYSQL_TOOLS.includes(tool) || tool === 'mycli') {
|
|
221
|
-
commonPaths.push(`/opt/homebrew/opt/mysql/bin/${tool}`)
|
|
222
|
-
commonPaths.push(`/opt/homebrew/opt/mysql-client/bin/${tool}`)
|
|
223
|
-
commonPaths.push(`/usr/local/opt/mysql/bin/${tool}`)
|
|
224
|
-
commonPaths.push(`/usr/local/opt/mysql-client/bin/${tool}`)
|
|
225
|
-
// Linux MySQL/MariaDB paths
|
|
226
|
-
commonPaths.push(`/usr/bin/${tool}`)
|
|
227
|
-
commonPaths.push(`/usr/sbin/${tool}`)
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// General Linux paths
|
|
231
|
-
commonPaths.push(`/usr/bin/${tool}`)
|
|
232
|
-
|
|
233
|
-
return commonPaths
|
|
172
|
+
// Use platformService which handles cross-platform differences
|
|
173
|
+
// (which vs where, .exe extension, platform-specific search paths)
|
|
174
|
+
return platformService.findToolPath(tool)
|
|
234
175
|
}
|
|
235
176
|
|
|
236
177
|
/**
|
|
@@ -349,6 +290,37 @@ export class ConfigManager {
|
|
|
349
290
|
config.binaries = {}
|
|
350
291
|
await this.save()
|
|
351
292
|
}
|
|
293
|
+
|
|
294
|
+
// ============================================================
|
|
295
|
+
// SQLite Registry Methods
|
|
296
|
+
// ============================================================
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get the SQLite registry from config
|
|
300
|
+
* Returns empty registry if none exists
|
|
301
|
+
*/
|
|
302
|
+
async getSqliteRegistry(): Promise<SQLiteEngineRegistry> {
|
|
303
|
+
const config = await this.load()
|
|
304
|
+
return (
|
|
305
|
+
config.registry?.sqlite ?? {
|
|
306
|
+
version: 1,
|
|
307
|
+
entries: [],
|
|
308
|
+
ignoreFolders: {},
|
|
309
|
+
}
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Save the SQLite registry to config
|
|
315
|
+
*/
|
|
316
|
+
async saveSqliteRegistry(registry: SQLiteEngineRegistry): Promise<void> {
|
|
317
|
+
const config = await this.load()
|
|
318
|
+
if (!config.registry) {
|
|
319
|
+
config.registry = {}
|
|
320
|
+
}
|
|
321
|
+
config.registry.sqlite = registry
|
|
322
|
+
await this.save()
|
|
323
|
+
}
|
|
352
324
|
}
|
|
353
325
|
|
|
354
326
|
export const configManager = new ConfigManager()
|
|
@@ -22,9 +22,37 @@ import {
|
|
|
22
22
|
} from '../config/os-dependencies'
|
|
23
23
|
import { platformService } from './platform-service'
|
|
24
24
|
import { configManager } from './config-manager'
|
|
25
|
+
import type { BinaryTool } from '../types'
|
|
25
26
|
|
|
26
27
|
const execAsync = promisify(exec)
|
|
27
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Known binary tools that can be registered in config
|
|
31
|
+
*/
|
|
32
|
+
const KNOWN_BINARY_TOOLS: readonly BinaryTool[] = [
|
|
33
|
+
'psql',
|
|
34
|
+
'pg_dump',
|
|
35
|
+
'pg_restore',
|
|
36
|
+
'pg_basebackup',
|
|
37
|
+
'mysql',
|
|
38
|
+
'mysqldump',
|
|
39
|
+
'mysqlpump',
|
|
40
|
+
'mysqld',
|
|
41
|
+
'mysqladmin',
|
|
42
|
+
'sqlite3',
|
|
43
|
+
'pgcli',
|
|
44
|
+
'mycli',
|
|
45
|
+
'litecli',
|
|
46
|
+
'usql',
|
|
47
|
+
] as const
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Type guard to check if a string is a known BinaryTool
|
|
51
|
+
*/
|
|
52
|
+
function isBinaryTool(binary: string): binary is BinaryTool {
|
|
53
|
+
return KNOWN_BINARY_TOOLS.includes(binary as BinaryTool)
|
|
54
|
+
}
|
|
55
|
+
|
|
28
56
|
export type DependencyStatus = {
|
|
29
57
|
dependency: Dependency
|
|
30
58
|
installed: boolean
|
|
@@ -79,7 +107,17 @@ export async function findBinary(
|
|
|
79
107
|
binary: string,
|
|
80
108
|
): Promise<{ path: string; version?: string } | null> {
|
|
81
109
|
try {
|
|
82
|
-
//
|
|
110
|
+
// First check if we have this binary registered in config (e.g., from downloaded PostgreSQL)
|
|
111
|
+
if (isBinaryTool(binary)) {
|
|
112
|
+
const configPath = await configManager.getBinaryPath(binary)
|
|
113
|
+
if (configPath) {
|
|
114
|
+
const version =
|
|
115
|
+
(await platformService.getToolVersion(configPath)) || undefined
|
|
116
|
+
return { path: configPath, version }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Fall back to system PATH search
|
|
83
121
|
const path = await platformService.findToolPath(binary)
|
|
84
122
|
if (!path) return null
|
|
85
123
|
|
package/core/platform-service.ts
CHANGED
|
@@ -121,6 +121,28 @@ export abstract class BasePlatformService {
|
|
|
121
121
|
*/
|
|
122
122
|
abstract getZonkyPlatform(): string | null
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Get the null device path for this platform ('/dev/null' on Unix, 'NUL' on Windows)
|
|
126
|
+
*/
|
|
127
|
+
abstract getNullDevice(): string
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get the executable file extension for this platform ('' on Unix, '.exe' on Windows)
|
|
131
|
+
*/
|
|
132
|
+
abstract getExecutableExtension(): string
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Terminate a process by PID
|
|
136
|
+
* @param pid - Process ID to terminate
|
|
137
|
+
* @param force - If true, force kill (SIGKILL on Unix, /F on Windows)
|
|
138
|
+
*/
|
|
139
|
+
abstract terminateProcess(pid: number, force: boolean): Promise<void>
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Check if a process is running by PID
|
|
143
|
+
*/
|
|
144
|
+
abstract isProcessRunning(pid: number): boolean
|
|
145
|
+
|
|
124
146
|
/**
|
|
125
147
|
* Copy text to clipboard
|
|
126
148
|
*/
|
|
@@ -153,13 +175,17 @@ export abstract class BasePlatformService {
|
|
|
153
175
|
async findToolPath(toolName: string): Promise<string | null> {
|
|
154
176
|
const whichConfig = this.getWhichCommand()
|
|
155
177
|
|
|
156
|
-
// First try the which/where command
|
|
178
|
+
// First try the which/where command (with timeout to prevent hanging)
|
|
157
179
|
try {
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
180
|
+
const cmd = [whichConfig.command, ...whichConfig.args, toolName]
|
|
181
|
+
.filter(Boolean)
|
|
182
|
+
.join(' ')
|
|
183
|
+
const { stdout } = await execAsync(cmd, { timeout: 5000 })
|
|
184
|
+
const path = stdout
|
|
185
|
+
.split(/\r?\n/)
|
|
186
|
+
.map((line) => line.trim())
|
|
187
|
+
.find((line) => line.length > 0)
|
|
188
|
+
if (path && existsSync(path)) return path
|
|
163
189
|
} catch {
|
|
164
190
|
// Not found via which, continue to search paths
|
|
165
191
|
}
|
|
@@ -186,7 +212,9 @@ export abstract class BasePlatformService {
|
|
|
186
212
|
*/
|
|
187
213
|
async getToolVersion(toolPath: string): Promise<string | null> {
|
|
188
214
|
try {
|
|
189
|
-
const { stdout } = await execAsync(`"${toolPath}" --version
|
|
215
|
+
const { stdout } = await execAsync(`"${toolPath}" --version`, {
|
|
216
|
+
timeout: 5000,
|
|
217
|
+
})
|
|
190
218
|
const match = stdout.match(/(\d+\.\d+(\.\d+)?)/)
|
|
191
219
|
return match ? match[1] : null
|
|
192
220
|
} catch {
|
|
@@ -332,6 +360,28 @@ class DarwinPlatformService extends BasePlatformService {
|
|
|
332
360
|
return null
|
|
333
361
|
}
|
|
334
362
|
|
|
363
|
+
getNullDevice(): string {
|
|
364
|
+
return '/dev/null'
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
getExecutableExtension(): string {
|
|
368
|
+
return ''
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async terminateProcess(pid: number, force: boolean): Promise<void> {
|
|
372
|
+
const signal = force ? 'SIGKILL' : 'SIGTERM'
|
|
373
|
+
process.kill(pid, signal)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
isProcessRunning(pid: number): boolean {
|
|
377
|
+
try {
|
|
378
|
+
process.kill(pid, 0)
|
|
379
|
+
return true
|
|
380
|
+
} catch {
|
|
381
|
+
return false
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
335
385
|
protected buildToolPath(dir: string, toolName: string): string {
|
|
336
386
|
return `${dir}/${toolName}`
|
|
337
387
|
}
|
|
@@ -517,6 +567,28 @@ class LinuxPlatformService extends BasePlatformService {
|
|
|
517
567
|
return null
|
|
518
568
|
}
|
|
519
569
|
|
|
570
|
+
getNullDevice(): string {
|
|
571
|
+
return '/dev/null'
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
getExecutableExtension(): string {
|
|
575
|
+
return ''
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async terminateProcess(pid: number, force: boolean): Promise<void> {
|
|
579
|
+
const signal = force ? 'SIGKILL' : 'SIGTERM'
|
|
580
|
+
process.kill(pid, signal)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
isProcessRunning(pid: number): boolean {
|
|
584
|
+
try {
|
|
585
|
+
process.kill(pid, 0)
|
|
586
|
+
return true
|
|
587
|
+
} catch {
|
|
588
|
+
return false
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
520
592
|
protected buildToolPath(dir: string, toolName: string): string {
|
|
521
593
|
return `${dir}/${toolName}`
|
|
522
594
|
}
|
|
@@ -591,9 +663,12 @@ class Win32PlatformService extends BasePlatformService {
|
|
|
591
663
|
}
|
|
592
664
|
|
|
593
665
|
async detectPackageManager(): Promise<PackageManagerInfo | null> {
|
|
666
|
+
// Timeout for package manager detection (5 seconds)
|
|
667
|
+
const timeout = 5000
|
|
668
|
+
|
|
594
669
|
// Try chocolatey
|
|
595
670
|
try {
|
|
596
|
-
await execAsync('choco --version')
|
|
671
|
+
await execAsync('choco --version', { timeout })
|
|
597
672
|
return {
|
|
598
673
|
id: 'choco',
|
|
599
674
|
name: 'Chocolatey',
|
|
@@ -602,12 +677,12 @@ class Win32PlatformService extends BasePlatformService {
|
|
|
602
677
|
updateCommand: 'choco upgrade all',
|
|
603
678
|
}
|
|
604
679
|
} catch {
|
|
605
|
-
// Not chocolatey
|
|
680
|
+
// Not chocolatey or timed out
|
|
606
681
|
}
|
|
607
682
|
|
|
608
683
|
// Try winget
|
|
609
684
|
try {
|
|
610
|
-
await execAsync('winget --version')
|
|
685
|
+
await execAsync('winget --version', { timeout })
|
|
611
686
|
return {
|
|
612
687
|
id: 'winget',
|
|
613
688
|
name: 'Windows Package Manager',
|
|
@@ -616,7 +691,21 @@ class Win32PlatformService extends BasePlatformService {
|
|
|
616
691
|
updateCommand: 'winget upgrade --all',
|
|
617
692
|
}
|
|
618
693
|
} catch {
|
|
619
|
-
// Not winget
|
|
694
|
+
// Not winget or timed out
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Try scoop
|
|
698
|
+
try {
|
|
699
|
+
await execAsync('scoop --version', { timeout })
|
|
700
|
+
return {
|
|
701
|
+
id: 'scoop',
|
|
702
|
+
name: 'Scoop',
|
|
703
|
+
checkCommand: 'scoop --version',
|
|
704
|
+
installTemplate: 'scoop install {package}',
|
|
705
|
+
updateCommand: 'scoop update *',
|
|
706
|
+
}
|
|
707
|
+
} catch {
|
|
708
|
+
// Not scoop or timed out
|
|
620
709
|
}
|
|
621
710
|
|
|
622
711
|
return null
|
|
@@ -627,6 +716,40 @@ class Win32PlatformService extends BasePlatformService {
|
|
|
627
716
|
return null
|
|
628
717
|
}
|
|
629
718
|
|
|
719
|
+
getNullDevice(): string {
|
|
720
|
+
return 'NUL'
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
getExecutableExtension(): string {
|
|
724
|
+
return '.exe'
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
async terminateProcess(pid: number, force: boolean): Promise<void> {
|
|
728
|
+
// On Windows, use taskkill command
|
|
729
|
+
// /T = terminate child processes, /F = force termination
|
|
730
|
+
const args = force ? `/F /PID ${pid} /T` : `/PID ${pid}`
|
|
731
|
+
try {
|
|
732
|
+
await execAsync(`taskkill ${args}`)
|
|
733
|
+
} catch (error) {
|
|
734
|
+
// taskkill exits with error if process doesn't exist, which is fine
|
|
735
|
+
const e = error as { code?: number }
|
|
736
|
+
// Error code 128 means "process not found" which is acceptable
|
|
737
|
+
if (e.code !== 128) {
|
|
738
|
+
throw error
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
isProcessRunning(pid: number): boolean {
|
|
744
|
+
try {
|
|
745
|
+
// process.kill with signal 0 works on Windows for checking process existence
|
|
746
|
+
process.kill(pid, 0)
|
|
747
|
+
return true
|
|
748
|
+
} catch {
|
|
749
|
+
return false
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
630
753
|
protected buildToolPath(dir: string, toolName: string): string {
|
|
631
754
|
return `${dir}\\${toolName}.exe`
|
|
632
755
|
}
|
|
@@ -652,3 +775,18 @@ export function createPlatformService(): BasePlatformService {
|
|
|
652
775
|
|
|
653
776
|
// Export singleton instance for convenience
|
|
654
777
|
export const platformService = createPlatformService()
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Check if running on Windows
|
|
781
|
+
*/
|
|
782
|
+
export function isWindows(): boolean {
|
|
783
|
+
return process.platform === 'win32'
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Get spawn options for Windows shell requirements.
|
|
788
|
+
* Windows needs shell:true for proper command execution with quoted paths.
|
|
789
|
+
*/
|
|
790
|
+
export function getWindowsSpawnOptions(): { shell: true } | Record<string, never> {
|
|
791
|
+
return isWindows() ? { shell: true } : {}
|
|
792
|
+
}
|