spindb 0.5.5 → 0.7.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/README.md +19 -3
- package/cli/commands/backup.ts +13 -7
- package/cli/commands/config.ts +57 -2
- package/cli/commands/connect.ts +29 -9
- package/cli/commands/create.ts +7 -7
- package/cli/commands/list.ts +1 -2
- package/cli/commands/menu.ts +103 -5
- package/cli/commands/self-update.ts +109 -0
- package/cli/commands/version.ts +55 -0
- package/cli/index.ts +82 -1
- package/cli/ui/prompts.ts +2 -1
- package/config/defaults.ts +5 -29
- package/core/binary-manager.ts +2 -2
- package/core/config-manager.ts +2 -12
- package/core/container-manager.ts +6 -3
- package/core/update-manager.ts +194 -0
- package/package.json +1 -1
- package/types/index.ts +13 -4
package/README.md
CHANGED
|
@@ -18,11 +18,23 @@ Spin up local PostgreSQL and MySQL databases without Docker. A lightweight alter
|
|
|
18
18
|
## Installation
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
#
|
|
21
|
+
# Install globally (recommended)
|
|
22
|
+
npm install -g spindb
|
|
23
|
+
|
|
24
|
+
# Or run directly with pnpx (no install needed)
|
|
22
25
|
pnpx spindb
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Updating
|
|
29
|
+
|
|
30
|
+
SpinDB checks for updates automatically and will notify you when a new version is available:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Update to latest version
|
|
34
|
+
spindb self-update
|
|
23
35
|
|
|
24
|
-
# Or
|
|
25
|
-
|
|
36
|
+
# Or check manually
|
|
37
|
+
spindb version --check
|
|
26
38
|
```
|
|
27
39
|
|
|
28
40
|
## Quick Start
|
|
@@ -61,6 +73,10 @@ spindb connect mydb
|
|
|
61
73
|
| `spindb config detect` | Auto-detect database tools |
|
|
62
74
|
| `spindb deps check` | Check status of client tools |
|
|
63
75
|
| `spindb deps install` | Install missing client tools |
|
|
76
|
+
| `spindb version` | Show current version |
|
|
77
|
+
| `spindb version --check` | Check for available updates |
|
|
78
|
+
| `spindb self-update` | Update to latest version |
|
|
79
|
+
| `spindb config update-check [on\|off]` | Enable/disable update notifications |
|
|
64
80
|
|
|
65
81
|
## Supported Engines
|
|
66
82
|
|
package/cli/commands/backup.ts
CHANGED
|
@@ -51,7 +51,10 @@ export const backupCommand = new Command('backup')
|
|
|
51
51
|
.argument('[container]', 'Container name')
|
|
52
52
|
.option('-d, --database <name>', 'Database to backup')
|
|
53
53
|
.option('-n, --name <name>', 'Custom backup filename (without extension)')
|
|
54
|
-
.option(
|
|
54
|
+
.option(
|
|
55
|
+
'-o, --output <path>',
|
|
56
|
+
'Output directory (defaults to current directory)',
|
|
57
|
+
)
|
|
55
58
|
.option('--format <format>', 'Output format: sql or dump')
|
|
56
59
|
.option('--sql', 'Output as plain SQL (shorthand for --format sql)')
|
|
57
60
|
.option('--dump', 'Output as dump format (shorthand for --format dump)')
|
|
@@ -198,7 +201,10 @@ export const backupCommand = new Command('backup')
|
|
|
198
201
|
}
|
|
199
202
|
|
|
200
203
|
// Determine filename
|
|
201
|
-
const defaultFilename = generateDefaultFilename(
|
|
204
|
+
const defaultFilename = generateDefaultFilename(
|
|
205
|
+
containerName,
|
|
206
|
+
databaseName,
|
|
207
|
+
)
|
|
202
208
|
let filename = options.name || defaultFilename
|
|
203
209
|
|
|
204
210
|
// In interactive mode with no name provided, optionally prompt for custom name
|
|
@@ -229,17 +235,17 @@ export const backupCommand = new Command('backup')
|
|
|
229
235
|
console.log(success('Backup complete'))
|
|
230
236
|
console.log()
|
|
231
237
|
console.log(chalk.gray(' File:'), chalk.cyan(result.path))
|
|
232
|
-
console.log(
|
|
238
|
+
console.log(
|
|
239
|
+
chalk.gray(' Size:'),
|
|
240
|
+
chalk.white(formatBytes(result.size)),
|
|
241
|
+
)
|
|
233
242
|
console.log(chalk.gray(' Format:'), chalk.white(result.format))
|
|
234
243
|
console.log()
|
|
235
244
|
} catch (err) {
|
|
236
245
|
const e = err as Error
|
|
237
246
|
|
|
238
247
|
// Check if this is a missing tool error
|
|
239
|
-
const missingToolPatterns = [
|
|
240
|
-
'pg_dump not found',
|
|
241
|
-
'mysqldump not found',
|
|
242
|
-
]
|
|
248
|
+
const missingToolPatterns = ['pg_dump not found', 'mysqldump not found']
|
|
243
249
|
|
|
244
250
|
const matchingPattern = missingToolPatterns.find((p) =>
|
|
245
251
|
e.message.includes(p),
|
package/cli/commands/config.ts
CHANGED
|
@@ -8,7 +8,8 @@ import {
|
|
|
8
8
|
ENHANCED_SHELLS,
|
|
9
9
|
ALL_TOOLS,
|
|
10
10
|
} from '../../core/config-manager'
|
|
11
|
-
import {
|
|
11
|
+
import { updateManager } from '../../core/update-manager'
|
|
12
|
+
import { error, success, header, info } from '../ui/theme'
|
|
12
13
|
import { createSpinner } from '../ui/spinner'
|
|
13
14
|
import type { BinaryTool } from '../../types'
|
|
14
15
|
|
|
@@ -78,7 +79,9 @@ export const configCommand = new Command('config')
|
|
|
78
79
|
|
|
79
80
|
if (config.updatedAt) {
|
|
80
81
|
const isStale = await configManager.isStale()
|
|
81
|
-
const staleWarning = isStale
|
|
82
|
+
const staleWarning = isStale
|
|
83
|
+
? chalk.yellow(' (stale - run config detect to refresh)')
|
|
84
|
+
: ''
|
|
82
85
|
console.log(
|
|
83
86
|
chalk.gray(
|
|
84
87
|
` Last updated: ${new Date(config.updatedAt).toLocaleString()}${staleWarning}`,
|
|
@@ -291,3 +294,55 @@ export const configCommand = new Command('config')
|
|
|
291
294
|
}
|
|
292
295
|
}),
|
|
293
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
|
+
)
|
package/cli/commands/connect.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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'))
|
package/cli/commands/create.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { getMissingDependencies } from '../../core/dependency-manager'
|
|
|
20
20
|
import { platformService } from '../../core/platform-service'
|
|
21
21
|
import { startWithRetry } from '../../core/start-with-retry'
|
|
22
22
|
import { TransactionManager } from '../../core/transaction-manager'
|
|
23
|
-
import
|
|
23
|
+
import { Engine } from '../../types'
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Detect if a location string is a connection string or a file path
|
|
@@ -28,19 +28,19 @@ import type { EngineName } from '../../types'
|
|
|
28
28
|
*/
|
|
29
29
|
function detectLocationType(location: string): {
|
|
30
30
|
type: 'connection' | 'file' | 'not_found'
|
|
31
|
-
inferredEngine?:
|
|
31
|
+
inferredEngine?: Engine
|
|
32
32
|
} {
|
|
33
33
|
// Check for PostgreSQL connection string
|
|
34
34
|
if (
|
|
35
35
|
location.startsWith('postgresql://') ||
|
|
36
36
|
location.startsWith('postgres://')
|
|
37
37
|
) {
|
|
38
|
-
return { type: 'connection', inferredEngine:
|
|
38
|
+
return { type: 'connection', inferredEngine: Engine.PostgreSQL }
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
// Check for MySQL connection string
|
|
42
42
|
if (location.startsWith('mysql://')) {
|
|
43
|
-
return { type: 'connection', inferredEngine:
|
|
43
|
+
return { type: 'connection', inferredEngine: Engine.MySQL }
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
// Check if file exists
|
|
@@ -79,7 +79,7 @@ export const createCommand = new Command('create')
|
|
|
79
79
|
|
|
80
80
|
try {
|
|
81
81
|
let containerName = name
|
|
82
|
-
let engine:
|
|
82
|
+
let engine: Engine = (options.engine as Engine) || Engine.PostgreSQL
|
|
83
83
|
let version = options.version
|
|
84
84
|
let database = options.database
|
|
85
85
|
|
|
@@ -136,7 +136,7 @@ export const createCommand = new Command('create')
|
|
|
136
136
|
if (!containerName) {
|
|
137
137
|
const answers = await promptCreateOptions()
|
|
138
138
|
containerName = answers.name
|
|
139
|
-
engine = answers.engine as
|
|
139
|
+
engine = answers.engine as Engine
|
|
140
140
|
version = answers.version
|
|
141
141
|
database = answers.database
|
|
142
142
|
}
|
|
@@ -254,7 +254,7 @@ export const createCommand = new Command('create')
|
|
|
254
254
|
|
|
255
255
|
try {
|
|
256
256
|
await containerManager.create(containerName, {
|
|
257
|
-
engine: dbEngine.name as
|
|
257
|
+
engine: dbEngine.name as Engine,
|
|
258
258
|
version,
|
|
259
259
|
port,
|
|
260
260
|
database,
|
package/cli/commands/list.ts
CHANGED
|
@@ -85,8 +85,7 @@ export const listCommand = new Command('list')
|
|
|
85
85
|
const engineDisplay = `${engineIcon} ${container.engine}`
|
|
86
86
|
|
|
87
87
|
// Format size: show value if running, dash if stopped
|
|
88
|
-
const sizeDisplay =
|
|
89
|
-
size !== null ? formatBytes(size) : chalk.gray('—')
|
|
88
|
+
const sizeDisplay = size !== null ? formatBytes(size) : chalk.gray('—')
|
|
90
89
|
|
|
91
90
|
console.log(
|
|
92
91
|
chalk.gray(' ') +
|
package/cli/commands/menu.ts
CHANGED
|
@@ -35,7 +35,7 @@ import { platformService } from '../../core/platform-service'
|
|
|
35
35
|
import { portManager } from '../../core/port-manager'
|
|
36
36
|
import { defaults } from '../../config/defaults'
|
|
37
37
|
import { getPostgresHomebrewPackage } from '../../config/engine-defaults'
|
|
38
|
-
import
|
|
38
|
+
import { Engine } from '../../types'
|
|
39
39
|
import inquirer from 'inquirer'
|
|
40
40
|
import {
|
|
41
41
|
getMissingDependencies,
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
isMariaDB,
|
|
57
57
|
getMysqlInstallInfo,
|
|
58
58
|
} from '../../engines/mysql/binary-detection'
|
|
59
|
+
import { updateManager } from '../../core/update-manager'
|
|
59
60
|
|
|
60
61
|
type MenuChoice =
|
|
61
62
|
| {
|
|
@@ -167,6 +168,7 @@ async function showMainMenu(): Promise<void> {
|
|
|
167
168
|
disabled: hasEngines ? false : 'No engines installed',
|
|
168
169
|
},
|
|
169
170
|
new inquirer.Separator(),
|
|
171
|
+
{ name: `${chalk.cyan('↑')} Check for updates`, value: 'check-update' },
|
|
170
172
|
{ name: `${chalk.gray('⏻')} Exit`, value: 'exit' },
|
|
171
173
|
]
|
|
172
174
|
|
|
@@ -205,6 +207,9 @@ async function showMainMenu(): Promise<void> {
|
|
|
205
207
|
case 'engines':
|
|
206
208
|
await handleEngines()
|
|
207
209
|
break
|
|
210
|
+
case 'check-update':
|
|
211
|
+
await handleCheckUpdate()
|
|
212
|
+
break
|
|
208
213
|
case 'exit':
|
|
209
214
|
console.log(chalk.gray('\n Goodbye!\n'))
|
|
210
215
|
process.exit(0)
|
|
@@ -214,6 +219,94 @@ async function showMainMenu(): Promise<void> {
|
|
|
214
219
|
await showMainMenu()
|
|
215
220
|
}
|
|
216
221
|
|
|
222
|
+
async function handleCheckUpdate(): Promise<void> {
|
|
223
|
+
console.clear()
|
|
224
|
+
console.log(header('Check for Updates'))
|
|
225
|
+
console.log()
|
|
226
|
+
|
|
227
|
+
const spinner = createSpinner('Checking for updates...')
|
|
228
|
+
spinner.start()
|
|
229
|
+
|
|
230
|
+
const result = await updateManager.checkForUpdate(true)
|
|
231
|
+
|
|
232
|
+
if (!result) {
|
|
233
|
+
spinner.fail('Could not reach npm registry')
|
|
234
|
+
console.log()
|
|
235
|
+
console.log(info('Check your internet connection and try again.'))
|
|
236
|
+
console.log(chalk.gray(' Manual update: npm install -g spindb@latest'))
|
|
237
|
+
console.log()
|
|
238
|
+
await pressEnterToContinue()
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (result.updateAvailable) {
|
|
243
|
+
spinner.succeed('Update available')
|
|
244
|
+
console.log()
|
|
245
|
+
console.log(chalk.gray(` Current version: ${result.currentVersion}`))
|
|
246
|
+
console.log(
|
|
247
|
+
chalk.gray(` Latest version: ${chalk.green(result.latestVersion)}`),
|
|
248
|
+
)
|
|
249
|
+
console.log()
|
|
250
|
+
|
|
251
|
+
const { action } = await inquirer.prompt<{ action: string }>([
|
|
252
|
+
{
|
|
253
|
+
type: 'list',
|
|
254
|
+
name: 'action',
|
|
255
|
+
message: 'What would you like to do?',
|
|
256
|
+
choices: [
|
|
257
|
+
{ name: 'Update now', value: 'update' },
|
|
258
|
+
{ name: 'Remind me later', value: 'later' },
|
|
259
|
+
{ name: "Don't check for updates on startup", value: 'disable' },
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
])
|
|
263
|
+
|
|
264
|
+
if (action === 'update') {
|
|
265
|
+
console.log()
|
|
266
|
+
const updateSpinner = createSpinner('Updating spindb...')
|
|
267
|
+
updateSpinner.start()
|
|
268
|
+
|
|
269
|
+
const updateResult = await updateManager.performUpdate()
|
|
270
|
+
|
|
271
|
+
if (updateResult.success) {
|
|
272
|
+
updateSpinner.succeed('Update complete')
|
|
273
|
+
console.log()
|
|
274
|
+
console.log(
|
|
275
|
+
success(
|
|
276
|
+
`Updated from ${updateResult.previousVersion} to ${updateResult.newVersion}`,
|
|
277
|
+
),
|
|
278
|
+
)
|
|
279
|
+
console.log()
|
|
280
|
+
if (updateResult.previousVersion !== updateResult.newVersion) {
|
|
281
|
+
console.log(warning('Please restart spindb to use the new version.'))
|
|
282
|
+
console.log()
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
updateSpinner.fail('Update failed')
|
|
286
|
+
console.log()
|
|
287
|
+
console.log(error(updateResult.error || 'Unknown error'))
|
|
288
|
+
console.log()
|
|
289
|
+
console.log(info('Manual update: npm install -g spindb@latest'))
|
|
290
|
+
}
|
|
291
|
+
await pressEnterToContinue()
|
|
292
|
+
} else if (action === 'disable') {
|
|
293
|
+
await updateManager.setAutoCheckEnabled(false)
|
|
294
|
+
console.log()
|
|
295
|
+
console.log(info('Update checks disabled on startup.'))
|
|
296
|
+
console.log(chalk.gray(' Re-enable with: spindb config update-check on'))
|
|
297
|
+
console.log()
|
|
298
|
+
await pressEnterToContinue()
|
|
299
|
+
}
|
|
300
|
+
// 'later' just returns to menu
|
|
301
|
+
} else {
|
|
302
|
+
spinner.succeed('You are on the latest version')
|
|
303
|
+
console.log()
|
|
304
|
+
console.log(chalk.gray(` Version: ${result.currentVersion}`))
|
|
305
|
+
console.log()
|
|
306
|
+
await pressEnterToContinue()
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
217
310
|
async function handleCreate(): Promise<void> {
|
|
218
311
|
console.log()
|
|
219
312
|
const answers = await promptCreateOptions()
|
|
@@ -294,7 +387,7 @@ async function handleCreate(): Promise<void> {
|
|
|
294
387
|
createSpinnerInstance.start()
|
|
295
388
|
|
|
296
389
|
await containerManager.create(containerName, {
|
|
297
|
-
engine: dbEngine.name as
|
|
390
|
+
engine: dbEngine.name as Engine,
|
|
298
391
|
version,
|
|
299
392
|
port,
|
|
300
393
|
database,
|
|
@@ -1063,7 +1156,7 @@ async function handleCreateForRestore(): Promise<{
|
|
|
1063
1156
|
createSpinnerInstance.start()
|
|
1064
1157
|
|
|
1065
1158
|
await containerManager.create(containerName, {
|
|
1066
|
-
engine: dbEngine.name as
|
|
1159
|
+
engine: dbEngine.name as Engine,
|
|
1067
1160
|
version,
|
|
1068
1161
|
port,
|
|
1069
1162
|
database,
|
|
@@ -1630,7 +1723,9 @@ async function handleBackup(): Promise<void> {
|
|
|
1630
1723
|
missingDeps = await getMissingDependencies(config.engine)
|
|
1631
1724
|
if (missingDeps.length > 0) {
|
|
1632
1725
|
console.log(
|
|
1633
|
-
error(
|
|
1726
|
+
error(
|
|
1727
|
+
`Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
1728
|
+
),
|
|
1634
1729
|
)
|
|
1635
1730
|
return
|
|
1636
1731
|
}
|
|
@@ -1646,7 +1741,10 @@ async function handleBackup(): Promise<void> {
|
|
|
1646
1741
|
let databaseName: string
|
|
1647
1742
|
|
|
1648
1743
|
if (databases.length > 1) {
|
|
1649
|
-
databaseName = await promptDatabaseSelect(
|
|
1744
|
+
databaseName = await promptDatabaseSelect(
|
|
1745
|
+
databases,
|
|
1746
|
+
'Select database to backup:',
|
|
1747
|
+
)
|
|
1650
1748
|
} else {
|
|
1651
1749
|
databaseName = databases[0]
|
|
1652
1750
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import inquirer from 'inquirer'
|
|
4
|
+
import { updateManager } from '../../core/update-manager'
|
|
5
|
+
import { createSpinner } from '../ui/spinner'
|
|
6
|
+
import { success, error, info, header } from '../ui/theme'
|
|
7
|
+
|
|
8
|
+
export const selfUpdateCommand = new Command('self-update')
|
|
9
|
+
.alias('update')
|
|
10
|
+
.description('Update spindb to the latest version')
|
|
11
|
+
.option('-f, --force', 'Update even if already on latest version')
|
|
12
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
13
|
+
.action(
|
|
14
|
+
async (options: { force?: boolean; yes?: boolean }): Promise<void> => {
|
|
15
|
+
console.log()
|
|
16
|
+
console.log(header('SpinDB Self-Update'))
|
|
17
|
+
console.log()
|
|
18
|
+
|
|
19
|
+
const checkSpinner = createSpinner('Checking for updates...')
|
|
20
|
+
checkSpinner.start()
|
|
21
|
+
|
|
22
|
+
const result = await updateManager.checkForUpdate(true)
|
|
23
|
+
|
|
24
|
+
if (!result) {
|
|
25
|
+
checkSpinner.fail('Could not reach npm registry')
|
|
26
|
+
console.log()
|
|
27
|
+
console.log(info('Check your internet connection and try again.'))
|
|
28
|
+
console.log(chalk.gray(' Manual update: npm install -g spindb@latest'))
|
|
29
|
+
process.exit(1)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!result.updateAvailable && !options.force) {
|
|
33
|
+
checkSpinner.succeed('Already on latest version')
|
|
34
|
+
console.log()
|
|
35
|
+
console.log(chalk.gray(` Current version: ${result.currentVersion}`))
|
|
36
|
+
console.log(chalk.gray(` Latest version: ${result.latestVersion}`))
|
|
37
|
+
console.log()
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (result.updateAvailable) {
|
|
42
|
+
checkSpinner.succeed('Update available')
|
|
43
|
+
} else {
|
|
44
|
+
checkSpinner.succeed('Version check complete')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log()
|
|
48
|
+
console.log(chalk.gray(` Current version: ${result.currentVersion}`))
|
|
49
|
+
console.log(
|
|
50
|
+
chalk.gray(
|
|
51
|
+
` Latest version: ${result.updateAvailable ? chalk.green(result.latestVersion) : result.latestVersion}`,
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
console.log()
|
|
55
|
+
|
|
56
|
+
// Confirm unless --yes
|
|
57
|
+
if (!options.yes) {
|
|
58
|
+
const message = result.updateAvailable
|
|
59
|
+
? `Update spindb from ${result.currentVersion} to ${result.latestVersion}?`
|
|
60
|
+
: `Reinstall spindb ${result.currentVersion}?`
|
|
61
|
+
|
|
62
|
+
const { confirm } = await inquirer.prompt<{ confirm: boolean }>([
|
|
63
|
+
{
|
|
64
|
+
type: 'confirm',
|
|
65
|
+
name: 'confirm',
|
|
66
|
+
message,
|
|
67
|
+
default: true,
|
|
68
|
+
},
|
|
69
|
+
])
|
|
70
|
+
|
|
71
|
+
if (!confirm) {
|
|
72
|
+
console.log(chalk.yellow('Update cancelled'))
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log()
|
|
78
|
+
const updateSpinner = createSpinner('Updating spindb...')
|
|
79
|
+
updateSpinner.start()
|
|
80
|
+
|
|
81
|
+
const updateResult = await updateManager.performUpdate()
|
|
82
|
+
|
|
83
|
+
if (updateResult.success) {
|
|
84
|
+
updateSpinner.succeed('Update complete')
|
|
85
|
+
console.log()
|
|
86
|
+
console.log(
|
|
87
|
+
success(
|
|
88
|
+
`Updated from ${updateResult.previousVersion} to ${updateResult.newVersion}`,
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
console.log()
|
|
92
|
+
if (updateResult.previousVersion !== updateResult.newVersion) {
|
|
93
|
+
console.log(
|
|
94
|
+
chalk.gray(
|
|
95
|
+
' Please restart your terminal to use the new version.',
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
console.log()
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
updateSpinner.fail('Update failed')
|
|
102
|
+
console.log()
|
|
103
|
+
console.log(error(updateResult.error || 'Unknown error'))
|
|
104
|
+
console.log()
|
|
105
|
+
console.log(info('Manual update: npm install -g spindb@latest'))
|
|
106
|
+
process.exit(1)
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { updateManager } from '../../core/update-manager'
|
|
4
|
+
import { createSpinner } from '../ui/spinner'
|
|
5
|
+
|
|
6
|
+
export const versionCommand = new Command('version')
|
|
7
|
+
.description('Show version information and check for updates')
|
|
8
|
+
.option('-c, --check', 'Check for available updates')
|
|
9
|
+
.option('-j, --json', 'Output as JSON')
|
|
10
|
+
.action(
|
|
11
|
+
async (options: { check?: boolean; json?: boolean }): Promise<void> => {
|
|
12
|
+
const currentVersion = updateManager.getCurrentVersion()
|
|
13
|
+
|
|
14
|
+
if (options.check) {
|
|
15
|
+
const spinner = createSpinner('Checking for updates...')
|
|
16
|
+
if (!options.json) spinner.start()
|
|
17
|
+
|
|
18
|
+
const result = await updateManager.checkForUpdate(true)
|
|
19
|
+
|
|
20
|
+
if (!options.json) spinner.stop()
|
|
21
|
+
|
|
22
|
+
if (options.json) {
|
|
23
|
+
console.log(
|
|
24
|
+
JSON.stringify({
|
|
25
|
+
current: currentVersion,
|
|
26
|
+
latest: result?.latestVersion || null,
|
|
27
|
+
updateAvailable: result?.updateAvailable || false,
|
|
28
|
+
}),
|
|
29
|
+
)
|
|
30
|
+
} else {
|
|
31
|
+
console.log()
|
|
32
|
+
console.log(`SpinDB v${currentVersion}`)
|
|
33
|
+
if (result) {
|
|
34
|
+
if (result.updateAvailable) {
|
|
35
|
+
console.log(
|
|
36
|
+
chalk.yellow(`Update available: v${result.latestVersion}`),
|
|
37
|
+
)
|
|
38
|
+
console.log(chalk.gray("Run 'spindb self-update' to update."))
|
|
39
|
+
} else {
|
|
40
|
+
console.log(chalk.green('You are on the latest version.'))
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
console.log(chalk.gray('Could not check for updates (offline?)'))
|
|
44
|
+
}
|
|
45
|
+
console.log()
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
if (options.json) {
|
|
49
|
+
console.log(JSON.stringify({ current: currentVersion }))
|
|
50
|
+
} else {
|
|
51
|
+
console.log(`SpinDB v${currentVersion}`)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
)
|
package/cli/index.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { program } from 'commander'
|
|
2
|
+
import { createRequire } from 'module'
|
|
3
|
+
import chalk from 'chalk'
|
|
2
4
|
import { createCommand } from './commands/create'
|
|
5
|
+
|
|
6
|
+
const require = createRequire(import.meta.url)
|
|
7
|
+
const pkg = require('../package.json') as { version: string }
|
|
3
8
|
import { listCommand } from './commands/list'
|
|
4
9
|
import { startCommand } from './commands/start'
|
|
5
10
|
import { stopCommand } from './commands/stop'
|
|
@@ -15,12 +20,86 @@ import { enginesCommand } from './commands/engines'
|
|
|
15
20
|
import { editCommand } from './commands/edit'
|
|
16
21
|
import { urlCommand } from './commands/url'
|
|
17
22
|
import { infoCommand } from './commands/info'
|
|
23
|
+
import { selfUpdateCommand } from './commands/self-update'
|
|
24
|
+
import { versionCommand } from './commands/version'
|
|
25
|
+
import { updateManager } from '../core/update-manager'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Show update notification banner if an update is available (from cached data)
|
|
29
|
+
* This shows on every run until the user updates or disables checks
|
|
30
|
+
*/
|
|
31
|
+
async function showUpdateNotificationIfAvailable(): Promise<void> {
|
|
32
|
+
try {
|
|
33
|
+
const cached = await updateManager.getCachedUpdateInfo()
|
|
34
|
+
|
|
35
|
+
// Skip if auto-check is disabled or no cached version
|
|
36
|
+
if (!cached.autoCheckEnabled || !cached.latestVersion) return
|
|
37
|
+
|
|
38
|
+
const currentVersion = updateManager.getCurrentVersion()
|
|
39
|
+
const latestVersion = cached.latestVersion
|
|
40
|
+
|
|
41
|
+
// Skip if no update available
|
|
42
|
+
if (updateManager.compareVersions(latestVersion, currentVersion) <= 0)
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
// Show notification banner
|
|
46
|
+
console.log()
|
|
47
|
+
console.log(chalk.cyan('┌' + '─'.repeat(52) + '┐'))
|
|
48
|
+
console.log(
|
|
49
|
+
chalk.cyan('│') +
|
|
50
|
+
chalk.yellow(' Update available! ') +
|
|
51
|
+
chalk.gray(`${currentVersion} -> `) +
|
|
52
|
+
chalk.green(latestVersion) +
|
|
53
|
+
' '.repeat(
|
|
54
|
+
Math.max(
|
|
55
|
+
0,
|
|
56
|
+
52 - 21 - currentVersion.length - 4 - latestVersion.length,
|
|
57
|
+
),
|
|
58
|
+
) +
|
|
59
|
+
chalk.cyan('│'),
|
|
60
|
+
)
|
|
61
|
+
console.log(
|
|
62
|
+
chalk.cyan('│') +
|
|
63
|
+
chalk.gray(' Run: ') +
|
|
64
|
+
chalk.cyan('spindb self-update') +
|
|
65
|
+
' '.repeat(28) +
|
|
66
|
+
chalk.cyan('│'),
|
|
67
|
+
)
|
|
68
|
+
console.log(
|
|
69
|
+
chalk.cyan('│') +
|
|
70
|
+
chalk.gray(' To disable: ') +
|
|
71
|
+
chalk.gray('spindb config update-check off') +
|
|
72
|
+
' '.repeat(8) +
|
|
73
|
+
chalk.cyan('│'),
|
|
74
|
+
)
|
|
75
|
+
console.log(chalk.cyan('└' + '─'.repeat(52) + '┘'))
|
|
76
|
+
console.log()
|
|
77
|
+
} catch {
|
|
78
|
+
// Silently ignore errors - update notification is not critical
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Trigger background update check (fire and forget)
|
|
84
|
+
* This updates the cache for the next run's notification
|
|
85
|
+
*/
|
|
86
|
+
function triggerBackgroundUpdateCheck(): void {
|
|
87
|
+
updateManager.checkForUpdate(false).catch(() => {
|
|
88
|
+
// Silently ignore - background check is best-effort
|
|
89
|
+
})
|
|
90
|
+
}
|
|
18
91
|
|
|
19
92
|
export async function run(): Promise<void> {
|
|
93
|
+
// Trigger background update check (non-blocking, updates cache for next run)
|
|
94
|
+
triggerBackgroundUpdateCheck()
|
|
95
|
+
|
|
96
|
+
// Show update notification if an update is available (from cached data)
|
|
97
|
+
await showUpdateNotificationIfAvailable()
|
|
98
|
+
|
|
20
99
|
program
|
|
21
100
|
.name('spindb')
|
|
22
101
|
.description('Spin up local database containers without Docker')
|
|
23
|
-
.version(
|
|
102
|
+
.version(pkg.version, '-v, --version', 'output the version number')
|
|
24
103
|
|
|
25
104
|
program.addCommand(createCommand)
|
|
26
105
|
program.addCommand(listCommand)
|
|
@@ -38,6 +117,8 @@ export async function run(): Promise<void> {
|
|
|
38
117
|
program.addCommand(editCommand)
|
|
39
118
|
program.addCommand(urlCommand)
|
|
40
119
|
program.addCommand(infoCommand)
|
|
120
|
+
program.addCommand(selfUpdateCommand)
|
|
121
|
+
program.addCommand(versionCommand)
|
|
41
122
|
|
|
42
123
|
// If no arguments provided, show interactive menu
|
|
43
124
|
if (process.argv.length <= 2) {
|
package/cli/ui/prompts.ts
CHANGED
|
@@ -251,7 +251,8 @@ export async function promptDatabaseName(
|
|
|
251
251
|
engine?: string,
|
|
252
252
|
): Promise<string> {
|
|
253
253
|
// MySQL uses "schema" terminology (database and schema are synonymous)
|
|
254
|
-
const label =
|
|
254
|
+
const label =
|
|
255
|
+
engine === 'mysql' ? 'Database (schema) name:' : 'Database name:'
|
|
255
256
|
|
|
256
257
|
const { database } = await inquirer.prompt<{ database: string }>([
|
|
257
258
|
{
|
package/config/defaults.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
getSupportedEngines,
|
|
6
6
|
type EngineDefaults,
|
|
7
7
|
} from './engine-defaults'
|
|
8
|
+
import { Engine } from '../types'
|
|
8
9
|
|
|
9
10
|
// Re-export engine-related functions and types
|
|
10
11
|
export {
|
|
@@ -24,50 +25,25 @@ export type PortRange = {
|
|
|
24
25
|
end: number
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
/**
|
|
28
|
-
* Legacy Defaults type - kept for backward compatibility
|
|
29
|
-
* New code should use getEngineDefaults(engine) instead
|
|
30
|
-
*/
|
|
31
28
|
export type Defaults = {
|
|
32
|
-
/** @deprecated Use getEngineDefaults(engine).defaultVersion instead */
|
|
33
|
-
postgresVersion: string
|
|
34
29
|
port: number
|
|
35
30
|
portRange: PortRange
|
|
36
|
-
engine:
|
|
37
|
-
/** @deprecated Use getEngineDefaults(engine).supportedVersions instead */
|
|
38
|
-
supportedPostgresVersions: string[]
|
|
31
|
+
engine: Engine
|
|
39
32
|
superuser: string
|
|
40
33
|
platformMappings: PlatformMappings
|
|
41
34
|
}
|
|
42
35
|
|
|
43
|
-
// Get PostgreSQL defaults from engine-defaults
|
|
44
36
|
const pgDefaults = engineDefaults.postgresql
|
|
45
37
|
|
|
46
38
|
/**
|
|
47
|
-
* Default configuration values
|
|
48
|
-
*
|
|
49
|
-
* New code should use getEngineDefaults(engine) for engine-specific defaults.
|
|
39
|
+
* Default configuration values (PostgreSQL-based defaults)
|
|
40
|
+
* Use getEngineDefaults(engine) for engine-specific defaults.
|
|
50
41
|
*/
|
|
51
42
|
export const defaults: Defaults = {
|
|
52
|
-
// Default PostgreSQL version (from engine defaults)
|
|
53
|
-
postgresVersion: pgDefaults.defaultVersion,
|
|
54
|
-
|
|
55
|
-
// Default port (standard PostgreSQL port)
|
|
56
43
|
port: pgDefaults.defaultPort,
|
|
57
|
-
|
|
58
|
-
// Port range to scan if default is busy
|
|
59
44
|
portRange: pgDefaults.portRange,
|
|
60
|
-
|
|
61
|
-
// Default engine
|
|
62
|
-
engine: 'postgresql',
|
|
63
|
-
|
|
64
|
-
// Supported PostgreSQL versions (from engine defaults)
|
|
65
|
-
supportedPostgresVersions: pgDefaults.supportedVersions,
|
|
66
|
-
|
|
67
|
-
// Default superuser (from engine defaults)
|
|
45
|
+
engine: Engine.PostgreSQL,
|
|
68
46
|
superuser: pgDefaults.superuser,
|
|
69
|
-
|
|
70
|
-
// Platform mappings for zonky.io binaries (PostgreSQL specific)
|
|
71
47
|
platformMappings: {
|
|
72
48
|
'darwin-arm64': 'darwin-arm64v8',
|
|
73
49
|
'darwin-x64': 'darwin-amd64',
|
package/core/binary-manager.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { exec } from 'child_process'
|
|
|
6
6
|
import { promisify } from 'util'
|
|
7
7
|
import { paths } from '../config/paths'
|
|
8
8
|
import { defaults } from '../config/defaults'
|
|
9
|
-
import type
|
|
9
|
+
import { Engine, type ProgressCallback, type InstalledBinary } from '../types'
|
|
10
10
|
|
|
11
11
|
const execAsync = promisify(exec)
|
|
12
12
|
|
|
@@ -91,7 +91,7 @@ export class BinaryManager {
|
|
|
91
91
|
const parts = entry.name.split('-')
|
|
92
92
|
if (parts.length >= 4) {
|
|
93
93
|
installed.push({
|
|
94
|
-
engine: parts[0],
|
|
94
|
+
engine: parts[0] as Engine,
|
|
95
95
|
version: parts[1],
|
|
96
96
|
platform: parts[2],
|
|
97
97
|
arch: parts[3],
|
package/core/config-manager.ts
CHANGED
|
@@ -29,12 +29,7 @@ const POSTGRESQL_TOOLS: BinaryTool[] = [
|
|
|
29
29
|
'pg_basebackup',
|
|
30
30
|
]
|
|
31
31
|
|
|
32
|
-
const MYSQL_TOOLS: BinaryTool[] = [
|
|
33
|
-
'mysql',
|
|
34
|
-
'mysqldump',
|
|
35
|
-
'mysqladmin',
|
|
36
|
-
'mysqld',
|
|
37
|
-
]
|
|
32
|
+
const MYSQL_TOOLS: BinaryTool[] = ['mysql', 'mysqldump', 'mysqladmin', 'mysqld']
|
|
38
33
|
|
|
39
34
|
const ENHANCED_SHELLS: BinaryTool[] = ['pgcli', 'mycli', 'usql']
|
|
40
35
|
|
|
@@ -359,9 +354,4 @@ export class ConfigManager {
|
|
|
359
354
|
export const configManager = new ConfigManager()
|
|
360
355
|
|
|
361
356
|
// Export tool categories for use in commands
|
|
362
|
-
export {
|
|
363
|
-
POSTGRESQL_TOOLS,
|
|
364
|
-
MYSQL_TOOLS,
|
|
365
|
-
ENHANCED_SHELLS,
|
|
366
|
-
ALL_TOOLS,
|
|
367
|
-
}
|
|
357
|
+
export { POSTGRESQL_TOOLS, MYSQL_TOOLS, ENHANCED_SHELLS, ALL_TOOLS }
|
|
@@ -5,10 +5,11 @@ import { processManager } from './process-manager'
|
|
|
5
5
|
import { portManager } from './port-manager'
|
|
6
6
|
import { getEngineDefaults, getSupportedEngines } from '../config/defaults'
|
|
7
7
|
import { getEngine } from '../engines'
|
|
8
|
-
import type { ContainerConfig
|
|
8
|
+
import type { ContainerConfig } from '../types'
|
|
9
|
+
import { Engine } from '../types'
|
|
9
10
|
|
|
10
11
|
export type CreateOptions = {
|
|
11
|
-
engine:
|
|
12
|
+
engine: Engine
|
|
12
13
|
version: string
|
|
13
14
|
port: number
|
|
14
15
|
database: string
|
|
@@ -398,7 +399,9 @@ export class ContainerManager {
|
|
|
398
399
|
|
|
399
400
|
// Don't remove the primary database from the array
|
|
400
401
|
if (database === config.database) {
|
|
401
|
-
throw new Error(
|
|
402
|
+
throw new Error(
|
|
403
|
+
`Cannot remove primary database "${database}" from tracking`,
|
|
404
|
+
)
|
|
402
405
|
}
|
|
403
406
|
|
|
404
407
|
if (config.databases) {
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { exec } from 'child_process'
|
|
2
|
+
import { promisify } from 'util'
|
|
3
|
+
import { createRequire } from 'module'
|
|
4
|
+
import { configManager } from './config-manager'
|
|
5
|
+
|
|
6
|
+
const execAsync = promisify(exec)
|
|
7
|
+
const require = createRequire(import.meta.url)
|
|
8
|
+
|
|
9
|
+
const NPM_REGISTRY_URL = 'https://registry.npmjs.org/spindb'
|
|
10
|
+
const CHECK_THROTTLE_MS = 24 * 60 * 60 * 1000 // 24 hours
|
|
11
|
+
|
|
12
|
+
export type UpdateCheckResult = {
|
|
13
|
+
currentVersion: string
|
|
14
|
+
latestVersion: string
|
|
15
|
+
updateAvailable: boolean
|
|
16
|
+
lastChecked: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type UpdateResult = {
|
|
20
|
+
success: boolean
|
|
21
|
+
previousVersion: string
|
|
22
|
+
newVersion: string
|
|
23
|
+
error?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class UpdateManager {
|
|
27
|
+
/**
|
|
28
|
+
* Get currently installed version from package.json
|
|
29
|
+
*/
|
|
30
|
+
getCurrentVersion(): string {
|
|
31
|
+
const pkg = require('../package.json') as { version: string }
|
|
32
|
+
return pkg.version
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check npm registry for latest version
|
|
37
|
+
* Throttled to once per 24 hours unless force=true
|
|
38
|
+
*/
|
|
39
|
+
async checkForUpdate(force = false): Promise<UpdateCheckResult | null> {
|
|
40
|
+
const config = await configManager.load()
|
|
41
|
+
const lastCheck = config.update?.lastCheck
|
|
42
|
+
|
|
43
|
+
// Return cached result if within throttle period
|
|
44
|
+
if (!force && lastCheck) {
|
|
45
|
+
const elapsed = Date.now() - new Date(lastCheck).getTime()
|
|
46
|
+
if (elapsed < CHECK_THROTTLE_MS && config.update?.latestVersion) {
|
|
47
|
+
const currentVersion = this.getCurrentVersion()
|
|
48
|
+
return {
|
|
49
|
+
currentVersion,
|
|
50
|
+
latestVersion: config.update.latestVersion,
|
|
51
|
+
updateAvailable:
|
|
52
|
+
this.compareVersions(config.update.latestVersion, currentVersion) >
|
|
53
|
+
0,
|
|
54
|
+
lastChecked: lastCheck,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const latestVersion = await this.fetchLatestVersion()
|
|
61
|
+
const currentVersion = this.getCurrentVersion()
|
|
62
|
+
|
|
63
|
+
// Update cache
|
|
64
|
+
config.update = {
|
|
65
|
+
...config.update,
|
|
66
|
+
lastCheck: new Date().toISOString(),
|
|
67
|
+
latestVersion,
|
|
68
|
+
}
|
|
69
|
+
await configManager.save()
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
currentVersion,
|
|
73
|
+
latestVersion,
|
|
74
|
+
updateAvailable:
|
|
75
|
+
this.compareVersions(latestVersion, currentVersion) > 0,
|
|
76
|
+
lastChecked: new Date().toISOString(),
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// Offline or registry error - return null
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Perform self-update via npm
|
|
86
|
+
*/
|
|
87
|
+
async performUpdate(): Promise<UpdateResult> {
|
|
88
|
+
const previousVersion = this.getCurrentVersion()
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
// Execute npm install globally
|
|
92
|
+
await execAsync('npm install -g spindb@latest', { timeout: 60000 })
|
|
93
|
+
|
|
94
|
+
// Verify new version by checking what npm reports
|
|
95
|
+
const { stdout } = await execAsync('npm list -g spindb --json')
|
|
96
|
+
const npmData = JSON.parse(stdout) as {
|
|
97
|
+
dependencies?: { spindb?: { version?: string } }
|
|
98
|
+
}
|
|
99
|
+
const newVersion =
|
|
100
|
+
npmData.dependencies?.spindb?.version || previousVersion
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
success: true,
|
|
104
|
+
previousVersion,
|
|
105
|
+
newVersion,
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
109
|
+
|
|
110
|
+
// Detect permission issues
|
|
111
|
+
if (message.includes('EACCES') || message.includes('permission')) {
|
|
112
|
+
return {
|
|
113
|
+
success: false,
|
|
114
|
+
previousVersion,
|
|
115
|
+
newVersion: previousVersion,
|
|
116
|
+
error: 'Permission denied. Try: sudo npm install -g spindb@latest',
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
success: false,
|
|
122
|
+
previousVersion,
|
|
123
|
+
newVersion: previousVersion,
|
|
124
|
+
error: message,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get cached update info (for showing notification without network call)
|
|
131
|
+
*/
|
|
132
|
+
async getCachedUpdateInfo(): Promise<{
|
|
133
|
+
latestVersion?: string
|
|
134
|
+
autoCheckEnabled: boolean
|
|
135
|
+
}> {
|
|
136
|
+
const config = await configManager.load()
|
|
137
|
+
return {
|
|
138
|
+
latestVersion: config.update?.latestVersion,
|
|
139
|
+
autoCheckEnabled: config.update?.autoCheckEnabled !== false,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Set whether auto-update checks are enabled
|
|
145
|
+
*/
|
|
146
|
+
async setAutoCheckEnabled(enabled: boolean): Promise<void> {
|
|
147
|
+
const config = await configManager.load()
|
|
148
|
+
config.update = {
|
|
149
|
+
...config.update,
|
|
150
|
+
autoCheckEnabled: enabled,
|
|
151
|
+
}
|
|
152
|
+
await configManager.save()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Fetch latest version from npm registry
|
|
157
|
+
*/
|
|
158
|
+
private async fetchLatestVersion(): Promise<string> {
|
|
159
|
+
const controller = new AbortController()
|
|
160
|
+
const timeout = setTimeout(() => controller.abort(), 10000)
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const response = await fetch(NPM_REGISTRY_URL, {
|
|
164
|
+
signal: controller.signal,
|
|
165
|
+
})
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
throw new Error(`Registry returned ${response.status}`)
|
|
168
|
+
}
|
|
169
|
+
const data = (await response.json()) as {
|
|
170
|
+
'dist-tags': { latest: string }
|
|
171
|
+
}
|
|
172
|
+
return data['dist-tags'].latest
|
|
173
|
+
} finally {
|
|
174
|
+
clearTimeout(timeout)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Compare semver versions
|
|
180
|
+
* Returns >0 if a > b, <0 if a < b, 0 if equal
|
|
181
|
+
*/
|
|
182
|
+
compareVersions(a: string, b: string): number {
|
|
183
|
+
const partsA = a.split('.').map((n) => parseInt(n, 10) || 0)
|
|
184
|
+
const partsB = b.split('.').map((n) => parseInt(n, 10) || 0)
|
|
185
|
+
|
|
186
|
+
for (let i = 0; i < 3; i++) {
|
|
187
|
+
const diff = (partsA[i] || 0) - (partsB[i] || 0)
|
|
188
|
+
if (diff !== 0) return diff
|
|
189
|
+
}
|
|
190
|
+
return 0
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export const updateManager = new UpdateManager()
|
package/package.json
CHANGED
package/types/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type ContainerConfig = {
|
|
2
2
|
name: string
|
|
3
|
-
engine:
|
|
3
|
+
engine: Engine
|
|
4
4
|
version: string
|
|
5
5
|
port: number
|
|
6
6
|
database: string
|
|
@@ -14,7 +14,10 @@ export type ContainerConfig = {
|
|
|
14
14
|
* Supported database engine names
|
|
15
15
|
* Extendable for future engines (sqlite, etc.)
|
|
16
16
|
*/
|
|
17
|
-
export
|
|
17
|
+
export enum Engine {
|
|
18
|
+
PostgreSQL = 'postgresql',
|
|
19
|
+
MySQL = 'mysql',
|
|
20
|
+
}
|
|
18
21
|
|
|
19
22
|
export type ProgressCallback = (progress: {
|
|
20
23
|
stage: string
|
|
@@ -22,7 +25,7 @@ export type ProgressCallback = (progress: {
|
|
|
22
25
|
}) => void
|
|
23
26
|
|
|
24
27
|
export type InstalledBinary = {
|
|
25
|
-
engine:
|
|
28
|
+
engine: Engine
|
|
26
29
|
version: string
|
|
27
30
|
platform: string
|
|
28
31
|
arch: string
|
|
@@ -141,10 +144,16 @@ export type SpinDBConfig = {
|
|
|
141
144
|
}
|
|
142
145
|
// Default settings
|
|
143
146
|
defaults?: {
|
|
144
|
-
engine?:
|
|
147
|
+
engine?: Engine
|
|
145
148
|
version?: string
|
|
146
149
|
port?: number
|
|
147
150
|
}
|
|
148
151
|
// Last updated timestamp
|
|
149
152
|
updatedAt?: string
|
|
153
|
+
// Self-update tracking
|
|
154
|
+
update?: {
|
|
155
|
+
lastCheck?: string // ISO timestamp of last npm registry check
|
|
156
|
+
latestVersion?: string // Latest version found from registry
|
|
157
|
+
autoCheckEnabled?: boolean // Default true, user can disable
|
|
158
|
+
}
|
|
150
159
|
}
|