spindb 0.17.3 → 0.18.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/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  **The first npm CLI for running local databases without Docker.**
9
9
 
10
- Spin up PostgreSQL, MySQL, MariaDB, SQLite, MongoDB, Redis, and Valkey instances for local development. No Docker daemon, no container networking, no volume mounts. Just databases running on localhost, ready in seconds.
10
+ Spin up PostgreSQL, MySQL, MariaDB, SQLite, MongoDB, Redis, Valkey, and ClickHouse instances for local development. No Docker daemon, no container networking, no volume mounts. Just databases running on localhost, ready in seconds.
11
11
 
12
12
  ---
13
13
 
@@ -343,6 +343,46 @@ spindb connect myvalkey --iredis
343
343
 
344
344
  **Note:** Valkey uses `redis://` connection scheme for client compatibility since it's wire-compatible with Redis.
345
345
 
346
+ #### ClickHouse
347
+
348
+ | | |
349
+ |---|---|
350
+ | Versions | 25.12 |
351
+ | Default port | 9000 (native TCP), 8123 (HTTP) |
352
+ | Default user | `default` |
353
+ | Binary source | [hostdb](https://github.com/robertjbass/hostdb) |
354
+
355
+ ClickHouse is a column-oriented OLAP database designed for fast analytics on large datasets. SpinDB downloads ClickHouse server binaries automatically from [hostdb](https://github.com/robertjbass/hostdb) on GitHub Releases.
356
+
357
+ **Note:** ClickHouse is only available on macOS and Linux. Windows is not supported.
358
+
359
+ ```bash
360
+ # Create a ClickHouse container (downloads binaries automatically)
361
+ spindb create mydb --engine clickhouse
362
+
363
+ # Create with specific version
364
+ spindb create mydb --engine clickhouse --version 25.12
365
+
366
+ # Check what's available
367
+ spindb deps check --engine clickhouse
368
+ ```
369
+
370
+ ClickHouse uses SQL (with ClickHouse-specific extensions):
371
+
372
+ ```bash
373
+ # Create a table
374
+ spindb run mych -c "CREATE TABLE users (id UInt64, name String) ENGINE = MergeTree() ORDER BY id"
375
+
376
+ # Insert data
377
+ spindb run mych -c "INSERT INTO users VALUES (1, 'Alice'), (2, 'Bob')"
378
+
379
+ # Query data
380
+ spindb run mych -c "SELECT * FROM users"
381
+
382
+ # Run a SQL file
383
+ spindb run mych --file ./scripts/seed.sql
384
+ ```
385
+
346
386
  ### hostdb Platform Coverage
347
387
 
348
388
  SpinDB downloads database binaries from [hostdb](https://github.com/robertjbass/hostdb), a repository of pre-built database binaries for all major platforms. The following table shows current platform support and integration status:
@@ -363,6 +403,7 @@ SpinDB downloads database binaries from [hostdb](https://github.com/robertjbass/
363
403
  | MongoDB* | ✅ | ✅ | ✅ | ✅ | ✅ |
364
404
  | Redis* | ✅ | ✅ | ✅ | ✅ | ✅ |
365
405
  | Valkey | ✅ | ✅ | ✅ | ✅ | ✅ |
406
+ | ClickHouse* | ✅ | ✅ | ✅ | ✅ | ❌ |
366
407
  | **Planned for hostdb** |||||
367
408
  | CockroachDB | 🟪 | 🟪 | 🟪 | 🟪 | 🟪 |
368
409
  | TimescaleDB | 🟪 | 🟪 | 🟪 | 🟪 | 🟪 |
@@ -375,7 +416,6 @@ SpinDB downloads database binaries from [hostdb](https://github.com/robertjbass/
375
416
  | ArangoDB | 🟪 | 🟪 | 🟪 | 🟪 | 🟪 |
376
417
  | Qdrant | 🟪 | 🟪 | 🟪 | 🟪 | 🟪 |
377
418
  | Apache Cassandra | 🟪 | 🟪 | 🟪 | 🟪 | 🟪 |
378
- | ClickHouse | 🟪 | 🟪 | 🟪 | 🟪 | 🟪 |
379
419
  | InfluxDB | 🟪 | 🟪 | 🟪 | 🟪 | 🟪 |
380
420
  | CouchDB | 🟪 | 🟪 | 🟪 | 🟪 | 🟪 |
381
421
  | KeyDB | 🟪 | 🟪 | 🟪 | 🟪 | 🟪 |
@@ -386,6 +426,7 @@ SpinDB downloads database binaries from [hostdb](https://github.com/robertjbass/
386
426
  **Notes:**
387
427
  - **\*** Licensing considerations for commercial use — consider Valkey (Redis) or FerretDB (MongoDB) as alternatives
388
428
  - **PostgreSQL** uses [EDB](https://www.enterprisedb.com/) binaries on Windows instead of hostdb
429
+ - **ClickHouse** Windows binaries are not available on hostdb (macOS and Linux only)
389
430
  - **Valkey** is a Redis-compatible drop-in replacement with permissive licensing
390
431
  - **CockroachDB** is planned for both hostdb and SpinDB (see [roadmap](TODO.md))
391
432
  - All databases under "Planned for hostdb" have permissive open-source licenses (Apache 2.0, MIT, or BSD)
@@ -515,6 +515,7 @@ export const createCommand = new Command('create')
515
515
 
516
516
  try {
517
517
  await dbEngine.initDataDir(containerName, version, {
518
+ port,
518
519
  superuser: engineDefaults.superuser,
519
520
  maxConnections: options.maxConnections
520
521
  ? parseInt(options.maxConnections, 10)
@@ -51,6 +51,7 @@ import { mongodbBinaryManager } from '../../engines/mongodb/binary-manager'
51
51
  import { redisBinaryManager } from '../../engines/redis/binary-manager'
52
52
  import { valkeyBinaryManager } from '../../engines/valkey/binary-manager'
53
53
  import { sqliteBinaryManager } from '../../engines/sqlite/binary-manager'
54
+ import { clickhouseBinaryManager } from '../../engines/clickhouse/binary-manager'
54
55
 
55
56
  // Pad string to width, accounting for emoji taking 2 display columns
56
57
  function padWithEmoji(str: string, width: number): string {
@@ -1218,9 +1219,69 @@ enginesCommand
1218
1219
  return
1219
1220
  }
1220
1221
 
1222
+ if (['clickhouse', 'ch'].includes(normalizedEngine)) {
1223
+ // Check platform support
1224
+ const { platform } = platformService.getPlatformInfo()
1225
+ if (platform === 'win32') {
1226
+ console.error(
1227
+ uiError('ClickHouse is not supported on Windows via hostdb'),
1228
+ )
1229
+ console.log(
1230
+ chalk.gray(
1231
+ ' ClickHouse binaries are only available for macOS and Linux.',
1232
+ ),
1233
+ )
1234
+ process.exit(1)
1235
+ }
1236
+
1237
+ if (!version) {
1238
+ console.error(uiError('ClickHouse requires a version (e.g., 25.12)'))
1239
+ process.exit(1)
1240
+ }
1241
+
1242
+ const engine = getEngine(Engine.ClickHouse)
1243
+
1244
+ const spinner = createSpinner(
1245
+ `Checking ClickHouse ${version} binaries...`,
1246
+ )
1247
+ spinner.start()
1248
+
1249
+ let wasCached = false
1250
+ await engine.ensureBinaries(version, ({ stage, message }) => {
1251
+ if (stage === 'cached') {
1252
+ wasCached = true
1253
+ spinner.text = `ClickHouse ${version} binaries ready (cached)`
1254
+ } else {
1255
+ spinner.text = message
1256
+ }
1257
+ })
1258
+
1259
+ if (wasCached) {
1260
+ spinner.succeed(`ClickHouse ${version} binaries already installed`)
1261
+ } else {
1262
+ spinner.succeed(`ClickHouse ${version} binaries downloaded`)
1263
+ }
1264
+
1265
+ // Show the path for reference
1266
+ const { platform: chPlatform, arch: chArch } =
1267
+ platformService.getPlatformInfo()
1268
+ const chFullVersion = clickhouseBinaryManager.getFullVersion(version)
1269
+ const binPath = paths.getBinaryPath({
1270
+ engine: 'clickhouse',
1271
+ version: chFullVersion,
1272
+ platform: chPlatform,
1273
+ arch: chArch,
1274
+ })
1275
+ console.log(chalk.gray(` Location: ${binPath}`))
1276
+
1277
+ // Check for bundled client tools and install missing ones
1278
+ await checkAndInstallClientTools('clickhouse', binPath)
1279
+ return
1280
+ }
1281
+
1221
1282
  console.error(
1222
1283
  uiError(
1223
- `Unknown engine "${engineName}". Supported: postgresql, mysql, sqlite, mongodb, redis, valkey`,
1284
+ `Unknown engine "${engineName}". Supported: postgresql, mysql, sqlite, mongodb, redis, valkey, clickhouse`,
1224
1285
  ),
1225
1286
  )
1226
1287
  process.exit(1)
@@ -16,6 +16,7 @@ import {
16
16
  type InstalledMongodbEngine,
17
17
  type InstalledRedisEngine,
18
18
  type InstalledValkeyEngine,
19
+ type InstalledClickHouseEngine,
19
20
  } from '../../helpers'
20
21
 
21
22
  import { type MenuChoice } from './shared'
@@ -55,6 +56,7 @@ export async function handleEngines(): Promise<void> {
55
56
  mongodbEngines,
56
57
  redisEngines,
57
58
  valkeyEngines,
59
+ clickhouseEngines,
58
60
  ] = [
59
61
  engines.filter(
60
62
  (e): e is InstalledPostgresEngine => e.engine === 'postgresql',
@@ -65,6 +67,9 @@ export async function handleEngines(): Promise<void> {
65
67
  engines.filter((e): e is InstalledMongodbEngine => e.engine === 'mongodb'),
66
68
  engines.filter((e): e is InstalledRedisEngine => e.engine === 'redis'),
67
69
  engines.filter((e): e is InstalledValkeyEngine => e.engine === 'valkey'),
70
+ engines.filter(
71
+ (e): e is InstalledClickHouseEngine => e.engine === 'clickhouse',
72
+ ),
68
73
  ]
69
74
 
70
75
  const totalPgSize = pgEngines.reduce((acc, e) => acc + e.sizeBytes, 0)
@@ -80,6 +85,10 @@ export async function handleEngines(): Promise<void> {
80
85
  )
81
86
  const totalRedisSize = redisEngines.reduce((acc, e) => acc + e.sizeBytes, 0)
82
87
  const totalValkeySize = valkeyEngines.reduce((acc, e) => acc + e.sizeBytes, 0)
88
+ const totalClickhouseSize = clickhouseEngines.reduce(
89
+ (acc, e) => acc + e.sizeBytes,
90
+ 0,
91
+ )
83
92
 
84
93
  const COL_ENGINE = 14
85
94
  const COL_VERSION = 12
@@ -104,6 +113,7 @@ export async function handleEngines(): Promise<void> {
104
113
  ...mongodbEngines,
105
114
  ...redisEngines,
106
115
  ...valkeyEngines,
116
+ ...clickhouseEngines,
107
117
  ]
108
118
 
109
119
  for (const engine of allEnginesSorted) {
@@ -172,6 +182,13 @@ export async function handleEngines(): Promise<void> {
172
182
  ),
173
183
  )
174
184
  }
185
+ if (clickhouseEngines.length > 0) {
186
+ console.log(
187
+ chalk.gray(
188
+ ` ClickHouse: ${clickhouseEngines.length} version(s), ${formatBytes(totalClickhouseSize)}`,
189
+ ),
190
+ )
191
+ }
175
192
  console.log()
176
193
 
177
194
  const choices: MenuChoice[] = allEnginesSorted.map((e) => ({
@@ -507,6 +507,20 @@ async function launchShell(
507
507
  config.database,
508
508
  ]
509
509
  installHint = 'spindb engines download valkey'
510
+ } else if (config.engine === 'clickhouse') {
511
+ // ClickHouse uses a unified binary with subcommands
512
+ const clickhousePath = await configManager.getBinaryPath('clickhouse')
513
+ shellCmd = clickhousePath || 'clickhouse'
514
+ shellArgs = [
515
+ 'client',
516
+ '--host',
517
+ '127.0.0.1',
518
+ '--port',
519
+ String(config.port),
520
+ '--database',
521
+ config.database,
522
+ ]
523
+ installHint = 'spindb engines download clickhouse'
510
524
  } else {
511
525
  shellCmd = 'psql'
512
526
  shellArgs = [connectionString]
package/cli/constants.ts CHANGED
@@ -8,6 +8,7 @@ export const ENGINE_ICONS: Record<string, string> = {
8
8
  mongodb: '🍃',
9
9
  redis: '🔴',
10
10
  valkey: '🔷',
11
+ clickhouse: '🏠',
11
12
  }
12
13
 
13
14
  export const DEFAULT_ENGINE_ICON = '▣'
package/cli/helpers.ts CHANGED
@@ -134,6 +134,16 @@ export type InstalledValkeyEngine = {
134
134
  source: 'downloaded'
135
135
  }
136
136
 
137
+ export type InstalledClickHouseEngine = {
138
+ engine: 'clickhouse'
139
+ version: string
140
+ platform: string
141
+ arch: string
142
+ path: string
143
+ sizeBytes: number
144
+ source: 'downloaded'
145
+ }
146
+
137
147
  export type InstalledEngine =
138
148
  | InstalledPostgresEngine
139
149
  | InstalledMariadbEngine
@@ -142,6 +152,7 @@ export type InstalledEngine =
142
152
  | InstalledMongodbEngine
143
153
  | InstalledRedisEngine
144
154
  | InstalledValkeyEngine
155
+ | InstalledClickHouseEngine
145
156
 
146
157
  async function getPostgresVersion(binPath: string): Promise<string | null> {
147
158
  const ext = platformService.getExecutableExtension()
@@ -535,6 +546,61 @@ async function getInstalledValkeyEngines(): Promise<InstalledValkeyEngine[]> {
535
546
  return engines
536
547
  }
537
548
 
549
+ // Get ClickHouse version from binary path
550
+ async function getClickHouseVersion(binPath: string): Promise<string | null> {
551
+ const clickhousePath = join(binPath, 'bin', 'clickhouse')
552
+ if (!existsSync(clickhousePath)) {
553
+ return null
554
+ }
555
+
556
+ try {
557
+ const { stdout } = await execFileAsync(clickhousePath, ['client', '--version'])
558
+ // Parse output like "ClickHouse client version 25.12.3.21 (official build)"
559
+ const match = stdout.match(/version\s+([\d.]+)/)
560
+ return match ? match[1] : null
561
+ } catch {
562
+ return null
563
+ }
564
+ }
565
+
566
+ // Get installed ClickHouse engines from downloaded binaries
567
+ async function getInstalledClickHouseEngines(): Promise<InstalledClickHouseEngine[]> {
568
+ const binDir = paths.bin
569
+
570
+ if (!existsSync(binDir)) {
571
+ return []
572
+ }
573
+
574
+ const entries = await readdir(binDir, { withFileTypes: true })
575
+ const engines: InstalledClickHouseEngine[] = []
576
+
577
+ for (const entry of entries) {
578
+ if (!entry.isDirectory()) continue
579
+ if (!entry.name.startsWith('clickhouse-')) continue
580
+
581
+ const parsed = parseEngineDirectory(entry.name, 'clickhouse-', binDir)
582
+ if (!parsed) continue
583
+
584
+ const actualVersion =
585
+ (await getClickHouseVersion(parsed.path)) || parsed.version
586
+ const sizeBytes = await calculateDirectorySize(parsed.path)
587
+
588
+ engines.push({
589
+ engine: 'clickhouse',
590
+ version: actualVersion,
591
+ platform: parsed.platform,
592
+ arch: parsed.arch,
593
+ path: parsed.path,
594
+ sizeBytes,
595
+ source: 'downloaded',
596
+ })
597
+ }
598
+
599
+ engines.sort((a, b) => compareVersions(b.version, a.version))
600
+
601
+ return engines
602
+ }
603
+
538
604
  export function compareVersions(a: string, b: string): number {
539
605
  const partsA = a.split('.').map((p) => parseInt(p, 10) || 0)
540
606
  const partsB = b.split('.').map((p) => parseInt(p, 10) || 0)
@@ -571,6 +637,9 @@ export async function getInstalledEngines(): Promise<InstalledEngine[]> {
571
637
  const valkeyEngines = await getInstalledValkeyEngines()
572
638
  engines.push(...valkeyEngines)
573
639
 
640
+ const clickhouseEngines = await getInstalledClickHouseEngines()
641
+ engines.push(...clickhouseEngines)
642
+
574
643
  return engines
575
644
  }
576
645
 
@@ -581,4 +650,5 @@ export {
581
650
  getInstalledMongodbEngines,
582
651
  getInstalledRedisEngines,
583
652
  getInstalledValkeyEngines,
653
+ getInstalledClickHouseEngines,
584
654
  }
@@ -122,6 +122,22 @@ export const BACKUP_FORMATS: Record<string, EngineBackupFormats> = {
122
122
  supportsFormatChoice: true,
123
123
  defaultFormat: 'dump',
124
124
  },
125
+ clickhouse: {
126
+ sql: {
127
+ extension: '.sql',
128
+ label: '.sql',
129
+ description: 'SQL dump - DDL + INSERT statements',
130
+ spinnerLabel: 'SQL',
131
+ },
132
+ dump: {
133
+ extension: '.sql',
134
+ label: '.sql',
135
+ description: 'SQL dump - DDL + INSERT statements',
136
+ spinnerLabel: 'SQL',
137
+ },
138
+ supportsFormatChoice: false, // Only SQL format supported
139
+ defaultFormat: 'sql',
140
+ },
125
141
  }
126
142
 
127
143
  // Get backup format info for an engine
@@ -129,6 +129,20 @@ export const engineDefaults: Record<string, EngineDefaults> = {
129
129
  clientTools: ['valkey-cli'],
130
130
  maxConnections: 0, // Not applicable for Valkey
131
131
  },
132
+ clickhouse: {
133
+ defaultVersion: '25.12',
134
+ defaultPort: 9000, // Native TCP port (HTTP is 8123)
135
+ portRange: { start: 9000, end: 9100 },
136
+ supportedVersions: ['25.12'], // Keep in sync with engines/clickhouse/version-maps.ts
137
+ latestVersion: '25.12',
138
+ superuser: 'default', // Default user in ClickHouse
139
+ connectionScheme: 'clickhouse',
140
+ logFileName: 'clickhouse-server.log',
141
+ pidFileName: 'clickhouse.pid',
142
+ dataSubdir: 'data',
143
+ clientTools: ['clickhouse'],
144
+ maxConnections: 0, // Not applicable
145
+ },
132
146
  }
133
147
 
134
148
  /**
@@ -104,6 +104,22 @@
104
104
  "clientTools": ["valkey-server", "valkey-cli"],
105
105
  "licensing": "BSD-3-Clause",
106
106
  "notes": "Redis fork with permissive licensing. Shares default port 6379 with Redis; port conflicts are resolved automatically."
107
+ },
108
+ "clickhouse": {
109
+ "displayName": "ClickHouse",
110
+ "icon": "🏠",
111
+ "status": "integrated",
112
+ "binarySource": "hostdb",
113
+ "supportedVersions": ["25.12.3.21"],
114
+ "defaultVersion": "25.12.3.21",
115
+ "defaultPort": 9000,
116
+ "runtime": "server",
117
+ "queryLanguage": "sql",
118
+ "connectionScheme": "clickhouse",
119
+ "superuser": "default",
120
+ "clientTools": ["clickhouse"],
121
+ "licensing": "Apache-2.0",
122
+ "notes": "Column-oriented OLAP database. Uses YY.MM.X.build versioning. Native port 9000, HTTP port 8123."
107
123
  }
108
124
  }
109
125
  }
@@ -651,6 +651,41 @@ const valkeyDependencies: EngineDependencies = {
651
651
  ],
652
652
  }
653
653
 
654
+ // =============================================================================
655
+ // ClickHouse Dependencies
656
+ // =============================================================================
657
+
658
+ const clickhouseDependencies: EngineDependencies = {
659
+ engine: 'clickhouse',
660
+ displayName: 'ClickHouse',
661
+ dependencies: [
662
+ {
663
+ name: 'clickhouse',
664
+ binary: 'clickhouse',
665
+ description: 'ClickHouse server and client (unified binary)',
666
+ packages: {
667
+ brew: { package: 'clickhouse' },
668
+ // ClickHouse requires their own apt repository
669
+ },
670
+ manualInstall: {
671
+ darwin: [
672
+ 'Install with Homebrew: brew install clickhouse',
673
+ 'Or use SpinDB: spindb engines download clickhouse 25.12',
674
+ ],
675
+ linux: [
676
+ 'ClickHouse provides official packages.',
677
+ 'Add their apt repository: https://clickhouse.com/docs/en/install#install-from-deb-packages',
678
+ 'Or use SpinDB: spindb engines download clickhouse 25.12',
679
+ ],
680
+ win32: [
681
+ 'ClickHouse does not officially support Windows.',
682
+ 'Use WSL2 with Linux installation instructions.',
683
+ ],
684
+ },
685
+ },
686
+ ],
687
+ }
688
+
654
689
  // =============================================================================
655
690
  // Optional Tools (engine-agnostic)
656
691
  // =============================================================================
@@ -807,6 +842,7 @@ export const engineDependencies: EngineDependencies[] = [
807
842
  mongodbDependencies,
808
843
  redisDependencies,
809
844
  valkeyDependencies,
845
+ clickhouseDependencies,
810
846
  ]
811
847
 
812
848
  // Get dependencies for a specific engine
@@ -120,6 +120,13 @@ export abstract class BasePlatformService {
120
120
  // Check if a process is running by PID
121
121
  abstract isProcessRunning(pid: number): boolean
122
122
 
123
+ /**
124
+ * Find process PIDs listening on a specific port
125
+ * @param port - Port number to check
126
+ * @returns Array of PIDs listening on the port (empty if none found)
127
+ */
128
+ abstract findProcessByPort(port: number): Promise<number[]>
129
+
123
130
  // Copy text to clipboard
124
131
  async copyToClipboard(text: string): Promise<boolean> {
125
132
  const config = this.getClipboardConfig()
@@ -344,6 +351,21 @@ class DarwinPlatformService extends BasePlatformService {
344
351
  }
345
352
  }
346
353
 
354
+ async findProcessByPort(port: number): Promise<number[]> {
355
+ try {
356
+ const { stdout } = await execAsync(`lsof -ti tcp:${port} 2>/dev/null || true`)
357
+ const pids = stdout
358
+ .trim()
359
+ .split('\n')
360
+ .filter(Boolean)
361
+ .map((pid) => parseInt(pid, 10))
362
+ .filter((pid) => !isNaN(pid))
363
+ return pids
364
+ } catch {
365
+ return []
366
+ }
367
+ }
368
+
347
369
  protected buildToolPath(dir: string, toolName: string): string {
348
370
  return `${dir}/${toolName}`
349
371
  }
@@ -544,6 +566,21 @@ class LinuxPlatformService extends BasePlatformService {
544
566
  }
545
567
  }
546
568
 
569
+ async findProcessByPort(port: number): Promise<number[]> {
570
+ try {
571
+ const { stdout } = await execAsync(`lsof -ti tcp:${port} 2>/dev/null || true`)
572
+ const pids = stdout
573
+ .trim()
574
+ .split('\n')
575
+ .filter(Boolean)
576
+ .map((pid) => parseInt(pid, 10))
577
+ .filter((pid) => !isNaN(pid))
578
+ return pids
579
+ } catch {
580
+ return []
581
+ }
582
+ }
583
+
547
584
  protected buildToolPath(dir: string, toolName: string): string {
548
585
  return `${dir}/${toolName}`
549
586
  }
@@ -700,6 +737,37 @@ class Win32PlatformService extends BasePlatformService {
700
737
  }
701
738
  }
702
739
 
740
+ async findProcessByPort(port: number): Promise<number[]> {
741
+ try {
742
+ // Use netstat to find PIDs listening on the port
743
+ // -a = all connections, -n = numeric, -o = owner PID
744
+ const { stdout } = await execAsync(`netstat -ano | findstr :${port}`)
745
+ const pids: number[] = []
746
+
747
+ // Parse netstat output to find LISTENING processes on this exact port
748
+ // Format: TCP 0.0.0.0:PORT 0.0.0.0:0 LISTENING PID
749
+ const lines = stdout.trim().split('\n')
750
+ for (const line of lines) {
751
+ // Match lines with LISTENING state on the specific port
752
+ const parts = line.trim().split(/\s+/)
753
+ if (parts.length >= 5 && parts[3] === 'LISTENING') {
754
+ const localAddress = parts[1]
755
+ // Check if this is the exact port (not just containing the port number)
756
+ if (localAddress.endsWith(`:${port}`)) {
757
+ const pid = parseInt(parts[4], 10)
758
+ if (!isNaN(pid) && !pids.includes(pid)) {
759
+ pids.push(pid)
760
+ }
761
+ }
762
+ }
763
+ }
764
+
765
+ return pids
766
+ } catch {
767
+ return []
768
+ }
769
+ }
770
+
703
771
  protected buildToolPath(dir: string, toolName: string): string {
704
772
  return `${dir}\\${toolName}.exe`
705
773
  }
@@ -122,6 +122,15 @@ export abstract class BaseEngine {
122
122
  throw new Error('valkey-cli not found')
123
123
  }
124
124
 
125
+ /**
126
+ * Get the path to the clickhouse client if available
127
+ * Default implementation throws; engines that can provide a bundled or
128
+ * configured clickhouse should override this method.
129
+ */
130
+ async getClickHouseClientPath(): Promise<string> {
131
+ throw new Error('clickhouse client not found')
132
+ }
133
+
125
134
  /**
126
135
  * Get the path to the sqlite3 client if available
127
136
  * Default implementation returns null; SQLite engine overrides this method.