spindb 0.5.3 → 0.5.4

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
@@ -48,7 +48,7 @@ spindb connect mydb
48
48
  | `spindb info [name]` | Show container details (or all containers) |
49
49
  | `spindb start [name]` | Start a container |
50
50
  | `spindb stop [name]` | Stop a container |
51
- | `spindb connect [name]` | Connect with psql/mysql shell |
51
+ | `spindb connect [name]` | Connect with psql/mysql shell (`--pgcli`/`--mycli` for enhanced) |
52
52
  | `spindb url [name]` | Output connection string |
53
53
  | `spindb edit [name]` | Edit container properties (rename, port) |
54
54
  | `spindb restore [name] [backup]` | Restore a backup file |
@@ -107,6 +107,44 @@ Data is stored in `~/.spindb/`:
107
107
  └── config.json
108
108
  ```
109
109
 
110
+ ## How Data Persists
111
+
112
+ SpinDB runs database servers as **native processes** on your machine—no Docker or virtualization involved. When you start a container, SpinDB launches the actual `postgres` or `mysqld` binary, which listens on localhost at your configured port.
113
+
114
+ ### What happens when you start a container
115
+
116
+ 1. SpinDB runs the database server binary (e.g., `pg_ctl start`)
117
+ 2. The server binds to `127.0.0.1` on your configured port
118
+ 3. A **PID file** is created to track the running process
119
+ 4. Logs are written to the container's log file
120
+
121
+ ### What happens when you stop a container
122
+
123
+ 1. SpinDB sends a shutdown signal to the database process
124
+ 2. The server flushes any pending writes to disk
125
+ 3. The **PID file is removed**
126
+ 4. Your data remains safely in the data directory
127
+
128
+ ### Where your data lives
129
+
130
+ All database files persist in the container's `data/` directory:
131
+
132
+ ```
133
+ ~/.spindb/containers/postgresql/mydb/
134
+ ├── container.json # Container configuration (port, version, etc.)
135
+ ├── postgres.log # Server logs
136
+ └── data/ # ← Your actual database files live here
137
+ ├── base/ # Table data
138
+ ├── pg_wal/ # Transaction logs
139
+ └── ...
140
+ ```
141
+
142
+ The `data/` directory is a standard PostgreSQL/MySQL data directory. Stopping and starting a container doesn't affect your data—only the PID file changes.
143
+
144
+ ### How SpinDB knows if a container is running
145
+
146
+ SpinDB checks for the PID file and verifies the process is still alive. No PID file (or dead process) = stopped container.
147
+
110
148
  ## Client Tools
111
149
 
112
150
  SpinDB bundles the PostgreSQL **server** but not client tools. For `connect` and `restore` commands, you need client tools installed.
@@ -205,6 +243,18 @@ spindb start test-branch
205
243
  # Interactive shell (auto-detects engine)
206
244
  spindb connect mydb
207
245
 
246
+ # Use pgcli/mycli for enhanced shell (dropdown auto-completion)
247
+ spindb connect mydb --pgcli # PostgreSQL
248
+ spindb connect mydb --mycli # MySQL
249
+
250
+ # Install pgcli/mycli and connect in one command
251
+ spindb connect mydb --install-pgcli
252
+ spindb connect mydb --install-mycli
253
+
254
+ # Use usql for universal SQL client (works with both engines)
255
+ spindb connect mydb --tui
256
+ spindb connect mydb --install-tui
257
+
208
258
  # Or use connection string directly
209
259
  psql postgresql://postgres@localhost:5432/mydb
210
260
  mysql -u root -h 127.0.0.1 -P 3306 mydb
@@ -3,134 +3,359 @@ import { spawn } from 'child_process'
3
3
  import chalk from 'chalk'
4
4
  import { containerManager } from '../../core/container-manager'
5
5
  import { processManager } from '../../core/process-manager'
6
+ import {
7
+ isUsqlInstalled,
8
+ isPgcliInstalled,
9
+ isMycliInstalled,
10
+ detectPackageManager,
11
+ installUsql,
12
+ installPgcli,
13
+ installMycli,
14
+ getUsqlManualInstructions,
15
+ getPgcliManualInstructions,
16
+ getMycliManualInstructions,
17
+ } from '../../core/dependency-manager'
6
18
  import { getEngine } from '../../engines'
7
19
  import { getEngineDefaults } from '../../config/defaults'
8
20
  import { promptContainerSelect } from '../ui/prompts'
9
- import { error, warning, info } from '../ui/theme'
21
+ import { error, warning, info, success } from '../ui/theme'
10
22
 
11
23
  export const connectCommand = new Command('connect')
12
24
  .description('Connect to a container with database client')
13
25
  .argument('[name]', 'Container name')
14
26
  .option('-d, --database <name>', 'Database name')
15
- .action(async (name: string | undefined, options: { database?: string }) => {
16
- try {
17
- let containerName = name
27
+ .option('--tui', 'Use usql for enhanced shell experience')
28
+ .option('--install-tui', 'Install usql if not present, then connect')
29
+ .option('--pgcli', 'Use pgcli for enhanced PostgreSQL shell (dropdown auto-completion)')
30
+ .option('--install-pgcli', 'Install pgcli if not present, then connect')
31
+ .option('--mycli', 'Use mycli for enhanced MySQL shell (dropdown auto-completion)')
32
+ .option('--install-mycli', 'Install mycli if not present, then connect')
33
+ .action(
34
+ async (
35
+ name: string | undefined,
36
+ options: {
37
+ database?: string
38
+ tui?: boolean
39
+ installTui?: boolean
40
+ pgcli?: boolean
41
+ installPgcli?: boolean
42
+ mycli?: boolean
43
+ installMycli?: boolean
44
+ },
45
+ ) => {
46
+ try {
47
+ let containerName = name
18
48
 
19
- // Interactive selection if no name provided
20
- if (!containerName) {
21
- const containers = await containerManager.list()
22
- const running = containers.filter((c) => c.status === 'running')
49
+ // Interactive selection if no name provided
50
+ if (!containerName) {
51
+ const containers = await containerManager.list()
52
+ const running = containers.filter((c) => c.status === 'running')
23
53
 
24
- if (running.length === 0) {
25
- if (containers.length === 0) {
26
- console.log(
27
- warning('No containers found. Create one with: spindb create'),
28
- )
29
- } else {
30
- console.log(
31
- warning(
32
- 'No running containers. Start one first with: spindb start',
33
- ),
34
- )
54
+ if (running.length === 0) {
55
+ if (containers.length === 0) {
56
+ console.log(
57
+ warning('No containers found. Create one with: spindb create'),
58
+ )
59
+ } else {
60
+ console.log(
61
+ warning(
62
+ 'No running containers. Start one first with: spindb start',
63
+ ),
64
+ )
65
+ }
66
+ return
35
67
  }
36
- return
68
+
69
+ const selected = await promptContainerSelect(
70
+ running,
71
+ 'Select container to connect to:',
72
+ )
73
+ if (!selected) return
74
+ containerName = selected
37
75
  }
38
76
 
39
- const selected = await promptContainerSelect(
40
- running,
41
- 'Select container to connect to:',
42
- )
43
- if (!selected) return
44
- containerName = selected
45
- }
77
+ // Get container config
78
+ const config = await containerManager.getConfig(containerName)
79
+ if (!config) {
80
+ console.error(error(`Container "${containerName}" not found`))
81
+ process.exit(1)
82
+ }
46
83
 
47
- // Get container config
48
- const config = await containerManager.getConfig(containerName)
49
- if (!config) {
50
- console.error(error(`Container "${containerName}" not found`))
51
- process.exit(1)
52
- }
84
+ const { engine: engineName } = config
85
+ const engineDefaults = getEngineDefaults(engineName)
53
86
 
54
- const { engine: engineName } = config
55
- const engineDefaults = getEngineDefaults(engineName)
56
-
57
- // Default database: container's database or superuser
58
- const database =
59
- options.database ?? config.database ?? engineDefaults.superuser
60
-
61
- // Check if running
62
- const running = await processManager.isRunning(containerName, {
63
- engine: engineName,
64
- })
65
- if (!running) {
66
- console.error(
67
- error(`Container "${containerName}" is not running. Start it first.`),
68
- )
69
- process.exit(1)
70
- }
87
+ // Default database: container's database or superuser
88
+ const database =
89
+ options.database ?? config.database ?? engineDefaults.superuser
71
90
 
72
- // Get engine
73
- const engine = getEngine(engineName)
74
- const connectionString = engine.getConnectionString(config, database)
75
-
76
- console.log(info(`Connecting to ${containerName}:${database}...`))
77
- console.log()
78
-
79
- // Build client command based on engine
80
- let clientCmd: string
81
- let clientArgs: string[]
82
-
83
- if (engineName === 'mysql') {
84
- // MySQL: mysql -h 127.0.0.1 -P port -u root database
85
- clientCmd = 'mysql'
86
- clientArgs = [
87
- '-h',
88
- '127.0.0.1',
89
- '-P',
90
- String(config.port),
91
- '-u',
92
- engineDefaults.superuser,
93
- database,
94
- ]
95
- } else {
96
- // PostgreSQL: psql connection_string
97
- clientCmd = 'psql'
98
- clientArgs = [connectionString]
99
- }
91
+ // Check if running
92
+ const running = await processManager.isRunning(containerName, {
93
+ engine: engineName,
94
+ })
95
+ if (!running) {
96
+ console.error(
97
+ error(
98
+ `Container "${containerName}" is not running. Start it first.`,
99
+ ),
100
+ )
101
+ process.exit(1)
102
+ }
100
103
 
101
- const clientProcess = spawn(clientCmd, clientArgs, {
102
- stdio: 'inherit',
103
- })
104
-
105
- clientProcess.on('error', (err: NodeJS.ErrnoException) => {
106
- if (err.code === 'ENOENT') {
107
- console.log(warning(`${clientCmd} not found on your system.`))
108
- console.log()
109
- console.log(chalk.gray(' Install client tools or connect manually:'))
110
- console.log(chalk.cyan(` ${connectionString}`))
111
- console.log()
112
-
113
- if (engineName === 'mysql') {
114
- console.log(chalk.gray(' On macOS with Homebrew:'))
115
- console.log(chalk.cyan(' brew install mysql-client'))
116
- } else {
117
- console.log(chalk.gray(' On macOS with Homebrew:'))
118
- console.log(
119
- chalk.cyan(' brew install libpq && brew link --force libpq'),
120
- )
104
+ // Get engine
105
+ const engine = getEngine(engineName)
106
+ const connectionString = engine.getConnectionString(config, database)
107
+
108
+ // Handle --tui and --install-tui flags (usql)
109
+ const useUsql = options.tui || options.installTui
110
+ if (useUsql) {
111
+ const usqlInstalled = await isUsqlInstalled()
112
+
113
+ if (!usqlInstalled) {
114
+ if (options.installTui) {
115
+ // Try to install usql
116
+ console.log(
117
+ info('Installing usql for enhanced shell experience...'),
118
+ )
119
+ const pm = await detectPackageManager()
120
+ if (pm) {
121
+ const result = await installUsql(pm)
122
+ if (result.success) {
123
+ console.log(success('usql installed successfully!'))
124
+ console.log()
125
+ } else {
126
+ console.error(
127
+ error(`Failed to install usql: ${result.error}`),
128
+ )
129
+ console.log()
130
+ console.log(chalk.gray('Manual installation:'))
131
+ for (const instruction of getUsqlManualInstructions()) {
132
+ console.log(chalk.cyan(` ${instruction}`))
133
+ }
134
+ process.exit(1)
135
+ }
136
+ } else {
137
+ console.error(error('No supported package manager found'))
138
+ console.log()
139
+ console.log(chalk.gray('Manual installation:'))
140
+ for (const instruction of getUsqlManualInstructions()) {
141
+ console.log(chalk.cyan(` ${instruction}`))
142
+ }
143
+ process.exit(1)
144
+ }
145
+ } else {
146
+ // --tui flag but usql not installed
147
+ console.error(error('usql is not installed'))
148
+ console.log()
149
+ console.log(
150
+ chalk.gray('Install usql for enhanced shell experience:'),
151
+ )
152
+ console.log(chalk.cyan(' spindb connect --install-tui'))
153
+ console.log()
154
+ console.log(chalk.gray('Or install manually:'))
155
+ for (const instruction of getUsqlManualInstructions()) {
156
+ console.log(chalk.cyan(` ${instruction}`))
157
+ }
158
+ process.exit(1)
159
+ }
121
160
  }
122
- console.log()
161
+ }
162
+
163
+ // Handle --pgcli and --install-pgcli flags
164
+ const usePgcli = options.pgcli || options.installPgcli
165
+ if (usePgcli) {
166
+ if (engineName !== 'postgresql') {
167
+ console.error(error('pgcli is only available for PostgreSQL containers'))
168
+ console.log(chalk.gray('For MySQL, use: spindb connect --mycli'))
169
+ process.exit(1)
170
+ }
171
+
172
+ const pgcliInstalled = await isPgcliInstalled()
173
+
174
+ if (!pgcliInstalled) {
175
+ if (options.installPgcli) {
176
+ console.log(info('Installing pgcli for enhanced PostgreSQL shell...'))
177
+ const pm = await detectPackageManager()
178
+ if (pm) {
179
+ const result = await installPgcli(pm)
180
+ if (result.success) {
181
+ console.log(success('pgcli installed successfully!'))
182
+ console.log()
183
+ } else {
184
+ console.error(error(`Failed to install pgcli: ${result.error}`))
185
+ console.log()
186
+ console.log(chalk.gray('Manual installation:'))
187
+ for (const instruction of getPgcliManualInstructions()) {
188
+ console.log(chalk.cyan(` ${instruction}`))
189
+ }
190
+ process.exit(1)
191
+ }
192
+ } else {
193
+ console.error(error('No supported package manager found'))
194
+ console.log()
195
+ console.log(chalk.gray('Manual installation:'))
196
+ for (const instruction of getPgcliManualInstructions()) {
197
+ console.log(chalk.cyan(` ${instruction}`))
198
+ }
199
+ process.exit(1)
200
+ }
201
+ } else {
202
+ console.error(error('pgcli is not installed'))
203
+ console.log()
204
+ console.log(chalk.gray('Install pgcli for enhanced PostgreSQL shell:'))
205
+ console.log(chalk.cyan(' spindb connect --install-pgcli'))
206
+ console.log()
207
+ console.log(chalk.gray('Or install manually:'))
208
+ for (const instruction of getPgcliManualInstructions()) {
209
+ console.log(chalk.cyan(` ${instruction}`))
210
+ }
211
+ process.exit(1)
212
+ }
213
+ }
214
+ }
215
+
216
+ // Handle --mycli and --install-mycli flags
217
+ const useMycli = options.mycli || options.installMycli
218
+ if (useMycli) {
219
+ if (engineName !== 'mysql') {
220
+ console.error(error('mycli is only available for MySQL containers'))
221
+ console.log(chalk.gray('For PostgreSQL, use: spindb connect --pgcli'))
222
+ process.exit(1)
223
+ }
224
+
225
+ const mycliInstalled = await isMycliInstalled()
226
+
227
+ if (!mycliInstalled) {
228
+ if (options.installMycli) {
229
+ console.log(info('Installing mycli for enhanced MySQL shell...'))
230
+ const pm = await detectPackageManager()
231
+ if (pm) {
232
+ const result = await installMycli(pm)
233
+ if (result.success) {
234
+ console.log(success('mycli installed successfully!'))
235
+ console.log()
236
+ } else {
237
+ console.error(error(`Failed to install mycli: ${result.error}`))
238
+ console.log()
239
+ console.log(chalk.gray('Manual installation:'))
240
+ for (const instruction of getMycliManualInstructions()) {
241
+ console.log(chalk.cyan(` ${instruction}`))
242
+ }
243
+ process.exit(1)
244
+ }
245
+ } else {
246
+ console.error(error('No supported package manager found'))
247
+ console.log()
248
+ console.log(chalk.gray('Manual installation:'))
249
+ for (const instruction of getMycliManualInstructions()) {
250
+ console.log(chalk.cyan(` ${instruction}`))
251
+ }
252
+ process.exit(1)
253
+ }
254
+ } else {
255
+ console.error(error('mycli is not installed'))
256
+ console.log()
257
+ console.log(chalk.gray('Install mycli for enhanced MySQL shell:'))
258
+ console.log(chalk.cyan(' spindb connect --install-mycli'))
259
+ console.log()
260
+ console.log(chalk.gray('Or install manually:'))
261
+ for (const instruction of getMycliManualInstructions()) {
262
+ console.log(chalk.cyan(` ${instruction}`))
263
+ }
264
+ process.exit(1)
265
+ }
266
+ }
267
+ }
268
+
269
+ console.log(info(`Connecting to ${containerName}:${database}...`))
270
+ console.log()
271
+
272
+ // Build client command based on engine and shell preference
273
+ let clientCmd: string
274
+ let clientArgs: string[]
275
+
276
+ if (usePgcli) {
277
+ // pgcli accepts connection strings
278
+ clientCmd = 'pgcli'
279
+ clientArgs = [connectionString]
280
+ } else if (useMycli) {
281
+ // mycli: mycli -h host -P port -u user database
282
+ clientCmd = 'mycli'
283
+ clientArgs = [
284
+ '-h',
285
+ '127.0.0.1',
286
+ '-P',
287
+ String(config.port),
288
+ '-u',
289
+ engineDefaults.superuser,
290
+ database,
291
+ ]
292
+ } else if (useUsql) {
293
+ // usql accepts connection strings directly for both PostgreSQL and MySQL
294
+ clientCmd = 'usql'
295
+ clientArgs = [connectionString]
296
+ } else if (engineName === 'mysql') {
297
+ // MySQL: mysql -h 127.0.0.1 -P port -u root database
298
+ clientCmd = 'mysql'
299
+ clientArgs = [
300
+ '-h',
301
+ '127.0.0.1',
302
+ '-P',
303
+ String(config.port),
304
+ '-u',
305
+ engineDefaults.superuser,
306
+ database,
307
+ ]
123
308
  } else {
124
- console.error(error(err.message))
309
+ // PostgreSQL: psql connection_string
310
+ clientCmd = 'psql'
311
+ clientArgs = [connectionString]
125
312
  }
126
- })
127
-
128
- await new Promise<void>((resolve) => {
129
- clientProcess.on('close', () => resolve())
130
- })
131
- } catch (err) {
132
- const e = err as Error
133
- console.error(error(e.message))
134
- process.exit(1)
135
- }
136
- })
313
+
314
+ const clientProcess = spawn(clientCmd, clientArgs, {
315
+ stdio: 'inherit',
316
+ })
317
+
318
+ clientProcess.on('error', (err: NodeJS.ErrnoException) => {
319
+ if (err.code === 'ENOENT') {
320
+ console.log(warning(`${clientCmd} not found on your system.`))
321
+ console.log()
322
+ console.log(
323
+ chalk.gray(' Install client tools or connect manually:'),
324
+ )
325
+ console.log(chalk.cyan(` ${connectionString}`))
326
+ console.log()
327
+
328
+ if (clientCmd === 'usql') {
329
+ console.log(chalk.gray(' Install usql:'))
330
+ console.log(chalk.cyan(' brew tap xo/xo && brew install xo/xo/usql'))
331
+ } else if (clientCmd === 'pgcli') {
332
+ console.log(chalk.gray(' Install pgcli:'))
333
+ console.log(chalk.cyan(' brew install pgcli'))
334
+ } else if (clientCmd === 'mycli') {
335
+ console.log(chalk.gray(' Install mycli:'))
336
+ console.log(chalk.cyan(' brew install mycli'))
337
+ } else if (engineName === 'mysql') {
338
+ console.log(chalk.gray(' On macOS with Homebrew:'))
339
+ console.log(chalk.cyan(' brew install mysql-client'))
340
+ } else {
341
+ console.log(chalk.gray(' On macOS with Homebrew:'))
342
+ console.log(
343
+ chalk.cyan(' brew install libpq && brew link --force libpq'),
344
+ )
345
+ }
346
+ console.log()
347
+ } else {
348
+ console.error(error(err.message))
349
+ }
350
+ })
351
+
352
+ await new Promise<void>((resolve) => {
353
+ clientProcess.on('close', () => resolve())
354
+ })
355
+ } catch (err) {
356
+ const e = err as Error
357
+ console.error(error(e.message))
358
+ process.exit(1)
359
+ }
360
+ },
361
+ )
@@ -251,7 +251,7 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
251
251
 
252
252
  // PostgreSQL rows
253
253
  for (const engine of pgEngines) {
254
- const icon = engineIcons[engine.engine] || '🗄️'
254
+ const icon = engineIcons[engine.engine] || ''
255
255
  const platformInfo = `${engine.platform}-${engine.arch}`
256
256
 
257
257
  console.log(
@@ -65,7 +65,7 @@ async function displayContainerInfo(
65
65
  return
66
66
  }
67
67
 
68
- const icon = engineIcons[config.engine] || '🗄️'
68
+ const icon = engineIcons[config.engine] || ''
69
69
  const statusDisplay =
70
70
  actualStatus === 'running'
71
71
  ? chalk.green('● running')
@@ -168,7 +168,7 @@ async function displayAllContainersInfo(
168
168
  ? chalk.green('● running')
169
169
  : chalk.gray('○ stopped')
170
170
 
171
- const icon = engineIcons[container.engine] || '🗄️'
171
+ const icon = engineIcons[container.engine] || ''
172
172
  const engineDisplay = `${icon} ${container.engine}`
173
173
 
174
174
  console.log(
@@ -251,7 +251,7 @@ export const infoCommand = new Command('info')
251
251
  choices: [
252
252
  { name: 'All containers', value: 'all' },
253
253
  ...containers.map((c) => ({
254
- name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || '🗄️'} ${c.engine})`)}`,
254
+ name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || ''} ${c.engine})`)}`,
255
255
  value: c.name,
256
256
  })),
257
257
  ],
@@ -48,7 +48,7 @@ export const listCommand = new Command('list')
48
48
  ? chalk.green('● running')
49
49
  : chalk.gray('○ stopped')
50
50
 
51
- const engineIcon = engineIcons[container.engine] || '🗄️'
51
+ const engineIcon = engineIcons[container.engine] || ''
52
52
  const engineDisplay = `${engineIcon} ${container.engine}`
53
53
 
54
54
  console.log(