@runeditrun/rer 0.1.1 → 0.2.1

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.
Files changed (154) hide show
  1. package/README.md +113 -2
  2. package/cli/README.md +32 -5
  3. package/cli/contract.mjs +5 -5
  4. package/cli/deploy.mjs +17 -3
  5. package/cli/detached-server.mjs +51 -0
  6. package/cli/dev-identity.mjs +2 -1
  7. package/cli/door-mode.mjs +134 -0
  8. package/cli/door.mjs +263 -0
  9. package/cli/hosted-output.mjs +17 -7
  10. package/cli/hydration/browser.mjs +91 -0
  11. package/cli/hydration/cases.mjs +106 -0
  12. package/cli/hydration/check.mjs +105 -0
  13. package/cli/hydration/probe.mjs +164 -0
  14. package/cli/hydration/scripts.mjs +39 -0
  15. package/cli/hydration/verdict.mjs +85 -0
  16. package/cli/inspect.mjs +57 -10
  17. package/cli/lifecycle.mjs +36 -12
  18. package/cli/local.mjs +9 -3
  19. package/cli/main.mjs +3 -0
  20. package/cli/managed-server.mjs +62 -21
  21. package/cli/package-calls.mjs +81 -0
  22. package/cli/package-mode.mjs +46 -0
  23. package/cli/parse.mjs +15 -4
  24. package/cli/pnpm-runtime.mjs +96 -0
  25. package/cli/portability-worker.mjs +2 -2
  26. package/cli/release.mjs +1 -0
  27. package/cli/runtime-front-door.mjs +4 -2
  28. package/cli/server-proxy.mjs +69 -5
  29. package/cli/server-registry.mjs +10 -7
  30. package/cli/settings.mjs +14 -0
  31. package/cli/site-bindings.mjs +291 -0
  32. package/cli/site-browser-objects.mjs +36 -0
  33. package/cli/site-check.mjs +111 -0
  34. package/cli/site-client.mjs +50 -0
  35. package/cli/site-data.mjs +54 -0
  36. package/cli/site-entities.json +1 -0
  37. package/cli/site-input-assets.mjs +58 -0
  38. package/cli/site-inputs.mjs +79 -0
  39. package/cli/site-markup.mjs +102 -0
  40. package/cli/site-preview.mjs +53 -0
  41. package/cli/site-private.mjs +36 -0
  42. package/cli/site-references.mjs +93 -0
  43. package/cli/site-render.mjs +79 -0
  44. package/cli/site-syntax.mjs +21 -0
  45. package/cli/site.mjs +130 -0
  46. package/global/CHANGELOG.md +88 -0
  47. package/global/README.md +113 -2
  48. package/global/checkout.mjs +24 -10
  49. package/global/cli/install.mjs +6 -2
  50. package/global/delegate.mjs +9 -7
  51. package/global/door/command.mjs +254 -0
  52. package/global/door/control.mjs +65 -0
  53. package/global/door/cookies.mjs +41 -0
  54. package/global/door/pages.mjs +49 -0
  55. package/global/door/policy.mjs +49 -0
  56. package/global/door/routes.mjs +45 -0
  57. package/global/door/server.mjs +255 -0
  58. package/global/door/sites.mjs +192 -0
  59. package/global/launch/dev.mjs +21 -7
  60. package/global/launch/door.mjs +36 -0
  61. package/global/launch/install-pinned.mjs +22 -0
  62. package/global/launch/prepare.mjs +38 -5
  63. package/global/launch/render.mjs +6 -1
  64. package/global/launch/steps.mjs +12 -7
  65. package/global/launch/worker.mjs +3 -3
  66. package/global/list.mjs +6 -3
  67. package/global/main.mjs +39 -3
  68. package/global/output.mjs +12 -0
  69. package/global/rer.mjs +3 -0
  70. package/global/version.mjs +8 -8
  71. package/modules/dependencies/protocol.mjs +2 -1
  72. package/modules/deploy-kit/README.md +34 -1
  73. package/modules/deploy-kit/artifact.mjs +10 -0
  74. package/modules/deploy-kit/deploy.mjs +5 -2
  75. package/modules/deploy-kit/diagnostics.mjs +55 -0
  76. package/modules/deploy-kit/owner-cli.mjs +10 -4
  77. package/modules/deploy-kit/owner.mjs +8 -4
  78. package/modules/deploy-kit/prepare.mjs +3 -2
  79. package/modules/deploy-kit/provider.mjs +51 -9
  80. package/modules/deploy-kit/site.mjs +135 -0
  81. package/modules/erasure/CONTRACT.md +6 -3
  82. package/modules/erasure/README.md +8 -1
  83. package/modules/erasure/audit.mjs +2 -2
  84. package/modules/erasure/cascade.mjs +52 -11
  85. package/modules/erasure/order.mjs +29 -22
  86. package/modules/erasure/plan.mjs +4 -2
  87. package/modules/erasure/queue.mjs +41 -0
  88. package/modules/erasure/scope.mjs +3 -3
  89. package/modules/erasure/verification.mjs +4 -2
  90. package/modules/mail/README.md +1 -1
  91. package/modules/mail/index.mjs +1 -1
  92. package/modules/mail/src/core/errors.mjs +9 -0
  93. package/modules/mail/src/development/index.d.mts +6 -0
  94. package/modules/mail/src/development/index.mjs +6 -3
  95. package/modules/mail/src/development/viewer-html.mjs +3 -3
  96. package/modules/mail/src/development/viewer.mjs +8 -1
  97. package/modules/operator-auth/accounts/native.mjs +1 -1
  98. package/modules/operator-auth/dev/badge.mjs +4 -1
  99. package/modules/operator-auth/dev/index.mjs +15 -5
  100. package/modules/operator-auth/http.mjs +2 -3
  101. package/modules/operator-auth/index.d.mts +4 -9
  102. package/modules/operator-auth/loopback.mjs +17 -0
  103. package/modules/operator-auth/page.mjs +1 -1
  104. package/modules/operator-auth/service.mjs +1 -0
  105. package/modules/operator-auth/signin/index.d.mts +11 -0
  106. package/modules/operator-auth/signin/index.mjs +10 -7
  107. package/modules/owner-claim/verify.mjs +2 -1
  108. package/modules/portability/README.md +8 -0
  109. package/modules/portability/hash.mjs +6 -2
  110. package/modules/portability/import.mjs +1 -1
  111. package/modules/portability/legacy-files.mjs +4 -4
  112. package/modules/portability/legacy-safety.mjs +51 -10
  113. package/modules/portability/migration-history.mjs +15 -0
  114. package/modules/release/README.md +68 -3
  115. package/modules/release/keygen.mjs +18 -0
  116. package/modules/release/produce.mjs +14 -2
  117. package/modules/release/sign.mjs +44 -0
  118. package/modules/release/signature.mjs +336 -0
  119. package/modules/release/signing-cli.mjs +56 -0
  120. package/modules/release/signing-io.mjs +61 -0
  121. package/modules/release/signing-key.mjs +22 -0
  122. package/modules/release/source.mjs +15 -4
  123. package/modules/release/statement.mjs +80 -0
  124. package/modules/release/vectors/generate.mjs +381 -0
  125. package/modules/release/vectors/release-signature-vectors.json +2305 -0
  126. package/modules/release/verify.mjs +27 -0
  127. package/onboarding/adapter-auth.mjs +4 -3
  128. package/onboarding/adapter-settings.mjs +21 -5
  129. package/onboarding/adapter.mjs +2 -1
  130. package/onboarding/app-fetch.mjs +79 -0
  131. package/onboarding/archive-output.d.mts +4 -2
  132. package/onboarding/archive-output.mjs +22 -10
  133. package/onboarding/dependency-approval.mjs +4 -3
  134. package/onboarding/local/environment.mjs +7 -2
  135. package/onboarding/operator-install.mjs +8 -2
  136. package/onboarding/operator-prerequisites.mjs +5 -2
  137. package/onboarding/production-assets.mjs +23 -0
  138. package/onboarding/production-runtime.mjs +8 -4
  139. package/onboarding/queued-entry.mjs +9 -5
  140. package/operations/operations.mjs +11 -2
  141. package/package.json +76 -1
  142. package/scripts/publish-check/README.md +27 -17
  143. package/scripts/publish-check/content_identities.py +14 -233
  144. package/scripts/publish-check/git_history.py +2 -2
  145. package/scripts/publish-check/github_refs.py +28 -2
  146. package/scripts/publish-check/gitleaks.toml +1 -1
  147. package/scripts/publish-check/history_scan.py +6 -4
  148. package/scripts/publish-check/hygiene_config.py +3 -1
  149. package/scripts/publish-check/public_hygiene.py +63 -8
  150. package/scripts/publish-check/publication-config.json +16 -1
  151. package/scripts/publish-check/source_metadata.py +62 -0
  152. package/scripts/publish-check/source_shapes.py +91 -0
  153. package/scripts/publish-check/tree_scan.py +2 -2
  154. package/scripts/publish-check/source_hashes.mjs +0 -6
package/README.md CHANGED
@@ -97,6 +97,10 @@ RER_OPEN http://127.0.0.1:8787/__dev/key#...
97
97
  ```
98
98
 
99
99
  or `RER_SIGN_IN <outbox>` for a seed that signs in through its development outbox.
100
+ A seed that declares a run site prints one more line after those,
101
+ `RER_SITE http://127.0.0.1:8788`: a local preview of its static run site (its
102
+ marketing homepage, `/run` and docs) on the first free port above the app's. It
103
+ stops with the app.
100
104
  It never prompts and never re-runs setup on a checkout that is already set up. A
101
105
  setup a launch started that was stopped or failed is run again by the next
102
106
  `rer dev`, keeping the owner and origin it saved, unless a setup has finished
@@ -130,6 +134,27 @@ checkout never records a limit you did not set.
130
134
  A start is never refused for machine load alone unless `RER_SERVERS_LOAD_THRESHOLD`
131
135
  or `RER_JOBS_LOAD_THRESHOLD` is set; memory and disk guards stay.
132
136
 
137
+ ## The front door
138
+
139
+ ```sh
140
+ rer door enable # opt this computer in: port 8700, 10 servers, 12 hours idle
141
+ rer door add ~/review/* # register checkouts; prints each http://NAME.localhost:8700
142
+ rer door status --links # every checkout, its state, and sign-in links
143
+ ```
144
+
145
+ One process on one port serves every registered checkout at
146
+ `http://NAME.localhost:PORT` (NAME is its folder name, unique and stable);
147
+ `http://localhost:PORT` lists them. A stopped checkout starts when you open it
148
+ in a browser and stops when idle; subresources, API calls and WebSockets never
149
+ start one. `rer door enable` stores the policy (`--port`, `--server-limit`,
150
+ `--idle-timeout`) and the `RER_*` variables in effect, so whoever later starts
151
+ the door cannot change what its checkouts run with. While it is enabled, `rer
152
+ dev` sets fresh checkouts up behind it; `rer dev --door` moves an existing one
153
+ (keeping its data), `rer dev --direct` moves it back, and `--port N` always
154
+ serves directly. `rer door start`, `stop`, `restart`, `disable` and `remove
155
+ NAME` do what they say; after a reboot or logout the door needs `rer door
156
+ start`. `rer door --help` has the details.
157
+
133
158
  ## Find what to launch
134
159
 
135
160
  ```sh
@@ -144,15 +169,101 @@ Independent projects (an `OPERATIONS.json` version 2 without a seed contract) ru
144
169
  their declared commands with `rer <verb> [-- args]`; zero-setup launching does not
145
170
  apply to them. `OPERATIONS.json` version 1 is not supported.
146
171
 
172
+ ## Package mode: a checkout that pins `@runeditrun/rer`
173
+
174
+ A checkout runs one of two copies of rer. In **vendored mode** it carries
175
+ `scripts/rer.mjs`, `scripts/base-cli` and `scripts/base-onboarding`, and `rer`
176
+ runs that copy. In **package mode** it carries none of them and pins this package
177
+ as an exact devDependency instead (`"@runeditrun/rer": "0.2.0"`, locked in
178
+ `pnpm-lock.yaml`): its `OPERATIONS.json` argv and package scripts are `rer VERB`,
179
+ which inside `pnpm run` is the pinned copy in `node_modules/.bin`, so
180
+ `pnpm install --frozen-lockfile && pnpm run build` needs no global `rer`. A global
181
+ `rer` hands every verb to the pinned package, launching included. Until
182
+ `node_modules` holds the pinned version, `rer install` and `rer dev` install it
183
+ first (the frozen `pnpm install`) and every other verb says to run `rer install`.
184
+
185
+ Heavy package scripts wait for this computer's job queue with
186
+ `rer queue NAME -- COMMAND [ARGS]` (in vendored mode,
187
+ `node scripts/base-onboarding/queued-entry.mjs NAME -- COMMAND`).
188
+
189
+ `rer check --profile first-party` accepts either mode. In package mode it asks for
190
+ the exact devDependency (`SC-13.package.pin`), its lockfile entry
191
+ (`SC-13.package.lockfile`), no vendored copy (`SC-13.package.vendored.*`) and `rer
192
+ VERB` targets and aliases (`SC-02.target.*`, `SC-02.alias.*`); in vendored mode for
193
+ the copy and its recorded inventory, as before.
194
+
195
+ Convert a vendored seed from a Base checkout with
196
+
197
+ ```sh
198
+ node scripts/export-cli.mjs --to-package SEED_ROOT [--version X]
199
+ ```
200
+
201
+ (the default version is the latest published one, read with the seed's own npm
202
+ configuration). It refuses, listing them, when the vendored copy differs from its
203
+ recorded inventory: carry those edits with `pnpm patch @runeditrun/rer` once
204
+ converted, or upstream them to Base. Otherwise it pins and locks the package
205
+ (`pnpm install --lockfile-only`), rewrites imports into the copy to the exports
206
+ below (anything else to `@runeditrun/rer/internal/PATH`, which promises nothing
207
+ beyond the pinned version), rewrites the package scripts and `OPERATIONS.json`
208
+ argv to `rer VERB` and the delivery operations (`rer-project.json` and the
209
+ `scripts/delivery/base/packaging.json` it is generated from), the shell scripts
210
+ they and CI workflows run, and workflow lines to `pnpm exec rer VERB` (a frozen
211
+ `pnpm install` first where nothing has installed yet), drops the copy's own records from `base-copies.json` (the
212
+ app-runtime copies it records stay as they are), removes the copy and lists every
213
+ line that still names it. A later refresh is a version bump:
214
+ `pnpm add -D -E @runeditrun/rer@X`.
215
+
216
+ ### Exports
217
+
218
+ | Import | What it gives | Vendored path it replaces |
219
+ | --- | --- | --- |
220
+ | `@runeditrun/rer/adapter` | `commandAdapter` | `scripts/base-cli/cli/adapter.mjs` |
221
+ | `@runeditrun/rer/deploy` | `ownerDeploy` | `scripts/base-cli/cli/deploy.mjs` |
222
+ | `@runeditrun/rer/portability` | `localPortability` | `scripts/base-cli/cli/portability.mjs` |
223
+ | `@runeditrun/rer/errors` | the CLI's error helpers | `scripts/base-cli/cli/errors.mjs` |
224
+ | `@runeditrun/rer/jsonc` | JSONC reading | `scripts/base-cli/cli/jsonc.mjs` |
225
+ | `@runeditrun/rer/parse`, `/scheduled` | CLI argument parsing, scheduled-job helpers | `scripts/base-cli/cli/parse.mjs`, `scheduled.mjs` |
226
+ | `@runeditrun/rer/dev-identity` | dev identity helpers | `scripts/base-cli/cli/dev-identity.mjs` |
227
+ | `@runeditrun/rer/operator-installer` | `operatorInstaller`, `developmentTarget`, `previewTarget` | `scripts/base-onboarding/operator-installer.mjs` |
228
+ | `@runeditrun/rer/local-installer` | `localInstaller` | `scripts/base-onboarding/adapter.mjs` |
229
+ | `@runeditrun/rer/operator-install` | `installOperator` | `scripts/base-onboarding/operator-install.mjs` |
230
+ | `@runeditrun/rer/operator-prerequisites` | `operatorPrerequisites` | `scripts/base-onboarding/operator-prerequisites.mjs` |
231
+ | `@runeditrun/rer/local/state` | `selectLocalState` | `scripts/base-onboarding/local/state.mjs` |
232
+ | `@runeditrun/rer/local/build` | local build step | `scripts/base-onboarding/local/build.mjs` |
233
+ | `@runeditrun/rer/state`, `/commands` | onboarding state and command runners | `scripts/base-onboarding/state.mjs`, `commands.mjs` |
234
+ | `@runeditrun/rer/local/environment`, `/local/files`, `/local/operation`, `/local/recovery` | local setup helpers | `scripts/base-onboarding/local/*.mjs` |
235
+ | `@runeditrun/rer/private-files` | private file helpers | `scripts/base-onboarding/private-files.mjs` |
236
+ | `@runeditrun/rer/adapter-settings` | adapter settings helpers | `scripts/base-onboarding/adapter-settings.mjs` |
237
+ | `@runeditrun/rer/adapter-auth`, `/adapter-hosting` | adapter auth and hosting helpers | `scripts/base-onboarding/adapter-auth.mjs`, `adapter-hosting.mjs` |
238
+ | `@runeditrun/rer/process-diagnostics` | process diagnostics | `scripts/base-onboarding/process-diagnostics.mjs` |
239
+ | `@runeditrun/rer/process-cleanup`, `/process-scope` | process cleanup and scope | `scripts/base-onboarding/process-cleanup.mjs`, `process-scope.mjs` |
240
+ | `@runeditrun/rer/failures`, `/redaction` | failure and redaction helpers | `scripts/base-onboarding/failures.mjs`, `redaction.mjs` |
241
+ | `@runeditrun/rer/setup-manifest`, `/setup-schema` | setup manifest loading and its JSON schema | `scripts/base-onboarding/setup-manifest.mjs`, `schema/SETUP.schema.json` |
242
+ | `@runeditrun/rer/production-runtime`, `/production-modules` | production runtime helpers | `scripts/base-onboarding/production-runtime.mjs`, `production-modules.mjs` |
243
+ | `@runeditrun/rer/queued-entry` | the job-queue entry | `scripts/base-onboarding/queued-entry.mjs` |
244
+ | `@runeditrun/rer/archive-output` | `archiveOutputGuard` (with types) | `scripts/base-onboarding/archive-output.mjs` |
245
+ | `@runeditrun/rer/launcher` | the browser installer launcher | `scripts/base-onboarding/launcher.mjs` |
246
+ | `@runeditrun/rer/installer-server` | the browser installer's server (installer journeys) | `scripts/base-onboarding/server.mjs` |
247
+ | `@runeditrun/rer/server-resources` | managed server resource readings (runtime journeys) | `scripts/base-cli/cli/server-resources.mjs` |
248
+ | `@runeditrun/rer/operations` | OPERATIONS describe and check | `scripts/base-cli/operations/operations.mjs` |
249
+ | `@runeditrun/rer/modules/portability`, `/catalogue`, `/cli`, `/cli-options`, `/files`, `/format`, `/http`, `/import`, `/journal`, `/legacy-safety`, `/migration-history`, `/storage` | portability module | `scripts/base-cli/modules/portability/*.mjs` |
250
+ | `@runeditrun/rer/modules/release/export`, `/publication-scan`, `/source`, `/signature` | release export, publication scan and shared signature verifier | `scripts/base-cli/modules/release/*.mjs` |
251
+ | `@runeditrun/rer/modules/deploy-kit/owner`, `/owner-cli`, `/prepare`, `/provider` | deploy kit | `scripts/base-cli/modules/deploy-kit/*.mjs` |
252
+ | `@runeditrun/rer/modules/operator-auth`, `/dev`, `/dev/badge`, `/dev-admission`, `/accounts/native`, `/identity/email-link` | operator auth | `scripts/base-cli/modules/operator-auth/index.mjs`, `dev/index.mjs`, `dev/badge.mjs`, `dev-admission.mjs`, `accounts/native.mjs`, `identity/email-link/index.mjs` |
253
+ | `@runeditrun/rer/modules/erasure/fence` | erasure fence statements | `scripts/base-cli/modules/erasure/fence.mjs` |
254
+ | `@runeditrun/rer/modules/owner-claim`, `/modules/dependencies` | owner claim, dependency links | `scripts/base-cli/modules/owner-claim/index.mjs`, `dependencies/index.mjs` |
255
+ | `@runeditrun/rer/internal/PATH` | any other package file, for tests | `scripts/base-cli/PATH`, and `scripts/base-onboarding/PATH` with PATH under `onboarding/` |
256
+
147
257
  ## Verbs
148
258
 
149
259
  In a checkout, every verb runs that checkout's own pinned `rer`; `rer VERB --help`
150
260
  there lists its flags:
151
261
 
152
262
  `install`, `setup`, `dev`, `preview`, `build`, `verify`, `reset --yes`, `deploy`,
153
- `export`, `import`, `outbox`, `doctor`, `describe`, `check`, `release`.
263
+ `export`, `import`, `outbox`, `doctor`, `describe`, `check`, `release`,
264
+ `release sign`, `release receipt` and `release keygen`.
154
265
 
155
266
  Anywhere: `rer list [DIR]`, `rer cli status|install|uninstall`, `rer update`,
156
- `rer --version` and `rer --help`.
267
+ `rer queue NAME -- COMMAND`, `rer --version` and `rer --help`.
157
268
 
158
269
  Licence: MIT.
package/cli/README.md CHANGED
@@ -20,6 +20,18 @@ boundary and imports the existing `scripts/base-onboarding/`; no second onboardi
20
20
  tree or neighboring checkout is used. The foundation composition maps copy these
21
21
  same sources from an explicit committed Base revision and record their provenance.
22
22
 
23
+ `node scripts/export-cli.mjs --to-package SEED_ROOT [--version X]` instead moves a
24
+ vendored seed to package mode: it pins `@runeditrun/rer` exactly (default: the
25
+ latest published version), locks it, rewrites imports into the copy to the
26
+ package's exports and the command targets to `rer VERB`, rewrites delivery
27
+ operations, the shell scripts they and CI workflows run, and workflow lines to
28
+ `pnpm exec rer VERB` (`scripts/export-cli/calls.mjs`), and removes the copy; it
29
+ refuses, listing them, when the copy holds local edits. `cli/package-mode.mjs`
30
+ names what each mode's command targets are and reads the lockfile entry that
31
+ `check` requires (SC-13); `cli/package-calls.mjs` finds any of those commands
32
+ that still calls the removed copy, which `check` refuses (SC-13.package.calls). `global/README.md` lists the exports and the vendored
33
+ path each replaces.
34
+
23
35
  `node scripts/export-reference.mjs NEW_PATH` creates a standalone copy of the
24
36
  reference with runtime imports relocated into its own source tree and an explicit
25
37
  transformation inventory. It preserves source/tool pins and the real app; it does
@@ -119,8 +131,10 @@ Existing seeds receive this policy by refreshing `scripts/base-cli/` with
119
131
  onboarding copy). For production, adopt the persistent root-head stylesheet,
120
132
  primary preload and matched fallback described in `export/UI-CONVENTIONS.md`.
121
133
 
122
- Both are managed foreground servers; their first invocation stays active while
123
- serving. A compatible repeated invocation verifies the registered application,
134
+ Both are managed servers. `--detach` returns after readiness, leaving the server
135
+ running; `--foreground` keeps the invocation active while serving. Preview
136
+ refreshes changed built assets and their ETags; source edits need a restart and
137
+ rebuild. A compatible repeated invocation verifies the registered application,
124
138
  prints its safe ready/outbox URLs and exits without a new build or sign-in.
125
139
  Explicit conflicting host, port or timeout, or the other mode, refuses with
126
140
  exit 4 and the correct stop command. `--stop` is idempotent;
@@ -168,7 +182,7 @@ expiry clean up owned descendants before releasing state and capacity.
168
182
 
169
183
  The private registry defaults to the OS user's `~/.rer/servers` independently
170
184
  of checkout and Codex home; `RER_SERVERS_DIR` selects an isolated directory.
171
- The combined dev/preview cap is 10 (`RER_SERVER_LIMIT`, positive integer; a limit recorded by a server that did not set it never lowers the cap).
185
+ The combined dev/preview cap is 10 (`RER_SERVER_LIMIT`, positive integer). A start that sets `RER_SERVER_LIMIT` is capped by that value, higher or lower, whatever running servers recorded; a start that sets nothing is capped by the lowest limit a running server's person set explicitly, and a limit recorded by a server that did not set it never lowers the cap.
172
186
  Starting reservations count. At the cap a start is refused with a plain message
173
187
  naming each running server, its checkout and its stop command; rer never stops
174
188
  another checkout's server to make room, in any mode. A run that declares itself
@@ -207,6 +221,15 @@ Heavy verbs alone accept `--no-queue`, only with `CI=true`. Synthesized install
207
221
  child CI does not change the original decision. Inspection/help/outbox URL,
208
222
  stop and reset inventory/removal do not start a heavy job or application.
209
223
 
224
+ After the seed's verification target, `verify` runs two checks on the production
225
+ build served as `rer deploy` hosts it: `hosted-output.mjs` (built files and the deploy
226
+ identity) and `hydration/` (page sections hydrate as separate units; split into
227
+ `check.mjs` for the entry and standalone command, `browser.mjs` for Chromium, serving
228
+ and one load, `probe.mjs` for the in-page probe, `cases.mjs` for pages, cases and the
229
+ Cloudflare rewrite, `scripts.mjs` for which scripts are the application's, and
230
+ `verdict.mjs` for failures). SEED-COMMAND-CONTRACT.md,
231
+ "Hydration units", describes the check.
232
+
210
233
  Run the CLI contract and current first-party seed checks as part of Base's
211
234
  documented verification workflow. Structural `check` results and local runtime
212
235
  checks do not establish hosted acceptance or provider delivery.
@@ -233,11 +256,15 @@ is the tarball's path. `global/package.json` is the one file that names the next
233
256
  release version; a build from a commit not tagged `rer-v<version>` is stamped
234
257
  `<version>-dev.g<sha8>`. The installed `rer` finds the nearest `OPERATIONS.json`
235
258
  (or exactly `--cwd`, with no upward search) and runs the checkout's pinned `node_modules/@runeditrun/rer`, else
236
- its vendored `scripts/rer.mjs` in process, else its own bundled `bin/rer.mjs`.
259
+ its vendored `scripts/rer.mjs` in process, else its own bundled `bin/rer.mjs`. A
260
+ pinned package that `node_modules` does not hold yet is installed by `rer install`
261
+ and `rer dev` first (the frozen `pnpm install`).
237
262
  Global verbs: `rer cli status|install|uninstall`, `rer list [DIR]`, `rer update`,
238
263
  and the zero-setup `rer dev`, which composes the pinned dispatcher's public verbs so it
239
264
  works on every pin. `global/README.md` is the package's own README; the package
240
265
  root also carries the script installer, `install.sh`, kept in `global/`. The build is
241
266
  reproducible and `.github/workflows/rer-release.yml` publishes it; see
242
267
  `scripts/rer-package/README.md`. `verify:rer` packs, installs and launches it end
243
- to end, and drives `install.sh` and `rer update` against a local registry.
268
+ to end on the package-mode reference (served from a local registry, see
269
+ `scripts/rer-package/registry.mjs`), and drives `install.sh` and `rer update`
270
+ against that registry.
package/cli/contract.mjs CHANGED
@@ -5,13 +5,13 @@ export const serverDefaults = Object.freeze({ serverLimit: 10, idleSeconds: 4320
5
5
  export const commonFlags = ['--help', '--cwd', '--non-interactive', '--json'];
6
6
  const definitions = {
7
7
  install: [[], 'Frozen dependency installation; no setup or global tool changes.'],
8
- setup: [['--owner-email', '--owner-name', '--port', '--host', '--answers'], 'Build, migrate, prepare private settings and first owner; stop the temporary app.'],
9
- dev: [['--port', '--host', '--idle-timeout', '--stop', '--as', '--login', '--models', '--models-service'], 'Foreground development with assets, HMR and the declared scheduled triggers run locally on their schedule. With devIdentity it opens signed in as the owner (--as PERSONA picks another start, --login uses the outbox sign-in instead); otherwise ordinary owner sign-in through the outbox.'],
10
- preview: [['--port', '--host', '--idle-timeout', '--stop'], 'Foreground fixed production output; reuse intact build, preserve owner/state, idle timeout and explicit stop.'],
11
- build: [[], 'Production build with private local settings excluded and restored.'],
8
+ setup: [['--owner-email', '--owner-name', '--port', '--host', '--answers', '--door'], 'Build, migrate, prepare private settings and first owner; stop the temporary app. --door sets a fresh checkout up at its front-door origin http://NAME.localhost:PORT (after rer door enable).'],
9
+ dev: [['--port', '--host', '--idle-timeout', '--stop', '--detach', '--foreground', '--as', '--login', '--models', '--models-service', '--door', '--direct'], 'Development with assets, HMR and the declared scheduled triggers run locally on their schedule. --detach returns once ready; --foreground keeps this command serving. With devIdentity it opens signed in as the owner (--as PERSONA picks another start, --login uses the outbox sign-in instead); otherwise ordinary owner sign-in through the outbox. --door moves this checkout to its front-door origin http://NAME.localhost:PORT, keeping its data; --direct moves it back; --port and --host serve directly.'],
10
+ preview: [['--port', '--host', '--idle-timeout', '--stop', '--detach', '--foreground'], 'Production output; --detach returns once ready, --foreground keeps this command serving. Reuse intact build, refresh changed assets, preserve owner/state, idle timeout and explicit stop.'],
11
+ build: [['--site'], 'Production build with private local settings excluded and restored. --site instead writes the static run site (homepage, /run, docs) to its declared folder and prints that folder.'],
12
12
  verify: [[], 'Run the complete internal verification target.'],
13
13
  reset: [['--yes'], 'Confirmed removal of the declared local inventory only.'],
14
- deploy: [['--environment', '--dry-run', '--host', '--env-file', '--image', '--bind-address', '--http-port', '--https-port'], 'Owner-account deployment and health; dry run performs local validation/build only.'],
14
+ deploy: [['--environment', '--dry-run', '--site', '--host', '--env-file', '--image', '--bind-address', '--http-port', '--https-port'], 'Owner-account deployment and health; dry run performs local validation/build only. --site deploys the run site instead: an assets-only Worker on its custom domain with www redirected; with --dry-run it builds the site and prints the plan with no provider request.'],
15
15
  export: [['--target', '--output', '--origin', '--session-file'], 'Complete supported installation archive with integrity and exclusions.'],
16
16
  import: [['--target', '--input', '--journal', '--origin', '--session-file', '--yes', '--legacy-v1', '--acknowledge-unverified-metadata', '--output'], 'Confirmed atomic restore into an empty destination; --journal LATER_ARCHIVE also fences rows that installation erased since. Backup-only local v1 recovery requires --legacy-v1 --acknowledge-unverified-metadata and a new --output v2 archive; --journal requires normal v2 import.'],
17
17
  outbox: [['--port', '--host', '--owner-email', '--open'], 'Credential-free local outbox URL; optional ordinary sign-in request and explicit browser opening.'],
package/cli/deploy.mjs CHANGED
@@ -1,5 +1,7 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { resolve } from 'node:path';
3
+ import { deployDiagnostic } from '../modules/deploy-kit/diagnostics.mjs';
4
+ import { shared } from './shared.mjs';
3
5
  import { contained } from './project.mjs';
4
6
  import { missing, invalid, refuse } from './errors.mjs';
5
7
  import { ownerPlan, ownerWorkerName, localOnlyBinding } from '../modules/deploy-kit/owner.mjs';
@@ -30,8 +32,20 @@ export function ownerDeploy(declarationPath, lifecycle) {
30
32
  if (names.some(name => !process.env[name]?.trim())) throw missing('CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN are required. See the deploy-kit owner-account instructions.');
31
33
  const { main } = await import('../modules/deploy-kit/owner-cli.mjs');
32
34
  const argv = ['deploy', '--declaration', declarationPath, '--environment', environment];
33
- const result = await main(argv, process.env, { cwd: root, localOnlyKeys, notice: line => store.report(line) });
34
- await main(['health', ...argv.slice(1)], process.env, { cwd: root, localOnlyKeys });
35
- return { deployed: true, origin: result.origin };
35
+ const {createRedaction,credentialValues} = await shared('redaction.mjs');
36
+ const redaction = createRedaction();
37
+ redaction.addSecrets([...credentialValues(process.env),process.env.CLOUDFLARE_ACCOUNT_ID,...(declaration.secrets ?? []).map(item=>process.env[item.environment]).filter(Boolean)]);
38
+ // Keep confirmed creation events across deploy and the independent health call.
39
+ const created = [];
40
+ try {
41
+ const result = await main(argv, process.env, { cwd: root, localOnlyKeys, notice: line => store.report(line), onCreated: resource => created.push(redaction.redact(resource)) });
42
+ await main(['health', ...argv.slice(1)], process.env, { cwd: root, localOnlyKeys });
43
+ return { deployed: true, origin: result.origin };
44
+ } catch (error) {
45
+ const diagnostic = deployDiagnostic(error,redaction.redact);
46
+ const code = /^[a-zA-Z0-9_-]{1,120}$/.test(error.message) ? redaction.redact(error.message) : 'deploy-failed';
47
+ const {commandFailure} = await shared('failures.mjs');
48
+ throw commandFailure(`Deploy failed at ${diagnostic?.step ?? 'prepare deploy'}: ${diagnostic?.code ?? code}${diagnostic?.providerCodes?.length ? ` (provider ${diagnostic.providerCodes.join(', ')})` : ''}.\nFirst redacted output lines:\n${diagnostic?.output ?? '(no provider output)'}\nResources created this attempt: ${created.join(', ') || 'none recorded'}.`);
49
+ }
36
50
  }, { declaration: declarationPath });
37
51
  }
@@ -0,0 +1,51 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { open, readFile } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { setTimeout as delay } from 'node:timers/promises';
7
+ import { shared } from './shared.mjs';
8
+ import { ready } from './managed-server.mjs';
9
+ import { RerError } from './errors.mjs';
10
+
11
+ // The actual dispatcher owns the server, registry and shutdown. Only its
12
+ // lifetime changes: its parent follows private JSON events until readiness,
13
+ // then leaves that process serving. A failed/interrupted start stops its child.
14
+ export async function detachedServer(options, argv, out) {
15
+ const {privateDirectory} = await shared('private-files.mjs');
16
+ const directory = await privateDirectory(options.cwd,['.rer','local']);
17
+ const log = join(directory,`${options.verb}-launch-${randomUUID()}.log`), handle = await open(log,'wx',0o600);
18
+ const args = argv.filter(arg => !['--detach','--foreground','--json'].includes(arg));
19
+ let child, exit, spawnError, keep = false;
20
+ const stop = () => child?.kill('SIGTERM');
21
+ for (const signal of ['SIGINT','SIGTERM']) process.once(signal,stop);
22
+ try {
23
+ child = spawn(process.execPath,[fileURLToPath(new URL('../bin/rer.mjs',import.meta.url)),...args,'--foreground','--json'], {cwd:options.cwd, env:process.env, detached:true, stdio:['ignore',handle.fd,handle.fd]});
24
+ child.once('error',error => {spawnError=error;exit=1;});
25
+ child.once('exit',(code,signal) => {exit=code ?? (signal ? 128 : 1);});
26
+ await handle.close();
27
+ let offset=0, pending='', failure;
28
+ for (;;) {
29
+ const bytes = await readFile(log), text = bytes.subarray(offset).toString('utf8'); offset=bytes.length;
30
+ pending += text;
31
+ const lines=pending.split('\n');pending=lines.pop();
32
+ for (const line of lines) {
33
+ let event; try { event=JSON.parse(line); } catch { continue; }
34
+ if (event.event==='error') failure=event;
35
+ if (event.event==='ready') {
36
+ keep=true;child.unref();
37
+ const dev = event.openUrl ? {openUrl:event.openUrl,identity:options.verb === 'dev' && !options.login,signedInAs:event.signedInAs,ownerLeft:options.verb === 'dev' && !options.login && event.signedInAs === null} : undefined;
38
+ ready(out,event.url,dev,options.json,Boolean(event.signInUrl),event.siteUrl);
39
+ return 0;
40
+ }
41
+ if (event.event==='waiting' || event.event==='warning') out.event(event.event,event);
42
+ }
43
+ if (exit!==undefined) throw new RerError(failure?.code ?? 'command_failed',failure?.message ?? `The ${options.verb} server stopped before readiness (${spawnError?.code ?? exit}); inspect ${log}.`,failure?.exitCode ?? (exit || 1));
44
+ await delay(200);
45
+ }
46
+ } finally {
47
+ await handle.close().catch(() => {});
48
+ for (const signal of ['SIGINT','SIGTERM']) process.off(signal,stop);
49
+ if (!keep) stop();
50
+ }
51
+ }
@@ -58,7 +58,8 @@ export function refuseManagedInstallation(...sources) {
58
58
 
59
59
  // The persona list the running app declares, read with the key header.
60
60
  export async function resolvePersona(url, key, as, signal) {
61
- const response = await fetch(url + '/__dev/personas', { headers: { [DEV_KEY_HEADER]: key }, signal: AbortSignal.any([signal, AbortSignal.timeout(10000)].filter(Boolean)) });
61
+ const { appFetch } = await shared('app-fetch.mjs');
62
+ const response = await appFetch(url + '/__dev/personas', { headers: { [DEV_KEY_HEADER]: key }, signal: AbortSignal.any([signal, AbortSignal.timeout(10000)].filter(Boolean)) });
62
63
  if (!response.ok) { await response.body?.cancel(); throw refuse(`The application did not serve dev identity (HTTP ${response.status}). Check its local-only entry.`); }
63
64
  const personas = await response.json(), persona = personas.find(entry => entry.as === as);
64
65
  if (!persona) throw invalid(`Unknown dev persona ${as}. This app declares: ${personas.map(entry => entry.as).join(', ')}.`);
@@ -0,0 +1,134 @@
1
+ // Door mode for one checkout: its saved origin is http://NAME.localhost:PORT,
2
+ // it serves on 127.0.0.1:<OS-assigned port> behind the machine's front door,
3
+ // and its probes go straight to that port (never through the door). A saved
4
+ // 127.0.0.1 origin keeps direct mode; --port and --host always serve directly.
5
+ import { randomBytes } from 'node:crypto';
6
+ import { readFile, rm } from 'node:fs/promises';
7
+ import { join } from 'node:path';
8
+ import { claimSiteName, doorHost, doorOrigin, doorProtocol, doorSecretHeader, doorSite, readDoorState, runningDoor, verifyDoor } from './door.mjs';
9
+ import { refuse } from './errors.mjs';
10
+ import { firstFreePort } from './port.mjs';
11
+ import { localSettingKeys } from './settings.mjs';
12
+ import { shared } from './shared.mjs';
13
+
14
+ const moveRecord = '.rer/local/door-move.json';
15
+
16
+ // The enabled door this checkout can use, or a refusal saying why not.
17
+ export async function enabledDoor(env = process.env) {
18
+ const state = await readDoorState(env);
19
+ if (!state?.door?.enabled) throw refuse(`The front door is not enabled on this computer (${state?.directory ?? 'RER_DOOR_DIR'}). Enable it with rer door enable, or serve directly with rer dev --direct.`);
20
+ const running = await runningDoor(env);
21
+ const protocol = running?.protocol ?? state.door.protocol;
22
+ if (protocol !== doorProtocol) throw refuse(`The front door speaks protocol ${protocol}, which this checkout's rer (protocol ${doorProtocol}) does not know. Run rer door restart with the installed rer.`);
23
+ return state.door;
24
+ }
25
+
26
+ // The listening side of a door origin: 127.0.0.1 on a port the OS assigns,
27
+ // guarded by a secret chosen for this start.
28
+ export function doorSelection(name, port, fields) {
29
+ return { ...fields, url: doorOrigin(name, port), host: '127.0.0.1', port: 0, explicitPort: false, door: { name, port, host: doorHost(name, port), header: doorSecretHeader, secret: randomBytes(32).toString('base64url') } };
30
+ }
31
+
32
+ // Selection for a checkout whose saved origin is a door origin, or that asks
33
+ // for the door (--door on a fresh setup). Returns undefined for direct mode.
34
+ export async function selectDoor(project, options, previous, fields) {
35
+ if (options.port !== undefined || options.host !== undefined || options.direct) return undefined;
36
+ const saved = previous && doorSite(previous.origin);
37
+ if (saved) {
38
+ // A server or setup on a saved origin re-claims the name it names
39
+ // (registering a checkout that rer door remove unregistered) and never
40
+ // serves under a name another checkout holds.
41
+ await enabledDoor();
42
+ if (['dev', 'preview', 'setup'].includes(options.verb)) await claimSavedName(project, previous.origin);
43
+ return doorSelection(saved.name, saved.port, fields);
44
+ }
45
+ if (!options.door) return undefined;
46
+ if (previous) throw refuse('This checkout is already set up at ' + previous.origin + '. Move it to the front door with rer dev --door, which keeps its data.');
47
+ const door = await enabledDoor(), name = await claimSiteName(process.env, project.root, { mode: 'door' });
48
+ return doorSelection(name, door.port, fields);
49
+ }
50
+
51
+ // A server on a door origin holds the name that origin names: it registers it
52
+ // again after rer door remove, and refuses when another checkout holds it.
53
+ export async function claimSavedName(project, origin) {
54
+ const site = doorSite(origin);
55
+ if (site && (await readDoorState())?.door) await claimSiteName(process.env, project.root, { preferred: site.name, mode: 'door', exact: true });
56
+ }
57
+
58
+ // Record the server's door route on its registry entry and send rer's own
59
+ // requests for the origin straight to its proxy.
60
+ export async function announceDoorRoute(selected, proxy, registry) {
61
+ const { routeOrigin } = await shared('app-fetch.mjs');
62
+ routeOrigin(selected.url, { port: proxy.port, headers: { [doorSecretHeader]: selected.door.secret } });
63
+ await registry?.update({ port: proxy.port, door: { v: doorProtocol, name: selected.door.name, port: selected.door.port, secret: selected.door.secret } });
64
+ }
65
+
66
+ // A running server's route, from its registry entry: only while its server
67
+ // process is alive and itself listens on the recorded proxy port, so nothing
68
+ // rer sends (a dev key among it) reaches a process that took that port later.
69
+ export async function routeRunning(record) {
70
+ if (!record?.door) return;
71
+ const { alive } = await import('../modules/job-queue/identity.mjs');
72
+ const { listeners } = await shared('process-diagnostics.mjs');
73
+ const live = await alive(record.owner).catch(() => false);
74
+ const holds = live && (await listeners([record.owner.pid])).some(row => row.pid === record.owner.pid && row.names.some(name => /^(?:127\.0\.0\.1|\[?::ffff:127\.0\.0\.1\]?):(\d+)$/.exec(name)?.[1] === String(record.port)));
75
+ if (!holds) throw refuse(`This checkout's server (PID ${record.owner?.pid}) is not serving on its recorded port 127.0.0.1:${record.port}, so nothing was sent to it. Stop it with rer dev --stop, then start it again with rer dev.`);
76
+ const { routeOrigin } = await shared('app-fetch.mjs');
77
+ routeOrigin(record.url, { port: record.port, headers: { [doorSecretHeader]: record.door.secret } });
78
+ }
79
+
80
+ // Before a dev key is printed for a door origin.
81
+ export async function verifyDoorFor(url) {
82
+ const site = doorSite(url);
83
+ if (site) await verifyDoor(process.env, site.port);
84
+ }
85
+
86
+ // The saved origin, read without changing anything (rer describe).
87
+ export async function savedOrigin(project) {
88
+ const secrets = project.adapter?.hooks?.secrets;
89
+ if (!secrets) return null;
90
+ let text;
91
+ try { text = await readFile(join(project.root, secrets.file ?? '.dev.vars'), 'utf8'); } catch { return null; }
92
+ try {
93
+ const values = (secrets.format ?? 'dotenv') === 'json' ? JSON.parse(text) : (await shared('local/environment.mjs')).localVariables(text);
94
+ return typeof values?.[localSettingKeys(project).origin] === 'string' ? values[localSettingKeys(project).origin] : null;
95
+ } catch { return null; }
96
+ }
97
+
98
+ // rer dev --door / --direct: rewrite only the saved origin, keeping the local
99
+ // database and state. --door records the direct origin it replaces; --direct
100
+ // restores it (or the first free port from 8787, or --port).
101
+ export async function moveCheckout(project, options, out, registry) {
102
+ const secrets = project.adapter?.hooks?.secrets;
103
+ if (!secrets) throw refuse('This checkout declares no saved origin, so it cannot move to or from the front door.');
104
+ const { readSettings, replaceSavedOrigin } = await shared('adapter-settings.mjs');
105
+ const key = localSettingKeys(project).origin, current = (await readSettings(project.root, secrets)).values[key];
106
+ if (!current) throw refuse('This checkout is not set up yet. Run rer dev without --door or --direct; with the door enabled it is set up there.');
107
+ const atDoor = Boolean(doorSite(current));
108
+ if (options.door === atDoor) return;
109
+ const running = await registry.existing(options.verb, { ...options, supplied: new Set() });
110
+ if (running) throw refuse(`Stop this checkout's running server first (rer dev --stop), then move it.`);
111
+ let target, record;
112
+ const { privateDirectory, readPrivateFile, writePrivateFile } = await shared('private-files.mjs');
113
+ if (options.door) {
114
+ const door = await enabledDoor(), name = await claimSiteName(process.env, project.root, { mode: 'door' });
115
+ target = doorOrigin(name, door.port);
116
+ record = { previousOrigin: current, movedAt: new Date().toISOString() };
117
+ } else {
118
+ let previous;
119
+ try { previous = JSON.parse(await readPrivateFile(join(project.root, moveRecord))).previousOrigin; } catch {}
120
+ target = options.port !== undefined ? `http://127.0.0.1:${options.port}` : previous && !doorSite(previous) ? previous : `http://127.0.0.1:${await firstFreePort('127.0.0.1')}`;
121
+ const site = doorSite(current);
122
+ if (site && await readDoorState()) await claimSiteName(process.env, project.root, { preferred: site.name, mode: 'direct', origin: target });
123
+ }
124
+ const { localOperation } = await shared('local/operation.mjs');
125
+ let operation;
126
+ try { operation = await localOperation(project.root); } catch (error) { throw refuse(error.message); }
127
+ try {
128
+ await replaceSavedOrigin(project.root, secrets, target);
129
+ if (record) { await privateDirectory(project.root, ['.rer', 'local']); await writePrivateFile(join(project.root, moveRecord), JSON.stringify(record) + '\n'); }
130
+ else await rm(join(project.root, moveRecord), { force: true });
131
+ } finally { await operation.release(); }
132
+ const line = options.door ? `Moved this checkout from ${current} to ${target}, through the front door. Its data is kept; rer dev --direct moves it back.` : `Moved this checkout from ${current} back to ${target}. Its data is kept.`;
133
+ if (options.json) out.event('moved', { from: current, to: target }); else out.human(line);
134
+ }