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
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
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:
Repoint the entry so it describes the app's real contents — the game and its documentation/marketing UI — instead of the retired tool.
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
Removing the app registration entirely — the opposite is intended; it stays as the publish target.
Anything inside the game repo (its PM2 config, its process removal) — that is atomantic/ElsewhereAcres#56.
PM2 supervision semantics for GUI processes. If PM2 is a poor fit for a windowed binary, a recorded start command PortOS can invoke is an acceptable first step.
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.appIdresolves throughgetAppById()to an app whoserepoPathis 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:
Context
The entry currently points at the process being deleted
The managed-app entry for the game repo has
uiPort,apiPort, andtlsPortall set to the sprite-manager debugger's port, lists both PM2 processes inpm2ProcessNames/processes, and carriestype: 'express'. ItsdevUiPortis the marketing UI's port.atomantic/ElsewhereAcres#56removes 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:
repoPathunchanged — it is the asset publish target and must keep resolving. Publishing depends only onrepoPathbeing a real directory, never on a running process, so it is unaffected by the process churn.pm2ProcessNamesandprocesses.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/gamewrapper (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:
typeis a free-formz.string().optional().default('express')inserver/lib/validation.js, andapps.jsnormalizesuiPort: 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-712collectsuiPort/devUiPort/apiPort/tlsPortacross 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:undefined.collectReservedPorts-style logic doesn't addnull/undefinedto the reserved set.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
runverb that builds the project, imports assets, thenexecs 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:
autorestartmust be OFF. Every existing managed process in that repo usesautorestart: 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.runpath 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.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 adesktop-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 withatomantic/ElsewhereAcres#56so 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.
repoPath, with no dependency on registered processes.Acceptance criteria
repoPathunchanged, game added as a portless process.repoPath.publishBindingtargets it, with their destination paths.Out of scope
atomantic/ElsewhereAcres#56.