spindb 0.4.0 → 0.4.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.
- package/cli/commands/create.ts +97 -25
- package/cli/commands/menu.ts +147 -38
- package/cli/commands/restore.ts +89 -22
- package/cli/ui/prompts.ts +1 -1
- package/config/paths.ts +39 -1
- package/core/dependency-manager.ts +44 -22
- package/core/postgres-binary-manager.ts +10 -9
- package/package.json +1 -1
package/cli/commands/create.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { defaults } from '../../config/defaults'
|
|
|
9
9
|
import {
|
|
10
10
|
promptCreateOptions,
|
|
11
11
|
promptInstallDependencies,
|
|
12
|
+
promptContainerName,
|
|
12
13
|
} from '../ui/prompts'
|
|
13
14
|
import { createSpinner } from '../ui/spinner'
|
|
14
15
|
import { header, error, connectionBox } from '../ui/theme'
|
|
@@ -16,6 +17,7 @@ import { tmpdir } from 'os'
|
|
|
16
17
|
import { join } from 'path'
|
|
17
18
|
import { spawn } from 'child_process'
|
|
18
19
|
import { platform } from 'os'
|
|
20
|
+
import { getMissingDependencies } from '../../core/dependency-manager'
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Detect if a location string is a connection string or a file path
|
|
@@ -124,6 +126,43 @@ export const createCommand = new Command('create')
|
|
|
124
126
|
// Get the engine
|
|
125
127
|
const dbEngine = getEngine(engine)
|
|
126
128
|
|
|
129
|
+
// Check for required client tools BEFORE creating anything
|
|
130
|
+
const depsSpinner = createSpinner('Checking required tools...')
|
|
131
|
+
depsSpinner.start()
|
|
132
|
+
|
|
133
|
+
let missingDeps = await getMissingDependencies(engine)
|
|
134
|
+
if (missingDeps.length > 0) {
|
|
135
|
+
depsSpinner.warn(
|
|
136
|
+
`Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
// Offer to install
|
|
140
|
+
const installed = await promptInstallDependencies(
|
|
141
|
+
missingDeps[0].binary,
|
|
142
|
+
engine,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if (!installed) {
|
|
146
|
+
process.exit(1)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Verify installation worked
|
|
150
|
+
missingDeps = await getMissingDependencies(engine)
|
|
151
|
+
if (missingDeps.length > 0) {
|
|
152
|
+
console.error(
|
|
153
|
+
error(
|
|
154
|
+
`Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
process.exit(1)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log(chalk.green(' ✓ All required tools are now available'))
|
|
161
|
+
console.log()
|
|
162
|
+
} else {
|
|
163
|
+
depsSpinner.succeed('Required tools available')
|
|
164
|
+
}
|
|
165
|
+
|
|
127
166
|
// Find available port
|
|
128
167
|
const portSpinner = createSpinner('Finding available port...')
|
|
129
168
|
portSpinner.start()
|
|
@@ -165,6 +204,14 @@ export const createCommand = new Command('create')
|
|
|
165
204
|
binarySpinner.succeed(`PostgreSQL ${version} binaries downloaded`)
|
|
166
205
|
}
|
|
167
206
|
|
|
207
|
+
// Check if container name already exists and prompt for new name if needed
|
|
208
|
+
while (await containerManager.exists(containerName)) {
|
|
209
|
+
console.log(
|
|
210
|
+
chalk.yellow(` Container "${containerName}" already exists.`),
|
|
211
|
+
)
|
|
212
|
+
containerName = await promptContainerName()
|
|
213
|
+
}
|
|
214
|
+
|
|
168
215
|
// Create container
|
|
169
216
|
const createSpinnerInstance = createSpinner('Creating container...')
|
|
170
217
|
createSpinnerInstance.start()
|
|
@@ -217,41 +264,59 @@ export const createCommand = new Command('create')
|
|
|
217
264
|
|
|
218
265
|
// Handle --from restore if specified
|
|
219
266
|
if (restoreLocation && restoreType && config) {
|
|
220
|
-
let backupPath
|
|
267
|
+
let backupPath = ''
|
|
221
268
|
|
|
222
269
|
if (restoreType === 'connection') {
|
|
223
270
|
// Create dump from remote database
|
|
224
271
|
const timestamp = Date.now()
|
|
225
272
|
tempDumpPath = join(tmpdir(), `spindb-dump-${timestamp}.dump`)
|
|
226
273
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
dumpSpinner.start()
|
|
274
|
+
let dumpSuccess = false
|
|
275
|
+
let attempts = 0
|
|
276
|
+
const maxAttempts = 2 // Allow one retry after installing deps
|
|
231
277
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
278
|
+
while (!dumpSuccess && attempts < maxAttempts) {
|
|
279
|
+
attempts++
|
|
280
|
+
const dumpSpinner = createSpinner(
|
|
281
|
+
'Creating dump from remote database...',
|
|
236
282
|
)
|
|
237
|
-
dumpSpinner.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
) {
|
|
248
|
-
|
|
283
|
+
dumpSpinner.start()
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
await dbEngine.dumpFromConnectionString(
|
|
287
|
+
restoreLocation,
|
|
288
|
+
tempDumpPath,
|
|
289
|
+
)
|
|
290
|
+
dumpSpinner.succeed('Dump created from remote database')
|
|
291
|
+
backupPath = tempDumpPath
|
|
292
|
+
dumpSuccess = true
|
|
293
|
+
} catch (err) {
|
|
294
|
+
const e = err as Error
|
|
295
|
+
dumpSpinner.fail('Failed to create dump')
|
|
296
|
+
|
|
297
|
+
// Check if this is a missing tool error
|
|
298
|
+
if (
|
|
299
|
+
e.message.includes('pg_dump not found') ||
|
|
300
|
+
e.message.includes('ENOENT')
|
|
301
|
+
) {
|
|
302
|
+
const installed = await promptInstallDependencies('pg_dump')
|
|
303
|
+
if (!installed) {
|
|
304
|
+
process.exit(1)
|
|
305
|
+
}
|
|
306
|
+
// Loop will retry
|
|
307
|
+
continue
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
console.log()
|
|
311
|
+
console.error(error('pg_dump error:'))
|
|
312
|
+
console.log(chalk.gray(` ${e.message}`))
|
|
249
313
|
process.exit(1)
|
|
250
314
|
}
|
|
315
|
+
}
|
|
251
316
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
console.
|
|
317
|
+
// Safety check - should never reach here without backupPath set
|
|
318
|
+
if (!dumpSuccess) {
|
|
319
|
+
console.error(error('Failed to create dump after retries'))
|
|
255
320
|
process.exit(1)
|
|
256
321
|
}
|
|
257
322
|
} else {
|
|
@@ -350,7 +415,14 @@ export const createCommand = new Command('create')
|
|
|
350
415
|
: e.message.includes('pg_dump')
|
|
351
416
|
? 'pg_dump'
|
|
352
417
|
: 'psql'
|
|
353
|
-
await promptInstallDependencies(missingTool)
|
|
418
|
+
const installed = await promptInstallDependencies(missingTool)
|
|
419
|
+
if (installed) {
|
|
420
|
+
console.log(
|
|
421
|
+
chalk.yellow(
|
|
422
|
+
' Please re-run your command to continue.',
|
|
423
|
+
),
|
|
424
|
+
)
|
|
425
|
+
}
|
|
354
426
|
process.exit(1)
|
|
355
427
|
}
|
|
356
428
|
|
package/cli/commands/menu.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { processManager } from '../../core/process-manager'
|
|
|
5
5
|
import { getEngine } from '../../engines'
|
|
6
6
|
import {
|
|
7
7
|
promptContainerSelect,
|
|
8
|
+
promptContainerName,
|
|
8
9
|
promptDatabaseName,
|
|
9
10
|
promptCreateOptions,
|
|
10
11
|
promptConfirm,
|
|
@@ -28,6 +29,7 @@ import { paths } from '../../config/paths'
|
|
|
28
29
|
import { portManager } from '../../core/port-manager'
|
|
29
30
|
import { defaults } from '../../config/defaults'
|
|
30
31
|
import inquirer from 'inquirer'
|
|
32
|
+
import { getMissingDependencies } from '../../core/dependency-manager'
|
|
31
33
|
|
|
32
34
|
type MenuChoice =
|
|
33
35
|
| {
|
|
@@ -169,7 +171,8 @@ async function showMainMenu(): Promise<void> {
|
|
|
169
171
|
async function handleCreate(): Promise<void> {
|
|
170
172
|
console.log()
|
|
171
173
|
const answers = await promptCreateOptions()
|
|
172
|
-
|
|
174
|
+
let { name: containerName } = answers
|
|
175
|
+
const { engine, version, port, database } = answers
|
|
173
176
|
|
|
174
177
|
console.log()
|
|
175
178
|
console.log(header('Creating Database Container'))
|
|
@@ -177,6 +180,41 @@ async function handleCreate(): Promise<void> {
|
|
|
177
180
|
|
|
178
181
|
const dbEngine = getEngine(engine)
|
|
179
182
|
|
|
183
|
+
// Check for required client tools BEFORE creating anything
|
|
184
|
+
const depsSpinner = createSpinner('Checking required tools...')
|
|
185
|
+
depsSpinner.start()
|
|
186
|
+
|
|
187
|
+
let missingDeps = await getMissingDependencies(engine)
|
|
188
|
+
if (missingDeps.length > 0) {
|
|
189
|
+
depsSpinner.warn(
|
|
190
|
+
`Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
// Offer to install
|
|
194
|
+
const installed = await promptInstallDependencies(
|
|
195
|
+
missingDeps[0].binary,
|
|
196
|
+
engine,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if (!installed) {
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Verify installation worked
|
|
204
|
+
missingDeps = await getMissingDependencies(engine)
|
|
205
|
+
if (missingDeps.length > 0) {
|
|
206
|
+
console.log(
|
|
207
|
+
error(`Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`),
|
|
208
|
+
)
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
console.log(chalk.green(' ✓ All required tools are now available'))
|
|
213
|
+
console.log()
|
|
214
|
+
} else {
|
|
215
|
+
depsSpinner.succeed('Required tools available')
|
|
216
|
+
}
|
|
217
|
+
|
|
180
218
|
// Check if port is currently in use
|
|
181
219
|
const portAvailable = await portManager.isPortAvailable(port)
|
|
182
220
|
|
|
@@ -197,6 +235,12 @@ async function handleCreate(): Promise<void> {
|
|
|
197
235
|
binarySpinner.succeed(`PostgreSQL ${version} binaries downloaded`)
|
|
198
236
|
}
|
|
199
237
|
|
|
238
|
+
// Check if container name already exists and prompt for new name if needed
|
|
239
|
+
while (await containerManager.exists(containerName)) {
|
|
240
|
+
console.log(chalk.yellow(` Container "${containerName}" already exists.`))
|
|
241
|
+
containerName = await promptContainerName()
|
|
242
|
+
}
|
|
243
|
+
|
|
200
244
|
// Create container
|
|
201
245
|
const createSpinnerInstance = createSpinner('Creating container...')
|
|
202
246
|
createSpinnerInstance.start()
|
|
@@ -689,7 +733,8 @@ async function handleCreateForRestore(): Promise<{
|
|
|
689
733
|
} | null> {
|
|
690
734
|
console.log()
|
|
691
735
|
const answers = await promptCreateOptions()
|
|
692
|
-
|
|
736
|
+
let { name: containerName } = answers
|
|
737
|
+
const { engine, version, port, database } = answers
|
|
693
738
|
|
|
694
739
|
console.log()
|
|
695
740
|
console.log(header('Creating Database Container'))
|
|
@@ -723,6 +768,12 @@ async function handleCreateForRestore(): Promise<{
|
|
|
723
768
|
binarySpinner.succeed(`PostgreSQL ${version} binaries downloaded`)
|
|
724
769
|
}
|
|
725
770
|
|
|
771
|
+
// Check if container name already exists and prompt for new name if needed
|
|
772
|
+
while (await containerManager.exists(containerName)) {
|
|
773
|
+
console.log(chalk.yellow(` Container "${containerName}" already exists.`))
|
|
774
|
+
containerName = await promptContainerName()
|
|
775
|
+
}
|
|
776
|
+
|
|
726
777
|
// Create container
|
|
727
778
|
const createSpinnerInstance = createSpinner('Creating container...')
|
|
728
779
|
createSpinnerInstance.start()
|
|
@@ -826,6 +877,41 @@ async function handleRestore(): Promise<void> {
|
|
|
826
877
|
}
|
|
827
878
|
}
|
|
828
879
|
|
|
880
|
+
// Check for required client tools BEFORE doing anything
|
|
881
|
+
const depsSpinner = createSpinner('Checking required tools...')
|
|
882
|
+
depsSpinner.start()
|
|
883
|
+
|
|
884
|
+
let missingDeps = await getMissingDependencies(config.engine)
|
|
885
|
+
if (missingDeps.length > 0) {
|
|
886
|
+
depsSpinner.warn(
|
|
887
|
+
`Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
// Offer to install
|
|
891
|
+
const installed = await promptInstallDependencies(
|
|
892
|
+
missingDeps[0].binary,
|
|
893
|
+
config.engine,
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
if (!installed) {
|
|
897
|
+
return
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// Verify installation worked
|
|
901
|
+
missingDeps = await getMissingDependencies(config.engine)
|
|
902
|
+
if (missingDeps.length > 0) {
|
|
903
|
+
console.log(
|
|
904
|
+
error(`Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`),
|
|
905
|
+
)
|
|
906
|
+
return
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
console.log(chalk.green(' ✓ All required tools are now available'))
|
|
910
|
+
console.log()
|
|
911
|
+
} else {
|
|
912
|
+
depsSpinner.succeed('Required tools available')
|
|
913
|
+
}
|
|
914
|
+
|
|
829
915
|
// Ask for restore source
|
|
830
916
|
const { restoreSource } = await inquirer.prompt<{
|
|
831
917
|
restoreSource: 'file' | 'connection'
|
|
@@ -847,7 +933,7 @@ async function handleRestore(): Promise<void> {
|
|
|
847
933
|
},
|
|
848
934
|
])
|
|
849
935
|
|
|
850
|
-
let backupPath
|
|
936
|
+
let backupPath = ''
|
|
851
937
|
let isTempFile = false
|
|
852
938
|
|
|
853
939
|
if (restoreSource === 'connection') {
|
|
@@ -878,46 +964,64 @@ async function handleRestore(): Promise<void> {
|
|
|
878
964
|
const timestamp = Date.now()
|
|
879
965
|
const tempDumpPath = join(tmpdir(), `spindb-dump-${timestamp}.dump`)
|
|
880
966
|
|
|
881
|
-
|
|
882
|
-
|
|
967
|
+
let dumpSuccess = false
|
|
968
|
+
let attempts = 0
|
|
969
|
+
const maxAttempts = 2 // Allow one retry after installing deps
|
|
883
970
|
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
dumpSpinner
|
|
887
|
-
|
|
888
|
-
isTempFile = true
|
|
889
|
-
} catch (err) {
|
|
890
|
-
const e = err as Error
|
|
891
|
-
dumpSpinner.fail('Failed to create dump')
|
|
971
|
+
while (!dumpSuccess && attempts < maxAttempts) {
|
|
972
|
+
attempts++
|
|
973
|
+
const dumpSpinner = createSpinner('Creating dump from remote database...')
|
|
974
|
+
dumpSpinner.start()
|
|
892
975
|
|
|
893
|
-
// Clean up temp file if it was created
|
|
894
976
|
try {
|
|
895
|
-
await
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
977
|
+
await engine.dumpFromConnectionString(connectionString, tempDumpPath)
|
|
978
|
+
dumpSpinner.succeed('Dump created from remote database')
|
|
979
|
+
backupPath = tempDumpPath
|
|
980
|
+
isTempFile = true
|
|
981
|
+
dumpSuccess = true
|
|
982
|
+
} catch (err) {
|
|
983
|
+
const e = err as Error
|
|
984
|
+
dumpSpinner.fail('Failed to create dump')
|
|
985
|
+
|
|
986
|
+
// Check if this is a missing tool error
|
|
987
|
+
if (
|
|
988
|
+
e.message.includes('pg_dump not found') ||
|
|
989
|
+
e.message.includes('ENOENT')
|
|
990
|
+
) {
|
|
991
|
+
const installed = await promptInstallDependencies('pg_dump')
|
|
992
|
+
if (installed) {
|
|
993
|
+
// Loop will retry
|
|
994
|
+
continue
|
|
995
|
+
}
|
|
996
|
+
} else {
|
|
997
|
+
console.log()
|
|
998
|
+
console.log(error('pg_dump error:'))
|
|
999
|
+
console.log(chalk.gray(` ${e.message}`))
|
|
1000
|
+
console.log()
|
|
1001
|
+
}
|
|
899
1002
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1003
|
+
// Clean up temp file if it was created
|
|
1004
|
+
try {
|
|
1005
|
+
await rm(tempDumpPath, { force: true })
|
|
1006
|
+
} catch {
|
|
1007
|
+
// Ignore cleanup errors
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// Wait for user to see the error
|
|
1011
|
+
await inquirer.prompt([
|
|
1012
|
+
{
|
|
1013
|
+
type: 'input',
|
|
1014
|
+
name: 'continue',
|
|
1015
|
+
message: chalk.gray('Press Enter to continue...'),
|
|
1016
|
+
},
|
|
1017
|
+
])
|
|
1018
|
+
return
|
|
911
1019
|
}
|
|
1020
|
+
}
|
|
912
1021
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
type: 'input',
|
|
917
|
-
name: 'continue',
|
|
918
|
-
message: chalk.gray('Press Enter to continue...'),
|
|
919
|
-
},
|
|
920
|
-
])
|
|
1022
|
+
// Safety check - should never reach here without backupPath set
|
|
1023
|
+
if (!dumpSuccess) {
|
|
1024
|
+
console.log(error('Failed to create dump after retries'))
|
|
921
1025
|
return
|
|
922
1026
|
}
|
|
923
1027
|
} else {
|
|
@@ -1727,7 +1831,12 @@ export const menuCommand = new Command('menu')
|
|
|
1727
1831
|
: e.message.includes('pg_dump')
|
|
1728
1832
|
? 'pg_dump'
|
|
1729
1833
|
: 'psql'
|
|
1730
|
-
await promptInstallDependencies(missingTool)
|
|
1834
|
+
const installed = await promptInstallDependencies(missingTool)
|
|
1835
|
+
if (installed) {
|
|
1836
|
+
console.log(
|
|
1837
|
+
chalk.yellow(' Please re-run spindb to continue.'),
|
|
1838
|
+
)
|
|
1839
|
+
}
|
|
1731
1840
|
process.exit(1)
|
|
1732
1841
|
}
|
|
1733
1842
|
|
package/cli/commands/restore.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { success, error, warning } from '../ui/theme'
|
|
|
15
15
|
import { platform, tmpdir } from 'os'
|
|
16
16
|
import { spawn } from 'child_process'
|
|
17
17
|
import { join } from 'path'
|
|
18
|
+
import { getMissingDependencies } from '../../core/dependency-manager'
|
|
18
19
|
|
|
19
20
|
export const restoreCommand = new Command('restore')
|
|
20
21
|
.description('Restore a backup to a container')
|
|
@@ -89,6 +90,43 @@ export const restoreCommand = new Command('restore')
|
|
|
89
90
|
// Get engine
|
|
90
91
|
const engine = getEngine(config.engine)
|
|
91
92
|
|
|
93
|
+
// Check for required client tools BEFORE doing anything
|
|
94
|
+
const depsSpinner = createSpinner('Checking required tools...')
|
|
95
|
+
depsSpinner.start()
|
|
96
|
+
|
|
97
|
+
let missingDeps = await getMissingDependencies(config.engine)
|
|
98
|
+
if (missingDeps.length > 0) {
|
|
99
|
+
depsSpinner.warn(
|
|
100
|
+
`Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
// Offer to install
|
|
104
|
+
const installed = await promptInstallDependencies(
|
|
105
|
+
missingDeps[0].binary,
|
|
106
|
+
config.engine,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if (!installed) {
|
|
110
|
+
process.exit(1)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Verify installation worked
|
|
114
|
+
missingDeps = await getMissingDependencies(config.engine)
|
|
115
|
+
if (missingDeps.length > 0) {
|
|
116
|
+
console.error(
|
|
117
|
+
error(
|
|
118
|
+
`Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
process.exit(1)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log(chalk.green(' ✓ All required tools are now available'))
|
|
125
|
+
console.log()
|
|
126
|
+
} else {
|
|
127
|
+
depsSpinner.succeed('Required tools available')
|
|
128
|
+
}
|
|
129
|
+
|
|
92
130
|
// Handle --from-url option
|
|
93
131
|
if (options.fromUrl) {
|
|
94
132
|
// Validate connection string
|
|
@@ -108,31 +146,49 @@ export const restoreCommand = new Command('restore')
|
|
|
108
146
|
const timestamp = Date.now()
|
|
109
147
|
tempDumpPath = join(tmpdir(), `spindb-dump-${timestamp}.dump`)
|
|
110
148
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
dumpSpinner.start()
|
|
149
|
+
let dumpSuccess = false
|
|
150
|
+
let attempts = 0
|
|
151
|
+
const maxAttempts = 2 // Allow one retry after installing deps
|
|
115
152
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
dumpSpinner
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
) {
|
|
129
|
-
|
|
153
|
+
while (!dumpSuccess && attempts < maxAttempts) {
|
|
154
|
+
attempts++
|
|
155
|
+
const dumpSpinner = createSpinner(
|
|
156
|
+
'Creating dump from remote database...',
|
|
157
|
+
)
|
|
158
|
+
dumpSpinner.start()
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
await engine.dumpFromConnectionString(options.fromUrl, tempDumpPath)
|
|
162
|
+
dumpSpinner.succeed('Dump created from remote database')
|
|
163
|
+
backupPath = tempDumpPath
|
|
164
|
+
dumpSuccess = true
|
|
165
|
+
} catch (err) {
|
|
166
|
+
const e = err as Error
|
|
167
|
+
dumpSpinner.fail('Failed to create dump')
|
|
168
|
+
|
|
169
|
+
// Check if this is a missing tool error
|
|
170
|
+
if (
|
|
171
|
+
e.message.includes('pg_dump not found') ||
|
|
172
|
+
e.message.includes('ENOENT')
|
|
173
|
+
) {
|
|
174
|
+
const installed = await promptInstallDependencies('pg_dump')
|
|
175
|
+
if (!installed) {
|
|
176
|
+
process.exit(1)
|
|
177
|
+
}
|
|
178
|
+
// Loop will retry
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log()
|
|
183
|
+
console.error(error('pg_dump error:'))
|
|
184
|
+
console.log(chalk.gray(` ${e.message}`))
|
|
130
185
|
process.exit(1)
|
|
131
186
|
}
|
|
187
|
+
}
|
|
132
188
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
console.
|
|
189
|
+
// Safety check - should never reach here without backupPath set
|
|
190
|
+
if (!dumpSuccess) {
|
|
191
|
+
console.error(error('Failed to create dump after retries'))
|
|
136
192
|
process.exit(1)
|
|
137
193
|
}
|
|
138
194
|
} else {
|
|
@@ -162,6 +218,12 @@ export const restoreCommand = new Command('restore')
|
|
|
162
218
|
databaseName = await promptDatabaseName(containerName)
|
|
163
219
|
}
|
|
164
220
|
|
|
221
|
+
// At this point backupPath is guaranteed to be set
|
|
222
|
+
if (!backupPath) {
|
|
223
|
+
console.error(error('No backup path specified'))
|
|
224
|
+
process.exit(1)
|
|
225
|
+
}
|
|
226
|
+
|
|
165
227
|
// Detect backup format
|
|
166
228
|
const detectSpinner = createSpinner('Detecting backup format...')
|
|
167
229
|
detectSpinner.start()
|
|
@@ -259,7 +321,12 @@ export const restoreCommand = new Command('restore')
|
|
|
259
321
|
const missingTool = e.message.includes('pg_restore')
|
|
260
322
|
? 'pg_restore'
|
|
261
323
|
: 'psql'
|
|
262
|
-
await promptInstallDependencies(missingTool)
|
|
324
|
+
const installed = await promptInstallDependencies(missingTool)
|
|
325
|
+
if (installed) {
|
|
326
|
+
console.log(
|
|
327
|
+
chalk.yellow(' Please re-run your command to continue.'),
|
|
328
|
+
)
|
|
329
|
+
}
|
|
263
330
|
process.exit(1)
|
|
264
331
|
}
|
|
265
332
|
|
package/cli/ui/prompts.ts
CHANGED
|
@@ -403,7 +403,7 @@ export async function promptInstallDependencies(
|
|
|
403
403
|
console.log(
|
|
404
404
|
chalk.green(` ${engineName} client tools installed successfully!`),
|
|
405
405
|
)
|
|
406
|
-
console.log(chalk.gray('
|
|
406
|
+
console.log(chalk.gray(' Continuing with your operation...'))
|
|
407
407
|
console.log()
|
|
408
408
|
}
|
|
409
409
|
|
package/config/paths.ts
CHANGED
|
@@ -1,7 +1,45 @@
|
|
|
1
1
|
import { homedir } from 'os'
|
|
2
2
|
import { join } from 'path'
|
|
3
|
+
import { execSync } from 'child_process'
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Get the real user's home directory, even when running under sudo.
|
|
7
|
+
* When a user runs `sudo spindb`, we want to use their home directory,
|
|
8
|
+
* not root's home directory.
|
|
9
|
+
*/
|
|
10
|
+
function getRealHomeDir(): string {
|
|
11
|
+
// Check if running under sudo
|
|
12
|
+
const sudoUser = process.env.SUDO_USER
|
|
13
|
+
|
|
14
|
+
if (sudoUser) {
|
|
15
|
+
// Get the original user's home directory
|
|
16
|
+
try {
|
|
17
|
+
// Use getent to reliably get the home directory for the sudo user
|
|
18
|
+
const result = execSync(`getent passwd ${sudoUser}`, {
|
|
19
|
+
encoding: 'utf-8',
|
|
20
|
+
})
|
|
21
|
+
const parts = result.trim().split(':')
|
|
22
|
+
if (parts.length >= 6 && parts[5]) {
|
|
23
|
+
return parts[5]
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
// Fall back to constructing the path
|
|
27
|
+
// On most Linux systems, home dirs are /home/username
|
|
28
|
+
// On macOS, they're /Users/username
|
|
29
|
+
const platform = process.platform
|
|
30
|
+
if (platform === 'darwin') {
|
|
31
|
+
return `/Users/${sudoUser}`
|
|
32
|
+
} else {
|
|
33
|
+
return `/home/${sudoUser}`
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Not running under sudo, use normal homedir
|
|
39
|
+
return homedir()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const SPINDB_HOME = join(getRealHomeDir(), '.spindb')
|
|
5
43
|
|
|
6
44
|
export const paths = {
|
|
7
45
|
// Root directory for all spindb data
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* for database engines.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { exec } from 'child_process'
|
|
8
|
+
import { exec, spawnSync } from 'child_process'
|
|
9
9
|
import { promisify } from 'util'
|
|
10
10
|
import {
|
|
11
11
|
type PackageManagerId,
|
|
@@ -173,30 +173,51 @@ export async function getAllMissingDependencies(): Promise<Dependency[]> {
|
|
|
173
173
|
// =============================================================================
|
|
174
174
|
|
|
175
175
|
/**
|
|
176
|
-
*
|
|
176
|
+
* Check if stdin is a TTY (interactive terminal)
|
|
177
177
|
*/
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
178
|
+
function hasTTY(): boolean {
|
|
179
|
+
return process.stdin.isTTY === true
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Check if running as root
|
|
184
|
+
*/
|
|
185
|
+
function isRoot(): boolean {
|
|
186
|
+
return process.getuid?.() === 0
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Execute command with inherited stdio (for TTY support with sudo)
|
|
191
|
+
* Uses spawnSync to properly connect to the terminal for password prompts
|
|
192
|
+
*/
|
|
193
|
+
function execWithInheritedStdio(command: string): void {
|
|
194
|
+
let cmdToRun = command
|
|
195
|
+
|
|
196
|
+
// If already running as root, strip sudo from the command
|
|
197
|
+
if (isRoot() && command.startsWith('sudo ')) {
|
|
198
|
+
cmdToRun = command.replace(/^sudo\s+/, '')
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check if we need a TTY for sudo password prompts
|
|
202
|
+
if (!hasTTY() && cmdToRun.includes('sudo')) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
'Cannot run sudo commands without an interactive terminal. Please run the install command manually:\n' +
|
|
205
|
+
` ${command}`,
|
|
193
206
|
)
|
|
207
|
+
}
|
|
194
208
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}, timeoutMs)
|
|
209
|
+
const result = spawnSync(cmdToRun, [], {
|
|
210
|
+
shell: true,
|
|
211
|
+
stdio: 'inherit',
|
|
199
212
|
})
|
|
213
|
+
|
|
214
|
+
if (result.error) {
|
|
215
|
+
throw result.error
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (result.status !== 0) {
|
|
219
|
+
throw new Error(`Command failed with exit code ${result.status}: ${cmdToRun}`)
|
|
220
|
+
}
|
|
200
221
|
}
|
|
201
222
|
|
|
202
223
|
/**
|
|
@@ -246,7 +267,8 @@ export async function installDependency(
|
|
|
246
267
|
const commands = buildInstallCommand(dependency, packageManager)
|
|
247
268
|
|
|
248
269
|
for (const cmd of commands) {
|
|
249
|
-
|
|
270
|
+
// Use inherited stdio so sudo can prompt for password in terminal
|
|
271
|
+
execWithInheritedStdio(cmd)
|
|
250
272
|
}
|
|
251
273
|
|
|
252
274
|
// Verify installation
|
|
@@ -308,29 +308,30 @@ export async function installPostgresBinaries(): Promise<boolean> {
|
|
|
308
308
|
|
|
309
309
|
spinner.succeed(`Found package manager: ${packageManager.name}`)
|
|
310
310
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
)
|
|
314
|
-
|
|
311
|
+
// Don't use a spinner during installation - it blocks TTY access for sudo password prompts
|
|
312
|
+
console.log(chalk.cyan(` Installing PostgreSQL client tools with ${packageManager.name}...`))
|
|
313
|
+
console.log(chalk.gray(' You may be prompted for your password.'))
|
|
314
|
+
console.log()
|
|
315
315
|
|
|
316
316
|
try {
|
|
317
317
|
const results = await installEngineDependencies('postgresql', packageManager)
|
|
318
318
|
const allSuccess = results.every((r) => r.success)
|
|
319
319
|
|
|
320
320
|
if (allSuccess) {
|
|
321
|
-
|
|
322
|
-
console.log(success('
|
|
321
|
+
console.log()
|
|
322
|
+
console.log(success('PostgreSQL client tools installed successfully'))
|
|
323
323
|
return true
|
|
324
324
|
} else {
|
|
325
325
|
const failed = results.filter((r) => !r.success)
|
|
326
|
-
|
|
326
|
+
console.log()
|
|
327
|
+
console.log(themeError('Some installations failed:'))
|
|
327
328
|
for (const f of failed) {
|
|
328
|
-
console.log(themeError(`
|
|
329
|
+
console.log(themeError(` ${f.dependency.name}: ${f.error}`))
|
|
329
330
|
}
|
|
330
331
|
return false
|
|
331
332
|
}
|
|
332
333
|
} catch (error: unknown) {
|
|
333
|
-
|
|
334
|
+
console.log()
|
|
334
335
|
console.log(themeError('Failed to install PostgreSQL client tools'))
|
|
335
336
|
console.log(warning('Please install manually'))
|
|
336
337
|
if (error instanceof Error) {
|