feat(plugins): core read-only scripts and user plugin system
Introduce plugin/<id> installs (zip, HTTPS git, example, fork), jerapah-plugin.json manifests with jerapah semver ranges, isolated plugin dirs under data/plugins, and app version 0.1.0. Core scripts are non-editable; get-current-time moves to examples/plugins. Co-authored-by: Nasyarobby Putra <nasyarobby@gmail.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { SERVER_ROOT } from "./paths.js";
|
||||
|
||||
const ROOT_PKG = path.resolve(SERVER_ROOT, "../../package.json");
|
||||
|
||||
/**
|
||||
* JerapahFlow app version from the monorepo root package.json.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getAppVersion() {
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync(ROOT_PKG, "utf8"));
|
||||
if (typeof raw.version === "string" && raw.version.trim()) {
|
||||
return raw.version.trim();
|
||||
}
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
return "0.1.0";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} version
|
||||
* @returns {[number, number, number]}
|
||||
*/
|
||||
function parseSemver(version) {
|
||||
const cleaned = String(version).trim().replace(/^v/i, "");
|
||||
const core = cleaned.split("-")[0].split("+")[0];
|
||||
const parts = core.split(".").map((p) => Number(p));
|
||||
return [parts[0] || 0, parts[1] || 0, parts[2] || 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {[number, number, number]} a
|
||||
* @param {[number, number, number]} b
|
||||
*/
|
||||
function cmp(a, b) {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal semver range check for patterns used in manifests:
|
||||
* `1.2.3`, `>=0.1.0`, `<1.0.0`, `>=0.1.0 <1.0.0`
|
||||
*
|
||||
* @param {string} version
|
||||
* @param {string} range
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function satisfiesRange(version, range) {
|
||||
if (typeof range !== "string" || !range.trim()) return false;
|
||||
const ver = parseSemver(version);
|
||||
const tokens = range.trim().split(/\s+/);
|
||||
for (const token of tokens) {
|
||||
if (/^\d+\.\d+\.\d+/.test(token) && !token.startsWith(">") && !token.startsWith("<")) {
|
||||
if (cmp(ver, parseSemver(token)) !== 0) return false;
|
||||
continue;
|
||||
}
|
||||
const m = /^(>=|<=|>|<|=)?\s*v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/.exec(
|
||||
token,
|
||||
);
|
||||
if (!m) return false;
|
||||
const op = m[1] || "=";
|
||||
const bound = parseSemver(m[2]);
|
||||
const c = cmp(ver, bound);
|
||||
if (op === ">=" && c < 0) return false;
|
||||
if (op === "<=" && c > 0) return false;
|
||||
if (op === ">" && c <= 0) return false;
|
||||
if (op === "<" && c >= 0) return false;
|
||||
if (op === "=" && c !== 0) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void fileURLToPath;
|
||||
Reference in New Issue
Block a user