scripts/generate-package-manager-manifests.js
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env node
import { createHash } from "node:crypto";
import { access, mkdir, readFile, stat, writeFile } from "node:fs/promises";
import { basename, dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const projectRoot = fileURLToPath(new URL("..", import.meta.url));
const packageIdentifier = "Demonbane18.Relmio";
const packagePublisher = "Demonbane18";
const packageName = "Relmio";
const repositoryUrl = "https://github.com/Demonbane18/relmio";
const manifestVersion = "1.12.0";
const supportedArchitectures = new Set(["x64", "arm64"]);
const semver =
/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:(?:0|[1-9]\d*)|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:(?:0|[1-9]\d*)|\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u;
function assertStableCatalogVersion(version) {
if (version.split("+", 1)[0].includes("-")) {
throw new Error("Homebrew and WinGet catalog candidates require a stable version.");
}
}
function assertString(value, label) {
if (typeof value !== "string" || value.trim() === "") {
throw new Error(`${label} must be a non-empty string.`);
}
return value;
}
function assertSha256(value, label = "SHA-256") {
if (typeof value !== "string" || !/^[a-f0-9]{64}$/iu.test(value)) {
throw new Error(`${label} must be a 64-character hexadecimal digest.`);
}
return value.toLowerCase();
}
export function validateReleasePackage(packageJson) {
if (packageJson?.name !== "relmio") {
throw new Error('package.json name must be "relmio".');
}
if (typeof packageJson?.version !== "string" || !semver.test(packageJson.version)) {
throw new Error("package.json must contain a valid semantic version.");
}
if (packageJson.license !== "Apache-2.0") {
throw new Error("package.json license must be Apache-2.0.");
}
if (packageJson?.repository?.url !== `git+${repositoryUrl}.git`) {
throw new Error("package.json repository URL does not match the release source.");
}
return { version: packageJson.version };
}
export function registryTarballUrl(version) {
if (typeof version !== "string" || !semver.test(version)) {
throw new Error("A valid semantic version is required for the npm tarball URL.");
}
return `https://registry.npmjs.org/relmio/-/relmio-${version}.tgz`;
}
export function wingetInstallerUrl({ architecture, version }) {
if (!supportedArchitectures.has(architecture)) {
throw new Error(`Unsupported WinGet architecture: ${architecture}.`);
}
if (typeof version !== "string" || !semver.test(version)) {
throw new Error("A valid semantic version is required for the WinGet installer URL.");
}
return `${repositoryUrl}/releases/download/v${version}/relmio-${version}-windows-${architecture}.zip`;
}
export function createHomebrewFormula({ sha256, version }) {
const digest = assertSha256(sha256, "Homebrew tarball SHA-256");
const tarballUrl = registryTarballUrl(version);
assertStableCatalogVersion(version);
return `class Relmio < Formula
desc "Set up a private OpenAI-compatible endpoint for self-hosted n8n"
homepage "${repositoryUrl}"
url "${tarballUrl}"
sha256 "${digest}"
license "Apache-2.0"
depends_on "python" => :build
depends_on "node"
def install
system "npm", "install", *std_npm_args
bin.install_symlink libexec.glob("bin/*")
end
test do
assert_predicate bin/"relmio", :executable?
system bin/"relmio", "--version"
end
end
`;
}
function normalizeWingetInstaller({ architecture, sha256, url }) {
if (!supportedArchitectures.has(architecture)) {
throw new Error(`Unsupported WinGet architecture: ${architecture}.`);
}
return {
architecture,
sha256: assertSha256(sha256, `WinGet ${architecture} installer SHA-256`).toUpperCase(),
url: assertString(url, `WinGet ${architecture} installer URL`),
};
}
export function createWingetManifestFiles({ installers, version }) {
if (typeof version !== "string" || !semver.test(version)) {
throw new Error("A valid semantic version is required for WinGet manifests.");
}
assertStableCatalogVersion(version);
if (!Array.isArray(installers) || installers.length === 0) {
throw new Error("At least one WinGet installer is required.");
}
const normalizedInstallers = installers
.map(normalizeWingetInstaller)
.sort((left, right) => left.architecture.localeCompare(right.architecture));
const duplicateArchitecture = normalizedInstallers.find(
(installer, index) =>
index > 0 && installer.architecture === normalizedInstallers[index - 1].architecture,
);
if (duplicateArchitecture) {
throw new Error(
`WinGet installer architecture ${duplicateArchitecture.architecture} was supplied more than once.`,
);
}
const schemaPrefix = "https://aka.ms/winget-manifest";
const header = (type) =>
`# yaml-language-server: $schema=${schemaPrefix}.${type}.${manifestVersion}.schema.json\n`;
const installerEntries = normalizedInstallers
.map(
({ architecture, sha256, url }) => `- Architecture: ${architecture}
NestedInstallerFiles:
- RelativeFilePath: relmio.exe
PortableCommandAlias: relmio
InstallerUrl: ${url}
InstallerSha256: ${sha256}`,
)
.join("\n");
return new Map([
[
`${packageIdentifier}.yaml`,
`${header("version")}
PackageIdentifier: ${packageIdentifier}
PackageVersion: ${version}
DefaultLocale: en-US
ManifestType: version
ManifestVersion: ${manifestVersion}
`,
],
[
`${packageIdentifier}.locale.en-US.yaml`,
`${header("defaultLocale")}
PackageIdentifier: ${packageIdentifier}
PackageVersion: ${version}
PackageLocale: en-US
Publisher: ${packagePublisher}
PublisherUrl: ${repositoryUrl}
PublisherSupportUrl: ${repositoryUrl}/issues
PackageName: ${packageName}
PackageUrl: ${repositoryUrl}
License: Apache-2.0
LicenseUrl: ${repositoryUrl}/blob/main/LICENSE
ShortDescription: Create a private OpenAI-compatible endpoint beside self-hosted n8n.
Tags:
- n8n
- oauth
- openai
ManifestType: defaultLocale
ManifestVersion: ${manifestVersion}
`,
],
[
`${packageIdentifier}.installer.yaml`,
`${header("installer")}
PackageIdentifier: ${packageIdentifier}
PackageVersion: ${version}
InstallerType: zip
NestedInstallerType: portable
ArchiveBinariesDependOnPath: true
Commands:
- relmio
Installers:
${installerEntries}
ManifestType: installer
ManifestVersion: ${manifestVersion}
`,
],
]);
}
export async function sha256File(path) {
const contents = await readFile(path);
return createHash("sha256").update(contents).digest("hex");
}
async function assertFile(path, label) {
let metadata;
try {
metadata = await stat(path);
} catch (error) {
if (error.code === "ENOENT") {
throw new Error(`${label} does not exist: ${path}`);
}
throw error;
}
if (!metadata.isFile() || metadata.size === 0) {
throw new Error(`${label} must be a non-empty regular file: ${path}`);
}
}
export async function createPackageManagerCandidates({
npmTarballPath,
outputDirectory,
packageJson,
wingetInstallers = [],
}) {
const { version } = validateReleasePackage(packageJson);
assertStableCatalogVersion(version);
const resolvedOutputDirectory = resolve(outputDirectory);
if (npmTarballPath === undefined && wingetInstallers.length === 0) {
throw new Error("A published npm tarball or at least one WinGet installer is required.");
}
let resolvedNpmTarballPath;
if (npmTarballPath !== undefined) {
resolvedNpmTarballPath = resolve(npmTarballPath);
const expectedNpmTarball = `relmio-${version}.tgz`;
if (basename(resolvedNpmTarballPath) !== expectedNpmTarball) {
throw new Error(
`npm tarball must be named ${expectedNpmTarball}; received ${basename(resolvedNpmTarballPath)}.`,
);
}
await assertFile(resolvedNpmTarballPath, "npm tarball");
}
for (const installer of wingetInstallers) {
if (!supportedArchitectures.has(installer.architecture)) {
throw new Error(`Unsupported WinGet architecture: ${installer.architecture}.`);
}
const path = resolve(installer.path);
const expectedName = `relmio-${version}-windows-${installer.architecture}.zip`;
if (basename(path) !== expectedName) {
throw new Error(
`WinGet ${installer.architecture} archive must be named ${expectedName}; received ${basename(path)}.`,
);
}
await assertFile(path, `WinGet ${installer.architecture} archive`);
}
try {
await access(resolvedOutputDirectory);
throw new Error(
`Output directory already exists: ${resolvedOutputDirectory}. Choose a new empty staging directory.`,
);
} catch (error) {
if (error.code !== "ENOENT") {
throw error;
}
}
const generatedPaths = [];
if (resolvedNpmTarballPath !== undefined) {
const formulaPath = join(
resolvedOutputDirectory,
"homebrew-tap",
"Formula",
"relmio.rb",
);
const formula = createHomebrewFormula({
sha256: await sha256File(resolvedNpmTarballPath),
version,
});
await mkdir(dirname(formulaPath), { recursive: true });
await writeFile(formulaPath, formula, { encoding: "utf8", flag: "wx" });
generatedPaths.push(formulaPath);
}
if (wingetInstallers.length > 0) {
const wingetDirectory = join(
resolvedOutputDirectory,
"winget-pkgs",
"manifests",
"d",
packagePublisher,
packageName,
version,
);
const installerMetadata = await Promise.all(
wingetInstallers.map(async ({ architecture, path }) => ({
architecture,
sha256: await sha256File(resolve(path)),
url: wingetInstallerUrl({ architecture, version }),
})),
);
const manifests = createWingetManifestFiles({
installers: installerMetadata,
version,
});
await mkdir(wingetDirectory, { recursive: true });
for (const [name, contents] of manifests) {
const path = join(wingetDirectory, name);
await writeFile(path, contents, { encoding: "utf8", flag: "wx" });
generatedPaths.push(path);
}
}
return { generatedPaths, outputDirectory: resolvedOutputDirectory, version };
}
function parseArguments(argumentsList) {
const options = {};
const accepted = new Set([
"--npm-tarball",
"--output-dir",
"--winget-arm64",
"--winget-x64",
]);
for (let index = 0; index < argumentsList.length; index += 1) {
const argument = argumentsList[index];
if (!accepted.has(argument)) {
throw new Error(`Unknown argument: ${argument}`);
}
if (options[argument] !== undefined) {
throw new Error(`Argument supplied more than once: ${argument}`);
}
const value = argumentsList[index + 1];
if (value === undefined || value.startsWith("--")) {
throw new Error(`${argument} requires a value.`);
}
options[argument] = value;
index += 1;
}
for (const required of ["--output-dir"]) {
if (options[required] === undefined) {
throw new Error(`${required} is required.`);
}
}
return options;
}
async function main() {
const options = parseArguments(process.argv.slice(2));
const packageJson = JSON.parse(
await readFile(join(projectRoot, "package.json"), "utf8"),
);
const wingetInstallers = [
["x64", options["--winget-x64"]],
["arm64", options["--winget-arm64"]],
]
.filter(([, path]) => path !== undefined)
.map(([architecture, path]) => ({ architecture, path }));
const result = await createPackageManagerCandidates({
npmTarballPath: options["--npm-tarball"],
outputDirectory: options["--output-dir"],
packageJson,
wingetInstallers,
});
console.log(`Generated package-manager candidates for v${result.version}.`);
for (const path of result.generatedPaths) {
console.log(path);
}
console.log("No Homebrew tap, GitHub Release asset, or WinGet manifest was published.");
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});
}