# prod / dev release workflow

Status: design. Two viable approaches; pick one. Both deliver the same shape:

- `rebuild.cmd` publishes builds without affecting what casual users get.
- A separate `prod.cmd` step promotes a known-good build to be what casual users install.
- Casual users keep typing `npm install -g @bobfrankston/rmfmail` (no flags).
- Dev users explicitly opt in.

## Key invariant (answers the obvious confusion)

`@latest` is **what casual `npm install` gets**. It only moves when prod.cmd promotes. So between promotions:

```
@latest = @prod = 1.0.609   ← last promoted, what users get
@dev    = 1.0.620           ← every rebuild bumps this
```

Casual users keep installing 1.0.609 until you say so. Dev tip never accidentally becomes the casual install.

---

## Approach A — npm dist-tags (one package)

### Mechanics

npm's built-in [dist-tag](https://docs.npmjs.com/cli/v10/commands/npm-dist-tag) feature: one package, multiple movable pointers.

- `npm publish --tag dev` sets `@dev` to the new version. `@latest` untouched.
- `npm dist-tag add @bobfrankston/rmfmail@<v> prod` moves the `prod` pointer; `npm dist-tag add ... latest` moves `latest` too. Two HTTP calls, no republish.

### Required changes

**1. npmglobalize** — accepts a `--dist-tag` flag and forwards to `npm publish`. ~5 lines. See `docs/npmglobalize-disttag.md` for the patch.

**2. rebuild.cmd** — invoke `npmglobalize --dist-tag dev`. One-line edit.

**3. prod.cmd** — new file. Explicit arg wins; otherwise query npm for the
current `@dev` and promote that. (`@dev` is the right default because you've
just published it via `rebuild.cmd` and want it to become user-facing.)

```cmd
@echo off
setlocal
set V=%1
if "%V%"=="" (
  for /f "delims=" %%v in ('npm view @bobfrankston/rmfmail dist-tags.dev') do set V=%%v
)
if "%V%"=="" (
  echo No version supplied and 'npm view ... dist-tags.dev' returned empty.
  echo Usage: prod.cmd [version]   e.g. prod.cmd 1.0.611
  exit /b 1
)
echo Promoting @bobfrankston/rmfmail@%V% to prod and latest...
call npm dist-tag add @bobfrankston/rmfmail@%V% prod
call npm dist-tag add @bobfrankston/rmfmail@%V% latest
echo Done.
endlocal
```

Roll back to an older known-good: `prod.cmd 1.0.598` — explicit arg path.

The "default = local package.json" alternative (if you never want to query
the registry):

```cmd
for /f "tokens=2 delims=:," %%v in ('findstr /b "  \"version\":" "%~dp0app\package.json"') do set V=%%~v
set V=%V: =%
set V=%V:"=%
```

Use whichever default matches your workflow — the explicit-arg path works
either way.

### Workflow

1. Edit code, run `rebuild.cmd` — version bumps, publishes as `@dev`. `@latest`/`@prod` unaffected.
2. On the dev machine: `npm install -g @bobfrankston/rmfmail@dev` to grab the freshest build.
3. When something feels stable: run `prod.cmd`. Promotes the current `package.json` version to `@prod` and `@latest`.
4. On the prod machine (and any default install): `npm install -g @bobfrankston/rmfmail` (no tag) gets the newly-promoted version.

Roll back: `npm dist-tag add @bobfrankston/rmfmail@<earlier> prod` + `... latest`. No republish, no version bump.

### Effort

- npmglobalize change: ~5 lines, ~5 minutes.
- `rebuild.cmd`: one-line edit.
- `prod.cmd`: 15 lines, fresh file.
- Test cycle: ~10 minutes.

---

## Approach B — wrapper packages

### Mechanics

Split the current single package into three:

| Name | Role | Versioning |
|---|---|---|
| `@bobfrankston/rmfmailapp` | Today's actual code. | Bumps freely on every build. |
| `@bobfrankston/rmfmail` | Wrapper. `dependencies: { rmfmailapp: "1.0.609" }` pinned. | Bumps only on promotion. |
| `@bobfrankston/rmfmaildev` | Wrapper. `dependencies: { rmfmailapp: "*" }`. | Tracks rmfmailapp tip. |

Casual user: `npm install -g @bobfrankston/rmfmail` — installs the wrapper, which pulls in the pinned rmfmailapp via npm's normal dependency resolution. Wrapper's `bin` shim invokes rmfmailapp's actual entry point, so the CLI is identical.

Promotion = git commit on the wrapper bumping `dependencies.rmfmailapp` from one frozen version to another, plus `npm publish` of the wrapper.

### Layout

```
mailx/
  rmfmailapp/                # was app/
    package.json   "name": "@bobfrankston/rmfmailapp"
    bin/, packages/, client/  unchanged
  rmfmail/                   # NEW
    package.json   pins rmfmailapp version
    bin/rmfmail.js shim that imports rmfmailapp's entry
  rmfmaildev/                # NEW
    package.json   "*" range or always-repinned
    bin/rmfmaildev.js shim
```

### `rmfmail/package.json`

```json
{
  "name": "@bobfrankston/rmfmail",
  "version": "1.0.0",
  "type": "module",
  "bin": { "rmfmail": "bin/rmfmail.js" },
  "dependencies": { "@bobfrankston/rmfmailapp": "1.0.609" }
}
```

### `rmfmail/bin/rmfmail.js`

```js
#!/usr/bin/env node
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
await import(`file://${require.resolve("@bobfrankston/rmfmailapp/bin/mailx.js")}`);
```

### `rmfmaildev/package.json`

Same shape but `"@bobfrankston/rmfmailapp": "*"` so npm always resolves to the freshest published rmfmailapp.

### Required changes

**1. Rename** `@bobfrankston/rmfmail` → `@bobfrankston/rmfmailapp` in `app/package.json` and any workspace deps that reference it. One-time.

**2. Create** `rmfmail/` and `rmfmaildev/` directories with the package.json + shim listed above. ~30 lines total.

**3. prod.cmd** — bumps the pin and publishes the wrapper:

```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:"=%
)
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');"
cd rmfmail
call npm version patch
call npm publish --access public
cd ..
git add rmfmail/package.json
git commit -m "rmfmail: pin rmfmailapp@%TARGET%"
endlocal
```

**4. npmglobalize**: no changes required.

### Workflow

1. Build, run `rebuild.cmd` — publishes rmfmailapp.
2. Dev install: `npm install -g @bobfrankston/rmfmaildev`. Pulls latest rmfmailapp through the `*` range.
3. Promotion: `prod.cmd 1.0.611`. Edits rmfmail's pin, bumps wrapper patch version, publishes wrapper, commits.
4. Casual install: `npm install -g @bobfrankston/rmfmail` resolves the new wrapper, npm fetches the pinned rmfmailapp, both land under the wrapper.

Roll back: edit rmfmail/package.json to a prior pin, `git revert`, republish wrapper.

### Effort

- One-time rename: ~30 minutes.
- Wrapper skeletons: ~15 minutes.
- prod.cmd: ~15 minutes.
- Test: ~30 minutes.
- Total: ~1.5 hours up-front, then near-zero per promotion.

---

## Comparison

| | A: dist-tags | B: wrappers |
|--|--|--|
| Number of npm packages | 1 | 3 (app + 2 wrappers) |
| What "promotion" is | `npm dist-tag add` (HTTP call) | git commit + `npm publish` of wrapper |
| Promotion in git history | no (registry-only) | yes |
| Roll-back | retag prior version | `git revert` then republish wrapper |
| npmglobalize change needed | yes (`--tag` flag, ~5 lines) | no |
| Casual install command | `npm install -g @bobfrankston/rmfmail` | (same) |
| Dev install command | `... rmfmail@dev` | `... rmfmaildev` |
| One-time setup work | ~15 minutes | ~1.5 hours |
| Per-promotion work | seconds (registry call) | minutes (commit + publish) |
| Multi-channel future (beta, rc) | more dist-tags | more wrappers |

## Recommendation

If you want it working today with minimum disturbance: **A (dist-tags)**.

If auditability of "what version was prod when" matters and you're willing to spend the one-time rename: **B (wrappers)**.

A → B migration later is straightforward (the rename and wrapper creation is the same work whenever you do it; dist-tags become a non-issue once wrappers exist).

## What neither approach does

- Does not enable side-by-side coexistence on a single machine — that's the orthogonal `RMFMAIL_PROFILE` env-var design (separate config dir, AUMID, mailto handler, etc.).
- Does not change the in-app updater. mailx's existing version-check flow keeps working with either.
- Does not affect Android. APKs aren't on npm — `prod-android.md` is the parallel document with the equivalent channel-manifest design.
