Skip to content

Commit

Permalink
Merge pull request #3 from lafkpages/cli
Browse files Browse the repository at this point in the history
CLI
  • Loading branch information
lafkpages authored Oct 16, 2023
2 parents bfd3583 + 64ae84a commit 9b03fd1
Show file tree
Hide file tree
Showing 11 changed files with 211 additions and 1 deletion.
Binary file modified bun.lockb
Binary file not shown.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,10 @@
"pretest": "bun scripts/cleanupTestData.ts",
"test": "bun test",
"prepublishOnly": "bun run build"
},
"bin": "dist/cli/index.js",
"optionalDependencies": {
"ansi-colors": "^4.1.3",
"enquirer": "^2.4.1"
}
}
2 changes: 1 addition & 1 deletion scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ await tsc.exited;

// Build the project
await Bun.build({
entrypoints: ["src/index.ts"],
entrypoints: ["src/index.ts", "src/cli/index.ts"],
root: "src",
target: "node",
minify: true,
Expand Down
29 changes: 29 additions & 0 deletions src/cli/commands/extract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Asar, BaseEntry } from "../..";

import { readFile, writeFile, mkdir } from "fs/promises";

import { join as joinPaths, dirname } from "path";

export default async function extract(...args: string[]) {
const [archive, output] = args;

const asarBytes = await readFile(archive);

const asar = new Asar(asarBytes);

for (const [, filePathChunks, fileEntry] of asar.walkFiles(true)) {
if (BaseEntry.isDirectory(fileEntry)) {
const fileDirPath = joinPaths(output, ...filePathChunks);

await mkdir(fileDirPath, {
recursive: true,
});
} else {
const filePath = joinPaths(output, ...filePathChunks);

const fileData = asar.readFile(fileEntry);

await writeFile(filePath, fileData);
}
}
}
5 changes: 5 additions & 0 deletions src/cli/commands/help.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { help as showHelp } from "../help";

export default function help(...args: string[]) {
showHelp();
}
17 changes: 17 additions & 0 deletions src/cli/commands/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Asar } from "../..";

import { readFile } from "fs/promises";

import { join as joinPaths } from "path";

export default async function list(...args: string[]) {
const [archive] = args;

const asarBytes = await readFile(archive);

const asar = new Asar(asarBytes);

for (const [, filePathChunks] of asar.walkFiles(true)) {
console.log(joinPaths(...filePathChunks));
}
}
47 changes: 47 additions & 0 deletions src/cli/commands/pack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Asar } from "../..";

import { readFile, readdir, writeFile } from "fs/promises";

import { join as joinPaths } from "path";

async function* walk(
dir: string,
includeDirectories = false
): AsyncGenerator<[string, boolean], void, unknown> {
const entries = await readdir(dir, {
withFileTypes: true,
});

for (const entry of entries) {
const entryPath = joinPaths(dir, entry.name);

const isDir = entry.isDirectory();

if (isDir) {
if (includeDirectories) {
yield [entryPath, isDir];
}

yield* walk(entryPath);
} else {
yield [entryPath, isDir];
}
}
}

export default async function pack(...args: string[]) {
const [input, archive] = args;

const asar = new Asar();

for await (const [filePath] of walk(input)) {
const fileData = await readFile(joinPaths(input, "..", filePath));

asar.writeFile(filePath, fileData, true);
}

// Save Asar
const asarData = asar.getData();

await writeFile(archive, asarData.bytes);
}
41 changes: 41 additions & 0 deletions src/cli/help.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { colors } from "../log";

export function help(exit: number | false = 0) {
// If exiting with a non-zero exit code,
// print the error message to stderr
console[exit !== false && exit !== 0 ? "error" : "log"](
colors.reset(`${colors.magenta.bold("Usage:")} ${colors.green(
"fast-asar"
)} ${colors.blue("<command>")}
${colors.blue.bold("Commands:")}
${colors.blue("extract")} ${colors.yellow(
"<archive> <output>"
)} Extract an archive
${colors.blue("pack")} ${colors.yellow(
"<input> <archive>"
)} Pack a directory into an archive
${colors.blue("list")} ${colors.yellow(
"<archive>"
)} List the contents of an archive
${colors.blue("help")} Show this help message
${colors.magenta.bold("Examples:")}
${colors.magenta("bunx")} ${colors.green("fast-asar")} ${colors.blue(
"extract"
)} ${colors.yellow("app.asar app/")}
${colors.magenta("bunx")} ${colors.green("fast-asar")} ${colors.blue(
"pack"
)} ${colors.yellow("app/ app.asar")}
${colors.magenta("bunx")} ${colors.green("fast-asar")} ${colors.blue(
"list"
)} ${colors.yellow("app.asar")}
${colors.magenta("bunx")} ${colors.green("fast-asar")} ${colors.blue(
"help"
)}`)
);

if (exit !== false) {
process.exit(exit);
}
}
31 changes: 31 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env node

import { help as showHelp } from "./help";
import { colors, error } from "../log";

import extract from "./commands/extract";
import pack from "./commands/pack";
import list from "./commands/list";
import help from "./commands/help";

const command = process.argv[2];

if (!command) {
error("No command specified\n");
showHelp(2);
}

const commands = {
extract,
pack,
list,
help,
};
type Command = keyof typeof commands;

if (!(command in commands)) {
error(`Unknown command ${colors.bold(`"${command}"`)}\n`);
showHelp(2);
}

commands[command as Command](...process.argv.slice(3));
26 changes: 26 additions & 0 deletions src/log/colors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
let colors: typeof import("ansi-colors");
try {
colors = await import("ansi-colors");
} catch {
// If ansi-colors is not installed, return a proxy that returns itself
// when any property is accessed
// @ts-expect-error
colors = new Proxy((a: string) => a, {
get() {
return colors;
},
});

// This is done so that if color functions are called, they will
// just return the string that was passed to them without any
// colors, instead of throwing an error because the function
// doesn't exist.

// So for example, the following code would log "Hello, world!"
// in red if ansi-colors is installed, and just "Hello, world!"
// with no colors if it isn't installed:
//
// console.log(colors.red("Hello, world!"));
}

export { colors };
9 changes: 9 additions & 0 deletions src/log/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { colors } from "./colors";

export function error(message: string) {
console.error(colors.red("error") + colors.gray(":"), colors.reset(message));
}

export const ok = colors.ok;

export { colors };

0 comments on commit 9b03fd1

Please sign in to comment.