Skip to content

Run managed desktop apps from PortOS: a working Start button for portless apps, and repoint the game app entry off the retired sprite-manager process #2991

Description

@atomantic

Problem / Goal

PortOS is becoming the sprite manager and creative source for managed apps, publishing compiled assets into their repos. The app registry is the targeting mechanism for that: a sprite record's publishBinding.appId resolves through getAppById() to an app whose repoPath is the destination repo (server/services/sprites/publish.js, requireAppRepo).

That relationship works today, but the registry entry for the game repo currently describes it in terms of a tool that is being retired — its sole in-repo sprite manager. Two things need to change:

  1. Repoint the entry so it describes the app's real contents — the game and its documentation/marketing UI — instead of the retired tool.
  2. Let a desktop/GUI app be a first-class managed process, since the game is a binary with no HTTP port and the registry's shape assumes port-bearing web processes.

Context

The entry currently points at the process being deleted

The managed-app entry for the game repo has uiPort, apiPort, and tlsPort all set to the sprite-manager debugger's port, lists both PM2 processes in pm2ProcessNames / processes, and carries type: 'express'. Its devUiPort is the marketing UI's port.

atomantic/ElsewhereAcres#56 removes that sprite-manager process and frees its two ports. If the registry isn't repointed in the same change, the registered app keeps advertising a primary UI on a port nothing listens on — a dead "Open" link and a permanently-unhealthy process.

The correct end state for that entry:

  • repoPath unchanged — it is the asset publish target and must keep resolving. Publishing depends only on repoPath being a real directory, never on a running process, so it is unaffected by the process churn.
  • Primary UI repointed to the documentation/marketing UI's port, which becomes the app's face.
  • Sprite-manager process dropped from pm2ProcessNames and processes.
  • The game added as a managed process.

Why the game doesn't fit cleanly today

The other managed processes are web servers fronted by a TLS wrapper. The game is a Godot desktop binary started through the repo's own ./scripts/game wrapper (which bootstraps a vendored .NET SDK and Godot before building or running). It has no HTTP port and no health endpoint.

The stored shape already tolerates that much: type is a free-form z.string().optional().default('express') in server/lib/validation.js, and apps.js normalizes uiPort: appData.uiPort || null. So a portless entry can be stored. What is unverified is whether the surrounding surfaces degrade gracefully — status/health display, the "Open UI" affordance, port-conflict reservation (apps.js:694-712 collects uiPort/devUiPort/apiPort/tlsPort across apps), and start/stop actions — when a process has no port at all. That is the actual work here.

Proposed approach

1. Support a portless / desktop process type.

Give processes an explicit kind so a non-HTTP app is described rather than faked. Reuse the existing free-form type (e.g. desktop) rather than adding a parallel field, and make the port-dependent surfaces branch on "has a port" instead of assuming one:

  • Status/health: report process up/down from the supervisor only; do not attempt an HTTP probe or render an unreachable state as unhealthy.
  • The "Open UI" affordance: hide it (or offer "Launch") when there is no port, rather than linking to undefined.
  • Port reservation: skip null ports cleanly — verify collectReservedPorts-style logic doesn't add null/undefined to the reserved set.
  • Start/stop: drive the app's own start command (startCommands) for a desktop app instead of assuming a PM2-wrapped server.

1b. The Start button must actually launch the game.

This is the headline capability, not a side effect: pressing Start on the managed app in PortOS should launch the game window, and Stop should close it — the same affordance the port-bearing managed apps already have. The game repo's wrapper exposes a run verb that builds the project, imports assets, then execs the engine in the foreground — a long-lived foreground process, which is the shape a supervisor wants. A pre-exported binary can be launched instead for a faster start.

Four behaviors this needs that a web app never does:

  • autorestart must be OFF. Every existing managed process in that repo uses autorestart: true, max_restarts: 10. For a game, the user closing the window is a normal exit — autorestart would relaunch it in an endless loop. A desktop-kind process must default to no autorestart.
  • A clean exit is "stopped", not "crashed". Quitting the game exits 0; status must render that as stopped, not errored, and must not fire failure notifications.
  • Start is slow and must show progress. The run path compiles and imports assets before a window appears — tens of seconds or more, versus a web server's near-instant bind. The button must not read as hung; surface the build output (the app log stream) or offer launching a pre-exported build for a fast path.
  • Single instance. Starting an already-running game must not open a second window; the Start affordance reflects live process state.

Verify the GUI-session assumption rather than assuming it. A supervisor can launch a windowed macOS app only when its daemon runs inside the user's login session; a system-level daemon gets no window server. Confirm which applies here before committing to the supervisor path — if it can't produce a window, fall back to PortOS invoking the start command directly.

2. Repoint the game repo's registry entry.

Drop the retired sprite-manager process, move the primary UI to the documentation/marketing UI's port, keep repoPath, and add the game as a desktop-kind process with a ./scripts/game-based start command. This is a data change to the app record, not code — do it through the app-edit UI once (1) makes the shape expressible. It must land together with atomantic/ElsewhereAcres#56 so the entry never points at a removed process.

3. Make the publish-target relationship legible in the UI.

The sprite record's publish binding names an app, but from the app side nothing indicates that PortOS publishes assets into it. Surface it: on the app's detail view, list the sprite records bound to it (reverse lookup over publishBinding.appId) and their destination paths. This is what makes "PortOS is the creative source for this app" visible rather than a hidden per-record setting.

4. Tests.

  • An app with a portless process round-trips through create/update without a port ever reaching the reserved-port set.
  • Status for a portless process reflects supervisor state and does not attempt (or fail on) an HTTP probe.
  • The "Open UI" affordance is absent for a portless app and present when a port exists.
  • The reverse lookup lists exactly the sprite records bound to a given app, and is empty for an app with none.
  • Publishing continues to resolve purely from repoPath, with no dependency on registered processes.

Acceptance criteria

  • A managed app can register a process with no HTTP port, and that process's status, start/stop, and display degrade gracefully rather than rendering as broken or unhealthy.
  • Pressing Start on the game in PortOS launches the game window; Stop closes it.
  • A desktop-kind process defaults to no autorestart, so closing the game does not relaunch it.
  • A clean exit (the user quitting) renders as stopped, not errored, and raises no failure notification.
  • Start surfaces progress while the game builds/imports, so a slow launch never reads as hung.
  • Starting an already-running game does not spawn a second instance.
  • Null/absent ports never enter the cross-app reserved-port set.
  • The "Open UI" affordance is hidden (or replaced by a launch action) for a portless app.
  • The game repo's registry entry is repointed: sprite-manager process removed, primary UI on the documentation/marketing UI's port, repoPath unchanged, game added as a portless process.
  • Asset publishing to that app still resolves after the repoint, proving it depends only on repoPath.
  • An app's detail view lists the sprite records whose publishBinding targets it, with their destination paths.
  • Tests above pass.

Out of scope

Metadata

Metadata

Assignees

Labels

area:devtoolsDevtools/workspace/code-review surfacesblockedBlocked on another issue/PR — skip in autonomous claim runsenhancementNew feature or requestin-progressClaimed and being workedplanTracked by /do:replan

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions