spindb 0.8.2 → 0.9.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 +80 -7
- package/cli/commands/connect.ts +115 -14
- package/cli/commands/create.ts +113 -1
- package/cli/commands/doctor.ts +319 -0
- package/cli/commands/edit.ts +203 -5
- package/cli/commands/info.ts +79 -26
- package/cli/commands/list.ts +64 -9
- package/cli/commands/menu/backup-handlers.ts +26 -13
- package/cli/commands/menu/container-handlers.ts +408 -120
- package/cli/commands/menu/index.ts +5 -1
- package/cli/commands/menu/shell-handlers.ts +105 -21
- package/cli/commands/menu/sql-handlers.ts +16 -4
- package/cli/commands/menu/update-handlers.ts +278 -0
- package/cli/commands/run.ts +27 -11
- package/cli/commands/url.ts +17 -9
- package/cli/constants.ts +1 -0
- package/cli/index.ts +2 -0
- package/cli/ui/prompts.ts +140 -5
- package/config/engine-defaults.ts +14 -0
- package/config/os-dependencies.ts +66 -0
- package/config/paths.ts +8 -0
- package/core/container-manager.ts +119 -11
- package/core/dependency-manager.ts +18 -0
- package/engines/index.ts +4 -0
- package/engines/sqlite/index.ts +597 -0
- package/engines/sqlite/registry.ts +185 -0
- package/package.json +1 -1
- 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
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
|
+
}
|