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:
856
website/index.html
Normal file
856
website/index.html
Normal 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>
|
||||
Reference in New Issue
Block a user