@origami-minecraft/devbuilds
Version:
Origami is a terminal-first Minecraft launcher that supports authentication, installation, and launching of Minecraft versions โ with built-in support for Microsoft accounts, mod loaders, profile management, and more. Designed for power users, modders, an
430 lines (359 loc) โข 15.4 kB
text/typescript
import inquirer from 'inquirer';
//import axios from 'axios';
import ora from 'ora';
import chalk from 'chalk';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import * as tar from 'tar';
import AdmZip from 'adm-zip';
import { downloader } from '../core/utils/download';
import { ensureDir, localpath } from '../core/utils/common';
import * as data_manager from "../core/tools/data_manager";
//const API_BASE = 'https://api.adoptium.net/v3';
function detectOS(): string {
const platform = os.platform();
switch (platform) {
case 'linux': return 'linux';
case 'darwin': return 'mac';
case 'win32': return 'windows';
default: throw new Error(`Oops! Unsupported platform: ${platform} ๐ฅบ`);
}
}
function detectArch(): string {
const arch = os.arch();
switch (arch) {
case 'x64': return 'x64';
case 'arm64': return 'aarch64';
default: throw new Error(`Oops! Unsupported architecture: ${arch} ๐ฟ`);
}
}
/*async function getAvailableVersions(): Promise<string[]> {
const res = await axios.get(`${API_BASE}/info/available_releases`);
return res.data.available_lts_releases.map((v: number) => `Temurin ${v} โจ`).reverse();
}
async function getBinary(version: string, os: string, arch: string, imageType: string) {
const versionNum = version.replace('Temurin ', '').replace(' โจ', '');
const res = await axios.get(`${API_BASE}/assets/feature_releases/${versionNum}/ga`, {
params: {
architecture: arch,
image_type: imageType,
jvm_impl: 'hotspot',
os: os,
heap_size: 'normal',
vendor: 'eclipse'
}
});
const binaries = res.data;
if (!binaries.length) throw new Error('Oh no! No binary found for your selection ๐ข');
return binaries[0].binary ? binaries[0].binary.package : binaries[0].binaries[0].package;
}*/
async function extractArchive(filePath: string, outputDir: string) {
const isZip = filePath.endsWith('.zip');
const isTarGz = filePath.endsWith('.tar.gz');
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir);
}
if (isZip) {
const zip = new AdmZip(filePath);
zip.extractAllTo(outputDir, true);
} else if (isTarGz) {
await tar.x({
file: filePath,
cwd: outputDir
});
} else {
throw new Error('Uh-oh! Unsupported archive format ๐ฟ');
}
}
interface JavaProvider {
name: string;
withJre: boolean;
listVersions(): Promise<string[]>;
getBinary(version: string, os: string, arch: string, imageType: string): Promise<{
name: string;
link: string;
}>;
}
import { temurinProvider } from './providers/temurin';
import { graalvmProvider } from './providers/graalvm';
import { zuluProvider } from './providers/zulu';
import { correttoProvider } from './providers/corretto';
const providers: JavaProvider[] = [temurinProvider, graalvmProvider, zuluProvider, correttoProvider]
async function main() {
const { providerName } = await inquirer.prompt([
{
type: 'list',
name: 'providerName',
message: '๐ Select your Java distribution source:',
choices: providers.map(p => p.name)
}
]);
const provider = providers.find(p => p.name === providerName)!;
const osDetected = detectOS();
const archDetected = detectArch();
const versions = await provider.listVersions();
const { version, imageType } = await inquirer.prompt([
{
type: 'list',
name: 'version',
message: '๐ธ Pick your version:',
choices: versions
},
{
type: 'list',
name: 'imageType',
message: '๐ผ Choose your image type:',
choices: provider.withJre ? ['jdk', 'jre'] : ['jdk']
}
]);
const downloadSpinner = ora('๐ Fetching your binary info... hold tight...').start();
let pkg;
try {
pkg = await provider.getBinary(version, osDetected, archDetected, imageType);
downloadSpinner.succeed('โจ Got your binary info! Let\'s proceed! โจ');
} catch (err: any) {
downloadSpinner.fail('๐ฟ Failed to get the binary info, sorry!');
console.error(err.message);
return;
}
const fileName = pkg.name;
const downloadPath = path.join(localpath(true), fileName);
const LOCAL_PATH = localpath();
const binariesPath = path.join(LOCAL_PATH, 'binaries');
const extractPath = path.join(binariesPath, fileName);
ensureDir(binariesPath);
ensureDir(extractPath);
ensureDir(localpath(true));
console.log(`\n๐ Downloading ${fileName} with love... ๐\n`);
try {
await downloader(pkg.link, downloadPath);
console.log(`\n๐ Download finished! ๐\n`);
} catch (err: any) {
console.error(`๐ฟ Download failed: ${err.message}`);
return;
}
const extractSpinner = ora(`๐ Unwrapping your package into ./binaries ...`).start();
try {
await extractArchive(downloadPath, extractPath);
await fs.promises.rm(downloadPath, { recursive: true, force: true });
const providerTagPath = path.join(extractPath, '.provider');
await fs.promises.writeFile(providerTagPath, provider.name, 'utf-8');
extractSpinner.succeed('๐ Extraction complete! Your JDK is ready to use! ๐');
} catch (err: any) {
extractSpinner.fail('๐ฟ Extraction failed. Something went wrong!');
console.error(err.message);
}
}
/// --- SEPERATOR --- ///
const BANNER = `
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ ๐ ${chalk.cyanBright('Java Binary Selector')} ๐ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
`;
interface JavaBinary {
path: string;
version?: string;
provider?: string;
}
async function selectJavaBinary(use_new: boolean, profileName?: string): Promise<JavaBinary> {
console.log(BANNER);
const basePath = localpath();
const spinner = ora({
text: `${chalk.yellow('Scanning for Java installations...')}`,
spinner: 'dots',
color: 'cyan'
}).start();
const extractPath = path.join(basePath, 'binaries');
const javaInstallations = findJavaInstallations(extractPath);
spinner.stop();
const profileKey = profileName ? `profile:${profileName}:java` : 'use:temurin';
let savedJava: JavaBinary | undefined = data_manager.get(profileKey);
if (!savedJava && profileName) {
savedJava = data_manager.get('use:temurin');
}
if (!data_manager.get(profileKey) && profileName && savedJava) {
console.log(chalk.yellow(`โ ๏ธ No specific Java selected for "${profileName}". Using global Java.`));
}
function logUse(java: JavaBinary) {
console.log(chalk.green(`\nโ
Selected: ${chalk.cyan(java.version || 'Java')} ${chalk.gray(`from ${chalk.yellow(java.provider || 'Unknown')}`)}`));
console.log(chalk.gray(`๐ Path: ${java.path}\n`));
}
if (savedJava && !use_new && fs.existsSync(savedJava.path)) {
logUse(savedJava);
return savedJava;
}
if (javaInstallations.length === 0) {
console.log(chalk.yellow('โ ๏ธ No managed Java installations found. You can still use a custom path or JAVA_HOME.\n'));
}
const { selectedJava } = await inquirer.prompt([
{
type: 'list',
name: 'selectedJava',
message: chalk.hex('#FFA500')(`โจ Select Java binary${profileName ? ` for profile "${profileName}"` : ''}:`),
choices: [
...javaInstallations.map(java => ({
name: `${chalk.cyan(java.version || 'Unknown')} ${chalk.gray(`from ${chalk.yellow(java.provider || 'Unknown')}, ${java.path}`)}`,
value: java
})),
new inquirer.Separator(),
{
name: chalk.magenta('๐ง Use JAVA_HOME or enter custom Java path...'),
value: 'custom'
}
],
pageSize: Math.min(10, javaInstallations.length + 2),
loop: false
}
]);
let selected: JavaBinary;
if (selectedJava === 'custom') {
let customPath = process.env.JAVA_HOME
? path.join(process.env.JAVA_HOME, 'bin', process.platform === 'win32' ? 'java.exe' : 'java')
: '';
if (!customPath || !fs.existsSync(customPath)) {
const { javaPath } = await inquirer.prompt([
{
type: 'input',
name: 'javaPath',
message: chalk.cyan('๐ JAVA_HOME not set. Enter the full path to your Java installation:'),
validate: (input: string) => {
const trimmed = input.trim().replace(/^['"]|['"]$/g, '');
const binJava = path.join(trimmed, 'bin', process.platform === 'win32' ? 'java.exe' : 'java');
if (!fs.existsSync(trimmed)) {
return 'โ That path does not exist.';
}
if (!fs.statSync(trimmed).isDirectory()) {
return 'โ That path is not a directory.';
}
if (!fs.existsSync(binJava)) {
return `โ No Java executable found at: ${chalk.gray(binJava)}`;
}
return true;
},
filter: (input: string) => input.trim().replace(/^['"]|['"]$/g, '')
}
]);
customPath = path.join(javaPath, 'bin', process.platform === 'win32' ? 'java.exe' : 'java');
}
selected = {
path: customPath,
version: 'Custom',
provider: 'manual'
};
let custom_installations = !Array.isArray(data_manager.get('custom:java')) ? [] : data_manager.get('custom:java');
custom_installations.push(selected);
data_manager.set('custom:java', custom_installations);
} else {
selected = selectedJava;
}
logUse(selected);
data_manager.set(profileName ? `profile:${profileName}:java` : 'use:temurin', selected);
data_manager.set(`use:temurin`, selected);
return selected;
}
function findJavaInstallations(basePath: string): JavaBinary[] {
const installations: JavaBinary[] = [];
const custom_installations = data_manager.get('custom:java') || [];
for (const entry of custom_installations) {
if(entry && entry.path && fs.existsSync(entry.path)) installations.push(entry)
}
if (!fs.existsSync(basePath)) return installations;
const entries = fs.readdirSync(basePath, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const outerDir = path.join(basePath, entry.name);
const binDir = path.join(outerDir, 'bin');
const javaExe = process.platform === 'win32' ? 'java.exe' : 'java';
const javaPath = path.join(binDir, javaExe);
const providerPath = path.join(outerDir, '.provider');
const provider = fs.existsSync(providerPath)
? fs.readFileSync(providerPath, 'utf-8').trim()
: undefined;
if (fs.existsSync(javaPath)) {
installations.push({
path: javaPath,
version: extractJavaVersion(entry.name),
provider
});
continue;
}
const innerEntries = fs.readdirSync(outerDir, { withFileTypes: true });
for (const inner of innerEntries) {
if (!inner.isDirectory()) continue;
const innerDir = path.join(outerDir, inner.name);
const innerBin = path.join(innerDir, 'bin');
const innerJava = path.join(innerBin, javaExe);
const innerProviderPath = path.join(outerDir, '.provider');
const innerProvider = fs.existsSync(innerProviderPath)
? fs.readFileSync(innerProviderPath, 'utf-8').trim()
: undefined;
if (fs.existsSync(innerJava)) {
installations.push({
path: innerJava,
version: extractJavaVersion(inner.name),
provider: innerProvider
});
}
}
}
return installations;
}
function extractJavaVersion(folderName: string): string | undefined {
const versionMatch = folderName.match(/(jdk|jre)[-_]?(\d+(?:\.\d+)*)/i);
if (versionMatch) {
return `Java ${versionMatch[2]}`;
}
return undefined;
}
async function deleteJavaBinary(): Promise<void> {
console.log(BANNER);
const basePath = localpath();
const extractPath = path.join(basePath, 'binaries');
const javaInstallations = findJavaInstallations(extractPath);
if (javaInstallations.length === 0) {
console.log(chalk.yellow('โ ๏ธ No Java installations found to delete.'));
return;
}
const { binariesToDelete } = await inquirer.prompt([
{
type: 'checkbox',
name: 'binariesToDelete',
message: chalk.redBright('๐๏ธ Select Java versions to delete:'),
choices: javaInstallations.map(java => ({
name: `${chalk.cyan(java.version || 'Unknown')} ${chalk.gray(`from ${chalk.yellow(java.provider || 'Unknown')}, ${java.path}`)}`,
value: java
})),
pageSize: Math.min(10, javaInstallations.length),
loop: false
}
]);
if (binariesToDelete.length === 0) {
console.log(chalk.yellow('โ No selections made. Aborting deletion.'));
return;
}
for (const java of binariesToDelete) {
if(java.provider === 'manual') {
let custom_installations = Array.isArray(data_manager.get('custom:java')) ? data_manager.get('custom:java') : [];
custom_installations = custom_installations.filter((v: any) => v.path !== java.path);
data_manager.set('custom:java', custom_installations);
console.log(chalk.green(`โ
Deleted from Database: ${chalk.gray(java.path)}`));
continue;
};
const maybeLegacy = path.resolve(java.path, '..', '..');
const maybeModern = path.resolve(java.path, '..', '..', '..');
const deletePath = fs.existsSync(path.join(maybeLegacy, 'bin')) ? maybeLegacy : maybeModern;
if (fs.existsSync(deletePath)) {
try {
await fs.promises.rm(deletePath, { recursive: true, force: true });
console.log(chalk.green(`โ
Deleted: ${chalk.gray(deletePath)}`));
} catch (err: any) {
console.error(chalk.red(`โ Failed to delete ${deletePath}: ${err.message}`));
}
}
}
console.log(chalk.blueBright('\n๐งน Cleanup complete.'));
}
export default { download: main, select: selectJavaBinary, delete: deleteJavaBinary };