You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 9888008
Browse filesBrowse the repository at this point in the historyBrowse files
fix(cli): os dev -p and os serve --port export the port as OS_PORT/PORT, so a config reads the port the server listens on (#26268)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XUzmgvQZMEYuBB7qUesPo6L)_Fixes#26243
Clause-②: no
Metadata-protocol: none
## What was wrong
`os dev -p N` forwarded the port to its `serve` child as `--port` on
argv, and `os serve --port N` handed it to its listener. Neither put it
in the environment, so a config that reads `OS_PORT`/`PORT` while it
loads saw the inherited value or the 3000 default.
`examples/app-showcase`'s self URL is such a config. `os start --port`
was already right, because it hands the flag to its child as `OS_PORT` +
`PORT` (#12992).
Measured on `examples/app-showcase` at `81bba6c3` (main), with no
`OS_PORT`/`PORT`/`SHOWCASE_SELF_URL` in the environment. "ping" is `POST
/api/v1/automation/showcase_declarative_connector_ping/trigger`, and
"meta" is `providerConfig.baseUrl` from `GET
/api/v1/meta/connector/showcase_status_api`.
| boot | meta before | ping before | meta after (`af26658b`) | ping
after |
|---|---|---|---|---|
| `os dev --fresh -p 41302` | `http://127.0.0.1:3000` | 400 `FLOW_FAILED
… fetch failed` | `http://127.0.0.1:41302` | 200, `status: ok` |
| `os serve --port 41303 objectstack.config.ts` (production) |
`http://127.0.0.1:3000` | 400 `FLOW_FAILED … fetch failed` |
`http://127.0.0.1:41303` | 200, `status: ok` |
| `os start --port 41304` (control) | `http://127.0.0.1:41304` | 200 |
unchanged | unchanged |
`os serve --port` showed the same symptom, so it is fixed through the
same source, per the ruling.
## The fix
There is one writer for the port's environment, and all three doors call
it. No door restates the precedence.
- `packages/cli/src/utils/port-contract.ts`: `os start`'s `childPortEnv`
moves here as `portFlagEnv(port)`, unchanged in behaviour. It writes `{
OS_PORT, PORT }` with one value when a flag was given, and nothing
otherwise. Its docblock moves with it and now names all three callers.
- `packages/cli/src/commands/dev.ts`: the port door (resolution plus
refusal) moves ahead of the compile, because the compile is the first
child that evaluates the config. Once a `-p` passes the door, `os dev`
merges `portFlagEnv(parseRequestedPort(port))` into its own
`process.env`. The compile, the watch recompiles and the `serve` child
all run in that environment (the one #26240's `devCompilesAtBoot` relies
on), so both copies of the stack read the flag's port. An invalid `-p`
is now refused before the compile instead of after it. The argv
forwarding is unchanged.
- `packages/cli/src/commands/serve.ts`: once the port is validated, a
`--port` given on argv (oclif's `setFromDefault` is not `true`) is
merged into `process.env` the same way, before the config is loaded. A
port that came from `OS_PORT`/`PORT` or the default leaves the
environment as it is.
- `packages/cli/src/commands/start.ts`: imports `portFlagEnv`; its two
comment references are re-pointed. The behaviour is unchanged.
- `examples/app-showcase/src/system/self-url.ts`: header comment only.
Its claim that it reads "the same names … the CLI itself reads" is now
true for the flag too, and it says how.
The exported value is the parsed port, not the text: `--port 3e3`
selects port 3, so it exports `3`. `os dev` keeps forwarding its flag
text on argv, so the child's text-read notice still names `--port
"3e3"`. Both channels come from one variable at one door. The
`portFlagEnv` docblock says why that is deliberate, and why `os start`,
whose integer flag has no such text, stays on the environment alone.
## Tests
At `45116cbc` (the head; `af26658b` plus the changeset):
- `packages/cli/test/dev-connector-env-parity.integration.test.ts`
(#26240's file, reshaped): two boots of the built `bin/run.js` on the
minimal host config.
- `os serve objectstack.config.ts --port N`, production posture, with no
`OS_PORT`. The materialized connector `baseUrl` must be
`http://127.0.0.1:N`.
- `os dev -p N` over an inherited stale `OS_PORT=41302` and an artifact
built with `:3000`. The served and the materialized `baseUrl` must both
be `:N`. This one boot holds both cards: before #26240 the served copy
read `:3000`, and before this PR both copies read `:41302`.
- `#26130`'s old boot was `-p 0` with `OS_PORT=41302` as the config's
value. This PR makes `-p 0` win for the config too, so that shape no
longer holds. Each boot now draws a non-zero port with `reservePort()`,
because the config has to know the number before the boot. The draw is
ledgered in `released-port-production-spawn.pin.test.ts` (2 draws, with
the reason), and a lost race reads as
`portDriftError`/`portContentionError`.
- `start-port-forwarding-channel.pin.test.ts` now imports `portFlagEnv`
from the contract module. Its five behavioural cases are unchanged.
- Touched files together: `vitest run` on the integration file,
`released-port-production-spawn.pin.test.ts`,
`start-port-forwarding-channel.pin.test.ts` and
`port-contract-single-source.test.ts` gave `Test Files 4 passed (4)`,
`Tests 21 passed (21)`.
- `pnpm --filter @objectstack/cli exec vitest run --project unit` gave
`Test Files 334 passed (334)` and `Tests 4878 passed (4878)` (lock
VERDICT command-exit 0). `pnpm --filter @objectstack/cli typecheck`
exited 0. The integration tier ran locally for the touched file only;
the rest is declared to CI.
- Ablations, one per export, each through
`scripts/ablation-replace.mjs`. In each case the CLI was rebuilt and
`ablation-dist-preflight` found the marker in
`packages/cli/dist/commands/*.js`.
- The dev export was made a no-op: the served leg went red (`expected
'http://127.0.0.1:41302' to be 'http://127.0.0.1:33115'`) and the
materialized leg stayed green. That shows why the parent has to export:
the `serve` child re-exports from its own argv, but the compile only
sees the parent's environment.
- The serve export was made a no-op: the serve leg went red (`expected
'http://127.0.0.1:3000' to be 'http://127.0.0.1:44503'`) and both dev
legs stayed green.
- Both were restored (blob equals HEAD, `git status --porcelain` empty),
the CLI was rebuilt, and `ablation-dist-preflight --absent` exited 0 for
both markers.
- A first attempt at the dev ablation did not compile (`TS2367` on the
literal comparison). Nothing ran, and it is not counted.
## Gates
Local subset: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `45116cbc` derived 73 families. `--ran`
reconciliation: 72 ran with exit 0, 1 NOT MEASURED, 0 unrun.
- NOT MEASURED, reason: prerequisite not met (exit 3). `pnpm
check:dual-build-cjs-loads` reads a full-repo build that this container
did not make. CI runs it.
- `node scripts/check-plugin-teardown-shape.mjs --self-test` exited 3 on
the shallow clone. After `git fetch origin 621a487…` (its positive
control), it passed 48 cases.
- `pnpm check:cli-test-child-env` passed (exit 0). The reshaped test is
already in its census, and the new serve leg also spawns `bin/run.js`
with `NODE_ENV` unset.
- `pnpm exec eslint --no-inline-config` on the 8 touched source and test
files exited 0. Neither `@objectstack/cli` nor the showcase has a `lint`
script.
## Acceptance notes
- **Dev auto-shift.** Measured at `af26658b` with `41310` held by
another listener: `os dev --fresh -p 41310` printed `Port 41310 is in
use — serving on 41311 instead`. Its `/meta` served `:41310` and the
ping answered `400 FLOW_FAILED … fetch failed`. The config is evaluated
before the bind, so it reads the requested port, not the shifted one. A
bare `os dev` with 3000 busy has the same shape on `main`. This is the
#7538 family, and this PR does not widen to it, per the ruling. It goes
to the seat as a finding.
- **`--port 0`** now exports `0`, so a config like the showcase's builds
`:0` where it used to build `:3000` (or the inherited `OS_PORT`).
Neither address serves anything. A kernel-assigned port cannot be known
before the bind.
- **Port text from the environment** (`OS_PORT=3e3`) is not normalized.
Only a flag is exported, as `os start` already did, so an operator's own
variables reach the config exactly as set.
#26242 remains open; this PR does not address it.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01LrUJpT4UGJT1bwN5uvT8dh)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(cli): `os dev -p N` and `os serve --port N` export the port as `OS_PORT`/`PORT`, so a config that reads the port sees the one the server listens on
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
-**Before.** The port flag reached the listener only. A config that builds a URL from `OS_PORT`/`PORT` while it loads, such as `examples/app-showcase`'s self URL, got the inherited value or the 3000 default. On the showcase with no `OS_PORT`/`PORT`, `os dev -p 41302` and `os serve --port 41303` both served `GET /api/v1/meta/connector/showcase_status_api` with `http://127.0.0.1:3000`, and the self-ping flow answered `400 FLOW_FAILED … fetch failed`.
11
+
-**Now.**`os dev` and `os serve` export the port the flag selected as both `OS_PORT` and `PORT` before anything loads the config, the way `os start` already handed it to its server. Under `os dev` that covers the compile, the watch recompiles and the server. The flag overrides an inherited `OS_PORT` for the config, as it already did for the listener. The same two boots now serve `:41302` / `:41303`, and the ping answers 200.
12
+
- With no port flag, the environment is left as it is. The exported value is the parsed port (`--port 3e3` exports `3`, the port it selects).
0 commit comments