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
3 changes: 3 additions & 0 deletions .agents/notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@
[0] When planning Ledger v2 migrations for this repo, confirm rollout expectations first: if the user is resetting the non-production local DB, remove backfill work entirely and prefer simple additive/drop-recreate migrations (especially for table-shape changes like Monzo category mappings).
[0] In Fastify query schemas with coercion enabled, `dryRun=true` can match both boolean and string branches; using `oneOf` for `dryRun` (`boolean` + `'true'|'false'|'1'|'0'`) may fail validation with "must match exactly one schema". Use `anyOf` (or a single normalized schema) for approval-flow query params.
[0] When refactoring PWA React pages/components in this repo and a React best-practices skill is available (for example `vercel-react-best-practices`), load and apply it before making UI changes; the user explicitly expects skill usage, not just local reasoning.
[0] The global `tithe` shim under `~/Library/pnpm/` is just a launcher script; after `pnpm link --global ./apps/cli` it executes the workspace path (`.../apps/cli/dist/index.js`), so `tithe web` always uses the current local checkout via relative workspace-root resolution.
[0] For daemon metadata files, resolve path by checking existing state/pid files across candidates (`~/.tithe`, workspace fallback) before choosing writable location, and pass the chosen directory to child/supervisor processes to keep `status`/`stop` targeting consistent across privilege contexts.
[0] When using `gh pr create --body` through shell commands, avoid unescaped backticks in the argument string because zsh command substitution can execute unintended commands; prefer `--body-file` for multiline markdown.
15 changes: 10 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ Failure:

### Web runtime

- `tithe web [--mode dev|preview] [--api-port <port>] [--pwa-port <port>]`
- `tithe --json web [--mode dev|preview] [--api-port <port>] [--pwa-port <port>]`
- `tithe web [--mode dev|preview] [--api-port <port>] [--pwa-port <port>] [--daemon|--status|--stop]`
- `tithe --json web [--mode dev|preview] [--api-port <port>] [--pwa-port <port>] [--daemon|--status|--stop]`

### Monzo

Expand All @@ -120,11 +120,16 @@ Failure:
- DB migrations are expected to run lazily on command execution, not on help-only invocations.
- API and CLI entrypoints auto-load workspace-root `.env` via `dotenv` if present (existing exported env vars still take precedence).
- Default `DB_PATH` is `~/.tithe/tithe.db`; leading `~` is expanded to the current user's home directory.
- `tithe web` launches API + PWA in foreground mode (`--mode dev` by default).
- `tithe web --mode preview` builds `@tithe/api` and `@tithe/pwa` before launch.
- `tithe web` launches API + PWA in foreground mode (`--mode preview` by default).
- `tithe web --daemon` starts a detached supervisor process that auto-restarts API/PWA if either child process crashes or is killed.
- `tithe web --status` reports daemon state and access metadata from `~/.tithe/web-daemon.state.json` (fallback path when `~/.tithe` is not writable: `<workspace>/.tithe/web-daemon.state.json`).
- `tithe web --stop` sends `SIGTERM` to the daemon supervisor and waits for shutdown.
- `tithe web --mode preview` builds `@tithe/api` and `@tithe/pwa` before launch (foreground and daemon-supervisor startup).
- `--api-port` overrides API `PORT`; for `tithe web`, PWA `VITE_API_BASE` is preserved by default and has its port rewritten when `--api-port` is provided (fallback: `http://<api-host>:<api-port>/v1`).
- `--pwa-port` sets `PWA_PORT` in `dev` mode or `PWA_PREVIEW_PORT` in `preview` mode.
- `tithe --json web` emits one startup envelope first, then streams prefixed service logs.
- foreground `tithe --json web` emits one startup envelope first, then streams prefixed service logs.
- daemon startup/status/stop (`--daemon|--status|--stop`) emit a single JSON envelope and exit (no live log stream).
- web startup payloads include local PWA/API URLs and best-effort Tailnet URLs; if Tailscale is unavailable, startup continues with a warning and local URLs.
- PWA API requests use a 10-second timeout and transition to error state if backend is unreachable.
- `tithe --json monzo connect` stores short-lived OAuth `state` and returns `authUrl`.
- `GET /v1/integrations/monzo/connect/callback` requires query `code+state` or `error`.
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,19 +304,27 @@ Use `tithe web` to launch API + PWA together in the foreground:
```bash
tithe web
tithe web --mode preview
tithe web --daemon
tithe --json web --status
tithe --json web --stop
tithe web --api-port 9797 --pwa-port 5174
tithe --json web --mode dev
```

Runtime notes:

- `--mode dev` is the default.
- `--mode preview` is the default.
- `--daemon` starts a detached supervisor that auto-restarts API/PWA when child processes crash or are killed.
- `--status` returns daemon state from `~/.tithe/web-daemon.state.json` (fallback path when `~/.tithe` is not writable: `<workspace>/.tithe/web-daemon.state.json`).
- `--stop` requests daemon shutdown (SIGTERM) and waits for exit.
- `--mode preview` automatically runs `@tithe/api` and `@tithe/pwa` builds before starting preview services.
- `tithe web` preserves configured `VITE_API_BASE` by default.
- If `--api-port` is set, `tithe web` rewrites the port in `VITE_API_BASE` when possible and falls back to `http://<api-host>:<api-port>/v1`.
- `--api-port` overrides API `PORT` for this command.
- `--pwa-port` maps to `PWA_PORT` in dev mode and `PWA_PREVIEW_PORT` in preview mode.
- `--json` emits one startup envelope before live prefixed logs are streamed.
- foreground `--json` emits one startup envelope before live prefixed logs are streamed.
- daemon `--json` commands (`--daemon`, `--status`, `--stop`) emit one envelope and exit.
- startup payloads include local and best-effort Tailnet URLs; if Tailscale is unavailable, startup continues with a warning and local URLs.
- PWA API requests time out after 10 seconds and surface an error state instead of loading indefinitely.

### Safety gate for destructive operations
Expand Down
37 changes: 35 additions & 2 deletions apps/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { fail, ok } from '@tithe/contracts';
import { runMigrations } from '@tithe/db';
import { AppError, createDomainServices } from '@tithe/domain';
import { loadWorkspaceEnv } from './load-env.js';
import { runWebCommand } from './web.js';
import { runWebCommand, runWebSupervisorCommand } from './web.js';

loadWorkspaceEnv();

Expand Down Expand Up @@ -876,9 +876,12 @@ monzo.command('status').action(async () => {
program
.command('web')
.description('Run API + PWA web stack')
.option('--mode <mode>', 'dev|preview', 'dev')
.option('--mode <mode>', 'dev|preview')
.option('--api-port <port>', 'override API port (1-65535)')
.option('--pwa-port <port>', 'override PWA port (1-65535)')
.option('--daemon', 'run in background daemon mode with auto-restart', false)
.option('--status', 'show background web daemon status', false)
.option('--stop', 'stop background web daemon', false)
.action(async (options) => {
const opts = program.opts<{ json: boolean }>();

Expand All @@ -888,6 +891,9 @@ program
mode: options.mode,
apiPort: options.apiPort,
pwaPort: options.pwaPort,
daemon: options.daemon,
status: options.status,
stop: options.stop,
},
opts.json,
);
Expand All @@ -903,6 +909,33 @@ program
}
});

program
.command('web-supervisor')
.description('Internal daemon supervisor for tithe web --daemon')
.option('--mode <mode>', 'dev|preview')
.option('--api-port <port>', 'override API port (1-65535)')
.option('--pwa-port <port>', 'override PWA port (1-65535)')
.option('--run-id <runId>', 'daemon run identifier')
.action(async (options) => {
try {
await runWebSupervisorCommand({
mode: options.mode,
apiPort: options.apiPort,
pwaPort: options.pwaPort,
runId: options.runId,
});
} catch (error) {
if (error instanceof AppError) {
emit(fail(error.code, error.message, error.details), true);
process.exitCode = 1;
return;
}

emit(fail('INTERNAL_ERROR', error instanceof Error ? error.message : String(error)), true);
process.exitCode = 1;
}
});

if (process.argv.length <= 2) {
program.outputHelp();
process.exit(0);
Expand Down
Loading