@reventlessdev/reventless-local 3.0.0-alpha.243 → 3.0.0-alpha.245

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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.245 (2026-09-04)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **logging:** let a runtime declare whether a person reads its logs ([df7a6cb](https://github.com/ReventlessDev/reventless-core/commit/df7a6cbdff62b856ea6b9d1f169b913563ba13a1))
11
+ ### Features
12
+
13
+ * **local:** one platform per app directory, and a reset that refuses a served store ([e0fd219](https://github.com/ReventlessDev/reventless-core/commit/e0fd219a8c1bbda85589374ffa75aaab0f3e4fda))
14
+ * **local:** serve the event tap over a socket, on by default ([1babf96](https://github.com/ReventlessDev/reventless-core/commit/1babf960f032805418e3ac8f25d03ceb3dcd8d20))
15
+
16
+
17
+ # 3.0.0-alpha.244 (2026-09-04)
18
+
19
+ **Note:** Version bump only for package @reventlessdev/reventless-local
20
+
21
+
22
+
23
+
24
+
6
25
  # 3.0.0-alpha.243 (2026-09-02)
7
26
 
8
27
  * feat(aws)!: the messaging sender is configuration, and a stack can choose to only log ([23b8b4b](https://github.com/ReventlessDev/reventless-core/commit/23b8b4bfe9c70555de4d74266ca686cb427485ca))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-local",
3
- "version": "3.0.0-alpha.243",
3
+ "version": "3.0.0-alpha.245",
4
4
  "description": "Local platform for Reventless (in-memory or SQLite backend, for development and testing without AWS)",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -36,22 +36,22 @@
36
36
  "ws": "^8.18.0",
37
37
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
38
38
  "@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.27",
39
- "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.21",
40
- "@reventlessdev/rescript-node": "2.0.0-alpha.8",
41
39
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
42
- "@reventlessdev/reventless-core": "3.0.0-alpha.254",
40
+ "@reventlessdev/rescript-node": "2.0.0-alpha.9",
41
+ "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.21",
43
42
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
44
- "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.102",
45
- "@reventlessdev/reventless-infra": "3.0.0-alpha.155",
46
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.201",
47
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.118",
48
- "@reventlessdev/reventless-seed": "1.0.0-alpha.18",
49
- "@reventlessdev/reventless-spec": "3.0.0-alpha.127"
43
+ "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.104",
44
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.203",
45
+ "@reventlessdev/reventless-core": "3.0.0-alpha.256",
46
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.157",
47
+ "@reventlessdev/reventless-seed": "1.0.0-alpha.19",
48
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.129",
49
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.120"
50
50
  },
51
51
  "devDependencies": {
52
52
  "rescript": "12.3.0",
53
53
  "sury-ppx": "11.0.0-rc.2",
54
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.76"
54
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.77"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "rescript": "12.3.0"
@@ -1,38 +1,14 @@
1
- // Where the local platforms running on this machine say who they are.
2
- //
3
- // A platform's store is otherwise invisible from outside its own process, so
4
- // every tool that wants to act on "the" store — `seed`, `seed:reset`, a `sqlite3`
5
- // poke has to guess from its own environment. The guess is right exactly while
6
- // there is one platform and wrong exactly when there are two, which is the case
7
- // the tools exist for: a hand-started `pnpm run serve` on `local.db` beside the
8
- // VS Code runner's child on `runner.db`, same app, same directory. Both then
9
- // report success against different databases — a reset that empties an unserved
10
- // store, and a seed that refuses because the served one is full.
11
- //
12
- // So a platform publishes itself instead: one JSON entry per run, next to the
13
- // store it opened, removed on the way out. `LocalSeedTarget` turns that into the
14
- // answer to "which platform do you mean".
15
- //
16
- // ── Why one file per port ──────────────────────────────────────────────────
17
- //
18
- // The pair that broke is two platforms of the SAME app in the SAME package
19
- // directory, which is precisely what a single `platform.json` cannot represent.
20
- // The domain port is what already distinguishes them (`REVENTLESS_DOMAIN_PORT`,
21
- // added so the runner could start one child per app without colliding), so it
22
- // names the file.
23
- //
24
- // ── Why the path is absolute ───────────────────────────────────────────────
25
- //
26
- // `./.reventless/local.db` names a different file in every app. Relative to the
27
- // writer's cwd it is unambiguous and outside it means nothing, so the entry
28
- // carries the path `BackendState` resolved, not the env string it came from.
1
+ // Where the local platforms running on this machine say who they are: one JSON
2
+ // entry per run under `<cwd>/.reventless/running/<domainPort>.json`, removed on
3
+ // exit. Keyed by domain port because the pair that broke is two platforms of the
4
+ // SAME app in one directory. Paths are absolute`./.reventless/local.db` names
5
+ // a different file in every app. `LocalSeedTarget` turns this into the answer to
6
+ // "which platform do you mean".
29
7
 
30
8
  @schema
31
9
  type store = {
32
- // "sqlite" | "memory" | "postgres" what a reader can DO with this platform's
33
- // store, not which module implements it. A `:memory:` SQLite file reports
34
- // "memory": it has nothing on disk to open, which is the only distinction a
35
- // tool acting from outside can act on.
10
+ // What a reader can DO with the store, not which module implements it
11
+ // "sqlite" | "memory" | "postgres". A `:memory:` SQLite file reports "memory".
36
12
  kind: string,
37
13
  // Some only for a store this machine can open by name.
38
14
  path: option<string>,
@@ -47,20 +23,21 @@ type entry = {
47
23
  loginEndpoint: string,
48
24
  store: store,
49
25
  startedAt: string,
26
+ // Where the NDJSON event tap is served, when it is. Must stay optional: `list`
27
+ // DELETES an entry it cannot decode, so requiring it would silently hide every
28
+ // platform from an older build. Absent ⇒ stdout only.
29
+ tapPort?: int,
50
30
  }
51
31
 
52
- // `~cwd` is the directory the platform was started from, which is where its
53
- // `.reventless/` already lives. Threaded rather than read at each use so the
54
- // tests can exercise a registry without moving the whole process.
32
+ // Threaded rather than read at each use, so tests need not move the process.
55
33
  let runningDir = (~cwd=NodeProcess.cwd(), ()): string =>
56
34
  NodePath.join([cwd, ".reventless", "running"])
57
35
 
58
36
  let entryPath = (~port: int, ~cwd=NodeProcess.cwd()): string =>
59
37
  NodePath.join([runningDir(~cwd, ()), `${port->Int.toString}.json`])
60
38
 
61
- // The package this platform was started from, for the prompt line. Two platforms
62
- // of one app are told apart by port; the name is there for the case where they
63
- // are different apps and the port alone reads as an arbitrary number.
39
+ // The package name, for the prompt line port alone reads as an arbitrary
40
+ // number when the platforms are different apps.
64
41
  let appName = (~cwd=NodeProcess.cwd(), ()): string => {
65
42
  let manifest = NodePath.join([cwd, "package.json"])
66
43
  let fromManifest = if NodeFs.existsSync(manifest) {
@@ -85,11 +62,8 @@ let removeQuietly = (path: string): unit =>
85
62
  | _ => ()
86
63
  }
87
64
 
88
- /** Whether a process is still there. Signal `0` performs no signalling — it only
89
- runs the permission and existence checks and throws when either fails.
90
-
91
- Liveness by pid rather than by probing the endpoint keeps this synchronous,
92
- and prunes a wedged platform that holds its port but would never answer. */
65
+ /** Signal `0` signals nothing — it runs the existence/permission checks and
66
+ throws when either fails. By pid, so a wedged platform is pruned too. */
93
67
  let alive = (pid: int): bool =>
94
68
  try {
95
69
  NodeProcess.kill(pid, 0)
@@ -103,12 +77,8 @@ let readEntry = (path: string): option<entry> =>
103
77
  | _ => None
104
78
  }
105
79
 
106
- /** Every platform currently running in this directory, oldest port first.
107
-
108
- Removal on exit is best-effort — a `kill -9` leaves the entry behind — so
109
- staleness is settled here, on read: an entry whose process is gone is deleted
110
- rather than reported. A reader that trusted the file would offer a platform
111
- nothing is serving, which is the failure this module exists to remove. */
80
+ /** Every platform running here, lowest port first. Removal on exit is
81
+ best-effort (`kill -9`), so a dead entry is deleted here, on read. */
112
82
  let list = (~cwd=NodeProcess.cwd(), ()): array<entry> => {
113
83
  let dir = runningDir(~cwd, ())
114
84
  if !NodeFs.existsSync(dir) {
@@ -132,14 +102,14 @@ let list = (~cwd=NodeProcess.cwd(), ()): array<entry> => {
132
102
  }
133
103
  }
134
104
 
135
- /** Writes one entry and hands back the path it wrote, without touching the
136
- process. Split from {!register} so a test can populate a registry without
137
- installing exit handlers in the test runner. */
105
+ /** Writes one entry and returns its path. Split from {!register} so a test can
106
+ populate a registry without installing exit handlers in the runner. */
138
107
  let write = (
139
108
  ~port: int,
140
109
  ~endpoint: string,
141
110
  ~loginEndpoint: string,
142
111
  ~store: store,
112
+ ~tapPort: option<int>=?,
143
113
  ~pid: int=NodeProcess.pid,
144
114
  ~cwd: string=NodeProcess.cwd(),
145
115
  ): string => {
@@ -152,6 +122,7 @@ let write = (
152
122
  loginEndpoint,
153
123
  store,
154
124
  startedAt: Date.make()->Date.toISOString,
125
+ ?tapPort,
155
126
  }
156
127
  try {
157
128
  NodeFs.mkdirSync(runningDir(~cwd, ()), {recursive: true})
@@ -160,23 +131,43 @@ let write = (
160
131
  entry->Reventless.Util_Sury.toJson(entrySchema)->JSON.stringify(~space=2),
161
132
  )
162
133
  } catch {
163
- // A platform that cannot announce itself must still serve. The tools fall back
164
- // to their endpoint default, which is what they did before this existed.
134
+ // A platform that cannot announce itself must still serve.
165
135
  | _ => ()
166
136
  }
167
137
  path
168
138
  }
169
139
 
170
- /** Publishes this process, and arranges for it to stop being published.
140
+ /** Adds the tap port to an entry already written.
141
+
142
+ Separate from {!write} because the socket binds asynchronously: the entry is
143
+ published as soon as the servers are up, and this fills in the port once
144
+ `listen` calls back. So the entry only ever names a port that is genuinely
145
+ listening. A no-op when there is no entry to update. */
146
+ let publishTapPort = (~port: int, ~tapPort: int, ~cwd=NodeProcess.cwd()): unit => {
147
+ let path = entryPath(~port, ~cwd)
148
+ switch readEntry(path) {
149
+ | None => ()
150
+ | Some(entry) =>
151
+ try NodeFs.writeFileSync(
152
+ path,
153
+ {...entry, tapPort}->Reventless.Util_Sury.toJson(entrySchema)->JSON.stringify(~space=2),
154
+ ) catch {
155
+ | _ => ()
156
+ }
157
+ }
158
+ }
171
159
 
172
- Both shutdown paths are covered because they are not the same event: `exit`
173
- fires with the process already committed to leaving and may only do
174
- synchronous work, while `SIGINT`/`SIGTERM` fire before that decision — and
175
- registering a signal handler at all is what suppresses Node's default exit,
176
- so each one has to leave explicitly (128 + signal number, as a shell reports
177
- it). */
178
- let register = (~port: int, ~endpoint: string, ~loginEndpoint: string, ~store: store): unit => {
179
- let path = write(~port, ~endpoint, ~loginEndpoint, ~store)
160
+ /** Publishes this process, and unpublishes it on the way out. Both paths are
161
+ needed: `exit` is sync-only, and registering a signal handler suppresses
162
+ Node's default exit, so each signal must leave explicitly. */
163
+ let register = (
164
+ ~port: int,
165
+ ~endpoint: string,
166
+ ~loginEndpoint: string,
167
+ ~store: store,
168
+ ~tapPort: option<int>=?,
169
+ ): unit => {
170
+ let path = write(~port, ~endpoint, ~loginEndpoint, ~store, ~tapPort?)
180
171
  NodeProcess.onExit(_ => removeQuietly(path))
181
172
  NodeProcess.onSignal(#SIGINT, () => {
182
173
  removeQuietly(path)
@@ -21,7 +21,8 @@ let entrySchema = Sury.$schema(s => ({
21
21
  endpoint: s.m(Sury.string),
22
22
  loginEndpoint: s.m(Sury.string),
23
23
  store: s.m(storeSchema),
24
- startedAt: s.m(Sury.string)
24
+ startedAt: s.m(Sury.string),
25
+ tapPort: s.m(Sury.$option(Sury.int))
25
26
  }));
26
27
 
27
28
  function runningDir(cwdOpt, param) {
@@ -108,7 +109,7 @@ function list(cwdOpt, param) {
108
109
  }).toSorted((a, b) => Primitive_int.compare(a.port, b.port));
109
110
  }
110
111
 
111
- function write(port, endpoint, loginEndpoint, store, pidOpt, cwdOpt) {
112
+ function write(port, endpoint, loginEndpoint, store, tapPort, pidOpt, cwdOpt) {
112
113
  let pid = pidOpt !== undefined ? pidOpt : process.pid;
113
114
  let cwd = cwdOpt !== undefined ? cwdOpt : process.cwd();
114
115
  let path = entryPath(port, cwd);
@@ -121,7 +122,8 @@ function write(port, endpoint, loginEndpoint, store, pidOpt, cwdOpt) {
121
122
  endpoint: endpoint,
122
123
  loginEndpoint: loginEndpoint,
123
124
  store: store,
124
- startedAt: entry_startedAt
125
+ startedAt: entry_startedAt,
126
+ tapPort: tapPort
125
127
  };
126
128
  try {
127
129
  Nodefs.mkdirSync(runningDir(cwd, undefined), {
@@ -134,8 +136,24 @@ function write(port, endpoint, loginEndpoint, store, pidOpt, cwdOpt) {
134
136
  return path;
135
137
  }
136
138
 
137
- function register(port, endpoint, loginEndpoint, store) {
138
- let path = write(port, endpoint, loginEndpoint, store, undefined, undefined);
139
+ function publishTapPort(port, tapPort, cwdOpt) {
140
+ let cwd = cwdOpt !== undefined ? cwdOpt : process.cwd();
141
+ let path = entryPath(port, cwd);
142
+ let entry = readEntry(path);
143
+ if (entry === undefined) {
144
+ return;
145
+ }
146
+ try {
147
+ let newrecord = {...entry};
148
+ Nodefs.writeFileSync(path, JSON.stringify(Util_Sury$Reventless.toJson((newrecord.tapPort = tapPort, newrecord), entrySchema), undefined, 2), "utf8");
149
+ return;
150
+ } catch (exn) {
151
+ return;
152
+ }
153
+ }
154
+
155
+ function register(port, endpoint, loginEndpoint, store, tapPort) {
156
+ let path = write(port, endpoint, loginEndpoint, store, tapPort, undefined, undefined);
139
157
  process.on("exit", param => removeQuietly(path));
140
158
  process.on("SIGINT", () => {
141
159
  removeQuietly(path);
@@ -158,6 +176,7 @@ export {
158
176
  readEntry,
159
177
  list,
160
178
  write,
179
+ publishTapPort,
161
180
  register,
162
181
  }
163
182
  /* storeSchema Not a pure module */
@@ -0,0 +1,84 @@
1
+ // What a starter asks the registry before it starts a local platform: whether a
2
+ // reset would wipe a store something is serving, and whether to start at all.
3
+ // Scoped to <cwd>/.reventless/running/, so another app's store file is not
4
+ // covered. See docs/plans/done/one-local-platform-one-store.md.
5
+
6
+ // Naming a port IS the statement that this platform is meant to coexist — the
7
+ // escape hatch the runner and the e2e suites already take.
8
+ let bypassEnv = "REVENTLESS_DOMAIN_PORT"
9
+
10
+ let envNamed = (key: string): bool =>
11
+ switch NodeProcess.env->Dict.get(key) {
12
+ | Some(v) if v->String.trim != "" => true
13
+ | _ => false
14
+ }
15
+
16
+ type decision =
17
+ | Start
18
+ | AlreadyRunning(array<LocalPlatformRegistry.entry>)
19
+
20
+ /** Whether this process should go on to build a platform. `~cwd` is threaded so a
21
+ test can exercise a registry without moving the whole process. */
22
+ let decide = (~cwd=NodeProcess.cwd(), ()): decision =>
23
+ if envNamed(bypassEnv) {
24
+ Start
25
+ } else {
26
+ switch LocalPlatformRegistry.list(~cwd, ()) {
27
+ | [] => Start
28
+ | entries => AlreadyRunning(entries)
29
+ }
30
+ }
31
+
32
+ // Through LocalSeedTarget, so the starters and the seed tools describe the same
33
+ // platform the same way.
34
+ let entryLine = (e: LocalPlatformRegistry.entry): string =>
35
+ `→ already running at :${e.port->Int.toString} · ${e.store->LocalSeedTarget.storeLabel} (pid ${e.pid->Int.toString})`
36
+
37
+ /** What {!orAddressRunning} prints. Separate from the exit so it is testable. */
38
+ let report = (entries: array<LocalPlatformRegistry.entry>): array<string> =>
39
+ switch entries {
40
+ | [e] => [e->entryLine, ` nothing started — ${e.endpoint} is serving this directory.`]
41
+ | entries =>
42
+ Array.concat(
43
+ [`→ ${entries->Array.length->Int.toString} platforms are already running in this directory:`],
44
+ entries->Array.map(entryLine),
45
+ )->Array.concat([" nothing started — stop the ones you do not want, then start again."])
46
+ }
47
+
48
+ /** The live platform serving this store file, if one is. Resolved against this
49
+ process's cwd, because the registry's paths are absolute. */
50
+ let servedBy = (~path: string, ~cwd=NodeProcess.cwd(), ()): option<
51
+ LocalPlatformRegistry.entry,
52
+ > => {
53
+ let resolved = NodePath.resolve([path])
54
+ LocalPlatformRegistry.list(~cwd, ())->Array.find(e => e.store.path == Some(resolved))
55
+ }
56
+
57
+ let refusalMessage = (~path: string, e: LocalPlatformRegistry.entry): string =>
58
+ `refusing to reset ${path}: it is the store of ${e.app} running at :${e.port->Int.toString} (pid ${e.pid->Int.toString}). ` ++
59
+ `Stop that platform first, or start this one without ?reset.`
60
+
61
+ /** Throws rather than letting a reset unlink a store something is serving. Always
62
+ on: it must protect an app that never calls {!orAddressRunning}. */
63
+ let guardReset = (~path: string, ~cwd=NodeProcess.cwd(), ()): unit =>
64
+ switch servedBy(~path, ~cwd, ()) {
65
+ | Some(e) => JsError.throwWithMessage(refusalMessage(~path, e))
66
+ | None => ()
67
+ }
68
+
69
+ /** Start, or address the one already running. An app's `Main.res` calls this as
70
+ its first line, before `Platform.Make()`. */
71
+ let orAddressRunning = (~cwd=NodeProcess.cwd(), ()): unit => {
72
+ // The reset is checked first and throws: exit 0 is for a start that was merely
73
+ // redundant, never for a destructive one that did not happen.
74
+ switch Backend.fromEnv() {
75
+ | Backend.Sqlite({path, resetOnStart: true}) => guardReset(~path, ~cwd, ())
76
+ | Backend.Sqlite(_) | Backend.Memory | Backend.Postgres(_) => ()
77
+ }
78
+ switch decide(~cwd, ()) {
79
+ | Start => ()
80
+ | AlreadyRunning(entries) =>
81
+ report(entries)->Array.forEach(Console.log)
82
+ NodeProcess.exit(0)
83
+ }
84
+ }
@@ -0,0 +1,99 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Nodepath from "node:path";
4
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
5
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
6
+ import * as Backend$ReventlessLocal from "./adapter/Backend.res.mjs";
7
+ import * as LocalSeedTarget$ReventlessLocal from "./LocalSeedTarget.res.mjs";
8
+ import * as LocalPlatformRegistry$ReventlessLocal from "./LocalPlatformRegistry.res.mjs";
9
+
10
+ let bypassEnv = "REVENTLESS_DOMAIN_PORT";
11
+
12
+ function envNamed(key) {
13
+ let v = process.env[key];
14
+ if (v !== undefined) {
15
+ return v.trim() !== "";
16
+ } else {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ function decide(cwdOpt, param) {
22
+ let cwd = cwdOpt !== undefined ? cwdOpt : process.cwd();
23
+ if (envNamed(bypassEnv)) {
24
+ return "Start";
25
+ }
26
+ let entries = LocalPlatformRegistry$ReventlessLocal.list(cwd, undefined);
27
+ if (entries.length !== 0) {
28
+ return {
29
+ TAG: "AlreadyRunning",
30
+ _0: entries
31
+ };
32
+ } else {
33
+ return "Start";
34
+ }
35
+ }
36
+
37
+ function entryLine(e) {
38
+ return `→ already running at :` + e.port.toString() + ` · ` + LocalSeedTarget$ReventlessLocal.storeLabel(e.store) + ` (pid ` + e.pid.toString() + `)`;
39
+ }
40
+
41
+ function report(entries) {
42
+ if (entries.length !== 1) {
43
+ return [`→ ` + entries.length.toString() + ` platforms are already running in this directory:`].concat(entries.map(entryLine)).concat([" nothing started — stop the ones you do not want, then start again."]);
44
+ }
45
+ let e = entries[0];
46
+ return [
47
+ entryLine(e),
48
+ ` nothing started — ` + e.endpoint + ` is serving this directory.`
49
+ ];
50
+ }
51
+
52
+ function servedBy(path, cwdOpt, param) {
53
+ let cwd = cwdOpt !== undefined ? cwdOpt : process.cwd();
54
+ let resolved = Nodepath.resolve(path);
55
+ return LocalPlatformRegistry$ReventlessLocal.list(cwd, undefined).find(e => Primitive_object.equal(e.store.path, resolved));
56
+ }
57
+
58
+ function refusalMessage(path, e) {
59
+ return `refusing to reset ` + path + `: it is the store of ` + e.app + ` running at :` + e.port.toString() + ` (pid ` + e.pid.toString() + `). Stop that platform first, or start this one without ?reset.`;
60
+ }
61
+
62
+ function guardReset(path, cwdOpt, param) {
63
+ let cwd = cwdOpt !== undefined ? cwdOpt : process.cwd();
64
+ let e = servedBy(path, cwd, undefined);
65
+ if (e !== undefined) {
66
+ return Stdlib_JsError.throwWithMessage(refusalMessage(path, e));
67
+ }
68
+ }
69
+
70
+ function orAddressRunning(cwdOpt, param) {
71
+ let cwd = cwdOpt !== undefined ? cwdOpt : process.cwd();
72
+ let match = Backend$ReventlessLocal.fromEnv();
73
+ if (typeof match === "object" && match.TAG === "Sqlite") {
74
+ if (match.resetOnStart) {
75
+ guardReset(match.path, cwd, undefined);
76
+ }
77
+ }
78
+ let entries = decide(cwd, undefined);
79
+ if (typeof entries !== "object") {
80
+ return;
81
+ }
82
+ report(entries._0).forEach(prim => {
83
+ console.log(prim);
84
+ });
85
+ process.exit(0);
86
+ }
87
+
88
+ export {
89
+ bypassEnv,
90
+ envNamed,
91
+ decide,
92
+ entryLine,
93
+ report,
94
+ servedBy,
95
+ refusalMessage,
96
+ guardReset,
97
+ orAddressRunning,
98
+ }
99
+ /* node:path Not a pure module */
package/src/Platform.res CHANGED
@@ -17,6 +17,12 @@ let _ = ReventlessCore.EffectLogger.install
17
17
  // LOG_LEVEL env var still overrides this (e.g. LOG_LEVEL=info or =silent).
18
18
  ReventlessCore.EffectLogger.setDefaultMinLevel(ReventlessCore.Logger.Debug)
19
19
 
20
+ // Same reasoning, for the format: a person reads these logs, including when
21
+ // `dev:full`'s concurrently, `serve:watch`'s tsx or an IDE pipes them — which is
22
+ // where the TTY probe alone concludes "log collector" and emits JSON. An explicit
23
+ // REVENTLESS_LOG_FORMAT still wins.
24
+ Reventless.AnsiStyle.setDefaultFormat(#text)
25
+
20
26
  let log = ReventlessCore.Logger.fromEnv()
21
27
 
22
28
  // Module-level ref to hold the platform GraphQL server instance in split mode.
@@ -114,6 +120,10 @@ module MakeWithConfig = (
114
120
  // full replay needs no checkpoint.
115
121
  | Backend.Sqlite({path, resetOnStart}) =>
116
122
  if resetOnStart {
123
+ // Before the unlink: the ports are bound at the very end of construction,
124
+ // so a second `serve:reset` used to wipe a served store and only then
125
+ // discover it could not listen.
126
+ LocalPlatformStart.guardReset(~path, ())
117
127
  Backend.removeFileIfExists(path)
118
128
  }
119
129
  let db = SqliteDriver.openDb(~path)
@@ -1316,6 +1326,12 @@ module MakeWithConfig = (
1316
1326
  ~loginEndpoint=`http://localhost:${domainPort->Int.toString}/__inmemory/login`,
1317
1327
  ~store={kind, path},
1318
1328
  )
1329
+ // After the register, and it fills the tap port in once the socket is bound:
1330
+ // the port is ephemeral by default, so it is not known until `listen` calls
1331
+ // back. The entry therefore never names a port that is not listening.
1332
+ LocalEventTap.start(~onBound=tapPort =>
1333
+ LocalPlatformRegistry.publishTapPort(~port=domainPort, ~tapPort)
1334
+ , ())
1319
1335
  // Fire onPlatformDeployed after all servers are started so late-deployed
1320
1336
  // plugins (e.g. PlatformInspector) have their handler refs populated.
1321
1337
  ReventlessCore.Plugin_Helpers.firePlatformDeployedHook({
@@ -13,6 +13,7 @@ import * as Effect from "effect/Effect";
13
13
  import * as Pulumi from "@pulumi/pulumi";
14
14
  import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
15
15
  import * as Plugin$Reventless from "@reventlessdev/reventless-spec/src/components/Plugin.res.mjs";
16
+ import * as AnsiStyle$Reventless from "@reventlessdev/reventless-spec/src/AnsiStyle.res.mjs";
16
17
  import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
17
18
  import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
18
19
  import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
@@ -35,6 +36,7 @@ import * as ComponentType$ReventlessCore from "@reventlessdev/reventless-core/sr
35
36
  import * as SqliteDriver$ReventlessLocal from "./adapter/SqliteDriver.res.mjs";
36
37
  import * as Task_Builder$ReventlessLocal from "./components/Task_Builder.res.mjs";
37
38
  import * as BakedManifest$ReventlessLocal from "./BakedManifest.res.mjs";
39
+ import * as LocalEventTap$ReventlessLocal from "./adapter/LocalEventTap.res.mjs";
38
40
  import * as Platform_Admin$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_Admin.res.mjs";
39
41
  import * as PluginBehavior$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginBehavior.res.mjs";
40
42
  import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
@@ -59,6 +61,7 @@ import * as ProjectionPending$ReventlessLocal from "./adapter/ProjectionPending.
59
61
  import * as ReadModel_Builder$ReventlessLocal from "./components/ReadModel_Builder.res.mjs";
60
62
  import * as UiFragmentRegistry$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res.mjs";
61
63
  import * as LocalEvents_Server$ReventlessLocal from "./adapter/Api/LocalEvents_Server.res.mjs";
64
+ import * as LocalPlatformStart$ReventlessLocal from "./LocalPlatformStart.res.mjs";
62
65
  import * as PlatformMCP_Server$ReventlessLocal from "./adapter/PlatformMCP_Server.res.mjs";
63
66
  import * as ResourceAttribution$ReventlessCore from "@reventlessdev/reventless-core/src/ResourceAttribution.res.mjs";
64
67
  import * as Auth_GraphqlContext$ReventlessLocal from "./adapter/Auth/Auth_GraphqlContext.res.mjs";
@@ -102,6 +105,8 @@ import * as InboundTranslationResolvers_GraphQL$ReventlessLocal from "./adapter/
102
105
 
103
106
  EffectLogger$ReventlessCore.setDefaultMinLevel("Debug");
104
107
 
108
+ AnsiStyle$Reventless.setDefaultFormat("text");
109
+
105
110
  let log = Logger$ReventlessCore.fromEnv();
106
111
 
107
112
  let platformGraphQLRef = {
@@ -141,6 +146,7 @@ function MakeWithConfig(Config) {
141
146
  let resetOnStart = match.resetOnStart;
142
147
  let path = match.path;
143
148
  if (resetOnStart) {
149
+ LocalPlatformStart$ReventlessLocal.guardReset(path, undefined, undefined);
144
150
  Backend$ReventlessLocal.removeFileIfExists(path);
145
151
  }
146
152
  let db = SqliteDriver$ReventlessLocal.openDb(path);
@@ -1117,7 +1123,8 @@ function MakeWithConfig(Config) {
1117
1123
  LocalPlatformRegistry$ReventlessLocal.register(domainPort, `http://localhost:` + domainPort.toString() + `/graphql`, `http://localhost:` + domainPort.toString() + `/__inmemory/login`, {
1118
1124
  kind: match[0],
1119
1125
  path: match[1]
1120
- });
1126
+ }, undefined);
1127
+ LocalEventTap$ReventlessLocal.start(undefined, tapPort => LocalPlatformRegistry$ReventlessLocal.publishTapPort(domainPort, tapPort, undefined), undefined);
1121
1128
  Plugin_Helpers$ReventlessCore.firePlatformDeployedHook({
1122
1129
  name: "local",
1123
1130
  environment: Pulumi.getStack(),
@@ -1889,6 +1896,7 @@ function Make($star) {
1889
1896
  let resetOnStart = backend.resetOnStart;
1890
1897
  let path = backend.path;
1891
1898
  if (resetOnStart) {
1899
+ LocalPlatformStart$ReventlessLocal.guardReset(path, undefined, undefined);
1892
1900
  Backend$ReventlessLocal.removeFileIfExists(path);
1893
1901
  }
1894
1902
  let db = SqliteDriver$ReventlessLocal.openDb(path);
@@ -2855,7 +2863,8 @@ function Make($star) {
2855
2863
  LocalPlatformRegistry$ReventlessLocal.register(domainPort, `http://localhost:` + domainPort.toString() + `/graphql`, `http://localhost:` + domainPort.toString() + `/__inmemory/login`, {
2856
2864
  kind: match[0],
2857
2865
  path: match[1]
2858
- });
2866
+ }, undefined);
2867
+ LocalEventTap$ReventlessLocal.start(undefined, tapPort => LocalPlatformRegistry$ReventlessLocal.publishTapPort(domainPort, tapPort, undefined), undefined);
2859
2868
  Plugin_Helpers$ReventlessCore.firePlatformDeployedHook({
2860
2869
  name: "local",
2861
2870
  environment: Pulumi.getStack(),