@getpaseo/server 0.1.88 → 0.1.89

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.
Files changed (60) hide show
  1. package/dist/server/server/agent/agent-manager.js +4 -1
  2. package/dist/server/server/agent/agent-storage.d.ts +22 -22
  3. package/dist/server/server/agent/create-agent/create.d.ts +2 -0
  4. package/dist/server/server/agent/create-agent/create.js +16 -5
  5. package/dist/server/server/agent/create-agent-lifecycle-dispatch.d.ts +1 -0
  6. package/dist/server/server/agent/create-agent-lifecycle-dispatch.js +4 -0
  7. package/dist/server/server/agent/mcp-server.d.ts +1 -0
  8. package/dist/server/server/agent/mcp-server.js +113 -70
  9. package/dist/server/server/agent/providers/pi/agent.js +13 -0
  10. package/dist/server/server/agent/providers/pi/rpc-types.d.ts +3 -0
  11. package/dist/server/server/auto-archive-on-merge/archive-if-safe.d.ts +1 -0
  12. package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +6 -1
  13. package/dist/server/server/bootstrap.d.ts +7 -2
  14. package/dist/server/server/bootstrap.js +152 -115
  15. package/dist/server/server/config.js +41 -0
  16. package/dist/server/server/loop-service.d.ts +22 -22
  17. package/dist/server/server/package-version.d.ts +2 -2
  18. package/dist/server/server/paseo-worktree-archive-service.d.ts +2 -0
  19. package/dist/server/server/paseo-worktree-archive-service.js +28 -9
  20. package/dist/server/server/persisted-config.d.ts +84 -28
  21. package/dist/server/server/persisted-config.js +17 -0
  22. package/dist/server/server/pid-lock.d.ts +2 -2
  23. package/dist/server/server/script-health-monitor.d.ts +4 -4
  24. package/dist/server/server/script-health-monitor.js +6 -6
  25. package/dist/server/server/script-proxy.d.ts +2 -39
  26. package/dist/server/server/script-proxy.js +1 -244
  27. package/dist/server/server/script-route-branch-handler.d.ts +2 -2
  28. package/dist/server/server/script-route-branch-handler.js +3 -37
  29. package/dist/server/server/script-status-projection.d.ts +6 -4
  30. package/dist/server/server/script-status-projection.js +85 -37
  31. package/dist/server/server/service-proxy.d.ts +237 -0
  32. package/dist/server/server/service-proxy.js +714 -0
  33. package/dist/server/server/session.d.ts +7 -3
  34. package/dist/server/server/session.js +22 -10
  35. package/dist/server/server/websocket-server.d.ts +7 -4
  36. package/dist/server/server/websocket-server.js +9 -4
  37. package/dist/server/server/workspace-directory.js +4 -0
  38. package/dist/server/server/workspace-git-service.d.ts +3 -0
  39. package/dist/server/server/workspace-git-service.js +53 -12
  40. package/dist/server/server/workspace-registry.d.ts +2 -2
  41. package/dist/server/server/workspace-service-env.d.ts +1 -0
  42. package/dist/server/server/workspace-service-env.js +23 -18
  43. package/dist/server/server/worktree/commands.d.ts +2 -0
  44. package/dist/server/server/worktree/commands.js +4 -1
  45. package/dist/server/server/worktree-bootstrap.d.ts +4 -3
  46. package/dist/server/server/worktree-bootstrap.js +14 -13
  47. package/dist/server/server/worktree-core.d.ts +1 -0
  48. package/dist/server/server/worktree-core.js +2 -0
  49. package/dist/server/server/worktree-session.d.ts +6 -2
  50. package/dist/server/server/worktree-session.js +3 -0
  51. package/dist/server/services/github-service.d.ts +1 -0
  52. package/dist/server/services/github-service.js +7 -1
  53. package/dist/server/utils/checkout-git.d.ts +6 -2
  54. package/dist/server/utils/checkout-git.js +17 -7
  55. package/dist/server/utils/worktree.d.ts +17 -12
  56. package/dist/server/utils/worktree.js +39 -22
  57. package/dist/src/server/persisted-config.js +17 -0
  58. package/package.json +5 -5
  59. package/dist/server/utils/script-hostname.d.ts +0 -8
  60. package/dist/server/utils/script-hostname.js +0 -14
@@ -1,245 +1,2 @@
1
- import http from "node:http";
2
- import net from "node:net";
3
- // ---------------------------------------------------------------------------
4
- // Hop-by-hop headers that must not be forwarded
5
- // ---------------------------------------------------------------------------
6
- const HOP_BY_HOP_HEADERS = new Set([
7
- "connection",
8
- "transfer-encoding",
9
- "keep-alive",
10
- "upgrade",
11
- "proxy-connection",
12
- "proxy-authenticate",
13
- "proxy-authorization",
14
- "te",
15
- "trailer",
16
- ]);
17
- export class ScriptRouteStore {
18
- constructor() {
19
- this.routes = new Map();
20
- this.workspaceHostnames = new Map();
21
- }
22
- registerRoute(entry) {
23
- const previous = this.routes.get(entry.hostname);
24
- if (previous) {
25
- this.removeHostnameFromWorkspaceIndex(previous.workspaceId, previous.hostname);
26
- }
27
- const storedEntry = { ...entry };
28
- this.routes.set(storedEntry.hostname, storedEntry);
29
- this.addHostnameToWorkspaceIndex(storedEntry.workspaceId, storedEntry.hostname);
30
- }
31
- removeRoute(hostname) {
32
- const entry = this.routes.get(hostname);
33
- if (!entry) {
34
- return;
35
- }
36
- this.routes.delete(hostname);
37
- this.removeHostnameFromWorkspaceIndex(entry.workspaceId, hostname);
38
- }
39
- removeRouteForWorkspaceScript(params) {
40
- const routes = this.listRoutesForWorkspace(params.workspaceId);
41
- const route = routes.find((entry) => entry.scriptName === params.scriptName);
42
- if (!route) {
43
- return;
44
- }
45
- this.removeRoute(route.hostname);
46
- }
47
- removeRoutesForPort(port) {
48
- for (const [hostname, entry] of this.routes) {
49
- if (entry.port === port) {
50
- this.routes.delete(hostname);
51
- this.removeHostnameFromWorkspaceIndex(entry.workspaceId, hostname);
52
- }
53
- }
54
- }
55
- findRoute(host) {
56
- // Strip port suffix from the Host header value
57
- const hostname = host.replace(/:\d+$/, "");
58
- // 1. Exact match
59
- const exactRoute = this.routes.get(hostname);
60
- if (exactRoute !== undefined) {
61
- return { hostname: exactRoute.hostname, port: exactRoute.port };
62
- }
63
- // 2. Subdomain match — walk up the labels looking for a registered parent
64
- const parts = hostname.split(".");
65
- for (let i = 1; i < parts.length; i++) {
66
- const candidate = parts.slice(i).join(".");
67
- const candidateRoute = this.routes.get(candidate);
68
- if (candidateRoute !== undefined) {
69
- return { hostname: candidateRoute.hostname, port: candidateRoute.port };
70
- }
71
- }
72
- return null;
73
- }
74
- getRouteEntry(hostname) {
75
- const entry = this.routes.get(hostname);
76
- return entry ? { ...entry } : null;
77
- }
78
- listRoutes() {
79
- return Array.from(this.routes.values()).map((entry) => Object.assign({}, entry));
80
- }
81
- listRoutesForWorkspace(workspaceId) {
82
- const hostnames = this.workspaceHostnames.get(workspaceId);
83
- if (!hostnames) {
84
- return [];
85
- }
86
- const routes = [];
87
- for (const hostname of hostnames) {
88
- const entry = this.routes.get(hostname);
89
- if (entry) {
90
- routes.push({ ...entry });
91
- }
92
- }
93
- return routes;
94
- }
95
- addHostnameToWorkspaceIndex(workspaceId, hostname) {
96
- const hostnames = this.workspaceHostnames.get(workspaceId) ?? new Set();
97
- hostnames.add(hostname);
98
- this.workspaceHostnames.set(workspaceId, hostnames);
99
- }
100
- removeHostnameFromWorkspaceIndex(workspaceId, hostname) {
101
- const hostnames = this.workspaceHostnames.get(workspaceId);
102
- if (!hostnames) {
103
- return;
104
- }
105
- hostnames.delete(hostname);
106
- if (hostnames.size === 0) {
107
- this.workspaceHostnames.delete(workspaceId);
108
- }
109
- }
110
- }
111
- // ---------------------------------------------------------------------------
112
- // Helpers
113
- // ---------------------------------------------------------------------------
114
- function stripHopByHopHeaders(rawHeaders) {
115
- const out = {};
116
- for (const [key, value] of Object.entries(rawHeaders)) {
117
- if (value === undefined)
118
- continue;
119
- if (HOP_BY_HOP_HEADERS.has(key.toLowerCase()))
120
- continue;
121
- out[key] = value;
122
- }
123
- return out;
124
- }
125
- // ---------------------------------------------------------------------------
126
- // createScriptProxyMiddleware
127
- // ---------------------------------------------------------------------------
128
- export function createScriptProxyMiddleware({ routeStore, logger, }) {
129
- return (req, res, next) => {
130
- const hostHeader = req.headers.host;
131
- if (!hostHeader) {
132
- next();
133
- return;
134
- }
135
- const route = routeStore.findRoute(hostHeader);
136
- if (!route) {
137
- next();
138
- return;
139
- }
140
- const forwardedHeaders = stripHopByHopHeaders(req.headers);
141
- forwardedHeaders["x-forwarded-for"] = req.socket.remoteAddress ?? "127.0.0.1";
142
- forwardedHeaders["x-forwarded-host"] = hostHeader.replace(/:\d+$/, "");
143
- forwardedHeaders["x-forwarded-proto"] = req.protocol;
144
- const proxyReq = http.request({
145
- hostname: "127.0.0.1",
146
- port: route.port,
147
- path: req.originalUrl,
148
- method: req.method,
149
- headers: forwardedHeaders,
150
- }, (proxyRes) => {
151
- const responseHeaders = stripHopByHopHeaders(proxyRes.headers);
152
- res.writeHead(proxyRes.statusCode ?? 502, responseHeaders);
153
- proxyRes.pipe(res, { end: true });
154
- });
155
- proxyReq.on("error", (err) => {
156
- logger.warn({ err, hostname: route.hostname, port: route.port }, "Script proxy: upstream unreachable");
157
- if (!res.headersSent) {
158
- res.writeHead(502, { "content-type": "text/plain" });
159
- res.end("502 Bad Gateway");
160
- }
161
- });
162
- req.pipe(proxyReq, { end: true });
163
- };
164
- }
165
- // ---------------------------------------------------------------------------
166
- // createScriptProxyUpgradeHandler
167
- // ---------------------------------------------------------------------------
168
- export function createScriptProxyUpgradeHandler({ routeStore, logger, }) {
169
- return (req, socket, head) => {
170
- const hostHeader = req.headers.host;
171
- if (!hostHeader) {
172
- return;
173
- }
174
- const route = routeStore.findRoute(hostHeader);
175
- if (!route) {
176
- return;
177
- }
178
- const targetSocket = net.connect({ host: "127.0.0.1", port: route.port }, () => {
179
- // Reconstruct the raw HTTP upgrade request to send to the target
180
- const forwardedHeaders = stripHopByHopHeaders(req.headers);
181
- forwardedHeaders["x-forwarded-for"] = req.socket.remoteAddress ?? "127.0.0.1";
182
- forwardedHeaders["x-forwarded-host"] = hostHeader.replace(/:\d+$/, "");
183
- forwardedHeaders["x-forwarded-proto"] = "http";
184
- // Re-include upgrade and connection headers — they are required for
185
- // WebSocket handshake even though they are hop-by-hop.
186
- forwardedHeaders["connection"] = "Upgrade";
187
- forwardedHeaders["upgrade"] = req.headers.upgrade ?? "websocket";
188
- const headerLines = [];
189
- headerLines.push(`${req.method ?? "GET"} ${req.url ?? "/"} HTTP/${req.httpVersion}`);
190
- for (const [key, value] of Object.entries(forwardedHeaders)) {
191
- if (Array.isArray(value)) {
192
- for (const v of value) {
193
- headerLines.push(`${key}: ${v}`);
194
- }
195
- }
196
- else {
197
- headerLines.push(`${key}: ${value}`);
198
- }
199
- }
200
- headerLines.push("\r\n");
201
- targetSocket.write(headerLines.join("\r\n"));
202
- if (head.length > 0) {
203
- targetSocket.write(head);
204
- }
205
- // Pipe in both directions
206
- targetSocket.pipe(socket);
207
- socket.pipe(targetSocket);
208
- });
209
- targetSocket.on("error", (err) => {
210
- logger.warn({ err, hostname: route.hostname, port: route.port }, "Script proxy: WebSocket upstream unreachable");
211
- socket.end();
212
- });
213
- socket.on("error", () => {
214
- targetSocket.destroy();
215
- });
216
- };
217
- }
218
- // ---------------------------------------------------------------------------
219
- // findFreePort
220
- // ---------------------------------------------------------------------------
221
- export function findFreePort() {
222
- return new Promise((resolve, reject) => {
223
- const server = net.createServer();
224
- server.unref();
225
- server.listen(0, "127.0.0.1", () => {
226
- const address = server.address();
227
- if (!address || typeof address === "string") {
228
- server.close();
229
- reject(new Error("Failed to get assigned port"));
230
- return;
231
- }
232
- const { port } = address;
233
- server.close((err) => {
234
- if (err) {
235
- reject(err);
236
- }
237
- else {
238
- resolve(port);
239
- }
240
- });
241
- });
242
- server.on("error", reject);
243
- });
244
- }
1
+ export { createScriptProxyMiddleware, createScriptProxyUpgradeHandler, findFreePort, ScriptRouteStore, } from "./service-proxy.js";
245
2
  //# sourceMappingURL=script-proxy.js.map
@@ -1,7 +1,7 @@
1
1
  import type { Logger } from "pino";
2
- import type { ScriptRouteStore } from "./script-proxy.js";
2
+ import type { ServiceProxySubsystem } from "./service-proxy.js";
3
3
  interface BranchChangeRouteHandlerOptions {
4
- routeStore: ScriptRouteStore;
4
+ serviceProxy: ServiceProxySubsystem;
5
5
  onRoutesChanged: (workspaceId: string) => void;
6
6
  logger?: Logger;
7
7
  }
@@ -1,44 +1,10 @@
1
- import { buildScriptHostname } from "../utils/script-hostname.js";
2
1
  export function createBranchChangeRouteHandler(options) {
3
2
  return (workspaceId, _oldBranch, newBranch) => {
4
- // Only service scripts register routes, so branch renames only touch services.
5
- const routes = options.routeStore.listRoutesForWorkspace(workspaceId);
6
- if (routes.length === 0) {
3
+ const changed = options.serviceProxy.replaceWorkspaceBranchRoutes({ workspaceId, newBranch });
4
+ if (!changed) {
7
5
  return;
8
6
  }
9
- const updates = [];
10
- for (const route of routes) {
11
- const newHostname = buildScriptHostname({
12
- projectSlug: route.projectSlug,
13
- branchName: newBranch,
14
- scriptName: route.scriptName,
15
- });
16
- if (newHostname !== route.hostname) {
17
- updates.push({
18
- oldHostname: route.hostname,
19
- newHostname,
20
- route,
21
- });
22
- }
23
- }
24
- if (updates.length === 0) {
25
- return;
26
- }
27
- for (const { oldHostname, newHostname, route } of updates) {
28
- options.routeStore.removeRoute(oldHostname);
29
- options.routeStore.registerRoute({
30
- hostname: newHostname,
31
- port: route.port,
32
- workspaceId: route.workspaceId,
33
- projectSlug: route.projectSlug,
34
- scriptName: route.scriptName,
35
- });
36
- options.logger?.info({
37
- oldHostname,
38
- newHostname,
39
- scriptName: route.scriptName,
40
- }, "Updated script route for branch rename");
41
- }
7
+ options.logger?.info({ workspaceId, newBranch }, "Updated service proxy routes for branch rename");
42
8
  options.onRoutesChanged(workspaceId);
43
9
  };
44
10
  }
@@ -2,7 +2,7 @@ import type { Logger } from "pino";
2
2
  import type { SessionOutboundMessage, WorkspaceScriptPayload } from "@getpaseo/protocol/messages";
3
3
  import type { PaseoConfig } from "@getpaseo/protocol/paseo-config-schema";
4
4
  import type { ScriptHealthEntry, ScriptHealthState } from "./script-health-monitor.js";
5
- import type { ScriptRouteStore } from "./script-proxy.js";
5
+ import type { ServiceProxySubsystem } from "./service-proxy.js";
6
6
  import type { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
7
7
  interface SessionEmitter {
8
8
  emit(message: SessionOutboundMessage): void;
@@ -11,9 +11,10 @@ interface BuildWorkspaceScriptPayloadsOptions {
11
11
  workspaceId: string;
12
12
  workspaceDirectory: string;
13
13
  paseoConfig: PaseoConfig | null;
14
- routeStore: ScriptRouteStore;
14
+ serviceProxy: ServiceProxySubsystem;
15
15
  runtimeStore: WorkspaceScriptRuntimeStore;
16
16
  daemonPort: number | null;
17
+ serviceProxyPublicBaseUrl?: string | null;
17
18
  gitMetadata?: {
18
19
  projectSlug: string;
19
20
  currentBranch: string | null;
@@ -22,11 +23,12 @@ interface BuildWorkspaceScriptPayloadsOptions {
22
23
  }
23
24
  export declare function readPaseoConfigForProjection(workspaceDirectory: string, logger: Logger): PaseoConfig | null;
24
25
  export declare function buildWorkspaceScriptPayloads(options: BuildWorkspaceScriptPayloadsOptions): WorkspaceScriptPayload[];
25
- export declare function createScriptStatusEmitter({ sessions, routeStore, runtimeStore, daemonPort, resolveWorkspaceDirectory, logger, }: {
26
+ export declare function createScriptStatusEmitter({ sessions, serviceProxy, runtimeStore, daemonPort, serviceProxyPublicBaseUrl, resolveWorkspaceDirectory, logger, }: {
26
27
  sessions: () => SessionEmitter[];
27
- routeStore: ScriptRouteStore;
28
+ serviceProxy: ServiceProxySubsystem;
28
29
  runtimeStore: WorkspaceScriptRuntimeStore;
29
30
  daemonPort: number | null | (() => number | null);
31
+ serviceProxyPublicBaseUrl?: string | null;
30
32
  resolveWorkspaceDirectory: (workspaceId: string) => string | null | Promise<string | null>;
31
33
  logger: Logger;
32
34
  }): (workspaceId: string, scripts: ScriptHealthEntry[]) => void;
@@ -1,4 +1,3 @@
1
- import { buildScriptHostname } from "../utils/script-hostname.js";
2
1
  import { getScriptConfigs, isServiceScript, readPaseoConfig } from "../utils/worktree.js";
3
2
  import { deriveProjectSlug } from "./workspace-git-metadata.js";
4
3
  export function readPaseoConfigForProjection(workspaceDirectory, logger) {
@@ -15,12 +14,6 @@ function resolveDaemonPort(daemonPort) {
15
14
  }
16
15
  return daemonPort;
17
16
  }
18
- function toServiceProxyUrl(hostname, daemonPort) {
19
- if (daemonPort === null) {
20
- return null;
21
- }
22
- return `http://${hostname}:${daemonPort}`;
23
- }
24
17
  function toWireHealth(health) {
25
18
  if (health === "pending" || health === null) {
26
19
  return null;
@@ -33,48 +26,99 @@ function sortPayloads(payloads) {
33
26
  sensitivity: "base",
34
27
  }));
35
28
  }
36
- function buildConfiguredScriptPayload(scriptName, config, runtimeEntry, routeEntry, ctx) {
29
+ function projectWorkspaceServiceState(params) {
30
+ return params.ctx.serviceProxy.projectWorkspaceServiceState({
31
+ workspaceId: params.workspaceId,
32
+ projectSlug: params.ctx.projectSlug,
33
+ branchName: params.ctx.branchName,
34
+ scriptName: params.scriptName,
35
+ daemonPort: params.ctx.daemonPort,
36
+ publicBaseUrl: params.ctx.serviceProxyPublicBaseUrl,
37
+ });
38
+ }
39
+ function buildConfiguredPlainScriptPayload(scriptName, runtimeEntry) {
40
+ return {
41
+ scriptName,
42
+ type: "script",
43
+ hostname: scriptName,
44
+ port: null,
45
+ proxyUrl: null,
46
+ lifecycle: runtimeEntry?.lifecycle ?? "stopped",
47
+ health: null,
48
+ exitCode: runtimeEntry?.exitCode ?? null,
49
+ terminalId: runtimeEntry?.terminalId ?? null,
50
+ };
51
+ }
52
+ function buildConfiguredScriptPayload(scriptName, config, runtimeEntry, serviceState, ctx) {
37
53
  const configIsService = isServiceScript(config);
38
- const type = configIsService ? "service" : "script";
39
- const configuredPort = configIsService ? (config.port ?? null) : null;
40
- const hostname = type === "service"
41
- ? (routeEntry?.hostname ??
42
- buildScriptHostname({
43
- projectSlug: ctx.projectSlug,
44
- branchName: ctx.branchName,
45
- scriptName,
46
- }))
47
- : scriptName;
54
+ if (!configIsService) {
55
+ return buildConfiguredPlainScriptPayload(scriptName, runtimeEntry);
56
+ }
57
+ const type = "service";
58
+ const configuredPort = config.port ?? null;
59
+ const hostname = (serviceState ??
60
+ ctx.serviceProxy.projectWorkspaceService({
61
+ projectSlug: ctx.projectSlug,
62
+ branchName: ctx.branchName,
63
+ scriptName,
64
+ daemonPort: ctx.daemonPort,
65
+ publicBaseUrl: ctx.serviceProxyPublicBaseUrl,
66
+ })).hostname;
67
+ const urls = serviceState ??
68
+ ctx.serviceProxy.projectUrls({
69
+ projectSlug: ctx.projectSlug,
70
+ branchName: ctx.branchName,
71
+ scriptName,
72
+ daemonPort: ctx.daemonPort,
73
+ publicBaseUrl: ctx.serviceProxyPublicBaseUrl,
74
+ });
48
75
  return {
49
76
  scriptName,
50
77
  type,
51
78
  hostname,
52
- port: type === "service" ? (routeEntry?.port ?? configuredPort) : null,
53
- proxyUrl: type === "service" ? toServiceProxyUrl(hostname, ctx.daemonPort) : null,
79
+ port: serviceState?.port ?? configuredPort,
80
+ localProxyUrl: urls.localProxyUrl,
81
+ publicProxyUrl: urls.publicProxyUrl,
82
+ proxyUrl: urls.proxyUrl,
54
83
  lifecycle: runtimeEntry?.lifecycle ?? "stopped",
55
- health: type === "service" ? toWireHealth(ctx.resolveHealth?.(hostname) ?? null) : null,
84
+ health: toWireHealth(ctx.resolveHealth?.(hostname) ?? null),
56
85
  exitCode: runtimeEntry?.exitCode ?? null,
57
86
  terminalId: runtimeEntry?.terminalId ?? null,
58
87
  };
59
88
  }
60
- function buildOrphanRuntimePayload(runtimeEntry, routeEntry, ctx) {
89
+ function buildOrphanRuntimePayload(runtimeEntry, serviceState, ctx) {
61
90
  const type = runtimeEntry.type;
62
91
  const hostname = type === "service"
63
- ? (routeEntry?.hostname ??
64
- buildScriptHostname({
92
+ ? (serviceState ??
93
+ ctx.serviceProxy.projectWorkspaceService({
65
94
  projectSlug: ctx.projectSlug,
66
95
  branchName: ctx.branchName,
67
96
  scriptName: runtimeEntry.scriptName,
68
- }))
97
+ daemonPort: ctx.daemonPort,
98
+ publicBaseUrl: ctx.serviceProxyPublicBaseUrl,
99
+ })).hostname
69
100
  : runtimeEntry.scriptName;
101
+ const urls = serviceState ??
102
+ ctx.serviceProxy.projectUrls({
103
+ projectSlug: ctx.projectSlug,
104
+ branchName: ctx.branchName,
105
+ scriptName: runtimeEntry.scriptName,
106
+ daemonPort: ctx.daemonPort,
107
+ publicBaseUrl: ctx.serviceProxyPublicBaseUrl,
108
+ });
70
109
  return {
71
110
  scriptName: runtimeEntry.scriptName,
72
111
  type,
73
112
  hostname,
74
- port: type === "service" ? (routeEntry?.port ?? null) : null,
75
- proxyUrl: type === "service" ? toServiceProxyUrl(hostname, ctx.daemonPort) : null,
113
+ port: type === "service" ? (serviceState?.port ?? null) : null,
114
+ ...(type === "service"
115
+ ? { localProxyUrl: urls.localProxyUrl, publicProxyUrl: urls.publicProxyUrl }
116
+ : {}),
117
+ proxyUrl: type === "service" ? urls.proxyUrl : null,
76
118
  lifecycle: runtimeEntry.lifecycle,
77
- health: type === "service" && routeEntry ? toWireHealth(ctx.resolveHealth?.(hostname) ?? null) : null,
119
+ health: type === "service" && serviceState?.port !== null
120
+ ? toWireHealth(ctx.resolveHealth?.(hostname) ?? null)
121
+ : null,
78
122
  exitCode: runtimeEntry.exitCode,
79
123
  terminalId: runtimeEntry.terminalId,
80
124
  };
@@ -88,27 +132,30 @@ export function buildWorkspaceScriptPayloads(options) {
88
132
  const runtimeEntries = new Map(options.runtimeStore
89
133
  .listForWorkspace(workspaceId)
90
134
  .map((entry) => [entry.scriptName, entry]));
91
- const routesByScriptName = new Map(options.routeStore
92
- .listRoutesForWorkspace(workspaceId)
93
- .map((entry) => [entry.scriptName, entry]));
94
135
  const ctx = {
95
136
  projectSlug,
96
137
  branchName,
97
138
  daemonPort: options.daemonPort,
139
+ serviceProxyPublicBaseUrl: options.serviceProxyPublicBaseUrl,
140
+ serviceProxy: options.serviceProxy,
98
141
  resolveHealth: options.resolveHealth,
99
142
  };
100
143
  const payloads = [];
101
144
  for (const [scriptName, config] of scriptConfigs.entries()) {
102
145
  const runtimeEntry = runtimeEntries.get(scriptName) ?? null;
103
- const routeEntry = routesByScriptName.get(scriptName) ?? null;
104
- payloads.push(buildConfiguredScriptPayload(scriptName, config, runtimeEntry, routeEntry, ctx));
146
+ const serviceState = isServiceScript(config)
147
+ ? projectWorkspaceServiceState({ workspaceId, scriptName, ctx })
148
+ : null;
149
+ payloads.push(buildConfiguredScriptPayload(scriptName, config, runtimeEntry, serviceState, ctx));
105
150
  }
106
151
  for (const runtimeEntry of runtimeEntries.values()) {
107
152
  if (scriptConfigs.has(runtimeEntry.scriptName) || runtimeEntry.lifecycle !== "running") {
108
153
  continue;
109
154
  }
110
- const routeEntry = routesByScriptName.get(runtimeEntry.scriptName) ?? null;
111
- payloads.push(buildOrphanRuntimePayload(runtimeEntry, routeEntry, ctx));
155
+ const serviceState = runtimeEntry.type === "service"
156
+ ? projectWorkspaceServiceState({ workspaceId, scriptName: runtimeEntry.scriptName, ctx })
157
+ : null;
158
+ payloads.push(buildOrphanRuntimePayload(runtimeEntry, serviceState, ctx));
112
159
  }
113
160
  return sortPayloads(payloads);
114
161
  }
@@ -121,7 +168,7 @@ function buildScriptStatusUpdateMessage(params) {
121
168
  },
122
169
  };
123
170
  }
124
- export function createScriptStatusEmitter({ sessions, routeStore, runtimeStore, daemonPort, resolveWorkspaceDirectory, logger, }) {
171
+ export function createScriptStatusEmitter({ sessions, serviceProxy, runtimeStore, daemonPort, serviceProxyPublicBaseUrl, resolveWorkspaceDirectory, logger, }) {
125
172
  return (workspaceId, scripts) => {
126
173
  void (async () => {
127
174
  const workspaceDirectory = await resolveWorkspaceDirectory(workspaceId);
@@ -134,9 +181,10 @@ export function createScriptStatusEmitter({ sessions, routeStore, runtimeStore,
134
181
  workspaceId,
135
182
  workspaceDirectory,
136
183
  paseoConfig: readPaseoConfigForProjection(workspaceDirectory, logger),
137
- routeStore,
184
+ serviceProxy,
138
185
  runtimeStore,
139
186
  daemonPort: resolvedDaemonPort,
187
+ serviceProxyPublicBaseUrl,
140
188
  resolveHealth: (hostname) => scriptHealthByHostname.get(hostname) ?? null,
141
189
  });
142
190
  const message = buildScriptStatusUpdateMessage({