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,151 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Deno
uses: denoland/setup-deno@v1
with:
deno-version: v1.x
- name: Verify version matches tag
run: |
TAG_VERSION=${GITHUB_REF#refs/tags/v}
DENO_VERSION=$(grep '"version"' deno.json | head -1 | sed 's/.*"version": "\(.*\)".*/\1/')
echo "Git tag version: $TAG_VERSION"
echo "deno.json version: $DENO_VERSION"
if [ "$TAG_VERSION" != "$DENO_VERSION" ]; then
echo "❌ Version mismatch!"
echo "Git tag: v$TAG_VERSION"
echo "deno.json: $DENO_VERSION"
exit 1
fi
echo "✅ Versions match: $TAG_VERSION"
- name: Cache dependencies
uses: actions/cache@v4
with:
path: ~/.cache/deno
key: deno-${{ hashFiles('deno.lock') }}
- name: Install dependencies
run: deno cache --reload mod.ts
- name: Run tests
run: deno task test
- name: Compile binaries
run: |
bash scripts/compile-all.sh
- name: Generate checksums
run: |
cd dist/binaries
sha256sum * > checksums.txt
cat checksums.txt
- name: Extract changelog
id: changelog
run: |
TAG_VERSION=${GITHUB_REF#refs/tags/v}
# Extract changelog section for this version
CHANGELOG=$(awk "/## \[$TAG_VERSION\]/,/## \[/" changelog.md | sed '1d;$d')
if [ -z "$CHANGELOG" ]; then
echo "No changelog entry found for version $TAG_VERSION"
CHANGELOG="Release version $TAG_VERSION"
fi
# Save to file for release notes
echo "$CHANGELOG" > /tmp/release-notes.md
cat /tmp/release-notes.md
- name: Create Release
uses: actions/create-release@v1
id: create_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref_name }}
body_path: /tmp/release-notes.md
draft: false
prerelease: false
- name: Upload Linux x64 Binary
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dist/binaries/isocreator-linux-x64
asset_name: isocreator-linux-x64
asset_content_type: application/octet-stream
- name: Upload Linux ARM64 Binary
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dist/binaries/isocreator-linux-arm64
asset_name: isocreator-linux-arm64
asset_content_type: application/octet-stream
- name: Upload macOS x64 Binary
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dist/binaries/isocreator-macos-x64
asset_name: isocreator-macos-x64
asset_content_type: application/octet-stream
- name: Upload macOS ARM64 Binary
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dist/binaries/isocreator-macos-arm64
asset_name: isocreator-macos-arm64
asset_content_type: application/octet-stream
- name: Upload Windows x64 Binary
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dist/binaries/isocreator-windows-x64.exe
asset_name: isocreator-windows-x64.exe
asset_content_type: application/octet-stream
- name: Upload Checksums
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dist/binaries/checksums.txt
asset_name: checksums.txt
asset_content_type: text/plain
- name: Clean old releases
run: |
echo "Keeping only the last 3 releases..."
# This would require gitea API calls - implement if needed

52
.gitignore vendored Normal file
View File

@@ -0,0 +1,52 @@
# Deno
.deno/
deno.lock
# Node
node_modules/
package-lock.json
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Build artifacts
dist/
*.exe
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Cache
.cache/
# Temporary files
tmp/
temp/
*.tmp
# Test artifacts
coverage/
.nyc_output/
# Logs
*.log
logs/
# Environment
.env
.env.local
# Output
output/
*.iso
# User data
.nogit/

77
bin/isocreator-wrapper.js Executable file
View File

@@ -0,0 +1,77 @@
#!/usr/bin/env node
/**
* isocreator binary wrapper
* Spawns the appropriate pre-compiled binary for the current platform
*/
import { spawn } from 'child_process';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { platform, arch as osArch } from 'os';
import { existsSync } from 'fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Map Node.js platform names to our binary names
const platformMap = {
darwin: 'macos',
linux: 'linux',
win32: 'windows',
};
// Map Node.js architecture names
const archMap = {
x64: 'x64',
arm64: 'arm64',
};
const detectedPlatform = platformMap[platform()];
const detectedArch = archMap[osArch()];
if (!detectedPlatform || !detectedArch) {
console.error(`❌ Unsupported platform: ${platform()}-${osArch()}`);
console.error('Supported platforms: linux-x64, linux-arm64, darwin-x64, darwin-arm64, win32-x64');
process.exit(1);
}
const ext = detectedPlatform === 'windows' ? '.exe' : '';
const binaryName = `isocreator-${detectedPlatform}-${detectedArch}${ext}`;
const binaryPath = join(__dirname, '..', 'dist', 'binaries', binaryName);
// Check if binary exists
if (!existsSync(binaryPath)) {
console.error(`❌ Binary not found: ${binaryPath}`);
console.error('\nThis might be due to a failed installation.');
console.error('Try reinstalling:');
console.error(' npm install -g @serve.zone/isocreator');
console.error('\nOr use the direct installer:');
console.error(' curl -sSL https://code.foss.global/serve.zone/isocreator/raw/branch/main/install.sh | sudo bash');
process.exit(1);
}
// Spawn the binary with all arguments
const child = spawn(binaryPath, process.argv.slice(2), {
stdio: 'inherit',
windowsHide: false,
});
// Forward signals
process.on('SIGINT', () => child.kill('SIGINT'));
process.on('SIGTERM', () => child.kill('SIGTERM'));
process.on('SIGHUP', () => child.kill('SIGHUP'));
// Exit with the same code as the binary
child.on('exit', (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
} else {
process.exit(code || 0);
}
});
child.on('error', (err) => {
console.error('❌ Failed to start isocreator:', err.message);
process.exit(1);
});

39
changelog.md Normal file
View File

@@ -0,0 +1,39 @@
# Changelog
## 2025-10-24 - 1.1.0 - feat(core)
Initial project scaffold and implementation: Deno CLI, ISO tooling, cloud-init generation, packaging and installer scripts
- Add Deno-based CLI (commands: build, cache, template, validate) and entry point (mod.ts)
- Implement ISO management classes: iso-downloader, iso-cache, iso-extractor, iso-packer, iso-builder
- Add cloud-init generator and configuration manager with YAML template and validation
- Add packaging/distribution tooling: npm wrapper, postinstall installer, binary wrapper, compile script, install/uninstall scripts
- Add CI release workflow, changelog, README, license, and configuration files (deno.json, package.json, templates)
All notable changes to isocreator will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- Initial project structure
- Deno-based CLI framework
- Binary compilation for multiple platforms
- npm distribution with postinstall binary download
- Direct installation script
## [1.0.0] - TBD
### Added
- Ubuntu ISO customization for PC (x86_64) and Raspberry Pi (ARM64)
- Cloud-init configuration support
- WiFi pre-configuration via cloud-init network-config
- User account and SSH key injection
- Package installation via cloud-init
- Custom boot script support
- ISO caching system with multi-version support
- Interactive mode for guided setup
- Config file support (YAML)
- CLI flag support for quick customization
- Template generation for configuration files

46
deno.json Normal file
View File

@@ -0,0 +1,46 @@
{
"name": "@serve.zone/isocreator",
"version": "1.0.0",
"exports": "./mod.ts",
"tasks": {
"dev": "deno run --allow-all mod.ts",
"compile": "bash scripts/compile-all.sh",
"test": "deno test --allow-all",
"test:watch": "deno test --allow-all --watch",
"check": "deno check mod.ts",
"fmt": "deno fmt",
"fmt:check": "deno fmt --check",
"lint": "deno lint",
"cache": "deno cache --reload mod.ts"
},
"imports": {
"@std/path": "jsr:@std/path@^1.0.0",
"@std/fs": "jsr:@std/fs@^1.0.0",
"@std/yaml": "jsr:@std/yaml@^1.0.0",
"@std/assert": "jsr:@std/assert@^1.0.0",
"@std/fmt": "jsr:@std/fmt@^1.0.0",
"@push.rocks/smartcli": "npm:@push.rocks/smartcli@^5.0.0",
"@push.rocks/smartlog": "npm:@push.rocks/smartlog@^3.0.0",
"@push.rocks/smartfile": "npm:@push.rocks/smartfile@^11.0.0",
"@push.rocks/smartpromise": "npm:@push.rocks/smartpromise@^4.0.0",
"@push.rocks/smartrequest": "npm:@push.rocks/smartrequest@^2.0.0"
},
"compilerOptions": {
"lib": ["deno.window"],
"strict": true,
"allowJs": false
},
"fmt": {
"useTabs": false,
"lineWidth": 100,
"indentWidth": 2,
"semiColons": true,
"singleQuote": true,
"proseWrap": "preserve"
},
"lint": {
"rules": {
"tags": ["recommended"]
}
}
}

144
install.sh Executable file
View File

@@ -0,0 +1,144 @@
#!/usr/bin/env bash
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
REPO_URL="https://code.foss.global/serve.zone/isocreator"
INSTALL_DIR="/opt/isocreator"
BIN_LINK="/usr/local/bin/isocreator"
VERSION="latest"
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--version)
VERSION="$2"
shift 2
;;
--install-dir)
INSTALL_DIR="$2"
shift 2
;;
-h|--help)
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " --version VERSION Install specific version (default: latest)"
echo " --install-dir DIR Installation directory (default: /opt/isocreator)"
echo " -h, --help Show this help message"
exit 0
;;
*)
echo -e "${RED}Unknown option: $1${NC}"
exit 1
;;
esac
done
echo -e "${BLUE}🚀 isocreator installer${NC}"
echo ""
# Detect platform
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
case "$OS" in
linux)
PLATFORM="linux"
;;
darwin)
PLATFORM="macos"
;;
*)
echo -e "${RED}❌ Unsupported operating system: $OS${NC}"
exit 1
;;
esac
case "$ARCH" in
x86_64|amd64)
ARCH="x64"
;;
aarch64|arm64)
ARCH="arm64"
;;
*)
echo -e "${RED}❌ Unsupported architecture: $ARCH${NC}"
exit 1
;;
esac
BINARY_NAME="isocreator-${PLATFORM}-${ARCH}"
echo -e "${BLUE}📋 Detected platform: ${PLATFORM}-${ARCH}${NC}"
# Get version
if [ "$VERSION" == "latest" ]; then
echo -e "${BLUE}🔍 Fetching latest version...${NC}"
VERSION=$(curl -sSL "${REPO_URL}/raw/branch/main/deno.json" | grep -o '"version": "[^"]*' | cut -d'"' -f4)
if [ -z "$VERSION" ]; then
echo -e "${YELLOW}⚠️ Could not fetch latest version, using fallback${NC}"
VERSION="1.0.0"
fi
fi
echo -e "${GREEN}📦 Version: v${VERSION}${NC}"
# Download URL (try release first, fallback to raw branch)
RELEASE_URL="${REPO_URL}/releases/download/v${VERSION}/${BINARY_NAME}"
FALLBACK_URL="${REPO_URL}/raw/branch/main/dist/binaries/${BINARY_NAME}"
echo -e "${BLUE}⬇️ Downloading binary...${NC}"
# Try release URL first
if curl -fsSL -o "/tmp/${BINARY_NAME}" "$RELEASE_URL" 2>/dev/null; then
echo -e "${GREEN}✅ Downloaded from release${NC}"
else
echo -e "${YELLOW}⚠️ Release not found, trying fallback...${NC}"
if curl -fsSL -o "/tmp/${BINARY_NAME}" "$FALLBACK_URL"; then
echo -e "${GREEN}✅ Downloaded from fallback${NC}"
else
echo -e "${RED}❌ Failed to download binary${NC}"
exit 1
fi
fi
# Create installation directory
echo -e "${BLUE}📁 Creating installation directory...${NC}"
mkdir -p "$INSTALL_DIR"
# Move binary to installation directory
echo -e "${BLUE}📦 Installing binary...${NC}"
mv "/tmp/${BINARY_NAME}" "${INSTALL_DIR}/isocreator"
chmod +x "${INSTALL_DIR}/isocreator"
# Create symlink
echo -e "${BLUE}🔗 Creating symlink...${NC}"
if [ -L "$BIN_LINK" ]; then
rm "$BIN_LINK"
fi
ln -s "${INSTALL_DIR}/isocreator" "$BIN_LINK"
echo ""
echo -e "${GREEN}✅ isocreator installed successfully!${NC}"
echo ""
echo -e "${BLUE}📍 Installation location: ${INSTALL_DIR}/isocreator${NC}"
echo -e "${BLUE}🔗 Symlink created: ${BIN_LINK}${NC}"
echo ""
echo -e "${GREEN}💡 Try running: isocreator --help${NC}"
echo ""
echo -e "${YELLOW}⚠️ Note: isocreator requires the following system tools:${NC}"
echo -e " - xorriso (for ISO manipulation)"
echo -e " - isohybrid (for USB-bootable ISOs)"
echo -e ""
echo -e "${BLUE}Install on Ubuntu/Debian:${NC}"
echo -e " sudo apt install xorriso syslinux-utils"
echo -e ""
echo -e "${BLUE}Install on macOS:${NC}"
echo -e " brew install xorriso syslinux"

21
license Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Lossless GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

21
mod.ts Normal file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env -S deno run --allow-all
/**
* isocreator - Ubuntu ISO customization tool
*
* Creates customized Ubuntu Server ISOs for PC and Raspberry Pi with:
* - Pre-configured WiFi (via cloud-init)
* - User accounts and SSH keys
* - Custom packages and boot scripts
* - Full cloud-init configuration
*/
import { startCli } from './ts/cli.ts';
// Start the CLI
if (import.meta.main) {
await startCli();
}
// Export for library usage
export * from './ts/index.ts';

1
npmextra.json Normal file
View File

@@ -0,0 +1 @@
{}

53
package.json Normal file
View File

@@ -0,0 +1,53 @@
{
"name": "@serve.zone/isocreator",
"version": "1.0.0",
"description": "Ubuntu ISO customization tool for PC and Raspberry Pi with WiFi and cloud-init configuration",
"type": "module",
"bin": {
"isocreator": "./bin/isocreator-wrapper.js"
},
"scripts": {
"postinstall": "node scripts/install-binary.js",
"prepublishOnly": "echo 'Preparing to publish isocreator'",
"build": "echo 'No build needed - binaries pre-compiled'"
},
"keywords": [
"ubuntu",
"iso",
"raspberry-pi",
"cloud-init",
"wifi",
"automation",
"linux",
"customization"
],
"author": "Lossless GmbH",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://code.foss.global/serve.zone/isocreator.git"
},
"bugs": {
"url": "https://code.foss.global/serve.zone/isocreator/issues"
},
"homepage": "https://code.foss.global/serve.zone/isocreator",
"engines": {
"node": ">=14.0.0"
},
"os": [
"darwin",
"linux",
"win32"
],
"cpu": [
"x64",
"arm64"
],
"files": [
"bin/",
"scripts/install-binary.js",
"readme.md",
"license",
"changelog.md"
]
}

72
readme.hints.md Normal file
View File

@@ -0,0 +1,72 @@
# Development Hints
## Project Structure
This project follows the nupst/spark pattern for Deno-based CLI tools with binary distribution.
## Key Components
1. **ISO Management** (`ts/classes/`)
- `iso-downloader.ts` - Downloads Ubuntu ISOs from official mirrors
- `iso-cache.ts` - Manages cached ISOs with multi-version support
- `iso-extractor.ts` - Extracts ISOs using xorriso
- `iso-packer.ts` - Repacks modified ISOs into bootable images
- `iso-builder.ts` - Orchestrates the entire build process
2. **Cloud-Init** (`ts/classes/`)
- `cloud-init-generator.ts` - Generates cloud-init configuration files
- `config-manager.ts` - Loads and validates YAML configs
3. **CLI** (`ts/cli.ts`)
- Command-line interface with commands: build, cache, template, validate
## System Dependencies
The tool requires these system tools at runtime:
- `xorriso` - ISO manipulation
- `syslinux-utils` - For USB-bootable ISOs (isohybrid command)
## Development Commands
```bash
# Install dependencies
deno install
# Run in development mode
deno task dev --help
# Type checking
deno task check
# Format code
deno task fmt
# Lint
deno task lint
# Run tests
deno task test
# Compile binaries
deno task compile
```
## Distribution
Binaries are compiled for 5 platforms:
- Linux x64
- Linux ARM64
- macOS x64
- macOS ARM64
- Windows x64
## Next Steps
1. Implement CLI flag-based building (currently only config files work)
2. Add interactive mode with prompts
3. Implement boot scripts injection with systemd service creation
4. Add pre-install packages feature (requires squashfs manipulation)
5. Write comprehensive tests
6. Compile and test binaries
7. Set up CI/CD pipeline
8. Publish to npm

312
readme.md Normal file
View File

@@ -0,0 +1,312 @@
# isocreator
> Ubuntu ISO customization tool for PC and Raspberry Pi with WiFi and cloud-init configuration
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
## Overview
**isocreator** is a command-line tool that creates customized Ubuntu Server ISOs with pre-configured:
- 📡 **WiFi credentials** (via cloud-init)
- 👤 **User accounts** and SSH keys
- 📦 **Pre-installed packages**
- 🔧 **Custom boot scripts**
- ⚙️ **Full cloud-init configuration**
Perfect for:
- Raspberry Pi deployments
- Headless server installations
- Automated fleet provisioning
- Development environments
## Features
- **Multi-Platform Support**: PC (x86_64) and Raspberry Pi (ARM64)
- **Multi-Version Support**: Ubuntu 22.04 LTS, 24.04 LTS, and future versions
- **Cloud-Init Integration**: Full cloud-init user-data and network-config support
- **Caching System**: Intelligent ISO caching with multi-version support
- **Flexible Configuration**: YAML files, CLI flags, or interactive mode
- **USB Bootable**: Creates ISOs that can be written directly to USB drives
## Installation
### via npm
```bash
npm install -g @serve.zone/isocreator
```
### via Direct Script
```bash
curl -sSL https://code.foss.global/serve.zone/isocreator/raw/branch/main/install.sh | sudo bash
```
### System Dependencies
isocreator requires the following tools to be installed:
**Ubuntu/Debian:**
```bash
sudo apt install xorriso syslinux-utils
```
**macOS:**
```bash
brew install xorriso syslinux
```
## Quick Start
### Interactive Mode
```bash
isocreator build
```
### Using a Config File
```bash
# Generate a template
isocreator template create --output myconfig.yaml
# Edit the config file
nano myconfig.yaml
# Build the ISO
isocreator build --config myconfig.yaml
```
### Using CLI Flags
```bash
isocreator build \
--ubuntu-version 24.04 \
--arch amd64 \
--wifi-ssid "MyWiFi" \
--wifi-password "secret123" \
--ssh-key ~/.ssh/id_rsa.pub \
--hostname "myserver" \
--output ./custom-ubuntu.iso
```
## Configuration
### Example Config File
```yaml
version: "1.0"
# Base ISO settings
iso:
ubuntu_version: "24.04"
architecture: "amd64" # or arm64 for Raspberry Pi
# Output settings
output:
filename: "ubuntu-custom.iso"
path: "./output"
# WiFi configuration (via cloud-init)
network:
wifi:
ssid: "MyWiFi"
password: "secret123"
# Cloud-init configuration
cloud_init:
hostname: "myserver"
# User accounts
users:
- name: "admin"
ssh_authorized_keys:
- "ssh-rsa AAAAB3NzaC1yc2E..."
sudo: "ALL=(ALL) NOPASSWD:ALL"
shell: "/bin/bash"
# Packages to install on first boot
packages:
- docker.io
- git
- htop
# Commands to run on first boot
runcmd:
- systemctl enable docker
- systemctl start docker
- echo "Setup complete!"
# Custom boot scripts
boot_scripts:
- name: "setup-docker"
path: "./scripts/setup-docker.sh"
```
## Commands
### Build
Build a customized ISO:
```bash
isocreator build [OPTIONS]
```
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
- `--ssh-key <path>` - Path to SSH public key
- `--hostname <name>` - System hostname
- `--output <path>` - Output ISO path
### Cache Management
List cached ISOs:
```bash
isocreator cache list
```
Download an ISO to cache:
```bash
isocreator cache download 24.04 --arch amd64
```
Clean old cached ISOs:
```bash
isocreator cache clean
isocreator cache clean --older-than 6m
```
### Templates
Generate a config template:
```bash
isocreator template create
isocreator template create --output myconfig.yaml
```
### Validation
Validate a config file:
```bash
isocreator validate myconfig.yaml
```
## Use Cases
### Raspberry Pi Home Server
```yaml
iso:
ubuntu_version: "24.04"
architecture: "arm64"
network:
wifi:
ssid: "HomeNetwork"
password: "homepass123"
cloud_init:
hostname: "pi-server"
users:
- name: "pi"
ssh_authorized_keys:
- "ssh-rsa AAAAB3..."
sudo: "ALL=(ALL) NOPASSWD:ALL"
packages:
- docker.io
- docker-compose
runcmd:
- systemctl enable docker
```
### Headless Development Server
```yaml
iso:
ubuntu_version: "24.04"
architecture: "amd64"
cloud_init:
hostname: "devbox"
users:
- name: "developer"
ssh_authorized_keys:
- "ssh-rsa AAAAB3..."
sudo: "ALL=(ALL) NOPASSWD:ALL"
packages:
- build-essential
- git
- docker.io
- nodejs
- npm
```
## Architecture
isocreator is built with:
- **Deno** - Modern TypeScript runtime
- **Cloud-init** - Industry-standard cloud instance initialization
- **xorriso** - ISO 9660 filesystem manipulation
- **Self-contained binaries** - Zero runtime dependencies
## Development
### Prerequisites
- Deno 1.40+
- xorriso
- syslinux-utils (for isohybrid)
### Setup
```bash
git clone https://code.foss.global/serve.zone/isocreator.git
cd isocreator
deno task cache
```
### Development Mode
```bash
deno task dev --help
```
### Testing
```bash
deno task test
```
### Build Binaries
```bash
deno task compile
```
## Contributing
Contributions are welcome! Please feel free to submit issues and pull requests.
## License
MIT License - see [license](./license) file for details
## Support
- **Issues**: https://code.foss.global/serve.zone/isocreator/issues
- **Documentation**: https://code.foss.global/serve.zone/isocreator
---
Made with ❤️ by [Lossless GmbH](https://lossless.com)

33
scripts/compile-all.sh Executable file
View File

@@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -e
echo "🔨 Compiling isocreator binaries for all platforms..."
# Create dist directory if it doesn't exist
mkdir -p dist/binaries
# Linux x64
echo "📦 Compiling for Linux x64..."
deno compile --allow-all --no-check --target x86_64-unknown-linux-gnu --output dist/binaries/isocreator-linux-x64 mod.ts
# Linux ARM64
echo "📦 Compiling for Linux ARM64..."
deno compile --allow-all --no-check --target aarch64-unknown-linux-gnu --output dist/binaries/isocreator-linux-arm64 mod.ts
# macOS x64
echo "📦 Compiling for macOS x64..."
deno compile --allow-all --no-check --target x86_64-apple-darwin --output dist/binaries/isocreator-macos-x64 mod.ts
# macOS ARM64
echo "📦 Compiling for macOS ARM64..."
deno compile --allow-all --no-check --target aarch64-apple-darwin --output dist/binaries/isocreator-macos-arm64 mod.ts
# Windows x64
echo "📦 Compiling for Windows x64..."
deno compile --allow-all --no-check --target x86_64-pc-windows-msvc --output dist/binaries/isocreator-windows-x64.exe mod.ts
echo "✅ All binaries compiled successfully!"
echo ""
echo "Binaries location: dist/binaries/"
ls -lh dist/binaries/

144
scripts/install-binary.js Normal file
View File

@@ -0,0 +1,144 @@
#!/usr/bin/env node
/**
* npm postinstall script
* Downloads the correct binary for the current platform
*/
import { createWriteStream, chmodSync, existsSync, mkdirSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { get as httpsGet } from 'https';
import { get as httpGet } from 'http';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const rootDir = join(__dirname, '..');
const REPO_URL = 'https://code.foss.global/serve.zone/isocreator';
const VERSION = '1.0.0'; // Will be updated automatically
// Detect platform and architecture
const platformMap = {
darwin: 'macos',
linux: 'linux',
win32: 'windows',
};
const archMap = {
x64: 'x64',
arm64: 'arm64',
};
const platform = platformMap[process.platform];
const arch = archMap[process.arch];
if (!platform || !arch) {
console.error(`❌ Unsupported platform: ${process.platform}-${process.arch}`);
console.error('Supported platforms: linux-x64, linux-arm64, darwin-x64, darwin-arm64, win32-x64');
process.exit(1);
}
const ext = platform === 'windows' ? '.exe' : '';
const binaryName = `isocreator-${platform}-${arch}${ext}`;
const binaryDir = join(rootDir, 'dist', 'binaries');
const binaryPath = join(binaryDir, binaryName);
// Create directory if it doesn't exist
if (!existsSync(binaryDir)) {
mkdirSync(binaryDir, { recursive: true });
}
// Try release URL first, fallback to raw branch
const urls = [
`${REPO_URL}/releases/download/v${VERSION}/${binaryName}`,
`${REPO_URL}/raw/branch/main/dist/binaries/${binaryName}`,
];
console.log(`📦 Installing isocreator for ${platform}-${arch}...`);
function downloadFile(url, attempt = 1) {
return new Promise((resolve, reject) => {
const protocol = url.startsWith('https') ? httpsGet : httpGet;
console.log(`⬇️ Downloading from: ${url}`);
protocol(url, { timeout: 30000 }, (response) => {
// Handle redirects
if (response.statusCode === 301 || response.statusCode === 302) {
const redirectUrl = response.headers.location;
console.log(`↪️ Redirecting to: ${redirectUrl}`);
return downloadFile(redirectUrl, attempt).then(resolve).catch(reject);
}
if (response.statusCode !== 200) {
console.warn(`⚠️ Failed to download (HTTP ${response.statusCode})`);
if (attempt < urls.length) {
console.log(`🔄 Trying fallback URL (${attempt + 1}/${urls.length})...`);
return downloadFile(urls[attempt], attempt + 1).then(resolve).catch(reject);
}
return reject(new Error(`Failed to download binary from all sources`));
}
const file = createWriteStream(binaryPath);
const totalBytes = parseInt(response.headers['content-length'], 10);
let downloadedBytes = 0;
response.on('data', (chunk) => {
downloadedBytes += chunk.length;
if (totalBytes) {
const percent = ((downloadedBytes / totalBytes) * 100).toFixed(1);
process.stdout.write(`\r📥 Progress: ${percent}%`);
}
});
response.pipe(file);
file.on('finish', () => {
file.close();
console.log('\n✅ Download complete!');
// Make executable on Unix-like systems
if (platform !== 'windows') {
try {
chmodSync(binaryPath, 0o755);
console.log('✅ Binary made executable');
} catch (err) {
console.warn('⚠️ Failed to make binary executable:', err.message);
}
}
resolve();
});
file.on('error', (err) => {
file.close();
reject(err);
});
}).on('error', (err) => {
if (attempt < urls.length) {
console.warn(`⚠️ Download failed:`, err.message);
console.log(`🔄 Trying fallback URL (${attempt + 1}/${urls.length})...`);
downloadFile(urls[attempt], attempt + 1).then(resolve).catch(reject);
} else {
reject(err);
}
});
});
}
// Start download
downloadFile(urls[0], 1)
.then(() => {
console.log('🎉 isocreator installed successfully!');
console.log(`\n💡 Try running: isocreator --help`);
process.exit(0);
})
.catch((err) => {
console.error('\n❌ Installation failed:', err.message);
console.error('\nYou can try manual installation:');
console.error(` curl -sSL ${REPO_URL}/raw/branch/main/install.sh | sudo bash`);
process.exit(1);
});

View File

@@ -0,0 +1,90 @@
# isocreator configuration file
# Version: 1.0
version: "1.0"
# Base ISO settings
iso:
ubuntu_version: "24.04" # Ubuntu version (22.04, 24.04, etc.)
architecture: "amd64" # amd64 for PC, arm64 for Raspberry Pi
flavor: "server" # server or desktop (default: server)
# Output settings
output:
filename: "ubuntu-custom.iso"
path: "./output"
# Network configuration
network:
wifi:
ssid: "MyWiFi"
password: "changeme"
# Optional advanced settings:
# security: "wpa2"
# hidden: false
# Cloud-init configuration
cloud_init:
# System hostname
hostname: "ubuntu-server"
# User accounts
users:
- name: "admin"
ssh_authorized_keys:
- "ssh-rsa AAAAB3... your-key-here"
sudo: "ALL=(ALL) NOPASSWD:ALL"
shell: "/bin/bash"
# Optional:
# groups: ["docker", "sudo"]
# lock_passwd: true
# Packages to install on first boot
packages:
- docker.io
- git
- htop
- curl
- wget
# Package management
package_update: true
package_upgrade: false
# Commands to run on first boot
runcmd:
- systemctl enable docker
- systemctl start docker
- echo "Setup complete!" > /tmp/setup-done
# Write files
# write_files:
# - path: /etc/motd
# content: |
# Welcome to Ubuntu Custom ISO!
# owner: root:root
# permissions: '0644'
# Timezone
# timezone: "UTC"
# Locale
# locale: "en_US.UTF-8"
# SSH settings
# ssh:
# password_authentication: false
# permit_root_login: false
# Custom boot scripts (advanced)
# boot_scripts:
# - name: "setup-docker"
# path: "./scripts/setup-docker.sh"
# enable: true
# Pre-install packages (requires longer build time)
# preinstall:
# enabled: false
# packages:
# - vim
# - curl

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

58
uninstall.sh Executable file
View File

@@ -0,0 +1,58 @@
#!/usr/bin/env bash
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
INSTALL_DIR="/opt/isocreator"
BIN_LINK="/usr/local/bin/isocreator"
CACHE_DIR="$HOME/.isocreator"
echo -e "${BLUE}🗑️ isocreator uninstaller${NC}"
echo ""
# Remove binary
if [ -f "${INSTALL_DIR}/isocreator" ]; then
echo -e "${BLUE}📦 Removing binary from ${INSTALL_DIR}...${NC}"
rm -f "${INSTALL_DIR}/isocreator"
# Remove directory if empty
if [ -d "$INSTALL_DIR" ] && [ -z "$(ls -A $INSTALL_DIR)" ]; then
rmdir "$INSTALL_DIR"
fi
echo -e "${GREEN}✅ Binary removed${NC}"
else
echo -e "${YELLOW}⚠️ Binary not found at ${INSTALL_DIR}${NC}"
fi
# Remove symlink
if [ -L "$BIN_LINK" ]; then
echo -e "${BLUE}🔗 Removing symlink ${BIN_LINK}...${NC}"
rm -f "$BIN_LINK"
echo -e "${GREEN}✅ Symlink removed${NC}"
else
echo -e "${YELLOW}⚠️ Symlink not found at ${BIN_LINK}${NC}"
fi
# Ask about cache
if [ -d "$CACHE_DIR" ]; then
echo ""
echo -e "${YELLOW}Cache directory found: ${CACHE_DIR}${NC}"
read -p "Do you want to remove cached ISOs? (y/N) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo -e "${BLUE}🗑️ Removing cache directory...${NC}"
rm -rf "$CACHE_DIR"
echo -e "${GREEN}✅ Cache removed${NC}"
else
echo -e "${BLUE} Cache directory kept${NC}"
fi
fi
echo ""
echo -e "${GREEN}✅ isocreator uninstalled successfully!${NC}"