// grapho - Unified CLI for the Ultimate Notetaking, Sync & Backup System // // Usage: // grapho One-line health check (default) // grapho status Full status dashboard // grapho status -v Verbose status with all details // grapho doctor Diagnose issues and suggest fixes // grapho sync Sync all (nb + syncthing) // grapho backup Run backup now // grapho help Show help // grapho --json Machine-readable JSON output // ============================================================================= // Types // ============================================================================= type SyncStatus = | StatusOk | StatusWarn | StatusErr | StatusNone // ============================================================================= // Status icons // ============================================================================= fn statusIcon(s: SyncStatus): String = match s { StatusOk => "[ok]", StatusWarn => "[!!]", StatusErr => "[ERR]", StatusNone => "[--]" } fn statusEmoji(s: SyncStatus): String = match s { StatusOk => "✓", StatusWarn => "⚠", StatusErr => "✗", StatusNone => "-" } // ============================================================================= // 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" } // ============================================================================= // Syncthing helpers // ============================================================================= fn syncthingRunning(): Bool with {Process} = { let result = Process.exec("syncthing cli show system 2>/dev/null | jq -r '.myID' 2>/dev/null || true") String.length(String.trim(result)) > 10 } fn getSyncthingUptime(): String with {Process} = Process.exec("syncthing cli show system 2>/dev/null | jq -r '.uptime // 0' 2>/dev/null || echo 0") fn getSyncthingFolderCount(): String with {Process} = String.trim(Process.exec("syncthing cli show config 2>/dev/null | jq -r '.folders | length' 2>/dev/null || echo 0")) fn getSyncthingDeviceCount(): String with {Process} = String.trim(Process.exec("syncthing cli show config 2>/dev/null | jq -r '.devices | length' 2>/dev/null || echo 0")) // ============================================================================= // One-liner health check (new default) // ============================================================================= fn showHealthCheck(): Unit with {Console, Process} = { let hasIssues = false let hasWarnings = false let summary = "" // Check nb let nbOk = hasCommand("nb") let nbCount = if nbOk then execQuiet("nb notebooks --names 2>/dev/null | wc -l") else "0" // Check syncthing let stOk = hasCommand("syncthing") let stRunning = if stOk then syncthingRunning() else false let stFolders = if stRunning then getSyncthingFolderCount() else "0" let stDevices = if stRunning then getSyncthingDeviceCount() else "0" // Check backup let resticOk = hasCommand("restic") let timerActive = execQuiet("systemctl is-active restic-backup.timer") let backupOk = if timerActive == "active" then true else false // Determine overall status and print appropriate message if nbOk == false || stOk == false || resticOk == false then { Console.print(statusEmoji(StatusErr) + " Issues detected") if nbOk == false then Console.print(" " + statusIcon(StatusErr) + " nb: not installed -> nix develop") else () if stOk == false then Console.print(" " + statusIcon(StatusErr) + " syncthing: not installed -> nix develop") else () if resticOk == false then Console.print(" " + statusIcon(StatusErr) + " restic: not installed -> nix develop") else () } else if stRunning == false || backupOk == false then { Console.print(statusEmoji(StatusWarn) + " Warnings") if stRunning == false then Console.print(" " + statusIcon(StatusWarn) + " syncthing: not running -> systemctl --user start syncthing") else () if backupOk == false then Console.print(" " + statusIcon(StatusWarn) + " backup: timer inactive -> sudo systemctl enable --now restic-backup.timer") else () } else { Console.print(statusEmoji(StatusOk) + " All systems healthy (" + nbCount + " notebooks, " + stFolders + " folders, " + stDevices + " devices, backup active)") } } // ============================================================================= // Status dashboard // ============================================================================= fn showStatus(verbose: Bool): Unit with {Console, Process} = { Console.print("grapho status") Console.print("") // nb if hasCommand("nb") then { let nbCount = execQuiet("nb notebooks --names 2>/dev/null | wc -l") Console.print(statusIcon(StatusOk) + " nb: " + nbCount + " notebooks") if verbose then { let notebooks = execQuiet("nb notebooks --names") if String.length(notebooks) > 0 then { let lines = String.lines(notebooks) printNotebooks(lines) } else () } else () } else { Console.print(statusIcon(StatusErr) + " nb: not installed") Console.print(" Fix: nix develop") } // Syncthing if hasCommand("syncthing") then { if syncthingRunning() then { let folders = getSyncthingFolderCount() let devices = getSyncthingDeviceCount() Console.print(statusIcon(StatusOk) + " syncthing: " + folders + " folders, " + devices + " devices") if verbose then { let folderLabels = execQuiet("syncthing cli show config | jq -r '.folders[].label'") if String.length(folderLabels) > 0 then { Console.print(" Folders:") let flines = String.lines(folderLabels) printLinesIndented(flines) } else () let deviceNames = execQuiet("syncthing cli show config | jq -r '.devices[].name'") if String.length(deviceNames) > 0 then { Console.print(" Devices:") let dlines = String.lines(deviceNames) printLinesIndented(dlines) } else () } else () } else { Console.print(statusIcon(StatusWarn) + " syncthing: not running") Console.print(" Fix: systemctl --user start syncthing") } } else { Console.print(statusIcon(StatusErr) + " syncthing: not installed") Console.print(" Fix: nix develop") } // Backup if hasCommand("restic") then { let timerActive = execQuiet("systemctl is-active restic-backup.timer") if timerActive == "active" then { Console.print(statusIcon(StatusOk) + " backup: timer active") } else { Console.print(statusIcon(StatusWarn) + " backup: timer inactive") Console.print(" Fix: sudo systemctl enable --now restic-backup.timer") } } else { Console.print(statusIcon(StatusErr) + " backup: restic not installed") Console.print(" Fix: nix develop") } } fn printNotebooks(lines: List): Unit with {Console, Process} = match List.head(lines) { Some(name) => { if String.length(name) > 0 then { let count = execQuiet("find ~/.nb/" + name + " -name '*.md' 2>/dev/null | wc -l") Console.print(" " + name + ": " + count + " notes") } else () match List.tail(lines) { Some(rest) => printNotebooks(rest), None => () } }, None => () } fn printLinesIndented(lines: List): Unit with {Console} = match List.head(lines) { Some(line) => { if String.length(line) > 0 then Console.print(" - " + line) else () match List.tail(lines) { Some(rest) => printLinesIndented(rest), None => () } }, None => () } // ============================================================================= // Doctor command // ============================================================================= fn showDoctor(): Unit with {Console, Process} = { Console.print("grapho doctor") Console.print("") Console.print("Checking system health...") Console.print("") // Check nix if hasCommand("nix") then { let nixVer = execQuiet("nix --version") Console.print(statusIcon(StatusOk) + " nix: " + nixVer) } else { Console.print(statusIcon(StatusErr) + " nix: not found") Console.print(" Install: https://nixos.org/download") } // Check git if hasCommand("git") then { let gitVer = execQuiet("git --version | cut -d' ' -f3") Console.print(statusIcon(StatusOk) + " git: " + gitVer) } else { Console.print(statusIcon(StatusErr) + " git: not found") } // Check nb if hasCommand("nb") then { let nbCount = execQuiet("nb notebooks --names 2>/dev/null | wc -l") Console.print(statusIcon(StatusOk) + " nb: " + nbCount + " notebooks") } else { Console.print(statusIcon(StatusErr) + " nb: not installed") Console.print(" Fix: nix develop") } // Check syncthing if hasCommand("syncthing") then { if syncthingRunning() then { let folders = getSyncthingFolderCount() Console.print(statusIcon(StatusOk) + " syncthing: running (" + folders + " folders)") } else { Console.print(statusIcon(StatusWarn) + " syncthing: not running") Console.print(" Fix: systemctl --user start syncthing") } } else { Console.print(statusIcon(StatusErr) + " syncthing: not installed") Console.print(" Fix: nix develop") } // Check backup if hasCommand("restic") then { let timerActive = execQuiet("systemctl is-active restic-backup.timer") if timerActive == "active" then { Console.print(statusIcon(StatusOk) + " backup: timer active") } else { Console.print(statusIcon(StatusWarn) + " backup: timer inactive") Console.print(" Fix: sudo systemctl enable --now restic-backup.timer") } } else { Console.print(statusIcon(StatusErr) + " restic: not installed") Console.print(" Fix: nix develop") } // Additional checks Console.print("") Console.print("Additional checks:") // Check age key let ageKey = execQuiet("test -f ~/.config/sops/age/keys.txt && echo yes") if ageKey == "yes" then Console.print(statusIcon(StatusOk) + " Age encryption key exists") else { Console.print(statusIcon(StatusWarn) + " No age key") Console.print(" Fix: age-keygen -o ~/.config/sops/age/keys.txt") } Console.print("") Console.print("Run 'grapho status -v' for detailed component info") } // ============================================================================= // JSON output // ============================================================================= fn showJson(): Unit with {Console, Process} = { // Note: JSON output with quotes is difficult in Lux due to escape sequence limitations // Using a simple key=value format instead let nbOk = hasCommand("nb") let nbCount = if nbOk then execQuiet("nb notebooks --names 2>/dev/null | wc -l") else "0" let nbStatus = if nbOk then "healthy" else "error" let nbMsg = if nbOk then nbCount + " notebooks" else "not installed" let stOk = hasCommand("syncthing") let stRunning = if stOk then syncthingRunning() else false let stStatus = if stOk == false then "error" else if stRunning then "healthy" else "warning" let stMsg = if stOk == false then "not installed" else if stRunning then "running" else "not running" let resticOk = hasCommand("restic") let timerResult = execQuiet("systemctl is-active restic-backup.timer") let timerActive = if timerResult == "active" then true else false let backupStatus = if resticOk == false then "error" else if timerActive then "healthy" else "warning" let backupMsg = if resticOk == false then "not installed" else if timerActive then "timer active" else "timer inactive" // Output in a simple parseable format Console.print("nb.status=" + nbStatus) Console.print("nb.message=" + nbMsg) Console.print("syncthing.status=" + stStatus) Console.print("syncthing.message=" + stMsg) Console.print("backup.status=" + backupStatus) Console.print("backup.message=" + backupMsg) } // ============================================================================= // Sync command // ============================================================================= fn doSync(): Unit with {Console, Process} = { Console.print("Syncing...") // Sync nb notebooks if hasCommand("nb") then { Console.print("-> nb sync --all") let result = Process.exec("nb sync --all 2>&1 || true") if String.contains(result, "error") then Console.print(statusIcon(StatusWarn) + " nb sync had issues") else Console.print(statusIcon(StatusOk) + " nb synced") } else () // Trigger Syncthing scan if hasCommand("syncthing") then { if syncthingRunning() then { Console.print("-> syncthing cli scan") let scanResult = Process.exec("syncthing cli scan 2>/dev/null || true") Console.print(statusIcon(StatusOk) + " Syncthing scan triggered") } else () } else () Console.print("Done!") } // ============================================================================= // Backup command // ============================================================================= fn doBackup(): Unit with {Console, Process} = { Console.print("Running backup...") if hasCommand("restic") then { let hasService = execQuiet("systemctl cat restic-backup.service") if String.length(hasService) > 0 then { Console.print("-> sudo systemctl start restic-backup.service") let startResult = Process.exec("sudo systemctl start restic-backup.service 2>&1 || true") Console.print(statusIcon(StatusOk) + " Backup service triggered") } else { Console.print(statusIcon(StatusWarn) + " No systemd backup service configured") Console.print(" Configure in modules/backup.nix") } } else { Console.print(statusIcon(StatusErr) + " restic not installed") } } // ============================================================================= // Help // ============================================================================= fn showHelp(): Unit with {Console} = { Console.print("grapho - Personal Data Infrastructure") Console.print("") Console.print("Usage:") Console.print(" grapho Health check (one-liner)") Console.print(" grapho status Component status") Console.print(" grapho status -v Verbose status with details") Console.print(" grapho doctor Diagnose issues and fixes") Console.print(" grapho sync Sync all (nb + syncthing)") Console.print(" grapho backup Run backup now") Console.print(" grapho --json Machine-readable output (key=value format)") Console.print(" grapho help Show this help") Console.print("") Console.print("Quick start:") Console.print(" nb add Create a new note") Console.print(" nb search Search notes") Console.print(" grapho sync Sync to all devices") Console.print("") Console.print("More info: https://git.qrty.ink/blu/grapho") } // ============================================================================= // Main // ============================================================================= fn main(): Unit with {Console, Process} = { let args = Process.args() let cmd = match List.get(args, 1) { Some(c) => c, None => "" } match cmd { "" => showHealthCheck(), "status" => { let subcmd = match List.get(args, 2) { Some(s) => s, None => "" } match subcmd { "-v" => showStatus(true), "--verbose" => showStatus(true), "verbose" => showStatus(true), _ => showStatus(false) } }, "doctor" => showDoctor(), "sync" => doSync(), "backup" => doBackup(), "help" => showHelp(), "-h" => showHelp(), "--help" => showHelp(), "--json" => showJson(), "-j" => showJson(), "json" => showJson(), _ => showHelp() } } let result = run main() with {}