From ce06b5855aa531ed04774268944855ef519fab7e Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Fri, 24 Oct 2025 08:10:02 +0000 Subject: [PATCH] feat(core): Initial project scaffold and implementation: Deno CLI, ISO tooling, cloud-init generation, packaging and installer scripts --- .gitea/workflows/release.yml | 151 +++++++++ .gitignore | 52 ++++ bin/isocreator-wrapper.js | 77 +++++ changelog.md | 39 +++ deno.json | 46 +++ install.sh | 144 +++++++++ license | 21 ++ mod.ts | 21 ++ npmextra.json | 1 + package.json | 53 ++++ readme.hints.md | 72 +++++ readme.md | 312 +++++++++++++++++++ scripts/compile-all.sh | 33 ++ scripts/install-binary.js | 144 +++++++++ templates/config.template.yaml | 90 ++++++ ts/00_commitinfo_data.ts | 8 + ts/classes/cloud-init-generator.ts | 168 ++++++++++ ts/classes/config-manager.ts | 133 ++++++++ ts/classes/iso-builder.ts | 153 +++++++++ ts/classes/iso-cache.ts | 212 +++++++++++++ ts/classes/iso-downloader.ts | 172 ++++++++++ ts/classes/iso-extractor.ts | 82 +++++ ts/classes/iso-packer.ts | 123 ++++++++ ts/cli.ts | 275 ++++++++++++++++ ts/index.ts | 20 ++ ts/interfaces/cloud-init-config.interface.ts | 6 + ts/interfaces/iso-config.interface.ts | 97 ++++++ ts/logging.ts | 29 ++ ts/paths.ts | 63 ++++ ts/plugins.ts | 18 ++ uninstall.sh | 58 ++++ 31 files changed, 2873 insertions(+) create mode 100644 .gitea/workflows/release.yml create mode 100644 .gitignore create mode 100755 bin/isocreator-wrapper.js create mode 100644 changelog.md create mode 100644 deno.json create mode 100755 install.sh create mode 100644 license create mode 100644 mod.ts create mode 100644 npmextra.json create mode 100644 package.json create mode 100644 readme.hints.md create mode 100644 readme.md create mode 100755 scripts/compile-all.sh create mode 100644 scripts/install-binary.js create mode 100644 templates/config.template.yaml create mode 100644 ts/00_commitinfo_data.ts create mode 100644 ts/classes/cloud-init-generator.ts create mode 100644 ts/classes/config-manager.ts create mode 100644 ts/classes/iso-builder.ts create mode 100644 ts/classes/iso-cache.ts create mode 100644 ts/classes/iso-downloader.ts create mode 100644 ts/classes/iso-extractor.ts create mode 100644 ts/classes/iso-packer.ts create mode 100644 ts/cli.ts create mode 100644 ts/index.ts create mode 100644 ts/interfaces/cloud-init-config.interface.ts create mode 100644 ts/interfaces/iso-config.interface.ts create mode 100644 ts/logging.ts create mode 100644 ts/paths.ts create mode 100644 ts/plugins.ts create mode 100755 uninstall.sh diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..0b5c371 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b977bc --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/bin/isocreator-wrapper.js b/bin/isocreator-wrapper.js new file mode 100755 index 0000000..4e2d1c7 --- /dev/null +++ b/bin/isocreator-wrapper.js @@ -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); +}); diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..12a86c0 --- /dev/null +++ b/changelog.md @@ -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 diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..e2a83dc --- /dev/null +++ b/deno.json @@ -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"] + } + } +} diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..be6c886 --- /dev/null +++ b/install.sh @@ -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" diff --git a/license b/license new file mode 100644 index 0000000..376bc73 --- /dev/null +++ b/license @@ -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. diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..94eac05 --- /dev/null +++ b/mod.ts @@ -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'; diff --git a/npmextra.json b/npmextra.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/npmextra.json @@ -0,0 +1 @@ +{} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3789708 --- /dev/null +++ b/package.json @@ -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" + ] +} diff --git a/readme.hints.md b/readme.hints.md new file mode 100644 index 0000000..3c5366d --- /dev/null +++ b/readme.hints.md @@ -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 diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..0e25447 --- /dev/null +++ b/readme.md @@ -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 ` - Use a YAML config file +- `--ubuntu-version ` - Ubuntu version (22.04, 24.04, etc.) +- `--arch ` - Architecture (amd64 or arm64) +- `--wifi-ssid ` - WiFi SSID +- `--wifi-password ` - WiFi password +- `--ssh-key ` - Path to SSH public key +- `--hostname ` - System hostname +- `--output ` - 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) diff --git a/scripts/compile-all.sh b/scripts/compile-all.sh new file mode 100755 index 0000000..0708e0d --- /dev/null +++ b/scripts/compile-all.sh @@ -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/ diff --git a/scripts/install-binary.js b/scripts/install-binary.js new file mode 100644 index 0000000..7e47d5f --- /dev/null +++ b/scripts/install-binary.js @@ -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); + }); diff --git a/templates/config.template.yaml b/templates/config.template.yaml new file mode 100644 index 0000000..6bafd01 --- /dev/null +++ b/templates/config.template.yaml @@ -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 diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts new file mode 100644 index 0000000..fef7847 --- /dev/null +++ b/ts/00_commitinfo_data.ts @@ -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' +} diff --git a/ts/classes/cloud-init-generator.ts b/ts/classes/cloud-init-generator.ts new file mode 100644 index 0000000..ce2abff --- /dev/null +++ b/ts/classes/cloud-init-generator.ts @@ -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 = { + '#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 { + 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'); + } +} diff --git a/ts/classes/config-manager.ts b/ts/classes/config-manager.ts new file mode 100644 index 0000000..652c9cd --- /dev/null +++ b/ts/classes/config-manager.ts @@ -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 { + 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 { + const template = this.generateTemplate(); + await Deno.writeTextFile(filePath, template); + log.success(`Template saved to ${filePath}`); + } +} diff --git a/ts/classes/iso-builder.ts b/ts/classes/iso-builder.ts new file mode 100644 index 0000000..827374f --- /dev/null +++ b/ts/classes/iso-builder.ts @@ -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 { + 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 { + // 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 { + 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 { + // 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)}`); + } + } +} diff --git a/ts/classes/iso-cache.ts b/ts/classes/iso-cache.ts new file mode 100644 index 0000000..ccf55d7 --- /dev/null +++ b/ts/classes/iso-cache.ts @@ -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 { + await ensureDir(this.cacheDir); + } + + /** + * Get all cache entries + */ + async list(): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + await ensureDir(this.cacheDir); + await Deno.writeTextFile(this.metadataPath, JSON.stringify(entries, null, 2)); + } +} diff --git a/ts/classes/iso-downloader.ts b/ts/classes/iso-downloader.ts new file mode 100644 index 0000000..182f8b4 --- /dev/null +++ b/ts/classes/iso-downloader.ts @@ -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 { + 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 { + 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 { + 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; + } +} diff --git a/ts/classes/iso-extractor.ts b/ts/classes/iso-extractor.ts new file mode 100644 index 0000000..816dc53 --- /dev/null +++ b/ts/classes/iso-extractor.ts @@ -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 { + // 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 { + 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 { + 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'); + } + } +} diff --git a/ts/classes/iso-packer.ts b/ts/classes/iso-packer.ts new file mode 100644 index 0000000..5814edb --- /dev/null +++ b/ts/classes/iso-packer.ts @@ -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 { + 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 { + 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 { + 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 { + 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'); + } + } +} diff --git a/ts/cli.ts b/ts/cli.ts new file mode 100644 index 0000000..7eb5852 --- /dev/null +++ b/ts/cli.ts @@ -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 [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 Use a YAML config file + --ubuntu-version Ubuntu version (22.04, 24.04, etc.) + --arch Architecture (amd64 or arm64) + --wifi-ssid WiFi SSID + --wifi-password WiFi password + --hostname System hostname + --output Output ISO path + --ssh-key Path to SSH public key + +CACHE OPTIONS: + cache list List cached ISOs + cache download Download and cache an ISO + cache clean Clean all cached ISOs + +TEMPLATE OPTIONS: + template create Generate config template to stdout + template create --output Save template to file + +VALIDATE OPTIONS: + validate 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 { + const parsed = new Map(); + 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) { + 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) { + 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 '); + 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) { + 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) { + const configPath = args.get('_arg1') as string; + + if (!configPath) { + log.error('Please specify a config file: isocreator validate '); + 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); + } +} diff --git a/ts/index.ts b/ts/index.ts new file mode 100644 index 0000000..ebada6a --- /dev/null +++ b/ts/index.ts @@ -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'; diff --git a/ts/interfaces/cloud-init-config.interface.ts b/ts/interfaces/cloud-init-config.interface.ts new file mode 100644 index 0000000..18b5156 --- /dev/null +++ b/ts/interfaces/cloud-init-config.interface.ts @@ -0,0 +1,6 @@ +/** + * Cloud-init configuration interface + * Re-export from iso-config for convenience + */ + +export type { ICloudInitConfig } from './iso-config.interface.ts'; diff --git a/ts/interfaces/iso-config.interface.ts b/ts/interfaces/iso-config.interface.ts new file mode 100644 index 0000000..6f1c1c6 --- /dev/null +++ b/ts/interfaces/iso-config.interface.ts @@ -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; +} diff --git a/ts/logging.ts b/ts/logging.ts new file mode 100644 index 0000000..39bf9b2 --- /dev/null +++ b/ts/logging.ts @@ -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), +}; diff --git a/ts/paths.ts b/ts/paths.ts new file mode 100644 index 0000000..23c0a55 --- /dev/null +++ b/ts/paths.ts @@ -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 { + 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 { + try { + await Deno.remove(dirPath, { recursive: true }); + } catch (err) { + if (!(err instanceof Deno.errors.NotFound)) { + throw err; + } + } + await ensureDir(dirPath); +} diff --git a/ts/plugins.ts b/ts/plugins.ts new file mode 100644 index 0000000..3f10b90 --- /dev/null +++ b/ts/plugins.ts @@ -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'; diff --git a/uninstall.sh b/uninstall.sh new file mode 100755 index 0000000..34e4502 --- /dev/null +++ b/uninstall.sh @@ -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}"