spindb 0.9.0 → 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.
package/README.md CHANGED
@@ -227,6 +227,9 @@ spindb create mydb --port 5433 # Custom port
227
227
  spindb create mydb --database my_app # Custom database name
228
228
  spindb create mydb --no-start # Create without starting
229
229
 
230
+ # Create, start, and connect in one command
231
+ spindb create mydb --start --connect
232
+
230
233
  # SQLite with custom path
231
234
  spindb create mydb --engine sqlite --path ./data/app.sqlite
232
235
  ```
@@ -250,7 +253,9 @@ spindb create mydb --from "postgresql://user:pass@host:5432/production"
250
253
  | `--path` | File path for SQLite databases |
251
254
  | `--max-connections` | Maximum database connections (default: 200) |
252
255
  | `--from` | Restore from backup file or connection string |
256
+ | `--start` | Start container after creation (skip prompt) |
253
257
  | `--no-start` | Create without starting |
258
+ | `--connect` | Open a shell connection after creation |
254
259
 
255
260
  </details>
256
261
 
@@ -400,10 +405,12 @@ ENGINE VERSION SOURCE SIZE
400
405
  🐘 postgresql 17.7 darwin-arm64 45.2 MB
401
406
  🐘 postgresql 16.8 darwin-arm64 44.8 MB
402
407
  🐎 mysql 8.0.35 system (system-installed)
408
+ ðŸŠķ sqlite 3.43.2 system (system-installed)
403
409
  ────────────────────────────────────────────────────────
404
410
 
405
411
  PostgreSQL: 2 version(s), 90.0 MB
406
412
  MySQL: system-installed at /opt/homebrew/bin/mysqld
413
+ SQLite: system-installed at /usr/bin/sqlite3
407
414
  ```
408
415
 
409
416
  #### `deps` - Manage client tools
@@ -71,6 +71,12 @@ export const cloneCommand = new Command('clone')
71
71
  targetName = await promptContainerName(`${sourceName}-copy`)
72
72
  }
73
73
 
74
+ // Check if target container already exists
75
+ if (await containerManager.exists(targetName, { engine: sourceConfig.engine })) {
76
+ console.error(error(`Container "${targetName}" already exists`))
77
+ process.exit(1)
78
+ }
79
+
74
80
  const cloneSpinner = createSpinner(
75
81
  `Cloning ${sourceName} to ${targetName}...`,
76
82
  )
@@ -20,6 +20,7 @@ import { getMissingDependencies } from '../../core/dependency-manager'
20
20
  import { platformService } from '../../core/platform-service'
21
21
  import { startWithRetry } from '../../core/start-with-retry'
22
22
  import { TransactionManager } from '../../core/transaction-manager'
23
+ import { isValidDatabaseName } from '../../core/error-handler'
23
24
  import { Engine } from '../../types'
24
25
  import type { BaseEngine } from '../../engines/base-engine'
25
26
  import { resolve } from 'path'
@@ -32,9 +33,9 @@ async function createSqliteContainer(
32
33
  containerName: string,
33
34
  dbEngine: BaseEngine,
34
35
  version: string,
35
- options: { path?: string; from?: string | null },
36
+ options: { path?: string; from?: string | null; connect?: boolean },
36
37
  ): Promise<void> {
37
- const { path: filePath, from: restoreLocation } = options
38
+ const { path: filePath, from: restoreLocation, connect } = options
38
39
 
39
40
  // Check dependencies
40
41
  const depsSpinner = createSpinner('Checking required tools...')
@@ -107,9 +108,20 @@ async function createSqliteContainer(
107
108
  console.log(chalk.gray(' Connection string:'))
108
109
  console.log(chalk.cyan(` sqlite:///${absolutePath}`))
109
110
  console.log()
110
- console.log(chalk.gray(' Connect with:'))
111
- console.log(chalk.cyan(` spindb connect ${containerName}`))
112
- console.log()
111
+
112
+ // Connect if requested
113
+ if (connect) {
114
+ const config = await containerManager.getConfig(containerName)
115
+ if (config) {
116
+ console.log(chalk.gray(' Opening shell...'))
117
+ console.log()
118
+ await dbEngine.connect(config)
119
+ }
120
+ } else {
121
+ console.log(chalk.gray(' Connect with:'))
122
+ console.log(chalk.cyan(` spindb connect ${containerName}`))
123
+ console.log()
124
+ }
113
125
  }
114
126
 
115
127
  function detectLocationType(location: string): {
@@ -132,8 +144,9 @@ function detectLocationType(location: string): {
132
144
  }
133
145
 
134
146
  if (existsSync(location)) {
135
- // Check if it's a SQLite file
136
- if (location.endsWith('.sqlite') || location.endsWith('.db') || location.endsWith('.sqlite3')) {
147
+ // Check if it's a SQLite file (case-insensitive)
148
+ const lowerLocation = location.toLowerCase()
149
+ if (lowerLocation.endsWith('.sqlite') || lowerLocation.endsWith('.db') || lowerLocation.endsWith('.sqlite3')) {
137
150
  return { type: 'file', inferredEngine: Engine.SQLite }
138
151
  }
139
152
  return { type: 'file' }
@@ -157,7 +170,9 @@ export const createCommand = new Command('create')
157
170
  '--max-connections <number>',
158
171
  'Maximum number of database connections (default: 200)',
159
172
  )
173
+ .option('--start', 'Start the container after creation (skip prompt)')
160
174
  .option('--no-start', 'Do not start the container after creation')
175
+ .option('--connect', 'Open a shell connection after creation')
161
176
  .option(
162
177
  '--from <location>',
163
178
  'Restore from a dump file or connection string after creation',
@@ -172,7 +187,8 @@ export const createCommand = new Command('create')
172
187
  port?: string
173
188
  path?: string
174
189
  maxConnections?: string
175
- start: boolean
190
+ start?: boolean
191
+ connect?: boolean
176
192
  from?: string
177
193
  },
178
194
  ) => {
@@ -238,6 +254,16 @@ export const createCommand = new Command('create')
238
254
 
239
255
  database = database ?? containerName
240
256
 
257
+ // Validate database name to prevent SQL injection
258
+ if (!isValidDatabaseName(database)) {
259
+ console.error(
260
+ error(
261
+ 'Database name must start with a letter and contain only letters, numbers, hyphens, and underscores',
262
+ ),
263
+ )
264
+ process.exit(1)
265
+ }
266
+
241
267
  console.log(header('Creating Database Container'))
242
268
  console.log()
243
269
 
@@ -248,10 +274,21 @@ export const createCommand = new Command('create')
248
274
  await createSqliteContainer(containerName, dbEngine, version, {
249
275
  path: options.path,
250
276
  from: restoreLocation,
277
+ connect: options.connect,
251
278
  })
252
279
  return
253
280
  }
254
281
 
282
+ // For server databases, validate --connect with --no-start
283
+ if (options.connect && options.start === false) {
284
+ console.error(
285
+ error(
286
+ 'Cannot use --no-start with --connect (connection requires running container)',
287
+ ),
288
+ )
289
+ process.exit(1)
290
+ }
291
+
255
292
  const depsSpinner = createSpinner('Checking required tools...')
256
293
  depsSpinner.start()
257
294
 
@@ -384,9 +421,12 @@ export const createCommand = new Command('create')
384
421
  throw err
385
422
  }
386
423
 
387
- // --from requires start, --no-start skips, otherwise ask user
424
+ // --from requires start, --start forces start, --no-start skips, otherwise ask user
425
+ // --connect implies --start for server databases
388
426
  let shouldStart = false
389
- if (restoreLocation) {
427
+ if (restoreLocation || options.connect) {
428
+ shouldStart = true
429
+ } else if (options.start === true) {
390
430
  shouldStart = true
391
431
  } else if (options.start === false) {
392
432
  shouldStart = false
@@ -542,7 +582,7 @@ export const createCommand = new Command('create')
542
582
  createDatabase: false,
543
583
  })
544
584
 
545
- if (result.code === 0 || !result.stderr) {
585
+ if (result.code === 0) {
546
586
  restoreSpinner.succeed('Backup restored successfully')
547
587
  } else {
548
588
  restoreSpinner.warn('Restore completed with warnings')
@@ -573,7 +613,17 @@ export const createCommand = new Command('create')
573
613
  )
574
614
  console.log()
575
615
 
576
- if (shouldStart) {
616
+ if (options.connect && shouldStart) {
617
+ // --connect flag: open shell directly
618
+ const copied =
619
+ await platformService.copyToClipboard(connectionString)
620
+ if (copied) {
621
+ console.log(chalk.gray(' Connection string copied to clipboard'))
622
+ }
623
+ console.log(chalk.gray(' Opening shell...'))
624
+ console.log()
625
+ await dbEngine.connect(finalConfig, database)
626
+ } else if (shouldStart) {
577
627
  console.log(chalk.gray(' Connect with:'))
578
628
  console.log(chalk.cyan(` spindb connect ${containerName}`))
579
629
 
@@ -582,12 +632,12 @@ export const createCommand = new Command('create')
582
632
  if (copied) {
583
633
  console.log(chalk.gray(' Connection string copied to clipboard'))
584
634
  }
635
+ console.log()
585
636
  } else {
586
637
  console.log(chalk.gray(' Start the container:'))
587
638
  console.log(chalk.cyan(` spindb start ${containerName}`))
639
+ console.log()
588
640
  }
589
-
590
- console.log()
591
641
  }
592
642
  } catch (err) {
593
643
  const e = err as Error
@@ -247,10 +247,6 @@ export const doctorCommand = new Command('doctor')
247
247
  .description('Check system health and fix common issues')
248
248
  .option('--json', 'Output as JSON')
249
249
  .action(async (options: { json?: boolean }) => {
250
- console.log()
251
- console.log(header('SpinDB Health Check'))
252
- console.log()
253
-
254
250
  const checks = [
255
251
  await checkConfiguration(),
256
252
  await checkContainers(),
@@ -265,6 +261,11 @@ export const doctorCommand = new Command('doctor')
265
261
  return
266
262
  }
267
263
 
264
+ // Human-readable output - print header first
265
+ console.log()
266
+ console.log(header('SpinDB Health Check'))
267
+ console.log()
268
+
268
269
  // Display results
269
270
  for (const check of checks) {
270
271
  displayResult(check)
@@ -37,7 +37,7 @@ async function promptEditAction(
37
37
  }
38
38
 
39
39
  // Only show config option for engines that support it
40
- if (engine === 'postgresql') {
40
+ if (engine === Engine.PostgreSQL) {
41
41
  choices.push({ name: 'Edit database config (postgresql.conf)', value: 'config' })
42
42
  }
43
43
 
@@ -201,7 +201,7 @@ async function promptNewLocation(currentPath: string): Promise<string | null> {
201
201
  default: currentPath,
202
202
  validate: (input: string) => {
203
203
  if (!input.trim()) return 'Path is required'
204
- const resolvedPath = resolve(input)
204
+ const resolvedPath = resolve(input).toLowerCase()
205
205
  if (!resolvedPath.endsWith('.sqlite') && !resolvedPath.endsWith('.db') && !resolvedPath.endsWith('.sqlite3')) {
206
206
  return 'Path should end with .sqlite, .sqlite3, or .db'
207
207
  }
@@ -518,7 +518,7 @@ export const editCommand = new Command('edit')
518
518
  // Handle config change
519
519
  if (options.setConfig) {
520
520
  // Only PostgreSQL supports config editing for now
521
- if (config.engine !== 'postgresql') {
521
+ if (config.engine !== Engine.PostgreSQL) {
522
522
  console.error(
523
523
  error(`Config editing is only supported for PostgreSQL containers`),
524
524
  )
@@ -556,10 +556,12 @@ export const editCommand = new Command('edit')
556
556
  configKey,
557
557
  configValue,
558
558
  )
559
+ spinner.succeed(`Set ${configKey} = ${configValue}`)
560
+ } else {
561
+ spinner.fail('Config editing not supported for this engine')
562
+ process.exit(1)
559
563
  }
560
564
 
561
- spinner.succeed(`Set ${configKey} = ${configValue}`)
562
-
563
565
  // Check if container is running and warn about restart
564
566
  const running = await processManager.isRunning(containerName, {
565
567
  engine: config.engine,
@@ -12,8 +12,17 @@ import {
12
12
  getInstalledPostgresEngines,
13
13
  type InstalledPostgresEngine,
14
14
  type InstalledMysqlEngine,
15
+ type InstalledSqliteEngine,
15
16
  } from '../helpers'
16
17
 
18
+ /**
19
+ * Pad string to width, accounting for emoji taking 2 display columns
20
+ */
21
+ function padWithEmoji(str: string, width: number): string {
22
+ // Count emojis using Extended_Pictographic (excludes digits/symbols that \p{Emoji} matches)
23
+ const emojiCount = (str.match(/\p{Extended_Pictographic}/gu) || []).length
24
+ return str.padEnd(width + emojiCount)
25
+ }
17
26
 
18
27
  /**
19
28
  * List subcommand action
@@ -41,13 +50,16 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
41
50
  return
42
51
  }
43
52
 
44
- // Separate PostgreSQL and MySQL
53
+ // Separate engines by type
45
54
  const pgEngines = engines.filter(
46
55
  (e): e is InstalledPostgresEngine => e.engine === 'postgresql',
47
56
  )
48
57
  const mysqlEngine = engines.find(
49
58
  (e): e is InstalledMysqlEngine => e.engine === 'mysql',
50
59
  )
60
+ const sqliteEngine = engines.find(
61
+ (e): e is InstalledSqliteEngine => e.engine === 'sqlite',
62
+ )
51
63
 
52
64
  // Calculate total size for PostgreSQL
53
65
  const totalPgSize = pgEngines.reduce((acc, e) => acc + e.sizeBytes, 0)
@@ -67,10 +79,11 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
67
79
  for (const engine of pgEngines) {
68
80
  const icon = getEngineIcon(engine.engine)
69
81
  const platformInfo = `${engine.platform}-${engine.arch}`
82
+ const engineDisplay = `${icon} ${engine.engine}`
70
83
 
71
84
  console.log(
72
85
  chalk.gray(' ') +
73
- chalk.cyan(`${icon} ${engine.engine}`.padEnd(13)) +
86
+ chalk.cyan(padWithEmoji(engineDisplay, 13)) +
74
87
  chalk.yellow(engine.version.padEnd(12)) +
75
88
  chalk.gray(platformInfo.padEnd(18)) +
76
89
  chalk.white(formatBytes(engine.sizeBytes)),
@@ -81,16 +94,31 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
81
94
  if (mysqlEngine) {
82
95
  const icon = ENGINE_ICONS.mysql
83
96
  const displayName = mysqlEngine.isMariaDB ? 'mariadb' : 'mysql'
97
+ const engineDisplay = `${icon} ${displayName}`
84
98
 
85
99
  console.log(
86
100
  chalk.gray(' ') +
87
- chalk.cyan(`${icon} ${displayName}`.padEnd(13)) +
101
+ chalk.cyan(padWithEmoji(engineDisplay, 13)) +
88
102
  chalk.yellow(mysqlEngine.version.padEnd(12)) +
89
103
  chalk.gray('system'.padEnd(18)) +
90
104
  chalk.gray('(system-installed)'),
91
105
  )
92
106
  }
93
107
 
108
+ // SQLite row
109
+ if (sqliteEngine) {
110
+ const icon = ENGINE_ICONS.sqlite
111
+ const engineDisplay = `${icon} sqlite`
112
+
113
+ console.log(
114
+ chalk.gray(' ') +
115
+ chalk.cyan(padWithEmoji(engineDisplay, 13)) +
116
+ chalk.yellow(sqliteEngine.version.padEnd(12)) +
117
+ chalk.gray('system'.padEnd(18)) +
118
+ chalk.gray('(system-installed)'),
119
+ )
120
+ }
121
+
94
122
  console.log(chalk.gray(' ' + '─'.repeat(55)))
95
123
 
96
124
  // Summary
@@ -105,6 +133,9 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
105
133
  if (mysqlEngine) {
106
134
  console.log(chalk.gray(` MySQL: system-installed at ${mysqlEngine.path}`))
107
135
  }
136
+ if (sqliteEngine) {
137
+ console.log(chalk.gray(` SQLite: system-installed at ${sqliteEngine.path}`))
138
+ }
108
139
  console.log()
109
140
  }
110
141
 
@@ -2,6 +2,7 @@ import { Command } from 'commander'
2
2
  import chalk from 'chalk'
3
3
  import inquirer from 'inquirer'
4
4
  import { existsSync } from 'fs'
5
+ import { basename } from 'path'
5
6
  import { containerManager } from '../../core/container-manager'
6
7
  import { processManager } from '../../core/process-manager'
7
8
  import { paths } from '../../config/paths'
@@ -197,8 +198,9 @@ async function displayAllContainersInfo(
197
198
  // Show truncated file path for SQLite instead of port
198
199
  let portOrPath: string
199
200
  if (isSQLite) {
200
- const fileName = container.database.split('/').pop() || container.database
201
- portOrPath = fileName.length > 7 ? fileName.slice(0, 6) + 'â€Ķ' : fileName
201
+ const fileName = basename(container.database)
202
+ // Truncate if longer than 8 chars to fit in 8-char column
203
+ portOrPath = fileName.length > 8 ? fileName.slice(0, 7) + 'â€Ķ' : fileName
202
204
  } else {
203
205
  portOrPath = String(container.port)
204
206
  }
@@ -12,8 +12,8 @@ import type { ContainerConfig } from '../../types'
12
12
  * Pad string to width, accounting for emoji taking 2 display columns
13
13
  */
14
14
  function padWithEmoji(str: string, width: number): string {
15
- // Count emojis (they take 2 display columns but count as 1-2 chars)
16
- const emojiCount = (str.match(/[\u{1F300}-\u{1F9FF}]/gu) || []).length
15
+ // Count emojis using Extended_Pictographic (excludes digits/symbols that \p{Emoji} matches)
16
+ const emojiCount = (str.match(/\p{Extended_Pictographic}/gu) || []).length
17
17
  return str.padEnd(width + emojiCount)
18
18
  }
19
19
 
@@ -107,8 +107,8 @@ export const listCommand = new Command('list')
107
107
  let portOrPath: string
108
108
  if (container.engine === Engine.SQLite) {
109
109
  const fileName = basename(container.database)
110
- // Truncate if longer than 7 chars to fit in 8-char column
111
- portOrPath = fileName.length > 7 ? fileName.slice(0, 6) + 'â€Ķ' : fileName
110
+ // Truncate if longer than 8 chars to fit in 8-char column
111
+ portOrPath = fileName.length > 8 ? fileName.slice(0, 7) + 'â€Ķ' : fileName
112
112
  } else {
113
113
  portOrPath = String(container.port)
114
114
  }
@@ -88,13 +88,19 @@ export const logsCommand = new Command('logs')
88
88
  stdio: 'inherit',
89
89
  })
90
90
 
91
- process.on('SIGINT', () => {
91
+ // Use named handler so we can remove it to prevent listener leaks
92
+ const sigintHandler = () => {
93
+ process.removeListener('SIGINT', sigintHandler)
92
94
  child.kill('SIGTERM')
93
95
  process.exit(0)
94
- })
96
+ }
97
+ process.on('SIGINT', sigintHandler)
95
98
 
96
99
  await new Promise<void>((resolve) => {
97
- child.on('close', () => resolve())
100
+ child.on('close', () => {
101
+ process.removeListener('SIGINT', sigintHandler)
102
+ resolve()
103
+ })
98
104
  })
99
105
  return
100
106
  }
@@ -403,7 +403,7 @@ export async function handleRestore(): Promise<void> {
403
403
  createDatabase: false,
404
404
  })
405
405
 
406
- if (result.code === 0 || !result.stderr) {
406
+ if (result.code === 0) {
407
407
  restoreSpinner.succeed('Backup restored successfully')
408
408
  } else {
409
409
  const stderr = result.stderr || ''
@@ -552,7 +552,7 @@ export async function handleRestore(): Promise<void> {
552
552
  }
553
553
  }
554
554
 
555
- if (result.code === 0 || !result.stderr) {
555
+ if (result.code === 0) {
556
556
  const connectionString = engine.getConnectionString(config, databaseName)
557
557
  console.log()
558
558
  console.log(success(`Database "${databaseName}" restored`))
@@ -733,6 +733,12 @@ export async function handleClone(): Promise<void> {
733
733
  )
734
734
  if (!sourceName) return
735
735
 
736
+ const sourceConfig = await containerManager.getConfig(sourceName)
737
+ if (!sourceConfig) {
738
+ console.log(error(`Container "${sourceName}" not found`))
739
+ return
740
+ }
741
+
736
742
  const { targetName } = await inquirer.prompt<{ targetName: string }>([
737
743
  {
738
744
  type: 'input',
@@ -749,16 +755,28 @@ export async function handleClone(): Promise<void> {
749
755
  },
750
756
  ])
751
757
 
758
+ // Check if target container already exists
759
+ if (await containerManager.exists(targetName, { engine: sourceConfig.engine })) {
760
+ console.log(error(`Container "${targetName}" already exists`))
761
+ return
762
+ }
763
+
752
764
  const spinner = createSpinner(`Cloning ${sourceName} to ${targetName}...`)
753
765
  spinner.start()
754
766
 
755
- const newConfig = await containerManager.clone(sourceName, targetName)
767
+ try {
768
+ const newConfig = await containerManager.clone(sourceName, targetName)
756
769
 
757
- spinner.succeed(`Cloned "${sourceName}" to "${targetName}"`)
770
+ spinner.succeed(`Cloned "${sourceName}" to "${targetName}"`)
758
771
 
759
- const engine = getEngine(newConfig.engine)
760
- const connectionString = engine.getConnectionString(newConfig)
772
+ const engine = getEngine(newConfig.engine)
773
+ const connectionString = engine.getConnectionString(newConfig)
761
774
 
762
- console.log()
763
- console.log(connectionBox(targetName, connectionString, newConfig.port))
775
+ console.log()
776
+ console.log(connectionBox(targetName, connectionString, newConfig.port))
777
+ } catch (err) {
778
+ const e = err as Error
779
+ spinner.fail(`Failed to clone "${sourceName}"`)
780
+ console.log(error(e.message))
781
+ }
764
782
  }
@@ -1043,6 +1043,12 @@ async function handleCloneFromSubmenu(
1043
1043
  sourceName: string,
1044
1044
  showMainMenu: () => Promise<void>,
1045
1045
  ): Promise<void> {
1046
+ const sourceConfig = await containerManager.getConfig(sourceName)
1047
+ if (!sourceConfig) {
1048
+ console.log(error(`Container "${sourceName}" not found`))
1049
+ return
1050
+ }
1051
+
1046
1052
  const { targetName } = await inquirer.prompt<{ targetName: string }>([
1047
1053
  {
1048
1054
  type: 'input',
@@ -1059,20 +1065,32 @@ async function handleCloneFromSubmenu(
1059
1065
  },
1060
1066
  ])
1061
1067
 
1068
+ // Check if target container already exists
1069
+ if (await containerManager.exists(targetName, { engine: sourceConfig.engine })) {
1070
+ console.log(error(`Container "${targetName}" already exists`))
1071
+ return
1072
+ }
1073
+
1062
1074
  const spinner = createSpinner(`Cloning ${sourceName} to ${targetName}...`)
1063
1075
  spinner.start()
1064
1076
 
1065
- const newConfig = await containerManager.clone(sourceName, targetName)
1077
+ try {
1078
+ const newConfig = await containerManager.clone(sourceName, targetName)
1066
1079
 
1067
- spinner.succeed(`Cloned "${sourceName}" to "${targetName}"`)
1080
+ spinner.succeed(`Cloned "${sourceName}" to "${targetName}"`)
1068
1081
 
1069
- const engine = getEngine(newConfig.engine)
1070
- const connectionString = engine.getConnectionString(newConfig)
1082
+ const engine = getEngine(newConfig.engine)
1083
+ const connectionString = engine.getConnectionString(newConfig)
1071
1084
 
1072
- console.log()
1073
- console.log(connectionBox(targetName, connectionString, newConfig.port))
1085
+ console.log()
1086
+ console.log(connectionBox(targetName, connectionString, newConfig.port))
1074
1087
 
1075
- await showContainerSubmenu(targetName, showMainMenu)
1088
+ await showContainerSubmenu(targetName, showMainMenu)
1089
+ } catch (err) {
1090
+ spinner.fail(`Failed to clone "${sourceName}"`)
1091
+ console.log(error((err as Error).message))
1092
+ await pressEnterToContinue()
1093
+ }
1076
1094
  }
1077
1095
 
1078
1096
  async function handleDelete(containerName: string): Promise<void> {