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>
This commit is contained in:
2026-02-22 22:38:37 -05:00
parent 05c04b209c
commit d30d2efa4e
7 changed files with 637 additions and 208 deletions

2
.gitignore vendored
View File

@@ -4,7 +4,7 @@ result-*
.direnv/ .direnv/
# Compiled binaries # Compiled binaries
grapho pal
*_test *_test
# Secrets (NEVER commit unencrypted secrets) # Secrets (NEVER commit unencrypted secrets)

View File

@@ -12,11 +12,11 @@ A NixOS-based system for managing the three types of data across devices:
```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.

File diff suppressed because it is too large Load Diff

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

View File

@@ -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;

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 ""