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"] }