spindb 0.7.0 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,331 @@
1
+ import chalk from 'chalk'
2
+ import inquirer from 'inquirer'
3
+ import { spawn } from 'child_process'
4
+ import { containerManager } from '../../../core/container-manager'
5
+ import {
6
+ isUsqlInstalled,
7
+ isPgcliInstalled,
8
+ isMycliInstalled,
9
+ detectPackageManager,
10
+ installUsql,
11
+ installPgcli,
12
+ installMycli,
13
+ getUsqlManualInstructions,
14
+ getPgcliManualInstructions,
15
+ getMycliManualInstructions,
16
+ } from '../../../core/dependency-manager'
17
+ import { platformService } from '../../../core/platform-service'
18
+ import { getEngine } from '../../../engines'
19
+ import { createSpinner } from '../../ui/spinner'
20
+ import { error, warning, info, success } from '../../ui/theme'
21
+ import { pressEnterToContinue } from './shared'
22
+
23
+ export async function handleCopyConnectionString(
24
+ containerName: string,
25
+ ): Promise<void> {
26
+ const config = await containerManager.getConfig(containerName)
27
+ if (!config) {
28
+ console.error(error(`Container "${containerName}" not found`))
29
+ return
30
+ }
31
+
32
+ const engine = getEngine(config.engine)
33
+ const connectionString = engine.getConnectionString(config)
34
+
35
+ // Copy to clipboard using platform service
36
+ const copied = await platformService.copyToClipboard(connectionString)
37
+
38
+ console.log()
39
+ if (copied) {
40
+ console.log(success('Connection string copied to clipboard'))
41
+ console.log(chalk.gray(` ${connectionString}`))
42
+ } else {
43
+ console.log(warning('Could not copy to clipboard. Connection string:'))
44
+ console.log(chalk.cyan(` ${connectionString}`))
45
+ }
46
+ console.log()
47
+
48
+ await inquirer.prompt([
49
+ {
50
+ type: 'input',
51
+ name: 'continue',
52
+ message: chalk.gray('Press Enter to continue...'),
53
+ },
54
+ ])
55
+ }
56
+
57
+ export async function handleOpenShell(containerName: string): Promise<void> {
58
+ const config = await containerManager.getConfig(containerName)
59
+ if (!config) {
60
+ console.error(error(`Container "${containerName}" not found`))
61
+ return
62
+ }
63
+
64
+ const engine = getEngine(config.engine)
65
+ const connectionString = engine.getConnectionString(config)
66
+
67
+ // Check which enhanced shells are installed (with loading indicator)
68
+ const shellCheckSpinner = createSpinner('Checking available shells...')
69
+ shellCheckSpinner.start()
70
+
71
+ const [usqlInstalled, pgcliInstalled, mycliInstalled] = await Promise.all([
72
+ isUsqlInstalled(),
73
+ isPgcliInstalled(),
74
+ isMycliInstalled(),
75
+ ])
76
+
77
+ shellCheckSpinner.stop()
78
+ // Clear the spinner line
79
+ process.stdout.write('\x1b[1A\x1b[2K')
80
+
81
+ type ShellChoice =
82
+ | 'default'
83
+ | 'usql'
84
+ | 'install-usql'
85
+ | 'pgcli'
86
+ | 'install-pgcli'
87
+ | 'mycli'
88
+ | 'install-mycli'
89
+ | 'back'
90
+
91
+ const defaultShellName = config.engine === 'mysql' ? 'mysql' : 'psql'
92
+ const engineSpecificCli = config.engine === 'mysql' ? 'mycli' : 'pgcli'
93
+ const engineSpecificInstalled =
94
+ config.engine === 'mysql' ? mycliInstalled : pgcliInstalled
95
+
96
+ const choices: Array<{ name: string; value: ShellChoice } | inquirer.Separator> = [
97
+ {
98
+ name: `>_ Use default shell (${defaultShellName})`,
99
+ value: 'default',
100
+ },
101
+ ]
102
+
103
+ // Engine-specific enhanced CLI (pgcli for PostgreSQL, mycli for MySQL)
104
+ if (engineSpecificInstalled) {
105
+ choices.push({
106
+ name: `⚡ Use ${engineSpecificCli} (enhanced features, recommended)`,
107
+ value: config.engine === 'mysql' ? 'mycli' : 'pgcli',
108
+ })
109
+ } else {
110
+ choices.push({
111
+ name: `↓ Install ${engineSpecificCli} (enhanced features, recommended)`,
112
+ value: config.engine === 'mysql' ? 'install-mycli' : 'install-pgcli',
113
+ })
114
+ }
115
+
116
+ // usql - universal option
117
+ if (usqlInstalled) {
118
+ choices.push({
119
+ name: '⚡ Use usql (universal SQL client)',
120
+ value: 'usql',
121
+ })
122
+ } else {
123
+ choices.push({
124
+ name: '↓ Install usql (universal SQL client)',
125
+ value: 'install-usql',
126
+ })
127
+ }
128
+
129
+ choices.push(new inquirer.Separator())
130
+ choices.push({
131
+ name: `${chalk.blue('←')} Back`,
132
+ value: 'back',
133
+ })
134
+
135
+ const { shellChoice } = await inquirer.prompt<{ shellChoice: ShellChoice }>([
136
+ {
137
+ type: 'list',
138
+ name: 'shellChoice',
139
+ message: 'Select shell option:',
140
+ choices,
141
+ pageSize: 10,
142
+ },
143
+ ])
144
+
145
+ if (shellChoice === 'back') {
146
+ return
147
+ }
148
+
149
+ // Handle pgcli installation
150
+ if (shellChoice === 'install-pgcli') {
151
+ console.log()
152
+ console.log(info('Installing pgcli for enhanced PostgreSQL shell...'))
153
+ const pm = await detectPackageManager()
154
+ if (pm) {
155
+ const result = await installPgcli(pm)
156
+ if (result.success) {
157
+ console.log(success('pgcli installed successfully!'))
158
+ console.log()
159
+ await launchShell(containerName, config, connectionString, 'pgcli')
160
+ } else {
161
+ console.error(error(`Failed to install pgcli: ${result.error}`))
162
+ console.log()
163
+ console.log(chalk.gray('Manual installation:'))
164
+ for (const instruction of getPgcliManualInstructions()) {
165
+ console.log(chalk.cyan(` ${instruction}`))
166
+ }
167
+ console.log()
168
+ await pressEnterToContinue()
169
+ }
170
+ } else {
171
+ console.error(error('No supported package manager found'))
172
+ console.log()
173
+ console.log(chalk.gray('Manual installation:'))
174
+ for (const instruction of getPgcliManualInstructions()) {
175
+ console.log(chalk.cyan(` ${instruction}`))
176
+ }
177
+ console.log()
178
+ await pressEnterToContinue()
179
+ }
180
+ return
181
+ }
182
+
183
+ // Handle mycli installation
184
+ if (shellChoice === 'install-mycli') {
185
+ console.log()
186
+ console.log(info('Installing mycli for enhanced MySQL shell...'))
187
+ const pm = await detectPackageManager()
188
+ if (pm) {
189
+ const result = await installMycli(pm)
190
+ if (result.success) {
191
+ console.log(success('mycli installed successfully!'))
192
+ console.log()
193
+ await launchShell(containerName, config, connectionString, 'mycli')
194
+ } else {
195
+ console.error(error(`Failed to install mycli: ${result.error}`))
196
+ console.log()
197
+ console.log(chalk.gray('Manual installation:'))
198
+ for (const instruction of getMycliManualInstructions()) {
199
+ console.log(chalk.cyan(` ${instruction}`))
200
+ }
201
+ console.log()
202
+ await pressEnterToContinue()
203
+ }
204
+ } else {
205
+ console.error(error('No supported package manager found'))
206
+ console.log()
207
+ console.log(chalk.gray('Manual installation:'))
208
+ for (const instruction of getMycliManualInstructions()) {
209
+ console.log(chalk.cyan(` ${instruction}`))
210
+ }
211
+ console.log()
212
+ await pressEnterToContinue()
213
+ }
214
+ return
215
+ }
216
+
217
+ // Handle usql installation
218
+ if (shellChoice === 'install-usql') {
219
+ console.log()
220
+ console.log(info('Installing usql for enhanced shell experience...'))
221
+ const pm = await detectPackageManager()
222
+ if (pm) {
223
+ const result = await installUsql(pm)
224
+ if (result.success) {
225
+ console.log(success('usql installed successfully!'))
226
+ console.log()
227
+ await launchShell(containerName, config, connectionString, 'usql')
228
+ } else {
229
+ console.error(error(`Failed to install usql: ${result.error}`))
230
+ console.log()
231
+ console.log(chalk.gray('Manual installation:'))
232
+ for (const instruction of getUsqlManualInstructions()) {
233
+ console.log(chalk.cyan(` ${instruction}`))
234
+ }
235
+ console.log()
236
+ await pressEnterToContinue()
237
+ }
238
+ } else {
239
+ console.error(error('No supported package manager found'))
240
+ console.log()
241
+ console.log(chalk.gray('Manual installation:'))
242
+ for (const instruction of getUsqlManualInstructions()) {
243
+ console.log(chalk.cyan(` ${instruction}`))
244
+ }
245
+ console.log()
246
+ await pressEnterToContinue()
247
+ }
248
+ return
249
+ }
250
+
251
+ // Launch the selected shell
252
+ await launchShell(containerName, config, connectionString, shellChoice)
253
+ }
254
+
255
+ async function launchShell(
256
+ containerName: string,
257
+ config: NonNullable<Awaited<ReturnType<typeof containerManager.getConfig>>>,
258
+ connectionString: string,
259
+ shellType: 'default' | 'usql' | 'pgcli' | 'mycli',
260
+ ): Promise<void> {
261
+ console.log(info(`Connecting to ${containerName}...`))
262
+ console.log()
263
+
264
+ // Determine shell command based on engine and shell type
265
+ let shellCmd: string
266
+ let shellArgs: string[]
267
+ let installHint: string
268
+
269
+ if (shellType === 'pgcli') {
270
+ // pgcli accepts connection strings
271
+ shellCmd = 'pgcli'
272
+ shellArgs = [connectionString]
273
+ installHint = 'brew install pgcli'
274
+ } else if (shellType === 'mycli') {
275
+ // mycli: mycli -h host -P port -u user database
276
+ shellCmd = 'mycli'
277
+ shellArgs = [
278
+ '-h',
279
+ '127.0.0.1',
280
+ '-P',
281
+ String(config.port),
282
+ '-u',
283
+ 'root',
284
+ config.database,
285
+ ]
286
+ installHint = 'brew install mycli'
287
+ } else if (shellType === 'usql') {
288
+ // usql accepts connection strings directly for both PostgreSQL and MySQL
289
+ shellCmd = 'usql'
290
+ shellArgs = [connectionString]
291
+ installHint = 'brew tap xo/xo && brew install xo/xo/usql'
292
+ } else if (config.engine === 'mysql') {
293
+ shellCmd = 'mysql'
294
+ // MySQL connection: mysql -u root -h 127.0.0.1 -P port database
295
+ shellArgs = [
296
+ '-u',
297
+ 'root',
298
+ '-h',
299
+ '127.0.0.1',
300
+ '-P',
301
+ String(config.port),
302
+ config.database,
303
+ ]
304
+ installHint = 'brew install mysql-client'
305
+ } else {
306
+ // PostgreSQL (default)
307
+ shellCmd = 'psql'
308
+ shellArgs = [connectionString]
309
+ installHint = 'brew install libpq && brew link --force libpq'
310
+ }
311
+
312
+ const shellProcess = spawn(shellCmd, shellArgs, {
313
+ stdio: 'inherit',
314
+ })
315
+
316
+ shellProcess.on('error', (err: NodeJS.ErrnoException) => {
317
+ if (err.code === 'ENOENT') {
318
+ console.log(warning(`${shellCmd} not found on your system.`))
319
+ console.log()
320
+ console.log(chalk.gray(' Connect manually with:'))
321
+ console.log(chalk.cyan(` ${connectionString}`))
322
+ console.log()
323
+ console.log(chalk.gray(` Install ${shellCmd}:`))
324
+ console.log(chalk.cyan(` ${installHint}`))
325
+ }
326
+ })
327
+
328
+ await new Promise<void>((resolve) => {
329
+ shellProcess.on('close', () => resolve())
330
+ })
331
+ }
@@ -0,0 +1,197 @@
1
+ import chalk from 'chalk'
2
+ import inquirer from 'inquirer'
3
+ import { existsSync } from 'fs'
4
+ import { readFile } from 'fs/promises'
5
+ import { spawn } from 'child_process'
6
+ import { containerManager } from '../../../core/container-manager'
7
+ import { getMissingDependencies } from '../../../core/dependency-manager'
8
+ import { getEngine } from '../../../engines'
9
+ import { paths } from '../../../config/paths'
10
+ import { promptInstallDependencies, promptDatabaseSelect } from '../../ui/prompts'
11
+ import { error, warning, info, success } from '../../ui/theme'
12
+ import { pressEnterToContinue } from './shared'
13
+
14
+ export async function handleRunSql(containerName: string): Promise<void> {
15
+ const config = await containerManager.getConfig(containerName)
16
+ if (!config) {
17
+ console.error(error(`Container "${containerName}" not found`))
18
+ return
19
+ }
20
+
21
+ const engine = getEngine(config.engine)
22
+
23
+ // Check for required client tools
24
+ let missingDeps = await getMissingDependencies(config.engine)
25
+ if (missingDeps.length > 0) {
26
+ console.log(
27
+ warning(`Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`),
28
+ )
29
+
30
+ const installed = await promptInstallDependencies(
31
+ missingDeps[0].binary,
32
+ config.engine,
33
+ )
34
+
35
+ if (!installed) {
36
+ return
37
+ }
38
+
39
+ missingDeps = await getMissingDependencies(config.engine)
40
+ if (missingDeps.length > 0) {
41
+ console.log(
42
+ error(
43
+ `Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
44
+ ),
45
+ )
46
+ return
47
+ }
48
+
49
+ console.log(chalk.green(' ✓ All required tools are now available'))
50
+ console.log()
51
+ }
52
+
53
+ // Strip quotes that terminals add when drag-and-dropping files
54
+ const stripQuotes = (path: string) =>
55
+ path.replace(/^['"]|['"]$/g, '').trim()
56
+
57
+ // Prompt for file path (empty input = go back)
58
+ console.log(chalk.gray(' Drag & drop, enter path (abs or rel), or press Enter to go back'))
59
+ const { filePath: rawFilePath } = await inquirer.prompt<{
60
+ filePath: string
61
+ }>([
62
+ {
63
+ type: 'input',
64
+ name: 'filePath',
65
+ message: 'SQL file path:',
66
+ validate: (input: string) => {
67
+ if (!input) return true // Empty = go back
68
+ const cleanPath = stripQuotes(input)
69
+ if (!existsSync(cleanPath)) return 'File not found'
70
+ return true
71
+ },
72
+ },
73
+ ])
74
+
75
+ // Empty input = go back to submenu
76
+ if (!rawFilePath.trim()) {
77
+ return
78
+ }
79
+
80
+ const filePath = stripQuotes(rawFilePath)
81
+
82
+ // Select database if container has multiple
83
+ const databases = config.databases || [config.database]
84
+ let databaseName: string
85
+
86
+ if (databases.length > 1) {
87
+ databaseName = await promptDatabaseSelect(
88
+ databases,
89
+ 'Select database to run SQL against:',
90
+ )
91
+ } else {
92
+ databaseName = databases[0]
93
+ }
94
+
95
+ console.log()
96
+ console.log(info(`Running SQL file against "${databaseName}"...`))
97
+ console.log()
98
+
99
+ try {
100
+ await engine.runScript(config, {
101
+ file: filePath,
102
+ database: databaseName,
103
+ })
104
+ console.log()
105
+ console.log(success('SQL file executed successfully'))
106
+ } catch (err) {
107
+ const e = err as Error
108
+ console.log()
109
+ console.log(error(`SQL execution failed: ${e.message}`))
110
+ }
111
+
112
+ console.log()
113
+ await pressEnterToContinue()
114
+ }
115
+
116
+ /**
117
+ * View container logs with interactive options
118
+ */
119
+ export async function handleViewLogs(containerName: string): Promise<void> {
120
+ const config = await containerManager.getConfig(containerName)
121
+ if (!config) {
122
+ console.error(error(`Container "${containerName}" not found`))
123
+ return
124
+ }
125
+
126
+ const logPath = paths.getContainerLogPath(config.name, {
127
+ engine: config.engine,
128
+ })
129
+
130
+ if (!existsSync(logPath)) {
131
+ console.log(
132
+ info(
133
+ `No log file found for "${containerName}". The container may not have been started yet.`,
134
+ ),
135
+ )
136
+ await pressEnterToContinue()
137
+ return
138
+ }
139
+
140
+ const { action } = await inquirer.prompt<{ action: string }>([
141
+ {
142
+ type: 'list',
143
+ name: 'action',
144
+ message: 'How would you like to view logs?',
145
+ choices: [
146
+ { name: 'View last 50 lines', value: 'tail-50' },
147
+ { name: 'View last 100 lines', value: 'tail-100' },
148
+ { name: 'Follow logs (live)', value: 'follow' },
149
+ { name: 'Open in editor', value: 'editor' },
150
+ { name: `${chalk.blue('←')} Back`, value: 'back' },
151
+ ],
152
+ },
153
+ ])
154
+
155
+ if (action === 'back') {
156
+ return
157
+ }
158
+
159
+ if (action === 'editor') {
160
+ const editorCmd = process.env.EDITOR || 'vi'
161
+ const child = spawn(editorCmd, [logPath], { stdio: 'inherit' })
162
+ await new Promise<void>((resolve) => {
163
+ child.on('close', () => resolve())
164
+ })
165
+ return
166
+ }
167
+
168
+ if (action === 'follow') {
169
+ console.log(chalk.gray(' Press Ctrl+C to stop following logs'))
170
+ console.log()
171
+ const child = spawn('tail', ['-n', '50', '-f', logPath], {
172
+ stdio: 'inherit',
173
+ })
174
+ await new Promise<void>((resolve) => {
175
+ process.on('SIGINT', () => {
176
+ child.kill('SIGTERM')
177
+ resolve()
178
+ })
179
+ child.on('close', () => resolve())
180
+ })
181
+ return
182
+ }
183
+
184
+ // tail-50 or tail-100
185
+ const lineCount = action === 'tail-100' ? 100 : 50
186
+ const content = await readFile(logPath, 'utf-8')
187
+ if (content.trim() === '') {
188
+ console.log(info('Log file is empty'))
189
+ } else {
190
+ const lines = content.split('\n')
191
+ const nonEmptyLines =
192
+ lines[lines.length - 1] === '' ? lines.slice(0, -1) : lines
193
+ console.log(nonEmptyLines.slice(-lineCount).join('\n'))
194
+ }
195
+ console.log()
196
+ await pressEnterToContinue()
197
+ }
@@ -0,0 +1,94 @@
1
+ import chalk from 'chalk'
2
+ import inquirer from 'inquirer'
3
+ import { updateManager } from '../../../core/update-manager'
4
+ import { createSpinner } from '../../ui/spinner'
5
+ import { header, success, error, warning, info } from '../../ui/theme'
6
+ import { pressEnterToContinue } from './shared'
7
+
8
+ export async function handleCheckUpdate(): Promise<void> {
9
+ console.clear()
10
+ console.log(header('Check for Updates'))
11
+ console.log()
12
+
13
+ const spinner = createSpinner('Checking for updates...')
14
+ spinner.start()
15
+
16
+ const result = await updateManager.checkForUpdate(true)
17
+
18
+ if (!result) {
19
+ spinner.fail('Could not reach npm registry')
20
+ console.log()
21
+ console.log(info('Check your internet connection and try again.'))
22
+ console.log(chalk.gray(' Manual update: npm install -g spindb@latest'))
23
+ console.log()
24
+ await pressEnterToContinue()
25
+ return
26
+ }
27
+
28
+ if (result.updateAvailable) {
29
+ spinner.succeed('Update available')
30
+ console.log()
31
+ console.log(chalk.gray(` Current version: ${result.currentVersion}`))
32
+ console.log(
33
+ chalk.gray(` Latest version: ${chalk.green(result.latestVersion)}`),
34
+ )
35
+ console.log()
36
+
37
+ const { action } = await inquirer.prompt<{ action: string }>([
38
+ {
39
+ type: 'list',
40
+ name: 'action',
41
+ message: 'What would you like to do?',
42
+ choices: [
43
+ { name: 'Update now', value: 'update' },
44
+ { name: 'Remind me later', value: 'later' },
45
+ { name: "Don't check for updates on startup", value: 'disable' },
46
+ ],
47
+ },
48
+ ])
49
+
50
+ if (action === 'update') {
51
+ console.log()
52
+ const updateSpinner = createSpinner('Updating spindb...')
53
+ updateSpinner.start()
54
+
55
+ const updateResult = await updateManager.performUpdate()
56
+
57
+ if (updateResult.success) {
58
+ updateSpinner.succeed('Update complete')
59
+ console.log()
60
+ console.log(
61
+ success(
62
+ `Updated from ${updateResult.previousVersion} to ${updateResult.newVersion}`,
63
+ ),
64
+ )
65
+ console.log()
66
+ if (updateResult.previousVersion !== updateResult.newVersion) {
67
+ console.log(warning('Please restart spindb to use the new version.'))
68
+ console.log()
69
+ }
70
+ } else {
71
+ updateSpinner.fail('Update failed')
72
+ console.log()
73
+ console.log(error(updateResult.error || 'Unknown error'))
74
+ console.log()
75
+ console.log(info('Manual update: npm install -g spindb@latest'))
76
+ }
77
+ await pressEnterToContinue()
78
+ } else if (action === 'disable') {
79
+ await updateManager.setAutoCheckEnabled(false)
80
+ console.log()
81
+ console.log(info('Update checks disabled on startup.'))
82
+ console.log(chalk.gray(' Re-enable with: spindb config update-check on'))
83
+ console.log()
84
+ await pressEnterToContinue()
85
+ }
86
+ // 'later' just returns to menu
87
+ } else {
88
+ spinner.succeed('You are on the latest version')
89
+ console.log()
90
+ console.log(chalk.gray(` Version: ${result.currentVersion}`))
91
+ console.log()
92
+ await pressEnterToContinue()
93
+ }
94
+ }