spindb 0.4.0 → 0.5.2
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 +77 -100
- package/cli/commands/clone.ts +3 -1
- package/cli/commands/connect.ts +50 -24
- package/cli/commands/create.ts +265 -112
- package/cli/commands/delete.ts +3 -1
- package/cli/commands/list.ts +14 -3
- package/cli/commands/menu.ts +250 -84
- package/cli/commands/restore.ts +142 -38
- package/cli/commands/start.ts +30 -4
- package/cli/commands/stop.ts +3 -1
- package/cli/ui/prompts.ts +95 -32
- package/config/defaults.ts +40 -15
- package/config/engine-defaults.ts +84 -0
- package/config/os-dependencies.ts +68 -19
- package/config/paths.ts +116 -23
- package/core/binary-manager.ts +30 -5
- package/core/container-manager.ts +124 -60
- package/core/dependency-manager.ts +44 -22
- package/core/port-manager.ts +42 -31
- package/core/postgres-binary-manager.ts +10 -9
- package/core/process-manager.ts +14 -6
- package/engines/index.ts +7 -2
- package/engines/mysql/binary-detection.ts +248 -0
- package/engines/mysql/index.ts +699 -0
- package/engines/postgresql/index.ts +13 -6
- package/package.json +4 -2
- package/types/index.ts +29 -5
package/cli/commands/create.ts
CHANGED
|
@@ -5,10 +5,12 @@ import chalk from 'chalk'
|
|
|
5
5
|
import { containerManager } from '../../core/container-manager'
|
|
6
6
|
import { portManager } from '../../core/port-manager'
|
|
7
7
|
import { getEngine } from '../../engines'
|
|
8
|
-
import {
|
|
8
|
+
import { getEngineDefaults } from '../../config/defaults'
|
|
9
9
|
import {
|
|
10
10
|
promptCreateOptions,
|
|
11
11
|
promptInstallDependencies,
|
|
12
|
+
promptContainerName,
|
|
13
|
+
promptConfirm,
|
|
12
14
|
} from '../ui/prompts'
|
|
13
15
|
import { createSpinner } from '../ui/spinner'
|
|
14
16
|
import { header, error, connectionBox } from '../ui/theme'
|
|
@@ -16,38 +18,43 @@ import { tmpdir } from 'os'
|
|
|
16
18
|
import { join } from 'path'
|
|
17
19
|
import { spawn } from 'child_process'
|
|
18
20
|
import { platform } from 'os'
|
|
21
|
+
import { getMissingDependencies } from '../../core/dependency-manager'
|
|
22
|
+
import type { EngineName } from '../../types'
|
|
19
23
|
|
|
20
24
|
/**
|
|
21
25
|
* Detect if a location string is a connection string or a file path
|
|
26
|
+
* Also infers engine from connection string scheme
|
|
22
27
|
*/
|
|
23
|
-
function detectLocationType(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
function detectLocationType(location: string): {
|
|
29
|
+
type: 'connection' | 'file' | 'not_found'
|
|
30
|
+
inferredEngine?: EngineName
|
|
31
|
+
} {
|
|
32
|
+
// Check for PostgreSQL connection string
|
|
27
33
|
if (
|
|
28
34
|
location.startsWith('postgresql://') ||
|
|
29
35
|
location.startsWith('postgres://')
|
|
30
36
|
) {
|
|
31
|
-
return 'connection'
|
|
37
|
+
return { type: 'connection', inferredEngine: 'postgresql' }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check for MySQL connection string
|
|
41
|
+
if (location.startsWith('mysql://')) {
|
|
42
|
+
return { type: 'connection', inferredEngine: 'mysql' }
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
// Check if file exists
|
|
35
46
|
if (existsSync(location)) {
|
|
36
|
-
return 'file'
|
|
47
|
+
return { type: 'file' }
|
|
37
48
|
}
|
|
38
49
|
|
|
39
|
-
return 'not_found'
|
|
50
|
+
return { type: 'not_found' }
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
export const createCommand = new Command('create')
|
|
43
54
|
.description('Create a new database container')
|
|
44
55
|
.argument('[name]', 'Container name')
|
|
45
|
-
.option('-e, --engine <engine>', 'Database engine
|
|
46
|
-
.option(
|
|
47
|
-
'--pg-version <version>',
|
|
48
|
-
'PostgreSQL version',
|
|
49
|
-
defaults.postgresVersion,
|
|
50
|
-
)
|
|
56
|
+
.option('-e, --engine <engine>', 'Database engine (postgresql, mysql)')
|
|
57
|
+
.option('-v, --version <version>', 'Database version')
|
|
51
58
|
.option('-d, --database <database>', 'Database name')
|
|
52
59
|
.option('-p, --port <port>', 'Port number')
|
|
53
60
|
.option('--no-start', 'Do not start the container after creation')
|
|
@@ -59,8 +66,8 @@ export const createCommand = new Command('create')
|
|
|
59
66
|
async (
|
|
60
67
|
name: string | undefined,
|
|
61
68
|
options: {
|
|
62
|
-
engine
|
|
63
|
-
|
|
69
|
+
engine?: string
|
|
70
|
+
version?: string
|
|
64
71
|
database?: string
|
|
65
72
|
port?: string
|
|
66
73
|
start: boolean
|
|
@@ -71,41 +78,39 @@ export const createCommand = new Command('create')
|
|
|
71
78
|
|
|
72
79
|
try {
|
|
73
80
|
let containerName = name
|
|
74
|
-
let engine = options.engine
|
|
75
|
-
let version = options.
|
|
81
|
+
let engine: EngineName = (options.engine as EngineName) || 'postgresql'
|
|
82
|
+
let version = options.version
|
|
76
83
|
let database = options.database
|
|
77
84
|
|
|
78
|
-
//
|
|
79
|
-
if (!containerName) {
|
|
80
|
-
const answers = await promptCreateOptions()
|
|
81
|
-
containerName = answers.name
|
|
82
|
-
engine = answers.engine
|
|
83
|
-
version = answers.version
|
|
84
|
-
database = answers.database
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Default database name to container name if not specified
|
|
88
|
-
database = database ?? containerName
|
|
89
|
-
|
|
90
|
-
// Validate --from location if provided
|
|
85
|
+
// Validate --from location if provided (before prompts so we can infer engine)
|
|
91
86
|
let restoreLocation: string | null = null
|
|
92
87
|
let restoreType: 'connection' | 'file' | null = null
|
|
93
88
|
|
|
94
89
|
if (options.from) {
|
|
95
|
-
const
|
|
90
|
+
const locationInfo = detectLocationType(options.from)
|
|
96
91
|
|
|
97
|
-
if (
|
|
92
|
+
if (locationInfo.type === 'not_found') {
|
|
98
93
|
console.error(error(`Location not found: ${options.from}`))
|
|
99
94
|
console.log(
|
|
100
95
|
chalk.gray(
|
|
101
|
-
' Provide a valid file path or connection string (postgresql
|
|
96
|
+
' Provide a valid file path or connection string (postgresql://, mysql://)',
|
|
102
97
|
),
|
|
103
98
|
)
|
|
104
99
|
process.exit(1)
|
|
105
100
|
}
|
|
106
101
|
|
|
107
102
|
restoreLocation = options.from
|
|
108
|
-
restoreType =
|
|
103
|
+
restoreType = locationInfo.type
|
|
104
|
+
|
|
105
|
+
// Infer engine from connection string if not explicitly set
|
|
106
|
+
if (!options.engine && locationInfo.inferredEngine) {
|
|
107
|
+
engine = locationInfo.inferredEngine
|
|
108
|
+
console.log(
|
|
109
|
+
chalk.gray(
|
|
110
|
+
` Inferred engine "${engine}" from connection string`,
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
}
|
|
109
114
|
|
|
110
115
|
// If using --from, we must start the container
|
|
111
116
|
if (options.start === false) {
|
|
@@ -118,12 +123,69 @@ export const createCommand = new Command('create')
|
|
|
118
123
|
}
|
|
119
124
|
}
|
|
120
125
|
|
|
126
|
+
// Get engine defaults for port range and default version
|
|
127
|
+
const engineDefaults = getEngineDefaults(engine)
|
|
128
|
+
|
|
129
|
+
// Set version to engine default if not specified
|
|
130
|
+
if (!version) {
|
|
131
|
+
version = engineDefaults.defaultVersion
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Interactive mode if no name provided
|
|
135
|
+
if (!containerName) {
|
|
136
|
+
const answers = await promptCreateOptions()
|
|
137
|
+
containerName = answers.name
|
|
138
|
+
engine = answers.engine as EngineName
|
|
139
|
+
version = answers.version
|
|
140
|
+
database = answers.database
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Default database name to container name if not specified
|
|
144
|
+
database = database ?? containerName
|
|
145
|
+
|
|
121
146
|
console.log(header('Creating Database Container'))
|
|
122
147
|
console.log()
|
|
123
148
|
|
|
124
149
|
// Get the engine
|
|
125
150
|
const dbEngine = getEngine(engine)
|
|
126
151
|
|
|
152
|
+
// Check for required client tools BEFORE creating anything
|
|
153
|
+
const depsSpinner = createSpinner('Checking required tools...')
|
|
154
|
+
depsSpinner.start()
|
|
155
|
+
|
|
156
|
+
let missingDeps = await getMissingDependencies(engine)
|
|
157
|
+
if (missingDeps.length > 0) {
|
|
158
|
+
depsSpinner.warn(
|
|
159
|
+
`Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
// Offer to install
|
|
163
|
+
const installed = await promptInstallDependencies(
|
|
164
|
+
missingDeps[0].binary,
|
|
165
|
+
engine,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if (!installed) {
|
|
169
|
+
process.exit(1)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Verify installation worked
|
|
173
|
+
missingDeps = await getMissingDependencies(engine)
|
|
174
|
+
if (missingDeps.length > 0) {
|
|
175
|
+
console.error(
|
|
176
|
+
error(
|
|
177
|
+
`Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
process.exit(1)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log(chalk.green(' ✓ All required tools are now available'))
|
|
184
|
+
console.log()
|
|
185
|
+
} else {
|
|
186
|
+
depsSpinner.succeed('Required tools available')
|
|
187
|
+
}
|
|
188
|
+
|
|
127
189
|
// Find available port
|
|
128
190
|
const portSpinner = createSpinner('Finding available port...')
|
|
129
191
|
portSpinner.start()
|
|
@@ -139,30 +201,47 @@ export const createCommand = new Command('create')
|
|
|
139
201
|
portSpinner.succeed(`Using port ${port}`)
|
|
140
202
|
} else {
|
|
141
203
|
const { port: foundPort, isDefault } =
|
|
142
|
-
await portManager.findAvailablePort(
|
|
204
|
+
await portManager.findAvailablePort({
|
|
205
|
+
preferredPort: engineDefaults.defaultPort,
|
|
206
|
+
portRange: engineDefaults.portRange,
|
|
207
|
+
})
|
|
143
208
|
port = foundPort
|
|
144
209
|
if (isDefault) {
|
|
145
210
|
portSpinner.succeed(`Using default port ${port}`)
|
|
146
211
|
} else {
|
|
147
|
-
portSpinner.warn(
|
|
212
|
+
portSpinner.warn(
|
|
213
|
+
`Default port ${engineDefaults.defaultPort} is in use, using port ${port}`,
|
|
214
|
+
)
|
|
148
215
|
}
|
|
149
216
|
}
|
|
150
217
|
|
|
151
218
|
// Ensure binaries are available
|
|
152
219
|
const binarySpinner = createSpinner(
|
|
153
|
-
`Checking
|
|
220
|
+
`Checking ${dbEngine.displayName} ${version} binaries...`,
|
|
154
221
|
)
|
|
155
222
|
binarySpinner.start()
|
|
156
223
|
|
|
157
224
|
const isInstalled = await dbEngine.isBinaryInstalled(version)
|
|
158
225
|
if (isInstalled) {
|
|
159
|
-
binarySpinner.succeed(
|
|
226
|
+
binarySpinner.succeed(
|
|
227
|
+
`${dbEngine.displayName} ${version} binaries ready (cached)`,
|
|
228
|
+
)
|
|
160
229
|
} else {
|
|
161
|
-
binarySpinner.text = `Downloading
|
|
230
|
+
binarySpinner.text = `Downloading ${dbEngine.displayName} ${version} binaries...`
|
|
162
231
|
await dbEngine.ensureBinaries(version, ({ message }) => {
|
|
163
232
|
binarySpinner.text = message
|
|
164
233
|
})
|
|
165
|
-
binarySpinner.succeed(
|
|
234
|
+
binarySpinner.succeed(
|
|
235
|
+
`${dbEngine.displayName} ${version} binaries downloaded`,
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Check if container name already exists and prompt for new name if needed
|
|
240
|
+
while (await containerManager.exists(containerName)) {
|
|
241
|
+
console.log(
|
|
242
|
+
chalk.yellow(` Container "${containerName}" already exists.`),
|
|
243
|
+
)
|
|
244
|
+
containerName = await promptContainerName()
|
|
166
245
|
}
|
|
167
246
|
|
|
168
247
|
// Create container
|
|
@@ -170,7 +249,7 @@ export const createCommand = new Command('create')
|
|
|
170
249
|
createSpinnerInstance.start()
|
|
171
250
|
|
|
172
251
|
await containerManager.create(containerName, {
|
|
173
|
-
engine: dbEngine.name,
|
|
252
|
+
engine: dbEngine.name as EngineName,
|
|
174
253
|
version,
|
|
175
254
|
port,
|
|
176
255
|
database,
|
|
@@ -183,28 +262,68 @@ export const createCommand = new Command('create')
|
|
|
183
262
|
initSpinner.start()
|
|
184
263
|
|
|
185
264
|
await dbEngine.initDataDir(containerName, version, {
|
|
186
|
-
superuser:
|
|
265
|
+
superuser: engineDefaults.superuser,
|
|
187
266
|
})
|
|
188
267
|
|
|
189
268
|
initSpinner.succeed('Database cluster initialized')
|
|
190
269
|
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
270
|
+
// Determine if we should start the container
|
|
271
|
+
// If --from is specified, we must start to restore
|
|
272
|
+
// If --no-start is specified, don't start
|
|
273
|
+
// Otherwise, ask the user
|
|
274
|
+
let shouldStart = false
|
|
275
|
+
if (restoreLocation) {
|
|
276
|
+
// Must start to restore data
|
|
277
|
+
shouldStart = true
|
|
278
|
+
} else if (options.start === false) {
|
|
279
|
+
// User explicitly requested no start
|
|
280
|
+
shouldStart = false
|
|
281
|
+
} else {
|
|
282
|
+
// Ask the user
|
|
283
|
+
console.log()
|
|
284
|
+
shouldStart = await promptConfirm(
|
|
285
|
+
`Start ${containerName} now?`,
|
|
286
|
+
true,
|
|
287
|
+
)
|
|
288
|
+
}
|
|
195
289
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
290
|
+
// Get container config for starting and restoration
|
|
291
|
+
const config = await containerManager.getConfig(containerName)
|
|
292
|
+
|
|
293
|
+
// Start container if requested
|
|
294
|
+
if (shouldStart && config) {
|
|
295
|
+
// Check port availability before starting
|
|
296
|
+
const portAvailable = await portManager.isPortAvailable(config.port)
|
|
297
|
+
if (!portAvailable) {
|
|
298
|
+
// Find a new available port
|
|
299
|
+
const { port: newPort } = await portManager.findAvailablePort({
|
|
300
|
+
portRange: engineDefaults.portRange,
|
|
201
301
|
})
|
|
302
|
+
console.log(
|
|
303
|
+
chalk.yellow(
|
|
304
|
+
` ⚠ Port ${config.port} is in use, switching to port ${newPort}`,
|
|
305
|
+
),
|
|
306
|
+
)
|
|
307
|
+
config.port = newPort
|
|
308
|
+
port = newPort
|
|
309
|
+
await containerManager.updateConfig(containerName, { port: newPort })
|
|
202
310
|
}
|
|
203
311
|
|
|
204
|
-
startSpinner
|
|
312
|
+
const startSpinner = createSpinner(
|
|
313
|
+
`Starting ${dbEngine.displayName}...`,
|
|
314
|
+
)
|
|
315
|
+
startSpinner.start()
|
|
316
|
+
|
|
317
|
+
await dbEngine.start(config)
|
|
318
|
+
await containerManager.updateConfig(containerName, {
|
|
319
|
+
status: 'running',
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
startSpinner.succeed(`${dbEngine.displayName} started`)
|
|
205
323
|
|
|
206
|
-
// Create the user's database (if different from
|
|
207
|
-
|
|
324
|
+
// Create the user's database (if different from default)
|
|
325
|
+
const defaultDb = engineDefaults.superuser // postgres or root
|
|
326
|
+
if (database !== defaultDb) {
|
|
208
327
|
const dbSpinner = createSpinner(
|
|
209
328
|
`Creating database "${database}"...`,
|
|
210
329
|
)
|
|
@@ -214,16 +333,23 @@ export const createCommand = new Command('create')
|
|
|
214
333
|
|
|
215
334
|
dbSpinner.succeed(`Database "${database}" created`)
|
|
216
335
|
}
|
|
336
|
+
}
|
|
217
337
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
338
|
+
// Handle --from restore if specified (only if started)
|
|
339
|
+
if (restoreLocation && restoreType && config && shouldStart) {
|
|
340
|
+
let backupPath = ''
|
|
221
341
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
342
|
+
if (restoreType === 'connection') {
|
|
343
|
+
// Create dump from remote database
|
|
344
|
+
const timestamp = Date.now()
|
|
345
|
+
tempDumpPath = join(tmpdir(), `spindb-dump-${timestamp}.dump`)
|
|
226
346
|
|
|
347
|
+
let dumpSuccess = false
|
|
348
|
+
let attempts = 0
|
|
349
|
+
const maxAttempts = 2 // Allow one retry after installing deps
|
|
350
|
+
|
|
351
|
+
while (!dumpSuccess && attempts < maxAttempts) {
|
|
352
|
+
attempts++
|
|
227
353
|
const dumpSpinner = createSpinner(
|
|
228
354
|
'Creating dump from remote database...',
|
|
229
355
|
)
|
|
@@ -236,6 +362,7 @@ export const createCommand = new Command('create')
|
|
|
236
362
|
)
|
|
237
363
|
dumpSpinner.succeed('Dump created from remote database')
|
|
238
364
|
backupPath = tempDumpPath
|
|
365
|
+
dumpSuccess = true
|
|
239
366
|
} catch (err) {
|
|
240
367
|
const e = err as Error
|
|
241
368
|
dumpSpinner.fail('Failed to create dump')
|
|
@@ -245,8 +372,12 @@ export const createCommand = new Command('create')
|
|
|
245
372
|
e.message.includes('pg_dump not found') ||
|
|
246
373
|
e.message.includes('ENOENT')
|
|
247
374
|
) {
|
|
248
|
-
await promptInstallDependencies('pg_dump')
|
|
249
|
-
|
|
375
|
+
const installed = await promptInstallDependencies('pg_dump')
|
|
376
|
+
if (!installed) {
|
|
377
|
+
process.exit(1)
|
|
378
|
+
}
|
|
379
|
+
// Loop will retry
|
|
380
|
+
continue
|
|
250
381
|
}
|
|
251
382
|
|
|
252
383
|
console.log()
|
|
@@ -254,59 +385,66 @@ export const createCommand = new Command('create')
|
|
|
254
385
|
console.log(chalk.gray(` ${e.message}`))
|
|
255
386
|
process.exit(1)
|
|
256
387
|
}
|
|
257
|
-
} else {
|
|
258
|
-
backupPath = restoreLocation
|
|
259
388
|
}
|
|
260
389
|
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
390
|
+
// Safety check - should never reach here without backupPath set
|
|
391
|
+
if (!dumpSuccess) {
|
|
392
|
+
console.error(error('Failed to create dump after retries'))
|
|
393
|
+
process.exit(1)
|
|
394
|
+
}
|
|
395
|
+
} else {
|
|
396
|
+
backupPath = restoreLocation
|
|
397
|
+
}
|
|
264
398
|
|
|
265
|
-
|
|
266
|
-
|
|
399
|
+
// Detect backup format
|
|
400
|
+
const detectSpinner = createSpinner('Detecting backup format...')
|
|
401
|
+
detectSpinner.start()
|
|
267
402
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
restoreSpinner.start()
|
|
403
|
+
const format = await dbEngine.detectBackupFormat(backupPath)
|
|
404
|
+
detectSpinner.succeed(`Detected: ${format.description}`)
|
|
271
405
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
})
|
|
406
|
+
// Restore backup
|
|
407
|
+
const restoreSpinner = createSpinner('Restoring backup...')
|
|
408
|
+
restoreSpinner.start()
|
|
276
409
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
410
|
+
const result = await dbEngine.restore(config, backupPath, {
|
|
411
|
+
database,
|
|
412
|
+
createDatabase: false, // Already created above
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
if (result.code === 0 || !result.stderr) {
|
|
416
|
+
restoreSpinner.succeed('Backup restored successfully')
|
|
417
|
+
} else {
|
|
418
|
+
restoreSpinner.warn('Restore completed with warnings')
|
|
419
|
+
if (result.stderr) {
|
|
420
|
+
console.log(chalk.yellow('\n Warnings:'))
|
|
421
|
+
const lines = result.stderr.split('\n').slice(0, 5)
|
|
422
|
+
lines.forEach((line) => {
|
|
423
|
+
if (line.trim()) {
|
|
424
|
+
console.log(chalk.gray(` ${line}`))
|
|
291
425
|
}
|
|
426
|
+
})
|
|
427
|
+
if (result.stderr.split('\n').length > 5) {
|
|
428
|
+
console.log(chalk.gray(' ...'))
|
|
292
429
|
}
|
|
293
430
|
}
|
|
294
431
|
}
|
|
295
432
|
}
|
|
296
433
|
|
|
297
434
|
// Show success message
|
|
298
|
-
const
|
|
299
|
-
if (
|
|
300
|
-
const connectionString = dbEngine.getConnectionString(
|
|
435
|
+
const finalConfig = await containerManager.getConfig(containerName)
|
|
436
|
+
if (finalConfig) {
|
|
437
|
+
const connectionString = dbEngine.getConnectionString(finalConfig)
|
|
301
438
|
|
|
302
439
|
console.log()
|
|
303
|
-
console.log(connectionBox(containerName, connectionString, port))
|
|
440
|
+
console.log(connectionBox(containerName, connectionString, finalConfig.port))
|
|
304
441
|
console.log()
|
|
305
|
-
console.log(chalk.gray(' Connect with:'))
|
|
306
|
-
console.log(chalk.cyan(` spindb connect ${containerName}`))
|
|
307
442
|
|
|
308
|
-
|
|
309
|
-
|
|
443
|
+
if (shouldStart) {
|
|
444
|
+
console.log(chalk.gray(' Connect with:'))
|
|
445
|
+
console.log(chalk.cyan(` spindb connect ${containerName}`))
|
|
446
|
+
|
|
447
|
+
// Copy connection string to clipboard
|
|
310
448
|
try {
|
|
311
449
|
const cmd = platform() === 'darwin' ? 'pbcopy' : 'xclip'
|
|
312
450
|
const args =
|
|
@@ -332,6 +470,9 @@ export const createCommand = new Command('create')
|
|
|
332
470
|
} catch {
|
|
333
471
|
// Ignore clipboard errors
|
|
334
472
|
}
|
|
473
|
+
} else {
|
|
474
|
+
console.log(chalk.gray(' Start the container:'))
|
|
475
|
+
console.log(chalk.cyan(` spindb start ${containerName}`))
|
|
335
476
|
}
|
|
336
477
|
|
|
337
478
|
console.log()
|
|
@@ -339,18 +480,30 @@ export const createCommand = new Command('create')
|
|
|
339
480
|
} catch (err) {
|
|
340
481
|
const e = err as Error
|
|
341
482
|
|
|
342
|
-
// Check if this is a missing tool error
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
483
|
+
// Check if this is a missing tool error (PostgreSQL or MySQL)
|
|
484
|
+
const missingToolPatterns = [
|
|
485
|
+
// PostgreSQL
|
|
486
|
+
'pg_restore not found',
|
|
487
|
+
'psql not found',
|
|
488
|
+
'pg_dump not found',
|
|
489
|
+
// MySQL
|
|
490
|
+
'mysql not found',
|
|
491
|
+
'mysqldump not found',
|
|
492
|
+
'mysqld not found',
|
|
493
|
+
]
|
|
494
|
+
|
|
495
|
+
const matchingPattern = missingToolPatterns.find((p) =>
|
|
496
|
+
e.message.includes(p),
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
if (matchingPattern) {
|
|
500
|
+
const missingTool = matchingPattern.replace(' not found', '')
|
|
501
|
+
const installed = await promptInstallDependencies(missingTool)
|
|
502
|
+
if (installed) {
|
|
503
|
+
console.log(
|
|
504
|
+
chalk.yellow(' Please re-run your command to continue.'),
|
|
505
|
+
)
|
|
506
|
+
}
|
|
354
507
|
process.exit(1)
|
|
355
508
|
}
|
|
356
509
|
|
package/cli/commands/delete.ts
CHANGED
|
@@ -57,7 +57,9 @@ export const deleteCommand = new Command('delete')
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// Check if running
|
|
60
|
-
const running = await processManager.isRunning(containerName
|
|
60
|
+
const running = await processManager.isRunning(containerName, {
|
|
61
|
+
engine: config.engine,
|
|
62
|
+
})
|
|
61
63
|
if (running) {
|
|
62
64
|
if (options.force) {
|
|
63
65
|
// Stop the container first
|
package/cli/commands/list.ts
CHANGED
|
@@ -3,6 +3,14 @@ import chalk from 'chalk'
|
|
|
3
3
|
import { containerManager } from '../../core/container-manager'
|
|
4
4
|
import { info, error } from '../ui/theme'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Engine icons for display
|
|
8
|
+
*/
|
|
9
|
+
const engineIcons: Record<string, string> = {
|
|
10
|
+
postgresql: '🐘',
|
|
11
|
+
mysql: '🐬',
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
export const listCommand = new Command('list')
|
|
7
15
|
.alias('ls')
|
|
8
16
|
.description('List all containers')
|
|
@@ -26,12 +34,12 @@ export const listCommand = new Command('list')
|
|
|
26
34
|
console.log(
|
|
27
35
|
chalk.gray(' ') +
|
|
28
36
|
chalk.bold.white('NAME'.padEnd(20)) +
|
|
29
|
-
chalk.bold.white('ENGINE'.padEnd(
|
|
37
|
+
chalk.bold.white('ENGINE'.padEnd(15)) +
|
|
30
38
|
chalk.bold.white('VERSION'.padEnd(10)) +
|
|
31
39
|
chalk.bold.white('PORT'.padEnd(8)) +
|
|
32
40
|
chalk.bold.white('STATUS'),
|
|
33
41
|
)
|
|
34
|
-
console.log(chalk.gray(' ' + '─'.repeat(
|
|
42
|
+
console.log(chalk.gray(' ' + '─'.repeat(63)))
|
|
35
43
|
|
|
36
44
|
// Table rows
|
|
37
45
|
for (const container of containers) {
|
|
@@ -40,10 +48,13 @@ export const listCommand = new Command('list')
|
|
|
40
48
|
? chalk.green('● running')
|
|
41
49
|
: chalk.gray('○ stopped')
|
|
42
50
|
|
|
51
|
+
const engineIcon = engineIcons[container.engine] || '🗄️'
|
|
52
|
+
const engineDisplay = `${engineIcon} ${container.engine}`
|
|
53
|
+
|
|
43
54
|
console.log(
|
|
44
55
|
chalk.gray(' ') +
|
|
45
56
|
chalk.cyan(container.name.padEnd(20)) +
|
|
46
|
-
chalk.white(
|
|
57
|
+
chalk.white(engineDisplay.padEnd(14)) +
|
|
47
58
|
chalk.yellow(container.version.padEnd(10)) +
|
|
48
59
|
chalk.green(String(container.port).padEnd(8)) +
|
|
49
60
|
statusDisplay,
|