portless-studio 0.1.3 → 0.1.5
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.
- package/README.md +1 -1
- package/dist/cli/mcp/studio-tools.js +36 -2
- package/dist/cli/studio-items.js +2 -0
- package/dist/core/api/contract.js +6 -1
- package/dist/core/config/studio-config.js +83 -0
- package/dist/core/terminals/terminal-history.js +53 -0
- package/dist/core/terminals/terminal.js +0 -4
- package/dist/server/application/port-discovery.js +1 -0
- package/dist/server/application/stop-process.js +44 -3
- package/dist/server/application/studio-runtime.js +10 -0
- package/dist/server/application/terminals.js +267 -53
- package/dist/server/infrastructure/config/json-terminal-history-store.js +30 -0
- package/dist/server/infrastructure/system/dev-process-tree.js +70 -0
- package/dist/server/infrastructure/system/linux-ports.js +31 -1
- package/dist/server/infrastructure/system/macos-ports.js +31 -2
- package/dist/server/infrastructure/system/port-closed.js +15 -0
- package/dist/server/infrastructure/system/system-port-scanner.js +7 -4
- package/dist/server/infrastructure/system/system-process-control.js +44 -0
- package/dist/server/infrastructure/system/windows-ports.js +9 -1
- package/dist/server/interface/http/http-server.js +5 -1
- package/dist/server/interface/http/static-files.js +6 -2
- package/dist/server/main.js +14 -2
- package/dist/web/assets/index-DhggsQyU.js +23 -0
- package/dist/web/assets/index-Q29k6Jdf.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-9WPxv6pu.js +0 -17
- package/dist/web/assets/index-Bckc4cIr.css +0 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ portless replaces port numbers with names: `portless api pnpm dev` serves your a
|
|
|
16
16
|
with ten dev servers across several projects you have to remember which names exist and which
|
|
17
17
|
ports have none. Studio shows both, live, and names a port in one click.
|
|
18
18
|
|
|
19
|
-

|
|
20
20
|
|
|
21
21
|
The screenshots use the built-in sample data (`?demo`), not a real machine.
|
|
22
22
|
|
|
@@ -423,7 +423,9 @@ function terminalTools(client) {
|
|
|
423
423
|
name: "list_terminals",
|
|
424
424
|
title: "List Studio terminals",
|
|
425
425
|
description: "Lists the terminals Studio runs: id, title, folder, command, project, whether it still " +
|
|
426
|
-
"runs, exit code
|
|
426
|
+
"runs, exit code, the port it named and who started it. Terminals that were running when " +
|
|
427
|
+
"Studio last stopped are listed with stopped_by_restart: true; restart_terminal runs them " +
|
|
428
|
+
"again.",
|
|
427
429
|
inputSchema: NO_INPUT,
|
|
428
430
|
call: async () => {
|
|
429
431
|
const result = await client.readPrivate(API_PATHS.terminals);
|
|
@@ -431,8 +433,12 @@ function terminalTools(client) {
|
|
|
431
433
|
return failure(result.error);
|
|
432
434
|
const { terminals } = result.value;
|
|
433
435
|
const running = terminals.filter((terminal) => terminal.status === "running").length;
|
|
436
|
+
const stopped = terminals.filter((terminal) => terminal.stoppedByRestart === true).length;
|
|
437
|
+
const restorable = stopped > 0
|
|
438
|
+
? `, ${stopped} stopped by a Studio restart: run ${stopped === 1 ? "it" : "them"} again with restart_terminal`
|
|
439
|
+
: "";
|
|
434
440
|
return {
|
|
435
|
-
summary: `${plural(terminals.length, "terminal")} (${running} running).`,
|
|
441
|
+
summary: `${plural(terminals.length, "terminal")} (${running} running${restorable}).`,
|
|
436
442
|
data: terminals.map(terminalItem),
|
|
437
443
|
};
|
|
438
444
|
},
|
|
@@ -556,6 +562,32 @@ function terminalTools(client) {
|
|
|
556
562
|
return { summary: `Stopping ${id.value}. list_terminals shows when it has exited.` };
|
|
557
563
|
},
|
|
558
564
|
},
|
|
565
|
+
{
|
|
566
|
+
name: "restart_terminal",
|
|
567
|
+
title: "Run a terminal's command again",
|
|
568
|
+
description: "Runs a terminal's command again in the same terminal: stops it first when it still runs. " +
|
|
569
|
+
"Use it for a terminal stopped by a Studio restart (stopped_by_restart in list_terminals) " +
|
|
570
|
+
"or one whose command ended.",
|
|
571
|
+
inputSchema: {
|
|
572
|
+
type: "object",
|
|
573
|
+
properties: { id: TERMINAL_ID_SCHEMA },
|
|
574
|
+
required: ["id"],
|
|
575
|
+
additionalProperties: false,
|
|
576
|
+
},
|
|
577
|
+
call: async (args) => {
|
|
578
|
+
const id = readString(args, "id");
|
|
579
|
+
if (!id.ok)
|
|
580
|
+
return invalid(id.error);
|
|
581
|
+
const result = await client.write("POST", API_PATHS.terminalRestart(id.value));
|
|
582
|
+
if (!result.ok)
|
|
583
|
+
return failure(result.error);
|
|
584
|
+
const terminal = result.value;
|
|
585
|
+
return {
|
|
586
|
+
summary: `Running ${terminal.id} again in ${terminal.cwd}: ${terminal.command || "shell"}. Read its output with read_terminal {"id": "${terminal.id}"}.`,
|
|
587
|
+
data: terminalItem(terminal),
|
|
588
|
+
};
|
|
589
|
+
},
|
|
590
|
+
},
|
|
559
591
|
{
|
|
560
592
|
name: "focus_studio",
|
|
561
593
|
title: "Show something in Studio",
|
|
@@ -611,6 +643,8 @@ function terminalItem(terminal) {
|
|
|
611
643
|
pid: terminal.pid,
|
|
612
644
|
named: terminal.naming?.name ?? null,
|
|
613
645
|
port: terminal.naming?.port ?? null,
|
|
646
|
+
started_by: terminal.startedBy ?? null,
|
|
647
|
+
stopped_by_restart: terminal.stoppedByRestart === true,
|
|
614
648
|
};
|
|
615
649
|
}
|
|
616
650
|
async function readSnapshot(client, includeStudio = false) {
|
package/dist/cli/studio-items.js
CHANGED
|
@@ -8,6 +8,7 @@ export function routeItem(route) {
|
|
|
8
8
|
project: route.project?.name ?? null,
|
|
9
9
|
worktree: route.project?.worktree ?? null,
|
|
10
10
|
pid: route.listenerPid ?? route.pid,
|
|
11
|
+
startedAt: route.startedAt ?? null,
|
|
11
12
|
};
|
|
12
13
|
}
|
|
13
14
|
export function detectedItem(port) {
|
|
@@ -19,6 +20,7 @@ export function detectedItem(port) {
|
|
|
19
20
|
suggestedName: port.suggestedName,
|
|
20
21
|
project: port.project?.name ?? null,
|
|
21
22
|
worktree: port.project?.worktree ?? null,
|
|
23
|
+
startedAt: port.startedAt ?? null,
|
|
22
24
|
};
|
|
23
25
|
}
|
|
24
26
|
export function proxyItem(proxy) {
|
|
@@ -51,6 +51,8 @@ export const API_PATHS = {
|
|
|
51
51
|
* Studio shows.
|
|
52
52
|
*/
|
|
53
53
|
terminalsInFolder: "/api/terminals/in-folder",
|
|
54
|
+
/** GET returns `RecentCommandsResponse`: the last commands run in each folder. */
|
|
55
|
+
terminalsRecentCommands: "/api/terminals/recent-commands",
|
|
54
56
|
/**
|
|
55
57
|
* GET returns `TerminalInfo`; PATCH `RenameTerminalRequest` renames it; DELETE stops the command
|
|
56
58
|
* if it runs and closes the terminal.
|
|
@@ -67,7 +69,10 @@ export const API_PATHS = {
|
|
|
67
69
|
terminalInput: (id) => `/api/terminals/${encodeURIComponent(id)}/input`,
|
|
68
70
|
/** POST: Ctrl+C, then SIGTERM, then SIGKILL until the command ends. */
|
|
69
71
|
terminalStop: (id) => `/api/terminals/${encodeURIComponent(id)}/stop`,
|
|
70
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* POST: runs the same command again in the same terminal; also starts a terminal that was
|
|
74
|
+
* stopped by a Studio restart.
|
|
75
|
+
*/
|
|
71
76
|
terminalRestart: (id) => `/api/terminals/${encodeURIComponent(id)}/restart`,
|
|
72
77
|
/** POST `FocusRequest`: brings a Studio page to the front showing a route, port or terminal. */
|
|
73
78
|
focus: "/api/focus",
|
|
@@ -2,6 +2,14 @@ import { DEFAULT_NAME_TEMPLATE, parseNameTemplate } from "../ports/name-template
|
|
|
2
2
|
import { projectIconKind } from "../projects/project-icon.js";
|
|
3
3
|
import { err, ok } from "../result.js";
|
|
4
4
|
import { DEFAULT_VIEW, isItemKey, parseViews } from "../views/view.js";
|
|
5
|
+
export const DEFAULT_TERMINAL_APPEARANCE = {
|
|
6
|
+
fontSize: 12,
|
|
7
|
+
lineHeight: 1.2,
|
|
8
|
+
cursorStyle: "block",
|
|
9
|
+
cursorBlink: true,
|
|
10
|
+
scrollback: 10_000,
|
|
11
|
+
theme: "studio",
|
|
12
|
+
};
|
|
5
13
|
export const DEFAULT_CONFIG = {
|
|
6
14
|
port: 4949,
|
|
7
15
|
autoName: false,
|
|
@@ -39,6 +47,9 @@ export const DEFAULT_CONFIG = {
|
|
|
39
47
|
shortUrls: false,
|
|
40
48
|
showStudioRoute: false,
|
|
41
49
|
showEphemeralPorts: false,
|
|
50
|
+
terminal: DEFAULT_TERMINAL_APPEARANCE,
|
|
51
|
+
restoreTerminals: "ask",
|
|
52
|
+
defaultEditor: null,
|
|
42
53
|
};
|
|
43
54
|
const MIN_SCAN_INTERVAL_MS = 1000;
|
|
44
55
|
/**
|
|
@@ -147,8 +158,80 @@ export function parseConfig(input) {
|
|
|
147
158
|
return err(`"nameTemplate" ${parsed.error}`);
|
|
148
159
|
config.nameTemplate = template;
|
|
149
160
|
}
|
|
161
|
+
if (raw.terminal !== undefined) {
|
|
162
|
+
const terminal = parseTerminalAppearance(raw.terminal);
|
|
163
|
+
if (!terminal.ok)
|
|
164
|
+
return terminal;
|
|
165
|
+
config.terminal = terminal.value;
|
|
166
|
+
}
|
|
167
|
+
if (raw.restoreTerminals !== undefined) {
|
|
168
|
+
const value = raw.restoreTerminals;
|
|
169
|
+
if (value !== "ask" && value !== "always" && value !== "never") {
|
|
170
|
+
return err(`"restoreTerminals" must be "ask", "always" or "never".`);
|
|
171
|
+
}
|
|
172
|
+
config.restoreTerminals = value;
|
|
173
|
+
}
|
|
174
|
+
if (raw.defaultEditor !== undefined) {
|
|
175
|
+
const value = raw.defaultEditor;
|
|
176
|
+
if (value !== null && (typeof value !== "string" || !APP_ID.test(value))) {
|
|
177
|
+
return err(`"defaultEditor" must be the id of an app in Open with, like "vscode", or null.`);
|
|
178
|
+
}
|
|
179
|
+
config.defaultEditor = value;
|
|
180
|
+
}
|
|
150
181
|
return ok(config);
|
|
151
182
|
}
|
|
183
|
+
/** The shape of open-with app ids: `vscode`, `vscode-insiders`, `custom-1`. */
|
|
184
|
+
const APP_ID = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
185
|
+
function parseTerminalAppearance(input) {
|
|
186
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
187
|
+
return err(`"terminal" must be an object.`);
|
|
188
|
+
}
|
|
189
|
+
const raw = input;
|
|
190
|
+
const terminal = { ...DEFAULT_TERMINAL_APPEARANCE };
|
|
191
|
+
if (raw.fontSize !== undefined) {
|
|
192
|
+
if (!isWholeBetween(raw.fontSize, 10, 24)) {
|
|
193
|
+
return err(`"terminal.fontSize" must be a whole number from 10 to 24.`);
|
|
194
|
+
}
|
|
195
|
+
terminal.fontSize = raw.fontSize;
|
|
196
|
+
}
|
|
197
|
+
if (raw.lineHeight !== undefined) {
|
|
198
|
+
const value = raw.lineHeight;
|
|
199
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 1 || value > 2) {
|
|
200
|
+
return err(`"terminal.lineHeight" must be a number from 1 to 2.`);
|
|
201
|
+
}
|
|
202
|
+
terminal.lineHeight = value;
|
|
203
|
+
}
|
|
204
|
+
if (raw.cursorStyle !== undefined) {
|
|
205
|
+
const value = raw.cursorStyle;
|
|
206
|
+
if (value !== "block" && value !== "bar" && value !== "underline") {
|
|
207
|
+
return err(`"terminal.cursorStyle" must be "block", "bar" or "underline".`);
|
|
208
|
+
}
|
|
209
|
+
terminal.cursorStyle = value;
|
|
210
|
+
}
|
|
211
|
+
if (raw.cursorBlink !== undefined) {
|
|
212
|
+
if (typeof raw.cursorBlink !== "boolean") {
|
|
213
|
+
return err(`"terminal.cursorBlink" must be true or false.`);
|
|
214
|
+
}
|
|
215
|
+
terminal.cursorBlink = raw.cursorBlink;
|
|
216
|
+
}
|
|
217
|
+
if (raw.scrollback !== undefined) {
|
|
218
|
+
if (!isWholeBetween(raw.scrollback, 1000, 100_000)) {
|
|
219
|
+
return err(`"terminal.scrollback" must be a whole number from 1000 to 100000.`);
|
|
220
|
+
}
|
|
221
|
+
terminal.scrollback = raw.scrollback;
|
|
222
|
+
}
|
|
223
|
+
if (raw.theme !== undefined) {
|
|
224
|
+
const value = raw.theme;
|
|
225
|
+
if (value !== "studio" && value !== "dark" && value !== "light") {
|
|
226
|
+
return err(`"terminal.theme" must be "studio", "dark" or "light".`);
|
|
227
|
+
}
|
|
228
|
+
terminal.theme = value;
|
|
229
|
+
}
|
|
230
|
+
return ok(terminal);
|
|
231
|
+
}
|
|
232
|
+
function isWholeBetween(value, min, max) {
|
|
233
|
+
return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max;
|
|
234
|
+
}
|
|
152
235
|
/** Empty names and values are dropped, so clearing a field in the panel removes its icon. */
|
|
153
236
|
function parseProjectIcons(value) {
|
|
154
237
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Studio keeps about terminals between runs, in `terminals.json` next to the config: the
|
|
3
|
+
* terminals that were running when it stopped, so they can run again, and the last commands run
|
|
4
|
+
* in each folder, offered when a new terminal opens there. See docs/terminals.md.
|
|
5
|
+
*/
|
|
6
|
+
export const EMPTY_TERMINAL_HISTORY = { running: [], recentCommands: {} };
|
|
7
|
+
/** Commands remembered per folder. */
|
|
8
|
+
export const MAX_RECENT_COMMANDS = 20;
|
|
9
|
+
/** Reads `terminals.json`; anything it cannot read is left out rather than failing. */
|
|
10
|
+
export function parseTerminalHistory(input) {
|
|
11
|
+
if (typeof input !== "object" || input === null)
|
|
12
|
+
return { running: [], recentCommands: {} };
|
|
13
|
+
const raw = input;
|
|
14
|
+
const running = Array.isArray(raw.running) ? raw.running.filter(isSavedTerminal) : [];
|
|
15
|
+
const recentCommands = {};
|
|
16
|
+
if (typeof raw.recentCommands === "object" && raw.recentCommands !== null) {
|
|
17
|
+
for (const [folder, list] of Object.entries(raw.recentCommands)) {
|
|
18
|
+
if (folder === "__proto__" || !Array.isArray(list))
|
|
19
|
+
continue;
|
|
20
|
+
const commands = list.filter((item) => typeof item === "string" && item.trim() !== "");
|
|
21
|
+
if (commands.length > 0)
|
|
22
|
+
recentCommands[folder] = commands.slice(0, MAX_RECENT_COMMANDS);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { running, recentCommands };
|
|
26
|
+
}
|
|
27
|
+
/** Puts `command` first in the folder's list, once, and keeps the newest `MAX_RECENT_COMMANDS`. */
|
|
28
|
+
export function rememberCommand(recent, cwd, command) {
|
|
29
|
+
const wanted = command.trim();
|
|
30
|
+
const copy = {};
|
|
31
|
+
for (const [folder, list] of Object.entries(recent))
|
|
32
|
+
copy[folder] = [...list];
|
|
33
|
+
if (wanted === "")
|
|
34
|
+
return copy;
|
|
35
|
+
const list = (copy[cwd] ?? []).filter((item) => item !== wanted);
|
|
36
|
+
copy[cwd] = [wanted, ...list].slice(0, MAX_RECENT_COMMANDS);
|
|
37
|
+
return copy;
|
|
38
|
+
}
|
|
39
|
+
function isSavedTerminal(value) {
|
|
40
|
+
if (typeof value !== "object" || value === null)
|
|
41
|
+
return false;
|
|
42
|
+
const item = value;
|
|
43
|
+
const optionalText = (field) => field === null || typeof field === "string";
|
|
44
|
+
return (typeof item.command === "string" &&
|
|
45
|
+
typeof item.cwd === "string" &&
|
|
46
|
+
item.cwd !== "" &&
|
|
47
|
+
typeof item.title === "string" &&
|
|
48
|
+
typeof item.startedAt === "number" &&
|
|
49
|
+
(item.startedBy === "program" || item.startedBy === "person") &&
|
|
50
|
+
optionalText(item.project) &&
|
|
51
|
+
optionalText(item.worktree) &&
|
|
52
|
+
optionalText(item.branch));
|
|
53
|
+
}
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A terminal Studio runs: one command in one project folder, with its output kept so the page,
|
|
3
|
-
* the CLI and agents can read it. See docs/terminals.md.
|
|
4
|
-
*/
|
|
5
1
|
// Control sequences: CSI (colors, cursor), OSC (window title) and two-character escapes.
|
|
6
2
|
const ESCAPES = new RegExp([
|
|
7
3
|
String.raw `\u001b\[[0-?]*[ -/]*[@-~]`,
|
|
@@ -143,5 +143,6 @@ function toDetected(port, projects, nameTemplate) {
|
|
|
143
143
|
cwd: port.cwd,
|
|
144
144
|
suggestedName: info === null ? null : suggestName(info, nameTemplate, port.port),
|
|
145
145
|
...(project === null ? {} : { project }),
|
|
146
|
+
...(port.startedAt === undefined ? {} : { startedAt: port.startedAt }),
|
|
146
147
|
};
|
|
147
148
|
}
|
|
@@ -5,7 +5,7 @@ import { appError } from "./app-error.js";
|
|
|
5
5
|
* Stops a dev server from its row. Only a process of the current user that listens on a port the
|
|
6
6
|
* UI shows can be stopped, so the endpoint cannot be used to kill anything else on the machine.
|
|
7
7
|
*/
|
|
8
|
-
export async function stopProcess(input, deps) {
|
|
8
|
+
export async function stopProcess(input, deps, all = false) {
|
|
9
9
|
if (!/^\d+$/.test(input) || Number(input) < 1) {
|
|
10
10
|
return err(appError("invalid_request", "The pid must be a whole number above 0."));
|
|
11
11
|
}
|
|
@@ -19,6 +19,47 @@ export async function stopProcess(input, deps) {
|
|
|
19
19
|
const who = owner === "root" ? "root" : owner === "other" ? "another user" : "an unknown user";
|
|
20
20
|
return err(appError("process_not_allowed", `Process ${pid} on port ${port} belongs to ${who}; stop it from a terminal.`, killCommand(deps.platform ?? process.platform, [pid], true)));
|
|
21
21
|
}
|
|
22
|
-
const
|
|
23
|
-
|
|
22
|
+
const targets = all ? await deps.processes.devTree?.(pid) : [pid];
|
|
23
|
+
if (!targets?.length) {
|
|
24
|
+
return err(appError("unsupported", "Could not identify the development command tree. Stop it from its terminal."));
|
|
25
|
+
}
|
|
26
|
+
// Check the entire plan before sending any signal. A tree must never include Studio.
|
|
27
|
+
for (const target of targets) {
|
|
28
|
+
if (target === deps.selfPid || (await deps.processes.ownerOf(target)) !== "you") {
|
|
29
|
+
return err(appError("process_not_allowed", "The development command tree includes a protected process. Stop it from its terminal."));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const watchedPorts = new Set([port]);
|
|
33
|
+
for (const listener of (await deps.listeners?.()) ?? []) {
|
|
34
|
+
if (listener.pid !== null && targets.includes(listener.pid))
|
|
35
|
+
watchedPorts.add(listener.port);
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
for (const target of targets) {
|
|
39
|
+
const stopped = await deps.processes.terminate(target);
|
|
40
|
+
if (!stopped.ok)
|
|
41
|
+
return stopped;
|
|
42
|
+
}
|
|
43
|
+
if (deps.listeners || deps.portClosed) {
|
|
44
|
+
// Observe a settling window too: a supervisor may immediately restart the listener.
|
|
45
|
+
await new Promise((resolve) => setTimeout(resolve, deps.settleMs ?? 750));
|
|
46
|
+
const listeners = (await deps.listeners?.()) ?? [];
|
|
47
|
+
for (const watchedPort of watchedPorts) {
|
|
48
|
+
const survivor = listeners.find((item) => item.port === watchedPort);
|
|
49
|
+
const closed = deps.portClosed ? await deps.portClosed(watchedPort) : !survivor;
|
|
50
|
+
if (!closed) {
|
|
51
|
+
const reason = !survivor
|
|
52
|
+
? "The port could not be confirmed closed"
|
|
53
|
+
: targets.includes(survivor.pid ?? -1)
|
|
54
|
+
? `Process ${survivor.pid} is still listening`
|
|
55
|
+
: `Process ${survivor.pid ?? "unknown"} is now listening; a parent or another program may have restarted the server`;
|
|
56
|
+
return err(appError("port_in_use", `Port ${watchedPort} did not stay closed. ${reason}. Try Stop all to stop the development command, or stop it from its terminal.`));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return ok({ pid, port });
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
await deps.refresh?.();
|
|
64
|
+
}
|
|
24
65
|
}
|
|
@@ -196,6 +196,15 @@ export class StudioRuntime {
|
|
|
196
196
|
* pid -> port for every process listening on a detected or routed port, from a fresh scan.
|
|
197
197
|
* These are the only processes the UI may stop.
|
|
198
198
|
*/
|
|
199
|
+
/** Refresh listeners and health immediately after a process action and broadcast the snapshot. */
|
|
200
|
+
async refreshProcesses() {
|
|
201
|
+
// A scan started before the stop may still contain the listener that just exited.
|
|
202
|
+
await this.#scanning;
|
|
203
|
+
const state = await this.#registry.readSnapshot();
|
|
204
|
+
await this.#detect(state, await this.#currentConfig(), true);
|
|
205
|
+
await this.#probeRoutes(state.routes);
|
|
206
|
+
this.#notify();
|
|
207
|
+
}
|
|
199
208
|
async stoppablePids() {
|
|
200
209
|
const state = await this.#registry.readSnapshot();
|
|
201
210
|
const found = await this.#detect(state, await this.#currentConfig(), true);
|
|
@@ -403,6 +412,7 @@ export class StudioRuntime {
|
|
|
403
412
|
return {
|
|
404
413
|
...route,
|
|
405
414
|
...(listener?.pid == null ? {} : { listenerPid: listener.pid }),
|
|
415
|
+
...(listener?.startedAt === undefined ? {} : { startedAt: listener.startedAt }),
|
|
406
416
|
...(project === null ? {} : { project }),
|
|
407
417
|
...(health === undefined ? {} : { health }),
|
|
408
418
|
};
|