mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-10 13:51:45 +00:00
159 lines
4.7 KiB
JavaScript
159 lines
4.7 KiB
JavaScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import process from "node:process";
|
|
import { build } from "./build.mjs";
|
|
import { pack } from "./pack.mjs";
|
|
import { readManifest } from "../lib/plugin-dir.mjs";
|
|
|
|
const SOURCE_DIRS = new Set(["src", "locales", "migrations"]);
|
|
const SOURCE_FILES = new Set(["manifest.json", "package.json", "README.md"]);
|
|
|
|
function readOption(args, name) {
|
|
const index = args.indexOf(name);
|
|
if (index === -1) return null;
|
|
const value = args[index + 1];
|
|
if (!value) throw new Error(`${name} needs a value`);
|
|
return value;
|
|
}
|
|
|
|
/** True when a changed path (relative to the plugin) feeds its build. */
|
|
export function isSourceChange(filename) {
|
|
if (!filename) return false;
|
|
const parts = String(filename).replaceAll("\\", "/").split("/");
|
|
if (parts.length === 1) return SOURCE_FILES.has(parts[0]);
|
|
return SOURCE_DIRS.has(parts[0]);
|
|
}
|
|
|
|
export function resolveDevOptions(args, env = process.env) {
|
|
const server = (
|
|
readOption(args, "--server") ??
|
|
env.TERMIX_SERVER_URL ??
|
|
"http://localhost:30001"
|
|
).replace(/\/+$/, "");
|
|
const key = readOption(args, "--key") ?? env.TERMIX_API_KEY ?? null;
|
|
if (!key) {
|
|
throw new Error(
|
|
"An admin API key is needed. Pass --key tmx_... or set TERMIX_API_KEY.",
|
|
);
|
|
}
|
|
return { server, key, once: args.includes("--once") };
|
|
}
|
|
|
|
const HINTS = {
|
|
DEVELOPER_MODE_OFF:
|
|
"Turn on developer mode in Admin settings, under Plugins, then save again.",
|
|
SIGNED_ONLY:
|
|
"The server sets TERMIX_REQUIRE_SIGNED_PLUGINS, so it never installs unsigned files.",
|
|
BUNDLED_ID: "That id ships with Termix. Use a different id while developing.",
|
|
ALREADY_INSTALLED:
|
|
"Uninstall the copy installed from the registry first, then save again.",
|
|
};
|
|
|
|
async function request(fetchImpl, url, init) {
|
|
const response = await fetchImpl(url, init);
|
|
const text = await response.text();
|
|
let body = null;
|
|
try {
|
|
body = text ? JSON.parse(text) : null;
|
|
} catch {
|
|
body = { error: text };
|
|
}
|
|
if (!response.ok) {
|
|
const code = body?.code;
|
|
const hint = code && HINTS[code] ? `\n ${HINTS[code]}` : "";
|
|
throw new Error(
|
|
`${response.status} ${body?.error ?? response.statusText}${hint}`,
|
|
);
|
|
}
|
|
return body;
|
|
}
|
|
|
|
/** Uploads a packed .tmxplug and installs it, agreeing to what it asks for. */
|
|
export async function pushPlugin(
|
|
file,
|
|
{ server, key },
|
|
fetchImpl = globalThis.fetch,
|
|
) {
|
|
const auth = { Authorization: `Bearer ${key}` };
|
|
const preview = await request(fetchImpl, `${server}/plugins/upload`, {
|
|
method: "POST",
|
|
headers: { ...auth, "Content-Type": "application/octet-stream" },
|
|
body: fs.readFileSync(file),
|
|
});
|
|
const result = await request(
|
|
fetchImpl,
|
|
`${server}/plugins/upload/${preview.token}/install`,
|
|
{
|
|
method: "POST",
|
|
headers: { ...auth, "Content-Type": "application/json" },
|
|
body: JSON.stringify({ capabilities: preview.capabilities }),
|
|
},
|
|
);
|
|
return { preview, result };
|
|
}
|
|
|
|
/**
|
|
* Builds, packs and installs the plugin on a running Termix server, then
|
|
* does it again on every source change. The server needs developer mode on.
|
|
*/
|
|
export async function dev({ cwd, args = [] }) {
|
|
const options = resolveDevOptions(args);
|
|
const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "termix-plugin-dev-"));
|
|
let lastCapabilities = null;
|
|
|
|
const cycle = async () => {
|
|
const started = Date.now();
|
|
await build({ cwd });
|
|
const file = await pack({ cwd, args: ["--out", outDir] });
|
|
const { preview, result } = await pushPlugin(file, options);
|
|
fs.rmSync(file, { force: true });
|
|
const capabilities = preview.capabilities.join(", ") || "none";
|
|
if (capabilities !== lastCapabilities) {
|
|
console.log(` capabilities ${capabilities}`);
|
|
lastCapabilities = capabilities;
|
|
}
|
|
console.log(
|
|
`installed ${result.id}@${result.version} on ${options.server} (${result.state}, ${Date.now() - started} ms)`,
|
|
);
|
|
};
|
|
|
|
try {
|
|
await cycle();
|
|
} catch (error) {
|
|
if (options.once) throw error;
|
|
console.error(error?.message ?? error);
|
|
}
|
|
if (options.once) return;
|
|
|
|
const { id } = readManifest(cwd);
|
|
console.log(`watching ${id} for changes, Ctrl+C to stop`);
|
|
let timer = null;
|
|
let running = false;
|
|
let again = false;
|
|
const fire = async () => {
|
|
if (running) {
|
|
again = true;
|
|
return;
|
|
}
|
|
running = true;
|
|
try {
|
|
await cycle();
|
|
} catch (error) {
|
|
console.error(error?.message ?? error);
|
|
} finally {
|
|
running = false;
|
|
if (again) {
|
|
again = false;
|
|
void fire();
|
|
}
|
|
}
|
|
};
|
|
fs.watch(cwd, { recursive: true }, (_event, filename) => {
|
|
if (!isSourceChange(filename)) return;
|
|
clearTimeout(timer);
|
|
timer = setTimeout(() => void fire(), 300);
|
|
});
|
|
await new Promise(() => {});
|
|
}
|