spindb 0.9.1 → 0.9.3
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 +5 -8
- package/cli/commands/attach.ts +108 -0
- package/cli/commands/backup.ts +13 -11
- package/cli/commands/clone.ts +14 -10
- package/cli/commands/config.ts +29 -29
- package/cli/commands/connect.ts +51 -39
- package/cli/commands/create.ts +65 -32
- package/cli/commands/delete.ts +8 -8
- package/cli/commands/deps.ts +17 -15
- package/cli/commands/detach.ts +100 -0
- package/cli/commands/doctor.ts +27 -13
- package/cli/commands/edit.ts +120 -57
- package/cli/commands/engines.ts +17 -15
- package/cli/commands/info.ts +8 -6
- package/cli/commands/list.ts +127 -18
- package/cli/commands/logs.ts +15 -11
- package/cli/commands/menu/backup-handlers.ts +52 -47
- package/cli/commands/menu/container-handlers.ts +164 -79
- package/cli/commands/menu/engine-handlers.ts +21 -11
- package/cli/commands/menu/index.ts +4 -4
- package/cli/commands/menu/shell-handlers.ts +34 -31
- package/cli/commands/menu/sql-handlers.ts +22 -16
- package/cli/commands/menu/update-handlers.ts +19 -17
- package/cli/commands/restore.ts +22 -20
- package/cli/commands/run.ts +20 -18
- package/cli/commands/self-update.ts +5 -5
- package/cli/commands/sqlite.ts +247 -0
- package/cli/commands/start.ts +11 -9
- package/cli/commands/stop.ts +9 -9
- package/cli/commands/url.ts +12 -9
- package/cli/helpers.ts +9 -4
- package/cli/index.ts +6 -0
- package/cli/ui/prompts.ts +12 -5
- package/cli/ui/spinner.ts +4 -4
- package/cli/ui/theme.ts +4 -4
- package/config/paths.ts +0 -8
- package/core/binary-manager.ts +5 -1
- package/core/config-manager.ts +32 -0
- package/core/container-manager.ts +5 -5
- package/core/platform-service.ts +3 -3
- package/core/start-with-retry.ts +6 -6
- package/core/transaction-manager.ts +6 -6
- package/engines/mysql/backup.ts +37 -13
- package/engines/mysql/index.ts +11 -11
- package/engines/mysql/restore.ts +4 -4
- package/engines/mysql/version-validator.ts +2 -2
- package/engines/postgresql/binary-manager.ts +17 -17
- package/engines/postgresql/index.ts +7 -2
- package/engines/postgresql/restore.ts +2 -2
- package/engines/postgresql/version-validator.ts +2 -2
- package/engines/sqlite/index.ts +30 -15
- package/engines/sqlite/registry.ts +64 -33
- package/engines/sqlite/scanner.ts +99 -0
- package/package.json +4 -3
- package/types/index.ts +21 -1
package/cli/commands/deps.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import chalk from 'chalk'
|
|
3
|
-
import { header,
|
|
3
|
+
import { header, uiSuccess, uiWarning, uiError } from '../ui/theme'
|
|
4
4
|
import { createSpinner } from '../ui/spinner'
|
|
5
5
|
import {
|
|
6
6
|
detectPackageManager,
|
|
@@ -84,7 +84,7 @@ depsCommand
|
|
|
84
84
|
// Check specific engine
|
|
85
85
|
const engineConfig = getEngineDependencies(options.engine)
|
|
86
86
|
if (!engineConfig) {
|
|
87
|
-
console.error(
|
|
87
|
+
console.error(uiError(`Unknown engine: ${options.engine}`))
|
|
88
88
|
console.log(
|
|
89
89
|
chalk.gray(
|
|
90
90
|
` Available engines: ${engineDependencies.map((e) => e.engine).join(', ')}`,
|
|
@@ -104,9 +104,9 @@ depsCommand
|
|
|
104
104
|
const total = statuses.length
|
|
105
105
|
console.log()
|
|
106
106
|
if (installed === total) {
|
|
107
|
-
console.log(
|
|
107
|
+
console.log(uiSuccess(`All ${total} dependencies installed`))
|
|
108
108
|
} else {
|
|
109
|
-
console.log(
|
|
109
|
+
console.log(uiWarning(`${installed}/${total} dependencies installed`))
|
|
110
110
|
console.log()
|
|
111
111
|
console.log(
|
|
112
112
|
chalk.gray(` Run: spindb deps install --engine ${options.engine}`),
|
|
@@ -132,7 +132,7 @@ depsCommand
|
|
|
132
132
|
const packageManager = await detectPackageManager()
|
|
133
133
|
|
|
134
134
|
if (!packageManager) {
|
|
135
|
-
console.log(
|
|
135
|
+
console.log(uiError('No supported package manager detected'))
|
|
136
136
|
console.log()
|
|
137
137
|
|
|
138
138
|
const platform = getCurrentPlatform()
|
|
@@ -161,7 +161,7 @@ depsCommand
|
|
|
161
161
|
const missing = await getAllMissingDependencies()
|
|
162
162
|
|
|
163
163
|
if (missing.length === 0) {
|
|
164
|
-
console.log(
|
|
164
|
+
console.log(uiSuccess('All dependencies are already installed'))
|
|
165
165
|
return
|
|
166
166
|
}
|
|
167
167
|
|
|
@@ -182,14 +182,14 @@ depsCommand
|
|
|
182
182
|
spinner.warn('Some dependencies failed to install')
|
|
183
183
|
console.log()
|
|
184
184
|
for (const f of failed) {
|
|
185
|
-
console.log(
|
|
185
|
+
console.log(uiError(` ${f.dependency.name}: ${f.error}`))
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
if (succeeded.length > 0) {
|
|
190
190
|
console.log()
|
|
191
191
|
console.log(
|
|
192
|
-
|
|
192
|
+
uiSuccess(
|
|
193
193
|
`Installed: ${succeeded.map((r) => r.dependency.name).join(', ')}`,
|
|
194
194
|
),
|
|
195
195
|
)
|
|
@@ -198,7 +198,7 @@ depsCommand
|
|
|
198
198
|
// Install dependencies for specific engine
|
|
199
199
|
const engineConfig = getEngineDependencies(options.engine)
|
|
200
200
|
if (!engineConfig) {
|
|
201
|
-
console.error(
|
|
201
|
+
console.error(uiError(`Unknown engine: ${options.engine}`))
|
|
202
202
|
console.log(
|
|
203
203
|
chalk.gray(
|
|
204
204
|
` Available engines: ${engineDependencies.map((e) => e.engine).join(', ')}`,
|
|
@@ -211,7 +211,9 @@ depsCommand
|
|
|
211
211
|
|
|
212
212
|
if (missing.length === 0) {
|
|
213
213
|
console.log(
|
|
214
|
-
|
|
214
|
+
uiSuccess(
|
|
215
|
+
`All ${engineConfig.displayName} dependencies are installed`,
|
|
216
|
+
),
|
|
215
217
|
)
|
|
216
218
|
return
|
|
217
219
|
}
|
|
@@ -241,7 +243,7 @@ depsCommand
|
|
|
241
243
|
spinner.warn('Some dependencies failed to install')
|
|
242
244
|
console.log()
|
|
243
245
|
for (const f of failed) {
|
|
244
|
-
console.log(
|
|
246
|
+
console.log(uiError(` ${f.dependency.name}: ${f.error}`))
|
|
245
247
|
}
|
|
246
248
|
|
|
247
249
|
// Show manual instructions
|
|
@@ -259,7 +261,7 @@ depsCommand
|
|
|
259
261
|
if (succeeded.length > 0) {
|
|
260
262
|
console.log()
|
|
261
263
|
console.log(
|
|
262
|
-
|
|
264
|
+
uiSuccess(
|
|
263
265
|
`Installed: ${succeeded.map((r) => r.dependency.name).join(', ')}`,
|
|
264
266
|
),
|
|
265
267
|
)
|
|
@@ -276,7 +278,7 @@ depsCommand
|
|
|
276
278
|
const missing = await getMissingDependencies('postgresql')
|
|
277
279
|
|
|
278
280
|
if (missing.length === 0) {
|
|
279
|
-
console.log(
|
|
281
|
+
console.log(uiSuccess('All PostgreSQL dependencies are installed'))
|
|
280
282
|
return
|
|
281
283
|
}
|
|
282
284
|
|
|
@@ -300,14 +302,14 @@ depsCommand
|
|
|
300
302
|
spinner.warn('Some dependencies failed to install')
|
|
301
303
|
console.log()
|
|
302
304
|
for (const f of failed) {
|
|
303
|
-
console.log(
|
|
305
|
+
console.log(uiError(` ${f.dependency.name}: ${f.error}`))
|
|
304
306
|
}
|
|
305
307
|
}
|
|
306
308
|
|
|
307
309
|
if (succeeded.length > 0) {
|
|
308
310
|
console.log()
|
|
309
311
|
console.log(
|
|
310
|
-
|
|
312
|
+
uiSuccess(
|
|
311
313
|
`Installed: ${succeeded.map((r) => r.dependency.name).join(', ')}`,
|
|
312
314
|
),
|
|
313
315
|
)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { sqliteRegistry } from '../../engines/sqlite/registry'
|
|
4
|
+
import { containerManager } from '../../core/container-manager'
|
|
5
|
+
import { promptConfirm } from '../ui/prompts'
|
|
6
|
+
import { uiSuccess, uiError, uiWarning } from '../ui/theme'
|
|
7
|
+
import { Engine } from '../../types'
|
|
8
|
+
|
|
9
|
+
export const detachCommand = new Command('detach')
|
|
10
|
+
.description('Unregister a SQLite database from SpinDB (keeps file on disk)')
|
|
11
|
+
.argument('<name>', 'Container name')
|
|
12
|
+
.option('-f, --force', 'Skip confirmation prompt')
|
|
13
|
+
.option('--json', 'Output as JSON')
|
|
14
|
+
.action(
|
|
15
|
+
async (
|
|
16
|
+
name: string,
|
|
17
|
+
options: { force?: boolean; json?: boolean },
|
|
18
|
+
): Promise<void> => {
|
|
19
|
+
try {
|
|
20
|
+
// Get container config
|
|
21
|
+
const config = await containerManager.getConfig(name)
|
|
22
|
+
|
|
23
|
+
if (!config) {
|
|
24
|
+
if (options.json) {
|
|
25
|
+
console.log(
|
|
26
|
+
JSON.stringify({ success: false, error: 'Container not found' }),
|
|
27
|
+
)
|
|
28
|
+
} else {
|
|
29
|
+
console.error(uiError(`Container "${name}" not found`))
|
|
30
|
+
}
|
|
31
|
+
process.exit(1)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Verify it's a SQLite container
|
|
35
|
+
if (config.engine !== Engine.SQLite) {
|
|
36
|
+
if (options.json) {
|
|
37
|
+
console.log(
|
|
38
|
+
JSON.stringify({
|
|
39
|
+
success: false,
|
|
40
|
+
error:
|
|
41
|
+
'Not a SQLite container. Use "spindb delete" for server databases.',
|
|
42
|
+
}),
|
|
43
|
+
)
|
|
44
|
+
} else {
|
|
45
|
+
console.error(uiError(`"${name}" is not a SQLite container`))
|
|
46
|
+
console.log(
|
|
47
|
+
chalk.gray(
|
|
48
|
+
' Use "spindb delete" for server databases (PostgreSQL, MySQL)',
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
process.exit(1)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Confirm unless --force
|
|
56
|
+
if (!options.force && !options.json) {
|
|
57
|
+
const confirmed = await promptConfirm(
|
|
58
|
+
`Detach "${name}" from SpinDB? (file will be kept on disk)`,
|
|
59
|
+
true,
|
|
60
|
+
)
|
|
61
|
+
if (!confirmed) {
|
|
62
|
+
console.log(uiWarning('Cancelled'))
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const entry = await sqliteRegistry.get(name)
|
|
68
|
+
const filePath = entry?.filePath
|
|
69
|
+
|
|
70
|
+
// Remove from registry only (not the file)
|
|
71
|
+
await sqliteRegistry.remove(name)
|
|
72
|
+
|
|
73
|
+
if (options.json) {
|
|
74
|
+
console.log(
|
|
75
|
+
JSON.stringify({
|
|
76
|
+
success: true,
|
|
77
|
+
name,
|
|
78
|
+
filePath,
|
|
79
|
+
}),
|
|
80
|
+
)
|
|
81
|
+
} else {
|
|
82
|
+
console.log(uiSuccess(`Detached "${name}" from SpinDB`))
|
|
83
|
+
if (filePath) {
|
|
84
|
+
console.log(chalk.gray(` File remains at: ${filePath}`))
|
|
85
|
+
}
|
|
86
|
+
console.log()
|
|
87
|
+
console.log(chalk.gray(' Re-attach with:'))
|
|
88
|
+
console.log(chalk.cyan(` spindb attach ${filePath || '<path>'}`))
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
const e = error as Error
|
|
92
|
+
if (options.json) {
|
|
93
|
+
console.log(JSON.stringify({ success: false, error: e.message }))
|
|
94
|
+
} else {
|
|
95
|
+
console.error(uiError(e.message))
|
|
96
|
+
}
|
|
97
|
+
process.exit(1)
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
)
|
package/cli/commands/doctor.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { sqliteRegistry } from '../../engines/sqlite/registry'
|
|
|
18
18
|
import { paths } from '../../config/paths'
|
|
19
19
|
import { getSupportedEngines } from '../../config/engine-defaults'
|
|
20
20
|
import { checkEngineDependencies } from '../../core/dependency-manager'
|
|
21
|
-
import { header,
|
|
21
|
+
import { header, uiSuccess } from '../ui/theme'
|
|
22
22
|
import { Engine } from '../../types'
|
|
23
23
|
|
|
24
24
|
type HealthCheckResult = {
|
|
@@ -61,7 +61,7 @@ async function checkConfiguration(): Promise<HealthCheckResult> {
|
|
|
61
61
|
label: 'Refresh binary cache',
|
|
62
62
|
handler: async () => {
|
|
63
63
|
await configManager.refreshAllBinaries()
|
|
64
|
-
console.log(
|
|
64
|
+
console.log(uiSuccess('Binary cache refreshed'))
|
|
65
65
|
},
|
|
66
66
|
},
|
|
67
67
|
}
|
|
@@ -73,12 +73,12 @@ async function checkConfiguration(): Promise<HealthCheckResult> {
|
|
|
73
73
|
message: 'Configuration valid',
|
|
74
74
|
details: [`Binary tools cached: ${binaryCount}`],
|
|
75
75
|
}
|
|
76
|
-
} catch (
|
|
76
|
+
} catch (error) {
|
|
77
77
|
return {
|
|
78
78
|
name: 'Configuration',
|
|
79
79
|
status: 'error',
|
|
80
80
|
message: 'Configuration file is corrupted',
|
|
81
|
-
details: [(
|
|
81
|
+
details: [(error as Error).message],
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
}
|
|
@@ -125,12 +125,12 @@ async function checkContainers(): Promise<HealthCheckResult> {
|
|
|
125
125
|
message: `${containers.length} container(s)`,
|
|
126
126
|
details,
|
|
127
127
|
}
|
|
128
|
-
} catch (
|
|
128
|
+
} catch (error) {
|
|
129
129
|
return {
|
|
130
130
|
name: 'Containers',
|
|
131
131
|
status: 'error',
|
|
132
132
|
message: 'Failed to list containers',
|
|
133
|
-
details: [(
|
|
133
|
+
details: [(error as Error).message],
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
}
|
|
@@ -141,8 +141,9 @@ async function checkContainers(): Promise<HealthCheckResult> {
|
|
|
141
141
|
async function checkSqliteRegistry(): Promise<HealthCheckResult> {
|
|
142
142
|
try {
|
|
143
143
|
const entries = await sqliteRegistry.list()
|
|
144
|
+
const ignoredFolders = await sqliteRegistry.listIgnoredFolders()
|
|
144
145
|
|
|
145
|
-
if (entries.length === 0) {
|
|
146
|
+
if (entries.length === 0 && ignoredFolders.length === 0) {
|
|
146
147
|
return {
|
|
147
148
|
name: 'SQLite Registry',
|
|
148
149
|
status: 'ok',
|
|
@@ -153,32 +154,45 @@ async function checkSqliteRegistry(): Promise<HealthCheckResult> {
|
|
|
153
154
|
const orphans = await sqliteRegistry.findOrphans()
|
|
154
155
|
|
|
155
156
|
if (orphans.length > 0) {
|
|
157
|
+
const details = [
|
|
158
|
+
...orphans.map((o) => `"${o.name}" → ${o.filePath}`),
|
|
159
|
+
...(ignoredFolders.length > 0
|
|
160
|
+
? [`${ignoredFolders.length} folder(s) ignored`]
|
|
161
|
+
: []),
|
|
162
|
+
]
|
|
163
|
+
|
|
156
164
|
return {
|
|
157
165
|
name: 'SQLite Registry',
|
|
158
166
|
status: 'warning',
|
|
159
167
|
message: `${orphans.length} orphaned entr${orphans.length === 1 ? 'y' : 'ies'} found`,
|
|
160
|
-
details
|
|
168
|
+
details,
|
|
161
169
|
action: {
|
|
162
170
|
label: 'Remove orphaned entries from registry',
|
|
163
171
|
handler: async () => {
|
|
164
172
|
const count = await sqliteRegistry.removeOrphans()
|
|
165
|
-
console.log(
|
|
173
|
+
console.log(uiSuccess(`Removed ${count} orphaned entries`))
|
|
166
174
|
},
|
|
167
175
|
},
|
|
168
176
|
}
|
|
169
177
|
}
|
|
170
178
|
|
|
179
|
+
const details = [`${entries.length} database(s) registered, all files exist`]
|
|
180
|
+
if (ignoredFolders.length > 0) {
|
|
181
|
+
details.push(`${ignoredFolders.length} folder(s) ignored`)
|
|
182
|
+
}
|
|
183
|
+
|
|
171
184
|
return {
|
|
172
185
|
name: 'SQLite Registry',
|
|
173
186
|
status: 'ok',
|
|
174
187
|
message: `${entries.length} database(s) registered, all files exist`,
|
|
188
|
+
details: ignoredFolders.length > 0 ? details : undefined,
|
|
175
189
|
}
|
|
176
|
-
} catch (
|
|
190
|
+
} catch (error) {
|
|
177
191
|
return {
|
|
178
192
|
name: 'SQLite Registry',
|
|
179
193
|
status: 'warning',
|
|
180
194
|
message: 'Could not check registry',
|
|
181
|
-
details: [(
|
|
195
|
+
details: [(error as Error).message],
|
|
182
196
|
}
|
|
183
197
|
}
|
|
184
198
|
}
|
|
@@ -211,12 +225,12 @@ async function checkBinaries(): Promise<HealthCheckResult> {
|
|
|
211
225
|
message: hasWarning ? 'Some tools missing' : 'All tools available',
|
|
212
226
|
details: results,
|
|
213
227
|
}
|
|
214
|
-
} catch (
|
|
228
|
+
} catch (error) {
|
|
215
229
|
return {
|
|
216
230
|
name: 'Database Tools',
|
|
217
231
|
status: 'error',
|
|
218
232
|
message: 'Failed to check tools',
|
|
219
|
-
details: [(
|
|
233
|
+
details: [(error as Error).message],
|
|
220
234
|
}
|
|
221
235
|
}
|
|
222
236
|
}
|