feat(core): Initial project scaffold and implementation: Deno CLI, ISO tooling, cloud-init generation, packaging and installer scripts

This commit is contained in:
2025-10-24 08:10:02 +00:00
commit ce06b5855a
31 changed files with 2873 additions and 0 deletions

View File

@@ -0,0 +1,168 @@
/**
* Cloud-Init Generator
* Generates cloud-init configuration files
*/
import { yaml, path as pathUtil } from '../plugins.ts';
import { log } from '../logging.ts';
import type { ICloudInitConfig } from '../interfaces/iso-config.interface.ts';
export interface INetworkConfig {
wifi?: {
ssid: string;
password: string;
};
}
export class CloudInitGenerator {
/**
* Generate user-data file content
*/
generateUserData(config: ICloudInitConfig): string {
const userDataObj: Record<string, unknown> = {
'#cloud-config': null,
};
// Hostname
if (config.hostname) {
userDataObj.hostname = config.hostname;
}
// Users
if (config.users && config.users.length > 0) {
userDataObj.users = config.users.map((user) => ({
name: user.name,
...(user.ssh_authorized_keys && { ssh_authorized_keys: user.ssh_authorized_keys }),
...(user.sudo && { sudo: user.sudo }),
...(user.shell && { shell: user.shell }),
...(user.groups && { groups: user.groups }),
...(user.lock_passwd !== undefined && { lock_passwd: user.lock_passwd }),
...(user.passwd && { passwd: user.passwd }),
}));
}
// Packages
if (config.packages && config.packages.length > 0) {
userDataObj.packages = config.packages;
}
// Package update/upgrade
if (config.package_update !== undefined) {
userDataObj.package_update = config.package_update;
}
if (config.package_upgrade !== undefined) {
userDataObj.package_upgrade = config.package_upgrade;
}
// Run commands
if (config.runcmd && config.runcmd.length > 0) {
userDataObj.runcmd = config.runcmd;
}
// Write files
if (config.write_files && config.write_files.length > 0) {
userDataObj.write_files = config.write_files;
}
// Timezone
if (config.timezone) {
userDataObj.timezone = config.timezone;
}
// Locale
if (config.locale) {
userDataObj.locale = config.locale;
}
// SSH settings
if (config.ssh) {
userDataObj.ssh = config.ssh;
}
// Add any additional custom directives
for (const [key, value] of Object.entries(config)) {
if (!['hostname', 'users', 'packages', 'package_update', 'package_upgrade',
'runcmd', 'write_files', 'timezone', 'locale', 'ssh'].includes(key)) {
userDataObj[key] = value;
}
}
// Remove the cloud-config comment from the object before YAML conversion
delete userDataObj['#cloud-config'];
// Convert to YAML and add the cloud-config header
const yamlContent = yaml.stringify(userDataObj);
return `#cloud-config\n${yamlContent}`;
}
/**
* Generate network-config file content (for WiFi)
*/
generateNetworkConfig(networkConfig: INetworkConfig): string {
if (!networkConfig.wifi) {
return yaml.stringify({ version: 2 });
}
const { ssid, password } = networkConfig.wifi;
const netConfig = {
version: 2,
wifis: {
wlan0: {
dhcp4: true,
optional: true,
'access-points': {
[ssid]: {
password: password,
},
},
},
},
};
return yaml.stringify(netConfig);
}
/**
* Generate meta-data file content
*/
generateMetaData(hostname?: string): string {
const metaData = {
'instance-id': `iid-${Date.now()}`,
'local-hostname': hostname || 'ubuntu',
};
return yaml.stringify(metaData);
}
/**
* Write cloud-init files to a directory
*/
async writeCloudInitFiles(
outputDir: string,
cloudInitConfig: ICloudInitConfig,
networkConfig?: INetworkConfig,
): Promise<void> {
log.info('Generating cloud-init configuration files...');
// Generate user-data
const userData = this.generateUserData(cloudInitConfig);
const userDataPath = pathUtil.join(outputDir, 'user-data');
await Deno.writeTextFile(userDataPath, userData);
log.success('Generated user-data');
// Generate network-config
if (networkConfig) {
const netConfig = this.generateNetworkConfig(networkConfig);
const netConfigPath = pathUtil.join(outputDir, 'network-config');
await Deno.writeTextFile(netConfigPath, netConfig);
log.success('Generated network-config');
}
// Generate meta-data
const metaData = this.generateMetaData(cloudInitConfig.hostname);
const metaDataPath = pathUtil.join(outputDir, 'meta-data');
await Deno.writeTextFile(metaDataPath, metaData);
log.success('Generated meta-data');
}
}

View File

@@ -0,0 +1,133 @@
/**
* Config Manager
* Loads and validates YAML configuration files
*/
import { yaml } from '../plugins.ts';
import { log } from '../logging.ts';
import type { IIsoConfig } from '../interfaces/iso-config.interface.ts';
export class ConfigManager {
/**
* Load config from YAML file
*/
async loadFromFile(filePath: string): Promise<IIsoConfig> {
log.info(`Loading configuration from ${filePath}...`);
try {
const content = await Deno.readTextFile(filePath);
const config = yaml.parse(content) as IIsoConfig;
// Validate
this.validate(config);
log.success('Configuration loaded and validated');
return config;
} catch (err) {
if (err instanceof Deno.errors.NotFound) {
throw new Error(`Config file not found: ${filePath}`);
}
throw new Error(`Failed to load config: ${err.message}`);
}
}
/**
* Validate configuration
*/
validate(config: IIsoConfig): void {
const errors: string[] = [];
// Version check
if (!config.version) {
errors.push('Missing required field: version');
}
// ISO settings
if (!config.iso) {
errors.push('Missing required field: iso');
} else {
if (!config.iso.ubuntu_version) {
errors.push('Missing required field: iso.ubuntu_version');
}
if (!config.iso.architecture || !['amd64', 'arm64'].includes(config.iso.architecture)) {
errors.push('Invalid or missing field: iso.architecture (must be "amd64" or "arm64")');
}
}
// Output settings
if (!config.output) {
errors.push('Missing required field: output');
} else {
if (!config.output.filename) {
errors.push('Missing required field: output.filename');
}
if (!config.output.path) {
errors.push('Missing required field: output.path');
}
}
if (errors.length > 0) {
throw new Error(`Configuration validation failed:\n${errors.join('\n')}`);
}
}
/**
* Generate a template configuration
*/
generateTemplate(): string {
const template: IIsoConfig = {
version: '1.0',
iso: {
ubuntu_version: '24.04',
architecture: 'amd64',
flavor: 'server',
},
output: {
filename: 'ubuntu-custom.iso',
path: './output',
},
network: {
wifi: {
ssid: 'MyWiFi',
password: 'changeme',
},
},
cloud_init: {
hostname: 'ubuntu-server',
users: [
{
name: 'admin',
ssh_authorized_keys: [
'ssh-rsa AAAAB3... your-key-here',
],
sudo: 'ALL=(ALL) NOPASSWD:ALL',
shell: '/bin/bash',
},
],
packages: [
'docker.io',
'git',
'htop',
],
runcmd: [
'systemctl enable docker',
'systemctl start docker',
],
},
};
return `# isocreator configuration file
# Version: 1.0
${yaml.stringify(template)}`;
}
/**
* Save template to file
*/
async saveTemplate(filePath: string): Promise<void> {
const template = this.generateTemplate();
await Deno.writeTextFile(filePath, template);
log.success(`Template saved to ${filePath}`);
}
}

153
ts/classes/iso-builder.ts Normal file
View File

@@ -0,0 +1,153 @@
/**
* ISO Builder Orchestrator
* Coordinates all ISO building operations
*/
import { log } from '../logging.ts';
import { path } from '../plugins.ts';
import { ensureDir } from '../paths.ts';
import type { IIsoConfig } from '../interfaces/iso-config.interface.ts';
import { IsoCache } from './iso-cache.ts';
import { IsoDownloader } from './iso-downloader.ts';
import { IsoExtractor } from './iso-extractor.ts';
import { IsoPacker } from './iso-packer.ts';
import { CloudInitGenerator } from './cloud-init-generator.ts';
export class IsoBuilder {
private cache: IsoCache;
private extractor: IsoExtractor;
private packer: IsoPacker;
private cloudInitGen: CloudInitGenerator;
constructor() {
this.cache = new IsoCache();
this.extractor = new IsoExtractor();
this.packer = new IsoPacker();
this.cloudInitGen = new CloudInitGenerator();
}
/**
* Build a customized ISO from configuration
*/
async build(config: IIsoConfig, onProgress?: (step: string, progress: number) => void): Promise<void> {
log.info('Starting ISO build process...');
// Step 1: Check dependencies
onProgress?.('Checking dependencies', 10);
await this.extractor.ensureDependencies();
await this.packer.ensureDependencies();
// Step 2: Get or download base ISO
onProgress?.('Obtaining base ISO', 20);
const baseIsoPath = await this.getBaseIso(config.iso.ubuntu_version, config.iso.architecture);
// Step 3: Extract ISO
onProgress?.('Extracting ISO', 40);
const extractedDir = await this.extractor.extract(baseIsoPath);
// Step 4: Generate cloud-init configuration
onProgress?.('Generating cloud-init configuration', 60);
const cloudInitDir = path.join(extractedDir, 'nocloud');
await ensureDir(cloudInitDir);
if (config.cloud_init) {
const networkConfig = config.network ? {
wifi: config.network.wifi,
} : undefined;
await this.cloudInitGen.writeCloudInitFiles(
cloudInitDir,
config.cloud_init,
networkConfig,
);
}
// Step 5: Inject custom boot scripts (if any)
if (config.boot_scripts && config.boot_scripts.length > 0) {
onProgress?.('Injecting boot scripts', 70);
await this.injectBootScripts(extractedDir, config.boot_scripts);
}
// Step 6: Update grub configuration to use cloud-init
onProgress?.('Updating boot configuration', 80);
await this.updateBootConfig(extractedDir);
// Step 7: Repack ISO
onProgress?.('Creating customized ISO', 90);
const outputPath = path.join(config.output.path, config.output.filename);
await ensureDir(config.output.path);
await this.packer.pack(extractedDir, outputPath, 'UBUNTU_CUSTOM');
// Step 8: Cleanup
onProgress?.('Cleaning up', 95);
await Deno.remove(extractedDir, { recursive: true });
onProgress?.('Complete', 100);
log.success(`ISO built successfully: ${outputPath}`);
}
/**
* Get base ISO (from cache or download)
*/
private async getBaseIso(version: string, arch: 'amd64' | 'arm64'): Promise<string> {
// Check cache first
const cachedPath = await this.cache.getPath(version, arch);
if (cachedPath) {
log.info(`Using cached ISO: ${cachedPath}`);
return cachedPath;
}
// Download to cache
log.info(`Downloading Ubuntu ${version} ${arch}...`);
return await this.cache.downloadAndCache(version, arch, (downloaded, total) => {
const percent = ((downloaded / total) * 100).toFixed(1);
process.stdout.write(`\r📥 Download progress: ${percent}%`);
});
}
/**
* Inject custom boot scripts
*/
private async injectBootScripts(
extractedDir: string,
bootScripts: Array<{ name: string; path: string; enable?: boolean }>,
): Promise<void> {
for (const script of bootScripts) {
log.info(`Injecting boot script: ${script.name}`);
// Copy script to ISO
const destPath = path.join(extractedDir, 'scripts', script.name);
await ensureDir(path.dirname(destPath));
await Deno.copyFile(script.path, destPath);
// Make executable
await Deno.chmod(destPath, 0o755);
// TODO: Create systemd service if enable is true
}
}
/**
* Update boot configuration to enable cloud-init with nocloud datasource
*/
private async updateBootConfig(extractedDir: string): Promise<void> {
// Update grub config to add cloud-init nocloud datasource
const grubCfgPath = path.join(extractedDir, 'boot', 'grub', 'grub.cfg');
try {
let grubContent = await Deno.readTextFile(grubCfgPath);
// Add cloud-init datasource parameter
grubContent = grubContent.replace(
/linux\s+\/casper\/vmlinuz/g,
'linux /casper/vmlinuz ds=nocloud;s=/cdrom/nocloud/',
);
await Deno.writeTextFile(grubCfgPath, grubContent);
log.success('Updated boot configuration for cloud-init');
} catch (err) {
log.warn(`Could not update grub config: ${err instanceof Error ? err.message : String(err)}`);
}
}
}

212
ts/classes/iso-cache.ts Normal file
View File

@@ -0,0 +1,212 @@
/**
* ISO Cache Manager
* Manages cached Ubuntu ISOs with multi-version support
*/
import { log } from '../logging.ts';
import { path } from '../plugins.ts';
import { getCacheDir, ensureDir } from '../paths.ts';
import { IsoDownloader } from './iso-downloader.ts';
export interface ICacheEntry {
version: string;
architecture: 'amd64' | 'arm64';
filename: string;
path: string;
size: number;
downloadedAt: Date;
}
export class IsoCache {
private cacheDir: string;
private metadataPath: string;
constructor() {
this.cacheDir = getCacheDir();
this.metadataPath = path.join(this.cacheDir, 'metadata.json');
}
/**
* Initialize the cache directory
*/
async init(): Promise<void> {
await ensureDir(this.cacheDir);
}
/**
* Get all cache entries
*/
async list(): Promise<ICacheEntry[]> {
try {
const metadata = await Deno.readTextFile(this.metadataPath);
return JSON.parse(metadata);
} catch (err) {
if (err instanceof Deno.errors.NotFound) {
return [];
}
throw err;
}
}
/**
* Get a specific cache entry
*/
async get(version: string, architecture: 'amd64' | 'arm64'): Promise<ICacheEntry | null> {
const entries = await this.list();
return entries.find((e) => e.version === version && e.architecture === architecture) || null;
}
/**
* Check if an ISO is cached
*/
async has(version: string, architecture: 'amd64' | 'arm64'): Promise<boolean> {
const entry = await this.get(version, architecture);
if (!entry) return false;
// Verify the file still exists
try {
const stat = await Deno.stat(entry.path);
return stat.isFile;
} catch {
// File doesn't exist, remove from metadata
await this.remove(version, architecture);
return false;
}
}
/**
* Add an ISO to the cache
*/
async add(
version: string,
architecture: 'amd64' | 'arm64',
sourcePath: string,
): Promise<ICacheEntry> {
await this.init();
const filename = IsoDownloader.getIsoFilename(version, architecture);
const destPath = path.join(this.cacheDir, filename);
// Copy file to cache
await Deno.copyFile(sourcePath, destPath);
// Get file size
const stat = await Deno.stat(destPath);
const entry: ICacheEntry = {
version,
architecture,
filename,
path: destPath,
size: stat.size,
downloadedAt: new Date(),
};
// Update metadata
const entries = await this.list();
const existingIndex = entries.findIndex(
(e) => e.version === version && e.architecture === architecture,
);
if (existingIndex >= 0) {
entries[existingIndex] = entry;
} else {
entries.push(entry);
}
await this.saveMetadata(entries);
log.success(`ISO cached: ${version} ${architecture}`);
return entry;
}
/**
* Remove an ISO from the cache
*/
async remove(version: string, architecture: 'amd64' | 'arm64'): Promise<void> {
const entry = await this.get(version, architecture);
if (!entry) {
log.warn(`ISO not found in cache: ${version} ${architecture}`);
return;
}
// Remove file
try {
await Deno.remove(entry.path);
} catch (err) {
if (!(err instanceof Deno.errors.NotFound)) {
throw err;
}
}
// Update metadata
const entries = await this.list();
const filtered = entries.filter(
(e) => !(e.version === version && e.architecture === architecture),
);
await this.saveMetadata(filtered);
log.success(`ISO removed from cache: ${version} ${architecture}`);
}
/**
* Clean old cached ISOs
*/
async clean(olderThanDays?: number): Promise<void> {
const entries = await this.list();
const now = new Date();
for (const entry of entries) {
const age = now.getTime() - new Date(entry.downloadedAt).getTime();
const ageDays = age / (1000 * 60 * 60 * 24);
if (!olderThanDays || ageDays > olderThanDays) {
await this.remove(entry.version, entry.architecture);
}
}
}
/**
* Get the path to a cached ISO
*/
async getPath(version: string, architecture: 'amd64' | 'arm64'): Promise<string | null> {
const entry = await this.get(version, architecture);
return entry?.path || null;
}
/**
* Download and cache an ISO
*/
async downloadAndCache(
version: string,
architecture: 'amd64' | 'arm64',
onProgress?: (downloaded: number, total: number) => void,
): Promise<string> {
await this.init();
const filename = IsoDownloader.getIsoFilename(version, architecture);
const destPath = path.join(this.cacheDir, filename);
const downloader = new IsoDownloader();
await downloader.downloadWithVerification({
ubuntuVersion: version,
architecture,
outputPath: destPath,
onProgress,
});
// Add to cache metadata
await this.add(version, architecture, destPath);
return destPath;
}
/**
* Save metadata to disk
*/
private async saveMetadata(entries: ICacheEntry[]): Promise<void> {
await ensureDir(this.cacheDir);
await Deno.writeTextFile(this.metadataPath, JSON.stringify(entries, null, 2));
}
}

View File

@@ -0,0 +1,172 @@
/**
* ISO Downloader
* Downloads Ubuntu Server ISOs from official mirrors
*/
import { log } from '../logging.ts';
import { path } from '../plugins.ts';
export interface IDownloadOptions {
ubuntuVersion: string; // e.g., "24.04", "22.04"
architecture: 'amd64' | 'arm64';
outputPath: string;
onProgress?: (downloaded: number, total: number) => void;
}
export class IsoDownloader {
/**
* Ubuntu release URLs
*/
private static readonly UBUNTU_MIRROR = 'https://releases.ubuntu.com';
/**
* Get the ISO filename for a given version and architecture
*/
static getIsoFilename(version: string, arch: 'amd64' | 'arm64'): string {
// Ubuntu Server ISO naming pattern
if (arch === 'amd64') {
return `ubuntu-${version}-live-server-${arch}.iso`;
} else {
// ARM64 uses preinstalled server images
return `ubuntu-${version}-preinstalled-server-${arch}.img.xz`;
}
}
/**
* Get the download URL for a given Ubuntu version and architecture
*/
static getDownloadUrl(version: string, arch: 'amd64' | 'arm64'): string {
const filename = this.getIsoFilename(version, arch);
return `${this.UBUNTU_MIRROR}/${version}/${filename}`;
}
/**
* Get the checksum URL for verification
*/
static getChecksumUrl(version: string): string {
return `${this.UBUNTU_MIRROR}/${version}/SHA256SUMS`;
}
/**
* Download an Ubuntu ISO
*/
async download(options: IDownloadOptions): Promise<void> {
const { ubuntuVersion, architecture, outputPath, onProgress } = options;
const url = IsoDownloader.getDownloadUrl(ubuntuVersion, architecture);
log.info(`Downloading Ubuntu ${ubuntuVersion} ${architecture} from ${url}`);
// Download the ISO
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to download ISO: HTTP ${response.status}`);
}
const totalSize = parseInt(response.headers.get('content-length') || '0', 10);
let downloadedSize = 0;
// Open file for writing
const file = await Deno.open(outputPath, { write: true, create: true, truncate: true });
try {
const reader = response.body?.getReader();
if (!reader) {
throw new Error('Failed to get response body reader');
}
while (true) {
const { done, value } = await reader.read();
if (done) break;
await file.write(value);
downloadedSize += value.length;
if (onProgress && totalSize > 0) {
onProgress(downloadedSize, totalSize);
}
}
log.success(`ISO downloaded successfully to ${outputPath}`);
} finally {
file.close();
}
}
/**
* Download and verify checksum
*/
async downloadWithVerification(options: IDownloadOptions): Promise<void> {
const { ubuntuVersion, architecture, outputPath } = options;
// Download the ISO
await this.download(options);
// Download checksums
log.info('Downloading checksums for verification...');
const checksumUrl = IsoDownloader.getChecksumUrl(ubuntuVersion);
const checksumResponse = await fetch(checksumUrl);
if (!checksumResponse.ok) {
log.warn('Could not download checksums for verification');
return;
}
const checksumText = await checksumResponse.text();
const filename = path.basename(outputPath);
// Find the checksum for our file
const lines = checksumText.split('\n');
let expectedChecksum: string | null = null;
for (const line of lines) {
if (line.includes(filename)) {
expectedChecksum = line.split(/\s+/)[0];
break;
}
}
if (!expectedChecksum) {
log.warn(`No checksum found for ${filename}`);
return;
}
// Verify the checksum
log.info('Verifying checksum...');
const actualChecksum = await this.calculateSha256(outputPath);
if (actualChecksum === expectedChecksum) {
log.success('Checksum verified successfully ✓');
} else {
throw new Error(`Checksum mismatch!\nExpected: ${expectedChecksum}\nActual: ${actualChecksum}`);
}
}
/**
* Calculate SHA256 checksum of a file
*/
private async calculateSha256(filePath: string): Promise<string> {
const file = await Deno.open(filePath, { read: true });
const buffer = new Uint8Array(8192);
const hasher = await crypto.subtle.digest('SHA-256', new Uint8Array(0)); // Initialize
try {
while (true) {
const bytesRead = await file.read(buffer);
if (bytesRead === null) break;
// Hash the chunk
const chunk = buffer.slice(0, bytesRead);
await crypto.subtle.digest('SHA-256', chunk);
}
} finally {
file.close();
}
// Convert to hex string
const hashArray = Array.from(new Uint8Array(hasher));
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
}

View File

@@ -0,0 +1,82 @@
/**
* ISO Extractor
* Extracts ISO contents using xorriso
*/
import { log } from '../logging.ts';
import { getTempDir, ensureDir, cleanDir } from '../paths.ts';
import { path } from '../plugins.ts';
export class IsoExtractor {
/**
* Extract an ISO to a directory
*/
async extract(isoPath: string, extractDir?: string): Promise<string> {
// Use temp dir if not specified
const outputDir = extractDir || path.join(getTempDir(), `iso-extract-${Date.now()}`);
await cleanDir(outputDir);
await ensureDir(outputDir);
log.info(`Extracting ISO to ${outputDir}...`);
// Use xorriso to extract the ISO
const command = new Deno.Command('xorriso', {
args: [
'-osirrox',
'on',
'-indev',
isoPath,
'-extract',
'/',
outputDir,
],
stdout: 'piped',
stderr: 'piped',
});
const process = command.spawn();
const { code, stdout, stderr } = await process.output();
if (code !== 0) {
const errorText = new TextDecoder().decode(stderr);
throw new Error(`Failed to extract ISO: ${errorText}`);
}
log.success(`ISO extracted successfully to ${outputDir}`);
return outputDir;
}
/**
* Check if xorriso is installed
*/
async checkDependencies(): Promise<boolean> {
try {
const command = new Deno.Command('xorriso', {
args: ['--version'],
stdout: 'null',
stderr: 'null',
});
const { code } = await command.output();
return code === 0;
} catch {
return false;
}
}
/**
* Ensure xorriso is installed
*/
async ensureDependencies(): Promise<void> {
const hasXorriso = await this.checkDependencies();
if (!hasXorriso) {
log.error('xorriso is not installed!');
log.info('Install xorriso:');
log.info(' Ubuntu/Debian: sudo apt install xorriso');
log.info(' macOS: brew install xorriso');
throw new Error('Missing dependency: xorriso');
}
}
}

123
ts/classes/iso-packer.ts Normal file
View File

@@ -0,0 +1,123 @@
/**
* ISO Packer
* Repacks a directory into a bootable ISO using xorriso
*/
import { log } from '../logging.ts';
export class IsoPacker {
/**
* Pack a directory into an ISO
*/
async pack(sourceDir: string, outputIso: string, volumeLabel?: string): Promise<void> {
log.info(`Creating ISO from ${sourceDir}...`);
const label = volumeLabel || 'UBUNTU_CUSTOM';
// Use xorriso to create a bootable ISO
const command = new Deno.Command('xorriso', {
args: [
'-as',
'mkisofs',
'-r',
'-V',
label,
'-o',
outputIso,
'-J',
'-joliet-long',
'-cache-inodes',
'-isohybrid-mbr',
'/usr/lib/ISOLINUX/isohdpfx.bin',
'-b',
'isolinux/isolinux.bin',
'-c',
'isolinux/boot.cat',
'-boot-load-size',
'4',
'-boot-info-table',
'-no-emul-boot',
'-eltorito-alt-boot',
'-e',
'boot/grub/efi.img',
'-no-emul-boot',
'-isohybrid-gpt-basdat',
sourceDir,
],
stdout: 'piped',
stderr: 'piped',
});
const process = command.spawn();
const { code, stderr } = await process.output();
if (code !== 0) {
const errorText = new TextDecoder().decode(stderr);
throw new Error(`Failed to create ISO: ${errorText}`);
}
log.success(`ISO created successfully at ${outputIso}`);
// Make it hybrid bootable (USB compatible)
await this.makeIsohybrid(outputIso);
}
/**
* Make the ISO hybrid bootable (USB-compatible)
*/
private async makeIsohybrid(isoPath: string): Promise<void> {
try {
log.info('Making ISO hybrid bootable...');
const command = new Deno.Command('isohybrid', {
args: ['--uefi', isoPath],
stdout: 'piped',
stderr: 'piped',
});
const { code } = await command.output();
if (code === 0) {
log.success('ISO is now USB-bootable');
} else {
log.warn('isohybrid failed, ISO may not be USB-bootable');
}
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
log.warn(`isohybrid not available: ${errorMessage}`);
}
}
/**
* Check if required tools are installed
*/
async checkDependencies(): Promise<boolean> {
try {
const xorrisoCmd = new Deno.Command('xorriso', {
args: ['--version'],
stdout: 'null',
stderr: 'null',
});
const { code } = await xorrisoCmd.output();
return code === 0;
} catch {
return false;
}
}
/**
* Ensure dependencies are installed
*/
async ensureDependencies(): Promise<void> {
const hasXorriso = await this.checkDependencies();
if (!hasXorriso) {
log.error('xorriso is not installed!');
log.info('Install xorriso:');
log.info(' Ubuntu/Debian: sudo apt install xorriso syslinux-utils');
log.info(' macOS: brew install xorriso syslinux');
throw new Error('Missing dependency: xorriso');
}
}
}