spindb 0.5.4 → 0.6.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.
@@ -1,17 +1,44 @@
1
1
  import { Command } from 'commander'
2
2
  import chalk from 'chalk'
3
3
  import { existsSync } from 'fs'
4
- import { configManager } from '../../core/config-manager'
5
- import { error, success, header } from '../ui/theme'
4
+ import {
5
+ configManager,
6
+ POSTGRESQL_TOOLS,
7
+ MYSQL_TOOLS,
8
+ ENHANCED_SHELLS,
9
+ ALL_TOOLS,
10
+ } from '../../core/config-manager'
11
+ import { updateManager } from '../../core/update-manager'
12
+ import { error, success, header, info } from '../ui/theme'
6
13
  import { createSpinner } from '../ui/spinner'
7
14
  import type { BinaryTool } from '../../types'
8
15
 
9
- const VALID_TOOLS: BinaryTool[] = [
10
- 'psql',
11
- 'pg_dump',
12
- 'pg_restore',
13
- 'pg_basebackup',
14
- ]
16
+ /**
17
+ * Helper to display a tool's config
18
+ */
19
+ function displayToolConfig(
20
+ tool: BinaryTool,
21
+ binaryConfig: { path: string; version?: string; source: string } | undefined,
22
+ ): void {
23
+ if (binaryConfig) {
24
+ const sourceLabel =
25
+ binaryConfig.source === 'system'
26
+ ? chalk.blue('system')
27
+ : binaryConfig.source === 'custom'
28
+ ? chalk.yellow('custom')
29
+ : chalk.green('bundled')
30
+ const versionLabel = binaryConfig.version
31
+ ? chalk.gray(` (v${binaryConfig.version})`)
32
+ : ''
33
+ console.log(
34
+ ` ${chalk.cyan(tool.padEnd(15))} ${binaryConfig.path}${versionLabel} [${sourceLabel}]`,
35
+ )
36
+ } else {
37
+ console.log(
38
+ ` ${chalk.cyan(tool.padEnd(15))} ${chalk.gray('not detected')}`,
39
+ )
40
+ }
41
+ }
15
42
 
16
43
  export const configCommand = new Command('config')
17
44
  .description('Manage spindb configuration')
@@ -26,37 +53,38 @@ export const configCommand = new Command('config')
26
53
  console.log(header('SpinDB Configuration'))
27
54
  console.log()
28
55
 
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
- }
56
+ // PostgreSQL tools
57
+ console.log(chalk.bold(' 🐘 PostgreSQL Tools:'))
58
+ console.log(chalk.gray(' ' + '─'.repeat(60)))
59
+ for (const tool of POSTGRESQL_TOOLS) {
60
+ displayToolConfig(tool, config.binaries[tool])
52
61
  }
62
+ console.log()
53
63
 
64
+ // MySQL tools
65
+ console.log(chalk.bold(' 🐬 MySQL Tools:'))
66
+ console.log(chalk.gray(' ' + '─'.repeat(60)))
67
+ for (const tool of MYSQL_TOOLS) {
68
+ displayToolConfig(tool, config.binaries[tool])
69
+ }
70
+ console.log()
71
+
72
+ // Enhanced shells
73
+ console.log(chalk.bold(' ✨ Enhanced Shells (optional):'))
74
+ console.log(chalk.gray(' ' + '─'.repeat(60)))
75
+ for (const tool of ENHANCED_SHELLS) {
76
+ displayToolConfig(tool, config.binaries[tool])
77
+ }
54
78
  console.log()
55
79
 
56
80
  if (config.updatedAt) {
81
+ const isStale = await configManager.isStale()
82
+ const staleWarning = isStale
83
+ ? chalk.yellow(' (stale - run config detect to refresh)')
84
+ : ''
57
85
  console.log(
58
86
  chalk.gray(
59
- ` Last updated: ${new Date(config.updatedAt).toLocaleString()}`,
87
+ ` Last updated: ${new Date(config.updatedAt).toLocaleString()}${staleWarning}`,
60
88
  ),
61
89
  )
62
90
  console.log()
@@ -70,55 +98,108 @@ export const configCommand = new Command('config')
70
98
  )
71
99
  .addCommand(
72
100
  new Command('detect')
73
- .description('Auto-detect PostgreSQL client tools on your system')
101
+ .description('Auto-detect all database tools on your system')
74
102
  .action(async () => {
75
103
  try {
76
104
  console.log()
77
- console.log(header('Detecting PostgreSQL Tools'))
105
+ console.log(header('Detecting Database Tools'))
78
106
  console.log()
79
107
 
80
- const spinner = createSpinner(
81
- 'Searching for PostgreSQL client tools...',
82
- )
108
+ const spinner = createSpinner('Searching for database tools...')
83
109
  spinner.start()
84
110
 
85
111
  // Clear existing configs to force re-detection
86
112
  await configManager.clearAllBinaries()
87
113
 
88
- const { found, missing } = await configManager.initialize()
114
+ const result = await configManager.initialize()
89
115
 
90
116
  spinner.succeed('Detection complete')
91
117
  console.log()
92
118
 
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
- : ''
119
+ // Helper to display category results
120
+ async function displayCategory(
121
+ title: string,
122
+ icon: string,
123
+ found: BinaryTool[],
124
+ missing: BinaryTool[],
125
+ ): Promise<void> {
126
+ console.log(chalk.bold(` ${icon} ${title}:`))
127
+
128
+ if (found.length > 0) {
129
+ for (const tool of found) {
130
+ const config = await configManager.getBinaryConfig(tool)
131
+ if (config) {
132
+ const versionLabel = config.version
133
+ ? chalk.gray(` (v${config.version})`)
134
+ : ''
135
+ console.log(
136
+ ` ${chalk.green('✓')} ${chalk.cyan(tool.padEnd(15))} ${config.path}${versionLabel}`,
137
+ )
138
+ }
139
+ }
140
+ }
141
+
142
+ if (missing.length > 0) {
143
+ for (const tool of missing) {
101
144
  console.log(
102
- ` ${chalk.green('')} ${chalk.cyan(tool.padEnd(15))} ${config.path}${versionLabel}`,
145
+ ` ${chalk.gray('')} ${chalk.gray(tool.padEnd(15))} not found`,
103
146
  )
104
147
  }
105
148
  }
149
+
106
150
  console.log()
107
151
  }
108
152
 
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)}`)
153
+ await displayCategory(
154
+ 'PostgreSQL Tools',
155
+ '🐘',
156
+ result.postgresql.found,
157
+ result.postgresql.missing,
158
+ )
159
+ await displayCategory(
160
+ 'MySQL Tools',
161
+ '🐬',
162
+ result.mysql.found,
163
+ result.mysql.missing,
164
+ )
165
+ await displayCategory(
166
+ 'Enhanced Shells (optional)',
167
+ '✨',
168
+ result.enhanced.found,
169
+ result.enhanced.missing,
170
+ )
171
+
172
+ // Show install hints for missing required tools
173
+ if (
174
+ result.postgresql.missing.length > 0 ||
175
+ result.mysql.missing.length > 0
176
+ ) {
177
+ console.log(chalk.gray(' Install missing tools:'))
178
+ if (result.postgresql.missing.length > 0) {
179
+ console.log(
180
+ chalk.gray(' PostgreSQL: brew install postgresql@17'),
181
+ )
182
+ }
183
+ if (result.mysql.missing.length > 0) {
184
+ console.log(chalk.gray(' MySQL: brew install mysql'))
113
185
  }
114
186
  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'))
187
+ }
188
+
189
+ // Show enhanced shell hints
190
+ if (result.enhanced.missing.length > 0) {
191
+ console.log(chalk.gray(' Optional enhanced shells:'))
192
+ if (result.enhanced.missing.includes('pgcli')) {
193
+ console.log(chalk.gray(' pgcli: brew install pgcli'))
194
+ }
195
+ if (result.enhanced.missing.includes('mycli')) {
196
+ console.log(chalk.gray(' mycli: brew install mycli'))
197
+ }
198
+ if (result.enhanced.missing.includes('usql')) {
199
+ console.log(
200
+ chalk.gray(' usql: brew tap xo/xo && brew install usql'),
201
+ )
202
+ }
122
203
  console.log()
123
204
  }
124
205
  } catch (err) {
@@ -131,14 +212,14 @@ export const configCommand = new Command('config')
131
212
  .addCommand(
132
213
  new Command('set')
133
214
  .description('Set a custom binary path')
134
- .argument('<tool>', `Tool name (${VALID_TOOLS.join(', ')})`)
215
+ .argument('<tool>', 'Tool name (psql, mysql, pgcli, etc.)')
135
216
  .argument('<path>', 'Path to the binary')
136
217
  .action(async (tool: string, path: string) => {
137
218
  try {
138
219
  // Validate tool name
139
- if (!VALID_TOOLS.includes(tool as BinaryTool)) {
220
+ if (!ALL_TOOLS.includes(tool as BinaryTool)) {
140
221
  console.error(error(`Invalid tool: ${tool}`))
141
- console.log(chalk.gray(` Valid tools: ${VALID_TOOLS.join(', ')}`))
222
+ console.log(chalk.gray(` Valid tools: ${ALL_TOOLS.join(', ')}`))
142
223
  process.exit(1)
143
224
  }
144
225
 
@@ -164,13 +245,13 @@ export const configCommand = new Command('config')
164
245
  .addCommand(
165
246
  new Command('unset')
166
247
  .description('Remove a custom binary path')
167
- .argument('<tool>', `Tool name (${VALID_TOOLS.join(', ')})`)
248
+ .argument('<tool>', 'Tool name (psql, mysql, pgcli, etc.)')
168
249
  .action(async (tool: string) => {
169
250
  try {
170
251
  // Validate tool name
171
- if (!VALID_TOOLS.includes(tool as BinaryTool)) {
252
+ if (!ALL_TOOLS.includes(tool as BinaryTool)) {
172
253
  console.error(error(`Invalid tool: ${tool}`))
173
- console.log(chalk.gray(` Valid tools: ${VALID_TOOLS.join(', ')}`))
254
+ console.log(chalk.gray(` Valid tools: ${ALL_TOOLS.join(', ')}`))
174
255
  process.exit(1)
175
256
  }
176
257
 
@@ -186,13 +267,13 @@ export const configCommand = new Command('config')
186
267
  .addCommand(
187
268
  new Command('path')
188
269
  .description('Show the path for a specific tool')
189
- .argument('<tool>', `Tool name (${VALID_TOOLS.join(', ')})`)
270
+ .argument('<tool>', 'Tool name (psql, mysql, pgcli, etc.)')
190
271
  .action(async (tool: string) => {
191
272
  try {
192
273
  // Validate tool name
193
- if (!VALID_TOOLS.includes(tool as BinaryTool)) {
274
+ if (!ALL_TOOLS.includes(tool as BinaryTool)) {
194
275
  console.error(error(`Invalid tool: ${tool}`))
195
- console.log(chalk.gray(` Valid tools: ${VALID_TOOLS.join(', ')}`))
276
+ console.log(chalk.gray(` Valid tools: ${ALL_TOOLS.join(', ')}`))
196
277
  process.exit(1)
197
278
  }
198
279
 
@@ -213,3 +294,55 @@ export const configCommand = new Command('config')
213
294
  }
214
295
  }),
215
296
  )
297
+ .addCommand(
298
+ new Command('update-check')
299
+ .description('Enable or disable automatic update checks on startup')
300
+ .argument('[state]', 'on or off (omit to show current status)')
301
+ .action(async (state?: string) => {
302
+ try {
303
+ const cached = await updateManager.getCachedUpdateInfo()
304
+
305
+ if (!state) {
306
+ // Show current status
307
+ const status = cached.autoCheckEnabled
308
+ ? chalk.green('enabled')
309
+ : chalk.yellow('disabled')
310
+ console.log()
311
+ console.log(` Update checks on startup: ${status}`)
312
+ console.log()
313
+ console.log(chalk.gray(' Usage:'))
314
+ console.log(
315
+ chalk.gray(' spindb config update-check on # Enable'),
316
+ )
317
+ console.log(
318
+ chalk.gray(' spindb config update-check off # Disable'),
319
+ )
320
+ console.log()
321
+ return
322
+ }
323
+
324
+ if (state !== 'on' && state !== 'off') {
325
+ console.error(error('Invalid state. Use "on" or "off"'))
326
+ process.exit(1)
327
+ }
328
+
329
+ const enabled = state === 'on'
330
+ await updateManager.setAutoCheckEnabled(enabled)
331
+
332
+ if (enabled) {
333
+ console.log(success('Update checks enabled on startup'))
334
+ } else {
335
+ console.log(info('Update checks disabled on startup'))
336
+ console.log(
337
+ chalk.gray(
338
+ ' You can still manually check with: spindb version --check',
339
+ ),
340
+ )
341
+ }
342
+ } catch (err) {
343
+ const e = err as Error
344
+ console.error(error(e.message))
345
+ process.exit(1)
346
+ }
347
+ }),
348
+ )
@@ -26,9 +26,15 @@ export const connectCommand = new Command('connect')
26
26
  .option('-d, --database <name>', 'Database name')
27
27
  .option('--tui', 'Use usql for enhanced shell experience')
28
28
  .option('--install-tui', 'Install usql if not present, then connect')
29
- .option('--pgcli', 'Use pgcli for enhanced PostgreSQL shell (dropdown auto-completion)')
29
+ .option(
30
+ '--pgcli',
31
+ 'Use pgcli for enhanced PostgreSQL shell (dropdown auto-completion)',
32
+ )
30
33
  .option('--install-pgcli', 'Install pgcli if not present, then connect')
31
- .option('--mycli', 'Use mycli for enhanced MySQL shell (dropdown auto-completion)')
34
+ .option(
35
+ '--mycli',
36
+ 'Use mycli for enhanced MySQL shell (dropdown auto-completion)',
37
+ )
32
38
  .option('--install-mycli', 'Install mycli if not present, then connect')
33
39
  .action(
34
40
  async (
@@ -164,7 +170,9 @@ export const connectCommand = new Command('connect')
164
170
  const usePgcli = options.pgcli || options.installPgcli
165
171
  if (usePgcli) {
166
172
  if (engineName !== 'postgresql') {
167
- console.error(error('pgcli is only available for PostgreSQL containers'))
173
+ console.error(
174
+ error('pgcli is only available for PostgreSQL containers'),
175
+ )
168
176
  console.log(chalk.gray('For MySQL, use: spindb connect --mycli'))
169
177
  process.exit(1)
170
178
  }
@@ -173,7 +181,9 @@ export const connectCommand = new Command('connect')
173
181
 
174
182
  if (!pgcliInstalled) {
175
183
  if (options.installPgcli) {
176
- console.log(info('Installing pgcli for enhanced PostgreSQL shell...'))
184
+ console.log(
185
+ info('Installing pgcli for enhanced PostgreSQL shell...'),
186
+ )
177
187
  const pm = await detectPackageManager()
178
188
  if (pm) {
179
189
  const result = await installPgcli(pm)
@@ -181,7 +191,9 @@ export const connectCommand = new Command('connect')
181
191
  console.log(success('pgcli installed successfully!'))
182
192
  console.log()
183
193
  } else {
184
- console.error(error(`Failed to install pgcli: ${result.error}`))
194
+ console.error(
195
+ error(`Failed to install pgcli: ${result.error}`),
196
+ )
185
197
  console.log()
186
198
  console.log(chalk.gray('Manual installation:'))
187
199
  for (const instruction of getPgcliManualInstructions()) {
@@ -201,7 +213,9 @@ export const connectCommand = new Command('connect')
201
213
  } else {
202
214
  console.error(error('pgcli is not installed'))
203
215
  console.log()
204
- console.log(chalk.gray('Install pgcli for enhanced PostgreSQL shell:'))
216
+ console.log(
217
+ chalk.gray('Install pgcli for enhanced PostgreSQL shell:'),
218
+ )
205
219
  console.log(chalk.cyan(' spindb connect --install-pgcli'))
206
220
  console.log()
207
221
  console.log(chalk.gray('Or install manually:'))
@@ -218,7 +232,9 @@ export const connectCommand = new Command('connect')
218
232
  if (useMycli) {
219
233
  if (engineName !== 'mysql') {
220
234
  console.error(error('mycli is only available for MySQL containers'))
221
- console.log(chalk.gray('For PostgreSQL, use: spindb connect --pgcli'))
235
+ console.log(
236
+ chalk.gray('For PostgreSQL, use: spindb connect --pgcli'),
237
+ )
222
238
  process.exit(1)
223
239
  }
224
240
 
@@ -234,7 +250,9 @@ export const connectCommand = new Command('connect')
234
250
  console.log(success('mycli installed successfully!'))
235
251
  console.log()
236
252
  } else {
237
- console.error(error(`Failed to install mycli: ${result.error}`))
253
+ console.error(
254
+ error(`Failed to install mycli: ${result.error}`),
255
+ )
238
256
  console.log()
239
257
  console.log(chalk.gray('Manual installation:'))
240
258
  for (const instruction of getMycliManualInstructions()) {
@@ -327,7 +345,9 @@ export const connectCommand = new Command('connect')
327
345
 
328
346
  if (clientCmd === 'usql') {
329
347
  console.log(chalk.gray(' Install usql:'))
330
- console.log(chalk.cyan(' brew tap xo/xo && brew install xo/xo/usql'))
348
+ console.log(
349
+ chalk.cyan(' brew tap xo/xo && brew install xo/xo/usql'),
350
+ )
331
351
  } else if (clientCmd === 'pgcli') {
332
352
  console.log(chalk.gray(' Install pgcli:'))
333
353
  console.log(chalk.cyan(' brew install pgcli'))
@@ -10,7 +10,7 @@ import { paths } from '../../config/paths'
10
10
  import { containerManager } from '../../core/container-manager'
11
11
  import { promptConfirm } from '../ui/prompts'
12
12
  import { createSpinner } from '../ui/spinner'
13
- import { error, warning, info } from '../ui/theme'
13
+ import { error, warning, info, formatBytes } from '../ui/theme'
14
14
  import {
15
15
  getMysqldPath,
16
16
  getMysqlVersion,
@@ -185,14 +185,6 @@ function compareVersions(a: string, b: string): number {
185
185
  return 0
186
186
  }
187
187
 
188
- function formatBytes(bytes: number): string {
189
- if (bytes === 0) return '0 B'
190
- const k = 1024
191
- const sizes = ['B', 'KB', 'MB', 'GB']
192
- const i = Math.floor(Math.log(bytes) / Math.log(k))
193
- return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`
194
- }
195
-
196
188
  /**
197
189
  * Engine icons
198
190
  */
@@ -1,7 +1,9 @@
1
1
  import { Command } from 'commander'
2
2
  import chalk from 'chalk'
3
3
  import { containerManager } from '../../core/container-manager'
4
- import { info, error } from '../ui/theme'
4
+ import { getEngine } from '../../engines'
5
+ import { info, error, formatBytes } from '../ui/theme'
6
+ import type { ContainerConfig } from '../../types'
5
7
 
6
8
  /**
7
9
  * Engine icons for display
@@ -11,6 +13,23 @@ const engineIcons: Record<string, string> = {
11
13
  mysql: '🐬',
12
14
  }
13
15
 
16
+ /**
17
+ * Get database size for a container (only if running)
18
+ */
19
+ async function getContainerSize(
20
+ container: ContainerConfig,
21
+ ): Promise<number | null> {
22
+ if (container.status !== 'running') {
23
+ return null
24
+ }
25
+ try {
26
+ const engine = getEngine(container.engine)
27
+ return await engine.getDatabaseSize(container)
28
+ } catch {
29
+ return null
30
+ }
31
+ }
32
+
14
33
  export const listCommand = new Command('list')
15
34
  .alias('ls')
16
35
  .description('List all containers')
@@ -20,7 +39,14 @@ export const listCommand = new Command('list')
20
39
  const containers = await containerManager.list()
21
40
 
22
41
  if (options.json) {
23
- console.log(JSON.stringify(containers, null, 2))
42
+ // Include sizes in JSON output
43
+ const containersWithSize = await Promise.all(
44
+ containers.map(async (container) => ({
45
+ ...container,
46
+ sizeBytes: await getContainerSize(container),
47
+ })),
48
+ )
49
+ console.log(JSON.stringify(containersWithSize, null, 2))
24
50
  return
25
51
  }
26
52
 
@@ -29,6 +55,9 @@ export const listCommand = new Command('list')
29
55
  return
30
56
  }
31
57
 
58
+ // Fetch sizes for running containers in parallel
59
+ const sizes = await Promise.all(containers.map(getContainerSize))
60
+
32
61
  // Table header
33
62
  console.log()
34
63
  console.log(
@@ -37,12 +66,16 @@ export const listCommand = new Command('list')
37
66
  chalk.bold.white('ENGINE'.padEnd(15)) +
38
67
  chalk.bold.white('VERSION'.padEnd(10)) +
39
68
  chalk.bold.white('PORT'.padEnd(8)) +
69
+ chalk.bold.white('SIZE'.padEnd(10)) +
40
70
  chalk.bold.white('STATUS'),
41
71
  )
42
- console.log(chalk.gray(' ' + '─'.repeat(63)))
72
+ console.log(chalk.gray(' ' + '─'.repeat(73)))
43
73
 
44
74
  // Table rows
45
- for (const container of containers) {
75
+ for (let i = 0; i < containers.length; i++) {
76
+ const container = containers[i]
77
+ const size = sizes[i]
78
+
46
79
  const statusDisplay =
47
80
  container.status === 'running'
48
81
  ? chalk.green('● running')
@@ -51,12 +84,16 @@ export const listCommand = new Command('list')
51
84
  const engineIcon = engineIcons[container.engine] || '▣'
52
85
  const engineDisplay = `${engineIcon} ${container.engine}`
53
86
 
87
+ // Format size: show value if running, dash if stopped
88
+ const sizeDisplay = size !== null ? formatBytes(size) : chalk.gray('—')
89
+
54
90
  console.log(
55
91
  chalk.gray(' ') +
56
92
  chalk.cyan(container.name.padEnd(20)) +
57
93
  chalk.white(engineDisplay.padEnd(14)) +
58
94
  chalk.yellow(container.version.padEnd(10)) +
59
95
  chalk.green(String(container.port).padEnd(8)) +
96
+ chalk.magenta(sizeDisplay.padEnd(10)) +
60
97
  statusDisplay,
61
98
  )
62
99
  }