fix(services): accept --port 0, the worker shape deploy already supports

TH
2mo ago
+68-12

Closes #163.

The problem

insta services add compute <name> --port 0 was rejected by client-side validation:

$ insta services add compute wk --port 0
error: port must be an integer between 1 and 65535, got: 0

Port 0 is not an out-of-range port here, it is a service shape: the worker shape, a long-running process with no HTTP endpoint (queue consumers, background agents), which the compute plane gives no PORT env and no ClusterIP. The platform accepts it and the deploy path already creates it:

$ insta deploy --image alpine:3.20 --port 0 --group wk --json
{ "machineId": "insta-compute", "url": "", ... }     # empty url = no HTTP endpoint, correct

So a worker could only be created by picking some other port at creation and letting the first deploy converge it to 0. The one command whose job is "create a service of this shape" was the one that could not express it.

The change

parsePort accepts 0, and names it in the error. The bound moves from n < 1 to n < 0, and the message becomes port must be 0 (worker: no HTTP endpoint) or an integer between 1 and 65535, got: <raw>. Naming 0 rather than silently widening the range keeps the message useful for a genuine typo, and teaches the shape at the point of error. Every other rejection is unchanged, including the non-decimal spellings Number() would have honoured: 0x1f90 and 0o17620 start with a 0 that the widened range must not swallow, and the decimal-digits-only regex still refuses them.

The two --port checks in servicesAdd / servicesAddRequestBody move from a truthiness test to a presence test. They happen to work today only because the option arrives as the string "0". A truthy test on a parsed value would drop the worker port and provision a default-port HTTP service instead of the shape that was asked for, which is exactly the failure mode --no-always-on already hit (an explicit value read as "not given"), and the mapping is the place that bug would land. The presence test also fixes a smaller thing in passing: --port 0 on a non-compute type now fails locally the way --port 3000 does, instead of slipping past the guard as "no port given".

Docs. services add --port help now names the worker shape, and so does the interactive port prompt, which is the only place the shape is discoverable without reading the reference.

compute connect-repo --port comes along, because it shares the parser. A repo-backed worker is the same shape as an image-backed one, so --port 0 there now overrides the detected HTTP port rather than being refused. Its help text says so, and there is a test.

Deliberately not in scope

insta build --port keeps its own validator, which still rejects 0. Widening it means deciding what the report's port check says for a worker (today a missing port is a fail with "a port mismatch is the #1 deploy mistake"), and build.test.ts asserts the current rejection under an explicit rationale: a verifier must not bless bad input. That is a separate change with its own report semantics, not a side effect of this one. Happy to follow up if you want it.

The issue also asks whether --port 0 at create should imply endpoint.mode=none. I left that alone: the platform already converges port: 0 to none on deploy, and I cannot verify a create-time field against the control plane from here. Sending an unverified extra key seemed worse than sending the port the platform already understands.

Testing

npm run typecheck && npm test: green (813 passing, 6 skipped), except for one pre-existing unrelated failure on my machine, github-source.test.ts > refuses a manifest that resolves outside the clone through a symlink, which fails identically on unmodified main here because creating a symlink on Windows needs Developer Mode or elevation (EPERM). CI's windows-latest runner is privileged, so it passes there.

Six new tests, all negative-controlled (each fails against the current parsePort, verified by stashing the source change and re-running):

  • parsePort accepts 0 and " 0 "
  • parsePort names 0 and the rejected value in the error
  • servicesAddRequestBody sends port: 0 rather than dropping it as falsy
  • servicesAdd rejects --port 0 on a non-compute type, before any config or network access
  • sourceBody (connect-repo) honours --port 0 over the detected port
  • resolveServiceArgs lets --port 0 through the pre-flight check and does not ask for the port again

Manually, against a dead API URL so the local validators are what answer:

$ insta services add postgres db --port 0
error: --port is only valid for compute services
$ insta services add compute wk --port 70000
error: port must be 0 (worker: no HTTP endpoint) or an integer between 1 and 65535, got: 70000
$ insta services add compute wk --port 0
error: agent mode requires a linked project ...     # clears validation, fails at project resolution

Follow-up outside this repo

Per CONTRIBUTING, insta/cli-reference.md in InsForge/insta-skills needs the same edit, since that file is how agents learn the CLI surface. I do not have a change set there; the wording that matches this PR is:

--port <n> compute only: port the image listens on (default 8080). 0 creates a worker: a long-running process with no HTTP endpoint, and no URL.

and, for compute connect-repo, 0 for a worker with no HTTP endpoint.


Summary by cubic

Allows --port 0 on service creation to express the worker shape (no HTTP endpoint), which the platform and insta deploy already supported. Previously, insta services add compute <name> --port 0 was rejected by validation, forcing workers to be created at another port and converged later.

  • parsePort now accepts 0 and names it in the error message.
  • --port checks in servicesAdd and servicesAddRequestBody now use presence instead of truthiness, so 0 is not dropped.
  • compute connect-repo --port 0 also works because it shares the parser.
  • Help text and interactive prompts mention the worker shape.
  • insta build --port is untouched: it has its own validator and a separate rationale.