feat(core): Initial project scaffold and implementation: Deno CLI, ISO tooling, cloud-init generation, packaging and installer scripts
This commit is contained in:
8
ts/00_commitinfo_data.ts
Normal file
8
ts/00_commitinfo_data.ts
Normal 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'
|
||||
}
|
||||
168
ts/classes/cloud-init-generator.ts
Normal file
168
ts/classes/cloud-init-generator.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
133
ts/classes/config-manager.ts
Normal file
133
ts/classes/config-manager.ts
Normal 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
153
ts/classes/iso-builder.ts
Normal 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
212
ts/classes/iso-cache.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
172
ts/classes/iso-downloader.ts
Normal file
172
ts/classes/iso-downloader.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
82
ts/classes/iso-extractor.ts
Normal file
82
ts/classes/iso-extractor.ts
Normal 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
123
ts/classes/iso-packer.ts
Normal 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
275
ts/cli.ts
Normal 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
20
ts/index.ts
Normal 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';
|
||||
6
ts/interfaces/cloud-init-config.interface.ts
Normal file
6
ts/interfaces/cloud-init-config.interface.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Cloud-init configuration interface
|
||||
* Re-export from iso-config for convenience
|
||||
*/
|
||||
|
||||
export type { ICloudInitConfig } from './iso-config.interface.ts';
|
||||
97
ts/interfaces/iso-config.interface.ts
Normal file
97
ts/interfaces/iso-config.interface.ts
Normal 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
29
ts/logging.ts
Normal 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
63
ts/paths.ts
Normal 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
18
ts/plugins.ts
Normal 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';
|
||||
Reference in New Issue
Block a user