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

8
ts/00_commitinfo_data.ts Normal file
View File

@@ -0,0 +1,8 @@
/**
* autocreated commitinfo by @push.rocks/commitinfo
*/
export const commitinfo = {
name: '@serve.zone/isocreator',
version: '1.1.0',
description: 'Ubuntu ISO customization tool for PC and Raspberry Pi with WiFi and cloud-init configuration'
}

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');
}
}
}

275
ts/cli.ts Normal file
View File

@@ -0,0 +1,275 @@
/**
* CLI Interface for isocreator
*/
import { log } from './logging.ts';
import { IsoBuilder } from './classes/iso-builder.ts';
import { IsoCache } from './classes/iso-cache.ts';
import { ConfigManager } from './classes/config-manager.ts';
/**
* Display help information
*/
function showHelp() {
console.log(`
isocreator - Ubuntu ISO Customization Tool
USAGE:
isocreator <COMMAND> [OPTIONS]
COMMANDS:
build Build a customized ISO
cache Manage ISO cache
template Generate config template
validate Validate config file
version Show version information
help Show this help message
BUILD OPTIONS:
--config <file> Use a YAML config file
--ubuntu-version <version> Ubuntu version (22.04, 24.04, etc.)
--arch <arch> Architecture (amd64 or arm64)
--wifi-ssid <ssid> WiFi SSID
--wifi-password <password> WiFi password
--hostname <name> System hostname
--output <path> Output ISO path
--ssh-key <path> Path to SSH public key
CACHE OPTIONS:
cache list List cached ISOs
cache download <version> Download and cache an ISO
cache clean Clean all cached ISOs
TEMPLATE OPTIONS:
template create Generate config template to stdout
template create --output <file> Save template to file
VALIDATE OPTIONS:
validate <file> Validate a config file
EXAMPLES:
# Build from config file
isocreator build --config myconfig.yaml
# Quick build with CLI flags
isocreator build \\
--ubuntu-version 24.04 \\
--arch amd64 \\
--wifi-ssid "MyWiFi" \\
--wifi-password "secret123" \\
--hostname "myserver" \\
--output ./custom-ubuntu.iso
# Generate config template
isocreator template create --output config.yaml
# List cached ISOs
isocreator cache list
SYSTEM REQUIREMENTS:
- xorriso (for ISO manipulation)
- syslinux-utils (for USB-bootable ISOs)
Install on Ubuntu/Debian:
sudo apt install xorriso syslinux-utils
Install on macOS:
brew install xorriso syslinux
For more information, visit:
https://code.foss.global/serve.zone/isocreator
`);
}
/**
* Show version information
*/
function showVersion() {
console.log('isocreator version 1.0.0');
console.log('Ubuntu ISO customization tool');
console.log('https://code.foss.global/serve.zone/isocreator');
}
/**
* Parse CLI arguments
*/
function parseArgs(args: string[]): Map<string, string | boolean> {
const parsed = new Map<string, string | boolean>();
let i = 0;
while (i < args.length) {
const arg = args[i];
if (arg.startsWith('--')) {
const key = arg.substring(2);
const nextArg = args[i + 1];
if (nextArg && !nextArg.startsWith('--')) {
parsed.set(key, nextArg);
i += 2;
} else {
parsed.set(key, true);
i += 1;
}
} else {
parsed.set(`_arg${i}`, arg);
i += 1;
}
}
return parsed;
}
/**
* Handle build command
*/
async function handleBuild(args: Map<string, string | boolean>) {
const builder = new IsoBuilder();
const configManager = new ConfigManager();
// Check if config file is provided
if (args.has('config')) {
const configPath = args.get('config') as string;
const config = await configManager.loadFromFile(configPath);
await builder.build(config, (step, progress) => {
console.log(`[${progress}%] ${step}`);
});
} else {
// Build from CLI flags
log.error('CLI flag mode not yet implemented. Please use --config for now.');
log.info('Generate a template: isocreator template create --output config.yaml');
Deno.exit(1);
}
}
/**
* Handle cache commands
*/
async function handleCache(args: Map<string, string | boolean>) {
const cache = new IsoCache();
const subcommand = args.get('_arg1') as string;
if (!subcommand || subcommand === 'list') {
const entries = await cache.list();
if (entries.length === 0) {
console.log('No cached ISOs found.');
return;
}
console.log('\nCached ISOs:');
console.log('━'.repeat(80));
for (const entry of entries) {
const sizeMB = (entry.size / 1024 / 1024).toFixed(2);
const date = new Date(entry.downloadedAt).toLocaleDateString();
console.log(`${entry.version} (${entry.architecture}) - ${sizeMB} MB - ${date}`);
console.log(` ${entry.path}`);
}
console.log('━'.repeat(80));
} else if (subcommand === 'clean') {
await cache.clean();
console.log('Cache cleaned.');
} else if (subcommand === 'download') {
const version = args.get('_arg2') as string;
const arch = (args.get('arch') as string) || 'amd64';
if (!version) {
log.error('Please specify a version: isocreator cache download <version>');
Deno.exit(1);
}
await cache.downloadAndCache(version, arch as 'amd64' | 'arm64', (downloaded, total) => {
const percent = ((downloaded / total) * 100).toFixed(1);
process.stdout.write(`\r📥 Download progress: ${percent}%`);
});
console.log();
}
}
/**
* Handle template commands
*/
async function handleTemplate(args: Map<string, string | boolean>) {
const configManager = new ConfigManager();
const subcommand = args.get('_arg1') as string;
if (!subcommand || subcommand === 'create') {
const outputPath = args.get('output') as string | undefined;
if (outputPath) {
await configManager.saveTemplate(outputPath);
} else {
console.log(configManager.generateTemplate());
}
}
}
/**
* Handle validate command
*/
async function handleValidate(args: Map<string, string | boolean>) {
const configPath = args.get('_arg1') as string;
if (!configPath) {
log.error('Please specify a config file: isocreator validate <file>');
Deno.exit(1);
}
const configManager = new ConfigManager();
try {
await configManager.loadFromFile(configPath);
log.success('Configuration is valid!');
} catch (err) {
log.error(`Validation failed: ${err instanceof Error ? err.message : String(err)}`);
Deno.exit(1);
}
}
/**
* Main CLI entry point
*/
export async function startCli() {
const args = parseArgs(Deno.args);
const command = args.get('_arg0') as string;
try {
switch (command) {
case 'build':
await handleBuild(args);
break;
case 'cache':
await handleCache(args);
break;
case 'template':
await handleTemplate(args);
break;
case 'validate':
await handleValidate(args);
break;
case 'version':
showVersion();
break;
case 'help':
case undefined:
showHelp();
break;
default:
log.error(`Unknown command: ${command}`);
log.info('Run "isocreator help" for usage information');
Deno.exit(1);
}
} catch (err) {
log.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
Deno.exit(1);
}
}

20
ts/index.ts Normal file
View File

@@ -0,0 +1,20 @@
/**
* Public API exports for isocreator
*/
// Re-export classes when they're implemented
export { IsoBuilder } from './classes/iso-builder.ts';
export { IsoCache } from './classes/iso-cache.ts';
export { IsoDownloader } from './classes/iso-downloader.ts';
export { IsoExtractor } from './classes/iso-extractor.ts';
export { IsoPacker } from './classes/iso-packer.ts';
export { CloudInitGenerator } from './classes/cloud-init-generator.ts';
export { ConfigManager } from './classes/config-manager.ts';
// Export types
export type { IIsoConfig } from './interfaces/iso-config.interface.ts';
export type { ICloudInitConfig } from './interfaces/cloud-init-config.interface.ts';
// Export utilities
export * as paths from './paths.ts';
export { log, logger } from './logging.ts';

View File

@@ -0,0 +1,6 @@
/**
* Cloud-init configuration interface
* Re-export from iso-config for convenience
*/
export type { ICloudInitConfig } from './iso-config.interface.ts';

View File

@@ -0,0 +1,97 @@
/**
* Configuration interface for ISO customization
*/
export interface IIsoConfig {
version: string;
// Base ISO settings
iso: {
ubuntu_version: string; // e.g., "24.04", "22.04"
architecture: 'amd64' | 'arm64';
flavor?: 'server' | 'desktop'; // default: server
};
// Output settings
output: {
filename: string;
path: string;
};
// Network configuration
network?: {
wifi?: {
ssid: string;
password: string;
security?: 'wpa2' | 'wpa3';
hidden?: boolean;
};
};
// Cloud-init configuration
cloud_init?: ICloudInitConfig;
// Custom boot scripts
boot_scripts?: Array<{
name: string;
path: string;
enable?: boolean;
}>;
// Pre-install packages (requires longer build time)
preinstall?: {
enabled: boolean;
packages: string[];
};
}
/**
* Cloud-init configuration interface
*/
export interface ICloudInitConfig {
hostname?: string;
// Users
users?: Array<{
name: string;
ssh_authorized_keys?: string[];
sudo?: string;
shell?: string;
groups?: string[];
lock_passwd?: boolean;
passwd?: string; // Hashed password
}>;
// Packages to install on first boot
packages?: string[];
// Package update/upgrade
package_update?: boolean;
package_upgrade?: boolean;
// Run commands on first boot
runcmd?: string[];
// Write files
write_files?: Array<{
path: string;
content: string;
owner?: string;
permissions?: string;
}>;
// Timezone
timezone?: string;
// Locale
locale?: string;
// SSH settings
ssh?: {
password_authentication?: boolean;
permit_root_login?: boolean;
};
// Additional cloud-init directives
[key: string]: unknown;
}

29
ts/logging.ts Normal file
View File

@@ -0,0 +1,29 @@
/**
* Logging utilities for isocreator
*/
import { smartlog } from './plugins.ts';
// Create logger instance
export const logger = new smartlog.Smartlog({
logContext: {
company: 'Lossless GmbH',
companyunit: 'serve.zone',
containerName: 'isocreator',
environment: 'cli',
runtime: 'deno',
zone: 'local',
},
minimumLogLevel: 'info',
});
/**
* Log levels for convenience
*/
export const log = {
info: (message: string) => logger.log('info', message),
success: (message: string) => logger.log('info', `${message}`),
warn: (message: string) => logger.log('warn', `⚠️ ${message}`),
error: (message: string) => logger.log('error', `${message}`),
debug: (message: string) => logger.log('silly', message),
};

63
ts/paths.ts Normal file
View File

@@ -0,0 +1,63 @@
/**
* Path utilities for isocreator
*/
import { path } from './plugins.ts';
/**
* Get the user's home directory
*/
export function getHomeDir(): string {
return Deno.env.get('HOME') || Deno.env.get('USERPROFILE') || '/tmp';
}
/**
* Get the isocreator cache directory
*/
export function getCacheDir(): string {
const home = getHomeDir();
return path.join(home, '.isocreator', 'cache');
}
/**
* Get the isocreator config directory
*/
export function getConfigDir(): string {
const home = getHomeDir();
return path.join(home, '.isocreator', 'config');
}
/**
* Get the isocreator temp directory
*/
export function getTempDir(): string {
const home = getHomeDir();
return path.join(home, '.isocreator', 'temp');
}
/**
* Ensure a directory exists
*/
export async function ensureDir(dirPath: string): Promise<void> {
try {
await Deno.mkdir(dirPath, { recursive: true });
} catch (err) {
if (!(err instanceof Deno.errors.AlreadyExists)) {
throw err;
}
}
}
/**
* Clean a directory (remove and recreate)
*/
export async function cleanDir(dirPath: string): Promise<void> {
try {
await Deno.remove(dirPath, { recursive: true });
} catch (err) {
if (!(err instanceof Deno.errors.NotFound)) {
throw err;
}
}
await ensureDir(dirPath);
}

18
ts/plugins.ts Normal file
View File

@@ -0,0 +1,18 @@
/**
* Centralized dependency imports
* All external dependencies are imported here for easy management
*/
// Deno standard library
export * as path from '@std/path';
export * as fs from '@std/fs';
export * as yaml from '@std/yaml';
export * as assert from '@std/assert';
export * as fmt from '@std/fmt';
// Push.rocks ecosystem
export { smartcli } from '@push.rocks/smartcli';
export { smartlog } from '@push.rocks/smartlog';
export { smartfile } from '@push.rocks/smartfile';
export { Deferred } from '@push.rocks/smartpromise';
export { smartrequest } from '@push.rocks/smartrequest';