spindb 0.34.3 → 0.35.2

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.
Files changed (64) hide show
  1. package/README.md +4 -4
  2. package/cli/commands/attach.ts +38 -9
  3. package/cli/commands/backups.ts +5 -0
  4. package/cli/commands/connect.ts +6 -6
  5. package/cli/commands/create.ts +22 -1
  6. package/cli/commands/detach.ts +16 -9
  7. package/cli/commands/doctor.ts +2 -2
  8. package/cli/commands/duckdb.ts +273 -0
  9. package/cli/commands/edit.ts +31 -21
  10. package/cli/commands/engines.ts +51 -21
  11. package/cli/commands/info.ts +26 -16
  12. package/cli/commands/list.ts +44 -26
  13. package/cli/commands/menu/container-handlers.ts +17 -1
  14. package/cli/commands/menu/engine-handlers.ts +48 -29
  15. package/cli/commands/menu/update-handlers.ts +2 -2
  16. package/cli/commands/sqlite.ts +21 -0
  17. package/cli/index.ts +2 -0
  18. package/cli/ui/theme.ts +5 -2
  19. package/config/engines.json +2 -2
  20. package/core/base-binary-manager.ts +6 -2
  21. package/core/base-document-binary-manager.ts +5 -2
  22. package/core/base-embedded-binary-manager.ts +5 -2
  23. package/core/base-server-binary-manager.ts +5 -2
  24. package/core/hostdb-client.ts +157 -22
  25. package/core/hostdb-metadata.ts +67 -43
  26. package/engines/clickhouse/binary-urls.ts +1 -1
  27. package/engines/cockroachdb/binary-urls.ts +9 -7
  28. package/engines/cockroachdb/hostdb-releases.ts +18 -106
  29. package/engines/cockroachdb/version-maps.ts +1 -1
  30. package/engines/couchdb/binary-urls.ts +1 -1
  31. package/engines/duckdb/binary-urls.ts +1 -1
  32. package/engines/duckdb/index.ts +4 -74
  33. package/engines/duckdb/scanner.ts +22 -0
  34. package/engines/ferretdb/README.md +76 -38
  35. package/engines/ferretdb/backup.ts +18 -10
  36. package/engines/ferretdb/binary-manager.ts +233 -35
  37. package/engines/ferretdb/binary-urls.ts +69 -24
  38. package/engines/ferretdb/index.ts +424 -213
  39. package/engines/ferretdb/restore.ts +23 -16
  40. package/engines/ferretdb/version-maps.ts +36 -8
  41. package/engines/file-based-utils.ts +262 -0
  42. package/engines/index.ts +3 -4
  43. package/engines/influxdb/binary-urls.ts +1 -1
  44. package/engines/mariadb/binary-urls.ts +2 -2
  45. package/engines/meilisearch/binary-urls.ts +1 -1
  46. package/engines/mysql/binary-urls.ts +2 -2
  47. package/engines/postgresql/binary-urls.ts +1 -1
  48. package/engines/qdrant/binary-urls.ts +1 -1
  49. package/engines/questdb/binary-manager.ts +16 -9
  50. package/engines/questdb/binary-urls.ts +9 -10
  51. package/engines/questdb/hostdb-releases.ts +19 -97
  52. package/engines/questdb/version-maps.ts +2 -2
  53. package/engines/redis/binary-urls.ts +1 -8
  54. package/engines/sqlite/binary-urls.ts +1 -1
  55. package/engines/sqlite/index.ts +4 -74
  56. package/engines/sqlite/scanner.ts +11 -88
  57. package/engines/surrealdb/binary-urls.ts +9 -7
  58. package/engines/surrealdb/hostdb-releases.ts +18 -106
  59. package/engines/surrealdb/version-maps.ts +1 -1
  60. package/engines/typedb/binary-urls.ts +10 -8
  61. package/engines/typedb/hostdb-releases.ts +18 -113
  62. package/engines/typedb/version-maps.ts +1 -1
  63. package/engines/valkey/binary-urls.ts +1 -1
  64. package/package.json +4 -1
@@ -7,8 +7,13 @@ import {
7
7
  scanForUnregisteredSqliteFiles,
8
8
  deriveContainerName,
9
9
  } from '../../engines/sqlite/scanner'
10
+ import {
11
+ isValidExtensionForEngine,
12
+ formatExtensionsForEngine,
13
+ } from '../../engines/file-based-utils'
10
14
  import { containerManager } from '../../core/container-manager'
11
15
  import { uiSuccess, uiError, uiInfo } from '../ui/theme'
16
+ import { Engine } from '../../types'
12
17
  import { detachCommand } from './detach'
13
18
 
14
19
  export const sqliteCommand = new Command('sqlite').description(
@@ -146,6 +151,22 @@ sqliteCommand
146
151
  try {
147
152
  const absolutePath = resolve(path)
148
153
 
154
+ // Validate extension matches SQLite
155
+ if (!isValidExtensionForEngine(absolutePath, Engine.SQLite)) {
156
+ const msg = `File extension must be one of: ${formatExtensionsForEngine(Engine.SQLite)}`
157
+ if (options.json) {
158
+ console.log(JSON.stringify({ success: false, error: msg }))
159
+ } else {
160
+ console.error(uiError(msg))
161
+ console.log(
162
+ chalk.gray(
163
+ ' For DuckDB files, use: spindb duckdb attach <path>',
164
+ ),
165
+ )
166
+ }
167
+ process.exit(1)
168
+ }
169
+
149
170
  if (!existsSync(absolutePath)) {
150
171
  if (options.json) {
151
172
  console.log(
package/cli/index.ts CHANGED
@@ -27,6 +27,7 @@ import { doctorCommand } from './commands/doctor'
27
27
  import { attachCommand } from './commands/attach'
28
28
  import { detachCommand } from './commands/detach'
29
29
  import { sqliteCommand } from './commands/sqlite'
30
+ import { duckdbCommand } from './commands/duckdb'
30
31
  import { databasesCommand } from './commands/databases'
31
32
  import { pullCommand } from './commands/pull'
32
33
  import { whichCommand } from './commands/which'
@@ -142,6 +143,7 @@ export async function run(): Promise<void> {
142
143
  program.addCommand(attachCommand)
143
144
  program.addCommand(detachCommand)
144
145
  program.addCommand(sqliteCommand)
146
+ program.addCommand(duckdbCommand)
145
147
  program.addCommand(databasesCommand)
146
148
  program.addCommand(pullCommand)
147
149
  program.addCommand(whichCommand)
package/cli/ui/theme.ts CHANGED
@@ -117,9 +117,12 @@ export function connectionBox(
117
117
  }
118
118
 
119
119
  export function formatBytes(bytes: number): string {
120
- if (bytes === 0) return '0 B'
120
+ if (!Number.isFinite(bytes) || bytes <= 0) return '0 B'
121
121
  const units = ['B', 'KB', 'MB', 'GB', 'TB']
122
- const i = Math.floor(Math.log(bytes) / Math.log(1024))
122
+ const i = Math.max(
123
+ 0,
124
+ Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1),
125
+ )
123
126
  const value = bytes / Math.pow(1024, i)
124
127
  return `${value.toFixed(1)} ${units[i]}`
125
128
  }
@@ -185,7 +185,7 @@
185
185
  "icon": "🦔",
186
186
  "status": "integrated",
187
187
  "binarySource": "hostdb",
188
- "supportedVersions": ["2.7.0"],
188
+ "supportedVersions": ["1.24.2", "2.7.0"],
189
189
  "defaultVersion": "2.7.0",
190
190
  "defaultPort": 27017,
191
191
  "runtime": "server",
@@ -195,7 +195,7 @@
195
195
  "superuser": null,
196
196
  "clientTools": ["ferretdb", "mongosh", "mongodump", "mongorestore"],
197
197
  "licensing": "Apache-2.0",
198
- "notes": "MongoDB-compatible proxy using PostgreSQL as backend. Requires postgresql-documentdb."
198
+ "notes": "MongoDB-compatible proxy. v2 uses postgresql-documentdb (macOS/Linux). v1 uses plain PostgreSQL (all platforms incl. Windows)."
199
199
  },
200
200
  "couchdb": {
201
201
  "displayName": "CouchDB",
@@ -17,6 +17,7 @@ import { paths } from '../config/paths'
17
17
  import { spawnAsync } from './spawn-utils'
18
18
  import { moveEntry } from './fs-error-utils'
19
19
  import { logDebug } from './error-handler'
20
+ import { fetchWithRegistryFallback } from './hostdb-client'
20
21
  import {
21
22
  type Engine,
22
23
  Platform,
@@ -186,7 +187,9 @@ export abstract class BaseBinaryManager {
186
187
 
187
188
  let response: Response
188
189
  try {
189
- response = await fetch(url, { signal: controller.signal })
190
+ response = await fetchWithRegistryFallback(url, {
191
+ signal: controller.signal,
192
+ })
190
193
  } catch (error) {
191
194
  const err = error as Error
192
195
  if (err.name === 'AbortError') {
@@ -202,7 +205,7 @@ export abstract class BaseBinaryManager {
202
205
  throw new Error(
203
206
  `${this.config.displayName} ${fullVersion} binaries not found (404). ` +
204
207
  `This version may have been removed from hostdb. ` +
205
- `Try a different version or check https://github.com/robertjbass/hostdb/releases`,
208
+ `Try a different version or check https://registry.layerbase.host`,
206
209
  )
207
210
  }
208
211
  throw new Error(
@@ -451,6 +454,7 @@ export abstract class BaseBinaryManager {
451
454
  try {
452
455
  const { stdout, stderr } = await spawnAsync(serverPath, ['--version'], {
453
456
  timeout: this.verifyTimeoutMs,
457
+ cwd: binPath,
454
458
  })
455
459
  // Log stderr if present (may contain warnings)
456
460
  if (stderr && stderr.trim()) {
@@ -25,6 +25,7 @@ import { paths } from '../config/paths'
25
25
  import { spawnAsync, extractWindowsArchive } from './spawn-utils'
26
26
  import { isRenameFallbackError } from './fs-error-utils'
27
27
  import { logDebug } from './error-handler'
28
+ import { fetchWithRegistryFallback } from './hostdb-client'
28
29
  import {
29
30
  type Engine,
30
31
  Platform,
@@ -203,14 +204,16 @@ export abstract class BaseDocumentBinaryManager {
203
204
  let response: Response
204
205
  let fileStream: ReturnType<typeof createWriteStream> | null = null
205
206
  try {
206
- response = await fetch(url, { signal: controller.signal })
207
+ response = await fetchWithRegistryFallback(url, {
208
+ signal: controller.signal,
209
+ })
207
210
 
208
211
  if (!response.ok) {
209
212
  if (response.status === 404) {
210
213
  throw new Error(
211
214
  `${this.config.displayName} ${fullVersion} binaries not found (404). ` +
212
215
  `This version may have been removed from hostdb. ` +
213
- `Try a different version or check https://github.com/robertjbass/hostdb/releases`,
216
+ `Try a different version or check https://registry.layerbase.host`,
214
217
  )
215
218
  }
216
219
  throw new Error(
@@ -23,6 +23,7 @@ import { paths } from '../config/paths'
23
23
  import { spawnAsync } from './spawn-utils'
24
24
  import { moveEntry } from './fs-error-utils'
25
25
  import { compareVersions } from './version-utils'
26
+ import { fetchWithRegistryFallback } from './hostdb-client'
26
27
  import {
27
28
  type Engine,
28
29
  Platform,
@@ -201,7 +202,9 @@ export abstract class BaseEmbeddedBinaryManager {
201
202
 
202
203
  let response: Response
203
204
  try {
204
- response = await fetch(url, { signal: controller.signal })
205
+ response = await fetchWithRegistryFallback(url, {
206
+ signal: controller.signal,
207
+ })
205
208
  } catch (error) {
206
209
  const err = error as Error
207
210
  if (err.name === 'AbortError') {
@@ -220,7 +223,7 @@ export abstract class BaseEmbeddedBinaryManager {
220
223
  throw new Error(
221
224
  `${this.config.displayName} ${fullVersion} binaries not found (404). ` +
222
225
  `This version may have been removed from hostdb. ` +
223
- `Try a different version or check https://github.com/robertjbass/hostdb/releases`,
226
+ `Try a different version or check https://registry.layerbase.host`,
224
227
  )
225
228
  }
226
229
  throw new Error(
@@ -23,6 +23,7 @@ import { pipeline } from 'stream/promises'
23
23
  import { paths } from '../config/paths'
24
24
  import { spawnAsync, extractWindowsArchive } from './spawn-utils'
25
25
  import { isRenameFallbackError } from './fs-error-utils'
26
+ import { fetchWithRegistryFallback } from './hostdb-client'
26
27
  import {
27
28
  type Engine,
28
29
  Platform,
@@ -205,7 +206,9 @@ export abstract class BaseServerBinaryManager {
205
206
 
206
207
  let response: Response
207
208
  try {
208
- response = await fetch(url, { signal: controller.signal })
209
+ response = await fetchWithRegistryFallback(url, {
210
+ signal: controller.signal,
211
+ })
209
212
  } catch (error) {
210
213
  const err = error as Error
211
214
  if (err.name === 'AbortError') {
@@ -224,7 +227,7 @@ export abstract class BaseServerBinaryManager {
224
227
  throw new Error(
225
228
  `${this.config.displayName} ${fullVersion} binaries not found (404). ` +
226
229
  `This version may have been removed from hostdb. ` +
227
- `Try a different version or check https://github.com/robertjbass/hostdb/releases`,
230
+ `Try a different version or check https://registry.layerbase.host`,
228
231
  )
229
232
  }
230
233
  throw new Error(
@@ -1,15 +1,28 @@
1
1
  /**
2
2
  * Shared hostdb Client Module
3
3
  *
4
- * Provides centralized access to the hostdb repository at
5
- * https://github.com/robertjbass/hostdb
4
+ * Provides centralized access to pre-built database binaries.
5
+ * Primary registry: registry.layerbase.host
6
+ * Fallback registry: GitHub releases (robertjbass/hostdb)
6
7
  *
7
- * hostdb provides pre-built database binaries for multiple platforms.
8
8
  * This module handles fetching releases.json with caching to avoid
9
9
  * repeated network requests.
10
10
  */
11
11
 
12
12
  import { Platform, type Arch, type Engine } from '../types'
13
+ import { logDebug } from './error-handler'
14
+
15
+ // Registry base URLs
16
+ export const LAYERBASE_REGISTRY_BASE = 'https://registry.layerbase.host'
17
+ export const GITHUB_REGISTRY_BASE =
18
+ 'https://github.com/robertjbass/hostdb/releases/download'
19
+
20
+ /**
21
+ * Toggle GitHub fallback for binary downloads and releases.json fetches.
22
+ * Set to `false` to exclusively test the Layerbase registry.
23
+ * Must be re-enabled before release (see PRE_RELEASE_TASKS.md).
24
+ */
25
+ export const ENABLE_GITHUB_FALLBACK = false
13
26
 
14
27
  // Platform definition in hostdb releases.json
15
28
  export type HostdbPlatform = {
@@ -59,9 +72,21 @@ let cachedReleases: HostdbReleasesData | null = null
59
72
  let cacheTimestamp = 0
60
73
  const CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes
61
74
 
62
- const HOSTDB_RELEASES_URL =
75
+ export const LAYERBASE_RELEASES_URL =
76
+ 'https://registry.layerbase.host/releases.json'
77
+ export const GITHUB_RELEASES_URL =
63
78
  'https://raw.githubusercontent.com/robertjbass/hostdb/main/releases.json'
64
79
 
80
+ /**
81
+ * Get the list of releases.json URLs to try, respecting ENABLE_GITHUB_FALLBACK.
82
+ * When fallback is disabled, only the Layerbase URL is returned.
83
+ */
84
+ export function getReleasesUrls(): string[] {
85
+ return ENABLE_GITHUB_FALLBACK
86
+ ? [LAYERBASE_RELEASES_URL, GITHUB_RELEASES_URL]
87
+ : [LAYERBASE_RELEASES_URL]
88
+ }
89
+
65
90
  /**
66
91
  * Fetch releases.json from hostdb repository with caching.
67
92
  *
@@ -74,27 +99,39 @@ export async function fetchHostdbReleases(): Promise<HostdbReleasesData> {
74
99
  return cachedReleases
75
100
  }
76
101
 
77
- try {
78
- const response = await fetch(HOSTDB_RELEASES_URL, {
79
- signal: AbortSignal.timeout(5000),
80
- })
81
- if (!response.ok) {
82
- throw new Error(`HTTP ${response.status}: ${response.statusText}`)
83
- }
102
+ // Try layerbase registry first, fall back to GitHub (if enabled)
103
+ const urls = getReleasesUrls()
104
+ for (let i = 0; i < urls.length; i++) {
105
+ const url = urls[i]
106
+ const isLast = i === urls.length - 1
107
+ try {
108
+ const response = await fetch(url, {
109
+ signal: AbortSignal.timeout(5000),
110
+ })
111
+ if (!response.ok) {
112
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
113
+ }
84
114
 
85
- const data = (await response.json()) as HostdbReleasesData
115
+ const data = (await response.json()) as HostdbReleasesData
86
116
 
87
- // Cache the results
88
- cachedReleases = data
89
- cacheTimestamp = Date.now()
117
+ // Cache the results
118
+ cachedReleases = data
119
+ cacheTimestamp = Date.now()
90
120
 
91
- return data
92
- } catch (error) {
93
- const err = error as Error
94
- // Log the failure and rethrow - caller decides whether to use fallback
95
- console.warn(`Warning: Failed to fetch hostdb releases: ${err.message}`)
96
- throw error
121
+ return data
122
+ } catch (error) {
123
+ const err = error as Error
124
+ logDebug(`Failed to fetch releases from ${url}: ${err.message}`)
125
+ // If this was the last URL, rethrow
126
+ if (isLast) {
127
+ throw error
128
+ }
129
+ // Otherwise try the next URL
130
+ }
97
131
  }
132
+
133
+ // Should be unreachable (loop always throws on last iteration)
134
+ throw new Error('Failed to fetch hostdb releases from all registries')
98
135
  }
99
136
 
100
137
  /**
@@ -175,7 +212,105 @@ export function buildHostdbUrl(
175
212
  const tag = `${engine}-${version}`
176
213
  const filename = `${engine}-${version}-${hostdbPlatform}.${extension}`
177
214
 
178
- return `https://github.com/robertjbass/hostdb/releases/download/${tag}/${filename}`
215
+ return `${LAYERBASE_REGISTRY_BASE}/${tag}/${filename}`
216
+ }
217
+
218
+ /**
219
+ * Build a GitHub fallback URL for a hostdb release (same path scheme as layerbase).
220
+ */
221
+ export function buildGithubFallbackUrl(
222
+ engine: Engine | string,
223
+ options: BuildHostdbUrlOptions,
224
+ ): string {
225
+ const { version, hostdbPlatform, extension = 'tar.gz' } = options
226
+ const tag = `${engine}-${version}`
227
+ const filename = `${engine}-${version}-${hostdbPlatform}.${extension}`
228
+
229
+ return `${GITHUB_REGISTRY_BASE}/${tag}/${filename}`
230
+ }
231
+
232
+ /**
233
+ * Convert a layerbase registry URL to its GitHub fallback equivalent.
234
+ * Returns null if the URL is not a layerbase URL or if GitHub fallback is disabled.
235
+ */
236
+ export function getRegistryFallbackUrl(url: string): string | null {
237
+ if (!ENABLE_GITHUB_FALLBACK) return null
238
+ if (url.startsWith(LAYERBASE_REGISTRY_BASE)) {
239
+ return url.replace(LAYERBASE_REGISTRY_BASE, GITHUB_REGISTRY_BASE)
240
+ }
241
+ return null
242
+ }
243
+
244
+ /**
245
+ * Fetch wrapper that tries the primary URL first, then falls back to the
246
+ * GitHub registry if the primary is a layerbase URL and the request fails
247
+ * with a 404, 5xx, or network error.
248
+ *
249
+ * AbortError (timeout) is never retried — it propagates immediately.
250
+ */
251
+ export async function fetchWithRegistryFallback(
252
+ url: string,
253
+ options?: RequestInit,
254
+ ): Promise<Response> {
255
+ try {
256
+ const response = await fetch(url, options)
257
+ if (response.status === 404 || response.status >= 500) {
258
+ const fallbackUrl = getRegistryFallbackUrl(url)
259
+ if (fallbackUrl) {
260
+ logDebug(
261
+ `Primary registry returned ${response.status}, trying GitHub fallback`,
262
+ )
263
+ return await fetch(fallbackUrl, options)
264
+ }
265
+ }
266
+ return response
267
+ } catch (error) {
268
+ const err = error as Error
269
+ // Never retry on timeout (AbortError)
270
+ if (err.name === 'AbortError') {
271
+ throw error
272
+ }
273
+ const fallbackUrl = getRegistryFallbackUrl(url)
274
+ if (fallbackUrl) {
275
+ logDebug(
276
+ `Primary registry fetch failed (${err.message}), trying GitHub fallback`,
277
+ )
278
+ return await fetch(fallbackUrl, options)
279
+ }
280
+ throw error
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Try fetching from multiple registry URLs in order, returning the first
286
+ * successful (response.ok) Response. Logs per-URL failures via the
287
+ * supplied logger callback.
288
+ *
289
+ * @param urls - URLs to try in order (e.g., layerbase then GitHub)
290
+ * @param logger - Callback for logging per-URL failures
291
+ * @param timeoutMs - Per-request timeout in milliseconds (default: 5000)
292
+ * @returns The first successful Response
293
+ * @throws Error if all URLs fail
294
+ */
295
+ export async function fetchFromRegistryUrls(
296
+ urls: string[],
297
+ logger: (message: string) => void,
298
+ timeoutMs: number = 5000,
299
+ ): Promise<Response> {
300
+ let lastError: Error | null = null
301
+ for (const url of urls) {
302
+ try {
303
+ const response = await fetch(url, {
304
+ signal: AbortSignal.timeout(timeoutMs),
305
+ })
306
+ if (response.ok) return response
307
+ logger(`Registry fetch from ${url}: HTTP ${response.status}`)
308
+ } catch (error) {
309
+ logger(`Registry fetch from ${url} failed: ${error}`)
310
+ lastError = error as Error
311
+ }
312
+ }
313
+ throw lastError ?? new Error('All release registries failed')
179
314
  }
180
315
 
181
316
  export type BuildDownloadUrlOptions = {
@@ -2,15 +2,19 @@
2
2
  * Fetches metadata from hostdb (databases.json and downloads.json)
3
3
  * to understand what tools each engine needs and how to install them.
4
4
  *
5
+ * Primary registry: registry.layerbase.host
6
+ * Fallback registry: GitHub raw (robertjbass/hostdb)
7
+ *
5
8
  * Architecture:
6
9
  * - databases.json: Lists server, client, utilities, and enhanced CLI tools for each engine
7
10
  * - downloads.json: Provides package manager commands for installing tools
8
11
  */
9
12
 
10
13
  import { logDebug } from './error-handler'
14
+ import { LAYERBASE_REGISTRY_BASE } from './hostdb-client'
11
15
  import type { Engine } from '../types'
12
16
 
13
- const HOSTDB_RAW_BASE =
17
+ const GITHUB_RAW_BASE =
14
18
  'https://raw.githubusercontent.com/robertjbass/hostdb/main'
15
19
 
16
20
  const CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes
@@ -22,12 +26,29 @@ type CliTools = {
22
26
  enhanced: string[]
23
27
  }
24
28
 
29
+ export type VersionEntryObject = {
30
+ enabled?: boolean
31
+ platforms?: string[]
32
+ dependencies?: Array<{
33
+ database: string
34
+ cascadeDelete: boolean
35
+ note?: string
36
+ }>
37
+ cliTools?: CliTools
38
+ }
39
+
25
40
  type DatabaseEntry = {
26
41
  displayName: string
27
42
  cliTools: CliTools
28
- versions: Record<string, boolean> // version string -> available (true/false)
29
- latestLts: string
30
- // Other fields exist but we don't need them
43
+ versions: Record<string, boolean | VersionEntryObject>
44
+ platforms?: string[]
45
+ dependencies?: Array<{
46
+ database: string
47
+ cascadeDelete: boolean
48
+ note?: string
49
+ }>
50
+ spindbStatus?: string
51
+ hostedServiceAllowed?: boolean
31
52
  }
32
53
 
33
54
  type PackageManagerDef = {
@@ -69,7 +90,7 @@ let downloadsCache: { data: DownloadsJson; timestamp: number } | null = null
69
90
  const inFlightRequests = new Map<string, Promise<unknown>>()
70
91
 
71
92
  async function fetchWithCache<T>(
72
- url: string,
93
+ urls: string[],
73
94
  getCache: () => { data: T; timestamp: number } | null,
74
95
  setCache: (cache: { data: T; timestamp: number }) => void,
75
96
  ): Promise<T> {
@@ -80,35 +101,48 @@ async function fetchWithCache<T>(
80
101
  return cache.data
81
102
  }
82
103
 
83
- // Check for in-flight request to prevent duplicate fetches
84
- const inFlight = inFlightRequests.get(url)
104
+ // Check for in-flight request using the primary URL as key
105
+ const cacheKey = urls[0]
106
+ const inFlight = inFlightRequests.get(cacheKey)
85
107
  if (inFlight) {
86
108
  return inFlight as Promise<T>
87
109
  }
88
110
 
89
- // Create the fetch promise and store it
111
+ // Create the fetch promise try each URL in order
90
112
  const fetchPromise = (async () => {
91
113
  try {
92
- const response = await fetch(url)
93
- if (!response.ok) {
94
- throw new Error(`Failed to fetch ${url}: ${response.status}`)
114
+ let lastError: Error | null = null
115
+ for (const url of urls) {
116
+ try {
117
+ const response = await fetch(url)
118
+ if (!response.ok) {
119
+ throw new Error(`Failed to fetch ${url}: ${response.status}`)
120
+ }
121
+
122
+ const data = (await response.json()) as T
123
+ setCache({ data, timestamp: Date.now() })
124
+ return data
125
+ } catch (error) {
126
+ lastError = error as Error
127
+ logDebug(`Metadata fetch from ${url} failed: ${lastError.message}`)
128
+ }
95
129
  }
96
-
97
- const data = (await response.json()) as T
98
- setCache({ data, timestamp: Date.now() })
99
- return data
130
+ throw lastError ?? new Error('All metadata URLs failed')
100
131
  } finally {
101
- inFlightRequests.delete(url)
132
+ inFlightRequests.delete(cacheKey)
102
133
  }
103
134
  })()
104
135
 
105
- inFlightRequests.set(url, fetchPromise)
136
+ inFlightRequests.set(cacheKey, fetchPromise)
106
137
  return fetchPromise
107
138
  }
108
139
 
109
140
  export async function fetchDatabasesJson(): Promise<DatabasesJson> {
110
141
  return fetchWithCache(
111
- `${HOSTDB_RAW_BASE}/databases.json`,
142
+ [
143
+ `${LAYERBASE_REGISTRY_BASE}/databases.json`,
144
+ `${GITHUB_RAW_BASE}/databases.json`,
145
+ ],
112
146
  () => databasesCache,
113
147
  (c) => {
114
148
  databasesCache = c
@@ -118,7 +152,10 @@ export async function fetchDatabasesJson(): Promise<DatabasesJson> {
118
152
 
119
153
  export async function fetchDownloadsJson(): Promise<DownloadsJson> {
120
154
  return fetchWithCache(
121
- `${HOSTDB_RAW_BASE}/downloads.json`,
155
+ [
156
+ `${LAYERBASE_REGISTRY_BASE}/downloads.json`,
157
+ `${GITHUB_RAW_BASE}/downloads.json`,
158
+ ],
122
159
  () => downloadsCache,
123
160
  (c) => {
124
161
  downloadsCache = c
@@ -275,6 +312,16 @@ export async function getPackagesForTools(
275
312
  }
276
313
  }
277
314
 
315
+ /**
316
+ * Check if a version entry is enabled.
317
+ * Handles both old schema (boolean) and new schema (object with optional enabled field).
318
+ * Objects are enabled by default unless explicitly `{ enabled: false }`.
319
+ */
320
+ export function isVersionEnabled(value: boolean | VersionEntryObject): boolean {
321
+ if (typeof value === 'boolean') return value
322
+ return value.enabled !== false
323
+ }
324
+
278
325
  /**
279
326
  * Get available versions for a database engine from databases.json
280
327
  * This is the authoritative source for what versions are actually available in hostdb.
@@ -290,9 +337,8 @@ export async function getAvailableVersions(
290
337
  const entry = data[key]
291
338
  if (!entry?.versions) return null
292
339
 
293
- // Return only versions marked as available (true)
294
340
  return Object.entries(entry.versions)
295
- .filter(([, available]) => available)
341
+ .filter(([, value]) => isVersionEnabled(value))
296
342
  .map(([version]) => version)
297
343
  } catch (error) {
298
344
  logDebug('Failed to fetch available versions from hostdb', {
@@ -302,25 +348,3 @@ export async function getAvailableVersions(
302
348
  return null
303
349
  }
304
350
  }
305
-
306
- /**
307
- * Get the latest LTS version for a database engine
308
- * @param engine Engine (e.g., Engine.PostgreSQL or 'postgresql')
309
- * @returns Latest LTS version string, or null if not found
310
- */
311
- export async function getLatestLtsVersion(
312
- engine: Engine | string,
313
- ): Promise<string | null> {
314
- try {
315
- const data = await fetchDatabasesJson()
316
- const key = engine.toLowerCase()
317
- const entry = data[key]
318
- return entry?.latestLts || null
319
- } catch (error) {
320
- logDebug('Failed to fetch latest LTS version from hostdb', {
321
- engine,
322
- error: error instanceof Error ? error.message : String(error),
323
- })
324
- return null
325
- }
326
- }
@@ -35,7 +35,7 @@ export function getHostdbPlatform(
35
35
  /**
36
36
  * Build the download URL for ClickHouse binaries from hostdb
37
37
  *
38
- * Format: https://github.com/robertjbass/hostdb/releases/download/clickhouse-{version}/clickhouse-{version}-{platform}-{arch}.tar.gz
38
+ * Format: https://registry.layerbase.host/clickhouse-{version}/clickhouse-{version}-{platform}-{arch}.tar.gz
39
39
  *
40
40
  * @param version - ClickHouse version (e.g., '25.12', '25.12.3.21')
41
41
  * @param platform - Platform identifier (e.g., 'darwin', 'linux')
@@ -1,19 +1,17 @@
1
1
  /**
2
2
  * CockroachDB binary URL generation
3
3
  *
4
- * Generates download URLs for CockroachDB binaries from hostdb.
4
+ * Generates download URLs for CockroachDB binaries from the layerbase registry.
5
5
  */
6
6
 
7
- import type { Platform, Arch } from '../../types'
7
+ import { type Platform, type Arch, Engine } from '../../types'
8
8
  import { normalizeVersion } from './version-maps'
9
-
10
- const HOSTDB_BASE_URL =
11
- 'https://github.com/robertjbass/hostdb/releases/download'
9
+ import { buildHostdbUrl } from '../../core/hostdb-client'
12
10
 
13
11
  /**
14
12
  * Get the binary download URL for a specific version and platform
15
13
  *
16
- * URL format: https://github.com/robertjbass/hostdb/releases/download/cockroachdb-{version}/cockroachdb-{version}-{platform}-{arch}.{ext}
14
+ * URL format: https://registry.layerbase.host/cockroachdb-{version}/cockroachdb-{version}-{platform}-{arch}.{ext}
17
15
  *
18
16
  * @param version - CockroachDB version (e.g., '25.4.2' or '25')
19
17
  * @param platform - Target platform (darwin, linux, win32)
@@ -27,7 +25,11 @@ export function getBinaryUrl(
27
25
  const fullVersion = normalizeVersion(version)
28
26
  const ext = platform === 'win32' ? 'zip' : 'tar.gz'
29
27
 
30
- return `${HOSTDB_BASE_URL}/cockroachdb-${fullVersion}/cockroachdb-${fullVersion}-${platform}-${arch}.${ext}`
28
+ return buildHostdbUrl(Engine.CockroachDB, {
29
+ version: fullVersion,
30
+ hostdbPlatform: `${platform}-${arch}`,
31
+ extension: ext,
32
+ })
31
33
  }
32
34
 
33
35
  /**