Compare commits

..

4 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
8 changed files with 679 additions and 227 deletions

2
.gitignore vendored
View File

@@ -4,7 +4,7 @@ result-*
.direnv/
# Compiled binaries
grapho
pal
*_test
# 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
# 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
git clone https://git.qrty.ink/blu/grapho.git
cd grapho
git clone https://git.qrty.ink/blu/pal.git
cd pal
nix run .
```
@@ -26,8 +26,8 @@ nix run .
```bash
# 1. Clone the repo
git clone https://git.qrty.ink/blu/grapho.git
cd grapho
git clone https://git.qrty.ink/blu/pal.git
cd pal
# 2. Run setup (one command - includes all dependencies)
nix run .
@@ -39,18 +39,18 @@ nix run .
# 4. (Optional) Set up SSH for push access
# Add your SSH key to Gitea: https://git.qrty.ink/user/settings/keys
# 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)
```bash
# 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
# 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>
```
@@ -78,10 +78,10 @@ Add to your flake.nix inputs, then import the module:
```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:
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.
## Directory Structure
## Repository Structure
```
.
├── flake.nix # Entry point
├── flake.lock
├── README.md
├── cli/
│ └── pal.lux # CLI source (Lux language)
├── docs/
│ ├── research/
│ │ └── sync-tools-comparison.md
│ ├── ARCHITECTURE.md
│ └── LLM-CONTEXT.md # For AI assistants
├── modules/
│ ├── pal.nix # Core pal NixOS module
│ ├── nb.nix
│ ├── syncthing.nix
│ ├── neovim.nix
@@ -342,6 +345,47 @@ But cloud is fine too. This system works with GitHub, Backblaze B2, etc.
└── 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
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)
@@ -19,7 +19,7 @@ A lightweight, browser-based markdown editor that works on both desktop and mobi
- Syntax highlighting for code blocks
- 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
- Can be installed as a PWA on mobile home screen
- No account required
@@ -156,7 +156,7 @@ The best open-source markdown editor for Android.
- Supports markdown, todo.txt, and more
- Offline-first
**Best for:** grapho users on Android.
**Best for:** pal users on Android.
### iA Writer (iOS/Android)
**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)
1. **Desktop:** MarkText or VS Code
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
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:
@@ -225,7 +225,7 @@ marktext ~/.nb/home/meeting-notes.md
# Or on mobile, open the same file via Syncthing folder
# Sync happens automatically
grapho sync
pal sync
```
## Sources

8
flake.lock generated
View File

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

View File

@@ -15,7 +15,7 @@
};
lux = {
url = "path:/home/blu/src/lux";
url = "path:/home/blu/src/lux/lang";
inputs.nixpkgs.follows = "nixpkgs";
};
@@ -42,7 +42,7 @@
# Shared modules for all hosts
sharedModules = [
./modules/grapho.nix
./modules/pal.nix
./modules/nb.nix
./modules/syncthing.nix
./modules/backup.nix
@@ -66,36 +66,36 @@
};
in {
# Grapho CLI package
# Pal CLI package
packages = forAllSystems (system:
let
pkgs = nixpkgsFor.${system};
luxPkg = lux.packages.${system}.default;
in {
grapho = pkgs.stdenv.mkDerivation {
pname = "grapho";
pal = pkgs.stdenv.mkDerivation {
pname = "pal";
version = "0.1.0";
src = ./cli;
nativeBuildInputs = [ luxPkg pkgs.gcc ];
buildPhase = ''
${luxPkg}/bin/lux compile grapho.lux -o grapho
${luxPkg}/bin/lux compile pal.lux -o pal
'';
installPhase = ''
mkdir -p $out/bin
cp grapho $out/bin/
cp pal $out/bin/
'';
meta = {
description = "Personal data infrastructure CLI";
homepage = "https://github.com/user/grapho";
homepage = "https://github.com/user/pal";
license = pkgs.lib.licenses.mit;
};
};
default = self.packages.${system}.grapho;
default = self.packages.${system}.pal;
}
);
@@ -144,7 +144,7 @@
else
echo "Error: Cannot find setup script"
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
fi
fi
@@ -173,6 +173,7 @@
packages = [
lux.packages.${system}.default
self.packages.${system}.pal
] ++ (with pkgs; [
# Tier 2: Notes & Sync
nb # Notebook CLI
@@ -203,12 +204,11 @@
]);
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 "Get started: ./setup"
echo "Pair mobile: ./setup mobile"
echo "Sync: ./scripts/usync"
echo "Status: ./scripts/ustatus"
echo "Get started: pal onboard"
echo "Status: pal status"
echo "Help: pal help"
'';
};
}
@@ -216,7 +216,7 @@
# Export modules for use in other flakes
nixosModules = {
grapho = import ./modules/grapho.nix;
pal = import ./modules/pal.nix;
nb = import ./modules/nb.nix;
syncthing = import ./modules/syncthing.nix;
backup = import ./modules/backup.nix;

View File

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

2
setup
View File

@@ -133,7 +133,7 @@ EOF
age_key=$(grep 'AGE-SECRET-KEY' "$AGE_KEY_FILE")
# 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
echo ""