diff --git a/.prettierignore b/.prettierignore
index ec1292f9..fda850b6 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -7,4 +7,5 @@ attic
build
*.njk
_site
-forks
\ No newline at end of file
+forks
+src/mothership-app/pb_hooks/src/versions.pb.js
\ No newline at end of file
diff --git a/package.json b/package.json
index 33ff3f19..552d435b 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"check:types:pockethost": "tsc --noEmit --skipLibCheck",
"lint": "prettier -c \"./**/*.{ts,js,cjs,svelte,json}\"",
"lint:fix": "prettier -w \"./**/*.{ts,js,cjs,svelte,json}\"",
+ "download-versions": "tsx ./src/cli/download.ts",
"build": "concurrently 'pnpm:build:*'",
"build-pockethost": "concurrently 'pnpm:build:pockethost:*'",
"build-frontends": "concurrently 'pnpm:build:frontend:*'",
diff --git a/src/cli/download.ts b/src/cli/download.ts
index 76fa6e7b..1b04d6c0 100644
--- a/src/cli/download.ts
+++ b/src/cli/download.ts
@@ -1,24 +1,21 @@
-import { DEBUG, PH_BIN_CACHE } from '$constants'
+import {
+ DEBUG,
+ DefaultSettingsService,
+ PH_BIN_CACHE,
+ SETTINGS,
+} from '$constants'
import { PocketbaseReleaseDownloadService } from '$services'
import { LogLevelName, LoggerService } from '$shared'
-// gen:import
-const [major, minor, patch] = process.versions.node.split('.').map(Number)
+const check = async () => {
+ DefaultSettingsService(SETTINGS)
+ LoggerService({
+ level: DEBUG() ? LogLevelName.Debug : LogLevelName.Info,
+ errorTrace: !DEBUG(),
+ })
-if ((major || 0) < 18) {
- throw new Error(`Node 18 or higher required.`)
-}
-
-LoggerService({
- level: DEBUG() ? LogLevelName.Debug : LogLevelName.Info,
- errorTrace: !DEBUG(),
-})
-
-// npm install eventsource --save
-// @ts-ignore
-global.EventSource = require('eventsource')
-;(async () => {
const logger = LoggerService().create(`download.ts`)
+
const { dbg, error, info, warn } = logger
info(`Starting`)
@@ -26,4 +23,6 @@ global.EventSource = require('eventsource')
cachePath: PH_BIN_CACHE(),
})
await check()
-})()
+}
+
+check()
diff --git a/src/mothership-app/pb_hooks/src/versions-route.pb.js b/src/mothership-app/pb_hooks/src/versions-route.pb.js
new file mode 100644
index 00000000..56036b33
--- /dev/null
+++ b/src/mothership-app/pb_hooks/src/versions-route.pb.js
@@ -0,0 +1,14 @@
+///
+
+/**
+ * Return a list of available PocketBase versions
+ */
+routerAdd(
+ 'GET',
+ '/api/versions',
+ (c) => {
+ const { versions } = require(`${__hooks}/versions.pb.js`)
+
+ return c.json(200, { versions })
+ } /* optional middlewares */,
+)
diff --git a/src/mothership-app/pb_hooks/src/versions.pb.js b/src/mothership-app/pb_hooks/src/versions.pb.js
new file mode 100644
index 00000000..3effb8a0
--- /dev/null
+++ b/src/mothership-app/pb_hooks/src/versions.pb.js
@@ -0,0 +1 @@
+module.exports = {"versions":["0.19.*","0.19.2","0.19.1","0.19.0","0.18.*","0.18.10","0.18.9","0.18.8","0.18.7","0.18.6","0.18.5","0.18.4","0.18.3","0.18.2","0.18.1","0.18.0","0.17.*","0.17.7","0.17.6","0.17.5","0.17.4","0.17.3","0.17.2","0.17.1","0.17.0","0.16.*","0.16.10","0.16.9","0.16.8","0.16.7","0.16.6","0.16.5","0.16.4","0.16.3","0.16.2","0.16.1","0.16.0","0.15.*","0.15.3","0.15.2","0.15.1","0.15.0","0.14.*","0.14.5","0.14.4","0.14.3","0.14.2","0.14.1","0.14.0","0.13.*","0.13.4","0.13.3","0.13.2","0.13.1","0.13.0","0.12.*","0.12.3","0.12.2","0.12.1","0.12.0","0.11.*","0.11.4","0.11.3","0.11.2","0.11.1","0.11.0","0.10.*","0.10.4","0.10.3","0.10.2","0.10.1","0.10.0","0.9.*","0.9.2","0.9.1","0.9.0","0.8.*","0.8.0","0.7.*","0.7.10","0.7.9","0.7.8","0.7.7","0.7.6","0.7.5","0.7.4","0.7.3","0.7.2","0.7.1","0.7.0","0.6.*","0.6.0","0.5.*","0.5.2","0.5.1","0.5.0","0.4.*","0.4.2","0.4.1","0.4.0","0.3.*","0.3.4","0.3.3","0.3.2","0.3.1","0.3.0","0.2.*","0.2.8","0.2.7","0.2.6","0.2.5","0.2.4","0.2.3"]}
\ No newline at end of file
diff --git a/src/services/PocketbaseReleaseDownloadService/expandAndSortSemVers.ts b/src/services/PocketbaseReleaseDownloadService/expandAndSortSemVers.ts
new file mode 100644
index 00000000..16f2b71a
--- /dev/null
+++ b/src/services/PocketbaseReleaseDownloadService/expandAndSortSemVers.ts
@@ -0,0 +1,46 @@
+function compareSemVer(a: string, b: string): number {
+ // Consider wildcards as higher than any version number, hence represented by a large number for comparison
+ let splitA = a
+ .split('.')
+ .map((x) => (x === '*' ? Number.MAX_SAFE_INTEGER : parseInt(x)))
+ let splitB = b
+ .split('.')
+ .map((x) => (x === '*' ? Number.MAX_SAFE_INTEGER : parseInt(x)))
+
+ // Compare each part starting from major, minor, then patch
+ for (let i = 0; i < 3; i++) {
+ if (splitA[i] !== splitB[i]) {
+ return splitB[i]! - splitA[i]! // For descending order, compare b - a
+ }
+ }
+
+ // If all parts are equal or both have wildcards
+ return 0
+}
+
+export function expandAndSortSemVers(semvers: string[]): string[] {
+ let expandedVersions = new Set() // Use a set to avoid duplicates
+
+ // Helper function to add wildcard versions
+ const addWildcards = (version: string) => {
+ const parts = version.split('.')
+
+ if (parts.length === 3) {
+ if (parts[0] !== '0') expandedVersions.add(`${parts[0]}.*.*`)
+ expandedVersions.add(`${parts[0]}.${parts[1]}.*`)
+ if (parts[0] === '0' && parts[1] !== '0')
+ expandedVersions.add(`0.${parts[1]}.*`)
+ }
+ }
+
+ // Add wildcards and original versions to the set
+ semvers.forEach((version) => {
+ expandedVersions.add(version)
+ addWildcards(version)
+ })
+
+ // Add the global wildcard for the latest version
+ // expandedVersions.add('*')
+ // Convert the set to an array and sort it using the custom semver comparison function
+ return Array.from(expandedVersions).sort(compareSemVer)
+}
diff --git a/src/services/PocketbaseReleaseDownloadService/index.ts b/src/services/PocketbaseReleaseDownloadService/index.ts
index b84cf8e6..137e98b1 100644
--- a/src/services/PocketbaseReleaseDownloadService/index.ts
+++ b/src/services/PocketbaseReleaseDownloadService/index.ts
@@ -1,11 +1,12 @@
-import { PH_BIN_CACHE } from '$constants'
-import { LoggerService, mkSingleton, SingletonBaseConfig } from '$shared'
+import { MOTHERSHIP_HOOKS_DIR, PH_BIN_CACHE } from '$constants'
+import { LoggerService, SingletonBaseConfig, mkSingleton } from '$shared'
import { downloadAndExtract, mergeConfig, smartFetch } from '$util'
import { keys } from '@s-libs/micro-dash'
import Bottleneck from 'bottleneck'
-import { chmodSync, existsSync } from 'fs'
+import { chmodSync, existsSync, writeFileSync } from 'fs'
import { join } from 'path'
import { rsort } from 'semver'
+import { expandAndSortSemVers } from './expandAndSortSemVers'
type Release = {
url: string
@@ -100,6 +101,15 @@ export const PocketbaseReleaseDownloadService = mkSingleton(
}),
)
await Promise.all(promises)
+
+ console.log(`***keys`, keys(binPaths))
+ const sortedSemVers = expandAndSortSemVers(keys(binPaths))
+ writeFileSync(
+ join(MOTHERSHIP_HOOKS_DIR(), `versions.pb.js`),
+ `module.exports = ${JSON.stringify({ versions: sortedSemVers })}`,
+ )
+ console.log(JSON.stringify(sortedSemVers))
+
if (keys(binPaths).length === 0) {
throw new Error(
`No version found, probably mismatched architecture and OS (${osName}/${cpuArchitecture})`,
diff --git a/src/util/Settings.ts b/src/util/Settings.ts
index 071a26df..10f69f9e 100644
--- a/src/util/Settings.ts
+++ b/src/util/Settings.ts
@@ -2,120 +2,100 @@ import { mkSingleton } from '$shared'
import { boolean as castToBoolean } from 'boolean'
import { existsSync, mkdirSync } from 'fs'
-export type HandlerFactory = (key: string) => {
+export type Caster = {
+ stringToType: (value: string, config?: Partial) => TValue
+ typeToString: (value: TValue, config?: Partial) => string
+}
+
+export type Handler = {
get: () => TValue
set: (value: TValue) => void
}
+export type HandlerFactory = (key: string) => Handler
+
export type Maker = (
_default?: TValue,
config?: Partial,
) => HandlerFactory
-export const mkBoolean: Maker = (_default) => (name: string) => {
- return {
- get() {
- const v = process.env[name]
- if (typeof v === `undefined`) {
- if (typeof _default === `undefined`)
- throw new Error(`${name} must be defined`)
- return _default
- }
- return castToBoolean(v)
- },
- set(v) {
- process.env[name] = `${v}`
- },
- }
-}
-
-export const mkNumber: Maker = (_default) => (name: string) => {
- return {
- get() {
- const v = process.env[name]
- if (typeof v === `undefined`) {
- if (typeof _default === `undefined`)
- throw new Error(`${name} must be defined`)
- return _default
- }
- return parseInt(v, 10)
- },
- set(v) {
- process.env[name] = v.toString()
- },
- }
-}
-
-export const mkPath: Maker =
- (_default, options = {}) =>
+const mkMaker =
+ (
+ caster: Caster,
+ ): Maker =>
+ (_default, config) =>
(name: string) => {
- const { create = false, required = true } = options
return {
- get() {
- const v = (() => {
- const v = process.env[name]
- if (typeof v === `undefined`) {
- if (typeof _default === `undefined`)
- throw new Error(`${name} must be defined`)
- return _default
- }
- return v
- })()
- if (create) {
- mkdirSync(v, { recursive: true })
- }
- if (required && !existsSync(v)) {
- throw new Error(`${name} (${v}) must exist.`)
- }
- return v
- },
- set(v) {
- if (!existsSync(v)) {
- throw new Error(`${name} (${v}) must exist.`)
- }
- process.env[name] = v
- },
- }
- }
-
-export const mkString: Maker = (_default) => (name: string) => {
- return {
- get() {
- const v = process.env[name]
- if (typeof v === `undefined`) {
- if (typeof _default === `undefined`)
- throw new Error(`${name} must be defined`)
- return _default
- }
- return v
- },
- set(v) {
- process.env[name] = v
- },
- }
-}
-
-export const mkCsvString: Maker = (_default) => (name: string) => {
- return {
- get() {
- return (() => {
+ get(): TValue {
const v = process.env[name]
if (typeof v === `undefined`) {
if (typeof _default === `undefined`)
throw new Error(`${name} must be defined`)
- return _default
+ this.set(_default)
+ return this.get()
}
- return v
- .split(/,/)
- .map((s) => s.trim())
- .filter((v) => !!v)
- })()
- },
- set(v) {
- process.env[name] = v.join(',')
- },
+ try {
+ return caster.stringToType(v, config)
+ } catch (e) {
+ throw new Error(`${name}: ${e}`)
+ }
+ },
+ set(v: TValue) {
+ try {
+ process.env[name] = caster.typeToString(v, config)
+ } catch (e) {
+ throw new Error(`${name}: ${e}`)
+ }
+ },
+ }
}
-}
+
+export const mkBoolean = mkMaker({
+ stringToType: (v) => castToBoolean(v),
+ typeToString: (v) => `${v}`,
+})
+
+export const mkNumber = mkMaker({
+ stringToType: (s) => parseInt(s, 10),
+ typeToString: (v) => v.toString(),
+})
+
+export const mkPath = mkMaker({
+ stringToType: (v, options) => {
+ const { create = false, required = true } = options || {}
+ if (create) {
+ mkdirSync(v, { recursive: true })
+ }
+ if (required && !existsSync(v)) {
+ throw new Error(`${v} must exist.`)
+ }
+ return v
+ },
+ typeToString: (v, options) => {
+ const { create = false, required = true } = options || {}
+ if (create) {
+ mkdirSync(v, { recursive: true })
+ }
+ if (required && !existsSync(v)) {
+ throw new Error(`${v} must exist.`)
+ }
+ return v
+ },
+})
+
+export const mkString = mkMaker({
+ typeToString: (v) => v,
+ stringToType: (v) => v,
+})
+
+export const mkCsvString = mkMaker({
+ typeToString: (v) => v.join(','),
+ stringToType: (s) =>
+ s
+ .split(/,/)
+ .map((s) => s.trim())
+ .filter((v) => !!v),
+})
type Config = {
[K in keyof T]: HandlerFactory
@@ -132,6 +112,7 @@ export const SettingsService = (config: Config) => {
set: (value) => handler.set(value),
enumerable: true,
})
+ handler.get() // Initialize process.env
}
return lookup as T
diff --git a/tsconfig.json b/tsconfig.json
index 14686ba0..c3e85a71 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -24,9 +24,6 @@
"$shared": ["src/shared"]
}
},
- "ts-node": {
- "esm": true
- },
"include": ["./src"],
"exclude": ["src/mothership-app"]
}