feat(core): Initial project scaffold and implementation: Deno CLI, ISO tooling, cloud-init generation, packaging and installer scripts
This commit is contained in:
151
.gitea/workflows/release.yml
Normal file
151
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Deno
|
||||||
|
uses: denoland/setup-deno@v1
|
||||||
|
with:
|
||||||
|
deno-version: v1.x
|
||||||
|
|
||||||
|
- name: Verify version matches tag
|
||||||
|
run: |
|
||||||
|
TAG_VERSION=${GITHUB_REF#refs/tags/v}
|
||||||
|
DENO_VERSION=$(grep '"version"' deno.json | head -1 | sed 's/.*"version": "\(.*\)".*/\1/')
|
||||||
|
|
||||||
|
echo "Git tag version: $TAG_VERSION"
|
||||||
|
echo "deno.json version: $DENO_VERSION"
|
||||||
|
|
||||||
|
if [ "$TAG_VERSION" != "$DENO_VERSION" ]; then
|
||||||
|
echo "❌ Version mismatch!"
|
||||||
|
echo "Git tag: v$TAG_VERSION"
|
||||||
|
echo "deno.json: $DENO_VERSION"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Versions match: $TAG_VERSION"
|
||||||
|
|
||||||
|
- name: Cache dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.cache/deno
|
||||||
|
key: deno-${{ hashFiles('deno.lock') }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: deno cache --reload mod.ts
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: deno task test
|
||||||
|
|
||||||
|
- name: Compile binaries
|
||||||
|
run: |
|
||||||
|
bash scripts/compile-all.sh
|
||||||
|
|
||||||
|
- name: Generate checksums
|
||||||
|
run: |
|
||||||
|
cd dist/binaries
|
||||||
|
sha256sum * > checksums.txt
|
||||||
|
cat checksums.txt
|
||||||
|
|
||||||
|
- name: Extract changelog
|
||||||
|
id: changelog
|
||||||
|
run: |
|
||||||
|
TAG_VERSION=${GITHUB_REF#refs/tags/v}
|
||||||
|
|
||||||
|
# Extract changelog section for this version
|
||||||
|
CHANGELOG=$(awk "/## \[$TAG_VERSION\]/,/## \[/" changelog.md | sed '1d;$d')
|
||||||
|
|
||||||
|
if [ -z "$CHANGELOG" ]; then
|
||||||
|
echo "No changelog entry found for version $TAG_VERSION"
|
||||||
|
CHANGELOG="Release version $TAG_VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Save to file for release notes
|
||||||
|
echo "$CHANGELOG" > /tmp/release-notes.md
|
||||||
|
cat /tmp/release-notes.md
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
uses: actions/create-release@v1
|
||||||
|
id: create_release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref }}
|
||||||
|
release_name: Release ${{ github.ref_name }}
|
||||||
|
body_path: /tmp/release-notes.md
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
|
||||||
|
- name: Upload Linux x64 Binary
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./dist/binaries/isocreator-linux-x64
|
||||||
|
asset_name: isocreator-linux-x64
|
||||||
|
asset_content_type: application/octet-stream
|
||||||
|
|
||||||
|
- name: Upload Linux ARM64 Binary
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./dist/binaries/isocreator-linux-arm64
|
||||||
|
asset_name: isocreator-linux-arm64
|
||||||
|
asset_content_type: application/octet-stream
|
||||||
|
|
||||||
|
- name: Upload macOS x64 Binary
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./dist/binaries/isocreator-macos-x64
|
||||||
|
asset_name: isocreator-macos-x64
|
||||||
|
asset_content_type: application/octet-stream
|
||||||
|
|
||||||
|
- name: Upload macOS ARM64 Binary
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./dist/binaries/isocreator-macos-arm64
|
||||||
|
asset_name: isocreator-macos-arm64
|
||||||
|
asset_content_type: application/octet-stream
|
||||||
|
|
||||||
|
- name: Upload Windows x64 Binary
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./dist/binaries/isocreator-windows-x64.exe
|
||||||
|
asset_name: isocreator-windows-x64.exe
|
||||||
|
asset_content_type: application/octet-stream
|
||||||
|
|
||||||
|
- name: Upload Checksums
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./dist/binaries/checksums.txt
|
||||||
|
asset_name: checksums.txt
|
||||||
|
asset_content_type: text/plain
|
||||||
|
|
||||||
|
- name: Clean old releases
|
||||||
|
run: |
|
||||||
|
echo "Keeping only the last 3 releases..."
|
||||||
|
# This would require gitea API calls - implement if needed
|
||||||
52
.gitignore
vendored
Normal file
52
.gitignore
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Deno
|
||||||
|
.deno/
|
||||||
|
deno.lock
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
dist/
|
||||||
|
*.exe
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
.cache/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
*.tmp
|
||||||
|
|
||||||
|
# Test artifacts
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# Output
|
||||||
|
output/
|
||||||
|
*.iso
|
||||||
|
|
||||||
|
# User data
|
||||||
|
.nogit/
|
||||||
77
bin/isocreator-wrapper.js
Executable file
77
bin/isocreator-wrapper.js
Executable file
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* isocreator binary wrapper
|
||||||
|
* Spawns the appropriate pre-compiled binary for the current platform
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { platform, arch as osArch } from 'os';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
// Map Node.js platform names to our binary names
|
||||||
|
const platformMap = {
|
||||||
|
darwin: 'macos',
|
||||||
|
linux: 'linux',
|
||||||
|
win32: 'windows',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Map Node.js architecture names
|
||||||
|
const archMap = {
|
||||||
|
x64: 'x64',
|
||||||
|
arm64: 'arm64',
|
||||||
|
};
|
||||||
|
|
||||||
|
const detectedPlatform = platformMap[platform()];
|
||||||
|
const detectedArch = archMap[osArch()];
|
||||||
|
|
||||||
|
if (!detectedPlatform || !detectedArch) {
|
||||||
|
console.error(`❌ Unsupported platform: ${platform()}-${osArch()}`);
|
||||||
|
console.error('Supported platforms: linux-x64, linux-arm64, darwin-x64, darwin-arm64, win32-x64');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ext = detectedPlatform === 'windows' ? '.exe' : '';
|
||||||
|
const binaryName = `isocreator-${detectedPlatform}-${detectedArch}${ext}`;
|
||||||
|
const binaryPath = join(__dirname, '..', 'dist', 'binaries', binaryName);
|
||||||
|
|
||||||
|
// Check if binary exists
|
||||||
|
if (!existsSync(binaryPath)) {
|
||||||
|
console.error(`❌ Binary not found: ${binaryPath}`);
|
||||||
|
console.error('\nThis might be due to a failed installation.');
|
||||||
|
console.error('Try reinstalling:');
|
||||||
|
console.error(' npm install -g @serve.zone/isocreator');
|
||||||
|
console.error('\nOr use the direct installer:');
|
||||||
|
console.error(' curl -sSL https://code.foss.global/serve.zone/isocreator/raw/branch/main/install.sh | sudo bash');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn the binary with all arguments
|
||||||
|
const child = spawn(binaryPath, process.argv.slice(2), {
|
||||||
|
stdio: 'inherit',
|
||||||
|
windowsHide: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Forward signals
|
||||||
|
process.on('SIGINT', () => child.kill('SIGINT'));
|
||||||
|
process.on('SIGTERM', () => child.kill('SIGTERM'));
|
||||||
|
process.on('SIGHUP', () => child.kill('SIGHUP'));
|
||||||
|
|
||||||
|
// Exit with the same code as the binary
|
||||||
|
child.on('exit', (code, signal) => {
|
||||||
|
if (signal) {
|
||||||
|
process.kill(process.pid, signal);
|
||||||
|
} else {
|
||||||
|
process.exit(code || 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', (err) => {
|
||||||
|
console.error('❌ Failed to start isocreator:', err.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
39
changelog.md
Normal file
39
changelog.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-10-24 - 1.1.0 - feat(core)
|
||||||
|
Initial project scaffold and implementation: Deno CLI, ISO tooling, cloud-init generation, packaging and installer scripts
|
||||||
|
|
||||||
|
- Add Deno-based CLI (commands: build, cache, template, validate) and entry point (mod.ts)
|
||||||
|
- Implement ISO management classes: iso-downloader, iso-cache, iso-extractor, iso-packer, iso-builder
|
||||||
|
- Add cloud-init generator and configuration manager with YAML template and validation
|
||||||
|
- Add packaging/distribution tooling: npm wrapper, postinstall installer, binary wrapper, compile script, install/uninstall scripts
|
||||||
|
- Add CI release workflow, changelog, README, license, and configuration files (deno.json, package.json, templates)
|
||||||
|
|
||||||
|
All notable changes to isocreator will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial project structure
|
||||||
|
- Deno-based CLI framework
|
||||||
|
- Binary compilation for multiple platforms
|
||||||
|
- npm distribution with postinstall binary download
|
||||||
|
- Direct installation script
|
||||||
|
|
||||||
|
## [1.0.0] - TBD
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Ubuntu ISO customization for PC (x86_64) and Raspberry Pi (ARM64)
|
||||||
|
- Cloud-init configuration support
|
||||||
|
- WiFi pre-configuration via cloud-init network-config
|
||||||
|
- User account and SSH key injection
|
||||||
|
- Package installation via cloud-init
|
||||||
|
- Custom boot script support
|
||||||
|
- ISO caching system with multi-version support
|
||||||
|
- Interactive mode for guided setup
|
||||||
|
- Config file support (YAML)
|
||||||
|
- CLI flag support for quick customization
|
||||||
|
- Template generation for configuration files
|
||||||
46
deno.json
Normal file
46
deno.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "@serve.zone/isocreator",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"exports": "./mod.ts",
|
||||||
|
"tasks": {
|
||||||
|
"dev": "deno run --allow-all mod.ts",
|
||||||
|
"compile": "bash scripts/compile-all.sh",
|
||||||
|
"test": "deno test --allow-all",
|
||||||
|
"test:watch": "deno test --allow-all --watch",
|
||||||
|
"check": "deno check mod.ts",
|
||||||
|
"fmt": "deno fmt",
|
||||||
|
"fmt:check": "deno fmt --check",
|
||||||
|
"lint": "deno lint",
|
||||||
|
"cache": "deno cache --reload mod.ts"
|
||||||
|
},
|
||||||
|
"imports": {
|
||||||
|
"@std/path": "jsr:@std/path@^1.0.0",
|
||||||
|
"@std/fs": "jsr:@std/fs@^1.0.0",
|
||||||
|
"@std/yaml": "jsr:@std/yaml@^1.0.0",
|
||||||
|
"@std/assert": "jsr:@std/assert@^1.0.0",
|
||||||
|
"@std/fmt": "jsr:@std/fmt@^1.0.0",
|
||||||
|
"@push.rocks/smartcli": "npm:@push.rocks/smartcli@^5.0.0",
|
||||||
|
"@push.rocks/smartlog": "npm:@push.rocks/smartlog@^3.0.0",
|
||||||
|
"@push.rocks/smartfile": "npm:@push.rocks/smartfile@^11.0.0",
|
||||||
|
"@push.rocks/smartpromise": "npm:@push.rocks/smartpromise@^4.0.0",
|
||||||
|
"@push.rocks/smartrequest": "npm:@push.rocks/smartrequest@^2.0.0"
|
||||||
|
},
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["deno.window"],
|
||||||
|
"strict": true,
|
||||||
|
"allowJs": false
|
||||||
|
},
|
||||||
|
"fmt": {
|
||||||
|
"useTabs": false,
|
||||||
|
"lineWidth": 100,
|
||||||
|
"indentWidth": 2,
|
||||||
|
"semiColons": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"proseWrap": "preserve"
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"rules": {
|
||||||
|
"tags": ["recommended"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
144
install.sh
Executable file
144
install.sh
Executable file
@@ -0,0 +1,144 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
REPO_URL="https://code.foss.global/serve.zone/isocreator"
|
||||||
|
INSTALL_DIR="/opt/isocreator"
|
||||||
|
BIN_LINK="/usr/local/bin/isocreator"
|
||||||
|
VERSION="latest"
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--version)
|
||||||
|
VERSION="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--install-dir)
|
||||||
|
INSTALL_DIR="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
echo "Usage: $0 [OPTIONS]"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " --version VERSION Install specific version (default: latest)"
|
||||||
|
echo " --install-dir DIR Installation directory (default: /opt/isocreator)"
|
||||||
|
echo " -h, --help Show this help message"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}Unknown option: $1${NC}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -e "${BLUE}🚀 isocreator installer${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Detect platform
|
||||||
|
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
|
||||||
|
case "$OS" in
|
||||||
|
linux)
|
||||||
|
PLATFORM="linux"
|
||||||
|
;;
|
||||||
|
darwin)
|
||||||
|
PLATFORM="macos"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}❌ Unsupported operating system: $OS${NC}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$ARCH" in
|
||||||
|
x86_64|amd64)
|
||||||
|
ARCH="x64"
|
||||||
|
;;
|
||||||
|
aarch64|arm64)
|
||||||
|
ARCH="arm64"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}❌ Unsupported architecture: $ARCH${NC}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
BINARY_NAME="isocreator-${PLATFORM}-${ARCH}"
|
||||||
|
echo -e "${BLUE}📋 Detected platform: ${PLATFORM}-${ARCH}${NC}"
|
||||||
|
|
||||||
|
# Get version
|
||||||
|
if [ "$VERSION" == "latest" ]; then
|
||||||
|
echo -e "${BLUE}🔍 Fetching latest version...${NC}"
|
||||||
|
VERSION=$(curl -sSL "${REPO_URL}/raw/branch/main/deno.json" | grep -o '"version": "[^"]*' | cut -d'"' -f4)
|
||||||
|
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
echo -e "${YELLOW}⚠️ Could not fetch latest version, using fallback${NC}"
|
||||||
|
VERSION="1.0.0"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}📦 Version: v${VERSION}${NC}"
|
||||||
|
|
||||||
|
# Download URL (try release first, fallback to raw branch)
|
||||||
|
RELEASE_URL="${REPO_URL}/releases/download/v${VERSION}/${BINARY_NAME}"
|
||||||
|
FALLBACK_URL="${REPO_URL}/raw/branch/main/dist/binaries/${BINARY_NAME}"
|
||||||
|
|
||||||
|
echo -e "${BLUE}⬇️ Downloading binary...${NC}"
|
||||||
|
|
||||||
|
# Try release URL first
|
||||||
|
if curl -fsSL -o "/tmp/${BINARY_NAME}" "$RELEASE_URL" 2>/dev/null; then
|
||||||
|
echo -e "${GREEN}✅ Downloaded from release${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠️ Release not found, trying fallback...${NC}"
|
||||||
|
if curl -fsSL -o "/tmp/${BINARY_NAME}" "$FALLBACK_URL"; then
|
||||||
|
echo -e "${GREEN}✅ Downloaded from fallback${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ Failed to download binary${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create installation directory
|
||||||
|
echo -e "${BLUE}📁 Creating installation directory...${NC}"
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
|
||||||
|
# Move binary to installation directory
|
||||||
|
echo -e "${BLUE}📦 Installing binary...${NC}"
|
||||||
|
mv "/tmp/${BINARY_NAME}" "${INSTALL_DIR}/isocreator"
|
||||||
|
chmod +x "${INSTALL_DIR}/isocreator"
|
||||||
|
|
||||||
|
# Create symlink
|
||||||
|
echo -e "${BLUE}🔗 Creating symlink...${NC}"
|
||||||
|
if [ -L "$BIN_LINK" ]; then
|
||||||
|
rm "$BIN_LINK"
|
||||||
|
fi
|
||||||
|
ln -s "${INSTALL_DIR}/isocreator" "$BIN_LINK"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}✅ isocreator installed successfully!${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}📍 Installation location: ${INSTALL_DIR}/isocreator${NC}"
|
||||||
|
echo -e "${BLUE}🔗 Symlink created: ${BIN_LINK}${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}💡 Try running: isocreator --help${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}⚠️ Note: isocreator requires the following system tools:${NC}"
|
||||||
|
echo -e " - xorriso (for ISO manipulation)"
|
||||||
|
echo -e " - isohybrid (for USB-bootable ISOs)"
|
||||||
|
echo -e ""
|
||||||
|
echo -e "${BLUE}Install on Ubuntu/Debian:${NC}"
|
||||||
|
echo -e " sudo apt install xorriso syslinux-utils"
|
||||||
|
echo -e ""
|
||||||
|
echo -e "${BLUE}Install on macOS:${NC}"
|
||||||
|
echo -e " brew install xorriso syslinux"
|
||||||
21
license
Normal file
21
license
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Lossless GmbH
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
21
mod.ts
Normal file
21
mod.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env -S deno run --allow-all
|
||||||
|
|
||||||
|
/**
|
||||||
|
* isocreator - Ubuntu ISO customization tool
|
||||||
|
*
|
||||||
|
* Creates customized Ubuntu Server ISOs for PC and Raspberry Pi with:
|
||||||
|
* - Pre-configured WiFi (via cloud-init)
|
||||||
|
* - User accounts and SSH keys
|
||||||
|
* - Custom packages and boot scripts
|
||||||
|
* - Full cloud-init configuration
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { startCli } from './ts/cli.ts';
|
||||||
|
|
||||||
|
// Start the CLI
|
||||||
|
if (import.meta.main) {
|
||||||
|
await startCli();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export for library usage
|
||||||
|
export * from './ts/index.ts';
|
||||||
1
npmextra.json
Normal file
1
npmextra.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
53
package.json
Normal file
53
package.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"name": "@serve.zone/isocreator",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Ubuntu ISO customization tool for PC and Raspberry Pi with WiFi and cloud-init configuration",
|
||||||
|
"type": "module",
|
||||||
|
"bin": {
|
||||||
|
"isocreator": "./bin/isocreator-wrapper.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"postinstall": "node scripts/install-binary.js",
|
||||||
|
"prepublishOnly": "echo 'Preparing to publish isocreator'",
|
||||||
|
"build": "echo 'No build needed - binaries pre-compiled'"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"ubuntu",
|
||||||
|
"iso",
|
||||||
|
"raspberry-pi",
|
||||||
|
"cloud-init",
|
||||||
|
"wifi",
|
||||||
|
"automation",
|
||||||
|
"linux",
|
||||||
|
"customization"
|
||||||
|
],
|
||||||
|
"author": "Lossless GmbH",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://code.foss.global/serve.zone/isocreator.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://code.foss.global/serve.zone/isocreator/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://code.foss.global/serve.zone/isocreator",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"os": [
|
||||||
|
"darwin",
|
||||||
|
"linux",
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"cpu": [
|
||||||
|
"x64",
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
"bin/",
|
||||||
|
"scripts/install-binary.js",
|
||||||
|
"readme.md",
|
||||||
|
"license",
|
||||||
|
"changelog.md"
|
||||||
|
]
|
||||||
|
}
|
||||||
72
readme.hints.md
Normal file
72
readme.hints.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# Development Hints
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
This project follows the nupst/spark pattern for Deno-based CLI tools with binary distribution.
|
||||||
|
|
||||||
|
## Key Components
|
||||||
|
|
||||||
|
1. **ISO Management** (`ts/classes/`)
|
||||||
|
- `iso-downloader.ts` - Downloads Ubuntu ISOs from official mirrors
|
||||||
|
- `iso-cache.ts` - Manages cached ISOs with multi-version support
|
||||||
|
- `iso-extractor.ts` - Extracts ISOs using xorriso
|
||||||
|
- `iso-packer.ts` - Repacks modified ISOs into bootable images
|
||||||
|
- `iso-builder.ts` - Orchestrates the entire build process
|
||||||
|
|
||||||
|
2. **Cloud-Init** (`ts/classes/`)
|
||||||
|
- `cloud-init-generator.ts` - Generates cloud-init configuration files
|
||||||
|
- `config-manager.ts` - Loads and validates YAML configs
|
||||||
|
|
||||||
|
3. **CLI** (`ts/cli.ts`)
|
||||||
|
- Command-line interface with commands: build, cache, template, validate
|
||||||
|
|
||||||
|
## System Dependencies
|
||||||
|
|
||||||
|
The tool requires these system tools at runtime:
|
||||||
|
- `xorriso` - ISO manipulation
|
||||||
|
- `syslinux-utils` - For USB-bootable ISOs (isohybrid command)
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
deno install
|
||||||
|
|
||||||
|
# Run in development mode
|
||||||
|
deno task dev --help
|
||||||
|
|
||||||
|
# Type checking
|
||||||
|
deno task check
|
||||||
|
|
||||||
|
# Format code
|
||||||
|
deno task fmt
|
||||||
|
|
||||||
|
# Lint
|
||||||
|
deno task lint
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
deno task test
|
||||||
|
|
||||||
|
# Compile binaries
|
||||||
|
deno task compile
|
||||||
|
```
|
||||||
|
|
||||||
|
## Distribution
|
||||||
|
|
||||||
|
Binaries are compiled for 5 platforms:
|
||||||
|
- Linux x64
|
||||||
|
- Linux ARM64
|
||||||
|
- macOS x64
|
||||||
|
- macOS ARM64
|
||||||
|
- Windows x64
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Implement CLI flag-based building (currently only config files work)
|
||||||
|
2. Add interactive mode with prompts
|
||||||
|
3. Implement boot scripts injection with systemd service creation
|
||||||
|
4. Add pre-install packages feature (requires squashfs manipulation)
|
||||||
|
5. Write comprehensive tests
|
||||||
|
6. Compile and test binaries
|
||||||
|
7. Set up CI/CD pipeline
|
||||||
|
8. Publish to npm
|
||||||
312
readme.md
Normal file
312
readme.md
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
# isocreator
|
||||||
|
|
||||||
|
> Ubuntu ISO customization tool for PC and Raspberry Pi with WiFi and cloud-init configuration
|
||||||
|
|
||||||
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
**isocreator** is a command-line tool that creates customized Ubuntu Server ISOs with pre-configured:
|
||||||
|
|
||||||
|
- 📡 **WiFi credentials** (via cloud-init)
|
||||||
|
- 👤 **User accounts** and SSH keys
|
||||||
|
- 📦 **Pre-installed packages**
|
||||||
|
- 🔧 **Custom boot scripts**
|
||||||
|
- ⚙️ **Full cloud-init configuration**
|
||||||
|
|
||||||
|
Perfect for:
|
||||||
|
- Raspberry Pi deployments
|
||||||
|
- Headless server installations
|
||||||
|
- Automated fleet provisioning
|
||||||
|
- Development environments
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Multi-Platform Support**: PC (x86_64) and Raspberry Pi (ARM64)
|
||||||
|
- **Multi-Version Support**: Ubuntu 22.04 LTS, 24.04 LTS, and future versions
|
||||||
|
- **Cloud-Init Integration**: Full cloud-init user-data and network-config support
|
||||||
|
- **Caching System**: Intelligent ISO caching with multi-version support
|
||||||
|
- **Flexible Configuration**: YAML files, CLI flags, or interactive mode
|
||||||
|
- **USB Bootable**: Creates ISOs that can be written directly to USB drives
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### via npm
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g @serve.zone/isocreator
|
||||||
|
```
|
||||||
|
|
||||||
|
### via Direct Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sSL https://code.foss.global/serve.zone/isocreator/raw/branch/main/install.sh | sudo bash
|
||||||
|
```
|
||||||
|
|
||||||
|
### System Dependencies
|
||||||
|
|
||||||
|
isocreator requires the following tools to be installed:
|
||||||
|
|
||||||
|
**Ubuntu/Debian:**
|
||||||
|
```bash
|
||||||
|
sudo apt install xorriso syslinux-utils
|
||||||
|
```
|
||||||
|
|
||||||
|
**macOS:**
|
||||||
|
```bash
|
||||||
|
brew install xorriso syslinux
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Interactive Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
isocreator build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using a Config File
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate a template
|
||||||
|
isocreator template create --output myconfig.yaml
|
||||||
|
|
||||||
|
# Edit the config file
|
||||||
|
nano myconfig.yaml
|
||||||
|
|
||||||
|
# Build the ISO
|
||||||
|
isocreator build --config myconfig.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using CLI Flags
|
||||||
|
|
||||||
|
```bash
|
||||||
|
isocreator build \
|
||||||
|
--ubuntu-version 24.04 \
|
||||||
|
--arch amd64 \
|
||||||
|
--wifi-ssid "MyWiFi" \
|
||||||
|
--wifi-password "secret123" \
|
||||||
|
--ssh-key ~/.ssh/id_rsa.pub \
|
||||||
|
--hostname "myserver" \
|
||||||
|
--output ./custom-ubuntu.iso
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Example Config File
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: "1.0"
|
||||||
|
|
||||||
|
# Base ISO settings
|
||||||
|
iso:
|
||||||
|
ubuntu_version: "24.04"
|
||||||
|
architecture: "amd64" # or arm64 for Raspberry Pi
|
||||||
|
|
||||||
|
# Output settings
|
||||||
|
output:
|
||||||
|
filename: "ubuntu-custom.iso"
|
||||||
|
path: "./output"
|
||||||
|
|
||||||
|
# WiFi configuration (via cloud-init)
|
||||||
|
network:
|
||||||
|
wifi:
|
||||||
|
ssid: "MyWiFi"
|
||||||
|
password: "secret123"
|
||||||
|
|
||||||
|
# Cloud-init configuration
|
||||||
|
cloud_init:
|
||||||
|
hostname: "myserver"
|
||||||
|
|
||||||
|
# User accounts
|
||||||
|
users:
|
||||||
|
- name: "admin"
|
||||||
|
ssh_authorized_keys:
|
||||||
|
- "ssh-rsa AAAAB3NzaC1yc2E..."
|
||||||
|
sudo: "ALL=(ALL) NOPASSWD:ALL"
|
||||||
|
shell: "/bin/bash"
|
||||||
|
|
||||||
|
# Packages to install on first boot
|
||||||
|
packages:
|
||||||
|
- docker.io
|
||||||
|
- git
|
||||||
|
- htop
|
||||||
|
|
||||||
|
# Commands to run on first boot
|
||||||
|
runcmd:
|
||||||
|
- systemctl enable docker
|
||||||
|
- systemctl start docker
|
||||||
|
- echo "Setup complete!"
|
||||||
|
|
||||||
|
# Custom boot scripts
|
||||||
|
boot_scripts:
|
||||||
|
- name: "setup-docker"
|
||||||
|
path: "./scripts/setup-docker.sh"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
Build a customized ISO:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
isocreator build [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- `--config <file>` - Use a YAML config file
|
||||||
|
- `--ubuntu-version <version>` - Ubuntu version (22.04, 24.04, etc.)
|
||||||
|
- `--arch <arch>` - Architecture (amd64 or arm64)
|
||||||
|
- `--wifi-ssid <ssid>` - WiFi SSID
|
||||||
|
- `--wifi-password <password>` - WiFi password
|
||||||
|
- `--ssh-key <path>` - Path to SSH public key
|
||||||
|
- `--hostname <name>` - System hostname
|
||||||
|
- `--output <path>` - Output ISO path
|
||||||
|
|
||||||
|
### Cache Management
|
||||||
|
|
||||||
|
List cached ISOs:
|
||||||
|
```bash
|
||||||
|
isocreator cache list
|
||||||
|
```
|
||||||
|
|
||||||
|
Download an ISO to cache:
|
||||||
|
```bash
|
||||||
|
isocreator cache download 24.04 --arch amd64
|
||||||
|
```
|
||||||
|
|
||||||
|
Clean old cached ISOs:
|
||||||
|
```bash
|
||||||
|
isocreator cache clean
|
||||||
|
isocreator cache clean --older-than 6m
|
||||||
|
```
|
||||||
|
|
||||||
|
### Templates
|
||||||
|
|
||||||
|
Generate a config template:
|
||||||
|
```bash
|
||||||
|
isocreator template create
|
||||||
|
isocreator template create --output myconfig.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
|
||||||
|
Validate a config file:
|
||||||
|
```bash
|
||||||
|
isocreator validate myconfig.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
### Raspberry Pi Home Server
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
iso:
|
||||||
|
ubuntu_version: "24.04"
|
||||||
|
architecture: "arm64"
|
||||||
|
|
||||||
|
network:
|
||||||
|
wifi:
|
||||||
|
ssid: "HomeNetwork"
|
||||||
|
password: "homepass123"
|
||||||
|
|
||||||
|
cloud_init:
|
||||||
|
hostname: "pi-server"
|
||||||
|
users:
|
||||||
|
- name: "pi"
|
||||||
|
ssh_authorized_keys:
|
||||||
|
- "ssh-rsa AAAAB3..."
|
||||||
|
sudo: "ALL=(ALL) NOPASSWD:ALL"
|
||||||
|
|
||||||
|
packages:
|
||||||
|
- docker.io
|
||||||
|
- docker-compose
|
||||||
|
|
||||||
|
runcmd:
|
||||||
|
- systemctl enable docker
|
||||||
|
```
|
||||||
|
|
||||||
|
### Headless Development Server
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
iso:
|
||||||
|
ubuntu_version: "24.04"
|
||||||
|
architecture: "amd64"
|
||||||
|
|
||||||
|
cloud_init:
|
||||||
|
hostname: "devbox"
|
||||||
|
|
||||||
|
users:
|
||||||
|
- name: "developer"
|
||||||
|
ssh_authorized_keys:
|
||||||
|
- "ssh-rsa AAAAB3..."
|
||||||
|
sudo: "ALL=(ALL) NOPASSWD:ALL"
|
||||||
|
|
||||||
|
packages:
|
||||||
|
- build-essential
|
||||||
|
- git
|
||||||
|
- docker.io
|
||||||
|
- nodejs
|
||||||
|
- npm
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
isocreator is built with:
|
||||||
|
|
||||||
|
- **Deno** - Modern TypeScript runtime
|
||||||
|
- **Cloud-init** - Industry-standard cloud instance initialization
|
||||||
|
- **xorriso** - ISO 9660 filesystem manipulation
|
||||||
|
- **Self-contained binaries** - Zero runtime dependencies
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Deno 1.40+
|
||||||
|
- xorriso
|
||||||
|
- syslinux-utils (for isohybrid)
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://code.foss.global/serve.zone/isocreator.git
|
||||||
|
cd isocreator
|
||||||
|
deno task cache
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
deno task dev --help
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
deno task test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Binaries
|
||||||
|
|
||||||
|
```bash
|
||||||
|
deno task compile
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please feel free to submit issues and pull requests.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - see [license](./license) file for details
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- **Issues**: https://code.foss.global/serve.zone/isocreator/issues
|
||||||
|
- **Documentation**: https://code.foss.global/serve.zone/isocreator
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Made with ❤️ by [Lossless GmbH](https://lossless.com)
|
||||||
33
scripts/compile-all.sh
Executable file
33
scripts/compile-all.sh
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔨 Compiling isocreator binaries for all platforms..."
|
||||||
|
|
||||||
|
# Create dist directory if it doesn't exist
|
||||||
|
mkdir -p dist/binaries
|
||||||
|
|
||||||
|
# Linux x64
|
||||||
|
echo "📦 Compiling for Linux x64..."
|
||||||
|
deno compile --allow-all --no-check --target x86_64-unknown-linux-gnu --output dist/binaries/isocreator-linux-x64 mod.ts
|
||||||
|
|
||||||
|
# Linux ARM64
|
||||||
|
echo "📦 Compiling for Linux ARM64..."
|
||||||
|
deno compile --allow-all --no-check --target aarch64-unknown-linux-gnu --output dist/binaries/isocreator-linux-arm64 mod.ts
|
||||||
|
|
||||||
|
# macOS x64
|
||||||
|
echo "📦 Compiling for macOS x64..."
|
||||||
|
deno compile --allow-all --no-check --target x86_64-apple-darwin --output dist/binaries/isocreator-macos-x64 mod.ts
|
||||||
|
|
||||||
|
# macOS ARM64
|
||||||
|
echo "📦 Compiling for macOS ARM64..."
|
||||||
|
deno compile --allow-all --no-check --target aarch64-apple-darwin --output dist/binaries/isocreator-macos-arm64 mod.ts
|
||||||
|
|
||||||
|
# Windows x64
|
||||||
|
echo "📦 Compiling for Windows x64..."
|
||||||
|
deno compile --allow-all --no-check --target x86_64-pc-windows-msvc --output dist/binaries/isocreator-windows-x64.exe mod.ts
|
||||||
|
|
||||||
|
echo "✅ All binaries compiled successfully!"
|
||||||
|
echo ""
|
||||||
|
echo "Binaries location: dist/binaries/"
|
||||||
|
ls -lh dist/binaries/
|
||||||
144
scripts/install-binary.js
Normal file
144
scripts/install-binary.js
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* npm postinstall script
|
||||||
|
* Downloads the correct binary for the current platform
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createWriteStream, chmodSync, existsSync, mkdirSync } from 'fs';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { get as httpsGet } from 'https';
|
||||||
|
import { get as httpGet } from 'http';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const rootDir = join(__dirname, '..');
|
||||||
|
|
||||||
|
const REPO_URL = 'https://code.foss.global/serve.zone/isocreator';
|
||||||
|
const VERSION = '1.0.0'; // Will be updated automatically
|
||||||
|
|
||||||
|
// Detect platform and architecture
|
||||||
|
const platformMap = {
|
||||||
|
darwin: 'macos',
|
||||||
|
linux: 'linux',
|
||||||
|
win32: 'windows',
|
||||||
|
};
|
||||||
|
|
||||||
|
const archMap = {
|
||||||
|
x64: 'x64',
|
||||||
|
arm64: 'arm64',
|
||||||
|
};
|
||||||
|
|
||||||
|
const platform = platformMap[process.platform];
|
||||||
|
const arch = archMap[process.arch];
|
||||||
|
|
||||||
|
if (!platform || !arch) {
|
||||||
|
console.error(`❌ Unsupported platform: ${process.platform}-${process.arch}`);
|
||||||
|
console.error('Supported platforms: linux-x64, linux-arm64, darwin-x64, darwin-arm64, win32-x64');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ext = platform === 'windows' ? '.exe' : '';
|
||||||
|
const binaryName = `isocreator-${platform}-${arch}${ext}`;
|
||||||
|
const binaryDir = join(rootDir, 'dist', 'binaries');
|
||||||
|
const binaryPath = join(binaryDir, binaryName);
|
||||||
|
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
if (!existsSync(binaryDir)) {
|
||||||
|
mkdirSync(binaryDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try release URL first, fallback to raw branch
|
||||||
|
const urls = [
|
||||||
|
`${REPO_URL}/releases/download/v${VERSION}/${binaryName}`,
|
||||||
|
`${REPO_URL}/raw/branch/main/dist/binaries/${binaryName}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log(`📦 Installing isocreator for ${platform}-${arch}...`);
|
||||||
|
|
||||||
|
function downloadFile(url, attempt = 1) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const protocol = url.startsWith('https') ? httpsGet : httpGet;
|
||||||
|
|
||||||
|
console.log(`⬇️ Downloading from: ${url}`);
|
||||||
|
|
||||||
|
protocol(url, { timeout: 30000 }, (response) => {
|
||||||
|
// Handle redirects
|
||||||
|
if (response.statusCode === 301 || response.statusCode === 302) {
|
||||||
|
const redirectUrl = response.headers.location;
|
||||||
|
console.log(`↪️ Redirecting to: ${redirectUrl}`);
|
||||||
|
return downloadFile(redirectUrl, attempt).then(resolve).catch(reject);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
console.warn(`⚠️ Failed to download (HTTP ${response.statusCode})`);
|
||||||
|
|
||||||
|
if (attempt < urls.length) {
|
||||||
|
console.log(`🔄 Trying fallback URL (${attempt + 1}/${urls.length})...`);
|
||||||
|
return downloadFile(urls[attempt], attempt + 1).then(resolve).catch(reject);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reject(new Error(`Failed to download binary from all sources`));
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = createWriteStream(binaryPath);
|
||||||
|
const totalBytes = parseInt(response.headers['content-length'], 10);
|
||||||
|
let downloadedBytes = 0;
|
||||||
|
|
||||||
|
response.on('data', (chunk) => {
|
||||||
|
downloadedBytes += chunk.length;
|
||||||
|
if (totalBytes) {
|
||||||
|
const percent = ((downloadedBytes / totalBytes) * 100).toFixed(1);
|
||||||
|
process.stdout.write(`\r📥 Progress: ${percent}%`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
response.pipe(file);
|
||||||
|
|
||||||
|
file.on('finish', () => {
|
||||||
|
file.close();
|
||||||
|
console.log('\n✅ Download complete!');
|
||||||
|
|
||||||
|
// Make executable on Unix-like systems
|
||||||
|
if (platform !== 'windows') {
|
||||||
|
try {
|
||||||
|
chmodSync(binaryPath, 0o755);
|
||||||
|
console.log('✅ Binary made executable');
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('⚠️ Failed to make binary executable:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
file.on('error', (err) => {
|
||||||
|
file.close();
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
}).on('error', (err) => {
|
||||||
|
if (attempt < urls.length) {
|
||||||
|
console.warn(`⚠️ Download failed:`, err.message);
|
||||||
|
console.log(`🔄 Trying fallback URL (${attempt + 1}/${urls.length})...`);
|
||||||
|
downloadFile(urls[attempt], attempt + 1).then(resolve).catch(reject);
|
||||||
|
} else {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start download
|
||||||
|
downloadFile(urls[0], 1)
|
||||||
|
.then(() => {
|
||||||
|
console.log('🎉 isocreator installed successfully!');
|
||||||
|
console.log(`\n💡 Try running: isocreator --help`);
|
||||||
|
process.exit(0);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('\n❌ Installation failed:', err.message);
|
||||||
|
console.error('\nYou can try manual installation:');
|
||||||
|
console.error(` curl -sSL ${REPO_URL}/raw/branch/main/install.sh | sudo bash`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
90
templates/config.template.yaml
Normal file
90
templates/config.template.yaml
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# isocreator configuration file
|
||||||
|
# Version: 1.0
|
||||||
|
|
||||||
|
version: "1.0"
|
||||||
|
|
||||||
|
# Base ISO settings
|
||||||
|
iso:
|
||||||
|
ubuntu_version: "24.04" # Ubuntu version (22.04, 24.04, etc.)
|
||||||
|
architecture: "amd64" # amd64 for PC, arm64 for Raspberry Pi
|
||||||
|
flavor: "server" # server or desktop (default: server)
|
||||||
|
|
||||||
|
# Output settings
|
||||||
|
output:
|
||||||
|
filename: "ubuntu-custom.iso"
|
||||||
|
path: "./output"
|
||||||
|
|
||||||
|
# Network configuration
|
||||||
|
network:
|
||||||
|
wifi:
|
||||||
|
ssid: "MyWiFi"
|
||||||
|
password: "changeme"
|
||||||
|
# Optional advanced settings:
|
||||||
|
# security: "wpa2"
|
||||||
|
# hidden: false
|
||||||
|
|
||||||
|
# Cloud-init configuration
|
||||||
|
cloud_init:
|
||||||
|
# System hostname
|
||||||
|
hostname: "ubuntu-server"
|
||||||
|
|
||||||
|
# User accounts
|
||||||
|
users:
|
||||||
|
- name: "admin"
|
||||||
|
ssh_authorized_keys:
|
||||||
|
- "ssh-rsa AAAAB3... your-key-here"
|
||||||
|
sudo: "ALL=(ALL) NOPASSWD:ALL"
|
||||||
|
shell: "/bin/bash"
|
||||||
|
# Optional:
|
||||||
|
# groups: ["docker", "sudo"]
|
||||||
|
# lock_passwd: true
|
||||||
|
|
||||||
|
# Packages to install on first boot
|
||||||
|
packages:
|
||||||
|
- docker.io
|
||||||
|
- git
|
||||||
|
- htop
|
||||||
|
- curl
|
||||||
|
- wget
|
||||||
|
|
||||||
|
# Package management
|
||||||
|
package_update: true
|
||||||
|
package_upgrade: false
|
||||||
|
|
||||||
|
# Commands to run on first boot
|
||||||
|
runcmd:
|
||||||
|
- systemctl enable docker
|
||||||
|
- systemctl start docker
|
||||||
|
- echo "Setup complete!" > /tmp/setup-done
|
||||||
|
|
||||||
|
# Write files
|
||||||
|
# write_files:
|
||||||
|
# - path: /etc/motd
|
||||||
|
# content: |
|
||||||
|
# Welcome to Ubuntu Custom ISO!
|
||||||
|
# owner: root:root
|
||||||
|
# permissions: '0644'
|
||||||
|
|
||||||
|
# Timezone
|
||||||
|
# timezone: "UTC"
|
||||||
|
|
||||||
|
# Locale
|
||||||
|
# locale: "en_US.UTF-8"
|
||||||
|
|
||||||
|
# SSH settings
|
||||||
|
# ssh:
|
||||||
|
# password_authentication: false
|
||||||
|
# permit_root_login: false
|
||||||
|
|
||||||
|
# Custom boot scripts (advanced)
|
||||||
|
# boot_scripts:
|
||||||
|
# - name: "setup-docker"
|
||||||
|
# path: "./scripts/setup-docker.sh"
|
||||||
|
# enable: true
|
||||||
|
|
||||||
|
# Pre-install packages (requires longer build time)
|
||||||
|
# preinstall:
|
||||||
|
# enabled: false
|
||||||
|
# packages:
|
||||||
|
# - vim
|
||||||
|
# - curl
|
||||||
8
ts/00_commitinfo_data.ts
Normal file
8
ts/00_commitinfo_data.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* autocreated commitinfo by @push.rocks/commitinfo
|
||||||
|
*/
|
||||||
|
export const commitinfo = {
|
||||||
|
name: '@serve.zone/isocreator',
|
||||||
|
version: '1.1.0',
|
||||||
|
description: 'Ubuntu ISO customization tool for PC and Raspberry Pi with WiFi and cloud-init configuration'
|
||||||
|
}
|
||||||
168
ts/classes/cloud-init-generator.ts
Normal file
168
ts/classes/cloud-init-generator.ts
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
/**
|
||||||
|
* Cloud-Init Generator
|
||||||
|
* Generates cloud-init configuration files
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { yaml, path as pathUtil } from '../plugins.ts';
|
||||||
|
import { log } from '../logging.ts';
|
||||||
|
import type { ICloudInitConfig } from '../interfaces/iso-config.interface.ts';
|
||||||
|
|
||||||
|
export interface INetworkConfig {
|
||||||
|
wifi?: {
|
||||||
|
ssid: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CloudInitGenerator {
|
||||||
|
/**
|
||||||
|
* Generate user-data file content
|
||||||
|
*/
|
||||||
|
generateUserData(config: ICloudInitConfig): string {
|
||||||
|
const userDataObj: Record<string, unknown> = {
|
||||||
|
'#cloud-config': null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hostname
|
||||||
|
if (config.hostname) {
|
||||||
|
userDataObj.hostname = config.hostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users
|
||||||
|
if (config.users && config.users.length > 0) {
|
||||||
|
userDataObj.users = config.users.map((user) => ({
|
||||||
|
name: user.name,
|
||||||
|
...(user.ssh_authorized_keys && { ssh_authorized_keys: user.ssh_authorized_keys }),
|
||||||
|
...(user.sudo && { sudo: user.sudo }),
|
||||||
|
...(user.shell && { shell: user.shell }),
|
||||||
|
...(user.groups && { groups: user.groups }),
|
||||||
|
...(user.lock_passwd !== undefined && { lock_passwd: user.lock_passwd }),
|
||||||
|
...(user.passwd && { passwd: user.passwd }),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Packages
|
||||||
|
if (config.packages && config.packages.length > 0) {
|
||||||
|
userDataObj.packages = config.packages;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package update/upgrade
|
||||||
|
if (config.package_update !== undefined) {
|
||||||
|
userDataObj.package_update = config.package_update;
|
||||||
|
}
|
||||||
|
if (config.package_upgrade !== undefined) {
|
||||||
|
userDataObj.package_upgrade = config.package_upgrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run commands
|
||||||
|
if (config.runcmd && config.runcmd.length > 0) {
|
||||||
|
userDataObj.runcmd = config.runcmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write files
|
||||||
|
if (config.write_files && config.write_files.length > 0) {
|
||||||
|
userDataObj.write_files = config.write_files;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timezone
|
||||||
|
if (config.timezone) {
|
||||||
|
userDataObj.timezone = config.timezone;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Locale
|
||||||
|
if (config.locale) {
|
||||||
|
userDataObj.locale = config.locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSH settings
|
||||||
|
if (config.ssh) {
|
||||||
|
userDataObj.ssh = config.ssh;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any additional custom directives
|
||||||
|
for (const [key, value] of Object.entries(config)) {
|
||||||
|
if (!['hostname', 'users', 'packages', 'package_update', 'package_upgrade',
|
||||||
|
'runcmd', 'write_files', 'timezone', 'locale', 'ssh'].includes(key)) {
|
||||||
|
userDataObj[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the cloud-config comment from the object before YAML conversion
|
||||||
|
delete userDataObj['#cloud-config'];
|
||||||
|
|
||||||
|
// Convert to YAML and add the cloud-config header
|
||||||
|
const yamlContent = yaml.stringify(userDataObj);
|
||||||
|
return `#cloud-config\n${yamlContent}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate network-config file content (for WiFi)
|
||||||
|
*/
|
||||||
|
generateNetworkConfig(networkConfig: INetworkConfig): string {
|
||||||
|
if (!networkConfig.wifi) {
|
||||||
|
return yaml.stringify({ version: 2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ssid, password } = networkConfig.wifi;
|
||||||
|
|
||||||
|
const netConfig = {
|
||||||
|
version: 2,
|
||||||
|
wifis: {
|
||||||
|
wlan0: {
|
||||||
|
dhcp4: true,
|
||||||
|
optional: true,
|
||||||
|
'access-points': {
|
||||||
|
[ssid]: {
|
||||||
|
password: password,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return yaml.stringify(netConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate meta-data file content
|
||||||
|
*/
|
||||||
|
generateMetaData(hostname?: string): string {
|
||||||
|
const metaData = {
|
||||||
|
'instance-id': `iid-${Date.now()}`,
|
||||||
|
'local-hostname': hostname || 'ubuntu',
|
||||||
|
};
|
||||||
|
|
||||||
|
return yaml.stringify(metaData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write cloud-init files to a directory
|
||||||
|
*/
|
||||||
|
async writeCloudInitFiles(
|
||||||
|
outputDir: string,
|
||||||
|
cloudInitConfig: ICloudInitConfig,
|
||||||
|
networkConfig?: INetworkConfig,
|
||||||
|
): Promise<void> {
|
||||||
|
log.info('Generating cloud-init configuration files...');
|
||||||
|
|
||||||
|
// Generate user-data
|
||||||
|
const userData = this.generateUserData(cloudInitConfig);
|
||||||
|
const userDataPath = pathUtil.join(outputDir, 'user-data');
|
||||||
|
await Deno.writeTextFile(userDataPath, userData);
|
||||||
|
log.success('Generated user-data');
|
||||||
|
|
||||||
|
// Generate network-config
|
||||||
|
if (networkConfig) {
|
||||||
|
const netConfig = this.generateNetworkConfig(networkConfig);
|
||||||
|
const netConfigPath = pathUtil.join(outputDir, 'network-config');
|
||||||
|
await Deno.writeTextFile(netConfigPath, netConfig);
|
||||||
|
log.success('Generated network-config');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate meta-data
|
||||||
|
const metaData = this.generateMetaData(cloudInitConfig.hostname);
|
||||||
|
const metaDataPath = pathUtil.join(outputDir, 'meta-data');
|
||||||
|
await Deno.writeTextFile(metaDataPath, metaData);
|
||||||
|
log.success('Generated meta-data');
|
||||||
|
}
|
||||||
|
}
|
||||||
133
ts/classes/config-manager.ts
Normal file
133
ts/classes/config-manager.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
/**
|
||||||
|
* Config Manager
|
||||||
|
* Loads and validates YAML configuration files
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { yaml } from '../plugins.ts';
|
||||||
|
import { log } from '../logging.ts';
|
||||||
|
import type { IIsoConfig } from '../interfaces/iso-config.interface.ts';
|
||||||
|
|
||||||
|
export class ConfigManager {
|
||||||
|
/**
|
||||||
|
* Load config from YAML file
|
||||||
|
*/
|
||||||
|
async loadFromFile(filePath: string): Promise<IIsoConfig> {
|
||||||
|
log.info(`Loading configuration from ${filePath}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = await Deno.readTextFile(filePath);
|
||||||
|
const config = yaml.parse(content) as IIsoConfig;
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
this.validate(config);
|
||||||
|
|
||||||
|
log.success('Configuration loaded and validated');
|
||||||
|
return config;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Deno.errors.NotFound) {
|
||||||
|
throw new Error(`Config file not found: ${filePath}`);
|
||||||
|
}
|
||||||
|
throw new Error(`Failed to load config: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate configuration
|
||||||
|
*/
|
||||||
|
validate(config: IIsoConfig): void {
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
// Version check
|
||||||
|
if (!config.version) {
|
||||||
|
errors.push('Missing required field: version');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ISO settings
|
||||||
|
if (!config.iso) {
|
||||||
|
errors.push('Missing required field: iso');
|
||||||
|
} else {
|
||||||
|
if (!config.iso.ubuntu_version) {
|
||||||
|
errors.push('Missing required field: iso.ubuntu_version');
|
||||||
|
}
|
||||||
|
if (!config.iso.architecture || !['amd64', 'arm64'].includes(config.iso.architecture)) {
|
||||||
|
errors.push('Invalid or missing field: iso.architecture (must be "amd64" or "arm64")');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output settings
|
||||||
|
if (!config.output) {
|
||||||
|
errors.push('Missing required field: output');
|
||||||
|
} else {
|
||||||
|
if (!config.output.filename) {
|
||||||
|
errors.push('Missing required field: output.filename');
|
||||||
|
}
|
||||||
|
if (!config.output.path) {
|
||||||
|
errors.push('Missing required field: output.path');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
throw new Error(`Configuration validation failed:\n${errors.join('\n')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a template configuration
|
||||||
|
*/
|
||||||
|
generateTemplate(): string {
|
||||||
|
const template: IIsoConfig = {
|
||||||
|
version: '1.0',
|
||||||
|
iso: {
|
||||||
|
ubuntu_version: '24.04',
|
||||||
|
architecture: 'amd64',
|
||||||
|
flavor: 'server',
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
filename: 'ubuntu-custom.iso',
|
||||||
|
path: './output',
|
||||||
|
},
|
||||||
|
network: {
|
||||||
|
wifi: {
|
||||||
|
ssid: 'MyWiFi',
|
||||||
|
password: 'changeme',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cloud_init: {
|
||||||
|
hostname: 'ubuntu-server',
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
name: 'admin',
|
||||||
|
ssh_authorized_keys: [
|
||||||
|
'ssh-rsa AAAAB3... your-key-here',
|
||||||
|
],
|
||||||
|
sudo: 'ALL=(ALL) NOPASSWD:ALL',
|
||||||
|
shell: '/bin/bash',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
packages: [
|
||||||
|
'docker.io',
|
||||||
|
'git',
|
||||||
|
'htop',
|
||||||
|
],
|
||||||
|
runcmd: [
|
||||||
|
'systemctl enable docker',
|
||||||
|
'systemctl start docker',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return `# isocreator configuration file
|
||||||
|
# Version: 1.0
|
||||||
|
|
||||||
|
${yaml.stringify(template)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save template to file
|
||||||
|
*/
|
||||||
|
async saveTemplate(filePath: string): Promise<void> {
|
||||||
|
const template = this.generateTemplate();
|
||||||
|
await Deno.writeTextFile(filePath, template);
|
||||||
|
log.success(`Template saved to ${filePath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
153
ts/classes/iso-builder.ts
Normal file
153
ts/classes/iso-builder.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
/**
|
||||||
|
* ISO Builder Orchestrator
|
||||||
|
* Coordinates all ISO building operations
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { log } from '../logging.ts';
|
||||||
|
import { path } from '../plugins.ts';
|
||||||
|
import { ensureDir } from '../paths.ts';
|
||||||
|
import type { IIsoConfig } from '../interfaces/iso-config.interface.ts';
|
||||||
|
|
||||||
|
import { IsoCache } from './iso-cache.ts';
|
||||||
|
import { IsoDownloader } from './iso-downloader.ts';
|
||||||
|
import { IsoExtractor } from './iso-extractor.ts';
|
||||||
|
import { IsoPacker } from './iso-packer.ts';
|
||||||
|
import { CloudInitGenerator } from './cloud-init-generator.ts';
|
||||||
|
|
||||||
|
export class IsoBuilder {
|
||||||
|
private cache: IsoCache;
|
||||||
|
private extractor: IsoExtractor;
|
||||||
|
private packer: IsoPacker;
|
||||||
|
private cloudInitGen: CloudInitGenerator;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.cache = new IsoCache();
|
||||||
|
this.extractor = new IsoExtractor();
|
||||||
|
this.packer = new IsoPacker();
|
||||||
|
this.cloudInitGen = new CloudInitGenerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a customized ISO from configuration
|
||||||
|
*/
|
||||||
|
async build(config: IIsoConfig, onProgress?: (step: string, progress: number) => void): Promise<void> {
|
||||||
|
log.info('Starting ISO build process...');
|
||||||
|
|
||||||
|
// Step 1: Check dependencies
|
||||||
|
onProgress?.('Checking dependencies', 10);
|
||||||
|
await this.extractor.ensureDependencies();
|
||||||
|
await this.packer.ensureDependencies();
|
||||||
|
|
||||||
|
// Step 2: Get or download base ISO
|
||||||
|
onProgress?.('Obtaining base ISO', 20);
|
||||||
|
const baseIsoPath = await this.getBaseIso(config.iso.ubuntu_version, config.iso.architecture);
|
||||||
|
|
||||||
|
// Step 3: Extract ISO
|
||||||
|
onProgress?.('Extracting ISO', 40);
|
||||||
|
const extractedDir = await this.extractor.extract(baseIsoPath);
|
||||||
|
|
||||||
|
// Step 4: Generate cloud-init configuration
|
||||||
|
onProgress?.('Generating cloud-init configuration', 60);
|
||||||
|
const cloudInitDir = path.join(extractedDir, 'nocloud');
|
||||||
|
await ensureDir(cloudInitDir);
|
||||||
|
|
||||||
|
if (config.cloud_init) {
|
||||||
|
const networkConfig = config.network ? {
|
||||||
|
wifi: config.network.wifi,
|
||||||
|
} : undefined;
|
||||||
|
|
||||||
|
await this.cloudInitGen.writeCloudInitFiles(
|
||||||
|
cloudInitDir,
|
||||||
|
config.cloud_init,
|
||||||
|
networkConfig,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5: Inject custom boot scripts (if any)
|
||||||
|
if (config.boot_scripts && config.boot_scripts.length > 0) {
|
||||||
|
onProgress?.('Injecting boot scripts', 70);
|
||||||
|
await this.injectBootScripts(extractedDir, config.boot_scripts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 6: Update grub configuration to use cloud-init
|
||||||
|
onProgress?.('Updating boot configuration', 80);
|
||||||
|
await this.updateBootConfig(extractedDir);
|
||||||
|
|
||||||
|
// Step 7: Repack ISO
|
||||||
|
onProgress?.('Creating customized ISO', 90);
|
||||||
|
const outputPath = path.join(config.output.path, config.output.filename);
|
||||||
|
await ensureDir(config.output.path);
|
||||||
|
await this.packer.pack(extractedDir, outputPath, 'UBUNTU_CUSTOM');
|
||||||
|
|
||||||
|
// Step 8: Cleanup
|
||||||
|
onProgress?.('Cleaning up', 95);
|
||||||
|
await Deno.remove(extractedDir, { recursive: true });
|
||||||
|
|
||||||
|
onProgress?.('Complete', 100);
|
||||||
|
log.success(`ISO built successfully: ${outputPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get base ISO (from cache or download)
|
||||||
|
*/
|
||||||
|
private async getBaseIso(version: string, arch: 'amd64' | 'arm64'): Promise<string> {
|
||||||
|
// Check cache first
|
||||||
|
const cachedPath = await this.cache.getPath(version, arch);
|
||||||
|
if (cachedPath) {
|
||||||
|
log.info(`Using cached ISO: ${cachedPath}`);
|
||||||
|
return cachedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download to cache
|
||||||
|
log.info(`Downloading Ubuntu ${version} ${arch}...`);
|
||||||
|
return await this.cache.downloadAndCache(version, arch, (downloaded, total) => {
|
||||||
|
const percent = ((downloaded / total) * 100).toFixed(1);
|
||||||
|
process.stdout.write(`\r📥 Download progress: ${percent}%`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject custom boot scripts
|
||||||
|
*/
|
||||||
|
private async injectBootScripts(
|
||||||
|
extractedDir: string,
|
||||||
|
bootScripts: Array<{ name: string; path: string; enable?: boolean }>,
|
||||||
|
): Promise<void> {
|
||||||
|
for (const script of bootScripts) {
|
||||||
|
log.info(`Injecting boot script: ${script.name}`);
|
||||||
|
|
||||||
|
// Copy script to ISO
|
||||||
|
const destPath = path.join(extractedDir, 'scripts', script.name);
|
||||||
|
await ensureDir(path.dirname(destPath));
|
||||||
|
await Deno.copyFile(script.path, destPath);
|
||||||
|
|
||||||
|
// Make executable
|
||||||
|
await Deno.chmod(destPath, 0o755);
|
||||||
|
|
||||||
|
// TODO: Create systemd service if enable is true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update boot configuration to enable cloud-init with nocloud datasource
|
||||||
|
*/
|
||||||
|
private async updateBootConfig(extractedDir: string): Promise<void> {
|
||||||
|
// Update grub config to add cloud-init nocloud datasource
|
||||||
|
const grubCfgPath = path.join(extractedDir, 'boot', 'grub', 'grub.cfg');
|
||||||
|
|
||||||
|
try {
|
||||||
|
let grubContent = await Deno.readTextFile(grubCfgPath);
|
||||||
|
|
||||||
|
// Add cloud-init datasource parameter
|
||||||
|
grubContent = grubContent.replace(
|
||||||
|
/linux\s+\/casper\/vmlinuz/g,
|
||||||
|
'linux /casper/vmlinuz ds=nocloud;s=/cdrom/nocloud/',
|
||||||
|
);
|
||||||
|
|
||||||
|
await Deno.writeTextFile(grubCfgPath, grubContent);
|
||||||
|
log.success('Updated boot configuration for cloud-init');
|
||||||
|
} catch (err) {
|
||||||
|
log.warn(`Could not update grub config: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
212
ts/classes/iso-cache.ts
Normal file
212
ts/classes/iso-cache.ts
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
/**
|
||||||
|
* ISO Cache Manager
|
||||||
|
* Manages cached Ubuntu ISOs with multi-version support
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { log } from '../logging.ts';
|
||||||
|
import { path } from '../plugins.ts';
|
||||||
|
import { getCacheDir, ensureDir } from '../paths.ts';
|
||||||
|
import { IsoDownloader } from './iso-downloader.ts';
|
||||||
|
|
||||||
|
export interface ICacheEntry {
|
||||||
|
version: string;
|
||||||
|
architecture: 'amd64' | 'arm64';
|
||||||
|
filename: string;
|
||||||
|
path: string;
|
||||||
|
size: number;
|
||||||
|
downloadedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IsoCache {
|
||||||
|
private cacheDir: string;
|
||||||
|
private metadataPath: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.cacheDir = getCacheDir();
|
||||||
|
this.metadataPath = path.join(this.cacheDir, 'metadata.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the cache directory
|
||||||
|
*/
|
||||||
|
async init(): Promise<void> {
|
||||||
|
await ensureDir(this.cacheDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all cache entries
|
||||||
|
*/
|
||||||
|
async list(): Promise<ICacheEntry[]> {
|
||||||
|
try {
|
||||||
|
const metadata = await Deno.readTextFile(this.metadataPath);
|
||||||
|
return JSON.parse(metadata);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Deno.errors.NotFound) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific cache entry
|
||||||
|
*/
|
||||||
|
async get(version: string, architecture: 'amd64' | 'arm64'): Promise<ICacheEntry | null> {
|
||||||
|
const entries = await this.list();
|
||||||
|
return entries.find((e) => e.version === version && e.architecture === architecture) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an ISO is cached
|
||||||
|
*/
|
||||||
|
async has(version: string, architecture: 'amd64' | 'arm64'): Promise<boolean> {
|
||||||
|
const entry = await this.get(version, architecture);
|
||||||
|
if (!entry) return false;
|
||||||
|
|
||||||
|
// Verify the file still exists
|
||||||
|
try {
|
||||||
|
const stat = await Deno.stat(entry.path);
|
||||||
|
return stat.isFile;
|
||||||
|
} catch {
|
||||||
|
// File doesn't exist, remove from metadata
|
||||||
|
await this.remove(version, architecture);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an ISO to the cache
|
||||||
|
*/
|
||||||
|
async add(
|
||||||
|
version: string,
|
||||||
|
architecture: 'amd64' | 'arm64',
|
||||||
|
sourcePath: string,
|
||||||
|
): Promise<ICacheEntry> {
|
||||||
|
await this.init();
|
||||||
|
|
||||||
|
const filename = IsoDownloader.getIsoFilename(version, architecture);
|
||||||
|
const destPath = path.join(this.cacheDir, filename);
|
||||||
|
|
||||||
|
// Copy file to cache
|
||||||
|
await Deno.copyFile(sourcePath, destPath);
|
||||||
|
|
||||||
|
// Get file size
|
||||||
|
const stat = await Deno.stat(destPath);
|
||||||
|
|
||||||
|
const entry: ICacheEntry = {
|
||||||
|
version,
|
||||||
|
architecture,
|
||||||
|
filename,
|
||||||
|
path: destPath,
|
||||||
|
size: stat.size,
|
||||||
|
downloadedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update metadata
|
||||||
|
const entries = await this.list();
|
||||||
|
const existingIndex = entries.findIndex(
|
||||||
|
(e) => e.version === version && e.architecture === architecture,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
entries[existingIndex] = entry;
|
||||||
|
} else {
|
||||||
|
entries.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.saveMetadata(entries);
|
||||||
|
log.success(`ISO cached: ${version} ${architecture}`);
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an ISO from the cache
|
||||||
|
*/
|
||||||
|
async remove(version: string, architecture: 'amd64' | 'arm64'): Promise<void> {
|
||||||
|
const entry = await this.get(version, architecture);
|
||||||
|
if (!entry) {
|
||||||
|
log.warn(`ISO not found in cache: ${version} ${architecture}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove file
|
||||||
|
try {
|
||||||
|
await Deno.remove(entry.path);
|
||||||
|
} catch (err) {
|
||||||
|
if (!(err instanceof Deno.errors.NotFound)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update metadata
|
||||||
|
const entries = await this.list();
|
||||||
|
const filtered = entries.filter(
|
||||||
|
(e) => !(e.version === version && e.architecture === architecture),
|
||||||
|
);
|
||||||
|
await this.saveMetadata(filtered);
|
||||||
|
|
||||||
|
log.success(`ISO removed from cache: ${version} ${architecture}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean old cached ISOs
|
||||||
|
*/
|
||||||
|
async clean(olderThanDays?: number): Promise<void> {
|
||||||
|
const entries = await this.list();
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const age = now.getTime() - new Date(entry.downloadedAt).getTime();
|
||||||
|
const ageDays = age / (1000 * 60 * 60 * 24);
|
||||||
|
|
||||||
|
if (!olderThanDays || ageDays > olderThanDays) {
|
||||||
|
await this.remove(entry.version, entry.architecture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path to a cached ISO
|
||||||
|
*/
|
||||||
|
async getPath(version: string, architecture: 'amd64' | 'arm64'): Promise<string | null> {
|
||||||
|
const entry = await this.get(version, architecture);
|
||||||
|
return entry?.path || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download and cache an ISO
|
||||||
|
*/
|
||||||
|
async downloadAndCache(
|
||||||
|
version: string,
|
||||||
|
architecture: 'amd64' | 'arm64',
|
||||||
|
onProgress?: (downloaded: number, total: number) => void,
|
||||||
|
): Promise<string> {
|
||||||
|
await this.init();
|
||||||
|
|
||||||
|
const filename = IsoDownloader.getIsoFilename(version, architecture);
|
||||||
|
const destPath = path.join(this.cacheDir, filename);
|
||||||
|
|
||||||
|
const downloader = new IsoDownloader();
|
||||||
|
|
||||||
|
await downloader.downloadWithVerification({
|
||||||
|
ubuntuVersion: version,
|
||||||
|
architecture,
|
||||||
|
outputPath: destPath,
|
||||||
|
onProgress,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add to cache metadata
|
||||||
|
await this.add(version, architecture, destPath);
|
||||||
|
|
||||||
|
return destPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save metadata to disk
|
||||||
|
*/
|
||||||
|
private async saveMetadata(entries: ICacheEntry[]): Promise<void> {
|
||||||
|
await ensureDir(this.cacheDir);
|
||||||
|
await Deno.writeTextFile(this.metadataPath, JSON.stringify(entries, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
172
ts/classes/iso-downloader.ts
Normal file
172
ts/classes/iso-downloader.ts
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
/**
|
||||||
|
* ISO Downloader
|
||||||
|
* Downloads Ubuntu Server ISOs from official mirrors
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { log } from '../logging.ts';
|
||||||
|
import { path } from '../plugins.ts';
|
||||||
|
|
||||||
|
export interface IDownloadOptions {
|
||||||
|
ubuntuVersion: string; // e.g., "24.04", "22.04"
|
||||||
|
architecture: 'amd64' | 'arm64';
|
||||||
|
outputPath: string;
|
||||||
|
onProgress?: (downloaded: number, total: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IsoDownloader {
|
||||||
|
/**
|
||||||
|
* Ubuntu release URLs
|
||||||
|
*/
|
||||||
|
private static readonly UBUNTU_MIRROR = 'https://releases.ubuntu.com';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ISO filename for a given version and architecture
|
||||||
|
*/
|
||||||
|
static getIsoFilename(version: string, arch: 'amd64' | 'arm64'): string {
|
||||||
|
// Ubuntu Server ISO naming pattern
|
||||||
|
if (arch === 'amd64') {
|
||||||
|
return `ubuntu-${version}-live-server-${arch}.iso`;
|
||||||
|
} else {
|
||||||
|
// ARM64 uses preinstalled server images
|
||||||
|
return `ubuntu-${version}-preinstalled-server-${arch}.img.xz`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the download URL for a given Ubuntu version and architecture
|
||||||
|
*/
|
||||||
|
static getDownloadUrl(version: string, arch: 'amd64' | 'arm64'): string {
|
||||||
|
const filename = this.getIsoFilename(version, arch);
|
||||||
|
return `${this.UBUNTU_MIRROR}/${version}/${filename}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the checksum URL for verification
|
||||||
|
*/
|
||||||
|
static getChecksumUrl(version: string): string {
|
||||||
|
return `${this.UBUNTU_MIRROR}/${version}/SHA256SUMS`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download an Ubuntu ISO
|
||||||
|
*/
|
||||||
|
async download(options: IDownloadOptions): Promise<void> {
|
||||||
|
const { ubuntuVersion, architecture, outputPath, onProgress } = options;
|
||||||
|
const url = IsoDownloader.getDownloadUrl(ubuntuVersion, architecture);
|
||||||
|
|
||||||
|
log.info(`Downloading Ubuntu ${ubuntuVersion} ${architecture} from ${url}`);
|
||||||
|
|
||||||
|
// Download the ISO
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to download ISO: HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalSize = parseInt(response.headers.get('content-length') || '0', 10);
|
||||||
|
let downloadedSize = 0;
|
||||||
|
|
||||||
|
// Open file for writing
|
||||||
|
const file = await Deno.open(outputPath, { write: true, create: true, truncate: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
if (!reader) {
|
||||||
|
throw new Error('Failed to get response body reader');
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
await file.write(value);
|
||||||
|
downloadedSize += value.length;
|
||||||
|
|
||||||
|
if (onProgress && totalSize > 0) {
|
||||||
|
onProgress(downloadedSize, totalSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.success(`ISO downloaded successfully to ${outputPath}`);
|
||||||
|
} finally {
|
||||||
|
file.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download and verify checksum
|
||||||
|
*/
|
||||||
|
async downloadWithVerification(options: IDownloadOptions): Promise<void> {
|
||||||
|
const { ubuntuVersion, architecture, outputPath } = options;
|
||||||
|
|
||||||
|
// Download the ISO
|
||||||
|
await this.download(options);
|
||||||
|
|
||||||
|
// Download checksums
|
||||||
|
log.info('Downloading checksums for verification...');
|
||||||
|
const checksumUrl = IsoDownloader.getChecksumUrl(ubuntuVersion);
|
||||||
|
const checksumResponse = await fetch(checksumUrl);
|
||||||
|
|
||||||
|
if (!checksumResponse.ok) {
|
||||||
|
log.warn('Could not download checksums for verification');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checksumText = await checksumResponse.text();
|
||||||
|
const filename = path.basename(outputPath);
|
||||||
|
|
||||||
|
// Find the checksum for our file
|
||||||
|
const lines = checksumText.split('\n');
|
||||||
|
let expectedChecksum: string | null = null;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.includes(filename)) {
|
||||||
|
expectedChecksum = line.split(/\s+/)[0];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!expectedChecksum) {
|
||||||
|
log.warn(`No checksum found for ${filename}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the checksum
|
||||||
|
log.info('Verifying checksum...');
|
||||||
|
const actualChecksum = await this.calculateSha256(outputPath);
|
||||||
|
|
||||||
|
if (actualChecksum === expectedChecksum) {
|
||||||
|
log.success('Checksum verified successfully ✓');
|
||||||
|
} else {
|
||||||
|
throw new Error(`Checksum mismatch!\nExpected: ${expectedChecksum}\nActual: ${actualChecksum}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate SHA256 checksum of a file
|
||||||
|
*/
|
||||||
|
private async calculateSha256(filePath: string): Promise<string> {
|
||||||
|
const file = await Deno.open(filePath, { read: true });
|
||||||
|
const buffer = new Uint8Array(8192);
|
||||||
|
const hasher = await crypto.subtle.digest('SHA-256', new Uint8Array(0)); // Initialize
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const bytesRead = await file.read(buffer);
|
||||||
|
if (bytesRead === null) break;
|
||||||
|
|
||||||
|
// Hash the chunk
|
||||||
|
const chunk = buffer.slice(0, bytesRead);
|
||||||
|
await crypto.subtle.digest('SHA-256', chunk);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
file.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to hex string
|
||||||
|
const hashArray = Array.from(new Uint8Array(hasher));
|
||||||
|
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
return hashHex;
|
||||||
|
}
|
||||||
|
}
|
||||||
82
ts/classes/iso-extractor.ts
Normal file
82
ts/classes/iso-extractor.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* ISO Extractor
|
||||||
|
* Extracts ISO contents using xorriso
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { log } from '../logging.ts';
|
||||||
|
import { getTempDir, ensureDir, cleanDir } from '../paths.ts';
|
||||||
|
import { path } from '../plugins.ts';
|
||||||
|
|
||||||
|
export class IsoExtractor {
|
||||||
|
/**
|
||||||
|
* Extract an ISO to a directory
|
||||||
|
*/
|
||||||
|
async extract(isoPath: string, extractDir?: string): Promise<string> {
|
||||||
|
// Use temp dir if not specified
|
||||||
|
const outputDir = extractDir || path.join(getTempDir(), `iso-extract-${Date.now()}`);
|
||||||
|
|
||||||
|
await cleanDir(outputDir);
|
||||||
|
await ensureDir(outputDir);
|
||||||
|
|
||||||
|
log.info(`Extracting ISO to ${outputDir}...`);
|
||||||
|
|
||||||
|
// Use xorriso to extract the ISO
|
||||||
|
const command = new Deno.Command('xorriso', {
|
||||||
|
args: [
|
||||||
|
'-osirrox',
|
||||||
|
'on',
|
||||||
|
'-indev',
|
||||||
|
isoPath,
|
||||||
|
'-extract',
|
||||||
|
'/',
|
||||||
|
outputDir,
|
||||||
|
],
|
||||||
|
stdout: 'piped',
|
||||||
|
stderr: 'piped',
|
||||||
|
});
|
||||||
|
|
||||||
|
const process = command.spawn();
|
||||||
|
const { code, stdout, stderr } = await process.output();
|
||||||
|
|
||||||
|
if (code !== 0) {
|
||||||
|
const errorText = new TextDecoder().decode(stderr);
|
||||||
|
throw new Error(`Failed to extract ISO: ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.success(`ISO extracted successfully to ${outputDir}`);
|
||||||
|
return outputDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if xorriso is installed
|
||||||
|
*/
|
||||||
|
async checkDependencies(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const command = new Deno.Command('xorriso', {
|
||||||
|
args: ['--version'],
|
||||||
|
stdout: 'null',
|
||||||
|
stderr: 'null',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { code } = await command.output();
|
||||||
|
return code === 0;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure xorriso is installed
|
||||||
|
*/
|
||||||
|
async ensureDependencies(): Promise<void> {
|
||||||
|
const hasXorriso = await this.checkDependencies();
|
||||||
|
|
||||||
|
if (!hasXorriso) {
|
||||||
|
log.error('xorriso is not installed!');
|
||||||
|
log.info('Install xorriso:');
|
||||||
|
log.info(' Ubuntu/Debian: sudo apt install xorriso');
|
||||||
|
log.info(' macOS: brew install xorriso');
|
||||||
|
throw new Error('Missing dependency: xorriso');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
123
ts/classes/iso-packer.ts
Normal file
123
ts/classes/iso-packer.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* ISO Packer
|
||||||
|
* Repacks a directory into a bootable ISO using xorriso
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { log } from '../logging.ts';
|
||||||
|
|
||||||
|
export class IsoPacker {
|
||||||
|
/**
|
||||||
|
* Pack a directory into an ISO
|
||||||
|
*/
|
||||||
|
async pack(sourceDir: string, outputIso: string, volumeLabel?: string): Promise<void> {
|
||||||
|
log.info(`Creating ISO from ${sourceDir}...`);
|
||||||
|
|
||||||
|
const label = volumeLabel || 'UBUNTU_CUSTOM';
|
||||||
|
|
||||||
|
// Use xorriso to create a bootable ISO
|
||||||
|
const command = new Deno.Command('xorriso', {
|
||||||
|
args: [
|
||||||
|
'-as',
|
||||||
|
'mkisofs',
|
||||||
|
'-r',
|
||||||
|
'-V',
|
||||||
|
label,
|
||||||
|
'-o',
|
||||||
|
outputIso,
|
||||||
|
'-J',
|
||||||
|
'-joliet-long',
|
||||||
|
'-cache-inodes',
|
||||||
|
'-isohybrid-mbr',
|
||||||
|
'/usr/lib/ISOLINUX/isohdpfx.bin',
|
||||||
|
'-b',
|
||||||
|
'isolinux/isolinux.bin',
|
||||||
|
'-c',
|
||||||
|
'isolinux/boot.cat',
|
||||||
|
'-boot-load-size',
|
||||||
|
'4',
|
||||||
|
'-boot-info-table',
|
||||||
|
'-no-emul-boot',
|
||||||
|
'-eltorito-alt-boot',
|
||||||
|
'-e',
|
||||||
|
'boot/grub/efi.img',
|
||||||
|
'-no-emul-boot',
|
||||||
|
'-isohybrid-gpt-basdat',
|
||||||
|
sourceDir,
|
||||||
|
],
|
||||||
|
stdout: 'piped',
|
||||||
|
stderr: 'piped',
|
||||||
|
});
|
||||||
|
|
||||||
|
const process = command.spawn();
|
||||||
|
const { code, stderr } = await process.output();
|
||||||
|
|
||||||
|
if (code !== 0) {
|
||||||
|
const errorText = new TextDecoder().decode(stderr);
|
||||||
|
throw new Error(`Failed to create ISO: ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.success(`ISO created successfully at ${outputIso}`);
|
||||||
|
|
||||||
|
// Make it hybrid bootable (USB compatible)
|
||||||
|
await this.makeIsohybrid(outputIso);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make the ISO hybrid bootable (USB-compatible)
|
||||||
|
*/
|
||||||
|
private async makeIsohybrid(isoPath: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
log.info('Making ISO hybrid bootable...');
|
||||||
|
|
||||||
|
const command = new Deno.Command('isohybrid', {
|
||||||
|
args: ['--uefi', isoPath],
|
||||||
|
stdout: 'piped',
|
||||||
|
stderr: 'piped',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { code } = await command.output();
|
||||||
|
|
||||||
|
if (code === 0) {
|
||||||
|
log.success('ISO is now USB-bootable');
|
||||||
|
} else {
|
||||||
|
log.warn('isohybrid failed, ISO may not be USB-bootable');
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||||
|
log.warn(`isohybrid not available: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if required tools are installed
|
||||||
|
*/
|
||||||
|
async checkDependencies(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const xorrisoCmd = new Deno.Command('xorriso', {
|
||||||
|
args: ['--version'],
|
||||||
|
stdout: 'null',
|
||||||
|
stderr: 'null',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { code } = await xorrisoCmd.output();
|
||||||
|
return code === 0;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure dependencies are installed
|
||||||
|
*/
|
||||||
|
async ensureDependencies(): Promise<void> {
|
||||||
|
const hasXorriso = await this.checkDependencies();
|
||||||
|
|
||||||
|
if (!hasXorriso) {
|
||||||
|
log.error('xorriso is not installed!');
|
||||||
|
log.info('Install xorriso:');
|
||||||
|
log.info(' Ubuntu/Debian: sudo apt install xorriso syslinux-utils');
|
||||||
|
log.info(' macOS: brew install xorriso syslinux');
|
||||||
|
throw new Error('Missing dependency: xorriso');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
275
ts/cli.ts
Normal file
275
ts/cli.ts
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
/**
|
||||||
|
* CLI Interface for isocreator
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { log } from './logging.ts';
|
||||||
|
import { IsoBuilder } from './classes/iso-builder.ts';
|
||||||
|
import { IsoCache } from './classes/iso-cache.ts';
|
||||||
|
import { ConfigManager } from './classes/config-manager.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display help information
|
||||||
|
*/
|
||||||
|
function showHelp() {
|
||||||
|
console.log(`
|
||||||
|
isocreator - Ubuntu ISO Customization Tool
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
isocreator <COMMAND> [OPTIONS]
|
||||||
|
|
||||||
|
COMMANDS:
|
||||||
|
build Build a customized ISO
|
||||||
|
cache Manage ISO cache
|
||||||
|
template Generate config template
|
||||||
|
validate Validate config file
|
||||||
|
version Show version information
|
||||||
|
help Show this help message
|
||||||
|
|
||||||
|
BUILD OPTIONS:
|
||||||
|
--config <file> Use a YAML config file
|
||||||
|
--ubuntu-version <version> Ubuntu version (22.04, 24.04, etc.)
|
||||||
|
--arch <arch> Architecture (amd64 or arm64)
|
||||||
|
--wifi-ssid <ssid> WiFi SSID
|
||||||
|
--wifi-password <password> WiFi password
|
||||||
|
--hostname <name> System hostname
|
||||||
|
--output <path> Output ISO path
|
||||||
|
--ssh-key <path> Path to SSH public key
|
||||||
|
|
||||||
|
CACHE OPTIONS:
|
||||||
|
cache list List cached ISOs
|
||||||
|
cache download <version> Download and cache an ISO
|
||||||
|
cache clean Clean all cached ISOs
|
||||||
|
|
||||||
|
TEMPLATE OPTIONS:
|
||||||
|
template create Generate config template to stdout
|
||||||
|
template create --output <file> Save template to file
|
||||||
|
|
||||||
|
VALIDATE OPTIONS:
|
||||||
|
validate <file> Validate a config file
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
# Build from config file
|
||||||
|
isocreator build --config myconfig.yaml
|
||||||
|
|
||||||
|
# Quick build with CLI flags
|
||||||
|
isocreator build \\
|
||||||
|
--ubuntu-version 24.04 \\
|
||||||
|
--arch amd64 \\
|
||||||
|
--wifi-ssid "MyWiFi" \\
|
||||||
|
--wifi-password "secret123" \\
|
||||||
|
--hostname "myserver" \\
|
||||||
|
--output ./custom-ubuntu.iso
|
||||||
|
|
||||||
|
# Generate config template
|
||||||
|
isocreator template create --output config.yaml
|
||||||
|
|
||||||
|
# List cached ISOs
|
||||||
|
isocreator cache list
|
||||||
|
|
||||||
|
SYSTEM REQUIREMENTS:
|
||||||
|
- xorriso (for ISO manipulation)
|
||||||
|
- syslinux-utils (for USB-bootable ISOs)
|
||||||
|
|
||||||
|
Install on Ubuntu/Debian:
|
||||||
|
sudo apt install xorriso syslinux-utils
|
||||||
|
|
||||||
|
Install on macOS:
|
||||||
|
brew install xorriso syslinux
|
||||||
|
|
||||||
|
For more information, visit:
|
||||||
|
https://code.foss.global/serve.zone/isocreator
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show version information
|
||||||
|
*/
|
||||||
|
function showVersion() {
|
||||||
|
console.log('isocreator version 1.0.0');
|
||||||
|
console.log('Ubuntu ISO customization tool');
|
||||||
|
console.log('https://code.foss.global/serve.zone/isocreator');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse CLI arguments
|
||||||
|
*/
|
||||||
|
function parseArgs(args: string[]): Map<string, string | boolean> {
|
||||||
|
const parsed = new Map<string, string | boolean>();
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while (i < args.length) {
|
||||||
|
const arg = args[i];
|
||||||
|
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.substring(2);
|
||||||
|
const nextArg = args[i + 1];
|
||||||
|
|
||||||
|
if (nextArg && !nextArg.startsWith('--')) {
|
||||||
|
parsed.set(key, nextArg);
|
||||||
|
i += 2;
|
||||||
|
} else {
|
||||||
|
parsed.set(key, true);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parsed.set(`_arg${i}`, arg);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle build command
|
||||||
|
*/
|
||||||
|
async function handleBuild(args: Map<string, string | boolean>) {
|
||||||
|
const builder = new IsoBuilder();
|
||||||
|
const configManager = new ConfigManager();
|
||||||
|
|
||||||
|
// Check if config file is provided
|
||||||
|
if (args.has('config')) {
|
||||||
|
const configPath = args.get('config') as string;
|
||||||
|
const config = await configManager.loadFromFile(configPath);
|
||||||
|
|
||||||
|
await builder.build(config, (step, progress) => {
|
||||||
|
console.log(`[${progress}%] ${step}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Build from CLI flags
|
||||||
|
log.error('CLI flag mode not yet implemented. Please use --config for now.');
|
||||||
|
log.info('Generate a template: isocreator template create --output config.yaml');
|
||||||
|
Deno.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle cache commands
|
||||||
|
*/
|
||||||
|
async function handleCache(args: Map<string, string | boolean>) {
|
||||||
|
const cache = new IsoCache();
|
||||||
|
const subcommand = args.get('_arg1') as string;
|
||||||
|
|
||||||
|
if (!subcommand || subcommand === 'list') {
|
||||||
|
const entries = await cache.list();
|
||||||
|
|
||||||
|
if (entries.length === 0) {
|
||||||
|
console.log('No cached ISOs found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\nCached ISOs:');
|
||||||
|
console.log('━'.repeat(80));
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const sizeMB = (entry.size / 1024 / 1024).toFixed(2);
|
||||||
|
const date = new Date(entry.downloadedAt).toLocaleDateString();
|
||||||
|
console.log(`${entry.version} (${entry.architecture}) - ${sizeMB} MB - ${date}`);
|
||||||
|
console.log(` ${entry.path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('━'.repeat(80));
|
||||||
|
} else if (subcommand === 'clean') {
|
||||||
|
await cache.clean();
|
||||||
|
console.log('Cache cleaned.');
|
||||||
|
} else if (subcommand === 'download') {
|
||||||
|
const version = args.get('_arg2') as string;
|
||||||
|
const arch = (args.get('arch') as string) || 'amd64';
|
||||||
|
|
||||||
|
if (!version) {
|
||||||
|
log.error('Please specify a version: isocreator cache download <version>');
|
||||||
|
Deno.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
await cache.downloadAndCache(version, arch as 'amd64' | 'arm64', (downloaded, total) => {
|
||||||
|
const percent = ((downloaded / total) * 100).toFixed(1);
|
||||||
|
process.stdout.write(`\r📥 Download progress: ${percent}%`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle template commands
|
||||||
|
*/
|
||||||
|
async function handleTemplate(args: Map<string, string | boolean>) {
|
||||||
|
const configManager = new ConfigManager();
|
||||||
|
const subcommand = args.get('_arg1') as string;
|
||||||
|
|
||||||
|
if (!subcommand || subcommand === 'create') {
|
||||||
|
const outputPath = args.get('output') as string | undefined;
|
||||||
|
|
||||||
|
if (outputPath) {
|
||||||
|
await configManager.saveTemplate(outputPath);
|
||||||
|
} else {
|
||||||
|
console.log(configManager.generateTemplate());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle validate command
|
||||||
|
*/
|
||||||
|
async function handleValidate(args: Map<string, string | boolean>) {
|
||||||
|
const configPath = args.get('_arg1') as string;
|
||||||
|
|
||||||
|
if (!configPath) {
|
||||||
|
log.error('Please specify a config file: isocreator validate <file>');
|
||||||
|
Deno.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const configManager = new ConfigManager();
|
||||||
|
try {
|
||||||
|
await configManager.loadFromFile(configPath);
|
||||||
|
log.success('Configuration is valid!');
|
||||||
|
} catch (err) {
|
||||||
|
log.error(`Validation failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
Deno.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main CLI entry point
|
||||||
|
*/
|
||||||
|
export async function startCli() {
|
||||||
|
const args = parseArgs(Deno.args);
|
||||||
|
const command = args.get('_arg0') as string;
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (command) {
|
||||||
|
case 'build':
|
||||||
|
await handleBuild(args);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'cache':
|
||||||
|
await handleCache(args);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'template':
|
||||||
|
await handleTemplate(args);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'validate':
|
||||||
|
await handleValidate(args);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'version':
|
||||||
|
showVersion();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'help':
|
||||||
|
case undefined:
|
||||||
|
showHelp();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.error(`Unknown command: ${command}`);
|
||||||
|
log.info('Run "isocreator help" for usage information');
|
||||||
|
Deno.exit(1);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
Deno.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
ts/index.ts
Normal file
20
ts/index.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* Public API exports for isocreator
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Re-export classes when they're implemented
|
||||||
|
export { IsoBuilder } from './classes/iso-builder.ts';
|
||||||
|
export { IsoCache } from './classes/iso-cache.ts';
|
||||||
|
export { IsoDownloader } from './classes/iso-downloader.ts';
|
||||||
|
export { IsoExtractor } from './classes/iso-extractor.ts';
|
||||||
|
export { IsoPacker } from './classes/iso-packer.ts';
|
||||||
|
export { CloudInitGenerator } from './classes/cloud-init-generator.ts';
|
||||||
|
export { ConfigManager } from './classes/config-manager.ts';
|
||||||
|
|
||||||
|
// Export types
|
||||||
|
export type { IIsoConfig } from './interfaces/iso-config.interface.ts';
|
||||||
|
export type { ICloudInitConfig } from './interfaces/cloud-init-config.interface.ts';
|
||||||
|
|
||||||
|
// Export utilities
|
||||||
|
export * as paths from './paths.ts';
|
||||||
|
export { log, logger } from './logging.ts';
|
||||||
6
ts/interfaces/cloud-init-config.interface.ts
Normal file
6
ts/interfaces/cloud-init-config.interface.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Cloud-init configuration interface
|
||||||
|
* Re-export from iso-config for convenience
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type { ICloudInitConfig } from './iso-config.interface.ts';
|
||||||
97
ts/interfaces/iso-config.interface.ts
Normal file
97
ts/interfaces/iso-config.interface.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* Configuration interface for ISO customization
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface IIsoConfig {
|
||||||
|
version: string;
|
||||||
|
|
||||||
|
// Base ISO settings
|
||||||
|
iso: {
|
||||||
|
ubuntu_version: string; // e.g., "24.04", "22.04"
|
||||||
|
architecture: 'amd64' | 'arm64';
|
||||||
|
flavor?: 'server' | 'desktop'; // default: server
|
||||||
|
};
|
||||||
|
|
||||||
|
// Output settings
|
||||||
|
output: {
|
||||||
|
filename: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Network configuration
|
||||||
|
network?: {
|
||||||
|
wifi?: {
|
||||||
|
ssid: string;
|
||||||
|
password: string;
|
||||||
|
security?: 'wpa2' | 'wpa3';
|
||||||
|
hidden?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cloud-init configuration
|
||||||
|
cloud_init?: ICloudInitConfig;
|
||||||
|
|
||||||
|
// Custom boot scripts
|
||||||
|
boot_scripts?: Array<{
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
enable?: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// Pre-install packages (requires longer build time)
|
||||||
|
preinstall?: {
|
||||||
|
enabled: boolean;
|
||||||
|
packages: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cloud-init configuration interface
|
||||||
|
*/
|
||||||
|
export interface ICloudInitConfig {
|
||||||
|
hostname?: string;
|
||||||
|
|
||||||
|
// Users
|
||||||
|
users?: Array<{
|
||||||
|
name: string;
|
||||||
|
ssh_authorized_keys?: string[];
|
||||||
|
sudo?: string;
|
||||||
|
shell?: string;
|
||||||
|
groups?: string[];
|
||||||
|
lock_passwd?: boolean;
|
||||||
|
passwd?: string; // Hashed password
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// Packages to install on first boot
|
||||||
|
packages?: string[];
|
||||||
|
|
||||||
|
// Package update/upgrade
|
||||||
|
package_update?: boolean;
|
||||||
|
package_upgrade?: boolean;
|
||||||
|
|
||||||
|
// Run commands on first boot
|
||||||
|
runcmd?: string[];
|
||||||
|
|
||||||
|
// Write files
|
||||||
|
write_files?: Array<{
|
||||||
|
path: string;
|
||||||
|
content: string;
|
||||||
|
owner?: string;
|
||||||
|
permissions?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// Timezone
|
||||||
|
timezone?: string;
|
||||||
|
|
||||||
|
// Locale
|
||||||
|
locale?: string;
|
||||||
|
|
||||||
|
// SSH settings
|
||||||
|
ssh?: {
|
||||||
|
password_authentication?: boolean;
|
||||||
|
permit_root_login?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Additional cloud-init directives
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
29
ts/logging.ts
Normal file
29
ts/logging.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Logging utilities for isocreator
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { smartlog } from './plugins.ts';
|
||||||
|
|
||||||
|
// Create logger instance
|
||||||
|
export const logger = new smartlog.Smartlog({
|
||||||
|
logContext: {
|
||||||
|
company: 'Lossless GmbH',
|
||||||
|
companyunit: 'serve.zone',
|
||||||
|
containerName: 'isocreator',
|
||||||
|
environment: 'cli',
|
||||||
|
runtime: 'deno',
|
||||||
|
zone: 'local',
|
||||||
|
},
|
||||||
|
minimumLogLevel: 'info',
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log levels for convenience
|
||||||
|
*/
|
||||||
|
export const log = {
|
||||||
|
info: (message: string) => logger.log('info', message),
|
||||||
|
success: (message: string) => logger.log('info', `✅ ${message}`),
|
||||||
|
warn: (message: string) => logger.log('warn', `⚠️ ${message}`),
|
||||||
|
error: (message: string) => logger.log('error', `❌ ${message}`),
|
||||||
|
debug: (message: string) => logger.log('silly', message),
|
||||||
|
};
|
||||||
63
ts/paths.ts
Normal file
63
ts/paths.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* Path utilities for isocreator
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { path } from './plugins.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user's home directory
|
||||||
|
*/
|
||||||
|
export function getHomeDir(): string {
|
||||||
|
return Deno.env.get('HOME') || Deno.env.get('USERPROFILE') || '/tmp';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the isocreator cache directory
|
||||||
|
*/
|
||||||
|
export function getCacheDir(): string {
|
||||||
|
const home = getHomeDir();
|
||||||
|
return path.join(home, '.isocreator', 'cache');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the isocreator config directory
|
||||||
|
*/
|
||||||
|
export function getConfigDir(): string {
|
||||||
|
const home = getHomeDir();
|
||||||
|
return path.join(home, '.isocreator', 'config');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the isocreator temp directory
|
||||||
|
*/
|
||||||
|
export function getTempDir(): string {
|
||||||
|
const home = getHomeDir();
|
||||||
|
return path.join(home, '.isocreator', 'temp');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure a directory exists
|
||||||
|
*/
|
||||||
|
export async function ensureDir(dirPath: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await Deno.mkdir(dirPath, { recursive: true });
|
||||||
|
} catch (err) {
|
||||||
|
if (!(err instanceof Deno.errors.AlreadyExists)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean a directory (remove and recreate)
|
||||||
|
*/
|
||||||
|
export async function cleanDir(dirPath: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await Deno.remove(dirPath, { recursive: true });
|
||||||
|
} catch (err) {
|
||||||
|
if (!(err instanceof Deno.errors.NotFound)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await ensureDir(dirPath);
|
||||||
|
}
|
||||||
18
ts/plugins.ts
Normal file
18
ts/plugins.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Centralized dependency imports
|
||||||
|
* All external dependencies are imported here for easy management
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Deno standard library
|
||||||
|
export * as path from '@std/path';
|
||||||
|
export * as fs from '@std/fs';
|
||||||
|
export * as yaml from '@std/yaml';
|
||||||
|
export * as assert from '@std/assert';
|
||||||
|
export * as fmt from '@std/fmt';
|
||||||
|
|
||||||
|
// Push.rocks ecosystem
|
||||||
|
export { smartcli } from '@push.rocks/smartcli';
|
||||||
|
export { smartlog } from '@push.rocks/smartlog';
|
||||||
|
export { smartfile } from '@push.rocks/smartfile';
|
||||||
|
export { Deferred } from '@push.rocks/smartpromise';
|
||||||
|
export { smartrequest } from '@push.rocks/smartrequest';
|
||||||
58
uninstall.sh
Executable file
58
uninstall.sh
Executable file
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
INSTALL_DIR="/opt/isocreator"
|
||||||
|
BIN_LINK="/usr/local/bin/isocreator"
|
||||||
|
CACHE_DIR="$HOME/.isocreator"
|
||||||
|
|
||||||
|
echo -e "${BLUE}🗑️ isocreator uninstaller${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Remove binary
|
||||||
|
if [ -f "${INSTALL_DIR}/isocreator" ]; then
|
||||||
|
echo -e "${BLUE}📦 Removing binary from ${INSTALL_DIR}...${NC}"
|
||||||
|
rm -f "${INSTALL_DIR}/isocreator"
|
||||||
|
|
||||||
|
# Remove directory if empty
|
||||||
|
if [ -d "$INSTALL_DIR" ] && [ -z "$(ls -A $INSTALL_DIR)" ]; then
|
||||||
|
rmdir "$INSTALL_DIR"
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN}✅ Binary removed${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠️ Binary not found at ${INSTALL_DIR}${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove symlink
|
||||||
|
if [ -L "$BIN_LINK" ]; then
|
||||||
|
echo -e "${BLUE}🔗 Removing symlink ${BIN_LINK}...${NC}"
|
||||||
|
rm -f "$BIN_LINK"
|
||||||
|
echo -e "${GREEN}✅ Symlink removed${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠️ Symlink not found at ${BIN_LINK}${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ask about cache
|
||||||
|
if [ -d "$CACHE_DIR" ]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}Cache directory found: ${CACHE_DIR}${NC}"
|
||||||
|
read -p "Do you want to remove cached ISOs? (y/N) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo -e "${BLUE}🗑️ Removing cache directory...${NC}"
|
||||||
|
rm -rf "$CACHE_DIR"
|
||||||
|
echo -e "${GREEN}✅ Cache removed${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${BLUE}ℹ️ Cache directory kept${NC}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}✅ isocreator uninstalled successfully!${NC}"
|
||||||
Reference in New Issue
Block a user