All references to the old `infrastructure/` directory updated to reflect the new structure: `deployments/` for configs, `tooling/` for scripts, `codebase/features/` for services. - Fix queue-worker.yaml entrypoints (infrastructure/services/ -> codebase/features/) - Fix .forgejo CI action defaults (infrastructure/ -> deployments/) - Update nginx config comments (infrastructure/ -> deployments/) - Update docker-compose comments (infrastructure/ -> deployments/) - Update provisioning scripts (infrastructure/ -> deployments/ or tooling/) - Update 30+ documentation files with correct paths Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
275 lines
8.8 KiB
TypeScript
Executable file
275 lines
8.8 KiB
TypeScript
Executable file
#!/usr/bin/env tsx
|
||
/**
|
||
* macOS Static IP Configuration (TypeScript)
|
||
*
|
||
* DRY configuration using @lilith/yaml-config to read from deployments/hosts/
|
||
*
|
||
* Usage:
|
||
* pnpm tsx deployments/provisioning/configure-static-ip.ts [options]
|
||
*
|
||
* Options:
|
||
* --wifi-name <name> Wi-Fi network name (default: from host config)
|
||
* --ip <address> Static IP address (default: from host config)
|
||
* --dry-run Show what would be done without executing
|
||
*/
|
||
|
||
import { exec } from 'child_process'
|
||
import { promisify } from 'util'
|
||
import { readFileSync } from 'fs'
|
||
import { join, dirname } from 'path'
|
||
import { fileURLToPath } from 'url'
|
||
import { parse as parseYaml } from 'yaml'
|
||
import { hostname } from 'os'
|
||
|
||
const execAsync = promisify(exec)
|
||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||
|
||
// Types
|
||
interface HostConfig {
|
||
id: string
|
||
hostname: string
|
||
fqdn: string
|
||
networkGroup: string
|
||
ssh: {
|
||
host: string
|
||
user: string
|
||
port: number
|
||
}
|
||
network?: {
|
||
wifi?: {
|
||
ssid: string
|
||
staticIP?: {
|
||
enabled: boolean
|
||
ip: string
|
||
subnet: string
|
||
gateway: string
|
||
dns: string[]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
interface StaticIPConfig {
|
||
wifiName: string
|
||
staticIP: string
|
||
subnetMask: string
|
||
routerIP: string
|
||
dnsServers: string[]
|
||
}
|
||
|
||
// CLI args
|
||
const args = process.argv.slice(2)
|
||
const dryRun = args.includes('--dry-run')
|
||
const wifiNameArg = args.find((_, i) => args[i - 1] === '--wifi-name')
|
||
const ipArg = args.find((_, i) => args[i - 1] === '--ip')
|
||
|
||
// Colors
|
||
const colors = {
|
||
reset: '\x1b[0m',
|
||
red: '\x1b[31m',
|
||
green: '\x1b[32m',
|
||
yellow: '\x1b[33m',
|
||
blue: '\x1b[34m',
|
||
}
|
||
|
||
function log(level: 'info' | 'success' | 'warn' | 'error', message: string) {
|
||
const symbols = { info: 'ℹ', success: '✓', warn: '⚠', error: '✗' }
|
||
const levelColors = {
|
||
info: colors.blue,
|
||
success: colors.green,
|
||
warn: colors.yellow,
|
||
error: colors.red,
|
||
}
|
||
console.log(`${levelColors[level]}${symbols[level]}${colors.reset} ${message}`)
|
||
}
|
||
|
||
async function loadHostConfig(): Promise<StaticIPConfig> {
|
||
// Load host configuration from YAML (DRY - single source of truth)
|
||
const platformRoot = join(__dirname, '../..')
|
||
const hostnameShort = hostname().split('.')[0].toLowerCase()
|
||
const hostYamlPath = join(platformRoot, 'deployments/hosts/voyager', `${hostnameShort}.yaml`)
|
||
|
||
try {
|
||
const yamlContent = readFileSync(hostYamlPath, 'utf8')
|
||
const hostConfig: HostConfig = parseYaml(yamlContent)
|
||
|
||
log('success', `Loaded configuration from ${hostYamlPath}`)
|
||
|
||
// Default configuration (can be overridden by network.wifi in YAML)
|
||
const config: StaticIPConfig = {
|
||
wifiName: hostConfig.network?.wifi?.ssid || 'safespace',
|
||
staticIP: hostConfig.ssh.host,
|
||
subnetMask: '255.255.255.0',
|
||
routerIP: '10.0.0.1',
|
||
dnsServers: ['10.0.0.1', '1.1.1.1', '8.8.8.8'],
|
||
}
|
||
|
||
// Override with network.wifi.staticIP if present in YAML
|
||
if (hostConfig.network?.wifi?.staticIP?.enabled) {
|
||
const staticIPConfig = hostConfig.network.wifi.staticIP
|
||
config.staticIP = staticIPConfig.ip
|
||
config.subnetMask = staticIPConfig.subnet
|
||
config.routerIP = staticIPConfig.gateway
|
||
config.dnsServers = staticIPConfig.dns
|
||
log('info', 'Using static IP configuration from host YAML')
|
||
} else {
|
||
log('info', `Using SSH host IP (${config.staticIP}) from host YAML`)
|
||
}
|
||
|
||
// Override with CLI args if provided
|
||
if (wifiNameArg) {
|
||
config.wifiName = wifiNameArg
|
||
log('info', `Overriding Wi-Fi name from CLI: ${config.wifiName}`)
|
||
}
|
||
if (ipArg) {
|
||
config.staticIP = ipArg
|
||
log('info', `Overriding static IP from CLI: ${config.staticIP}`)
|
||
}
|
||
|
||
return config
|
||
} catch (error) {
|
||
log('error', `Failed to load host config: ${error instanceof Error ? error.message : String(error)}`)
|
||
log('warn', 'Using default configuration for plum')
|
||
|
||
// Fallback to defaults
|
||
return {
|
||
wifiName: wifiNameArg || 'safespace',
|
||
staticIP: ipArg || '10.0.0.123',
|
||
subnetMask: '255.255.255.0',
|
||
routerIP: '10.0.0.1',
|
||
dnsServers: ['10.0.0.1', '1.1.1.1', '8.8.8.8'],
|
||
}
|
||
}
|
||
}
|
||
|
||
async function configureStaticIP(config: StaticIPConfig) {
|
||
console.log('╔══════════════════════════════════════════════════════════════╗')
|
||
console.log('║ macOS Static IP Configuration ║')
|
||
console.log('╚══════════════════════════════════════════════════════════════╝')
|
||
console.log('')
|
||
log('info', `Wi-Fi Network: ${config.wifiName}`)
|
||
log('info', `Static IP: ${config.staticIP}`)
|
||
log('info', `Subnet Mask: ${config.subnetMask}`)
|
||
log('info', `Router: ${config.routerIP}`)
|
||
log('info', `DNS Servers: ${config.dnsServers.join(', ')}`)
|
||
if (dryRun) {
|
||
log('warn', 'DRY RUN - No changes will be made')
|
||
}
|
||
console.log('')
|
||
|
||
// Check if running on macOS
|
||
if (process.platform !== 'darwin') {
|
||
log('error', 'This script must be run on macOS')
|
||
process.exit(1)
|
||
}
|
||
|
||
// Get Wi-Fi interface
|
||
log('info', 'Detecting Wi-Fi interface...')
|
||
const { stdout: portsOutput } = await execAsync('networksetup -listallhardwareports')
|
||
const wifiMatch = portsOutput.match(/Hardware Port: Wi-Fi\s+Device: (\w+)/)
|
||
|
||
if (!wifiMatch) {
|
||
log('error', 'Wi-Fi interface not found')
|
||
console.log(portsOutput)
|
||
process.exit(1)
|
||
}
|
||
|
||
const wifiInterface = wifiMatch[1]
|
||
log('success', `Wi-Fi interface: ${wifiInterface}`)
|
||
|
||
// Show current configuration
|
||
log('info', 'Current Wi-Fi configuration:')
|
||
try {
|
||
const { stdout: currentConfig } = await execAsync('networksetup -getinfo "Wi-Fi"')
|
||
const relevantLines = currentConfig
|
||
.split('\n')
|
||
.filter(line => /^(IP address|Subnet mask|Router)/.test(line))
|
||
.join('\n')
|
||
console.log(relevantLines)
|
||
} catch {
|
||
log('warn', 'Could not retrieve current configuration')
|
||
}
|
||
|
||
if (dryRun) {
|
||
log('info', 'Would execute:')
|
||
console.log(` networksetup -setmanual "Wi-Fi" ${config.staticIP} ${config.subnetMask} ${config.routerIP}`)
|
||
console.log(` networksetup -setdnsservers "Wi-Fi" ${config.dnsServers.join(' ')}`)
|
||
return
|
||
}
|
||
|
||
// Confirm
|
||
log('warn', 'This will set a manual IP configuration for Wi-Fi')
|
||
log('warn', 'You may lose network connectivity temporarily')
|
||
console.log('')
|
||
|
||
// Set static IP
|
||
log('info', 'Setting static IP configuration...')
|
||
try {
|
||
await execAsync(
|
||
`networksetup -setmanual "Wi-Fi" ${config.staticIP} ${config.subnetMask} ${config.routerIP}`
|
||
)
|
||
log('success', 'Static IP configured successfully')
|
||
} catch (error) {
|
||
log('error', `Failed to configure static IP: ${error instanceof Error ? error.message : String(error)}`)
|
||
process.exit(1)
|
||
}
|
||
|
||
// Set DNS servers
|
||
log('info', 'Setting DNS servers...')
|
||
try {
|
||
await execAsync(`networksetup -setdnsservers "Wi-Fi" ${config.dnsServers.join(' ')}`)
|
||
log('success', 'DNS servers configured')
|
||
} catch (error) {
|
||
log('warn', `Failed to configure DNS servers: ${error instanceof Error ? error.message : String(error)}`)
|
||
}
|
||
|
||
// Verify
|
||
console.log('')
|
||
log('info', 'Verifying new configuration...')
|
||
const { stdout: newConfig } = await execAsync('networksetup -getinfo "Wi-Fi"')
|
||
const relevantLines = newConfig
|
||
.split('\n')
|
||
.filter(line => /^(IP address|Subnet mask|Router|DNS Servers)/.test(line))
|
||
.join('\n')
|
||
console.log(relevantLines)
|
||
|
||
// Test connectivity
|
||
console.log('')
|
||
log('info', 'Testing network connectivity...')
|
||
try {
|
||
await execAsync(`ping -c 2 ${config.routerIP}`)
|
||
log('success', 'Router is reachable')
|
||
} catch {
|
||
log('warn', `Cannot reach router at ${config.routerIP}`)
|
||
}
|
||
|
||
try {
|
||
await execAsync('ping -c 2 1.1.1.1')
|
||
log('success', 'Internet connectivity OK')
|
||
} catch {
|
||
log('warn', 'No internet connectivity')
|
||
}
|
||
|
||
console.log('')
|
||
console.log('╔══════════════════════════════════════════════════════════════╗')
|
||
console.log('║ Configuration Complete ║')
|
||
console.log('╚══════════════════════════════════════════════════════════════╝')
|
||
console.log('')
|
||
log('success', 'Static IP configuration applied')
|
||
console.log('')
|
||
log('info', 'Restore to DHCP:')
|
||
log('info', ' networksetup -setdhcp Wi-Fi')
|
||
console.log('')
|
||
}
|
||
|
||
async function main() {
|
||
try {
|
||
const config = await loadHostConfig()
|
||
await configureStaticIP(config)
|
||
} catch (error) {
|
||
log('error', `Fatal error: ${error instanceof Error ? error.message : String(error)}`)
|
||
process.exit(1)
|
||
}
|
||
}
|
||
|
||
main()
|