spindb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +20 -0
- package/.env.example +1 -0
- package/.prettierignore +4 -0
- package/.prettierrc +6 -0
- package/CLAUDE.md +162 -0
- package/README.md +204 -0
- package/TODO.md +66 -0
- package/bin/cli.js +7 -0
- package/eslint.config.js +18 -0
- package/package.json +52 -0
- package/seeds/mysql/sample-db.sql +22 -0
- package/seeds/postgres/sample-db.sql +27 -0
- package/src/bin/cli.ts +8 -0
- package/src/cli/commands/clone.ts +101 -0
- package/src/cli/commands/config.ts +215 -0
- package/src/cli/commands/connect.ts +106 -0
- package/src/cli/commands/create.ts +148 -0
- package/src/cli/commands/delete.ts +94 -0
- package/src/cli/commands/list.ts +69 -0
- package/src/cli/commands/menu.ts +675 -0
- package/src/cli/commands/restore.ts +161 -0
- package/src/cli/commands/start.ts +95 -0
- package/src/cli/commands/stop.ts +91 -0
- package/src/cli/index.ts +38 -0
- package/src/cli/ui/prompts.ts +197 -0
- package/src/cli/ui/spinner.ts +94 -0
- package/src/cli/ui/theme.ts +113 -0
- package/src/config/defaults.ts +49 -0
- package/src/config/paths.ts +53 -0
- package/src/core/binary-manager.ts +239 -0
- package/src/core/config-manager.ts +259 -0
- package/src/core/container-manager.ts +234 -0
- package/src/core/port-manager.ts +84 -0
- package/src/core/process-manager.ts +353 -0
- package/src/engines/base-engine.ts +103 -0
- package/src/engines/index.ts +46 -0
- package/src/engines/postgresql/binary-urls.ts +52 -0
- package/src/engines/postgresql/index.ts +298 -0
- package/src/engines/postgresql/restore.ts +173 -0
- package/src/types/index.ts +97 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { containerManager } from '@/core/container-manager'
|
|
4
|
+
import { processManager } from '@/core/process-manager'
|
|
5
|
+
import { getEngine } from '@/engines'
|
|
6
|
+
import { promptContainerSelect, promptContainerName } from '@/cli/ui/prompts'
|
|
7
|
+
import { createSpinner } from '@/cli/ui/spinner'
|
|
8
|
+
import { error, warning, connectionBox } from '@/cli/ui/theme'
|
|
9
|
+
|
|
10
|
+
export const cloneCommand = new Command('clone')
|
|
11
|
+
.description('Clone a container with all its data')
|
|
12
|
+
.argument('[source]', 'Source container name')
|
|
13
|
+
.argument('[target]', 'Target container name')
|
|
14
|
+
.action(async (source: string | undefined, target: string | undefined) => {
|
|
15
|
+
try {
|
|
16
|
+
let sourceName = source
|
|
17
|
+
let targetName = target
|
|
18
|
+
|
|
19
|
+
// Interactive selection if no source provided
|
|
20
|
+
if (!sourceName) {
|
|
21
|
+
const containers = await containerManager.list()
|
|
22
|
+
const stopped = containers.filter((c) => c.status !== 'running')
|
|
23
|
+
|
|
24
|
+
if (containers.length === 0) {
|
|
25
|
+
console.log(
|
|
26
|
+
warning('No containers found. Create one with: spindb create'),
|
|
27
|
+
)
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (stopped.length === 0) {
|
|
32
|
+
console.log(
|
|
33
|
+
warning(
|
|
34
|
+
'All containers are running. Stop a container first to clone it.',
|
|
35
|
+
),
|
|
36
|
+
)
|
|
37
|
+
console.log(
|
|
38
|
+
chalk.gray(
|
|
39
|
+
' Cloning requires the source container to be stopped.',
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const selected = await promptContainerSelect(
|
|
46
|
+
stopped,
|
|
47
|
+
'Select container to clone:',
|
|
48
|
+
)
|
|
49
|
+
if (!selected) return
|
|
50
|
+
sourceName = selected
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check source exists
|
|
54
|
+
const sourceConfig = await containerManager.getConfig(sourceName)
|
|
55
|
+
if (!sourceConfig) {
|
|
56
|
+
console.error(error(`Container "${sourceName}" not found`))
|
|
57
|
+
process.exit(1)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check source is stopped
|
|
61
|
+
const running = await processManager.isRunning(sourceName)
|
|
62
|
+
if (running) {
|
|
63
|
+
console.error(
|
|
64
|
+
error(
|
|
65
|
+
`Container "${sourceName}" is running. Stop it first to clone.`,
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
process.exit(1)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Get target name
|
|
72
|
+
if (!targetName) {
|
|
73
|
+
targetName = await promptContainerName(`${sourceName}-copy`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Clone the container
|
|
77
|
+
const cloneSpinner = createSpinner(
|
|
78
|
+
`Cloning ${sourceName} to ${targetName}...`,
|
|
79
|
+
)
|
|
80
|
+
cloneSpinner.start()
|
|
81
|
+
|
|
82
|
+
const newConfig = await containerManager.clone(sourceName, targetName)
|
|
83
|
+
|
|
84
|
+
cloneSpinner.succeed(`Cloned "${sourceName}" to "${targetName}"`)
|
|
85
|
+
|
|
86
|
+
// Get engine for connection string
|
|
87
|
+
const engine = getEngine(newConfig.engine)
|
|
88
|
+
const connectionString = engine.getConnectionString(newConfig)
|
|
89
|
+
|
|
90
|
+
console.log()
|
|
91
|
+
console.log(connectionBox(targetName, connectionString, newConfig.port))
|
|
92
|
+
console.log()
|
|
93
|
+
console.log(chalk.gray(' Start the cloned container:'))
|
|
94
|
+
console.log(chalk.cyan(` spindb start ${targetName}`))
|
|
95
|
+
console.log()
|
|
96
|
+
} catch (err) {
|
|
97
|
+
const e = err as Error
|
|
98
|
+
console.error(error(e.message))
|
|
99
|
+
process.exit(1)
|
|
100
|
+
}
|
|
101
|
+
})
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { existsSync } from 'fs'
|
|
4
|
+
import { configManager } from '@/core/config-manager'
|
|
5
|
+
import { error, success, header } from '@/cli/ui/theme'
|
|
6
|
+
import { createSpinner } from '@/cli/ui/spinner'
|
|
7
|
+
import type { BinaryTool } from '@/types'
|
|
8
|
+
|
|
9
|
+
const VALID_TOOLS: BinaryTool[] = [
|
|
10
|
+
'psql',
|
|
11
|
+
'pg_dump',
|
|
12
|
+
'pg_restore',
|
|
13
|
+
'pg_basebackup',
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
export const configCommand = new Command('config')
|
|
17
|
+
.description('Manage spindb configuration')
|
|
18
|
+
.addCommand(
|
|
19
|
+
new Command('show')
|
|
20
|
+
.description('Show current configuration')
|
|
21
|
+
.action(async () => {
|
|
22
|
+
try {
|
|
23
|
+
const config = await configManager.getConfig()
|
|
24
|
+
|
|
25
|
+
console.log()
|
|
26
|
+
console.log(header('SpinDB Configuration'))
|
|
27
|
+
console.log()
|
|
28
|
+
|
|
29
|
+
console.log(chalk.bold(' Binary Paths:'))
|
|
30
|
+
console.log(chalk.gray(' ' + '─'.repeat(50)))
|
|
31
|
+
|
|
32
|
+
for (const tool of VALID_TOOLS) {
|
|
33
|
+
const binaryConfig = config.binaries[tool]
|
|
34
|
+
if (binaryConfig) {
|
|
35
|
+
const sourceLabel =
|
|
36
|
+
binaryConfig.source === 'system'
|
|
37
|
+
? chalk.blue('system')
|
|
38
|
+
: binaryConfig.source === 'custom'
|
|
39
|
+
? chalk.yellow('custom')
|
|
40
|
+
: chalk.green('bundled')
|
|
41
|
+
const versionLabel = binaryConfig.version
|
|
42
|
+
? chalk.gray(` (v${binaryConfig.version})`)
|
|
43
|
+
: ''
|
|
44
|
+
console.log(
|
|
45
|
+
` ${chalk.cyan(tool.padEnd(15))} ${binaryConfig.path}${versionLabel} [${sourceLabel}]`,
|
|
46
|
+
)
|
|
47
|
+
} else {
|
|
48
|
+
console.log(
|
|
49
|
+
` ${chalk.cyan(tool.padEnd(15))} ${chalk.gray('not configured')}`,
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log()
|
|
55
|
+
|
|
56
|
+
if (config.updatedAt) {
|
|
57
|
+
console.log(
|
|
58
|
+
chalk.gray(
|
|
59
|
+
` Last updated: ${new Date(config.updatedAt).toLocaleString()}`,
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
console.log()
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
const e = err as Error
|
|
66
|
+
console.error(error(e.message))
|
|
67
|
+
process.exit(1)
|
|
68
|
+
}
|
|
69
|
+
}),
|
|
70
|
+
)
|
|
71
|
+
.addCommand(
|
|
72
|
+
new Command('detect')
|
|
73
|
+
.description('Auto-detect PostgreSQL client tools on your system')
|
|
74
|
+
.action(async () => {
|
|
75
|
+
try {
|
|
76
|
+
console.log()
|
|
77
|
+
console.log(header('Detecting PostgreSQL Tools'))
|
|
78
|
+
console.log()
|
|
79
|
+
|
|
80
|
+
const spinner = createSpinner(
|
|
81
|
+
'Searching for PostgreSQL client tools...',
|
|
82
|
+
)
|
|
83
|
+
spinner.start()
|
|
84
|
+
|
|
85
|
+
// Clear existing configs to force re-detection
|
|
86
|
+
await configManager.clearAllBinaries()
|
|
87
|
+
|
|
88
|
+
const { found, missing } = await configManager.initialize()
|
|
89
|
+
|
|
90
|
+
spinner.succeed('Detection complete')
|
|
91
|
+
console.log()
|
|
92
|
+
|
|
93
|
+
if (found.length > 0) {
|
|
94
|
+
console.log(chalk.bold(' Found:'))
|
|
95
|
+
for (const tool of found) {
|
|
96
|
+
const config = await configManager.getBinaryConfig(tool)
|
|
97
|
+
if (config) {
|
|
98
|
+
const versionLabel = config.version
|
|
99
|
+
? chalk.gray(` (v${config.version})`)
|
|
100
|
+
: ''
|
|
101
|
+
console.log(
|
|
102
|
+
` ${chalk.green('✓')} ${chalk.cyan(tool.padEnd(15))} ${config.path}${versionLabel}`,
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
console.log()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (missing.length > 0) {
|
|
110
|
+
console.log(chalk.bold(' Not found:'))
|
|
111
|
+
for (const tool of missing) {
|
|
112
|
+
console.log(` ${chalk.red('✗')} ${chalk.cyan(tool)}`)
|
|
113
|
+
}
|
|
114
|
+
console.log()
|
|
115
|
+
console.log(chalk.gray(' Install missing tools:'))
|
|
116
|
+
console.log(
|
|
117
|
+
chalk.gray(
|
|
118
|
+
' macOS: brew install libpq && brew link --force libpq',
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
console.log(chalk.gray(' Ubuntu: apt install postgresql-client'))
|
|
122
|
+
console.log()
|
|
123
|
+
}
|
|
124
|
+
} catch (err) {
|
|
125
|
+
const e = err as Error
|
|
126
|
+
console.error(error(e.message))
|
|
127
|
+
process.exit(1)
|
|
128
|
+
}
|
|
129
|
+
}),
|
|
130
|
+
)
|
|
131
|
+
.addCommand(
|
|
132
|
+
new Command('set')
|
|
133
|
+
.description('Set a custom binary path')
|
|
134
|
+
.argument('<tool>', `Tool name (${VALID_TOOLS.join(', ')})`)
|
|
135
|
+
.argument('<path>', 'Path to the binary')
|
|
136
|
+
.action(async (tool: string, path: string) => {
|
|
137
|
+
try {
|
|
138
|
+
// Validate tool name
|
|
139
|
+
if (!VALID_TOOLS.includes(tool as BinaryTool)) {
|
|
140
|
+
console.error(error(`Invalid tool: ${tool}`))
|
|
141
|
+
console.log(chalk.gray(` Valid tools: ${VALID_TOOLS.join(', ')}`))
|
|
142
|
+
process.exit(1)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Validate path exists
|
|
146
|
+
if (!existsSync(path)) {
|
|
147
|
+
console.error(error(`File not found: ${path}`))
|
|
148
|
+
process.exit(1)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await configManager.setBinaryPath(tool as BinaryTool, path, 'custom')
|
|
152
|
+
|
|
153
|
+
const config = await configManager.getBinaryConfig(tool as BinaryTool)
|
|
154
|
+
const versionLabel = config?.version ? ` (v${config.version})` : ''
|
|
155
|
+
|
|
156
|
+
console.log(success(`Set ${tool} to: ${path}${versionLabel}`))
|
|
157
|
+
} catch (err) {
|
|
158
|
+
const e = err as Error
|
|
159
|
+
console.error(error(e.message))
|
|
160
|
+
process.exit(1)
|
|
161
|
+
}
|
|
162
|
+
}),
|
|
163
|
+
)
|
|
164
|
+
.addCommand(
|
|
165
|
+
new Command('unset')
|
|
166
|
+
.description('Remove a custom binary path')
|
|
167
|
+
.argument('<tool>', `Tool name (${VALID_TOOLS.join(', ')})`)
|
|
168
|
+
.action(async (tool: string) => {
|
|
169
|
+
try {
|
|
170
|
+
// Validate tool name
|
|
171
|
+
if (!VALID_TOOLS.includes(tool as BinaryTool)) {
|
|
172
|
+
console.error(error(`Invalid tool: ${tool}`))
|
|
173
|
+
console.log(chalk.gray(` Valid tools: ${VALID_TOOLS.join(', ')}`))
|
|
174
|
+
process.exit(1)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
await configManager.clearBinaryPath(tool as BinaryTool)
|
|
178
|
+
console.log(success(`Removed ${tool} configuration`))
|
|
179
|
+
} catch (err) {
|
|
180
|
+
const e = err as Error
|
|
181
|
+
console.error(error(e.message))
|
|
182
|
+
process.exit(1)
|
|
183
|
+
}
|
|
184
|
+
}),
|
|
185
|
+
)
|
|
186
|
+
.addCommand(
|
|
187
|
+
new Command('path')
|
|
188
|
+
.description('Show the path for a specific tool')
|
|
189
|
+
.argument('<tool>', `Tool name (${VALID_TOOLS.join(', ')})`)
|
|
190
|
+
.action(async (tool: string) => {
|
|
191
|
+
try {
|
|
192
|
+
// Validate tool name
|
|
193
|
+
if (!VALID_TOOLS.includes(tool as BinaryTool)) {
|
|
194
|
+
console.error(error(`Invalid tool: ${tool}`))
|
|
195
|
+
console.log(chalk.gray(` Valid tools: ${VALID_TOOLS.join(', ')}`))
|
|
196
|
+
process.exit(1)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const path = await configManager.getBinaryPath(tool as BinaryTool)
|
|
200
|
+
if (path) {
|
|
201
|
+
console.log(path)
|
|
202
|
+
} else {
|
|
203
|
+
console.error(error(`${tool} not found`))
|
|
204
|
+
console.log(
|
|
205
|
+
chalk.gray(` Run 'spindb config detect' to auto-detect tools`),
|
|
206
|
+
)
|
|
207
|
+
process.exit(1)
|
|
208
|
+
}
|
|
209
|
+
} catch (err) {
|
|
210
|
+
const e = err as Error
|
|
211
|
+
console.error(error(e.message))
|
|
212
|
+
process.exit(1)
|
|
213
|
+
}
|
|
214
|
+
}),
|
|
215
|
+
)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { spawn } from 'child_process'
|
|
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 { promptContainerSelect } from '@/cli/ui/prompts'
|
|
8
|
+
import { error, warning, info } from '@/cli/ui/theme'
|
|
9
|
+
|
|
10
|
+
export const connectCommand = new Command('connect')
|
|
11
|
+
.description('Connect to a container with psql')
|
|
12
|
+
.argument('[name]', 'Container name')
|
|
13
|
+
.option('-d, --database <name>', 'Database name', 'postgres')
|
|
14
|
+
.action(async (name: string | undefined, options: { database: string }) => {
|
|
15
|
+
try {
|
|
16
|
+
let containerName = name
|
|
17
|
+
|
|
18
|
+
// Interactive selection if no name provided
|
|
19
|
+
if (!containerName) {
|
|
20
|
+
const containers = await containerManager.list()
|
|
21
|
+
const running = containers.filter((c) => c.status === 'running')
|
|
22
|
+
|
|
23
|
+
if (running.length === 0) {
|
|
24
|
+
if (containers.length === 0) {
|
|
25
|
+
console.log(
|
|
26
|
+
warning('No containers found. Create one with: spindb create'),
|
|
27
|
+
)
|
|
28
|
+
} else {
|
|
29
|
+
console.log(
|
|
30
|
+
warning(
|
|
31
|
+
'No running containers. Start one first with: spindb start',
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const selected = await promptContainerSelect(
|
|
39
|
+
running,
|
|
40
|
+
'Select container to connect to:',
|
|
41
|
+
)
|
|
42
|
+
if (!selected) return
|
|
43
|
+
containerName = selected
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Get container config
|
|
47
|
+
const config = await containerManager.getConfig(containerName)
|
|
48
|
+
if (!config) {
|
|
49
|
+
console.error(error(`Container "${containerName}" not found`))
|
|
50
|
+
process.exit(1)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check if running
|
|
54
|
+
const running = await processManager.isRunning(containerName)
|
|
55
|
+
if (!running) {
|
|
56
|
+
console.error(
|
|
57
|
+
error(`Container "${containerName}" is not running. Start it first.`),
|
|
58
|
+
)
|
|
59
|
+
process.exit(1)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Get engine
|
|
63
|
+
const engine = getEngine(config.engine)
|
|
64
|
+
const connectionString = engine.getConnectionString(
|
|
65
|
+
config,
|
|
66
|
+
options.database,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
console.log(info(`Connecting to ${containerName}:${options.database}...`))
|
|
70
|
+
console.log()
|
|
71
|
+
|
|
72
|
+
// Try to use system psql (the bundled binaries don't include psql)
|
|
73
|
+
const psqlProcess = spawn('psql', [connectionString], {
|
|
74
|
+
stdio: 'inherit',
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
psqlProcess.on('error', (err: NodeJS.ErrnoException) => {
|
|
78
|
+
if (err.code === 'ENOENT') {
|
|
79
|
+
console.log(warning('psql not found on your system.'))
|
|
80
|
+
console.log()
|
|
81
|
+
console.log(
|
|
82
|
+
chalk.gray(
|
|
83
|
+
' Install PostgreSQL client tools or connect manually:',
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
console.log(chalk.cyan(` ${connectionString}`))
|
|
87
|
+
console.log()
|
|
88
|
+
console.log(chalk.gray(' On macOS with Homebrew:'))
|
|
89
|
+
console.log(
|
|
90
|
+
chalk.cyan(' brew install libpq && brew link --force libpq'),
|
|
91
|
+
)
|
|
92
|
+
console.log()
|
|
93
|
+
} else {
|
|
94
|
+
console.error(error(err.message))
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
await new Promise<void>((resolve) => {
|
|
99
|
+
psqlProcess.on('close', () => resolve())
|
|
100
|
+
})
|
|
101
|
+
} catch (err) {
|
|
102
|
+
const e = err as Error
|
|
103
|
+
console.error(error(e.message))
|
|
104
|
+
process.exit(1)
|
|
105
|
+
}
|
|
106
|
+
})
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { containerManager } from '@/core/container-manager'
|
|
4
|
+
import { portManager } from '@/core/port-manager'
|
|
5
|
+
import { getEngine } from '@/engines'
|
|
6
|
+
import { defaults } from '@/config/defaults'
|
|
7
|
+
import { promptCreateOptions } from '@/cli/ui/prompts'
|
|
8
|
+
import { createSpinner } from '@/cli/ui/spinner'
|
|
9
|
+
import { header, error, connectionBox } from '@/cli/ui/theme'
|
|
10
|
+
|
|
11
|
+
export const createCommand = new Command('create')
|
|
12
|
+
.description('Create a new database container')
|
|
13
|
+
.argument('[name]', 'Container name')
|
|
14
|
+
.option('-e, --engine <engine>', 'Database engine', defaults.engine)
|
|
15
|
+
.option(
|
|
16
|
+
'--pg-version <version>',
|
|
17
|
+
'PostgreSQL version',
|
|
18
|
+
defaults.postgresVersion,
|
|
19
|
+
)
|
|
20
|
+
.option('-p, --port <port>', 'Port number')
|
|
21
|
+
.option('--no-start', 'Do not start the container after creation')
|
|
22
|
+
.action(
|
|
23
|
+
async (
|
|
24
|
+
name: string | undefined,
|
|
25
|
+
options: {
|
|
26
|
+
engine: string
|
|
27
|
+
pgVersion: string
|
|
28
|
+
port?: string
|
|
29
|
+
start: boolean
|
|
30
|
+
},
|
|
31
|
+
) => {
|
|
32
|
+
try {
|
|
33
|
+
let containerName = name
|
|
34
|
+
let engine = options.engine
|
|
35
|
+
let version = options.pgVersion
|
|
36
|
+
|
|
37
|
+
// Interactive mode if no name provided
|
|
38
|
+
if (!containerName) {
|
|
39
|
+
const answers = await promptCreateOptions()
|
|
40
|
+
containerName = answers.name
|
|
41
|
+
engine = answers.engine
|
|
42
|
+
version = answers.version
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log(header('Creating Database Container'))
|
|
46
|
+
console.log()
|
|
47
|
+
|
|
48
|
+
// Get the engine
|
|
49
|
+
const dbEngine = getEngine(engine)
|
|
50
|
+
|
|
51
|
+
// Find available port
|
|
52
|
+
const portSpinner = createSpinner('Finding available port...')
|
|
53
|
+
portSpinner.start()
|
|
54
|
+
|
|
55
|
+
let port: number
|
|
56
|
+
if (options.port) {
|
|
57
|
+
port = parseInt(options.port, 10)
|
|
58
|
+
const available = await portManager.isPortAvailable(port)
|
|
59
|
+
if (!available) {
|
|
60
|
+
portSpinner.fail(`Port ${port} is already in use`)
|
|
61
|
+
process.exit(1)
|
|
62
|
+
}
|
|
63
|
+
portSpinner.succeed(`Using port ${port}`)
|
|
64
|
+
} else {
|
|
65
|
+
const { port: foundPort, isDefault } =
|
|
66
|
+
await portManager.findAvailablePort()
|
|
67
|
+
port = foundPort
|
|
68
|
+
if (isDefault) {
|
|
69
|
+
portSpinner.succeed(`Using default port ${port}`)
|
|
70
|
+
} else {
|
|
71
|
+
portSpinner.warn(`Default port 5432 is in use, using port ${port}`)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Ensure binaries are available
|
|
76
|
+
const binarySpinner = createSpinner(
|
|
77
|
+
`Checking PostgreSQL ${version} binaries...`,
|
|
78
|
+
)
|
|
79
|
+
binarySpinner.start()
|
|
80
|
+
|
|
81
|
+
const isInstalled = await dbEngine.isBinaryInstalled(version)
|
|
82
|
+
if (isInstalled) {
|
|
83
|
+
binarySpinner.succeed(`PostgreSQL ${version} binaries ready (cached)`)
|
|
84
|
+
} else {
|
|
85
|
+
binarySpinner.text = `Downloading PostgreSQL ${version} binaries...`
|
|
86
|
+
await dbEngine.ensureBinaries(version, ({ message }) => {
|
|
87
|
+
binarySpinner.text = message
|
|
88
|
+
})
|
|
89
|
+
binarySpinner.succeed(`PostgreSQL ${version} binaries downloaded`)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Create container
|
|
93
|
+
const createSpinnerInstance = createSpinner('Creating container...')
|
|
94
|
+
createSpinnerInstance.start()
|
|
95
|
+
|
|
96
|
+
await containerManager.create(containerName, {
|
|
97
|
+
engine: dbEngine.name,
|
|
98
|
+
version,
|
|
99
|
+
port,
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
createSpinnerInstance.succeed('Container created')
|
|
103
|
+
|
|
104
|
+
// Initialize database
|
|
105
|
+
const initSpinner = createSpinner('Initializing database...')
|
|
106
|
+
initSpinner.start()
|
|
107
|
+
|
|
108
|
+
await dbEngine.initDataDir(containerName, version, {
|
|
109
|
+
superuser: defaults.superuser,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
initSpinner.succeed('Database initialized')
|
|
113
|
+
|
|
114
|
+
// Start container if requested
|
|
115
|
+
if (options.start !== false) {
|
|
116
|
+
const startSpinner = createSpinner('Starting PostgreSQL...')
|
|
117
|
+
startSpinner.start()
|
|
118
|
+
|
|
119
|
+
const config = await containerManager.getConfig(containerName)
|
|
120
|
+
if (config) {
|
|
121
|
+
await dbEngine.start(config)
|
|
122
|
+
await containerManager.updateConfig(containerName, {
|
|
123
|
+
status: 'running',
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
startSpinner.succeed('PostgreSQL started')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Show success message
|
|
131
|
+
const config = await containerManager.getConfig(containerName)
|
|
132
|
+
if (config) {
|
|
133
|
+
const connectionString = dbEngine.getConnectionString(config)
|
|
134
|
+
|
|
135
|
+
console.log()
|
|
136
|
+
console.log(connectionBox(containerName, connectionString, port))
|
|
137
|
+
console.log()
|
|
138
|
+
console.log(chalk.gray(' Connect with:'))
|
|
139
|
+
console.log(chalk.cyan(` spindb connect ${containerName}`))
|
|
140
|
+
console.log()
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
const e = err as Error
|
|
144
|
+
console.error(error(e.message))
|
|
145
|
+
process.exit(1)
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { containerManager } from '@/core/container-manager'
|
|
3
|
+
import { processManager } from '@/core/process-manager'
|
|
4
|
+
import { getEngine } from '@/engines'
|
|
5
|
+
import { promptContainerSelect, promptConfirm } from '@/cli/ui/prompts'
|
|
6
|
+
import { createSpinner } from '@/cli/ui/spinner'
|
|
7
|
+
import { error, warning } from '@/cli/ui/theme'
|
|
8
|
+
|
|
9
|
+
export const deleteCommand = new Command('delete')
|
|
10
|
+
.alias('rm')
|
|
11
|
+
.description('Delete a container')
|
|
12
|
+
.argument('[name]', 'Container name')
|
|
13
|
+
.option('-f, --force', 'Force delete (stop if running)')
|
|
14
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
15
|
+
.action(
|
|
16
|
+
async (
|
|
17
|
+
name: string | undefined,
|
|
18
|
+
options: { force?: boolean; yes?: boolean },
|
|
19
|
+
) => {
|
|
20
|
+
try {
|
|
21
|
+
let containerName = name
|
|
22
|
+
|
|
23
|
+
// Interactive selection if no name provided
|
|
24
|
+
if (!containerName) {
|
|
25
|
+
const containers = await containerManager.list()
|
|
26
|
+
|
|
27
|
+
if (containers.length === 0) {
|
|
28
|
+
console.log(warning('No containers found'))
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const selected = await promptContainerSelect(
|
|
33
|
+
containers,
|
|
34
|
+
'Select container to delete:',
|
|
35
|
+
)
|
|
36
|
+
if (!selected) return
|
|
37
|
+
containerName = selected
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Get container config
|
|
41
|
+
const config = await containerManager.getConfig(containerName)
|
|
42
|
+
if (!config) {
|
|
43
|
+
console.error(error(`Container "${containerName}" not found`))
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Confirm deletion
|
|
48
|
+
if (!options.yes) {
|
|
49
|
+
const confirmed = await promptConfirm(
|
|
50
|
+
`Are you sure you want to delete "${containerName}"? This cannot be undone.`,
|
|
51
|
+
false,
|
|
52
|
+
)
|
|
53
|
+
if (!confirmed) {
|
|
54
|
+
console.log(warning('Deletion cancelled'))
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check if running
|
|
60
|
+
const running = await processManager.isRunning(containerName)
|
|
61
|
+
if (running) {
|
|
62
|
+
if (options.force) {
|
|
63
|
+
// Stop the container first
|
|
64
|
+
const stopSpinner = createSpinner(`Stopping ${containerName}...`)
|
|
65
|
+
stopSpinner.start()
|
|
66
|
+
|
|
67
|
+
const engine = getEngine(config.engine)
|
|
68
|
+
await engine.stop(config)
|
|
69
|
+
|
|
70
|
+
stopSpinner.succeed(`Stopped "${containerName}"`)
|
|
71
|
+
} else {
|
|
72
|
+
console.error(
|
|
73
|
+
error(
|
|
74
|
+
`Container "${containerName}" is running. Stop it first or use --force`,
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
process.exit(1)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Delete the container
|
|
82
|
+
const deleteSpinner = createSpinner(`Deleting ${containerName}...`)
|
|
83
|
+
deleteSpinner.start()
|
|
84
|
+
|
|
85
|
+
await containerManager.delete(containerName, { force: true })
|
|
86
|
+
|
|
87
|
+
deleteSpinner.succeed(`Container "${containerName}" deleted`)
|
|
88
|
+
} catch (err) {
|
|
89
|
+
const e = err as Error
|
|
90
|
+
console.error(error(e.message))
|
|
91
|
+
process.exit(1)
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
)
|