spindb 0.7.0 → 0.7.3

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.
@@ -0,0 +1,150 @@
1
+ import { Command } from 'commander'
2
+ import { existsSync } from 'fs'
3
+ import chalk from 'chalk'
4
+ import { containerManager } from '../../core/container-manager'
5
+ import { processManager } from '../../core/process-manager'
6
+ import { getEngine } from '../../engines'
7
+ import { promptInstallDependencies } from '../ui/prompts'
8
+ import { error, warning } from '../ui/theme'
9
+ import { getMissingDependencies } from '../../core/dependency-manager'
10
+
11
+ export const runCommand = new Command('run')
12
+ .description('Run SQL file or statement against a container')
13
+ .argument('<name>', 'Container name')
14
+ .argument('[file]', 'Path to SQL file')
15
+ .option('-d, --database <name>', 'Target database (defaults to primary)')
16
+ .option('--sql <statement>', 'SQL statement to execute (alternative to file)')
17
+ .action(
18
+ async (
19
+ name: string,
20
+ file: string | undefined,
21
+ options: { database?: string; sql?: string },
22
+ ) => {
23
+ try {
24
+ const containerName = name
25
+
26
+ // Get container config
27
+ const config = await containerManager.getConfig(containerName)
28
+ if (!config) {
29
+ console.error(error(`Container "${containerName}" not found`))
30
+ process.exit(1)
31
+ }
32
+
33
+ const { engine: engineName } = config
34
+
35
+ // Check if running
36
+ const running = await processManager.isRunning(containerName, {
37
+ engine: engineName,
38
+ })
39
+ if (!running) {
40
+ console.error(
41
+ error(
42
+ `Container "${containerName}" is not running. Start it first with: spindb start ${containerName}`,
43
+ ),
44
+ )
45
+ process.exit(1)
46
+ }
47
+
48
+ // Validate: must have either file or --sql, not both
49
+ if (file && options.sql) {
50
+ console.error(
51
+ error('Cannot specify both a file and --sql option. Choose one.'),
52
+ )
53
+ process.exit(1)
54
+ }
55
+
56
+ if (!file && !options.sql) {
57
+ console.error(error('Must provide either a SQL file or --sql option'))
58
+ console.log(
59
+ chalk.gray(' Usage: spindb run <container> <file.sql>'),
60
+ )
61
+ console.log(
62
+ chalk.gray(' or: spindb run <container> --sql "SELECT ..."'),
63
+ )
64
+ process.exit(1)
65
+ }
66
+
67
+ // Validate file exists
68
+ if (file && !existsSync(file)) {
69
+ console.error(error(`SQL file not found: ${file}`))
70
+ process.exit(1)
71
+ }
72
+
73
+ // Get engine
74
+ const engine = getEngine(engineName)
75
+
76
+ // Check for required client tools
77
+ let missingDeps = await getMissingDependencies(engineName)
78
+ if (missingDeps.length > 0) {
79
+ console.log(
80
+ warning(
81
+ `Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
82
+ ),
83
+ )
84
+
85
+ // Offer to install
86
+ const installed = await promptInstallDependencies(
87
+ missingDeps[0].binary,
88
+ engineName,
89
+ )
90
+
91
+ if (!installed) {
92
+ process.exit(1)
93
+ }
94
+
95
+ // Verify installation worked
96
+ missingDeps = await getMissingDependencies(engineName)
97
+ if (missingDeps.length > 0) {
98
+ console.error(
99
+ error(
100
+ `Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
101
+ ),
102
+ )
103
+ process.exit(1)
104
+ }
105
+
106
+ console.log(chalk.green(' ✓ All required tools are now available'))
107
+ console.log()
108
+ }
109
+
110
+ // Determine target database
111
+ const database = options.database || config.database
112
+
113
+ // Run the SQL
114
+ await engine.runScript(config, {
115
+ file,
116
+ sql: options.sql,
117
+ database,
118
+ })
119
+ } catch (err) {
120
+ const e = err as Error
121
+
122
+ // Check if this is a missing tool error
123
+ const missingToolPatterns = [
124
+ 'psql not found',
125
+ 'mysql not found',
126
+ 'mysql client not found',
127
+ ]
128
+
129
+ const matchingPattern = missingToolPatterns.find((p) =>
130
+ e.message.toLowerCase().includes(p.toLowerCase()),
131
+ )
132
+
133
+ if (matchingPattern) {
134
+ const missingTool = matchingPattern
135
+ .replace(' not found', '')
136
+ .replace(' client', '')
137
+ const installed = await promptInstallDependencies(missingTool)
138
+ if (installed) {
139
+ console.log(
140
+ chalk.yellow(' Please re-run your command to continue.'),
141
+ )
142
+ }
143
+ process.exit(1)
144
+ }
145
+
146
+ console.error(error(e.message))
147
+ process.exit(1)
148
+ }
149
+ },
150
+ )
@@ -11,10 +11,11 @@ export const urlCommand = new Command('url')
11
11
  .argument('[name]', 'Container name')
12
12
  .option('-c, --copy', 'Copy to clipboard')
13
13
  .option('-d, --database <database>', 'Use different database name')
14
+ .option('--json', 'Output as JSON with additional connection info')
14
15
  .action(
15
16
  async (
16
17
  name: string | undefined,
17
- options: { copy?: boolean; database?: string },
18
+ options: { copy?: boolean; database?: string; json?: boolean },
18
19
  ) => {
19
20
  try {
20
21
  let containerName = name
@@ -45,10 +46,23 @@ export const urlCommand = new Command('url')
45
46
 
46
47
  // Get connection string
47
48
  const engine = getEngine(config.engine)
48
- const connectionString = engine.getConnectionString(
49
- config,
50
- options.database,
51
- )
49
+ const databaseName = options.database || config.database
50
+ const connectionString = engine.getConnectionString(config, databaseName)
51
+
52
+ // JSON output
53
+ if (options.json) {
54
+ const jsonOutput = {
55
+ connectionString,
56
+ host: '127.0.0.1',
57
+ port: config.port,
58
+ database: databaseName,
59
+ user: config.engine === 'postgresql' ? 'postgres' : 'root',
60
+ engine: config.engine,
61
+ container: config.name,
62
+ }
63
+ console.log(JSON.stringify(jsonOutput, null, 2))
64
+ return
65
+ }
52
66
 
53
67
  // Copy to clipboard if requested
54
68
  if (options.copy) {
@@ -0,0 +1,10 @@
1
+ export const ENGINE_ICONS: Record<string, string> = {
2
+ postgresql: '🐘',
3
+ mysql: '🐬',
4
+ }
5
+
6
+ export const DEFAULT_ENGINE_ICON = '▣'
7
+
8
+ export function getEngineIcon(engine: string): string {
9
+ return ENGINE_ICONS[engine] || DEFAULT_ENGINE_ICON
10
+ }
package/cli/helpers.ts ADDED
@@ -0,0 +1,152 @@
1
+ import { existsSync } from 'fs'
2
+ import { readdir, lstat } from 'fs/promises'
3
+ import { join } from 'path'
4
+ import { exec } from 'child_process'
5
+ import { promisify } from 'util'
6
+ import { paths } from '../config/paths'
7
+ import {
8
+ getMysqldPath,
9
+ getMysqlVersion,
10
+ isMariaDB,
11
+ } from '../engines/mysql/binary-detection'
12
+
13
+ const execAsync = promisify(exec)
14
+
15
+ export type InstalledPostgresEngine = {
16
+ engine: 'postgresql'
17
+ version: string
18
+ platform: string
19
+ arch: string
20
+ path: string
21
+ sizeBytes: number
22
+ source: 'downloaded'
23
+ }
24
+
25
+ export type InstalledMysqlEngine = {
26
+ engine: 'mysql'
27
+ version: string
28
+ path: string
29
+ source: 'system'
30
+ isMariaDB: boolean
31
+ }
32
+
33
+ export type InstalledEngine = InstalledPostgresEngine | InstalledMysqlEngine
34
+
35
+ async function getPostgresVersion(binPath: string): Promise<string | null> {
36
+ const postgresPath = join(binPath, 'bin', 'postgres')
37
+ if (!existsSync(postgresPath)) {
38
+ return null
39
+ }
40
+
41
+ try {
42
+ const { stdout } = await execAsync(`"${postgresPath}" --version`)
43
+ const match = stdout.match(/\(PostgreSQL\)\s+([\d.]+)/)
44
+ return match ? match[1] : null
45
+ } catch {
46
+ return null
47
+ }
48
+ }
49
+
50
+ export async function getInstalledPostgresEngines(): Promise<InstalledPostgresEngine[]> {
51
+ const binDir = paths.bin
52
+
53
+ if (!existsSync(binDir)) {
54
+ return []
55
+ }
56
+
57
+ const entries = await readdir(binDir, { withFileTypes: true })
58
+ const engines: InstalledPostgresEngine[] = []
59
+
60
+ for (const entry of entries) {
61
+ if (entry.isDirectory()) {
62
+ const match = entry.name.match(/^(\w+)-(.+)-(\w+)-(\w+)$/)
63
+ if (match && match[1] === 'postgresql') {
64
+ const [, , majorVersion, platform, arch] = match
65
+ const dirPath = join(binDir, entry.name)
66
+
67
+ const actualVersion =
68
+ (await getPostgresVersion(dirPath)) || majorVersion
69
+
70
+ let sizeBytes = 0
71
+ try {
72
+ const files = await readdir(dirPath, { recursive: true })
73
+ for (const file of files) {
74
+ try {
75
+ const filePath = join(dirPath, file.toString())
76
+ const fileStat = await lstat(filePath)
77
+ if (fileStat.isFile()) {
78
+ sizeBytes += fileStat.size
79
+ }
80
+ } catch {
81
+ // Skip files we can't stat
82
+ }
83
+ }
84
+ } catch {
85
+ // Skip directories we can't read
86
+ }
87
+
88
+ engines.push({
89
+ engine: 'postgresql',
90
+ version: actualVersion,
91
+ platform,
92
+ arch,
93
+ path: dirPath,
94
+ sizeBytes,
95
+ source: 'downloaded',
96
+ })
97
+ }
98
+ }
99
+ }
100
+
101
+ engines.sort((a, b) => compareVersions(b.version, a.version))
102
+
103
+ return engines
104
+ }
105
+
106
+ async function getInstalledMysqlEngine(): Promise<InstalledMysqlEngine | null> {
107
+ const mysqldPath = await getMysqldPath()
108
+ if (!mysqldPath) {
109
+ return null
110
+ }
111
+
112
+ const version = await getMysqlVersion(mysqldPath)
113
+ if (!version) {
114
+ return null
115
+ }
116
+
117
+ const mariadb = await isMariaDB()
118
+
119
+ return {
120
+ engine: 'mysql',
121
+ version,
122
+ path: mysqldPath,
123
+ source: 'system',
124
+ isMariaDB: mariadb,
125
+ }
126
+ }
127
+
128
+ export function compareVersions(a: string, b: string): number {
129
+ const partsA = a.split('.').map((p) => parseInt(p, 10) || 0)
130
+ const partsB = b.split('.').map((p) => parseInt(p, 10) || 0)
131
+
132
+ for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
133
+ const numA = partsA[i] || 0
134
+ const numB = partsB[i] || 0
135
+ if (numA !== numB) return numA - numB
136
+ }
137
+ return 0
138
+ }
139
+
140
+ export async function getInstalledEngines(): Promise<InstalledEngine[]> {
141
+ const engines: InstalledEngine[] = []
142
+
143
+ const pgEngines = await getInstalledPostgresEngines()
144
+ engines.push(...pgEngines)
145
+
146
+ const mysqlEngine = await getInstalledMysqlEngine()
147
+ if (mysqlEngine) {
148
+ engines.push(mysqlEngine)
149
+ }
150
+
151
+ return engines
152
+ }
package/cli/index.ts CHANGED
@@ -22,6 +22,8 @@ import { urlCommand } from './commands/url'
22
22
  import { infoCommand } from './commands/info'
23
23
  import { selfUpdateCommand } from './commands/self-update'
24
24
  import { versionCommand } from './commands/version'
25
+ import { runCommand } from './commands/run'
26
+ import { logsCommand } from './commands/logs'
25
27
  import { updateManager } from '../core/update-manager'
26
28
 
27
29
  /**
@@ -119,11 +121,12 @@ export async function run(): Promise<void> {
119
121
  program.addCommand(infoCommand)
120
122
  program.addCommand(selfUpdateCommand)
121
123
  program.addCommand(versionCommand)
124
+ program.addCommand(runCommand)
125
+ program.addCommand(logsCommand)
122
126
 
123
127
  // If no arguments provided, show interactive menu
124
128
  if (process.argv.length <= 2) {
125
- const { menuCommand: menu } = await import('./commands/menu')
126
- await menu.parseAsync([])
129
+ await menuCommand.parseAsync([])
127
130
  return
128
131
  }
129
132
 
package/cli/ui/prompts.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  installEngineDependencies,
12
12
  } from '../../core/dependency-manager'
13
13
  import { getEngineDependencies } from '../../config/os-dependencies'
14
+ import { getEngineIcon } from '../constants'
14
15
  import type { ContainerConfig } from '../../types'
15
16
 
16
17
  /**
@@ -37,23 +38,14 @@ export async function promptContainerName(
37
38
  return name
38
39
  }
39
40
 
40
- /**
41
- * Engine icons for display
42
- */
43
- const engineIcons: Record<string, string> = {
44
- postgresql: '🐘',
45
- mysql: '🐬',
46
- }
47
-
48
41
  /**
49
42
  * Prompt for database engine selection
50
43
  */
51
44
  export async function promptEngine(): Promise<string> {
52
45
  const engines = listEngines()
53
46
 
54
- // Build choices from available engines
55
47
  const choices = engines.map((e) => ({
56
- name: `${engineIcons[e.name] || '▣'} ${e.displayName} ${chalk.gray(`(versions: ${e.supportedVersions.join(', ')})`)}`,
48
+ name: `${getEngineIcon(e.name)} ${e.displayName} ${chalk.gray(`(versions: ${e.supportedVersions.join(', ')})`)}`,
57
49
  value: e.name,
58
50
  short: e.displayName,
59
51
  }))
@@ -227,7 +219,7 @@ export async function promptContainerSelect(
227
219
  name: 'container',
228
220
  message,
229
221
  choices: containers.map((c) => ({
230
- name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || '▣'} ${c.engine} ${c.version}, port ${c.port})`)} ${
222
+ name: `${c.name} ${chalk.gray(`(${getEngineIcon(c.engine)} ${c.engine} ${c.version}, port ${c.port})`)} ${
231
223
  c.status === 'running'
232
224
  ? chalk.green('● running')
233
225
  : chalk.gray('○ stopped')
@@ -43,13 +43,6 @@ export type InstallResult = {
43
43
  error?: string
44
44
  }
45
45
 
46
- // =============================================================================
47
- // Package Manager Detection
48
- // =============================================================================
49
-
50
- /**
51
- * Detect which package manager is available on the current system
52
- */
53
46
  export async function detectPackageManager(): Promise<DetectedPackageManager | null> {
54
47
  const { platform } = platformService.getPlatformInfo()
55
48
 
@@ -81,13 +74,6 @@ export function getCurrentPlatform(): Platform {
81
74
  return platformService.getPlatformInfo().platform as Platform
82
75
  }
83
76
 
84
- // =============================================================================
85
- // Dependency Checking
86
- // =============================================================================
87
-
88
- /**
89
- * Check if a binary is installed and get its path
90
- */
91
77
  export async function findBinary(
92
78
  binary: string,
93
79
  ): Promise<{ path: string; version?: string } | null> {
@@ -164,13 +150,6 @@ export async function getAllMissingDependencies(): Promise<Dependency[]> {
164
150
  return statuses.filter((s) => !s.installed).map((s) => s.dependency)
165
151
  }
166
152
 
167
- // =============================================================================
168
- // Installation
169
- // =============================================================================
170
-
171
- /**
172
- * Check if stdin is a TTY (interactive terminal)
173
- */
174
153
  function hasTTY(): boolean {
175
154
  return process.stdin.isTTY === true
176
155
  }
@@ -362,13 +341,6 @@ export async function installAllDependencies(
362
341
  return results
363
342
  }
364
343
 
365
- // =============================================================================
366
- // Manual Installation Instructions
367
- // =============================================================================
368
-
369
- /**
370
- * Get manual installation instructions for a dependency
371
- */
372
344
  export function getManualInstallInstructions(
373
345
  dependency: Dependency,
374
346
  platform: Platform = getCurrentPlatform(),
@@ -376,188 +348,53 @@ export function getManualInstallInstructions(
376
348
  return dependency.manualInstall[platform] || []
377
349
  }
378
350
 
379
- /**
380
- * Get manual installation instructions for all missing dependencies of an engine
381
- */
382
- export function getEngineManualInstallInstructions(
383
- engine: string,
384
- missingDeps: Dependency[],
385
- platform: Platform = getCurrentPlatform(),
386
- ): string[] {
387
- // Since all deps usually come from the same package, just get instructions from the first one
388
- if (missingDeps.length === 0) return []
389
-
390
- return getManualInstallInstructions(missingDeps[0], platform)
391
- }
392
-
393
- // =============================================================================
394
- // High-Level API
395
- // =============================================================================
396
-
397
- export type DependencyCheckResult = {
398
- engine: string
399
- allInstalled: boolean
400
- installed: DependencyStatus[]
401
- missing: DependencyStatus[]
402
- }
403
-
404
- /**
405
- * Get a complete dependency report for an engine
406
- */
407
- export async function getDependencyReport(
408
- engine: string,
409
- ): Promise<DependencyCheckResult> {
410
- const statuses = await checkEngineDependencies(engine)
411
-
412
- return {
413
- engine,
414
- allInstalled: statuses.every((s) => s.installed),
415
- installed: statuses.filter((s) => s.installed),
416
- missing: statuses.filter((s) => !s.installed),
417
- }
418
- }
419
-
420
- /**
421
- * Get dependency reports for all engines
422
- */
423
- export async function getAllDependencyReports(): Promise<
424
- DependencyCheckResult[]
425
- > {
426
- const engines = ['postgresql', 'mysql']
427
- const reports = await Promise.all(
428
- engines.map((engine) => getDependencyReport(engine)),
429
- )
430
- return reports
431
- }
432
-
433
- // =============================================================================
434
- // usql (Enhanced Shell) Support
435
- // =============================================================================
436
-
437
- /**
438
- * Check if usql is installed
439
- */
440
351
  export async function isUsqlInstalled(): Promise<boolean> {
441
352
  const status = await checkDependency(usqlDependency)
442
353
  return status.installed
443
354
  }
444
355
 
445
- /**
446
- * Get usql dependency status
447
- */
448
- export async function getUsqlStatus(): Promise<DependencyStatus> {
449
- return checkDependency(usqlDependency)
450
- }
451
-
452
- /**
453
- * Install usql using the detected package manager
454
- */
455
356
  export async function installUsql(
456
357
  packageManager: DetectedPackageManager,
457
358
  ): Promise<InstallResult> {
458
359
  return installDependency(usqlDependency, packageManager)
459
360
  }
460
361
 
461
- /**
462
- * Get usql manual installation instructions
463
- */
464
362
  export function getUsqlManualInstructions(
465
363
  platform: Platform = getCurrentPlatform(),
466
364
  ): string[] {
467
365
  return getManualInstallInstructions(usqlDependency, platform)
468
366
  }
469
367
 
470
- /**
471
- * Get the usql dependency definition
472
- */
473
- export function getUsqlDependency(): Dependency {
474
- return usqlDependency
475
- }
476
-
477
- // =============================================================================
478
- // pgcli (PostgreSQL Enhanced Shell) Support
479
- // =============================================================================
480
-
481
- /**
482
- * Check if pgcli is installed
483
- */
484
368
  export async function isPgcliInstalled(): Promise<boolean> {
485
369
  const status = await checkDependency(pgcliDependency)
486
370
  return status.installed
487
371
  }
488
372
 
489
- /**
490
- * Get pgcli dependency status
491
- */
492
- export async function getPgcliStatus(): Promise<DependencyStatus> {
493
- return checkDependency(pgcliDependency)
494
- }
495
-
496
- /**
497
- * Install pgcli using the detected package manager
498
- */
499
373
  export async function installPgcli(
500
374
  packageManager: DetectedPackageManager,
501
375
  ): Promise<InstallResult> {
502
376
  return installDependency(pgcliDependency, packageManager)
503
377
  }
504
378
 
505
- /**
506
- * Get pgcli manual installation instructions
507
- */
508
379
  export function getPgcliManualInstructions(
509
380
  platform: Platform = getCurrentPlatform(),
510
381
  ): string[] {
511
382
  return getManualInstallInstructions(pgcliDependency, platform)
512
383
  }
513
384
 
514
- /**
515
- * Get the pgcli dependency definition
516
- */
517
- export function getPgcliDependency(): Dependency {
518
- return pgcliDependency
519
- }
520
-
521
- // =============================================================================
522
- // mycli (MySQL Enhanced Shell) Support
523
- // =============================================================================
524
-
525
- /**
526
- * Check if mycli is installed
527
- */
528
385
  export async function isMycliInstalled(): Promise<boolean> {
529
386
  const status = await checkDependency(mycliDependency)
530
387
  return status.installed
531
388
  }
532
389
 
533
- /**
534
- * Get mycli dependency status
535
- */
536
- export async function getMycliStatus(): Promise<DependencyStatus> {
537
- return checkDependency(mycliDependency)
538
- }
539
-
540
- /**
541
- * Install mycli using the detected package manager
542
- */
543
390
  export async function installMycli(
544
391
  packageManager: DetectedPackageManager,
545
392
  ): Promise<InstallResult> {
546
393
  return installDependency(mycliDependency, packageManager)
547
394
  }
548
395
 
549
- /**
550
- * Get mycli manual installation instructions
551
- */
552
396
  export function getMycliManualInstructions(
553
397
  platform: Platform = getCurrentPlatform(),
554
398
  ): string[] {
555
399
  return getManualInstallInstructions(mycliDependency, platform)
556
400
  }
557
-
558
- /**
559
- * Get the mycli dependency definition
560
- */
561
- export function getMycliDependency(): Dependency {
562
- return mycliDependency
563
- }