spindb 0.8.2 → 0.9.1

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.
Files changed (40) hide show
  1. package/README.md +87 -7
  2. package/cli/commands/clone.ts +6 -0
  3. package/cli/commands/connect.ts +115 -14
  4. package/cli/commands/create.ts +170 -8
  5. package/cli/commands/doctor.ts +320 -0
  6. package/cli/commands/edit.ts +209 -9
  7. package/cli/commands/engines.ts +34 -3
  8. package/cli/commands/info.ts +81 -26
  9. package/cli/commands/list.ts +64 -9
  10. package/cli/commands/logs.ts +9 -3
  11. package/cli/commands/menu/backup-handlers.ts +52 -21
  12. package/cli/commands/menu/container-handlers.ts +433 -127
  13. package/cli/commands/menu/engine-handlers.ts +128 -4
  14. package/cli/commands/menu/index.ts +5 -1
  15. package/cli/commands/menu/shell-handlers.ts +105 -21
  16. package/cli/commands/menu/sql-handlers.ts +16 -4
  17. package/cli/commands/menu/update-handlers.ts +278 -0
  18. package/cli/commands/restore.ts +83 -23
  19. package/cli/commands/run.ts +27 -11
  20. package/cli/commands/url.ts +17 -9
  21. package/cli/constants.ts +1 -0
  22. package/cli/helpers.ts +41 -1
  23. package/cli/index.ts +2 -0
  24. package/cli/ui/prompts.ts +148 -7
  25. package/config/engine-defaults.ts +14 -0
  26. package/config/os-dependencies.ts +66 -0
  27. package/config/paths.ts +8 -0
  28. package/core/container-manager.ts +191 -32
  29. package/core/dependency-manager.ts +18 -0
  30. package/core/error-handler.ts +31 -0
  31. package/core/port-manager.ts +2 -0
  32. package/core/process-manager.ts +25 -3
  33. package/engines/index.ts +4 -0
  34. package/engines/mysql/backup.ts +53 -36
  35. package/engines/mysql/index.ts +48 -5
  36. package/engines/postgresql/index.ts +6 -0
  37. package/engines/sqlite/index.ts +606 -0
  38. package/engines/sqlite/registry.ts +185 -0
  39. package/package.json +1 -1
  40. package/types/index.ts +26 -0
@@ -0,0 +1,185 @@
1
+ /**
2
+ * SQLite Registry Manager
3
+ *
4
+ * Unlike PostgreSQL/MySQL which store containers in ~/.spindb/containers/,
5
+ * SQLite databases are stored in user project directories. This registry
6
+ * tracks the file paths of all SQLite databases managed by SpinDB.
7
+ */
8
+
9
+ import { existsSync } from 'fs'
10
+ import { readFile, writeFile, mkdir } from 'fs/promises'
11
+ import { dirname } from 'path'
12
+ import { paths } from '../../config/paths'
13
+ import type { SQLiteRegistry, SQLiteRegistryEntry } from '../../types'
14
+
15
+ /**
16
+ * SQLite Registry Manager
17
+ * Manages the JSON registry that tracks external SQLite database files
18
+ */
19
+ class SQLiteRegistryManager {
20
+ private registryPath: string
21
+
22
+ constructor() {
23
+ this.registryPath = paths.getSqliteRegistryPath()
24
+ }
25
+
26
+ /**
27
+ * Load the registry from disk
28
+ * Returns an empty registry if the file doesn't exist
29
+ */
30
+ async load(): Promise<SQLiteRegistry> {
31
+ if (!existsSync(this.registryPath)) {
32
+ return { version: 1, entries: [] }
33
+ }
34
+ try {
35
+ const content = await readFile(this.registryPath, 'utf8')
36
+ return JSON.parse(content) as SQLiteRegistry
37
+ } catch {
38
+ // If file is corrupted, return empty registry
39
+ return { version: 1, entries: [] }
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Save the registry to disk
45
+ * Creates the parent directory if it doesn't exist
46
+ */
47
+ async save(registry: SQLiteRegistry): Promise<void> {
48
+ const dir = dirname(this.registryPath)
49
+ if (!existsSync(dir)) {
50
+ await mkdir(dir, { recursive: true })
51
+ }
52
+ await writeFile(this.registryPath, JSON.stringify(registry, null, 2))
53
+ }
54
+
55
+ /**
56
+ * Add a new entry to the registry
57
+ * @throws Error if a container with the same name already exists
58
+ */
59
+ async add(entry: SQLiteRegistryEntry): Promise<void> {
60
+ const registry = await this.load()
61
+
62
+ // Check for duplicate name
63
+ if (registry.entries.some((e) => e.name === entry.name)) {
64
+ throw new Error(`SQLite container "${entry.name}" already exists`)
65
+ }
66
+
67
+ registry.entries.push(entry)
68
+ await this.save(registry)
69
+ }
70
+
71
+ /**
72
+ * Get an entry by name
73
+ * Returns null if not found
74
+ */
75
+ async get(name: string): Promise<SQLiteRegistryEntry | null> {
76
+ const registry = await this.load()
77
+ return registry.entries.find((e) => e.name === name) || null
78
+ }
79
+
80
+ /**
81
+ * Remove an entry by name
82
+ * Returns true if the entry was found and removed, false otherwise
83
+ */
84
+ async remove(name: string): Promise<boolean> {
85
+ const registry = await this.load()
86
+ const index = registry.entries.findIndex((e) => e.name === name)
87
+
88
+ if (index === -1) {
89
+ return false
90
+ }
91
+
92
+ registry.entries.splice(index, 1)
93
+ await this.save(registry)
94
+ return true
95
+ }
96
+
97
+ /**
98
+ * Update an existing entry
99
+ * Returns true if the entry was found and updated, false otherwise
100
+ */
101
+ async update(
102
+ name: string,
103
+ updates: Partial<Omit<SQLiteRegistryEntry, 'name'>>,
104
+ ): Promise<boolean> {
105
+ const registry = await this.load()
106
+ const entry = registry.entries.find((e) => e.name === name)
107
+
108
+ if (!entry) {
109
+ return false
110
+ }
111
+
112
+ Object.assign(entry, updates)
113
+ await this.save(registry)
114
+ return true
115
+ }
116
+
117
+ /**
118
+ * List all entries in the registry
119
+ */
120
+ async list(): Promise<SQLiteRegistryEntry[]> {
121
+ const registry = await this.load()
122
+ return registry.entries
123
+ }
124
+
125
+ /**
126
+ * Check if a container with the given name exists
127
+ */
128
+ async exists(name: string): Promise<boolean> {
129
+ const entry = await this.get(name)
130
+ return entry !== null
131
+ }
132
+
133
+ /**
134
+ * Find orphaned entries (where the file no longer exists)
135
+ */
136
+ async findOrphans(): Promise<SQLiteRegistryEntry[]> {
137
+ const registry = await this.load()
138
+ return registry.entries.filter((e) => !existsSync(e.filePath))
139
+ }
140
+
141
+ /**
142
+ * Remove all orphaned entries from the registry
143
+ * Returns the number of entries removed
144
+ */
145
+ async removeOrphans(): Promise<number> {
146
+ const registry = await this.load()
147
+ const originalCount = registry.entries.length
148
+
149
+ registry.entries = registry.entries.filter((e) => existsSync(e.filePath))
150
+
151
+ const removedCount = originalCount - registry.entries.length
152
+ if (removedCount > 0) {
153
+ await this.save(registry)
154
+ }
155
+
156
+ return removedCount
157
+ }
158
+
159
+ /**
160
+ * Update the lastVerified timestamp for an entry
161
+ */
162
+ async updateVerified(name: string): Promise<void> {
163
+ await this.update(name, { lastVerified: new Date().toISOString() })
164
+ }
165
+
166
+ /**
167
+ * Check if a file path is already registered (by any container)
168
+ */
169
+ async isPathRegistered(filePath: string): Promise<boolean> {
170
+ const registry = await this.load()
171
+ return registry.entries.some((e) => e.filePath === filePath)
172
+ }
173
+
174
+ /**
175
+ * Get the container name for a given file path
176
+ * Returns null if not found
177
+ */
178
+ async getByPath(filePath: string): Promise<SQLiteRegistryEntry | null> {
179
+ const registry = await this.load()
180
+ return registry.entries.find((e) => e.filePath === filePath) || null
181
+ }
182
+ }
183
+
184
+ // Export singleton instance
185
+ export const sqliteRegistry = new SQLiteRegistryManager()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spindb",
3
- "version": "0.8.2",
3
+ "version": "0.9.1",
4
4
  "description": "Spin up local database containers without Docker. A DBngin-like CLI for PostgreSQL and MySQL.",
5
5
  "type": "module",
6
6
  "bin": {
package/types/index.ts CHANGED
@@ -17,6 +17,7 @@ export type ContainerConfig = {
17
17
  export enum Engine {
18
18
  PostgreSQL = 'postgresql',
19
19
  MySQL = 'mysql',
20
+ SQLite = 'sqlite',
20
21
  }
21
22
 
22
23
  export type ProgressCallback = (progress: {
@@ -100,9 +101,12 @@ export type BinaryTool =
100
101
  | 'mysqlpump'
101
102
  | 'mysqld'
102
103
  | 'mysqladmin'
104
+ // SQLite tools
105
+ | 'sqlite3'
103
106
  // Enhanced shells (optional)
104
107
  | 'pgcli'
105
108
  | 'mycli'
109
+ | 'litecli'
106
110
  | 'usql'
107
111
 
108
112
  /**
@@ -137,9 +141,12 @@ export type SpinDBConfig = {
137
141
  mysqlpump?: BinaryConfig
138
142
  mysqld?: BinaryConfig
139
143
  mysqladmin?: BinaryConfig
144
+ // SQLite tools
145
+ sqlite3?: BinaryConfig
140
146
  // Enhanced shells (optional)
141
147
  pgcli?: BinaryConfig
142
148
  mycli?: BinaryConfig
149
+ litecli?: BinaryConfig
143
150
  usql?: BinaryConfig
144
151
  }
145
152
  // Default settings
@@ -157,3 +164,22 @@ export type SpinDBConfig = {
157
164
  autoCheckEnabled?: boolean // Default true, user can disable
158
165
  }
159
166
  }
167
+
168
+ /**
169
+ * SQLite registry entry - tracks external database files
170
+ * Unlike PostgreSQL/MySQL, SQLite databases are stored in user project directories
171
+ */
172
+ export type SQLiteRegistryEntry = {
173
+ name: string // Container name (used in spindb commands)
174
+ filePath: string // Absolute path to .sqlite file
175
+ created: string // ISO timestamp
176
+ lastVerified?: string // ISO timestamp of last existence check
177
+ }
178
+
179
+ /**
180
+ * SQLite registry stored at ~/.spindb/sqlite-registry.json
181
+ */
182
+ export type SQLiteRegistry = {
183
+ version: 1
184
+ entries: SQLiteRegistryEntry[]
185
+ }