Files
mac_os/website/index.html
andreas 2fcf26506a Flatten directory structure and update documentation
- Moved all files from mac_os/ subdirectory to repository root
- Updated README.adoc to reflect simplified architecture
- Updated QUICK_INSTALL.md with all current apps
- Added claude-cli to install.sh and bin/install_homebrew_formulas
- Repository now shows clean file structure without nested mac_os folder
- Documentation now accurately describes opinionated installer approach

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-09 13:31:07 -06:00

857 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>macOS Installer Generator</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #f8fafc;
min-height: 100vh;
padding: 20px;
color: #1e293b;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 40px;
}
h1 {
font-size: 2.5rem;
margin-bottom: 10px;
font-weight: 700;
color: #0f172a;
}
.subtitle {
font-size: 1.1rem;
color: #64748b;
}
.card {
background: white;
border-radius: 12px;
padding: 30px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid #e2e8f0;
}
.search-box {
width: 100%;
padding: 12px 20px;
font-size: 16px;
border: 2px solid #e2e8f0;
border-radius: 8px;
margin-bottom: 30px;
transition: border-color 0.3s;
}
.search-box:focus {
outline: none;
border-color: #3b82f6;
}
.category {
margin-bottom: 30px;
}
.category-title {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 15px;
color: #0f172a;
display: flex;
align-items: center;
gap: 10px;
}
.software-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px;
}
.software-item {
display: flex;
align-items: flex-start;
padding: 12px;
border: 1px solid #e2e8f0;
border-radius: 8px;
transition: all 0.2s;
cursor: pointer;
background: white;
}
.software-item:hover {
border-color: #3b82f6;
background: #f0f9ff;
}
.software-item.hidden {
display: none;
}
.software-item input[type="checkbox"] {
margin-right: 12px;
width: 18px;
height: 18px;
cursor: pointer;
flex-shrink: 0;
margin-top: 2px;
accent-color: #3b82f6;
}
.software-info {
flex: 1;
}
.software-name {
font-weight: 600;
color: #0f172a;
margin-bottom: 3px;
}
.software-desc {
font-size: 0.85rem;
color: #64748b;
}
.custom-section {
margin-bottom: 30px;
}
.custom-input-group {
display: grid;
grid-template-columns: 150px 1fr auto;
gap: 12px;
margin-bottom: 12px;
}
.custom-input {
padding: 10px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
}
.custom-input:focus {
outline: none;
border-color: #3b82f6;
}
select.custom-input {
cursor: pointer;
}
.actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
margin-top: 20px;
}
.btn {
padding: 14px 24px;
font-size: 16px;
font-weight: 600;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-primary {
background: #3b82f6;
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #2563eb;
}
.btn-secondary {
background: #10b981;
color: white;
}
.btn-secondary:hover:not(:disabled) {
background: #059669;
}
.btn-tertiary {
background: #f59e0b;
color: white;
}
.btn-tertiary:hover:not(:disabled) {
background: #d97706;
}
.btn-small {
padding: 8px 16px;
font-size: 14px;
background: #ef4444;
color: white;
}
.btn-small:hover {
background: #dc2626;
}
.btn-add {
background: #8b5cf6;
color: white;
}
.btn-add:hover {
background: #7c3aed;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.stats {
display: flex;
gap: 20px;
margin-bottom: 20px;
padding: 15px;
background: #f8fafc;
border-radius: 8px;
}
.stat {
flex: 1;
text-align: center;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: #3b82f6;
}
.stat-label {
font-size: 0.9rem;
color: #64748b;
margin-top: 5px;
}
.preview {
background: #1e293b;
color: #e2e8f0;
padding: 20px;
border-radius: 8px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 13px;
max-height: 400px;
overflow-y: auto;
line-height: 1.6;
display: none;
}
.preview.visible {
display: block;
}
.custom-items-list {
margin-top: 15px;
}
.custom-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
background: #f8fafc;
border-radius: 6px;
margin-bottom: 8px;
}
.custom-item-info {
display: flex;
align-items: center;
gap: 12px;
}
.type-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
.type-brew {
background: #dbeafe;
color: #1e40af;
}
.type-cask {
background: #d1fae5;
color: #065f46;
}
.type-mas {
background: #fef3c7;
color: #92400e;
}
footer {
text-align: center;
color: #64748b;
margin-top: 40px;
}
.toast {
position: fixed;
bottom: 20px;
right: 20px;
background: #10b981;
color: white;
padding: 16px 24px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateY(100px);
opacity: 0;
transition: all 0.3s;
}
.toast.show {
transform: translateY(0);
opacity: 1;
}
@media (max-width: 768px) {
h1 {
font-size: 2rem;
}
.software-grid {
grid-template-columns: 1fr;
}
.actions {
grid-template-columns: 1fr;
}
.custom-input-group {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>🍎 macOS Installer Generator</h1>
<p class="subtitle">Select your software and generate a one-line install script</p>
</header>
<div class="card">
<div class="stats">
<div class="stat">
<div class="stat-value" id="selectedCount">0</div>
<div class="stat-label">Selected</div>
</div>
<div class="stat">
<div class="stat-value" id="customCount">0</div>
<div class="stat-label">Custom</div>
</div>
<div class="stat">
<div class="stat-value" id="totalCount">0</div>
<div class="stat-label">Available</div>
</div>
</div>
<input type="text"
class="search-box"
id="searchBox"
placeholder="🔍 Search software..."
autocomplete="off">
<div class="custom-section">
<div class="category-title"> Add Custom Software</div>
<div class="custom-input-group">
<select class="custom-input" id="customType">
<option value="brew">Homebrew Formula</option>
<option value="cask">Homebrew Cask</option>
<option value="mas">App Store (ID)</option>
</select>
<input type="text" class="custom-input" id="customName" placeholder="Package name or App Store ID">
<button class="btn btn-add btn-small" id="addCustomBtn">Add</button>
</div>
<div class="custom-items-list" id="customItemsList"></div>
</div>
<div id="categories"></div>
<div class="actions">
<button class="btn btn-primary" id="generateBtn" disabled>
📝 Generate Script
</button>
<button class="btn btn-secondary" id="copyBtn" disabled>
📋 Copy to Clipboard
</button>
<button class="btn btn-tertiary" id="shareBtn" disabled>
🔗 Share URL
</button>
</div>
<div class="preview" id="preview"></div>
</div>
<footer>
<p>Open source • MIT License • <a href="https://github.com" style="color: #3b82f6;">Contribute on GitHub</a></p>
</footer>
</div>
<div class="toast" id="toast"></div>
<script>
// Software catalog
const SOFTWARE = {
'Development': [
{ name: 'node', desc: 'Node.js JavaScript runtime', type: 'brew' },
{ name: 'python', desc: 'Python programming language', type: 'brew' },
{ name: 'go', desc: 'Go programming language', type: 'brew' },
{ name: 'rust', desc: 'Rust programming language', type: 'brew' },
{ name: 'git', desc: 'Version control system', type: 'brew' },
{ name: 'gh', desc: 'GitHub CLI', type: 'brew' },
{ name: 'docker', desc: 'Container platform', type: 'cask' },
{ name: 'visual-studio-code', desc: 'VS Code editor', type: 'cask' },
{ name: 'iterm2', desc: 'Terminal emulator', type: 'cask' },
{ name: 'sublime-text', desc: 'Text editor', type: 'cask' },
{ name: 'nova', desc: 'Nova editor', type: 'cask' },
{ name: 'codex', desc: 'AI coding assistant', type: 'cask' },
{ name: 'steipete/tap/codexbar', desc: 'AI assistant menu bar', type: 'cask' },
{ name: 'postman', desc: 'API development', type: 'cask' },
],
'Shell & Terminal': [
{ name: 'zsh', desc: 'Z shell', type: 'brew' },
{ name: 'bash', desc: 'Bash shell', type: 'brew' },
{ name: 'bash-completion', desc: 'Bash completions', type: 'brew' },
{ name: 'fish', desc: 'Fish shell', type: 'brew' },
{ name: 'starship', desc: 'Cross-shell prompt', type: 'brew' },
{ name: 'atuin', desc: 'Shell history manager', type: 'brew' },
{ name: 'tmux', desc: 'Terminal multiplexer', type: 'brew' },
{ name: 'vim', desc: 'Vim text editor', type: 'brew' },
{ name: 'neovim', desc: 'Neovim text editor', type: 'brew' },
],
'Browsers': [
{ name: 'google-chrome', desc: 'Google Chrome', type: 'cask' },
{ name: 'firefox', desc: 'Mozilla Firefox', type: 'cask' },
{ name: 'brave-browser', desc: 'Brave Browser', type: 'cask' },
{ name: 'eloston-chromium', desc: 'Ungoogled Chromium', type: 'cask' },
{ name: 'arc', desc: 'Arc Browser', type: 'cask' },
],
'Communication': [
{ name: 'slack', desc: 'Team communication', type: 'cask' },
{ name: 'discord', desc: 'Voice and chat', type: 'cask' },
{ name: 'zoom', desc: 'Video conferencing', type: 'cask' },
{ name: 'signal', desc: 'Encrypted messaging', type: 'cask' },
{ name: 'element', desc: 'Matrix client', type: 'cask' },
{ name: 'telegram', desc: 'Telegram messenger', type: 'cask' },
{ name: 'whatsapp', desc: 'WhatsApp messenger', type: 'cask' },
{ name: 'deepl', desc: 'DeepL translator', type: 'cask' },
],
'Productivity': [
{ name: 'notion', desc: 'Notes and docs', type: 'cask' },
{ name: 'obsidian', desc: 'Knowledge base', type: 'cask' },
{ name: 'rectangle', desc: 'Window manager', type: 'cask' },
{ name: 'alfred', desc: 'Productivity app', type: 'cask' },
{ name: 'raycast', desc: 'Command launcher', type: 'cask' },
{ name: '1password', desc: 'Password manager', type: 'cask' },
],
'Mac App Store': [
{ name: '1352778147', desc: 'Bitwarden', type: 'mas' },
{ name: '497799835', desc: 'Xcode', type: 'mas' },
{ name: '409203825', desc: 'Numbers', type: 'mas' },
{ name: '409201541', desc: 'Pages', type: 'mas' },
{ name: '409183694', desc: 'Keynote', type: 'mas' },
{ name: '1295203466', desc: 'Microsoft Remote Desktop', type: 'mas' },
{ name: '1475387142', desc: 'Tailscale', type: 'mas' },
{ name: '1278508951', desc: 'Trello', type: 'mas' },
{ name: '1569813296', desc: '1Password for Safari', type: 'mas' },
{ name: '441258766', desc: 'Magnet', type: 'mas' },
{ name: '1451685025', desc: 'WireGuard', type: 'mas' },
{ name: '1480933944', desc: 'Vimari', type: 'mas' },
],
'Media': [
{ name: 'ffmpeg', desc: 'Media processing', type: 'brew' },
{ name: 'imagemagick', desc: 'Image manipulation', type: 'brew' },
{ name: 'vlc', desc: 'VLC media player', type: 'cask' },
{ name: 'spotify', desc: 'Music streaming', type: 'cask' },
{ name: 'iina', desc: 'Modern media player', type: 'cask' },
{ name: 'handbrake', desc: 'Video transcoder', type: 'cask' },
{ name: 'trimmy', desc: 'Video trimming tool', type: 'cask' },
],
'Cloud & Sync': [
{ name: 'nextcloud', desc: 'Nextcloud client', type: 'cask' },
{ name: 'dropbox', desc: 'Dropbox sync', type: 'cask' },
{ name: 'google-drive', desc: 'Google Drive', type: 'cask' },
{ name: 'proton-drive', desc: 'Proton Drive', type: 'cask' },
{ name: 'proton-mail', desc: 'Proton Mail', type: 'cask' },
{ name: 'protonvpn', desc: 'Proton VPN', type: 'cask' },
],
'Utilities': [
{ name: 'mas', desc: 'Mac App Store CLI', type: 'brew' },
{ name: 'wget', desc: 'File downloader', type: 'brew' },
{ name: 'curl', desc: 'Transfer tool', type: 'brew' },
{ name: 'jq', desc: 'JSON processor', type: 'brew' },
{ name: 'tree', desc: 'Directory tree', type: 'brew' },
{ name: 'htop', desc: 'System monitor', type: 'brew' },
{ name: 'rename', desc: 'Rename utility', type: 'brew' },
{ name: 'mole', desc: 'SSH tunneling', type: 'brew' },
{ name: 'ykman', desc: 'YubiKey manager', type: 'brew' },
{ name: 'the-unarchiver', desc: 'Archive extractor', type: 'cask' },
{ name: 'appcleaner', desc: 'App uninstaller', type: 'cask' },
{ name: 'transmit', desc: 'FTP client', type: 'cask' },
],
'AI Tools': [
{ name: 'claude-cli', desc: 'Claude AI CLI', type: 'brew' },
{ name: 'gemini-cli', desc: 'Gemini AI CLI', type: 'brew' },
{ name: 'ollama', desc: 'Run LLMs locally', type: 'brew' },
{ name: 'codex', desc: 'AI coding assistant', type: 'cask' },
{ name: 'steipete/tap/codexbar', desc: 'AI assistant menu bar', type: 'cask' },
],
};
// State
let selected = new Set();
let customItems = [];
let generatedScript = '';
// Initialize
function init() {
renderCategories();
updateStats();
loadFromURL();
setupEventListeners();
}
// Render categories
function renderCategories() {
const container = document.getElementById('categories');
let totalCount = 0;
Object.entries(SOFTWARE).forEach(([category, items]) => {
totalCount += items.length;
const categoryDiv = document.createElement('div');
categoryDiv.className = 'category';
categoryDiv.innerHTML = `
<div class="category-title">${category}</div>
<div class="software-grid" data-category="${category}">
${items.map(item => `
<label class="software-item" data-name="${item.name}" data-type="${item.type}">
<input type="checkbox" value="${item.name}" data-type="${item.type}">
<div class="software-info">
<div class="software-name">${item.desc}</div>
<div class="software-desc">${item.name} (${item.type})</div>
</div>
</label>
`).join('')}
</div>
`;
container.appendChild(categoryDiv);
});
document.getElementById('totalCount').textContent = totalCount;
}
// Setup event listeners
function setupEventListeners() {
// Checkbox changes
document.addEventListener('change', (e) => {
if (e.target.type === 'checkbox') {
const key = `${e.target.dataset.type}:${e.target.value}`;
if (e.target.checked) {
selected.add(key);
} else {
selected.delete(key);
}
updateStats();
updateURL();
}
});
// Search
document.getElementById('searchBox').addEventListener('input', (e) => {
const query = e.target.value.toLowerCase();
document.querySelectorAll('.software-item').forEach(item => {
const text = item.textContent.toLowerCase();
item.classList.toggle('hidden', !text.includes(query));
});
});
// Custom software
document.getElementById('addCustomBtn').addEventListener('click', addCustomItem);
document.getElementById('customName').addEventListener('keypress', (e) => {
if (e.key === 'Enter') addCustomItem();
});
// Buttons
document.getElementById('generateBtn').addEventListener('click', generateScript);
document.getElementById('copyBtn').addEventListener('click', copyScript);
document.getElementById('shareBtn').addEventListener('click', shareURL);
}
// Add custom item
function addCustomItem() {
const type = document.getElementById('customType').value;
const name = document.getElementById('customName').value.trim();
if (!name) {
showToast('Please enter a package name', true);
return;
}
const item = { type, name };
customItems.push(item);
renderCustomItems();
updateStats();
updateURL();
document.getElementById('customName').value = '';
showToast('Custom item added!');
}
// Remove custom item
function removeCustomItem(index) {
customItems.splice(index, 1);
renderCustomItems();
updateStats();
updateURL();
}
// Render custom items
function renderCustomItems() {
const container = document.getElementById('customItemsList');
if (customItems.length === 0) {
container.innerHTML = '';
return;
}
container.innerHTML = customItems.map((item, index) => `
<div class="custom-item">
<div class="custom-item-info">
<span class="type-badge type-${item.type}">${item.type}</span>
<span>${item.name}</span>
</div>
<button class="btn btn-small" onclick="removeCustomItem(${index})">Remove</button>
</div>
`).join('');
}
// Expose removeCustomItem to global scope
window.removeCustomItem = removeCustomItem;
// Update stats
function updateStats() {
const count = selected.size;
document.getElementById('selectedCount').textContent = count;
document.getElementById('customCount').textContent = customItems.length;
const hasSelection = count > 0 || customItems.length > 0;
document.getElementById('generateBtn').disabled = !hasSelection;
document.getElementById('copyBtn').disabled = !generatedScript;
document.getElementById('shareBtn').disabled = !hasSelection;
}
// Generate script
function generateScript() {
const brews = [];
const casks = [];
const masApps = [];
selected.forEach(item => {
const [type, name] = item.split(':');
if (type === 'brew') brews.push(name);
else if (type === 'cask') casks.push(name);
else if (type === 'mas') masApps.push(name);
});
customItems.forEach(item => {
if (item.type === 'brew') brews.push(item.name);
else if (item.type === 'cask') casks.push(item.name);
else if (item.type === 'mas') masApps.push(item.name);
});
// Extract custom taps from casks
const taps = new Set();
const casksWithTaps = [];
const regularCasks = [];
casks.forEach(cask => {
if (cask.includes('/')) {
const tap = cask.substring(0, cask.lastIndexOf('/'));
taps.add(tap);
casksWithTaps.push(cask);
} else {
regularCasks.push(cask);
}
});
generatedScript = `#!/usr/bin/env bash
#
# macOS Installation Script
# Generated by install.due.ren
#
set -e
echo "🍎 Starting macOS setup..."
# Install Homebrew
if ! command -v brew &> /dev/null; then
echo "📦 Installing Homebrew..."
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
if [[ "$(/usr/bin/arch)" == "arm64" ]]; then
BREW_PATH="/opt/homebrew/bin/brew"
else
BREW_PATH="/usr/local/bin/brew"
fi
echo "eval \\"\$($BREW_PATH shellenv)\\"" >> "$HOME/.zprofile"
eval "$($BREW_PATH shellenv)"
fi
echo "✅ Homebrew ready"
${taps.size > 0 ? `
# Add custom Homebrew taps
echo "🔌 Adding custom taps..."
${Array.from(taps).map(tap => `brew tap ${tap}`).join('\n')}
` : ''}${brews.length > 0 ? `
# Install Homebrew Formulas
echo "🔧 Installing CLI tools..."
brew install ${brews.join(' ')}
` : ''}${regularCasks.length > 0 ? `
# Install Homebrew Casks
echo "📱 Installing applications..."
brew install --cask ${regularCasks.join(' ')}
` : ''}${casksWithTaps.length > 0 ? `
# Install custom tap casks
echo "📱 Installing custom tap applications..."
${casksWithTaps.map(cask => `brew install --cask ${cask}`).join('\n')}
` : ''}${masApps.length > 0 ? `
# Install Mac App Store apps
if command -v mas &> /dev/null; then
echo "🏪 Installing App Store apps..."
${masApps.map(id => `mas install ${id}`).join('\n ')}
else
echo "⚠️ Install 'mas' to install App Store apps: brew install mas"
fi
` : ''}
echo "🎉 Installation complete!"
`;
document.getElementById('preview').textContent = generatedScript;
document.getElementById('preview').classList.add('visible');
updateStats();
showToast('Script generated!');
}
// Copy to clipboard
async function copyScript() {
try {
await navigator.clipboard.writeText(generatedScript);
showToast('Copied to clipboard!');
} catch (err) {
showToast('Failed to copy', true);
}
}
// Share URL
function shareURL() {
const url = window.location.href;
navigator.clipboard.writeText(url);
showToast('URL copied to clipboard!');
}
// Update URL with selections
function updateURL() {
const brews = [];
const casks = [];
const mas = [];
selected.forEach(item => {
const [type, name] = item.split(':');
if (type === 'brew') brews.push(name);
else if (type === 'cask') casks.push(name);
else if (type === 'mas') mas.push(name);
});
customItems.forEach(item => {
if (item.type === 'brew') brews.push(item.name);
else if (item.type === 'cask') casks.push(item.name);
else if (item.type === 'mas') mas.push(item.name);
});
const params = new URLSearchParams();
if (brews.length) params.set('brew', brews.join(','));
if (casks.length) params.set('cask', casks.join(','));
if (mas.length) params.set('mas', mas.join(','));
const newURL = params.toString() ? `?${params.toString()}` : window.location.pathname;
history.replaceState(null, '', newURL);
}
// Load from URL
function loadFromURL() {
const params = new URLSearchParams(window.location.search);
['brew', 'cask', 'mas'].forEach(type => {
const items = params.get(type);
if (items) {
items.split(',').forEach(name => {
const checkbox = document.querySelector(`input[value="${name}"][data-type="${type}"]`);
if (checkbox) {
checkbox.checked = true;
selected.add(`${type}:${name}`);
} else {
// Add as custom item if not found in catalog
customItems.push({ type, name });
}
});
}
});
renderCustomItems();
updateStats();
}
// Show toast notification
function showToast(message, isError = false) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.style.background = isError ? '#ef4444' : '#10b981';
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 3000);
}
// Start the app
init();
</script>
</body>
</html>