Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement โ†’
Sign In

gorilator

Package Overview
Dependencies
Maintainers
1
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

gorilator - npm Package Compare versions

Comparing version
1.7.4
to
1.8.0
+263
dist/commands/plugin.js
// gorilator plugin โ€” manage the checkout's plugin system from the CLI.
//
// gorilator plugin list discovered plugins + enabled state
// gorilator plugin enable <name> remove <name> from realm.json plugins.disabled
// gorilator plugin disable <name> add <name> to realm.json plugins.disabled
// gorilator plugin add <path|npub> vendor a local plugin dir into plugins/, or
// trust a Nostr realm-pack author (.env REALM_PACK_AUTHORS)
//
// Mirrors the server's discovery rules (packages/server/src/systems/plugins/
// discovery.ts): plugins/<dir>/plugin.json plus node_modules/gorilator-plugin-*,
// names starting with "_" are templates, and realm.json's plugins.disabled list
// turns plugins off by name without deleting them. The CLI ships standalone, so
// the manifest shape is re-declared here instead of importing @rpg/shared.
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, symlinkSync, writeFileSync, } from "node:fs";
import { basename, join, resolve } from "node:path";
import * as log from "../lib/log.js";
import { isValidNpub, npubToHex } from "../lib/npub.js";
export function pluginCmd(ctx, args, flags) {
const root = ctx.appDir;
if (!existsSync(root)) {
log.die(`No Gorilator checkout at ${root}. Run inside a repo or 'gorilator install' first.`);
}
const [sub, arg] = args;
switch (sub) {
case undefined:
case "list":
listPlugins(root);
return;
case "enable":
enablePlugin(root, requireArg(sub, arg, "<name>"));
return;
case "disable":
disablePlugin(root, requireArg(sub, arg, "<name>"));
return;
case "add":
addPlugin(root, requireArg(sub, arg, "<path|npub>"), flags);
return;
default:
log.die(`Unknown plugin subcommand: ${sub}. Try list | enable | disable | add.`);
}
}
function requireArg(sub, arg, placeholder) {
if (!arg)
log.die(`Usage: gorilator plugin ${sub} ${placeholder}`);
return arg;
}
// ---- discovery (read-only mirror of the server's rules) ----
function readManifest(dir) {
const file = join(dir, "plugin.json");
if (!existsSync(file))
return null;
try {
const raw = JSON.parse(readFileSync(file, "utf8"));
if (!raw?.name || !raw?.apiVersion) {
log.warn(`${file}: manifest needs "name" and "apiVersion" โ€” skipped`);
return null;
}
return raw;
}
catch {
log.warn(`${file}: invalid JSON โ€” skipped`);
return null;
}
}
function discoverPlugins(root) {
const found = [];
const seen = new Set();
const consider = (dir, source) => {
const manifest = readManifest(dir);
if (!manifest || seen.has(manifest.name))
return;
if (manifest.name.startsWith("_"))
return; // _template and friends
seen.add(manifest.name);
found.push({ manifest, dir, source });
};
const pluginRoot = join(root, "plugins");
if (existsSync(pluginRoot)) {
for (const entry of readdirSync(pluginRoot, { withFileTypes: true })) {
if (entry.isDirectory() || entry.isSymbolicLink())
consider(join(pluginRoot, entry.name), "plugins");
}
}
const nm = join(root, "node_modules");
if (existsSync(nm)) {
for (const entry of readdirSync(nm)) {
if (entry.startsWith("gorilator-plugin-"))
consider(join(nm, entry), "npm");
}
}
return found;
}
function realmPath(root) {
return join(root, "realm.json");
}
function readRealm(root) {
const file = realmPath(root);
if (!existsSync(file))
return {};
let parsed;
try {
parsed = JSON.parse(readFileSync(file, "utf8"));
}
catch {
return log.die(`${file} is not valid JSON โ€” fix it before editing plugins.disabled.`);
}
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return log.die(`${file} must contain a JSON object.`);
}
return parsed;
}
function disabledList(realm) {
const disabled = realm.plugins?.disabled;
return Array.isArray(disabled) ? disabled.map(String) : [];
}
/** Persist a new plugins.disabled list, preserving every other realm.json key. */
function writeDisabled(root, realm, disabled) {
realm.plugins = { ...(realm.plugins ?? {}), disabled };
writeFileSync(realmPath(root), `${JSON.stringify(realm, null, 2)}\n`);
}
// ---- subcommands ----
function listPlugins(root) {
const realmDisabled = new Set(disabledList(readRealm(root)));
const plugins = discoverPlugins(root);
if (plugins.length === 0) {
log.info("No plugins found.");
process.stdout.write(log.dim(` Drop one into ${join(root, "plugins")}/<name>/plugin.json, install a\n` +
" gorilator-plugin-* npm package, or start from plugins/_template โ€” see docs/plugins.md.\n"));
return;
}
const rows = plugins.map(({ manifest, source }) => {
const reason = realmDisabled.has(manifest.name)
? "realm.json"
: manifest.enabled === false
? "plugin.json"
: null;
return {
name: manifest.name,
version: manifest.version ? `v${manifest.version}` : "-",
status: reason ? `disabled (${reason})` : "enabled",
enabled: !reason,
source: source === "npm" ? "npm" : "plugins/",
caps: manifest.capabilities?.length ? manifest.capabilities.join(", ") : "-",
};
});
// Pad before colorizing โ€” escape codes would skew the column widths.
const nameW = Math.max(...rows.map((r) => r.name.length));
const verW = Math.max(...rows.map((r) => r.version.length));
const statusW = Math.max(...rows.map((r) => r.status.length));
process.stdout.write(`Plugins (${rows.length})\n\n`);
for (const r of rows) {
const status = (r.enabled ? log.green : log.yellow)(r.status.padEnd(statusW));
process.stdout.write(` ${log.bold(r.name.padEnd(nameW))} ${r.version.padEnd(verW)} ${status} ` +
`${log.dim(r.source.padEnd(8))} ${log.dim(r.caps)}\n`);
}
}
function enablePlugin(root, name) {
const realm = readRealm(root);
const disabled = disabledList(realm);
const plugin = discoverPlugins(root).find((p) => p.manifest.name === name);
if (!disabled.includes(name)) {
if (plugin?.manifest.enabled === false) {
log.warn(`${name} is disabled by its own manifest ("enabled": false) โ€” edit ${join(plugin.dir, "plugin.json")}.`);
}
else {
log.ok(`${name} is already enabled.`);
}
return;
}
writeDisabled(root, realm, disabled.filter((n) => n !== name));
log.ok(`Enabled ${name} (removed from realm.json plugins.disabled).`);
if (!plugin)
log.warn(`${name} is not currently in plugins/ or node_modules โ€” nothing will load.`);
if (plugin?.manifest.enabled === false) {
log.warn(`${name} still sets "enabled": false in its plugin.json โ€” edit it to fully enable.`);
}
log.info("Restart the server to apply.");
}
function disablePlugin(root, name) {
const realm = readRealm(root);
const disabled = disabledList(realm);
if (disabled.includes(name)) {
log.ok(`${name} is already disabled.`);
return;
}
if (!discoverPlugins(root).some((p) => p.manifest.name === name)) {
log.warn(`${name} is not currently in plugins/ or node_modules โ€” disabling the name anyway.`);
}
writeDisabled(root, realm, [...disabled, name]);
log.ok(`Disabled ${name} (added to realm.json plugins.disabled).`);
log.info("Restart the server to apply.");
}
function addPlugin(root, target, flags) {
if (isValidNpub(target)) {
addPackAuthor(root, target);
return;
}
addLocalPlugin(root, target, flags);
}
/** Vendor a local plugin directory into plugins/<manifest.name> (copy, or
* symlink with --link so an out-of-tree plugin keeps its own dev loop). */
function addLocalPlugin(root, path, flags) {
const src = resolve(path);
if (!existsSync(src) || !statSync(src).isDirectory()) {
log.die(`${path} is neither a plugin directory nor a valid npub1โ€ฆ key.`);
}
const manifest = readManifest(src);
if (!manifest) {
log.die(`${src} has no valid plugin.json ("name" + "apiVersion") โ€” see docs/plugins.md.`);
}
const pluginsDir = join(root, "plugins");
const dest = join(pluginsDir, manifest.name);
if (existsSync(dest))
log.die(`${dest} already exists โ€” remove it first.`);
mkdirSync(pluginsDir, { recursive: true });
if (flags.link) {
symlinkSync(src, dest, "dir");
log.ok(`Linked ${manifest.name} โ†’ ${src}`);
}
else {
cpSync(src, dest, {
recursive: true,
filter: (p) => !["node_modules", ".git"].includes(basename(p)),
});
log.ok(`Copied ${manifest.name} into ${dest}`);
}
log.info("Restart the server to load it (run pnpm build:plugins first if it only ships src/).");
}
/** Trust a Nostr realm-pack author: upsert the npub into REALM_PACK_AUTHORS in
* the repo-root .env (kind-30333 packs โ€” pure JSON data plugins consumed by
* packages/server/src/systems/plugins/nostrContent.ts). The edit is surgical โ€”
* one line touched โ€” so hand-written .env comments survive. */
function addPackAuthor(root, npub) {
const hex = npubToHex(npub);
if (!hex)
log.die(`${npub} is not a valid npub.`);
const envPath = join(root, ".env");
const lines = (existsSync(envPath) ? readFileSync(envPath, "utf8") : "").split("\n");
const idx = lines.findIndex((l) => /^\s*REALM_PACK_AUTHORS\s*=/.test(l));
let rawValue = idx === -1 ? "" : lines[idx].slice(lines[idx].indexOf("=") + 1).trim();
if (/^(['"]).*\1$/.test(rawValue))
rawValue = rawValue.slice(1, -1);
const authors = rawValue.split(",").map((s) => s.trim()).filter(Boolean);
// The server accepts npub or hex entries โ€” dedupe on the decoded key.
if (authors.some((a) => a.toLowerCase() === hex || npubToHex(a) === hex)) {
log.ok(`${npub.slice(0, 14)}โ€ฆ${npub.slice(-6)} is already a trusted realm-pack author.`);
return;
}
const value = [...authors, npub].join(",");
if (idx >= 0) {
lines[idx] = `REALM_PACK_AUTHORS=${value}`;
}
else {
while (lines.length && lines[lines.length - 1].trim() === "")
lines.pop();
if (lines.length)
lines.push("");
lines.push("# Nostr realm packs (kind-30333 data plugins): JSON content events published by", "# these trusted authors are loaded live โ€” validated, never executed. Optional", "# relay override: REALM_PACK_RELAYS=wss://โ€ฆ โ€” see docs/plugins.md.", `REALM_PACK_AUTHORS=${value}`, "");
}
writeFileSync(envPath, lines.join("\n"), { mode: 0o600 });
log.ok(`Added ${npub.slice(0, 14)}โ€ฆ${npub.slice(-6)} to REALM_PACK_AUTHORS in ${envPath}`);
log.info("Restart the server to start syncing this author's packs.");
}
+38
-0

@@ -11,2 +11,3 @@ #!/usr/bin/env node

// gorilator tunnel <login|status|restart> manage the Cloudflare tunnel
// gorilator plugin <list|enable|disable|add> manage plugins + realm-pack authors
// gorilator uninstall stop and remove Gorilator from this machine

@@ -19,2 +20,3 @@ // gorilator serve internal: the supervised foreground process

import { install } from "./commands/install.js";
import { pluginCmd } from "./commands/plugin.js";
import { remoteCmd } from "./commands/remote.js";

@@ -80,2 +82,3 @@ import { serve } from "./commands/serve.js";

"remote",
"plugin",
]);

@@ -113,2 +116,3 @@ /** One-line banner naming the target a command will act on. */

tunnel <cmd> Manage the Cloudflare tunnel โ€” login | status | restart
plugin <cmd> Manage plugins โ€” list | enable | disable | add <path|npub>
uninstall Stop and remove Gorilator services, config, global command, and installed files

@@ -147,2 +151,5 @@ serve Run the server in the foreground (used by the service)

--no-npm Skip the published npm package version check
Options (plugin add):
--link Symlink a local plugin directory into plugins/ instead of copying
`);

@@ -292,2 +299,29 @@ }

`,
plugin: `${log.bold("gorilator plugin")} โ€” manage plugins and realm-pack authors
Usage:
gorilator plugin [list]
gorilator plugin enable <name>
gorilator plugin disable <name>
gorilator plugin add <path|npub> [--link]
gorilator help plugin
Subcommands:
list Show discovered plugins (plugins/ + node_modules/gorilator-plugin-*)
with version, enabled state, and declared capabilities
enable <name> Remove <name> from realm.json's plugins.disabled list
disable <name> Add <name> to realm.json's plugins.disabled list (created if missing)
add <path> Copy a local plugin directory into plugins/<name> (--link symlinks instead)
add <npub> Trust a Nostr realm-pack author: appends the npub to
REALM_PACK_AUTHORS in .env (kind-30333 data packs, see docs/plugins.md)
Options:
--link With 'add <path>': symlink instead of copy
Examples:
gorilator plugin list
gorilator plugin disable example-arena
gorilator plugin add ../my-plugin --link
gorilator plugin add npub1abcโ€ฆ
`,
uninstall: `${log.bold("gorilator uninstall")} โ€” remove Gorilator from this machine

@@ -376,2 +410,3 @@

"no-npm": { type: "boolean" },
link: { type: "boolean" },
help: { type: "boolean", short: "h" },

@@ -458,2 +493,5 @@ version: { type: "boolean", short: "v" },

break;
case "plugin":
pluginCmd(ctx, positionals.slice(1), { link: Boolean(values.link) });
break;
case "uninstall":

@@ -460,0 +498,0 @@ uninstall(opts);

+4
-2

@@ -120,4 +120,6 @@ // Process & privilege helpers โ€” synchronous, dependency-free wrappers around

export function runPrivileged(cmd, args, opts = {}) {
if (isRoot())
return run(cmd, args, opts);
if (isRoot()) {
run(cmd, args, opts);
return;
}
run("sudo", [cmd, ...args], opts);

@@ -124,0 +126,0 @@ }

{
"name": "gorilator",
"version": "1.7.4",
"version": "1.8.0",
"description": "One command to install, run, and supervise the Gorilator RPG game server natively (no Docker) as a systemd/launchd service, with an optional Cloudflare Tunnel setup.",

@@ -21,2 +21,4 @@ "type": "module",

"test": "npm run build && node --test test/*.test.mjs",
"typecheck": "tsc -p tsconfig.json --noEmit",
"lint": "biome lint .",
"prepublishOnly": "npm run build"

@@ -23,0 +25,0 @@ },

@@ -80,2 +80,3 @@ # ๐Ÿฆ gorilator

gorilator tunnel <cmd># Cloudflare tunnel โ€” login | status | restart
gorilator plugin <cmd># plugins โ€” list | enable | disable | add <path|npub> (--link)
gorilator uninstall # stop and remove services, config, command, and installed files

@@ -82,0 +83,0 @@ gorilator help <cmd> # show detailed help for any command