spindb 0.9.2 → 0.10.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.
@@ -1,12 +1,18 @@
1
1
  import { Command } from 'commander'
2
2
  import chalk from 'chalk'
3
+ import inquirer from 'inquirer'
4
+ import { dirname, basename } from 'path'
3
5
  import { containerManager } from '../../core/container-manager'
4
6
  import { getEngine } from '../../engines'
5
7
  import { uiInfo, uiError, formatBytes } from '../ui/theme'
6
8
  import { getEngineIcon } from '../constants'
7
9
  import { Engine } from '../../types'
8
- import { basename } from 'path'
9
10
  import type { ContainerConfig } from '../../types'
11
+ import { sqliteRegistry } from '../../engines/sqlite/registry'
12
+ import {
13
+ scanForUnregisteredSqliteFiles,
14
+ deriveContainerName,
15
+ } from '../../engines/sqlite/scanner'
10
16
 
11
17
  /**
12
18
  * Pad string to width, accounting for emoji taking 2 display columns
@@ -17,6 +23,88 @@ function padWithEmoji(str: string, width: number): string {
17
23
  return str.padEnd(width + emojiCount)
18
24
  }
19
25
 
26
+ /**
27
+ * Prompt user about unregistered SQLite files in CWD
28
+ * Returns true if user registered any files (refresh needed)
29
+ */
30
+ async function promptUnregisteredFiles(): Promise<boolean> {
31
+ const unregistered = await scanForUnregisteredSqliteFiles()
32
+
33
+ if (unregistered.length === 0) {
34
+ return false
35
+ }
36
+
37
+ let anyRegistered = false
38
+
39
+ for (let i = 0; i < unregistered.length; i++) {
40
+ const file = unregistered[i]
41
+ const prompt =
42
+ unregistered.length > 1 ? `[${i + 1} of ${unregistered.length}] ` : ''
43
+
44
+ const { action } = await inquirer.prompt<{ action: string }>([
45
+ {
46
+ type: 'list',
47
+ name: 'action',
48
+ message: `${prompt}Unregistered SQLite database "${file.fileName}" found in current directory. Register with SpinDB?`,
49
+ choices: [
50
+ { name: 'Yes', value: 'yes' },
51
+ { name: 'No', value: 'no' },
52
+ { name: "No - don't ask again for this folder", value: 'ignore' },
53
+ ],
54
+ },
55
+ ])
56
+
57
+ if (action === 'yes') {
58
+ const suggestedName = deriveContainerName(file.fileName)
59
+ const { containerName } = await inquirer.prompt<{
60
+ containerName: string
61
+ }>([
62
+ {
63
+ type: 'input',
64
+ name: 'containerName',
65
+ message: 'Container name:',
66
+ default: suggestedName,
67
+ validate: (input: string) => {
68
+ if (!input) return 'Name is required'
69
+ if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(input)) {
70
+ return 'Name must start with a letter and contain only letters, numbers, hyphens, and underscores'
71
+ }
72
+ return true
73
+ },
74
+ },
75
+ ])
76
+
77
+ // Check if name already exists
78
+ if (await sqliteRegistry.exists(containerName)) {
79
+ console.log(
80
+ chalk.yellow(` Container "${containerName}" already exists. Skipping.`),
81
+ )
82
+ continue
83
+ }
84
+
85
+ await sqliteRegistry.add({
86
+ name: containerName,
87
+ filePath: file.absolutePath,
88
+ created: new Date().toISOString(),
89
+ })
90
+ console.log(
91
+ chalk.green(` Registered "${file.fileName}" as "${containerName}"`),
92
+ )
93
+ anyRegistered = true
94
+ } else if (action === 'ignore') {
95
+ await sqliteRegistry.addIgnoreFolder(dirname(file.absolutePath))
96
+ console.log(chalk.gray(' Folder will be ignored in future scans.'))
97
+ break // Exit early
98
+ }
99
+ }
100
+
101
+ if (anyRegistered) {
102
+ console.log() // Add spacing before list
103
+ }
104
+
105
+ return anyRegistered
106
+ }
107
+
20
108
  async function getContainerSize(
21
109
  container: ContainerConfig,
22
110
  ): Promise<number | null> {
@@ -46,8 +134,14 @@ export const listCommand = new Command('list')
46
134
  .alias('ls')
47
135
  .description('List all containers')
48
136
  .option('--json', 'Output as JSON')
49
- .action(async (options: { json?: boolean }) => {
137
+ .option('--no-scan', 'Skip scanning for unregistered SQLite files in CWD')
138
+ .action(async (options: { json?: boolean; scan?: boolean }) => {
50
139
  try {
140
+ // Scan for unregistered SQLite files in CWD (unless JSON mode or --no-scan)
141
+ if (!options.json && options.scan !== false) {
142
+ await promptUnregisteredFiles()
143
+ }
144
+
51
145
  const containers = await containerManager.list()
52
146
 
53
147
  if (options.json) {
@@ -6,13 +6,7 @@ import { containerManager } from '../../core/container-manager'
6
6
  import { paths } from '../../config/paths'
7
7
  import { promptContainerSelect } from '../ui/prompts'
8
8
  import { uiError, uiWarning, uiInfo } from '../ui/theme'
9
-
10
- function getLastNLines(content: string, n: number): string {
11
- const lines = content.split('\n')
12
- const nonEmptyLines =
13
- lines[lines.length - 1] === '' ? lines.slice(0, -1) : lines
14
- return nonEmptyLines.slice(-n).join('\n')
15
- }
9
+ import { followFile, getLastNLines } from '../utils/file-follower'
16
10
 
17
11
  export const logsCommand = new Command('logs')
18
12
  .description('View container logs')
@@ -84,28 +78,8 @@ export const logsCommand = new Command('logs')
84
78
 
85
79
  if (options.follow) {
86
80
  const lineCount = parseInt(options.lines || '50', 10)
87
- const child = spawn(
88
- 'tail',
89
- ['-n', String(lineCount), '-f', logPath],
90
- {
91
- stdio: 'inherit',
92
- },
93
- )
94
-
95
- // Use named handler so we can remove it to prevent listener leaks
96
- const sigintHandler = () => {
97
- process.removeListener('SIGINT', sigintHandler)
98
- child.kill('SIGTERM')
99
- process.exit(0)
100
- }
101
- process.on('SIGINT', sigintHandler)
102
-
103
- await new Promise<void>((resolve) => {
104
- child.on('close', () => {
105
- process.removeListener('SIGINT', sigintHandler)
106
- resolve()
107
- })
108
- })
81
+ // Use cross-platform file following (works on Windows, macOS, Linux)
82
+ await followFile(logPath, lineCount)
109
83
  return
110
84
  }
111
85
 
@@ -53,8 +53,37 @@ export async function handleCreate(): Promise<void> {
53
53
 
54
54
  const dbEngine = getEngine(engine)
55
55
  const isSQLite = engine === 'sqlite'
56
+ const isPostgreSQL = engine === 'postgresql'
57
+
58
+ // For PostgreSQL, download binaries FIRST - they include client tools (psql, pg_dump, etc.)
59
+ // This avoids requiring a separate system installation of client tools
60
+ let portAvailable = true
61
+ if (isPostgreSQL) {
62
+ portAvailable = await portManager.isPortAvailable(port)
63
+
64
+ const binarySpinner = createSpinner(
65
+ `Checking ${dbEngine.displayName} ${version} binaries...`,
66
+ )
67
+ binarySpinner.start()
68
+
69
+ const isInstalled = await dbEngine.isBinaryInstalled(version)
70
+ if (isInstalled) {
71
+ binarySpinner.succeed(
72
+ `${dbEngine.displayName} ${version} binaries ready (cached)`,
73
+ )
74
+ } else {
75
+ binarySpinner.text = `Downloading ${dbEngine.displayName} ${version} binaries...`
76
+ await dbEngine.ensureBinaries(version, ({ message }) => {
77
+ binarySpinner.text = message
78
+ })
79
+ binarySpinner.succeed(
80
+ `${dbEngine.displayName} ${version} binaries downloaded`,
81
+ )
82
+ }
83
+ }
56
84
 
57
85
  // Check dependencies (all engines need this)
86
+ // For PostgreSQL, this runs AFTER binary download so client tools are available
58
87
  const depsSpinner = createSpinner('Checking required tools...')
59
88
  depsSpinner.start()
60
89
 
@@ -89,9 +118,9 @@ export async function handleCreate(): Promise<void> {
89
118
  depsSpinner.succeed('Required tools available')
90
119
  }
91
120
 
92
- // Server databases: check port and binaries
93
- let portAvailable = true
94
- if (!isSQLite) {
121
+ // Server databases (MySQL): check port and binaries
122
+ // PostgreSQL already handled above
123
+ if (!isSQLite && !isPostgreSQL) {
95
124
  portAvailable = await portManager.isPortAvailable(port)
96
125
 
97
126
  const binarySpinner = createSpinner(
@@ -200,7 +229,9 @@ export async function handleCreate(): Promise<void> {
200
229
 
201
230
  startSpinner.succeed(`${dbEngine.displayName} started`)
202
231
 
203
- if (config && database !== 'postgres') {
232
+ // Skip creating 'postgres' database for PostgreSQL - it's created by initdb
233
+ // For other engines (MySQL, SQLite), allow creating a database named 'postgres'
234
+ if (config && !(config.engine === 'postgresql' && database === 'postgres')) {
204
235
  const dbSpinner = createSpinner(`Creating database "${database}"...`)
205
236
  dbSpinner.start()
206
237
 
@@ -532,6 +563,14 @@ export async function showContainerSubmenu(
532
563
  })
533
564
  }
534
565
 
566
+ // Detach - only for SQLite (unregisters without deleting file)
567
+ if (isSQLite) {
568
+ actionChoices.push({
569
+ name: `${chalk.yellow('⊘')} Detach from SpinDB`,
570
+ value: 'detach',
571
+ })
572
+ }
573
+
535
574
  // Delete container - SQLite can always delete, server databases must be stopped
536
575
  const canDelete = isSQLite ? true : !isRunning
537
576
  actionChoices.push({
@@ -606,6 +645,9 @@ export async function showContainerSubmenu(
606
645
  await handleCopyConnectionString(containerName)
607
646
  await showContainerSubmenu(containerName, showMainMenu)
608
647
  return
648
+ case 'detach':
649
+ await handleDetachContainer(containerName, showMainMenu)
650
+ return // Return to list after detach
609
651
  case 'delete':
610
652
  await handleDelete(containerName)
611
653
  return // Don't show submenu again after delete
@@ -1135,6 +1177,36 @@ async function handleCloneFromSubmenu(
1135
1177
  }
1136
1178
  }
1137
1179
 
1180
+ async function handleDetachContainer(
1181
+ containerName: string,
1182
+ showMainMenu: () => Promise<void>,
1183
+ ): Promise<void> {
1184
+ const confirmed = await promptConfirm(
1185
+ `Detach "${containerName}" from SpinDB? (file will be kept on disk)`,
1186
+ true,
1187
+ )
1188
+
1189
+ if (!confirmed) {
1190
+ console.log(uiWarning('Cancelled'))
1191
+ await pressEnterToContinue()
1192
+ await showContainerSubmenu(containerName, showMainMenu)
1193
+ return
1194
+ }
1195
+
1196
+ const entry = await sqliteRegistry.get(containerName)
1197
+ await sqliteRegistry.remove(containerName)
1198
+
1199
+ console.log(uiSuccess(`Detached "${containerName}" from SpinDB`))
1200
+ if (entry?.filePath) {
1201
+ console.log(chalk.gray(` File remains at: ${entry.filePath}`))
1202
+ console.log()
1203
+ console.log(chalk.gray(' Re-attach with:'))
1204
+ console.log(chalk.cyan(` spindb attach ${entry.filePath}`))
1205
+ }
1206
+ await pressEnterToContinue()
1207
+ await handleList(showMainMenu)
1208
+ }
1209
+
1138
1210
  async function handleDelete(containerName: string): Promise<void> {
1139
1211
  const config = await containerManager.getConfig(containerName)
1140
1212
  if (!config) {
@@ -13,6 +13,7 @@ import {
13
13
  } from '../../ui/prompts'
14
14
  import { uiError, uiWarning, uiInfo, uiSuccess } from '../../ui/theme'
15
15
  import { pressEnterToContinue } from './shared'
16
+ import { followFile, getLastNLines } from '../../utils/file-follower'
16
17
 
17
18
  export async function handleRunSql(containerName: string): Promise<void> {
18
19
  const config = await containerManager.getConfig(containerName)
@@ -171,28 +172,8 @@ export async function handleViewLogs(containerName: string): Promise<void> {
171
172
  if (action === 'follow') {
172
173
  console.log(chalk.gray(' Press Ctrl+C to stop following logs'))
173
174
  console.log()
174
- const child = spawn('tail', ['-n', '50', '-f', logPath], {
175
- stdio: 'inherit',
176
- })
177
- await new Promise<void>((resolve) => {
178
- let settled = false
179
-
180
- const cleanup = () => {
181
- if (!settled) {
182
- settled = true
183
- process.off('SIGINT', handleSigint)
184
- resolve()
185
- }
186
- }
187
-
188
- const handleSigint = () => {
189
- child.kill('SIGTERM')
190
- cleanup()
191
- }
192
-
193
- process.on('SIGINT', handleSigint)
194
- child.on('close', cleanup)
195
- })
175
+ // Use cross-platform file following (works on Windows, macOS, Linux)
176
+ await followFile(logPath, 50)
196
177
  return
197
178
  }
198
179
 
@@ -202,10 +183,7 @@ export async function handleViewLogs(containerName: string): Promise<void> {
202
183
  if (content.trim() === '') {
203
184
  console.log(uiInfo('Log file is empty'))
204
185
  } else {
205
- const lines = content.split('\n')
206
- const nonEmptyLines =
207
- lines[lines.length - 1] === '' ? lines.slice(0, -1) : lines
208
- console.log(nonEmptyLines.slice(-lineCount).join('\n'))
186
+ console.log(getLastNLines(content, lineCount))
209
187
  }
210
188
  console.log()
211
189
  await pressEnterToContinue()
@@ -0,0 +1,247 @@
1
+ import { Command } from 'commander'
2
+ import chalk from 'chalk'
3
+ import { existsSync } from 'fs'
4
+ import { resolve, basename } from 'path'
5
+ import { sqliteRegistry } from '../../engines/sqlite/registry'
6
+ import {
7
+ scanForUnregisteredSqliteFiles,
8
+ deriveContainerName,
9
+ } from '../../engines/sqlite/scanner'
10
+ import { containerManager } from '../../core/container-manager'
11
+ import { uiSuccess, uiError, uiInfo } from '../ui/theme'
12
+
13
+ export const sqliteCommand = new Command('sqlite').description(
14
+ 'SQLite-specific operations',
15
+ )
16
+
17
+ // sqlite scan
18
+ sqliteCommand
19
+ .command('scan')
20
+ .description('Scan folder for unregistered SQLite files')
21
+ .option(
22
+ '-p, --path <dir>',
23
+ 'Directory to scan (default: current directory)',
24
+ )
25
+ .option('--json', 'Output as JSON')
26
+ .action(async (options: { path?: string; json?: boolean }): Promise<void> => {
27
+ const dir = options.path ? resolve(options.path) : process.cwd()
28
+
29
+ if (!existsSync(dir)) {
30
+ if (options.json) {
31
+ console.log(
32
+ JSON.stringify({ error: 'Directory not found', directory: dir }),
33
+ )
34
+ } else {
35
+ console.error(uiError(`Directory not found: ${dir}`))
36
+ }
37
+ process.exit(1)
38
+ }
39
+
40
+ const unregistered = await scanForUnregisteredSqliteFiles(dir)
41
+
42
+ if (options.json) {
43
+ console.log(JSON.stringify({ directory: dir, files: unregistered }))
44
+ return
45
+ }
46
+
47
+ if (unregistered.length === 0) {
48
+ console.log(uiInfo(`No unregistered SQLite files found in ${dir}`))
49
+ return
50
+ }
51
+
52
+ console.log(
53
+ chalk.cyan(`Found ${unregistered.length} unregistered SQLite file(s):`),
54
+ )
55
+ for (const file of unregistered) {
56
+ console.log(chalk.gray(` ${file.fileName}`))
57
+ }
58
+ console.log()
59
+ console.log(chalk.gray(' Register with: spindb attach <path>'))
60
+ })
61
+
62
+ // sqlite ignore
63
+ sqliteCommand
64
+ .command('ignore')
65
+ .description('Add folder to ignore list for CWD scanning')
66
+ .argument('[folder]', 'Folder path to ignore (default: current directory)')
67
+ .option('--json', 'Output as JSON')
68
+ .action(
69
+ async (folder: string | undefined, options: { json?: boolean }): Promise<void> => {
70
+ const absolutePath = resolve(folder || process.cwd())
71
+ await sqliteRegistry.addIgnoreFolder(absolutePath)
72
+
73
+ if (options.json) {
74
+ console.log(JSON.stringify({ success: true, folder: absolutePath }))
75
+ } else {
76
+ console.log(uiSuccess(`Added to ignore list: ${absolutePath}`))
77
+ }
78
+ },
79
+ )
80
+
81
+ // sqlite unignore
82
+ sqliteCommand
83
+ .command('unignore')
84
+ .description('Remove folder from ignore list')
85
+ .argument('[folder]', 'Folder path to unignore (default: current directory)')
86
+ .option('--json', 'Output as JSON')
87
+ .action(
88
+ async (folder: string | undefined, options: { json?: boolean }): Promise<void> => {
89
+ const absolutePath = resolve(folder || process.cwd())
90
+ const removed = await sqliteRegistry.removeIgnoreFolder(absolutePath)
91
+
92
+ if (options.json) {
93
+ console.log(JSON.stringify({ success: removed, folder: absolutePath }))
94
+ } else {
95
+ if (removed) {
96
+ console.log(uiSuccess(`Removed from ignore list: ${absolutePath}`))
97
+ } else {
98
+ console.log(uiInfo(`Folder was not in ignore list: ${absolutePath}`))
99
+ }
100
+ }
101
+ },
102
+ )
103
+
104
+ // sqlite ignored (list ignored folders)
105
+ sqliteCommand
106
+ .command('ignored')
107
+ .description('List ignored folders')
108
+ .option('--json', 'Output as JSON')
109
+ .action(async (options: { json?: boolean }): Promise<void> => {
110
+ const folders = await sqliteRegistry.listIgnoredFolders()
111
+
112
+ if (options.json) {
113
+ console.log(JSON.stringify({ folders }))
114
+ return
115
+ }
116
+
117
+ if (folders.length === 0) {
118
+ console.log(uiInfo('No folders are being ignored'))
119
+ return
120
+ }
121
+
122
+ console.log(chalk.cyan('Ignored folders:'))
123
+ for (const folder of folders) {
124
+ console.log(chalk.gray(` ${folder}`))
125
+ }
126
+ })
127
+
128
+ // sqlite attach (alias to top-level attach)
129
+ sqliteCommand
130
+ .command('attach')
131
+ .description('Register an existing SQLite database (alias for "spindb attach")')
132
+ .argument('<path>', 'Path to SQLite database file')
133
+ .option('-n, --name <name>', 'Container name')
134
+ .option('--json', 'Output as JSON')
135
+ .action(
136
+ async (
137
+ path: string,
138
+ options: { name?: string; json?: boolean },
139
+ ): Promise<void> => {
140
+ try {
141
+ const absolutePath = resolve(path)
142
+
143
+ if (!existsSync(absolutePath)) {
144
+ if (options.json) {
145
+ console.log(
146
+ JSON.stringify({ success: false, error: 'File not found' }),
147
+ )
148
+ } else {
149
+ console.error(uiError(`File not found: ${absolutePath}`))
150
+ }
151
+ process.exit(1)
152
+ }
153
+
154
+ if (await sqliteRegistry.isPathRegistered(absolutePath)) {
155
+ const entry = await sqliteRegistry.getByPath(absolutePath)
156
+ if (options.json) {
157
+ console.log(
158
+ JSON.stringify({
159
+ success: false,
160
+ error: 'Already registered',
161
+ existingName: entry?.name,
162
+ }),
163
+ )
164
+ } else {
165
+ console.error(
166
+ uiError(`File is already registered as "${entry?.name}"`),
167
+ )
168
+ }
169
+ process.exit(1)
170
+ }
171
+
172
+ const containerName =
173
+ options.name || deriveContainerName(basename(absolutePath))
174
+
175
+ if (await containerManager.exists(containerName)) {
176
+ if (options.json) {
177
+ console.log(
178
+ JSON.stringify({
179
+ success: false,
180
+ error: 'Container name already exists',
181
+ }),
182
+ )
183
+ } else {
184
+ console.error(uiError(`Container "${containerName}" already exists`))
185
+ }
186
+ process.exit(1)
187
+ }
188
+
189
+ await sqliteRegistry.add({
190
+ name: containerName,
191
+ filePath: absolutePath,
192
+ created: new Date().toISOString(),
193
+ })
194
+
195
+ if (options.json) {
196
+ console.log(
197
+ JSON.stringify({
198
+ success: true,
199
+ name: containerName,
200
+ filePath: absolutePath,
201
+ }),
202
+ )
203
+ } else {
204
+ console.log(
205
+ uiSuccess(
206
+ `Registered "${basename(absolutePath)}" as "${containerName}"`,
207
+ ),
208
+ )
209
+ console.log()
210
+ console.log(chalk.gray(' Connect with:'))
211
+ console.log(chalk.cyan(` spindb connect ${containerName}`))
212
+ }
213
+ } catch (error) {
214
+ const e = error as Error
215
+ if (options.json) {
216
+ console.log(JSON.stringify({ success: false, error: e.message }))
217
+ } else {
218
+ console.error(uiError(e.message))
219
+ }
220
+ process.exit(1)
221
+ }
222
+ },
223
+ )
224
+
225
+ // sqlite detach (alias to top-level detach)
226
+ sqliteCommand
227
+ .command('detach')
228
+ .description('Unregister a SQLite database (alias for "spindb detach")')
229
+ .argument('<name>', 'Container name')
230
+ .option('-f, --force', 'Skip confirmation')
231
+ .option('--json', 'Output as JSON')
232
+ .action(
233
+ async (
234
+ name: string,
235
+ options: { force?: boolean; json?: boolean },
236
+ ): Promise<void> => {
237
+ // Import dynamically to avoid circular dependency issues
238
+ const { detachCommand } = await import('./detach')
239
+
240
+ // Build args array
241
+ const args = ['node', 'detach', name]
242
+ if (options.force) args.push('-f')
243
+ if (options.json) args.push('--json')
244
+
245
+ await detachCommand.parseAsync(args, { from: 'node' })
246
+ },
247
+ )
package/cli/helpers.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  import { existsSync } from 'fs'
2
2
  import { readdir, lstat } from 'fs/promises'
3
3
  import { join } from 'path'
4
- import { exec, execFile } from 'child_process'
4
+ import { execFile } from 'child_process'
5
5
  import { promisify } from 'util'
6
6
  import { paths } from '../config/paths'
7
+ import { platformService } from '../core/platform-service'
7
8
  import {
8
9
  getMysqldPath,
9
10
  getMysqlVersion,
10
11
  isMariaDB,
11
12
  } from '../engines/mysql/binary-detection'
12
13
 
13
- const execAsync = promisify(exec)
14
14
  const execFileAsync = promisify(execFile)
15
15
 
16
16
  export type InstalledPostgresEngine = {
@@ -44,7 +44,8 @@ export type InstalledEngine =
44
44
  | InstalledSqliteEngine
45
45
 
46
46
  async function getPostgresVersion(binPath: string): Promise<string | null> {
47
- const postgresPath = join(binPath, 'bin', 'postgres')
47
+ const ext = platformService.getExecutableExtension()
48
+ const postgresPath = join(binPath, 'bin', `postgres${ext}`)
48
49
  if (!existsSync(postgresPath)) {
49
50
  return null
50
51
  }
@@ -140,9 +141,8 @@ async function getInstalledMysqlEngine(): Promise<InstalledMysqlEngine | null> {
140
141
 
141
142
  async function getInstalledSqliteEngine(): Promise<InstalledSqliteEngine | null> {
142
143
  try {
143
- // TODO: Use 'where sqlite3' on Windows when adding Windows support
144
- const { stdout: whichOutput } = await execAsync('which sqlite3')
145
- const sqlitePath = whichOutput.trim()
144
+ // Use platform service for cross-platform binary detection
145
+ const sqlitePath = await platformService.findToolPath('sqlite3')
146
146
  if (!sqlitePath) {
147
147
  return null
148
148
  }
package/cli/index.ts CHANGED
@@ -2,9 +2,6 @@ import { program } from 'commander'
2
2
  import { createRequire } from 'module'
3
3
  import chalk from 'chalk'
4
4
  import { createCommand } from './commands/create'
5
-
6
- const require = createRequire(import.meta.url)
7
- const pkg = require('../package.json') as { version: string }
8
5
  import { listCommand } from './commands/list'
9
6
  import { startCommand } from './commands/start'
10
7
  import { stopCommand } from './commands/stop'
@@ -25,8 +22,14 @@ import { versionCommand } from './commands/version'
25
22
  import { runCommand } from './commands/run'
26
23
  import { logsCommand } from './commands/logs'
27
24
  import { doctorCommand } from './commands/doctor'
25
+ import { attachCommand } from './commands/attach'
26
+ import { detachCommand } from './commands/detach'
27
+ import { sqliteCommand } from './commands/sqlite'
28
28
  import { updateManager } from '../core/update-manager'
29
29
 
30
+ const require = createRequire(import.meta.url)
31
+ const pkg = require('../package.json') as { version: string }
32
+
30
33
  /**
31
34
  * Show update notification banner if an update is available (from cached data)
32
35
  * This shows on every run until the user updates or disables checks
@@ -125,6 +128,9 @@ export async function run(): Promise<void> {
125
128
  program.addCommand(runCommand)
126
129
  program.addCommand(logsCommand)
127
130
  program.addCommand(doctorCommand)
131
+ program.addCommand(attachCommand)
132
+ program.addCommand(detachCommand)
133
+ program.addCommand(sqliteCommand)
128
134
 
129
135
  // If no arguments provided, show interactive menu
130
136
  if (process.argv.length <= 2) {