commit ce06b5855aa531ed04774268944855ef519fab7e Author: Juergen Kunz Date: Fri Oct 24 08:10:02 2025 +0000 feat(core): Initial project scaffold and implementation: Deno CLI, ISO tooling, cloud-init generation, packaging and installer scripts 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}"