spindb 0.7.0 → 0.7.5
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 +421 -294
- package/cli/commands/backup.ts +1 -30
- package/cli/commands/clone.ts +0 -6
- package/cli/commands/config.ts +7 -1
- package/cli/commands/connect.ts +1 -16
- package/cli/commands/create.ts +4 -55
- package/cli/commands/delete.ts +0 -6
- package/cli/commands/edit.ts +9 -25
- package/cli/commands/engines.ts +10 -188
- package/cli/commands/info.ts +7 -34
- package/cli/commands/list.ts +2 -18
- package/cli/commands/logs.ts +118 -0
- package/cli/commands/menu/backup-handlers.ts +749 -0
- package/cli/commands/menu/container-handlers.ts +825 -0
- package/cli/commands/menu/engine-handlers.ts +362 -0
- package/cli/commands/menu/index.ts +179 -0
- package/cli/commands/menu/shared.ts +26 -0
- package/cli/commands/menu/shell-handlers.ts +320 -0
- package/cli/commands/menu/sql-handlers.ts +194 -0
- package/cli/commands/menu/update-handlers.ts +94 -0
- package/cli/commands/restore.ts +2 -28
- package/cli/commands/run.ts +139 -0
- package/cli/commands/start.ts +2 -10
- package/cli/commands/stop.ts +0 -5
- package/cli/commands/url.ts +18 -13
- package/cli/constants.ts +10 -0
- package/cli/helpers.ts +152 -0
- package/cli/index.ts +5 -2
- package/cli/ui/prompts.ts +3 -11
- package/core/dependency-manager.ts +0 -163
- package/core/error-handler.ts +0 -26
- package/core/platform-service.ts +60 -40
- package/core/start-with-retry.ts +3 -28
- package/core/transaction-manager.ts +0 -8
- package/engines/base-engine.ts +10 -0
- package/engines/mysql/binary-detection.ts +1 -1
- package/engines/mysql/index.ts +78 -2
- package/engines/postgresql/index.ts +49 -0
- package/package.json +1 -1
- package/cli/commands/menu.ts +0 -2670
package/cli/commands/edit.ts
CHANGED
|
@@ -8,16 +8,10 @@ import { promptContainerSelect } from '../ui/prompts'
|
|
|
8
8
|
import { createSpinner } from '../ui/spinner'
|
|
9
9
|
import { error, warning, success } from '../ui/theme'
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
* Validate container name format
|
|
13
|
-
*/
|
|
14
11
|
function isValidName(name: string): boolean {
|
|
15
12
|
return /^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name)
|
|
16
13
|
}
|
|
17
14
|
|
|
18
|
-
/**
|
|
19
|
-
* Prompt for what to edit when no options provided
|
|
20
|
-
*/
|
|
21
15
|
async function promptEditAction(): Promise<'name' | 'port' | null> {
|
|
22
16
|
const { action } = await inquirer.prompt<{ action: string }>([
|
|
23
17
|
{
|
|
@@ -36,9 +30,6 @@ async function promptEditAction(): Promise<'name' | 'port' | null> {
|
|
|
36
30
|
return action as 'name' | 'port'
|
|
37
31
|
}
|
|
38
32
|
|
|
39
|
-
/**
|
|
40
|
-
* Prompt for new container name
|
|
41
|
-
*/
|
|
42
33
|
async function promptNewName(currentName: string): Promise<string | null> {
|
|
43
34
|
const { newName } = await inquirer.prompt<{ newName: string }>([
|
|
44
35
|
{
|
|
@@ -64,9 +55,6 @@ async function promptNewName(currentName: string): Promise<string | null> {
|
|
|
64
55
|
return newName
|
|
65
56
|
}
|
|
66
57
|
|
|
67
|
-
/**
|
|
68
|
-
* Prompt for new port
|
|
69
|
-
*/
|
|
70
58
|
async function promptNewPort(currentPort: number): Promise<number | null> {
|
|
71
59
|
const { newPort } = await inquirer.prompt<{ newPort: number }>([
|
|
72
60
|
{
|
|
@@ -90,6 +78,15 @@ async function promptNewPort(currentPort: number): Promise<number | null> {
|
|
|
90
78
|
return null
|
|
91
79
|
}
|
|
92
80
|
|
|
81
|
+
const portAvailable = await portManager.isPortAvailable(newPort)
|
|
82
|
+
if (!portAvailable) {
|
|
83
|
+
console.log(
|
|
84
|
+
warning(
|
|
85
|
+
`Note: Port ${newPort} is currently in use. It will be used when the container starts.`,
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
93
90
|
return newPort
|
|
94
91
|
}
|
|
95
92
|
|
|
@@ -106,7 +103,6 @@ export const editCommand = new Command('edit')
|
|
|
106
103
|
try {
|
|
107
104
|
let containerName = name
|
|
108
105
|
|
|
109
|
-
// Interactive selection if no name provided
|
|
110
106
|
if (!containerName) {
|
|
111
107
|
const containers = await containerManager.list()
|
|
112
108
|
|
|
@@ -123,14 +119,12 @@ export const editCommand = new Command('edit')
|
|
|
123
119
|
containerName = selected
|
|
124
120
|
}
|
|
125
121
|
|
|
126
|
-
// Get container config
|
|
127
122
|
const config = await containerManager.getConfig(containerName)
|
|
128
123
|
if (!config) {
|
|
129
124
|
console.error(error(`Container "${containerName}" not found`))
|
|
130
125
|
process.exit(1)
|
|
131
126
|
}
|
|
132
127
|
|
|
133
|
-
// If no options provided, prompt for what to edit
|
|
134
128
|
if (options.name === undefined && options.port === undefined) {
|
|
135
129
|
const action = await promptEditAction()
|
|
136
130
|
if (!action) return
|
|
@@ -152,9 +146,7 @@ export const editCommand = new Command('edit')
|
|
|
152
146
|
}
|
|
153
147
|
}
|
|
154
148
|
|
|
155
|
-
// Handle rename
|
|
156
149
|
if (options.name) {
|
|
157
|
-
// Validate new name
|
|
158
150
|
if (!isValidName(options.name)) {
|
|
159
151
|
console.error(
|
|
160
152
|
error(
|
|
@@ -164,7 +156,6 @@ export const editCommand = new Command('edit')
|
|
|
164
156
|
process.exit(1)
|
|
165
157
|
}
|
|
166
158
|
|
|
167
|
-
// Check if new name already exists
|
|
168
159
|
const exists = await containerManager.exists(options.name, {
|
|
169
160
|
engine: config.engine,
|
|
170
161
|
})
|
|
@@ -173,7 +164,6 @@ export const editCommand = new Command('edit')
|
|
|
173
164
|
process.exit(1)
|
|
174
165
|
}
|
|
175
166
|
|
|
176
|
-
// Check if container is running
|
|
177
167
|
const running = await processManager.isRunning(containerName, {
|
|
178
168
|
engine: config.engine,
|
|
179
169
|
})
|
|
@@ -186,7 +176,6 @@ export const editCommand = new Command('edit')
|
|
|
186
176
|
process.exit(1)
|
|
187
177
|
}
|
|
188
178
|
|
|
189
|
-
// Rename the container
|
|
190
179
|
const spinner = createSpinner(
|
|
191
180
|
`Renaming "${containerName}" to "${options.name}"...`,
|
|
192
181
|
)
|
|
@@ -196,19 +185,15 @@ export const editCommand = new Command('edit')
|
|
|
196
185
|
|
|
197
186
|
spinner.succeed(`Renamed "${containerName}" to "${options.name}"`)
|
|
198
187
|
|
|
199
|
-
// Update containerName for subsequent operations
|
|
200
188
|
containerName = options.name
|
|
201
189
|
}
|
|
202
190
|
|
|
203
|
-
// Handle port change
|
|
204
191
|
if (options.port !== undefined) {
|
|
205
|
-
// Validate port
|
|
206
192
|
if (options.port < 1 || options.port > 65535) {
|
|
207
193
|
console.error(error('Port must be between 1 and 65535'))
|
|
208
194
|
process.exit(1)
|
|
209
195
|
}
|
|
210
196
|
|
|
211
|
-
// Check port availability (warning only)
|
|
212
197
|
const portAvailable = await portManager.isPortAvailable(options.port)
|
|
213
198
|
if (!portAvailable) {
|
|
214
199
|
console.log(
|
|
@@ -218,7 +203,6 @@ export const editCommand = new Command('edit')
|
|
|
218
203
|
)
|
|
219
204
|
}
|
|
220
205
|
|
|
221
|
-
// Update the config
|
|
222
206
|
const spinner = createSpinner(`Changing port to ${options.port}...`)
|
|
223
207
|
spinner.start()
|
|
224
208
|
|
package/cli/commands/engines.ts
CHANGED
|
@@ -1,197 +1,19 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import chalk from 'chalk'
|
|
3
|
-
import {
|
|
4
|
-
import { existsSync } from 'fs'
|
|
5
|
-
import { join } from 'path'
|
|
6
|
-
import { exec } from 'child_process'
|
|
7
|
-
import { promisify } from 'util'
|
|
3
|
+
import { rm } from 'fs/promises'
|
|
8
4
|
import inquirer from 'inquirer'
|
|
9
|
-
import { paths } from '../../config/paths'
|
|
10
5
|
import { containerManager } from '../../core/container-manager'
|
|
11
6
|
import { promptConfirm } from '../ui/prompts'
|
|
12
7
|
import { createSpinner } from '../ui/spinner'
|
|
13
8
|
import { error, warning, info, formatBytes } from '../ui/theme'
|
|
9
|
+
import { getEngineIcon, ENGINE_ICONS } from '../constants'
|
|
14
10
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
getInstalledEngines,
|
|
12
|
+
getInstalledPostgresEngines,
|
|
13
|
+
type InstalledPostgresEngine,
|
|
14
|
+
type InstalledMysqlEngine,
|
|
15
|
+
} from '../helpers'
|
|
19
16
|
|
|
20
|
-
const execAsync = promisify(exec)
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Installed engine info for PostgreSQL (downloaded binaries)
|
|
24
|
-
*/
|
|
25
|
-
type InstalledPostgresEngine = {
|
|
26
|
-
engine: 'postgresql'
|
|
27
|
-
version: string
|
|
28
|
-
platform: string
|
|
29
|
-
arch: string
|
|
30
|
-
path: string
|
|
31
|
-
sizeBytes: number
|
|
32
|
-
source: 'downloaded'
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Installed engine info for MySQL (system-installed)
|
|
37
|
-
*/
|
|
38
|
-
type InstalledMysqlEngine = {
|
|
39
|
-
engine: 'mysql'
|
|
40
|
-
version: string
|
|
41
|
-
path: string
|
|
42
|
-
source: 'system'
|
|
43
|
-
isMariaDB: boolean
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
type InstalledEngine = InstalledPostgresEngine | InstalledMysqlEngine
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Get the actual PostgreSQL version from the binary
|
|
50
|
-
*/
|
|
51
|
-
async function getPostgresVersion(binPath: string): Promise<string | null> {
|
|
52
|
-
const postgresPath = join(binPath, 'bin', 'postgres')
|
|
53
|
-
if (!existsSync(postgresPath)) {
|
|
54
|
-
return null
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
const { stdout } = await execAsync(`"${postgresPath}" --version`)
|
|
59
|
-
// Output: postgres (PostgreSQL) 17.7
|
|
60
|
-
const match = stdout.match(/\(PostgreSQL\)\s+([\d.]+)/)
|
|
61
|
-
return match ? match[1] : null
|
|
62
|
-
} catch {
|
|
63
|
-
return null
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Get installed PostgreSQL engines from ~/.spindb/bin/
|
|
69
|
-
*/
|
|
70
|
-
async function getInstalledPostgresEngines(): Promise<
|
|
71
|
-
InstalledPostgresEngine[]
|
|
72
|
-
> {
|
|
73
|
-
const binDir = paths.bin
|
|
74
|
-
|
|
75
|
-
if (!existsSync(binDir)) {
|
|
76
|
-
return []
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const entries = await readdir(binDir, { withFileTypes: true })
|
|
80
|
-
const engines: InstalledPostgresEngine[] = []
|
|
81
|
-
|
|
82
|
-
for (const entry of entries) {
|
|
83
|
-
if (entry.isDirectory()) {
|
|
84
|
-
// Parse directory name: postgresql-17-darwin-arm64
|
|
85
|
-
const match = entry.name.match(/^(\w+)-(.+)-(\w+)-(\w+)$/)
|
|
86
|
-
if (match && match[1] === 'postgresql') {
|
|
87
|
-
const [, , majorVersion, platform, arch] = match
|
|
88
|
-
const dirPath = join(binDir, entry.name)
|
|
89
|
-
|
|
90
|
-
// Get actual version from the binary
|
|
91
|
-
const actualVersion =
|
|
92
|
-
(await getPostgresVersion(dirPath)) || majorVersion
|
|
93
|
-
|
|
94
|
-
// Get directory size
|
|
95
|
-
let sizeBytes = 0
|
|
96
|
-
try {
|
|
97
|
-
const files = await readdir(dirPath, { recursive: true })
|
|
98
|
-
for (const file of files) {
|
|
99
|
-
try {
|
|
100
|
-
const filePath = join(dirPath, file.toString())
|
|
101
|
-
const fileStat = await lstat(filePath)
|
|
102
|
-
if (fileStat.isFile()) {
|
|
103
|
-
sizeBytes += fileStat.size
|
|
104
|
-
}
|
|
105
|
-
} catch {
|
|
106
|
-
// Skip files we can't stat
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
} catch {
|
|
110
|
-
// Skip directories we can't read
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
engines.push({
|
|
114
|
-
engine: 'postgresql',
|
|
115
|
-
version: actualVersion,
|
|
116
|
-
platform,
|
|
117
|
-
arch,
|
|
118
|
-
path: dirPath,
|
|
119
|
-
sizeBytes,
|
|
120
|
-
source: 'downloaded',
|
|
121
|
-
})
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Sort by version descending
|
|
127
|
-
engines.sort((a, b) => compareVersions(b.version, a.version))
|
|
128
|
-
|
|
129
|
-
return engines
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Detect system-installed MySQL
|
|
134
|
-
*/
|
|
135
|
-
async function getInstalledMysqlEngine(): Promise<InstalledMysqlEngine | null> {
|
|
136
|
-
const mysqldPath = await getMysqldPath()
|
|
137
|
-
if (!mysqldPath) {
|
|
138
|
-
return null
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const version = await getMysqlVersion(mysqldPath)
|
|
142
|
-
if (!version) {
|
|
143
|
-
return null
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const mariadb = await isMariaDB()
|
|
147
|
-
|
|
148
|
-
return {
|
|
149
|
-
engine: 'mysql',
|
|
150
|
-
version,
|
|
151
|
-
path: mysqldPath,
|
|
152
|
-
source: 'system',
|
|
153
|
-
isMariaDB: mariadb,
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Get all installed engines (PostgreSQL + MySQL)
|
|
159
|
-
*/
|
|
160
|
-
async function getInstalledEngines(): Promise<InstalledEngine[]> {
|
|
161
|
-
const engines: InstalledEngine[] = []
|
|
162
|
-
|
|
163
|
-
// Get PostgreSQL engines
|
|
164
|
-
const pgEngines = await getInstalledPostgresEngines()
|
|
165
|
-
engines.push(...pgEngines)
|
|
166
|
-
|
|
167
|
-
// Get MySQL engine
|
|
168
|
-
const mysqlEngine = await getInstalledMysqlEngine()
|
|
169
|
-
if (mysqlEngine) {
|
|
170
|
-
engines.push(mysqlEngine)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return engines
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function compareVersions(a: string, b: string): number {
|
|
177
|
-
const partsA = a.split('.').map((p) => parseInt(p, 10) || 0)
|
|
178
|
-
const partsB = b.split('.').map((p) => parseInt(p, 10) || 0)
|
|
179
|
-
|
|
180
|
-
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
|
181
|
-
const numA = partsA[i] || 0
|
|
182
|
-
const numB = partsB[i] || 0
|
|
183
|
-
if (numA !== numB) return numA - numB
|
|
184
|
-
}
|
|
185
|
-
return 0
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Engine icons
|
|
190
|
-
*/
|
|
191
|
-
const engineIcons: Record<string, string> = {
|
|
192
|
-
postgresql: '🐘',
|
|
193
|
-
mysql: '🐬',
|
|
194
|
-
}
|
|
195
17
|
|
|
196
18
|
/**
|
|
197
19
|
* List subcommand action
|
|
@@ -243,7 +65,7 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
|
243
65
|
|
|
244
66
|
// PostgreSQL rows
|
|
245
67
|
for (const engine of pgEngines) {
|
|
246
|
-
const icon =
|
|
68
|
+
const icon = getEngineIcon(engine.engine)
|
|
247
69
|
const platformInfo = `${engine.platform}-${engine.arch}`
|
|
248
70
|
|
|
249
71
|
console.log(
|
|
@@ -257,7 +79,7 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
|
257
79
|
|
|
258
80
|
// MySQL row
|
|
259
81
|
if (mysqlEngine) {
|
|
260
|
-
const icon =
|
|
82
|
+
const icon = ENGINE_ICONS.mysql
|
|
261
83
|
const displayName = mysqlEngine.isMariaDB ? 'mariadb' : 'mysql'
|
|
262
84
|
|
|
263
85
|
console.log(
|
|
@@ -313,7 +135,7 @@ async function deleteEngine(
|
|
|
313
135
|
// Interactive selection if not provided
|
|
314
136
|
if (!engineName || !engineVersion) {
|
|
315
137
|
const choices = pgEngines.map((e) => ({
|
|
316
|
-
name: `${
|
|
138
|
+
name: `${getEngineIcon(e.engine)} ${e.engine} ${e.version} ${chalk.gray(`(${formatBytes(e.sizeBytes)})`)}`,
|
|
317
139
|
value: `${e.engine}:${e.version}:${e.path}`,
|
|
318
140
|
}))
|
|
319
141
|
|
package/cli/commands/info.ts
CHANGED
|
@@ -1,31 +1,19 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import chalk from 'chalk'
|
|
3
|
+
import inquirer from 'inquirer'
|
|
3
4
|
import { containerManager } from '../../core/container-manager'
|
|
4
5
|
import { processManager } from '../../core/process-manager'
|
|
5
6
|
import { paths } from '../../config/paths'
|
|
6
7
|
import { getEngine } from '../../engines'
|
|
7
8
|
import { error, info, header } from '../ui/theme'
|
|
9
|
+
import { getEngineIcon } from '../constants'
|
|
8
10
|
import type { ContainerConfig } from '../../types'
|
|
9
11
|
|
|
10
|
-
/**
|
|
11
|
-
* Engine icons
|
|
12
|
-
*/
|
|
13
|
-
const engineIcons: Record<string, string> = {
|
|
14
|
-
postgresql: '🐘',
|
|
15
|
-
mysql: '🐬',
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Format a date for display
|
|
20
|
-
*/
|
|
21
12
|
function formatDate(dateString: string): string {
|
|
22
13
|
const date = new Date(dateString)
|
|
23
14
|
return date.toLocaleString()
|
|
24
15
|
}
|
|
25
16
|
|
|
26
|
-
/**
|
|
27
|
-
* Get actual running status (not just config status)
|
|
28
|
-
*/
|
|
29
17
|
async function getActualStatus(
|
|
30
18
|
config: ContainerConfig,
|
|
31
19
|
): Promise<'running' | 'stopped'> {
|
|
@@ -35,9 +23,6 @@ async function getActualStatus(
|
|
|
35
23
|
return running ? 'running' : 'stopped'
|
|
36
24
|
}
|
|
37
25
|
|
|
38
|
-
/**
|
|
39
|
-
* Display info for a single container
|
|
40
|
-
*/
|
|
41
26
|
async function displayContainerInfo(
|
|
42
27
|
config: ContainerConfig,
|
|
43
28
|
options: { json?: boolean },
|
|
@@ -65,7 +50,7 @@ async function displayContainerInfo(
|
|
|
65
50
|
return
|
|
66
51
|
}
|
|
67
52
|
|
|
68
|
-
const icon =
|
|
53
|
+
const icon = getEngineIcon(config.engine)
|
|
69
54
|
const statusDisplay =
|
|
70
55
|
actualStatus === 'running'
|
|
71
56
|
? chalk.green('● running')
|
|
@@ -115,15 +100,11 @@ async function displayContainerInfo(
|
|
|
115
100
|
console.log()
|
|
116
101
|
}
|
|
117
102
|
|
|
118
|
-
/**
|
|
119
|
-
* Display summary info for all containers
|
|
120
|
-
*/
|
|
121
103
|
async function displayAllContainersInfo(
|
|
122
104
|
containers: ContainerConfig[],
|
|
123
105
|
options: { json?: boolean },
|
|
124
106
|
): Promise<void> {
|
|
125
107
|
if (options.json) {
|
|
126
|
-
// Get actual status for all containers
|
|
127
108
|
const containersWithStatus = await Promise.all(
|
|
128
109
|
containers.map(async (config) => {
|
|
129
110
|
const actualStatus = await getActualStatus(config)
|
|
@@ -148,7 +129,6 @@ async function displayAllContainersInfo(
|
|
|
148
129
|
console.log(header('All Containers'))
|
|
149
130
|
console.log()
|
|
150
131
|
|
|
151
|
-
// Table header
|
|
152
132
|
console.log(
|
|
153
133
|
chalk.gray(' ') +
|
|
154
134
|
chalk.bold.white('NAME'.padEnd(18)) +
|
|
@@ -160,7 +140,6 @@ async function displayAllContainersInfo(
|
|
|
160
140
|
)
|
|
161
141
|
console.log(chalk.gray(' ' + '─'.repeat(78)))
|
|
162
142
|
|
|
163
|
-
// Table rows
|
|
164
143
|
for (const container of containers) {
|
|
165
144
|
const actualStatus = await getActualStatus(container)
|
|
166
145
|
const statusDisplay =
|
|
@@ -168,7 +147,7 @@ async function displayAllContainersInfo(
|
|
|
168
147
|
? chalk.green('● running')
|
|
169
148
|
: chalk.gray('○ stopped')
|
|
170
149
|
|
|
171
|
-
const icon =
|
|
150
|
+
const icon = getEngineIcon(container.engine)
|
|
172
151
|
const engineDisplay = `${icon} ${container.engine}`
|
|
173
152
|
|
|
174
153
|
console.log(
|
|
@@ -184,7 +163,6 @@ async function displayAllContainersInfo(
|
|
|
184
163
|
|
|
185
164
|
console.log()
|
|
186
165
|
|
|
187
|
-
// Summary
|
|
188
166
|
const statusChecks = await Promise.all(
|
|
189
167
|
containers.map((c) => getActualStatus(c)),
|
|
190
168
|
)
|
|
@@ -198,7 +176,6 @@ async function displayAllContainersInfo(
|
|
|
198
176
|
)
|
|
199
177
|
console.log()
|
|
200
178
|
|
|
201
|
-
// Connection strings
|
|
202
179
|
console.log(chalk.bold.white(' Connection Strings:'))
|
|
203
180
|
console.log(chalk.gray(' ' + '─'.repeat(78)))
|
|
204
181
|
for (const container of containers) {
|
|
@@ -214,6 +191,7 @@ async function displayAllContainersInfo(
|
|
|
214
191
|
}
|
|
215
192
|
|
|
216
193
|
export const infoCommand = new Command('info')
|
|
194
|
+
.alias('status')
|
|
217
195
|
.description('Show container details')
|
|
218
196
|
.argument('[name]', 'Container name (omit to show all)')
|
|
219
197
|
.option('--json', 'Output as JSON')
|
|
@@ -226,7 +204,6 @@ export const infoCommand = new Command('info')
|
|
|
226
204
|
return
|
|
227
205
|
}
|
|
228
206
|
|
|
229
|
-
// If name provided, show single container
|
|
230
207
|
if (name) {
|
|
231
208
|
const config = await containerManager.getConfig(name)
|
|
232
209
|
if (!config) {
|
|
@@ -237,11 +214,8 @@ export const infoCommand = new Command('info')
|
|
|
237
214
|
return
|
|
238
215
|
}
|
|
239
216
|
|
|
240
|
-
// If running interactively without name, ask if they want all or specific
|
|
241
217
|
if (!options.json && process.stdout.isTTY && containers.length > 1) {
|
|
242
|
-
const { choice } = await
|
|
243
|
-
await import('inquirer')
|
|
244
|
-
).default.prompt<{
|
|
218
|
+
const { choice } = await inquirer.prompt<{
|
|
245
219
|
choice: string
|
|
246
220
|
}>([
|
|
247
221
|
{
|
|
@@ -251,7 +225,7 @@ export const infoCommand = new Command('info')
|
|
|
251
225
|
choices: [
|
|
252
226
|
{ name: 'All containers', value: 'all' },
|
|
253
227
|
...containers.map((c) => ({
|
|
254
|
-
name: `${c.name} ${chalk.gray(`(${
|
|
228
|
+
name: `${c.name} ${chalk.gray(`(${getEngineIcon(c.engine)} ${c.engine})`)}`,
|
|
255
229
|
value: c.name,
|
|
256
230
|
})),
|
|
257
231
|
],
|
|
@@ -269,7 +243,6 @@ export const infoCommand = new Command('info')
|
|
|
269
243
|
return
|
|
270
244
|
}
|
|
271
245
|
|
|
272
|
-
// Non-interactive or only one container: show all
|
|
273
246
|
await displayAllContainersInfo(containers, options)
|
|
274
247
|
} catch (err) {
|
|
275
248
|
const e = err as Error
|
package/cli/commands/list.ts
CHANGED
|
@@ -3,19 +3,9 @@ import chalk from 'chalk'
|
|
|
3
3
|
import { containerManager } from '../../core/container-manager'
|
|
4
4
|
import { getEngine } from '../../engines'
|
|
5
5
|
import { info, error, formatBytes } from '../ui/theme'
|
|
6
|
+
import { getEngineIcon } from '../constants'
|
|
6
7
|
import type { ContainerConfig } from '../../types'
|
|
7
8
|
|
|
8
|
-
/**
|
|
9
|
-
* Engine icons for display
|
|
10
|
-
*/
|
|
11
|
-
const engineIcons: Record<string, string> = {
|
|
12
|
-
postgresql: '🐘',
|
|
13
|
-
mysql: '🐬',
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Get database size for a container (only if running)
|
|
18
|
-
*/
|
|
19
9
|
async function getContainerSize(
|
|
20
10
|
container: ContainerConfig,
|
|
21
11
|
): Promise<number | null> {
|
|
@@ -39,7 +29,6 @@ export const listCommand = new Command('list')
|
|
|
39
29
|
const containers = await containerManager.list()
|
|
40
30
|
|
|
41
31
|
if (options.json) {
|
|
42
|
-
// Include sizes in JSON output
|
|
43
32
|
const containersWithSize = await Promise.all(
|
|
44
33
|
containers.map(async (container) => ({
|
|
45
34
|
...container,
|
|
@@ -55,10 +44,8 @@ export const listCommand = new Command('list')
|
|
|
55
44
|
return
|
|
56
45
|
}
|
|
57
46
|
|
|
58
|
-
// Fetch sizes for running containers in parallel
|
|
59
47
|
const sizes = await Promise.all(containers.map(getContainerSize))
|
|
60
48
|
|
|
61
|
-
// Table header
|
|
62
49
|
console.log()
|
|
63
50
|
console.log(
|
|
64
51
|
chalk.gray(' ') +
|
|
@@ -71,7 +58,6 @@ export const listCommand = new Command('list')
|
|
|
71
58
|
)
|
|
72
59
|
console.log(chalk.gray(' ' + '─'.repeat(73)))
|
|
73
60
|
|
|
74
|
-
// Table rows
|
|
75
61
|
for (let i = 0; i < containers.length; i++) {
|
|
76
62
|
const container = containers[i]
|
|
77
63
|
const size = sizes[i]
|
|
@@ -81,10 +67,9 @@ export const listCommand = new Command('list')
|
|
|
81
67
|
? chalk.green('● running')
|
|
82
68
|
: chalk.gray('○ stopped')
|
|
83
69
|
|
|
84
|
-
const engineIcon =
|
|
70
|
+
const engineIcon = getEngineIcon(container.engine)
|
|
85
71
|
const engineDisplay = `${engineIcon} ${container.engine}`
|
|
86
72
|
|
|
87
|
-
// Format size: show value if running, dash if stopped
|
|
88
73
|
const sizeDisplay = size !== null ? formatBytes(size) : chalk.gray('—')
|
|
89
74
|
|
|
90
75
|
console.log(
|
|
@@ -100,7 +85,6 @@ export const listCommand = new Command('list')
|
|
|
100
85
|
|
|
101
86
|
console.log()
|
|
102
87
|
|
|
103
|
-
// Summary
|
|
104
88
|
const running = containers.filter((c) => c.status === 'running').length
|
|
105
89
|
const stopped = containers.filter((c) => c.status !== 'running').length
|
|
106
90
|
console.log(
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { spawn } from 'child_process'
|
|
3
|
+
import { existsSync } from 'fs'
|
|
4
|
+
import { readFile } from 'fs/promises'
|
|
5
|
+
import { containerManager } from '../../core/container-manager'
|
|
6
|
+
import { paths } from '../../config/paths'
|
|
7
|
+
import { promptContainerSelect } from '../ui/prompts'
|
|
8
|
+
import { error, warning, info } 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
|
+
}
|
|
16
|
+
|
|
17
|
+
export const logsCommand = new Command('logs')
|
|
18
|
+
.description('View container logs')
|
|
19
|
+
.argument('[name]', 'Container name')
|
|
20
|
+
.option('-f, --follow', 'Follow log output (like tail -f)')
|
|
21
|
+
.option('-n, --lines <number>', 'Number of lines to show', '50')
|
|
22
|
+
.option('--editor', 'Open logs in $EDITOR')
|
|
23
|
+
.action(
|
|
24
|
+
async (
|
|
25
|
+
name: string | undefined,
|
|
26
|
+
options: { follow?: boolean; lines?: string; editor?: boolean },
|
|
27
|
+
) => {
|
|
28
|
+
try {
|
|
29
|
+
let containerName = name
|
|
30
|
+
|
|
31
|
+
if (!containerName) {
|
|
32
|
+
const containers = await containerManager.list()
|
|
33
|
+
|
|
34
|
+
if (containers.length === 0) {
|
|
35
|
+
console.log(warning('No containers found'))
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const selected = await promptContainerSelect(
|
|
40
|
+
containers,
|
|
41
|
+
'Select container:',
|
|
42
|
+
)
|
|
43
|
+
if (!selected) return
|
|
44
|
+
containerName = selected
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const config = await containerManager.getConfig(containerName)
|
|
48
|
+
if (!config) {
|
|
49
|
+
console.error(error(`Container "${containerName}" not found`))
|
|
50
|
+
process.exit(1)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const logPath = paths.getContainerLogPath(config.name, {
|
|
54
|
+
engine: config.engine,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
if (!existsSync(logPath)) {
|
|
58
|
+
console.log(
|
|
59
|
+
info(
|
|
60
|
+
`No log file found for "${containerName}". The container may not have been started yet.`,
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (options.editor) {
|
|
67
|
+
const editorCmd = process.env.EDITOR || 'vi'
|
|
68
|
+
const child = spawn(editorCmd, [logPath], {
|
|
69
|
+
stdio: 'inherit',
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
await new Promise<void>((resolve, reject) => {
|
|
73
|
+
child.on('close', (code) => {
|
|
74
|
+
if (code === 0) {
|
|
75
|
+
resolve()
|
|
76
|
+
} else {
|
|
77
|
+
reject(new Error(`Editor exited with code ${code}`))
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
child.on('error', reject)
|
|
81
|
+
})
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (options.follow) {
|
|
86
|
+
const lineCount = parseInt(options.lines || '50', 10)
|
|
87
|
+
const child = spawn('tail', ['-n', String(lineCount), '-f', logPath], {
|
|
88
|
+
stdio: 'inherit',
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
process.on('SIGINT', () => {
|
|
92
|
+
child.kill('SIGTERM')
|
|
93
|
+
process.exit(0)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
await new Promise<void>((resolve) => {
|
|
97
|
+
child.on('close', () => resolve())
|
|
98
|
+
})
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const lineCount = parseInt(options.lines || '50', 10)
|
|
103
|
+
const content = await readFile(logPath, 'utf-8')
|
|
104
|
+
|
|
105
|
+
if (content.trim() === '') {
|
|
106
|
+
console.log(info('Log file is empty'))
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const output = getLastNLines(content, lineCount)
|
|
111
|
+
console.log(output)
|
|
112
|
+
} catch (err) {
|
|
113
|
+
const e = err as Error
|
|
114
|
+
console.error(error(e.message))
|
|
115
|
+
process.exit(1)
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
)
|