platform-deployments/provisioning/configure-static-ip.ts
Quinn Ftw abbef7ae89 refactor: Replace stale infrastructure/ path references after workspace restructure
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>
2026-01-29 00:00:23 -08:00

275 lines
8.8 KiB
TypeScript
Executable file
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.

#!/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()