// pal - Your personal data, everywhere // // A modern CLI for managing your digital life across all devices. // ============================================================================= // Types // ============================================================================= type Status = | Ok | Warn | Err | None // ============================================================================= // Color output helpers (using shell printf for ANSI codes) // ============================================================================= fn print(msg: String): Unit with {Process} = { let ignore = Process.exec("printf '%s\\n' \"" + msg + "\" >&2") () } fn printColor(color: String, msg: String): Unit with {Process} = { let code = match color { "green" => "32", "yellow" => "33", "red" => "31", "blue" => "34", "magenta" => "35", "cyan" => "36", "bold" => "1", "dim" => "2", _ => "0" } let ignore = Process.exec("printf '\\033[" + code + "m%s\\033[0m\\n' \"" + msg + "\" >&2") () } fn printBold(msg: String): Unit with {Process} = { let ignore = Process.exec("printf '\\033[1m%s\\033[0m\\n' \"" + msg + "\" >&2") () } fn printSuccess(msg: String): Unit with {Process} = { let ignore = Process.exec("printf '\\033[32m✓\\033[0m %s\\n' \"" + msg + "\" >&2") () } fn printWarn(msg: String): Unit with {Process} = { let ignore = Process.exec("printf '\\033[33m⚠\\033[0m %s\\n' \"" + msg + "\" >&2") () } fn printErr(msg: String): Unit with {Process} = { let ignore = Process.exec("printf '\\033[31m✗\\033[0m %s\\n' \"" + msg + "\" >&2") () } fn printInfo(msg: String): Unit with {Process} = { let ignore = Process.exec("printf '\\033[36m→\\033[0m %s\\n' \"" + msg + "\" >&2") () } fn printDim(msg: String): Unit with {Process} = { let ignore = Process.exec("printf '\\033[2m%s\\033[0m\\n' \"" + msg + "\" >&2") () } fn printBox(title: String, content: String): Unit with {Process} = { let ignore = Process.exec("printf '\\033[36m┌─\\033[1m %s \\033[0m\\033[36m─┐\\033[0m\\n' \"" + title + "\" >&2") let ignore2 = Process.exec("printf '\\033[36m│\\033[0m %s\\n' \"" + content + "\" >&2") let ignore3 = Process.exec("printf '\\033[36m└──────────────────────────────────────────┘\\033[0m\\n' >&2") () } fn spinner(msg: String): Unit with {Process} = { let ignore = Process.exec("printf '\\033[36m⠋\\033[0m %s\\r' \"" + msg + "\" >&2") () } fn clearLine(): Unit with {Process} = { let ignore = Process.exec("printf '\\033[2K\\r' >&2") () } // ============================================================================= // Modern output helpers (gh/cargo style) // ============================================================================= // Print a step with aligned status: " Creating directories done" fn printStep(label: String, status: String): Unit with {Process} = { // Pad label to 24 chars for alignment let ignore = Process.exec("printf ' %-24s \\033[32m%s\\033[0m\\n' \"" + label + "\" \"" + status + "\" >&2") () } fn printStepPending(label: String): Unit with {Process} = { let ignore = Process.exec("printf ' %-24s \\033[2mrunning...\\033[0m\\r' \"" + label + "\" >&2") () } fn printStepWarn(label: String, status: String): Unit with {Process} = { let ignore = Process.exec("printf ' %-24s \\033[33m%s\\033[0m\\n' \"" + label + "\" \"" + status + "\" >&2") () } fn printStepErr(label: String, status: String): Unit with {Process} = { let ignore = Process.exec("printf ' %-24s \\033[31m%s\\033[0m\\n' \"" + label + "\" \"" + status + "\" >&2") () } // Print a status line: " sync Running 3 folders, 2 devices" fn printStatusLine(name: String, state: String, detail: String): Unit with {Process} = { let stateColor = match state { "Running" => "32", "Ready" => "32", "Mounted" => "32", "Configured" => "32", "Stopped" => "33", "Not running" => "33", "Not mounted" => "2", "Not configured" => "2", _ => "0" } let ignore = Process.exec("printf ' \\033[1m%-10s\\033[0m \\033[" + stateColor + "m%-16s\\033[0m \\033[2m%s\\033[0m\\n' \"" + name + "\" \"" + state + "\" \"" + detail + "\" >&2") () } // Ask for confirmation: returns true if user confirms fn askConfirm(question: String, defaultYes: Bool): Bool with {Console, Process} = { let prompt = if defaultYes then " [Y/n] " else " [y/N] " let ignore = Process.exec("printf '%s%s' \"" + question + "\" \"" + prompt + "\" >&2") let response = String.trim(Console.readLine()) let lower = execQuiet("echo '" + response + "' | tr A-Z a-z") if String.length(lower) == 0 then defaultYes else if lower == "y" then true else if lower == "yes" then true else if lower == "n" then false else if lower == "no" then false else defaultYes } // Ask for text input with a default value fn askInput(prompt: String, defaultVal: String): String with {Console, Process} = { let display = if String.length(defaultVal) > 0 then "printf '%s [%s]: ' \"" + prompt + "\" \"" + defaultVal + "\" >&2" else "printf '%s: ' \"" + prompt + "\" >&2" let ignore = Process.exec(display) let input = String.trim(Console.readLine()) if String.length(input) == 0 then defaultVal else input } // Print section header fn printHeader(title: String): Unit with {Process} = { let ignore = Process.exec("printf '\\033[1m%s\\033[0m\\n\\n' \"" + title + "\" >&2") () } // Print a hint/suggestion fn printHint(msg: String): Unit with {Process} = { let ignore = Process.exec("printf '\\033[2m%s\\033[0m\\n' \"" + msg + "\" >&2") () } // Print a command suggestion fn printCmd(cmd: String): Unit with {Process} = { let ignore = Process.exec("printf ' \\033[32m\\044\\033[0m %s\\n' \"" + cmd + "\" >&2") () } // Print a QR code for easy scanning (e.g., device IDs) fn printQR(data: String): Unit with {Process} = { if hasCommand("qrencode") then { // Generate small terminal QR code and redirect to stderr let ignore = Process.exec("qrencode -t ANSIUTF8 -m 1 '" + data + "' -o - >&2") () } else { // Fallback: just show the data printHint("(Install qrencode for QR codes)") } } // ============================================================================= // Shell helpers // ============================================================================= fn hasCommand(cmd: String): Bool with {Process} = { let result = Process.exec("command -v " + cmd + " 2>/dev/null || true") String.length(String.trim(result)) > 0 } fn execQuiet(cmd: String): String with {Process} = String.trim(Process.exec(cmd + " 2>/dev/null || true")) fn homeDir(): String with {Process} = match Process.env("HOME") { Some(h) => h, None => "/tmp" } fn isYes(s: String): Bool = String.contains(s, "yes") fn isNo(s: String): Bool = String.contains(s, "no") fn isActive(s: String): Bool = String.contains(s, "active") fn palDir(): String with {Process} = homeDir() + "/.config/pal" fn palConfig(): String with {Process} = palDir() + "/pal.toml" fn stateDb(): String with {Process} = palDir() + "/state.db" // ============================================================================= // Initialization // ============================================================================= fn isInitialized(): Bool with {Process} = { let result = execQuiet("test -f " + palConfig() + " && echo yes || echo no") result |> isYes } fn ensureDir(path: String): Unit with {Process} = { let ignore = Process.exec("mkdir -p " + path) () } // ============================================================================= // State tracking // ============================================================================= fn initStateDb(): Unit with {Process} = { let db = stateDb() let schema = "CREATE TABLE IF NOT EXISTS events (id INTEGER PRIMARY KEY, timestamp TEXT, type TEXT, message TEXT);" let ignore = Process.exec("sqlite3 " + db + " \"" + schema + "\" 2>/dev/null || true") () } fn logEvent(eventType: String, message: String): Unit with {Process} = { let db = stateDb() let timestamp = execQuiet("date -Iseconds") let sql = "INSERT INTO events (timestamp, type, message) VALUES ('" + timestamp + "', '" + eventType + "', '" + message + "');" let ignore = Process.exec("sqlite3 " + db + " \"" + sql + "\" 2>/dev/null || true") () } fn getRecentEvents(limit: String): String with {Process} = { let db = stateDb() execQuiet("sqlite3 -separator ' | ' " + db + " \"SELECT timestamp, type, message FROM events ORDER BY id DESC LIMIT " + limit + ";\" 2>/dev/null") } // ============================================================================= // Syncthing helpers // ============================================================================= fn palSyncthingRunning(): Bool with {Process} = { let result = execQuiet("curl -s http://127.0.0.1:8385/rest/system/status 2>/dev/null | head -c 1") String.length(result) > 0 } fn getSyncFolderCount(): String with {Process} = execQuiet("syncthing cli --home=" + palDir() + "/syncthing/config show config 2>/dev/null | jq -r '.folders | length' 2>/dev/null || echo 0") fn getSyncDeviceCount(): String with {Process} = execQuiet("syncthing cli --home=" + palDir() + "/syncthing/config show config 2>/dev/null | jq -r '.devices | length' 2>/dev/null || echo 0") // ============================================================================= // Backup helpers // ============================================================================= fn getBackupRepo(): String with {Process} = execQuiet("cat " + palDir() + "/restic/repository 2>/dev/null") fn getBackupPassword(): String with {Process} = palDir() + "/restic/password" // ============================================================================= // Logo and branding // ============================================================================= fn showLogo(): Unit with {Process} = { let ignore = Process.exec("printf '\\033[36m _\\n' >&2") let ignore2 = Process.exec("printf ' _ __ __ _| |\\n' >&2") let ignore3 = Process.exec("printf ' | '\"'\"'_ \\\\/ _'\"'\"' | |\\n' >&2") let ignore4 = Process.exec("printf ' | |_) | (_| | |\\n' >&2") let ignore5 = Process.exec("printf ' | .__/ \\\\__,_|_|\\n' >&2") let ignore6 = Process.exec("printf ' |_|\\n\\033[0m' >&2") print("") printDim(" Your personal data, everywhere.") print("") } fn showMiniLogo(): Unit with {Process} = { let ignore = Process.exec("printf '\\033[1;36mpal\\033[0m ' >&2") () } // ============================================================================= // Welcome flow (Interactive) // ============================================================================= fn showInteractiveWelcome(): Unit with {Console, Process} = { print("") showLogo() print("") printBold("Welcome to pal!") print("") printHint("pal manages your data across all your devices:") print("") let ignore = Process.exec("printf ' \\033[1mconfig\\033[0m Machine configuration (git-managed)\\n' >&2") let ignore2 = Process.exec("printf ' \\033[1msync\\033[0m Notes & documents (real-time sync)\\n' >&2") let ignore3 = Process.exec("printf ' \\033[1mbackup\\033[0m Point-in-time snapshots\\n' >&2") let ignore4 = Process.exec("printf ' \\033[1mserver\\033[0m Large files on your server\\n' >&2") print("") // Interactive choice printBold("What would you like to do?") print("") let ignore5 = Process.exec("printf ' \\033[36m1\\033[0m Set up a new system\\n' >&2") let ignore6 = Process.exec("printf ' \\033[36m2\\033[0m Join an existing setup (import)\\n' >&2") let ignore7 = Process.exec("printf ' \\033[36m3\\033[0m Just explore (show help)\\n' >&2") print("") let ignore8 = Process.exec("printf 'Choice [1]: ' >&2") let choice = String.trim(Console.readLine()) match choice { "2" => { print("") printBold("Import from archive") print("") printHint("Provide a pal export archive:") printCmd("pal import pal-export.tar.zst") print("") }, "3" => showHelp(), _ => { print("") doOnboard() } } } // Non-interactive welcome for scripts fn showWelcome(): Unit with {Process} = { print("") showLogo() print("") printBold("Not initialized. Run:") printCmd("pal setup") print("") } // ============================================================================= // Setup wizard // ============================================================================= fn createDirectories(): Unit with {Process} = { let base = palDir() ensureDir(base) ensureDir(base + "/config-repo") ensureDir(base + "/syncthing/config") ensureDir(base + "/syncthing/db") ensureDir(base + "/sync/notes") ensureDir(base + "/sync/documents") ensureDir(base + "/sync/dotfiles") ensureDir(base + "/restic/cache") ensureDir(base + "/server") } fn doSetup(): Unit with {Console, Process} = { print("") if isInitialized() then { printWarn("pal is already initialized on this device.") print("") let rerun = askConfirm("Re-run setup? This will overwrite your config.", false) if rerun == false then { print("") printHint("No changes made.") print("") () } else { print("") doSetupSteps() } } else { doSetupSteps() } } fn doSetupSteps(): Unit with {Process} = { print("") printHeader("Setting up pal") // Step 1: Directories printStepPending("Creating directories") createDirectories() printStep("Creating directories", "done") // Step 2: Config printStepPending("Writing configuration") let hostname = execQuiet("hostname") let configContent = "[pal]\nversion = 1\ndevice_name = \"" + hostname + "\"\n\n[sync]\ngui_port = 8385\nsync_port = 22001\n\n[backup]\nschedule = \"hourly\"\n" let writeCmd = "cat > " + palConfig() + " << 'EOF'\n" + configContent + "EOF" let ignore = Process.exec(writeCmd) printStep("Writing configuration", "done") // Step 3: Age key printStepPending("Setting up encryption") let hasAgeKey = execQuiet("test -f " + palDir() + "/age-key.txt && echo yes || echo no") if hasAgeKey |> isYes then printStep("Setting up encryption", "exists") else { let ignore2 = Process.exec("age-keygen -o " + palDir() + "/age-key.txt 2>&1 || true") printStep("Setting up encryption", "done") } // Step 4: Syncthing printStepPending("Initializing sync") if hasCommand("syncthing") then { let hasConfig = execQuiet("test -f " + palDir() + "/syncthing/config/config.xml && echo yes || echo no") if hasConfig |> isNo then { let ignore3 = Process.exec("syncthing generate --home=" + palDir() + "/syncthing/config 2>&1") printStep("Initializing sync", "done") } else { printStep("Initializing sync", "ready") } } else { printStepWarn("Initializing sync", "not installed") } // State DB if hasCommand("sqlite3") then { initStateDb() logEvent("setup", "pal initialized") } else () print("") printSuccess("Setup complete!") print("") printHint("Your data lives in: " + palDir()) print("") printBold("Next steps:") printCmd("pal sync setup # Pair with other devices") printCmd("pal backup init # Configure backups") print("") } // ============================================================================= // Init from repo // ============================================================================= fn doInit(repoUrl: String): Unit with {Process} = { print("") if String.length(repoUrl) == 0 then { printHeader("Initialize from repository") printHint("Clone a config repository to set up this machine:") print("") printCmd("pal init https://github.com/you/pal-config") print("") } else { printHeader("Initializing from repository") printStepPending("Creating directories") createDirectories() printStep("Creating directories", "done") printStepPending("Cloning repository") let cloneResult = Process.exec("git clone " + repoUrl + " " + palDir() + "/config-repo 2>&1") if String.contains(cloneResult, "fatal") then { printStepErr("Cloning repository", "failed") print("") printDim(cloneResult) } else { printStep("Cloning repository", "done") let hasFlake = execQuiet("test -f " + palDir() + "/config-repo/flake.nix && echo yes || echo no") if hasFlake |> isYes then { printStep("Found flake.nix", "yes") print("") printBold("Apply your config:") printCmd("cd " + palDir() + "/config-repo") printCmd("sudo nixos-rebuild switch --flake .") } else { printStepWarn("Found flake.nix", "no") } logEvent("init", "Cloned " + repoUrl) } print("") } } // ============================================================================= // Sync commands // ============================================================================= fn doSyncStatus(): Unit with {Process} = { print("") if hasCommand("syncthing") == false then { printErr("Syncthing not installed") printHint("Install syncthing and run 'pal sync setup'") } else if palSyncthingRunning() then { let folders = getSyncFolderCount() let devices = getSyncDeviceCount() printStatusLine("sync", "Running", folders + " folders, " + devices + " devices") print("") let ignore = Process.exec("printf ' Web UI: \\033[4;36mhttp://127.0.0.1:8385\\033[0m\\n' >&2") } else { printStatusLine("sync", "Stopped", "") print("") printCmd("pal sync start") } print("") } fn doSyncSetup(): Unit with {Process} = { print("") if hasCommand("syncthing") == false then { printErr("Syncthing not installed") printHint("Add to NixOS: services.pal.enable = true;") print("") } else { printHeader("Setting up sync") let hasConfig = execQuiet("test -f " + palDir() + "/syncthing/config/config.xml && echo yes || echo no") if hasConfig |> isNo then { printStepPending("Creating config") let ignore = Process.exec("syncthing generate --home=" + palDir() + "/syncthing/config 2>&1") printStep("Creating config", "done") } else { printStep("Creating config", "exists") } // Start if not running if palSyncthingRunning() == false then { printStepPending("Starting daemon") let ignore = Process.exec("syncthing serve --home=" + palDir() + "/syncthing/config --no-browser --gui-address=127.0.0.1:8385 >/dev/null 2>&1 &") let ignore2 = Process.exec("sleep 2") printStep("Starting daemon", "done") } else { printStep("Starting daemon", "running") } let deviceId = execQuiet("syncthing cli --home=" + palDir() + "/syncthing/config show system 2>/dev/null | grep myID | cut -d'\"' -f4") if String.length(deviceId) > 10 then { print("") printBold("Your Device ID") let ignore = Process.exec("printf '\\033[33m%s\\033[0m\\n' \"" + deviceId + "\" >&2") print("") printQR(deviceId) print("") printHint("Scan the QR code or share this ID to pair devices.") logEvent("sync", "Device ID displayed") } else { printStepWarn("Getting device ID", "retry in a moment") } print("") } } fn doSyncStart(): Unit with {Process} = { print("") if palSyncthingRunning() then { printSuccess("Syncthing is already running") let ignore = Process.exec("printf ' \\033[4;36mhttp://127.0.0.1:8385\\033[0m\\n' >&2") } else { printStepPending("Starting Syncthing") let ignore = Process.exec("syncthing serve --home=" + palDir() + "/syncthing/config --no-browser --gui-address=127.0.0.1:8385 >/dev/null 2>&1 &") let ignore2 = Process.exec("sleep 2") if palSyncthingRunning() then { printStep("Starting Syncthing", "done") print("") let ignore3 = Process.exec("printf ' Web UI: \\033[4;36mhttp://127.0.0.1:8385\\033[0m\\n' >&2") } else { printStepErr("Starting Syncthing", "failed") } } print("") } fn doSync(): Unit with {Process} = { print("") if palSyncthingRunning() then { let ignore = Process.exec("syncthing cli --home=" + palDir() + "/syncthing/config scan 2>/dev/null || true") printSuccess("Sync triggered") logEvent("sync", "Manual sync") } else { printWarn("Syncthing not running") printCmd("pal sync start") } print("") } // ============================================================================= // Backup commands // ============================================================================= fn doBackupInit(repoArg: String): Unit with {Process} = { print("") if hasCommand("restic") == false then { printErr("Restic not installed") print("") } else if String.length(repoArg) == 0 then { printHeader("Initialize backup repository") printHint("Provide a repository location:") print("") printCmd("pal backup init /path/to/backup") printCmd("pal backup init sftp:server:/backups") printCmd("pal backup init b2:bucket:path") print("") } else { printHeader("Initializing backup") printHint("Repository: " + repoArg) print("") let passwordFile = palDir() + "/restic/password" let hasPassword = execQuiet("test -f " + passwordFile + " && echo yes || echo no") if hasPassword |> isNo then { printStepPending("Generating password") let ignore = Process.exec("head -c 32 /dev/urandom | base64 > " + passwordFile + " && chmod 600 " + passwordFile) printStep("Generating password", "done") } else { printStep("Generating password", "exists") } printStepPending("Creating repository") let initResult = Process.exec("restic -r " + repoArg + " --password-file " + passwordFile + " init 2>&1") if String.contains(initResult, "created restic repository") then { printStep("Creating repository", "done") let ignore = Process.exec("echo '" + repoArg + "' > " + palDir() + "/restic/repository") printStep("Saving config", "done") print("") printSuccess("Backup initialized!") printCmd("pal backup # Create your first snapshot") logEvent("backup", "Initialized " + repoArg) } else if String.contains(initResult, "already initialized") then { printStep("Creating repository", "exists") let ignore = Process.exec("echo '" + repoArg + "' > " + palDir() + "/restic/repository") } else { printStepErr("Creating repository", "failed") print("") printDim(initResult) } print("") } } fn doBackupList(): Unit with {Process} = { print("") let repo = getBackupRepo() if String.length(repo) == 0 then { printWarn("No backup configured") printCmd("pal backup init ") } else { printHeader("Backup snapshots") printHint("Repository: " + repo) print("") // Format snapshots nicely let ignore = Process.exec("printf ' \\033[2m%-10s %-20s %s\\033[0m\\n' 'ID' 'DATE' 'PATHS' >&2") let ignore2 = Process.exec("printf ' \\033[2m%s\\033[0m\\n' '─────────────────────────────────────────────────' >&2") let snapshots = Process.exec("restic -r " + repo + " --password-file " + getBackupPassword() + " snapshots --json 2>&1 | jq -r '.[] | \" \" + .short_id + \" \" + .time[:19] + \" \" + (.paths | join(\", \"))' 2>/dev/null || echo ' No snapshots'") print(snapshots) } print("") } fn doBackup(): Unit with {Process} = { print("") let repo = getBackupRepo() if String.length(repo) == 0 then { printWarn("No backup configured") printCmd("pal backup init ") } else { printStepPending("Creating snapshot") let backupResult = Process.exec("restic -r " + repo + " --password-file " + getBackupPassword() + " backup " + palDir() + "/sync 2>&1") if String.contains(backupResult, "snapshot") then { let snapshotId = execQuiet("echo '" + backupResult + "' | grep -o 'snapshot [a-f0-9]*' | cut -d' ' -f2") if String.length(snapshotId) > 0 then { printStep("Creating snapshot", snapshotId) } else { printStep("Creating snapshot", "done") } logEvent("backup", "Backup completed") } else { printStepErr("Creating snapshot", "failed") print("") printDim(backupResult) } } print("") } // ============================================================================= // Server commands // ============================================================================= fn doServerStatus(): Unit with {Process} = { print("") let serverDir = palDir() + "/server" let isMounted = execQuiet("mountpoint -q " + serverDir + " && echo yes || echo no") if isMounted |> isYes then { let dfOut = execQuiet("df -h " + serverDir + " 2>/dev/null | tail -1 | tr -s ' ' | cut -d' ' -f3,2 | sed 's/ / \\/ /'") printStatusLine("server", "Mounted", dfOut) printHint(" " + serverDir) } else { printStatusLine("server", "Not mounted", "") print("") printCmd("pal server mount") } print("") } fn doServerMount(): Unit with {Process} = { print("") let serverDir = palDir() + "/server" let isMounted = execQuiet("mountpoint -q " + serverDir + " && echo yes || echo no") if isMounted |> isYes then { printSuccess("Already mounted at " + serverDir) } else { printHeader("Mount server") printHint("Mount a remote filesystem to access large files, media, and archives.") print("") // SSHFS section printBold("SSHFS (recommended for most users)") printHint("Mounts remote directories over SSH. No server config needed.") print("") printCmd("sshfs user@server:/path " + serverDir) print("") printHint("Useful options:") print(" -o reconnect Auto-reconnect on connection loss") print(" -o follow_symlinks Follow symbolic links") print(" -o cache=yes Enable caching for better performance") print("") printHint("Example with options:") printCmd("sshfs -o reconnect,cache=yes user@server:/data " + serverDir) print("") // NFS section printBold("NFS (for local network)") printHint("Faster than SSHFS but requires NFS server setup.") print("") printCmd("sudo mount -t nfs server:/export " + serverDir) print("") // NixOS section printBold("NixOS declarative mount") printHint("Add to configuration.nix for automatic mounting.") printHint("See: nixos.wiki/wiki/SSHFS") print("") // Unmount hint printHint("To unmount: pal server unmount") } print("") } fn doServerUnmount(): Unit with {Process} = { print("") let serverDir = palDir() + "/server" let isMounted = execQuiet("mountpoint -q " + serverDir + " && echo yes || echo no") if isMounted |> isYes then { printStepPending("Unmounting") let ignore = Process.exec("fusermount -u " + serverDir + " 2>&1 || umount " + serverDir + " 2>&1 || true") let stillMounted = execQuiet("mountpoint -q " + serverDir + " && echo yes || echo no") if stillMounted |> isNo then printStep("Unmounting", "done") else printStepErr("Unmounting", "failed") } else { printHint("Server is not mounted") } print("") } // ============================================================================= // Export/Import // ============================================================================= fn doExport(): Unit with {Console, Process} = { print("") let timestamp = execQuiet("date +%Y-%m-%d") let exportFile = "pal-export-" + timestamp + ".tar.zst" let exportPath = execQuiet("pwd") + "/" + exportFile printStepPending("Creating archive") let ignore = Process.exec("tar -C " + palDir() + " -cf - . 2>/dev/null | zstd -q > " + exportPath + " 2>&1") let fileExists = execQuiet("test -f " + exportPath + " && echo yes || echo no") if fileExists |> isYes then { let fileSize = execQuiet("du -h " + exportPath + " | cut -f1") printStep("Creating archive", fileSize) print("") printSuccess("Export created: " + exportPath) print("") printHint("Restore with:") printCmd("pal import " + exportFile) logEvent("export", "Created " + exportFile) } else { printStepErr("Creating archive", "failed") } print("") } fn doImport(archivePath: String): Unit with {Console, Process} = { print("") if String.length(archivePath) == 0 then { printHeader("Import pal data") printHint("Restore from a pal export archive:") print("") printCmd("pal import pal-export.tar.zst") print("") } else { let fileExists = execQuiet("test -f " + archivePath + " && echo yes || echo no") if fileExists |> isNo then { printErr("File not found: " + archivePath) print("") } else { // Check if there's existing data let hasPal = execQuiet("test -d " + palDir() + " && echo yes || echo no") if hasPal |> isYes then { print("") printWarn("This will replace your existing pal data.") printHint("Existing data will be moved to ~/.config/pal.bak") print("") let confirmed = askConfirm("Continue?", false) if confirmed then { print("") printHeader("Importing") printStepPending("Backing up existing") let ignore = Process.exec("mv " + palDir() + " " + palDir() + ".bak 2>&1") printStep("Backing up existing", "done") printStepPending("Extracting archive") let ignore2 = Process.exec("mkdir -p " + palDir()) let ignore3 = Process.exec("zstd -d < " + archivePath + " | tar -C " + palDir() + " -xf - 2>&1") printStep("Extracting archive", "done") printStepPending("Verifying config") let configExists = execQuiet("test -f " + palConfig() + " && echo yes || echo no") if configExists |> isYes then { printStep("Verifying config", "done") print("") printSuccess("Import complete.") logEvent("import", "Imported from " + archivePath) } else { printStepErr("Verifying config", "failed") print("") printErr("Import failed - config not found in archive") } print("") } else { print("") printHint("Import cancelled.") print("") } } else { // No existing data, proceed without confirmation printHeader("Importing") printStepPending("Extracting archive") let ignore = Process.exec("mkdir -p " + palDir()) let ignore2 = Process.exec("zstd -d < " + archivePath + " | tar -C " + palDir() + " -xf - 2>&1") printStep("Extracting archive", "done") printStepPending("Verifying config") let configExists = execQuiet("test -f " + palConfig() + " && echo yes || echo no") if configExists |> isYes then { printStep("Verifying config", "done") print("") printSuccess("Import complete.") logEvent("import", "Imported from " + archivePath) } else { printStepErr("Verifying config", "failed") print("") printErr("Import failed - config not found in archive") } print("") } } } } // ============================================================================= // Status dashboard // ============================================================================= fn showStatus(): Unit with {Process} = { print("") if isInitialized() == false then { printWarn("Not initialized") printCmd("pal setup") } else { printHeader("Status") // Config let hasRepo = execQuiet("test -d " + palDir() + "/config-repo/.git && echo yes || echo no") if hasRepo |> isYes then { let repoRemote = execQuiet("cd " + palDir() + "/config-repo && git remote get-url origin 2>/dev/null | sed 's/.*\\///' | sed 's/.git//'") printStatusLine("config", "Linked", repoRemote) } else { printStatusLine("config", "Not linked", "") } // Sync if palSyncthingRunning() then { let folders = getSyncFolderCount() let devices = getSyncDeviceCount() printStatusLine("sync", "Running", folders + " folders, " + devices + " devices") } else { printStatusLine("sync", "Not running", "") } // Backup let repo = getBackupRepo() if String.length(repo) > 0 then { let repoShort = execQuiet("echo '" + repo + "' | sed 's/.*\\///'") printStatusLine("backup", "Configured", repoShort) } else { printStatusLine("backup", "Not configured", "") } // Server let serverDir = palDir() + "/server" let isMounted = execQuiet("mountpoint -q " + serverDir + " && echo yes || echo no") if isMounted |> isYes then { printStatusLine("server", "Mounted", serverDir) } else { printStatusLine("server", "Not mounted", "") } } print("") } // ============================================================================= // Compact Status (default command when initialized) // ============================================================================= fn showCompactStatus(): Unit with {Process} = { let syncOk = palSyncthingRunning() let hasBackup = String.length(getBackupRepo()) > 0 let folders = if syncOk then getSyncFolderCount() else "0" let devices = if syncOk then getSyncDeviceCount() else "0" // Header line let ignore = Process.exec("printf '\\033[1;36mpal\\033[0m ' >&2") if syncOk && hasBackup then { let ignore2 = Process.exec("printf '\\033[32mAll systems operational\\033[0m\\n' >&2") } else { let ignore2 = Process.exec("printf '\\033[33mPartially configured\\033[0m\\n' >&2") } print("") // Sync status if syncOk then { printStatusLine("sync", "Running", folders + " folders, " + devices + " devices") } else { printStatusLine("sync", "Not running", "Run: pal sync start") } // Backup status if hasBackup then { let snapCount = execQuiet("restic -r " + getBackupRepo() + " --password-file " + getBackupPassword() + " snapshots --json 2>/dev/null | jq -r 'length' 2>/dev/null || echo 0") let lastBackup = execQuiet("restic -r " + getBackupRepo() + " --password-file " + getBackupPassword() + " snapshots --json 2>/dev/null | jq -r '.[-1].time[:10]' 2>/dev/null || echo 'unknown'") printStatusLine("backup", "Ready", snapCount + " snapshots, last " + lastBackup) } else { printStatusLine("backup", "Not configured", "Run: pal backup init") } // Server status let serverDir = palDir() + "/server" let isMounted = execQuiet("mountpoint -q " + serverDir + " && echo yes || echo no") if isMounted |> isYes then { printStatusLine("server", "Mounted", serverDir) } else { printStatusLine("server", "Not mounted", "") } print("") } // ============================================================================= // Health check (default command) // ============================================================================= fn showHealthCheck(): Unit with {Console, Process} = { // Check if this is an interactive terminal let isTty = execQuiet("test -t 0 && echo yes || echo no") if isInitialized() == false then { // Interactive wizard for first run if isTty |> isYes then showInteractiveWelcome() else showWelcome() } else { showCompactStatus() } } // ============================================================================= // Doctor // ============================================================================= fn showDoctor(): Unit with {Process} = { print("") printHeader("System check") // Check tools let ignore = Process.exec("printf '\\033[2mDependencies:\\033[0m\\n' >&2") if hasCommand("nix") then { let ver = execQuiet("nix --version | cut -d' ' -f3") printStep("nix", ver) } else { printStepErr("nix", "not found") } if hasCommand("git") then { let ver = execQuiet("git --version | cut -d' ' -f3") printStep("git", ver) } else { printStepErr("git", "not found") } if hasCommand("syncthing") then { if palSyncthingRunning() then printStep("syncthing", "running") else printStepWarn("syncthing", "installed, not running") } else { printStepErr("syncthing", "not found") } if hasCommand("restic") then { printStep("restic", "installed") } else { printStepErr("restic", "not found") } if hasCommand("age") then { let hasKey = execQuiet("test -f " + palDir() + "/age-key.txt && echo yes || echo no") if hasKey |> isYes then printStep("age", "key exists") else printStepWarn("age", "no key") } else { printStepErr("age", "not found") } if hasCommand("qrencode") then { printStep("qrencode", "installed") } else { printStepWarn("qrencode", "not found (QR codes disabled)") } // Directory check print("") let ignore2 = Process.exec("printf '\\033[2mDirectories:\\033[0m\\n' >&2") checkDir("config-repo") checkDir("syncthing") checkDir("sync") checkDir("restic") checkDir("server") print("") } fn checkDir(dir: String): Unit with {Process} = { let exists = execQuiet("test -d " + palDir() + "/" + dir + " && echo yes || echo no") if exists |> isYes then { printStep(dir + "/", "exists") } else { printStepWarn(dir + "/", "missing") } } // ============================================================================= // History // ============================================================================= fn showHistory(): Unit with {Process} = { print("") if hasCommand("sqlite3") == false then { printErr("sqlite3 not installed") } else { let dbExists = execQuiet("test -f " + stateDb() + " && echo yes || echo no") if dbExists |> isYes then { printHeader("Recent events") let ignore = Process.exec("printf ' \\033[2m%-20s %-10s %s\\033[0m\\n' 'TIME' 'TYPE' 'DETAILS' >&2") let ignore2 = Process.exec("printf ' \\033[2m%s\\033[0m\\n' '──────────────────────────────────────────────────' >&2") // Format events nicely let events = execQuiet("sqlite3 -separator ' ' " + stateDb() + " \"SELECT substr(timestamp,1,16), type, message FROM events ORDER BY id DESC LIMIT 15;\" 2>/dev/null | sed 's/^/ /'") if String.length(events) > 0 then { print(events) } else { printHint(" No events yet") } } else { printHint("No history") } } print("") } // ============================================================================= // Dashboard // ============================================================================= fn doDashboard(): Unit with {Process} = { // Show terminal dashboard instead of HTML print("") printHeader("Dashboard") let deviceName = execQuiet("hostname") printHint("Device: " + deviceName) print("") // Sync if palSyncthingRunning() then { let folders = getSyncFolderCount() let devices = getSyncDeviceCount() printStatusLine("sync", "Running", folders + " folders, " + devices + " devices") } else { printStatusLine("sync", "Stopped", "") } // Backup let repo = getBackupRepo() if String.length(repo) > 0 then { let snapCount = execQuiet("restic -r " + repo + " --password-file " + getBackupPassword() + " snapshots --json 2>/dev/null | jq -r 'length' 2>/dev/null || echo 0") printStatusLine("backup", "Ready", snapCount + " snapshots") } else { printStatusLine("backup", "Not configured", "") } // Server let serverDir = palDir() + "/server" let isMounted = execQuiet("mountpoint -q " + serverDir + " && echo yes || echo no") if isMounted |> isYes then { let dfOut = execQuiet("df -h " + serverDir + " 2>/dev/null | tail -1 | tr -s ' ' | cut -d' ' -f3,2 | sed 's/ / \\/ /'") printStatusLine("server", "Mounted", dfOut) } else { printStatusLine("server", "Not mounted", "") } // Config let hasRepo = execQuiet("test -d " + palDir() + "/config-repo/.git && echo yes || echo no") if hasRepo |> isYes then { let repoRemote = execQuiet("cd " + palDir() + "/config-repo && git remote get-url origin 2>/dev/null | sed 's/.*\\///' | sed 's/.git//'") printStatusLine("config", "Linked", repoRemote) } else { printStatusLine("config", "Not linked", "") } print("") } // ============================================================================= // Onboard wizard // ============================================================================= fn showOnboardSummary(deviceName: String): Unit with {Process} = { let ignore = Process.exec("printf '\\n\\033[1m── Setup Complete ──\\033[0m\\n\\n' >&2") // Device info let osInfo = execQuiet("uname -sr") printStatusLine("device", deviceName, osInfo) // Config status let hasRepo = execQuiet("test -d " + palDir() + "/config-repo/.git && echo yes || echo no") if hasRepo |> isYes then { let repoRemote = execQuiet("cd " + palDir() + "/config-repo && git remote get-url origin 2>/dev/null | sed 's/.*\\///' | sed 's/.git//'") if String.length(repoRemote) > 0 then printStatusLine("config", "Linked", repoRemote) else printStatusLine("config", "Initialized", "local repo") } else { printStatusLine("config", "Not linked", "") } // Sync status if palSyncthingRunning() then { let folders = getSyncFolderCount() let devices = getSyncDeviceCount() printStatusLine("sync", "Running", folders + " folders, " + devices + " devices") } else if hasCommand("syncthing") then { printStatusLine("sync", "Ready", "not running") } else { printStatusLine("sync", "Not installed", "") } // Backup status let finalRepo = getBackupRepo() if String.length(finalRepo) > 0 then { printStatusLine("backup", "Configured", finalRepo) } else { printStatusLine("backup", "Not configured", "") } print("") // Connected devices if palSyncthingRunning() then { printBold("Connected Devices") let ignore2 = Process.exec("printf ' \\033[32m●\\033[0m %s \\033[2m(this device)\\033[0m\\n' \"" + deviceName + "\" >&2") let otherDevices = execQuiet("syncthing cli --home=" + palDir() + "/syncthing/config show config 2>/dev/null | jq -r '.devices[] | .name // .deviceID[:8]' 2>/dev/null | grep -v '^$'") if String.length(otherDevices) > 1 then { let ignore3 = Process.exec("echo '" + otherDevices + "' | while read -r dev; do printf ' \\033[2m●\\033[0m %s\\n' \"$dev\" >&2; done") () } else () print("") } else { printBold("Connected Devices") let ignore2 = Process.exec("printf ' \\033[2m●\\033[0m %s \\033[2m(this device, sync not running)\\033[0m\\n' \"" + deviceName + "\" >&2") print("") } // Synced data let syncDir = palDir() + "/sync" let hasSyncDir = execQuiet("test -d " + syncDir + " && echo yes || echo no") if hasSyncDir |> isYes then { printBold("Synced Data") let notesCount = execQuiet("find " + syncDir + "/notes -type f 2>/dev/null | wc -l | tr -d ' '") let docsCount = execQuiet("find " + syncDir + "/documents -type f 2>/dev/null | wc -l | tr -d ' '") let dotfilesCount = execQuiet("find " + syncDir + "/dotfiles -type f 2>/dev/null | wc -l | tr -d ' '") let ignore4 = Process.exec("printf ' %-16s %s files\\n' 'notes/' '" + notesCount + "' >&2") let ignore5 = Process.exec("printf ' %-16s %s files\\n' 'documents/' '" + docsCount + "' >&2") let ignore6 = Process.exec("printf ' %-16s %s files\\n' 'dotfiles/' '" + dotfilesCount + "' >&2") print("") } else () printSuccess("Onboarding complete!") print("") printHint("Data directory: " + palDir()) print("") } fn doOnboardSteps(): Unit with {Console, Process} = { // ── Step 1: Device ────────────────────────────────────────────────── let ignore = Process.exec("printf '\\033[1m── Step 1: Device ──\\033[0m\\n\\n' >&2") printStepPending("Creating directories") createDirectories() printStep("Creating directories", "done") let hostname = execQuiet("hostname") let deviceName = askInput("Device name", hostname) print("") printStepPending("Writing configuration") let configContent = "[pal]\nversion = 1\ndevice_name = \"" + deviceName + "\"\n\n[sync]\ngui_port = 8385\nsync_port = 22001\n\n[backup]\nschedule = \"hourly\"\n" let writeCmd = "cat > " + palConfig() + " << 'EOF'\n" + configContent + "EOF" let ignore2 = Process.exec(writeCmd) printStep("Writing configuration", "done") printStepPending("Setting up encryption") let hasAgeKey = execQuiet("test -f " + palDir() + "/age-key.txt && echo yes || echo no") if hasAgeKey |> isYes then printStep("Setting up encryption", "exists") else if hasCommand("age-keygen") then { let ignore3 = Process.exec("age-keygen -o " + palDir() + "/age-key.txt 2>&1 || true") printStep("Setting up encryption", "done") } else { printStepWarn("Setting up encryption", "age not installed") } if hasCommand("sqlite3") then { initStateDb() logEvent("onboard", "Started onboarding for " + deviceName) } else () print("") // ── Step 2: Config Repository ─────────────────────────────────────── let ignore4 = Process.exec("printf '\\033[1m── Step 2: Config Repository ──\\033[0m\\n\\n' >&2") printHint("A git repo lets you version-control your system config") printHint("(NixOS flakes, dotfiles, etc.) and share it across machines.") print("") let ignore5 = Process.exec("printf ' \\033[36m1\\033[0m I have a git repo URL\\n' >&2") let ignore6 = Process.exec("printf ' \\033[36m2\\033[0m Initialize a new repo in a directory\\n' >&2") let ignore7 = Process.exec("printf ' \\033[36m3\\033[0m Skip for now\\n' >&2") print("") let ignore8 = Process.exec("printf 'Choice [3]: ' >&2") let configChoice = String.trim(Console.readLine()) print("") match configChoice { "1" => { let repoUrl = askInput("Repository URL", "") if String.length(repoUrl) == 0 then { printHint("No URL provided, skipping.") } else { print("") printStepPending("Cloning repository") let cloneResult = Process.exec("git clone " + repoUrl + " " + palDir() + "/config-repo 2>&1") if String.contains(cloneResult, "fatal") then { printStepErr("Cloning repository", "failed") printDim(cloneResult) print("") printHint("You can try again later with:") printCmd("pal init " + repoUrl) } else { printStep("Cloning repository", "done") logEvent("onboard", "Cloned config: " + repoUrl) } } }, "2" => { let defaultDir = palDir() + "/config-repo" let repoDir = askInput("Directory", defaultDir) print("") printStepPending("Initializing repository") let ignore9 = Process.exec("mkdir -p " + repoDir) let initResult = Process.exec("cd " + repoDir + " && git init 2>&1") if String.contains(initResult, "nitialized") then { printStep("Initializing repository", "done") printHint("Add a remote later:") printCmd("git -C " + repoDir + " remote add origin ") logEvent("onboard", "Init config repo: " + repoDir) } else { printStepErr("Initializing repository", "failed") printDim(initResult) } }, _ => { printHint("Skipping config repository.") printHint("Set this up later with:") printCmd("pal init ") } } print("") // ── Step 3: File Sync ─────────────────────────────────────────────── let ignore10 = Process.exec("printf '\\033[1m── Step 3: File Sync ──\\033[0m\\n\\n' >&2") printHint("Syncthing keeps notes, documents, and dotfiles") printHint("in sync across all your devices in real-time.") print("") if hasCommand("syncthing") == false then { printStepWarn("Syncthing", "not installed") printHint("Install syncthing and run 'pal sync setup' later.") } else { let setupSync = askConfirm("Set up sync?", true) print("") if setupSync then { let hasConfig = execQuiet("test -f " + palDir() + "/syncthing/config/config.xml && echo yes || echo no") if hasConfig |> isNo then { printStepPending("Creating sync config") let ignore11 = Process.exec("syncthing generate --home=" + palDir() + "/syncthing/config 2>&1") printStep("Creating sync config", "done") } else { printStep("Creating sync config", "exists") } if palSyncthingRunning() == false then { printStepPending("Starting sync daemon") let ignore12 = Process.exec("syncthing serve --home=" + palDir() + "/syncthing/config --no-browser --gui-address=127.0.0.1:8385 >/dev/null 2>&1 &") let ignore13 = Process.exec("sleep 2") if palSyncthingRunning() then printStep("Starting sync daemon", "done") else printStepWarn("Starting sync daemon", "may need a moment") } else { printStep("Starting sync daemon", "running") } let deviceId = execQuiet("syncthing cli --home=" + palDir() + "/syncthing/config show system 2>/dev/null | grep myID | cut -d'\"' -f4") if String.length(deviceId) > 10 then { print("") printBold("Your Device ID") let ignore14 = Process.exec("printf '\\033[33m%s\\033[0m\\n' \"" + deviceId + "\" >&2") print("") printQR(deviceId) print("") printHint("Share this ID or scan the QR code to pair devices.") logEvent("onboard", "Sync configured") } else { printHint("Device ID not ready yet. Try 'pal sync setup' in a moment.") } } else { printHint("Skipping sync setup.") printHint("Set this up later with:") printCmd("pal sync setup") } } print("") // ── Step 4: Backups ───────────────────────────────────────────────── let ignore15 = Process.exec("printf '\\033[1m── Step 4: Backups ──\\033[0m\\n\\n' >&2") printHint("Restic creates encrypted, deduplicated snapshots of") printHint("your synced data. Supports local, SFTP, S3, B2, and more.") print("") if hasCommand("restic") == false then { printStepWarn("Restic", "not installed") printHint("Install restic and run 'pal backup init ' later.") } else { let existingRepo = getBackupRepo() let setupBackup = if String.length(existingRepo) > 0 then { printHint("Current repository: " + existingRepo) print("") askConfirm("Reconfigure backup location?", false) } else { askConfirm("Set up backups?", true) } print("") if setupBackup then { let defaultRepo = existingRepo printHint("Enter a path or URL where backup snapshots will be stored.") print("") let ignore15b = Process.exec("printf ' \\033[36mLocal directory\\033[0m /mnt/backup, /media/usb/backups\\n' >&2") let ignore15c = Process.exec("printf ' \\033[36mSFTP (SSH)\\033[0m sftp:user@host:/backups\\n' >&2") let ignore15d = Process.exec("printf ' \\033[36mBackblaze B2\\033[0m b2:bucket-name:path\\n' >&2") let ignore15e = Process.exec("printf ' \\033[36mAmazon S3\\033[0m s3:bucket-name/path\\n' >&2") print("") let backupRepo = askInput("Backup location", defaultRepo) if String.length(backupRepo) == 0 then { printHint("Skipping backup setup.") printCmd("pal backup init ") } else { print("") let passwordFile = palDir() + "/restic/password" let hasPassword = execQuiet("test -f " + passwordFile + " && echo yes || echo no") if hasPassword |> isNo then { printStepPending("Generating password") let ignore16 = Process.exec("head -c 32 /dev/urandom | base64 > " + passwordFile + " && chmod 600 " + passwordFile) printStep("Generating password", "done") } else { printStep("Generating password", "exists") } printStepPending("Initializing repository") let initResult = Process.exec("restic -r " + backupRepo + " --password-file " + passwordFile + " init 2>&1") if String.contains(initResult, "created restic repository") || String.contains(initResult, "already initialized") then { printStep("Initializing repository", "done") let ignore17 = Process.exec("echo '" + backupRepo + "' > " + palDir() + "/restic/repository") logEvent("onboard", "Backup: " + backupRepo) } else { printStepErr("Initializing repository", "failed") printDim(initResult) printHint("Try again later with:") printCmd("pal backup init " + backupRepo) } } } else { printHint("Skipping backup setup.") printHint("Set this up later with:") printCmd("pal backup init ") } } print("") // ── Summary ───────────────────────────────────────────────────────── showOnboardSummary(deviceName) if hasCommand("sqlite3") then { logEvent("onboard", "Onboarding completed for " + deviceName) } else () } fn doOnboard(): Unit with {Console, Process} = { print("") if isInitialized() then { printWarn("pal is already set up on this device.") print("") // Show current state so user knows what exists printBold("Current setup:") print("") let deviceName = execQuiet("grep device_name " + palConfig() + " 2>/dev/null | cut -d'\"' -f2") if String.length(deviceName) > 0 then printStep("Device", deviceName) else () let hasRepo = execQuiet("test -d " + palDir() + "/config-repo/.git && echo yes || echo no") if hasRepo |> isYes then printStep("Config repo", "configured") else printStepWarn("Config repo", "not set up") if palSyncthingRunning() then printStep("Sync", "running") else if hasCommand("syncthing") then printStepWarn("Sync", "not running") else printStepErr("Sync", "not installed") let repo = getBackupRepo() if String.length(repo) > 0 then printStep("Backup", repo) else printStepWarn("Backup", "not configured") print("") printWarn("Re-running will overwrite your device name and config file.") printHint("Existing sync, backup, and repo settings will be kept if you skip those steps.") print("") let rerun = askConfirm("Continue?", false) if rerun then { print("") doOnboardSteps() } else { print("") printHint("No changes made.") print("") } } else { showLogo() printBold("Welcome! Let's get your system set up.") print("") printHint("This wizard will walk you through configuring:") print(" - Device identity") print(" - Config repository (git-managed)") print(" - File sync (Syncthing)") print(" - Backups (restic)") print("") doOnboardSteps() } } // ============================================================================= // Version // ============================================================================= fn showVersion(): Unit with {Process} = { let ignore = Process.exec("printf 'pal 0.2.0\\n' >&2") () } // ============================================================================= // Help // ============================================================================= fn showHelp(): Unit with {Process} = { print("") let ignore = Process.exec("printf '\\033[1mpal\\033[0m - Your personal data, everywhere\\n' >&2") print("") let ignore2 = Process.exec("printf '\\033[1mUSAGE\\033[0m\\n' >&2") let ignore3 = Process.exec("printf ' pal [COMMAND]\\n' >&2") print("") let ignore4 = Process.exec("printf '\\033[1mCOMMANDS\\033[0m\\n' >&2") print("") let ignore5 = Process.exec("printf ' \\033[36msetup\\033[0m Initialize pal on this machine\\n' >&2") let ignore6 = Process.exec("printf ' \\033[36minit\\033[0m Clone config from git repository\\n' >&2") let ignore6b = Process.exec("printf ' \\033[36monboard\\033[0m Interactive setup wizard\\n' >&2") print("") let ignore7 = Process.exec("printf ' \\033[36msync\\033[0m Trigger a rescan\\n' >&2") let ignore8 = Process.exec("printf ' \\033[36msync status\\033[0m Show sync status\\n' >&2") let ignore9 = Process.exec("printf ' \\033[36msync setup\\033[0m Initialize and show device ID\\n' >&2") let ignore10 = Process.exec("printf ' \\033[36msync start\\033[0m Start Syncthing daemon\\n' >&2") print("") let ignore11 = Process.exec("printf ' \\033[36mbackup\\033[0m Create a backup snapshot\\n' >&2") let ignore12 = Process.exec("printf ' \\033[36mbackup list\\033[0m List snapshots\\n' >&2") let ignore13 = Process.exec("printf ' \\033[36mbackup init\\033[0m Initialize backup repository\\n' >&2") print("") let ignore14 = Process.exec("printf ' \\033[36mserver\\033[0m Show server mount status\\n' >&2") let ignore15 = Process.exec("printf ' \\033[36mserver mount\\033[0m Mount remote server\\n' >&2") let ignore16 = Process.exec("printf ' \\033[36mserver unmount\\033[0m Unmount server\\n' >&2") print("") let ignore17 = Process.exec("printf ' \\033[36mstatus\\033[0m Show detailed status\\n' >&2") let ignore18 = Process.exec("printf ' \\033[36mdoctor\\033[0m Check system dependencies\\n' >&2") let ignore19 = Process.exec("printf ' \\033[36mhistory\\033[0m Show recent events\\n' >&2") let ignore20 = Process.exec("printf ' \\033[36mexport\\033[0m Create portable archive\\n' >&2") let ignore21 = Process.exec("printf ' \\033[36mimport\\033[0m Restore from archive\\n' >&2") print("") let ignore22 = Process.exec("printf '\\033[1mOPTIONS\\033[0m\\n' >&2") let ignore23 = Process.exec("printf ' -h, --help Show this help\\n' >&2") let ignore24 = Process.exec("printf ' -V, --version Show version\\n' >&2") print("") printHint("Data directory: " + palDir()) print("") } // Subcommand-specific help fn showSyncHelp(): Unit with {Process} = { print("") let ignore = Process.exec("printf '\\033[1mpal sync\\033[0m - Real-time file synchronization\\n' >&2") print("") let ignore2 = Process.exec("printf '\\033[1mUSAGE\\033[0m\\n' >&2") let ignore3 = Process.exec("printf ' pal sync [COMMAND]\\n' >&2") print("") let ignore4 = Process.exec("printf '\\033[1mCOMMANDS\\033[0m\\n' >&2") let ignore5 = Process.exec("printf ' (default) Trigger a rescan of all folders\\n' >&2") let ignore6 = Process.exec("printf ' status Show Syncthing status\\n' >&2") let ignore7 = Process.exec("printf ' setup Initialize and display device ID for pairing\\n' >&2") let ignore8 = Process.exec("printf ' start Start Syncthing daemon\\n' >&2") print("") } fn showBackupHelp(): Unit with {Process} = { print("") let ignore = Process.exec("printf '\\033[1mpal backup\\033[0m - Point-in-time snapshots\\n' >&2") print("") let ignore2 = Process.exec("printf '\\033[1mUSAGE\\033[0m\\n' >&2") let ignore3 = Process.exec("printf ' pal backup [COMMAND]\\n' >&2") print("") let ignore4 = Process.exec("printf '\\033[1mCOMMANDS\\033[0m\\n' >&2") let ignore5 = Process.exec("printf ' (default) Create a new backup snapshot\\n' >&2") let ignore6 = Process.exec("printf ' list List all snapshots\\n' >&2") let ignore7 = Process.exec("printf ' init Initialize backup repository\\n' >&2") print("") let ignore8 = Process.exec("printf '\\033[1mEXAMPLES\\033[0m\\n' >&2") let ignore9 = Process.exec("printf ' pal backup init /mnt/backup\\n' >&2") let ignore10 = Process.exec("printf ' pal backup init sftp:user@server:/backups\\n' >&2") let ignore11 = Process.exec("printf ' pal backup init b2:bucket-name:path\\n' >&2") print("") } fn showServerHelp(): Unit with {Process} = { print("") let ignore = Process.exec("printf '\\033[1mpal server\\033[0m - Remote server storage\\n' >&2") print("") let ignore2 = Process.exec("printf '\\033[1mUSAGE\\033[0m\\n' >&2") let ignore3 = Process.exec("printf ' pal server [COMMAND]\\n' >&2") print("") let ignore4 = Process.exec("printf '\\033[1mCOMMANDS\\033[0m\\n' >&2") let ignore5 = Process.exec("printf ' (default) Show mount status\\n' >&2") let ignore6 = Process.exec("printf ' mount Mount remote server\\n' >&2") let ignore7 = Process.exec("printf ' unmount Unmount server\\n' >&2") print("") } // ============================================================================= // Main // ============================================================================= fn main(): Unit with {Console, Process} = { let args = Process.args() let cmd = match List.get(args, 1) { Some(c) => c, None => "" } let subcmd = match List.get(args, 2) { Some(s) => s, None => "" } let arg3 = match List.get(args, 3) { Some(a) => a, None => "" } match cmd { "" => showHealthCheck(), "-V" => showVersion(), "--version" => showVersion(), "version" => showVersion(), "init" => doInit(subcmd), "setup" => doSetup(), "onboard" => doOnboard(), "sync" => { match subcmd { "-h" => showSyncHelp(), "--help" => showSyncHelp(), "help" => showSyncHelp(), "status" => doSyncStatus(), "setup" => doSyncSetup(), "start" => doSyncStart(), "" => doSync(), _ => doSyncStatus() } }, "backup" => { match subcmd { "-h" => showBackupHelp(), "--help" => showBackupHelp(), "help" => showBackupHelp(), "init" => doBackupInit(arg3), "list" => doBackupList(), "" => doBackup(), _ => doBackup() } }, "server" => { match subcmd { "-h" => showServerHelp(), "--help" => showServerHelp(), "help" => showServerHelp(), "mount" => doServerMount(), "unmount" => doServerUnmount(), "" => doServerStatus(), _ => doServerStatus() } }, "export" => doExport(), "import" => doImport(subcmd), "status" => showStatus(), "doctor" => showDoctor(), "history" => showHistory(), "dashboard" => doDashboard(), "help" => showHelp(), "-h" => showHelp(), "--help" => showHelp(), _ => { // Unknown command - suggest similar printErr("Unknown command: " + cmd) print("") printHint("Run 'pal --help' for usage.") print("") } } } let result = run main() with {}