# prod / dev release workflow — wrapper-package variant

Status: design only. Alternative to `prod.md` (dist-tag approach). Pick one.

## Shape

Today: one package `@bobfrankston/rmfmail` ships everything.

Proposed: split into two packages, three install names.

| Name | Role | Versioning |
|---|---|---|
| `@bobfrankston/rmfmailapp` | The actual code (today's rmfmail content). | Bumps freely on every build. |
| `@bobfrankston/rmfmail` | Thin wrapper. `dependencies: { rmfmailapp: "1.0.NNN" }` pinned. | Bumps only on promotion. |
| `@bobfrankston/rmfmaildev` | Thin wrapper. `dependencies: { rmfmailapp: "*" }` (or repinned at every publish to current `@latest`). | Tracks rmfmailapp tip. |

Casual user: `npm install -g @bobfrankston/rmfmail` — pulls the wrapper, npm resolves the pinned rmfmailapp, installs both. The wrapper's `bin` shim invokes rmfmailapp's actual entry point.

Dev user: `npm install -g @bobfrankston/rmfmaildev` — wrapper that pulls `rmfmailapp@latest` (or its current `*`-resolved match).

Promotion = single git commit on the wrapper repo bumping its `dependencies.rmfmailapp` from one frozen version to a newer frozen version, plus `npm publish` of the wrapper.

## Why this rather than dist-tags

- **Audit trail in git.** The history of "what version was current prod when" is `git log app/wrapper/package.json`, not registry-only state.
- **Promotion is reversible by `git revert`.** Same as any code change.
- **Multi-channel scales.** `rmfmail-beta`, `rmfmail-rc`, etc. are just more wrappers, each pinned independently. dist-tags work too but become a bigger pile.
- **Pre-flight gating possible.** A CI check on the wrapper's package.json (lint, smoke test) can refuse promotions that name a version that hasn't been validated.

## Why NOT, vs dist-tags

- **Two packages to maintain** instead of one. Two `package.json`s, two `npm publish` calls per promotion.
- **One more rename.** Current `@bobfrankston/rmfmail` → `@bobfrankston/rmfmailapp` (every workspace dep that references it has to flip too — not many in mailx, but it's a non-trivial rename).
- **npm cache pressure.** Each version of rmfmailapp lingers in users' npm caches; the wrapper resolves at install time, so old transitive resolutions can sit around. dist-tags don't multiply package versions.

## Layout

```
mailx/
  rmfmailapp/                      # was app/, the actual code
    package.json   → "name": "@bobfrankston/rmfmailapp"
    bin/mailx.ts   → unchanged
    packages/      → unchanged
    client/        → unchanged
    ...

  rmfmail/                         # NEW — wrapper for prod
    package.json   → see below
    bin/rmfmail.js → 3-line shim

  rmfmaildev/                      # NEW — wrapper for dev
    package.json   → see below
    bin/rmfmaildev.js → 3-line shim
```

### `rmfmail/package.json` (prod wrapper)

```json
{
  "name": "@bobfrankston/rmfmail",
  "version": "1.0.0",
  "description": "Local-first email client (current stable)",
  "type": "module",
  "bin": { "rmfmail": "bin/rmfmail.js" },
  "dependencies": {
    "@bobfrankston/rmfmailapp": "1.0.609"
  },
  "license": "MIT"
}
```

Wrapper's own version (`1.0.0`) only bumps on shape changes (e.g., bin renamed, scope changed). Day-to-day promotions just re-pin the dependency line.

### `rmfmail/bin/rmfmail.js` (shim)

```js
#!/usr/bin/env node
// Wrapper bin — defer to the pinned rmfmailapp entry point. Keeps argv
// and exit code identical so the wrapper is invisible at the CLI.
import { fileURLToPath } from "node:url";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const appBin = require.resolve("@bobfrankston/rmfmailapp/bin/mailx.js");
await import(`file://${appBin}`);
```

(Or even just `require("@bobfrankston/rmfmailapp/bin/mailx.js")` if the rmfmailapp bin runs on `import`.)

### `rmfmaildev/package.json` (dev wrapper)

```json
{
  "name": "@bobfrankston/rmfmaildev",
  "version": "1.0.0",
  "description": "Local-first email client (development tip)",
  "type": "module",
  "bin": { "rmfmail": "bin/rmfmaildev.js" },
  "dependencies": {
    "@bobfrankston/rmfmailapp": "*"
  },
  "license": "MIT"
}
```

`*` means "any version" — npm picks the latest published. New `npm install -g @bobfrankston/rmfmaildev` always installs whatever rmfmailapp tip is at install time. Existing dev users run `npm update -g @bobfrankston/rmfmaildev` to refresh.

Alternative: `rmfmaildev` re-pins on every rmfmailapp publish (`prepublish` hook in rmfmailapp's CI bumps rmfmaildev's pin and publishes). Adds steps but gives dev installs the same lockstep behavior as prod.

### Workflow

**rebuild.cmd** (publishes rmfmailapp):
```cmd
cd rmfmailapp
npmglobalize
```

No tag flag needed (rmfmailapp's `@latest` is just "newest dev build"; rmfmail prod doesn't read from it directly).

**prod.cmd** (promote):
```cmd
@echo off
setlocal
set TARGET=%1
if "%TARGET%"=="" (
  for /f "tokens=2 delims=:," %%v in ('findstr /b "  \"version\":" "%~dp0rmfmailapp\package.json"') do set TARGET=%%~v
  set TARGET=%TARGET: =%
  set TARGET=%TARGET:"=%
)
echo Promoting rmfmail wrapper to pin rmfmailapp@%TARGET%...

rem Edit rmfmail/package.json's dependency line. 5-line node one-liner:
node -e "const p=require('./rmfmail/package.json'); p.dependencies['@bobfrankston/rmfmailapp']='%TARGET%'; require('fs').writeFileSync('./rmfmail/package.json', JSON.stringify(p,null,2)+'\n');"

rem Bump wrapper patch version (so npm sees a new wrapper to install).
cd rmfmail && npm version patch && cd ..
git add rmfmail/package.json
git commit -m "rmfmail: pin rmfmailapp@%TARGET%"

cd rmfmail
npm publish --access public
cd ..
echo Done. Casual users running 'npm install -g @bobfrankston/rmfmail' now get rmfmail@x.y.z → rmfmailapp@%TARGET%.
endlocal
```

**Casual install** (unchanged from today's user-facing command):
```
npm install -g @bobfrankston/rmfmail
```

**Dev install**:
```
npm install -g @bobfrankston/rmfmaildev
```

## Required code changes

### 1. Rename `@bobfrankston/rmfmail` → `@bobfrankston/rmfmailapp`

- `app/package.json` `"name"` field.
- Every `dependencies` reference in workspace `packages/*/package.json`. Today they use `file:` paths so this might already be transparent — check.
- `bin/postinstall.js` — any hard-coded package name strings.
- `npmglobalize` config — no changes if it reads from package.json.

The rename is a one-time event. After it lands, no package.json edits in normal day-to-day work.

### 2. Create `rmfmail/` and `rmfmaildev/` directories

Top-level (not under `app/`). Each gets a 5-line `package.json` and a 3-line bin shim. ~30 lines total.

### 3. CI / build wiring

Today's `rebuild.cmd` builds and publishes app. Add:
- `prod.cmd` — promotes by bumping rmfmail's pin.
- Optionally automate rmfmaildev re-pin on every app publish (postpublish hook).

### 4. npmglobalize: no changes needed

The wrapper packages publish like any other. dist-tag flag work in `prod.md` is **not required** for this approach.

## What this does NOT do

- Does not enable side-by-side coexistence on a single machine — same as dist-tag plan, that's the orthogonal `RMFMAIL_PROFILE` env-var design.
- Does not eliminate user upgrades — users still need to re-run `npm install -g` (or rely on mailx's in-app updater) to pick up a new prod pin.
- Does not split testing or CI per channel — same code, same tests; only the promotion step gates the user-facing pin.

## Comparison to `prod.md`

| | `prod.md` (dist-tag) | `prod-wrapper.md` (this) |
|--|--|--|
| Number of npm packages | 1 (rmfmail) | 3 (rmfmailapp + rmfmail + rmfmaildev) |
| What "promotion" is | `npm dist-tag add` (registry call) | git commit + `npm publish` of wrapper |
| Promotion auditable in git | no | yes |
| Roll-back mechanism | retag earlier version | `git revert` then republish wrapper |
| Required npmglobalize change | yes (`--tag` flag) | no |
| Casual install command | `npm install -g @bobfrankston/rmfmail` | (same) |
| Dev install command | `... rmfmail@dev` | `... rmfmaildev` |
| One-time rename of code package | no | yes (rmfmail → rmfmailapp) |
| Multi-channel future (beta, rc, …) | more dist-tags | more wrappers |

## Recommendation

If you want:
- **Minimum mechanical change**: dist-tag (`prod.md`).
- **Maximum traceability**: wrapper (`prod-wrapper.md`).
- **Both eventually**: nothing stops doing dist-tag now and migrating to wrapper later. The dist-tag is a cheap throwaway; the wrapper is a structural commitment.

## Effort estimate

- One-time rename `rmfmail` → `rmfmailapp`: ~30 minutes (search-replace + verify, plus npm deprecate of old name to point users at new install path).
- `rmfmail/` + `rmfmaildev/` skeletons: ~15 minutes.
- `prod.cmd`: ~15 minutes.
- Test cycle: build app, publish, run prod.cmd, install in clean dir, verify it runs. ~30 minutes.

About 1.5 hours wall-clock. Bigger one-time investment than dist-tag (~15 minutes); smaller per-promotion friction (one git commit vs `npm dist-tag add ... prod && add ... latest`).
