spindb 0.30.1 → 0.30.7
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 +32 -1
- package/cli/commands/menu/container-handlers.ts +79 -60
- package/cli/commands/menu/index.ts +93 -89
- package/cli/commands/menu/settings-handlers.ts +32 -4
- package/cli/commands/restore.ts +3 -2
- package/core/docker-exporter.ts +146 -38
- package/core/update-manager.ts +4 -1
- package/engines/qdrant/index.ts +4 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -386,6 +386,36 @@ Generated files:
|
|
|
386
386
|
- `entrypoint.sh` - Startup script
|
|
387
387
|
- `README.md` - Instructions
|
|
388
388
|
|
|
389
|
+
### Deploying Your Container
|
|
390
|
+
|
|
391
|
+
**SpinDB doesn't require Docker for local development**, but it can repackage your database as a Docker image for deployment to cloud servers, EC2 instances, Kubernetes clusters, or any Docker-compatible environment.
|
|
392
|
+
|
|
393
|
+
```bash
|
|
394
|
+
# Export your local database to Docker
|
|
395
|
+
spindb export docker mydb -o ./mydb-deploy
|
|
396
|
+
|
|
397
|
+
# Build and run
|
|
398
|
+
cd ./mydb-deploy
|
|
399
|
+
docker compose build --no-cache
|
|
400
|
+
docker compose up -d
|
|
401
|
+
|
|
402
|
+
# Connect from host (credentials in .env)
|
|
403
|
+
source .env
|
|
404
|
+
psql "postgresql://$SPINDB_USER:$SPINDB_PASSWORD@localhost:$PORT/$DATABASE"
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**Schema-only vs Full Data:**
|
|
408
|
+
```bash
|
|
409
|
+
spindb export docker mydb # Include all data (default)
|
|
410
|
+
spindb export docker mydb --no-data # Schema only (empty tables)
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
> **Development Tool Notice:** SpinDB is currently a development tool. While Docker exports include TLS encryption and authentication, they are intended for staging and testing—not production workloads. For production databases, consider managed services.
|
|
414
|
+
|
|
415
|
+
**Future Export Options:** Additional export targets are planned for future releases, including direct deployment to managed database services like Neon, Supabase, and PlanetScale.
|
|
416
|
+
|
|
417
|
+
See [DEPLOY.md](DEPLOY.md) for comprehensive deployment documentation.
|
|
418
|
+
|
|
389
419
|
### Container Management
|
|
390
420
|
|
|
391
421
|
```bash
|
|
@@ -433,7 +463,8 @@ spindb deps install # Install missing tools
|
|
|
433
463
|
# Configuration
|
|
434
464
|
spindb config show # Show current config
|
|
435
465
|
spindb config detect # Re-detect tool paths
|
|
436
|
-
spindb config update-check on # Enable update
|
|
466
|
+
spindb config update-check on # Enable update checks
|
|
467
|
+
spindb config update-check off # Disable update checks
|
|
437
468
|
|
|
438
469
|
# Doctor
|
|
439
470
|
spindb doctor # Interactive health check
|
|
@@ -66,7 +66,7 @@ import { Engine, isFileBasedEngine } from '../../../types'
|
|
|
66
66
|
import { type MenuChoice, pressEnterToContinue } from './shared'
|
|
67
67
|
import { getEngineIcon } from '../../constants'
|
|
68
68
|
|
|
69
|
-
export async function handleCreate(): Promise<'main' | void> {
|
|
69
|
+
export async function handleCreate(): Promise<'main' | string | void> {
|
|
70
70
|
console.log()
|
|
71
71
|
console.log(header('Create New Database Container'))
|
|
72
72
|
console.log()
|
|
@@ -342,11 +342,11 @@ export async function handleCreate(): Promise<'main' | void> {
|
|
|
342
342
|
{
|
|
343
343
|
type: 'input',
|
|
344
344
|
name: 'continue',
|
|
345
|
-
message: chalk.gray('Press Enter to
|
|
345
|
+
message: chalk.gray('Press Enter to continue...'),
|
|
346
346
|
},
|
|
347
347
|
])
|
|
348
348
|
}
|
|
349
|
-
return
|
|
349
|
+
return containerNameFinal
|
|
350
350
|
}
|
|
351
351
|
|
|
352
352
|
// Server databases: start and create database
|
|
@@ -410,7 +410,7 @@ export async function handleCreate(): Promise<'main' | void> {
|
|
|
410
410
|
{
|
|
411
411
|
type: 'input',
|
|
412
412
|
name: 'continue',
|
|
413
|
-
message: chalk.gray('Press Enter to
|
|
413
|
+
message: chalk.gray('Press Enter to continue...'),
|
|
414
414
|
},
|
|
415
415
|
])
|
|
416
416
|
}
|
|
@@ -426,7 +426,18 @@ export async function handleCreate(): Promise<'main' | void> {
|
|
|
426
426
|
`Start it later with: ${chalk.cyan(`spindb start ${containerNameFinal}`)}`,
|
|
427
427
|
),
|
|
428
428
|
)
|
|
429
|
+
console.log()
|
|
430
|
+
|
|
431
|
+
await escapeablePrompt([
|
|
432
|
+
{
|
|
433
|
+
type: 'input',
|
|
434
|
+
name: 'continue',
|
|
435
|
+
message: chalk.gray('Press Enter to continue...'),
|
|
436
|
+
},
|
|
437
|
+
])
|
|
429
438
|
}
|
|
439
|
+
|
|
440
|
+
return containerNameFinal
|
|
430
441
|
}
|
|
431
442
|
|
|
432
443
|
export async function handleList(
|
|
@@ -557,14 +568,13 @@ export async function handleList(
|
|
|
557
568
|
(c) => !isFileBasedEngine(c.engine),
|
|
558
569
|
)
|
|
559
570
|
|
|
560
|
-
// Build
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
// Build the full choice list with footer items
|
|
564
|
-
const summary = hints
|
|
565
|
-
? `${containers.length} container(s): ${parts.join('; ')} — ${hints}`
|
|
566
|
-
: `${containers.length} container(s): ${parts.join('; ')}`
|
|
571
|
+
// Build the full choice list with header hint and footer items
|
|
572
|
+
const summary = `${containers.length} container(s): ${parts.join('; ')}`
|
|
567
573
|
const allChoices: (FilterableChoice | inquirer.Separator)[] = [
|
|
574
|
+
// Show toggle hint at top when server-based containers exist
|
|
575
|
+
...(hasServerContainers
|
|
576
|
+
? [new inquirer.Separator(chalk.cyan('── [Shift+Tab] toggle start/stop ──'))]
|
|
577
|
+
: []),
|
|
568
578
|
...containerChoices,
|
|
569
579
|
new inquirer.Separator(),
|
|
570
580
|
new inquirer.Separator(summary),
|
|
@@ -694,12 +704,14 @@ export async function showContainerSubmenu(
|
|
|
694
704
|
// Build action choices based on engine type
|
|
695
705
|
const actionChoices: MenuChoice[] = []
|
|
696
706
|
|
|
697
|
-
// Helper for disabled menu items
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
707
|
+
// Helper for disabled menu items (hint shown in separator, not on each item)
|
|
708
|
+
function disabledItem(icon: string, label: string) {
|
|
709
|
+
return {
|
|
710
|
+
name: chalk.gray(`${icon} ${label}`),
|
|
711
|
+
value: '_disabled_',
|
|
712
|
+
disabled: '', // Empty string hides the "(Disabled)" text
|
|
713
|
+
}
|
|
714
|
+
}
|
|
703
715
|
|
|
704
716
|
// Determine if database-specific actions can be performed
|
|
705
717
|
// Requires: database selected + (running for server DBs OR file exists for file-based DBs)
|
|
@@ -707,13 +719,25 @@ export async function showContainerSubmenu(
|
|
|
707
719
|
const hasMultipleDatabases = databases.length > 1
|
|
708
720
|
const canDoDbAction = !!activeDatabase && containerReady
|
|
709
721
|
|
|
710
|
-
//
|
|
711
|
-
|
|
712
|
-
if (!activeDatabase && hasMultipleDatabases) return 'Select database first'
|
|
722
|
+
// Label for data section separator - shows state or required action
|
|
723
|
+
function getDataSectionLabel(): string {
|
|
713
724
|
if (!containerReady) {
|
|
714
725
|
return isFileBasedDB ? 'Database file missing' : 'Start container first'
|
|
715
726
|
}
|
|
716
|
-
|
|
727
|
+
if (!activeDatabase && hasMultipleDatabases) {
|
|
728
|
+
return 'Select database first'
|
|
729
|
+
}
|
|
730
|
+
// Show positive state when actions are available
|
|
731
|
+
return isFileBasedDB ? 'Available' : 'Running'
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Label for management section separator - shows state or required action
|
|
735
|
+
function getManageSectionLabel(): string {
|
|
736
|
+
if (!isFileBasedDB && isRunning) {
|
|
737
|
+
return 'Stop container first'
|
|
738
|
+
}
|
|
739
|
+
// Show positive state when actions are available
|
|
740
|
+
return isFileBasedDB ? 'Available' : 'Stopped'
|
|
717
741
|
}
|
|
718
742
|
|
|
719
743
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -733,6 +757,12 @@ export async function showContainerSubmenu(
|
|
|
733
757
|
value: 'stop',
|
|
734
758
|
})
|
|
735
759
|
}
|
|
760
|
+
|
|
761
|
+
// View logs - available anytime for server-based DBs
|
|
762
|
+
actionChoices.push({
|
|
763
|
+
name: `${chalk.gray('☰')} View logs`,
|
|
764
|
+
value: 'logs',
|
|
765
|
+
})
|
|
736
766
|
}
|
|
737
767
|
|
|
738
768
|
// Database selection - show current selection or prompt to select
|
|
@@ -748,18 +778,20 @@ export async function showContainerSubmenu(
|
|
|
748
778
|
})
|
|
749
779
|
}
|
|
750
780
|
|
|
751
|
-
actionChoices.push(new inquirer.Separator())
|
|
752
|
-
|
|
753
781
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
754
782
|
// SECTION 2: Data Operations
|
|
783
|
+
// Separator shows current state or required action
|
|
755
784
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
785
|
+
const dataSectionLabel = getDataSectionLabel()
|
|
786
|
+
actionChoices.push(
|
|
787
|
+
new inquirer.Separator(chalk.gray(`── ${dataSectionLabel} ──`)),
|
|
788
|
+
)
|
|
756
789
|
|
|
757
790
|
// Open shell - requires database selection for multi-db containers
|
|
758
|
-
const shellHint = getDbActionHint()
|
|
759
791
|
actionChoices.push(
|
|
760
792
|
canDoDbAction
|
|
761
793
|
? { name: `${chalk.blue('>')} Open shell`, value: 'shell' }
|
|
762
|
-
: disabledItem('>', 'Open shell'
|
|
794
|
+
: disabledItem('>', 'Open shell'),
|
|
763
795
|
)
|
|
764
796
|
|
|
765
797
|
// Run SQL/script - requires database selection for multi-db containers
|
|
@@ -778,66 +810,64 @@ export async function showContainerSubmenu(
|
|
|
778
810
|
: config.engine === Engine.SurrealDB
|
|
779
811
|
? 'Run SurrealQL file'
|
|
780
812
|
: 'Run SQL file'
|
|
781
|
-
const runSqlHint = getDbActionHint()
|
|
782
813
|
actionChoices.push(
|
|
783
814
|
canDoDbAction
|
|
784
815
|
? { name: `${chalk.yellow('▷')} ${runScriptLabel}`, value: 'run-sql' }
|
|
785
|
-
: disabledItem('▷', runScriptLabel
|
|
816
|
+
: disabledItem('▷', runScriptLabel),
|
|
786
817
|
)
|
|
787
818
|
}
|
|
788
819
|
|
|
789
820
|
// Copy connection string - requires database selection for multi-db containers
|
|
790
|
-
const copyHint = getDbActionHint()
|
|
791
821
|
actionChoices.push(
|
|
792
822
|
canDoDbAction
|
|
793
823
|
? { name: `${chalk.magenta('⊕')} Copy connection string`, value: 'copy' }
|
|
794
|
-
: disabledItem('⊕', 'Copy connection string'
|
|
824
|
+
: disabledItem('⊕', 'Copy connection string'),
|
|
795
825
|
)
|
|
796
826
|
|
|
797
827
|
// Backup - requires database selection for multi-db containers
|
|
798
|
-
const backupHint = getDbActionHint()
|
|
799
828
|
actionChoices.push(
|
|
800
829
|
canDoDbAction
|
|
801
830
|
? { name: `${chalk.magenta('↓')} Backup database`, value: 'backup' }
|
|
802
|
-
: disabledItem('↓', 'Backup database'
|
|
831
|
+
: disabledItem('↓', 'Backup database'),
|
|
803
832
|
)
|
|
804
833
|
|
|
805
834
|
// Restore - requires database selection for multi-db containers
|
|
806
|
-
const restoreHint = getDbActionHint()
|
|
807
835
|
actionChoices.push(
|
|
808
836
|
canDoDbAction
|
|
809
837
|
? { name: `${chalk.magenta('↑')} Restore from backup`, value: 'restore' }
|
|
810
|
-
: disabledItem('↑', 'Restore from backup'
|
|
838
|
+
: disabledItem('↑', 'Restore from backup'),
|
|
811
839
|
)
|
|
812
840
|
|
|
813
|
-
//
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
name: `${chalk.
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
actionChoices.push(new inquirer.Separator())
|
|
841
|
+
// Export - server-based DBs must be running, file-based must have the file
|
|
842
|
+
actionChoices.push(
|
|
843
|
+
containerReady
|
|
844
|
+
? { name: `${chalk.cyan('⬆')} Export`, value: 'export' }
|
|
845
|
+
: disabledItem('⬆', 'Export'),
|
|
846
|
+
)
|
|
822
847
|
|
|
823
848
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
824
|
-
// SECTION 3: Container Management (requires stopped)
|
|
849
|
+
// SECTION 3: Container Management (requires stopped for server-based)
|
|
850
|
+
// Separator shows current state or required action
|
|
825
851
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
852
|
+
const manageSectionLabel = getManageSectionLabel()
|
|
853
|
+
actionChoices.push(
|
|
854
|
+
new inquirer.Separator(chalk.gray(`── ${manageSectionLabel} ──`)),
|
|
855
|
+
)
|
|
826
856
|
|
|
827
857
|
// Edit container - file-based DBs can always edit (no running state), server databases must be stopped
|
|
828
|
-
const canEdit = isFileBasedDB
|
|
858
|
+
const canEdit = isFileBasedDB || !isRunning
|
|
829
859
|
actionChoices.push(
|
|
830
860
|
canEdit
|
|
831
861
|
? { name: `${chalk.yellow('⚙')} Edit container`, value: 'edit' }
|
|
832
|
-
: disabledItem('⚙', 'Edit container'
|
|
862
|
+
: disabledItem('⚙', 'Edit container'),
|
|
833
863
|
)
|
|
834
864
|
|
|
835
865
|
// Clone container - file-based DBs can always clone, server databases must be stopped
|
|
836
|
-
const canClone = isFileBasedDB
|
|
866
|
+
const canClone = isFileBasedDB || !isRunning
|
|
837
867
|
actionChoices.push(
|
|
838
868
|
canClone
|
|
839
869
|
? { name: `${chalk.cyan('◇')} Clone container`, value: 'clone' }
|
|
840
|
-
: disabledItem('◇', 'Clone container'
|
|
870
|
+
: disabledItem('◇', 'Clone container'),
|
|
841
871
|
)
|
|
842
872
|
|
|
843
873
|
// Detach - only for file-based DBs (unregisters without deleting file)
|
|
@@ -849,22 +879,11 @@ export async function showContainerSubmenu(
|
|
|
849
879
|
}
|
|
850
880
|
|
|
851
881
|
// Delete container - file-based DBs can always delete, server databases must be stopped
|
|
852
|
-
const canDelete = isFileBasedDB
|
|
882
|
+
const canDelete = isFileBasedDB || !isRunning
|
|
853
883
|
actionChoices.push(
|
|
854
884
|
canDelete
|
|
855
885
|
? { name: `${chalk.red('✕')} Delete container`, value: 'delete' }
|
|
856
|
-
: disabledItem('✕', 'Delete container'
|
|
857
|
-
)
|
|
858
|
-
|
|
859
|
-
// Export - server-based DBs must be running, file-based must have the file
|
|
860
|
-
const canExport = containerReady
|
|
861
|
-
const exportHint = isFileBasedDB
|
|
862
|
-
? 'Database file missing'
|
|
863
|
-
: 'Start container first'
|
|
864
|
-
actionChoices.push(
|
|
865
|
-
canExport
|
|
866
|
-
? { name: `${chalk.cyan('↑')} Export`, value: 'export' }
|
|
867
|
-
: disabledItem('⬆', 'Export', exportHint),
|
|
886
|
+
: disabledItem('✕', 'Delete container'),
|
|
868
887
|
)
|
|
869
888
|
|
|
870
889
|
actionChoices.push(new inquirer.Separator())
|
|
@@ -2,6 +2,7 @@ import { Command } from 'commander'
|
|
|
2
2
|
import chalk from 'chalk'
|
|
3
3
|
import inquirer from 'inquirer'
|
|
4
4
|
import { containerManager } from '../../../core/container-manager'
|
|
5
|
+
import { updateManager, type UpdateCheckResult } from '../../../core/update-manager'
|
|
5
6
|
import {
|
|
6
7
|
promptInstallDependencies,
|
|
7
8
|
enableGlobalEscape,
|
|
@@ -9,35 +10,48 @@ import {
|
|
|
9
10
|
escapeablePrompt,
|
|
10
11
|
EscapeError,
|
|
11
12
|
} from '../../ui/prompts'
|
|
12
|
-
import { header, uiError } from '../../ui/theme'
|
|
13
|
+
import { header, uiError, uiSuccess, uiWarning } from '../../ui/theme'
|
|
13
14
|
import { MissingToolError } from '../../../core/error-handler'
|
|
14
|
-
import { hasAnyInstalledEngines } from '../../helpers'
|
|
15
15
|
import {
|
|
16
16
|
handleCreate,
|
|
17
17
|
handleList,
|
|
18
|
-
|
|
19
|
-
handleStop,
|
|
18
|
+
showContainerSubmenu,
|
|
20
19
|
} from './container-handlers'
|
|
21
|
-
import { handleBackup, handleRestore, handleClone } from './backup-handlers'
|
|
22
|
-
import { handleEngines } from './engine-handlers'
|
|
23
|
-
import { handleCheckUpdate, handleDoctor } from './update-handlers'
|
|
24
20
|
import { handleSettings } from './settings-handlers'
|
|
25
21
|
import { configManager } from '../../../core/config-manager'
|
|
26
|
-
import {
|
|
22
|
+
import { createSpinner } from '../../ui/spinner'
|
|
23
|
+
import { type MenuChoice, pressEnterToContinue } from './shared'
|
|
27
24
|
import { getPageSize } from '../../constants'
|
|
28
25
|
|
|
26
|
+
// Track update check state for this session (only check once on first menu load)
|
|
27
|
+
let updateCheckPromise: Promise<UpdateCheckResult | null> | null = null
|
|
28
|
+
let cachedUpdateResult: UpdateCheckResult | null = null
|
|
29
|
+
|
|
29
30
|
async function showMainMenu(): Promise<void> {
|
|
30
31
|
console.clear()
|
|
31
32
|
console.log(header('SpinDB - Local Database Manager'))
|
|
32
33
|
console.log()
|
|
33
34
|
|
|
34
|
-
// Parallelize container list
|
|
35
|
-
const [containers,
|
|
35
|
+
// Parallelize container list and config loading for faster startup
|
|
36
|
+
const [containers, config] = await Promise.all([
|
|
36
37
|
containerManager.list(),
|
|
37
|
-
hasAnyInstalledEngines(),
|
|
38
38
|
configManager.getConfig(),
|
|
39
39
|
])
|
|
40
40
|
|
|
41
|
+
// Check for updates on first menu load only (if auto-check is enabled)
|
|
42
|
+
// The check runs in background and updates cachedUpdateResult when complete
|
|
43
|
+
const autoCheckEnabled = config.update?.autoCheckEnabled !== false
|
|
44
|
+
if (autoCheckEnabled && !updateCheckPromise) {
|
|
45
|
+
// Start update check in background - it will populate cachedUpdateResult when done
|
|
46
|
+
updateCheckPromise = updateManager
|
|
47
|
+
.checkForUpdate()
|
|
48
|
+
.then((result) => {
|
|
49
|
+
cachedUpdateResult = result
|
|
50
|
+
return result
|
|
51
|
+
})
|
|
52
|
+
.catch(() => null)
|
|
53
|
+
}
|
|
54
|
+
|
|
41
55
|
// Check if icon mode preference is set
|
|
42
56
|
const iconModeSet = config.preferences?.iconMode !== undefined
|
|
43
57
|
|
|
@@ -51,12 +65,7 @@ async function showMainMenu(): Promise<void> {
|
|
|
51
65
|
)
|
|
52
66
|
console.log()
|
|
53
67
|
|
|
54
|
-
|
|
55
|
-
const canStop = running > 0
|
|
56
|
-
const canRestore = running > 0
|
|
57
|
-
const canClone = containers.length > 0
|
|
58
|
-
|
|
59
|
-
// If containers exist, show List first; otherwise show Create first
|
|
68
|
+
// If containers exist, show Containers first; otherwise show Create first
|
|
60
69
|
const hasContainers = containers.length > 0
|
|
61
70
|
|
|
62
71
|
const choices: MenuChoice[] = [
|
|
@@ -69,53 +78,18 @@ async function showMainMenu(): Promise<void> {
|
|
|
69
78
|
{ name: `${chalk.green('+')} Create container`, value: 'create' },
|
|
70
79
|
{ name: `${chalk.cyan('◉')} Containers`, value: 'list' },
|
|
71
80
|
]),
|
|
72
|
-
{
|
|
73
|
-
name: canStart
|
|
74
|
-
? `${chalk.green('▶')} Start container`
|
|
75
|
-
: chalk.gray('▶ Start container'),
|
|
76
|
-
value: 'start',
|
|
77
|
-
disabled: canStart ? false : 'No stopped containers',
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
name: canStop
|
|
81
|
-
? `${chalk.red('■')} Stop container`
|
|
82
|
-
: chalk.gray('■ Stop container'),
|
|
83
|
-
value: 'stop',
|
|
84
|
-
disabled: canStop ? false : 'No running containers',
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
name: canRestore
|
|
88
|
-
? `${chalk.magenta('↓')} Backup database`
|
|
89
|
-
: chalk.gray('↓ Backup database'),
|
|
90
|
-
value: 'backup',
|
|
91
|
-
disabled: canRestore ? false : 'No running containers',
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
name: canRestore
|
|
95
|
-
? `${chalk.magenta('↑')} Restore backup`
|
|
96
|
-
: chalk.gray('↑ Restore backup'),
|
|
97
|
-
value: 'restore',
|
|
98
|
-
disabled: canRestore ? false : 'No running containers',
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
name: canClone
|
|
102
|
-
? `${chalk.cyan('◇')} Clone container`
|
|
103
|
-
: chalk.gray('◇ Clone container'),
|
|
104
|
-
value: 'clone',
|
|
105
|
-
disabled: canClone ? false : 'No containers',
|
|
106
|
-
},
|
|
107
81
|
new inquirer.Separator(),
|
|
108
|
-
{
|
|
109
|
-
name: hasEngines
|
|
110
|
-
? `${chalk.magenta('⬢')} Manage engines`
|
|
111
|
-
: chalk.gray('⬢ Manage engines'),
|
|
112
|
-
value: 'engines',
|
|
113
|
-
disabled: hasEngines ? false : 'No engines installed',
|
|
114
|
-
},
|
|
115
|
-
{ name: `${chalk.red.bold('+')} Health check`, value: 'doctor' },
|
|
116
|
-
{ name: `${chalk.cyan('↑')} Check for updates`, value: 'check-update' },
|
|
117
82
|
{ name: `${chalk.yellow('⚙')} Settings`, value: 'settings' },
|
|
118
|
-
|
|
83
|
+
// Show update option if a new version is available (only when auto-check enabled)
|
|
84
|
+
...(cachedUpdateResult?.updateAvailable
|
|
85
|
+
? [
|
|
86
|
+
{
|
|
87
|
+
name: `${chalk.green('↑')} Update to v${cachedUpdateResult.latestVersion}`,
|
|
88
|
+
value: 'update',
|
|
89
|
+
},
|
|
90
|
+
]
|
|
91
|
+
: []),
|
|
92
|
+
{ name: `${chalk.gray('⎋')} Exit`, value: 'exit' },
|
|
119
93
|
new inquirer.Separator(),
|
|
120
94
|
]
|
|
121
95
|
|
|
@@ -150,45 +124,75 @@ async function showMainMenu(): Promise<void> {
|
|
|
150
124
|
}
|
|
151
125
|
|
|
152
126
|
switch (action) {
|
|
153
|
-
case 'create':
|
|
154
|
-
await handleCreate()
|
|
127
|
+
case 'create': {
|
|
128
|
+
const result = await handleCreate()
|
|
129
|
+
// If a container name is returned, navigate to its submenu
|
|
130
|
+
if (result && result !== 'main') {
|
|
131
|
+
await showContainerSubmenu(result, showMainMenu)
|
|
132
|
+
}
|
|
155
133
|
break
|
|
134
|
+
}
|
|
156
135
|
case 'list':
|
|
157
136
|
await handleList(showMainMenu)
|
|
158
137
|
break
|
|
159
|
-
case 'start':
|
|
160
|
-
await handleStart()
|
|
161
|
-
break
|
|
162
|
-
case 'stop':
|
|
163
|
-
await handleStop()
|
|
164
|
-
break
|
|
165
|
-
case 'restore':
|
|
166
|
-
await handleRestore()
|
|
167
|
-
break
|
|
168
|
-
case 'backup':
|
|
169
|
-
await handleBackup()
|
|
170
|
-
break
|
|
171
|
-
case 'clone':
|
|
172
|
-
await handleClone()
|
|
173
|
-
break
|
|
174
|
-
case 'engines':
|
|
175
|
-
await handleEngines()
|
|
176
|
-
break
|
|
177
|
-
case 'doctor':
|
|
178
|
-
await handleDoctor()
|
|
179
|
-
break
|
|
180
|
-
case 'check-update':
|
|
181
|
-
await handleCheckUpdate()
|
|
182
|
-
break
|
|
183
138
|
case 'settings':
|
|
184
139
|
await handleSettings()
|
|
185
140
|
break
|
|
141
|
+
case 'update':
|
|
142
|
+
await handleUpdate()
|
|
143
|
+
break
|
|
186
144
|
case 'exit':
|
|
187
145
|
console.log(chalk.gray('\n Goodbye!\n'))
|
|
188
146
|
process.exit(0)
|
|
189
147
|
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function handleUpdate(): Promise<void> {
|
|
151
|
+
console.clear()
|
|
152
|
+
console.log(header('Update SpinDB'))
|
|
153
|
+
console.log()
|
|
154
|
+
|
|
155
|
+
if (!cachedUpdateResult) {
|
|
156
|
+
console.log(uiError('No update information available'))
|
|
157
|
+
await pressEnterToContinue()
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.log(chalk.gray(` Current version: ${cachedUpdateResult.currentVersion}`))
|
|
162
|
+
console.log(
|
|
163
|
+
chalk.gray(` Latest version: ${chalk.green(cachedUpdateResult.latestVersion)}`),
|
|
164
|
+
)
|
|
165
|
+
console.log()
|
|
166
|
+
|
|
167
|
+
const spinner = createSpinner('Updating spindb...')
|
|
168
|
+
spinner.start()
|
|
169
|
+
|
|
170
|
+
const result = await updateManager.performUpdate()
|
|
171
|
+
|
|
172
|
+
if (result.success) {
|
|
173
|
+
spinner.succeed('Update complete')
|
|
174
|
+
console.log()
|
|
175
|
+
console.log(
|
|
176
|
+
uiSuccess(`Updated from ${result.previousVersion} to ${result.newVersion}`),
|
|
177
|
+
)
|
|
178
|
+
console.log()
|
|
179
|
+
if (result.previousVersion !== result.newVersion) {
|
|
180
|
+
console.log(uiWarning('Please restart spindb to use the new version.'))
|
|
181
|
+
console.log()
|
|
182
|
+
}
|
|
183
|
+
// Clear cached result so the update option disappears
|
|
184
|
+
cachedUpdateResult = null
|
|
185
|
+
updateCheckPromise = null
|
|
186
|
+
} else {
|
|
187
|
+
spinner.fail('Update failed')
|
|
188
|
+
console.log()
|
|
189
|
+
console.log(uiError(result.error || 'Unknown error'))
|
|
190
|
+
console.log()
|
|
191
|
+
const pm = await updateManager.detectPackageManager()
|
|
192
|
+
console.log(chalk.gray(` Manual update: ${updateManager.getInstallCommand(pm)}`))
|
|
193
|
+
}
|
|
190
194
|
|
|
191
|
-
await
|
|
195
|
+
await pressEnterToContinue()
|
|
192
196
|
}
|
|
193
197
|
|
|
194
198
|
export const menuCommand = new Command('menu')
|
|
@@ -4,9 +4,12 @@ import { configManager } from '../../../core/config-manager'
|
|
|
4
4
|
import { updateManager } from '../../../core/update-manager'
|
|
5
5
|
import { escapeablePrompt } from '../../ui/prompts'
|
|
6
6
|
import { header, uiSuccess, uiInfo } from '../../ui/theme'
|
|
7
|
-
import { setCachedIconMode, ENGINE_BRAND_COLORS } from '../../constants'
|
|
7
|
+
import { setCachedIconMode, ENGINE_BRAND_COLORS, getPageSize } from '../../constants'
|
|
8
|
+
import { hasAnyInstalledEngines } from '../../helpers'
|
|
8
9
|
import { Engine, type IconMode } from '../../../types'
|
|
9
10
|
import { type MenuChoice, pressEnterToContinue } from './shared'
|
|
11
|
+
import { handleEngines } from './engine-handlers'
|
|
12
|
+
import { handleCheckUpdate, handleDoctor } from './update-handlers'
|
|
10
13
|
|
|
11
14
|
// Sample engines for icon preview
|
|
12
15
|
const PREVIEW_ENGINES = [
|
|
@@ -178,6 +181,7 @@ async function handleIconModeSettings(): Promise<void> {
|
|
|
178
181
|
name: 'iconMode',
|
|
179
182
|
message: 'Select icon mode:',
|
|
180
183
|
choices,
|
|
184
|
+
pageSize: getPageSize(),
|
|
181
185
|
},
|
|
182
186
|
])
|
|
183
187
|
|
|
@@ -244,6 +248,7 @@ async function handleUpdateCheckSettings(): Promise<void> {
|
|
|
244
248
|
name: 'action',
|
|
245
249
|
message: 'Update check setting:',
|
|
246
250
|
choices,
|
|
251
|
+
pageSize: getPageSize(),
|
|
247
252
|
},
|
|
248
253
|
])
|
|
249
254
|
|
|
@@ -273,9 +278,12 @@ async function handleUpdateCheckSettings(): Promise<void> {
|
|
|
273
278
|
*/
|
|
274
279
|
export async function handleSettings(): Promise<void> {
|
|
275
280
|
while (true) {
|
|
276
|
-
const config = await
|
|
281
|
+
const [config, hasEngines, cached] = await Promise.all([
|
|
282
|
+
configManager.getConfig(),
|
|
283
|
+
hasAnyInstalledEngines(),
|
|
284
|
+
updateManager.getCachedUpdateInfo(),
|
|
285
|
+
])
|
|
277
286
|
const currentIconMode = config.preferences?.iconMode || 'ascii'
|
|
278
|
-
const cached = await updateManager.getCachedUpdateInfo()
|
|
279
287
|
const updateCheckEnabled = cached.autoCheckEnabled !== false
|
|
280
288
|
|
|
281
289
|
console.clear()
|
|
@@ -283,6 +291,16 @@ export async function handleSettings(): Promise<void> {
|
|
|
283
291
|
console.log()
|
|
284
292
|
|
|
285
293
|
const choices: MenuChoice[] = [
|
|
294
|
+
{
|
|
295
|
+
name: hasEngines
|
|
296
|
+
? `${chalk.magenta('⬢')} Manage engines`
|
|
297
|
+
: chalk.gray('⬢ Manage engines'),
|
|
298
|
+
value: 'engines',
|
|
299
|
+
disabled: hasEngines ? false : 'No engines installed',
|
|
300
|
+
},
|
|
301
|
+
{ name: `${chalk.red.bold('+')} Health check`, value: 'doctor' },
|
|
302
|
+
{ name: `${chalk.cyan('↑')} Check for updates`, value: 'check-update' },
|
|
303
|
+
new inquirer.Separator(),
|
|
286
304
|
{
|
|
287
305
|
name: `Icon mode: ${chalk.cyan(currentIconMode)}`,
|
|
288
306
|
value: 'icon-mode',
|
|
@@ -293,7 +311,7 @@ export async function handleSettings(): Promise<void> {
|
|
|
293
311
|
},
|
|
294
312
|
new inquirer.Separator(),
|
|
295
313
|
{
|
|
296
|
-
name: `${chalk.blue('
|
|
314
|
+
name: `${chalk.blue('←')} Back`,
|
|
297
315
|
value: 'back',
|
|
298
316
|
},
|
|
299
317
|
]
|
|
@@ -304,10 +322,20 @@ export async function handleSettings(): Promise<void> {
|
|
|
304
322
|
name: 'action',
|
|
305
323
|
message: 'What would you like to configure?',
|
|
306
324
|
choices,
|
|
325
|
+
pageSize: getPageSize(),
|
|
307
326
|
},
|
|
308
327
|
])
|
|
309
328
|
|
|
310
329
|
switch (action) {
|
|
330
|
+
case 'engines':
|
|
331
|
+
await handleEngines()
|
|
332
|
+
break
|
|
333
|
+
case 'doctor':
|
|
334
|
+
await handleDoctor()
|
|
335
|
+
break
|
|
336
|
+
case 'check-update':
|
|
337
|
+
await handleCheckUpdate()
|
|
338
|
+
break
|
|
311
339
|
case 'icon-mode':
|
|
312
340
|
await handleIconModeSettings()
|
|
313
341
|
break
|
package/cli/commands/restore.ts
CHANGED
|
@@ -380,7 +380,7 @@ export const restoreCommand = new Command('restore')
|
|
|
380
380
|
}
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
-
// Drop existing database
|
|
383
|
+
// Drop existing database (tracking entry stays - we're recreating same name)
|
|
384
384
|
const dropSpinner = createSpinner(
|
|
385
385
|
`Dropping existing database "${databaseName}"...`,
|
|
386
386
|
)
|
|
@@ -388,7 +388,8 @@ export const restoreCommand = new Command('restore')
|
|
|
388
388
|
|
|
389
389
|
try {
|
|
390
390
|
await engine.dropDatabase(config, databaseName)
|
|
391
|
-
|
|
391
|
+
// Don't remove from tracking - the database name stays the same
|
|
392
|
+
// and addDatabase() is idempotent, so tracking remains valid
|
|
392
393
|
dropSpinner.succeed(`Dropped database "${databaseName}"`)
|
|
393
394
|
} catch (dropErr) {
|
|
394
395
|
dropSpinner.fail('Failed to drop database')
|
package/core/docker-exporter.ts
CHANGED
|
@@ -159,9 +159,9 @@ function generateDockerfile(
|
|
|
159
159
|
// Server-based engines check for running status
|
|
160
160
|
const healthcheck = isFileBased
|
|
161
161
|
? `HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
|
|
162
|
-
CMD gosu spindb spindb list --json | grep -q '"engine"
|
|
162
|
+
CMD gosu spindb spindb list --json | grep -q '"engine":.*"${engine}"'`
|
|
163
163
|
: `HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \\
|
|
164
|
-
CMD gosu spindb spindb list --json | grep -q '"status"
|
|
164
|
+
CMD gosu spindb spindb list --json | grep -q '"status":.*"running"'`
|
|
165
165
|
|
|
166
166
|
// Only copy TLS certificates if they were generated
|
|
167
167
|
const copyCerts = useTLS
|
|
@@ -248,6 +248,37 @@ function generateEntrypoint(
|
|
|
248
248
|
): string {
|
|
249
249
|
const isFileBased = isFileBasedEngine(engine)
|
|
250
250
|
|
|
251
|
+
// Engine-specific network configuration (run after create, before start)
|
|
252
|
+
// This configures databases to accept connections from Docker network
|
|
253
|
+
let networkConfig = ''
|
|
254
|
+
|
|
255
|
+
switch (engine) {
|
|
256
|
+
case Engine.PostgreSQL:
|
|
257
|
+
case Engine.CockroachDB:
|
|
258
|
+
// PostgreSQL/CockroachDB need to listen on all interfaces and allow network connections
|
|
259
|
+
networkConfig = `
|
|
260
|
+
# Configure PostgreSQL to accept connections from Docker network
|
|
261
|
+
echo "Configuring network access..."
|
|
262
|
+
PG_CONF="/home/spindb/.spindb/containers/${engine}/\${CONTAINER_NAME}/data/postgresql.conf"
|
|
263
|
+
PG_HBA="/home/spindb/.spindb/containers/${engine}/\${CONTAINER_NAME}/data/pg_hba.conf"
|
|
264
|
+
|
|
265
|
+
# Set listen_addresses to allow connections from any interface
|
|
266
|
+
if [ -f "$PG_CONF" ]; then
|
|
267
|
+
sed -i "s/^#*listen_addresses.*/listen_addresses = '*'/" "$PG_CONF"
|
|
268
|
+
fi
|
|
269
|
+
|
|
270
|
+
# Add rule to allow password-authenticated connections from any IP
|
|
271
|
+
if [ -f "$PG_HBA" ] && ! grep -q "0.0.0.0/0" "$PG_HBA"; then
|
|
272
|
+
echo "host all all 0.0.0.0/0 scram-sha-256" >> "$PG_HBA"
|
|
273
|
+
fi
|
|
274
|
+
`
|
|
275
|
+
break
|
|
276
|
+
|
|
277
|
+
default:
|
|
278
|
+
// Other engines don't need special network config (they listen on all interfaces by default)
|
|
279
|
+
break
|
|
280
|
+
}
|
|
281
|
+
|
|
251
282
|
// Engine-specific user creation commands
|
|
252
283
|
let userCreationCommands = ''
|
|
253
284
|
|
|
@@ -256,7 +287,7 @@ function generateEntrypoint(
|
|
|
256
287
|
userCreationCommands = `
|
|
257
288
|
# Create user with password
|
|
258
289
|
echo "Creating database user..."
|
|
259
|
-
|
|
290
|
+
cat > /tmp/create-user.sql <<EOSQL
|
|
260
291
|
DO \\$\\$
|
|
261
292
|
BEGIN
|
|
262
293
|
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '$SPINDB_USER') THEN
|
|
@@ -267,7 +298,9 @@ BEGIN
|
|
|
267
298
|
END
|
|
268
299
|
\\$\\$;
|
|
269
300
|
GRANT ALL PRIVILEGES ON DATABASE "$DATABASE" TO "$SPINDB_USER";
|
|
270
|
-
|
|
301
|
+
EOSQL
|
|
302
|
+
run_as_spindb spindb run "$CONTAINER_NAME" /tmp/create-user.sql --database postgres
|
|
303
|
+
rm -f /tmp/create-user.sql
|
|
271
304
|
`
|
|
272
305
|
break
|
|
273
306
|
|
|
@@ -276,11 +309,13 @@ EOF
|
|
|
276
309
|
userCreationCommands = `
|
|
277
310
|
# Create user with password
|
|
278
311
|
echo "Creating database user..."
|
|
279
|
-
|
|
312
|
+
cat > /tmp/create-user.sql <<EOSQL
|
|
280
313
|
CREATE USER IF NOT EXISTS '$SPINDB_USER'@'%' IDENTIFIED BY '$SPINDB_PASSWORD';
|
|
281
314
|
GRANT ALL PRIVILEGES ON \\\`$DATABASE\\\`.* TO '$SPINDB_USER'@'%';
|
|
282
315
|
FLUSH PRIVILEGES;
|
|
283
|
-
|
|
316
|
+
EOSQL
|
|
317
|
+
run_as_spindb spindb run "$CONTAINER_NAME" /tmp/create-user.sql --database mysql
|
|
318
|
+
rm -f /tmp/create-user.sql
|
|
284
319
|
`
|
|
285
320
|
break
|
|
286
321
|
|
|
@@ -289,13 +324,15 @@ EOF
|
|
|
289
324
|
userCreationCommands = `
|
|
290
325
|
# Create user with password
|
|
291
326
|
echo "Creating database user..."
|
|
292
|
-
|
|
327
|
+
cat > /tmp/create-user.js <<EOJS
|
|
293
328
|
db.createUser({
|
|
294
329
|
user: "$SPINDB_USER",
|
|
295
330
|
pwd: "$SPINDB_PASSWORD",
|
|
296
331
|
roles: [{ role: "readWrite", db: "$DATABASE" }]
|
|
297
332
|
});
|
|
298
|
-
|
|
333
|
+
EOJS
|
|
334
|
+
run_as_spindb spindb run "$CONTAINER_NAME" /tmp/create-user.js --database admin
|
|
335
|
+
rm -f /tmp/create-user.js
|
|
299
336
|
`
|
|
300
337
|
break
|
|
301
338
|
|
|
@@ -312,10 +349,12 @@ echo "Authentication configured via server settings"
|
|
|
312
349
|
userCreationCommands = `
|
|
313
350
|
# Create user with password
|
|
314
351
|
echo "Creating database user..."
|
|
315
|
-
|
|
352
|
+
cat > /tmp/create-user.sql <<EOSQL
|
|
316
353
|
CREATE USER IF NOT EXISTS $SPINDB_USER IDENTIFIED BY '$SPINDB_PASSWORD';
|
|
317
354
|
GRANT ALL ON $DATABASE.* TO $SPINDB_USER;
|
|
318
|
-
|
|
355
|
+
EOSQL
|
|
356
|
+
run_as_spindb spindb run "$CONTAINER_NAME" /tmp/create-user.sql
|
|
357
|
+
rm -f /tmp/create-user.sql
|
|
319
358
|
`
|
|
320
359
|
break
|
|
321
360
|
|
|
@@ -330,10 +369,12 @@ echo "Admin credentials configured via server settings"
|
|
|
330
369
|
userCreationCommands = `
|
|
331
370
|
# Create user with password
|
|
332
371
|
echo "Creating database user..."
|
|
333
|
-
|
|
372
|
+
cat > /tmp/create-user.sql <<EOSQL
|
|
334
373
|
CREATE USER IF NOT EXISTS $SPINDB_USER WITH PASSWORD '$SPINDB_PASSWORD';
|
|
335
374
|
GRANT ALL ON DATABASE $DATABASE TO $SPINDB_USER;
|
|
336
|
-
|
|
375
|
+
EOSQL
|
|
376
|
+
run_as_spindb spindb run "$CONTAINER_NAME" /tmp/create-user.sql --database defaultdb
|
|
377
|
+
rm -f /tmp/create-user.sql
|
|
337
378
|
`
|
|
338
379
|
break
|
|
339
380
|
|
|
@@ -411,6 +452,40 @@ if ls ${initDir}/* 1> /dev/null 2>&1; then
|
|
|
411
452
|
fi
|
|
412
453
|
`
|
|
413
454
|
|
|
455
|
+
// Post-restore commands - grant table/sequence permissions to the spindb user
|
|
456
|
+
// Tables created during restore are owned by postgres, so spindb user needs grants
|
|
457
|
+
let postRestoreCommands = ''
|
|
458
|
+
|
|
459
|
+
switch (engine) {
|
|
460
|
+
case Engine.PostgreSQL:
|
|
461
|
+
case Engine.CockroachDB:
|
|
462
|
+
postRestoreCommands = `
|
|
463
|
+
# Grant table and sequence permissions to spindb user
|
|
464
|
+
# (Tables from restore are owned by postgres, spindb user needs access)
|
|
465
|
+
echo "Granting table permissions..."
|
|
466
|
+
cat > /tmp/grant-permissions.sql <<EOSQL
|
|
467
|
+
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "$SPINDB_USER";
|
|
468
|
+
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO "$SPINDB_USER";
|
|
469
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO "$SPINDB_USER";
|
|
470
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO "$SPINDB_USER";
|
|
471
|
+
EOSQL
|
|
472
|
+
run_as_spindb spindb run "$CONTAINER_NAME" /tmp/grant-permissions.sql --database "$DATABASE" || echo "Permission grants completed with warnings"
|
|
473
|
+
rm -f /tmp/grant-permissions.sql
|
|
474
|
+
`
|
|
475
|
+
break
|
|
476
|
+
|
|
477
|
+
case Engine.MySQL:
|
|
478
|
+
case Engine.MariaDB:
|
|
479
|
+
// MySQL grants are already handled by GRANT ALL ON database.* in user creation
|
|
480
|
+
postRestoreCommands = ''
|
|
481
|
+
break
|
|
482
|
+
|
|
483
|
+
default:
|
|
484
|
+
// Other engines don't need post-restore permission grants
|
|
485
|
+
postRestoreCommands = ''
|
|
486
|
+
break
|
|
487
|
+
}
|
|
488
|
+
|
|
414
489
|
return `#!/bin/bash
|
|
415
490
|
set -e
|
|
416
491
|
|
|
@@ -442,6 +517,9 @@ FILE_DB_PATH="/home/spindb/.spindb/containers/${engine}/\${CONTAINER_NAME}/\${CO
|
|
|
442
517
|
# Export environment variables for the spindb user
|
|
443
518
|
export SPINDB_CONTAINER SPINDB_DATABASE SPINDB_ENGINE SPINDB_VERSION SPINDB_PORT SPINDB_USER SPINDB_PASSWORD
|
|
444
519
|
|
|
520
|
+
# Add ~/.local/bin to PATH for symlinked database binaries
|
|
521
|
+
export PATH="/home/spindb/.local/bin:$PATH"
|
|
522
|
+
|
|
445
523
|
# Fix permissions on mounted volume (may have been created with root ownership)
|
|
446
524
|
echo "Setting up directories..."
|
|
447
525
|
chown -R spindb:spindb /home/spindb/.spindb 2>/dev/null || true
|
|
@@ -472,12 +550,27 @@ else
|
|
|
472
550
|
: `run_as_spindb spindb create "$CONTAINER_NAME" --engine "$ENGINE" --db-version "$VERSION" --port "$PORT" --database "$DATABASE" --force`
|
|
473
551
|
}
|
|
474
552
|
fi
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
553
|
+
|
|
554
|
+
# Create symlinks for database binaries in ~/.local/bin
|
|
555
|
+
# This allows users to run psql, mysql, etc. directly in the container
|
|
556
|
+
echo "Creating binary symlinks..."
|
|
557
|
+
mkdir -p /home/spindb/.local/bin
|
|
558
|
+
BIN_DIR=$(ls -d /home/spindb/.spindb/bin/\${ENGINE}-*/bin 2>/dev/null | head -1)
|
|
559
|
+
if [ -d "$BIN_DIR" ]; then
|
|
560
|
+
for binary in "$BIN_DIR"/*; do
|
|
561
|
+
if [ -x "$binary" ] && [ -f "$binary" ]; then
|
|
562
|
+
name=$(basename "$binary")
|
|
563
|
+
ln -sf "$binary" "/home/spindb/.local/bin/$name"
|
|
564
|
+
fi
|
|
565
|
+
done
|
|
566
|
+
echo "Binaries available: $(ls /home/spindb/.local/bin | tr '\\n' ' ')"
|
|
567
|
+
fi
|
|
568
|
+
${networkConfig}${
|
|
569
|
+
isFileBased
|
|
570
|
+
? `
|
|
478
571
|
# File-based database: no server to start, just verify file exists after restore
|
|
479
572
|
`
|
|
480
|
-
|
|
573
|
+
: `
|
|
481
574
|
# Start the database
|
|
482
575
|
echo "Starting database..."
|
|
483
576
|
run_as_spindb spindb start "$CONTAINER_NAME"
|
|
@@ -485,7 +578,7 @@ run_as_spindb spindb start "$CONTAINER_NAME"
|
|
|
485
578
|
# Wait for database to be ready
|
|
486
579
|
echo "Waiting for database to be ready..."
|
|
487
580
|
RETRIES=30
|
|
488
|
-
until run_as_spindb spindb list --json 2>/dev/null | grep -q '"status"
|
|
581
|
+
until run_as_spindb spindb list --json 2>/dev/null | grep -q '"status":.*"running"' || [ $RETRIES -eq 0 ]; do
|
|
489
582
|
echo "Waiting for database... ($RETRIES attempts remaining)"
|
|
490
583
|
sleep 2
|
|
491
584
|
RETRIES=$((RETRIES-1))
|
|
@@ -495,11 +588,12 @@ if [ $RETRIES -eq 0 ]; then
|
|
|
495
588
|
echo "Error: Database failed to start"
|
|
496
589
|
exit 1
|
|
497
590
|
fi`
|
|
498
|
-
}
|
|
591
|
+
}
|
|
499
592
|
|
|
500
593
|
echo "Database is running!"
|
|
501
594
|
${userCreationCommands}
|
|
502
595
|
${restoreSection}
|
|
596
|
+
${postRestoreCommands}
|
|
503
597
|
echo "========================================"
|
|
504
598
|
echo "SpinDB container ready!"
|
|
505
599
|
echo ""
|
|
@@ -520,7 +614,7 @@ exec gosu spindb tail -f /dev/null &
|
|
|
520
614
|
while true; do
|
|
521
615
|
sleep 60
|
|
522
616
|
# Check if database is still running
|
|
523
|
-
if ! run_as_spindb spindb list --json 2>/dev/null | grep -q '"status"
|
|
617
|
+
if ! run_as_spindb spindb list --json 2>/dev/null | grep -q '"status":.*"running"'; then
|
|
524
618
|
echo "Database stopped unexpectedly, restarting..."
|
|
525
619
|
run_as_spindb spindb start "$CONTAINER_NAME" || true
|
|
526
620
|
fi
|
|
@@ -543,9 +637,10 @@ function generateDockerCompose(
|
|
|
543
637
|
// Engine-aware healthcheck matching Dockerfile behavior
|
|
544
638
|
// Server-based: check for running status
|
|
545
639
|
// File-based: check that container exists (no server process to check)
|
|
640
|
+
// Note: Double quotes must be escaped for YAML string context
|
|
546
641
|
const healthcheckCommand = isFileBased
|
|
547
|
-
? `gosu spindb spindb list --json | grep -q '"engine"
|
|
548
|
-
: `gosu spindb spindb list --json | grep -q '"status"
|
|
642
|
+
? `gosu spindb spindb list --json | grep -q '\\"engine\\":.*\\"${engine}\\"'`
|
|
643
|
+
: `gosu spindb spindb list --json | grep -q '\\"status\\":.*\\"running\\"'`
|
|
549
644
|
|
|
550
645
|
const startPeriod = isFileBased ? '30s' : '60s'
|
|
551
646
|
|
|
@@ -626,6 +721,29 @@ function generateReadme(
|
|
|
626
721
|
useTLS,
|
|
627
722
|
)
|
|
628
723
|
|
|
724
|
+
// TLS-conditional content
|
|
725
|
+
const tlsSecurityNote = useTLS
|
|
726
|
+
? '- TLS certificates in `certs/` are self-signed. For production, replace with valid certificates.'
|
|
727
|
+
: '- TLS is disabled for this export. Consider enabling TLS for production use.'
|
|
728
|
+
const certsFileEntry = useTLS ? '| `certs/` | TLS certificates |\n' : ''
|
|
729
|
+
const tlsCustomization = useTLS
|
|
730
|
+
? `
|
|
731
|
+
### Use Custom Certificates
|
|
732
|
+
|
|
733
|
+
Replace the files in \`certs/\`:
|
|
734
|
+
- \`server.crt\` - TLS certificate
|
|
735
|
+
- \`server.key\` - TLS private key
|
|
736
|
+
|
|
737
|
+
### Disable TLS
|
|
738
|
+
|
|
739
|
+
Edit \`entrypoint.sh\` and remove TLS-related flags (not recommended for production).
|
|
740
|
+
`
|
|
741
|
+
: `
|
|
742
|
+
### Enable TLS
|
|
743
|
+
|
|
744
|
+
To enable TLS, re-export the container without the \`--skip-tls\` flag (requires OpenSSL).
|
|
745
|
+
`
|
|
746
|
+
|
|
629
747
|
return `# ${containerName} - SpinDB Docker Export
|
|
630
748
|
|
|
631
749
|
This directory contains a Docker-ready package for running your SpinDB ${displayName} container.
|
|
@@ -664,7 +782,7 @@ Replace \`\${SPINDB_USER}\` and \`\${SPINDB_PASSWORD}\` with the values from \`.
|
|
|
664
782
|
## Security Notes
|
|
665
783
|
|
|
666
784
|
- The \`.env\` file contains auto-generated credentials. **Change these in production.**
|
|
667
|
-
|
|
785
|
+
${tlsSecurityNote}
|
|
668
786
|
- The default \`spindb\` user has full access to the database. Create restricted users for applications.
|
|
669
787
|
|
|
670
788
|
## Files
|
|
@@ -675,8 +793,7 @@ Replace \`\${SPINDB_USER}\` and \`\${SPINDB_PASSWORD}\` with the values from \`.
|
|
|
675
793
|
| \`docker-compose.yml\` | Container orchestration |
|
|
676
794
|
| \`.env\` | Environment variables and credentials |
|
|
677
795
|
| \`entrypoint.sh\` | Container startup script |
|
|
678
|
-
| \`
|
|
679
|
-
| \`data/\` | Database backup for initialization |
|
|
796
|
+
${certsFileEntry}| \`data/\` | Database backup for initialization |
|
|
680
797
|
|
|
681
798
|
## Customization
|
|
682
799
|
|
|
@@ -686,17 +803,7 @@ Edit \`.env\`:
|
|
|
686
803
|
\`\`\`
|
|
687
804
|
PORT=5433
|
|
688
805
|
\`\`\`
|
|
689
|
-
|
|
690
|
-
### Use Custom Certificates
|
|
691
|
-
|
|
692
|
-
Replace the files in \`certs/\`:
|
|
693
|
-
- \`server.crt\` - TLS certificate
|
|
694
|
-
- \`server.key\` - TLS private key
|
|
695
|
-
|
|
696
|
-
### Disable TLS
|
|
697
|
-
|
|
698
|
-
Edit \`entrypoint.sh\` and remove TLS-related flags (not recommended for production).
|
|
699
|
-
|
|
806
|
+
${tlsCustomization}
|
|
700
807
|
---
|
|
701
808
|
|
|
702
809
|
Generated by [SpinDB](https://github.com/robertjbass/spindb)
|
|
@@ -731,15 +838,16 @@ export async function exportToDocker(
|
|
|
731
838
|
const outputDirExisted = existsSync(outputDir)
|
|
732
839
|
|
|
733
840
|
if (outputDirExisted) {
|
|
734
|
-
// If it exists, check if it's empty
|
|
841
|
+
// If it exists, check if it's empty or only contains 'data' (from backup step)
|
|
735
842
|
const existingFiles = await readdir(outputDir)
|
|
736
|
-
|
|
843
|
+
const nonDataFiles = existingFiles.filter((f) => f !== 'data')
|
|
844
|
+
if (nonDataFiles.length > 0) {
|
|
737
845
|
throw new Error(
|
|
738
846
|
`Output directory "${outputDir}" already exists and is not empty. ` +
|
|
739
847
|
`Please use an empty directory or remove the existing files.`,
|
|
740
848
|
)
|
|
741
849
|
}
|
|
742
|
-
// Directory exists but is empty - don't register rollback for it
|
|
850
|
+
// Directory exists but is empty or only has data/ - don't register rollback for it
|
|
743
851
|
} else {
|
|
744
852
|
// Directory doesn't exist - create it and register rollback
|
|
745
853
|
await mkdir(outputDir, { recursive: true })
|
package/core/update-manager.ts
CHANGED
|
@@ -80,6 +80,7 @@ export class UpdateManager {
|
|
|
80
80
|
try {
|
|
81
81
|
const { stdout } = await execAsync('pnpm list -g spindb --json', {
|
|
82
82
|
timeout: 5000,
|
|
83
|
+
cwd: '/',
|
|
83
84
|
})
|
|
84
85
|
const data = JSON.parse(stdout) as Array<{
|
|
85
86
|
dependencies?: { spindb?: unknown }
|
|
@@ -94,6 +95,7 @@ export class UpdateManager {
|
|
|
94
95
|
try {
|
|
95
96
|
const { stdout } = await execAsync('yarn global list --json', {
|
|
96
97
|
timeout: 5000,
|
|
98
|
+
cwd: '/',
|
|
97
99
|
})
|
|
98
100
|
// yarn outputs newline-delimited JSON, look for spindb in any line
|
|
99
101
|
if (stdout.includes('"spindb@')) {
|
|
@@ -106,6 +108,7 @@ export class UpdateManager {
|
|
|
106
108
|
try {
|
|
107
109
|
const { stdout } = await execAsync('bun pm ls -g', {
|
|
108
110
|
timeout: 5000,
|
|
111
|
+
cwd: '/',
|
|
109
112
|
})
|
|
110
113
|
if (stdout.includes('spindb@')) {
|
|
111
114
|
return 'bun'
|
|
@@ -181,7 +184,7 @@ export class UpdateManager {
|
|
|
181
184
|
|
|
182
185
|
// Run install command
|
|
183
186
|
try {
|
|
184
|
-
await execAsync(installCmd, { timeout: 60000 })
|
|
187
|
+
await execAsync(installCmd, { timeout: 60000, cwd: '/' })
|
|
185
188
|
} catch (error) {
|
|
186
189
|
const message = error instanceof Error ? error.message : String(error)
|
|
187
190
|
|
package/engines/qdrant/index.ts
CHANGED
|
@@ -710,9 +710,10 @@ export class QdrantEngine extends BaseEngine {
|
|
|
710
710
|
}
|
|
711
711
|
|
|
712
712
|
// Wait for process to fully terminate
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
713
|
+
// Windows needs longer due to file handle release
|
|
714
|
+
// Linux/macOS need a brief wait after SIGKILL before checking ports
|
|
715
|
+
const terminationWait = isWindows() ? 3000 : 1000
|
|
716
|
+
await new Promise((resolve) => setTimeout(resolve, terminationWait))
|
|
716
717
|
|
|
717
718
|
// Kill any processes still listening on the ports
|
|
718
719
|
// This handles cases where the PID file is stale, child processes exist,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spindb",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.7",
|
|
4
4
|
"author": "Bob Bass <bob@bbass.co>",
|
|
5
5
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
6
6
|
"description": "Zero-config Docker-free local database containers. Create, backup, and clone a variety of popular databases.",
|