Files
nyaterm/scripts/create-plugin.mjs
T

132 lines
5.6 KiB
JavaScript

import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
renameSync,
rmSync,
writeFileSync,
} from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const root = fileURLToPath(new URL("../", import.meta.url));
const args = process.argv.slice(2);
const [id, destination, option, template = "ui"] = args;
if (
args.length < 2 ||
args.length > 4 ||
(option && option !== "--template") ||
(option && args.length !== 4) ||
!["ui", "rust"].includes(template)
) {
process.stderr.write(
"Usage: pnpm plugin:create <publisher.plugin> <directory> [--template ui|rust]\n",
);
process.exit(1);
}
if (
!/^[a-z0-9][a-z0-9._-]{0,127}$/.test(id) ||
!id.includes(".") ||
id.includes("..")
) {
throw new Error(
"Use a namespaced lowercase plugin ID, for example example.hello",
);
}
const target = path.resolve(destination);
if (existsSync(target))
throw new Error(`Destination already exists: ${target}`);
const { version } = JSON.parse(
readFileSync(path.join(root, "package.json"), "utf8"),
);
const sdkPath = path
.relative(target, path.join(root, "plugins/sdk/rust/nyaterm-plugin-sdk"))
.split(path.sep)
.join("/");
const manifest = {
manifestVersion: 1,
id,
name: id,
version: "1.0.0",
description: "A NyaTerm plugin starter",
publisher: id.split(".")[0],
engine: `>=${version}, <2.0.0`,
permissions: template === "rust" ? ["native"] : ["session.read"],
contributions: {
panels: [{ id: "main", title: id, entry: "ui/index.html" }],
commands: [
{
id: "hello",
title: `Open ${id}`,
panel: "main",
menus: ["terminal", "connection"],
},
],
},
};
if (template === "rust") {
manifest.backend = { transport: "stdio-jsonl", executables: {} };
manifest.contributions.commands.push({
id: "run",
title: "Run native hello",
method: "command/hello",
menus: ["terminal", "connection"],
});
}
mkdirSync(path.dirname(target), { recursive: true });
const stage = mkdtempSync(path.join(path.dirname(target), ".nyaterm-plugin-"));
try {
const put = (file, content) => {
const output = path.join(stage, file);
mkdirSync(path.dirname(output), { recursive: true });
writeFileSync(output, content);
};
put("manifest.json", `${JSON.stringify(manifest, null, 2)}\n`);
put(".gitignore", "target/\nbin/\n*.nyap\n");
put(
"ui/index.html",
readFileSync(path.join(root, "plugins/templates/ui/index.html"), "utf8"),
);
put(
"ui/main.js",
template === "rust"
? 'document.getElementById("run").onclick = async () => {\n const output = document.getElementById("output");\n try { await NyaTerm.ready; output.textContent = JSON.stringify(await NyaTerm.backend("ui/hello"), null, 2); }\n catch (error) { output.textContent = error.message; }\n};\n'
: 'document.getElementById("run").onclick = async () => {\n const output = document.getElementById("output");\n try { await NyaTerm.ready; output.textContent = JSON.stringify(await NyaTerm.session(), null, 2); }\n catch (error) { output.textContent = error.message; }\n};\n',
);
put(
"ui/nyaterm-sdk.js",
readFileSync(path.join(root, "plugins/sdk/nyaterm.js")),
);
put(
"ui/nyaterm-sdk.d.ts",
readFileSync(path.join(root, "plugins/sdk/nyaterm.d.ts")),
);
if (template === "rust") {
put(
"Cargo.toml",
`[package]\nname = "nyaterm-plugin-starter"\nversion = "1.0.0"\nedition = "2024"\nrust-version = "1.94"\n\n[dependencies]\nnyaterm-plugin-sdk = { path = ${JSON.stringify(sdkPath)} }\ntokio = { version = "1", features = ["rt", "macros"] }\n`,
);
put(
"src/main.rs",
readFileSync(path.join(root, "plugins/templates/rust/main.rs")),
);
put(
"build.mjs",
readFileSync(path.join(root, "plugins/templates/rust/build.mjs")),
);
}
const packer = path.join(root, "scripts/package-plugin.mjs");
put(
"README.md",
`# ${id}\n\nGenerated from the NyaTerm ${template} template.\n\n${template === "rust" ? "1. Install Rust 1.94 or newer, then run `node build.mjs` here. This builds the SDK backend and fills the current platform executable in manifest.json.\n2." : "1."} Package from this directory:\n\n\`\`\`sh\nnode ${JSON.stringify(packer)} . ../${id}.nyap\n\`\`\`\n\nInstall the .nyap from NyaTerm's Plugins panel, review permissions, enable it and open its panel. ${template === "rust" ? "The native command reuses the process; check status/logs in Plugins. Stop the backend to reset its counter. Never print protocol data to stdout: use Context::log or stderr. The Rust SDK is a relative local dependency; distribute this source with the SDK or change its path when moving the project. For framed transport change backend.transport to stdio-framed; the SDK selects the transport from the host environment. Add other-platform binaries under bin/ and their executable entries before packing." : "Select a connected terminal before inspecting its session. The UI runs in an isolated iframe; use the SDK instead of Tauri APIs or direct file/network access."}\n\nEdit manifest.json to add permissions and contributions. UI scripts and styles must be local; use textContent for host/plugin data. Packaging includes only manifest.json, ui/, assets/ and bin/; target and source are excluded. The packager injects the current UI SDK. To update an installed plugin, bump manifest.version (and Cargo version for Rust) because released versions are immutable.\n`,
);
renameSync(stage, target);
} catch (error) {
rmSync(stage, { recursive: true, force: true });
throw error;
}
process.stdout.write(
`Created ${template} plugin ${id} at ${target}\nSee README.md for build and packaging instructions.\n`,
);