spindb 0.7.0 → 0.7.5
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 +421 -294
- package/cli/commands/backup.ts +1 -30
- package/cli/commands/clone.ts +0 -6
- package/cli/commands/config.ts +7 -1
- package/cli/commands/connect.ts +1 -16
- package/cli/commands/create.ts +4 -55
- package/cli/commands/delete.ts +0 -6
- package/cli/commands/edit.ts +9 -25
- package/cli/commands/engines.ts +10 -188
- package/cli/commands/info.ts +7 -34
- package/cli/commands/list.ts +2 -18
- package/cli/commands/logs.ts +118 -0
- package/cli/commands/menu/backup-handlers.ts +749 -0
- package/cli/commands/menu/container-handlers.ts +825 -0
- package/cli/commands/menu/engine-handlers.ts +362 -0
- package/cli/commands/menu/index.ts +179 -0
- package/cli/commands/menu/shared.ts +26 -0
- package/cli/commands/menu/shell-handlers.ts +320 -0
- package/cli/commands/menu/sql-handlers.ts +194 -0
- package/cli/commands/menu/update-handlers.ts +94 -0
- package/cli/commands/restore.ts +2 -28
- package/cli/commands/run.ts +139 -0
- package/cli/commands/start.ts +2 -10
- package/cli/commands/stop.ts +0 -5
- package/cli/commands/url.ts +18 -13
- package/cli/constants.ts +10 -0
- package/cli/helpers.ts +152 -0
- package/cli/index.ts +5 -2
- package/cli/ui/prompts.ts +3 -11
- package/core/dependency-manager.ts +0 -163
- package/core/error-handler.ts +0 -26
- package/core/platform-service.ts +60 -40
- package/core/start-with-retry.ts +3 -28
- package/core/transaction-manager.ts +0 -8
- package/engines/base-engine.ts +10 -0
- package/engines/mysql/binary-detection.ts +1 -1
- package/engines/mysql/index.ts +78 -2
- package/engines/postgresql/index.ts +49 -0
- package/package.json +1 -1
- package/cli/commands/menu.ts +0 -2670
package/cli/commands/backup.ts
CHANGED
|
@@ -15,18 +15,11 @@ import { createSpinner } from '../ui/spinner'
|
|
|
15
15
|
import { success, error, warning, formatBytes } from '../ui/theme'
|
|
16
16
|
import { getMissingDependencies } from '../../core/dependency-manager'
|
|
17
17
|
|
|
18
|
-
/**
|
|
19
|
-
* Generate a timestamp string for backup filenames
|
|
20
|
-
* Format: YYYY-MM-DDTHHMMSS (ISO 8601 without colons for filesystem compatibility)
|
|
21
|
-
*/
|
|
22
18
|
function generateTimestamp(): string {
|
|
23
19
|
const now = new Date()
|
|
24
20
|
return now.toISOString().replace(/:/g, '').split('.')[0]
|
|
25
21
|
}
|
|
26
22
|
|
|
27
|
-
/**
|
|
28
|
-
* Generate default backup filename
|
|
29
|
-
*/
|
|
30
23
|
function generateDefaultFilename(
|
|
31
24
|
containerName: string,
|
|
32
25
|
database: string,
|
|
@@ -35,9 +28,6 @@ function generateDefaultFilename(
|
|
|
35
28
|
return `${containerName}-${database}-backup-${timestamp}`
|
|
36
29
|
}
|
|
37
30
|
|
|
38
|
-
/**
|
|
39
|
-
* Get file extension for backup format
|
|
40
|
-
*/
|
|
41
31
|
function getExtension(format: 'sql' | 'dump', engine: string): string {
|
|
42
32
|
if (format === 'sql') {
|
|
43
33
|
return '.sql'
|
|
@@ -73,7 +63,6 @@ export const backupCommand = new Command('backup')
|
|
|
73
63
|
try {
|
|
74
64
|
let containerName = containerArg
|
|
75
65
|
|
|
76
|
-
// Interactive selection if no container provided
|
|
77
66
|
if (!containerName) {
|
|
78
67
|
const containers = await containerManager.list()
|
|
79
68
|
const running = containers.filter((c) => c.status === 'running')
|
|
@@ -101,7 +90,6 @@ export const backupCommand = new Command('backup')
|
|
|
101
90
|
containerName = selected
|
|
102
91
|
}
|
|
103
92
|
|
|
104
|
-
// Get container config
|
|
105
93
|
const config = await containerManager.getConfig(containerName)
|
|
106
94
|
if (!config) {
|
|
107
95
|
console.error(error(`Container "${containerName}" not found`))
|
|
@@ -110,7 +98,6 @@ export const backupCommand = new Command('backup')
|
|
|
110
98
|
|
|
111
99
|
const { engine: engineName } = config
|
|
112
100
|
|
|
113
|
-
// Check if running
|
|
114
101
|
const running = await processManager.isRunning(containerName, {
|
|
115
102
|
engine: engineName,
|
|
116
103
|
})
|
|
@@ -123,10 +110,8 @@ export const backupCommand = new Command('backup')
|
|
|
123
110
|
process.exit(1)
|
|
124
111
|
}
|
|
125
112
|
|
|
126
|
-
// Get engine
|
|
127
113
|
const engine = getEngine(engineName)
|
|
128
114
|
|
|
129
|
-
// Check for required client tools
|
|
130
115
|
const depsSpinner = createSpinner('Checking required tools...')
|
|
131
116
|
depsSpinner.start()
|
|
132
117
|
|
|
@@ -136,7 +121,6 @@ export const backupCommand = new Command('backup')
|
|
|
136
121
|
`Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
137
122
|
)
|
|
138
123
|
|
|
139
|
-
// Offer to install
|
|
140
124
|
const installed = await promptInstallDependencies(
|
|
141
125
|
missingDeps[0].binary,
|
|
142
126
|
config.engine,
|
|
@@ -146,7 +130,6 @@ export const backupCommand = new Command('backup')
|
|
|
146
130
|
process.exit(1)
|
|
147
131
|
}
|
|
148
132
|
|
|
149
|
-
// Verify installation worked
|
|
150
133
|
missingDeps = await getMissingDependencies(config.engine)
|
|
151
134
|
if (missingDeps.length > 0) {
|
|
152
135
|
console.error(
|
|
@@ -163,27 +146,22 @@ export const backupCommand = new Command('backup')
|
|
|
163
146
|
depsSpinner.succeed('Required tools available')
|
|
164
147
|
}
|
|
165
148
|
|
|
166
|
-
// Determine which database to backup
|
|
167
149
|
let databaseName = options.database
|
|
168
150
|
|
|
169
151
|
if (!databaseName) {
|
|
170
|
-
// Get list of databases in container
|
|
171
152
|
const databases = config.databases || [config.database]
|
|
172
153
|
|
|
173
154
|
if (databases.length > 1) {
|
|
174
|
-
// Interactive mode: prompt for database selection
|
|
175
155
|
databaseName = await promptDatabaseSelect(
|
|
176
156
|
databases,
|
|
177
157
|
'Select database to backup:',
|
|
178
158
|
)
|
|
179
159
|
} else {
|
|
180
|
-
// Single database: use it
|
|
181
160
|
databaseName = databases[0]
|
|
182
161
|
}
|
|
183
162
|
}
|
|
184
163
|
|
|
185
|
-
|
|
186
|
-
let format: 'sql' | 'dump' = 'sql' // Default to SQL
|
|
164
|
+
let format: 'sql' | 'dump' = 'sql'
|
|
187
165
|
|
|
188
166
|
if (options.sql) {
|
|
189
167
|
format = 'sql'
|
|
@@ -196,28 +174,23 @@ export const backupCommand = new Command('backup')
|
|
|
196
174
|
}
|
|
197
175
|
format = options.format as 'sql' | 'dump'
|
|
198
176
|
} else if (!containerArg) {
|
|
199
|
-
// Interactive mode: prompt for format
|
|
200
177
|
format = await promptBackupFormat(engineName)
|
|
201
178
|
}
|
|
202
179
|
|
|
203
|
-
// Determine filename
|
|
204
180
|
const defaultFilename = generateDefaultFilename(
|
|
205
181
|
containerName,
|
|
206
182
|
databaseName,
|
|
207
183
|
)
|
|
208
184
|
let filename = options.name || defaultFilename
|
|
209
185
|
|
|
210
|
-
// In interactive mode with no name provided, optionally prompt for custom name
|
|
211
186
|
if (!containerArg && !options.name) {
|
|
212
187
|
filename = await promptBackupFilename(defaultFilename)
|
|
213
188
|
}
|
|
214
189
|
|
|
215
|
-
// Build full output path
|
|
216
190
|
const extension = getExtension(format, engineName)
|
|
217
191
|
const outputDir = options.output || process.cwd()
|
|
218
192
|
const outputPath = join(outputDir, `${filename}${extension}`)
|
|
219
193
|
|
|
220
|
-
// Create backup
|
|
221
194
|
const backupSpinner = createSpinner(
|
|
222
195
|
`Creating ${format === 'sql' ? 'SQL' : 'dump'} backup of "${databaseName}"...`,
|
|
223
196
|
)
|
|
@@ -230,7 +203,6 @@ export const backupCommand = new Command('backup')
|
|
|
230
203
|
|
|
231
204
|
backupSpinner.succeed('Backup created successfully')
|
|
232
205
|
|
|
233
|
-
// Show result
|
|
234
206
|
console.log()
|
|
235
207
|
console.log(success('Backup complete'))
|
|
236
208
|
console.log()
|
|
@@ -244,7 +216,6 @@ export const backupCommand = new Command('backup')
|
|
|
244
216
|
} catch (err) {
|
|
245
217
|
const e = err as Error
|
|
246
218
|
|
|
247
|
-
// Check if this is a missing tool error
|
|
248
219
|
const missingToolPatterns = ['pg_dump not found', 'mysqldump not found']
|
|
249
220
|
|
|
250
221
|
const matchingPattern = missingToolPatterns.find((p) =>
|
package/cli/commands/clone.ts
CHANGED
|
@@ -16,7 +16,6 @@ export const cloneCommand = new Command('clone')
|
|
|
16
16
|
let sourceName = source
|
|
17
17
|
let targetName = target
|
|
18
18
|
|
|
19
|
-
// Interactive selection if no source provided
|
|
20
19
|
if (!sourceName) {
|
|
21
20
|
const containers = await containerManager.list()
|
|
22
21
|
const stopped = containers.filter((c) => c.status !== 'running')
|
|
@@ -50,14 +49,12 @@ export const cloneCommand = new Command('clone')
|
|
|
50
49
|
sourceName = selected
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
// Check source exists
|
|
54
52
|
const sourceConfig = await containerManager.getConfig(sourceName)
|
|
55
53
|
if (!sourceConfig) {
|
|
56
54
|
console.error(error(`Container "${sourceName}" not found`))
|
|
57
55
|
process.exit(1)
|
|
58
56
|
}
|
|
59
57
|
|
|
60
|
-
// Check source is stopped
|
|
61
58
|
const running = await processManager.isRunning(sourceName, {
|
|
62
59
|
engine: sourceConfig.engine,
|
|
63
60
|
})
|
|
@@ -70,12 +67,10 @@ export const cloneCommand = new Command('clone')
|
|
|
70
67
|
process.exit(1)
|
|
71
68
|
}
|
|
72
69
|
|
|
73
|
-
// Get target name
|
|
74
70
|
if (!targetName) {
|
|
75
71
|
targetName = await promptContainerName(`${sourceName}-copy`)
|
|
76
72
|
}
|
|
77
73
|
|
|
78
|
-
// Clone the container
|
|
79
74
|
const cloneSpinner = createSpinner(
|
|
80
75
|
`Cloning ${sourceName} to ${targetName}...`,
|
|
81
76
|
)
|
|
@@ -85,7 +80,6 @@ export const cloneCommand = new Command('clone')
|
|
|
85
80
|
|
|
86
81
|
cloneSpinner.succeed(`Cloned "${sourceName}" to "${targetName}"`)
|
|
87
82
|
|
|
88
|
-
// Get engine for connection string
|
|
89
83
|
const engine = getEngine(newConfig.engine)
|
|
90
84
|
const connectionString = engine.getConnectionString(newConfig)
|
|
91
85
|
|
package/cli/commands/config.ts
CHANGED
|
@@ -45,10 +45,16 @@ export const configCommand = new Command('config')
|
|
|
45
45
|
.addCommand(
|
|
46
46
|
new Command('show')
|
|
47
47
|
.description('Show current configuration')
|
|
48
|
-
.
|
|
48
|
+
.option('--json', 'Output as JSON')
|
|
49
|
+
.action(async (options: { json?: boolean }) => {
|
|
49
50
|
try {
|
|
50
51
|
const config = await configManager.getConfig()
|
|
51
52
|
|
|
53
|
+
if (options.json) {
|
|
54
|
+
console.log(JSON.stringify(config, null, 2))
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
52
58
|
console.log()
|
|
53
59
|
console.log(header('SpinDB Configuration'))
|
|
54
60
|
console.log()
|
package/cli/commands/connect.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { promptContainerSelect } from '../ui/prompts'
|
|
|
21
21
|
import { error, warning, info, success } from '../ui/theme'
|
|
22
22
|
|
|
23
23
|
export const connectCommand = new Command('connect')
|
|
24
|
+
.alias('shell')
|
|
24
25
|
.description('Connect to a container with database client')
|
|
25
26
|
.argument('[name]', 'Container name')
|
|
26
27
|
.option('-d, --database <name>', 'Database name')
|
|
@@ -52,7 +53,6 @@ export const connectCommand = new Command('connect')
|
|
|
52
53
|
try {
|
|
53
54
|
let containerName = name
|
|
54
55
|
|
|
55
|
-
// Interactive selection if no name provided
|
|
56
56
|
if (!containerName) {
|
|
57
57
|
const containers = await containerManager.list()
|
|
58
58
|
const running = containers.filter((c) => c.status === 'running')
|
|
@@ -80,7 +80,6 @@ export const connectCommand = new Command('connect')
|
|
|
80
80
|
containerName = selected
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
// Get container config
|
|
84
83
|
const config = await containerManager.getConfig(containerName)
|
|
85
84
|
if (!config) {
|
|
86
85
|
console.error(error(`Container "${containerName}" not found`))
|
|
@@ -90,11 +89,9 @@ export const connectCommand = new Command('connect')
|
|
|
90
89
|
const { engine: engineName } = config
|
|
91
90
|
const engineDefaults = getEngineDefaults(engineName)
|
|
92
91
|
|
|
93
|
-
// Default database: container's database or superuser
|
|
94
92
|
const database =
|
|
95
93
|
options.database ?? config.database ?? engineDefaults.superuser
|
|
96
94
|
|
|
97
|
-
// Check if running
|
|
98
95
|
const running = await processManager.isRunning(containerName, {
|
|
99
96
|
engine: engineName,
|
|
100
97
|
})
|
|
@@ -107,18 +104,15 @@ export const connectCommand = new Command('connect')
|
|
|
107
104
|
process.exit(1)
|
|
108
105
|
}
|
|
109
106
|
|
|
110
|
-
// Get engine
|
|
111
107
|
const engine = getEngine(engineName)
|
|
112
108
|
const connectionString = engine.getConnectionString(config, database)
|
|
113
109
|
|
|
114
|
-
// Handle --tui and --install-tui flags (usql)
|
|
115
110
|
const useUsql = options.tui || options.installTui
|
|
116
111
|
if (useUsql) {
|
|
117
112
|
const usqlInstalled = await isUsqlInstalled()
|
|
118
113
|
|
|
119
114
|
if (!usqlInstalled) {
|
|
120
115
|
if (options.installTui) {
|
|
121
|
-
// Try to install usql
|
|
122
116
|
console.log(
|
|
123
117
|
info('Installing usql for enhanced shell experience...'),
|
|
124
118
|
)
|
|
@@ -149,7 +143,6 @@ export const connectCommand = new Command('connect')
|
|
|
149
143
|
process.exit(1)
|
|
150
144
|
}
|
|
151
145
|
} else {
|
|
152
|
-
// --tui flag but usql not installed
|
|
153
146
|
console.error(error('usql is not installed'))
|
|
154
147
|
console.log()
|
|
155
148
|
console.log(
|
|
@@ -166,7 +159,6 @@ export const connectCommand = new Command('connect')
|
|
|
166
159
|
}
|
|
167
160
|
}
|
|
168
161
|
|
|
169
|
-
// Handle --pgcli and --install-pgcli flags
|
|
170
162
|
const usePgcli = options.pgcli || options.installPgcli
|
|
171
163
|
if (usePgcli) {
|
|
172
164
|
if (engineName !== 'postgresql') {
|
|
@@ -227,7 +219,6 @@ export const connectCommand = new Command('connect')
|
|
|
227
219
|
}
|
|
228
220
|
}
|
|
229
221
|
|
|
230
|
-
// Handle --mycli and --install-mycli flags
|
|
231
222
|
const useMycli = options.mycli || options.installMycli
|
|
232
223
|
if (useMycli) {
|
|
233
224
|
if (engineName !== 'mysql') {
|
|
@@ -287,16 +278,13 @@ export const connectCommand = new Command('connect')
|
|
|
287
278
|
console.log(info(`Connecting to ${containerName}:${database}...`))
|
|
288
279
|
console.log()
|
|
289
280
|
|
|
290
|
-
// Build client command based on engine and shell preference
|
|
291
281
|
let clientCmd: string
|
|
292
282
|
let clientArgs: string[]
|
|
293
283
|
|
|
294
284
|
if (usePgcli) {
|
|
295
|
-
// pgcli accepts connection strings
|
|
296
285
|
clientCmd = 'pgcli'
|
|
297
286
|
clientArgs = [connectionString]
|
|
298
287
|
} else if (useMycli) {
|
|
299
|
-
// mycli: mycli -h host -P port -u user database
|
|
300
288
|
clientCmd = 'mycli'
|
|
301
289
|
clientArgs = [
|
|
302
290
|
'-h',
|
|
@@ -308,11 +296,9 @@ export const connectCommand = new Command('connect')
|
|
|
308
296
|
database,
|
|
309
297
|
]
|
|
310
298
|
} else if (useUsql) {
|
|
311
|
-
// usql accepts connection strings directly for both PostgreSQL and MySQL
|
|
312
299
|
clientCmd = 'usql'
|
|
313
300
|
clientArgs = [connectionString]
|
|
314
301
|
} else if (engineName === 'mysql') {
|
|
315
|
-
// MySQL: mysql -h 127.0.0.1 -P port -u root database
|
|
316
302
|
clientCmd = 'mysql'
|
|
317
303
|
clientArgs = [
|
|
318
304
|
'-h',
|
|
@@ -324,7 +310,6 @@ export const connectCommand = new Command('connect')
|
|
|
324
310
|
database,
|
|
325
311
|
]
|
|
326
312
|
} else {
|
|
327
|
-
// PostgreSQL: psql connection_string
|
|
328
313
|
clientCmd = 'psql'
|
|
329
314
|
clientArgs = [connectionString]
|
|
330
315
|
}
|
package/cli/commands/create.ts
CHANGED
|
@@ -22,15 +22,10 @@ import { startWithRetry } from '../../core/start-with-retry'
|
|
|
22
22
|
import { TransactionManager } from '../../core/transaction-manager'
|
|
23
23
|
import { Engine } from '../../types'
|
|
24
24
|
|
|
25
|
-
/**
|
|
26
|
-
* Detect if a location string is a connection string or a file path
|
|
27
|
-
* Also infers engine from connection string scheme
|
|
28
|
-
*/
|
|
29
25
|
function detectLocationType(location: string): {
|
|
30
26
|
type: 'connection' | 'file' | 'not_found'
|
|
31
27
|
inferredEngine?: Engine
|
|
32
28
|
} {
|
|
33
|
-
// Check for PostgreSQL connection string
|
|
34
29
|
if (
|
|
35
30
|
location.startsWith('postgresql://') ||
|
|
36
31
|
location.startsWith('postgres://')
|
|
@@ -38,12 +33,10 @@ function detectLocationType(location: string): {
|
|
|
38
33
|
return { type: 'connection', inferredEngine: Engine.PostgreSQL }
|
|
39
34
|
}
|
|
40
35
|
|
|
41
|
-
// Check for MySQL connection string
|
|
42
36
|
if (location.startsWith('mysql://')) {
|
|
43
37
|
return { type: 'connection', inferredEngine: Engine.MySQL }
|
|
44
38
|
}
|
|
45
39
|
|
|
46
|
-
// Check if file exists
|
|
47
40
|
if (existsSync(location)) {
|
|
48
41
|
return { type: 'file' }
|
|
49
42
|
}
|
|
@@ -83,7 +76,6 @@ export const createCommand = new Command('create')
|
|
|
83
76
|
let version = options.version
|
|
84
77
|
let database = options.database
|
|
85
78
|
|
|
86
|
-
// Validate --from location if provided (before prompts so we can infer engine)
|
|
87
79
|
let restoreLocation: string | null = null
|
|
88
80
|
let restoreType: 'connection' | 'file' | null = null
|
|
89
81
|
|
|
@@ -103,7 +95,6 @@ export const createCommand = new Command('create')
|
|
|
103
95
|
restoreLocation = options.from
|
|
104
96
|
restoreType = locationInfo.type
|
|
105
97
|
|
|
106
|
-
// Infer engine from connection string if not explicitly set
|
|
107
98
|
if (!options.engine && locationInfo.inferredEngine) {
|
|
108
99
|
engine = locationInfo.inferredEngine
|
|
109
100
|
console.log(
|
|
@@ -113,7 +104,6 @@ export const createCommand = new Command('create')
|
|
|
113
104
|
)
|
|
114
105
|
}
|
|
115
106
|
|
|
116
|
-
// If using --from, we must start the container
|
|
117
107
|
if (options.start === false) {
|
|
118
108
|
console.error(
|
|
119
109
|
error(
|
|
@@ -124,15 +114,12 @@ export const createCommand = new Command('create')
|
|
|
124
114
|
}
|
|
125
115
|
}
|
|
126
116
|
|
|
127
|
-
// Get engine defaults for port range and default version
|
|
128
117
|
const engineDefaults = getEngineDefaults(engine)
|
|
129
118
|
|
|
130
|
-
// Set version to engine default if not specified
|
|
131
119
|
if (!version) {
|
|
132
120
|
version = engineDefaults.defaultVersion
|
|
133
121
|
}
|
|
134
122
|
|
|
135
|
-
// Interactive mode if no name provided
|
|
136
123
|
if (!containerName) {
|
|
137
124
|
const answers = await promptCreateOptions()
|
|
138
125
|
containerName = answers.name
|
|
@@ -141,16 +128,13 @@ export const createCommand = new Command('create')
|
|
|
141
128
|
database = answers.database
|
|
142
129
|
}
|
|
143
130
|
|
|
144
|
-
// Default database name to container name if not specified
|
|
145
131
|
database = database ?? containerName
|
|
146
132
|
|
|
147
133
|
console.log(header('Creating Database Container'))
|
|
148
134
|
console.log()
|
|
149
135
|
|
|
150
|
-
// Get the engine
|
|
151
136
|
const dbEngine = getEngine(engine)
|
|
152
137
|
|
|
153
|
-
// Check for required client tools BEFORE creating anything
|
|
154
138
|
const depsSpinner = createSpinner('Checking required tools...')
|
|
155
139
|
depsSpinner.start()
|
|
156
140
|
|
|
@@ -160,7 +144,6 @@ export const createCommand = new Command('create')
|
|
|
160
144
|
`Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
161
145
|
)
|
|
162
146
|
|
|
163
|
-
// Offer to install
|
|
164
147
|
const installed = await promptInstallDependencies(
|
|
165
148
|
missingDeps[0].binary,
|
|
166
149
|
engine,
|
|
@@ -170,7 +153,6 @@ export const createCommand = new Command('create')
|
|
|
170
153
|
process.exit(1)
|
|
171
154
|
}
|
|
172
155
|
|
|
173
|
-
// Verify installation worked
|
|
174
156
|
missingDeps = await getMissingDependencies(engine)
|
|
175
157
|
if (missingDeps.length > 0) {
|
|
176
158
|
console.error(
|
|
@@ -187,7 +169,6 @@ export const createCommand = new Command('create')
|
|
|
187
169
|
depsSpinner.succeed('Required tools available')
|
|
188
170
|
}
|
|
189
171
|
|
|
190
|
-
// Find available port
|
|
191
172
|
const portSpinner = createSpinner('Finding available port...')
|
|
192
173
|
portSpinner.start()
|
|
193
174
|
|
|
@@ -216,7 +197,6 @@ export const createCommand = new Command('create')
|
|
|
216
197
|
}
|
|
217
198
|
}
|
|
218
199
|
|
|
219
|
-
// Ensure binaries are available
|
|
220
200
|
const binarySpinner = createSpinner(
|
|
221
201
|
`Checking ${dbEngine.displayName} ${version} binaries...`,
|
|
222
202
|
)
|
|
@@ -237,7 +217,6 @@ export const createCommand = new Command('create')
|
|
|
237
217
|
)
|
|
238
218
|
}
|
|
239
219
|
|
|
240
|
-
// Check if container name already exists and prompt for new name if needed
|
|
241
220
|
while (await containerManager.exists(containerName)) {
|
|
242
221
|
console.log(
|
|
243
222
|
chalk.yellow(` Container "${containerName}" already exists.`),
|
|
@@ -245,10 +224,8 @@ export const createCommand = new Command('create')
|
|
|
245
224
|
containerName = await promptContainerName()
|
|
246
225
|
}
|
|
247
226
|
|
|
248
|
-
// Create transaction manager for rollback support
|
|
249
227
|
const tx = new TransactionManager()
|
|
250
228
|
|
|
251
|
-
// Create container
|
|
252
229
|
const createSpinnerInstance = createSpinner('Creating container...')
|
|
253
230
|
createSpinnerInstance.start()
|
|
254
231
|
|
|
@@ -260,7 +237,6 @@ export const createCommand = new Command('create')
|
|
|
260
237
|
database,
|
|
261
238
|
})
|
|
262
239
|
|
|
263
|
-
// Register rollback action for container deletion
|
|
264
240
|
tx.addRollback({
|
|
265
241
|
description: `Delete container "${containerName}"`,
|
|
266
242
|
execute: async () => {
|
|
@@ -274,7 +250,6 @@ export const createCommand = new Command('create')
|
|
|
274
250
|
throw err
|
|
275
251
|
}
|
|
276
252
|
|
|
277
|
-
// Initialize database cluster
|
|
278
253
|
const initSpinner = createSpinner('Initializing database cluster...')
|
|
279
254
|
initSpinner.start()
|
|
280
255
|
|
|
@@ -282,7 +257,6 @@ export const createCommand = new Command('create')
|
|
|
282
257
|
await dbEngine.initDataDir(containerName, version, {
|
|
283
258
|
superuser: engineDefaults.superuser,
|
|
284
259
|
})
|
|
285
|
-
// Note: initDataDir is covered by the container delete rollback
|
|
286
260
|
initSpinner.succeed('Database cluster initialized')
|
|
287
261
|
} catch (err) {
|
|
288
262
|
initSpinner.fail('Failed to initialize database cluster')
|
|
@@ -290,27 +264,19 @@ export const createCommand = new Command('create')
|
|
|
290
264
|
throw err
|
|
291
265
|
}
|
|
292
266
|
|
|
293
|
-
//
|
|
294
|
-
// If --from is specified, we must start to restore
|
|
295
|
-
// If --no-start is specified, don't start
|
|
296
|
-
// Otherwise, ask the user
|
|
267
|
+
// --from requires start, --no-start skips, otherwise ask user
|
|
297
268
|
let shouldStart = false
|
|
298
269
|
if (restoreLocation) {
|
|
299
|
-
// Must start to restore data
|
|
300
270
|
shouldStart = true
|
|
301
271
|
} else if (options.start === false) {
|
|
302
|
-
// User explicitly requested no start
|
|
303
272
|
shouldStart = false
|
|
304
273
|
} else {
|
|
305
|
-
// Ask the user
|
|
306
274
|
console.log()
|
|
307
275
|
shouldStart = await promptConfirm(`Start ${containerName} now?`, true)
|
|
308
276
|
}
|
|
309
277
|
|
|
310
|
-
// Get container config for starting and restoration
|
|
311
278
|
const config = await containerManager.getConfig(containerName)
|
|
312
279
|
|
|
313
|
-
// Start container if requested
|
|
314
280
|
if (shouldStart && config) {
|
|
315
281
|
const startSpinner = createSpinner(
|
|
316
282
|
`Starting ${dbEngine.displayName}...`,
|
|
@@ -318,7 +284,6 @@ export const createCommand = new Command('create')
|
|
|
318
284
|
startSpinner.start()
|
|
319
285
|
|
|
320
286
|
try {
|
|
321
|
-
// Use startWithRetry to handle port race conditions
|
|
322
287
|
const result = await startWithRetry({
|
|
323
288
|
engine: dbEngine,
|
|
324
289
|
config,
|
|
@@ -337,7 +302,6 @@ export const createCommand = new Command('create')
|
|
|
337
302
|
throw new Error('Failed to start container')
|
|
338
303
|
}
|
|
339
304
|
|
|
340
|
-
// Register rollback action for stopping the container
|
|
341
305
|
tx.addRollback({
|
|
342
306
|
description: `Stop container "${containerName}"`,
|
|
343
307
|
execute: async () => {
|
|
@@ -370,8 +334,7 @@ export const createCommand = new Command('create')
|
|
|
370
334
|
throw err
|
|
371
335
|
}
|
|
372
336
|
|
|
373
|
-
|
|
374
|
-
const defaultDb = engineDefaults.superuser // postgres or root
|
|
337
|
+
const defaultDb = engineDefaults.superuser
|
|
375
338
|
if (database !== defaultDb) {
|
|
376
339
|
const dbSpinner = createSpinner(
|
|
377
340
|
`Creating database "${database}"...`,
|
|
@@ -389,18 +352,16 @@ export const createCommand = new Command('create')
|
|
|
389
352
|
}
|
|
390
353
|
}
|
|
391
354
|
|
|
392
|
-
// Handle --from restore if specified (only if started)
|
|
393
355
|
if (restoreLocation && restoreType && config && shouldStart) {
|
|
394
356
|
let backupPath = ''
|
|
395
357
|
|
|
396
358
|
if (restoreType === 'connection') {
|
|
397
|
-
// Create dump from remote database
|
|
398
359
|
const timestamp = Date.now()
|
|
399
360
|
tempDumpPath = join(tmpdir(), `spindb-dump-${timestamp}.dump`)
|
|
400
361
|
|
|
401
362
|
let dumpSuccess = false
|
|
402
363
|
let attempts = 0
|
|
403
|
-
const maxAttempts = 2
|
|
364
|
+
const maxAttempts = 2
|
|
404
365
|
|
|
405
366
|
while (!dumpSuccess && attempts < maxAttempts) {
|
|
406
367
|
attempts++
|
|
@@ -421,7 +382,6 @@ export const createCommand = new Command('create')
|
|
|
421
382
|
const e = err as Error
|
|
422
383
|
dumpSpinner.fail('Failed to create dump')
|
|
423
384
|
|
|
424
|
-
// Check if this is a missing tool error
|
|
425
385
|
if (
|
|
426
386
|
e.message.includes('pg_dump not found') ||
|
|
427
387
|
e.message.includes('ENOENT')
|
|
@@ -430,7 +390,6 @@ export const createCommand = new Command('create')
|
|
|
430
390
|
if (!installed) {
|
|
431
391
|
process.exit(1)
|
|
432
392
|
}
|
|
433
|
-
// Loop will retry
|
|
434
393
|
continue
|
|
435
394
|
}
|
|
436
395
|
|
|
@@ -441,7 +400,6 @@ export const createCommand = new Command('create')
|
|
|
441
400
|
}
|
|
442
401
|
}
|
|
443
402
|
|
|
444
|
-
// Safety check - should never reach here without backupPath set
|
|
445
403
|
if (!dumpSuccess) {
|
|
446
404
|
console.error(error('Failed to create dump after retries'))
|
|
447
405
|
process.exit(1)
|
|
@@ -450,20 +408,18 @@ export const createCommand = new Command('create')
|
|
|
450
408
|
backupPath = restoreLocation
|
|
451
409
|
}
|
|
452
410
|
|
|
453
|
-
// Detect backup format
|
|
454
411
|
const detectSpinner = createSpinner('Detecting backup format...')
|
|
455
412
|
detectSpinner.start()
|
|
456
413
|
|
|
457
414
|
const format = await dbEngine.detectBackupFormat(backupPath)
|
|
458
415
|
detectSpinner.succeed(`Detected: ${format.description}`)
|
|
459
416
|
|
|
460
|
-
// Restore backup
|
|
461
417
|
const restoreSpinner = createSpinner('Restoring backup...')
|
|
462
418
|
restoreSpinner.start()
|
|
463
419
|
|
|
464
420
|
const result = await dbEngine.restore(config, backupPath, {
|
|
465
421
|
database,
|
|
466
|
-
createDatabase: false,
|
|
422
|
+
createDatabase: false,
|
|
467
423
|
})
|
|
468
424
|
|
|
469
425
|
if (result.code === 0 || !result.stderr) {
|
|
@@ -485,10 +441,8 @@ export const createCommand = new Command('create')
|
|
|
485
441
|
}
|
|
486
442
|
}
|
|
487
443
|
|
|
488
|
-
// Commit the transaction - all operations succeeded
|
|
489
444
|
tx.commit()
|
|
490
445
|
|
|
491
|
-
// Show success message
|
|
492
446
|
const finalConfig = await containerManager.getConfig(containerName)
|
|
493
447
|
if (finalConfig) {
|
|
494
448
|
const connectionString = dbEngine.getConnectionString(finalConfig)
|
|
@@ -503,7 +457,6 @@ export const createCommand = new Command('create')
|
|
|
503
457
|
console.log(chalk.gray(' Connect with:'))
|
|
504
458
|
console.log(chalk.cyan(` spindb connect ${containerName}`))
|
|
505
459
|
|
|
506
|
-
// Copy connection string to clipboard
|
|
507
460
|
const copied =
|
|
508
461
|
await platformService.copyToClipboard(connectionString)
|
|
509
462
|
if (copied) {
|
|
@@ -519,13 +472,10 @@ export const createCommand = new Command('create')
|
|
|
519
472
|
} catch (err) {
|
|
520
473
|
const e = err as Error
|
|
521
474
|
|
|
522
|
-
// Check if this is a missing tool error (PostgreSQL or MySQL)
|
|
523
475
|
const missingToolPatterns = [
|
|
524
|
-
// PostgreSQL
|
|
525
476
|
'pg_restore not found',
|
|
526
477
|
'psql not found',
|
|
527
478
|
'pg_dump not found',
|
|
528
|
-
// MySQL
|
|
529
479
|
'mysql not found',
|
|
530
480
|
'mysqldump not found',
|
|
531
481
|
'mysqld not found',
|
|
@@ -549,7 +499,6 @@ export const createCommand = new Command('create')
|
|
|
549
499
|
console.error(error(e.message))
|
|
550
500
|
process.exit(1)
|
|
551
501
|
} finally {
|
|
552
|
-
// Clean up temp file if we created one
|
|
553
502
|
if (tempDumpPath) {
|
|
554
503
|
try {
|
|
555
504
|
await rm(tempDumpPath, { force: true })
|
package/cli/commands/delete.ts
CHANGED
|
@@ -20,7 +20,6 @@ export const deleteCommand = new Command('delete')
|
|
|
20
20
|
try {
|
|
21
21
|
let containerName = name
|
|
22
22
|
|
|
23
|
-
// Interactive selection if no name provided
|
|
24
23
|
if (!containerName) {
|
|
25
24
|
const containers = await containerManager.list()
|
|
26
25
|
|
|
@@ -37,14 +36,12 @@ export const deleteCommand = new Command('delete')
|
|
|
37
36
|
containerName = selected
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
// Get container config
|
|
41
39
|
const config = await containerManager.getConfig(containerName)
|
|
42
40
|
if (!config) {
|
|
43
41
|
console.error(error(`Container "${containerName}" not found`))
|
|
44
42
|
process.exit(1)
|
|
45
43
|
}
|
|
46
44
|
|
|
47
|
-
// Confirm deletion
|
|
48
45
|
if (!options.yes) {
|
|
49
46
|
const confirmed = await promptConfirm(
|
|
50
47
|
`Are you sure you want to delete "${containerName}"? This cannot be undone.`,
|
|
@@ -56,13 +53,11 @@ export const deleteCommand = new Command('delete')
|
|
|
56
53
|
}
|
|
57
54
|
}
|
|
58
55
|
|
|
59
|
-
// Check if running
|
|
60
56
|
const running = await processManager.isRunning(containerName, {
|
|
61
57
|
engine: config.engine,
|
|
62
58
|
})
|
|
63
59
|
if (running) {
|
|
64
60
|
if (options.force) {
|
|
65
|
-
// Stop the container first
|
|
66
61
|
const stopSpinner = createSpinner(`Stopping ${containerName}...`)
|
|
67
62
|
stopSpinner.start()
|
|
68
63
|
|
|
@@ -80,7 +75,6 @@ export const deleteCommand = new Command('delete')
|
|
|
80
75
|
}
|
|
81
76
|
}
|
|
82
77
|
|
|
83
|
-
// Delete the container
|
|
84
78
|
const deleteSpinner = createSpinner(`Deleting ${containerName}...`)
|
|
85
79
|
deleteSpinner.start()
|
|
86
80
|
|