spindb 0.1.0 → 0.2.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.
- package/.claude/settings.local.json +6 -1
- package/CLAUDE.md +42 -12
- package/README.md +24 -3
- package/TODO.md +12 -3
- package/eslint.config.js +7 -1
- package/package.json +1 -1
- package/src/cli/commands/create.ts +23 -3
- package/src/cli/commands/menu.ts +955 -142
- package/src/cli/commands/postgres-tools.ts +216 -0
- package/src/cli/commands/restore.ts +28 -0
- package/src/cli/index.ts +2 -0
- package/src/cli/ui/prompts.ts +111 -21
- package/src/cli/ui/theme.ts +54 -10
- package/src/config/defaults.ts +6 -3
- package/src/core/binary-manager.ts +42 -12
- package/src/core/container-manager.ts +53 -5
- package/src/core/port-manager.ts +76 -1
- package/src/core/postgres-binary-manager.ts +499 -0
- package/src/core/process-manager.ts +4 -4
- package/src/engines/base-engine.ts +22 -0
- package/src/engines/postgresql/binary-urls.ts +130 -12
- package/src/engines/postgresql/index.ts +40 -4
- package/src/engines/postgresql/restore.ts +20 -9
- package/src/types/index.ts +15 -13
- package/tsconfig.json +6 -3
|
@@ -88,6 +88,14 @@ export abstract class BaseEngine {
|
|
|
88
88
|
database: string,
|
|
89
89
|
): Promise<void>
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Drop a database within the container
|
|
93
|
+
*/
|
|
94
|
+
abstract dropDatabase(
|
|
95
|
+
container: ContainerConfig,
|
|
96
|
+
database: string,
|
|
97
|
+
): Promise<void>
|
|
98
|
+
|
|
91
99
|
/**
|
|
92
100
|
* Check if binaries are installed
|
|
93
101
|
*/
|
|
@@ -100,4 +108,18 @@ export abstract class BaseEngine {
|
|
|
100
108
|
version: string,
|
|
101
109
|
onProgress?: ProgressCallback,
|
|
102
110
|
): Promise<string>
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Fetch all available versions from remote source (grouped by major version)
|
|
114
|
+
* Returns a map of major version -> array of full versions (sorted latest first)
|
|
115
|
+
* Falls back to hardcoded versions if network fails
|
|
116
|
+
*/
|
|
117
|
+
async fetchAvailableVersions(): Promise<Record<string, string[]>> {
|
|
118
|
+
// Default implementation returns supported versions as single-item arrays
|
|
119
|
+
const versions: Record<string, string[]> = {}
|
|
120
|
+
for (const v of this.supportedVersions) {
|
|
121
|
+
versions[v] = [v]
|
|
122
|
+
}
|
|
123
|
+
return versions
|
|
124
|
+
}
|
|
103
125
|
}
|
|
@@ -1,15 +1,126 @@
|
|
|
1
|
+
import { platform, arch } from 'os'
|
|
1
2
|
import { defaults } from '@/config/defaults'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
+
* Fallback map of major versions to stable patch versions
|
|
6
|
+
* Used when Maven repository is unreachable
|
|
5
7
|
*/
|
|
6
|
-
export const
|
|
7
|
-
'14': '14.
|
|
8
|
-
'15': '15.
|
|
9
|
-
'16': '16.
|
|
10
|
-
'17': '17.
|
|
8
|
+
export const FALLBACK_VERSION_MAP: Record<string, string> = {
|
|
9
|
+
'14': '14.20.0',
|
|
10
|
+
'15': '15.15.0',
|
|
11
|
+
'16': '16.11.0',
|
|
12
|
+
'17': '17.7.0',
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Supported major versions (in order of display)
|
|
17
|
+
*/
|
|
18
|
+
export const SUPPORTED_MAJOR_VERSIONS = ['14', '15', '16', '17']
|
|
19
|
+
|
|
20
|
+
// Cache for fetched versions
|
|
21
|
+
let cachedVersions: Record<string, string[]> | null = null
|
|
22
|
+
let cacheTimestamp = 0
|
|
23
|
+
const CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Fetch available versions from Maven repository
|
|
27
|
+
*/
|
|
28
|
+
export async function fetchAvailableVersions(): Promise<
|
|
29
|
+
Record<string, string[]>
|
|
30
|
+
> {
|
|
31
|
+
// Return cached versions if still valid
|
|
32
|
+
if (cachedVersions && Date.now() - cacheTimestamp < CACHE_TTL_MS) {
|
|
33
|
+
return cachedVersions
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const zonkyPlatform = getZonkyPlatform(platform(), arch())
|
|
37
|
+
if (!zonkyPlatform) {
|
|
38
|
+
throw new Error(`Unsupported platform: ${platform()}-${arch()}`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const url = `https://repo1.maven.org/maven2/io/zonky/test/postgres/embedded-postgres-binaries-${zonkyPlatform}/`
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(5000) })
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new Error(`HTTP ${response.status}`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const html = await response.text()
|
|
50
|
+
|
|
51
|
+
// Parse version directories from the HTML listing
|
|
52
|
+
// Format: <a href="14.15.0/">14.15.0/</a>
|
|
53
|
+
const versionRegex = /href="(\d+\.\d+\.\d+)\/"/g
|
|
54
|
+
const versions: string[] = []
|
|
55
|
+
let match
|
|
56
|
+
|
|
57
|
+
while ((match = versionRegex.exec(html)) !== null) {
|
|
58
|
+
versions.push(match[1])
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Group versions by major version
|
|
62
|
+
const grouped: Record<string, string[]> = {}
|
|
63
|
+
for (const major of SUPPORTED_MAJOR_VERSIONS) {
|
|
64
|
+
grouped[major] = versions
|
|
65
|
+
.filter((v) => v.startsWith(`${major}.`))
|
|
66
|
+
.sort((a, b) => compareVersions(b, a)) // Sort descending (latest first)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Cache the results
|
|
70
|
+
cachedVersions = grouped
|
|
71
|
+
cacheTimestamp = Date.now()
|
|
72
|
+
|
|
73
|
+
return grouped
|
|
74
|
+
} catch {
|
|
75
|
+
// Return fallback on any error
|
|
76
|
+
return getFallbackVersions()
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get fallback versions when network is unavailable
|
|
82
|
+
*/
|
|
83
|
+
function getFallbackVersions(): Record<string, string[]> {
|
|
84
|
+
const grouped: Record<string, string[]> = {}
|
|
85
|
+
for (const major of SUPPORTED_MAJOR_VERSIONS) {
|
|
86
|
+
grouped[major] = [FALLBACK_VERSION_MAP[major]]
|
|
87
|
+
}
|
|
88
|
+
return grouped
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Compare two version strings (e.g., "16.11.0" vs "16.9.0")
|
|
93
|
+
* Returns positive if a > b, negative if a < b, 0 if equal
|
|
94
|
+
*/
|
|
95
|
+
function compareVersions(a: string, b: string): number {
|
|
96
|
+
const partsA = a.split('.').map(Number)
|
|
97
|
+
const partsB = b.split('.').map(Number)
|
|
98
|
+
|
|
99
|
+
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
|
100
|
+
const numA = partsA[i] || 0
|
|
101
|
+
const numB = partsB[i] || 0
|
|
102
|
+
if (numA !== numB) {
|
|
103
|
+
return numA - numB
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return 0
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get the latest version for a major version
|
|
111
|
+
*/
|
|
112
|
+
export async function getLatestVersion(major: string): Promise<string> {
|
|
113
|
+
const versions = await fetchAvailableVersions()
|
|
114
|
+
const majorVersions = versions[major]
|
|
115
|
+
if (majorVersions && majorVersions.length > 0) {
|
|
116
|
+
return majorVersions[0] // First is latest due to descending sort
|
|
117
|
+
}
|
|
118
|
+
return FALLBACK_VERSION_MAP[major] || `${major}.0.0`
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Legacy export for backward compatibility
|
|
122
|
+
export const VERSION_MAP = FALLBACK_VERSION_MAP
|
|
123
|
+
|
|
13
124
|
/**
|
|
14
125
|
* Get the zonky.io platform identifier
|
|
15
126
|
*/
|
|
@@ -34,16 +145,23 @@ export function getBinaryUrl(
|
|
|
34
145
|
throw new Error(`Unsupported platform: ${platform}-${arch}`)
|
|
35
146
|
}
|
|
36
147
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
throw new Error(
|
|
40
|
-
`Unsupported PostgreSQL version: ${version}. Supported: ${Object.keys(VERSION_MAP).join(', ')}`,
|
|
41
|
-
)
|
|
42
|
-
}
|
|
148
|
+
// Use VERSION_MAP for major versions, otherwise treat as full version
|
|
149
|
+
const fullVersion = VERSION_MAP[version] || normalizeVersion(version)
|
|
43
150
|
|
|
44
151
|
return `https://repo1.maven.org/maven2/io/zonky/test/postgres/embedded-postgres-binaries-${zonkyPlatform}/${fullVersion}/embedded-postgres-binaries-${zonkyPlatform}-${fullVersion}.jar`
|
|
45
152
|
}
|
|
46
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Normalize version string to X.Y.Z format
|
|
156
|
+
*/
|
|
157
|
+
function normalizeVersion(version: string): string {
|
|
158
|
+
const parts = version.split('.')
|
|
159
|
+
if (parts.length === 2) {
|
|
160
|
+
return `${version}.0`
|
|
161
|
+
}
|
|
162
|
+
return version
|
|
163
|
+
}
|
|
164
|
+
|
|
47
165
|
/**
|
|
48
166
|
* Get the full version string for a major version
|
|
49
167
|
*/
|
|
@@ -8,7 +8,11 @@ import { processManager } from '@/core/process-manager'
|
|
|
8
8
|
import { configManager } from '@/core/config-manager'
|
|
9
9
|
import { paths } from '@/config/paths'
|
|
10
10
|
import { defaults } from '@/config/defaults'
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
getBinaryUrl,
|
|
13
|
+
SUPPORTED_MAJOR_VERSIONS,
|
|
14
|
+
fetchAvailableVersions,
|
|
15
|
+
} from './binary-urls'
|
|
12
16
|
import { detectBackupFormat, restoreBackup } from './restore'
|
|
13
17
|
import type {
|
|
14
18
|
ContainerConfig,
|
|
@@ -24,7 +28,15 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
24
28
|
name = 'postgresql'
|
|
25
29
|
displayName = 'PostgreSQL'
|
|
26
30
|
defaultPort = 5432
|
|
27
|
-
supportedVersions =
|
|
31
|
+
supportedVersions = SUPPORTED_MAJOR_VERSIONS
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Fetch all available versions from Maven (grouped by major version)
|
|
35
|
+
* Falls back to hardcoded versions if network fails
|
|
36
|
+
*/
|
|
37
|
+
async fetchAvailableVersions(): Promise<Record<string, string[]>> {
|
|
38
|
+
return fetchAvailableVersions()
|
|
39
|
+
}
|
|
28
40
|
|
|
29
41
|
/**
|
|
30
42
|
* Get current platform info
|
|
@@ -178,6 +190,7 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
178
190
|
port,
|
|
179
191
|
database,
|
|
180
192
|
user: defaults.superuser,
|
|
193
|
+
pgRestorePath: options.pgRestorePath as string, // Use custom path if provided
|
|
181
194
|
...(options as { format?: string }),
|
|
182
195
|
})
|
|
183
196
|
}
|
|
@@ -187,8 +200,8 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
187
200
|
*/
|
|
188
201
|
getConnectionString(container: ContainerConfig, database?: string): string {
|
|
189
202
|
const { port } = container
|
|
190
|
-
const db = database || 'postgres'
|
|
191
|
-
return `postgresql://${defaults.superuser}@
|
|
203
|
+
const db = database || container.database || 'postgres'
|
|
204
|
+
return `postgresql://${defaults.superuser}@127.0.0.1:${port}/${db}`
|
|
192
205
|
}
|
|
193
206
|
|
|
194
207
|
/**
|
|
@@ -293,6 +306,29 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
293
306
|
}
|
|
294
307
|
}
|
|
295
308
|
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Drop a database
|
|
312
|
+
*/
|
|
313
|
+
async dropDatabase(
|
|
314
|
+
container: ContainerConfig,
|
|
315
|
+
database: string,
|
|
316
|
+
): Promise<void> {
|
|
317
|
+
const { port } = container
|
|
318
|
+
const psqlPath = await this.getPsqlPath()
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
await execAsync(
|
|
322
|
+
`"${psqlPath}" -h 127.0.0.1 -p ${port} -U ${defaults.superuser} -d postgres -c 'DROP DATABASE IF EXISTS "${database}"'`,
|
|
323
|
+
)
|
|
324
|
+
} catch (error) {
|
|
325
|
+
const err = error as Error
|
|
326
|
+
// Ignore "database does not exist" error
|
|
327
|
+
if (!err.message.includes('does not exist')) {
|
|
328
|
+
throw error
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
296
332
|
}
|
|
297
333
|
|
|
298
334
|
export const postgresqlEngine = new PostgreSQLEngine()
|
|
@@ -2,6 +2,7 @@ import { readFile } from 'fs/promises'
|
|
|
2
2
|
import { exec } from 'child_process'
|
|
3
3
|
import { promisify } from 'util'
|
|
4
4
|
import { configManager } from '@/core/config-manager'
|
|
5
|
+
import { findBinaryPathFresh } from '@/core/postgres-binary-manager'
|
|
5
6
|
import type { BackupFormat, RestoreResult } from '@/types'
|
|
6
7
|
|
|
7
8
|
const execAsync = promisify(exec)
|
|
@@ -77,11 +78,12 @@ export async function detectBackupFormat(
|
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
export
|
|
81
|
+
export type RestoreOptions = {
|
|
81
82
|
port: number
|
|
82
83
|
database: string
|
|
83
84
|
user?: string
|
|
84
85
|
format?: string
|
|
86
|
+
pgRestorePath?: string
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
/**
|
|
@@ -101,19 +103,27 @@ async function getPsqlPath(): Promise<string> {
|
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
/**
|
|
104
|
-
* Get pg_restore path from config, with helpful error message
|
|
106
|
+
* Get pg_restore path from config or system PATH, with helpful error message
|
|
105
107
|
*/
|
|
106
108
|
async function getPgRestorePath(): Promise<string> {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
// First try to get from config (in case user has set a custom path)
|
|
110
|
+
const configPath = await configManager.getBinaryPath('pg_restore')
|
|
111
|
+
if (configPath) {
|
|
112
|
+
return configPath
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Fall back to finding it on the system PATH with cache refresh
|
|
116
|
+
const systemPath = await findBinaryPathFresh('pg_restore')
|
|
117
|
+
if (!systemPath) {
|
|
109
118
|
throw new Error(
|
|
110
119
|
'pg_restore not found. Install PostgreSQL client tools:\n' +
|
|
111
120
|
' macOS: brew install libpq && brew link --force libpq\n' +
|
|
112
|
-
' Ubuntu/Debian: apt install postgresql-client\n
|
|
121
|
+
' Ubuntu/Debian: apt install postgresql-client\n' +
|
|
122
|
+
' CentOS/RHEL/Fedora: yum install postgresql\n\n' +
|
|
113
123
|
'Or configure manually: spindb config set pg_restore /path/to/pg_restore',
|
|
114
124
|
)
|
|
115
125
|
}
|
|
116
|
-
return
|
|
126
|
+
return systemPath
|
|
117
127
|
}
|
|
118
128
|
|
|
119
129
|
/**
|
|
@@ -124,7 +134,7 @@ export async function restoreBackup(
|
|
|
124
134
|
backupPath: string,
|
|
125
135
|
options: RestoreOptions,
|
|
126
136
|
): Promise<RestoreResult> {
|
|
127
|
-
const { port, database, user = 'postgres', format } = options
|
|
137
|
+
const { port, database, user = 'postgres', format, pgRestorePath } = options
|
|
128
138
|
|
|
129
139
|
const detectedFormat = format || (await detectBackupFormat(backupPath)).format
|
|
130
140
|
|
|
@@ -141,7 +151,8 @@ export async function restoreBackup(
|
|
|
141
151
|
...result,
|
|
142
152
|
}
|
|
143
153
|
} else {
|
|
144
|
-
|
|
154
|
+
// Use custom path if provided, otherwise find it dynamically
|
|
155
|
+
const restorePath = pgRestorePath || (await getPgRestorePath())
|
|
145
156
|
|
|
146
157
|
try {
|
|
147
158
|
const formatFlag =
|
|
@@ -151,7 +162,7 @@ export async function restoreBackup(
|
|
|
151
162
|
? '-Ft'
|
|
152
163
|
: ''
|
|
153
164
|
const result = await execAsync(
|
|
154
|
-
`"${
|
|
165
|
+
`"${restorePath}" -h 127.0.0.1 -p ${port} -U ${user} -d ${database} --no-owner --no-privileges ${formatFlag} "${backupPath}"`,
|
|
155
166
|
{ maxBuffer: 50 * 1024 * 1024 },
|
|
156
167
|
)
|
|
157
168
|
|
package/src/types/index.ts
CHANGED
|
@@ -1,54 +1,56 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type ContainerConfig = {
|
|
2
2
|
name: string
|
|
3
3
|
engine: string
|
|
4
4
|
version: string
|
|
5
5
|
port: number
|
|
6
|
+
database: string
|
|
6
7
|
created: string
|
|
7
8
|
status: 'created' | 'running' | 'stopped'
|
|
8
9
|
clonedFrom?: string
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
export type ProgressCallback = (progress: {
|
|
13
|
+
stage: string
|
|
14
|
+
message: string
|
|
15
|
+
}) => void
|
|
14
16
|
|
|
15
|
-
export
|
|
17
|
+
export type InstalledBinary = {
|
|
16
18
|
engine: string
|
|
17
19
|
version: string
|
|
18
20
|
platform: string
|
|
19
21
|
arch: string
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
export
|
|
24
|
+
export type PortResult = {
|
|
23
25
|
port: number
|
|
24
26
|
isDefault: boolean
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
export
|
|
29
|
+
export type ProcessResult = {
|
|
28
30
|
stdout: string
|
|
29
31
|
stderr: string
|
|
30
32
|
code?: number
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
export
|
|
35
|
+
export type StatusResult = {
|
|
34
36
|
running: boolean
|
|
35
37
|
message: string
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
export
|
|
40
|
+
export type BackupFormat = {
|
|
39
41
|
format: string
|
|
40
42
|
description: string
|
|
41
43
|
restoreCommand: string
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
export
|
|
46
|
+
export type RestoreResult = {
|
|
45
47
|
format: string
|
|
46
48
|
stdout?: string
|
|
47
49
|
stderr?: string
|
|
48
50
|
code?: number
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
export
|
|
53
|
+
export type EngineInfo = {
|
|
52
54
|
name: string
|
|
53
55
|
displayName: string
|
|
54
56
|
defaultPort: number
|
|
@@ -68,7 +70,7 @@ export type BinarySource = 'bundled' | 'system' | 'custom'
|
|
|
68
70
|
/**
|
|
69
71
|
* Configuration for a single binary tool
|
|
70
72
|
*/
|
|
71
|
-
export
|
|
73
|
+
export type BinaryConfig = {
|
|
72
74
|
tool: BinaryTool
|
|
73
75
|
path: string
|
|
74
76
|
source: BinarySource
|
|
@@ -78,7 +80,7 @@ export interface BinaryConfig {
|
|
|
78
80
|
/**
|
|
79
81
|
* Global spindb configuration stored in ~/.spindb/config.json
|
|
80
82
|
*/
|
|
81
|
-
export
|
|
83
|
+
export type SpinDBConfig = {
|
|
82
84
|
// Binary paths for client tools
|
|
83
85
|
binaries: {
|
|
84
86
|
psql?: BinaryConfig
|
package/tsconfig.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "ES2022",
|
|
4
|
-
"module": "
|
|
5
|
-
"moduleResolution": "
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
6
|
"lib": ["ES2022"],
|
|
7
7
|
"outDir": "./dist",
|
|
8
8
|
"rootDir": "./src",
|
|
9
9
|
"strict": true,
|
|
10
10
|
"esModuleInterop": true,
|
|
11
|
+
"allowSyntheticDefaultImports": true,
|
|
11
12
|
"skipLibCheck": true,
|
|
12
13
|
"forceConsistentCasingInFileNames": true,
|
|
13
14
|
"resolveJsonModule": true,
|
|
@@ -17,7 +18,9 @@
|
|
|
17
18
|
"baseUrl": ".",
|
|
18
19
|
"paths": {
|
|
19
20
|
"@/*": ["./src/*"]
|
|
20
|
-
}
|
|
21
|
+
},
|
|
22
|
+
"allowImportingTsExtensions": true,
|
|
23
|
+
"noEmit": true
|
|
21
24
|
},
|
|
22
25
|
"include": ["src/**/*"],
|
|
23
26
|
"exclude": ["node_modules", "dist"]
|