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>
This commit is contained in:
2026-01-09 13:31:07 -06:00
parent 74943e31f4
commit 2fcf26506a
89 changed files with 167 additions and 249 deletions

856
website/index.html Normal file
View File

@@ -0,0 +1,856 @@
<!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>