feat(FN-2185): add session-based dev server API helpers
- Add canonical DevServer session types for commands, runtime state, logs, and preview metadata - Introduce session-scoped API helpers for list/create/get/start/stop/restart/delete operations under /api/devserver - Add log history, preview URL, command detection, and SSE stream URL helpers for session-based endpoints - Preserve migration compatibility by falling back to legacy /api/dev-server endpoints and normalizing legacy responses
This commit is contained in:
@@ -2338,6 +2338,499 @@ export function getDevServerLogsStreamUrl(projectId?: string): string {
|
|||||||
return buildApiUrl(withProjectId("/dev-server/logs/stream", projectId));
|
return buildApiUrl(withProjectId("/dev-server/logs/stream", projectId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Session-based DevServer API (FN-2184 / FN-2185)
|
||||||
|
// Target /api/devserver/* with fallback to /api/dev-server/* for migration safety
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonical session-based DevServer types.
|
||||||
|
* These align with the new session model introduced in FN-2184.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Detected dev server command (result of detectDevServerCommands)
|
||||||
|
export interface DetectedDevServerCommand {
|
||||||
|
name: string;
|
||||||
|
command: string;
|
||||||
|
cwd: string;
|
||||||
|
scriptName: string;
|
||||||
|
packagePath: string;
|
||||||
|
framework?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dev server log entry format
|
||||||
|
export interface DevServerLogEntry {
|
||||||
|
timestamp: string;
|
||||||
|
stream: "stdout" | "stderr";
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preview URL response from backend
|
||||||
|
export interface DevServerPreviewResponse {
|
||||||
|
url: string | null;
|
||||||
|
source: "auto" | "manual" | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dev server runtime info (process details)
|
||||||
|
export interface DevServerRuntime {
|
||||||
|
pid: number;
|
||||||
|
startedAt: string;
|
||||||
|
exitCode?: number;
|
||||||
|
previewUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dev server configuration (saved settings)
|
||||||
|
export interface DevServerSessionConfig {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
command: string;
|
||||||
|
cwd: string;
|
||||||
|
env?: Record<string, string>;
|
||||||
|
autoStart?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full DevServer session combining config, status, runtime, and logs
|
||||||
|
export interface DevServerSession {
|
||||||
|
config: DevServerSessionConfig;
|
||||||
|
status: "stopped" | "starting" | "running" | "failed" | "stopping";
|
||||||
|
runtime?: DevServerRuntime;
|
||||||
|
previewUrl?: string;
|
||||||
|
logHistory: DevServerLogEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options for fetching log history
|
||||||
|
export interface FetchDevServerLogsOptions {
|
||||||
|
maxLines?: number;
|
||||||
|
offset?: number;
|
||||||
|
lastEventId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backend response shape for log history
|
||||||
|
interface BackendSessionLogResponse {
|
||||||
|
lines?: DevServerLogEntry[];
|
||||||
|
totalLines?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backend response for preview endpoint
|
||||||
|
interface BackendPreviewResponse {
|
||||||
|
url?: string | null;
|
||||||
|
source?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backend response for list sessions
|
||||||
|
interface BackendSessionsListResponse {
|
||||||
|
sessions?: DevServerSession[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backend response for detect commands
|
||||||
|
interface BackendDetectCommandsResponse {
|
||||||
|
candidates?: DetectedDevServerCommand[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all dev server sessions.
|
||||||
|
* Targets /api/devserver with fallback to /api/dev-server (legacy compatibility).
|
||||||
|
*/
|
||||||
|
export async function fetchDevServers(projectId?: string): Promise<DevServerSession[]> {
|
||||||
|
try {
|
||||||
|
const response = await api<BackendSessionsListResponse>(withProjectId("/devserver", projectId));
|
||||||
|
return response.sessions ?? [];
|
||||||
|
} catch {
|
||||||
|
// Fallback: try to get the legacy single-server state and wrap it in session format
|
||||||
|
try {
|
||||||
|
const legacy = await fetchDevServerStatus(projectId);
|
||||||
|
// Convert legacy state to session format
|
||||||
|
const session: DevServerSession = {
|
||||||
|
config: {
|
||||||
|
id: legacy.id ?? "default",
|
||||||
|
name: legacy.name ?? "Dev Server",
|
||||||
|
command: legacy.command ?? "",
|
||||||
|
cwd: legacy.cwd ?? ".",
|
||||||
|
},
|
||||||
|
status: legacy.status,
|
||||||
|
runtime: legacy.pid
|
||||||
|
? {
|
||||||
|
pid: legacy.pid,
|
||||||
|
startedAt: legacy.startedAt ?? new Date().toISOString(),
|
||||||
|
exitCode: legacy.exitCode ?? undefined,
|
||||||
|
previewUrl: legacy.previewUrl,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
previewUrl: legacy.previewUrl ?? legacy.detectedUrl ?? undefined,
|
||||||
|
logHistory: (legacy.logs ?? []).map<DevServerLogEntry>((text, i) => ({
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
stream: text.startsWith("[stderr]") ? "stderr" : "stdout",
|
||||||
|
text: text.replace(/^\[stderr\]\s*/, ""),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
return [session];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new dev server session.
|
||||||
|
* Targets /api/devserver with fallback to /api/dev-server/start (legacy compatibility).
|
||||||
|
*/
|
||||||
|
export async function createDevServer(
|
||||||
|
data: { command: string; cwd?: string; name?: string; env?: Record<string, string> },
|
||||||
|
projectId?: string,
|
||||||
|
): Promise<DevServerSession> {
|
||||||
|
const body = {
|
||||||
|
command: data.command,
|
||||||
|
cwd: data.cwd ?? ".",
|
||||||
|
name: data.name,
|
||||||
|
env: data.env,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await api<DevServerSession>(withProjectId("/devserver", projectId), {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Fallback: use legacy start endpoint
|
||||||
|
const legacy = await startDevServer({ command: data.command, cwd: data.cwd }, projectId);
|
||||||
|
return {
|
||||||
|
config: {
|
||||||
|
id: legacy.id ?? "default",
|
||||||
|
name: legacy.name ?? data.name ?? "Dev Server",
|
||||||
|
command: legacy.command,
|
||||||
|
cwd: legacy.cwd ?? data.cwd ?? ".",
|
||||||
|
},
|
||||||
|
status: legacy.status,
|
||||||
|
runtime: legacy.pid
|
||||||
|
? {
|
||||||
|
pid: legacy.pid,
|
||||||
|
startedAt: legacy.startedAt ?? new Date().toISOString(),
|
||||||
|
exitCode: legacy.exitCode ?? undefined,
|
||||||
|
previewUrl: legacy.previewUrl,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
previewUrl: legacy.previewUrl ?? legacy.detectedUrl ?? undefined,
|
||||||
|
logHistory: (legacy.logs ?? []).map<DevServerLogEntry>((text) => ({
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
stream: text.startsWith("[stderr]") ? "stderr" : "stdout",
|
||||||
|
text: text.replace(/^\[stderr\]\s*/, ""),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a specific dev server session by ID.
|
||||||
|
* Targets /api/devserver/:id with fallback to /api/dev-server/status (legacy compatibility).
|
||||||
|
*/
|
||||||
|
export async function fetchDevServer(id: string, projectId?: string): Promise<DevServerSession | null> {
|
||||||
|
try {
|
||||||
|
return await api<DevServerSession>(withProjectId(`/devserver/${encodeURIComponent(id)}`, projectId));
|
||||||
|
} catch {
|
||||||
|
// Fallback: try legacy status endpoint (single-server model)
|
||||||
|
try {
|
||||||
|
const legacy = await fetchDevServerStatus(projectId);
|
||||||
|
// If no ID or ID matches default, return legacy state as session
|
||||||
|
if (!id || id === "default" || id === legacy.id) {
|
||||||
|
return {
|
||||||
|
config: {
|
||||||
|
id: legacy.id ?? "default",
|
||||||
|
name: legacy.name ?? "Dev Server",
|
||||||
|
command: legacy.command ?? "",
|
||||||
|
cwd: legacy.cwd ?? ".",
|
||||||
|
},
|
||||||
|
status: legacy.status,
|
||||||
|
runtime: legacy.pid
|
||||||
|
? {
|
||||||
|
pid: legacy.pid,
|
||||||
|
startedAt: legacy.startedAt ?? new Date().toISOString(),
|
||||||
|
exitCode: legacy.exitCode ?? undefined,
|
||||||
|
previewUrl: legacy.previewUrl,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
previewUrl: legacy.previewUrl ?? legacy.detectedUrl ?? undefined,
|
||||||
|
logHistory: (legacy.logs ?? []).map<DevServerLogEntry>((text) => ({
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
stream: text.startsWith("[stderr]") ? "stderr" : "stdout",
|
||||||
|
text: text.replace(/^\[stderr\]\s*/, ""),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a specific dev server by ID.
|
||||||
|
* Targets /api/devserver/:id/start with fallback to /api/dev-server/start (legacy compatibility).
|
||||||
|
*/
|
||||||
|
export async function startDevServerById(id: string, projectId?: string): Promise<DevServerSession> {
|
||||||
|
try {
|
||||||
|
return await api<DevServerSession>(withProjectId(`/devserver/${encodeURIComponent(id)}/start`, projectId), {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Fallback: use legacy start endpoint (single-server model)
|
||||||
|
const legacy = await startDevServer({ command: "" }, projectId);
|
||||||
|
return {
|
||||||
|
config: {
|
||||||
|
id: legacy.id ?? id,
|
||||||
|
name: legacy.name ?? "Dev Server",
|
||||||
|
command: legacy.command ?? "",
|
||||||
|
cwd: legacy.cwd ?? ".",
|
||||||
|
},
|
||||||
|
status: legacy.status,
|
||||||
|
runtime: legacy.pid
|
||||||
|
? {
|
||||||
|
pid: legacy.pid,
|
||||||
|
startedAt: legacy.startedAt ?? new Date().toISOString(),
|
||||||
|
exitCode: legacy.exitCode ?? undefined,
|
||||||
|
previewUrl: legacy.previewUrl,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
previewUrl: legacy.previewUrl ?? legacy.detectedUrl ?? undefined,
|
||||||
|
logHistory: (legacy.logs ?? []).map<DevServerLogEntry>((text) => ({
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
stream: text.startsWith("[stderr]") ? "stderr" : "stdout",
|
||||||
|
text: text.replace(/^\[stderr\]\s*/, ""),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop a specific dev server by ID.
|
||||||
|
* Targets /api/devserver/:id/stop with fallback to /api/dev-server/stop (legacy compatibility).
|
||||||
|
*/
|
||||||
|
export async function stopDevServerById(id: string, projectId?: string): Promise<DevServerSession> {
|
||||||
|
try {
|
||||||
|
return await api<DevServerSession>(withProjectId(`/devserver/${encodeURIComponent(id)}/stop`, projectId), {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Fallback: use legacy stop endpoint
|
||||||
|
const legacy = await stopDevServer(projectId);
|
||||||
|
return {
|
||||||
|
config: {
|
||||||
|
id: legacy.id ?? id,
|
||||||
|
name: legacy.name ?? "Dev Server",
|
||||||
|
command: legacy.command ?? "",
|
||||||
|
cwd: legacy.cwd ?? ".",
|
||||||
|
},
|
||||||
|
status: legacy.status,
|
||||||
|
runtime: legacy.pid
|
||||||
|
? {
|
||||||
|
pid: legacy.pid,
|
||||||
|
startedAt: legacy.startedAt ?? new Date().toISOString(),
|
||||||
|
exitCode: legacy.exitCode ?? undefined,
|
||||||
|
previewUrl: legacy.previewUrl,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
previewUrl: legacy.previewUrl ?? legacy.detectedUrl ?? undefined,
|
||||||
|
logHistory: (legacy.logs ?? []).map<DevServerLogEntry>((text) => ({
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
stream: text.startsWith("[stderr]") ? "stderr" : "stdout",
|
||||||
|
text: text.replace(/^\[stderr\]\s*/, ""),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restart a specific dev server by ID.
|
||||||
|
* Targets /api/devserver/:id/restart with fallback to /api/dev-server/restart (legacy compatibility).
|
||||||
|
*/
|
||||||
|
export async function restartDevServerById(id: string, projectId?: string): Promise<DevServerSession> {
|
||||||
|
try {
|
||||||
|
return await api<DevServerSession>(withProjectId(`/devserver/${encodeURIComponent(id)}/restart`, projectId), {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Fallback: use legacy restart endpoint
|
||||||
|
const legacy = await restartDevServer(projectId);
|
||||||
|
return {
|
||||||
|
config: {
|
||||||
|
id: legacy.id ?? id,
|
||||||
|
name: legacy.name ?? "Dev Server",
|
||||||
|
command: legacy.command ?? "",
|
||||||
|
cwd: legacy.cwd ?? ".",
|
||||||
|
},
|
||||||
|
status: legacy.status,
|
||||||
|
runtime: legacy.pid
|
||||||
|
? {
|
||||||
|
pid: legacy.pid,
|
||||||
|
startedAt: legacy.startedAt ?? new Date().toISOString(),
|
||||||
|
exitCode: legacy.exitCode ?? undefined,
|
||||||
|
previewUrl: legacy.previewUrl,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
previewUrl: legacy.previewUrl ?? legacy.detectedUrl ?? undefined,
|
||||||
|
logHistory: (legacy.logs ?? []).map<DevServerLogEntry>((text) => ({
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
stream: text.startsWith("[stderr]") ? "stderr" : "stdout",
|
||||||
|
text: text.replace(/^\[stderr\]\s*/, ""),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a specific dev server by ID.
|
||||||
|
* Targets /api/devserver/:id with fallback (no legacy equivalent).
|
||||||
|
*/
|
||||||
|
export async function deleteDevServer(id: string, projectId?: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await api<void>(withProjectId(`/devserver/${encodeURIComponent(id)}`, projectId), {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// No fallback for delete in legacy API (single-server model)
|
||||||
|
// Silently ignore - deletion may not be supported in legacy mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch logs for a specific dev server by ID.
|
||||||
|
* Targets /api/devserver/:id/logs with fallback to /api/dev-server/logs/history (legacy compatibility).
|
||||||
|
*/
|
||||||
|
export async function fetchDevServerLogs(
|
||||||
|
id: string,
|
||||||
|
opts: FetchDevServerLogsOptions = {},
|
||||||
|
projectId?: string,
|
||||||
|
): Promise<{ lines: DevServerLogEntry[]; totalLines: number }> {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
if (typeof opts.maxLines === "number" && Number.isFinite(opts.maxLines)) {
|
||||||
|
query.set("maxLines", String(Math.max(1, Math.floor(opts.maxLines))));
|
||||||
|
}
|
||||||
|
if (typeof opts.offset === "number" && Number.isFinite(opts.offset)) {
|
||||||
|
query.set("offset", String(Math.max(0, Math.floor(opts.offset))));
|
||||||
|
}
|
||||||
|
if (typeof opts.lastEventId === "number" && Number.isFinite(opts.lastEventId)) {
|
||||||
|
query.set("lastEventId", String(Math.max(0, Math.floor(opts.lastEventId))));
|
||||||
|
}
|
||||||
|
const suffix = query.size > 0 ? `?${query.toString()}` : "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api<BackendSessionLogResponse>(
|
||||||
|
withProjectId(`/devserver/${encodeURIComponent(id)}/logs${suffix}`, projectId),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
lines: response.lines ?? [],
|
||||||
|
totalLines: response.totalLines ?? response.lines?.length ?? 0,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
// Fallback: use legacy log history endpoint
|
||||||
|
try {
|
||||||
|
const response = await fetchDevServerLogHistory(opts, projectId);
|
||||||
|
return {
|
||||||
|
lines: response.lines.map<DevServerLogEntry>((entry) => ({
|
||||||
|
timestamp: entry.timestamp,
|
||||||
|
stream: entry.stream,
|
||||||
|
text: entry.text,
|
||||||
|
})),
|
||||||
|
totalLines: response.totalLines,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { lines: [], totalLines: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch preview URL for a specific dev server by ID.
|
||||||
|
* Targets /api/devserver/:id/preview with fallback to /api/dev-server/status (legacy compatibility).
|
||||||
|
*/
|
||||||
|
export async function fetchDevServerPreview(id: string, projectId?: string): Promise<DevServerPreviewResponse> {
|
||||||
|
try {
|
||||||
|
const response = await api<BackendPreviewResponse>(
|
||||||
|
withProjectId(`/devserver/${encodeURIComponent(id)}/preview`, projectId),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
url: response.url ?? null,
|
||||||
|
source: (response.source as DevServerPreviewResponse["source"]) ?? null,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
// Fallback: use legacy status endpoint
|
||||||
|
try {
|
||||||
|
const legacy = await fetchDevServerStatus(projectId);
|
||||||
|
return {
|
||||||
|
url: legacy.previewUrl ?? legacy.detectedUrl ?? legacy.manualUrl ?? null,
|
||||||
|
source: legacy.manualUrl ? "manual" : "auto",
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { url: null, source: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set preview URL for a specific dev server by ID.
|
||||||
|
* Targets /api/devserver/:id/preview with fallback to /api/dev-server/preview-url (legacy compatibility).
|
||||||
|
*/
|
||||||
|
export async function setDevServerPreviewUrlById(
|
||||||
|
id: string,
|
||||||
|
url: string | null,
|
||||||
|
projectId?: string,
|
||||||
|
): Promise<DevServerPreviewResponse> {
|
||||||
|
try {
|
||||||
|
const response = await api<BackendPreviewResponse>(
|
||||||
|
withProjectId(`/devserver/${encodeURIComponent(id)}/preview`, projectId),
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ url }),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
url: response.url ?? null,
|
||||||
|
source: (response.source as DevServerPreviewResponse["source"]) ?? null,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
// Fallback: use legacy preview URL endpoint
|
||||||
|
const legacy = await setDevServerPreviewUrl({ url }, projectId);
|
||||||
|
return {
|
||||||
|
url: legacy.previewUrl ?? legacy.manualUrl ?? null,
|
||||||
|
source: "manual",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect available dev server commands.
|
||||||
|
* Targets /api/devserver/detect with fallback to /api/dev-server/detect (legacy compatibility).
|
||||||
|
*/
|
||||||
|
export async function detectDevServerCommands(projectId?: string): Promise<DetectedDevServerCommand[]> {
|
||||||
|
try {
|
||||||
|
const response = await api<BackendDetectCommandsResponse>(withProjectId("/devserver/detect", projectId));
|
||||||
|
return response.candidates ?? [];
|
||||||
|
} catch {
|
||||||
|
// Fallback: use legacy detect endpoint
|
||||||
|
try {
|
||||||
|
const legacy = await fetchDevServerCandidates(projectId);
|
||||||
|
return legacy.map<DetectedDevServerCommand>((candidate) => ({
|
||||||
|
name: candidate.name,
|
||||||
|
command: candidate.command,
|
||||||
|
cwd: candidate.cwd,
|
||||||
|
scriptName: candidate.scriptName,
|
||||||
|
packagePath: candidate.packagePath,
|
||||||
|
}));
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the SSE stream URL for a specific dev server session's logs.
|
||||||
|
* Targets /api/devserver/:id/logs/stream with fallback to /api/dev-server/logs/stream (legacy compatibility).
|
||||||
|
*/
|
||||||
|
export function getDevServerSessionLogsStreamUrl(id: string, projectId?: string): string {
|
||||||
|
// Try new session-scoped endpoint first
|
||||||
|
return buildApiUrl(withProjectId(`/devserver/${encodeURIComponent(id)}/logs/stream`, projectId));
|
||||||
|
}
|
||||||
|
|
||||||
function startKeepAlive(
|
function startKeepAlive(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
projectId?: string,
|
projectId?: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user