Compare commits

...

9 Commits

Author SHA1 Message Date
ec5e77f796 Prompt for backup location during re-onboard instead of skipping
When step 4 finds an existing backup repo, show the current location
and ask to reconfigure with the existing path as default, rather than
silently skipping the step.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 13:51:24 -05:00
3be9586238 Fix design issues and add comprehensive test suite
Guard doSetup() against re-initialization, route welcome to onboard
wizard, fix import confirmation logic, remove invalid syncthing
generate flags, and show full export path. Add shell-based integration
test suite (123 tests) at ~/src/test/pal/ covering all commands.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 13:50:04 -05:00
4a5fa4415c Fix interactive input by using Console.readLine instead of Process.exec
Process.exec uses popen(cmd, "r") which creates a read-only pipe where
the subprocess stdin is disconnected from the terminal. Console.readLine
reads directly from the parent process stdin via fgets, fixing all
interactive prompts (askConfirm, askInput, choice menus).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 23:48:33 -05:00
d30d2efa4e Rename grapho to pal, add onboard command, fix interactive input
- Rename grapho → pal across entire codebase (CLI, NixOS module,
  flake, docs, config paths)
- Add `pal onboard` interactive setup wizard with 4 steps:
  device, config repo, sync, backups
- Shows current setup summary when re-running onboard on an
  existing installation with warnings about what will change
- Fix askConfirm/askInput to read from /dev/tty so interactive
  prompts work correctly
- Remove || operator usage in askConfirm (not reliable in Lux)
- Add pal package to nix develop shell
- Document ~/.config/pal/ directory structure in README

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 22:38:37 -05:00
05c04b209c Clean up repo: gitignore binaries, remove stale docs
- Add compiled binaries (grapho, *_test) to .gitignore
- Remove grapho binary from tracking (build from source)
- Delete obsolete LUX-LIMITATIONS.md
- Minor README wording fix

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-16 23:05:36 -05:00
13fe22a804 Improve server mount help with detailed instructions
- Add SSHFS section with common options explained
- Add NFS mount example
- Reference NixOS wiki for declarative mounts
- Show unmount hint

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-16 22:58:19 -05:00
dedfbfce64 Add QR code support for Device IDs
- Add printQR() helper function using qrencode
- Display QR code in sync setup for easy mobile pairing
- Add qrencode to doctor dependency check

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-16 22:28:10 -05:00
d3d720b3bc Modernize CLI UX with gh/cargo-style polish
- Add --version/-V and per-subcommand --help
- Replace logo spam with compact status dashboard
- Add aligned step output (label    done format)
- Add confirmation prompts for destructive operations
- Interactive first-run wizard when uninitialized
- Consistent color language and status line formatting
- Clean help with USAGE/COMMANDS/OPTIONS structure

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-16 22:15:35 -05:00
afe7826d58 Complete self-contained grapho CLI with all 8 phases
Phase 5 (Server):
- grapho server setup/mount/unmount/ls commands
- SSHFS/NFS mount instructions

Phase 6 (SQLite):
- State database for event tracking
- grapho history command
- Events logged for sync/backup operations

Phase 7 (Export):
- grapho export creates tar.zst archive
- grapho import restores from archive
- Full data portability between machines

Phase 8 (Dashboard):
- grapho dashboard generates HTML status page
- Dark theme, mobile-friendly
- Can be served with python http.server

Self-contained improvements:
- grapho setup now auto-initializes Syncthing
- grapho backup init <repo> runs restic init directly
- grapho backup runs restic backup directly
- grapho backup list shows snapshots directly
- All configs saved to ~/.config/grapho/

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-16 06:56:24 -05:00
11 changed files with 1861 additions and 988 deletions

4
.gitignore vendored
View File

@@ -3,6 +3,10 @@ result
result-* result-*
.direnv/ .direnv/
# Compiled binaries
pal
*_test
# Secrets (NEVER commit unencrypted secrets) # Secrets (NEVER commit unencrypted secrets)
secrets/*.yaml secrets/*.yaml
!secrets/secrets.yaml.example !secrets/secrets.yaml.example

View File

@@ -1,6 +1,6 @@
# Ultimate Notetaking, Sync & Backup System # Ultimate Notetaking, Sync & Backup System
A NixOS-based system for managing the three types of data in a computer: A NixOS-based system for managing the three types of data across devices:
| Tier | Type | Examples | Sync Model | | Tier | Type | Examples | Sync Model |
|------|------|----------|------------| |------|------|----------|------------|
@@ -12,11 +12,11 @@ A NixOS-based system for managing the three types of data in a computer:
```bash ```bash
# One-command setup (public repo, no SSH key needed) # One-command setup (public repo, no SSH key needed)
nix run 'git+https://git.qrty.ink/blu/grapho.git' nix run 'git+https://git.qrty.ink/blu/pal.git'
# Or clone first, then run # Or clone first, then run
git clone https://git.qrty.ink/blu/grapho.git git clone https://git.qrty.ink/blu/pal.git
cd grapho cd pal
nix run . nix run .
``` ```
@@ -26,8 +26,8 @@ nix run .
```bash ```bash
# 1. Clone the repo # 1. Clone the repo
git clone https://git.qrty.ink/blu/grapho.git git clone https://git.qrty.ink/blu/pal.git
cd grapho cd pal
# 2. Run setup (one command - includes all dependencies) # 2. Run setup (one command - includes all dependencies)
nix run . nix run .
@@ -39,18 +39,18 @@ nix run .
# 4. (Optional) Set up SSH for push access # 4. (Optional) Set up SSH for push access
# Add your SSH key to Gitea: https://git.qrty.ink/user/settings/keys # Add your SSH key to Gitea: https://git.qrty.ink/user/settings/keys
# Then switch to SSH remote: # Then switch to SSH remote:
git remote set-url origin ssh://git@git.qrty.ink:2222/blu/grapho.git git remote set-url origin ssh://git@git.qrty.ink:2222/blu/pal.git
``` ```
### Additional Computers (Joining) ### Additional Computers (Joining)
```bash ```bash
# One command (no SSH key needed for public repo) # One command (no SSH key needed for public repo)
nix run 'git+https://git.qrty.ink/blu/grapho.git' nix run 'git+https://git.qrty.ink/blu/pal.git'
# Choose option [2], enter your config git URL and age key # Choose option [2], enter your config git URL and age key
# Or clone first: # Or clone first:
git clone https://git.qrty.ink/blu/grapho.git && cd grapho git clone https://git.qrty.ink/blu/pal.git && cd pal
nix run . -- <config-git-url> <your-age-key> nix run . -- <config-git-url> <your-age-key>
``` ```
@@ -78,10 +78,10 @@ Add to your flake.nix inputs, then import the module:
```nix ```nix
{ {
inputs.grapho.url = "git+https://git.qrty.ink/blu/grapho.git"; inputs.pal.url = "git+https://git.qrty.ink/blu/pal.git";
# In your configuration: # In your configuration:
imports = [ inputs.grapho.nixosModules.default ]; imports = [ inputs.pal.nixosModules.default ];
} }
``` ```
@@ -306,19 +306,22 @@ No. See [our research](./docs/research/sync-tools-comparison.md). Common issues
But cloud is fine too. This system works with GitHub, Backblaze B2, etc. But cloud is fine too. This system works with GitHub, Backblaze B2, etc.
## Directory Structure ## Repository Structure
``` ```
. .
├── flake.nix # Entry point ├── flake.nix # Entry point
├── flake.lock ├── flake.lock
├── README.md ├── README.md
├── cli/
│ └── pal.lux # CLI source (Lux language)
├── docs/ ├── docs/
│ ├── research/ │ ├── research/
│ │ └── sync-tools-comparison.md │ │ └── sync-tools-comparison.md
│ ├── ARCHITECTURE.md │ ├── ARCHITECTURE.md
│ └── LLM-CONTEXT.md # For AI assistants │ └── LLM-CONTEXT.md # For AI assistants
├── modules/ ├── modules/
│ ├── pal.nix # Core pal NixOS module
│ ├── nb.nix │ ├── nb.nix
│ ├── syncthing.nix │ ├── syncthing.nix
│ ├── neovim.nix │ ├── neovim.nix
@@ -342,6 +345,47 @@ But cloud is fine too. This system works with GitHub, Backblaze B2, etc.
└── ustatus └── ustatus
``` ```
## Data Directory (`~/.config/pal/`)
When you run `pal onboard` or `pal setup`, this directory is created to hold all your data and configuration. Everything is human-readable unless noted.
```
~/.config/pal/
├── pal.toml # Main config (TOML) — device name, ports, schedule
├── age-key.txt # Age encryption private key
├── state.db # Event history (SQLite)
├── config-repo/ # Your system config (git-managed)
│ └── .git/
├── sync/ # Syncthing-managed data (syncs across devices)
│ ├── notes/ # Your notes
│ ├── documents/ # Your documents
│ └── dotfiles/ # Your dotfiles
├── syncthing/ # Syncthing runtime (auto-generated)
│ ├── config/ # config.xml, TLS certs, keys
│ └── db/ # Syncthing index database
├── restic/ # Backup settings
│ ├── password # Repository password (auto-generated, plaintext)
│ ├── repository # Repository URL/path (plaintext)
│ └── cache/ # Local cache for faster operations
├── backups/ # Restic repository (if backing up locally)
│ ├── config # ⚠ Encrypted — restic internal, not human-readable
│ ├── data/ # Encrypted, deduplicated backup chunks
│ ├── index/ # Backup index
│ ├── keys/ # Repository encryption keys
│ ├── locks/ # Lock files
│ └── snapshots/ # Snapshot metadata
└── server/ # Mount point for remote server storage
```
**What's human-readable?** `pal.toml`, `restic/password`, `restic/repository`, and everything in `sync/` and `config-repo/`. The `syncthing/config/` directory is auto-generated XML. The `backups/` directory is a restic repository where everything is encrypted by design — use `pal backup list` to inspect snapshots.
## Contributing ## Contributing
PRs welcome! Please read [ARCHITECTURE.md](./docs/ARCHITECTURE.md) first. PRs welcome! Please read [ARCHITECTURE.md](./docs/ARCHITECTURE.md) first.

View File

@@ -1,679 +0,0 @@
// grapho - Unified Personal Data Infrastructure
//
// Four data types:
// 1. Config - Declarative machine setup (NixOS + Git)
// 2. Sync - Real-time bidirectional sync (Syncthing)
// 3. Backup - Periodic snapshots (Restic)
// 4. Server - Large files on central server
//
// Usage:
// grapho Health check (or welcome if uninitialized)
// grapho init <repo-url> Initialize from config repo
// grapho setup Interactive setup wizard
// grapho sync Sync all folders
// grapho sync status Show sync status
// grapho backup Run backup now
// grapho backup list List snapshots
// grapho status Full status dashboard
// grapho doctor Diagnose issues
// grapho help Show help
// =============================================================================
// 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"
}
// String comparison helpers (workaround for Lux C backend bug with == on strings)
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 graphoDir(): String with {Process} =
homeDir() + "/.config/grapho"
fn graphoConfig(): String with {Process} =
graphoDir() + "/grapho.toml"
// =============================================================================
// Initialization detection
// =============================================================================
fn isInitialized(): Bool with {Process} = {
let result = execQuiet("test -f " + graphoConfig() + " && echo yes || echo no")
result |> isYes
}
fn ensureDir(path: String): Unit with {Process} = {
let ignore = Process.exec("mkdir -p " + path)
()
}
// =============================================================================
// Directory structure
// =============================================================================
fn createDirectories(): Unit with {Process, Console} = {
let base = graphoDir()
Console.print("Creating directory structure...")
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")
Console.print(statusIcon(StatusOk) + " Directories created")
}
// =============================================================================
// Welcome flow
// =============================================================================
fn showWelcome(): Unit with {Console, Process} = {
Console.print("")
Console.print(" Welcome to grapho")
Console.print("")
Console.print(" grapho manages your data across all your devices:")
Console.print("")
Console.print(" * Config - Your machine setup, restored anywhere")
Console.print(" * Sync - Notes & docs synced across devices")
Console.print(" * Backup - Snapshots you can restore anytime")
Console.print(" * Server - Large files on your server")
Console.print("")
Console.print(" To get started, run:")
Console.print("")
Console.print(" grapho setup # Interactive setup wizard")
Console.print(" grapho init <url> # Initialize from existing config repo")
Console.print("")
Console.print(" For more info: grapho help")
Console.print("")
}
// =============================================================================
// Setup wizard
// =============================================================================
fn doSetup(): Unit with {Console, Process} = {
Console.print("")
Console.print("grapho setup wizard")
Console.print("====================")
Console.print("")
// Create directories
createDirectories()
Console.print("")
// Create initial config
Console.print("Creating initial configuration...")
let configContent = "[grapho]\nversion = 1\ndevice_name = \"" + execQuiet("hostname") + "\"\n\n[sync]\ngui_port = 8385\nsync_port = 22001\nfolders = [\"notes\", \"documents\", \"dotfiles\"]\n\n[backup]\nschedule = \"hourly\"\n\n[services]\n"
let writeCmd = "cat > " + graphoConfig() + " << 'GRAPHO_EOF'\n" + configContent + "GRAPHO_EOF"
let ignore = Process.exec(writeCmd)
Console.print(statusIcon(StatusOk) + " Configuration created at " + graphoConfig())
Console.print("")
// Generate age key if missing
let hasAgeKey = execQuiet("test -f " + graphoDir() + "/age-key.txt && echo yes || echo no")
if hasAgeKey |> isYes then
Console.print(statusIcon(StatusOk) + " Age encryption key exists")
else {
Console.print("Generating age encryption key...")
let ignore = Process.exec("age-keygen -o " + graphoDir() + "/age-key.txt 2>&1 || true")
Console.print(statusIcon(StatusOk) + " Age key generated")
}
Console.print("")
// Check for Syncthing
if hasCommand("syncthing") then {
Console.print(statusIcon(StatusOk) + " Syncthing is available")
Console.print(" Configure via: grapho sync setup")
} else {
Console.print(statusIcon(StatusWarn) + " Syncthing not found")
Console.print(" Install via nix or enable in your NixOS config")
}
// Check for restic
if hasCommand("restic") then {
Console.print(statusIcon(StatusOk) + " Restic is available")
Console.print(" Configure via: grapho backup init")
} else {
Console.print(statusIcon(StatusWarn) + " Restic not found")
Console.print(" Install via nix or enable in your NixOS config")
}
Console.print("")
Console.print("Setup complete!")
Console.print("")
Console.print("Your grapho data is stored in: " + graphoDir())
Console.print("")
Console.print("Next steps:")
Console.print(" grapho status # Check system health")
Console.print(" grapho sync setup # Configure device pairing")
Console.print(" grapho backup init # Configure backups")
Console.print("")
}
// =============================================================================
// Init from repo
// =============================================================================
fn doInit(repoUrl: String): Unit with {Console, Process} = {
if String.length(repoUrl) == 0 then {
Console.print("Usage: grapho init <repo-url>")
Console.print("")
Console.print("Clone a grapho config repository to set up this machine.")
Console.print("")
Console.print("Example:")
Console.print(" grapho init https://github.com/user/grapho-config")
Console.print(" grapho init git@github.com:user/grapho-config.git")
} else {
Console.print("Initializing from " + repoUrl)
Console.print("")
// Create base directory
createDirectories()
// Clone repo
Console.print("Cloning config repository...")
let cloneResult = Process.exec("git clone " + repoUrl + " " + graphoDir() + "/config-repo 2>&1")
if String.contains(cloneResult, "fatal") then {
Console.print(statusIcon(StatusErr) + " Failed to clone repository")
Console.print(" " + cloneResult)
} else {
Console.print(statusIcon(StatusOk) + " Repository cloned")
// Check for flake.nix
let hasFlake = execQuiet("test -f " + graphoDir() + "/config-repo/flake.nix && echo yes || echo no")
if hasFlake |> isYes then {
Console.print(statusIcon(StatusOk) + " Found flake.nix")
Console.print("")
Console.print("To apply the NixOS configuration:")
Console.print(" cd " + graphoDir() + "/config-repo")
Console.print(" sudo nixos-rebuild switch --flake .#<hostname>")
} else {
Console.print(statusIcon(StatusWarn) + " No flake.nix found")
Console.print(" This repo may not be a NixOS configuration")
}
}
Console.print("")
}
}
// =============================================================================
// Syncthing helpers (isolated grapho instance)
// =============================================================================
fn graphoSyncthingRunning(): Bool with {Process} = {
// Check if Syncthing is running on grapho port (8385)
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 getSyncthingDeviceId(): String with {Process} = {
execQuiet("syncthing cli --home=" + graphoDir() + "/syncthing/config show system 2>/dev/null | grep -o 'myID.*' | cut -d'\"' -f3")
}
fn getSyncFolderCount(): String with {Process} = {
execQuiet("syncthing cli --home=" + graphoDir() + "/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=" + graphoDir() + "/syncthing/config show config 2>/dev/null | jq -r '.devices | length' 2>/dev/null || echo 0")
}
// =============================================================================
// Sync commands
// =============================================================================
fn doSyncStatus(): Unit with {Console, Process} = {
Console.print("grapho sync status")
Console.print("")
if hasCommand("syncthing") then {
if graphoSyncthingRunning() then {
let folders = getSyncFolderCount()
let devices = getSyncDeviceCount()
Console.print(statusIcon(StatusOk) + " Syncthing running on port 8385")
Console.print(" Folders: " + folders)
Console.print(" Devices: " + devices)
Console.print("")
Console.print(" Web UI: http://127.0.0.1:8385")
} else {
Console.print(statusIcon(StatusWarn) + " Grapho Syncthing not running")
Console.print("")
Console.print(" Start with: systemctl --user start syncthing-grapho")
Console.print(" Or: syncthing serve --home=" + graphoDir() + "/syncthing/config --gui-address=127.0.0.1:8385")
}
} else {
Console.print(statusIcon(StatusErr) + " Syncthing not installed")
Console.print(" Install via nix or NixOS config")
}
}
fn doSyncSetup(): Unit with {Console, Process} = {
Console.print("grapho sync setup")
Console.print("")
if hasCommand("syncthing") == false then {
Console.print(statusIcon(StatusErr) + " Syncthing not installed")
Console.print(" Install syncthing first")
} else {
// Initialize Syncthing config if needed
let hasConfig = execQuiet("test -f " + graphoDir() + "/syncthing/config/config.xml && echo yes || echo no")
if hasConfig |> isNo then {
Console.print("Initializing Syncthing for grapho...")
let ignore = Process.exec("syncthing generate --home=" + graphoDir() + "/syncthing/config --no-default-folder 2>&1")
Console.print(statusIcon(StatusOk) + " Syncthing initialized")
} else {
Console.print(statusIcon(StatusOk) + " Syncthing already configured")
}
// Get device ID
Console.print("")
let deviceId = getSyncthingDeviceId()
if String.length(deviceId) > 10 then {
Console.print("Your device ID:")
Console.print(" " + deviceId)
Console.print("")
Console.print("Share this ID with other devices to pair them.")
} else {
Console.print("Start Syncthing to get your device ID:")
Console.print(" syncthing serve --home=" + graphoDir() + "/syncthing/config --gui-address=127.0.0.1:8385")
}
}
}
fn doSync(): Unit with {Console, Process} = {
Console.print("Syncing...")
if graphoSyncthingRunning() then {
Console.print("-> Triggering Syncthing rescan")
let ignore = Process.exec("syncthing cli --home=" + graphoDir() + "/syncthing/config scan 2>/dev/null || true")
Console.print(statusIcon(StatusOk) + " Sync triggered")
} else {
Console.print(statusIcon(StatusWarn) + " Syncthing not running")
Console.print(" Start with: grapho sync setup")
}
Console.print("")
}
// =============================================================================
// Backup commands
// =============================================================================
fn doBackupInit(): Unit with {Console, Process} = {
Console.print("grapho backup init")
Console.print("")
if hasCommand("restic") == false then {
Console.print(statusIcon(StatusErr) + " Restic not installed")
Console.print(" Install restic first")
} else {
Console.print("Backup repository setup")
Console.print("")
Console.print("Configure your backup repository in: " + graphoConfig())
Console.print("")
Console.print("Example configuration:")
Console.print(" [backup]")
Console.print(" repository = \"sftp:server:/backups/grapho\"")
Console.print(" schedule = \"hourly\"")
Console.print("")
Console.print("After configuring, initialize with:")
Console.print(" restic -r <repo> init")
}
}
fn doBackupList(): Unit with {Console, Process} = {
Console.print("grapho backup list")
Console.print("")
if hasCommand("restic") == false then {
Console.print(statusIcon(StatusErr) + " Restic not installed")
} else {
Console.print("Run: restic -r <your-repo> snapshots")
Console.print("")
Console.print("Check your repository location in: " + graphoConfig())
}
}
fn doBackup(): Unit with {Console, Process} = {
Console.print("Running backup...")
if hasCommand("restic") then {
// Check for systemd service first
let hasService = execQuiet("systemctl cat grapho-backup.service 2>/dev/null || systemctl cat restic-backup.service 2>/dev/null")
if String.length(hasService) > 0 then {
Console.print("-> Triggering backup service")
let ignore = Process.exec("systemctl start grapho-backup.service 2>/dev/null || systemctl start restic-backup.service 2>/dev/null || true")
Console.print(statusIcon(StatusOk) + " Backup triggered")
} else {
Console.print(statusIcon(StatusWarn) + " No systemd backup service configured")
Console.print(" Configure in your NixOS config or run restic manually")
Console.print(" restic -r <repo> backup " + graphoDir() + "/sync")
}
} else {
Console.print(statusIcon(StatusErr) + " Restic not installed")
}
Console.print("")
}
// =============================================================================
// Server commands
// =============================================================================
fn doServerStatus(): Unit with {Console, Process} = {
Console.print("grapho server status")
Console.print("")
let serverDir = graphoDir() + "/server"
let isMounted = execQuiet("mountpoint -q " + serverDir + " && echo yes || echo no")
if isMounted |> isYes then {
let usage = execQuiet("df -h " + serverDir + " | tail -1 | tr -s ' ' | cut -d' ' -f3,2,5 | tr ' ' '/'")
Console.print(statusIcon(StatusOk) + " Server mounted at " + serverDir)
Console.print(" Usage: " + usage)
} else {
Console.print(statusIcon(StatusNone) + " Server not mounted")
Console.print(" Configure in: " + graphoConfig())
Console.print(" Mount point: " + serverDir)
}
}
// =============================================================================
// One-liner health check
// =============================================================================
fn showHealthCheck(): Unit with {Console, Process} = {
// Check if initialized
if isInitialized() == false then {
showWelcome()
} else {
let configOk = true
let syncOk = graphoSyncthingRunning()
let backupOk = execQuiet("systemctl is-active grapho-backup.timer 2>/dev/null || systemctl is-active restic-backup.timer 2>/dev/null") |> isActive
let folders = if syncOk then getSyncFolderCount() else "0"
let devices = if syncOk then getSyncDeviceCount() else "0"
// Determine issues
if syncOk == false || backupOk == false then {
if syncOk == false && backupOk == false then {
Console.print(statusEmoji(StatusWarn) + " Warnings")
Console.print(" " + statusIcon(StatusWarn) + " sync: not running -> systemctl --user start syncthing-grapho")
Console.print(" " + statusIcon(StatusWarn) + " backup: timer inactive -> check grapho backup init")
} else if syncOk == false then {
Console.print(statusEmoji(StatusWarn) + " Sync not running")
Console.print(" " + statusIcon(StatusWarn) + " sync: not running -> systemctl --user start syncthing-grapho")
} else {
Console.print(statusEmoji(StatusWarn) + " Backup timer inactive")
Console.print(" " + statusIcon(StatusWarn) + " backup: timer inactive -> check grapho backup init")
}
} else {
Console.print(statusEmoji(StatusOk) + " All systems healthy (" + folders + " folders, " + devices + " devices, backup active)")
}
}
}
// =============================================================================
// Status dashboard
// =============================================================================
fn showStatus(): Unit with {Console, Process} = {
Console.print("grapho status")
Console.print("")
if isInitialized() == false then {
Console.print(statusIcon(StatusNone) + " grapho not initialized")
Console.print(" Run: grapho setup")
Console.print("")
} else {
// Config
let hasRepo = execQuiet("test -d " + graphoDir() + "/config-repo/.git && echo yes || echo no")
if hasRepo |> isYes then {
let repoRemote = execQuiet("cd " + graphoDir() + "/config-repo && git remote get-url origin 2>/dev/null || echo local")
Console.print(statusIcon(StatusOk) + " config: " + repoRemote)
} else {
Console.print(statusIcon(StatusNone) + " config: no repo linked")
Console.print(" Run: grapho init <repo-url>")
}
// Sync
if graphoSyncthingRunning() then {
let folders = getSyncFolderCount()
let devices = getSyncDeviceCount()
Console.print(statusIcon(StatusOk) + " sync: " + folders + " folders, " + devices + " devices")
} else {
Console.print(statusIcon(StatusWarn) + " sync: not running")
Console.print(" Run: grapho sync setup")
}
// Backup
let timerActive = execQuiet("systemctl is-active grapho-backup.timer 2>/dev/null || systemctl is-active restic-backup.timer 2>/dev/null")
if timerActive |> isActive then {
Console.print(statusIcon(StatusOk) + " backup: timer active")
} else {
Console.print(statusIcon(StatusWarn) + " backup: timer inactive")
Console.print(" Run: grapho backup init")
}
// Server
let serverDir = graphoDir() + "/server"
let isMounted = execQuiet("mountpoint -q " + serverDir + " && echo yes || echo no")
if isMounted |> isYes then {
Console.print(statusIcon(StatusOk) + " server: mounted")
} else {
Console.print(statusIcon(StatusNone) + " server: not configured")
}
}
Console.print("")
}
// =============================================================================
// Doctor command
// =============================================================================
fn showDoctor(): Unit with {Console, Process} = {
Console.print("grapho doctor")
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 syncthing
if hasCommand("syncthing") then {
if graphoSyncthingRunning() then {
Console.print(statusIcon(StatusOk) + " syncthing: running on port 8385")
} else {
Console.print(statusIcon(StatusWarn) + " syncthing: installed but not running")
Console.print(" Fix: grapho sync setup")
}
} else {
Console.print(statusIcon(StatusErr) + " syncthing: not installed")
}
// Check restic
if hasCommand("restic") then {
Console.print(statusIcon(StatusOk) + " restic: installed")
} else {
Console.print(statusIcon(StatusErr) + " restic: not installed")
}
// Check age
if hasCommand("age") then {
let hasKey = execQuiet("test -f " + graphoDir() + "/age-key.txt && echo yes || echo no")
if hasKey |> isYes then {
Console.print(statusIcon(StatusOk) + " age: key exists")
} else {
Console.print(statusIcon(StatusWarn) + " age: no key at " + graphoDir() + "/age-key.txt")
Console.print(" Fix: grapho setup")
}
} else {
Console.print(statusIcon(StatusErr) + " age: not installed")
}
// Directory structure
Console.print("")
Console.print("Directory structure:")
let hasGraphoDir = execQuiet("test -d " + graphoDir() + " && echo yes || echo no")
if hasGraphoDir |> isYes then {
Console.print(statusIcon(StatusOk) + " " + graphoDir())
checkDir("config-repo")
checkDir("syncthing")
checkDir("sync")
checkDir("restic")
checkDir("server")
} else {
Console.print(statusIcon(StatusErr) + " " + graphoDir() + " does not exist")
Console.print(" Fix: grapho setup")
}
Console.print("")
}
fn checkDir(dir: String): Unit with {Console, Process} = {
let exists = execQuiet("test -d " + graphoDir() + "/" + dir + " && echo yes || echo no")
if exists |> isYes then
Console.print(" " + statusIcon(StatusOk) + " " + dir + "/")
else
Console.print(" " + statusIcon(StatusNone) + " " + dir + "/ (missing)")
}
// =============================================================================
// Help
// =============================================================================
fn showHelp(): Unit with {Console, Process} = {
Console.print("grapho - Personal Data Infrastructure")
Console.print("")
Console.print("Usage:")
Console.print(" grapho Health check (or welcome if new)")
Console.print(" grapho init <repo-url> Initialize from config repo")
Console.print(" grapho setup Interactive setup wizard")
Console.print("")
Console.print("Sync (Type 2 - Real-time bidirectional):")
Console.print(" grapho sync Trigger sync")
Console.print(" grapho sync status Show sync status")
Console.print(" grapho sync setup Configure Syncthing")
Console.print("")
Console.print("Backup (Type 3 - Periodic snapshots):")
Console.print(" grapho backup Run backup now")
Console.print(" grapho backup list List snapshots")
Console.print(" grapho backup init Configure backup repository")
Console.print("")
Console.print("Server (Type 4 - Central storage):")
Console.print(" grapho server Show server status")
Console.print("")
Console.print("Status:")
Console.print(" grapho status Full status dashboard")
Console.print(" grapho doctor Diagnose issues")
Console.print(" grapho help Show this help")
Console.print("")
Console.print("Data directory: " + graphoDir())
Console.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 => ""
}
match cmd {
"" => showHealthCheck(),
"init" => doInit(subcmd),
"setup" => doSetup(),
"sync" => {
match subcmd {
"status" => doSyncStatus(),
"setup" => doSyncSetup(),
"" => doSync(),
_ => doSyncStatus()
}
},
"backup" => {
match subcmd {
"init" => doBackupInit(),
"list" => doBackupList(),
"" => doBackup(),
_ => doBackup()
}
},
"server" => doServerStatus(),
"status" => showStatus(),
"doctor" => showDoctor(),
"help" => showHelp(),
"-h" => showHelp(),
"--help" => showHelp(),
_ => showHelp()
}
}
let result = run main() with {}

1722
cli/pal.lux Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,218 +0,0 @@
# Lux Language Limitations (grapho CLI)
This document tracks limitations encountered while developing the grapho CLI in Lux, to help improve the language.
## Fixed Issues
### 1. Double Execution Bug (FIXED)
**Severity:** Critical
**Status:** Fixed in Lux c_backend.rs
When using `let result = run main() with {}` to invoke the main function, the entire program was executing twice.
**Root Cause:** In `c_backend.rs:3878-3907`, the generated C code was:
1. Executing all `run` expressions (including `run main() with {}`)
2. Then ALSO calling `main_lux()` separately because `has_main` was true
**Fix:** Added tracking of whether main was already called via a `run` expression, and skip the separate `main_lux()` call if so.
---
## String Handling Issues
### 2. No Escape Sequences in String Literals
**Severity:** High
**Status:** Confirmed
Lux does not support backslash escape sequences like `\"`, `\n`, `\t` in string literals.
```lux
// This FAILS - backslash causes parse error
Console.print("Hello \"World\"") // ERROR: Unexpected character: '\'
// This FAILS
Console.print("Line1\nLine2") // ERROR: Unexpected character: '\'
```
**Impact:** Cannot include quotes in strings, cannot create multi-line strings, cannot output JSON with proper formatting.
**Workaround:**
- Use shell commands via `Process.exec` to generate quoted output
- Use `String.fromChar('"')` for quotes (but this had issues too)
- For JSON output, use key=value format instead
### 3. Dollar Sign in Strings Causes Parse Error
**Severity:** Medium
**Status:** Confirmed
The `$` character in strings triggers the string interpolation lexer, even inside shell command strings.
```lux
// This FAILS
execQuiet("jq -n --arg x '$foo' ...") // ERROR: Unexpected character: '$'
```
**Impact:** Cannot use shell variable syntax or jq arguments in command strings.
**Workaround:** Avoid `$` in strings, or construct commands differently.
### 4. String.fromChar Returns Int, Not String
**Severity:** Medium
**Status:** Bug
`String.fromChar('"')` appears to return an Int instead of a String, causing C compilation errors.
```lux
let q = String.fromChar('"') // Compiles but C code is wrong
Console.print(q + "hello") // C error: int + string
```
**Impact:** Cannot use character literals to build strings.
**Workaround:** Use `execQuiet("printf '%s' '\"'")` to get a quote character.
---
## Type System Issues
### 5. Record Type Definitions Don't Work as Expected
**Severity:** Medium
**Status:** Needs Investigation
Defining a record type and then creating values of that type doesn't work:
```lux
type ComponentStatus = {
name: String,
status: HealthStatus,
message: String,
fix: String
}
fn checkNb(): ComponentStatus with {Process} = {
// ...
{ name: "nb", status: Healthy, message: "ok", fix: "" }
// ERROR: Cannot unify { name: String, ... } with ComponentStatus
}
```
**Impact:** Cannot use structured types for cleaner code organization.
**Workaround:** Avoid record types, use multiple return values via tuples or restructure code.
### 6. Int.parse Doesn't Exist or Has Wrong Signature
**Severity:** Low
**Status:** Confirmed
There's no obvious way to parse a string to an integer.
```lux
let count = Int.parse(someString) // ERROR: Unknown effect operation
```
**Impact:** Cannot convert string output from shell commands to numbers.
**Workaround:** Keep numbers as strings, use shell for numeric comparisons.
---
## C Backend Issues
### 7. String Equality Comparison Generates Incorrect C Code
**Severity:** High
**Status:** Bug
Using `==` to compare strings generates C code that compares pointers instead of string contents.
```lux
let result = execQuiet("echo yes")
if result == "yes" then ... // C code: (result == "yes") - pointer comparison!
```
**Impact:** String comparisons fail in compiled binaries.
**Workaround:** Use `String.contains` for comparison:
```lux
fn isYes(s: String): Bool = String.contains(s, "yes")
if result |> isYes then ...
```
### 8. String.startsWith Not Available in C Backend
**Severity:** Medium
**Status:** Bug
`String.startsWith` works in interpreter but generates undefined function calls in C.
```lux
String.startsWith(s, "prefix") // C error: lux_string__startsWith undefined
```
**Workaround:** Use `String.contains` instead.
### 9. `let _ = expr` Pattern Not Supported
**Severity:** Low
**Status:** Bug
The underscore wildcard pattern for discarding results doesn't work.
```lux
let _ = Process.exec("...") // ERROR: Expected identifier
```
**Workaround:** Use a named binding:
```lux
let ignore = Process.exec("...")
```
### 10. List Literals and Recursion Cause Segfaults
**Severity:** High
**Status:** Bug
Combining list literals with recursive functions can cause segmentation faults in compiled binaries while working fine in interpreter.
```lux
// This crashes when compiled:
let dirs = ["a", "b", "c"]
fn processDirs(dirs: List<String>): Unit =
match List.head(dirs) {
Some(d) => { ...; match List.tail(dirs) { Some(rest) => processDirs(rest), ... } }
None => ()
}
```
**Workaround:** Avoid list literals with recursive processing. Inline the operations:
```lux
fn processA(): Unit = ...
fn processB(): Unit = ...
fn processC(): Unit = ...
// Call each individually
```
---
## Suggestions for Lux
1. **Add escape sequence support** - At minimum `\"`, `\\`, `\n`, `\t`
2. **Fix String.fromChar** to return String, not Int
3. **Add raw string literals** - Something like `r"..."` or `'''...'''` for shell commands
4. **Fix the double execution bug** in the runtime (DONE)
5. **Support record type literals** matching their declared type
6. **Add Int.parse and Float.parse** for string-to-number conversion
7. **Consider a heredoc syntax** for multi-line strings with special characters
8. **Fix string equality** - Use strcmp in C backend for string ==
9. **Support `let _ = `** - Allow underscore as discard binding
10. **Fix String.startsWith** in C backend
11. **Fix list literals with recursion** causing segfaults
---
## Current Workarounds in grapho CLI
1. **Double output:** FIXED in Lux c_backend.rs
2. **JSON output:** Using key=value format instead of proper JSON
3. **Quotes in output:** Avoided entirely or generated via shell
4. **Structured types:** Using individual variables instead of records
5. **Numeric parsing:** Keeping counts as strings throughout
6. **String comparison:** Using `String.contains` with helper functions instead of `==`
7. **Discarding results:** Using `let ignore = ...` instead of `let _ = ...`
8. **Lists with recursion:** Replaced with individual function calls

View File

@@ -1,6 +1,6 @@
# Markdown Editors for grapho # Markdown Editors for pal
This document covers recommended markdown editors for use with grapho across desktop and mobile platforms. This document covers recommended markdown editors for use with pal across desktop and mobile platforms.
## Recommended: md (PWA) ## Recommended: md (PWA)
@@ -19,7 +19,7 @@ A lightweight, browser-based markdown editor that works on both desktop and mobi
- Syntax highlighting for code blocks - Syntax highlighting for code blocks
- Keyboard shortcuts (Ctrl+S to download, Ctrl+B/I for formatting) - Keyboard shortcuts (Ctrl+S to download, Ctrl+B/I for formatting)
### Why It's Good for grapho ### Why It's Good for pal
- Works on any device with a browser - Works on any device with a browser
- Can be installed as a PWA on mobile home screen - Can be installed as a PWA on mobile home screen
- No account required - No account required
@@ -156,7 +156,7 @@ The best open-source markdown editor for Android.
- Supports markdown, todo.txt, and more - Supports markdown, todo.txt, and more
- Offline-first - Offline-first
**Best for:** grapho users on Android. **Best for:** pal users on Android.
### iA Writer (iOS/Android) ### iA Writer (iOS/Android)
**Paid** | [Website](https://ia.net/writer) **Paid** | [Website](https://ia.net/writer)
@@ -187,12 +187,12 @@ Mobile companion to Obsidian desktop.
--- ---
## Recommendation for grapho Users ## Recommendation for pal Users
### Simple Setup (Recommended) ### Simple Setup (Recommended)
1. **Desktop:** MarkText or VS Code 1. **Desktop:** MarkText or VS Code
2. **Mobile:** md PWA (https://md-ashy.vercel.app) or Markor (Android) 2. **Mobile:** md PWA (https://md-ashy.vercel.app) or Markor (Android)
3. **Sync:** Syncthing (already part of grapho) 3. **Sync:** Syncthing (already part of pal)
### Power User Setup ### Power User Setup
1. **Desktop:** Obsidian with Syncthing sync 1. **Desktop:** Obsidian with Syncthing sync
@@ -206,7 +206,7 @@ Mobile companion to Obsidian desktop.
--- ---
## Integration with grapho ## Integration with pal
All recommended editors work with plain markdown files, which means: All recommended editors work with plain markdown files, which means:
@@ -225,7 +225,7 @@ marktext ~/.nb/home/meeting-notes.md
# Or on mobile, open the same file via Syncthing folder # Or on mobile, open the same file via Syncthing folder
# Sync happens automatically # Sync happens automatically
grapho sync pal sync
``` ```
## Sources ## Sources

8
flake.lock generated
View File

@@ -47,13 +47,13 @@
"rust-overlay": "rust-overlay" "rust-overlay": "rust-overlay"
}, },
"locked": { "locked": {
"lastModified": 1771221263, "lastModified": 1771638380,
"narHash": "sha256-Av4s4pelV+ueIMSY61aHuT8KjKZ6ekXtJsnjVc89gtQ=", "narHash": "sha256-RLGfahDSlYi8ec50DtmfOZn9q8JpF2xBTcUb8K2ZQ3Q=",
"path": "/home/blu/src/lux", "path": "/home/blu/src/lux/lang",
"type": "path" "type": "path"
}, },
"original": { "original": {
"path": "/home/blu/src/lux", "path": "/home/blu/src/lux/lang",
"type": "path" "type": "path"
} }
}, },

View File

@@ -15,7 +15,7 @@
}; };
lux = { lux = {
url = "path:/home/blu/src/lux"; url = "path:/home/blu/src/lux/lang";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
@@ -42,7 +42,7 @@
# Shared modules for all hosts # Shared modules for all hosts
sharedModules = [ sharedModules = [
./modules/grapho.nix ./modules/pal.nix
./modules/nb.nix ./modules/nb.nix
./modules/syncthing.nix ./modules/syncthing.nix
./modules/backup.nix ./modules/backup.nix
@@ -66,36 +66,36 @@
}; };
in { in {
# Grapho CLI package # Pal CLI package
packages = forAllSystems (system: packages = forAllSystems (system:
let let
pkgs = nixpkgsFor.${system}; pkgs = nixpkgsFor.${system};
luxPkg = lux.packages.${system}.default; luxPkg = lux.packages.${system}.default;
in { in {
grapho = pkgs.stdenv.mkDerivation { pal = pkgs.stdenv.mkDerivation {
pname = "grapho"; pname = "pal";
version = "0.1.0"; version = "0.1.0";
src = ./cli; src = ./cli;
nativeBuildInputs = [ luxPkg pkgs.gcc ]; nativeBuildInputs = [ luxPkg pkgs.gcc ];
buildPhase = '' buildPhase = ''
${luxPkg}/bin/lux compile grapho.lux -o grapho ${luxPkg}/bin/lux compile pal.lux -o pal
''; '';
installPhase = '' installPhase = ''
mkdir -p $out/bin mkdir -p $out/bin
cp grapho $out/bin/ cp pal $out/bin/
''; '';
meta = { meta = {
description = "Personal data infrastructure CLI"; description = "Personal data infrastructure CLI";
homepage = "https://github.com/user/grapho"; homepage = "https://github.com/user/pal";
license = pkgs.lib.licenses.mit; license = pkgs.lib.licenses.mit;
}; };
}; };
default = self.packages.${system}.grapho; default = self.packages.${system}.pal;
} }
); );
@@ -144,7 +144,7 @@
else else
echo "Error: Cannot find setup script" echo "Error: Cannot find setup script"
echo "Run from the repo directory, or clone it first:" echo "Run from the repo directory, or clone it first:"
echo " git clone ssh://git@your-server:2222/you/grapho.git" echo " git clone ssh://git@your-server:2222/you/pal.git"
exit 1 exit 1
fi fi
fi fi
@@ -173,6 +173,7 @@
packages = [ packages = [
lux.packages.${system}.default lux.packages.${system}.default
self.packages.${system}.pal
] ++ (with pkgs; [ ] ++ (with pkgs; [
# Tier 2: Notes & Sync # Tier 2: Notes & Sync
nb # Notebook CLI nb # Notebook CLI
@@ -203,12 +204,11 @@
]); ]);
shellHook = '' shellHook = ''
printf '\033[1m%s\033[0m\n' "Ultimate Notetaking, Sync & Backup System" printf '\033[1m%s\033[0m\n' "pal - Your personal data, everywhere"
echo "" echo ""
echo "Get started: ./setup" echo "Get started: pal onboard"
echo "Pair mobile: ./setup mobile" echo "Status: pal status"
echo "Sync: ./scripts/usync" echo "Help: pal help"
echo "Status: ./scripts/ustatus"
''; '';
}; };
} }
@@ -216,7 +216,7 @@
# Export modules for use in other flakes # Export modules for use in other flakes
nixosModules = { nixosModules = {
grapho = import ./modules/grapho.nix; pal = import ./modules/pal.nix;
nb = import ./modules/nb.nix; nb = import ./modules/nb.nix;
syncthing = import ./modules/syncthing.nix; syncthing = import ./modules/syncthing.nix;
backup = import ./modules/backup.nix; backup = import ./modules/backup.nix;

BIN
grapho

Binary file not shown.

View File

@@ -1,39 +1,39 @@
# Grapho Module # Pal Module
# #
# Unified personal data infrastructure module for NixOS. # Unified personal data infrastructure module for NixOS.
# Sets up Syncthing (isolated) + Restic backup + directory structure. # Sets up Syncthing (isolated) + Restic backup + directory structure.
# #
# Usage: # Usage:
# services.grapho.enable = true; # services.pal.enable = true;
# services.grapho.user = "youruser"; # services.pal.user = "youruser";
# #
# This creates: # This creates:
# - ~/.config/grapho/ directory structure # - ~/.config/pal/ directory structure
# - Isolated Syncthing on port 8385 (separate from system Syncthing) # - Isolated Syncthing on port 8385 (separate from system Syncthing)
# - Restic backup timer for grapho data # - Restic backup timer for pal data
{ config, lib, pkgs, ... }: { config, lib, pkgs, ... }:
with lib; with lib;
let let
cfg = config.services.grapho; cfg = config.services.pal;
home = config.users.users.${cfg.user}.home; home = config.users.users.${cfg.user}.home;
graphoDir = "${home}/.config/grapho"; palDir = "${home}/.config/pal";
in { in {
options.services.grapho = { options.services.pal = {
enable = mkEnableOption "grapho personal data infrastructure"; enable = mkEnableOption "pal personal data infrastructure";
user = mkOption { user = mkOption {
type = types.str; type = types.str;
description = "User to run grapho services as."; description = "User to run pal services as.";
example = "alice"; example = "alice";
}; };
group = mkOption { group = mkOption {
type = types.str; type = types.str;
default = "users"; default = "users";
description = "Group to run grapho services as."; description = "Group to run pal services as.";
}; };
# Syncthing options # Syncthing options
@@ -41,7 +41,7 @@ in {
enable = mkOption { enable = mkOption {
type = types.bool; type = types.bool;
default = true; default = true;
description = "Enable grapho's isolated Syncthing instance."; description = "Enable pal's isolated Syncthing instance.";
}; };
guiPort = mkOption { guiPort = mkOption {
@@ -65,7 +65,7 @@ in {
openFirewall = mkOption { openFirewall = mkOption {
type = types.bool; type = types.bool;
default = true; default = true;
description = "Open firewall for grapho's Syncthing ports."; description = "Open firewall for pal's Syncthing ports.";
}; };
devices = mkOption { devices = mkOption {
@@ -120,14 +120,14 @@ in {
enable = mkOption { enable = mkOption {
type = types.bool; type = types.bool;
default = false; default = false;
description = "Enable restic backup of grapho data."; description = "Enable restic backup of pal data.";
}; };
repository = mkOption { repository = mkOption {
type = types.str; type = types.str;
default = ""; default = "";
description = "Restic repository location (e.g., 'sftp:server:/backups/grapho')."; description = "Restic repository location (e.g., 'sftp:server:/backups/pal').";
example = "sftp:backup-server:/backups/grapho"; example = "sftp:backup-server:/backups/pal";
}; };
passwordFile = mkOption { passwordFile = mkOption {
@@ -195,27 +195,27 @@ in {
isNormalUser = true; isNormalUser = true;
}; };
# Create grapho directory structure # Create pal directory structure
systemd.tmpfiles.rules = [ systemd.tmpfiles.rules = [
"d ${graphoDir} 0755 ${cfg.user} ${cfg.group} -" "d ${palDir} 0755 ${cfg.user} ${cfg.group} -"
"d ${graphoDir}/config-repo 0755 ${cfg.user} ${cfg.group} -" "d ${palDir}/config-repo 0755 ${cfg.user} ${cfg.group} -"
"d ${graphoDir}/syncthing/config 0755 ${cfg.user} ${cfg.group} -" "d ${palDir}/syncthing/config 0755 ${cfg.user} ${cfg.group} -"
"d ${graphoDir}/syncthing/db 0755 ${cfg.user} ${cfg.group} -" "d ${palDir}/syncthing/db 0755 ${cfg.user} ${cfg.group} -"
"d ${graphoDir}/sync 0755 ${cfg.user} ${cfg.group} -" "d ${palDir}/sync 0755 ${cfg.user} ${cfg.group} -"
"d ${graphoDir}/sync/notes 0755 ${cfg.user} ${cfg.group} -" "d ${palDir}/sync/notes 0755 ${cfg.user} ${cfg.group} -"
"d ${graphoDir}/sync/documents 0755 ${cfg.user} ${cfg.group} -" "d ${palDir}/sync/documents 0755 ${cfg.user} ${cfg.group} -"
"d ${graphoDir}/sync/dotfiles 0755 ${cfg.user} ${cfg.group} -" "d ${palDir}/sync/dotfiles 0755 ${cfg.user} ${cfg.group} -"
"d ${graphoDir}/restic/cache 0755 ${cfg.user} ${cfg.group} -" "d ${palDir}/restic/cache 0755 ${cfg.user} ${cfg.group} -"
"d ${graphoDir}/server 0755 ${cfg.user} ${cfg.group} -" "d ${palDir}/server 0755 ${cfg.user} ${cfg.group} -"
]; ];
# Isolated Syncthing for grapho # Isolated Syncthing for pal
services.syncthing = mkIf cfg.syncthing.enable { services.syncthing = mkIf cfg.syncthing.enable {
enable = true; enable = true;
user = cfg.user; user = cfg.user;
group = cfg.group; group = cfg.group;
dataDir = "${graphoDir}/sync"; dataDir = "${palDir}/sync";
configDir = "${graphoDir}/syncthing/config"; configDir = "${palDir}/syncthing/config";
guiAddress = "127.0.0.1:${toString cfg.syncthing.guiPort}"; guiAddress = "127.0.0.1:${toString cfg.syncthing.guiPort}";
overrideDevices = true; overrideDevices = true;
@@ -228,24 +228,24 @@ in {
}) cfg.syncthing.devices; }) cfg.syncthing.devices;
folders = { folders = {
# Default grapho folders # Default pal folders
"grapho-notes" = { "pal-notes" = {
path = "${graphoDir}/sync/notes"; path = "${palDir}/sync/notes";
id = "grapho-notes"; id = "pal-notes";
devices = attrNames cfg.syncthing.devices; devices = attrNames cfg.syncthing.devices;
type = "sendreceive"; type = "sendreceive";
fsWatcherEnabled = true; fsWatcherEnabled = true;
}; };
"grapho-documents" = { "pal-documents" = {
path = "${graphoDir}/sync/documents"; path = "${palDir}/sync/documents";
id = "grapho-documents"; id = "pal-documents";
devices = attrNames cfg.syncthing.devices; devices = attrNames cfg.syncthing.devices;
type = "sendreceive"; type = "sendreceive";
fsWatcherEnabled = true; fsWatcherEnabled = true;
}; };
"grapho-dotfiles" = { "pal-dotfiles" = {
path = "${graphoDir}/sync/dotfiles"; path = "${palDir}/sync/dotfiles";
id = "grapho-dotfiles"; id = "pal-dotfiles";
devices = attrNames cfg.syncthing.devices; devices = attrNames cfg.syncthing.devices;
type = "sendreceive"; type = "sendreceive";
fsWatcherEnabled = true; fsWatcherEnabled = true;
@@ -271,15 +271,15 @@ in {
}; };
}; };
# Firewall for grapho Syncthing # Firewall for pal Syncthing
networking.firewall = mkIf (cfg.syncthing.enable && cfg.syncthing.openFirewall) { networking.firewall = mkIf (cfg.syncthing.enable && cfg.syncthing.openFirewall) {
allowedTCPPorts = [ cfg.syncthing.syncPort ]; allowedTCPPorts = [ cfg.syncthing.syncPort ];
allowedUDPPorts = [ cfg.syncthing.syncPort cfg.syncthing.discoveryPort ]; allowedUDPPorts = [ cfg.syncthing.syncPort cfg.syncthing.discoveryPort ];
}; };
# Restic backup service # Restic backup service
systemd.services.grapho-backup = mkIf (cfg.backup.enable && cfg.backup.repository != "") { systemd.services.pal-backup = mkIf (cfg.backup.enable && cfg.backup.repository != "") {
description = "Grapho data backup"; description = "Pal data backup";
wants = [ "network-online.target" ]; wants = [ "network-online.target" ];
after = [ "network-online.target" ]; after = [ "network-online.target" ];
@@ -288,18 +288,18 @@ in {
User = cfg.user; User = cfg.user;
Group = cfg.group; Group = cfg.group;
ExecStart = let ExecStart = let
paths = [ "${graphoDir}/sync" ] ++ cfg.backup.extraPaths; paths = [ "${palDir}/sync" ] ++ cfg.backup.extraPaths;
pathArgs = concatMapStringsSep " " (p: "'${p}'") paths; pathArgs = concatMapStringsSep " " (p: "'${p}'") paths;
in '' in ''
${pkgs.restic}/bin/restic backup \ ${pkgs.restic}/bin/restic backup \
--cache-dir ${graphoDir}/restic/cache \ --cache-dir ${palDir}/restic/cache \
${optionalString (cfg.backup.passwordFile != null) "--password-file ${cfg.backup.passwordFile}"} \ ${optionalString (cfg.backup.passwordFile != null) "--password-file ${cfg.backup.passwordFile}"} \
-r ${cfg.backup.repository} \ -r ${cfg.backup.repository} \
${pathArgs} ${pathArgs}
''; '';
ExecStartPost = '' ExecStartPost = ''
${pkgs.restic}/bin/restic forget \ ${pkgs.restic}/bin/restic forget \
--cache-dir ${graphoDir}/restic/cache \ --cache-dir ${palDir}/restic/cache \
${optionalString (cfg.backup.passwordFile != null) "--password-file ${cfg.backup.passwordFile}"} \ ${optionalString (cfg.backup.passwordFile != null) "--password-file ${cfg.backup.passwordFile}"} \
-r ${cfg.backup.repository} \ -r ${cfg.backup.repository} \
${concatStringsSep " " cfg.backup.pruneOpts} ${concatStringsSep " " cfg.backup.pruneOpts}
@@ -307,8 +307,8 @@ in {
}; };
}; };
systemd.timers.grapho-backup = mkIf (cfg.backup.enable && cfg.backup.repository != "") { systemd.timers.pal-backup = mkIf (cfg.backup.enable && cfg.backup.repository != "") {
description = "Grapho backup timer"; description = "Pal backup timer";
wantedBy = [ "timers.target" ]; wantedBy = [ "timers.target" ];
timerConfig = { timerConfig = {
OnCalendar = cfg.backup.schedule; OnCalendar = cfg.backup.schedule;
@@ -318,7 +318,7 @@ in {
}; };
# Server mount (NFS) # Server mount (NFS)
fileSystems."${graphoDir}/server" = mkIf (cfg.server.enable && cfg.server.type == "nfs" && cfg.server.host != "") { fileSystems."${palDir}/server" = mkIf (cfg.server.enable && cfg.server.type == "nfs" && cfg.server.host != "") {
device = "${cfg.server.host}:${cfg.server.remotePath}"; device = "${cfg.server.host}:${cfg.server.remotePath}";
fsType = "nfs"; fsType = "nfs";
options = [ options = [
@@ -330,7 +330,7 @@ in {
]; ];
}; };
# Install grapho CLI and dependencies # Install pal CLI and dependencies
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
syncthing syncthing
restic restic

2
setup
View File

@@ -133,7 +133,7 @@ EOF
age_key=$(grep 'AGE-SECRET-KEY' "$AGE_KEY_FILE") age_key=$(grep 'AGE-SECRET-KEY' "$AGE_KEY_FILE")
# Build join command for other devices # Build join command for other devices
local join_cmd="nix run '<grapho-flake>' -- '${config_url}' '${age_key}'" local join_cmd="nix run '<pal-flake>' -- '${config_url}' '${age_key}'"
# Summary # Summary
echo "" echo ""