@bobfrankston/rmfmail 1.0.632 → 1.0.640

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,215 @@
1
+ # prod / dev release workflow — wrapper-package variant
2
+
3
+ Status: design only. Alternative to `prod.md` (dist-tag approach). Pick one.
4
+
5
+ ## Shape
6
+
7
+ Today: one package `@bobfrankston/rmfmail` ships everything.
8
+
9
+ Proposed: split into two packages, three install names.
10
+
11
+ | Name | Role | Versioning |
12
+ |---|---|---|
13
+ | `@bobfrankston/rmfmailapp` | The actual code (today's rmfmail content). | Bumps freely on every build. |
14
+ | `@bobfrankston/rmfmail` | Thin wrapper. `dependencies: { rmfmailapp: "1.0.NNN" }` pinned. | Bumps only on promotion. |
15
+ | `@bobfrankston/rmfmaildev` | Thin wrapper. `dependencies: { rmfmailapp: "*" }` (or repinned at every publish to current `@latest`). | Tracks rmfmailapp tip. |
16
+
17
+ 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.
18
+
19
+ Dev user: `npm install -g @bobfrankston/rmfmaildev` — wrapper that pulls `rmfmailapp@latest` (or its current `*`-resolved match).
20
+
21
+ 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.
22
+
23
+ ## Why this rather than dist-tags
24
+
25
+ - **Audit trail in git.** The history of "what version was current prod when" is `git log app/wrapper/package.json`, not registry-only state.
26
+ - **Promotion is reversible by `git revert`.** Same as any code change.
27
+ - **Multi-channel scales.** `rmfmail-beta`, `rmfmail-rc`, etc. are just more wrappers, each pinned independently. dist-tags work too but become a bigger pile.
28
+ - **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.
29
+
30
+ ## Why NOT, vs dist-tags
31
+
32
+ - **Two packages to maintain** instead of one. Two `package.json`s, two `npm publish` calls per promotion.
33
+ - **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).
34
+ - **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.
35
+
36
+ ## Layout
37
+
38
+ ```
39
+ mailx/
40
+ rmfmailapp/ # was app/, the actual code
41
+ package.json → "name": "@bobfrankston/rmfmailapp"
42
+ bin/mailx.ts → unchanged
43
+ packages/ → unchanged
44
+ client/ → unchanged
45
+ ...
46
+
47
+ rmfmail/ # NEW — wrapper for prod
48
+ package.json → see below
49
+ bin/rmfmail.js → 3-line shim
50
+
51
+ rmfmaildev/ # NEW — wrapper for dev
52
+ package.json → see below
53
+ bin/rmfmaildev.js → 3-line shim
54
+ ```
55
+
56
+ ### `rmfmail/package.json` (prod wrapper)
57
+
58
+ ```json
59
+ {
60
+ "name": "@bobfrankston/rmfmail",
61
+ "version": "1.0.0",
62
+ "description": "Local-first email client (current stable)",
63
+ "type": "module",
64
+ "bin": { "rmfmail": "bin/rmfmail.js" },
65
+ "dependencies": {
66
+ "@bobfrankston/rmfmailapp": "1.0.609"
67
+ },
68
+ "license": "MIT"
69
+ }
70
+ ```
71
+
72
+ 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.
73
+
74
+ ### `rmfmail/bin/rmfmail.js` (shim)
75
+
76
+ ```js
77
+ #!/usr/bin/env node
78
+ // Wrapper bin — defer to the pinned rmfmailapp entry point. Keeps argv
79
+ // and exit code identical so the wrapper is invisible at the CLI.
80
+ import { fileURLToPath } from "node:url";
81
+ import { createRequire } from "node:module";
82
+ const require = createRequire(import.meta.url);
83
+ const appBin = require.resolve("@bobfrankston/rmfmailapp/bin/mailx.js");
84
+ await import(`file://${appBin}`);
85
+ ```
86
+
87
+ (Or even just `require("@bobfrankston/rmfmailapp/bin/mailx.js")` if the rmfmailapp bin runs on `import`.)
88
+
89
+ ### `rmfmaildev/package.json` (dev wrapper)
90
+
91
+ ```json
92
+ {
93
+ "name": "@bobfrankston/rmfmaildev",
94
+ "version": "1.0.0",
95
+ "description": "Local-first email client (development tip)",
96
+ "type": "module",
97
+ "bin": { "rmfmail": "bin/rmfmaildev.js" },
98
+ "dependencies": {
99
+ "@bobfrankston/rmfmailapp": "*"
100
+ },
101
+ "license": "MIT"
102
+ }
103
+ ```
104
+
105
+ `*` 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.
106
+
107
+ 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.
108
+
109
+ ### Workflow
110
+
111
+ **rebuild.cmd** (publishes rmfmailapp):
112
+ ```cmd
113
+ cd rmfmailapp
114
+ npmglobalize
115
+ ```
116
+
117
+ No tag flag needed (rmfmailapp's `@latest` is just "newest dev build"; rmfmail prod doesn't read from it directly).
118
+
119
+ **prod.cmd** (promote):
120
+ ```cmd
121
+ @echo off
122
+ setlocal
123
+ set TARGET=%1
124
+ if "%TARGET%"=="" (
125
+ for /f "tokens=2 delims=:," %%v in ('findstr /b " \"version\":" "%~dp0rmfmailapp\package.json"') do set TARGET=%%~v
126
+ set TARGET=%TARGET: =%
127
+ set TARGET=%TARGET:"=%
128
+ )
129
+ echo Promoting rmfmail wrapper to pin rmfmailapp@%TARGET%...
130
+
131
+ rem Edit rmfmail/package.json's dependency line. 5-line node one-liner:
132
+ 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');"
133
+
134
+ rem Bump wrapper patch version (so npm sees a new wrapper to install).
135
+ cd rmfmail && npm version patch && cd ..
136
+ git add rmfmail/package.json
137
+ git commit -m "rmfmail: pin rmfmailapp@%TARGET%"
138
+
139
+ cd rmfmail
140
+ npm publish --access public
141
+ cd ..
142
+ echo Done. Casual users running 'npm install -g @bobfrankston/rmfmail' now get rmfmail@x.y.z → rmfmailapp@%TARGET%.
143
+ endlocal
144
+ ```
145
+
146
+ **Casual install** (unchanged from today's user-facing command):
147
+ ```
148
+ npm install -g @bobfrankston/rmfmail
149
+ ```
150
+
151
+ **Dev install**:
152
+ ```
153
+ npm install -g @bobfrankston/rmfmaildev
154
+ ```
155
+
156
+ ## Required code changes
157
+
158
+ ### 1. Rename `@bobfrankston/rmfmail` → `@bobfrankston/rmfmailapp`
159
+
160
+ - `app/package.json` `"name"` field.
161
+ - Every `dependencies` reference in workspace `packages/*/package.json`. Today they use `file:` paths so this might already be transparent — check.
162
+ - `bin/postinstall.js` — any hard-coded package name strings.
163
+ - `npmglobalize` config — no changes if it reads from package.json.
164
+
165
+ The rename is a one-time event. After it lands, no package.json edits in normal day-to-day work.
166
+
167
+ ### 2. Create `rmfmail/` and `rmfmaildev/` directories
168
+
169
+ Top-level (not under `app/`). Each gets a 5-line `package.json` and a 3-line bin shim. ~30 lines total.
170
+
171
+ ### 3. CI / build wiring
172
+
173
+ Today's `rebuild.cmd` builds and publishes app. Add:
174
+ - `prod.cmd` — promotes by bumping rmfmail's pin.
175
+ - Optionally automate rmfmaildev re-pin on every app publish (postpublish hook).
176
+
177
+ ### 4. npmglobalize: no changes needed
178
+
179
+ The wrapper packages publish like any other. dist-tag flag work in `prod.md` is **not required** for this approach.
180
+
181
+ ## What this does NOT do
182
+
183
+ - 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.
184
+ - 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.
185
+ - Does not split testing or CI per channel — same code, same tests; only the promotion step gates the user-facing pin.
186
+
187
+ ## Comparison to `prod.md`
188
+
189
+ | | `prod.md` (dist-tag) | `prod-wrapper.md` (this) |
190
+ |--|--|--|
191
+ | Number of npm packages | 1 (rmfmail) | 3 (rmfmailapp + rmfmail + rmfmaildev) |
192
+ | What "promotion" is | `npm dist-tag add` (registry call) | git commit + `npm publish` of wrapper |
193
+ | Promotion auditable in git | no | yes |
194
+ | Roll-back mechanism | retag earlier version | `git revert` then republish wrapper |
195
+ | Required npmglobalize change | yes (`--tag` flag) | no |
196
+ | Casual install command | `npm install -g @bobfrankston/rmfmail` | (same) |
197
+ | Dev install command | `... rmfmail@dev` | `... rmfmaildev` |
198
+ | One-time rename of code package | no | yes (rmfmail → rmfmailapp) |
199
+ | Multi-channel future (beta, rc, …) | more dist-tags | more wrappers |
200
+
201
+ ## Recommendation
202
+
203
+ If you want:
204
+ - **Minimum mechanical change**: dist-tag (`prod.md`).
205
+ - **Maximum traceability**: wrapper (`prod-wrapper.md`).
206
+ - **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.
207
+
208
+ ## Effort estimate
209
+
210
+ - One-time rename `rmfmail` → `rmfmailapp`: ~30 minutes (search-replace + verify, plus npm deprecate of old name to point users at new install path).
211
+ - `rmfmail/` + `rmfmaildev/` skeletons: ~15 minutes.
212
+ - `prod.cmd`: ~15 minutes.
213
+ - Test cycle: build app, publish, run prod.cmd, install in clean dir, verify it runs. ~30 minutes.
214
+
215
+ 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`).
package/docs/prod.md CHANGED
@@ -1,105 +1,224 @@
1
1
  # prod / dev release workflow
2
2
 
3
- Status: design only. Not implemented.
3
+ Status: design. Two viable approaches; pick one. Both deliver the same shape:
4
4
 
5
- ## Goal
5
+ - `rebuild.cmd` publishes builds without affecting what casual users get.
6
+ - A separate `prod.cmd` step promotes a known-good build to be what casual users install.
7
+ - Casual users keep typing `npm install -g @bobfrankston/rmfmail` (no flags).
8
+ - Dev users explicitly opt in.
6
9
 
7
- Two flavors of `@bobfrankston/rmfmail` published to the same npm package, distinguished only by [dist-tag](https://docs.npmjs.com/cli/v10/commands/npm-dist-tag):
10
+ ## Key invariant (answers the obvious confusion)
8
11
 
9
- - **`@dev`** every successful `rebuild.cmd` publishes here. Bumps freely; no stability promise.
10
- - **`@prod`** (and **`@latest`**) — what casual `npm install -g @bobfrankston/rmfmail` gets. Promoted manually from the current `@dev` tip when something has proved stable.
12
+ `@latest` is **what casual `npm install` gets**. It only moves when prod.cmd promotes. So between promotions:
11
13
 
12
- One node_modules root per machine; the active version is whichever the install pulled. No coexistence machinery, no env-var profiles. The simple case stays simple.
14
+ ```
15
+ @latest = @prod = 1.0.609 ← last promoted, what users get
16
+ @dev = 1.0.620 ← every rebuild bumps this
17
+ ```
18
+
19
+ Casual users keep installing 1.0.609 until you say so. Dev tip never accidentally becomes the casual install.
20
+
21
+ ---
22
+
23
+ ## Approach A — npm dist-tags (one package)
24
+
25
+ ### Mechanics
26
+
27
+ npm's built-in [dist-tag](https://docs.npmjs.com/cli/v10/commands/npm-dist-tag) feature: one package, multiple movable pointers.
13
28
 
14
- ## Mechanics
29
+ - `npm publish --tag dev` sets `@dev` to the new version. `@latest` untouched.
30
+ - `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.
15
31
 
16
- npm publishes default to `--tag latest`. Passing `--tag <name>` sets that tag on the new version and **leaves `latest` untouched**. dist-tags are pointers — moving one is two HTTP calls, no republish.
32
+ ### Required changes
17
33
 
34
+ **1. npmglobalize** — accepts a `--dist-tag` flag and forwards to `npm publish`. ~5 lines. See `docs/npmglobalize-disttag.md` for the patch.
35
+
36
+ **2. rebuild.cmd** — invoke `npmglobalize --dist-tag dev`. One-line edit.
37
+
38
+ **3. prod.cmd** — new file. Explicit arg wins; otherwise query npm for the
39
+ current `@dev` and promote that. (`@dev` is the right default because you've
40
+ just published it via `rebuild.cmd` and want it to become user-facing.)
41
+
42
+ ```cmd
43
+ @echo off
44
+ setlocal
45
+ set V=%1
46
+ if "%V%"=="" (
47
+ for /f "delims=" %%v in ('npm view @bobfrankston/rmfmail dist-tags.dev') do set V=%%v
48
+ )
49
+ if "%V%"=="" (
50
+ echo No version supplied and 'npm view ... dist-tags.dev' returned empty.
51
+ echo Usage: prod.cmd [version] e.g. prod.cmd 1.0.611
52
+ exit /b 1
53
+ )
54
+ echo Promoting @bobfrankston/rmfmail@%V% to prod and latest...
55
+ call npm dist-tag add @bobfrankston/rmfmail@%V% prod
56
+ call npm dist-tag add @bobfrankston/rmfmail@%V% latest
57
+ echo Done.
58
+ endlocal
18
59
  ```
19
- # what rebuild.cmd should run (via npmglobalize):
20
- npm publish <tarball> --tag dev # version exists, dev → it, latest unchanged
21
60
 
22
- # what prod.cmd should run:
23
- npm dist-tag add @bobfrankston/rmfmail@<version> prod
24
- npm dist-tag add @bobfrankston/rmfmail@<version> latest
61
+ Roll back to an older known-good: `prod.cmd 1.0.598` — explicit arg path.
62
+
63
+ The "default = local package.json" alternative (if you never want to query
64
+ the registry):
65
+
66
+ ```cmd
67
+ for /f "tokens=2 delims=:," %%v in ('findstr /b " \"version\":" "%~dp0app\package.json"') do set V=%%~v
68
+ set V=%V: =%
69
+ set V=%V:"=%
25
70
  ```
26
71
 
27
- Anyone running `npm install -g @bobfrankston/rmfmail` keeps installing whatever `latest` currently points at i.e., the most recent prod-promoted build. Dev users explicitly run `npm install -g @bobfrankston/rmfmail@dev` (one time; subsequent installs stay on dev tip until the user reinstalls).
72
+ Use whichever default matches your workflow — the explicit-arg path works
73
+ either way.
28
74
 
29
- ## Required changes
75
+ ### Workflow
30
76
 
31
- ### 1. npmglobalize (small flag plumbing)
77
+ 1. Edit code, run `rebuild.cmd` — version bumps, publishes as `@dev`. `@latest`/`@prod` unaffected.
78
+ 2. On the dev machine: `npm install -g @bobfrankston/rmfmail@dev` to grab the freshest build.
79
+ 3. When something feels stable: run `prod.cmd`. Promotes the current `package.json` version to `@prod` and `@latest`.
80
+ 4. On the prod machine (and any default install): `npm install -g @bobfrankston/rmfmail` (no tag) gets the newly-promoted version.
32
81
 
33
- `/c/users/bob/onedrive/xfer/bin/linuxbin/node_modules/@bobfrankston/npmglobalize/lib.js:4486`:
82
+ Roll back: `npm dist-tag add @bobfrankston/rmfmail@<earlier> prod` + `... latest`. No republish, no version bump.
83
+
84
+ ### Effort
85
+
86
+ - npmglobalize change: ~5 lines, ~5 minutes.
87
+ - `rebuild.cmd`: one-line edit.
88
+ - `prod.cmd`: 15 lines, fresh file.
89
+ - Test cycle: ~10 minutes.
90
+
91
+ ---
92
+
93
+ ## Approach B — wrapper packages
94
+
95
+ ### Mechanics
96
+
97
+ Split the current single package into three:
98
+
99
+ | Name | Role | Versioning |
100
+ |---|---|---|
101
+ | `@bobfrankston/rmfmailapp` | Today's actual code. | Bumps freely on every build. |
102
+ | `@bobfrankston/rmfmail` | Wrapper. `dependencies: { rmfmailapp: "1.0.609" }` pinned. | Bumps only on promotion. |
103
+ | `@bobfrankston/rmfmaildev` | Wrapper. `dependencies: { rmfmailapp: "*" }`. | Tracks rmfmailapp tip. |
104
+
105
+ 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.
106
+
107
+ Promotion = git commit on the wrapper bumping `dependencies.rmfmailapp` from one frozen version to another, plus `npm publish` of the wrapper.
108
+
109
+ ### Layout
34
110
 
35
- ```js
36
- const npmArgs = ['publish', tarballName];
111
+ ```
112
+ mailx/
113
+ rmfmailapp/ # was app/
114
+ package.json "name": "@bobfrankston/rmfmailapp"
115
+ bin/, packages/, client/ unchanged
116
+ rmfmail/ # NEW
117
+ package.json pins rmfmailapp version
118
+ bin/rmfmail.js shim that imports rmfmailapp's entry
119
+ rmfmaildev/ # NEW
120
+ package.json "*" range or always-repinned
121
+ bin/rmfmaildev.js shim
37
122
  ```
38
123
 
39
- Becomes:
124
+ ### `rmfmail/package.json`
125
+
126
+ ```json
127
+ {
128
+ "name": "@bobfrankston/rmfmail",
129
+ "version": "1.0.0",
130
+ "type": "module",
131
+ "bin": { "rmfmail": "bin/rmfmail.js" },
132
+ "dependencies": { "@bobfrankston/rmfmailapp": "1.0.609" }
133
+ }
134
+ ```
135
+
136
+ ### `rmfmail/bin/rmfmail.js`
40
137
 
41
138
  ```js
42
- const npmArgs = ['publish', tarballName];
43
- if (distTag) npmArgs.push('--tag', distTag);
139
+ #!/usr/bin/env node
140
+ import { createRequire } from "node:module";
141
+ const require = createRequire(import.meta.url);
142
+ await import(`file://${require.resolve("@bobfrankston/rmfmailapp/bin/mailx.js")}`);
44
143
  ```
45
144
 
46
- Plus argv parsing for `--dist-tag <name>` (or `--tag`) at the top of the script. ~5 lines total. Default behavior unchanged when the flag is absent — still publishes to `@latest` for every other consumer of npmglobalize.
145
+ ### `rmfmaildev/package.json`
47
146
 
48
- ### 2. rebuild.cmd
147
+ Same shape but `"@bobfrankston/rmfmailapp": "*"` so npm always resolves to the freshest published rmfmailapp.
49
148
 
50
- Change the npmglobalize invocation to pass `--dist-tag dev`:
149
+ ### Required changes
51
150
 
52
- ```cmd
53
- npmglobalize --dist-tag dev
54
- ```
151
+ **1. Rename** `@bobfrankston/rmfmail` → `@bobfrankston/rmfmailapp` in `app/package.json` and any workspace deps that reference it. One-time.
55
152
 
56
- After this, every build that reaches publish goes to `@dev`. `@latest` and `@prod` are frozen at whatever was last promoted.
153
+ **2. Create** `rmfmail/` and `rmfmaildev/` directories with the package.json + shim listed above. ~30 lines total.
57
154
 
58
- ### 3. prod.cmd (new file)
155
+ **3. prod.cmd** bumps the pin and publishes the wrapper:
59
156
 
60
157
  ```cmd
61
158
  @echo off
62
159
  setlocal
63
- for /f "tokens=2 delims=:," %%v in ('findstr /b " \"version\":" "%~dp0app\package.json"') do set V=%%~v
64
- set V=%V: =%
65
- set V=%V:"=%
66
- echo Promoting @bobfrankston/rmfmail@%V% to prod and latest...
67
- call npm dist-tag add @bobfrankston/rmfmail@%V% prod
68
- call npm dist-tag add @bobfrankston/rmfmail@%V% latest
69
- echo Done. End-users running 'npm install -g @bobfrankston/rmfmail' now get %V%.
160
+ set TARGET=%1
161
+ if "%TARGET%"=="" (
162
+ for /f "tokens=2 delims=:," %%v in ('findstr /b " \"version\":" "%~dp0rmfmailapp\package.json"') do set TARGET=%%~v
163
+ set TARGET=%TARGET: =%
164
+ set TARGET=%TARGET:"=%
165
+ )
166
+ 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');"
167
+ cd rmfmail
168
+ call npm version patch
169
+ call npm publish --access public
170
+ cd ..
171
+ git add rmfmail/package.json
172
+ git commit -m "rmfmail: pin rmfmailapp@%TARGET%"
70
173
  endlocal
71
174
  ```
72
175
 
73
- (Reads the current `package.json` version. Promotes that version to both `prod` and `latest`. No build, no publish.)
176
+ **4. npmglobalize**: no changes required.
74
177
 
75
- ### 4. README / one-liner doc
178
+ ### Workflow
76
179
 
77
- User-facing instructions for the two install paths:
180
+ 1. Build, run `rebuild.cmd` publishes rmfmailapp.
181
+ 2. Dev install: `npm install -g @bobfrankston/rmfmaildev`. Pulls latest rmfmailapp through the `*` range.
182
+ 3. Promotion: `prod.cmd 1.0.611`. Edits rmfmail's pin, bumps wrapper patch version, publishes wrapper, commits.
183
+ 4. Casual install: `npm install -g @bobfrankston/rmfmail` resolves the new wrapper, npm fetches the pinned rmfmailapp, both land under the wrapper.
78
184
 
79
- - Stable: `npm install -g @bobfrankston/rmfmail` (gets `@latest` = current prod).
80
- - Dev: `npm install -g @bobfrankston/rmfmail@dev` (current dev tip).
81
- - Pin to a specific version: `npm install -g @bobfrankston/rmfmail@1.0.NNN`.
185
+ Roll back: edit rmfmail/package.json to a prior pin, `git revert`, republish wrapper.
82
186
 
83
- ## Workflow once wired
187
+ ### Effort
84
188
 
85
- 1. Edit, build, run `rebuild.cmd` — version bumps, publishes as `@dev`. `@latest`/`@prod` unaffected.
86
- 2. Daily use on the dev machine: `npm install -g @bobfrankston/rmfmail@dev` to grab the freshest build, then run `rmfmail`.
87
- 3. When a recent dev build feels solid, run `prod.cmd`. That single command promotes the *current package.json version* to `prod` + `latest`.
88
- 4. On the prod machine (or any other), `npm install -g @bobfrankston/rmfmail` (no tag) gets the newly-promoted version.
189
+ - One-time rename: ~30 minutes.
190
+ - Wrapper skeletons: ~15 minutes.
191
+ - prod.cmd: ~15 minutes.
192
+ - Test: ~30 minutes.
193
+ - Total: ~1.5 hours up-front, then near-zero per promotion.
89
194
 
90
- Rolling back a bad prod is also a tag move — `npm dist-tag add @bobfrankston/rmfmail@<earlier> prod latest`. No republish; no version-bump needed; old install scripts that pin a version keep working.
195
+ ---
91
196
 
92
- ## What this does NOT do
197
+ ## Comparison
93
198
 
94
- - It does not enable both versions side-by-side on one machine. Single global install per machine; whichever tag was most recently installed wins. Coexistence (separate config dirs / WebView2 dirs / mailto handlers) is the *other* design (`RMFMAIL_PROFILE` env-var pattern, separately).
95
- - It does not freeze any specific dependency version. Pinning of workspace deps is whatever was bundled at publish time — same as today.
96
- - It does not change the semantic-version sequence. Both dev and prod publishes share the same monotonic `1.0.N` numbering; the dist-tag is just a movable pointer.
199
+ | | A: dist-tags | B: wrappers |
200
+ |--|--|--|
201
+ | Number of npm packages | 1 | 3 (app + 2 wrappers) |
202
+ | What "promotion" is | `npm dist-tag add` (HTTP call) | git commit + `npm publish` of wrapper |
203
+ | Promotion in git history | no (registry-only) | yes |
204
+ | Roll-back | retag prior version | `git revert` then republish wrapper |
205
+ | npmglobalize change needed | yes (`--tag` flag, ~5 lines) | no |
206
+ | Casual install command | `npm install -g @bobfrankston/rmfmail` | (same) |
207
+ | Dev install command | `... rmfmail@dev` | `... rmfmaildev` |
208
+ | One-time setup work | ~15 minutes | ~1.5 hours |
209
+ | Per-promotion work | seconds (registry call) | minutes (commit + publish) |
210
+ | Multi-channel future (beta, rc) | more dist-tags | more wrappers |
97
211
 
98
- ## Effort estimate
212
+ ## Recommendation
99
213
 
100
- - npmglobalize change: ~5 lines, ~5 minutes.
101
- - `rebuild.cmd`: one-line edit.
102
- - `prod.cmd`: 15 lines, fresh file.
103
- - Test cycle: publish a dev, verify `@latest` unchanged, run `prod.cmd`, verify `@latest` moved.
214
+ If you want it working today with minimum disturbance: **A (dist-tags)**.
215
+
216
+ If auditability of "what version was prod when" matters and you're willing to spend the one-time rename: **B (wrappers)**.
217
+
218
+ 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).
219
+
220
+ ## What neither approach does
104
221
 
105
- Order of operations: npmglobalize first, then `rebuild.cmd`, then `prod.cmd`. Ship npmglobalize before changing rebuild.cmd to avoid one cycle of "rebuild.cmd uses --dist-tag dev but npmglobalize ignores it and publishes to latest anyway."
222
+ - 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.).
223
+ - Does not change the in-app updater. mailx's existing version-check flow keeps working with either.
224
+ - Does not affect Android. APKs aren't on npm — `prod-android.md` is the parallel document with the equivalent channel-manifest design.
@@ -0,0 +1,156 @@
1
+ # `rmf-tiny` — optional TinyMCE editor adapter for rmfmail
2
+
3
+ Status: design only. Not implemented.
4
+
5
+ ## Goal
6
+
7
+ Let users who want Thunderbird-class paste fidelity opt into TinyMCE without baking it into rmfmail itself. Bob 2026-05-09 verified: pasted a Word document into Thunderbird, tiptap, Quill, and TinyMCE — only TinyMCE preserved the formatting (boxed monospace block with borders, paragraph spacing, font preservation). Nothing else came close.
8
+
9
+ The license catch — TinyMCE is GPLv2 — means rmfmail (MIT) can't bundle it. The arms-length pattern is the resolution: rmfmail ships **no** TinyMCE bytes; a separate `rmf-tiny` package the user installs themselves provides the adapter + pulls in TinyMCE via npm.
10
+
11
+ ## Shape
12
+
13
+ ```
14
+ @bobfrankston/rmfmail ← MIT, ships no TinyMCE
15
+ client/compose/editor.ts ← MailxEditor interface + factory
16
+ gains a "tinymce" branch that
17
+ dynamically imports rmf-tiny
18
+
19
+ @bobfrankston/rmf-tiny ← MIT (the adapter glue is original code)
20
+ package.json ← peerDependency: tinymce
21
+ src/adapter.ts ← implements MailxEditor against TinyMCE
22
+ README.md ← install instructions
23
+ ```
24
+
25
+ User opts in:
26
+
27
+ ```
28
+ npm install -g @bobfrankston/rmf-tiny tinymce
29
+ ```
30
+
31
+ (Two packages explicit — keeps the licensing posture clean. rmf-tiny is just the adapter; tinymce comes from Tiny's own npm publication, not bundled with anything rmfmail-related.)
32
+
33
+ In rmfmail Settings → Compose → Editor: the dropdown grows a "TinyMCE (requires rmf-tiny)" option. If selected and the import fails, surface a status-bar error pointing at the install command.
34
+
35
+ ## Why a separate package, not a flag inside rmfmail
36
+
37
+ - rmfmail's git history and npm tarball never contain TinyMCE → distribution layer is clean.
38
+ - rmf-tiny is small (~200 lines of adapter glue + a thin install README) and can be MIT — no GPL infection because it imports TinyMCE at runtime via the same kind of dynamic boundary npm packages always use. Combination at user's machine is fully GPL-permitted.
39
+ - Other editors can follow the same pattern (`rmf-ckeditor`, `rmf-prosemirror-pro`) without polluting rmfmail.
40
+
41
+ ## Why call it `rmf-tiny`
42
+
43
+ Short, distinguishable from the upstream package, namespaces under your scope (`@bobfrankston/rmf-tiny`), and "tiny" is the upstream's own brand so the connection is obvious. Other naming options:
44
+ - `@bobfrankston/mailx-tinymce-adapter` (verbose, descriptive)
45
+ - `@bobfrankston/rmftiny` (no hyphen, matches rmfmail style)
46
+
47
+ `rmf-tiny` is fine; pick the form that scans cleanest in the install line.
48
+
49
+ ## Files
50
+
51
+ ### `rmf-tiny/package.json`
52
+
53
+ ```json
54
+ {
55
+ "name": "@bobfrankston/rmf-tiny",
56
+ "version": "0.1.0",
57
+ "description": "TinyMCE editor adapter for rmfmail. Bring-your-own TinyMCE.",
58
+ "type": "module",
59
+ "main": "src/adapter.js",
60
+ "license": "MIT",
61
+ "peerDependencies": {
62
+ "tinymce": ">=6"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "tinymce": { "optional": false }
66
+ }
67
+ }
68
+ ```
69
+
70
+ `peerDependencies` puts the install responsibility on the user — `npm install @bobfrankston/rmf-tiny` warns that `tinymce` must also be installed; rmf-tiny does not vendor or bundle TinyMCE.
71
+
72
+ ### `rmf-tiny/src/adapter.ts`
73
+
74
+ ```ts
75
+ // MIT-licensed adapter glue. Imports the user-installed TinyMCE at
76
+ // runtime; doesn't redistribute any TinyMCE bytes. Implements the same
77
+ // `MailxEditor` interface rmfmail's createEditor factory expects, so
78
+ // the host code path is identical.
79
+ import type { Editor as TinyEditor } from "tinymce";
80
+
81
+ export interface MailxEditor {
82
+ setHtml(html: string): void;
83
+ getHtml(): string;
84
+ getText(): string;
85
+ focus(): void;
86
+ setCursor(pos: number): void;
87
+ root: HTMLElement;
88
+ // …other methods rmfmail's interface requires
89
+ }
90
+
91
+ export async function createTinyMceEditor(container: HTMLElement): Promise<MailxEditor> {
92
+ // Dynamic import — fails clearly if the user hasn't installed tinymce.
93
+ const tinymce = (await import("tinymce")).default;
94
+ // Plus the plugins TinyMCE's paste-from-Word handler needs:
95
+ await Promise.all([
96
+ import("tinymce/themes/silver"),
97
+ import("tinymce/icons/default"),
98
+ import("tinymce/plugins/paste"),
99
+ import("tinymce/plugins/lists"),
100
+ import("tinymce/plugins/link"),
101
+ import("tinymce/plugins/table"),
102
+ import("tinymce/plugins/code"),
103
+ ]);
104
+ // ... initialize tinymce against `container`, return MailxEditor shim.
105
+ }
106
+ ```
107
+
108
+ The actual init/wire-up is the work — wrapping TinyMCE's API in our `MailxEditor` shape, hooking paste events, setHtml/getHtml round-tripping, etc. Maybe ~200 lines.
109
+
110
+ ### rmfmail-side change to `editor.ts`
111
+
112
+ ```ts
113
+ export async function createEditor(
114
+ container: HTMLElement,
115
+ type: "quill" | "tiptap" | "tinymce"
116
+ ): Promise<MailxEditor> {
117
+ if (type === "tinymce") {
118
+ try {
119
+ const m = await import("@bobfrankston/rmf-tiny");
120
+ return m.createTinyMceEditor(container);
121
+ } catch (e: any) {
122
+ const status = document.getElementById("status-sync");
123
+ if (status) status.textContent = `TinyMCE editor not available — install: npm install -g @bobfrankston/rmf-tiny tinymce`;
124
+ return createQuillEditor(container); // fall back so compose still works
125
+ }
126
+ }
127
+ if (type === "tiptap") return createTiptapEditor(container);
128
+ return createQuillEditor(container);
129
+ }
130
+ ```
131
+
132
+ Settings panel adds the "TinyMCE" option in the editor dropdown.
133
+
134
+ ## Effort
135
+
136
+ - `rmf-tiny` package skeleton (package.json, README, basic adapter shell): ~30 minutes.
137
+ - Adapter wiring (init, setHtml/getHtml, paste handling, toolbar config): ~3 hours including testing the Word-paste case.
138
+ - rmfmail-side factory branch + Settings dropdown option: ~30 minutes.
139
+ - Documentation for users (README in rmf-tiny + Settings tooltip): ~30 minutes.
140
+
141
+ About half a day total.
142
+
143
+ ## Order
144
+
145
+ 1. Build `rmf-tiny` against your local TinyMCE install. Verify the Word-paste case actually works through your adapter.
146
+ 2. Add the factory branch and Settings option to rmfmail.
147
+ 3. Publish `rmf-tiny` to npm.
148
+ 4. Document the opt-in install command in rmfmail's Settings tooltip and `README.md`.
149
+
150
+ Don't publish the adapter until the Word-paste case actually works end-to-end through the adapter — TinyMCE direct is one thing, our adapter possibly mangles it differently.
151
+
152
+ ## Caveats
153
+
154
+ - The adapter must not re-implement parts of TinyMCE — it's a thin wrapper. Re-implementing parts of GPL code in MIT is the legal trap; staying purely in adapter / interface territory is safe.
155
+ - TinyMCE's bundle is large (~1MB minified). Users opting in pay that cost on the first compose-window load. Fine for opt-in; would be unacceptable as a default.
156
+ - `rmf-tiny` major version should track TinyMCE's major version (rmf-tiny@7.x for tinymce@7.x) so peerDependency mismatches are obvious.