Expand software catalog with archived releases

This commit is contained in:
2026-08-22 14:49:54 +03:00
parent aa8deee35a
commit 073afb8ed8
92 changed files with 1752 additions and 17 deletions
+35 -12
View File
@@ -22,6 +22,7 @@ type Options = {
source: string;
output: string;
replace: boolean;
temporaryDirectory: string;
};
type TreeEntry = {
@@ -35,17 +36,19 @@ const execFileAsync = promisify(execFile);
const supportedExtensions = new Set([".7z", ".exe", ".rar", ".zip"]);
function usage(): void {
console.log(`Usage: pnpm repack-archive [options] <archive-or-exe>
console.log(`Usage: pnpm repack-archive [options] <archive-exe-or-directory>
Creates a Windows XP-compatible ZIP next to the source file.
Creates a Windows XP-compatible ZIP next to the source.
-o, --output <path> write to a specific ZIP path
--temp-dir <path> use a specific directory for temporary files
--replace delete the source after successful verification
--help show this help message`);
}
function parseArguments(args: string[]): Options {
let output: string | undefined;
let temporaryDirectory = tmpdir();
let replace = false;
const positional: string[] = [];
@@ -62,6 +65,13 @@ function parseArguments(args: string[]): Options {
throw new Error(`${argument} requires a path`);
}
index += 1;
} else if (argument === "--temp-dir") {
const value = args[index + 1];
if (value === undefined) {
throw new Error(`${argument} requires a path`);
}
temporaryDirectory = path.resolve(value);
index += 1;
} else if (argument.startsWith("-")) {
throw new Error(`Unknown option: ${argument}`);
} else {
@@ -70,17 +80,17 @@ function parseArguments(args: string[]): Options {
}
if (positional.length !== 1) {
throw new Error("Expected one archive or EXE path");
throw new Error("Expected one archive, EXE, or directory path");
}
const source = path.resolve(positional[0]);
const extension = path.extname(source).toLowerCase();
if (!supportedExtensions.has(extension)) {
throw new Error(`Unsupported source type: ${extension || "no extension"}`);
}
const sourceBasename = supportedExtensions.has(extension)
? path.basename(source, extension)
: path.basename(source);
const destination = path.resolve(
output ?? path.join(path.dirname(source), `${path.basename(source, extension)}.zip`),
output ?? path.join(path.dirname(source), `${sourceBasename}.zip`),
);
if (path.extname(destination).toLowerCase() !== ".zip") {
throw new Error("Output path must have the .zip extension");
@@ -89,7 +99,7 @@ function parseArguments(args: string[]): Options {
throw new Error("Source and output paths are the same; use --output for ZIP input");
}
return { source, output: destination, replace };
return { source, output: destination, replace, temporaryDirectory };
}
async function run(
@@ -217,12 +227,23 @@ async function ensureOutputDoesNotExist(output: string): Promise<void> {
async function main(): Promise<void> {
const options = parseArguments(process.argv.slice(2));
const sourceStat = await stat(options.source);
if (!sourceStat.isFile()) {
throw new Error(`Source is not a regular file: ${options.source}`);
if (!sourceStat.isFile() && !sourceStat.isDirectory()) {
throw new Error(`Unsupported source type: ${options.source}`);
}
if (sourceStat.isFile()) {
const extension = path.extname(options.source).toLowerCase();
if (!supportedExtensions.has(extension)) {
throw new Error(`Unsupported source type: ${extension || "no extension"}`);
}
} else if (options.replace) {
throw new Error("--replace is not supported for directory sources");
}
await ensureOutputDoesNotExist(options.output);
const temporaryRoot = await mkdtemp(path.join(tmpdir(), "repack-archive-"));
await mkdir(options.temporaryDirectory, { recursive: true });
const temporaryRoot = await mkdtemp(
path.join(options.temporaryDirectory, "repack-archive-"),
);
const originalDirectory = path.join(temporaryRoot, "original");
const verifiedDirectory = path.join(temporaryRoot, "verified");
const temporaryZip = path.join(temporaryRoot, "output.zip");
@@ -230,7 +251,9 @@ async function main(): Promise<void> {
await mkdir(verifiedDirectory);
try {
if (path.extname(options.source).toLowerCase() === ".exe") {
if (sourceStat.isDirectory()) {
await run("cp", ["-a", "--", path.join(options.source, "."), originalDirectory]);
} else if (path.extname(options.source).toLowerCase() === ".exe") {
await run("cp", ["-p", "--", options.source, originalDirectory]);
} else {
await run("7z", ["t", options.source, "-bso0", "-bsp0"]);