Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/content/docs/cli/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,23 @@ recur install https://example.com/plugin.tar.gz # Download and install
recur uninstall <id> # Remove plugin
```

## App Bundles

```sh
recur app install ./habits.recur # Install a .recur bundle and register it
recur app install ./habits.recur --name morning # Install under a chosen name
recur app install ./habits.recur --force # Overwrite an existing app, no prompt
recur app install https://example.com/habits.recur # Install from a URL
recur app list # Installed apps and registration status
recur app remove habits # Deregister and delete an app
recur app pack ./habits # Build ./habits.recur from a directory
recur app pack ./habits -o out.recur # Choose the output path
```

Apps are unpacked into `~/.config/recur/app/<name>/`. Installing while the daemon
is stopped still unpacks the app; it registers automatically on the next daemon
start. URL installs require the host in `allowed_hosts` (see Settings).

## Settings

```sh
Expand Down
105 changes: 105 additions & 0 deletions docs/content/docs/configuration/app-bundles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
---
title: "App Bundles"
weight: 4
description: "Package, share, and install recur automations as .recur bundles"
---

An **app bundle** packages a recurfile together with the local scripts it needs
into a single `.recur` file. Instead of sharing a loose recurfile plus a folder
of scripts, you can hand someone one file — they install it with a single
command and it registers itself with their daemon.

A bundle is just a **zip archive** with a `.recur` extension. It contains:

- a **recurfile** — the single YAML file at the archive root, and
- any **scripts or assets** your automation calls, in whatever layout you like.

There is no separate manifest and no special schema — a bundle is a recurfile
and its files. See the **Recurfile Format** guide for how to write the recurfile
itself.

## Installing an app

```sh
recur app install ./habits.recur
```

This unpacks the bundle into `~/.config/recur/app/<name>/` and registers its
recurfile. You can also install directly from a URL:

```sh
recur app install https://example.com/apps/habits.recur
```

Downloads are only allowed from hosts you have permitted, using the same
`allowed_hosts` setting as plugin installs:

```sh
recur config set allowed_hosts example.com
```

**If the daemon isn't running,** the app is still unpacked and will be
registered automatically the next time the daemon starts — you don't need to run
`recur register` yourself.

### Choosing the app name

By default the app name comes from the recurfile's filename (e.g. `habits.yaml`
→ app `habits`). If the recurfile uses the generic name `recurfile.yaml`, the
bundle's own filename is used instead. Override it explicitly with `--name`:

```sh
recur app install ./habits.recur --name morning-routine
```

If an app with that name already exists, you'll be prompted to overwrite or
abandon. Use `--force` to overwrite without prompting.

## Listing and removing apps

```sh
recur app list # installed apps and whether each is registered
recur app remove habits # deregister and delete the app
```

## Creating a bundle

Lay out a directory with your recurfile at the top and scripts alongside it:

```
habits/
├── habits.yaml # the recurfile (any single root YAML works)
└── scripts/
├── prompt.sh
└── record.sh
```

Reference the scripts from the recurfile as usual, then pack the directory:

```sh
recur app pack ./habits # writes ./habits.recur
recur app pack ./habits -o out.recur
```

`pack` preserves your directory layout and file modes (so an executable script
stays executable after install), and it fails if the directory has no root
recurfile.

### Naming the recurfile

Inside a bundle the recurfile is simply the one YAML file at the root, so you can
give it a meaningful name like `habits.yaml` — that name becomes the default app
name. YAML files inside subdirectories are treated as assets, not the recurfile.
If you include more than one YAML at the root, name one of them `recurfile.yaml`
to disambiguate; otherwise the bundle is rejected as ambiguous.

## Where apps live

Installed apps are unpacked as-is under:

```
~/.config/recur/app/<name>/
```

You can inspect an installed app's files there directly. Removing an app deletes
this directory.
114 changes: 114 additions & 0 deletions requirements/app-bundles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# App Bundles

## Description

An **app bundle** is a single-file, distributable unit of recur automation: a
`.recur` archive containing a recurfile plus any local scripts the automation
needs. Installing a bundle unpacks it into a managed per-app directory and
registers its recurfile with the daemon, so an "app" (for example a habit
tracker that prompts and records responses) can be shared and installed as one
file rather than a loose recurfile plus scattered scripts.

App bundles deliberately introduce **no recurfile schema change** and no new
manifest format; a bundle is just a recurfile and its assets in a zip.

## Bundle Format

- A bundle is a **zip archive** with the `.recur` extension.
- The **recurfile is the single YAML file at the archive root** (`*.yaml` or
`*.yml`). Any meaningful filename is accepted — its stem is used to name the
app — so the strict `recurfile.*` naming convention is *not* required inside a
bundle.
- YAML files in subdirectories are treated as app assets, never as the
recurfile.
- All other files (scripts, data, nested directories) are carried verbatim;
directory layout and file modes (e.g. the executable bit on scripts) are
preserved on both pack and unpack.

**Recurfile selection rules:**
- Exactly one root YAML → that file is the recurfile.
- Multiple root YAML files → if exactly one is named per the conventional
`recurfile.*` form it is used as a tie-breaker; otherwise the bundle is
rejected as ambiguous.
- No root YAML → the bundle is rejected.

## Installed Layout

- Installed apps live under `~/.config/recur/app/<name>/`.
- A bundle is unpacked **as-is** into its app directory, preserving layout.
- `<name>` is a single path segment (no separators, not `.` or `..`).

## `recur app install`

Accepts a local `.recur` path **or** an `http(s)` URL.

**Source resolution:**
- A URL is downloaded to a temporary file first. The URL host must be permitted
by the `allowed_hosts` config (same gate as `recur install` for plugins);
otherwise the command errors with guidance to add the host.
- A local path is used directly.

**Name resolution (highest priority first):**
1. `--name <name>` flag.
2. The recurfile's filename stem — unless it is the generic `recurfile`, whose
stem carries no meaning.
3. The bundle's filename stem (with `.recur` removed).

**Behavior:**
- The bundle is unpacked into a staging directory and validated first, so an
invalid bundle never clobbers an already-installed app.
- If `~/.config/recur/app/<name>/` already exists, the user is prompted to
overwrite or abandon. `--force` overwrites without prompting; a
non-interactive (closed) stdin is treated as abandon.
- After unpacking, the recurfile is registered:
- **Daemon running:** registered immediately via the daemon.
- **Daemon stopped:** the app is left unpacked and a hint is printed; the
daemon registers it on next start (see startup scan below). Installation
still succeeds.

## `recur app list`

Lists installed apps (directories under `~/.config/recur/app/` that contain a
recurfile). When the daemon is running, each app is annotated as `registered`
or `not registered` by matching its recurfile path against the daemon's
registered recurfiles. `--json` emits the structured form.

## `recur app remove <name>`

Best-effort deregisters the app's recurfile from the daemon if it is running,
then removes `~/.config/recur/app/<name>/`. Removal proceeds even if the daemon
is stopped or the app was never registered.

## `recur app pack <dir>`

Creates a `.recur` bundle from a directory. The directory must contain a
recurfile (a single root YAML) or packing fails. Output defaults to
`<dir>.recur`; `--output/-o` overrides.

## Daemon Startup Scan

On startup, after replaying persisted state, the daemon scans
`~/.config/recur/app/` and registers the recurfile of any app that is not
already registered. This lets apps installed while the daemon was stopped
register themselves without a manual `recur register`.

- Registration is idempotent: apps already restored from the state file (by
physical file identity) are skipped.
- Directories without a recurfile, and dotfile-prefixed staging directories, are
ignored.
- A malformed app recurfile is logged and skipped; it does not abort startup.

## Acceptance Criteria

- A directory with a root recurfile and scripts round-trips through
`pack` → `install`, preserving nested files and executable bits.
- A bundle with no root YAML, or with an ambiguous set of root YAML files, is
rejected by both `pack` and `install`.
- A traversal entry (e.g. `../escape`) in a crafted archive is rejected on
unpack without writing outside the destination.
- Installing while the daemon is stopped unpacks the app and, on next daemon
start, the app is registered automatically.
- Installing over an existing app prompts; answering no leaves the existing app
intact; `--force` replaces it.
- `--name`, recurfile-stem, and bundle-stem name resolution behave in that
priority order.
42 changes: 42 additions & 0 deletions requirements/cli-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ The full verb-noun command tree, derived from the daemon API:
- `recur install <path>` — install a plugin from directory or archive
- `recur uninstall <id>` — remove a plugin (errors if triggers/actions registered)

**App bundle commands:**
- `recur app install <bundle.recur|url>` — install a `.recur` app bundle and register its recurfile
- `recur app list` — list installed apps and their registration status
- `recur app remove <name>` — deregister and remove an installed app
- `recur app pack <dir>` — create a `.recur` bundle from a directory

**Config commands:**
- `recur config get [key]` — show config value(s). No key = show all.
- `recur config set <key> <value>` — set a config value (persisted immediately)
Expand Down Expand Up @@ -250,6 +256,42 @@ Remove a plugin from the plugin directory.
- Errors if any triggers or actions from the plugin are currently registered — user must deregister them first
- On success: removes the plugin directory

### `recur app install <bundle.recur|url>`

Install a `.recur` app bundle (a zip of a recurfile plus its local scripts) into `~/.config/recur/app/<name>/` and register its recurfile.

| Flag | Description |
|---|---|
| `--name <name>` | Install under this app name instead of the derived default. |
| `--force` | Overwrite an existing app of the same name without prompting. |

**Argument:** A local `.recur` path or an `http(s)` URL. URLs are downloaded first and their host must be permitted by `allowed_hosts` (same gate as `recur install`).

**Name resolution:** `--name`, else the recurfile's filename stem, else (when that stem is the generic `recurfile`) the bundle's filename stem.

**Behavior:**
- Unpacks to a staging dir and validates before touching the destination, so a bad bundle never clobbers an installed app.
- If the target app already exists: prompt to overwrite or abandon (`--force` skips the prompt; a non-interactive stdin abandons).
- Daemon running: registers immediately. Daemon stopped: leaves the app unpacked and prints a hint — the [startup scan](daemon-lifecycle.md) registers it on next start. Install still succeeds.

See [App Bundles](app-bundles.md) for the bundle format and recurfile-selection rules.

### `recur app list`

List installed apps. Each app shows a `registered` / `not registered` status when the daemon is running. Supports `--json`.

### `recur app remove <name>`

Best-effort deregister the app's recurfile (if the daemon is running), then remove `~/.config/recur/app/<name>/`. Proceeds even when the daemon is stopped.

### `recur app pack <dir>`

Create a `.recur` bundle from a directory. The directory must contain a root recurfile (a single YAML) or packing fails.

| Flag | Description |
|---|---|
| `--output`, `-o` | Output bundle path (default: `<dir>.recur`). |

### `recur config get [key]`

Show configuration values.
Expand Down
1 change: 1 addition & 0 deletions requirements/daemon-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Auto-start on login via system service for persistent automations
- Single instance enforced via PID file at `~/.config/recur/run/recurd.pid`
- On startup: configure logging level, read config, load state file, load plugin manifests, resume persisted triggers
- After state replay, scan `~/.config/recur/app/` and register any [app bundle](app-bundles.md) not already registered (so apps installed while the daemon was stopped register themselves); registration is idempotent
- The `recurd` binary accepts `--log-level` to override the config file's `log_level` at startup

**Shutdown:**
Expand Down
Loading
Loading