Rewrite README for modern docs standards

This commit is contained in:
Avi 2026-08-06 10:13:22 -05:00
commit 8cbe0b81bc

374
README.md
View file

@ -1,85 +1,114 @@
# Nostr Feed Manager # Nostr Feed Manager
<!-- Forgejo Actions CI badge — uncomment once a workflow exists in `.forgejo/workflows/ci.yml`: > A friendly Linux desktop app for managing Nostr profiles, publishing notes, and acting as a
[![CI]([YOUR_FORGEJO_INSTANCE_URL]/<OWNER>/<REPO>/actions/workflows/ci.yml/badge.svg)]([YOUR_FORGEJO_INSTANCE_URL]/<OWNER>/<REPO>/actions/workflows/ci.yml) > **NIP-46 remote signer** — all while your private keys never leave your machine.
-->
[![Version](https://img.shields.io/badge/version-0.1.0-blue)]() [![Version](https://img.shields.io/badge/version-0.1.0-blue)]()
[![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE) [![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE)
[![Platform](https://img.shields.io/badge/platform-Linux-lightgrey)]() [![Platform](https://img.shields.io/badge/platform-Linux-lightgrey)]()
[![Status](https://img.shields.io/badge/status-early%20beta-orange)]() [![Status](https://img.shields.io/badge/status-early%20beta-orange)]()
[![Rust](https://img.shields.io/badge/Rust-2021-edition-red)]()
[![TypeScript](https://img.shields.io/badge/TypeScript-5.6-blue)]()
A friendly Linux desktop application for managing [Nostr](https://nostr.com/) profiles and <!--
publishing text notes. Forgejo CI badge — enable once a workflow exists in `.forgejo/workflows/ci.yml`:
[![CI]([YOUR_FORGEJO_INSTANCE_URL]/<OWNER>/<REPO>/actions/workflows/ci.yml/badge.svg)]([YOUR_FORGEJO_INSTANCE_URL]/<OWNER>/<REPO>/actions/workflows/ci.yml)
-->
**Host:** this project is hosted on **Forgejo**. Every link in this document uses the generic > **Hosting note:** this project is hosted on **Forgejo**. Throughout this document,
Forgejo URL format `[YOUR_FORGEJO_INSTANCE_URL]/<OWNER>/<REPO>/` — replace > replace `[YOUR_FORGEJO_INSTANCE_URL]` with your instance's base URL (e.g. `https://codeberg.org`)
`[YOUR_FORGEJO_INSTANCE_URL]` with your instance's base URL (e.g. `https://codeberg.org`, > and `<OWNER>/<REPO>` with the actual repository path.
`https://git.example.com`) and `<OWNER>/<REPO>` with the actual repository path before sharing.
**Status: early beta (v0.1.0).** The app is usable daily, actively developed, and not yet
packaged for distribution repositories. Expect rough edges and API churn until 1.0.
Nostr Feed Manager pairs a Rust core (the same library behind the original command-line tool) with
a polished Electron + React interface. All Nostr logic — key generation, signing, relay
communication — runs in the Rust backend, which the GUI talks to over a JSON-lines IPC channel.
--- ---
## Features ## 📖 Introduction
- **Profile management** — create and switch between Nostr profiles (`npub` addresses) Nostr Feed Manager pairs a hardened **Rust core** (the same engine behind the original CLI) with a
- **Compose screen** — write or preview a note before publishing: polished **Electron + React** desktop interface. All Nostr work — key generation, event signing,
- **Write / Preview tabs** with a soft length limit and live character count relay communication, encryption — happens inside the Rust backend. The GUI talks to it over a
- **Image attachments** — pick a local image, upload it to [nostr.build](https://nostr.build), secure JSON-lines IPC channel and **never sees your secret keys**.
and publish a note that carries NIP-92 `imeta` (plus legacy `image`) tags so clients render it
- **Link preview cards** — up to three web links per note get a title/image/description card
(fetched locally; your note text is unchanged, clients render their own cards too)
- **Publishing with receipts** — per-relay publish reports: you always know where a note was accepted
- **Relay management** — add, remove, enable/disable, and test relays
- **Encrypted vault** — secret keys are encrypted at rest with AES-256-GCM under an Argon2id-derived
key. Without a password set, the vault is stored in plaintext (readable only by your user account)
and this is disclosed in the app
- **Secret key recovery** — reveal a profile's secret key (hex + `nsec1...`) from the app or the CLI,
only after the vault password is entered, so keys stay encrypted at rest
- **Settings** — light / dark / system theme, publish confirmation, key shortening, and vault backup
- **One binary, two interfaces** — the same Rust crate provides a full CLI and the GUI's IPC server
## Architecture It also turns your machine into a **NIP-46 remote signer ("bunker")**: instead of pasting your
`nsec` into other Nostr apps, they send signing requests here, and you approve each one with a
single click. This is central to keeping your keys out of third-party apps while staying
interoperable across the Nostr ecosystem.
``` **Status: early beta (v0.1.0).** Actively developed and usable daily, but not yet packaged for
┌──────────────────────────┐ JSON-lines over stdio ┌──────────────────────────┐ distribution repositories. Expect some API churn until 1.0. No Docker or database required.
│ Electron (React) GUI │ ────────────────────────────────▶ │ Rust backend │
│ - React + TypeScript │ {"id":1,"method":"publish_note",│ - nostr-sdk 0.40 │ ## ✨ Features
│ - renders in app:// │ "params":{"content":"..."}} │ - vault / keys / signing │
│ - never sees secret keys │ ◀──────────────────────────────── │ - relay communication │ - **Profile management** — create and quickly switch between Nostr profiles (`npub` identities)
└──────────────────────────┘ {"id":1,"status":"ok","data":…} └──────────────────────────┘ - **Compose screen** — write with live character count and instant **Write / Preview** tabs
- **Image attachments** — pick a local image, upload to [nostr.build](https://nostr.build), and
publish with NIP-92 `imeta` (plus legacy `image`) tags so clients render it
- **Link preview cards** — up to three URLs per note get a title/image/description preview
- **Publishing receipts** — per-relay publish reports, so you always know where a note landed
- **Relay management** — add, remove, enable/disable, and latency-test relays from GUI or CLI
- **Feed aggregation** *(coming soon)* — an aggregated view of incoming text notes and link cards
- **Encrypted vault** — secret keys encrypted at rest with **AES-256-GCM** under a key derived via
**Argon2id** from your password
- **Secret key recovery** — reveal a key (hex + `nsec1...`) only after unlocking, from GUI or CLI
- **NIP-46 remote signer** — sign for other Nostr apps; every sign/decrypt request needs your
explicit **Approve**/**Reject**
- **Settings** — light / dark / system theme, publish confirmation, key shortening, vault backup
- **Two interfaces, one core** — the same Rust crate powers a full CLI *and* the GUI's IPC server
## 🏗️ Architecture
The design keeps a clean, security-critical boundary: **the renderer never sees secret keys.**
```mermaid
flowchart LR
subgraph UI[Electron + React]
R[Renderer<br/>React 18 / TS]
M[Main process<br/>spawns backend, dialogs, HTTP]
end
subgraph Core[Rust backend]
IPC[JSON-lines IPC]
S[Signer · NIP-46]
V[Vault · AES-256-GCM]
REL[Relay client · nostr-sdk]
end
subgraph Net[Network]
RELAYS[(Relays)]
HU[nostr.build]
end
R <-->|window.backend bridge| M
M <-->|"stdin/stdout 1 JSON per line"| IPC
IPC --> S
IPC --> V
IPC --> REL
REL --> V
S <--> RELAYS
REL --> HU
``` ```
- `src/` — the Rust library (`nostr-manager-backend`). Exposes the same functionality as a Or, as a plain step-by-step flow:
command-line binary and as a long-running `serve` process.
- `frontend/electron/` — the Electron main and preload scripts. The main process spawns the
Rust backend and correlates requests by `id`. Native-adjacent work that Rust shouldn't do (file
pickers, HTTP, clipboard) lives here.
- `frontend/src/` — the React renderer. It talks only to a thin `window.backend` bridge; profile
summaries and publish reports contain no secret material.
**Security model:** secret keys never leave the Rust backend, and the renderer never sees them. 1. **Client (UI)** — the React renderer requests an action (e.g. `publish_note`) through the
Image uploads to nostr.build are authorized with a NIP-98 auth event signed by the active profile's `window.backend` bridge.
key inside the backend. 2. **Electron main** — forwards the request to the Rust backend over stdio as one JSON object per
line.
3. **Backend** — the `serve` IPC loop dispatches to the right module (`profiles`, `publish`,
`relays`, `settings`, `signer`).
4. **Relays** — the backend publishes signed events to the configured relays and returns
per-relay receipts.
## Tech stack ### Tech stack
**Backend (Rust)** — built and linted with **Cargo**, **rustfmt**, and **Clippy** **Backend (Rust)** — Cargo · rustfmt · Clippy
| Tool | Purpose | | Tool | Purpose |
| --- | --- | | --- | --- |
| [nostr-sdk](https://crates.io/crates/nostr-sdk) 0.40 (NIP-44, NIP-98) | Nostr protocol, relay client, signing | | [nostr-sdk](https://crates.io/crates/nostr-sdk) 0.40 (NIP-44, NIP-46, NIP-98) | Nostr protocol, relay client, signing, remote signer |
| tokio | async runtime | | tokio | async runtime |
| argon2 + aes-gcm | vault key derivation and at-rest encryption | | argon2 + aes-gcm | vault key derivation and at-rest encryption |
| serde / serde_json | IPC and storage encoding | | serde / serde_json | IPC and storage encoding |
| rpassword | interactive password prompts | | rpassword | interactive password prompts |
**Frontend (TypeScript)** — managed with **npm** **Frontend (TypeScript)** — npm
| Tool | Purpose | | Tool | Purpose |
| --- | --- | | --- | --- |
@ -90,16 +119,15 @@ key inside the backend.
| ESLint + Prettier | linting and formatting | | ESLint + Prettier | linting and formatting |
| electron-builder | Linux packaging | | electron-builder | Linux packaging |
## Requirements ## 🚀 Installation & setup
### Prerequisites
- **Linux** with a display server (X11 or Wayland) - **Linux** with a display server (X11 or Wayland)
- **Rust** (stable) and Cargo - **Rust** (stable) and Cargo
- **Node.js 20+** and npm - **Node.js 20+** and npm 10+
No Docker or database is required. The Rust backend also exists as a standalone CLI if you prefer No Docker, database, or account setup is required.
the terminal.
## Installation & quick start
### 1. Clone and build ### 1. Clone and build
@ -118,18 +146,27 @@ npm install
### 2. Run ### 2. Run
```sh ```sh
# Production-style: renders from the built bundle via app:// # Renders from the built bundle via app://
cd frontend
npm start npm start
``` ```
On first launch the app finds a vault left behind by the original Python CLI (if any) in its On first launch the app detects a vault left behind by the original Python CLI (if you have one),
working directory, backs it up to a timestamped `*.backup-<ts>` file, and imports your profiles. backs it up to a timestamped `*.backup-<ts>` file, and imports your profiles — leaving the original
The original file is left untouched. untouched.
### 3. Configure ### 3. Configure environment
The app reads configuration from environment variables — it does not load `.env` files itself, but The app reads its configuration from environment variables. It does not load a `.env` file
you can export them from your shell or an `.env` sourced before launching: directly, but you can export these from your shell or source an `.env` before launching:
| Variable | Purpose | Default |
| --- | --- | --- |
| `NFM_PASSWORD` | Vault password for non-interactive CLI use (create / publish / show-secret) | _prompt_ |
| `NOSTR_GUI_DEV_URL` | Renderer dev-server URL for hot reload (development only) | — |
| `XDG_DATA_HOME` | Override the data directory | `~/.local/share/nost-feed-manager` |
**`.env.example`** — copy to `.env` and adjust:
```bash ```bash
# .env — sourced by your shell, not read directly by the app # .env — sourced by your shell, not read directly by the app
@ -144,6 +181,9 @@ NOSTR_GUI_DEV_URL=http://localhost:5173
XDG_DATA_HOME= XDG_DATA_HOME=
``` ```
> ⚠️ Keep `.env` out of version control. It is **not** gitignored by default — add it to
> `.gitignore` if you create one in the repo root, and never commit real passwords.
### 4. Package (optional) ### 4. Package (optional)
```sh ```sh
@ -151,28 +191,19 @@ cd frontend
npm run dist # builds renderer + backend and runs electron-builder npm run dist # builds renderer + backend and runs electron-builder
``` ```
The unpacked application lands in `frontend/release/linux-unpacked/`; launch it with The unpacked app lands in `frontend/release/linux-unpacked/`; run it with `./nost-feed-manager`.
`./nost-feed-manager`.
## Usage ## 🧑‍💻 Usage guide
### Desktop app ### Desktop app
The GUI covers profiles, composing/publishing, relays, and settings. Tabs cover **Compose**, **Home**, **Profiles**, **Relays**, **Settings**, and **Signer**. Connect
a relay by entering its `wss://` URL, then compose a note, attach an image, and publish to see a
> **Screenshot placeholder** — add `docs/screenshots/compose.png` (the Compose screen with the per-relay report.
> Write / Preview tabs, an attached image, and a link-preview card) and reference it here.
>
> ```md
> ![Compose screen](docs/screenshots/compose.png)
> ```
>
> **GIF placeholder** — add `docs/screenshots/compose-demo.gif` (attaching an image and publishing)
> and reference it here.
### Command-line interface ### Command-line interface
The Rust crate builds a single binary with both a CLI and the GUI IPC server: The Rust crate builds a single binary that acts as both the GUI's backend and a full CLI:
```sh ```sh
cargo run --release -- create "Alice" # create a profile cargo run --release -- create "Alice" # create a profile
@ -180,56 +211,64 @@ cargo run --release -- list # list profiles (no secret keys
cargo run --release -- switch <npub> # select the active profile cargo run --release -- switch <npub> # select the active profile
cargo run --release -- publish <npub> "Hello" # publish a text note cargo run --release -- publish <npub> "Hello" # publish a text note
cargo run --release -- relays list|add|remove|enable|disable|test cargo run --release -- relays list|add|remove|enable|disable|test
cargo run --release -- settings get|set theme|confirm|shorten cargo run --release -- settings get|set <key> <value>
cargo run --release -- set-password # encrypt the vault (or change its password) cargo run --release -- set-password # encrypt the vault (or change its password)
cargo run --release -- remove-password # remove vault encryption cargo run --release -- remove-password # remove vault encryption
cargo run --release -- unlock # verify the vault password for this process cargo run --release -- unlock # verify the vault password for this process
cargo run --release -- show-secret <npub> # reveal a profile's secret key (hex + nsec) cargo run --release -- show-secret <npub> # reveal a profile's secret key
cargo run --release -- signer status # show the signer profile + relays
cargo run --release -- signer connect <nostrconnect://...>
cargo run --release -- info # show storage locations and version cargo run --release -- info # show storage locations and version
cargo run --release -- serve # JSON-lines IPC server (used by the GUI) cargo run --release -- serve # JSON-lines IPC server (used by the GUI)
``` ```
Passwords are read from the `NFM_PASSWORD` environment variable when set, otherwise you are Passwords are read from `NFM_PASSWORD` when set, otherwise you are prompted. They are **never**
prompted interactively. They are never accepted as command-line arguments. `create` and `publish` accepted as command-line arguments — `create`, `publish`, `unlock`, and `show-secret` all unlock
prompt for the vault password automatically when the vault is encrypted, and so does `show-secret` the vault automatically when it is encrypted.
the key is only displayed after the vault password is entered.
> **CLI + signer caveat:** the CLI's blocking `signer connect` loop cannot answer approval
> prompts — run the **GUI** to approve NIP-46 requests.
### Remote signer (GUI)
The **Signer** screen sets up the **NIP-46 remote signer** with the active profile. Paste a
`nostrconnect://` link from another Nostr app. When that app asks to sign an event or
decrypt/encrypt a message, a **Requests waiting for approval** panel appears with an
**Approve**/**Reject** button. Nothing is signed until you confirm it.
### IPC protocol (for tooling) ### IPC protocol (for tooling)
The `serve` command speaks one JSON object per line. The Electron main process does the same thing The `serve` command speaks one JSON object per line — the same protocol Electron adopts:
when it spawns the backend:
```json ```json
{"id": 1, "method": "publish_note", "params": {"npub": "...", "content": "Hello world"}} {"id": 1, "method": "publish_note", "params": {"npub": "...", "content": "Hello world"}}
{"id": 1, "status": "ok", "data": {"npub": "...", "reports": [{"relay": "wss://nos.lol", "accepted": true}]}} {"id": 1, "status": "ok", "data": {"npub": "...", "report": {"event_id": "...", "succeeded": ["wss://relay.damus.io"]}}}
``` ```
## Storage and security ## 🔐 Security notes
- The vault (`profiles_vault.json`) and settings live in Your keys are the crown jewels in any Nostr app, and nothing here compromises them:
`$XDG_DATA_HOME/nost-feed-manager/` (defaulting to `~/.local/share/nost-feed-manager/`),
created with permissions `0700` for the directory and `0600` for the files.
- The vault is stored in plaintext until you set a password (Settings → Storage, or
`set-password` in the CLI). Once protected, every secret key is encrypted at rest with
AES-256-GCM under a key derived from your password with Argon2id. Labels and public keys stay
readable so profiles can be browsed while the vault is locked. You unlock once per session; the
derived key lives only in memory and is never written to disk.
- Anyone with access to your user account can still read the vault file, so the password is a
defence-in-depth layer, not a replacement for keeping your account secure.
## Development guide - **Secret keys stay in Rust.** The renderer never receives secret key material; it only sees
profile summaries and publish reports.
- **Encrypted at rest.** The vault (`profiles_vault.json`) is plaintext until you set a password
(`set-password`, or Settings → Storage). Once set, every secret key is encrypted with
**AES-256-GCM** under a key derived from your password with **Argon2id**. Labels and public keys
remain readable so you can browse profiles while the vault is locked.
- **In-memory key only.** You unlock once per session; the derived key lives only in memory and is
never written to disk.
- **Approve-before-any-signing.** The NIP-46 remote signer will not sign, encrypt, or decrypt
until you explicitly approve each request.
- **Least-privileged storage.** Files are written with directories `0700` and files `0600`.
NIP-98 auth events authorize image uploads to nostr.build without exposing sk signs.
### Building from source > **Defence in depth.** Anyone with access to your user account can still read vault files. The
> password is an additional layer, not a replacement for securing your account, and the underlying
> OS should be considered your last line of defense.
```sh ## 🛠️ Development
cargo build --release # Rust backend
cd frontend
npm install
npm run build # renderer type-check + Vite build
npm run electron:build # compile Electron main/preload
```
### Running with hot reload ### Run with hot reload
```sh ```sh
# terminal 1 — Vite dev server # terminal 1 — Vite dev server
@ -240,109 +279,84 @@ npm run dev
NOSTR_GUI_DEV_URL=http://localhost:5173 npm start NOSTR_GUI_DEV_URL=http://localhost:5173 npm start
``` ```
### Tests and linting ### Verify (all green — `cargo 75`, `npm 71`, clippy/fmt/typecheck/lint/format clean)
**All verification is green (cargo 64 tests, npm 65 tests,
clippy/fmt/typecheck/lint/format/builds clean) per the AGENTS.md checkpoint rule.** The project's
`AGENTS.md` mandates a full verification run before every change lands, and that status is recorded
in the committed checkpoint.
```sh ```sh
# Rust # Rust
cargo test # 64 unit tests cargo test # 75 unit tests
cargo fmt --check # rustfmt cargo fmt --check # rustfmt
cargo clippy --all-targets cargo clippy --all-targets
# Frontend # Frontend
cd frontend cd frontend
npm run typecheck # TypeScript (renderer + electron) npm run typecheck # TypeScript (renderer + electron)
npm run lint # ESLint npm run lint # ESLint
npm run format:check # Prettier npm run format:check # Prettier
npm test # Vitest (jsdom), 65 tests across 12 files npm test # Vitest (jsdom) — 71 tests across 13 files
``` ```
The test suites exercise the real IPC protocol through a fake backend, so new features are The test suites exercise the real IPC protocol against a fake backend, so features are tested at
tested at the protocol boundary rather than in isolation. the protocol boundary rather than in isolation.
### Project layout ### Project layout
``` ```
src/ Rust library + CLI + IPC server src/ Rust library + CLI + IPC server
app.rs application state, vault password/unlock lifecycle app.rs app state, vault password/unlock lifecycle
crypto.rs Argon2id key derivation + AES-256-GCM encryption crypto.rs Argon2id derivation + AES-256-GCM encryption
errors.rs structured AppError errors.rs structured AppError
ipc.rs JSON-lines serve() loop and request/reply envelope ipc.rs JSON-lines serve() loop and request/reply envelope
main.rs CLI entry point main.rs CLI entry point
profiles.rs profile create/list/select profiles.rs profile create/list/select, secret recovery
publish.rs note publishing with per-relay reports, image/`imeta` tagging publish.rs note publishing with receipts, images and imeta tags
relays.rs default relays, validation, connection tests relays.rs default relays, validation, connection tests
settings.rs theme and user preferences settings.rs theme and preferences
uploads.rs NIP-98 authorization for image-host uploads signer.rs NIP-46 remote signer + approval gate
vault.rs vault storage (plaintext or password-encrypted) and migration uploads.rs NIP-98 auth for image-host uploads
vault.rs vault storage (plaintext or password-encrypted)
frontend/ frontend/
electron/ Electron main + preload (backend spawn, native dialogs, electron/ Electron main + preload (backend spawn, dialogs, clipboard)
image uploads, link-preview fetching, clipboard)
src/ src/
screens/ Compose, Home, Profiles, Relays, Settings screens/ Compose, Home, Profiles, Relays, Settings, Signer
lib/ types, api bridge, media helpers (URL extraction) lib/ types, api bridge, media helpers
state/ React context (AppProvider) state/ React context (AppProvider)
test/ Vitest suite with a fake backend speaking the real protocol test/ Vitest suite over a fake backend
styles.css global styles package.json scripts + electron-builder config
package.json scripts and electron-builder config
``` ```
## Contributing ## 🤝 Contributing
Contributions are welcome — bug reports, documentation, and pull requests all help. Please be Contributions are welcome — bug reports, documentation, and pull requests all help. Please be
respectful and constructive; everyone is expected to follow the [Code of Conduct](#code-of-conduct). respectful and constructive; everyone is expected to follow the
[Code of Conduct](#code-of-conduct).
### Reporting bugs ### Reporting bugs
Open an issue on the [issue tracker]([YOUR_FORGEJO_INSTANCE_URL]/<OWNER>/<REPO>/issues) and include: Open an issue on the [issue tracker]([YOUR_FORGEJO_INSTANCE_URL]/<OWNER>/<REPO>/issues) and include:
- A clear title and description of the expected vs. actual behavior - A clear title and description of expected vs. actual behavior
- Steps to reproduce - Steps to reproduce
- Platform details (distro, Wayland/X11, app version from `info`) - Platform details (distro, Wayland/X11, version from `info`)
- Any relevant log output - Relevant log output
### Requesting features
Open a feature request issue and describe the problem you're trying to solve, not just the feature
you want. That makes it easier to design something that fits the existing architecture.
### Opening a pull request ### Opening a pull request
1. **Fork** the repository using the **Fork** button on Forgejo. 1. **Fork** the repository from the **Fork** button on Forgejo.
2. **Branch** from `master` with a descriptive name: `fix/relay-test-timeout`, 2. **Branch** from `master` with a descriptive name (`fix/relay-timeout`, `feat/avatar-support`).
`feat/avatar-support`, etc. 3. **Make your change.** Match the surrounding style and keep the diff focused.
3. **Make your change.** Match the surrounding style (see below) and keep the diff focused. 4. **Run the full verification suite** (see [Verify](#🛠️-development)) — everything must pass.
4. **Run the full verification suite** (see [Tests and linting](#tests-and-linting)) — everything 5. **Write tests** for new behavior (test through the IPC surface wherever possible).
must pass. 6. **Update docs** for user-visible changes.
5. **Write tests** for new behavior. Existing tests exercise the IPC protocol end to end, so 7. **Commit** with a concise imperative subject line, **push** your branch, and open a **pull
prefer testing through that public surface. request** against `master`.
6. **Update docs** if the change is user-visible (README, feature list). 8. In the PR description, summarize the change, link issues, and list what you tested.
7. **Commit** with a concise imperative subject line (e.g. `Add relay connection timeout`), then
**push** your branch to your fork and open a **Pull Request** against `master` on Forgejo.
8. In the PR description, summarize the change, link any related issues, and list what you tested.
### Code style ### Code style
- **Rust:** formatted with `cargo fmt`; `cargo clippy --all-targets` clean; public items get doc - **Rust:** `cargo fmt`; `cargo clippy --all-targets` clean; public items get doc comments
comments; errors use the structured `AppError` types. - **TypeScript/React:** Prettier + ESLint clean; strict `tsconfig`
- **TypeScript / React:** formatted with Prettier; ESLint clean; `tsconfig` strictness is - **General:** no secrets in code, commits, or logs; no generated files checked in
enforced by `npm run typecheck`; tests use Vitest + Testing Library (user-event, never raw
`fireEvent` where user-event applies).
- **General:** no secrets in code, commits, or logs; no generated files checked in; keep changes
minimal and reviewable.
## Community & support ## 📄 License
- **Issues** — [bug reports and feature requests]([YOUR_FORGEJO_INSTANCE_URL]/<OWNER>/<REPO>/issues) **MIT** — see the [LICENSE](LICENSE) file. Copyright (c) 2026 Avi.
- **Wiki** — [project wiki]([YOUR_FORGEJO_INSTANCE_URL]/<OWNER>/<REPO>/wiki) for longer-form
documentation; questions can also be filed as issues.
- **Code of Conduct** — <a id="code-of-conduct"></a> until `CODE_OF_CONDUCT.md` is added,
contributors are expected to follow the [Contributor Covenant v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
## License
MIT. See the [LICENSE](LICENSE) file. Copyright (c) 2026 Avi.